@revealui/auth 0.4.10 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Audit storage boundary + signer composition — the ONE shared home (GAP-338).
3
+ *
4
+ * Moved here from `apps/server/src/lib/{audit-storage,audit-signer}.ts` so BOTH
5
+ * server processes (Hono api + Next.js admin) can swap the process-wide
6
+ * `@revealui/security` AuditSystem onto persistent storage. Before this move the
7
+ * boundary was stranded in apps/server, so every admin-process audit emit
8
+ * (including the GAP-334 login receipts wired through `audit-bridge.ts` in this
9
+ * package) landed in that process's default `InMemoryAuditStorage` and
10
+ * evaporated on restart. `@revealui/auth` is the home because it already sits
11
+ * above `@revealui/security`, `@revealui/db`, and `@revealui/core`, is consumed
12
+ * by both apps, and owns the auth→audit bridge whose emits this rescues.
13
+ *
14
+ * This is THE boundary between two audit models that intentionally differ
15
+ * (see docs/decisions/2026-07-12-audit-receipt-architecture.md §5):
16
+ *
17
+ * - `@revealui/security` emits `AuditEvent` with severity
18
+ * `low | medium | high | critical` (the request-middleware vocabulary).
19
+ * - `@revealui/db` / `@revealui/ai` own `audit_log`, whose CHECK constraint
20
+ * accepts only `info | warn | critical`. That model wins; the security
21
+ * vocabulary is mapped here, at the boundary, so every emitted event lands
22
+ * instead of being silently rejected by the constraint.
23
+ *
24
+ * Signing (GAP-355 Stage 3): the `DrizzleAuditStore` is built via
25
+ * `createAuditStore`, which injects the process-wide Ed25519 row signer. On a
26
+ * signing deployment every row lands with a `v1.ed25519` signature verifiable
27
+ * offline from the published public key; in dev/test (no key) rows stay honestly
28
+ * unsigned. `previous_signature` is never written (the hash chain is abandoned),
29
+ * which is what makes the two-writer topology (api + admin processes appending
30
+ * to one `audit_log`) safe: rows are independent, each signed at its own door
31
+ * with the same env-derived key. Ruled for GAP-338 — see the gap file.
32
+ */
33
+ import type { AuditRowSignerFn, DrizzleAuditStore as DrizzleAuditStoreType } from '@revealui/db';
34
+ import type { Database } from '@revealui/db/client';
35
+ import type { AuditEvent, AuditQuery, AuditSeverity, AuditStorage, AuditSystem } from '@revealui/security/server';
36
+ /**
37
+ * The process-wide audit-row signer (composed once from `process.env`), or
38
+ * `undefined` in unsigned mode. Cached so the mode is resolved and logged
39
+ * exactly once per process (GAP-355 Stage 3, spec D5/D6: key present → SIGNING;
40
+ * absent → UNSIGNED, legal only in dev/test — a production signing deployment
41
+ * refuses to boot without the key).
42
+ */
43
+ export declare function getAuditRowSigner(): AuditRowSignerFn | undefined;
44
+ /**
45
+ * Construct a `DrizzleAuditStore` wired to the process-wide signer. Every audit
46
+ * writer builds its store through this helper so a row written through the one
47
+ * door on a signing deployment always carries a signature.
48
+ */
49
+ export declare function createAuditStore(db: Database): DrizzleAuditStoreType;
50
+ /** Test-only reset of the cached signer (re-reads env on next `getAuditRowSigner`). */
51
+ export declare function __resetAuditSignerForTest(): void;
52
+ /** DB-side severity vocabulary — matches the `audit_log_severity_check` constraint. */
53
+ type DbSeverity = 'info' | 'warn' | 'critical';
54
+ export declare function mapSeverityToDb(severity: AuditSeverity): DbSeverity;
55
+ /**
56
+ * `AuditStorage` implementation backed by `DrizzleAuditStore`. Re-homes the
57
+ * row-storing responsibility onto `DrizzleAuditStore.append()` and keeps the
58
+ * security-model boundary mapping (severity + columns) here, so `@revealui/db`
59
+ * stays free of a dependency on the security package.
60
+ */
61
+ export declare class DrizzleBackedAuditStorage implements AuditStorage {
62
+ private readonly store;
63
+ constructor(db: Database);
64
+ write(event: AuditEvent): Promise<void>;
65
+ query(query: AuditQuery): Promise<AuditEvent[]>;
66
+ count(query: AuditQuery): Promise<number>;
67
+ }
68
+ /**
69
+ * Synchronous env-parity assertion for the audit write path — GAP-355 Stage 1
70
+ * closure, owner-ruled 2026-07-17.
71
+ *
72
+ * A serving process installs audit storage but cannot always run the async
73
+ * round-trip self-test (serverless has no clean "refuse to serve" for a
74
+ * boot-time DB round trip). This asserts the audit path's ENV preconditions
75
+ * SYNCHRONOUSLY at the install point, so a deploy whose audit-critical env has
76
+ * diverged (a required var missing or empty) FAILS THE DEPLOY rather than
77
+ * serving with an audit sink that silently drops every row.
78
+ *
79
+ * Scope, honestly: this catches env-var ABSENCE/emptiness synchronously — which
80
+ * `installAuditStorage()` itself does not, because `getClient()` is a lazy pool
81
+ * factory, so a missing connection URL would otherwise surface only on the
82
+ * first write, at request time. It does NOT, and on serverless cannot, catch
83
+ * migration-state divergence (the `audit_log` table missing or misshapen on the
84
+ * target DB); that needs the write-read round trip of `auditStorageSelfTest`.
85
+ */
86
+ export declare function assertAuditStorageEnv(env?: NodeJS.ProcessEnv): void;
87
+ /**
88
+ * Swap the process-wide `audit` system onto persistent Postgres storage.
89
+ * Synchronous and side-effect-free at call time: `getClient()` is a lazy pool
90
+ * factory (no connection opened here), so this is safe to run on a serverless
91
+ * cold-start path. WITHOUT this call, audit events fall into the default
92
+ * `InMemoryAuditStorage` and evaporate on restart — which is exactly what
93
+ * happened to every admin-process emit before GAP-338.
94
+ *
95
+ * Call `assertAuditStorageEnv()` before this at each install site so a
96
+ * diverged-env deploy fails loudly instead of installing a store that can
97
+ * never write.
98
+ */
99
+ export declare function installAuditStorage(): void;
100
+ /**
101
+ * Boot-time round-trip self-test. Writes a synthetic audit event through the
102
+ * REAL installed path (AuditSystem → boundary → DrizzleAuditStore → audit_log)
103
+ * and reads it back. Throws if the round trip fails, so a runtime that cannot
104
+ * record agent actions REFUSES TO SERVE rather than silently dropping them
105
+ * (fail-closed integrity — ADR §2a).
106
+ *
107
+ * Uses `severity: 'low'` deliberately: that value would be rejected by the DB
108
+ * CHECK constraint if the boundary mapping regressed, so this exercises the
109
+ * exact defect Stage 1 fixes. A healthy audit path lets startup proceed.
110
+ *
111
+ * Runs on the long-running worker (once per deploy) and the dev boot path,
112
+ * where an async boot chain already exists and `process.exit(1)` gives
113
+ * "refuse to serve" clean semantics. It is NOT run on serverless cold-start
114
+ * paths, which stay free of per-invocation DB round-trips (storage is still
115
+ * installed there synchronously by `installAuditStorage()`).
116
+ */
117
+ export declare function auditStorageSelfTest(auditSystem?: AuditSystem): Promise<void>;
118
+ export {};
119
+ //# sourceMappingURL=audit-storage.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"audit-storage.d.ts","sourceRoot":"","sources":["../../src/server/audit-storage.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAIH,OAAO,KAAK,EAAE,gBAAgB,EAAE,iBAAiB,IAAI,qBAAqB,EAAE,MAAM,cAAc,CAAC;AAEjG,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AACpD,OAAO,KAAK,EACV,UAAU,EACV,UAAU,EACV,aAAa,EACb,YAAY,EACZ,WAAW,EACZ,MAAM,2BAA2B,CAAC;AAWnC;;;;;;GAMG;AACH,wBAAgB,iBAAiB,IAAI,gBAAgB,GAAG,SAAS,CAgBhE;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,QAAQ,GAAG,qBAAqB,CAEpE;AAED,uFAAuF;AACvF,wBAAgB,yBAAyB,IAAI,IAAI,CAGhD;AAED,uFAAuF;AACvF,KAAK,UAAU,GAAG,MAAM,GAAG,MAAM,GAAG,UAAU,CAAC;AAe/C,wBAAgB,eAAe,CAAC,QAAQ,EAAE,aAAa,GAAG,UAAU,CAEnE;AASD;;;;;GAKG;AACH,qBAAa,yBAA0B,YAAW,YAAY;IAC5D,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAwB;gBAElC,EAAE,EAAE,QAAQ;IAOlB,KAAK,CAAC,KAAK,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC;IA0BvC,KAAK,CAAC,KAAK,EAAE,UAAU,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC;IA4B/C,KAAK,CAAC,KAAK,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC;CAMhD;AAgCD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,qBAAqB,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,IAAI,CA0ChF;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,mBAAmB,IAAI,IAAI,CAE1C;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAsB,oBAAoB,CAAC,WAAW,GAAE,WAAmB,GAAG,OAAO,CAAC,IAAI,CAAC,CAmB1F"}
@@ -0,0 +1,295 @@
1
+ /**
2
+ * Audit storage boundary + signer composition — the ONE shared home (GAP-338).
3
+ *
4
+ * Moved here from `apps/server/src/lib/{audit-storage,audit-signer}.ts` so BOTH
5
+ * server processes (Hono api + Next.js admin) can swap the process-wide
6
+ * `@revealui/security` AuditSystem onto persistent storage. Before this move the
7
+ * boundary was stranded in apps/server, so every admin-process audit emit
8
+ * (including the GAP-334 login receipts wired through `audit-bridge.ts` in this
9
+ * package) landed in that process's default `InMemoryAuditStorage` and
10
+ * evaporated on restart. `@revealui/auth` is the home because it already sits
11
+ * above `@revealui/security`, `@revealui/db`, and `@revealui/core`, is consumed
12
+ * by both apps, and owns the auth→audit bridge whose emits this rescues.
13
+ *
14
+ * This is THE boundary between two audit models that intentionally differ
15
+ * (see docs/decisions/2026-07-12-audit-receipt-architecture.md §5):
16
+ *
17
+ * - `@revealui/security` emits `AuditEvent` with severity
18
+ * `low | medium | high | critical` (the request-middleware vocabulary).
19
+ * - `@revealui/db` / `@revealui/ai` own `audit_log`, whose CHECK constraint
20
+ * accepts only `info | warn | critical`. That model wins; the security
21
+ * vocabulary is mapped here, at the boundary, so every emitted event lands
22
+ * instead of being silently rejected by the constraint.
23
+ *
24
+ * Signing (GAP-355 Stage 3): the `DrizzleAuditStore` is built via
25
+ * `createAuditStore`, which injects the process-wide Ed25519 row signer. On a
26
+ * signing deployment every row lands with a `v1.ed25519` signature verifiable
27
+ * offline from the published public key; in dev/test (no key) rows stay honestly
28
+ * unsigned. `previous_signature` is never written (the hash chain is abandoned),
29
+ * which is what makes the two-writer topology (api + admin processes appending
30
+ * to one `audit_log`) safe: rows are independent, each signed at its own door
31
+ * with the same env-derived key. Ruled for GAP-338 — see the gap file.
32
+ */
33
+ import { randomUUID } from 'node:crypto';
34
+ import { logger } from '@revealui/core/observability/logger';
35
+ import { DrizzleAuditStore, getClient, hasDatabaseConnectionEnv } from '@revealui/db';
36
+ import { audit, classifyAuditWriteFailure, createAuditRowSignerFromEnv, recordAuditWriteResult, } from '@revealui/security/server';
37
+ let cachedSigner;
38
+ let resolved = false;
39
+ /**
40
+ * The process-wide audit-row signer (composed once from `process.env`), or
41
+ * `undefined` in unsigned mode. Cached so the mode is resolved and logged
42
+ * exactly once per process (GAP-355 Stage 3, spec D5/D6: key present → SIGNING;
43
+ * absent → UNSIGNED, legal only in dev/test — a production signing deployment
44
+ * refuses to boot without the key).
45
+ */
46
+ export function getAuditRowSigner() {
47
+ if (!resolved) {
48
+ const { signer, mode, kid } = createAuditRowSignerFromEnv(process.env);
49
+ if (mode === 'signed') {
50
+ logger.info(`AUDIT SIGNING: ENABLED (alg=ed25519, kid=${kid})`);
51
+ }
52
+ else {
53
+ logger.warn('AUDIT SIGNING: DISABLED — audit rows will be written UNSIGNED (no ' +
54
+ 'REVEALUI_AUDIT_SIGNING_KEY). Legal only in dev/test; a production signing ' +
55
+ 'deployment refuses to boot without the key.');
56
+ }
57
+ cachedSigner = signer;
58
+ resolved = true;
59
+ }
60
+ return cachedSigner;
61
+ }
62
+ /**
63
+ * Construct a `DrizzleAuditStore` wired to the process-wide signer. Every audit
64
+ * writer builds its store through this helper so a row written through the one
65
+ * door on a signing deployment always carries a signature.
66
+ */
67
+ export function createAuditStore(db) {
68
+ return new DrizzleAuditStore(db, getAuditRowSigner());
69
+ }
70
+ /** Test-only reset of the cached signer (re-reads env on next `getAuditRowSigner`). */
71
+ export function __resetAuditSignerForTest() {
72
+ cachedSigner = undefined;
73
+ resolved = false;
74
+ }
75
+ /**
76
+ * Severity mapping AT THE BOUNDARY (ADR §5). Total over the security
77
+ * vocabulary; every value lands in the DB CHECK vocabulary. Declared as a
78
+ * `Record<AuditSeverity, ...>` so the compiler rejects any future severity
79
+ * that is not mapped.
80
+ */
81
+ const SEVERITY_TO_DB = {
82
+ low: 'info',
83
+ medium: 'warn',
84
+ high: 'critical',
85
+ critical: 'critical',
86
+ };
87
+ export function mapSeverityToDb(severity) {
88
+ return SEVERITY_TO_DB[severity];
89
+ }
90
+ /** Reverse map for reconstructing legacy rows written without a full payload. */
91
+ const DB_TO_SEVERITY = {
92
+ info: 'low',
93
+ warn: 'medium',
94
+ critical: 'critical',
95
+ };
96
+ /**
97
+ * `AuditStorage` implementation backed by `DrizzleAuditStore`. Re-homes the
98
+ * row-storing responsibility onto `DrizzleAuditStore.append()` and keeps the
99
+ * security-model boundary mapping (severity + columns) here, so `@revealui/db`
100
+ * stays free of a dependency on the security package.
101
+ */
102
+ export class DrizzleBackedAuditStorage {
103
+ store;
104
+ constructor(db) {
105
+ // createAuditStore injects the process-wide Ed25519 signer (GAP-355 Stage 3),
106
+ // so rows land signed on a signing deployment and NULL-signature in unsigned
107
+ // (dev/test) mode. The severity/column boundary mapping stays here.
108
+ this.store = createAuditStore(db);
109
+ }
110
+ async write(event) {
111
+ try {
112
+ // The injected signer (createAuditStore) signs the row at the door on a
113
+ // signing deployment; a signer failure makes append THROW (fail-closed),
114
+ // caught below and routed through the write-result rails.
115
+ await this.store.append({
116
+ id: event.id,
117
+ timestamp: new Date(event.timestamp),
118
+ eventType: event.type,
119
+ severity: mapSeverityToDb(event.severity),
120
+ agentId: event.actor.id,
121
+ payload: event,
122
+ policyViolations: [],
123
+ });
124
+ recordAuditWriteResult({ ok: true, eventId: event.id, eventType: event.type });
125
+ }
126
+ catch (err) {
127
+ recordAuditWriteResult({
128
+ ok: false,
129
+ reason: classifyAuditWriteFailure(err),
130
+ eventId: event.id,
131
+ eventType: event.type,
132
+ });
133
+ throw err;
134
+ }
135
+ }
136
+ async query(query) {
137
+ const dbSeverities = query.severity && query.severity.length > 0
138
+ ? [...new Set(query.severity.map(mapSeverityToDb))]
139
+ : undefined;
140
+ const entries = await this.store.query({
141
+ agentId: query.actorId,
142
+ eventTypes: query.types ? [...query.types] : undefined,
143
+ severity: dbSeverities,
144
+ startTime: query.startDate,
145
+ endTime: query.endDate,
146
+ limit: query.limit ?? 100,
147
+ offset: query.offset ?? 0,
148
+ });
149
+ return entries
150
+ .map((entry) => reconstructEvent(entry))
151
+ .filter((event) => {
152
+ if (query.resourceType && event.resource?.type !== query.resourceType)
153
+ return false;
154
+ if (query.resourceId && event.resource?.id !== query.resourceId)
155
+ return false;
156
+ if (query.result && query.result.length > 0 && !query.result.includes(event.result)) {
157
+ return false;
158
+ }
159
+ return true;
160
+ });
161
+ }
162
+ async count(query) {
163
+ // Count ignores pagination (matches InMemoryAuditStorage semantics). Query
164
+ // with an unbounded limit and count the post-filtered results.
165
+ const events = await this.query({ ...query, limit: Number.MAX_SAFE_INTEGER, offset: 0 });
166
+ return events.length;
167
+ }
168
+ }
169
+ /**
170
+ * Reconstruct the full `AuditEvent` from a stored row. The complete event is
171
+ * persisted in the `payload` column, so it round-trips exactly. Rows written
172
+ * before this adapter (or by another writer without a full-event payload) fall
173
+ * back to a column-derived event.
174
+ */
175
+ function reconstructEvent(entry) {
176
+ const stored = entry.payload;
177
+ if (stored && typeof stored === 'object' && 'type' in stored) {
178
+ return stored;
179
+ }
180
+ return {
181
+ id: entry.id,
182
+ timestamp: entry.timestamp.toISOString(),
183
+ type: entry.eventType,
184
+ severity: DB_TO_SEVERITY[entry.severity] ?? 'low',
185
+ actor: { id: entry.agentId, type: 'system' },
186
+ action: entry.eventType,
187
+ result: 'success',
188
+ metadata: stored ?? undefined,
189
+ };
190
+ }
191
+ /**
192
+ * Synchronous env-parity assertion for the audit write path — GAP-355 Stage 1
193
+ * closure, owner-ruled 2026-07-17.
194
+ *
195
+ * A serving process installs audit storage but cannot always run the async
196
+ * round-trip self-test (serverless has no clean "refuse to serve" for a
197
+ * boot-time DB round trip). This asserts the audit path's ENV preconditions
198
+ * SYNCHRONOUSLY at the install point, so a deploy whose audit-critical env has
199
+ * diverged (a required var missing or empty) FAILS THE DEPLOY rather than
200
+ * serving with an audit sink that silently drops every row.
201
+ *
202
+ * Scope, honestly: this catches env-var ABSENCE/emptiness synchronously — which
203
+ * `installAuditStorage()` itself does not, because `getClient()` is a lazy pool
204
+ * factory, so a missing connection URL would otherwise surface only on the
205
+ * first write, at request time. It does NOT, and on serverless cannot, catch
206
+ * migration-state divergence (the `audit_log` table missing or misshapen on the
207
+ * target DB); that needs the write-read round trip of `auditStorageSelfTest`.
208
+ */
209
+ export function assertAuditStorageEnv(env = process.env) {
210
+ // GAP-417 item 5: the predicate is OWNED by @revealui/db and matches
211
+ // getClient()'s resolution exactly (config url, then POSTGRES_URL /
212
+ // DATABASE_URL). The previous local triple also accepted DATABASE_HOST,
213
+ // which getClient() never consults — so the assert passed, the install then
214
+ // threw, and production silently kept the in-memory sink (proven in the
215
+ // #2161 re-review). Never re-inline this check.
216
+ if (!hasDatabaseConnectionEnv(env)) {
217
+ throw new Error('AUDIT STORAGE ENV PARITY FAILED: no usable database connection is configured ' +
218
+ '(set POSTGRES_URL or DATABASE_URL, or provide @revealui/config database.url), ' +
219
+ 'so the audit write path cannot persist rows. Refusing to serve — an agent ' +
220
+ 'action that cannot be recorded must not execute (fail-closed integrity, ' +
221
+ 'docs/decisions/2026-07-12-audit-receipt-architecture.md §2a).');
222
+ }
223
+ // Signing key (GAP-355 Stage 3): a production signing deployment must have the
224
+ // Ed25519 key so rows land signed. Absence is a diverged-env deploy — fail it
225
+ // synchronously here (the serverless serving process's refuse-to-serve), the
226
+ // same rail the DB-connection check above uses. Full Ed25519 PKCS#8 parsing is
227
+ // done by validate-startup; this is the audit-owned presence parity check, so
228
+ // the contract cannot silently drift on one serving process. Dev/test
229
+ // (NODE_ENV !== 'production') runs unsigned by design and is exempt.
230
+ //
231
+ // GAP-417 items 1-2 (owner-countersigned 2026-07-25): SKIP_ENV_VALIDATION no
232
+ // longer exempts this check. The audit path has NO escape hatch — a
233
+ // production process that cannot sign must not boot, because unsigned rows
234
+ // are indistinguishable from tampering AND permanently stall anchor
235
+ // contiguity once the sweep filters them. SKIP still covers non-audit env
236
+ // validation elsewhere; it never buys an unsigned production audit log.
237
+ if (env.NODE_ENV === 'production') {
238
+ if (!env.REVEALUI_AUDIT_SIGNING_KEY) {
239
+ throw new Error('AUDIT STORAGE ENV PARITY FAILED: REVEALUI_AUDIT_SIGNING_KEY is not set on a ' +
240
+ 'production deployment, so the audit write path cannot sign rows. Refusing to ' +
241
+ 'serve — an unsigned row in the post-Stage-3 era is indistinguishable from ' +
242
+ 'tampering (docs/decisions/2026-07-12-audit-receipt-architecture.md §2a). ' +
243
+ 'SKIP_ENV_VALIDATION does not exempt the audit path (GAP-417).');
244
+ }
245
+ }
246
+ }
247
+ /**
248
+ * Swap the process-wide `audit` system onto persistent Postgres storage.
249
+ * Synchronous and side-effect-free at call time: `getClient()` is a lazy pool
250
+ * factory (no connection opened here), so this is safe to run on a serverless
251
+ * cold-start path. WITHOUT this call, audit events fall into the default
252
+ * `InMemoryAuditStorage` and evaporate on restart — which is exactly what
253
+ * happened to every admin-process emit before GAP-338.
254
+ *
255
+ * Call `assertAuditStorageEnv()` before this at each install site so a
256
+ * diverged-env deploy fails loudly instead of installing a store that can
257
+ * never write.
258
+ */
259
+ export function installAuditStorage() {
260
+ audit.setStorage(new DrizzleBackedAuditStorage(getClient()));
261
+ }
262
+ /**
263
+ * Boot-time round-trip self-test. Writes a synthetic audit event through the
264
+ * REAL installed path (AuditSystem → boundary → DrizzleAuditStore → audit_log)
265
+ * and reads it back. Throws if the round trip fails, so a runtime that cannot
266
+ * record agent actions REFUSES TO SERVE rather than silently dropping them
267
+ * (fail-closed integrity — ADR §2a).
268
+ *
269
+ * Uses `severity: 'low'` deliberately: that value would be rejected by the DB
270
+ * CHECK constraint if the boundary mapping regressed, so this exercises the
271
+ * exact defect Stage 1 fixes. A healthy audit path lets startup proceed.
272
+ *
273
+ * Runs on the long-running worker (once per deploy) and the dev boot path,
274
+ * where an async boot chain already exists and `process.exit(1)` gives
275
+ * "refuse to serve" clean semantics. It is NOT run on serverless cold-start
276
+ * paths, which stay free of per-invocation DB round-trips (storage is still
277
+ * installed there synchronously by `installAuditStorage()`).
278
+ */
279
+ export async function auditStorageSelfTest(auditSystem = audit) {
280
+ const marker = `__audit-self-test__:${randomUUID()}`;
281
+ const written = await auditSystem.log({
282
+ type: 'security.audit_self_test',
283
+ severity: 'low',
284
+ actor: { id: marker, type: 'system' },
285
+ action: 'audit-storage-self-test',
286
+ result: 'success',
287
+ metadata: { synthetic: true },
288
+ });
289
+ const found = await auditSystem.query({ actorId: marker, limit: 5 });
290
+ if (!found.some((event) => event.id === written.id)) {
291
+ throw new Error('AUDIT STORAGE SELF-TEST FAILED: wrote a synthetic audit event but could not read it ' +
292
+ 'back. Refusing to serve — a runtime that cannot record agent actions must not accept ' +
293
+ 'traffic (fail-closed integrity, docs/decisions/2026-07-12-audit-receipt-architecture.md §2a).');
294
+ }
295
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,YAAY,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE9D,OAAO,EACL,kBAAkB,EAClB,iBAAiB,EACjB,iBAAiB,EACjB,gBAAgB,EAChB,eAAe,EACf,mBAAmB,EACnB,kBAAkB,EAClB,mBAAmB,GACpB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,eAAe,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AAC5D,OAAO,EACL,mBAAmB,EACnB,mBAAmB,EACnB,qBAAqB,EACrB,eAAe,EACf,mBAAmB,EACnB,qBAAqB,GACtB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EACL,SAAS,EACT,mBAAmB,EACnB,aAAa,EACb,yBAAyB,EACzB,YAAY,EACZ,UAAU,GACX,MAAM,aAAa,CAAC;AAErB,YAAY,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AACvD,OAAO,EACL,kBAAkB,EAClB,eAAe,EACf,oBAAoB,EACpB,eAAe,GAChB,MAAM,iBAAiB,CAAC;AACzB,YAAY,EAAE,SAAS,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAC3E,OAAO,EACL,YAAY,EACZ,UAAU,EACV,gBAAgB,EAChB,YAAY,EACZ,qBAAqB,EACrB,cAAc,EACd,gBAAgB,EAChB,aAAa,EACb,cAAc,GACf,MAAM,UAAU,CAAC;AAElB,YAAY,EACV,cAAc,EACd,qBAAqB,EACrB,gBAAgB,EAChB,UAAU,EACV,UAAU,EACV,cAAc,GACf,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,eAAe,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AACjF,OAAO,EACL,YAAY,EACZ,YAAY,EACZ,iBAAiB,EACjB,kBAAkB,EAClB,kBAAkB,EAClB,gBAAgB,EAChB,KAAK,YAAY,EACjB,KAAK,kBAAkB,EACvB,kBAAkB,EAClB,eAAe,EACf,gBAAgB,GACjB,MAAM,YAAY,CAAC;AAEpB,YAAY,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAClD,OAAO,EACL,gBAAgB,EAChB,oBAAoB,EACpB,aAAa,EACb,+BAA+B,EAC/B,6BAA6B,EAC7B,YAAY,EACZ,aAAa,EACb,kBAAkB,EAClB,YAAY,EACZ,oBAAoB,EACpB,kBAAkB,GACnB,MAAM,cAAc,CAAC;AACtB,YAAY,EACV,oBAAoB,EACpB,mBAAmB,EACnB,kBAAkB,GACnB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EACL,cAAc,EACd,0BAA0B,EAC1B,4BAA4B,EAC5B,sBAAsB,EACtB,0BAA0B,GAC3B,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EACL,gCAAgC,EAChC,wBAAwB,GACzB,MAAM,0BAA0B,CAAC;AAClC,OAAO,EACL,cAAc,EACd,kBAAkB,EAClB,kBAAkB,EAClB,cAAc,EACd,oBAAoB,GACrB,MAAM,iBAAiB,CAAC;AACzB,YAAY,EAAE,cAAc,EAAE,oBAAoB,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AACtF,OAAO,EACL,uBAAuB,EACvB,aAAa,EACb,qBAAqB,EACrB,uBAAuB,EACvB,aAAa,EACb,UAAU,EACV,iBAAiB,EACjB,yBAAyB,EACzB,aAAa,EACb,sBAAsB,GACvB,MAAM,cAAc,CAAC;AAEtB,OAAO,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAC5E,YAAY,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,EACL,aAAa,EACb,eAAe,EACf,UAAU,EACV,eAAe,EACf,YAAY,GACb,MAAM,oBAAoB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"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"}
@@ -6,6 +6,12 @@
6
6
  */
7
7
  // Audit bridge
8
8
  export { auditAccountLocked, auditLoginFailure, auditLoginSuccess, auditMfaDisabled, auditMfaEnabled, auditPasswordChange, auditPasswordReset, auditSessionRevoked, } from './audit-bridge.js';
9
+ // NOTE (GAP-338): the audit storage boundary lives at the DEDICATED subpath
10
+ // `@revealui/auth/audit-storage`, deliberately NOT re-exported from this
11
+ // barrel. `@revealui/auth/server` is the fleet's most-mocked module (route
12
+ // tests bare-mock it for getSession), and a bare vi.mock of this barrel must
13
+ // never swallow the audit write path (it broke the GAP-352 + mcp-endpoint
14
+ // integration suites when the boundary briefly lived here).
9
15
  export { isSignupAllowed, signIn, signUp } from './auth.js';
10
16
  export { clearFailedAttempts, configureBruteForce, getFailedAttemptCount, isAccountLocked, recordFailedAttempt, resetBruteForceConfig, } from './brute-force.js';
11
17
  export { AuthError, AuthenticationError, DatabaseError, OAuthAccountConflictError, SessionError, TokenError, } from './errors.js';
@@ -18,7 +18,7 @@ export declare function getTestDatabaseUrl(): string;
18
18
  /**
19
19
  * Creates a test database client
20
20
  */
21
- export declare function createTestDatabaseClient(): import("@revealui/db/client").Database;
21
+ export declare function createTestDatabaseClient(): import("@revealui/db").DatabaseClient;
22
22
  /**
23
23
  * Cleans up test data from database
24
24
  */
@@ -1 +1 @@
1
- {"version":3,"file":"database.d.ts","sourceRoot":"","sources":["../../src/utils/database.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAKH,OAAO,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC;AAEjD;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC,gBAAgB,EAAE,MAAM,CAAC;CAC1B;AAED;;GAEG;AACH,wBAAgB,kBAAkB,IAAI,MAAM,CAU3C;AAED;;GAEG;AACH,wBAAgB,wBAAwB,2CAMvC;AAED;;GAEG;AACH,wBAAsB,eAAe,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAmBtE;AAED;;GAEG;AACH,wBAAsB,cAAc,CAAC,SAAS,CAAC,EAAE,OAAO,CAAC,IAAI,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CA+B7E;AAED;;GAEG;AACH,wBAAsB,iBAAiB,CACrC,MAAM,EAAE,MAAM,EACd,SAAS,CAAC,EAAE,OAAO,CAAC,OAAO,CAAC,GAC3B,OAAO,CAAC,OAAO,CAAC,CAuBlB;AAED;;GAEG;AACH,wBAAsB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,GAAG,IAAI,CAAC,CASxE;AAED;;GAEG;AACH,wBAAsB,qBAAqB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC,CAStF;AAED;;GAEG;AACH,wBAAsB,0BAA0B,CAC9C,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,MAAM,EAChB,IAAI,CAAC,EAAE,MAAM,GACZ,OAAO,CAAC,IAAI,CAAC,CASf;AAED;;GAEG;AACH,wBAAsB,yBAAyB,CAC7C,aAAa,CAAC,EAAE,OAAO,CAAC,IAAI,CAAC,EAC7B,gBAAgB,CAAC,EAAE,OAAO,CAAC,OAAO,CAAC,GAClC,OAAO,CAAC;IAAE,IAAI,EAAE,IAAI,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,CAAC,CAI3C"}
1
+ {"version":3,"file":"database.d.ts","sourceRoot":"","sources":["../../src/utils/database.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAKH,OAAO,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC;AAEjD;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC,gBAAgB,EAAE,MAAM,CAAC;CAC1B;AAED;;GAEG;AACH,wBAAgB,kBAAkB,IAAI,MAAM,CAU3C;AAED;;GAEG;AACH,wBAAgB,wBAAwB,0CAMvC;AAED;;GAEG;AACH,wBAAsB,eAAe,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAmBtE;AAED;;GAEG;AACH,wBAAsB,cAAc,CAAC,SAAS,CAAC,EAAE,OAAO,CAAC,IAAI,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CA+B7E;AAED;;GAEG;AACH,wBAAsB,iBAAiB,CACrC,MAAM,EAAE,MAAM,EACd,SAAS,CAAC,EAAE,OAAO,CAAC,OAAO,CAAC,GAC3B,OAAO,CAAC,OAAO,CAAC,CAuBlB;AAED;;GAEG;AACH,wBAAsB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,GAAG,IAAI,CAAC,CASxE;AAED;;GAEG;AACH,wBAAsB,qBAAqB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC,CAStF;AAED;;GAEG;AACH,wBAAsB,0BAA0B,CAC9C,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,MAAM,EAChB,IAAI,CAAC,EAAE,MAAM,GACZ,OAAO,CAAC,IAAI,CAAC,CASf;AAED;;GAEG;AACH,wBAAsB,yBAAyB,CAC7C,aAAa,CAAC,EAAE,OAAO,CAAC,IAAI,CAAC,EAC7B,gBAAgB,CAAC,EAAE,OAAO,CAAC,OAAO,CAAC,GAClC,OAAO,CAAC;IAAE,IAAI,EAAE,IAAI,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,CAAC,CAI3C"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@revealui/auth",
3
- "version": "0.4.10",
3
+ "version": "0.5.0",
4
4
  "description": "Database-backed session auth for Hono and Next.js — bcrypt, OAuth, brute-force protection, rate limiting, password reset. Ships with RevealUI.",
5
5
  "keywords": [
6
6
  "auth",
@@ -16,9 +16,9 @@
16
16
  "zod": "^4.4.3",
17
17
  "@revealui/config": "0.6.0",
18
18
  "@revealui/contracts": "0.8.1",
19
- "@revealui/core": "0.12.1",
20
- "@revealui/db": "0.9.0",
21
- "@revealui/security": "0.5.1"
19
+ "@revealui/core": "0.12.2",
20
+ "@revealui/db": "0.10.0",
21
+ "@revealui/security": "0.6.0"
22
22
  },
23
23
  "devDependencies": {
24
24
  "@simplewebauthn/browser": "^13.3.0",
@@ -46,6 +46,11 @@
46
46
  "import": "./dist/server/index.js",
47
47
  "default": "./dist/server/index.js"
48
48
  },
49
+ "./audit-storage": {
50
+ "types": "./dist/server/audit-storage.d.ts",
51
+ "import": "./dist/server/audit-storage.js",
52
+ "default": "./dist/server/audit-storage.js"
53
+ },
49
54
  "./react": {
50
55
  "types": "./dist/react/index.d.ts",
51
56
  "import": "./dist/react/index.js",