@voltro/plugin-audit 0.33.0 → 0.34.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.
- package/CHANGELOG.md +1801 -0
- package/dist/index.d.ts +121 -0
- package/dist/index.js +196 -71
- package/package.json +4 -4
package/dist/index.d.ts
CHANGED
|
@@ -43,6 +43,68 @@ export declare const auditBySubject: (store: AuditQueryStore, subjectId: string,
|
|
|
43
43
|
*/
|
|
44
44
|
export declare const auditByTrace: (store: AuditQueryStore, traceId: string) => Promise<ReadonlyArray<Record<string, unknown>>>;
|
|
45
45
|
|
|
46
|
+
/** A writer's chain state. One per `dataStoreAuditSink`, i.e. one per process. */
|
|
47
|
+
export declare interface AuditChain {
|
|
48
|
+
readonly chainId: string;
|
|
49
|
+
/**
|
|
50
|
+
* Allocate this row's link and stamp it onto the row. SYNCHRONOUS and
|
|
51
|
+
* `await`-free by construction — that is the whole concurrency argument (see
|
|
52
|
+
* the header). Do not make it async.
|
|
53
|
+
*/
|
|
54
|
+
readonly link: (row: Record<string, unknown>) => Record<string, unknown>;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export declare interface AuditChainIssue {
|
|
58
|
+
readonly kind: AuditChainIssueKind;
|
|
59
|
+
readonly chainId: string;
|
|
60
|
+
readonly seq: number;
|
|
61
|
+
/** The offending row's id, when it has one (a `gap` names the MISSING seq,
|
|
62
|
+
* so it has no row and no id). */
|
|
63
|
+
readonly rowId?: string;
|
|
64
|
+
readonly detail: string;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export declare type AuditChainIssueKind = 'tampered' | 'broken-link' | 'gap';
|
|
68
|
+
|
|
69
|
+
/** The columns the chain adds to `_voltro_audit_log`. */
|
|
70
|
+
export declare interface AuditChainLink {
|
|
71
|
+
/** Which writer's chain this row belongs to. Null for an unchained row. */
|
|
72
|
+
readonly chainId: string;
|
|
73
|
+
/** 1-based position within `chainId`. */
|
|
74
|
+
readonly seq: number;
|
|
75
|
+
/** The previous row's `hash`, or null for the first row of a chain. */
|
|
76
|
+
readonly prevHash: string | null;
|
|
77
|
+
/** `H(chainId, seq, prevHash, content)` — hex. */
|
|
78
|
+
readonly hash: string;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export declare interface AuditChainSummary {
|
|
82
|
+
readonly chainId: string;
|
|
83
|
+
/** Lowest `seq` present. `> 1` means the retention sweep pruned a prefix. */
|
|
84
|
+
readonly from: number;
|
|
85
|
+
/** Highest `seq` present. */
|
|
86
|
+
readonly to: number;
|
|
87
|
+
/** The tip hash — publish this somewhere append-only to defeat tail
|
|
88
|
+
* truncation and to bind an unkeyed chain to a point in time. */
|
|
89
|
+
readonly tip: string;
|
|
90
|
+
/** Whether the chain starts above `seq` 1 (a pruned prefix, not an issue). */
|
|
91
|
+
readonly prunedPrefix: boolean;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export declare interface AuditChainVerification {
|
|
95
|
+
readonly ok: boolean;
|
|
96
|
+
readonly rowsChecked: number;
|
|
97
|
+
/** Rows carrying no `hash` — written before chaining shipped, or stripped.
|
|
98
|
+
* Not an `issue` (a pre-chain deployment is not tampering) but never
|
|
99
|
+
* silently dropped either. */
|
|
100
|
+
readonly unchainedRows: number;
|
|
101
|
+
readonly chains: ReadonlyArray<AuditChainSummary>;
|
|
102
|
+
readonly issues: ReadonlyArray<AuditChainIssue>;
|
|
103
|
+
/** Whether the digest was keyed. An `ok: true` from an UNKEYED run is a
|
|
104
|
+
* weaker statement — surfaced so a report cannot quietly overstate it. */
|
|
105
|
+
readonly keyed: boolean;
|
|
106
|
+
}
|
|
107
|
+
|
|
46
108
|
/** Narrow slice of the framework DataStore the durable sink needs. */
|
|
47
109
|
export declare interface AuditDataStore {
|
|
48
110
|
insert: (table: string, row: Record<string, unknown>) => Promise<unknown>;
|
|
@@ -113,6 +175,35 @@ export declare const auditLogTables: ReadonlyArray<AuditLogTable>;
|
|
|
113
175
|
export declare const auditPlugin: (options?: AuditPluginOptions) => VoltroPlugin;
|
|
114
176
|
|
|
115
177
|
export declare interface AuditPluginOptions {
|
|
178
|
+
/**
|
|
179
|
+
* Namespace for this plugin's inspect surface + any rpc tags it contributes. Default `audit`.
|
|
180
|
+
*
|
|
181
|
+
* Set it when your app already publishes under that name — an exact tag
|
|
182
|
+
* collision is fatal at codegen, and this is the way out.
|
|
183
|
+
*/
|
|
184
|
+
readonly alias?: string;
|
|
185
|
+
/**
|
|
186
|
+
* Contribute this plugin's tables via `extendSchema.tables`. Default `true`.
|
|
187
|
+
*
|
|
188
|
+
* Set `false` when your app ALREADY declares equivalent tables and you want to
|
|
189
|
+
* keep them — the seam this exists for. The plugin then contributes no DDL and
|
|
190
|
+
* the declarative differ never proposes its tables; everything else (routes,
|
|
191
|
+
* inspect, interceptors) is unchanged.
|
|
192
|
+
*
|
|
193
|
+
* **What you take over, exactly:** a table named `_voltro_audit_log` with the shape
|
|
194
|
+
* `auditLogTable` declares (exported from this package, so declare it with
|
|
195
|
+
* `.renamedFrom()` or copy its columns). The `datastore` sink writes to it BY
|
|
196
|
+
* NAME through the bound store; nothing validates that it exists, so a missing
|
|
197
|
+
* or mis-shaped table fails at the first audited mutation, not at boot.
|
|
198
|
+
*
|
|
199
|
+
* It is not offered on every plugin, and the omissions are deliberate rather
|
|
200
|
+
* than unfinished: a `tables: false` that quietly disables a table carrying an
|
|
201
|
+
* AUTHORIZATION or SAFETY decision — the SAML replay cache, SCIM provisioning
|
|
202
|
+
* state, billing's usage counters, cdc-out's outbox — is a security regression
|
|
203
|
+
* shipped as an ergonomics feature. Those plugins need a named store seam
|
|
204
|
+
* first, not a boolean.
|
|
205
|
+
*/
|
|
206
|
+
readonly tables?: boolean;
|
|
116
207
|
/**
|
|
117
208
|
* Where to record audit events:
|
|
118
209
|
* - 'console' (default) — pretty-prints to stdout for local dev.
|
|
@@ -336,6 +427,15 @@ export declare interface AuditQueryStore {
|
|
|
336
427
|
query: (descriptor: unknown) => Promise<ReadonlyArray<Record<string, unknown>>>;
|
|
337
428
|
}
|
|
338
429
|
|
|
430
|
+
/** `H(chainId, seq, prevHash, content)` for one row. Pure — the verifier and
|
|
431
|
+
* the writer call the SAME function, which is what makes the check mean
|
|
432
|
+
* anything. */
|
|
433
|
+
export declare const auditRowHash: (row: Record<string, unknown>, link: {
|
|
434
|
+
readonly chainId: string;
|
|
435
|
+
readonly seq: number;
|
|
436
|
+
readonly prevHash: string | null;
|
|
437
|
+
}) => string;
|
|
438
|
+
|
|
339
439
|
export declare type AuditSink = (event: AuditEvent) => void | Promise<void> | Effect.Effect<void>;
|
|
340
440
|
|
|
341
441
|
/** Drop everything in the in-process buffer. Useful between smoke phases. */
|
|
@@ -349,6 +449,10 @@ export declare const clearAuditBuffer: () => void;
|
|
|
349
449
|
*/
|
|
350
450
|
export declare const dataStoreAuditSink: (store: AuditDataStore) => (event: AuditEvent) => Promise<void>;
|
|
351
451
|
|
|
452
|
+
/** Mint a fresh chain. `chainId` is 96 bits of CSPRNG — collisions across
|
|
453
|
+
* replicas would merge two chains into one apparent fork. */
|
|
454
|
+
export declare const makeAuditChain: (chainId?: string) => AuditChain;
|
|
455
|
+
|
|
352
456
|
/**
|
|
353
457
|
* Read the in-process audit buffer. Returns a stable snapshot copy —
|
|
354
458
|
* mutating it has no effect on the running buffer. Empty array when
|
|
@@ -356,6 +460,23 @@ export declare const dataStoreAuditSink: (store: AuditDataStore) => (event: Audi
|
|
|
356
460
|
*/
|
|
357
461
|
export declare const readAuditBuffer: () => ReadonlyArray<AuditEvent>;
|
|
358
462
|
|
|
463
|
+
/**
|
|
464
|
+
* Recompute every chain in `_voltro_audit_log` and report what does not add up.
|
|
465
|
+
*
|
|
466
|
+
* Reads through the same `AuditQueryStore` the other read entry points use, so
|
|
467
|
+
* it works against any dialect and inside a transaction. Ordered by
|
|
468
|
+
* `(chainId, seq)` — the `byAuditChain` index exists for exactly this scan.
|
|
469
|
+
*/
|
|
470
|
+
export declare const verifyAuditChain: (store: AuditQueryStore, options?: VerifyAuditChainOptions) => Promise<AuditChainVerification>;
|
|
471
|
+
|
|
472
|
+
export declare interface VerifyAuditChainOptions {
|
|
473
|
+
/** Verify one writer's chain instead of all of them. */
|
|
474
|
+
readonly chainId?: string;
|
|
475
|
+
/** Cap the rows read. Unset = every row, which is the honest default for a
|
|
476
|
+
* completeness check; pass one for a spot check on a very large table. */
|
|
477
|
+
readonly limit?: number;
|
|
478
|
+
}
|
|
479
|
+
|
|
359
480
|
export { VoltroPlugin }
|
|
360
481
|
|
|
361
482
|
export { }
|
package/dist/index.js
CHANGED
|
@@ -1,25 +1,142 @@
|
|
|
1
1
|
import { audit as e } from "./mixin.js";
|
|
2
2
|
import { Effect as t } from "effect";
|
|
3
|
-
import { definePlugin as n } from "@voltro/protocol";
|
|
4
|
-
import { createLogger as
|
|
5
|
-
import { and as
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
tag
|
|
10
|
-
at
|
|
11
|
-
subjectId
|
|
12
|
-
tenantId
|
|
13
|
-
traceId
|
|
14
|
-
status
|
|
15
|
-
durationMs
|
|
16
|
-
subject
|
|
17
|
-
actor
|
|
18
|
-
scope
|
|
19
|
-
metadata
|
|
20
|
-
input
|
|
21
|
-
outcome
|
|
22
|
-
errorTag
|
|
3
|
+
import { definePlugin as n, pluginInstanceName as r } from "@voltro/protocol";
|
|
4
|
+
import { createLogger as i } from "@voltro/logger";
|
|
5
|
+
import { and as a, eq as o, id as s, integer as c, json as l, registerRetention as u, resolveActorSnapshot as d, retentionTtlMsFromEnv as f, table as p, text as m, timestamp as h } from "@voltro/database";
|
|
6
|
+
import { createHash as g, createHmac as _, randomBytes as v } from "node:crypto";
|
|
7
|
+
//#region src/tableName.ts
|
|
8
|
+
var y = "_voltro_audit_log", b = [
|
|
9
|
+
"tag",
|
|
10
|
+
"at",
|
|
11
|
+
"subjectId",
|
|
12
|
+
"tenantId",
|
|
13
|
+
"traceId",
|
|
14
|
+
"status",
|
|
15
|
+
"durationMs",
|
|
16
|
+
"subject",
|
|
17
|
+
"actor",
|
|
18
|
+
"scope",
|
|
19
|
+
"metadata",
|
|
20
|
+
"input",
|
|
21
|
+
"outcome",
|
|
22
|
+
"errorTag"
|
|
23
|
+
], x = (e) => {
|
|
24
|
+
if (e == null) return "null";
|
|
25
|
+
if (e instanceof Date) return JSON.stringify(e.toISOString());
|
|
26
|
+
let t = typeof e;
|
|
27
|
+
return t === "number" || t === "boolean" || t === "string" ? JSON.stringify(e) : t === "bigint" ? JSON.stringify(String(e)) : Array.isArray(e) ? `[${e.map(x).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)}:${x(t)}`).join(",")}}` : JSON.stringify(String(e));
|
|
28
|
+
}, S = (e) => e instanceof Date ? e.toISOString() : e, C = (e) => x(Object.fromEntries(b.map((t) => [t, S(e[t] ?? null)]))), w = (e) => {
|
|
29
|
+
let t = process.env.VOLTRO_AUDIT_CHAIN_SECRET;
|
|
30
|
+
return t ? _("sha256", t).update(e, "utf8").digest("hex") : g("sha256").update(e, "utf8").digest("hex");
|
|
31
|
+
}, T = (e, t) => w(JSON.stringify([
|
|
32
|
+
t.chainId,
|
|
33
|
+
t.seq,
|
|
34
|
+
t.prevHash,
|
|
35
|
+
C(e)
|
|
36
|
+
])), E = (e = v(12).toString("base64url")) => {
|
|
37
|
+
let t = 0, n = null;
|
|
38
|
+
return {
|
|
39
|
+
chainId: e,
|
|
40
|
+
link: (r) => {
|
|
41
|
+
t += 1;
|
|
42
|
+
let i = T(r, {
|
|
43
|
+
chainId: e,
|
|
44
|
+
seq: t,
|
|
45
|
+
prevHash: n
|
|
46
|
+
}), a = {
|
|
47
|
+
...r,
|
|
48
|
+
chainId: e,
|
|
49
|
+
seq: t,
|
|
50
|
+
prevHash: n,
|
|
51
|
+
hash: i
|
|
52
|
+
};
|
|
53
|
+
return n = i, a;
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
}, D = async (e, t = {}) => {
|
|
57
|
+
let n = t.chainId === void 0 ? void 0 : o("chainId", t.chainId), r = await e.query({
|
|
58
|
+
table: y,
|
|
59
|
+
...n ? { predicate: n } : {},
|
|
60
|
+
order: [{
|
|
61
|
+
column: "chainId",
|
|
62
|
+
direction: "asc"
|
|
63
|
+
}, {
|
|
64
|
+
column: "seq",
|
|
65
|
+
direction: "asc"
|
|
66
|
+
}],
|
|
67
|
+
...t.limit === void 0 ? {} : { take: t.limit }
|
|
68
|
+
}), i = [], a = [], s = 0, c = 0, l = null, u = () => {
|
|
69
|
+
l &&= (a.push({
|
|
70
|
+
chainId: l.chainId,
|
|
71
|
+
from: l.from,
|
|
72
|
+
to: l.last,
|
|
73
|
+
tip: l.tip,
|
|
74
|
+
prunedPrefix: l.from > 1
|
|
75
|
+
}), null);
|
|
76
|
+
};
|
|
77
|
+
for (let e of r) {
|
|
78
|
+
let t = e.chainId, n = e.hash, r = e.seq;
|
|
79
|
+
if (typeof t != "string" || typeof n != "string" || r == null) {
|
|
80
|
+
s += 1;
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
let a = Number(r), o = typeof e.prevHash == "string" ? e.prevHash : null, d = typeof e.id == "string" ? e.id : void 0;
|
|
84
|
+
c += 1, !l || l.chainId !== t ? (u(), l = {
|
|
85
|
+
chainId: t,
|
|
86
|
+
from: a,
|
|
87
|
+
last: a,
|
|
88
|
+
tip: n
|
|
89
|
+
}) : (a === l.last + 1 ? o !== l.tip && i.push({
|
|
90
|
+
kind: "broken-link",
|
|
91
|
+
chainId: t,
|
|
92
|
+
seq: a,
|
|
93
|
+
...d ? { rowId: d } : {},
|
|
94
|
+
detail: `prevHash does not match the hash of seq ${l.last}`
|
|
95
|
+
}) : i.push({
|
|
96
|
+
kind: "gap",
|
|
97
|
+
chainId: t,
|
|
98
|
+
seq: l.last + 1,
|
|
99
|
+
detail: `chain jumps from seq ${l.last} to ${a} — ${a - l.last - 1} row(s) missing`
|
|
100
|
+
}), l.last = a, l.tip = n), T(e, {
|
|
101
|
+
chainId: t,
|
|
102
|
+
seq: a,
|
|
103
|
+
prevHash: o
|
|
104
|
+
}) !== n && i.push({
|
|
105
|
+
kind: "tampered",
|
|
106
|
+
chainId: t,
|
|
107
|
+
seq: a,
|
|
108
|
+
...d ? { rowId: d } : {},
|
|
109
|
+
detail: "the row content does not hash to its stored hash — a column was altered"
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
return u(), {
|
|
113
|
+
ok: i.length === 0,
|
|
114
|
+
rowsChecked: c,
|
|
115
|
+
unchainedRows: s,
|
|
116
|
+
chains: a,
|
|
117
|
+
issues: i,
|
|
118
|
+
keyed: !!process.env.VOLTRO_AUDIT_CHAIN_SECRET
|
|
119
|
+
};
|
|
120
|
+
}, O = p(y, {
|
|
121
|
+
id: s({ prefix: "audit" }),
|
|
122
|
+
tag: m(),
|
|
123
|
+
at: h(),
|
|
124
|
+
subjectId: m().nullable(),
|
|
125
|
+
tenantId: m().nullable(),
|
|
126
|
+
traceId: m(),
|
|
127
|
+
status: m(),
|
|
128
|
+
durationMs: m(),
|
|
129
|
+
subject: l(),
|
|
130
|
+
actor: l().nullable(),
|
|
131
|
+
scope: l().nullable(),
|
|
132
|
+
metadata: l().nullable(),
|
|
133
|
+
input: l(),
|
|
134
|
+
outcome: l(),
|
|
135
|
+
errorTag: m().nullable(),
|
|
136
|
+
chainId: m().nullable(),
|
|
137
|
+
seq: c().nullable(),
|
|
138
|
+
prevHash: m().nullable(),
|
|
139
|
+
hash: m().nullable()
|
|
23
140
|
}).index("byAuditTag", ["tag"]).index("byAuditTrace", ["traceId"]).index("byAuditSubjectStatus", [
|
|
24
141
|
"subjectId",
|
|
25
142
|
"status",
|
|
@@ -28,7 +145,7 @@ var m = "_voltro_audit_log", h = d(m, {
|
|
|
28
145
|
"tenantId",
|
|
29
146
|
"status",
|
|
30
147
|
"at"
|
|
31
|
-
]),
|
|
148
|
+
]).index("byAuditChain", ["chainId", "seq"]), k = [O], A = (e) => ({
|
|
32
149
|
tag: e.tag,
|
|
33
150
|
at: new Date(e.ts),
|
|
34
151
|
subjectId: e.subject.id ?? null,
|
|
@@ -42,40 +159,43 @@ var m = "_voltro_audit_log", h = d(m, {
|
|
|
42
159
|
metadata: e.metadata ?? null,
|
|
43
160
|
input: e.input,
|
|
44
161
|
outcome: e.outcome,
|
|
45
|
-
errorTag: e.outcome.kind === "error" ?
|
|
46
|
-
}),
|
|
162
|
+
errorTag: e.outcome.kind === "error" ? j(e.outcome.error) : null
|
|
163
|
+
}), j = (e) => {
|
|
47
164
|
if (typeof e != "object" || !e) return null;
|
|
48
165
|
let t = e._tag;
|
|
49
166
|
return typeof t == "string" && t !== "" ? t : null;
|
|
50
|
-
},
|
|
51
|
-
table:
|
|
52
|
-
predicate:
|
|
167
|
+
}, M = async (e, t) => e.query({
|
|
168
|
+
table: y,
|
|
169
|
+
predicate: o("traceId", t),
|
|
53
170
|
order: [{
|
|
54
171
|
column: "at",
|
|
55
172
|
direction: "asc"
|
|
56
173
|
}]
|
|
57
|
-
}),
|
|
58
|
-
let r =
|
|
174
|
+
}), N = async (e, t, n = {}) => {
|
|
175
|
+
let r = o("subjectId", t);
|
|
59
176
|
return e.query({
|
|
60
|
-
table:
|
|
61
|
-
predicate: n.status === void 0 ? r :
|
|
177
|
+
table: y,
|
|
178
|
+
predicate: n.status === void 0 ? r : a(r, o("status", n.status)),
|
|
62
179
|
order: [{
|
|
63
180
|
column: "at",
|
|
64
181
|
direction: "desc"
|
|
65
182
|
}],
|
|
66
183
|
take: n.limit ?? 100
|
|
67
184
|
});
|
|
68
|
-
},
|
|
69
|
-
let
|
|
70
|
-
|
|
71
|
-
actor
|
|
185
|
+
}, P = (e) => {
|
|
186
|
+
let t = E();
|
|
187
|
+
return async (n) => {
|
|
188
|
+
let r = n.actor !== void 0 || typeof e.query != "function" ? n : {
|
|
189
|
+
...n,
|
|
190
|
+
actor: await d(e, n.subject.id ?? null, n.subject.type)
|
|
191
|
+
};
|
|
192
|
+
await e.insert(y, t.link(A(r)));
|
|
72
193
|
};
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
}, O = (e) => {
|
|
194
|
+
}, F = i({ scope: "@voltro/plugin-audit" }), I = 1e3, L = [], R = () => [...L], z = () => {
|
|
195
|
+
L.length = 0;
|
|
196
|
+
}, B = (e) => {
|
|
197
|
+
L.push(e), L.length > I && L.splice(0, L.length - I);
|
|
198
|
+
}, V = (e) => {
|
|
79
199
|
let t = e.subject.id ? `${e.subject.type}:${e.subject.id}` : e.subject.type, n = e.outcome.kind === "ok" ? "ok" : "error", r = {
|
|
80
200
|
subject: t,
|
|
81
201
|
tenant: e.subject.tenantId ?? null,
|
|
@@ -83,8 +203,8 @@ var m = "_voltro_audit_log", h = d(m, {
|
|
|
83
203
|
durationMs: e.outcome.durationMs,
|
|
84
204
|
traceId: e.traceId
|
|
85
205
|
};
|
|
86
|
-
n === "error" ?
|
|
87
|
-
},
|
|
206
|
+
n === "error" ? F.warn(e.tag, r) : F.info(e.tag, r);
|
|
207
|
+
}, H = (e, n) => typeof e == "function" ? (n) => t.suspend(() => {
|
|
88
208
|
let r = e(n);
|
|
89
209
|
return r === void 0 ? t.void : t.isEffect(r) ? r : t.tryPromise({
|
|
90
210
|
try: () => Promise.resolve(r),
|
|
@@ -96,7 +216,7 @@ var m = "_voltro_audit_log", h = d(m, {
|
|
|
96
216
|
try: () => r(e),
|
|
97
217
|
catch: (e) => e
|
|
98
218
|
}) : t.void;
|
|
99
|
-
}) : e === "memory" ? (e) => t.sync(() =>
|
|
219
|
+
}) : e === "memory" ? (e) => t.sync(() => B(e)) : (e) => t.sync(() => V(e)), U = (e) => {
|
|
100
220
|
let t = [], n = [], r = (e) => {
|
|
101
221
|
if (e._tag !== "Empty") {
|
|
102
222
|
if (e._tag === "Fail") {
|
|
@@ -120,26 +240,27 @@ var m = "_voltro_audit_log", h = d(m, {
|
|
|
120
240
|
}
|
|
121
241
|
};
|
|
122
242
|
return r(e), t[0] ?? n[0] ?? e;
|
|
123
|
-
},
|
|
124
|
-
let
|
|
125
|
-
|
|
126
|
-
|
|
243
|
+
}, W = "@voltro/plugin-audit", G = (e = {}) => {
|
|
244
|
+
let i = e.sink === "datastore";
|
|
245
|
+
i && u({
|
|
246
|
+
source: "plugin",
|
|
247
|
+
table: y,
|
|
127
248
|
timeColumn: "at",
|
|
128
|
-
ttlMs:
|
|
249
|
+
ttlMs: f(process.env.VOLTRO_AUDIT_LOG_TTL_HOURS, 24 * 365)
|
|
129
250
|
});
|
|
130
|
-
let
|
|
251
|
+
let a, o = H(e.sink, () => a), s = (e) => e === void 0 || !e.global && !e.sticky ? e : new RegExp(e.source, e.flags.replace(/[gy]/g, "")), c = s(e.exclude), l = s(e.include), d = (e) => !(c?.test(e) || l && !l.test(e)), p = (t) => {
|
|
131
252
|
let n = e.record ?? "all";
|
|
132
253
|
return n === "all" ? !0 : n === "errors" ? t.outcome.kind === "error" : n(t);
|
|
133
|
-
},
|
|
254
|
+
}, m = { __redacted: "all" }, h = (t) => {
|
|
134
255
|
let n = e.redactInput ?? "all";
|
|
135
256
|
return n === "none" ? t : {
|
|
136
257
|
...t,
|
|
137
|
-
input: n === "all" ?
|
|
258
|
+
input: n === "all" ? m : n(t)
|
|
138
259
|
};
|
|
139
|
-
},
|
|
260
|
+
}, g = (t) => {
|
|
140
261
|
let n = e.redactOutcome ?? "all";
|
|
141
262
|
if (n === "none") return t;
|
|
142
|
-
let r = n === "all" ?
|
|
263
|
+
let r = n === "all" ? m : n(t), i = t.outcome;
|
|
143
264
|
if (i.kind === "ok") return {
|
|
144
265
|
...t,
|
|
145
266
|
outcome: {
|
|
@@ -158,7 +279,7 @@ var m = "_voltro_audit_log", h = d(m, {
|
|
|
158
279
|
}
|
|
159
280
|
}
|
|
160
281
|
};
|
|
161
|
-
},
|
|
282
|
+
}, _ = (t) => {
|
|
162
283
|
let n = e.redactSubject ?? "metadata";
|
|
163
284
|
if (n === "none") return t;
|
|
164
285
|
if (typeof n == "function") return {
|
|
@@ -172,18 +293,18 @@ var m = "_voltro_audit_log", h = d(m, {
|
|
|
172
293
|
...t,
|
|
173
294
|
subject: {
|
|
174
295
|
...a,
|
|
175
|
-
metadata:
|
|
296
|
+
metadata: m
|
|
176
297
|
}
|
|
177
298
|
};
|
|
178
|
-
},
|
|
299
|
+
}, v = (e) => p(e) ? o(g(_(h(e)))) : t.void, b = (t) => {
|
|
179
300
|
if (e.resolveScope !== void 0) try {
|
|
180
301
|
return e.resolveScope(t);
|
|
181
302
|
} catch {
|
|
182
303
|
return;
|
|
183
304
|
}
|
|
184
|
-
},
|
|
185
|
-
let r = Date.now(), i =
|
|
186
|
-
return e.pipe(t.tap((e) =>
|
|
305
|
+
}, x = (e, n) => d(n.tag) ? t.suspend(() => {
|
|
306
|
+
let r = Date.now(), i = b(n);
|
|
307
|
+
return e.pipe(t.tap((e) => v({
|
|
187
308
|
ts: r,
|
|
188
309
|
tag: n.tag,
|
|
189
310
|
subject: n.subject,
|
|
@@ -195,7 +316,7 @@ var m = "_voltro_audit_log", h = d(m, {
|
|
|
195
316
|
value: e,
|
|
196
317
|
durationMs: Date.now() - r
|
|
197
318
|
}
|
|
198
|
-
}).pipe(t.catchAllCause(() => t.void))), t.tapErrorCause((e) =>
|
|
319
|
+
}).pipe(t.catchAllCause(() => t.void))), t.tapErrorCause((e) => v({
|
|
199
320
|
ts: r,
|
|
200
321
|
tag: n.tag,
|
|
201
322
|
subject: n.subject,
|
|
@@ -204,30 +325,34 @@ var m = "_voltro_audit_log", h = d(m, {
|
|
|
204
325
|
input: n.input,
|
|
205
326
|
outcome: {
|
|
206
327
|
kind: "error",
|
|
207
|
-
error:
|
|
328
|
+
error: U(e),
|
|
208
329
|
durationMs: Date.now() - r
|
|
209
330
|
}
|
|
210
331
|
}).pipe(t.catchAllCause(() => t.void))));
|
|
211
|
-
}) : e,
|
|
332
|
+
}) : e, S = x, C = x, w = x;
|
|
212
333
|
return n({
|
|
213
|
-
name:
|
|
334
|
+
name: r({
|
|
335
|
+
base: W,
|
|
336
|
+
alias: e.alias
|
|
337
|
+
}),
|
|
338
|
+
baseName: W,
|
|
214
339
|
description: "Records every mutation invocation; ships an audit() schema mixin for row-level metadata.",
|
|
215
340
|
permissions: [
|
|
216
341
|
"rpc:intercept:mutation",
|
|
217
342
|
"rpc:intercept:action",
|
|
218
|
-
...
|
|
343
|
+
...i ? ["store:write"] : [],
|
|
219
344
|
...e.recordQueries === !0 ? ["rpc:intercept:query"] : []
|
|
220
345
|
],
|
|
221
|
-
...
|
|
222
|
-
extendSchema: { tables:
|
|
346
|
+
...i ? {
|
|
347
|
+
...e.tables === !1 ? {} : { extendSchema: { tables: k } },
|
|
223
348
|
bindDataStore: (e) => {
|
|
224
|
-
|
|
349
|
+
a = P(e);
|
|
225
350
|
}
|
|
226
351
|
} : {},
|
|
227
|
-
interceptMutation:
|
|
228
|
-
interceptAction:
|
|
229
|
-
...e.recordQueries === !0 ? { interceptQuery:
|
|
352
|
+
interceptMutation: S,
|
|
353
|
+
interceptAction: C,
|
|
354
|
+
...e.recordQueries === !0 ? { interceptQuery: w } : {}
|
|
230
355
|
});
|
|
231
356
|
};
|
|
232
357
|
//#endregion
|
|
233
|
-
export {
|
|
358
|
+
export { y as AUDIT_LOG_TABLE, e as audit, N as auditBySubject, M as auditByTrace, A as auditEventToRow, O as auditLogTable, k as auditLogTables, G as auditPlugin, T as auditRowHash, z as clearAuditBuffer, P as dataStoreAuditSink, E as makeAuditChain, R as readAuditBuffer, D as verifyAuditChain };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@voltro/plugin-audit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.34.0",
|
|
4
4
|
"description": "Audit plugin — ships the `audit()` schema mixin (createdAt/updatedAt/createdBy/updatedBy → Actor) plus an optional mutation interceptor that records every call to a configurable sink (console / memory / custom function).",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"voltro",
|
|
@@ -38,9 +38,9 @@
|
|
|
38
38
|
"node": ">=24.0.0"
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
|
-
"@voltro/database": "0.
|
|
42
|
-
"@voltro/logger": "0.
|
|
43
|
-
"@voltro/protocol": "0.
|
|
41
|
+
"@voltro/database": "0.34.0",
|
|
42
|
+
"@voltro/logger": "0.34.0",
|
|
43
|
+
"@voltro/protocol": "0.34.0"
|
|
44
44
|
},
|
|
45
45
|
"peerDependencies": {
|
|
46
46
|
"effect": "^3.22.0"
|