@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
@@ -8,6 +8,18 @@ import { Schema } from 'effect'
8
8
  export const createProfile = defineMutation({
9
9
  name: 'profiles.create',
10
10
  target: { table: 'profiles', op: 'insert' },
11
+ // Open: writes exactly the three fields the caller passed and echoes back
12
+ // only `{ id, name, email }` — `ssn` is `.serverOnly()` and is deliberately
13
+ // absent from the output, so what the caller sent in never comes back out.
14
+ // It reads nothing and cannot reach an existing row.
15
+ //
16
+ // No auth strategy and no rbac ship in this template, so every caller is an
17
+ // anonymous Subject holding no scopes; a `guards: [{ scope }]` would deny all
18
+ // of them rather than the wrong ones. Add an identity, then the guard.
19
+ openAccess:
20
+ 'inserts only the fields the caller supplied and echoes back `{ id, name, email }` — the '
21
+ + '`ssn` it takes is `.serverOnly()` and never appears in a response. Reads nothing, '
22
+ + 'modifies no existing row.',
11
23
  input: Schema.Struct({
12
24
  name: Schema.NonEmptyString,
13
25
  email: Schema.String,
@@ -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/plugin-audit": "0.33.0",
20
- "@voltro/plugin-governance": "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/plugin-audit": "0.34.0",
20
+ "@voltro/plugin-governance": "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
  }
@@ -13,6 +13,21 @@ import { Schema } from 'effect'
13
13
 
14
14
  export const syncPull = defineAction({
15
15
  name: 'sync.pull',
16
+ // Open: every KV key this touches is namespaced with the request's tenant
17
+ // (`sync:<tenantId>:cursor` / `:seen:*` in the executor) and the rows it
18
+ // writes go into a `tenant()` table, so one caller's sync state is not
19
+ // reachable from another's. The "upstream" is a pure local generator — there
20
+ // is no network call and no credential.
21
+ //
22
+ // No auth strategy and no rbac ship in this template, so a
23
+ // `guards: [{ scope: 'sync:run' }]` would be unsatisfiable: every caller is
24
+ // an anonymous Subject holding no scopes, and the guard would deny all of
25
+ // them. Growing a role map here purely to look guarded would teach a pattern
26
+ // this template is not about.
27
+ openAccess:
28
+ 'advances the request tenant\'s own sync watermark; every KV key is namespaced by that '
29
+ + 'tenant and the rows land in a `tenant()` table. The upstream is a local pure generator '
30
+ + '— no network call, no credential.',
16
31
  input: Schema.Struct({
17
32
  // How many upstream events to pull this call (default 5, capped in the executor).
18
33
  limit: Schema.optional(Schema.Int.pipe(Schema.greaterThan(0))),
@@ -11,6 +11,19 @@ import { Schema } from 'effect'
11
11
 
12
12
  export const syncReset = defineAction({
13
13
  name: 'sync.reset',
14
+ // Open — but note what bounds the blast radius, because it is a deliberate
15
+ // choice in the executor and not a property of "reset": it deletes only keys
16
+ // under `sync:<request tenantId>:`, never `ctx.kv.clear()`, which would drop
17
+ // the app's ENTIRE KV namespace across every tenant in one call. It destroys
18
+ // no rows; a following `sync.pull` rebuilds the watermark.
19
+ //
20
+ // No auth strategy and no rbac ship here, so a scope guard would deny every
21
+ // caller rather than the wrong ones. An operational reset is a good candidate
22
+ // for a `guards: [{ scope: 'sync:admin' }]` the moment you have an identity.
23
+ openAccess:
24
+ 'deletes only the request tenant\'s own `sync:<tenant>:` keys — never `ctx.kv.clear()`, '
25
+ + 'which would wipe every tenant\'s namespace — and destroys no rows; the next `sync.pull` '
26
+ + 'rebuilds the watermark. An operational reset: guard it once an identity exists.',
14
27
  input: Schema.Struct({
15
28
  // Also delete the idempotency markers (default: keep them).
16
29
  markers: Schema.optional(Schema.Boolean),
@@ -9,6 +9,13 @@ import { Schema } from 'effect'
9
9
 
10
10
  export const syncStatus = defineAction({
11
11
  name: 'sync.status',
12
+ // Open: returns two integers about the request tenant's OWN sync state — a
13
+ // watermark and a marker count read from `sync:<tenantId>:` keys. No row
14
+ // contents, no other tenant's keys, no writes. No auth strategy or rbac ships
15
+ // in this template, so a scope guard would deny every caller.
16
+ openAccess:
17
+ 'returns two integers describing the request tenant\'s own sync state (watermark + live '
18
+ + 'marker count) from its own KV namespace; no row contents, no writes.',
12
19
  input: Schema.Struct({}),
13
20
  output: Schema.Struct({
14
21
  cursor: Schema.Int, // the durable watermark
@@ -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/kv": "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/kv": "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
  }
@@ -11,13 +11,24 @@ import { Schema } from 'effect'
11
11
 
12
12
  export const listEvents = defineQuery({
13
13
  name: 'events.list',
14
+ // Open: `tenant()` confines every delivery to the tenantId on the request, so
15
+ // one caller's synced rows never reach another's subscription. With no auth
16
+ // strategy configured that tenantId is the caller's own `x-tenant` header —
17
+ // it shapes the feed, it does not authorize the caller — and a
18
+ // `guards: [{ scope }]` would be unsatisfiable, denying 100% of traffic.
19
+ openAccess:
20
+ 'streams the synced event rows of the request\'s tenant (`tenant()` scopes every '
21
+ + 'delivery); the rows are this demo\'s own generated events. No auth strategy ships here, '
22
+ + 'so the tenant comes from the caller\'s `x-tenant` header — add a strategy, then a `guards:`.',
14
23
  input: Schema.Struct({}),
15
- output: Schema.Struct({
16
- id: Schema.String,
17
- externalId: Schema.String,
18
- kind: Schema.String,
19
- sequence: Schema.Number,
20
- tenantId: Schema.String,
21
- createdAt: Schema.Date,
22
- }),
24
+ output: Schema.Array(
25
+ Schema.Struct({
26
+ id: Schema.String,
27
+ externalId: Schema.String,
28
+ kind: Schema.String,
29
+ sequence: Schema.Number,
30
+ tenantId: Schema.String,
31
+ createdAt: Schema.Date,
32
+ }),
33
+ ),
23
34
  })
@@ -9,6 +9,17 @@ import { Schema } from 'effect'
9
9
  export const createComment = defineMutation({
10
10
  name: 'comments.create',
11
11
  target: { table: 'comments', op: 'insert' },
12
+ // Open, same reasoning as `posts.create` — and here the `flag` action makes
13
+ // the distinction sharper still: it does not reject anything, it writes the
14
+ // row and queues it for review. A rule that never refuses is plainly not an
15
+ // access control, so the access decision has to be its own line.
16
+ //
17
+ // No auth strategy and no rbac ship here, so a `guards: [{ scope }]` would
18
+ // deny every caller instead of the wrong ones.
19
+ openAccess:
20
+ 'inserts caller-supplied text into the request\'s tenant; the `flag` moderation rule '
21
+ + 'queues it for review and rejects nothing, so it is a review workflow rather than an '
22
+ + 'access control.',
12
23
  input: Schema.Struct({
13
24
  body: Schema.String,
14
25
  }),
@@ -11,6 +11,19 @@ import { Schema } from 'effect'
11
11
  export const createPost = defineMutation({
12
12
  name: 'posts.create',
13
13
  target: { table: 'posts', op: 'insert' },
14
+ // Content moderation is not access control, and this template is the place
15
+ // to be exact about that: the `block` rule inspects WHAT is being written and
16
+ // can reject it; `guards:` / `openAccess:` decide WHO may write at all. The
17
+ // moderation interceptor lets an authorized troll through and stops an
18
+ // unauthorized poet — neither substitutes for the other.
19
+ //
20
+ // Open because no auth strategy and no rbac ship here: every caller is an
21
+ // anonymous Subject with no scopes, so a scope guard would deny all of them.
22
+ // The write carries only caller-supplied text into the request's tenant.
23
+ openAccess:
24
+ 'inserts caller-supplied text into the request\'s tenant, after the `block` moderation '
25
+ + 'rule has vetted it. Moderation judges the CONTENT, not the caller — it is not an '
26
+ + 'access decision, which is why this line exists beside it.',
14
27
  input: Schema.Struct({
15
28
  title: Schema.NonEmptyString,
16
29
  body: Schema.String,
@@ -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/plugin-moderation": "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/plugin-moderation": "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
  }
@@ -4,6 +4,14 @@ import { Schema } from 'effect'
4
4
  export const createNote = defineMutation({
5
5
  name: 'notes.create',
6
6
  target: { table: 'notes', op: 'insert' },
7
+ // Open: writes only the title + body the caller sent, into the request's
8
+ // tenant (`tenant()` auto-stamps `tenantId`), and touches no existing row.
9
+ // No auth strategy and no rbac ship here, so every caller is an anonymous
10
+ // Subject holding no scopes and a scope guard would deny all of them.
11
+ openAccess:
12
+ 'inserts a note built only from caller-supplied fields into the request\'s tenant; reads '
13
+ + 'nothing and modifies no existing row. No auth strategy ships here, so there is no '
14
+ + 'identity a scope guard could name.',
7
15
  input: Schema.Struct({ title: Schema.NonEmptyString, body: Schema.String }),
8
16
  output: Schema.Struct({
9
17
  id: Schema.String,
@@ -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/plugin-multitenancy": "0.33.0",
19
- "@voltro/plugin-prometheus": "0.33.0",
20
- "@voltro/plugin-sentry": "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/plugin-multitenancy": "0.34.0",
19
+ "@voltro/plugin-prometheus": "0.34.0",
20
+ "@voltro/plugin-sentry": "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
  }
@@ -4,6 +4,19 @@ import { Schema } from 'effect'
4
4
  export const listNotes = defineQuery({
5
5
  name: 'notes.list',
6
6
  source: 'notes',
7
+ // Open: `tenant()` confines every delivery to the tenantId on the request, so
8
+ // one tenant's notes never reach another's subscription. With no auth
9
+ // strategy configured that tenantId comes from the caller's own `x-tenant`
10
+ // header — result shaping, not access control — and a `guards: [{ scope }]`
11
+ // would be unsatisfiable, denying 100% of traffic rather than some of it.
12
+ //
13
+ // Worth stating in the OBSERVABILITY template specifically: a denied call is
14
+ // still a span and still a metric. Traces show you what happened, not who was
15
+ // allowed to make it happen.
16
+ openAccess:
17
+ 'lists notes for the request\'s tenant only (`tenant()` scopes every delivery). No auth '
18
+ + 'strategy ships here, so the tenant comes from the caller\'s `x-tenant` header — add a '
19
+ + 'strategy, then a `guards:`. Tracing records calls; it does not authorize them.',
7
20
  input: Schema.Struct({}),
8
21
  output: Schema.Array(Schema.Struct({
9
22
  id: Schema.String,
@@ -10,6 +10,20 @@ import { Schema } from 'effect'
10
10
 
11
11
  export const createNote = defineMutation({
12
12
  name: 'notes.create',
13
+ // A rate limit is not an access decision, and this template is where that is
14
+ // easiest to get wrong. The interceptor answers HOW OFTEN a caller may do
15
+ // this; `guards:` / `openAccess:` answer WHETHER THEY MAY AT ALL. A limit
16
+ // slows an attacker down and lets them through 99 times an hour, so it can
17
+ // never stand in for the second answer — the two live side by side.
18
+ //
19
+ // Open here because no auth strategy and no rbac ship in this template: every
20
+ // caller resolves to an anonymous Subject holding no scopes, so a
21
+ // `guards: [{ scope }]` would deny all of them. `assertOwnTenant` still
22
+ // rejects a `tenantId` that does not match the resolved subject.
23
+ openAccess:
24
+ 'inserts a note carrying only what the caller sent, into the caller\'s own tenant '
25
+ + '(`assertOwnTenant` rejects a mismatch). The rate limit bounds HOW OFTEN, never WHETHER '
26
+ + '— it is not an access decision, which is why this line exists beside it.',
13
27
  target: {
14
28
  table: 'notes',
15
29
  op: 'insert',
@@ -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/plugin-multitenancy": "0.33.0",
20
- "@voltro/plugin-ratelimit": "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/plugin-multitenancy": "0.34.0",
20
+ "@voltro/plugin-ratelimit": "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
  }
@@ -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/plugin-multitenancy": "0.33.0",
20
- "@voltro/plugin-rbac": "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/plugin-multitenancy": "0.34.0",
20
+ "@voltro/plugin-rbac": "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
  }
@@ -13,17 +13,18 @@
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/plugin-openapi": "0.33.0",
19
- "@voltro/protocol": "0.33.0",
20
- "@voltro/runtime": "0.33.0",
21
- "@voltro/sql-postgres": "0.33.0",
16
+ "@voltro/cli": "0.34.0",
17
+ "@voltro/database": "0.34.0",
18
+ "@voltro/plugin-openapi": "0.34.0",
19
+ "@voltro/protocol": "0.34.0",
20
+ "@voltro/runtime": "0.34.0",
21
+ "@voltro/sql-postgres": "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
  }
@@ -9,6 +9,19 @@ import { Schema } from 'effect'
9
9
  export const createProject = defineMutation({
10
10
  name: 'projects.create',
11
11
  target: { table: 'projects', op: 'insert' },
12
+ // An entitlement check is not an access decision. `requireEntitlement`
13
+ // answers HOW MANY the tenant has paid for; `guards:` / `openAccess:` answer
14
+ // WHO MAY CALL IT. A quota gate refuses the fourth project and cheerfully
15
+ // allows the first three — from anyone.
16
+ //
17
+ // Open because this template ships no auth strategy and no rbac (it is the
18
+ // billing bundle; `api-saas-starter` is the same domain WITH auth, and it
19
+ // guards these procedures). With no identity, every caller is an anonymous
20
+ // Subject holding no scopes and a `guards: [{ scope }]` would deny all of them.
21
+ openAccess:
22
+ 'inserts a project named by the caller into the request\'s tenant, after the billing '
23
+ + 'entitlement gate. A quota bounds HOW MANY, never WHO — see `api-saas-starter` for the '
24
+ + 'same procedure with an auth strategy and a `projects:write` guard.',
12
25
  input: Schema.Struct({ name: Schema.NonEmptyString }),
13
26
  output: Schema.Struct({
14
27
  id: Schema.String,
@@ -13,21 +13,22 @@
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/plugin-analytics-postgres": "0.33.0",
19
- "@voltro/plugin-billing": "0.33.0",
20
- "@voltro/plugin-multitenancy": "0.33.0",
21
- "@voltro/plugin-notifications": "0.33.0",
22
- "@voltro/plugin-presence": "0.33.0",
23
- "@voltro/protocol": "0.33.0",
24
- "@voltro/runtime": "0.33.0",
25
- "@voltro/sql-postgres": "0.33.0",
16
+ "@voltro/cli": "0.34.0",
17
+ "@voltro/database": "0.34.0",
18
+ "@voltro/plugin-analytics-postgres": "0.34.0",
19
+ "@voltro/plugin-billing": "0.34.0",
20
+ "@voltro/plugin-multitenancy": "0.34.0",
21
+ "@voltro/plugin-notifications": "0.34.0",
22
+ "@voltro/plugin-presence": "0.34.0",
23
+ "@voltro/protocol": "0.34.0",
24
+ "@voltro/runtime": "0.34.0",
25
+ "@voltro/sql-postgres": "0.34.0",
26
26
  "effect": "^3.22.0"
27
27
  },
28
28
  "devDependencies": {
29
- "@voltro/testing": "0.33.0",
29
+ "@voltro/testing": "0.34.0",
30
30
  "typescript": "^6.0.3",
31
+ "@vitest/coverage-v8": "^4.1.10",
31
32
  "vitest": "^4.1.10"
32
33
  }
33
34
  }
@@ -8,6 +8,17 @@ import { Schema } from 'effect'
8
8
  export const listProjects = defineQuery({
9
9
  name: 'projects.list',
10
10
  source: 'projects',
11
+ // Open: `tenant()` confines every delivery to the tenantId on the request, so
12
+ // one tenant's projects never reach another's subscription. This template
13
+ // bundles billing + notifications + presence and deliberately ships NO auth
14
+ // strategy — pair it with `api-auth` / `api-saas-starter` for the identity
15
+ // half. Until then the tenantId is the caller's own `x-tenant` header, and a
16
+ // `guards: [{ scope }]` would be unsatisfiable: an anonymous Subject holds no
17
+ // scopes, so the guard would deny 100% of traffic.
18
+ openAccess:
19
+ 'lists the request tenant\'s own projects (`tenant()` scopes every delivery). This '
20
+ + 'template ships no auth strategy — see `api-saas-starter`, which does and guards these '
21
+ + 'same procedures — so the tenant comes from the caller\'s `x-tenant` header.',
11
22
  input: Schema.Struct({}),
12
23
  output: Schema.Array(Schema.Struct({
13
24
  id: Schema.String,
@@ -9,6 +9,19 @@ import { Schema } from 'effect'
9
9
 
10
10
  export const me = defineAction({
11
11
  name: 'session.me',
12
+ // The ONE open procedure in this app, and it has to be. The four domain
13
+ // procedures beside it carry guards an anonymous caller cannot satisfy —
14
+ // which is correct — but the dashboard's SSR gate has to be able to ask
15
+ // "who am I" while it is still nobody, or it has nothing to branch on and the
16
+ // redirect to /login never happens.
17
+ //
18
+ // Safe because the answer is a projection of the caller's own request: the
19
+ // Subject `voltroPasswordStrategy` resolved from the cookie they sent. It
20
+ // reads no table and can say nothing about anybody else.
21
+ openAccess:
22
+ 'echoes the caller\'s own resolved Subject (type/id/tenantId) — reads no table and reveals '
23
+ + 'nothing the caller did not present. Anonymous callers must reach it, or the SSR gate '
24
+ + 'cannot redirect them to sign in.',
12
25
  input: Schema.Struct({}),
13
26
  output: Schema.Struct({
14
27
  // 'anonymous' | 'user' | 'apiKey' | 'serviceAccount'
@@ -22,6 +22,7 @@
22
22
  import { defineEnv, envVar } from '@voltro/env'
23
23
  import { authRoutesPlugin, memoryUserStore, voltroPasswordStrategy } from '@voltro/plugin-auth'
24
24
  import { billingPlugin } from '@voltro/plugin-billing'
25
+ import { resolveScopes } from './authz'
25
26
 
26
27
  export const env = defineEnv({
27
28
  LOG_LEVEL: envVar.enum(['debug', 'info', 'warn', 'error'], { access: 'public', default: 'info' }),
@@ -83,5 +84,23 @@ export default {
83
84
  // Subject. Reads the SAME VOLTRO_SESSION_SECRET the routes sign with, so
84
85
  // the signing and verifying sides cannot drift.
85
86
  strategies: [voltroPasswordStrategy()],
87
+ // …and turns that identity into AUTHORITY. A session cookie carries no
88
+ // `scopes` by design (identity is settled at sign-in; authority is re-read
89
+ // per request), so without this the guards on the four domain procedures
90
+ // would be unsatisfiable — the dashboard would boot and answer `ScopeError`
91
+ // to every caller, which is an outage, not security.
92
+ //
93
+ // It runs ONLY on a subject a strategy matched, never for anonymous. That
94
+ // asymmetry IS what the guards enforce: signed in → member scopes; signed
95
+ // out → nothing. See `authz.ts` for the vocabulary, and for why a quota and
96
+ // a tenant filter are not substitutes for it.
97
+ resolveScopes,
86
98
  },
99
+ // Point `voltro doctor` / `voltro check` at the scope vocabulary so the
100
+ // unknown-scope rule is LIVE here. This app does scope-based authorization
101
+ // without `@voltro/plugin-rbac`, and the plugin is what normally publishes
102
+ // that set — without this pointer the rule stays dormant and a guard naming a
103
+ // scope nothing grants (a typo, a rename) becomes a permanently uncallable
104
+ // procedure that nothing reports.
105
+ doctor: { scopeVocabulary: './authz.ts#MEMBER_SCOPES' },
87
106
  }
@@ -0,0 +1,75 @@
1
+ // The app's authorization vocabulary, in ONE place.
2
+ //
3
+ // Split out of `app.config.ts` for the same reason `api-rbac` splits its role
4
+ // map: the tests can build the real resolver from the real scopes. A test that
5
+ // redeclares the vocabulary it is testing proves only that its own copy is
6
+ // self-consistent — the copy drifts, the test stays green, and the drift is
7
+ // exactly the thing worth catching.
8
+ //
9
+ // ── Three gates, three different questions ─────────────────────────────────
10
+ //
11
+ // This template stacks all three, and conflating any two of them is the
12
+ // classic SaaS authorization bug:
13
+ //
14
+ // tenant() WHICH ROWS — the runtime AND-merges
15
+ // `tenantId = subject.tenantId` into every read.
16
+ // requireEntitlement() HOW MANY — the plan's metered quota. It refuses the
17
+ // fourth project and allows the first three, to anyone.
18
+ // guards: [{ scope }] WHO MAY CALL IT AT ALL — this file.
19
+ //
20
+ // A quota is not an access control and neither is a tenant filter. The gate
21
+ // that answers "may this caller do this" is the third one, and every
22
+ // wire-exposed procedure here declares it (or an `openAccess:` reason — there
23
+ // is exactly one, `session.me`).
24
+
25
+ import type { Subject } from '@voltro/protocol'
26
+
27
+ /**
28
+ * What a signed-in member of a tenant may do.
29
+ *
30
+ * **One role, on purpose.** This template has exactly one kind of account: a
31
+ * member of the tenant they signed up into. Inventing `owner` / `billing-admin`
32
+ * / `viewer` here would be a role map written to look secure rather than to
33
+ * describe this app — and a scope every caller already holds is a guard that
34
+ * reads as protection and enforces nothing.
35
+ *
36
+ * The line these guards DO draw is real: **signed in or not.** With no cookie a
37
+ * caller cannot list a tenant's projects, cannot read the invite roster (which
38
+ * carries teammates' email addresses), and cannot spend a seat.
39
+ *
40
+ * Split it by BLAST RADIUS the day your product has two kinds of account.
41
+ * `team:invite` is already separate from `projects:write` for that reason:
42
+ * inviting a person costs a seat and puts an outsider inside the tenant, which
43
+ * is a bigger blast than creating a project.
44
+ */
45
+ export const MEMBER_SCOPES = [
46
+ 'projects:read',
47
+ 'projects:write',
48
+ 'team:read',
49
+ 'team:invite',
50
+ ] as const
51
+
52
+ /**
53
+ * Where a caller's authority comes from.
54
+ *
55
+ * `auth.resolveScopes` runs ONLY on a subject a strategy MATCHED — never for
56
+ * anonymous, where there is no identity to look anything up for. That is
57
+ * exactly what makes these guards satisfiable AND meaningful:
58
+ *
59
+ * • no session cookie → `voltroPasswordStrategy` skips → anonymous Subject,
60
+ * no scopes, every guard denies.
61
+ * • valid cookie → matched `user` Subject → this runs → member scopes.
62
+ *
63
+ * A session cookie deliberately carries no `scopes` of its own (identity is
64
+ * settled at sign-in; authority is re-read per request), so this function is
65
+ * the only thing that can grant them — which is why removing a member's access
66
+ * takes effect on their EXISTING session rather than when the cookie expires.
67
+ *
68
+ * PRODUCTION replaces the body with a lookup: `subject.metadata.roles` (set by
69
+ * your strategy) or a read against your own tables through the `store` the
70
+ * framework hands the resolver. Failure posture: return
71
+ * `{ kind: 'unavailable', reason }` if the lookup itself fails — the request
72
+ * then fails CLOSED with a named reason, instead of an empty array being
73
+ * applied as though it were a policy decision.
74
+ */
75
+ export const resolveScopes = (_subject: Subject): ReadonlyArray<string> => [...MEMBER_SCOPES]
@@ -8,6 +8,16 @@ import { Schema } from 'effect'
8
8
  export const createInvite = defineMutation({
9
9
  name: 'invites.create',
10
10
  target: { table: 'invites', op: 'insert' },
11
+ // Members only, and on its OWN scope rather than `projects:write` — the
12
+ // vocabulary is split by blast radius, not by convenience. Inviting a person
13
+ // spends a seat (real money) and puts an outsider inside the tenant; creating
14
+ // a project does neither. When this app grows an `owner` role, `team:invite`
15
+ // is the scope that moves to it and `projects:write` is the one that stays.
16
+ //
17
+ // The guard runs before `requireEntitlement`, so an anonymous call cannot
18
+ // spend the tenant's seat quota. Satisfiable as shipped: a matched session
19
+ // gets `team:invite` from `auth.resolveScopes` (authz.ts).
20
+ guards: [{ scope: 'team:invite' }],
11
21
  input: Schema.Struct({ email: Schema.NonEmptyString }),
12
22
  output: Schema.Struct({
13
23
  id: Schema.String,