@vantageos/mcp-doctor 0.5.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 +123 -0
- package/README.md +22 -3
- package/dist/doctor.js +1430 -165
- package/package.json +3 -7
package/dist/doctor.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
// src/doctor.ts
|
|
4
4
|
import { readFileSync as readFileSync13, realpathSync } from "fs";
|
|
5
|
-
import { dirname as
|
|
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
|
|
@@ -129,9 +129,14 @@ function loadTree(root, backendRoot) {
|
|
|
129
129
|
var COM = 1;
|
|
130
130
|
var CODE = 2;
|
|
131
131
|
var maskCache = /* @__PURE__ */ new WeakMap();
|
|
132
|
+
var maskEnd = /* @__PURE__ */ new WeakMap();
|
|
132
133
|
function buildCodeMask(file) {
|
|
133
134
|
const mask = [];
|
|
134
135
|
let mode = "code";
|
|
136
|
+
const jsxFile = /\.[tj]sx$/.test(file.path);
|
|
137
|
+
let jsxDepth = 0;
|
|
138
|
+
let jstrQuote = "";
|
|
139
|
+
const jsxExprStack = [];
|
|
135
140
|
const tplStack = [];
|
|
136
141
|
let braceDepth = 0;
|
|
137
142
|
let prevSig = "";
|
|
@@ -193,6 +198,75 @@ function buildCodeMask(file) {
|
|
|
193
198
|
} else i++;
|
|
194
199
|
continue;
|
|
195
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
|
+
}
|
|
196
270
|
if (mode === "sq" || mode === "dq") {
|
|
197
271
|
if (c === "\\") i += 2;
|
|
198
272
|
else if (mode === "sq" && c === "'" || mode === "dq" && c === '"') {
|
|
@@ -260,6 +334,11 @@ function buildCodeMask(file) {
|
|
|
260
334
|
i = j;
|
|
261
335
|
continue;
|
|
262
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
|
+
}
|
|
263
342
|
if (c === '"' || c === "'" || c === "`") {
|
|
264
343
|
mode = c === '"' ? "dq" : c === "'" ? "sq" : "tpl";
|
|
265
344
|
i++;
|
|
@@ -279,6 +358,14 @@ function buildCodeMask(file) {
|
|
|
279
358
|
i++;
|
|
280
359
|
continue;
|
|
281
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
|
+
}
|
|
282
369
|
}
|
|
283
370
|
if (c !== " " && c !== " ") prevSig = c;
|
|
284
371
|
i++;
|
|
@@ -286,6 +373,7 @@ function buildCodeMask(file) {
|
|
|
286
373
|
if (mode === "sq" || mode === "dq" || mode === "re" || mode === "recls") mode = "code";
|
|
287
374
|
mask.push(row);
|
|
288
375
|
}
|
|
376
|
+
if (mode !== "code") maskEnd.set(file, mode);
|
|
289
377
|
return mask;
|
|
290
378
|
}
|
|
291
379
|
function maskOf(file) {
|
|
@@ -296,6 +384,12 @@ function maskOf(file) {
|
|
|
296
384
|
}
|
|
297
385
|
return mask;
|
|
298
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
|
+
}
|
|
299
393
|
function isCodeAt(file, n, col) {
|
|
300
394
|
const mask = maskOf(file);
|
|
301
395
|
return mask[n - 1]?.[col] === CODE;
|
|
@@ -752,6 +846,34 @@ var MATCHERS = [
|
|
|
752
846
|
re: /\bcreateHash\s*\(|\bcreateHmac\s*\(|\bsubtle\s*\.\s*(digest|sign|verify)\s*\(/,
|
|
753
847
|
accept: (m, ctx) => !digestsStaticInput(m, ctx),
|
|
754
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");'
|
|
755
877
|
}
|
|
756
878
|
];
|
|
757
879
|
function biteSample(mt, text) {
|
|
@@ -782,6 +904,12 @@ function localIdentityPositiveControls() {
|
|
|
782
904
|
out["negative:switch-on-unrelated-field"] = !bites('switch (row.kind) { case "a": return 1; }');
|
|
783
905
|
out["negative:literal-array-unrelated"] = !bites("return [kind].includes(row.status);");
|
|
784
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);"));
|
|
785
913
|
out["negative:object-is-nullish"] = !bites("return Object.is(row.orgId, undefined);");
|
|
786
914
|
return out;
|
|
787
915
|
}
|
|
@@ -861,19 +989,55 @@ function unreadableReason(file) {
|
|
|
861
989
|
if (file.lines.some((l) => l.raw.includes("\0") || l.raw.includes("\uFFFD"))) {
|
|
862
990
|
return "file is not valid UTF-8 text";
|
|
863
991
|
}
|
|
992
|
+
const open = unterminatedContext(file);
|
|
993
|
+
if (open) return `the file ends inside ${open} \u2014 the tail cannot be read as code`;
|
|
864
994
|
const pairs = [
|
|
865
995
|
["{", "}"],
|
|
866
996
|
["(", ")"],
|
|
867
997
|
["[", "]"]
|
|
868
998
|
];
|
|
869
999
|
const off = [];
|
|
870
|
-
for (const [
|
|
1000
|
+
for (const [open2, close] of pairs) {
|
|
871
1001
|
let net = 0;
|
|
872
|
-
for (const l of file.lines) net += countInCode(file, l,
|
|
873
|
-
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}`);
|
|
874
1004
|
}
|
|
875
1005
|
return off.length > 0 ? `brackets never balance (${off.join(", ")}) \u2014 the file cannot be read as code` : null;
|
|
876
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
|
+
}
|
|
877
1041
|
function findLocalIdentitySites(tree) {
|
|
878
1042
|
const pkg = tree.packageJson;
|
|
879
1043
|
const selfExempt = !pkg?.unreadable && pkg?.json.name === CLOUD_IDENTITY_PACKAGE;
|
|
@@ -882,6 +1046,7 @@ function findLocalIdentitySites(tree) {
|
|
|
882
1046
|
violations: [],
|
|
883
1047
|
exempt: [],
|
|
884
1048
|
delegatedCalls: 0,
|
|
1049
|
+
minting: [],
|
|
885
1050
|
unreadable: []
|
|
886
1051
|
};
|
|
887
1052
|
for (const file of tree.files) {
|
|
@@ -890,6 +1055,7 @@ function findLocalIdentitySites(tree) {
|
|
|
890
1055
|
const bindings = cloudIdentityBindings(file);
|
|
891
1056
|
const statics = staticConstants(file);
|
|
892
1057
|
const catalog = [...MATCHERS, ...aliasMatchers(file)];
|
|
1058
|
+
const signingChain = signingChainLines(file);
|
|
893
1059
|
scan.delegatedCalls += countDelegatedCalls(file, bindings);
|
|
894
1060
|
for (const line of file.lines) {
|
|
895
1061
|
if (line.comment) continue;
|
|
@@ -909,6 +1075,16 @@ function findLocalIdentitySites(tree) {
|
|
|
909
1075
|
}
|
|
910
1076
|
if (!hit) continue;
|
|
911
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
|
+
}
|
|
912
1088
|
const site = {
|
|
913
1089
|
file: file.rel,
|
|
914
1090
|
line: line.n,
|
|
@@ -932,6 +1108,10 @@ function exemptSuffix(exempt) {
|
|
|
932
1108
|
const rows = exempt.map((e) => `${describeSite(e)} \u2014 "${e.justification}"`);
|
|
933
1109
|
return `; EXEMPT (${exempt.length}): ${rows.join("; ")}`;
|
|
934
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
|
+
}
|
|
935
1115
|
function makeLocalIdentity(opts = {}) {
|
|
936
1116
|
const ceiling = opts.ceiling ?? 0;
|
|
937
1117
|
if (!Number.isInteger(ceiling) || ceiling < 0) {
|
|
@@ -967,7 +1147,7 @@ function makeLocalIdentity(opts = {}) {
|
|
|
967
1147
|
id,
|
|
968
1148
|
anchor: `${v.file}:${v.line}`,
|
|
969
1149
|
state: "non-conforming",
|
|
970
|
-
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>"`
|
|
971
1151
|
};
|
|
972
1152
|
} else if (s.violations.length > 0) {
|
|
973
1153
|
const v = s.violations[0];
|
|
@@ -975,14 +1155,14 @@ function makeLocalIdentity(opts = {}) {
|
|
|
975
1155
|
id,
|
|
976
1156
|
anchor: `${v.file}:${v.line}`,
|
|
977
1157
|
state: "present",
|
|
978
|
-
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)}`
|
|
979
1159
|
};
|
|
980
1160
|
} else {
|
|
981
1161
|
decided = {
|
|
982
1162
|
id,
|
|
983
1163
|
anchor: s.exempt[0] ? `${s.exempt[0].file}:${s.exempt[0].line}` : "-",
|
|
984
1164
|
state: "present",
|
|
985
|
-
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)}`
|
|
986
1166
|
};
|
|
987
1167
|
}
|
|
988
1168
|
return settle(id, decided, s.unreadable);
|
|
@@ -1169,6 +1349,237 @@ function resolveNameBindings(tree) {
|
|
|
1169
1349
|
}
|
|
1170
1350
|
return bindings;
|
|
1171
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
|
+
}
|
|
1172
1583
|
function findRegisteredTools(tree) {
|
|
1173
1584
|
const bindings = resolveNameBindings(tree);
|
|
1174
1585
|
const out = [];
|
|
@@ -1227,6 +1638,7 @@ function findRegisteredTools(tree) {
|
|
|
1227
1638
|
if (hasSchema) push(m[2], `${file.rel}:${line.n}`);
|
|
1228
1639
|
}
|
|
1229
1640
|
}
|
|
1641
|
+
for (const w of scanWrappers(tree).sites) push(w.name, w.anchor);
|
|
1230
1642
|
return out;
|
|
1231
1643
|
}
|
|
1232
1644
|
var DESCRIPTOR_KEY_RE = new RegExp(`^(\\s*)name:\\s*["'\`](${NAME_LITERAL})["'\`]\\s*,?\\s*$`);
|
|
@@ -1297,7 +1709,9 @@ function findUnresolvedToolSites(tree) {
|
|
|
1297
1709
|
}
|
|
1298
1710
|
if (awaitingName) out.push(openerAnchor);
|
|
1299
1711
|
}
|
|
1300
|
-
|
|
1712
|
+
const wrappers = scanWrappers(tree);
|
|
1713
|
+
const kept = out.filter((a) => !wrappers.internal.has(a));
|
|
1714
|
+
return [...kept, ...wrappers.unresolved];
|
|
1301
1715
|
}
|
|
1302
1716
|
function findListReadBuilders(tree) {
|
|
1303
1717
|
const out = [];
|
|
@@ -1350,6 +1764,45 @@ function sdkFirstAnchor(tree) {
|
|
|
1350
1764
|
}
|
|
1351
1765
|
return null;
|
|
1352
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
|
+
}
|
|
1353
1806
|
var VERB_FIELD_RE = /\bverb\s*:\s*["'`]([A-Za-z][A-Za-z-]*)["'`]/;
|
|
1354
1807
|
var TABLE_FIELD_RE = /\btable\s*:\s*["'`]([A-Za-z0-9_.-]+)["'`]/;
|
|
1355
1808
|
var MECHANISM_FIELD_RE = /\b(?:searchMechanism|mechanism)\s*:\s*["'`]([A-Za-z0-9_-]+(?:\([^)]*\))?)["'`]/;
|
|
@@ -1437,7 +1890,7 @@ function callOpenerLine(file, siteLine) {
|
|
|
1437
1890
|
for (let n = siteLine; n >= Math.max(1, siteLine - CALL_OPENER_LOOKBACK); n--) {
|
|
1438
1891
|
const line = file.lines[n - 1];
|
|
1439
1892
|
if (!line || line.comment) continue;
|
|
1440
|
-
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)) {
|
|
1441
1894
|
return line.n;
|
|
1442
1895
|
}
|
|
1443
1896
|
}
|
|
@@ -1458,6 +1911,34 @@ function ownCallEnd(file, siteLine) {
|
|
|
1458
1911
|
}
|
|
1459
1912
|
return file.lines.length;
|
|
1460
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
|
+
}
|
|
1461
1942
|
function findToolAnnotations(tree) {
|
|
1462
1943
|
const allSites = findRegisteredTools(tree);
|
|
1463
1944
|
const out = [];
|
|
@@ -1482,10 +1963,31 @@ function findToolAnnotations(tree) {
|
|
|
1482
1963
|
let paginationCursorDeclared = false;
|
|
1483
1964
|
let paginationDefault;
|
|
1484
1965
|
let paginationMax;
|
|
1966
|
+
let annotationsUnresolved;
|
|
1967
|
+
const windowRaws = [];
|
|
1485
1968
|
for (const line of file.lines) {
|
|
1486
1969
|
if (line.n <= windowStart || line.n > windowEnd) continue;
|
|
1487
1970
|
if (line.comment) continue;
|
|
1488
|
-
|
|
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) {
|
|
1489
1991
|
if (!verb) {
|
|
1490
1992
|
const m = raw.match(VERB_FIELD_RE);
|
|
1491
1993
|
if (m) verb = m[1];
|
|
@@ -1530,6 +2032,22 @@ function findToolAnnotations(tree) {
|
|
|
1530
2032
|
if (m) paginationMax = Number.parseInt(m[1], 10);
|
|
1531
2033
|
}
|
|
1532
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
|
+
}
|
|
1533
2051
|
out.push({
|
|
1534
2052
|
name: site.name,
|
|
1535
2053
|
anchor: site.anchor,
|
|
@@ -1546,7 +2064,8 @@ function findToolAnnotations(tree) {
|
|
|
1546
2064
|
tokensEstimeContexte,
|
|
1547
2065
|
paginationCursorDeclared,
|
|
1548
2066
|
paginationDefault,
|
|
1549
|
-
paginationMax
|
|
2067
|
+
paginationMax,
|
|
2068
|
+
annotationsUnresolved
|
|
1550
2069
|
});
|
|
1551
2070
|
}
|
|
1552
2071
|
}
|
|
@@ -1570,6 +2089,18 @@ function countKeywordInCode(tree, re) {
|
|
|
1570
2089
|
}
|
|
1571
2090
|
return { count, firstFile, firstLine };
|
|
1572
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
|
+
}
|
|
1573
2104
|
|
|
1574
2105
|
// src/detectors/c8-authority-declared.ts
|
|
1575
2106
|
var TIER_SET = /* @__PURE__ */ new Set(["public", "org-member", "org-admin", "fleet-internal", "master"]);
|
|
@@ -1622,6 +2153,8 @@ var c8AuthorityDeclared = {
|
|
|
1622
2153
|
verdict: "no tool registrations found; authority-declared check n/a"
|
|
1623
2154
|
};
|
|
1624
2155
|
}
|
|
2156
|
+
const opaque = opaqueAnnotationsAbstain(this.id, "C-8", tools);
|
|
2157
|
+
if (opaque) return opaque;
|
|
1625
2158
|
for (const t of tools) {
|
|
1626
2159
|
if (isFolded(t)) {
|
|
1627
2160
|
return {
|
|
@@ -1725,6 +2258,8 @@ var c9VerbDeclared = {
|
|
|
1725
2258
|
verdict: "no tool registrations found; verb-declared check n/a"
|
|
1726
2259
|
};
|
|
1727
2260
|
}
|
|
2261
|
+
const opaque = opaqueAnnotationsAbstain(this.id, "C-9", tools);
|
|
2262
|
+
if (opaque) return opaque;
|
|
1728
2263
|
for (const t of tools) {
|
|
1729
2264
|
if (!t.verb) {
|
|
1730
2265
|
return {
|
|
@@ -1820,6 +2355,8 @@ var c10MechanismDeclared = {
|
|
|
1820
2355
|
verdict: "no list/search tool found; mechanism-declared check n/a"
|
|
1821
2356
|
};
|
|
1822
2357
|
}
|
|
2358
|
+
const opaque = opaqueAnnotationsAbstain(this.id, "C-10", applicable);
|
|
2359
|
+
if (opaque) return opaque;
|
|
1823
2360
|
for (const t of applicable) {
|
|
1824
2361
|
if (!t.searchMechanism) {
|
|
1825
2362
|
return {
|
|
@@ -1894,6 +2431,8 @@ var c11Pagination = {
|
|
|
1894
2431
|
verdict: "no list/read/get/search tool registrations found; pagination declaration check n/a"
|
|
1895
2432
|
};
|
|
1896
2433
|
}
|
|
2434
|
+
const opaque = opaqueAnnotationsAbstain(this.id, "C-11", tools);
|
|
2435
|
+
if (opaque) return opaque;
|
|
1897
2436
|
for (const t of tools) {
|
|
1898
2437
|
const missing = [];
|
|
1899
2438
|
if (!t.paginationCursorDeclared) missing.push("paginationCursor");
|
|
@@ -2117,6 +2656,8 @@ var c13ContextCeiling = {
|
|
|
2117
2656
|
verdict: "no tool registrations found; context-ceiling check n/a"
|
|
2118
2657
|
};
|
|
2119
2658
|
}
|
|
2659
|
+
const opaque = opaqueAnnotationsAbstain(this.id, "C-13", tools);
|
|
2660
|
+
if (opaque) return opaque;
|
|
2120
2661
|
const ceiling = findCeiling(tree);
|
|
2121
2662
|
if (!ceiling) {
|
|
2122
2663
|
return {
|
|
@@ -2350,7 +2891,7 @@ var c15AnnotationsUntrusted = {
|
|
|
2350
2891
|
};
|
|
2351
2892
|
}
|
|
2352
2893
|
for (const t of tools) {
|
|
2353
|
-
if (!allHintsDeclared(t)) {
|
|
2894
|
+
if (!allHintsDeclared(t) && !t.annotationsUnresolved) {
|
|
2354
2895
|
const missing = missingHints(t).join(", ");
|
|
2355
2896
|
return {
|
|
2356
2897
|
id: this.id,
|
|
@@ -2360,6 +2901,17 @@ var c15AnnotationsUntrusted = {
|
|
|
2360
2901
|
};
|
|
2361
2902
|
}
|
|
2362
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
|
+
}
|
|
2363
2915
|
return {
|
|
2364
2916
|
id: this.id,
|
|
2365
2917
|
anchor: tools[0].anchor,
|
|
@@ -2448,21 +3000,92 @@ var c16UntrustedOutput = {
|
|
|
2448
3000
|
|
|
2449
3001
|
// src/detectors/c17-supply-chain.ts
|
|
2450
3002
|
import { existsSync as existsSync3, readFileSync as readFileSync4, readdirSync as readdirSync2 } from "fs";
|
|
2451
|
-
import { join as join3 } from "path";
|
|
3003
|
+
import { dirname as dirname2, join as join3, relative as relative2, resolve } from "path";
|
|
2452
3004
|
var PIN4 = "a74a042b53c71a095ab88b3073bd47728eeda814";
|
|
2453
3005
|
var LOCKFILES = ["package-lock.json", "pnpm-lock.yaml", "yarn.lock", "bun.lock", "bun.lockb"];
|
|
2454
3006
|
var WORKFLOW_DIR = join3(".github", "workflows");
|
|
2455
|
-
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";
|
|
2456
3067
|
function c17PositiveControls() {
|
|
2457
3068
|
const sample = "npm publish --provenance --access public";
|
|
2458
3069
|
const bare = "npm publish --access public";
|
|
3070
|
+
const claimed = (t) => workflowStatements(t).some(([, l]) => PROVENANCE_RE2.test(l));
|
|
2459
3071
|
return {
|
|
2460
3072
|
provenanceMatcher: {
|
|
2461
3073
|
pattern: PROVENANCE_RE2.source,
|
|
2462
3074
|
sample,
|
|
2463
3075
|
matched: PROVENANCE_RE2.test(sample),
|
|
2464
|
-
|
|
2465
|
-
|
|
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
|
|
2466
3089
|
};
|
|
2467
3090
|
}
|
|
2468
3091
|
function findLockfile(root) {
|
|
@@ -2479,30 +3102,45 @@ function findProvenanceClaim(tree, root) {
|
|
|
2479
3102
|
return { site: `${pkg.path}:publishConfig.provenance`, text: "provenance: true" };
|
|
2480
3103
|
}
|
|
2481
3104
|
}
|
|
2482
|
-
const
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
entries = readdirSync2(workflowsDir);
|
|
2487
|
-
} catch {
|
|
2488
|
-
return null;
|
|
2489
|
-
}
|
|
2490
|
-
for (const name of entries) {
|
|
2491
|
-
if (!/\.ya?ml$/.test(name)) continue;
|
|
2492
|
-
const path = join3(workflowsDir, name);
|
|
2493
|
-
let text;
|
|
3105
|
+
for (const dir of workflowDirs(root)) {
|
|
3106
|
+
const workflowsDir = join3(dir, WORKFLOW_DIR);
|
|
3107
|
+
if (!existsSync3(workflowsDir)) continue;
|
|
3108
|
+
let entries = [];
|
|
2494
3109
|
try {
|
|
2495
|
-
|
|
3110
|
+
entries = readdirSync2(workflowsDir).sort();
|
|
2496
3111
|
} catch {
|
|
2497
3112
|
continue;
|
|
2498
3113
|
}
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
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
|
+
}
|
|
2502
3129
|
}
|
|
2503
3130
|
}
|
|
2504
3131
|
return null;
|
|
2505
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
|
+
}
|
|
2506
3144
|
var c17SupplyChainInner = {
|
|
2507
3145
|
id: "C17 supply-chain",
|
|
2508
3146
|
rule: "C-17",
|
|
@@ -2514,6 +3152,11 @@ var c17SupplyChainInner = {
|
|
|
2514
3152
|
`C17 provenance-matcher control FAILED: accept(\`${controls.provenanceMatcher.sample}\`)=${controls.provenanceMatcher.matched} rejectsBare=${controls.provenanceMatcher.rejectsBare} \u2014 matcher broken, verdict untrustworthy`
|
|
2515
3153
|
);
|
|
2516
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
|
+
}
|
|
2517
3160
|
const lockfile = findLockfile(tree.root);
|
|
2518
3161
|
if (!lockfile) {
|
|
2519
3162
|
return {
|
|
@@ -2524,12 +3167,31 @@ var c17SupplyChainInner = {
|
|
|
2524
3167
|
};
|
|
2525
3168
|
}
|
|
2526
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
|
+
}
|
|
2527
3189
|
if (!claim) {
|
|
2528
3190
|
return {
|
|
2529
3191
|
id: this.id,
|
|
2530
3192
|
anchor: `${lockfile}:1`,
|
|
2531
3193
|
state: "non-conforming",
|
|
2532
|
-
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)`
|
|
2533
3195
|
};
|
|
2534
3196
|
}
|
|
2535
3197
|
return {
|
|
@@ -2553,6 +3215,28 @@ function manifestPath(root) {
|
|
|
2553
3215
|
if (existsSync4(extPath)) return { kind: "extension", path: extPath };
|
|
2554
3216
|
return { kind: "server", path: join4(root, "mcp.json") };
|
|
2555
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
|
+
}
|
|
2556
3240
|
function readJson(path) {
|
|
2557
3241
|
try {
|
|
2558
3242
|
return JSON.parse(readFileSync5(path, "utf8"));
|
|
@@ -2566,7 +3250,10 @@ function c18PositiveControls() {
|
|
|
2566
3250
|
versionParity: {
|
|
2567
3251
|
matched: equalCheck("0.1.0", "0.1.0"),
|
|
2568
3252
|
rejectsMismatch: !equalCheck("0.1.0", "0.2.0")
|
|
2569
|
-
}
|
|
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"
|
|
2570
3257
|
};
|
|
2571
3258
|
}
|
|
2572
3259
|
var c18StructureManifestInner = {
|
|
@@ -2575,6 +3262,11 @@ var c18StructureManifestInner = {
|
|
|
2575
3262
|
pin: PIN5,
|
|
2576
3263
|
run(tree) {
|
|
2577
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
|
+
}
|
|
2578
3270
|
if (!controls.versionParity.matched || !controls.versionParity.rejectsMismatch) {
|
|
2579
3271
|
throw new Error(
|
|
2580
3272
|
"C18 version-parity control FAILED: equal-strings must match and unequal-strings must be rejected \u2014 verdict untrustworthy"
|
|
@@ -2601,25 +3293,64 @@ var c18StructureManifestInner = {
|
|
|
2601
3293
|
verdict: `could not judge: ${reason}`
|
|
2602
3294
|
};
|
|
2603
3295
|
}
|
|
2604
|
-
const
|
|
2605
|
-
const
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
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
|
+
}
|
|
2622
3352
|
}
|
|
3353
|
+
if (!manifest) throw new Error("C18: manifest selection produced no manifest \u2014 unreachable");
|
|
2623
3354
|
if (typeof manifest.name !== "string" || manifest.name.length === 0) {
|
|
2624
3355
|
return {
|
|
2625
3356
|
id: this.id,
|
|
@@ -2653,7 +3384,7 @@ var c18StructureManifestInner = {
|
|
|
2653
3384
|
id: this.id,
|
|
2654
3385
|
anchor: path,
|
|
2655
3386
|
state: "non-conforming",
|
|
2656
|
-
verdict:
|
|
3387
|
+
verdict: `${manifestName} is missing required field(s): ${missing.join(", ")} not declared -> declare capabilities/transport (C-18)`
|
|
2657
3388
|
};
|
|
2658
3389
|
}
|
|
2659
3390
|
}
|
|
@@ -2689,30 +3420,52 @@ var c18StructureManifest = guardUnreadablePackageJson(
|
|
|
2689
3420
|
// src/detectors/c19-served-descriptions.ts
|
|
2690
3421
|
var PIN6 = "a74a042b53c71a095ab88b3073bd47728eeda814";
|
|
2691
3422
|
var TOOL_DESC_INLINE_RE = /\b(?:s\.tool|\.tool|registerTool)\s*\(\s*(?:["'`][^"'`]+["'`]|[A-Za-z_$][\w$]*)\s*,\s*["'`]([^"'`]+)["'`]/;
|
|
2692
|
-
var DESCRIPTOR_DESC_RE = /\bdescription\s*:\s*["'`]([^"'`]+)["'`]/;
|
|
2693
3423
|
var BARE_STRING_RE = /^\s*["'`]([^"'`]+)["'`]\s*,?\s*$/;
|
|
2694
3424
|
var BACKWARD_LOOKBACK = 10;
|
|
2695
3425
|
var FORWARD_LOOKAHEAD = 6;
|
|
3426
|
+
var DESCRIPTION_KEY_RE = /\bdescription\s*:/;
|
|
2696
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
|
+
}
|
|
2697
3435
|
const [rel, lineStr] = siteAnchor.split(":");
|
|
2698
3436
|
const file = tree.files.find((f) => f.rel === rel);
|
|
2699
3437
|
if (!file) return void 0;
|
|
2700
3438
|
const siteLine = Number.parseInt(lineStr, 10);
|
|
2701
|
-
|
|
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);
|
|
2702
3446
|
const end = Math.min(siteLine + FORWARD_LOOKAHEAD, file.lines.length + 1);
|
|
3447
|
+
let nonLiteral;
|
|
2703
3448
|
for (let n = start; n < end; n++) {
|
|
2704
3449
|
const line = file.lines[n - 1];
|
|
2705
3450
|
if (!line || line.comment) continue;
|
|
2706
3451
|
const inline = line.raw.match(TOOL_DESC_INLINE_RE);
|
|
2707
3452
|
if (inline) return inline[1];
|
|
2708
|
-
const
|
|
2709
|
-
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
|
+
}
|
|
2710
3463
|
if (n >= siteLine) {
|
|
2711
3464
|
const bare = line.raw.match(BARE_STRING_RE);
|
|
2712
3465
|
if (bare && bare[1] !== toolName) return bare[1];
|
|
2713
3466
|
}
|
|
2714
3467
|
}
|
|
2715
|
-
return
|
|
3468
|
+
return nonLiteral;
|
|
2716
3469
|
}
|
|
2717
3470
|
var TRIGGER_LIST_RE = /use this whenever/i;
|
|
2718
3471
|
var TRIGGER_CLOSE_RE = /even if they don'?t say/i;
|
|
@@ -2727,7 +3480,10 @@ function c19PositiveControls() {
|
|
|
2727
3480
|
triggerListAccepts: TRIGGER_LIST_RE.test(triggerSample),
|
|
2728
3481
|
triggerCloseAccepts: TRIGGER_CLOSE_RE.test(triggerSample),
|
|
2729
3482
|
bilingualAccepts: BILINGUAL_SPLIT_RE.test(bilingualSample),
|
|
2730
|
-
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"
|
|
2731
3487
|
};
|
|
2732
3488
|
}
|
|
2733
3489
|
var c19ServedDescriptions = {
|
|
@@ -2736,7 +3492,7 @@ var c19ServedDescriptions = {
|
|
|
2736
3492
|
pin: PIN6,
|
|
2737
3493
|
run(tree) {
|
|
2738
3494
|
const controls = c19PositiveControls();
|
|
2739
|
-
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) {
|
|
2740
3496
|
throw new Error(
|
|
2741
3497
|
`C19 positive control FAILED: ${JSON.stringify(controls)} \u2014 matcher broken, verdict untrustworthy`
|
|
2742
3498
|
);
|
|
@@ -2764,8 +3520,13 @@ var c19ServedDescriptions = {
|
|
|
2764
3520
|
verdict: "no tool registrations found; served-descriptions check n/a (C-19)"
|
|
2765
3521
|
};
|
|
2766
3522
|
}
|
|
3523
|
+
let opaque;
|
|
2767
3524
|
for (const site of sites) {
|
|
2768
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
|
+
}
|
|
2769
3530
|
if (!desc) {
|
|
2770
3531
|
return {
|
|
2771
3532
|
id: this.id,
|
|
@@ -2775,6 +3536,16 @@ var c19ServedDescriptions = {
|
|
|
2775
3536
|
};
|
|
2776
3537
|
}
|
|
2777
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
|
+
}
|
|
2778
3549
|
return {
|
|
2779
3550
|
id: this.id,
|
|
2780
3551
|
anchor: sites[0].anchor,
|
|
@@ -3117,6 +3888,11 @@ var IDENTITY_KEY_NAMES = [
|
|
|
3117
3888
|
"gitCommit",
|
|
3118
3889
|
"gitRevision"
|
|
3119
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
|
+
);
|
|
3120
3896
|
var CONSTRUCTOR_RE = /\bnew\s+(?:McpServer|Server)\s*\(/;
|
|
3121
3897
|
var IDENTITY_KEY_LINE_RE = new RegExp(
|
|
3122
3898
|
`\\b(${IDENTITY_KEY_NAMES.join("|")})\\s*:\\s*(.+?)\\s*,?\\s*$`
|
|
@@ -3132,12 +3908,25 @@ function isLiteralValue(expr) {
|
|
|
3132
3908
|
if (m[1] === "`" && /\$\{/.test(m[2])) return false;
|
|
3133
3909
|
return true;
|
|
3134
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
|
+
}
|
|
3135
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);
|
|
3136
3922
|
const literalMatch = LITERAL_SAMPLE.match(IDENTITY_KEY_LINE_RE);
|
|
3137
3923
|
const derivedMatch = DERIVED_SAMPLE.match(IDENTITY_KEY_LINE_RE);
|
|
3138
3924
|
return {
|
|
3139
3925
|
literalAccepts: literalMatch ? isLiteralValue(literalMatch[2]) : false,
|
|
3140
|
-
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
|
|
3141
3930
|
};
|
|
3142
3931
|
}
|
|
3143
3932
|
function extractConstructorArgs(file, startIdx) {
|
|
@@ -3162,6 +3951,27 @@ function extractConstructorArgs(file, startIdx) {
|
|
|
3162
3951
|
}
|
|
3163
3952
|
return null;
|
|
3164
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
|
+
}
|
|
3165
3975
|
function findConstructorSites(tree) {
|
|
3166
3976
|
const sites = [];
|
|
3167
3977
|
for (const file of tree.files) {
|
|
@@ -3177,14 +3987,14 @@ function findConstructorSites(tree) {
|
|
|
3177
3987
|
}
|
|
3178
3988
|
return sites;
|
|
3179
3989
|
}
|
|
3180
|
-
function classifyArgs(args, anchor) {
|
|
3990
|
+
function classifyArgs(tree, args, anchor) {
|
|
3181
3991
|
for (const raw of args.split("\n")) {
|
|
3182
3992
|
const bare = raw.match(IDENTITY_KEY_BARE_RE);
|
|
3183
3993
|
if (bare) return { kind: "bare-continuation", key: bare[1], anchor };
|
|
3184
3994
|
const m = raw.match(IDENTITY_KEY_LINE_RE);
|
|
3185
3995
|
if (m) {
|
|
3186
3996
|
const [, key, value] = m;
|
|
3187
|
-
if (
|
|
3997
|
+
if (valueIsLiteral(tree, value)) return { kind: "literal", key, value: value.trim(), anchor };
|
|
3188
3998
|
return { kind: "derived", key, anchor };
|
|
3189
3999
|
}
|
|
3190
4000
|
}
|
|
@@ -3196,7 +4006,7 @@ var c25BuildIdentity = {
|
|
|
3196
4006
|
pin: PIN8,
|
|
3197
4007
|
run(tree) {
|
|
3198
4008
|
const controls = c25PositiveControls();
|
|
3199
|
-
if (!controls.literalAccepts || controls.derivedRejectsAsLiteral) {
|
|
4009
|
+
if (!controls.literalAccepts || controls.derivedRejectsAsLiteral || !controls.healthRouteMatches || !controls.healthDerivedCommitReads || !controls.healthLiteralCommitReads) {
|
|
3200
4010
|
throw new Error(
|
|
3201
4011
|
`C25 literal/derived control FAILED: ${JSON.stringify(controls)} \u2014 matcher broken, verdict untrustworthy`
|
|
3202
4012
|
);
|
|
@@ -3222,8 +4032,18 @@ var c25BuildIdentity = {
|
|
|
3222
4032
|
};
|
|
3223
4033
|
}
|
|
3224
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
|
+
}
|
|
3225
4045
|
for (const site of sites) {
|
|
3226
|
-
const finding = classifyArgs(site.args, site.anchor);
|
|
4046
|
+
const finding = classifyArgs(tree, site.args, site.anchor);
|
|
3227
4047
|
if (finding.kind === "bare-continuation") {
|
|
3228
4048
|
const reason = `identity key "${finding.key}" found at ${finding.anchor} but its value continues past the line (not statically joined) \u2014 verdict unreadable`;
|
|
3229
4049
|
return {
|
|
@@ -3251,6 +4071,15 @@ var c25BuildIdentity = {
|
|
|
3251
4071
|
};
|
|
3252
4072
|
}
|
|
3253
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
|
+
}
|
|
3254
4083
|
return {
|
|
3255
4084
|
id: this.id,
|
|
3256
4085
|
anchor: sites[0].anchor,
|
|
@@ -3359,7 +4188,7 @@ import {
|
|
|
3359
4188
|
writeFileSync
|
|
3360
4189
|
} from "fs";
|
|
3361
4190
|
import { tmpdir } from "os";
|
|
3362
|
-
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";
|
|
3363
4192
|
var PIN9 = "20136b50";
|
|
3364
4193
|
var DEPLOY_CONFIG_FILES = [
|
|
3365
4194
|
"railway.json",
|
|
@@ -3462,12 +4291,12 @@ function c26DeriveScope(root, tree) {
|
|
|
3462
4291
|
const skipped = [];
|
|
3463
4292
|
const onSkip = (skippedDir, depth) => {
|
|
3464
4293
|
const entry = tree ? recordSkip(tree, skippedDir, root) : {
|
|
3465
|
-
path:
|
|
4294
|
+
path: relative3(root, skippedDir).split(sep2).join("/"),
|
|
3466
4295
|
reason: SKIP_DIR_INFO[basename2(skippedDir)]?.reason ?? "skipped"
|
|
3467
4296
|
};
|
|
3468
4297
|
if (SKIP_DIR_INFO[basename2(skippedDir)]?.authored) {
|
|
3469
4298
|
const hit = probeForCandidate(skippedDir, depth);
|
|
3470
|
-
if (hit) entry.candidate =
|
|
4299
|
+
if (hit) entry.candidate = relative3(root, hit).split(sep2).join("/");
|
|
3471
4300
|
}
|
|
3472
4301
|
if (!skipped.some((d) => d.path === entry.path)) skipped.push(entry);
|
|
3473
4302
|
};
|
|
@@ -3651,7 +4480,7 @@ function renderCoverage(scope, verdicts) {
|
|
|
3651
4480
|
}
|
|
3652
4481
|
}
|
|
3653
4482
|
return verdicts.map((v) => {
|
|
3654
|
-
const rel =
|
|
4483
|
+
const rel = relative3(process.cwd(), v.site.dir) || v.site.dir;
|
|
3655
4484
|
if (v.status === "skipped") return `SKIPPED ${rel} (${v.reason})`;
|
|
3656
4485
|
return `ANALYSED ${rel} -> ${v.ok ? "GREEN" : "RED"}: ${v.detail}`;
|
|
3657
4486
|
}).join(" | ");
|
|
@@ -3729,30 +4558,102 @@ var c26SubpackageLockfile = {
|
|
|
3729
4558
|
|
|
3730
4559
|
// src/detectors/d2.ts
|
|
3731
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
|
+
}
|
|
3732
4608
|
var d2 = {
|
|
3733
4609
|
id: "D2 stateless",
|
|
3734
4610
|
run(tree) {
|
|
3735
|
-
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 = [];
|
|
3736
4620
|
for (const file of tree.files) {
|
|
3737
|
-
|
|
3738
|
-
if (line.comment)
|
|
3739
|
-
|
|
3740
|
-
|
|
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
|
+
});
|
|
3741
4633
|
}
|
|
3742
|
-
if (
|
|
3743
|
-
const anchor = `${hits[0].file}:${hits.map((h) => h.line).join(",")}`;
|
|
4634
|
+
if (keying.length > 0) {
|
|
3744
4635
|
return {
|
|
3745
4636
|
id: this.id,
|
|
3746
|
-
anchor
|
|
4637
|
+
anchor: `${keying[0].site}`,
|
|
3747
4638
|
state: "non-conforming",
|
|
3748
|
-
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}`
|
|
3749
4650
|
};
|
|
3750
4651
|
}
|
|
3751
4652
|
return {
|
|
3752
4653
|
id: this.id,
|
|
3753
|
-
anchor: "-",
|
|
4654
|
+
anchor: cors[0] ?? "-",
|
|
3754
4655
|
state: "present",
|
|
3755
|
-
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)"
|
|
3756
4657
|
};
|
|
3757
4658
|
}
|
|
3758
4659
|
};
|
|
@@ -3990,9 +4891,53 @@ function depListed(pkgJson) {
|
|
|
3990
4891
|
}
|
|
3991
4892
|
return false;
|
|
3992
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
|
+
}
|
|
3993
4932
|
var d7Inner = {
|
|
3994
4933
|
id: "D7 protocol-kit",
|
|
3995
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
|
+
}
|
|
3996
4941
|
const listed = depListed(tree.packageJson?.json);
|
|
3997
4942
|
let importLine = null;
|
|
3998
4943
|
let callLine = null;
|
|
@@ -4021,9 +4966,38 @@ var d7Inner = {
|
|
|
4021
4966
|
verdict: "protocol-kit listed but no builder called -> wire buildToolsListResult/handleServerDiscover"
|
|
4022
4967
|
};
|
|
4023
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
|
+
}
|
|
4991
|
+
return {
|
|
4992
|
+
id: this.id,
|
|
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`
|
|
4996
|
+
};
|
|
4997
|
+
}
|
|
4024
4998
|
return {
|
|
4025
4999
|
id: this.id,
|
|
4026
|
-
anchor:
|
|
5000
|
+
anchor: pkgAnchor,
|
|
4027
5001
|
state: "absent",
|
|
4028
5002
|
verdict: "hand-rolled builders, protocol-kit not a dependency -> add @vantageos/mcp-protocol-kit"
|
|
4029
5003
|
};
|
|
@@ -4033,7 +5007,7 @@ var d7 = guardUnreadablePackageJson(d7Inner, { authoritative: true });
|
|
|
4033
5007
|
|
|
4034
5008
|
// src/detectors/d8.ts
|
|
4035
5009
|
import { existsSync as existsSync7, statSync as statSync3 } from "fs";
|
|
4036
|
-
import { dirname as
|
|
5010
|
+
import { dirname as dirname3, join as join7, relative as relative4 } from "path";
|
|
4037
5011
|
var POSITIVE_CONTROL2 = 'server.registerTool("list_items", { ... }, handler);';
|
|
4038
5012
|
function d8PositiveControl() {
|
|
4039
5013
|
return {
|
|
@@ -4043,9 +5017,9 @@ function d8PositiveControl() {
|
|
|
4043
5017
|
};
|
|
4044
5018
|
}
|
|
4045
5019
|
function resolveBaseDir(tree) {
|
|
4046
|
-
if (tree.packageJson) return
|
|
5020
|
+
if (tree.packageJson) return dirname3(tree.packageJson.path);
|
|
4047
5021
|
try {
|
|
4048
|
-
return statSync3(tree.root).isDirectory() ? tree.root :
|
|
5022
|
+
return statSync3(tree.root).isDirectory() ? tree.root : dirname3(tree.root);
|
|
4049
5023
|
} catch {
|
|
4050
5024
|
return tree.root;
|
|
4051
5025
|
}
|
|
@@ -4055,7 +5029,7 @@ function findEvalsFile(tree) {
|
|
|
4055
5029
|
for (let depth = 0; depth < 8; depth++) {
|
|
4056
5030
|
const candidate = join7(dir, "evals", "evals.json");
|
|
4057
5031
|
if (existsSync7(candidate)) return candidate;
|
|
4058
|
-
const parent =
|
|
5032
|
+
const parent = dirname3(dir);
|
|
4059
5033
|
if (parent === dir) break;
|
|
4060
5034
|
dir = parent;
|
|
4061
5035
|
}
|
|
@@ -4116,7 +5090,7 @@ var d8 = {
|
|
|
4116
5090
|
};
|
|
4117
5091
|
}
|
|
4118
5092
|
const baseDir = resolveBaseDir(tree);
|
|
4119
|
-
const displayPath =
|
|
5093
|
+
const displayPath = relative4(baseDir, evalsPath) || evalsPath;
|
|
4120
5094
|
const parsed = readJsonObject(evalsPath);
|
|
4121
5095
|
if (parsed === null || !Array.isArray(parsed.cases)) {
|
|
4122
5096
|
return couldNotJudge(
|
|
@@ -4200,7 +5174,7 @@ var d8 = {
|
|
|
4200
5174
|
|
|
4201
5175
|
// src/detectors/d9.ts
|
|
4202
5176
|
import { existsSync as existsSync8 } from "fs";
|
|
4203
|
-
import { dirname as
|
|
5177
|
+
import { dirname as dirname4, join as join8 } from "path";
|
|
4204
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-]+)*))?$/;
|
|
4205
5179
|
var POSITIVE_CONTROL3 = "1.2.3-beta.1+build.7";
|
|
4206
5180
|
function d9PositiveControl() {
|
|
@@ -4248,7 +5222,7 @@ var d9Inner = {
|
|
|
4248
5222
|
verdict: `no "version" field in ${pkgPath.split("/").pop()} -> set a valid SemVer version`
|
|
4249
5223
|
};
|
|
4250
5224
|
}
|
|
4251
|
-
const projectDir =
|
|
5225
|
+
const projectDir = dirname4(pkgPath);
|
|
4252
5226
|
const changelogPath = join8(projectDir, "CHANGELOG.md");
|
|
4253
5227
|
const hasChangelog = existsSync8(changelogPath);
|
|
4254
5228
|
const validSemver = SEMVER_RE.test(version);
|
|
@@ -4280,7 +5254,7 @@ var d9 = guardUnreadablePackageJson(d9Inner, { authoritative: false });
|
|
|
4280
5254
|
|
|
4281
5255
|
// src/detectors/d10.ts
|
|
4282
5256
|
import { existsSync as existsSync9 } from "fs";
|
|
4283
|
-
import { dirname as
|
|
5257
|
+
import { dirname as dirname5, join as join9 } from "path";
|
|
4284
5258
|
var DESCRIPTION_FR_RE = /"description_fr"\s*:\s*"[^"]+"/;
|
|
4285
5259
|
var POSITIVE_CONTROL4 = '"description_fr": "Un serveur MCP conforme.",';
|
|
4286
5260
|
function d10PositiveControl() {
|
|
@@ -4315,17 +5289,38 @@ function firstSourceAnchor(tree, re) {
|
|
|
4315
5289
|
function scanSourceDescriptionFr(tree) {
|
|
4316
5290
|
const recognized = firstSourceAnchor(tree, SOURCE_DESCRIPTION_FR_RE);
|
|
4317
5291
|
if (recognized) return { present: true, anchor: recognized.anchor };
|
|
4318
|
-
const
|
|
4319
|
-
|
|
4320
|
-
|
|
4321
|
-
|
|
4322
|
-
|
|
4323
|
-
|
|
4324
|
-
|
|
4325
|
-
|
|
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;
|
|
4326
5306
|
}
|
|
4327
|
-
|
|
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
|
+
}
|
|
4328
5322
|
}
|
|
5323
|
+
if (undecidable) return { present: false, anchor: null, undecidable };
|
|
4329
5324
|
return { present: false, anchor: null };
|
|
4330
5325
|
}
|
|
4331
5326
|
var d10Inner = {
|
|
@@ -4345,6 +5340,11 @@ var d10Inner = {
|
|
|
4345
5340
|
`D10 source-form control FAILED: /${sourceControl.pattern}/ accept=${sourceControl.matchedPositive} reject=${!sourceControl.matchedNegative} \u2014 matcher broken, absence verdict untrustworthy`
|
|
4346
5341
|
);
|
|
4347
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
|
+
}
|
|
4348
5348
|
if (!tree.packageJson) {
|
|
4349
5349
|
return {
|
|
4350
5350
|
id: this.id,
|
|
@@ -4355,7 +5355,7 @@ var d10Inner = {
|
|
|
4355
5355
|
};
|
|
4356
5356
|
}
|
|
4357
5357
|
const { path: pkgPath, json: pkg } = tree.packageJson;
|
|
4358
|
-
const root =
|
|
5358
|
+
const root = dirname5(pkgPath);
|
|
4359
5359
|
const descriptionFr = typeof pkg.description_fr === "string" ? pkg.description_fr.trim() : "";
|
|
4360
5360
|
const hasDescriptionFrJson = descriptionFr.length > 0;
|
|
4361
5361
|
const sourceScan = scanSourceDescriptionFr(tree);
|
|
@@ -4775,11 +5775,11 @@ var d15 = {
|
|
|
4775
5775
|
|
|
4776
5776
|
// src/detectors/d16.ts
|
|
4777
5777
|
import { existsSync as existsSync10, readFileSync as readFileSync8, statSync as statSync4 } from "fs";
|
|
4778
|
-
import { dirname as
|
|
5778
|
+
import { dirname as dirname6, join as join10 } from "path";
|
|
4779
5779
|
var DISCOVER_RE2 = /server\/discover|DiscoverResult|ServerDiscover|handleServerDiscover/;
|
|
4780
5780
|
var HEALTH_ROUTE_FRAMEWORK_RE = /\b[A-Za-z_$][\w$]*\.(?:get|post|put|patch|delete|all|route|use)\s*\(\s*["'`][^"'`]*\/health/;
|
|
4781
5781
|
var HEALTH_ROUTE_RAWPATH_RE = /\bpathname\s*===?\s*["'`][^"'`]*\/health/;
|
|
4782
|
-
var
|
|
5782
|
+
var HEALTH_ROUTE_RE2 = new RegExp(
|
|
4783
5783
|
`${HEALTH_ROUTE_FRAMEWORK_RE.source}|${HEALTH_ROUTE_RAWPATH_RE.source}`
|
|
4784
5784
|
);
|
|
4785
5785
|
var VERSION_FIELD_RE = /\bversion\s*:/;
|
|
@@ -4790,6 +5790,7 @@ var VERSION_COMPUTED_RE = /\bversion\s*:\s*[A-Za-z_$][\w.]*\s*\(/;
|
|
|
4790
5790
|
var VERSION_LITERAL_RE = /\bversion\s*:\s*["'`][^"'`]+["'`]/;
|
|
4791
5791
|
var AUTH_GATE_RE = /requireAuth|verifyBearer|assertAuth|ensureAuthenticated|withAuth|requireBearer/;
|
|
4792
5792
|
var WINDOW = 25;
|
|
5793
|
+
var SDK_SERVER_NEW_RE = /\bnew\s+McpServer\s*\(/;
|
|
4793
5794
|
var CONTROLS2 = [
|
|
4794
5795
|
{
|
|
4795
5796
|
key: "surface",
|
|
@@ -4799,7 +5800,7 @@ var CONTROLS2 = [
|
|
|
4799
5800
|
},
|
|
4800
5801
|
{
|
|
4801
5802
|
key: "health-framework",
|
|
4802
|
-
re:
|
|
5803
|
+
re: HEALTH_ROUTE_RE2,
|
|
4803
5804
|
positive: 'app.get("/health", handler)',
|
|
4804
5805
|
negative: 'app.get("/tools", handler)'
|
|
4805
5806
|
},
|
|
@@ -4808,7 +5809,7 @@ var CONTROLS2 = [
|
|
|
4808
5809
|
// Positive is the REAL vantage-registry-mcp:297 shape; negative is the
|
|
4809
5810
|
// sibling `/mcp` route on the same server (a near-miss that must NOT match).
|
|
4810
5811
|
key: "health-rawpath",
|
|
4811
|
-
re:
|
|
5812
|
+
re: HEALTH_ROUTE_RE2,
|
|
4812
5813
|
positive: 'if (req.method === "GET" && url.pathname === "/health") {',
|
|
4813
5814
|
negative: 'if (url.pathname === "/mcp") {'
|
|
4814
5815
|
},
|
|
@@ -4818,6 +5819,12 @@ var CONTROLS2 = [
|
|
|
4818
5819
|
positive: "version: pkg.version",
|
|
4819
5820
|
negative: "version: tool.version"
|
|
4820
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
|
+
},
|
|
4821
5828
|
{
|
|
4822
5829
|
key: "auth-gate",
|
|
4823
5830
|
re: AUTH_GATE_RE,
|
|
@@ -4890,13 +5897,58 @@ function classifySurface(file, startIdx, anchor, pkgIdents) {
|
|
|
4890
5897
|
if (!hasVersion) return { kind: "absent-version", anchor };
|
|
4891
5898
|
return { kind: "unreadable-source", anchor };
|
|
4892
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
|
+
}
|
|
4893
5943
|
function evalSurfaces(tree) {
|
|
4894
5944
|
const out = [];
|
|
5945
|
+
const sdk = sdkServerVersionSurface(tree);
|
|
5946
|
+
if (sdk) out.push(sdk);
|
|
4895
5947
|
for (const file of tree.files) {
|
|
4896
5948
|
const pkgIdents = collectPkgIdents(file);
|
|
4897
5949
|
file.lines.forEach((line, idx) => {
|
|
4898
5950
|
if (line.comment) return;
|
|
4899
|
-
if (DISCOVER_RE2.test(line.raw) ||
|
|
5951
|
+
if (DISCOVER_RE2.test(line.raw) || HEALTH_ROUTE_RE2.test(line.raw)) {
|
|
4900
5952
|
out.push(classifySurface(file, idx, `${file.rel}:${line.n}`, pkgIdents));
|
|
4901
5953
|
}
|
|
4902
5954
|
});
|
|
@@ -4928,7 +5980,7 @@ var d16 = {
|
|
|
4928
5980
|
id: this.id,
|
|
4929
5981
|
anchor: present.anchor,
|
|
4930
5982
|
state: "present",
|
|
4931
|
-
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)`
|
|
4932
5984
|
};
|
|
4933
5985
|
}
|
|
4934
5986
|
const decisive = surfaces.find(
|
|
@@ -5042,14 +6094,14 @@ var d16 = {
|
|
|
5042
6094
|
};
|
|
5043
6095
|
function readPackageVersion(root) {
|
|
5044
6096
|
try {
|
|
5045
|
-
let dir = statSync4(root).isDirectory() ? root :
|
|
6097
|
+
let dir = statSync4(root).isDirectory() ? root : dirname6(root);
|
|
5046
6098
|
for (let depth = 0; depth < 8; depth++) {
|
|
5047
6099
|
const candidate = join10(dir, "package.json");
|
|
5048
6100
|
if (existsSync10(candidate)) {
|
|
5049
6101
|
const pkg = JSON.parse(readFileSync8(candidate, "utf8"));
|
|
5050
6102
|
return typeof pkg.version === "string" ? pkg.version : null;
|
|
5051
6103
|
}
|
|
5052
|
-
const parent =
|
|
6104
|
+
const parent = dirname6(dir);
|
|
5053
6105
|
if (parent === dir) break;
|
|
5054
6106
|
dir = parent;
|
|
5055
6107
|
}
|
|
@@ -5061,7 +6113,7 @@ function readPackageVersion(root) {
|
|
|
5061
6113
|
|
|
5062
6114
|
// src/detectors/dAppI18n.ts
|
|
5063
6115
|
import { existsSync as existsSync11, readFileSync as readFileSync9 } from "fs";
|
|
5064
|
-
import { dirname as
|
|
6116
|
+
import { dirname as dirname7, join as join11 } from "path";
|
|
5065
6117
|
function flattenKeys(obj, prefix = "") {
|
|
5066
6118
|
if (obj === null || typeof obj !== "object" || Array.isArray(obj)) {
|
|
5067
6119
|
return prefix ? [prefix] : [];
|
|
@@ -5108,7 +6160,7 @@ var dAppI18n = {
|
|
|
5108
6160
|
`DAppI18n key-diff control FAILED: detectsGap=${control.detectsGap} detectsComplete=${control.detectsComplete} \u2014 matcher broken, verdict untrustworthy`
|
|
5109
6161
|
);
|
|
5110
6162
|
}
|
|
5111
|
-
const root = tree.packageJson ?
|
|
6163
|
+
const root = tree.packageJson ? dirname7(tree.packageJson.path) : tree.root;
|
|
5112
6164
|
const i18nDir = join11(root, "i18n");
|
|
5113
6165
|
const enPath = join11(i18nDir, "en.json");
|
|
5114
6166
|
const frPath = join11(i18nDir, "fr.json");
|
|
@@ -5981,7 +7033,7 @@ var dExt4 = {
|
|
|
5981
7033
|
|
|
5982
7034
|
// src/detectors/dExt5.ts
|
|
5983
7035
|
import { existsSync as existsSync12 } from "fs";
|
|
5984
|
-
import { dirname as
|
|
7036
|
+
import { dirname as dirname8, join as join14 } from "path";
|
|
5985
7037
|
function nonEmptyString(v) {
|
|
5986
7038
|
return typeof v === "string" && v.trim().length > 0;
|
|
5987
7039
|
}
|
|
@@ -6018,7 +7070,7 @@ var dExt5 = {
|
|
|
6018
7070
|
}
|
|
6019
7071
|
for (const manifestPath2 of manifests) {
|
|
6020
7072
|
const shortPath = manifestPath2.split("/").slice(-2).join("/");
|
|
6021
|
-
const root =
|
|
7073
|
+
const root = dirname8(manifestPath2);
|
|
6022
7074
|
const cannotRead = (reason) => ({
|
|
6023
7075
|
id: this.id,
|
|
6024
7076
|
anchor: shortPath,
|
|
@@ -6085,7 +7137,7 @@ var dExt5 = {
|
|
|
6085
7137
|
|
|
6086
7138
|
// src/detectors/dExt7.ts
|
|
6087
7139
|
import { existsSync as existsSync13 } from "fs";
|
|
6088
|
-
import { dirname as
|
|
7140
|
+
import { dirname as dirname9, join as join15 } from "path";
|
|
6089
7141
|
var ABSENCE_FLAG_FIELDS = [
|
|
6090
7142
|
"extensionAbsent",
|
|
6091
7143
|
"extension_absent",
|
|
@@ -6149,7 +7201,7 @@ var dExt7 = {
|
|
|
6149
7201
|
const unreadable = [];
|
|
6150
7202
|
const decided = (() => {
|
|
6151
7203
|
for (const manifestPath2 of manifests) {
|
|
6152
|
-
const extDir =
|
|
7204
|
+
const extDir = dirname9(manifestPath2);
|
|
6153
7205
|
const extLabel = extDir.split("/").pop() ?? extDir;
|
|
6154
7206
|
const evalsPath = join15(extDir, "evals", "evals.json");
|
|
6155
7207
|
const shortPath = `${extLabel}/evals/evals.json`;
|
|
@@ -6218,7 +7270,7 @@ var dExt7 = {
|
|
|
6218
7270
|
|
|
6219
7271
|
// src/detectors/dExt8.ts
|
|
6220
7272
|
import { existsSync as existsSync14 } from "fs";
|
|
6221
|
-
import { dirname as
|
|
7273
|
+
import { dirname as dirname10, join as join16 } from "path";
|
|
6222
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-]+)*))?$/;
|
|
6223
7275
|
var POSITIVE_CONTROL5 = "1.2.3-beta.1+build.7";
|
|
6224
7276
|
function dExt8PositiveControl() {
|
|
@@ -6250,7 +7302,7 @@ var dExt8 = {
|
|
|
6250
7302
|
let finding;
|
|
6251
7303
|
let judgedConforming = 0;
|
|
6252
7304
|
for (const manifestPath2 of manifests) {
|
|
6253
|
-
const extDir =
|
|
7305
|
+
const extDir = dirname10(manifestPath2);
|
|
6254
7306
|
const extLabel = extDir.split("/").pop() ?? extDir;
|
|
6255
7307
|
const pkgPath = join16(extDir, "package.json");
|
|
6256
7308
|
const shortPkgPath = `${extLabel}/package.json`;
|
|
@@ -6806,6 +7858,50 @@ function collectLogCalls(file) {
|
|
|
6806
7858
|
}
|
|
6807
7859
|
return { calls, unbalanced };
|
|
6808
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
|
+
};
|
|
6809
7905
|
var PIN10 = "a74a042b53c71a095ab88b3073bd47728eeda814";
|
|
6810
7906
|
var dServer6 = {
|
|
6811
7907
|
id: "DServer6 secret-hygiene",
|
|
@@ -6833,8 +7929,15 @@ var dServer6 = {
|
|
|
6833
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`
|
|
6834
7930
|
);
|
|
6835
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
|
+
}
|
|
6836
7937
|
const unreadable = [];
|
|
6837
7938
|
const decided = (() => {
|
|
7939
|
+
const findings = [];
|
|
7940
|
+
const resolved = [];
|
|
6838
7941
|
for (const file of tree.files) {
|
|
6839
7942
|
const { calls, unbalanced } = collectLogCalls(file);
|
|
6840
7943
|
for (const anchor of unbalanced) {
|
|
@@ -6851,30 +7954,20 @@ var dServer6 = {
|
|
|
6851
7954
|
});
|
|
6852
7955
|
continue;
|
|
6853
7956
|
}
|
|
6854
|
-
const
|
|
6855
|
-
|
|
6856
|
-
|
|
6857
|
-
|
|
6858
|
-
|
|
6859
|
-
|
|
6860
|
-
|
|
6861
|
-
|
|
6862
|
-
|
|
6863
|
-
|
|
6864
|
-
|
|
6865
|
-
|
|
6866
|
-
|
|
6867
|
-
|
|
6868
|
-
verdict: "Form C \u2014 log call references a PII-shaped identifier (positive-control OK) -> never log PII; redact before logging (C-6)"
|
|
6869
|
-
};
|
|
6870
|
-
}
|
|
6871
|
-
if (INTERNAL_RE.test(refs)) {
|
|
6872
|
-
return {
|
|
6873
|
-
id: this.id,
|
|
6874
|
-
anchor: call.anchor,
|
|
6875
|
-
state: "non-conforming",
|
|
6876
|
-
verdict: "Form D \u2014 log call references an internal-infrastructure identifier (positive-control OK) -> never log internal system detail; redact before logging (C-6)"
|
|
6877
|
-
};
|
|
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
|
+
});
|
|
6878
7971
|
}
|
|
6879
7972
|
}
|
|
6880
7973
|
}
|
|
@@ -6882,20 +7975,29 @@ var dServer6 = {
|
|
|
6882
7975
|
for (const line of file.lines) {
|
|
6883
7976
|
if (line.comment) continue;
|
|
6884
7977
|
if (STACK_RE.test(line.raw) && CLIENT_SINK_RE.test(line.raw)) {
|
|
6885
|
-
|
|
6886
|
-
|
|
6887
|
-
|
|
6888
|
-
|
|
6889
|
-
|
|
6890
|
-
};
|
|
7978
|
+
findings.push({
|
|
7979
|
+
site: `${file.rel}:${line.n}`,
|
|
7980
|
+
form: "B",
|
|
7981
|
+
note: "client-facing stack"
|
|
7982
|
+
});
|
|
6891
7983
|
}
|
|
6892
7984
|
}
|
|
6893
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
|
+
}
|
|
6894
7996
|
return {
|
|
6895
7997
|
id: this.id,
|
|
6896
7998
|
anchor: tree.files.length > 0 ? `${tree.files[0].rel}:1` : tree.root,
|
|
6897
7999
|
state: "present",
|
|
6898
|
-
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).`
|
|
6899
8001
|
};
|
|
6900
8002
|
})();
|
|
6901
8003
|
return settle(this.id, decided, unreadable);
|
|
@@ -7051,7 +8153,7 @@ var PRM_ROUTE_RE = /(?:app|router|server)\.(?:get|route|all)\s*\(\s*["'`][^"'`]*
|
|
|
7051
8153
|
var CLERK_RE = /clerk/i;
|
|
7052
8154
|
var AS_DISCOVERY_RE = /authorizationServers|authorization_servers|openid-configuration|oauth-authorization-server/i;
|
|
7053
8155
|
var JWKS_RE = /jwks|createRemoteJWKSet|createLocalJWKSet/i;
|
|
7054
|
-
var RS256_RE = /RS256|jwtVerify|verifyMcpBearer|verifyBearer/;
|
|
8156
|
+
var RS256_RE = /RS256|jwtVerify|verifyMcpBearer|verifyBearer|verifyMcpAccessToken/;
|
|
7055
8157
|
var AUDIENCE_RE = /audience|\baud\b/i;
|
|
7056
8158
|
var ISSUER_RE = /issuer/i;
|
|
7057
8159
|
var REUSE_RE = /@vantageos\/cloud-identity/;
|
|
@@ -7085,13 +8187,163 @@ var CONTROLS4 = [
|
|
|
7085
8187
|
negative: 'from "@vantageos/other-kit"'
|
|
7086
8188
|
}
|
|
7087
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
|
+
}
|
|
7088
8325
|
function dServer14PositiveControls() {
|
|
7089
|
-
|
|
8326
|
+
const base = CONTROLS4.map((c) => ({
|
|
7090
8327
|
key: c.key,
|
|
7091
8328
|
pattern: c.re.source,
|
|
7092
8329
|
positive: c.re.test(c.positive),
|
|
7093
8330
|
negative: c.re.test(c.negative)
|
|
7094
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;
|
|
7095
8347
|
}
|
|
7096
8348
|
function codeBlob(tree) {
|
|
7097
8349
|
const parts = [];
|
|
@@ -7134,6 +8386,8 @@ var dServer14 = {
|
|
|
7134
8386
|
);
|
|
7135
8387
|
}
|
|
7136
8388
|
}
|
|
8389
|
+
const pkgVerdict = packageRule(tree, this.id);
|
|
8390
|
+
if (pkgVerdict) return pkgVerdict;
|
|
7137
8391
|
const blob = codeBlob(tree);
|
|
7138
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);
|
|
7139
8393
|
if (!surfacePresent) {
|
|
@@ -7144,6 +8398,17 @@ var dServer14 = {
|
|
|
7144
8398
|
verdict: "no OAuth 2.1 resource-server surface detected (n/a for Server Rule 14)"
|
|
7145
8399
|
};
|
|
7146
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
|
+
}
|
|
7147
8412
|
if (!CHALLENGE_PARAM_RE.test(blob)) {
|
|
7148
8413
|
const computedAnchor = firstAnchor(tree, COMPUTED_CHALLENGE_RE);
|
|
7149
8414
|
if (computedAnchor) {
|
|
@@ -7160,13 +8425,13 @@ var dServer14 = {
|
|
|
7160
8425
|
const elements = [
|
|
7161
8426
|
{
|
|
7162
8427
|
name: "Element 1 CHALLENGE (401 + WWW-Authenticate: Bearer \u2026 resource_metadata=\u2026)",
|
|
7163
|
-
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),
|
|
7164
8429
|
probe: WWW_AUTH_RE,
|
|
7165
8430
|
fix: "answer 401 with WWW-Authenticate: Bearer \u2026 resource_metadata=\u2026 (RFC 9728)"
|
|
7166
8431
|
},
|
|
7167
8432
|
{
|
|
7168
8433
|
name: "Element 2 PRM ROUTE (/.well-known/oauth-protected-resource)",
|
|
7169
|
-
present: PRM_ROUTE_RE.test(blob),
|
|
8434
|
+
present: PRM_ROUTE_RE.test(blob) || prmRouteViaConstant(tree),
|
|
7170
8435
|
probe: PRM_ROUTE_RE,
|
|
7171
8436
|
fix: "serve /.well-known/oauth-protected-resource (Protected Resource Metadata, RFC 9728)"
|
|
7172
8437
|
},
|
|
@@ -7218,7 +8483,7 @@ var dServer14 = {
|
|
|
7218
8483
|
|
|
7219
8484
|
// src/detectors/s28-claim-forwarded.ts
|
|
7220
8485
|
import { existsSync as existsSync15, readFileSync as readFileSync11, readdirSync as readdirSync7, statSync as statSync8 } from "fs";
|
|
7221
|
-
import { dirname as
|
|
8486
|
+
import { dirname as dirname11, join as join18, relative as relative5 } from "path";
|
|
7222
8487
|
var PIN11 = "bacc54a53d5f6f9f20e5254908e2b46807902faa";
|
|
7223
8488
|
var CLAIM_TOKEN = /\bverified(?:Org|Person)\b/;
|
|
7224
8489
|
var ANY_CLAIM = /\bverified(?:Actor|Org|Person)\b/g;
|
|
@@ -7300,14 +8565,14 @@ function listTs(dir, out = []) {
|
|
|
7300
8565
|
function discoverBackend(root) {
|
|
7301
8566
|
let dir = root;
|
|
7302
8567
|
try {
|
|
7303
|
-
if (!statSync8(root).isDirectory()) dir =
|
|
8568
|
+
if (!statSync8(root).isDirectory()) dir = dirname11(root);
|
|
7304
8569
|
} catch {
|
|
7305
8570
|
return void 0;
|
|
7306
8571
|
}
|
|
7307
8572
|
for (let up = 0; up < 5; up++) {
|
|
7308
8573
|
const cand = join18(dir, "convex");
|
|
7309
8574
|
if (existsSync15(join18(cand, "schema.ts"))) return cand;
|
|
7310
|
-
const parent =
|
|
8575
|
+
const parent = dirname11(dir);
|
|
7311
8576
|
if (parent === dir) break;
|
|
7312
8577
|
dir = parent;
|
|
7313
8578
|
}
|
|
@@ -7324,11 +8589,11 @@ function readDoors(backendRoot) {
|
|
|
7324
8589
|
}
|
|
7325
8590
|
const file = {
|
|
7326
8591
|
path,
|
|
7327
|
-
rel:
|
|
8592
|
+
rel: relative5(backendRoot, path),
|
|
7328
8593
|
lines: text.split("\n").map((raw, i) => ({ n: i + 1, raw, comment: false }))
|
|
7329
8594
|
};
|
|
7330
8595
|
const m = masked(file);
|
|
7331
|
-
const module =
|
|
8596
|
+
const module = relative5(backendRoot, path).replace(/\.ts$/, "").split("\\").join("/");
|
|
7332
8597
|
const re = /\bexport\s+const\s+([A-Za-z_$][\w$]*)\s*=\s*(?:query|mutation|action)\s*\(/g;
|
|
7333
8598
|
for (let hit = re.exec(m); hit; hit = re.exec(m)) {
|
|
7334
8599
|
const open = hit.index + hit[0].length - 1;
|
|
@@ -7382,8 +8647,8 @@ function readDoors(backendRoot) {
|
|
|
7382
8647
|
return doors;
|
|
7383
8648
|
}
|
|
7384
8649
|
function definitionOf(name, files) {
|
|
7385
|
-
const
|
|
7386
|
-
const re = new RegExp(`(?:\\b(?:const|let|var)\\s+${
|
|
8650
|
+
const esc2 = name.replace(/\$/g, "\\$");
|
|
8651
|
+
const re = new RegExp(`(?:\\b(?:const|let|var)\\s+${esc2}\\b|\\bfunction\\s+${esc2}\\b)`);
|
|
7387
8652
|
for (const f of files) {
|
|
7388
8653
|
const hit = re.exec(f.m);
|
|
7389
8654
|
if (!hit) continue;
|
|
@@ -7700,17 +8965,17 @@ var detectors = [
|
|
|
7700
8965
|
// src/runtime/launch.ts
|
|
7701
8966
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
7702
8967
|
import { existsSync as existsSync16, readFileSync as readFileSync12, statSync as statSync9 } from "fs";
|
|
7703
|
-
import { dirname as
|
|
8968
|
+
import { dirname as dirname12, isAbsolute, join as join19, resolve as resolve2 } from "path";
|
|
7704
8969
|
function findPackageDir(target) {
|
|
7705
8970
|
let dir;
|
|
7706
8971
|
try {
|
|
7707
|
-
dir = statSync9(target).isDirectory() ? target :
|
|
8972
|
+
dir = statSync9(target).isDirectory() ? target : dirname12(target);
|
|
7708
8973
|
} catch {
|
|
7709
8974
|
return null;
|
|
7710
8975
|
}
|
|
7711
8976
|
for (let depth = 0; depth < 8; depth++) {
|
|
7712
8977
|
if (existsSync16(join19(dir, "package.json"))) return dir;
|
|
7713
|
-
const parent =
|
|
8978
|
+
const parent = dirname12(dir);
|
|
7714
8979
|
if (parent === dir) break;
|
|
7715
8980
|
dir = parent;
|
|
7716
8981
|
}
|
|
@@ -7744,7 +9009,7 @@ function deriveLaunchPlan(target) {
|
|
|
7744
9009
|
if (bin) {
|
|
7745
9010
|
return {
|
|
7746
9011
|
command: process.execPath,
|
|
7747
|
-
args: [isAbsolute(bin) ? bin :
|
|
9012
|
+
args: [isAbsolute(bin) ? bin : resolve2(pkgDir, bin)],
|
|
7748
9013
|
cwd: pkgDir
|
|
7749
9014
|
};
|
|
7750
9015
|
}
|
|
@@ -7757,7 +9022,7 @@ function deriveLaunchPlan(target) {
|
|
|
7757
9022
|
if (main2) {
|
|
7758
9023
|
return {
|
|
7759
9024
|
command: process.execPath,
|
|
7760
|
-
args: [isAbsolute(main2) ? main2 :
|
|
9025
|
+
args: [isAbsolute(main2) ? main2 : resolve2(pkgDir, main2)],
|
|
7761
9026
|
cwd: pkgDir
|
|
7762
9027
|
};
|
|
7763
9028
|
}
|
|
@@ -7826,7 +9091,7 @@ function readEvalCounts(target) {
|
|
|
7826
9091
|
}
|
|
7827
9092
|
return counts;
|
|
7828
9093
|
}
|
|
7829
|
-
const parent =
|
|
9094
|
+
const parent = dirname12(dir);
|
|
7830
9095
|
if (parent === dir) break;
|
|
7831
9096
|
dir = parent;
|
|
7832
9097
|
}
|
|
@@ -8103,7 +9368,7 @@ var USAGE = [
|
|
|
8103
9368
|
].join("\n");
|
|
8104
9369
|
function readVersion() {
|
|
8105
9370
|
try {
|
|
8106
|
-
const here =
|
|
9371
|
+
const here = dirname13(fileURLToPath(import.meta.url));
|
|
8107
9372
|
const manifest = JSON.parse(readFileSync13(join20(here, "..", "package.json"), "utf8"));
|
|
8108
9373
|
return manifest.version ?? null;
|
|
8109
9374
|
} catch {
|
|
@@ -8167,7 +9432,7 @@ function isDirectInvocation() {
|
|
|
8167
9432
|
return false;
|
|
8168
9433
|
}
|
|
8169
9434
|
try {
|
|
8170
|
-
return realpathSync(
|
|
9435
|
+
return realpathSync(resolve3(entry)) === selfReal;
|
|
8171
9436
|
} catch {
|
|
8172
9437
|
return false;
|
|
8173
9438
|
}
|