@voltro/cli 0.33.0 → 0.34.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (337) hide show
  1. package/CHANGELOG.md +1801 -0
  2. package/bin/nodeEnvironment.d.mts +30 -0
  3. package/bin/nodeEnvironment.mjs +158 -0
  4. package/bin/voltro.mjs +69 -5
  5. package/dist/addCommand-BNeoeSxe.js +124 -0
  6. package/dist/addCommand-aXSQveak.js +2 -0
  7. package/dist/agentsMd-BTchIZku.js +2 -0
  8. package/dist/agentsMd-mhQMF1bx.js +254 -0
  9. package/dist/apiBuild-BICVGnEC.js +2 -0
  10. package/dist/{apiBuild-h9VHtnlw.js → apiBuild-DZF_f0_I.js} +46 -46
  11. package/dist/appPort-B_HpJ_ck.js +48 -0
  12. package/dist/baselineCommand-C2ClWZN3.js +2 -0
  13. package/dist/baselineCommand-DIttzO8A.js +227 -0
  14. package/dist/bin.js +71 -28
  15. package/dist/build-CE7Ca9gm.js +711 -0
  16. package/dist/cacheCommand-DA4OH9xt.js +42 -0
  17. package/dist/capabilitiesCommand-nq_pz5xd.js +123 -0
  18. package/dist/checkCommand-Dv8r9tGO.js +231 -0
  19. package/dist/checkCommand-YZDmkAGd.js +2 -0
  20. package/dist/{cliArgs-qdZSElM3.js → cliArgs-D4p8n7EE.js} +12 -1
  21. package/dist/cliError-BmdYnghb.js +10 -0
  22. package/dist/cliOutput-D1tSBoRM.js +15 -0
  23. package/dist/{cliRuntime-Oh517vCV.js → cliRuntime-Dh7UDinH.js} +20 -20
  24. package/dist/cloudClient-DWL-Hw_T.js +67 -0
  25. package/dist/cloudCmd-Cvv5HGaZ.js +364 -0
  26. package/dist/clusterCmd-CNXKlcvD.js +54 -0
  27. package/dist/codegen-CYM3Zqrf.js +605 -0
  28. package/dist/codegen-ChBi_hVa.js +2 -0
  29. package/dist/codegenCommand-DC6w2tNZ.js +30 -0
  30. package/dist/codemodRunner-DRRqXR74.js +5243 -0
  31. package/dist/commandRunner-BLAEFLjp.js +47 -0
  32. package/dist/commands-gutsz-Ac.js +808 -0
  33. package/dist/connectionConfig-UFlIEiys.js +66 -0
  34. package/dist/dashboardCommand-3YG8p-UA.js +25 -0
  35. package/dist/dataCommand-qL0r7fPO.js +535 -0
  36. package/dist/dataProfile-dW-PsfLB.js +15 -0
  37. package/dist/{dbCommand-DTLKAfbA.js → dbCommand-B6X0FZbq.js} +551 -401
  38. package/dist/dbCommand-CpdKLeQq.js +2 -0
  39. package/dist/{dev-C_P8FLSx.js → dev-CerMd0mW.js} +3122 -3129
  40. package/dist/dev-CoG-ZPx8.js +3 -0
  41. package/dist/devActivity-Dx_3nnGv.js +100 -0
  42. package/dist/devActivity.js +1 -1
  43. package/dist/dialectDriver-CgXnDfec.js +39 -0
  44. package/dist/discover-C9XKJDco.js +25 -0
  45. package/dist/doctorCommand-BvqGBwNG.js +2 -0
  46. package/dist/{checkCommand-xGhRFFg2.js → doctorCommand-CnDXQxfa.js} +520 -1278
  47. package/dist/dormancyCommand-Dszo57d6.js +69 -0
  48. package/dist/e2eCmd-BRabZww-.js +147 -0
  49. package/dist/embeddingsCommand-C0sKVRo1.js +73 -0
  50. package/dist/envCommand-DPgdV1Bq.js +60 -0
  51. package/dist/evalCommand-6RUfPen4.js +118 -0
  52. package/dist/evolveCommand-DHpkgjgH.js +281 -0
  53. package/dist/fileTaxonomy-CJfgOllU.js +457 -0
  54. package/dist/frameworkTableAssembly-BGHmck-x.js +2 -0
  55. package/dist/{frameworkTableAssembly-BwIrO5nv.js → frameworkTableAssembly-DkkP6BgC.js} +184 -148
  56. package/dist/generateCommand-oibemh97.js +147 -0
  57. package/dist/index.d.ts +45 -0
  58. package/dist/index.js +4 -3
  59. package/dist/infoCommand-BJw9nLUR.js +60 -0
  60. package/dist/{inspect-BUUjt773.js → inspect-CBqFtAKA.js} +82 -40
  61. package/dist/inspect-C_T_WGvl.js +2 -0
  62. package/dist/inspectCmd-Bppy-GGw.js +224 -0
  63. package/dist/inspectFetch-Cm8_wVvp.js +151 -0
  64. package/dist/inspectMetrics-CfdKLh6t.js +72 -0
  65. package/dist/loadEnv-D9nEOClM.js +44 -0
  66. package/dist/logFileSink-C_D2wRN1.js +105 -0
  67. package/dist/logsCmd-CCca3KRZ.js +260 -0
  68. package/dist/manifestBuild-ChsKAhmn.js +2 -0
  69. package/dist/{manifestBuild-BLrVuSlM.js → manifestBuild-sxpwdKY1.js} +1 -1
  70. package/dist/metaCommands-7MJfZ5cf.js +196 -0
  71. package/dist/migrate-CBwOt_iV.js +83 -0
  72. package/dist/mssqlClusterPatch-_4cE_nun.js +44 -0
  73. package/dist/newCommand-COWOJ1_E.js +156 -0
  74. package/dist/nodeEnvironment-cGFAj1J8.js +28 -0
  75. package/dist/packageCommand-Cug_3Ogl.js +271 -0
  76. package/dist/pageConvention-cEiRxdab.js +5 -0
  77. package/dist/privacyCommand-C-Df56U_.js +146 -0
  78. package/dist/projectScaffold-DmzEKHib.js +2 -0
  79. package/dist/projectScaffold-LMMtaavR.js +814 -0
  80. package/dist/renderModeScan-D7J1B7Kw.js +105 -0
  81. package/dist/renderProfile-1OWWAAtx.js +81 -0
  82. package/dist/runtimeRegistry-DMeKfTHP.js +81 -0
  83. package/dist/runtimeTrace-CRxalXTs.js +91 -0
  84. package/dist/scheduleCmd--jksTrf6.js +69 -0
  85. package/dist/scheduleManifestCmd-D2x0CTTY.js +249 -0
  86. package/dist/schemaIr-UJybUUZW.js +103 -0
  87. package/dist/{sdkgen-C81QIkiL.js → sdkgen-CYJscZC7.js} +111 -209
  88. package/dist/seedRunner-TFHHiToI.js +329 -0
  89. package/dist/serveCommand-B_isw7q4.js +1647 -0
  90. package/dist/serveCommand-DOvbgRnQ.js +2 -0
  91. package/dist/serveEntry.js +5 -5
  92. package/dist/serverlessCommand-CfJZy6dS.js +482 -0
  93. package/dist/start-9LiUOfES.js +1087 -0
  94. package/dist/start-B-9Nsp-S.js +3 -0
  95. package/dist/startEntry.js +2 -2
  96. package/dist/staticCommand-Dr2M6tpU.js +304 -0
  97. package/dist/storageCommand-Co6NfLqN.js +42 -0
  98. package/dist/templates-De8IR5-c.js +102 -0
  99. package/dist/test-CI6iDsYc.js +115 -0
  100. package/dist/tracesCmd-CkEZQrtt.js +232 -0
  101. package/dist/tsconfigPaths-BWXBWgcl.js +107 -0
  102. package/dist/tsxLoader-EuXmSJ1K.js +51 -0
  103. package/dist/typecheckCommand-BlsWiCNq.js +61 -0
  104. package/dist/updateCommand-Bkptutss.js +585 -0
  105. package/dist/updateCommand-us1_hdIC.js +2 -0
  106. package/dist/{inspectMetrics-1xzTKAFx.js → webDev-CBYvPqQr.js} +975 -1559
  107. package/dist/webDev-Cg-fFiyd2.js +2 -0
  108. package/dist/webhookDiscovery-CrGAfhIG.js +2 -0
  109. package/dist/webhookDiscovery-D7VaeMlz.js +51 -0
  110. package/dist/webhooksCommand-CID96Rga.js +267 -0
  111. package/dist/workflowsCmd-D1VTmLMY.js +608 -0
  112. package/package.json +179 -17
  113. package/templates/AGENTS.core.md +58 -3
  114. package/templates/AGENTS.md +64 -7
  115. package/templates/agent-docs/_index.md +6 -4
  116. package/templates/agent-docs/_manifest.json +22 -5
  117. package/templates/agent-docs/ai.md +370 -0
  118. package/templates/agent-docs/authentication.md +265 -31
  119. package/templates/agent-docs/caching.md +6 -0
  120. package/templates/agent-docs/cli.md +794 -50
  121. package/templates/agent-docs/data.md +550 -11
  122. package/templates/agent-docs/database/migrations.md +174 -25
  123. package/templates/agent-docs/database/misc.md +156 -40
  124. package/templates/agent-docs/database/querying.md +19 -1
  125. package/templates/agent-docs/database/scaling.md +60 -0
  126. package/templates/agent-docs/database/schema.md +1 -0
  127. package/templates/agent-docs/database/seedsdialects.md +208 -19
  128. package/templates/agent-docs/database/transactions.md +68 -0
  129. package/templates/agent-docs/deployment.md +69 -3
  130. package/templates/agent-docs/introduction.md +87 -16
  131. package/templates/agent-docs/local-first-mobile.md +79 -4
  132. package/templates/agent-docs/multi-tenancy.md +95 -20
  133. package/templates/agent-docs/observability.md +58 -3
  134. package/templates/agent-docs/plugins/ai-flows.md +161 -2
  135. package/templates/agent-docs/plugins/analytics-postgres.md +1 -1
  136. package/templates/agent-docs/plugins/audit.md +37 -1
  137. package/templates/agent-docs/plugins/auth-social.md +143 -0
  138. package/templates/agent-docs/plugins/auth-workos.md +4 -2
  139. package/templates/agent-docs/plugins/auth.md +131 -6
  140. package/templates/agent-docs/plugins/billing.md +132 -15
  141. package/templates/agent-docs/plugins/cdc-out.md +46 -7
  142. package/templates/agent-docs/plugins/clickhouse.md +1 -1
  143. package/templates/agent-docs/plugins/duckdb.md +1 -1
  144. package/templates/agent-docs/plugins/flags.md +132 -0
  145. package/templates/agent-docs/plugins/governance.md +105 -7
  146. package/templates/agent-docs/plugins/multitenancy.md +9 -4
  147. package/templates/agent-docs/plugins/presence.md +13 -2
  148. package/templates/agent-docs/plugins/ratelimit.md +9 -0
  149. package/templates/agent-docs/plugins/search.md +157 -6
  150. package/templates/agent-docs/plugins/sso-saml.md +47 -8
  151. package/templates/agent-docs/plugins/webhooks.md +105 -0
  152. package/templates/agent-docs/plugins.md +150 -18
  153. package/templates/agent-docs/reference.md +60 -3
  154. package/templates/agent-docs/releases.md +1117 -0
  155. package/templates/agent-docs/routing.md +43 -25
  156. package/templates/agent-docs/scheduling.md +10 -0
  157. package/templates/agent-docs/schema-driven-ui.md +92 -12
  158. package/templates/agent-docs/security.md +426 -0
  159. package/templates/agent-docs/templates/apibackends.md +87 -18
  160. package/templates/agent-docs/templates/appshells.md +32 -14
  161. package/templates/agent-docs/templates/overview.md +13 -8
  162. package/templates/agent-docs/testing.md +211 -14
  163. package/templates/agent-docs/whats-new.md +1700 -104
  164. package/templates/agent-docs/workflows.md +130 -14
  165. package/templates/apps/api-ai/actions/summarize.action.ts +11 -0
  166. package/templates/apps/api-ai/package.json +8 -7
  167. package/templates/apps/api-auth/actions/me.action.ts +13 -0
  168. package/templates/apps/api-auth/package.json +9 -8
  169. package/templates/apps/api-backend/mutations/notes.create.mutation.ts +9 -0
  170. package/templates/apps/api-backend/package.json +12 -8
  171. package/templates/apps/api-backend/queries/notes.query.ts +30 -8
  172. package/templates/apps/api-backend-deactivation/actions/users.get.action.ts +14 -0
  173. package/templates/apps/api-backend-deactivation/mutations/users.create.mutation.ts +8 -0
  174. package/templates/apps/api-backend-deactivation/mutations/users.deactivate.mutation.ts +10 -0
  175. package/templates/apps/api-backend-deactivation/package.json +8 -7
  176. package/templates/apps/api-backend-mail/actions/sendWelcome.action.ts +17 -0
  177. package/templates/apps/api-backend-mail/mutations/notes.create.mutation.ts +9 -0
  178. package/templates/apps/api-backend-mail/package.json +9 -8
  179. package/templates/apps/api-backend-mail/queries/notes.query.ts +30 -8
  180. package/templates/apps/api-backend-mariadb/.env.example +14 -0
  181. package/templates/apps/api-backend-mariadb/mutations/notes.create.mutation.ts +9 -0
  182. package/templates/apps/api-backend-mariadb/package.json +10 -9
  183. package/templates/apps/api-backend-mariadb/queries/notes.query.ts +30 -8
  184. package/templates/apps/api-backend-sqlite/.env.example +14 -0
  185. package/templates/apps/api-backend-sqlite/mutations/notes.create.mutation.ts +9 -0
  186. package/templates/apps/api-backend-sqlite/package.json +9 -8
  187. package/templates/apps/api-backend-sqlite/queries/notes.query.ts +30 -8
  188. package/templates/apps/api-backend-storage/actions/uploadAvatar.action.ts +13 -0
  189. package/templates/apps/api-backend-storage/actions/uploadDocument.action.ts +12 -0
  190. package/templates/apps/api-backend-storage/mutations/notes.create.mutation.ts +9 -0
  191. package/templates/apps/api-backend-storage/package.json +9 -8
  192. package/templates/apps/api-backend-storage/queries/notes.query.ts +30 -8
  193. package/templates/apps/api-cms/actions/content.get.action.ts +7 -0
  194. package/templates/apps/api-cms/actions/content.types.action.ts +6 -0
  195. package/templates/apps/api-cms/actions/me.action.ts +13 -0
  196. package/templates/apps/api-cms/app.config.ts +19 -0
  197. package/templates/apps/api-cms/authz.ts +63 -0
  198. package/templates/apps/api-cms/mutations/content.publish.mutation.ts +10 -0
  199. package/templates/apps/api-cms/mutations/content.saveDraft.mutation.ts +10 -0
  200. package/templates/apps/api-cms/mutations/content.unpublish.mutation.ts +5 -0
  201. package/templates/apps/api-cms/package.json +11 -10
  202. package/templates/apps/api-cms/queries/content.list.query.ts +23 -7
  203. package/templates/apps/api-cms/tests/accessDecisions.test.ts +121 -0
  204. package/templates/apps/api-cms/tests/content.descriptors.test.ts +8 -4
  205. package/templates/apps/api-collab/mutations/documents.create.mutation.ts +8 -0
  206. package/templates/apps/api-collab/mutations/documents.setBody.mutation.ts +13 -0
  207. package/templates/apps/api-collab/package.json +9 -8
  208. package/templates/apps/api-collab/queries/documents.query.ts +19 -7
  209. package/templates/apps/api-data-advanced/package.json +9 -8
  210. package/templates/apps/api-data-advanced/queries/authors.withBooks.query.ts +12 -0
  211. package/templates/apps/api-data-advanced/queries/books.search.query.ts +9 -0
  212. package/templates/apps/api-durable/mutations/orders.approve.mutation.ts +17 -0
  213. package/templates/apps/api-durable/mutations/orders.place.mutation.ts +9 -0
  214. package/templates/apps/api-durable/package.json +9 -8
  215. package/templates/apps/api-feature-flags/actions/notes.export.action.ts +16 -0
  216. package/templates/apps/api-feature-flags/mutations/notes.create.mutation.ts +12 -0
  217. package/templates/apps/api-feature-flags/package.json +10 -9
  218. package/templates/apps/api-governance/README.md +30 -8
  219. package/templates/apps/api-governance/actions/profiles.get.action.server.ts +17 -1
  220. package/templates/apps/api-governance/actions/profiles.get.action.ts +29 -4
  221. package/templates/apps/api-governance/database/schema.ts +16 -4
  222. package/templates/apps/api-governance/mutations/profiles.create.mutation.ts +12 -0
  223. package/templates/apps/api-governance/package.json +9 -8
  224. package/templates/apps/api-kv/actions/sync.pull.action.ts +15 -0
  225. package/templates/apps/api-kv/actions/sync.reset.action.ts +13 -0
  226. package/templates/apps/api-kv/actions/sync.status.action.ts +7 -0
  227. package/templates/apps/api-kv/package.json +9 -8
  228. package/templates/apps/api-kv/queries/events.list.query.ts +19 -8
  229. package/templates/apps/api-moderation/mutations/comments.create.mutation.ts +11 -0
  230. package/templates/apps/api-moderation/mutations/posts.create.mutation.ts +13 -0
  231. package/templates/apps/api-moderation/package.json +9 -8
  232. package/templates/apps/api-observability/mutations/notes.create.mutation.ts +8 -0
  233. package/templates/apps/api-observability/package.json +9 -8
  234. package/templates/apps/api-observability/queries/notes.list.query.ts +13 -0
  235. package/templates/apps/api-ratelimit/mutations/notes.create.mutation.ts +14 -0
  236. package/templates/apps/api-ratelimit/package.json +9 -8
  237. package/templates/apps/api-rbac/package.json +9 -8
  238. package/templates/apps/api-rest/package.json +8 -7
  239. package/templates/apps/api-saas/mutations/projects.create.mutation.ts +13 -0
  240. package/templates/apps/api-saas/package.json +12 -11
  241. package/templates/apps/api-saas/queries/projects.list.query.ts +11 -0
  242. package/templates/apps/api-saas-starter/actions/me.action.ts +13 -0
  243. package/templates/apps/api-saas-starter/app.config.ts +19 -0
  244. package/templates/apps/api-saas-starter/authz.ts +75 -0
  245. package/templates/apps/api-saas-starter/mutations/invites.create.mutation.ts +10 -0
  246. package/templates/apps/api-saas-starter/mutations/projects.create.mutation.ts +9 -0
  247. package/templates/apps/api-saas-starter/package.json +15 -11
  248. package/templates/apps/api-saas-starter/queries/invites.list.query.ts +19 -8
  249. package/templates/apps/api-saas-starter/queries/projects.list.query.ts +18 -7
  250. package/templates/apps/api-saas-starter/tests/accessDecisions.test.ts +135 -0
  251. package/templates/apps/api-search/mutations/articles.create.mutation.ts +14 -0
  252. package/templates/apps/api-search/package.json +9 -8
  253. package/templates/apps/api-search/queries/articles.list.query.ts +21 -8
  254. package/templates/apps/api-status/README.md +10 -3
  255. package/templates/apps/api-status/app.config.ts +8 -3
  256. package/templates/apps/api-status/authz.ts +5 -3
  257. package/templates/apps/api-status/package.json +9 -8
  258. package/templates/apps/api-status/queries/components.list.query.ts +14 -6
  259. package/templates/apps/api-status/queries/incidents.live.query.ts +23 -10
  260. package/templates/apps/api-status/queries/updates.list.query.ts +16 -9
  261. package/templates/apps/api-status/tests/status.test.ts +9 -1
  262. package/templates/apps/api-versioning/actions/documents.asOf.action.ts +12 -0
  263. package/templates/apps/api-versioning/actions/documents.history.action.ts +13 -0
  264. package/templates/apps/api-versioning/mutations/documents.create.mutation.ts +9 -0
  265. package/templates/apps/api-versioning/mutations/documents.update.mutation.ts +12 -0
  266. package/templates/apps/api-versioning/package.json +9 -8
  267. package/templates/apps/api-webhooks/mutations/orders.fulfill.mutation.ts +16 -0
  268. package/templates/apps/api-webhooks/package.json +10 -9
  269. package/templates/apps/api-webhooks/queries/orders.list.query.ts +10 -0
  270. package/templates/apps/changelog/package.json +8 -6
  271. package/templates/apps/edge-functions/package.json +2 -2
  272. package/templates/apps/frontend-admin/package.json +10 -8
  273. package/templates/apps/frontend-admin/src/lib/admin.ts +20 -9
  274. package/templates/apps/frontend-admin/src/locales/de.ts +11 -1
  275. package/templates/apps/frontend-admin/src/locales/en.ts +13 -1
  276. package/templates/apps/frontend-admin/src/pages/admin/[entity]/page.tsx +65 -22
  277. package/templates/apps/frontend-admin/src/pages/admin/entity.test.tsx +130 -23
  278. package/templates/apps/frontend-admin/src/pages/admin/layout.tsx +3 -2
  279. package/templates/apps/frontend-admin/src/pages/admin/page.test.tsx +19 -2
  280. package/templates/apps/frontend-admin/src/pages/admin/page.tsx +9 -4
  281. package/templates/apps/frontend-app/app.config.ts +4 -3
  282. package/templates/apps/frontend-app/package.json +11 -8
  283. package/templates/apps/frontend-app/src/lib/api.ts +25 -0
  284. package/templates/apps/frontend-app/src/pages/page.test.tsx +130 -82
  285. package/templates/apps/frontend-app/src/pages/page.tsx +14 -18
  286. package/templates/apps/frontend-auth/package.json +10 -8
  287. package/templates/apps/frontend-blank/package.json +9 -7
  288. package/templates/apps/frontend-cms/package.json +11 -9
  289. package/templates/apps/frontend-collab/package.json +12 -9
  290. package/templates/apps/frontend-collab/src/pages/page.test.tsx +122 -78
  291. package/templates/apps/frontend-contact/package.json +9 -7
  292. package/templates/apps/frontend-dashboard/package.json +9 -7
  293. package/templates/apps/frontend-docs/package.json +9 -7
  294. package/templates/apps/frontend-i18n/package.json +8 -6
  295. package/templates/apps/frontend-landing/package.json +9 -7
  296. package/templates/apps/frontend-portal/package.json +10 -8
  297. package/templates/apps/frontend-saas/app.config.ts +10 -6
  298. package/templates/apps/frontend-saas/package.json +10 -8
  299. package/templates/apps/frontend-saas/src/lib/api.ts +27 -32
  300. package/templates/apps/frontend-saas/src/pages/dashboard/billing/page.tsx +7 -8
  301. package/templates/apps/frontend-saas/src/pages/dashboard/page.test.tsx +27 -3
  302. package/templates/apps/frontend-saas/src/pages/dashboard/page.tsx +4 -4
  303. package/templates/apps/frontend-saas/src/pages/dashboard/team/page.tsx +3 -4
  304. package/templates/apps/frontend-spa/package.json +9 -7
  305. package/templates/apps/frontend-ssr/package.json +9 -7
  306. package/templates/apps/frontend-ssr-api/package.json +10 -8
  307. package/templates/apps/frontend-static-blog/package.json +8 -6
  308. package/templates/apps/frontend-status/package.json +10 -8
  309. package/templates/apps/mobile-app/README.md +1 -0
  310. package/templates/apps/mobile-app/package.json +4 -2
  311. package/templates/apps/mobile-app/src/app/index.tsx +22 -12
  312. package/templates/apps/mobile-app/src/app/orders/[id].tsx +1 -1
  313. package/templates/apps/mobile-app/src/lib/api.ts +34 -0
  314. package/templates/apps/mobile-app/voltro.mobile.ts +4 -2
  315. package/templates/baselines/bare/.env.example +14 -0
  316. package/templates/baselines/bare/baseline.json +4 -4
  317. package/templates/baselines/compose/.env.example +14 -0
  318. package/templates/baselines/compose/README.md +1 -1
  319. package/templates/baselines/compose/baseline.json +5 -5
  320. package/templates/baselines/compose-mariadb/.env.example +14 -0
  321. package/templates/baselines/compose-mariadb/README.md +1 -1
  322. package/templates/baselines/compose-mariadb/baseline.json +5 -5
  323. package/templates/baselines/helm/.env.example +14 -0
  324. package/templates/baselines/helm/baseline.json +4 -4
  325. package/dist/apiBuild-C-x9YacA.js +0 -2
  326. package/dist/checkCommand-DRovTKza.js +0 -2
  327. package/dist/commands-CJfepbm4.js +0 -11541
  328. package/dist/dbCommand-b1gum4td.js +0 -2
  329. package/dist/dev-iiMtlkfs.js +0 -3
  330. package/dist/devActivity-BhIu6ncs.js +0 -159
  331. package/dist/frameworkTableAssembly-D-EebUQX.js +0 -2
  332. package/dist/inspect-mmBuRXmy.js +0 -2
  333. package/dist/manifestBuild-Dj8Jjoto.js +0 -2
  334. package/dist/seedRunner-Bqxgp7HZ.js +0 -230
  335. package/dist/serveCommand-DdaM4Hup.js +0 -1608
  336. package/dist/start-C0koT0UO.js +0 -1084
  337. /package/templates/apps/api-ai/actions/{summarize.action.server.tsx → summarize.action.server.ts} +0 -0
@@ -8,6 +8,18 @@ import { Schema } from 'effect'
8
8
  export const updateDocument = defineMutation({
9
9
  name: 'documents.update',
10
10
  target: { table: 'documents', op: 'update' },
11
+ // Open: `tenant()` scopes the update, so a caller-supplied `id` from another
12
+ // tenant matches no row. Nothing is destroyed either — the versioning plugin
13
+ // snapshots the previous value, so every edit is recoverable through
14
+ // `documents.history` / `documents.asOf`.
15
+ //
16
+ // History is an audit trail, not an access control: it tells you WHAT
17
+ // changed, never whether the change was allowed. No auth strategy or rbac
18
+ // ships here, so a `guards: [{ scope }]` would deny every caller.
19
+ openAccess:
20
+ 'edits a document of the request\'s tenant (an id outside it matches no row); the previous '
21
+ + 'value is snapshotted, so nothing is destroyed. Version history records changes — it '
22
+ + 'does not authorize them.',
11
23
  input: Schema.Struct({
12
24
  id: Schema.String,
13
25
  content: Schema.String,
@@ -13,18 +13,19 @@
13
13
  "dependencies": {
14
14
  "@effect/platform": "^0.97.0",
15
15
  "@effect/rpc": "^0.76.0",
16
- "@voltro/cli": "0.33.0",
17
- "@voltro/database": "0.33.0",
18
- "@voltro/env": "0.33.0",
19
- "@voltro/plugin-multitenancy": "0.33.0",
20
- "@voltro/plugin-versioning": "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-versioning": "0.34.0",
21
+ "@voltro/protocol": "0.34.0",
22
+ "@voltro/runtime": "0.34.0",
23
23
  "effect": "^3.22.0"
24
24
  },
25
25
  "devDependencies": {
26
- "@voltro/testing": "0.33.0",
26
+ "@voltro/testing": "0.34.0",
27
27
  "typescript": "^6.0.3",
28
+ "@vitest/coverage-v8": "^4.1.10",
28
29
  "vitest": "^4.1.10"
29
30
  }
30
31
  }
@@ -4,6 +4,22 @@ import { Schema } from 'effect'
4
4
  export const fulfillOrder = defineMutation({
5
5
  name: 'orders.fulfill',
6
6
  target: { table: 'orders', op: 'insert' },
7
+ // Open — and unlike a plain insert, this one FANS OUT: on commit it emits
8
+ // `order.completed` to every subscribed target, so an unguarded call makes
9
+ // this server POST to third parties. Two things bound that today and both are
10
+ // checkable: the emit is post-commit (a rollback ships nothing), and the
11
+ // target list is empty until someone calls `ctx.webhooks.subscribe`, which is
12
+ // not reachable from this procedure.
13
+ //
14
+ // No auth strategy and no rbac ship in this template, so a
15
+ // `guards: [{ scope: 'orders:write' }]` would deny every caller instead of
16
+ // the wrong ones. Add an identity, then that guard — before real subscribers
17
+ // exist. (The INCOMING `orders` webhook is a different door with its own
18
+ // control: an HMAC signature over `<unix>.<rawBody>`, not this decision.)
19
+ openAccess:
20
+ 'inserts an order from caller-supplied fields into the request\'s tenant, then emits '
21
+ + '`order.completed` post-commit. The emit reaches only targets someone registered via '
22
+ + '`ctx.webhooks.subscribe` — none by default — so guard this before subscribers exist.',
7
23
  input: Schema.Struct({ sku: Schema.NonEmptyString, totalCents: Schema.Number }),
8
24
  output: Schema.Struct({
9
25
  id: Schema.String,
@@ -13,19 +13,20 @@
13
13
  "dependencies": {
14
14
  "@effect/platform": "^0.97.0",
15
15
  "@effect/rpc": "^0.76.0",
16
- "@voltro/cli": "0.33.0",
17
- "@voltro/database": "0.33.0",
18
- "@voltro/env": "0.33.0",
19
- "@voltro/plugin-multitenancy": "0.33.0",
20
- "@voltro/plugin-webhooks": "0.33.0",
21
- "@voltro/protocol": "0.33.0",
22
- "@voltro/runtime": "0.33.0",
23
- "@voltro/sql-postgres": "0.33.0",
16
+ "@voltro/cli": "0.34.0",
17
+ "@voltro/database": "0.34.0",
18
+ "@voltro/env": "0.34.0",
19
+ "@voltro/plugin-multitenancy": "0.34.0",
20
+ "@voltro/plugin-webhooks": "0.34.0",
21
+ "@voltro/protocol": "0.34.0",
22
+ "@voltro/runtime": "0.34.0",
23
+ "@voltro/sql-postgres": "0.34.0",
24
24
  "effect": "^3.22.0"
25
25
  },
26
26
  "devDependencies": {
27
- "@voltro/testing": "0.33.0",
27
+ "@voltro/testing": "0.34.0",
28
28
  "typescript": "^6.0.3",
29
+ "@vitest/coverage-v8": "^4.1.10",
29
30
  "vitest": "^4.1.10"
30
31
  }
31
32
  }
@@ -4,6 +4,16 @@ import { Schema } from 'effect'
4
4
  export const listOrders = defineQuery({
5
5
  name: 'orders.list',
6
6
  source: 'orders',
7
+ // Open: `tenant()` confines every delivery to the tenantId on the request, so
8
+ // one tenant's orders never reach another's subscription, and the rows carry
9
+ // only sku / amount / status — no customer identity. With no auth strategy
10
+ // configured that tenantId is the caller's own `x-tenant` header, and a
11
+ // `guards: [{ scope }]` would be unsatisfiable: an anonymous Subject holds no
12
+ // scopes, so the guard would deny 100% of traffic.
13
+ openAccess:
14
+ 'lists the request tenant\'s own orders (sku, amount, status — no customer identity); '
15
+ + '`tenant()` scopes every delivery. No auth strategy ships here, so the tenant comes from '
16
+ + 'the caller\'s `x-tenant` header — add a strategy, then a `guards:`.',
7
17
  input: Schema.Struct({}),
8
18
  output: Schema.Array(Schema.Struct({
9
19
  id: Schema.String,
@@ -12,12 +12,12 @@
12
12
  "test": "voltro test"
13
13
  },
14
14
  "dependencies": {
15
- "@voltro/changelog": "0.33.0",
16
- "@voltro/cli": "0.33.0",
17
- "@voltro/client": "0.33.0",
18
- "@voltro/env": "0.33.0",
19
- "@voltro/i18n": "0.33.0",
20
- "@voltro/web": "0.33.0",
15
+ "@voltro/changelog": "0.34.0",
16
+ "@voltro/cli": "0.34.0",
17
+ "@voltro/client": "0.34.0",
18
+ "@voltro/env": "0.34.0",
19
+ "@voltro/i18n": "0.34.0",
20
+ "@voltro/web": "0.34.0",
21
21
  "react": "^19.0.0",
22
22
  "react-dom": "^19.0.0"
23
23
  },
@@ -25,7 +25,9 @@
25
25
  "@types/react": "^19.0.0",
26
26
  "@types/react-dom": "^19.0.0",
27
27
  "tsx": "^4.22.4",
28
+ "jsdom": "^30.0.0",
28
29
  "typescript": "^6.0.3",
30
+ "@vitest/coverage-v8": "^4.1.10",
29
31
  "vitest": "^4.1.10"
30
32
  }
31
33
  }
@@ -12,8 +12,8 @@
12
12
  },
13
13
  "dependencies": {
14
14
  "@effect/platform": "^0.97.0",
15
- "@voltro/cli": "0.33.0",
16
- "@voltro/serverless": "0.33.0",
15
+ "@voltro/cli": "0.34.0",
16
+ "@voltro/serverless": "0.34.0",
17
17
  "effect": "^3.22.0"
18
18
  },
19
19
  "devDependencies": {
@@ -12,21 +12,23 @@
12
12
  "test": "voltro test"
13
13
  },
14
14
  "dependencies": {
15
- "@{{projectName}}/api": "0.33.0",
16
- "@voltro/cli": "0.33.0",
17
- "@voltro/client": "0.33.0",
18
- "@voltro/env": "0.33.0",
19
- "@voltro/i18n": "0.33.0",
20
- "@voltro/ui-shadcn": "0.33.0",
21
- "@voltro/web": "0.33.0",
15
+ "@{{projectName}}/api": "0.34.0",
16
+ "@voltro/cli": "0.34.0",
17
+ "@voltro/client": "0.34.0",
18
+ "@voltro/env": "0.34.0",
19
+ "@voltro/i18n": "0.34.0",
20
+ "@voltro/ui-shadcn": "0.34.0",
21
+ "@voltro/web": "0.34.0",
22
22
  "react": "^19.0.0",
23
23
  "react-dom": "^19.0.0"
24
24
  },
25
25
  "devDependencies": {
26
- "@voltro/devtools": "0.33.0",
26
+ "@voltro/devtools": "0.34.0",
27
27
  "@types/react": "^19.0.0",
28
28
  "@types/react-dom": "^19.0.0",
29
+ "jsdom": "^30.0.0",
29
30
  "typescript": "^6.0.3",
31
+ "@vitest/coverage-v8": "^4.1.10",
30
32
  "vitest": "^4.1.10"
31
33
  }
32
34
  }
@@ -1,16 +1,27 @@
1
- // The scopes the admin gates its write actions on (via useCan). The admin
2
- // derives a CONVENTIONAL scope per entity action — `<table>:create`,
3
- // `<table>:write`, `<table>:delete` (see deriveEntityAdmins) — and hides any
4
- // action the current subject's scopes don't satisfy.
1
+ // The scopes the admin judges its write affordances against.
5
2
  //
6
- // DEMO: we feed `admin:full` (the blanket-bypass scope) so the demo shows every
7
- // action. In a real app, REPLACE this with the current subject's scopes from
8
- // your session e.g. a reactive query:
3
+ // READ THIS BEFORE SWAPPING IT OUT — the shape changed, and the old shape had a
4
+ // trap. The admin no longer invents a scope per entity (`<table>:create` and
5
+ // friends). It reads each procedure's OWN declared access off the capability
6
+ // map (`spec.create.guards` — the descriptor's `guards:` / `openAccess:`) and
7
+ // decides with `useAccessDecision`. So these scopes are matched against what
8
+ // your api really declares; a name that appears in neither place gates nothing.
9
+ //
10
+ // That is the whole fix. A UI gating on an invented convention hides every write
11
+ // from every caller of every app that named its scopes differently — a total
12
+ // outage of the affordance that reads like a working permission check, and that
13
+ // a demo seeding `admin:full` can never surface.
14
+ //
15
+ // DEMO: we feed `admin:full` (the blanket-bypass scope) so the scaffold shows
16
+ // every action on first boot. In a real app, REPLACE this with the current
17
+ // subject's scopes from your session — e.g. a reactive query:
9
18
  //
10
19
  // const { data } = useSubscription<{ scopes: string[] }>('app', 'auth.session')
11
20
  // <PermissionProvider scopes={data?.scopes ?? []}>…</PermissionProvider>
12
21
  //
13
- // useCan reads these reactively, so the admin's affordances update the instant
22
+ // useAccessDecision reads these reactively, so affordances update the instant
14
23
  // the subject's roles change. (This is UX gating, not enforcement — the api's
15
- // own permission() guards remain the real authorization boundary.)
24
+ // own guards remain the real authorization boundary. A control shown while the
25
+ // decision is `unknown` is deliberate: a per-row guard is the server's to
26
+ // answer, and it answers with a typed ScopeError.)
16
27
  export const DEMO_SCOPES: ReadonlyArray<string> = ['admin:full']
@@ -18,7 +18,7 @@ export default defineLocale<typeof en>()({
18
18
  'lang.label': 'Sprache',
19
19
 
20
20
  'landing.heading': 'Das Backoffice, für dich erledigt.',
21
- 'landing.body': 'Dieser Admin liest zur Laufzeit die <strong>Capability-Map</strong> deiner API und rendert eine live, reaktive <code>DataTable</code> + <code>AutoForm</code> für jede Entität, die sie freigibt — rechtegeprüft mit <code>useCan</code>, mit einer Herkunfts-Lade pro Zeile und universellem Undo/Redo. Richte ihn auf eine beliebige Voltro-API und er passt sich an; der Seiten-Code unter <code>src/pages/admin/</code> gehört dir zum Bearbeiten.',
21
+ 'landing.body': 'Dieser Admin liest zur Laufzeit die <strong>Capability-Map</strong> deiner API und rendert eine live, reaktive <code>DataTable</code> + <code>AutoForm</code> für jede Entität, die sie freigibt — gegated auf den Zugriff, den jede Prozedur selbst deklariert, mit einer Herkunfts-Lade pro Zeile und universellem Undo/Redo. Richte ihn auf eine beliebige Voltro-API und er passt sich an; der Seiten-Code unter <code>src/pages/admin/</code> gehört dir zum Bearbeiten.',
22
22
  'landing.cta': 'Beim Admin anmelden →',
23
23
 
24
24
  'login.heading': 'Anmelden',
@@ -38,6 +38,7 @@ export default defineLocale<typeof en>()({
38
38
  'overview.reactiveSuffix': ' · reaktiv',
39
39
  'overview.readonly': 'schreibgeschützt',
40
40
  'overview.empty': 'Diese API stellt keine Benutzer-Entitäten bereit.',
41
+ 'overview.serverOnly': '{count, plural, one {# reine Server-Spalte, ausgeblendet} other {# reine Server-Spalten, ausgeblendet}}',
41
42
 
42
43
  'entity.create': 'Erstellen',
43
44
  'entity.addLabel': '{table} hinzufügen',
@@ -48,6 +49,15 @@ export default defineLocale<typeof en>()({
48
49
  'entity.delete': 'Löschen',
49
50
  'entity.unknown': 'Keine Entität <code>{entity}</code> in der Capability-Map dieser API.',
50
51
 
52
+ 'entity.requires': 'erfordert {scopes}',
53
+ 'entity.openAccess': 'offener Zugriff — {reason}',
54
+ 'entity.accessUndeclared': 'keine Zugriffsentscheidung deklariert',
55
+ 'entity.accessUnknown': 'Diese API prüft die Berechtigung pro Zeile, das kann nur sie beantworten. Das Bedienelement wird angezeigt; die API entscheidet beim Absenden.',
56
+
57
+ 'entity.serverOnly': '{count, plural, one {# reine Server-Spalte verlässt} other {# reine Server-Spalten verlassen}} die API nie und {count, plural, one {kann} other {können}} nicht übermittelt werden: {names}',
58
+ 'entity.sensitive': 'Personenbezogene Daten — in Exporten maskiert: {names}',
59
+ 'entity.notEditable': 'Kein einzelner Primärschlüssel, daher lassen sich Zeilen nicht zum Bearbeiten oder Löschen adressieren. Nur Liste.',
60
+
51
61
  'prov.title': 'Herkunft · <code>{table}</code> #{id}',
52
62
  'prov.close': 'schließen',
53
63
  'prov.loading': 'lädt…',
@@ -24,7 +24,7 @@ export default defineCatalog({
24
24
 
25
25
  // ---- Public landing ((marketing)/index.tsx) ----
26
26
  'landing.heading': 'The back-office, done for you.',
27
- 'landing.body': 'This admin reads your api’s <strong>capability map</strong> at runtime and renders a live, reactive <code>DataTable</code> + <code>AutoForm</code> for every entity it exposes — permission-gated with <code>useCan</code>, with a per-row provenance drawer and universal undo/redo. Point it at any Voltro api and it adapts; the page code under <code>src/pages/admin/</code> is yours to edit.',
27
+ 'landing.body': 'This admin reads your api’s <strong>capability map</strong> at runtime and renders a live, reactive <code>DataTable</code> + <code>AutoForm</code> for every entity it exposes — gated on the access each procedure itself declares, with a per-row provenance drawer and universal undo/redo. Point it at any Voltro api and it adapts; the page code under <code>src/pages/admin/</code> is yours to edit.',
28
28
  'landing.cta': 'Sign in to the admin →',
29
29
 
30
30
  // ---- Public sign-in ((marketing)/login.tsx) ----
@@ -47,6 +47,7 @@ export default defineCatalog({
47
47
  'overview.reactiveSuffix': ' · reactive',
48
48
  'overview.readonly': 'read-only',
49
49
  'overview.empty': 'No user entities exposed by this api.',
50
+ 'overview.serverOnly': '{count, plural, one {# server-only column, hidden} other {# server-only columns, hidden}}',
50
51
 
51
52
  // ---- Per-entity CRUD (admin/[entity].tsx) ----
52
53
  'entity.create': 'Create',
@@ -58,6 +59,17 @@ export default defineCatalog({
58
59
  'entity.delete': 'Delete',
59
60
  'entity.unknown': 'No entity <code>{entity}</code> in this api’s capability map.',
60
61
 
62
+ // ---- Declared access, shown rather than guessed (admin/[entity].tsx) ----
63
+ 'entity.requires': 'requires {scopes}',
64
+ 'entity.openAccess': 'open access — {reason}',
65
+ 'entity.accessUndeclared': 'no access decision declared',
66
+ 'entity.accessUnknown': 'This api checks permission per row, so only it can answer. The control is shown; the api decides when you submit.',
67
+
68
+ // ---- Exposure axes, straight from the schema (admin/[entity].tsx) ----
69
+ 'entity.serverOnly': '{count, plural, one {# server-only column} other {# server-only columns}} never leave the api and cannot be submitted: {names}',
70
+ 'entity.sensitive': 'Personal data — masked in exports: {names}',
71
+ 'entity.notEditable': 'No single primary key, so rows can’t be addressed for edit or delete. List only.',
72
+
61
73
  // ---- Provenance drawer (admin/[entity].tsx) ----
62
74
  'prov.title': 'Provenance · <code>{table}</code> #{id}',
63
75
  'prov.close': 'close',
@@ -1,22 +1,36 @@
1
1
  // Per-entity CRUD — URL `/admin/<entity>`. Everything here is DISCOVERED:
2
2
  // useCapabilityManifest → deriveEntityAdmins gives this entity's list query +
3
- // create/update/delete mutation tags + the columns. We bind the shipped
4
- // schema-driven components to those tags:
5
- // - <DataTable api="app" query={listTag}/> — live, reactive rows
6
- // - <AutoForm api="app" mutation={createTag}/> — fields from the input schema
7
- // Writes are gated by useCan on the conventional `<table>:create|delete` scopes;
8
- // each row has a provenance ("why is this here?") drawer (08) + delete; the
9
- // layout's undo bar reverts the last change (00/09). Edit this file freely — it's
10
- // your template code, not a sealed runtime feature.
3
+ // create/update/delete actions + the columns. We bind the shipped schema-driven
4
+ // components to those tags:
5
+ // - <DataTable api="app" query={spec.list.tag}/> — live, reactive rows
6
+ // - <AutoForm api="app" mutation={spec.create.tag}/> — fields from the input schema
7
+ //
8
+ // WRITES ARE GATED ON THE ACCESS THE API DECLARES, not on a scope string this
9
+ // page invented. `spec.create.guards` is the procedure's own `guards:` /
10
+ // `openAccess:`, straight off the capability map, so this gate and the server's
11
+ // check read the same data and cannot drift.
12
+ //
13
+ // The decision is THREE-valued, and `unknown` is SHOWN, not hidden: a guard with
14
+ // a `resource` extractor is answered per ROW by the server, and a browser
15
+ // holding only global scopes cannot pre-compute it. Hiding those would empty the
16
+ // admin for exactly the multi-tenant apps whose subjects carry no global scopes.
17
+ // The server stays the authorization boundary and answers a typed ScopeError.
18
+ //
19
+ // Each row has a provenance ("why is this here?") drawer (08) + delete; the
20
+ // layout's undo bar reverts the last change (00/09). Edit this file freely —
21
+ // it's your template code, not a sealed runtime feature.
11
22
  import { useState, type ReactNode } from 'react'
12
23
  import type { PageMeta } from '@voltro/web'
13
24
  import { useParams, AutoForm, DataTable } from '@voltro/web'
14
25
  import {
15
26
  useCapabilityManifest,
16
27
  deriveEntityAdmins,
17
- useCan,
28
+ useAccessDecision,
29
+ requiredScopes,
30
+ openAccessReason,
18
31
  useMutation,
19
32
  useProvenance,
33
+ type EntityAction,
20
34
  type EntityAdminSpec,
21
35
  } from '@voltro/client'
22
36
  import { T, useTFn } from '@voltro/i18n'
@@ -55,23 +69,36 @@ const ProvenanceDrawer = ({ table, id, onClose }: { readonly table: string; read
55
69
  )
56
70
  }
57
71
 
72
+ // What the api requires for this action, in words — so an operator who hits a
73
+ // ScopeError can read the scope they are missing instead of guessing at it.
74
+ const AccessNote = ({ action }: { readonly action: EntityAction }): ReactNode => {
75
+ const reason = openAccessReason(action.guards)
76
+ const scopes = requiredScopes(action.guards)
77
+ if (reason !== undefined) return <span className="meta"><T id="entity.openAccess" values={{ reason }} /></span>
78
+ if (scopes.length > 0) return <span className="meta"><T id="entity.requires" values={{ scopes: scopes.join(', ') }} /></span>
79
+ return <span className="meta"><T id="entity.accessUndeclared" /></span>
80
+ }
81
+
58
82
  // The bound CRUD surface for one entity. A child component so its hooks
59
- // (useCan/useMutation) get the real spec, never undefined.
83
+ // (useAccessDecision/useMutation) get the real spec, never undefined.
60
84
  const EntityAdmin = ({ spec }: { readonly spec: EntityAdminSpec }): ReactNode => {
61
85
  const [sel, setSel] = useState<string | null>(null)
62
86
  const t = useTFn()
63
- const canCreate = useCan(spec.createScope)
64
- const canDelete = useCan(spec.deleteScope)
65
- // Always-called; the tag is only used when deleteTag exists (gated below).
66
- const del = useMutation<{ id: string }>('app', spec.deleteTag ?? '__voltro.noop')
87
+ const create = useAccessDecision(spec.create.guards)
88
+ const remove = useAccessDecision(spec.delete.guards)
89
+ // Always-called; the tag is only used when spec.delete.tag exists (gated below).
90
+ const del = useMutation<Record<string, string>>('app', spec.delete.tag ?? '__voltro.noop')
91
+ // The column the api keys rows on — NOT a hard-coded `id`. A table keyed on
92
+ // anything else would otherwise get an empty id sent on every delete.
93
+ const pk = spec.pkColumn ?? 'id'
67
94
 
68
95
  const rowActions = (row: Row): ReactNode => {
69
- const id = String(row['id'] ?? '')
96
+ const id = String(row[pk] ?? '')
70
97
  return (
71
98
  <span className="row-actions">
72
99
  <button type="button" className="btn ghost" onClick={() => setSel(id)}><T id="entity.why" /></button>
73
- {spec.deleteTag && canDelete && id ? (
74
- <button type="button" className="btn danger" onClick={() => void del.mutate({ id })}><T id="entity.delete" /></button>
100
+ {spec.delete.tag && spec.editable && remove !== 'denied' && id ? (
101
+ <button type="button" className="btn danger" onClick={() => void del.mutate({ [pk]: id })}><T id="entity.delete" /></button>
75
102
  ) : null}
76
103
  </span>
77
104
  )
@@ -81,17 +108,33 @@ const EntityAdmin = ({ spec }: { readonly spec: EntityAdminSpec }): ReactNode =>
81
108
  <section>
82
109
  <h1>{spec.table}</h1>
83
110
 
84
- {spec.createTag && canCreate ? (
111
+ {/* `.serverOnly()` columns are absent from `spec.columns` by construction
112
+ they never cross the wire, and the runtime REFUSES a mutation input that
113
+ sets one. Saying so beats a silently shorter column list. */}
114
+ {spec.serverOnlyColumns.length > 0 ? (
115
+ <p className="meta">
116
+ <T id="entity.serverOnly" values={{ count: spec.serverOnlyColumns.length, names: spec.serverOnlyColumns.join(', ') }} />
117
+ </p>
118
+ ) : null}
119
+ {spec.sensitiveColumns.length > 0 ? (
120
+ <p className="meta">
121
+ <T id="entity.sensitive" values={{ names: spec.sensitiveColumns.join(', ') }} />
122
+ </p>
123
+ ) : null}
124
+ {!spec.editable ? <p className="meta"><T id="entity.notEditable" /></p> : null}
125
+
126
+ {spec.create.tag && create !== 'denied' ? (
85
127
  <div className="section">
86
- <h2><T id="entity.create" /></h2>
87
- <AutoForm api="app" mutation={spec.createTag} submitLabel={t('entity.addLabel', { table: spec.table })} />
128
+ <h2><T id="entity.create" /> <AccessNote action={spec.create} /></h2>
129
+ {create === 'unknown' ? <p className="muted"><T id="entity.accessUnknown" /></p> : null}
130
+ <AutoForm api="app" mutation={spec.create.tag} submitLabel={t('entity.addLabel', { table: spec.table })} />
88
131
  </div>
89
132
  ) : null}
90
133
 
91
134
  <div className="section">
92
135
  <h2><T id="entity.rows" /></h2>
93
- {spec.listTag ? (
94
- <DataTable api="app" query={spec.listTag} rowActions={rowActions} emptyText={t('entity.empty', { table: spec.table })} />
136
+ {spec.list.tag ? (
137
+ <DataTable api="app" query={spec.list.tag} rowActions={rowActions} emptyText={t('entity.empty', { table: spec.table })} />
95
138
  ) : (
96
139
  <p className="muted"><T id="entity.noList" /></p>
97
140
  )}
@@ -5,7 +5,13 @@
5
5
  // renders one of three observable states: a loading skeleton, an "unknown
6
6
  // entity" banner, or the bound CRUD surface (heading + AutoForm + DataTable).
7
7
  // We mock @voltro/web (useParams + the AutoForm/DataTable components) and
8
- // @voltro/client (the discovery + gating hooks).
8
+ // @voltro/client (the discovery + access hooks).
9
+ //
10
+ // The access cases below are the point of this file. The page must gate on the
11
+ // DECLARED access the manifest carries, never on a scope string it invented,
12
+ // and it must SHOW an `unknown` decision instead of hiding it — a per-row guard
13
+ // is the server's to answer, and hiding it empties the admin for every
14
+ // multi-tenant app whose subjects hold no global scopes.
9
15
 
10
16
  import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'
11
17
  import { act, createElement, type ReactNode } from 'react'
@@ -19,20 +25,25 @@ vi.mock('@voltro/web', () => ({
19
25
  useParams: () => params(),
20
26
  AutoForm: ({ mutation }: { mutation: string }) =>
21
27
  createElement('div', { 'data-testid': 'auto-form' }, mutation),
22
- DataTable: ({ query }: { query: string }) =>
23
- createElement('div', { 'data-testid': 'data-table' }, query),
28
+ DataTable: ({ query, rowActions }: { query: string; rowActions?: (r: Record<string, unknown>) => ReactNode }) =>
29
+ createElement('div', { 'data-testid': 'data-table' }, query, rowActions?.({ id: 'r1', entryNo: 'e1' })),
24
30
  }))
25
31
 
26
32
  interface ManifestState { manifest: unknown; loading: boolean }
27
33
  const capabilityManifest = vi.fn<() => ManifestState>()
28
34
  const entityAdmins = vi.fn<() => Array<Record<string, unknown>>>()
29
- const can = vi.fn<() => boolean>()
35
+ const accessDecision = vi.fn<() => 'allowed' | 'denied' | 'unknown'>()
36
+ const mutate = vi.fn()
30
37
 
31
38
  vi.mock('@voltro/client', () => ({
32
39
  useCapabilityManifest: () => capabilityManifest(),
33
40
  deriveEntityAdmins: () => entityAdmins(),
34
- useCan: () => can(),
35
- useMutation: () => ({ mutate: vi.fn() }),
41
+ useAccessDecision: () => accessDecision(),
42
+ requiredScopes: (guards?: ReadonlyArray<{ kind: string; scope?: ReadonlyArray<string> }>) =>
43
+ (guards ?? []).flatMap((g) => (g.kind === 'scope' ? [...(g.scope ?? [])] : [])),
44
+ openAccessReason: (guards?: ReadonlyArray<{ kind: string; reason?: string }>) =>
45
+ (guards ?? []).find((g) => g.kind === 'open')?.reason,
46
+ useMutation: () => ({ mutate }),
36
47
  useProvenance: () => ({ data: undefined, loading: false, error: undefined }),
37
48
  }))
38
49
 
@@ -58,12 +69,36 @@ const render = (node: ReactNode): void => {
58
69
  const cell = (id: string): string | null =>
59
70
  container.querySelector(`[data-testid="${id}"]`)?.textContent ?? null
60
71
 
72
+ /** A derived spec in its current shape. */
73
+ const spec = (over: Record<string, unknown> = {}): Record<string, unknown> => ({
74
+ table: 'todos',
75
+ columns: [{ name: 'id' }, { name: 'title' }],
76
+ serverOnlyColumns: [],
77
+ sensitiveColumns: [],
78
+ reactive: true,
79
+ pkColumn: 'id',
80
+ editable: true,
81
+ list: { tag: 'todos.list' },
82
+ create: { tag: 'todos.create', guards: [{ kind: 'scope', scope: ['todos:manage'], mode: 'all', resourceScoped: false }] },
83
+ update: {},
84
+ delete: { tag: 'todos.delete' },
85
+ ...over,
86
+ })
87
+
88
+ const showEntity = (over: Record<string, unknown> = {}): void => {
89
+ params.mockReturnValue({ entity: (over['table'] as string) ?? 'todos' })
90
+ capabilityManifest.mockReturnValue({ manifest: {}, loading: false })
91
+ entityAdmins.mockReturnValue([spec(over)])
92
+ render(createElement(EntityPage))
93
+ }
94
+
61
95
  beforeEach(() => {
62
96
  params.mockReset()
63
97
  capabilityManifest.mockReset()
64
98
  entityAdmins.mockReset()
65
- can.mockReset()
66
- can.mockReturnValue(true)
99
+ accessDecision.mockReset()
100
+ mutate.mockReset()
101
+ accessDecision.mockReturnValue('allowed')
67
102
  })
68
103
 
69
104
  afterEach(() => {
@@ -98,22 +133,94 @@ describe('entity page — render states', () => {
98
133
  })
99
134
 
100
135
  test('renders the bound CRUD surface (heading + AutoForm + DataTable) for a known entity', () => {
101
- params.mockReturnValue({ entity: 'todos' })
102
- capabilityManifest.mockReturnValue({ manifest: {}, loading: false })
103
- entityAdmins.mockReturnValue([
104
- {
105
- table: 'todos',
106
- columns: ['id', 'title'],
107
- listTag: 'todos.list',
108
- createTag: 'todos.create',
109
- createScope: 'todos:create',
110
- deleteScope: 'todos:delete',
111
- deleteTag: 'todos.delete',
112
- },
113
- ])
114
- render(createElement(EntityPage))
136
+ showEntity()
115
137
  expect(container.querySelector('h1')!.textContent).toBe('todos')
116
138
  expect(cell('auto-form')).toBe('todos.create')
117
- expect(cell('data-table')).toBe('todos.list')
139
+ expect(cell('data-table')).toContain('todos.list')
140
+ })
141
+ })
142
+
143
+ describe('entity page — gates on the DECLARED access', () => {
144
+ test('shows the create form when the api says the caller may create', () => {
145
+ accessDecision.mockReturnValue('allowed')
146
+ showEntity()
147
+ expect(cell('auto-form')).toBe('todos.create')
148
+ })
149
+
150
+ test('hides it on a DEFINITE denial', () => {
151
+ accessDecision.mockReturnValue('denied')
152
+ showEntity()
153
+ expect(container.querySelector('[data-testid="auto-form"]')).toBeNull()
154
+ // …and the list is still there. A denied write is not a denied read.
155
+ expect(cell('data-table')).toContain('todos.list')
156
+ })
157
+
158
+ test('SHOWS it on an indeterminate decision, and says why', () => {
159
+ // The false-deny trap: a per-row guard cannot be pre-computed in a browser.
160
+ // Hiding it would empty the admin for exactly the multi-tenant apps whose
161
+ // subjects carry no global scopes.
162
+ accessDecision.mockReturnValue('unknown')
163
+ showEntity()
164
+ expect(cell('auto-form')).toBe('todos.create')
165
+ expect(container.textContent).toContain('checks permission per row')
166
+ })
167
+
168
+ test('names the scope the api ACTUALLY requires, so a refusal is readable', () => {
169
+ showEntity()
170
+ expect(container.textContent).toContain('requires todos:manage')
171
+ })
172
+
173
+ test('reports an openAccess decision with its reason', () => {
174
+ showEntity({ create: { tag: 'todos.create', guards: [{ kind: 'open', reason: 'demo seeding' }] } })
175
+ expect(container.textContent).toContain('open access — demo seeding')
176
+ })
177
+
178
+ test('says so when a procedure declared no access at all', () => {
179
+ showEntity({ create: { tag: 'todos.create' } })
180
+ expect(container.textContent).toContain('no access decision declared')
181
+ })
182
+
183
+ test('renders nothing to create when the api exposes no create mutation', () => {
184
+ showEntity({ create: {} })
185
+ expect(container.querySelector('[data-testid="auto-form"]')).toBeNull()
186
+ })
187
+ })
188
+
189
+ describe('entity page — the three exposure axes', () => {
190
+ test('names the .serverOnly() columns it withheld, and why', () => {
191
+ showEntity({ serverOnlyColumns: ['pwHash', 'internalNote'] })
192
+ expect(container.textContent).toContain('2 server-only columns')
193
+ expect(container.textContent).toContain('pwHash, internalNote')
194
+ expect(container.textContent).toContain('cannot be submitted')
195
+ })
196
+
197
+ test('flags .sensitive() columns as export-masked, NOT as hidden', () => {
198
+ // The category error to avoid: `.sensitive()` is the export axis. The value
199
+ // is real wire data the operator is meant to see.
200
+ showEntity({ sensitiveColumns: ['email'] })
201
+ expect(container.textContent).toContain('masked in exports: email')
202
+ })
203
+
204
+ test('says nothing when no column is marked', () => {
205
+ showEntity()
206
+ expect(container.textContent).not.toContain('server-only')
207
+ expect(container.textContent).not.toContain('masked in exports')
208
+ })
209
+ })
210
+
211
+ describe('entity page — row identity', () => {
212
+ test('keys the delete on the pk the api declares, not a hard-coded id', () => {
213
+ showEntity({ pkColumn: 'entryNo' })
214
+ const del = container.querySelector('button.danger')!
215
+ act(() => { del.dispatchEvent(new MouseEvent('click', { bubbles: true })) })
216
+ // A hard-coded `id` would have sent `{ id: 'r1' }` — or `{ id: '' }` for a
217
+ // table that has no `id` column at all.
218
+ expect(mutate).toHaveBeenCalledWith({ entryNo: 'e1' })
219
+ })
220
+
221
+ test('offers no delete for a table with no addressable row', () => {
222
+ showEntity({ editable: false, pkColumn: undefined })
223
+ expect(container.querySelector('button.danger')).toBeNull()
224
+ expect(container.textContent).toContain('No single primary key')
118
225
  })
119
226
  })