@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
  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.34.0",
20
+ "@voltro/database": "0.34.0",
21
+ "@voltro/env": "0.34.0",
22
+ "@voltro/plugin-auth": "0.34.0",
23
+ "@voltro/plugin-billing": "0.34.0",
24
+ "@voltro/plugin-multitenancy": "0.34.0",
25
+ "@voltro/protocol": "0.34.0",
26
+ "@voltro/runtime": "0.34.0",
27
+ "@voltro/sql-postgres": "0.34.0",
25
28
  "effect": "^3.22.0"
26
29
  },
27
30
  "devDependencies": {
28
- "@voltro/testing": "0.33.0",
31
+ "@voltro/testing": "0.34.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.34.0",
17
+ "@voltro/database": "0.34.0",
18
+ "@voltro/env": "0.34.0",
19
+ "@voltro/plugin-multitenancy": "0.34.0",
20
+ "@voltro/plugin-search": "0.34.0",
21
+ "@voltro/protocol": "0.34.0",
22
+ "@voltro/runtime": "0.34.0",
23
23
  "effect": "^3.22.0"
24
24
  },
25
25
  "devDependencies": {
26
- "@voltro/testing": "0.33.0",
26
+ "@voltro/testing": "0.34.0",
27
27
  "typescript": "^6.0.3",
28
+ "@vitest/coverage-v8": "^4.1.10",
28
29
  "vitest": "^4.1.10"
29
30
  }
30
31
  }
@@ -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.34.0",
17
+ "@voltro/database": "0.34.0",
18
+ "@voltro/env": "0.34.0",
19
+ "@voltro/plugin-rbac": "0.34.0",
20
+ "@voltro/protocol": "0.34.0",
21
+ "@voltro/runtime": "0.34.0",
22
+ "@voltro/sql-postgres": "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
  }
@@ -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
  })
@@ -1,20 +1,33 @@
1
- // The live incident feed — the heart of the status page. NO `guards:`, so an
2
- // anonymous visitor's browser can subscribe: `useSubscription('app',
1
+ // The live incident feed — the heart of the status page. Deliberately OPEN, so
2
+ // an anonymous visitor's browser can subscribe: `useSubscription('app',
3
3
  // 'incidents.live')`. Every insert/update to `incidents` lands as a delta, so a
4
4
  // newly-declared incident (or a resolve) shows up on every open status page
5
5
  // within seconds — no polling.
6
+ //
7
+ // This template is the clearest place in the library to see that `openAccess:`
8
+ // and `guards:` are two spellings of the SAME obligation, not a strict option
9
+ // and a lax one. The reads here are open on purpose; the four mutations beside
10
+ // them carry `guards: [{ scope: 'status:write' }]` and are satisfiable through
11
+ // the real role map in `authz.ts`. Both are decisions. What the framework
12
+ // refuses to boot on is neither.
6
13
  import { defineQuery } from '@voltro/protocol'
7
14
  import { Schema } from 'effect'
8
15
 
9
16
  export const incidentsLive = defineQuery({
10
17
  name: 'incidents.live',
18
+ openAccess:
19
+ 'the public incident feed a status page exists to publish — title, impact, status and '
20
+ + 'timestamps of THIS operator\'s own incidents, nothing caller-derived and no personal '
21
+ + 'data. A status page that requires a login is useless during the outage it reports.',
11
22
  input: Schema.Struct({}),
12
- output: Schema.Struct({
13
- id: Schema.String,
14
- title: Schema.String,
15
- impact: Schema.String,
16
- status: Schema.String,
17
- startedAt: Schema.Date,
18
- resolvedAt: Schema.NullOr(Schema.Date),
19
- }),
23
+ output: Schema.Array(
24
+ Schema.Struct({
25
+ id: Schema.String,
26
+ title: Schema.String,
27
+ impact: Schema.String,
28
+ status: Schema.String,
29
+ startedAt: Schema.Date,
30
+ resolvedAt: Schema.NullOr(Schema.Date),
31
+ }),
32
+ ),
20
33
  })
@@ -1,17 +1,24 @@
1
- // Live feed of incident timeline entries. Public (no guard). The page joins
1
+ // Live feed of incident timeline entries public by design. The page joins
2
2
  // these to incidents by `incidentId` client-side to render each incident's
3
- // update history.
3
+ // update history. `incidents.update`, which writes them, is
4
+ // `status:write`-guarded.
4
5
  import { defineQuery } from '@voltro/protocol'
5
6
  import { Schema } from 'effect'
6
7
 
7
8
  export const updatesList = defineQuery({
8
9
  name: 'updates.list',
10
+ openAccess:
11
+ 'the public incident timeline a status page exists to publish — the operator\'s own '
12
+ + 'update posts, nothing caller-derived and no personal data. Posting one is '
13
+ + '`status:write`-guarded.',
9
14
  input: Schema.Struct({}),
10
- output: Schema.Struct({
11
- id: Schema.String,
12
- incidentId: Schema.String,
13
- body: Schema.String,
14
- status: Schema.String,
15
- createdAt: Schema.Date,
16
- }),
15
+ output: Schema.Array(
16
+ Schema.Struct({
17
+ id: Schema.String,
18
+ incidentId: Schema.String,
19
+ body: Schema.String,
20
+ status: Schema.String,
21
+ createdAt: Schema.Date,
22
+ }),
23
+ ),
17
24
  })
@@ -54,10 +54,18 @@ describe('incidents.create — operator-gated', () => {
54
54
  })
55
55
 
56
56
  describe('incidents.live — reads are PUBLIC', () => {
57
- it('an anonymous visitor may subscribe (no guard on the query)', async () => {
57
+ it('an anonymous visitor may subscribe the query declares `openAccess`', async () => {
58
58
  const ctx = ctxFor(subjectFor(null, null, 'anonymous'))
59
59
  await expect(invoke(incidentsLive, incidentsLiveHandler, {}, ctx)).resolves.toBeDefined()
60
60
  })
61
+
62
+ it('…which is a DECISION, not an omission', () => {
63
+ // Before `openAccess` existed, "nobody decided" and "we decided it is open"
64
+ // were the same descriptor and a reviewer could not tell them apart. Assert
65
+ // the marker is present, or this suite would keep passing on a query whose
66
+ // guard someone deleted by accident.
67
+ expect(incidentsLive.openAccess).toBeTruthy()
68
+ })
61
69
  })
62
70
 
63
71
  describe('incidents.create — descriptor', () => {
@@ -6,6 +6,18 @@ import { Schema } from 'effect'
6
6
 
7
7
  export const documentAsOf = defineAction({
8
8
  name: 'documents.asOf',
9
+ // Open, with the same load-bearing detail as `documents.history`: the
10
+ // executor passes `ctx.request.subject.tenantId` into `rowAsOf` explicitly,
11
+ // because a version table is not auto-scoped by the `tenant()` mixin. That
12
+ // argument is what keeps a caller-supplied id from reading another tenant's
13
+ // past values; do not drop it.
14
+ //
15
+ // No auth strategy and no rbac ship in this template, so a scope guard would
16
+ // deny every caller rather than the wrong ones.
17
+ openAccess:
18
+ 'returns one past value of a document in the CALLER\'S OWN tenant — `rowAsOf` is passed '
19
+ + '`subject.tenantId` explicitly, so an id from another tenant returns null. Time travel '
20
+ + 'reaches only where the live read already could.',
9
21
  input: Schema.Struct({ id: Schema.String, at: Schema.Number }),
10
22
  output: Schema.NullOr(Schema.Struct({
11
23
  title: Schema.String,
@@ -6,6 +6,19 @@ import { Schema } from 'effect'
6
6
 
7
7
  export const documentHistory = defineAction({
8
8
  name: 'documents.history',
9
+ // Open — and this one needs its bound named, because a version table is a
10
+ // second copy of your data that the `tenant()` mixin does NOT scope for you.
11
+ // The executor passes `ctx.request.subject.tenantId` to `rowHistory`
12
+ // explicitly, so a caller-supplied id from another tenant yields nothing.
13
+ // Read that as the load-bearing line it is: drop the argument and this
14
+ // becomes a cross-tenant read of every past value.
15
+ //
16
+ // No auth strategy and no rbac ship in this template, so a
17
+ // `guards: [{ scope: 'documents:history' }]` would deny every caller.
18
+ openAccess:
19
+ 'lists past versions of a document in the CALLER\'S OWN tenant — `rowHistory` is passed '
20
+ + '`subject.tenantId` explicitly, so an id from another tenant returns nothing (a version '
21
+ + 'table is not auto-scoped by `tenant()`). No auth strategy ships here to guard against.',
9
22
  input: Schema.Struct({ id: Schema.String }),
10
23
  output: Schema.Array(Schema.Struct({
11
24
  version: Schema.Number,
@@ -8,6 +8,15 @@ import { Schema } from 'effect'
8
8
  export const createDocument = defineMutation({
9
9
  name: 'documents.create',
10
10
  target: { table: 'documents', op: 'insert' },
11
+ // Open: writes only the title + content the caller sent, into the request's
12
+ // tenant (`tenant()` auto-stamps `tenantId`), and touches no existing row.
13
+ // No auth strategy and no rbac ship in this template, so every caller is an
14
+ // anonymous Subject holding no scopes and a `guards: [{ scope }]` would deny
15
+ // all of them rather than the wrong ones.
16
+ openAccess:
17
+ 'inserts a document built only from caller-supplied fields into the request\'s tenant; '
18
+ + 'reads nothing and modifies no existing row. No auth strategy ships here, so there is no '
19
+ + 'identity a scope guard could name.',
11
20
  input: Schema.Struct({
12
21
  title: Schema.NonEmptyString,
13
22
  content: Schema.String,