@voltro/plugin-audit 0.84.0 → 0.86.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.
@@ -1,70 +1,14 @@
1
1
  # Third-Party Notices — @voltro/plugin-audit
2
2
 
3
- This package depends on the open-source software listed below. Each is the
3
+ This package uses the open-source software listed below. Each is the
4
4
  property of its respective copyright holders and is used under the terms of
5
5
  its license. This file is provided for attribution; it grants no rights in
6
6
  @voltro/plugin-audit itself, which is proprietary (see LICENSE).
7
7
 
8
- Generated from the resolved runtime dependency closure (7 packages).
8
+ Generated from the resolved runtime dependency closure (4 packages).
9
9
 
10
10
  ---
11
11
 
12
- ## @effect/experimental@0.61.1
13
-
14
- License: MIT
15
-
16
- ```
17
- MIT License
18
-
19
- Copyright (c) 2023-present The Contributors
20
-
21
- Permission is hereby granted, free of charge, to any person obtaining a copy
22
- of this software and associated documentation files (the "Software"), to deal
23
- in the Software without restriction, including without limitation the rights
24
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
25
- copies of the Software, and to permit persons to whom the Software is
26
- furnished to do so, subject to the following conditions:
27
-
28
- The above copyright notice and this permission notice shall be included in all
29
- copies or substantial portions of the Software.
30
-
31
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
32
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
33
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
34
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
35
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
36
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
37
- SOFTWARE.
38
- ```
39
-
40
- ## @effect/sql@0.52.1
41
-
42
- License: MIT
43
-
44
- ```
45
- MIT License
46
-
47
- Copyright (c) 2023-present The Contributors
48
-
49
- Permission is hereby granted, free of charge, to any person obtaining a copy
50
- of this software and associated documentation files (the "Software"), to deal
51
- in the Software without restriction, including without limitation the rights
52
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
53
- copies of the Software, and to permit persons to whom the Software is
54
- furnished to do so, subject to the following conditions:
55
-
56
- The above copyright notice and this permission notice shall be included in all
57
- copies or substantial portions of the Software.
58
-
59
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
60
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
61
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
62
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
63
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
64
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
65
- SOFTWARE.
66
- ```
67
-
68
12
  ## jose@6.2.12
69
13
 
70
14
  License: MIT
@@ -93,34 +37,6 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
93
37
  SOFTWARE.
94
38
  ```
95
39
 
96
- ## layerr@3.0.0
97
-
98
- License: MIT
99
-
100
- ```
101
- MIT License
102
-
103
- Copyright (c) 2020 Perry Mitchell
104
-
105
- Permission is hereby granted, free of charge, to any person obtaining a copy
106
- of this software and associated documentation files (the "Software"), to deal
107
- in the Software without restriction, including without limitation the rights
108
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
109
- copies of the Software, and to permit persons to whom the Software is
110
- furnished to do so, subject to the following conditions:
111
-
112
- The above copyright notice and this permission notice shall be included in all
113
- copies or substantial portions of the Software.
114
-
115
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
116
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
117
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
118
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
119
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
120
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
121
- SOFTWARE.
122
- ```
123
-
124
40
  ## typeid-js@1.2.0
125
41
 
126
42
  License: Apache-2.0
@@ -329,14 +245,14 @@ Apache License
329
245
  limitations under the License.
330
246
  ```
331
247
 
332
- ## ulidx@2.4.1
248
+ ## ulid@3.0.2
333
249
 
334
250
  License: MIT
335
251
 
336
252
  ```
337
- MIT License
253
+ The MIT License (MIT)
338
254
 
339
- Copyright (c) 2021 Perry Mitchell
255
+ Copyright (c) 2017 Alizain Feerasta
340
256
 
341
257
  Permission is hereby granted, free of charge, to any person obtaining a copy
342
258
  of this software and associated documentation files (the "Software"), to deal
package/dist/chain.js ADDED
@@ -0,0 +1,205 @@
1
+ import { AUDIT_LOG_TABLE as e } from "./tableName.js";
2
+ import { auditInvocationIdOf as t, hasConsistentAuditInvocationId as n } from "./invocationIdentity.js";
3
+ import { and as r, column as i, eq as a, generateId as o, gt as s, isNotNull as c, isNull as l, lte as u, or as d } from "@voltro/database";
4
+ import { createHash as f, createHmac as p, randomBytes as m } from "node:crypto";
5
+ //#region src/chain.ts
6
+ var h = [
7
+ "tag",
8
+ "at",
9
+ "subjectId",
10
+ "tenantId",
11
+ "traceId",
12
+ "status",
13
+ "durationMs",
14
+ "subject",
15
+ "actor",
16
+ "scope",
17
+ "metadata",
18
+ "input",
19
+ "outcome",
20
+ "errorTag"
21
+ ], g = [
22
+ ...h,
23
+ "hashVersion",
24
+ "id",
25
+ "impersonation",
26
+ "scopeKey"
27
+ ], _ = (e) => {
28
+ if (e == null) return "null";
29
+ if (e instanceof Date) return JSON.stringify(e.toISOString());
30
+ let t = typeof e;
31
+ return t === "number" || t === "boolean" || t === "string" ? JSON.stringify(e) : t === "bigint" ? JSON.stringify(String(e)) : Array.isArray(e) ? `[${e.map(_).join(",")}]` : t === "object" ? `{${Object.entries(e).filter(([, e]) => e !== void 0).sort(([e], [t]) => e < t ? -1 : +(e > t)).map(([e, t]) => `${JSON.stringify(e)}:${_(t)}`).join(",")}}` : JSON.stringify(String(e));
32
+ }, v = (e) => e instanceof Date ? e.toISOString() : e, y = (e, t) => _(Object.fromEntries((t === 2 ? g : h).map((t) => [t, v(e[t] ?? null)]))), b = (e) => {
33
+ let t = process.env.VOLTRO_AUDIT_CHAIN_SECRET;
34
+ return t ? p("sha256", t).update(e, "utf8").digest("hex") : f("sha256").update(e, "utf8").digest("hex");
35
+ }, x = (e, t) => {
36
+ let n = e.hashVersion ?? 1;
37
+ if (n !== 1 && n !== 2) throw TypeError("Unsupported audit hash version");
38
+ return b(JSON.stringify([
39
+ t.chainId,
40
+ t.seq,
41
+ t.prevHash,
42
+ y(e, n)
43
+ ]));
44
+ }, S = (e = m(12).toString("base64url")) => {
45
+ let n = 0, r = null;
46
+ return {
47
+ chainId: e,
48
+ link: (i) => {
49
+ n += 1;
50
+ let a = {
51
+ ...i,
52
+ invocationId: t(i.outcome),
53
+ id: i.id ?? o({
54
+ kind: "typeid",
55
+ prefix: "audit"
56
+ }, "_voltro_audit_log"),
57
+ hashVersion: 2
58
+ }, s = x(a, {
59
+ chainId: e,
60
+ seq: n,
61
+ prevHash: r
62
+ }), c = {
63
+ ...a,
64
+ chainId: e,
65
+ seq: n,
66
+ prevHash: r,
67
+ hash: s
68
+ };
69
+ return r = s, c;
70
+ }
71
+ };
72
+ };
73
+ async function* C(t, n) {
74
+ let o = n.chainId === void 0 ? void 0 : a("chainId", n.chainId), f = (t, n, r) => ({
75
+ table: e,
76
+ predicate: t,
77
+ order: r,
78
+ take: n,
79
+ skip: void 0,
80
+ projection: void 0
81
+ });
82
+ if (n.limit !== void 0) {
83
+ yield* await t.query(f(o, n.limit, [{
84
+ column: "chainId",
85
+ direction: "asc"
86
+ }, {
87
+ column: "seq",
88
+ direction: "asc"
89
+ }]));
90
+ return;
91
+ }
92
+ let p = [...await t.query({
93
+ ...f(o, void 0, [{
94
+ column: "chainId",
95
+ direction: "asc"
96
+ }]),
97
+ groupBy: ["chainId"],
98
+ aggregations: [
99
+ i("chainId"),
100
+ {
101
+ op: "max",
102
+ column: "seq",
103
+ alias: "lastSeq"
104
+ },
105
+ {
106
+ op: "max",
107
+ column: "id",
108
+ alias: "lastId"
109
+ }
110
+ ]
111
+ })].sort((e, t) => String(e.chainId) < String(t.chainId) ? -1 : +(String(e.chainId) > String(t.chainId)));
112
+ for (let e of p) {
113
+ let n = e.chainId, i = n == null ? l("chainId") : a("chainId", n), o = n == null ? [!1] : [!1, !0];
114
+ for (let p of o) {
115
+ if (p && e.lastSeq == null) continue;
116
+ let o = [i, u("id", e.lastId)];
117
+ n != null && o.push(p ? r(c("seq"), u("seq", e.lastSeq)) : l("seq"));
118
+ let m;
119
+ for (;;) {
120
+ let e = m === void 0 ? void 0 : p ? d(s("seq", m.seq), r(a("seq", m.seq), s("id", m.id))) : s("id", m.id), n = await t.query(f(r(...o, ...e ? [e] : []), 1e3, [...p ? [{
121
+ column: "seq",
122
+ direction: "asc"
123
+ }] : [], {
124
+ column: "id",
125
+ direction: "asc"
126
+ }]));
127
+ if (yield* n, n.length < 1e3) break;
128
+ let i = n[n.length - 1];
129
+ if (i.id === void 0 || i.id === m?.id) throw Error("audit verification cursor did not advance");
130
+ m = i;
131
+ }
132
+ }
133
+ }
134
+ }
135
+ var w = async (e, t = {}) => {
136
+ let r = [], i = [], a = 0, o = 0, s = 0, c = null, l = () => {
137
+ c &&= (i.push({
138
+ chainId: c.chainId,
139
+ from: c.from,
140
+ to: c.last,
141
+ tip: c.tip,
142
+ prunedPrefix: c.from > 1
143
+ }), null);
144
+ };
145
+ for await (let i of C(e, t)) {
146
+ let e = i.chainId, t = i.hash, u = i.seq;
147
+ if (typeof e != "string" || typeof t != "string" || u == null) {
148
+ a += 1;
149
+ continue;
150
+ }
151
+ let d = Number(u), f = typeof i.prevHash == "string" ? i.prevHash : null, p = typeof i.id == "string" ? i.id : void 0;
152
+ s += 1, !c || c.chainId !== e ? (l(), c = {
153
+ chainId: e,
154
+ from: d,
155
+ last: d,
156
+ tip: t
157
+ }) : (d === c.last + 1 ? f !== c.tip && r.push({
158
+ kind: "broken-link",
159
+ chainId: e,
160
+ seq: d,
161
+ ...p ? { rowId: p } : {},
162
+ detail: `prevHash does not match the hash of seq ${c.last}`
163
+ }) : r.push({
164
+ kind: "gap",
165
+ chainId: e,
166
+ seq: c.last + 1,
167
+ detail: `chain jumps from seq ${c.last} to ${d} — ${d - c.last - 1} row(s) missing`
168
+ }), c.last = d, c.tip = t);
169
+ let m = i.hashVersion ?? 1;
170
+ m !== 1 && m !== 2 ? r.push({
171
+ kind: "tampered",
172
+ chainId: e,
173
+ seq: d,
174
+ ...p ? { rowId: p } : {},
175
+ detail: "unsupported audit hash version; the row cannot be verified"
176
+ }) : (m === 1 && (o += 1), n(i) ? x(i, {
177
+ chainId: e,
178
+ seq: d,
179
+ prevHash: f
180
+ }) !== t && r.push({
181
+ kind: "tampered",
182
+ chainId: e,
183
+ seq: d,
184
+ ...p ? { rowId: p } : {},
185
+ detail: "the row content does not hash to its stored hash — a column was altered"
186
+ }) : r.push({
187
+ kind: "tampered",
188
+ chainId: e,
189
+ seq: d,
190
+ ...p ? { rowId: p } : {},
191
+ detail: "the invocation lookup does not match its protected outcome"
192
+ }));
193
+ }
194
+ return l(), {
195
+ ok: r.length === 0,
196
+ rowsChecked: s,
197
+ unchainedRows: a,
198
+ legacyHashRows: o,
199
+ chains: i,
200
+ issues: r,
201
+ keyed: !!process.env.VOLTRO_AUDIT_CHAIN_SECRET
202
+ };
203
+ };
204
+ //#endregion
205
+ export { x as auditRowHash, S as makeAuditChain, w as verifyAuditChain };
package/dist/index.d.ts CHANGED
@@ -2,9 +2,11 @@ import { AuditActor } from '@voltro/database';
2
2
  import { ColumnDefinition } from '@voltro/database';
3
3
  import { Effect } from 'effect';
4
4
  import { MixinDefinition } from '@voltro/database';
5
+ import { Query } from '@voltro/database';
6
+ import { Row } from '@voltro/database';
7
+ import { SchemaTable } from '@voltro/database';
5
8
  import { Subject } from '@voltro/protocol';
6
9
  import { TableIndex } from '@voltro/database';
7
- import { TableLike } from '@voltro/database';
8
10
  import { VoltroPlugin } from '@voltro/protocol';
9
11
 
10
12
  export declare const audit: () => MixinDefinition<{
@@ -72,6 +74,8 @@ export declare type AuditChainIssueKind = 'tampered' | 'broken-link' | 'gap';
72
74
 
73
75
  /** The columns the chain adds to `_voltro_audit_log`. */
74
76
  export declare interface AuditChainLink {
77
+ /** NULL/1 is historical coverage; new writers use format 2. */
78
+ readonly hashVersion?: number | null;
75
79
  /** Which writer's chain this row belongs to. Null for an unchained row. */
76
80
  readonly chainId: string;
77
81
  /** 1-based position within `chainId`. */
@@ -102,6 +106,9 @@ export declare interface AuditChainVerification {
102
106
  * Not an `issue` (a pre-chain deployment is not tampering) but never
103
107
  * silently dropped either. */
104
108
  readonly unchainedRows: number;
109
+ /** Rows using the historical digest, which did not cover impersonation or
110
+ * the derived scope key. Optional for compatibility with older result DTOs. */
111
+ readonly legacyHashRows?: number;
105
112
  readonly chains: ReadonlyArray<AuditChainSummary>;
106
113
  readonly issues: ReadonlyArray<AuditChainIssue>;
107
114
  /** Whether the digest was keyed. An `ok: true` from an UNKEYED run is a
@@ -125,7 +132,7 @@ export declare interface AuditEvent {
125
132
  * (anonymous, system). Absent is honest; a fabricated placeholder would be
126
133
  * the thing this field exists to prevent.
127
134
  */
128
- readonly actor?: AuditActor | undefined;
135
+ readonly actor?: AuditActor | null | undefined;
129
136
  /** The app's own scoping dimension — opaque, stored verbatim, filterable.
130
137
  * `.with(tenant())` is one level too coarse for a per-team trail. */
131
138
  readonly scope?: unknown;
@@ -158,22 +165,38 @@ export declare interface AuditEvent {
158
165
  readonly kind: 'ok';
159
166
  readonly value: unknown;
160
167
  readonly durationMs: number;
168
+ readonly invocationId?: string;
161
169
  } | {
162
170
  readonly kind: 'error';
163
171
  readonly error: unknown;
164
172
  readonly durationMs: number;
173
+ readonly invocationId?: string;
174
+ } | {
175
+ readonly kind: 'unknown';
176
+ readonly invocationId: string;
177
+ readonly durationMs: number;
165
178
  };
166
179
  }
167
180
 
168
181
  /** Map an {@link AuditEvent} to an audit-log row. */
169
182
  export declare const auditEventToRow: (event: AuditEvent) => Record<string, unknown>;
170
183
 
184
+ /** Stable display identity when an unknown admission acquires a terminal row.
185
+ * A pagination cursor still uses the selected raw row's actual ordering key. */
186
+ export declare const auditInvocationIdentity: (row: Readonly<Record<string, unknown>>) => string;
187
+
188
+ /** One logical invocation per result, before filtering/order/pagination. Raw
189
+ * admissions remain immutable. Qualify caller predicates/order with audit. */
190
+ export declare const auditInvocationQuery: () => Query<Row, string, {
191
+ completed: Row;
192
+ }>;
193
+
171
194
  /**
172
195
  * The slice of a built `Table` this package exposes — annotated explicitly so
173
196
  * @voltro/database's private column-builder class doesn't leak across the
174
197
  * package boundary (TS4094). Only name + columns + indexes are needed.
175
198
  */
176
- export declare interface AuditLogTable extends TableLike {
199
+ export declare interface AuditLogTable extends SchemaTable {
177
200
  readonly fields: Record<string, ColumnDefinition<unknown>>;
178
201
  readonly appliedIndexes: ReadonlyArray<TableIndex>;
179
202
  }
@@ -200,6 +223,32 @@ export declare const auditLogTables: ReadonlyArray<AuditLogTable>;
200
223
  export declare const auditPlugin: (options?: AuditPluginOptions) => VoltroPlugin;
201
224
 
202
225
  export declare interface AuditPluginOptions {
226
+ /** Successful registered mutations may capture a required redacted intent on
227
+ * the current transaction outbox. Requires the literal datastore sink and the
228
+ * supported Runtime/CLI hook. Default best-effort recording is unchanged.
229
+ * Required scope/metadata/redaction/encoding failures roll back the mutation.
230
+ * Callbacks must remain free of external side effects: replay may repeat them.
231
+ * Refusals, actions, workflows and HTTP remain best effort in this mode. */
232
+ readonly mutationCapture?: 'best-effort' | 'transactional-outbox';
233
+ /** Registered mutation/action/workflow failures enqueue a redacted outcome
234
+ * after the business transaction has rolled back. Requires datastore sink
235
+ * and the current host's root outcome capability. Capture failures preserve
236
+ * the original operation error and report only bounded diagnostics. Query,
237
+ * HTTP and successful non-mutation capture are unchanged. */
238
+ readonly failureCapture?: 'best-effort' | 'outbox';
239
+ /** Successful registered actions and workflow-start calls enqueue a frozen
240
+ * redacted outcome through the host's root outbox. Requires datastore sink.
241
+ * This is durable delivery after completion, not a transaction around external
242
+ * side effects: admission failures preserve the successful business result and
243
+ * emit a bounded diagnostic. Queries, HTTP and mutation capture are unchanged. */
244
+ readonly successCapture?: 'best-effort' | 'outbox';
245
+ /** Required admission for selected action/workflow-start calls. Persist an
246
+ * immutable privacy-safe unknown outcome before executing their effect, then
247
+ * correlate the terminal outcome by invocationId. Requires datastore sink,
248
+ * success/failure outbox capture and record: all. Admission failure prevents
249
+ * execution; interruption leaves unknown evidence without claiming rollback.
250
+ * Mutations, queries and HTTP are unchanged. */
251
+ readonly invocationCapture?: 'best-effort' | 'required';
203
252
  /**
204
253
  * Namespace for this plugin's inspect surface + any rpc tags it contributes. Default `audit`.
205
254
  *
@@ -259,7 +308,7 @@ export declare interface AuditPluginOptions {
259
308
  * `Effect<void>`. Persist events wherever you
260
309
  * like — e.g. an `Effect<void>` sink that
261
310
  * writes rows to your own audit table on top
262
- * of `@effect/sql`.
311
+ * of `effect/sql`.
263
312
  *
264
313
  * **A function sink gets NEITHER the table NOR the retention sweep**, and the
265
314
  * cliff is invisible until the next environment. Both are gated on `sink`
@@ -306,7 +355,7 @@ export declare interface AuditPluginOptions {
306
355
  * between an indexed read path and stuffing `teamId` into `metadata` and
307
356
  * scanning — a column documented as a free-form note is not a read path.
308
357
  *
309
- * Throwing here never fails the mutation being recorded: a scope that cannot
358
+ * In default best-effort mode, throwing never fails the mutation: a scope that cannot
310
359
  * be derived is null, the same answer as not configuring one.
311
360
  */
312
361
  readonly resolveScope?: (ctx: {
@@ -323,10 +372,10 @@ export declare interface AuditPluginOptions {
323
372
  *
324
373
  * Resolved AFTER the call, so it sees the result (`outcome.kind === 'ok'`
325
374
  * with `value`, or `'error'` with the error) — the note for a failed removal
326
- * is a different note. May be async. Throwing, rejecting, or returning
327
- * `undefined` records no note; it never fails the call being recorded, and
328
- * never delays it: the note is resolved on the same fire-and-forget path
329
- * the sink write takes.
375
+ * is a different note. May be async. In default best-effort mode, throwing, rejecting, or returning
376
+ * `undefined` records no note. Resolution is awaited on the interceptor's
377
+ * recording path, but failure does not replace the call's original outcome.
378
+ * Transactional capture requires successful callback resolution.
330
379
  *
331
380
  * ```ts
332
381
  * auditPlugin({
@@ -470,6 +519,13 @@ export declare interface AuditPluginOptions {
470
519
  * "who really did this" is not app-controlled metadata, it is the event.
471
520
  */
472
521
  readonly redactSubject?: 'metadata' | 'metadata-shape' | 'none' | ((subject: Subject) => unknown);
522
+ /** Project resolved actor displayName/email before capture or sink delivery.
523
+ * The framework retains id/type even if JavaScript returns extra fields.
524
+ * Return both labels as strings or null. The synchronous callback must be
525
+ * pure: required capture failure aborts its transaction; refusal admission
526
+ * keeps the original error. Decisions are frozen before enqueue, not rerun
527
+ * during delivery. Absent preserves the existing policy; no actor stays null. */
528
+ readonly redactActor?: (actor: Readonly<AuditActor>) => Pick<AuditActor, 'displayName' | 'email'>;
473
529
  /**
474
530
  * What happens to `AuditEvent.outcome`'s payload before it is handed to the
475
531
  * sink.
@@ -555,12 +611,17 @@ export declare type AuditSink = (event: AuditEvent) => void | Promise<void> | Ef
555
611
  export declare const clearAuditBuffer: () => void;
556
612
 
557
613
  /**
558
- * A DataStore-backed audit sink — appends each event to `_voltro_audit_log`.
559
- * Returns a `(event) => Promise<void>` so it plugs straight into the plugin's
560
- * function-sink path (which lifts it into an Effect + swallows failures, so a
561
- * write hiccup never masks the mutation).
614
+ * A DataStore-backed sink. Optional receiptId identifies one logical append
615
+ * across delivery retries and sink/process reconstruction. Stable receipts
616
+ * require a query-capable store and are refused if their content differs.
617
+ *
618
+ * A per-writer semaphore serializes append attempts. Read-capable stores derive
619
+ * the next link from persisted state, so rejected inserts and lost write
620
+ * acknowledgements cannot advance or fork the writer's chain. Insert-only
621
+ * substitutes retain the old surface, with in-memory state advanced on success.
622
+ * This is append recovery, not durable event capture before business commit.
562
623
  */
563
- export declare const dataStoreAuditSink: (store: AuditDataStore) => (event: AuditEvent) => Promise<void>;
624
+ export declare const dataStoreAuditSink: (store: AuditDataStore) => (event: AuditEvent, receiptId?: string) => Promise<void>;
564
625
 
565
626
  /** Mint a fresh chain. `chainId` is 96 bits of CSPRNG — collisions across
566
627
  * replicas would merge two chains into one apparent fork. */