@voltro/cli 0.37.0 → 0.38.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 (104) hide show
  1. package/CHANGELOG.md +92 -0
  2. package/dist/{apiBuild-DGsShRuF.js → apiBuild-CvacV4zA.js} +2 -2
  3. package/dist/apiBuild-ttSDDOGp.js +2 -0
  4. package/dist/bin.js +1 -1
  5. package/dist/bootRefusal-pgvDxlrU.js +7 -0
  6. package/dist/{build-CiFA-E19.js → build-_LoTC1s0.js} +30 -30
  7. package/dist/{checkCommand-CLPTzYqM.js → checkCommand-Pjk5sBl2.js} +2 -2
  8. package/dist/{checkCommand-CIPYyNyc.js → checkCommand-wibtQx-N.js} +1 -1
  9. package/dist/{codegenCommand-BxS3sCeJ.js → codegenCommand-C60m_LCp.js} +1 -1
  10. package/dist/{codemodRunner-cyqu5oxr.js → codemodRunner-CXgQ-ecJ.js} +314 -201
  11. package/dist/{commands-BwG9A9Vv.js → commands-DaZVi9wi.js} +20 -20
  12. package/dist/{dashboardCommand-C7U146xQ.js → dashboardCommand-Dxy_kmoW.js} +3 -3
  13. package/dist/{dataCommand-BOwd8_aJ.js → dataCommand-D6-Ha2lf.js} +1 -1
  14. package/dist/{dev-BV6XNYKf.js → dev-CAOoXOkc.js} +1 -1
  15. package/dist/{dev-bwDgSObH.js → dev-E7Eo5GHc.js} +261 -260
  16. package/dist/doctorCommand-Cjl8Yii8.js +2 -0
  17. package/dist/{doctorCommand-BXyrtN2l.js → doctorCommand-gmDyALFL.js} +10 -10
  18. package/dist/{dormancyCommand-BDdRRLsa.js → dormancyCommand-CE6wG8lj.js} +1 -1
  19. package/dist/{embeddingsCommand-CjWGcAE6.js → embeddingsCommand-C3uAuf6N.js} +1 -1
  20. package/dist/{envCommand-B_TeG297.js → envCommand-DzuWh8ya.js} +19 -19
  21. package/dist/{evolveCommand-D6N-WfA4.js → evolveCommand-DPLqcYb2.js} +2 -2
  22. package/dist/index.js +1 -1
  23. package/dist/{infoCommand-BHKTprrx.js → infoCommand-CmczGj_l.js} +1 -1
  24. package/dist/{runtimeTrace-WYV7qYvz.js → runtimeTrace-6jfznSzr.js} +1 -1
  25. package/dist/{sdkgen-TM5sSdf7.js → sdkgen-CEJZZQUA.js} +1 -1
  26. package/dist/serveCommand-DNRF2nDI.js +2 -0
  27. package/dist/{serveCommand-CyZsXL1l.js → serveCommand-DnY4VS3D.js} +541 -540
  28. package/dist/serveEntry.js +1 -1
  29. package/dist/{start-BgN62boB.js → start-CidwCcjl.js} +1 -1
  30. package/dist/{start-T4VesWiM.js → start-RCh6qKFe.js} +24 -24
  31. package/dist/startEntry.js +1 -1
  32. package/dist/startup.d.ts +40 -1
  33. package/dist/startup.js +2 -2
  34. package/dist/startupRunner-CEqQq7ax.js +108 -0
  35. package/dist/updateCommand-B0J_T9Vs.js +2 -0
  36. package/dist/{updateCommand-9GHHsmDf.js → updateCommand-CmksX7m_.js} +1 -1
  37. package/dist/{webDev-Dybxew86.js → webDev-BKdD7c_a.js} +689 -691
  38. package/dist/webDev-CcJfdSjl2.js +2 -0
  39. package/dist/{webhooksCommand-CHWSPk5z.js → webhooksCommand-BHeaF1ZJ.js} +1 -1
  40. package/package.json +29 -17
  41. package/templates/AGENTS.md +1 -1
  42. package/templates/agent-docs/_index.md +1 -1
  43. package/templates/agent-docs/plugins/audit.md +4 -0
  44. package/templates/agent-docs/plugins/auth.md +4 -6
  45. package/templates/agent-docs/reference.md +3 -2
  46. package/templates/agent-docs/whats-new.md +49 -47
  47. package/templates/apps/api-ai/package.json +7 -7
  48. package/templates/apps/api-auth/package.json +8 -8
  49. package/templates/apps/api-backend/package.json +7 -7
  50. package/templates/apps/api-backend-deactivation/package.json +7 -7
  51. package/templates/apps/api-backend-mail/package.json +8 -8
  52. package/templates/apps/api-backend-mariadb/package.json +9 -9
  53. package/templates/apps/api-backend-sqlite/package.json +8 -8
  54. package/templates/apps/api-backend-storage/package.json +8 -8
  55. package/templates/apps/api-cms/package.json +10 -10
  56. package/templates/apps/api-cms/tests/content.write.test.ts +16 -2
  57. package/templates/apps/api-collab/mutations/documents.setBody.mutation.server.ts +11 -5
  58. package/templates/apps/api-collab/package.json +8 -8
  59. package/templates/apps/api-collab/tests/documents.setBody.test.ts +8 -2
  60. package/templates/apps/api-data-advanced/package.json +8 -8
  61. package/templates/apps/api-durable/package.json +8 -8
  62. package/templates/apps/api-feature-flags/package.json +9 -9
  63. package/templates/apps/api-governance/package.json +8 -8
  64. package/templates/apps/api-kv/package.json +8 -8
  65. package/templates/apps/api-moderation/package.json +8 -8
  66. package/templates/apps/api-observability/package.json +8 -8
  67. package/templates/apps/api-ratelimit/package.json +8 -8
  68. package/templates/apps/api-rbac/package.json +8 -8
  69. package/templates/apps/api-rest/package.json +7 -7
  70. package/templates/apps/api-saas/package.json +11 -11
  71. package/templates/apps/api-saas-starter/package.json +10 -10
  72. package/templates/apps/api-search/package.json +8 -8
  73. package/templates/apps/api-status/package.json +8 -8
  74. package/templates/apps/api-versioning/mutations/documents.update.mutation.server.ts +7 -5
  75. package/templates/apps/api-versioning/package.json +8 -8
  76. package/templates/apps/api-webhooks/package.json +9 -9
  77. package/templates/apps/changelog/package.json +6 -6
  78. package/templates/apps/edge-functions/package.json +2 -2
  79. package/templates/apps/frontend-admin/package.json +8 -8
  80. package/templates/apps/frontend-app/package.json +9 -9
  81. package/templates/apps/frontend-auth/package.json +8 -8
  82. package/templates/apps/frontend-blank/package.json +7 -7
  83. package/templates/apps/frontend-cms/package.json +9 -9
  84. package/templates/apps/frontend-collab/package.json +10 -10
  85. package/templates/apps/frontend-contact/package.json +7 -7
  86. package/templates/apps/frontend-dashboard/package.json +7 -7
  87. package/templates/apps/frontend-docs/package.json +7 -7
  88. package/templates/apps/frontend-i18n/package.json +6 -6
  89. package/templates/apps/frontend-landing/package.json +7 -7
  90. package/templates/apps/frontend-portal/package.json +8 -8
  91. package/templates/apps/frontend-saas/package.json +8 -8
  92. package/templates/apps/frontend-spa/package.json +7 -7
  93. package/templates/apps/frontend-ssr/package.json +7 -7
  94. package/templates/apps/frontend-ssr-api/package.json +8 -8
  95. package/templates/apps/frontend-static-blog/package.json +6 -6
  96. package/templates/apps/frontend-status/package.json +8 -8
  97. package/templates/apps/mobile-app/package.json +3 -3
  98. package/templates/patches/@effect__cluster@0.60.0.patch +51 -3
  99. package/dist/apiBuild-D0f6hYbH.js +0 -2
  100. package/dist/doctorCommand-C4L61G_L.js +0 -2
  101. package/dist/serveCommand-qumm7jdF.js +0 -2
  102. package/dist/startupRunner-DPGFchOa.js +0 -71
  103. package/dist/updateCommand-BN0tkl9-.js +0 -2
  104. package/dist/webDev-BcykISYQ2.js +0 -2
@@ -2764,11 +2764,11 @@ var x = (e, t, n) => {
2764
2764
  "that legitimately resolves to `undefined` does not throw — only a missing",
2765
2765
  "`loader` does."
2766
2766
  ].join("\n")
2767
- }), U = "@voltro/ui-shadcn", W = "voltro:lang", hn = "voltro:locale", G = "parsePreferenceCookies", gn = (e, t) => {
2767
+ }), U = "@voltro/ui-shadcn", W = "voltro:lang", hn = "voltro:locale", gn = "parsePreferenceCookies", _n = (e, t) => {
2768
2768
  let n = [];
2769
2769
  for (let r of e.getImportDeclarations()) if (r.getModuleSpecifierValue() === U) for (let e of r.getNamedImports()) e.getName() === t && n.push(e.getAliasNode()?.getText() ?? e.getName());
2770
2770
  return n;
2771
- }, _n = (e) => {
2771
+ }, vn = (e) => {
2772
2772
  let t = 0;
2773
2773
  for (let n of e.getSourceFiles()) n.getFullText().includes(W) && n.forEachDescendant((e) => {
2774
2774
  if (y.isStringLiteral(e) || y.isNoSubstitutionTemplateLiteral(e) || y.isTemplateHead(e) || y.isTemplateMiddle(e) || y.isTemplateTail(e) || y.isRegularExpressionLiteral(e)) {
@@ -2778,10 +2778,10 @@ var x = (e, t, n) => {
2778
2778
  }
2779
2779
  });
2780
2780
  return t;
2781
- }, vn = (e) => {
2781
+ }, yn = (e) => {
2782
2782
  let t = 0, n = 0;
2783
2783
  for (let r of e.getSourceFiles()) {
2784
- let e = gn(r, G);
2784
+ let e = _n(r, gn);
2785
2785
  e.length !== 0 && r.forEachDescendant((r) => {
2786
2786
  if (!y.isCallExpression(r) || !e.includes(r.getExpression().getText())) return;
2787
2787
  let i = r.getParent();
@@ -2804,29 +2804,29 @@ var x = (e, t, n) => {
2804
2804
  access: t,
2805
2805
  destructured: n
2806
2806
  };
2807
- }, yn = C({
2807
+ }, bn = C({
2808
2808
  version: "0.30.0",
2809
2809
  id: "0.30.0/01_locale-cookie-rename",
2810
2810
  title: "the language-preference cookie is `voltro:locale` (was `voltro:lang`)",
2811
2811
  kind: "transform",
2812
2812
  appliesTo: ({ project: e }) => e.getSourceFiles().some((e) => {
2813
2813
  let t = e.getFullText();
2814
- return t.includes(W) || t.includes(U) && (t.includes("LANG_COOKIE") || t.includes(G));
2814
+ return t.includes(W) || t.includes(U) && (t.includes("LANG_COOKIE") || t.includes(gn));
2815
2815
  }),
2816
2816
  transform: ({ project: e, helpers: t, log: n }) => {
2817
2817
  let r = t.renameImport(e, {
2818
2818
  module: U,
2819
2819
  from: "LANG_COOKIE",
2820
2820
  to: "LOCALE_COOKIE"
2821
- }), i = _n(e), { access: a, destructured: o } = vn(e), s = [];
2821
+ }), i = vn(e), { access: a, destructured: o } = yn(e), s = [];
2822
2822
  r > 0 && s.push(`LANG_COOKIE → LOCALE_COOKIE (${r} import(s))`), i > 0 && s.push(`'voltro:lang' → 'voltro:locale' (${i} literal(s))`), a + o > 0 && s.push(`parsePreferenceCookies().lang → .locale (${a + o} site(s))`), s.length !== 0 && n(`locale cookie rename: ${s.join("; ")}. Your SOURCE is migrated. Your USERS' cookies are not — the old \`voltro:lang\` cookie in an already-visited browser is no longer read, so each user falls back to Accept-Language / defaultLocale once and re-picks their language. Nothing errors. If that reset is unacceptable, copy the old value forward at your own boot — the codemod file has the three-line bridge.` + (o > 0 ? ` ${o} destructure(s) were rewritten as \`{ locale: lang }\` to keep the local name safe from a scope collision — rename the binding yourself if you want the vocabulary to match.` : ""));
2823
2823
  }
2824
- }), bn = /auditPlugin\s*\(|versioningPlugin\s*\(|atlassianPlugin\s*\(/, xn = C({
2824
+ }), xn = /auditPlugin\s*\(|versioningPlugin\s*\(|atlassianPlugin\s*\(/, Sn = C({
2825
2825
  version: "0.30.0",
2826
2826
  id: "0.30.0/02_audit-purge-query-recheck",
2827
2827
  title: "re-run the credential-purge query — the 0.28.0 spelling under-reported on MySQL/MariaDB",
2828
2828
  kind: "manual",
2829
- appliesTo: ({ project: e }) => e.getSourceFiles().some((e) => bn.test(e.getFullText())),
2829
+ appliesTo: ({ project: e }) => e.getSourceFiles().some((e) => xn.test(e.getFullText())),
2830
2830
  note: [
2831
2831
  "IF YOU ALREADY RAN THE 0.28.0 PURGE CHECK ON MySQL OR MariaDB, RUN IT AGAIN.",
2832
2832
  "The query that note shipped under-reported, and it under-reported by",
@@ -2873,14 +2873,14 @@ var x = (e, t, n) => {
2873
2873
  " `connectionCredentials(...)` keeps it in the framework vault",
2874
2874
  " (see `0.28.0/03_atlassian-credentials-context`)."
2875
2875
  ].join("\n")
2876
- }), Sn = /\.webhooks\s*\.\s*emit\s*\(|useWebhooks\s*\(/, Cn = C({
2876
+ }), Cn = /\.webhooks\s*\.\s*emit\s*\(|useWebhooks\s*\(/, wn = C({
2877
2877
  version: "0.30.0",
2878
2878
  id: "0.30.0/03_webhook-emit-durable-deferral",
2879
2879
  title: "a deferred `webhooks.emit` rides the transactional outbox; `{ immediate: true }` opts out",
2880
2880
  kind: "manual",
2881
2881
  appliesTo: ({ project: e }) => e.getSourceFiles().some((e) => {
2882
2882
  let t = e.getFullText();
2883
- return Sn.test(t) || w("emit").test(t);
2883
+ return Cn.test(t) || w("emit").test(t);
2884
2884
  }),
2885
2885
  note: [
2886
2886
  "A `ctx.webhooks.emit(...)` inside a MUTATION now enqueues a transactional",
@@ -2930,14 +2930,14 @@ var x = (e, t, n) => {
2930
2930
  "created by the declarative differ on the next `voltro dev` boot or",
2931
2931
  "`voltro db apply` — there is nothing to run by hand."
2932
2932
  ].join("\n")
2933
- }), wn = /\.executionId\b|workflows\s*\.\s*start\s*\(|workflows\s*\.\s*child\s*\(/, Tn = C({
2933
+ }), Tn = /\.executionId\b|workflows\s*\.\s*start\s*\(|workflows\s*\.\s*child\s*\(/, En = C({
2934
2934
  version: "0.30.0",
2935
2935
  id: "0.30.0/04_workflow-run-handle-nullable-execution",
2936
2936
  title: "`WorkflowRunHandle.executionId` is `string | null`; `status` gains queued/dropped/skipped",
2937
2937
  kind: "manual",
2938
2938
  appliesTo: ({ project: e }) => e.getSourceFiles().some((e) => {
2939
2939
  let t = e.getFullText();
2940
- return wn.test(t) || w("start").test(t);
2940
+ return Tn.test(t) || w("start").test(t);
2941
2941
  }),
2942
2942
  note: [
2943
2943
  "A workflow start no longer always becomes a run. With declarative flow",
@@ -3002,12 +3002,12 @@ var x = (e, t, n) => {
3002
3002
  " and `singleton: { mode: 'skip' }` throws `WorkflowSingletonHeldError`",
3003
3003
  " carrying the incumbent's execution id, so you can wait on THAT run."
3004
3004
  ].join("\n")
3005
- }), En = /auditPlugin\s*\(|versioningPlugin\s*\(|atlassianPlugin\s*\(/, Dn = C({
3005
+ }), Dn = /auditPlugin\s*\(|versioningPlugin\s*\(|atlassianPlugin\s*\(/, On = C({
3006
3006
  version: "0.31.0",
3007
3007
  id: "0.31.0/01_rerun-credential-scan",
3008
3008
  title: "re-run `voltro db scan-credentials` — on 0.30.0–0.30.2 it skipped the column credentials land in",
3009
3009
  kind: "manual",
3010
- appliesTo: ({ project: e }) => e.getSourceFiles().some((e) => En.test(e.getFullText())),
3010
+ appliesTo: ({ project: e }) => e.getSourceFiles().some((e) => Dn.test(e.getFullText())),
3011
3011
  note: [
3012
3012
  "IF YOU RAN `voltro db scan-credentials` ON 0.30.0, 0.30.1 OR 0.30.2, RUN IT",
3013
3013
  "AGAIN. Its answer did not cover the column a leaked credential is most",
@@ -3041,7 +3041,7 @@ var x = (e, t, n) => {
3041
3041
  "credential on the Subject — `connectionCredentials(...)` keeps it in the",
3042
3042
  "framework vault."
3043
3043
  ].join("\n")
3044
- }), On = C({
3044
+ }), kn = C({
3045
3045
  version: "0.32.0",
3046
3046
  id: "0.32.0/01_audit-redacts-outcome",
3047
3047
  title: "`auditPlugin` now redacts `outcome` by default — opt out if your trail reads it",
@@ -3082,7 +3082,7 @@ var x = (e, t, n) => {
3082
3082
  "purge the rows AND rotate the credentials — assume anything written to a",
3083
3083
  "log table has been read."
3084
3084
  ].join("\n")
3085
- }), kn = C({
3085
+ }), An = C({
3086
3086
  version: "0.33.0",
3087
3087
  id: "0.33.0/01_opentelemetry-optional-peer",
3088
3088
  title: "`@effect/opentelemetry` is now an optional peer — install it if you use tracing",
@@ -3114,7 +3114,7 @@ var x = (e, t, n) => {
3114
3114
  "never wrote down. It is a startup failure, not a silent loss of traces —",
3115
3115
  "losing telemetry quietly would be the worse outcome."
3116
3116
  ].join("\n")
3117
- }), An = C({
3117
+ }), jn = C({
3118
3118
  version: "0.33.0",
3119
3119
  id: "0.33.0/02_coordinated-handle-gained-wake",
3120
3120
  title: "`CoordinatedScheduleHandle` gained `wake()`, `currentIntervalMs()` and `isArmed()` — update hand-written test doubles",
@@ -3164,7 +3164,7 @@ var x = (e, t, n) => {
3164
3164
  "Reporting nothing is the conservative default on purpose — a task that",
3165
3165
  "cannot tell whether it was idle must not be slowed down on a guess."
3166
3166
  ].join("\n")
3167
- }), jn = C({
3167
+ }), Mn = C({
3168
3168
  version: "0.34.0",
3169
3169
  id: "0.34.0/01_serve-does-mount-inspect",
3170
3170
  title: "Correction: `voltro serve` DOES mount an inspect surface (the 0.12.0 note said it mounts none)",
@@ -3205,12 +3205,12 @@ var x = (e, t, n) => {
3205
3205
  "place, everyone already past 0.12.0 — which is everyone the wrong sentence",
3206
3206
  "could have misled — would never have seen it."
3207
3207
  ].join("\n")
3208
- }), Mn = (e) => /\.with\(\s*[\w.]*tenant\(/.test(e) || /\.serverOnly\(\s*\)/.test(e), Nn = C({
3208
+ }), Nn = (e) => /\.with\(\s*[\w.]*tenant\(/.test(e) || /\.serverOnly\(\s*\)/.test(e), Pn = C({
3209
3209
  version: "0.34.0",
3210
3210
  id: "0.34.0/02_keyed-writes-are-tenant-scoped",
3211
3211
  title: "Keyed-by-pk writes are tenant-enforced, and `crud` refuses `.serverOnly()` input",
3212
3212
  kind: "manual",
3213
- appliesTo: ({ project: e }) => e.getSourceFiles().some((e) => Mn(e.getFullText())),
3213
+ appliesTo: ({ project: e }) => e.getSourceFiles().some((e) => Nn(e.getFullText())),
3214
3214
  note: [
3215
3215
  "TWO WRITE-SIDE REFUSALS ARE NEW. Neither changes a type, so `tsc` will not",
3216
3216
  "point at the call sites — these are runtime behaviour changes.",
@@ -3274,12 +3274,12 @@ var x = (e, t, n) => {
3274
3274
  " an attack look like a no-op and leaves an honest caller debugging a",
3275
3275
  " value that quietly did not land."
3276
3276
  ].join("\n")
3277
- }), Pn = /* @__PURE__ */ new Set([
3277
+ }), Fn = /* @__PURE__ */ new Set([
3278
3278
  "defineQuery",
3279
3279
  "defineMutation",
3280
3280
  "defineAction",
3281
3281
  "defineStream"
3282
- ]), Fn = (e) => {
3282
+ ]), In = (e) => {
3283
3283
  let t = e.getArguments()[0];
3284
3284
  if (t === void 0 || !t.isKind(b.ObjectLiteralExpression)) return /* @__PURE__ */ new Set();
3285
3285
  let n = /* @__PURE__ */ new Set();
@@ -3288,16 +3288,16 @@ var x = (e, t, n) => {
3288
3288
  t !== void 0 && n.add(t);
3289
3289
  }
3290
3290
  return n;
3291
- }, In = (e, t) => /define(Query|Mutation|Action|Stream)\s*\(/.test(e) ? t.some((e) => {
3292
- if (!Pn.has(e.getExpression().getText())) return !1;
3293
- let t = Fn(e);
3291
+ }, Ln = (e, t) => /define(Query|Mutation|Action|Stream)\s*\(/.test(e) ? t.some((e) => {
3292
+ if (!Fn.has(e.getExpression().getText())) return !1;
3293
+ let t = In(e);
3294
3294
  return !t.has("internal") && !t.has("guards") && !t.has("openAccess");
3295
- }) : !1, Ln = C({
3295
+ }) : !1, Rn = C({
3296
3296
  version: "0.34.0",
3297
3297
  id: "0.34.0/03_procedure-access-decision",
3298
3298
  title: "Every wire-exposed procedure declares an access decision (`guards:` or `openAccess:`)",
3299
3299
  kind: "manual",
3300
- appliesTo: ({ project: e }) => e.getSourceFiles().some((e) => In(e.getFullText(), e.getDescendantsOfKind(b.CallExpression))),
3300
+ appliesTo: ({ project: e }) => e.getSourceFiles().some((e) => Ln(e.getFullText(), e.getDescendantsOfKind(b.CallExpression))),
3301
3301
  note: [
3302
3302
  "YOUR APP WILL NOT BOOT UNTIL EVERY WIRE-EXPOSED PROCEDURE DECIDES WHO MAY",
3303
3303
  "CALL IT. This is the single largest behaviour change in this release, and it",
@@ -3342,18 +3342,18 @@ var x = (e, t, n) => {
3342
3342
  "procedures your PLUGINS declare — the gate reads your app's own discovered",
3343
3343
  "files only, so adopting this does not turn into a plugin bug report."
3344
3344
  ].join("\n")
3345
- }), Rn = [
3345
+ }), zn = [
3346
3346
  "workosAuthorizationUrl",
3347
3347
  "workosAuthenticateWithCode",
3348
3348
  "workosBeginLogin"
3349
- ], zn = C({
3349
+ ], Bn = C({
3350
3350
  version: "0.34.0",
3351
3351
  id: "0.34.0/04_workos-oauth-state-pkce",
3352
3352
  title: "WorkOS hosted login: `workosAuthorizationUrl` → `workosBeginLogin`, with mandatory `state` + PKCE",
3353
3353
  kind: "manual",
3354
3354
  appliesTo: ({ project: e }) => e.getSourceFiles().some((e) => {
3355
3355
  let t = e.getFullText();
3356
- return t.includes("@voltro/plugin-auth-workos") && Rn.some((e) => t.includes(e));
3356
+ return t.includes("@voltro/plugin-auth-workos") && zn.some((e) => t.includes(e));
3357
3357
  }),
3358
3358
  note: [
3359
3359
  "YOUR WORKOS HOSTED-LOGIN FLOW HAD NO CSRF PROTECTION BY DEFAULT, AND NO PKCE.",
@@ -3411,12 +3411,12 @@ var x = (e, t, n) => {
3411
3411
  "are accepted on the existing endpoints; your registered redirect URI is",
3412
3412
  "unchanged."
3413
3413
  ].join("\n")
3414
- }), Bn = /mirrorTables|_?voltro_mirror_/, Vn = C({
3414
+ }), Vn = /mirrorTables|_?voltro_mirror_/, Hn = C({
3415
3415
  version: "0.34.0",
3416
3416
  id: "0.34.0/05_analytics-mirror-versioned-writes",
3417
3417
  title: "Analytics CDC-mirror: version-ordered writes, tombstoned deletes, retry + repair",
3418
3418
  kind: "manual",
3419
- appliesTo: ({ project: e }) => e.getSourceFiles().some((e) => Bn.test(e.getFullText())),
3419
+ appliesTo: ({ project: e }) => e.getSourceFiles().some((e) => Vn.test(e.getFullText())),
3420
3420
  note: [
3421
3421
  "The analytics CDC-mirror (`mirrorTables`) now orders every write and",
3422
3422
  "retries the ones that fail. Three things to do.",
@@ -3471,7 +3471,7 @@ var x = (e, t, n) => {
3471
3471
  " VOLTRO_ANALYTICS_MIRROR_REPAIR_INTERVAL_MS (60000, 0 disables)",
3472
3472
  " VOLTRO_ANALYTICS_MIRROR_REPAIR_QUEUE_LIMIT (10000)"
3473
3473
  ].join("\n")
3474
- }), Hn = C({
3474
+ }), Un = C({
3475
3475
  version: "0.34.0",
3476
3476
  id: "0.34.0/06_origin-guard-and-trusted-proxies",
3477
3477
  title: "Cross-site protection, trusted proxies and security headers are ON by default on the api listener",
@@ -3595,7 +3595,7 @@ var x = (e, t, n) => {
3595
3595
  " a `Transfer-Encoding: chunked` body skipped it and was buffered without",
3596
3596
  " bound. No action needed; VOLTRO_MAX_RPC_BODY_BYTES still tunes it."
3597
3597
  ].join("\n")
3598
- }), Un = C({
3598
+ }), Wn = C({
3599
3599
  version: "0.34.0",
3600
3600
  id: "0.34.0/07_incoming-webhook-declares-verification",
3601
3601
  title: "An incoming webhook with no signature scheme now refuses to boot",
@@ -3644,7 +3644,7 @@ var x = (e, t, n) => {
3644
3644
  "value — the sender holds the other half of it, so a generated one would",
3645
3645
  "authenticate nobody."
3646
3646
  ].join("\n")
3647
- }), Wn = C({
3647
+ }), Gn = C({
3648
3648
  version: "0.34.0",
3649
3649
  id: "0.34.0/08_init-initialises-the-workspace",
3650
3650
  title: "`voltro init` initialises the workspace root; scaffolding is `create-project` only",
@@ -3696,20 +3696,20 @@ var x = (e, t, n) => {
3696
3696
  "all 18 of its scripts — including the `pnpm db:up` the CLI then printed as a",
3697
3697
  "next step. Run `voltro init` in the root first."
3698
3698
  ].join("\n")
3699
- }), Gn = (e, t) => (e.getProperty(t)?.asKind(b.PropertyAssignment))?.getInitializer()?.asKind(b.ObjectLiteralExpression), Kn = (e) => {
3699
+ }), Kn = (e, t) => (e.getProperty(t)?.asKind(b.PropertyAssignment))?.getInitializer()?.asKind(b.ObjectLiteralExpression), qn = (e) => {
3700
3700
  if (e.getExpression().getText() !== "workflow") return !1;
3701
3701
  let t = e.getArguments()[0]?.asKind(b.ObjectLiteralExpression);
3702
3702
  if (t === void 0) return !1;
3703
- let n = Gn(t, "messages");
3703
+ let n = Kn(t, "messages");
3704
3704
  return n !== void 0 && n.getProperty("queries") !== void 0;
3705
- }, qn = C({
3705
+ }, Jn = C({
3706
3706
  version: "0.34.0",
3707
3707
  id: "0.34.0/09_workflow-messages-queries-removed",
3708
3708
  title: "`workflow({ messages: { queries } })` is removed — it never had a send path",
3709
3709
  kind: "manual",
3710
3710
  appliesTo: ({ project: e }) => e.getSourceFiles().some((e) => {
3711
3711
  let t = e.getFullText();
3712
- return !t.includes("queries") || !/workflow\s*\(/.test(t) ? !1 : e.getDescendantsOfKind(b.CallExpression).some(Kn);
3712
+ return !t.includes("queries") || !/workflow\s*\(/.test(t) ? !1 : e.getDescendantsOfKind(b.CallExpression).some(qn);
3713
3713
  }),
3714
3714
  note: [
3715
3715
  "YOU DECLARED `messages: { queries: { … } }` ON A WORKFLOW. That channel is",
@@ -3752,7 +3752,7 @@ var x = (e, t, n) => {
3752
3752
  "in-flight runs finish on the old path after a deploy. See the Versioning",
3753
3753
  "page in the docs."
3754
3754
  ].join("\n")
3755
- }), Jn = C({
3755
+ }), Yn = C({
3756
3756
  version: "0.34.0",
3757
3757
  id: "0.34.0/11_ai-flows-per-step-human-signal",
3758
3758
  title: "`HUMAN_RESPONSE_SIGNAL` removed — a human step's signal name is now per-step",
@@ -3796,23 +3796,23 @@ var x = (e, t, n) => {
3796
3796
  "per flow (`humanTimeoutMs`), per app (`aiFlowsPlugin({ humanReviewTimeoutMs })`)",
3797
3797
  "or per deploy (`VOLTRO_AI_FLOW_HUMAN_REVIEW_TIMEOUT_HOURS`). `0` = wait forever."
3798
3798
  ].join("\n")
3799
- }), Yn = /* @__PURE__ */ new Set([
3799
+ }), Xn = /* @__PURE__ */ new Set([
3800
3800
  "@voltro/plugin-auth",
3801
3801
  "@voltro/protocol/session",
3802
3802
  "@voltro/plugin-sso-saml"
3803
- ]), Xn = /* @__PURE__ */ new Set(["issueSession", "signSession"]), Zn = (e) => {
3803
+ ]), Zn = /* @__PURE__ */ new Set(["issueSession", "signSession"]), Qn = (e) => {
3804
3804
  let t = e.getFullText();
3805
3805
  if (!/issueSession|signSession|resolveScopes|@voltro\/(plugin-auth|protocol\/session|plugin-sso-saml)/.test(t)) return !1;
3806
- for (let t of e.getImportDeclarations()) if (Yn.has(t.getModuleSpecifierValue())) return !0;
3807
- for (let t of e.getDescendantsOfKind(b.CallExpression)) if (Xn.has(t.getExpression().getText())) return !0;
3806
+ for (let t of e.getImportDeclarations()) if (Xn.has(t.getModuleSpecifierValue())) return !0;
3807
+ for (let t of e.getDescendantsOfKind(b.CallExpression)) if (Zn.has(t.getExpression().getText())) return !0;
3808
3808
  for (let t of e.getDescendantsOfKind(b.PropertyAssignment)) if (t.getName() === "resolveScopes") return !0;
3809
3809
  return !1;
3810
- }, Qn = C({
3810
+ }, $n = C({
3811
3811
  version: "0.34.0",
3812
3812
  id: "0.34.0/12_session-cookie-carries-identity",
3813
3813
  title: "The session cookie carries identity only — authority resolves per request",
3814
3814
  kind: "manual",
3815
- appliesTo: ({ project: e }) => e.getSourceFiles().some(Zn),
3815
+ appliesTo: ({ project: e }) => e.getSourceFiles().some(Qn),
3816
3816
  note: [
3817
3817
  "YOUR USERS WILL SIGN IN ONCE MORE, AND A SESSION COOKIE CAN NO LONGER",
3818
3818
  "CARRY PERMISSIONS.",
@@ -3903,12 +3903,12 @@ var x = (e, t, n) => {
3903
3903
  "not have: an authoritative resolver now overrides a token's own scope",
3904
3904
  "claim, which union-only could never do."
3905
3905
  ].join("\n")
3906
- }), $n = /\bMockEmail\b|\.email\.(sent|lastTo|clear)\b|\bSentEmail\b/, er = C({
3906
+ }), er = /\bMockEmail\b|\.email\.(sent|lastTo|clear)\b|\bSentEmail\b/, tr = C({
3907
3907
  version: "0.34.0",
3908
3908
  id: "0.34.0/10_testing-mail-is-a-service",
3909
3909
  title: "`ctx.email` / `MockEmail` removed — assert mail through the mail plugin",
3910
3910
  kind: "manual",
3911
- appliesTo: ({ project: e }) => e.getSourceFiles().some((e) => $n.test(e.getFullText())),
3911
+ appliesTo: ({ project: e }) => e.getSourceFiles().some((e) => er.test(e.getFullText())),
3912
3912
  note: [
3913
3913
  "`@voltro/testing` no longer ships `MockEmail`, and `makeTestContext()` no",
3914
3914
  "longer returns `ctx.email`. Your tests that touch it will not compile.",
@@ -3977,12 +3977,12 @@ var x = (e, t, n) => {
3977
3977
  " • `makeTestContext({ layers })` — your app's own `app.config.ts`",
3978
3978
  " `layers:` now resolve for an Effect-mode handler under test."
3979
3979
  ].join("\n")
3980
- }), tr = /from\s+['"]@voltro\/plugin-billing(?:\/[\w-]+)?['"]/, nr = C({
3980
+ }), nr = /from\s+['"]@voltro\/plugin-billing(?:\/[\w-]+)?['"]/, rr = C({
3981
3981
  version: "0.34.0",
3982
3982
  id: "0.34.0/13_billing-provider-dunning",
3983
3983
  title: "Billing dunning — `BillingProvider.fetchSubscription` is required, and `plan()` now honours a grace period",
3984
3984
  kind: "manual",
3985
- appliesTo: ({ project: e }) => e.getSourceFiles().some((e) => tr.test(e.getFullText())),
3985
+ appliesTo: ({ project: e }) => e.getSourceFiles().some((e) => nr.test(e.getFullText())),
3986
3986
  note: [
3987
3987
  "BILLING GAINED A DUNNING SEQUENCE. TWO THINGS CHANGED UNDER YOU.",
3988
3988
  "",
@@ -4058,12 +4058,12 @@ var x = (e, t, n) => {
4058
4058
  "`pastDueSince` / `statusEventAt` columns ride the declarative differ — they",
4059
4059
  "are applied by `voltro db apply` and by a `voltro dev` boot, on every dialect."
4060
4060
  ].join("\n")
4061
- }), rr = /from\s+['"]@voltro\/plugin-search(?:\/[\w-]+)?['"]/, ir = C({
4061
+ }), ir = /from\s+['"]@voltro\/plugin-search(?:\/[\w-]+)?['"]/, ar = C({
4062
4062
  version: "0.34.0",
4063
4063
  id: "0.34.0/14_search-memory-backend-refuses-production",
4064
4064
  title: "`plugin-search` on the in-memory backend now refuses to boot in production",
4065
4065
  kind: "manual",
4066
- appliesTo: ({ project: e }) => e.getSourceFiles().some((e) => rr.test(e.getFullText())),
4066
+ appliesTo: ({ project: e }) => e.getSourceFiles().some((e) => ir.test(e.getFullText())),
4067
4067
  note: [
4068
4068
  "THE ZERO-CONFIG SEARCH BACKEND NO LONGER BOOTS IN PRODUCTION.",
4069
4069
  "",
@@ -4132,7 +4132,7 @@ var x = (e, t, n) => {
4132
4132
  "(`ts_headline` / `snippet()` / …) that `SearchQuery` promises. Degrading",
4133
4133
  "those silently would replace one lie with another."
4134
4134
  ].join("\n")
4135
- }), ar = C({
4135
+ }), or = C({
4136
4136
  version: "0.34.0",
4137
4137
  id: "0.34.0/15_mysql-mssql-tls-honoured",
4138
4138
  title: "mysql/mariadb and mssql connections honour ?ssl= / ?sslmode= — or refuse to boot",
@@ -4204,7 +4204,7 @@ var x = (e, t, n) => {
4204
4204
  "spins in-tick and starves the event loop. Set it only if you know nothing in",
4205
4205
  "your process does that, and set it well above their peak concurrency."
4206
4206
  ].join("\n")
4207
- }), or = C({
4207
+ }), sr = C({
4208
4208
  version: "0.34.0",
4209
4209
  id: "0.34.0/16_declare-node-env-for-migration-commands",
4210
4210
  title: "`voltro db apply` / `voltro migrate` now treat an unset NODE_ENV as production",
@@ -4268,12 +4268,12 @@ var x = (e, t, n) => {
4268
4268
  "process environment if you want a non-production `voltro serve` without a",
4269
4269
  "built bundle. Everything downstream of the launcher does honour `.env`."
4270
4270
  ].join("\n")
4271
- }), sr = /\/(?:src\/)?pages\//, cr = /\bcacheInvalidatesOn\b/, lr = C({
4271
+ }), cr = /\/(?:src\/)?pages\//, lr = /\bcacheInvalidatesOn\b/, ur = C({
4272
4272
  version: "0.34.0",
4273
4273
  id: "0.34.0/17_isr-postgres-refuses-without-a-database",
4274
4274
  title: "The postgres ISR cache + CDC invalidator read DB_URL, and no longer fall back silently",
4275
4275
  kind: "manual",
4276
- appliesTo: ({ project: e }) => e.getSourceFiles().some((e) => cr.test(e.getFullText()) || sr.test(e.getFilePath())),
4276
+ appliesTo: ({ project: e }) => e.getSourceFiles().some((e) => lr.test(e.getFullText()) || cr.test(e.getFilePath())),
4277
4277
  note: [
4278
4278
  "YOUR WEB PROCESS MAY HAVE BEEN USING THE MEMORY ISR CACHE ALL ALONG.",
4279
4279
  "",
@@ -4318,12 +4318,12 @@ var x = (e, t, n) => {
4318
4318
  " until their `revalidate` window expires. Give the web process a `DB_URL`",
4319
4319
  " or remove the declaration."
4320
4320
  ].join("\n")
4321
- }), ur = /\.migration\.ts$/, dr = C({
4321
+ }), dr = /\.migration\.ts$/, fr = C({
4322
4322
  version: "0.34.0",
4323
4323
  id: "0.34.0/18_serve-refuses-pending-file-migrations",
4324
4324
  title: "`voltro serve` refuses to boot with pending file-based migrations",
4325
4325
  kind: "manual",
4326
- appliesTo: ({ project: e }) => e.getSourceFiles().some((e) => ur.test(e.getFilePath())),
4326
+ appliesTo: ({ project: e }) => e.getSourceFiles().some((e) => dr.test(e.getFilePath())),
4327
4327
  note: [
4328
4328
  "YOUR DEPLOY MUST RUN `migrations/*.migration.ts` BEFORE `voltro serve`.",
4329
4329
  "",
@@ -4366,11 +4366,11 @@ var x = (e, t, n) => {
4366
4366
  "fingerprint check — it is the one switch for \"no boot-time schema checks\",",
4367
4367
  "and it now covers both."
4368
4368
  ].join("\n")
4369
- }), fr = /* @__PURE__ */ new Set([
4369
+ }), pr = /* @__PURE__ */ new Set([
4370
4370
  "@voltro/plugin-auth",
4371
4371
  "@voltro/plugin-auth/schema",
4372
4372
  "@voltro/plugin-auth/plugin"
4373
- ]), pr = /* @__PURE__ */ new Set([
4373
+ ]), mr = /* @__PURE__ */ new Set([
4374
4374
  "UserStore",
4375
4375
  "authTables",
4376
4376
  "SendEmail",
@@ -4378,20 +4378,20 @@ var x = (e, t, n) => {
4378
4378
  "memoryUserStore",
4379
4379
  "postgresUserStore",
4380
4380
  "authRoutesPlugin"
4381
- ]), mr = (e) => {
4381
+ ]), hr = (e) => {
4382
4382
  if (!e.getFullText().includes("@voltro/plugin-auth")) return !1;
4383
- for (let t of e.getImportDeclarations()) if (fr.has(t.getModuleSpecifierValue())) {
4384
- for (let e of t.getNamedImports()) if (pr.has(e.getName())) return !0;
4383
+ for (let t of e.getImportDeclarations()) if (pr.has(t.getModuleSpecifierValue())) {
4384
+ for (let e of t.getNamedImports()) if (mr.has(e.getName())) return !0;
4385
4385
  if (t.getNamespaceImport() !== void 0) return !0;
4386
4386
  }
4387
4387
  for (let t of e.getDescendantsOfKind(b.TypeReference)) if (t.getTypeName().getText() === "UserStore") return !0;
4388
4388
  return !1;
4389
- }, hr = C({
4389
+ }, gr = C({
4390
4390
  version: "0.34.0",
4391
4391
  id: "0.34.0/20_user-store-auth-feature-methods",
4392
4392
  title: "UserStore gained the email-verification, invitation and impersonation methods",
4393
4393
  kind: "manual",
4394
- appliesTo: ({ project: e }) => e.getSourceFiles().some(mr),
4394
+ appliesTo: ({ project: e }) => e.getSourceFiles().some(hr),
4395
4395
  note: [
4396
4396
  "IF YOU USE memoryUserStore OR postgresUserStore, THERE IS NOTHING TO DO.",
4397
4397
  "Both already implement everything below. Read on only if you wrote your",
@@ -4471,26 +4471,26 @@ var x = (e, t, n) => {
4471
4471
  " is the difference between \"new signups must confirm\" and \"everyone is",
4472
4472
  " locked out until they check their mail\"."
4473
4473
  ].join("\n")
4474
- }), K = /* @__PURE__ */ new Set([
4474
+ }), _r = /* @__PURE__ */ new Set([
4475
4475
  "bindConnectionSubject",
4476
4476
  "getConnectionSubject",
4477
4477
  "unbindConnectionSubject",
4478
4478
  "onBindConnectionSubject",
4479
4479
  "connectionSubjectsSnapshot"
4480
- ]), gr = (e) => {
4480
+ ]), vr = (e) => {
4481
4481
  let t = e.getFullText();
4482
4482
  if (!/ConnectionSubject|connectionSubjectsSnapshot|rebind/.test(t)) return !1;
4483
- for (let t of e.getDescendantsOfKind(b.CallExpression)) if (K.has(t.getExpression().getText())) return !0;
4484
- for (let t of e.getImportDeclarations()) for (let e of t.getNamedImports()) if (K.has(e.getName())) return !0;
4483
+ for (let t of e.getDescendantsOfKind(b.CallExpression)) if (_r.has(t.getExpression().getText())) return !0;
4484
+ for (let t of e.getImportDeclarations()) for (let e of t.getNamedImports()) if (_r.has(e.getName())) return !0;
4485
4485
  for (let t of e.getDescendantsOfKind(b.PropertyAssignment)) if (t.getName() === "rebind") return !0;
4486
4486
  for (let t of e.getDescendantsOfKind(b.ShorthandPropertyAssignment)) if (t.getName() === "rebind") return !0;
4487
4487
  return !1;
4488
- }, _r = C({
4488
+ }, yr = C({
4489
4489
  version: "0.34.0",
4490
4490
  id: "0.34.0/19_connection-override-carries-a-credential",
4491
4491
  title: "Soft re-auth presents a credential — it no longer hands the server a Subject",
4492
4492
  kind: "manual",
4493
- appliesTo: ({ project: e }) => e.getSourceFiles().some(gr),
4493
+ appliesTo: ({ project: e }) => e.getSourceFiles().some(vr),
4494
4494
  note: [
4495
4495
  "A LIVE CONNECTION CAN NO LONGER BE HANDED A SUBJECT. IT PRESENTS A",
4496
4496
  "CREDENTIAL, AND THE SERVER RESOLVES IT LIKE ANY OTHER REQUEST.",
@@ -4575,19 +4575,19 @@ var x = (e, t, n) => {
4575
4575
  "it behaves identically under `voltro dev` and `voltro serve`, because both",
4576
4576
  "now build the middleware from one function (`makeAuthMiddlewareLayers`)."
4577
4577
  ].join("\n")
4578
- }), vr = (e) => {
4578
+ }), br = (e) => {
4579
4579
  if (/\.workflow(\.server)?\.tsx?$/.test(e.getBaseName())) return !0;
4580
4580
  let t = e.getFullText();
4581
4581
  if (!/resolveScopes|defineWorkflow|ctx\.workflows/.test(t)) return !1;
4582
4582
  for (let t of e.getDescendantsOfKind(b.PropertyAssignment)) if (t.getName() === "resolveScopes") return !0;
4583
4583
  for (let t of e.getDescendantsOfKind(b.CallExpression)) if (t.getExpression().getText() === "defineWorkflow") return !0;
4584
4584
  return !1;
4585
- }, yr = C({
4585
+ }, xr = C({
4586
4586
  version: "0.34.0",
4587
4587
  id: "0.34.0/20_workflow-authority-resolves-at-resume",
4588
4588
  title: "A resumed workflow re-resolves its caller's authority instead of restoring it",
4589
4589
  kind: "manual",
4590
- appliesTo: ({ project: e }) => e.getSourceFiles().some(vr),
4590
+ appliesTo: ({ project: e }) => e.getSourceFiles().some(br),
4591
4591
  note: [
4592
4592
  "A WORKFLOW NO LONGER CARRIES ITS CALLER'S PERMISSIONS FORWARD. IT CARRIES",
4593
4593
  "THEIR IDENTITY, AND ASKS YOUR APP WHAT THEY MAY DO WHEN IT RUNS.",
@@ -4652,7 +4652,7 @@ var x = (e, t, n) => {
4652
4652
  " contain scopes and are stripped on READ — a resumed run cannot re-assert",
4653
4653
  " authority the old build persisted."
4654
4654
  ].join("\n")
4655
- }), br = /* @__PURE__ */ new Set([
4655
+ }), Sr = /* @__PURE__ */ new Set([
4656
4656
  "listTag",
4657
4657
  "createTag",
4658
4658
  "updateTag",
@@ -4660,21 +4660,21 @@ var x = (e, t, n) => {
4660
4660
  "createScope",
4661
4661
  "writeScope",
4662
4662
  "deleteScope"
4663
- ]), xr = (e) => {
4663
+ ]), Cr = (e) => {
4664
4664
  let t = e.getFullText();
4665
4665
  if (!/EntityAdminSpec|deriveEntityAdmins|Tag\b|Scope\b/.test(t)) return !1;
4666
4666
  for (let t of e.getImportDeclarations()) for (let e of t.getNamedImports()) {
4667
4667
  let t = e.getName();
4668
4668
  if (t === "EntityAdminSpec" || t === "deriveEntityAdmins") return !0;
4669
4669
  }
4670
- for (let t of e.getDescendantsOfKind(b.PropertyAccessExpression)) if (br.has(t.getName())) return !0;
4670
+ for (let t of e.getDescendantsOfKind(b.PropertyAccessExpression)) if (Sr.has(t.getName())) return !0;
4671
4671
  return !1;
4672
- }, Sr = C({
4672
+ }, wr = C({
4673
4673
  version: "0.34.0",
4674
4674
  id: "0.34.0/21_admin-gates-on-declared-access",
4675
4675
  title: "The derived admin gates on the api's DECLARED access — the invented `<table>:create` scopes are gone",
4676
4676
  kind: "manual",
4677
- appliesTo: ({ project: e }) => e.getSourceFiles().some(xr),
4677
+ appliesTo: ({ project: e }) => e.getSourceFiles().some(Cr),
4678
4678
  note: [
4679
4679
  "YOUR ADMIN WAS GATING ON SCOPE STRINGS NOTHING PRODUCED.",
4680
4680
  "",
@@ -4764,14 +4764,14 @@ var x = (e, t, n) => {
4764
4764
  "from \"undeclared\", so an intentionally public action rendered as forbidden",
4765
4765
  "to everybody."
4766
4766
  ].join("\n")
4767
- }), Cr = /\b(useMutation|useAction|useWorkflow|useWorkflowSignal|useWorkflowUpdate|useUpload)\b/, wr = /FiberFailure|Cause\.squash|squashWith/, Tr = C({
4767
+ }), Tr = /\b(useMutation|useAction|useWorkflow|useWorkflowSignal|useWorkflowUpdate|useUpload)\b/, Er = /FiberFailure|Cause\.squash|squashWith/, Dr = C({
4768
4768
  version: "0.34.0",
4769
4769
  id: "0.34.0/22_write-rejections-carry-the-typed-error",
4770
4770
  title: "A rejected mutate/run/signal/upload carries the typed error, not a FiberFailure",
4771
4771
  kind: "manual",
4772
4772
  appliesTo: ({ project: e }) => e.getSourceFiles().some((e) => {
4773
4773
  let t = e.getFullText();
4774
- return Cr.test(t) || wr.test(t);
4774
+ return Tr.test(t) || Er.test(t);
4775
4775
  }),
4776
4776
  note: [
4777
4777
  "WHAT CHANGED. A write hook that rejects now rejects with the typed error",
@@ -4824,12 +4824,12 @@ var x = (e, t, n) => {
4824
4824
  "contract, reject from a real rpc client, or assert on the value your",
4825
4825
  "component branches on rather than on the literal the test wrote."
4826
4826
  ].join("\n")
4827
- }), Er = /@voltro\/plugin-sso-saml|samlSsoPlugin/, Dr = C({
4827
+ }), Or = /@voltro\/plugin-sso-saml|samlSsoPlugin/, kr = C({
4828
4828
  version: "0.34.0",
4829
4829
  id: "0.34.0/23_saml-signature-and-replay-defaults",
4830
4830
  title: "SAML: response-signature requirement relaxed + surfaced; replay protection ON by default",
4831
4831
  kind: "manual",
4832
- appliesTo: ({ project: e }) => e.getSourceFiles().some((e) => Er.test(e.getFullText())),
4832
+ appliesTo: ({ project: e }) => e.getSourceFiles().some((e) => Or.test(e.getFullText())),
4833
4833
  note: [
4834
4834
  "Two defaults in `samlSsoPlugin` moved. Read both — they interact.",
4835
4835
  "",
@@ -4920,24 +4920,24 @@ var x = (e, t, n) => {
4920
4920
  "request, and the envelope is unauthenticated. If you must run unsolicited,",
4921
4921
  "set `wantAuthnResponseSigned: true`."
4922
4922
  ].join("\n")
4923
- }), Or = /\b(signPayload|verifySignature)\s*\(/, kr = C({
4923
+ }), Ar = /\b(signPayload|verifySignature)\s*\(/, jr = C({
4924
4924
  version: "0.34.0",
4925
4925
  id: "0.34.0/24_webhook-signing-renders-headers",
4926
4926
  title: "Webhook signing: `signPayload`/`verifySignature` → `signRequest`/`verifyRequest`",
4927
4927
  kind: "manual",
4928
- appliesTo: ({ project: e }) => e.getSourceFiles().some((e) => Or.test(e.getFullText())),
4928
+ appliesTo: ({ project: e }) => e.getSourceFiles().some((e) => Ar.test(e.getFullText())),
4929
4929
  note: "A webhook signature scheme now renders a header MAP rather than one header,\nand verifies against the request's whole header map.\n\n - signPayload(scheme, rawBody, secret)\n → signRequest(scheme, { rawBody, secret, messageId })\n returns { headers } instead of { headerName, headerValue }\n\n - verifySignature(scheme, rawBody, secret, headerValue)\n → verifyRequest(scheme, { rawBody, secret, headers })\n\nRewrite each call site:\n\n // before\n const { headerName, headerValue } = signPayload(scheme, body, secret)\n await fetch(url, { headers: { [headerName]: headerValue }, body })\n\n // after\n const { headers } = signRequest(scheme, { rawBody: body, secret, messageId: deliveryId })\n await fetch(url, { headers, body })\n\nWHY `messageId` IS REQUIRED, AND WHY THIS IS NOT AUTOMATED:\n\n Standard Webhooks signs over `<messageId>.<timestamp>.<body>` and sends the\n id as `webhook-id` — which is also the value the spec tells consumers to\n use as their IDEMPOTENCY KEY. Pass the id that identifies the message to\n THAT receiver, stable across retries (the framework passes its delivery\n id). A fresh `randomUUID()` per attempt compiles and reads fine, and\n quietly makes every retry look like a new message to the receiver.\n\nTHE DEFAULT OUTGOING SCHEME CHANGED:\n\n `defaultOutgoingSignature()` now returns Standard Webhooks v1.0.0 —\n `webhook-id` / `webhook-timestamp` / `webhook-signature: v1,<base64>` — so\n a NEW subscription signs to an interoperable spec every conformant\n consumer library already verifies. Existing target rows are untouched:\n each stores the scheme it was created with.\n\n Its key serialization differs too: `whsec_` + base64 of 24..64 random\n bytes, HMAC-ing the DECODED bytes. `subscribe()` mints the right shape\n automatically; passing your own hex secret for a standard-webhooks target\n is REFUSED at subscribe rather than producing signatures no consumer\n accepts.\n\n To keep the previous house format on a new subscription, pass it\n explicitly: `webhooks.subscribe({ …, signing: genericHmacSignature() })`\n — the same shape as before, under a name that says it is a choice.\n\nALSO IN THIS RELEASE, no code change needed:\n\n - a 3xx response is a failure and redirects are no longer FOLLOWED (they\n were, by fetch's default — which also walked past the SSRF guard);\n - a `410 Gone` disables the target immediately, whatever the\n consecutive-failure threshold says;\n - each attempt has a 30s wire timeout (`VOLTRO_WEBHOOK_TIMEOUT_MS`, or\n `timeoutMs`). There was none, so a receiver that accepted the connection\n and never answered held the delivery workflow open indefinitely.\n\nAnd `slackSignature()` is now a REAL verifier. It shipped as a stub that\nreturned false unconditionally, workable only through a header-packing\nconvention in the route mounter. A scheme now receives the whole header map\nand reads `x-slack-request-timestamp` itself."
4930
- }), Ar = /from\s+['"]@voltro\/plugin-presence(?:\/[a-z]+)?['"]/, jr = /\b(lastSeen|presenceTable|memoryPresenceStore|isOnline|filterOnline|staleKeys|PresenceStore|PresenceEntry|PresenceTracker)\b/, Mr = /_voltro_presence/, Nr = C({
4930
+ }), Mr = /from\s+['"]@voltro\/plugin-presence(?:\/[a-z]+)?['"]/, Nr = /\b(lastSeen|presenceTable|memoryPresenceStore|isOnline|filterOnline|staleKeys|PresenceStore|PresenceEntry|PresenceTracker)\b/, Pr = /_voltro_presence/, Fr = C({
4931
4931
  version: "0.34.0",
4932
4932
  id: "0.34.0/25_presence-has-no-table",
4933
4933
  title: "Presence: `_voltro_presence` is gone, and so is `lastSeen` on the roster",
4934
4934
  kind: "manual",
4935
4935
  appliesTo: ({ project: e }) => e.getSourceFiles().some((e) => {
4936
4936
  let t = e.getFullText();
4937
- return Mr.test(t) || Ar.test(t) && jr.test(t);
4937
+ return Pr.test(t) || Mr.test(t) && Nr.test(t);
4938
4938
  }),
4939
4939
  note: "@voltro/plugin-presence no longer declares a table. Four things moved.\n\n1. A roster member is now { key, meta } — there is NO `lastSeen`.\n\n It was the OWNING replica's clock, so on more than one replica \"active 3\n minutes ago\" rendered from it was wrong by the skew between two pods, with\n nothing to say so. It was also the only field a plain heartbeat changed,\n which is what made the roster push on every beat instead of on every change.\n\n // before\n members.map((m) => `${m.key} — ${ago(m.lastSeen)}`)\n // after: put anything time-shaped you need into meta\n usePresence(channel, { meta: { joinedAt: Date.now() } })\n members.map((m) => `${m.key} — ${ago(m.meta?.joinedAt)}`)\n\n2. `PresenceTracker.track()` and `.untrack()` return { delta, changed }.\n\n // before\n bus.announce(tracker.track({ channel, tenantId, key }))\n // after — announce ALWAYS (a peer's sweep needs the liveness signal),\n // push only on a change\n const write = tracker.track({ channel, tenantId, key })\n bus.announce(write.delta)\n if (write.changed) publishReactivity(store, presenceRoster)\n\n Do not blind-append `.delta` and move on: dropping `changed` restores the\n per-heartbeat push this release removed, and nothing will report it.\n\n `attachPresenceBus` also takes an optional `onRosterChanged` now — call it\n if you drive the bus yourself, or a peer's join and a peer's death will\n update your tracker and push nobody.\n\n3. These exports are DELETED, with no replacement:\n\n presenceTable, PresenceStore, PresenceEntry,\n memoryPresenceStore, isOnline, filterOnline, staleKeys\n\n They were the table era's read model — an abstraction over rows plus the\n staleness filter a row needed because it outlived the client that wrote it.\n Presence has neither. Reading the roster is `presence.list` / `usePresence`;\n the staleness sweep is the plugin's own and needs nothing from you.\n\n4. If your `source:` names '_voltro_presence' as a string, point it at the\n channel object instead:\n\n import { presenceRoster } from '@voltro/plugin-presence'\n defineQuery({ name: 'room.summary', source: presenceRoster, ... })\n\n And to push it yourself:\n\n import { publishReactivity } from '@voltro/protocol'\n publishReactivity(ctx.store, presenceRoster)\n\nTHE TABLE IS NOT DROPPED FOR YOU. The declarative differ never plans a drop for\na framework table no app declares — deliberately, so a live `_voltro_*` table is\nnever destroyed by an app that simply does not know about it. So an existing\n`_voltro_presence` stays, empty, until you remove it:\n\n DROP TABLE IF EXISTS _voltro_presence;\n\nIt holds no data anyone can use: nothing has written a row to it since presence\nmoved into memory."
4940
- }), Pr = (e) => {
4940
+ }), Ir = (e) => {
4941
4941
  let t = e.getArguments()[0];
4942
4942
  if (t === void 0 || !t.isKind(b.ObjectLiteralExpression)) return /* @__PURE__ */ new Set();
4943
4943
  let n = /* @__PURE__ */ new Set();
@@ -4946,32 +4946,32 @@ var x = (e, t, n) => {
4946
4946
  t !== void 0 && n.add(t);
4947
4947
  }
4948
4948
  return n;
4949
- }, Fr = (e, t) => /defineEvent\s*\(/.test(e) ? t.some((e) => {
4949
+ }, Lr = (e, t) => /defineEvent\s*\(/.test(e) ? t.some((e) => {
4950
4950
  if (e.getExpression().getText() !== "defineEvent") return !1;
4951
- let t = Pr(e);
4951
+ let t = Ir(e);
4952
4952
  return !t.has("guards") && !t.has("openAccess");
4953
- }) : !1, Ir = C({
4953
+ }) : !1, Rr = C({
4954
4954
  version: "0.35.0",
4955
4955
  id: "0.35.0/01_event-access-decision",
4956
4956
  title: "Every declared event decides who may listen (`guards:` or `openAccess:`)",
4957
4957
  kind: "manual",
4958
- appliesTo: ({ project: e }) => e.getSourceFiles().some((e) => Fr(e.getFullText(), e.getDescendantsOfKind(b.CallExpression))),
4958
+ appliesTo: ({ project: e }) => e.getSourceFiles().some((e) => Lr(e.getFullText(), e.getDescendantsOfKind(b.CallExpression))),
4959
4959
  note: (/* @__PURE__ */ "YOUR APP WILL NOT BOOT UNTIL EVERY DECLARED EVENT DECIDES WHO MAY LISTEN.(This closes the sibling of 0.34.0's procedure gate: `defineEvent`'s `guards:`(was optional and enforcement skipped an empty list, so an event with no(decision was silently subscribable by ANY authenticated session that could(open the socket.((WHAT TO DO, IN THIS ORDER:((1) LIST THEM. `voltro doctor` prints every undecided event with its tag and( file — the same set, from the same function, that the boot refuses on.( `voltro doctor --json` carries it as `accessDecisions.undecided` for CI.((2) DECIDE EACH ONE. There are exactly two answers and they are not the same( claim:(( guards: [{ scope: 'display:read' }] a subscriber must hold a scope( openAccess: 'public scoreboard' anyone may listen — and why(( `openAccess` takes a REASON, not a boolean. It is what a reviewer reads( later, and it is what makes \"we decided this is open\" distinguishable( from \"nobody looked\".((3) DO NOT RUBBER-STAMP. Reaching for a scope every caller already holds( satisfies the gate, reads as protection, and enforces nothing. If the( event is open, say `openAccess`. If it should not be open, this gate just( found a real hole — an event often carries exactly the data its table( guards behind a scoped query.((IF YOU NEED TO SHIP FIRST. The same field that covers procedures covers(events, in `app.config.ts`:(( export default { security: { defaultDeny: false } }((That restores the old default-allow for the WHOLE app, in one place a(reviewer can see. There is no env var for it, on purpose. `voltro doctor`(keeps listing the undecided events while it is off, marked advisory.((NOT AFFECTED: apps already running `security: { defaultDeny: false }`, and(events your PLUGINS declare — the gate reads your app's own discovered(`*.event.ts` files only.".split("(")).join("\n")
4960
- }), Lr = /\brouteHttp\s*\(/, Rr = C({
4960
+ }), zr = /\brouteHttp\s*\(/, Br = C({
4961
4961
  version: "0.34.0",
4962
4962
  id: "0.34.0/26_route-http-is-async",
4963
4963
  title: "`routeHttp` returns a Promise",
4964
4964
  kind: "manual",
4965
- appliesTo: ({ project: e }) => e.getSourceFiles().some((e) => Lr.test(e.getFullText())),
4965
+ appliesTo: ({ project: e }) => e.getSourceFiles().some((e) => zr.test(e.getFullText())),
4966
4966
  note: "`routeHttp` (@voltro/mcp) is async now:\n\n routeHttp(options): HttpOutcome // before\n routeHttp(options): Promise<HttpOutcome> // after\n\nAwait it, and make the enclosing function async if it is not already:\n\n // before\n const outcome = routeHttp({ method, url, body, tools })\n if (outcome.kind === 'json') respond(outcome)\n\n // after\n const outcome = await routeHttp({ method, url, body, tools })\n if (outcome.kind === 'json') respond(outcome)\n\nWhy it moved: `tools/list` now folds in the app's agent tools and `tools/call`\nexecutes one, so two of the methods reach the app. The rest still resolve without\ntouching anything, and the function is still pure in the sense that matters — it\nbinds no socket, and a test supplies a `live` double.\n\nIf you use the shipped `serveHttp` transport there is nothing to do; it already\nawaits."
4967
- }), zr = /@voltro\/plugin-(billing|governance|storage)/, Br = /* @__PURE__ */ RegExp("\\b(billing\\.(startCheckout|portalUrl|previewChange|changePlan|changeSeats|invoices|reportUsage)|governance\\.(export|erase)|storage\\.(mintUploadUrl|ingestUrl|listRefs))\\b"), Vr = /\bdefinePluginRoute\s*\(|\broutes\s*:\s*\[/, Hr = C({
4967
+ }), Vr = /@voltro\/plugin-(billing|governance|storage)/, Hr = /* @__PURE__ */ RegExp("\\b(billing\\.(startCheckout|portalUrl|previewChange|changePlan|changeSeats|invoices|reportUsage)|governance\\.(export|erase)|storage\\.(mintUploadUrl|ingestUrl|listRefs))\\b"), Ur = /\bdefinePluginRoute\s*\(|\broutes\s*:\s*\[/, Wr = C({
4968
4968
  version: "0.35.0",
4969
4969
  id: "0.35.0/02_plugin-procedures-carry-access-decisions",
4970
4970
  title: "First-party plugin procedures carry access decisions; twelve now require a scope",
4971
4971
  kind: "manual",
4972
4972
  appliesTo: ({ project: e }) => e.getSourceFiles().some((e) => {
4973
4973
  let t = e.getFullText();
4974
- return zr.test(t) || Br.test(t) || Vr.test(t) && /VoltroPlugin|definePlugin/.test(t);
4974
+ return Vr.test(t) || Hr.test(t) || Ur.test(t) && /VoltroPlugin|definePlugin/.test(t);
4975
4975
  }),
4976
4976
  note: [
4977
4977
  "Every first-party plugin rpc route now declares an access decision, and the",
@@ -5036,12 +5036,12 @@ var x = (e, t, n) => {
5036
5036
  "`openAccess` with the reason each is safe (self-scoped rows, signed",
5037
5037
  "subject-bound tickets, per-object grants), and behave exactly as before."
5038
5038
  ].join("\n")
5039
- }), Ur = /AnalyticsMirrorImpl/, Wr = C({
5039
+ }), Gr = /AnalyticsMirrorImpl/, Kr = C({
5040
5040
  version: "0.35.0",
5041
5041
  id: "0.35.0/03_analytics-mirror-max-version",
5042
5042
  title: "Custom analytics mirror sinks answer `maxVersion` (fleet-scope seed read)",
5043
5043
  kind: "manual",
5044
- appliesTo: ({ project: e }) => e.getSourceFiles().some((e) => Ur.test(e.getFullText())),
5044
+ appliesTo: ({ project: e }) => e.getSourceFiles().some((e) => Gr.test(e.getFullText())),
5045
5045
  note: [
5046
5046
  "The analytics CDC-mirror now derives its version from the CHANGE under",
5047
5047
  "`changeScope: 'fleet'` (postgres CDC, mysql binlog): warehouse baseline +",
@@ -5066,37 +5066,95 @@ var x = (e, t, n) => {
5066
5066
  "",
5067
5067
  "The read is issued at most once per key per process, off the commit path."
5068
5068
  ].join("\n")
5069
- }), Gr = (e) => /RunStepStatus/.test(e) || /['"]succeeded['"]/.test(e) && /['"]waiting['"]/.test(e) && /RunStep|runStep|flow/i.test(e), Kr = C({
5069
+ }), qr = (e) => /RunStepStatus/.test(e) || /['"]succeeded['"]/.test(e) && /['"]waiting['"]/.test(e) && /RunStep|runStep|flow/i.test(e), Jr = C({
5070
5070
  version: "0.35.0",
5071
5071
  id: "0.35.0/04_flow-run-step-skipped",
5072
5072
  title: "A flow run step can now be 'skipped' — handle it where you render a run timeline",
5073
5073
  kind: "manual",
5074
- appliesTo: ({ project: e }) => e.getSourceFiles().some((e) => Gr(e.getFullText())),
5074
+ appliesTo: ({ project: e }) => e.getSourceFiles().some((e) => qr(e.getFullText())),
5075
5075
  note: (/* @__PURE__ */ "A FLOW RUN STEP CAN NOW BE 'skipped'.((`when:` on a flow step gates it on a prior value, and a step whose condition(is false is SKIPPED rather than run. That is a sixth member of(`RunStepStatus`, so an exhaustive switch over it no longer compiles, and a(lookup keyed by status now has a hole.((WHAT TO DO:((1) FIND IT. Anywhere you render or branch on a run step status — a timeline( component, a status badge map, a progress calculation.((2) DECIDE WHAT A SKIPPED STEP LOOKS LIKE. It is NOT a failure and NOT a( success: it did not run, on purpose, and it produced no output. The( step carries `skipReason` — the rendered condition, e.g.( `{{mode}} equals \"full\"` — so the timeline can say WHY.((3) DO NOT DEFAULT IT TO INVISIBLE. Dropping the row compiles and reads fine( in review, and it reproduces exactly the problem `skipReason` exists to( prevent: a step that vanished is indistinguishable from a step nobody( declared, which is the first thing a user reports as a bug.((A PROGRESS COUNT probably needs a second look too: `succeeded / total` now(under-reports a completed run that skipped a step. Counting(`succeeded + skipped` against the total is usually what was meant.((If your app does not render a flow timeline, there is nothing to do — this(note fires on any file that names the status type or its literals.".split("(")).join("\n")
5076
- }), qr = C({
5076
+ }), Yr = C({
5077
5077
  version: "0.35.0",
5078
5078
  id: "0.35.0/05_declared-schema-drops-runtime-flags",
5079
5079
  title: "The declared schema no longer reads `CDC` / `VOLTRO_UNDO` / `VOLTRO_TRACING_PERSIST`",
5080
5080
  kind: "manual",
5081
5081
  appliesTo: () => !0,
5082
5082
  note: (/* @__PURE__ */ "The set of framework tables your app DECLARES is hashed into the schema(fingerprint. A pre-deploy `voltro db apply` records that hash; every serving(container recomputes it and refuses to boot if it differs. Until this release(three RUNTIME flags fed into that set:(( CDC=0 removed `_voltro_cdc_offsets`( VOLTRO_UNDO=on added `_voltro_undo_log`( VOLTRO_TRACING_PERSIST=all added `_voltro_traces`((All three read as pure runtime knobs, so they tend to live on the PODS and(not on the pre-upgrade migrate Job — which is a green migrate followed by(every pod in CrashLoopBackOff. This was reported against a Helm deployment(where `CDC: \"0\"` was in all three overlays' pod blocks and none of their(jobs; it had been latent for months and would have fired on the next deploy.((WHAT YOU MUST DO — only if you set one of these to turn a table ON in(production:(( * `VOLTRO_UNDO=on` in production → add to app.config.ts:( export default { schema: { undo: true }, … }( * `VOLTRO_TRACING_PERSIST` keeping trace history in production → add:( export default { schema: { traces: true }, … }(( Without the declaration, `voltro serve` now REFUSES to boot and names the( field. That refusal is the point: before, the same configuration either( wrote to a table nobody created or produced a fingerprint the migrate job( could not reproduce.(( Turning capture OFF still needs no declaration and never will — a declared( table nobody writes to is harmless. Only turning it ON is a schema claim.((WHAT HAPPENS WITHOUT ANY ACTION:(( * On mariadb or mssql you will see ONE additive `create-table` in your next( plan: `_voltro_cdc_offsets` is now declared by the DIALECT rather than by( `CDC`, so it exists whether or not that process drives CDC. It is an( offsets table — a handful of rows — and the alternative was leaving a( runtime flag able to split your fleet.( * On postgres/sqlite nothing changes: those dialects have no offsets table.((AND ONE THING WORTH DOING ANYWAY: if you are upgrading a deployment that has(a separate migrate job, diff that job's `env:` against your pods' once. The(framework can now guarantee its OWN tables do not depend on a runtime flag;(it cannot make the same guarantee for a plugin's `extendSchema.tables`, which(is app code and may read anything. A `prod-mismatch` refusal now prints which(tables this process decided and from which input, so the comparison is quick.".split("(")).join("\n")
5083
- }), q = "session_expired", Jr = C({
5083
+ }), G = "session_expired", Xr = C({
5084
5084
  version: "0.35.1",
5085
5085
  id: "0.35.1/01_unauthorized-replaces-session-expired",
5086
5086
  title: "A 401 is `code: 'unauthorized'`; the vault's own failure is `'credential_unusable'`",
5087
5087
  kind: "transform",
5088
- appliesTo: ({ project: e }) => e.getSourceFiles().some((e) => e.getFullText().includes(q)),
5088
+ appliesTo: ({ project: e }) => e.getSourceFiles().some((e) => e.getFullText().includes(G)),
5089
5089
  transform: ({ project: e, log: t }) => {
5090
5090
  let n = [];
5091
5091
  for (let t of e.getSourceFiles()) {
5092
- if (!t.getFullText().includes(q)) continue;
5092
+ if (!t.getFullText().includes(G)) continue;
5093
5093
  let e = !1;
5094
- for (let n of t.getDescendantsOfKind(11)) n.getLiteralValue() === q && (n.setLiteralValue("unauthorized"), e = !0);
5094
+ for (let n of t.getDescendantsOfKind(11)) n.getLiteralValue() === G && (n.setLiteralValue("unauthorized"), e = !0);
5095
5095
  e && n.push(t.getFilePath());
5096
5096
  }
5097
- n.length !== 0 && t(`rewrote '${q}' to 'unauthorized' in ${n.length} file(s).\n A 401 now means only "the credential was refused" — expired, revoked, wrong\n scope and MALFORMED all produce that status, and the old name asserted the\n first one. If a rewritten branch was meant to catch a credential your\n CONNECTION VAULT could not produce (no grant, revoked grant, failed refresh),\n change it to 'credential_unusable' — that is the code the vault raises, and it\n is the one where re-auth is genuinely the right action.\n A health check that logs the user out on this code should read 'unauthorized'\n as "ask again", not as "the session is gone".\n` + n.map((e) => ` ${e}`).join("\n"));
5097
+ n.length !== 0 && t(`rewrote '${G}' to 'unauthorized' in ${n.length} file(s).\n A 401 now means only "the credential was refused" — expired, revoked, wrong\n scope and MALFORMED all produce that status, and the old name asserted the\n first one. If a rewritten branch was meant to catch a credential your\n CONNECTION VAULT could not produce (no grant, revoked grant, failed refresh),\n change it to 'credential_unusable' — that is the code the vault raises, and it\n is the one where re-auth is genuinely the right action.\n A health check that logs the user out on this code should read 'unauthorized'\n as "ask again", not as "the session is gone".\n` + n.map((e) => ` ${e}`).join("\n"));
5098
5098
  }
5099
- }), Yr = /\(\s*\{[^}]*\.\.\./, Xr = /\bquery\??\.?\(\s*['"`]/, J = [
5099
+ }), Zr = /\(\s*\{[^}]*\.\.\./, Qr = /\bquery\??\.?\(\s*['"`]/, $r = C({
5100
+ version: "0.37.0",
5101
+ id: "0.37.0/01_procedure-input-rejects-undeclared-fields",
5102
+ title: "a procedure input rejects a field it does not declare (it used to drop it)",
5103
+ kind: "manual",
5104
+ appliesTo: ({ project: e }) => e.getSourceFiles().some((e) => {
5105
+ let t = e.getFullText();
5106
+ return Zr.test(t) || Qr.test(t) || w("query").test(t);
5107
+ }),
5108
+ note: [
5109
+ "A procedure call whose input carries a field the schema does not declare",
5110
+ "now fails with a ParseError naming the field. Before this release the",
5111
+ "field was DISCARDED and the call ran with what was left.",
5112
+ "",
5113
+ "The measurement that caused it — a consumer's root layout:",
5114
+ "",
5115
+ " query?.('userSettings.list', { employeeId })",
5116
+ "",
5117
+ "That procedure declares `userId` / `userIdIn`. The payload decoded to `{}`,",
5118
+ "which for a LIST query is not a narrower filter — it is NO filter. An admin",
5119
+ "was served a different user's settings row, and nothing logged anything.",
5120
+ "",
5121
+ "WHAT TO DO",
5122
+ "",
5123
+ "1. Run your test suite and your app against a dev build. A wrong field now",
5124
+ " surfaces as a ParseError that NAMES the key and lists the accepted ones,",
5125
+ " so each one is a one-line read rather than an investigation.",
5126
+ "",
5127
+ "2. Look at every SPREAD into a procedure input:",
5128
+ "",
5129
+ " await api.orders.list({ ...filters, limit })",
5130
+ "",
5131
+ " This is the construct the change is aimed at. The keys come from a value,",
5132
+ " so nobody reading the call site can see what is being sent — which is",
5133
+ " exactly the condition under which a stray key survived unnoticed.",
5134
+ "",
5135
+ "3. If a spread legitimately carries more than the procedure declares, narrow",
5136
+ " it at the call site (`{ status: filters.status, limit }`) rather than",
5137
+ " widening the input schema. Widening restores the silent drop under a",
5138
+ " different name: the field is accepted and still does nothing.",
5139
+ "",
5140
+ "WHAT DOES NOT CHANGE",
5141
+ "",
5142
+ " • A DECLARED field, including every optional one, decodes exactly as",
5143
+ " before. An empty input to a procedure that declares none is still fine.",
5144
+ " • Nested objects and union members follow the same rule — that is the",
5145
+ " point, since a filter object one level down is the commoner spelling.",
5146
+ " • A non-object input (a scalar, a void input) is unaffected; there is no",
5147
+ " excess property for it to have.",
5148
+ "",
5149
+ "WHY IT IS NOT A WARNING",
5150
+ "",
5151
+ "The decoder cannot tell a projection field from a FILTER field. Dropping an",
5152
+ "unknown `include` costs a caller some data; dropping an unknown `tenantId`",
5153
+ "hands them somebody else's. Since nothing at decode time distinguishes the",
5154
+ "two, the only safe direction is to refuse — and a warning would have to be",
5155
+ "read by someone, in a log, after the wrong rows were already served."
5156
+ ].join("\n")
5157
+ }), K = "@voltro/plugin-auth", q = "@voltro/protocol", J = "IMPERSONATION_METADATA_KEY", ei = [
5100
5158
  ge,
5101
5159
  _e,
5102
5160
  ye,
@@ -5157,104 +5215,159 @@ var x = (e, t, n) => {
5157
5215
  un,
5158
5216
  fn,
5159
5217
  mn,
5160
- yn,
5161
- xn,
5162
- Cn,
5163
- Tn,
5164
- Dn,
5218
+ bn,
5219
+ Sn,
5220
+ wn,
5221
+ En,
5165
5222
  On,
5166
5223
  kn,
5167
5224
  An,
5168
5225
  jn,
5169
- Nn,
5170
- Ln,
5171
- zn,
5172
- Vn,
5226
+ Mn,
5227
+ Pn,
5228
+ Rn,
5229
+ Bn,
5173
5230
  Hn,
5174
5231
  Un,
5175
5232
  Wn,
5176
- qn,
5177
- er,
5233
+ Gn,
5178
5234
  Jn,
5179
- Qn,
5180
- nr,
5181
- ir,
5235
+ tr,
5236
+ Yn,
5237
+ $n,
5238
+ rr,
5182
5239
  ar,
5183
5240
  or,
5184
- lr,
5185
- dr,
5186
- _r,
5241
+ sr,
5242
+ ur,
5243
+ fr,
5187
5244
  yr,
5188
- hr,
5189
- Sr,
5190
- Tr,
5245
+ xr,
5246
+ gr,
5247
+ wr,
5191
5248
  Dr,
5192
5249
  kr,
5193
- Nr,
5250
+ jr,
5251
+ Fr,
5252
+ Br,
5194
5253
  Rr,
5195
- Ir,
5196
- Hr,
5197
5254
  Wr,
5198
5255
  Kr,
5199
- qr,
5200
5256
  Jr,
5257
+ Yr,
5258
+ Xr,
5259
+ $r,
5260
+ C({
5261
+ version: "0.38.0",
5262
+ id: "0.38.0/01_impersonation-key-in-protocol",
5263
+ title: `\`${J}\` is imported from @voltro/protocol`,
5264
+ kind: "transform",
5265
+ appliesTo: ({ project: e }) => e.getSourceFiles().some((e) => e.getImportDeclarations().some((e) => e.getModuleSpecifierValue() === K && e.getNamedImports().some((e) => e.getName() === J))),
5266
+ transform: ({ project: e, log: t }) => {
5267
+ let n = 0;
5268
+ for (let t of e.getSourceFiles()) for (let e of t.getImportDeclarations()) {
5269
+ if (e.getModuleSpecifierValue() !== K) continue;
5270
+ let r = e.getNamedImports().find((e) => e.getName() === J);
5271
+ if (r === void 0) continue;
5272
+ let i = r.getAliasNode()?.getText(), a = e.isTypeOnly() || r.isTypeOnly();
5273
+ r.remove();
5274
+ let o = t.getImportDeclarations().find((e) => e.getModuleSpecifierValue() === q && e.isTypeOnly() === a), s = {
5275
+ name: J,
5276
+ ...i === void 0 ? {} : { alias: i }
5277
+ };
5278
+ o === void 0 ? t.addImportDeclaration({
5279
+ moduleSpecifier: q,
5280
+ isTypeOnly: a,
5281
+ namedImports: [s]
5282
+ }) : o.addNamedImport(s), e.getNamedImports().length === 0 && e.getDefaultImport() === void 0 && e.getNamespaceImport() === void 0 && e.remove(), n += 1;
5283
+ }
5284
+ n > 0 && t(`${J}: ${n} import(s) repointed to ${q}`);
5285
+ }
5286
+ }),
5201
5287
  C({
5202
- version: "0.37.0",
5203
- id: "0.37.0/01_procedure-input-rejects-undeclared-fields",
5204
- title: "a procedure input rejects a field it does not declare (it used to drop it)",
5288
+ version: "0.38.0",
5289
+ id: "0.38.0/02_startup-failure-refuses-boot",
5290
+ title: "a `*.startup.ts` that fails refuses the boot (it used to warn and continue)",
5205
5291
  kind: "manual",
5206
- appliesTo: ({ project: e }) => e.getSourceFiles().some((e) => {
5207
- let t = e.getFullText();
5208
- return Yr.test(t) || Xr.test(t) || w("query").test(t);
5209
- }),
5292
+ appliesTo: ({ project: e }) => e.getSourceFiles().some((e) => /\.startup\.tsx?$/.test(e.getFilePath())),
5210
5293
  note: [
5211
- "A procedure call whose input carries a field the schema does not declare",
5212
- "now fails with a ParseError naming the field. Before this release the",
5213
- "field was DISCARDED and the call ran with what was left.",
5294
+ "A `*.startup.ts` whose function throws or rejects now ABORTS the boot,",
5295
+ "naming the file and the cause. Before this release it logged a warning and",
5296
+ "the server came up without whatever the startup was there to install.",
5297
+ "",
5298
+ "WHY IT CHANGED",
5214
5299
  "",
5215
- "The measurement that caused it a consumer's root layout:",
5300
+ "A startup is the documented place to REGISTER things the request path",
5301
+ "depends on — `setRowFilter` (row-level security) above all. We measured a",
5302
+ "startup that threw on its first line and produced this:",
5216
5303
  "",
5217
- " query?.('userSettings.list', { employeeId })",
5304
+ " warn startup: function rejected",
5305
+ " info startup: registered <- next line, same file",
5218
5306
  "",
5219
- "That procedure declares `userId` / `userIdIn`. The payload decoded to `{}`,",
5220
- "which for a LIST query is not a narrower filter — it is NO filter. An admin",
5221
- "was served a different user's settings row, and nothing logged anything.",
5307
+ "and then a server that answered every request with no row filter",
5308
+ "registered. The two lines contradict each other and the second asserts",
5309
+ "exactly the thing that had just failed.",
5310
+ "",
5311
+ "The runner cannot tell a registration from a background loop, and the two",
5312
+ "outcomes are not symmetric: \"the app refuses to boot\" is fixed in seconds",
5313
+ "and everyone sees it; \"the app serves without its access control\" is seen",
5314
+ "by nobody.",
5222
5315
  "",
5223
5316
  "WHAT TO DO",
5224
5317
  "",
5225
- "1. Run your test suite and your app against a dev build. A wrong field now",
5226
- " surfaces as a ParseError that NAMES the key and lists the accepted ones,",
5227
- " so each one is a one-line read rather than an investigation.",
5318
+ "1. Boot each app once. A startup that was failing silently now says so, and",
5319
+ " the message names the file. This is the whole migration for most apps ",
5320
+ " a startup that worked keeps working.",
5321
+ "",
5322
+ "2. If a startup CAN fail and that is acceptable — a best-effort cache warm,",
5323
+ " an optional metrics bridge — catch it yourself:",
5324
+ "",
5325
+ " export default async (ctx) => {",
5326
+ " try {",
5327
+ " await warmTheCache(ctx.store)",
5328
+ " } catch (err) {",
5329
+ " ctx.log.warn('cache warm failed; serving cold', {}, err)",
5330
+ " }",
5331
+ " }",
5228
5332
  "",
5229
- "2. Look at every SPREAD into a procedure input:",
5333
+ " The catch belongs at the site where somebody decided the failure was",
5334
+ " acceptable, where a reviewer can see the decision. That is the ONLY",
5335
+ " reason this is not a flag: a flag would move the decision away from the",
5336
+ " startup it applies to and make it one setting for all of them.",
5230
5337
  "",
5231
- " await api.orders.list({ ...filters, limit })",
5338
+ "TWO MORE WAYS A STARTUP USED TO VANISH, now also refusals",
5232
5339
  "",
5233
- " This is the construct the change is aimed at. The keys come from a value,",
5234
- " so nobody reading the call site can see what is being sent — which is",
5235
- " exactly the condition under which a stray key survived unnoticed.",
5340
+ " A startup file that cannot be IMPORTED (a bad import path, a syntax",
5341
+ " error) used to warn and skip the file.",
5342
+ " A startup file with no DEFAULT-exported function used to warn and skip.",
5343
+ " The convention is `export default async (ctx) => {…}` — a named export",
5344
+ " is discovered and never runs, which is indistinguishable from a startup",
5345
+ " that ran and did nothing.",
5236
5346
  "",
5237
- "3. If a spread legitimately carries more than the procedure declares, narrow",
5238
- " it at the call site (`{ status: filters.status, limit }`) rather than",
5239
- " widening the input schema. Widening restores the silent drop under a",
5240
- " different name: the field is accepted and still does nothing.",
5347
+ "THE OTHER HALF: A STARTUP MUST RETURN",
5241
5348
  "",
5242
- "WHAT DOES NOT CHANGE",
5349
+ "A startup function that never returns now refuses the boot too, after",
5350
+ "`VOLTRO_STARTUP_TIMEOUT_MS` (60s default). Awaiting forever used to be",
5351
+ "allowed and documented, and it is what made a slow-then-failing startup",
5352
+ "unreportable: the boot moved on after 2 seconds, so a rejection arriving",
5353
+ "later had nothing left to refuse.",
5243
5354
  "",
5244
- " A DECLARED field, including every optional one, decodes exactly as",
5245
- " before. An empty input to a procedure that declares none is still fine.",
5246
- " • Nested objects and union members follow the same rule — that is the",
5247
- " point, since a filter object one level down is the commoner spelling.",
5248
- " • A non-object input (a scalar, a void input) is unaffected; there is no",
5249
- " excess property for it to have.",
5355
+ "Start the work and RETURN; hand the teardown to `onShutdown`:",
5250
5356
  "",
5251
- "WHY IT IS NOT A WARNING",
5357
+ " export default async ({ store, log, onShutdown }) => {",
5358
+ " const timer = setInterval(() => sync(store), 60_000)",
5359
+ " onShutdown(() => clearInterval(timer))",
5360
+ " }",
5252
5361
  "",
5253
- "The decoder cannot tell a projection field from a FILTER field. Dropping an",
5254
- "unknown `include` costs a caller some data; dropping an unknown `tenantId`",
5255
- "hands them somebody else's. Since nothing at decode time distinguishes the",
5256
- "two, the only safe direction is to refuse — and a warning would have to be",
5257
- "read by someone, in a log, after the wrong rows were already served."
5362
+ "Every startup this framework ships already has that shape, so most apps",
5363
+ "need no change. If yours awaits a fiber until shutdown, move the await",
5364
+ "behind the return.",
5365
+ "",
5366
+ "A startup that is legitimately SLOW is unaffected it is waited for, and",
5367
+ "it registers when it finishes. Raise VOLTRO_STARTUP_TIMEOUT_MS if a real",
5368
+ "backfill needs more than a minute, or (better) register first and do the",
5369
+ "long work after, so a failure in the long half cannot hide a missing",
5370
+ "registration."
5258
5371
  ].join("\n")
5259
5372
  })
5260
5373
  ], Y = /* @__PURE__ */ new Set([
@@ -5269,12 +5382,12 @@ var x = (e, t, n) => {
5269
5382
  ".turbo",
5270
5383
  ".cache",
5271
5384
  ".output"
5272
- ]), Zr = () => [...[...Y].map((e) => `**/${e}/**`), "**/.voltro-*/**"], X = /(?:from|import)\s*\(?\s*['"]([^'"]+)['"]|require\s*\(\s*['"]([^'"]+)['"]\s*\)/g, Z = /\.(?:tsx?|jsx?|mjs|cjs)$/, Qr = (e) => m(e).replace(Z, ""), $r = (e) => {
5385
+ ]), ti = () => [...[...Y].map((e) => `**/${e}/**`), "**/.voltro-*/**"], X = /(?:from|import)\s*\(?\s*['"]([^'"]+)['"]|require\s*\(\s*['"]([^'"]+)['"]\s*\)/g, ni = /\.(?:tsx?|jsx?|mjs|cjs)$/, ri = (e) => m(e).replace(ni, ""), ii = (e) => {
5273
5386
  let t = e.split("/").filter((e) => e.length > 0).pop();
5274
5387
  if (t === void 0 || t === "." || t === "..") return;
5275
- let n = t.replace(Z, "");
5388
+ let n = t.replace(ni, "");
5276
5389
  return n.length > 0 ? n : void 0;
5277
- }, ei = (e) => {
5390
+ }, ai = (e) => {
5278
5391
  let t = /* @__PURE__ */ new Map();
5279
5392
  for (let n of e) {
5280
5393
  let e;
@@ -5285,55 +5398,55 @@ var x = (e, t, n) => {
5285
5398
  }
5286
5399
  X.lastIndex = 0;
5287
5400
  for (let r = X.exec(e); r !== null; r = X.exec(e)) {
5288
- let e = $r(r[1] ?? r[2] ?? "");
5401
+ let e = ii(r[1] ?? r[2] ?? "");
5289
5402
  if (e === void 0) continue;
5290
5403
  let i = t.get(e);
5291
5404
  i === void 0 ? t.set(e, /* @__PURE__ */ new Set([n])) : i.add(n);
5292
5405
  }
5293
5406
  }
5294
5407
  return t;
5295
- }, ti = (e, t) => {
5408
+ }, oi = (e, t) => {
5296
5409
  let n = new Set(e);
5297
- for (let r of e) for (let e of [Qr(r), m(h(r))]) {
5410
+ for (let r of e) for (let e of [ri(r), m(h(r))]) {
5298
5411
  let r = t.get(e);
5299
5412
  if (r !== void 0) for (let e of r) n.add(e);
5300
5413
  }
5301
5414
  return n;
5302
- }, ni = [
5415
+ }, si = [
5303
5416
  "**/*.ts",
5304
5417
  "**/*.tsx",
5305
5418
  "!**/node_modules/**",
5306
5419
  "!**/dist/**",
5307
5420
  "!**/.framework/**",
5308
5421
  "!**/*.d.ts"
5309
- ], Q = (e, t, n = ai) => {
5422
+ ], Z = (e, t, n = ui) => {
5310
5423
  let r = /* @__PURE__ */ new Set();
5311
- for (let e of t) for (let t of e.scope ?? ni) r.add(t);
5424
+ for (let e of t) for (let t of e.scope ?? si) r.add(t);
5312
5425
  let i = n(e);
5313
5426
  if (i !== void 0) {
5314
- let t = [...r].filter((e) => !e.startsWith("!")).map((e) => ii(e)), n = [...r].filter((e) => e.startsWith("!")).map((e) => ii(e.slice(1))), a = [...Y], o = [];
5427
+ let t = [...r].filter((e) => !e.startsWith("!")).map((e) => li(e)), n = [...r].filter((e) => e.startsWith("!")).map((e) => li(e.slice(1))), a = [...Y], o = [];
5315
5428
  for (let r of i) {
5316
5429
  if (a.some((e) => r === e || r.startsWith(`${e}/`) || r.includes(`/${e}/`)) || !t.some((e) => e(r)) || n.some((e) => e(r))) continue;
5317
5430
  let i = g(e, r);
5318
5431
  v(i) && o.push(i);
5319
5432
  }
5320
- return ri(o.length, e), o;
5433
+ return ci(o.length, e), o;
5321
5434
  }
5322
5435
  let a = oe([...r], {
5323
5436
  cwd: e,
5324
5437
  absolute: !0,
5325
5438
  expandDirectories: !1,
5326
5439
  followSymbolicLinks: !1,
5327
- ignore: [...Zr()]
5440
+ ignore: [...ti()]
5328
5441
  });
5329
- return ri(a.length, e), a;
5330
- }, ri = (e, t) => {
5331
- let n = oi();
5442
+ return ci(a.length, e), a;
5443
+ }, ci = (e, t) => {
5444
+ let n = di();
5332
5445
  if (!(e <= n)) throw Error(`voltro update: the codemod scan matched ${e} files under ${t}, past the ${n} ceiling.\n Every matched file is parsed into one ts-morph project, so the heap needed grows with\n this number — past the ceiling it runs out of memory, and V8 aborts without naming a\n limit, which is why this stops here instead. Three ways out, in order:\n • put non-source directories in .gitignore (inside a git repo the scan then never\n sees them at all — a build cache, a data dump, a virtualenv),\n • run \`voltro update\` from the app directory rather than the workspace root, or\n • take one codemod at a time: \`voltro update --dry-run\` lists the ids, then\n \`voltro update --codemods-only --only <id>\` runs a single one.\n If every one of those files really is source this pass must rewrite, raise the ceiling\n with VOLTRO_CODEMOD_MAX_FILES and give node room: NODE_OPTIONS=--max-old-space-size=8192.`);
5333
- }, ii = (e) => {
5446
+ }, li = (e) => {
5334
5447
  let t = e.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*\//g, "\0").replace(/\*\*/g, ".*").replace(/\*/g, "[^/]*").replace(/\u0000/g, "(?:.*/)?").replace(/\\\{([^}]*)\\\}/g, (e, t) => `(?:${t.split(",").join("|")})`);
5335
5448
  return (e) => RegExp(`^${t}$`).test(e);
5336
- }, ai = (e) => {
5449
+ }, ui = (e) => {
5337
5450
  try {
5338
5451
  let t = re("git", [
5339
5452
  "ls-files",
@@ -5354,17 +5467,17 @@ var x = (e, t, n) => {
5354
5467
  } catch {
5355
5468
  return;
5356
5469
  }
5357
- }, oi = () => {
5470
+ }, di = () => {
5358
5471
  let e = Number(process.env.VOLTRO_CODEMOD_MAX_FILES);
5359
5472
  return Number.isInteger(e) && e > 0 ? e : 2e4;
5360
- }, $ = (e, t) => {
5473
+ }, Q = (e, t) => {
5361
5474
  let n = e.split(".").map(Number), r = t.split(".").map(Number);
5362
5475
  for (let e = 0; e < 3; e++) {
5363
5476
  let t = (n[e] ?? 0) - (r[e] ?? 0);
5364
5477
  if (t !== 0) return t;
5365
5478
  }
5366
5479
  return 0;
5367
- }, si = (e, t, n = J) => n.filter((n) => $(e, n.version) < 0 && $(n.version, t) <= 0).slice().sort(he), ci = (e, t, n = () => {}) => {
5480
+ }, fi = (e, t, n = ei) => n.filter((n) => Q(e, n.version) < 0 && Q(n.version, t) <= 0).slice().sort(he), $ = (e, t, n = () => {}) => {
5368
5481
  let r = [], i = [], a = [];
5369
5482
  for (let o of t) {
5370
5483
  if (!(!o.appliesTo || o.appliesTo({ project: e }))) {
@@ -5410,8 +5523,8 @@ var x = (e, t, n) => {
5410
5523
  manual: i,
5411
5524
  skipped: a
5412
5525
  };
5413
- }, li = async (e) => {
5414
- let t = e.log ?? (() => {}), n = si(e.from, e.to, e.codemods ?? J), r = e.only === void 0 || e.only.length === 0 ? n : (() => {
5526
+ }, pi = async (e) => {
5527
+ let t = e.log ?? (() => {}), n = fi(e.from, e.to, e.codemods ?? ei), r = e.only === void 0 || e.only.length === 0 ? n : (() => {
5415
5528
  let t = n.filter((t) => e.only.includes(t.id)), r = e.only.filter((e) => !n.some((t) => t.id === e));
5416
5529
  if (r.length > 0) throw Error(`voltro update --only: no codemod in ${e.from} → ${e.to} has id ${r.map((e) => `"${e}"`).join(", ")}.\n Available for this jump:\n${n.map((e) => ` ${e.id}`).join("\n")}`);
5417
5530
  return t;
@@ -5430,15 +5543,15 @@ var x = (e, t, n) => {
5430
5543
  baseUrl: i.baseUrl,
5431
5544
  paths: i.paths
5432
5545
  } }
5433
- }), o, s = Q(e.root, r), c = r.filter((e) => e.needsImporters === !0 && e.scope !== void 0), l = [], u = [], d = [], p = c.length > 0 ? ei(Q(e.root, [{
5546
+ }), o, s = Z(e.root, r), c = r.filter((e) => e.needsImporters === !0 && e.scope !== void 0), l = [], u = [], d = [], p = c.length > 0 ? ai(Z(e.root, [{
5434
5547
  ...c[0],
5435
- scope: ni
5548
+ scope: si
5436
5549
  }])) : void 0;
5437
5550
  for (let n of r) {
5438
5551
  if (c.includes(n)) {
5439
- let r = a(), i = Q(e.root, [n]);
5440
- for (let e of ti(i, p)) v(e) && r.addSourceFileAtPath(e);
5441
- let s = ci(r, [n], t);
5552
+ let r = a(), i = Z(e.root, [n]);
5553
+ for (let e of oi(i, p)) v(e) && r.addSourceFileAtPath(e);
5554
+ let s = $(r, [n], t);
5442
5555
  e.dryRun || await r.save(), l.push(...s.applied), u.push(...s.manual), d.push(...s.skipped), o = void 0;
5443
5556
  continue;
5444
5557
  }
@@ -5447,7 +5560,7 @@ var x = (e, t, n) => {
5447
5560
  for (let t of s) v(t) && e.addSourceFileAtPath(t);
5448
5561
  return e;
5449
5562
  })();
5450
- let r = ci(o, [n], t);
5563
+ let r = $(o, [n], t);
5451
5564
  e.dryRun || await o.save(), l.push(...r.applied), u.push(...r.manual), d.push(...r.skipped);
5452
5565
  }
5453
5566
  return {
@@ -5457,4 +5570,4 @@ var x = (e, t, n) => {
5457
5570
  };
5458
5571
  };
5459
5572
  //#endregion
5460
- export { C as i, li as n, Y as r, $ as t };
5573
+ export { C as i, pi as n, Y as r, Q as t };