@vantageos/mcp-doctor 0.4.0 → 0.6.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.
- package/CHANGELOG.md +132 -4
- package/README.md +26 -4
- package/dist/doctor.d.ts +9 -0
- package/dist/doctor.js +1880 -176
- package/package.json +3 -7
package/dist/doctor.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/doctor.ts
|
|
4
|
-
import { readFileSync as
|
|
5
|
-
import { dirname as
|
|
4
|
+
import { readFileSync as readFileSync13, realpathSync } from "fs";
|
|
5
|
+
import { dirname as dirname13, join as join20, resolve as resolve3 } from "path";
|
|
6
6
|
import { fileURLToPath } from "url";
|
|
7
7
|
|
|
8
8
|
// src/scan.ts
|
|
@@ -114,11 +114,12 @@ function findPackageJson(startDir) {
|
|
|
114
114
|
}
|
|
115
115
|
return null;
|
|
116
116
|
}
|
|
117
|
-
function loadTree(root) {
|
|
117
|
+
function loadTree(root, backendRoot) {
|
|
118
118
|
const st = statSync(root);
|
|
119
119
|
const baseDir = st.isDirectory() ? root : dirname(root);
|
|
120
120
|
const filePaths = [];
|
|
121
121
|
const tree = { root, files: [], packageJson: null, skippedDirs: [] };
|
|
122
|
+
if (backendRoot !== void 0) tree.backendRoot = backendRoot;
|
|
122
123
|
if (st.isDirectory()) walk(root, filePaths, (d) => recordSkip(tree, d, root));
|
|
123
124
|
else if (root.endsWith(".ts") || root.endsWith(".tsx")) filePaths.push(root);
|
|
124
125
|
tree.files = filePaths.map((p) => loadFile(p, baseDir));
|
|
@@ -128,9 +129,14 @@ function loadTree(root) {
|
|
|
128
129
|
var COM = 1;
|
|
129
130
|
var CODE = 2;
|
|
130
131
|
var maskCache = /* @__PURE__ */ new WeakMap();
|
|
132
|
+
var maskEnd = /* @__PURE__ */ new WeakMap();
|
|
131
133
|
function buildCodeMask(file) {
|
|
132
134
|
const mask = [];
|
|
133
135
|
let mode = "code";
|
|
136
|
+
const jsxFile = /\.[tj]sx$/.test(file.path);
|
|
137
|
+
let jsxDepth = 0;
|
|
138
|
+
let jstrQuote = "";
|
|
139
|
+
const jsxExprStack = [];
|
|
134
140
|
const tplStack = [];
|
|
135
141
|
let braceDepth = 0;
|
|
136
142
|
let prevSig = "";
|
|
@@ -192,6 +198,75 @@ function buildCodeMask(file) {
|
|
|
192
198
|
} else i++;
|
|
193
199
|
continue;
|
|
194
200
|
}
|
|
201
|
+
if (mode === "jstr") {
|
|
202
|
+
if (c === jstrQuote) mode = "jtag";
|
|
203
|
+
i++;
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
if (mode === "jtag") {
|
|
207
|
+
if (c === "/" && n === ">") {
|
|
208
|
+
row[i] = CODE;
|
|
209
|
+
row[i + 1] = CODE;
|
|
210
|
+
i += 2;
|
|
211
|
+
mode = jsxDepth > 0 ? "jtext" : "code";
|
|
212
|
+
if (mode === "code") prevSig = "x";
|
|
213
|
+
} else if (c === ">") {
|
|
214
|
+
row[i] = CODE;
|
|
215
|
+
i++;
|
|
216
|
+
jsxDepth++;
|
|
217
|
+
mode = "jtext";
|
|
218
|
+
} else if (c === '"' || c === "'") {
|
|
219
|
+
jstrQuote = c;
|
|
220
|
+
mode = "jstr";
|
|
221
|
+
i++;
|
|
222
|
+
} else if (c === "{") {
|
|
223
|
+
row[i] = CODE;
|
|
224
|
+
jsxExprStack.push({ depth: braceDepth, back: "jtag", savedJsxDepth: jsxDepth });
|
|
225
|
+
jsxDepth = 0;
|
|
226
|
+
braceDepth++;
|
|
227
|
+
i++;
|
|
228
|
+
mode = "code";
|
|
229
|
+
prevSig = "{";
|
|
230
|
+
} else {
|
|
231
|
+
if (c !== " " && c !== " ") row[i] = CODE;
|
|
232
|
+
i++;
|
|
233
|
+
}
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
if (mode === "jtext") {
|
|
237
|
+
if (c === "{") {
|
|
238
|
+
row[i] = CODE;
|
|
239
|
+
jsxExprStack.push({ depth: braceDepth, back: "jtext", savedJsxDepth: jsxDepth });
|
|
240
|
+
jsxDepth = 0;
|
|
241
|
+
braceDepth++;
|
|
242
|
+
i++;
|
|
243
|
+
mode = "code";
|
|
244
|
+
prevSig = "{";
|
|
245
|
+
} else if (c === "<" && n === "/") {
|
|
246
|
+
row[i] = CODE;
|
|
247
|
+
row[i + 1] = CODE;
|
|
248
|
+
i += 2;
|
|
249
|
+
mode = "jclose";
|
|
250
|
+
} else if (c === "<" && n !== void 0 && /[A-Za-z_$>]/.test(n)) {
|
|
251
|
+
row[i] = CODE;
|
|
252
|
+
i++;
|
|
253
|
+
mode = "jtag";
|
|
254
|
+
} else i++;
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
if (mode === "jclose") {
|
|
258
|
+
if (c === ">") {
|
|
259
|
+
row[i] = CODE;
|
|
260
|
+
i++;
|
|
261
|
+
jsxDepth = Math.max(0, jsxDepth - 1);
|
|
262
|
+
mode = jsxDepth > 0 ? "jtext" : "code";
|
|
263
|
+
if (mode === "code") prevSig = "x";
|
|
264
|
+
} else {
|
|
265
|
+
if (c !== " " && c !== " ") row[i] = CODE;
|
|
266
|
+
i++;
|
|
267
|
+
}
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
195
270
|
if (mode === "sq" || mode === "dq") {
|
|
196
271
|
if (c === "\\") i += 2;
|
|
197
272
|
else if (mode === "sq" && c === "'" || mode === "dq" && c === '"') {
|
|
@@ -259,6 +334,11 @@ function buildCodeMask(file) {
|
|
|
259
334
|
i = j;
|
|
260
335
|
continue;
|
|
261
336
|
}
|
|
337
|
+
if (jsxFile && c === "<" && n !== void 0 && /[A-Za-z_$>]/.test(n) && (prevSig === "" || "(,=:?&|{};![".includes(prevSig) || wordBefore === "return" || prevSig === ">" && /=>\s*$/.test(s.slice(0, i)) || prevSig === "}") && !/^<[A-Za-z_$][\w$]*\s+extends\b/.test(s.slice(i)) && !/^<[A-Za-z_$][\w$]*\s*,/.test(s.slice(i))) {
|
|
338
|
+
i++;
|
|
339
|
+
mode = "jtag";
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
262
342
|
if (c === '"' || c === "'" || c === "`") {
|
|
263
343
|
mode = c === '"' ? "dq" : c === "'" ? "sq" : "tpl";
|
|
264
344
|
i++;
|
|
@@ -278,6 +358,14 @@ function buildCodeMask(file) {
|
|
|
278
358
|
i++;
|
|
279
359
|
continue;
|
|
280
360
|
}
|
|
361
|
+
const jx = jsxExprStack[jsxExprStack.length - 1];
|
|
362
|
+
if (jx && jx.depth === braceDepth) {
|
|
363
|
+
jsxExprStack.pop();
|
|
364
|
+
jsxDepth = jx.savedJsxDepth;
|
|
365
|
+
mode = jx.back;
|
|
366
|
+
i++;
|
|
367
|
+
continue;
|
|
368
|
+
}
|
|
281
369
|
}
|
|
282
370
|
if (c !== " " && c !== " ") prevSig = c;
|
|
283
371
|
i++;
|
|
@@ -285,6 +373,7 @@ function buildCodeMask(file) {
|
|
|
285
373
|
if (mode === "sq" || mode === "dq" || mode === "re" || mode === "recls") mode = "code";
|
|
286
374
|
mask.push(row);
|
|
287
375
|
}
|
|
376
|
+
if (mode !== "code") maskEnd.set(file, mode);
|
|
288
377
|
return mask;
|
|
289
378
|
}
|
|
290
379
|
function maskOf(file) {
|
|
@@ -295,6 +384,12 @@ function maskOf(file) {
|
|
|
295
384
|
}
|
|
296
385
|
return mask;
|
|
297
386
|
}
|
|
387
|
+
function unterminatedContext(file) {
|
|
388
|
+
maskOf(file);
|
|
389
|
+
const m = maskEnd.get(file);
|
|
390
|
+
if (!m) return null;
|
|
391
|
+
return m.startsWith("j") ? "an unterminated JSX element" : m === "block" ? "an unterminated block comment" : m === "tpl" ? "an unterminated template literal" : `an unterminated ${m} context`;
|
|
392
|
+
}
|
|
298
393
|
function isCodeAt(file, n, col) {
|
|
299
394
|
const mask = maskOf(file);
|
|
300
395
|
return mask[n - 1]?.[col] === CODE;
|
|
@@ -751,6 +846,34 @@ var MATCHERS = [
|
|
|
751
846
|
re: /\bcreateHash\s*\(|\bcreateHmac\s*\(|\bsubtle\s*\.\s*(digest|sign|verify)\s*\(/,
|
|
752
847
|
accept: (m, ctx) => !digestsStaticInput(m, ctx),
|
|
753
848
|
sample: 'const h = await crypto.subtle.digest("SHA-256", data);'
|
|
849
|
+
},
|
|
850
|
+
// ── Minting / signing: shown, not counted (category "mints-token") ──────────
|
|
851
|
+
{
|
|
852
|
+
id: "mint-sign-jwt",
|
|
853
|
+
category: "mints-token",
|
|
854
|
+
re: /\bnew\s+SignJWT\s*\(/,
|
|
855
|
+
sample: "return new SignJWT({ sub }).setProtectedHeader({ alg }).sign(key);"
|
|
856
|
+
},
|
|
857
|
+
{
|
|
858
|
+
id: "mint-jwt-sign",
|
|
859
|
+
category: "mints-token",
|
|
860
|
+
re: /\b(jwt|jsonwebtoken)\.sign\s*\(/,
|
|
861
|
+
sample: "const token = jwt.sign(claims, privateKey);"
|
|
862
|
+
},
|
|
863
|
+
{
|
|
864
|
+
// A key that can only be a private one by construction.
|
|
865
|
+
id: "mint-private-key-import",
|
|
866
|
+
category: "mints-token",
|
|
867
|
+
re: /\b(importPKCS8|createPrivateKey)\s*\(/,
|
|
868
|
+
sample: "const key = await importPKCS8(pem, 'RS256');"
|
|
869
|
+
},
|
|
870
|
+
{
|
|
871
|
+
// importJWK is neutral (it also loads PUBLIC verification keys); it is a minting
|
|
872
|
+
// site only when its argument is named as a private/secret key.
|
|
873
|
+
id: "mint-private-key-import",
|
|
874
|
+
category: "mints-token",
|
|
875
|
+
re: /\bimportJWK\s*\(\s*[\w$.]*(?:priv|secret)[\w$.]*/i,
|
|
876
|
+
sample: 'const key = await importJWK(privateJwk, "RS256");'
|
|
754
877
|
}
|
|
755
878
|
];
|
|
756
879
|
function biteSample(mt, text) {
|
|
@@ -781,6 +904,12 @@ function localIdentityPositiveControls() {
|
|
|
781
904
|
out["negative:switch-on-unrelated-field"] = !bites('switch (row.kind) { case "a": return 1; }');
|
|
782
905
|
out["negative:literal-array-unrelated"] = !bites("return [kind].includes(row.status);");
|
|
783
906
|
out["negative:localecompare-unrelated"] = !bites("return a.name.localeCompare(b.name) === 0;");
|
|
907
|
+
out["negative:public-jwk-import-is-not-minting"] = !bites(
|
|
908
|
+
'const key = await importJWK(publicJwk, "RS256");'
|
|
909
|
+
);
|
|
910
|
+
out["negative:verify-is-not-minting"] = !MATCHERS.filter(
|
|
911
|
+
(m) => m.category === "mints-token"
|
|
912
|
+
).some((mt) => biteSample(mt, "const { payload } = await jwtVerify(token, keys);"));
|
|
784
913
|
out["negative:object-is-nullish"] = !bites("return Object.is(row.orgId, undefined);");
|
|
785
914
|
return out;
|
|
786
915
|
}
|
|
@@ -860,19 +989,55 @@ function unreadableReason(file) {
|
|
|
860
989
|
if (file.lines.some((l) => l.raw.includes("\0") || l.raw.includes("\uFFFD"))) {
|
|
861
990
|
return "file is not valid UTF-8 text";
|
|
862
991
|
}
|
|
992
|
+
const open = unterminatedContext(file);
|
|
993
|
+
if (open) return `the file ends inside ${open} \u2014 the tail cannot be read as code`;
|
|
863
994
|
const pairs = [
|
|
864
995
|
["{", "}"],
|
|
865
996
|
["(", ")"],
|
|
866
997
|
["[", "]"]
|
|
867
998
|
];
|
|
868
999
|
const off = [];
|
|
869
|
-
for (const [
|
|
1000
|
+
for (const [open2, close] of pairs) {
|
|
870
1001
|
let net = 0;
|
|
871
|
-
for (const l of file.lines) net += countInCode(file, l,
|
|
872
|
-
if (net !== 0) off.push(`${
|
|
1002
|
+
for (const l of file.lines) net += countInCode(file, l, open2) - countInCode(file, l, close);
|
|
1003
|
+
if (net !== 0) off.push(`${open2}${close} ${net > 0 ? "+" : ""}${net}`);
|
|
873
1004
|
}
|
|
874
1005
|
return off.length > 0 ? `brackets never balance (${off.join(", ")}) \u2014 the file cannot be read as code` : null;
|
|
875
1006
|
}
|
|
1007
|
+
function signingChainLines(file) {
|
|
1008
|
+
const inChain = /* @__PURE__ */ new Set();
|
|
1009
|
+
const lines = file.lines;
|
|
1010
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1011
|
+
const l = lines[i];
|
|
1012
|
+
if (l.comment) continue;
|
|
1013
|
+
const start = l.raw.match(/\bnew\s+SignJWT\s*\(|\b(?:jwt|jsonwebtoken)\.sign\s*\(/);
|
|
1014
|
+
if (!start || start.index === void 0 || !isCodeAt(file, l.n, start.index)) continue;
|
|
1015
|
+
let end = -1;
|
|
1016
|
+
if (/SignJWT/.test(start[0])) {
|
|
1017
|
+
for (let j = i; j < Math.min(lines.length, i + 60); j++) {
|
|
1018
|
+
if (!lines[j].comment && /\.sign\s*\(/.test(lines[j].raw)) {
|
|
1019
|
+
end = j;
|
|
1020
|
+
break;
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
if (end < 0) {
|
|
1025
|
+
let depth = 0;
|
|
1026
|
+
let opened = false;
|
|
1027
|
+
for (let j = i; j < Math.min(lines.length, i + 60); j++) {
|
|
1028
|
+
const raw = j === i ? lines[j].raw.slice(start.index) : lines[j].raw;
|
|
1029
|
+
depth += (raw.match(/\(/g) ?? []).length - (raw.match(/\)/g) ?? []).length;
|
|
1030
|
+
if (/\(/.test(raw)) opened = true;
|
|
1031
|
+
if (opened && depth <= 0) {
|
|
1032
|
+
end = j;
|
|
1033
|
+
break;
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
for (let j = i; j <= (end < 0 ? i : end); j++) inChain.add(lines[j].n);
|
|
1038
|
+
}
|
|
1039
|
+
return inChain;
|
|
1040
|
+
}
|
|
876
1041
|
function findLocalIdentitySites(tree) {
|
|
877
1042
|
const pkg = tree.packageJson;
|
|
878
1043
|
const selfExempt = !pkg?.unreadable && pkg?.json.name === CLOUD_IDENTITY_PACKAGE;
|
|
@@ -881,6 +1046,7 @@ function findLocalIdentitySites(tree) {
|
|
|
881
1046
|
violations: [],
|
|
882
1047
|
exempt: [],
|
|
883
1048
|
delegatedCalls: 0,
|
|
1049
|
+
minting: [],
|
|
884
1050
|
unreadable: []
|
|
885
1051
|
};
|
|
886
1052
|
for (const file of tree.files) {
|
|
@@ -889,6 +1055,7 @@ function findLocalIdentitySites(tree) {
|
|
|
889
1055
|
const bindings = cloudIdentityBindings(file);
|
|
890
1056
|
const statics = staticConstants(file);
|
|
891
1057
|
const catalog = [...MATCHERS, ...aliasMatchers(file)];
|
|
1058
|
+
const signingChain = signingChainLines(file);
|
|
892
1059
|
scan.delegatedCalls += countDelegatedCalls(file, bindings);
|
|
893
1060
|
for (const line of file.lines) {
|
|
894
1061
|
if (line.comment) continue;
|
|
@@ -908,6 +1075,16 @@ function findLocalIdentitySites(tree) {
|
|
|
908
1075
|
}
|
|
909
1076
|
if (!hit) continue;
|
|
910
1077
|
const decl = enclosingDecl(file, line.n);
|
|
1078
|
+
if (hit.category === "mints-token" || signingChain.has(line.n)) {
|
|
1079
|
+
scan.minting.push({
|
|
1080
|
+
file: file.rel,
|
|
1081
|
+
line: line.n,
|
|
1082
|
+
primitive: hit.category === "mints-token" ? hit.id : `signing-payload (${hit.id})`,
|
|
1083
|
+
category: "mints-token",
|
|
1084
|
+
enclosing: decl
|
|
1085
|
+
});
|
|
1086
|
+
continue;
|
|
1087
|
+
}
|
|
911
1088
|
const site = {
|
|
912
1089
|
file: file.rel,
|
|
913
1090
|
line: line.n,
|
|
@@ -931,6 +1108,10 @@ function exemptSuffix(exempt) {
|
|
|
931
1108
|
const rows = exempt.map((e) => `${describeSite(e)} \u2014 "${e.justification}"`);
|
|
932
1109
|
return `; EXEMPT (${exempt.length}): ${rows.join("; ")}`;
|
|
933
1110
|
}
|
|
1111
|
+
function mintingSuffix(minting) {
|
|
1112
|
+
if (minting.length === 0) return "";
|
|
1113
|
+
return `; MINTING/SIGNING (${minting.length}, shown, not counted): ${minting.map(describeSite).join("; ")}`;
|
|
1114
|
+
}
|
|
934
1115
|
function makeLocalIdentity(opts = {}) {
|
|
935
1116
|
const ceiling = opts.ceiling ?? 0;
|
|
936
1117
|
if (!Number.isInteger(ceiling) || ceiling < 0) {
|
|
@@ -966,7 +1147,7 @@ function makeLocalIdentity(opts = {}) {
|
|
|
966
1147
|
id,
|
|
967
1148
|
anchor: `${v.file}:${v.line}`,
|
|
968
1149
|
state: "non-conforming",
|
|
969
|
-
verdict: `${s.violations.length} local identity primitive(s), ceiling ${ceiling} (positive-control OK): ${listed}${exemptSuffix(s.exempt)} -> delegate to ${CLOUD_IDENTITY_PACKAGE} (validateMasterBearer, verifyClerkSessionToken, isMasterScope, ...) or justify the site with "// ${EXEMPT_MARKER} <reason>"`
|
|
1150
|
+
verdict: `${s.violations.length} local identity primitive(s), ceiling ${ceiling} (positive-control OK): ${listed}${exemptSuffix(s.exempt)}${mintingSuffix(s.minting)} -> delegate to ${CLOUD_IDENTITY_PACKAGE} (validateMasterBearer, verifyClerkSessionToken, isMasterScope, ...) or justify the site with "// ${EXEMPT_MARKER} <reason>"`
|
|
970
1151
|
};
|
|
971
1152
|
} else if (s.violations.length > 0) {
|
|
972
1153
|
const v = s.violations[0];
|
|
@@ -974,14 +1155,14 @@ function makeLocalIdentity(opts = {}) {
|
|
|
974
1155
|
id,
|
|
975
1156
|
anchor: `${v.file}:${v.line}`,
|
|
976
1157
|
state: "present",
|
|
977
|
-
verdict: `${s.violations.length} local identity primitive(s) at or under the ceiling ${ceiling} \u2014 a ratchet, not a clean bill: ${listed}${exemptSuffix(s.exempt)}`
|
|
1158
|
+
verdict: `${s.violations.length} local identity primitive(s) at or under the ceiling ${ceiling} \u2014 a ratchet, not a clean bill: ${listed}${exemptSuffix(s.exempt)}${mintingSuffix(s.minting)}`
|
|
978
1159
|
};
|
|
979
1160
|
} else {
|
|
980
1161
|
decided = {
|
|
981
1162
|
id,
|
|
982
1163
|
anchor: s.exempt[0] ? `${s.exempt[0].file}:${s.exempt[0].line}` : "-",
|
|
983
1164
|
state: "present",
|
|
984
|
-
verdict: `0 local identity primitives; ${delegated} (positive-control OK)${exemptSuffix(s.exempt)}`
|
|
1165
|
+
verdict: `0 local identity primitives; ${delegated} (positive-control OK)${exemptSuffix(s.exempt)}${mintingSuffix(s.minting)}`
|
|
985
1166
|
};
|
|
986
1167
|
}
|
|
987
1168
|
return settle(id, decided, s.unreadable);
|
|
@@ -1168,6 +1349,237 @@ function resolveNameBindings(tree) {
|
|
|
1168
1349
|
}
|
|
1169
1350
|
return bindings;
|
|
1170
1351
|
}
|
|
1352
|
+
var READ_STRING_LITERAL_RE = /^(?:"((?:[^"\\\n]|\\.)*)"|'((?:[^'\\\n]|\\.)*)'|`((?:[^`\\$]|\\.|\$(?!\{))*)`)/;
|
|
1353
|
+
function readDescriptionValue(text) {
|
|
1354
|
+
let rest = text.replace(/^\s+/, "");
|
|
1355
|
+
let acc = "";
|
|
1356
|
+
let first = true;
|
|
1357
|
+
for (; ; ) {
|
|
1358
|
+
const m = rest.match(READ_STRING_LITERAL_RE);
|
|
1359
|
+
if (!m) {
|
|
1360
|
+
const expr = rest.split("\n")[0].trim().slice(0, 60);
|
|
1361
|
+
return { kind: "non-literal", expr: first ? expr : `\u2026+ ${expr}` };
|
|
1362
|
+
}
|
|
1363
|
+
first = false;
|
|
1364
|
+
acc += m[1] ?? m[2] ?? m[3] ?? "";
|
|
1365
|
+
rest = rest.slice(m[0].length).replace(/^\s+/, "");
|
|
1366
|
+
if (rest.startsWith("+")) {
|
|
1367
|
+
rest = rest.slice(1).replace(/^\s+/, "");
|
|
1368
|
+
continue;
|
|
1369
|
+
}
|
|
1370
|
+
return { kind: "literal", text: acc };
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
function constExpression(tree, name) {
|
|
1374
|
+
const decl = new RegExp(`\\bconst\\s+${name.replace(/\$/g, "\\$")}\\s*(?::[^=\\n]+)?=(?!=)`);
|
|
1375
|
+
let found = null;
|
|
1376
|
+
let count = 0;
|
|
1377
|
+
for (const file of tree.files) {
|
|
1378
|
+
const code = file.lines.map((l) => l.comment ? "" : l.raw);
|
|
1379
|
+
for (let i = 0; i < code.length; i++) {
|
|
1380
|
+
const m = code[i].match(decl);
|
|
1381
|
+
if (!m) continue;
|
|
1382
|
+
count++;
|
|
1383
|
+
let text = code[i].slice((m.index ?? 0) + m[0].length);
|
|
1384
|
+
let depth = 0;
|
|
1385
|
+
let j = i;
|
|
1386
|
+
for (; ; ) {
|
|
1387
|
+
for (const ch of text) {
|
|
1388
|
+
if ("([{".includes(ch)) depth++;
|
|
1389
|
+
else if (")]}".includes(ch)) depth--;
|
|
1390
|
+
}
|
|
1391
|
+
if (depth <= 0 && /;\s*$/.test(text)) break;
|
|
1392
|
+
if (++j >= code.length || j > i + 60) break;
|
|
1393
|
+
text += `
|
|
1394
|
+
${code[j]}`;
|
|
1395
|
+
}
|
|
1396
|
+
found = text.replace(/;\s*$/, "").trim();
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
return count === 1 ? found : null;
|
|
1400
|
+
}
|
|
1401
|
+
var wrapperScans = /* @__PURE__ */ new WeakMap();
|
|
1402
|
+
var wrapperOpeners = /* @__PURE__ */ new WeakMap();
|
|
1403
|
+
var FUNCTION_DECL_RE = /^\s*(?:export\s+)?(?:default\s+)?(?:async\s+)?function\s+([A-Za-z_$][\w$]*)\s*(?:<[^>(]*>)?\s*\(|^\s*(?:export\s+)?const\s+([A-Za-z_$][\w$]*)\s*(?::[^=]+)?=\s*(?:async\s*)?\(/;
|
|
1404
|
+
function splitArgs(text, open, max = Number.POSITIVE_INFINITY) {
|
|
1405
|
+
const args = [];
|
|
1406
|
+
let depth = 0;
|
|
1407
|
+
let start = open + 1;
|
|
1408
|
+
for (let i = open; i < text.length; i++) {
|
|
1409
|
+
const c = text[i];
|
|
1410
|
+
if (c === '"' || c === "'" || c === "`") {
|
|
1411
|
+
for (i++; i < text.length && text[i] !== c; i++) if (text[i] === "\\") i++;
|
|
1412
|
+
continue;
|
|
1413
|
+
}
|
|
1414
|
+
if (c === "/" && text[i + 1] === "/") {
|
|
1415
|
+
while (i < text.length && text[i] !== "\n") i++;
|
|
1416
|
+
continue;
|
|
1417
|
+
}
|
|
1418
|
+
if (c === "/" && text[i + 1] === "*") {
|
|
1419
|
+
const e = text.indexOf("*/", i + 2);
|
|
1420
|
+
if (e < 0) return null;
|
|
1421
|
+
i = e + 1;
|
|
1422
|
+
continue;
|
|
1423
|
+
}
|
|
1424
|
+
if (c === "(" || c === "[" || c === "{") depth++;
|
|
1425
|
+
else if (c === ")" || c === "]" || c === "}") {
|
|
1426
|
+
depth--;
|
|
1427
|
+
if (depth === 0) {
|
|
1428
|
+
args.push(text.slice(start, i).trim());
|
|
1429
|
+
return { args, end: i };
|
|
1430
|
+
}
|
|
1431
|
+
} else if (c === "," && depth === 1) {
|
|
1432
|
+
args.push(text.slice(start, i).trim());
|
|
1433
|
+
start = i + 1;
|
|
1434
|
+
if (args.length >= max) return { args, end: i };
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1437
|
+
return null;
|
|
1438
|
+
}
|
|
1439
|
+
function scanWrappers(tree) {
|
|
1440
|
+
const cached = wrapperScans.get(tree);
|
|
1441
|
+
if (cached) return cached;
|
|
1442
|
+
const scan = {
|
|
1443
|
+
sites: [],
|
|
1444
|
+
unresolved: [],
|
|
1445
|
+
internal: /* @__PURE__ */ new Set(),
|
|
1446
|
+
descriptions: /* @__PURE__ */ new Map()
|
|
1447
|
+
};
|
|
1448
|
+
wrapperScans.set(tree, scan);
|
|
1449
|
+
const wrappers = [];
|
|
1450
|
+
for (const file of tree.files) {
|
|
1451
|
+
const text = file.lines.map((l) => l.comment ? " ".repeat(l.raw.length) : l.raw).join("\n");
|
|
1452
|
+
const lineStart = [];
|
|
1453
|
+
let off = 0;
|
|
1454
|
+
for (const l of file.lines) {
|
|
1455
|
+
lineStart.push(off);
|
|
1456
|
+
off += l.raw.length + 1;
|
|
1457
|
+
}
|
|
1458
|
+
const lineOf = (idx) => {
|
|
1459
|
+
let lo = 0;
|
|
1460
|
+
let hi = lineStart.length - 1;
|
|
1461
|
+
while (lo < hi) {
|
|
1462
|
+
const mid = lo + hi + 1 >> 1;
|
|
1463
|
+
if (lineStart[mid] <= idx) lo = mid;
|
|
1464
|
+
else hi = mid - 1;
|
|
1465
|
+
}
|
|
1466
|
+
return lo + 1;
|
|
1467
|
+
};
|
|
1468
|
+
const aliases = /* @__PURE__ */ new Set(["registerTool", "tool"]);
|
|
1469
|
+
for (const m of text.matchAll(
|
|
1470
|
+
/\bconst\s+([A-Za-z_$][\w$]*)\s*=[^;\n]*\.(?:registerTool|tool)\b/g
|
|
1471
|
+
)) {
|
|
1472
|
+
aliases.add(m[1]);
|
|
1473
|
+
}
|
|
1474
|
+
const callRe = new RegExp(
|
|
1475
|
+
`(?:\\b(?:${[...aliases].join("|")})|\\.(?:registerTool|tool))\\s*\\(\\s*([A-Za-z_$][\\w$]*)\\s*,`,
|
|
1476
|
+
"g"
|
|
1477
|
+
);
|
|
1478
|
+
file.lines.forEach((line, i) => {
|
|
1479
|
+
if (line.comment) return;
|
|
1480
|
+
const d = line.raw.match(FUNCTION_DECL_RE);
|
|
1481
|
+
if (!d) return;
|
|
1482
|
+
const name = d[1] ?? d[2];
|
|
1483
|
+
if (name === "registerTool" || name === "tool") return;
|
|
1484
|
+
const open = lineStart[i] + (d.index ?? 0) + d[0].length - 1;
|
|
1485
|
+
const params = splitArgs(text, open);
|
|
1486
|
+
if (!params) return;
|
|
1487
|
+
const paramNames = params.args.map(
|
|
1488
|
+
(a) => a.replace(/^\.\.\./, "").match(/^([A-Za-z_$][\w$]*)/)?.[1] ?? null
|
|
1489
|
+
);
|
|
1490
|
+
const bodyOpen = text.indexOf("{", params.end);
|
|
1491
|
+
if (bodyOpen < 0) return;
|
|
1492
|
+
let depth = 0;
|
|
1493
|
+
let bodyEnd = -1;
|
|
1494
|
+
for (let k = bodyOpen; k < text.length; k++) {
|
|
1495
|
+
if (text[k] === "{") depth++;
|
|
1496
|
+
else if (text[k] === "}" && --depth === 0) {
|
|
1497
|
+
bodyEnd = k;
|
|
1498
|
+
break;
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
if (bodyEnd < 0) return;
|
|
1502
|
+
const body = text.slice(bodyOpen, bodyEnd);
|
|
1503
|
+
const idxs = /* @__PURE__ */ new Set();
|
|
1504
|
+
let descIdx = -1;
|
|
1505
|
+
for (const c of body.matchAll(callRe)) {
|
|
1506
|
+
const k = paramNames.indexOf(c[1]);
|
|
1507
|
+
if (k < 0) continue;
|
|
1508
|
+
idxs.add(k);
|
|
1509
|
+
scan.internal.add(`${file.rel}:${lineOf(bodyOpen + (c.index ?? 0))}`);
|
|
1510
|
+
const after = body.slice((c.index ?? 0) + c[0].length, (c.index ?? 0) + c[0].length + 3e3);
|
|
1511
|
+
const dm = after.match(/(?:^|[\s{,])description\b\s*(?::\s*([A-Za-z_$][\w$]*))?\s*[,}]/);
|
|
1512
|
+
if (dm) {
|
|
1513
|
+
const ref = dm[1] ?? "description";
|
|
1514
|
+
descIdx = paramNames.indexOf(ref);
|
|
1515
|
+
}
|
|
1516
|
+
}
|
|
1517
|
+
if (idxs.size !== 1) return;
|
|
1518
|
+
wrappers.push({
|
|
1519
|
+
name,
|
|
1520
|
+
nameIdx: [...idxs][0],
|
|
1521
|
+
descIdx,
|
|
1522
|
+
file,
|
|
1523
|
+
declLine: line.n,
|
|
1524
|
+
endLine: lineOf(bodyEnd)
|
|
1525
|
+
});
|
|
1526
|
+
});
|
|
1527
|
+
}
|
|
1528
|
+
for (const w of wrappers) {
|
|
1529
|
+
const callRe = new RegExp(`(?<![\\w$.])${w.name.replace(/\$/g, "\\$")}\\s*\\(`, "g");
|
|
1530
|
+
for (const file of tree.files) {
|
|
1531
|
+
const text = file.lines.map((l) => l.comment ? " ".repeat(l.raw.length) : l.raw).join("\n");
|
|
1532
|
+
const lineStart = [];
|
|
1533
|
+
let off = 0;
|
|
1534
|
+
for (const l of file.lines) {
|
|
1535
|
+
lineStart.push(off);
|
|
1536
|
+
off += l.raw.length + 1;
|
|
1537
|
+
}
|
|
1538
|
+
file.lines.forEach((line, i) => {
|
|
1539
|
+
if (line.comment) return;
|
|
1540
|
+
if (file === w.file && line.n >= w.declLine && line.n <= w.endLine) return;
|
|
1541
|
+
if (/^\s*(?:export\s+)?(?:async\s+)?function\b|^\s*import\b|^\s*\}?\s*from\b/.test(line.raw))
|
|
1542
|
+
return;
|
|
1543
|
+
for (const m of line.raw.matchAll(callRe)) {
|
|
1544
|
+
if (!isCodeAtCached(file, line.n, m.index ?? 0)) continue;
|
|
1545
|
+
const open = lineStart[i] + (m.index ?? 0) + m[0].length - 1;
|
|
1546
|
+
const parsed = splitArgs(text, open, Math.max(w.nameIdx, w.descIdx) + 1);
|
|
1547
|
+
const anchor = `${file.rel}:${line.n}`;
|
|
1548
|
+
if (!parsed || parsed.args.length <= w.nameIdx) {
|
|
1549
|
+
scan.unresolved.push(anchor);
|
|
1550
|
+
continue;
|
|
1551
|
+
}
|
|
1552
|
+
let nameArg = parsed.args[w.nameIdx];
|
|
1553
|
+
if (/^[A-Za-z_$][\w$]*$/.test(nameArg))
|
|
1554
|
+
nameArg = constExpression(tree, nameArg) ?? nameArg;
|
|
1555
|
+
const nameVal = readDescriptionValue(nameArg);
|
|
1556
|
+
if (nameVal.kind !== "literal" || nameVal.text === "") {
|
|
1557
|
+
scan.unresolved.push(anchor);
|
|
1558
|
+
continue;
|
|
1559
|
+
}
|
|
1560
|
+
const toolName = nameVal.text;
|
|
1561
|
+
scan.sites.push({ name: toolName, anchor });
|
|
1562
|
+
let set = wrapperOpeners.get(file);
|
|
1563
|
+
if (!set) {
|
|
1564
|
+
set = /* @__PURE__ */ new Set();
|
|
1565
|
+
wrapperOpeners.set(file, set);
|
|
1566
|
+
}
|
|
1567
|
+
set.add(line.n);
|
|
1568
|
+
if (w.descIdx >= 0 && parsed.args.length > w.descIdx) {
|
|
1569
|
+
scan.descriptions.set(anchor, parsed.args[w.descIdx]);
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
});
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
return scan;
|
|
1576
|
+
}
|
|
1577
|
+
function isCodeAtCached(file, n, col) {
|
|
1578
|
+
return isCodeAt(file, n, col);
|
|
1579
|
+
}
|
|
1580
|
+
function wrapperDescriptionArg(tree, anchor) {
|
|
1581
|
+
return scanWrappers(tree).descriptions.get(anchor);
|
|
1582
|
+
}
|
|
1171
1583
|
function findRegisteredTools(tree) {
|
|
1172
1584
|
const bindings = resolveNameBindings(tree);
|
|
1173
1585
|
const out = [];
|
|
@@ -1226,6 +1638,7 @@ function findRegisteredTools(tree) {
|
|
|
1226
1638
|
if (hasSchema) push(m[2], `${file.rel}:${line.n}`);
|
|
1227
1639
|
}
|
|
1228
1640
|
}
|
|
1641
|
+
for (const w of scanWrappers(tree).sites) push(w.name, w.anchor);
|
|
1229
1642
|
return out;
|
|
1230
1643
|
}
|
|
1231
1644
|
var DESCRIPTOR_KEY_RE = new RegExp(`^(\\s*)name:\\s*["'\`](${NAME_LITERAL})["'\`]\\s*,?\\s*$`);
|
|
@@ -1296,7 +1709,9 @@ function findUnresolvedToolSites(tree) {
|
|
|
1296
1709
|
}
|
|
1297
1710
|
if (awaitingName) out.push(openerAnchor);
|
|
1298
1711
|
}
|
|
1299
|
-
|
|
1712
|
+
const wrappers = scanWrappers(tree);
|
|
1713
|
+
const kept = out.filter((a) => !wrappers.internal.has(a));
|
|
1714
|
+
return [...kept, ...wrappers.unresolved];
|
|
1300
1715
|
}
|
|
1301
1716
|
function findListReadBuilders(tree) {
|
|
1302
1717
|
const out = [];
|
|
@@ -1349,6 +1764,45 @@ function sdkFirstAnchor(tree) {
|
|
|
1349
1764
|
}
|
|
1350
1765
|
return null;
|
|
1351
1766
|
}
|
|
1767
|
+
var ANNOTATIONS_NONLITERAL_RE = /\bannotations\s*:\s*([^\s{][^\n]*?)\s*,?\s*$/;
|
|
1768
|
+
var SIMPLE_IDENT_RE = /^([A-Za-z_$][A-Za-z0-9_$]*)(?:\s+(?:as\s+const|satisfies\s+[\w.<>\[\]]+))?$/;
|
|
1769
|
+
function resolveConstObjectLines(tree, home, ident, seen = /* @__PURE__ */ new Set()) {
|
|
1770
|
+
if (seen.has(ident)) return null;
|
|
1771
|
+
seen.add(ident);
|
|
1772
|
+
const openRe = new RegExp(
|
|
1773
|
+
`\\bconst\\s+${ident.replace(/\$/g, "\\$")}\\s*(?::[^=]+)?=\\s*(?:Object\\.freeze\\(\\s*)?\\{`
|
|
1774
|
+
);
|
|
1775
|
+
const found = [];
|
|
1776
|
+
for (const file of tree.files) {
|
|
1777
|
+
for (let i = 0; i < file.lines.length; i++) {
|
|
1778
|
+
const l = file.lines[i];
|
|
1779
|
+
if (!l.comment && openRe.test(l.raw)) found.push({ file, index: i });
|
|
1780
|
+
}
|
|
1781
|
+
}
|
|
1782
|
+
const local = found.filter((f) => f.file === home);
|
|
1783
|
+
const pick = local.length === 1 ? local[0] : local.length === 0 && found.length === 1 ? found[0] : null;
|
|
1784
|
+
if (!pick) return null;
|
|
1785
|
+
const out = [];
|
|
1786
|
+
let depth = 0;
|
|
1787
|
+
const started = true;
|
|
1788
|
+
for (let i = pick.index; i < pick.file.lines.length; i++) {
|
|
1789
|
+
const l = pick.file.lines[i];
|
|
1790
|
+
if (l.comment) continue;
|
|
1791
|
+
const text = i === pick.index ? l.raw.slice(l.raw.search(openRe)) : l.raw;
|
|
1792
|
+
out.push(text);
|
|
1793
|
+
depth += (text.match(/\{/g) ?? []).length - (text.match(/\}/g) ?? []).length;
|
|
1794
|
+
const spread = text.match(/\.\.\.\s*([A-Za-z_$][A-Za-z0-9_$]*)\s*[,}]?\s*$/);
|
|
1795
|
+
if (spread) {
|
|
1796
|
+
const inner = resolveConstObjectLines(tree, pick.file, spread[1], seen);
|
|
1797
|
+
if (!inner) return null;
|
|
1798
|
+
out.push(...inner);
|
|
1799
|
+
} else if (/\.\.\./.test(text)) {
|
|
1800
|
+
return null;
|
|
1801
|
+
}
|
|
1802
|
+
if (started && depth <= 0) return out;
|
|
1803
|
+
}
|
|
1804
|
+
return null;
|
|
1805
|
+
}
|
|
1352
1806
|
var VERB_FIELD_RE = /\bverb\s*:\s*["'`]([A-Za-z][A-Za-z-]*)["'`]/;
|
|
1353
1807
|
var TABLE_FIELD_RE = /\btable\s*:\s*["'`]([A-Za-z0-9_.-]+)["'`]/;
|
|
1354
1808
|
var MECHANISM_FIELD_RE = /\b(?:searchMechanism|mechanism)\s*:\s*["'`]([A-Za-z0-9_-]+(?:\([^)]*\))?)["'`]/;
|
|
@@ -1436,7 +1890,7 @@ function callOpenerLine(file, siteLine) {
|
|
|
1436
1890
|
for (let n = siteLine; n >= Math.max(1, siteLine - CALL_OPENER_LOOKBACK); n--) {
|
|
1437
1891
|
const line = file.lines[n - 1];
|
|
1438
1892
|
if (!line || line.comment) continue;
|
|
1439
|
-
if (TOOL_INLINE_LITERAL_RE.test(line.raw) || TOOL_INLINE_IDENT_RE.test(line.raw) || OPENER_RE.test(line.raw)) {
|
|
1893
|
+
if (TOOL_INLINE_LITERAL_RE.test(line.raw) || TOOL_INLINE_IDENT_RE.test(line.raw) || OPENER_RE.test(line.raw) || wrapperOpeners.get(file)?.has(line.n)) {
|
|
1440
1894
|
return line.n;
|
|
1441
1895
|
}
|
|
1442
1896
|
}
|
|
@@ -1457,6 +1911,34 @@ function ownCallEnd(file, siteLine) {
|
|
|
1457
1911
|
}
|
|
1458
1912
|
return file.lines.length;
|
|
1459
1913
|
}
|
|
1914
|
+
var HINT_KEYS = ["readOnlyHint", "destructiveHint", "idempotentHint", "openWorldHint"];
|
|
1915
|
+
function effectiveHints(tree, file, objectText, seen = /* @__PURE__ */ new Set()) {
|
|
1916
|
+
const state = {
|
|
1917
|
+
readOnlyHint: false,
|
|
1918
|
+
destructiveHint: false,
|
|
1919
|
+
idempotentHint: false,
|
|
1920
|
+
openWorldHint: false
|
|
1921
|
+
};
|
|
1922
|
+
const open = objectText.indexOf("{");
|
|
1923
|
+
const parsed = open < 0 ? null : splitArgs(objectText, open);
|
|
1924
|
+
if (!parsed) return { unresolved: objectText.trim().slice(0, 60) };
|
|
1925
|
+
for (const member of parsed.args) {
|
|
1926
|
+
if (member.startsWith("...")) {
|
|
1927
|
+
const id = member.slice(3).trim().match(SIMPLE_IDENT_RE);
|
|
1928
|
+
const lines = id && !seen.has(id[1]) ? resolveConstObjectLines(tree, file, id[1]) : null;
|
|
1929
|
+
if (!lines) return { unresolved: member };
|
|
1930
|
+
const inner = effectiveHints(tree, file, lines.join("\n"), /* @__PURE__ */ new Set([...seen, id?.[1] ?? ""]));
|
|
1931
|
+
if ("unresolved" in inner) return inner;
|
|
1932
|
+
for (const k of HINT_KEYS) if (inner[k]) state[k] = true;
|
|
1933
|
+
continue;
|
|
1934
|
+
}
|
|
1935
|
+
const kv = member.match(/^["'`]?([A-Za-z_$][\w$]*)["'`]?\s*:\s*([\s\S]*)$/);
|
|
1936
|
+
if (kv && HINT_KEYS.includes(kv[1])) {
|
|
1937
|
+
state[kv[1]] = /^(?:true|false)$/.test(kv[2].trim());
|
|
1938
|
+
}
|
|
1939
|
+
}
|
|
1940
|
+
return state;
|
|
1941
|
+
}
|
|
1460
1942
|
function findToolAnnotations(tree) {
|
|
1461
1943
|
const allSites = findRegisteredTools(tree);
|
|
1462
1944
|
const out = [];
|
|
@@ -1481,10 +1963,31 @@ function findToolAnnotations(tree) {
|
|
|
1481
1963
|
let paginationCursorDeclared = false;
|
|
1482
1964
|
let paginationDefault;
|
|
1483
1965
|
let paginationMax;
|
|
1966
|
+
let annotationsUnresolved;
|
|
1967
|
+
const windowRaws = [];
|
|
1484
1968
|
for (const line of file.lines) {
|
|
1485
1969
|
if (line.n <= windowStart || line.n > windowEnd) continue;
|
|
1486
1970
|
if (line.comment) continue;
|
|
1487
|
-
|
|
1971
|
+
windowRaws.push(line.raw);
|
|
1972
|
+
}
|
|
1973
|
+
const ownStart = callOpenerLine(file, site.line);
|
|
1974
|
+
const ownRaws = [];
|
|
1975
|
+
let ownExpr;
|
|
1976
|
+
for (const line of file.lines) {
|
|
1977
|
+
if (line.n < ownStart || line.n > windowEnd || line.comment) continue;
|
|
1978
|
+
ownRaws.push(line.raw);
|
|
1979
|
+
const nl = line.raw.match(ANNOTATIONS_NONLITERAL_RE);
|
|
1980
|
+
if (nl && ownExpr === void 0) ownExpr = nl[1];
|
|
1981
|
+
}
|
|
1982
|
+
if (ownExpr !== void 0) {
|
|
1983
|
+
const id = ownExpr.match(SIMPLE_IDENT_RE);
|
|
1984
|
+
const body = id ? resolveConstObjectLines(tree, file, id[1]) : null;
|
|
1985
|
+
windowRaws.length = 0;
|
|
1986
|
+
windowRaws.push(...ownRaws);
|
|
1987
|
+
if (body) windowRaws.push(...body);
|
|
1988
|
+
else annotationsUnresolved = ownExpr;
|
|
1989
|
+
}
|
|
1990
|
+
for (const raw of windowRaws) {
|
|
1488
1991
|
if (!verb) {
|
|
1489
1992
|
const m = raw.match(VERB_FIELD_RE);
|
|
1490
1993
|
if (m) verb = m[1];
|
|
@@ -1529,6 +2032,22 @@ function findToolAnnotations(tree) {
|
|
|
1529
2032
|
if (m) paginationMax = Number.parseInt(m[1], 10);
|
|
1530
2033
|
}
|
|
1531
2034
|
}
|
|
2035
|
+
const ownText = ownRaws.join("\n");
|
|
2036
|
+
const inlineAnn = ownText.match(/\bannotations\s*:\s*\{/);
|
|
2037
|
+
if (inlineAnn && inlineAnn.index !== void 0) {
|
|
2038
|
+
const objText = ownText.slice(inlineAnn.index + inlineAnn[0].length - 1);
|
|
2039
|
+
const parsedObj = splitArgs(objText, 0);
|
|
2040
|
+
if (parsedObj?.args.some((m) => m.startsWith("..."))) {
|
|
2041
|
+
const eff = effectiveHints(tree, file, objText);
|
|
2042
|
+
if ("unresolved" in eff) annotationsUnresolved = eff.unresolved;
|
|
2043
|
+
else {
|
|
2044
|
+
readOnlyHintDeclared = eff.readOnlyHint;
|
|
2045
|
+
destructiveHintDeclared = eff.destructiveHint;
|
|
2046
|
+
idempotentHintDeclared = eff.idempotentHint;
|
|
2047
|
+
openWorldHintDeclared = eff.openWorldHint;
|
|
2048
|
+
}
|
|
2049
|
+
}
|
|
2050
|
+
}
|
|
1532
2051
|
out.push({
|
|
1533
2052
|
name: site.name,
|
|
1534
2053
|
anchor: site.anchor,
|
|
@@ -1545,7 +2064,8 @@ function findToolAnnotations(tree) {
|
|
|
1545
2064
|
tokensEstimeContexte,
|
|
1546
2065
|
paginationCursorDeclared,
|
|
1547
2066
|
paginationDefault,
|
|
1548
|
-
paginationMax
|
|
2067
|
+
paginationMax,
|
|
2068
|
+
annotationsUnresolved
|
|
1549
2069
|
});
|
|
1550
2070
|
}
|
|
1551
2071
|
}
|
|
@@ -1569,6 +2089,18 @@ function countKeywordInCode(tree, re) {
|
|
|
1569
2089
|
}
|
|
1570
2090
|
return { count, firstFile, firstLine };
|
|
1571
2091
|
}
|
|
2092
|
+
function opaqueAnnotationsAbstain(id, rule, tools) {
|
|
2093
|
+
const opaque = tools.find((t) => t.annotationsUnresolved);
|
|
2094
|
+
if (!opaque) return null;
|
|
2095
|
+
const reason = `tool "${opaque.name}" at ${opaque.anchor} passes \`annotations: ${opaque.annotationsUnresolved}\`, which is not an inline object and does not resolve to a \`const\` object literal in this tree \u2014 the fields it declares are unreadable statically (${rule})`;
|
|
2096
|
+
return {
|
|
2097
|
+
id,
|
|
2098
|
+
anchor: opaque.anchor,
|
|
2099
|
+
state: "could-not-judge",
|
|
2100
|
+
reason,
|
|
2101
|
+
verdict: `could not judge: ${reason}`
|
|
2102
|
+
};
|
|
2103
|
+
}
|
|
1572
2104
|
|
|
1573
2105
|
// src/detectors/c8-authority-declared.ts
|
|
1574
2106
|
var TIER_SET = /* @__PURE__ */ new Set(["public", "org-member", "org-admin", "fleet-internal", "master"]);
|
|
@@ -1621,6 +2153,8 @@ var c8AuthorityDeclared = {
|
|
|
1621
2153
|
verdict: "no tool registrations found; authority-declared check n/a"
|
|
1622
2154
|
};
|
|
1623
2155
|
}
|
|
2156
|
+
const opaque = opaqueAnnotationsAbstain(this.id, "C-8", tools);
|
|
2157
|
+
if (opaque) return opaque;
|
|
1624
2158
|
for (const t of tools) {
|
|
1625
2159
|
if (isFolded(t)) {
|
|
1626
2160
|
return {
|
|
@@ -1724,6 +2258,8 @@ var c9VerbDeclared = {
|
|
|
1724
2258
|
verdict: "no tool registrations found; verb-declared check n/a"
|
|
1725
2259
|
};
|
|
1726
2260
|
}
|
|
2261
|
+
const opaque = opaqueAnnotationsAbstain(this.id, "C-9", tools);
|
|
2262
|
+
if (opaque) return opaque;
|
|
1727
2263
|
for (const t of tools) {
|
|
1728
2264
|
if (!t.verb) {
|
|
1729
2265
|
return {
|
|
@@ -1819,6 +2355,8 @@ var c10MechanismDeclared = {
|
|
|
1819
2355
|
verdict: "no list/search tool found; mechanism-declared check n/a"
|
|
1820
2356
|
};
|
|
1821
2357
|
}
|
|
2358
|
+
const opaque = opaqueAnnotationsAbstain(this.id, "C-10", applicable);
|
|
2359
|
+
if (opaque) return opaque;
|
|
1822
2360
|
for (const t of applicable) {
|
|
1823
2361
|
if (!t.searchMechanism) {
|
|
1824
2362
|
return {
|
|
@@ -1893,6 +2431,8 @@ var c11Pagination = {
|
|
|
1893
2431
|
verdict: "no list/read/get/search tool registrations found; pagination declaration check n/a"
|
|
1894
2432
|
};
|
|
1895
2433
|
}
|
|
2434
|
+
const opaque = opaqueAnnotationsAbstain(this.id, "C-11", tools);
|
|
2435
|
+
if (opaque) return opaque;
|
|
1896
2436
|
for (const t of tools) {
|
|
1897
2437
|
const missing = [];
|
|
1898
2438
|
if (!t.paginationCursorDeclared) missing.push("paginationCursor");
|
|
@@ -2116,6 +2656,8 @@ var c13ContextCeiling = {
|
|
|
2116
2656
|
verdict: "no tool registrations found; context-ceiling check n/a"
|
|
2117
2657
|
};
|
|
2118
2658
|
}
|
|
2659
|
+
const opaque = opaqueAnnotationsAbstain(this.id, "C-13", tools);
|
|
2660
|
+
if (opaque) return opaque;
|
|
2119
2661
|
const ceiling = findCeiling(tree);
|
|
2120
2662
|
if (!ceiling) {
|
|
2121
2663
|
return {
|
|
@@ -2349,7 +2891,7 @@ var c15AnnotationsUntrusted = {
|
|
|
2349
2891
|
};
|
|
2350
2892
|
}
|
|
2351
2893
|
for (const t of tools) {
|
|
2352
|
-
if (!allHintsDeclared(t)) {
|
|
2894
|
+
if (!allHintsDeclared(t) && !t.annotationsUnresolved) {
|
|
2353
2895
|
const missing = missingHints(t).join(", ");
|
|
2354
2896
|
return {
|
|
2355
2897
|
id: this.id,
|
|
@@ -2359,6 +2901,17 @@ var c15AnnotationsUntrusted = {
|
|
|
2359
2901
|
};
|
|
2360
2902
|
}
|
|
2361
2903
|
}
|
|
2904
|
+
const opaque = tools.find((t) => t.annotationsUnresolved);
|
|
2905
|
+
if (opaque) {
|
|
2906
|
+
const reason = `tool "${opaque.name}" at ${opaque.anchor} passes \`annotations: ${opaque.annotationsUnresolved}\`, which is not an inline object and does not resolve to a \`const\` object literal in this tree \u2014 its four behavioral hints are unreadable statically (C-15)`;
|
|
2907
|
+
return {
|
|
2908
|
+
id: this.id,
|
|
2909
|
+
anchor: opaque.anchor,
|
|
2910
|
+
state: "could-not-judge",
|
|
2911
|
+
reason,
|
|
2912
|
+
verdict: `could not judge: ${reason}`
|
|
2913
|
+
};
|
|
2914
|
+
}
|
|
2362
2915
|
return {
|
|
2363
2916
|
id: this.id,
|
|
2364
2917
|
anchor: tools[0].anchor,
|
|
@@ -2447,21 +3000,92 @@ var c16UntrustedOutput = {
|
|
|
2447
3000
|
|
|
2448
3001
|
// src/detectors/c17-supply-chain.ts
|
|
2449
3002
|
import { existsSync as existsSync3, readFileSync as readFileSync4, readdirSync as readdirSync2 } from "fs";
|
|
2450
|
-
import { join as join3 } from "path";
|
|
3003
|
+
import { dirname as dirname2, join as join3, relative as relative2, resolve } from "path";
|
|
2451
3004
|
var PIN4 = "a74a042b53c71a095ab88b3073bd47728eeda814";
|
|
2452
3005
|
var LOCKFILES = ["package-lock.json", "pnpm-lock.yaml", "yarn.lock", "bun.lock", "bun.lockb"];
|
|
2453
3006
|
var WORKFLOW_DIR = join3(".github", "workflows");
|
|
2454
|
-
var PROVENANCE_RE2 =
|
|
3007
|
+
var PROVENANCE_RE2 = /\b(?:npm|pnpm|yarn|bun)\s+publish\b[^\n]*?--provenance\b/;
|
|
3008
|
+
function workflowStatements(text) {
|
|
3009
|
+
const out = [];
|
|
3010
|
+
const lines = text.split("\n").map((l) => l.replace(/(^|\s)#.*$/, "$1"));
|
|
3011
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3012
|
+
let stmt = lines[i];
|
|
3013
|
+
const start = i + 1;
|
|
3014
|
+
while (/\\\s*$/.test(stmt) && i + 1 < lines.length) {
|
|
3015
|
+
stmt = `${stmt.replace(/\\\s*$/, " ")}${lines[++i].trim()}`;
|
|
3016
|
+
}
|
|
3017
|
+
out.push([start, stmt]);
|
|
3018
|
+
}
|
|
3019
|
+
return out;
|
|
3020
|
+
}
|
|
3021
|
+
var PROVENANCE_RECORD_FILES = ["PROVENANCE", "PROVENANCE.md", "CHANGELOG.md", "README.md"];
|
|
3022
|
+
var RECORD_LINE_RE = /^[\s>*_\-+]*Provenance\s*:\s*(.*)$/i;
|
|
3023
|
+
var SHASUM_HEX_RE = /\bshasum\b[^\n]*?\b([0-9a-f]{40})\b/i;
|
|
3024
|
+
function judgeProvenanceRecord(line, name, version) {
|
|
3025
|
+
const body = line.match(RECORD_LINE_RE)?.[1] ?? "";
|
|
3026
|
+
const esc2 = (x) => x.replace(/[.+*?^$()[\]{}|\\/]/g, "\\$&");
|
|
3027
|
+
const named = new RegExp(`(?:^|[\\s(])${esc2(name)}@v?${esc2(version)}(?![\\w.-]*\\d)`);
|
|
3028
|
+
if (!named.test(body)) {
|
|
3029
|
+
return { ok: false, why: `it does not name ${name}@${version}, the artifact being published` };
|
|
3030
|
+
}
|
|
3031
|
+
if (!SHASUM_HEX_RE.test(body)) {
|
|
3032
|
+
return { ok: false, why: "it carries no 40-hex `shasum` (the width of dist.shasum)" };
|
|
3033
|
+
}
|
|
3034
|
+
return { ok: true, why: "" };
|
|
3035
|
+
}
|
|
3036
|
+
function findProvenanceRecord(root, name, version) {
|
|
3037
|
+
let firstBad = null;
|
|
3038
|
+
for (const recordFile of PROVENANCE_RECORD_FILES) {
|
|
3039
|
+
const path = join3(root, recordFile);
|
|
3040
|
+
if (!existsSync3(path)) continue;
|
|
3041
|
+
let text;
|
|
3042
|
+
try {
|
|
3043
|
+
text = readFileSync4(path, "utf8");
|
|
3044
|
+
} catch {
|
|
3045
|
+
continue;
|
|
3046
|
+
}
|
|
3047
|
+
const lines = text.split("\n");
|
|
3048
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3049
|
+
if (!RECORD_LINE_RE.test(lines[i])) continue;
|
|
3050
|
+
const site = `${recordFile}:${i + 1}`;
|
|
3051
|
+
if (!version || !name) {
|
|
3052
|
+
firstBad ??= {
|
|
3053
|
+
site,
|
|
3054
|
+
ok: false,
|
|
3055
|
+
why: "package.json declares no name and version to match it against"
|
|
3056
|
+
};
|
|
3057
|
+
continue;
|
|
3058
|
+
}
|
|
3059
|
+
const j = judgeProvenanceRecord(lines[i], name, version);
|
|
3060
|
+
if (j.ok) return { site, ok: true, why: "" };
|
|
3061
|
+
firstBad ??= { site, ok: false, why: j.why };
|
|
3062
|
+
}
|
|
3063
|
+
}
|
|
3064
|
+
return firstBad;
|
|
3065
|
+
}
|
|
3066
|
+
var RECORD_SAMPLE = "Provenance: p@1.2.3 npm-pack shasum 4b825dc642cb6eb9a060e54bf8d69288fbee4904 reproduced by r, read back from the registry 2026-10-10";
|
|
2455
3067
|
function c17PositiveControls() {
|
|
2456
3068
|
const sample = "npm publish --provenance --access public";
|
|
2457
3069
|
const bare = "npm publish --access public";
|
|
3070
|
+
const claimed = (t) => workflowStatements(t).some(([, l]) => PROVENANCE_RE2.test(l));
|
|
2458
3071
|
return {
|
|
2459
3072
|
provenanceMatcher: {
|
|
2460
3073
|
pattern: PROVENANCE_RE2.source,
|
|
2461
3074
|
sample,
|
|
2462
3075
|
matched: PROVENANCE_RE2.test(sample),
|
|
2463
|
-
|
|
2464
|
-
|
|
3076
|
+
// Bare publish, a comment naming the flag, the OIDC permission alone, and a continuation
|
|
3077
|
+
// line are the four shapes the matcher must tell apart.
|
|
3078
|
+
rejectsBare: !PROVENANCE_RE2.test(bare) && !claimed("# run: npm publish --provenance") && !claimed(" run: npm publish --access public # --provenance later") && !claimed("permissions:\n id-token: write") && claimed("run: npm publish \\\n --provenance --access public")
|
|
3079
|
+
},
|
|
3080
|
+
recordAcceptsComplete: judgeProvenanceRecord(RECORD_SAMPLE, "p", "1.2.3").ok,
|
|
3081
|
+
recordRejectsOtherVersion: !judgeProvenanceRecord(RECORD_SAMPLE, "p", "1.2.4").ok,
|
|
3082
|
+
recordRejectsOtherPackage: !judgeProvenanceRecord(RECORD_SAMPLE, "q", "1.2.3").ok,
|
|
3083
|
+
recordRejectsBareVersion: !judgeProvenanceRecord(
|
|
3084
|
+
"Provenance: version 1.2.3 npm-pack shasum 4b825dc642cb6eb9a060e54bf8d69288fbee4904",
|
|
3085
|
+
"p",
|
|
3086
|
+
"1.2.3"
|
|
3087
|
+
).ok,
|
|
3088
|
+
recordRejectsNoShasum: !judgeProvenanceRecord("Provenance: p@1.2.3 trust us", "p", "1.2.3").ok
|
|
2465
3089
|
};
|
|
2466
3090
|
}
|
|
2467
3091
|
function findLockfile(root) {
|
|
@@ -2478,30 +3102,45 @@ function findProvenanceClaim(tree, root) {
|
|
|
2478
3102
|
return { site: `${pkg.path}:publishConfig.provenance`, text: "provenance: true" };
|
|
2479
3103
|
}
|
|
2480
3104
|
}
|
|
2481
|
-
const
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
entries = readdirSync2(workflowsDir);
|
|
2486
|
-
} catch {
|
|
2487
|
-
return null;
|
|
2488
|
-
}
|
|
2489
|
-
for (const name of entries) {
|
|
2490
|
-
if (!/\.ya?ml$/.test(name)) continue;
|
|
2491
|
-
const path = join3(workflowsDir, name);
|
|
2492
|
-
let text;
|
|
3105
|
+
for (const dir of workflowDirs(root)) {
|
|
3106
|
+
const workflowsDir = join3(dir, WORKFLOW_DIR);
|
|
3107
|
+
if (!existsSync3(workflowsDir)) continue;
|
|
3108
|
+
let entries = [];
|
|
2493
3109
|
try {
|
|
2494
|
-
|
|
3110
|
+
entries = readdirSync2(workflowsDir).sort();
|
|
2495
3111
|
} catch {
|
|
2496
3112
|
continue;
|
|
2497
3113
|
}
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
3114
|
+
for (const name of entries) {
|
|
3115
|
+
if (!/\.ya?ml$/.test(name)) continue;
|
|
3116
|
+
const path = join3(workflowsDir, name);
|
|
3117
|
+
let text;
|
|
3118
|
+
try {
|
|
3119
|
+
text = readFileSync4(path, "utf8");
|
|
3120
|
+
} catch {
|
|
3121
|
+
continue;
|
|
3122
|
+
}
|
|
3123
|
+
const hit = workflowStatements(text).find(([, l]) => PROVENANCE_RE2.test(l));
|
|
3124
|
+
if (hit) {
|
|
3125
|
+
const line = hit[0];
|
|
3126
|
+
const rel = relative2(root, workflowsDir) || WORKFLOW_DIR;
|
|
3127
|
+
return { site: `${rel}/${name}:${line}`, text };
|
|
3128
|
+
}
|
|
2501
3129
|
}
|
|
2502
3130
|
}
|
|
2503
3131
|
return null;
|
|
2504
3132
|
}
|
|
3133
|
+
function workflowDirs(root) {
|
|
3134
|
+
const chain = [];
|
|
3135
|
+
let dir = resolve(root);
|
|
3136
|
+
for (; ; ) {
|
|
3137
|
+
chain.push(dir);
|
|
3138
|
+
if (existsSync3(join3(dir, ".git"))) return chain;
|
|
3139
|
+
const parent = dirname2(dir);
|
|
3140
|
+
if (parent === dir) return [resolve(root)];
|
|
3141
|
+
dir = parent;
|
|
3142
|
+
}
|
|
3143
|
+
}
|
|
2505
3144
|
var c17SupplyChainInner = {
|
|
2506
3145
|
id: "C17 supply-chain",
|
|
2507
3146
|
rule: "C-17",
|
|
@@ -2513,6 +3152,11 @@ var c17SupplyChainInner = {
|
|
|
2513
3152
|
`C17 provenance-matcher control FAILED: accept(\`${controls.provenanceMatcher.sample}\`)=${controls.provenanceMatcher.matched} rejectsBare=${controls.provenanceMatcher.rejectsBare} \u2014 matcher broken, verdict untrustworthy`
|
|
2514
3153
|
);
|
|
2515
3154
|
}
|
|
3155
|
+
if (!controls.recordAcceptsComplete || !controls.recordRejectsOtherVersion || !controls.recordRejectsNoShasum || !controls.recordRejectsOtherPackage || !controls.recordRejectsBareVersion) {
|
|
3156
|
+
throw new Error(
|
|
3157
|
+
`C17 provenance-record control FAILED: ${JSON.stringify(controls)} \u2014 record parser broken, verdict untrustworthy`
|
|
3158
|
+
);
|
|
3159
|
+
}
|
|
2516
3160
|
const lockfile = findLockfile(tree.root);
|
|
2517
3161
|
if (!lockfile) {
|
|
2518
3162
|
return {
|
|
@@ -2523,12 +3167,31 @@ var c17SupplyChainInner = {
|
|
|
2523
3167
|
};
|
|
2524
3168
|
}
|
|
2525
3169
|
const claim = findProvenanceClaim(tree, tree.root);
|
|
3170
|
+
const pkgVersion = typeof tree.packageJson?.json.version === "string" ? tree.packageJson.json.version : void 0;
|
|
3171
|
+
const pkgName = typeof tree.packageJson?.json.name === "string" ? tree.packageJson.json.name : void 0;
|
|
3172
|
+
const record = claim ? null : findProvenanceRecord(tree.root, pkgName, pkgVersion);
|
|
3173
|
+
if (!claim && record?.ok) {
|
|
3174
|
+
return {
|
|
3175
|
+
id: this.id,
|
|
3176
|
+
anchor: record.site,
|
|
3177
|
+
state: "present",
|
|
3178
|
+
verdict: `lockfile ${lockfile} pinned + reproduced-shasum record at ${record.site} (non-CI path, C-17; PROPOSED EVIDENCE FORM \u2014 the reviewer rules on it). Whether that shasum equals the registry's dist.shasum is the gate check at publish time \u2014 this static run asserts the record is complete and names version ${pkgVersion}, not that the gate has resolved it.`
|
|
3179
|
+
};
|
|
3180
|
+
}
|
|
3181
|
+
if (!claim && record && !record.ok) {
|
|
3182
|
+
return {
|
|
3183
|
+
id: this.id,
|
|
3184
|
+
anchor: record.site,
|
|
3185
|
+
state: "non-conforming",
|
|
3186
|
+
verdict: `lockfile ${lockfile} present and a \`Provenance:\` line at ${record.site}, but ${record.why} -> a record covers only the version it names and must carry the reproduced 40-hex shasum (C-17, non-CI path; proposed evidence form)`
|
|
3187
|
+
};
|
|
3188
|
+
}
|
|
2526
3189
|
if (!claim) {
|
|
2527
3190
|
return {
|
|
2528
3191
|
id: this.id,
|
|
2529
3192
|
anchor: `${lockfile}:1`,
|
|
2530
3193
|
state: "non-conforming",
|
|
2531
|
-
verdict: `lockfile ${lockfile} present, but no provenance attestation claim found in package.json publishConfig or ${WORKFLOW_DIR}/*.yml -> add \`npm publish --provenance\` or \`publishConfig.provenance: true\` (C-17b)`
|
|
3194
|
+
verdict: `lockfile ${lockfile} present, but no provenance attestation claim found in package.json publishConfig or ${WORKFLOW_DIR}/*.yml -> add \`npm publish --provenance\` or \`publishConfig.provenance: true\`, or, when no CI publishes, a \`Provenance: <pkg>@<version> npm-pack shasum <40-hex> ...\` record in PROVENANCE / CHANGELOG.md / README.md (C-17b; record form proposed)`
|
|
2532
3195
|
};
|
|
2533
3196
|
}
|
|
2534
3197
|
return {
|
|
@@ -2552,6 +3215,28 @@ function manifestPath(root) {
|
|
|
2552
3215
|
if (existsSync4(extPath)) return { kind: "extension", path: extPath };
|
|
2553
3216
|
return { kind: "server", path: join4(root, "mcp.json") };
|
|
2554
3217
|
}
|
|
3218
|
+
var SERVER_MANIFEST_NAMES = ["mcp.json", "mcp-server.json"];
|
|
3219
|
+
function isObject(v) {
|
|
3220
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
3221
|
+
}
|
|
3222
|
+
function classifyRootJson(json) {
|
|
3223
|
+
if (!json) return "unparseable";
|
|
3224
|
+
if (isObject(json.mcpServers)) return "plugin-config";
|
|
3225
|
+
if (isObject(json.capabilities) && isObject(json.transport) && typeof json.version === "string") {
|
|
3226
|
+
return "server-manifest";
|
|
3227
|
+
}
|
|
3228
|
+
return "partial";
|
|
3229
|
+
}
|
|
3230
|
+
function rootCandidates(root) {
|
|
3231
|
+
const out = [];
|
|
3232
|
+
for (const name of SERVER_MANIFEST_NAMES) {
|
|
3233
|
+
const path = join4(root, name);
|
|
3234
|
+
if (!existsSync4(path)) continue;
|
|
3235
|
+
const json = readJson(path);
|
|
3236
|
+
out.push({ name, path, role: classifyRootJson(json), json });
|
|
3237
|
+
}
|
|
3238
|
+
return out;
|
|
3239
|
+
}
|
|
2555
3240
|
function readJson(path) {
|
|
2556
3241
|
try {
|
|
2557
3242
|
return JSON.parse(readFileSync5(path, "utf8"));
|
|
@@ -2565,7 +3250,10 @@ function c18PositiveControls() {
|
|
|
2565
3250
|
versionParity: {
|
|
2566
3251
|
matched: equalCheck("0.1.0", "0.1.0"),
|
|
2567
3252
|
rejectsMismatch: !equalCheck("0.1.0", "0.2.0")
|
|
2568
|
-
}
|
|
3253
|
+
},
|
|
3254
|
+
contentIdentifiesPluginConfig: classifyRootJson({ mcpServers: { a: {} } }) === "plugin-config",
|
|
3255
|
+
contentIdentifiesServerManifest: classifyRootJson({ version: "1", capabilities: {}, transport: {} }) === "server-manifest",
|
|
3256
|
+
contentRejectsPartial: classifyRootJson({ version: "1", name: "x" }) === "partial"
|
|
2569
3257
|
};
|
|
2570
3258
|
}
|
|
2571
3259
|
var c18StructureManifestInner = {
|
|
@@ -2574,6 +3262,11 @@ var c18StructureManifestInner = {
|
|
|
2574
3262
|
pin: PIN5,
|
|
2575
3263
|
run(tree) {
|
|
2576
3264
|
const controls = c18PositiveControls();
|
|
3265
|
+
if (!controls.contentIdentifiesPluginConfig || !controls.contentIdentifiesServerManifest || !controls.contentRejectsPartial) {
|
|
3266
|
+
throw new Error(
|
|
3267
|
+
"C18 manifest-by-content control FAILED: mcpServers must read as plugin config, capabilities+transport+version as server manifest, a bare name+version as partial \u2014 verdict untrustworthy"
|
|
3268
|
+
);
|
|
3269
|
+
}
|
|
2577
3270
|
if (!controls.versionParity.matched || !controls.versionParity.rejectsMismatch) {
|
|
2578
3271
|
throw new Error(
|
|
2579
3272
|
"C18 version-parity control FAILED: equal-strings must match and unequal-strings must be rejected \u2014 verdict untrustworthy"
|
|
@@ -2600,25 +3293,64 @@ var c18StructureManifestInner = {
|
|
|
2600
3293
|
verdict: `could not judge: ${reason}`
|
|
2601
3294
|
};
|
|
2602
3295
|
}
|
|
2603
|
-
const
|
|
2604
|
-
const
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
3296
|
+
const ext = manifestPath(tree.root);
|
|
3297
|
+
const kind = ext.kind;
|
|
3298
|
+
let path = ext.path;
|
|
3299
|
+
let manifest = null;
|
|
3300
|
+
let manifestName = kind === "extension" ? "extension.json" : "mcp.json";
|
|
3301
|
+
if (kind === "server") {
|
|
3302
|
+
const cands = rootCandidates(tree.root);
|
|
3303
|
+
const bad = cands.find((c) => c.role === "unparseable");
|
|
3304
|
+
if (bad) {
|
|
3305
|
+
return {
|
|
3306
|
+
id: this.id,
|
|
3307
|
+
anchor: bad.path,
|
|
3308
|
+
state: "non-conforming",
|
|
3309
|
+
verdict: `${bad.name} present but could not be parsed as JSON -> fix the manifest (C-18)`
|
|
3310
|
+
};
|
|
3311
|
+
}
|
|
3312
|
+
const manifests = cands.filter((c) => c.role === "server-manifest");
|
|
3313
|
+
if (manifests.length > 1) {
|
|
3314
|
+
return {
|
|
3315
|
+
id: this.id,
|
|
3316
|
+
anchor: manifests[0].path,
|
|
3317
|
+
state: "non-conforming",
|
|
3318
|
+
verdict: `two root files each carry capabilities + transport + version (${manifests.map((c) => c.name).join(" and ")}) -> a repository has ONE server manifest; keep it in mcp-server.json when mcp.json is the OpenAI plugin config, otherwise in mcp.json (C-18)`
|
|
3319
|
+
};
|
|
3320
|
+
}
|
|
3321
|
+
const chosen = manifests[0] ?? cands.find((c) => c.role === "partial" && c.name === "mcp-server.json") ?? cands.find((c) => c.role === "partial");
|
|
3322
|
+
if (!chosen) {
|
|
3323
|
+
const plugin = cands.find((c) => c.role === "plugin-config");
|
|
3324
|
+
return {
|
|
3325
|
+
id: this.id,
|
|
3326
|
+
anchor: plugin ? plugin.path : tree.root,
|
|
3327
|
+
state: "non-conforming",
|
|
3328
|
+
verdict: plugin ? `no server manifest found: ${plugin.name} carries \`mcpServers\` (the OpenAI plugin config, not the server manifest) and no root file carries capabilities + transport + version -> add mcp-server.json with the server manifest (C-18)` : "no mcp.json or mcp-server.json found at the tree root -> the server/App manifest is required (C-18)"
|
|
3329
|
+
};
|
|
3330
|
+
}
|
|
3331
|
+
path = chosen.path;
|
|
3332
|
+
manifest = chosen.json;
|
|
3333
|
+
manifestName = chosen.name;
|
|
3334
|
+
} else {
|
|
3335
|
+
if (!existsSync4(path)) {
|
|
3336
|
+
return {
|
|
3337
|
+
id: this.id,
|
|
3338
|
+
anchor: tree.root,
|
|
3339
|
+
state: "non-conforming",
|
|
3340
|
+
verdict: "no extension.json found at the tree root -> the extension manifest is required (C-18)"
|
|
3341
|
+
};
|
|
3342
|
+
}
|
|
3343
|
+
manifest = readJson(path);
|
|
3344
|
+
if (!manifest) {
|
|
3345
|
+
return {
|
|
3346
|
+
id: this.id,
|
|
3347
|
+
anchor: path,
|
|
3348
|
+
state: "non-conforming",
|
|
3349
|
+
verdict: `${manifestName} present but could not be parsed as JSON -> fix the manifest (C-18)`
|
|
3350
|
+
};
|
|
3351
|
+
}
|
|
2621
3352
|
}
|
|
3353
|
+
if (!manifest) throw new Error("C18: manifest selection produced no manifest \u2014 unreachable");
|
|
2622
3354
|
if (typeof manifest.name !== "string" || manifest.name.length === 0) {
|
|
2623
3355
|
return {
|
|
2624
3356
|
id: this.id,
|
|
@@ -2652,7 +3384,7 @@ var c18StructureManifestInner = {
|
|
|
2652
3384
|
id: this.id,
|
|
2653
3385
|
anchor: path,
|
|
2654
3386
|
state: "non-conforming",
|
|
2655
|
-
verdict:
|
|
3387
|
+
verdict: `${manifestName} is missing required field(s): ${missing.join(", ")} not declared -> declare capabilities/transport (C-18)`
|
|
2656
3388
|
};
|
|
2657
3389
|
}
|
|
2658
3390
|
}
|
|
@@ -2688,30 +3420,52 @@ var c18StructureManifest = guardUnreadablePackageJson(
|
|
|
2688
3420
|
// src/detectors/c19-served-descriptions.ts
|
|
2689
3421
|
var PIN6 = "a74a042b53c71a095ab88b3073bd47728eeda814";
|
|
2690
3422
|
var TOOL_DESC_INLINE_RE = /\b(?:s\.tool|\.tool|registerTool)\s*\(\s*(?:["'`][^"'`]+["'`]|[A-Za-z_$][\w$]*)\s*,\s*["'`]([^"'`]+)["'`]/;
|
|
2691
|
-
var DESCRIPTOR_DESC_RE = /\bdescription\s*:\s*["'`]([^"'`]+)["'`]/;
|
|
2692
3423
|
var BARE_STRING_RE = /^\s*["'`]([^"'`]+)["'`]\s*,?\s*$/;
|
|
2693
3424
|
var BACKWARD_LOOKBACK = 10;
|
|
2694
3425
|
var FORWARD_LOOKAHEAD = 6;
|
|
3426
|
+
var DESCRIPTION_KEY_RE = /\bdescription\s*:/;
|
|
2695
3427
|
function findInlineDescription(tree, siteAnchor, toolName) {
|
|
3428
|
+
const forwarded = wrapperDescriptionArg(tree, siteAnchor);
|
|
3429
|
+
if (forwarded !== void 0) {
|
|
3430
|
+
const resolved = /^[A-Za-z_$][\w$]*$/.test(forwarded) ? constExpression(tree, forwarded) : null;
|
|
3431
|
+
const v = readDescriptionValue(resolved ?? forwarded);
|
|
3432
|
+
if (v.kind === "literal" && v.text.length > 0) return v.text;
|
|
3433
|
+
if (v.kind === "non-literal") return v;
|
|
3434
|
+
}
|
|
2696
3435
|
const [rel, lineStr] = siteAnchor.split(":");
|
|
2697
3436
|
const file = tree.files.find((f) => f.rel === rel);
|
|
2698
3437
|
if (!file) return void 0;
|
|
2699
3438
|
const siteLine = Number.parseInt(lineStr, 10);
|
|
2700
|
-
|
|
3439
|
+
let floor = 1;
|
|
3440
|
+
for (const other of findRegisteredTools(tree)) {
|
|
3441
|
+
const [orel, oline] = other.anchor.split(":");
|
|
3442
|
+
const n = Number.parseInt(oline, 10);
|
|
3443
|
+
if (orel === rel && n < siteLine) floor = Math.max(floor, ownCallEnd(file, n) + 1);
|
|
3444
|
+
}
|
|
3445
|
+
const start = Math.max(floor, siteLine - BACKWARD_LOOKBACK);
|
|
2701
3446
|
const end = Math.min(siteLine + FORWARD_LOOKAHEAD, file.lines.length + 1);
|
|
3447
|
+
let nonLiteral;
|
|
2702
3448
|
for (let n = start; n < end; n++) {
|
|
2703
3449
|
const line = file.lines[n - 1];
|
|
2704
3450
|
if (!line || line.comment) continue;
|
|
2705
3451
|
const inline = line.raw.match(TOOL_DESC_INLINE_RE);
|
|
2706
3452
|
if (inline) return inline[1];
|
|
2707
|
-
const
|
|
2708
|
-
if (
|
|
3453
|
+
const key = line.raw.match(DESCRIPTION_KEY_RE);
|
|
3454
|
+
if (key && key.index !== void 0) {
|
|
3455
|
+
const after = line.raw.slice(key.index + key[0].length);
|
|
3456
|
+
const tail = file.lines.slice(n, n + 8).filter((l) => !l.comment).map((l) => l.raw).join("\n");
|
|
3457
|
+
const v = readDescriptionValue(`${after}
|
|
3458
|
+
${tail}`);
|
|
3459
|
+
if (v.kind === "literal" && v.text.length > 0) return v.text;
|
|
3460
|
+
if (v.kind === "non-literal" && !nonLiteral) nonLiteral = v;
|
|
3461
|
+
continue;
|
|
3462
|
+
}
|
|
2709
3463
|
if (n >= siteLine) {
|
|
2710
3464
|
const bare = line.raw.match(BARE_STRING_RE);
|
|
2711
3465
|
if (bare && bare[1] !== toolName) return bare[1];
|
|
2712
3466
|
}
|
|
2713
3467
|
}
|
|
2714
|
-
return
|
|
3468
|
+
return nonLiteral;
|
|
2715
3469
|
}
|
|
2716
3470
|
var TRIGGER_LIST_RE = /use this whenever/i;
|
|
2717
3471
|
var TRIGGER_CLOSE_RE = /even if they don'?t say/i;
|
|
@@ -2726,7 +3480,10 @@ function c19PositiveControls() {
|
|
|
2726
3480
|
triggerListAccepts: TRIGGER_LIST_RE.test(triggerSample),
|
|
2727
3481
|
triggerCloseAccepts: TRIGGER_CLOSE_RE.test(triggerSample),
|
|
2728
3482
|
bilingualAccepts: BILINGUAL_SPLIT_RE.test(bilingualSample),
|
|
2729
|
-
bilingualRejectsMonolingual: !BILINGUAL_SPLIT_RE.test(monoSample)
|
|
3483
|
+
bilingualRejectsMonolingual: !BILINGUAL_SPLIT_RE.test(monoSample),
|
|
3484
|
+
concatenationAccepts: JSON.stringify(readDescriptionValue(`"a, b's " +
|
|
3485
|
+
"c"`)) === JSON.stringify({ kind: "literal", text: "a, b's c" }),
|
|
3486
|
+
nonLiteralRejects: readDescriptionValue("describeTool(x)").kind === "non-literal"
|
|
2730
3487
|
};
|
|
2731
3488
|
}
|
|
2732
3489
|
var c19ServedDescriptions = {
|
|
@@ -2735,7 +3492,7 @@ var c19ServedDescriptions = {
|
|
|
2735
3492
|
pin: PIN6,
|
|
2736
3493
|
run(tree) {
|
|
2737
3494
|
const controls = c19PositiveControls();
|
|
2738
|
-
if (!controls.inlineDescAccepts || !controls.triggerListAccepts || !controls.triggerCloseAccepts || !controls.bilingualAccepts || !controls.bilingualRejectsMonolingual) {
|
|
3495
|
+
if (!controls.inlineDescAccepts || !controls.triggerListAccepts || !controls.triggerCloseAccepts || !controls.bilingualAccepts || !controls.bilingualRejectsMonolingual || !controls.concatenationAccepts || !controls.nonLiteralRejects) {
|
|
2739
3496
|
throw new Error(
|
|
2740
3497
|
`C19 positive control FAILED: ${JSON.stringify(controls)} \u2014 matcher broken, verdict untrustworthy`
|
|
2741
3498
|
);
|
|
@@ -2763,8 +3520,13 @@ var c19ServedDescriptions = {
|
|
|
2763
3520
|
verdict: "no tool registrations found; served-descriptions check n/a (C-19)"
|
|
2764
3521
|
};
|
|
2765
3522
|
}
|
|
3523
|
+
let opaque;
|
|
2766
3524
|
for (const site of sites) {
|
|
2767
3525
|
const desc = findInlineDescription(tree, site.anchor, site.name);
|
|
3526
|
+
if (typeof desc === "object") {
|
|
3527
|
+
opaque ??= { anchor: site.anchor, name: site.name, expr: desc.expr };
|
|
3528
|
+
continue;
|
|
3529
|
+
}
|
|
2768
3530
|
if (!desc) {
|
|
2769
3531
|
return {
|
|
2770
3532
|
id: this.id,
|
|
@@ -2774,6 +3536,16 @@ var c19ServedDescriptions = {
|
|
|
2774
3536
|
};
|
|
2775
3537
|
}
|
|
2776
3538
|
}
|
|
3539
|
+
if (opaque) {
|
|
3540
|
+
const reason = `tool "${opaque.name}" at ${opaque.anchor} declares \`description: ${opaque.expr}\`, which is not a string literal, a concatenation of literals, or a template literal without expressions \u2014 the declared text is unreadable statically (C-19)`;
|
|
3541
|
+
return {
|
|
3542
|
+
id: this.id,
|
|
3543
|
+
anchor: opaque.anchor,
|
|
3544
|
+
state: "could-not-judge",
|
|
3545
|
+
reason,
|
|
3546
|
+
verdict: `could not judge: ${reason}`
|
|
3547
|
+
};
|
|
3548
|
+
}
|
|
2777
3549
|
return {
|
|
2778
3550
|
id: this.id,
|
|
2779
3551
|
anchor: sites[0].anchor,
|
|
@@ -3116,6 +3888,11 @@ var IDENTITY_KEY_NAMES = [
|
|
|
3116
3888
|
"gitCommit",
|
|
3117
3889
|
"gitRevision"
|
|
3118
3890
|
];
|
|
3891
|
+
var HEALTH_ROUTE_RE = /\b[A-Za-z_$][\w$]*\.(?:get|all|route|use)\s*\(\s*["'`][^"'`]*\/health|\bpathname\s*===?\s*["'`][^"'`]*\/health/;
|
|
3892
|
+
var HEALTH_IDENTITY_KEYS = [...IDENTITY_KEY_NAMES, "commit"];
|
|
3893
|
+
var HEALTH_KEY_LINE_RE = new RegExp(
|
|
3894
|
+
`(?:^|[\\s{,])(${HEALTH_IDENTITY_KEYS.join("|")})\\s*:\\s*(.+?)\\s*,?\\s*$`
|
|
3895
|
+
);
|
|
3119
3896
|
var CONSTRUCTOR_RE = /\bnew\s+(?:McpServer|Server)\s*\(/;
|
|
3120
3897
|
var IDENTITY_KEY_LINE_RE = new RegExp(
|
|
3121
3898
|
`\\b(${IDENTITY_KEY_NAMES.join("|")})\\s*:\\s*(.+?)\\s*,?\\s*$`
|
|
@@ -3131,12 +3908,25 @@ function isLiteralValue(expr) {
|
|
|
3131
3908
|
if (m[1] === "`" && /\$\{/.test(m[2])) return false;
|
|
3132
3909
|
return true;
|
|
3133
3910
|
}
|
|
3911
|
+
function valueIsLiteral(tree, expr) {
|
|
3912
|
+
const trimmed = expr.trim().replace(/,$/, "").trim();
|
|
3913
|
+
if (/^[A-Za-z_$][\w$]*$/.test(trimmed)) {
|
|
3914
|
+
const bound = constExpression(tree, trimmed);
|
|
3915
|
+
if (bound !== null) return isLiteralValue(bound);
|
|
3916
|
+
}
|
|
3917
|
+
return isLiteralValue(expr);
|
|
3918
|
+
}
|
|
3134
3919
|
function c25PositiveControls() {
|
|
3920
|
+
const hd = ' commit: process.env.RAILWAY_GIT_COMMIT_SHA ?? "unknown",'.match(HEALTH_KEY_LINE_RE);
|
|
3921
|
+
const hl = ' commit: "4b825dc",'.match(HEALTH_KEY_LINE_RE);
|
|
3135
3922
|
const literalMatch = LITERAL_SAMPLE.match(IDENTITY_KEY_LINE_RE);
|
|
3136
3923
|
const derivedMatch = DERIVED_SAMPLE.match(IDENTITY_KEY_LINE_RE);
|
|
3137
3924
|
return {
|
|
3138
3925
|
literalAccepts: literalMatch ? isLiteralValue(literalMatch[2]) : false,
|
|
3139
|
-
derivedRejectsAsLiteral: derivedMatch ? isLiteralValue(derivedMatch[2]) : false
|
|
3926
|
+
derivedRejectsAsLiteral: derivedMatch ? isLiteralValue(derivedMatch[2]) : false,
|
|
3927
|
+
healthRouteMatches: HEALTH_ROUTE_RE.test('app.get("/health", (c) =>') && !HEALTH_ROUTE_RE.test('app.get("/tools", h)'),
|
|
3928
|
+
healthDerivedCommitReads: hd ? !isLiteralValue(hd[2]) : false,
|
|
3929
|
+
healthLiteralCommitReads: hl ? isLiteralValue(hl[2]) : false
|
|
3140
3930
|
};
|
|
3141
3931
|
}
|
|
3142
3932
|
function extractConstructorArgs(file, startIdx) {
|
|
@@ -3161,6 +3951,27 @@ function extractConstructorArgs(file, startIdx) {
|
|
|
3161
3951
|
}
|
|
3162
3952
|
return null;
|
|
3163
3953
|
}
|
|
3954
|
+
function findHealthSurfaces(tree) {
|
|
3955
|
+
const out = [];
|
|
3956
|
+
for (const file of tree.files) {
|
|
3957
|
+
file.lines.forEach((line, idx) => {
|
|
3958
|
+
if (line.comment || !HEALTH_ROUTE_RE.test(line.raw)) return;
|
|
3959
|
+
const anchor = `${file.rel}:${line.n}`;
|
|
3960
|
+
const body = extractConstructorArgs(file, idx);
|
|
3961
|
+
if (body === null || !/\bversion\s*:/.test(body)) return;
|
|
3962
|
+
for (const raw of body.split("\n")) {
|
|
3963
|
+
const m = raw.match(HEALTH_KEY_LINE_RE);
|
|
3964
|
+
if (!m) continue;
|
|
3965
|
+
const value = m[2];
|
|
3966
|
+
out.push(
|
|
3967
|
+
valueIsLiteral(tree, value) ? { kind: "literal", key: m[1], value: value.trim(), anchor: `${anchor} (/health)` } : { kind: "derived", key: m[1], anchor: `${anchor} (/health)` }
|
|
3968
|
+
);
|
|
3969
|
+
return;
|
|
3970
|
+
}
|
|
3971
|
+
});
|
|
3972
|
+
}
|
|
3973
|
+
return out;
|
|
3974
|
+
}
|
|
3164
3975
|
function findConstructorSites(tree) {
|
|
3165
3976
|
const sites = [];
|
|
3166
3977
|
for (const file of tree.files) {
|
|
@@ -3176,14 +3987,14 @@ function findConstructorSites(tree) {
|
|
|
3176
3987
|
}
|
|
3177
3988
|
return sites;
|
|
3178
3989
|
}
|
|
3179
|
-
function classifyArgs(args, anchor) {
|
|
3990
|
+
function classifyArgs(tree, args, anchor) {
|
|
3180
3991
|
for (const raw of args.split("\n")) {
|
|
3181
3992
|
const bare = raw.match(IDENTITY_KEY_BARE_RE);
|
|
3182
3993
|
if (bare) return { kind: "bare-continuation", key: bare[1], anchor };
|
|
3183
3994
|
const m = raw.match(IDENTITY_KEY_LINE_RE);
|
|
3184
3995
|
if (m) {
|
|
3185
3996
|
const [, key, value] = m;
|
|
3186
|
-
if (
|
|
3997
|
+
if (valueIsLiteral(tree, value)) return { kind: "literal", key, value: value.trim(), anchor };
|
|
3187
3998
|
return { kind: "derived", key, anchor };
|
|
3188
3999
|
}
|
|
3189
4000
|
}
|
|
@@ -3195,7 +4006,7 @@ var c25BuildIdentity = {
|
|
|
3195
4006
|
pin: PIN8,
|
|
3196
4007
|
run(tree) {
|
|
3197
4008
|
const controls = c25PositiveControls();
|
|
3198
|
-
if (!controls.literalAccepts || controls.derivedRejectsAsLiteral) {
|
|
4009
|
+
if (!controls.literalAccepts || controls.derivedRejectsAsLiteral || !controls.healthRouteMatches || !controls.healthDerivedCommitReads || !controls.healthLiteralCommitReads) {
|
|
3199
4010
|
throw new Error(
|
|
3200
4011
|
`C25 literal/derived control FAILED: ${JSON.stringify(controls)} \u2014 matcher broken, verdict untrustworthy`
|
|
3201
4012
|
);
|
|
@@ -3221,8 +4032,18 @@ var c25BuildIdentity = {
|
|
|
3221
4032
|
};
|
|
3222
4033
|
}
|
|
3223
4034
|
}
|
|
4035
|
+
const health = findHealthSurfaces(tree);
|
|
4036
|
+
const healthDerived = health.find((h) => h.kind === "derived");
|
|
4037
|
+
if (healthDerived && healthDerived.kind === "derived") {
|
|
4038
|
+
return {
|
|
4039
|
+
id: this.id,
|
|
4040
|
+
anchor: healthDerived.anchor,
|
|
4041
|
+
state: "present",
|
|
4042
|
+
verdict: `build-identity field "${healthDerived.key}" at ${healthDerived.anchor} is fed by the build (not a literal) \u2014 served beside version on the unauthenticated /health surface (C-25). Whether the SERVED value equals the expected commit is the dynamique half, run with a live session.`
|
|
4043
|
+
};
|
|
4044
|
+
}
|
|
3224
4045
|
for (const site of sites) {
|
|
3225
|
-
const finding = classifyArgs(site.args, site.anchor);
|
|
4046
|
+
const finding = classifyArgs(tree, site.args, site.anchor);
|
|
3226
4047
|
if (finding.kind === "bare-continuation") {
|
|
3227
4048
|
const reason = `identity key "${finding.key}" found at ${finding.anchor} but its value continues past the line (not statically joined) \u2014 verdict unreadable`;
|
|
3228
4049
|
return {
|
|
@@ -3250,6 +4071,15 @@ var c25BuildIdentity = {
|
|
|
3250
4071
|
};
|
|
3251
4072
|
}
|
|
3252
4073
|
}
|
|
4074
|
+
const healthLiteral = health.find((h) => h.kind === "literal");
|
|
4075
|
+
if (healthLiteral && healthLiteral.kind === "literal") {
|
|
4076
|
+
return {
|
|
4077
|
+
id: this.id,
|
|
4078
|
+
anchor: healthLiteral.anchor,
|
|
4079
|
+
state: "non-conforming",
|
|
4080
|
+
verdict: `build-identity field "${healthLiteral.key}" at ${healthLiteral.anchor} is a hand-written literal (${healthLiteral.value}) -> a literal never changes on its own and proves nothing about the tree actually served; derive it from the build (C-25)`
|
|
4081
|
+
};
|
|
4082
|
+
}
|
|
3253
4083
|
return {
|
|
3254
4084
|
id: this.id,
|
|
3255
4085
|
anchor: sites[0].anchor,
|
|
@@ -3358,7 +4188,7 @@ import {
|
|
|
3358
4188
|
writeFileSync
|
|
3359
4189
|
} from "fs";
|
|
3360
4190
|
import { tmpdir } from "os";
|
|
3361
|
-
import { basename as basename2, join as join6, relative as
|
|
4191
|
+
import { basename as basename2, join as join6, relative as relative3, sep as sep2 } from "path";
|
|
3362
4192
|
var PIN9 = "20136b50";
|
|
3363
4193
|
var DEPLOY_CONFIG_FILES = [
|
|
3364
4194
|
"railway.json",
|
|
@@ -3461,12 +4291,12 @@ function c26DeriveScope(root, tree) {
|
|
|
3461
4291
|
const skipped = [];
|
|
3462
4292
|
const onSkip = (skippedDir, depth) => {
|
|
3463
4293
|
const entry = tree ? recordSkip(tree, skippedDir, root) : {
|
|
3464
|
-
path:
|
|
4294
|
+
path: relative3(root, skippedDir).split(sep2).join("/"),
|
|
3465
4295
|
reason: SKIP_DIR_INFO[basename2(skippedDir)]?.reason ?? "skipped"
|
|
3466
4296
|
};
|
|
3467
4297
|
if (SKIP_DIR_INFO[basename2(skippedDir)]?.authored) {
|
|
3468
4298
|
const hit = probeForCandidate(skippedDir, depth);
|
|
3469
|
-
if (hit) entry.candidate =
|
|
4299
|
+
if (hit) entry.candidate = relative3(root, hit).split(sep2).join("/");
|
|
3470
4300
|
}
|
|
3471
4301
|
if (!skipped.some((d) => d.path === entry.path)) skipped.push(entry);
|
|
3472
4302
|
};
|
|
@@ -3650,7 +4480,7 @@ function renderCoverage(scope, verdicts) {
|
|
|
3650
4480
|
}
|
|
3651
4481
|
}
|
|
3652
4482
|
return verdicts.map((v) => {
|
|
3653
|
-
const rel =
|
|
4483
|
+
const rel = relative3(process.cwd(), v.site.dir) || v.site.dir;
|
|
3654
4484
|
if (v.status === "skipped") return `SKIPPED ${rel} (${v.reason})`;
|
|
3655
4485
|
return `ANALYSED ${rel} -> ${v.ok ? "GREEN" : "RED"}: ${v.detail}`;
|
|
3656
4486
|
}).join(" | ");
|
|
@@ -3728,30 +4558,102 @@ var c26SubpackageLockfile = {
|
|
|
3728
4558
|
|
|
3729
4559
|
// src/detectors/d2.ts
|
|
3730
4560
|
var SESSION_RE = /mcp-session-id/i;
|
|
4561
|
+
var GENERATOR_RE = /\bsessionIdGenerator\s*:\s*(?!undefined\b)\S/;
|
|
4562
|
+
var HEADER_CALL_RE = /\b(?:get|header|set|setHeader|append|getHeader)\s*\(\s*["'`]mcp-session-id["'`]/i;
|
|
4563
|
+
var HEADER_INDEX_RE = /\[\s*["'`]mcp-session-id["'`]\s*\]/i;
|
|
4564
|
+
var HEADER_KEY_RE = /["'`]mcp-session-id["'`]\s*:/i;
|
|
4565
|
+
var STORE_RE = /\b(?:sessions?|transports?)\w*\s*(?:\[\s*\w*[sS]essionId|\.(?:get|set|has|delete)\(\s*\w*[sS]essionId)/;
|
|
4566
|
+
var KEYING = [
|
|
4567
|
+
{ id: "sessionIdGenerator set", re: GENERATOR_RE },
|
|
4568
|
+
{ id: "header read/written by call", re: HEADER_CALL_RE },
|
|
4569
|
+
{ id: "header read by index", re: HEADER_INDEX_RE },
|
|
4570
|
+
{ id: "header sent as an object key", re: HEADER_KEY_RE },
|
|
4571
|
+
{ id: "store keyed by a session id", re: STORE_RE }
|
|
4572
|
+
];
|
|
4573
|
+
var CORS_OPEN_RE = /\b(?:allow|expose)d?Headers\s*:\s*\[|Access-Control-(?:Allow|Expose)-Headers/i;
|
|
4574
|
+
function inCorsList(file, idx) {
|
|
4575
|
+
if (CORS_OPEN_RE.test(file.lines[idx].raw)) return true;
|
|
4576
|
+
let depth = 0;
|
|
4577
|
+
for (let i = idx; i >= Math.max(0, idx - 25); i--) {
|
|
4578
|
+
const l = file.lines[i];
|
|
4579
|
+
if (l.comment) continue;
|
|
4580
|
+
const raw = i === idx ? l.raw.slice(0, l.raw.search(SESSION_RE)) : l.raw;
|
|
4581
|
+
depth += (raw.match(/\]/g) ?? []).length - (raw.match(/\[/g) ?? []).length;
|
|
4582
|
+
if (i !== idx && CORS_OPEN_RE.test(l.raw) && depth < 0) return true;
|
|
4583
|
+
if (depth < 0 && i !== idx) return false;
|
|
4584
|
+
}
|
|
4585
|
+
return false;
|
|
4586
|
+
}
|
|
4587
|
+
function d2PositiveControls() {
|
|
4588
|
+
const f = (s) => ({
|
|
4589
|
+
path: "x.ts",
|
|
4590
|
+
rel: "x.ts",
|
|
4591
|
+
lines: s.split("\n").map((raw, i) => ({ n: i + 1, raw, comment: false }))
|
|
4592
|
+
});
|
|
4593
|
+
const cors = f('cors({\n allowHeaders: [\n "Content-Type",\n "mcp-session-id",\n ],\n})');
|
|
4594
|
+
const notCors = f('const x = [\n "mcp-session-id",\n];');
|
|
4595
|
+
return {
|
|
4596
|
+
"keying:generator": GENERATOR_RE.test("sessionIdGenerator: () => randomUUID(),"),
|
|
4597
|
+
"negative:generator-undefined": !GENERATOR_RE.test("sessionIdGenerator: undefined,"),
|
|
4598
|
+
"keying:index": HEADER_INDEX_RE.test('req.headers["mcp-session-id"]'),
|
|
4599
|
+
"keying:call": HEADER_CALL_RE.test('c.req.header("mcp-session-id")'),
|
|
4600
|
+
"keying:store": STORE_RE.test("transports[sessionId]"),
|
|
4601
|
+
"negative:cors-entry-is-not-a-keying-form": !KEYING.some(
|
|
4602
|
+
(k) => k.re.test(' "mcp-session-id",')
|
|
4603
|
+
),
|
|
4604
|
+
"cors:array-above": inCorsList(cors, 3),
|
|
4605
|
+
"negative:plain-array-is-not-cors": !inCorsList(notCors, 1)
|
|
4606
|
+
};
|
|
4607
|
+
}
|
|
3731
4608
|
var d2 = {
|
|
3732
4609
|
id: "D2 stateless",
|
|
3733
4610
|
run(tree) {
|
|
3734
|
-
const
|
|
4611
|
+
const failed = Object.entries(d2PositiveControls()).filter(([, ok]) => !ok);
|
|
4612
|
+
if (failed.length > 0) {
|
|
4613
|
+
throw new Error(
|
|
4614
|
+
`D2 positive control FAILED: ${failed.map(([k]) => k).join(", ")} \u2014 matcher broken, verdict untrustworthy`
|
|
4615
|
+
);
|
|
4616
|
+
}
|
|
4617
|
+
const keying = [];
|
|
4618
|
+
const cors = [];
|
|
4619
|
+
const unknown = [];
|
|
3735
4620
|
for (const file of tree.files) {
|
|
3736
|
-
|
|
3737
|
-
if (line.comment)
|
|
3738
|
-
|
|
3739
|
-
|
|
4621
|
+
file.lines.forEach((line, idx) => {
|
|
4622
|
+
if (line.comment) return;
|
|
4623
|
+
const site = `${file.rel}:${line.n}`;
|
|
4624
|
+
const form = KEYING.find((k) => k.re.test(line.raw));
|
|
4625
|
+
if (form) {
|
|
4626
|
+
keying.push({ site, form: form.id });
|
|
4627
|
+
return;
|
|
4628
|
+
}
|
|
4629
|
+
if (!SESSION_RE.test(line.raw)) return;
|
|
4630
|
+
if (inCorsList(file, idx)) cors.push(site);
|
|
4631
|
+
else unknown.push(site);
|
|
4632
|
+
});
|
|
3740
4633
|
}
|
|
3741
|
-
if (
|
|
3742
|
-
const anchor = `${hits[0].file}:${hits.map((h) => h.line).join(",")}`;
|
|
4634
|
+
if (keying.length > 0) {
|
|
3743
4635
|
return {
|
|
3744
4636
|
id: this.id,
|
|
3745
|
-
anchor
|
|
4637
|
+
anchor: `${keying[0].site}`,
|
|
3746
4638
|
state: "non-conforming",
|
|
3747
|
-
verdict:
|
|
4639
|
+
verdict: `session-keyed -> migrate to stateless: ${keying.map((k) => `${k.site} (${k.form})`).join("; ")}`
|
|
4640
|
+
};
|
|
4641
|
+
}
|
|
4642
|
+
if (unknown.length > 0) {
|
|
4643
|
+
const reason = `mcp-session-id at ${unknown.join(", ")} is neither a CORS header-list entry nor a recognised session-keying form (generator, header read/write, header key, session-keyed store) \u2014 whether it keys sessions is undecidable statically`;
|
|
4644
|
+
return {
|
|
4645
|
+
id: this.id,
|
|
4646
|
+
anchor: unknown[0],
|
|
4647
|
+
state: "could-not-judge",
|
|
4648
|
+
reason,
|
|
4649
|
+
verdict: `could not judge: ${reason}`
|
|
3748
4650
|
};
|
|
3749
4651
|
}
|
|
3750
4652
|
return {
|
|
3751
4653
|
id: this.id,
|
|
3752
|
-
anchor: "-",
|
|
4654
|
+
anchor: cors[0] ?? "-",
|
|
3753
4655
|
state: "present",
|
|
3754
|
-
verdict: "stateless (no mcp-session-id session-keying)"
|
|
4656
|
+
verdict: cors.length > 0 ? `stateless: mcp-session-id is only named in CORS header lists (${cors.join(", ")}) \u2014 an inert header name, no session id generator, header read or session-keyed store found` : "stateless (no mcp-session-id session-keying)"
|
|
3755
4657
|
};
|
|
3756
4658
|
}
|
|
3757
4659
|
};
|
|
@@ -3989,9 +4891,53 @@ function depListed(pkgJson) {
|
|
|
3989
4891
|
}
|
|
3990
4892
|
return false;
|
|
3991
4893
|
}
|
|
4894
|
+
var SDK_V2_PKG = "@modelcontextprotocol/server";
|
|
4895
|
+
var DEP_KEYS = ["dependencies", "devDependencies", "peerDependencies", "optionalDependencies"];
|
|
4896
|
+
function sdkV2Dependency(pkgJson) {
|
|
4897
|
+
if (!pkgJson) return { kind: "none" };
|
|
4898
|
+
for (const key of DEP_KEYS) {
|
|
4899
|
+
const deps = pkgJson[key];
|
|
4900
|
+
if (!deps || typeof deps !== "object") continue;
|
|
4901
|
+
const range = deps[SDK_V2_PKG];
|
|
4902
|
+
if (typeof range !== "string") continue;
|
|
4903
|
+
const m = range.trim().match(/^(?:[\^~]|>=|>|=)?\s*v?(\d+)(?:\.(?:\d+|x|\*))*(?:\s|$|-)/);
|
|
4904
|
+
if (!m) return { kind: "unreadable", range };
|
|
4905
|
+
return Number(m[1]) >= 2 ? { kind: "v2", range } : { kind: "below-v2", range };
|
|
4906
|
+
}
|
|
4907
|
+
return { kind: "none" };
|
|
4908
|
+
}
|
|
4909
|
+
var HANDROLLED_FIELD_RE = /\b(resultType|ttlMs|cacheScope)\s*:/;
|
|
4910
|
+
function handRolledBuilder(tree) {
|
|
4911
|
+
for (const file of tree.files) {
|
|
4912
|
+
const code = file.lines;
|
|
4913
|
+
let hintsDepth = 0;
|
|
4914
|
+
for (const line of code) {
|
|
4915
|
+
if (line.comment) continue;
|
|
4916
|
+
const opens = (line.raw.match(/\{/g) ?? []).length;
|
|
4917
|
+
const closes = (line.raw.match(/\}/g) ?? []).length;
|
|
4918
|
+
const startsHints = /\bcacheHints\s*:/.test(line.raw);
|
|
4919
|
+
const inside = hintsDepth > 0 || startsHints;
|
|
4920
|
+
if (!inside) {
|
|
4921
|
+
const m = line.raw.match(HANDROLLED_FIELD_RE);
|
|
4922
|
+
if (m) return { file: file.rel, line: line.n, field: m[1] };
|
|
4923
|
+
}
|
|
4924
|
+
if (startsHints || hintsDepth > 0) {
|
|
4925
|
+
hintsDepth += opens - closes;
|
|
4926
|
+
if (hintsDepth < 0) hintsDepth = 0;
|
|
4927
|
+
}
|
|
4928
|
+
}
|
|
4929
|
+
}
|
|
4930
|
+
return null;
|
|
4931
|
+
}
|
|
3992
4932
|
var d7Inner = {
|
|
3993
4933
|
id: "D7 protocol-kit",
|
|
3994
4934
|
run(tree) {
|
|
4935
|
+
const v2 = (r) => sdkV2Dependency({ dependencies: { [SDK_V2_PKG]: r } }).kind;
|
|
4936
|
+
if (v2("^2.3.1") !== "v2" || v2(">=2 <3") !== "v2" || v2("^1.12.0") !== "below-v2" || v2("latest") !== "unreadable" || !HANDROLLED_FIELD_RE.test('{ resultType: "complete" }') || HANDROLLED_FIELD_RE.test("{ resultTypes: [] }")) {
|
|
4937
|
+
throw new Error(
|
|
4938
|
+
"D7 control FAILED: SDK v2 range reader / hand-rolled matcher broken \u2014 verdict untrustworthy"
|
|
4939
|
+
);
|
|
4940
|
+
}
|
|
3995
4941
|
const listed = depListed(tree.packageJson?.json);
|
|
3996
4942
|
let importLine = null;
|
|
3997
4943
|
let callLine = null;
|
|
@@ -4012,17 +4958,46 @@ var d7Inner = {
|
|
|
4012
4958
|
verdict: "protocol-kit listed + builder called"
|
|
4013
4959
|
};
|
|
4014
4960
|
}
|
|
4015
|
-
if (listed && !called) {
|
|
4961
|
+
if (listed && !called) {
|
|
4962
|
+
return {
|
|
4963
|
+
id: this.id,
|
|
4964
|
+
anchor: tree.packageJson ? tree.packageJson.path.split("/").slice(-2).join("/") : "-",
|
|
4965
|
+
state: "non-conforming",
|
|
4966
|
+
verdict: "protocol-kit listed but no builder called -> wire buildToolsListResult/handleServerDiscover"
|
|
4967
|
+
};
|
|
4968
|
+
}
|
|
4969
|
+
const pkgAnchor = tree.packageJson ? tree.packageJson.path.split("/").slice(-2).join("/") : "-";
|
|
4970
|
+
const sdk = sdkV2Dependency(tree.packageJson?.json);
|
|
4971
|
+
if (sdk.kind === "unreadable") {
|
|
4972
|
+
const reason = `${SDK_V2_PKG} is declared as "${sdk.range}", whose lowest admissible major cannot be read \u2014 whether the SDK v2 path applies is undecidable (D7)`;
|
|
4973
|
+
return {
|
|
4974
|
+
id: this.id,
|
|
4975
|
+
anchor: pkgAnchor,
|
|
4976
|
+
state: "could-not-judge",
|
|
4977
|
+
reason,
|
|
4978
|
+
verdict: `could not judge: ${reason}`
|
|
4979
|
+
};
|
|
4980
|
+
}
|
|
4981
|
+
if (sdk.kind === "v2") {
|
|
4982
|
+
const hand = handRolledBuilder(tree);
|
|
4983
|
+
if (hand) {
|
|
4984
|
+
return {
|
|
4985
|
+
id: this.id,
|
|
4986
|
+
anchor: `${hand.file}:${hand.line}`,
|
|
4987
|
+
state: "non-conforming",
|
|
4988
|
+
verdict: `hand-rolled result builder (\`${hand.field}:\` written in source at ${hand.file}:${hand.line}) and @vantageos/mcp-protocol-kit not a dependency -> let the SDK emit resultType/ttlMs/cacheScope (cacheHints) or adopt the kit (D7)`
|
|
4989
|
+
};
|
|
4990
|
+
}
|
|
4016
4991
|
return {
|
|
4017
4992
|
id: this.id,
|
|
4018
|
-
anchor:
|
|
4019
|
-
state: "
|
|
4020
|
-
verdict:
|
|
4993
|
+
anchor: pkgAnchor,
|
|
4994
|
+
state: "present",
|
|
4995
|
+
verdict: `${SDK_V2_PKG} ${sdk.range} (SDK v2) and no hand-rolled result builder found: the SDK emits resultType/ttlMs/cacheScope, so protocol-kit is not required (D7). That the SDK serves all three fields is proven on the served response (S-2/S-3), not here`
|
|
4021
4996
|
};
|
|
4022
4997
|
}
|
|
4023
4998
|
return {
|
|
4024
4999
|
id: this.id,
|
|
4025
|
-
anchor:
|
|
5000
|
+
anchor: pkgAnchor,
|
|
4026
5001
|
state: "absent",
|
|
4027
5002
|
verdict: "hand-rolled builders, protocol-kit not a dependency -> add @vantageos/mcp-protocol-kit"
|
|
4028
5003
|
};
|
|
@@ -4032,7 +5007,7 @@ var d7 = guardUnreadablePackageJson(d7Inner, { authoritative: true });
|
|
|
4032
5007
|
|
|
4033
5008
|
// src/detectors/d8.ts
|
|
4034
5009
|
import { existsSync as existsSync7, statSync as statSync3 } from "fs";
|
|
4035
|
-
import { dirname as
|
|
5010
|
+
import { dirname as dirname3, join as join7, relative as relative4 } from "path";
|
|
4036
5011
|
var POSITIVE_CONTROL2 = 'server.registerTool("list_items", { ... }, handler);';
|
|
4037
5012
|
function d8PositiveControl() {
|
|
4038
5013
|
return {
|
|
@@ -4042,9 +5017,9 @@ function d8PositiveControl() {
|
|
|
4042
5017
|
};
|
|
4043
5018
|
}
|
|
4044
5019
|
function resolveBaseDir(tree) {
|
|
4045
|
-
if (tree.packageJson) return
|
|
5020
|
+
if (tree.packageJson) return dirname3(tree.packageJson.path);
|
|
4046
5021
|
try {
|
|
4047
|
-
return statSync3(tree.root).isDirectory() ? tree.root :
|
|
5022
|
+
return statSync3(tree.root).isDirectory() ? tree.root : dirname3(tree.root);
|
|
4048
5023
|
} catch {
|
|
4049
5024
|
return tree.root;
|
|
4050
5025
|
}
|
|
@@ -4054,7 +5029,7 @@ function findEvalsFile(tree) {
|
|
|
4054
5029
|
for (let depth = 0; depth < 8; depth++) {
|
|
4055
5030
|
const candidate = join7(dir, "evals", "evals.json");
|
|
4056
5031
|
if (existsSync7(candidate)) return candidate;
|
|
4057
|
-
const parent =
|
|
5032
|
+
const parent = dirname3(dir);
|
|
4058
5033
|
if (parent === dir) break;
|
|
4059
5034
|
dir = parent;
|
|
4060
5035
|
}
|
|
@@ -4115,7 +5090,7 @@ var d8 = {
|
|
|
4115
5090
|
};
|
|
4116
5091
|
}
|
|
4117
5092
|
const baseDir = resolveBaseDir(tree);
|
|
4118
|
-
const displayPath =
|
|
5093
|
+
const displayPath = relative4(baseDir, evalsPath) || evalsPath;
|
|
4119
5094
|
const parsed = readJsonObject(evalsPath);
|
|
4120
5095
|
if (parsed === null || !Array.isArray(parsed.cases)) {
|
|
4121
5096
|
return couldNotJudge(
|
|
@@ -4199,7 +5174,7 @@ var d8 = {
|
|
|
4199
5174
|
|
|
4200
5175
|
// src/detectors/d9.ts
|
|
4201
5176
|
import { existsSync as existsSync8 } from "fs";
|
|
4202
|
-
import { dirname as
|
|
5177
|
+
import { dirname as dirname4, join as join8 } from "path";
|
|
4203
5178
|
var SEMVER_RE = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/;
|
|
4204
5179
|
var POSITIVE_CONTROL3 = "1.2.3-beta.1+build.7";
|
|
4205
5180
|
function d9PositiveControl() {
|
|
@@ -4247,7 +5222,7 @@ var d9Inner = {
|
|
|
4247
5222
|
verdict: `no "version" field in ${pkgPath.split("/").pop()} -> set a valid SemVer version`
|
|
4248
5223
|
};
|
|
4249
5224
|
}
|
|
4250
|
-
const projectDir =
|
|
5225
|
+
const projectDir = dirname4(pkgPath);
|
|
4251
5226
|
const changelogPath = join8(projectDir, "CHANGELOG.md");
|
|
4252
5227
|
const hasChangelog = existsSync8(changelogPath);
|
|
4253
5228
|
const validSemver = SEMVER_RE.test(version);
|
|
@@ -4279,7 +5254,7 @@ var d9 = guardUnreadablePackageJson(d9Inner, { authoritative: false });
|
|
|
4279
5254
|
|
|
4280
5255
|
// src/detectors/d10.ts
|
|
4281
5256
|
import { existsSync as existsSync9 } from "fs";
|
|
4282
|
-
import { dirname as
|
|
5257
|
+
import { dirname as dirname5, join as join9 } from "path";
|
|
4283
5258
|
var DESCRIPTION_FR_RE = /"description_fr"\s*:\s*"[^"]+"/;
|
|
4284
5259
|
var POSITIVE_CONTROL4 = '"description_fr": "Un serveur MCP conforme.",';
|
|
4285
5260
|
function d10PositiveControl() {
|
|
@@ -4314,17 +5289,38 @@ function firstSourceAnchor(tree, re) {
|
|
|
4314
5289
|
function scanSourceDescriptionFr(tree) {
|
|
4315
5290
|
const recognized = firstSourceAnchor(tree, SOURCE_DESCRIPTION_FR_RE);
|
|
4316
5291
|
if (recognized) return { present: true, anchor: recognized.anchor };
|
|
4317
|
-
const
|
|
4318
|
-
|
|
4319
|
-
|
|
4320
|
-
|
|
4321
|
-
|
|
4322
|
-
|
|
4323
|
-
|
|
4324
|
-
|
|
5292
|
+
const bindings = resolveNameBindings(tree);
|
|
5293
|
+
let undecidable;
|
|
5294
|
+
for (const file of tree.files) {
|
|
5295
|
+
for (let i = 0; i < file.lines.length; i++) {
|
|
5296
|
+
const line = file.lines[i];
|
|
5297
|
+
if (line.comment || !MENTION_RE.test(line.raw)) continue;
|
|
5298
|
+
const anchor = `${file.rel}:${line.n}`;
|
|
5299
|
+
const key = line.raw.match(/description_fr\s*:/);
|
|
5300
|
+
if (!key || key.index === void 0) {
|
|
5301
|
+
undecidable ??= {
|
|
5302
|
+
anchor,
|
|
5303
|
+
reason: `description_fr occurrence at ${anchor} (\`${line.raw.trim()}\`) does not match the recognized source form /${SOURCE_DESCRIPTION_FR_RE.source}/ \u2014 unknown bilingual-declaration shape, verdict undecidable`
|
|
5304
|
+
};
|
|
5305
|
+
continue;
|
|
4325
5306
|
}
|
|
4326
|
-
|
|
5307
|
+
const after = line.raw.slice(key.index + key[0].length);
|
|
5308
|
+
const tail = file.lines.slice(i + 1, i + 9).filter((l) => !l.comment).map((l) => l.raw).join("\n");
|
|
5309
|
+
const v = readDescriptionValue(`${after}
|
|
5310
|
+
${tail}`);
|
|
5311
|
+
if (v.kind === "literal" && v.text.length > 0) return { present: true, anchor };
|
|
5312
|
+
if (v.kind === "non-literal") {
|
|
5313
|
+
const ident = v.expr.match(/^([A-Za-z_$][\w$]*)\s*[,}]?\s*$/);
|
|
5314
|
+
if (ident && bindings.get(ident[1])) return { present: true, anchor };
|
|
5315
|
+
const form = ident ? "an identifier that does not resolve to a same-tree string const" : /\w\s*\(/.test(v.expr) ? "a call" : "a non-literal expression";
|
|
5316
|
+
undecidable ??= {
|
|
5317
|
+
anchor,
|
|
5318
|
+
reason: `description_fr at ${anchor} is followed by ${form} (\`${v.expr}\`) \u2014 the French text is computed, so whether it is declared and non-empty is unverifiable statically; verdict undecidable`
|
|
5319
|
+
};
|
|
5320
|
+
}
|
|
5321
|
+
}
|
|
4327
5322
|
}
|
|
5323
|
+
if (undecidable) return { present: false, anchor: null, undecidable };
|
|
4328
5324
|
return { present: false, anchor: null };
|
|
4329
5325
|
}
|
|
4330
5326
|
var d10Inner = {
|
|
@@ -4344,6 +5340,11 @@ var d10Inner = {
|
|
|
4344
5340
|
`D10 source-form control FAILED: /${sourceControl.pattern}/ accept=${sourceControl.matchedPositive} reject=${!sourceControl.matchedNegative} \u2014 matcher broken, absence verdict untrustworthy`
|
|
4345
5341
|
);
|
|
4346
5342
|
}
|
|
5343
|
+
if (readDescriptionValue('\n "Un serveur."').kind !== "literal" || readDescriptionValue("t(locale, k)").kind !== "non-literal") {
|
|
5344
|
+
throw new Error(
|
|
5345
|
+
"D10 value-reader control FAILED: must accept a next-line literal and reject a call \u2014 verdict untrustworthy"
|
|
5346
|
+
);
|
|
5347
|
+
}
|
|
4347
5348
|
if (!tree.packageJson) {
|
|
4348
5349
|
return {
|
|
4349
5350
|
id: this.id,
|
|
@@ -4354,7 +5355,7 @@ var d10Inner = {
|
|
|
4354
5355
|
};
|
|
4355
5356
|
}
|
|
4356
5357
|
const { path: pkgPath, json: pkg } = tree.packageJson;
|
|
4357
|
-
const root =
|
|
5358
|
+
const root = dirname5(pkgPath);
|
|
4358
5359
|
const descriptionFr = typeof pkg.description_fr === "string" ? pkg.description_fr.trim() : "";
|
|
4359
5360
|
const hasDescriptionFrJson = descriptionFr.length > 0;
|
|
4360
5361
|
const sourceScan = scanSourceDescriptionFr(tree);
|
|
@@ -4774,11 +5775,11 @@ var d15 = {
|
|
|
4774
5775
|
|
|
4775
5776
|
// src/detectors/d16.ts
|
|
4776
5777
|
import { existsSync as existsSync10, readFileSync as readFileSync8, statSync as statSync4 } from "fs";
|
|
4777
|
-
import { dirname as
|
|
5778
|
+
import { dirname as dirname6, join as join10 } from "path";
|
|
4778
5779
|
var DISCOVER_RE2 = /server\/discover|DiscoverResult|ServerDiscover|handleServerDiscover/;
|
|
4779
5780
|
var HEALTH_ROUTE_FRAMEWORK_RE = /\b[A-Za-z_$][\w$]*\.(?:get|post|put|patch|delete|all|route|use)\s*\(\s*["'`][^"'`]*\/health/;
|
|
4780
5781
|
var HEALTH_ROUTE_RAWPATH_RE = /\bpathname\s*===?\s*["'`][^"'`]*\/health/;
|
|
4781
|
-
var
|
|
5782
|
+
var HEALTH_ROUTE_RE2 = new RegExp(
|
|
4782
5783
|
`${HEALTH_ROUTE_FRAMEWORK_RE.source}|${HEALTH_ROUTE_RAWPATH_RE.source}`
|
|
4783
5784
|
);
|
|
4784
5785
|
var VERSION_FIELD_RE = /\bversion\s*:/;
|
|
@@ -4789,6 +5790,7 @@ var VERSION_COMPUTED_RE = /\bversion\s*:\s*[A-Za-z_$][\w.]*\s*\(/;
|
|
|
4789
5790
|
var VERSION_LITERAL_RE = /\bversion\s*:\s*["'`][^"'`]+["'`]/;
|
|
4790
5791
|
var AUTH_GATE_RE = /requireAuth|verifyBearer|assertAuth|ensureAuthenticated|withAuth|requireBearer/;
|
|
4791
5792
|
var WINDOW = 25;
|
|
5793
|
+
var SDK_SERVER_NEW_RE = /\bnew\s+McpServer\s*\(/;
|
|
4792
5794
|
var CONTROLS2 = [
|
|
4793
5795
|
{
|
|
4794
5796
|
key: "surface",
|
|
@@ -4798,7 +5800,7 @@ var CONTROLS2 = [
|
|
|
4798
5800
|
},
|
|
4799
5801
|
{
|
|
4800
5802
|
key: "health-framework",
|
|
4801
|
-
re:
|
|
5803
|
+
re: HEALTH_ROUTE_RE2,
|
|
4802
5804
|
positive: 'app.get("/health", handler)',
|
|
4803
5805
|
negative: 'app.get("/tools", handler)'
|
|
4804
5806
|
},
|
|
@@ -4807,7 +5809,7 @@ var CONTROLS2 = [
|
|
|
4807
5809
|
// Positive is the REAL vantage-registry-mcp:297 shape; negative is the
|
|
4808
5810
|
// sibling `/mcp` route on the same server (a near-miss that must NOT match).
|
|
4809
5811
|
key: "health-rawpath",
|
|
4810
|
-
re:
|
|
5812
|
+
re: HEALTH_ROUTE_RE2,
|
|
4811
5813
|
positive: 'if (req.method === "GET" && url.pathname === "/health") {',
|
|
4812
5814
|
negative: 'if (url.pathname === "/mcp") {'
|
|
4813
5815
|
},
|
|
@@ -4817,6 +5819,12 @@ var CONTROLS2 = [
|
|
|
4817
5819
|
positive: "version: pkg.version",
|
|
4818
5820
|
negative: "version: tool.version"
|
|
4819
5821
|
},
|
|
5822
|
+
{
|
|
5823
|
+
key: "sdk-construction",
|
|
5824
|
+
re: SDK_SERVER_NEW_RE,
|
|
5825
|
+
positive: "const s = new McpServer({ name, version }, opts)",
|
|
5826
|
+
negative: "const s = new Server({ name, version })"
|
|
5827
|
+
},
|
|
4820
5828
|
{
|
|
4821
5829
|
key: "auth-gate",
|
|
4822
5830
|
re: AUTH_GATE_RE,
|
|
@@ -4889,13 +5897,58 @@ function classifySurface(file, startIdx, anchor, pkgIdents) {
|
|
|
4889
5897
|
if (!hasVersion) return { kind: "absent-version", anchor };
|
|
4890
5898
|
return { kind: "unreadable-source", anchor };
|
|
4891
5899
|
}
|
|
5900
|
+
function packageJsonVersionConsts(tree) {
|
|
5901
|
+
const names = /* @__PURE__ */ new Set();
|
|
5902
|
+
const decl = /\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\b/;
|
|
5903
|
+
for (const file of tree.files) {
|
|
5904
|
+
const code = file.lines.filter((l) => !l.comment);
|
|
5905
|
+
for (let i = 0; i < code.length; i++) {
|
|
5906
|
+
const m = code[i].raw.match(decl);
|
|
5907
|
+
if (!m) continue;
|
|
5908
|
+
let stmt = "";
|
|
5909
|
+
let depth = 0;
|
|
5910
|
+
for (let j = i; j < Math.min(code.length, i + 10); j++) {
|
|
5911
|
+
const raw = code[j].raw;
|
|
5912
|
+
stmt += `${raw}
|
|
5913
|
+
`;
|
|
5914
|
+
depth += (raw.match(/[({[]/g) ?? []).length - (raw.match(/[)}\]]/g) ?? []).length;
|
|
5915
|
+
if (depth <= 0 && /;\s*$/.test(raw)) break;
|
|
5916
|
+
}
|
|
5917
|
+
if (/package\.json/.test(stmt) && /\.version\b/.test(stmt)) names.add(m[1]);
|
|
5918
|
+
}
|
|
5919
|
+
}
|
|
5920
|
+
return names;
|
|
5921
|
+
}
|
|
5922
|
+
function sdkServerVersionSurface(tree) {
|
|
5923
|
+
const derived = packageJsonVersionConsts(tree);
|
|
5924
|
+
for (const file of tree.files) {
|
|
5925
|
+
const pkgIdents = collectPkgIdents(file);
|
|
5926
|
+
for (let idx = 0; idx < file.lines.length; idx++) {
|
|
5927
|
+
const line = file.lines[idx];
|
|
5928
|
+
if (line.comment || !SDK_SERVER_NEW_RE.test(line.raw)) continue;
|
|
5929
|
+
const body = extractHandlerBody(file, idx);
|
|
5930
|
+
if (body === null) continue;
|
|
5931
|
+
const v = body.match(/\bversion\s*:\s*([A-Za-z_$][\w$]*(?:\.[\w$]+)*)\s*(?:[,}]|$)/m);
|
|
5932
|
+
if (!v) continue;
|
|
5933
|
+
const expr = v[1];
|
|
5934
|
+
const root = expr.split(".")[0];
|
|
5935
|
+
const direct = pkgIdents.has(root) && expr.endsWith(".version") || PKG_SOURCE_RE.test(`version: ${expr}`);
|
|
5936
|
+
if (direct || !expr.includes(".") && derived.has(expr)) {
|
|
5937
|
+
return { kind: "present", anchor: `${file.rel}:${line.n}`, via: "sdk-server-info" };
|
|
5938
|
+
}
|
|
5939
|
+
}
|
|
5940
|
+
}
|
|
5941
|
+
return null;
|
|
5942
|
+
}
|
|
4892
5943
|
function evalSurfaces(tree) {
|
|
4893
5944
|
const out = [];
|
|
5945
|
+
const sdk = sdkServerVersionSurface(tree);
|
|
5946
|
+
if (sdk) out.push(sdk);
|
|
4894
5947
|
for (const file of tree.files) {
|
|
4895
5948
|
const pkgIdents = collectPkgIdents(file);
|
|
4896
5949
|
file.lines.forEach((line, idx) => {
|
|
4897
5950
|
if (line.comment) return;
|
|
4898
|
-
if (DISCOVER_RE2.test(line.raw) ||
|
|
5951
|
+
if (DISCOVER_RE2.test(line.raw) || HEALTH_ROUTE_RE2.test(line.raw)) {
|
|
4899
5952
|
out.push(classifySurface(file, idx, `${file.rel}:${line.n}`, pkgIdents));
|
|
4900
5953
|
}
|
|
4901
5954
|
});
|
|
@@ -4927,7 +5980,7 @@ var d16 = {
|
|
|
4927
5980
|
id: this.id,
|
|
4928
5981
|
anchor: present.anchor,
|
|
4929
5982
|
state: "present",
|
|
4930
|
-
verdict: `[static read] version exposed pre-auth at ${present.anchor}, sourced from package.json (Server Rule 16)`
|
|
5983
|
+
verdict: present.via === "sdk-server-info" ? `[static read] version sourced from package.json is carried by the SDK server construction (new McpServer({ name, version })) at ${present.anchor}; the SDK serves it on server/discover and initialize. Declared residual: whether that SDK transport is reachable pre-auth is not readable statically \u2014 run --mode=dynamic for the served-value half (Server Rule 16)` : `[static read] version exposed pre-auth at ${present.anchor}, sourced from package.json (Server Rule 16)`
|
|
4931
5984
|
};
|
|
4932
5985
|
}
|
|
4933
5986
|
const decisive = surfaces.find(
|
|
@@ -5041,14 +6094,14 @@ var d16 = {
|
|
|
5041
6094
|
};
|
|
5042
6095
|
function readPackageVersion(root) {
|
|
5043
6096
|
try {
|
|
5044
|
-
let dir = statSync4(root).isDirectory() ? root :
|
|
6097
|
+
let dir = statSync4(root).isDirectory() ? root : dirname6(root);
|
|
5045
6098
|
for (let depth = 0; depth < 8; depth++) {
|
|
5046
6099
|
const candidate = join10(dir, "package.json");
|
|
5047
6100
|
if (existsSync10(candidate)) {
|
|
5048
6101
|
const pkg = JSON.parse(readFileSync8(candidate, "utf8"));
|
|
5049
6102
|
return typeof pkg.version === "string" ? pkg.version : null;
|
|
5050
6103
|
}
|
|
5051
|
-
const parent =
|
|
6104
|
+
const parent = dirname6(dir);
|
|
5052
6105
|
if (parent === dir) break;
|
|
5053
6106
|
dir = parent;
|
|
5054
6107
|
}
|
|
@@ -5060,7 +6113,7 @@ function readPackageVersion(root) {
|
|
|
5060
6113
|
|
|
5061
6114
|
// src/detectors/dAppI18n.ts
|
|
5062
6115
|
import { existsSync as existsSync11, readFileSync as readFileSync9 } from "fs";
|
|
5063
|
-
import { dirname as
|
|
6116
|
+
import { dirname as dirname7, join as join11 } from "path";
|
|
5064
6117
|
function flattenKeys(obj, prefix = "") {
|
|
5065
6118
|
if (obj === null || typeof obj !== "object" || Array.isArray(obj)) {
|
|
5066
6119
|
return prefix ? [prefix] : [];
|
|
@@ -5107,7 +6160,7 @@ var dAppI18n = {
|
|
|
5107
6160
|
`DAppI18n key-diff control FAILED: detectsGap=${control.detectsGap} detectsComplete=${control.detectsComplete} \u2014 matcher broken, verdict untrustworthy`
|
|
5108
6161
|
);
|
|
5109
6162
|
}
|
|
5110
|
-
const root = tree.packageJson ?
|
|
6163
|
+
const root = tree.packageJson ? dirname7(tree.packageJson.path) : tree.root;
|
|
5111
6164
|
const i18nDir = join11(root, "i18n");
|
|
5112
6165
|
const enPath = join11(i18nDir, "en.json");
|
|
5113
6166
|
const frPath = join11(i18nDir, "fr.json");
|
|
@@ -5980,7 +7033,7 @@ var dExt4 = {
|
|
|
5980
7033
|
|
|
5981
7034
|
// src/detectors/dExt5.ts
|
|
5982
7035
|
import { existsSync as existsSync12 } from "fs";
|
|
5983
|
-
import { dirname as
|
|
7036
|
+
import { dirname as dirname8, join as join14 } from "path";
|
|
5984
7037
|
function nonEmptyString(v) {
|
|
5985
7038
|
return typeof v === "string" && v.trim().length > 0;
|
|
5986
7039
|
}
|
|
@@ -6017,7 +7070,7 @@ var dExt5 = {
|
|
|
6017
7070
|
}
|
|
6018
7071
|
for (const manifestPath2 of manifests) {
|
|
6019
7072
|
const shortPath = manifestPath2.split("/").slice(-2).join("/");
|
|
6020
|
-
const root =
|
|
7073
|
+
const root = dirname8(manifestPath2);
|
|
6021
7074
|
const cannotRead = (reason) => ({
|
|
6022
7075
|
id: this.id,
|
|
6023
7076
|
anchor: shortPath,
|
|
@@ -6084,7 +7137,7 @@ var dExt5 = {
|
|
|
6084
7137
|
|
|
6085
7138
|
// src/detectors/dExt7.ts
|
|
6086
7139
|
import { existsSync as existsSync13 } from "fs";
|
|
6087
|
-
import { dirname as
|
|
7140
|
+
import { dirname as dirname9, join as join15 } from "path";
|
|
6088
7141
|
var ABSENCE_FLAG_FIELDS = [
|
|
6089
7142
|
"extensionAbsent",
|
|
6090
7143
|
"extension_absent",
|
|
@@ -6148,7 +7201,7 @@ var dExt7 = {
|
|
|
6148
7201
|
const unreadable = [];
|
|
6149
7202
|
const decided = (() => {
|
|
6150
7203
|
for (const manifestPath2 of manifests) {
|
|
6151
|
-
const extDir =
|
|
7204
|
+
const extDir = dirname9(manifestPath2);
|
|
6152
7205
|
const extLabel = extDir.split("/").pop() ?? extDir;
|
|
6153
7206
|
const evalsPath = join15(extDir, "evals", "evals.json");
|
|
6154
7207
|
const shortPath = `${extLabel}/evals/evals.json`;
|
|
@@ -6217,7 +7270,7 @@ var dExt7 = {
|
|
|
6217
7270
|
|
|
6218
7271
|
// src/detectors/dExt8.ts
|
|
6219
7272
|
import { existsSync as existsSync14 } from "fs";
|
|
6220
|
-
import { dirname as
|
|
7273
|
+
import { dirname as dirname10, join as join16 } from "path";
|
|
6221
7274
|
var SEMVER_RE2 = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/;
|
|
6222
7275
|
var POSITIVE_CONTROL5 = "1.2.3-beta.1+build.7";
|
|
6223
7276
|
function dExt8PositiveControl() {
|
|
@@ -6249,7 +7302,7 @@ var dExt8 = {
|
|
|
6249
7302
|
let finding;
|
|
6250
7303
|
let judgedConforming = 0;
|
|
6251
7304
|
for (const manifestPath2 of manifests) {
|
|
6252
|
-
const extDir =
|
|
7305
|
+
const extDir = dirname10(manifestPath2);
|
|
6253
7306
|
const extLabel = extDir.split("/").pop() ?? extDir;
|
|
6254
7307
|
const pkgPath = join16(extDir, "package.json");
|
|
6255
7308
|
const shortPkgPath = `${extLabel}/package.json`;
|
|
@@ -6805,6 +7858,50 @@ function collectLogCalls(file) {
|
|
|
6805
7858
|
}
|
|
6806
7859
|
return { calls, unbalanced };
|
|
6807
7860
|
}
|
|
7861
|
+
var LITERAL_VALUE_RE = /^\s*(?:"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|`[^`$\\]*`|-?\d[\d_.]*)\s*[;,)]?\s*$/;
|
|
7862
|
+
function codeLines(tree) {
|
|
7863
|
+
const out = [];
|
|
7864
|
+
for (const file of tree.files) for (const l of file.lines) if (!l.comment) out.push(l.raw);
|
|
7865
|
+
return out;
|
|
7866
|
+
}
|
|
7867
|
+
var esc = (x) => x.replace(/[$]/g, "\\$&");
|
|
7868
|
+
function resolvesToLiteral(tree, path) {
|
|
7869
|
+
const lines = codeLines(tree);
|
|
7870
|
+
const plainLiteral = (name) => {
|
|
7871
|
+
const decl = new RegExp(`\\b(?:const|let|var)\\s+${esc(name)}\\s*(?::[^=]+)?=\\s*(.*)$`);
|
|
7872
|
+
const found = lines.map((l) => l.match(decl)).filter((m) => m !== null);
|
|
7873
|
+
if (found.length !== 1 || !LITERAL_VALUE_RE.test(found[0][1])) return false;
|
|
7874
|
+
const param = new RegExp(`[(,]\\s*${esc(name)}\\s*[:,)=?]|\\bfunction\\s+${esc(name)}\\b`);
|
|
7875
|
+
return !lines.some((l) => param.test(l));
|
|
7876
|
+
};
|
|
7877
|
+
if (/[()]/.test(path)) return false;
|
|
7878
|
+
if (!path.includes(".")) return plainLiteral(path);
|
|
7879
|
+
const prop = path.split(".").pop() ?? "";
|
|
7880
|
+
const def = new RegExp(`(?:^|[\\s{,])${esc(prop)}\\s*:\\s*([^,}\\n]+)`);
|
|
7881
|
+
const shorthand = new RegExp(`[{,]\\s*${esc(prop)}\\s*[,}]`);
|
|
7882
|
+
const defs = lines.map((l) => l.match(def)).filter((m) => m !== null);
|
|
7883
|
+
if (defs.length === 0 || lines.some((l) => shorthand.test(l))) return false;
|
|
7884
|
+
return defs.every((m) => {
|
|
7885
|
+
const v = m[1].trim();
|
|
7886
|
+
return LITERAL_VALUE_RE.test(v) || /^[A-Za-z_$][\w$]*$/.test(v) && plainLiteral(v);
|
|
7887
|
+
});
|
|
7888
|
+
}
|
|
7889
|
+
function referenceHits(tree, refs) {
|
|
7890
|
+
const out = [];
|
|
7891
|
+
for (const m of refs.matchAll(/[A-Za-z_$][\w$]*(?:\??\.[A-Za-z_$][\w$]*)*\s*\(?/g)) {
|
|
7892
|
+
const isCall = m[0].trimEnd().endsWith("(");
|
|
7893
|
+
const path = m[0].replace(/\s*\(?$/, "").replace(/\?/g, "");
|
|
7894
|
+
const form = SECRET_RE.test(path) ? "A" : PII_RE.test(path) ? "C" : INTERNAL_RE.test(path) ? "D" : null;
|
|
7895
|
+
if (form) out.push({ form, path, resolved: !isCall && resolvesToLiteral(tree, path) });
|
|
7896
|
+
}
|
|
7897
|
+
return out;
|
|
7898
|
+
}
|
|
7899
|
+
var FORM_TEXT = {
|
|
7900
|
+
A: "log call references a secret-like identifier -> never log secrets; redact before logging",
|
|
7901
|
+
C: "log call references a PII-shaped identifier -> never log PII; redact before logging",
|
|
7902
|
+
D: "log call references an internal-infrastructure identifier -> never log internal system detail; redact before logging",
|
|
7903
|
+
B: "raw error.stack serialized into a client-facing response -> return an McpError message, never the raw stack"
|
|
7904
|
+
};
|
|
6808
7905
|
var PIN10 = "a74a042b53c71a095ab88b3073bd47728eeda814";
|
|
6809
7906
|
var dServer6 = {
|
|
6810
7907
|
id: "DServer6 secret-hygiene",
|
|
@@ -6832,8 +7929,15 @@ var dServer6 = {
|
|
|
6832
7929
|
`DServer6 stack control FAILED: /${controls.stack.pattern}/ accept(\`${STACK_POSITIVE}\`)=${controls.stack.positive} reject(\`${STACK_NEGATIVE}\`)=${!controls.stack.negative} \u2014 matcher broken, verdict untrustworthy`
|
|
6833
7930
|
);
|
|
6834
7931
|
}
|
|
7932
|
+
if (!LITERAL_VALUE_RE.test('"0.0.0.0";') || LITERAL_VALUE_RE.test("os.hostname();")) {
|
|
7933
|
+
throw new Error(
|
|
7934
|
+
"DServer6 literal-value control FAILED: must accept a string literal and reject a call \u2014 verdict untrustworthy"
|
|
7935
|
+
);
|
|
7936
|
+
}
|
|
6835
7937
|
const unreadable = [];
|
|
6836
7938
|
const decided = (() => {
|
|
7939
|
+
const findings = [];
|
|
7940
|
+
const resolved = [];
|
|
6837
7941
|
for (const file of tree.files) {
|
|
6838
7942
|
const { calls, unbalanced } = collectLogCalls(file);
|
|
6839
7943
|
for (const anchor of unbalanced) {
|
|
@@ -6850,30 +7954,20 @@ var dServer6 = {
|
|
|
6850
7954
|
});
|
|
6851
7955
|
continue;
|
|
6852
7956
|
}
|
|
6853
|
-
const
|
|
6854
|
-
|
|
6855
|
-
|
|
6856
|
-
|
|
6857
|
-
|
|
6858
|
-
|
|
6859
|
-
|
|
6860
|
-
|
|
6861
|
-
|
|
6862
|
-
|
|
6863
|
-
|
|
6864
|
-
|
|
6865
|
-
|
|
6866
|
-
|
|
6867
|
-
verdict: "Form C \u2014 log call references a PII-shaped identifier (positive-control OK) -> never log PII; redact before logging (C-6)"
|
|
6868
|
-
};
|
|
6869
|
-
}
|
|
6870
|
-
if (INTERNAL_RE.test(refs)) {
|
|
6871
|
-
return {
|
|
6872
|
-
id: this.id,
|
|
6873
|
-
anchor: call.anchor,
|
|
6874
|
-
state: "non-conforming",
|
|
6875
|
-
verdict: "Form D \u2014 log call references an internal-infrastructure identifier (positive-control OK) -> never log internal system detail; redact before logging (C-6)"
|
|
6876
|
-
};
|
|
7957
|
+
const hits = referenceHits(tree, referencedIdentifiers(call.argText));
|
|
7958
|
+
for (const form of ["A", "C", "D"]) {
|
|
7959
|
+
const mine = hits.filter((h) => h.form === form);
|
|
7960
|
+
if (mine.length === 0) continue;
|
|
7961
|
+
const open = mine.filter((h) => !h.resolved);
|
|
7962
|
+
if (open.length === 0) {
|
|
7963
|
+
resolved.push(`${call.anchor} (${mine.map((h) => h.path).join(", ")})`);
|
|
7964
|
+
continue;
|
|
7965
|
+
}
|
|
7966
|
+
findings.push({
|
|
7967
|
+
site: call.anchor,
|
|
7968
|
+
form,
|
|
7969
|
+
note: `identifier-name match, value unresolved: ${open.map((h) => h.path).join(", ")}`
|
|
7970
|
+
});
|
|
6877
7971
|
}
|
|
6878
7972
|
}
|
|
6879
7973
|
}
|
|
@@ -6881,20 +7975,29 @@ var dServer6 = {
|
|
|
6881
7975
|
for (const line of file.lines) {
|
|
6882
7976
|
if (line.comment) continue;
|
|
6883
7977
|
if (STACK_RE.test(line.raw) && CLIENT_SINK_RE.test(line.raw)) {
|
|
6884
|
-
|
|
6885
|
-
|
|
6886
|
-
|
|
6887
|
-
|
|
6888
|
-
|
|
6889
|
-
};
|
|
7978
|
+
findings.push({
|
|
7979
|
+
site: `${file.rel}:${line.n}`,
|
|
7980
|
+
form: "B",
|
|
7981
|
+
note: "client-facing stack"
|
|
7982
|
+
});
|
|
6890
7983
|
}
|
|
6891
7984
|
}
|
|
6892
7985
|
}
|
|
7986
|
+
if (findings.length > 0) {
|
|
7987
|
+
const forms = [...new Set(findings.map((f) => f.form))].sort();
|
|
7988
|
+
const sites = findings.map((f) => `${f.site} [Form ${f.form}, ${f.note}]`).join("; ");
|
|
7989
|
+
return {
|
|
7990
|
+
id: this.id,
|
|
7991
|
+
anchor: findings[0].site,
|
|
7992
|
+
state: "non-conforming",
|
|
7993
|
+
verdict: `${findings.length} site(s) (positive-control OK): ${sites}. ${forms.map((f) => `Form ${f} \u2014 ${FORM_TEXT[f]}`).join("; ")} (C-6)${resolved.length > 0 ? `; not counted, value is a literal constant: ${resolved.join("; ")}` : ""}`
|
|
7994
|
+
};
|
|
7995
|
+
}
|
|
6893
7996
|
return {
|
|
6894
7997
|
id: this.id,
|
|
6895
7998
|
anchor: tree.files.length > 0 ? `${tree.files[0].rel}:1` : tree.root,
|
|
6896
7999
|
state: "present",
|
|
6897
|
-
verdict:
|
|
8000
|
+
verdict: `no secret/PII/internal-detail referenced in any log call; no raw error.stack exposed to the client (all positive-controls OK)${resolved.length > 0 ? `; ${resolved.length} name match(es) resolved to a literal constant and not counted: ${resolved.join("; ")}` : ""}. Logging-sink configuration and explicit-activation-only diagnostic mode are the gate/runtime half, out of static scope (declared).`
|
|
6898
8001
|
};
|
|
6899
8002
|
})();
|
|
6900
8003
|
return settle(this.id, decided, unreadable);
|
|
@@ -7050,7 +8153,7 @@ var PRM_ROUTE_RE = /(?:app|router|server)\.(?:get|route|all)\s*\(\s*["'`][^"'`]*
|
|
|
7050
8153
|
var CLERK_RE = /clerk/i;
|
|
7051
8154
|
var AS_DISCOVERY_RE = /authorizationServers|authorization_servers|openid-configuration|oauth-authorization-server/i;
|
|
7052
8155
|
var JWKS_RE = /jwks|createRemoteJWKSet|createLocalJWKSet/i;
|
|
7053
|
-
var RS256_RE = /RS256|jwtVerify|verifyMcpBearer|verifyBearer/;
|
|
8156
|
+
var RS256_RE = /RS256|jwtVerify|verifyMcpBearer|verifyBearer|verifyMcpAccessToken/;
|
|
7054
8157
|
var AUDIENCE_RE = /audience|\baud\b/i;
|
|
7055
8158
|
var ISSUER_RE = /issuer/i;
|
|
7056
8159
|
var REUSE_RE = /@vantageos\/cloud-identity/;
|
|
@@ -7084,13 +8187,163 @@ var CONTROLS4 = [
|
|
|
7084
8187
|
negative: 'from "@vantageos/other-kit"'
|
|
7085
8188
|
}
|
|
7086
8189
|
];
|
|
8190
|
+
var DELEGATING_FNS = ["unauthorizedChallenge", "verifyMcpAccessToken"];
|
|
8191
|
+
var KIT_IMPORT_RE2 = /import\s+(?:type\s+)?\{([^}]*)\}\s*from\s*["']@vantageos\/cloud-identity(?:\/resource-server)?["']/g;
|
|
8192
|
+
function delegatingLocals(text) {
|
|
8193
|
+
const out = [];
|
|
8194
|
+
for (const m of text.matchAll(KIT_IMPORT_RE2)) {
|
|
8195
|
+
for (const part of m[1].split(",")) {
|
|
8196
|
+
const spec = part.trim().replace(/^type\s+/, "");
|
|
8197
|
+
if (!spec) continue;
|
|
8198
|
+
const [imported, local] = spec.split(/\s+as\s+/).map((x) => x.trim());
|
|
8199
|
+
if (DELEGATING_FNS.includes(imported)) out.push(local ?? imported);
|
|
8200
|
+
}
|
|
8201
|
+
}
|
|
8202
|
+
return out;
|
|
8203
|
+
}
|
|
8204
|
+
var PRM_PATH_LITERAL = ".well-known/oauth-protected-resource";
|
|
8205
|
+
function prmRouteViaConstant(tree) {
|
|
8206
|
+
for (const file of tree.files) {
|
|
8207
|
+
const text = file.lines.filter((l) => !l.comment).map((l) => l.raw).join("\n");
|
|
8208
|
+
if (!text.includes(PRM_PATH_LITERAL)) continue;
|
|
8209
|
+
const holders = /* @__PURE__ */ new Set();
|
|
8210
|
+
const decl = /\bconst\s+([A-Za-z_$][\w$]*)\s*(?::[^=\n]+)?=\s*(\[[^\]]*\]|"[^"\n]*"|'[^'\n]*'|`[^`]*`)/g;
|
|
8211
|
+
for (const m of text.matchAll(decl)) if (m[2].includes(PRM_PATH_LITERAL)) holders.add(m[1]);
|
|
8212
|
+
if (holders.size === 0) continue;
|
|
8213
|
+
const names = new Set(holders);
|
|
8214
|
+
const loop = /\bfor\s*\(\s*(?:const|let)\s+([A-Za-z_$][\w$]*)\s+of\s+([A-Za-z_$][\w$]*)\s*\)/g;
|
|
8215
|
+
for (const m of text.matchAll(loop)) if (holders.has(m[2])) names.add(m[1]);
|
|
8216
|
+
const route = /\b(?:app|router|server)\.(?:get|route|all)\s*\(\s*([A-Za-z_$][\w$]*)\s*,/g;
|
|
8217
|
+
for (const m of text.matchAll(route)) if (names.has(m[1])) return true;
|
|
8218
|
+
}
|
|
8219
|
+
return false;
|
|
8220
|
+
}
|
|
8221
|
+
var FORWARD_HEADER_LINE_RE = /WWW-Authenticate[^\n]*\.headers\b|\bheaders\s*:\s*[\w$.]+\.headers\b/i;
|
|
8222
|
+
var FORWARD_STATUS_RE = /\bstatus\s*:\s*[\w$.]+\.status\b|[\w$]+\.status\b/;
|
|
8223
|
+
function challengeForwarded(tree) {
|
|
8224
|
+
for (const file of tree.files) {
|
|
8225
|
+
const code = file.lines.filter((l) => !l.comment);
|
|
8226
|
+
const text = code.map((l) => l.raw).join("\n");
|
|
8227
|
+
if (!FORWARD_STATUS_RE.test(text)) continue;
|
|
8228
|
+
const hit = code.find((l) => FORWARD_HEADER_LINE_RE.test(l.raw));
|
|
8229
|
+
if (hit) return `${file.rel}:${hit.n}`;
|
|
8230
|
+
}
|
|
8231
|
+
return null;
|
|
8232
|
+
}
|
|
8233
|
+
function findDelegation(tree) {
|
|
8234
|
+
let call = null;
|
|
8235
|
+
for (const file of tree.files) {
|
|
8236
|
+
const text = file.lines.filter((l) => !l.comment).map((l) => l.raw).join("\n");
|
|
8237
|
+
const locals = delegatingLocals(text);
|
|
8238
|
+
if (locals.length === 0) continue;
|
|
8239
|
+
for (const line of file.lines) {
|
|
8240
|
+
if (line.comment || /^\s*(?:import\b|\}\s*from\b)/.test(line.raw)) continue;
|
|
8241
|
+
if (locals.some((n) => new RegExp(`\\b${n}\\s*\\(`).test(line.raw))) {
|
|
8242
|
+
call = `${file.rel}:${line.n}`;
|
|
8243
|
+
break;
|
|
8244
|
+
}
|
|
8245
|
+
}
|
|
8246
|
+
if (call) break;
|
|
8247
|
+
}
|
|
8248
|
+
if (!call) return null;
|
|
8249
|
+
const fwd = challengeForwarded(tree);
|
|
8250
|
+
return { anchor: fwd ?? call, forwarded: fwd !== null };
|
|
8251
|
+
}
|
|
8252
|
+
var IDENTITY_PKG = "@vantageos/cloud-identity";
|
|
8253
|
+
var DEPRECATED_AUTH_PKG = "@vantageos/mcp-auth-kit";
|
|
8254
|
+
var SHIPPED_DEP_KEYS = ["dependencies", "peerDependencies", "optionalDependencies"];
|
|
8255
|
+
var IDENTITY_FLOOR = [0, 15, 0];
|
|
8256
|
+
function lowestAdmissible(range) {
|
|
8257
|
+
const alts = range.split("||").map((a) => a.trim());
|
|
8258
|
+
let lowest = null;
|
|
8259
|
+
for (const alt of alts) {
|
|
8260
|
+
let bound = null;
|
|
8261
|
+
for (const tok of alt.split(/\s+/).filter(Boolean)) {
|
|
8262
|
+
const m = tok.match(
|
|
8263
|
+
/^(?:[\^~]|>=|>|=)?v?(\d+)(?:\.(\d+|x|\*))?(?:\.(\d+|x|\*))?(?:[-+].*)?$/
|
|
8264
|
+
);
|
|
8265
|
+
if (!m) {
|
|
8266
|
+
if (/^<=?/.test(tok)) continue;
|
|
8267
|
+
return null;
|
|
8268
|
+
}
|
|
8269
|
+
const t = [
|
|
8270
|
+
Number(m[1]),
|
|
8271
|
+
m[2] && /^\d+$/.test(m[2]) ? Number(m[2]) : 0,
|
|
8272
|
+
m[3] && /^\d+$/.test(m[3]) ? Number(m[3]) : 0
|
|
8273
|
+
];
|
|
8274
|
+
if (!bound || cmp(t, bound) > 0) bound = t;
|
|
8275
|
+
}
|
|
8276
|
+
if (!bound) return null;
|
|
8277
|
+
if (!lowest || cmp(bound, lowest) < 0) lowest = bound;
|
|
8278
|
+
}
|
|
8279
|
+
return lowest;
|
|
8280
|
+
}
|
|
8281
|
+
function cmp(a, b) {
|
|
8282
|
+
for (let i = 0; i < 3; i++) if (a[i] !== b[i]) return a[i] - b[i];
|
|
8283
|
+
return 0;
|
|
8284
|
+
}
|
|
8285
|
+
function shippedDep(pkg, name) {
|
|
8286
|
+
for (const key of SHIPPED_DEP_KEYS) {
|
|
8287
|
+
const deps = pkg[key];
|
|
8288
|
+
if (deps && typeof deps === "object") {
|
|
8289
|
+
const v = deps[name];
|
|
8290
|
+
if (typeof v === "string") return { key, range: v };
|
|
8291
|
+
}
|
|
8292
|
+
}
|
|
8293
|
+
return null;
|
|
8294
|
+
}
|
|
8295
|
+
function packageRule(tree, id) {
|
|
8296
|
+
const pj = tree.packageJson;
|
|
8297
|
+
if (!pj || pj.unreadable) return null;
|
|
8298
|
+
const anchor = pj.path.split("/").slice(-2).join("/");
|
|
8299
|
+
const kit = shippedDep(pj.json, DEPRECATED_AUTH_PKG);
|
|
8300
|
+
if (kit) {
|
|
8301
|
+
return {
|
|
8302
|
+
id,
|
|
8303
|
+
anchor,
|
|
8304
|
+
state: "non-conforming",
|
|
8305
|
+
verdict: `package depends on ${DEPRECATED_AUTH_PKG} (${kit.key}: "${kit.range}") \u2014 deprecated -> remove it; ${IDENTITY_PKG} >= 0.15.0 is the only identity package (Server Rule 14 / S-5)`
|
|
8306
|
+
};
|
|
8307
|
+
}
|
|
8308
|
+
const ci = shippedDep(pj.json, IDENTITY_PKG);
|
|
8309
|
+
if (!ci) return null;
|
|
8310
|
+
const floor = lowestAdmissible(ci.range);
|
|
8311
|
+
if (!floor) {
|
|
8312
|
+
const reason = `${IDENTITY_PKG} is declared as "${ci.range}" (${ci.key}), whose lowest admissible version cannot be read \u2014 the >= 0.15.0 floor is undecidable (Server Rule 14 / S-5)`;
|
|
8313
|
+
return { id, anchor, state: "could-not-judge", reason, verdict: `could not judge: ${reason}` };
|
|
8314
|
+
}
|
|
8315
|
+
if (cmp(floor, IDENTITY_FLOOR) < 0) {
|
|
8316
|
+
return {
|
|
8317
|
+
id,
|
|
8318
|
+
anchor,
|
|
8319
|
+
state: "non-conforming",
|
|
8320
|
+
verdict: `${IDENTITY_PKG} is declared as "${ci.range}" (${ci.key}), admitting ${floor.join(".")} \u2014 below the 0.15.0 floor of the resource-server entry point -> raise the range to >= 0.15.0 (Server Rule 14 / S-5)`
|
|
8321
|
+
};
|
|
8322
|
+
}
|
|
8323
|
+
return null;
|
|
8324
|
+
}
|
|
7087
8325
|
function dServer14PositiveControls() {
|
|
7088
|
-
|
|
8326
|
+
const base = CONTROLS4.map((c) => ({
|
|
7089
8327
|
key: c.key,
|
|
7090
8328
|
pattern: c.re.source,
|
|
7091
8329
|
positive: c.re.test(c.positive),
|
|
7092
8330
|
negative: c.re.test(c.negative)
|
|
7093
8331
|
}));
|
|
8332
|
+
base.push({
|
|
8333
|
+
key: "dependency-floor",
|
|
8334
|
+
pattern: "lowestAdmissible",
|
|
8335
|
+
positive: lowestAdmissible("^0.14.2")?.join(".") === "0.14.2" && lowestAdmissible(">=0.15.0 <1")?.join(".") === "0.15.0",
|
|
8336
|
+
negative: lowestAdmissible("latest") !== null
|
|
8337
|
+
});
|
|
8338
|
+
base.push({
|
|
8339
|
+
key: "delegation",
|
|
8340
|
+
pattern: KIT_IMPORT_RE2.source,
|
|
8341
|
+
positive: delegatingLocals(
|
|
8342
|
+
'import {\n verifyMcpAccessToken as v,\n} from "@vantageos/cloud-identity/resource-server";'
|
|
8343
|
+
).join() === "v",
|
|
8344
|
+
negative: delegatingLocals('import { verifyMcpAccessToken } from "some-other-kit";').length > 0
|
|
8345
|
+
});
|
|
8346
|
+
return base;
|
|
7094
8347
|
}
|
|
7095
8348
|
function codeBlob(tree) {
|
|
7096
8349
|
const parts = [];
|
|
@@ -7133,6 +8386,8 @@ var dServer14 = {
|
|
|
7133
8386
|
);
|
|
7134
8387
|
}
|
|
7135
8388
|
}
|
|
8389
|
+
const pkgVerdict = packageRule(tree, this.id);
|
|
8390
|
+
if (pkgVerdict) return pkgVerdict;
|
|
7136
8391
|
const blob = codeBlob(tree);
|
|
7137
8392
|
const surfacePresent = WWW_AUTH_RE.test(blob) || PRM_ROUTE_RE.test(blob) || JWKS_RE.test(blob) || CLERK_RE.test(blob) || REUSE_RE.test(blob);
|
|
7138
8393
|
if (!surfacePresent) {
|
|
@@ -7143,6 +8398,17 @@ var dServer14 = {
|
|
|
7143
8398
|
verdict: "no OAuth 2.1 resource-server surface detected (n/a for Server Rule 14)"
|
|
7144
8399
|
};
|
|
7145
8400
|
}
|
|
8401
|
+
const delegation = findDelegation(tree);
|
|
8402
|
+
if (delegation && !delegation.forwarded && !CHALLENGE_PARAM_RE.test(blob)) {
|
|
8403
|
+
const reason = `${delegation.anchor} calls a challenge-returning function of @vantageos/cloud-identity but no \`<result>.status\` / \`<result>.headers\` forwarding is readable in that file \u2014 whether the 401 + WWW-Authenticate reaches the client is unverifiable statically (Server Rule 14, Element 1)`;
|
|
8404
|
+
return {
|
|
8405
|
+
id: this.id,
|
|
8406
|
+
anchor: delegation.anchor,
|
|
8407
|
+
state: "could-not-judge",
|
|
8408
|
+
reason,
|
|
8409
|
+
verdict: `could not judge: ${reason}`
|
|
8410
|
+
};
|
|
8411
|
+
}
|
|
7146
8412
|
if (!CHALLENGE_PARAM_RE.test(blob)) {
|
|
7147
8413
|
const computedAnchor = firstAnchor(tree, COMPUTED_CHALLENGE_RE);
|
|
7148
8414
|
if (computedAnchor) {
|
|
@@ -7159,13 +8425,13 @@ var dServer14 = {
|
|
|
7159
8425
|
const elements = [
|
|
7160
8426
|
{
|
|
7161
8427
|
name: "Element 1 CHALLENGE (401 + WWW-Authenticate: Bearer \u2026 resource_metadata=\u2026)",
|
|
7162
|
-
present: WWW_AUTH_RE.test(blob) && CHALLENGE_PARAM_RE.test(blob) && STATUS_401_RE.test(blob) && BEARER_RE.test(blob),
|
|
8428
|
+
present: Boolean(delegation?.forwarded) || WWW_AUTH_RE.test(blob) && CHALLENGE_PARAM_RE.test(blob) && STATUS_401_RE.test(blob) && BEARER_RE.test(blob),
|
|
7163
8429
|
probe: WWW_AUTH_RE,
|
|
7164
8430
|
fix: "answer 401 with WWW-Authenticate: Bearer \u2026 resource_metadata=\u2026 (RFC 9728)"
|
|
7165
8431
|
},
|
|
7166
8432
|
{
|
|
7167
8433
|
name: "Element 2 PRM ROUTE (/.well-known/oauth-protected-resource)",
|
|
7168
|
-
present: PRM_ROUTE_RE.test(blob),
|
|
8434
|
+
present: PRM_ROUTE_RE.test(blob) || prmRouteViaConstant(tree),
|
|
7169
8435
|
probe: PRM_ROUTE_RE,
|
|
7170
8436
|
fix: "serve /.well-known/oauth-protected-resource (Protected Resource Metadata, RFC 9728)"
|
|
7171
8437
|
},
|
|
@@ -7215,6 +8481,436 @@ var dServer14 = {
|
|
|
7215
8481
|
}
|
|
7216
8482
|
};
|
|
7217
8483
|
|
|
8484
|
+
// src/detectors/s28-claim-forwarded.ts
|
|
8485
|
+
import { existsSync as existsSync15, readFileSync as readFileSync11, readdirSync as readdirSync7, statSync as statSync8 } from "fs";
|
|
8486
|
+
import { dirname as dirname11, join as join18, relative as relative5 } from "path";
|
|
8487
|
+
var PIN11 = "bacc54a53d5f6f9f20e5254908e2b46807902faa";
|
|
8488
|
+
var CLAIM_TOKEN = /\bverified(?:Org|Person)\b/;
|
|
8489
|
+
var ANY_CLAIM = /\bverified(?:Actor|Org|Person)\b/g;
|
|
8490
|
+
var CALL_RE = /\b([A-Za-z_$][\w$]*)\s*\.\s*(query|mutation|action)\s*\(/g;
|
|
8491
|
+
var GUARD_RE = /\b(?:unresolved\w*Denial|requireVerified\w+|requireClaim\w*)\s*\(|verified-org-unresolved/;
|
|
8492
|
+
var FLEET_WIDE = /fleet-wide:\s*\S[^\n]{7,}/;
|
|
8493
|
+
var SCOPE_MARKERS = /\bwithOrgScope\b|\brequireAuthenticatedCaller\b|\bresolveInboxReader\b|\bresolveVerified\w*\b|\brequireScope\b|\bgetUserIdentity\b|\bisMaster\b|\bscope\b/;
|
|
8494
|
+
var ACTOR_SET_RE = /\bconst\s+(VERIFIED_\w*DOORS)\b[^=]*=\s*new\s+Set\s*(?:<[^>]*>)?\s*\(\s*\[([\s\S]*?)\]\s*\)/;
|
|
8495
|
+
function masked(file) {
|
|
8496
|
+
const out = [];
|
|
8497
|
+
for (const line of file.lines) {
|
|
8498
|
+
let row = "";
|
|
8499
|
+
for (let c = 0; c < line.raw.length; c++) {
|
|
8500
|
+
const ch = line.raw[c];
|
|
8501
|
+
row += ch === " " || ch === " " || ch === "\r" || isCodeAt(file, line.n, c) ? ch : " ";
|
|
8502
|
+
}
|
|
8503
|
+
out.push(row);
|
|
8504
|
+
}
|
|
8505
|
+
return out.join("\n");
|
|
8506
|
+
}
|
|
8507
|
+
function rawText(file) {
|
|
8508
|
+
return file.lines.map((l) => l.raw).join("\n");
|
|
8509
|
+
}
|
|
8510
|
+
function lineAt(text, offset) {
|
|
8511
|
+
let n = 1;
|
|
8512
|
+
for (let i = 0; i < offset && i < text.length; i++) if (text.charCodeAt(i) === 10) n++;
|
|
8513
|
+
return n;
|
|
8514
|
+
}
|
|
8515
|
+
var OPEN = { "(": ")", "[": "]", "{": "}" };
|
|
8516
|
+
function matchClose(m, open) {
|
|
8517
|
+
const stack = [];
|
|
8518
|
+
for (let i = open; i < m.length; i++) {
|
|
8519
|
+
const ch = m[i];
|
|
8520
|
+
if (OPEN[ch] !== void 0) stack.push(OPEN[ch]);
|
|
8521
|
+
else if (ch === ")" || ch === "]" || ch === "}") {
|
|
8522
|
+
if (stack.pop() !== ch) return -1;
|
|
8523
|
+
if (stack.length === 0) return i;
|
|
8524
|
+
}
|
|
8525
|
+
}
|
|
8526
|
+
return -1;
|
|
8527
|
+
}
|
|
8528
|
+
function splitTop(m, from, to) {
|
|
8529
|
+
const parts = [];
|
|
8530
|
+
let depth = 0;
|
|
8531
|
+
let start = from;
|
|
8532
|
+
for (let i = from; i < to; i++) {
|
|
8533
|
+
const ch = m[i];
|
|
8534
|
+
if (OPEN[ch] !== void 0) depth++;
|
|
8535
|
+
else if (ch === ")" || ch === "]" || ch === "}") depth--;
|
|
8536
|
+
else if (ch === "," && depth === 0) {
|
|
8537
|
+
parts.push([start, i]);
|
|
8538
|
+
start = i + 1;
|
|
8539
|
+
}
|
|
8540
|
+
}
|
|
8541
|
+
if (start < to && m.slice(start, to).trim() !== "") parts.push([start, to]);
|
|
8542
|
+
return parts;
|
|
8543
|
+
}
|
|
8544
|
+
function listTs(dir, out = []) {
|
|
8545
|
+
let names;
|
|
8546
|
+
try {
|
|
8547
|
+
names = readdirSync7(dir);
|
|
8548
|
+
} catch {
|
|
8549
|
+
return out;
|
|
8550
|
+
}
|
|
8551
|
+
for (const n of names) {
|
|
8552
|
+
if (n === "node_modules" || n === "_generated" || n === "__tests__") continue;
|
|
8553
|
+
const p = join18(dir, n);
|
|
8554
|
+
let st;
|
|
8555
|
+
try {
|
|
8556
|
+
st = statSync8(p);
|
|
8557
|
+
} catch {
|
|
8558
|
+
continue;
|
|
8559
|
+
}
|
|
8560
|
+
if (st.isDirectory()) listTs(p, out);
|
|
8561
|
+
else if (n.endsWith(".ts") && !n.endsWith(".test.ts") && !n.endsWith(".d.ts")) out.push(p);
|
|
8562
|
+
}
|
|
8563
|
+
return out;
|
|
8564
|
+
}
|
|
8565
|
+
function discoverBackend(root) {
|
|
8566
|
+
let dir = root;
|
|
8567
|
+
try {
|
|
8568
|
+
if (!statSync8(root).isDirectory()) dir = dirname11(root);
|
|
8569
|
+
} catch {
|
|
8570
|
+
return void 0;
|
|
8571
|
+
}
|
|
8572
|
+
for (let up = 0; up < 5; up++) {
|
|
8573
|
+
const cand = join18(dir, "convex");
|
|
8574
|
+
if (existsSync15(join18(cand, "schema.ts"))) return cand;
|
|
8575
|
+
const parent = dirname11(dir);
|
|
8576
|
+
if (parent === dir) break;
|
|
8577
|
+
dir = parent;
|
|
8578
|
+
}
|
|
8579
|
+
return void 0;
|
|
8580
|
+
}
|
|
8581
|
+
function readDoors(backendRoot) {
|
|
8582
|
+
const doors = /* @__PURE__ */ new Map();
|
|
8583
|
+
for (const path of listTs(backendRoot)) {
|
|
8584
|
+
let text;
|
|
8585
|
+
try {
|
|
8586
|
+
text = readFileSync11(path, "utf8");
|
|
8587
|
+
} catch {
|
|
8588
|
+
continue;
|
|
8589
|
+
}
|
|
8590
|
+
const file = {
|
|
8591
|
+
path,
|
|
8592
|
+
rel: relative5(backendRoot, path),
|
|
8593
|
+
lines: text.split("\n").map((raw, i) => ({ n: i + 1, raw, comment: false }))
|
|
8594
|
+
};
|
|
8595
|
+
const m = masked(file);
|
|
8596
|
+
const module = relative5(backendRoot, path).replace(/\.ts$/, "").split("\\").join("/");
|
|
8597
|
+
const re = /\bexport\s+const\s+([A-Za-z_$][\w$]*)\s*=\s*(?:query|mutation|action)\s*\(/g;
|
|
8598
|
+
for (let hit = re.exec(m); hit; hit = re.exec(m)) {
|
|
8599
|
+
const open = hit.index + hit[0].length - 1;
|
|
8600
|
+
const close = matchClose(m, open);
|
|
8601
|
+
if (close < 0) continue;
|
|
8602
|
+
let start = hit.index;
|
|
8603
|
+
const lines = text.slice(0, start).split("\n");
|
|
8604
|
+
let k = lines.length - 2;
|
|
8605
|
+
while (k >= 0 && (lines[k].trim() === "" || /^\s*(\/\/|\/\*|\*)/.test(lines[k])) && lines.length - 2 - k < 60)
|
|
8606
|
+
k--;
|
|
8607
|
+
start = lines.slice(0, k + 1).join("\n").length;
|
|
8608
|
+
const region = text.slice(start, close + 1);
|
|
8609
|
+
const mRegion = m.slice(hit.index, close + 1);
|
|
8610
|
+
const claims = [];
|
|
8611
|
+
const am = /\bargs\s*:\s*/.exec(mRegion);
|
|
8612
|
+
if (am) {
|
|
8613
|
+
const at = hit.index + am.index + am[0].length;
|
|
8614
|
+
let argsText = "";
|
|
8615
|
+
if (m[at] === "{") argsText = text.slice(at, matchClose(m, at) + 1);
|
|
8616
|
+
else {
|
|
8617
|
+
const id = /^[A-Za-z_$][\w$]*/.exec(m.slice(at));
|
|
8618
|
+
if (id) {
|
|
8619
|
+
const dm = new RegExp(`\\bconst\\s+${id[0]}\\b[^=]*=\\s*`).exec(m);
|
|
8620
|
+
if (dm) {
|
|
8621
|
+
let p = dm.index + dm[0].length;
|
|
8622
|
+
if (m.slice(p).startsWith("v.object(")) p += "v.object(".length;
|
|
8623
|
+
if (m[p] === "{") argsText = text.slice(p, matchClose(m, p) + 1);
|
|
8624
|
+
}
|
|
8625
|
+
}
|
|
8626
|
+
}
|
|
8627
|
+
for (const c of argsText.match(ANY_CLAIM) ?? []) if (!claims.includes(c)) claims.push(c);
|
|
8628
|
+
}
|
|
8629
|
+
let exit;
|
|
8630
|
+
if (/\brequireServiceAccount\b/.test(mRegion))
|
|
8631
|
+
exit = "service-account-only (requireServiceAccount)";
|
|
8632
|
+
else if (/\brequireResolvedCaller\s*\([^)]*masterOnly[^)]*mcpBoundOnly|\brequireResolvedCaller\s*\([^)]*mcpBoundOnly[^)]*masterOnly/s.test(
|
|
8633
|
+
mRegion
|
|
8634
|
+
))
|
|
8635
|
+
exit = "service-account-only (requireResolvedCaller masterOnly+mcpBoundOnly)";
|
|
8636
|
+
else {
|
|
8637
|
+
const fw = FLEET_WIDE.exec(region);
|
|
8638
|
+
if (fw) exit = `fleet-wide declaration (${fw[0].slice(11).trim().slice(0, 80)})`;
|
|
8639
|
+
}
|
|
8640
|
+
doors.set(`${module}:${hit[1]}`, {
|
|
8641
|
+
claims,
|
|
8642
|
+
exit,
|
|
8643
|
+
resolvesCaller: SCOPE_MARKERS.test(mRegion)
|
|
8644
|
+
});
|
|
8645
|
+
}
|
|
8646
|
+
}
|
|
8647
|
+
return doors;
|
|
8648
|
+
}
|
|
8649
|
+
function definitionOf(name, files) {
|
|
8650
|
+
const esc2 = name.replace(/\$/g, "\\$");
|
|
8651
|
+
const re = new RegExp(`(?:\\b(?:const|let|var)\\s+${esc2}\\b|\\bfunction\\s+${esc2}\\b)`);
|
|
8652
|
+
for (const f of files) {
|
|
8653
|
+
const hit = re.exec(f.m);
|
|
8654
|
+
if (!hit) continue;
|
|
8655
|
+
let depth = 0;
|
|
8656
|
+
for (let i = hit.index + hit[0].length; i < f.m.length; i++) {
|
|
8657
|
+
const ch = f.m[i];
|
|
8658
|
+
if (OPEN[ch] !== void 0) {
|
|
8659
|
+
depth++;
|
|
8660
|
+
} else if (ch === ")" || ch === "]" || ch === "}") {
|
|
8661
|
+
depth--;
|
|
8662
|
+
if (depth === 0 && ch === "}" && /\bfunction\b/.test(hit[0]))
|
|
8663
|
+
return { mask: f.m.slice(hit.index, i + 1) };
|
|
8664
|
+
} else if (ch === ";" && depth === 0 && !/\bfunction\b/.test(hit[0])) {
|
|
8665
|
+
return { mask: f.m.slice(hit.index, i + 1) };
|
|
8666
|
+
}
|
|
8667
|
+
if (depth < 0) break;
|
|
8668
|
+
}
|
|
8669
|
+
return { mask: f.m.slice(hit.index, Math.min(f.m.length, hit.index + 1500)) };
|
|
8670
|
+
}
|
|
8671
|
+
return void 0;
|
|
8672
|
+
}
|
|
8673
|
+
function readHelper(name, files, seen = /* @__PURE__ */ new Set(), depth = 0) {
|
|
8674
|
+
if (seen.has(name) || depth > 3) return { forwards: false, optional: false };
|
|
8675
|
+
seen.add(name);
|
|
8676
|
+
const def = definitionOf(name, files);
|
|
8677
|
+
if (!def) return { forwards: false, optional: false };
|
|
8678
|
+
let forwards = CLAIM_TOKEN.test(def.mask);
|
|
8679
|
+
const optional = /:\s*\{\s*\}|return\s*\{\s*\}|\?\?\s*\{\s*\}/.test(def.mask);
|
|
8680
|
+
if (!forwards) {
|
|
8681
|
+
for (const c of def.mask.matchAll(/\b([A-Za-z_$][\w$]*)\s*\(/g)) {
|
|
8682
|
+
const sub = readHelper(c[1], files, seen, depth + 1);
|
|
8683
|
+
if (sub.forwards) {
|
|
8684
|
+
forwards = true;
|
|
8685
|
+
break;
|
|
8686
|
+
}
|
|
8687
|
+
}
|
|
8688
|
+
}
|
|
8689
|
+
return { forwards, optional };
|
|
8690
|
+
}
|
|
8691
|
+
function toolNameBefore(text, offset) {
|
|
8692
|
+
const head = text.slice(0, offset);
|
|
8693
|
+
const open = Math.max(head.lastIndexOf("defineTool("), head.lastIndexOf("registerTool("));
|
|
8694
|
+
if (open >= 0) {
|
|
8695
|
+
const m = /^\s*"([a-z][a-z0-9_]*)",?\s*$/m.exec(head.slice(open));
|
|
8696
|
+
if (m) return { name: m[1], start: open };
|
|
8697
|
+
const r = /registerTool\(\s*"([a-z][a-z0-9_]*)"/.exec(head.slice(open));
|
|
8698
|
+
if (r) return { name: r[1], start: open };
|
|
8699
|
+
return { name: void 0, start: open };
|
|
8700
|
+
}
|
|
8701
|
+
const lines = head.split("\n");
|
|
8702
|
+
const back = Math.max(0, lines.length - 120);
|
|
8703
|
+
return { name: void 0, start: lines.slice(0, back).join("\n").length };
|
|
8704
|
+
}
|
|
8705
|
+
function scanS28(tree) {
|
|
8706
|
+
const scan = { violate: [], conform: [], cnj: [], calls: 0, backend: void 0 };
|
|
8707
|
+
const backendRoot = tree.backendRoot ?? discoverBackend(tree.root);
|
|
8708
|
+
scan.backend = backendRoot;
|
|
8709
|
+
const doors = backendRoot ? readDoors(backendRoot) : void 0;
|
|
8710
|
+
const files = tree.files.map((f) => ({ f, text: rawText(f), m: masked(f) }));
|
|
8711
|
+
const actorDoors = /* @__PURE__ */ new Set();
|
|
8712
|
+
for (const { text } of files) {
|
|
8713
|
+
const hit = ACTOR_SET_RE.exec(text);
|
|
8714
|
+
if (hit) for (const s of hit[2].matchAll(/["'`]([\w/]+:[\w]+)["'`]/g)) actorDoors.add(s[1]);
|
|
8715
|
+
}
|
|
8716
|
+
for (const { f, text, m } of files) {
|
|
8717
|
+
for (let hit = CALL_RE.exec(m); hit; hit = CALL_RE.exec(m)) {
|
|
8718
|
+
const recv = hit[1];
|
|
8719
|
+
if (!/convex|client/i.test(recv)) continue;
|
|
8720
|
+
const open = hit.index + hit[0].length - 1;
|
|
8721
|
+
const close = matchClose(m, open);
|
|
8722
|
+
if (close < 0) continue;
|
|
8723
|
+
scan.calls++;
|
|
8724
|
+
const line = lineAt(text, hit.index);
|
|
8725
|
+
const tool = toolNameBefore(text, hit.index);
|
|
8726
|
+
const parts = splitTop(m, open + 1, close);
|
|
8727
|
+
let door;
|
|
8728
|
+
if (parts[0]) {
|
|
8729
|
+
const rawFirst = text.slice(parts[0][0], parts[0][1]);
|
|
8730
|
+
const lit = /^\s*(?:\/\/[^\n]*\n\s*)*["'`]([\w/]+:[\w]+)["'`]/.exec(rawFirst);
|
|
8731
|
+
const ref = /^\s*(?:api|internal)\.([\w]+)\.([\w]+)/.exec(rawFirst);
|
|
8732
|
+
if (lit) door = lit[1];
|
|
8733
|
+
else if (ref) door = `${ref[1]}:${ref[2]}`;
|
|
8734
|
+
}
|
|
8735
|
+
const site = (note) => ({
|
|
8736
|
+
file: f.rel,
|
|
8737
|
+
line,
|
|
8738
|
+
door: door ?? "?",
|
|
8739
|
+
tool: tool.name,
|
|
8740
|
+
note
|
|
8741
|
+
});
|
|
8742
|
+
if (door === void 0) {
|
|
8743
|
+
scan.cnj.push(
|
|
8744
|
+
site(
|
|
8745
|
+
"the door is named by an expression the scan cannot resolve (not statically resolvable)"
|
|
8746
|
+
)
|
|
8747
|
+
);
|
|
8748
|
+
continue;
|
|
8749
|
+
}
|
|
8750
|
+
const argText = parts[1] ? m.slice(parts[1][0], parts[1][1]) : "";
|
|
8751
|
+
const region = m.slice(tool.start, hit.index);
|
|
8752
|
+
const regionRaw = text.slice(tool.start, hit.index);
|
|
8753
|
+
const guard = GUARD_RE.test(region) || GUARD_RE.test(regionRaw);
|
|
8754
|
+
let direct = CLAIM_TOKEN.test(argText);
|
|
8755
|
+
let viaHelper;
|
|
8756
|
+
let virtual = argText;
|
|
8757
|
+
const bare = /^\s*([A-Za-z_$][\w$]*)\s*$/.exec(argText);
|
|
8758
|
+
if (bare && !direct) {
|
|
8759
|
+
const v = bare[1].replace(/\$/g, "\\$");
|
|
8760
|
+
const pieces = [];
|
|
8761
|
+
for (const a of region.matchAll(new RegExp(`Object\\.assign\\s*\\(\\s*${v}\\s*,`, "g"))) {
|
|
8762
|
+
const o = a.index + a[0].length - 1;
|
|
8763
|
+
const c = matchClose(region, region.lastIndexOf("(", o));
|
|
8764
|
+
pieces.push(region.slice(o, c < 0 ? o + 400 : c));
|
|
8765
|
+
}
|
|
8766
|
+
for (const a of region.matchAll(
|
|
8767
|
+
new RegExp(`\\b${v}\\.verified(?:Org|Person|Actor)\\b`, "g")
|
|
8768
|
+
))
|
|
8769
|
+
pieces.push(a[0]);
|
|
8770
|
+
const decl = new RegExp(`\\b(?:const|let)\\s+${v}\\b[^=]*=\\s*\\{`).exec(region);
|
|
8771
|
+
if (decl) {
|
|
8772
|
+
const o = decl.index + decl[0].length - 1;
|
|
8773
|
+
const c = matchClose(region, o);
|
|
8774
|
+
pieces.push(region.slice(o, c < 0 ? o + 600 : c + 1));
|
|
8775
|
+
}
|
|
8776
|
+
virtual = pieces.join("\n");
|
|
8777
|
+
direct = CLAIM_TOKEN.test(virtual);
|
|
8778
|
+
}
|
|
8779
|
+
if (!direct) {
|
|
8780
|
+
for (const sp of virtual.matchAll(/\.\.\.\s*([A-Za-z_$][\w$]*)/g)) {
|
|
8781
|
+
const h = readHelper(sp[1], files);
|
|
8782
|
+
if (h.forwards) {
|
|
8783
|
+
viaHelper = h;
|
|
8784
|
+
break;
|
|
8785
|
+
}
|
|
8786
|
+
}
|
|
8787
|
+
if (!viaHelper) {
|
|
8788
|
+
for (const sp of virtual.matchAll(/\b([A-Za-z_$][\w$]*)\s*\(\s*\)/g)) {
|
|
8789
|
+
const h = readHelper(sp[1], files);
|
|
8790
|
+
if (h.forwards) {
|
|
8791
|
+
viaHelper = h;
|
|
8792
|
+
break;
|
|
8793
|
+
}
|
|
8794
|
+
}
|
|
8795
|
+
}
|
|
8796
|
+
}
|
|
8797
|
+
if (direct) {
|
|
8798
|
+
scan.conform.push(site("forwards the verified claim in the call"));
|
|
8799
|
+
continue;
|
|
8800
|
+
}
|
|
8801
|
+
if (viaHelper) {
|
|
8802
|
+
if (viaHelper.optional && !guard) {
|
|
8803
|
+
scan.violate.push(
|
|
8804
|
+
site(
|
|
8805
|
+
"forwards its claim through an optional helper (it returns {} when the bearer holds none) and no refusal precedes the call in the handler: a bearer with no resolvable organisation is sent claimless"
|
|
8806
|
+
)
|
|
8807
|
+
);
|
|
8808
|
+
} else {
|
|
8809
|
+
scan.conform.push(
|
|
8810
|
+
site(
|
|
8811
|
+
viaHelper.optional ? "forwards the verified claim through a helper and refuses before the call when it holds none" : "forwards the verified claim through a helper"
|
|
8812
|
+
)
|
|
8813
|
+
);
|
|
8814
|
+
}
|
|
8815
|
+
continue;
|
|
8816
|
+
}
|
|
8817
|
+
if (!doors) {
|
|
8818
|
+
scan.cnj.push(
|
|
8819
|
+
site(
|
|
8820
|
+
"forwards no verified claim and no backend source was found above the scanned tree (no convex/ with a schema.ts, and no backend root given): whether the door is an exit cannot be read"
|
|
8821
|
+
)
|
|
8822
|
+
);
|
|
8823
|
+
continue;
|
|
8824
|
+
}
|
|
8825
|
+
const info = doors.get(door);
|
|
8826
|
+
if (!info) {
|
|
8827
|
+
scan.cnj.push(
|
|
8828
|
+
site(
|
|
8829
|
+
`forwards no verified claim and the door ${door} is not defined in the supplied backend (${backendRoot})`
|
|
8830
|
+
)
|
|
8831
|
+
);
|
|
8832
|
+
continue;
|
|
8833
|
+
}
|
|
8834
|
+
if (info.exit) {
|
|
8835
|
+
scan.conform.push(site(`needs no claim: the door is ${info.exit}`));
|
|
8836
|
+
continue;
|
|
8837
|
+
}
|
|
8838
|
+
if (!info.resolvesCaller) {
|
|
8839
|
+
scan.cnj.push(
|
|
8840
|
+
site(
|
|
8841
|
+
`forwards no verified claim and the door ${door} shows no caller resolution in its own source (its scope comes from a helper or a run call that this scan does not follow)`
|
|
8842
|
+
)
|
|
8843
|
+
);
|
|
8844
|
+
continue;
|
|
8845
|
+
}
|
|
8846
|
+
const accepts = info.claims.length > 0 ? `the door accepts ${info.claims.join(", ")} and the tool omits it` : "the door accepts no claim argument, so R-54 must add one before the tool can forward it";
|
|
8847
|
+
const actorNote = actorDoors.has(door) ? "; verifiedActor is added by the wrapped client only for a bearer that resolved an agent, and a bearer that resolved no agent (a seat or person token, an org-level token) is sent claimless" : "";
|
|
8848
|
+
scan.violate.push(site(`forwards no verified claim \u2014 ${accepts}${actorNote}`));
|
|
8849
|
+
}
|
|
8850
|
+
}
|
|
8851
|
+
return scan;
|
|
8852
|
+
}
|
|
8853
|
+
function fmt(s) {
|
|
8854
|
+
return `${s.file}:${s.line} ${s.door} ${s.tool ? `[${s.tool}] ` : ""}(${s.note})`;
|
|
8855
|
+
}
|
|
8856
|
+
var S28_ID = "S-28 claim-forwarded";
|
|
8857
|
+
var s28ClaimForwarded = {
|
|
8858
|
+
id: S28_ID,
|
|
8859
|
+
rule: "S-28",
|
|
8860
|
+
pin: PIN11,
|
|
8861
|
+
run(tree) {
|
|
8862
|
+
const s = scanS28(tree);
|
|
8863
|
+
const breakdown = `[conform=${s.conform.length} non-conforming=${s.violate.length} could-not-judge=${s.cnj.length}]`;
|
|
8864
|
+
const list = (xs) => xs.slice(0, 5).map(fmt).join("; ");
|
|
8865
|
+
if (s.violate.length > 0) {
|
|
8866
|
+
const first = s.violate[0];
|
|
8867
|
+
return {
|
|
8868
|
+
id: S28_ID,
|
|
8869
|
+
anchor: `${first.file}:${first.line}`,
|
|
8870
|
+
state: "non-conforming",
|
|
8871
|
+
verdict: `S-28: ${s.violate.length} backend call(s) VIOLATE (a tool forwards the verified claim of its caller to every backend door it calls, and refuses when it has none). ${breakdown} Violations: ${list(s.violate)}${s.violate.length > 5 ? `; +${s.violate.length - 5} more` : ""}.${s.cnj.length ? ` could-not-judge calls (a verdict is owed): ${list(s.cnj)}.` : ""}`
|
|
8872
|
+
};
|
|
8873
|
+
}
|
|
8874
|
+
if (s.cnj.length > 0) {
|
|
8875
|
+
const reason2 = `S-28: ${s.cnj.length} backend call(s) could not be judged \u2014 ${list(s.cnj)}. This is NOT a clean bill. ${breakdown}`;
|
|
8876
|
+
return {
|
|
8877
|
+
id: S28_ID,
|
|
8878
|
+
anchor: `${s.cnj[0].file}:${s.cnj[0].line}`,
|
|
8879
|
+
state: "could-not-judge",
|
|
8880
|
+
verdict: reason2,
|
|
8881
|
+
reason: reason2
|
|
8882
|
+
};
|
|
8883
|
+
}
|
|
8884
|
+
if (s.conform.length > 0) {
|
|
8885
|
+
return {
|
|
8886
|
+
id: S28_ID,
|
|
8887
|
+
anchor: `${s.conform[0].file}:${s.conform[0].line}`,
|
|
8888
|
+
state: "present",
|
|
8889
|
+
verdict: `S-28: ${s.conform.length} backend call(s) forward the verified claim or call a door that needs none; 0 violations. ${breakdown} Conform: ${list(s.conform)}.`
|
|
8890
|
+
};
|
|
8891
|
+
}
|
|
8892
|
+
const usesBackend = tree.files.some(
|
|
8893
|
+
(f) => f.lines.some((l) => !l.comment && /\bConvexHttpClient\b|from\s+["']convex\//.test(l.raw))
|
|
8894
|
+
);
|
|
8895
|
+
if (!usesBackend) {
|
|
8896
|
+
return {
|
|
8897
|
+
id: S28_ID,
|
|
8898
|
+
anchor: "source:no-backend-client",
|
|
8899
|
+
state: "absent",
|
|
8900
|
+
verdict: `S-28: no Convex client is imported anywhere in the ${tree.files.length} file(s) read; no backend door is called, so there is no claim to forward (rule out of subject).`
|
|
8901
|
+
};
|
|
8902
|
+
}
|
|
8903
|
+
const reason = `S-28: no applicable site \u2014 a Convex client is imported but none of the ${s.calls} <client>.query|mutation|action call(s) read names a door statically. NOT a clean bill.`;
|
|
8904
|
+
return {
|
|
8905
|
+
id: S28_ID,
|
|
8906
|
+
anchor: "source:no-applicable-site",
|
|
8907
|
+
state: "could-not-judge",
|
|
8908
|
+
verdict: reason,
|
|
8909
|
+
reason
|
|
8910
|
+
};
|
|
8911
|
+
}
|
|
8912
|
+
};
|
|
8913
|
+
|
|
7218
8914
|
// src/detectors/index.ts
|
|
7219
8915
|
var detectors = [
|
|
7220
8916
|
d2,
|
|
@@ -7248,6 +8944,7 @@ var detectors = [
|
|
|
7248
8944
|
c22TestRatioGate,
|
|
7249
8945
|
c25BuildIdentity,
|
|
7250
8946
|
c26SubpackageLockfile,
|
|
8947
|
+
s28ClaimForwarded,
|
|
7251
8948
|
dServer2,
|
|
7252
8949
|
dServer6,
|
|
7253
8950
|
dDebugOptIn,
|
|
@@ -7267,18 +8964,18 @@ var detectors = [
|
|
|
7267
8964
|
|
|
7268
8965
|
// src/runtime/launch.ts
|
|
7269
8966
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
7270
|
-
import { existsSync as
|
|
7271
|
-
import { dirname as
|
|
8967
|
+
import { existsSync as existsSync16, readFileSync as readFileSync12, statSync as statSync9 } from "fs";
|
|
8968
|
+
import { dirname as dirname12, isAbsolute, join as join19, resolve as resolve2 } from "path";
|
|
7272
8969
|
function findPackageDir(target) {
|
|
7273
8970
|
let dir;
|
|
7274
8971
|
try {
|
|
7275
|
-
dir =
|
|
8972
|
+
dir = statSync9(target).isDirectory() ? target : dirname12(target);
|
|
7276
8973
|
} catch {
|
|
7277
8974
|
return null;
|
|
7278
8975
|
}
|
|
7279
8976
|
for (let depth = 0; depth < 8; depth++) {
|
|
7280
|
-
if (
|
|
7281
|
-
const parent =
|
|
8977
|
+
if (existsSync16(join19(dir, "package.json"))) return dir;
|
|
8978
|
+
const parent = dirname12(dir);
|
|
7282
8979
|
if (parent === dir) break;
|
|
7283
8980
|
dir = parent;
|
|
7284
8981
|
}
|
|
@@ -7302,17 +8999,17 @@ function deriveLaunchPlan(target) {
|
|
|
7302
8999
|
}
|
|
7303
9000
|
let pkg;
|
|
7304
9001
|
try {
|
|
7305
|
-
pkg = JSON.parse(
|
|
9002
|
+
pkg = JSON.parse(readFileSync12(join19(pkgDir, "package.json"), "utf8"));
|
|
7306
9003
|
} catch (err) {
|
|
7307
9004
|
return {
|
|
7308
|
-
error: `could not parse ${
|
|
9005
|
+
error: `could not parse ${join19(pkgDir, "package.json")}: ${err instanceof Error ? err.message : String(err)}`
|
|
7309
9006
|
};
|
|
7310
9007
|
}
|
|
7311
9008
|
const bin = firstBinEntry(pkg.bin);
|
|
7312
9009
|
if (bin) {
|
|
7313
9010
|
return {
|
|
7314
9011
|
command: process.execPath,
|
|
7315
|
-
args: [isAbsolute(bin) ? bin :
|
|
9012
|
+
args: [isAbsolute(bin) ? bin : resolve2(pkgDir, bin)],
|
|
7316
9013
|
cwd: pkgDir
|
|
7317
9014
|
};
|
|
7318
9015
|
}
|
|
@@ -7325,7 +9022,7 @@ function deriveLaunchPlan(target) {
|
|
|
7325
9022
|
if (main2) {
|
|
7326
9023
|
return {
|
|
7327
9024
|
command: process.execPath,
|
|
7328
|
-
args: [isAbsolute(main2) ? main2 :
|
|
9025
|
+
args: [isAbsolute(main2) ? main2 : resolve2(pkgDir, main2)],
|
|
7329
9026
|
cwd: pkgDir
|
|
7330
9027
|
};
|
|
7331
9028
|
}
|
|
@@ -7383,10 +9080,10 @@ function readEvalCounts(target) {
|
|
|
7383
9080
|
if (!pkgDir) return counts;
|
|
7384
9081
|
let dir = pkgDir;
|
|
7385
9082
|
for (let depth = 0; depth < 8; depth++) {
|
|
7386
|
-
const candidate =
|
|
7387
|
-
if (
|
|
9083
|
+
const candidate = join19(dir, "evals", "evals.json");
|
|
9084
|
+
if (existsSync16(candidate)) {
|
|
7388
9085
|
try {
|
|
7389
|
-
const parsed = JSON.parse(
|
|
9086
|
+
const parsed = JSON.parse(readFileSync12(candidate, "utf8"));
|
|
7390
9087
|
for (const c of parsed.cases ?? []) {
|
|
7391
9088
|
if (c && typeof c.tool === "string") counts.set(c.tool, (counts.get(c.tool) ?? 0) + 1);
|
|
7392
9089
|
}
|
|
@@ -7394,7 +9091,7 @@ function readEvalCounts(target) {
|
|
|
7394
9091
|
}
|
|
7395
9092
|
return counts;
|
|
7396
9093
|
}
|
|
7397
|
-
const parent =
|
|
9094
|
+
const parent = dirname12(dir);
|
|
7398
9095
|
if (parent === dir) break;
|
|
7399
9096
|
dir = parent;
|
|
7400
9097
|
}
|
|
@@ -7402,7 +9099,7 @@ function readEvalCounts(target) {
|
|
|
7402
9099
|
}
|
|
7403
9100
|
function statSyncSafe(p) {
|
|
7404
9101
|
try {
|
|
7405
|
-
return
|
|
9102
|
+
return statSync9(p).isDirectory();
|
|
7406
9103
|
} catch {
|
|
7407
9104
|
return false;
|
|
7408
9105
|
}
|
|
@@ -7525,7 +9222,7 @@ function runDoctor(target, detectorList = detectors, opts = {}) {
|
|
|
7525
9222
|
}
|
|
7526
9223
|
function runDoctorReport(target, detectorList = detectors, opts = {}) {
|
|
7527
9224
|
const mode = opts.mode ?? "static";
|
|
7528
|
-
const tree = loadTree(target);
|
|
9225
|
+
const tree = loadTree(target, opts.backend);
|
|
7529
9226
|
const needsRuntime = mode !== "static" && detectorList.some((d) => typeof d.runDynamic === "function");
|
|
7530
9227
|
const runtime = needsRuntime ? launchAndIntrospect(target) : null;
|
|
7531
9228
|
const results = [];
|
|
@@ -7654,7 +9351,7 @@ function computeExitCode(results) {
|
|
|
7654
9351
|
return 0;
|
|
7655
9352
|
}
|
|
7656
9353
|
var USAGE = [
|
|
7657
|
-
"usage: mcp-doctor <source-tree-path> [--mode=static|dynamic|both]",
|
|
9354
|
+
"usage: mcp-doctor <source-tree-path> [--mode=static|dynamic|both] [--backend=<convex-dir>]",
|
|
7658
9355
|
"",
|
|
7659
9356
|
" --mode=static read the tree only (default)",
|
|
7660
9357
|
" --mode=dynamic launch the server and judge what it serves",
|
|
@@ -7662,6 +9359,8 @@ var USAGE = [
|
|
|
7662
9359
|
" --local-identity-ceiling=N",
|
|
7663
9360
|
" local identity primitives tolerated before DLocalIdentity",
|
|
7664
9361
|
" fails (default 0)",
|
|
9362
|
+
" --backend=DIR the backend source (a convex/ dir) whose door list S-28 reads;",
|
|
9363
|
+
" default: the nearest convex/ above the tree",
|
|
7665
9364
|
" --version print the version and exit",
|
|
7666
9365
|
" --help print this and exit",
|
|
7667
9366
|
"",
|
|
@@ -7669,8 +9368,8 @@ var USAGE = [
|
|
|
7669
9368
|
].join("\n");
|
|
7670
9369
|
function readVersion() {
|
|
7671
9370
|
try {
|
|
7672
|
-
const here =
|
|
7673
|
-
const manifest = JSON.parse(
|
|
9371
|
+
const here = dirname13(fileURLToPath(import.meta.url));
|
|
9372
|
+
const manifest = JSON.parse(readFileSync13(join20(here, "..", "package.json"), "utf8"));
|
|
7674
9373
|
return manifest.version ?? null;
|
|
7675
9374
|
} catch {
|
|
7676
9375
|
return null;
|
|
@@ -7692,6 +9391,7 @@ function main() {
|
|
|
7692
9391
|
return;
|
|
7693
9392
|
}
|
|
7694
9393
|
const modeArg = args.find((a) => a.startsWith("--mode="));
|
|
9394
|
+
const backendArg = args.find((a) => a.startsWith("--backend="));
|
|
7695
9395
|
const target = args.find((a) => !a.startsWith("--"));
|
|
7696
9396
|
if (!target) {
|
|
7697
9397
|
console.error(USAGE);
|
|
@@ -7714,7 +9414,11 @@ function main() {
|
|
|
7714
9414
|
const ratchet = makeLocalIdentity({ ceiling });
|
|
7715
9415
|
detectorList = detectors.map((d) => d === localIdentity ? ratchet : d);
|
|
7716
9416
|
}
|
|
7717
|
-
const
|
|
9417
|
+
const backend = backendArg ? backendArg.slice("--backend=".length) : void 0;
|
|
9418
|
+
const { results, skipped } = runDoctorReport(target, detectorList, {
|
|
9419
|
+
mode,
|
|
9420
|
+
...backend !== void 0 ? { backend } : {}
|
|
9421
|
+
});
|
|
7718
9422
|
console.log(renderTable(target, results, skipped));
|
|
7719
9423
|
process.exitCode = computeExitCode(results);
|
|
7720
9424
|
}
|
|
@@ -7728,7 +9432,7 @@ function isDirectInvocation() {
|
|
|
7728
9432
|
return false;
|
|
7729
9433
|
}
|
|
7730
9434
|
try {
|
|
7731
|
-
return realpathSync(
|
|
9435
|
+
return realpathSync(resolve3(entry)) === selfReal;
|
|
7732
9436
|
} catch {
|
|
7733
9437
|
return false;
|
|
7734
9438
|
}
|