rea-agents 1.5.0 → 1.7.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 (172) hide show
  1. package/README.md +144 -58
  2. package/bridge/ghidra/ReaGhidraBridge.java +2075 -0
  3. package/bridge/hopper_bridge.py +13 -3
  4. package/dist/application/AbortablePromise.js +25 -0
  5. package/dist/application/AnalysisProvider.js +3 -0
  6. package/dist/application/AnalysisProviderEvaluation.js +60 -0
  7. package/dist/application/AnalysisProviderRegistry.js +228 -0
  8. package/dist/application/AnalysisSnapshotCache.js +41 -21
  9. package/dist/application/AnalysisSnapshotFiles.js +5 -6
  10. package/dist/application/BinarySession.js +229 -102
  11. package/dist/application/BrowserEvidence.js +87 -15
  12. package/dist/application/BrowserObservationService.js +65 -1
  13. package/dist/application/CapabilityInventory.js +7 -0
  14. package/dist/application/ClientConfigPath.js +33 -0
  15. package/dist/application/CompositeProvider.js +20 -8
  16. package/dist/application/CrossVersionInventory.js +16 -3
  17. package/dist/application/CrossVersionInvestigation.js +17 -62
  18. package/dist/application/CrossVersionInvestigationEvidence.js +64 -0
  19. package/dist/application/CrossVersionInvestigationReplay.js +170 -0
  20. package/dist/application/DirectAnalysis.js +49 -35
  21. package/dist/application/Doctor.js +70 -13
  22. package/dist/application/ElectronEvidence.js +35 -0
  23. package/dist/application/ElectronObservationPort.js +1 -0
  24. package/dist/application/ElectronObservationService.js +46 -0
  25. package/dist/application/EnhancedTools.js +1 -1
  26. package/dist/application/EvidenceLedger.js +5 -2
  27. package/dist/application/InvestigationProviders.js +11 -0
  28. package/dist/application/JavaScriptArtifactAnalysis.js +299 -0
  29. package/dist/application/JavaScriptArtifactAnalysisTypes.js +1 -0
  30. package/dist/application/JavaScriptArtifactFiles.js +231 -0
  31. package/dist/application/JavaScriptArtifactGraphAccumulator.js +54 -0
  32. package/dist/application/JavaScriptArtifactGraphBuilder.js +101 -0
  33. package/dist/application/JavaScriptArtifactGraphContext.js +205 -0
  34. package/dist/application/JavaScriptArtifactGraphDocuments.js +108 -0
  35. package/dist/application/JavaScriptArtifactGraphEvidence.js +89 -0
  36. package/dist/application/JavaScriptArtifactGraphFindings.js +271 -0
  37. package/dist/application/JavaScriptArtifactGraphStructure.js +409 -0
  38. package/dist/application/JavaScriptArtifactReconstruction.js +74 -0
  39. package/dist/application/JavaScriptArtifactReconstructionInput.js +99 -0
  40. package/dist/application/MacHopper.js +20 -4
  41. package/dist/application/PermissionAuthority.js +30 -0
  42. package/dist/application/ProcessCaptureLifecycle.js +2 -6
  43. package/dist/application/ProcessHarness.js +13 -7
  44. package/dist/application/ProcessSampling.js +59 -8
  45. package/dist/application/ProjectPermissionStore.js +1 -0
  46. package/dist/application/ReferenceSourceImport.js +13 -16
  47. package/dist/application/SessionProviderRouter.js +219 -0
  48. package/dist/application/Setup.js +153 -70
  49. package/dist/application/SetupClients.js +8 -5
  50. package/dist/application/SetupInstallFailure.js +24 -0
  51. package/dist/application/SetupPlan.js +19 -8
  52. package/dist/application/SetupSkill.js +20 -3
  53. package/dist/application/SupportedClients.js +40 -16
  54. package/dist/application/Uninstall.js +20 -7
  55. package/dist/application/Upgrade.js +15 -4
  56. package/dist/application/runtime.js +9 -6
  57. package/dist/artifacts/AsarArtifactReader.js +23 -3
  58. package/dist/browser/CdpBrowserProvider.js +158 -53
  59. package/dist/browser/CdpCaptureCompleteness.js +90 -0
  60. package/dist/browser/CdpCaptureDocuments.js +180 -21
  61. package/dist/browser/CdpCaptureEvents.js +342 -43
  62. package/dist/browser/CdpCaptureValues.js +5 -7
  63. package/dist/browser/CdpConnection.js +47 -24
  64. package/dist/browser/CdpElectronInspection.js +330 -0
  65. package/dist/browser/CdpElectronProvider.js +136 -0
  66. package/dist/browser/CdpEndpoint.js +77 -5
  67. package/dist/browser/CdpObservationSession.js +301 -0
  68. package/dist/browser/CdpPageCapture.js +159 -56
  69. package/dist/browser/CdpSafeMetadata.js +203 -0
  70. package/dist/browser/CdpScreenshot.js +101 -0
  71. package/dist/browser/CdpTargetSession.js +47 -0
  72. package/dist/browser/CdpWebMcpDiscovery.js +242 -0
  73. package/dist/browser/ElectronFileScope.js +50 -0
  74. package/dist/browser/PngVisualDiff.js +177 -0
  75. package/dist/browser/SensitiveTextCapture.js +25 -0
  76. package/dist/browser/WebSourceMapFetcher.js +357 -0
  77. package/dist/catalogIdentity.js +2 -33
  78. package/dist/cli.js +62 -34
  79. package/dist/cliBrowserAdvancedCommands.js +170 -0
  80. package/dist/cliBrowserCommands.js +180 -3
  81. package/dist/cliCommandNames.js +43 -0
  82. package/dist/cliElectronCommands.js +115 -0
  83. package/dist/cliEvidenceCommands.js +4 -3
  84. package/dist/cliInvestigationCommands.js +17 -9
  85. package/dist/cliPolicyCommands.js +49 -3
  86. package/dist/cliProcessCommands.js +3 -2
  87. package/dist/config.js +58 -7
  88. package/dist/contracts/browserToolContracts.js +304 -3
  89. package/dist/contracts/electronToolContracts.js +73 -0
  90. package/dist/contracts/enhancedInputs.js +2 -2
  91. package/dist/contracts/providerSelection.js +28 -0
  92. package/dist/contracts/sessionLifecycleInputs.js +2 -0
  93. package/dist/contracts/toolContracts.js +24 -20
  94. package/dist/contracts/toolOutputSchemas.js +117 -22
  95. package/dist/doctorRuntime.js +6 -0
  96. package/dist/domain/analysisProfile.js +78 -0
  97. package/dist/domain/analysisSnapshot.js +92 -46
  98. package/dist/domain/artifactGraph.js +1 -0
  99. package/dist/domain/binaryTarget.js +4 -29
  100. package/dist/domain/browserCompleteness.js +101 -0
  101. package/dist/domain/browserObservation.js +240 -14
  102. package/dist/domain/browserSession.js +63 -0
  103. package/dist/domain/electronObservation.js +162 -0
  104. package/dist/domain/errors.js +81 -6
  105. package/dist/domain/evidence.js +44 -5
  106. package/dist/domain/evidenceBundle.js +9 -7
  107. package/dist/domain/functionComparison.js +9 -3
  108. package/dist/domain/functionComparisonNormalization.js +37 -0
  109. package/dist/domain/functionDossierEvidence.js +4 -1
  110. package/dist/domain/hopperValues.js +66 -9
  111. package/dist/domain/investigationWorkspace.js +6 -1
  112. package/dist/domain/javascriptApplicationEvidenceSchemas.js +361 -0
  113. package/dist/domain/javascriptApplicationGraph.js +281 -0
  114. package/dist/domain/javascriptApplicationGraphSchemas.js +132 -0
  115. package/dist/domain/javascriptAstFingerprint.js +143 -0
  116. package/dist/domain/javascriptStaticAnalysis.js +358 -0
  117. package/dist/domain/javascriptStaticAnalysisHelpers.js +264 -0
  118. package/dist/domain/javascriptStaticAnalysisState.js +17 -0
  119. package/dist/domain/javascriptStaticAnalysisTypes.js +1 -0
  120. package/dist/domain/jsonShape.js +125 -0
  121. package/dist/domain/referenceSourceGraph.js +3 -1
  122. package/dist/domain/webBundleAnalysis.js +195 -0
  123. package/dist/domain/webBundleAnalyzer.js +423 -0
  124. package/dist/domain/webCaptureDiff.js +184 -0
  125. package/dist/domain/webContentArtifact.js +47 -0
  126. package/dist/domain/webInventory.js +61 -0
  127. package/dist/domain/webMcpDiscovery.js +68 -0
  128. package/dist/domain/webScreenshot.js +114 -0
  129. package/dist/generatedPackageMetadata.js +2 -2
  130. package/dist/ghidra/GhidraAnalysisProfile.js +37 -0
  131. package/dist/ghidra/GhidraClient.js +400 -0
  132. package/dist/ghidra/GhidraClientTypes.js +1 -0
  133. package/dist/ghidra/GhidraDefaults.js +20 -0
  134. package/dist/ghidra/GhidraDiagnostics.js +43 -0
  135. package/dist/ghidra/GhidraDoctor.js +59 -0
  136. package/dist/ghidra/GhidraFunctionValues.js +264 -0
  137. package/dist/ghidra/GhidraInstallation.js +239 -0
  138. package/dist/ghidra/GhidraInventoryValues.js +217 -0
  139. package/dist/ghidra/GhidraLauncher.js +139 -0
  140. package/dist/ghidra/GhidraProvider.js +360 -0
  141. package/dist/ghidra/GhidraRequestQueue.js +98 -0
  142. package/dist/ghidra/GhidraResponseBuffer.js +31 -0
  143. package/dist/ghidra/GhidraResponseRouter.js +24 -0
  144. package/dist/ghidra/GhidraSessionError.js +39 -0
  145. package/dist/ghidra/GhidraSessionValues.js +62 -0
  146. package/dist/ghidra/GhidraSocketConnection.js +46 -0
  147. package/dist/ghidra/protocol.js +69 -0
  148. package/dist/hopper/BridgeLauncher.js +21 -57
  149. package/dist/hopper/HopperAnalysisProfile.js +106 -0
  150. package/dist/hopper/HopperClient.js +197 -165
  151. package/dist/hopper/HopperProvider.js +105 -8
  152. package/dist/hopper/HopperSessionValues.js +27 -0
  153. package/dist/hopper/HopperSocketConnection.js +31 -0
  154. package/dist/main.js +36 -31
  155. package/dist/native/NativeMacOSProvider.js +6 -3
  156. package/dist/native/NativeMachoInspection.js +21 -6
  157. package/dist/process/PendingOperations.js +59 -0
  158. package/dist/process/PrivateRuntimeRoot.js +33 -0
  159. package/dist/{application → process}/ProcessOwnership.js +40 -20
  160. package/dist/process/ProviderDeadline.js +95 -0
  161. package/dist/process/ProviderProcess.js +275 -0
  162. package/dist/server/createServer.js +8 -0
  163. package/dist/server/promptCompletion.js +9 -3
  164. package/dist/server/registerBrowserTools.js +61 -2
  165. package/dist/server/registerElectronTools.js +36 -0
  166. package/dist/server/registerEnhancedTools.js +9 -6
  167. package/dist/server/registerInvestigationTools.js +11 -12
  168. package/dist/server/registerOfficialTools.js +3 -0
  169. package/dist/server/registerSessionTools.js +7 -1
  170. package/package.json +12 -6
  171. package/scripts/rea.mjs +37 -3
  172. package/skills/rea-analysis/SKILL.md +19 -9
@@ -0,0 +1,299 @@
1
+ import { createHash } from "node:crypto";
2
+ import { analyzeJavaScriptStaticSource } from "../domain/javascriptStaticAnalysis.js";
3
+ /** Analyze selected text files under one shared AST, finding, module, and time budget. */
4
+ export const analyzeJavaScriptArtifactFiles = (fileSet, input, now) => {
5
+ const state = emptyArtifactAnalysis();
6
+ const context = {
7
+ state,
8
+ input,
9
+ deadline: now() + input.limits.max_parse_milliseconds,
10
+ now,
11
+ };
12
+ for (const file of fileSet.files)
13
+ analyzeArtifactFile(file, context);
14
+ return finalizeArtifactAnalysis(state, input);
15
+ };
16
+ const finalizeArtifactAnalysis = (state, input) => {
17
+ const omittedHtmlFindings = Math.max(0, state.htmlScripts.length + state.findings - input.limits.max_findings);
18
+ const findings = Math.min(input.limits.max_findings, state.findings + state.htmlScripts.length);
19
+ const sourceMapTruncations = state.sourceMaps.filter(({ status }) => status === "truncated").length;
20
+ const truncatedScopes = state.truncatedScopes +
21
+ sourceMapTruncations +
22
+ (omittedHtmlFindings > 0 ? 1 : 0);
23
+ return {
24
+ files: state.files,
25
+ packages: state.packages,
26
+ html_scripts: state.htmlScripts.slice(0, input.limits.max_findings),
27
+ source_maps: state.sourceMaps,
28
+ visited_ast_nodes: state.visitedNodes,
29
+ findings,
30
+ modules: state.modules,
31
+ parse_failures: state.parseFailures,
32
+ truncated_scopes: truncatedScopes,
33
+ limitations: [
34
+ "JavaScript and HTML were parsed as inert text; bundle bootstrap code was never executed.",
35
+ "Static paths and relationships may remain unresolved when expressions are dynamic or obfuscated.",
36
+ ...(input.source_map_read_approved
37
+ ? []
38
+ : [
39
+ "Source maps were inventoried but their contents were not read without separate approval.",
40
+ ]),
41
+ ...(truncatedScopes === 0
42
+ ? []
43
+ : ["One or more static-analysis scopes reached an approved bound."]),
44
+ ],
45
+ };
46
+ };
47
+ const analyzeArtifactFile = (file, context) => {
48
+ const { state, input } = context;
49
+ if (file.kind === "package-json")
50
+ state.packages.push(parsePackage(file));
51
+ if (file.kind === "html" && file.text.included)
52
+ state.htmlScripts.push(...parseHtmlScripts(file.path, file.text.value, Math.max(0, input.limits.max_findings - state.findings)));
53
+ if (file.kind === "source-map")
54
+ addSourceMap(file, context);
55
+ if (file.kind !== "javascript" || !file.text.included) {
56
+ state.files.push({ file, javascript: null });
57
+ return;
58
+ }
59
+ const remainingNodes = input.limits.max_ast_nodes - state.visitedNodes;
60
+ const remainingFindings = input.limits.max_findings - state.findings;
61
+ const remainingModules = input.limits.max_modules - state.modules;
62
+ if (remainingNodes <= 0 ||
63
+ remainingFindings <= 0 ||
64
+ remainingModules <= 0 ||
65
+ context.now() > context.deadline) {
66
+ state.files.push({ file, javascript: null });
67
+ state.truncatedScopes += 1;
68
+ return;
69
+ }
70
+ const analysis = analyzeJavaScriptStaticSource(file.text.value, {
71
+ maxAstNodes: remainingNodes,
72
+ maxFindings: remainingFindings,
73
+ maxModules: remainingModules,
74
+ deadline: context.deadline,
75
+ now: context.now,
76
+ });
77
+ state.files.push({ file, javascript: analysis });
78
+ state.visitedNodes += analysis.visited_ast_nodes;
79
+ state.findings += findingCount(analysis);
80
+ state.modules += analysis.bundler_registrations.reduce((count, registration) => count + registration.modules.length, 0);
81
+ if (analysis.parse_status === "failed")
82
+ state.parseFailures += 1;
83
+ if (analysis.parse_status === "truncated")
84
+ state.truncatedScopes += 1;
85
+ };
86
+ const addSourceMap = (file, context) => {
87
+ const sourceMap = parseSourceMap(file, {
88
+ deadline: context.deadline,
89
+ now: context.now,
90
+ maximumSources: Math.max(0, context.input.limits.max_source_map_sources -
91
+ context.state.sourceMapSources),
92
+ });
93
+ context.state.sourceMaps.push(sourceMap);
94
+ context.state.sourceMapSources += sourceMap.sources.length;
95
+ };
96
+ const emptyArtifactAnalysis = () => ({
97
+ files: [],
98
+ packages: [],
99
+ htmlScripts: [],
100
+ sourceMaps: [],
101
+ visitedNodes: 0,
102
+ findings: 0,
103
+ modules: 0,
104
+ parseFailures: 0,
105
+ truncatedScopes: 0,
106
+ sourceMapSources: 0,
107
+ });
108
+ const parsePackage = (file) => {
109
+ if (!file.text.included)
110
+ return unavailablePackage(file, "Package metadata text was unavailable.");
111
+ let value;
112
+ try {
113
+ value = JSON.parse(file.text.value);
114
+ }
115
+ catch {
116
+ return invalidPackage(file, "package.json is not valid JSON.");
117
+ }
118
+ if (!isRecord(value))
119
+ return invalidPackage(file, "package.json root is not an object.");
120
+ return {
121
+ path: file.path,
122
+ sha256: file.sha256,
123
+ status: "included",
124
+ name: boundedString(value.name),
125
+ version: boundedString(value.version),
126
+ main: boundedString(value.main),
127
+ renderer: boundedString(value.renderer) ??
128
+ boundedString(value.browser) ??
129
+ boundedString(value.module),
130
+ limitation: null,
131
+ };
132
+ };
133
+ const invalidPackage = (file, limitation) => ({
134
+ ...unavailablePackage(file, limitation),
135
+ status: "invalid",
136
+ });
137
+ const unavailablePackage = (file, limitation) => ({
138
+ path: file.path,
139
+ sha256: file.sha256,
140
+ status: "unavailable",
141
+ name: null,
142
+ version: null,
143
+ main: null,
144
+ renderer: null,
145
+ limitation,
146
+ });
147
+ const parseHtmlScripts = (path, text, maximum) => {
148
+ const scripts = [];
149
+ const pattern = /<script\b[^>]*\bsrc\s*=\s*(["'])([^"']+)\1[^>]*>/giu;
150
+ for (const match of text.matchAll(pattern)) {
151
+ const script = match[2]?.slice(0, 4_096);
152
+ if (script === undefined || scripts.length >= maximum)
153
+ break;
154
+ const start = match.index;
155
+ scripts.push({
156
+ html_path: path,
157
+ script_path: script,
158
+ location: rangeForOffsets(text, start, start + match[0].length),
159
+ });
160
+ }
161
+ return scripts;
162
+ };
163
+ const parseSourceMap = (file, context) => {
164
+ if (!file.text.included)
165
+ return unavailableSourceMap(file);
166
+ if (context.now() > context.deadline)
167
+ return truncatedSourceMap(file, null, "Parse deadline reached before source-map decoding.");
168
+ let value;
169
+ try {
170
+ value = JSON.parse(file.text.value);
171
+ }
172
+ catch {
173
+ return invalidSourceMap(file, "Source map is not valid JSON.");
174
+ }
175
+ if (context.now() > context.deadline)
176
+ return truncatedSourceMap(file, null, "Parse deadline elapsed during source-map decoding.");
177
+ const maps = flattenSourceMaps(value, 10_000);
178
+ if (maps === undefined)
179
+ return invalidSourceMap(file, "Source map is not a bounded version 3 map.");
180
+ return collectSourceMapOriginals(file, maps, context);
181
+ };
182
+ const collectSourceMapOriginals = (file, maps, context) => {
183
+ const sources = [];
184
+ let total = 0;
185
+ for (const map of maps) {
186
+ const names = map.sources;
187
+ if (!Array.isArray(names))
188
+ return invalidSourceMap(file, "Source map has no sources array.");
189
+ const contents = Array.isArray(map.sourcesContent)
190
+ ? map.sourcesContent
191
+ : [];
192
+ const root = typeof map.sourceRoot === "string" ? map.sourceRoot : "";
193
+ for (const [index, raw] of names.entries()) {
194
+ if (total % 1_024 === 0 && context.now() > context.deadline)
195
+ return {
196
+ path: file.path,
197
+ sha256: file.sha256,
198
+ status: "truncated",
199
+ sources,
200
+ omitted_sources: null,
201
+ limitation: "Parse deadline elapsed during source-map traversal.",
202
+ };
203
+ if (typeof raw !== "string")
204
+ return invalidSourceMap(file, "Source map contains a non-string source name.");
205
+ total += 1;
206
+ if (sources.length >= context.maximumSources)
207
+ continue;
208
+ const content = typeof contents[index] === "string" ? contents[index] : null;
209
+ sources.push({
210
+ source: resolveSourceName(root, raw),
211
+ content,
212
+ content_sha256: content === null ? null : sha256(content),
213
+ });
214
+ }
215
+ }
216
+ const omitted = total - sources.length;
217
+ return {
218
+ path: file.path,
219
+ sha256: file.sha256,
220
+ status: omitted === 0 ? "included" : "truncated",
221
+ sources,
222
+ omitted_sources: omitted,
223
+ limitation: omitted === 0
224
+ ? null
225
+ : "Original-source inventory reached the approved source-map limit.",
226
+ };
227
+ };
228
+ const unavailableSourceMap = (file) => {
229
+ if (file.text.included)
230
+ throw new TypeError("Expected unavailable source-map text");
231
+ const notApproved = file.text.reason === "not-approved";
232
+ return {
233
+ path: file.path,
234
+ sha256: file.sha256,
235
+ status: notApproved ? "not-approved" : "invalid",
236
+ sources: [],
237
+ omitted_sources: 0,
238
+ limitation: notApproved
239
+ ? "Source-map content requires separate approval."
240
+ : "Source-map text was unavailable within approved limits.",
241
+ };
242
+ };
243
+ const flattenSourceMaps = (root, maximum) => {
244
+ if (!isRecord(root) || root.version !== 3)
245
+ return undefined;
246
+ const maps = [];
247
+ const pending = [root];
248
+ while (pending.length > 0) {
249
+ const map = pending.pop();
250
+ if (map === undefined || map.version !== 3)
251
+ return undefined;
252
+ if (Array.isArray(map.sections)) {
253
+ if (pending.length + maps.length + map.sections.length > maximum)
254
+ return undefined;
255
+ for (const section of map.sections) {
256
+ if (!isRecord(section) || !isRecord(section.map))
257
+ return undefined;
258
+ pending.push(section.map);
259
+ }
260
+ }
261
+ else
262
+ maps.push(map);
263
+ }
264
+ return maps;
265
+ };
266
+ const truncatedSourceMap = (file, omitted, limitation) => ({
267
+ path: file.path,
268
+ sha256: file.sha256,
269
+ status: "truncated",
270
+ sources: [],
271
+ omitted_sources: omitted,
272
+ limitation,
273
+ });
274
+ const invalidSourceMap = (file, limitation) => ({
275
+ path: file.path,
276
+ sha256: file.sha256,
277
+ status: "invalid",
278
+ sources: [],
279
+ omitted_sources: 0,
280
+ limitation,
281
+ });
282
+ const findingCount = (analysis) => analysis.references.length +
283
+ analysis.endpoints.length +
284
+ analysis.storage.length +
285
+ analysis.role_paths.length +
286
+ analysis.source_map_urls.length +
287
+ analysis.bundler_registrations.length;
288
+ const rangeForOffsets = (text, start, end) => ({
289
+ start: pointForOffset(text, start),
290
+ end: pointForOffset(text, end),
291
+ });
292
+ const pointForOffset = (text, offset) => {
293
+ const lines = text.slice(0, offset).split("\n");
294
+ return { line: lines.length, column: lines.at(-1)?.length ?? 0 };
295
+ };
296
+ const resolveSourceName = (root, source) => `${root}${root !== "" && !root.endsWith("/") ? "/" : ""}${source}`.slice(0, 4_096);
297
+ const boundedString = (value) => typeof value === "string" && value.length > 0 ? value.slice(0, 4_096) : null;
298
+ const sha256 = (value) => createHash("sha256").update(value).digest("hex");
299
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
@@ -0,0 +1,231 @@
1
+ import { createHash } from "node:crypto";
2
+ import { isAbsolute } from "node:path";
3
+ import { AsarArtifactReader } from "../artifacts/AsarArtifactReader.js";
4
+ import { ArtifactPathRegistry, normalizeArtifactPath, } from "../artifacts/ArtifactPaths.js";
5
+ import { ArtifactReaderFailure, } from "../artifacts/ArtifactReader.js";
6
+ import { streamChunkToBuffer } from "../artifacts/StreamBytes.js";
7
+ /** Read only selected textual entries through an already-inventoried reader. */
8
+ export const readJavaScriptArtifactFiles = async (reader, snapshot, input, signal) => {
9
+ const inventory = expectedInventory(snapshot);
10
+ const expected = inventory.files;
11
+ const selections = selectTextFiles(expected, input);
12
+ const context = {
13
+ input,
14
+ expected,
15
+ expectedContainers: inventory.containers,
16
+ selections,
17
+ registry: new ArtifactPathRegistry(),
18
+ files: [],
19
+ containers: [],
20
+ signal,
21
+ textBytes: 0,
22
+ invalidUtf8: 0,
23
+ };
24
+ await visitReader(reader, "", snapshot.manifest.root_sha256, context);
25
+ const files = context.files.sort((left, right) => compareCodePoints(left.path, right.path));
26
+ assertExpectedFilesWereVisited(expected, files);
27
+ const selected = [...selections.values()].filter(({ selected: isSelected }) => isSelected).length;
28
+ return {
29
+ files,
30
+ containers: context.containers.sort((left, right) => compareCodePoints(left.path, right.path)),
31
+ text_files_selected: selected,
32
+ text_bytes_read: context.textBytes,
33
+ omitted_text_files: [...selections.values()].filter(({ selected: isSelected }) => !isSelected).length,
34
+ limit_omitted_text_files: [...selections.values()].filter(({ reason }) => reason === "file-limit" || reason === "byte-limit").length,
35
+ policy_filtered_text_files: [...selections.values()].filter(({ reason }) => reason === "not-approved").length,
36
+ invalid_utf8_files: context.invalidUtf8,
37
+ };
38
+ };
39
+ const visitReader = async (reader, prefix, containerSha256, context) => {
40
+ for await (const entry of reader.entries(context.signal)) {
41
+ abortIfNeeded(context.signal);
42
+ const path = normalizeArtifactPath(prefix === "" ? entry.path : `${prefix}/${entry.path}`, {
43
+ maxDepth: context.input.limits.max_depth,
44
+ maxPathBytes: context.input.limits.max_path_bytes,
45
+ });
46
+ const nestedAsar = isFilesystemAsar(entry, path);
47
+ context.registry.add(path, nestedAsar ? "directory" : entry.kind);
48
+ if (nestedAsar) {
49
+ await visitNestedAsar(entry, path, context);
50
+ continue;
51
+ }
52
+ const expected = context.expected.get(path);
53
+ if (expected === undefined)
54
+ continue;
55
+ const selection = context.selections.get(path);
56
+ const text = await readTextIfSelected(context, {
57
+ reader,
58
+ entry,
59
+ expected,
60
+ selection,
61
+ });
62
+ context.files.push({
63
+ path,
64
+ container_sha256: containerSha256,
65
+ sha256: expected.sha256,
66
+ bytes: expected.bytes,
67
+ inventory_artifact_id: expected.inventoryArtifactId,
68
+ kind: expected.kind,
69
+ unpacked: entry.unpacked,
70
+ text,
71
+ });
72
+ }
73
+ };
74
+ const visitNestedAsar = async (entry, path, context) => {
75
+ const inventory = expectedContainer(path, context);
76
+ context.containers.push(inventory);
77
+ const nested = new AsarArtifactReader(entry.adapterKey);
78
+ try {
79
+ await visitReader(nested, path, inventory.sha256, context);
80
+ }
81
+ finally {
82
+ await nested.close();
83
+ }
84
+ };
85
+ const expectedContainer = (path, context) => {
86
+ const container = context.expectedContainers.get(path);
87
+ if (container === undefined)
88
+ throw new ArtifactReaderFailure("integrity", `Nested ASAR disappeared from inventory: ${path}`);
89
+ return container;
90
+ };
91
+ const readTextIfSelected = async (context, input) => {
92
+ if (input.expected.kind === "native-addon")
93
+ return { included: false, reason: "not-applicable" };
94
+ if (input.selection?.selected !== true)
95
+ return {
96
+ included: false,
97
+ reason: input.selection?.reason ?? "file-limit",
98
+ };
99
+ const bytes = await readBounded(await input.reader.open(input.entry, context.signal), context.input.limits.max_text_file_bytes, context.signal);
100
+ const digest = createHash("sha256").update(bytes).digest("hex");
101
+ if (digest !== input.expected.sha256 || bytes.length !== input.expected.bytes)
102
+ throw new ArtifactReaderFailure("integrity", `Artifact entry changed after inventory: ${input.expected.path}`);
103
+ context.textBytes += bytes.length;
104
+ try {
105
+ return {
106
+ included: true,
107
+ value: new TextDecoder("utf-8", { fatal: true }).decode(bytes),
108
+ };
109
+ }
110
+ catch {
111
+ context.invalidUtf8 += 1;
112
+ return { included: false, reason: "invalid-utf8" };
113
+ }
114
+ };
115
+ const expectedInventory = (snapshot) => {
116
+ const nodes = new Map(snapshot.nodes.map((node) => [node.artifact_id, node]));
117
+ const files = new Map();
118
+ const containers = new Map();
119
+ for (const occurrence of snapshot.occurrences) {
120
+ if (occurrence.artifact_id === null || occurrence.logical_path === ".")
121
+ continue;
122
+ const node = nodes.get(occurrence.artifact_id);
123
+ if (node === undefined)
124
+ throw new ArtifactReaderFailure("integrity", `Artifact inventory node is missing: ${occurrence.logical_path}`);
125
+ if (occurrence.logical_path.toLowerCase().endsWith(".asar"))
126
+ containers.set(occurrence.logical_path, {
127
+ path: occurrence.logical_path,
128
+ sha256: node.sha256,
129
+ bytes: node.size,
130
+ inventory_artifact_id: node.artifact_id,
131
+ });
132
+ const kind = relevantKind(occurrence.logical_path);
133
+ if (kind === undefined)
134
+ continue;
135
+ files.set(occurrence.logical_path, {
136
+ path: occurrence.logical_path,
137
+ sha256: node.sha256,
138
+ bytes: node.size,
139
+ inventoryArtifactId: node.artifact_id,
140
+ kind,
141
+ });
142
+ }
143
+ return { files, containers };
144
+ };
145
+ const selectTextFiles = (files, input) => {
146
+ const selected = new Map();
147
+ let fileCount = 0;
148
+ let bytes = 0;
149
+ const ordered = [...files.values()].sort((left, right) => {
150
+ const priority = filePriority(left.kind) - filePriority(right.kind);
151
+ return priority === 0 ? compareCodePoints(left.path, right.path) : priority;
152
+ });
153
+ for (const file of ordered) {
154
+ if (file.kind === "native-addon")
155
+ continue;
156
+ if (file.kind === "source-map" && !input.source_map_read_approved) {
157
+ selected.set(file.path, { selected: false, reason: "not-approved" });
158
+ continue;
159
+ }
160
+ if (fileCount >= input.limits.max_text_files ||
161
+ file.bytes > input.limits.max_text_file_bytes) {
162
+ selected.set(file.path, { selected: false, reason: "file-limit" });
163
+ continue;
164
+ }
165
+ if (bytes + file.bytes > input.limits.max_total_text_bytes) {
166
+ selected.set(file.path, { selected: false, reason: "byte-limit" });
167
+ continue;
168
+ }
169
+ selected.set(file.path, { selected: true, reason: null });
170
+ fileCount += 1;
171
+ bytes += file.bytes;
172
+ }
173
+ return selected;
174
+ };
175
+ const readBounded = async (stream, maximum, signal) => {
176
+ const chunks = [];
177
+ let bytes = 0;
178
+ for await (const raw of stream) {
179
+ abortIfNeeded(signal);
180
+ const chunk = streamChunkToBuffer(raw);
181
+ bytes += chunk.length;
182
+ if (bytes > maximum) {
183
+ stream.destroy();
184
+ throw new ArtifactReaderFailure("limit", "Selected JavaScript artifact text exceeded its byte limit");
185
+ }
186
+ chunks.push(chunk);
187
+ }
188
+ return Buffer.concat(chunks, bytes);
189
+ };
190
+ const assertExpectedFilesWereVisited = (expected, actual) => {
191
+ const observed = new Set(actual.map(({ path }) => path));
192
+ for (const path of expected.keys())
193
+ if (!observed.has(path))
194
+ throw new ArtifactReaderFailure("integrity", `Relevant artifact entry disappeared after inventory: ${path}`);
195
+ };
196
+ const relevantKind = (path) => {
197
+ const lower = path.toLowerCase();
198
+ if (lower === "package.json" || lower.endsWith("/package.json"))
199
+ return "package-json";
200
+ if (/\.(?:cjs|mjs|js|jsx|ts|tsx)$/u.test(lower))
201
+ return "javascript";
202
+ if (/\.html?$/u.test(lower))
203
+ return "html";
204
+ if (lower.endsWith(".map"))
205
+ return "source-map";
206
+ if (lower.endsWith(".node"))
207
+ return "native-addon";
208
+ return undefined;
209
+ };
210
+ const filePriority = (kind) => {
211
+ switch (kind) {
212
+ case "package-json":
213
+ return 0;
214
+ case "html":
215
+ return 1;
216
+ case "javascript":
217
+ return 2;
218
+ case "source-map":
219
+ return 3;
220
+ case "native-addon":
221
+ return 4;
222
+ }
223
+ };
224
+ const isFilesystemAsar = (entry, path) => entry.kind === "file" &&
225
+ path.toLowerCase().endsWith(".asar") &&
226
+ isAbsolute(entry.adapterKey);
227
+ const abortIfNeeded = (signal) => {
228
+ if (signal?.aborted === true)
229
+ throw new ArtifactReaderFailure("cancelled", "JavaScript artifact reconstruction cancelled");
230
+ };
231
+ const compareCodePoints = (left, right) => left < right ? -1 : left > right ? 1 : 0;
@@ -0,0 +1,54 @@
1
+ import { createJavaScriptApplicationEdge, createJavaScriptApplicationNode, } from "../domain/javascriptApplicationGraph.js";
2
+ /** Deduplicate graph entities while retaining distinct evidence observations. */
3
+ export class JavaScriptArtifactGraphAccumulator {
4
+ #nodes = new Map();
5
+ #edges = new Map();
6
+ #omittedObservations = 0;
7
+ /** Create or merge one canonical node. */
8
+ addNode(input) {
9
+ const created = createJavaScriptApplicationNode(input);
10
+ const existing = this.#nodes.get(created.node_id);
11
+ if (existing === undefined) {
12
+ this.#nodes.set(created.node_id, created);
13
+ return created;
14
+ }
15
+ const bySemanticId = new Map([...existing.observations, ...created.observations].map((observation) => [
16
+ observation.observation_id,
17
+ observationInput(observation),
18
+ ]));
19
+ const observations = [...bySemanticId.entries()]
20
+ .sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0))
21
+ .slice(0, 64)
22
+ .map(([, observation]) => observation);
23
+ this.#omittedObservations += Math.max(0, bySemanticId.size - 64);
24
+ const merged = createJavaScriptApplicationNode({
25
+ kind: existing.kind,
26
+ identity: existing.identity,
27
+ observations,
28
+ });
29
+ this.#nodes.set(merged.node_id, merged);
30
+ return merged;
31
+ }
32
+ /** Create or replace one canonical edge by semantic identifier. */
33
+ addEdge(input) {
34
+ const edge = createJavaScriptApplicationEdge(input);
35
+ this.#edges.set(edge.edge_id, edge);
36
+ return edge;
37
+ }
38
+ /** Return all accumulated canonical nodes. */
39
+ nodes() {
40
+ return [...this.#nodes.values()];
41
+ }
42
+ /** Return all accumulated canonical edges. */
43
+ edges() {
44
+ return [...this.#edges.values()];
45
+ }
46
+ /** Count distinct node observations omitted by the graph contract bound. */
47
+ omittedObservations() {
48
+ return this.#omittedObservations;
49
+ }
50
+ }
51
+ const observationInput = (observation) => {
52
+ const { observation_id: _observationId, identifier_strategy: _strategy, ...input } = observation;
53
+ return input;
54
+ };
@@ -0,0 +1,101 @@
1
+ import { createJavaScriptApplicationGraph, } from "../domain/javascriptApplicationGraph.js";
2
+ import { JavaScriptArtifactGraphAccumulator } from "./JavaScriptArtifactGraphAccumulator.js";
3
+ import { javaScriptAnalysisLimits, } from "./JavaScriptArtifactGraphContext.js";
4
+ import { addJavaScriptHtmlRoles, addJavaScriptSourceMapOriginals, } from "./JavaScriptArtifactGraphDocuments.js";
5
+ import { addJavaScriptStaticFindings } from "./JavaScriptArtifactGraphFindings.js";
6
+ import { completeReconstructionCoverage, partialReconstructionCoverage, } from "./JavaScriptArtifactGraphEvidence.js";
7
+ import { addJavaScriptArtifactContainers, addJavaScriptArtifactFiles, addJavaScriptBundlerNodes, addJavaScriptPackageNodes, createJavaScriptArtifactRootNode, } from "./JavaScriptArtifactGraphStructure.js";
8
+ /** Project bounded artifact and AST facts into JavaScript Application Graph v1. */
9
+ export const buildJavaScriptArtifactGraph = (snapshot, fileSet, analysis, input) => {
10
+ const accumulator = new JavaScriptArtifactGraphAccumulator();
11
+ const root = createJavaScriptArtifactRootNode(accumulator, snapshot);
12
+ const context = {
13
+ accumulator,
14
+ snapshot,
15
+ fileSet,
16
+ analysis,
17
+ input,
18
+ root,
19
+ filesByPath: new Map(fileSet.files.map((file) => [file.path, file])),
20
+ fileNodes: new Map(),
21
+ assetNodes: new Map(),
22
+ moduleNodes: new Map(),
23
+ containerNodes: new Map([[snapshot.manifest.root_sha256, root]]),
24
+ };
25
+ addJavaScriptArtifactContainers(context);
26
+ addJavaScriptArtifactFiles(context);
27
+ const packageRoots = addJavaScriptPackageNodes(context);
28
+ addJavaScriptBundlerNodes(context);
29
+ addJavaScriptStaticFindings(context);
30
+ addJavaScriptHtmlRoles(context);
31
+ addJavaScriptSourceMapOriginals(context);
32
+ const coverage = graphCoverage(context);
33
+ return createJavaScriptApplicationGraph({
34
+ schema: "JavaScriptApplicationGraph",
35
+ schema_version: 1,
36
+ root_node_ids: packageRoots.length === 0
37
+ ? [root.node_id]
38
+ : packageRoots.map(({ node_id: id }) => id),
39
+ nodes: accumulator.nodes(),
40
+ edges: accumulator.edges(),
41
+ coverage,
42
+ limitations: graphLimitations(context, coverage.status),
43
+ });
44
+ };
45
+ const graphCoverage = (context) => {
46
+ const exactLimitOmissions = context.fileSet.limit_omitted_text_files +
47
+ context.accumulator.omittedObservations();
48
+ const sourceMapPolicyGap = context.analysis.source_maps.some(({ status }) => status === "not-approved");
49
+ const malformedStructuredData = context.analysis.packages.some(({ status }) => status !== "included") ||
50
+ context.analysis.source_maps.some(({ status }) => status === "invalid");
51
+ const unknownGap = context.analysis.parse_failures > 0 ||
52
+ context.fileSet.invalid_utf8_files > 0 ||
53
+ sourceMapPolicyGap ||
54
+ malformedStructuredData;
55
+ if (context.analysis.truncated_scopes > 0)
56
+ return partialReconstructionCoverage(reconstructionLimits(context.input), null, true);
57
+ if (exactLimitOmissions > 0)
58
+ return partialReconstructionCoverage(reconstructionLimits(context.input), exactLimitOmissions, true);
59
+ if (unknownGap)
60
+ return partialReconstructionCoverage(reconstructionLimits(context.input), null, false);
61
+ return completeReconstructionCoverage(reconstructionLimits(context.input));
62
+ };
63
+ const graphLimitations = (context, coverage) => [
64
+ ...context.analysis.limitations,
65
+ "Webpack/Rspack factories were recovered from AST literals; REA did not invoke push handlers or bundle bootstrap code.",
66
+ "Static imports, entrypoints, workers, endpoints, and storage relationships do not prove runtime execution.",
67
+ ...(context.accumulator.omittedObservations() === 0
68
+ ? []
69
+ : [
70
+ "Repeated content identities exceeded the per-node observation bound; containment edges still preserve inventoried paths.",
71
+ ]),
72
+ ...(coverage === "complete"
73
+ ? []
74
+ : [
75
+ "Graph coverage is incomplete; unavailable facts remain explicit and must not be read as absence.",
76
+ ]),
77
+ ];
78
+ const reconstructionLimits = (input) => [
79
+ {
80
+ name: "max-entries",
81
+ value: input.limits.max_entries,
82
+ unit: "items",
83
+ },
84
+ {
85
+ name: "max-total-artifact-bytes",
86
+ value: input.limits.max_total_artifact_bytes,
87
+ unit: "bytes",
88
+ },
89
+ {
90
+ name: "max-text-files",
91
+ value: input.limits.max_text_files,
92
+ unit: "items",
93
+ },
94
+ {
95
+ name: "max-total-text-bytes",
96
+ value: input.limits.max_total_text_bytes,
97
+ unit: "bytes",
98
+ },
99
+ ...javaScriptAnalysisLimits(input),
100
+ { name: "max-observations-per-node", value: 64, unit: "items" },
101
+ ];