Audit Signing Key Source Selection
meridian intermediate 4 min read
ELI5
The signing pen has two forms: in production it lives in a locked drawer with serial numbers and rotation history, and in dev it is a disposable felt-tip you grab fresh every time the app starts. The disposable one is fine because dev has no permanent ledger anyway.
Technical Deep Dive
src/lib/audit-keypair.ts exposes getAuditKey(): Promise<KeyMaterial> returning { privateKey, publicKey, kid }. The branch is the presence of DATABASE_URL.
Selection Flow
flowchart TD CALL[getAuditKey] --> ENV{DATABASE_URL set?} ENV -- yes --> POOL{cachedStore?} POOL -- no --> NEWPOOL[new pg.Pool<br/>new AuditKeys store] POOL -- yes --> ACTIVE NEWPOOL --> ACTIVE[store.activeKey] ACTIVE --> RET1[return privateKey, publicKey, kid] ENV -- no --> DEVCACHE{cachedDev set?} DEVCACHE -- no --> GEN[generateKeypair<br/>kid = 'meridian-dev'] DEVCACHE -- yes --> RET2 GEN --> RET2[return cached dev key]Production Path
AuditKeys (from @stratt/orchestrator/src/audit/keys.js) reads the active row from the audit_keys table. Rotations insert a new active row but preserve the previous rows so previously-minted tokens still verify against their original kid until their envelope exp. The kid is what lets a verifier pick the right historical key.
Dev Path
generateKeypair() from @stratt/signature runs once per process; cachedDev is a Promise<KeyMaterial> (so concurrent first calls share the same generation). The kid is the literal string meridian-dev. Restarting the process produces a fresh pair, so any token minted before the restart fails Ed25519 verification — which is correct because dev has no audit_keys row to look the old key up in either.
Back-Compat Alias
getAuditKeypair() returns just { privateKey, publicKey } for callers that predate the kid-aware API. New code should call getAuditKey() so the kid travels with the material.
Key Terms
kid→ Key id; stamped on every minted token so verifiers can pick the right public key from a JWKS-equivalent.AuditKeys.activeKey→ Returns the currently-active row from theaudit_keystable.@stratt/signature→ Workspace package providing the Ed25519generateKeypairprimitive.
Q&A
Q: Why is cachedDev a Promise rather than the resolved value?
A: It serialises concurrent first callers — both await the same in-flight generateKeypair() instead of racing and producing two pairs.
Q: Can STRATT_KMS_KEY alone activate the production path?
A: No — getAuditKey only checks DATABASE_URL. KMS encryption of the private key happens inside AuditKeys, but without a database there is nowhere to read encrypted material from.
Q: What kid do production tokens carry?
A: Whatever audit_keys.activeKey returns — typically a UUID or human label assigned at rotation time, never meridian-dev.
Examples
Local bun dev with no DATABASE_URL: every minted audit URL works for the lifetime of the dev server. Restart bun dev, refresh an audit page from before, see “unable to verify — bad_signature”. Expected.
Production deploy with DATABASE_URL: rotate by inserting a new audit_keys row with active=true (and demoting the old one). Newly-minted tokens carry the new kid; tokens minted yesterday continue to verify against the demoted row until their exp.
neighbors on the map
- DeploymentRef & VAULT Credential Path auditing where a workspace credential is stored
- LORE+CAIRNET Deployment Topology & Service Map understanding the LORE deployment architecture
- Deployment Topology & Proxy Conflict Resolution setting up a new environment (kitten/cat/lion)