@voltro/cli 0.33.0 → 0.35.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (343) hide show
  1. package/CHANGELOG.md +1968 -0
  2. package/bin/nodeEnvironment.d.mts +30 -0
  3. package/bin/nodeEnvironment.mjs +158 -0
  4. package/bin/voltro.mjs +69 -5
  5. package/dist/addCommand-BNeoeSxe.js +124 -0
  6. package/dist/addCommand-aXSQveak.js +2 -0
  7. package/dist/agentsMd-BTchIZku.js +2 -0
  8. package/dist/agentsMd-mhQMF1bx.js +254 -0
  9. package/dist/apiBuild-B8aoJvuw.js +2 -0
  10. package/dist/{apiBuild-h9VHtnlw.js → apiBuild-DYD_ONLD.js} +46 -46
  11. package/dist/{appGraph-gQ_6GkQQ.js → appGraph-KGDPTuTy.js} +1 -1
  12. package/dist/appGraph-zuMGKVYX.js +2 -0
  13. package/dist/appPort-B_HpJ_ck.js +48 -0
  14. package/dist/baselineCommand-C2ClWZN3.js +2 -0
  15. package/dist/baselineCommand-DIttzO8A.js +227 -0
  16. package/dist/bin.js +71 -28
  17. package/dist/build-CD8K4XOr.js +711 -0
  18. package/dist/cacheCommand-DA4OH9xt.js +42 -0
  19. package/dist/capabilitiesCommand-nq_pz5xd.js +123 -0
  20. package/dist/checkCommand-Ct9xkTrS.js +232 -0
  21. package/dist/checkCommand-DKpDLlqu.js +2 -0
  22. package/dist/{cliArgs-qdZSElM3.js → cliArgs-D4p8n7EE.js} +12 -1
  23. package/dist/cliError-BmdYnghb.js +10 -0
  24. package/dist/cliOutput-D1tSBoRM.js +15 -0
  25. package/dist/{cliRuntime-Oh517vCV.js → cliRuntime-Dh7UDinH.js} +20 -20
  26. package/dist/cloudClient-DWL-Hw_T.js +67 -0
  27. package/dist/cloudCmd-Cvv5HGaZ.js +364 -0
  28. package/dist/clusterCmd-D5wsCmA_.js +54 -0
  29. package/dist/codegen-CYM3Zqrf.js +605 -0
  30. package/dist/codegen-ChBi_hVa.js +2 -0
  31. package/dist/codegenCommand-C4YoQIc2.js +30 -0
  32. package/dist/codemodRunner-BnFq3Fgu.js +5384 -0
  33. package/dist/commandRunner-BLAEFLjp.js +47 -0
  34. package/dist/commands-BE8E7zF3.js +816 -0
  35. package/dist/connectionConfig-UFlIEiys.js +66 -0
  36. package/dist/dashboardCommand-D7SgZGaN.js +25 -0
  37. package/dist/dataCommand-BhYwDgg-.js +537 -0
  38. package/dist/dataProfile-dW-PsfLB.js +15 -0
  39. package/dist/dbCommand-DS4b97Is.js +2 -0
  40. package/dist/{dbCommand-DTLKAfbA.js → dbCommand-O8HA63s2.js} +552 -402
  41. package/dist/{dev-C_P8FLSx.js → dev-C7sFZq3m.js} +3670 -3169
  42. package/dist/dev-D2BikO7a.js +3 -0
  43. package/dist/devActivity-Dx_3nnGv.js +100 -0
  44. package/dist/devActivity.js +1 -1
  45. package/dist/dialectDriver-CgXnDfec.js +39 -0
  46. package/dist/discover-C9XKJDco.js +25 -0
  47. package/dist/doctorCommand-CM4Ch9C7.js +2 -0
  48. package/dist/{checkCommand-xGhRFFg2.js → doctorCommand-DnimF5IM.js} +523 -1278
  49. package/dist/dormancyCommand-QewYug_s.js +69 -0
  50. package/dist/e2eCmd-BRabZww-.js +147 -0
  51. package/dist/embeddingsCommand-BfiLS_QI.js +73 -0
  52. package/dist/envCommand-CCGPRQY1.js +60 -0
  53. package/dist/evalCommand-6RUfPen4.js +118 -0
  54. package/dist/evolveCommand-CHsLCtDf.js +281 -0
  55. package/dist/fileTaxonomy-CJfgOllU.js +457 -0
  56. package/dist/frameworkTableAssembly-YyVe32Cb.js +2 -0
  57. package/dist/frameworkTableAssembly-oMPBKqlE.js +511 -0
  58. package/dist/generateCommand-DbgcUpGw.js +147 -0
  59. package/dist/index.d.ts +45 -0
  60. package/dist/index.js +4 -3
  61. package/dist/infoCommand-DwOgK1t6.js +60 -0
  62. package/dist/{inspect-BUUjt773.js → inspect-CjTYzAs_.js} +113 -41
  63. package/dist/inspect-P4pxoMaV.js +2 -0
  64. package/dist/inspectCmd-EHFZ9yYu.js +224 -0
  65. package/dist/inspectFetch-EMuhTG_9.js +151 -0
  66. package/dist/inspectMetrics-CfdKLh6t.js +72 -0
  67. package/dist/loadEnv-D9nEOClM.js +44 -0
  68. package/dist/logFileSink-C_D2wRN1.js +105 -0
  69. package/dist/logsCmd-D36xK7Zu.js +260 -0
  70. package/dist/manifestBuild-COkJoyAr.js +2 -0
  71. package/dist/{manifestBuild-BLrVuSlM.js → manifestBuild-hpPLaGxV.js} +1 -1
  72. package/dist/metaCommands-7MJfZ5cf.js +196 -0
  73. package/dist/migrate-BV7I-ZHZ.js +83 -0
  74. package/dist/mssqlClusterPatch-_4cE_nun.js +44 -0
  75. package/dist/newCommand-COWOJ1_E.js +156 -0
  76. package/dist/nodeEnvironment-cGFAj1J8.js +28 -0
  77. package/dist/packageCommand-Cug_3Ogl.js +271 -0
  78. package/dist/pageConvention-cEiRxdab.js +5 -0
  79. package/dist/privacyCommand-C-Df56U_.js +146 -0
  80. package/dist/probeCommand-CZfaaUOZ.js +122 -0
  81. package/dist/projectScaffold-DmzEKHib.js +2 -0
  82. package/dist/projectScaffold-LMMtaavR.js +814 -0
  83. package/dist/renderModeScan-D7J1B7Kw.js +105 -0
  84. package/dist/renderProfile-1OWWAAtx.js +81 -0
  85. package/dist/runtimeRegistry-DMeKfTHP.js +81 -0
  86. package/dist/runtimeTrace-CH3eUiMw.js +91 -0
  87. package/dist/scheduleCmd-DQRu6BZC.js +149 -0
  88. package/dist/scheduleManifestCmd-D2x0CTTY.js +249 -0
  89. package/dist/schemaIr-UJybUUZW.js +103 -0
  90. package/dist/{sdkgen-C81QIkiL.js → sdkgen-BLkvGRfX.js} +111 -209
  91. package/dist/seedRunner-ZmLSqNe2.js +333 -0
  92. package/dist/serveCommand-CbDHU6l-.js +2 -0
  93. package/dist/serveCommand-iwlUBNS1.js +1766 -0
  94. package/dist/serveEntry.js +5 -5
  95. package/dist/serverlessCommand-CfJZy6dS.js +482 -0
  96. package/dist/start-BgN62boB.js +3 -0
  97. package/dist/start-T4VesWiM.js +1087 -0
  98. package/dist/startEntry.js +2 -2
  99. package/dist/staticCommand-Dr2M6tpU.js +304 -0
  100. package/dist/storageCommand-Co6NfLqN.js +42 -0
  101. package/dist/templates-De8IR5-c.js +102 -0
  102. package/dist/test-CI6iDsYc.js +115 -0
  103. package/dist/tracesCmd-DStmCJPi.js +232 -0
  104. package/dist/tsconfigPaths-BWXBWgcl.js +107 -0
  105. package/dist/tsxLoader-EuXmSJ1K.js +51 -0
  106. package/dist/typecheckCommand-BlsWiCNq.js +61 -0
  107. package/dist/updateCommand-BlMZhWgO.js +2 -0
  108. package/dist/updateCommand-x0pI_x-B.js +585 -0
  109. package/dist/webDev-BcykISYQ2.js +2 -0
  110. package/dist/{inspectMetrics-1xzTKAFx.js → webDev-Dybxew86.js} +988 -1560
  111. package/dist/webhookDiscovery-CrGAfhIG.js +2 -0
  112. package/dist/webhookDiscovery-D7VaeMlz.js +51 -0
  113. package/dist/webhooksCommand-DlAgS2Iw.js +267 -0
  114. package/dist/workflowsCmd-BGF-mRZ5.js +608 -0
  115. package/package.json +209 -17
  116. package/templates/AGENTS.core.md +58 -3
  117. package/templates/AGENTS.md +64 -7
  118. package/templates/agent-docs/_index.md +6 -4
  119. package/templates/agent-docs/_manifest.json +23 -6
  120. package/templates/agent-docs/ai.md +370 -0
  121. package/templates/agent-docs/authentication.md +313 -31
  122. package/templates/agent-docs/caching.md +6 -0
  123. package/templates/agent-docs/cli.md +853 -50
  124. package/templates/agent-docs/data.md +608 -12
  125. package/templates/agent-docs/database/migrations.md +223 -25
  126. package/templates/agent-docs/database/misc.md +156 -40
  127. package/templates/agent-docs/database/querying.md +19 -1
  128. package/templates/agent-docs/database/scaling.md +60 -0
  129. package/templates/agent-docs/database/schema.md +1 -0
  130. package/templates/agent-docs/database/seedsdialects.md +208 -19
  131. package/templates/agent-docs/database/transactions.md +68 -0
  132. package/templates/agent-docs/deployment.md +284 -25
  133. package/templates/agent-docs/introduction.md +88 -17
  134. package/templates/agent-docs/local-first-mobile.md +79 -4
  135. package/templates/agent-docs/multi-tenancy.md +188 -42
  136. package/templates/agent-docs/observability.md +58 -3
  137. package/templates/agent-docs/plugins/ai-flows.md +247 -2
  138. package/templates/agent-docs/plugins/analytics-postgres.md +1 -1
  139. package/templates/agent-docs/plugins/audit.md +37 -1
  140. package/templates/agent-docs/plugins/auth-social.md +143 -0
  141. package/templates/agent-docs/plugins/auth-workos.md +4 -2
  142. package/templates/agent-docs/plugins/auth.md +131 -6
  143. package/templates/agent-docs/plugins/billing.md +132 -15
  144. package/templates/agent-docs/plugins/cdc-out.md +46 -7
  145. package/templates/agent-docs/plugins/clickhouse.md +32 -1
  146. package/templates/agent-docs/plugins/duckdb.md +1 -1
  147. package/templates/agent-docs/plugins/flags.md +132 -0
  148. package/templates/agent-docs/plugins/governance.md +105 -7
  149. package/templates/agent-docs/plugins/multitenancy.md +9 -4
  150. package/templates/agent-docs/plugins/presence.md +13 -2
  151. package/templates/agent-docs/plugins/ratelimit.md +9 -0
  152. package/templates/agent-docs/plugins/search.md +162 -8
  153. package/templates/agent-docs/plugins/sso-saml.md +47 -8
  154. package/templates/agent-docs/plugins/storage.md +11 -0
  155. package/templates/agent-docs/plugins/webhooks.md +105 -0
  156. package/templates/agent-docs/plugins.md +152 -18
  157. package/templates/agent-docs/reference.md +60 -3
  158. package/templates/agent-docs/releases.md +1117 -0
  159. package/templates/agent-docs/routing.md +43 -25
  160. package/templates/agent-docs/scheduling.md +27 -0
  161. package/templates/agent-docs/schema-driven-ui.md +92 -12
  162. package/templates/agent-docs/security.md +426 -0
  163. package/templates/agent-docs/templates/apibackends.md +87 -18
  164. package/templates/agent-docs/templates/appshells.md +32 -14
  165. package/templates/agent-docs/templates/overview.md +13 -8
  166. package/templates/agent-docs/testing.md +211 -14
  167. package/templates/agent-docs/whats-new.md +98 -136
  168. package/templates/agent-docs/workflows.md +231 -14
  169. package/templates/apps/api-ai/actions/summarize.action.ts +11 -0
  170. package/templates/apps/api-ai/package.json +8 -7
  171. package/templates/apps/api-auth/actions/me.action.ts +13 -0
  172. package/templates/apps/api-auth/package.json +9 -8
  173. package/templates/apps/api-backend/mutations/notes.create.mutation.ts +9 -0
  174. package/templates/apps/api-backend/package.json +12 -8
  175. package/templates/apps/api-backend/queries/notes.query.ts +30 -8
  176. package/templates/apps/api-backend-deactivation/actions/users.get.action.ts +14 -0
  177. package/templates/apps/api-backend-deactivation/mutations/users.create.mutation.ts +8 -0
  178. package/templates/apps/api-backend-deactivation/mutations/users.deactivate.mutation.ts +10 -0
  179. package/templates/apps/api-backend-deactivation/package.json +8 -7
  180. package/templates/apps/api-backend-mail/actions/sendWelcome.action.ts +17 -0
  181. package/templates/apps/api-backend-mail/mutations/notes.create.mutation.ts +9 -0
  182. package/templates/apps/api-backend-mail/package.json +9 -8
  183. package/templates/apps/api-backend-mail/queries/notes.query.ts +30 -8
  184. package/templates/apps/api-backend-mariadb/.env.example +14 -0
  185. package/templates/apps/api-backend-mariadb/mutations/notes.create.mutation.ts +9 -0
  186. package/templates/apps/api-backend-mariadb/package.json +10 -9
  187. package/templates/apps/api-backend-mariadb/queries/notes.query.ts +30 -8
  188. package/templates/apps/api-backend-sqlite/.env.example +14 -0
  189. package/templates/apps/api-backend-sqlite/mutations/notes.create.mutation.ts +9 -0
  190. package/templates/apps/api-backend-sqlite/package.json +9 -8
  191. package/templates/apps/api-backend-sqlite/queries/notes.query.ts +30 -8
  192. package/templates/apps/api-backend-storage/actions/uploadAvatar.action.ts +13 -0
  193. package/templates/apps/api-backend-storage/actions/uploadDocument.action.ts +12 -0
  194. package/templates/apps/api-backend-storage/mutations/notes.create.mutation.ts +9 -0
  195. package/templates/apps/api-backend-storage/package.json +9 -8
  196. package/templates/apps/api-backend-storage/queries/notes.query.ts +30 -8
  197. package/templates/apps/api-cms/actions/content.get.action.ts +7 -0
  198. package/templates/apps/api-cms/actions/content.types.action.ts +6 -0
  199. package/templates/apps/api-cms/actions/me.action.ts +13 -0
  200. package/templates/apps/api-cms/app.config.ts +19 -0
  201. package/templates/apps/api-cms/authz.ts +63 -0
  202. package/templates/apps/api-cms/mutations/content.publish.mutation.ts +10 -0
  203. package/templates/apps/api-cms/mutations/content.saveDraft.mutation.ts +10 -0
  204. package/templates/apps/api-cms/mutations/content.unpublish.mutation.ts +5 -0
  205. package/templates/apps/api-cms/package.json +11 -10
  206. package/templates/apps/api-cms/queries/content.list.query.ts +23 -7
  207. package/templates/apps/api-cms/tests/accessDecisions.test.ts +121 -0
  208. package/templates/apps/api-cms/tests/content.descriptors.test.ts +8 -4
  209. package/templates/apps/api-collab/mutations/documents.create.mutation.ts +8 -0
  210. package/templates/apps/api-collab/mutations/documents.setBody.mutation.ts +13 -0
  211. package/templates/apps/api-collab/package.json +9 -8
  212. package/templates/apps/api-collab/queries/documents.query.ts +19 -7
  213. package/templates/apps/api-data-advanced/package.json +9 -8
  214. package/templates/apps/api-data-advanced/queries/authors.withBooks.query.ts +12 -0
  215. package/templates/apps/api-data-advanced/queries/books.search.query.ts +9 -0
  216. package/templates/apps/api-durable/mutations/orders.approve.mutation.ts +17 -0
  217. package/templates/apps/api-durable/mutations/orders.place.mutation.ts +9 -0
  218. package/templates/apps/api-durable/package.json +9 -8
  219. package/templates/apps/api-feature-flags/actions/notes.export.action.ts +16 -0
  220. package/templates/apps/api-feature-flags/mutations/notes.create.mutation.ts +12 -0
  221. package/templates/apps/api-feature-flags/package.json +10 -9
  222. package/templates/apps/api-governance/README.md +30 -8
  223. package/templates/apps/api-governance/actions/profiles.get.action.server.ts +17 -1
  224. package/templates/apps/api-governance/actions/profiles.get.action.ts +29 -4
  225. package/templates/apps/api-governance/database/schema.ts +16 -4
  226. package/templates/apps/api-governance/mutations/profiles.create.mutation.ts +12 -0
  227. package/templates/apps/api-governance/package.json +9 -8
  228. package/templates/apps/api-kv/actions/sync.pull.action.ts +15 -0
  229. package/templates/apps/api-kv/actions/sync.reset.action.ts +13 -0
  230. package/templates/apps/api-kv/actions/sync.status.action.ts +7 -0
  231. package/templates/apps/api-kv/package.json +9 -8
  232. package/templates/apps/api-kv/queries/events.list.query.ts +19 -8
  233. package/templates/apps/api-moderation/mutations/comments.create.mutation.ts +11 -0
  234. package/templates/apps/api-moderation/mutations/posts.create.mutation.ts +13 -0
  235. package/templates/apps/api-moderation/package.json +9 -8
  236. package/templates/apps/api-observability/mutations/notes.create.mutation.ts +8 -0
  237. package/templates/apps/api-observability/package.json +9 -8
  238. package/templates/apps/api-observability/queries/notes.list.query.ts +13 -0
  239. package/templates/apps/api-ratelimit/mutations/notes.create.mutation.ts +14 -0
  240. package/templates/apps/api-ratelimit/package.json +9 -8
  241. package/templates/apps/api-rbac/package.json +9 -8
  242. package/templates/apps/api-rest/package.json +8 -7
  243. package/templates/apps/api-saas/mutations/projects.create.mutation.ts +13 -0
  244. package/templates/apps/api-saas/package.json +12 -11
  245. package/templates/apps/api-saas/queries/projects.list.query.ts +11 -0
  246. package/templates/apps/api-saas-starter/actions/me.action.ts +13 -0
  247. package/templates/apps/api-saas-starter/app.config.ts +19 -0
  248. package/templates/apps/api-saas-starter/authz.ts +75 -0
  249. package/templates/apps/api-saas-starter/mutations/invites.create.mutation.ts +10 -0
  250. package/templates/apps/api-saas-starter/mutations/projects.create.mutation.ts +9 -0
  251. package/templates/apps/api-saas-starter/package.json +15 -11
  252. package/templates/apps/api-saas-starter/queries/invites.list.query.ts +19 -8
  253. package/templates/apps/api-saas-starter/queries/projects.list.query.ts +18 -7
  254. package/templates/apps/api-saas-starter/tests/accessDecisions.test.ts +135 -0
  255. package/templates/apps/api-search/mutations/articles.create.mutation.ts +14 -0
  256. package/templates/apps/api-search/package.json +9 -8
  257. package/templates/apps/api-search/queries/articles.list.query.ts +21 -8
  258. package/templates/apps/api-status/README.md +10 -3
  259. package/templates/apps/api-status/app.config.ts +8 -3
  260. package/templates/apps/api-status/authz.ts +5 -3
  261. package/templates/apps/api-status/package.json +9 -8
  262. package/templates/apps/api-status/queries/components.list.query.ts +14 -6
  263. package/templates/apps/api-status/queries/incidents.live.query.ts +23 -10
  264. package/templates/apps/api-status/queries/updates.list.query.ts +16 -9
  265. package/templates/apps/api-status/tests/status.test.ts +9 -1
  266. package/templates/apps/api-versioning/actions/documents.asOf.action.ts +12 -0
  267. package/templates/apps/api-versioning/actions/documents.history.action.ts +13 -0
  268. package/templates/apps/api-versioning/mutations/documents.create.mutation.ts +9 -0
  269. package/templates/apps/api-versioning/mutations/documents.update.mutation.ts +12 -0
  270. package/templates/apps/api-versioning/package.json +9 -8
  271. package/templates/apps/api-webhooks/mutations/orders.fulfill.mutation.ts +16 -0
  272. package/templates/apps/api-webhooks/package.json +10 -9
  273. package/templates/apps/api-webhooks/queries/orders.list.query.ts +10 -0
  274. package/templates/apps/changelog/package.json +8 -6
  275. package/templates/apps/edge-functions/package.json +2 -2
  276. package/templates/apps/frontend-admin/package.json +10 -8
  277. package/templates/apps/frontend-admin/src/lib/admin.ts +20 -9
  278. package/templates/apps/frontend-admin/src/locales/de.ts +11 -1
  279. package/templates/apps/frontend-admin/src/locales/en.ts +13 -1
  280. package/templates/apps/frontend-admin/src/pages/admin/[entity]/page.tsx +65 -22
  281. package/templates/apps/frontend-admin/src/pages/admin/entity.test.tsx +130 -23
  282. package/templates/apps/frontend-admin/src/pages/admin/layout.tsx +3 -2
  283. package/templates/apps/frontend-admin/src/pages/admin/page.test.tsx +19 -2
  284. package/templates/apps/frontend-admin/src/pages/admin/page.tsx +9 -4
  285. package/templates/apps/frontend-app/app.config.ts +4 -3
  286. package/templates/apps/frontend-app/package.json +11 -8
  287. package/templates/apps/frontend-app/src/lib/api.ts +25 -0
  288. package/templates/apps/frontend-app/src/pages/page.test.tsx +130 -82
  289. package/templates/apps/frontend-app/src/pages/page.tsx +14 -18
  290. package/templates/apps/frontend-auth/package.json +10 -8
  291. package/templates/apps/frontend-blank/package.json +9 -7
  292. package/templates/apps/frontend-cms/package.json +11 -9
  293. package/templates/apps/frontend-collab/package.json +12 -9
  294. package/templates/apps/frontend-collab/src/pages/page.test.tsx +122 -78
  295. package/templates/apps/frontend-contact/package.json +9 -7
  296. package/templates/apps/frontend-dashboard/package.json +9 -7
  297. package/templates/apps/frontend-docs/package.json +9 -7
  298. package/templates/apps/frontend-i18n/package.json +8 -6
  299. package/templates/apps/frontend-landing/package.json +9 -7
  300. package/templates/apps/frontend-portal/package.json +10 -8
  301. package/templates/apps/frontend-saas/app.config.ts +10 -6
  302. package/templates/apps/frontend-saas/package.json +10 -8
  303. package/templates/apps/frontend-saas/src/lib/api.ts +27 -32
  304. package/templates/apps/frontend-saas/src/pages/dashboard/billing/page.tsx +7 -8
  305. package/templates/apps/frontend-saas/src/pages/dashboard/page.test.tsx +27 -3
  306. package/templates/apps/frontend-saas/src/pages/dashboard/page.tsx +4 -4
  307. package/templates/apps/frontend-saas/src/pages/dashboard/team/page.tsx +3 -4
  308. package/templates/apps/frontend-spa/package.json +9 -7
  309. package/templates/apps/frontend-ssr/package.json +9 -7
  310. package/templates/apps/frontend-ssr-api/package.json +10 -8
  311. package/templates/apps/frontend-static-blog/package.json +8 -6
  312. package/templates/apps/frontend-status/package.json +10 -8
  313. package/templates/apps/mobile-app/README.md +1 -0
  314. package/templates/apps/mobile-app/package.json +4 -2
  315. package/templates/apps/mobile-app/src/app/index.tsx +22 -12
  316. package/templates/apps/mobile-app/src/app/orders/[id].tsx +1 -1
  317. package/templates/apps/mobile-app/src/lib/api.ts +34 -0
  318. package/templates/apps/mobile-app/voltro.mobile.ts +4 -2
  319. package/templates/baselines/bare/.env.example +14 -0
  320. package/templates/baselines/bare/baseline.json +4 -4
  321. package/templates/baselines/compose/.env.example +14 -0
  322. package/templates/baselines/compose/README.md +1 -1
  323. package/templates/baselines/compose/baseline.json +5 -5
  324. package/templates/baselines/compose-mariadb/.env.example +14 -0
  325. package/templates/baselines/compose-mariadb/README.md +1 -1
  326. package/templates/baselines/compose-mariadb/baseline.json +5 -5
  327. package/templates/baselines/helm/.env.example +14 -0
  328. package/templates/baselines/helm/baseline.json +4 -4
  329. package/dist/apiBuild-C-x9YacA.js +0 -2
  330. package/dist/appGraph-CvQCte0z.js +0 -2
  331. package/dist/checkCommand-DRovTKza.js +0 -2
  332. package/dist/commands-CJfepbm4.js +0 -11541
  333. package/dist/dbCommand-b1gum4td.js +0 -2
  334. package/dist/dev-iiMtlkfs.js +0 -3
  335. package/dist/devActivity-BhIu6ncs.js +0 -159
  336. package/dist/frameworkTableAssembly-BwIrO5nv.js +0 -638
  337. package/dist/frameworkTableAssembly-D-EebUQX.js +0 -2
  338. package/dist/inspect-mmBuRXmy.js +0 -2
  339. package/dist/manifestBuild-Dj8Jjoto.js +0 -2
  340. package/dist/seedRunner-Bqxgp7HZ.js +0 -230
  341. package/dist/serveCommand-DdaM4Hup.js +0 -1608
  342. package/dist/start-C0koT0UO.js +0 -1084
  343. /package/templates/apps/api-ai/actions/{summarize.action.server.tsx → summarize.action.server.ts} +0 -0
@@ -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,
@@ -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.35.0",
17
+ "@voltro/database": "0.35.0",
18
+ "@voltro/env": "0.35.0",
19
+ "@voltro/plugin-multitenancy": "0.35.0",
20
+ "@voltro/plugin-versioning": "0.35.0",
21
+ "@voltro/protocol": "0.35.0",
22
+ "@voltro/runtime": "0.35.0",
23
23
  "effect": "^3.22.0"
24
24
  },
25
25
  "devDependencies": {
26
- "@voltro/testing": "0.33.0",
26
+ "@voltro/testing": "0.35.0",
27
27
  "typescript": "^6.0.3",
28
+ "@vitest/coverage-v8": "^4.1.10",
28
29
  "vitest": "^4.1.10"
29
30
  }
30
31
  }
@@ -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.35.0",
17
+ "@voltro/database": "0.35.0",
18
+ "@voltro/env": "0.35.0",
19
+ "@voltro/plugin-multitenancy": "0.35.0",
20
+ "@voltro/plugin-webhooks": "0.35.0",
21
+ "@voltro/protocol": "0.35.0",
22
+ "@voltro/runtime": "0.35.0",
23
+ "@voltro/sql-postgres": "0.35.0",
24
24
  "effect": "^3.22.0"
25
25
  },
26
26
  "devDependencies": {
27
- "@voltro/testing": "0.33.0",
27
+ "@voltro/testing": "0.35.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.35.0",
16
+ "@voltro/cli": "0.35.0",
17
+ "@voltro/client": "0.35.0",
18
+ "@voltro/env": "0.35.0",
19
+ "@voltro/i18n": "0.35.0",
20
+ "@voltro/web": "0.35.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.35.0",
16
+ "@voltro/serverless": "0.35.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.35.0",
16
+ "@voltro/cli": "0.35.0",
17
+ "@voltro/client": "0.35.0",
18
+ "@voltro/env": "0.35.0",
19
+ "@voltro/i18n": "0.35.0",
20
+ "@voltro/ui-shadcn": "0.35.0",
21
+ "@voltro/web": "0.35.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.35.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
  )}