@voltro/cli 0.33.0 → 0.35.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (343) hide show
  1. package/CHANGELOG.md +1968 -0
  2. package/bin/nodeEnvironment.d.mts +30 -0
  3. package/bin/nodeEnvironment.mjs +158 -0
  4. package/bin/voltro.mjs +69 -5
  5. package/dist/addCommand-BNeoeSxe.js +124 -0
  6. package/dist/addCommand-aXSQveak.js +2 -0
  7. package/dist/agentsMd-BTchIZku.js +2 -0
  8. package/dist/agentsMd-mhQMF1bx.js +254 -0
  9. package/dist/apiBuild-B8aoJvuw.js +2 -0
  10. package/dist/{apiBuild-h9VHtnlw.js → apiBuild-DYD_ONLD.js} +46 -46
  11. package/dist/{appGraph-gQ_6GkQQ.js → appGraph-KGDPTuTy.js} +1 -1
  12. package/dist/appGraph-zuMGKVYX.js +2 -0
  13. package/dist/appPort-B_HpJ_ck.js +48 -0
  14. package/dist/baselineCommand-C2ClWZN3.js +2 -0
  15. package/dist/baselineCommand-DIttzO8A.js +227 -0
  16. package/dist/bin.js +71 -28
  17. package/dist/build-CD8K4XOr.js +711 -0
  18. package/dist/cacheCommand-DA4OH9xt.js +42 -0
  19. package/dist/capabilitiesCommand-nq_pz5xd.js +123 -0
  20. package/dist/checkCommand-Ct9xkTrS.js +232 -0
  21. package/dist/checkCommand-DKpDLlqu.js +2 -0
  22. package/dist/{cliArgs-qdZSElM3.js → cliArgs-D4p8n7EE.js} +12 -1
  23. package/dist/cliError-BmdYnghb.js +10 -0
  24. package/dist/cliOutput-D1tSBoRM.js +15 -0
  25. package/dist/{cliRuntime-Oh517vCV.js → cliRuntime-Dh7UDinH.js} +20 -20
  26. package/dist/cloudClient-DWL-Hw_T.js +67 -0
  27. package/dist/cloudCmd-Cvv5HGaZ.js +364 -0
  28. package/dist/clusterCmd-D5wsCmA_.js +54 -0
  29. package/dist/codegen-CYM3Zqrf.js +605 -0
  30. package/dist/codegen-ChBi_hVa.js +2 -0
  31. package/dist/codegenCommand-C4YoQIc2.js +30 -0
  32. package/dist/codemodRunner-BnFq3Fgu.js +5384 -0
  33. package/dist/commandRunner-BLAEFLjp.js +47 -0
  34. package/dist/commands-BE8E7zF3.js +816 -0
  35. package/dist/connectionConfig-UFlIEiys.js +66 -0
  36. package/dist/dashboardCommand-D7SgZGaN.js +25 -0
  37. package/dist/dataCommand-BhYwDgg-.js +537 -0
  38. package/dist/dataProfile-dW-PsfLB.js +15 -0
  39. package/dist/dbCommand-DS4b97Is.js +2 -0
  40. package/dist/{dbCommand-DTLKAfbA.js → dbCommand-O8HA63s2.js} +552 -402
  41. package/dist/{dev-C_P8FLSx.js → dev-C7sFZq3m.js} +3670 -3169
  42. package/dist/dev-D2BikO7a.js +3 -0
  43. package/dist/devActivity-Dx_3nnGv.js +100 -0
  44. package/dist/devActivity.js +1 -1
  45. package/dist/dialectDriver-CgXnDfec.js +39 -0
  46. package/dist/discover-C9XKJDco.js +25 -0
  47. package/dist/doctorCommand-CM4Ch9C7.js +2 -0
  48. package/dist/{checkCommand-xGhRFFg2.js → doctorCommand-DnimF5IM.js} +523 -1278
  49. package/dist/dormancyCommand-QewYug_s.js +69 -0
  50. package/dist/e2eCmd-BRabZww-.js +147 -0
  51. package/dist/embeddingsCommand-BfiLS_QI.js +73 -0
  52. package/dist/envCommand-CCGPRQY1.js +60 -0
  53. package/dist/evalCommand-6RUfPen4.js +118 -0
  54. package/dist/evolveCommand-CHsLCtDf.js +281 -0
  55. package/dist/fileTaxonomy-CJfgOllU.js +457 -0
  56. package/dist/frameworkTableAssembly-YyVe32Cb.js +2 -0
  57. package/dist/frameworkTableAssembly-oMPBKqlE.js +511 -0
  58. package/dist/generateCommand-DbgcUpGw.js +147 -0
  59. package/dist/index.d.ts +45 -0
  60. package/dist/index.js +4 -3
  61. package/dist/infoCommand-DwOgK1t6.js +60 -0
  62. package/dist/{inspect-BUUjt773.js → inspect-CjTYzAs_.js} +113 -41
  63. package/dist/inspect-P4pxoMaV.js +2 -0
  64. package/dist/inspectCmd-EHFZ9yYu.js +224 -0
  65. package/dist/inspectFetch-EMuhTG_9.js +151 -0
  66. package/dist/inspectMetrics-CfdKLh6t.js +72 -0
  67. package/dist/loadEnv-D9nEOClM.js +44 -0
  68. package/dist/logFileSink-C_D2wRN1.js +105 -0
  69. package/dist/logsCmd-D36xK7Zu.js +260 -0
  70. package/dist/manifestBuild-COkJoyAr.js +2 -0
  71. package/dist/{manifestBuild-BLrVuSlM.js → manifestBuild-hpPLaGxV.js} +1 -1
  72. package/dist/metaCommands-7MJfZ5cf.js +196 -0
  73. package/dist/migrate-BV7I-ZHZ.js +83 -0
  74. package/dist/mssqlClusterPatch-_4cE_nun.js +44 -0
  75. package/dist/newCommand-COWOJ1_E.js +156 -0
  76. package/dist/nodeEnvironment-cGFAj1J8.js +28 -0
  77. package/dist/packageCommand-Cug_3Ogl.js +271 -0
  78. package/dist/pageConvention-cEiRxdab.js +5 -0
  79. package/dist/privacyCommand-C-Df56U_.js +146 -0
  80. package/dist/probeCommand-CZfaaUOZ.js +122 -0
  81. package/dist/projectScaffold-DmzEKHib.js +2 -0
  82. package/dist/projectScaffold-LMMtaavR.js +814 -0
  83. package/dist/renderModeScan-D7J1B7Kw.js +105 -0
  84. package/dist/renderProfile-1OWWAAtx.js +81 -0
  85. package/dist/runtimeRegistry-DMeKfTHP.js +81 -0
  86. package/dist/runtimeTrace-CH3eUiMw.js +91 -0
  87. package/dist/scheduleCmd-DQRu6BZC.js +149 -0
  88. package/dist/scheduleManifestCmd-D2x0CTTY.js +249 -0
  89. package/dist/schemaIr-UJybUUZW.js +103 -0
  90. package/dist/{sdkgen-C81QIkiL.js → sdkgen-BLkvGRfX.js} +111 -209
  91. package/dist/seedRunner-ZmLSqNe2.js +333 -0
  92. package/dist/serveCommand-CbDHU6l-.js +2 -0
  93. package/dist/serveCommand-iwlUBNS1.js +1766 -0
  94. package/dist/serveEntry.js +5 -5
  95. package/dist/serverlessCommand-CfJZy6dS.js +482 -0
  96. package/dist/start-BgN62boB.js +3 -0
  97. package/dist/start-T4VesWiM.js +1087 -0
  98. package/dist/startEntry.js +2 -2
  99. package/dist/staticCommand-Dr2M6tpU.js +304 -0
  100. package/dist/storageCommand-Co6NfLqN.js +42 -0
  101. package/dist/templates-De8IR5-c.js +102 -0
  102. package/dist/test-CI6iDsYc.js +115 -0
  103. package/dist/tracesCmd-DStmCJPi.js +232 -0
  104. package/dist/tsconfigPaths-BWXBWgcl.js +107 -0
  105. package/dist/tsxLoader-EuXmSJ1K.js +51 -0
  106. package/dist/typecheckCommand-BlsWiCNq.js +61 -0
  107. package/dist/updateCommand-BlMZhWgO.js +2 -0
  108. package/dist/updateCommand-x0pI_x-B.js +585 -0
  109. package/dist/webDev-BcykISYQ2.js +2 -0
  110. package/dist/{inspectMetrics-1xzTKAFx.js → webDev-Dybxew86.js} +988 -1560
  111. package/dist/webhookDiscovery-CrGAfhIG.js +2 -0
  112. package/dist/webhookDiscovery-D7VaeMlz.js +51 -0
  113. package/dist/webhooksCommand-DlAgS2Iw.js +267 -0
  114. package/dist/workflowsCmd-BGF-mRZ5.js +608 -0
  115. package/package.json +209 -17
  116. package/templates/AGENTS.core.md +58 -3
  117. package/templates/AGENTS.md +64 -7
  118. package/templates/agent-docs/_index.md +6 -4
  119. package/templates/agent-docs/_manifest.json +23 -6
  120. package/templates/agent-docs/ai.md +370 -0
  121. package/templates/agent-docs/authentication.md +313 -31
  122. package/templates/agent-docs/caching.md +6 -0
  123. package/templates/agent-docs/cli.md +853 -50
  124. package/templates/agent-docs/data.md +608 -12
  125. package/templates/agent-docs/database/migrations.md +223 -25
  126. package/templates/agent-docs/database/misc.md +156 -40
  127. package/templates/agent-docs/database/querying.md +19 -1
  128. package/templates/agent-docs/database/scaling.md +60 -0
  129. package/templates/agent-docs/database/schema.md +1 -0
  130. package/templates/agent-docs/database/seedsdialects.md +208 -19
  131. package/templates/agent-docs/database/transactions.md +68 -0
  132. package/templates/agent-docs/deployment.md +284 -25
  133. package/templates/agent-docs/introduction.md +88 -17
  134. package/templates/agent-docs/local-first-mobile.md +79 -4
  135. package/templates/agent-docs/multi-tenancy.md +188 -42
  136. package/templates/agent-docs/observability.md +58 -3
  137. package/templates/agent-docs/plugins/ai-flows.md +247 -2
  138. package/templates/agent-docs/plugins/analytics-postgres.md +1 -1
  139. package/templates/agent-docs/plugins/audit.md +37 -1
  140. package/templates/agent-docs/plugins/auth-social.md +143 -0
  141. package/templates/agent-docs/plugins/auth-workos.md +4 -2
  142. package/templates/agent-docs/plugins/auth.md +131 -6
  143. package/templates/agent-docs/plugins/billing.md +132 -15
  144. package/templates/agent-docs/plugins/cdc-out.md +46 -7
  145. package/templates/agent-docs/plugins/clickhouse.md +32 -1
  146. package/templates/agent-docs/plugins/duckdb.md +1 -1
  147. package/templates/agent-docs/plugins/flags.md +132 -0
  148. package/templates/agent-docs/plugins/governance.md +105 -7
  149. package/templates/agent-docs/plugins/multitenancy.md +9 -4
  150. package/templates/agent-docs/plugins/presence.md +13 -2
  151. package/templates/agent-docs/plugins/ratelimit.md +9 -0
  152. package/templates/agent-docs/plugins/search.md +162 -8
  153. package/templates/agent-docs/plugins/sso-saml.md +47 -8
  154. package/templates/agent-docs/plugins/storage.md +11 -0
  155. package/templates/agent-docs/plugins/webhooks.md +105 -0
  156. package/templates/agent-docs/plugins.md +152 -18
  157. package/templates/agent-docs/reference.md +60 -3
  158. package/templates/agent-docs/releases.md +1117 -0
  159. package/templates/agent-docs/routing.md +43 -25
  160. package/templates/agent-docs/scheduling.md +27 -0
  161. package/templates/agent-docs/schema-driven-ui.md +92 -12
  162. package/templates/agent-docs/security.md +426 -0
  163. package/templates/agent-docs/templates/apibackends.md +87 -18
  164. package/templates/agent-docs/templates/appshells.md +32 -14
  165. package/templates/agent-docs/templates/overview.md +13 -8
  166. package/templates/agent-docs/testing.md +211 -14
  167. package/templates/agent-docs/whats-new.md +98 -136
  168. package/templates/agent-docs/workflows.md +231 -14
  169. package/templates/apps/api-ai/actions/summarize.action.ts +11 -0
  170. package/templates/apps/api-ai/package.json +8 -7
  171. package/templates/apps/api-auth/actions/me.action.ts +13 -0
  172. package/templates/apps/api-auth/package.json +9 -8
  173. package/templates/apps/api-backend/mutations/notes.create.mutation.ts +9 -0
  174. package/templates/apps/api-backend/package.json +12 -8
  175. package/templates/apps/api-backend/queries/notes.query.ts +30 -8
  176. package/templates/apps/api-backend-deactivation/actions/users.get.action.ts +14 -0
  177. package/templates/apps/api-backend-deactivation/mutations/users.create.mutation.ts +8 -0
  178. package/templates/apps/api-backend-deactivation/mutations/users.deactivate.mutation.ts +10 -0
  179. package/templates/apps/api-backend-deactivation/package.json +8 -7
  180. package/templates/apps/api-backend-mail/actions/sendWelcome.action.ts +17 -0
  181. package/templates/apps/api-backend-mail/mutations/notes.create.mutation.ts +9 -0
  182. package/templates/apps/api-backend-mail/package.json +9 -8
  183. package/templates/apps/api-backend-mail/queries/notes.query.ts +30 -8
  184. package/templates/apps/api-backend-mariadb/.env.example +14 -0
  185. package/templates/apps/api-backend-mariadb/mutations/notes.create.mutation.ts +9 -0
  186. package/templates/apps/api-backend-mariadb/package.json +10 -9
  187. package/templates/apps/api-backend-mariadb/queries/notes.query.ts +30 -8
  188. package/templates/apps/api-backend-sqlite/.env.example +14 -0
  189. package/templates/apps/api-backend-sqlite/mutations/notes.create.mutation.ts +9 -0
  190. package/templates/apps/api-backend-sqlite/package.json +9 -8
  191. package/templates/apps/api-backend-sqlite/queries/notes.query.ts +30 -8
  192. package/templates/apps/api-backend-storage/actions/uploadAvatar.action.ts +13 -0
  193. package/templates/apps/api-backend-storage/actions/uploadDocument.action.ts +12 -0
  194. package/templates/apps/api-backend-storage/mutations/notes.create.mutation.ts +9 -0
  195. package/templates/apps/api-backend-storage/package.json +9 -8
  196. package/templates/apps/api-backend-storage/queries/notes.query.ts +30 -8
  197. package/templates/apps/api-cms/actions/content.get.action.ts +7 -0
  198. package/templates/apps/api-cms/actions/content.types.action.ts +6 -0
  199. package/templates/apps/api-cms/actions/me.action.ts +13 -0
  200. package/templates/apps/api-cms/app.config.ts +19 -0
  201. package/templates/apps/api-cms/authz.ts +63 -0
  202. package/templates/apps/api-cms/mutations/content.publish.mutation.ts +10 -0
  203. package/templates/apps/api-cms/mutations/content.saveDraft.mutation.ts +10 -0
  204. package/templates/apps/api-cms/mutations/content.unpublish.mutation.ts +5 -0
  205. package/templates/apps/api-cms/package.json +11 -10
  206. package/templates/apps/api-cms/queries/content.list.query.ts +23 -7
  207. package/templates/apps/api-cms/tests/accessDecisions.test.ts +121 -0
  208. package/templates/apps/api-cms/tests/content.descriptors.test.ts +8 -4
  209. package/templates/apps/api-collab/mutations/documents.create.mutation.ts +8 -0
  210. package/templates/apps/api-collab/mutations/documents.setBody.mutation.ts +13 -0
  211. package/templates/apps/api-collab/package.json +9 -8
  212. package/templates/apps/api-collab/queries/documents.query.ts +19 -7
  213. package/templates/apps/api-data-advanced/package.json +9 -8
  214. package/templates/apps/api-data-advanced/queries/authors.withBooks.query.ts +12 -0
  215. package/templates/apps/api-data-advanced/queries/books.search.query.ts +9 -0
  216. package/templates/apps/api-durable/mutations/orders.approve.mutation.ts +17 -0
  217. package/templates/apps/api-durable/mutations/orders.place.mutation.ts +9 -0
  218. package/templates/apps/api-durable/package.json +9 -8
  219. package/templates/apps/api-feature-flags/actions/notes.export.action.ts +16 -0
  220. package/templates/apps/api-feature-flags/mutations/notes.create.mutation.ts +12 -0
  221. package/templates/apps/api-feature-flags/package.json +10 -9
  222. package/templates/apps/api-governance/README.md +30 -8
  223. package/templates/apps/api-governance/actions/profiles.get.action.server.ts +17 -1
  224. package/templates/apps/api-governance/actions/profiles.get.action.ts +29 -4
  225. package/templates/apps/api-governance/database/schema.ts +16 -4
  226. package/templates/apps/api-governance/mutations/profiles.create.mutation.ts +12 -0
  227. package/templates/apps/api-governance/package.json +9 -8
  228. package/templates/apps/api-kv/actions/sync.pull.action.ts +15 -0
  229. package/templates/apps/api-kv/actions/sync.reset.action.ts +13 -0
  230. package/templates/apps/api-kv/actions/sync.status.action.ts +7 -0
  231. package/templates/apps/api-kv/package.json +9 -8
  232. package/templates/apps/api-kv/queries/events.list.query.ts +19 -8
  233. package/templates/apps/api-moderation/mutations/comments.create.mutation.ts +11 -0
  234. package/templates/apps/api-moderation/mutations/posts.create.mutation.ts +13 -0
  235. package/templates/apps/api-moderation/package.json +9 -8
  236. package/templates/apps/api-observability/mutations/notes.create.mutation.ts +8 -0
  237. package/templates/apps/api-observability/package.json +9 -8
  238. package/templates/apps/api-observability/queries/notes.list.query.ts +13 -0
  239. package/templates/apps/api-ratelimit/mutations/notes.create.mutation.ts +14 -0
  240. package/templates/apps/api-ratelimit/package.json +9 -8
  241. package/templates/apps/api-rbac/package.json +9 -8
  242. package/templates/apps/api-rest/package.json +8 -7
  243. package/templates/apps/api-saas/mutations/projects.create.mutation.ts +13 -0
  244. package/templates/apps/api-saas/package.json +12 -11
  245. package/templates/apps/api-saas/queries/projects.list.query.ts +11 -0
  246. package/templates/apps/api-saas-starter/actions/me.action.ts +13 -0
  247. package/templates/apps/api-saas-starter/app.config.ts +19 -0
  248. package/templates/apps/api-saas-starter/authz.ts +75 -0
  249. package/templates/apps/api-saas-starter/mutations/invites.create.mutation.ts +10 -0
  250. package/templates/apps/api-saas-starter/mutations/projects.create.mutation.ts +9 -0
  251. package/templates/apps/api-saas-starter/package.json +15 -11
  252. package/templates/apps/api-saas-starter/queries/invites.list.query.ts +19 -8
  253. package/templates/apps/api-saas-starter/queries/projects.list.query.ts +18 -7
  254. package/templates/apps/api-saas-starter/tests/accessDecisions.test.ts +135 -0
  255. package/templates/apps/api-search/mutations/articles.create.mutation.ts +14 -0
  256. package/templates/apps/api-search/package.json +9 -8
  257. package/templates/apps/api-search/queries/articles.list.query.ts +21 -8
  258. package/templates/apps/api-status/README.md +10 -3
  259. package/templates/apps/api-status/app.config.ts +8 -3
  260. package/templates/apps/api-status/authz.ts +5 -3
  261. package/templates/apps/api-status/package.json +9 -8
  262. package/templates/apps/api-status/queries/components.list.query.ts +14 -6
  263. package/templates/apps/api-status/queries/incidents.live.query.ts +23 -10
  264. package/templates/apps/api-status/queries/updates.list.query.ts +16 -9
  265. package/templates/apps/api-status/tests/status.test.ts +9 -1
  266. package/templates/apps/api-versioning/actions/documents.asOf.action.ts +12 -0
  267. package/templates/apps/api-versioning/actions/documents.history.action.ts +13 -0
  268. package/templates/apps/api-versioning/mutations/documents.create.mutation.ts +9 -0
  269. package/templates/apps/api-versioning/mutations/documents.update.mutation.ts +12 -0
  270. package/templates/apps/api-versioning/package.json +9 -8
  271. package/templates/apps/api-webhooks/mutations/orders.fulfill.mutation.ts +16 -0
  272. package/templates/apps/api-webhooks/package.json +10 -9
  273. package/templates/apps/api-webhooks/queries/orders.list.query.ts +10 -0
  274. package/templates/apps/changelog/package.json +8 -6
  275. package/templates/apps/edge-functions/package.json +2 -2
  276. package/templates/apps/frontend-admin/package.json +10 -8
  277. package/templates/apps/frontend-admin/src/lib/admin.ts +20 -9
  278. package/templates/apps/frontend-admin/src/locales/de.ts +11 -1
  279. package/templates/apps/frontend-admin/src/locales/en.ts +13 -1
  280. package/templates/apps/frontend-admin/src/pages/admin/[entity]/page.tsx +65 -22
  281. package/templates/apps/frontend-admin/src/pages/admin/entity.test.tsx +130 -23
  282. package/templates/apps/frontend-admin/src/pages/admin/layout.tsx +3 -2
  283. package/templates/apps/frontend-admin/src/pages/admin/page.test.tsx +19 -2
  284. package/templates/apps/frontend-admin/src/pages/admin/page.tsx +9 -4
  285. package/templates/apps/frontend-app/app.config.ts +4 -3
  286. package/templates/apps/frontend-app/package.json +11 -8
  287. package/templates/apps/frontend-app/src/lib/api.ts +25 -0
  288. package/templates/apps/frontend-app/src/pages/page.test.tsx +130 -82
  289. package/templates/apps/frontend-app/src/pages/page.tsx +14 -18
  290. package/templates/apps/frontend-auth/package.json +10 -8
  291. package/templates/apps/frontend-blank/package.json +9 -7
  292. package/templates/apps/frontend-cms/package.json +11 -9
  293. package/templates/apps/frontend-collab/package.json +12 -9
  294. package/templates/apps/frontend-collab/src/pages/page.test.tsx +122 -78
  295. package/templates/apps/frontend-contact/package.json +9 -7
  296. package/templates/apps/frontend-dashboard/package.json +9 -7
  297. package/templates/apps/frontend-docs/package.json +9 -7
  298. package/templates/apps/frontend-i18n/package.json +8 -6
  299. package/templates/apps/frontend-landing/package.json +9 -7
  300. package/templates/apps/frontend-portal/package.json +10 -8
  301. package/templates/apps/frontend-saas/app.config.ts +10 -6
  302. package/templates/apps/frontend-saas/package.json +10 -8
  303. package/templates/apps/frontend-saas/src/lib/api.ts +27 -32
  304. package/templates/apps/frontend-saas/src/pages/dashboard/billing/page.tsx +7 -8
  305. package/templates/apps/frontend-saas/src/pages/dashboard/page.test.tsx +27 -3
  306. package/templates/apps/frontend-saas/src/pages/dashboard/page.tsx +4 -4
  307. package/templates/apps/frontend-saas/src/pages/dashboard/team/page.tsx +3 -4
  308. package/templates/apps/frontend-spa/package.json +9 -7
  309. package/templates/apps/frontend-ssr/package.json +9 -7
  310. package/templates/apps/frontend-ssr-api/package.json +10 -8
  311. package/templates/apps/frontend-static-blog/package.json +8 -6
  312. package/templates/apps/frontend-status/package.json +10 -8
  313. package/templates/apps/mobile-app/README.md +1 -0
  314. package/templates/apps/mobile-app/package.json +4 -2
  315. package/templates/apps/mobile-app/src/app/index.tsx +22 -12
  316. package/templates/apps/mobile-app/src/app/orders/[id].tsx +1 -1
  317. package/templates/apps/mobile-app/src/lib/api.ts +34 -0
  318. package/templates/apps/mobile-app/voltro.mobile.ts +4 -2
  319. package/templates/baselines/bare/.env.example +14 -0
  320. package/templates/baselines/bare/baseline.json +4 -4
  321. package/templates/baselines/compose/.env.example +14 -0
  322. package/templates/baselines/compose/README.md +1 -1
  323. package/templates/baselines/compose/baseline.json +5 -5
  324. package/templates/baselines/compose-mariadb/.env.example +14 -0
  325. package/templates/baselines/compose-mariadb/README.md +1 -1
  326. package/templates/baselines/compose-mariadb/baseline.json +5 -5
  327. package/templates/baselines/helm/.env.example +14 -0
  328. package/templates/baselines/helm/baseline.json +4 -4
  329. package/dist/apiBuild-C-x9YacA.js +0 -2
  330. package/dist/appGraph-CvQCte0z.js +0 -2
  331. package/dist/checkCommand-DRovTKza.js +0 -2
  332. package/dist/commands-CJfepbm4.js +0 -11541
  333. package/dist/dbCommand-b1gum4td.js +0 -2
  334. package/dist/dev-iiMtlkfs.js +0 -3
  335. package/dist/devActivity-BhIu6ncs.js +0 -159
  336. package/dist/frameworkTableAssembly-BwIrO5nv.js +0 -638
  337. package/dist/frameworkTableAssembly-D-EebUQX.js +0 -2
  338. package/dist/inspect-mmBuRXmy.js +0 -2
  339. package/dist/manifestBuild-Dj8Jjoto.js +0 -2
  340. package/dist/seedRunner-Bqxgp7HZ.js +0 -230
  341. package/dist/serveCommand-DdaM4Hup.js +0 -1608
  342. package/dist/start-C0koT0UO.js +0 -1084
  343. /package/templates/apps/api-ai/actions/{summarize.action.server.tsx → summarize.action.server.ts} +0 -0
@@ -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,
@@ -9,6 +9,15 @@ import { Schema } from 'effect'
9
9
  export const createProject = defineMutation({
10
10
  name: 'projects.create',
11
11
  target: { table: 'projects', op: 'insert' },
12
+ // Members only. Note the ORDER, because it is the whole reason this line
13
+ // exists next to `requireEntitlement`: the guard runs in the dispatch spine
14
+ // BEFORE the executor and before the transaction opens, so an anonymous call
15
+ // is refused without ever consuming a paying tenant's metered quota. A quota
16
+ // gate alone would let a stranger burn the free plan's three projects.
17
+ //
18
+ // Satisfiable as shipped: a matched session gets `projects:write` from
19
+ // `auth.resolveScopes` (authz.ts); anonymous callers hold no scopes.
20
+ guards: [{ scope: 'projects:write' }],
12
21
  input: Schema.Struct({ name: Schema.NonEmptyString }),
13
22
  output: Schema.Struct({
14
23
  id: Schema.String,
@@ -3,30 +3,34 @@
3
3
  "version": "0.0.0",
4
4
  "private": true,
5
5
  "type": "module",
6
+ "exports": {
7
+ "./rpcGroup": "./rpcGroup.generated.ts"
8
+ },
6
9
  "scripts": {
7
10
  "dev": "voltro dev .",
8
11
  "lint": "voltro doctor .",
9
12
  "migrate": "voltro migrate",
10
13
  "test": "voltro test",
11
- "typecheck": "tsc --noEmit"
14
+ "typecheck": "voltro codegen . && tsc --noEmit"
12
15
  },
13
16
  "dependencies": {
14
17
  "@effect/platform": "^0.97.0",
15
18
  "@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-auth": "0.33.0",
20
- "@voltro/plugin-billing": "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",
19
+ "@voltro/cli": "0.35.0",
20
+ "@voltro/database": "0.35.0",
21
+ "@voltro/env": "0.35.0",
22
+ "@voltro/plugin-auth": "0.35.0",
23
+ "@voltro/plugin-billing": "0.35.0",
24
+ "@voltro/plugin-multitenancy": "0.35.0",
25
+ "@voltro/protocol": "0.35.0",
26
+ "@voltro/runtime": "0.35.0",
27
+ "@voltro/sql-postgres": "0.35.0",
25
28
  "effect": "^3.22.0"
26
29
  },
27
30
  "devDependencies": {
28
- "@voltro/testing": "0.33.0",
31
+ "@voltro/testing": "0.35.0",
29
32
  "typescript": "^6.0.3",
33
+ "@vitest/coverage-v8": "^4.1.10",
30
34
  "vitest": "^4.1.10"
31
35
  }
32
36
  }
@@ -1,17 +1,28 @@
1
1
  // Streaming subscription of the tenant's teammate invites. Tenant-scoped like
2
2
  // projects.list — read live from the frontend's team page with
3
- // `useSubscription('app', 'invites.list')`.
3
+ // `useSubscription('invites.list')`.
4
4
  import { defineQuery } from '@voltro/protocol'
5
5
  import { Schema } from 'effect'
6
6
 
7
7
  export const listInvites = defineQuery({
8
8
  name: 'invites.list',
9
+ // Members only — and of the four domain procedures this is the one whose
10
+ // guard is load-bearing rather than tidy: the rows are teammates' EMAIL
11
+ // ADDRESSES. `tenant()` keeps them inside the tenant; this keeps them away
12
+ // from a caller who has not signed in to that tenant at all.
13
+ //
14
+ // Satisfiable as shipped: a matched session gets `team:read` from
15
+ // `auth.resolveScopes` (authz.ts). Anonymous callers hold no scopes and are
16
+ // refused with a typed `ScopeError`.
17
+ guards: [{ scope: 'team:read' }],
9
18
  input: Schema.Struct({}),
10
- output: Schema.Struct({
11
- id: Schema.String,
12
- email: Schema.String,
13
- status: Schema.String,
14
- tenantId: Schema.String,
15
- createdAt: Schema.Date,
16
- }),
19
+ output: Schema.Array(
20
+ Schema.Struct({
21
+ id: Schema.String,
22
+ email: Schema.String,
23
+ status: Schema.String,
24
+ tenantId: Schema.String,
25
+ createdAt: Schema.Date,
26
+ }),
27
+ ),
17
28
  })
@@ -1,6 +1,6 @@
1
1
  // Streaming subscription of the tenant's projects. Every change to `projects`
2
2
  // for the caller's tenant lands as a delta — the frontend upgrades its first
3
- // paint to live with `useSubscription('app', 'projects.list')`. The runtime
3
+ // paint to live with `useSubscription('projects.list')`. The runtime
4
4
  // AND-merges the tenant predicate (the table carries `tenant()`), so this never
5
5
  // leaks across tenants — no manual `eq('tenantId', ...)` needed.
6
6
  import { defineQuery } from '@voltro/protocol'
@@ -8,11 +8,22 @@ import { Schema } from 'effect'
8
8
 
9
9
  export const listProjects = defineQuery({
10
10
  name: 'projects.list',
11
+ // Members only. `tenant()` decides WHICH rows; this decides WHO may open the
12
+ // subscription at all — and the guard is re-checked on EVERY delivery, so a
13
+ // member whose access is revoked mid-session stops receiving rows instead of
14
+ // keeping a live feed open on a grant that has expired.
15
+ //
16
+ // Satisfiable by this app as shipped: sign in → `voltroPasswordStrategy`
17
+ // matches the cookie → `auth.resolveScopes` (authz.ts) grants
18
+ // `projects:read`. Anonymous → no scopes → refused.
19
+ guards: [{ scope: 'projects:read' }],
11
20
  input: Schema.Struct({}),
12
- output: Schema.Struct({
13
- id: Schema.String,
14
- name: Schema.String,
15
- tenantId: Schema.String,
16
- createdAt: Schema.Date,
17
- }),
21
+ output: Schema.Array(
22
+ Schema.Struct({
23
+ id: Schema.String,
24
+ name: Schema.String,
25
+ tenantId: Schema.String,
26
+ createdAt: Schema.Date,
27
+ }),
28
+ ),
18
29
  })
@@ -0,0 +1,135 @@
1
+ // The app's ACCESS POSTURE, asserted from BOTH sides against the real resolver.
2
+ //
3
+ // Why this file exists rather than one guard assertion per handler test: a
4
+ // per-procedure test hands `invoke` a subject it constructed itself, so it
5
+ // passes with a scope literal no real caller could ever obtain. The question it
6
+ // structurally cannot ask is the one that matters —
7
+ // **can the caller this app actually has satisfy these guards, and is there
8
+ // anything that still cannot?** Both halves, or the suite stays green on an app
9
+ // that is either wide open or completely dead.
10
+ //
11
+ // The second failure is the easy one to ship by accident. A guard nobody can
12
+ // satisfy is not strict security; it is a 100% outage wearing security's
13
+ // clothes, and every unit test in this directory keeps passing while the
14
+ // dashboard answers `ScopeError` to its own sign-in flow. So the subjects below
15
+ // are built the way the framework builds them (`applyScopeDecision` over this
16
+ // app's real `resolveScopes`), never by writing the scopes the assertions want.
17
+
18
+ import { describe, it, expect } from 'vitest'
19
+ import {
20
+ anonymousSubject,
21
+ applyScopeDecision,
22
+ checkGuards,
23
+ ScopeError,
24
+ type Subject,
25
+ } from '@voltro/protocol'
26
+ import { MEMBER_SCOPES, resolveScopes } from '../authz'
27
+
28
+ import { listProjects } from '../queries/projects.list.query'
29
+ import { listInvites } from '../queries/invites.list.query'
30
+ import { createProject } from '../mutations/projects.create.mutation'
31
+ import { createInvite } from '../mutations/invites.create.mutation'
32
+ import { me } from '../actions/me.action'
33
+
34
+ /** The Subject a browser with no session cookie gets: `voltroPasswordStrategy`
35
+ * finds nothing to match, so the composer falls back to anonymous. */
36
+ const visitor = anonymousSubject('acme')
37
+
38
+ /** The Subject a signed-in member gets. The cookie resolves to a `user` Subject
39
+ * carrying NO scopes (a cookie never carries authority), then the composer
40
+ * applies this app's own `resolveScopes`. Built through the framework's
41
+ * `applyScopeDecision` so this file cannot disagree with the runtime about how
42
+ * a grant lands on a subject. */
43
+ const member = (): Subject => {
44
+ const matched: Subject = { type: 'user', id: 'user_1', tenantId: 'acme', scopes: [] }
45
+ return applyScopeDecision(matched, { kind: 'grant', scopes: resolveScopes(matched) }).subject
46
+ }
47
+
48
+ /** Exactly what the dispatch spine asks before an executor runs — and, for the
49
+ * two creates, before `requireEntitlement` can spend the tenant's quota. */
50
+ const decide = (
51
+ descriptor: { readonly name: string; readonly guards?: ReadonlyArray<unknown> | undefined },
52
+ subject: Subject,
53
+ ): ScopeError | null =>
54
+ checkGuards(
55
+ subject,
56
+ descriptor.guards as Parameters<typeof checkGuards>[1],
57
+ { defaultDeny: true, procedure: descriptor.name },
58
+ )
59
+
60
+ const GUARDED = [listProjects, listInvites, createProject, createInvite] as const
61
+
62
+ describe('the caller this app actually has — a signed-in member', () => {
63
+ // If any of these regress the dashboard boots and shows nothing but denials,
64
+ // which no other test in this directory can see.
65
+ it.each(GUARDED.map((d) => [d.name, d] as const))('%s is callable', (_name, descriptor) => {
66
+ expect(decide(descriptor, member())).toBeNull()
67
+ })
68
+
69
+ it('is not just passing because the guards are empty', () => {
70
+ // The silent-zero shape: `checkGuards` with no guards returns null too.
71
+ for (const d of GUARDED) expect(d.guards?.length ?? 0).toBeGreaterThan(0)
72
+ expect(GUARDED).toHaveLength(4)
73
+ })
74
+ })
75
+
76
+ describe('the caller with no session — every domain procedure is refused', () => {
77
+ it.each(GUARDED.map((d) => [d.name, d] as const))('%s denies an anonymous visitor', (_name, descriptor) => {
78
+ expect(decide(descriptor, visitor)).toBeInstanceOf(ScopeError)
79
+ })
80
+
81
+ it('so a stranger cannot read teammate emails or spend a seat', () => {
82
+ // The two that carry real cost. `invites.list` returns email addresses;
83
+ // `invites.create` consumes the tenant's metered `seats` entitlement, and
84
+ // the guard runs BEFORE that gate — a quota is not an access control.
85
+ expect(decide(listInvites, visitor)).toBeInstanceOf(ScopeError)
86
+ expect(decide(createInvite, visitor)).toBeInstanceOf(ScopeError)
87
+ })
88
+
89
+ it('…and that is structural, not a resolver quirk', () => {
90
+ // Pinned so a framework change would surface here: `applyScopeDecision`
91
+ // returns an anonymous Subject UNTOUCHED under both decision kinds (the
92
+ // schema has no `scopes` field to write). `auth.resolveScopes` also never
93
+ // runs for one — it fires only on a MATCHED subject — so a session cookie
94
+ // is the only way into the granted set.
95
+ const granted = applyScopeDecision(visitor, { kind: 'grant', scopes: [...MEMBER_SCOPES] })
96
+ expect(granted.subject).toEqual(visitor)
97
+ expect(decide(createProject, granted.subject)).toBeInstanceOf(ScopeError)
98
+ })
99
+ })
100
+
101
+ describe('session.me stays open, and that is the point', () => {
102
+ it('answers an anonymous caller instead of denying them', () => {
103
+ // A guard here would return `ScopeError` to the dashboard's SSR gate, which
104
+ // would then have nothing to branch on and would never redirect to /login.
105
+ expect(decide(me, visitor)).toBeNull()
106
+ expect(me.openAccess).toBeTruthy()
107
+ })
108
+ })
109
+
110
+ describe('the vocabulary is a boundary, not a rubber stamp', () => {
111
+ it('grants no wildcard and no admin bypass', () => {
112
+ expect(MEMBER_SCOPES).not.toContain('*')
113
+ expect(MEMBER_SCOPES).not.toContain('admin:full')
114
+ })
115
+
116
+ it('separates inviting a person from creating a project', () => {
117
+ // Split by blast radius: one spends a seat and lets an outsider into the
118
+ // tenant, the other does not. Collapsing them into one scope is what makes
119
+ // a later `owner` role impossible to express.
120
+ const scopeOf = (d: { readonly guards?: ReadonlyArray<unknown> | undefined }): string =>
121
+ ((d.guards ?? []) as ReadonlyArray<{ scope?: string }>)[0]?.scope ?? ''
122
+ expect(scopeOf(createInvite)).not.toBe(scopeOf(createProject))
123
+ })
124
+
125
+ it('every guard names a scope the resolver actually grants', () => {
126
+ // The other half of unsatisfiability: a typo'd scope makes a procedure
127
+ // permanently uncallable, and nothing else in this app would notice.
128
+ const granted = new Set(resolveScopes(member()))
129
+ for (const d of GUARDED) {
130
+ for (const g of (d.guards ?? []) as ReadonlyArray<{ scope?: string }>) {
131
+ expect(granted.has(g.scope ?? '')).toBe(true)
132
+ }
133
+ }
134
+ })
135
+ })
@@ -8,6 +8,20 @@ import { Schema } from 'effect'
8
8
 
9
9
  export const createArticle = defineMutation({
10
10
  name: 'articles.create',
11
+ // Open — with one consequence worth naming because it reaches OUTSIDE the
12
+ // database: on commit the search plugin's ChangeEvent tap mirrors the row
13
+ // into the external index (Typesense / Meilisearch / Algolia). So an
14
+ // unguarded write here is an unguarded write there too, and the index is
15
+ // usually not tenant-partitioned by the same machinery your tables are.
16
+ //
17
+ // `assertOwnTenant` still rejects a `tenantId` that does not match the
18
+ // resolved subject, and no auth strategy or rbac ships in this template, so a
19
+ // `guards: [{ scope: 'articles:write' }]` would deny every caller. Add an
20
+ // identity, then that guard — before you point `search` at a real cluster.
21
+ openAccess:
22
+ 'inserts an article carrying only caller-supplied fields into the caller\'s own tenant '
23
+ + '(`assertOwnTenant` rejects a mismatch). On commit it also mirrors the row into the '
24
+ + 'external search index, so guard it before pointing the plugin at a real cluster.',
11
25
  target: {
12
26
  table: 'articles',
13
27
  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-search": "0.33.0",
21
- "@voltro/protocol": "0.33.0",
22
- "@voltro/runtime": "0.33.0",
16
+ "@voltro/cli": "0.35.0",
17
+ "@voltro/database": "0.35.0",
18
+ "@voltro/env": "0.35.0",
19
+ "@voltro/plugin-multitenancy": "0.35.0",
20
+ "@voltro/plugin-search": "0.35.0",
21
+ "@voltro/protocol": "0.35.0",
22
+ "@voltro/runtime": "0.35.0",
23
23
  "effect": "^3.22.0"
24
24
  },
25
25
  "devDependencies": {
26
- "@voltro/testing": "0.33.0",
26
+ "@voltro/testing": "0.35.0",
27
27
  "typescript": "^6.0.3",
28
+ "@vitest/coverage-v8": "^4.1.10",
28
29
  "vitest": "^4.1.10"
29
30
  }
30
31
  }
@@ -7,13 +7,26 @@ import { Schema } from 'effect'
7
7
 
8
8
  export const listArticles = defineQuery({
9
9
  name: 'articles.list',
10
+ // Open: the rows are this template's own seeded demo articles
11
+ // (`seeds/articles.seed.ts`), and `tenant()` confines every delivery to the
12
+ // tenantId on the request. No auth strategy and no rbac ship here, so a
13
+ // `guards: [{ scope }]` would be unsatisfiable and deny 100% of traffic.
14
+ //
15
+ // The plugin's own `search.query` procedure is a SEPARATE surface with its
16
+ // own decision, made by the plugin — this line covers only the list.
17
+ openAccess:
18
+ 'lists the request tenant\'s seeded demo articles (`tenant()` scopes every delivery); the '
19
+ + 'seed holds no personal data. No auth strategy ships here, so the tenant comes from the '
20
+ + 'caller\'s `x-tenant` header.',
10
21
  input: Schema.Struct({}),
11
- output: Schema.Struct({
12
- id: Schema.String,
13
- title: Schema.String,
14
- body: Schema.String,
15
- tag: Schema.String,
16
- tenantId: Schema.String,
17
- createdAt: Schema.Date,
18
- }),
22
+ output: Schema.Array(
23
+ Schema.Struct({
24
+ id: Schema.String,
25
+ title: Schema.String,
26
+ body: Schema.String,
27
+ tag: Schema.String,
28
+ tenantId: Schema.String,
29
+ createdAt: Schema.Date,
30
+ }),
31
+ ),
19
32
  })
@@ -14,15 +14,22 @@ voltro dev
14
14
 
15
15
  A status page is read by everyone and written by a few — so:
16
16
 
17
- - **Reads are PUBLIC.** `incidents.live`, `updates.list`, `components.list`
18
- carry **no guard**, so an anonymous browser can `useSubscription('app',
19
- 'incidents.live')` and the timeline updates live.
17
+ - **Reads are PUBLIC.** `incidents.live`, `updates.list`, `components.list` each
18
+ declare `openAccess: '<why>'`, so an anonymous browser can
19
+ `useSubscription('app', 'incidents.live')` and the timeline updates live.
20
20
  - **Writes are OPERATOR-ONLY.** Every mutation declares
21
21
  `guards: [{ scope: 'status:write' }]`, enforced by `@voltro/plugin-rbac` in the
22
22
  dispatch spine before the executor. The demo resolver (`authz.ts`) grants the
23
23
  `operator` role to the `ops` tenant; everyone else — anonymous included — is
24
24
  denied.
25
25
 
26
+ Note that **both** of those are DECISIONS, and the framework requires one or the
27
+ other: under `security.defaultDeny` (on by default) a wire-exposed procedure
28
+ that declares neither `guards:` nor `openAccess:` refuses to boot. `openAccess`
29
+ is not the lax option — it is the honest spelling of an open endpoint, and the
30
+ reason it carries is what a reviewer reads and what `voltro doctor` prints. This
31
+ template is the clearest place in the library to see the two side by side.
32
+
26
33
  ## Surface
27
34
 
28
35
  | Tag | Kind | Guard |
@@ -2,10 +2,15 @@
2
2
  //
3
3
  // The shape of a status page is asymmetric, and this config encodes it:
4
4
  // • READS are PUBLIC — `incidents.live` / `updates.list` / `components.list`
5
- // carry NO guard, so an anonymous visitor's browser can subscribe and see
6
- // the live timeline. (A status page that requires a login is useless during
7
- // an outage.)
5
+ // each declare `openAccess: '<why>'`, so an anonymous visitor's browser can
6
+ // subscribe and see the live timeline. (A status page that requires a login
7
+ // is useless during an outage.)
8
8
  // • WRITES are OPERATOR-ONLY — every mutation declares a `status:write` guard.
9
+ //
10
+ // Both halves are DECISIONS, and the framework insists on one or the other:
11
+ // under `security.defaultDeny` a wire-exposed procedure declaring neither does
12
+ // not boot. `openAccess` is not the lax option — it is the honest spelling of an
13
+ // open endpoint, and it leaves the reason where a reviewer will read it.
9
14
  // `rbacPlugin` resolves each caller's ROLES to SCOPES and the framework
10
15
  // enforces the guard in the dispatch spine, before the executor. `voltro
11
16
  // check` can see the guard statically.
@@ -3,9 +3,11 @@
3
3
  // test that redeclares the roles only proves its own copy is self-consistent).
4
4
  //
5
5
  // A status page has exactly two kinds of caller: the PUBLIC (anonymous
6
- // visitors, who only READ — the live queries carry no guard) and OPERATORS
7
- // (who post incidents). So there is one write scope, `status:write`, held by
8
- // the `operator` role; `admin` gets the wildcard bypass.
6
+ // visitors, who only READ — the live queries declare `openAccess` with their
7
+ // reason) and OPERATORS (who post incidents). So there is one write scope,
8
+ // `status:write`, held by the `operator` role; `admin` gets the wildcard
9
+ // bypass. Both kinds of caller are DECIDED about: `security.defaultDeny`
10
+ // refuses to boot a procedure that declares neither form.
9
11
  //
10
12
  // This map is ALSO the app's declared scope vocabulary: `rbacPlugin` publishes
11
13
  // its union, and `voltro check` errors on any descriptor guard requiring a
@@ -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-rbac": "0.33.0",
20
- "@voltro/protocol": "0.33.0",
21
- "@voltro/runtime": "0.33.0",
22
- "@voltro/sql-postgres": "0.33.0",
16
+ "@voltro/cli": "0.35.0",
17
+ "@voltro/database": "0.35.0",
18
+ "@voltro/env": "0.35.0",
19
+ "@voltro/plugin-rbac": "0.35.0",
20
+ "@voltro/protocol": "0.35.0",
21
+ "@voltro/runtime": "0.35.0",
22
+ "@voltro/sql-postgres": "0.35.0",
23
23
  "effect": "^3.22.0"
24
24
  },
25
25
  "devDependencies": {
26
- "@voltro/testing": "0.33.0",
26
+ "@voltro/testing": "0.35.0",
27
27
  "typescript": "^6.0.3",
28
+ "@vitest/coverage-v8": "^4.1.10",
28
29
  "vitest": "^4.1.10"
29
30
  }
30
31
  }
@@ -1,13 +1,21 @@
1
- // Live list of service components + their current status. Public (no guard).
1
+ // Live list of service components + their current status public by design.
2
+ // `components.create` beside it is `status:write`-guarded: reads are for
3
+ // visitors, writes are for operators.
2
4
  import { defineQuery } from '@voltro/protocol'
3
5
  import { Schema } from 'effect'
4
6
 
5
7
  export const componentsList = defineQuery({
6
8
  name: 'components.list',
9
+ openAccess:
10
+ 'the public component roster a status page exists to publish — this operator\'s own '
11
+ + 'service names and their current status, nothing caller-derived and no personal data. '
12
+ + 'Writing that roster is `status:write`-guarded.',
7
13
  input: Schema.Struct({}),
8
- output: Schema.Struct({
9
- id: Schema.String,
10
- name: Schema.String,
11
- status: Schema.String,
12
- }),
14
+ output: Schema.Array(
15
+ Schema.Struct({
16
+ id: Schema.String,
17
+ name: Schema.String,
18
+ status: Schema.String,
19
+ }),
20
+ ),
13
21
  })