Release v0.81.0
Release v0.81.0 — DB-backed editor user registry (HA fix for F4)
The post-hardening audit’s finding F4 noted that the named-user editor registry was
per-instance (in-memory + flat file): in HA it diverged across replicas, and with
OIDC_AUTO_PROVISION on, a shared EDITOR_USERS_FILE was clobbered last-writer-wins. v0.80.0
documented and warned about this and deferred the real fix. This release implements it.
In STORAGE_MODE=postgres/sqlserver, editor users now live in a shared editor_users table, so
named users — including OIDC auto-provisioned ones — are consistent across all replicas. File mode
is unchanged (single-instance flat-file registry).
Architecture
UserRegistry is now an in-memory cache over an IEditorUserStore persistence port:
- Reads stay off the DB hot path. Auth lookups (
getById/getByKey/getByOidcSub) serve from the cache; on a miss they read through to the store (so a user created on another replica is visible immediately), and the cache does a periodic full reload (EDITOR_USERS_CACHE_TTL_MS, default 10 s). - Local writes are immediate; cross-replica propagation of role changes /
tokenIssuedAfterrevocations / deletes is bounded by the cache TTL (new users are immediate via read-through). - Three adapters: File (single-instance flat file, 0o600), Postgres (Prisma), SQL Server
(mssql). The registry is built over a bootstrap in-memory store in
authPluginand atomically swapped to the real store afterstoragePluginboots.
Behavior changes
- Seeding: in DB mode,
EDITOR_USERS_JSON/EDITOR_USERS_FILEnow seed an emptyeditor_userstable on first boot only (under a seed-only-when-empty guard, so a user deleted from the DB is never resurrected from config). After that the DB is authoritative. Concurrent multi-pod boot converges via the table’s unique constraints. POST /admin/users/reloadnow reloads the in-memory cache from the authoritative store (file in file mode, DB in DB mode) rather than re-readingEDITOR_USERS_FILE+ replacing.EDITOR_USERS_DB=true(new) enables DB-backed named-user mode with no JSON/FILE seed (bootstrap the first user with an admin API key viaPOST /admin/users). Valid only withSTORAGE_MODE=postgres|sqlserver.- The v0.80.0 file-registry HA divergence startup warning is removed in DB mode (the divergence is
fixed); it remains relevant only to file mode (single-instance).
identityModenow also resolves tonamed-usersonce the DB registry is non-empty, even without a JSON/FILE seed.
Security
- Editor keys remain stored recoverably (they double as the editor-token HMAC secret) — plaintext,
parity with the 0o600 file. DB dumps contain editor login/token-verification secrets —
access-control the database. Keys are never logged; only
keyHint(last 4 chars) is exposed. - SQL Server
id,[key], andoidc_subuse a case-sensitive binary collation (Latin1_General_BIN2) so a secret lookup cannot match case-insensitively (Postgres is case-sensitive by default). - DB-loaded rows are validated against active API keys: a collision fails boot (attach) or is skipped + logged on a runtime reload.
Schema & migrations
- Prisma
EditorUsermodel + migration20260602000000_add_editor_users. - Hand-written SQL Server migration
012_editor_users.sql(boundedNVARCHAR, binary collation, filtered-uniqueoidc_sub), kept in lockstep with the Prisma schema by thesqlserver-schema-paritytest (EditorUseradded to its curated set).
Tests
editor-user-registry.test.ts— store contract (CRUD, duplicate-constraint naming,NOT_FOUND) and cache behaviour (read-through, TTL reload, write-through, attach seed-when-empty + no resurrection, API-key collision fail/skip).postgres-editor-user.store.test.ts— DB-gated adapter tests (CRUD, constraint naming, case-sensitive lookups, null-oidc_submultiplicity, default-tenant omission).oidc-registry.test.ts/user-registry-persist.test.tsupdated to the new async cache + the file adapter;config-validation.test.tscoversEDITOR_USERS_DB(file-mode rejection, invalid value) andEDITOR_USERS_CACHE_TTL_MS.
Deferred
- A management UI for editor users (CRUD exists via
/admin/users). - The DB-backed registry does not change file mode, which remains single-instance.