fedbe81690
isManagedHost() treated a missing metadata.managed flag as permission, so any host the SSO merely discovered -- an unpromoted Proxmox guest, a UniFi client -- was offered in the TUI picker and accepted by the username grammar. Replaced with isCatalogHost(), mirroring the rule the SSO Directory's own listing applies: a resource carrying discovery_sources but never promoted is excluded; hand-created hosts and promoted ones are included; an explicit managed:false is always excluded. The two copies of this rule have now drifted apart once. If a third consumer needs it, hoist it into @simpleworkjs/directory-schema rather than copying again. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
20 KiB
20 KiB
v1.19.0
- fix: only catalog hosts are jump targets.
isManagedHosttreated a missingmetadata.managedflag as permission, so any host the SSO merely discovered — an unpromoted Proxmox guest, a UniFi client — was offered in the TUI picker and accepted by the username grammar. The filter is nowisCatalogHost, mirroring the SSO Directory's own rule: a resource carryingdiscovery_sourcesbut never promoted is excluded, while hand-created hosts (nodiscovery_sources) and promoted ones (managed: true) are included, and an explicitmanaged: falseis always excluded. - test: regression coverage for all five cases (hand-made, discovered-unpromoted, discovered-promoted,
manualsource, explicitly unmanaged). - docs:
docs/connecting.mdstates that discovery results are not jump targets until promoted into the catalog.
v1.18.0
- feat: Add SSO-style error page (404/500) for browser navigation instead of a bare text response
- feat: navbar — username no longer underlined; only the active link is bold + underlined
v1.16.1
- fix: remove missing DEPLOYMENT.md from Docker build context
v1.16.0
- Added OpenBao PKI SSH Certificate Support
- Fallback to LDAP Key injection
v1.15.0
- feat: Rename SSO Manager to Jump in UI
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.
[1.14.1] - 2026-08-01
Fixed
- Bumped
@simpleworkjs/bao-confto 1.0.1 so standalone/no-OpenBao boots don't crash. bao-conf 1.0.0'sinit()threw whenVAULT_TOKENwas unset, which — combined withbin/www's.catch(() => process.exit(1))— made the jump host exit at boot in any deployment without an OpenBao sidecar (standalone Docker, bare metal). 1.0.1 makesinit()fail-soft on a missing token (warn + continue fromCONF_SECRETS), matching the documented contract. The theta-env stack is unaffected (it always sets a scopedVAULT_TOKEN).
[1.14.0] - 2026-08-01
Changed
- Secrets now load from OpenBao at boot via
@simpleworkjs/bao-conf, which
deep-merges
secret/jump-host/confover the file-loaded config. The jump host authenticates to OpenBao with a scopedVAULT_TOKEN(policyjump-host— read-only on its own path), never the root token. Because the OIDCclientSecretis captured at require time insidecreateOidcClient(duringrequire('../models')),bin/wwwnow runsbao-conf.init()beforerequire('../models'). Fail-soft: if OpenBao is unreachable, boot continues fromCONF_SECRETS. Theconfig/jump-secrets.jsfile 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.11.0] - 2026-07-30
Added
app_super_admin(cross-app) andapp_jump_admingroups: super admins are full admins here same asapp_sso_admin; jump admins get audit page/data access without other admin rights. The Audit page/API is now actually admin-gated server-side (previously the page shell rendered for any logged-in user, only its data was gated).- Host list adds Last connection/Last failed connection columns and highlights rows green (a session is live right now) or yellow (the most recent attempt failed), backed by new per-host last-success/last-fail timestamps in
models/metrics.js.services/ssh_server.jsnow attributes grammar/TUI connect failures to the resolved host when one was found, not just aggregate counters.
Changed
- Dashboard's stat boxes and Top hosts/Top users cards moved to the Audit page (audit is now the admin-facing metrics home; dashboard stays focused on "hosts I can reach"). "All hosts" renamed to "My hosts".
[1.10.2] - 2026-07-30
Changed
- Dashboard, Sessions, and Audit pages now match sso-manager-node/proxy's page width, wrapping content in a standard container instead of rendering full-bleed inside the fluid shell.
- Audit's nav entry is now admin-gated (
groups: ['admin']inutils/ui.js), reusing the existing synthetic-admin-group nav-gating convention — the API route was already server-side admin-gated; this hides the nav link for non-admins too.
[1.10.1] - 2026-07-28
Fixed
- The API-token reveal modal silently didn't show after creating a token —
submitApiToken()calledapp.modal.close()immediately beforeshowToken()'sapp.modal.open()in the same tick, colliding with Bootstrap's hide-transition guard on the singleton modal. Same root cause as the OAuth-secret-reveal race fixed in sso-manager-node (v1.8.2) and the create-token race fixed in proxy (v1.7.0).
[1.10.0] - 2026-07-28
Added
- API-token UI unified with sso-manager-node/proxy: card grid replacing the bare table, a new Edit modal (footer shows real created-by/on data), and a Description field on both the create and edit flows — the model and API already fully supported all of this, it just wasn't exposed anywhere in the dashboard.
Changed
@simpleworkjs/frontendbumped to^0.2.6(this app was still on^0.2.5).
[1.9.0] - 2026-07-28
Added
- "Quick Jump" copy-to-clipboard section on the dashboard — the
uid_-_targetgrammar-mode SSH command was documented in the README but nowhere in the UI. A new card gives a one-click-copy command for interactive-picker mode, and every row in "Hosts you can reach" has its own copy button for the exact grammar-mode command to that host, ready to paste and run as-is (uses the logged-in user's own uid).
[1.8.2] - 2026-07-28
Fixed
- Audit records for a failed upstream connection only ever said
upstream-unreachable—resolveAndConnectdiscarded the real error fromconnectUpstream(ECONNREFUSED, ETIMEDOUT, an ssh2 auth-failure message, etc.) and replaced it with that one generic string, so there was no way to tell a network-layer failure from an auth failure from the audit log alone. This is what blocked root-causing the "Could not reach 192.168.1.206" (emby host) report — the real error is now captured and surfaced as a newfailDetailfield on the audit record, shown as a tooltip on the fail badge in the admin audit table.
[1.8.1] - 2026-07-28
Fixed
- Redis had zero persistence (
--save '' --appendonly no, no data-dir volume) — every container rebuild/recreation silently wiped all sessions, in-flight OAuth logins, and any admin-created API token. This is why re-runningsetup.shappeared to "break OAuth with jump": the jump-host container gets recreated, and any token or in-flight login vanished with it. Now Redis persists (AOF + periodic RDB) to/data, mounted as a named volume (jump-redis-data) in theta-env's compose file. Verified live: minted a PAT, force-recreated the container, confirmed the same PAT still authenticated afterward.
[1.8.0] - 2026-07-28
Fixed
- TUI-mode SSH connections (a bare
ssh user@host, no target) could drop with "PTY allocation request failed" / "shell request failed" —runTuiSessionawaited two real round-trips (an audit-log write, then a directory API call) before attaching the session's pty/shell/exec listeners, so a client that sent those requests quickly enough got auto-rejected by ssh2 before anything was listening.runGrammar(theuid_-_targetpath) already had the equivalent fix; this ports it to the picker path. formAJAX's loading indicator showed literal HTML, not a spinner — same fix as sso-manager-node/proxy's companion releases.
[1.7.1] - 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. This app has never had one; keeps it that way.
[1.7.0] - 2026-07-27
Added
- Self-service API tokens (PATs) —
models/api_token.js+routes/api_token.js(mounted at/api-token), Bearer-token support inmiddleware/auth.js, and a create/list/rotate/revoke card on the dashboard. Ports proxy'sjmp_<id>_<secret>pattern; unlike proxy's, a jump-host token carries no group claims, so it authenticates as its creator for non-admin routes only (never passesrequireAdmin). jump-host previously had no PAT support at all.
[1.6.0] - 2026-07-27
Changed
- Adopted
@simpleworkjs/frontend'sapp.messages,app.modal, andapp.validatemodules, replacing the vendoredapp.util.actionMessage/actionConfirminpublic/lib/js/app-base.jsand the vendoredpublic/lib/js/val.js(unused by any current view here, so this is dedup/future-proofing rather than a behavior change).app.api/app.auth/app.pubsub/app.socketare untouched.
[1.5.0] - 2026-07-27
Added
- Web UI dashboard now lists the hosts you can reach ("Hosts you can reach", or "All hosts" for admins) — previously the dashboard only showed usage metrics, with no way to see your actual access from the browser. Backed by a new
GET /api/user/hostsendpoint (auth-only, not admin-gated): admins get the full inventory viautils/access.js's newallHosts(), everyone else gets the same group-based resolution the SSH front door uses. utils/access.js'saccessibleHosts()now accepts a pre-resolvedgroupsarray on the user object, skipping the LDAPgetGroups(dn)round-trip — the web UI's OIDC session already has its groups claim and has no LDAPdnto query with.
Fixed
- Bumped
@simpleworkjs/ldapto 1.0.1, which fixesaddSshKeythrowingObjectClassViolationError(LDAP0x41) on accounts predating theldapPublicKeyauxiliary objectClass. This is the code path this jump host's key-injection (utils/key_inject.js) uses on every first connection for a user — on affected accounts it aborted the SSH connection entirely (key-inject-failed).
[1.4.0] - 2026-07-26
Added
- Standalone mode — run the jump host with no LDAP directory and no SSO Manager at all. Set
standalone.enabled: trueand user authentication and host discovery switch to@simpleworkjs/orm-backed stores (Sequelize; SQLite by default, any Sequelize-supported dialect viaconf.orm) instead of the directory services.models/user_ldap.jsandutils/access.jsbecome conditional facades that pick their backend at require time —ssh_server.js,bridge.js,key_inject.js,tui_picker.js, and the web UI are unchanged either way. - New ORM models:
StandaloneUser(uid,passwordHash,sshPublicKeys,groups) andStandaloneHost(slug,displayName,kind,metadata), plusmodels/user_file.jsandutils/hosts_file.js, which implement the same interfaces as the LDAP client andaccessibleHosts()respectively. There's no admin UI for standalone users/hosts yet — see the README's "Standalone mode" section for the ORM-model seeding snippet. In standalone mode every stored host is reachable by every stored user; there's no group-based authorization yet. - 47 tests pass (24 existing + 15 new unit + 3 existing integration + 5 new standalone integration).
Fixed
services/ssh_server.jsused|| 2222for the listen port, so an explicitlistenPort: 0(ephemeral port, used by the test suite) was silently overridden back to 2222. Changed to?? 2222.services/ssh_server.jsawaitedaudit.create()before registering session listeners. A client that sendsexec/shellimmediately after connecting could have its request dropped because nothing was listening yet. Listener registration now happens first.
[1.3.0] - 2026-07-26
Changed
- Unified the front-end UI shell across the three theta42 apps.
views/top.ejs,views/bottom.ejsandpublic/lib/js/app-base.jsare 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 newnodejs/utils/ui.js, exposed to every render asuiviaapp.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.jsreveals.group-required-<cn>elements for each group the current user is in, read fromGET /api/user/me. sso-manager-node reports LDAP DNs inmemberOfand the OIDC clients report CNs ingroups; both normalise to CNs client-side, and the clients' effective-rightsisAdminflag is exposed as a syntheticadmingroup — 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/meis fetched once per page load and cached (app.auth.loadUser). The nav, per-viewforceLoginand every group-gated element read that one promise instead of issuing their own request.app.auth.isLoggedInis dual-mode: it returns a Promise and invokes an optional node-style callback, so the async and callback call styles both work against one sharedtop.ejs.app.auth.forceLoginno longer uses$.holdReady(removed in jQuery 4). An unauthenticated user is redirected to/login?redirect=<path>; group requirements are still enforced, andlogOutnow only clears the session, leaving the destination to the caller (ui.logoutRedirect).- Dependency alignment across all three apps:
jquery^4.0.0andejs^3.1.10.
Fixed
app.api.deletedropped its callback when called byformAJAX.formAJAXalways passes the serialized form as the second argument, so a DELETE-method form's callback landed in the data slot and never ran.deletenow accepts both(url, callback)and(url, data, callback).app.api.post/putreferenced an undefinedcallback2and 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. logInRedirecton 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 }inpublic/css/styles.css, the base rule the shared gating model reveals against.#spa-shelldropped its inlinemargin-top;styles.cssalready sets it and the shared shell adjusts it when a banner is shown.
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.jsblocks 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.2.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 localutils/oidc.js,utils/safe_redirect.js,models/oidc_state.js,models/token.js,models/auth.js,routes/auth.js;models/index.jswires the factory and the local-admin bootstrap.@simpleworkjs/directory-schema— the sso↔jump-host directory contract.utils/access.jsnow fetches reachable hosts through the sharedcreateDirectoryClient(getResourcesByGroup).@simpleworkjs/ldap—models/user_ldap.jsis now a thin wrapper overcreateLdapClient, preserving this app's loose TLS default (rejectUnauthorized: false) and the exact export shape.@simpleworkjs/app-stack— unifiedbuild_info({buildVersion, buildHash, buildYear}) and thestatic-modulesmounting helper.build_infomoved frommodels/toutils/;routes/render.jsusesmountStaticModules.
Fixed
- Directory envelope drift was silently treated as "no reachable hosts".
utils/access.jspreviously readdata.results || [], so if the SSO directory ever returned a bare array (envelope drift) every per-group query collapsed to[]and no user could bridge. The shared client now validates the{ results }envelope on every call and treats an envelope violation as a failed group fetch rather than silently returning[].
Changed
- Dependency alignment:
ldapts^8.1.2→^8.1.8,redis^4.7→^6.1.0(the directredisdep is unused — onlymodel-redisis used, which already bringsredis^6.1.0). The new@simpleworkjs/*deps resolve from the npm registry (^1.0.0); nofile:/link:entries in the lockfile, sonpm ciis clean in docker builds. build_infoexport shape changed from{commit, version}to{buildVersion, buildHash, buildYear}(the shared shape used by all three apps). The/healthendpoint and footer now reportbuildVersion/buildHash.
[1.1.0] - 2026-07-23
Changed
- Rebuilt the web UI on the shared theta42 app stack so it looks and behaves like the SSO Manager and Proxy: Express + EJS with the same
top.ejs/bottom.ejsshell, Bootstrap 5, jQuery, jq-repeat, FontAwesome, the sharedapp-base.jsclient framework, and Socket.IO — replacing the bespoke minimal theme. Dashboard, Sessions, and Audit pages now render in the common look/feel. - Web-UI auth is now OIDC + a local anti-lockout admin (the proxy's model), replacing the direct LDAP-bind login. Normal users log in through the SSO ("Log in with SSO"); a local
auth.adminUsersaccount (bootstrapped on first boot, password fromauth.localAdminPass) still works if the SSO is unreachable. Admin access is gated byauth.adminGroupsor the local admin account. New config:oidcblock +auth.adminUsers/localAdminPass. Note: the SSH bridge and its own LDAP auth are unchanged — this only affects the web management UI.
[1.0.1] - 2026-07-23
Fixed
- Test scripts use shell-expanded globs and CI provides a redis service, so
npm testruns green on the Node 20/22 CI runners (thenode --test**glob and the redis-backed models only worked locally before). No runtime change.
[1.0.0] - 2026-07-23
Added
- Initial release. An SSH jump host for the theta42 stack:
- Username-grammar routing:
ssh {uid}_-_{target}@jumphostbridges straight to the downstream host (target= a directory host slug, bare hostname, or IP). Shell, exec, and the SFTP subsystem all pass through, so WinSCP/sftpwork. - Interactive TUI picker: plain
ssh {uid}@jumphostlists the hosts the user can reach (from the SSO directory) and bridges to the chosen one. - LDAP auth of the inbound user (publickey against the user's
sshPublicKey, or password via LDAP bind — password policy is off/local/all). - Directory-driven access: reachable hosts are the union of the user's
LDAP groups × the SSO directory (
/api/discovery/resources?group=). - Per-user key injection: the jump host appends its own public key to the
user's
sshPublicKeyon first use, then connects downstream as that user (downstream hosts already serve LDAP keys via ldap-client's AuthorizedKeysCommand). - Web UI + HTTP API (
:3002) for auditing and metrics: active sessions, paged audit log, per-user/per-host counters. Admin login gated by LDAP group membership. - Audit logging of every connection attempt/session (user, target, method, result, bytes, duration, downstream host-key fingerprint).
- Packaged like theta42/proxy: idempotent
ops/install.sh+ systemd unit, all-in-one Docker image, standalonedocker-compose.yml.
- Username-grammar routing: