Commit Graph

241 Commits

Author SHA1 Message Date
wmantly c419249e98 chore: release v1.13.2 2026-08-02 00:16:19 -04:00
wmantly 2e92f58750 v1.13.1: bump @simpleworkjs/bao-conf to 1.0.1
bao-conf 1.0.0's init() threw when VAULT_TOKEN was unset, crashing boot
(.catch -> process.exit(1)) 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). The theta-env stack
is unaffected (it always sets a scoped VAULT_TOKEN).

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-01 12:48:10 -04:00
wmantly 15b154fc8d v1.13.0: load secrets from OpenBao at boot via @simpleworkjs/bao-conf
bin/www now defers require('../app') until bao-conf.init({ path: 'proxy' })
resolves, so models + createOidcClient see the OpenBao-merged config (the
OIDC clientSecret is captured at require time). Authenticates to OpenBao
with a scoped VAULT_TOKEN (policy proxy), never the root token; fail-soft
to CONF_SECRETS if OpenBao is unreachable. config/proxy-secrets.js becomes
an operator-edit seed artifact (OpenBao authoritative). README gains a
Secrets section.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-01 12:29:22 -04:00
dependabot[bot] b54a738524 Bump brace-expansion and ejs in /nodejs (#179)
Bumps [brace-expansion](https://github.com/juliangruber/brace-expansion) to 5.0.9 and updates ancestor dependency [ejs](https://github.com/mde/ejs). These dependencies need to be updated together.


Updates `brace-expansion` from 5.0.7 to 5.0.9
- [Release notes](https://github.com/juliangruber/brace-expansion/releases)
- [Commits](https://github.com/juliangruber/brace-expansion/compare/v5.0.7...v5.0.9)

Updates `ejs` from 3.1.10 to 6.0.1
- [Release notes](https://github.com/mde/ejs/releases)
- [Changelog](https://github.com/mde/ejs/blob/main/RELEASE_NOTES_v5.md)
- [Commits](https://github.com/mde/ejs/compare/v3.1.10...v6.0.1)

---
updated-dependencies:
- dependency-name: brace-expansion
  dependency-version: 5.0.8
  dependency-type: indirect
- dependency-name: ejs
  dependency-version: 6.0.1
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: William Mantly <wmantly@gmail.com>
2026-08-01 11:11:21 -04:00
dependabot[bot] 8bf963f48b Bump body-parser from 2.2.2 to 2.3.0 in /nodejs (#175)
Bumps [body-parser](https://github.com/expressjs/body-parser) from 2.2.2 to 2.3.0.
- [Release notes](https://github.com/expressjs/body-parser/releases)
- [Changelog](https://github.com/expressjs/body-parser/blob/master/HISTORY.md)
- [Commits](https://github.com/expressjs/body-parser/compare/v2.2.2...v2.3.0)

---
updated-dependencies:
- dependency-name: body-parser
  dependency-version: 2.3.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: William Mantly <wmantly@gmail.com>
2026-08-01 11:02:25 -04:00
wmantly 8c81128235 Release 1.9.0: cross-app super admin, modal add-buttons, LE key persistence 2026-07-30 12:01:50 -04:00
wmantly be666f5b2f Recognize app_super_admin; add-user/add-permission as modal buttons; persist LE key
- app_super_admin is a new cross-app LDAP group (also recognized by
  sso-manager-node and jump-host): added to conf.auth.adminGroups so
  members are always global admins here, same as the existing anti-lockout
  adminUsers/adminGroups mechanism.
- 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.
- The Let's Encrypt ACME account key now defaults to the already-persisted
  /data volume (models/host.js) instead of a CWD-relative path
  (./le_key.cert -> /app/le_key.cert in the container), which was lost on
  every image rebuild. Falls back to the old relative path when /data isn't
  present (e.g. local dev outside docker).
2026-07-30 11:57:44 -04:00
wmantly 8cee583da3 Release 1.8.0: page width standardization, card layouts, external-user marking 2026-07-29 22:12:30 -04:00
wmantly c7ec65e0d9 Standardize page width, card layouts; mark SSO users external and read-only
- All pages now wrap their content in <div class="container mt-4">,
  matching sso-manager-node's width instead of rendering full-bleed inside
  the fluid shell.
- Users and Permissions pages converted from bare <table>s to the same
  card-grid convention already used on the Groups page.
- Users backed by SSO/OIDC login (backing === 'oidc', set by the redis
  user model's JIT-provisioning path) are now marked "External (SSO)" and
  their password-change control is hidden; PUT /password/:username also
  rejects with 403 server-side for such users. Deletion stays allowed.
  Redis-backend only -- LDAP/PAM deployments have no per-record marker for
  this today.
- app-base.js (byte-identical across the 3 apps): added
  app.util.revealItem(), wired into the Users/Permissions create flows.
- Bumped @simpleworkjs/frontend to ^0.2.7.
2026-07-29 22:09:50 -04:00
wmantly 3f2ef57c78 Release 1.7.0: API-token modal-based create, Edit modal, reveal-modal fix (#192) 2026-07-28 20:43:42 -04:00
wmantly c0e04d1a56 Unify API-token UI: modal-based create, new Edit modal (#191)
Continues the cross-app API-token UI unification (jump-host landed first).
proxy already had the card grid and a description field, so this is a
smaller diff: converts the always-visible inline create-form card into a
"+ New Token" button + app.modal (matching the Add-Resource/Add-Host
convention used stack-wide, per explicit direction to standardize on the
modal-button approach rather than sso-manager-node's inline-card style),
adds a net-new Edit modal (proxy's PUT /api-token/:id already fully
supported it -- no route change needed), and replaces the static
#secretModal with the same bare app.modal showToken()/copyFieldValue()
pattern jump-host uses.

Found and fixed a real timing bug along the way: the create flow's
evalAJAX called app.modal.close() immediately before showToken() (which
calls app.modal.open()) in the same synchronous tick. app.modal is a
singleton, and close() immediately followed by open() collides with
Bootstrap's hide-transition guard -- show() silently no-ops while
_isTransitioning is still true from the just-started hide(), so the
"secret revealed" modal never actually appeared after creating a token.
Confirmed via a live click-through: the reveal modal stayed invisible
(title set, `.show` class never added) with the close() call, and rendered
correctly with it removed.

Also fixed the same latent bug in jump-host's already-shipped v1.10.0
(submitApiToken() had the identical close()-then-open() sequence) and in
sso-manager-node's directory.ejs (saveResource()'s OAuth-secret-reveal
path, softened there by an intervening `await loadResources()` but not
guaranteed race-free).

Verified live: create -> reveal modal now appears correctly; Edit modal
shows real created-by/on data, saves a description change, card refreshes.
2026-07-28 20:40:39 -04:00
wmantly 130d4a2c9b Release 1.6.0: host modal standardization (#190) 2026-07-28 19:06:51 -04:00
wmantly 3b729295b0 Standardize the host modal: tabs onto app.modal, footer, linkable URL (#189)
Migrates proxy's hostModal (the modal this session's app.modal tabs/footer/
url support was originally modeled on) off its old always-in-DOM static
element and onto the shared app.modal component (@simpleworkjs/frontend
0.2.6), continuing the entity-modal standardization started with
sso-manager-node's resource modal.

Unlike the resource modal, this one already had 6 tabs and Host's audit
trail (created_by/created_on/updated_by/updated_on) already existed and was
already populated correctly by routes/host.js -- no model or route changes
needed there. The tab markup itself is kept as one hand-built bodyHtml
string rather than going through app.modal's own `tabs` array option: that
option builds the tab-content div itself, and there'd be no way to wrap a
<form> around just that piece without extending app.modal again, whereas
this modal's tabs already are exactly the pattern app.modal's own tabs
option was modeled on.

Key correctness points, found and handled:
- The one JS binding that was unsafe under DOM rebuild -- a `keyup` handler
  driving the Let's-Encrypt challenge-type/wildcard visibility, previously
  bound once directly against a captured selector -- is now delegated via
  app.modal.on(), the same bug class already found and fixed in the
  resource modal.
- hostLoadAuthSuggestions() (fills the SSO allow-list autocomplete
  datalists) now re-runs on every modal open, not just once at page load,
  since the datalists are rebuilt fresh (empty) each time.
- .actionMessage moved from a sibling of the old modal's <form> into a
  descendant of it: formAJAX's error/success target resolution
  (app.messages.action's closest('div.card') then a find('.actionMessage')
  fallback) only succeeds via the fallback path once app.modal owns the
  DOM, since app.modal's .modal-content carries no 'card' class.
- The footer's audit dates needed the explicit 'x' (unix ms) format token
  for moment() -- Host's created_on/updated_on come back as redis-hash
  strings, and moment's bare fallback parser silently produces "Invalid
  date" for a numeric string without it (this app's own hostParseRow
  already does this correctly elsewhere; the new footer code needed the
  same treatment).

Also adds GET /hosts/:host (mirroring sso-manager-node's /directory/:slug)
plus a client-side deep-link check, threaded through a new onLoaded
callback on hostPopulate().

Verified live against the running dev stack: all 6 tabs render and save
correctly; the footer shows real created/updated-by/on dates; the SSO
autocomplete has options on a second modal open (not just the first);
the challenge-type keyup logic fires correctly on a second-ever modal
open (confirmed via the actual GET /api/host/wildcard-parent/... network
request); the address bar updates to /hosts/{host} and reverts on close;
a direct load of /hosts/{host} auto-opens the right host's modal; and a
real save (PUT) closes the modal and live-updates the row via the existing
pubsub subscription, end to end.
2026-07-28 19:04:56 -04:00
wmantly e9032ec4fb Release 1.5.3: fix HTML-escaped loading indicator
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-28 13:31:39 -04:00
wmantly 8350ab9b57 Fix HTML-escaped loading indicator in formAJAX
Same fix as sso-manager-node: formAJAX's loading indicator passed a raw
<div class="spinner-border"> string to app.messages.action, which
HTML-escapes its message by design (@simpleworkjs/frontend) -- so every
form submit briefly showed the literal markup as text instead of a
spinner. Replaced with plain text ("Saving…").

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-28 12:52:39 -04:00
wmantly e05ec1d2ad Release 1.5.2: add no-native-dialogs regression test
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-28 00:18:35 -04:00
wmantly 9370734084 Add regression test: no native alert()/confirm()/prompt()
Native confirm() blocks all further browser events on the page (found
live, mid browser-automation testing, on sso-manager-node's equivalent
secret-rotate flow -- it froze the tab). Every call site in this app was
already removed in favor of app.messages.action/confirm/toast and
app.modal.open; this static check (scans views/ and public/js|lib/js for
bare alert(/confirm(/prompt() calls) keeps a regression from shipping
unnoticed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-27 23:36:34 -04:00
wmantly 2d7eadec60 Release 1.5.1: remove native confirm() calls
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-27 17:23:07 -04:00
wmantly 36c3f7a881 Remove native confirm() calls in revokeToken/rotateToken
Native confirm() blocks browser automation entirely (found live, mid
browser-test of the app.messages/app.modal adoption, on sso-manager-node's
equivalent flow). Both functions already receive btn, whose .closest('.card')
is already used for the error path, so app.messages.confirm targets the
same card.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-27 16:50:33 -04:00
wmantly ce013d7e31 Release 1.5.0: adopt @simpleworkjs/frontend messages/modal/validate
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-27 14:16:47 -04:00
wmantly 0a659428dd Adopt @simpleworkjs/frontend's messages/modal/validate modules
Same swap as sso-manager-node/jump-host: vendored app.util.actionMessage/
actionConfirm replaced by @simpleworkjs/frontend's app.messages.action/
confirm (real HTML-escaping, toast fallback); vendored val.js replaced by
the package's app.validate.js.

proxy's host/target/hostname validation rules (mirrored from the backend's
utils/hostname_validate.js — wildcard DNS patterns, not something other
apps need) move 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 (eq/user/password/ip).

app.api/app.auth/app.pubsub/app.socket in app-base.js are untouched, same
reasoning as sso-manager-node's PR.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-27 14:07:14 -04:00
wmantly 2e959f7c48 Release 1.4.0: unified front-end UI shell across the theta42 apps
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 00:21:51 -04:00
wmantly fcc09d1e4b logInRedirect: keep the query string on the legacy /login/<path> form
The OIDC provider sends an unauthenticated authorize request through
/login/oauth/authorize?client_id=…&state=…; dropping the query there
loses the whole authorization request. The ?redirect= form is unaffected.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-25 23:55:08 -04:00
wmantly a83a5fd39a app.api.delete: accept the (url, data, callback) form formAJAX uses; defer the login-card reveal to DOM ready
formAJAX always passes the serialized form as the second argument, so a
DELETE-method form (the host/DNS delete buttons) landed its callback in
the data slot and never ran.

The login page's "reveal the card once we know you're logged out" branch
touched an element further down the same page, which threw when
isLoggedIn answered before the parser got there (it always did without a
stored token). It now runs on DOM ready.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-25 23:15:07 -04:00
wmantly 3a307c1563 Unify the front-end UI shell across the theta42 apps
views/top.ejs, views/bottom.ejs and public/lib/js/app-base.js are now
byte-identical across sso-manager-node, proxy and jump-host. Everything
per-app moved into utils/ui.js, exposed to every render as `ui` via
app.locals (nav items + their group gates, footer repo/docs/ToS links,
favicon, profile/logout targets, update-banner on/off + label).

Client framework changes:
- One gating model everywhere: app-base.js reveals .group-required-<cn>
  for each of the current user user/me groups. sso-manager-node sends LDAP
  DNs in memberOf, the OIDC clients send CNs in groups; both normalise to
  CNs, and the clients isAdmin flag becomes a synthetic `admin` group, so
  proxy nav-admin items are now group-required-admin.
- user/me is fetched once per page load and cached (app.auth.loadUser);
  nav, forceLogin and group-required elements all read that one promise.
- isLoggedIn is dual-mode (Promise + node-style callback), so the async
  and callback call styles both work from one shared top.ejs.
- forceLogin no longer uses $.holdReady (removed in jQuery 4): it redirects
  to /login?redirect=<path>, and still enforces required groups.
- logOut only clears the session; the caller decides where to go next.
- post/put/delete are dual-mode Promise/callback, which also removes the
  undefined `callback2` reference that threw on a non-function callback.

Dependencies: jquery ^4.0.0 and ejs ^3.1.10 in all three apps.

proxy specifics:
- .group-required base rule added to styles.css; the admin nav items lost
  their inline display:none in favour of it.
- The brand link points at / instead of #.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-25 22:57:34 -04:00
wmantly f4efdfb957 Release 1.3.0: adopt shared @simpleworkjs/* packages; fix LDAP filter injection
Rewire onto the shared @simpleworkjs/oidc-client, /ldap, and /app-stack
packages (deleting the byte-identical local forks of the same code), close the
LDAP filter-injection in User.get by routing the username through escapeFilter
(RFC 4515), align model-redis ^1.6.0 and ldapts ^8.1.8, and unify build_info to
{buildVersion, buildHash, buildYear}. package-lock regenerated from the npm
registry (no file:/link:), so npm ci is clean in docker builds.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-25 15:53:30 -04:00
wmantly ecdced16fc Release 1.2.2: fix load-balancing crash (wrong lua-resty-balancer module)
Every request to a host with additional load-balancing targets 500'd:
targetinfo.lua required 'resty.balancer.round_robin', which does not
exist in the lua-resty-balancer rock actually installed by the
Dockerfile/install.sh. That rock provides resty.roundrobin instead,
with a different constructor (roundrobin:new(nodes), not
:new() + :reinit(nodes)).

Verified end-to-end in a rebuilt image: requests to a load-balanced
host now return 200 and alternate across both backend targets, with
no Lua errors in the OpenResty log.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-21 16:21:03 -04:00
wmantly 3f46a807e5 Correct version to 1.2.1: v1.2.0 was already tagged for load balancing
The load-balancing feature (merged in #172/#173) was tagged v1.2.0 on
GitHub, but nodejs/package.json and CHANGELOG.md were never bumped for
it. Backfill a 1.2.0 CHANGELOG entry and bump this release to 1.2.1
instead of 1.1.18, so package.json/CHANGELOG stay ahead of the highest
existing tag rather than behind it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-21 02:07:28 -04:00
wmantly bb1b84b56d Release 1.1.18: fix bootstrap admin lockout, refresh screenshots
models/user_redis.js hardcoded the bootstrap anti-lockout admin
username to 'proxyadmin2', while migrations/permission_bootstrap.js
grants the global-admin permission to conf.auth.adminUsers[0]. An
operator who customized adminUsers away from the default ended up
with a bootstrapped account that had no admin permissions -- a
silent lockout. user_redis.js now derives the bootstrap username the
same way permission_bootstrap.js does.

Also corrected a secrets.js.example comment that claimed the
bootstrap password defaults to the username itself (it actually
generates and logs a random password), and refreshed all README
screenshots against the current UI, including a new load-balancing
screenshot.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-21 02:04:26 -04:00
wmantly 4321826dc8 feat: Add multi-target load balancing support
Fixes #47.
- Added lua-resty-balancer to dependencies (Dockerfile & install.sh).
- Added 'targets' field to the Host model to hold additional targets.
- Updated the UI to allow inputting additional targets (IP:port).
- Updated targetinfo.lua to parse the additional targets and load balance between them and the primary target using resty.balancer.round_robin.
2026-07-21 00:48:43 -04:00
wmantly 7452ccd655 Bump version to 1.1.17; update CHANGELOG
Move the wildcard sibling-parent fix into its own 1.1.17 section (it is not
in the v1.1.16 tag) and bump nodejs/package.json + lockfile to 1.1.17.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 00:27:48 -04:00
wmantly fcd97b12aa Fix attaching an existing sibling subdomain to a parent wildcard
Host.lookUpWildcardParent() walked all labels of the host down to its own
leaf and only inspected that leaf's "*" child, so it found a wildcard
nested under the host (the base-domain case, e.g. *.cool.mysite.com for
cool.mysite.com) but missed the common case where the wildcard is a
SIBLING of the host's leftmost label (e.g. *.nl.wgnode.com covering an
already-existing sso.nl.wgnode.com). The /wildcard-parent route then
returned nothing and the edit form's "Parent Wildcard" option stayed
greyed out, leaving no way to convert an existing auto-SSL host onto a
wildcard issued afterward.

Track the parent node during the walk and check the sibling "*" slot too.
The never-created-subdomain case is unchanged (plain lookUp()'s wildcard
fallback in the route still handles it). Sync the test mock and add
regression coverage for the sibling case (existing host, never-created
host via the route fallback, and a deeper-wildcard negative case).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 00:13:13 -04:00
wmantly 6ede072213 security: swap sanitizer to xss for Jest-compatible SSR output
- Replace isomorphic-dompurify with xss to avoid ESM-only jsdom/htmlparser2
  transitive deps that break the existing Jest test suite.
- Keep rendered docs HTML sanitized via xss() in routes/docs.js.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-18 23:02:11 -04:00
wmantly bdaba513a7 security: sanitize markdown output and harden unix socket permissions
- Add isomorphic-dompurify to sanitize rendered docs HTML
- Tighten SocketServerJson socket permissions from 777 to 660
- Keep package.json version at 1.1.16

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-18 22:41:47 -04:00
wmantly d1dd40d60a chore(release): public-release readiness fixes for 1.1.16
- Fix MIT LICENSE copyright placeholder
- Remove private flag and correct GitHub repository URL in package.json
- Bump version to 1.1.16
- Genericize committed config defaults (example.com/localhost)
- Harden global error handler against information leakage
- Generate random initial password for proxyadmin2 bootstrap account
- Correct docs to describe CONF_SECRETS instead of symlink behavior

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-18 22:02:27 -04:00
wmantly f8d620f4d3 Bump version to 1.1.15; update CHANGELOG
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-18 01:15:31 -04:00
wmantly a02ca4d3e7 Bump version to 1.1.14; update CHANGELOG
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-17 23:42:00 -04:00
wmantly 355a9d68e5 Bump @simpleworkjs/conf to 1.2.0, jq-repeat to 2.2.0
conf 1.2.0 adds CONF_SECRETS, an env var to point at the secrets file
directly -- use it in the Docker entrypoint instead of symlinking the
mounted file into /app/conf/secrets.js, so the app no longer needs
write access to its own conf/ directory to pick up mounted secrets.
jq-repeat 2.2.0 is a compatible feature release (sort(), replace(),
faster leading-edge update() timing); no call-site changes needed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-17 23:35:24 -04:00
wmantly 426fa111ec Add plain-language concept docs; fix docs viewer rendering; link API tokens
- New docs/concepts-{hosts,dns,access,api-tokens}.md -- plain-language
  guides aimed at less technical readers, each linking onward to the
  existing system-design-level doc for anyone who wants that detail.
  Card help links (Proxy List, Add/Edit host, DNS Provider cards,
  Users/Permissions/Groups cards) now point here instead of straight at
  Installation/Architecture.
- The "New API Token" card had no help link at all -- added, pointing to
  the new API Tokens doc.
- Fixed the in-app docs viewer rendering every docs/*.md page with a
  garbled heading + stray <hr> at the top: Jekyll front matter (meant
  only for the GitHub Pages build) was never stripped before being
  handed to the markdown renderer.
- Fixed cross-doc links never resolving in-app, since this viewer serves
  docs at /docs/<slug> with no .html suffix: rewritten to the correct
  in-app URL, first by registered slug, falling back to the doc's real
  filename (the correct, working link form on the Jekyll/GitHub Pages
  build) -- same idea as the existing image-path fix, and lets one link
  written in a doc work on both targets.

Bumps to v1.1.13.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KDEx8ghuZR61pqPXc6da9C
2026-07-17 22:09:53 -04:00
wmantly 4f1fce367e Fix wildcard-parent edit greying and deprecated nginx http2 directive
- The edit form's "Parent Wildcard" option stayed greyed out even when a
  valid wildcard existed, since hostEditOpen() never ran the eligibility
  check (only the host field's keyup handler did, which setting .val()
  programmatically doesn't fire) -- and the check itself, GET
  /host/lookup/:item, had the same self-match bug as the recently-fixed
  Host.prototype.update() case: it resolves an already-existing host to
  its own record instead of a sibling wildcard. Added a dedicated
  /host/wildcard-parent/:item route combining lookUp() (handles a
  brand-new subdomain) with lookUpWildcardParent() (handles an
  already-existing host), and hostEditOpen() now actually runs it.

- Migrated ops/nginx_conf/autossl.conf's deprecated "listen ... http2"
  directive to the standalone "http2 on;" directive (nginx 1.25.1+).

Bumps to v1.1.12.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KDEx8ghuZR61pqPXc6da9C
2026-07-17 21:33:26 -04:00
wmantly a40da55993 Move help links from the global header onto each relevant card
The single header-wide help icon (added last release) pointed at a
per-page doc guess, but a page can have several cards covering different
topics. Removed it and added a small help icon directly to each card
that has real corresponding doc content, linking straight to that doc:
Proxy List + Add/Edit host modal (hosts.ejs), Add DNS Provider + Dynamic
A Records (dns.ejs), Add New User + User List (users.ejs), Add
Permission + Permissions (permissions.ejs), Add Group (groups.ejs).

Bumps to v1.1.11.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KDEx8ghuZR61pqPXc6da9C
2026-07-17 20:00:34 -04:00
wmantly fcd73169e0 Add header help icon and in-app docs search
- A ? icon in the top-right header deep-links to the doc most relevant to
  the current page (client-side path mapping, same pattern already used
  for top-nav active-link highlighting -- no server-side "current section"
  local exists to key off of instead). Falls back to the docs index.
- GET /docs/search does a plain line-substring search over the existing
  allowlisted doc set. No new dependency, stays usable with no internet
  access.

Bumps to v1.1.10.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KDEx8ghuZR61pqPXc6da9C
2026-07-17 19:28:07 -04:00
wmantly 9100e92549 Host form/list UX polish: editable hostname, created-by column, mobile tabs, more help text
- Plain hosts can now be renamed after creation (wildcard/child/cache hosts
  stay locked, since other records reference them by name). Migrates the
  cert cache key on rename.
- Along the way, found and fixed a real bug in the vendored model-redis
  library: its rename path leaves a stray, incomplete hash behind under
  the old key when an `always`-type field (updated_on) is defined earlier
  in the schema than the primary key -- silently blocking that hostname
  from ever being reused. Worked around at the Host model level (can't
  patch node_modules).
- Host list now shows who created each host, and when.
- Host modal's tabs now scroll horizontally on narrow screens instead of
  overflowing awkwardly.
- Added missing inline help text (Target SSL, wildcard matching behavior).

Bumps to v1.1.9.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KDEx8ghuZR61pqPXc6da9C
2026-07-17 19:03:26 -04:00
wmantly 17b903e228 Fix two wildcard-cert gaps: attaching an existing host, and the wildcard's own base domain
- Host.prototype.update() had no challengeType handling (only create() did),
  so selecting "Parent Wildcard" on an existing host's edit form silently
  did nothing. Added the same wildcard-parent lookup to update(), using a
  new Host.lookUpWildcardParent() -- the existing lookUp() can't be reused
  here since an already-created host resolves to its own leaf rather than
  falling through to a sibling wildcard.

- A wildcard's issued cert covers both the base domain and *.base domain
  (altNames), but the lookup tree stores the wildcard one level below its
  base -- looking up the bare base domain landed on an empty parent node
  and found nothing. buildLookUpObj() now also stamps that parent node,
  order-independent (a real host explicitly created at that exact name
  always still wins).

Verified both fixes against a real Redis-backed Host model (not just the
mocked lookup-tree tests) -- see PR description.

Bumps to v1.1.8.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KDEx8ghuZR61pqPXc6da9C
2026-07-17 18:46:09 -04:00
wmantly b4d971b508 Bump version to 1.1.7; update CHANGELOG 2026-07-16 20:22:02 -04:00
wmantly 34b1413c96 Bump version to 1.1.6; update CHANGELOG 2026-07-16 19:02:59 -04:00
wmantly a57f3f03f6 hosts.ejs: fix Authentication tab radios not enforcing mutual exclusivity
The three auth_mode radios (Off / Basic / SSO) had no shared [name]
attribute, so per the HTML spec each was its own independent group --
clicking one didn't uncheck the others, letting multiple options
appear selected at once despite the page's own text saying "basic
auth and SSO can't both be enabled."

Added name="auth_mode" to restore native browser radio-group
behavior. The original comment claimed the radios were deliberately
kept nameless to avoid polluting the submitted form data (formAJAX
serializes every [name] field in the form), but that reasoning
doesn't hold: model-redis's processKeys() rebuilds the saved object
strictly from the Host model's own _keyMap, so an unrecognized
auth_mode field is silently stripped before anything is ever
persisted -- confirmed directly with model-redis's own
object_validate.js. Updated the stale comment accordingly.
2026-07-16 18:59:50 -04:00
wmantly 7c0cb5eabd Bump version to 1.1.5; update CHANGELOG 2026-07-16 18:34:03 -04:00
wmantly 6cd3a5bc58 Update jq-repeat to 2.1.0; fix removed __setPut/__setTake API
jq-repeat 2.1.0 (release notes: https://github.com/wmantly/jq-repeat/releases/tag/v2.1.0)
brings real fixes (throttled-update race conditions, sorted-list
reverse() leaking elements, nested-scope isolation) and a few
behavior changes. Audited every usage in this repo against the
changelog before upgrading:

- push()/unshift() now return the new array length -- every call
  site in this repo is a bare statement, none consume the return
  value. No risk.
- update() is now trailing-edge throttled (~50ms) even on the first
  call, not just rapid subsequent ones -- no code in this repo reads
  DOM/item state immediately after calling update(), so no risk here
  (unlike sso-manager-node's companion PR, which needed a fix).
- jr-order-reverse and nested jq-repeat templates: not used anywhere
  in this repo.

Real breakage found and fixed: users.ejs/groups.ejs/permissions.ejs
called $.scope.X.__setPut(fn)/__setTake(fn) as setter METHODS -- that
API is gone in 2.1.0. Insert/remove hooks are now set via direct
property assignment ($.scope.X.__put = fn), per the current README.
Verified live (real dev server + Playwright): before the fix, all
three pages threw "__setTake is not a function" and the
insert/remove row animations were broken; after, zero errors and the
hooks fire correctly.
2026-07-16 18:26:02 -04:00
wmantly 526545ee68 Bump version to 1.1.4; update CHANGELOG 2026-07-16 17:44:00 -04:00