@xagents_in/unmask 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +123 -2
- package/dist/cli/hosted.mjs +251 -0
- package/dist/cli/main.mjs +210 -0
- package/dist/core/acquire.mjs +283 -0
- package/dist/core/archive.mjs +485 -0
- package/dist/core/compare.mjs +230 -0
- package/dist/core/manifests.mjs +548 -0
- package/dist/core/online.mjs +310 -0
- package/dist/core/pipeline.mjs +345 -0
- package/dist/core/policy.mjs +92 -0
- package/dist/core/report.mjs +506 -0
- package/dist/core/resolve.mjs +186 -0
- package/dist/core/source.mjs +106 -0
- package/dist/core/syntax.mjs +340 -0
- package/dist/model/client.mjs +1257 -0
- package/dist/model/config.mjs +90 -0
- package/dist/server/app.mjs +299 -0
- package/dist/server/main.mjs +146 -0
- package/dist/server/queue.mjs +177 -0
- package/dist/server/runner.mjs +280 -0
- package/dist/server/store.mjs +105 -0
- package/dist/server/worker.mjs +57 -0
- package/dist/version.json +1 -0
- package/dist/viewer/render.mjs +255 -0
- package/dist/viewer/shell.mjs +73 -0
- package/package.json +39 -4
|
@@ -0,0 +1,548 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { validateReport } from "./report.mjs";
|
|
3
|
+
|
|
4
|
+
export const MANIFEST_LIMITS = Object.freeze({
|
|
5
|
+
fieldNodes: 4096,
|
|
6
|
+
depth: 12,
|
|
7
|
+
valueBytes: 65536,
|
|
8
|
+
totalValueBytes: 262144,
|
|
9
|
+
fieldComparisons: 8192,
|
|
10
|
+
cues: 128,
|
|
11
|
+
declarations: 256,
|
|
12
|
+
pathComparisons: 20000,
|
|
13
|
+
annotations: 512,
|
|
14
|
+
});
|
|
15
|
+
const fields = [
|
|
16
|
+
["dependencies"],
|
|
17
|
+
["optionalDependencies"],
|
|
18
|
+
["peerDependencies"],
|
|
19
|
+
["bundledDependencies"],
|
|
20
|
+
["bundleDependencies"],
|
|
21
|
+
["peerDependenciesMeta"],
|
|
22
|
+
["main"],
|
|
23
|
+
["module"],
|
|
24
|
+
["bin"],
|
|
25
|
+
["exports"],
|
|
26
|
+
["scripts", "preinstall"],
|
|
27
|
+
["scripts", "install"],
|
|
28
|
+
["scripts", "postinstall"],
|
|
29
|
+
["name"],
|
|
30
|
+
["version"],
|
|
31
|
+
["browser"],
|
|
32
|
+
["types"],
|
|
33
|
+
["typings"],
|
|
34
|
+
["os"],
|
|
35
|
+
["cpu"],
|
|
36
|
+
["engines"],
|
|
37
|
+
["gypfile"],
|
|
38
|
+
];
|
|
39
|
+
const entrypoints = new Set(["main", "module", "bin", "exports"]);
|
|
40
|
+
const dependencyFields = new Set([
|
|
41
|
+
"dependencies",
|
|
42
|
+
"optionalDependencies",
|
|
43
|
+
"peerDependencies",
|
|
44
|
+
"bundledDependencies",
|
|
45
|
+
"bundleDependencies",
|
|
46
|
+
]);
|
|
47
|
+
const hash = (value) => createHash("sha256").update(value).digest("hex");
|
|
48
|
+
const pointer = (parts) =>
|
|
49
|
+
"/" +
|
|
50
|
+
parts
|
|
51
|
+
.map((part) => String(part).replace(/~/g, "~0").replace(/\//g, "~1"))
|
|
52
|
+
.join("/");
|
|
53
|
+
const absent = () => ({
|
|
54
|
+
known: true,
|
|
55
|
+
present: false,
|
|
56
|
+
type: "absent",
|
|
57
|
+
signature: "absent",
|
|
58
|
+
children: new Map(),
|
|
59
|
+
keys: [],
|
|
60
|
+
});
|
|
61
|
+
class Unassessed extends Error {}
|
|
62
|
+
function wholeEvidence(result, side, inventory) {
|
|
63
|
+
const entry = inventory?.entries.find(
|
|
64
|
+
(item) => item.path === "package.json" && item.type === "file",
|
|
65
|
+
);
|
|
66
|
+
const blob = (
|
|
67
|
+
side === "artifact" ? result.inputs.artifact : result.inputs.sourceArchive
|
|
68
|
+
)?.sha256;
|
|
69
|
+
const existing = result.evidence.find(
|
|
70
|
+
(item) =>
|
|
71
|
+
item.side === side &&
|
|
72
|
+
item.path === "package.json" &&
|
|
73
|
+
item.blobDigest === blob &&
|
|
74
|
+
item.range[0] === 0 &&
|
|
75
|
+
item.range[1] === item.size &&
|
|
76
|
+
(!entry ||
|
|
77
|
+
(item.fileSha256 === entry.sha256 && item.size === entry.size)),
|
|
78
|
+
);
|
|
79
|
+
if (existing) return existing;
|
|
80
|
+
if (!entry || !blob || inventory.digest?.sha256 !== blob) return null;
|
|
81
|
+
const range = [0, entry.size];
|
|
82
|
+
const evidence = {
|
|
83
|
+
id: `ev-${hash(JSON.stringify([side, blob, "package.json", range]))}`,
|
|
84
|
+
side,
|
|
85
|
+
blobDigest: blob,
|
|
86
|
+
path: "package.json",
|
|
87
|
+
originalPath: entry.originalPath,
|
|
88
|
+
fileSha256: entry.sha256,
|
|
89
|
+
size: entry.size,
|
|
90
|
+
range,
|
|
91
|
+
};
|
|
92
|
+
if (Buffer.isBuffer(entry.content)) {
|
|
93
|
+
for (
|
|
94
|
+
let end = Math.min(entry.size, 512);
|
|
95
|
+
end >= Math.max(0, Math.min(entry.size, 512) - 3);
|
|
96
|
+
end--
|
|
97
|
+
) {
|
|
98
|
+
try {
|
|
99
|
+
evidence.excerpt = new TextDecoder("utf-8", {
|
|
100
|
+
fatal: true,
|
|
101
|
+
ignoreBOM: true,
|
|
102
|
+
}).decode(entry.content.subarray(0, end));
|
|
103
|
+
evidence.excerptRange = [0, end];
|
|
104
|
+
break;
|
|
105
|
+
} catch {}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
if (!result.evidence.some((item) => item.id === evidence.id))
|
|
109
|
+
result.evidence.push(evidence);
|
|
110
|
+
return evidence;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function addManifestObservations(
|
|
114
|
+
report,
|
|
115
|
+
{ artifact, source, limits: overrides } = {},
|
|
116
|
+
) {
|
|
117
|
+
validateReport(report);
|
|
118
|
+
const limits = {
|
|
119
|
+
...MANIFEST_LIMITS,
|
|
120
|
+
...(overrides ?? report.inputs.limits?.manifest ?? {}),
|
|
121
|
+
};
|
|
122
|
+
for (const [key, value] of Object.entries(limits))
|
|
123
|
+
if (
|
|
124
|
+
!Object.hasOwn(MANIFEST_LIMITS, key) ||
|
|
125
|
+
!Number.isSafeInteger(value) ||
|
|
126
|
+
value < 0 ||
|
|
127
|
+
value > MANIFEST_LIMITS[key]
|
|
128
|
+
)
|
|
129
|
+
throw new TypeError("Invalid manifest observation limit");
|
|
130
|
+
const result = structuredClone(report);
|
|
131
|
+
result.cues = result.cues.filter((item) => item.module !== "manifest");
|
|
132
|
+
result.annotations = result.annotations.filter(
|
|
133
|
+
(item) => item.module !== "manifest",
|
|
134
|
+
);
|
|
135
|
+
result.inputs.limits = { ...result.inputs.limits, manifest: limits };
|
|
136
|
+
const reasons = new Set();
|
|
137
|
+
const counters = {
|
|
138
|
+
fieldNodes: 0,
|
|
139
|
+
valueBytes: 0,
|
|
140
|
+
fieldComparisons: 0,
|
|
141
|
+
cues: 0,
|
|
142
|
+
declarations: 0,
|
|
143
|
+
pathComparisons: 0,
|
|
144
|
+
annotations: 0,
|
|
145
|
+
};
|
|
146
|
+
const proofs = {
|
|
147
|
+
artifact: wholeEvidence(result, "artifact", artifact),
|
|
148
|
+
source: wholeEvidence(result, "source", source),
|
|
149
|
+
};
|
|
150
|
+
const usable = (side) =>
|
|
151
|
+
result.manifests[side]?.status === "usable" &&
|
|
152
|
+
(side === "registry"
|
|
153
|
+
? /^[a-f0-9]{64}$/.test(result.inputs.registryDocument?.sha256 || "")
|
|
154
|
+
: Boolean(proofs[side]));
|
|
155
|
+
const sourceUsable =
|
|
156
|
+
usable("source") &&
|
|
157
|
+
usable("artifact") &&
|
|
158
|
+
["selected", "resolved"].includes(result.source.status) &&
|
|
159
|
+
result.source.complete === true &&
|
|
160
|
+
result.coverage.sourceComplete === true &&
|
|
161
|
+
!["unresolved", "conflicting"].includes(result.source.method);
|
|
162
|
+
for (const side of ["artifact", "source"])
|
|
163
|
+
if (result.manifests[side]?.status === "usable" && !proofs[side])
|
|
164
|
+
reasons.add(`${side}-manifest-evidence-unavailable`);
|
|
165
|
+
const cache = new Map();
|
|
166
|
+
function consume(bytes) {
|
|
167
|
+
if (bytes > limits.totalValueBytes - counters.valueBytes)
|
|
168
|
+
throw new Unassessed("total-value-cap");
|
|
169
|
+
counters.valueBytes += bytes;
|
|
170
|
+
}
|
|
171
|
+
function snapshot(value, depth = 0) {
|
|
172
|
+
if (counters.fieldNodes >= limits.fieldNodes)
|
|
173
|
+
throw new Unassessed("field-cap");
|
|
174
|
+
counters.fieldNodes++;
|
|
175
|
+
if (depth > limits.depth) throw new Unassessed("depth-cap");
|
|
176
|
+
const type =
|
|
177
|
+
value === null ? "null" : Array.isArray(value) ? "array" : typeof value;
|
|
178
|
+
const node = {
|
|
179
|
+
known: true,
|
|
180
|
+
present: true,
|
|
181
|
+
type,
|
|
182
|
+
value,
|
|
183
|
+
children: new Map(),
|
|
184
|
+
keys: [],
|
|
185
|
+
};
|
|
186
|
+
if (type !== "object" && type !== "array") {
|
|
187
|
+
if (type === "string" && Buffer.byteLength(value) > limits.valueBytes)
|
|
188
|
+
throw new Unassessed("field-value-cap");
|
|
189
|
+
node.signature = JSON.stringify(value);
|
|
190
|
+
consume(Buffer.byteLength(node.signature));
|
|
191
|
+
} else {
|
|
192
|
+
for (const key in value) {
|
|
193
|
+
if (!Object.hasOwn(value, key)) continue;
|
|
194
|
+
if (node.keys.length >= limits.fieldNodes - counters.fieldNodes)
|
|
195
|
+
throw new Unassessed("field-cap");
|
|
196
|
+
node.keys.push(key);
|
|
197
|
+
consume(Buffer.byteLength(key));
|
|
198
|
+
}
|
|
199
|
+
for (const key of node.keys)
|
|
200
|
+
node.children.set(key, snapshot(value[key], depth + 1));
|
|
201
|
+
node.signature =
|
|
202
|
+
type === "array"
|
|
203
|
+
? "[" +
|
|
204
|
+
node.keys.map((key) => node.children.get(key).signature).join(",") +
|
|
205
|
+
"]"
|
|
206
|
+
: "{" +
|
|
207
|
+
node.keys
|
|
208
|
+
.map(
|
|
209
|
+
(key) =>
|
|
210
|
+
JSON.stringify(key) + ":" + node.children.get(key).signature,
|
|
211
|
+
)
|
|
212
|
+
.join(",") +
|
|
213
|
+
"}";
|
|
214
|
+
}
|
|
215
|
+
if (Buffer.byteLength(node.signature) > limits.valueBytes)
|
|
216
|
+
throw new Unassessed("field-value-cap");
|
|
217
|
+
return node;
|
|
218
|
+
}
|
|
219
|
+
function field(side, parts) {
|
|
220
|
+
const key = side + ":" + pointer(parts);
|
|
221
|
+
if (cache.has(key)) return cache.get(key);
|
|
222
|
+
let node = { known: false, present: false };
|
|
223
|
+
if (usable(side)) {
|
|
224
|
+
let value = result.manifests[side].value,
|
|
225
|
+
present = true;
|
|
226
|
+
for (const part of parts) {
|
|
227
|
+
if (
|
|
228
|
+
value === null ||
|
|
229
|
+
typeof value !== "object" ||
|
|
230
|
+
!Object.hasOwn(value, part)
|
|
231
|
+
) {
|
|
232
|
+
present = false;
|
|
233
|
+
break;
|
|
234
|
+
}
|
|
235
|
+
value = value[part];
|
|
236
|
+
}
|
|
237
|
+
try {
|
|
238
|
+
node = present ? snapshot(value) : absent();
|
|
239
|
+
} catch (error) {
|
|
240
|
+
if (!(error instanceof Unassessed)) throw error;
|
|
241
|
+
reasons.add(error.message);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
cache.set(key, node);
|
|
245
|
+
return node;
|
|
246
|
+
}
|
|
247
|
+
function binding(side, parts, node) {
|
|
248
|
+
const resultBinding = {
|
|
249
|
+
side,
|
|
250
|
+
pointer: pointer(parts),
|
|
251
|
+
present: node.present,
|
|
252
|
+
};
|
|
253
|
+
if (node.present) {
|
|
254
|
+
resultBinding.valueType = node.type;
|
|
255
|
+
if (node.type === "object" || node.type === "array") {
|
|
256
|
+
resultBinding.valueSha256 = hash(node.signature);
|
|
257
|
+
resultBinding.keyOrder = node.keys;
|
|
258
|
+
} else resultBinding.value = node.value;
|
|
259
|
+
}
|
|
260
|
+
if (side === "registry")
|
|
261
|
+
resultBinding.documentSha256 = result.inputs.registryDocument.sha256;
|
|
262
|
+
else {
|
|
263
|
+
const proof = proofs[side];
|
|
264
|
+
resultBinding.evidenceIds = [proof.id];
|
|
265
|
+
resultBinding.evidenceScope = "whole-manifest";
|
|
266
|
+
resultBinding.excerptRange = proof.excerptRange || null;
|
|
267
|
+
resultBinding.excerptOmitted =
|
|
268
|
+
!proof.excerptRange || proof.excerptRange[1] < proof.size;
|
|
269
|
+
}
|
|
270
|
+
return resultBinding;
|
|
271
|
+
}
|
|
272
|
+
function emit(ruleId, priority, parts, bindings, extra = {}) {
|
|
273
|
+
if (counters.cues >= limits.cues) {
|
|
274
|
+
reasons.add("cue-cap");
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
counters.cues++;
|
|
278
|
+
const summaries = {
|
|
279
|
+
"manifest-runtime-dependency-added":
|
|
280
|
+
"Dependency declaration added relative to the complete selected source manifest.",
|
|
281
|
+
"manifest-entrypoint-source-absent":
|
|
282
|
+
"Declared entrypoint differs from the complete selected source manifest; target content is unverified.",
|
|
283
|
+
"manifest-lifecycle-source-absent":
|
|
284
|
+
"Nonempty install hook differs from the complete selected source manifest; target code was not executed.",
|
|
285
|
+
"manifest-registry-runtime-divergence":
|
|
286
|
+
"Registry and artifact runtime fields differ; npm normalization is unassessed.",
|
|
287
|
+
"manifest-registry-source-observation":
|
|
288
|
+
"Registry and source declared fields differ; normalization is unassessed.",
|
|
289
|
+
"manifest-artifact-runtime-observation":
|
|
290
|
+
"Artifact declaration observed; source comparison is unavailable.",
|
|
291
|
+
"manifest-runtime-field-difference":
|
|
292
|
+
"Declared fields differ; this observation does not establish runtime behavior.",
|
|
293
|
+
};
|
|
294
|
+
result.cues.push({
|
|
295
|
+
module: "manifest",
|
|
296
|
+
ruleId,
|
|
297
|
+
priority,
|
|
298
|
+
summary: summaries[ruleId],
|
|
299
|
+
field: pointer(parts),
|
|
300
|
+
bindings,
|
|
301
|
+
evidenceIds: [
|
|
302
|
+
...new Set(bindings.flatMap((item) => item.evidenceIds || [])),
|
|
303
|
+
].sort(),
|
|
304
|
+
...extra,
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
function diff(left, right, parts, visit) {
|
|
308
|
+
if (counters.fieldComparisons >= limits.fieldComparisons) {
|
|
309
|
+
reasons.add("field-comparison-cap");
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
counters.fieldComparisons++;
|
|
313
|
+
if (!left.known || !right.known || left.signature === right.signature)
|
|
314
|
+
return;
|
|
315
|
+
const lContainer = left.type === "object" || left.type === "array",
|
|
316
|
+
rContainer = right.type === "object" || right.type === "array";
|
|
317
|
+
if (
|
|
318
|
+
(lContainer && rContainer && left.type === right.type) ||
|
|
319
|
+
(lContainer && !right.present) ||
|
|
320
|
+
(rContainer && !left.present)
|
|
321
|
+
) {
|
|
322
|
+
const keys = [...new Set([...left.keys, ...right.keys])].sort();
|
|
323
|
+
if (keys.length === 0) {
|
|
324
|
+
visit(left, right, parts, "value");
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
if (
|
|
328
|
+
left.present &&
|
|
329
|
+
right.present &&
|
|
330
|
+
left.keys.length === right.keys.length &&
|
|
331
|
+
left.keys.every((key) => right.children.has(key)) &&
|
|
332
|
+
left.keys.join("\0") !== right.keys.join("\0")
|
|
333
|
+
)
|
|
334
|
+
visit(left, right, parts, "member-order");
|
|
335
|
+
for (const key of keys)
|
|
336
|
+
diff(
|
|
337
|
+
left.children.get(key) || absent(),
|
|
338
|
+
right.children.get(key) || absent(),
|
|
339
|
+
[...parts, key],
|
|
340
|
+
visit,
|
|
341
|
+
);
|
|
342
|
+
} else visit(left, right, parts, "value");
|
|
343
|
+
}
|
|
344
|
+
const pairs = {
|
|
345
|
+
artifactSource: sourceUsable ? "available" : "unavailable",
|
|
346
|
+
registryArtifact:
|
|
347
|
+
usable("registry") && usable("artifact") ? "available" : "unavailable",
|
|
348
|
+
registrySource:
|
|
349
|
+
usable("registry") && usable("source") ? "available" : "unavailable",
|
|
350
|
+
};
|
|
351
|
+
if (!sourceUsable) reasons.add("source-comparison-unavailable");
|
|
352
|
+
for (const parts of fields) {
|
|
353
|
+
const a = field("artifact", parts),
|
|
354
|
+
s = field("source", parts),
|
|
355
|
+
r = field("registry", parts);
|
|
356
|
+
if (sourceUsable) {
|
|
357
|
+
diff(a, s, parts, (left, right, path, change) => {
|
|
358
|
+
const binds = [
|
|
359
|
+
binding("artifact", path, left),
|
|
360
|
+
binding("source", path, right),
|
|
361
|
+
];
|
|
362
|
+
const root = path[0];
|
|
363
|
+
if (
|
|
364
|
+
dependencyFields.has(root) &&
|
|
365
|
+
path.length > 1 &&
|
|
366
|
+
left.present &&
|
|
367
|
+
!right.present &&
|
|
368
|
+
left.type === "string" &&
|
|
369
|
+
left.value.length > 0
|
|
370
|
+
)
|
|
371
|
+
emit("manifest-runtime-dependency-added", "MEDIUM", path, binds);
|
|
372
|
+
else if (
|
|
373
|
+
root === "scripts" &&
|
|
374
|
+
left.present &&
|
|
375
|
+
left.type === "string" &&
|
|
376
|
+
left.value.length > 0
|
|
377
|
+
)
|
|
378
|
+
emit("manifest-lifecycle-source-absent", "HIGH", path, binds);
|
|
379
|
+
else if (
|
|
380
|
+
entrypoints.has(root) &&
|
|
381
|
+
left.present &&
|
|
382
|
+
((left.type === "string" && left.value.length > 0) ||
|
|
383
|
+
(change === "member-order" && root === "exports") ||
|
|
384
|
+
(left.type === "object" && right.present))
|
|
385
|
+
)
|
|
386
|
+
emit("manifest-entrypoint-source-absent", "HIGH", path, binds, {
|
|
387
|
+
change,
|
|
388
|
+
});
|
|
389
|
+
else
|
|
390
|
+
emit("manifest-runtime-field-difference", "LOW", path, binds, {
|
|
391
|
+
change,
|
|
392
|
+
});
|
|
393
|
+
});
|
|
394
|
+
} else if (a.known && a.present && usable("artifact"))
|
|
395
|
+
emit(
|
|
396
|
+
"manifest-artifact-runtime-observation",
|
|
397
|
+
"LOW",
|
|
398
|
+
parts,
|
|
399
|
+
[binding("artifact", parts, a)],
|
|
400
|
+
{ sourceComparison: "unavailable" },
|
|
401
|
+
);
|
|
402
|
+
if (pairs.registryArtifact === "available")
|
|
403
|
+
diff(r, a, parts, (left, right, path, change) =>
|
|
404
|
+
emit(
|
|
405
|
+
"manifest-registry-runtime-divergence",
|
|
406
|
+
"MEDIUM",
|
|
407
|
+
path,
|
|
408
|
+
[binding("registry", path, left), binding("artifact", path, right)],
|
|
409
|
+
{ normalization: "unassessed", change },
|
|
410
|
+
),
|
|
411
|
+
);
|
|
412
|
+
if (pairs.registrySource === "available")
|
|
413
|
+
diff(r, s, parts, (left, right, path, change) =>
|
|
414
|
+
emit(
|
|
415
|
+
"manifest-registry-source-observation",
|
|
416
|
+
"LOW",
|
|
417
|
+
path,
|
|
418
|
+
[binding("registry", path, left), binding("source", path, right)],
|
|
419
|
+
{ normalization: "unassessed", change },
|
|
420
|
+
),
|
|
421
|
+
);
|
|
422
|
+
}
|
|
423
|
+
const declarations = [];
|
|
424
|
+
function literal(value) {
|
|
425
|
+
if (
|
|
426
|
+
typeof value !== "string" ||
|
|
427
|
+
!value ||
|
|
428
|
+
/[\0\\*?\[\]{}!]/.test(value) ||
|
|
429
|
+
value.startsWith("/") ||
|
|
430
|
+
/^[a-z]:/i.test(value)
|
|
431
|
+
)
|
|
432
|
+
return null;
|
|
433
|
+
const normalized = (
|
|
434
|
+
value.startsWith("./") ? value.slice(2) : value
|
|
435
|
+
).replace(/\/$/, "");
|
|
436
|
+
return normalized &&
|
|
437
|
+
normalized
|
|
438
|
+
.split("/")
|
|
439
|
+
.every((part) => part && part !== "." && part !== "..")
|
|
440
|
+
? normalized
|
|
441
|
+
: null;
|
|
442
|
+
}
|
|
443
|
+
function declaration(node, parts, prefix) {
|
|
444
|
+
if (!node.known || !node.present) return;
|
|
445
|
+
if (node.type !== "string" || !literal(node.value)) {
|
|
446
|
+
reasons.add("unsupported-declaration");
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
if (counters.declarations >= limits.declarations)
|
|
450
|
+
throw new Unassessed("declaration-cap");
|
|
451
|
+
counters.declarations++;
|
|
452
|
+
declarations.push({
|
|
453
|
+
path: literal(node.value),
|
|
454
|
+
prefix,
|
|
455
|
+
field: pointer(parts),
|
|
456
|
+
binding: binding("source", parts, node),
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
function leaves(node, parts, mode) {
|
|
460
|
+
if (!node.known || !node.present) return;
|
|
461
|
+
if (parts.some((part) => /[\*?\[\]{}!]/.test(part))) {
|
|
462
|
+
reasons.add("unsupported-declaration");
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
if (node.type === "string") {
|
|
466
|
+
declaration(node, parts, false);
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
if (node.type === "null") return;
|
|
470
|
+
if (node.type !== "object" && node.type !== "array") {
|
|
471
|
+
reasons.add("unsupported-declaration");
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
for (const key of node.keys) {
|
|
475
|
+
const child = node.children.get(key);
|
|
476
|
+
if (mode === "bin" && child.type !== "string") {
|
|
477
|
+
reasons.add("unsupported-declaration");
|
|
478
|
+
continue;
|
|
479
|
+
}
|
|
480
|
+
leaves(child, [...parts, key], mode);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
if (
|
|
484
|
+
usable("source") &&
|
|
485
|
+
["selected", "resolved"].includes(result.source.status)
|
|
486
|
+
) {
|
|
487
|
+
try {
|
|
488
|
+
const fileList = field("source", ["files"]);
|
|
489
|
+
if (fileList.known && fileList.present) {
|
|
490
|
+
if (fileList.type === "array")
|
|
491
|
+
for (const key of fileList.keys)
|
|
492
|
+
declaration(fileList.children.get(key), ["files", key], true);
|
|
493
|
+
else reasons.add("unsupported-declaration");
|
|
494
|
+
}
|
|
495
|
+
for (const name of ["main", "module"])
|
|
496
|
+
declaration(field("source", [name]), [name], false);
|
|
497
|
+
leaves(field("source", ["bin"]), ["bin"], "bin");
|
|
498
|
+
leaves(field("source", ["exports"]), ["exports"], "exports");
|
|
499
|
+
for (const declared of declarations)
|
|
500
|
+
for (const file of result.files) {
|
|
501
|
+
if (counters.pathComparisons >= limits.pathComparisons)
|
|
502
|
+
throw new Unassessed("path-comparison-cap");
|
|
503
|
+
counters.pathComparisons++;
|
|
504
|
+
if (
|
|
505
|
+
file.path !== declared.path &&
|
|
506
|
+
!(declared.prefix && file.path?.startsWith(declared.path + "/"))
|
|
507
|
+
)
|
|
508
|
+
continue;
|
|
509
|
+
if (counters.annotations >= limits.annotations)
|
|
510
|
+
throw new Unassessed("annotation-cap");
|
|
511
|
+
counters.annotations++;
|
|
512
|
+
result.annotations.push({
|
|
513
|
+
module: "manifest",
|
|
514
|
+
type: "declared-output-location",
|
|
515
|
+
label: "declared-output-location; content unverified",
|
|
516
|
+
path: file.path,
|
|
517
|
+
field: declared.field,
|
|
518
|
+
bindings: [declared.binding],
|
|
519
|
+
evidenceIds: declared.binding.evidenceIds,
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
} catch (error) {
|
|
523
|
+
if (!(error instanceof Unassessed)) throw error;
|
|
524
|
+
reasons.add(error.message);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
result.annotations.push({
|
|
528
|
+
module: "manifest",
|
|
529
|
+
type: "manifest-scope",
|
|
530
|
+
sourceDeclarationStatus:
|
|
531
|
+
usable("source") &&
|
|
532
|
+
["selected", "resolved"].includes(result.source.status)
|
|
533
|
+
? "available"
|
|
534
|
+
: "unassessed",
|
|
535
|
+
sourceDeclarationReason:
|
|
536
|
+
typeof result.manifests.source.reason === "string"
|
|
537
|
+
? result.manifests.source.reason.slice(0, 128)
|
|
538
|
+
: result.manifests.source.status,
|
|
539
|
+
status: reasons.size ? "partial" : "complete",
|
|
540
|
+
pairs,
|
|
541
|
+
reasons: [...reasons].sort(),
|
|
542
|
+
unassessed: [...reasons].sort(),
|
|
543
|
+
counts: counters,
|
|
544
|
+
limitation:
|
|
545
|
+
"Declarations describe observed fields only; no normalization, glob expansion, publish-set prediction, build reproduction or content equivalence is established.",
|
|
546
|
+
});
|
|
547
|
+
return validateReport(result);
|
|
548
|
+
}
|