# Changelog All notable changes to this project are documented here. Format loosely follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versions correspond to git tags (`vX.Y.Z`) and `nodejs/package.json`'s `version`. ## [Unreleased] ## [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-` 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=`; 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/` 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=))`. 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/` 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**: ``, 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](https://github.com/theta42/proxy/issues/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](https://github.com/theta42/theta-env/issues/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](https://github.com/theta42/proxy/pull/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. [Unreleased]: https://github.com/theta42/proxy/compare/v1.1.17...HEAD [1.1.17]: https://github.com/theta42/proxy/compare/v1.1.16...v1.1.17 [1.1.16]: https://github.com/theta42/proxy/compare/v1.1.15...v1.1.16 [1.1.15]: https://github.com/theta42/proxy/compare/v1.1.14...v1.1.15 [1.1.14]: https://github.com/theta42/proxy/compare/v1.1.13...v1.1.14 [1.1.13]: https://github.com/theta42/proxy/compare/v1.1.12...v1.1.13 [1.1.12]: https://github.com/theta42/proxy/compare/v1.1.11...v1.1.12 [1.1.11]: https://github.com/theta42/proxy/compare/v1.1.10...v1.1.11 [1.1.10]: https://github.com/theta42/proxy/compare/v1.1.9...v1.1.10 [1.1.9]: https://github.com/theta42/proxy/compare/v1.1.8...v1.1.9 [1.1.8]: https://github.com/theta42/proxy/compare/v1.1.7...v1.1.8 [1.1.7]: https://github.com/theta42/proxy/compare/v1.1.6...v1.1.7 [1.1.6]: https://github.com/theta42/proxy/compare/v1.1.5...v1.1.6 [1.1.5]: https://github.com/theta42/proxy/compare/v1.1.4...v1.1.5 [1.1.4]: https://github.com/theta42/proxy/compare/v1.1.3...v1.1.4 [1.1.3]: https://github.com/theta42/proxy/compare/v1.1.2...v1.1.3 [1.1.2]: https://github.com/theta42/proxy/compare/v1.1.1...v1.1.2 [1.1.1]: https://github.com/theta42/proxy/compare/v1.1.0...v1.1.1 [1.1.0]: https://github.com/theta42/proxy/releases/tag/v1.1.0