@xagents_in/unmask 0.1.0 → 0.1.2

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.
@@ -0,0 +1,638 @@
1
+ import path from "node:path";
2
+ import { validateReport } from "./report.mjs";
3
+
4
+ export const PACKAGE_DETAILS_LIMITS = Object.freeze({
5
+ maxItems: 20000,
6
+ maxValueBytes: 32768,
7
+ maxSectionBytes: 262144,
8
+ maxTotalBytes: 2097152,
9
+ });
10
+ const order = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
11
+ const object = (value) =>
12
+ value !== null && typeof value === "object" && !Array.isArray(value);
13
+ const pointer = (value) =>
14
+ String(value).replace(/~/g, "~0").replace(/\//g, "~1");
15
+ const sorted = (value) => Object.keys(value).sort(order);
16
+ const type = (value) =>
17
+ value === null ? "null" : Array.isArray(value) ? "array" : typeof value;
18
+
19
+ export function buildPackageDetails(report, options = {}) {
20
+ validateReport(report);
21
+ const limits = { ...PACKAGE_DETAILS_LIMITS, ...options };
22
+ if (
23
+ Object.entries(limits).some(
24
+ ([key, value]) =>
25
+ !Object.hasOwn(PACKAGE_DETAILS_LIMITS, key) ||
26
+ !Number.isSafeInteger(value) ||
27
+ value < 1 ||
28
+ value > PACKAGE_DETAILS_LIMITS[key],
29
+ ) ||
30
+ limits.maxTotalBytes < 65536
31
+ )
32
+ throw new TypeError("Invalid package details limits");
33
+ let remaining = limits.maxTotalBytes - 32768;
34
+ function take(values) {
35
+ const result = {
36
+ items: [],
37
+ total: values.length,
38
+ returned: 0,
39
+ omitted: 0,
40
+ omissionReasons: {},
41
+ };
42
+ let bytes = 2;
43
+ for (const value of values) {
44
+ const json = JSON.stringify(value);
45
+ const size = Buffer.byteLength(json) + 1;
46
+ const reason =
47
+ size > limits.maxValueBytes
48
+ ? "value-byte-cap"
49
+ : result.returned >= limits.maxItems
50
+ ? "item-cap"
51
+ : bytes + size > limits.maxSectionBytes
52
+ ? "section-byte-cap"
53
+ : size > remaining
54
+ ? "response-byte-cap"
55
+ : null;
56
+ if (reason) {
57
+ result.omitted++;
58
+ result.omissionReasons[reason] =
59
+ (result.omissionReasons[reason] ?? 0) + 1;
60
+ } else {
61
+ result.items.push(JSON.parse(json));
62
+ result.returned++;
63
+ bytes += size;
64
+ remaining -= size;
65
+ }
66
+ }
67
+ return result;
68
+ }
69
+ const manifest = report.manifests.artifact;
70
+ const declared = manifest.status === "usable" ? manifest.value : null;
71
+ const allManifestEvidenceIds = declared
72
+ ? [...new Set(manifest.evidenceIds)].sort(order)
73
+ : [];
74
+ const evidenceIds = allManifestEvidenceIds.slice(0, 16);
75
+ const evidenceIdsOmitted = allManifestEvidenceIds.length - evidenceIds.length;
76
+ function field(name) {
77
+ const present = declared !== null && Object.hasOwn(declared, name);
78
+ return {
79
+ field: name,
80
+ pointer: `/${pointer(name)}`,
81
+ status:
82
+ declared === null
83
+ ? manifest.status
84
+ : present
85
+ ? "declared"
86
+ : "not-declared-in-manifest",
87
+ ...(present
88
+ ? { value: declared[name], valueType: type(declared[name]) }
89
+ : {}),
90
+ evidenceIds,
91
+ };
92
+ }
93
+ function entries(name, mode = "map") {
94
+ const entry = field(name);
95
+ const value = entry.value;
96
+ let status = entry.status;
97
+ let values = [];
98
+ let declaredCount = null;
99
+ if (entry.status === "declared") {
100
+ if (mode === "bundled" && typeof value === "boolean") {
101
+ status = value
102
+ ? "declared-all-runtime-dependencies"
103
+ : "declared-disabled";
104
+ } else if (mode === "bundled" && Array.isArray(value)) {
105
+ values = value.map((item, index) => ({
106
+ name: item,
107
+ index,
108
+ status: typeof item === "string" ? "declared" : "invalid-type",
109
+ pointer: `/${name}/${index}`,
110
+ evidenceIds,
111
+ }));
112
+ values.sort(
113
+ (a, b) =>
114
+ order(JSON.stringify(a.name), JSON.stringify(b.name)) ||
115
+ a.index - b.index,
116
+ );
117
+ declaredCount = value.length;
118
+ } else if (mode === "map" && object(value)) {
119
+ values = sorted(value).map((key) => ({
120
+ name: key,
121
+ value: value[key],
122
+ status: typeof value[key] === "string" ? "declared" : "invalid-type",
123
+ pointer: `/${name}/${pointer(key)}`,
124
+ ...(name === "scripts"
125
+ ? {
126
+ installHook: ["preinstall", "install", "postinstall"].includes(
127
+ key,
128
+ ),
129
+ }
130
+ : {}),
131
+ evidenceIds,
132
+ }));
133
+ declaredCount = values.length;
134
+ } else status = "invalid-type";
135
+ } else if (entry.status === "not-declared-in-manifest") declaredCount = 0;
136
+ return {
137
+ field: name,
138
+ status,
139
+ declaredCount,
140
+ validEntryCount:
141
+ declaredCount === null
142
+ ? null
143
+ : values.filter((item) => item.status === "declared").length,
144
+ entries: take(values),
145
+ evidenceIds,
146
+ evidenceIdsOmitted,
147
+ };
148
+ }
149
+ const fileOrder = (a, b) =>
150
+ order(a.path ?? "", b.path ?? "") || order(a.originalPath, b.originalPath);
151
+ const files = [...report.files].sort(fileOrder);
152
+ const extensions = new Map();
153
+ for (const file of files) {
154
+ const extension =
155
+ path.posix.extname(file.path ?? file.originalPath).toLowerCase() ||
156
+ "(none)";
157
+ const group = extensions.get(extension) ?? {
158
+ extension,
159
+ files: 0,
160
+ bytes: 0,
161
+ };
162
+ group.files++;
163
+ group.bytes += file.size;
164
+ extensions.set(extension, group);
165
+ }
166
+ const observations = [
167
+ ...report.cues.map((cue, index) => ({ ...cue, cueRef: `/cues/${index}` })),
168
+ ...report.annotations.map((cue, index) => ({
169
+ ...cue,
170
+ cueRef: `/annotations/${index}`,
171
+ })),
172
+ ].filter(
173
+ (cue) => typeof cue.ruleId === "string" && cue.ruleId.startsWith("syntax-"),
174
+ );
175
+ observations.sort(
176
+ (a, b) => order(a.ruleId, b.ruleId) || order(a.cueRef, b.cueRef),
177
+ );
178
+ const capabilityGroups = new Map();
179
+ for (const cue of observations) {
180
+ const group = capabilityGroups.get(cue.ruleId) ?? {
181
+ ruleId: cue.ruleId,
182
+ observations: 0,
183
+ };
184
+ group.observations++;
185
+ capabilityGroups.set(cue.ruleId, group);
186
+ }
187
+ const byPath = new Map(
188
+ files.filter((file) => file.path !== null).map((file) => [file.path, file]),
189
+ );
190
+ const topDirectories = new Map();
191
+ for (const file of files) {
192
+ const name = (file.path ?? file.originalPath).includes("/")
193
+ ? (file.path ?? file.originalPath).split("/")[0]
194
+ : "(root)";
195
+ const item = topDirectories.get(name) ?? {
196
+ directory: name,
197
+ files: 0,
198
+ bytes: 0,
199
+ };
200
+ item.files++;
201
+ item.bytes += file.size;
202
+ topDirectories.set(name, item);
203
+ }
204
+ const evidenceById = new Map(report.evidence.map((item) => [item.id, item]));
205
+ const cueRefsByPath = new Map();
206
+ for (const cue of observations)
207
+ for (const id of cue.evidenceIds ?? []) {
208
+ const evidence = evidenceById.get(id);
209
+ if (evidence?.side !== "artifact") continue;
210
+ const refs = cueRefsByPath.get(evidence.path) ?? new Set();
211
+ refs.add(cue.cueRef);
212
+ cueRefsByPath.set(evidence.path, refs);
213
+ }
214
+ const skippedByPath = new Map();
215
+ for (const annotation of report.annotations)
216
+ if (
217
+ annotation.type === "syntax-not-inspected" &&
218
+ annotation.side === "artifact"
219
+ ) {
220
+ const reasons = skippedByPath.get(annotation.path) ?? new Set();
221
+ reasons.add(annotation.reason);
222
+ skippedByPath.set(annotation.path, reasons);
223
+ }
224
+ const targets = [];
225
+ let targetNodes = 0;
226
+ let targetTraversalOmitted = false;
227
+ function visitTarget(value, parts) {
228
+ if (++targetNodes > 4096) {
229
+ targetTraversalOmitted = true;
230
+ return;
231
+ }
232
+ if (typeof value === "string") {
233
+ const candidate = value.startsWith("./") ? value.slice(2) : value;
234
+ const safe =
235
+ candidate.length > 0 &&
236
+ !/[\\\0*?\[\]{}:]/u.test(candidate) &&
237
+ !candidate.startsWith("/") &&
238
+ candidate
239
+ .split("/")
240
+ .every((part) => part && part !== "." && part !== "..");
241
+ const file = safe ? byPath.get(candidate) : undefined;
242
+ const cueRefs = file
243
+ ? [...(cueRefsByPath.get(file.path) ?? [])].sort(order)
244
+ : [];
245
+ const skipped = file
246
+ ? [...(skippedByPath.get(file.path) ?? [])].sort(order)
247
+ : [];
248
+ targets.push({
249
+ field: parts[0],
250
+ pointer: "/" + parts.map(pointer).join("/"),
251
+ target: value,
252
+ matchStatus: !safe
253
+ ? "not-assessed-unsafe-or-pattern-target"
254
+ : file
255
+ ? "exact-file-match"
256
+ : "no-exact-match-in-accepted-inventory",
257
+ ...(file
258
+ ? {
259
+ path: file.path,
260
+ state: file.state,
261
+ bytes: file.size,
262
+ evidenceIds: file.evidenceIds,
263
+ syntaxStatus: cueRefs.length
264
+ ? "observed-syntax-cues"
265
+ : skipped.length
266
+ ? "recorded-not-inspected"
267
+ : "not-recorded-per-file",
268
+ syntaxCueRefs: cueRefs,
269
+ syntaxSkippedReasons: skipped,
270
+ }
271
+ : { evidenceIds: [] }),
272
+ });
273
+ } else if (object(value) || Array.isArray(value)) {
274
+ for (const key of sorted(value)) {
275
+ if (targetNodes >= 4096) {
276
+ targetTraversalOmitted = true;
277
+ break;
278
+ }
279
+ visitTarget(value[key], [...parts, key]);
280
+ }
281
+ }
282
+ }
283
+ if (declared)
284
+ for (const name of [
285
+ "main",
286
+ "module",
287
+ "exports",
288
+ "bin",
289
+ "types",
290
+ "typings",
291
+ "browser",
292
+ ])
293
+ if (Object.hasOwn(declared, name)) visitTarget(declared[name], [name]);
294
+ function dependencyDelta(side) {
295
+ const other = report.manifests[side];
296
+ const proven =
297
+ declared &&
298
+ other.status === "usable" &&
299
+ (side === "source"
300
+ ? report.source.complete &&
301
+ report.coverage.sourceComplete &&
302
+ ["selected", "resolved"].includes(report.source.status) &&
303
+ !["unresolved", "conflicting"].includes(report.source.method)
304
+ : typeof report.inputs.registryDocument?.sha256 === "string");
305
+ const differences = [];
306
+ const unassessedFields = [];
307
+ if (proven)
308
+ for (const field of [
309
+ "dependencies",
310
+ "optionalDependencies",
311
+ "peerDependencies",
312
+ "devDependencies",
313
+ ]) {
314
+ const left = declared[field];
315
+ const right = other.value[field];
316
+ if (
317
+ (left !== undefined && !object(left)) ||
318
+ (right !== undefined && !object(right))
319
+ ) {
320
+ unassessedFields.push(field);
321
+ continue;
322
+ }
323
+ for (const name of [
324
+ ...new Set([...Object.keys(left ?? {}), ...Object.keys(right ?? {})]),
325
+ ].sort(order)) {
326
+ const inArtifact = Object.hasOwn(left ?? {}, name);
327
+ const inOther = Object.hasOwn(right ?? {}, name);
328
+ const artifactValue = inArtifact ? left[name] : null;
329
+ const otherValue = inOther ? right[name] : null;
330
+ if (
331
+ (inArtifact && typeof artifactValue !== "string") ||
332
+ (inOther && typeof otherValue !== "string")
333
+ ) {
334
+ unassessedFields.push(field + "/" + pointer(name));
335
+ continue;
336
+ }
337
+ if (artifactValue !== otherValue)
338
+ differences.push({
339
+ field,
340
+ name,
341
+ change: !inOther
342
+ ? "artifact-declared-only"
343
+ : !inArtifact
344
+ ? side + "-declared-only"
345
+ : "declared-value-differs",
346
+ artifactValue,
347
+ otherValue,
348
+ pointer: "/" + field + "/" + pointer(name),
349
+ evidenceIds,
350
+ otherEvidenceIds: other.evidenceIds ?? [],
351
+ ...(side === "registry"
352
+ ? { documentSha256: report.inputs.registryDocument.sha256 }
353
+ : {}),
354
+ });
355
+ }
356
+ }
357
+ return {
358
+ side,
359
+ status: proven ? "declared-values-compared" : "not-assessed",
360
+ normalization: "not-assessed",
361
+ differences: take(differences),
362
+ unassessedFields: take(unassessedFields),
363
+ };
364
+ }
365
+ const result = {
366
+ detailsVersion: "1",
367
+ reportSchemaVersion: report.schemaVersion,
368
+ versions: take([
369
+ {
370
+ analyzer: report.analyzerVersion,
371
+ policy: report.policyVersion,
372
+ limits: report.limitsVersion,
373
+ },
374
+ ]),
375
+ scope: {
376
+ method: "bounded-static-projection-of-existing-report",
377
+ safetyVerdict: "not-provided",
378
+ allCodeRead: false,
379
+ vulnerabilityScan: "not-performed",
380
+ licenseScan: "not-performed",
381
+ cryptographicProvenance:
382
+ "not-assessed-by-this-projection; consult recorded provenance status",
383
+ additionalFetches: 0,
384
+ additionalModelCalls: 0,
385
+ targetExecution: "not-performed",
386
+ limitation:
387
+ "Accepted inventory and bounded observations only. Declaration omissions are not runtime absence. Partial coverage cannot establish package-wide absence; extension groups do not prove language or behavior. Raw schema-1 report retains the underlying accepted detail.",
388
+ },
389
+ identity: take([
390
+ {
391
+ requested: report.inputs.claim ?? null,
392
+ artifactDigests: {
393
+ sha256: report.inputs.artifact.sha256,
394
+ sha512: report.inputs.artifact.sha512,
395
+ },
396
+ },
397
+ ]),
398
+ declarations: {
399
+ side: "artifact",
400
+ evidenceIdsOmitted,
401
+ label: "publisher-declared; runtime behavior unverified",
402
+ status: manifest.status,
403
+ fields: take(
404
+ [
405
+ "name",
406
+ "version",
407
+ "description",
408
+ "license",
409
+ "licenses",
410
+ "engines",
411
+ "os",
412
+ "cpu",
413
+ "type",
414
+ "main",
415
+ "module",
416
+ "exports",
417
+ "bin",
418
+ "types",
419
+ "typings",
420
+ "homepage",
421
+ "keywords",
422
+ "browser",
423
+ ]
424
+ .map(field)
425
+ .sort((a, b) => order(a.field, b.field)),
426
+ ),
427
+ },
428
+ entrypoints: {
429
+ matching:
430
+ "Safe exact relative declaration target only; no Node conditional selection, wildcard expansion, extension inference or runtime resolution. No exact match is not a package-wide absence claim.",
431
+ traversalNodeLimit: 4096,
432
+ traversalOmitted: targetTraversalOmitted,
433
+ targets: take(targets),
434
+ },
435
+ dependencies: {
436
+ transitive: "not-assessed",
437
+ counting:
438
+ "Exact declared entries per field; overlapping categories are not deduplicated. Invalid and omitted entries remain in declaredCount; boolean bundled declarations have no resolved count.",
439
+ categories: [
440
+ "dependencies",
441
+ "optionalDependencies",
442
+ "peerDependencies",
443
+ "devDependencies",
444
+ "bundledDependencies",
445
+ "bundleDependencies",
446
+ ].map((name) =>
447
+ entries(name, name.includes("undle") ? "bundled" : "map"),
448
+ ),
449
+ peerMetadata: take([field("peerDependenciesMeta")]),
450
+ comparisons: [dependencyDelta("source"), dependencyDelta("registry")],
451
+ },
452
+ scripts: {
453
+ ...entries("scripts"),
454
+ execution: "not-performed",
455
+ bindingGyp: {
456
+ interpretation:
457
+ "Observed binding.gyp file and gypfile declaration only; npm implicit-install behavior is not executed or inferred beyond recorded policy cues.",
458
+ files: take(files.filter((file) => file.path === "binding.gyp")),
459
+ declaration: take([field("gypfile")]),
460
+ recordedCues: take(
461
+ report.policy.findings
462
+ .map((finding, index) => ({
463
+ ...finding,
464
+ findingRef: `/policy/findings/${index}`,
465
+ }))
466
+ .filter((finding) => finding.ruleId === "lifecycle-implicit-gyp"),
467
+ ),
468
+ },
469
+ },
470
+ inventory: {
471
+ scope: "accepted-artifact-regular-files",
472
+ complete: report.coverage.inventory.complete,
473
+ counts: {
474
+ files: report.coverage.inventory.files,
475
+ bytes: report.coverage.inventory.bytes,
476
+ lowerBound: report.coverage.inventory.lowerBound,
477
+ },
478
+ files: take(files),
479
+ typeGrouping:
480
+ "Lowercase final path extension; (none) includes extensionless and dotfiles. Classification is lexical only.",
481
+ typeGroups: take(
482
+ [...extensions.values()].sort((a, b) =>
483
+ order(a.extension, b.extension),
484
+ ),
485
+ ),
486
+ topDirectories: take(
487
+ [...topDirectories.values()].sort((a, b) =>
488
+ order(a.directory, b.directory),
489
+ ),
490
+ ),
491
+ largestFiles: take(
492
+ [...files]
493
+ .sort((a, b) => b.size - a.size || fileOrder(a, b))
494
+ .slice(0, 20),
495
+ ),
496
+ sourceOnlyPaths: take([...report.coverage.sourceOnly].sort(order)),
497
+ inertLinks: take(report.coverage.linkEntries),
498
+ linkCounts: {
499
+ artifact: report.coverage.links.artifact,
500
+ source: report.coverage.links.source,
501
+ },
502
+ nonregularCounts: {
503
+ artifact: report.coverage.nonregular.artifact,
504
+ source: report.coverage.nonregular.source,
505
+ },
506
+ },
507
+ capabilities: {
508
+ label: "observed syntax references; execution and bindings unproven",
509
+ referenceFormat:
510
+ "cueRef is a JSON pointer into the raw report; existing cue IDs, when present, are preserved. evidenceIds refer to raw-report evidence IDs.",
511
+ groups: take(
512
+ [...capabilityGroups.values()].sort((a, b) =>
513
+ order(a.ruleId, b.ruleId),
514
+ ),
515
+ ),
516
+ observations: take(
517
+ observations.map((cue) => ({
518
+ ...cue,
519
+ locations: (cue.evidenceIds ?? [])
520
+ .map((id) => evidenceById.get(id))
521
+ .filter(Boolean)
522
+ .map(({ id, side, path, range }) => ({
523
+ evidenceId: id,
524
+ side,
525
+ path,
526
+ range,
527
+ })),
528
+ })),
529
+ ),
530
+ },
531
+ provenance: {
532
+ label:
533
+ "report-recorded facts; publisher claims retain their recorded verification status",
534
+ facts: take([
535
+ { field: "source", value: report.source },
536
+ ...["artifact", "sourceArchive", "registryDocument"]
537
+ .filter((name) => report.inputs[name] !== undefined)
538
+ .map((name) => ({ field: name, value: report.inputs[name] })),
539
+ ]),
540
+ },
541
+ ai: {
542
+ advisory: true,
543
+ selection: {
544
+ selectedGroups: Array.isArray(report.model.evidenceGroups)
545
+ ? report.model.evidenceGroups.length
546
+ : null,
547
+ omittedGroups: Array.isArray(report.model.omittedEvidenceGroups)
548
+ ? report.model.omittedEvidenceGroups.length
549
+ : null,
550
+ omittedEvidenceItems: Array.isArray(report.model.omittedEvidenceIds)
551
+ ? report.model.omittedEvidenceIds.length
552
+ : null,
553
+ interpretation:
554
+ "Prompt-selected groups as recorded, not full-file review or proof of model attention. Null means no selection count recorded; original modelReviewed counters are unchanged.",
555
+ },
556
+ validation:
557
+ "Upstream model-result validation is preserved; this projection performs no new inference or usefulness assessment.",
558
+ metadata: take([
559
+ Object.fromEntries(
560
+ Object.entries(report.model).filter(([key]) => key !== "hypotheses"),
561
+ ),
562
+ ]),
563
+ hypotheses: take(
564
+ report.model.status === "complete" &&
565
+ Array.isArray(report.model.hypotheses)
566
+ ? report.model.hypotheses
567
+ : [],
568
+ ),
569
+ },
570
+ coverage: {
571
+ counters: {
572
+ ...Object.fromEntries(
573
+ [
574
+ "inventory",
575
+ "compared",
576
+ "unresolved",
577
+ "syntaxInspected",
578
+ "modelReviewed",
579
+ ].map((key) => [
580
+ key,
581
+ {
582
+ files: report.coverage[key].files,
583
+ bytes: report.coverage[key].bytes,
584
+ ...(key === "inventory"
585
+ ? {
586
+ complete: report.coverage.inventory.complete,
587
+ lowerBound: report.coverage.inventory.lowerBound,
588
+ }
589
+ : {}),
590
+ },
591
+ ]),
592
+ ),
593
+ complete: report.coverage.complete,
594
+ sourceAvailable: report.coverage.sourceAvailable,
595
+ sourceComplete: report.coverage.sourceComplete,
596
+ states: Object.fromEntries(
597
+ ["byte-identical", "changed", "artifact-only", "not-compared"].map(
598
+ (key) => [
599
+ key,
600
+ {
601
+ files: report.coverage.states[key].files,
602
+ bytes: report.coverage.states[key].bytes,
603
+ },
604
+ ],
605
+ ),
606
+ ),
607
+ },
608
+ effectiveLimits: take([
609
+ {
610
+ limits: report.inputs.limits ?? null,
611
+ limitsDigest: report.inputs.limitsDigest ?? null,
612
+ },
613
+ ]),
614
+ observations: take(
615
+ report.annotations
616
+ .map((item, index) => ({
617
+ ...item,
618
+ annotationRef: `/annotations/${index}`,
619
+ }))
620
+ .filter((item) =>
621
+ ["syntax-scope", "syntax-not-inspected", "manifest-scope"].includes(
622
+ item.type,
623
+ ),
624
+ ),
625
+ ),
626
+ failures: take(report.failures),
627
+ policyFindings: take(report.policy.findings),
628
+ },
629
+ projection: {
630
+ limits,
631
+ sorting:
632
+ "UTF-16 code-unit ascending: declaration fields, dependency/script keys, file path then originalPath, extension and ruleId. Dependency categories use the documented fixed field order. Bundled lists sort by JSON name then original index. exports object member order is preserved. Other lists preserve report order.",
633
+ },
634
+ };
635
+ if (Buffer.byteLength(JSON.stringify(result)) > limits.maxTotalBytes)
636
+ throw new TypeError("Package details metadata exceeds response cap");
637
+ return result;
638
+ }