@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
@@ -9,6 +9,15 @@ import { Schema } from 'effect'
9
9
 
10
10
  export const createNote = defineMutation({
11
11
  name: 'notes.create',
12
+ // Open, deliberately — see `queries/notes.query.ts` for the long form. In
13
+ // short: `assertOwnTenant` still rejects a `tenantId` that does not match the
14
+ // resolved subject, but with no auth strategy configured that subject is
15
+ // anonymous and holds no scopes, so any `guards: [{ scope }]` would deny
16
+ // every caller instead of some of them.
17
+ openAccess:
18
+ 'inserts a note carrying only what the caller sent, into the caller\'s own tenant '
19
+ + '(`assertOwnTenant` rejects a mismatch). No auth strategy ships in this template, so '
20
+ + 'there is no identity a scope guard could name.',
12
21
  target: {
13
22
  table: 'notes',
14
23
  op: 'insert',
@@ -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-multitenancy": "0.33.0",
20
- "@voltro/plugin-storage": "0.33.0",
21
- "@voltro/protocol": "0.33.0",
22
- "@voltro/runtime": "0.33.0",
23
- "@voltro/sql-mysql": "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-storage": "0.34.0",
21
+ "@voltro/protocol": "0.34.0",
22
+ "@voltro/runtime": "0.34.0",
23
+ "@voltro/sql-mysql": "0.34.0",
24
24
  "effect": "^3.22.0"
25
25
  },
26
26
  "devDependencies": {
27
- "@voltro/testing": "0.33.0",
27
+ "@voltro/testing": "0.34.0",
28
28
  "typescript": "^6.0.3",
29
+ "@vitest/coverage-v8": "^4.1.10",
29
30
  "vitest": "^4.1.10"
30
31
  }
31
32
  }
@@ -8,13 +8,35 @@ import { Schema } from 'effect'
8
8
 
9
9
  export const listNotes = defineQuery({
10
10
  name: 'notes.list',
11
+ // Every wire-exposed procedure must declare an access decision — `guards:` or
12
+ // `openAccess:` — or the app refuses to boot (`security.defaultDeny`). This
13
+ // one is open, and the reason says what that buys and what it does not:
14
+ //
15
+ // `tenant()` confines the result to the tenantId on the request and re-checks
16
+ // it on every delivery, so one tenant's rows never reach another's
17
+ // subscription. But this template configures NO auth strategy, so that
18
+ // tenantId comes from the caller's own `x-tenant` header — it shapes the
19
+ // result, it does not authorize the caller. A `guards: [{ scope }]` here
20
+ // would be unsatisfiable: with no auth strategy and no rbac, every caller
21
+ // resolves to an anonymous Subject that holds no scopes, so the guard would
22
+ // deny 100% of traffic. That is not strict security, it is an outage.
23
+ //
24
+ // So: add an auth strategy (see the `api-auth` template) FIRST, then swap
25
+ // this line for a `guards:` in the same change — at that point `tenant()`
26
+ // becomes real isolation because the tenant comes from a verified session.
27
+ openAccess:
28
+ 'lists notes for the request\'s tenant only (`tenant()` scopes every delivery). No auth '
29
+ + 'strategy ships in this template, so that tenant comes from the caller\'s own `x-tenant` '
30
+ + 'header — result shaping, not access control. Add a strategy, then a `guards:`.',
11
31
  input: Schema.Struct({}),
12
- output: Schema.Struct({
13
- id: Schema.String,
14
- title: Schema.String,
15
- body: Schema.String,
16
- done: Schema.Boolean,
17
- tenantId: Schema.String,
18
- createdAt: Schema.Date,
19
- }),
32
+ output: Schema.Array(
33
+ Schema.Struct({
34
+ id: Schema.String,
35
+ title: Schema.String,
36
+ body: Schema.String,
37
+ done: Schema.Boolean,
38
+ tenantId: Schema.String,
39
+ createdAt: Schema.Date,
40
+ }),
41
+ ),
20
42
  })
@@ -1,5 +1,19 @@
1
1
  # {{capProjectName}} — SQLite backend env. Copy to `.env` and adjust.
2
2
 
3
+ # ── Environment ────────────────────────────────────────────────────────
4
+ #
5
+ # DECLARE IT. Every `voltro db …` / `voltro migrate` invocation resolves an
6
+ # UNSET `NODE_ENV` to `production` — the same way `voltro serve` and `voltro
7
+ # start` do — so a bare `pnpm migrate` with no NODE_ENV refuses rather than
8
+ # applying an un-reviewed diff to what might be a production database. It also
9
+ # decides which `_voltro_*` bookkeeping tables the command declares, and a
10
+ # migration command that resolves it differently from the serving process
11
+ # declares a DIFFERENT schema.
12
+ #
13
+ # `voltro dev` declares `development` for itself and needs nothing from here;
14
+ # this line is what makes the explicit schema commands work locally.
15
+ NODE_ENV=development
16
+
3
17
  # ── Database (embedded SQLite) ─────────────────────────────────────────
4
18
  DB_DIALECT=sqlite
5
19
  # A file path keeps data across restarts (the `.data/` dir is gitignored):
@@ -9,6 +9,15 @@ import { Schema } from 'effect'
9
9
 
10
10
  export const createNote = defineMutation({
11
11
  name: 'notes.create',
12
+ // Open, deliberately — see `queries/notes.query.ts` for the long form. In
13
+ // short: `assertOwnTenant` still rejects a `tenantId` that does not match the
14
+ // resolved subject, but with no auth strategy configured that subject is
15
+ // anonymous and holds no scopes, so any `guards: [{ scope }]` would deny
16
+ // every caller instead of some of them.
17
+ openAccess:
18
+ 'inserts a note carrying only what the caller sent, into the caller\'s own tenant '
19
+ + '(`assertOwnTenant` rejects a mismatch). No auth strategy ships in this template, so '
20
+ + 'there is no identity a scope guard could name.',
12
21
  target: {
13
22
  table: 'notes',
14
23
  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/protocol": "0.33.0",
21
- "@voltro/runtime": "0.33.0",
22
- "@voltro/sql-sqlite": "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/protocol": "0.34.0",
21
+ "@voltro/runtime": "0.34.0",
22
+ "@voltro/sql-sqlite": "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
  }
@@ -8,13 +8,35 @@ import { Schema } from 'effect'
8
8
 
9
9
  export const listNotes = defineQuery({
10
10
  name: 'notes.list',
11
+ // Every wire-exposed procedure must declare an access decision — `guards:` or
12
+ // `openAccess:` — or the app refuses to boot (`security.defaultDeny`). This
13
+ // one is open, and the reason says what that buys and what it does not:
14
+ //
15
+ // `tenant()` confines the result to the tenantId on the request and re-checks
16
+ // it on every delivery, so one tenant's rows never reach another's
17
+ // subscription. But this template configures NO auth strategy, so that
18
+ // tenantId comes from the caller's own `x-tenant` header — it shapes the
19
+ // result, it does not authorize the caller. A `guards: [{ scope }]` here
20
+ // would be unsatisfiable: with no auth strategy and no rbac, every caller
21
+ // resolves to an anonymous Subject that holds no scopes, so the guard would
22
+ // deny 100% of traffic. That is not strict security, it is an outage.
23
+ //
24
+ // So: add an auth strategy (see the `api-auth` template) FIRST, then swap
25
+ // this line for a `guards:` in the same change — at that point `tenant()`
26
+ // becomes real isolation because the tenant comes from a verified session.
27
+ openAccess:
28
+ 'lists notes for the request\'s tenant only (`tenant()` scopes every delivery). No auth '
29
+ + 'strategy ships in this template, so that tenant comes from the caller\'s own `x-tenant` '
30
+ + 'header — result shaping, not access control. Add a strategy, then a `guards:`.',
11
31
  input: Schema.Struct({}),
12
- output: Schema.Struct({
13
- id: Schema.String,
14
- title: Schema.String,
15
- body: Schema.String,
16
- done: Schema.Boolean,
17
- tenantId: Schema.String,
18
- createdAt: Schema.Date,
19
- }),
32
+ output: Schema.Array(
33
+ Schema.Struct({
34
+ id: Schema.String,
35
+ title: Schema.String,
36
+ body: Schema.String,
37
+ done: Schema.Boolean,
38
+ tenantId: Schema.String,
39
+ createdAt: Schema.Date,
40
+ }),
41
+ ),
20
42
  })
@@ -10,6 +10,19 @@ import { defineAction } from '@voltro/protocol'
10
10
 
11
11
  export const uploadAvatar = defineAction({
12
12
  name: 'storage.uploadAvatar',
13
+ // Writes bytes the caller sent to a PUBLIC object and returns its URL. It
14
+ // reads nothing and can overwrite nothing — the key is minted server-side per
15
+ // upload — but an unguarded upload endpoint is free hosting for whatever the
16
+ // internet feels like storing. `provider: 'memory'` in app.config.ts keeps
17
+ // that inside this process; the plugin's `maxBytes` / content-type policy is
18
+ // what bounds it once you point at S3/R2.
19
+ //
20
+ // No auth strategy ships in this template, so a scope guard would deny every
21
+ // caller. Add one, then `guards: [{ scope: 'files:write' }]`.
22
+ openAccess:
23
+ 'stores caller-supplied bytes as a new PUBLIC object and returns its URL — reads nothing, '
24
+ + 'overwrites nothing (the key is minted server-side). The `memory` provider keeps it '
25
+ + 'in-process; guard it before pointing at a real bucket.',
13
26
  input: Schema.Struct({
14
27
  bytesBase64: Schema.String,
15
28
  contentType: Schema.String,
@@ -9,6 +9,18 @@ import { defineAction } from '@voltro/protocol'
9
9
 
10
10
  export const uploadDocument = defineAction({
11
11
  name: 'storage.uploadDocument',
12
+ // The WRITE is open; the READ is not, and the split is the point. This stores
13
+ // caller-supplied bytes as a PRIVATE object and returns only its id — the
14
+ // bytes come back exclusively through the plugin's access-checked
15
+ // `storage.mintUrl` route, which applies the object's policy (owner + tenant
16
+ // + granted roles) and is not this procedure's decision to make.
17
+ //
18
+ // No auth strategy ships in this template, so a `guards:` here would deny
19
+ // every caller. Add one, then `guards: [{ scope: 'files:write' }]`.
20
+ openAccess:
21
+ 'stores caller-supplied bytes as a new PRIVATE object and returns only its id; reading it '
22
+ + 'back goes through the plugin\'s access-checked `storage.mintUrl`, which this does not '
23
+ + 'bypass. Guard the write too once the app has an identity.',
12
24
  input: Schema.Struct({
13
25
  bytesBase64: Schema.String,
14
26
  contentType: Schema.String,
@@ -9,6 +9,15 @@ import { Schema } from 'effect'
9
9
 
10
10
  export const createNote = defineMutation({
11
11
  name: 'notes.create',
12
+ // Open, deliberately — see `queries/notes.query.ts` for the long form. In
13
+ // short: `assertOwnTenant` still rejects a `tenantId` that does not match the
14
+ // resolved subject, but with no auth strategy configured that subject is
15
+ // anonymous and holds no scopes, so any `guards: [{ scope }]` would deny
16
+ // every caller instead of some of them.
17
+ openAccess:
18
+ 'inserts a note carrying only what the caller sent, into the caller\'s own tenant '
19
+ + '(`assertOwnTenant` rejects a mismatch). No auth strategy ships in this template, so '
20
+ + 'there is no identity a scope guard could name.',
12
21
  target: {
13
22
  table: 'notes',
14
23
  op: 'insert',
@@ -11,18 +11,19 @@
11
11
  "test": "voltro test"
12
12
  },
13
13
  "dependencies": {
14
- "@voltro/cli": "0.33.0",
15
- "@voltro/database": "0.33.0",
16
- "@voltro/env": "0.33.0",
17
- "@voltro/plugin-multitenancy": "0.33.0",
18
- "@voltro/plugin-storage": "0.33.0",
19
- "@voltro/protocol": "0.33.0",
20
- "@voltro/runtime": "0.33.0",
14
+ "@voltro/cli": "0.34.0",
15
+ "@voltro/database": "0.34.0",
16
+ "@voltro/env": "0.34.0",
17
+ "@voltro/plugin-multitenancy": "0.34.0",
18
+ "@voltro/plugin-storage": "0.34.0",
19
+ "@voltro/protocol": "0.34.0",
20
+ "@voltro/runtime": "0.34.0",
21
21
  "effect": "^3.22.0"
22
22
  },
23
23
  "devDependencies": {
24
- "@voltro/testing": "0.33.0",
24
+ "@voltro/testing": "0.34.0",
25
25
  "typescript": "^6.0.3",
26
+ "@vitest/coverage-v8": "^4.1.10",
26
27
  "vitest": "^4.1.10"
27
28
  }
28
29
  }
@@ -8,13 +8,35 @@ import { Schema } from 'effect'
8
8
 
9
9
  export const listNotes = defineQuery({
10
10
  name: 'notes.list',
11
+ // Every wire-exposed procedure must declare an access decision — `guards:` or
12
+ // `openAccess:` — or the app refuses to boot (`security.defaultDeny`). This
13
+ // one is open, and the reason says what that buys and what it does not:
14
+ //
15
+ // `tenant()` confines the result to the tenantId on the request and re-checks
16
+ // it on every delivery, so one tenant's rows never reach another's
17
+ // subscription. But this template configures NO auth strategy, so that
18
+ // tenantId comes from the caller's own `x-tenant` header — it shapes the
19
+ // result, it does not authorize the caller. A `guards: [{ scope }]` here
20
+ // would be unsatisfiable: with no auth strategy and no rbac, every caller
21
+ // resolves to an anonymous Subject that holds no scopes, so the guard would
22
+ // deny 100% of traffic. That is not strict security, it is an outage.
23
+ //
24
+ // So: add an auth strategy (see the `api-auth` template) FIRST, then swap
25
+ // this line for a `guards:` in the same change — at that point `tenant()`
26
+ // becomes real isolation because the tenant comes from a verified session.
27
+ openAccess:
28
+ 'lists notes for the request\'s tenant only (`tenant()` scopes every delivery). No auth '
29
+ + 'strategy ships in this template, so that tenant comes from the caller\'s own `x-tenant` '
30
+ + 'header — result shaping, not access control. Add a strategy, then a `guards:`.',
11
31
  input: Schema.Struct({}),
12
- output: Schema.Struct({
13
- id: Schema.String,
14
- title: Schema.String,
15
- body: Schema.String,
16
- done: Schema.Boolean,
17
- tenantId: Schema.String,
18
- createdAt: Schema.Date,
19
- }),
32
+ output: Schema.Array(
33
+ Schema.Struct({
34
+ id: Schema.String,
35
+ title: Schema.String,
36
+ body: Schema.String,
37
+ done: Schema.Boolean,
38
+ tenantId: Schema.String,
39
+ createdAt: Schema.Date,
40
+ }),
41
+ ),
20
42
  })
@@ -8,6 +8,13 @@ import { Schema } from 'effect'
8
8
 
9
9
  export const getContent = defineAction({
10
10
  name: 'content.get',
11
+ // Editors only — it returns the FULL row, every per-type field, for a
12
+ // caller-supplied id. Tenant scoping already makes another tenant's row read
13
+ // as absent; this decides whether an unauthenticated visitor may ask at all.
14
+ // Satisfied by a signed-in session (`auth.resolveScopes` in app.config.ts
15
+ // grants `content:read` to a matched subject); anonymous callers hold no
16
+ // scopes and are refused.
17
+ guards: [{ scope: 'content:read' }],
11
18
  input: Schema.Struct({
12
19
  type: Schema.NonEmptyString,
13
20
  id: Schema.NonEmptyString,
@@ -23,6 +23,12 @@ const FieldWire = Schema.Struct({
23
23
 
24
24
  export const contentTypes = defineAction({
25
25
  name: 'content.types',
26
+ // Editors only. It carries no row data at all — but it IS a map of your
27
+ // content model (every type, every field, every constraint), which is
28
+ // reconnaissance you have no reason to hand an anonymous caller. Guarded with
29
+ // the same `content:read` the editor already needs, so this costs a signed-in
30
+ // editor nothing and tells a stranger nothing.
31
+ guards: [{ scope: 'content:read' }],
26
32
  input: Schema.Struct({}),
27
33
  output: Schema.Struct({
28
34
  types: Schema.Array(
@@ -7,6 +7,19 @@ import { Schema } from 'effect'
7
7
 
8
8
  export const me = defineAction({
9
9
  name: 'session.me',
10
+ // The ONE open procedure in this app, and it has to be. Every `content.*`
11
+ // procedure beside it carries a `content:*` guard that an anonymous caller
12
+ // cannot satisfy — which is correct — but the SSR auth gate has to be able to
13
+ // ask "who am I" while it is still nobody, or it has nothing to branch on and
14
+ // the redirect to /login never happens.
15
+ //
16
+ // Safe because the answer is a projection of the caller's own request: the
17
+ // Subject `voltroPasswordStrategy` resolved from the cookie they sent. It
18
+ // reads no table and can say nothing about anybody else.
19
+ openAccess:
20
+ 'echoes the caller\'s own resolved Subject (type/id/tenantId) — reads no table and reveals '
21
+ + 'nothing the caller did not present. Anonymous callers must reach it, or the SSR gate '
22
+ + 'cannot redirect them to sign in.',
10
23
  input: Schema.Struct({}),
11
24
  output: Schema.Struct({
12
25
  type: Schema.String,
@@ -19,6 +19,7 @@
19
19
  // (`store: 'memory'`, in-process users); the README shows the production swaps.
20
20
  import { defineEnv, envVar } from '@voltro/env'
21
21
  import { authRoutesPlugin, memoryUserStore, voltroPasswordStrategy } from '@voltro/plugin-auth'
22
+ import { resolveScopes } from './authz'
22
23
 
23
24
  export const env = defineEnv({
24
25
  LOG_LEVEL: envVar.enum(['debug', 'info', 'warn', 'error'], { access: 'public', default: 'info' }),
@@ -57,5 +58,23 @@ export default {
57
58
  // Resolves the session cookie into the request Subject the handlers read
58
59
  // (`ctx.request.subject`) and the store spine scopes writes by.
59
60
  strategies: [voltroPasswordStrategy()],
61
+ // …and turns that identity into AUTHORITY. A session cookie carries no
62
+ // `scopes` by design (identity is settled at sign-in; authority is re-read
63
+ // per request), so without this the `content:*` guards on every procedure
64
+ // below would be unsatisfiable — the editor would boot and answer
65
+ // `ScopeError` to everyone, which is an outage, not security.
66
+ //
67
+ // It runs ONLY on a subject a strategy matched, never for anonymous. That
68
+ // asymmetry is what the guards actually enforce here: signed in → editor
69
+ // scopes; signed out → nothing. See `authz.ts` for the vocabulary and for
70
+ // what to replace this with when roles live in your own tables.
71
+ resolveScopes,
60
72
  },
73
+ // Point `voltro doctor` / `voltro check` at the scope vocabulary so the
74
+ // unknown-scope rule is LIVE here. This app does scope-based authorization
75
+ // without `@voltro/plugin-rbac`, and the plugin is what normally publishes
76
+ // that set — without this pointer the rule stays dormant and a guard naming a
77
+ // scope nothing grants (a typo, a rename) becomes a permanently uncallable
78
+ // procedure that nothing reports.
79
+ doctor: { scopeVocabulary: './authz.ts#EDITOR_SCOPES' },
61
80
  }
@@ -0,0 +1,63 @@
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
+ // ── Every procedure in this app declares an access decision ────────────────
10
+ //
11
+ // `guards: [{ scope }]` naming a scope from this file, or an `openAccess:`
12
+ // reason. The framework refuses to boot on a procedure that declares neither
13
+ // (`security.defaultDeny`, on by default). There is exactly one `openAccess`
14
+ // here — `session.me`, which an anonymous browser MUST be able to call, or
15
+ // nothing can tell it to sign in.
16
+
17
+ import type { Subject } from '@voltro/protocol'
18
+
19
+ /**
20
+ * What a signed-in editor may do.
21
+ *
22
+ * **One role, on purpose, and read the reason before you copy it.** This
23
+ * template has exactly one kind of account: an editor. Inventing `author` /
24
+ * `reviewer` / `publisher` here would be a role map written to look secure
25
+ * rather than to describe this app, and a scope every caller already holds is
26
+ * a guard that reads as protection and enforces nothing.
27
+ *
28
+ * The line these guards DO draw is real, and it is the line the template is
29
+ * about: **signed in or not.** `content.saveDraft` / `publish` / `unpublish`
30
+ * are refused to an anonymous caller, and the README's claim that only
31
+ * signed-in editors reach the write surface stops being a comment and becomes
32
+ * something the dispatch spine enforces before the executor runs.
33
+ *
34
+ * Split it the day your product has two kinds of account — by BLAST RADIUS,
35
+ * not by job title: publishing to the live site is a bigger blast than saving
36
+ * a draft, so `content:publish` is the natural second scope.
37
+ */
38
+ export const EDITOR_SCOPES = ['content:read', 'content:write'] as const
39
+
40
+ /**
41
+ * Where a caller's authority comes from.
42
+ *
43
+ * `auth.resolveScopes` runs ONLY on a subject a strategy MATCHED — never for
44
+ * anonymous, where there is no identity to look anything up for. That is
45
+ * precisely what makes these guards satisfiable AND meaningful here:
46
+ *
47
+ * • no session cookie → `voltroPasswordStrategy` skips → anonymous Subject,
48
+ * no scopes, every `content:*` guard denies.
49
+ * • valid cookie → matched `user` Subject → this runs → editor scopes.
50
+ *
51
+ * A session cookie deliberately carries no `scopes` of its own (identity is
52
+ * settled at sign-in; authority is re-read per request), so this function is
53
+ * the only thing that can grant them — which is why removing an editor's
54
+ * access takes effect on their EXISTING session rather than when it expires.
55
+ *
56
+ * PRODUCTION replaces the body with a lookup: `subject.metadata.roles` (set by
57
+ * your strategy) or a read against your own tables via the `store` the
58
+ * framework hands the resolver. Failure posture: return
59
+ * `{ kind: 'unavailable', reason }` if the lookup itself fails — the request
60
+ * then fails CLOSED with a named reason instead of an empty array being
61
+ * applied as a policy decision.
62
+ */
63
+ export const resolveScopes = (_subject: Subject): ReadonlyArray<string> => [...EDITOR_SCOPES]
@@ -7,6 +7,16 @@ import { Schema } from 'effect'
7
7
 
8
8
  export const publishContent = defineMutation({
9
9
  name: 'content.publish',
10
+ // Editors only — this is the procedure that makes content LIVE, so it is the
11
+ // one whose guard matters most. Enforced before the executor and before the
12
+ // transaction opens; satisfied by a signed-in session (`auth.resolveScopes`
13
+ // grants `content:write`), refused to an anonymous caller.
14
+ //
15
+ // When your product grows a review step, this is where the vocabulary splits:
16
+ // give publishing its own `content:publish` scope in `authz.ts` and leave
17
+ // `content:write` on saveDraft. Split by BLAST RADIUS — going live is a
18
+ // bigger blast than saving a draft.
19
+ guards: [{ scope: 'content:write' }],
10
20
  input: Schema.Struct({ type: Schema.NonEmptyString, id: Schema.NonEmptyString }),
11
21
  output: Schema.Union(
12
22
  Schema.Struct({ ok: Schema.Literal(true), id: Schema.String, status: Schema.String }),
@@ -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.34.0",
17
+ "@voltro/cms": "0.34.0",
18
+ "@voltro/database": "0.34.0",
19
+ "@voltro/env": "0.34.0",
20
+ "@voltro/plugin-auth": "0.34.0",
21
+ "@voltro/plugin-multitenancy": "0.34.0",
22
+ "@voltro/protocol": "0.34.0",
23
+ "@voltro/runtime": "0.34.0",
24
+ "@voltro/sql-postgres": "0.34.0",
25
25
  "effect": "^3.22.0"
26
26
  },
27
27
  "devDependencies": {
28
- "@voltro/testing": "0.33.0",
28
+ "@voltro/testing": "0.34.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
  })