@voltro/plugin-audit 0.31.0 → 0.32.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 CHANGED
@@ -39,6 +39,169 @@ _Changes staged for the next release accumulate here (rolled up from
39
39
 
40
40
  ---
41
41
 
42
+ ## [0.32.0] — 2026-08-10
43
+
44
+ ### ⚠ BREAKING
45
+
46
+ - **@voltro/plugin-audit** — **`auditPlugin` stored what a call RETURNED, verbatim, with no option to reach it. `redactOutcome` now exists and defaults to `'all'`.**
47
+
48
+ Found on the first run of `voltro db scan-credentials` after we widened its columns, against a real database:
49
+
50
+ ```
51
+ ✗ _voltro_audit_log.outcome — 9 of 263 row(s) match a credential-shaped key
52
+ ```
53
+
54
+ Four were `webhooks.create` rows carrying a live 64-character `signingSecret` in full. The reporter had BOTH existing options on — `redactInput: 'all'`, `redactSubject: 'metadata'` — and there was no third to reach this field with.
55
+
56
+ **The option that existed covers the field these calls leave empty.** `redactInput`'s own docstring names "an API key at issuance" as its motivating case, and for a credential-ISSUING call the secret is never in the input:
57
+
58
+ ```ts
59
+ apiKeys.createPersonalApiKey({ name, scopes }) // input: nothing sensitive
60
+ → { keyValue: '<the plaintext key>' } // outcome: the whole point
61
+ webhooks.create({ url, subscribedEvents }) // input: nothing sensitive
62
+ → { signingSecret: '<live secret>' } // outcome: returned once, by design
63
+ ```
64
+
65
+ MIGRATION: `outcome.value` and `outcome.error` become `{ __redacted: 'all' }`. The error's `_tag` SURVIVES — a trail recording "something failed" without saying what is not a trail, and a tag is a schema-declared discriminant that structurally cannot be a secret. `kind` and `durationMs` are untouched, the caller still receives the real result, and a `record` predicate still sees the live outcome. Set `redactOutcome: 'none'` to keep the old behaviour.
66
+
67
+ Also documented: a FUNCTION sink gets neither the `_voltro_audit_log` table nor the retention sweep — both are gated on `sink` being the literal `'datastore'`. A function that redacts and delegates to `dataStoreAuditSink` still writes rows on a database where the table exists, while creating it nowhere and arming the TTL nowhere. It works where you tested it and fails on the next fresh database. `redactOutcome` removes the reason to reach for that composition; the docstring now names the cliff for anyone who reaches for it anyway.
68
+
69
+ **`voltro update` carries you across this** — codemod `0.32.0/01_audit-redacts-outcome`.
70
+
71
+ ### Added
72
+
73
+ - **@voltro/cli** — **`voltro doctor` said nothing when the unknown-scope rule did not run, which read exactly like a clean result.**
74
+
75
+ The rule needs a declared scope vocabulary — with `@voltro/plugin-rbac` that is the union of its `roles` map. An app doing RBAC without the plugin (roles as plain literal arrays in `lib/teamRoles.ts`) publishes none, so the rule stays quiet. A consumer confirmed it empirically: doctor reports **0** unknown-scope findings on the tree that contains the exact bug the rule was built for — `webhooks.test` guarded by `webhooks:test` while no role grants it. Silence read as coverage.
76
+
77
+ Two things now:
78
+
79
+ - **The dormant state SAYS it is dormant**, in the human report and in `--json` (`evaluated: false`, which used to be an empty findings array indistinguishable from a clean app). It ends with the sentence that matters: this section is not a clean bill of health. - **An app can point the rule at its own vocabulary**, which the consumer proposed and which doctor now reads:
80
+
81
+ ```ts
82
+ // app.config.ts
83
+ doctor: { scopeVocabulary: './lib/teamRoles.ts#ALL_TEAM_SCOPES' }
84
+ ```
85
+
86
+ Deliberately a plain read of an exported string array. Anything cleverer (evaluating a roles map, following a builder) fails differently per app and lands back at "quiet for reasons you cannot see".
87
+
88
+ ### Changed
89
+
90
+ - **@voltro/cli** — **`voltro db scan-credentials` now says its name set is a heuristic, so a clean run stops reading as an all-clear.**
91
+
92
+ Reported with the case that proves it: on the same table, in the same column, the scan found `signingSecret` and missed `keyValue` — the plaintext API key an `apiKeys.*` mutation returns. `keyValue` matches none of the needles and never will; no name list covers every convention an app can invent.
93
+
94
+ Not a defect, a heuristic being a heuristic — and the reporter's framing is the fix: *"one line in the output saying the name set IS a heuristic would stop a clean run reading as an all-clear."* The report ends with what a clean result actually means: no credential-SHAPED key found, which is not the same as no credential.
95
+ - **@voltro/cli** — **`voltro db scan-credentials` reported a hit count without saying which key matched, under advice that could not be carried out or argued with.**
96
+
97
+ The output was `69 of 149 row(s) match a credential-shaped key` followed by `Purge them AND rotate the credentials`. Purge what? Rotate which credential? And a column mentioning the word `token` in prose reads identically to one holding a live one — the reporter had both kinds in the same column and no way to separate them from the output.
98
+
99
+ Each hit line now names the needles and their row counts — which OVERLAP and
100
+ do not sum to the hit count, because a row holding both a token and a secret is
101
+ counted by both, and two numbers printed under a total otherwise invite being
102
+ added up:
103
+
104
+ ```
105
+ ✗ _voltro_audit_log.outcome — 69 of 149 row(s) match a credential-shaped key
106
+ matched (rows per needle, may overlap): token (61), secret (12)
107
+ ```
108
+
109
+ One extra COUNT per needle, taken only for a target that already matched — so the cost lands exactly where somebody is about to do work and nowhere else.
110
+
111
+ ### Fixed
112
+
113
+ - **@voltro/cli** — **`voltro update --dry-run` could never preview a jump's codemods — the manifest it reads has never been published.** Checked against the registry rather than inferred: `@voltro/cli` at 0.25.0, 0.29.0, 0.30.2 and 0.31.0 all ship no `voltro` field at all, while the repo's own `package.json` carries 65 entries.
114
+
115
+ `scripts/prepare-publish.mjs`'s `cleanManifest` builds a fresh publish manifest field by field rather than deleting from a copy — an allowlist by construction — so a new top-level key is dropped in silence and nothing downstream mentions it.
116
+
117
+ **What this did NOT cost, stated because the alarming reading is the wrong one:** no user has ever missed a codemod that should have run. The manifest feeds the PREVIEW only; the actual run happens after the install, out of the target CLI's own registry, which is present by then. And the missing case was already handled loudly — the preview printed *"could not preview … This is NOT the same as 'no codemods'"* rather than an confident "none". An honest "I could not look" for four releases, where the feature was built to look.
118
+
119
+ The fix carries `voltro` through, and the guard that goes with it is the part worth keeping: `codemodManifest.test.ts` asserts the REPO's package.json carries every codemod, and it was green on every one of those releases. A source-reading guard cannot see what the publish pipeline does downstream of it. So the assertion is made against the STAGED file, read back off disk after it is written, and it runs inside `prepare-publish` itself — which the gate's `Pack + verify` step already invokes, so it needs no separate wiring.
120
+
121
+ Red-verified by removing the one-line fix and re-running: the staged manifest comes back with NO entries against the source's 65, and publishing aborts.
122
+ - **@voltro/cli** — **`voltro dev` read the COMPILED config, so editing `app.config.ts` had no effect in any app that had ever run `voltro build`.**
123
+
124
+ `loadConfig` preferred `.framework/dist/server/appConfig.js` whenever it existed — a build output, i.e. whatever the config said the last time somebody built. The comment on that branch said the `.ts` source is the fallback "for dev", but the condition was `existsSync`, which cannot know which command is running.
125
+
126
+ Measured on the fixture: `app.config.ts` set to `locales: ['de','en'], defaultLocale: 'de'`, `voltro dev` booted, SSR response `<html lang="en">` — the value from a build hours earlier. Neither the declared default nor the first declared locale reached the render.
127
+
128
+ Found while reproducing a consumer's report that a declared `locales:` did not affect `<html lang>` in dev. An app that has never built is unaffected, which is why this survived.
129
+
130
+ **And the second half of the same report: dev never negotiated `Accept-Language` in resolver-only mode.** `makeSsrI18nResolver` returns `() => undefined` for an empty wrap set — right for the PROVIDER, wrong for the locale — so the caller fell through to `cookie ?? defaultLocale`. `voltro build` had already grown the resolver-only runtime; dev had not. The same dev/build drift as the release before, one release later, in the opposite direction.
131
+
132
+ Measured after the fix, all three signals: no headers → the declared default; `Accept-Language: en` → `en`; cookie beats `Accept-Language`. `ssrIntlDiagnostic` is gated on a REAL provider now, so resolver-only mode does not claim one was supplied.
133
+ - **@voltro/cli** — **The duplicate-version check read the pnpm store, so it fired for everyone on the release right after they upgraded.**
134
+
135
+ Last release taught it to see copies a walk from the app root cannot reach — a second version pulled in by a SIBLING workspace package. It did that by reading the pnpm virtual store directly, and the store also holds every version pnpm ever unpacked. On a real tree, one release later:
136
+
137
+ ```
138
+ @voltro/cli — 0.29.0, 0.30.0, 0.30.1, 0.30.2, 0.31.0
139
+ ```
140
+
141
+ …while EVERY `@voltro/*` in that workspace was linked at exactly 0.31.0. The extras were residue: nothing links them, `pnpm store prune` removes them, they cannot be loaded. The reporter's summary is the right one — *"the rule moved from blind-to-the-real-case to noisy-on-every-upgrade, and both endings are the same: the reader stops looking."* This ending is the worse of the two, because it fires for everyone at exactly the moment they are reading the output.
142
+
143
+ **A package manager's cache is not a statement about the program.** It counts what is LINKED now: one `package.json` per (workspace package × framework package), derived from `pnpm-workspace.yaml` or the conventional `packages/*` / `apps/*` layout. That is cheaper than the store walk it replaces, free of residue, and still finds the sibling case it was widened for.
144
+ - **@voltro/cli** — **The declared-event scan searched for the event NAME, so it missed every app following the typed path — 19 findings, 0 real.**
145
+
146
+ `emit` is typed `(event: string | OutgoingEventDescriptor<P> | DeclaredEventLike, …)`, and the DESCRIPTOR overload is the type-safe one — the path the schema-decode behaviour rewards. An app on it writes `emitEvent(ctx, apiKeyCreated, …)` and never repeats the name string anywhere near the emit.
147
+
148
+ So the rule found apps that emit by string literal and missed apps that emit by descriptor, which is backwards: the second group is the one doing it properly. The ADVISORY caveat we shipped was exact and useless — it said "an emit through a variable will read as missing here", and the variable IS the idiomatic call.
149
+
150
+ It resolves the binding now. The exported const and the `name:` are in ONE statement (`export const apiKeyCreated = defineEvent({ name: 'apiKey.created' … })`), so this is a local read of the declaring file rather than a resolution. Both spellings count as an emit. The caveat is narrowed to what actually remains invisible: a name assembled at runtime.
151
+
152
+ Reported with a working reference implementation attached, which is where the three lines came from.
153
+ - **@voltro/sql-postgres** — **A query with no free pooled connection waited forever, with no error and no log line.**
154
+
155
+ `new pg.Pool({...})` was constructed without `connectionTimeoutMillis`, and node-postgres defaults it to `0` — wait indefinitely. Reported as: *"ein Query ohne freie Verbindung wartet unbegrenzt, ohne Fehler und ohne Logzeile."*
156
+
157
+ The distinction that makes this worth a default: `statementTimeoutMs` bounds a query the SERVER is running; nothing bounded a query the CLIENT had not sent yet. Those are the two halves of "a request is stuck", and only one was covered.
158
+
159
+ Defaults to 10 s (`DEFAULT_ACQUIRE_TIMEOUT_MS`), overridable per connection with `acquireTimeoutMs`; `0` restores the driver's unbounded wait. A bounded failure is more useful than an unbounded wait even when the pool would have freed up: it names the pool as the cause at the moment it IS the cause, instead of surfacing as unexplained latency somewhere with no connection information in it.
160
+
161
+ **Also documented, from the same report:** with `changeStrategy: 'cdc'` a process needs `maxConnections + 1`. The LISTEN consumer cannot use a pooled connection, so `@effect/sql-pg` opens a standalone `new Pg.Client(pool.options)` that is outside `max` and outside every number derived from it. A per-pod budget built on `maxConnections` is short by exactly one, which surfaces as the last pod of a rollout failing to connect rather than as a pool warning.
162
+ - **@voltro/cli** — **Shipping `whats-new.md` for the right release depended on a human remembering a step between two other steps.**
163
+
164
+ The module is generated from the top CHANGELOG section, and the command that writes a new one (`changelog-release.mjs --release`) did not regenerate it. So whether the published guide described the release you just installed came down to whether someone ran the generator in the window between the roll and the publish.
165
+
166
+ Measured across the published tarballs rather than asserted:
167
+
168
+ | `@voltro/cli` | ships | |---|---| | 0.28.0 | `# What's new in 0.27.0` | | 0.29.0 – 0.31.0 | each names its own version |
169
+
170
+ So it has gone wrong once in the last six, not every time — and the four that are right are right because a human did the step, which is exactly the property being removed here. One in six is not a small number for a module whose whole job is *"read this FIRST when a task touches an area you have not worked in recently"*: on that release it was the one piece of the shipped guide guaranteed to be wrong about the version the reader had just installed. And it fails in the direction that reads as fine — a real version, described correctly, just not theirs.
171
+
172
+ The roll regenerates now, in the same command, rather than as a checklist line: a step a human must remember between two others is the step that gets skipped on the release nobody is watching. A regeneration failure is loud and non-fatal, and `check-whats-new-version.mjs` (which already gates this in static-checks) stays as the backstop for the paths that bypass the roll.
173
+
174
+ **If you are looking at a stale `whats-new.md` in your own project, this is probably not the cause.** The per-project copy is seeded once and never overwritten on boot — it refreshes only on `voltro agents-md --force`. That file being behind is the seeder's documented behaviour, not this defect, and no framework release fixes it for you.
175
+ - **@voltro/cli** — **The retention sweep was armed, announced at every boot, and never ran in a process that restarted more often than every five minutes.**
176
+
177
+ It was `setInterval(sweepAll, 5 * 60_000)` with no initial run, so the first sweep was always five minutes away. `voltro dev` restarts on every file save, and at least one consumer runs `voltro dev` as their deployment: their `_voltro_schedule_claims` reached **86 214 rows / 33 MB** with the policy registered, and the boot printing `retention: N policy(ies) armed — rows older than the TTL are DELETED` every time.
178
+
179
+ That is this file's own lesson one turn further in. We fixed "a standing delete that never introduces itself" and shipped a standing delete that introduces itself and then does not run.
180
+
181
+ The first sweep now fires 30 s after boot, then on the interval. The delay is a compromise with the failures on either side: zero would put a multi-table DELETE in front of the first request of every boot, five minutes is what produced the report. The timer is `unref`'d, so it never holds a process open on its own.
182
+ - **@voltro/cli** — **`voltro dev` reported `engine: "in-memory"` while running a cluster engine, and a consumer built a diagnosis on it.**
183
+
184
+ The line derived its label from `store === 'postgres' ? 'cluster-postgres' : 'in-memory'` — so mariadb, mysql, sqlite and mssql all printed `in-memory` while the block directly above had just built `cluster-sql` for exactly those stores. The boot therefore printed two lines that contradicted each other:
185
+
186
+ ```
187
+ workflow engine: cluster-sql, dialect=mariadb
188
+ workflow engine selected · engine: "in-memory"
189
+ ```
190
+
191
+ A consumer read the second — later, more definitive-sounding — and concluded that `voltro dev` runs workflows on a different engine than a deployment does. That is a fair reading of a line that is simply false, and it sent them down the wrong half of an investigation into why their outgoing webhooks delivered in dev and produced zero rows under `voltro serve`.
192
+
193
+ The label is now decided where the engine is chosen. A log line is an assertion the framework makes about itself, and this one had no reader that could catch it: it is not typed against the layer it describes, and nothing asserted the pair agreed.
194
+
195
+ **Both boot paths also report WHICH workflow entity types they registered.** `API listening … workflowWorkers: 0` counts app-exported worker LAYERS — an advanced surface almost no app uses — and reads as "nothing consumes durable work here", which is the question the consumer could then answer from no other line. A delivery that fails with `Entity type 'Workflow/voltro.deliverWebhook' not registered` and one that never starts look identical from outside; this separates them before anyone reproduces anything.
196
+
197
+ ### Internal (no consumer-facing effect)
198
+
199
+ - **@voltro/database** — The count of `descriptor.order` read sites is ASSERTED rather than stated: the prose said ELEVEN and there are twelve (three in `sqlCompiler.ts`, nine in `jsonEagerCompiler.ts`). Written from memory of a replacement run instead of from the file — the exact mistake two consumer rounds have been about, made inside the document describing it.
200
+
201
+ `queryDescriptorOrderAbsent.test.ts` now pins `{ 'sqlCompiler.ts': 3, 'jsonEagerCompiler.ts': 9 }`, so a compiler growing or losing a read site fails the suite instead of drifting quietly into a number somebody quotes. No behaviour change.
202
+
203
+ ---
204
+
42
205
  ## [0.31.0] — 2026-08-10
43
206
 
44
207
  ### Added
package/dist/index.d.ts CHANGED
@@ -133,6 +133,20 @@ export declare interface AuditPluginOptions {
133
133
  * like — e.g. an `Effect<void>` sink that
134
134
  * writes rows to your own audit table on top
135
135
  * of `@effect/sql`.
136
+ *
137
+ * **A function sink gets NEITHER the table NOR the retention sweep**, and the
138
+ * cliff is invisible until the next environment. Both are gated on `sink`
139
+ * being the literal `'datastore'`, so a function that redacts and then
140
+ * delegates to `dataStoreAuditSink` still writes rows on a database where
141
+ * `_voltro_audit_log` already exists — while creating the table nowhere and
142
+ * arming the TTL policy nowhere. It works where you tested it and fails on
143
+ * the next `voltro dev` against a fresh database.
144
+ *
145
+ * Reported by a consumer who reached for exactly that composition to redact a
146
+ * field, before {@link AuditPluginOptions.redactOutcome} existed. If you want
147
+ * the durable trail with different redaction, use `sink: 'datastore'` plus
148
+ * `redactInput` / `redactSubject` / `redactOutcome` — those compose; the sink
149
+ * does not.
136
150
  */
137
151
  readonly sink?: 'console' | 'memory' | 'datastore' | AuditSink;
138
152
  /**
@@ -276,6 +290,45 @@ export declare interface AuditPluginOptions {
276
290
  * the plugin can compute.
277
291
  */
278
292
  readonly redactSubject?: 'metadata' | 'none' | ((subject: Subject) => unknown);
293
+ /**
294
+ * What happens to `AuditEvent.outcome`'s payload before it is handed to the
295
+ * sink.
296
+ *
297
+ * - `'all'` (DEFAULT) — `outcome.value` on success, and `outcome.error` on
298
+ * failure, are replaced by `{ __redacted: 'all' }`. `kind`, `durationMs`
299
+ * and the error's TAG survive, which is what the trail is read for.
300
+ * - `'none'` — the outcome verbatim. What every sink did before this
301
+ * option existed.
302
+ * - a function — `(event) => unknown`, for field-level control.
303
+ *
304
+ * **This is `redactInput`'s reasoning applied to the field it structurally
305
+ * cannot cover.** `redactInput`'s own docstring names "an API key at issuance"
306
+ * as its motivating case — and for a credential-ISSUING call the secret is
307
+ * never in the input:
308
+ *
309
+ * apiKeys.createPersonalApiKey({ name, scopes }) // input: nothing sensitive
310
+ * → { keyValue: '<the plaintext key>' } // outcome: the whole point
311
+ *
312
+ * webhooks.create({ url, subscribedEvents }) // input: nothing sensitive
313
+ * → { signingSecret: '<live secret>' } // outcome: returned once
314
+ *
315
+ * The option that existed covered the field those calls leave empty; the
316
+ * field they fill had none. Found by a consumer on the first run of
317
+ * `voltro db scan-credentials` after we widened its columns: nine rows of
318
+ * `_voltro_audit_log.outcome` matched, four of them `webhooks.create` carrying
319
+ * a live 64-character signing secret in full. They had BOTH existing options
320
+ * on — `redactInput: 'all'`, `redactSubject: 'metadata'` — and no way to
321
+ * reach this field.
322
+ *
323
+ * The default is `'all'` for the same reason the other two default to
324
+ * redacting: losing the payload is visible the first time you read a row,
325
+ * leaking a credential is not visible at all. Opt out per app once you know
326
+ * your own outcomes.
327
+ *
328
+ * A `record` predicate still sees the LIVE outcome, so a filter that keys on
329
+ * the result keeps working — redaction applies to what is STORED.
330
+ */
331
+ readonly redactOutcome?: 'all' | 'none' | ((event: AuditEvent) => unknown);
279
332
  }
280
333
 
281
334
  /** The narrow read surface the entry points need. */
package/dist/index.js CHANGED
@@ -137,6 +137,28 @@ var m = "_voltro_audit_log", h = d(m, {
137
137
  input: n === "all" ? l : n(t)
138
138
  };
139
139
  }, f = (t) => {
140
+ let n = e.redactOutcome ?? "all";
141
+ if (n === "none") return t;
142
+ let r = n === "all" ? l : n(t), i = t.outcome;
143
+ if (i.kind === "ok") return {
144
+ ...t,
145
+ outcome: {
146
+ ...i,
147
+ value: r
148
+ }
149
+ };
150
+ let a = i.error, o = typeof a == "object" && a ? a._tag : void 0;
151
+ return {
152
+ ...t,
153
+ outcome: {
154
+ ...i,
155
+ error: o === void 0 ? r : {
156
+ _tag: o,
157
+ ...r
158
+ }
159
+ }
160
+ };
161
+ }, p = (t) => {
140
162
  let n = e.redactSubject ?? "metadata";
141
163
  if (n === "none") return t;
142
164
  if (typeof n == "function") return {
@@ -153,15 +175,15 @@ var m = "_voltro_audit_log", h = d(m, {
153
175
  metadata: l
154
176
  }
155
177
  };
156
- }, p = (e) => s(e) ? a(f(d(e))) : t.void, h = (t) => {
178
+ }, h = (e) => s(e) ? a(f(p(d(e)))) : t.void, _ = (t) => {
157
179
  if (e.resolveScope !== void 0) try {
158
180
  return e.resolveScope(t);
159
181
  } catch {
160
182
  return;
161
183
  }
162
- }, _ = (e, n) => o(n.tag) ? t.suspend(() => {
163
- let r = Date.now(), i = h(n);
164
- return e.pipe(t.tap((e) => p({
184
+ }, v = (e, n) => o(n.tag) ? t.suspend(() => {
185
+ let r = Date.now(), i = _(n);
186
+ return e.pipe(t.tap((e) => h({
165
187
  ts: r,
166
188
  tag: n.tag,
167
189
  subject: n.subject,
@@ -173,7 +195,7 @@ var m = "_voltro_audit_log", h = d(m, {
173
195
  value: e,
174
196
  durationMs: Date.now() - r
175
197
  }
176
- }).pipe(t.catchAllCause(() => t.void))), t.tapErrorCause((e) => p({
198
+ }).pipe(t.catchAllCause(() => t.void))), t.tapErrorCause((e) => h({
177
199
  ts: r,
178
200
  tag: n.tag,
179
201
  subject: n.subject,
@@ -186,7 +208,7 @@ var m = "_voltro_audit_log", h = d(m, {
186
208
  durationMs: Date.now() - r
187
209
  }
188
210
  }).pipe(t.catchAllCause(() => t.void))));
189
- }) : e, v = _, y = _, b = _;
211
+ }) : e, y = v, b = v, S = v;
190
212
  return n({
191
213
  name: "@voltro/plugin-audit",
192
214
  description: "Records every mutation invocation; ships an audit() schema mixin for row-level metadata.",
@@ -202,9 +224,9 @@ var m = "_voltro_audit_log", h = d(m, {
202
224
  i = x(e);
203
225
  }
204
226
  } : {},
205
- interceptMutation: v,
206
- interceptAction: y,
207
- ...e.recordQueries === !0 ? { interceptQuery: b } : {}
227
+ interceptMutation: y,
228
+ interceptAction: b,
229
+ ...e.recordQueries === !0 ? { interceptQuery: S } : {}
208
230
  });
209
231
  };
210
232
  //#endregion
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@voltro/plugin-audit",
3
- "version": "0.31.0",
3
+ "version": "0.32.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",
@@ -37,9 +37,9 @@
37
37
  "node": ">=24.0.0"
38
38
  },
39
39
  "dependencies": {
40
- "@voltro/database": "0.31.0",
41
- "@voltro/logger": "0.31.0",
42
- "@voltro/protocol": "0.31.0"
40
+ "@voltro/database": "0.32.0",
41
+ "@voltro/logger": "0.32.0",
42
+ "@voltro/protocol": "0.32.0"
43
43
  },
44
44
  "peerDependencies": {
45
45
  "effect": "^3.22.0"