@voltro/cli 0.33.0 → 0.35.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 (343) hide show
  1. package/CHANGELOG.md +1968 -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-B8aoJvuw.js +2 -0
  10. package/dist/{apiBuild-h9VHtnlw.js → apiBuild-DYD_ONLD.js} +46 -46
  11. package/dist/{appGraph-gQ_6GkQQ.js → appGraph-KGDPTuTy.js} +1 -1
  12. package/dist/appGraph-zuMGKVYX.js +2 -0
  13. package/dist/appPort-B_HpJ_ck.js +48 -0
  14. package/dist/baselineCommand-C2ClWZN3.js +2 -0
  15. package/dist/baselineCommand-DIttzO8A.js +227 -0
  16. package/dist/bin.js +71 -28
  17. package/dist/build-CD8K4XOr.js +711 -0
  18. package/dist/cacheCommand-DA4OH9xt.js +42 -0
  19. package/dist/capabilitiesCommand-nq_pz5xd.js +123 -0
  20. package/dist/checkCommand-Ct9xkTrS.js +232 -0
  21. package/dist/checkCommand-DKpDLlqu.js +2 -0
  22. package/dist/{cliArgs-qdZSElM3.js → cliArgs-D4p8n7EE.js} +12 -1
  23. package/dist/cliError-BmdYnghb.js +10 -0
  24. package/dist/cliOutput-D1tSBoRM.js +15 -0
  25. package/dist/{cliRuntime-Oh517vCV.js → cliRuntime-Dh7UDinH.js} +20 -20
  26. package/dist/cloudClient-DWL-Hw_T.js +67 -0
  27. package/dist/cloudCmd-Cvv5HGaZ.js +364 -0
  28. package/dist/clusterCmd-D5wsCmA_.js +54 -0
  29. package/dist/codegen-CYM3Zqrf.js +605 -0
  30. package/dist/codegen-ChBi_hVa.js +2 -0
  31. package/dist/codegenCommand-C4YoQIc2.js +30 -0
  32. package/dist/codemodRunner-BnFq3Fgu.js +5384 -0
  33. package/dist/commandRunner-BLAEFLjp.js +47 -0
  34. package/dist/commands-BE8E7zF3.js +816 -0
  35. package/dist/connectionConfig-UFlIEiys.js +66 -0
  36. package/dist/dashboardCommand-D7SgZGaN.js +25 -0
  37. package/dist/dataCommand-BhYwDgg-.js +537 -0
  38. package/dist/dataProfile-dW-PsfLB.js +15 -0
  39. package/dist/dbCommand-DS4b97Is.js +2 -0
  40. package/dist/{dbCommand-DTLKAfbA.js → dbCommand-O8HA63s2.js} +552 -402
  41. package/dist/{dev-C_P8FLSx.js → dev-C7sFZq3m.js} +3670 -3169
  42. package/dist/dev-D2BikO7a.js +3 -0
  43. package/dist/devActivity-Dx_3nnGv.js +100 -0
  44. package/dist/devActivity.js +1 -1
  45. package/dist/dialectDriver-CgXnDfec.js +39 -0
  46. package/dist/discover-C9XKJDco.js +25 -0
  47. package/dist/doctorCommand-CM4Ch9C7.js +2 -0
  48. package/dist/{checkCommand-xGhRFFg2.js → doctorCommand-DnimF5IM.js} +523 -1278
  49. package/dist/dormancyCommand-QewYug_s.js +69 -0
  50. package/dist/e2eCmd-BRabZww-.js +147 -0
  51. package/dist/embeddingsCommand-BfiLS_QI.js +73 -0
  52. package/dist/envCommand-CCGPRQY1.js +60 -0
  53. package/dist/evalCommand-6RUfPen4.js +118 -0
  54. package/dist/evolveCommand-CHsLCtDf.js +281 -0
  55. package/dist/fileTaxonomy-CJfgOllU.js +457 -0
  56. package/dist/frameworkTableAssembly-YyVe32Cb.js +2 -0
  57. package/dist/frameworkTableAssembly-oMPBKqlE.js +511 -0
  58. package/dist/generateCommand-DbgcUpGw.js +147 -0
  59. package/dist/index.d.ts +45 -0
  60. package/dist/index.js +4 -3
  61. package/dist/infoCommand-DwOgK1t6.js +60 -0
  62. package/dist/{inspect-BUUjt773.js → inspect-CjTYzAs_.js} +113 -41
  63. package/dist/inspect-P4pxoMaV.js +2 -0
  64. package/dist/inspectCmd-EHFZ9yYu.js +224 -0
  65. package/dist/inspectFetch-EMuhTG_9.js +151 -0
  66. package/dist/inspectMetrics-CfdKLh6t.js +72 -0
  67. package/dist/loadEnv-D9nEOClM.js +44 -0
  68. package/dist/logFileSink-C_D2wRN1.js +105 -0
  69. package/dist/logsCmd-D36xK7Zu.js +260 -0
  70. package/dist/manifestBuild-COkJoyAr.js +2 -0
  71. package/dist/{manifestBuild-BLrVuSlM.js → manifestBuild-hpPLaGxV.js} +1 -1
  72. package/dist/metaCommands-7MJfZ5cf.js +196 -0
  73. package/dist/migrate-BV7I-ZHZ.js +83 -0
  74. package/dist/mssqlClusterPatch-_4cE_nun.js +44 -0
  75. package/dist/newCommand-COWOJ1_E.js +156 -0
  76. package/dist/nodeEnvironment-cGFAj1J8.js +28 -0
  77. package/dist/packageCommand-Cug_3Ogl.js +271 -0
  78. package/dist/pageConvention-cEiRxdab.js +5 -0
  79. package/dist/privacyCommand-C-Df56U_.js +146 -0
  80. package/dist/probeCommand-CZfaaUOZ.js +122 -0
  81. package/dist/projectScaffold-DmzEKHib.js +2 -0
  82. package/dist/projectScaffold-LMMtaavR.js +814 -0
  83. package/dist/renderModeScan-D7J1B7Kw.js +105 -0
  84. package/dist/renderProfile-1OWWAAtx.js +81 -0
  85. package/dist/runtimeRegistry-DMeKfTHP.js +81 -0
  86. package/dist/runtimeTrace-CH3eUiMw.js +91 -0
  87. package/dist/scheduleCmd-DQRu6BZC.js +149 -0
  88. package/dist/scheduleManifestCmd-D2x0CTTY.js +249 -0
  89. package/dist/schemaIr-UJybUUZW.js +103 -0
  90. package/dist/{sdkgen-C81QIkiL.js → sdkgen-BLkvGRfX.js} +111 -209
  91. package/dist/seedRunner-ZmLSqNe2.js +333 -0
  92. package/dist/serveCommand-CbDHU6l-.js +2 -0
  93. package/dist/serveCommand-iwlUBNS1.js +1766 -0
  94. package/dist/serveEntry.js +5 -5
  95. package/dist/serverlessCommand-CfJZy6dS.js +482 -0
  96. package/dist/start-BgN62boB.js +3 -0
  97. package/dist/start-T4VesWiM.js +1087 -0
  98. package/dist/startEntry.js +2 -2
  99. package/dist/staticCommand-Dr2M6tpU.js +304 -0
  100. package/dist/storageCommand-Co6NfLqN.js +42 -0
  101. package/dist/templates-De8IR5-c.js +102 -0
  102. package/dist/test-CI6iDsYc.js +115 -0
  103. package/dist/tracesCmd-DStmCJPi.js +232 -0
  104. package/dist/tsconfigPaths-BWXBWgcl.js +107 -0
  105. package/dist/tsxLoader-EuXmSJ1K.js +51 -0
  106. package/dist/typecheckCommand-BlsWiCNq.js +61 -0
  107. package/dist/updateCommand-BlMZhWgO.js +2 -0
  108. package/dist/updateCommand-x0pI_x-B.js +585 -0
  109. package/dist/webDev-BcykISYQ2.js +2 -0
  110. package/dist/{inspectMetrics-1xzTKAFx.js → webDev-Dybxew86.js} +988 -1560
  111. package/dist/webhookDiscovery-CrGAfhIG.js +2 -0
  112. package/dist/webhookDiscovery-D7VaeMlz.js +51 -0
  113. package/dist/webhooksCommand-DlAgS2Iw.js +267 -0
  114. package/dist/workflowsCmd-BGF-mRZ5.js +608 -0
  115. package/package.json +209 -17
  116. package/templates/AGENTS.core.md +58 -3
  117. package/templates/AGENTS.md +64 -7
  118. package/templates/agent-docs/_index.md +6 -4
  119. package/templates/agent-docs/_manifest.json +23 -6
  120. package/templates/agent-docs/ai.md +370 -0
  121. package/templates/agent-docs/authentication.md +313 -31
  122. package/templates/agent-docs/caching.md +6 -0
  123. package/templates/agent-docs/cli.md +853 -50
  124. package/templates/agent-docs/data.md +608 -12
  125. package/templates/agent-docs/database/migrations.md +223 -25
  126. package/templates/agent-docs/database/misc.md +156 -40
  127. package/templates/agent-docs/database/querying.md +19 -1
  128. package/templates/agent-docs/database/scaling.md +60 -0
  129. package/templates/agent-docs/database/schema.md +1 -0
  130. package/templates/agent-docs/database/seedsdialects.md +208 -19
  131. package/templates/agent-docs/database/transactions.md +68 -0
  132. package/templates/agent-docs/deployment.md +284 -25
  133. package/templates/agent-docs/introduction.md +88 -17
  134. package/templates/agent-docs/local-first-mobile.md +79 -4
  135. package/templates/agent-docs/multi-tenancy.md +188 -42
  136. package/templates/agent-docs/observability.md +58 -3
  137. package/templates/agent-docs/plugins/ai-flows.md +247 -2
  138. package/templates/agent-docs/plugins/analytics-postgres.md +1 -1
  139. package/templates/agent-docs/plugins/audit.md +37 -1
  140. package/templates/agent-docs/plugins/auth-social.md +143 -0
  141. package/templates/agent-docs/plugins/auth-workos.md +4 -2
  142. package/templates/agent-docs/plugins/auth.md +131 -6
  143. package/templates/agent-docs/plugins/billing.md +132 -15
  144. package/templates/agent-docs/plugins/cdc-out.md +46 -7
  145. package/templates/agent-docs/plugins/clickhouse.md +32 -1
  146. package/templates/agent-docs/plugins/duckdb.md +1 -1
  147. package/templates/agent-docs/plugins/flags.md +132 -0
  148. package/templates/agent-docs/plugins/governance.md +105 -7
  149. package/templates/agent-docs/plugins/multitenancy.md +9 -4
  150. package/templates/agent-docs/plugins/presence.md +13 -2
  151. package/templates/agent-docs/plugins/ratelimit.md +9 -0
  152. package/templates/agent-docs/plugins/search.md +162 -8
  153. package/templates/agent-docs/plugins/sso-saml.md +47 -8
  154. package/templates/agent-docs/plugins/storage.md +11 -0
  155. package/templates/agent-docs/plugins/webhooks.md +105 -0
  156. package/templates/agent-docs/plugins.md +152 -18
  157. package/templates/agent-docs/reference.md +60 -3
  158. package/templates/agent-docs/releases.md +1117 -0
  159. package/templates/agent-docs/routing.md +43 -25
  160. package/templates/agent-docs/scheduling.md +27 -0
  161. package/templates/agent-docs/schema-driven-ui.md +92 -12
  162. package/templates/agent-docs/security.md +426 -0
  163. package/templates/agent-docs/templates/apibackends.md +87 -18
  164. package/templates/agent-docs/templates/appshells.md +32 -14
  165. package/templates/agent-docs/templates/overview.md +13 -8
  166. package/templates/agent-docs/testing.md +211 -14
  167. package/templates/agent-docs/whats-new.md +98 -136
  168. package/templates/agent-docs/workflows.md +231 -14
  169. package/templates/apps/api-ai/actions/summarize.action.ts +11 -0
  170. package/templates/apps/api-ai/package.json +8 -7
  171. package/templates/apps/api-auth/actions/me.action.ts +13 -0
  172. package/templates/apps/api-auth/package.json +9 -8
  173. package/templates/apps/api-backend/mutations/notes.create.mutation.ts +9 -0
  174. package/templates/apps/api-backend/package.json +12 -8
  175. package/templates/apps/api-backend/queries/notes.query.ts +30 -8
  176. package/templates/apps/api-backend-deactivation/actions/users.get.action.ts +14 -0
  177. package/templates/apps/api-backend-deactivation/mutations/users.create.mutation.ts +8 -0
  178. package/templates/apps/api-backend-deactivation/mutations/users.deactivate.mutation.ts +10 -0
  179. package/templates/apps/api-backend-deactivation/package.json +8 -7
  180. package/templates/apps/api-backend-mail/actions/sendWelcome.action.ts +17 -0
  181. package/templates/apps/api-backend-mail/mutations/notes.create.mutation.ts +9 -0
  182. package/templates/apps/api-backend-mail/package.json +9 -8
  183. package/templates/apps/api-backend-mail/queries/notes.query.ts +30 -8
  184. package/templates/apps/api-backend-mariadb/.env.example +14 -0
  185. package/templates/apps/api-backend-mariadb/mutations/notes.create.mutation.ts +9 -0
  186. package/templates/apps/api-backend-mariadb/package.json +10 -9
  187. package/templates/apps/api-backend-mariadb/queries/notes.query.ts +30 -8
  188. package/templates/apps/api-backend-sqlite/.env.example +14 -0
  189. package/templates/apps/api-backend-sqlite/mutations/notes.create.mutation.ts +9 -0
  190. package/templates/apps/api-backend-sqlite/package.json +9 -8
  191. package/templates/apps/api-backend-sqlite/queries/notes.query.ts +30 -8
  192. package/templates/apps/api-backend-storage/actions/uploadAvatar.action.ts +13 -0
  193. package/templates/apps/api-backend-storage/actions/uploadDocument.action.ts +12 -0
  194. package/templates/apps/api-backend-storage/mutations/notes.create.mutation.ts +9 -0
  195. package/templates/apps/api-backend-storage/package.json +9 -8
  196. package/templates/apps/api-backend-storage/queries/notes.query.ts +30 -8
  197. package/templates/apps/api-cms/actions/content.get.action.ts +7 -0
  198. package/templates/apps/api-cms/actions/content.types.action.ts +6 -0
  199. package/templates/apps/api-cms/actions/me.action.ts +13 -0
  200. package/templates/apps/api-cms/app.config.ts +19 -0
  201. package/templates/apps/api-cms/authz.ts +63 -0
  202. package/templates/apps/api-cms/mutations/content.publish.mutation.ts +10 -0
  203. package/templates/apps/api-cms/mutations/content.saveDraft.mutation.ts +10 -0
  204. package/templates/apps/api-cms/mutations/content.unpublish.mutation.ts +5 -0
  205. package/templates/apps/api-cms/package.json +11 -10
  206. package/templates/apps/api-cms/queries/content.list.query.ts +23 -7
  207. package/templates/apps/api-cms/tests/accessDecisions.test.ts +121 -0
  208. package/templates/apps/api-cms/tests/content.descriptors.test.ts +8 -4
  209. package/templates/apps/api-collab/mutations/documents.create.mutation.ts +8 -0
  210. package/templates/apps/api-collab/mutations/documents.setBody.mutation.ts +13 -0
  211. package/templates/apps/api-collab/package.json +9 -8
  212. package/templates/apps/api-collab/queries/documents.query.ts +19 -7
  213. package/templates/apps/api-data-advanced/package.json +9 -8
  214. package/templates/apps/api-data-advanced/queries/authors.withBooks.query.ts +12 -0
  215. package/templates/apps/api-data-advanced/queries/books.search.query.ts +9 -0
  216. package/templates/apps/api-durable/mutations/orders.approve.mutation.ts +17 -0
  217. package/templates/apps/api-durable/mutations/orders.place.mutation.ts +9 -0
  218. package/templates/apps/api-durable/package.json +9 -8
  219. package/templates/apps/api-feature-flags/actions/notes.export.action.ts +16 -0
  220. package/templates/apps/api-feature-flags/mutations/notes.create.mutation.ts +12 -0
  221. package/templates/apps/api-feature-flags/package.json +10 -9
  222. package/templates/apps/api-governance/README.md +30 -8
  223. package/templates/apps/api-governance/actions/profiles.get.action.server.ts +17 -1
  224. package/templates/apps/api-governance/actions/profiles.get.action.ts +29 -4
  225. package/templates/apps/api-governance/database/schema.ts +16 -4
  226. package/templates/apps/api-governance/mutations/profiles.create.mutation.ts +12 -0
  227. package/templates/apps/api-governance/package.json +9 -8
  228. package/templates/apps/api-kv/actions/sync.pull.action.ts +15 -0
  229. package/templates/apps/api-kv/actions/sync.reset.action.ts +13 -0
  230. package/templates/apps/api-kv/actions/sync.status.action.ts +7 -0
  231. package/templates/apps/api-kv/package.json +9 -8
  232. package/templates/apps/api-kv/queries/events.list.query.ts +19 -8
  233. package/templates/apps/api-moderation/mutations/comments.create.mutation.ts +11 -0
  234. package/templates/apps/api-moderation/mutations/posts.create.mutation.ts +13 -0
  235. package/templates/apps/api-moderation/package.json +9 -8
  236. package/templates/apps/api-observability/mutations/notes.create.mutation.ts +8 -0
  237. package/templates/apps/api-observability/package.json +9 -8
  238. package/templates/apps/api-observability/queries/notes.list.query.ts +13 -0
  239. package/templates/apps/api-ratelimit/mutations/notes.create.mutation.ts +14 -0
  240. package/templates/apps/api-ratelimit/package.json +9 -8
  241. package/templates/apps/api-rbac/package.json +9 -8
  242. package/templates/apps/api-rest/package.json +8 -7
  243. package/templates/apps/api-saas/mutations/projects.create.mutation.ts +13 -0
  244. package/templates/apps/api-saas/package.json +12 -11
  245. package/templates/apps/api-saas/queries/projects.list.query.ts +11 -0
  246. package/templates/apps/api-saas-starter/actions/me.action.ts +13 -0
  247. package/templates/apps/api-saas-starter/app.config.ts +19 -0
  248. package/templates/apps/api-saas-starter/authz.ts +75 -0
  249. package/templates/apps/api-saas-starter/mutations/invites.create.mutation.ts +10 -0
  250. package/templates/apps/api-saas-starter/mutations/projects.create.mutation.ts +9 -0
  251. package/templates/apps/api-saas-starter/package.json +15 -11
  252. package/templates/apps/api-saas-starter/queries/invites.list.query.ts +19 -8
  253. package/templates/apps/api-saas-starter/queries/projects.list.query.ts +18 -7
  254. package/templates/apps/api-saas-starter/tests/accessDecisions.test.ts +135 -0
  255. package/templates/apps/api-search/mutations/articles.create.mutation.ts +14 -0
  256. package/templates/apps/api-search/package.json +9 -8
  257. package/templates/apps/api-search/queries/articles.list.query.ts +21 -8
  258. package/templates/apps/api-status/README.md +10 -3
  259. package/templates/apps/api-status/app.config.ts +8 -3
  260. package/templates/apps/api-status/authz.ts +5 -3
  261. package/templates/apps/api-status/package.json +9 -8
  262. package/templates/apps/api-status/queries/components.list.query.ts +14 -6
  263. package/templates/apps/api-status/queries/incidents.live.query.ts +23 -10
  264. package/templates/apps/api-status/queries/updates.list.query.ts +16 -9
  265. package/templates/apps/api-status/tests/status.test.ts +9 -1
  266. package/templates/apps/api-versioning/actions/documents.asOf.action.ts +12 -0
  267. package/templates/apps/api-versioning/actions/documents.history.action.ts +13 -0
  268. package/templates/apps/api-versioning/mutations/documents.create.mutation.ts +9 -0
  269. package/templates/apps/api-versioning/mutations/documents.update.mutation.ts +12 -0
  270. package/templates/apps/api-versioning/package.json +9 -8
  271. package/templates/apps/api-webhooks/mutations/orders.fulfill.mutation.ts +16 -0
  272. package/templates/apps/api-webhooks/package.json +10 -9
  273. package/templates/apps/api-webhooks/queries/orders.list.query.ts +10 -0
  274. package/templates/apps/changelog/package.json +8 -6
  275. package/templates/apps/edge-functions/package.json +2 -2
  276. package/templates/apps/frontend-admin/package.json +10 -8
  277. package/templates/apps/frontend-admin/src/lib/admin.ts +20 -9
  278. package/templates/apps/frontend-admin/src/locales/de.ts +11 -1
  279. package/templates/apps/frontend-admin/src/locales/en.ts +13 -1
  280. package/templates/apps/frontend-admin/src/pages/admin/[entity]/page.tsx +65 -22
  281. package/templates/apps/frontend-admin/src/pages/admin/entity.test.tsx +130 -23
  282. package/templates/apps/frontend-admin/src/pages/admin/layout.tsx +3 -2
  283. package/templates/apps/frontend-admin/src/pages/admin/page.test.tsx +19 -2
  284. package/templates/apps/frontend-admin/src/pages/admin/page.tsx +9 -4
  285. package/templates/apps/frontend-app/app.config.ts +4 -3
  286. package/templates/apps/frontend-app/package.json +11 -8
  287. package/templates/apps/frontend-app/src/lib/api.ts +25 -0
  288. package/templates/apps/frontend-app/src/pages/page.test.tsx +130 -82
  289. package/templates/apps/frontend-app/src/pages/page.tsx +14 -18
  290. package/templates/apps/frontend-auth/package.json +10 -8
  291. package/templates/apps/frontend-blank/package.json +9 -7
  292. package/templates/apps/frontend-cms/package.json +11 -9
  293. package/templates/apps/frontend-collab/package.json +12 -9
  294. package/templates/apps/frontend-collab/src/pages/page.test.tsx +122 -78
  295. package/templates/apps/frontend-contact/package.json +9 -7
  296. package/templates/apps/frontend-dashboard/package.json +9 -7
  297. package/templates/apps/frontend-docs/package.json +9 -7
  298. package/templates/apps/frontend-i18n/package.json +8 -6
  299. package/templates/apps/frontend-landing/package.json +9 -7
  300. package/templates/apps/frontend-portal/package.json +10 -8
  301. package/templates/apps/frontend-saas/app.config.ts +10 -6
  302. package/templates/apps/frontend-saas/package.json +10 -8
  303. package/templates/apps/frontend-saas/src/lib/api.ts +27 -32
  304. package/templates/apps/frontend-saas/src/pages/dashboard/billing/page.tsx +7 -8
  305. package/templates/apps/frontend-saas/src/pages/dashboard/page.test.tsx +27 -3
  306. package/templates/apps/frontend-saas/src/pages/dashboard/page.tsx +4 -4
  307. package/templates/apps/frontend-saas/src/pages/dashboard/team/page.tsx +3 -4
  308. package/templates/apps/frontend-spa/package.json +9 -7
  309. package/templates/apps/frontend-ssr/package.json +9 -7
  310. package/templates/apps/frontend-ssr-api/package.json +10 -8
  311. package/templates/apps/frontend-static-blog/package.json +8 -6
  312. package/templates/apps/frontend-status/package.json +10 -8
  313. package/templates/apps/mobile-app/README.md +1 -0
  314. package/templates/apps/mobile-app/package.json +4 -2
  315. package/templates/apps/mobile-app/src/app/index.tsx +22 -12
  316. package/templates/apps/mobile-app/src/app/orders/[id].tsx +1 -1
  317. package/templates/apps/mobile-app/src/lib/api.ts +34 -0
  318. package/templates/apps/mobile-app/voltro.mobile.ts +4 -2
  319. package/templates/baselines/bare/.env.example +14 -0
  320. package/templates/baselines/bare/baseline.json +4 -4
  321. package/templates/baselines/compose/.env.example +14 -0
  322. package/templates/baselines/compose/README.md +1 -1
  323. package/templates/baselines/compose/baseline.json +5 -5
  324. package/templates/baselines/compose-mariadb/.env.example +14 -0
  325. package/templates/baselines/compose-mariadb/README.md +1 -1
  326. package/templates/baselines/compose-mariadb/baseline.json +5 -5
  327. package/templates/baselines/helm/.env.example +14 -0
  328. package/templates/baselines/helm/baseline.json +4 -4
  329. package/dist/apiBuild-C-x9YacA.js +0 -2
  330. package/dist/appGraph-CvQCte0z.js +0 -2
  331. package/dist/checkCommand-DRovTKza.js +0 -2
  332. package/dist/commands-CJfepbm4.js +0 -11541
  333. package/dist/dbCommand-b1gum4td.js +0 -2
  334. package/dist/dev-iiMtlkfs.js +0 -3
  335. package/dist/devActivity-BhIu6ncs.js +0 -159
  336. package/dist/frameworkTableAssembly-BwIrO5nv.js +0 -638
  337. package/dist/frameworkTableAssembly-D-EebUQX.js +0 -2
  338. package/dist/inspect-mmBuRXmy.js +0 -2
  339. package/dist/manifestBuild-Dj8Jjoto.js +0 -2
  340. package/dist/seedRunner-Bqxgp7HZ.js +0 -230
  341. package/dist/serveCommand-DdaM4Hup.js +0 -1608
  342. package/dist/start-C0koT0UO.js +0 -1084
  343. /package/templates/apps/api-ai/actions/{summarize.action.server.tsx → summarize.action.server.ts} +0 -0
@@ -19,6 +19,16 @@ const Violation = Schema.Struct({
19
19
 
20
20
  export const saveDraft = defineMutation({
21
21
  name: 'content.saveDraft',
22
+ // The write surface the README promises is editors-only — enforced here
23
+ // rather than described. `guards:` runs in the dispatch spine BEFORE the
24
+ // executor and before the transaction opens, so an unauthorized save never
25
+ // touches the database.
26
+ //
27
+ // Satisfiable by this app as shipped: sign in via POST /auth/sign-in,
28
+ // `voltroPasswordStrategy` matches the cookie, `auth.resolveScopes`
29
+ // (authz.ts) grants `content:write`. No cookie → anonymous → no scopes →
30
+ // `ScopeError`, which is exactly the boundary this template exists to show.
31
+ guards: [{ scope: 'content:write' }],
22
32
  input: Schema.Struct({
23
33
  type: Schema.NonEmptyString,
24
34
  // Arbitrary per-type field values (may carry `id` to update an existing
@@ -6,6 +6,11 @@ import { Schema } from 'effect'
6
6
 
7
7
  export const unpublishContent = defineMutation({
8
8
  name: 'content.unpublish',
9
+ // Editors only. Taking a page DOWN is as much a live-site change as putting
10
+ // one up — an unguarded unpublish is a denial-of-service against your own
11
+ // content — so it carries the same `content:write`, satisfied by a signed-in
12
+ // session and refused to an anonymous caller.
13
+ guards: [{ scope: 'content:write' }],
9
14
  input: Schema.Struct({ type: Schema.NonEmptyString, id: Schema.NonEmptyString }),
10
15
  output: Schema.Struct({ ok: Schema.Boolean }),
11
16
  })
@@ -13,20 +13,21 @@
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/cms": "0.33.0",
18
- "@voltro/database": "0.33.0",
19
- "@voltro/env": "0.33.0",
20
- "@voltro/plugin-auth": "0.33.0",
21
- "@voltro/plugin-multitenancy": "0.33.0",
22
- "@voltro/protocol": "0.33.0",
23
- "@voltro/runtime": "0.33.0",
24
- "@voltro/sql-postgres": "0.33.0",
16
+ "@voltro/cli": "0.35.0",
17
+ "@voltro/cms": "0.35.0",
18
+ "@voltro/database": "0.35.0",
19
+ "@voltro/env": "0.35.0",
20
+ "@voltro/plugin-auth": "0.35.0",
21
+ "@voltro/plugin-multitenancy": "0.35.0",
22
+ "@voltro/protocol": "0.35.0",
23
+ "@voltro/runtime": "0.35.0",
24
+ "@voltro/sql-postgres": "0.35.0",
25
25
  "effect": "^3.22.0"
26
26
  },
27
27
  "devDependencies": {
28
- "@voltro/testing": "0.33.0",
28
+ "@voltro/testing": "0.35.0",
29
29
  "typescript": "^6.0.3",
30
+ "@vitest/coverage-v8": "^4.1.10",
30
31
  "vitest": "^4.1.10"
31
32
  }
32
33
  }
@@ -13,15 +13,31 @@ import { Schema } from 'effect'
13
13
 
14
14
  export const listContent = defineQuery({
15
15
  name: 'content.list',
16
+ // Editors only. `guards:` is enforced in the dispatch spine — when the
17
+ // subscription OPENS and again on EVERY delivery — so an editor whose access
18
+ // is revoked mid-session stops receiving rows instead of keeping a live feed
19
+ // open on an expired grant. `tenant()` on the derived content tables still
20
+ // scopes WHICH rows; this decides WHO may ask.
21
+ //
22
+ // Satisfiable by this app's own configuration: `voltroPasswordStrategy`
23
+ // matches the session cookie, `auth.resolveScopes` (authz.ts) grants
24
+ // `content:read` to the matched subject. Anonymous → no scopes → denied,
25
+ // which is the point.
26
+ //
27
+ // Consumers of PUBLISHED content are a separate surface (`handleCmsRest`,
28
+ // api-key scoped) — see the README. They do not come through here.
29
+ guards: [{ scope: 'content:read' }],
16
30
  input: Schema.Struct({
17
31
  type: Schema.NonEmptyString,
18
32
  status: Schema.optional(Schema.Literal('draft', 'published')),
19
33
  }),
20
- output: Schema.Struct({
21
- id: Schema.String,
22
- status: Schema.String,
23
- title: Schema.optional(Schema.String),
24
- slug: Schema.optional(Schema.String),
25
- updatedAt: Schema.Date,
26
- }),
34
+ output: Schema.Array(
35
+ Schema.Struct({
36
+ id: Schema.String,
37
+ status: Schema.String,
38
+ title: Schema.optional(Schema.String),
39
+ slug: Schema.optional(Schema.String),
40
+ updatedAt: Schema.Date,
41
+ }),
42
+ ),
27
43
  })
@@ -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.35.0",
17
+ "@voltro/database": "0.35.0",
18
+ "@voltro/env": "0.35.0",
19
+ "@voltro/local-first": "0.35.0",
20
+ "@voltro/plugin-multitenancy": "0.35.0",
21
+ "@voltro/protocol": "0.35.0",
22
+ "@voltro/runtime": "0.35.0",
23
23
  "effect": "^3.22.0"
24
24
  },
25
25
  "devDependencies": {
26
- "@voltro/testing": "0.33.0",
26
+ "@voltro/testing": "0.35.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.35.0",
16
+ "@voltro/database": "0.35.0",
17
+ "@voltro/env": "0.35.0",
18
+ "@voltro/plugin-governance": "0.35.0",
19
+ "@voltro/plugin-multitenancy": "0.35.0",
20
+ "@voltro/protocol": "0.35.0",
21
+ "@voltro/runtime": "0.35.0",
22
22
  "effect": "^3.22.0"
23
23
  },
24
24
  "devDependencies": {
25
- "@voltro/testing": "0.33.0",
25
+ "@voltro/testing": "0.35.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.35.0",
16
+ "@voltro/database": "0.35.0",
17
+ "@voltro/env": "0.35.0",
18
+ "@voltro/plugin-multitenancy": "0.35.0",
19
+ "@voltro/protocol": "0.35.0",
20
+ "@voltro/runtime": "0.35.0",
21
+ "@voltro/workflow": "0.35.0",
22
22
  "effect": "^3.22.0"
23
23
  },
24
24
  "devDependencies": {
25
- "@voltro/testing": "0.33.0",
25
+ "@voltro/testing": "0.35.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.35.0",
17
+ "@voltro/database": "0.35.0",
18
+ "@voltro/env": "0.35.0",
19
+ "@voltro/plugin-flags": "0.35.0",
20
+ "@voltro/plugin-multitenancy": "0.35.0",
21
+ "@voltro/protocol": "0.35.0",
22
+ "@voltro/runtime": "0.35.0",
23
+ "@voltro/sql-postgres": "0.35.0",
24
24
  "effect": "^3.22.0"
25
25
  },
26
26
  "devDependencies": {
27
- "@voltro/testing": "0.33.0",
27
+ "@voltro/testing": "0.35.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.