@voltro/cli 0.33.0 → 0.34.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (337) hide show
  1. package/CHANGELOG.md +1801 -0
  2. package/bin/nodeEnvironment.d.mts +30 -0
  3. package/bin/nodeEnvironment.mjs +158 -0
  4. package/bin/voltro.mjs +69 -5
  5. package/dist/addCommand-BNeoeSxe.js +124 -0
  6. package/dist/addCommand-aXSQveak.js +2 -0
  7. package/dist/agentsMd-BTchIZku.js +2 -0
  8. package/dist/agentsMd-mhQMF1bx.js +254 -0
  9. package/dist/apiBuild-BICVGnEC.js +2 -0
  10. package/dist/{apiBuild-h9VHtnlw.js → apiBuild-DZF_f0_I.js} +46 -46
  11. package/dist/appPort-B_HpJ_ck.js +48 -0
  12. package/dist/baselineCommand-C2ClWZN3.js +2 -0
  13. package/dist/baselineCommand-DIttzO8A.js +227 -0
  14. package/dist/bin.js +71 -28
  15. package/dist/build-CE7Ca9gm.js +711 -0
  16. package/dist/cacheCommand-DA4OH9xt.js +42 -0
  17. package/dist/capabilitiesCommand-nq_pz5xd.js +123 -0
  18. package/dist/checkCommand-Dv8r9tGO.js +231 -0
  19. package/dist/checkCommand-YZDmkAGd.js +2 -0
  20. package/dist/{cliArgs-qdZSElM3.js → cliArgs-D4p8n7EE.js} +12 -1
  21. package/dist/cliError-BmdYnghb.js +10 -0
  22. package/dist/cliOutput-D1tSBoRM.js +15 -0
  23. package/dist/{cliRuntime-Oh517vCV.js → cliRuntime-Dh7UDinH.js} +20 -20
  24. package/dist/cloudClient-DWL-Hw_T.js +67 -0
  25. package/dist/cloudCmd-Cvv5HGaZ.js +364 -0
  26. package/dist/clusterCmd-CNXKlcvD.js +54 -0
  27. package/dist/codegen-CYM3Zqrf.js +605 -0
  28. package/dist/codegen-ChBi_hVa.js +2 -0
  29. package/dist/codegenCommand-DC6w2tNZ.js +30 -0
  30. package/dist/codemodRunner-DRRqXR74.js +5243 -0
  31. package/dist/commandRunner-BLAEFLjp.js +47 -0
  32. package/dist/commands-gutsz-Ac.js +808 -0
  33. package/dist/connectionConfig-UFlIEiys.js +66 -0
  34. package/dist/dashboardCommand-3YG8p-UA.js +25 -0
  35. package/dist/dataCommand-qL0r7fPO.js +535 -0
  36. package/dist/dataProfile-dW-PsfLB.js +15 -0
  37. package/dist/{dbCommand-DTLKAfbA.js → dbCommand-B6X0FZbq.js} +551 -401
  38. package/dist/dbCommand-CpdKLeQq.js +2 -0
  39. package/dist/{dev-C_P8FLSx.js → dev-CerMd0mW.js} +3122 -3129
  40. package/dist/dev-CoG-ZPx8.js +3 -0
  41. package/dist/devActivity-Dx_3nnGv.js +100 -0
  42. package/dist/devActivity.js +1 -1
  43. package/dist/dialectDriver-CgXnDfec.js +39 -0
  44. package/dist/discover-C9XKJDco.js +25 -0
  45. package/dist/doctorCommand-BvqGBwNG.js +2 -0
  46. package/dist/{checkCommand-xGhRFFg2.js → doctorCommand-CnDXQxfa.js} +520 -1278
  47. package/dist/dormancyCommand-Dszo57d6.js +69 -0
  48. package/dist/e2eCmd-BRabZww-.js +147 -0
  49. package/dist/embeddingsCommand-C0sKVRo1.js +73 -0
  50. package/dist/envCommand-DPgdV1Bq.js +60 -0
  51. package/dist/evalCommand-6RUfPen4.js +118 -0
  52. package/dist/evolveCommand-DHpkgjgH.js +281 -0
  53. package/dist/fileTaxonomy-CJfgOllU.js +457 -0
  54. package/dist/frameworkTableAssembly-BGHmck-x.js +2 -0
  55. package/dist/{frameworkTableAssembly-BwIrO5nv.js → frameworkTableAssembly-DkkP6BgC.js} +184 -148
  56. package/dist/generateCommand-oibemh97.js +147 -0
  57. package/dist/index.d.ts +45 -0
  58. package/dist/index.js +4 -3
  59. package/dist/infoCommand-BJw9nLUR.js +60 -0
  60. package/dist/{inspect-BUUjt773.js → inspect-CBqFtAKA.js} +82 -40
  61. package/dist/inspect-C_T_WGvl.js +2 -0
  62. package/dist/inspectCmd-Bppy-GGw.js +224 -0
  63. package/dist/inspectFetch-Cm8_wVvp.js +151 -0
  64. package/dist/inspectMetrics-CfdKLh6t.js +72 -0
  65. package/dist/loadEnv-D9nEOClM.js +44 -0
  66. package/dist/logFileSink-C_D2wRN1.js +105 -0
  67. package/dist/logsCmd-CCca3KRZ.js +260 -0
  68. package/dist/manifestBuild-ChsKAhmn.js +2 -0
  69. package/dist/{manifestBuild-BLrVuSlM.js → manifestBuild-sxpwdKY1.js} +1 -1
  70. package/dist/metaCommands-7MJfZ5cf.js +196 -0
  71. package/dist/migrate-CBwOt_iV.js +83 -0
  72. package/dist/mssqlClusterPatch-_4cE_nun.js +44 -0
  73. package/dist/newCommand-COWOJ1_E.js +156 -0
  74. package/dist/nodeEnvironment-cGFAj1J8.js +28 -0
  75. package/dist/packageCommand-Cug_3Ogl.js +271 -0
  76. package/dist/pageConvention-cEiRxdab.js +5 -0
  77. package/dist/privacyCommand-C-Df56U_.js +146 -0
  78. package/dist/projectScaffold-DmzEKHib.js +2 -0
  79. package/dist/projectScaffold-LMMtaavR.js +814 -0
  80. package/dist/renderModeScan-D7J1B7Kw.js +105 -0
  81. package/dist/renderProfile-1OWWAAtx.js +81 -0
  82. package/dist/runtimeRegistry-DMeKfTHP.js +81 -0
  83. package/dist/runtimeTrace-CRxalXTs.js +91 -0
  84. package/dist/scheduleCmd--jksTrf6.js +69 -0
  85. package/dist/scheduleManifestCmd-D2x0CTTY.js +249 -0
  86. package/dist/schemaIr-UJybUUZW.js +103 -0
  87. package/dist/{sdkgen-C81QIkiL.js → sdkgen-CYJscZC7.js} +111 -209
  88. package/dist/seedRunner-TFHHiToI.js +329 -0
  89. package/dist/serveCommand-B_isw7q4.js +1647 -0
  90. package/dist/serveCommand-DOvbgRnQ.js +2 -0
  91. package/dist/serveEntry.js +5 -5
  92. package/dist/serverlessCommand-CfJZy6dS.js +482 -0
  93. package/dist/start-9LiUOfES.js +1087 -0
  94. package/dist/start-B-9Nsp-S.js +3 -0
  95. package/dist/startEntry.js +2 -2
  96. package/dist/staticCommand-Dr2M6tpU.js +304 -0
  97. package/dist/storageCommand-Co6NfLqN.js +42 -0
  98. package/dist/templates-De8IR5-c.js +102 -0
  99. package/dist/test-CI6iDsYc.js +115 -0
  100. package/dist/tracesCmd-CkEZQrtt.js +232 -0
  101. package/dist/tsconfigPaths-BWXBWgcl.js +107 -0
  102. package/dist/tsxLoader-EuXmSJ1K.js +51 -0
  103. package/dist/typecheckCommand-BlsWiCNq.js +61 -0
  104. package/dist/updateCommand-Bkptutss.js +585 -0
  105. package/dist/updateCommand-us1_hdIC.js +2 -0
  106. package/dist/{inspectMetrics-1xzTKAFx.js → webDev-CBYvPqQr.js} +975 -1559
  107. package/dist/webDev-Cg-fFiyd2.js +2 -0
  108. package/dist/webhookDiscovery-CrGAfhIG.js +2 -0
  109. package/dist/webhookDiscovery-D7VaeMlz.js +51 -0
  110. package/dist/webhooksCommand-CID96Rga.js +267 -0
  111. package/dist/workflowsCmd-D1VTmLMY.js +608 -0
  112. package/package.json +179 -17
  113. package/templates/AGENTS.core.md +58 -3
  114. package/templates/AGENTS.md +64 -7
  115. package/templates/agent-docs/_index.md +6 -4
  116. package/templates/agent-docs/_manifest.json +22 -5
  117. package/templates/agent-docs/ai.md +370 -0
  118. package/templates/agent-docs/authentication.md +265 -31
  119. package/templates/agent-docs/caching.md +6 -0
  120. package/templates/agent-docs/cli.md +794 -50
  121. package/templates/agent-docs/data.md +550 -11
  122. package/templates/agent-docs/database/migrations.md +174 -25
  123. package/templates/agent-docs/database/misc.md +156 -40
  124. package/templates/agent-docs/database/querying.md +19 -1
  125. package/templates/agent-docs/database/scaling.md +60 -0
  126. package/templates/agent-docs/database/schema.md +1 -0
  127. package/templates/agent-docs/database/seedsdialects.md +208 -19
  128. package/templates/agent-docs/database/transactions.md +68 -0
  129. package/templates/agent-docs/deployment.md +69 -3
  130. package/templates/agent-docs/introduction.md +87 -16
  131. package/templates/agent-docs/local-first-mobile.md +79 -4
  132. package/templates/agent-docs/multi-tenancy.md +95 -20
  133. package/templates/agent-docs/observability.md +58 -3
  134. package/templates/agent-docs/plugins/ai-flows.md +161 -2
  135. package/templates/agent-docs/plugins/analytics-postgres.md +1 -1
  136. package/templates/agent-docs/plugins/audit.md +37 -1
  137. package/templates/agent-docs/plugins/auth-social.md +143 -0
  138. package/templates/agent-docs/plugins/auth-workos.md +4 -2
  139. package/templates/agent-docs/plugins/auth.md +131 -6
  140. package/templates/agent-docs/plugins/billing.md +132 -15
  141. package/templates/agent-docs/plugins/cdc-out.md +46 -7
  142. package/templates/agent-docs/plugins/clickhouse.md +1 -1
  143. package/templates/agent-docs/plugins/duckdb.md +1 -1
  144. package/templates/agent-docs/plugins/flags.md +132 -0
  145. package/templates/agent-docs/plugins/governance.md +105 -7
  146. package/templates/agent-docs/plugins/multitenancy.md +9 -4
  147. package/templates/agent-docs/plugins/presence.md +13 -2
  148. package/templates/agent-docs/plugins/ratelimit.md +9 -0
  149. package/templates/agent-docs/plugins/search.md +157 -6
  150. package/templates/agent-docs/plugins/sso-saml.md +47 -8
  151. package/templates/agent-docs/plugins/webhooks.md +105 -0
  152. package/templates/agent-docs/plugins.md +150 -18
  153. package/templates/agent-docs/reference.md +60 -3
  154. package/templates/agent-docs/releases.md +1117 -0
  155. package/templates/agent-docs/routing.md +43 -25
  156. package/templates/agent-docs/scheduling.md +10 -0
  157. package/templates/agent-docs/schema-driven-ui.md +92 -12
  158. package/templates/agent-docs/security.md +426 -0
  159. package/templates/agent-docs/templates/apibackends.md +87 -18
  160. package/templates/agent-docs/templates/appshells.md +32 -14
  161. package/templates/agent-docs/templates/overview.md +13 -8
  162. package/templates/agent-docs/testing.md +211 -14
  163. package/templates/agent-docs/whats-new.md +1700 -104
  164. package/templates/agent-docs/workflows.md +130 -14
  165. package/templates/apps/api-ai/actions/summarize.action.ts +11 -0
  166. package/templates/apps/api-ai/package.json +8 -7
  167. package/templates/apps/api-auth/actions/me.action.ts +13 -0
  168. package/templates/apps/api-auth/package.json +9 -8
  169. package/templates/apps/api-backend/mutations/notes.create.mutation.ts +9 -0
  170. package/templates/apps/api-backend/package.json +12 -8
  171. package/templates/apps/api-backend/queries/notes.query.ts +30 -8
  172. package/templates/apps/api-backend-deactivation/actions/users.get.action.ts +14 -0
  173. package/templates/apps/api-backend-deactivation/mutations/users.create.mutation.ts +8 -0
  174. package/templates/apps/api-backend-deactivation/mutations/users.deactivate.mutation.ts +10 -0
  175. package/templates/apps/api-backend-deactivation/package.json +8 -7
  176. package/templates/apps/api-backend-mail/actions/sendWelcome.action.ts +17 -0
  177. package/templates/apps/api-backend-mail/mutations/notes.create.mutation.ts +9 -0
  178. package/templates/apps/api-backend-mail/package.json +9 -8
  179. package/templates/apps/api-backend-mail/queries/notes.query.ts +30 -8
  180. package/templates/apps/api-backend-mariadb/.env.example +14 -0
  181. package/templates/apps/api-backend-mariadb/mutations/notes.create.mutation.ts +9 -0
  182. package/templates/apps/api-backend-mariadb/package.json +10 -9
  183. package/templates/apps/api-backend-mariadb/queries/notes.query.ts +30 -8
  184. package/templates/apps/api-backend-sqlite/.env.example +14 -0
  185. package/templates/apps/api-backend-sqlite/mutations/notes.create.mutation.ts +9 -0
  186. package/templates/apps/api-backend-sqlite/package.json +9 -8
  187. package/templates/apps/api-backend-sqlite/queries/notes.query.ts +30 -8
  188. package/templates/apps/api-backend-storage/actions/uploadAvatar.action.ts +13 -0
  189. package/templates/apps/api-backend-storage/actions/uploadDocument.action.ts +12 -0
  190. package/templates/apps/api-backend-storage/mutations/notes.create.mutation.ts +9 -0
  191. package/templates/apps/api-backend-storage/package.json +9 -8
  192. package/templates/apps/api-backend-storage/queries/notes.query.ts +30 -8
  193. package/templates/apps/api-cms/actions/content.get.action.ts +7 -0
  194. package/templates/apps/api-cms/actions/content.types.action.ts +6 -0
  195. package/templates/apps/api-cms/actions/me.action.ts +13 -0
  196. package/templates/apps/api-cms/app.config.ts +19 -0
  197. package/templates/apps/api-cms/authz.ts +63 -0
  198. package/templates/apps/api-cms/mutations/content.publish.mutation.ts +10 -0
  199. package/templates/apps/api-cms/mutations/content.saveDraft.mutation.ts +10 -0
  200. package/templates/apps/api-cms/mutations/content.unpublish.mutation.ts +5 -0
  201. package/templates/apps/api-cms/package.json +11 -10
  202. package/templates/apps/api-cms/queries/content.list.query.ts +23 -7
  203. package/templates/apps/api-cms/tests/accessDecisions.test.ts +121 -0
  204. package/templates/apps/api-cms/tests/content.descriptors.test.ts +8 -4
  205. package/templates/apps/api-collab/mutations/documents.create.mutation.ts +8 -0
  206. package/templates/apps/api-collab/mutations/documents.setBody.mutation.ts +13 -0
  207. package/templates/apps/api-collab/package.json +9 -8
  208. package/templates/apps/api-collab/queries/documents.query.ts +19 -7
  209. package/templates/apps/api-data-advanced/package.json +9 -8
  210. package/templates/apps/api-data-advanced/queries/authors.withBooks.query.ts +12 -0
  211. package/templates/apps/api-data-advanced/queries/books.search.query.ts +9 -0
  212. package/templates/apps/api-durable/mutations/orders.approve.mutation.ts +17 -0
  213. package/templates/apps/api-durable/mutations/orders.place.mutation.ts +9 -0
  214. package/templates/apps/api-durable/package.json +9 -8
  215. package/templates/apps/api-feature-flags/actions/notes.export.action.ts +16 -0
  216. package/templates/apps/api-feature-flags/mutations/notes.create.mutation.ts +12 -0
  217. package/templates/apps/api-feature-flags/package.json +10 -9
  218. package/templates/apps/api-governance/README.md +30 -8
  219. package/templates/apps/api-governance/actions/profiles.get.action.server.ts +17 -1
  220. package/templates/apps/api-governance/actions/profiles.get.action.ts +29 -4
  221. package/templates/apps/api-governance/database/schema.ts +16 -4
  222. package/templates/apps/api-governance/mutations/profiles.create.mutation.ts +12 -0
  223. package/templates/apps/api-governance/package.json +9 -8
  224. package/templates/apps/api-kv/actions/sync.pull.action.ts +15 -0
  225. package/templates/apps/api-kv/actions/sync.reset.action.ts +13 -0
  226. package/templates/apps/api-kv/actions/sync.status.action.ts +7 -0
  227. package/templates/apps/api-kv/package.json +9 -8
  228. package/templates/apps/api-kv/queries/events.list.query.ts +19 -8
  229. package/templates/apps/api-moderation/mutations/comments.create.mutation.ts +11 -0
  230. package/templates/apps/api-moderation/mutations/posts.create.mutation.ts +13 -0
  231. package/templates/apps/api-moderation/package.json +9 -8
  232. package/templates/apps/api-observability/mutations/notes.create.mutation.ts +8 -0
  233. package/templates/apps/api-observability/package.json +9 -8
  234. package/templates/apps/api-observability/queries/notes.list.query.ts +13 -0
  235. package/templates/apps/api-ratelimit/mutations/notes.create.mutation.ts +14 -0
  236. package/templates/apps/api-ratelimit/package.json +9 -8
  237. package/templates/apps/api-rbac/package.json +9 -8
  238. package/templates/apps/api-rest/package.json +8 -7
  239. package/templates/apps/api-saas/mutations/projects.create.mutation.ts +13 -0
  240. package/templates/apps/api-saas/package.json +12 -11
  241. package/templates/apps/api-saas/queries/projects.list.query.ts +11 -0
  242. package/templates/apps/api-saas-starter/actions/me.action.ts +13 -0
  243. package/templates/apps/api-saas-starter/app.config.ts +19 -0
  244. package/templates/apps/api-saas-starter/authz.ts +75 -0
  245. package/templates/apps/api-saas-starter/mutations/invites.create.mutation.ts +10 -0
  246. package/templates/apps/api-saas-starter/mutations/projects.create.mutation.ts +9 -0
  247. package/templates/apps/api-saas-starter/package.json +15 -11
  248. package/templates/apps/api-saas-starter/queries/invites.list.query.ts +19 -8
  249. package/templates/apps/api-saas-starter/queries/projects.list.query.ts +18 -7
  250. package/templates/apps/api-saas-starter/tests/accessDecisions.test.ts +135 -0
  251. package/templates/apps/api-search/mutations/articles.create.mutation.ts +14 -0
  252. package/templates/apps/api-search/package.json +9 -8
  253. package/templates/apps/api-search/queries/articles.list.query.ts +21 -8
  254. package/templates/apps/api-status/README.md +10 -3
  255. package/templates/apps/api-status/app.config.ts +8 -3
  256. package/templates/apps/api-status/authz.ts +5 -3
  257. package/templates/apps/api-status/package.json +9 -8
  258. package/templates/apps/api-status/queries/components.list.query.ts +14 -6
  259. package/templates/apps/api-status/queries/incidents.live.query.ts +23 -10
  260. package/templates/apps/api-status/queries/updates.list.query.ts +16 -9
  261. package/templates/apps/api-status/tests/status.test.ts +9 -1
  262. package/templates/apps/api-versioning/actions/documents.asOf.action.ts +12 -0
  263. package/templates/apps/api-versioning/actions/documents.history.action.ts +13 -0
  264. package/templates/apps/api-versioning/mutations/documents.create.mutation.ts +9 -0
  265. package/templates/apps/api-versioning/mutations/documents.update.mutation.ts +12 -0
  266. package/templates/apps/api-versioning/package.json +9 -8
  267. package/templates/apps/api-webhooks/mutations/orders.fulfill.mutation.ts +16 -0
  268. package/templates/apps/api-webhooks/package.json +10 -9
  269. package/templates/apps/api-webhooks/queries/orders.list.query.ts +10 -0
  270. package/templates/apps/changelog/package.json +8 -6
  271. package/templates/apps/edge-functions/package.json +2 -2
  272. package/templates/apps/frontend-admin/package.json +10 -8
  273. package/templates/apps/frontend-admin/src/lib/admin.ts +20 -9
  274. package/templates/apps/frontend-admin/src/locales/de.ts +11 -1
  275. package/templates/apps/frontend-admin/src/locales/en.ts +13 -1
  276. package/templates/apps/frontend-admin/src/pages/admin/[entity]/page.tsx +65 -22
  277. package/templates/apps/frontend-admin/src/pages/admin/entity.test.tsx +130 -23
  278. package/templates/apps/frontend-admin/src/pages/admin/layout.tsx +3 -2
  279. package/templates/apps/frontend-admin/src/pages/admin/page.test.tsx +19 -2
  280. package/templates/apps/frontend-admin/src/pages/admin/page.tsx +9 -4
  281. package/templates/apps/frontend-app/app.config.ts +4 -3
  282. package/templates/apps/frontend-app/package.json +11 -8
  283. package/templates/apps/frontend-app/src/lib/api.ts +25 -0
  284. package/templates/apps/frontend-app/src/pages/page.test.tsx +130 -82
  285. package/templates/apps/frontend-app/src/pages/page.tsx +14 -18
  286. package/templates/apps/frontend-auth/package.json +10 -8
  287. package/templates/apps/frontend-blank/package.json +9 -7
  288. package/templates/apps/frontend-cms/package.json +11 -9
  289. package/templates/apps/frontend-collab/package.json +12 -9
  290. package/templates/apps/frontend-collab/src/pages/page.test.tsx +122 -78
  291. package/templates/apps/frontend-contact/package.json +9 -7
  292. package/templates/apps/frontend-dashboard/package.json +9 -7
  293. package/templates/apps/frontend-docs/package.json +9 -7
  294. package/templates/apps/frontend-i18n/package.json +8 -6
  295. package/templates/apps/frontend-landing/package.json +9 -7
  296. package/templates/apps/frontend-portal/package.json +10 -8
  297. package/templates/apps/frontend-saas/app.config.ts +10 -6
  298. package/templates/apps/frontend-saas/package.json +10 -8
  299. package/templates/apps/frontend-saas/src/lib/api.ts +27 -32
  300. package/templates/apps/frontend-saas/src/pages/dashboard/billing/page.tsx +7 -8
  301. package/templates/apps/frontend-saas/src/pages/dashboard/page.test.tsx +27 -3
  302. package/templates/apps/frontend-saas/src/pages/dashboard/page.tsx +4 -4
  303. package/templates/apps/frontend-saas/src/pages/dashboard/team/page.tsx +3 -4
  304. package/templates/apps/frontend-spa/package.json +9 -7
  305. package/templates/apps/frontend-ssr/package.json +9 -7
  306. package/templates/apps/frontend-ssr-api/package.json +10 -8
  307. package/templates/apps/frontend-static-blog/package.json +8 -6
  308. package/templates/apps/frontend-status/package.json +10 -8
  309. package/templates/apps/mobile-app/README.md +1 -0
  310. package/templates/apps/mobile-app/package.json +4 -2
  311. package/templates/apps/mobile-app/src/app/index.tsx +22 -12
  312. package/templates/apps/mobile-app/src/app/orders/[id].tsx +1 -1
  313. package/templates/apps/mobile-app/src/lib/api.ts +34 -0
  314. package/templates/apps/mobile-app/voltro.mobile.ts +4 -2
  315. package/templates/baselines/bare/.env.example +14 -0
  316. package/templates/baselines/bare/baseline.json +4 -4
  317. package/templates/baselines/compose/.env.example +14 -0
  318. package/templates/baselines/compose/README.md +1 -1
  319. package/templates/baselines/compose/baseline.json +5 -5
  320. package/templates/baselines/compose-mariadb/.env.example +14 -0
  321. package/templates/baselines/compose-mariadb/README.md +1 -1
  322. package/templates/baselines/compose-mariadb/baseline.json +5 -5
  323. package/templates/baselines/helm/.env.example +14 -0
  324. package/templates/baselines/helm/baseline.json +4 -4
  325. package/dist/apiBuild-C-x9YacA.js +0 -2
  326. package/dist/checkCommand-DRovTKza.js +0 -2
  327. package/dist/commands-CJfepbm4.js +0 -11541
  328. package/dist/dbCommand-b1gum4td.js +0 -2
  329. package/dist/dev-iiMtlkfs.js +0 -3
  330. package/dist/devActivity-BhIu6ncs.js +0 -159
  331. package/dist/frameworkTableAssembly-D-EebUQX.js +0 -2
  332. package/dist/inspect-mmBuRXmy.js +0 -2
  333. package/dist/manifestBuild-Dj8Jjoto.js +0 -2
  334. package/dist/seedRunner-Bqxgp7HZ.js +0 -230
  335. package/dist/serveCommand-DdaM4Hup.js +0 -1608
  336. package/dist/start-C0koT0UO.js +0 -1084
  337. /package/templates/apps/api-ai/actions/{summarize.action.server.tsx → summarize.action.server.ts} +0 -0
@@ -0,0 +1,121 @@
1
+ // The app's ACCESS POSTURE, asserted from BOTH sides against the real resolver.
2
+ //
3
+ // Why this file exists rather than one guard assertion per handler test: a
4
+ // per-procedure test hands `invoke` a subject it constructed itself, so it
5
+ // passes with a scope literal no real caller could ever obtain. The question it
6
+ // structurally cannot ask is the one that matters —
7
+ // **can the caller this app actually has satisfy these guards, and is there
8
+ // anything that still cannot?** Both halves, or the suite stays green on an app
9
+ // that is either wide open or completely dead.
10
+ //
11
+ // The second failure is the easy one to ship by accident. A guard nobody can
12
+ // satisfy is not strict security; it is a 100% outage wearing security's
13
+ // clothes, and every unit test in the directory keeps passing while the editor
14
+ // answers `ScopeError` to its own sign-in flow. So the subjects below are built
15
+ // the way the framework builds them (`applyScopeDecision` over this app's real
16
+ // `resolveScopes`), never by writing the scopes the assertions want to see.
17
+
18
+ import { describe, it, expect } from 'vitest'
19
+ import {
20
+ anonymousSubject,
21
+ applyScopeDecision,
22
+ checkGuards,
23
+ ScopeError,
24
+ type Subject,
25
+ } from '@voltro/protocol'
26
+ import { EDITOR_SCOPES, resolveScopes } from '../authz'
27
+
28
+ import { listContent } from '../queries/content.list.query'
29
+ import { getContent } from '../actions/content.get.action'
30
+ import { contentTypes } from '../actions/content.types.action'
31
+ import { me } from '../actions/me.action'
32
+ import { saveDraft } from '../mutations/content.saveDraft.mutation'
33
+ import { publishContent } from '../mutations/content.publish.mutation'
34
+ import { unpublishContent } from '../mutations/content.unpublish.mutation'
35
+
36
+ /** The Subject a browser with no session cookie gets: `voltroPasswordStrategy`
37
+ * finds nothing to match, so the composer falls back to anonymous. */
38
+ const visitor = anonymousSubject('acme')
39
+
40
+ /** The Subject a signed-in editor gets. `voltroPasswordStrategy` matches the
41
+ * cookie into a `user` Subject carrying NO scopes (a cookie never carries
42
+ * authority), then the composer applies this app's own `resolveScopes` to it.
43
+ * Built through the framework's `applyScopeDecision` so this file cannot
44
+ * disagree with the runtime about how a grant lands on a subject. */
45
+ const editor = (): Subject => {
46
+ const matched: Subject = { type: 'user', id: 'usr_editor', tenantId: 'acme', scopes: [] }
47
+ return applyScopeDecision(matched, { kind: 'grant', scopes: resolveScopes(matched) }).subject
48
+ }
49
+
50
+ /** Exactly what the dispatch spine asks before an executor runs. */
51
+ const decide = (
52
+ descriptor: { readonly name: string; readonly guards?: ReadonlyArray<unknown> | undefined },
53
+ subject: Subject,
54
+ ): ScopeError | null =>
55
+ checkGuards(
56
+ subject,
57
+ descriptor.guards as Parameters<typeof checkGuards>[1],
58
+ { defaultDeny: true, procedure: descriptor.name },
59
+ )
60
+
61
+ const GUARDED = [listContent, getContent, contentTypes, saveDraft, publishContent, unpublishContent] as const
62
+
63
+ describe('the caller this app actually has — a signed-in editor', () => {
64
+ // If any of these regress the editor boots and shows nothing but denials,
65
+ // which no other test in this directory can see.
66
+ it.each(GUARDED.map((d) => [d.name, d] as const))('%s is callable', (_name, descriptor) => {
67
+ expect(decide(descriptor, editor())).toBeNull()
68
+ })
69
+
70
+ it('is not just passing because the guards are empty', () => {
71
+ // The silent-zero shape: `checkGuards` with no guards would return null too.
72
+ // Assert the decisions are really there before trusting the line above.
73
+ for (const d of GUARDED) expect(d.guards?.length ?? 0).toBeGreaterThan(0)
74
+ expect(GUARDED).toHaveLength(6)
75
+ })
76
+ })
77
+
78
+ describe('the caller with no session — every content procedure is refused', () => {
79
+ it.each(GUARDED.map((d) => [d.name, d] as const))('%s denies an anonymous visitor', (_name, descriptor) => {
80
+ expect(decide(descriptor, visitor)).toBeInstanceOf(ScopeError)
81
+ })
82
+
83
+ it('…and that is structural, not a resolver quirk', () => {
84
+ // The reason an anonymous caller can never hold `content:write` in this app,
85
+ // pinned so a framework change would surface here: `applyScopeDecision`
86
+ // returns an anonymous Subject UNTOUCHED under both decision kinds (the
87
+ // schema has no `scopes` field to write). `auth.resolveScopes` also never
88
+ // runs for one — it fires only on a MATCHED subject — so a cookie is the
89
+ // only way into the granted set.
90
+ const granted = applyScopeDecision(visitor, { kind: 'grant', scopes: [...EDITOR_SCOPES] })
91
+ expect(granted.subject).toEqual(visitor)
92
+ expect(decide(saveDraft, granted.subject)).toBeInstanceOf(ScopeError)
93
+ })
94
+ })
95
+
96
+ describe('session.me stays open, and that is the point', () => {
97
+ it('answers an anonymous caller instead of denying them', () => {
98
+ // A guard here would return `ScopeError` to the SSR gate, which would then
99
+ // have nothing to branch on and would never redirect to /login.
100
+ expect(decide(me, visitor)).toBeNull()
101
+ expect(me.openAccess).toBeTruthy()
102
+ })
103
+ })
104
+
105
+ describe('the vocabulary is a boundary, not a rubber stamp', () => {
106
+ it('grants no wildcard and no admin bypass', () => {
107
+ expect(EDITOR_SCOPES).not.toContain('*')
108
+ expect(EDITOR_SCOPES).not.toContain('admin:full')
109
+ })
110
+
111
+ it('every guard names a scope the resolver actually grants', () => {
112
+ // The other half of unsatisfiability: a typo'd scope makes a procedure
113
+ // permanently uncallable, and nothing else in this app would notice.
114
+ const granted = new Set(resolveScopes(editor()))
115
+ for (const d of GUARDED) {
116
+ for (const g of (d.guards ?? []) as ReadonlyArray<{ scope?: string }>) {
117
+ expect(granted.has(g.scope ?? '')).toBe(true)
118
+ }
119
+ }
120
+ })
121
+ })
@@ -30,11 +30,15 @@ describe('content.list descriptor', () => {
30
30
  expect(decodeIn({ type: 'blogPost', status: 'draft' })).toEqual({ type: 'blogPost', status: 'draft' })
31
31
  // The output carries only what a list view needs — id/status/title?/slug?/
32
32
  // updatedAt; the runtime drops a row's other columns against this shape.
33
+ // It is an ARRAY schema (a query delivers rows, not a row), so encode a
34
+ // list and read the first element — passing a bare object here typechecked
35
+ // as the array type and made `wire.id` a compile error that `voltro test`
36
+ // never sees, because it transpiles without type-checking.
33
37
  const encode = Schema.encodeUnknownSync(listContent.output)
34
- const wire = encode({ id: 'p1', status: 'published', title: 'T', slug: 't', updatedAt: new Date(0) })
35
- expect(wire.id).toBe('p1')
36
- expect(wire.status).toBe('published')
37
- expect(wire.title).toBe('T')
38
+ const [wire] = encode([{ id: 'p1', status: 'published', title: 'T', slug: 't', updatedAt: new Date(0) }])
39
+ expect(wire?.id).toBe('p1')
40
+ expect(wire?.status).toBe('published')
41
+ expect(wire?.title).toBe('T')
38
42
  })
39
43
  })
40
44
 
@@ -9,6 +9,14 @@ import { Schema } from 'effect'
9
9
 
10
10
  export const createDocument = defineMutation({
11
11
  name: 'documents.create',
12
+ // Open: inserts a title the caller supplied into the caller's own tenant
13
+ // (`assertOwnTenant` rejects a mismatched `tenantId`) and touches no existing
14
+ // row. With no auth strategy configured every caller is an anonymous Subject
15
+ // holding no scopes, so a `guards: [{ scope }]` would deny all of them.
16
+ openAccess:
17
+ 'inserts a new document (title only, empty CRDT body) into the caller\'s own tenant; '
18
+ + '`assertOwnTenant` rejects a mismatch and no existing row is touched. No auth strategy '
19
+ + 'ships here, so there is no identity a scope guard could name.',
12
20
  target: {
13
21
  table: 'documents',
14
22
  op: 'insert',
@@ -16,6 +16,19 @@ import { Schema } from 'effect'
16
16
  export const setDocumentBody = defineMutation({
17
17
  name: 'documents.setBody',
18
18
  target: { table: 'documents', op: 'update' },
19
+ // Open, and note WHAT the CRDT changes about the risk: the server does not
20
+ // overwrite `body` with the caller's bytes, it MERGES them
21
+ // (`mergeCrdtStates`), so a concurrent editor's text is not destroyed by a
22
+ // conflicting write — the two converge. `tenant()` scopes the update, so an
23
+ // id from another tenant matches no row at all.
24
+ //
25
+ // What remains open is who may join an editing session, and that needs an
26
+ // identity this template does not ship: with no auth strategy every caller is
27
+ // an anonymous Subject holding no scopes, so a `guards:` would deny everyone.
28
+ openAccess:
29
+ 'merges a caller-supplied CRDT update into a document of the request\'s tenant — a merge, '
30
+ + 'not an overwrite, so no concurrent editor\'s text is lost, and an id outside the tenant '
31
+ + 'matches no row. Guard who may edit once the app has an identity.',
19
32
  input: Schema.Struct({
20
33
  id: Schema.NonEmptyString,
21
34
  update: Schema.Uint8ArrayFromBase64,
@@ -13,18 +13,19 @@
13
13
  "dependencies": {
14
14
  "@effect/platform": "^0.97.0",
15
15
  "@effect/rpc": "^0.76.0",
16
- "@voltro/cli": "0.33.0",
17
- "@voltro/database": "0.33.0",
18
- "@voltro/env": "0.33.0",
19
- "@voltro/local-first": "0.33.0",
20
- "@voltro/plugin-multitenancy": "0.33.0",
21
- "@voltro/protocol": "0.33.0",
22
- "@voltro/runtime": "0.33.0",
16
+ "@voltro/cli": "0.34.0",
17
+ "@voltro/database": "0.34.0",
18
+ "@voltro/env": "0.34.0",
19
+ "@voltro/local-first": "0.34.0",
20
+ "@voltro/plugin-multitenancy": "0.34.0",
21
+ "@voltro/protocol": "0.34.0",
22
+ "@voltro/runtime": "0.34.0",
23
23
  "effect": "^3.22.0"
24
24
  },
25
25
  "devDependencies": {
26
- "@voltro/testing": "0.33.0",
26
+ "@voltro/testing": "0.34.0",
27
27
  "typescript": "^6.0.3",
28
+ "@vitest/coverage-v8": "^4.1.10",
28
29
  "vitest": "^4.1.10"
29
30
  }
30
31
  }
@@ -12,12 +12,24 @@ import { Schema } from 'effect'
12
12
 
13
13
  export const listDocuments = defineQuery({
14
14
  name: 'documents.list',
15
+ // Open: `tenant()` confines every delivery to the tenantId on the request, so
16
+ // one tenant's CRDT state never reaches another's subscription — but this
17
+ // template configures no auth strategy, so that tenantId is the caller's own
18
+ // `x-tenant` header. It shapes the feed; it does not authorize the caller.
19
+ // A `guards: [{ scope }]` would be unsatisfiable here (an anonymous Subject
20
+ // holds no scopes) and would deny 100% of traffic instead of some of it.
21
+ openAccess:
22
+ 'streams the documents of the request\'s tenant, CRDT body included (`tenant()` scopes '
23
+ + 'every delivery). No auth strategy ships here, so the tenant comes from the caller\'s own '
24
+ + '`x-tenant` header — add a strategy, then a `guards:`.',
15
25
  input: Schema.Struct({}),
16
- output: Schema.Struct({
17
- id: Schema.String,
18
- title: Schema.String,
19
- body: Schema.NullOr(Schema.Uint8ArrayFromBase64),
20
- tenantId: Schema.String,
21
- createdAt: Schema.Date,
22
- }),
26
+ output: Schema.Array(
27
+ Schema.Struct({
28
+ id: Schema.String,
29
+ title: Schema.String,
30
+ body: Schema.NullOr(Schema.Uint8ArrayFromBase64),
31
+ tenantId: Schema.String,
32
+ createdAt: Schema.Date,
33
+ }),
34
+ ),
23
35
  })
@@ -12,18 +12,19 @@
12
12
  "dependencies": {
13
13
  "@effect/platform": "^0.97.0",
14
14
  "@effect/rpc": "^0.76.0",
15
- "@voltro/cli": "0.33.0",
16
- "@voltro/database": "0.33.0",
17
- "@voltro/env": "0.33.0",
18
- "@voltro/plugin-governance": "0.33.0",
19
- "@voltro/plugin-multitenancy": "0.33.0",
20
- "@voltro/protocol": "0.33.0",
21
- "@voltro/runtime": "0.33.0",
15
+ "@voltro/cli": "0.34.0",
16
+ "@voltro/database": "0.34.0",
17
+ "@voltro/env": "0.34.0",
18
+ "@voltro/plugin-governance": "0.34.0",
19
+ "@voltro/plugin-multitenancy": "0.34.0",
20
+ "@voltro/protocol": "0.34.0",
21
+ "@voltro/runtime": "0.34.0",
22
22
  "effect": "^3.22.0"
23
23
  },
24
24
  "devDependencies": {
25
- "@voltro/testing": "0.33.0",
25
+ "@voltro/testing": "0.34.0",
26
26
  "typescript": "^6.0.3",
27
+ "@vitest/coverage-v8": "^4.1.10",
27
28
  "vitest": "^4.1.10"
28
29
  }
29
30
  }
@@ -26,6 +26,18 @@ const Book = Schema.Struct({
26
26
  export const authorsWithBooks = defineQuery({
27
27
  name: 'authors.withBooks',
28
28
  source: ['authors', 'books'],
29
+ // Open: the rows are this template's own seeded catalogue
30
+ // (`seeds/catalog.seed.ts` — a public book list, no personal data), and
31
+ // `tenant()` on both tables scopes every delivery to the request's tenant.
32
+ //
33
+ // `bio` is the `.encrypted()` column and it IS in this output — deliberately,
34
+ // because the lesson is that `.encrypted()` is about bytes AT REST and says
35
+ // nothing about the wire. If a column must not leave the server, that is a
36
+ // different marker (`.serverOnly()`), not this one.
37
+ openAccess:
38
+ 'streams the seeded demo catalogue (authors + their books) for the request\'s tenant; the '
39
+ + 'seed holds no personal data. `bio` is `.encrypted()` and still crosses the wire on '
40
+ + 'purpose — encryption at rest is not an exposure marker; `.serverOnly()` is.',
29
41
  input: Schema.Struct({}),
30
42
  output: Schema.Struct({
31
43
  id: Schema.String,
@@ -24,6 +24,15 @@ import { Schema } from 'effect'
24
24
  export const searchBooks = defineQuery({
25
25
  name: 'books.search',
26
26
  source: 'books',
27
+ // Open: full-text search over this template's own seeded book catalogue
28
+ // (`seeds/catalog.seed.ts`), scoped to the request's tenant by `tenant()`.
29
+ // Note that `cache.scope: 'subject'` below is a SEPARATE decision from this
30
+ // one and stays right either way — the snapshot key includes the subject, so
31
+ // an open procedure still cannot serve one caller's rows to another.
32
+ openAccess:
33
+ 'full-text search over the seeded demo book catalogue, tenant-scoped; the seed holds no '
34
+ + 'personal data. The result cache is `scope: \'subject\'`, so an open read still cannot '
35
+ + 'serve one caller a snapshot built for another.',
27
36
  input: Schema.Struct({ q: Schema.String }),
28
37
  output: Schema.Struct({
29
38
  id: Schema.String,
@@ -11,6 +11,23 @@ import { Schema } from 'effect'
11
11
 
12
12
  export const approveOrder = defineMutation({
13
13
  name: 'orders.approve',
14
+ // This is the human-APPROVAL surface, so state plainly what "open" costs
15
+ // here rather than hiding it behind the word demo: whoever can call this
16
+ // decides whether an order ships. `assertOwnTenant` confines the signal to
17
+ // the caller's own tenant and the executionId is DERIVED from
18
+ // `{ orderId, tenantId }` (so no caller-supplied id can address another
19
+ // tenant's run), but nothing in this template distinguishes an approver from
20
+ // anyone else — it configures no auth strategy and no rbac, so an anonymous
21
+ // Subject with no scopes is the only caller that exists and a
22
+ // `guards: [{ scope: 'orders:approve' }]` would deny 100% of traffic.
23
+ //
24
+ // This is THE procedure in this template to guard first. Add an auth
25
+ // strategy (`api-auth`) or rbac (`api-rbac`), then that guard.
26
+ openAccess:
27
+ 'injects the approval signal into the caller\'s own tenant\'s parked fulfilment run — the '
28
+ + 'executionId is derived from `{ orderId, tenantId }` and `assertOwnTenant` rejects a '
29
+ + 'mismatch, so no other tenant\'s run is addressable. It still decides whether an order '
30
+ + 'ships: the first procedure here to grow an `orders:approve` guard once an identity exists.',
14
31
  input: Schema.Struct({
15
32
  orderId: Schema.String,
16
33
  tenantId: Schema.String,
@@ -12,6 +12,15 @@ import { Schema } from 'effect'
12
12
 
13
13
  export const placeOrder = defineMutation({
14
14
  name: 'orders.place',
15
+ // Open: writes an order carrying only what the caller sent, into the caller's
16
+ // own tenant (`assertOwnTenant` rejects a mismatched `tenantId`), and touches
17
+ // no existing row. No auth strategy and no rbac ship in this template, so
18
+ // every caller resolves to an anonymous Subject with no scopes and a
19
+ // `guards: [{ scope }]` would deny all of them.
20
+ openAccess:
21
+ 'inserts an order built only from caller-supplied fields into the caller\'s own tenant '
22
+ + '(`assertOwnTenant` rejects a mismatch); reads nothing and modifies no existing row. '
23
+ + 'No auth strategy ships here, so there is no identity a scope guard could name.',
15
24
  // Auto-optimistic: every query whose `source: 'orders'` matches gets a
16
25
  // placeholder row prepended; the server delta replaces it on commit.
17
26
  target: {
@@ -12,18 +12,19 @@
12
12
  "dependencies": {
13
13
  "@effect/platform": "^0.97.0",
14
14
  "@effect/rpc": "^0.76.0",
15
- "@voltro/cli": "0.33.0",
16
- "@voltro/database": "0.33.0",
17
- "@voltro/env": "0.33.0",
18
- "@voltro/plugin-multitenancy": "0.33.0",
19
- "@voltro/protocol": "0.33.0",
20
- "@voltro/runtime": "0.33.0",
21
- "@voltro/workflow": "0.33.0",
15
+ "@voltro/cli": "0.34.0",
16
+ "@voltro/database": "0.34.0",
17
+ "@voltro/env": "0.34.0",
18
+ "@voltro/plugin-multitenancy": "0.34.0",
19
+ "@voltro/protocol": "0.34.0",
20
+ "@voltro/runtime": "0.34.0",
21
+ "@voltro/workflow": "0.34.0",
22
22
  "effect": "^3.22.0"
23
23
  },
24
24
  "devDependencies": {
25
- "@voltro/testing": "0.33.0",
25
+ "@voltro/testing": "0.34.0",
26
26
  "typescript": "^6.0.3",
27
+ "@vitest/coverage-v8": "^4.1.10",
27
28
  "vitest": "^4.1.10"
28
29
  }
29
30
  }
@@ -12,6 +12,22 @@ import { Schema } from 'effect'
12
12
 
13
13
  export const exportNotes = defineAction({
14
14
  name: 'notes.export',
15
+ // A flag gate is NOT an access decision, and this pair is the clearest place
16
+ // in the templates to see why. `gatedBy` decides WHETHER THE FEATURE IS ON —
17
+ // it is a release control, flipped for everyone at once (or by rollout
18
+ // bucket), and `betaExport: false` in app.config.ts happens to close this
19
+ // door today. `guards:` / `openAccess:` decide WHO MAY CALL IT. Turning the
20
+ // flag on must not silently also decide the second question, so the
21
+ // procedure declares both.
22
+ //
23
+ // Open because no auth strategy and no rbac ship here: every caller resolves
24
+ // to an anonymous Subject with no scopes, so a scope guard would deny all of
25
+ // them. What the handler returns is a COUNT of the request tenant's own notes
26
+ // — no bodies, no other tenant's rows (`tenant()` scopes the read).
27
+ openAccess:
28
+ 'returns a COUNT of the request tenant\'s own notes — no row contents leave. The '
29
+ + '`betaExport` flag gates whether the feature runs at all; that is a release control, not '
30
+ + 'an access decision, which is why this line exists alongside it.',
15
31
  input: Schema.Struct({}),
16
32
  output: Schema.Struct({ exported: Schema.Number }),
17
33
  error: FlagDisabled,
@@ -13,6 +13,18 @@ import { Schema } from 'effect'
13
13
  export const createNote = defineMutation({
14
14
  name: 'notes.create',
15
15
  target: { table: 'notes', op: 'insert' },
16
+ // `requireFlag(ctx, 'newEditor')` in the executor answers "is this feature
17
+ // switched on"; this line answers "who may call it". Two different questions
18
+ // — a flag flipped on for everyone would otherwise silently answer both.
19
+ //
20
+ // Open because no auth strategy and no rbac ship here, so every caller is an
21
+ // anonymous Subject holding no scopes and a `guards: [{ scope }]` would deny
22
+ // all of them. The write carries only what the caller sent, into the tenant
23
+ // on the request.
24
+ openAccess:
25
+ 'inserts a note built only from caller-supplied fields into the request\'s tenant. The '
26
+ + '`newEditor` flag decides whether the feature runs; this decides who may call it — a '
27
+ + 'release control is not an access decision.',
16
28
  input: Schema.Struct({
17
29
  tenantId: Schema.String,
18
30
  title: Schema.NonEmptyString,
@@ -13,19 +13,20 @@
13
13
  "dependencies": {
14
14
  "@effect/platform": "^0.97.0",
15
15
  "@effect/rpc": "^0.76.0",
16
- "@voltro/cli": "0.33.0",
17
- "@voltro/database": "0.33.0",
18
- "@voltro/env": "0.33.0",
19
- "@voltro/plugin-flags": "0.33.0",
20
- "@voltro/plugin-multitenancy": "0.33.0",
21
- "@voltro/protocol": "0.33.0",
22
- "@voltro/runtime": "0.33.0",
23
- "@voltro/sql-postgres": "0.33.0",
16
+ "@voltro/cli": "0.34.0",
17
+ "@voltro/database": "0.34.0",
18
+ "@voltro/env": "0.34.0",
19
+ "@voltro/plugin-flags": "0.34.0",
20
+ "@voltro/plugin-multitenancy": "0.34.0",
21
+ "@voltro/protocol": "0.34.0",
22
+ "@voltro/runtime": "0.34.0",
23
+ "@voltro/sql-postgres": "0.34.0",
24
24
  "effect": "^3.22.0"
25
25
  },
26
26
  "devDependencies": {
27
- "@voltro/testing": "0.33.0",
27
+ "@voltro/testing": "0.34.0",
28
28
  "typescript": "^6.0.3",
29
+ "@vitest/coverage-v8": "^4.1.10",
29
30
  "vitest": "^4.1.10"
30
31
  }
31
32
  }
@@ -11,17 +11,32 @@ pnpm --filter @{{projectName}}/{{appName}} dev
11
11
  # → http://localhost:4000 (store: memory — zero infra)
12
12
  ```
13
13
 
14
- The `.env` ships a DEV encryption key. Generate your own with
15
- `openssl rand -hex 32` and keep it in a secret store for production.
14
+ **No encryption key ships with this template** — a key committed to a template
15
+ is a key published to everyone who downloads it. `VOLTRO_FIELD_ENCRYPTION_KEY`
16
+ is declared with `generate: 'hex'`, so `voltro dev` mints a unique one into a
17
+ gitignored `.env.local` on first boot. Your deployment mints its own:
18
+ `voltro secret generate field-encryption`.
16
19
 
17
20
  ## What this shows
18
21
 
19
- - **Field encryption** — `profiles.ssn` is `text().encrypted()`. You pass
20
- plaintext; the store middleware encrypts it (AES-256-GCM) on write and
21
- decrypts on read, so handlers always see plaintext while the column holds an
22
- opaque `enc:v1:…` string at rest. `governancePlugin({ fieldEncryption: true })`
22
+ - **Field encryption** — `profiles.ssn` is `text().encrypted().serverOnly()`.
23
+ You pass plaintext; the store middleware encrypts it (AES-256-GCM) on write
24
+ and decrypts on read, so handlers always see plaintext while the column holds
25
+ an opaque `enc:v1:…` string at rest. `governancePlugin({ fieldEncryption: true })`
23
26
  registers the cipher from `VOLTRO_FIELD_ENCRYPTION_KEY`. Boot fails loud if an
24
27
  `.encrypted()` column exists but no cipher is registered.
28
+ - **…and why the second marker is there.** `.encrypted()` is about bytes AT
29
+ REST; it says nothing about the wire, and the runtime decrypts for the
30
+ handler — so an encrypted column reaches a client exactly like any other
31
+ unless it is also `.serverOnly()`. `profiles.get` therefore returns the last
32
+ four digits it derived server-side, which proves the cipher ran without
33
+ publishing the value.
34
+ - **Every procedure declares an access decision.** `guards: [{ scope }]` or
35
+ `openAccess: '<why>'`, or the app refuses to boot (`security.defaultDeny`).
36
+ Both procedures here are `openAccess` with the reason in the file — this
37
+ template configures no auth strategy and no rbac, so every caller resolves to
38
+ an anonymous Subject holding no scopes and a scope guard would deny 100% of
39
+ traffic. Add an identity (see `api-auth` / `api-rbac`), then the guard.
25
40
  - **GDPR** — admin-gated `governance.export` / `governance.erase` walk the
26
41
  declared `subjectScopes` (`[{ table: 'profiles', subjectField: 'id' }]`) to
27
42
  bundle or erase everything belonging to a subject.
@@ -33,12 +48,14 @@ The `.env` ships a DEV encryption key. Generate your own with
33
48
  ## Try it
34
49
 
35
50
  ```bash
36
- # create a profile, then read it back — ssn round-trips as plaintext:
51
+ # create a profile, then read it back — the cipher round-trips, the value doesn't:
37
52
  ID=$(curl -s localhost:4000/_voltro/inspect/invoke -H 'content-type: application/json' \
38
53
  -d '{"tag":"profiles.create","input":{"name":"Ada","email":"ada@acme.com","ssn":"123-45-6789"}}' | python3 -c 'import sys,json;print(json.load(sys.stdin)["result"]["id"])')
39
54
  curl -s localhost:4000/_voltro/inspect/invoke -H 'content-type: application/json' \
40
55
  -d "{\"tag\":\"profiles.get\",\"input\":{\"id\":\"$ID\"}}"
41
- # → { ok:true, result:{ …, ssn:"123-45-6789" } } (plaintext — cipher decrypted it)
56
+ # → { ok:true, result:{ …, ssnLast4:"6789" } }
57
+ # The handler read "123-45-6789" as plaintext (the cipher decrypted it) and
58
+ # published four digits, because the column is `.serverOnly()`.
42
59
 
43
60
  # record consent (a mutation):
44
61
  curl -s localhost:4000/_voltro/inspect/invoke -H 'content-type: application/json' \
@@ -59,3 +76,8 @@ subject with the `admin:full` scope (see [`api-rbac`](https://voltro.cloud/docs/
59
76
  whole (PII, tokens, notes).
60
77
  - **The key is everything.** GCM fails closed on a bad key — you get an error,
61
78
  never silent corruption. Back the key up; rotating it needs re-encrypting.
79
+ - **`.encrypted()` is not `.serverOnly()`.** Three orthogonal markers, three
80
+ questions: `.encrypted()` = encrypted at rest, `.serverOnly()` = may it leave
81
+ the server at all, `.sensitive()`/`.safe()` = may it appear in a
82
+ `voltro data export`. Using one to answer another's question is the mistake —
83
+ say both when you mean both.
@@ -1,7 +1,22 @@
1
+ // Executor for `profiles.get`.
2
+ //
3
+ // `p['ssn']` is PLAINTEXT here — the store middleware decrypted the
4
+ // `.encrypted()` column on the way out, and the handler never sees the
5
+ // `enc:v1:…` ciphertext. That is the round-trip this template exists to show,
6
+ // and the last-four projection below is what proves it happened.
7
+ //
8
+ // The projection is not decoration: the column is `.serverOnly()`, so the full
9
+ // value must not reach a client. Deriving the suffix HERE — rather than sending
10
+ // the whole string and trimming it in the browser — is the difference between a
11
+ // value that never left the server and one that did.
1
12
  import type { AppContext } from '@voltro/runtime'
2
13
  import { eq } from '@voltro/database'
3
14
  import { database } from '../database/schema'
4
15
 
16
+ /** Last four characters, or '' when there is nothing to show. Never throws on a
17
+ * short or empty value — a masked field is not a place to fail a read. */
18
+ const last4 = (value: string): string => (value.length >= 4 ? value.slice(-4) : '')
19
+
5
20
  const execute = async (input: { id: string }, ctx: AppContext) => {
6
21
  const rows = await ctx.store.query(database.profiles.where(eq('id', input.id)).descriptor)
7
22
  const p = rows[0]
@@ -10,7 +25,8 @@ const execute = async (input: { id: string }, ctx: AppContext) => {
10
25
  id: p['id'] as string,
11
26
  name: p['name'] as string,
12
27
  email: p['email'] as string,
13
- ssn: p['ssn'] as string, // decrypted by the store middleware
28
+ // Decrypted by the store middleware, then narrowed to what may leave.
29
+ ssnLast4: last4(p['ssn'] as string),
14
30
  }
15
31
  }
16
32
 
@@ -1,17 +1,42 @@
1
- // Reads a profile back `ssn` comes out as PLAINTEXT. The store middleware
2
- // decrypts the `.encrypted()` column on read; the handler never touches the
3
- // `enc:v1:…` ciphertext. (In a real app you'd gate this read behind auth.)
1
+ // Reads a profile back. The store middleware DECRYPTS the `.encrypted()` column
2
+ // on read, so the handler holds `ssn` as plaintext and never touches the
3
+ // `enc:v1:…` ciphertext that is the round-trip this template exists to show.
4
+ //
5
+ // What crosses the WIRE is a SEPARATE decision, and the schema makes it: `ssn`
6
+ // is also `.serverOnly()`, so the full value may not leave the server. The
7
+ // executor proves the decryption worked by returning the last four digits it
8
+ // derived from the plaintext — which is what a support UI would actually
9
+ // render. Encryption at rest is not an exposure marker; `.serverOnly()` is.
4
10
 
5
11
  import { defineAction } from '@voltro/protocol'
6
12
  import { Schema } from 'effect'
7
13
 
8
14
  export const getProfile = defineAction({
9
15
  name: 'profiles.get',
16
+ // Open, and the reason is a claim you can check against the two files beside
17
+ // this one: the full `ssn` is `.serverOnly()` in `database/schema.ts` and is
18
+ // not named in the output below, so the sensitive value cannot cross the wire
19
+ // at all. What is left — name, email, four digits — is the row this demo's
20
+ // own `profiles.create` put there; the template ships no seed and no other
21
+ // writer.
22
+ //
23
+ // A `guards: [{ scope: 'profiles:read' }]` is what a real deployment wants
24
+ // here, and it is unsatisfiable in THIS app: no auth strategy and no rbac are
25
+ // configured, so every caller resolves to an anonymous Subject holding no
26
+ // scopes and the guard would deny 100% of traffic — an outage, not security.
27
+ // Add an identity (see `api-auth` / `api-rbac`), then the guard.
28
+ openAccess:
29
+ 'returns name, email and the last four digits derived server-side from the decrypted '
30
+ + '`ssn`; the full value is `.serverOnly()` and is absent from this output, so it cannot '
31
+ + 'cross the wire. Rows come only from this demo\'s own `profiles.create` — no seed, no '
32
+ + 'other writer.',
10
33
  input: Schema.Struct({ id: Schema.String }),
11
34
  output: Schema.NullOr(Schema.Struct({
12
35
  id: Schema.String,
13
36
  name: Schema.String,
14
37
  email: Schema.String,
15
- ssn: Schema.String,
38
+ // Proof the cipher decrypted the column, without publishing it. Empty when
39
+ // the stored value is shorter than four characters.
40
+ ssnLast4: Schema.String,
16
41
  })),
17
42
  })
@@ -28,10 +28,22 @@ export const profiles = table('profiles', {
28
28
  id: id({ prefix: 'prof' }),
29
29
  name: text(),
30
30
  email: text(),
31
- // .encrypted() AES-256-GCM at rest. The handler always sees plaintext; the
32
- // stored value is an opaque `enc:v1:…` string. Encrypt only what you read
33
- // back WHOLE (you can't filter/sort on it in SQL — it's ciphertext on disk).
34
- ssn: text().encrypted(),
31
+ // TWO markers, because they answer two different questions say both when
32
+ // you mean both:
33
+ //
34
+ // .encrypted() → AES-256-GCM AT REST. The handler always sees plaintext;
35
+ // the stored value is an opaque `enc:v1:…` string. Encrypt
36
+ // only what you read back WHOLE (you can't filter/sort on
37
+ // it in SQL — it's ciphertext on disk).
38
+ // .serverOnly() → may this value leave the server AT ALL. It may not.
39
+ //
40
+ // Reading `.encrypted()` as "safe to expose" is a category error and a
41
+ // tempting one: the runtime DECRYPTS for the handler, so an encrypted column
42
+ // flows to a client exactly like any other unless it is also `.serverOnly()`.
43
+ // `crud.*` strips serverOnly columns for you; a hand-written procedure must
44
+ // simply not name it in its output — see `actions/profiles.get.action.ts`,
45
+ // which returns the last four digits it derived server-side instead.
46
+ ssn: text().encrypted().serverOnly(),
35
47
  })
36
48
  .with(audit()) // createdAt drives the retention sweep
37
49