rea-agents 0.2.1 → 0.4.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.
Files changed (122) hide show
  1. package/README.md +124 -20
  2. package/bridge/hopper_bridge.py +141 -13
  3. package/dist/application/AnalysisProvider.js +12 -1
  4. package/dist/application/ArtifactExtraction.js +166 -0
  5. package/dist/application/ArtifactGraphConstruction.js +257 -0
  6. package/dist/application/ArtifactInventory.js +253 -0
  7. package/dist/application/BinarySession.js +164 -14
  8. package/dist/application/CompositeProvider.js +73 -0
  9. package/dist/application/DirectAnalysis.js +27 -3
  10. package/dist/application/Doctor.js +35 -6
  11. package/dist/application/EnhancedTools.js +8 -7
  12. package/dist/application/EvidenceBundleCommands.js +29 -0
  13. package/dist/application/EvidenceBundleFiles.js +127 -0
  14. package/dist/application/EvidenceLedger.js +225 -18
  15. package/dist/application/FilesystemSnapshot.js +124 -0
  16. package/dist/application/LinuxHopper.js +186 -0
  17. package/dist/application/LoopbackReplay.js +195 -37
  18. package/dist/application/ProcessHarness.js +200 -191
  19. package/dist/application/ProcessNormalization.js +44 -0
  20. package/dist/application/ProcessOwnership.js +84 -0
  21. package/dist/application/ProcessSampling.js +284 -0
  22. package/dist/application/RealHopperAssertions.js +105 -0
  23. package/dist/application/ReferenceSourceImport.js +182 -0
  24. package/dist/application/ReferenceSourceImportEntries.js +122 -0
  25. package/dist/application/ReferenceSourceImportPolicy.js +73 -0
  26. package/dist/application/ReferenceSourceImportTypes.js +18 -0
  27. package/dist/application/ReferenceSourceVcsAdapter.js +34 -0
  28. package/dist/application/Setup.js +192 -41
  29. package/dist/application/Uninstall.js +130 -0
  30. package/dist/application/runtime.js +8 -1
  31. package/dist/artifacts/ArtifactPaths.js +51 -0
  32. package/dist/artifacts/ArtifactProvider.js +130 -0
  33. package/dist/artifacts/ArtifactReader.js +9 -0
  34. package/dist/artifacts/AsarArtifactReader.js +62 -0
  35. package/dist/artifacts/DirectoryArtifactReader.js +107 -0
  36. package/dist/artifacts/MachOSliceArtifactReader.js +66 -0
  37. package/dist/artifacts/SafeOutputTree.js +199 -0
  38. package/dist/artifacts/StreamBytes.js +10 -0
  39. package/dist/artifacts/ZipArtifactReader.js +109 -0
  40. package/dist/cli.js +153 -6
  41. package/dist/config.js +35 -1
  42. package/dist/contracts/artifactComparisonExample.js +95 -0
  43. package/dist/contracts/artifactToolContracts.js +84 -0
  44. package/dist/contracts/enhancedInputs.js +4 -0
  45. package/dist/contracts/functionComparisonExample.js +57 -0
  46. package/dist/contracts/investigationExamples.js +118 -0
  47. package/dist/contracts/nativeToolContracts.js +52 -0
  48. package/dist/contracts/processCaptureExample.js +25 -0
  49. package/dist/contracts/toolContractExamples.js +69 -0
  50. package/dist/contracts/toolContracts.js +99 -36
  51. package/dist/contracts/toolOutputSchemas.js +154 -82
  52. package/dist/contracts/unknownContractExamples.js +33 -0
  53. package/dist/domain/artifactComparison.js +273 -0
  54. package/dist/domain/artifactGraph.js +194 -0
  55. package/dist/domain/artifactInventoryEvidence.js +150 -0
  56. package/dist/domain/binaryTarget.js +55 -0
  57. package/dist/domain/bundleComparison.js +266 -0
  58. package/dist/domain/callPath.js +346 -0
  59. package/dist/domain/changedBehavior.js +294 -0
  60. package/dist/domain/errors.js +192 -4
  61. package/dist/domain/evidence.js +41 -10
  62. package/dist/domain/evidenceBundle.js +187 -6
  63. package/dist/domain/functionComparison.js +201 -0
  64. package/dist/domain/functionComparisonNormalization.js +112 -0
  65. package/dist/domain/functionComparisonResults.js +54 -0
  66. package/dist/domain/functionComparisonSchemas.js +82 -0
  67. package/dist/domain/functionDossierEvidence.js +171 -0
  68. package/dist/domain/hopperValues.js +14 -7
  69. package/dist/domain/nativeInspection.js +142 -0
  70. package/dist/domain/processCapture.js +152 -54
  71. package/dist/domain/processComparison.js +106 -0
  72. package/dist/domain/reconstructionUnknowns.js +90 -0
  73. package/dist/domain/reconstructionVerification.js +285 -0
  74. package/dist/domain/reconstructionVerificationSchemas.js +126 -0
  75. package/dist/domain/referenceSourceClassification.js +496 -0
  76. package/dist/domain/referenceSourceGraph.js +376 -0
  77. package/dist/domain/referenceSourceImportParsing.js +235 -0
  78. package/dist/domain/residualUnknown.js +239 -0
  79. package/dist/domain/staticRuntimeCorrelation.js +375 -0
  80. package/dist/hopper/BridgeLauncher.js +40 -3
  81. package/dist/hopper/HopperClient.js +14 -5
  82. package/dist/hopper/HopperProvider.js +57 -22
  83. package/dist/identity.js +1 -0
  84. package/dist/main.js +5 -1
  85. package/dist/native/CommandRunner.js +156 -0
  86. package/dist/native/NativeMacOSProvider.js +306 -0
  87. package/dist/native/NativeMachoInspection.js +135 -0
  88. package/dist/native/parsers/codesign.js +55 -0
  89. package/dist/native/parsers/demangle.js +26 -0
  90. package/dist/native/parsers/dyldInfo.js +25 -0
  91. package/dist/native/parsers/lipo.js +67 -0
  92. package/dist/native/parsers/otool.js +193 -0
  93. package/dist/native/parsers/plist.js +23 -0
  94. package/dist/reference/ReferenceSourceReader.js +73 -0
  95. package/dist/reference/ReferenceSourceReaderEntries.js +206 -0
  96. package/dist/reference/ReferenceSourceReaderErrors.js +19 -0
  97. package/dist/reference/ReferenceSourceReaderFile.js +119 -0
  98. package/dist/reference/ReferenceSourceReaderPaths.js +23 -0
  99. package/dist/reference/ReferenceSourceReaderTypes.js +2 -0
  100. package/dist/reference/ReferenceSourceReaderValidate.js +71 -0
  101. package/dist/server/createServer.js +31 -5
  102. package/dist/server/recordDerivedEvidence.js +10 -0
  103. package/dist/server/registerArtifactComparisonTool.js +62 -0
  104. package/dist/server/registerArtifactTools.js +6 -0
  105. package/dist/server/registerBundleComparisonTool.js +47 -0
  106. package/dist/server/registerEnhancedTools.js +64 -12
  107. package/dist/server/registerEvidenceTools.js +36 -0
  108. package/dist/server/registerFunctionComparisonTool.js +68 -0
  109. package/dist/server/registerInvestigationTools.js +224 -0
  110. package/dist/server/registerNativeTools.js +6 -0
  111. package/dist/server/registerOfficialTools.js +47 -14
  112. package/dist/server/registerProcessComparisonTool.js +106 -0
  113. package/dist/server/registerSessionTools.js +179 -70
  114. package/dist/server/sessionEvidence.js +28 -0
  115. package/dist/server/sessionToolPolicies.js +64 -0
  116. package/dist/server/toolRegistrationOptions.js +7 -0
  117. package/dist/server/toolResult.js +8 -5
  118. package/install.sh +198 -0
  119. package/package.json +19 -2
  120. package/scripts/rea.mjs +5 -1
  121. package/skills/rea-analysis/SKILL.md +77 -2
  122. /package/dist/{application/HopperToolPort.js → domain/referenceSourcePolicy.js} +0 -0
@@ -0,0 +1,257 @@
1
+ import { createHash } from "node:crypto";
2
+ import { extname } from "node:path";
3
+ import canonicalize from "canonicalize";
4
+ export const createOccurrence = (entry, path, parent) => ({
5
+ occurrence_id: `occ_${digestCanonical({ path, kind: entry.kind })}`,
6
+ artifact_id: null,
7
+ parent_occurrence_id: parent,
8
+ logical_path: path,
9
+ entry_kind: entry.kind,
10
+ declared_size: entry.declaredSize,
11
+ compressed_size: entry.compressedSize,
12
+ executable: entry.executable,
13
+ encrypted: entry.encrypted,
14
+ hash_status: entry.encrypted ? "unavailable" : "not-hashed-limit",
15
+ source_location: entry.byteOffset === null || entry.declaredSize === null
16
+ ? null
17
+ : { offset: entry.byteOffset, length: entry.declaredSize },
18
+ limitations: [...entry.limitations],
19
+ });
20
+ export const materializeDirectoryNodes = (occurrences, nodes) => {
21
+ const directories = occurrences
22
+ .filter(({ entry_kind: kind }) => kind === "directory")
23
+ .sort((left, right) => depth(right.logical_path) - depth(left.logical_path));
24
+ const childrenByParent = new Map();
25
+ for (const occurrence of occurrences) {
26
+ if (occurrence.parent_occurrence_id === null)
27
+ continue;
28
+ const children = childrenByParent.get(occurrence.parent_occurrence_id);
29
+ if (children === undefined)
30
+ childrenByParent.set(occurrence.parent_occurrence_id, [occurrence]);
31
+ else
32
+ children.push(occurrence);
33
+ }
34
+ for (const directory of directories) {
35
+ const children = (childrenByParent.get(directory.occurrence_id) ?? [])
36
+ .map(({ logical_path, artifact_id, entry_kind }) => ({
37
+ name: logical_path.split("/").at(-1),
38
+ artifact_id,
39
+ entry_kind,
40
+ }))
41
+ .sort((left, right) => String(left.name).localeCompare(String(right.name)));
42
+ const node = createArtifactNode({
43
+ sha256: digestCanonical({ kind: "directory", children }),
44
+ size: 0,
45
+ kind: directory.logical_path.toLowerCase().endsWith(".framework")
46
+ ? "framework"
47
+ : "container",
48
+ format: "directory",
49
+ executable: false,
50
+ contentState: "virtual",
51
+ });
52
+ const existing = nodes.get(node.artifact_id);
53
+ nodes.set(node.artifact_id, existing?.kind === "framework" ? existing : node);
54
+ directory.artifact_id = node.artifact_id;
55
+ directory.hash_status = "verified";
56
+ }
57
+ };
58
+ export const createRootNode = (input) => createArtifactNode({
59
+ sha256: input.digest?.sha256 ??
60
+ digestCanonical({
61
+ kind: "directory-root",
62
+ children: input.occurrences.map(({ logical_path, artifact_id }) => ({
63
+ logical_path,
64
+ artifact_id,
65
+ })),
66
+ }),
67
+ size: input.digest?.bytes ?? 0,
68
+ kind: input.directory ||
69
+ ["zip", "ipa", "apk", "asar", "dmg", "pkg"].includes(input.format)
70
+ ? "container"
71
+ : classifyArtifactPath(input.path).kind,
72
+ format: input.format,
73
+ executable: false,
74
+ contentState: "materialized",
75
+ });
76
+ export const createArtifactNode = (input) => ({
77
+ artifact_id: `art_${digestCanonical({ schema_version: 1, sha256: input.sha256 })}`,
78
+ kind: input.kind,
79
+ format: input.format,
80
+ sha256: input.sha256,
81
+ size: input.size,
82
+ media_type: null,
83
+ architecture: null,
84
+ executable: input.executable,
85
+ content_state: input.contentState,
86
+ limitations: [],
87
+ });
88
+ export const rootOccurrenceFor = (node, declaredSize) => ({
89
+ occurrence_id: `occ_${digestCanonical({ root: node.artifact_id })}`,
90
+ artifact_id: node.artifact_id,
91
+ parent_occurrence_id: null,
92
+ logical_path: ".",
93
+ entry_kind: node.format === "directory" ? "directory" : "file",
94
+ declared_size: declaredSize,
95
+ compressed_size: null,
96
+ executable: node.executable,
97
+ encrypted: false,
98
+ hash_status: "verified",
99
+ source_location: null,
100
+ limitations: [],
101
+ });
102
+ export const rekeyOccurrences = (rootArtifactId, occurrences) => {
103
+ const replacements = new Map();
104
+ for (const occurrence of occurrences)
105
+ replacements.set(occurrence.occurrence_id, `occ_${digestCanonical({
106
+ root_artifact_id: rootArtifactId,
107
+ logical_path: occurrence.logical_path,
108
+ entry_kind: occurrence.entry_kind,
109
+ })}`);
110
+ for (const occurrence of occurrences) {
111
+ occurrence.occurrence_id =
112
+ replacements.get(occurrence.occurrence_id) ?? occurrence.occurrence_id;
113
+ if (occurrence.parent_occurrence_id !== null)
114
+ occurrence.parent_occurrence_id =
115
+ replacements.get(occurrence.parent_occurrence_id) ??
116
+ occurrence.parent_occurrence_id;
117
+ }
118
+ };
119
+ export const createArtifactEdges = (rootArtifactId, occurrences, producer) => {
120
+ const byId = new Map(occurrences.map((item) => [item.occurrence_id, item]));
121
+ const edges = [];
122
+ for (const occurrence of occurrences) {
123
+ if (occurrence.parent_occurrence_id === null ||
124
+ occurrence.artifact_id === null)
125
+ continue;
126
+ const mappedSource = occurrence.logical_path.endsWith(".map")
127
+ ? occurrences.find(({ logical_path: path }) => path === occurrence.logical_path.slice(0, -".map".length))
128
+ : undefined;
129
+ const parentArtifactId = mappedSource?.artifact_id ??
130
+ byId.get(occurrence.parent_occurrence_id)?.artifact_id ??
131
+ rootArtifactId;
132
+ const semantic = {
133
+ parent_artifact_id: parentArtifactId,
134
+ child_artifact_id: occurrence.artifact_id,
135
+ relation: occurrence.entry_kind === "slice"
136
+ ? "slice-of"
137
+ : relationFor(occurrence.logical_path),
138
+ occurrence_id: occurrence.occurrence_id,
139
+ logical_path: occurrence.logical_path,
140
+ };
141
+ edges.push({
142
+ edge_id: `edge_${digestCanonical(semantic)}`,
143
+ ...semantic,
144
+ producer: occurrence.entry_kind === "slice" ? (producer ?? null) : null,
145
+ ordinal: edges.length,
146
+ });
147
+ }
148
+ return edges;
149
+ };
150
+ export const nearestParent = (path, occurrences) => {
151
+ const parts = path.split("/");
152
+ while (parts.length > 1) {
153
+ parts.pop();
154
+ const candidate = occurrences.get(parts.join("/"));
155
+ if (candidate?.entry_kind === "directory")
156
+ return candidate;
157
+ }
158
+ return undefined;
159
+ };
160
+ export const classifyArtifactPath = (path) => {
161
+ const lower = path.toLowerCase();
162
+ if (lower.endsWith(".map"))
163
+ return { kind: "source-map", format: "source-map" };
164
+ if (/\.(?:m?js|cjs)$/u.test(lower))
165
+ return { kind: "javascript", format: "javascript-bundle" };
166
+ if (lower.endsWith(".asar"))
167
+ return { kind: "container", format: "asar" };
168
+ const archiveFormat = archiveFormatForExtension(extname(lower));
169
+ if (archiveFormat !== undefined)
170
+ return { kind: "container", format: archiveFormat };
171
+ if (/\.framework(?:\/|$)/u.test(lower))
172
+ return { kind: "framework", format: "file" };
173
+ if (/\.(?:node|dylib|so)$/u.test(lower))
174
+ return {
175
+ kind: lower.endsWith(".node") ? "native-addon" : "dynamic-library",
176
+ format: "file",
177
+ };
178
+ if (lower.endsWith(".plist"))
179
+ return { kind: "plist", format: "plist" };
180
+ if (lower.endsWith(".entitlements"))
181
+ return { kind: "entitlements", format: "entitlements" };
182
+ return { kind: "resource", format: "file" };
183
+ };
184
+ /** Classify executable formats from bytes while retaining path-specific roles. */
185
+ export const classifyArtifactContent = (path, prefix) => {
186
+ const byPath = classifyArtifactPath(path);
187
+ if (prefix.length >= 4) {
188
+ const magic = prefix.readUInt32BE(0);
189
+ if ([0xcafebabe, 0xbebafeca, 0xcafebabf, 0xbfbafeca].includes(magic))
190
+ return {
191
+ kind: byPath.kind === "native-addon" ? "native-addon" : "executable",
192
+ format: "mach-o-universal",
193
+ };
194
+ if ([0xfeedface, 0xfeedfacf, 0xcefaedfe, 0xcffaedfe].includes(magic))
195
+ return {
196
+ kind: ["native-addon", "dynamic-library"].includes(byPath.kind)
197
+ ? byPath.kind
198
+ : "executable",
199
+ format: "mach-o",
200
+ };
201
+ if (prefix.subarray(0, 4).equals(Buffer.from([0x7f, 0x45, 0x4c, 0x46])))
202
+ return {
203
+ kind: ["native-addon", "dynamic-library"].includes(byPath.kind)
204
+ ? byPath.kind
205
+ : "executable",
206
+ format: "elf",
207
+ };
208
+ }
209
+ if (prefix.length >= 2 && prefix[0] === 0x4d && prefix[1] === 0x5a)
210
+ return {
211
+ kind: byPath.kind === "native-addon" ? "native-addon" : "executable",
212
+ format: "pe",
213
+ };
214
+ return byPath;
215
+ };
216
+ export const pageOf = (items, offset, limit) => ({
217
+ items: items.slice(offset, offset + limit),
218
+ offset,
219
+ limit,
220
+ total: items.length,
221
+ next_offset: offset + limit < items.length ? offset + limit : null,
222
+ });
223
+ export const toOutputLimits = (limits) => ({
224
+ max_entries: limits.maxEntries,
225
+ max_total_bytes: limits.maxTotalBytes,
226
+ max_entry_bytes: limits.maxEntryBytes,
227
+ max_compression_ratio: limits.maxCompressionRatio,
228
+ max_depth: limits.maxDepth,
229
+ max_path_bytes: limits.maxPathBytes,
230
+ });
231
+ export const digestCanonical = (value) => {
232
+ const encoded = canonicalize(value);
233
+ if (encoded === undefined)
234
+ throw new TypeError("Artifact value is not canonical JSON");
235
+ return createHash("sha256").update(encoded).digest("hex");
236
+ };
237
+ const relationFor = (path) => {
238
+ const lower = path.toLowerCase();
239
+ if (lower.endsWith(".map"))
240
+ return "maps-source";
241
+ if (lower.includes(".framework/"))
242
+ return "embeds";
243
+ return "contains";
244
+ };
245
+ const archiveFormatForExtension = (extension) => {
246
+ switch (extension) {
247
+ case ".zip":
248
+ return "zip";
249
+ case ".ipa":
250
+ return "ipa";
251
+ case ".apk":
252
+ return "apk";
253
+ default:
254
+ return undefined;
255
+ }
256
+ };
257
+ const depth = (path) => path.split("/").length;
@@ -0,0 +1,253 @@
1
+ import { createHash } from "node:crypto";
2
+ import { createReadStream } from "node:fs";
3
+ import { lstat, open, realpath } from "node:fs/promises";
4
+ import { AsarArtifactReader } from "../artifacts/AsarArtifactReader.js";
5
+ import { ArtifactPathRegistry, normalizeArtifactPath, } from "../artifacts/ArtifactPaths.js";
6
+ import { ArtifactReaderFailure, } from "../artifacts/ArtifactReader.js";
7
+ import { DirectoryArtifactReader } from "../artifacts/DirectoryArtifactReader.js";
8
+ import { ZipArtifactReader } from "../artifacts/ZipArtifactReader.js";
9
+ import { MachOSliceArtifactReader } from "../artifacts/MachOSliceArtifactReader.js";
10
+ import { streamChunkToBuffer } from "../artifacts/StreamBytes.js";
11
+ import { artifactInventoryResultSchema, } from "../domain/artifactGraph.js";
12
+ import { classifyArtifactPath, classifyArtifactContent, createArtifactEdges, createArtifactNode, createOccurrence, createRootNode, digestCanonical, materializeDirectoryNodes, nearestParent, pageOf, rekeyOccurrences, rootOccurrenceFor, toOutputLimits, } from "./ArtifactGraphConstruction.js";
13
+ /** Inventory one local artifact without extracting or mounting it. */
14
+ export const inventoryArtifact = async (inputPath, limits, page, signal) => {
15
+ const snapshot = await scanArtifactInventory(inputPath, limits, signal);
16
+ return paginateArtifactInventory(snapshot, page);
17
+ };
18
+ /** Scan an artifact once and retain the complete immutable graph for projection. */
19
+ export const scanArtifactInventory = async (inputPath, limits, signal) => {
20
+ abortIfNeeded(signal);
21
+ const path = await realpath(inputPath);
22
+ const metadata = await lstat(path);
23
+ const rootFormat = await classifyRoot(path, metadata.isDirectory());
24
+ const rootDigest = metadata.isDirectory()
25
+ ? null
26
+ : await hashReadable(createReadStream(path), limits.maxTotalBytes, signal);
27
+ const reader = await createReader(path, rootFormat);
28
+ try {
29
+ const { nodes, occurrences } = await scanReader(reader, limits, signal);
30
+ materializeDirectoryNodes(occurrences, nodes);
31
+ const rootNode = createRootNode({
32
+ path,
33
+ format: rootFormat,
34
+ directory: metadata.isDirectory(),
35
+ digest: rootDigest,
36
+ occurrences,
37
+ });
38
+ nodes.set(rootNode.artifact_id, rootNode);
39
+ rekeyOccurrences(rootNode.artifact_id, occurrences);
40
+ const rootOccurrence = rootOccurrenceFor(rootNode, metadata.size);
41
+ for (const occurrence of occurrences)
42
+ if (occurrence.parent_occurrence_id === null)
43
+ occurrence.parent_occurrence_id = rootOccurrence.occurrence_id;
44
+ occurrences.unshift(rootOccurrence);
45
+ const edges = createArtifactEdges(rootNode.artifact_id, occurrences, reader?.provenance()[0]);
46
+ const orderedNodes = [...nodes.values()].sort((left, right) => left.artifact_id.localeCompare(right.artifact_id));
47
+ const orderedOccurrences = occurrences.sort((left, right) => left.logical_path.localeCompare(right.logical_path, "en"));
48
+ const orderedEdges = edges.sort((left, right) => left.edge_id.localeCompare(right.edge_id));
49
+ if (rootDigest !== null) {
50
+ const verified = await hashReadable(createReadStream(path), limits.maxTotalBytes, signal);
51
+ if (verified.sha256 !== rootDigest.sha256 ||
52
+ verified.bytes !== rootDigest.bytes)
53
+ throw new ArtifactReaderFailure("integrity", "Root artifact changed during inventory");
54
+ }
55
+ const graphSha256 = digestCanonical({
56
+ nodes: orderedNodes,
57
+ occurrences: orderedOccurrences,
58
+ edges: orderedEdges,
59
+ });
60
+ const manifestId = `agm_${digestCanonical({
61
+ schema_version: 1,
62
+ root_artifact_id: rootNode.artifact_id,
63
+ graph_sha256: graphSha256,
64
+ })}`;
65
+ return {
66
+ manifest: artifactInventoryResultSchema.shape.manifest.parse({
67
+ schema_version: 1,
68
+ manifest_id: manifestId,
69
+ root_artifact_id: rootNode.artifact_id,
70
+ root_sha256: rootNode.sha256,
71
+ root_format: rootNode.format,
72
+ graph_sha256: graphSha256,
73
+ node_count: orderedNodes.length,
74
+ occurrence_count: orderedOccurrences.length,
75
+ edge_count: orderedEdges.length,
76
+ }),
77
+ nodes: orderedNodes,
78
+ occurrences: orderedOccurrences,
79
+ edges: orderedEdges,
80
+ limits: toOutputLimits(limits),
81
+ provenance: reader?.provenance() ?? [],
82
+ limitations: inventoryLimitations(rootFormat, reader),
83
+ };
84
+ }
85
+ finally {
86
+ await reader?.close();
87
+ }
88
+ };
89
+ /** Project independently paged graph collections from one inventory snapshot. */
90
+ export const paginateArtifactInventory = (snapshot, page) => artifactInventoryResultSchema.parse({
91
+ ...snapshot,
92
+ nodes: pageOf(snapshot.nodes, page.nodeOffset, page.nodeLimit),
93
+ occurrences: pageOf(snapshot.occurrences, page.occurrenceOffset, page.occurrenceLimit),
94
+ edges: pageOf(snapshot.edges, page.edgeOffset, page.edgeLimit),
95
+ });
96
+ const inventoryLimitations = (format, reader) => {
97
+ if (reader !== undefined)
98
+ return [];
99
+ if (format === "dmg" || format === "pkg")
100
+ return [
101
+ `${format.toUpperCase()} root hash is observed; child inventory requires an approved native macOS adapter.`,
102
+ ];
103
+ if (format === "mach-o-universal" && process.platform !== "darwin")
104
+ return ["Universal Mach-O slices require the native macOS lipo adapter."];
105
+ return ["Artifact has no child container entries."];
106
+ };
107
+ const createReader = async (path, format) => {
108
+ switch (format) {
109
+ case "directory":
110
+ return new DirectoryArtifactReader(path);
111
+ case "zip":
112
+ case "ipa":
113
+ case "apk":
114
+ return new ZipArtifactReader(path, format);
115
+ case "asar":
116
+ return new AsarArtifactReader(path);
117
+ case "mach-o-universal":
118
+ return process.platform === "darwin"
119
+ ? new MachOSliceArtifactReader(path)
120
+ : undefined;
121
+ default:
122
+ return undefined;
123
+ }
124
+ };
125
+ const scanReader = async (reader, limits, signal) => {
126
+ const nodes = new Map();
127
+ const occurrences = [];
128
+ if (reader === undefined)
129
+ return { nodes, occurrences };
130
+ const occurrenceByPath = new Map();
131
+ const registry = new ArtifactPathRegistry();
132
+ let totalBytes = 0;
133
+ for await (const entry of reader.entries(signal)) {
134
+ if (occurrences.length >= limits.maxEntries)
135
+ throw new ArtifactReaderFailure("limit", "Artifact entry limit exceeded");
136
+ const logicalPath = normalizeArtifactPath(entry.path, limits);
137
+ registry.add(logicalPath, entry.kind);
138
+ preflightEntry(entry, limits);
139
+ const parent = nearestParent(logicalPath, occurrenceByPath);
140
+ const occurrence = createOccurrence(entry, logicalPath, parent?.occurrence_id ?? null);
141
+ if ((entry.kind === "file" || entry.kind === "slice") && !entry.encrypted) {
142
+ const remainingBytes = limits.maxTotalBytes - totalBytes;
143
+ if (remainingBytes <= 0)
144
+ throw new ArtifactReaderFailure("limit", "Artifact total byte limit exceeded");
145
+ if (entry.declaredSize !== null && entry.declaredSize > remainingBytes)
146
+ throw new ArtifactReaderFailure("limit", "Declared artifact bytes exceed remaining cumulative limit");
147
+ const digest = await hashReadable(await reader.open(entry, signal), Math.min(limits.maxEntryBytes, remainingBytes), signal);
148
+ totalBytes += digest.bytes;
149
+ if (entry.declaredSha256 !== null &&
150
+ entry.declaredSha256 !== digest.sha256)
151
+ throw new ArtifactReaderFailure("integrity", `Artifact integrity metadata disagrees with content: ${logicalPath}`);
152
+ if (totalBytes > limits.maxTotalBytes)
153
+ throw new ArtifactReaderFailure("limit", "Artifact total byte limit exceeded");
154
+ const classified = entry.kind === "slice"
155
+ ? { kind: "universal-slice", format: "mach-o" }
156
+ : classifyArtifactContent(logicalPath, digest.prefix);
157
+ const node = createArtifactNode({
158
+ sha256: digest.sha256,
159
+ size: digest.bytes,
160
+ kind: classified.kind,
161
+ format: classified.format,
162
+ executable: entry.executable,
163
+ contentState: "embedded",
164
+ });
165
+ nodes.set(node.artifact_id, node);
166
+ occurrence.artifact_id = node.artifact_id;
167
+ occurrence.hash_status = "verified";
168
+ }
169
+ occurrences.push(occurrence);
170
+ occurrenceByPath.set(logicalPath, occurrence);
171
+ }
172
+ return { nodes, occurrences };
173
+ };
174
+ const classifyRoot = async (path, directory) => {
175
+ if (directory)
176
+ return "directory";
177
+ const extensionFormat = classifyContainerExtension(path);
178
+ if (extensionFormat !== undefined)
179
+ return extensionFormat;
180
+ const handle = await open(path, "r");
181
+ try {
182
+ const magic = Buffer.alloc(4);
183
+ const observed = await handle.read(magic, 0, magic.length, 0);
184
+ if (observed.bytesRead === 4 &&
185
+ magic[0] === 0x50 &&
186
+ magic[1] === 0x4b &&
187
+ [0x03, 0x05, 0x07].includes(magic[2] ?? -1) &&
188
+ [0x04, 0x06, 0x08].includes(magic[3] ?? -1))
189
+ return "zip";
190
+ if (observed.bytesRead === 4) {
191
+ const header = magic.readUInt32BE(0);
192
+ if ([0xcafebabe, 0xbebafeca, 0xcafebabf, 0xbfbafeca].includes(header))
193
+ return "mach-o-universal";
194
+ if ([0xfeedface, 0xfeedfacf, 0xcefaedfe, 0xcffaedfe].includes(header))
195
+ return "mach-o";
196
+ if (magic.equals(Buffer.from([0x7f, 0x45, 0x4c, 0x46])))
197
+ return "elf";
198
+ }
199
+ if (observed.bytesRead >= 2 && magic[0] === 0x4d && magic[1] === 0x5a)
200
+ return "pe";
201
+ }
202
+ finally {
203
+ await handle.close();
204
+ }
205
+ return classifyArtifactPath(path).format;
206
+ };
207
+ const classifyContainerExtension = (path) => {
208
+ const lower = path.toLowerCase();
209
+ for (const format of ["asar", "ipa", "apk", "zip", "dmg", "pkg"])
210
+ if (lower.endsWith(`.${format}`))
211
+ return format;
212
+ return undefined;
213
+ };
214
+ const preflightEntry = (entry, limits) => {
215
+ if (entry.declaredSize !== null && entry.declaredSize > limits.maxEntryBytes)
216
+ throw new ArtifactReaderFailure("limit", `Entry exceeds byte limit: ${entry.path}`);
217
+ if (entry.declaredSize !== null &&
218
+ entry.compressedSize !== null &&
219
+ entry.compressedSize === 0 &&
220
+ entry.declaredSize > 0)
221
+ throw new ArtifactReaderFailure("limit", `Invalid compression ratio: ${entry.path}`);
222
+ if (entry.declaredSize !== null &&
223
+ entry.compressedSize !== null &&
224
+ entry.compressedSize > 0 &&
225
+ entry.declaredSize / entry.compressedSize > limits.maxCompressionRatio)
226
+ throw new ArtifactReaderFailure("limit", `Compression ratio exceeds limit: ${entry.path}`);
227
+ };
228
+ const hashReadable = async (stream, maximum, signal) => {
229
+ const hash = createHash("sha256");
230
+ const prefixes = [];
231
+ let prefixBytes = 0;
232
+ let bytes = 0;
233
+ for await (const raw of stream) {
234
+ abortIfNeeded(signal);
235
+ const chunk = streamChunkToBuffer(raw);
236
+ bytes += chunk.length;
237
+ if (bytes > maximum) {
238
+ stream.destroy();
239
+ throw new ArtifactReaderFailure("limit", "Observed entry bytes exceed limit");
240
+ }
241
+ hash.update(chunk);
242
+ if (prefixBytes < 16) {
243
+ const selected = chunk.subarray(0, 16 - prefixBytes);
244
+ prefixes.push(selected);
245
+ prefixBytes += selected.length;
246
+ }
247
+ }
248
+ return { sha256: hash.digest("hex"), bytes, prefix: Buffer.concat(prefixes) };
249
+ };
250
+ const abortIfNeeded = (signal) => {
251
+ if (signal?.aborted === true)
252
+ throw new ArtifactReaderFailure("cancelled", "Artifact inventory cancelled");
253
+ };