@voltro/cli 0.30.1 → 0.30.2
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.
- package/CHANGELOG.md +48 -0
- package/dist/{apiBuild-DmkzRyv7.js → apiBuild-BUS1RfPi.js} +2 -2
- package/dist/apiBuild-DMlxvL8h.js +2 -0
- package/dist/bin.js +3 -3
- package/dist/checkCommand-CARZ36Tr.js +2 -0
- package/dist/{checkCommand-C4konUy9.js → checkCommand-D_wdOFhw.js} +346 -293
- package/dist/{commands-BkJft9K2.js → commands-BgGAUcku.js} +1680 -1680
- package/dist/{dbCommand-qn__kLEt.js → dbCommand-CbPRCD1x.js} +56 -19
- package/dist/dbCommand-Ht47LrV-.js +2 -0
- package/dist/{dev-C_NPcLDp.js → dev-GAOIot9G.js} +2169 -2153
- package/dist/{dev-DXhhZ7-f.js → dev-tPcOmjPJ.js} +1 -1
- package/dist/{frameworkTableAssembly-Cpgn2e88.js → frameworkTableAssembly-BwIrO5nv.js} +122 -103
- package/dist/frameworkTableAssembly-D-EebUQX.js +2 -0
- package/dist/index.js +1 -1
- package/dist/{inspectMetrics-CkB2J0Zn.js → inspectMetrics-BM1vrsga.js} +314 -314
- package/dist/{sdkgen-wsWLrq4l.js → sdkgen-BV4fFucu.js} +1 -1
- package/dist/{serveCommand-HtI4X5kj.js → serveCommand-ClqXCX1y.js} +17 -16
- package/dist/serveEntry.js +2 -2
- package/dist/{start-egkbpugc.js → start-CTnNvKlk.js} +348 -348
- package/dist/startEntry.js +2 -2
- package/package.json +17 -17
- package/templates/AGENTS.md +1 -1
- package/templates/agent-docs/_index.md +1 -1
- package/templates/agent-docs/cli.md +4 -2
- package/templates/agent-docs/internationalization.md +25 -3
- package/templates/agent-docs/whats-new.md +24 -47
- package/templates/apps/api-ai/package.json +7 -7
- package/templates/apps/api-auth/package.json +8 -8
- package/templates/apps/api-backend/package.json +7 -7
- package/templates/apps/api-backend-deactivation/package.json +7 -7
- package/templates/apps/api-backend-mail/package.json +8 -8
- package/templates/apps/api-backend-mariadb/package.json +9 -9
- package/templates/apps/api-backend-sqlite/package.json +8 -8
- package/templates/apps/api-backend-storage/package.json +8 -8
- package/templates/apps/api-cms/package.json +10 -10
- package/templates/apps/api-collab/package.json +8 -8
- package/templates/apps/api-data-advanced/package.json +8 -8
- package/templates/apps/api-durable/package.json +8 -8
- package/templates/apps/api-feature-flags/package.json +9 -9
- package/templates/apps/api-governance/package.json +8 -8
- package/templates/apps/api-kv/package.json +8 -8
- package/templates/apps/api-moderation/package.json +8 -8
- package/templates/apps/api-observability/package.json +8 -8
- package/templates/apps/api-ratelimit/package.json +8 -8
- package/templates/apps/api-rbac/package.json +8 -8
- package/templates/apps/api-rest/package.json +7 -7
- package/templates/apps/api-saas/package.json +11 -11
- package/templates/apps/api-saas-starter/package.json +10 -10
- package/templates/apps/api-search/package.json +8 -8
- package/templates/apps/api-status/package.json +8 -8
- package/templates/apps/api-versioning/package.json +8 -8
- package/templates/apps/api-webhooks/package.json +9 -9
- package/templates/apps/changelog/package.json +6 -6
- package/templates/apps/edge-functions/package.json +2 -2
- package/templates/apps/frontend-admin/package.json +8 -8
- package/templates/apps/frontend-app/package.json +8 -8
- package/templates/apps/frontend-auth/package.json +8 -8
- package/templates/apps/frontend-blank/package.json +7 -7
- package/templates/apps/frontend-cms/package.json +9 -9
- package/templates/apps/frontend-collab/package.json +9 -9
- package/templates/apps/frontend-contact/package.json +7 -7
- package/templates/apps/frontend-dashboard/package.json +7 -7
- package/templates/apps/frontend-docs/package.json +7 -7
- package/templates/apps/frontend-i18n/package.json +6 -6
- package/templates/apps/frontend-landing/package.json +7 -7
- package/templates/apps/frontend-portal/package.json +8 -8
- package/templates/apps/frontend-saas/package.json +8 -8
- package/templates/apps/frontend-spa/package.json +7 -7
- package/templates/apps/frontend-ssr/package.json +7 -7
- package/templates/apps/frontend-ssr-api/package.json +8 -8
- package/templates/apps/frontend-static-blog/package.json +6 -6
- package/templates/apps/frontend-status/package.json +8 -8
- package/templates/apps/mobile-app/package.json +2 -2
- package/dist/apiBuild-Ca6Sq5jy.js +0 -2
- package/dist/checkCommand-DHD0aBeR.js +0 -2
- package/dist/dbCommand-wfGi3FgG.js +0 -2
- package/dist/frameworkTableAssembly-Bn_c4Mnl.js +0 -2
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { Ut as a, Wt as o } from "./dev-
|
|
1
|
+
import { f as e, gt as t, n, o as r, z as i } from "./inspectMetrics-BM1vrsga.js";
|
|
2
|
+
import { Ut as a, Wt as o } from "./dev-GAOIot9G.js";
|
|
3
3
|
import { C as s, E as c, O as l, T as u, d, h as f, i as p, l as m, m as h, r as g, u as _ } from "./fileConventions-Cof68_BL.js";
|
|
4
|
-
import { r as v, u as y } from "./apiBuild-
|
|
4
|
+
import { r as v, u as y } from "./apiBuild-BUS1RfPi.js";
|
|
5
5
|
import { dirname as b, isAbsolute as ee, join as x, relative as S, resolve as C, sep as w } from "node:path";
|
|
6
6
|
import { routeDiagnosticsToStderr as te } from "@voltro/logger";
|
|
7
7
|
import { existsSync as T, readFileSync as E, readdirSync as D, statSync as O, writeFileSync as ne } from "node:fs";
|
|
@@ -119,7 +119,7 @@ var re = () => {
|
|
|
119
119
|
status: "live",
|
|
120
120
|
logFile: "",
|
|
121
121
|
...e.token === void 0 ? {} : { inspectToken: e.token }
|
|
122
|
-
}), le = async (e,
|
|
122
|
+
}), le = async (e, n) => n === void 0 ? (await t()).apps.filter((t) => t.kind === "api" && t.status === "live" && t.url.length > 0 && (e === void 0 || t.name === e)) : [ce(n)], ue = (e) => {
|
|
123
123
|
let t = "pretty", n = [];
|
|
124
124
|
for (let r = 0; r < e.length; r++) {
|
|
125
125
|
let i = e[r];
|
|
@@ -468,7 +468,7 @@ var re = () => {
|
|
|
468
468
|
".git",
|
|
469
469
|
"coverage",
|
|
470
470
|
".publish"
|
|
471
|
-
]), ke = /\.tsx?$/,
|
|
471
|
+
]), ke = /\.tsx?$/, Ae = (e, t = [], n = 0) => {
|
|
472
472
|
if (n > 12) return t;
|
|
473
473
|
let r;
|
|
474
474
|
try {
|
|
@@ -484,20 +484,20 @@ var re = () => {
|
|
|
484
484
|
} catch {
|
|
485
485
|
continue;
|
|
486
486
|
}
|
|
487
|
-
a.isDirectory() ?
|
|
487
|
+
a.isDirectory() ? Ae(r, t, n + 1) : a.isFile() && ke.test(i) && !i.endsWith(".d.ts") && t.push(r);
|
|
488
488
|
}
|
|
489
489
|
return t;
|
|
490
|
-
},
|
|
491
|
-
if (
|
|
492
|
-
if (k.isExportAssignment(e) || k.isAsExpression(e) || k.isSatisfiesExpression(e) || k.isParenthesizedExpression(e)) return
|
|
490
|
+
}, je = /\b(?:FC|FunctionComponent|ComponentType|ForwardRefExoticComponent|MemoExoticComponent)\b/, Me = /\b(?:memo|forwardRef|observer|styled)\b/, z = (e) => k.isArrowFunction(e) || k.isFunctionExpression(e) || k.isFunctionDeclaration(e) || k.isClassDeclaration(e) || k.isClassExpression(e), B = (e) => {
|
|
491
|
+
if (z(e)) return !0;
|
|
492
|
+
if (k.isExportAssignment(e) || k.isAsExpression(e) || k.isSatisfiesExpression(e) || k.isParenthesizedExpression(e)) return B(e.getExpression());
|
|
493
493
|
if (k.isVariableDeclaration(e)) {
|
|
494
494
|
let t = e.getTypeNode();
|
|
495
|
-
if (t !== void 0 &&
|
|
495
|
+
if (t !== void 0 && je.test(t.getText())) return !0;
|
|
496
496
|
let n = e.getInitializer();
|
|
497
|
-
return n === void 0 ||
|
|
497
|
+
return n === void 0 || B(n);
|
|
498
498
|
}
|
|
499
|
-
return k.isCallExpression(e) ?
|
|
500
|
-
},
|
|
499
|
+
return k.isCallExpression(e) ? Me.test(e.getExpression().getText()) ? !0 : e.getArguments().some((e) => z(e) || k.isIdentifier(e)) : k.isTaggedTemplateExpression(e) ? !0 : !(k.isObjectLiteralExpression(e) || k.isArrayLiteralExpression(e) || k.isStringLiteral(e) || k.isNoSubstitutionTemplateLiteral(e) || k.isNumericLiteral(e) || k.isTemplateExpression(e) || k.isNewExpression(e) || k.isPropertyAccessExpression(e) || e.getKind() === j.TrueKeyword || e.getKind() === j.FalseKeyword || e.getKind() === j.NullKeyword);
|
|
500
|
+
}, Ne = (e) => {
|
|
501
501
|
let t = [], n = [], r = [], i = /* @__PURE__ */ new Set();
|
|
502
502
|
for (let [a, o] of e.getExportedDeclarations()) {
|
|
503
503
|
let e = o[0];
|
|
@@ -508,7 +508,7 @@ var re = () => {
|
|
|
508
508
|
n.push(s);
|
|
509
509
|
continue;
|
|
510
510
|
}
|
|
511
|
-
if (/^[A-Z]/.test(s) &&
|
|
511
|
+
if (/^[A-Z]/.test(s) && B(e)) {
|
|
512
512
|
t.push(s);
|
|
513
513
|
continue;
|
|
514
514
|
}
|
|
@@ -520,7 +520,7 @@ var re = () => {
|
|
|
520
520
|
others: r,
|
|
521
521
|
hasRuntimeExport: t.length + n.length + r.length > 0
|
|
522
522
|
};
|
|
523
|
-
},
|
|
523
|
+
}, Pe = [], Fe = (e, t, n) => {
|
|
524
524
|
let r = b(e), i = [];
|
|
525
525
|
for (let e = r;; e = b(e)) {
|
|
526
526
|
let r = n.get(e);
|
|
@@ -537,9 +537,9 @@ var re = () => {
|
|
|
537
537
|
}
|
|
538
538
|
if (e === t || b(e) === e) break;
|
|
539
539
|
}
|
|
540
|
-
for (let e of i) n.set(e,
|
|
541
|
-
return
|
|
542
|
-
},
|
|
540
|
+
for (let e of i) n.set(e, Pe);
|
|
541
|
+
return Pe;
|
|
542
|
+
}, Ie = (e, t, n) => {
|
|
543
543
|
let r = t.startsWith(".") ? C(b(e), t) : ye(t, n);
|
|
544
544
|
if (r === void 0) return null;
|
|
545
545
|
for (let e of [
|
|
@@ -552,9 +552,9 @@ var re = () => {
|
|
|
552
552
|
if (O(e).isFile()) return e;
|
|
553
553
|
} catch {}
|
|
554
554
|
return null;
|
|
555
|
-
},
|
|
555
|
+
}, Le = (e, t, n) => {
|
|
556
556
|
let r = [], i = /* @__PURE__ */ new Set(), a = /* @__PURE__ */ new Map(), o = (e) => {
|
|
557
|
-
let i =
|
|
557
|
+
let i = Ie(t, e, n);
|
|
558
558
|
return i !== null && r.push(i), i;
|
|
559
559
|
};
|
|
560
560
|
for (let t of e.getImportDeclarations()) if (o(t.getModuleSpecifierValue()), !t.isTypeOnly()) for (let e of t.getNamedImports()) e.isTypeOnly() || i.add(e.getName());
|
|
@@ -581,22 +581,22 @@ var re = () => {
|
|
|
581
581
|
namedBindings: i,
|
|
582
582
|
reExports: a
|
|
583
583
|
};
|
|
584
|
-
},
|
|
584
|
+
}, Re = (e) => {
|
|
585
585
|
let t = /* @__PURE__ */ new Map();
|
|
586
586
|
for (let n of e.getDescendantsOfKind(j.CallExpression)) {
|
|
587
587
|
let e = n.getExpression(), r = k.isPropertyAccessExpression(e) ? e.getName() : e.getText();
|
|
588
588
|
t.set(r, (t.get(r) ?? 0) + 1);
|
|
589
589
|
}
|
|
590
590
|
return t;
|
|
591
|
-
},
|
|
591
|
+
}, ze = (e) => {
|
|
592
592
|
if (e.length <= 1) return e.length === 1;
|
|
593
593
|
let t = [...e].sort((e, t) => e.length - t.length)[0];
|
|
594
594
|
return e.every((e) => e.startsWith(t));
|
|
595
|
-
},
|
|
595
|
+
}, Be = (e, t) => {
|
|
596
596
|
let n = b(t);
|
|
597
597
|
return e.startsWith(`${n}${w}`) || b(e) === n;
|
|
598
|
-
},
|
|
599
|
-
let t =
|
|
598
|
+
}, V = (e) => {
|
|
599
|
+
let t = Ae(e);
|
|
600
600
|
if (t.length === 0) return {
|
|
601
601
|
findings: [],
|
|
602
602
|
declared: 0,
|
|
@@ -627,7 +627,7 @@ var re = () => {
|
|
|
627
627
|
} catch {
|
|
628
628
|
continue;
|
|
629
629
|
}
|
|
630
|
-
s.set(t,
|
|
630
|
+
s.set(t, Ne(r)), c.set(t, Le(r, t, Fe(t, e, u))), l.set(t, Re(r));
|
|
631
631
|
}
|
|
632
632
|
let d = /* @__PURE__ */ new Map();
|
|
633
633
|
for (let [e, t] of c) for (let n of t.localTargets) {
|
|
@@ -663,7 +663,7 @@ var re = () => {
|
|
|
663
663
|
t !== null && !i && (m += 1);
|
|
664
664
|
let u = d.get(e) ?? [];
|
|
665
665
|
if ((t === "component" || t === "component.ui") && !i) {
|
|
666
|
-
let t =
|
|
666
|
+
let t = ze(n.components);
|
|
667
667
|
(n.components.length === 0 || !t) && a.push({
|
|
668
668
|
rule: "component/one-per-file",
|
|
669
669
|
file: o(e),
|
|
@@ -701,7 +701,7 @@ var re = () => {
|
|
|
701
701
|
].join(", ")}`,
|
|
702
702
|
fix: "a *.types.ts has NO runtime export. That is what makes importing it provably free AND makes it impossible for it to sit in a runtime import cycle — the guarantee is the point, so move the value to a sibling module"
|
|
703
703
|
}), t === "internal") {
|
|
704
|
-
let t = u.filter((t) => !
|
|
704
|
+
let t = u.filter((t) => !Be(t, e));
|
|
705
705
|
t.length > 0 && a.push({
|
|
706
706
|
rule: "internal/foreign-import",
|
|
707
707
|
file: o(e),
|
|
@@ -798,8 +798,8 @@ var re = () => {
|
|
|
798
798
|
scanned: r.length,
|
|
799
799
|
vendored: n
|
|
800
800
|
};
|
|
801
|
-
},
|
|
802
|
-
let { findings: t, declared: n, scanned: r, vendored: i } =
|
|
801
|
+
}, Ve = 10, He = (e) => {
|
|
802
|
+
let { findings: t, declared: n, scanned: r, vendored: i } = V(e);
|
|
803
803
|
console.log("");
|
|
804
804
|
for (let t of Se(e, i)) console.log(`· not ours, exempt: ${t}`);
|
|
805
805
|
if (r === 0) return console.log("• file-taxonomy scan: no sources here — nothing to check."), 0;
|
|
@@ -813,12 +813,12 @@ var re = () => {
|
|
|
813
813
|
console.log(`• file taxonomy: ${t.length} finding(s) — ${o} error, ${t.length - o} advisory (${n} declared / ${r} scanned):`);
|
|
814
814
|
for (let [e, t] of [...a].sort()) {
|
|
815
815
|
console.log(` ${t[0].severity === "error" ? "✗" : "·"} ${e} (${t.length})`);
|
|
816
|
-
for (let e of t.slice(0,
|
|
817
|
-
let n = t.length -
|
|
816
|
+
for (let e of t.slice(0, Ve)) console.log(` ${e.file} — ${e.detail}`);
|
|
817
|
+
let n = t.length - Ve;
|
|
818
818
|
n > 0 && console.log(` (+${n} more — \`voltro doctor --json\` prints every one.)`), console.log(` → ${t[0].fix}`);
|
|
819
819
|
}
|
|
820
820
|
return t.length;
|
|
821
|
-
},
|
|
821
|
+
}, Ue = /* @__PURE__ */ new Set([
|
|
822
822
|
"eq",
|
|
823
823
|
"ne",
|
|
824
824
|
"gt",
|
|
@@ -836,10 +836,10 @@ var re = () => {
|
|
|
836
836
|
"endsWith",
|
|
837
837
|
"between",
|
|
838
838
|
"jsonField"
|
|
839
|
-
]),
|
|
839
|
+
]), We = (e) => {
|
|
840
840
|
if (k.isPropertyAccessExpression(e)) return e.getName();
|
|
841
841
|
if (k.isIdentifier(e)) return e.getText();
|
|
842
|
-
},
|
|
842
|
+
}, Ge = (e) => {
|
|
843
843
|
let t = e;
|
|
844
844
|
for (; k.isCallExpression(t);) {
|
|
845
845
|
let e = t.getExpression();
|
|
@@ -847,7 +847,7 @@ var re = () => {
|
|
|
847
847
|
t = e.getExpression();
|
|
848
848
|
}
|
|
849
849
|
return t;
|
|
850
|
-
},
|
|
850
|
+
}, Ke = (e, t, n) => {
|
|
851
851
|
let r = new Map(t.map((e) => [e.name, new Set(e.columns)])), i = new Map(t.map((e) => [e.name, e.columns])), a = [], o = 0, s = new A({
|
|
852
852
|
useInMemoryFileSystem: !0,
|
|
853
853
|
skipAddingFilesFromTsConfig: !0
|
|
@@ -865,7 +865,7 @@ var re = () => {
|
|
|
865
865
|
for (let t of e.getDescendantsOfKind(j.CallExpression)) {
|
|
866
866
|
let e = t.getExpression();
|
|
867
867
|
if (!k.isPropertyAccessExpression(e) || e.getName() !== "where") continue;
|
|
868
|
-
let n =
|
|
868
|
+
let n = We(Ge(e.getExpression()));
|
|
869
869
|
if (n === void 0 || !r.has(n)) {
|
|
870
870
|
let t = e.getExpression();
|
|
871
871
|
if (k.isCallExpression(t)) {
|
|
@@ -888,7 +888,7 @@ var re = () => {
|
|
|
888
888
|
else for (let e of [u, ...u.getDescendantsOfKind(j.CallExpression)]) {
|
|
889
889
|
if (!k.isCallExpression(e)) continue;
|
|
890
890
|
let t = e.getExpression(), n = k.isIdentifier(t) ? t.getText() : k.isPropertyAccessExpression(t) ? t.getName() : void 0;
|
|
891
|
-
if (n === void 0 || !
|
|
891
|
+
if (n === void 0 || !Ue.has(n)) continue;
|
|
892
892
|
let r = e.getArguments()[0];
|
|
893
893
|
r !== void 0 && k.isStringLiteral(r) && l.push({
|
|
894
894
|
column: r.getLiteralValue(),
|
|
@@ -909,16 +909,16 @@ var re = () => {
|
|
|
909
909
|
issues: a,
|
|
910
910
|
checked: o
|
|
911
911
|
};
|
|
912
|
-
},
|
|
912
|
+
}, H = 12, qe = (e) => {
|
|
913
913
|
if (e.checked === 0) return null;
|
|
914
914
|
if (e.issues.length === 0) return `predicate columns: ${e.checked} checked — every filtered column exists on its table`;
|
|
915
915
|
let t = [`predicate columns: ${e.issues.length} filter${e.issues.length, "s"} on a column that does not exist (${e.checked} checked)`];
|
|
916
916
|
for (let n of e.issues) {
|
|
917
|
-
let e = n.available.slice(0,
|
|
917
|
+
let e = n.available.slice(0, H).join(", "), r = n.available.length > H ? `, … (${n.available.length - H} more)` : "";
|
|
918
918
|
t.push(` ${n.file}:${n.line} '${n.table}' has no column '${n.column}'`), t.push(` columns: ${e}${r}`);
|
|
919
919
|
}
|
|
920
920
|
return t.push(" This type-checks today and fails at runtime as a bare SQL error."), t.join("\n");
|
|
921
|
-
},
|
|
921
|
+
}, Je = /* @__PURE__ */ new Set([
|
|
922
922
|
"id",
|
|
923
923
|
"tenantId",
|
|
924
924
|
"tenant_id",
|
|
@@ -938,7 +938,7 @@ var re = () => {
|
|
|
938
938
|
"idempotency_id",
|
|
939
939
|
"idempotencyKey",
|
|
940
940
|
"idempotency_key"
|
|
941
|
-
]),
|
|
941
|
+
]), Ye = /* @__PURE__ */ new Set([
|
|
942
942
|
"id",
|
|
943
943
|
"createdAt",
|
|
944
944
|
"created_at",
|
|
@@ -948,10 +948,10 @@ var re = () => {
|
|
|
948
948
|
"deleted_at",
|
|
949
949
|
"tenantId",
|
|
950
950
|
"tenant_id"
|
|
951
|
-
]),
|
|
951
|
+
]), Xe = (e) => Je.has(e) ? !1 : /[a-z0-9]Id$/.test(e) || /_id$/.test(e), Ze = (e) => e.type === "reference", Qe = (e) => {
|
|
952
952
|
let t = [];
|
|
953
953
|
for (let n of e) {
|
|
954
|
-
let e = n.columns.filter((e) =>
|
|
954
|
+
let e = n.columns.filter((e) => Xe(e.name)), r = e.filter((e) => !Ze(e)).map((e) => e.name);
|
|
955
955
|
if (r.length === 0) continue;
|
|
956
956
|
let i = new Set(n.primaryKey), a = r.filter((e) => i.has(e));
|
|
957
957
|
if (n.primaryKey.length >= 2 && a.length > 0) {
|
|
@@ -962,7 +962,7 @@ var re = () => {
|
|
|
962
962
|
});
|
|
963
963
|
continue;
|
|
964
964
|
}
|
|
965
|
-
if (e.some(
|
|
965
|
+
if (e.some(Ze) && e.length >= 2) {
|
|
966
966
|
t.push({
|
|
967
967
|
table: n.name,
|
|
968
968
|
columns: r,
|
|
@@ -970,7 +970,7 @@ var re = () => {
|
|
|
970
970
|
});
|
|
971
971
|
continue;
|
|
972
972
|
}
|
|
973
|
-
e.length >= 2 && n.columns.filter((e) => !
|
|
973
|
+
e.length >= 2 && n.columns.filter((e) => !Xe(e.name) && !Ye.has(e.name)).length === 0 && t.push({
|
|
974
974
|
table: n.name,
|
|
975
975
|
columns: r,
|
|
976
976
|
signal: "link-only"
|
|
@@ -980,20 +980,20 @@ var re = () => {
|
|
|
980
980
|
issues: t,
|
|
981
981
|
tablesChecked: e.length
|
|
982
982
|
};
|
|
983
|
-
},
|
|
983
|
+
}, $e = {
|
|
984
984
|
"composite-pk": "part of a composite primary key",
|
|
985
985
|
"reference-sibling": "sits beside a wired reference() on this table",
|
|
986
986
|
"link-only": "this table is nothing but link columns"
|
|
987
|
-
},
|
|
987
|
+
}, et = (e) => {
|
|
988
988
|
if (e.tablesChecked === 0) return null;
|
|
989
989
|
if (e.issues.length === 0) return `junction FKs: ${e.tablesChecked} table${e.tablesChecked === 1 ? "" : "s"} checked — every link column is a wired reference()`;
|
|
990
990
|
let t = e.issues.length, n = [`junction FKs: ${t} junction table${t === 1 ? "" : "s"} with an id column that is a plain text() and not a reference()`];
|
|
991
991
|
for (let t of e.issues) {
|
|
992
992
|
let e = t.columns.map((e) => `'${e}'`).join(", ");
|
|
993
|
-
n.push(` '${t.table}': ${e} (${
|
|
993
|
+
n.push(` '${t.table}': ${e} (${$e[t.signal]})`);
|
|
994
994
|
}
|
|
995
995
|
return n.push(" Declare each as reference(() => <table>): the FK is enforced, the column is auto-indexed,"), n.push(" and the relationship becomes walkable (a plain text() id column is an invisible edge)."), n.join("\n");
|
|
996
|
-
},
|
|
996
|
+
}, tt = /* @__PURE__ */ new Set(/* @__PURE__ */ "position.positions.move.moved.moving.cursor.pointer.mouse.drag.dragging.hover.frame.frames.tick.ticks.ping.heartbeat.typing.scroll.scrolling.zoom.pan.coord.coords.coordinate.coordinates.telemetry.sensor.sample.samples.viewport".split(".")), nt = (e) => e.replace(/([a-z0-9])([A-Z])/g, "$1 $2").split(/[^A-Za-z0-9]+/).filter((e) => e.length > 0).map((e) => e.toLowerCase()), rt = (e) => nt(e).some((e) => tt.has(e)), it = (e) => {
|
|
997
997
|
let t = [], n = [];
|
|
998
998
|
for (let r of e) t.push({
|
|
999
999
|
event: r.name,
|
|
@@ -1002,7 +1002,7 @@ var re = () => {
|
|
|
1002
1002
|
event: r.name,
|
|
1003
1003
|
kind: "wide-key",
|
|
1004
1004
|
keyFieldCount: r.keyFieldCount
|
|
1005
|
-
}), r.hasWebhook &&
|
|
1005
|
+
}), r.hasWebhook && rt(r.name) && n.push({
|
|
1006
1006
|
event: r.name,
|
|
1007
1007
|
kind: "webhook-high-rate"
|
|
1008
1008
|
});
|
|
@@ -1011,26 +1011,26 @@ var re = () => {
|
|
|
1011
1011
|
modes: t,
|
|
1012
1012
|
warnings: n
|
|
1013
1013
|
};
|
|
1014
|
-
},
|
|
1014
|
+
}, at = (e) => {
|
|
1015
1015
|
if (e.eventsChecked === 0) return null;
|
|
1016
1016
|
let t = e.eventsChecked, n = [`event delivery: ${t} declared event${t === 1 ? "" : "s"} — the mode decides what a MISSING message means`];
|
|
1017
1017
|
for (let t of e.modes) n.push(` '${t.event}': ${t.delivery}`);
|
|
1018
1018
|
return n.push(" each — every delivery matters; a slow subscriber loses the oldest and is TOLD how many (the default)."), n.push(" latest — a newer delivery supersedes a pending one; a slow subscriber gets the current value, told nothing."), n.push(" Two events with identical route/subscriber/buffer numbers mean OPPOSITE things about a drop — the mode is the difference."), n.join("\n");
|
|
1019
|
-
},
|
|
1019
|
+
}, ot = (e) => {
|
|
1020
1020
|
if (e.warnings.length === 0) return null;
|
|
1021
1021
|
let t = e.warnings.length, n = [`events that may not scale as declared: ${t} finding${t === 1 ? "" : "s"}`];
|
|
1022
1022
|
for (let t of e.warnings) t.kind === "wide-key" ? (n.push(` '${t.event}': routing key has ${t.keyFieldCount ?? 0} fields — every field FRAGMENTS the subscriber set (distinct routes are the PRODUCT of the fields’ value spaces).`), n.push(" Check each is an ADDRESS the delivery is decided by (arenaId), not a discriminator the handler reads (gameType) — the latter is payload, not key.")) : (n.push(` '${t.event}': a webhook block on a per-frame-looking event — every publish is N HTTP deliveries per second per subscribed target.`), n.push(" The webhook rate limit DEFERS the excess as pending rows rather than failing, so the symptom is a growing table. Publish a coarser event (a summary / state change) for the outside world."));
|
|
1023
1023
|
return n.push(" WARNING, not a fault — a producer may intend the fan-out. `voltro doctor --json` prints every finding."), n.join("\n");
|
|
1024
|
-
},
|
|
1024
|
+
}, st = /* @__PURE__ */ new Set([
|
|
1025
1025
|
"start",
|
|
1026
1026
|
"run",
|
|
1027
1027
|
"child"
|
|
1028
|
-
]),
|
|
1028
|
+
]), ct = (e) => k.isCallExpression(e) && (() => {
|
|
1029
1029
|
let t = e.getExpression();
|
|
1030
|
-
if (!k.isPropertyAccessExpression(t) || !
|
|
1030
|
+
if (!k.isPropertyAccessExpression(t) || !st.has(t.getName())) return !1;
|
|
1031
1031
|
let n = t.getExpression();
|
|
1032
1032
|
return k.isPropertyAccessExpression(n) ? n.getName() === "workflows" : k.isIdentifier(n) && n.getText() === "workflows";
|
|
1033
|
-
})(),
|
|
1033
|
+
})(), lt = (e, t, n) => {
|
|
1034
1034
|
let r = new Map(t.map((e) => [e.name, e])), i = [...r.keys()].sort(), a = [], o = [], s = 0, c = 0, l = new A({
|
|
1035
1035
|
useInMemoryFileSystem: !0,
|
|
1036
1036
|
skipAddingFilesFromTsConfig: !0
|
|
@@ -1046,7 +1046,7 @@ var re = () => {
|
|
|
1046
1046
|
}
|
|
1047
1047
|
let u = S(n, t);
|
|
1048
1048
|
for (let t of e.getDescendantsOfKind(j.CallExpression)) {
|
|
1049
|
-
if (!
|
|
1049
|
+
if (!ct(t)) continue;
|
|
1050
1050
|
c++;
|
|
1051
1051
|
let e = t.getStartLineNumber(), [n, l] = t.getArguments();
|
|
1052
1052
|
if (n === void 0 || !k.isStringLiteral(n)) {
|
|
@@ -1130,7 +1130,7 @@ var re = () => {
|
|
|
1130
1130
|
checked: s,
|
|
1131
1131
|
callSites: c
|
|
1132
1132
|
};
|
|
1133
|
-
},
|
|
1133
|
+
}, ut = (e) => {
|
|
1134
1134
|
if (e.callSites === 0) return null;
|
|
1135
1135
|
let t = [];
|
|
1136
1136
|
t.push(`workflow starts: ${e.checked}/${e.callSites} call sites checked`);
|
|
@@ -1143,7 +1143,7 @@ var re = () => {
|
|
|
1143
1143
|
for (let n of e.unchecked) t.push(` ${n.file}:${n.line} — ${n.reason}`);
|
|
1144
1144
|
}
|
|
1145
1145
|
return e.issues.length === 0 && e.unchecked.length === 0 && t.push(" every call site matches its workflow"), t.join("\n");
|
|
1146
|
-
},
|
|
1146
|
+
}, dt = /(^|[/\\])(\.env(\.[\w.-]+)?|.*\.ya?ml|Dockerfile|.*\.tf|.*\.toml|Procfile)$/i, U = "VOLTRO_FIELD_ENCRYPTION_KEY", ft = (e) => {
|
|
1147
1147
|
let t;
|
|
1148
1148
|
for (let [n, r] of e.sources) if (t === void 0 && /\bfieldEncryption\b/.test(r) && (t = {
|
|
1149
1149
|
reason: "fieldEncryption",
|
|
@@ -1151,13 +1151,13 @@ var re = () => {
|
|
|
1151
1151
|
}), t === void 0 && /\.encrypted\s*\(/.test(r) && (t = {
|
|
1152
1152
|
reason: "encrypted-column",
|
|
1153
1153
|
file: n
|
|
1154
|
-
}), r.includes(
|
|
1154
|
+
}), r.includes(U)) return;
|
|
1155
1155
|
if (t !== void 0) {
|
|
1156
|
-
for (let [, t] of e.envFiles) if (t.includes(
|
|
1156
|
+
for (let [, t] of e.envFiles) if (t.includes(U)) return;
|
|
1157
1157
|
return t;
|
|
1158
1158
|
}
|
|
1159
|
-
},
|
|
1160
|
-
`✗ ${
|
|
1159
|
+
}, pt = (e) => [
|
|
1160
|
+
`✗ ${U} is named nowhere under this app root, and the app encrypts columns at rest`,
|
|
1161
1161
|
e.reason === "fieldEncryption" ? ` (\`fieldEncryption\` in ${e.file})` : ` (an \`.encrypted()\` column in ${e.file})`,
|
|
1162
1162
|
"",
|
|
1163
1163
|
" A deploy without it does not degrade — it refuses to boot at `onActivate`,",
|
|
@@ -1173,7 +1173,7 @@ var re = () => {
|
|
|
1173
1173
|
" reaches your deploy target is not knowable from here — a secret manager",
|
|
1174
1174
|
" or a CI variable is not a file. Naming it nowhere is conclusive; naming",
|
|
1175
1175
|
" it somewhere is not a guarantee."
|
|
1176
|
-
].join("\n"),
|
|
1176
|
+
].join("\n"), mt = (e) => e === "effect" || e.startsWith("@effect/") || e.startsWith("@voltro/") || e === "react" || e === "react-dom", ht = /(^|\/)(\.bin|\.cache|\.pnpm-store|dist|build)(\/|$)/, gt = (e) => {
|
|
1177
1177
|
let t = [], n = e;
|
|
1178
1178
|
for (let e = 0; e < 12; e++) {
|
|
1179
1179
|
let e = x(n, "node_modules");
|
|
@@ -1183,14 +1183,14 @@ var re = () => {
|
|
|
1183
1183
|
n = r;
|
|
1184
1184
|
}
|
|
1185
1185
|
return t;
|
|
1186
|
-
},
|
|
1186
|
+
}, _t = (e) => {
|
|
1187
1187
|
try {
|
|
1188
1188
|
let t = JSON.parse(E(x(e, "package.json"), "utf8"));
|
|
1189
1189
|
return typeof t.version == "string" ? t.version : void 0;
|
|
1190
1190
|
} catch {
|
|
1191
1191
|
return;
|
|
1192
1192
|
}
|
|
1193
|
-
},
|
|
1193
|
+
}, vt = (e) => {
|
|
1194
1194
|
let t = [], n;
|
|
1195
1195
|
try {
|
|
1196
1196
|
n = D(e);
|
|
@@ -1198,7 +1198,7 @@ var re = () => {
|
|
|
1198
1198
|
return t;
|
|
1199
1199
|
}
|
|
1200
1200
|
for (let r of n) {
|
|
1201
|
-
if (r.startsWith(".") ||
|
|
1201
|
+
if (r.startsWith(".") || ht.test(r)) continue;
|
|
1202
1202
|
let n = x(e, r);
|
|
1203
1203
|
if (r.startsWith("@")) {
|
|
1204
1204
|
let e;
|
|
@@ -1224,11 +1224,11 @@ var re = () => {
|
|
|
1224
1224
|
});
|
|
1225
1225
|
}
|
|
1226
1226
|
return t;
|
|
1227
|
-
},
|
|
1227
|
+
}, yt = (e) => {
|
|
1228
1228
|
let t = /* @__PURE__ */ new Map();
|
|
1229
|
-
for (let n of
|
|
1230
|
-
if (!
|
|
1231
|
-
let n =
|
|
1229
|
+
for (let n of gt(e)) for (let { pkg: e, dir: r } of vt(n)) {
|
|
1230
|
+
if (!mt(e)) continue;
|
|
1231
|
+
let n = _t(r);
|
|
1232
1232
|
if (n === void 0) continue;
|
|
1233
1233
|
let i = t.get(e) ?? /* @__PURE__ */ new Map();
|
|
1234
1234
|
i.has(n) || i.set(n, r), t.set(e, i);
|
|
@@ -1240,7 +1240,7 @@ var re = () => {
|
|
|
1240
1240
|
paths: [...i.values()].map((t) => S(e, t)).sort()
|
|
1241
1241
|
});
|
|
1242
1242
|
return n.sort((e, t) => e.pkg.localeCompare(t.pkg));
|
|
1243
|
-
},
|
|
1243
|
+
}, bt = (e) => {
|
|
1244
1244
|
if (e.length !== 0) return [
|
|
1245
1245
|
`• ${e.length} package(s) resolved at more than one version:`,
|
|
1246
1246
|
...e.flatMap((e) => [` ${e.pkg} — ${e.versions.join(", ")}`, ...e.paths.map((e) => ` ${e}`)]),
|
|
@@ -1258,14 +1258,14 @@ var re = () => {
|
|
|
1258
1258
|
" instrument), then reinstall. Do not silence the generated file — it is where",
|
|
1259
1259
|
" a genuine mistake in your own descriptors surfaces."
|
|
1260
1260
|
].join("\n");
|
|
1261
|
-
},
|
|
1261
|
+
}, xt = (e) => {
|
|
1262
1262
|
if (e.catalogCodes.length === 0) return;
|
|
1263
1263
|
let t = new Set(e.declaredLocales), n = e.catalogCodes.filter((e) => !t.has(e)).sort();
|
|
1264
1264
|
if (n.length !== 0) return {
|
|
1265
1265
|
orphaned: n,
|
|
1266
1266
|
noneDeclared: t.size === 0
|
|
1267
1267
|
};
|
|
1268
|
-
},
|
|
1268
|
+
}, St = (e, t) => {
|
|
1269
1269
|
let n = e.orphaned.join(", ");
|
|
1270
1270
|
return e.noneDeclared ? [
|
|
1271
1271
|
`⚠ ${t} has ${e.orphaned.length} catalog(s) — ${n} — and the app declares no \`locales:\`.`,
|
|
@@ -1277,49 +1277,49 @@ var re = () => {
|
|
|
1277
1277
|
" …or delete the files. A catalog that is never loaded looks from the inside",
|
|
1278
1278
|
" exactly like one that works, which is why this is worth a line."
|
|
1279
1279
|
] : [`⚠ ${t} has catalog(s) not in \`locales:\` — ${n} — so they are never imported.`, " Add the code(s) to `locales:` in your app config, or delete the file(s)."];
|
|
1280
|
-
},
|
|
1280
|
+
}, Ct = (e) => e.replace(/\/\*[\s\S]*?\*\//g, "").replace(/(^|[^:])\/\/[^\n]*/g, "$1"), wt = /* @__PURE__ */ new Set([
|
|
1281
1281
|
"requireScope",
|
|
1282
1282
|
"requireAnyScope",
|
|
1283
1283
|
"requireAuth",
|
|
1284
1284
|
"assertScope",
|
|
1285
1285
|
"assertCan",
|
|
1286
1286
|
"can"
|
|
1287
|
-
]),
|
|
1287
|
+
]), Tt = (e) => {
|
|
1288
1288
|
let t = /* @__PURE__ */ new Set();
|
|
1289
|
-
for (let n of e) for (let e of
|
|
1289
|
+
for (let n of e) for (let e of Ct(n).matchAll(/\bexport\s+(?:const|function|async\s+function)\s+((?:require|assert)[A-Z]\w*)/g)) t.add(e[1]);
|
|
1290
1290
|
return t;
|
|
1291
|
-
},
|
|
1291
|
+
}, Et = (e, t) => {
|
|
1292
1292
|
for (let n of t) if (RegExp(`\\b${n}\\s*\\(`).test(e)) return n;
|
|
1293
|
-
},
|
|
1293
|
+
}, Dt = (e) => {
|
|
1294
1294
|
let t = /\bsubject\s*\.\s*id\b[^\n]*?(?:!==|===|!=|==)|(?:!==|===|!=|==)[^\n]*?\bsubject\s*\.\s*id\b/.test(e), n = /(?:Error|Denied|Forbidden|Unauthorized)\b|\b(?:fail|die|throw)\b/.test(e);
|
|
1295
1295
|
return t && n;
|
|
1296
|
-
},
|
|
1296
|
+
}, Ot = (e, t, n) => {
|
|
1297
1297
|
if (e.hasDeclaredGuards) return { kind: "declared-guard" };
|
|
1298
|
-
let r =
|
|
1299
|
-
return i === void 0 ?
|
|
1298
|
+
let r = Ct(e.src), i = Et(r, t);
|
|
1299
|
+
return i === void 0 ? Dt(r) ? { kind: "inline-ownership" } : n.has(e.tag) ? { kind: "allowlisted" } : { kind: "unchecked" } : {
|
|
1300
1300
|
kind: "vocabulary-guard",
|
|
1301
1301
|
name: i
|
|
1302
1302
|
};
|
|
1303
|
-
},
|
|
1303
|
+
}, kt = (e, t, n) => {
|
|
1304
1304
|
let r = e.op === "delete" ? 3 : e.op === "update" ? 2 : 1;
|
|
1305
1305
|
return e.table !== void 0 && (t.has(e.table) && (r += 2), n.has(e.table) && e.op === "delete" && (r += 2)), r;
|
|
1306
|
-
},
|
|
1306
|
+
}, At = (e, t, n) => {
|
|
1307
1307
|
switch (t.kind) {
|
|
1308
1308
|
case "declared-guard": return "guards: on the descriptor";
|
|
1309
1309
|
case "vocabulary-guard": return `calls ${t.name}()`;
|
|
1310
1310
|
case "inline-ownership": return "compares the caller to the row and refuses — correct, but only this file knows it";
|
|
1311
1311
|
case "allowlisted": return "accepted by a previous run";
|
|
1312
|
-
case "unchecked": return e.op === "read" ? "returns a row the client selected, with nothing compared to the caller" : `${e.op}s${e.table ? ` ${e.table}` : ""} with nothing constraining WHICH row`;
|
|
1312
|
+
case "unchecked": return n ? e.op === "read" ? "no check in this file — this app registers a row filter; VERIFY it covers this table, then record it as reviewed= rather than debt" : `${e.op}s${e.table ? ` ${e.table}` : ""} with no check in this file — this app registers a row filter; VERIFY it covers this table, then record it as reviewed= rather than debt` : e.op === "read" ? "returns a row the client selected, with nothing compared to the caller" : `${e.op}s${e.table ? ` ${e.table}` : ""} with nothing constraining WHICH row`;
|
|
1313
1313
|
}
|
|
1314
|
-
},
|
|
1314
|
+
}, jt = (e) => {
|
|
1315
1315
|
let t = e.executors.map((t) => {
|
|
1316
|
-
let n =
|
|
1316
|
+
let n = Ot(t, e.guardVocabulary, e.allowlist);
|
|
1317
1317
|
return {
|
|
1318
1318
|
tag: t.tag,
|
|
1319
1319
|
path: t.path,
|
|
1320
1320
|
verdict: n,
|
|
1321
|
-
severity:
|
|
1322
|
-
why:
|
|
1321
|
+
severity: kt(t, e.tenantScopedTables, e.referencedTables),
|
|
1322
|
+
why: At(t, n, e.rowFilterRegistered === !0)
|
|
1323
1323
|
};
|
|
1324
1324
|
}), n = (e) => t.filter((t) => t.verdict.kind === e).length;
|
|
1325
1325
|
return {
|
|
@@ -1333,7 +1333,7 @@ var re = () => {
|
|
|
1333
1333
|
},
|
|
1334
1334
|
unchecked: t.filter((e) => e.verdict.kind === "unchecked").sort((e, t) => t.severity - e.severity || e.tag.localeCompare(t.tag))
|
|
1335
1335
|
};
|
|
1336
|
-
},
|
|
1336
|
+
}, Mt = [
|
|
1337
1337
|
"# voltro doctor — authz allowlist",
|
|
1338
1338
|
"#",
|
|
1339
1339
|
"# Two KINDS of line, and the difference is the point:",
|
|
@@ -1358,21 +1358,46 @@ var re = () => {
|
|
|
1358
1358
|
"# `--write-authz-allowlist` PRESERVES reviewed= lines. It only ever adds debt",
|
|
1359
1359
|
"# lines for executors that are newly unchecked.",
|
|
1360
1360
|
""
|
|
1361
|
-
],
|
|
1361
|
+
], Nt = (e) => {
|
|
1362
1362
|
let t = [...e].map((e) => typeof e == "string" ? { tag: e } : e), n = t.filter((e) => e.reviewed === void 0).map((e) => e.tag).sort(), r = t.filter((e) => e.reviewed !== void 0).sort((e, t) => e.tag.localeCompare(t.tag));
|
|
1363
1363
|
return [
|
|
1364
|
-
...
|
|
1364
|
+
...Mt,
|
|
1365
1365
|
...n,
|
|
1366
1366
|
...r.length > 0 ? ["", "# reviewed — read by a human, open on purpose"] : [],
|
|
1367
1367
|
...r.map((e) => `${e.tag} reviewed=${e.reviewed}`),
|
|
1368
1368
|
""
|
|
1369
1369
|
].join("\n");
|
|
1370
|
-
},
|
|
1370
|
+
}, Pt = (e, t) => {
|
|
1371
|
+
let n = [...new Set(t)].sort();
|
|
1372
|
+
if (e === void 0) return Nt(n.map((e) => ({ tag: e })));
|
|
1373
|
+
let r = W(e).tags, i = n.filter((e) => !r.has(e));
|
|
1374
|
+
return i.length === 0 ? e : [
|
|
1375
|
+
e.replace(/\n+$/, ""),
|
|
1376
|
+
"",
|
|
1377
|
+
"# added by `voltro doctor --write-authz-allowlist` — newly unchecked executors.",
|
|
1378
|
+
"# Nothing above this line was touched.",
|
|
1379
|
+
...i,
|
|
1380
|
+
""
|
|
1381
|
+
].join("\n");
|
|
1382
|
+
}, Ft = (e, t) => {
|
|
1383
|
+
let n = [...new Set(t)];
|
|
1384
|
+
if (e === void 0) return {
|
|
1385
|
+
added: n.length,
|
|
1386
|
+
kept: 0,
|
|
1387
|
+
removed: 0
|
|
1388
|
+
};
|
|
1389
|
+
let r = W(e).tags;
|
|
1390
|
+
return {
|
|
1391
|
+
added: n.filter((e) => !r.has(e)).length,
|
|
1392
|
+
kept: r.size,
|
|
1393
|
+
removed: 0
|
|
1394
|
+
};
|
|
1395
|
+
}, It = class extends Error {
|
|
1371
1396
|
tag;
|
|
1372
1397
|
constructor(e) {
|
|
1373
1398
|
super(`authz allowlist: '${e}' is marked reviewed with no reason. Write \`${e} reviewed=<why it is genuinely open>\`. A bare tag is the other, honest option — it records the executor as DEBT, which is what an unexplained exemption is.`), this.tag = e, this.name = "AllowlistReasonMissingError";
|
|
1374
1399
|
}
|
|
1375
|
-
},
|
|
1400
|
+
}, W = (e) => {
|
|
1376
1401
|
let t = [];
|
|
1377
1402
|
for (let n of e.split("\n")) {
|
|
1378
1403
|
let e = n.trim();
|
|
@@ -1385,7 +1410,7 @@ var re = () => {
|
|
|
1385
1410
|
continue;
|
|
1386
1411
|
}
|
|
1387
1412
|
let a = r[2].trim();
|
|
1388
|
-
if (a.length === 0) throw new
|
|
1413
|
+
if (a.length === 0) throw new It(i);
|
|
1389
1414
|
t.push({
|
|
1390
1415
|
tag: i,
|
|
1391
1416
|
reviewed: a
|
|
@@ -1395,13 +1420,13 @@ var re = () => {
|
|
|
1395
1420
|
tags: new Set(t.map((e) => e.tag)),
|
|
1396
1421
|
entries: t
|
|
1397
1422
|
};
|
|
1398
|
-
},
|
|
1423
|
+
}, Lt = (e) => {
|
|
1399
1424
|
let t = e.indexOf(".");
|
|
1400
1425
|
return t <= 0 ? void 0 : e.slice(0, t);
|
|
1401
|
-
},
|
|
1426
|
+
}, Rt = (e, t) => {
|
|
1402
1427
|
let n = new Set(t.tables), r = new Set(t.rpcTags), i = /* @__PURE__ */ new Map();
|
|
1403
1428
|
for (let e of t.rpcTags) {
|
|
1404
|
-
let t =
|
|
1429
|
+
let t = Lt(e);
|
|
1405
1430
|
if (t === void 0) continue;
|
|
1406
1431
|
let n = i.get(t);
|
|
1407
1432
|
n ? n.push(e) : i.set(t, [e]);
|
|
@@ -1430,7 +1455,7 @@ var re = () => {
|
|
|
1430
1455
|
});
|
|
1431
1456
|
continue;
|
|
1432
1457
|
}
|
|
1433
|
-
let s = new Set(o.rpcTags.map(
|
|
1458
|
+
let s = new Set(o.rpcTags.map(Lt).filter((e) => e !== void 0));
|
|
1434
1459
|
for (let e of s) {
|
|
1435
1460
|
let t = i.get(e);
|
|
1436
1461
|
t !== void 0 && a.push({
|
|
@@ -1443,12 +1468,12 @@ var re = () => {
|
|
|
1443
1468
|
}
|
|
1444
1469
|
}
|
|
1445
1470
|
return a;
|
|
1446
|
-
},
|
|
1471
|
+
}, zt = (e, t) => {
|
|
1447
1472
|
let n = e.reduce((e, t) => e + t.tables.length, 0), r = [`plugin surface · ${e.length} plugin(s) contribute ${n} table(s)`];
|
|
1448
1473
|
if (t.length === 0) return r.push(" ✓ no overlap with your own tables or rpc namespaces"), r;
|
|
1449
1474
|
for (let e of t) r.push(` ${e.fatal ? "✗" : "⚠"} ${e.plugin} — ${e.detail}`), r.push(` ${e.fix}`);
|
|
1450
1475
|
return r;
|
|
1451
|
-
},
|
|
1476
|
+
}, Bt = (e) => T(C(e, "app.config.ts")) || T(C(e, "app.config.js")), Vt = (e) => {
|
|
1452
1477
|
let t = y(e), n = v(e);
|
|
1453
1478
|
return {
|
|
1454
1479
|
root: e,
|
|
@@ -1457,19 +1482,19 @@ var re = () => {
|
|
|
1457
1482
|
apiBundle: n,
|
|
1458
1483
|
apiBundlePresent: T(n)
|
|
1459
1484
|
};
|
|
1460
|
-
},
|
|
1485
|
+
}, Ht = [
|
|
1461
1486
|
"Production `voltro serve` requires a precompiled serve bundle and never transpiles on demand.",
|
|
1462
1487
|
"Add a `voltro build .` step BEFORE `voltro serve .` in your Dockerfile / start script.",
|
|
1463
1488
|
"(Generated Dockerfiles from `voltro create-project` already do this.)"
|
|
1464
|
-
].join("\n "),
|
|
1465
|
-
let n =
|
|
1466
|
-
if (console.log(`voltro doctor — serve preflight for ${n.root}`), !
|
|
1489
|
+
].join("\n "), Ut = (e, t = !1) => {
|
|
1490
|
+
let n = Vt(e);
|
|
1491
|
+
if (console.log(`voltro doctor — serve preflight for ${n.root}`), !Bt(e)) return console.log("• no app.config found here — nothing to check (run against an app root)."), 0;
|
|
1467
1492
|
console.log(`${n.serveBundlePresent ? "✓" : "✗"} serve bundle: ${n.serveBundlePresent ? "present" : "MISSING"} (${n.serveBundle})`), console.log(`${n.apiBundlePresent ? "✓" : "•"} api bundle: ${n.apiBundlePresent ? "present" : "absent"} (${n.apiBundle})`);
|
|
1468
1493
|
let r = process.env.NODE_ENV === "production";
|
|
1469
1494
|
if (n.serveBundlePresent) return console.log("✓ serve preflight: ready for `voltro serve`."), 0;
|
|
1470
1495
|
let i = t || r;
|
|
1471
|
-
return console.error(`${i ? "✗" : "•"} serve bundle missing — ${i ? "production `voltro serve` will FAIL." : "a non-production `voltro serve` still runs via tsx."}`), console.error(` ${
|
|
1472
|
-
},
|
|
1496
|
+
return console.error(`${i ? "✗" : "•"} serve bundle missing — ${i ? "production `voltro serve` will FAIL." : "a non-production `voltro serve` still runs via tsx."}`), console.error(` ${Ht}`), +!!i;
|
|
1497
|
+
}, Wt = (e, t, n) => {
|
|
1473
1498
|
let r = new Set(e), i = I(n), a = (e) => (/\.(tsx?|jsx?)$/.test(e) ? [e] : [
|
|
1474
1499
|
`${e}.ts`,
|
|
1475
1500
|
`${e}.tsx`,
|
|
@@ -1495,7 +1520,7 @@ var re = () => {
|
|
|
1495
1520
|
r !== void 0 && o.push(r);
|
|
1496
1521
|
}
|
|
1497
1522
|
return o;
|
|
1498
|
-
}, s = /* @__PURE__ */ new Set(), c = e.filter((e) =>
|
|
1523
|
+
}, s = /* @__PURE__ */ new Set(), c = e.filter((e) => Xt.test(S(n, e))), l = new Set(c);
|
|
1499
1524
|
for (; c.length > 0;) {
|
|
1500
1525
|
let e = c.shift();
|
|
1501
1526
|
if (!s.has(e)) {
|
|
@@ -1508,11 +1533,11 @@ var re = () => {
|
|
|
1508
1533
|
let d = /* @__PURE__ */ new Set();
|
|
1509
1534
|
for (let e of s) l.has(e) || u.has(e) || d.add(e);
|
|
1510
1535
|
return d;
|
|
1511
|
-
},
|
|
1536
|
+
}, Gt = (e) => e.replace(/\/\*[\s\S]*?\*\//g, " ").replace(/(^|[^:])\/\/[^\n]*/g, "$1").replace(/'(?:[^'\\\n]|\\.)*'/g, "''").replace(/"(?:[^"\\\n]|\\.)*"/g, "\"\""), Kt = (e, t) => (e.match(t) ?? []).length, G = (e, t) => e.getDescendantsOfKind(j.CallExpression).filter((e) => t.test(e.getExpression().getText())).length, qt = (e, t) => {
|
|
1512
1537
|
if (e.src.includes(t)) return !0;
|
|
1513
1538
|
let n = t.replace(/^@voltro\/plugin-/, "");
|
|
1514
1539
|
return n !== t && G(e.file, RegExp(`^${n}Plugin$`)) > 0;
|
|
1515
|
-
},
|
|
1540
|
+
}, Jt = (e, t) => {
|
|
1516
1541
|
let n = [], r = RegExp(`\\b${t}\\s*(?:<[^()]*>)?\\s*\\(`, "g"), i;
|
|
1517
1542
|
for (; (i = r.exec(e)) !== null;) {
|
|
1518
1543
|
let t = i.index + i[0].length, r = 1, a = t;
|
|
@@ -1520,10 +1545,10 @@ var re = () => {
|
|
|
1520
1545
|
n.push(e.slice(t, a - 1));
|
|
1521
1546
|
}
|
|
1522
1547
|
return n;
|
|
1523
|
-
},
|
|
1548
|
+
}, Yt = (e, t) => {
|
|
1524
1549
|
let n = t.getSymbol()?.getDeclarations()?.[0];
|
|
1525
1550
|
return n === void 0 ? [] : e.getDescendantsOfKind(j.Identifier).filter((e) => e.getSymbol()?.getDeclarations()?.[0] === n);
|
|
1526
|
-
},
|
|
1551
|
+
}, Xt = /\.(server|cron|workflow\.server|subscribe|reaction|outbox|startup|trigger|webhook|migration|seed)\.tsx?$/, Zt = /\.(query|mutation|action|stream)\.server\.tsx?$/, Qt = [
|
|
1527
1552
|
"Id",
|
|
1528
1553
|
"ID",
|
|
1529
1554
|
"Hash",
|
|
@@ -1549,7 +1574,7 @@ var re = () => {
|
|
|
1549
1574
|
"Comment",
|
|
1550
1575
|
"Provider",
|
|
1551
1576
|
"Format"
|
|
1552
|
-
],
|
|
1577
|
+
], $t = RegExp(`(?:${Qt.join("|")})$|_(?:${Qt.map((e) => e.toLowerCase()).join("|")})$`), en = [
|
|
1553
1578
|
{
|
|
1554
1579
|
id: "hardcoded-framework-cookie",
|
|
1555
1580
|
scope: "client",
|
|
@@ -1562,7 +1587,7 @@ var re = () => {
|
|
|
1562
1587
|
scope: "server",
|
|
1563
1588
|
smell: "an executor that never names its own descriptor — its input type is hand-written, so it can drift from the contract it implements",
|
|
1564
1589
|
use: "import the descriptor and type the input as `ExecutorInput<typeof descriptor>`, so the wire contract and the handler cannot disagree",
|
|
1565
|
-
match: ({ src: e, path: t }) =>
|
|
1590
|
+
match: ({ src: e, path: t }) => Zt.test(t) && !/\bimport\s[^;]*\bfrom\s*['"][^'"]*['"]/.test((e.match(/^\s*import\s[^;]*from\s*['"]\.[^'"]*['"]/gm) ?? []).join("\n"))
|
|
1566
1591
|
},
|
|
1567
1592
|
{
|
|
1568
1593
|
id: "subject-write-no-guard",
|
|
@@ -1570,17 +1595,17 @@ var re = () => {
|
|
|
1570
1595
|
smell: "writes ctx.request.subject.id into the database with no guard on the descriptor — an anonymous caller reaches the write and gets a raw statement failure instead of a typed refusal",
|
|
1571
1596
|
use: "add a guard (e.g. `.guard(requireScope('…'))`) or an explicit auth check, so an unauthenticated caller is refused BEFORE the write",
|
|
1572
1597
|
match: ({ src: e, path: t }) => {
|
|
1573
|
-
if (!
|
|
1574
|
-
let n =
|
|
1598
|
+
if (!Zt.test(t)) return !1;
|
|
1599
|
+
let n = Gt(e);
|
|
1575
1600
|
return /\bsubject\s*\.\s*id\b/.test(n) && /\b(?:insert|update|upsert|insertMany|updateMany|insertRow|upsertRow|updateManyRow)\s*\(/.test(n) && !/\b(?:guard|guards|requireScope|requireAnyScope|assertCan|assertScope|requireAuth|Unauthenticated)\b/.test(n);
|
|
1576
1601
|
}
|
|
1577
1602
|
},
|
|
1578
1603
|
{
|
|
1579
1604
|
id: "raw-fetch",
|
|
1580
1605
|
scope: "server",
|
|
1581
|
-
smell: "raw fetch() for outbound HTTP on the server (no
|
|
1582
|
-
use: "yield* HttpClient — the framework client. It blocks loopback / RFC-1918 / link-local targets (incl. 169.254.169.254) on the initial request AND every redirect hop
|
|
1583
|
-
match: ({ file: e, path: t, serverOnly: n }) => (
|
|
1606
|
+
smell: "raw fetch() for outbound HTTP on the server (no traceparent, no retry, and no SSRF guard where the URL is not a constant)",
|
|
1607
|
+
use: "yield* HttpClient — the framework client. It injects traceparent, retries, and honours `http.allowHosts`. Where the URL is built from anything a caller supplied it ALSO blocks loopback / RFC-1918 / link-local targets (incl. 169.254.169.254) on the initial request AND every redirect hop — for a hard-coded host that half is redundant, and the first three are still the reason to switch",
|
|
1608
|
+
match: ({ file: e, path: t, serverOnly: n }) => (Xt.test(t) || n) && G(e, /^(?:globalThis\.|window\.|self\.)?fetch$/) > 0
|
|
1584
1609
|
},
|
|
1585
1610
|
{
|
|
1586
1611
|
id: "form",
|
|
@@ -1615,7 +1640,7 @@ var re = () => {
|
|
|
1615
1640
|
scope: "client",
|
|
1616
1641
|
smell: "useMemo fanning in multiple subscriptions (hand-maintained dep array)",
|
|
1617
1642
|
use: "useDerived — dependency-tracked, referentially stable",
|
|
1618
|
-
match: ({ src: e }) =>
|
|
1643
|
+
match: ({ src: e }) => Kt(e, /\buseSubscription\s*[(<]/g) >= 3 && Kt(e, /\buseMemo\s*\(/g) >= 3
|
|
1619
1644
|
},
|
|
1620
1645
|
{
|
|
1621
1646
|
id: "next-shim",
|
|
@@ -1640,7 +1665,7 @@ var re = () => {
|
|
|
1640
1665
|
match: ({ file: e }) => e.getDescendantsOfKind(j.CallExpression).filter((e) => /(^|\.)useSubscription$/.test(e.getExpression().getText())).flatMap((e) => {
|
|
1641
1666
|
let t = e.getFirstAncestorByKind(j.VariableDeclaration)?.getNameNode();
|
|
1642
1667
|
return t === void 0 ? [] : k.isObjectBindingPattern(t) ? t.getElements().filter((e) => (e.getPropertyNameNode() ?? e.getNameNode()).getText() === "data").map((e) => e.getNameNode()) : [];
|
|
1643
|
-
}).some((t) =>
|
|
1668
|
+
}).some((t) => Yt(e, t).some((e) => {
|
|
1644
1669
|
let t = e.getParent();
|
|
1645
1670
|
if (k.isBinaryExpression(t)) {
|
|
1646
1671
|
let n = t.getOperatorToken().getText(), r = t.getLeft() === e ? t.getRight() : t.getLeft();
|
|
@@ -1691,7 +1716,17 @@ var re = () => {
|
|
|
1691
1716
|
use: ".encrypted() on the column — boot fails loudly if no cipher is configured, so it can never silently persist plaintext (a *Hash column and a vault* handle are NOT plaintext secrets: the hash IS the protection — often a unique lookup key — and the handle only names a secret held elsewhere, so both are skipped)",
|
|
1692
1717
|
match: ({ src: e }) => e.split("\n").some((e) => [...e.matchAll(/\b([A-Za-z_$][\w$]*)\s*:\s*(?:text|varchar|string|longtext)\s*\(/g)].some((t) => {
|
|
1693
1718
|
let n = t[1];
|
|
1694
|
-
return !/token|secret|credential|password|apikey|accesskey/i.test(n) ||
|
|
1719
|
+
return !/token|secret|credential|password|apikey|accesskey/i.test(n) || $t.test(n) || /^vault/i.test(n) || /\.unique\s*\(/.test(e) ? !1 : !/\.encrypted\s*\(/.test(e);
|
|
1720
|
+
}))
|
|
1721
|
+
},
|
|
1722
|
+
{
|
|
1723
|
+
id: "plaintext-secret-lookup-key",
|
|
1724
|
+
scope: "server",
|
|
1725
|
+
smell: "a credential-shaped column that is ALSO a unique lookup key",
|
|
1726
|
+
use: "you cannot look a value up by equality AND encrypt it non-deterministically — `.encrypted()` would make this column unfindable. Store the HASH as the unique lookup key and keep the secret itself either `.encrypted()` in a second column or nowhere (the framework's own `_voltro_api_keys.hashedKey` is the shape: the presented value is hashed and compared, never read back). A deterministic cipher is the other option and buys equality lookup at the cost of leaking which rows share a value",
|
|
1727
|
+
match: ({ src: e }) => e.split("\n").some((e) => [...e.matchAll(/\b([A-Za-z_$][\w$]*)\s*:\s*(?:text|varchar|string|longtext)\s*\(/g)].some((t) => {
|
|
1728
|
+
let n = t[1];
|
|
1729
|
+
return !/token|secret|credential|password|apikey|accesskey/i.test(n) || $t.test(n) || /^vault/i.test(n) ? !1 : /\.unique\s*\(/.test(e) && !/\.encrypted\s*\(/.test(e);
|
|
1695
1730
|
}))
|
|
1696
1731
|
},
|
|
1697
1732
|
{
|
|
@@ -1756,8 +1791,8 @@ var re = () => {
|
|
|
1756
1791
|
smell: "setRowFilter is registered, but no test passes `rowFilter:` to makeTestContext",
|
|
1757
1792
|
use: "wire the filter into at least one test (`makeTestContext({ subject, store, rowFilter })`) and sabotage the predicate once to watch it go red — an untested row filter is a visibility rule nothing verifies",
|
|
1758
1793
|
match: ({ file: e, testSources: t }) => G(e, /^setRowFilter$/) > 0 && ![...t.values()].some((e) => {
|
|
1759
|
-
let t =
|
|
1760
|
-
return
|
|
1794
|
+
let t = Gt(e);
|
|
1795
|
+
return Jt(t, "makeTestContext").length > 0 && /\browFilter\b/.test(t);
|
|
1761
1796
|
})
|
|
1762
1797
|
},
|
|
1763
1798
|
{
|
|
@@ -1767,14 +1802,14 @@ var re = () => {
|
|
|
1767
1802
|
use: "timestampMs / timestampMsOrNull from @voltro/database/wire in the descriptor's output struct — `output` IS the serializer (it is the rpc success schema), so a Date field declared as timestampMs crosses as epoch ms on its own; declaring Schema.Number describes the WIRE type and leaves nothing to convert",
|
|
1768
1803
|
match: ({ src: e }) => /\.(getTime|toISOString)\s*\(\s*\)/.test(e) && /\.map\s*\(|=>\s*\(\{/.test(e) && /\bexport\s+default\b/.test(e)
|
|
1769
1804
|
}
|
|
1770
|
-
],
|
|
1805
|
+
], tn = (e) => q(e), nn = (e, t) => {
|
|
1771
1806
|
let n = /* @__PURE__ */ new Set();
|
|
1772
1807
|
for (let r of t) {
|
|
1773
1808
|
let t = S(e, r), i = t.split(/[\\/]/)[0];
|
|
1774
1809
|
n.add(i !== void 0 && i !== t ? i : ".");
|
|
1775
1810
|
}
|
|
1776
1811
|
return [...n].sort();
|
|
1777
|
-
},
|
|
1812
|
+
}, rn = /\.(tsx?|jsx?)$/, K = /(^|\/)(node_modules|dist|build|\.framework|\.next|coverage)(\/|$)/, an = /(^|[\\/])[^\\/]*\.generated\.[cm]?[jt]sx?$|\.d\.[cm]?ts$/, on = (e, t = [], n = 0) => {
|
|
1778
1813
|
if (n > 12 || K.test(e)) return t;
|
|
1779
1814
|
let r;
|
|
1780
1815
|
try {
|
|
@@ -1786,8 +1821,8 @@ var re = () => {
|
|
|
1786
1821
|
let r = x(e, i.name);
|
|
1787
1822
|
if (i.isDirectory()) {
|
|
1788
1823
|
if (i.name.startsWith(".")) continue;
|
|
1789
|
-
|
|
1790
|
-
} else
|
|
1824
|
+
on(r, t, n + 1);
|
|
1825
|
+
} else dt.test(i.name) && t.push(r);
|
|
1791
1826
|
}
|
|
1792
1827
|
return t;
|
|
1793
1828
|
}, q = (e, t = [], n = 0) => {
|
|
@@ -1805,10 +1840,10 @@ var re = () => {
|
|
|
1805
1840
|
} catch {
|
|
1806
1841
|
continue;
|
|
1807
1842
|
}
|
|
1808
|
-
a.isDirectory() ? q(r, t, n + 1) :
|
|
1843
|
+
a.isDirectory() ? q(r, t, n + 1) : rn.test(i) && !/\.(test|spec)\./.test(i) && !an.test(i) && t.push(r);
|
|
1809
1844
|
}
|
|
1810
1845
|
return t;
|
|
1811
|
-
},
|
|
1846
|
+
}, sn = (e, t = [], n = 0) => {
|
|
1812
1847
|
if (n > 12 || K.test(e)) return t;
|
|
1813
1848
|
let r;
|
|
1814
1849
|
try {
|
|
@@ -1823,17 +1858,17 @@ var re = () => {
|
|
|
1823
1858
|
} catch {
|
|
1824
1859
|
continue;
|
|
1825
1860
|
}
|
|
1826
|
-
a.isDirectory() ?
|
|
1861
|
+
a.isDirectory() ? sn(r, t, n + 1) : rn.test(i) && /\.(test|spec)\./.test(i) && t.push(r);
|
|
1827
1862
|
}
|
|
1828
1863
|
return t;
|
|
1829
|
-
},
|
|
1830
|
-
let t =
|
|
1864
|
+
}, cn = (e) => {
|
|
1865
|
+
let t = tn(e);
|
|
1831
1866
|
return {
|
|
1832
|
-
dirs:
|
|
1867
|
+
dirs: nn(e, t),
|
|
1833
1868
|
fileCount: t.length
|
|
1834
1869
|
};
|
|
1835
|
-
},
|
|
1836
|
-
let t =
|
|
1870
|
+
}, ln = (e) => {
|
|
1871
|
+
let t = tn(e);
|
|
1837
1872
|
if (t.length === 0) return [];
|
|
1838
1873
|
let n = new A({
|
|
1839
1874
|
useInMemoryFileSystem: !0,
|
|
@@ -1842,8 +1877,8 @@ var re = () => {
|
|
|
1842
1877
|
for (let e of t) try {
|
|
1843
1878
|
i.set(e, E(e, "utf8"));
|
|
1844
1879
|
} catch {}
|
|
1845
|
-
let a =
|
|
1846
|
-
for (let t of
|
|
1880
|
+
let a = Wt([...i.keys()], i, e), o = /* @__PURE__ */ new Map();
|
|
1881
|
+
for (let t of sn(e)) try {
|
|
1847
1882
|
o.set(t, E(t, "utf8"));
|
|
1848
1883
|
} catch {}
|
|
1849
1884
|
for (let s of t) {
|
|
@@ -1861,23 +1896,23 @@ var re = () => {
|
|
|
1861
1896
|
} catch {
|
|
1862
1897
|
continue;
|
|
1863
1898
|
}
|
|
1864
|
-
for (let t of
|
|
1865
|
-
if (t.plugin !== void 0 &&
|
|
1899
|
+
for (let t of en) {
|
|
1900
|
+
if (t.plugin !== void 0 && qt(c, t.plugin) || !t.match(c)) continue;
|
|
1866
1901
|
let n = r.get(t.id) ?? [];
|
|
1867
1902
|
n.push(S(e, s)), r.set(t.id, n);
|
|
1868
1903
|
}
|
|
1869
1904
|
}
|
|
1870
|
-
return
|
|
1905
|
+
return en.filter((e) => r.has(e.id)).map((e) => ({
|
|
1871
1906
|
rule: e,
|
|
1872
1907
|
files: r.get(e.id)
|
|
1873
1908
|
})).sort((e, t) => t.files.length - e.files.length || e.rule.id.localeCompare(t.rule.id));
|
|
1874
|
-
},
|
|
1875
|
-
let t =
|
|
1909
|
+
}, un = (e) => {
|
|
1910
|
+
let t = cn(e);
|
|
1876
1911
|
return {
|
|
1877
1912
|
root: e,
|
|
1878
1913
|
scannedFiles: t.fileCount,
|
|
1879
1914
|
scannedDirs: t.dirs,
|
|
1880
|
-
findings:
|
|
1915
|
+
findings: ln(e).map(({ rule: e, files: t }) => ({
|
|
1881
1916
|
id: e.id,
|
|
1882
1917
|
scope: e.scope,
|
|
1883
1918
|
smell: e.smell,
|
|
@@ -1885,22 +1920,22 @@ var re = () => {
|
|
|
1885
1920
|
files: t
|
|
1886
1921
|
}))
|
|
1887
1922
|
};
|
|
1888
|
-
},
|
|
1923
|
+
}, dn = 3, fn = .2, pn = 50, mn = (e, t) => e >= pn && t > 0 && e / t >= fn, hn = (e) => {
|
|
1889
1924
|
let t = /* @__PURE__ */ new Map();
|
|
1890
1925
|
for (let n of q(e)) try {
|
|
1891
1926
|
t.set(S(e, n), E(n, "utf8"));
|
|
1892
1927
|
} catch {}
|
|
1893
1928
|
let n = /* @__PURE__ */ new Map();
|
|
1894
|
-
for (let t of
|
|
1929
|
+
for (let t of on(e)) try {
|
|
1895
1930
|
n.set(S(e, t), E(t, "utf8"));
|
|
1896
1931
|
} catch {}
|
|
1897
|
-
let r =
|
|
1932
|
+
let r = ft({
|
|
1898
1933
|
sources: t,
|
|
1899
1934
|
envFiles: n
|
|
1900
1935
|
});
|
|
1901
|
-
r !== void 0 && (console.log(""), console.log(
|
|
1902
|
-
},
|
|
1903
|
-
let t =
|
|
1936
|
+
r !== void 0 && (console.log(""), console.log(pt(r)));
|
|
1937
|
+
}, gn = (e) => {
|
|
1938
|
+
let t = un(e);
|
|
1904
1939
|
if (console.log(""), t.scannedFiles === 0) return console.log(`\u2022 hand-roll scan: no source directories found under ${e}.`), console.log(" Walked the app root recursively (node_modules / dist / .framework pruned). Run `voltro doctor <app>` against an app root."), 0;
|
|
1905
1940
|
if (t.findings.length === 0) return console.log(`\u2713 hand-roll scan: ${t.scannedFiles} file${t.scannedFiles === 1 ? "" : "s"} across ${t.scannedDirs.join(", ")} \u2014 no hand-rolled primitives found.`), 0;
|
|
1906
1941
|
console.log(`\u2022 Shipped primitives you may be hand-rolling (scanned ${t.scannedFiles} files across ${t.scannedDirs.join(", ")}):`);
|
|
@@ -1911,56 +1946,56 @@ var re = () => {
|
|
|
1911
1946
|
console.log(` [${e}]`);
|
|
1912
1947
|
let i = [...r].sort((e, t) => e.files.length - t.files.length);
|
|
1913
1948
|
for (let e of i) {
|
|
1914
|
-
if (
|
|
1949
|
+
if (mn(e.files.length, t.scannedFiles)) {
|
|
1915
1950
|
let n = Math.round(e.files.length / t.scannedFiles * 100);
|
|
1916
1951
|
console.log(` ${e.smell} \u2014 ${e.files.length} file(s), ${n}% of those scanned: a codebase-wide pattern rather than a work list`), console.log(` \u2192 ${e.use}`), console.log(" (paths omitted on purpose at this scale — `voltro doctor --json` has every one.)");
|
|
1917
1952
|
continue;
|
|
1918
1953
|
}
|
|
1919
|
-
let r = e.files.slice(0,
|
|
1954
|
+
let r = e.files.slice(0, dn).join(", "), i = e.files.length - dn;
|
|
1920
1955
|
i > 0 && (n += i);
|
|
1921
1956
|
let a = i > 0 ? `, +${i} more` : "";
|
|
1922
1957
|
console.log(` ${e.smell} \u2014 ${e.files.length} file(s): ${r}${a}`), console.log(` \u2192 ${e.use}`);
|
|
1923
1958
|
}
|
|
1924
1959
|
}
|
|
1925
1960
|
return n > 0 && console.log(` (${n} more file path${n === 1 ? "" : "s"} not shown \u2014 \`voltro doctor --json\` prints every one.)`), console.log(" (See the \"Pick the SERVER primitive\" / \"Pick the CLIENT primitive\" rubrics in AGENTS.md / CLAUDE.md.)"), t.findings.length;
|
|
1926
|
-
},
|
|
1927
|
-
let t =
|
|
1961
|
+
}, _n = "renderMode:'spa' skips this page's SSR compile while its layout shell still renders server-side — adopt it if the page BODY does not need SSR (internal/authenticated pages); keep 'ssr' if the page content needs SEO or server first-paint.", vn = /export\s+const\s+renderMode\s*(?::[^=]+)?=\s*['"]([^'"]*)['"]/, yn = /export\s+const\s+dynamic\s*=\s*['"]force-(dynamic|static)['"]/, bn = (e) => {
|
|
1962
|
+
let t = vn.exec(e)?.[1];
|
|
1928
1963
|
if (t !== void 0) return t === "ssr" ? "ssr" : null;
|
|
1929
|
-
let n =
|
|
1964
|
+
let n = yn.exec(e)?.[1];
|
|
1930
1965
|
return n === "dynamic" ? "ssr" : n === "static" ? null : "default";
|
|
1931
|
-
},
|
|
1932
|
-
let
|
|
1933
|
-
if (!T(
|
|
1934
|
-
let { pages: a, dirs: o } = await
|
|
1935
|
-
for (let
|
|
1936
|
-
if (!
|
|
1966
|
+
}, xn = async (t) => {
|
|
1967
|
+
let i = x(t, "src", "pages");
|
|
1968
|
+
if (!T(i)) return [];
|
|
1969
|
+
let { pages: a, dirs: o } = await e(i), s = [];
|
|
1970
|
+
for (let e of a) {
|
|
1971
|
+
if (!r(o, e.dirChain).some((e) => e.hasLayout)) continue;
|
|
1937
1972
|
let a;
|
|
1938
1973
|
try {
|
|
1939
|
-
a = E(x(
|
|
1974
|
+
a = E(x(i, e.file), "utf8");
|
|
1940
1975
|
} catch {
|
|
1941
1976
|
continue;
|
|
1942
1977
|
}
|
|
1943
|
-
if (
|
|
1944
|
-
let c =
|
|
1978
|
+
if (n.test(a)) continue;
|
|
1979
|
+
let c = bn(a);
|
|
1945
1980
|
c !== null && s.push({
|
|
1946
|
-
file: S(
|
|
1947
|
-
pattern:
|
|
1981
|
+
file: S(t, x(i, e.file)),
|
|
1982
|
+
pattern: e.pattern,
|
|
1948
1983
|
currentMode: c
|
|
1949
1984
|
});
|
|
1950
1985
|
}
|
|
1951
1986
|
return s;
|
|
1952
|
-
},
|
|
1987
|
+
}, Sn = 10, Cn = async (e) => {
|
|
1953
1988
|
if (console.log(""), !T(x(e, "src", "pages"))) return console.log("• spa-candidate scan: no src/pages here — nothing to check (web apps only)."), 0;
|
|
1954
|
-
let t = await
|
|
1989
|
+
let t = await xn(e);
|
|
1955
1990
|
if (t.length === 0) return console.log("✓ spa-candidate scan: no loader-free, layout-backed pages to flag."), 0;
|
|
1956
1991
|
console.log(`• renderMode:'spa' candidates (${t.length} page${t.length === 1 ? "" : "s"} — loader-free, under a layout, currently ssr/default):`);
|
|
1957
|
-
for (let e of t.slice(0,
|
|
1992
|
+
for (let e of t.slice(0, Sn)) {
|
|
1958
1993
|
let t = e.currentMode === "ssr" ? "renderMode:'ssr'" : "default renderMode";
|
|
1959
1994
|
console.log(` ${e.file} (${e.pattern}) — ${t}`);
|
|
1960
1995
|
}
|
|
1961
|
-
let n = t.length -
|
|
1962
|
-
return n > 0 && console.log(` (+${n} more page${n === 1 ? "" : "s"} not shown — \`voltro doctor --json\` prints every one.)`), console.log(` → ${
|
|
1963
|
-
},
|
|
1996
|
+
let n = t.length - Sn;
|
|
1997
|
+
return n > 0 && console.log(` (+${n} more page${n === 1 ? "" : "s"} not shown — \`voltro doctor --json\` prints every one.)`), console.log(` → ${_n}`), t.length;
|
|
1998
|
+
}, wn = /* @__PURE__ */ new Set([
|
|
1964
1999
|
"node_modules",
|
|
1965
2000
|
"dist",
|
|
1966
2001
|
"build",
|
|
@@ -1968,7 +2003,7 @@ var re = () => {
|
|
|
1968
2003
|
".vite",
|
|
1969
2004
|
".framework",
|
|
1970
2005
|
".git"
|
|
1971
|
-
]),
|
|
2006
|
+
]), J = (e, t, n) => {
|
|
1972
2007
|
let r;
|
|
1973
2008
|
try {
|
|
1974
2009
|
r = D(e, { withFileTypes: !0 });
|
|
@@ -1976,11 +2011,11 @@ var re = () => {
|
|
|
1976
2011
|
return;
|
|
1977
2012
|
}
|
|
1978
2013
|
for (let i of r) {
|
|
1979
|
-
if (
|
|
2014
|
+
if (wn.has(i.name)) continue;
|
|
1980
2015
|
let r = x(e, i.name);
|
|
1981
|
-
i.isDirectory() ?
|
|
2016
|
+
i.isDirectory() ? J(r, t, n) : i.isFile() && i.name.endsWith(".tsx") && n.push(S(t, r));
|
|
1982
2017
|
}
|
|
1983
|
-
},
|
|
2018
|
+
}, Tn = (e, t) => {
|
|
1984
2019
|
let n = /* @__PURE__ */ new Set(), r = /(?:from|import)\s*\(?\s*['"](\.[^'"]*)['"]/g;
|
|
1985
2020
|
for (let i of t) {
|
|
1986
2021
|
let t;
|
|
@@ -2005,59 +2040,67 @@ var re = () => {
|
|
|
2005
2040
|
}
|
|
2006
2041
|
}
|
|
2007
2042
|
return n;
|
|
2008
|
-
},
|
|
2043
|
+
}, En = (e, t) => {
|
|
2009
2044
|
let n = e.slice(t.length).split(w).slice(0, -1)[0];
|
|
2010
2045
|
return n !== void 0 && n.startsWith("[");
|
|
2011
|
-
},
|
|
2012
|
-
let
|
|
2013
|
-
if (!T(
|
|
2046
|
+
}, Y = (e) => {
|
|
2047
|
+
let t = [], n = x(e, "src", "pages"), r = x(e, "src");
|
|
2048
|
+
if (!T(r)) return t;
|
|
2014
2049
|
let a = [];
|
|
2015
|
-
|
|
2016
|
-
let o = `${S(
|
|
2017
|
-
|
|
2018
|
-
for (let
|
|
2019
|
-
let
|
|
2020
|
-
if (
|
|
2021
|
-
if (
|
|
2022
|
-
T(x(
|
|
2050
|
+
J(r, e, a), a.sort();
|
|
2051
|
+
let o = `${S(e, n)}${w}`;
|
|
2052
|
+
Tn(e, a);
|
|
2053
|
+
for (let n of a) {
|
|
2054
|
+
let r = n.slice(n.lastIndexOf(w) + 1);
|
|
2055
|
+
if (n.startsWith(o)) {
|
|
2056
|
+
if (r !== "page.tsx") continue;
|
|
2057
|
+
T(x(e, i(n))) || t.push({
|
|
2023
2058
|
kind: "page-without-test",
|
|
2024
|
-
file:
|
|
2025
|
-
})
|
|
2059
|
+
file: n
|
|
2060
|
+
});
|
|
2061
|
+
try {
|
|
2062
|
+
let r = E(x(e, n), "utf8");
|
|
2063
|
+
!/\bexport\s+(?:const|let|var|function)\s+meta\b/.test(r) && !/\bexport\s*\{[^}]*\bmeta\b/.test(r) && t.push({
|
|
2064
|
+
kind: "page-without-meta",
|
|
2065
|
+
file: n
|
|
2066
|
+
});
|
|
2067
|
+
} catch {}
|
|
2068
|
+
En(n, o) && t.push({
|
|
2026
2069
|
kind: "top-level-dynamic-route",
|
|
2027
|
-
file:
|
|
2070
|
+
file: n
|
|
2028
2071
|
});
|
|
2029
2072
|
continue;
|
|
2030
2073
|
}
|
|
2031
|
-
|
|
2074
|
+
r === "page.tsx" && t.push({
|
|
2032
2075
|
kind: "suffixed-outside-pages",
|
|
2033
|
-
file:
|
|
2076
|
+
file: n
|
|
2034
2077
|
});
|
|
2035
2078
|
}
|
|
2036
|
-
return
|
|
2037
|
-
},
|
|
2079
|
+
return t;
|
|
2080
|
+
}, Dn = 10, X = (e, t, n) => {
|
|
2038
2081
|
if (t.length === 0) return;
|
|
2039
2082
|
console.log(`• ${e} (${t.length}):`);
|
|
2040
|
-
for (let e of t.slice(0,
|
|
2041
|
-
let r = t.length -
|
|
2083
|
+
for (let e of t.slice(0, Dn)) console.log(` ${e}`);
|
|
2084
|
+
let r = t.length - Dn;
|
|
2042
2085
|
r > 0 && console.log(` (+${r} more not shown — \`voltro doctor --json\` prints every one.)`), console.log(` → ${n}`);
|
|
2043
|
-
},
|
|
2086
|
+
}, On = (e) => {
|
|
2044
2087
|
if (console.log(""), !T(x(e, "src", "pages"))) return console.log("• page-convention scan: no src/pages here — nothing to check (web apps only)."), 0;
|
|
2045
|
-
let t =
|
|
2088
|
+
let t = Y(e);
|
|
2046
2089
|
if (t.length === 0) return console.log("✓ page-convention scan: every route is a page.tsx in its own directory, with a test beside it."), 0;
|
|
2047
2090
|
let n = (e) => t.filter((t) => t.kind === e).map((e) => e.file);
|
|
2048
|
-
return X("will NEVER route — page.tsx outside src/pages/", n("suffixed-outside-pages"), "the name says \"route\", the location says otherwise, and the location wins: the URL comes from the directory chain under src/pages/. Move it into the directory whose route it serves, or rename it."), X("pages with no test beside them", n("page-without-test"), "a page and its test belong together — `users/[id].page.tsx` → `users/[id].page.test.tsx`. Advisory only; a missing test is a gap, not a defect."), X("routes whose FIRST segment is dynamic — they claim every URL of that depth", n("top-level-dynamic-route"), "a route like `[id]/[code]` answers `/api/health`, `/internal/readiness` and anything else with that many segments on this origin. That is what the pattern means, so it is not a bug — but it is invisible until something requests such a URL, and then the page renders, its loader runs, and the failure reads as an application error rather than as a route claiming a path nobody meant it to. Give the route a literal first segment (`scan/[id]/[code]`) if you did not intend to own the whole origin at that depth. Advisory."), t.length;
|
|
2049
|
-
},
|
|
2050
|
-
let t =
|
|
2091
|
+
return X("will NEVER route — page.tsx outside src/pages/", n("suffixed-outside-pages"), "the name says \"route\", the location says otherwise, and the location wins: the URL comes from the directory chain under src/pages/. Move it into the directory whose route it serves, or rename it."), X("pages with no test beside them", n("page-without-test"), "a page and its test belong together — `users/[id].page.tsx` → `users/[id].page.test.tsx`. Advisory only; a missing test is a gap, not a defect."), X("routes whose FIRST segment is dynamic — they claim every URL of that depth", n("top-level-dynamic-route"), "a route like `[id]/[code]` answers `/api/health`, `/internal/readiness` and anything else with that many segments on this origin. That is what the pattern means, so it is not a bug — but it is invisible until something requests such a URL, and then the page renders, its loader runs, and the failure reads as an application error rather than as a route claiming a path nobody meant it to. Give the route a literal first segment (`scan/[id]/[code]`) if you did not intend to own the whole origin at that depth. Advisory."), X("pages with no `meta` export — they serve the APP name as their title", n("page-without-meta"), "a page with no `meta` sets no title, so the response carries `<title>{your app.config name}</title>` — the same string on every such page, to every reader that does not run JavaScript: link previews, crawlers, a bookmark taken before hydration, a screen reader announcing the document. The client router fixes the tab after hydration, which is exactly why this is easy to miss. Add `export const meta = { title: \"…\" }` (or a function of params/loaderData). Advisory."), t.length;
|
|
2092
|
+
}, kn = "voltro doctor [app] [flags]\n\nPreflight an app root: verify it can boot under production `voltro serve`,\nreport shipped primitives the source looks to be hand-rolling, and flag pages\nthat are safe `renderMode:'spa'` candidates.\n\nArguments:\n [app] App directory to check (default: the current directory).\n\nFlags:\n --strict Treat a missing serve bundle as blocking regardless of NODE_ENV\n (the `voltro serve --preflight` behavior). Without it, a missing\n bundle only fails when NODE_ENV=production.\n --json Print the complete scan as JSON — hand-roll findings AND spa\n candidates, every file path, nothing elided. Skips the preflight output.\n --write-authz-allowlist\n Record today's executors that reference NO access check into\n voltro-authz-allowlist.txt, so every later run fails only on\n ADDITIONS. The list is DEBT, not approval — an entry stops\n mattering as soon as its executor gains a guard.\n -h, --help Print this help.", An = /* @__PURE__ */ new Map(), Z = (e) => {
|
|
2093
|
+
let t = An.get(e);
|
|
2051
2094
|
if (t !== void 0) return t;
|
|
2052
2095
|
let n = (async () => {
|
|
2053
|
-
let { walk: t, loadDiscovered: n } = await import("./dev-
|
|
2096
|
+
let { walk: t, loadDiscovered: n } = await import("./dev-tPcOmjPJ.js"), r = await t(e);
|
|
2054
2097
|
return {
|
|
2055
2098
|
files: r,
|
|
2056
2099
|
discovered: await n(r)
|
|
2057
2100
|
};
|
|
2058
2101
|
})();
|
|
2059
|
-
return
|
|
2060
|
-
},
|
|
2102
|
+
return An.set(e, n), n;
|
|
2103
|
+
}, jn = async (e) => {
|
|
2061
2104
|
let t;
|
|
2062
2105
|
try {
|
|
2063
2106
|
let { files: n, discovered: r } = await Z(e), i = r.tables.map((e) => {
|
|
@@ -2068,12 +2111,12 @@ var re = () => {
|
|
|
2068
2111
|
};
|
|
2069
2112
|
});
|
|
2070
2113
|
if (i.length === 0) return;
|
|
2071
|
-
t = Ke(
|
|
2114
|
+
t = qe(Ke(n, i, e));
|
|
2072
2115
|
} catch {
|
|
2073
2116
|
return;
|
|
2074
2117
|
}
|
|
2075
2118
|
t !== null && (console.log(""), console.log(`${t.includes("does not exist") ? "✗" : "✓"} ${t}`));
|
|
2076
|
-
},
|
|
2119
|
+
}, Mn = (e) => e.map((e) => {
|
|
2077
2120
|
let t = e, n = Object.entries(t.fields ?? {}).map(([e, t]) => ({
|
|
2078
2121
|
name: e,
|
|
2079
2122
|
type: typeof t?.type == "string" ? t.type : ""
|
|
@@ -2083,22 +2126,22 @@ var re = () => {
|
|
|
2083
2126
|
columns: n,
|
|
2084
2127
|
primaryKey: t.appliedPrimaryKey ?? []
|
|
2085
2128
|
};
|
|
2086
|
-
}),
|
|
2129
|
+
}), Nn = async (e) => {
|
|
2087
2130
|
try {
|
|
2088
2131
|
let { discovered: t } = await Z(e);
|
|
2089
|
-
return
|
|
2132
|
+
return Qe(Mn(t.tables));
|
|
2090
2133
|
} catch {
|
|
2091
2134
|
return null;
|
|
2092
2135
|
}
|
|
2093
|
-
},
|
|
2094
|
-
let t = await
|
|
2136
|
+
}, Pn = async (e) => {
|
|
2137
|
+
let t = await Nn(e);
|
|
2095
2138
|
if (t === null || t.tablesChecked === 0) return;
|
|
2096
|
-
let n =
|
|
2139
|
+
let n = et(t);
|
|
2097
2140
|
n !== null && (console.log(""), console.log(`${t.issues.length > 0 ? "✗" : "✓"} ${n}`));
|
|
2098
|
-
},
|
|
2141
|
+
}, Fn = async (e) => {
|
|
2099
2142
|
try {
|
|
2100
2143
|
let { payloadPropertyNames: t } = await import("@voltro/runtime"), { discovered: n } = await Z(e);
|
|
2101
|
-
return
|
|
2144
|
+
return it(n.events.map((e) => {
|
|
2102
2145
|
let n = e.descriptor, r = t(n.key);
|
|
2103
2146
|
return {
|
|
2104
2147
|
name: n.name,
|
|
@@ -2110,17 +2153,17 @@ var re = () => {
|
|
|
2110
2153
|
} catch {
|
|
2111
2154
|
return null;
|
|
2112
2155
|
}
|
|
2113
|
-
},
|
|
2114
|
-
let t = await
|
|
2156
|
+
}, In = async (e) => {
|
|
2157
|
+
let t = await Fn(e);
|
|
2115
2158
|
if (t === null || t.eventsChecked === 0) return;
|
|
2116
|
-
let n =
|
|
2159
|
+
let n = at(t);
|
|
2117
2160
|
n !== null && (console.log(""), console.log(`• ${n}`));
|
|
2118
|
-
let r =
|
|
2161
|
+
let r = ot(t);
|
|
2119
2162
|
r !== null && (console.log(""), console.log(`⚠️ ${r}`));
|
|
2120
|
-
},
|
|
2163
|
+
}, Ln = (e) => {
|
|
2121
2164
|
let t = e, n = typeof t?.message == "string" && t.message !== "" ? t.message : String(e), r = typeof t?.code == "string" ? t.code : void 0;
|
|
2122
2165
|
return r === void 0 ? n : `${r}: ${n}`;
|
|
2123
|
-
},
|
|
2166
|
+
}, Rn = async (e) => {
|
|
2124
2167
|
try {
|
|
2125
2168
|
let { formatServerOnlyLeaks: t } = await import("@voltro/runtime"), n = (await Z(e)).discovered.serverOnlyLeaks, r = t(n);
|
|
2126
2169
|
return {
|
|
@@ -2133,17 +2176,17 @@ var re = () => {
|
|
|
2133
2176
|
} catch (e) {
|
|
2134
2177
|
return { status: {
|
|
2135
2178
|
checked: !1,
|
|
2136
|
-
reason:
|
|
2179
|
+
reason: Ln(e)
|
|
2137
2180
|
} };
|
|
2138
2181
|
}
|
|
2139
|
-
},
|
|
2140
|
-
let { status: t, message: n } = await
|
|
2182
|
+
}, zn = async (e) => {
|
|
2183
|
+
let { status: t, message: n } = await Rn(e);
|
|
2141
2184
|
return t.checked ? n === void 0 ? !1 : (console.log(""), console.log(`✗ [serverOnly] ${n}`), !0) : (console.log(""), console.log("• serverOnly: NOT CHECKED — the app's descriptors could not be loaded (not a pass)"), console.log(` reason: ${t.reason ?? "unknown"}`), console.log(" This audit is the automated check that a hash or a token cannot reach the wire, so"), console.log(" an app that leans on `.serverOnly()` should treat a persistent skip as a failure."), console.log(" `voltro doctor --json` reports it as `serverOnly.checked: false` for a CI gate."), !1);
|
|
2142
|
-
},
|
|
2185
|
+
}, Bn = async (e) => {
|
|
2143
2186
|
let t;
|
|
2144
2187
|
try {
|
|
2145
2188
|
let { payloadPropertyNames: n } = await import("@voltro/runtime"), { files: r, discovered: i } = await Z(e);
|
|
2146
|
-
t =
|
|
2189
|
+
t = lt(r, i.workflows.map((e) => {
|
|
2147
2190
|
let t = e.definition, r = t.payloadSchema === void 0 ? {
|
|
2148
2191
|
required: [],
|
|
2149
2192
|
optional: []
|
|
@@ -2157,18 +2200,18 @@ var re = () => {
|
|
|
2157
2200
|
} catch {
|
|
2158
2201
|
return;
|
|
2159
2202
|
}
|
|
2160
|
-
let n =
|
|
2203
|
+
let n = ut(t);
|
|
2161
2204
|
if (n === null) return;
|
|
2162
2205
|
console.log("");
|
|
2163
2206
|
let r = t.issues.length > 0 ? "✗" : t.unchecked.length > 0 ? "•" : "✓";
|
|
2164
2207
|
console.log(`${r} ${n}`);
|
|
2165
|
-
}, Q = "voltro-authz-allowlist.txt",
|
|
2208
|
+
}, Q = "voltro-authz-allowlist.txt", Vn = (e, t = (e) => {
|
|
2166
2209
|
try {
|
|
2167
2210
|
return E(e, "utf8");
|
|
2168
2211
|
} catch {
|
|
2169
2212
|
return "";
|
|
2170
2213
|
}
|
|
2171
|
-
}) => /* @__PURE__ */ new Set([...
|
|
2214
|
+
}) => /* @__PURE__ */ new Set([...wt, ...Tt(q(e).map(t))]), Hn = async (e) => {
|
|
2172
2215
|
let t = ["src/locales", "locales"], n, r = [];
|
|
2173
2216
|
for (let i of t) {
|
|
2174
2217
|
let t = x(e, i);
|
|
@@ -2180,23 +2223,23 @@ var re = () => {
|
|
|
2180
2223
|
if (n === void 0 || r.length === 0) return;
|
|
2181
2224
|
let i = [];
|
|
2182
2225
|
try {
|
|
2183
|
-
let { loadApiConfig: t } = await import("./dev-
|
|
2226
|
+
let { loadApiConfig: t } = await import("./dev-tPcOmjPJ.js");
|
|
2184
2227
|
i = (await t(e))?.locales ?? [];
|
|
2185
2228
|
} catch {
|
|
2186
2229
|
return;
|
|
2187
2230
|
}
|
|
2188
|
-
let a =
|
|
2231
|
+
let a = xt({
|
|
2189
2232
|
catalogCodes: r,
|
|
2190
2233
|
declaredLocales: i
|
|
2191
2234
|
});
|
|
2192
2235
|
if (a !== void 0) {
|
|
2193
2236
|
console.log("");
|
|
2194
|
-
for (let e of
|
|
2237
|
+
for (let e of St(a, n)) console.log(e);
|
|
2195
2238
|
}
|
|
2196
|
-
},
|
|
2239
|
+
}, Un = async (e) => {
|
|
2197
2240
|
let { discovered: t } = await Z(e), n = [];
|
|
2198
2241
|
try {
|
|
2199
|
-
let { loadApiConfig: t } = await import("./dev-
|
|
2242
|
+
let { loadApiConfig: t } = await import("./dev-tPcOmjPJ.js");
|
|
2200
2243
|
n = ((await t(e))?.plugins ?? []).map((e) => {
|
|
2201
2244
|
let t = (e.extendSchema?.tables ?? []).map((e) => {
|
|
2202
2245
|
let t = e;
|
|
@@ -2212,8 +2255,8 @@ var re = () => {
|
|
|
2212
2255
|
plugin: e.name,
|
|
2213
2256
|
tables: t,
|
|
2214
2257
|
rpcTags: n,
|
|
2215
|
-
canDisableTables:
|
|
2216
|
-
canAlias:
|
|
2258
|
+
canDisableTables: Wn.has(e.name),
|
|
2259
|
+
canAlias: Gn.has(e.name)
|
|
2217
2260
|
};
|
|
2218
2261
|
});
|
|
2219
2262
|
} catch {
|
|
@@ -2225,14 +2268,14 @@ var re = () => {
|
|
|
2225
2268
|
...t.mutations,
|
|
2226
2269
|
...t.actions,
|
|
2227
2270
|
...t.streams ?? []
|
|
2228
|
-
].map((e) => String(e.descriptor.name)), i =
|
|
2271
|
+
].map((e) => String(e.descriptor.name)), i = Rt(n, {
|
|
2229
2272
|
tables: t.tables.map((e) => e.tableName),
|
|
2230
2273
|
rpcTags: r
|
|
2231
2274
|
});
|
|
2232
2275
|
console.log();
|
|
2233
|
-
for (let e of
|
|
2234
|
-
},
|
|
2235
|
-
let t = await
|
|
2276
|
+
for (let e of zt(n, i)) console.log(e);
|
|
2277
|
+
}, Wn = /* @__PURE__ */ new Set(["@voltro/plugin-rbac"]), Gn = /* @__PURE__ */ new Set(["@voltro/plugin-ai-flows"]), $ = 20, Kn = async (e) => {
|
|
2278
|
+
let t = await qn(e), n = new Set(t.reviewed.map((e) => e.tag)), r = t.result.findings.filter((e) => e.verdict.kind === "allowlisted" && !n.has(e.tag)).length;
|
|
2236
2279
|
return {
|
|
2237
2280
|
executors: t.executorCount,
|
|
2238
2281
|
guardVocabulary: [...t.vocabulary].sort(),
|
|
@@ -2244,7 +2287,7 @@ var re = () => {
|
|
|
2244
2287
|
},
|
|
2245
2288
|
unchecked: t.result.unchecked
|
|
2246
2289
|
};
|
|
2247
|
-
},
|
|
2290
|
+
}, qn = async (e) => {
|
|
2248
2291
|
let { discovered: t } = await Z(e), n = /* @__PURE__ */ new Map(), r = (e) => {
|
|
2249
2292
|
if (!n.has(e)) try {
|
|
2250
2293
|
n.set(e, E(e, "utf8"));
|
|
@@ -2252,7 +2295,7 @@ var re = () => {
|
|
|
2252
2295
|
n.set(e, "");
|
|
2253
2296
|
}
|
|
2254
2297
|
return n.get(e);
|
|
2255
|
-
}, i =
|
|
2298
|
+
}, i = Vn(e, r), a = /* @__PURE__ */ new Set();
|
|
2256
2299
|
for (let e of t.tables) for (let t of Object.values(e.fields ?? {})) try {
|
|
2257
2300
|
let e = t.references?.();
|
|
2258
2301
|
e && a.add(e.tableName);
|
|
@@ -2284,56 +2327,66 @@ var re = () => {
|
|
|
2284
2327
|
op: a?.op === "delete" || a?.op === "insert" ? a.op : t,
|
|
2285
2328
|
...a?.table === void 0 ? {} : { table: a.table }
|
|
2286
2329
|
};
|
|
2287
|
-
})), s = x(e, Q), c = T(s) ?
|
|
2330
|
+
})), s = x(e, Q), c = T(s) ? W(E(s, "utf8")) : void 0, l = !1;
|
|
2331
|
+
for (let t of q(e)) if (/\bsetRowFilter\s*[<(]/.test(r(t))) {
|
|
2332
|
+
l = !0;
|
|
2333
|
+
break;
|
|
2334
|
+
}
|
|
2335
|
+
let u = jt({
|
|
2336
|
+
executors: o,
|
|
2337
|
+
guardVocabulary: i,
|
|
2338
|
+
tenantScopedTables: t.tenantScopedTables,
|
|
2339
|
+
referencedTables: a,
|
|
2340
|
+
allowlist: c?.tags ?? /* @__PURE__ */ new Set(),
|
|
2341
|
+
rowFilterRegistered: l
|
|
2342
|
+
}), d = (c?.entries ?? []).filter((e) => e.reviewed !== void 0);
|
|
2288
2343
|
return {
|
|
2289
|
-
result:
|
|
2290
|
-
executors: o,
|
|
2291
|
-
guardVocabulary: i,
|
|
2292
|
-
tenantScopedTables: t.tenantScopedTables,
|
|
2293
|
-
referencedTables: a,
|
|
2294
|
-
allowlist: c?.tags ?? /* @__PURE__ */ new Set()
|
|
2295
|
-
}),
|
|
2344
|
+
result: u,
|
|
2296
2345
|
vocabulary: i,
|
|
2346
|
+
rowFilterRegistered: l,
|
|
2297
2347
|
existing: c,
|
|
2298
|
-
reviewed:
|
|
2348
|
+
reviewed: d,
|
|
2299
2349
|
executorCount: o.length,
|
|
2300
2350
|
allowPath: s
|
|
2301
2351
|
};
|
|
2302
|
-
},
|
|
2303
|
-
let { result: n, vocabulary: r, existing: i, reviewed: a, executorCount: o, allowPath: s } = await
|
|
2352
|
+
}, Jn = async (e, t) => {
|
|
2353
|
+
let { result: n, vocabulary: r, existing: i, reviewed: a, executorCount: o, allowPath: s, rowFilterRegistered: c } = await qn(e), l = n.counts;
|
|
2304
2354
|
console.log(`\nauthz scan · ${o} executor(s)`);
|
|
2305
|
-
let
|
|
2306
|
-
console.log(
|
|
2307
|
-
let
|
|
2308
|
-
if (console.log(` – accepted as recorded debt ${String(
|
|
2355
|
+
let u = [...r].filter((e) => !wt.has(e)).sort();
|
|
2356
|
+
console.log(u.length === 0 ? " guard vocabulary: framework names only — no exported require*/assert* found in this app" : ` guard vocabulary: ${u.length} from your source (${u.slice(0, 6).join(", ")}${u.length > 6 ? ", …" : ""})`), console.log(` ✗ no access check ${String(l.unchecked).padStart(4)}`), console.log(` ⚠ inline ownership check, no named guard${String(l.inlineOwnership).padStart(4)} (informational)`), console.log(` ✓ guards: on the descriptor ${String(l.declaredGuard).padStart(4)}`), console.log(` ✓ calls a guard from the vocabulary ${String(l.vocabularyGuard).padStart(4)}`);
|
|
2357
|
+
let d = new Set(a.map((e) => e.tag)), f = n.findings.filter((e) => e.verdict.kind === "allowlisted" && !d.has(e.tag)).length, p = l.allowlisted - f;
|
|
2358
|
+
if (console.log(` – accepted as recorded debt ${String(f).padStart(4)} (${Q})`), console.log(` ✓ reviewed, open on purpose ${String(p).padStart(4)} (\`reviewed=<why>\` in that file)`), c && console.log(" ⓘ this app registers setRowFilter — the scan CANNOT see through it. A finding below may\n already be constrained; verify the filter covers that table and record it as `reviewed=`."), t) {
|
|
2359
|
+
let e = T(s) ? E(s, "utf8") : void 0, t = n.unchecked.map((e) => e.tag), r = Ft(e, t);
|
|
2360
|
+
return ne(s, Pt(e, t), "utf8"), console.log(`\n${Q}: +${r.added} new, ${r.kept} kept, ${r.removed} removed. Doctor now fails on ADDITIONS only.`), r.added === 0 && console.log(" (nothing new was unchecked — the file is unchanged.)"), !1;
|
|
2361
|
+
}
|
|
2309
2362
|
if (n.unchecked.length === 0) return !1;
|
|
2310
2363
|
console.log("");
|
|
2311
2364
|
for (let e of n.unchecked.slice(0, $)) console.log(` ✗ ${e.tag} — ${e.why}\n ${e.path}`);
|
|
2312
2365
|
return n.unchecked.length > $ && console.log(` … and ${n.unchecked.length - $} more — \`voltro doctor --json\` prints every one (\`authz.unchecked\`).`), i === void 0 ? (console.log(`\nNo ${Q} yet, so this run is advisory. To start the ratchet:`), console.log("\n voltro doctor --write-authz-allowlist\n"), console.log("That records today's unchecked handlers as DEBT and fails every run after on"), console.log("anything NEW. Fix a handler and its line stops mattering — the scan reports it as"), console.log("guarded whether or not the line is still there."), !1) : (console.log(`\n${n.unchecked.length} executor(s) with no access check are not in ${Q}.`), console.log("Add a guard, or add the tag to that file if it is genuinely open."), console.log("\nBefore hand-rolling another check: `guards: [{ action, resourceType, resource }]`"), console.log("answers \"may this subject act on THIS row\" declaratively, and an app whose"), console.log("relationships live in its own tables registers its own tuple source rather than"), console.log("copying data into a framework table. See the authorization docs."), !0);
|
|
2313
|
-
},
|
|
2314
|
-
if (e.includes("--help") || e.includes("-h")) return console.log(
|
|
2366
|
+
}, Yn = async (e) => {
|
|
2367
|
+
if (e.includes("--help") || e.includes("-h")) return console.log(kn), 0;
|
|
2315
2368
|
let t = e.includes("--strict"), n = C(e.find((e) => !e.startsWith("-")) ?? process.cwd());
|
|
2316
2369
|
if (ie(e)) return re(), M({
|
|
2317
|
-
...
|
|
2318
|
-
spaCandidates: await
|
|
2319
|
-
pageConvention:
|
|
2320
|
-
fileTaxonomy:
|
|
2321
|
-
junctionFks: (await
|
|
2322
|
-
eventDelivery: await
|
|
2323
|
-
duplicateInstances:
|
|
2324
|
-
serverOnly: (await
|
|
2325
|
-
authz: await
|
|
2370
|
+
...un(n),
|
|
2371
|
+
spaCandidates: await xn(n),
|
|
2372
|
+
pageConvention: Y(n),
|
|
2373
|
+
fileTaxonomy: V(n).findings,
|
|
2374
|
+
junctionFks: (await Nn(n))?.issues ?? [],
|
|
2375
|
+
eventDelivery: await Fn(n),
|
|
2376
|
+
duplicateInstances: yt(n),
|
|
2377
|
+
serverOnly: (await Rn(n)).status,
|
|
2378
|
+
authz: await Kn(n)
|
|
2326
2379
|
}), 0;
|
|
2327
|
-
let r =
|
|
2328
|
-
|
|
2329
|
-
let i = await
|
|
2380
|
+
let r = Ut(n, t);
|
|
2381
|
+
gn(n), hn(n), On(n), He(n), await Cn(n), await Bn(n), await jn(n), await Pn(n), await In(n), await Un(n), await Hn(n);
|
|
2382
|
+
let i = await zn(n), a = await Jn(n, e.includes("--write-authz-allowlist")), o = bt(yt(n));
|
|
2330
2383
|
return o !== void 0 && console.log(`\n${o}`), i || a ? Math.max(r, 1) : r;
|
|
2331
|
-
},
|
|
2384
|
+
}, Xn = (e) => e === void 0 ? [] : typeof e == "string" ? [e] : e, Zn = /* @__PURE__ */ new Set([
|
|
2332
2385
|
"insert",
|
|
2333
2386
|
"update",
|
|
2334
2387
|
"delete",
|
|
2335
2388
|
"upsert"
|
|
2336
|
-
]),
|
|
2389
|
+
]), Qn = (e, t) => {
|
|
2337
2390
|
let n = [], r = new Set(t.exercised), i = /* @__PURE__ */ new Map();
|
|
2338
2391
|
for (let e of t.edges) {
|
|
2339
2392
|
let t = i.get(e.tag);
|
|
@@ -2345,8 +2398,8 @@ var re = () => {
|
|
|
2345
2398
|
a.push(t.tag);
|
|
2346
2399
|
continue;
|
|
2347
2400
|
}
|
|
2348
|
-
let e = `${t.kind}(${t.tag})`, s = t.file, c = i.get(t.tag) ?? [], l = new Set(
|
|
2349
|
-
for (let t of c) if (
|
|
2401
|
+
let e = `${t.kind}(${t.tag})`, s = t.file, c = i.get(t.tag) ?? [], l = new Set(Xn(t.source)), u = new Map((t.targets ?? []).map((e) => [e.table, e.op]));
|
|
2402
|
+
for (let t of c) if (Zn.has(t.op)) {
|
|
2350
2403
|
let r = u.get(t.table);
|
|
2351
2404
|
r === void 0 ? n.push({
|
|
2352
2405
|
rule: "observed/undeclared-write",
|
|
@@ -2402,7 +2455,7 @@ var re = () => {
|
|
|
2402
2455
|
total: e.length
|
|
2403
2456
|
}
|
|
2404
2457
|
};
|
|
2405
|
-
},
|
|
2458
|
+
}, $n = (e, t = [], n = 0) => {
|
|
2406
2459
|
if (n > 12) return t;
|
|
2407
2460
|
let r;
|
|
2408
2461
|
try {
|
|
@@ -2426,24 +2479,24 @@ var re = () => {
|
|
|
2426
2479
|
} catch {
|
|
2427
2480
|
continue;
|
|
2428
2481
|
}
|
|
2429
|
-
o.isDirectory() ?
|
|
2482
|
+
o.isDirectory() ? $n(r, t, n + 1) : o.isFile() && a.test(i) && t.push(r);
|
|
2430
2483
|
}
|
|
2431
2484
|
return t;
|
|
2432
|
-
},
|
|
2485
|
+
}, er = /* @__PURE__ */ new Set(["actors", "tenants"]), tr = async (e) => {
|
|
2433
2486
|
try {
|
|
2434
2487
|
let { readFile: t } = await import("node:fs/promises"), { join: n } = await import("node:path"), r = await t(n(e, "app.graph.observed.generated.json"), "utf8"), i = JSON.parse(r);
|
|
2435
2488
|
return i?.version !== 1 || !Array.isArray(i.edges) || !Array.isArray(i.exercised) ? null : i;
|
|
2436
2489
|
} catch {
|
|
2437
2490
|
return null;
|
|
2438
2491
|
}
|
|
2439
|
-
},
|
|
2492
|
+
}, nr = (e) => {
|
|
2440
2493
|
let { exercised: t, total: n } = e.coverage, r = `\nobserved: ${t}/${n} procedures exercised (${n === 0 ? 0 : Math.round(t / n * 100)}%)`;
|
|
2441
2494
|
if (e.diagnostics.length === 0) return `${r}\n no declared/observed mismatches${e.unexercised.length > 0 ? `\n ${e.unexercised.length} unexercised — no recording, so nothing is claimed about them` : ""}`;
|
|
2442
2495
|
let i = e.diagnostics.map((e) => ` ${e.node}${e.file === void 0 ? "" : ` (${e.file})`}\n` + e.breaks.map((e) => ` ${e}`).join("\n") + `\n fix: ${e.fix}`), a = e.unexercised.length > 0 ? `\n ${e.unexercised.length} unexercised — no recording, so nothing is claimed about them` : "";
|
|
2443
2496
|
return `${r}\n${i.join("\n")}${a}`;
|
|
2444
|
-
},
|
|
2497
|
+
}, rr = (e, t) => ({
|
|
2445
2498
|
tables: (t.tables ?? []).map((e) => e.name),
|
|
2446
|
-
frameworkOwnedTables: (t.tables ?? []).filter((e) => e.framework === !0 || e.name.startsWith("_voltro_") ||
|
|
2499
|
+
frameworkOwnedTables: (t.tables ?? []).filter((e) => e.framework === !0 || e.name.startsWith("_voltro_") || er.has(e.name)).map((e) => e.name),
|
|
2447
2500
|
scopes: e.scopes ?? [],
|
|
2448
2501
|
procedures: (e.procedures ?? []).map((e) => {
|
|
2449
2502
|
let t = (e.guards ?? []).flatMap((e) => e.kind === "scope" ? e.scope ?? [] : []);
|
|
@@ -2456,16 +2509,16 @@ var re = () => {
|
|
|
2456
2509
|
...e.guards === void 0 ? {} : { hasGuards: e.guards.length > 0 }
|
|
2457
2510
|
};
|
|
2458
2511
|
})
|
|
2459
|
-
}),
|
|
2512
|
+
}), ir = (e) => e.length === 0 ? "✓ voltro check: no issues" : e.map((e) => {
|
|
2460
2513
|
let t = e.severity === "error" ? "✗ error" : "⚠ warn", n = e.file ? ` (${e.file})` : "";
|
|
2461
2514
|
return `${t} ${e.rule} ${e.node}${n}\n ${e.breaks.join("; ")}\n fix: ${e.fix}`;
|
|
2462
|
-
}).join("\n"),
|
|
2463
|
-
let { walk: t, loadDiscovered: n, loadApiConfig: r } = await import("./dev-
|
|
2515
|
+
}).join("\n"), ar = async (e) => {
|
|
2516
|
+
let { walk: t, loadDiscovered: n, loadApiConfig: r } = await import("./dev-tPcOmjPJ.js"), { buildRpcEntry: i } = await import("./manifestBuild-Dj8Jjoto.js"), { buildCapabilityManifest: a } = await import("./inspect-mmBuRXmy.js"), { relative: o } = await import("node:path"), s = await n(await t(e)), c = (t, n, r) => i(t, n, o(e, r)), l = [
|
|
2464
2517
|
...s.queries.map((e) => c(e.descriptor, "query", e.file)),
|
|
2465
2518
|
...s.mutations.map((e) => c(e.descriptor, "mutation", e.file)),
|
|
2466
2519
|
...s.actions.map((e) => c(e.descriptor, "action", e.file)),
|
|
2467
2520
|
...s.streams.map((e) => c(e.descriptor, "stream", e.file))
|
|
2468
|
-
], { assembleFrameworkTables: u } = await import("./frameworkTableAssembly-
|
|
2521
|
+
], { assembleFrameworkTables: u } = await import("./frameworkTableAssembly-D-EebUQX.js"), d = await u({ root: e }).catch((e) => (console.warn(`\n⚠ plugin tables could not be collected — this report is INCOMPLETE.\n ${e instanceof Error ? e.message.split("\n")[0] : String(e)}\n`), [])), f = new Set(s.tables.map((e) => e.tableName)), p = [...[...f].map((e) => ({ name: e })), ...d.map((e) => e.tableName).filter((e) => !f.has(e)).map((e) => ({
|
|
2469
2522
|
name: e,
|
|
2470
2523
|
framework: !0
|
|
2471
2524
|
}))], m = await r(e).then((e) => e?.plugins ?? []).catch(() => []);
|
|
@@ -2476,7 +2529,7 @@ var re = () => {
|
|
|
2476
2529
|
})), [], m),
|
|
2477
2530
|
tables: { tables: p }
|
|
2478
2531
|
};
|
|
2479
|
-
},
|
|
2532
|
+
}, or = async (e) => {
|
|
2480
2533
|
let t = ue(e.flatMap((e) => e === "--json" ? ["--format", "json"] : [e]));
|
|
2481
2534
|
if ("error" in t) return console.error(`voltro check: ${t.error}`), 1;
|
|
2482
2535
|
let { process: n, target: r, rest: i } = de(t.rest), a;
|
|
@@ -2500,7 +2553,7 @@ var re = () => {
|
|
|
2500
2553
|
if (l.length === 0) {
|
|
2501
2554
|
if (!c && r !== void 0) return console.error("voltro check: --url given but no api reachable there."), 1;
|
|
2502
2555
|
try {
|
|
2503
|
-
let e = await
|
|
2556
|
+
let e = await ar(C(s));
|
|
2504
2557
|
u = e.manifest, d = e.tables;
|
|
2505
2558
|
} catch (e) {
|
|
2506
2559
|
return console.error(`voltro check: could not assemble the graph from source: ${e.message}`), console.error(" Start `voltro dev` to check against a running api instead."), 1;
|
|
@@ -2510,21 +2563,21 @@ var re = () => {
|
|
|
2510
2563
|
if (t.kind === "err") return console.error(`voltro check: manifest fetch failed: ${t.error}`), 1;
|
|
2511
2564
|
u = t.data, d = n.kind === "ok" ? n.data : { tables: [] };
|
|
2512
2565
|
}
|
|
2513
|
-
let f =
|
|
2566
|
+
let f = rr(u, d), p = he(f, a), m = [...V(s).findings.map((e) => ({
|
|
2514
2567
|
rule: e.rule,
|
|
2515
2568
|
node: e.file,
|
|
2516
2569
|
file: e.file,
|
|
2517
2570
|
breaks: [e.detail],
|
|
2518
2571
|
fix: e.fix,
|
|
2519
2572
|
severity: e.severity
|
|
2520
|
-
})), ...
|
|
2573
|
+
})), ...Y(s).map((e) => ({
|
|
2521
2574
|
rule: `page/${e.kind}`,
|
|
2522
2575
|
node: e.file,
|
|
2523
2576
|
file: e.file,
|
|
2524
2577
|
breaks: [e.kind === "suffixed-outside-pages" ? "a page.tsx outside src/pages/ never routes" : "no test beside it"],
|
|
2525
2578
|
fix: e.kind === "suffixed-outside-pages" ? "move it under src/pages/ into the directory whose route it serves, or rename it — page.tsx is reserved" : "write the test with the file — the name is derivable",
|
|
2526
2579
|
severity: e.kind === "suffixed-outside-pages" ? "error" : "warn"
|
|
2527
|
-
}))], h = (await o(s,
|
|
2580
|
+
}))], h = (await o(s, $n(s))).map((e) => ({
|
|
2528
2581
|
rule: "client/not-browser-safe",
|
|
2529
2582
|
node: e.file,
|
|
2530
2583
|
file: e.file,
|
|
@@ -2535,7 +2588,7 @@ var re = () => {
|
|
|
2535
2588
|
...p.diagnostics,
|
|
2536
2589
|
...m,
|
|
2537
2590
|
...h
|
|
2538
|
-
], _ = p.ok && !m.some((e) => e.severity === "error") && h.length === 0, v = await
|
|
2591
|
+
], _ = p.ok && !m.some((e) => e.severity === "error") && h.length === 0, v = await tr(C(s)), y = v === null ? null : Qn(f.procedures, v);
|
|
2539
2592
|
return t.format === "json" ? M(y === null ? {
|
|
2540
2593
|
...p,
|
|
2541
2594
|
ok: _,
|
|
@@ -2545,7 +2598,7 @@ var re = () => {
|
|
|
2545
2598
|
ok: _,
|
|
2546
2599
|
diagnostics: g,
|
|
2547
2600
|
observed: y
|
|
2548
|
-
}) : (console.log(
|
|
2601
|
+
}) : (console.log(ir(g)), y !== null && console.log(nr(y)), console.log(_ ? "\nvoltro check: OK" : "\nvoltro check: FAILED (errors above)")), +!_;
|
|
2549
2602
|
};
|
|
2550
2603
|
//#endregion
|
|
2551
|
-
export { re as _,
|
|
2604
|
+
export { re as _, ze as a, I as c, N as d, ue as f, le as g, ce as h, Ut as i, ve as l, se as m, or as n, be as o, de as p, Yn as r, xe as s, ar as t, he as u, M as v, ie as y };
|