@voltro/cli 0.35.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.
Files changed (109) hide show
  1. package/CHANGELOG.md +158 -0
  2. package/dist/apiBuild-D0f6hYbH.js +2 -0
  3. package/dist/{apiBuild-DYD_ONLD.js → apiBuild-DGsShRuF.js} +1 -1
  4. package/dist/{baselineCommand-DIttzO8A.js → baselineCommand-C6NMt-oa.js} +1 -4
  5. package/dist/baselineCommand-Cfw2Afwm.js +2 -0
  6. package/dist/bin.js +1 -1
  7. package/dist/{build-CD8K4XOr.js → build-CiFA-E19.js} +7 -7
  8. package/dist/{checkCommand-DKpDLlqu.js → checkCommand-CIPYyNyc.js} +1 -1
  9. package/dist/{checkCommand-Ct9xkTrS.js → checkCommand-CLPTzYqM.js} +3 -3
  10. package/dist/{cloudCmd-Cvv5HGaZ.js → cloudCmd-NQSwe_Qk.js} +1 -1
  11. package/dist/{codegen-CYM3Zqrf.js → codegen-GYYcdpCg.js} +2 -2
  12. package/dist/codegen-uYrrDzQv.js +2 -0
  13. package/dist/{codegenCommand-C4YoQIc2.js → codegenCommand-BxS3sCeJ.js} +2 -2
  14. package/dist/{codemodRunner-BnFq3Fgu.js → codemodRunner-cyqu5oxr.js} +122 -46
  15. package/dist/{commands-BE8E7zF3.js → commands-BwG9A9Vv.js} +73 -72
  16. package/dist/{dashboardCommand-D7SgZGaN.js → dashboardCommand-C7U146xQ.js} +1 -1
  17. package/dist/{dataCommand-BhYwDgg-.js → dataCommand-BOwd8_aJ.js} +2 -2
  18. package/dist/{dbCommand-O8HA63s2.js → dbCommand-C4UW8cVN.js} +353 -351
  19. package/dist/dbCommand-CC-qNzpz.js +2 -0
  20. package/dist/{dev-D2BikO7a.js → dev-BV6XNYKf.js} +1 -1
  21. package/dist/{dev-C7sFZq3m.js → dev-bwDgSObH.js} +2000 -2000
  22. package/dist/{doctorCommand-DnimF5IM.js → doctorCommand-BXyrtN2l.js} +9 -9
  23. package/dist/doctorCommand-C4L61G_L.js +2 -0
  24. package/dist/{dormancyCommand-QewYug_s.js → dormancyCommand-BDdRRLsa.js} +1 -1
  25. package/dist/{embeddingsCommand-BfiLS_QI.js → embeddingsCommand-CjWGcAE6.js} +1 -1
  26. package/dist/{envCommand-CCGPRQY1.js → envCommand-B_TeG297.js} +1 -1
  27. package/dist/{evolveCommand-CHsLCtDf.js → evolveCommand-D6N-WfA4.js} +2 -2
  28. package/dist/index.js +2 -2
  29. package/dist/{infoCommand-DwOgK1t6.js → infoCommand-BHKTprrx.js} +1 -1
  30. package/dist/inspectGateHint-BF6608UT.js +4 -0
  31. package/dist/{metaCommands-7MJfZ5cf.js → metaCommands-CIoQNlaQ.js} +1 -1
  32. package/dist/{migrate-BV7I-ZHZ.js → migrate-RwgUWXfc.js} +1 -1
  33. package/dist/{privacyCommand-C-Df56U_.js → privacyCommand-XejDMvmu.js} +4 -3
  34. package/dist/{probeCommand-CZfaaUOZ.js → probeCommand-BPEpwT32.js} +40 -39
  35. package/dist/{projectScaffold-DmzEKHib.js → projectScaffold-CxgtJvlb.js} +1 -1
  36. package/dist/{projectScaffold-LMMtaavR.js → projectScaffold-DDgWbNLe.js} +2 -2
  37. package/dist/{runtimeTrace-CH3eUiMw.js → runtimeTrace-WYV7qYvz.js} +1 -1
  38. package/dist/{sdkgen-BLkvGRfX.js → sdkgen-TM5sSdf7.js} +1 -1
  39. package/dist/{serveCommand-iwlUBNS1.js → serveCommand-CyZsXL1l.js} +487 -487
  40. package/dist/serveCommand-qumm7jdF.js +2 -0
  41. package/dist/serveEntry.js +1 -1
  42. package/dist/subcommandNames-CKG5Dz3a.js +27 -0
  43. package/dist/{templates-De8IR5-c.js → templates-BR-fb4SP.js} +1 -4
  44. package/dist/{test-CI6iDsYc.js → test-DIQ0jlkQ.js} +1 -1
  45. package/dist/{updateCommand-x0pI_x-B.js → updateCommand-9GHHsmDf.js} +1 -1
  46. package/dist/updateCommand-BN0tkl9-.js +2 -0
  47. package/dist/{webhooksCommand-DlAgS2Iw.js → webhooksCommand-CHWSPk5z.js} +1 -1
  48. package/package.json +29 -17
  49. package/templates/AGENTS.md +1 -1
  50. package/templates/agent-docs/_index.md +1 -1
  51. package/templates/agent-docs/cli.md +38 -0
  52. package/templates/agent-docs/data.md +30 -0
  53. package/templates/agent-docs/database/misc.md +33 -0
  54. package/templates/agent-docs/plugins/audit.md +46 -0
  55. package/templates/agent-docs/whats-new.md +44 -121
  56. package/templates/apps/api-ai/package.json +7 -7
  57. package/templates/apps/api-auth/package.json +8 -8
  58. package/templates/apps/api-backend/package.json +7 -7
  59. package/templates/apps/api-backend-deactivation/package.json +7 -7
  60. package/templates/apps/api-backend-mail/package.json +8 -8
  61. package/templates/apps/api-backend-mariadb/package.json +9 -9
  62. package/templates/apps/api-backend-sqlite/package.json +8 -8
  63. package/templates/apps/api-backend-storage/package.json +8 -8
  64. package/templates/apps/api-cms/package.json +10 -10
  65. package/templates/apps/api-collab/package.json +8 -8
  66. package/templates/apps/api-data-advanced/package.json +8 -8
  67. package/templates/apps/api-durable/package.json +8 -8
  68. package/templates/apps/api-feature-flags/package.json +9 -9
  69. package/templates/apps/api-governance/package.json +8 -8
  70. package/templates/apps/api-kv/package.json +8 -8
  71. package/templates/apps/api-moderation/package.json +8 -8
  72. package/templates/apps/api-observability/package.json +8 -8
  73. package/templates/apps/api-ratelimit/package.json +8 -8
  74. package/templates/apps/api-rbac/package.json +8 -8
  75. package/templates/apps/api-rest/package.json +7 -7
  76. package/templates/apps/api-saas/package.json +11 -11
  77. package/templates/apps/api-saas-starter/package.json +10 -10
  78. package/templates/apps/api-search/package.json +8 -8
  79. package/templates/apps/api-status/package.json +8 -8
  80. package/templates/apps/api-versioning/package.json +8 -8
  81. package/templates/apps/api-webhooks/package.json +9 -9
  82. package/templates/apps/changelog/package.json +6 -6
  83. package/templates/apps/edge-functions/package.json +2 -2
  84. package/templates/apps/frontend-admin/package.json +8 -8
  85. package/templates/apps/frontend-app/package.json +9 -9
  86. package/templates/apps/frontend-auth/package.json +8 -8
  87. package/templates/apps/frontend-blank/package.json +7 -7
  88. package/templates/apps/frontend-cms/package.json +9 -9
  89. package/templates/apps/frontend-collab/package.json +10 -10
  90. package/templates/apps/frontend-contact/package.json +7 -7
  91. package/templates/apps/frontend-dashboard/package.json +7 -7
  92. package/templates/apps/frontend-docs/package.json +7 -7
  93. package/templates/apps/frontend-i18n/package.json +6 -6
  94. package/templates/apps/frontend-landing/package.json +7 -7
  95. package/templates/apps/frontend-portal/package.json +8 -8
  96. package/templates/apps/frontend-saas/package.json +8 -8
  97. package/templates/apps/frontend-spa/package.json +7 -7
  98. package/templates/apps/frontend-ssr/package.json +7 -7
  99. package/templates/apps/frontend-ssr-api/package.json +8 -8
  100. package/templates/apps/frontend-static-blog/package.json +6 -6
  101. package/templates/apps/frontend-status/package.json +8 -8
  102. package/templates/apps/mobile-app/package.json +3 -3
  103. package/dist/apiBuild-B8aoJvuw.js +0 -2
  104. package/dist/baselineCommand-C2ClWZN3.js +0 -2
  105. package/dist/codegen-ChBi_hVa.js +0 -2
  106. package/dist/dbCommand-DS4b97Is.js +0 -2
  107. package/dist/doctorCommand-CM4Ch9C7.js +0 -2
  108. package/dist/serveCommand-CbDHU6l-.js +0 -2
  109. package/dist/updateCommand-BlMZhWgO.js +0 -2
@@ -0,0 +1,2 @@
1
+ import { n as e } from "./serveCommand-CyZsXL1l.js";
2
+ export { e as runServeDispatch };
@@ -1,5 +1,5 @@
1
1
  import { t as e } from "./loadEnv-D9nEOClM.js";
2
2
  import { r as t } from "./appModuleLoader-C9r9mxZt.js";
3
3
  import { i as n } from "./dialectDriver-CgXnDfec.js";
4
- import { t as r } from "./serveCommand-iwlUBNS1.js";
4
+ import { t as r } from "./serveCommand-CyZsXL1l.js";
5
5
  export { e as loadDotEnv, t as registerAppModules, n as registerDriver, r as runServe };
@@ -0,0 +1,27 @@
1
+ //#region src/subcommandNames.ts
2
+ var e = [
3
+ "plan",
4
+ "apply",
5
+ "branch",
6
+ "plans",
7
+ "drift",
8
+ "squash",
9
+ "restore-snapshot",
10
+ "generate",
11
+ "migrate",
12
+ "rollback",
13
+ "status",
14
+ "files",
15
+ "rollback-file",
16
+ "seed",
17
+ "gc-snapshots",
18
+ "adopt",
19
+ "scan-credentials",
20
+ "encrypt-column"
21
+ ], t = [
22
+ "scope",
23
+ "export",
24
+ "erase"
25
+ ], n = (e) => `<${e.join("|")}>`;
26
+ //#endregion
27
+ export { t as n, n as r, e as t };
@@ -4,10 +4,7 @@ import { fileURLToPath as s } from "node:url";
4
4
  //#region src/templates.ts
5
5
  var c = e(s(import.meta.url)), l = (() => {
6
6
  let n = process.env.VOLTRO_TEMPLATES_DIR;
7
- if (n) {
8
- let e = t(n, "apps");
9
- if (i(e)) return e;
10
- }
7
+ if (n) return t(n, "apps");
11
8
  let a = r(c, "..", "templates", "apps"), o = c;
12
9
  for (let n = 0; n < 12; n++) {
13
10
  for (let e of ["voltro-templates", ".voltro-templates"]) {
@@ -79,7 +79,7 @@ var d = l({ scope: "voltro:test" }), f = (e) => {
79
79
  }, C = async (e) => {
80
80
  let t = m(e);
81
81
  if (!o(t.root)) return console.error(`voltro test: no such directory: ${t.root}`), 1;
82
- let { checkRpcGroupFreshness: n } = await import("./codegen-ChBi_hVa.js"), r = await n(t.root);
82
+ let { checkRpcGroupFreshness: n } = await import("./codegen-uYrrDzQv.js"), r = await n(t.root);
83
83
  if (r.kind === "stale") return console.error(`voltro test: ${r.file} is stale — ${r.reason}.`), console.error(" The tests would run against the previously generated procedure group, pass, and prove nothing about the descriptors you just edited.\n Run `voltro codegen` (or boot `voltro dev` once) and try again."), 1;
84
84
  d.info("running vitest", {
85
85
  root: t.root,
@@ -1,4 +1,4 @@
1
- import { n as e, r as t, t as n } from "./codemodRunner-BnFq3Fgu.js";
1
+ import { n as e, r as t, t as n } from "./codemodRunner-cyqu5oxr.js";
2
2
  import { basename as r, dirname as i, join as a, relative as o, resolve as s } from "node:path";
3
3
  import { existsSync as c, readFileSync as l, readdirSync as u, statSync as d, writeFileSync as f } from "node:fs";
4
4
  import { totalmem as p } from "node:os";
@@ -0,0 +1,2 @@
1
+ import { f as e, p as t } from "./updateCommand-9GHHsmDf.js";
2
+ export { e as runApplyCodemods, t as runUpdateCommand };
@@ -222,7 +222,7 @@ createVerifier({ secret: [process.env.WEBHOOK_SECRET, process.env.WEBHOOK_SECRET
222
222
  ...t === void 0 ? {} : { payload: t }
223
223
  };
224
224
  }, S = u({ scope: "voltro:webhooks" }), C = ["--out", "--name"], w = async (e) => {
225
- let { walk: t, loadDiscovered: n } = await import("./dev-D2BikO7a.js"), { outgoingFromEvents: r } = await import("./webhookDiscovery-CrGAfhIG.js");
225
+ let { walk: t, loadDiscovered: n } = await import("./dev-BV6XNYKf.js"), { outgoingFromEvents: r } = await import("./webhookDiscovery-CrGAfhIG.js");
226
226
  return r((await n(await t(e))).events.map((e) => ({
227
227
  file: e.file,
228
228
  descriptor: e.descriptor
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@voltro/cli",
3
- "version": "0.35.0",
3
+ "version": "0.37.0",
4
4
  "description": "The `voltro` CLI — dev server, codegen, migrations, project scaffolding, agent-docs seeding, and production serve.",
5
5
  "keywords": [
6
6
  "voltro",
@@ -568,6 +568,18 @@
568
568
  "title": "The declared schema no longer reads `CDC` / `VOLTRO_UNDO` / `VOLTRO_TRACING_PERSIST`",
569
569
  "kind": "manual"
570
570
  },
571
+ {
572
+ "version": "0.35.1",
573
+ "id": "0.35.1/01_unauthorized-replaces-session-expired",
574
+ "title": "A 401 is `code: 'unauthorized'`; the vault's own failure is `'credential_unusable'`",
575
+ "kind": "transform"
576
+ },
577
+ {
578
+ "version": "0.37.0",
579
+ "id": "0.37.0/01_procedure-input-rejects-undeclared-fields",
580
+ "title": "a procedure input rejects a field it does not declare (it used to drop it)",
581
+ "kind": "manual"
582
+ },
571
583
  {
572
584
  "version": "0.4.0",
573
585
  "id": "0.4.0/01_rbac-forbidden-to-scopeerror",
@@ -667,22 +679,22 @@
667
679
  "@effect/platform-node": "^0.108.0",
668
680
  "@effect/sql": "^0.52.0",
669
681
  "@effect/workflow": "^0.19.0",
670
- "@voltro/ai": "0.35.0",
671
- "@voltro/cache": "0.35.0",
672
- "@voltro/data-transfer": "0.35.0",
673
- "@voltro/database": "0.35.0",
674
- "@voltro/env": "0.35.0",
675
- "@voltro/kv": "0.35.0",
676
- "@voltro/logger": "0.35.0",
677
- "@voltro/plugin-auth": "0.35.0",
678
- "@voltro/plugin-broadcast": "0.35.0",
679
- "@voltro/plugin-mail": "0.35.0",
680
- "@voltro/plugin-storage": "0.35.0",
681
- "@voltro/plugin-webhooks": "0.35.0",
682
- "@voltro/protocol": "0.35.0",
683
- "@voltro/runtime": "0.35.0",
684
- "@voltro/serverless": "0.35.0",
685
- "@voltro/workflow": "0.35.0",
682
+ "@voltro/ai": "0.37.0",
683
+ "@voltro/cache": "0.37.0",
684
+ "@voltro/data-transfer": "0.37.0",
685
+ "@voltro/database": "0.37.0",
686
+ "@voltro/env": "0.37.0",
687
+ "@voltro/kv": "0.37.0",
688
+ "@voltro/logger": "0.37.0",
689
+ "@voltro/plugin-auth": "0.37.0",
690
+ "@voltro/plugin-broadcast": "0.37.0",
691
+ "@voltro/plugin-mail": "0.37.0",
692
+ "@voltro/plugin-storage": "0.37.0",
693
+ "@voltro/plugin-webhooks": "0.37.0",
694
+ "@voltro/protocol": "0.37.0",
695
+ "@voltro/runtime": "0.37.0",
696
+ "@voltro/serverless": "0.37.0",
697
+ "@voltro/workflow": "0.37.0",
686
698
  "chokidar": "^5.0.0",
687
699
  "ioredis": "^5.11.1",
688
700
  "tinyglobby": "^0.2.17",
@@ -707,7 +707,7 @@ each plugin's own README.
707
707
 
708
708
  | Topic | Open | Summary |
709
709
  |---|---|---|
710
- | **What's new in 0.35.0** | `node_modules/@voltro/cli/templates/agent-docs/whats-new.md` | Everything that changed in this version. Read it before hand-rolling something the framework may now ship. |
710
+ | **What's new in 0.37.0** | `node_modules/@voltro/cli/templates/agent-docs/whats-new.md` | Everything that changed in this version. Read it before hand-rolling something the framework may now ship. |
711
711
  | AI | `node_modules/@voltro/cli/templates/agent-docs/ai.md` | How Voltro treats AI — agents, tools, streaming, RAG — all primitives over the same WebSocket as the rest of the framework. |
712
712
  | Authentication | `node_modules/@voltro/cli/templates/agent-docs/authentication.md` | How @voltro/plugin-auth wires password + session-cookie auth across api + web, plus the pluggable identity-strategy protocol. |
713
713
  | Caching | `node_modules/@voltro/cli/templates/agent-docs/caching.md` | Voltro's caching layer (@voltro/cache) — an always-on memory default, swappable Redis-compatible backends, a low-level wrap primitive, and automatic query-result invalidation. |
@@ -9,7 +9,7 @@ each plugin's own README.
9
9
 
10
10
  | Topic | Open | Summary |
11
11
  |---|---|---|
12
- | **What's new in 0.35.0** | `node_modules/@voltro/cli/templates/agent-docs/whats-new.md` | Everything that changed in this version. Read it before hand-rolling something the framework may now ship. |
12
+ | **What's new in 0.37.0** | `node_modules/@voltro/cli/templates/agent-docs/whats-new.md` | Everything that changed in this version. Read it before hand-rolling something the framework may now ship. |
13
13
  | AI | `node_modules/@voltro/cli/templates/agent-docs/ai.md` | How Voltro treats AI — agents, tools, streaming, RAG — all primitives over the same WebSocket as the rest of the framework. |
14
14
  | Authentication | `node_modules/@voltro/cli/templates/agent-docs/authentication.md` | How @voltro/plugin-auth wires password + session-cookie auth across api + web, plus the pluggable identity-strategy protocol. |
15
15
  | Caching | `node_modules/@voltro/cli/templates/agent-docs/caching.md` | Voltro's caching layer (@voltro/cache) — an always-on memory default, swappable Redis-compatible backends, a low-level wrap primitive, and automatic query-result invalidation. |
@@ -173,6 +173,22 @@ voltro db apply # execute it (dev)
173
173
  # `voltro migrate` is an alias of `db apply` — same differ, shorter name (CI / ops)
174
174
  ```
175
175
 
176
+ ### "I want to encrypt a column that already has rows in it"
177
+
178
+ Turning on `.encrypted()` needs two things: the cipher, and a migration of the
179
+ rows that are already there. The second has a command — it is easy to miss,
180
+ because nothing about a schema change suggests a data pass is owed.
181
+
182
+ ```bash
183
+ voltro db scan-credentials # find plaintext secrets, column by column
184
+ voltro db encrypt-column users.apiToken --dry-run
185
+ voltro db encrypt-column users.apiToken --yes
186
+ ```
187
+
188
+ `--key-env` names the key variable if it is not the default; the key must be the
189
+ SAME one the app runs with, or the rows come back undecryptable. `voltro db`
190
+ with no subcommand prints the full list.
191
+
176
192
  ### "I want a clean rebuild"
177
193
 
178
194
  ```bash
@@ -3677,6 +3693,28 @@ Codemods that span multiple versions run in order (e.g. upgrading `0.2.0 → 0.4
3677
3693
 
3678
3694
  Use `voltro db apply` (the declarative diff), not `voltro db migrate` (the imperative file-runner) — only the former reconciles framework tables. If your app also ships `migrations/*.migration.ts`, `voltro db migrate .` runs both halves and is what refusal 4 above asks your pre-deploy job for.
3679
3695
 
3696
+ ## Restart every process — a running one keeps the OLD modules
3697
+
3698
+ `voltro update` changes what is on disk. A process that was already running when
3699
+ you ran it keeps the module graph it loaded at boot, so it goes on executing the
3700
+ previous version indefinitely — and against a `.framework` directory that has
3701
+ since been rewritten.
3702
+
3703
+ That mix is worse than either version alone. A consumer lost half an hour to a
3704
+ pod whose api had started before the upgrade: it served requests, reported
3705
+ healthy, and returned no SSR at all, because the running process held the old
3706
+ modules while the build output on disk was new.
3707
+
3708
+ Restart every process after an update, including ones you did not deploy:
3709
+
3710
+ ```sh
3711
+ kubectl rollout restart deploy/api deploy/web # or: docker compose up -d --force-recreate
3712
+ ```
3713
+
3714
+ `voltro dev` reloads itself, so a development machine is not affected. Anything
3715
+ long-running is — a `voltro serve` / `voltro start` container, a worker, a
3716
+ process a supervisor kept alive across the upgrade.
3717
+
3680
3718
  ## After the update — the checklist
3681
3719
 
3682
3720
  ```bash
@@ -263,6 +263,36 @@ const { data } = useSubscription('app', 'messages.list', {
263
263
  })
264
264
  ```
265
265
 
266
+ ### An undeclared field is refused, not dropped
267
+
268
+ The input schema is the whole accepted key set. A field it does not declare
269
+ fails the call with a `ParseError` naming the key and listing what was expected:
270
+
271
+ ```
272
+ { readonly channelId: string; readonly limit: number }
273
+ └─ ["employeeId"]
274
+ └─ is unexpected, expected: "channelId" | "limit"
275
+ ```
276
+
277
+ It used to be discarded silently, and the reason that is worse than it sounds is
278
+ what a discarded FILTER means. A consumer's layout called a `userSettings.list`
279
+ that declares `userId` with `{ employeeId }`; the payload decoded to `{}`, which
280
+ for a list query is not a narrower filter but the absence of one, and an admin
281
+ was served another user's row.
282
+
283
+ The decoder cannot tell a projection field from a filter field, so it refuses
284
+ either way. Three consequences worth knowing:
285
+
286
+ - Nested objects and union members follow the same rule — a stray key inside
287
+ `{ page: { limit, offset } }` is refused too.
288
+ - `Schema.Struct({})` means "this procedure takes nothing", and a call carrying
289
+ anything is refused. `Schema.Record(...)` keeps its open key set: there the
290
+ openness is declared.
291
+ - If a call site legitimately holds more than the procedure declares — a spread
292
+ of a wider filter object — narrow it at the call site rather than widening the
293
+ schema. Widening restores the silent drop under a different name: the field is
294
+ accepted and still does nothing.
295
+
266
296
  ## Descriptor-return vs computed-return
267
297
 
268
298
  A query executor can return either:
@@ -330,6 +330,39 @@ Precedence, exactly:
330
330
  3. **`.safe()`** on an encrypted column suppresses the implied secret and copies
331
331
  it verbatim — say this only when you have genuinely reviewed the plaintext.
332
332
 
333
+ ## Which stores decrypt — every one the framework hands you
334
+
335
+ The codec is applied by the store, so "which store" is the whole question. All
336
+ three of these decrypt on read and encrypt on write:
337
+
338
+ | store | where you get it |
339
+ | --- | --- |
340
+ | `ctx.store` | inside a handler |
341
+ | the boot store | an auth strategy, a plugin HTTP route, `bindDataStore` |
342
+ | a transaction view | inside `store.transactional(...)` |
343
+
344
+ The boot store is deliberately NOT the request-scoped one — it has no resolved
345
+ Subject, so it carries no tenant scope, no soft-delete filter, no audit stamping
346
+ and no row filter. It does carry the **storage codec**, because that needs no
347
+ Subject: it is how a declared column is spelled on disk versus in JS.
348
+
349
+ That split is worth knowing because getting it wrong is silent. Ciphertext is a
350
+ string. It compares, concatenates, renders and logs without complaint, so a
351
+ value read through a store that skipped the codec fails somewhere else entirely
352
+ — a team sent `enc:v1:…` upstream as a bearer token, got a 401, and spent a day
353
+ inside their auth code. If a value that should be plaintext arrives as
354
+ `enc:v1:…`, the question is which store produced it, not whether the column is
355
+ declared correctly.
356
+
357
+ A store you construct yourself from a driver has no codec. If you need one —
358
+ a migration script, a maintenance task — wrap it:
359
+
360
+ ```ts
361
+ import { wrapStoreWithBootCodec } from '@voltro/runtime'
362
+
363
+ const store = wrapStoreWithBootCodec(rawDriverStore, 'postgres')
364
+ ```
365
+
333
366
  ## Encrypting a column that already has rows
334
367
 
335
368
  `.encrypted()` encrypts on **write**. Adding it to a populated column converts
@@ -327,6 +327,52 @@ surfaces where the READ is the sensitive act — a GDPR export, a salary view
327
327
  usually together with `include` so it stays targeted.
328
328
 
329
329
 
330
+ ## Reading a redacted row — `'shape'`
331
+
332
+ `redactInput`, `redactOutcome` and `redactSubject` default to removing the
333
+ payload entirely (`{ "__redacted": "all" }`). That is the right default and it
334
+ is unreadable on purpose.
335
+
336
+ `'shape'` is the middle option: the payload's STRUCTURE, no value from it.
337
+
338
+ ```ts
339
+ auditPlugin({ redactOutcome: 'shape', redactInput: 'shape' })
340
+ ```
341
+
342
+ ```json
343
+ { "__redacted": { "jiraToken": "string(113)", "attempts": "number" } }
344
+ ```
345
+
346
+ It exists because a team spent a day on a bug their own trail could have ended
347
+ in seconds: a value arrived as 113 characters where 44 were due, and the row
348
+ that would have said so read `{"__redacted":"all"}`. They had asked for that
349
+ redaction one round earlier, and both requests were right.
350
+
351
+ What it discloses, and what it never does:
352
+
353
+ | | |
354
+ | --- | --- |
355
+ | a string | its LENGTH — `string(113)`. Never a prefix, never a hash |
356
+ | a number, a boolean, a date | its TYPE only. A number can BE the secret |
357
+ | a declared field name | survives — `jiraToken` |
358
+ | a key that is not an identifier | described, not reproduced — `<key:string(36)>` |
359
+ | depth / breadth | capped, and the shape says where it stopped |
360
+
361
+ Two of those are worth a sentence each.
362
+
363
+ **A key can be the value.** An object keyed by user data (`{ "user@example.com":
364
+ … }`) puts a datum where a schema name belongs, so a key is reproduced only when
365
+ it looks like a declared field: a plain short identifier. A legitimate key that
366
+ is not one (`content-type`) loses its name and keeps its shape.
367
+
368
+ **A string's length is a real disclosure, and a small one.** For a fixed-format
369
+ credential it carries nothing — every token of a given kind is the same length.
370
+ For a human-chosen password it is a weak hint. If that matters in your threat
371
+ model, `'all'` is the default and stays available.
372
+
373
+ `redactSubject` spells its variant `'metadata-shape'`, because its default
374
+ (`'metadata'`) already names the field it acts on.
375
+
330
376
  ## Scoping the trail — `scope`
331
377
 
332
378
  ```ts