@voltro/cli 0.32.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.
Files changed (339) hide show
  1. package/CHANGELOG.md +2006 -0
  2. package/THIRD-PARTY-NOTICES.md +1 -29
  3. package/bin/nodeEnvironment.d.mts +30 -0
  4. package/bin/nodeEnvironment.mjs +158 -0
  5. package/bin/voltro.mjs +69 -5
  6. package/dist/addCommand-BNeoeSxe.js +124 -0
  7. package/dist/addCommand-aXSQveak.js +2 -0
  8. package/dist/agentsMd-BTchIZku.js +2 -0
  9. package/dist/agentsMd-mhQMF1bx.js +254 -0
  10. package/dist/apiBuild-BICVGnEC.js +2 -0
  11. package/dist/{apiBuild-C4uEbs3s.js → apiBuild-DZF_f0_I.js} +46 -46
  12. package/dist/appPort-B_HpJ_ck.js +48 -0
  13. package/dist/baselineCommand-C2ClWZN3.js +2 -0
  14. package/dist/baselineCommand-DIttzO8A.js +227 -0
  15. package/dist/bin.js +71 -28
  16. package/dist/build-CE7Ca9gm.js +711 -0
  17. package/dist/cacheCommand-DA4OH9xt.js +42 -0
  18. package/dist/capabilitiesCommand-nq_pz5xd.js +123 -0
  19. package/dist/checkCommand-Dv8r9tGO.js +231 -0
  20. package/dist/checkCommand-YZDmkAGd.js +2 -0
  21. package/dist/{cliArgs-qdZSElM3.js → cliArgs-D4p8n7EE.js} +12 -1
  22. package/dist/cliError-BmdYnghb.js +10 -0
  23. package/dist/cliOutput-D1tSBoRM.js +15 -0
  24. package/dist/{cliRuntime-Oh517vCV.js → cliRuntime-Dh7UDinH.js} +20 -20
  25. package/dist/cloudClient-DWL-Hw_T.js +67 -0
  26. package/dist/cloudCmd-Cvv5HGaZ.js +364 -0
  27. package/dist/clusterCmd-CNXKlcvD.js +54 -0
  28. package/dist/codegen-CYM3Zqrf.js +605 -0
  29. package/dist/codegen-ChBi_hVa.js +2 -0
  30. package/dist/codegenCommand-DC6w2tNZ.js +30 -0
  31. package/dist/codemodRunner-DRRqXR74.js +5243 -0
  32. package/dist/commandRunner-BLAEFLjp.js +47 -0
  33. package/dist/commands-gutsz-Ac.js +808 -0
  34. package/dist/connectionConfig-UFlIEiys.js +66 -0
  35. package/dist/dashboardCommand-3YG8p-UA.js +25 -0
  36. package/dist/dataCommand-qL0r7fPO.js +535 -0
  37. package/dist/dataProfile-dW-PsfLB.js +15 -0
  38. package/dist/dbCommand-B6X0FZbq.js +1621 -0
  39. package/dist/dbCommand-CpdKLeQq.js +2 -0
  40. package/dist/{dev-5ficNnvF.js → dev-CerMd0mW.js} +3155 -3110
  41. package/dist/dev-CoG-ZPx8.js +3 -0
  42. package/dist/devActivity-Dx_3nnGv.js +100 -0
  43. package/dist/devActivity.js +1 -1
  44. package/dist/dialectDriver-CgXnDfec.js +39 -0
  45. package/dist/discover-C9XKJDco.js +25 -0
  46. package/dist/doctorCommand-BvqGBwNG.js +2 -0
  47. package/dist/{checkCommand-dm7OHtPt.js → doctorCommand-CnDXQxfa.js} +520 -1278
  48. package/dist/dormancyCommand-Dszo57d6.js +69 -0
  49. package/dist/e2eCmd-BRabZww-.js +147 -0
  50. package/dist/embeddingsCommand-C0sKVRo1.js +73 -0
  51. package/dist/envCommand-DPgdV1Bq.js +60 -0
  52. package/dist/evalCommand-6RUfPen4.js +118 -0
  53. package/dist/evolveCommand-DHpkgjgH.js +281 -0
  54. package/dist/fileTaxonomy-CJfgOllU.js +457 -0
  55. package/dist/frameworkTableAssembly-BGHmck-x.js +2 -0
  56. package/dist/{frameworkTableAssembly-BwIrO5nv.js → frameworkTableAssembly-DkkP6BgC.js} +184 -148
  57. package/dist/generateCommand-oibemh97.js +147 -0
  58. package/dist/index.d.ts +45 -0
  59. package/dist/index.js +4 -3
  60. package/dist/infoCommand-BJw9nLUR.js +60 -0
  61. package/dist/{inspect-BUUjt773.js → inspect-CBqFtAKA.js} +82 -40
  62. package/dist/inspect-C_T_WGvl.js +2 -0
  63. package/dist/inspectCmd-Bppy-GGw.js +224 -0
  64. package/dist/inspectFetch-Cm8_wVvp.js +151 -0
  65. package/dist/inspectMetrics-CfdKLh6t.js +72 -0
  66. package/dist/loadEnv-D9nEOClM.js +44 -0
  67. package/dist/logFileSink-C_D2wRN1.js +105 -0
  68. package/dist/logsCmd-CCca3KRZ.js +260 -0
  69. package/dist/manifestBuild-ChsKAhmn.js +2 -0
  70. package/dist/{manifestBuild-BLrVuSlM.js → manifestBuild-sxpwdKY1.js} +1 -1
  71. package/dist/metaCommands-7MJfZ5cf.js +196 -0
  72. package/dist/migrate-CBwOt_iV.js +83 -0
  73. package/dist/mssqlClusterPatch-_4cE_nun.js +44 -0
  74. package/dist/newCommand-COWOJ1_E.js +156 -0
  75. package/dist/nodeEnvironment-cGFAj1J8.js +28 -0
  76. package/dist/packageCommand-Cug_3Ogl.js +271 -0
  77. package/dist/pageConvention-cEiRxdab.js +5 -0
  78. package/dist/privacyCommand-C-Df56U_.js +146 -0
  79. package/dist/projectScaffold-DmzEKHib.js +2 -0
  80. package/dist/projectScaffold-LMMtaavR.js +814 -0
  81. package/dist/renderModeScan-D7J1B7Kw.js +105 -0
  82. package/dist/renderProfile-1OWWAAtx.js +81 -0
  83. package/dist/runtimeRegistry-DMeKfTHP.js +81 -0
  84. package/dist/runtimeTrace-CRxalXTs.js +91 -0
  85. package/dist/scheduleCmd--jksTrf6.js +69 -0
  86. package/dist/scheduleManifestCmd-D2x0CTTY.js +249 -0
  87. package/dist/schemaIr-UJybUUZW.js +103 -0
  88. package/dist/{sdkgen-B_5mHQS2.js → sdkgen-CYJscZC7.js} +111 -209
  89. package/dist/seedRunner-TFHHiToI.js +329 -0
  90. package/dist/serveCommand-B_isw7q4.js +1647 -0
  91. package/dist/serveCommand-DOvbgRnQ.js +2 -0
  92. package/dist/serveEntry.js +5 -5
  93. package/dist/serverlessCommand-CfJZy6dS.js +482 -0
  94. package/dist/start-9LiUOfES.js +1087 -0
  95. package/dist/start-B-9Nsp-S.js +3 -0
  96. package/dist/startEntry.js +2 -2
  97. package/dist/staticCommand-Dr2M6tpU.js +304 -0
  98. package/dist/storageCommand-Co6NfLqN.js +42 -0
  99. package/dist/templates-De8IR5-c.js +102 -0
  100. package/dist/test-CI6iDsYc.js +115 -0
  101. package/dist/tracesCmd-CkEZQrtt.js +232 -0
  102. package/dist/tsconfigPaths-BWXBWgcl.js +107 -0
  103. package/dist/tsxLoader-EuXmSJ1K.js +51 -0
  104. package/dist/typecheckCommand-BlsWiCNq.js +61 -0
  105. package/dist/updateCommand-Bkptutss.js +585 -0
  106. package/dist/updateCommand-us1_hdIC.js +2 -0
  107. package/dist/{inspectMetrics-BqO4E9G0.js → webDev-CBYvPqQr.js} +1006 -1567
  108. package/dist/webDev-Cg-fFiyd2.js +2 -0
  109. package/dist/webhookDiscovery-CrGAfhIG.js +2 -0
  110. package/dist/webhookDiscovery-D7VaeMlz.js +51 -0
  111. package/dist/webhooksCommand-CID96Rga.js +267 -0
  112. package/dist/workflowsCmd-D1VTmLMY.js +608 -0
  113. package/package.json +193 -18
  114. package/templates/AGENTS.core.md +58 -3
  115. package/templates/AGENTS.md +64 -7
  116. package/templates/agent-docs/_index.md +6 -4
  117. package/templates/agent-docs/_manifest.json +22 -5
  118. package/templates/agent-docs/ai.md +370 -0
  119. package/templates/agent-docs/authentication.md +265 -31
  120. package/templates/agent-docs/caching.md +6 -0
  121. package/templates/agent-docs/cli.md +794 -50
  122. package/templates/agent-docs/data.md +550 -11
  123. package/templates/agent-docs/database/migrations.md +174 -25
  124. package/templates/agent-docs/database/misc.md +193 -40
  125. package/templates/agent-docs/database/querying.md +19 -1
  126. package/templates/agent-docs/database/scaling.md +60 -0
  127. package/templates/agent-docs/database/schema.md +5 -2
  128. package/templates/agent-docs/database/seedsdialects.md +208 -19
  129. package/templates/agent-docs/database/transactions.md +68 -0
  130. package/templates/agent-docs/deployment.md +156 -4
  131. package/templates/agent-docs/introduction.md +87 -16
  132. package/templates/agent-docs/local-first-mobile.md +79 -4
  133. package/templates/agent-docs/multi-tenancy.md +95 -20
  134. package/templates/agent-docs/observability.md +58 -3
  135. package/templates/agent-docs/plugins/ai-flows.md +161 -2
  136. package/templates/agent-docs/plugins/analytics-postgres.md +1 -1
  137. package/templates/agent-docs/plugins/audit.md +37 -1
  138. package/templates/agent-docs/plugins/auth-social.md +143 -0
  139. package/templates/agent-docs/plugins/auth-workos.md +4 -2
  140. package/templates/agent-docs/plugins/auth.md +131 -6
  141. package/templates/agent-docs/plugins/billing.md +132 -15
  142. package/templates/agent-docs/plugins/cdc-out.md +46 -7
  143. package/templates/agent-docs/plugins/clickhouse.md +1 -1
  144. package/templates/agent-docs/plugins/duckdb.md +1 -1
  145. package/templates/agent-docs/plugins/flags.md +132 -0
  146. package/templates/agent-docs/plugins/governance.md +105 -7
  147. package/templates/agent-docs/plugins/multitenancy.md +9 -4
  148. package/templates/agent-docs/plugins/presence.md +13 -2
  149. package/templates/agent-docs/plugins/ratelimit.md +9 -0
  150. package/templates/agent-docs/plugins/search.md +157 -6
  151. package/templates/agent-docs/plugins/sso-saml.md +47 -8
  152. package/templates/agent-docs/plugins/webhooks.md +105 -0
  153. package/templates/agent-docs/plugins.md +200 -18
  154. package/templates/agent-docs/reference.md +60 -3
  155. package/templates/agent-docs/releases.md +1117 -0
  156. package/templates/agent-docs/routing.md +43 -25
  157. package/templates/agent-docs/scheduling.md +14 -1
  158. package/templates/agent-docs/schema-driven-ui.md +92 -12
  159. package/templates/agent-docs/security.md +449 -2
  160. package/templates/agent-docs/templates/apibackends.md +87 -18
  161. package/templates/agent-docs/templates/appshells.md +32 -14
  162. package/templates/agent-docs/templates/overview.md +13 -8
  163. package/templates/agent-docs/testing.md +211 -14
  164. package/templates/agent-docs/whats-new.md +1722 -84
  165. package/templates/agent-docs/workflows.md +130 -14
  166. package/templates/apps/api-ai/actions/summarize.action.ts +11 -0
  167. package/templates/apps/api-ai/package.json +8 -7
  168. package/templates/apps/api-auth/actions/me.action.ts +13 -0
  169. package/templates/apps/api-auth/package.json +9 -8
  170. package/templates/apps/api-backend/mutations/notes.create.mutation.ts +9 -0
  171. package/templates/apps/api-backend/package.json +12 -8
  172. package/templates/apps/api-backend/queries/notes.query.ts +30 -8
  173. package/templates/apps/api-backend-deactivation/actions/users.get.action.ts +14 -0
  174. package/templates/apps/api-backend-deactivation/mutations/users.create.mutation.ts +8 -0
  175. package/templates/apps/api-backend-deactivation/mutations/users.deactivate.mutation.ts +10 -0
  176. package/templates/apps/api-backend-deactivation/package.json +8 -7
  177. package/templates/apps/api-backend-mail/actions/sendWelcome.action.ts +17 -0
  178. package/templates/apps/api-backend-mail/mutations/notes.create.mutation.ts +9 -0
  179. package/templates/apps/api-backend-mail/package.json +9 -8
  180. package/templates/apps/api-backend-mail/queries/notes.query.ts +30 -8
  181. package/templates/apps/api-backend-mariadb/.env.example +14 -0
  182. package/templates/apps/api-backend-mariadb/mutations/notes.create.mutation.ts +9 -0
  183. package/templates/apps/api-backend-mariadb/package.json +10 -9
  184. package/templates/apps/api-backend-mariadb/queries/notes.query.ts +30 -8
  185. package/templates/apps/api-backend-sqlite/.env.example +14 -0
  186. package/templates/apps/api-backend-sqlite/mutations/notes.create.mutation.ts +9 -0
  187. package/templates/apps/api-backend-sqlite/package.json +9 -8
  188. package/templates/apps/api-backend-sqlite/queries/notes.query.ts +30 -8
  189. package/templates/apps/api-backend-storage/actions/uploadAvatar.action.ts +13 -0
  190. package/templates/apps/api-backend-storage/actions/uploadDocument.action.ts +12 -0
  191. package/templates/apps/api-backend-storage/mutations/notes.create.mutation.ts +9 -0
  192. package/templates/apps/api-backend-storage/package.json +9 -8
  193. package/templates/apps/api-backend-storage/queries/notes.query.ts +30 -8
  194. package/templates/apps/api-cms/actions/content.get.action.ts +7 -0
  195. package/templates/apps/api-cms/actions/content.types.action.ts +6 -0
  196. package/templates/apps/api-cms/actions/me.action.ts +13 -0
  197. package/templates/apps/api-cms/app.config.ts +19 -0
  198. package/templates/apps/api-cms/authz.ts +63 -0
  199. package/templates/apps/api-cms/mutations/content.publish.mutation.ts +10 -0
  200. package/templates/apps/api-cms/mutations/content.saveDraft.mutation.ts +10 -0
  201. package/templates/apps/api-cms/mutations/content.unpublish.mutation.ts +5 -0
  202. package/templates/apps/api-cms/package.json +11 -10
  203. package/templates/apps/api-cms/queries/content.list.query.ts +23 -7
  204. package/templates/apps/api-cms/tests/accessDecisions.test.ts +121 -0
  205. package/templates/apps/api-cms/tests/content.descriptors.test.ts +8 -4
  206. package/templates/apps/api-collab/mutations/documents.create.mutation.ts +8 -0
  207. package/templates/apps/api-collab/mutations/documents.setBody.mutation.ts +13 -0
  208. package/templates/apps/api-collab/package.json +9 -8
  209. package/templates/apps/api-collab/queries/documents.query.ts +19 -7
  210. package/templates/apps/api-data-advanced/package.json +9 -8
  211. package/templates/apps/api-data-advanced/queries/authors.withBooks.query.ts +12 -0
  212. package/templates/apps/api-data-advanced/queries/books.search.query.ts +9 -0
  213. package/templates/apps/api-durable/mutations/orders.approve.mutation.ts +17 -0
  214. package/templates/apps/api-durable/mutations/orders.place.mutation.ts +9 -0
  215. package/templates/apps/api-durable/package.json +9 -8
  216. package/templates/apps/api-feature-flags/actions/notes.export.action.ts +16 -0
  217. package/templates/apps/api-feature-flags/mutations/notes.create.mutation.ts +12 -0
  218. package/templates/apps/api-feature-flags/package.json +10 -9
  219. package/templates/apps/api-governance/README.md +30 -8
  220. package/templates/apps/api-governance/actions/profiles.get.action.server.ts +17 -1
  221. package/templates/apps/api-governance/actions/profiles.get.action.ts +29 -4
  222. package/templates/apps/api-governance/database/schema.ts +16 -4
  223. package/templates/apps/api-governance/mutations/profiles.create.mutation.ts +12 -0
  224. package/templates/apps/api-governance/package.json +9 -8
  225. package/templates/apps/api-kv/actions/sync.pull.action.ts +15 -0
  226. package/templates/apps/api-kv/actions/sync.reset.action.ts +13 -0
  227. package/templates/apps/api-kv/actions/sync.status.action.ts +7 -0
  228. package/templates/apps/api-kv/package.json +9 -8
  229. package/templates/apps/api-kv/queries/events.list.query.ts +19 -8
  230. package/templates/apps/api-moderation/mutations/comments.create.mutation.ts +11 -0
  231. package/templates/apps/api-moderation/mutations/posts.create.mutation.ts +13 -0
  232. package/templates/apps/api-moderation/package.json +9 -8
  233. package/templates/apps/api-observability/mutations/notes.create.mutation.ts +8 -0
  234. package/templates/apps/api-observability/package.json +9 -8
  235. package/templates/apps/api-observability/queries/notes.list.query.ts +13 -0
  236. package/templates/apps/api-ratelimit/mutations/notes.create.mutation.ts +14 -0
  237. package/templates/apps/api-ratelimit/package.json +9 -8
  238. package/templates/apps/api-rbac/package.json +9 -8
  239. package/templates/apps/api-rest/package.json +8 -7
  240. package/templates/apps/api-saas/mutations/projects.create.mutation.ts +13 -0
  241. package/templates/apps/api-saas/package.json +12 -11
  242. package/templates/apps/api-saas/queries/projects.list.query.ts +11 -0
  243. package/templates/apps/api-saas-starter/actions/me.action.ts +13 -0
  244. package/templates/apps/api-saas-starter/app.config.ts +19 -0
  245. package/templates/apps/api-saas-starter/authz.ts +75 -0
  246. package/templates/apps/api-saas-starter/mutations/invites.create.mutation.ts +10 -0
  247. package/templates/apps/api-saas-starter/mutations/projects.create.mutation.ts +9 -0
  248. package/templates/apps/api-saas-starter/package.json +15 -11
  249. package/templates/apps/api-saas-starter/queries/invites.list.query.ts +19 -8
  250. package/templates/apps/api-saas-starter/queries/projects.list.query.ts +18 -7
  251. package/templates/apps/api-saas-starter/tests/accessDecisions.test.ts +135 -0
  252. package/templates/apps/api-search/mutations/articles.create.mutation.ts +14 -0
  253. package/templates/apps/api-search/package.json +9 -8
  254. package/templates/apps/api-search/queries/articles.list.query.ts +21 -8
  255. package/templates/apps/api-status/README.md +10 -3
  256. package/templates/apps/api-status/app.config.ts +8 -3
  257. package/templates/apps/api-status/authz.ts +5 -3
  258. package/templates/apps/api-status/package.json +9 -8
  259. package/templates/apps/api-status/queries/components.list.query.ts +14 -6
  260. package/templates/apps/api-status/queries/incidents.live.query.ts +23 -10
  261. package/templates/apps/api-status/queries/updates.list.query.ts +16 -9
  262. package/templates/apps/api-status/tests/status.test.ts +9 -1
  263. package/templates/apps/api-versioning/actions/documents.asOf.action.ts +12 -0
  264. package/templates/apps/api-versioning/actions/documents.history.action.ts +13 -0
  265. package/templates/apps/api-versioning/mutations/documents.create.mutation.ts +9 -0
  266. package/templates/apps/api-versioning/mutations/documents.update.mutation.ts +12 -0
  267. package/templates/apps/api-versioning/package.json +9 -8
  268. package/templates/apps/api-webhooks/mutations/orders.fulfill.mutation.ts +16 -0
  269. package/templates/apps/api-webhooks/package.json +10 -9
  270. package/templates/apps/api-webhooks/queries/orders.list.query.ts +10 -0
  271. package/templates/apps/changelog/package.json +8 -6
  272. package/templates/apps/edge-functions/package.json +2 -2
  273. package/templates/apps/frontend-admin/package.json +10 -8
  274. package/templates/apps/frontend-admin/src/lib/admin.ts +20 -9
  275. package/templates/apps/frontend-admin/src/locales/de.ts +11 -1
  276. package/templates/apps/frontend-admin/src/locales/en.ts +13 -1
  277. package/templates/apps/frontend-admin/src/pages/admin/[entity]/page.tsx +65 -22
  278. package/templates/apps/frontend-admin/src/pages/admin/entity.test.tsx +130 -23
  279. package/templates/apps/frontend-admin/src/pages/admin/layout.tsx +3 -2
  280. package/templates/apps/frontend-admin/src/pages/admin/page.test.tsx +19 -2
  281. package/templates/apps/frontend-admin/src/pages/admin/page.tsx +9 -4
  282. package/templates/apps/frontend-app/app.config.ts +4 -3
  283. package/templates/apps/frontend-app/package.json +11 -8
  284. package/templates/apps/frontend-app/src/lib/api.ts +25 -0
  285. package/templates/apps/frontend-app/src/pages/page.test.tsx +130 -82
  286. package/templates/apps/frontend-app/src/pages/page.tsx +14 -18
  287. package/templates/apps/frontend-auth/package.json +10 -8
  288. package/templates/apps/frontend-blank/package.json +9 -7
  289. package/templates/apps/frontend-cms/package.json +11 -9
  290. package/templates/apps/frontend-collab/package.json +12 -9
  291. package/templates/apps/frontend-collab/src/pages/page.test.tsx +122 -78
  292. package/templates/apps/frontend-contact/package.json +9 -7
  293. package/templates/apps/frontend-dashboard/package.json +9 -7
  294. package/templates/apps/frontend-docs/package.json +9 -7
  295. package/templates/apps/frontend-i18n/package.json +8 -6
  296. package/templates/apps/frontend-landing/package.json +9 -7
  297. package/templates/apps/frontend-portal/package.json +10 -8
  298. package/templates/apps/frontend-saas/app.config.ts +10 -6
  299. package/templates/apps/frontend-saas/package.json +10 -8
  300. package/templates/apps/frontend-saas/src/lib/api.ts +27 -32
  301. package/templates/apps/frontend-saas/src/pages/dashboard/billing/page.tsx +7 -8
  302. package/templates/apps/frontend-saas/src/pages/dashboard/page.test.tsx +27 -3
  303. package/templates/apps/frontend-saas/src/pages/dashboard/page.tsx +4 -4
  304. package/templates/apps/frontend-saas/src/pages/dashboard/team/page.tsx +3 -4
  305. package/templates/apps/frontend-spa/package.json +9 -7
  306. package/templates/apps/frontend-ssr/package.json +9 -7
  307. package/templates/apps/frontend-ssr-api/package.json +10 -8
  308. package/templates/apps/frontend-static-blog/package.json +8 -6
  309. package/templates/apps/frontend-status/package.json +10 -8
  310. package/templates/apps/mobile-app/README.md +1 -0
  311. package/templates/apps/mobile-app/package.json +4 -2
  312. package/templates/apps/mobile-app/src/app/index.tsx +22 -12
  313. package/templates/apps/mobile-app/src/app/orders/[id].tsx +1 -1
  314. package/templates/apps/mobile-app/src/lib/api.ts +34 -0
  315. package/templates/apps/mobile-app/voltro.mobile.ts +4 -2
  316. package/templates/baselines/bare/.env.example +14 -0
  317. package/templates/baselines/bare/baseline.json +4 -4
  318. package/templates/baselines/compose/.env.example +14 -0
  319. package/templates/baselines/compose/README.md +1 -1
  320. package/templates/baselines/compose/baseline.json +5 -5
  321. package/templates/baselines/compose-mariadb/.env.example +14 -0
  322. package/templates/baselines/compose-mariadb/README.md +1 -1
  323. package/templates/baselines/compose-mariadb/baseline.json +5 -5
  324. package/templates/baselines/helm/.env.example +14 -0
  325. package/templates/baselines/helm/baseline.json +4 -4
  326. package/dist/apiBuild-OJEjtwcn.js +0 -2
  327. package/dist/checkCommand-CwMrzAgV.js +0 -2
  328. package/dist/commands-C0nEePif.js +0 -11457
  329. package/dist/dbCommand-By__Ev0R.js +0 -2
  330. package/dist/dbCommand-ifOMafuG.js +0 -1311
  331. package/dist/dev-rc3fwPSZ.js +0 -3
  332. package/dist/devActivity-BhIu6ncs.js +0 -159
  333. package/dist/frameworkTableAssembly-D-EebUQX.js +0 -2
  334. package/dist/inspect-mmBuRXmy.js +0 -2
  335. package/dist/manifestBuild-Dj8Jjoto.js +0 -2
  336. package/dist/seedRunner-Bqxgp7HZ.js +0 -230
  337. package/dist/serveCommand-PxMmn96o.js +0 -1578
  338. package/dist/start-D1-8eKrO.js +0 -1084
  339. /package/templates/apps/api-ai/actions/{summarize.action.server.tsx → summarize.action.server.ts} +0 -0
@@ -22,6 +22,35 @@ Please report suspected vulnerabilities **privately** — never in public issues
22
22
 
23
23
  You'll get an **acknowledgement within 3 business days**, an assessment once we've reproduced the issue, and a **coordinated disclosure** timeline agreed with you — with credit in the release notes if you'd like it. We don't run a paid bug-bounty program yet, but we genuinely value responsible disclosure.
24
24
 
25
+ ## Procedures are default-DENY
26
+
27
+ Every wire-exposed procedure must declare **who may call it**. A `*.query.ts` /
28
+ `*.mutation.ts` / `*.action.ts` / `*.stream.ts` that declares neither `guards:`
29
+ nor `openAccess:` is refused at boot — by `voltro dev`, by `voltro serve`, and by
30
+ `voltro doctor` as a preflight.
31
+
32
+ Before this, `guards:` defaulted to "allowed": a discovered procedure with no
33
+ guard was callable by any authenticated session. Nothing in the code said so,
34
+ and the only structural check was a report a human had to run.
35
+
36
+ ```ts
37
+ guards: [{ scope: 'invoices:read' }] // the caller must hold a scope
38
+ openAccess: 'public pricing, no caller data' // anyone may call it — and why
39
+ internal: true // not on the wire at all
40
+ ```
41
+
42
+ `openAccess` takes a reason rather than a boolean on purpose: without an explicit
43
+ way to declare an endpoint open, the only way to satisfy the gate is to invent a
44
+ guard, and the guard people invent is one every caller already holds. That reads
45
+ as protection and enforces nothing.
46
+
47
+ The whole app can opt out with one field — `security: { defaultDeny: false }` in
48
+ `app.config.ts`. There is no environment variable for it: the only direction
49
+ anyone reaches for is off, and an env var is how that becomes permanent in one CI
50
+ job with no diff to review. Full detail, including what the gate does not cover
51
+ (a plugin's own procedures), in
52
+ [Authorization](/docs/authentication/authorization).
53
+
25
54
  ## Outbound HTTP is SSRF-guarded by default
26
55
 
27
56
  The `HttpClient` your handlers `yield*` refuses internal targets:
@@ -88,6 +117,403 @@ DNS is not resolved. A public hostname that *resolves* to a private address (DNS
88
117
  rebinding) still passes. That vector needs network-layer egress control; it is
89
118
  stated here rather than silently implied.
90
119
 
120
+ ## Where the transport settings live
121
+
122
+ Everything in the next three sections — the origin check, which proxies may
123
+ forward a client IP, and the response headers — is one `security:` block in
124
+ `app.config.ts`, read identically by `voltro dev` and `voltro serve`:
125
+
126
+ ```ts
127
+ // app.config.ts
128
+ export default {
129
+ security: {
130
+ // Cross-site protection for every state-changing request. Default 'same-origin'.
131
+ originGuard: 'same-origin',
132
+ // Extra origins to accept — a split web/api deployment.
133
+ allowedOrigins: ['https://app.example.com'],
134
+ // Whose x-forwarded-for to believe. Empty (the default) ignores the header.
135
+ trustedProxies: ['private'],
136
+ // Security response headers. On by default; 'strict' applies the api policy to HTML too.
137
+ headers: { mode: 'default' },
138
+ },
139
+ }
140
+ ```
141
+
142
+ Every field has a safe default, so an app that writes none of this is already
143
+ protected. Each also has an environment override, listed with its section below
144
+ — use those to correct a deployment you cannot rebuild, and the config file for
145
+ everything else. A security decision that exists only as an exported variable on
146
+ one machine is invisible to code review.
147
+
148
+ ## Cross-site requests are refused
149
+
150
+ **Every request that can change state is checked** — anything but `GET`, `HEAD`
151
+ and `OPTIONS` — plus the `/ws` upgrade, which is a GET. That is deliberately a
152
+ rule about the METHOD rather than a list of paths, because a mutation reaches
153
+ your api by more than one road: `POST /rpc`, every REST route projected from a
154
+ `publicApi:` mutation, everything in `apiConfig.restRoutes`, and
155
+ `POST /v1/api-keys`. They all resolve the caller through the same auth chain,
156
+ so they are all reachable **with the session cookie automatically attached**.
157
+
158
+ A browser request is accepted when its `Origin` matches the `Host` it was
159
+ addressed to, or is in your allowlist. Anything else gets `403 origin not
160
+ allowed` — including `Origin: null`, which is what a sandboxed iframe sends, and
161
+ a request with no `Origin` whose `Sec-Fetch-Site` says `cross-site`.
162
+
163
+ Before this, framework-wide CSRF protection rested entirely on the
164
+ `SameSite=Lax` cookie default — one caller-overridable attribute that still
165
+ permits top-level-navigation POST, and that does nothing at all for a
166
+ bearer/JWT flow.
167
+
168
+ Reads are not checked: a `GET` cannot be a cross-site write, and checking it
169
+ would break every link into your api.
170
+
171
+ ### A split web/api deployment must declare its origins
172
+
173
+ If your web app is served from a different origin than your api, say so — or
174
+ every mutation, every REST write and every socket from that page will 403:
175
+
176
+ ```ts
177
+ // app.config.ts
178
+ export default {
179
+ security: {
180
+ allowedOrigins: ['https://app.example.com', 'https://admin.example.com'],
181
+ },
182
+ }
183
+ ```
184
+
185
+ Same-origin apps (the default layout, and anything behind one ingress) need no
186
+ change. `security: { originGuard: 'off' }` disables the check; a misspelled
187
+ value falls back to **enforcing**, never to off.
188
+
189
+ Environment overrides, for a deployment you cannot rebuild:
190
+ `VOLTRO_ALLOWED_ORIGINS` (comma-separated) and `VOLTRO_ORIGIN_GUARD=off`. If you
191
+ embed the runtime yourself, the same values are
192
+ `RpcServerOptions.security.originGuard` / `.allowedOrigins`.
193
+
194
+ ### Server-to-server callers are exempt, and that is deliberate
195
+
196
+ A request carrying neither `Origin` nor `Sec-Fetch-Site` did not come from a
197
+ browsing context, so it is allowed. That covers:
198
+
199
+ - **SSR loaders** — `voltro dev` / `voltro start` render pages server-side and
200
+ their loaders call the api from node, which attaches no origin header. Without
201
+ this exemption, server-side first paint would break on every page with a
202
+ loader.
203
+ - mobile SDKs, `curl`, and any other service calling your api.
204
+
205
+ A CSRF attack needs the victim's ambient credentials, and only a browser attaches
206
+ those — a browser cannot be made to omit `Origin` on a cross-origin POST or a
207
+ WebSocket handshake. An attacker's own server can POST without one, but it has no
208
+ session to ride: that is simply an unauthenticated request, and auth and guards
209
+ still apply to it.
210
+
211
+ Origins are compared by **authority** (host + port), not scheme. Behind a
212
+ TLS-terminating ingress your app sees plain http while the browser reports
213
+ `https://…`, and there is no unforgeable way to learn the external scheme.
214
+
215
+ ### Local development is not a special case you have to configure
216
+
217
+ When **both** the origin and the `Host` are loopback (`localhost`, `127.0.0.1`,
218
+ `*.localhost`), the request is accepted. That is the `voltro dev` layout: the web
219
+ dev server proxies the api with `changeOrigin: true`, so the api sees
220
+ `Host: localhost:4000` while the browser correctly reports
221
+ `Origin: http://localhost:5190`. Strictly compared those differ, and every dev
222
+ session would lose its websocket.
223
+
224
+ Both sides must be loopback. A production api on `api.example.com` still refuses
225
+ `Origin: http://localhost:5190` — a browser cannot be made to claim a loopback
226
+ origin it is not on.
227
+
228
+ **Testing a dev server from a phone on your wifi is the one case that needs a
229
+ line.** The page is then `http://192.168.1.5:5190`, which is not loopback, so the
230
+ socket is refused and the api logs `refused cross-site request` naming the
231
+ origin. Add it while you test:
232
+
233
+ ```ts
234
+ // app.config.ts
235
+ export default {
236
+ security: {
237
+ allowedOrigins: ['http://192.168.1.5:5190'],
238
+ },
239
+ }
240
+ ```
241
+
242
+ The carve-out is deliberately not widened to "both sides are private
243
+ addresses": on a self-hosted internal deployment that would accept any other
244
+ host on the same network, which is a different claim than "the attacker already
245
+ runs code on this machine".
246
+
247
+ ### Routes that a third party legitimately POSTs to
248
+
249
+ A few surfaces are outside the check, because their caller cannot be a browser
250
+ riding your user's cookie:
251
+
252
+ - **The inspect surface** (`/_voltro/inspect/*`) — token-gated, and meant to be
253
+ read cross-origin by the local and cloud dashboards.
254
+ - **Incoming webhooks** (`*.webhook.tsx`) — authenticated by the sender's
255
+ signature, which the framework refuses to boot without.
256
+ - **Plugin routes that declare it.** First-party examples:
257
+ `@voltro/plugin-sso-saml` (`/saml` — the IdP makes the browser form-POST a
258
+ signed assertion, which IS a cross-site POST), `@voltro/plugin-storage`'s
259
+ upload routes (a signed upload ticket, plus their own CORS allowlist),
260
+ `@voltro/plugin-billing`'s webhook and `@voltro/plugin-scim` (bearer-only).
261
+
262
+ If you write a plugin HTTP route in that category, declare it:
263
+
264
+ ```ts
265
+ import type { PluginHttpRoute } from '@voltro/protocol'
266
+
267
+ const route: PluginHttpRoute = {
268
+ method: 'POST',
269
+ path: '/partner/callback',
270
+ // The signature on the body is the authority — not the session cookie.
271
+ originGuard: 'exempt',
272
+ handle: async (req) => {
273
+ if (!verifySignature(req)) return { status: 401 }
274
+ return { status: 204 }
275
+ },
276
+ }
277
+ ```
278
+
279
+ Apply exactly one test before you write that line: *if an attacker's page makes
280
+ a browser send this request with your user's cookies attached, does anything
281
+ happen?* If the answer is "no — it still needs a signature, a bearer token or a
282
+ signed ticket the attacker does not have", the route is exempt. Otherwise it is
283
+ not. The exemption covers the route's whole path prefix, not the sub-paths its
284
+ handler branches on.
285
+
286
+ ## The client IP comes from the socket, not from a header
287
+
288
+ `x-forwarded-for` is a request header, so **any client can write it**. Voltro
289
+ ignores it unless you declare which proxies are allowed to forward — the address
290
+ used for rate limiting, geo-blocking and audit records is
291
+ `socket.remoteAddress`, the one value nobody upstream of the kernel can forge.
292
+
293
+ If you run behind a load balancer, ingress or CDN **and** rate-limit or geo-block
294
+ per IP, declare it:
295
+
296
+ ```ts
297
+ // app.config.ts
298
+ export default {
299
+ security: {
300
+ trustedProxies: ['private'], // RFC1918 + CGNAT + link-local + unique-local
301
+ // ['loopback'] — local / docker-compose
302
+ // ['10.0.0.0/8', 'fc00::/7'] — explicit CIDRs
303
+ // ['2'] — trust two hops
304
+ // ['*'] — any peer; only when your ingress
305
+ // OVERWRITES rather than appends
306
+ },
307
+ }
308
+ ```
309
+
310
+ The environment override is `VOLTRO_TRUSTED_PROXIES`, comma-separated
311
+ (`VOLTRO_TRUSTED_PROXIES=10.0.0.0/8,fc00::/7`).
312
+
313
+ With a list configured, the peer that opened the connection must itself be
314
+ trusted, and the chain is walked right-to-left past every declared proxy — so a
315
+ client that prepends a fake hop cannot push the resolved address further left.
316
+ The same setting decides whether `x-forwarded-proto` is believed, which is what
317
+ gates HSTS below.
318
+
319
+ Without it, a per-IP limiter still works; it just counts every request against
320
+ your proxy's address instead of the caller's. Embedders pass the same list as
321
+ `RpcServerOptions.security.trustedProxies`.
322
+
323
+ ### The same address reaches your plugin routes
324
+
325
+ A plugin HTTP route receives it as `req.remoteAddr` — already resolved through
326
+ the policy above. **Use that, never `req.headers['x-forwarded-for']`.** The
327
+ built-in auth routes do: what `@voltro/plugin-auth` and
328
+ `@voltro/plugin-auth-social` write into `sessions.ipAddress` is the resolved
329
+ address, so the column a breach investigation reads cannot be chosen by the
330
+ caller.
331
+
332
+ ```ts
333
+ import type { PluginHttpRoute } from '@voltro/protocol'
334
+
335
+ const route: PluginHttpRoute = {
336
+ method: 'POST',
337
+ path: '/partner/callback',
338
+ handle: async (req) => {
339
+ // Resolved once per request; `undefined` only when there is no socket
340
+ // address (a unix socket, or a hand-built request in a test).
341
+ await audit(req.remoteAddr ?? null)
342
+ return { status: 204 }
343
+ },
344
+ }
345
+ ```
346
+
347
+ ## Security headers ship by default
348
+
349
+ Every response from the api listener carries:
350
+
351
+ ```
352
+ content-security-policy: default-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'none'
353
+ x-frame-options: DENY
354
+ referrer-policy: no-referrer
355
+ x-content-type-options: nosniff
356
+ strict-transport-security: max-age=15552000; includeSubDomains (https only)
357
+ ```
358
+
359
+ `default-src 'none'` is safe here because this listener serves the **api** —
360
+ rpc JSON, the socket upgrade, inspect routes, webhooks, plugin routes — not your
361
+ web app's HTML, which is a different listener. A `text/html` response from a
362
+ plugin route gets a relaxed policy instead
363
+ (`frame-ancestors 'none'; base-uri 'none'; object-src 'none'`), so a docs page
364
+ still renders while clickjacking and base-tag injection stay closed.
365
+
366
+ HSTS is sent **only over https** and deliberately never carries `preload`:
367
+ preload is effectively irreversible for a domain, so it has to be your decision.
368
+ `Cross-Origin-Opener-Policy` and `Cross-Origin-Resource-Policy` are not defaulted
369
+ either — guessing them breaks a legitimate cross-origin dashboard — but you can
370
+ add them.
371
+
372
+ A route that sets a header itself always wins; the framework only fills gaps.
373
+
374
+ The whole policy is one field, including an `extra` bag for headers the
375
+ framework does not default:
376
+
377
+ ```ts
378
+ // app.config.ts
379
+ export default {
380
+ security: {
381
+ headers: {
382
+ mode: 'default', // 'off' | 'default' | 'strict'
383
+ hsts: 'max-age=31536000; includeSubDomains', // or false to drop it
384
+ extra: { 'permissions-policy': 'geolocation=()' },
385
+ },
386
+ },
387
+ }
388
+ ```
389
+
390
+ `mode: 'strict'` applies the api policy to HTML too. That is the documented
391
+ trade: it blanks `@voltro/plugin-openapi`'s `/docs` page, which loads its viewer
392
+ from a CDN. Choose it when this listener serves no HTML you own.
393
+
394
+ Environment overrides: `VOLTRO_SECURITY_HEADERS` (`off|default|strict`),
395
+ `VOLTRO_CSP`, `VOLTRO_CSP_HTML`, `VOLTRO_HSTS` — each accepting `off` to drop
396
+ just that one. Embedders pass the same object as
397
+ `RpcServerOptions.security.headers`.
398
+
399
+ ## Incoming webhooks must verify their caller
400
+
401
+ An incoming webhook is a **public POST that runs your application code**. A
402
+ `defineIncomingWebhook({...})` that declares no signature scheme now refuses to
403
+ boot, naming the endpoint.
404
+
405
+ ```ts
406
+ export default defineIncomingWebhook({
407
+ id: 'orders.paid',
408
+ provider: stripeWebhookProvider(), // HMAC scheme + replay window + idempotency
409
+ payload: OrderPaid,
410
+ handler: async (ctx) => { /* the caller is already verified here */ },
411
+ })
412
+ ```
413
+
414
+ Four ways to satisfy it:
415
+
416
+ | Declaration | Means |
417
+ |---|---|
418
+ | `provider: stripeWebhookProvider()` | a preset brings the scheme, replay window and idempotency key |
419
+ | `signature: { _tag: 'hmac', … }` | a hand-declared scheme for a sender with its own convention |
420
+ | `verification: 'provider'` | your handler verifies with the provider's own SDK |
421
+ | `verification: 'none'` | deliberately public — a gateway or IP allow-list owns the boundary; logged as a warning on every boot |
422
+
423
+ A signature-verified webhook reads its shared secret from an env var named after
424
+ its id:
425
+
426
+ ```bash
427
+ VOLTRO_WEBHOOK_SECRET_ORDERS_PAID=<the value the sender holds>
428
+ ```
429
+
430
+ **A missing secret is now a 503, not a skipped check.** It used to run the
431
+ handler unverified, so forgetting one variable silently converted a verified
432
+ webhook into an open one — in production, with nothing in the code to review.
433
+ The framework never invents this value: the sender holds the other half, so a
434
+ generated secret would authenticate nobody.
435
+
436
+ ### `voltro doctor` tells you before a deploy does
437
+
438
+ The preflight reports the same verdict the boot reaches:
439
+
440
+ ```
441
+ incoming webhook verification
442
+ ✓ signature / provider verified 2
443
+ ! deliberately public (none) 1
444
+ internal.sync — `verification: 'none'`; a gateway / IP allow-list owns this URL
445
+ ✗ nothing verifies the caller 0
446
+ ```
447
+
448
+ An endpoint in the last row exits non-zero — that app does not start. The middle
449
+ row never fails; it is there so a deliberately public URL is visible in a review
450
+ instead of only in a boot log. Both rows are in `voltro doctor --json` under
451
+ `webhookVerification`.
452
+
453
+ ## The rpc body cap counts bytes as they arrive — and answers 413 either way
454
+
455
+ `POST /rpc` is capped at 8 MiB (`VOLTRO_MAX_RPC_BODY_BYTES`). The limit is
456
+ enforced **while the body streams**, so a `Transfer-Encoding: chunked` request
457
+ with no `Content-Length` is cut off at the cap rather than buffered without
458
+ bound. A declared oversize length is still rejected up front, so an honest client
459
+ gets its `413` without uploading anything.
460
+
461
+ **Both shapes end in the same response:**
462
+
463
+ | Request | Response |
464
+ | --- | --- |
465
+ | `Content-Length` over the cap | `413 Payload Too Large`, before the upload |
466
+ | `Transfer-Encoding: chunked` over the cap | `413 Payload Too Large`, once the counter crosses the cap |
467
+ | Either, under the cap | handled normally |
468
+
469
+ The refusal is logged server-side under the `voltro:security` scope, with the cap
470
+ and the number of bytes read before the server stopped (never the body's real
471
+ size — it is not read).
472
+
473
+ Uploads ride separate plugin routes with their own `limits.maxBytes`, and are
474
+ unaffected.
475
+
476
+ ## Logs are redacted by default
477
+
478
+ Every logger surface — `createLogger`, `makeLogger`, `LoggerLayer` — installs a redactor with no configuration, and it runs **before** formatting and **before** the sink fan-out, so a masked value reaches neither stdout nor any downstream sink (the CLI buffer, logship, Datadog).
479
+
480
+ ```ts
481
+ import { createLogger } from '@voltro/logger'
482
+
483
+ const log = createLogger({ scope: 'orders' })
484
+ log.info('request', { headers: req.headers, body: input })
485
+ // → headers.authorization = [redacted]
486
+ // headers.cookie = [redacted]
487
+ // body.newPassword = [redacted]
488
+ // headers['user-agent'] = curl/8 ← untouched
489
+ ```
490
+
491
+ Three rules, all on:
492
+
493
+ - **Key names** (`DEFAULT_REDACT_KEYS`) — matched exactly on a normalised key, so `apiKey`, `api_key`, `API-KEY` and `Api Key` are one key. Credentials, bearer/API tokens, `authorization` / `cookie` / `set-cookie` / `x-api-key`, session and second-factor values, and the payment / government identifiers (`creditCard`, `cvv`, `iban`, `ssn`, …) that must never reach a log index.
494
+ - **Key fragments** (`DEFAULT_REDACT_KEY_PARTS`) — substring matches for the compound names real code writes: `newPassword`, `oldPassword`, `stripeApiKey`, `userAccessToken`.
495
+ - **Value shapes** — a credential is masked regardless of its key: `Bearer …`, `Basic …`, a JWT, `sk_` / `pk_` / `whsec_`-prefixed keys, GitHub / Slack / AWS key ids, a PEM private key.
496
+
497
+ There is deliberately **no entropy heuristic**. A trace id, a content hash, a git sha and a base64 thumbnail are all long and high-entropy — a redactor that eats the fields an incident is read through gets switched off wholesale, which costs more than the gap it closed.
498
+
499
+ ### Adding to the list, and why nothing subtracts
500
+
501
+ `redactKeys` **adds**; it does not replace:
502
+
503
+ ```ts
504
+ createLogger({ redactKeys: ['patientId', 'policyNumber'] })
505
+ ```
506
+
507
+ There is no way to remove a built-in key. The only reason to un-mask `password` is to read it while debugging, and the answer to that is to log a non-secret projection — not to publish the value to stdout and to whatever log shipper the deployment happens to have. A subtraction knob would also apply to every **dependency** logging under that key, including plugins you never read, which is a blast radius an app cannot assess.
508
+
509
+ The deliberate escape hatch is `redact`, a transform you write and own — and it runs *after* the built-in redactor, so it can mask more and structurally cannot unmask:
510
+
511
+ ```ts
512
+ createLogger({ redact: (record) => ({ ...record, fields: { ...record.fields, region: 'eu' } }) })
513
+ ```
514
+
515
+ > **If a log line you relied on went quiet**, rename the field rather than reaching for an exemption. `traceId`, `requestId` and `cookieName` are all untouched — the framework's own session diagnostic was renamed from `cookie` to `cookieName` for exactly this reason.
516
+
91
517
  ## Auditing what your log tables actually hold
92
518
 
93
519
  ```bash
@@ -95,7 +521,7 @@ voltro db scan-credentials
95
521
  voltro db scan-credentials --table my_events:actor
96
522
  ```
97
523
 
98
- Counts rows carrying a credential-shaped key — `token`, `secret`, `password`, `apikey`, `credential`, `privatekey` — plus any table you name with `--table <name>[:<column>]`. Exit code `1` on a hit, so CI can gate on it.
524
+ Counts rows in which a credential-shaped **name** appears anywhere in the stored value — `token`, `secret`, `password`, `apikey`, `credential`, `privatekey` — plus any table you name with `--table <name>[:<column>]`. Exit code `1` on an unexplained hit, so CI can gate on it.
99
525
 
100
526
  By default it scans every column that can physically HOLD one:
101
527
 
@@ -109,12 +535,31 @@ By default it scans every column that can physically HOLD one:
109
535
  Every hit line names the needles that matched, with the number of rows each appears in:
110
536
 
111
537
  ```
112
- ✗ _voltro_audit_log.outcome — 69 of 149 row(s) match a credential-shaped key
538
+ ✗ _voltro_audit_log.outcome — 69 of 149 row(s) contain a credential-shaped NAME (anywhere in the value)
113
539
  matched (rows per needle, may overlap): token (61), secret (12)
540
+ examined 69 of 69 matched row(s): 61 as a JSON KEY, 8 only inside a redaction marker
114
541
  ```
115
542
 
116
543
  **Those counts overlap and do not sum to the hit count** — a row holding both a token and a secret is counted by both needles. Without the breakdown the advice underneath (*purge them AND rotate the credentials*) is neither executable — purge what, rotate which — nor refutable: a column mentioning the word `token` in prose reads identically to one holding a live one.
117
544
 
545
+ ### Hits are explained, not just counted
546
+
547
+ The predicate is a substring match over the whole serialized column, so a match is a match on a *name*, wherever it sits. The third line above is a bounded second pass that reads the matched rows back and says what actually matched — a JSON **key**, or only a **redaction marker**. Values are never printed and never logged.
548
+
549
+ A redaction marker is the framework's own record that a credential was deliberately *not* stored: `_omitted` (`@voltro/plugin-versioning`, the column names left out of a row snapshot) and `__redacted` (`@voltro/plugin-audit`). A target whose every matched row is one of those is reported as explained, and does **not** fail CI:
550
+
551
+ ```
552
+ ~ _voltro_row_history.data — 69 of 149 row(s) contain a credential-shaped NAME (anywhere in the value)
553
+ matched (rows per needle, may overlap): token (69)
554
+ examined 69 of 69 matched row(s): 0 as a JSON KEY, 69 only inside a redaction marker
555
+ EXPLAINED — every match is `_omitted` / `__redacted`, the framework's own
556
+ record that a `.sensitive()` column was left OUT. Nothing to purge, nothing to rotate.
557
+ ```
558
+
559
+ The bar for that is deliberately high: **every** matched row examined, **every** one of them a marker and nothing else. A target where the read-back was capped (at 500 rows), or where one row is a real key, or where one row is not parseable as JSON, stays a finding and still exits `1`. A partially-explained target is still a target.
560
+
561
+ > **Why this exists.** A team with `.serverOnly().sensitive('secret')` columns and correctly-redacting plugins got 69 rows flagged, with *purge them AND rotate the credentials* underneath, against the framework's own proof that nothing was stored. The shape mattered more than the one key name: the more columns an app classifies correctly, the more markers it writes, and the redder the scan turns. A false-positive rate that rises with the care you take gets the tool muted — and a muted scan is worth less than none, because its silence still reads as evidence.
562
+
118
563
  > **If you ran this on 0.30.0, 0.30.1 or 0.30.2, run it again.** Those releases scanned `subjectId` on both tables — a flat opaque id that cannot hold a credential — and none of the blob columns above. The command ran cleanly, printed a scanned count beside a hit count, and exited `0` having never looked where credentials are. It came from fixing a crash: the default column had been the literal `subject` for every table, `_voltro_row_history` has no such column, and the fix replaced the name on *both* tables instead of the one that was wrong. A clean answer from a scan that looked in the wrong place is worse than the crash it replaced. `voltro update` prints this as a manual step on the way to 0.31.0.
119
564
 
120
565
  **It is a command and not a documented query on purpose.** The same check once shipped as SQL you were asked to run yourself, in its postgres spelling (`subject::text ILIKE '%token%'`). On MySQL/MariaDB the natural translation is a bare `LIKE` — and against the `utf8mb4_bin` collation the migrator emits for a `json()` column, `LIKE` is case-**sensitive**. So `'%token%'` does not match `jiraToken`, and almost every JSON key that carries a credential is camelCase. A team ran the translated query over 141 rows, got `0`, and nearly reported themselves clean; 117 of those rows held a working credential. Every dialect now casts to its own text type before lowering, in code you do not have to translate.
@@ -125,6 +570,8 @@ Run it on every environment. A development database is not a sample of productio
125
570
 
126
571
  If it finds something: purge the rows **and** rotate the credentials — assume anything written to a log table has been read — then move the credential off the Subject entirely with `connectionCredentials(...)`, which keeps it in the framework vault.
127
572
 
573
+ If the finding is a credential column of your own sitting in plaintext, `.encrypted()` only protects rows written *after* you add it — [`voltro db encrypt-column`](/docs/database/sensitivity) converts the ones already there.
574
+
128
575
  ## Supply-chain assurance
129
576
 
130
577
  Every release passes automated supply-chain gates in CI before a single package is published: