174 KiB
Changelog
All notable changes to this project are documented here. Format loosely
follows Keep a Changelog; versions
correspond to git tags (vX.Y.Z). Entries here cover theta-suite's own
orchestration code; see each submodule's own CHANGELOG.md
(proxy,
theta-directory,
jump-host)
for what changed inside the apps it composes.
[v2.7.0] - 2026-08-11
Rolls up sso-manager-node v2.7.0. Closes the last "operator hand-sets this" item on the multi-site TODO: OpenLDAP N-way multi-master replication now configures itself.
theta-suite orchestration
bootstrap/site-ldap-register.js: runs on everysetup.shinvocation (master and spoke), fetches this node's auto-assignedLDAP_SERVER_ID+ current peer list from sso-manager-node's new endpoints, and restartssso-manageronly when the computed config actually changed.CFG_LDAP_MMR_MANUAL=trueskips the automatic step for a topology outside this cluster the script can't derive on its own -- otherwise it always runs (every fresh install starts as a master) and would overwrite hand-set values.setup.env.example's old#LDAP_SERVER_ID=1/#LDAP_REPLICATION_HOSTS=manual-config prompt is gone for the common case.
sso-manager-node v2.7.0
SiteSpoke.ldapServerIdauto-assigned at registration (same pattern as jump-host's mesh index); each site's LDAP URL derived from its already-known HTTP(S) endpoint. NewGET /api/site/ldap-peers(spoke-facing) andGET /directory-admin/ldap-replication-config(master-local). Verified against real running containers.
[v2.6.0] - 2026-08-11
Rolls up sso-manager-node v2.6.0 and jump-host v2.1.1 (already
current). Fixes several real bugs found on a live deployment: theta-agent
never got its server_url written, theta-agent update 404'd because
setup.sh installed a stale committed binary, the Directory's site slug and
gateway count were both wrong, and duplicate group rows accumulated on
repeated resource promotion.
theta-suite orchestration
server_urlnow gets written into/etc/theta42/agent.yml. setup.sh's theta-agent install stepsed'd injoin_keybut never touchedserver_url, so it keptagent.yml.example's literal placeholder forever. Self-heals an already-installedagent.ymltoo (never touchesjoin_key/auth_token).theta-agent updateno longer 404s. setup.sh now downloads the current release binary from GitHub instead of trusting one committed in the theta-agent submodule checkout, which predated an upstream fix and could never self-update out of the bug. Matches theta-agent's owninstall.sh. The stale committed binaries were removed from the theta-agent repo.SITE_SLUGauto-derived fromCFG_SITE_NAME. Nothing ever set it before, so a fresh master always showed the app's own literal "site-default" fallback.PROXY_INTERNAL_URL/JUMP_INTERNAL_URLwired intodocker-compose.yml. Both the no-inbound relay automation and the new real gateway-mesh count existed in sso-manager-node's code but were completely unreachable in every real deployment -- neither env var was ever actually set.spoke.env.example-- a dedicated file for the join-a-cluster vars, split out ofsetup.envfor clarity (which still has every option). Layered on top ofsetup.envwhen present. Also addsCFG_PUBLIC_DOMAIN(documented in the spec but never wired into setup.sh before).
sso-manager-node v2.6.0
- Fixed duplicate access/admin groups accumulating on repeated resource promotion (three independent copies of the same missing-existence-check bug).
GET /api/directory-admin/resourcesno longer runs a full LDAP group self-heal fan-out on every list -- moved to write-time, with an explicitPOST /resources/heal-groupsfor backfill.- Recovers an nmap scan that completed successfully despite a benign stderr
warning nmap itself prints (
node-nmaptreated it as a fatal failure). - The Multi-Site modal's gateway count now queries jump-host's real mesh registry instead of an unrelated WireGuard subsystem.
[v2.5.0] - 2026-08-10
Rolls up sso-manager-node v2.5.0 and jump-host v2.1.1 — closes the last gap in no-inbound relay automation (the mechanism existed at the API level but nothing in the real operator bring-up flow could reach it) and fixes two real bugs found live-testing it.
theta-suite orchestration
bootstrap/site-relay-register.js+CFG_SPOKE_NO_INBOUND/CFG_SPOKE_PUBLIC_HOST(setup.env.example): a no-inbound spoke'ssetup.shrun now discovers its own jump-host's WireGuard mesh IP and registers it with the master on every invocation (idempotent no-op until the two jump-hosts are actually meshed — that peering stays a deliberate manual step, same as minting/pasting a site join key).docs/MULTI_SITE_SPEC.mdand the publisheddocs/jump-host/mesh.mdpage updated — both still described this as "designed but not automated" after the API-level work had already shipped in sso-manager-node/jump-host.
sso-manager-node v2.5.0
- No-inbound relay automation reachable from the real join flow.
POST /api/site/joinnow forwardsnoInbound/meshIp/publicHostthrough toPOST /api/site/spokes, which drivesutils/proxy_client.jsto auto-create/update the relay route on the master'stheta-proxy(a new self-serviceprx_...API token client — reusestheta-proxy's existing token system, not a new credential type). Verified against a real runningtheta-proxycontainer. - Replication traffic prefers the mesh.
utils/site_replicate.js's fire-and-forget resync push tries a registered spoke'smeshIpfirst, falling back to its public endpoint on failure.
jump-host v2.1.1
GET /api/mesh/self— this gateway's own mesh IP, for local scripts (gated by any self-service API token, not a full admin session).- Fixed:
/api/mesh/registerwas unreachable via HTTP. A route-mounting order bug meant every/api/mesh/*request hit an admin-session gate beforeroutes/mesh.jsever ran, so a real gateway-to-gateway mesh join always 401'd. Found live-testing/selfwith two real containers. - Fixed: the initiating side of a mesh join never recorded its own
identity —
GET /api/mesh/selfand the mesh UI's own-entry handling silently saw nothing on whichever gateway called/join(only the receiving side of/registerpersisted a self-entry). Verified with two real meshed containers: both sides now report their own correct mesh IP. - Mesh peer removal now cleans up its kernel routes (
wg_iface.removePeer()) — verified live: routes present aftersetPeer, gone afterremovePeer.
[v2.4.0] - 2026-08-10
Rolls up theta-agent v2.2.0 — mDNS local-discovery is now Windows-capable, closing the last gap in the original multi-site design on the agent side.
theta-agent v2.2.0
- Windows local-discovery: the hosts override now runs on Windows
(
%SystemRoot%\System32\drivers\etc\hosts, CRLF-aware,ipconfig /flushdnsafter every change). Reachable because the agent runs as a SYSTEM service, so the elevation question in the spec resolved in our favor. The Windows CI leg now runs the real Windows write path instead of skipping. - Local route pinning (
local_route*.go): the hosts override only fixes name resolution; the packet path is the routing table's job. If the WireGuard mesh tunnel is up withAllowedIPscovering the LAN subnet (or a full-tunnel0.0.0.0/0), the tunnel route would swallow the direct connection to the discovered LAN IP. Discovery now pins a/32host route via the owning local interface (route.exe add ... metric 1on Windows,ip route replaceon Linux) and drops it on revert — closing a real gap in the shipped Linux path. - Prompt reconnect: an apply/revert signals the WebSocket loop, which reconnects immediately instead of waiting out its 5s backoff.
- Installer version fix: the setup.exe previously hardcoded
2.1.0in its file name and version resources no matter the tag; it now derives the version from the git tag.
docs
docs/MULTI_SITE_SPEC.mdstatus table updated: Windows local-discovery marked shipped; macOS remains the one unbuilt piece (hosts override compiles on darwin but needsdscacheutil -flushcache+ real hardware testing, being done on a macOS VM).
[v2.3.0] - 2026-08-10
Rolls up theta-directory v2.4.0, jump-host v2.1.0, theta-agent v2.1.2. Live catalog replication and a real gateway-to-gateway WireGuard mesh land in the same pass — multi-site directory sync stops being a one-time snapshot, and site-to-site networking becomes real infrastructure instead of a documented-but-unbuilt design. See docs/MULTI_SITE_SPEC.md for the full architecture and an explicit TODO list of what's still open (Windows/macOS mDNS, routing directory traffic over the mesh, theta-proxy no-inbound relay automation).
theta-directory v2.4.0
Added
- Live catalog replication. A spoke now stays in sync after joining instead of only getting a one-time snapshot: it registers its own endpoint with the master at join time (
POST /api/site/spokes, Bearer the site join key), and every successful master catalog write fires a fire-and-forget push (utils/site_replicate.js) at every registered spoke, concurrently — one unreachable spoke never blocks or delays delivery to another. The spoke'sPOST /api/site/resynchandler re-runs the same tested export-pull-and-import path used at join time rather than applying a partial diff. - Identical-directory agent-signing key.
POST /api/site/exportnow best-effort includes the master's agent-signing key; a spoke adopts it viaagent_keys.adopt()on both join and every resync, so any site'ssso-manager-nodecan validly sign a command for any agent enrolled at any other site (a deliberate blast-radius tradeoff for this deployment's small, trusted scale — seedocs/MULTI_SITE_SPEC.md§2). - Coordinated master promotion.
POST /api/directory-admin/site-promotenow demotes the previous master as part of the same action (mints it a fresh join key, calls its newPOST /api/site/demote) instead of leaving a manual two-step gap where two nodes could both believe they're master. Best-effort: an unreachable old master never blocks the local promotion — the response'shandofffield reports what happened. - Master Site modal UI: new "Live Replication" (spoke) / "Registered Spokes" (master) status rows; the join form gained a "this site's own reachable URL" field wired to
selfUrl, which the join API already supported but the UI never sent; the promote button's success toast now reports the actual handoff result.
Fixed
site-promote's god_admin check was dead on arrival — it readreq.user.groups, a field nothing in the codebase ever populates, so the check silently evaluated to an empty array on every request. Promotion returned 403 for every user, including a real god_admin, since it shipped in v2.0.0. Only surfaced by live two-container testing, not by inspection.- The read-only write-gate blocked
site-promoteon a spoke before its handler could run — the one mutating request a spoke must be able to make to itself. GET /api/site/configwas returning live credentials (masterJoinKey,replicationPushToken) directly to the browser on every admin session. Replaced with boolean derivatives.
jump-host v2.1.0
Added
- Gateway-to-gateway WireGuard mesh (
routes/mesh.js) — real site-to-site tunnels between theta-gateway instances, distinct from the existing roaming-client/exit-node WireGuard feature. Join-token bootstrap, mesh-index addressing (172.24.<idx>.0/16 + 10.<idx>.0.0/16). - In-kernel WireGuard with a userspace fallback (
utils/wg_iface.js) — prefersip link add type wireguard, falls back towireguard-gowhen the kernel module isn't available. - mDNS local-discovery announcer (
services/mdns_announce.js) — advertises which public hostnames this site fronts so atheta-agenton the same LAN segment can skip the relay/WAN path. - Mesh UI (
/mesh) — gateway identity, join-token minting, remote-join form, meshed-gateways table.
Verified with real two-container tests: an actual encrypted WireGuard tunnel passing ICMP traffic end to end (0% loss), and the mDNS announce/discover/apply/revert cycle over real multicast. Two real bugs found and fixed: wg set ... allowed-ips doesn't add a kernel route (a real handshake completed with zero routing until setPeer() was fixed to add it); mDNS's default IPv6 query aborting the entire lookup after a valid IPv4 response had already arrived.
theta-agent v2.1.2
Added
- Linux mDNS local-discovery (
local_discovery.go,hosts_override.go) — opt-in viaprefer_local_directory; skips the relay/WAN path when a localtheta-gateway/theta-proxyannounces it fronts this agent'sserver_urlhost. Never touches TLS/certificate validation — only changes where the agent connects, never whether it trusts what answers.
Fixed (found via live two-container testing over real multicast)
mdns.Lookup()'s default IPv6 query aborted the whole lookup — discarding an already-valid IPv4 response — when IPv6 wasn't available. Fixed by disabling IPv6 querying explicitly.- Hosts-file writes used write-tmp-then-rename;
/etc/hostsis frequently a bind mount (every container runtime does this) andrename()onto one fails withEBUSY. Switched to truncate-and-rewrite in place.
Windows/macOS local-discovery remain unbuilt — see docs/AGENT_LOCAL_DISCOVERY_SPEC.md.
Also backfills v2.1.0/v2.1.1 changelog entries (Windows agent, WireGuard client, installer, CI) in theta-agent's own CHANGELOG.md, which were tagged and released earlier but never documented there.
[v2.2.0] - 2026-08-10
Multi-site join is now end-to-end: theta-directory can adopt an existing master's directory as a read-only spoke (v2.3.0), and setup.sh wires it into a first-run bring-up.
theta-directory v2.3.0
- Master Site modal: "Join an Existing Site" form (fresh installs only),
Site Join Keys manager (mint/revoke/list), live WAN Sync Health via
POST /api/site/ping. - Spoke read-only: directory writes (resources/edges/groups/secrets/grants/ driver actions/discovery merges) return 403 pointing at the master.
- Fresh-install guard:
/api/site/joinrefuses unless no users beyond the bootstrap admin and no enrolled agents;site-statusexposescanJoin. - (Server endpoints, join keys, persisted role shipped in theta-directory v2.2.0.)
theta-suite (this repo)
- First-run site join:
setup.shstep 5b runsbootstrap/site-join.js(logs in as the admin, calls/api/site/join) whensetup.envsetsCFG_MASTER_DIRECTORY_URL+CFG_MASTER_DIRECTORY_JOIN_KEY. Honored only on first run (once./config/exists it is ignored), so a populated directory can never be merged. Idempotent — an already-joined node reports "already a spoke". setup.env.exampledocuments the two vars; the lint job (branch-protected name "Syntax check bootstrap.js") now alsonode --checkssite-join.js.
[v2.1.1] - 2026-08-10
Fresh-install fixes for the v2.1.0 Windows rollout.
theta-agent v2.1.1
- Silent installs wrote an empty
server_url.CurPageChangedfires even when the wizard is walked programmatically in silent mode, so it read the (empty) edit boxes and clobbered the/SERVER_URL/JOIN_KEYcommand-line params. Now guarded withWizardSilent()— silent installs keep the params (this was also why the service exited at first connect and the Directory showed the agent as a placeholder version). - Tray post-install launch had
skipifsilent— a silent install (the common path from the Directory's Windows command) never started the tray. Removed. theta-agent update404'd — it downloaded from the SSO/resourcespath, which no longer serves binaries (they are GitHub release artifacts). Pointed atreleases/latest/download/.
theta-directory v2.1.1
- "Master Site" button error (
app.modal.show is not a function) — the multi-site status modal used the legacyapp.modal.show()signature; nowapp.modal.open({ title, bodyHtml, size }). - Agents with no discovery showed a fake
v2.0.0— hardcoded fallbacks now reportunknown.
[v2.1.0] - 2026-08-10
Rolls up theta-agent v2.1.0 and theta-directory v2.1.0: the theta-agent Windows client is now a first-class citizen (Windows service, WireGuard mesh, IAM, tray, fully-offline installer) and the Directory can hand a Windows host the same one-command install it has always handed Linux.
theta-agent v2.1.0
- Windows agent (
feat/windows-agent): platform ops (shutdown.exe / sc.exe / PowerShell / Get-WinEvent), Windows service wrapper (install-servicestarts it immediately), session helper (lock/display/logout/staged self-update), signedwireguard_apply/wireguard_remove+ auto-VPN, IAM via local groups + OpenSSH keys, tray icon fix (PNG→ICO), and the fully-offline Inno installer with a wizard page (Theta Directory URL + join key + "open install-agent page"). - Release pipeline:
.github/workflows/release.ymlbuilds every binary on GitHub Actions and attaches them to the GitHub release as artifacts (with optional Azure Trusted Signing); nothing binary is committed to the repo.install.shand the Directory modal download fromreleases/latest/download/.
theta-directory v2.1.0
- Windows install commands in the Install Agent modal. PowerShell one-liners
for the join-key / pre-register / custom-config flows (download the offline
setup.exe, pass/SERVER_URL,/JOIN_KEY,/AUTH_TOKEN,/PUBLIC_KEYor/B64_CONFIG). - Dropped the committed binaries from
nodejs/public/resources/theta-agent/(they are GitHub release artifacts now); the smallinstall.shbootstrap script remains.
[v2.0.2] - 2026-08-09
Rolls up theta-directory v2.0.2, proxy v2.0.1, jump-host v2.0.1. Unifies the GitHub Pages docs site: the SSO/Proxy/Jump Host pages, their nav labels, and each component's own README now consistently say Theta Directory / Theta Proxy / Theta Gateway, drop marketing sections ("Why this over the alternatives", "Get it", "Related projects") that don't apply to a suite component, remove every standalone/bare-metal install path, and link to theta42.github.io/theta-suite/... instead of the old per-repo Pages sites.
theta-directory v2.0.2
- README rebranding & standalone-install cleanup. Removed the "Why this over the alternatives" section and stale links to the old per-repo GitHub Pages site (
theta42.github.io/sso-manager-node/); documentation and secrets links now point at the unifiedtheta42.github.io/theta-suite/site. Made explicit that Theta Directory is deployed as part of Theta Suite and isn't installed or run on its own. Added the agent capability/install screenshots to the gallery. - Docs site (
docs/sso/): full rewrite of the landing page — dropped "Why this over the alternatives" / "Get it" / "Related projects", refreshed all screenshots, added the two agent screenshots, and swept "SSO Manager" → "Theta Directory" across every sub-page (configuration, OAuth, LDAP, directory, agents, replication, vault, concepts-*).
proxy v2.0.1
- Finishes the pending v2.0.0 Docker-only rewrite (README's Quick start already trimmed to one Docker Compose path via Theta Suite).
- Removed "Why this over the alternatives"; trimmed Requirements to actual Docker-host requirements (was still listing bare-metal items: root access, directly-installed OpenResty/Redis).
- Fixed stale links to the old per-repo GitHub Pages site; synced
package-lock.json's version (missed by the earlier 2.0.0 bump commit). - Docs site (
docs/proxy/): renamed to Theta Proxy, dropped the same three sections, refreshed screenshots, relative links within the unified site. Deleted a staledocs/proxy/README.mdmeta-doc left over from when this repo had its own separate Pages site (wrong live URL, referenced a deletedinstallation.md).
jump-host v2.0.1
- Rebranded docs to Theta Gateway (matches the repo's own README/package.json naming).
- Removed the "Standalone Docker" and "Bare metal" install paths, which contradicted the Deployment section's own "exclusively via Docker Compose within Theta Suite" claim.
- Fixed stale links to the old per-repo GitHub Pages sites.
- Docs site (
docs/jump-host/): same section removal, added a WireGuard mesh-routing feature bullet, refreshed screenshots, fixed three more stale absolute links inarchitecture.md. Deleted the same kind of staledocs/jump-host/README.mdmeta-doc.
[v2.0.4] - 2026-08-10
Rolls up theta-directory v2.0.4.
Changed
Dockerfile.openldapno longer compiles OpenLDAP from source. Extracted the from-source compile (nestgroup overlay, ~5 min, dependent ongit.openldap.orgbeing reachable — it 502'd twice tonight, blocking two PRs) into its own image,ghcr.io/theta42/openldap-nestgroup:<pinned commit>, published by a new workflow whenever the pin changes.Dockerfile.openldap'sldapbuildstage now just pulls it. Cut CI's "Build LDAP test image" step from ~5-6 minutes to ~1.5 minutes total per matrix run; every local build of this Dockerfile gets the same speedup.
[v2.0.3] - 2026-08-09
Rolls up theta-directory v2.0.3.
Fixed
- Directory tab showed unpromoted discoveries.
GET /api/directory-admin/resourcesunconditionally admitted everykind: 'host'resource, and every discovery plugin (UniFi, Proxmox, nmap) creates its finds askind: 'host'— so unchecking "Auto-promote to Directory" on a plugin never actually kept undiscovered/unpromoted devices out of the Directory tab, only out of the LDAP-group auto-provisioning. Now onlysiteresources are unconditionally shown; anything else that discovery ever touched requiresmetadata.managed === true(set by promotion, an agent, or merging into an already-managed resource). GET /api/directory-admin/site-status500'd. QueriedResource.list({ where: { subType: 'wireguard' } }), butsubTypeonly ever lives inmetadata.subType— never a top-level DB column, so SQLite raisedno such column: Resource.subType. Filters in JS overmetadata.subTypenow.- Discovered Inventory had no way to review ignored devices. Added a "Show ignored" toggle (off by default).
Chore
- Untracked
nodejs/config/inventory.sqlitein theta-directory — the app's default runtime DB, not a fixture; had been committed by mistake across 13 prior releases.
[v2.0.2] - 2026-08-09
Rolls up theta-directory v2.0.2, proxy v2.0.1, jump-host v2.0.1. Unifies the GitHub Pages docs site: the SSO/Proxy/Jump Host pages, their nav labels, and each component's own README now consistently say Theta Directory / Theta Proxy / Theta Gateway, drop marketing sections ("Why this over the alternatives", "Get it", "Related projects") that don't apply to a suite component, remove every standalone/bare-metal install path, and link to theta42.github.io/theta-suite/... instead of the old per-repo Pages sites.
theta-directory v2.0.2
- README rebranding & standalone-install cleanup. Removed the "Why this over the alternatives" section and stale links to the old per-repo GitHub Pages site (
theta42.github.io/sso-manager-node/); documentation and secrets links now point at the unifiedtheta42.github.io/theta-suite/site. Made explicit that Theta Directory is deployed as part of Theta Suite and isn't installed or run on its own. Added the agent capability/install screenshots to the gallery. - Docs site (
docs/sso/): full rewrite of the landing page — dropped "Why this over the alternatives" / "Get it" / "Related projects", refreshed all screenshots, added the two agent screenshots, and swept "SSO Manager" → "Theta Directory" across every sub-page (configuration, OAuth, LDAP, directory, agents, replication, vault, concepts-*).
proxy v2.0.1
- Finishes the pending v2.0.0 Docker-only rewrite (README's Quick start already trimmed to one Docker Compose path via Theta Suite).
- Removed "Why this over the alternatives"; trimmed Requirements to actual Docker-host requirements (was still listing bare-metal items: root access, directly-installed OpenResty/Redis).
- Fixed stale links to the old per-repo GitHub Pages site; synced
package-lock.json's version (missed by the earlier 2.0.0 bump commit). - Docs site (
docs/proxy/): renamed to Theta Proxy, dropped the same three sections, refreshed screenshots, relative links within the unified site. Deleted a staledocs/proxy/README.mdmeta-doc left over from when this repo had its own separate Pages site (wrong live URL, referenced a deletedinstallation.md).
jump-host v2.0.1
- Rebranded docs to Theta Gateway (matches the repo's own README/package.json naming).
- Removed the "Standalone Docker" and "Bare metal" install paths, which contradicted the Deployment section's own "exclusively via Docker Compose within Theta Suite" claim.
- Fixed stale links to the old per-repo GitHub Pages sites.
- Docs site (
docs/jump-host/): same section removal, added a WireGuard mesh-routing feature bullet, refreshed screenshots, fixed three more stale absolute links inarchitecture.md. Deleted the same kind of staledocs/jump-host/README.mdmeta-doc.
[v2.0.1] - 2026-08-09
Rolls up sso-manager-node v2.0.1 and theta-agent v2.0.1.
Added
- Non-Root Secrets Group Access: Set up
theta-secretsandthetasystem groups insetup.shand set permission flags to0750on/etc/theta42and0640onagent.ymlto allow non-root users in the group to request secrets. - Autostart Tray Icon Companion: Configured
/etc/xdg/autostart/theta-agent-tray.desktopinsetup.shto automatically launch the desktop tray icon companion. - OpenBao / OpenBoa Resource Exclusion: Skipped internal secret/renewer services from populating the directory resources catalogue.
- Full Docker Integration Tests: Rewrote the integration test runner (
test-integration.sh) to support full env-mode LDAP seeding (seed-test-user.sh) and pass test environment configs/parameters reliably inside Docker containers.
[v2.0.0] - 2026-08-09
Rolls up sso-manager-node v2.0.0, theta-agent v2.0.0, jump-host v2.0.0.
Added
- Theta Gateway (`jump-host` v2.0.0): WireGuard mesh exit node management, QR code profiles, `.conf` file downloads, and automatic X25519 keypair bootstrap.
- Theta Agent (v2.0.0): System tray status companion badge, systemd logind desktop session detection, and real-time CPU / disk / process telemetry stream.
- SSO Directory (`sso-manager-node` v2.0.0): Live telemetry dashboard cards, desktop session power controls (lock, display off, logout, sleep), and site reconciler.
[v1.48.0] - 2026-08-08
Rolls up sso-manager-node v1.33.0, theta-agent v1.8.0, proxy v1.35.1, jump-host v1.19.1.
Added
- Directory Key Badges & Secret Search/Filter. Added key badges
🔑 Secretto resources with stored OpenBao secrets, secret key search filtering, and aWith Secretstree toggle. - Discovered Inventory Merge & Ignore Actions. Supported merging discovered network devices into existing Directory resources and ignoring unmanaged entries.
- Kind-Specific Resource Creation Modals. Added dedicated
+ Add Site, Host, and Service creation workflows. - Optional Child Secret Key Name on Inheritance. Made key name optional when inheriting parent secrets — defaulting to the original parent secret key name if left blank.
- Agent Tab Versioning, Logged Users & Desktop Operations. Added Agent Version badge (
v1.8.0), physical partitions table, active logged-in sessions list (who/host.Users()), and desktop session/power controls (Lock, Display Off, Log Out, Sleep Host).
[v1.47.0] - 2026-08-08
Rolls up sso-manager-node v1.32.0, theta-agent v1.7.0, proxy v1.35.1, jump-host v1.19.1.
Added
- Subtype Management & Metrics Drivers Architecture. Implemented a 4-tier resolution engine (
services/driver_registry.js) binding resourcesubTypemetadata (systemd,docker,proxmox,wireguard,postgresql,redis,unifi,k8s) to operational telemetry, log streaming, and remote lifecycle control. - Explicit Secret Inheritance Mode. Enforced strict upward ancestor lineage (
Resource -> Host -> Cluster -> Site) for secret inheritance with explicit pointer resolution (INHERIT:<parentSlug>:<parentKey>). - Cross-Platform Theta Agent Binaries. Compiled native zero-dependency Go binaries for Linux (amd64, arm64, armv7), Windows (amd64, arm64), and macOS (Intel, Apple Silicon M1/M2/M3/M4).
- Consolidated External App Tokens. Relocated external OpenBao App Token minting into the Configuration page (
/conf-> External App Tokens tab) and deprecated standalone/vaultnavigation item. - Multi-Secret Support. Supported multiple secret keys per resource in OpenBao
secret/data/resources/<slug>/confwith per-key merging and deletion. - Automated Integration Testing. Fixed
test-integration.shto use moderndocker composesyntax and added driver test coverage.
Fixed
- Ancestry Lineage Querying. Fixed
Resource.findAllAncestors(id)memory filtering overResourceEdge.list()to resolve deep ancestor lineage across all graph depths. - Dockerfile Driver Staging. Added
COPY nodejs/drivers ./driverstoDockerfile.openldapandDockerfile.test-runnerfor clean container execution.
[v1.46.0] - 2026-08-07
Rolls up theta-agent v1.6.0, sso-manager-node v1.31.0, jump-host v1.19.1.
Added
- On-demand CLI Secret Fetching.
theta-agent get-secret <key>andtheta-agent get-secrets [--env|--json]for dynamic secret resolution without plaintext files on disk. - Resource Secrets Engine & Zero-View Security. OpenBao KV-v2 encrypted secrets for directory resources with strict regex validation (
^[A-Za-z0-9_]+$), password generator, and multi-level hierarchy secret inheritance. - OpenBao
sso-brokerPolicy. Grantedsecret/data/resources/*andsecret/metadata/resources/*permissions tosso-broker. - Zero-Trust LDAP WebSocket Tunnel. Auto-starts local
/run/theta/ldap.sockand127.0.0.1:3890loopback listeners on managed nodes. - Agent Self-Update & Service Control. Added
theta-agent updateandtheta-agent reinitializeCLI commands with automated service restarts (sssd,sshd).
[v1.45.0] - 2026-08-06
Rolls up sso-manager-node v1.30.2, proxy v1.35.1, jump-host v1.19.1.
bootstrap.js — Directory topology fix
host_theta-proxy / host_theta-jump were synthetic kind: 'host' resources that never should have existed. "Host" means a real, independently-existing machine — something with its own OS and sshd. A Docker container backing one of this stack's own services is never that: it has no sshd, no independent network identity. Proxy and jump-host are two of this stack's five containers, running on the one real stack host — not machines of their own.
A 2026-08-05 change gave them their own host resources to fix their services being parented to the stack host, solving that parenting problem with the wrong tool — the correct one, kind: 'container', already existed one layer below service (same as sso-manager and openbao already used correctly). Beyond being conceptually wrong, this had a real functional consequence: jump-host resolves its "hosts you can reach" list from exactly kind: host resources, so it could offer theta-proxy/theta-jump as SSH targets — machines that don't exist and can't be reached.
Fixed: bootstrap.js no longer creates the synthetic hosts. Proxy's and jump-host's services parent directly onto the stack host, like every other component. On an install seeded between 2026-08-05 and this release, the fix self-heals on the next ./setup.sh run — existing children are re-parented onto the real host and the now-empty synthetic host resources are removed automatically; a fresh install never creates them.
Docs
README.md's architecture diagram and "Repo layout" section described a stale 2-service (sso-manager + proxy) architecture from before jump-host and OpenBao existed — updated to match the (already-accurate)docs/architecture.md, and listed only 2 of 5 git submodules — added the rest.docs/fixtures.md(new) — the canonical demo-fixtures reference: exact users/groups/hosts for a consistent homelab/small-business demo dataset, so future screenshot passes only need to re-capture pages whose UI actually changed.docs/screenshots.md(new) — the screenshot-capture workflow, including two gotchas hit while building it: a stale-browser-cache issue withapp.modal.js, and never touching a login form that autofills a real saved credential.bootstrap/seed-demo-users.sh(new) — idempotent script seeding the fixtures.md user/group list via direct LDAP writes matching the app's own schema.
[v1.44.0] - 2026-08-06
Rolls up sso-manager-node v1.30.1.
sso-manager-node v1.30.1
Test Email and Test SMS could never have worked, and all SMS delivery was broken.
- Test Email threw
Email.send is not a function:models/email.jsexports{Mail}, and the handler required the module and called.sendon it directly. - Test SMS threw
Unexpected token '<', "<!DOCTYPE "...: it POSTed tohttps://api.voip.ms/v1.0/sms/send, an endpoint that does not exist. VoIP.ms's REST API is a GET againstvoip.ms/api/v1/rest.phpwithapi_username/api_passwordandmethod=sendSMS, so the fabricated URL returned an HTML page andresponse.json()threw. - Every SMS was broken, not just the test.
models/sms.jscalledPluginInstance.find({…}), but the ORM has nofind— the query method islist({where}). It threw on every send, before it could even fall back to the direct VoIP.ms path, so OTP-by-SMS and notifications were dead too. - Both test endpoints now send through the same senders every real message uses. A test that reimplements delivery proves nothing about whether real delivery works — which is how two independently broken paths went unnoticed. Failures report as
400with the underlying reason instead of an opaque500. - New guard suite fails the build on any call to a non-existent ORM static, on requiring
models/emailwithout destructuring{Mail}, and on any reference to the bogusapi.voip.mshost.
Install Agent offers the join-key flow. v1.43.0 shipped join keys in the API and documented the modal as the place to get one, but the modal itself still only did the pre-register flow. It now leads with "Join key" — mint one, copy a single install command, and the host enrolls itself.
Release note
Tagged with GitHub Actions in a major outage. CI could not run (every job failed at Set up job with Failed to resolve action download info: Service Unavailable, before reaching any test). Verified locally instead, on the exact merged commit: the full Docker suite — same LDAP + Redis service containers CI uses — passed 299/299, plus proxy 176/176, jump-host 43/43 and theta-agent green. The Node 18/20/22 matrix was not exercised.
[v1.43.0] - 2026-08-06
Rolls up sso-manager-node v1.30.0, theta-agent v1.5.1, proxy v1.35.0. Fixes what a fresh setup.sh install actually produced under v1.42.0.
No manual step to re-enroll agents. v1.42.0 required an admin to pre-register every host.
setup.shnow mints a join key and the agent enrolls itself, so installing the agent is once again all it takes to add a host.
Fixed — theta-suite orchestration
- The stack's own theta-agent could never connect.
setup.shgenerated a random token locally and wrote it intoagent.yml. The SSO only accepts credentials it issued, so that token was rejected on every attempt and the agent looped onclose 4001: Unauthorizedforever. It now writes a join key the SSO minted; the agent exchanges it for its own token and the SSO's public key on first connect and rewrites its own config. agent.ymlwas left holding literal placeholders. TheREPLACE_WITH_ISSUED_AGENT_TOKEN/REPLACE_WITH_SSO_PUBLIC_KEYstrings were shipped as-is when the seds no longer matched the renamed fields, so the file on a fresh install contained no credential at all. The file is alsochmod 600now that it holds one.- A fresh install presented its own five containers as unmanaged discoveries (
theta-proxy,theta-jump,sso-manager,bao-renewer,openbao). The compose project name is now passed to the Docker discovery plugin, which recognises them as ours and links each to the service it implements. openbaoandbao-renewerhad no directory entries, so their containers had nothing to attach to and appeared as parentless roots. Both are seeded as services now — they are part of what the stack deploys and belong in the directory like every other component.
Added
- The bootstrap mints a theta-agent join key and hands it to
setup.sh(AGENT_JOIN_KEY), reusing thesetup-labelled key across runs.
sso-manager-node v1.30.0
Join keys. POST /api/agent/join-keys mints one credential an operator hands out; a host presenting it is enrolled automatically and immediately issued its own per-agent token plus the public key to pin. The join key is a bootstrap credential, never the host's identity — one key stays convenient without becoming a fleet-wide skeleton key, every host remains individually revocable, and revoking a key stops new hosts joining without touching enrolled ones.
Collapsing the Directory tree did nothing. applyTreeCollapse found the caret with .tree-caret i and returned early when absent — Font Awesome's SVG-with-JS mode rewrites <i> to <svg>, so that selector matched nothing and the early return skipped setting hideBelowDepth, meaning no row was ever hidden. State now lives on the caret button, rotated by CSS.
Discovery Plugins. The delete button called deleteDiscoveryPlugin(), which was never defined. The pane also had no .actionMessage, and confirmations render into one — without it the promise never settles, so an awaited confirmation hangs forever and the gated action silently never happens. Instances can now be edited (secrets shown blank rather than prefilled with the mask).
Discovery. Docker container slugs came from the container id, which changes on recreate, so every deploy minted a new resource and orphaned the old one; they now derive from compose project + service.
Docs. /docs/discovery 404'd; new docs/discovery.md. The agents slug pointed at plugins.md, leaving docs/agents.md unreachable in-app.
theta-agent v1.5.1
join_keyconfig field, presented whileauth_tokenis empty. The agent persists the issued token + public key into its ownagent.yml— line-based, so comments, capabilities and formatting survive — and blanks the join key.- Sends
?hostname=so a self-enrolling host is named after itself; refuses to connect with no credential rather than presenting an empty one. install.sh --join-key.- v1.5.1 rebuilds the prebuilt
theta-agent-linux-amd64.setup.shinstalls that committed binary rather than building from source, and the v1.5.0 one predated join-key support — it would have received ajoin_keyit did not understand. Same trap as the v1.3.0 heartbeat fix.
proxy v1.35.0
- Permission entries can be edited; previously only Delete existed, so changing a role meant delete-and-re-add. Because a permission's id is derived from (subjectType, subject, scope, domain), changing any of those replaces the record — the endpoint creates the new grant and removes the superseded one in that order, so an edit can never leave the old grant conferring access.
[v1.42.0] - 2026-08-05
Rolls up sso-manager-node v1.29.0, theta-agent v1.4.0, proxy v1.34.0 and jump-host v1.19.0.
Breaking — re-enroll your theta-agents. The SSO now rejects agent tokens it did not issue. Any agent installed before this release carries a token generated in the browser that the server never recorded, and will be refused with close code
4001until re-enrolled from Directory → Install Agent.Re-run
./setup.sh. Thesso-brokerOpenBao policy needs the newsecret/agent/*grant, or the SSO cannot persist its agent signing key and will refuse every high-risk agent command.
Fixed — theta-suite orchestration
- Per-host SSO returned
400 redirect_uri is not registered for this client. The bootstrap registered only the proxy's own management callback (https://<proxy-host>/api/auth/oidc/callback), but per-host SSO calls back tohttps://<protected-host>/__proxy_auth/callback— a different URL for every host the proxy fronts, all against that one OAuth client. It now also registershttps://**.<domain>/__proxy_auth/callbackand the bare apex, andensureRedirectUris()backfills them onto an existing client so upgraded stacks are fixed too, not just fresh installs. (The SSO's wildcard matcher already supported this; nothing was ever registered to use it.) - Seeded services were parented to the wrong host.
theta-proxyandtheta-jumpwere created as host resources and then left childless, while the Proxy, OpenResty Edge and SSH Jump Host services hung off the stack host instead. They now parent to the host that runs them.reparent()corrects existing installs on the next run, and only when the current parent is exactly the one the old code set — a layout an operator arranged deliberately is left alone. - The directory-edge fetch added for re-parenting is tolerated separately from the resource list, so losing it can't skip seeding the resources themselves.
Added — theta-suite orchestration
- The proxy gets a read-only SSO API token. Minted by the bootstrap and written into
proxy-secrets.js(before the OpenBao snapshot, so the running proxy actually receives it), backing the per-host SSO group autocomplete. Idempotent: only mints whensso.apiTokenis still empty. setup.shwrites ansso: { url, apiToken }block into the generatedproxy-secrets.js.- The
sso-brokerOpenBao policy grantssecret/agent/*for the persistent theta-agent signing key. docs/secrets.mddocuments the signing key, why it must be stable, and what happens when the grant is missing.
sso-manager-node v1.29.0
Security — the theta-agent channel authenticated nothing. /api/agent/ws accepted any token string; there was no agent registry, because tokens were generated in the browser and never recorded server-side. Anyone who could reach the SSO could register as a node, publish discovery/telemetry into the admin view, and receive commands — including a signed arbitrary_bash — addressed to a token they guessed.
- Agents are now rows in a new
Agenttable, authenticated by SHA-256 token hash before the connection is registered or the welcome payload is sent. Unknown/revoked → close4001, audited. POST /api/agent/enrollmints the token server-side and returns it once; only its hash is stored. Rotate/revoke/delete drop the live socket immediately (4004/4003).- Commands are addressed by agent id, never by token.
- The Ed25519 signing key was generated in the
AgentManagerconstructor, so it changed on every restart and thepublic_keypinned in an agent'sagent.ymlstopped matching. It now lives in OpenBao atsecret/agent/signing-key; if it can't be loaded the SSO refuses high-risk commands rather than signing with a key no agent has seen. - Enroll/update/rotate/revoke/delete, every command, and every rejected connection are audited with the acting user.
Directory & agents. Agents bind to a host resource instead of being matched by hostname; a bound agent's discovery is written onto that resource (discovery_sources: ["theta-agent"]) — previously the one source running on the host contributed nothing. Enrollments survive restarts, so "installed but offline" (red) is now distinguishable from "no agent" (grey). The Install Agent modal enrolls first and emits --public-key, which was never written into agent.yml before.
Directory tree. Collapsible, with per-browser persisted state; an active search overrides collapse so matches inside folded subtrees aren't hidden.
Discovery — found by running against a live 3-node Proxmox cluster.
- MACs and IPs were collected into two flat lists and zipped by index, attributing addresses to the wrong NIC on multi-NIC guests. NICs are now keyed by MAC.
- A Proxmox endpoint resource now parents its nodes (one endpoint = one subtree), carrying no IP — giving it the address it's reached at made the reconciler merge it with the node answering there, producing a resource that was its own parent. Self-edges and cycle-closing edges are refused.
- Hosts were named after their MAC address, because
bestNamepreferred the longer string. Names are ranked hostname > IP > MAC. isIpnever matched anything (\\.in a regex literal matches a backslash, not a dot).- Guests carry
sourceId/node/vmid/macAddress; container and overlay interfaces (docker0,veth*) are filtered out; stopped VMs still report a MAC; DHCP LXCs get an address; nodes report their own IP/MAC; offline nodes are recorded rather than skipped. - Cross-kind merges prevented; the inventory is read once per run instead of once per incoming resource.
Other. The Profile page's API Tokens card is no longer wider than every other card (it sat outside the page container). Dockerfile.test-runner never copied nodejs/plugins, so every plugin test suite had been failing in CI as "Cannot find module" — suites 27 → 29, 296 tests passing.
theta-agent v1.4.0 (protocol v1.2.0)
- Fail-closed verification.
verifySignaturereturnedtruewhen nopublic_keywas configured — and the installer never wrote one, so a default install executedreboot,configure_ldap,arbitrary_bashandupdate_binaryunverified. - Canonicalization disagreed with the server. Go's
encoding/jsonescapes<,>and&;JSON.stringifydoes not. Any payload containing them failed verification — forarbitrary_bashthat is most real scripts (>redirection,&&). Now usesSetEscapeHTML(false). - Handles the SSO's enrollment close codes and backs off 5 minutes instead of retrying a dead credential every 5 seconds forever.
- The connect log no longer prints the URL, which carried
?token=. install.sh --public-key, and a loud warning when none is configured.
proxy v1.34.0
- The per-host SSO Allowed groups field autocompletes from the SSO directory's groups. It previously suggested only local groups — the one set of values that can never match, since the allow-list is checked against the SSO's
groupsclaim. Newconf.ssoblock; degrades silently when unset. - Authenticates with
Authorization: Bearer, not theauth-tokenheader.
jump-host v1.19.0
- Only catalog hosts are jump targets. The filter treated a missing
managedflag as permission, so unpromoted discovery results — Proxmox guests, UniFi clients — appeared in the TUI picker and were accepted by the username grammar. It now mirrors the SSO Directory's own rule.
[v1.41.0] - 2026-08-05
Fixed
- The Local Docker daemon discovery plugin no longer errors — the sso-manager container had no access to the host docker socket, so the seeded
docker-localplugin (socketPath/var/run/docker.sock) failed withENOENTand showed "Last run: error".docker-compose.ymlnow mounts/var/run/docker.sockinto the container. Recreate the container (docker compose up -d sso-manager) and hit "Run now" on the plugin. - theta-agent ships the rebuilt binary with the heartbeat fix (v1.3.1, gitlink
51750d0) — the prebuilttheta-agent-linux-amd64predated the v1.3.0heartbeat_ackfix, so the installed agent still logged "Unknown command type: heartbeat_ack". Now rebuilt + tested.
[v1.40.0] - 2026-08-05
Fixed
- No more spurious "Invalid Credentials, login failed" during LDAP enrollment (ldap-client v1.25.0, gitlink
68fcdb5) —index.shself-registered the host in the Directory whensso_tokenwas declared but empty (it checked[[ -v ]]), POSTing an empty Bearer token and getting a misleadingLDAPLoginFailed. It now only registers with a real token; the stack host (already seeded by the bootstrap) skips registration. - The
cn=ldapclientservice account now shows in the SSO Users UI — it was created as a bareorganizationalRole(invisible to theposixAccountuser filter) and never joinedapp_sso_service_account, so it never appeared as a service account. The bootstrap now creates it as aposixAccount(uid 10001, above the regular-user reserved floor) and adds it toapp_sso_service_account; for an existing account it best-effort adds theposixAccountshape (auxiliary, so it can't conflict with the structuralorganizationalRole) + the group membership.
[v1.39.0] - 2026-08-05
Fixed
- Plain LDAP (389) now reachable from the host —
docker-compose.ymlpublished only LDAPS (636); plain LDAP (389) was deliberately not mapped, so the stack host's own enrollment (setup.sh→ ldap-client, which configures sssd againstldap://localhost:389andldaps://localhost:636) could not reach the directory over loopback. Both 389 and 636 are now published to the host (bind 0.0.0.0;LDAP_BIND/LDAPS_BIND=127.0.0.1to lock to the host only).
[v1.38.0] - 2026-08-04
Fixed
- LDAP enrollment no longer reaches for the public domain —
setup.shgeneratedldap.varswithldap_hostdefaulting to the public SSO host (sso.<domain>), which the NAT/firewall blocks on the LDAP ports (389/636). It now defaults tolocalhost(the LDAP server is co-located on the stack host;ldap_tls_reqcert=nevermakes this safe), overridable withCFG_LDAPS_HOSTfor an internal hostname/IP. - SSH access groups match the SSO group model (ldap-client v1.24.0) — the generated
sssd.confaccess filter andldap-ssh-key.shreferenced the legacy names (<location>_access,app_super_admin); they now usesite_<location>_hosts_access(all-hosts aggregate),site_<location>_host_<hostname>_access, andgod_admin. GROUPS.md §8's example updated to match.
[v1.37.0] - 2026-08-04
Changed
- Group naming corrected to match docs/GROUPS.md — per-resource groups are
{site}_{kind}_{name}_{level}(kind always present; a hosthost_theta-env→site_local_host_theta-env_access, a service →site_local_app_sso-manager_access). The spec's §3 text was updated to state this explicitly. - Roll up sso v1.27.0 — group names match the docs, a site carries only god + site-wide groups, duplicate group links removed,
/api/agent/*no longer 404s, shared-secrets POST/GET fixed, Vault Apps tab lists minted tokens, discovery promote + plugin run logs fixed. See the sso changelog.
[v1.36.1] - 2026-08-04
Fixed
setup.shno longer aborts withCFG_BASE_DN: unbound variable— the ldap-clientldap.varsgeneration read the CFG_* first-run vars, whichensure_configonly derives once (it returns early on a re-run oncesso-secrets.jsexists). It now reads the real values from the operator-owned./config/sso-secrets.jswhen the CFG_* vars are unset, so LDAP enrollment works on re-runs too. The generatedldap_access_groupsnow referencesgod_admin(the legacyapp_super_adminis gone).- Roll up sso v1.26.1 — drops the legacy
app_super_admin:SUPER_ADMIN_GROUPis nowgod_admin(nested into every resource's_admingroup), anddocker-entrypoint.shno longer seeds/nestsapp_super_admin. See the sso changelog.
[v1.36.0] - 2026-08-04
Added
god_adminseeded + site groups auto-provisioned (sso v1.26.0) —god_adminexists from first boot; every site gets{site}_super_admin,{site}_hosts_*/{site}_apps_*aggregates and{site}_everyone; per-resource groups ({site}_{slug}_{level}) nest into the site aggregates (the inheritance lattice now exists in LDAP, not just the resolver). See the sso changelog for the full group-model completeness + server-side naming enforcement + Directory god_admin management.- Docker discovery plugin configured out of the box — the bootstrap seeds a
docker-localplugin instance pointed at/var/run/docker.sock, so a fresh stack discovers its own containers into the Directory immediately (idempotent; an operator-created instance is left alone).
Fixed
- ldap-client enrollment no longer fails —
setup.shwas callingldap-client/index.sh, which refuses to run without a gitignoredldap.varsthat nothing ever created (the "ldap.vars file not found!" + "enrollment failed" you saw). It now generatesldap-client/ldap.varsfrom the stack's own config (LDAPS host, base DN,cn=ldapclientbind + service password, SSO URL, site name) before enrolling; an operator-providedldap.varsis always kept. - theta-agent no longer logs
Unknown command type: heartbeat_ackevery minute — the server's ack of the agent's own heartbeat is now silently ignored instead of falling through to the unknown-command handler (which also answered with a spurious error).
Changed
- Roll up sso v1.26.0 + theta-agent v1.3.0 — gitlinks point at the version-tagged commits for both submodules (sso-manager-node → 8a9de94, theta-agent → 52379c2). Full changelogs: sso, theta-agent.
[v1.35.18] - 2026-08-04
Changed
- Sync proxy + jump-host gitlinks to their version-tagged commits — proxy v1.33.0 and jump-host v1.18.0 bumped their package.json to match their tags; this release picks up those corrected gitlinks so a fresh deploy reports the matching versions.
[v1.35.17] - 2026-08-04
Added
- Group & Permission Model spec — canonical documentation of the hierarchical group schema (
god_admin,{site}_super_admin, per-site/per-resource host+appadmin/access/<capability>groups, metaeveryone/{site}_everyone), the inheritance resolver, Directory-only group management, multi-site isolation, host-side SSSD GID mapping (groups aregroupOfNames, nogidNumber), downstream-app consumption, and migration from the legacyapp_*groups. See GROUPS.md. - sso v1.25.0 — the resolver + schema implemented in the SSO (see its changelog); the standalone Groups page removed.
[v1.35.16] - 2026-08-04
Added
- theta-proxy + theta-jump as first-class managed host resources — the bootstrap now seeds them as managed
host-kind resources in the Directory (in addition to the existing stack host and its service entries), so a fresh install shows them as hosts.
[v1.35.15] - 2026-08-04
Fixed
- theta-agent re-install failed with "Text file busy" — setup.sh copied the prebuilt binary over a running agent service, which cp refuses. It now stops the service before copying.
[v1.35.14] - 2026-08-04
Fixed
- The recurring
/vault403 "permission denied" is actually dead this time — it was never a policy problem. sso's/api/vaultproxy declared its request hook with http-proxy-middleware v3 syntax (on: { proxyReq }) while the app installs HPM v2, which silently ignores the unknown key — soX-Vault-Tokenwas never injected and every vault call reached OpenBao unauthenticated. All the policy work of v1.35.10/v1.31.1 was correct and is unchanged; the requests just never carried a token. Ships as sso v1.23.0 (see its changelog for the companionfixRequestBodyheader-ordering fix and the initORM schema heal that unbreaks the plugin scheduler on upgraded databases).
Added
- OpenBao token lifecycle — nothing expires by surprise anymore.
- New
theta-svctoken role (periodic 768h):SSO/PROXY/JUMP_VAULT_TOKENare now minted through it instead of as plain orphan tokens with a hard ~32-day death date.ensure_tokenrenews periodic tokens on everysetup.shre-run and detects, revokes, and re-mints valid-but-non-periodic tokens from older installs (detection is the token'srole— OpenBao token lookup does not expose aperiodfield). - New
bao-renewersidecar (docker-compose): renews the three service tokens every 12h while the stack runs, logging each result. Recreated on everysetup.shrun so it always holds the current tokens. - New
sso-apptoken role (periodic 768h): external-app tokens minted from the vault UI go through it instead of the broker's 24h role, and sso now stores each app token's accessor and auto-renews it (boot + every 6h) — a downstream app's credential stays valid as long as sso runs, with no renewal code in the downstream app. sso-brokerpolicy gainedupdateonauth/token/create/sso-appandauth/token/renew-accessor/revoke-accessor/lookup-accessor.docs/secrets.mdrewritten around the new lifecycle (roles table, renewal layers, disaster recovery).
- New
[v1.35.13] - 2026-08-04
Added
- sso v1.22.0 — new Agents page (live list of connected theta-agent hosts with CPU/RAM/disk/ZFS/GPU telemetry + online status) and a security fix gating the
/api/agentREST routes. Bumped the sso-manager-node gitlink to v1.22.0.
[v1.35.12] - 2026-08-04
Fixed
- theta-agent crash-looped (
cannot unmarshal !!bool 'true' into []string) — setup.sh's "full control" edit wroteservice_control: true, but that field is a[]stringallowlist, so the agent failed to decode the config and restart-loop. Removed the invalid edit;service_controlnow stays as its allowlist (default[]= deny all) and the operator can list specific services.
[v1.35.11] - 2026-08-04
Fixed
setup.shaborted withUNSEAL_KEY: unbound variableon re-runs — when OpenBao was already unsealed, the unseal block was skipped andUNSEAL_KEYwas never set, so the laterif [[ -n "$UNSEAL_KEY" ]]crashed underset -u. Guarded with${UNSEAL_KEY:-}.
[v1.35.10] - 2026-08-04
Added
--reset-openbao— full clean OpenBao reset for clearing stale policies/tokens (re-inits the store, flushes the Redis vault-token cache). Use when the vault UI shows a recurring403 permission deniedon the secrets list.- sso v1.21.0 — shared secrets: users publish secrets to
secret/shared/<owner>/<slug>and grant read access to other users and apps; plus a durable fix for the recurring vault 403 (broker now always reconciles policy content before serving a cached token). Bumped the sso-manager-node submodule gitlink to v1.21.0.
Fixed
- theta-agent was never installed —
setup.shtried togo buildfrom an incomplete source-file list (omittingexecutor.go/telemetry.go), which failed silently and skipped install. It now installs the prebuilttheta-agent-linux-amd64binary from the submodule and writes config to/etc/theta42/agent.yml(the path the agent actually reads).
[v1.35.9] - 2026-08-03
Fixed
- sso & proxy version strings now match their release tags — The v1.20.2 / v1.32.0 release tags were created but their
nodejs/package.jsonversion fields were left behind (1.20.1 / 1.14.3), so the deployed apps' update-check banner falsely reported a newer version. Bumped submodules to the corrected commits sobuildVersionmatches the deployed tag.
[v1.35.2] - 2026-08-03
Fixed
- Unbound
CFG_CREATE_ALL_HTTPvariable insetup.sh— Fixed unbound variable error during host registration insetup.shwhenensure_secrets_files()is skipped on pre-configured installations.
[v1.35.1] - 2026-08-03
Fixed
- Directory & Configuration UI enhancements — Live Cytoscape graph update on parent/child edge modifications, improved discovery reconciler host matching, updated configuration sidebar layout, relocated discovery and messaging plugins to Directory and Configuration pages.
- Managed Host Target Filter — Filter SSH connection targets in Jump Host to managed hosts only.
[v1.35.0] - 2026-08-02
Added
- Non-interactive theta-agent configuration — Added three
setup.envvariables to control theta-agent installation and configuration without interactive prompts:CFG_THETA_AGENT_ENABLE(default: 1) — Enable theta-agent installationCFG_THETA_AGENT_LDAP_AUTH(default: 1) — Configure LDAP authentication via ldap-clientCFG_THETA_AGENT_FULL_CONTROL(default: 1) — Enable all agent capabilities
Changed
setup.sh: Made theta-agent setup fully non-interactive, driven bysetup.envvariables. Defaults preserve existing behavior (all features enabled).
[v1.34.0] - 2026-08-02
Added
- theta-agent: Added the agent submodule and C2 WebSocket endpoint integrations to the suite.
- PKI Certificates: Integrated PKI certificate generation and management capabilities.
Changed
- Submodules bumped:
sso-manager-nodeupdated tov1.19.2(Includes Discovery graph merge fix).proxyupdated tov1.14.1(Removed invalid documentation copy from Dockerfile).jump-hostupdated tov1.16.1.
setup.sh: Added robust|| truefallback to RedisLASTSAVEandCONFIG GETcommands to gracefully bypass snapshoting if the target container is in a crash-loop.- Docs: Removed all standalone deployment documentation to officially deprecate standalone mode.
- CI/CD: Removed redundant submodule unit test jobs from the main orchestration pipeline.
[v1.33.0] - 2026-08-02
Changed
- Submodules bumped for OpenBao secret integration.
[v1.32.0] - 2026-08-01
Added
- CI/CD: Added robust GitHub Actions CI/CD workflows for the suite.
- Docs: Updated plugin ecosystem documentation.
[v1.31.1] - 2026-08-01
Pairs the sso v1.17.2 post-deploy fixes with the theta-suite half of the
/vault secrets-list 403 fix (the sso-admin OpenBao policy grant that lives
in setup.sh), and rolls the sso-manager-node submodule gitlink to v1.17.2.
proxy (v1.13.1), jump-host (v1.14.1), and ldap-client (v1.23.0) are
unchanged.
Changed (theta-suite)
setup.sh—sso-adminpolicy: added alistgrant on the bare KV mount rootsecret/metadataso an admin can list the top-level dirs in the/vaultUI.secret/metadata/*already covered nested paths, but not the mount root itself — so the secrets list 403'd. (The matching per-user/per-app directory grants ship in sso v1.17.2'svault_broker.js.)setup.sh—ensure_policy: now always (re)writes the policy instead of skipping when it exists.bao policy writeis an idempotent overwrite, so a re-run applies policy edits (like the new grant above) instead of stranding the old HCL with "already exists — keeping."
Changed (submodule gitlinks)
- sso-manager-node:
v1.17.1→v1.17.2— the post-deploy fixes (auto-slug plugins, schedule dropdown,/profilerendering, plugin-edit persistence, nmap in the image, the sso-side/vaultpolicy grants) plus the SMS (VoIP.ms) and Terms-of-Service configuration on/conf. Full changelog below.
Deploy
Operators upgrading from v1.31.0:
git pullandgit submodule update --init --recursive.- Re-run
./setup.sh— required: applies the newsso-adminsecret/metadatalist grant and theensure_policyalways-write refresh (idempotent). Per-user vault policies self-heal on the next/vaultvisit (sso v1.17.2 re-writes them). docker compose build && docker compose up -d— the rebuild installsnmapin the sso image (fixes the nmap plugin "not found" error).
Bundled submodule release notes
sso-manager-node v1.17.2 — post-deploy fixes + SMS/TOS on /conf
Post-deploy fixes from testing the v1.31.0 stack, plus the SMS (VoIP.ms) and
Terms-of-Service configuration the /conf page was missing.
Fixed
- Plugin slug is now auto-generated from the instance name — the New Plugin
modal no longer asks for a Slug (it derives a stable, unique handle from the
name, appending
-2,-3, … on collision). The generated slug still shows in the table and the Edit (read-only) modal.POST /api/pluginsslugis now optional; an explicit slug is still accepted and validated. - Plugin schedule is a dropdown, not a raw cron box: Hourly / Daily / Weekly, plus Custom which reveals the raw 5-field cron input. Stored value is still a cron string, so the server is unchanged.
/vaultsecrets list no longer 403s. The per-user, per-app, and admin OpenBao policies grantedlistonly onsecret/metadata/.../*(nested paths), never on the directory path itself — so listing a directory's contents (which checksliston the directory, e.g.secret/metadata/users/<uid>or the mount rootsecret/metadata) was denied.vault_broker.js'suserPolicyHcl/appPolicyHclnow also grantliston the bare directory path, andensurePolicynow always re-writes the policy (idempotent) so already-createduser-<uid>policies pick up the new grant on the next vault-page visit. The matchingsso-adminmount-root grant ships in theta-suite v1.31.1 (setup.sh), whereensure_policyis likewise made always-write so re-running./setup.shapplies policy edits./profileno longer shows literal{{…}}tags. Three template fragments sat outside thejq-repeat="user"scope, so they rendered raw: the card headerProfile: {{user.uid}}, theMembers of {{user.uid}}'s Grouptab label, and the Admin Actions block's{{#isActive}}/{{#isInactive}}buttons. The header/label are now populated by JS (theMemberslabel already had a setter pointing at a missing id); the Admin Actions block is moved inside the scope so{{uid}}/{{#isActive}}/{{#isInactive}}render and the correct Activate/Deactivate button shows.- Editing a plugin now persists. The Edit modal had been prefilled with the
masked secret values and rendered them as fields, but
PUT /:idonly saves non-secret config — so an edited secret was silently dropped. The Edit modal now shows non-secret fields only (secrets have their own Edit-Secrets modal), removing the confusion. - nmap plugin: "NMAP not found at command location: nmap" — the
nmapbinary was not installed in the app image.Dockerfile.openldapnowapk addsnmapin the runtime stage, andplugins/discovery/nmap.jstranslates the opaque node-nmap spawn-missing error into an actionablelastError.
Added
- SMS (VoIP.ms) configuration on
/conf. The existing VoIP.ms SMS sender (models/sms.js, used for 2FA OTP delivery) was configurable only via env / config files. It now has an SMS card on/conf(API username, DID, API password), saved to OpenBao atsecret/sso-manager/confundervoipms, with the API password masked (********) and leave-blank-to-keep — mirroring the SMTP card exactly.models/sms.jsreadsconf.voipms.*at call time, so a saved change takes effect live without a restart. - Terms of Service editor moved to
/conffrom the admin Overview dashboard, where it never belonged. The sameapp.tos.get/updateflow, the "require all users to re-accept" checkbox, and theapp_sso_admingate (matchingroutes/tos.js's PUT gate) are preserved. The Overview page keeps stats, notifications, and metrics.
[v1.31.0] - 2026-08-01
Roll-up release: bumps the composed submodules to their latest tags so a fresh
git clone + ./setup.sh deploys the SSO Manager plugin system, the /conf
SMTP/OAuth secret masking, and the ldap-client changelog. proxy (v1.13.1) and
jump-host (v1.14.1) were already at latest and are unchanged.
Changed (submodule gitlinks)
- sso-manager-node:
v1.16.1→v1.17.1(the plugin system shipped in v1.17.0, plus the v1.17.1/confsecret-masking hardening). - ldap-client:
v1.1.1→v1.23.0— a CHANGELOG-only release (the newCHANGELOG.mddocumenting v1.1.0/v1.0.0; no code change — the "UI polish" tag message is misleading, the v1.1.1…v1.23.0 diff isCHANGELOG.mdonly).
Deploy
Operators upgrading from a prior release:
git pullandgit submodule update --init --recursive(or a fresh clone).- Re-run
./setup.sh— this is required if you haven't yet applied the v1.30.1sso-brokerOpenBao policy grant forsecret/plugins/*(idempotent; it grants the existingSSO_VAULT_TOKENaccess live, so plugin-secrets storage works). docker compose build && docker compose up -d. Existingconf.discovery.pluginssetups auto-migrate intoPluginInstancerows + OpenBao secrets on first boot of sso v1.17.x.
Bundled submodule release notes
sso-manager-node v1.17.0 — real plugin system (loadable instances + OpenBao secrets)
[1.17.0] - 2026-08-01
A real plugin system: the half-built discovery plugins (statically
configured in sso-secrets.js, only toggleable for cron/enabled) become
configurable, loadable/unloadable plugin instances you manage from a
dedicated Plugins page and the /api/plugins API, with multiple runtime
copies of each type and per-instance secrets stored in OpenBao.
Added
- Plugin instances — a new
PluginInstanceORM model (nodejs/models/plugin_instance.js, Sequelize) is the registry of configured, scheduled plugin copies. Each has apluginType, a uniqueslug(the discovery source name), a cron schedule, anenabledflag (load/unload), non-secretconfig(JSON), and last-run bookkeeping. Multiple instances of the same type are supported. - Plugin registry (
nodejs/services/plugin_registry.js) — generalizes the one-shot discovery-plugin scan inscheduler.js. Plugin types are modules undernodejs/plugins/<category>/<type>.jsexporting a manifest (type,category,name,description,configSchema,validate,run/discover). ExposesgetTypes,getModule,splitConfig(secret vs non-secret),mask, and required-field helpers for the UI/API. - Per-instance secrets in OpenBao (
nodejs/utils/plugin_secrets.js) —configSchemafields flaggedsecret:true(e.g. a ProxmoxtokenSecret, UniFipassword) are stored atsecret/plugins/<instance-id>/conf, never in the DB. The UI only ever sees masked (********) values. Plugins run in-process (BullMQ workers), so they need no OpenBao token of their own — the SSO reads/writes via thesso-brokertoken. Requires theta-suite ≥ v1.30.1 for thesso-brokerpolicy grant onsecret/plugins/*; the API fails-soft with a clear error if absent. /api/pluginsAPI (nodejs/routes/api_plugins.js, replaces the oldroutes/plugins.js) —GET /types, list/get/create/update/update-secrets/ test/load/unload/run/delete/runs. Admin-only (app_sso_admin/app_sso_directory_admin/app_super_admin).- Plugins page (
/plugins,views/plugins.ejs) + nav entry — instance table with New/Edit/Edit-Secrets/Test/Run-now/Load/Unload/Delete, config forms rendered from each type'sconfigSchema. validate("Test" button) on the built-in Proxmox/UniFi/Nmap plugins.
Changed
services/scheduler.jsnow schedules from thePluginInstancetable instead of staticconf.discovery.plugins+ a Redis override hash. Each instance owns a stable BullMQ JobScheduler id (plugin:<instanceId>) so load/unload upsert/remove one schedule without disturbing the rest. Discovery plugins reconcile results under the instance'sslug.- The three discovery plugins (
plugins/discovery/{proxmox,unifi,nmap}.js) gained manifests (configSchema,validate,runalias).nmap'stargetRangeis non-secret; ProxmoxtokenSecretand UniFipasswordare secret. - The
/pluginspage route renders the page instead of redirecting to/directory; the Agents & Scheduler tab was removed from/directory(plugins are now managed on the Plugins page). The/docs/agentslink is aliased to/docs/plugins. docs/plugins.md,docs/vault.md,docs/_config.yml(nav), andAPI.md(Plugin Endpoints section) document the new system.
Legacy migration
On first boot of v1.17.0, if the PluginInstance table is empty and
conf.discovery.plugins has entries, one instance per configured type is seeded
automatically (secret fields copied into OpenBao). After that the static
config is ignored — manage plugins from the UI/API. Idempotent (guarded by the
empty-table check).
Prerequisite
theta-suite ≥ v1.30.1 — re-run ./setup.sh after upgrading so the
sso-broker OpenBao policy is granted secret/plugins/*. Without it, storing
plugin secrets fails with a clear error.
sso-manager-node v1.17.1 — mask SMTP/OAuth secrets + leave-blank-to-keep on /conf
[1.17.1] - 2026-08-01
Hardens the runtime SMTP/OAuth secret handling on the /conf admin page to
match the plugin-secrets discipline: the SMTP password and OAuth JWT secret are
no longer returned in cleartext by GET /api/conf or round-tripped through the
form. They remain saved in OpenBao at secret/sso-manager/conf at runtime
(unchanged) — only how they're surfaced to the admin changes.
Changed
GET /api/confnow maskssmtp.passandoauth.jwtSecretto********(was: returned in cleartext). Non-secret fields (host, port, user, from, secure, issuer, token lifetimes) are returned as before.POST /api/confnow treats a blank or********secret-field submission as "keep the current stored value" — so an admin editing the From address or token lifetimes no longer has to re-enter (or leak) the SMTP password / JWT secret. Only a genuinely new, non-blank value overwrites. The preserved values are re-applied to liveconfimmediately, as before./confpage (views/conf.ejs): the Password and JWT Secret fields carry a "leave unchanged to keep the current value stored in OpenBao" hint; the page copy notes secret fields are masked. No JSON-textarea editing is involved — SMTP is and remains configured through structured form fields.
Notes
- SMTP (and OAuth) config was already saved to OpenBao at runtime before
this release (via
POST /api/conf→baoConf.set('sso-manager/conf'), and overlaid back at boot bybao-conf.init). This release closes the cleartext-exposure gap; it does not move the storage path. - No theta-suite policy change required —
secret/sso-manager/confwas already granted to thesso-brokerpolicy.
ldap-client v1.23.0 — CHANGELOG-only (no code change)
Adds a CHANGELOG.md documenting v1.1.0 (app_super_admin / app_jump_admin
group support in SSSD access filters; the sso/jump-host TLS-validation
divergence) and v1.0.0 (initial SSSD LDAP auth release). No source changes vs
v1.1.1; the v1.23.0 tag commit only adds this file.
[v1.30.1] - 2026-08-01
Prerequisite release for the SSO Manager plugin system (shipped in
sso-manager-node v1.17.0). Grants the sso-broker OpenBao policy access to the
new per-instance plugin secrets namespace so the SSO can store plugin secrets in
OpenBao instead of sso-secrets.js.
Changed (theta-suite orchestration)
setup.sh: addedsecret/data/plugins/*(CRUD+list) andsecret/metadata/plugins/*(list/read/delete) to thesso-brokerpolicy HCL.ensure_policy sso-brokeris idempotent, so re-running./setup.shimmediately grants the existingSSO_VAULT_TOKENaccess tosecret/plugins/*(policies are evaluated live; the token keeps its id). The SSO side fails-soft with a clear error if this grant is absent.- Docs:
docs/secrets.md(new "Plugin secrets" section +sso-brokerpolicy row) anddocs/architecture.md(sso-manager access row) now listsecret/plugins/*.
The plugin system itself (configurable plugin instances, load/unload, UI/API, multi-copy, secrets in OpenBao) is in sso-manager-node v1.17.0; theta-suite will bump its submodule gitlink to that release next.
[v1.30.0] - 2026-08-01
The project is renamed theta-env → theta-suite — it has grown from a
docker-compose wiring two projects into an integrated suite of four
applications around a shared OpenBao secrets store, and the name should reflect
that. The GitHub repository is renamed theta42/theta-env →
theta42/theta-suite (old URLs redirect), and the docs site moves to
https://theta42.github.io/theta-suite/.
Changed (theta-suite orchestration)
- Renamed theta-env → theta-suite across the superproject:
docs/_config.yml(title+baseurl: /theta-suite+ repo URLs),README.md,setup.sh(incl. theTHETA_SUITE_REEXECEDself-update sentinel),docker-compose.yml,bootstrap/bootstrap.js,.github/workflows/lint.yml,config.example/*,docs/robots.txt, all docs pages, and this changelog. - Compose project name note: docker compose derives the project name from
the clone directory, so named volumes follow it (
<project>_openbao-data). A freshgit cloneoftheta-suiteuses thetheta-suiteproject name; an existing deployment that keeps itstheta-envdirectory keeps itstheta-env_*volumes — no data migration is required, just don't mix the two. - Docs site baseurl is now
/theta-suite, matching the renamed repo's GitHub Pages URL.
Docs
architecture.mdrewritten. Replaced the outdated "The two containers" / "The three repos" framing with the actual topology — four always-on services (openbao,sso-manager,proxy,jump-host) plus theldap-clienthost-enrollment tool — a real diagram, a full Secrets (OpenBao) section (central store, scoped per-app policies/tokens, the@simpleworkjs/bao-confboot overlay, per-user KV, external-app minting), and an OpenBao-aware "how config reaches the apps". Removed the LDAP-"legacy apps" wording (direct LDAP binds are first-class: Linux hosts PAM/SSSD, sudo, SSH keys).index.md— integrated-suite framing; added Central secrets (OpenBao) and ldap-client to "What you get" and "Related projects".standalone.md+README.md— standalone is now framed as an advanced opt-in; the integrated./setup.shstack is the supported path.
Submodule bump
- sso-manager-node → v1.16.1 — fixes the 401 on
/confand/vaultfor a logged-in admin. Both view routes 401'd because this app's auth-token is a header set by client JS (localStorage), not a cookie, soreq.useris undefined on a browser navigation; the routes now render the shell and gate client-side (app.auth.forceLogin), with/api/conf+/api/vaultstill enforcing auth + OpenBao scope server-side. See the sso v1.16.1 release.
[v1.29.0] - 2026-08-01
Two fixes for a fresh ./setup.sh install, plus the SSH jump host promoted
from an opt-in component to a core part of the stack.
Fixed (theta-suite orchestration)
setup.sh— fresh installs aborted silently right afterMinting per-app OpenBao tokens. Theenv_gethelper'sgrep | cutpipeline returns non-zero underset -euo pipefailwhen.envexists (it's created earlier by the root-VAULT_TOKENenv_upsert) but a given app-token key is absent — the normal first-run state. The unguardedexisting="$(env_get ...)"then trippedset -eand killed the script before any token was minted.env_getnow always returns 0 (|| true), so "key absent" resolves to empty and the run continues through token minting, the SSO/proxy bring-up, and the jump host. Reproduced + verified the fix under the exact fresh-install condition.setup.sh—JUMP_VAULT_TOKENis now always minted (jump host is core; the mint was already unconditional, this just documents it).
Changed (theta-suite orchestration)
- jump host is no longer optional — it is built + started on every run,
with no
CFG_JUMP_HOST_ENABLEDflag.docker-compose.yml: removedprofiles: ["jump-host"]from thejump-hostservice sodocker compose upincludes it unconditionally. The test-onlyldap-test-hostdownstream fixture keeps an opt-in profile, renamedjump-host→ldap-test(docker compose --profile ldap-test up).setup.sh:SUBMODULESalways includesjump-host; the build/start + host-register + summary lines for the jump host are no longer wrapped in aJUMP_ENABLEDguard; theCOMPOSE_PROFILESexport is gone.bootstrap/bootstrap.js: jump-host provisioning (mint API token + writejump-secrets.js+ mirror into OpenBao) and its directory service record now run unconditionally — noCFG_JUMP_HOST_ENABLEDgate.setup.env.example/docs/index.md/docs/quickstart.md: dropped the "optional / enable withCFG_JUMP_HOST_ENABLED=true" wording; theCFG_JUMP_HOSThostname override +JUMP_SSH_PORTremain.
[v1.28.0] - 2026-08-01
OpenBao becomes the central secrets store for the whole stack. Every app now
loads its secrets from OpenBao at boot via the new
@simpleworkjs/bao-conf package;
end users get personal per-user secret storage through the SSO UI; external
apps get scoped secret/apps/<app>/* access. setup.sh mints the policies
and scoped tokens, bootstrap.js writes generated creds into OpenBao, and a
new docs/secrets.md documents the architecture.
Changed (theta-suite orchestration)
setup.sh— after the KV-v2 enable, a new idempotent block writes four OpenBao policies (sso-broker,sso-admin,proxy,jump-host) via heredocs, asso-brokertoken role (allowed_policies_globuser-*/app-*, 24h period), and mints scopedSSO_VAULT_TOKEN/PROXY_VAULT_TOKEN/JUMP_VAULT_TOKEN(orphan, renewable, reused fromsetup.envif still valid).seed_app_confseedssecret/{sso-manager,proxy,jump-host}/conffrom the operator-edit/config/*-secrets.jsfiles on first run. The bootstrap exec now passes the rootVAULT_ADDR/VAULT_TOKENfor seeding. The root token stays insetup.envfor maintenance only — it is never passed to a service container.bash -n+shellcheckclean.docker-compose.yml—sso-manager/proxy/jump-hostgetVAULT_ADDR=http://openbao:8200andVAULT_TOKEN=${SSO|PROXY|JUMP}_VAULT_TOKEN:-;proxy/jump-hostgaindepends_on: openbao: service_started. compose config valid.bootstrap/bootstrap.js—baoPut()writes the generated OAuth client creds tosecret/proxy/confandsecret/jump-host/conf(POST replaces; the full file object), so OpenBao — not the on-disk/config/*-secrets.js— is authoritative after first boot. Fail-soft.node --checkclean.docs/secrets.md(new) — the full secrets architecture: load path, policy/token table, thesso-brokerrole, seeding, end-user personal secrets, external-app convention (curl + Nodebao-confexamples), operator rotation, backups. Linked from the README and the docs nav (_config.yml).- README — Configuration section rewritten (OpenBao authoritative, link to
docs/secrets.md); Secrets-backup section adds theopenbao-datavolume.
Submodule bumps
sso-manager-node→ v1.16.0 (was v1.15.2-era).proxy→ v1.13.1 (was v1.12.1; passes through v1.13.0).jump-host→ v1.14.1 (was v1.11.0-era; passes through v1.14.0).ldap-clientunchanged (v1.1.1).
sso-manager-node — v1.16.0
OpenBao becomes the central secrets store for the theta42 stack, and the SSO Manager becomes its broker. This is the SSO's half of the move: it loads its own secrets from OpenBao, mints scoped tokens for users and external apps, and exposes a fixed, role-scoped personal-secrets UI.
Changed
- Secrets now load from OpenBao at boot via
@simpleworkjs/bao-conf, which
deep-merges
secret/sso-manager/confover the file-loaded config (replacing the oldutils/conf_manager.js, which did a shallow-per-key merge).bin/wwwrunsbao-conf.init()aftermodels.initORM()and beforelisten. Fail-soft: if OpenBao is unreachable, boot continues fromCONF_SECRETS. The SSO authenticates with a scopedVAULT_TOKEN(policysso-broker), never the root token. The admin Configuration UI (/api/conf) now writes throughbao-conf.set('sso-manager', …). /api/vaultproxy reworked — the old endpoint was an ungated pass-through that never injected anX-Vault-Token(so the UI was both ungated and broken). It is nowmiddleware.auth→scopeGuard→ a token-injecting proxy.scopeGuardresolves a per-user (user-<uid>) or per-admin (sso-admin) token via the newutils/vault_broker.js(Redis-cached, minted through thesso-brokertoken role) and enforces a path prefix as a second layer on top of the OpenBao policy. The clientauth-tokenis stripped; only the server-minted token reaches OpenBao.- Vault UI reworked and renamed (
views/vaultwarden.ejs→views/vault.ejs; the/vaultroute is nowmiddleware.auth-gated). Non-admin users see only theirsecret/users/<uid>/namespace; admins get free-form path entry acrosssecret/plus an Apps tab to mint scoped tokens for external apps (secret/apps/<name>/*, shown once with copy +curlconvention). - Bumped package version to track the release tag.
Removed
nodejs/utils/conf_manager.js(replaced by@simpleworkjs/bao-conf).nodejs/views/vaultwarden.ejs(renamedvault.ejs).
Security
- Committed-secrets remediation.
config/sso-secrets.js(LDAP bind password, SMTP,oauth.jwtSecret) andnodejs/test_plugins.js(a hardcoded Proxmox root API token and a UniFi password) were tracked on master. They are now untracked + gitignored (config/*-secrets.js), andtest_plugins.jsis deleted;config/proxy-secrets.js.exampleadded as a placeholder template. The secrets remain in git history — rotation at the providers is the real remediation and is the operator's to perform. OpenBao is now the authoritative store; the local files are seed artifacts only.
Note: sso releases v1.12.0–v1.15.2 were tagged from merge PRs without corresponding
CHANGELOG.mdentries or GitHub releases; v1.16.0 resumes the changelog.
proxy — v1.13.1 (via v1.13.0)
v1.13.1 — 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 proxy 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 from
CONF_SECRETS), matching the documented contract. The theta-suite stack is unaffected (it always sets a scopedVAULT_TOKEN).
- continue from
v1.13.0 — Changed
- Secrets now load from OpenBao at boot via
@simpleworkjs/bao-conf, which
deep-merges
secret/proxy/confover the file-loaded config. The proxy authenticates to OpenBao with a scopedVAULT_TOKEN(policyproxy— read-only on its own path), never the root token. Because the OIDCclientSecretis captured at require time insidecreateOidcClient(duringrequire('../models'), whichrequire('../app')triggers transitively),bin/wwwnow defersrequire('../app')until afterbao-conf.init()resolves. Fail-soft: if OpenBao is unreachable, boot continues fromCONF_SECRETS. Theconfig/proxy-secrets.jsfile is now an operator-edit seed artifact (gitignored); OpenBao is authoritative. - Bumped package version to track the release tag.
jump-host — v1.14.1 (via v1.14.0)
v1.14.1 — 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-suite stack is unaffected (it always sets a scopedVAULT_TOKEN).
v1.14.0 — 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. - Bumped package version to track the release tag.
[v1.27.2] - 2026-08-01
- Updated
proxyto v1.12.1 (Dependabot security/maintenance bumps).
proxy — v1.12.1
Changed
- Bumped
body-parser2.2.2 → 2.3.0 (Dependabot #175). - Bumped
ejsandbrace-expansion(Dependabot #179, security maintenance).
[v1.27.1] - 2026-08-01
- Added automated integration tests for the environment (
test-integration.sh). - Updated
sso-manager-nodeto v1.15.2 (Directory UI tab styling fixes and Vault documentation).
[v1.27.0] - 2026-08-01
- Updated
sso-manager-nodeto v1.15.0 (UI/UX improvements and structured conf page).
[v1.26.0] - 2026-08-01
- Made OpenBao production-ready by using a persistent file backend, enabling
IPC_LOCK, and dynamically generating a robust config file. - Automated OpenBao initialization, unsealing, and secrets seeding via
setup.sh.
[v1.25.0] - 2026-08-01
- Added OpenBao (Vault) container for secrets management and native UI proxying.
- Updated
sso-manager-nodeto v1.14.0 (Discovery and Vault integration). - Updated
proxyto v1.12.0.
Unreleased
[1.23.0] - 2026-07-31
Submodules bumped
jump-host — v1.13.0
Changed
- Title changed to "SSO Manager" — the jump-host web UI now presents itself as "SSO Manager" in the navbar and page title, matching its role as the unified access portal for both services and hosts.
sso-manager-node — v1.13.0
Changed
- Directory page cleaned up — removed the parent badge and slug display from the directory table; resource names now align with the badges above for a cleaner, more compact layout.
- Users list SSH key column fixed — users with multiple SSH keys no longer show multiple checkmarks; the column now shows a single checkmark indicating "has key" regardless of key count.
proxy — v1.11.0
Changed
- Permissions, Users, and Groups pages converted to table layouts — card grids replaced with striped tables for better scanability and alignment. Users page adds per-field validation error display alongside the summary message.
- Groups page auto-refreshes — adding or removing a group now triggers an explicit reload, ensuring the list stays in sync without manual refresh.
[1.22.0] - 2026-07-31
Submodules bumped
jump-host — v1.12.0
Added
- TUI host picker with colors: ANSI-colored terminal UI with box-drawing header, cyan/magenta/green title treatment, per-row coloring (cyan hostnames, blue IPs), environment badges (red PROD / dim DEV), green inverse selection highlight with "◄ SELECTED ►" indicator, yellow filter text, and a footer separator with quick-select hint.
sso-manager-node — v1.12.0
Changed
- Catalog page (
/) redesigned: Removed the portal banner; "My Access" section now has tabs separating Services and Hosts; icons support both Font Awesome classes and image URLs (http/https). - Profile page redesigned as a single card with tabs: Password reset is now a modal button; "My groups", "My Services", "Security & Usage Stats", and "Members of X's group" are now tabs on the main profile card instead of separate cards; metrics display fixed to properly load and show service usage data.
proxy — v1.10.0
Changed
- Permissions page: Converted from card grid to table/list layout with columns: Subject, Scope, Domain, Role, Actions.
- Users page: Converted from card grid to table/list layout; form validation now shows both a summary message AND per-field error messages with visual highlighting.
- Groups page: Added automatic refresh after adding/deleting groups to ensure new entries appear immediately.
[1.21.0] - 2026-07-31
Submodules bumped
Operational note — the SSO image now builds slapd from source. OpenLDAP's
nestgroupoverlay (nested groups) exists only on master; no 2.6.x release ships it.Dockerfile.openldaptherefore compiles OpenLDAP from a pinned commit, which makes the SSO image slower to build and pullspw-sha2from contrib. Two consequences worth knowing:
- Master ships LMDB 1.0.0, whose on-disk format is mutually unreadable with the 0.9.x in 2.6.x (
MDB_INVALID: File is not an LMDB file). Moving an existing/var/lib/ldaponto this image is aslapcat->slapaddreload, not a restart.- There is a
TODOto drop the whole from-source stage oncenestgroupships in a release. The entrypoint already probes fornestgroup.soand the app keys offapp_ldap__nestedGroupsServerSide, so that swap needs no other changes.
sso-manager-node — v1.11.0
Added
- End-user catalog at
/— the first ungated nav item; previously every nav entry was admin-only and a normal user had no signposted destination. Search/filter, per-kind icons, and a how to reach it block per card: the URL for a service, the SSH invocation for a host (using the jump-hostuid_-_slug@hostgrammar whendirectory.jumpHostis set). - Self-service access requests —
/api/access-requests(create, list own, list decidable, approve, deny, withdraw) with approve/deny queues on the catalog. Approving performs the LDAP group add, so LDAP stays the access-control truth. Requests target a resource's_accessgroup, never_admin. Replaces the "coming soon" stub. - Admin access visibility — an Access column on the directory table (member/group counts, flagging links whose LDAP group was deleted) and a "what can this user reach" lookup, the reverse question that previously had no UI at all.
- Nested LDAP groups.
groupOfNames.memberalready accepts a group DN, so nesting needs no schema — what it needs is resolution, which no released OpenLDAP performs. Server-side via the pinned-masternestgroupoverlay; client-side the app computes the closure itself (cycle-detected, depth-capped) against any other server.PUT/DELETE /api/group/:group/nested/:child,GET /api/group/:group/effective, and a Nested tab on each group card. app_super_adminis now seeded (it never was) and nested intoapp_sso_admin/app_sso_invite/app_sso_oauth_admin, so the privilege is real LDAP membership visible to SSSD and sudo rather than a special case in app code. Resource creation nestsapp_super_admin-><slug>_adminand<slug>_admin-><slug>_access.- Resource metadata
iconandtagline, collected on the admin form with a live icon preview.
Fixed
GET /api/discovery/mereturned onlyisPublicresources for every human caller — it readreq.user.groups, which does not exist (req.usercarriesmemberOf), so the empty list failed open. "My Services" was blank for everyone, andisDirectoryAdmin()was false even for real directory admins.- The portal's "Discover More Services" was dead for every non-admin — it called the admin-gated endpoint and swallowed the 403 into an empty array.
- Services reported no address —
/mehad reimplementedgetMyAccesswithout its parent-walking resolution, so a service reached at its host's IP resolved to nothing. GET /api/user/mederivedisAdminfrommemberOf, which is only transitive withnestgroup; against a stock server an admin holding their group via nesting lost the entire admin UI while still passing every server-side check.utils/permission.js'sbyGroupsaw only direct membership.- Adding an existing group member, and removing a group's last member, both returned bare 500s; now 409s that explain themselves.
DELETE /api/directory-admin/resources/:iddeleted the resource before its edges and group links, orphaning rows on a mid-way failure./api/directory-admin/audit-logsshelled out totailviaexecSync; replaced with a bounded async read.- Broken
api.htmllink in the published docs.
Changed
@simpleworkjs/directory-schema->^1.1.0, declaring ten metadata keys the admin form always wrote but the schema never listed. Undeclared keys are dropped for non-admin callers — which blanked the portal'sOS:field, hid every service's port, and left machine tokens unable to read the port mapping the firewall consumer exists to render.
ldap-client — v1.1.1
Fixed
- Sets
ldap_group_nesting_level = 5so SSSD walks nested groups itself when pointed at a server withoutnestgroup. Against the SSO's bundled slapd the existingmemberof=access filter is already transitive, so SSH login inherits nesting for free. The explicitapp_super_adminclause is kept, to keep super-admin login working against a directory predating the new nesting.
[1.20.0] - 2026-07-30
Added
setup.shpersistsSSO_GIT_COMMIT/PROXY_GIT_COMMIT/JUMP_GIT_COMMITinto./.env(newenv_upserthelper), whichdocker composeauto-loads on every future invocation in this directory. Previously these were onlyexported for the current shell, so an ad-hocdocker compose up --build <service>run later (outside a fullsetup.shrun) would build with an emptyGIT_COMMITarg — and since each submodule's checked-out.gitis a pointer file, not a real repo, the in-container git fallback can't resolve it either, so the image silently baked "unknown" as its commit hash..gitignore's.envcomment updated to describe this (it was previously labeled "legacy, no longer used").
Submodules bumped
- jump-host
v1.10.2->v1.11.0 - ldap-client
v1.0.0->v1.1.0 - proxy
v1.8.0->v1.9.0 - sso-manager-node
v1.9.0->v1.10.0
sso-manager-node — v1.10.0
Added
app_super_admincross-app group: members are full admins here regardless ofapp_sso_adminmembership. The same group is now also recognized by proxy and jump-host, and byldap-client's SSSD access filter (SSH login on every host).
Changed
- Renamed the Executive page to Overview (route, view,
/api/metrics/overview, nav label, docs)./executivekept as a 301 redirect.
proxy — v1.9.0
Added
app_super_admincross-app group recognized as a global admin (conf.auth.adminGroups).
Changed
- Users and Permissions pages: the always-visible sidebar "Add" forms are now an "Add User"/"Add Permission" button that opens an
app.modaldialog, matching the hosts.ejs convention. - Let's Encrypt ACME account key now defaults to the already-persisted
/datavolume instead of a CWD-relative path (./le_key.cert->/app/le_key.certin the container), which was lost on every image rebuild.
jump-host — v1.11.0
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.
Changed
- Dashboard's stat boxes and Top hosts/Top users cards moved to the Audit page. "All hosts" renamed to "My hosts".
ldap-client — v1.1.0
Added
app_super_admincross-app group now grants SSH login access to every host (ldap_access_filter+ldap_access_groups), matching the same group's admin rights in sso-manager-node, proxy, and jump-host. Sudo is not yet extended to super admins (ldap_sudo_search_filterremains non-functional on this SSSD version — pre-existing, documented gap).
[1.19.0] - 2026-07-30
Added
- New
ldap-clientsubmodule +ldap-test-hostservice (jump-hostcompose profile): a real SSSD + AuthorizedKeysCommand LDAP-joined downstream host for testing jump-host's actual key-injection -> upstream-connect flow end-to-end against this stack's own local LDAP, instead of a container with a manually-dropped public key inauthorized_keys. Verified live (SSH CLI and WinSCP) through jump-host'suid_-_targetgrammar.
ldap-client — v1.0.0 (first tagged release)
Added
- Docker test fixture (
Dockerfile+entrypoint.sh): Ubuntu 22.04 + sssd + sshd, no systemd required.
Fixed
Building that fixture surfaced three real bugs that would break login on any deployment, not just the test fixture:
sssd.conf.mousedldap_bind_dn/ldap_bind_pw, which aren't real SSSD options — corrected toldap_default_bind_dn/ldap_default_authtok(_type).sssd.conf.mohad no explicitservices =list, so SSSD started only its backend, never the nss/pam responders —getent passwd <ldap-user>silently failed even with the domain reachable.ldap-ssh-key.sh'smemberoffilter was missing thecn=prefix on the group name, so the AuthorizedKeysCommand script always returned zero keys for a correctly-provisioned user — no error, just silently nothing.
sso-manager-node — v1.9.0
Added
- Directory modal's Associated LDAP Groups tab now supports full membership management: view, add, and remove members/owners of each associated group directly from the tab.
app.util.revealItem()(sharedapp-base.js): scrolls a just-added/-edited element into view and flashes its background.
Changed
- Groups page's search/sort bar is now sticky while scrolling.
- Directory table: Kind/Name/Env/Host merged into a single "Resource" column.
proxy — v1.8.0
Added
- Users backed by SSO/OIDC login are now marked "External (SSO)" and read-only (password-change hidden client-side,
PUT /password/:usernamerejects with 403 server-side). Redis user-backend only.
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.
jump-host — v1.10.2
Changed
- Dashboard, Sessions, and Audit pages now match sso-manager-node/proxy's page width.
- Audit's nav entry is now admin-gated (
groups: ['admin']).
Bumped
- sso-manager-node -> v1.9.0
- proxy -> v1.8.0
- jump-host -> v1.10.2
- ldap-client -> v1.0.0 (new submodule)
[1.18.0] - 2026-07-28
Changed
Cross-app API-token self-service UI unification: all 3 apps now share the
same card-grid list, "+ New Token" modal-based create flow, app.modal-based
secret reveal, and Edit modal (with real created-by/on audit metadata).
sso-manager-node — v1.8.2, v1.8.3
v1.8.2
Fixed
- Creating a new OAuth integration didn't reliably show the "save this client secret now" reveal modal —
saveResource()calledapp.modal.close()immediately before conditionally showing the secret viaapp.modal.open().app.modalis a singleton, andclose()immediately followed byopen()collides with Bootstrap's hide-transition guard. An interveningawait loadResources()made this race unlikely to lose in practice, but not guaranteed to — found while fixing the same, guaranteed-to-lose bug in jump-host and proxy's API-token create flows.
v1.8.3
Changed
profile.ejs's self-service API-token UI unified ontoapp.modal, matching the pattern already shipped this round indirectory.ejs, proxy, and jump-host: the static#secretModal/#editModalelements are retired in favor of the sharedapp.modalsingleton, the always-visible inline create-form card becomes a "+ New Token" button + modal, and badge classes switch frombg-*totext-bg-*.- Checkmark-flash copy feedback (silently broken by FontAwesome's
<i>→<svg>replacement) replaced with toast-basedcopyFieldValue, matching proxy and jump-host.
proxy — v1.7.0
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/:idroute 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 beforeapp.modal.open()(to show the secret) in the same tick; sinceapp.modalis a singleton, that collided with Bootstrap's hide-transition guard and the reveal modal silently never appeared.
jump-host — v1.10.0, v1.10.1
v1.10.0
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).
v1.10.1
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).
Bumped
[1.17.0] - 2026-07-28
Added
- proxy's host modal now has a footer (created/updated-by/on metadata) and a linkable
/hosts/{host}URL, migrated onto the same sharedapp.modalcomponent as sso-manager-node's resource modal — continuing the entity-modal standardization across the stack.
Fixed
- proxy: the Let's-Encrypt challenge-type/wildcard-matching visibility logic could stop reacting to the hostname field after the first Add/Edit host, and the SSO allow-list autocomplete could go empty starting on the second Add/Edit — both were DOM-rebuild timing bugs in the same class as the resource-modal fixes already shipped.
- sso-manager-node: the resource modal's "Associated LDAP Groups" autocomplete went empty after the first Add/Edit — same DOM-rebuild timing bug, now fixed.
Bumped
[1.16.0] - 2026-07-28
Added
- "Quick Jump" copy-to-clipboard section on the jump-host dashboard — one-click-copy SSH commands (interactive-picker mode, plus a per-host
uid_-_targetgrammar-mode command) instead of having to remember/reconstruct the format by hand.
Fixed
- jump-host audit records for a failed downstream connection only ever said
upstream-unreachable, with no way to tell a network-layer failure from an auth failure — the real error (ECONNREFUSED, ETIMEDOUT, an ssh2 auth-failure message, etc.) is now captured and shown as a tooltip on the audit table's fail badge.
Bumped
- jump-host -> v1.9.0
[1.15.0] - 2026-07-28
Fixed
- sso-manager's Directory data (every site/host/service/oauth-client resource and their relationships/LDAP-group associations) had no persistent volume —
@simpleworkjs/ormfell back to./config/inventory.sqlite(relative to the app's/appcwd) wheneverconf.ormwasn't set, which sits in the container's ephemeral writable layer, not any mounted volume. Every container recreate (docker compose up --build,down/up, an image rebuild) silently wiped the entire Directory Management page.setup.sh's generatedsso-secrets.js(and the example template) now setorm: { dialect: 'sqlite', storage: '/data/inventory.sqlite' }, co-locating it with the already-persistedsso-datavolume (where Redis lives). Existing deployments: this repo doesn't rewrite an operator's existingconfig/sso-secrets.js(re-runningsetup.shleaves it untouched by design) — add theormblock above manually, and copy the container's current/app/config/inventory.sqliteto/data/inventory.sqlitebefore recreating the container, or the existing Directory data will be lost on the next recreate instead of migrated.
Bumped
- sso-manager-node -> v1.8.0
[1.14.0] - 2026-07-28
Fixed
- jump-host's Redis had zero persistence (
--save '' --appendonly no, no data-dir volume) — every container rebuild/recreation (including asetup.shre-run) silently wiped all sessions, in-flight OAuth logins, and any admin-created API token. This is the root cause of the reported "re-running setup.sh breaks OAuth with jump" — the jump-host container gets recreated, and any token or in-flight login vanished with it, while proxy was unaffected because its Redis was already persisted. Now jump-host's Redis persists (AOF + periodic RDB) to/data, mounted as a new named volume,jump-redis-data. Verified live: minted a PAT, force-recreated the container, confirmed the same PAT still authenticated afterward.
Changed
docker-compose.yml: added thejump-redis-datavolume, mounted at/dataon thejump-hostservice.
Bumped
- jump-host -> v1.8.1
[1.13.0] - 2026-07-28
Fixed
Found via feedback on a fresh install:
- jump-host's OAuth client had no parent in the Directory.
seedDirectory()only ever linked the proxy's OAuth client; jump-host's own (minted byprovisionJumpHost) was created but never passed through, so it never got aResourceEdge. Existing deployments self-heal on the nextsetup.shrun. - TUI-mode SSH connections (bare
ssh user@host) could drop with "PTY allocation request failed" / "shell request failed" — a session-listener race in jump-host, same class of bugrunGrammaralready had a fix for. - Every form submit briefly showed literal HTML instead of a loading spinner, across all three apps.
POST /api/user/andPUT /api/user/passwordhad no success message — a green notification with nothing in it right after adding a user.- The login page gave no explanation for why the user landed there when redirected mid-OAuth-flow.
Changed
- Directory: tree view is now the only view; clicking a resource's name opens its detail modal.
Bumped
No setup.sh or compose change. Also confirmed (no fix needed): the Let's Encrypt ACME account key persists correctly across container rebuilds — lua-resty-auto-ssl's Redis storage adapter writes through the bundled Redis, which is started with --appendonly yes into /data, mapped to the persisted proxy-data volume. Only an explicit docker-compose down -v / volume removal would lose it (which is also what's required, and expected, on a domain change).
[1.12.0] - 2026-07-28
Bumped
- sso-manager-node -> v1.6.3 — fixes the root cause of a real "lost user" report:
routes/group.jsnever invalidated the User cache on membership changes, so an account added to theapp_sso_service_accountmarker group (which hides accounts from the Users page's People tab) could look like it had vanished for up to 5 minutes — and, separately, could be added to that group with no warning at all. Both fixed; see the linked release for detail.
No setup.sh or compose change.
[1.11.0] - 2026-07-28
Added
test/check_jump_ldap_tls.js, wired into theLintworkflow: a static consistency check on the jump-secrets.js templatebootstrap.jsgenerates, so theldap://+tlsOptionsmistake that broke every SSH login in 1.10.0 fails CI before it ever reaches a real deployment again.- A static "no native
alert()/confirm()/prompt()" check is now part of all three apps' own test suites (they block all further browser events on the page — see 1.9.0/1.10.0's release notes).
Bumped
- sso-manager-node -> v1.6.2 — fixes
DELETE /api/oauth/client/:id(client.remove is not a function, a genuine 500 masked by tests that never checked the response status), plus the regression test above. - proxy -> v1.5.2 — the regression test above.
- jump-host -> v1.7.1 — the regression test above.
No setup.sh or compose change.
[1.10.0] - 2026-07-27
Fixed
bootstrap/bootstrap.js's jump-secrets.js template now points jump-host atldaps://sso-manager:636, notldap://sso-manager:389. The plain-port URL combined with jump-host'stlsOptionsmadeldaptsattempt implicit TLS against a port serving plaintext LDAP — slapd dropped every connection before any LDAP message parsed, so SSH password login failed for every account, with any password, indistinguishable from a wrong credential. Root-caused by standing up a local jump-host, editing its config, and callinggetUser/checkPassworddirectly inside the container. Existing deployments must edit./config/jump-secrets.jsthemselves (this template only affects fresh bootstraps) — see theta42/theta-suite#99. Companion defensive fix: simpleworkjs/ldap v1.0.2 now rejects thisldap://+tlsOptionscombination outright.
Bumped
- jump-host -> v1.7.0 — adds self-service API tokens (create/list/rotate/revoke from its dashboard); jump-host previously had none.
No setup.sh or compose change.
[1.9.0] - 2026-07-27
Bumped
Both apps had every native alert()/confirm() call removed, replaced by
@simpleworkjs/frontend's app.messages.action/confirm/toast (the
same modules adopted in 1.8.0). This was found live,
mid browser-verification of that release: clicking sso-manager-node
directory.ejs's "Rotate Client Secret" triggered a native confirm(),
which blocks all further browser events on the page — a real hazard for
anyone driving the app with browser automation, not just a cosmetic
inconsistency. sso-manager-node also dropped app.user.remove/
app.oauthClient.remove from public/js/app.js (dead code with a native
confirm() guard and zero callers).
No setup.sh, compose, or config change.
[1.8.0] - 2026-07-27
Bumped
All three apps adopt the newly published @simpleworkjs/frontend package's
app.messages, app.modal, and app.validate modules, replacing the
vendored app.util.actionMessage/actionConfirm/alert in
public/lib/js/app-base.js (byte-identical across all three apps) and the
vendored public/lib/js/val.js (byte-identical in sso-manager-node and
jump-host, and the same engine plus proxy-only DNS/hostname rules in proxy).
Message content is now HTML-escaped — the ad hoc app.util.alert() this
replaces had none — and app.messages.action falls back to a page-wide
toast when there's no inline .actionMessage target on the page. proxy's
host/target/hostname wildcard-DNS validation rules (mirroring
utils/hostname_validate.js) move to its own 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).
jump-host doesn't currently use any [validate] attributes, so its val.js
swap is dedup/future-proofing rather than a behavior change.
app.api/app.auth/app.pubsub/app.socket in each app's app-base.js
are untouched: they're app-specific (a dual-mode callback/promise API with
auth-token header injection) and not something the frontend package's
generic app.js provides, so it isn't loaded.
No setup.sh, compose, or config change.
[1.7.0] - 2026-07-27
Bumped
Two production bugs fixed: PUT /api/user/:uid 500'd with an LDAP
ObjectClassViolationError when setting sshPublicKey on any account
predating the ldapPublicKey objectClass (notably the bootstrap admin) —
and the exact same bug, in the shared @simpleworkjs/ldap package's
addSshKey, was silently aborting SSH connections at jump-host's
key-injection step for the same class of accounts. Both are fixed by
ensuring the objectClass is present before writing the attribute. Also
fixed: the Directory's "add resource" modal left the parent-Service
dropdown blank when adding an OAuth Integration.
Jump-host's web dashboard also gained a "Hosts you can reach" list (admins see "All hosts") — previously it only showed usage metrics with no way to see your actual access from the browser.
No setup.sh, compose, or config change.
[1.6.0] - 2026-07-26
Bumped
- jump-host -> v1.4.0
Jump-host gains standalone mode: it can now run with no LDAP directory and
no SSO Manager at all, storing users and hosts itself via
@simpleworkjs/orm (Sequelize; SQLite by default, any Sequelize-supported
dialect). This is an app-internal capability, opt-in via
standalone.enabled in jump-host's own config — the bundled theta-suite stack
is unaffected and continues to wire jump-host to the shared LDAP directory
and SSO Manager as before. Two bugs were also fixed in jump-host's SSH
server: an ephemeral listen port (0) was silently overridden back to the
default, and session listeners could miss a client's immediate exec/shell
request.
No setup.sh, compose, or config change on the theta-suite side.
[1.5.0] - 2026-07-26
Bumped
This release finishes the UI half of the unification that 1.4.0 deferred: the
three apps now share one front-end shell. views/top.ejs, views/bottom.ejs
and public/lib/js/app-base.js are byte-identical across sso-manager-node,
proxy and jump-host, and everything per-app moved into each repo's new
nodejs/utils/ui.js (nav items and the groups that may see them, footer links,
favicon, profile/logout targets, update-banner on/off). Nav gating is one model
everywhere — the shell reveals .group-required-<cn> from GET /api/user/me,
normalising sso's LDAP DNs and the OIDC clients' group CNs to the same shape,
with the clients' isAdmin flag exposed as a synthetic admin group. jQuery is
4.0.0 and EJS 3.1.10 in all three.
Five client-side bugs were fixed along the way, including two that broke real
flows: app.api.delete ignored the callback that formAJAX passes (so
DELETE-method forms — the proxy's host and DNS delete buttons — never refreshed),
and the login page threw on every logged-out visit while revealing its card.
No setup.sh, compose or config change: this is app-internal UI work. Verified
by driving a full stack of all three apps in a browser — every page renders
console-clean, nav gating is correct per role, and the OIDC login round trip
completes on both OIDC clients.
sso-manager-node 1.5.0:
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.
Fixed (sso-manager-node)
public/lib/js/val.jsshadowedmessagewithletinsidevalidateField, so a custom rule's return value never reachedvalidateMessageand the caller always saw the generic length message. Resolved by adopting the shared validator, which also brings thetarget/hostnamerules and the real password policy (>= 8 chars, and either 12+ or 3 of 4 character classes) to this app.public/js/app.jsused$.isFunction, removed in jQuery 4.
Added (sso-manager-node)
GET /api/user/menow also reportsisAdmin(membership inapp_sso_admin), the single effective-rights flag the shared UI shell gates the update banner on. Group-level gating still readsmemberOf.
Verified
- Browser-verified against a full theta-suite 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.
proxy 1.4.0:
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.- Admin-only nav items lost their inline
display: nonein favour of that class, and the brand link points at/instead of#.
Verified
- Browser-verified against a full theta-suite 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.
jump-host 1.3.0:
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-suite 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.4.0 - 2026-07-25
Bumped
This release unifies the three theta42 apps onto shared @simpleworkjs/* packages
(oidc-client, directory-schema, ldap, app-stack — published under the
simpleworkjs org at 1.0.0), replacing each app's byte-identical forks of the same
code so they share one codebase and API schema. It also fixes a security
regression in the SSO directory discovery API (OAuth client_secret_hash leaked
to every authenticated caller) and the envelope drift that broke jump-host
bridging. The shared UI chrome (top.ejs/bottom.ejs, app-base.js, val.js)
is intentionally not unified in this release — that work is deferred to a
browser-verified session; see UI_UNIFICATION_HANDOFF.md. No setup.sh change:
the new @simpleworkjs/* deps resolve from npm inside each app's image build
(npm ci stays clean; no file:/link:).
sso-manager-node 1.4.0:
Security
- The directory discovery API leaked OAuth
client_secret_hash(and any secret-ish metadata key) to every authenticated caller.Resourcedoesn't overridetoJSON, so the ORM serializedmetadatawholesale — including theclient_secret_hashstored onkind:'oauth'resources — acrossGET /api/discovery/resources,/graph,/me,/resources/:slug, and the directory-adminGET /api/directory-admin/resources. Every discovery read endpoint and the admin list now route throughprojectResource/projectResourcesfrom@simpleworkjs/directory-schema, which unconditionally strips secret keys (anything matching/secret|password|privatekey/i, includingclient_secret_hash) and, for non-directory-admins, reduces metadata to a public allowlist. Admins never receiveclient_secret_hasheither.
Fixed
- Directory discovery envelope drift.
routes/discovery.js(theautoRouter(Resource)mounted live atapp.js:87) returned bare arrays, not the{ results: [...] }envelope the directory contract specifies — so jump-host'sdata.results || []collapsed every per-group query to[]and no user could bridge. Discovery is now served by explicit/resources,/resources/:slug,/graph,/mehandlers that all return the{ results }envelope. The deadroutes/api_discovery.js(mounted atapp.js:112, after the 404 catcher) and its mount were removed. GET /api/discovery/resources?group=<cn>now returns 200 with{ results: [...] }instead of 404.
Added
@simpleworkjs/directory-schema— the directory contract: thekindenum,Resource/ResourceEdge/ResourceGroupfield defs, the{ results }envelope, the security projection (projectResource/projectResources/isDirectoryAdmin), and the discovery client.models/resource.jsimports the field defs; the discovery + directory-admin routes use the projection.@simpleworkjs/ldap—models/user_ldap.jsandmodels/group_ldap.jsnow takeescapeFilter/escapeDNandmakeClient/withClientfrom the shared package (via local wrappers that passconf); sso keeps its richUser.get/Group.get/User.login/User.addSSHkey(posix/write-side stays app-local). sso'smakeClientpasses notlsOptions, so cert validation is unchanged.@simpleworkjs/app-stack— unifiedbuild_info({buildVersion, buildHash, buildYear}) and thestatic-modulesmounting helper.utils/build_info.jsand the static-modules loop inroutes/index.jsuse the shared helpers.- New
tests/discovery.test.js(jest + supertest, runs under the docker harness): locks in the{ results }envelope on/resources,/graph,/me,/resources/:slug, the?group=200-regression, and the no-client_secret_hash/no-secret-key guarantee for every caller.
Changed
- Dependency alignment:
ldapts^8.1.2→^8.1.8. 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).
proxy 1.3.0:
Added
@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. The per-host SSO inroutes/host_auth.jsis unchanged but consumes the shared OIDC utils.@simpleworkjs/ldap— the ldapts client + RFC 4515/4514 escaping.@simpleworkjs/app-stack— unifiedbuild_info({buildVersion, buildHash, buildYear}) and thestatic-modulesmounting helper.utils/build_info.jsand the static-modules loop inroutes/render.jsnow use the shared helpers.
Security
- LDAP filter injection in
User.get. The user lookup built its search filter by interpolatingdata.usernameraw 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 throughescapeFilterfrom@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); 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.
jump-host 1.2.0:
Added
@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.app-base.jsforceLogin/logInRedirectswitched to the?redirect=query-param convention (matching the server-side/login?redirect=route).
[1.3.7] - 2026-07-23
Added
- The bootstrap now provisions the jump host's web-UI SSO login when the jump host is enabled: it mints a dedicated
theta-jumpOAuth client and writes a fulloidcblock (endpoints, client id/secret, callback) plus a generated local anti-lockout admin password into./config/jump-secrets.js. Matches how the proxy's OIDC client is provisioned. An existing pre-OIDCjump-secrets.js(API token but no OIDC client) is regenerated so upgraders get SSO login. Requires jump-host ≥ v1.1.0.
[1.3.6] - 2026-07-23
Bumped
- sso-manager-node -> v1.3.2
sso-manager-node 1.3.2:
Fixed
- OAuth client management API returned
client_id: undefinedon every GET, which broke this stack's bootstrap: it lists the OAuth clients and rotates by the returnedclient_id, so it called/api/oauth/client/undefined/rotateand got a 500 — abortingsetup.shwithbootstrap failedwheneverproxy-secrets.jshad no usable secret (e.g. a fresh/rotated deployment). The ORM'stoJSON()was stripping the mappedclient_id/scopes/… fields;OAuthClient.get()now emits them explicitly (and omitsclient_secret_hash). Unknown client ids now 404 instead of 500.
[1.3.5] - 2026-07-23
Added
- Optional SSH jump host (theta42/jump-host) as a third, opt-in submodule. Enable with
CFG_JUMP_HOST_ENABLED=trueinsetup.env: setup.sh clones/tag-tracks the submodule and builds it behind thejump-hostcompose profile, the bootstrap mints a directory API token and writes./config/jump-secrets.js(LDAP admin bind so it can inject users'sshPublicKey), the jump host is registered as a proxy Host (its web UI) and seeded as a directory service. Users thenssh uid_-_host@jump.<domain>(WinSCP-friendly) orssh uid@jump.<domain>for a TUI host picker; the web UI on :3002 shows audit + metrics. Off by default — existing installs are unaffected.
[1.3.4] - 2026-07-23
Bumped
- sso-manager-node -> v1.3.1
sso-manager-node 1.3.1:
Added
- The Directory documentation (
docs/directory.md) is now surfaced: registered in-app at/docs/directory("Directory & Inventory"), help-linked from the Directory page header, and linked from the docs-site index. Extended with the shared slug conventions (site_<name>,host_<hostname>— as used by ldap-client and the theta-suite seed), the automatic-registration story (theta-suite stack seeding, ldap-client Linux host enrollment), and the API surface (admin at/api/directory-admin, read-only graph at/api/discovery).
Changed
- Direct LDAP binds are described as first-class, not "legacy", across README, DEPLOYMENT.md, docs, and the Dockerfile: Linux hosts are a primary consumer of the directory (PAM/SSSD login, LDAP-backed
sudoviasudoRole, SSH public keys via openssh-lpk) — exactly what the custom schemas exist for.
theta-suite own changes
Added
CFG_SITE_NAMEinsetup.env(right belowCFG_DOMAIN, defaultlocal): names the SSO directory site the stack registers itself under — slugsite_<name>, matching theparentSlugconvention ldap-client-joined Linux hosts use, so they land under the same site.- The directory seed now collects real host facts on the machine (hostname, IP, MAC of the default-route interface, OS pretty-name, kernel — same collection as
ldap-client/index.sh) and registers the stack host ashost_<hostname>with that metadata, plus fills in each service's internal port and git repo (sso-manager3001,proxy3000,openldap389/636,openresty443). Existing resources from the earlier seed layout (stack-host, domain-slug site) are adopted in place — seed metadata only fills fields the operator hasn't set, never overwrites. - The bootstrap now seeds the SSO directory with the stack's own resources: a site (from the configured domain), a "Stack host", and the SSO Manager + Proxy services (with their public URLs in metadata), linking the proxy's auto-registered OAuth client under its service. Also seeds the two non-obvious services the stack runs: the OpenLDAP directory (advertising the
ldaps://endpoint Linux hosts and LDAP-native apps bind to, honoringldap.ldapsHost) and the OpenResty edge (the 80/443 data plane every hostname flows through, with a wildcardhttps://*.<domain>address). The Directory page is populated out of the box instead of starting empty. Idempotent — resources whose slug already exists are operator-owned and never touched, and a seed failure only warns (never fails a bring-up, e.g. against an older sso-manager image without/api/directory).
[1.3.3] - 2026-07-23
Bumped
- sso-manager-node -> v1.3.0 (from v1.1.18; includes the intermediate v1.2.1 release)
sso-manager-node 1.3.0:
Added
- OAuth client management API at
/api/oauth/client(groupapp_sso_oauth_admin): list, create, update, delete, and rotate-secret for OAuth clients, backed by the Resource model. Accepts form-style string inputs (newline-separatedredirect_uris/allowed_groups, space-separatedscopes). - Dockerized test suite:
docker-compose -f docker-compose.test.yml up --buildspins up OpenLDAP + Redis + a test-runner that seeds the test user and runs the full jest suite (174 tests) against them.tests/globalSetup.jshonorsREDIS_URL.
Fixed
- Completed the model-redis →
@simpleworkjs/ormport that shipped half-finished in 1.2.1:OtpToken.issue/verifycalled nonexistentfind()/listDetail()— every OTP login 500'd.- Impersonation create/revoke called nonexistent
ImpersonationToken.listDetail()— both endpoints 500'd. OAuthClientreadis_validfrom the Resource model, which has no such column — every client evaluated as disabled and all/oauth/authorizerequests were rejected with 400. Client validity now lives inmetadata(absent = valid).OAuthClient.adddidn't set the required-uniqueResource.slug; clients now get a slug derived from the client name.GET /api/token/:name/:tokenreturned{results: null}with 200 for unknown tokens (ormget()returns null instead of throwing); now 404s.
User.loginreturns a clean 401 instead of crashing when neitheruidnorusernameis supplied.- Depend on published
@simpleworkjs/orm^0.2.8 andmodel-redis^1.6.0 instead of a localfile:link that brokenpm ciin docker builds.
Changed
- Removed the Mobile Phone field from the user create/edit form.
sso-manager-node 1.2.1:
Added
- Actionable Metrics: New real-time metrics tracking for failed logins, top IPs, and service usage per user.
- LDAP Monitor: Background service to parse OpenLDAP binds over port 389 and track metrics for legacy apps.
- UI Updates: Executive dashboard now displays actionable metrics cards instead of raw logs. User profiles show individual service usage stats to admins.
- Directory Management: Integrated site/host/service abstractions into directory UI and allowed associating OAuth apps directly to services.
[1.3.2] - 2026-07-21
Bumped
- proxy -> v1.2.2
proxy:
Fixed
- Multi-target load balancing (added in 1.2.0) crashed every request to a load-balanced host:
ops/nginx_conf/targetinfo.luarequired a nonexistentresty.balancer.round_robinmodule. Thelua-resty-balancerrock actually installed providesresty.roundrobininstead, with a different constructor API. Fixedtargetinfo.luato use the real module — verified end-to-end that requests now round-robin across targets with no Lua errors.
[1.3.1] - 2026-07-21
Bumped
proxy:
Fixed
- The bootstrap anti-lockout admin account was always created as
proxyadmin2regardless ofconf.auth.adminUsers, whilemigrations/permission_bootstrap.jsgrants the global-admin permission toconf.auth.adminUsers[0]. If an operator customizedadminUsersaway 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.jsnow derives the bootstrap username fromconf.auth.adminUsers[0](falling back toproxyadmin2), matchingpermission_bootstrap.js. - Corrected a
secrets.js.examplecomment 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.
sso-manager-node:
Added
- N-Way Multi-Master LDAP replication:
LDAP_SERVER_ID+LDAP_REPLICATION_HOSTSconfiguresyncreplpeers in the bundled OpenLDAP, and a new/sitespage (nav: Sites) shows each configured peer's LDAP URL and live reachability. - A
locationproperty on users, editable from the profile and user-edit forms.
Fixed
/sites(added above) 500'd on every load:views/sites.ejsincluded nonexistent partialsheader/footerinstead of this app's actualtop/bottom. Fixed to match every other view.
Changed
- Refreshed all README screenshots (dashboard, users, groups, OAuth apps) against the current UI, and added a new Sites & Replication screenshot.
theta-suite own changes
- Refreshed
docs/images/sso-dashboard.pnganddocs/images/proxy-hosts.pngto match the submodules' updated screenshots.
[1.1.20] - 2026-07-20
Bumped
- proxy -> v1.1.17
proxy:
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.19] - 2026-07-18
Bumped
- sso-manager-node -> v1.1.17
sso-manager-node:
Added
conf.ldap.ldapsHostandconf.ldap.ldapsPortconfig options for advertising a separate, internal-only LDAPS hostname on the/integrationspage. Falls back to the public OAuth issuer host when unset.- Contextual help panel on
/integrations→ LDAP explaining why LDAPS needs a hostname, why port 636 should not be forwarded publicly, and the recommended internal-DNS / Docker-internal alternatives. - Tests for the
/integrationsroute's LDAPS URL derivation andldapsHostoverride.
Changed
nodejs/package.json/package-lock.jsonversion bumped to1.1.17.routes/index.jsnow derives the displayed LDAPS URL fromconf.ldap.ldapsHost/ldapsPortwith fallback to the OAuth issuer host.docs/configuration.md,docs/ldap.md,DEPLOYMENT.md, andsecrets.js.exampledocument the newldapsHost/ldapsPortoptions and recommended network layouts.
theta-suite own changes
setup.env.exampleadds optionalCFG_LDAPS_HOSTfor the internal LDAPS hostname.setup.shpassesCFG_LDAPS_HOSTinto the generated./config/sso-secrets.jsasldap.ldapsHost.config.example/sso-secrets.js.exampledocumentsldap.ldapsHost/ldap.ldapsPort..env.exampleaddsLDAPS_HOSTfor legacy.envmigrations.docker-compose.ymlcomments warn against forwarding 636 to the public internet.README.mdexplains theCFG_LDAPS_HOSTrecommendation in the port-forwarding section.
[1.1.18] - 2026-07-18
Bumped
proxy:
Changed
- Public-release packaging: removed
"private": truefromnodejs/package.json, corrected the repository URL tohttps://github.com/theta42/proxy.git, and fixed the MITLICENSEcopyright line. - Genericized committed config defaults in
conf/base.jsandconf/development.js(example.com/localhostinstead of theta42 infrastructure). - The bootstrap
proxyadmin2account now gets a random, one-time password whenauth.localAdminPassis unset, instead of the well-known default.
Security
- Sanitized rendered docs HTML with
xssinroutes/docs.js. - The Unix socket JSON-RPC socket is now created with mode
660instead of world-writable777.
Fixed
- The global error handler no longer leaks
err.keys, stack traces, or internal details in JSON responses. DEPLOYMENT.mdanddocs/docker.mdnow correctly describe theCONF_SECRETSenv-var mechanism.
sso-manager-node:
Security
- Hardened LDAP filter and DN construction against injection in
models/group_ldap.jsandmodels/user_ldap.js. - Replaced
Math.random()-based token/UUID/OTP generation withcrypto.randomUUID()/crypto.randomInt()inmodels/token.js,models/oauth_code.js, andmodels/oauth_client.js. - Refused startup when
oauth.jwtSecretis missing or placeholder. - Sanitized rendered docs/Terms-of-Service HTML with
xssto block malicious markdown output. - Removed full-object
console.logof new-user data and reduced login error logging toname/messageonly.
Changed
- Public-release packaging: removed
"private": truefromnodejs/package.jsonand bumped version to1.1.16.
Fixed
models/email.js: fixed from-address template rendering bug.
theta-suite own changes
CHANGELOG.mdnow embeds the full app-level release notes for each submodule bump, not just links..env.exampleno longer ships realistic-looking default passwords; values are clearly placeholders.config.example/*.js.examplecomments now describe the actualCONF_SECRETSenv-var loading mechanism.setup.shsummary no longer prints generated passwords to stdout; it points to./config/*.js.bootstrap/bootstrap.jsfails hard instead of falling back to weak default passwords when config is missing.
1.1.17 - 2026-07-18
Bumped
Both apps' bare-metal install.sh now installs to /opt/theta42/<app> and seeds /etc/<app>/secrets.js on first run, matching a wget -O - .../install.sh | sudo bash one-line install for both (previously proxy-only); re-running it prints the version it's updating from/to. sso-manager-node's installer was rewritten from a flag-driven, copy-based script into the same idempotent git-clone pattern proxy already used, and now bootstraps OpenLDAP itself on first run instead of requiring the repo to already be checked out locally. None of this affects the Docker/unified-stack deployment this repo orchestrates — bare-metal-only.
1.1.16 - 2026-07-18
Bumped
Both: bumped @simpleworkjs/conf to 1.2.0 and jq-repeat to 2.2.0.
Changed
./config/sso-secrets.jsand./config/proxy-secrets.jsare now loaded via each app'sCONF_SECRETSenv var (set by the entrypoint) instead of being symlinked into/app/conf/secrets.js— neither container needs write access to its ownconf/directory anymore. No change to the config file format or bind mounts; existing./config/directories keep working as-is.
1.1.15 - 2026-07-17
Bumped
proxy:
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
keyuphandler, ever saw it become available). The underlying/host/lookup/:itemcheck 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/:itemendpoint 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 standalonehttp2 on;directive (nginx 1.25.1+).
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/*.mdpage 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.htmlsuffix — 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.
sso-manager-node:
Added
- Three new plain-language docs aimed at less technical readers, replacing the schema-level LDAP/OAuth/API docs as the target of most card help links: Accounts, Groups & Managers, Connecting Apps (SSO), 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) now links to its own doc.
Fixed
- The in-app docs viewer rendered every
docs/*.mdpage 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 (ldap.html,index.html, etc.) never resolved in-app, since this viewer serves docs at/docs/<slug>with no.htmlsuffix — they're now rewritten to the correct in-app URL, the same way image paths already were. - The new concept docs' cross-links (
concepts-accounts.htmletc.) are the correct, working URL on the Jekyll/GitHub Pages build (where the page's URL is its filename stem) but didn't resolve in the in-app docs viewer, which serves docs at a separate short slug (/docs/accounts). The in-app renderer now also resolves a doc's real filename as a fallback, so one link written in a doc works on both targets.
1.1.14 - 2026-07-17
Bumped
Both: moved the help (❓) link out of the global header and onto each relevant card individually, so it deep-links straight to the doc that actually covers that card instead of one generic per-page guess.
1.1.13 - 2026-07-17
Bumped
Both: added a help icon (❓) in the top-right header that deep-links to the doc most relevant to the current page, and made the in-app docs viewer (/docs) searchable (a simple line-substring search over the local doc set, no new dependency, still works with no internet access).
1.1.12 - 2026-07-17
Bumped
- proxy -> v1.1.9
proxy:
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-redislibrary's record-rename path: renaming a record's primary key while anotheralways-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 inHost.prototype.update().
1.1.11 - 2026-07-17
Bumped
- proxy -> v1.1.8
proxy:
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 nochallengeTypehandling at all (onlyHost.create()did) — selecting it and saving silently did nothing. Added the same wildcard-parent lookup toupdate(). - Couldn't register a wildcard's own base domain as a host. A wildcard cert's
altNamesalready 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.
1.1.10 - 2026-07-17
Bumped
- sso-manager-node -> v1.1.9
sso-manager-node:
Added
- Every account's personal Unix group (its primary GID holder) can now have supplementary members managed from the account's profile page ("Members of
<uid>'s group", admin-only) — e.g. to share write access to files owned by that group. Uses the standardmemberUidattribute (RFC 2307posixGroup).
1.1.9 - 2026-07-17
Bumped
- sso-manager-node -> v1.1.8
sso-manager-node:
Added
- Group membership is now editable directly from a user's profile page ("My groups" -- add via a group-name picker, remove with a button per row), instead of only from each group's own card on the Groups page. Admin-only, using the existing per-group member add/remove endpoints.
Fixed
- The Edit Profile form's Mobile Phone field had a stray
validate=":9"making it effectively required (submission was blocked with "Please fix the form errors" if left blank) -- it was always meant to be optional, matching the "Add user" form. Removed. - A service account's profile always showed
Name: Service Account-- every service account has the same literal filler given/last name (a schema-satisfying placeholder, not meant to be shown), making them indistinguishable by name. The Name line is now hidden for service accounts. - The Users page's Service Accounts tab, and a freshly-created service account's own profile, could appear empty/not-a-service-account for up to 5 minutes right after creation. Creating a user caches it via
User.get()before the route handler marks it as a service account (group membership), so the cached copy hadisServiceAccountstuck wrong until the cache TTL expired. Now cleared and re-fetched immediately after marking. - A user belonging to exactly one LDAP group had their
memberOfattribute returned as a bare string instead of a one-element array (ldapts's normal behavior for single-valued attributes) -- client-side permission checks (for(let group of user.memberOf)) would then iterate the DN character-by-character instead of once, causing pages gated on that group (e.g. Groups) to incorrectly show "You do not have permission to be here." NormalizedmemberOfto always be an array, same fix already applied tomanager.
1.1.8 - 2026-07-17
Bumped
- sso-manager-node -> v1.1.7
sso-manager-node:
Changed
- Service accounts unified to one kind. Removed the LDAP bind-only service account type (the Integrations → LDAP "Service Accounts" card, and its
/api/service-accountroutes) -- every service account is now a real Unix/POSIX account with a UID, created from the new Users → Service Accounts tab. Email and password are both optional for service accounts; a blank password means nouserPasswordis set at all (the account simply can't bind). - Added a
managerfield to every account. Multi-valued (a list of usernames), defaults to whoever created the account (the admin who added it, or whoever sent the invite), and reassignable from the account's Edit form. Anyone listed as a manager can edit that account -- same fields an admin can (mobile, description, SSH key, date of birth, home directory, login shell, manager list) -- without needingapp_sso_admin. homeDirectoryandloginShellare now editable from the Edit Profile form (previously view-only).
1.1.7 - 2026-07-16
Bumped
Both: redesigned the GitHub Pages docs site to match each 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), and mobile-responsive layout. theta-suite's own docs site got the same treatment in this release too (see below).
Changed
- theta-suite's own docs site redesigned the same way -- dark navbar/footer using the shared theta42 logo (this repo has no app UI of its own), cross-page nav, SEO, mobile-responsive.
docs/index.md's "More docs" section removed (redundant with the new nav). - Added
docs/_siteto.gitignore(missing entirely before).
1.1.6 - 2026-07-16
Bumped
- proxy -> v1.1.6
proxy: Hosts admin UI's Authentication tab radios (Off / Basic / SSO) had no shared name, so clicking one didn't uncheck the others. Added name="auth_mode" to restore standard exclusive radio-group behavior.
1.1.5 - 2026-07-16
Bumped
Both: bumped jq-repeat 2.0.1 -> 2.1.0. proxy fixed real breakage from the removed __setPut/__setTake API (insert/remove row hooks in the admin UI); sso-manager-node fixed a stale-data flash in the edit-profile flow caused by update()'s new throttling.
1.1.4 - 2026-07-16
Added
- White-label support in both proxy and sso-manager-node --
<title>, navbar brand, and logo are now conf-driven (conf.name/conf.logo) instead of hardcoded. Closes proxy#45 and sso-manager-node#6.
Fixed
- sso-manager-node: the bundled default LDAP ppolicy had
pwdLockout: FALSE, silently making "deactivate user" not actually block login. Fixed, with a drift-correction path for already-deployed instances.
Bumped
1.1.3 - 2026-07-16
Added
CHANGELOG.md(this file). Closes #43.
Bumped
1.1.2 - 2026-07-16
Changed
docs/index.md(the published site's home page) never linked toarchitecture.md,quickstart.md, orstandalone.md— added a "More docs" section so they're reachable from the site instead of only by direct URL.
Bumped
1.1.1 - 2026-07-16
Changed
setup.shnow pinsproxyandsso-manager-nodeto their latest release tag (vX.Y.Z) instead of the tip ofmaster. A rebuild now always lands on a tagged, versioned release of each app rather than whatever was most recently merged upstream.
Bumped
1.1.0 - 2026-07-16
First tagged release. Establishes the vX.Y.Z tag convention going forward.
Added
setup.shnow reports which submodules actually moved to a newer commit during an update, instead of updating silently.
Bumped
[1.34.4] - 2026-08-02
Changed
- Updated
sso-manager-nodesubmodule tov1.19.6to pull in a fix for the Vault API 403 error on the Secrets List.
[1.34.5] - 2026-08-02
Added
- Automatically build and install
theta-agenton the host system as a systemd service duringsetup.sh. - Added
CFG_CREATE_ALL_HTTPoption tosetup.envto create all default proxy host entries withforcessl=false.