@voltro/plugin-audit 0.36.0 → 0.37.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 +90 -0
- package/dist/index.d.ts +25 -3
- package/dist/index.js +83 -59
- package/package.json +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -39,6 +39,96 @@ _Changes staged for the next release accumulate here (rolled up from
|
|
|
39
39
|
|
|
40
40
|
---
|
|
41
41
|
|
|
42
|
+
## [0.37.0] — 2026-08-13
|
|
43
|
+
|
|
44
|
+
### ⚠ BREAKING
|
|
45
|
+
|
|
46
|
+
- **@voltro/protocol, @voltro/runtime, @voltro/cli** — A field a procedure's input schema does not declare now REJECTS the call. It used to be discarded and the call ran with what was left.
|
|
47
|
+
|
|
48
|
+
The measurement, from a consumer's root layout:
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
query?.('userSettings.list', { employeeId })
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
That procedure declares `userId` / `userIdIn`. Effect's default `onExcessProperty: 'ignore'` decoded the payload to `{}` — not reasoned, measured:
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
decodeUnknownSync(Struct({ userId: optional(String) }))({ employeeId: 'e' }) // → {}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
An empty input to a LIST query is not a narrower filter, it is the ABSENCE of one. Their admin, signed in as `2d0add2c…`, was served the settings row of `4410c2f8…` — another user's language and theme in the first paint, with nothing in any log to say so.
|
|
61
|
+
|
|
62
|
+
**Why refuse rather than warn.** The decoder cannot tell a projection field from a FILTER field, and that asymmetry is the whole risk: dropping an unknown `include` costs a caller some data, dropping an unknown `tenantId` hands them somebody else's. Nothing at decode time distinguishes the two, so the safe direction is the only one available — the same fail-closed reasoning as the row filter's refusal, one layer up. A warning would have to be read by someone, in a log, after the wrong rows were already served.
|
|
63
|
+
|
|
64
|
+
The typed loader query that shipped in 0.36.0 closes the same hole for callers we compile. This closes it for the ones we do not: a plain `fetch`, a curl, a still-cached bundle after a field rename, and every untyped caller.
|
|
65
|
+
|
|
66
|
+
Three things measured rather than assumed, because none follows from the annotation's name: it propagates into NESTED structs, through every member of a UNION, and leaves a non-struct payload (`Schema.Void`, a scalar) alone.
|
|
67
|
+
|
|
68
|
+
**`Schema.Struct({})` needed a filter, and only a real process showed it.** The fixture's `notes.list` declares an empty input; `POST /rpc` with `{ employeeId }` came back `200` with a snapshot, which for twenty minutes read as the whole change having failed. An empty `TypeLiteral` has no property signatures, so Effect has no expected key set for a key to be excess OF — self-consistent, and the wrong answer here, because `input: Schema.Struct({})` is the STRONGEST declaration a procedure can make and it was the one shape that accepted everything. It gets an explicit predicate now; `Schema.Record` keeps its open key set, because there the openness is declared.
|
|
69
|
+
|
|
70
|
+
**Verified against a running `voltro serve`, not only in units.** A declared input succeeds and inserts its row; an undeclared field is refused naming the key and the accepted set. The refusal arrives on the channel a payload decode failure ALREADY used — a missing required field produces the same `Die` with a `ParseError` message — so this adds no new error shape for a client to handle, it moves one case onto the channel the sibling case was always on.
|
|
71
|
+
|
|
72
|
+
`strictInput` lives in one module and every `Rpc.make` payload in `@voltro/protocol` goes through it — query, mutation, action, stream, event, plus the workflow start on both the server lifter and the browser-loaded rpc group. `strictInput.test.ts` asserts that SET by scanning the source, not the five lifters somebody remembered: a rule applied at the sites you can list is the shape that let `bootStoreCodec` be fixed twice and break a third time.
|
|
73
|
+
|
|
74
|
+
**`voltro update` carries you across this** — codemod `0.37.0/01_procedure-input-rejects-undeclared-fields`, a written note. A transform would have to guess which declared field a stray one meant, which is the same guess that produced the defect.
|
|
75
|
+
|
|
76
|
+
### Added
|
|
77
|
+
|
|
78
|
+
- **@voltro/plugin-audit** — `redactInput` / `redactOutcome` gained `'shape'`, and `redactSubject` gained `'metadata-shape'`: the payload's STRUCTURE survives, no value from it.
|
|
79
|
+
|
|
80
|
+
```json
|
|
81
|
+
{ "__redacted": { "jiraToken": "string(113)", "attempts": "number" } }
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Requested by the consumer who had asked for the redaction one round earlier, and both requests were right. They spent a day on a bug their own audit trail could have ended in seconds — a value arrived as 113 characters where 44 were due, and the row that would have said so read `{"__redacted":"all"}`. `'all'` remains the default on every field; this is opt-in.
|
|
85
|
+
|
|
86
|
+
The rules, and the two that are decisions rather than details:
|
|
87
|
+
|
|
88
|
+
- A string reports its LENGTH. Never a prefix, never a hash — `enc:v1:` is a prefix and so is the first byte of a private key, so there is no prefix length that is safe for every credential format. - A number, boolean or date reports its TYPE only. A number can BE the secret. - **A key can be the value.** An object keyed by user data puts a datum where a schema name belongs, so a key is reproduced only when it looks like a declared field — a short plain identifier. The first version truncated long keys and documented the weakness instead; this module's own test caught 62 characters of a secret surviving on the first run. A leak with a footnote is still a leak. - **A string's length is a real disclosure, and a small one.** Stated in the docs rather than buried: for a fixed-format credential it carries nothing, for a human-chosen password it is a weak hint. `'all'` stays the default for anyone that matters to.
|
|
89
|
+
|
|
90
|
+
The `'shape'` outcome describes the payload it REPLACES — the value on success, the error on failure — rather than the event. Describing the event would report `{ kind, value, durationMs }` and hide the field, which is the failure the option exists to end. An error's `_tag` still survives, as it does under `'all'`.
|
|
91
|
+
|
|
92
|
+
### Fixed
|
|
93
|
+
|
|
94
|
+
- **@voltro/cli** — `voltro db --help` listed fifteen of eighteen subcommands. `adopt`, `scan-credentials` and `encrypt-column` shipped and never joined the hand-written string.
|
|
95
|
+
|
|
96
|
+
A consumer wrote both halves of that gap into a requirements document, as separate items, neither of them about help text:
|
|
97
|
+
|
|
98
|
+
- **`voltro db encrypt-column` "does not exist"** — filed as a feature request, quoting the fifteen names they saw as evidence. It shipped in 0.33.0, and enabling `.encrypted()` on a populated column by hand is exactly the migration they were about to write themselves. - **`scan-credentials` "no longer exists"** — filed as CLOSED, a credential scanner struck off their list as removed. It had not moved.
|
|
99
|
+
|
|
100
|
+
A quoted enumeration is read as exhaustive, and the more careful the reader the more thoroughly they act on the missing entry. Same lesson a boot refusal in `procedureAccessGate` had already taught us, in a place nobody thought of as a message.
|
|
101
|
+
|
|
102
|
+
The usage line is now GENERATED from the dispatch table's key type (`Record<DbSubcommand, Handler>` in `dbCommand.ts`, names in `subcommandNames.ts`), so a handler with no name or a name with no handler fails to compile. `voltro privacy` is keyed the same way. The prose summary beside it cannot be generated — it carries per-command annotations — so a test asserts it mentions every name, because it carried the identical three omissions and it is what `voltro --help` prints first.
|
|
103
|
+
|
|
104
|
+
`subcommandHelpParity.test.ts` also NAMES the six commands whose subcommand menus have no dispatch table behind them (`webhooks`, `evolve`, `new`, `data`, `storage`, `add`). They dispatch through a switch and are unchecked; a silently-unchecked command reads exactly like a checked one.
|
|
105
|
+
- **@voltro/cli** — A 401 or 403 from the inspect surface now names `VOLTRO_INSPECT_TOKEN` and says which side is missing.
|
|
106
|
+
|
|
107
|
+
`voltro db plan --against <url>` printed `remote returned 403` and stopped. A consumer read that as a DATABASE permission problem — the natural reading of a 403 from a command whose entire subject is a database — and went looking at grants. The cause is one unset environment variable, which the command reads four lines above the message.
|
|
108
|
+
|
|
109
|
+
`voltro probe access` had half of it: it named the variable on 401 and not on 403, while classifying both as `refused`. So the two commands somebody needs during an access migration were the two that would not say what was wrong, and one of them said something misleading instead.
|
|
110
|
+
|
|
111
|
+
`inspectGateHint` is shared by both call sites and distinguishes the two statuses, because they call for different actions: a 401 means no credential was sent (set the variable), a 403 means the one sent was not accepted (the two values differ). Both halves of the sentence name the server AND the calling shell — naming one side produces a second failed attempt.
|
|
112
|
+
- **@voltro/cli** — `VOLTRO_TEMPLATES_DIR` is authoritative when set. It used to be a HINT: if the path it named held no `apps/` (or no `baselines/`), both resolvers fell through to the sibling-checkout walk-up and quietly used a different tree — or none.
|
|
113
|
+
|
|
114
|
+
A pointer that silently isn't followed is worse than a wrong one. A CI job aimed at the wrong path scaffolded from whatever it happened to find, and a job whose checkout had failed reported an empty template catalogue with nothing connecting that emptiness to the variable it was given. `scripts/lib/docsSite.mjs` states the same rule for `VOLTRO_DOCS_DIR`, and arrived at it the same way: you said where it is; it is not there.
|
|
115
|
+
|
|
116
|
+
Behaviourally this only changes the misconfigured case — a correct `VOLTRO_TEMPLATES_DIR` resolved to the same place before and after. What changes is that a wrong one now shows up as "not found, here is the path I was told" at the first thing that reads it, instead of as a different tree three steps later.
|
|
117
|
+
|
|
118
|
+
The unbundled resolution order is otherwise untouched: sibling `voltro-templates` → `.voltro-templates` → the bundled `templates/` a published CLI ships.
|
|
119
|
+
|
|
120
|
+
### Internal (no consumer-facing effect)
|
|
121
|
+
|
|
122
|
+
- **@voltro/plugin-ai-flows** — Two comments in the flow engine cited task records from a plans tracker that has since been deleted. Comment-only; no behavior, no API, nothing a consumer can observe.
|
|
123
|
+
|
|
124
|
+
Worth writing down because of HOW it surfaced. The tracker was retired in the META repo, and the gate that went red was in THIS one — `check-stale-task-comments.mjs` resolves a comment's `task #NN` against `../plans`, so deleting a plan document in one repo can only be half a change, and the other half is in a repo the deleting commit never touched.
|
|
125
|
+
|
|
126
|
+
Neither comment was WRONG, which is the part that makes the rule earn its keep. The first claims `@voltro/ai` has first-class media generation — true: `generateImage`, `generateSpeech`, `generateVideo` all ship in `packages/ai/src/media.ts`. It now names those three instead of a record number, which is checkable without the deleted document. The second only quoted the retired id inside its own account of a defect (a `"not yet wired (task #35)"` message that outlived the shipped HITL park and misled an audit into filing it as unbuilt); the quote lost the number and kept the whole lesson.
|
|
127
|
+
|
|
128
|
+
The check's own failure text is the reasoning: a plan is retired for exactly two reasons — the work shipped, or it was dropped without shipping — and a comment still citing it asserts the second while usually meaning the first.
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
42
132
|
## [0.36.0] — 2026-08-13
|
|
43
133
|
|
|
44
134
|
### ⚠ BREAKING
|
package/dist/index.d.ts
CHANGED
|
@@ -325,6 +325,10 @@ export declare interface AuditPluginOptions {
|
|
|
325
325
|
*
|
|
326
326
|
* - `'all'` (DEFAULT) — the payload is replaced by `{ __redacted: 'all' }`.
|
|
327
327
|
* The row still proves a payload existed; it just does not carry it.
|
|
328
|
+
* - `'shape'` — the payload's STRUCTURE, no value from it:
|
|
329
|
+
* `{ __redacted: { token: 'string(113)', limit: 'number' } }`. See
|
|
330
|
+
* `redactionShape.ts` for the rules and for the one real trade (a
|
|
331
|
+
* string's LENGTH is disclosed).
|
|
328
332
|
* - `'none'` — the raw input, verbatim. What every sink did before this
|
|
329
333
|
* option existed.
|
|
330
334
|
* - a function — `(event) => unknown`, for field-level control.
|
|
@@ -345,13 +349,17 @@ export declare interface AuditPluginOptions {
|
|
|
345
349
|
* row; leaking a credential is not visible at all. Opt in per app with a
|
|
346
350
|
* function once you know your own inputs.
|
|
347
351
|
*/
|
|
348
|
-
readonly redactInput?: 'all' | 'none' | ((event: AuditEvent) => unknown);
|
|
352
|
+
readonly redactInput?: 'all' | 'shape' | 'none' | ((event: AuditEvent) => unknown);
|
|
349
353
|
/**
|
|
350
354
|
* What happens to `AuditEvent.subject` before it is handed to the sink.
|
|
351
355
|
*
|
|
352
356
|
* - `'metadata'` (DEFAULT) — `subject.metadata` is replaced by
|
|
353
357
|
* `{ __redacted: 'all' }`. `type`, `id`, `tenantId` and `scopes` survive,
|
|
354
358
|
* which is everything the trail is actually read for.
|
|
359
|
+
* - `'metadata-shape'` — the same, but `metadata`'s STRUCTURE survives
|
|
360
|
+
* instead of nothing. Useful for the case this whole option exists for:
|
|
361
|
+
* seeing THAT a credential-shaped value sits in the bag, and how long it
|
|
362
|
+
* is, without recording it.
|
|
355
363
|
* - `'none'` — the subject verbatim. What every sink did before this option
|
|
356
364
|
* existed.
|
|
357
365
|
* - a function — `(subject) => unknown`, for field-level control.
|
|
@@ -379,8 +387,18 @@ export declare interface AuditPluginOptions {
|
|
|
379
387
|
* `resolveScope` still sees the LIVE subject, so a scope derived from
|
|
380
388
|
* `metadata` keeps working — redaction applies to what is STORED, not to what
|
|
381
389
|
* the plugin can compute.
|
|
390
|
+
*
|
|
391
|
+
* KNOWN DEBT, on the DEFAULT. An impersonation mark lives in `metadata`, so
|
|
392
|
+
* under `'metadata'` a durable trail records an impersonated action
|
|
393
|
+
* indistinguishably from the user's own — the one distinction an audit trail
|
|
394
|
+
* exists to make. `impersonationAuditRedactor()` plus an always-on grant row
|
|
395
|
+
* mitigates it, and `'metadata-shape'` narrows it further (the KEY survives,
|
|
396
|
+
* so the row at least says an impersonation mark was there). Neither is the
|
|
397
|
+
* fix. The fix is a keep-these-keys option, or a first-class
|
|
398
|
+
* `AuditEvent.impersonation` field that the redactors cannot reach — because
|
|
399
|
+
* "who really did this" is not app-controlled metadata, it is the event.
|
|
382
400
|
*/
|
|
383
|
-
readonly redactSubject?: 'metadata' | 'none' | ((subject: Subject) => unknown);
|
|
401
|
+
readonly redactSubject?: 'metadata' | 'metadata-shape' | 'none' | ((subject: Subject) => unknown);
|
|
384
402
|
/**
|
|
385
403
|
* What happens to `AuditEvent.outcome`'s payload before it is handed to the
|
|
386
404
|
* sink.
|
|
@@ -388,6 +406,10 @@ export declare interface AuditPluginOptions {
|
|
|
388
406
|
* - `'all'` (DEFAULT) — `outcome.value` on success, and `outcome.error` on
|
|
389
407
|
* failure, are replaced by `{ __redacted: 'all' }`. `kind`, `durationMs`
|
|
390
408
|
* and the error's TAG survive, which is what the trail is read for.
|
|
409
|
+
* - `'shape'` — the STRUCTURE survives instead of nothing. This is the
|
|
410
|
+
* option a consumer asked for after a day spent on a bug their own trail
|
|
411
|
+
* could have ended: a value arrived as 113 characters where 44 were due,
|
|
412
|
+
* and the row that would have said so read `{"__redacted":"all"}`.
|
|
391
413
|
* - `'none'` — the outcome verbatim. What every sink did before this
|
|
392
414
|
* option existed.
|
|
393
415
|
* - a function — `(event) => unknown`, for field-level control.
|
|
@@ -419,7 +441,7 @@ export declare interface AuditPluginOptions {
|
|
|
419
441
|
* A `record` predicate still sees the LIVE outcome, so a filter that keys on
|
|
420
442
|
* the result keeps working — redaction applies to what is STORED.
|
|
421
443
|
*/
|
|
422
|
-
readonly redactOutcome?: 'all' | 'none' | ((event: AuditEvent) => unknown);
|
|
444
|
+
readonly redactOutcome?: 'all' | 'shape' | 'none' | ((event: AuditEvent) => unknown);
|
|
423
445
|
}
|
|
424
446
|
|
|
425
447
|
/** The narrow read surface the entry points need. */
|
package/dist/index.js
CHANGED
|
@@ -4,8 +4,26 @@ import { definePlugin as n, pluginInstanceName as r } from "@voltro/protocol";
|
|
|
4
4
|
import { createLogger as i } from "@voltro/logger";
|
|
5
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
6
|
import { createHash as g, createHmac as _, randomBytes as v } from "node:crypto";
|
|
7
|
-
//#region src/
|
|
8
|
-
var y =
|
|
7
|
+
//#region src/redactionShape.ts
|
|
8
|
+
var y = 4, b = 32, x = 64, S = /^[A-Za-z_$][A-Za-z0-9_$]*$/, C = (e) => e.length <= x && S.test(e) ? e : `<key:string(${e.length})>`, w = (e, t = 0) => {
|
|
9
|
+
if (e === null) return "null";
|
|
10
|
+
if (e === void 0) return "undefined";
|
|
11
|
+
let n = typeof e;
|
|
12
|
+
if (n === "string") return `string(${e.length})`;
|
|
13
|
+
if (n === "number" || n === "boolean" || n === "bigint" || n === "symbol" || n === "function") return n;
|
|
14
|
+
if (e instanceof Date) return "date";
|
|
15
|
+
if (t >= y) return Array.isArray(e) ? "array(…)" : "object(…)";
|
|
16
|
+
if (Array.isArray(e)) {
|
|
17
|
+
let n = e.slice(0, b).map((e) => w(e, t + 1));
|
|
18
|
+
return e.length > b ? [...n, `…${e.length - b} more`] : n;
|
|
19
|
+
}
|
|
20
|
+
if (n === "object") {
|
|
21
|
+
let n = Object.entries(e), r = {};
|
|
22
|
+
for (let [e, i] of n.slice(0, b)) r[C(e)] = w(i, t + 1);
|
|
23
|
+
return n.length > b && (r["…"] = `${n.length - b} more key(s)`), r;
|
|
24
|
+
}
|
|
25
|
+
return n;
|
|
26
|
+
}, T = (e) => ({ __redacted: w(e) }), E = "_voltro_audit_log", D = [
|
|
9
27
|
"tag",
|
|
10
28
|
"at",
|
|
11
29
|
"subjectId",
|
|
@@ -20,26 +38,26 @@ var y = "_voltro_audit_log", b = [
|
|
|
20
38
|
"input",
|
|
21
39
|
"outcome",
|
|
22
40
|
"errorTag"
|
|
23
|
-
],
|
|
41
|
+
], O = (e) => {
|
|
24
42
|
if (e == null) return "null";
|
|
25
43
|
if (e instanceof Date) return JSON.stringify(e.toISOString());
|
|
26
44
|
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(
|
|
28
|
-
},
|
|
45
|
+
return t === "number" || t === "boolean" || t === "string" ? JSON.stringify(e) : t === "bigint" ? JSON.stringify(String(e)) : Array.isArray(e) ? `[${e.map(O).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)}:${O(t)}`).join(",")}}` : JSON.stringify(String(e));
|
|
46
|
+
}, k = (e) => e instanceof Date ? e.toISOString() : e, A = (e) => O(Object.fromEntries(D.map((t) => [t, k(e[t] ?? null)]))), j = (e) => {
|
|
29
47
|
let t = process.env.VOLTRO_AUDIT_CHAIN_SECRET;
|
|
30
48
|
return t ? _("sha256", t).update(e, "utf8").digest("hex") : g("sha256").update(e, "utf8").digest("hex");
|
|
31
|
-
},
|
|
49
|
+
}, M = (e, t) => j(JSON.stringify([
|
|
32
50
|
t.chainId,
|
|
33
51
|
t.seq,
|
|
34
52
|
t.prevHash,
|
|
35
|
-
|
|
36
|
-
])),
|
|
53
|
+
A(e)
|
|
54
|
+
])), N = (e = v(12).toString("base64url")) => {
|
|
37
55
|
let t = 0, n = null;
|
|
38
56
|
return {
|
|
39
57
|
chainId: e,
|
|
40
58
|
link: (r) => {
|
|
41
59
|
t += 1;
|
|
42
|
-
let i =
|
|
60
|
+
let i = M(r, {
|
|
43
61
|
chainId: e,
|
|
44
62
|
seq: t,
|
|
45
63
|
prevHash: n
|
|
@@ -53,9 +71,9 @@ var y = "_voltro_audit_log", b = [
|
|
|
53
71
|
return n = i, a;
|
|
54
72
|
}
|
|
55
73
|
};
|
|
56
|
-
},
|
|
74
|
+
}, P = async (e, t = {}) => {
|
|
57
75
|
let n = t.chainId === void 0 ? void 0 : o("chainId", t.chainId), r = await e.query({
|
|
58
|
-
table:
|
|
76
|
+
table: E,
|
|
59
77
|
...n ? { predicate: n } : {},
|
|
60
78
|
order: [{
|
|
61
79
|
column: "chainId",
|
|
@@ -97,7 +115,7 @@ var y = "_voltro_audit_log", b = [
|
|
|
97
115
|
chainId: t,
|
|
98
116
|
seq: l.last + 1,
|
|
99
117
|
detail: `chain jumps from seq ${l.last} to ${a} — ${a - l.last - 1} row(s) missing`
|
|
100
|
-
}), l.last = a, l.tip = n),
|
|
118
|
+
}), l.last = a, l.tip = n), M(e, {
|
|
101
119
|
chainId: t,
|
|
102
120
|
seq: a,
|
|
103
121
|
prevHash: o
|
|
@@ -117,7 +135,7 @@ var y = "_voltro_audit_log", b = [
|
|
|
117
135
|
issues: i,
|
|
118
136
|
keyed: !!process.env.VOLTRO_AUDIT_CHAIN_SECRET
|
|
119
137
|
};
|
|
120
|
-
},
|
|
138
|
+
}, F = p(E, {
|
|
121
139
|
id: s({ prefix: "audit" }),
|
|
122
140
|
tag: m(),
|
|
123
141
|
at: h(),
|
|
@@ -145,7 +163,7 @@ var y = "_voltro_audit_log", b = [
|
|
|
145
163
|
"tenantId",
|
|
146
164
|
"status",
|
|
147
165
|
"at"
|
|
148
|
-
]).index("byAuditChain", ["chainId", "seq"]),
|
|
166
|
+
]).index("byAuditChain", ["chainId", "seq"]), I = [F], L = (e) => ({
|
|
149
167
|
tag: e.tag,
|
|
150
168
|
at: new Date(e.ts),
|
|
151
169
|
subjectId: e.subject.id ?? null,
|
|
@@ -159,22 +177,22 @@ var y = "_voltro_audit_log", b = [
|
|
|
159
177
|
metadata: e.metadata ?? null,
|
|
160
178
|
input: e.input,
|
|
161
179
|
outcome: e.outcome,
|
|
162
|
-
errorTag: e.outcome.kind === "error" ?
|
|
163
|
-
}),
|
|
180
|
+
errorTag: e.outcome.kind === "error" ? R(e.outcome.error) : null
|
|
181
|
+
}), R = (e) => {
|
|
164
182
|
if (typeof e != "object" || !e) return null;
|
|
165
183
|
let t = e._tag;
|
|
166
184
|
return typeof t == "string" && t !== "" ? t : null;
|
|
167
|
-
},
|
|
168
|
-
table:
|
|
185
|
+
}, z = async (e, t) => e.query({
|
|
186
|
+
table: E,
|
|
169
187
|
predicate: o("traceId", t),
|
|
170
188
|
order: [{
|
|
171
189
|
column: "at",
|
|
172
190
|
direction: "asc"
|
|
173
191
|
}]
|
|
174
|
-
}),
|
|
192
|
+
}), B = async (e, t, n = {}) => {
|
|
175
193
|
let r = o("subjectId", t);
|
|
176
194
|
return e.query({
|
|
177
|
-
table:
|
|
195
|
+
table: E,
|
|
178
196
|
predicate: n.status === void 0 ? r : a(r, o("status", n.status)),
|
|
179
197
|
order: [{
|
|
180
198
|
column: "at",
|
|
@@ -182,20 +200,20 @@ var y = "_voltro_audit_log", b = [
|
|
|
182
200
|
}],
|
|
183
201
|
take: n.limit ?? 100
|
|
184
202
|
});
|
|
185
|
-
},
|
|
186
|
-
let t =
|
|
203
|
+
}, V = (e) => {
|
|
204
|
+
let t = N();
|
|
187
205
|
return async (n) => {
|
|
188
206
|
let r = n.actor !== void 0 || typeof e.query != "function" ? n : {
|
|
189
207
|
...n,
|
|
190
208
|
actor: await d(e, n.subject.id ?? null, n.subject.type)
|
|
191
209
|
};
|
|
192
|
-
await e.insert(
|
|
210
|
+
await e.insert(E, t.link(L(r)));
|
|
193
211
|
};
|
|
194
|
-
},
|
|
195
|
-
|
|
196
|
-
},
|
|
197
|
-
|
|
198
|
-
},
|
|
212
|
+
}, H = i({ scope: "@voltro/plugin-audit" }), U = 1e3, W = [], G = () => [...W], K = () => {
|
|
213
|
+
W.length = 0;
|
|
214
|
+
}, q = (e) => {
|
|
215
|
+
W.push(e), W.length > U && W.splice(0, W.length - U);
|
|
216
|
+
}, J = (e) => {
|
|
199
217
|
let t = e.subject.id ? `${e.subject.type}:${e.subject.id}` : e.subject.type, n = e.outcome.kind === "ok" ? "ok" : "error", r = {
|
|
200
218
|
subject: t,
|
|
201
219
|
tenant: e.subject.tenantId ?? null,
|
|
@@ -203,8 +221,8 @@ var y = "_voltro_audit_log", b = [
|
|
|
203
221
|
durationMs: e.outcome.durationMs,
|
|
204
222
|
traceId: e.traceId
|
|
205
223
|
};
|
|
206
|
-
n === "error" ?
|
|
207
|
-
},
|
|
224
|
+
n === "error" ? H.warn(e.tag, r) : H.info(e.tag, r);
|
|
225
|
+
}, Y = (e, n) => typeof e == "function" ? (n) => t.suspend(() => {
|
|
208
226
|
let r = e(n);
|
|
209
227
|
return r === void 0 ? t.void : t.isEffect(r) ? r : t.tryPromise({
|
|
210
228
|
try: () => Promise.resolve(r),
|
|
@@ -216,7 +234,7 @@ var y = "_voltro_audit_log", b = [
|
|
|
216
234
|
try: () => r(e),
|
|
217
235
|
catch: (e) => e
|
|
218
236
|
}) : t.void;
|
|
219
|
-
}) : e === "memory" ? (e) => t.sync(() =>
|
|
237
|
+
}) : e === "memory" ? (e) => t.sync(() => q(e)) : (e) => t.sync(() => J(e)), X = (e) => {
|
|
220
238
|
let t = [], n = [], r = (e) => {
|
|
221
239
|
if (e._tag !== "Empty") {
|
|
222
240
|
if (e._tag === "Fail") {
|
|
@@ -240,42 +258,48 @@ var y = "_voltro_audit_log", b = [
|
|
|
240
258
|
}
|
|
241
259
|
};
|
|
242
260
|
return r(e), t[0] ?? n[0] ?? e;
|
|
243
|
-
},
|
|
261
|
+
}, Z = "@voltro/plugin-audit", Q = (e = {}) => {
|
|
244
262
|
let i = e.sink === "datastore";
|
|
245
263
|
i && u({
|
|
246
264
|
source: "plugin",
|
|
247
|
-
table:
|
|
265
|
+
table: E,
|
|
248
266
|
timeColumn: "at",
|
|
249
267
|
ttlMs: f(process.env.VOLTRO_AUDIT_LOG_TTL_HOURS, 24 * 365)
|
|
250
268
|
});
|
|
251
|
-
let a, o =
|
|
269
|
+
let a, o = Y(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) => {
|
|
252
270
|
let n = e.record ?? "all";
|
|
253
271
|
return n === "all" ? !0 : n === "errors" ? t.outcome.kind === "error" : n(t);
|
|
254
272
|
}, m = { __redacted: "all" }, h = (t) => {
|
|
255
273
|
let n = e.redactInput ?? "all";
|
|
256
|
-
return n === "none" ? t : {
|
|
274
|
+
return n === "none" ? t : n === "all" ? {
|
|
257
275
|
...t,
|
|
258
|
-
input:
|
|
276
|
+
input: m
|
|
277
|
+
} : n === "shape" ? {
|
|
278
|
+
...t,
|
|
279
|
+
input: T(t.input)
|
|
280
|
+
} : {
|
|
281
|
+
...t,
|
|
282
|
+
input: n(t)
|
|
259
283
|
};
|
|
260
284
|
}, g = (t) => {
|
|
261
285
|
let n = e.redactOutcome ?? "all";
|
|
262
286
|
if (n === "none") return t;
|
|
263
|
-
let r = n === "all" ? m : n(
|
|
264
|
-
if (
|
|
287
|
+
let r = t.outcome, i = n === "all" ? m : n === "shape" ? ((e) => T(e))(r.kind === "ok" ? r.value : r.error) : n(t);
|
|
288
|
+
if (r.kind === "ok") return {
|
|
265
289
|
...t,
|
|
266
290
|
outcome: {
|
|
267
|
-
...
|
|
268
|
-
value:
|
|
291
|
+
...r,
|
|
292
|
+
value: i
|
|
269
293
|
}
|
|
270
294
|
};
|
|
271
|
-
let a =
|
|
295
|
+
let a = r.error, o = typeof a == "object" && a ? a._tag : void 0;
|
|
272
296
|
return {
|
|
273
297
|
...t,
|
|
274
298
|
outcome: {
|
|
275
|
-
...
|
|
276
|
-
error: o === void 0 ?
|
|
299
|
+
...r,
|
|
300
|
+
error: o === void 0 ? i : {
|
|
277
301
|
_tag: o,
|
|
278
|
-
...
|
|
302
|
+
...i
|
|
279
303
|
}
|
|
280
304
|
}
|
|
281
305
|
};
|
|
@@ -288,22 +312,22 @@ var y = "_voltro_audit_log", b = [
|
|
|
288
312
|
};
|
|
289
313
|
let r = t.subject;
|
|
290
314
|
if (r.metadata === void 0) return t;
|
|
291
|
-
let { metadata: i, ...a } = r;
|
|
315
|
+
let { metadata: i, ...a } = r, o = n === "metadata-shape" ? T(i) : m;
|
|
292
316
|
return {
|
|
293
317
|
...t,
|
|
294
318
|
subject: {
|
|
295
319
|
...a,
|
|
296
|
-
metadata:
|
|
320
|
+
metadata: o
|
|
297
321
|
}
|
|
298
322
|
};
|
|
299
|
-
}, v = (e) => p(e) ? o(g(_(h(e)))) : t.void,
|
|
323
|
+
}, v = (e) => p(e) ? o(g(_(h(e)))) : t.void, y = (t) => {
|
|
300
324
|
if (e.resolveScope !== void 0) try {
|
|
301
325
|
return e.resolveScope(t);
|
|
302
326
|
} catch {
|
|
303
327
|
return;
|
|
304
328
|
}
|
|
305
|
-
},
|
|
306
|
-
let r = Date.now(), i =
|
|
329
|
+
}, b = (e, n) => d(n.tag) ? t.suspend(() => {
|
|
330
|
+
let r = Date.now(), i = y(n);
|
|
307
331
|
return e.pipe(t.tap((e) => v({
|
|
308
332
|
ts: r,
|
|
309
333
|
tag: n.tag,
|
|
@@ -325,17 +349,17 @@ var y = "_voltro_audit_log", b = [
|
|
|
325
349
|
input: n.input,
|
|
326
350
|
outcome: {
|
|
327
351
|
kind: "error",
|
|
328
|
-
error:
|
|
352
|
+
error: X(e),
|
|
329
353
|
durationMs: Date.now() - r
|
|
330
354
|
}
|
|
331
355
|
}).pipe(t.catchAllCause(() => t.void))));
|
|
332
|
-
}) : e,
|
|
356
|
+
}) : e, x = b, S = b, C = b;
|
|
333
357
|
return n({
|
|
334
358
|
name: r({
|
|
335
|
-
base:
|
|
359
|
+
base: Z,
|
|
336
360
|
alias: e.alias
|
|
337
361
|
}),
|
|
338
|
-
baseName:
|
|
362
|
+
baseName: Z,
|
|
339
363
|
description: "Records every mutation invocation; ships an audit() schema mixin for row-level metadata.",
|
|
340
364
|
permissions: [
|
|
341
365
|
"rpc:intercept:mutation",
|
|
@@ -344,15 +368,15 @@ var y = "_voltro_audit_log", b = [
|
|
|
344
368
|
...e.recordQueries === !0 ? ["rpc:intercept:query"] : []
|
|
345
369
|
],
|
|
346
370
|
...i ? {
|
|
347
|
-
...e.tables === !1 ? {} : { extendSchema: { tables:
|
|
371
|
+
...e.tables === !1 ? {} : { extendSchema: { tables: I } },
|
|
348
372
|
bindDataStore: (e) => {
|
|
349
|
-
a =
|
|
373
|
+
a = V(e);
|
|
350
374
|
}
|
|
351
375
|
} : {},
|
|
352
|
-
interceptMutation:
|
|
353
|
-
interceptAction:
|
|
354
|
-
...e.recordQueries === !0 ? { interceptQuery:
|
|
376
|
+
interceptMutation: x,
|
|
377
|
+
interceptAction: S,
|
|
378
|
+
...e.recordQueries === !0 ? { interceptQuery: C } : {}
|
|
355
379
|
});
|
|
356
380
|
};
|
|
357
381
|
//#endregion
|
|
358
|
-
export {
|
|
382
|
+
export { E as AUDIT_LOG_TABLE, e as audit, B as auditBySubject, z as auditByTrace, L as auditEventToRow, F as auditLogTable, I as auditLogTables, Q as auditPlugin, M as auditRowHash, K as clearAuditBuffer, V as dataStoreAuditSink, N as makeAuditChain, G as readAuditBuffer, P as verifyAuditChain };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@voltro/plugin-audit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.37.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.37.0",
|
|
42
|
+
"@voltro/logger": "0.37.0",
|
|
43
|
+
"@voltro/protocol": "0.37.0"
|
|
44
44
|
},
|
|
45
45
|
"peerDependencies": {
|
|
46
46
|
"effect": "^3.22.0"
|