@open-matrix/system-auth (1.1.32)

Published 2026-10-24 11:05:50 +00:00 by hivecast-admin

Installation

@open-matrix:registry=
npm install @open-matrix/system-auth@1.1.32
"@open-matrix/system-auth": "1.1.32"

About this package

@open-matrix/system-auth

Bus-addressable Matrix authentication primitives.

Related UX research: signup, provisioning and pairing. The proposals preserve task continuation and distinguish account availability, device approval, saved credentials and local readiness. Starter defaults do not imply native installation or an implemented pack editor. This is interaction guidance for consideration, not a new auth protocol, consent rule or provisioning owner.

Home projections serialize the services account's resolver read, lane merge and claim publication by its public key across all adapters in the owning system.auth process. Concurrent homes retain each other's machine-inference and chat-activity imports and live revocations. The existing per-home barrier still orders credential issuance and retirement. Deploy shares one offline runtime-credential reconciler: mint-account receives no resolver connection and preserves an existing sealed credential. Only system.auth publishes online changes to the services claim; deploy's birth state cannot replace its signed-up homes. Initial broker rendering still owns birth claims before the service starts. Run pnpm exec node --import tsx --test src/__tests__/nats-authority-claim-race.spec.ts src/__tests__/deploy-services-claim.spec.ts from this package for the merge and deploy regressions. The deploy regression requires nats-server on PATH, starts an isolated resolver, and checks the live claim and credential across initial issuance and replay without Docker.

The starter-application writer keeps one services-account NATS session and renews its scoped credential at four-fifths of the JWT lifetime through the shared SDK refresh owner. JWT callbacks read the renewed credential on the same connection, preserving its reply inbox across expiry. A closed connection, or a reconnecting connection whose current credential is expired, is closed and replaced before another projection is sent. The issuer's 300-second TTL is not changed. starter-writer-credential-refresh.spec.ts proves this boundary with an isolated real nats-server and a seconds-long credential.

profile.update announces profile.changed after saving, with an empty payload. Recipients come from auth.namespace.list for the authenticated principal, never from the request. The platform publishes on system.profile.<home>.$events; the account bridge requires a signed stream activation for that exact home and imports it as <home>.system.profile.$events. Other accounts cannot import it. Consumers subscribe before profile.get and reread on invalidation or reconnect. The existing account reconciliation installs these imports; browser user grants already allow their own home subtree, so no credential replacement is required.

People management uses the same browser-reachable system.profile boundary. profile.people.* and profile.homes.invitation.accept resolve the caller only from the supplied bus token, allow-list their forwarded fields, and invoke the scope-protected membership operations internally. Browser credentials never gain direct access to the platform-owned system.auth inbox.

Conversation sharing is a separate per-person PostgreSQL consent record. The browser reads and changes it only through profile.sharing.get and profile.sharing.set; each change and each consented builder read is audited. The record contains only the principal, enabled flag and update time—never a transcript. The cross-account system.auth.introspect mount admits the verified owner, or a verified platform.operator while consent is currently enabled. It refuses a builder read if the audit ledger cannot acknowledge that read.

Application release lifecycle calls use that same browser-reachable profile boundary. The facade resolves the caller from the bus token, allow-lists release, install, update, rollback, uninstall, withdraw and delete fields, and forwards only that verified token to the services-owned release facade. Browser credentials never gain direct access to the Component Store account.

The profile facade similarly exposes profile.tickets.file/list/get/summary while forwarding only allow-listed ticket fields and the verified bus token to system.tickets. Hosted agent sessions expose their ticket context only to the session owner, and stamp stored turns with the request trace used for later ticket evidence. session.replay runs selected user turns in one new session, in order, with one caller deadline and stops at the first refused prompt. The operator-only facade also forwards classified transition fixes and bounded profile.tickets.smells queries; it does not infer fix metadata from commit messages or caller identity.

Discovery follows that boundary through profile.discovery.search. It resolves the caller from the session bus token, selects only a home projected for that verified principal, and forwards an allow-listed query to that home's system.directory registry.search. A caller-supplied authority root cannot redirect the search into another home.

This package owns Host session tokens, principal state, credential metadata, the tenant-imported system.auth actor surface, and the platform-account-only system.auth.controlPlane inventory and bounded operator surface. Host Service may route HTTP requests to these primitives during the transition, but the state and bus interface live in this package.

The platform-only control plane includes exact scalar authority-root resolution without principal inventory disclosure and reversible hosted-probe retirement. Retirement suspends the principal, disables signup-sourced hosted-service eligibility, and revokes exact active browser-session NATS users; partial revocation is a typed, idempotently retryable result. The retirement receipt does not claim synchronous responder shutdown. The registry and hosted-application service hosts observe disabled eligibility on their existing 30-second reconciliation loop; closure must wait beyond one complete loop and independently prove both responder classes absent for each retired root while the sanctioned seed remains responsive.

Account deletion records unfinished state owners. WO-2.15 resumes the existing finishing pass at system.auth startup, when an owner's readiness changes to READY, after a new deletion leaves unfinished owners, and periodically while owners remain unfinished, without an operator or a deleted person's session. An empty pass stops polling. The Auth Console's Finish cleaning up remains available. Each pass retains its 3,000 ms budget; an in-flight owner may exceed that budget, so it is a bound on starting another owner, not a cancellation deadline. Passes share one in-flight background operation, and a cursor prevents a slow refusing owner starving later work. A deletion committed during that pass replies successfully with its unfinished owners immediately and requests another pass, so the self-deletion reply never waits behind an older account's owner.

The retry delay is max(3,000 ms, ceil(measured pass duration × 10)), measured afresh with performance.now() after every pass. The minimum reuses the existing finishing budget; otherwise at least ten pass-costs separate attempts. The in-process real-owner regression on 2026-10-03 measured a healthy fresh-home self-deletion at 175 ms and the foreground prefix at 55 ms (148 ms on the first run), yielding the 3,000 ms minimum locally. Production passes that take longer automatically reduce the retry rate; no RPC timeout or TTL is increased. Readiness bursts coalesce and retries do not overlap. Each principal/owner refusal logs its error once on entry and once on recovery; repeated refusals do not repeat the log. The diagnostic state is retained only for unfinished owners. Disposal cancels the timer and watches, aborts the pass before it starts another owner, and waits for its outstanding owner call before disposing the actor.

The regression reproduces the three-owner suffix seen on staging by exhausting the foreground budget during hosted-application withdrawal: registry, notifications and directory are not reached on that pass. A subsequent owner-ready transition removes a real directory row while retaining an unrelated row, after the person's session is already invalid. A healthy no-machine home completes synchronously. The staging pass timings were not available to this isolated fixture, so that specific staging cause still requires the coordinator's browser re-walk. Run pnpm exec node --import tsx --test src/__tests__/account-deletion-convergence.spec.ts src/__tests__/platform-operator-operations.spec.ts from this package for the real-owner boot and refusal-then-confirmation, startup, periodic, fairness and disposal regressions.

system.notifications is asked before the directory via notification.account.purge {principalId}; the inbox owner independently checks the auth deletion ledger and resolves its homes. Migration 038 adds this owner to prior deletions, including completed ones. See docs/platform/accounts.md for the complete ownership order.

Completed device enrollment is a durable notification source. The platform-only auth.device.notification.get {eventId} operation on system.auth.controlPlane resolves the recipient from the approved principal's primary home and the device from its installation association. It returns no credentials. Tenant imports of system.auth do not expose this operation. Only the first completed enrollment for a device created within 30 days produces a notification; later enrollment replays cannot create a new source identity.

Migration 039 adds a delivery watermark to the existing authorization completion row. Finalization, startup and the existing device reconciliation cadence replay up to 100 pending completions from the last 30 days. Refused delivery retains the row and reports a warning. Inbox commits deduplicate by canonical device source; a crash between delivery and its watermark cannot create a second card.

Owned accounts (docs/decisions/0015) notify the same way. A claim, a release from either side, and an owner's suspend or reactivate queue a row in account_owner_notices (migration 061). The projection hints notification.source.reconcile {sourceKind: 'account.owner'}, and system.notifications reads the recipient's home and text back from the platform-only auth.account.owner.notification.get {eventId}. A delivered row is deleted; a refused one stays and is retried.

Durable management audit

PostgreSQL owner transactions enqueue successful account deletion (self/operator), operator grant/revoke, profile update, application selection, device enrollment, label update and revocation outcomes. An intent contains actor, operation, target, result and the home before deletion. Platform-wide operator changes use the configured platform root, not a fabricated home. Audit intents survive account content deletion and are removed only after the ledger acknowledges them.

Replay starts with the actor and uses its existing 60-second maintenance cadence: up to 100 rows per sweep, at most 10,000 pending rows per owner. Capacity refusal rolls back the management mutation instead of dropping an audit event. A lost reply reuses the intent UUID as the ledger idempotency key. These bounds keep outages from growing owner storage indefinitely and let normal work remain cheap. Organization creation, membership add/remove, clean-name activation, service-principal creation and service API-key issue/revoke also enqueue in their owning SQL transactions. Their actor comes from verified bus claims or namespace-administration admission. No-op revocation/name retries add no event; refused transactions add no success record.

Home invitations are PostgreSQL-owned records on the existing Space membership model. Owners and admins create or withdraw them through namespace-administration admission; acceptance takes its principal only from a verified bus token. The link contains a random, single-use code while storage retains only its SHA-256 digest. Invitations expire after seven days, which bounds the lifetime of a forwarded link without making ordinary handoff timing fragile. Create, withdraw and accept enqueue their audit event in the same transaction as the record or membership change. A personal home becomes organization-owned on its first invitation, preserving its owner as the immutable owner membership. Account deletion follows that ownership and removes the shared home while leaving invited members' personal homes intact. Namespace discovery includes each visible home's membership role (owner, admin, or member), so browser surfaces can distinguish management authority from ordinary entry.

Explicit agent session.create, $sessionDelete, $sessionClearAll and $sessionSeed mutations enqueue in the same transaction as their session rows. A verified operation context or signed bus session supplies the audit actor; a payload principalId remains row owner metadata for owner-scoped listing and audit outcomes, never audit identity. A cheap existence check first lets idempotent creates and absent deletes avoid an unused authentication round trip. When validation is needed it completes before opening the mutation transaction; the subsequent insert/delete and outbox write remain atomic, and a concurrent duplicate create is harmless and emits no record. An invalid or expired token returns the typed AUTHENTICATED_PRINCIPAL_REQUIRED refusal, while an authentication transport failure retains its request/reply code instead of being mislabeled as an authorization refusal. Calls with no caller proof are labeled actorType: system. Delete records name the caller and retain the stored owner in the outcome; clear-all records its cleared count. The implicit session created by a hosted Chat $prompt is conversation persistence, not a management mutation, and does not enter the capacity-checked management outbox.

Profile updates, device label/revocation and application selections retain the existing trusted internal profile-facade provenance: that facade resolves the account identity before forwarding its principal. The central mutation does not independently verify that forwarded human identity. Application selection requires an explicit actor and never infers the actor from the home's owner.

Installation enrollment-token issuance forwards the already verified browser session from the profile facade to the API-key owner. The owner validates that proof and records the person, rather than system, on the single auth.apiKey.issue intent committed with the short-lived enrollment key.

Principal creation/identity linking, anonymous password signup, password reset, space creation/default selection, namespace claim, ordinary API-key issue/revoke, workspace-realm changes and seed application also commit one owner intent. The PostgreSQL workspace-realm operation now updates the durable principal instead of the unused legacy file store. Seed application writes one event for its whole transaction; unchanged seed retries add none. Namespace claim returns its committed claim immediately; its former best-effort audit append response is no longer returned.

Raw central operations already require the platform broker boundary. Calls without human proof at that boundary record actorType: system, with the source owner as subject and no invented principal: this includes anonymous signup, provider-driven creation and internal credential finalization/compensation. This identifies the service performing provisioning, not an authenticated signup user. A supplied bus token, browser headers or API key must validate; invalid proof never falls back to system. Verified ordinary mutations bind the target to that principal. HTTP key management forwards browser proof or the API key and rechecks its existing account:api-keys:manage scope; CLI authorization-code exchange forwards its saved browser proof. Namespace and workspace-realm HTTP operations forward browser proof. No credentials, seed contents or password material enter audit intents.

Device approval, denial and cancellation commit one intent with the verified approver, request ID and approved/denied outcome; stale and repeated decisions add none. Failed/refused operations and runtime mutations remain outside this producer; runtime/host-control is separately owned. Credential issuance/session refresh and NATS projection/reconciliation are authentication transport, not account-management mutations in this matrix. File-backed development auth has no PostgreSQL outbox.

The platform-owned service bridge also carries system.notifications and system.audit requests into the platform account; notification events return through a protected stream import for hosted-home fanout. These central owner inboxes are never imported by tenants.

Runtime-host registration and pairing

An account-wide key cannot activate a first-seen machine. auth.apiKey.exchangeForRuntimeHost records it with pending association state and returns DEVICE_PAIRING_REQUIRED before issuing any broker credential, including when a legacy caller omits installId. Repeated attempts retain the pending device ID. Browser approval or an installation-bound enrollment token completes pairing and records installation key and broker credential lineage. Demo provisioning uses that token path. Existing legacy active rows retain their state and the suspension/deletion lineage refusals; registration does not relabel an already-issued untracked credential as harmless pending enrollment.

The account-key-pairing.spec.ts regression runs the real auth actor and stores against isolated PostgreSQL, observes zero issuer calls for pending devices, and exercises the demo provisioner's identity client with requests dispatched into the real token finalizer and runtime-host exchange.

Machine removal with account deletion

PostgresAccountDeviceStore and PostgresInstallationGrantStore own machine enrollment. The account device row also owns the machine-to-HiveDock join: authenticated registration records a supplied HiveDock ID, and the next managed heartbeat reads and backfills it without a manual step. The account-deletion transaction locks installation grants and snapshots their unretired NATS public identities into migration 041’s pending_device_credentials ledger column before removing enrollments and grants. The account-devices finalizer runs first and asks the credential authority to revoke each identity, including derivatives in another home. Successful revocations remove only their own public identity; failure keeps the owner unfinished for the existing retry surface. A replay cannot purge a live account. No secret is retained. Incomplete legacy lineage refuses deletion before credential cuts; re-enroll or retire the machine first. Already-deleted lineage cannot be recovered.

auth.platform.accounts.list includes machineCount, the number of active device associations, including pending enrollment. Auth Console uses it in single and bulk confirmations; Settings reads its own current device list before opening confirmation. This disconnects platform access and forgets enrollment, not the physical computer’s OS. No new retry interval, timeout or TTL is introduced by this finalizer.

Run pnpm --filter @open-matrix/system-auth test. The deletion-store regression checks retained identities, retries, live-account refusal and incomplete lineage. The real NATS factory spec deletes an enrolled account, verifies its connected credential is cut and cannot reconnect, and verifies a bystander credential in the same root still works.

Account suspension and reactivation

auth.platform.account.suspend {principalId, reason, sessionToken} and auth.platform.account.reactivate {principalId, sessionToken} require a platform operator. They refuse self, other operators and service principals. Migration 042 records a monotonic session epoch, current session families, retained service selections and unfinished public credential revocations. Issuance holds the account row lock; suspension captures credentials before committing the inactive account. Each completed cut is durable. Retry Suspend finishes a partial cut; Reactivate refuses until all cuts finish. Enrollment, keys and account data stay. sessionCount is null for legacy accounts whose old sessions were never inventoried; the console says unavailable. New sessions share one family across sliding renewal. Older epochs never revive. The password path checks the password before disclosing suspension and keeps the existing backoff.

The notification uses existing operator-authenticated announcement.post, home-targeted, with its banner expiry taken from that operator session's existing policy. Failed delivery remains visible in inventory for an operator retry; no session token is stored. There is no new timer, retry interval or TTL. A crash after posting but before acknowledgement may duplicate a notice.

The retained-installation refresh seam accepts an omitted profile label only for an authenticated installation grant: grants and device binding, not that label, determine its credential authority. Runtime-host-engine and identity-client own sealed-file refresh; host-auth-store owns the signed session epoch and stable family. Those are the necessary owner changes outside system-auth. pnpm --filter @open-matrix/system-auth test includes real password/session suspension and reactivation, durable-cut retry, real NATS old-credential refusal and fresh-credential reconnect.

The account-device store spec runs against an isolated PostgreSQL wire-protocol database in the default test suite. It covers owner-scoped rename, revision fencing, last-seen updates, and retirement retained across a refused reconnect; it does not need a live environment.

Home convergence visibility

auth.platform.home.status and auth.platform.home.retry require the session's platform.operator capability before reading or changing a home. The profile facade exposes these as profile.platform.home.status / profile.platform.home.retry. A retry resolves the active home owner and calls HomeReconciliationCoordinator.retryNow: it bypasses only that home's backoff, refuses an already-satisfied projection, and still respects the existing durable lease and concurrency admission. auth.platform.homes.notReady returns every durable receipt whose state is not ready, oldest change first, including the secret-redacted diagnostic and last-change time needed during an outage.

auth.platform.principals.count and auth.platform.conversations.count expose owner-computed inventory to operators; conversations remain grouped by home and target key. The profile facade also reaches Registry claim and Directory row count operations through their well-known owner mounts. Every non-operator refusal names platform.operator and its owner, system.auth.

The canonical account.lifecycle notification source alerts every current operator home when an account deletion remains unfinished for 15 minutes. Normal foreground cleanup has a three-second budget, so the horizon leaves ample time for transient owner recovery without hiding a sustained outage. The home.projection source uses the same 15-minute horizon, spanning three maximum five-minute coordinator backoff windows. It creates one card per home failure episode and clears that card when the home becomes ready. Both cards direct operators to the Auth Console; their audience and content come only from the auth owner's durable rows. A founding operator's grant is still recorded lazily by the first platform.operator capability check; until then that account is intentionally absent from lifecycle-notification snapshots.

profile.home.status resolves the caller through the normal profile authentication path; it accepts no principal override and cannot select another home's state. Its central read, auth.home.status, uses that principal's current primary authority root. All reads go through PostgresHomeProjectionStatusStore, returning state, attempt, next retry, safe step words, last error code, failure start and last confirmed convergence. Platform-operator reads also return the secret-redacted lastErrorMessage persisted by migration 046; person-scoped reads deliberately omit that diagnostic. Migration 043 preserves the last convergence across failures and the first failure across retries. History before this migration is unknown, never inferred from an unrelated row update.

The actor's periodic bounded sweep calls resumeOutstanding every minute. A recovering home whose next_retry_at is past and whose lease is free is selected again automatically, so a deployment that restores the failed dependency converges stuck homes without logout, reload, or an operator retry. The real-Postgres reconciliation suite runs against an isolated database in the default package gate and covers overdue attempt-one and attempt-two rows directly through the production selector. Acquiring a retry lease retains the previous operator diagnostic while the new attempt is in flight; only that attempt's settlement replaces or clears the message.

Removing untracked offline devices

profile.devices.forget {deviceId, expectedAssociationRevision} resolves the caller exactly like device revocation and delegates to auth.device.forget. Only an owned device with no installation grant (including historical grants) and a known last-seen older than DEVICE_OFFLINE_AFTER_MS can be forgotten. Refusals are DEVICE_HAS_GRANT (use Retire), DEVICE_RECENTLY_SEEN, DEVICE_NOT_FOUND, and ASSOCIATION_REVISION_STALE.

Migration 044 preserves the row as revoked with forgotten_at; the list excludes it and suspension no longer treats it as active incomplete history. The same transaction writes one account audit intent. An exact retry returns the completed removal without another audit. A returning installation receives a new pending device ID, retaining the old history. Legacy host credential exchange checks inventory before issuance and returns DEVICE_PAIRING_REQUIRED until pairing grants authority; repeated check-ins stay pending. The owner schema and package/control declarations are required companion changes outside src.

Run pnpm --filter @open-matrix/system-auth test for store and in-process actor regressions.

Agent session credential redaction

PostgresAgentSessionStore applies the shared @open-matrix/core credential redactor before writing message content, titles and session state. The SQLite backfill uses the store's same helpers for titles, message content, tool arguments/results and state. Derived titles redact the complete first message before the 72-character summary, including for pre-existing messages.

State uses shape-only, span-scoped redaction on decoded JSON string values or plain text. It has no recursive depth cutoff and never withholds a value merely because its field is named secret, password or apiKey. Credential-free serialized state retains its exact bytes, including whitespace, escaped characters and large numeric literals. This covers prompts, seeded transcripts, direct canvas/harness state writes and failure activity without replacing deep documents. The home-side AgentsStore.saveSessionState also preserves ordinary state, but stores it verbatim; the two implementations agree on ordinary documents, not on credential-bearing state at rest.

agent-session-redaction.spec.ts exercises the actor through request/reply and checks the raw rows in a disposable PGlite database; it requires neither Docker nor a running environment. agent-session-backfill.live.spec.ts verifies the actual SQLite import on PGlite and checks all imported content fields. Redaction covers recognized shapes in these content fields, not arbitrary metadata identifiers or unrecognizable secrets. Existing rows are not scanned or migrated, and backfill still leaves conflicts untouched. Their cleanup decision is recorded in 2026-10-03-platform-agent-sessions-replay-credentials.

Password signup and Settings share readProfileDisplayName from @open-matrix/contracts: trim, collapse whitespace, and cap at 120 characters. Registration requires a nonblank displayName; the password principal stores that name. OAuth retains the provider's name.

Schema initialization

SystemAuthActor.onBootstrap awaits the schema owner before starting account or home reconciliation. PostgreSQL accessors use the initialized database without process-local readiness requirements, so the HTTP runtime's shared admission reader remains usable. The actor fences every declared auth operation until its schema is ready; password sign-in cannot start or retry migrations. A database outage at boot intentionally fails service startup; supervision restarts the service instead of running it in a degraded state. Auth requests arriving before or during initialization, or after a failed startup, immediately receive { ok: false, error: "AUTHENTICATION_SERVICE_UNAVAILABLE", retryable: true }. Only an explicit service start or administrative initialization retries a failed migration. Standalone seed administration and tools/migration/backfill-agent-sessions.mjs initialize explicitly. The backfill command leaves report-only discovery read-only and closes its database pool on exit. Store-only tests must initialize their disposable database with migrateSystemAuthPostgres before querying it.

password-login-schema.spec.ts counts migration-table statements on an already-migrated PGlite database and covers cold requests, startup, failed startup and explicit startup retry. It also drives auth.nats.account.ensure and password login through the subscribed inbox before bootstrap and during a held migration, then verifies account ensure after startup. agent-session-backfill.live.spec.ts also runs the standalone command against a fresh database and checks the copied conversation. These regressions require neither Docker nor a running environment.

Conversation target keys

Platform prompts and Chat share SDK core's canonicalTargetKey: <authorityRoot>\u001F<logicalMount>, with an optional interface qualifier. $sessionList reads canonical, legacy dotted, and legacy bare-mount keys in a single tenant-scoped query, with the existing ordering and result limit. Stored keys are not rewritten. Interface-qualified sessions remain separate. agent-session-target-key.spec.ts covers platform-created conversations, legacy reads, tenant isolation, and preservation of stored keys.

Conversation list pages

$sessionList accepts targetKey or principalId, plus optional cursor and limit. Pages default to 50, clamp to 1–100, and order by last activity then session ID descending. Pass the returned nextCursor unchanged to fetch older conversations; its absence means the end. Cursors retain the ordering values, so deleting the boundary conversation does not skip the following page. Every page retains the existing tenant/selection checks. There is no title-search operation yet; that is tracked in the Chat title-search queue item.

Dependencies

Dependencies

ID Version
@node-oauth/oauth2-server 5.3.1
@open-matrix/contracts 1.1.32
@open-matrix/core 1.1.32
@open-matrix/host-auth-store 1.1.32
@open-matrix/nats-auth 1.1.32
@open-matrix/nats-authority-port 1.1.32
@open-matrix/package-runtime 1.1.32
@open-matrix/sqlite-store 1.1.32
@open-matrix/subjects 1.1.32
@open-matrix/system-audit 1.1.32
@open-matrix/system-postgres 1.1.32
nats ^2.29.3

Development Dependencies

ID Version
@matrix/test-utils 1.1.32
@open-matrix/nats-authority-nauth 1.1.32
@types/node ^25.0.10
esbuild ^0.25.0
tsx ^4.0.0
typescript ^5.3.0

Peer Dependencies

ID Version
@open-matrix/nats-authority-nauth 1.1.32
Details
npm
2026-10-24 11:05:50 +00:00
0
410 KiB
Assets (1)
Versions (33) View all
1.1.33 2026-10-24
1.1.32 2026-10-24
1.1.2 2026-09-22
1.1.1 2026-09-22
0.1.35 2026-09-22