@voltro/cli 0.34.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 (137) hide show
  1. package/CHANGELOG.md +167 -0
  2. package/dist/apiBuild-B8aoJvuw.js +2 -0
  3. package/dist/{apiBuild-DZF_f0_I.js → apiBuild-DYD_ONLD.js} +2 -2
  4. package/dist/{appGraph-gQ_6GkQQ.js → appGraph-KGDPTuTy.js} +1 -1
  5. package/dist/appGraph-zuMGKVYX.js +2 -0
  6. package/dist/bin.js +1 -1
  7. package/dist/{build-CE7Ca9gm.js → build-CD8K4XOr.js} +7 -7
  8. package/dist/{checkCommand-Dv8r9tGO.js → checkCommand-Ct9xkTrS.js} +8 -7
  9. package/dist/{checkCommand-YZDmkAGd.js → checkCommand-DKpDLlqu.js} +1 -1
  10. package/dist/{clusterCmd-CNXKlcvD.js → clusterCmd-D5wsCmA_.js} +1 -1
  11. package/dist/{codegenCommand-DC6w2tNZ.js → codegenCommand-C4YoQIc2.js} +3 -3
  12. package/dist/{codemodRunner-DRRqXR74.js → codemodRunner-BnFq3Fgu.js} +175 -34
  13. package/dist/{commands-gutsz-Ac.js → commands-BE8E7zF3.js} +37 -29
  14. package/dist/{dashboardCommand-3YG8p-UA.js → dashboardCommand-D7SgZGaN.js} +3 -3
  15. package/dist/{dataCommand-qL0r7fPO.js → dataCommand-BhYwDgg-.js} +56 -54
  16. package/dist/dbCommand-DS4b97Is.js +2 -0
  17. package/dist/{dbCommand-B6X0FZbq.js → dbCommand-O8HA63s2.js} +6 -6
  18. package/dist/{dev-CerMd0mW.js → dev-C7sFZq3m.js} +2696 -2188
  19. package/dist/{dev-CoG-ZPx8.js → dev-D2BikO7a.js} +1 -1
  20. package/dist/doctorCommand-CM4Ch9C7.js +2 -0
  21. package/dist/{doctorCommand-CnDXQxfa.js → doctorCommand-DnimF5IM.js} +76 -73
  22. package/dist/{dormancyCommand-Dszo57d6.js → dormancyCommand-QewYug_s.js} +1 -1
  23. package/dist/{embeddingsCommand-C0sKVRo1.js → embeddingsCommand-BfiLS_QI.js} +1 -1
  24. package/dist/{envCommand-DPgdV1Bq.js → envCommand-CCGPRQY1.js} +2 -2
  25. package/dist/{evolveCommand-DHpkgjgH.js → evolveCommand-CHsLCtDf.js} +2 -2
  26. package/dist/frameworkTableAssembly-YyVe32Cb.js +2 -0
  27. package/dist/frameworkTableAssembly-oMPBKqlE.js +511 -0
  28. package/dist/{generateCommand-oibemh97.js → generateCommand-DbgcUpGw.js} +1 -1
  29. package/dist/index.js +1 -1
  30. package/dist/{infoCommand-BJw9nLUR.js → infoCommand-DwOgK1t6.js} +1 -1
  31. package/dist/{inspect-CBqFtAKA.js → inspect-CjTYzAs_.js} +32 -2
  32. package/dist/inspect-P4pxoMaV.js +2 -0
  33. package/dist/{inspectCmd-Bppy-GGw.js → inspectCmd-EHFZ9yYu.js} +1 -1
  34. package/dist/{inspectFetch-Cm8_wVvp.js → inspectFetch-EMuhTG_9.js} +1 -1
  35. package/dist/{logsCmd-CCca3KRZ.js → logsCmd-D36xK7Zu.js} +1 -1
  36. package/dist/manifestBuild-COkJoyAr.js +2 -0
  37. package/dist/{manifestBuild-sxpwdKY1.js → manifestBuild-hpPLaGxV.js} +1 -1
  38. package/dist/{migrate-CBwOt_iV.js → migrate-BV7I-ZHZ.js} +3 -3
  39. package/dist/probeCommand-CZfaaUOZ.js +122 -0
  40. package/dist/{runtimeTrace-CRxalXTs.js → runtimeTrace-CH3eUiMw.js} +1 -1
  41. package/dist/scheduleCmd-DQRu6BZC.js +149 -0
  42. package/dist/{sdkgen-CYJscZC7.js → sdkgen-BLkvGRfX.js} +1 -1
  43. package/dist/{seedRunner-TFHHiToI.js → seedRunner-ZmLSqNe2.js} +74 -70
  44. package/dist/serveCommand-CbDHU6l-.js +2 -0
  45. package/dist/serveCommand-iwlUBNS1.js +1766 -0
  46. package/dist/serveEntry.js +1 -1
  47. package/dist/{start-B-9Nsp-S.js → start-BgN62boB.js} +1 -1
  48. package/dist/{start-9LiUOfES.js → start-T4VesWiM.js} +2 -2
  49. package/dist/startEntry.js +1 -1
  50. package/dist/{tracesCmd-CkEZQrtt.js → tracesCmd-DStmCJPi.js} +1 -1
  51. package/dist/updateCommand-BlMZhWgO.js +2 -0
  52. package/dist/{updateCommand-Bkptutss.js → updateCommand-x0pI_x-B.js} +1 -1
  53. package/dist/webDev-BcykISYQ2.js +2 -0
  54. package/dist/{webDev-CBYvPqQr.js → webDev-Dybxew86.js} +14 -2
  55. package/dist/{webhooksCommand-CID96Rga.js → webhooksCommand-DlAgS2Iw.js} +1 -1
  56. package/dist/{workflowsCmd-D1VTmLMY.js → workflowsCmd-BGF-mRZ5.js} +1 -1
  57. package/package.json +47 -17
  58. package/templates/AGENTS.md +1 -1
  59. package/templates/agent-docs/_index.md +1 -1
  60. package/templates/agent-docs/_manifest.json +1 -1
  61. package/templates/agent-docs/authentication.md +55 -7
  62. package/templates/agent-docs/cli.md +59 -0
  63. package/templates/agent-docs/data.md +59 -2
  64. package/templates/agent-docs/database/migrations.md +50 -1
  65. package/templates/agent-docs/deployment.md +214 -21
  66. package/templates/agent-docs/introduction.md +1 -1
  67. package/templates/agent-docs/multi-tenancy.md +93 -22
  68. package/templates/agent-docs/plugins/ai-flows.md +86 -0
  69. package/templates/agent-docs/plugins/billing.md +1 -1
  70. package/templates/agent-docs/plugins/clickhouse.md +31 -0
  71. package/templates/agent-docs/plugins/search.md +6 -3
  72. package/templates/agent-docs/plugins/storage.md +11 -0
  73. package/templates/agent-docs/plugins.md +3 -1
  74. package/templates/agent-docs/scheduling.md +17 -0
  75. package/templates/agent-docs/security.md +2 -2
  76. package/templates/agent-docs/whats-new.md +95 -1729
  77. package/templates/agent-docs/workflows.md +101 -0
  78. package/templates/apps/api-ai/package.json +7 -7
  79. package/templates/apps/api-auth/package.json +8 -8
  80. package/templates/apps/api-backend/package.json +7 -7
  81. package/templates/apps/api-backend-deactivation/package.json +7 -7
  82. package/templates/apps/api-backend-mail/package.json +8 -8
  83. package/templates/apps/api-backend-mariadb/package.json +9 -9
  84. package/templates/apps/api-backend-sqlite/package.json +8 -8
  85. package/templates/apps/api-backend-storage/package.json +8 -8
  86. package/templates/apps/api-cms/package.json +10 -10
  87. package/templates/apps/api-collab/package.json +8 -8
  88. package/templates/apps/api-data-advanced/package.json +8 -8
  89. package/templates/apps/api-durable/package.json +8 -8
  90. package/templates/apps/api-feature-flags/package.json +9 -9
  91. package/templates/apps/api-governance/package.json +8 -8
  92. package/templates/apps/api-kv/package.json +8 -8
  93. package/templates/apps/api-moderation/package.json +8 -8
  94. package/templates/apps/api-observability/package.json +8 -8
  95. package/templates/apps/api-ratelimit/package.json +8 -8
  96. package/templates/apps/api-rbac/package.json +8 -8
  97. package/templates/apps/api-rest/package.json +7 -7
  98. package/templates/apps/api-saas/package.json +11 -11
  99. package/templates/apps/api-saas-starter/package.json +10 -10
  100. package/templates/apps/api-search/package.json +8 -8
  101. package/templates/apps/api-status/package.json +8 -8
  102. package/templates/apps/api-versioning/package.json +8 -8
  103. package/templates/apps/api-webhooks/package.json +9 -9
  104. package/templates/apps/changelog/package.json +6 -6
  105. package/templates/apps/edge-functions/package.json +2 -2
  106. package/templates/apps/frontend-admin/package.json +8 -8
  107. package/templates/apps/frontend-app/package.json +9 -9
  108. package/templates/apps/frontend-auth/package.json +8 -8
  109. package/templates/apps/frontend-blank/package.json +7 -7
  110. package/templates/apps/frontend-cms/package.json +9 -9
  111. package/templates/apps/frontend-collab/package.json +10 -10
  112. package/templates/apps/frontend-contact/package.json +7 -7
  113. package/templates/apps/frontend-dashboard/package.json +7 -7
  114. package/templates/apps/frontend-docs/package.json +7 -7
  115. package/templates/apps/frontend-i18n/package.json +6 -6
  116. package/templates/apps/frontend-landing/package.json +7 -7
  117. package/templates/apps/frontend-portal/package.json +8 -8
  118. package/templates/apps/frontend-saas/package.json +8 -8
  119. package/templates/apps/frontend-spa/package.json +7 -7
  120. package/templates/apps/frontend-ssr/package.json +7 -7
  121. package/templates/apps/frontend-ssr-api/package.json +8 -8
  122. package/templates/apps/frontend-static-blog/package.json +6 -6
  123. package/templates/apps/frontend-status/package.json +8 -8
  124. package/templates/apps/mobile-app/package.json +3 -3
  125. package/dist/apiBuild-BICVGnEC.js +0 -2
  126. package/dist/appGraph-CvQCte0z.js +0 -2
  127. package/dist/dbCommand-CpdKLeQq.js +0 -2
  128. package/dist/doctorCommand-BvqGBwNG.js +0 -2
  129. package/dist/frameworkTableAssembly-BGHmck-x.js +0 -2
  130. package/dist/frameworkTableAssembly-DkkP6BgC.js +0 -674
  131. package/dist/inspect-C_T_WGvl.js +0 -2
  132. package/dist/manifestBuild-ChsKAhmn.js +0 -2
  133. package/dist/scheduleCmd--jksTrf6.js +0 -69
  134. package/dist/serveCommand-B_isw7q4.js +0 -1647
  135. package/dist/serveCommand-DOvbgRnQ.js +0 -2
  136. package/dist/updateCommand-us1_hdIC.js +0 -2
  137. package/dist/webDev-Cg-fFiyd2.js +0 -2
@@ -1,5 +1,5 @@
1
1
  import { t as e } from "./loadEnv-D9nEOClM.js";
2
2
  import { r as t } from "./appModuleLoader-C9r9mxZt.js";
3
3
  import { i as n } from "./dialectDriver-CgXnDfec.js";
4
- import { t as r } from "./serveCommand-B_isw7q4.js";
4
+ import { t as r } from "./serveCommand-iwlUBNS1.js";
5
5
  export { e as loadDotEnv, t as registerAppModules, n as registerDriver, r as runServe };
@@ -1,3 +1,3 @@
1
1
  import "./renderModeScan-D7J1B7Kw.js";
2
- import { t as e } from "./start-9LiUOfES.js";
2
+ import { t as e } from "./start-T4VesWiM.js";
3
3
  export { e as runStartCommand };
@@ -1,8 +1,8 @@
1
1
  import { i as e, t } from "./nodeEnvironment-cGFAj1J8.js";
2
2
  import { r as n } from "./appPort-B_HpJ_ck.js";
3
- import { A as r, B as i, C as a, D as o, E as s, H as c, I as l, J as u, N as d, O as f, P as p, S as m, T as ee, U as h, V as g, a as _, b as v, d as y, f as b, i as x, j as S, k as te, n as ne, q as C, s as re, st as ie, ut as w, w as T, x as E, y as ae, z as D } from "./webDev-CBYvPqQr.js";
3
+ import { A as r, B as i, C as a, D as o, E as s, H as c, I as l, J as u, N as d, O as f, P as p, S as m, T as ee, U as h, V as g, a as _, b as v, d as y, f as b, i as x, j as S, k as te, n as ne, q as C, s as re, st as ie, ut as w, w as T, x as E, y as ae, z as D } from "./webDev-Dybxew86.js";
4
4
  import { o as O, t as k } from "./connectionConfig-UFlIEiys.js";
5
- import { A, D as j, a as M, h as oe, j as N, k as se } from "./inspect-CBqFtAKA.js";
5
+ import { A, D as j, a as M, h as oe, j as N, k as se } from "./inspect-CjTYzAs_.js";
6
6
  import { a as ce, c as P, d as le, f as ue, l as F, p as I, u as de } from "./renderModeScan-D7J1B7Kw.js";
7
7
  import { i as fe, n as pe, r as me, t as he } from "./runtimeRegistry-DMeKfTHP.js";
8
8
  import { n as ge } from "./logFileSink-C_D2wRN1.js";
@@ -1,3 +1,3 @@
1
1
  import { t as e } from "./loadEnv-D9nEOClM.js";
2
- import { t } from "./start-9LiUOfES.js";
2
+ import { t } from "./start-T4VesWiM.js";
3
3
  export { e as loadDotEnv, t as runStartCommand };
@@ -1,6 +1,6 @@
1
1
  import { n as e } from "./runtimeRegistry-DMeKfTHP.js";
2
2
  import { n as t } from "./cliOutput-D1tSBoRM.js";
3
- import { a as n, i as r } from "./inspectFetch-Cm8_wVvp.js";
3
+ import { a as n, i as r } from "./inspectFetch-EMuhTG_9.js";
4
4
  import { ansi as i } from "@voltro/logger";
5
5
  //#region src/tracesCmd.ts
6
6
  var a = (e) => {
@@ -0,0 +1,2 @@
1
+ import { f as e, p as t } from "./updateCommand-x0pI_x-B.js";
2
+ export { e as runApplyCodemods, t as runUpdateCommand };
@@ -1,4 +1,4 @@
1
- import { n as e, r as t, t as n } from "./codemodRunner-DRRqXR74.js";
1
+ import { n as e, r as t, t as n } from "./codemodRunner-BnFq3Fgu.js";
2
2
  import { basename as r, dirname as i, join as a, relative as o, resolve as s } from "node:path";
3
3
  import { existsSync as c, readFileSync as l, readdirSync as u, statSync as d, writeFileSync as f } from "node:fs";
4
4
  import { totalmem as p } from "node:os";
@@ -0,0 +1,2 @@
1
+ import { _ as e } from "./webDev-Dybxew86.js";
2
+ export { e as tryRunWebServe };
@@ -2,7 +2,7 @@ import { t as e } from "./nodeEnvironment-cGFAj1J8.js";
2
2
  import { n as t } from "./loadEnv-D9nEOClM.js";
3
3
  import { r as n } from "./appPort-B_HpJ_ck.js";
4
4
  import { h as r, m as i } from "./fileConventions-Cof68_BL.js";
5
- import { A as a, D as o, a as s, h as c, j as l, k as u } from "./inspect-CBqFtAKA.js";
5
+ import { A as a, D as o, a as s, h as c, j as l, k as u } from "./inspect-CjTYzAs_.js";
6
6
  import { n as d } from "./agentsMd-mhQMF1bx.js";
7
7
  import { c as f, p, t as m } from "./renderModeScan-D7J1B7Kw.js";
8
8
  import { t as h } from "./pageConvention-cEiRxdab.js";
@@ -1854,7 +1854,19 @@ import {
1854
1854
  if (!o.ok) throw Error(`[loader.query] ${r}: HTTP ${o.status} ${await o.text()}`);
1855
1855
  let s = await o.json();
1856
1856
  if (!Array.isArray(s)) throw Error(`[loader.query] ${r}: unexpected non-array rpc response`);
1857
- let c = (e) => e && typeof e == "object" && "_tag" in e && "data" in e && e._tag === "snapshot" ? e.data : e, l, u = !1;
1857
+ let c = (e) => {
1858
+ if (!e || typeof e != "object" || !("_tag" in e)) return e;
1859
+ let t = e;
1860
+ switch (t._tag) {
1861
+ case "snapshot": return t.data;
1862
+ case "error": {
1863
+ let e = t.error;
1864
+ throw Object.assign(Error(`[loader.query] ${r}: ${e?._tag ?? "error"}${e?.message ? ` — ${e.message}` : ""}`, { cause: t.error }), { voltroError: t.error });
1865
+ }
1866
+ case "delta": throw Error(`[loader.query] ${r}: received a 'delta' as the first event, which should be impossible — a subscription always opens with a snapshot. This is a framework bug.`);
1867
+ default: return e;
1868
+ }
1869
+ }, l, u = !1;
1858
1870
  for (let e of s) {
1859
1871
  if (e._tag === "ClientProtocolError") throw Error(`[loader.query] ${r}: protocol error ${JSON.stringify(e.error)}`);
1860
1872
  if (!(e.requestId !== void 0 && String(e.requestId) !== "0")) {
@@ -222,7 +222,7 @@ createVerifier({ secret: [process.env.WEBHOOK_SECRET, process.env.WEBHOOK_SECRET
222
222
  ...t === void 0 ? {} : { payload: t }
223
223
  };
224
224
  }, S = u({ scope: "voltro:webhooks" }), C = ["--out", "--name"], w = async (e) => {
225
- let { walk: t, loadDiscovered: n } = await import("./dev-CoG-ZPx8.js"), { outgoingFromEvents: r } = await import("./webhookDiscovery-CrGAfhIG.js");
225
+ let { walk: t, loadDiscovered: n } = await import("./dev-D2BikO7a.js"), { outgoingFromEvents: r } = await import("./webhookDiscovery-CrGAfhIG.js");
226
226
  return r((await n(await t(e))).events.map((e) => ({
227
227
  file: e.file,
228
228
  descriptor: e.descriptor
@@ -1,5 +1,5 @@
1
1
  import { n as e } from "./cliOutput-D1tSBoRM.js";
2
- import { n as t, o as n, r, t as i } from "./inspectFetch-Cm8_wVvp.js";
2
+ import { n as t, o as n, r, t as i } from "./inspectFetch-EMuhTG_9.js";
3
3
  import { ansi as a } from "@voltro/logger";
4
4
  //#region src/workflowsCmd.ts
5
5
  var o = [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@voltro/cli",
3
- "version": "0.34.0",
3
+ "version": "0.35.0",
4
4
  "description": "The `voltro` CLI — dev server, codegen, migrations, project scaffolding, agent-docs seeding, and production serve.",
5
5
  "keywords": [
6
6
  "voltro",
@@ -538,6 +538,36 @@
538
538
  "title": "`routeHttp` returns a Promise",
539
539
  "kind": "manual"
540
540
  },
541
+ {
542
+ "version": "0.35.0",
543
+ "id": "0.35.0/01_event-access-decision",
544
+ "title": "Every declared event decides who may listen (`guards:` or `openAccess:`)",
545
+ "kind": "manual"
546
+ },
547
+ {
548
+ "version": "0.35.0",
549
+ "id": "0.35.0/02_plugin-procedures-carry-access-decisions",
550
+ "title": "First-party plugin procedures carry access decisions; twelve now require a scope",
551
+ "kind": "manual"
552
+ },
553
+ {
554
+ "version": "0.35.0",
555
+ "id": "0.35.0/03_analytics-mirror-max-version",
556
+ "title": "Custom analytics mirror sinks answer `maxVersion` (fleet-scope seed read)",
557
+ "kind": "manual"
558
+ },
559
+ {
560
+ "version": "0.35.0",
561
+ "id": "0.35.0/04_flow-run-step-skipped",
562
+ "title": "A flow run step can now be 'skipped' — handle it where you render a run timeline",
563
+ "kind": "manual"
564
+ },
565
+ {
566
+ "version": "0.35.0",
567
+ "id": "0.35.0/05_declared-schema-drops-runtime-flags",
568
+ "title": "The declared schema no longer reads `CDC` / `VOLTRO_UNDO` / `VOLTRO_TRACING_PERSIST`",
569
+ "kind": "manual"
570
+ },
541
571
  {
542
572
  "version": "0.4.0",
543
573
  "id": "0.4.0/01_rbac-forbidden-to-scopeerror",
@@ -637,22 +667,22 @@
637
667
  "@effect/platform-node": "^0.108.0",
638
668
  "@effect/sql": "^0.52.0",
639
669
  "@effect/workflow": "^0.19.0",
640
- "@voltro/ai": "0.34.0",
641
- "@voltro/cache": "0.34.0",
642
- "@voltro/data-transfer": "0.34.0",
643
- "@voltro/database": "0.34.0",
644
- "@voltro/env": "0.34.0",
645
- "@voltro/kv": "0.34.0",
646
- "@voltro/logger": "0.34.0",
647
- "@voltro/plugin-auth": "0.34.0",
648
- "@voltro/plugin-broadcast": "0.34.0",
649
- "@voltro/plugin-mail": "0.34.0",
650
- "@voltro/plugin-storage": "0.34.0",
651
- "@voltro/plugin-webhooks": "0.34.0",
652
- "@voltro/protocol": "0.34.0",
653
- "@voltro/runtime": "0.34.0",
654
- "@voltro/serverless": "0.34.0",
655
- "@voltro/workflow": "0.34.0",
670
+ "@voltro/ai": "0.35.0",
671
+ "@voltro/cache": "0.35.0",
672
+ "@voltro/data-transfer": "0.35.0",
673
+ "@voltro/database": "0.35.0",
674
+ "@voltro/env": "0.35.0",
675
+ "@voltro/kv": "0.35.0",
676
+ "@voltro/logger": "0.35.0",
677
+ "@voltro/plugin-auth": "0.35.0",
678
+ "@voltro/plugin-broadcast": "0.35.0",
679
+ "@voltro/plugin-mail": "0.35.0",
680
+ "@voltro/plugin-storage": "0.35.0",
681
+ "@voltro/plugin-webhooks": "0.35.0",
682
+ "@voltro/protocol": "0.35.0",
683
+ "@voltro/runtime": "0.35.0",
684
+ "@voltro/serverless": "0.35.0",
685
+ "@voltro/workflow": "0.35.0",
656
686
  "chokidar": "^5.0.0",
657
687
  "ioredis": "^5.11.1",
658
688
  "tinyglobby": "^0.2.17",
@@ -707,7 +707,7 @@ each plugin's own README.
707
707
 
708
708
  | Topic | Open | Summary |
709
709
  |---|---|---|
710
- | **What's new in 0.34.0** | `node_modules/@voltro/cli/templates/agent-docs/whats-new.md` | Everything that changed in this version. Read it before hand-rolling something the framework may now ship. |
710
+ | **What's new in 0.35.0** | `node_modules/@voltro/cli/templates/agent-docs/whats-new.md` | Everything that changed in this version. Read it before hand-rolling something the framework may now ship. |
711
711
  | AI | `node_modules/@voltro/cli/templates/agent-docs/ai.md` | How Voltro treats AI — agents, tools, streaming, RAG — all primitives over the same WebSocket as the rest of the framework. |
712
712
  | Authentication | `node_modules/@voltro/cli/templates/agent-docs/authentication.md` | How @voltro/plugin-auth wires password + session-cookie auth across api + web, plus the pluggable identity-strategy protocol. |
713
713
  | Caching | `node_modules/@voltro/cli/templates/agent-docs/caching.md` | Voltro's caching layer (@voltro/cache) — an always-on memory default, swappable Redis-compatible backends, a low-level wrap primitive, and automatic query-result invalidation. |
@@ -9,7 +9,7 @@ each plugin's own README.
9
9
 
10
10
  | Topic | Open | Summary |
11
11
  |---|---|---|
12
- | **What's new in 0.34.0** | `node_modules/@voltro/cli/templates/agent-docs/whats-new.md` | Everything that changed in this version. Read it before hand-rolling something the framework may now ship. |
12
+ | **What's new in 0.35.0** | `node_modules/@voltro/cli/templates/agent-docs/whats-new.md` | Everything that changed in this version. Read it before hand-rolling something the framework may now ship. |
13
13
  | AI | `node_modules/@voltro/cli/templates/agent-docs/ai.md` | How Voltro treats AI — agents, tools, streaming, RAG — all primitives over the same WebSocket as the rest of the framework. |
14
14
  | Authentication | `node_modules/@voltro/cli/templates/agent-docs/authentication.md` | How @voltro/plugin-auth wires password + session-cookie auth across api + web, plus the pluggable identity-strategy protocol. |
15
15
  | Caching | `node_modules/@voltro/cli/templates/agent-docs/caching.md` | Voltro's caching layer (@voltro/cache) — an always-on memory default, swappable Redis-compatible backends, a low-level wrap primitive, and automatic query-result invalidation. |
@@ -161,7 +161,7 @@
161
161
  "group": null,
162
162
  "description": "Voltro Cloud (coming soon) — the managed runtime for your Voltro project. Today the Free control plane registers + observes your self-hosted apps.",
163
163
  "path": "agent-docs/deployment.md",
164
- "files": 7
164
+ "files": 8
165
165
  },
166
166
  {
167
167
  "id": "internationalization",
@@ -2167,6 +2167,11 @@ default to "allowed", so a discovered `*.query.ts` / `*.mutation.ts` /
2167
2167
  `*.action.ts` / `*.stream.ts` with no guard was callable by **any authenticated
2168
2168
  session** — the door defaulted open, and nothing said so.
2169
2169
 
2170
+ The same rule covers **events**: a `*.event.ts` declaration is a wire surface
2171
+ too, and one that declares neither `guards:` nor `openAccess:` was silently
2172
+ subscribable by anyone who could open the socket. `defineEvent` takes the same
2173
+ two answers — see [Events](/docs/data/events).
2174
+
2170
2175
  There are exactly two answers, and they are not the same claim:
2171
2176
 
2172
2177
  ```ts
@@ -2204,8 +2209,8 @@ refusal names **every** offending procedure with its file, because the fix is on
2204
2209
  pass over the whole list:
2205
2210
 
2206
2211
  ```
2207
- [access] 3 wire-exposed procedures declare no access decision, and this app runs
2208
- with `security.defaultDeny`:
2212
+ [access] 3 wire-exposed procedures or events declare no access decision, and
2213
+ this app runs with `security.defaultDeny`:
2209
2214
 
2210
2215
  invoices.list (query)
2211
2216
  src/api/invoices.query.ts
@@ -2229,11 +2234,24 @@ anyone reaches for is off, and an env var is how a security default becomes
2229
2234
  permanently off in one CI job with no diff to review. `voltro doctor` keeps
2230
2235
  listing the undecided procedures while it is off, marked advisory.
2231
2236
 
2232
- ### What it does NOT cover
2233
-
2234
- The gate reads **your app's own** discovered procedures. The procedures a plugin
2235
- declares are the plugin author's decision and are not checked here — adopting
2236
- this does not turn into a bug report against a plugin you installed.
2237
+ ### What it does NOT cover — and what covers the rest
2238
+
2239
+ The **boot** gate reads **your app's own** discovered procedures and events. The
2240
+ procedures a plugin declares are the plugin author's decision and are not judged
2241
+ at boot — adopting this does not turn into a bug report against a plugin you
2242
+ installed.
2243
+
2244
+ They are not unpoliced, though: the same `security.defaultDeny` is also enforced
2245
+ **per request in the dispatch spine**, as defense in depth. A descriptor that
2246
+ reaches the wire with no access decision — a plugin route, a hand-bound
2247
+ descriptor — is refused with the same typed `ScopeError` before the transaction
2248
+ opens or any external I/O runs. Every first-party plugin route declares its own
2249
+ decision (a scope where a real authority exists — e.g. `billing:manage`,
2250
+ `storage:browse` — or `openAccess` with the reason on the routes that are
2251
+ self-scoped or anonymous-capable by design; each plugin's page lists them). A
2252
+ third-party plugin that declares neither on a route will see that route refused
2253
+ per-request under default-deny — the fix is one field on the route, exactly as
2254
+ for your own procedures.
2237
2255
 
2238
2256
  ## Decide — `can` / `assertCan`
2239
2257
 
@@ -2486,6 +2504,36 @@ error frame, rather than delivering an empty snapshot — an empty snapshot on a
2486
2504
  live subscription reads to a client as "every row you could see was just
2487
2505
  deleted". Make sure your subscription error handling surfaces it.
2488
2506
 
2507
+ ## A path that cannot apply the filter refuses, rather than serving rows
2508
+
2509
+ If a filter is registered and a scoped store is built without a resolved scope,
2510
+ the store **throws**. It does not fall back to unfiltered reads.
2511
+
2512
+ That fallback used to exist, and it is the reason this section does. A team
2513
+ measured four read paths returning every row of the tenant to every employee,
2514
+ on both transports, with `row filter registered` in the boot log and a green
2515
+ test suite. The registration lived in a module-local variable, so an app's
2516
+ `*.startup.tsx` and the framework's request pipeline could hold two different
2517
+ copies of it — the serve bundle inlines the framework while app modules stay
2518
+ external, and a strict pnpm tree can resolve one version into two directories.
2519
+ The pipeline read "no filter registered", which was indistinguishable from an
2520
+ app that has none, and served everything.
2521
+
2522
+ The registration is process-global for real now (`globalThis`, so every copy
2523
+ shares one cell), and the ambiguity that made the failure silent is gone: those
2524
+ two readings are different claims and only one of them is a decision.
2525
+
2526
+ If a code path is deliberately unfiltered — a system sweep, a migration, a
2527
+ seeding helper in a test — say so:
2528
+
2529
+ ```ts
2530
+ wrapStoreWithMixinBehaviour(store, { subject, schemaRegistry, rowFilter: NO_ROW_FILTER })
2531
+ ```
2532
+
2533
+ `runAsSystem`, change-stream subscribers and the webhook trigger context already
2534
+ do this; a system subject bypasses row filters by design, and it is now written
2535
+ down rather than inferred from an absence.
2536
+
2489
2537
  ## What does *not* bypass it
2490
2538
 
2491
2539
  | | Bypasses the row filter? |
@@ -2107,6 +2107,45 @@ voltro check --url https://api.example.com
2107
2107
 
2108
2108
  `--token` (or `VOLTRO_INSPECT_TOKEN`) supplies the bearer; `VOLTRO_INSPECT_URL` sets a default target so you can drop the flag. Works for `inspect`, `logs`, `traces`, `workflows`, `cluster` and `check`.
2109
2109
 
2110
+ ## `voltro probe access` — is a declared guard actually enforced?
2111
+
2112
+ `voltro check` reads an app's manifest and reports a procedure with **no access
2113
+ decision**. It cannot tell you whether the decisions that ARE declared are
2114
+ enforced. `voltro probe access` asks the running app:
2115
+
2116
+ ```bash
2117
+ voltro probe access # every live api
2118
+ voltro probe access --url https://api.example.com --strict
2119
+ ```
2120
+
2121
+ It calls every procedure that declares a guard with **no credentials at all**
2122
+ and reports any that answer anyway:
2123
+
2124
+ ```
2125
+ api: probed 14 guarded procedure(s) with NO credentials
2126
+ ✗ orders.export — ANSWERED an unauthenticated call
2127
+ ? billing.invoice — answered 'ParseError' — not an access refusal, so the guard was not reached
2128
+ ✓ 12 refused · 1 inconclusive · 1 admitted
2129
+ ```
2130
+
2131
+ Three verdicts, and the third is what keeps the command honest:
2132
+
2133
+ | Verdict | Meaning |
2134
+ |---|---|
2135
+ | `refused` | The call came back as an access refusal. The declaration is enforced. |
2136
+ | `admitted` | The call SUCCEEDED without credentials. **This is the finding.** |
2137
+ | `inconclusive` | The call failed for a reason that is not an access refusal — usually payload validation running before the guard. **Not a pass.** |
2138
+
2139
+ Exit code is non-zero on any `admitted`. `--strict` also fails on
2140
+ `inconclusive`, which is what you want in CI: "I could not tell" should block.
2141
+
2142
+ **What it deliberately does not do.** It probes ANONYMOUSLY, so it cannot tell
2143
+ `orders:read` from `orders:write` — it answers exactly one question, and the
2144
+ alternative (minting a subject per guard) would put credential minting into a
2145
+ command that can be pointed at production. Procedures declared `openAccess:` are
2146
+ skipped; probing them would report every deliberately-public route as a finding
2147
+ and bury the real ones.
2148
+
2110
2149
  ## Securing the local surface
2111
2150
 
2112
2151
  `voltro dev` mints a per-project `VOLTRO_INSPECT_TOKEN` into `.env.local`, so the inspect surface is authenticated from the first boot — the dev server listens on every interface, and without a token anyone on the same network could read your rows, schema and logs. You don't have to wire it anywhere: the CLI picks the token up from the runtime registry (so the commands work from any directory), and the dashboard's server-side proxy supplies it for same-machine targets. Setting `VOLTRO_INSPECT_TOKEN` yourself always wins.
@@ -2564,6 +2603,26 @@ voltro data import ./out --force # import despite schema drift AND c
2564
2603
 
2565
2604
  Import is **integrity-checked** (each table's checksum + row count verified as it decodes; each asset re-hashed against its content address), applies tables in **FK-parent-first order**, and a resumed run skips already-applied tables via the ledger.
2566
2605
 
2606
+ ### Postgres targets bulk-load via COPY
2607
+
2608
+ On a **postgres** target, the direct import switches to `COPY … FROM STDIN`
2609
+ wherever plain-INSERT semantics provably hold: `--mode replace` (the tables were
2610
+ just truncated) and the default `upsert` into a table that is **empty** at
2611
+ import time — the fresh-target shape every cross-dialect migration
2612
+ (mysql → postgres, sqlite → postgres, …) lands in. Measured on a 7-column table
2613
+ (text/int/bool/jsonb/timestamptz) with 50 000 rows against a local postgres:
2614
+ row-by-row **12.8 s (~3.9 k rows/s)** vs COPY **0.59 s (~84.6 k rows/s)** —
2615
+ **21.7× faster**. Your factor depends on row width and network latency;
2616
+ COPY's advantage grows with per-row round-trip cost.
2617
+
2618
+ Everything else keeps the per-row `DataStore` writes: `upsert` into a non-empty
2619
+ table (COPY cannot upsert), `append` (per-row conflict handling), `--atomic`
2620
+ (the COPY connection would sit outside the transaction), and every other
2621
+ dialect. A refused COPY batch (an FK the deferred pass repairs later, a value
2622
+ COPY text can't carry) is atomic — nothing landed — so the importer replays
2623
+ exactly that batch through the per-row path and continues; semantics are
2624
+ identical, only the speed differs.
2625
+
2567
2626
  ### Schema-drift pre-flight
2568
2627
 
2569
2628
  Every bundle records the **schema fingerprint** of the source it was exported from (a stable hash of the schema shape — the same fingerprint prod boot uses to detect drift). Before touching the target, `import` **recomputes the target app's fingerprint the same way** and compares it to the bundle's. On a mismatch it **refuses, fail-closed, before any row lands** — a drifted target (a missing column, a renamed table) would otherwise fail mid-load with a raw database error after some rows already committed:
@@ -1606,6 +1606,37 @@ Names are lowercase kebab segments separated by dots — `presence`, `job-queue`
1606
1606
  uppercase letter (a key that differs only by case reads as one channel and
1607
1607
  routes as two). Declaring the same name twice returns the same channel.
1608
1608
 
1609
+ ### A publish is LOCAL news, and says so
1610
+
1611
+ The change event a publish emits carries `origin: 'inline'` — the same stamp a
1612
+ write made in this process carries. An event that arrives from another replica
1613
+ carries `origin: 'injected'`. So a listener can tell the two apart:
1614
+
1615
+ ```ts
1616
+ store.onChange((event) => {
1617
+ if (event.table !== jobQueue.key) return
1618
+ if (event.origin === 'injected') {
1619
+ // a PEER published — mirror it into local state, then publish onward if
1620
+ // this node has anything of its own to add
1621
+ }
1622
+ })
1623
+ ```
1624
+
1625
+ That distinction is what makes a fan-out pattern writable. Without it, a
1626
+ replica's own publish and a peer's are indistinguishable, and the only way to
1627
+ avoid an echo is a boolean per channel.
1628
+
1629
+ Two consequences worth knowing:
1630
+
1631
+ - **Re-publishing from inside a change listener works, synchronously.** A
1632
+ channel published while handling a peer's event reaches the other replicas
1633
+ like any other publish. (This is worth stating because it did not use to:
1634
+ `plugin-broadcast` suppressed every emission made while it was injecting, so
1635
+ such a publish woke the local node and silently never left it.)
1636
+ - **`origin` never crosses the wire.** It describes how an event reached *this*
1637
+ process, so the receiving replica always stamps its own. A publish that says
1638
+ `'inline'` here arrives as `'injected'` there, which is the truth on both ends.
1639
+
1609
1640
  ### What a channel is not
1610
1641
 
1611
1642
  - **Not an event.** [`defineEvent`](/docs/data/subscriptions#streams-are-different)
@@ -1881,7 +1912,16 @@ So deliveries run **concurrently, up to a bound**. The default is 8 in flight.
1881
1912
  Measured with 50 subscribers behind a 5 ms guard: 517 ms to serve all of them
1882
1913
  serially, 72 ms at 8 lanes.
1883
1914
 
1884
- Set it with `VOLTRO_REACTIVE_DELIVERY_CONCURRENCY`:
1915
+ Declare it in `app.config.ts`, or override per deployment with the env var —
1916
+ the env wins, because an operator acting on a running system outranks what the
1917
+ project declared:
1918
+
1919
+ ```ts
1920
+ export default {
1921
+ type: 'api' as const, name: 'api',
1922
+ reactive: { deliveryConcurrency: 16, rawReadTrackingLimit: 64 },
1923
+ }
1924
+ ```
1885
1925
 
1886
1926
  ```bash
1887
1927
  VOLTRO_REACTIVE_DELIVERY_CONCURRENCY=16 voltro serve
@@ -2120,6 +2160,17 @@ A `*.event.ts` file is **browser-safe** and may hold several declarations — a
2120
2160
 
2121
2161
  **`guards` decide who may listen**, in the same vocabulary a query uses, and they are checked *before* the subscription is registered — a refused client never holds one. The routing key is the guard input, so a resource-scoped guard (`{ scope: 'arena:read', from: 'arenaId' }`) sees which arena was asked for. A refusal reaches the client as the `ScopeError` the rpc declares.
2122
2162
 
2163
+ **`openAccess: '<reason>'` is the other answer to the same question**, exactly as on a procedure. Under `security.defaultDeny` (the default) an event that declares *neither* `guards:` nor `openAccess:` refuses the boot, by name — an access decision nobody made is a hole, not a default. For an event that really is open — a public scoreboard, a status pulse — declare it and say why; the reason is what a reviewer reads and what `voltro doctor` prints beside the tag. Do not reach for a scope every caller already holds just to satisfy the gate: that guard reads as protection and enforces nothing.
2164
+
2165
+ ```ts
2166
+ export const scoreboardTick = defineEvent({
2167
+ name: 'scoreboard.tick',
2168
+ key: Schema.Struct({ arenaId: Schema.String }),
2169
+ payload: Schema.Struct({ score: Schema.Number }),
2170
+ openAccess: 'public scoreboard — carries no caller data',
2171
+ })
2172
+ ```
2173
+
2123
2174
  **The tenant is not part of the key and must never be added.** It comes from the subject on both sides, so a cross-tenant delivery is impossible by construction rather than by remembering to filter.
2124
2175
 
2125
2176
  The name shares the rpc tag space. Two declarations answering to one name fail the **boot**, not the first delivery.
@@ -2548,6 +2599,8 @@ Migrating is mechanical: declare the event, replace the insert with `ctx.events.
2548
2599
  ```ts
2549
2600
  defineEvent({ name: 'orders paid', … }) // ✗ whitespace — see below
2550
2601
  defineEvent({ name: 'orders.paid', guards: [], … }) // ✗ enforces nothing
2602
+ defineEvent({ name: 'x', openAccess: '', … }) // ✗ the reason is the point
2603
+ defineEvent({ name: 'x', openAccess: 'why', guards: [{ scope: 's' }], … }) // ✗ two decisions
2551
2604
  defineEvent({ name: 'x', webhook: { rateLimit: { perMinute: 0 } } }) // ✗ never delivers
2552
2605
  ```
2553
2606
 
@@ -2559,7 +2612,11 @@ production only. Use a dot to namespace: `orders.paid`.
2559
2612
 
2560
2613
  **`guards: []` is refused** because it reads at the call site as if the event
2561
2614
  were protected and enforces nothing — the empty list never reaches the check.
2562
- Omit the field for an unguarded event.
2615
+ An event with no decision at all does not boot under `security.defaultDeny`;
2616
+ declare `openAccess: '<why>'` for a deliberately open one. **An empty
2617
+ `openAccess` reason is refused** — the reason is the reviewable half of the
2618
+ decision — and **`openAccess` + `guards` together are refused**: two decisions
2619
+ say the event is protected AND open.
2563
2620
 
2564
2621
  **`rateLimit: { perMinute: 0 }`** defers every delivery forever. There is no
2565
2622
  "unlimited" spelling — omit `rateLimit` for no ceiling. **`version: 0`** would
@@ -887,6 +887,10 @@ A multi-step plan runs inside one `BEGIN ... COMMIT`. Mid-flight failure rolls E
887
887
 
888
888
  The advisory-lock variants serialise concurrent applies — two operators running `voltro db apply` against the same DB at the same time go through serially.
889
889
 
890
+ **The lock is scoped to your configured schema.** With `DB_SCHEMA` set, the lock key (Postgres) / lock name (MySQL, MSSQL) is derived from the schema, so two apps sharing one database in different schemas do not serialise — or defer — each other's migrations and trigger repairs. Without `DB_SCHEMA` (or with `DB_SCHEMA=public`) every instance takes one stable framework-wide key, which is what makes a rolling deploy safe: old and new replicas contend on the same lock. On MySQL/MariaDB `GET_LOCK` is server-wide; setting `DB_SCHEMA` to your database name un-shares the lock between two apps on one server.
891
+
892
+ **Transient DDL failures are retried, per the dialect's own predicate.** A `CREATE TABLE IF NOT EXISTS` that meets SQLite/Turso's schema lock (`database is locked` / `SQLITE_BUSY`), a MySQL lock-wait timeout, or a Postgres/MSSQL deadlock victim during the boot auto-migrate is retried with bounded attempts and exponential backoff instead of failing the boot on the first attempt — only statements that are safe to re-run, and on Postgres/MSSQL as a fresh transaction (their deadlock classes roll the whole transaction back). `VOLTRO_MIGRATION_DDL_RETRIES` moves the retry count (default 4; `0` disables).
893
+
890
894
  ## MySQL / MariaDB / SQLite / Turso — the resume ledger
891
895
 
892
896
  Every DDL statement implicitly commits. A 5-op plan on MySQL is effectively 5 separate "atomic statements" with the prior ones already committed when a later one fails. If op 5 of 5 fails, ops 1–4 stay applied:
@@ -3154,6 +3158,48 @@ you would review is the one you just wrote.
3154
3158
 
3155
3159
  If the cause is drift (someone DDL'd prod manually), see [drift.md](./drift.md) for reconciliation paths.
3156
3160
 
3161
+ ### If the two processes are the same image: compare their `env:` blocks
3162
+
3163
+ A fingerprint mismatch does not always mean the schema changed. The declared
3164
+ table set is what gets hashed, and until 0.35.0 three RUNTIME flags could move
3165
+ it — so a pre-deploy migrate Job and the pods it feeds could disagree while
3166
+ running identical code against one database.
3167
+
3168
+ That was reported as a green migrate job followed by every pod in
3169
+ CrashLoopBackOff. The chart gave the Job its own `env:` list (`NODE_ENV`,
3170
+ `DB_*`) while `CDC: "0"` lived in the pods' block, because change data capture
3171
+ reads as a runtime concern. Measured on mariadb at `NODE_ENV=production`, each
3172
+ flag flipped alone:
3173
+
3174
+ | flag | effect on the DECLARED set |
3175
+ | --- | --- |
3176
+ | `CDC=0` | removes `_voltro_cdc_offsets` |
3177
+ | `VOLTRO_UNDO=on` | adds `_voltro_undo_log` |
3178
+ | `VOLTRO_TRACING_PERSIST=all` | adds `_voltro_traces` |
3179
+
3180
+ **From 0.35.0 none of them does.** `_voltro_cdc_offsets` follows the DIALECT, so
3181
+ a mariadb/mssql app declares it either way; the other two are declared in
3182
+ `app.config.ts` and the env vars only choose what a process captures:
3183
+
3184
+ ```ts
3185
+ export default {
3186
+ // Both default to on outside production. Declare them when you want the
3187
+ // table in a production schema — `VOLTRO_UNDO` / `VOLTRO_TRACING_PERSIST`
3188
+ // alone is no longer enough, because your migrate job does not carry them.
3189
+ schema: { undo: true, traces: true },
3190
+ }
3191
+ ```
3192
+
3193
+ Setting a capture flag ON without the declaration now refuses the boot and names
3194
+ the field, rather than writing to a table nobody created.
3195
+
3196
+ The refusal also prints which of the three tables this process decided and from
3197
+ which input, so the comparison is a glance rather than a hash diff. Do compare
3198
+ the two `env:` blocks anyway if you use plugins: a plugin's `extendSchema.tables`
3199
+ is your code and may read anything, which is the one part the framework cannot
3200
+ guarantee for you.
3201
+
3202
+
3157
3203
  ## "duplicate column name in plan" (logically invalid plan)
3158
3204
 
3159
3205
  ```
@@ -3493,7 +3539,10 @@ Three things about it are worth knowing before you deploy a fleet:
3493
3539
  `pg_try_advisory_lock` and SKIPS if anything holds it, so N replicas booting
3494
3540
  together produce one repairing and N-1 logging `another instance … is
3495
3541
  converging it`. A concurrent `voltro db apply` holds the same lock, so the two
3496
- can never run each other's DDL.
3542
+ can never run each other's DDL. The lock is scoped to your configured
3543
+ `DB_SCHEMA`, so `another instance` really means an instance of YOUR
3544
+ deployment — a second app sharing the database in a different schema takes a
3545
+ different key and neither defers the other.
3497
3546
  - **It never fails a boot.** A check that cannot run warns and the process
3498
3547
  continues; reactivity may be degraded, and that is still better than a
3499
3548
  diagnostic taking the app down.