@vantageos/mcp-doctor 0.3.2 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +55 -0
- package/README.md +26 -4
- package/dist/doctor.d.ts +46 -2
- package/dist/doctor.js +1678 -486
- package/package.json +1 -1
package/dist/doctor.js
CHANGED
|
@@ -1,13 +1,999 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/doctor.ts
|
|
4
|
-
import { readFileSync as
|
|
5
|
-
import { dirname as
|
|
4
|
+
import { readFileSync as readFileSync13, realpathSync } from "fs";
|
|
5
|
+
import { dirname as dirname12, join as join20, resolve as resolve2 } from "path";
|
|
6
6
|
import { fileURLToPath } from "url";
|
|
7
7
|
|
|
8
|
+
// src/scan.ts
|
|
9
|
+
import { existsSync, readFileSync, readdirSync, statSync } from "fs";
|
|
10
|
+
import { basename, dirname, join, relative, sep } from "path";
|
|
11
|
+
var SKIP_DIR_INFO = {
|
|
12
|
+
node_modules: { reason: "vendored dependencies", authored: false },
|
|
13
|
+
dist: { reason: "build output", authored: false },
|
|
14
|
+
build: { reason: "build output", authored: false },
|
|
15
|
+
".git": { reason: "version-control metadata", authored: false },
|
|
16
|
+
coverage: { reason: "generated coverage output", authored: false },
|
|
17
|
+
// `authored: true` marks content the target's own authors wrote, as opposed
|
|
18
|
+
// to vendored or generated content. A walk that looks for a site (C-26) may
|
|
19
|
+
// probe an authored directory it skips: a deployable there would otherwise
|
|
20
|
+
// be reported as if it did not exist.
|
|
21
|
+
tests: { reason: "test material", authored: true },
|
|
22
|
+
__tests__: { reason: "test material", authored: true },
|
|
23
|
+
// Test material, not a target's served surface: deliberately-malformed
|
|
24
|
+
// servers and foreign copies live here. Scanning them makes the instrument
|
|
25
|
+
// trip its own fail-closed detectors when pointed at its own tree, and lints
|
|
26
|
+
// inputs that are meant to be broken. A real target's fixtures are equally
|
|
27
|
+
// test inputs, not its product surface.
|
|
28
|
+
fixtures: { reason: "test material (fixtures)", authored: true }
|
|
29
|
+
};
|
|
30
|
+
var SKIP_DIRS = new Set(Object.keys(SKIP_DIR_INFO));
|
|
31
|
+
function recordSkip(tree, dir, root, reason) {
|
|
32
|
+
const path = relative(root, dir).split(sep).join("/");
|
|
33
|
+
const existing = tree.skippedDirs.find((d) => d.path === path);
|
|
34
|
+
if (existing) return existing;
|
|
35
|
+
const entry = {
|
|
36
|
+
path,
|
|
37
|
+
reason: reason ?? SKIP_DIR_INFO[basename(dir)]?.reason ?? "skipped"
|
|
38
|
+
};
|
|
39
|
+
tree.skippedDirs.push(entry);
|
|
40
|
+
return entry;
|
|
41
|
+
}
|
|
42
|
+
function isCommentLine(raw) {
|
|
43
|
+
const t = raw.trim();
|
|
44
|
+
if (t === "") return false;
|
|
45
|
+
return t.startsWith("*") || t.startsWith("//") || t.startsWith("/*") || t.startsWith("*/");
|
|
46
|
+
}
|
|
47
|
+
function walk(dir, out, onSkip) {
|
|
48
|
+
let entries;
|
|
49
|
+
try {
|
|
50
|
+
entries = readdirSync(dir);
|
|
51
|
+
} catch {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
for (const name of entries) {
|
|
55
|
+
const full = join(dir, name);
|
|
56
|
+
let st;
|
|
57
|
+
try {
|
|
58
|
+
st = statSync(full);
|
|
59
|
+
} catch {
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (st.isDirectory()) {
|
|
63
|
+
if (SKIP_DIRS.has(name)) {
|
|
64
|
+
onSkip(full);
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
walk(full, out, onSkip);
|
|
68
|
+
} else if (name.endsWith(".ts") || name.endsWith(".tsx")) {
|
|
69
|
+
if (name.endsWith(".d.ts") || name.endsWith(".test.ts")) continue;
|
|
70
|
+
out.push(full);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
function loadFile(path, root) {
|
|
75
|
+
const raw = readFileSync(path, "utf8");
|
|
76
|
+
const lines = raw.split("\n").map((r, i) => ({ n: i + 1, raw: r, comment: isCommentLine(r) }));
|
|
77
|
+
const file = { path, rel: relative(root, path) || path, lines };
|
|
78
|
+
const mask = maskOf(file);
|
|
79
|
+
for (const line of lines) {
|
|
80
|
+
if (line.comment) continue;
|
|
81
|
+
const row = mask[line.n - 1];
|
|
82
|
+
let any = false;
|
|
83
|
+
let allComment = true;
|
|
84
|
+
for (let c = 0; c < line.raw.length; c++) {
|
|
85
|
+
if (line.raw[c] === " " || line.raw[c] === " " || line.raw[c] === "\r") continue;
|
|
86
|
+
any = true;
|
|
87
|
+
if (row[c] !== COM) {
|
|
88
|
+
allComment = false;
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (any && allComment) line.comment = true;
|
|
93
|
+
}
|
|
94
|
+
return file;
|
|
95
|
+
}
|
|
96
|
+
function findPackageJson(startDir) {
|
|
97
|
+
let dir = startDir;
|
|
98
|
+
for (let depth = 0; depth < 8; depth++) {
|
|
99
|
+
const candidate = join(dir, "package.json");
|
|
100
|
+
if (existsSync(candidate)) {
|
|
101
|
+
try {
|
|
102
|
+
const parsed = JSON.parse(readFileSync(candidate, "utf8"));
|
|
103
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
104
|
+
return { path: candidate, json: {}, unreadable: "not a JSON object" };
|
|
105
|
+
}
|
|
106
|
+
return { path: candidate, json: parsed };
|
|
107
|
+
} catch (err) {
|
|
108
|
+
return { path: candidate, json: {}, unreadable: err.message };
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
const parent = dirname(dir);
|
|
112
|
+
if (parent === dir) break;
|
|
113
|
+
dir = parent;
|
|
114
|
+
}
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
function loadTree(root, backendRoot) {
|
|
118
|
+
const st = statSync(root);
|
|
119
|
+
const baseDir = st.isDirectory() ? root : dirname(root);
|
|
120
|
+
const filePaths = [];
|
|
121
|
+
const tree = { root, files: [], packageJson: null, skippedDirs: [] };
|
|
122
|
+
if (backendRoot !== void 0) tree.backendRoot = backendRoot;
|
|
123
|
+
if (st.isDirectory()) walk(root, filePaths, (d) => recordSkip(tree, d, root));
|
|
124
|
+
else if (root.endsWith(".ts") || root.endsWith(".tsx")) filePaths.push(root);
|
|
125
|
+
tree.files = filePaths.map((p) => loadFile(p, baseDir));
|
|
126
|
+
tree.packageJson = findPackageJson(baseDir);
|
|
127
|
+
return tree;
|
|
128
|
+
}
|
|
129
|
+
var COM = 1;
|
|
130
|
+
var CODE = 2;
|
|
131
|
+
var maskCache = /* @__PURE__ */ new WeakMap();
|
|
132
|
+
function buildCodeMask(file) {
|
|
133
|
+
const mask = [];
|
|
134
|
+
let mode = "code";
|
|
135
|
+
const tplStack = [];
|
|
136
|
+
let braceDepth = 0;
|
|
137
|
+
let prevSig = "";
|
|
138
|
+
let prevWord = "";
|
|
139
|
+
const regexAfterKeyword = /* @__PURE__ */ new Set([
|
|
140
|
+
"case",
|
|
141
|
+
"default",
|
|
142
|
+
"delete",
|
|
143
|
+
"do",
|
|
144
|
+
"else",
|
|
145
|
+
"extends",
|
|
146
|
+
"in",
|
|
147
|
+
"instanceof",
|
|
148
|
+
"new",
|
|
149
|
+
"of",
|
|
150
|
+
"return",
|
|
151
|
+
"throw",
|
|
152
|
+
"typeof",
|
|
153
|
+
"void",
|
|
154
|
+
"yield",
|
|
155
|
+
"await"
|
|
156
|
+
]);
|
|
157
|
+
const regexAfter = /* @__PURE__ */ new Set([
|
|
158
|
+
"",
|
|
159
|
+
"(",
|
|
160
|
+
",",
|
|
161
|
+
"=",
|
|
162
|
+
":",
|
|
163
|
+
"[",
|
|
164
|
+
"!",
|
|
165
|
+
"&",
|
|
166
|
+
"|",
|
|
167
|
+
"?",
|
|
168
|
+
"{",
|
|
169
|
+
"}",
|
|
170
|
+
";",
|
|
171
|
+
"+",
|
|
172
|
+
"-",
|
|
173
|
+
"*",
|
|
174
|
+
"%",
|
|
175
|
+
"<",
|
|
176
|
+
">",
|
|
177
|
+
"~",
|
|
178
|
+
"^"
|
|
179
|
+
]);
|
|
180
|
+
for (const line of file.lines) {
|
|
181
|
+
const s = line.raw;
|
|
182
|
+
const row = new Uint8Array(s.length);
|
|
183
|
+
let i = 0;
|
|
184
|
+
while (i < s.length) {
|
|
185
|
+
const c = s[i];
|
|
186
|
+
const n = s[i + 1];
|
|
187
|
+
if (mode === "block") {
|
|
188
|
+
row[i] = COM;
|
|
189
|
+
if (c === "*" && n === "/") {
|
|
190
|
+
row[i + 1] = COM;
|
|
191
|
+
i += 2;
|
|
192
|
+
mode = "code";
|
|
193
|
+
} else i++;
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
if (mode === "sq" || mode === "dq") {
|
|
197
|
+
if (c === "\\") i += 2;
|
|
198
|
+
else if (mode === "sq" && c === "'" || mode === "dq" && c === '"') {
|
|
199
|
+
row[i] = CODE;
|
|
200
|
+
i++;
|
|
201
|
+
mode = "code";
|
|
202
|
+
prevSig = c;
|
|
203
|
+
} else i++;
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
if (mode === "tpl") {
|
|
207
|
+
if (c === "\\") i += 2;
|
|
208
|
+
else if (c === "`") {
|
|
209
|
+
row[i] = CODE;
|
|
210
|
+
i++;
|
|
211
|
+
mode = "code";
|
|
212
|
+
prevSig = c;
|
|
213
|
+
} else if (c === "$" && n === "{") {
|
|
214
|
+
row[i] = CODE;
|
|
215
|
+
row[i + 1] = CODE;
|
|
216
|
+
tplStack.push(braceDepth);
|
|
217
|
+
braceDepth++;
|
|
218
|
+
i += 2;
|
|
219
|
+
mode = "code";
|
|
220
|
+
prevSig = "{";
|
|
221
|
+
} else i++;
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
if (mode === "re" || mode === "recls") {
|
|
225
|
+
if (c === "\\") i += 2;
|
|
226
|
+
else if (mode === "re" && c === "[") {
|
|
227
|
+
mode = "recls";
|
|
228
|
+
i++;
|
|
229
|
+
} else if (mode === "recls" && c === "]") {
|
|
230
|
+
mode = "re";
|
|
231
|
+
i++;
|
|
232
|
+
} else if (mode === "re" && c === "/") {
|
|
233
|
+
row[i] = CODE;
|
|
234
|
+
i++;
|
|
235
|
+
mode = "code";
|
|
236
|
+
prevSig = "/";
|
|
237
|
+
} else i++;
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
if (c === "/" && n === "/") {
|
|
241
|
+
row.fill(COM, i);
|
|
242
|
+
break;
|
|
243
|
+
}
|
|
244
|
+
if (c === "/" && n === "*") {
|
|
245
|
+
mode = "block";
|
|
246
|
+
row[i] = COM;
|
|
247
|
+
row[i + 1] = COM;
|
|
248
|
+
i += 2;
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
row[i] = CODE;
|
|
252
|
+
const wordBefore = prevWord;
|
|
253
|
+
if (c !== " " && c !== " ") prevWord = "";
|
|
254
|
+
if (/[A-Za-z_$]/.test(c)) {
|
|
255
|
+
let j = i + 1;
|
|
256
|
+
while (j < s.length && /[\w$]/.test(s[j])) j++;
|
|
257
|
+
for (let k = i; k < j; k++) row[k] = CODE;
|
|
258
|
+
prevWord = prevSig === "." ? "" : s.slice(i, j);
|
|
259
|
+
prevSig = s[j - 1];
|
|
260
|
+
i = j;
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
if (c === '"' || c === "'" || c === "`") {
|
|
264
|
+
mode = c === '"' ? "dq" : c === "'" ? "sq" : "tpl";
|
|
265
|
+
i++;
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
if (c === "/" && (regexAfter.has(prevSig) || regexAfterKeyword.has(wordBefore))) {
|
|
269
|
+
mode = "re";
|
|
270
|
+
i++;
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
if (c === "{") braceDepth++;
|
|
274
|
+
else if (c === "}") {
|
|
275
|
+
braceDepth--;
|
|
276
|
+
if (tplStack.length > 0 && tplStack[tplStack.length - 1] === braceDepth) {
|
|
277
|
+
tplStack.pop();
|
|
278
|
+
mode = "tpl";
|
|
279
|
+
i++;
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
if (c !== " " && c !== " ") prevSig = c;
|
|
284
|
+
i++;
|
|
285
|
+
}
|
|
286
|
+
if (mode === "sq" || mode === "dq" || mode === "re" || mode === "recls") mode = "code";
|
|
287
|
+
mask.push(row);
|
|
288
|
+
}
|
|
289
|
+
return mask;
|
|
290
|
+
}
|
|
291
|
+
function maskOf(file) {
|
|
292
|
+
let mask = maskCache.get(file);
|
|
293
|
+
if (!mask) {
|
|
294
|
+
mask = buildCodeMask(file);
|
|
295
|
+
maskCache.set(file, mask);
|
|
296
|
+
}
|
|
297
|
+
return mask;
|
|
298
|
+
}
|
|
299
|
+
function isCodeAt(file, n, col) {
|
|
300
|
+
const mask = maskOf(file);
|
|
301
|
+
return mask[n - 1]?.[col] === CODE;
|
|
302
|
+
}
|
|
303
|
+
function matchInCode(file, line, re) {
|
|
304
|
+
const g = new RegExp(re.source, re.flags.includes("g") ? re.flags : `${re.flags}g`);
|
|
305
|
+
for (const m of line.raw.matchAll(g)) {
|
|
306
|
+
if (isCodeAt(file, line.n, m.index ?? 0)) return m;
|
|
307
|
+
}
|
|
308
|
+
return null;
|
|
309
|
+
}
|
|
310
|
+
function testInCode(file, line, re) {
|
|
311
|
+
return matchInCode(file, line, re) !== null;
|
|
312
|
+
}
|
|
313
|
+
function countInCode(file, line, ch) {
|
|
314
|
+
let n = 0;
|
|
315
|
+
for (let c = 0; c < line.raw.length; c++) {
|
|
316
|
+
if (line.raw[c] === ch && isCodeAt(file, line.n, c)) n++;
|
|
317
|
+
}
|
|
318
|
+
return n;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// src/detectors/couldNotJudge.ts
|
|
322
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
323
|
+
function couldNotJudge(id, anchor, reason) {
|
|
324
|
+
return {
|
|
325
|
+
id,
|
|
326
|
+
anchor,
|
|
327
|
+
state: "could-not-judge",
|
|
328
|
+
reason,
|
|
329
|
+
verdict: `could not judge: ${reason}`
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
function settle(id, decided, unreadable) {
|
|
333
|
+
if (unreadable.length === 0) return decided;
|
|
334
|
+
const named = unreadable.map((u) => `${u.anchor}: ${u.reason}`).join("; ");
|
|
335
|
+
if (decided.state === "non-conforming") {
|
|
336
|
+
return { ...decided, verdict: `${decided.verdict} [also could not read: ${named}]` };
|
|
337
|
+
}
|
|
338
|
+
const judgedRest = decided.state === "could-not-judge" ? ` ${decided.reason ?? decided.verdict}` : ` The readable remainder judged ${decided.state}: ${decided.verdict}`;
|
|
339
|
+
return couldNotJudge(
|
|
340
|
+
id,
|
|
341
|
+
unreadable[0].anchor,
|
|
342
|
+
`${unreadable.length} input(s) present but unreadable \u2014 ${named}.${judgedRest}`
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
function readJsonObject(path) {
|
|
346
|
+
let value;
|
|
347
|
+
try {
|
|
348
|
+
value = JSON.parse(readFileSync2(path, "utf8"));
|
|
349
|
+
} catch {
|
|
350
|
+
return null;
|
|
351
|
+
}
|
|
352
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return null;
|
|
353
|
+
return value;
|
|
354
|
+
}
|
|
355
|
+
function guardUnreadablePackageJson(inner, opts) {
|
|
356
|
+
return {
|
|
357
|
+
...inner,
|
|
358
|
+
run(tree) {
|
|
359
|
+
const decided = inner.run(tree);
|
|
360
|
+
const pkg = tree.packageJson;
|
|
361
|
+
if (!pkg?.unreadable) return decided;
|
|
362
|
+
const u = {
|
|
363
|
+
anchor: pkg.path,
|
|
364
|
+
reason: `package.json is present but could not be parsed (${pkg.unreadable})`
|
|
365
|
+
};
|
|
366
|
+
if (opts.authoritative && decided.state !== "could-not-judge") {
|
|
367
|
+
return couldNotJudge(inner.id, u.anchor, `${u.anchor}: ${u.reason}`);
|
|
368
|
+
}
|
|
369
|
+
return settle(inner.id, decided, [u]);
|
|
370
|
+
}
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// src/detectors/dLocalIdentity.ts
|
|
375
|
+
var CLOUD_IDENTITY_PACKAGE = "@vantageos/cloud-identity";
|
|
376
|
+
var EXEMPT_MARKER = "allow-local-identity:";
|
|
377
|
+
var MIN_JUSTIFICATION = 8;
|
|
378
|
+
var TENANT_NAMES = "orgId|org_id|orgSlug|org_slug|tenantId|tenant_id";
|
|
379
|
+
var TENANT_PROP = new RegExp(`\\.(${TENANT_NAMES})$`, "i");
|
|
380
|
+
var unbracket = (x) => x.replace(/\??\.?\[\s*["'`]([\w$]+)["'`]\s*\]/g, ".$1").replace(/\?/g, "");
|
|
381
|
+
var FOLD_NAMES = "toLowerCase|toUpperCase|toLocaleLowerCase|toLocaleUpperCase|trim|normalize";
|
|
382
|
+
var FOLD_CALL = String.raw`(?:\s*\.\s*(?:${FOLD_NAMES})\s*\([^()]*\))*`;
|
|
383
|
+
var PATH = String.raw`[\w$.?]+(?![\w$])(?<!\.(?:${FOLD_NAMES}))`;
|
|
384
|
+
var OPERAND = String.raw`${PATH}(?:\[\s*["\`][\w$]+["\`]\s*\]\??[\w$.?]*)*${FOLD_CALL}`;
|
|
385
|
+
var NON_OPERAND = /* @__PURE__ */ new Set(["undefined", "null", "true", "false", "NaN"]);
|
|
386
|
+
var isLiteral = (x) => NON_OPERAND.has(x) || /^-?\d/.test(x);
|
|
387
|
+
var unfold = (x) => unbracket(x.replace(new RegExp(`${FOLD_CALL}$`), ""));
|
|
388
|
+
var lastSegment = (x) => x.split(".").pop() ?? "";
|
|
389
|
+
var ACTING_FIELD = /^(createdBy|assignedTo|assignee|from|fromOrchestrator|toOrchestrator|recipient|sender|callerOrchestrator|orchestrator|orchestratorName|actor|actorName|actingAs|claimedBy|callerName|agentName|fromName)$/;
|
|
390
|
+
var ACTING_BARE = /^(createdBy|assignedTo|assignee|fromOrchestrator|toOrchestrator|callerOrchestrator|actingAs|claimedBy)$/;
|
|
391
|
+
var CALLERISH = /^(caller\w*|actor\w*|acting\w*|claimed\w*|\w*Orchestrator|\w*AgentName)$/i;
|
|
392
|
+
var NAME_FIELD = /^(name|agentName|orchestratorName|displayName)$/;
|
|
393
|
+
var isActing = (x) => ACTING_FIELD.test(lastSegment(x)) && (x.includes(".") || ACTING_BARE.test(x)) || x.includes(".") && CALLERISH.test(lastSegment(x));
|
|
394
|
+
var isNameField = (x) => x.includes(".") && NAME_FIELD.test(lastSegment(x));
|
|
395
|
+
var isBareCallerish = (x) => !x.includes(".") && CALLERISH.test(x);
|
|
396
|
+
var comparesTenants = (a, b) => TENANT_PROP.test(a) || TENANT_PROP.test(b);
|
|
397
|
+
var comparesNames = (a, b) => isActing(a) && (isActing(b) || CALLERISH.test(lastSegment(b)) || isNameField(b)) || isActing(b) && (CALLERISH.test(lastSegment(a)) || isNameField(a)) || isBareCallerish(a) && isNameField(b) || isBareCallerish(b) && isNameField(a);
|
|
398
|
+
var comparesIdentity = (a, b) => comparesTenants(a, b) || comparesNames(a, b);
|
|
399
|
+
function operands(x, y) {
|
|
400
|
+
const [a, b] = [unfold(x), unfold(y)];
|
|
401
|
+
return isLiteral(a) || isLiteral(b) ? null : [a, b];
|
|
402
|
+
}
|
|
403
|
+
var CMP = String.raw`(${OPERAND})\s*[!=]==?\s*(${OPERAND})`;
|
|
404
|
+
var OBJECT_IS = String.raw`\bObject\s*\.\s*is\s*\(\s*(${OPERAND})\s*,\s*(${OPERAND})\s*\)`;
|
|
405
|
+
var acceptWith = (pred) => (m) => {
|
|
406
|
+
const ops = operands(m[1], m[2]);
|
|
407
|
+
return ops !== null && pred(ops[0], ops[1]);
|
|
408
|
+
};
|
|
409
|
+
var STRING_LITERAL = String.raw`(?:"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|\`[^\`$\\]*\`)`;
|
|
410
|
+
var LITERAL_CONCAT = new RegExp(String.raw`^${STRING_LITERAL}(?:\s*\+\s*${STRING_LITERAL})*$`);
|
|
411
|
+
function staticConstants(file) {
|
|
412
|
+
const out = /* @__PURE__ */ new Set();
|
|
413
|
+
const decl = /^(?:export\s+)?const\s+([\w$]+)\s*(?::[^=]+)?=\s*(.*)$/;
|
|
414
|
+
for (let i = 0; i < file.lines.length; i++) {
|
|
415
|
+
const m = file.lines[i].raw.match(decl);
|
|
416
|
+
if (!m) continue;
|
|
417
|
+
let init = m[2];
|
|
418
|
+
for (let j = i + 1; !/;\s*$/.test(init) && j < Math.min(file.lines.length, i + 40); j++) {
|
|
419
|
+
init += `
|
|
420
|
+
${file.lines[j].raw}`;
|
|
421
|
+
}
|
|
422
|
+
const body = init.replace(/;\s*$/, "").trim();
|
|
423
|
+
if (LITERAL_CONCAT.test(body)) out.add(m[1]);
|
|
424
|
+
}
|
|
425
|
+
return out;
|
|
426
|
+
}
|
|
427
|
+
function callArgs(text, from) {
|
|
428
|
+
const args = [];
|
|
429
|
+
let depth = 0;
|
|
430
|
+
let start = from;
|
|
431
|
+
for (let i = from; i < text.length; i++) {
|
|
432
|
+
const c = text[i];
|
|
433
|
+
if (c === '"' || c === "'" || c === "`") {
|
|
434
|
+
for (i++; i < text.length && text[i] !== c; i++) if (text[i] === "\\") i++;
|
|
435
|
+
continue;
|
|
436
|
+
}
|
|
437
|
+
if (c === "(" || c === "[" || c === "{") depth++;
|
|
438
|
+
else if (c === ")" || c === "]" || c === "}") {
|
|
439
|
+
if (depth === 0) {
|
|
440
|
+
args.push(text.slice(start, i).trim());
|
|
441
|
+
return { args, end: i + 1 };
|
|
442
|
+
}
|
|
443
|
+
depth--;
|
|
444
|
+
} else if (c === "," && depth === 0) {
|
|
445
|
+
args.push(text.slice(start, i).trim());
|
|
446
|
+
start = i + 1;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
return null;
|
|
450
|
+
}
|
|
451
|
+
function isStaticInput(expr, statics) {
|
|
452
|
+
const e = expr.trim();
|
|
453
|
+
if (LITERAL_CONCAT.test(e) || statics.has(e)) return true;
|
|
454
|
+
const wrapped = e.match(/^(?:new\s+TextEncoder\s*\(\s*\)\s*\.\s*encode|Buffer\s*\.\s*from)\s*\(/);
|
|
455
|
+
if (!wrapped) return false;
|
|
456
|
+
const inner = callArgs(e, wrapped[0].length);
|
|
457
|
+
return inner !== null && inner.end === e.length && isStaticInput(inner.args[0], statics);
|
|
458
|
+
}
|
|
459
|
+
function textFrom(ctx, extra = 12) {
|
|
460
|
+
const { file, line, idx } = ctx;
|
|
461
|
+
const tail = file.lines.slice(line.n, line.n + extra).map((l) => l.raw);
|
|
462
|
+
return [line.raw.slice(idx), ...tail].join("\n");
|
|
463
|
+
}
|
|
464
|
+
function blockFrom(text, from) {
|
|
465
|
+
const open = text.indexOf("{", from);
|
|
466
|
+
if (open < 0) return null;
|
|
467
|
+
let depth = 0;
|
|
468
|
+
for (let i = open; i < text.length; i++) {
|
|
469
|
+
const c = text[i];
|
|
470
|
+
if (c === '"' || c === "'" || c === "`") {
|
|
471
|
+
for (i++; i < text.length && text[i] !== c; i++) if (text[i] === "\\") i++;
|
|
472
|
+
continue;
|
|
473
|
+
}
|
|
474
|
+
if (c === "{") depth++;
|
|
475
|
+
else if (c === "}" && --depth === 0) return text.slice(open, i + 1);
|
|
476
|
+
}
|
|
477
|
+
return null;
|
|
478
|
+
}
|
|
479
|
+
function switchComparesIdentity(m, ctx) {
|
|
480
|
+
if (!ctx) return false;
|
|
481
|
+
const subject = unfold(m[1]);
|
|
482
|
+
const body = blockFrom(textFrom(ctx, 60), m[0].length);
|
|
483
|
+
if (body === null) return false;
|
|
484
|
+
const label = new RegExp(String.raw`\bcase\s+(${OPERAND})\s*:`, "g");
|
|
485
|
+
for (const c of body.matchAll(label)) {
|
|
486
|
+
const ops = operands(subject, c[1]);
|
|
487
|
+
if (ops !== null && comparesIdentity(ops[0], ops[1])) return true;
|
|
488
|
+
}
|
|
489
|
+
return false;
|
|
490
|
+
}
|
|
491
|
+
function literalHoldsIdentity(m) {
|
|
492
|
+
const arg = unfold(m[2]);
|
|
493
|
+
return m[1].split(",").map((e) => unfold(e.trim())).some((e) => e !== "" && !isLiteral(e) && !isLiteral(arg) && comparesIdentity(e, arg));
|
|
494
|
+
}
|
|
495
|
+
function digestsStaticInput(m, ctx) {
|
|
496
|
+
if (!ctx) return false;
|
|
497
|
+
const text = textFrom(ctx);
|
|
498
|
+
const open = text.indexOf("(");
|
|
499
|
+
if (open < 0) return false;
|
|
500
|
+
const call = callArgs(text, open + 1);
|
|
501
|
+
if (!call) return false;
|
|
502
|
+
if (/^subtle/.test(m[0])) {
|
|
503
|
+
return m[1] === "digest" && call.args.length >= 2 && isStaticInput(call.args[1], ctx.statics);
|
|
504
|
+
}
|
|
505
|
+
if (!/^createHash/.test(m[0])) return false;
|
|
506
|
+
const update = text.slice(call.end).match(/^\s*\.\s*update\s*\(/);
|
|
507
|
+
if (!update) return false;
|
|
508
|
+
const data = callArgs(text, call.end + update[0].length);
|
|
509
|
+
return data !== null && isStaticInput(data.args[0], ctx.statics);
|
|
510
|
+
}
|
|
511
|
+
var RENAMEABLE = [
|
|
512
|
+
"createHash",
|
|
513
|
+
"createHmac",
|
|
514
|
+
"timingSafeEqual",
|
|
515
|
+
"jwtVerify",
|
|
516
|
+
"jwtDecrypt",
|
|
517
|
+
"createRemoteJWKSet",
|
|
518
|
+
"createLocalJWKSet",
|
|
519
|
+
"decodeJwt",
|
|
520
|
+
"compactVerify",
|
|
521
|
+
"verifyToken"
|
|
522
|
+
];
|
|
523
|
+
function renamedPrimitives(file) {
|
|
524
|
+
const text = file.lines.map((l) => l.raw).join("\n");
|
|
525
|
+
const out = [];
|
|
526
|
+
const named = /import\s+(type\s+)?\{([^}]*)\}\s*from\s*["']([^"']+)["']/g;
|
|
527
|
+
for (const m of text.matchAll(named)) {
|
|
528
|
+
if (m[1] || m[3].startsWith(CLOUD_IDENTITY_PACKAGE)) continue;
|
|
529
|
+
for (const part of m[2].split(",")) {
|
|
530
|
+
const as = part.trim().match(/^([\w$]+)\s+as\s+([\w$]+)$/);
|
|
531
|
+
if (as && RENAMEABLE.includes(as[1])) out.push([as[1], as[2]]);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
const destructured = /(?:const|let|var)\s*\{([^}]*)\}\s*=\s*(?:await\s+)?(?:require|import)\s*\(\s*["']([^"']+)["']/g;
|
|
535
|
+
for (const m of text.matchAll(destructured)) {
|
|
536
|
+
if (m[2].startsWith(CLOUD_IDENTITY_PACKAGE)) continue;
|
|
537
|
+
for (const part of m[1].split(",")) {
|
|
538
|
+
const as = part.trim().match(/^([\w$]+)\s*:\s*([\w$]+)$/);
|
|
539
|
+
if (as && RENAMEABLE.includes(as[1])) out.push([as[1], as[2]]);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
return out;
|
|
543
|
+
}
|
|
544
|
+
var escapeRe = (x) => x.replace(/[$]/g, "\\$&");
|
|
545
|
+
function aliasMatchers(file) {
|
|
546
|
+
const out = [];
|
|
547
|
+
for (const [orig, alias] of renamedPrimitives(file)) {
|
|
548
|
+
for (const mt of MATCHERS) {
|
|
549
|
+
if (!mt.re.source.includes(orig)) continue;
|
|
550
|
+
const re = new RegExp(mt.re.source.split(orig).join(escapeRe(alias)), mt.re.flags);
|
|
551
|
+
const { accept } = mt;
|
|
552
|
+
out.push({
|
|
553
|
+
...mt,
|
|
554
|
+
re,
|
|
555
|
+
// The predicate reads the matched text; give it the original callee name.
|
|
556
|
+
accept: accept ? (m, ctx) => {
|
|
557
|
+
const fake = Object.assign(
|
|
558
|
+
[...m].map((x) => x?.replace(alias, orig)),
|
|
559
|
+
{
|
|
560
|
+
index: m.index,
|
|
561
|
+
input: m.input
|
|
562
|
+
}
|
|
563
|
+
);
|
|
564
|
+
return accept(fake, ctx);
|
|
565
|
+
} : void 0
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
return out;
|
|
570
|
+
}
|
|
571
|
+
var MATCHERS = [
|
|
572
|
+
{
|
|
573
|
+
id: "local-jwt-verify",
|
|
574
|
+
category: "resolves-caller",
|
|
575
|
+
re: /\b(jwtVerify|jwtDecrypt|createRemoteJWKSet|createLocalJWKSet|decodeJwt|compactVerify|verifyToken)\s*\(/,
|
|
576
|
+
callee: 1,
|
|
577
|
+
sample: "const { payload } = await jwtVerify(token, jwks);"
|
|
578
|
+
},
|
|
579
|
+
{
|
|
580
|
+
id: "local-jwt-verify",
|
|
581
|
+
category: "resolves-caller",
|
|
582
|
+
re: /\b(jwt|jsonwebtoken)\.(verify|decode)\s*\(/,
|
|
583
|
+
sample: "const claims = jwt.verify(token, key);"
|
|
584
|
+
},
|
|
585
|
+
{
|
|
586
|
+
id: "raw-claim-read",
|
|
587
|
+
category: "resolves-caller",
|
|
588
|
+
re: /\b(payload|claims|decoded)\??\.(org_id|org_slug|org_role|orgId|orgSlug|orgRole)\b/,
|
|
589
|
+
sample: "const orgId = payload.org_id as string;"
|
|
590
|
+
},
|
|
591
|
+
{
|
|
592
|
+
// A populated grant attached locally: a non-empty literal, or a value read
|
|
593
|
+
// off another object (a mapping row). A pass-through copy of the same
|
|
594
|
+
// field (`fromAllowList: ctx.fromAllowList`) grants nothing new.
|
|
595
|
+
id: "local-grant",
|
|
596
|
+
category: "resolves-caller",
|
|
597
|
+
re: /\bfromAllowList\s*:\s*(\[\s*[^\]\s][^\]]*\]|[\w$]+(?:\??\.[\w$]+)+)/,
|
|
598
|
+
accept: (m) => !/\.fromAllowList$/.test(m[1]),
|
|
599
|
+
sample: 'fromAllowList: ["*"],'
|
|
600
|
+
},
|
|
601
|
+
{
|
|
602
|
+
// A role compared to a value. A presence test (`orgRole !== undefined`,
|
|
603
|
+
// forwarding the claim on) asserts nothing and is not a site.
|
|
604
|
+
id: "org-role-assert",
|
|
605
|
+
category: "asserts-membership",
|
|
606
|
+
re: /\b(orgRole|org_role)\s*[!=]==\s*([^\s);&|?:]+)/,
|
|
607
|
+
accept: (m) => !NON_OPERAND.has(m[2]),
|
|
608
|
+
sample: 'if (orgRole === "org:admin") return true;'
|
|
609
|
+
},
|
|
610
|
+
{
|
|
611
|
+
id: "org-role-assert",
|
|
612
|
+
category: "asserts-membership",
|
|
613
|
+
re: /[!=]==\s*["'`]org:|\.has\(\s*\{\s*(role|permission)\s*:/,
|
|
614
|
+
sample: 'if (auth.has({ role: "org:admin" })) return true;'
|
|
615
|
+
},
|
|
616
|
+
{
|
|
617
|
+
// Identifying an agent by NAME: a typed name compared to a roster or an
|
|
618
|
+
// allow-list, by method or by iteration.
|
|
619
|
+
id: "name-allowlist",
|
|
620
|
+
category: "filters-by-scope",
|
|
621
|
+
re: /\b[\w$]*(roster|allow_?list|allowed_?orchestrators)\s*\??\.\s*(includes|indexOf|some|find|findIndex|every|has)\s*\(/i,
|
|
622
|
+
sample: "if (ctx.fromAllowList.includes(from)) return null;"
|
|
623
|
+
},
|
|
624
|
+
{
|
|
625
|
+
id: "name-allowlist",
|
|
626
|
+
category: "filters-by-scope",
|
|
627
|
+
re: /\b(isIn\w*Allow_?list)\s*\(/i,
|
|
628
|
+
callee: 1,
|
|
629
|
+
sample: "if (!isInAllowList(list, name)) return false;"
|
|
630
|
+
},
|
|
631
|
+
{
|
|
632
|
+
id: "name-allowlist",
|
|
633
|
+
category: "filters-by-scope",
|
|
634
|
+
re: /\bof\s+[\w$.?]*(roster|allow_?list|allowed_?orchestrators)\s*\)/i,
|
|
635
|
+
sample: "for (const entry of ctx.fromAllowList) {"
|
|
636
|
+
},
|
|
637
|
+
{
|
|
638
|
+
// A row kept or dropped by comparing its tenant to the caller's: ===, !==, == or !=.
|
|
639
|
+
id: "tenant-row-filter",
|
|
640
|
+
category: "filters-by-scope",
|
|
641
|
+
re: new RegExp(CMP),
|
|
642
|
+
accept: acceptWith(comparesTenants),
|
|
643
|
+
sample: "return row.orgId == ctx.actor.orgSlug;"
|
|
644
|
+
},
|
|
645
|
+
{
|
|
646
|
+
id: "tenant-row-filter",
|
|
647
|
+
category: "filters-by-scope",
|
|
648
|
+
re: new RegExp(OBJECT_IS),
|
|
649
|
+
accept: acceptWith(comparesTenants),
|
|
650
|
+
sample: "return Object.is(row.orgId, ctx.actor.orgSlug);"
|
|
651
|
+
},
|
|
652
|
+
{
|
|
653
|
+
// A tenant passed to a membership test: `callerOrgs.includes(row.orgId)`.
|
|
654
|
+
id: "tenant-row-filter",
|
|
655
|
+
category: "filters-by-scope",
|
|
656
|
+
re: new RegExp(
|
|
657
|
+
String.raw`\.(includes|has|indexOf|some|find|findIndex)\s*\(\s*(${OPERAND})\s*[,)]`
|
|
658
|
+
),
|
|
659
|
+
accept: (m) => TENANT_PROP.test(unfold(m[2])),
|
|
660
|
+
sample: "return callerOrgs.includes(row.orgId);"
|
|
661
|
+
},
|
|
662
|
+
{
|
|
663
|
+
// Identifying an agent by NAME through a normaliser: either side of a strict
|
|
664
|
+
// comparison is a call to `normalize...Id/Name/Orchestrator/Agent/...`.
|
|
665
|
+
id: "name-compare",
|
|
666
|
+
category: "filters-by-scope",
|
|
667
|
+
re: /\bnormali[sz]e\w*(id|name|orchestrator|agent|label|slug|handle)\w*\s*\([^()]*(?:\([^()]*\)[^()]*)*\)\s*[!=]==|[!=]==\s*normali[sz]e\w*(id|name|orchestrator|agent|label|slug|handle)\w*\s*\(/i,
|
|
668
|
+
sample: "return normalizeOrchestratorId(claimedName) === only;"
|
|
669
|
+
},
|
|
670
|
+
{
|
|
671
|
+
// A typed or argument name compared straight to an allow-list / roster entry.
|
|
672
|
+
id: "name-compare",
|
|
673
|
+
category: "filters-by-scope",
|
|
674
|
+
re: /(?:\b[\w$.?]*(?:roster|allow_?list|allowed_?orchestrators)\s*\??\.?\[[^\]]*\]\s*[!=]==|[!=]==\s*[\w$.?]*(?:roster|allow_?list|allowed_?orchestrators)\s*\??\.?\[)/i,
|
|
675
|
+
sample: "return claimedName === ctx.fromAllowList[0];"
|
|
676
|
+
},
|
|
677
|
+
{
|
|
678
|
+
// Identifying an agent by NAME through its acting-name fields: who made a row
|
|
679
|
+
// against who is asking. ===, !==, == and !=, case-folded or not.
|
|
680
|
+
id: "name-compare",
|
|
681
|
+
category: "filters-by-scope",
|
|
682
|
+
re: new RegExp(CMP),
|
|
683
|
+
accept: acceptWith(comparesNames),
|
|
684
|
+
sample: "return row.assignedTo.toLowerCase() !== a.callerOrchestrator.toLowerCase();"
|
|
685
|
+
},
|
|
686
|
+
{
|
|
687
|
+
id: "name-compare",
|
|
688
|
+
category: "filters-by-scope",
|
|
689
|
+
re: new RegExp(OBJECT_IS),
|
|
690
|
+
accept: acceptWith(comparesNames),
|
|
691
|
+
sample: "return Object.is(row.createdBy, m.from);"
|
|
692
|
+
},
|
|
693
|
+
{
|
|
694
|
+
// `switch (row.createdBy) { case caller: }` is the same compare, spelled as a jump table.
|
|
695
|
+
id: "name-compare",
|
|
696
|
+
category: "filters-by-scope",
|
|
697
|
+
re: new RegExp(String.raw`\bswitch\s*\(\s*(${OPERAND})\s*\)`),
|
|
698
|
+
accept: (m, ctx) => switchComparesIdentity(m, ctx),
|
|
699
|
+
sample: "switch (row.createdBy) { case caller: return true; }"
|
|
700
|
+
},
|
|
701
|
+
{
|
|
702
|
+
// `[caller].includes(row.createdBy)`: a membership test against a literal that holds the caller.
|
|
703
|
+
id: "name-compare",
|
|
704
|
+
category: "filters-by-scope",
|
|
705
|
+
re: new RegExp(
|
|
706
|
+
String.raw`(?<![\w$\])])\[([^\[\]]*)\]\s*\.\s*(?:includes|indexOf)\s*\(\s*(${OPERAND})\s*[,)]`
|
|
707
|
+
),
|
|
708
|
+
accept: (m) => literalHoldsIdentity(m),
|
|
709
|
+
sample: "return [caller].includes(row.createdBy);"
|
|
710
|
+
},
|
|
711
|
+
{
|
|
712
|
+
id: "name-compare",
|
|
713
|
+
category: "filters-by-scope",
|
|
714
|
+
re: new RegExp(
|
|
715
|
+
String.raw`\bnew\s+Set\s*\(\s*\[([^\[\]]*)\]\s*\)\s*\.\s*has\s*\(\s*(${OPERAND})\s*[,)]`
|
|
716
|
+
),
|
|
717
|
+
accept: (m) => literalHoldsIdentity(m),
|
|
718
|
+
sample: "return new Set([caller]).has(row.assignedTo);"
|
|
719
|
+
},
|
|
720
|
+
{
|
|
721
|
+
// `a.localeCompare(b) === 0` is an equality test, whichever way round it is written.
|
|
722
|
+
id: "name-compare",
|
|
723
|
+
category: "filters-by-scope",
|
|
724
|
+
re: new RegExp(
|
|
725
|
+
String.raw`(${OPERAND})\s*\.\s*localeCompare\s*\(\s*(${OPERAND})\s*(?:,[^()]*(?:\([^()]*\)[^()]*)*)?\)\s*[!=]==?\s*0(?![\w$.])`
|
|
726
|
+
),
|
|
727
|
+
accept: acceptWith(comparesIdentity),
|
|
728
|
+
sample: "return row.assignedTo.localeCompare(a.callerOrchestrator) === 0;"
|
|
729
|
+
},
|
|
730
|
+
{
|
|
731
|
+
id: "name-compare",
|
|
732
|
+
category: "filters-by-scope",
|
|
733
|
+
re: new RegExp(
|
|
734
|
+
String.raw`(?<![\w$.])0\s*[!=]==?\s*(${OPERAND})\s*\.\s*localeCompare\s*\(\s*(${OPERAND})\s*[,)]`
|
|
735
|
+
),
|
|
736
|
+
accept: acceptWith(comparesIdentity),
|
|
737
|
+
sample: "return 0 === row.assignedTo.localeCompare(a.callerOrchestrator);"
|
|
738
|
+
},
|
|
739
|
+
{
|
|
740
|
+
id: "local-secret-compare",
|
|
741
|
+
category: "validates-credential",
|
|
742
|
+
re: /\b(timingSafeEqual)\s*\(/,
|
|
743
|
+
callee: 1,
|
|
744
|
+
sample: "return timingSafeEqual(a, b);"
|
|
745
|
+
},
|
|
746
|
+
{
|
|
747
|
+
// Any digest of a presented credential, whatever the algorithm.
|
|
748
|
+
id: "local-digest",
|
|
749
|
+
category: "validates-credential",
|
|
750
|
+
// A digest over static content (a literal, or a constant made of literals) is
|
|
751
|
+
// not a digest of a credential. HMAC and sign/verify always take a key: a site.
|
|
752
|
+
re: /\bcreateHash\s*\(|\bcreateHmac\s*\(|\bsubtle\s*\.\s*(digest|sign|verify)\s*\(/,
|
|
753
|
+
accept: (m, ctx) => !digestsStaticInput(m, ctx),
|
|
754
|
+
sample: 'const h = await crypto.subtle.digest("SHA-256", data);'
|
|
755
|
+
}
|
|
756
|
+
];
|
|
757
|
+
function biteSample(mt, text) {
|
|
758
|
+
const m = text.match(mt.re);
|
|
759
|
+
if (m === null) return false;
|
|
760
|
+
if (!mt.accept) return true;
|
|
761
|
+
const line = { n: 1, raw: text, comment: false };
|
|
762
|
+
const file = { path: "sample", rel: "sample", lines: [line] };
|
|
763
|
+
return mt.accept(m, { file, line, idx: m.index ?? 0, statics: /* @__PURE__ */ new Set() });
|
|
764
|
+
}
|
|
765
|
+
function localIdentityPositiveControls() {
|
|
766
|
+
const out = {};
|
|
767
|
+
MATCHERS.forEach((mt, i) => {
|
|
768
|
+
out[`${mt.id}#${i}`] = biteSample(mt, mt.sample);
|
|
769
|
+
});
|
|
770
|
+
const bites = (line) => MATCHERS.some((mt) => biteSample(mt, line));
|
|
771
|
+
out["negative:pass-through-grant"] = !bites("fromAllowList: ctx.fromAllowList,");
|
|
772
|
+
out["negative:delegated-bearer"] = !MATCHERS.some(
|
|
773
|
+
(mt) => mt.re.test("const r = await validateMasterBearer(authHeader, secret);")
|
|
774
|
+
);
|
|
775
|
+
out["negative:role-presence-test"] = !bites(
|
|
776
|
+
"...(ctx.orgRole !== undefined ? { role: ctx.orgRole } : {}),"
|
|
777
|
+
);
|
|
778
|
+
out["negative:undefined-compare"] = !bites("if (row.orgId === undefined) return;");
|
|
779
|
+
out["negative:acting-field-vs-nullish"] = !bites("if (row.assignedTo !== undefined) return;");
|
|
780
|
+
out["negative:acting-field-vs-literal"] = !bites('return t.createdBy === "system";');
|
|
781
|
+
out["negative:unrelated-field-compare"] = !bites("return page.from === page.to;");
|
|
782
|
+
out["negative:switch-on-unrelated-field"] = !bites('switch (row.kind) { case "a": return 1; }');
|
|
783
|
+
out["negative:literal-array-unrelated"] = !bites("return [kind].includes(row.status);");
|
|
784
|
+
out["negative:localecompare-unrelated"] = !bites("return a.name.localeCompare(b.name) === 0;");
|
|
785
|
+
out["negative:object-is-nullish"] = !bites("return Object.is(row.orgId, undefined);");
|
|
786
|
+
return out;
|
|
787
|
+
}
|
|
788
|
+
function cloudIdentityBindings(file) {
|
|
789
|
+
const text = file.lines.map((l) => l.raw).join("\n");
|
|
790
|
+
const names = /* @__PURE__ */ new Set();
|
|
791
|
+
const namespaces = /* @__PURE__ */ new Set();
|
|
792
|
+
const pkg = `@vantageos\\/cloud-identity(?:\\/[^"']*)?`;
|
|
793
|
+
const named = new RegExp(`import\\s+(type\\s+)?\\{([^}]*)\\}\\s*from\\s*["']${pkg}["']`, "g");
|
|
794
|
+
for (const m of text.matchAll(named)) {
|
|
795
|
+
if (m[1]) continue;
|
|
796
|
+
for (const part of m[2].split(",")) {
|
|
797
|
+
const p = part.trim();
|
|
798
|
+
if (!p || p.startsWith("type ")) continue;
|
|
799
|
+
const as = p.match(/^[\w$]+\s+as\s+([\w$]+)$/);
|
|
800
|
+
names.add(as ? as[1] : p);
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
const ns = new RegExp(`import\\s+\\*\\s+as\\s+([\\w$]+)\\s+from\\s*["']${pkg}["']`, "g");
|
|
804
|
+
for (const m of text.matchAll(ns)) namespaces.add(m[1]);
|
|
805
|
+
return { names, namespaces };
|
|
806
|
+
}
|
|
807
|
+
function isDelegated(raw, start, callee, b) {
|
|
808
|
+
const before = raw.slice(0, start);
|
|
809
|
+
const qualified = before.match(/([\w$]+)\s*\.\s*$/);
|
|
810
|
+
if (qualified) return b.namespaces.has(qualified[1]);
|
|
811
|
+
return b.names.has(callee);
|
|
812
|
+
}
|
|
813
|
+
function countDelegatedCalls(file, b) {
|
|
814
|
+
if (b.names.size === 0 && b.namespaces.size === 0) return 0;
|
|
815
|
+
let n = 0;
|
|
816
|
+
for (const line of file.lines) {
|
|
817
|
+
if (line.comment) continue;
|
|
818
|
+
for (const m of line.raw.matchAll(/([\w$]+)\s*\(/g)) {
|
|
819
|
+
const idx = m.index ?? 0;
|
|
820
|
+
if (isCodeAt(file, line.n, idx) && isDelegated(line.raw, idx, m[1], b)) n++;
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
return n;
|
|
824
|
+
}
|
|
825
|
+
var DECL = /^(?:export\s+)?(?:default\s+)?(?:async\s+)?(?:function\s*\*?\s*([\w$]+)|(?:const|let|var|class)\s+([\w$]+))/;
|
|
826
|
+
function enclosingDecl(file, n) {
|
|
827
|
+
for (let i = n - 1; i >= 0; i--) {
|
|
828
|
+
const l = file.lines[i];
|
|
829
|
+
if (l.comment) continue;
|
|
830
|
+
const m = l.raw.match(DECL);
|
|
831
|
+
if (m) return { name: m[1] ?? m[2], line: l.n };
|
|
832
|
+
}
|
|
833
|
+
return null;
|
|
834
|
+
}
|
|
835
|
+
function justificationIn(line, file) {
|
|
836
|
+
const at = line.raw.indexOf(EXEMPT_MARKER);
|
|
837
|
+
if (at < 0) return null;
|
|
838
|
+
if (!line.comment && isCodeAt(file, line.n, at)) return null;
|
|
839
|
+
if (!line.comment && !/\/\/|\/\*/.test(line.raw.slice(0, at))) return null;
|
|
840
|
+
return line.raw.slice(at + EXEMPT_MARKER.length).replace(/\*\/\s*$/, "").trim();
|
|
841
|
+
}
|
|
842
|
+
function commentBlockAbove(file, n) {
|
|
843
|
+
const out = [];
|
|
844
|
+
for (let i = n - 2; i >= 0; i--) {
|
|
845
|
+
const l = file.lines[i];
|
|
846
|
+
if (!l.comment) break;
|
|
847
|
+
out.push(l);
|
|
848
|
+
}
|
|
849
|
+
return out;
|
|
850
|
+
}
|
|
851
|
+
function findJustification(file, line, decl) {
|
|
852
|
+
const candidates = [line, ...commentBlockAbove(file, line.n)];
|
|
853
|
+
if (decl) candidates.push(...commentBlockAbove(file, decl.line));
|
|
854
|
+
for (const c of candidates) {
|
|
855
|
+
const j = justificationIn(c, file);
|
|
856
|
+
if (j !== null) return { text: j, valid: j.length >= MIN_JUSTIFICATION };
|
|
857
|
+
}
|
|
858
|
+
return null;
|
|
859
|
+
}
|
|
860
|
+
function unreadableReason(file) {
|
|
861
|
+
if (file.lines.some((l) => l.raw.includes("\0") || l.raw.includes("\uFFFD"))) {
|
|
862
|
+
return "file is not valid UTF-8 text";
|
|
863
|
+
}
|
|
864
|
+
const pairs = [
|
|
865
|
+
["{", "}"],
|
|
866
|
+
["(", ")"],
|
|
867
|
+
["[", "]"]
|
|
868
|
+
];
|
|
869
|
+
const off = [];
|
|
870
|
+
for (const [open, close] of pairs) {
|
|
871
|
+
let net = 0;
|
|
872
|
+
for (const l of file.lines) net += countInCode(file, l, open) - countInCode(file, l, close);
|
|
873
|
+
if (net !== 0) off.push(`${open}${close} ${net > 0 ? "+" : ""}${net}`);
|
|
874
|
+
}
|
|
875
|
+
return off.length > 0 ? `brackets never balance (${off.join(", ")}) \u2014 the file cannot be read as code` : null;
|
|
876
|
+
}
|
|
877
|
+
function findLocalIdentitySites(tree) {
|
|
878
|
+
const pkg = tree.packageJson;
|
|
879
|
+
const selfExempt = !pkg?.unreadable && pkg?.json.name === CLOUD_IDENTITY_PACKAGE;
|
|
880
|
+
const scan = {
|
|
881
|
+
selfExempt,
|
|
882
|
+
violations: [],
|
|
883
|
+
exempt: [],
|
|
884
|
+
delegatedCalls: 0,
|
|
885
|
+
unreadable: []
|
|
886
|
+
};
|
|
887
|
+
for (const file of tree.files) {
|
|
888
|
+
const bad = unreadableReason(file);
|
|
889
|
+
if (bad) scan.unreadable.push({ anchor: file.rel, reason: bad });
|
|
890
|
+
const bindings = cloudIdentityBindings(file);
|
|
891
|
+
const statics = staticConstants(file);
|
|
892
|
+
const catalog = [...MATCHERS, ...aliasMatchers(file)];
|
|
893
|
+
scan.delegatedCalls += countDelegatedCalls(file, bindings);
|
|
894
|
+
for (const line of file.lines) {
|
|
895
|
+
if (line.comment) continue;
|
|
896
|
+
let hit = null;
|
|
897
|
+
for (const mt of catalog) {
|
|
898
|
+
const g = new RegExp(mt.re.source, `${mt.re.flags.replace("g", "")}g`);
|
|
899
|
+
for (const m of line.raw.matchAll(g)) {
|
|
900
|
+
const idx = m.index ?? 0;
|
|
901
|
+
if (!isCodeAt(file, line.n, idx)) continue;
|
|
902
|
+
if (mt.accept && !mt.accept(m, { file, line, idx, statics })) continue;
|
|
903
|
+
const callee = mt.callee !== void 0 ? m[mt.callee] : void 0;
|
|
904
|
+
if (callee && isDelegated(line.raw, idx, callee, bindings)) continue;
|
|
905
|
+
hit = mt;
|
|
906
|
+
break;
|
|
907
|
+
}
|
|
908
|
+
if (hit) break;
|
|
909
|
+
}
|
|
910
|
+
if (!hit) continue;
|
|
911
|
+
const decl = enclosingDecl(file, line.n);
|
|
912
|
+
const site = {
|
|
913
|
+
file: file.rel,
|
|
914
|
+
line: line.n,
|
|
915
|
+
primitive: hit.id,
|
|
916
|
+
category: hit.category,
|
|
917
|
+
enclosing: decl
|
|
918
|
+
};
|
|
919
|
+
const j = findJustification(file, line, decl);
|
|
920
|
+
if (j?.valid) scan.exempt.push({ ...site, justification: j.text });
|
|
921
|
+
else scan.violations.push(site);
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
return scan;
|
|
925
|
+
}
|
|
926
|
+
function describeSite(s) {
|
|
927
|
+
const where = s.enclosing ? ` in ${s.enclosing.name} (declared :${s.enclosing.line})` : "";
|
|
928
|
+
return `${s.file}:${s.line} [${s.category}/${s.primitive}]${where}`;
|
|
929
|
+
}
|
|
930
|
+
function exemptSuffix(exempt) {
|
|
931
|
+
if (exempt.length === 0) return "";
|
|
932
|
+
const rows = exempt.map((e) => `${describeSite(e)} \u2014 "${e.justification}"`);
|
|
933
|
+
return `; EXEMPT (${exempt.length}): ${rows.join("; ")}`;
|
|
934
|
+
}
|
|
935
|
+
function makeLocalIdentity(opts = {}) {
|
|
936
|
+
const ceiling = opts.ceiling ?? 0;
|
|
937
|
+
if (!Number.isInteger(ceiling) || ceiling < 0) {
|
|
938
|
+
throw new Error(`local-identity ceiling must be a non-negative integer, got ${ceiling}`);
|
|
939
|
+
}
|
|
940
|
+
return {
|
|
941
|
+
id: "DLocalIdentity no-local-identity",
|
|
942
|
+
run(tree) {
|
|
943
|
+
const controls = localIdentityPositiveControls();
|
|
944
|
+
const failed = Object.entries(controls).filter(([, ok]) => !ok);
|
|
945
|
+
if (failed.length > 0) {
|
|
946
|
+
throw new Error(
|
|
947
|
+
`DLocalIdentity positive control FAILED: ${failed.map(([k]) => k).join(", ")} \u2014 matcher broken, verdict untrustworthy`
|
|
948
|
+
);
|
|
949
|
+
}
|
|
950
|
+
const id = this.id;
|
|
951
|
+
const s = findLocalIdentitySites(tree);
|
|
952
|
+
if (s.selfExempt) {
|
|
953
|
+
const n = s.violations.length + s.exempt.length;
|
|
954
|
+
return {
|
|
955
|
+
id,
|
|
956
|
+
anchor: tree.packageJson?.path ?? "-",
|
|
957
|
+
state: "present",
|
|
958
|
+
verdict: `EXEMPT: the target is ${CLOUD_IDENTITY_PACKAGE} itself, the one place identity primitives live (${n} primitive site(s) not counted)`
|
|
959
|
+
};
|
|
960
|
+
}
|
|
961
|
+
const listed = s.violations.map(describeSite).join("; ");
|
|
962
|
+
const delegated = `${s.delegatedCalls} call(s) to ${CLOUD_IDENTITY_PACKAGE}`;
|
|
963
|
+
let decided;
|
|
964
|
+
if (s.violations.length > ceiling) {
|
|
965
|
+
const v = s.violations[0];
|
|
966
|
+
decided = {
|
|
967
|
+
id,
|
|
968
|
+
anchor: `${v.file}:${v.line}`,
|
|
969
|
+
state: "non-conforming",
|
|
970
|
+
verdict: `${s.violations.length} local identity primitive(s), ceiling ${ceiling} (positive-control OK): ${listed}${exemptSuffix(s.exempt)} -> delegate to ${CLOUD_IDENTITY_PACKAGE} (validateMasterBearer, verifyClerkSessionToken, isMasterScope, ...) or justify the site with "// ${EXEMPT_MARKER} <reason>"`
|
|
971
|
+
};
|
|
972
|
+
} else if (s.violations.length > 0) {
|
|
973
|
+
const v = s.violations[0];
|
|
974
|
+
decided = {
|
|
975
|
+
id,
|
|
976
|
+
anchor: `${v.file}:${v.line}`,
|
|
977
|
+
state: "present",
|
|
978
|
+
verdict: `${s.violations.length} local identity primitive(s) at or under the ceiling ${ceiling} \u2014 a ratchet, not a clean bill: ${listed}${exemptSuffix(s.exempt)}`
|
|
979
|
+
};
|
|
980
|
+
} else {
|
|
981
|
+
decided = {
|
|
982
|
+
id,
|
|
983
|
+
anchor: s.exempt[0] ? `${s.exempt[0].file}:${s.exempt[0].line}` : "-",
|
|
984
|
+
state: "present",
|
|
985
|
+
verdict: `0 local identity primitives; ${delegated} (positive-control OK)${exemptSuffix(s.exempt)}`
|
|
986
|
+
};
|
|
987
|
+
}
|
|
988
|
+
return settle(id, decided, s.unreadable);
|
|
989
|
+
}
|
|
990
|
+
};
|
|
991
|
+
}
|
|
992
|
+
var localIdentity = makeLocalIdentity();
|
|
993
|
+
|
|
8
994
|
// src/detectors/c7-artifact-verifiable.ts
|
|
9
|
-
import { existsSync, readFileSync } from "fs";
|
|
10
|
-
import { join } from "path";
|
|
995
|
+
import { existsSync as existsSync2, readFileSync as readFileSync3 } from "fs";
|
|
996
|
+
import { join as join2 } from "path";
|
|
11
997
|
var PIN = "a74a042b53c71a095ab88b3073bd47728eeda814";
|
|
12
998
|
var DOC_FILES = ["README.md", "README.fr.md"];
|
|
13
999
|
var INSTALL_RE = /\b(?:npm\s+(?:install|i)|npx|pnpm\s+add|yarn\s+add)\s+(@?[\w./-]+)/i;
|
|
@@ -46,10 +1032,10 @@ function c7PositiveControls() {
|
|
|
46
1032
|
}
|
|
47
1033
|
function readDocText(root) {
|
|
48
1034
|
for (const name of DOC_FILES) {
|
|
49
|
-
const path =
|
|
50
|
-
if (!
|
|
1035
|
+
const path = join2(root, name);
|
|
1036
|
+
if (!existsSync2(path)) continue;
|
|
51
1037
|
try {
|
|
52
|
-
return { text:
|
|
1038
|
+
return { text: readFileSync3(path, "utf8"), file: name };
|
|
53
1039
|
} catch {
|
|
54
1040
|
}
|
|
55
1041
|
}
|
|
@@ -1177,59 +2163,6 @@ var c13ContextCeiling = {
|
|
|
1177
2163
|
}
|
|
1178
2164
|
};
|
|
1179
2165
|
|
|
1180
|
-
// src/detectors/couldNotJudge.ts
|
|
1181
|
-
import { readFileSync as readFileSync2 } from "fs";
|
|
1182
|
-
function couldNotJudge(id, anchor, reason) {
|
|
1183
|
-
return {
|
|
1184
|
-
id,
|
|
1185
|
-
anchor,
|
|
1186
|
-
state: "could-not-judge",
|
|
1187
|
-
reason,
|
|
1188
|
-
verdict: `could not judge: ${reason}`
|
|
1189
|
-
};
|
|
1190
|
-
}
|
|
1191
|
-
function settle(id, decided, unreadable) {
|
|
1192
|
-
if (unreadable.length === 0) return decided;
|
|
1193
|
-
const named = unreadable.map((u) => `${u.anchor}: ${u.reason}`).join("; ");
|
|
1194
|
-
if (decided.state === "non-conforming") {
|
|
1195
|
-
return { ...decided, verdict: `${decided.verdict} [also could not read: ${named}]` };
|
|
1196
|
-
}
|
|
1197
|
-
const judgedRest = decided.state === "could-not-judge" ? ` ${decided.reason ?? decided.verdict}` : ` The readable remainder judged ${decided.state}: ${decided.verdict}`;
|
|
1198
|
-
return couldNotJudge(
|
|
1199
|
-
id,
|
|
1200
|
-
unreadable[0].anchor,
|
|
1201
|
-
`${unreadable.length} input(s) present but unreadable \u2014 ${named}.${judgedRest}`
|
|
1202
|
-
);
|
|
1203
|
-
}
|
|
1204
|
-
function readJsonObject(path) {
|
|
1205
|
-
let value;
|
|
1206
|
-
try {
|
|
1207
|
-
value = JSON.parse(readFileSync2(path, "utf8"));
|
|
1208
|
-
} catch {
|
|
1209
|
-
return null;
|
|
1210
|
-
}
|
|
1211
|
-
if (value === null || typeof value !== "object" || Array.isArray(value)) return null;
|
|
1212
|
-
return value;
|
|
1213
|
-
}
|
|
1214
|
-
function guardUnreadablePackageJson(inner, opts) {
|
|
1215
|
-
return {
|
|
1216
|
-
...inner,
|
|
1217
|
-
run(tree) {
|
|
1218
|
-
const decided = inner.run(tree);
|
|
1219
|
-
const pkg = tree.packageJson;
|
|
1220
|
-
if (!pkg?.unreadable) return decided;
|
|
1221
|
-
const u = {
|
|
1222
|
-
anchor: pkg.path,
|
|
1223
|
-
reason: `package.json is present but could not be parsed (${pkg.unreadable})`
|
|
1224
|
-
};
|
|
1225
|
-
if (opts.authoritative && decided.state !== "could-not-judge") {
|
|
1226
|
-
return couldNotJudge(inner.id, u.anchor, `${u.anchor}: ${u.reason}`);
|
|
1227
|
-
}
|
|
1228
|
-
return settle(inner.id, decided, [u]);
|
|
1229
|
-
}
|
|
1230
|
-
};
|
|
1231
|
-
}
|
|
1232
|
-
|
|
1233
2166
|
// src/detectors/c14-catalog-registration.ts
|
|
1234
2167
|
var PIN3 = "a74a042b53c71a095ab88b3073bd47728eeda814";
|
|
1235
2168
|
var REQUIRED_FIELDS = [
|
|
@@ -1514,11 +2447,11 @@ var c16UntrustedOutput = {
|
|
|
1514
2447
|
};
|
|
1515
2448
|
|
|
1516
2449
|
// src/detectors/c17-supply-chain.ts
|
|
1517
|
-
import { existsSync as
|
|
1518
|
-
import { join as
|
|
2450
|
+
import { existsSync as existsSync3, readFileSync as readFileSync4, readdirSync as readdirSync2 } from "fs";
|
|
2451
|
+
import { join as join3 } from "path";
|
|
1519
2452
|
var PIN4 = "a74a042b53c71a095ab88b3073bd47728eeda814";
|
|
1520
2453
|
var LOCKFILES = ["package-lock.json", "pnpm-lock.yaml", "yarn.lock", "bun.lock", "bun.lockb"];
|
|
1521
|
-
var WORKFLOW_DIR =
|
|
2454
|
+
var WORKFLOW_DIR = join3(".github", "workflows");
|
|
1522
2455
|
var PROVENANCE_RE2 = /--provenance\b|\bprovenance\s*:\s*true\b|id-token:\s*write/i;
|
|
1523
2456
|
function c17PositiveControls() {
|
|
1524
2457
|
const sample = "npm publish --provenance --access public";
|
|
@@ -1534,7 +2467,7 @@ function c17PositiveControls() {
|
|
|
1534
2467
|
}
|
|
1535
2468
|
function findLockfile(root) {
|
|
1536
2469
|
for (const name of LOCKFILES) {
|
|
1537
|
-
if (
|
|
2470
|
+
if (existsSync3(join3(root, name))) return name;
|
|
1538
2471
|
}
|
|
1539
2472
|
return null;
|
|
1540
2473
|
}
|
|
@@ -1546,20 +2479,20 @@ function findProvenanceClaim(tree, root) {
|
|
|
1546
2479
|
return { site: `${pkg.path}:publishConfig.provenance`, text: "provenance: true" };
|
|
1547
2480
|
}
|
|
1548
2481
|
}
|
|
1549
|
-
const workflowsDir =
|
|
1550
|
-
if (!
|
|
2482
|
+
const workflowsDir = join3(root, WORKFLOW_DIR);
|
|
2483
|
+
if (!existsSync3(workflowsDir)) return null;
|
|
1551
2484
|
let entries = [];
|
|
1552
2485
|
try {
|
|
1553
|
-
entries =
|
|
2486
|
+
entries = readdirSync2(workflowsDir);
|
|
1554
2487
|
} catch {
|
|
1555
2488
|
return null;
|
|
1556
2489
|
}
|
|
1557
2490
|
for (const name of entries) {
|
|
1558
2491
|
if (!/\.ya?ml$/.test(name)) continue;
|
|
1559
|
-
const path =
|
|
2492
|
+
const path = join3(workflowsDir, name);
|
|
1560
2493
|
let text;
|
|
1561
2494
|
try {
|
|
1562
|
-
text =
|
|
2495
|
+
text = readFileSync4(path, "utf8");
|
|
1563
2496
|
} catch {
|
|
1564
2497
|
continue;
|
|
1565
2498
|
}
|
|
@@ -1612,17 +2545,17 @@ var c17SupplyChain = guardUnreadablePackageJson(c17SupplyChainInner, {
|
|
|
1612
2545
|
});
|
|
1613
2546
|
|
|
1614
2547
|
// src/detectors/c18-structure-manifest.ts
|
|
1615
|
-
import { existsSync as
|
|
1616
|
-
import { join as
|
|
2548
|
+
import { existsSync as existsSync4, readFileSync as readFileSync5 } from "fs";
|
|
2549
|
+
import { join as join4 } from "path";
|
|
1617
2550
|
var PIN5 = "a74a042b53c71a095ab88b3073bd47728eeda814";
|
|
1618
2551
|
function manifestPath(root) {
|
|
1619
|
-
const extPath =
|
|
1620
|
-
if (
|
|
1621
|
-
return { kind: "server", path:
|
|
2552
|
+
const extPath = join4(root, "extension.json");
|
|
2553
|
+
if (existsSync4(extPath)) return { kind: "extension", path: extPath };
|
|
2554
|
+
return { kind: "server", path: join4(root, "mcp.json") };
|
|
1622
2555
|
}
|
|
1623
2556
|
function readJson(path) {
|
|
1624
2557
|
try {
|
|
1625
|
-
return JSON.parse(
|
|
2558
|
+
return JSON.parse(readFileSync5(path, "utf8"));
|
|
1626
2559
|
} catch {
|
|
1627
2560
|
return null;
|
|
1628
2561
|
}
|
|
@@ -1670,7 +2603,7 @@ var c18StructureManifestInner = {
|
|
|
1670
2603
|
}
|
|
1671
2604
|
const { kind, path } = manifestPath(tree.root);
|
|
1672
2605
|
const manifestName = kind === "extension" ? "extension.json" : "mcp.json";
|
|
1673
|
-
if (!
|
|
2606
|
+
if (!existsSync4(path)) {
|
|
1674
2607
|
return {
|
|
1675
2608
|
id: this.id,
|
|
1676
2609
|
anchor: tree.root,
|
|
@@ -1753,89 +2686,6 @@ var c18StructureManifest = guardUnreadablePackageJson(
|
|
|
1753
2686
|
{ authoritative: false }
|
|
1754
2687
|
);
|
|
1755
2688
|
|
|
1756
|
-
// src/scan.ts
|
|
1757
|
-
import { existsSync as existsSync4, readFileSync as readFileSync5, readdirSync as readdirSync2, statSync } from "fs";
|
|
1758
|
-
import { dirname, join as join4, relative } from "path";
|
|
1759
|
-
var SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
1760
|
-
"node_modules",
|
|
1761
|
-
"dist",
|
|
1762
|
-
"build",
|
|
1763
|
-
".git",
|
|
1764
|
-
"tests",
|
|
1765
|
-
"__tests__",
|
|
1766
|
-
"coverage",
|
|
1767
|
-
// Test material, not a target's served surface: deliberately-malformed
|
|
1768
|
-
// servers and foreign copies live here. Scanning them makes the instrument
|
|
1769
|
-
// trip its own fail-closed detectors when pointed at its own tree, and lints
|
|
1770
|
-
// inputs that are meant to be broken. A real target's fixtures are equally
|
|
1771
|
-
// test inputs, not its product surface.
|
|
1772
|
-
"fixtures"
|
|
1773
|
-
]);
|
|
1774
|
-
function isCommentLine(raw) {
|
|
1775
|
-
const t = raw.trim();
|
|
1776
|
-
if (t === "") return false;
|
|
1777
|
-
return t.startsWith("*") || t.startsWith("//") || t.startsWith("/*") || t.startsWith("*/");
|
|
1778
|
-
}
|
|
1779
|
-
function walk(dir, out) {
|
|
1780
|
-
let entries;
|
|
1781
|
-
try {
|
|
1782
|
-
entries = readdirSync2(dir);
|
|
1783
|
-
} catch {
|
|
1784
|
-
return;
|
|
1785
|
-
}
|
|
1786
|
-
for (const name of entries) {
|
|
1787
|
-
const full = join4(dir, name);
|
|
1788
|
-
let st;
|
|
1789
|
-
try {
|
|
1790
|
-
st = statSync(full);
|
|
1791
|
-
} catch {
|
|
1792
|
-
continue;
|
|
1793
|
-
}
|
|
1794
|
-
if (st.isDirectory()) {
|
|
1795
|
-
if (SKIP_DIRS.has(name)) continue;
|
|
1796
|
-
walk(full, out);
|
|
1797
|
-
} else if (name.endsWith(".ts") || name.endsWith(".tsx")) {
|
|
1798
|
-
if (name.endsWith(".d.ts") || name.endsWith(".test.ts")) continue;
|
|
1799
|
-
out.push(full);
|
|
1800
|
-
}
|
|
1801
|
-
}
|
|
1802
|
-
}
|
|
1803
|
-
function loadFile(path, root) {
|
|
1804
|
-
const raw = readFileSync5(path, "utf8");
|
|
1805
|
-
const lines = raw.split("\n").map((r, i) => ({ n: i + 1, raw: r, comment: isCommentLine(r) }));
|
|
1806
|
-
return { path, rel: relative(root, path) || path, lines };
|
|
1807
|
-
}
|
|
1808
|
-
function findPackageJson(startDir) {
|
|
1809
|
-
let dir = startDir;
|
|
1810
|
-
for (let depth = 0; depth < 8; depth++) {
|
|
1811
|
-
const candidate = join4(dir, "package.json");
|
|
1812
|
-
if (existsSync4(candidate)) {
|
|
1813
|
-
try {
|
|
1814
|
-
const parsed = JSON.parse(readFileSync5(candidate, "utf8"));
|
|
1815
|
-
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
1816
|
-
return { path: candidate, json: {}, unreadable: "not a JSON object" };
|
|
1817
|
-
}
|
|
1818
|
-
return { path: candidate, json: parsed };
|
|
1819
|
-
} catch (err) {
|
|
1820
|
-
return { path: candidate, json: {}, unreadable: err.message };
|
|
1821
|
-
}
|
|
1822
|
-
}
|
|
1823
|
-
const parent = dirname(dir);
|
|
1824
|
-
if (parent === dir) break;
|
|
1825
|
-
dir = parent;
|
|
1826
|
-
}
|
|
1827
|
-
return null;
|
|
1828
|
-
}
|
|
1829
|
-
function loadTree(root) {
|
|
1830
|
-
const st = statSync(root);
|
|
1831
|
-
const baseDir = st.isDirectory() ? root : dirname(root);
|
|
1832
|
-
const filePaths = [];
|
|
1833
|
-
if (st.isDirectory()) walk(root, filePaths);
|
|
1834
|
-
else if (root.endsWith(".ts") || root.endsWith(".tsx")) filePaths.push(root);
|
|
1835
|
-
const files = filePaths.map((p) => loadFile(p, baseDir));
|
|
1836
|
-
return { root, files, packageJson: findPackageJson(baseDir) };
|
|
1837
|
-
}
|
|
1838
|
-
|
|
1839
2689
|
// src/detectors/c19-served-descriptions.ts
|
|
1840
2690
|
var PIN6 = "a74a042b53c71a095ab88b3073bd47728eeda814";
|
|
1841
2691
|
var TOOL_DESC_INLINE_RE = /\b(?:s\.tool|\.tool|registerTool)\s*\(\s*(?:["'`][^"'`]+["'`]|[A-Za-z_$][\w$]*)\s*,\s*["'`]([^"'`]+)["'`]/;
|
|
@@ -2509,7 +3359,7 @@ import {
|
|
|
2509
3359
|
writeFileSync
|
|
2510
3360
|
} from "fs";
|
|
2511
3361
|
import { tmpdir } from "os";
|
|
2512
|
-
import { join as join6, relative as relative2 } from "path";
|
|
3362
|
+
import { basename as basename2, join as join6, relative as relative2, sep as sep2 } from "path";
|
|
2513
3363
|
var PIN9 = "20136b50";
|
|
2514
3364
|
var DEPLOY_CONFIG_FILES = [
|
|
2515
3365
|
"railway.json",
|
|
@@ -2526,8 +3376,7 @@ var LOCKFILES2 = [
|
|
|
2526
3376
|
{ name: "pnpm-lock.yaml", command: "pnpm", args: ["install", "--frozen-lockfile"] },
|
|
2527
3377
|
{ name: "yarn.lock", command: "yarn", args: ["install", "--frozen-lockfile"] }
|
|
2528
3378
|
];
|
|
2529
|
-
|
|
2530
|
-
function walkForConfigs(dir, depth, seen) {
|
|
3379
|
+
function walkForConfigs(dir, depth, seen, onSkip) {
|
|
2531
3380
|
if (depth > 8) return;
|
|
2532
3381
|
let entries;
|
|
2533
3382
|
try {
|
|
@@ -2544,8 +3393,11 @@ function walkForConfigs(dir, depth, seen) {
|
|
|
2544
3393
|
continue;
|
|
2545
3394
|
}
|
|
2546
3395
|
if (st.isDirectory()) {
|
|
2547
|
-
if (
|
|
2548
|
-
|
|
3396
|
+
if (SKIP_DIRS.has(name)) {
|
|
3397
|
+
onSkip(full, depth + 1);
|
|
3398
|
+
continue;
|
|
3399
|
+
}
|
|
3400
|
+
walkForConfigs(full, depth + 1, seen, onSkip);
|
|
2549
3401
|
} else if (DEPLOY_CONFIG_FILES.includes(name)) {
|
|
2550
3402
|
if (!seen.has(dir)) {
|
|
2551
3403
|
seen.set(dir, { dir, configFile: name, configPath: full });
|
|
@@ -2553,6 +3405,37 @@ function walkForConfigs(dir, depth, seen) {
|
|
|
2553
3405
|
}
|
|
2554
3406
|
}
|
|
2555
3407
|
}
|
|
3408
|
+
function probeForCandidate(dir, depth) {
|
|
3409
|
+
if (depth > 8) return null;
|
|
3410
|
+
let entries;
|
|
3411
|
+
try {
|
|
3412
|
+
entries = readdirSync3(dir).sort();
|
|
3413
|
+
} catch {
|
|
3414
|
+
return null;
|
|
3415
|
+
}
|
|
3416
|
+
const subdirs = [];
|
|
3417
|
+
for (const name of entries) {
|
|
3418
|
+
const full = join6(dir, name);
|
|
3419
|
+
let st;
|
|
3420
|
+
try {
|
|
3421
|
+
st = statSync2(full);
|
|
3422
|
+
} catch {
|
|
3423
|
+
continue;
|
|
3424
|
+
}
|
|
3425
|
+
if (st.isDirectory()) {
|
|
3426
|
+
const info = SKIP_DIR_INFO[name];
|
|
3427
|
+
if (info && !info.authored) continue;
|
|
3428
|
+
subdirs.push(full);
|
|
3429
|
+
} else if (DEPLOY_CONFIG_FILES.includes(name)) {
|
|
3430
|
+
return full;
|
|
3431
|
+
}
|
|
3432
|
+
}
|
|
3433
|
+
for (const sub of subdirs) {
|
|
3434
|
+
const hit = probeForCandidate(sub, depth + 1);
|
|
3435
|
+
if (hit) return hit;
|
|
3436
|
+
}
|
|
3437
|
+
return null;
|
|
3438
|
+
}
|
|
2556
3439
|
function resolveRailwayRootDirectory(configPath) {
|
|
2557
3440
|
let text;
|
|
2558
3441
|
try {
|
|
@@ -2574,15 +3457,31 @@ function resolveRailwayRootDirectory(configPath) {
|
|
|
2574
3457
|
const rootDirectory = typeof parsed.rootDirectory === "string" && parsed.rootDirectory || build && typeof build.rootDirectory === "string" && build.rootDirectory || null;
|
|
2575
3458
|
return rootDirectory;
|
|
2576
3459
|
}
|
|
2577
|
-
function c26DeriveScope(root) {
|
|
3460
|
+
function c26DeriveScope(root, tree) {
|
|
2578
3461
|
const seen = /* @__PURE__ */ new Map();
|
|
2579
|
-
|
|
3462
|
+
const skipped = [];
|
|
3463
|
+
const onSkip = (skippedDir, depth) => {
|
|
3464
|
+
const entry = tree ? recordSkip(tree, skippedDir, root) : {
|
|
3465
|
+
path: relative2(root, skippedDir).split(sep2).join("/"),
|
|
3466
|
+
reason: SKIP_DIR_INFO[basename2(skippedDir)]?.reason ?? "skipped"
|
|
3467
|
+
};
|
|
3468
|
+
if (SKIP_DIR_INFO[basename2(skippedDir)]?.authored) {
|
|
3469
|
+
const hit = probeForCandidate(skippedDir, depth);
|
|
3470
|
+
if (hit) entry.candidate = relative2(root, hit).split(sep2).join("/");
|
|
3471
|
+
}
|
|
3472
|
+
if (!skipped.some((d) => d.path === entry.path)) skipped.push(entry);
|
|
3473
|
+
};
|
|
3474
|
+
walkForConfigs(root, 0, seen, onSkip);
|
|
2580
3475
|
const sites = [];
|
|
2581
3476
|
for (const site of seen.values()) {
|
|
2582
3477
|
if (site.configFile === "railway.json") {
|
|
2583
3478
|
const resolved = resolveRailwayRootDirectory(site.configPath);
|
|
2584
3479
|
if (resolved && typeof resolved === "object" && "error" in resolved) {
|
|
2585
|
-
return {
|
|
3480
|
+
return {
|
|
3481
|
+
sites: [],
|
|
3482
|
+
skipped,
|
|
3483
|
+
unparsable: { path: site.configPath, reason: resolved.error }
|
|
3484
|
+
};
|
|
2586
3485
|
}
|
|
2587
3486
|
if (typeof resolved === "string" && resolved.trim()) {
|
|
2588
3487
|
sites.push({ ...site, dir: join6(site.dir, resolved) });
|
|
@@ -2595,6 +3494,7 @@ function c26DeriveScope(root) {
|
|
|
2595
3494
|
} catch (err) {
|
|
2596
3495
|
return {
|
|
2597
3496
|
sites: [],
|
|
3497
|
+
skipped,
|
|
2598
3498
|
unparsable: {
|
|
2599
3499
|
path: site.configPath,
|
|
2600
3500
|
reason: `could not parse ${site.configPath} as JSON: ${err instanceof Error ? err.message : String(err)}`
|
|
@@ -2604,7 +3504,7 @@ function c26DeriveScope(root) {
|
|
|
2604
3504
|
}
|
|
2605
3505
|
sites.push(site);
|
|
2606
3506
|
}
|
|
2607
|
-
return { sites };
|
|
3507
|
+
return { sites, skipped };
|
|
2608
3508
|
}
|
|
2609
3509
|
function findLockfile2(dir) {
|
|
2610
3510
|
for (const l of LOCKFILES2) {
|
|
@@ -2761,7 +3661,18 @@ var c26SubpackageLockfile = {
|
|
|
2761
3661
|
rule: "C-26",
|
|
2762
3662
|
pin: PIN9,
|
|
2763
3663
|
run(tree) {
|
|
2764
|
-
const scope = c26DeriveScope(tree.root);
|
|
3664
|
+
const scope = c26DeriveScope(tree.root, tree);
|
|
3665
|
+
const hidden = scope.skipped.filter((d) => d.candidate !== void 0);
|
|
3666
|
+
const unjudgedHidden = (prefix) => {
|
|
3667
|
+
const reason = `${prefix}deployment configuration found only under skipped director${hidden.length === 1 ? "y" : "ies"}: ${hidden.map((d) => `${d.path} (${d.reason}) -> ${d.candidate}`).join("; ")} \u2014 C-26 does not descend into skipped directories, so these site(s) were not judged`;
|
|
3668
|
+
return {
|
|
3669
|
+
id: this.id,
|
|
3670
|
+
anchor: join6(tree.root, hidden[0].candidate),
|
|
3671
|
+
state: "could-not-judge",
|
|
3672
|
+
reason,
|
|
3673
|
+
verdict: `could not judge: ${reason}`
|
|
3674
|
+
};
|
|
3675
|
+
};
|
|
2765
3676
|
if (scope.unparsable) {
|
|
2766
3677
|
const reason = `deployment configuration ${scope.unparsable.path} could not be parsed: ${scope.unparsable.reason} \u2014 scope unestablished, C-26 cannot derive its targets`;
|
|
2767
3678
|
return {
|
|
@@ -2773,6 +3684,7 @@ var c26SubpackageLockfile = {
|
|
|
2773
3684
|
};
|
|
2774
3685
|
}
|
|
2775
3686
|
if (scope.sites.length === 0) {
|
|
3687
|
+
if (hidden.length > 0) return unjudgedHidden("");
|
|
2776
3688
|
return {
|
|
2777
3689
|
id: this.id,
|
|
2778
3690
|
anchor: tree.root,
|
|
@@ -2784,6 +3696,7 @@ var c26SubpackageLockfile = {
|
|
|
2784
3696
|
const inventory = renderCoverage(scope.sites, verdicts);
|
|
2785
3697
|
const analysed = verdicts.filter((v) => v.status === "analysed");
|
|
2786
3698
|
if (analysed.length === 0) {
|
|
3699
|
+
if (hidden.length > 0) return unjudgedHidden("no analysable site; ");
|
|
2787
3700
|
return {
|
|
2788
3701
|
id: this.id,
|
|
2789
3702
|
anchor: tree.root,
|
|
@@ -2800,6 +3713,11 @@ var c26SubpackageLockfile = {
|
|
|
2800
3713
|
verdict: reds.some((r) => r.detail?.includes("no lockfile")) ? `${reds[0].detail} (C-26). Coverage: ${inventory}` : `${reds.map((r) => r.detail).join(" ; ")} (C-26). Coverage: ${inventory}`
|
|
2801
3714
|
};
|
|
2802
3715
|
}
|
|
3716
|
+
if (hidden.length > 0) {
|
|
3717
|
+
return unjudgedHidden(
|
|
3718
|
+
`${analysed.length} analysed site(s) are consistent, but coverage is partial; `
|
|
3719
|
+
);
|
|
3720
|
+
}
|
|
2803
3721
|
return {
|
|
2804
3722
|
id: this.id,
|
|
2805
3723
|
anchor: analysed[0].site.dir,
|
|
@@ -4369,24 +5287,12 @@ var dDebugOptIn = {
|
|
|
4369
5287
|
}
|
|
4370
5288
|
};
|
|
4371
5289
|
|
|
4372
|
-
// src/detectors/
|
|
5290
|
+
// src/detectors/extManifests.ts
|
|
4373
5291
|
import { readdirSync as readdirSync4, statSync as statSync5 } from "fs";
|
|
4374
5292
|
import { join as join12 } from "path";
|
|
4375
|
-
|
|
4376
|
-
var IDENTIFIER_POSITIVE = "com.elpi/middleware-redact-pii";
|
|
4377
|
-
var IDENTIFIER_NEGATIVE = "Com.Elpi/Bad_Id";
|
|
4378
|
-
function dExt1PositiveControls() {
|
|
4379
|
-
return {
|
|
4380
|
-
identifier: {
|
|
4381
|
-
pattern: IDENTIFIER_RE2.source,
|
|
4382
|
-
positive: IDENTIFIER_RE2.test(IDENTIFIER_POSITIVE),
|
|
4383
|
-
negative: IDENTIFIER_RE2.test(IDENTIFIER_NEGATIVE)
|
|
4384
|
-
}
|
|
4385
|
-
};
|
|
4386
|
-
}
|
|
4387
|
-
var SKIP_DIRS2 = /* @__PURE__ */ new Set(["node_modules", "dist", "build", ".git", "coverage"]);
|
|
4388
|
-
function findExtensionManifests(root) {
|
|
5293
|
+
function findExtensionManifests(tree) {
|
|
4389
5294
|
const out = [];
|
|
5295
|
+
const root = tree.root;
|
|
4390
5296
|
let base;
|
|
4391
5297
|
try {
|
|
4392
5298
|
base = statSync5(root).isDirectory() ? root : join12(root, "..");
|
|
@@ -4409,7 +5315,10 @@ function findExtensionManifests(root) {
|
|
|
4409
5315
|
continue;
|
|
4410
5316
|
}
|
|
4411
5317
|
if (st.isDirectory()) {
|
|
4412
|
-
if (
|
|
5318
|
+
if (SKIP_DIRS.has(name)) {
|
|
5319
|
+
recordSkip(tree, full, base);
|
|
5320
|
+
continue;
|
|
5321
|
+
}
|
|
4413
5322
|
walk2(full);
|
|
4414
5323
|
} else if (name === "extension.json") {
|
|
4415
5324
|
out.push(full);
|
|
@@ -4419,20 +5328,34 @@ function findExtensionManifests(root) {
|
|
|
4419
5328
|
walk2(base);
|
|
4420
5329
|
return out;
|
|
4421
5330
|
}
|
|
5331
|
+
|
|
5332
|
+
// src/detectors/dExt1.ts
|
|
5333
|
+
var IDENTIFIER_RE2 = /^[a-z0-9]+(\.[a-z0-9-]+)+\/[a-z0-9-]+$/;
|
|
5334
|
+
var IDENTIFIER_POSITIVE = "com.elpi/middleware-redact-pii";
|
|
5335
|
+
var IDENTIFIER_NEGATIVE = "Com.Elpi/Bad_Id";
|
|
5336
|
+
function dExt1PositiveControls() {
|
|
5337
|
+
return {
|
|
5338
|
+
identifier: {
|
|
5339
|
+
pattern: IDENTIFIER_RE2.source,
|
|
5340
|
+
positive: IDENTIFIER_RE2.test(IDENTIFIER_POSITIVE),
|
|
5341
|
+
negative: IDENTIFIER_RE2.test(IDENTIFIER_NEGATIVE)
|
|
5342
|
+
}
|
|
5343
|
+
};
|
|
5344
|
+
}
|
|
4422
5345
|
var TS_IDENT_LITERAL_RE = /\bidentifier:\s*["'`]([^"'`]*)["'`]/;
|
|
4423
|
-
var TS_IDENT_KEY_RE = /\bidentifier:\s*[^\s"'`]/;
|
|
5346
|
+
var TS_IDENT_KEY_RE = /\bidentifier:\s*[^\s"'`{]/;
|
|
4424
5347
|
function findTsIdentifiers(tree) {
|
|
4425
5348
|
const sites = [];
|
|
4426
5349
|
const unresolved = [];
|
|
4427
5350
|
for (const file of tree.files) {
|
|
4428
5351
|
for (const line of file.lines) {
|
|
4429
5352
|
if (line.comment) continue;
|
|
4430
|
-
const lit = line
|
|
5353
|
+
const lit = matchInCode(file, line, TS_IDENT_LITERAL_RE);
|
|
4431
5354
|
if (lit) {
|
|
4432
5355
|
sites.push({ value: lit[1], anchor: `${file.rel}:${line.n}` });
|
|
4433
5356
|
continue;
|
|
4434
5357
|
}
|
|
4435
|
-
if (
|
|
5358
|
+
if (testInCode(file, line, TS_IDENT_KEY_RE)) {
|
|
4436
5359
|
unresolved.push(`${file.rel}:${line.n}`);
|
|
4437
5360
|
}
|
|
4438
5361
|
}
|
|
@@ -4450,7 +5373,7 @@ var dExt1 = {
|
|
|
4450
5373
|
}
|
|
4451
5374
|
const unreadable = [];
|
|
4452
5375
|
const decided = (() => {
|
|
4453
|
-
const manifests = findExtensionManifests(tree
|
|
5376
|
+
const manifests = findExtensionManifests(tree);
|
|
4454
5377
|
for (const manifestPath2 of manifests) {
|
|
4455
5378
|
const shortPath = manifestPath2.split("/").slice(-2).join("/");
|
|
4456
5379
|
const parsed = readJsonObject(manifestPath2);
|
|
@@ -4517,8 +5440,6 @@ var dExt1 = {
|
|
|
4517
5440
|
};
|
|
4518
5441
|
|
|
4519
5442
|
// src/detectors/dExt2.ts
|
|
4520
|
-
import { readdirSync as readdirSync5, statSync as statSync6 } from "fs";
|
|
4521
|
-
import { join as join13 } from "path";
|
|
4522
5443
|
var TS_GD_TRUE_RE = /\bgracefulDegradation:\s*true\b/;
|
|
4523
5444
|
var TS_GD_FALSE_RE = /\bgracefulDegradation:\s*false\b/;
|
|
4524
5445
|
var TS_GD_KEY_RE = /\bgracefulDegradation:\s*([^\s,}]+)/;
|
|
@@ -4533,41 +5454,6 @@ function dExt2PositiveControls() {
|
|
|
4533
5454
|
}
|
|
4534
5455
|
};
|
|
4535
5456
|
}
|
|
4536
|
-
var SKIP_DIRS3 = /* @__PURE__ */ new Set(["node_modules", "dist", "build", ".git", "coverage"]);
|
|
4537
|
-
function findExtensionManifests2(root) {
|
|
4538
|
-
const out = [];
|
|
4539
|
-
let base;
|
|
4540
|
-
try {
|
|
4541
|
-
base = statSync6(root).isDirectory() ? root : join13(root, "..");
|
|
4542
|
-
} catch {
|
|
4543
|
-
return out;
|
|
4544
|
-
}
|
|
4545
|
-
const walk2 = (dir) => {
|
|
4546
|
-
let entries;
|
|
4547
|
-
try {
|
|
4548
|
-
entries = readdirSync5(dir);
|
|
4549
|
-
} catch {
|
|
4550
|
-
return;
|
|
4551
|
-
}
|
|
4552
|
-
for (const name of entries) {
|
|
4553
|
-
const full = join13(dir, name);
|
|
4554
|
-
let st;
|
|
4555
|
-
try {
|
|
4556
|
-
st = statSync6(full);
|
|
4557
|
-
} catch {
|
|
4558
|
-
continue;
|
|
4559
|
-
}
|
|
4560
|
-
if (st.isDirectory()) {
|
|
4561
|
-
if (SKIP_DIRS3.has(name)) continue;
|
|
4562
|
-
walk2(full);
|
|
4563
|
-
} else if (name === "extension.json") {
|
|
4564
|
-
out.push(full);
|
|
4565
|
-
}
|
|
4566
|
-
}
|
|
4567
|
-
};
|
|
4568
|
-
walk2(base);
|
|
4569
|
-
return out;
|
|
4570
|
-
}
|
|
4571
5457
|
function findTsFactorySites(tree) {
|
|
4572
5458
|
const sites = [];
|
|
4573
5459
|
for (const file of tree.files) {
|
|
@@ -4575,7 +5461,7 @@ function findTsFactorySites(tree) {
|
|
|
4575
5461
|
for (let i = 0; i < lines.length; i++) {
|
|
4576
5462
|
const line = lines[i];
|
|
4577
5463
|
if (line.comment) continue;
|
|
4578
|
-
if (!
|
|
5464
|
+
if (!testInCode(file, line, DEFINE_EXTENSION_OPEN_RE)) continue;
|
|
4579
5465
|
const anchor = `${file.rel}:${line.n}`;
|
|
4580
5466
|
let depth = 0;
|
|
4581
5467
|
let started = false;
|
|
@@ -4585,20 +5471,20 @@ function findTsFactorySites(tree) {
|
|
|
4585
5471
|
const bl = lines[j];
|
|
4586
5472
|
if (!bl.comment) {
|
|
4587
5473
|
if (state === "absent") {
|
|
4588
|
-
if (
|
|
5474
|
+
if (testInCode(file, bl, TS_GD_TRUE_RE)) {
|
|
4589
5475
|
state = "true";
|
|
4590
|
-
} else if (
|
|
5476
|
+
} else if (testInCode(file, bl, TS_GD_FALSE_RE)) {
|
|
4591
5477
|
state = "false";
|
|
4592
5478
|
} else {
|
|
4593
|
-
const key = bl
|
|
5479
|
+
const key = matchInCode(file, bl, TS_GD_KEY_RE);
|
|
4594
5480
|
if (key) {
|
|
4595
5481
|
state = "unresolved";
|
|
4596
5482
|
value = key[1];
|
|
4597
5483
|
}
|
|
4598
5484
|
}
|
|
4599
5485
|
}
|
|
4600
|
-
depth += (bl
|
|
4601
|
-
depth -= (bl
|
|
5486
|
+
depth += countInCode(file, bl, "{");
|
|
5487
|
+
depth -= countInCode(file, bl, "}");
|
|
4602
5488
|
if (depth > 0) started = true;
|
|
4603
5489
|
}
|
|
4604
5490
|
if (started && depth <= 0) break;
|
|
@@ -4619,7 +5505,7 @@ var dExt2 = {
|
|
|
4619
5505
|
}
|
|
4620
5506
|
const unreadable = [];
|
|
4621
5507
|
const decided = (() => {
|
|
4622
|
-
const manifests =
|
|
5508
|
+
const manifests = findExtensionManifests(tree);
|
|
4623
5509
|
for (const manifestPath2 of manifests) {
|
|
4624
5510
|
const shortPath = manifestPath2.split("/").slice(-2).join("/");
|
|
4625
5511
|
const parsed = readJsonObject(manifestPath2);
|
|
@@ -4695,8 +5581,6 @@ var dExt2 = {
|
|
|
4695
5581
|
};
|
|
4696
5582
|
|
|
4697
5583
|
// src/detectors/dExt3.ts
|
|
4698
|
-
import { readdirSync as readdirSync6, statSync as statSync7 } from "fs";
|
|
4699
|
-
import { join as join14 } from "path";
|
|
4700
5584
|
var TYPE_MATCHERS = [
|
|
4701
5585
|
{ type: "middleware", re: /\bmiddleware\b/ },
|
|
4702
5586
|
{ type: "tool-extension", re: /\btool-ext(?:ension)?\b/ },
|
|
@@ -4719,41 +5603,6 @@ function dExt3PositiveControls() {
|
|
|
4719
5603
|
}
|
|
4720
5604
|
};
|
|
4721
5605
|
}
|
|
4722
|
-
var SKIP_DIRS4 = /* @__PURE__ */ new Set(["node_modules", "dist", "build", ".git", "coverage"]);
|
|
4723
|
-
function findExtensionManifests3(root) {
|
|
4724
|
-
const out = [];
|
|
4725
|
-
let base;
|
|
4726
|
-
try {
|
|
4727
|
-
base = statSync7(root).isDirectory() ? root : join14(root, "..");
|
|
4728
|
-
} catch {
|
|
4729
|
-
return out;
|
|
4730
|
-
}
|
|
4731
|
-
const walk2 = (dir) => {
|
|
4732
|
-
let entries;
|
|
4733
|
-
try {
|
|
4734
|
-
entries = readdirSync6(dir);
|
|
4735
|
-
} catch {
|
|
4736
|
-
return;
|
|
4737
|
-
}
|
|
4738
|
-
for (const name of entries) {
|
|
4739
|
-
const full = join14(dir, name);
|
|
4740
|
-
let st;
|
|
4741
|
-
try {
|
|
4742
|
-
st = statSync7(full);
|
|
4743
|
-
} catch {
|
|
4744
|
-
continue;
|
|
4745
|
-
}
|
|
4746
|
-
if (st.isDirectory()) {
|
|
4747
|
-
if (SKIP_DIRS4.has(name)) continue;
|
|
4748
|
-
walk2(full);
|
|
4749
|
-
} else if (name === "extension.json") {
|
|
4750
|
-
out.push(full);
|
|
4751
|
-
}
|
|
4752
|
-
}
|
|
4753
|
-
};
|
|
4754
|
-
walk2(base);
|
|
4755
|
-
return out;
|
|
4756
|
-
}
|
|
4757
5606
|
var DEFINE_EXTENSION_OPEN_RE2 = /\bdefineExtension\s*\(\s*\{/;
|
|
4758
5607
|
var TS_TYPE_LITERAL_RE = /\btype:\s*["'`]([^"'`]*)["'`]/;
|
|
4759
5608
|
var TS_TYPE_KEY_RE = /\btype:\s*[^\s"'`]/;
|
|
@@ -4764,7 +5613,7 @@ function findTsTypeSites(tree) {
|
|
|
4764
5613
|
for (let i = 0; i < lines.length; i++) {
|
|
4765
5614
|
const line = lines[i];
|
|
4766
5615
|
if (line.comment) continue;
|
|
4767
|
-
if (!
|
|
5616
|
+
if (!testInCode(file, line, DEFINE_EXTENSION_OPEN_RE2)) continue;
|
|
4768
5617
|
const anchor = `${file.rel}:${line.n}`;
|
|
4769
5618
|
let depth = 0;
|
|
4770
5619
|
let started = false;
|
|
@@ -4775,17 +5624,17 @@ function findTsTypeSites(tree) {
|
|
|
4775
5624
|
const bl = lines[j];
|
|
4776
5625
|
if (!bl.comment) {
|
|
4777
5626
|
if (!found) {
|
|
4778
|
-
const lit = bl
|
|
5627
|
+
const lit = matchInCode(file, bl, TS_TYPE_LITERAL_RE);
|
|
4779
5628
|
if (lit) {
|
|
4780
5629
|
value = lit[1];
|
|
4781
5630
|
found = true;
|
|
4782
|
-
} else if (
|
|
5631
|
+
} else if (testInCode(file, bl, TS_TYPE_KEY_RE)) {
|
|
4783
5632
|
unresolved = true;
|
|
4784
5633
|
found = true;
|
|
4785
5634
|
}
|
|
4786
5635
|
}
|
|
4787
|
-
depth += (bl
|
|
4788
|
-
depth -= (bl
|
|
5636
|
+
depth += countInCode(file, bl, "{");
|
|
5637
|
+
depth -= countInCode(file, bl, "}");
|
|
4789
5638
|
if (depth > 0) started = true;
|
|
4790
5639
|
}
|
|
4791
5640
|
if (started && depth <= 0) break;
|
|
@@ -4813,7 +5662,7 @@ var dExt3 = {
|
|
|
4813
5662
|
const decided = (() => {
|
|
4814
5663
|
const declared = /* @__PURE__ */ new Set();
|
|
4815
5664
|
let firstAnchor2 = null;
|
|
4816
|
-
const manifests =
|
|
5665
|
+
const manifests = findExtensionManifests(tree);
|
|
4817
5666
|
for (const manifestPath2 of manifests) {
|
|
4818
5667
|
const shortPath = manifestPath2.split("/").slice(-2).join("/");
|
|
4819
5668
|
const parsed = readJsonObject(manifestPath2);
|
|
@@ -4920,8 +5769,8 @@ var dExt3 = {
|
|
|
4920
5769
|
};
|
|
4921
5770
|
|
|
4922
5771
|
// src/detectors/dExt4.ts
|
|
4923
|
-
import { readdirSync as
|
|
4924
|
-
import { join as
|
|
5772
|
+
import { readdirSync as readdirSync5, statSync as statSync6 } from "fs";
|
|
5773
|
+
import { join as join13 } from "path";
|
|
4925
5774
|
function canonical(name) {
|
|
4926
5775
|
return name.replace(/[-_]/g, "").toLowerCase();
|
|
4927
5776
|
}
|
|
@@ -4940,71 +5789,40 @@ function dExt4PositiveControls() {
|
|
|
4940
5789
|
}
|
|
4941
5790
|
};
|
|
4942
5791
|
}
|
|
4943
|
-
|
|
4944
|
-
|
|
5792
|
+
function findHooksDirs(tree) {
|
|
5793
|
+
const root = tree.root;
|
|
4945
5794
|
const out = [];
|
|
4946
5795
|
let base;
|
|
4947
5796
|
try {
|
|
4948
|
-
base =
|
|
5797
|
+
base = statSync6(root).isDirectory() ? root : join13(root, "..");
|
|
4949
5798
|
} catch {
|
|
4950
5799
|
return out;
|
|
4951
5800
|
}
|
|
4952
5801
|
const walk2 = (dir) => {
|
|
4953
5802
|
let entries;
|
|
4954
5803
|
try {
|
|
4955
|
-
entries =
|
|
5804
|
+
entries = readdirSync5(dir);
|
|
4956
5805
|
} catch {
|
|
4957
5806
|
return;
|
|
4958
5807
|
}
|
|
4959
5808
|
for (const name of entries) {
|
|
4960
|
-
const full =
|
|
5809
|
+
const full = join13(dir, name);
|
|
4961
5810
|
let st;
|
|
4962
5811
|
try {
|
|
4963
|
-
st =
|
|
5812
|
+
st = statSync6(full);
|
|
4964
5813
|
} catch {
|
|
4965
5814
|
continue;
|
|
4966
5815
|
}
|
|
4967
|
-
if (st.isDirectory())
|
|
4968
|
-
|
|
4969
|
-
|
|
4970
|
-
} else if (name === "extension.json") {
|
|
4971
|
-
out.push(full);
|
|
4972
|
-
}
|
|
4973
|
-
}
|
|
4974
|
-
};
|
|
4975
|
-
walk2(base);
|
|
4976
|
-
return out;
|
|
4977
|
-
}
|
|
4978
|
-
function findHooksDirs(root) {
|
|
4979
|
-
const out = [];
|
|
4980
|
-
let base;
|
|
4981
|
-
try {
|
|
4982
|
-
base = statSync8(root).isDirectory() ? root : join15(root, "..");
|
|
4983
|
-
} catch {
|
|
4984
|
-
return out;
|
|
4985
|
-
}
|
|
4986
|
-
const walk2 = (dir) => {
|
|
4987
|
-
let entries;
|
|
4988
|
-
try {
|
|
4989
|
-
entries = readdirSync7(dir);
|
|
4990
|
-
} catch {
|
|
4991
|
-
return;
|
|
4992
|
-
}
|
|
4993
|
-
for (const name of entries) {
|
|
4994
|
-
const full = join15(dir, name);
|
|
4995
|
-
let st;
|
|
4996
|
-
try {
|
|
4997
|
-
st = statSync8(full);
|
|
4998
|
-
} catch {
|
|
5816
|
+
if (!st.isDirectory()) continue;
|
|
5817
|
+
if (SKIP_DIRS.has(name)) {
|
|
5818
|
+
recordSkip(tree, full, base);
|
|
4999
5819
|
continue;
|
|
5000
5820
|
}
|
|
5001
|
-
if (!st.isDirectory()) continue;
|
|
5002
|
-
if (SKIP_DIRS5.has(name)) continue;
|
|
5003
5821
|
if (name === "hooks") {
|
|
5004
5822
|
const tokens = /* @__PURE__ */ new Set();
|
|
5005
5823
|
let files;
|
|
5006
5824
|
try {
|
|
5007
|
-
files =
|
|
5825
|
+
files = readdirSync5(full);
|
|
5008
5826
|
} catch {
|
|
5009
5827
|
files = [];
|
|
5010
5828
|
}
|
|
@@ -5033,7 +5851,7 @@ function findFactorySites(tree) {
|
|
|
5033
5851
|
for (let i = 0; i < lines.length; i++) {
|
|
5034
5852
|
const line = lines[i];
|
|
5035
5853
|
if (line.comment) continue;
|
|
5036
|
-
if (
|
|
5854
|
+
if (testInCode(file, line, LIFECYCLE_OBJ_RE)) {
|
|
5037
5855
|
const anchor = `${file.rel}:${line.n}`;
|
|
5038
5856
|
const tokens = /* @__PURE__ */ new Set();
|
|
5039
5857
|
let depth = 0;
|
|
@@ -5041,20 +5859,19 @@ function findFactorySites(tree) {
|
|
|
5041
5859
|
for (let j = i; j < lines.length; j++) {
|
|
5042
5860
|
const bl = lines[j];
|
|
5043
5861
|
if (bl.comment) continue;
|
|
5044
|
-
const raw = bl.raw;
|
|
5045
5862
|
if (started && depth === 1) {
|
|
5046
|
-
const k =
|
|
5863
|
+
const k = matchInCode(file, bl, OBJECT_KEY_RE);
|
|
5047
5864
|
if (k) tokens.add(canonical(k[1]));
|
|
5048
5865
|
}
|
|
5049
|
-
depth += (
|
|
5050
|
-
depth -= (
|
|
5866
|
+
depth += countInCode(file, bl, "{");
|
|
5867
|
+
depth -= countInCode(file, bl, "}");
|
|
5051
5868
|
if (depth > 0) started = true;
|
|
5052
5869
|
if (started && depth <= 0) break;
|
|
5053
5870
|
}
|
|
5054
5871
|
sites.push({ anchor, tokens });
|
|
5055
5872
|
continue;
|
|
5056
5873
|
}
|
|
5057
|
-
if (
|
|
5874
|
+
if (testInCode(file, line, LIFECYCLE_KEY_RE)) {
|
|
5058
5875
|
unresolved.push(`${file.rel}:${line.n}`);
|
|
5059
5876
|
}
|
|
5060
5877
|
}
|
|
@@ -5082,7 +5899,7 @@ var dExt4 = {
|
|
|
5082
5899
|
}
|
|
5083
5900
|
const unreadable = [];
|
|
5084
5901
|
const decided = (() => {
|
|
5085
|
-
const hooksDirs = findHooksDirs(tree
|
|
5902
|
+
const hooksDirs = findHooksDirs(tree);
|
|
5086
5903
|
const { sites, unresolved } = findFactorySites(tree);
|
|
5087
5904
|
for (const anchor of unresolved) {
|
|
5088
5905
|
unreadable.push({
|
|
@@ -5094,7 +5911,7 @@ var dExt4 = {
|
|
|
5094
5911
|
for (const d of hooksDirs) for (const t of d.tokens) implemented.add(t);
|
|
5095
5912
|
for (const s of sites) for (const t of s.tokens) implemented.add(t);
|
|
5096
5913
|
const implSurfacePresent = hooksDirs.length > 0 || sites.length > 0;
|
|
5097
|
-
const manifests =
|
|
5914
|
+
const manifests = findExtensionManifests(tree);
|
|
5098
5915
|
let declaredAnyLifecycle = false;
|
|
5099
5916
|
for (const manifestPath2 of manifests) {
|
|
5100
5917
|
const shortPath = manifestPath2.split("/").slice(-2).join("/");
|
|
@@ -5163,8 +5980,8 @@ var dExt4 = {
|
|
|
5163
5980
|
};
|
|
5164
5981
|
|
|
5165
5982
|
// src/detectors/dExt5.ts
|
|
5166
|
-
import { existsSync as existsSync12
|
|
5167
|
-
import { dirname as dirname7, join as
|
|
5983
|
+
import { existsSync as existsSync12 } from "fs";
|
|
5984
|
+
import { dirname as dirname7, join as join14 } from "path";
|
|
5168
5985
|
function nonEmptyString(v) {
|
|
5169
5986
|
return typeof v === "string" && v.trim().length > 0;
|
|
5170
5987
|
}
|
|
@@ -5178,41 +5995,6 @@ function dExt5PositiveControls() {
|
|
|
5178
5995
|
}
|
|
5179
5996
|
};
|
|
5180
5997
|
}
|
|
5181
|
-
var SKIP_DIRS6 = /* @__PURE__ */ new Set(["node_modules", "dist", "build", ".git", "coverage"]);
|
|
5182
|
-
function findExtensionManifests5(root) {
|
|
5183
|
-
const out = [];
|
|
5184
|
-
let base;
|
|
5185
|
-
try {
|
|
5186
|
-
base = statSync9(root).isDirectory() ? root : join16(root, "..");
|
|
5187
|
-
} catch {
|
|
5188
|
-
return out;
|
|
5189
|
-
}
|
|
5190
|
-
const walk2 = (dir) => {
|
|
5191
|
-
let entries;
|
|
5192
|
-
try {
|
|
5193
|
-
entries = readdirSync8(dir);
|
|
5194
|
-
} catch {
|
|
5195
|
-
return;
|
|
5196
|
-
}
|
|
5197
|
-
for (const name of entries) {
|
|
5198
|
-
const full = join16(dir, name);
|
|
5199
|
-
let st;
|
|
5200
|
-
try {
|
|
5201
|
-
st = statSync9(full);
|
|
5202
|
-
} catch {
|
|
5203
|
-
continue;
|
|
5204
|
-
}
|
|
5205
|
-
if (st.isDirectory()) {
|
|
5206
|
-
if (SKIP_DIRS6.has(name)) continue;
|
|
5207
|
-
walk2(full);
|
|
5208
|
-
} else if (name === "extension.json") {
|
|
5209
|
-
out.push(full);
|
|
5210
|
-
}
|
|
5211
|
-
}
|
|
5212
|
-
};
|
|
5213
|
-
walk2(base);
|
|
5214
|
-
return out;
|
|
5215
|
-
}
|
|
5216
5998
|
function isToolExtension(type) {
|
|
5217
5999
|
return typeof type === "string" && type.includes("tool-extension");
|
|
5218
6000
|
}
|
|
@@ -5225,7 +6007,7 @@ var dExt5 = {
|
|
|
5225
6007
|
`DExt5 bilingual control FAILED: accept(\`${FR_POSITIVE}\`)=${controls.descriptionFr.positive} reject(empty)=${!controls.descriptionFr.negative} \u2014 FR-string matcher broken, absence verdict untrustworthy`
|
|
5226
6008
|
);
|
|
5227
6009
|
}
|
|
5228
|
-
const manifests =
|
|
6010
|
+
const manifests = findExtensionManifests(tree);
|
|
5229
6011
|
if (manifests.length === 0) {
|
|
5230
6012
|
return {
|
|
5231
6013
|
id: this.id,
|
|
@@ -5259,7 +6041,7 @@ var dExt5 = {
|
|
|
5259
6041
|
if (!nonEmptyString(parsed.description_fr)) {
|
|
5260
6042
|
missing.push("description_fr (extension.json)");
|
|
5261
6043
|
}
|
|
5262
|
-
if (!existsSync12(
|
|
6044
|
+
if (!existsSync12(join14(root, "README.fr.md"))) {
|
|
5263
6045
|
missing.push("README.fr.md");
|
|
5264
6046
|
}
|
|
5265
6047
|
if (isToolExtension(parsed.type) && "tools" in parsed) {
|
|
@@ -5302,8 +6084,8 @@ var dExt5 = {
|
|
|
5302
6084
|
};
|
|
5303
6085
|
|
|
5304
6086
|
// src/detectors/dExt7.ts
|
|
5305
|
-
import { existsSync as existsSync13
|
|
5306
|
-
import { dirname as dirname8, join as
|
|
6087
|
+
import { existsSync as existsSync13 } from "fs";
|
|
6088
|
+
import { dirname as dirname8, join as join15 } from "path";
|
|
5307
6089
|
var ABSENCE_FLAG_FIELDS = [
|
|
5308
6090
|
"extensionAbsent",
|
|
5309
6091
|
"extension_absent",
|
|
@@ -5346,41 +6128,6 @@ function dExt7PositiveControls() {
|
|
|
5346
6128
|
negative: isAbsenceCase(ABSENCE_NEGATIVE)
|
|
5347
6129
|
};
|
|
5348
6130
|
}
|
|
5349
|
-
var SKIP_DIRS7 = /* @__PURE__ */ new Set(["node_modules", "dist", "build", ".git", "coverage"]);
|
|
5350
|
-
function findExtensionManifests6(root) {
|
|
5351
|
-
const out = [];
|
|
5352
|
-
let base;
|
|
5353
|
-
try {
|
|
5354
|
-
base = statSync10(root).isDirectory() ? root : join17(root, "..");
|
|
5355
|
-
} catch {
|
|
5356
|
-
return out;
|
|
5357
|
-
}
|
|
5358
|
-
const walk2 = (dir) => {
|
|
5359
|
-
let entries;
|
|
5360
|
-
try {
|
|
5361
|
-
entries = readdirSync9(dir);
|
|
5362
|
-
} catch {
|
|
5363
|
-
return;
|
|
5364
|
-
}
|
|
5365
|
-
for (const name of entries) {
|
|
5366
|
-
const full = join17(dir, name);
|
|
5367
|
-
let st;
|
|
5368
|
-
try {
|
|
5369
|
-
st = statSync10(full);
|
|
5370
|
-
} catch {
|
|
5371
|
-
continue;
|
|
5372
|
-
}
|
|
5373
|
-
if (st.isDirectory()) {
|
|
5374
|
-
if (SKIP_DIRS7.has(name)) continue;
|
|
5375
|
-
walk2(full);
|
|
5376
|
-
} else if (name === "extension.json") {
|
|
5377
|
-
out.push(full);
|
|
5378
|
-
}
|
|
5379
|
-
}
|
|
5380
|
-
};
|
|
5381
|
-
walk2(base);
|
|
5382
|
-
return out;
|
|
5383
|
-
}
|
|
5384
6131
|
var dExt7 = {
|
|
5385
6132
|
id: "DExt7 eval-coverage",
|
|
5386
6133
|
run(tree) {
|
|
@@ -5390,7 +6137,7 @@ var dExt7 = {
|
|
|
5390
6137
|
`DExt7 absence-matcher control FAILED: flag=${controls.flag} token=${controls.token} label=${controls.label} reject(normal)=${!controls.negative} \u2014 absence matcher broken, eval-coverage verdict untrustworthy`
|
|
5391
6138
|
);
|
|
5392
6139
|
}
|
|
5393
|
-
const manifests =
|
|
6140
|
+
const manifests = findExtensionManifests(tree);
|
|
5394
6141
|
if (manifests.length === 0) {
|
|
5395
6142
|
return {
|
|
5396
6143
|
id: this.id,
|
|
@@ -5404,7 +6151,7 @@ var dExt7 = {
|
|
|
5404
6151
|
for (const manifestPath2 of manifests) {
|
|
5405
6152
|
const extDir = dirname8(manifestPath2);
|
|
5406
6153
|
const extLabel = extDir.split("/").pop() ?? extDir;
|
|
5407
|
-
const evalsPath =
|
|
6154
|
+
const evalsPath = join15(extDir, "evals", "evals.json");
|
|
5408
6155
|
const shortPath = `${extLabel}/evals/evals.json`;
|
|
5409
6156
|
if (!existsSync13(evalsPath)) {
|
|
5410
6157
|
return {
|
|
@@ -5470,8 +6217,8 @@ var dExt7 = {
|
|
|
5470
6217
|
};
|
|
5471
6218
|
|
|
5472
6219
|
// src/detectors/dExt8.ts
|
|
5473
|
-
import { existsSync as existsSync14
|
|
5474
|
-
import { dirname as dirname9, join as
|
|
6220
|
+
import { existsSync as existsSync14 } from "fs";
|
|
6221
|
+
import { dirname as dirname9, join as join16 } from "path";
|
|
5475
6222
|
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-]+)*))?$/;
|
|
5476
6223
|
var POSITIVE_CONTROL5 = "1.2.3-beta.1+build.7";
|
|
5477
6224
|
function dExt8PositiveControl() {
|
|
@@ -5481,41 +6228,6 @@ function dExt8PositiveControl() {
|
|
|
5481
6228
|
matched: SEMVER_RE2.test(POSITIVE_CONTROL5)
|
|
5482
6229
|
};
|
|
5483
6230
|
}
|
|
5484
|
-
var SKIP_DIRS8 = /* @__PURE__ */ new Set(["node_modules", "dist", "build", ".git", "coverage"]);
|
|
5485
|
-
function findExtensionManifests7(root) {
|
|
5486
|
-
const out = [];
|
|
5487
|
-
let base;
|
|
5488
|
-
try {
|
|
5489
|
-
base = statSync11(root).isDirectory() ? root : join18(root, "..");
|
|
5490
|
-
} catch {
|
|
5491
|
-
return out;
|
|
5492
|
-
}
|
|
5493
|
-
const walk2 = (dir) => {
|
|
5494
|
-
let entries;
|
|
5495
|
-
try {
|
|
5496
|
-
entries = readdirSync10(dir);
|
|
5497
|
-
} catch {
|
|
5498
|
-
return;
|
|
5499
|
-
}
|
|
5500
|
-
for (const name of entries) {
|
|
5501
|
-
const full = join18(dir, name);
|
|
5502
|
-
let st;
|
|
5503
|
-
try {
|
|
5504
|
-
st = statSync11(full);
|
|
5505
|
-
} catch {
|
|
5506
|
-
continue;
|
|
5507
|
-
}
|
|
5508
|
-
if (st.isDirectory()) {
|
|
5509
|
-
if (SKIP_DIRS8.has(name)) continue;
|
|
5510
|
-
walk2(full);
|
|
5511
|
-
} else if (name === "extension.json") {
|
|
5512
|
-
out.push(full);
|
|
5513
|
-
}
|
|
5514
|
-
}
|
|
5515
|
-
};
|
|
5516
|
-
walk2(base);
|
|
5517
|
-
return out;
|
|
5518
|
-
}
|
|
5519
6231
|
var dExt8 = {
|
|
5520
6232
|
id: "DExt8 semver+changelog",
|
|
5521
6233
|
run(tree) {
|
|
@@ -5525,7 +6237,7 @@ var dExt8 = {
|
|
|
5525
6237
|
`DExt8 positive control FAILED: pattern /${control.pattern}/ did not match sample \`${control.sample}\` \u2014 matcher broken, verdict untrustworthy`
|
|
5526
6238
|
);
|
|
5527
6239
|
}
|
|
5528
|
-
const manifests =
|
|
6240
|
+
const manifests = findExtensionManifests(tree);
|
|
5529
6241
|
if (manifests.length === 0) {
|
|
5530
6242
|
return {
|
|
5531
6243
|
id: this.id,
|
|
@@ -5540,7 +6252,7 @@ var dExt8 = {
|
|
|
5540
6252
|
for (const manifestPath2 of manifests) {
|
|
5541
6253
|
const extDir = dirname9(manifestPath2);
|
|
5542
6254
|
const extLabel = extDir.split("/").pop() ?? extDir;
|
|
5543
|
-
const pkgPath =
|
|
6255
|
+
const pkgPath = join16(extDir, "package.json");
|
|
5544
6256
|
const shortPkgPath = `${extLabel}/package.json`;
|
|
5545
6257
|
if (!existsSync14(pkgPath)) {
|
|
5546
6258
|
unreadable.push({
|
|
@@ -5577,7 +6289,7 @@ var dExt8 = {
|
|
|
5577
6289
|
continue;
|
|
5578
6290
|
}
|
|
5579
6291
|
const validSemver = SEMVER_RE2.test(version);
|
|
5580
|
-
const changelogPath =
|
|
6292
|
+
const changelogPath = join16(extDir, "CHANGELOG.md");
|
|
5581
6293
|
const hasChangelog = existsSync14(changelogPath);
|
|
5582
6294
|
if (!validSemver) {
|
|
5583
6295
|
finding ??= {
|
|
@@ -5612,38 +6324,41 @@ var dExt8 = {
|
|
|
5612
6324
|
};
|
|
5613
6325
|
|
|
5614
6326
|
// src/detectors/dExt9.ts
|
|
5615
|
-
import { readFileSync as readFileSync10, readdirSync as
|
|
5616
|
-
import { join as
|
|
5617
|
-
|
|
5618
|
-
|
|
6327
|
+
import { readFileSync as readFileSync10, readdirSync as readdirSync6, statSync as statSync7 } from "fs";
|
|
6328
|
+
import { join as join17 } from "path";
|
|
6329
|
+
function collectHooksDirSources(tree, unreadable) {
|
|
6330
|
+
const root = tree.root;
|
|
5619
6331
|
const out = [];
|
|
5620
6332
|
let base;
|
|
5621
6333
|
try {
|
|
5622
|
-
base =
|
|
6334
|
+
base = statSync7(root).isDirectory() ? root : join17(root, "..");
|
|
5623
6335
|
} catch {
|
|
5624
6336
|
return out;
|
|
5625
6337
|
}
|
|
5626
6338
|
const walk2 = (dir) => {
|
|
5627
6339
|
let entries;
|
|
5628
6340
|
try {
|
|
5629
|
-
entries =
|
|
6341
|
+
entries = readdirSync6(dir);
|
|
5630
6342
|
} catch {
|
|
5631
6343
|
return;
|
|
5632
6344
|
}
|
|
5633
6345
|
for (const name of entries) {
|
|
5634
|
-
const full =
|
|
6346
|
+
const full = join17(dir, name);
|
|
5635
6347
|
let st;
|
|
5636
6348
|
try {
|
|
5637
|
-
st =
|
|
6349
|
+
st = statSync7(full);
|
|
5638
6350
|
} catch {
|
|
5639
6351
|
continue;
|
|
5640
6352
|
}
|
|
5641
6353
|
if (!st.isDirectory()) continue;
|
|
5642
|
-
if (
|
|
6354
|
+
if (SKIP_DIRS.has(name)) {
|
|
6355
|
+
recordSkip(tree, full, base);
|
|
6356
|
+
continue;
|
|
6357
|
+
}
|
|
5643
6358
|
if (name === "hooks") {
|
|
5644
6359
|
let files;
|
|
5645
6360
|
try {
|
|
5646
|
-
files =
|
|
6361
|
+
files = readdirSync6(full);
|
|
5647
6362
|
} catch {
|
|
5648
6363
|
files = [];
|
|
5649
6364
|
}
|
|
@@ -5651,7 +6366,7 @@ function collectHooksDirSources(root, unreadable) {
|
|
|
5651
6366
|
if (!f.endsWith(".ts") || f.endsWith(".d.ts")) continue;
|
|
5652
6367
|
const stem = f.replace(/\.ts$/, "");
|
|
5653
6368
|
if (stem === "index") continue;
|
|
5654
|
-
const filePath =
|
|
6369
|
+
const filePath = join17(full, f);
|
|
5655
6370
|
let body;
|
|
5656
6371
|
try {
|
|
5657
6372
|
body = readFileSync10(filePath, "utf8");
|
|
@@ -5685,7 +6400,7 @@ function collectFactorySources(tree) {
|
|
|
5685
6400
|
for (let i = 0; i < lines.length; i++) {
|
|
5686
6401
|
const line = lines[i];
|
|
5687
6402
|
if (line.comment) continue;
|
|
5688
|
-
if (!
|
|
6403
|
+
if (!testInCode(file, line, LIFECYCLE_OBJ_RE2)) continue;
|
|
5689
6404
|
let depth = 0;
|
|
5690
6405
|
let started = false;
|
|
5691
6406
|
let curKey = null;
|
|
@@ -5695,7 +6410,7 @@ function collectFactorySources(tree) {
|
|
|
5695
6410
|
if (bl.comment) continue;
|
|
5696
6411
|
const raw = bl.raw;
|
|
5697
6412
|
if (started && depth === 1) {
|
|
5698
|
-
const k =
|
|
6413
|
+
const k = matchInCode(file, bl, OBJECT_KEY_RE2);
|
|
5699
6414
|
if (k) {
|
|
5700
6415
|
curKey = canonical(k[1]);
|
|
5701
6416
|
keyBodies.set(curKey, []);
|
|
@@ -5704,8 +6419,8 @@ function collectFactorySources(tree) {
|
|
|
5704
6419
|
if (curKey && depth >= 1) {
|
|
5705
6420
|
keyBodies.get(curKey)?.push(raw);
|
|
5706
6421
|
}
|
|
5707
|
-
depth += (
|
|
5708
|
-
depth -= (
|
|
6422
|
+
depth += countInCode(file, bl, "{");
|
|
6423
|
+
depth -= countInCode(file, bl, "}");
|
|
5709
6424
|
if (depth > 0) started = true;
|
|
5710
6425
|
if (started && depth <= 0) break;
|
|
5711
6426
|
}
|
|
@@ -5756,7 +6471,7 @@ var dExt9 = {
|
|
|
5756
6471
|
const unreadable = [];
|
|
5757
6472
|
const decided = (() => {
|
|
5758
6473
|
const hookSources = [
|
|
5759
|
-
...collectHooksDirSources(tree
|
|
6474
|
+
...collectHooksDirSources(tree, unreadable),
|
|
5760
6475
|
...collectFactorySources(tree)
|
|
5761
6476
|
];
|
|
5762
6477
|
const implByToken = /* @__PURE__ */ new Map();
|
|
@@ -5765,7 +6480,7 @@ var dExt9 = {
|
|
|
5765
6480
|
list.push(s);
|
|
5766
6481
|
implByToken.set(s.token, list);
|
|
5767
6482
|
}
|
|
5768
|
-
const manifests =
|
|
6483
|
+
const manifests = findExtensionManifests(tree);
|
|
5769
6484
|
let declaredAnyLifecycle = false;
|
|
5770
6485
|
for (const manifestPath2 of manifests) {
|
|
5771
6486
|
const shortPath = manifestPath2.split("/").slice(-2).join("/");
|
|
@@ -6501,6 +7216,436 @@ var dServer14 = {
|
|
|
6501
7216
|
}
|
|
6502
7217
|
};
|
|
6503
7218
|
|
|
7219
|
+
// src/detectors/s28-claim-forwarded.ts
|
|
7220
|
+
import { existsSync as existsSync15, readFileSync as readFileSync11, readdirSync as readdirSync7, statSync as statSync8 } from "fs";
|
|
7221
|
+
import { dirname as dirname10, join as join18, relative as relative4 } from "path";
|
|
7222
|
+
var PIN11 = "bacc54a53d5f6f9f20e5254908e2b46807902faa";
|
|
7223
|
+
var CLAIM_TOKEN = /\bverified(?:Org|Person)\b/;
|
|
7224
|
+
var ANY_CLAIM = /\bverified(?:Actor|Org|Person)\b/g;
|
|
7225
|
+
var CALL_RE = /\b([A-Za-z_$][\w$]*)\s*\.\s*(query|mutation|action)\s*\(/g;
|
|
7226
|
+
var GUARD_RE = /\b(?:unresolved\w*Denial|requireVerified\w+|requireClaim\w*)\s*\(|verified-org-unresolved/;
|
|
7227
|
+
var FLEET_WIDE = /fleet-wide:\s*\S[^\n]{7,}/;
|
|
7228
|
+
var SCOPE_MARKERS = /\bwithOrgScope\b|\brequireAuthenticatedCaller\b|\bresolveInboxReader\b|\bresolveVerified\w*\b|\brequireScope\b|\bgetUserIdentity\b|\bisMaster\b|\bscope\b/;
|
|
7229
|
+
var ACTOR_SET_RE = /\bconst\s+(VERIFIED_\w*DOORS)\b[^=]*=\s*new\s+Set\s*(?:<[^>]*>)?\s*\(\s*\[([\s\S]*?)\]\s*\)/;
|
|
7230
|
+
function masked(file) {
|
|
7231
|
+
const out = [];
|
|
7232
|
+
for (const line of file.lines) {
|
|
7233
|
+
let row = "";
|
|
7234
|
+
for (let c = 0; c < line.raw.length; c++) {
|
|
7235
|
+
const ch = line.raw[c];
|
|
7236
|
+
row += ch === " " || ch === " " || ch === "\r" || isCodeAt(file, line.n, c) ? ch : " ";
|
|
7237
|
+
}
|
|
7238
|
+
out.push(row);
|
|
7239
|
+
}
|
|
7240
|
+
return out.join("\n");
|
|
7241
|
+
}
|
|
7242
|
+
function rawText(file) {
|
|
7243
|
+
return file.lines.map((l) => l.raw).join("\n");
|
|
7244
|
+
}
|
|
7245
|
+
function lineAt(text, offset) {
|
|
7246
|
+
let n = 1;
|
|
7247
|
+
for (let i = 0; i < offset && i < text.length; i++) if (text.charCodeAt(i) === 10) n++;
|
|
7248
|
+
return n;
|
|
7249
|
+
}
|
|
7250
|
+
var OPEN = { "(": ")", "[": "]", "{": "}" };
|
|
7251
|
+
function matchClose(m, open) {
|
|
7252
|
+
const stack = [];
|
|
7253
|
+
for (let i = open; i < m.length; i++) {
|
|
7254
|
+
const ch = m[i];
|
|
7255
|
+
if (OPEN[ch] !== void 0) stack.push(OPEN[ch]);
|
|
7256
|
+
else if (ch === ")" || ch === "]" || ch === "}") {
|
|
7257
|
+
if (stack.pop() !== ch) return -1;
|
|
7258
|
+
if (stack.length === 0) return i;
|
|
7259
|
+
}
|
|
7260
|
+
}
|
|
7261
|
+
return -1;
|
|
7262
|
+
}
|
|
7263
|
+
function splitTop(m, from, to) {
|
|
7264
|
+
const parts = [];
|
|
7265
|
+
let depth = 0;
|
|
7266
|
+
let start = from;
|
|
7267
|
+
for (let i = from; i < to; i++) {
|
|
7268
|
+
const ch = m[i];
|
|
7269
|
+
if (OPEN[ch] !== void 0) depth++;
|
|
7270
|
+
else if (ch === ")" || ch === "]" || ch === "}") depth--;
|
|
7271
|
+
else if (ch === "," && depth === 0) {
|
|
7272
|
+
parts.push([start, i]);
|
|
7273
|
+
start = i + 1;
|
|
7274
|
+
}
|
|
7275
|
+
}
|
|
7276
|
+
if (start < to && m.slice(start, to).trim() !== "") parts.push([start, to]);
|
|
7277
|
+
return parts;
|
|
7278
|
+
}
|
|
7279
|
+
function listTs(dir, out = []) {
|
|
7280
|
+
let names;
|
|
7281
|
+
try {
|
|
7282
|
+
names = readdirSync7(dir);
|
|
7283
|
+
} catch {
|
|
7284
|
+
return out;
|
|
7285
|
+
}
|
|
7286
|
+
for (const n of names) {
|
|
7287
|
+
if (n === "node_modules" || n === "_generated" || n === "__tests__") continue;
|
|
7288
|
+
const p = join18(dir, n);
|
|
7289
|
+
let st;
|
|
7290
|
+
try {
|
|
7291
|
+
st = statSync8(p);
|
|
7292
|
+
} catch {
|
|
7293
|
+
continue;
|
|
7294
|
+
}
|
|
7295
|
+
if (st.isDirectory()) listTs(p, out);
|
|
7296
|
+
else if (n.endsWith(".ts") && !n.endsWith(".test.ts") && !n.endsWith(".d.ts")) out.push(p);
|
|
7297
|
+
}
|
|
7298
|
+
return out;
|
|
7299
|
+
}
|
|
7300
|
+
function discoverBackend(root) {
|
|
7301
|
+
let dir = root;
|
|
7302
|
+
try {
|
|
7303
|
+
if (!statSync8(root).isDirectory()) dir = dirname10(root);
|
|
7304
|
+
} catch {
|
|
7305
|
+
return void 0;
|
|
7306
|
+
}
|
|
7307
|
+
for (let up = 0; up < 5; up++) {
|
|
7308
|
+
const cand = join18(dir, "convex");
|
|
7309
|
+
if (existsSync15(join18(cand, "schema.ts"))) return cand;
|
|
7310
|
+
const parent = dirname10(dir);
|
|
7311
|
+
if (parent === dir) break;
|
|
7312
|
+
dir = parent;
|
|
7313
|
+
}
|
|
7314
|
+
return void 0;
|
|
7315
|
+
}
|
|
7316
|
+
function readDoors(backendRoot) {
|
|
7317
|
+
const doors = /* @__PURE__ */ new Map();
|
|
7318
|
+
for (const path of listTs(backendRoot)) {
|
|
7319
|
+
let text;
|
|
7320
|
+
try {
|
|
7321
|
+
text = readFileSync11(path, "utf8");
|
|
7322
|
+
} catch {
|
|
7323
|
+
continue;
|
|
7324
|
+
}
|
|
7325
|
+
const file = {
|
|
7326
|
+
path,
|
|
7327
|
+
rel: relative4(backendRoot, path),
|
|
7328
|
+
lines: text.split("\n").map((raw, i) => ({ n: i + 1, raw, comment: false }))
|
|
7329
|
+
};
|
|
7330
|
+
const m = masked(file);
|
|
7331
|
+
const module = relative4(backendRoot, path).replace(/\.ts$/, "").split("\\").join("/");
|
|
7332
|
+
const re = /\bexport\s+const\s+([A-Za-z_$][\w$]*)\s*=\s*(?:query|mutation|action)\s*\(/g;
|
|
7333
|
+
for (let hit = re.exec(m); hit; hit = re.exec(m)) {
|
|
7334
|
+
const open = hit.index + hit[0].length - 1;
|
|
7335
|
+
const close = matchClose(m, open);
|
|
7336
|
+
if (close < 0) continue;
|
|
7337
|
+
let start = hit.index;
|
|
7338
|
+
const lines = text.slice(0, start).split("\n");
|
|
7339
|
+
let k = lines.length - 2;
|
|
7340
|
+
while (k >= 0 && (lines[k].trim() === "" || /^\s*(\/\/|\/\*|\*)/.test(lines[k])) && lines.length - 2 - k < 60)
|
|
7341
|
+
k--;
|
|
7342
|
+
start = lines.slice(0, k + 1).join("\n").length;
|
|
7343
|
+
const region = text.slice(start, close + 1);
|
|
7344
|
+
const mRegion = m.slice(hit.index, close + 1);
|
|
7345
|
+
const claims = [];
|
|
7346
|
+
const am = /\bargs\s*:\s*/.exec(mRegion);
|
|
7347
|
+
if (am) {
|
|
7348
|
+
const at = hit.index + am.index + am[0].length;
|
|
7349
|
+
let argsText = "";
|
|
7350
|
+
if (m[at] === "{") argsText = text.slice(at, matchClose(m, at) + 1);
|
|
7351
|
+
else {
|
|
7352
|
+
const id = /^[A-Za-z_$][\w$]*/.exec(m.slice(at));
|
|
7353
|
+
if (id) {
|
|
7354
|
+
const dm = new RegExp(`\\bconst\\s+${id[0]}\\b[^=]*=\\s*`).exec(m);
|
|
7355
|
+
if (dm) {
|
|
7356
|
+
let p = dm.index + dm[0].length;
|
|
7357
|
+
if (m.slice(p).startsWith("v.object(")) p += "v.object(".length;
|
|
7358
|
+
if (m[p] === "{") argsText = text.slice(p, matchClose(m, p) + 1);
|
|
7359
|
+
}
|
|
7360
|
+
}
|
|
7361
|
+
}
|
|
7362
|
+
for (const c of argsText.match(ANY_CLAIM) ?? []) if (!claims.includes(c)) claims.push(c);
|
|
7363
|
+
}
|
|
7364
|
+
let exit;
|
|
7365
|
+
if (/\brequireServiceAccount\b/.test(mRegion))
|
|
7366
|
+
exit = "service-account-only (requireServiceAccount)";
|
|
7367
|
+
else if (/\brequireResolvedCaller\s*\([^)]*masterOnly[^)]*mcpBoundOnly|\brequireResolvedCaller\s*\([^)]*mcpBoundOnly[^)]*masterOnly/s.test(
|
|
7368
|
+
mRegion
|
|
7369
|
+
))
|
|
7370
|
+
exit = "service-account-only (requireResolvedCaller masterOnly+mcpBoundOnly)";
|
|
7371
|
+
else {
|
|
7372
|
+
const fw = FLEET_WIDE.exec(region);
|
|
7373
|
+
if (fw) exit = `fleet-wide declaration (${fw[0].slice(11).trim().slice(0, 80)})`;
|
|
7374
|
+
}
|
|
7375
|
+
doors.set(`${module}:${hit[1]}`, {
|
|
7376
|
+
claims,
|
|
7377
|
+
exit,
|
|
7378
|
+
resolvesCaller: SCOPE_MARKERS.test(mRegion)
|
|
7379
|
+
});
|
|
7380
|
+
}
|
|
7381
|
+
}
|
|
7382
|
+
return doors;
|
|
7383
|
+
}
|
|
7384
|
+
function definitionOf(name, files) {
|
|
7385
|
+
const esc = name.replace(/\$/g, "\\$");
|
|
7386
|
+
const re = new RegExp(`(?:\\b(?:const|let|var)\\s+${esc}\\b|\\bfunction\\s+${esc}\\b)`);
|
|
7387
|
+
for (const f of files) {
|
|
7388
|
+
const hit = re.exec(f.m);
|
|
7389
|
+
if (!hit) continue;
|
|
7390
|
+
let depth = 0;
|
|
7391
|
+
for (let i = hit.index + hit[0].length; i < f.m.length; i++) {
|
|
7392
|
+
const ch = f.m[i];
|
|
7393
|
+
if (OPEN[ch] !== void 0) {
|
|
7394
|
+
depth++;
|
|
7395
|
+
} else if (ch === ")" || ch === "]" || ch === "}") {
|
|
7396
|
+
depth--;
|
|
7397
|
+
if (depth === 0 && ch === "}" && /\bfunction\b/.test(hit[0]))
|
|
7398
|
+
return { mask: f.m.slice(hit.index, i + 1) };
|
|
7399
|
+
} else if (ch === ";" && depth === 0 && !/\bfunction\b/.test(hit[0])) {
|
|
7400
|
+
return { mask: f.m.slice(hit.index, i + 1) };
|
|
7401
|
+
}
|
|
7402
|
+
if (depth < 0) break;
|
|
7403
|
+
}
|
|
7404
|
+
return { mask: f.m.slice(hit.index, Math.min(f.m.length, hit.index + 1500)) };
|
|
7405
|
+
}
|
|
7406
|
+
return void 0;
|
|
7407
|
+
}
|
|
7408
|
+
function readHelper(name, files, seen = /* @__PURE__ */ new Set(), depth = 0) {
|
|
7409
|
+
if (seen.has(name) || depth > 3) return { forwards: false, optional: false };
|
|
7410
|
+
seen.add(name);
|
|
7411
|
+
const def = definitionOf(name, files);
|
|
7412
|
+
if (!def) return { forwards: false, optional: false };
|
|
7413
|
+
let forwards = CLAIM_TOKEN.test(def.mask);
|
|
7414
|
+
const optional = /:\s*\{\s*\}|return\s*\{\s*\}|\?\?\s*\{\s*\}/.test(def.mask);
|
|
7415
|
+
if (!forwards) {
|
|
7416
|
+
for (const c of def.mask.matchAll(/\b([A-Za-z_$][\w$]*)\s*\(/g)) {
|
|
7417
|
+
const sub = readHelper(c[1], files, seen, depth + 1);
|
|
7418
|
+
if (sub.forwards) {
|
|
7419
|
+
forwards = true;
|
|
7420
|
+
break;
|
|
7421
|
+
}
|
|
7422
|
+
}
|
|
7423
|
+
}
|
|
7424
|
+
return { forwards, optional };
|
|
7425
|
+
}
|
|
7426
|
+
function toolNameBefore(text, offset) {
|
|
7427
|
+
const head = text.slice(0, offset);
|
|
7428
|
+
const open = Math.max(head.lastIndexOf("defineTool("), head.lastIndexOf("registerTool("));
|
|
7429
|
+
if (open >= 0) {
|
|
7430
|
+
const m = /^\s*"([a-z][a-z0-9_]*)",?\s*$/m.exec(head.slice(open));
|
|
7431
|
+
if (m) return { name: m[1], start: open };
|
|
7432
|
+
const r = /registerTool\(\s*"([a-z][a-z0-9_]*)"/.exec(head.slice(open));
|
|
7433
|
+
if (r) return { name: r[1], start: open };
|
|
7434
|
+
return { name: void 0, start: open };
|
|
7435
|
+
}
|
|
7436
|
+
const lines = head.split("\n");
|
|
7437
|
+
const back = Math.max(0, lines.length - 120);
|
|
7438
|
+
return { name: void 0, start: lines.slice(0, back).join("\n").length };
|
|
7439
|
+
}
|
|
7440
|
+
function scanS28(tree) {
|
|
7441
|
+
const scan = { violate: [], conform: [], cnj: [], calls: 0, backend: void 0 };
|
|
7442
|
+
const backendRoot = tree.backendRoot ?? discoverBackend(tree.root);
|
|
7443
|
+
scan.backend = backendRoot;
|
|
7444
|
+
const doors = backendRoot ? readDoors(backendRoot) : void 0;
|
|
7445
|
+
const files = tree.files.map((f) => ({ f, text: rawText(f), m: masked(f) }));
|
|
7446
|
+
const actorDoors = /* @__PURE__ */ new Set();
|
|
7447
|
+
for (const { text } of files) {
|
|
7448
|
+
const hit = ACTOR_SET_RE.exec(text);
|
|
7449
|
+
if (hit) for (const s of hit[2].matchAll(/["'`]([\w/]+:[\w]+)["'`]/g)) actorDoors.add(s[1]);
|
|
7450
|
+
}
|
|
7451
|
+
for (const { f, text, m } of files) {
|
|
7452
|
+
for (let hit = CALL_RE.exec(m); hit; hit = CALL_RE.exec(m)) {
|
|
7453
|
+
const recv = hit[1];
|
|
7454
|
+
if (!/convex|client/i.test(recv)) continue;
|
|
7455
|
+
const open = hit.index + hit[0].length - 1;
|
|
7456
|
+
const close = matchClose(m, open);
|
|
7457
|
+
if (close < 0) continue;
|
|
7458
|
+
scan.calls++;
|
|
7459
|
+
const line = lineAt(text, hit.index);
|
|
7460
|
+
const tool = toolNameBefore(text, hit.index);
|
|
7461
|
+
const parts = splitTop(m, open + 1, close);
|
|
7462
|
+
let door;
|
|
7463
|
+
if (parts[0]) {
|
|
7464
|
+
const rawFirst = text.slice(parts[0][0], parts[0][1]);
|
|
7465
|
+
const lit = /^\s*(?:\/\/[^\n]*\n\s*)*["'`]([\w/]+:[\w]+)["'`]/.exec(rawFirst);
|
|
7466
|
+
const ref = /^\s*(?:api|internal)\.([\w]+)\.([\w]+)/.exec(rawFirst);
|
|
7467
|
+
if (lit) door = lit[1];
|
|
7468
|
+
else if (ref) door = `${ref[1]}:${ref[2]}`;
|
|
7469
|
+
}
|
|
7470
|
+
const site = (note) => ({
|
|
7471
|
+
file: f.rel,
|
|
7472
|
+
line,
|
|
7473
|
+
door: door ?? "?",
|
|
7474
|
+
tool: tool.name,
|
|
7475
|
+
note
|
|
7476
|
+
});
|
|
7477
|
+
if (door === void 0) {
|
|
7478
|
+
scan.cnj.push(
|
|
7479
|
+
site(
|
|
7480
|
+
"the door is named by an expression the scan cannot resolve (not statically resolvable)"
|
|
7481
|
+
)
|
|
7482
|
+
);
|
|
7483
|
+
continue;
|
|
7484
|
+
}
|
|
7485
|
+
const argText = parts[1] ? m.slice(parts[1][0], parts[1][1]) : "";
|
|
7486
|
+
const region = m.slice(tool.start, hit.index);
|
|
7487
|
+
const regionRaw = text.slice(tool.start, hit.index);
|
|
7488
|
+
const guard = GUARD_RE.test(region) || GUARD_RE.test(regionRaw);
|
|
7489
|
+
let direct = CLAIM_TOKEN.test(argText);
|
|
7490
|
+
let viaHelper;
|
|
7491
|
+
let virtual = argText;
|
|
7492
|
+
const bare = /^\s*([A-Za-z_$][\w$]*)\s*$/.exec(argText);
|
|
7493
|
+
if (bare && !direct) {
|
|
7494
|
+
const v = bare[1].replace(/\$/g, "\\$");
|
|
7495
|
+
const pieces = [];
|
|
7496
|
+
for (const a of region.matchAll(new RegExp(`Object\\.assign\\s*\\(\\s*${v}\\s*,`, "g"))) {
|
|
7497
|
+
const o = a.index + a[0].length - 1;
|
|
7498
|
+
const c = matchClose(region, region.lastIndexOf("(", o));
|
|
7499
|
+
pieces.push(region.slice(o, c < 0 ? o + 400 : c));
|
|
7500
|
+
}
|
|
7501
|
+
for (const a of region.matchAll(
|
|
7502
|
+
new RegExp(`\\b${v}\\.verified(?:Org|Person|Actor)\\b`, "g")
|
|
7503
|
+
))
|
|
7504
|
+
pieces.push(a[0]);
|
|
7505
|
+
const decl = new RegExp(`\\b(?:const|let)\\s+${v}\\b[^=]*=\\s*\\{`).exec(region);
|
|
7506
|
+
if (decl) {
|
|
7507
|
+
const o = decl.index + decl[0].length - 1;
|
|
7508
|
+
const c = matchClose(region, o);
|
|
7509
|
+
pieces.push(region.slice(o, c < 0 ? o + 600 : c + 1));
|
|
7510
|
+
}
|
|
7511
|
+
virtual = pieces.join("\n");
|
|
7512
|
+
direct = CLAIM_TOKEN.test(virtual);
|
|
7513
|
+
}
|
|
7514
|
+
if (!direct) {
|
|
7515
|
+
for (const sp of virtual.matchAll(/\.\.\.\s*([A-Za-z_$][\w$]*)/g)) {
|
|
7516
|
+
const h = readHelper(sp[1], files);
|
|
7517
|
+
if (h.forwards) {
|
|
7518
|
+
viaHelper = h;
|
|
7519
|
+
break;
|
|
7520
|
+
}
|
|
7521
|
+
}
|
|
7522
|
+
if (!viaHelper) {
|
|
7523
|
+
for (const sp of virtual.matchAll(/\b([A-Za-z_$][\w$]*)\s*\(\s*\)/g)) {
|
|
7524
|
+
const h = readHelper(sp[1], files);
|
|
7525
|
+
if (h.forwards) {
|
|
7526
|
+
viaHelper = h;
|
|
7527
|
+
break;
|
|
7528
|
+
}
|
|
7529
|
+
}
|
|
7530
|
+
}
|
|
7531
|
+
}
|
|
7532
|
+
if (direct) {
|
|
7533
|
+
scan.conform.push(site("forwards the verified claim in the call"));
|
|
7534
|
+
continue;
|
|
7535
|
+
}
|
|
7536
|
+
if (viaHelper) {
|
|
7537
|
+
if (viaHelper.optional && !guard) {
|
|
7538
|
+
scan.violate.push(
|
|
7539
|
+
site(
|
|
7540
|
+
"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"
|
|
7541
|
+
)
|
|
7542
|
+
);
|
|
7543
|
+
} else {
|
|
7544
|
+
scan.conform.push(
|
|
7545
|
+
site(
|
|
7546
|
+
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"
|
|
7547
|
+
)
|
|
7548
|
+
);
|
|
7549
|
+
}
|
|
7550
|
+
continue;
|
|
7551
|
+
}
|
|
7552
|
+
if (!doors) {
|
|
7553
|
+
scan.cnj.push(
|
|
7554
|
+
site(
|
|
7555
|
+
"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"
|
|
7556
|
+
)
|
|
7557
|
+
);
|
|
7558
|
+
continue;
|
|
7559
|
+
}
|
|
7560
|
+
const info = doors.get(door);
|
|
7561
|
+
if (!info) {
|
|
7562
|
+
scan.cnj.push(
|
|
7563
|
+
site(
|
|
7564
|
+
`forwards no verified claim and the door ${door} is not defined in the supplied backend (${backendRoot})`
|
|
7565
|
+
)
|
|
7566
|
+
);
|
|
7567
|
+
continue;
|
|
7568
|
+
}
|
|
7569
|
+
if (info.exit) {
|
|
7570
|
+
scan.conform.push(site(`needs no claim: the door is ${info.exit}`));
|
|
7571
|
+
continue;
|
|
7572
|
+
}
|
|
7573
|
+
if (!info.resolvesCaller) {
|
|
7574
|
+
scan.cnj.push(
|
|
7575
|
+
site(
|
|
7576
|
+
`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)`
|
|
7577
|
+
)
|
|
7578
|
+
);
|
|
7579
|
+
continue;
|
|
7580
|
+
}
|
|
7581
|
+
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";
|
|
7582
|
+
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" : "";
|
|
7583
|
+
scan.violate.push(site(`forwards no verified claim \u2014 ${accepts}${actorNote}`));
|
|
7584
|
+
}
|
|
7585
|
+
}
|
|
7586
|
+
return scan;
|
|
7587
|
+
}
|
|
7588
|
+
function fmt(s) {
|
|
7589
|
+
return `${s.file}:${s.line} ${s.door} ${s.tool ? `[${s.tool}] ` : ""}(${s.note})`;
|
|
7590
|
+
}
|
|
7591
|
+
var S28_ID = "S-28 claim-forwarded";
|
|
7592
|
+
var s28ClaimForwarded = {
|
|
7593
|
+
id: S28_ID,
|
|
7594
|
+
rule: "S-28",
|
|
7595
|
+
pin: PIN11,
|
|
7596
|
+
run(tree) {
|
|
7597
|
+
const s = scanS28(tree);
|
|
7598
|
+
const breakdown = `[conform=${s.conform.length} non-conforming=${s.violate.length} could-not-judge=${s.cnj.length}]`;
|
|
7599
|
+
const list = (xs) => xs.slice(0, 5).map(fmt).join("; ");
|
|
7600
|
+
if (s.violate.length > 0) {
|
|
7601
|
+
const first = s.violate[0];
|
|
7602
|
+
return {
|
|
7603
|
+
id: S28_ID,
|
|
7604
|
+
anchor: `${first.file}:${first.line}`,
|
|
7605
|
+
state: "non-conforming",
|
|
7606
|
+
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)}.` : ""}`
|
|
7607
|
+
};
|
|
7608
|
+
}
|
|
7609
|
+
if (s.cnj.length > 0) {
|
|
7610
|
+
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}`;
|
|
7611
|
+
return {
|
|
7612
|
+
id: S28_ID,
|
|
7613
|
+
anchor: `${s.cnj[0].file}:${s.cnj[0].line}`,
|
|
7614
|
+
state: "could-not-judge",
|
|
7615
|
+
verdict: reason2,
|
|
7616
|
+
reason: reason2
|
|
7617
|
+
};
|
|
7618
|
+
}
|
|
7619
|
+
if (s.conform.length > 0) {
|
|
7620
|
+
return {
|
|
7621
|
+
id: S28_ID,
|
|
7622
|
+
anchor: `${s.conform[0].file}:${s.conform[0].line}`,
|
|
7623
|
+
state: "present",
|
|
7624
|
+
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)}.`
|
|
7625
|
+
};
|
|
7626
|
+
}
|
|
7627
|
+
const usesBackend = tree.files.some(
|
|
7628
|
+
(f) => f.lines.some((l) => !l.comment && /\bConvexHttpClient\b|from\s+["']convex\//.test(l.raw))
|
|
7629
|
+
);
|
|
7630
|
+
if (!usesBackend) {
|
|
7631
|
+
return {
|
|
7632
|
+
id: S28_ID,
|
|
7633
|
+
anchor: "source:no-backend-client",
|
|
7634
|
+
state: "absent",
|
|
7635
|
+
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).`
|
|
7636
|
+
};
|
|
7637
|
+
}
|
|
7638
|
+
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.`;
|
|
7639
|
+
return {
|
|
7640
|
+
id: S28_ID,
|
|
7641
|
+
anchor: "source:no-applicable-site",
|
|
7642
|
+
state: "could-not-judge",
|
|
7643
|
+
verdict: reason,
|
|
7644
|
+
reason
|
|
7645
|
+
};
|
|
7646
|
+
}
|
|
7647
|
+
};
|
|
7648
|
+
|
|
6504
7649
|
// src/detectors/index.ts
|
|
6505
7650
|
var detectors = [
|
|
6506
7651
|
d2,
|
|
@@ -6534,6 +7679,7 @@ var detectors = [
|
|
|
6534
7679
|
c22TestRatioGate,
|
|
6535
7680
|
c25BuildIdentity,
|
|
6536
7681
|
c26SubpackageLockfile,
|
|
7682
|
+
s28ClaimForwarded,
|
|
6537
7683
|
dServer2,
|
|
6538
7684
|
dServer6,
|
|
6539
7685
|
dDebugOptIn,
|
|
@@ -6547,23 +7693,24 @@ var detectors = [
|
|
|
6547
7693
|
dExt5,
|
|
6548
7694
|
dExt7,
|
|
6549
7695
|
dExt8,
|
|
6550
|
-
dExt9
|
|
7696
|
+
dExt9,
|
|
7697
|
+
localIdentity
|
|
6551
7698
|
];
|
|
6552
7699
|
|
|
6553
7700
|
// src/runtime/launch.ts
|
|
6554
7701
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
6555
|
-
import { existsSync as
|
|
6556
|
-
import { dirname as
|
|
7702
|
+
import { existsSync as existsSync16, readFileSync as readFileSync12, statSync as statSync9 } from "fs";
|
|
7703
|
+
import { dirname as dirname11, isAbsolute, join as join19, resolve } from "path";
|
|
6557
7704
|
function findPackageDir(target) {
|
|
6558
7705
|
let dir;
|
|
6559
7706
|
try {
|
|
6560
|
-
dir =
|
|
7707
|
+
dir = statSync9(target).isDirectory() ? target : dirname11(target);
|
|
6561
7708
|
} catch {
|
|
6562
7709
|
return null;
|
|
6563
7710
|
}
|
|
6564
7711
|
for (let depth = 0; depth < 8; depth++) {
|
|
6565
|
-
if (
|
|
6566
|
-
const parent =
|
|
7712
|
+
if (existsSync16(join19(dir, "package.json"))) return dir;
|
|
7713
|
+
const parent = dirname11(dir);
|
|
6567
7714
|
if (parent === dir) break;
|
|
6568
7715
|
dir = parent;
|
|
6569
7716
|
}
|
|
@@ -6587,10 +7734,10 @@ function deriveLaunchPlan(target) {
|
|
|
6587
7734
|
}
|
|
6588
7735
|
let pkg;
|
|
6589
7736
|
try {
|
|
6590
|
-
pkg = JSON.parse(
|
|
7737
|
+
pkg = JSON.parse(readFileSync12(join19(pkgDir, "package.json"), "utf8"));
|
|
6591
7738
|
} catch (err) {
|
|
6592
7739
|
return {
|
|
6593
|
-
error: `could not parse ${
|
|
7740
|
+
error: `could not parse ${join19(pkgDir, "package.json")}: ${err instanceof Error ? err.message : String(err)}`
|
|
6594
7741
|
};
|
|
6595
7742
|
}
|
|
6596
7743
|
const bin = firstBinEntry(pkg.bin);
|
|
@@ -6668,10 +7815,10 @@ function readEvalCounts(target) {
|
|
|
6668
7815
|
if (!pkgDir) return counts;
|
|
6669
7816
|
let dir = pkgDir;
|
|
6670
7817
|
for (let depth = 0; depth < 8; depth++) {
|
|
6671
|
-
const candidate =
|
|
6672
|
-
if (
|
|
7818
|
+
const candidate = join19(dir, "evals", "evals.json");
|
|
7819
|
+
if (existsSync16(candidate)) {
|
|
6673
7820
|
try {
|
|
6674
|
-
const parsed = JSON.parse(
|
|
7821
|
+
const parsed = JSON.parse(readFileSync12(candidate, "utf8"));
|
|
6675
7822
|
for (const c of parsed.cases ?? []) {
|
|
6676
7823
|
if (c && typeof c.tool === "string") counts.set(c.tool, (counts.get(c.tool) ?? 0) + 1);
|
|
6677
7824
|
}
|
|
@@ -6679,7 +7826,7 @@ function readEvalCounts(target) {
|
|
|
6679
7826
|
}
|
|
6680
7827
|
return counts;
|
|
6681
7828
|
}
|
|
6682
|
-
const parent =
|
|
7829
|
+
const parent = dirname11(dir);
|
|
6683
7830
|
if (parent === dir) break;
|
|
6684
7831
|
dir = parent;
|
|
6685
7832
|
}
|
|
@@ -6687,7 +7834,7 @@ function readEvalCounts(target) {
|
|
|
6687
7834
|
}
|
|
6688
7835
|
function statSyncSafe(p) {
|
|
6689
7836
|
try {
|
|
6690
|
-
return
|
|
7837
|
+
return statSync9(p).isDirectory();
|
|
6691
7838
|
} catch {
|
|
6692
7839
|
return false;
|
|
6693
7840
|
}
|
|
@@ -6806,8 +7953,11 @@ function reconcile(id, staticR, dynamicR) {
|
|
|
6806
7953
|
};
|
|
6807
7954
|
}
|
|
6808
7955
|
function runDoctor(target, detectorList = detectors, opts = {}) {
|
|
7956
|
+
return runDoctorReport(target, detectorList, opts).results;
|
|
7957
|
+
}
|
|
7958
|
+
function runDoctorReport(target, detectorList = detectors, opts = {}) {
|
|
6809
7959
|
const mode = opts.mode ?? "static";
|
|
6810
|
-
const tree = loadTree(target);
|
|
7960
|
+
const tree = loadTree(target, opts.backend);
|
|
6811
7961
|
const needsRuntime = mode !== "static" && detectorList.some((d) => typeof d.runDynamic === "function");
|
|
6812
7962
|
const runtime = needsRuntime ? launchAndIntrospect(target) : null;
|
|
6813
7963
|
const results = [];
|
|
@@ -6840,7 +7990,7 @@ function runDoctor(target, detectorList = detectors, opts = {}) {
|
|
|
6840
7990
|
results.push(reconcile(d.id, staticR, dynamicR));
|
|
6841
7991
|
}
|
|
6842
7992
|
}
|
|
6843
|
-
return results;
|
|
7993
|
+
return { results, skipped: tree.skippedDirs };
|
|
6844
7994
|
}
|
|
6845
7995
|
async function invokeLiveChecked(d, session) {
|
|
6846
7996
|
if (!session) {
|
|
@@ -6895,14 +8045,32 @@ async function runDoctorAsync(target, opts = {}) {
|
|
|
6895
8045
|
}
|
|
6896
8046
|
return results;
|
|
6897
8047
|
}
|
|
6898
|
-
function
|
|
8048
|
+
function renderSkipped(skipped) {
|
|
8049
|
+
if (skipped.length === 0) {
|
|
8050
|
+
return "skipped directories (0) \u2014 none: no directory was skipped on this run.";
|
|
8051
|
+
}
|
|
8052
|
+
const sorted = [...skipped].sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
|
|
8053
|
+
const head = `skipped directories (${sorted.length}) \u2014 NOT read on this run; a verdict above says nothing about what lies inside:`;
|
|
8054
|
+
const rows = sorted.map(
|
|
8055
|
+
(d) => `${d.path} | ${d.reason}${d.candidate ? ` | holds a deployment configuration: ${d.candidate}` : ""}`
|
|
8056
|
+
);
|
|
8057
|
+
return [head, "-".repeat(head.length), ...rows].join("\n");
|
|
8058
|
+
}
|
|
8059
|
+
function renderTable(surface, results, skipped) {
|
|
8060
|
+
const body = renderResults(surface, results);
|
|
8061
|
+
const section = skipped === void 0 ? "skipped directories: NOT REPORTED \u2014 the caller did not pass this run's skipped list (use runDoctorReport), so the scope above may be narrower than it reads." : renderSkipped(skipped);
|
|
8062
|
+
return `${body}
|
|
8063
|
+
|
|
8064
|
+
${section}`;
|
|
8065
|
+
}
|
|
8066
|
+
function renderResults(surface, results) {
|
|
6899
8067
|
const cols = { item: 24, anchor: 26, state: 15 };
|
|
6900
8068
|
const header = `${pad("item", cols.item)} | ${pad("file:line", cols.anchor)} | ${pad("state", cols.state)} | verdict`;
|
|
6901
|
-
const
|
|
8069
|
+
const sep3 = "-".repeat(header.length);
|
|
6902
8070
|
const rows = results.map(
|
|
6903
8071
|
(r) => `${pad(r.id, cols.item)} | ${pad(r.anchor, cols.anchor)} | ${pad(r.state, cols.state)} | ${r.verdict}`
|
|
6904
8072
|
);
|
|
6905
|
-
const table = [`surface: ${surface}`, header,
|
|
8073
|
+
const table = [`surface: ${surface}`, header, sep3, ...rows].join("\n");
|
|
6906
8074
|
const unjudged = results.filter((r) => r.state === "could-not-judge");
|
|
6907
8075
|
if (unjudged.length === 0) return table;
|
|
6908
8076
|
const cnjHeader = `could-not-judge (${unjudged.length}) \u2014 the doctor could NOT read these; this is NOT a clean bill:`;
|
|
@@ -6918,11 +8086,16 @@ function computeExitCode(results) {
|
|
|
6918
8086
|
return 0;
|
|
6919
8087
|
}
|
|
6920
8088
|
var USAGE = [
|
|
6921
|
-
"usage: mcp-doctor <source-tree-path> [--mode=static|dynamic|both]",
|
|
8089
|
+
"usage: mcp-doctor <source-tree-path> [--mode=static|dynamic|both] [--backend=<convex-dir>]",
|
|
6922
8090
|
"",
|
|
6923
8091
|
" --mode=static read the tree only (default)",
|
|
6924
8092
|
" --mode=dynamic launch the server and judge what it serves",
|
|
6925
8093
|
" --mode=both both, dynamic findings taking precedence",
|
|
8094
|
+
" --local-identity-ceiling=N",
|
|
8095
|
+
" local identity primitives tolerated before DLocalIdentity",
|
|
8096
|
+
" fails (default 0)",
|
|
8097
|
+
" --backend=DIR the backend source (a convex/ dir) whose door list S-28 reads;",
|
|
8098
|
+
" default: the nearest convex/ above the tree",
|
|
6926
8099
|
" --version print the version and exit",
|
|
6927
8100
|
" --help print this and exit",
|
|
6928
8101
|
"",
|
|
@@ -6930,8 +8103,8 @@ var USAGE = [
|
|
|
6930
8103
|
].join("\n");
|
|
6931
8104
|
function readVersion() {
|
|
6932
8105
|
try {
|
|
6933
|
-
const here =
|
|
6934
|
-
const manifest = JSON.parse(
|
|
8106
|
+
const here = dirname12(fileURLToPath(import.meta.url));
|
|
8107
|
+
const manifest = JSON.parse(readFileSync13(join20(here, "..", "package.json"), "utf8"));
|
|
6935
8108
|
return manifest.version ?? null;
|
|
6936
8109
|
} catch {
|
|
6937
8110
|
return null;
|
|
@@ -6953,6 +8126,7 @@ function main() {
|
|
|
6953
8126
|
return;
|
|
6954
8127
|
}
|
|
6955
8128
|
const modeArg = args.find((a) => a.startsWith("--mode="));
|
|
8129
|
+
const backendArg = args.find((a) => a.startsWith("--backend="));
|
|
6956
8130
|
const target = args.find((a) => !a.startsWith("--"));
|
|
6957
8131
|
if (!target) {
|
|
6958
8132
|
console.error(USAGE);
|
|
@@ -6963,8 +8137,24 @@ function main() {
|
|
|
6963
8137
|
console.error(`unknown --mode "${mode}" \u2014 expected static|dynamic|both`);
|
|
6964
8138
|
process.exit(1);
|
|
6965
8139
|
}
|
|
6966
|
-
const
|
|
6967
|
-
|
|
8140
|
+
const ceilingArg = args.find((a) => a.startsWith("--local-identity-ceiling="));
|
|
8141
|
+
let detectorList = detectors;
|
|
8142
|
+
if (ceilingArg) {
|
|
8143
|
+
const raw = ceilingArg.slice("--local-identity-ceiling=".length);
|
|
8144
|
+
const ceiling = /^\d+$/.test(raw) ? Number(raw) : Number.NaN;
|
|
8145
|
+
if (!Number.isInteger(ceiling)) {
|
|
8146
|
+
console.error(`invalid --local-identity-ceiling "${raw}" \u2014 expected a non-negative integer`);
|
|
8147
|
+
process.exit(1);
|
|
8148
|
+
}
|
|
8149
|
+
const ratchet = makeLocalIdentity({ ceiling });
|
|
8150
|
+
detectorList = detectors.map((d) => d === localIdentity ? ratchet : d);
|
|
8151
|
+
}
|
|
8152
|
+
const backend = backendArg ? backendArg.slice("--backend=".length) : void 0;
|
|
8153
|
+
const { results, skipped } = runDoctorReport(target, detectorList, {
|
|
8154
|
+
mode,
|
|
8155
|
+
...backend !== void 0 ? { backend } : {}
|
|
8156
|
+
});
|
|
8157
|
+
console.log(renderTable(target, results, skipped));
|
|
6968
8158
|
process.exitCode = computeExitCode(results);
|
|
6969
8159
|
}
|
|
6970
8160
|
function isDirectInvocation() {
|
|
@@ -6986,7 +8176,9 @@ if (isDirectInvocation()) main();
|
|
|
6986
8176
|
export {
|
|
6987
8177
|
computeExitCode,
|
|
6988
8178
|
isLiveDetector,
|
|
8179
|
+
renderSkipped,
|
|
6989
8180
|
renderTable,
|
|
6990
8181
|
runDoctor,
|
|
6991
|
-
runDoctorAsync
|
|
8182
|
+
runDoctorAsync,
|
|
8183
|
+
runDoctorReport
|
|
6992
8184
|
};
|