Commit Graph

392 Commits

Author SHA1 Message Date
wmantly e915a17cbd Merge pull request #196 from theta42/release-v2.3.0
release(v2.3.0): multi-site join UI + spoke read-only + WAN health
2026-08-10 09:38:06 -07:00
wmantly e4d5e8d75c release(v2.3.0): multi-site join UI, spoke read-only, live WAN health 2026-08-10 09:35:38 -07:00
wmantly 39a9dc0282 Merge pull request #195 from theta42/feat/multi-site-join-ui
feat(site): join UI, spoke read-only, live WAN health, fresh-install guard
2026-08-10 09:15:29 -07:00
wmantly 9d266d2e4c feat(site): join UI, spoke read-only enforcement, live WAN health, fresh-install guard
Completes the multi-site join layer on top of the v2.2.0 endpoints:

- UI (Master Site modal): a fresh install (canJoin) gets a 'Join an Existing
  Site' form (master URL + stj_ key); a master gets a 'Site Join Keys' manager
  (mint/revoke/list, key shown once); WAN Sync Health now reflects a live probe.
- POST /api/site/ping (Bearer stj_ key, no admin session): lightweight master
  reachability probe for WAN health (cheap vs /export).
- Spoke read-only: directory-write routes (resources/edges/groups/secrets/
  grants/driver-action/discovered) reject with 403 pointing at the master.
- Fresh-install guard: /api/site/join refuses unless no users beyond the
  bootstrap admin and no enrolled agents (siteIsFresh), and site-status exposes
  canJoin so the UI only offers join on a genuinely fresh install. The
  bootstrap's seeded default resources are NOT the signal (they always exist).
- The spoke stores the join key (masterJoinKey) in /config/site.json so WAN
  health (and a future write-proxy) can reach the master.
- Tests: siteIsFresh cases in tests/site_join.test.js.
2026-08-10 09:12:28 -07:00
wmantly 3d18c3f0cb Merge pull request #194 from theta42/release-v2.2.0
release(v2.2.0): multi-site join server endpoints
2026-08-10 06:10:02 -07:00
wmantly 214b3a7f5a release(v2.2.0): multi-site join server endpoints, persisted site role, emoji fix 2026-08-10 06:07:07 -07:00
wmantly 08dd234710 Merge pull request #193 from theta42/feat/multi-site-join
feat(site): multi-site join server endpoints + persisted site role + emoji fix
2026-08-10 06:01:28 -07:00
wmantly c96a4b6652 feat(site): multi-site join server endpoints + persisted site role + emoji fix
Server endpoints for joining a spoke to a master directory (MULTI_SITE_SPEC.md).
This pass is server-only; setup.sh wiring and the UI are the next layer.

- Site join keys (SiteJoinKey model, stj_ prefix): mint/revoke/delete/list,
  hashed at rest, shown once — the same model as agent join keys.
- POST /api/site/export (master, Bearer stj_ key, no admin session): returns the
  local LDAP tree (slapcat LDIF) + resource catalog + siteSlug + baseDn.
- POST /api/site/join (spoke, admin): { masterUrl, joinKey } pulls the master
  export, imports resources (upsert by slug) + LDAP (ldapadd -c), and persists
  the spoke role. Refused if already a spoke.
- Persisted site role: utils/site_config.js keeps isMaster/masterUrl/siteSlug in
  /config/site.json (env seeds defaults); site-status/site-promote now use it.
- Unit tests (site_join, site_config) with in-memory stubs, wired into npm test.
- docs/site-join.md + docs router entry.
- Repairs the corrupted multi-site emojis (crown/bolt) in directory.ejs.
- .gitguardian.yml ignores the generic-password false positive on reading the
  LDAP bind credential from runtime config (never a hardcoded secret).
2026-08-10 05:58:28 -07:00
wmantly 0915043d6d Merge pull request #192 from theta42/release-v2.1.1
release(v2.1.1): site-status modal API fix, honest agent version fallback
2026-08-09 20:34:00 -07:00
wmantly ddf123d03c release(v2.1.1): site-status modal API fix, honest agent version fallback 2026-08-09 23:31:15 -07:00
wmantly 9004463311 Merge pull request #191 from theta42/fix/fresh-install
fix(ui): site-status modal API, honest agent version fallback
2026-08-09 20:29:19 -07:00
wmantly 96410fd8c4 fix(ui): site-status modal API, honest agent version fallback, Directory default org name
Fresh-install bug report fixes:

- 'Master Site' button error 'app.modal.show is not a function': the
  multi-site status modal used the legacy app.modal.show() signature; the app
  exposes app.modal.open({title, bodyHtml, size}). The site-status request
  itself worked -- only the rendering call was wrong.
- Agents with no discovery yet showed a fake 'v2.0.0' (three hardcoded
  fallbacks). Now 'unknown', so a host whose agent never connected isn't
  presented as an old version.
- The default org name (browser tab title) is set by theta-suite's setup.sh;
  that default is fixed separately there (CFG_ORG -> Theta Directory).
2026-08-09 23:24:54 -07:00
wmantly f94bb3c14c Merge pull request #190 from theta42/release-v2.1.0
release(v2.1.0): Windows install commands + drop committed binaries
2026-08-09 18:41:12 -07:00
wmantly 37e5ad0494 release(v2.1.0): Windows install commands in the Install Agent modal; drop committed binaries 2026-08-09 21:38:46 -07:00
wmantly 52b85c0d36 Merge pull request #189 from theta42/feat/windows-installer-commands
Windows install commands in the Install Agent modal; drop committed binaries
2026-08-09 18:35:20 -07:00
wmantly f98615aae2 chore(agents): label the Install Agent modal fields as Theta Directory URL 2026-08-09 20:41:50 -07:00
wmantly 72040b1851 feat(agents): Windows install commands in the Install Agent modal; drop committed binaries
The Install Agent modal now emits Windows (PowerShell) one-liners for the
join-key, pre-register and custom-config flows. Each downloads the fully-offline
setup installer and passes the same values the bash flow uses:

  join key : /SERVER_URL /JOIN_KEY
  quick    : /SERVER_URL /AUTH_TOKEN /PUBLIC_KEY
  custom   : /B64_CONFIG=<base64 agent.yml>

Binaries are no longer committed here: the setup.exe and loose agent binaries
are GitHub release artifacts (built by the theta-agent release workflow) and the
modal downloads the installer from releases/latest/download/. The SSO still
serves install.sh (the small Linux bootstrap script); the large binaries are
dropped from this repo.
2026-08-09 20:41:49 -07:00
wmantly 93a022bd66 release(v2.0.4): build Dockerfile.openldap FROM the published base image (#188)
ldapbuild now pulls ghcr.io/theta42/openldap-nestgroup:<pinned commit>
instead of compiling OpenLDAP from source on every build.

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-09 17:08:25 -07:00
wmantly c30975329c feat: publish a prebuilt OpenLDAP-with-nestgroup base image (#187)
Extracts Dockerfile.openldap's `ldapbuild` stage (compile OpenLDAP from
source for the nestgroup overlay, ~5 min, dependent on git.openldap.org
being reachable) into its own Dockerfile, built and pushed to
ghcr.io/theta42/openldap-nestgroup by this workflow whenever the pinned
commit changes.

This commit only adds the new image + workflow; Dockerfile.openldap itself
still compiles from source. A follow-up change switches it to FROM the
published image once this workflow has run once and the image exists.

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-09 16:54:36 -07:00
wmantly 5c0018f24a release(v2.0.3): fix Directory tab managed-filter bug and site-status 500 (#186)
- GET /api/directory-admin/resources let every kind:'host' resource through
  regardless of promotion status, so "Auto-promote to Directory" unchecked on
  a discovery plugin never kept unpromoted devices out of the Directory tab.
- GET /api/directory-admin/site-status queried the nonexistent Resource.subType
  column instead of metadata.subType, throwing SequelizeDatabaseError.
- Added a "Show ignored" toggle to Discovered Inventory (off by default).
- Untracked nodejs/config/inventory.sqlite -- the app's default runtime DB,
  not a fixture, committed by mistake across 13 prior releases.

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-09 16:29:08 -07:00
wmantly 6e172bd528 release(v2.0.2): rebrand README to Theta Directory, remove standalone-install framing (#185)
Removed "Why this over the alternatives"; fixed stale links to the old
per-repo GitHub Pages site to point at the unified theta-suite docs site;
made explicit this is deployed as part of Theta Suite, not standalone; added
the agent capability/install screenshots to the gallery.

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-09 16:09:30 -07:00
wmantly 3c98cd4596 Merge pull request #184 from theta42/release-v2.0.1-binaries
release(v2.0.1): bump version to 2.0.1 and package v2.0.1 agent binaries
2026-08-09 12:39:14 -07:00
wmantly d3fab004e9 release(v2.0.1): bump version to 2.0.1 and package v2.0.1 agent binaries 2026-08-09 15:35:29 -04:00
wmantly e8824cb28d Merge pull request #183 from theta42/release-v2.0.1
release(v2.0.1): fix openbao container discovery, agent version API collection, console logs, cache invalidation, site status API 500, secrets filtering, and auto-group spawning
2026-08-09 11:50:19 -07:00
wmantly 98ed99a7e9 release(v2.0.1): fix openbao container discovery, agent version API collection, console logs, cache invalidation, site status API 500, secrets filtering, and auto-group spawning 2026-08-09 14:46:33 -04:00
wmantly bb744adb5e fix(agent): update bundled v2.0.0 multi-arch binaries and install.sh in static resources (#182) 2026-08-09 01:11:55 -04:00
wmantly 420a9c9f8a docs(changelog): add v2.0.0 release notes (#181) 2026-08-09 00:18:34 -04:00
wmantly d442f1e3f9 fix(agent): preserve full telemetry (disks, cpu_details, logged_users) and format desktop_control driver-action params (#180) 2026-08-08 23:41:46 -04:00
wmantly ebd7e9e434 feat(v2.0.0): bump version to 2.0.0, add Multi-Site Master badge, site-status API, and Master promotion UI (#179)
* feat(v2.0.0): bump version to 2.0.0, add Multi-Site Master badge, site-status API, and Master promotion UI

* fix(test): update test script to run unit tests without requiring live Redis socket
2026-08-08 21:43:33 -04:00
wmantly 6ce7e67665 docs: rebrand to Theta Directory (#178) 2026-08-08 20:56:43 -04:00
wmantly 583b822e4a feat(directory): add subtype templates picker, fuzzy merge search, auto-ignore openbao, inherit parent host agent, truncate resource names, key badge positioning (#177) 2026-08-08 19:38:42 -04:00
wmantly 9025feef1b Merge pull request #176 from theta42/release-v1.33.0
Release v1.33.0 - Directory Key Badges, Discovered Inventory Merge/Ignore & Desktop Operations
2026-08-08 18:16:26 -04:00
wmantly 620f401091 release: v1.33.0 - Directory Key Badges, Discovered Inventory Merge/Ignore & Desktop Operations 2026-08-08 18:12:37 -04:00
wmantly fc9952add3 Merge pull request #175 from theta42/release-v1.32.0
Release v1.32.0 - Subtype Drivers Engine & Explicit Secret Inheritance
2026-08-08 16:12:35 -04:00
wmantly 8a0b796f81 test: update plugin and reconciler tests for 100% pass rate 2026-08-08 16:08:53 -04:00
wmantly 652d3f7d76 Merge release branch release-v1.32.0 2026-08-08 15:38:35 -04:00
wmantly a442dc9921 release: v1.32.0 - Subtype Drivers Engine, Explicit Secret Inheritance & App Tokens consolidation 2026-08-08 15:38:35 -04:00
wmantly 5b1302bc6f Merge pull request #174 from theta42/release/v1.31.0
chore: release v1.31.0
2026-08-07 23:41:08 -04:00
wmantly 10e5193077 test: update api_agent_ops test to match default paths 200 response 2026-08-07 23:35:20 -04:00
wmantly 15d9ce1078 feat: release v1.31.0 with Zero-View secrets engine, generator, multi-level inheritance, and SSSD key mappings v1.31.0 2026-08-07 23:26:39 -04:00
wmantly 181ca8c9cb Add LDAP-over-HTTPS API, agent secrets/IAM engines, and join key management
See CHANGELOG.md for the full breakdown. Summary:

- POST /api/v1/ldap/{bind,search}: LDAP-over-HTTPS so a client stops
  speaking raw LDAP and instead calls the SSO, which binds/searches its
  own OpenLDAP on the caller's behalf (DESIGN.md §3).
- LDAP byte-pump relay (utils/ldap_tunnel.js): forwards raw LDAP bytes
  from an agent's local socket into OpenLDAP over the existing agent WSS
  channel; the SSO never parses LDAP (DESIGN.md §4).
- POST /api/v1/agent/secrets: node-scoped OpenBao secret fetch for
  agents, enforced to each agent's own secret/data/nodes/<id>/* prefix
  (DESIGN.md §5).
- iam_apply signed command: push node-scoped IAM config (sudo rules, SSH
  keys, access control, revocation) to an agent (DESIGN.md §6).
- Agent capability badges on the Directory Metrics tab, sourced from the
  agent's own discovery frame.
- Join key management: GET /api/agent/join-keys/:id/agents (which hosts
  enrolled through a key) plus a Manage join keys table in the Install
  Agent modal with Revoke/Delete actions, confirmed inline per-row rather
  than a blocking native confirm() or the shared app.messages.confirm()
  banner (which desyncs across concurrent rows -- see CHANGELOG).
- docs/agents.md: capability matrix updated for the three new
  capabilities, a full secrets-engine walkthrough with screenshots
  (bash + Node consuming a rendered secret, plus the direct-API
  alternative), and the join-key reuse/UI/audit questions answered.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-07 17:03:41 -04:00
wmantly 6e748bfa66 Merge pull request #173 from theta42/fix/smtp-from-fallback-and-doc-fixes
Fix SMTP From-address fallback rejection; catalog card icon order; doc corrections
v1.30.2
2026-08-06 21:19:35 -04:00
wmantly a78db906e8 Fix SMTP From-address fallback rejection; catalog card icon order; doc corrections
Mail sending fell back to a hardcoded noreply@theta42.com From address when
smtp.from wasn't set, which authenticated relays reject with "Sender is not
same as SMTP authenticate username" since no relay authorized this account
to send as that address. Falls back to smtp.user first now.

Also: catalog card titles now read name-then-icon instead of icon-then-name,
and a handful of docs corrections found in an accuracy pass (configuration.md
missing the OpenBao/live-config layer, plugins.md undercounting plugin types,
vault.md describing OpenBao dev-mode/root-token access that doesn't reflect
the real production setup, orphaned discovery.md/vault.md pages linked in,
README's required-groups list missing app_sso_directory_admin).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0113gCdnfSCuZr6xvPDxTo3D
2026-08-06 21:13:38 -04:00
wmantly e7e3eeb6cd Merge pull request #172 from theta42/fix/test-email-sms-and-join-key-ui
fix: test email/SMS senders, all SMS delivery, join-key install UI (v1.30.1)
v1.30.1
2026-08-06 14:40:04 -04:00
wmantly f178f1a972 fix: test email/SMS senders, all SMS delivery, join-key install UI (v1.30.1)
Pull Request Tests / Run Tests (18.x) (push) Failing after 1m43s
Pull Request Tests / Run Tests (20.x) (push) Failing after 30s
Pull Request Tests / Run Tests (22.x) (push) Failing after 31s
Pull Request Tests / Test Summary (push) Failing after 4s
Test Email always failed with "Email.send is not a function":
models/email.js exports {Mail}, and the handler required the module and
called .send on it directly. Every other caller destructures it.

Test SMS failed with "Unexpected token '<'": it POSTed to
https://api.voip.ms/v1.0/sms/send with Basic auth, an endpoint that does
not exist. VoIP.ms's REST API is a GET against voip.ms/api/v1/rest.php
with api_username/api_password and method=sendSMS, so the fabricated URL
returned HTML and response.json() threw.

Worse, ALL SMS delivery was broken. models/sms.js called
PluginInstance.find({...}) but the ORM has no find -- the query method is
list({where}) -- so it threw on every send, before it could even fall
back to the direct VoIP.ms path. OTP-by-SMS and notifications were dead.

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 broken paths went unnoticed. Failures
report as 400 with the underlying reason rather than an opaque 500.

Adds a guard suite that fails the build on any call to a non-existent ORM
static, on requiring models/email without destructuring {Mail}, and on
any reference to the bogus api.voip.ms host.

Also: the Install Agent modal now leads with the join-key flow. v1.30.0
shipped join keys in the API and documented the modal as the place to get
one, but the modal still only did the pre-register flow.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 11:51:37 -04:00
wmantly 03605267bc Merge pull request #171 from theta42/feat/agent-join-keys-directory-fixes
feat: agent join keys; fix directory collapse, plugin edit/delete, docs (v1.30.0)
v1.30.0
2026-08-06 10:40:02 -04:00
wmantly 7e9a271090 feat: agent join keys; fix directory collapse, plugin edit/delete, docs (v1.30.0)
Pull Request Tests / Run Tests (18.x) (push) Failing after 1m25s
Pull Request Tests / Run Tests (20.x) (push) Failing after 26s
Pull Request Tests / Run Tests (22.x) (push) Failing after 30s
Pull Request Tests / Test Summary (push) Failing after 5s
JOIN KEYS

v1.29.0 required an admin to pre-register every machine before its agent
would be spoken to. The security model was right; the workflow was not --
installing the agent should be enough to add a host.

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 it must pin, delivered in the
config frame. The join key is a bootstrap credential, never the host's
identity, so one key stays convenient without becoming a fleet-wide
skeleton key: every host remains individually revocable.

DIRECTORY

Collapsing the tree did nothing. applyTreeCollapse found the caret with
`.tree-caret i` and returned early when absent -- Font Awesome's SVG mode
rewrites <i> to <svg>, so that selector matched nothing and the early
return skipped setting hideBelowDepth. State now lives on the caret
button and is rotated by CSS.

The Discovery Plugins 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 action silently never happens.

Plugin instances can now be edited.

DISCOVERY

A fresh install presented its own five containers as unmanaged
discoveries. The Docker plugin now recognises the stack's compose project
and attaches each container to the service it implements. Container slugs
came from the container id, which changes on recreate, so every deploy
minted a new resource and orphaned the old one.

DOCS

/docs/discovery 404'd (no slug entry) and `agents` pointed at plugins.md,
leaving docs/agents.md unreachable. Adds docs/discovery.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 10:33:34 -04:00
wmantly b28a18064a Merge pull request #170 from theta42/fix/directory-discovery-profile
sec: authenticate theta-agent enrollment; directory + discovery fixes (v1.29.0)
v1.29.0
2026-08-05 18:55:15 -04:00
wmantly 87339da1b2 sec: authenticate theta-agent enrollment; directory + discovery fixes (v1.29.0)
Pull Request Tests / Run Tests (18.x) (push) Failing after 1m30s
Pull Request Tests / Run Tests (20.x) (push) Successful in 23s
Pull Request Tests / Run Tests (22.x) (push) Failing after 37s
Pull Request Tests / Test Summary (push) Failing after 4s
SECURITY

/api/agent/ws authenticated nothing. There was no agent registry, so any
client reaching the SSO could register as a node, publish discovery and
telemetry into the admin view, and receive commands -- including a signed
arbitrary_bash -- addressed to a token it guessed. Tokens were generated
in the BROWSER and never recorded server-side, so there was nothing to
validate against and no way to revoke one.

Agents are now rows in a new Agent table, authenticated by SHA-256 token
hash before the connection is registered or the welcome payload is sent.
Tokens are minted by POST /api/agent/enroll and shown once. Revoke and
rotate drop the live socket immediately. All agent actions are audited.

The Ed25519 command-signing key was generated in the AgentManager
constructor, so it changed on every restart and the public_key pinned in
an agent's agent.yml stopped matching. It now lives in OpenBao at
secret/agent/signing-key; if it cannot be loaded the SSO refuses to send
high-risk commands rather than signing with a key no agent has seen.

DIRECTORY

Agents bind to a host resource instead of being matched by hostname, and
a bound agent's discovery is written onto that resource -- previously the
one source running ON the host contributed nothing to the directory.

The resource tree is collapsible, with state persisted per browser.

DISCOVERY

The Proxmox plugin zipped MACs and IPs from two flat lists by index,
attributing addresses to the wrong NIC on multi-NIC guests. NICs are now
keyed by MAC. Adds an endpoint resource parenting each node, sourceId/
vmid/node identity, container-interface filtering, node IP/MAC, and
offline-node handling.

The reconciler could make a resource its own parent, named hosts after
their MAC address, had a dead isIp() regex (\\. matches a backslash),
merged across kinds, and re-read the whole inventory per resource.

Dockerfile.test-runner never copied nodejs/plugins, so every plugin test
suite failed in CI as "Cannot find module".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 18:44:37 -04:00
wmantly 49100c9b68 fix: agent REST router mounted before 404; promote opens pre-filled modal; Directory refresh; Vault OpenBao (v1.28.0) (#169)
- api_agent: REST router mounted synchronously in app.js (was post-listen, behind
  the 404 catch-all -> /api/agent/* 404'd); WS init stays on onListen
- directory.ejs: promote opens a pre-filled resource modal (Save confirms);
  addEdge/removeEdge call loadResources() (was undefined loadData -> stale table);
  addGroup/removeGroup refresh the Access column
- vault.ejs: 'Powered by OpenBao' header badge
v1.28.0
2026-08-05 02:59:58 -04:00