Files
proxy/CHANGELOG.md
wmantly 255835af7a
Pull Request Tests / Run Tests (18.x) (push) Successful in 49s
Pull Request Tests / Run Tests (20.x) (push) Successful in 40s
Pull Request Tests / Run Tests (22.x) (push) Successful in 41s
Pull Request Tests / Test Summary (push) Successful in 4s
feat: edit permission entries (v1.35.0)
The Permissions page only offered Delete, so changing a role or scope
meant removing the grant and re-adding it from memory.

A permission's id is derived from (subjectType, subject, scope, domain),
so changing any of those is a different record rather than an update. The
new PUT creates the new grant and removes the superseded one in that
order, so an edit can never leave the old grant behind still conferring
access.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 10:34:11 -04:00

28 KiB

v1.35.0

  • feat: permission entries can be edited. The Permissions page only offered Delete, so changing a role or scope meant removing the grant and re-adding it from memory. New PUT /api/permission/:id plus an Edit modal pre-filled from the record.
  • fix: a permission's id is derived from (subjectType, subject, scope, domain), so changing any of those is a different record, not an update. The endpoint creates the new grant and removes the superseded one in that order, so an edit can never leave the old grant behind still conferring access.

v1.34.0

  • feat: the per-host SSO Allowed groups field now autocompletes from the SSO directory's groups. Suggestions previously came only from local groups, permission subjects and conf.auth maps — none of which can match an SSO-gated host, because its allow-list is checked against the groups claim the SSO issues. New conf.sso block (url + read-only apiToken, minted by theta-suite's bootstrap); results are cached for 5 minutes and the endpoint degrades silently to the old local-only list when unset.
  • fix: the SSO group lookup authenticates with Authorization: Bearer <token>, not the auth-token header — the latter is for browser session UUIDs and would be rejected for a minted API token.
  • docs: docs/concepts-hosts.md gains a "Putting a host behind single sign-on" section covering the per-host /__proxy_auth flow, the wildcard redirect URI the IdP must allow, and where group suggestions come from.
  • docs: secrets.js.example documents the new sso block.

v1.33.0

  • feat: Add SSO-style error page (404/500) for browser navigation instead of a bare JSON/text response
  • feat: DNS page is now admin-only (hidden from non-admins; API already admin-gated)
  • feat: navbar — username no longer underlined; only the active link is bold + underlined

v1.13.3

  • fix: remove missing DEPLOYMENT.md and docs/ from Docker build context

v1.13.2

  • chore: Update CI pipeline integration

Changelog

All notable changes to this project are documented here. Format loosely follows Keep a Changelog; versions correspond to git tags (vX.Y.Z) and nodejs/package.json's version.

Unreleased

[1.13.1] - 2026-08-01

Fixed

  • Bumped @simpleworkjs/bao-conf to 1.0.1 so standalone/no-OpenBao boots don't crash. bao-conf 1.0.0's init() threw when VAULT_TOKEN was unset, which — combined with bin/www's .catch(() => process.exit(1)) — made the proxy exit at boot in any deployment without an OpenBao sidecar (standalone Docker, bare metal). 1.0.1 makes init() fail-soft on a missing token (warn
    • continue from CONF_SECRETS), matching the documented contract. The theta-env stack is unaffected (it always sets a scoped VAULT_TOKEN).

[1.13.0] - 2026-08-01

Changed

  • Secrets now load from OpenBao at boot via @simpleworkjs/bao-conf, which deep-merges secret/proxy/conf over the file-loaded config. The proxy authenticates to OpenBao with a scoped VAULT_TOKEN (policy proxy — read-only on its own path), never the root token. Because the OIDC clientSecret is captured at require time inside createOidcClient (during require('../models'), which require('../app') triggers transitively), bin/www now defers require('../app') until after bao-conf.init() resolves. Fail-soft: if OpenBao is unreachable, boot continues from CONF_SECRETS. The config/proxy-secrets.js file is now an operator-edit seed artifact (gitignored); OpenBao is authoritative. See theta-env's Secrets docs.
  • Bumped package version to track the release tag.

[1.12.1] - 2026-08-01

Changed

  • Bumped body-parser 2.2.2 → 2.3.0 (Dependabot #175).
  • Bumped ejs and brace-expansion (Dependabot #179, security maintenance).

[1.12.0] - 2026-08-01

Fixed

  • Changed UNIX socket permission in unix_socket_json.js to 666 so OpenResty Nginx workers running as nobody can resolve targets properly.

[1.9.0] - 2026-07-30

Added

  • app_super_admin cross-app group recognized as a global admin (conf.auth.adminGroups), same group also recognized by sso-manager-node and jump-host, and by ldap-client's SSSD access filter (SSH login on every host).

Changed

  • Users and Permissions pages: the always-visible sidebar "Add" forms are now an "Add User"/"Add Permission" button in the list header that opens an app.modal dialog, matching the hosts.ejs convention.
  • Let's Encrypt ACME account key now defaults to the already-persisted /data volume instead of a CWD-relative path (./le_key.cert -> /app/le_key.cert in the container), which was lost on every image rebuild.

[1.8.0] - 2026-07-28

Added

  • Users backed by SSO/OIDC login are now marked "External (SSO)" and read-only: their password-change control is hidden, and PUT /password/:username rejects with 403 server-side. Deletion stays allowed. Redis user-backend only.
  • app.util.revealItem() (shared app-base.js): scrolls a just-added/-edited element into view and flashes its background. Wired into the Users/Permissions create flows.

Changed

  • All pages now wrap their content in a standard-width container, matching sso-manager-node instead of rendering full-bleed.
  • Users and Permissions pages converted from bare <table>s to the card-grid convention already used on the Groups page.
  • @simpleworkjs/frontend bumped to ^0.2.7.

[1.7.0] - 2026-07-28

Added

  • API tokens: "+ New Token" modal button (replacing the always-visible inline create-form card) and a new Edit modal — continues the cross-app API-token UI unification started in jump-host. The Edit modal's footer shows real created-by/on data; the PUT /api-token/:id route already fully supported editing, so no backend change was needed.

Fixed

  • Creating an API token didn't show the "save this secret now" reveal modal — the create flow called app.modal.close() immediately before app.modal.open() (to show the secret) in the same tick; since app.modal is a singleton, that collided with Bootstrap's hide-transition guard and the reveal modal silently never appeared.

[1.6.0] - 2026-07-28

Added

  • Host modal: footer with created/updated-by/on metadata, linkable /hosts/{host} URL — migrated onto the shared app.modal component (@simpleworkjs/frontend 0.2.6), continuing the entity-modal standardization started in sso-manager-node. Kept the existing 6-tab layout (General/TLS & Wildcard/Traffic/Headers/Access/Authentication) unchanged.

Fixed

  • The Let's-Encrypt challenge-type/wildcard-matching visibility logic could stop reacting to the hostname field after the first Add/Edit — its keyup handler was bound once directly against a selector captured at page load; now delegated so it survives the modal being rebuilt on every open.
  • The SSO allow-list autocomplete (users/groups) could go empty starting on the second Add/Edit — its backing <datalist>s are rebuilt fresh on every modal open now, so the suggestions are refetched each time too.

[1.5.3] - 2026-07-28

Fixed

  • formAJAX's loading indicator showed literal HTML, not a spinner — same fix as sso-manager-node's companion release. Replaced with plain text.

[1.5.2] - 2026-07-28

Added

  • Regression test: a static check across all views/client-side scripts fails CI if any native alert()/confirm()/prompt() call appears — these block all further browser events on the page and were fully removed in 1.5.1.

[1.5.1] - 2026-07-27

Fixed

  • Removed native confirm() calls in revokeToken/rotateToken (profile.ejs), replacing them with app.messages.confirm. Native confirm() blocks all further browser events on the page (discovered live, mid browser-verification of sso-manager-node's equivalent flow, where it froze the tab).

[1.5.0] - 2026-07-27

Changed

  • Adopted @simpleworkjs/frontend's app.messages, app.modal, and app.validate modules, replacing the vendored app.util.actionMessage/actionConfirm in public/lib/js/app-base.js and the vendored public/lib/js/val.js. Message content is now HTML-escaped, and app.messages.action falls back to a page-wide toast when there's no inline .actionMessage target. proxy's host/target/hostname wildcard-DNS validation rules (mirroring utils/hostname_validate.js) moved to public/js/app.js, registered via $.validateSettings, since they're proxy-specific and don't belong in the shared package's generic rule set. app.api/app.auth/app.pubsub/app.socket are untouched.

[1.4.0] - 2026-07-26

Changed

  • Unified the front-end UI shell across the three theta42 apps. views/top.ejs, views/bottom.ejs and public/lib/js/app-base.js are now byte-identical in sso-manager-node, proxy and jump-host, so the apps look and behave the same and a shell change lands in one edit per repo instead of three divergent ones. Everything that differs between the apps moved into a new nodejs/utils/ui.js, exposed to every render as ui via app.locals: nav items and the groups that may see them, footer repo/license/docs/Terms links, favicon, the profile and post-logout targets, and whether the update banner exists at all.
  • One nav-gating model everywhere. app-base.js reveals .group-required-<cn> elements for each group the current user is in, read from GET /api/user/me. sso-manager-node reports LDAP DNs in memberOf and the OIDC clients report CNs in groups; both normalise to CNs client-side, and the clients' effective-rights isAdmin flag is exposed as a synthetic admin group — so one gating model covers a group-based provider and boolean-admin clients without either app learning the other's response shape.
  • GET /api/user/me is fetched once per page load and cached (app.auth.loadUser). The nav, per-view forceLogin and every group-gated element read that one promise instead of issuing their own request.
  • app.auth.isLoggedIn is dual-mode: it returns a Promise and invokes an optional node-style callback, so the async and callback call styles both work against one shared top.ejs.
  • app.auth.forceLogin no longer uses $.holdReady (removed in jQuery 4). An unauthenticated user is redirected to /login?redirect=<path>; group requirements are still enforced, and logOut now only clears the session, leaving the destination to the caller (ui.logoutRedirect).
  • Dependency alignment across all three apps: jquery ^4.0.0 and ejs ^3.1.10.

Fixed

  • app.api.delete dropped its callback when called by formAJAX. formAJAX always passes the serialized form as the second argument, so a DELETE-method form's callback landed in the data slot and never ran. delete now accepts both (url, callback) and (url, data, callback).
  • app.api.post/put referenced an undefined callback2 and threw when handed a non-function callback. Both are now dual-mode Promise/callback.
  • The login page's "reveal the card once we know you're logged out" branch threw (Cannot read properties of null) whenever the logged-in check answered before the parser reached that element — which it always did without a stored token. It now runs on DOM ready.
  • logInRedirect on the legacy /login/<path> form kept only the path. The OIDC provider routes an unauthenticated authorization request through /login/oauth/authorize?client_id=…&state=…; dropping the query there loses the entire authorization request. The suffix form now preserves its query string.

Added

  • .group-required { display: none } in public/css/styles.css, the base rule the shared gating model reveals against.
  • Admin-only nav items lost their inline display: none in favour of that class, and the brand link points at / instead of #.

Verified

  • Browser-verified against a full theta-env stack (sso-manager + proxy + jump-host): every top-level page renders with a clean console; nav gating is correct for admin and non-admin; forceLogin's onboarding and group gates fire; val.js blocks a weak password and accepts a strong one through a real form submit; the DELETE-method forms work; and the OIDC login round trip (authorize with PKCE -> login -> consent -> callback -> token fragment) completes on both OIDC clients.

[1.3.0] - 2026-07-25

Added

  • Adopted the shared @simpleworkjs/* packages published under the simpleworkjs org, replacing this app's byte-identical forks of the same code so the theta42 apps share one codebase and API schema:
    • @simpleworkjs/oidc-client — the OIDC client (session models, auth router, OIDC utils, safe-redirect, local-admin bootstrap). Deleted the local utils/oidc.js, utils/safe_redirect.js, models/oidc_state.js, models/token.js, models/auth.js, and routes/auth.js; models/index.js now wires the factory. The per-host SSO in routes/host_auth.js is unchanged but consumes the shared OIDC utils.
    • @simpleworkjs/ldap — the ldapts client + RFC 4515/4514 escaping.
    • @simpleworkjs/app-stack — unified build_info ({buildVersion, buildHash, buildYear}) and the static-modules mounting helper. utils/build_info.js and the static-modules loop in routes/render.js now use the shared helpers.

Security

  • LDAP filter injection in User.get. The user lookup built its search filter by interpolating data.username raw into (&(objectClass=inetOrgPerson)(uid=<username>)). A username containing *, (, ), \, or NUL could widen or alter the filter (e.g. * → match-all). The filter value is now passed through escapeFilter from @simpleworkjs/ldap (RFC 4515 escaping).

Changed

  • Dependency alignment: model-redis ^1.5^1.6.0, ldapts ^8.1.2^8.1.8. The four new @simpleworkjs/* deps resolve from the npm registry (^1.0.0); no file:/link: entries in the lockfile, so npm ci is clean in docker builds.
  • build_info export shape changed from {commit, version} to {buildVersion, buildHash, buildYear} (the shared shape used by all three apps). The /health endpoint and footer now report buildVersion/buildHash.

[1.2.2] - 2026-07-21

Fixed

  • Multi-target load balancing (added in 1.2.0) crashed every request to a load-balanced host: ops/nginx_conf/targetinfo.lua required a nonexistent resty.balancer.round_robin module. The lua-resty-balancer rock installed by the Dockerfile/install.sh doesn't provide that path — it provides resty.roundrobin (constructed as roundrobin:new(nodes), not :new() + :reinit(nodes)). Fixed targetinfo.lua to use the real module and API; verified end-to-end that requests now round-robin across targets with no Lua errors.

[1.2.1] - 2026-07-21

Fixed

  • The bootstrap anti-lockout admin account was always created as proxyadmin2 regardless of conf.auth.adminUsers, while migrations/permission_bootstrap.js grants the global-admin permission to conf.auth.adminUsers[0]. If an operator customized adminUsers away from the default, the bootstrapped account and the permissioned account were two different (non-matching) usernames, so the anti-lockout account ended up with no admin access. models/user_redis.js now derives the bootstrap username from conf.auth.adminUsers[0] (falling back to proxyadmin2), matching permission_bootstrap.js.
  • Corrected a secrets.js.example comment that claimed the bootstrap admin's password "defaults to the username itself" — it actually generates a random password printed to the container log on first boot.

Changed

  • Refreshed all README screenshots (hosts, per-host SSO auth, per-host basic auth) against the current UI, and added a new load-balancing screenshot for the multi-target feature.

[1.2.0] - 2026-07-21

Added

  • Multi-target load balancing: hosts can now specify additional backend targets (IP:port, one per line) alongside the primary target; the proxy distributes requests across all of them round-robin via lua-resty-balancer. Fixes #47.

1.1.17 - 2026-07-20

Fixed

  • An existing single-label subdomain host (e.g. sso.nl.wgnode.com) could not be attached to a wildcard cert added later (e.g. *.nl.wgnode.com): Host.lookUpWildcardParent() only checked the wildcard-as-child position (the wildcard's own base domain) and missed the far more common wildcard-as-sibling case, so the edit form's "Parent Wildcard" option stayed permanently greyed out. It now checks both positions, and a regression test covers the sibling case.

1.1.16 - 2026-07-18

Changed

  • Public-release packaging: removed "private": true from nodejs/package.json, corrected the repository URL to https://github.com/theta42/proxy.git, and fixed the MIT LICENSE copyright line.
  • Genericized committed defaults in conf/base.js and conf/development.js: LDAP now defaults to ldap://localhost with dc=example,dc=com, and OIDC endpoints default to https://sso.example.com instead of internal theta42 infrastructure.
  • The bootstrap proxyadmin2 account now gets a random, one-time password when auth.localAdminPass is unset, instead of the well-known default proxyadmin2. The password is printed to the log on first creation and can be made deterministic by setting auth.localAdminPass in the secrets file.

Security

  • Sanitized rendered docs HTML via xss in routes/docs.js so malicious markdown cannot inject scripts or other dangerous markup into the in-app docs viewer.
  • The Unix socket JSON-RPC socket is now created with mode 660 instead of world-writable 777.

Fixed

  • The global error handler no longer leaks err.keys, stack traces, or other internal details in JSON responses; only name and message are returned to clients.
  • DEPLOYMENT.md and docs/docker.md now correctly describe the CONF_SECRETS env-var mechanism instead of the old symlink behavior.

1.1.15 - 2026-07-18

Changed

  • ops/install.sh now installs to /opt/theta42/proxy (was /var/www/proxy) and seeds /etc/proxy/secrets.js from secrets.js.example on first run (never overwritten on later runs), instead of requiring a manual nodejs/conf/secrets.js edit inside the repo checkout. ops/proxy.service sets CONF_SECRETS=/etc/proxy/secrets.js to match.
  • install.sh now prints the version it's updating from/to (or "Already up to date") on every run, instead of updating silently.

Fixed

  • install.sh could hang indefinitely on a fresh host if a base package pulled in tzdata as a new dependency — it prompted interactively for a timezone with no TTY attached. Set DEBIAN_FRONTEND=noninteractive.

1.1.14 - 2026-07-17

Changed

  • Bumped @simpleworkjs/conf to 1.2.0 and jq-repeat to 2.2.0. The Docker entrypoint now sets the new CONF_SECRETS env var to point directly at a mounted proxy-secrets.js instead of symlinking it into /app/conf/secrets.js — the app no longer needs write access to its own conf/ directory to pick up mounted secrets.

1.1.13 - 2026-07-17

Added

  • Four new plain-language docs aimed at less technical readers, replacing the system-design-level Architecture/Installation docs as the target of most card help links: Hosts & HTTPS, DNS Providers, Users, Groups & Permissions, and API Tokens. Each links onward to the deeper technical reference for readers who want it; the technical docs link back the other way too. The personal-access-token card (previously missed entirely) now has a help link.

Fixed

  • The in-app docs viewer rendered every docs/*.md page with a garbled heading and a stray horizontal rule at the top — Jekyll front matter (meant only for the GitHub Pages build) was never stripped before being handed to the markdown renderer. Also fixed: cross-doc links never resolved in-app, since this viewer serves docs at /docs/<slug> with no .html suffix — they're now rewritten to the correct in-app URL (by registered slug, falling back to the doc's real filename), the same way image paths already were.

1.1.12 - 2026-07-17

Fixed

  • The host edit form's "Parent Wildcard" option stayed greyed out even when a valid wildcard actually existed for that host, so an already-created host could never be switched onto one from the edit modal (only brand-new hosts, via the field's keyup handler, ever saw it become available). The underlying /host/lookup/:item check also had the same self-match issue as the recently-fixed backend bug: it resolved an already-existing host to its own record instead of a sibling wildcard. Added a dedicated /host/wildcard-parent/:item endpoint that checks both directions, and the edit form now actually runs the check when it opens.
  • Fixed an nginx startup warning: the "listen ... http2" directive is deprecated, use the "http2" directive instead. Migrated to the standalone http2 on; directive (nginx 1.25.1+).

1.1.11 - 2026-07-17

Changed

  • Moved the help () link out of the global header and onto each relevant card individually (Proxy List, Add/Edit host, Add DNS Provider, Dynamic A Records, Add New User, User List, Add Permission, Permissions, Add Group) — each now deep-links straight to the doc that actually covers it, instead of one generic header icon.

1.1.10 - 2026-07-17

Added

  • A help icon () in the top-right header now deep-links to the doc most relevant to the current page (falls back to the docs index elsewhere).
  • The in-app docs viewer (/docs) is now searchable — a simple line-substring search over the same local doc set, no new dependency, still works with no internet access.

1.1.9 - 2026-07-17

Added

  • The host list now shows who created each host, and when.
  • Plain (non-wildcard) hosts can now be renamed after creation — the hostname field is no longer permanently locked. Wildcard hosts, wildcard children, and auto-created subdomain cache entries stay locked, since other records reference them by name.
  • More inline help text on the host create/edit form (Target SSL, wildcard matching behavior).

Fixed

  • The host create/edit modal's tabs could overflow awkwardly on narrow (mobile) screens — they now scroll horizontally instead.
  • Fixed a bug in the vendored model-redis library's record-rename path: renaming a record's primary key while another always-type field (e.g. updated_on) is defined earlier in the schema left a stray, incomplete hash behind under the old key, making that name permanently unavailable for reuse. Worked around in Host.prototype.update().

1.1.8 - 2026-07-17

Fixed

  • Couldn't attach an existing host to a parent wildcard. The host edit form's "Parent Wildcard" option submitted correctly, but Host.prototype.update() had no challengeType handling at all (only Host.create() did) — selecting it and saving silently did nothing. Added the same wildcard-parent lookup to update().
  • Couldn't register a wildcard's own base domain as a host. A wildcard cert's altNames already cover both the base domain and *.base domain, but the lookup tree stores the wildcard one level below its base domain, and a lookup for the bare base domain landed on that empty parent node and found nothing — even though the already-issued cert covers it. buildLookUpObj() now also stamps the parent node so this resolves correctly, without re-issuing or duplicating the cert.

Both required a corrected lookup: attaching an existing host (which already has its own tree leaf) needed a new Host.lookUpWildcardParent() that checks the sibling wildcard slot instead of resolving to the host's own record.

1.1.7 - 2026-07-16

Changed

  • Redesigned the GitHub Pages docs site to match the app's own look (dark navbar/footer, Bootstrap 5, Font Awesome) instead of the generic jekyll-theme-cayman theme, added a real cross-page nav, SEO (jekyll-seo-tag + jekyll-sitemap, per-page descriptions, OG/Twitter tags, sitemap.xml, robots.txt), and mobile-responsive layout.

1.1.6 - 2026-07-16

Fixed

  • Hosts admin UI's Authentication tab radios (Off / Basic / SSO) had no shared name, so clicking one didn't uncheck the others -- multiple options could appear selected at once. Added name="auth_mode" to restore standard exclusive radio-group behavior.

1.1.5 - 2026-07-16

Fixed

  • Bumped jq-repeat 2.0.1 -> 2.1.0. Fixed real breakage: users.ejs/groups.ejs/permissions.ejs called the removed $.scope.X.__setPut(fn)/__setTake(fn) setter-method API; insert/remove row hooks are now set via direct property assignment ($.scope.X.__put = fn), matching 2.1.0's API.

1.1.4 - 2026-07-16

Added

  • White-label: <title>, the navbar brand text, and the nav logo image were hardcoded "Proxy - Theta 42"/"Dynamic Proxy". Now driven by new conf.name/conf.logo keys (defaults unchanged). Footer attribution (copyright, theta42.com link, GitHub/license links) and favicon are left as-is. Closes #45.

1.1.3 - 2026-07-16

Added

  • CHANGELOG.md (this file), backfilled from the release notes for every tag so far and served in-app at /docs/changelog. Closes theta-env#43.

1.1.2 - 2026-07-16

Fixed

  • Air-gap: DynamicRecord.refreshAll() called the public-IP resolvers (api.ipify.org, icanhazip.com, ifconfig.me) every 4h on a timer regardless of whether any dynamic DNS records were configured — the one background network call in the repo not actually gated by feature use. Now skips the lookup entirely when there's nothing to refresh.
  • Removed the stray, unauthenticated GET /test page (a leftover jq-repeat demo) that loaded jQuery + Mustache from external CDNs.
  • Removed a dead IE<9-only html5shim script tag pointing at a domain that no longer resolves.

Added

  • In-app documentation: GET /docs and GET /docs/:slug render this project's own README, DEPLOYMENT, api.md, and docs/*.md server-side — readable from the running app with no dependency on GitHub Pages, which requires internet access to view. Public, no auth, rate-limited.

1.1.1 - 2026-07-16

Fixed

  • DuckDNS provider: adding a DuckDNS provider no longer pushes this host's public IP to the domain's live A/AAAA record as a side effect of token validation. Validation now writes a fixed marker to the TXT record instead, leaving routing untouched. (#142)

1.1.0 - 2026-07-16

First tagged release. Establishes the vX.Y.Z tag convention that the in-app update-check banner polls against going forward.

Added

  • Standalone backup script (ops/backup.sh) for deployments not using theta-env's orchestrator — snapshots Redis and ./config, with retention.
  • Admin-only in-app banner that checks GitHub releases every 24h and surfaces available updates.