@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
@@ -55,6 +55,14 @@ SLO works in **both directions**. A `LogoutRequest` must reference the IdP `Name
55
55
  - **SP-initiated** (`GET /saml/logout`) — loads the stored NameID/SessionIndex, builds the (signed) `LogoutRequest`, clears the session cookie, and redirects to the IdP SLO endpoint.
56
56
  - **IdP-initiated** (`GET|POST /saml/slo`) — the IdP's signed `LogoutRequest` clears the SP session and is answered with a `LogoutResponse`; a bad signature is rejected (`401`) and the session is left intact.
57
57
 
58
+ An SLO message with **no signature at all** is rejected `401` before the SAML
59
+ library is consulted. This is not a formality: the underlying verifier treats
60
+ an absent redirect-binding signature as "nothing to verify" and returns
61
+ valid, and `/saml/slo` is a `GET` that is deliberately CSRF-exempt — so an
62
+ unsigned message would let any page log a visitor out with an `<img>` tag.
63
+ **If your IdP does not sign SLO, it now gets a 401**; enable message signing
64
+ on the IdP side.
65
+
58
66
  The SLO state store mirrors the replay cache — in-process by default (single replica), shared via the DataStore with `sloStore: { store: true }` (a `_voltro_saml_logout` table) so a login on one replica can log out on another.
59
67
 
60
68
  ```ts
@@ -104,22 +112,53 @@ samlSsoPlugin({
104
112
 
105
113
  All three secrets come from env / a secrets backend — never a literal, never logged.
106
114
 
107
- ## Replay / `InResponseTo` protection
115
+ ## Which signatures are required
116
+
117
+ Two separate requirements, and only one of them is a choice:
118
+
119
+ | | Required? | Option |
120
+ |---|---|---|
121
+ | **Assertion** signature | Always. Not configurable. | — |
122
+ | **Response** envelope signature | No, by default | `wantAuthnResponseSigned` |
123
+
124
+ The assertion is the part that matters: only signature-covered XML is ever read, and the assertion is what carries the NameID, the attributes, the audience restriction, the validity window and the `SubjectConfirmationData`. An envelope signature additionally covers the response-level `Status`, `Destination` and `InResponseTo`.
108
125
 
109
- By default node-saml runs with `validateInResponseTo: 'never'`, so a captured `SAMLResponse` can be replayed to the ACS within its validity window (and unsolicited IdP-initiated posts are accepted). Turn on the one-time request-id cache with `replayProtection`:
126
+ **The default accepts an assertion-signed, envelope-unsigned response** Okta's default application profile ("Sign assertion", response unsigned), and Azure AD's. Set `wantAuthnResponseSigned: true` if your IdP signs the response; it is one checkbox on both, and it is the stronger posture:
110
127
 
111
128
  ```ts
112
129
  samlSsoPlugin({
113
130
  // …idp / sp / sessionSecret / onLogin…
114
- replayProtection: { store: true }, // shared across replicas the production choice
131
+ wantAuthnResponseSigned: true, // require the envelope signature too
115
132
  })
116
133
  ```
117
134
 
118
- - `false` (default)off. Single-replica dev / PoC only.
119
- - `true` — on with an **in-process** cache. Correct for one replica only; a login and its ACS POST that land on different processes fail the check (boot warns).
120
- - `{ store: true, ttlMs? }` — on, backed by the framework DataStore (a contributed `_voltro_saml_replay` table). Shared across replicas. Declares `store:write`. `ttlMs` bounds an outstanding request's validity (default 10 min).
135
+ One edge worth knowing at the default: an envelope signature that does **not** verify is treated the same as no envelope signature it is discarded and the assertion signature decides. That is not a bypass (an attacker holding a validly signed assertion would simply send no envelope signature, and an attacker-signed *assertion* is refused at every setting), but it does mean an IdP misconfigured to sign responses with the wrong key goes unnoticed. `wantAuthnResponseSigned: true` surfaces it.
136
+
137
+ ## Replay / `InResponseTo` protection
138
+
139
+ **On by default, backed by the DataStore.** The `AuthnRequest` id is stored at `/saml/login` and **consumed** at `/saml/acs`, so the same assertion can't be replayed and a response referencing no request this SP issued is rejected.
140
+
141
+ - `{ store: true, ttlMs? }` (**default**) — backed by the framework DataStore (a contributed `_voltro_saml_replay` table). Shared across replicas. Declares `store:write`. `ttlMs` bounds an outstanding request's validity (default 10 min).
142
+ - `true` — an **in-process** cache. Correct for one replica only; a login and its ACS POST that land on different processes fail the check (boot warns). The default is store-backed rather than this precisely because the in-process mode is not a milder version of the same protection — under more than one replica it is a total login outage.
143
+ - `false` — off, and a captured `SAMLResponse` is replayable within its validity window.
144
+
145
+ ### It refuses IdP-initiated SSO — and `false` is the only way back
146
+
147
+ Replay protection sets `validateInResponseTo: 'always'`, which means a `SAMLResponse` carrying no `InResponseTo` is rejected. That is exactly an **IdP-initiated** login: the Okta / Azure dashboard app tile, rather than a user arriving at `/saml/login`.
148
+
149
+ You cannot keep both, and the reason is structural rather than a missing feature: the protection *is* the requirement that the response answer a request this SP issued, and an unsolicited response answers none. (`'ifPresent'` looks like the compromise and is not one — an attacker replaying a captured response just deletes the attribute and the check declines to run.)
150
+
151
+ If you need the app tile, opt out deliberately:
152
+
153
+ ```ts
154
+ samlSsoPlugin({
155
+ // …idp / sp / sessionSecret / onLogin…
156
+ replayProtection: false, // accepts unsolicited responses — and replays
157
+ wantAuthnResponseSigned: true, // recommended if you must run unsolicited
158
+ })
159
+ ```
121
160
 
122
- The `AuthnRequest` id is stored at `/saml/login` and **consumed** at `/saml/acs`, so the same assertion can't be replayed and a response referencing no request this SP issued is rejected.
161
+ Boot warns when replay protection is off, because nothing about it is visible at runtime.
123
162
 
124
163
  ## Notes
125
164
 
@@ -129,4 +168,4 @@ The `AuthnRequest` id is stored at `/saml/login` and **consumed** at `/saml/acs`
129
168
 
130
169
  ## Permissions
131
170
 
132
- None by default (it serves the SAML routes + mints a session). `replayProtection: { store: true }` and `sloStore: { store: true }` each declare `store:write` for their shared table (`_voltro_saml_replay` / `_voltro_saml_logout`).
171
+ `store:write` by default replay protection is on and store-backed, so the plugin contributes `_voltro_saml_replay`. `sloStore: { store: true }` adds `_voltro_saml_logout` under the same permission. Setting `replayProtection: false` (and leaving `sloStore` at its in-process default) drops the permission entirely: the plugin then only serves the SAML routes + mints a session.
@@ -417,6 +417,17 @@ procedure's wire-error union so they decode typed on the client:
417
417
  `storage.revoke`, `storage.listGrants`, `storage.listRefs`. (The upload hook picks
418
418
  the right ones per `prefer`; you rarely call them directly.)
419
419
 
420
+ Every route declares its access decision. The self-service upload/read flow is
421
+ `openAccess` — what bounds it is structural (mints bind the caller's subject +
422
+ tenant into a signed ticket, finalize/complete refuse another subject's token,
423
+ reads go through the per-object access policy + grants, the grant RPCs enforce
424
+ owner-or-admin in-handler). Three routes require a scope instead: **`storage.mintUploadUrl`**
425
+ (the raw bring-your-own-key PUT, which bypasses limits + scan) and
426
+ **`storage.ingestUrl`** (the server fetches a caller-supplied URL — an SSRF
427
+ surface) need `storage:manage`; **`storage.listRefs`** (the tenant-WIDE media
428
+ library index) needs `storage:browse`. Grant them via an rbac role or
429
+ `resolveScopes`; `admin:full` passes.
430
+
420
431
  ### The grant RPCs are owner-only
421
432
 
422
433
  `storage.share`, `storage.revoke` and `storage.listGrants` require that the
@@ -328,6 +328,8 @@ import { Schema } from 'effect'
328
328
 
329
329
  export const subscribeWebhook = defineMutation({
330
330
  name: 'webhooks.subscribe',
331
+ // Registers a URL this server will POST your events to — never openAccess.
332
+ guards: [{ scope: 'webhooks:manage' }],
331
333
  input: Schema.Struct({ url: Schema.String }),
332
334
  output: Schema.Struct({ targetId: Schema.String }),
333
335
  error: WebhookSubscribeInvalid,
@@ -374,6 +376,37 @@ The endpoint mounts at `/webhooks/<id>` by default; override with
374
376
  `VOLTRO_WEBHOOK_SECRET_STRIPE` (non-alphanumeric characters in the id become
375
377
  `_`).
376
378
 
379
+ ### Verification is not optional
380
+
381
+ An incoming webhook is a **public POST that runs your application code**, so the
382
+ framework refuses to mount one that has made no decision about who may call it.
383
+ A descriptor with no `signature` and no `provider` fails the boot, naming the
384
+ endpoint. Four ways to satisfy it:
385
+
386
+ | Declaration | Means |
387
+ |---|---|
388
+ | `provider: stripeProvider()` | a preset brings the scheme, replay window and idempotency key |
389
+ | `signature: { _tag: 'hmac', … }` | a hand-declared scheme for a sender with its own convention |
390
+ | `verification: 'provider'` | your handler verifies with the provider's own SDK |
391
+ | `verification: 'none'` | deliberately public — a gateway or IP allow-list owns the boundary |
392
+
393
+ ```ts
394
+ export default defineIncomingWebhook({
395
+ id: 'internal.reindex',
396
+ verification: 'none', // behind the cluster gateway; nothing else may reach it
397
+ payload: Schema.Struct({ index: Schema.String }),
398
+ handler: async (ctx) => { /* … */ },
399
+ })
400
+ ```
401
+
402
+ `verification: 'none'` logs a warning at every boot, on purpose — an open
403
+ endpoint should stay visible.
404
+
405
+ **A declared signature with no configured secret answers 503**, on every
406
+ delivery, naming the variable to set. It does not skip the check: that fallback
407
+ meant one missing env var silently turned a verified webhook into an open one.
408
+ The framework never generates the secret — the sender holds the other half of it.
409
+
377
410
  ### Provider presets
378
411
 
379
412
  Built-in presets configure the signature scheme, idempotency extraction, and
@@ -425,6 +458,78 @@ export default defineIncomingWebhook({
425
458
  })
426
459
  ```
427
460
 
461
+ ## Signing — Standard Webhooks v1.0.0
462
+
463
+ Outbound deliveries are signed to the [Standard Webhooks](https://github.com/standard-webhooks/standard-webhooks) spec by default. That is the point of a spec: every conformant consumer library verifies your deliveries with no code specific to you.
464
+
465
+ ```http
466
+ POST /your/hook
467
+ webhook-id: 3f6a… ← the delivery id — USE THIS AS THE IDEMPOTENCY KEY
468
+ webhook-timestamp: 1786503000 ← unix seconds
469
+ webhook-signature: v1,K5oZfzN95Z…= ← base64 HMAC-SHA256, space-delimited during a rotation
470
+ x-voltro-event: orders.paid
471
+ x-voltro-attempt: 1
472
+ content-type: application/json
473
+ ```
474
+
475
+ The signed content is `<webhook-id>.<webhook-timestamp>.<raw body>`. The key is `whsec_` + base64 of 24–64 random bytes, and the HMAC runs over the **decoded** bytes — `subscribe()` mints the right shape automatically. Passing your own hex secret for a spec-signed target is REFUSED at subscribe rather than producing signatures no consumer accepts: a hex string is also valid base64, so a lenient decoder would key the HMAC on bytes that round-trip against itself and fail against everyone else.
476
+
477
+ Implemented: the symmetric `v1` scheme, multi-signature rotation, the `.`-delimiter constraint on the id (asserted, not assumed), a constant-time compare, a 300s replay tolerance (the spec requires a tolerance and names no number — this one is ours, and tunable). NOT implemented: the asymmetric half (ed25519 / `v1a` / `whsk_`). A `v1a`-only signature is rejected by name rather than reported as a generic mismatch.
478
+
479
+ Other schemes stay available as explicit choices — `genericHmacSignature()` (the previous house format, `X-Webhook-Signature: t=…,v1=<hex>`), `stripeSignature()`, `githubSignature()`, `slackSignature()`:
480
+
481
+ ```ts
482
+ subscribe({ event: 'orders.paid', url, signing: genericHmacSignature() })
483
+ ```
484
+
485
+ ### Delivery semantics the spec dictates
486
+
487
+ | response | what happens |
488
+ |---|---|
489
+ | `2xx` | success |
490
+ | `3xx` | **failure. The redirect is not followed** — the target URL is the one we validated, and following one walks past that check |
491
+ | `410 Gone` | the target is disabled **immediately**, whatever `autoDisableAfter` says. The receiver answered the question |
492
+ | `429` / `5xx` | retried with backoff; `Retry-After` is honoured |
493
+
494
+ Each attempt has a **30s wire timeout** (`VOLTRO_WEBHOOK_TIMEOUT_MS`, or `timeoutMs` on the delivery workflow) — the top of the spec's recommended 15–30s band.
495
+
496
+ ## `voltro webhooks consumer` — the package your subscribers install
497
+
498
+ Your app already knows every event a subscriber can register for, every payload's shape, and the exact scheme it signs with. So generate the verification package rather than making the receiving team write it:
499
+
500
+ ```
501
+ voltro webhooks consumer --out ../partner-sdk
502
+ voltro webhooks events --json # what a subscriber can register for
503
+ ```
504
+
505
+ It emits `index.js` + `index.d.ts` + `package.json` + a README, with **no runtime dependencies at all** — it imports `node:crypto` and nothing else. That constraint is the design, not an optimisation: the package runs in your subscriber's service, which is usually a different codebase and often not a Voltro app, and a dependency list is where "npm i and paste this in" stops being true.
506
+
507
+ ```js
508
+ import { createVerifier, WebhookVerificationError } from 'acme-webhooks'
509
+
510
+ const verifier = createVerifier({ secret: process.env.WEBHOOK_SECRET })
511
+
512
+ app.post('/webhooks/acme', (req, res) => {
513
+ let delivery
514
+ try {
515
+ delivery = verifier.verify(req.rawBody, req.headers) // ← RAW bytes
516
+ } catch (err) {
517
+ if (err instanceof WebhookVerificationError) return res.status(401).send(err.reason)
518
+ throw err
519
+ }
520
+ if (alreadyProcessed(delivery.id)) return res.sendStatus(200) // webhook-id
521
+ switch (delivery.event) { /* … */ }
522
+ res.sendStatus(200)
523
+ })
524
+ ```
525
+
526
+ Notes worth knowing before you hand it over:
527
+
528
+ - **The raw body is the whole trap.** The signature covers the exact bytes that arrived; a JSON body-parser re-serialises and the signature then never matches. The generated README carries the per-framework recipe (Express / Fastify / Next / Hono).
529
+ - **Node 18+**, deliberately. Web Crypto's HMAC is async, which would make `verify()` return a Promise and force every handler using it to be async too.
530
+ - **Payload types only.** They are generated from each event's Schema; no decoder ships. The types describe what you send — the signature is what proves you sent it.
531
+ - Pass an array as `secret` during a rotation; each is tried.
532
+
428
533
  ## Managing targets
429
534
 
430
535
  ```ts
@@ -19,7 +19,7 @@ The framework ships some plugins; you write your own; the contract is small enou
19
19
 
20
20
  - [The plugin contract](/docs/plugins/contract) — `definePlugin`, lifecycle hooks, rpc interceptors, framework-version compatibility
21
21
  - [plugin-audit](/docs/plugins/audit) — mutation audit log + `audit()` mixin
22
- - [plugin-auth](/docs/plugins/auth) — full auth suite: password (rehash-on-verify) + sessions (multi-key rotation + sliding-window) + magic-link/reset + passkeys (atomic clone detection, BYO multi-replica challenge store) + CSRF + session revocation + memberships/switch-tenant + TOTP/MFA (sign-in enforcement + recovery codes), mounted by `authRoutesPlugin()` (see also the [Authentication section](/docs/authentication/overview))
22
+ - [plugin-auth](/docs/plugins/auth) — full auth suite: password (rehash-on-verify) + sessions (multi-key rotation + sliding-window) + magic-link/reset + email verification + tenant invitations + user impersonation + passkeys (atomic clone detection, BYO multi-replica challenge store) + CSRF + session revocation + memberships/switch-tenant + TOTP/MFA (sign-in enforcement + recovery codes), mounted by `authRoutesPlugin()` (see also the [Authentication section](/docs/authentication/overview))
23
23
  - [plugin-multitenancy](/docs/plugins/multitenancy) — `tenant()` schema mixin + `assertOwnTenant` write-guard + `TenantMismatch`
24
24
  - [plugin-soft-delete](/docs/plugins/soft-delete) — `softDelete()` schema mixin (hide on delete, `hardDelete()` bypass)
25
25
  - [plugin-rbac](/docs/plugins/rbac) — roles + permissions + the `permission()` guard
@@ -46,7 +46,8 @@ The framework ships some plugins; you write your own; the contract is small enou
46
46
  - [plugin-governance](/docs/plugins/governance) — data governance: retention TTL sweep, GDPR export + erasure, consent ledger, field encryption
47
47
  - [plugin-openapi](/docs/plugins/openapi) — OpenAPI 3.1 spec + Swagger-UI docs generated from your `defineRestRoute` descriptors and (opt-in) rpc procedures
48
48
  - [plugin-versioning](/docs/plugins/versioning) — full row history + time-travel (`rowHistory` / `rowAsOf` / `restoreAsOf` / `diffVersions`); what-changed-to-what on every write
49
- - [plugin-presence](/docs/plugins/presence) — ephemeral realtime presence: heartbeat roster per channel + `usePresence` / `useTyping` hooks, cross-instance
49
+ - [plugin-presence](/docs/plugins/presence) — ephemeral realtime presence: heartbeat roster per channel + `usePresence` / `useTyping` hooks, held in memory; cross-instance with [plugin-broadcast](/docs/plugins/broadcast)
50
+ - [plugin-auth-social](/docs/plugins/auth-social) — first-party Sign in with Google / GitHub / Apple: mandatory PKCE + state, JWKS-verified ID tokens, a deliberate account-linking policy, sessions issued through plugin-auth
50
51
  - [plugin-scim](/docs/plugins/scim) — SCIM 2.0 provisioning (Users + Groups at `/scim/v2`) so an enterprise IdP can create/deactivate users
51
52
  - [plugin-sso-saml](/docs/plugins/sso-saml) — enterprise SAML 2.0 SSO: SP-initiated login + Single Logout, ACS, metadata (+ IdP-metadata-URL auto cert rotation, encrypted assertions, SP request signing); mints a framework session
52
53
  - [API keys](/docs/configuration/api-keys) — **first-class** (not a plugin): `apiKeys: true` enables Bearer-key auth + admin-gated issue/list/revoke
@@ -60,7 +61,7 @@ Status legend: ✓ shipped · ◐ partial · — planned.
60
61
  | Plugin | Status | What it does |
61
62
  |---|---|---|
62
63
  | `@voltro/plugin-audit` | ✓ | Mutation audit log + `audit()` mixin |
63
- | `@voltro/plugin-auth` | ✓ | Full auth suite via `authRoutesPlugin()`: password (rehash-on-verify), sessions (multi-key rotation + sliding-window), magic-link + password-reset, passkeys/WebAuthn (atomic clone detection, BYO multi-replica challenge store), CSRF, session enumeration + revocation, memberships + switch-tenant, TOTP/MFA (sign-in enforcement + recovery codes); `authTables` schemas |
64
+ | `@voltro/plugin-auth` | ✓ | Full auth suite via `authRoutesPlugin()`: password (rehash-on-verify), sessions (multi-key rotation + sliding-window), magic-link + password-reset, email verification (off/soft/strict policy), tenant invitations (addressed, single-use, role chosen by the inviter), user impersonation (marked, time-bounded, escalation-proof), passkeys/WebAuthn (atomic clone detection, BYO multi-replica challenge store), CSRF, session enumeration + revocation, memberships + switch-tenant, TOTP/MFA (sign-in enforcement + recovery codes); `authTables` schemas |
64
65
  | `@voltro/plugin-multitenancy` | ✓ | `tenant()` schema mixin (read-scope + write-fill) + `assertOwnTenant` guard + typed `TenantMismatch` |
65
66
  | `@voltro/plugin-soft-delete` | ✓ | `softDelete()` schema mixin — `deletedAt` / `deletedBy`; `delete` → UPDATE, `hardDelete()` bypass |
66
67
  | `@voltro/plugin-rbac` | ✓ | Roles compile to scopes + the `permission()` handler guard + typed `ScopeError` |
@@ -71,6 +72,7 @@ Status legend: ✓ shipped · ◐ partial · — planned.
71
72
  | `@voltro/plugin-postgis` | ✓ | Postgres-native `geography` / `geometry` columns + spatial predicates (`ST_DWithin`, `ST_Contains`, `ST_Intersects`); GiST indexes via `.expressionIndex(..., { kind: 'gist' })`. No `ST_Distance` projection yet. Postgres-only by design (fails loud elsewhere). [→ details](/docs/plugins/postgis) |
72
73
  | `@voltro/plugin-broadcast` | ✓ | Cross-replica reactivity — fans out app-mutation change events to every replica over a pub/sub bus (Redis / NATS). Closes the single-instance gap for every non-postgres dialect. [→ details](/docs/plugins/broadcast) |
73
74
  | `@voltro/plugin-webhooks` | ✓ | Incoming + outgoing webhooks — `defineIncomingWebhook` (signature verify + idempotency, Stripe/GitHub/Slack presets) and `defineEvent` (durable delivery workflow, HMAC signing, retries, filters). [→ details](/docs/plugins/webhooks) |
75
+ | `@voltro/plugin-auth-social` | ✓ | First-party social login — Sign in with Google / GitHub / Apple with no identity vendor: authorize URL + code exchange + JWKS-verified ID tokens, mandatory PKCE (S256) and `state`, an explicit account-linking policy (`never` by default), Apple's signed-JWT client secret / one-time name / private-relay email all handled; sessions via `issueUserSession`. [→ details](/docs/plugins/auth-social) |
74
76
  | `@voltro/plugin-auth-{workos,kinde,clerk,auth0,supabase,oidc}` | ✓ | Six IdP adapters over the shared `jwtBearerStrategy` — JWKS verify + claims→tenant mapping; WorkOS additionally ships hosted-login OAuth primitives (`workosAuthorizationUrl` / `workosAuthenticateWithCode`) for a redirect-based SSO login flow. [→ details](/docs/authentication/external-idp) |
75
77
  | `@voltro/plugin-analytics-postgres` | ✓ | First-party lite — events on the main DataStore, cross-dialect (postgres / mysql / mariadb / mssql / sqlite / turso). [→ details](/docs/plugins/analytics#voltroplugin-analytics-postgres) |
76
78
  | `@voltro/plugin-duckdb` | ✓ | Embedded DuckDB sidecar — real OLAP performance, no external service. [→ details](/docs/plugins/analytics#voltroplugin-duckdb) |
@@ -91,7 +93,7 @@ Status legend: ✓ shipped · ◐ partial · — planned.
91
93
  | `@voltro/plugin-governance` | ✓ | Data governance — retention TTL sweep (delete / anonymise), GDPR subject export + erasure (admin-gated routes + `GovernanceService`), consent ledger, field encryption. [→ details](/docs/plugins/governance) |
92
94
  | `@voltro/plugin-openapi` | ✓ | OpenAPI 3.1 spec (`GET /openapi.json`) + Swagger-UI (`GET /docs`) generated from `defineRestRoute` descriptors AND (opt-in) rpc procedures (queries/mutations/actions/streams → `POST /rpc/<name>`) — input/output/error Schemas via `JSONSchema.make`. [→ details](/docs/plugins/openapi) |
93
95
  | `@voltro/plugin-versioning` | ✓ | Full row history + time-travel — value snapshot of every insert/update/delete on listed tables into `_voltro_row_history` (rides the ChangeEvent tap); `rowHistory` / `rowAsOf` queries + `restoreAsOf` / `diffVersions`; TTL + per-row cap retention. [→ details](/docs/plugins/versioning) |
94
- | `@voltro/plugin-presence` | ✓ | Ephemeral realtime presence — heartbeat roster per channel (`presence.heartbeat`/`list`/`leave` + `usePresence`), a `useTyping` typing indicator, swept `_voltro_presence` table, cross-instance. [→ details](/docs/plugins/presence) |
96
+ | `@voltro/plugin-presence` | ✓ | Ephemeral realtime presence — heartbeat roster per channel (`presence.heartbeat`/`list`/`leave` + `usePresence`), a `useTyping` typing indicator. Held **in memory**, owner-partitioned — no table is written; cross-instance requires [`@voltro/plugin-broadcast`](/docs/plugins/broadcast), and without a broker each replica sees only its own clients. [→ details](/docs/plugins/presence) |
95
97
  | `@voltro/plugin-scim` | ✓ | SCIM 2.0 provisioning — Users + Groups REST at `/scim/v2` (bearer-gated) incl. group-membership PATCH/PUT + the RFC 7644 discovery trio (ServiceProviderConfig/Schemas/ResourceTypes), `userName`/`externalId`/`displayName eq` filters, pagination, unique `userName`, `active:false` deactivation; `_voltro_scim_users`/`_voltro_scim_groups`. [→ details](/docs/plugins/scim) |
96
98
  | `@voltro/plugin-sso-saml` | ✓ | Enterprise SAML 2.0 SSO — SP-initiated login + Single Logout (both directions) + ACS + SP metadata under `/saml`; IdP-metadata-URL auto cert rotation, encrypted assertions, clock-skew, SP request signing. Signature verify via `@node-saml/node-saml` (optional+lazy), mints a framework session. [→ details](/docs/plugins/sso-saml) |
97
99
 
@@ -126,7 +128,7 @@ Order matters: the framework composes outer→inner, so the rate-limit intercept
126
128
  | `extendSchema` | Contribute tables + custom SQL migrations (tracked in `_voltro_plugin_migrations`). |
127
129
  | `services` | Provide an Effect `Layer` whose Tags every handler can `yield*` (e.g. `MailService`, `StorageService`). |
128
130
  | `routes` | Register plugin-owned rpc queries / mutations / actions (alias-prefixed tags). |
129
- | `httpRoutes` | Serve public raw-HTTP endpoints on the framework listener (e.g. `GET /_voltro/storage/:id`). The request carries `store` — the app's DataStore — for a route that must read or write (a login endpoint minting a session row cannot be an rpc mutation). Not tenant-scoped: raw HTTP has no resolved Subject, so scope it yourself. |
131
+ | `httpRoutes` | Serve public raw-HTTP endpoints on the framework listener (e.g. `GET /_voltro/storage/:id`). The request carries `store` — the app's DataStore — for a route that must read or write (a login endpoint minting a session row cannot be an rpc mutation), and `remoteAddr`, the client address already resolved through `security.trustedProxies` (use it instead of `x-forwarded-for`). Not tenant-scoped: raw HTTP has no resolved Subject, so scope it yourself. A state-changing route is [origin-checked](/docs/security/overview#cross-site-requests-are-refused) unless it declares `originGuard: 'exempt'`. |
130
132
  | `inspectEndpoints` | Mount tooling under `/_voltro/inspect/plugins/<alias>/…`. |
131
133
  | `onScheduleFire` / `onWorkflowStep` / `onHttpRequest` | Wrap every cron firing, every workflow `step()`, every pre-auth HTTP request. |
132
134
  | `onInstall` / `onActivate` / `onDeactivate` / `onUninstall` | Lifecycle hooks at first-install, boot, shutdown, and removal. |
@@ -207,10 +209,15 @@ To REPLACE one deliberately, declare it:
207
209
 
208
210
  ```ts
209
211
  // Adopt the plugin's namespace, add your own leaves beside it…
210
- defineQuery({ name: 'notifications.archive', … })
211
-
212
- // …and REPLACE just the one you need to behave differently.
213
- defineMutation({ name: 'notifications.markRead', overridesPlugin: true, })
212
+ defineQuery({ name: 'notifications.archive', guards: [{ scope: 'notifications:read' }], … })
213
+
214
+ // …and REPLACE just the one you need to behave differently. Your replacement is
215
+ // YOUR procedure, so it needs its own access decision — the plugin's does not
216
+ // carry over with the tag.
217
+ defineMutation({
218
+ name: 'notifications.markRead', overridesPlugin: true,
219
+ guards: [{ scope: 'notifications:write' }], …
220
+ })
214
221
  ```
215
222
 
216
223
  The plugin's route is dropped, not merely permitted alongside yours — permitting
@@ -345,6 +352,8 @@ onChangeEvent: (event: PluginChangeEvent) => Effect.Effect<void, unknown>
345
352
  // PluginChangeEvent = {
346
353
  // table, op: 'insert'|'update'|'delete', new: Row | null, old: Row | null,
347
354
  // origin?: 'inline' | 'injected', changeScope: 'local' | 'fleet',
355
+ // traceId?, subjectId?, procedure?,
356
+ // oversized?: 'rehydrated' | 'tombstone' | 'unrecovered',
348
357
  // }
349
358
  ```
350
359
 
@@ -359,6 +368,8 @@ onChangeEvent: (event) =>
359
368
  )
360
369
  ```
361
370
 
371
+ **Check `event.oversized` before you treat an image as a snapshot.** It is absent on an ordinary event, and set when the transport could not carry the row and the images were reconstructed — a row over postgres' 8000-byte NOTIFY cap. `'rehydrated'` means `new` is the row re-read from the database (correct to index or forward, but the row as it is NOW rather than the image at commit); `'tombstone'` means a delete whose `old` is the primary key and nothing else (enough to REMOVE the row, never a record of what it held); `'unrecovered'` means both images are null and the content is gone. A tap that stores history must not write a tombstone as a snapshot. Full guarantee: [postgres — oversized rows](/docs/database/dialects/postgres).
372
+
362
373
  Runs under BOTH `voltro dev` and `voltro serve` (the prod serve path fans out the same way). Requires the `'store:changes:read'` permission. It is NOT durable at the framework layer — a crash between commit and the fork loses the event; build durability INSIDE the Effect (insert into an outbox and retry against the typed error channel, the way `@voltro/plugin-cdc-out` does). Exactly-once / change-scope semantics are unchanged: read `event.origin` + `event.changeScope` inside the Effect to act once per change fleet-wide (skip `origin: 'injected'` on `'local'` scope; elect one worker on `'fleet'`). Used by `@voltro/plugin-search` to mirror rows into an external index. For in-transaction reactions use a mutation; for best-effort per-table reactions in app code prefer a `*.subscribe.ts` — `onChangeEvent` is the plugin-level equivalent.
363
374
 
364
375
  ### `onInstall` / `onUninstall` / `onActivate` / `onDeactivate` lifecycle
@@ -523,9 +534,8 @@ export const auditPlugin = (): VoltroPlugin =>
523
534
  })
524
535
  ```
525
536
 
526
- Tag derivation: `<plugin-alias>.<query.name>` unless `query.name` contains
527
- a dot (escape hatch). Plugin alias strips `@scope/` + the `plugin-` prefix
528
- and kebab→camelCase:
537
+ Tag derivation: `<plugin-alias>.<query.name>`. The plugin alias strips
538
+ `@scope/` + the `plugin-` prefix and kebab→camelCase:
529
539
 
530
540
  | Plugin name | Alias |
531
541
  |---|---|
@@ -535,6 +545,71 @@ and kebab→camelCase:
535
545
  | `plain-name` | `plainName` |
536
546
  | `@voltro/audit` | `audit` (no `plugin-` to strip) |
537
547
 
548
+ A `query.name` that already contains a dot is handled by whether it names the
549
+ plugin's OWN namespace:
550
+
551
+ - `'notifications.inbox'` on a plugin whose canonical name is
552
+ `@voltro/plugin-notifications` is re-namespaced — under
553
+ `alias: 'inbox'` it becomes `inbox.inbox`, not `notifications.inbox`.
554
+ A deeper path keeps its depth: `'audit.admin.events'` under `alias: 'trail'`
555
+ becomes `trail.admin.events`.
556
+ - `'acme.legacyBridge'` — a namespace that is not the plugin's own — passes
557
+ through untouched. That is the escape hatch, and it is the only case that
558
+ still bypasses the alias.
559
+
560
+ The re-namespacing needs the plugin to declare `baseName` (its canonical name,
561
+ before any app-supplied `alias`); a plugin that omits it keeps the older
562
+ behaviour where any dotted name passes through.
563
+
564
+ ### Naming a plugin: `alias` vs `name`
565
+
566
+ Two different app-side problems land on a plugin's name, so first-party plugins
567
+ that carry tables or routes accept two separate options:
568
+
569
+ | Option | Question it answers | Effect |
570
+ |---|---|---|
571
+ | `alias` | "your namespace collides with mine" | replaces the namespace — tags become `<alias>.<route>`, the inspect mount becomes `/_voltro/inspect/plugins/<alias>/…` |
572
+ | `name` | "I want two of these" | appends a `#suffix` discriminator so two installs never register the same tag |
573
+
574
+ ```ts
575
+ notificationsPlugin({ alias: 'alerts' }) // alerts.inbox
576
+ notificationsPlugin({ name: 'ops' }) // notifications#ops.inbox
577
+ notificationsPlugin({ alias: 'alerts', name: 'ops' }) // alerts#ops.inbox
578
+ ```
579
+
580
+ `alias` exists to escape a tag collision, which is fatal at codegen. Two costs
581
+ are worth knowing before you reach for it:
582
+
583
+ - the local and cloud dashboards fetch a plugin's inspect panel at its DEFAULT
584
+ slug, so an aliased plugin keeps serving its inspect endpoints while its
585
+ dashboard panel stops resolving;
586
+ - the plugin-migration ledger key is `<plugin-alias>__<migration.id>`, so
587
+ aliasing a plugin that ships `extendSchema.migrations` makes its already-applied
588
+ migrations look unapplied. Choose the alias before first boot, not after.
589
+
590
+ ### `tables: false` — keeping your own tables
591
+
592
+ Plugins whose tables carry no authorization or safety decision accept
593
+ `tables: false`, which stops them contributing DDL through `extendSchema` so an
594
+ app can keep equivalent tables it already has. Everything else — routes,
595
+ inspect, interceptors — is unchanged.
596
+
597
+ ```ts
598
+ notificationsPlugin({ tables: false }) // you declare the six notification tables
599
+ ```
600
+
601
+ It is offered on `@voltro/plugin-rbac`, `@voltro/plugin-audit`,
602
+ `@voltro/plugin-notifications` and `@voltro/plugin-ai-flows`. The plugin still
603
+ writes to those tables BY NAME, so you take over declaring each one with the
604
+ shape the package exports, and a missing or mis-shaped table fails at the first
605
+ write rather than at boot.
606
+
607
+ It is deliberately NOT offered on plugins whose tables carry a guarantee — the
608
+ SAML assertion replay cache, SCIM provisioning state, billing's usage counters,
609
+ cdc-out's delivery outbox, the governance consent ledger, search's tenant-scoped
610
+ index rows. A `tables: false` there would disable a security or correctness
611
+ decision with no signal to the app that it now owns it.
612
+
538
613
  Boot fails with a clear error on tag collisions (between two plugins, or
539
614
  with a user-authored tag).
540
615
 
@@ -770,7 +845,7 @@ markers. The plugin sees the api name + the list of discovered user-query
770
845
  rpc tags so it can emit per-rpc bindings. Returning `null` contributes
771
846
  nothing.
772
847
 
773
- ### `templates: PluginTemplate[]` — ship `voltro init` templates
848
+ ### `templates: PluginTemplate[]` — ship scaffolding templates
774
849
 
775
850
  ```ts
776
851
  definePlugin({
@@ -819,8 +894,14 @@ export const rateLimitPlugin = (opts: { perMinute: number }) =>
819
894
  name: '@vendor/plugin-rate-limit',
820
895
  permissions: ['http:intercept'],
821
896
  onHttpRequest: async (next, ctx) => {
822
- const remoteAddr = ctx.headers['x-forwarded-for'] ?? ctx.remoteAddr ?? 'unknown'
823
- if (buckets.consume(remoteAddr, opts.perMinute) === 'exhausted') {
897
+ // `ctx.remoteAddr` is ALREADY resolved through the app's
898
+ // `security.trustedProxies` policy the same value the framework's own
899
+ // rate limiter, geo-block and audit rows use. Never read
900
+ // `ctx.headers['x-forwarded-for']`: it is a request header, so any
901
+ // caller can write it, and a limiter keyed on it is bypassed by one
902
+ // extra header. `undefined` only when the socket address is unavailable.
903
+ const clientAddr = ctx.remoteAddr ?? 'unknown'
904
+ if (buckets.consume(clientAddr, opts.perMinute) === 'exhausted') {
824
905
  return {
825
906
  status: 429,
826
907
  headers: { 'retry-after': '60', 'content-type': 'application/json' },
@@ -832,10 +913,28 @@ export const rateLimitPlugin = (opts: { perMinute: number }) =>
832
913
  })
833
914
  ```
834
915
 
916
+ **`ctx.remoteAddr` is the client address — `x-forwarded-for` is not.**
917
+ The framework resolves `remoteAddr` through the app's
918
+ `security.trustedProxies` policy (`VOLTRO_TRUSTED_PROXIES`) before it hands you
919
+ the context: with no trusted proxy declared the header is ignored entirely and
920
+ the socket address wins, and with one declared only the hops that are actually
921
+ a configured proxy are believed. Reading `ctx.headers['x-forwarded-for']`
922
+ yourself throws that away and keys your limiter on a string the caller typed —
923
+ one extra header and every request looks like a new client. The same rule
924
+ applies to a plugin's raw HTTP routes, where the resolved value arrives as
925
+ `req.remoteAddr`; see [Trusted proxies](/docs/security/overview#the-same-address-reaches-your-plugin-routes).
926
+
835
927
  A per-plugin `plugin.<name>.http-intercept` metric is auto-emitted so
836
928
  the dashboard's Plugins panel surfaces HTTP-intercept latency next to
837
929
  RPC-intercept latency.
838
930
 
931
+ **Runs on both boot paths.** The chain is composed and installed identically by
932
+ `voltro dev` and `voltro serve` — this is a production capability, and for a
933
+ pre-auth shield production is the point. There is exactly ONE exemption, and it
934
+ is deliberate: `GET /internal/liveness` and `GET /internal/readiness` are
935
+ answered before the interceptor, so a rate-limit or geo-block plugin cannot 503
936
+ a Kubernetes probe and take the replica out of rotation.
937
+
839
938
  ### `extendSchema: { tables, migrations }` — contribute schema + migrations
840
939
 
841
940
  A plugin contributes BOTH declarative table descriptors AND custom SQL
@@ -848,6 +947,14 @@ The ledger key is `<plugin-alias>__<migration.id>` so two plugins can
848
947
  each ship `'001-init'` without collision. Failure aborts boot;
849
948
  re-runs are no-ops.
850
949
 
950
+ **Which commands run them:** `voltro dev`'s boot auto-migrate, `voltro db apply`
951
+ (bare and `--plan`) and `voltro migrate --create-only`. NOT `voltro serve` —
952
+ serve never applies a schema, so a plugin's steps land in the pre-deploy job
953
+ alongside the schema, which is where they belong. Until 0.34.0 only the `voltro
954
+ dev` boot ran them, so a plugin's SQL steps executed on every developer machine
955
+ and on no deployed database; if you ship migrations, verify against a deployed
956
+ database rather than a dev boot.
957
+
851
958
  ```ts
852
959
  import { Effect, Schema } from 'effect'
853
960
  import { definePlugin } from '@voltro/protocol'
@@ -1387,17 +1494,44 @@ analytics: duckdbAnalytics({
1387
1494
  }),
1388
1495
  ```
1389
1496
 
1390
- The postgres-lite (`postgresAnalytics({ mirrorTables: [...] })`) and ClickHouse (`clickhouseAnalytics({ url, mirrorTables: [...] })`) sinks take the same options. Each mirrored table lands as `_voltro_mirror_<table>` (`voltro_mirror_<table>` on DuckDB / ClickHouse) holding `{ id, data }` — `id` is the source row's primary key, `data` is the full row as JSON. Analytical queries JOIN events against the mirror:
1497
+ The postgres-lite (`postgresAnalytics({ mirrorTables: [...] })`) and ClickHouse (`clickhouseAnalytics({ url, mirrorTables: [...] })`) sinks take the same options. Each mirrored table lands as `_voltro_mirror_<table>` (`voltro_mirror_<table>` on DuckDB / ClickHouse) holding `{ id, data, version, is_deleted }` — `id` is the source row's primary key, `data` is the full row as JSON, `version` orders the writes (see below) and `is_deleted` marks a tombstone. Analytical queries JOIN events against the mirror and filter tombstones out:
1391
1498
 
1392
1499
  ```sql
1393
1500
  -- DuckDB: events per user tier
1394
1501
  SELECT json_extract_string(m.data, '$.tier'), COUNT(*)
1395
1502
  FROM voltro_events e
1396
- JOIN voltro_mirror_users m ON m.id = e.subject_id
1503
+ JOIN voltro_mirror_users m ON m.id = e.subject_id AND m.is_deleted = false
1397
1504
  GROUP BY 1
1398
1505
  ```
1399
1506
 
1400
- The mirror is **opt-in** (omit `mirrorTables` → events-only) and **idempotent** — inserts/updates upsert by primary key, deletes remove by key, so a re-delivered change (e.g. after a reconnect) is a no-op-equivalent overwrite. Per-change failures are isolated into the log channel: the OLTP write that produced the change already committed, so a warehouse hiccup never surfaces to the request. On DuckDB the mirror table is a plain `(id, data)` table; on ClickHouse it's a `ReplacingMergeTree(version)` so re-inserts collapse to the latest version on merge (deletes write a `is_deleted = 1` tombstone — filter `is_deleted = 0` or use `FINAL`).
1507
+ The mirror is **opt-in** (omit `mirrorTables` → events-only) and **idempotent** — inserts/updates upsert by primary key, deletes write a tombstone by key, so a re-delivered change (e.g. after a reconnect) is a no-op-equivalent overwrite.
1508
+
1509
+ ### Delivery guarantee
1510
+
1511
+ **At-least-once for the lifetime of the process, ordered per row.** Read that sentence literally — every clause is a promise the framework keeps, and the sentence stops where the implementation does:
1512
+
1513
+ - **Retried, not dropped.** A failing mirror write is retried with exponential backoff (`retryAttempts`, default 5). A write that outlives its retries is queued for **repair**: a timer re-reads the row's *current* state from your database and re-applies it, so the mirror converges on the truth rather than on a stale change that happened to be in flight.
1514
+ - **Ordered per row.** Writes for the same primary key are applied one at a time, in commit order, and every write carries a **version** stamped when the change left the store — never a clock read inside the warehouse client. A change that arrives late therefore *loses*: ClickHouse's `ReplacingMergeTree(version)` keeps the highest version, and the DuckDB / postgres mirrors apply the update only when the incoming version is newer. Different rows are still mirrored concurrently.
1515
+ - **A delete is a tombstone, not a row removal.** Filter `is_deleted = false` (`is_deleted = 0` / `FINAL` on ClickHouse). A physical delete would leave nothing for a late, stale insert of the same key to lose against — the row would silently come back. The tombstone also keeps its version, which is what lets a replacement replica seed a key's numbering after a delete.
1516
+ - **N replicas converge to ONE row per change.** Under postgres CDC / mysql binlog every replica observes every change and mirrors it, so a change is written N times — but the version is derived from the CHANGE (the warehouse's own high-water mark for the key, plus the change's position in the fleet stream), never from a replica's clock. The N duplicates are byte-identical, and the sink's version guard collapses them for free: no leader election, and no clock-skew window in which an older image could out-version a newer one. A replica that boots mid-stream reads each key's high-water mark from the warehouse before its first write, so it continues the fleet's numbering instead of restarting it.
1517
+ - **Never surfaces to the request.** The OLTP write that produced the change has already committed; a warehouse outage is isolated into the log channel and the metrics below.
1518
+ - **A graceful shutdown is not a crash.** On SIGINT / SIGTERM the mirror stops taking new changes and then **settles** what is already queued and in flight, before the sink itself is disposed. That drain is bounded (3 s of the teardown budget you set with `VOLTRO_SHUTDOWN_GRACE_MS`, default 10 s): a warehouse that has stopped answering cannot hold the process open until the orchestrator's SIGKILL, which would lose strictly more. The two outcomes log differently — `analytics mirror drained` at info, or a `warn` naming what was still pending when the deadline cut it, because that is the moment those counters can still be read.
1519
+ - **Not durable across a crash.** The repair queue lives in memory. A change still awaiting repair when the process dies — SIGKILL, an OOM, a host failure — is lost, as is one evicted after `repairQueueLimit`. Both are logged at error level and counted by `voltro_analytics_mirror_dropped_total` — if that counter is non-zero, the affected tables need a re-seed.
1520
+
1521
+ Metrics: `voltro_analytics_mirror_forwarded_total`, `..._retries_total`, `..._repair_queued_total`, `..._dropped_total`.
1522
+
1523
+ ### Tuning
1524
+
1525
+ Every number the mirror picks on your behalf has a default and an environment override:
1526
+
1527
+ | Env var | Default | Meaning |
1528
+ |---|---|---|
1529
+ | `VOLTRO_ANALYTICS_MIRROR_RETRY_ATTEMPTS` | `5` | Total attempts per mirror write (`1` = no retry). |
1530
+ | `VOLTRO_ANALYTICS_MIRROR_RETRY_BASE_MS` | `100` | First backoff delay; doubles per attempt. |
1531
+ | `VOLTRO_ANALYTICS_MIRROR_RETRY_MAX_MS` | `30000` | Ceiling for the doubling backoff. |
1532
+ | `VOLTRO_ANALYTICS_MIRROR_REPAIR_INTERVAL_MS` | `60000` | How often the repair loop re-drives exhausted changes (`0` disables it). |
1533
+ | `VOLTRO_ANALYTICS_MIRROR_REPAIR_QUEUE_LIMIT` | `10000` | Maximum keys held for repair before the oldest is dropped and counted. |
1534
+ | `VOLTRO_ANALYTICS_MIRROR_VERSION_STATE_LIMIT` | `100000` | Maximum keys whose fleet-scope version state stays in memory; an evicted key re-seeds from the warehouse on its next change. |
1401
1535
 
1402
1536
  ## Default — no sink configured
1403
1537