@vantageos/mcp-doctor 0.4.0 → 0.6.0

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