rea-agents 1.3.0 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. package/README.md +55 -12
  2. package/bridge/hopper_bridge.py +12 -3
  3. package/dist/application/AnalysisSnapshotCache.js +8 -6
  4. package/dist/application/ArtifactGraphConstruction.js +12 -3
  5. package/dist/application/ArtifactInventory.js +100 -27
  6. package/dist/application/AuthorizedArtifactInventory.js +4 -3
  7. package/dist/application/BinarySession.js +92 -4
  8. package/dist/application/BoundedJsonFiles.js +6 -29
  9. package/dist/application/BrowserEvidence.js +35 -0
  10. package/dist/application/BrowserObservationPort.js +1 -0
  11. package/dist/application/BrowserObservationService.js +52 -0
  12. package/dist/application/CapabilityInventory.js +134 -0
  13. package/dist/application/ClientRegistrationStatus.js +69 -0
  14. package/dist/application/ConfiguredRoots.js +19 -0
  15. package/dist/application/CrossVersionInventory.js +19 -8
  16. package/dist/application/CrossVersionInvestigation.js +74 -5
  17. package/dist/application/DeferredFileAuthorization.js +61 -0
  18. package/dist/application/DirectAnalysis.js +117 -9
  19. package/dist/application/Doctor.js +74 -2
  20. package/dist/application/EvidenceLedger.js +6 -5
  21. package/dist/application/InvestigationWorkspaceStore.js +2 -26
  22. package/dist/application/PermissionAuthority.js +183 -0
  23. package/dist/application/PermissionConfiguration.js +26 -0
  24. package/dist/application/ProcessCaptureAuthority.js +3 -2
  25. package/dist/application/ProcessCaptureError.js +13 -0
  26. package/dist/application/ProcessCaptureLifecycle.js +21 -9
  27. package/dist/application/ProcessCheckpoints.js +1 -1
  28. package/dist/application/ProcessCli.js +40 -15
  29. package/dist/application/ProcessHarness.js +10 -20
  30. package/dist/application/ProcessNormalization.js +17 -2
  31. package/dist/application/ProcessOwnership.js +4 -3
  32. package/dist/application/ProcessSampling.js +52 -17
  33. package/dist/application/ProgressReporter.js +30 -0
  34. package/dist/application/ProjectPermissionStore.js +213 -0
  35. package/dist/application/ReferenceSourceImportPolicy.js +3 -2
  36. package/dist/application/Setup.js +2 -41
  37. package/dist/application/SupportedClients.js +41 -0
  38. package/dist/application/TerminalRenderer.js +1 -1
  39. package/dist/application/Uninstall.js +1 -1
  40. package/dist/application/Upgrade.js +2 -0
  41. package/dist/application/runtime.js +1 -1
  42. package/dist/artifacts/ArtifactProvider.js +13 -3
  43. package/dist/artifacts/MachOSliceArtifactReader.js +1 -1
  44. package/dist/artifacts/NativeDmgArtifactReader.js +1 -1
  45. package/dist/artifacts/SafeOutputTree.js +3 -3
  46. package/dist/browser/CdpBrowserProvider.js +158 -0
  47. package/dist/browser/CdpCaptureDocuments.js +140 -0
  48. package/dist/browser/CdpCaptureEvents.js +248 -0
  49. package/dist/browser/CdpCaptureStorage.js +53 -0
  50. package/dist/browser/CdpCaptureValues.js +70 -0
  51. package/dist/browser/CdpConnection.js +218 -0
  52. package/dist/browser/CdpEndpoint.js +114 -0
  53. package/dist/browser/CdpOptionalCommand.js +15 -0
  54. package/dist/browser/CdpPageCapture.js +273 -0
  55. package/dist/catalogIdentity.js +123 -0
  56. package/dist/cli.js +48 -3
  57. package/dist/cliBrowserCommands.js +136 -0
  58. package/dist/cliEvidenceCommands.js +42 -1
  59. package/dist/cliInvestigationCommands.js +42 -6
  60. package/dist/cliLogging.js +20 -3
  61. package/dist/cliPolicyCommands.js +118 -0
  62. package/dist/cliProcessCommands.js +4 -14
  63. package/dist/config.js +107 -3
  64. package/dist/contracts/artifactToolContracts.js +14 -1
  65. package/dist/contracts/browserToolContracts.js +80 -0
  66. package/dist/contracts/errorSchemas.js +100 -0
  67. package/dist/contracts/toolContracts.js +13 -6
  68. package/dist/contracts/toolOutputSchemas.js +62 -30
  69. package/dist/domain/artifactComparison.js +38 -11
  70. package/dist/domain/artifactGraph.js +25 -1
  71. package/dist/domain/artifactInventoryEvidence.js +18 -0
  72. package/dist/domain/browserObservation.js +361 -0
  73. package/dist/domain/changedBehavior.js +9 -18
  74. package/dist/domain/errors.js +257 -3
  75. package/dist/domain/investigationWorkspace.js +131 -4
  76. package/dist/domain/jsonLimits.js +26 -0
  77. package/dist/domain/permissionPolicy.js +161 -0
  78. package/dist/domain/processCapture.js +3 -1
  79. package/dist/domain/processComparison.js +23 -34
  80. package/dist/domain/reconstructionVerification.js +3 -16
  81. package/dist/domain/reconstructionVerificationSchemas.js +3 -0
  82. package/dist/domain/staticRuntimeCorrelation.js +7 -1
  83. package/dist/domain/symbolAnalysis.js +1 -1
  84. package/dist/generatedPackageMetadata.js +9 -0
  85. package/dist/hopper/BridgeLauncher.js +7 -3
  86. package/dist/hopper/HopperClient.js +32 -5
  87. package/dist/identity.js +10 -3
  88. package/dist/logger.js +2 -2
  89. package/dist/main.js +101 -7
  90. package/dist/native/CommandRunner.js +11 -4
  91. package/dist/server/createServer.js +59 -5
  92. package/dist/server/mcpProgress.js +23 -0
  93. package/dist/server/registerArtifactComparisonTool.js +6 -2
  94. package/dist/server/registerBrowserTools.js +36 -0
  95. package/dist/server/registerBundleComparisonTool.js +6 -2
  96. package/dist/server/registerEnhancedTools.js +18 -1
  97. package/dist/server/registerEvidenceResources.js +302 -0
  98. package/dist/server/registerEvidenceTools.js +58 -1
  99. package/dist/server/registerFunctionComparisonTool.js +6 -2
  100. package/dist/server/registerInvestigationTools.js +78 -10
  101. package/dist/server/registerOfficialTools.js +19 -1
  102. package/dist/server/registerProcessComparisonTool.js +9 -3
  103. package/dist/server/registerSessionStatusTool.js +44 -3
  104. package/dist/server/registerSessionTools.js +144 -9
  105. package/dist/server/runDerivedOperation.js +43 -0
  106. package/dist/server/toolResult.js +27 -3
  107. package/dist/serverIdentity.js +70 -0
  108. package/package.json +7 -4
  109. package/scripts/rea.mjs +1 -1
  110. package/skills/rea-analysis/SKILL.md +18 -3
@@ -0,0 +1,218 @@
1
+ import WebSocket, {} from "ws";
2
+ import { AnalysisCancelledError, AnalysisError, AnalysisTimeoutError, BrowserObservationError, } from "../domain/errors.js";
3
+ const CONNECT_TIMEOUT_MS = 5_000;
4
+ const COMMAND_TIMEOUT_MS = 5_000;
5
+ const MAX_PAYLOAD_BYTES = 16 * 1_024 * 1_024;
6
+ const MAX_PENDING_COMMANDS = 128;
7
+ /** Correlated, bounded JSON-RPC transport for a browser-level CDP socket. */
8
+ export class CdpConnection {
9
+ socket;
10
+ #pending = new Map();
11
+ #listeners = new Set();
12
+ #nextId = 1;
13
+ #closed = false;
14
+ #protocolFailed = false;
15
+ constructor(socket) {
16
+ this.socket = socket;
17
+ socket.on("message", (data) => this.#receive(data));
18
+ socket.on("close", () => this.#failPending("disconnected"));
19
+ socket.on("error", () => this.#failPending("disconnected"));
20
+ }
21
+ /** Connect to one already-validated loopback browser WebSocket. */
22
+ static async connect(url, signal) {
23
+ if (signal?.aborted === true)
24
+ throw new AnalysisCancelledError("inspect_web_page");
25
+ const socket = new WebSocket(url, {
26
+ handshakeTimeout: CONNECT_TIMEOUT_MS,
27
+ maxPayload: MAX_PAYLOAD_BYTES,
28
+ perMessageDeflate: false,
29
+ });
30
+ await waitForOpen(socket, signal);
31
+ return new CdpConnection(socket);
32
+ }
33
+ /** Subscribe to validated CDP event envelopes. */
34
+ onEvent(listener) {
35
+ this.#listeners.add(listener);
36
+ return () => this.#listeners.delete(listener);
37
+ }
38
+ /** Execute one bounded command, optionally within a flat target session. */
39
+ async send(method, params = {}, sessionId, signal) {
40
+ if (this.#protocolFailed)
41
+ throw new BrowserObservationError("inspect_web_page", "protocol_error");
42
+ if (this.#closed || this.socket.readyState !== WebSocket.OPEN)
43
+ throw new BrowserObservationError("inspect_web_page", "disconnected");
44
+ if (this.#pending.size >= MAX_PENDING_COMMANDS)
45
+ throw new BrowserObservationError("inspect_web_page", "payload_limit");
46
+ if (signal?.aborted === true)
47
+ throw new AnalysisCancelledError("inspect_web_page");
48
+ const id = this.#nextId;
49
+ this.#nextId += 1;
50
+ return await new Promise((resolve, reject) => {
51
+ const onAbort = () => {
52
+ const pending = this.#pending.get(id);
53
+ if (pending === undefined)
54
+ return;
55
+ clearTimeout(pending.timer);
56
+ pending.removeAbort();
57
+ this.#pending.delete(id);
58
+ reject(new AnalysisCancelledError("inspect_web_page"));
59
+ };
60
+ signal?.addEventListener("abort", onAbort, { once: true });
61
+ const timer = setTimeout(() => {
62
+ signal?.removeEventListener("abort", onAbort);
63
+ this.#pending.delete(id);
64
+ reject(new AnalysisTimeoutError("inspect_web_page", COMMAND_TIMEOUT_MS));
65
+ }, COMMAND_TIMEOUT_MS);
66
+ this.#pending.set(id, {
67
+ resolve,
68
+ reject,
69
+ timer,
70
+ removeAbort: () => signal?.removeEventListener("abort", onAbort),
71
+ });
72
+ this.socket.send(JSON.stringify({
73
+ id,
74
+ method,
75
+ params,
76
+ ...(sessionId ? { sessionId } : {}),
77
+ }), (error) => {
78
+ if (error === undefined || error === null)
79
+ return;
80
+ const pending = this.#pending.get(id);
81
+ if (pending === undefined)
82
+ return;
83
+ this.#complete(id, pending);
84
+ reject(new BrowserObservationError("inspect_web_page", "disconnected", {
85
+ cause: error,
86
+ }));
87
+ });
88
+ });
89
+ }
90
+ /** Close only REA's socket; never close the browser or selected page. */
91
+ async close() {
92
+ if (this.#closed)
93
+ return;
94
+ this.#closed = true;
95
+ this.#failPending("disconnected");
96
+ if (this.socket.readyState === WebSocket.CLOSED)
97
+ return;
98
+ await new Promise((resolve) => {
99
+ const timer = setTimeout(() => {
100
+ this.socket.terminate();
101
+ resolve();
102
+ }, 1_000);
103
+ this.socket.once("close", () => {
104
+ clearTimeout(timer);
105
+ resolve();
106
+ });
107
+ this.socket.close();
108
+ });
109
+ }
110
+ #receive(data) {
111
+ let message;
112
+ try {
113
+ message = JSON.parse(rawText(data));
114
+ }
115
+ catch {
116
+ this.#failPending("protocol_error");
117
+ return;
118
+ }
119
+ if (!isRecord(message)) {
120
+ this.#failPending("protocol_error");
121
+ return;
122
+ }
123
+ if ("id" in message) {
124
+ if (typeof message.id !== "number" ||
125
+ !Number.isSafeInteger(message.id) ||
126
+ message.id < 1) {
127
+ this.#failPending("protocol_error");
128
+ return;
129
+ }
130
+ this.#receiveResponse(message, message.id);
131
+ return;
132
+ }
133
+ if (typeof message.method !== "string" ||
134
+ message.method.length === 0 ||
135
+ ("params" in message && !isRecord(message.params)) ||
136
+ ("sessionId" in message && typeof message.sessionId !== "string")) {
137
+ this.#failPending("protocol_error");
138
+ return;
139
+ }
140
+ const event = {
141
+ method: message.method,
142
+ params: "params" in message ? message.params : {},
143
+ ...(typeof message.sessionId === "string"
144
+ ? { sessionId: message.sessionId }
145
+ : {}),
146
+ };
147
+ for (const listener of this.#listeners)
148
+ listener(event);
149
+ }
150
+ #receiveResponse(message, id) {
151
+ const pending = this.#pending.get(id);
152
+ if (pending === undefined)
153
+ return;
154
+ this.#complete(id, pending);
155
+ if ("error" in message) {
156
+ pending.reject(new BrowserObservationError("inspect_web_page", "protocol_error"));
157
+ return;
158
+ }
159
+ pending.resolve("result" in message ? message.result : {});
160
+ }
161
+ #complete(id, pending) {
162
+ clearTimeout(pending.timer);
163
+ pending.removeAbort();
164
+ this.#pending.delete(id);
165
+ }
166
+ #failPending(reason) {
167
+ if (reason === "disconnected")
168
+ this.#closed = true;
169
+ else
170
+ this.#protocolFailed = true;
171
+ for (const [id, pending] of this.#pending) {
172
+ this.#complete(id, pending);
173
+ pending.reject(new BrowserObservationError("inspect_web_page", reason));
174
+ }
175
+ }
176
+ }
177
+ const rawText = (data) => Array.isArray(data)
178
+ ? Buffer.concat(data).toString("utf8")
179
+ : Buffer.isBuffer(data)
180
+ ? data.toString("utf8")
181
+ : Buffer.from(data).toString("utf8");
182
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
183
+ const waitForOpen = async (socket, signal) => await new Promise((resolve, reject) => {
184
+ const timer = setTimeout(() => {
185
+ cleanup();
186
+ terminateSocket(socket);
187
+ reject(new AnalysisTimeoutError("inspect_web_page", CONNECT_TIMEOUT_MS));
188
+ }, CONNECT_TIMEOUT_MS);
189
+ const onOpen = () => {
190
+ cleanup();
191
+ resolve();
192
+ };
193
+ const onFailure = (cause) => {
194
+ cleanup();
195
+ reject(signal?.aborted === true
196
+ ? new AnalysisCancelledError("inspect_web_page")
197
+ : new BrowserObservationError("inspect_web_page", "endpoint_unreachable", { cause }));
198
+ };
199
+ const onAbort = () => {
200
+ terminateSocket(socket);
201
+ onFailure(new AnalysisCancelledError("inspect_web_page"));
202
+ };
203
+ const cleanup = () => {
204
+ clearTimeout(timer);
205
+ socket.off("open", onOpen);
206
+ socket.off("error", onFailure);
207
+ signal?.removeEventListener("abort", onAbort);
208
+ };
209
+ socket.once("open", onOpen);
210
+ socket.once("error", onFailure);
211
+ signal?.addEventListener("abort", onAbort, { once: true });
212
+ if (signal?.aborted === true)
213
+ onAbort();
214
+ });
215
+ const terminateSocket = (socket) => {
216
+ socket.once("error", () => undefined);
217
+ socket.terminate();
218
+ };
@@ -0,0 +1,114 @@
1
+ import { request } from "node:http";
2
+ import { z } from "zod";
3
+ import { AnalysisCancelledError, AnalysisError, AnalysisTimeoutError, BrowserObservationError, } from "../domain/errors.js";
4
+ const HTTP_TIMEOUT_MS = 5_000;
5
+ const MAX_VERSION_BYTES = 64 * 1_024;
6
+ const MAX_TARGET_LIST_BYTES = 2 * 1_024 * 1_024;
7
+ const endpointVersionSchema = z.object({
8
+ Browser: z.string().min(1).max(1_024),
9
+ "Protocol-Version": z.string().min(1).max(100),
10
+ "User-Agent": z.string().max(16_384),
11
+ "V8-Version": z.string().max(1_024),
12
+ "WebKit-Version": z.string().max(1_024),
13
+ webSocketDebuggerUrl: z.string().min(1).max(2_048),
14
+ });
15
+ const endpointTargetSchema = z.object({
16
+ id: z.string().min(1).max(256),
17
+ type: z.string().min(1).max(100),
18
+ title: z.string().max(16_384).default(""),
19
+ url: z.string().max(65_536),
20
+ attached: z.boolean().default(false),
21
+ });
22
+ const endpointTargetsSchema = z.array(endpointTargetSchema).max(1_000);
23
+ /** Read bounded CDP discovery endpoints without following redirects. */
24
+ export const discoverCdpEndpoint = async (endpoint, operation, signal) => {
25
+ const versionInput = await readJson(new URL("/json/version", endpoint), MAX_VERSION_BYTES, operation, signal);
26
+ const targetsInput = await readJson(new URL("/json/list", endpoint), MAX_TARGET_LIST_BYTES, operation, signal);
27
+ const version = parseEndpointValue(endpointVersionSchema, versionInput, operation);
28
+ const targets = parseEndpointValue(endpointTargetsSchema, targetsInput, operation);
29
+ return {
30
+ browserWebSocketUrl: safeBrowserWebSocketUrl(endpoint, version.webSocketDebuggerUrl, operation),
31
+ version: {
32
+ product: version.Browser,
33
+ protocol_version: version["Protocol-Version"],
34
+ revision: version["WebKit-Version"],
35
+ user_agent: version["User-Agent"],
36
+ js_version: version["V8-Version"],
37
+ },
38
+ targets,
39
+ };
40
+ };
41
+ const parseEndpointValue = (schema, input, operation) => {
42
+ const parsed = schema.safeParse(input);
43
+ if (!parsed.success)
44
+ throw new BrowserObservationError(operation, "invalid_endpoint_response", {
45
+ cause: parsed.error,
46
+ });
47
+ return parsed.data;
48
+ };
49
+ const safeBrowserWebSocketUrl = (endpoint, reported, operation) => {
50
+ let parsed;
51
+ try {
52
+ parsed = new URL(reported);
53
+ }
54
+ catch (cause) {
55
+ throw new BrowserObservationError(operation, "invalid_endpoint_response", {
56
+ cause,
57
+ });
58
+ }
59
+ const trustedEndpoint = new URL(endpoint);
60
+ if (parsed.protocol !== "ws:" ||
61
+ parsed.port !== trustedEndpoint.port ||
62
+ parsed.username !== "" ||
63
+ parsed.password !== "" ||
64
+ parsed.search !== "" ||
65
+ parsed.hash !== "" ||
66
+ !parsed.pathname.startsWith("/devtools/browser/"))
67
+ throw new BrowserObservationError(operation, "invalid_endpoint_response");
68
+ parsed.hostname = trustedEndpoint.hostname;
69
+ return parsed.href;
70
+ };
71
+ const readJson = async (url, maximumBytes, operation, signal) => await new Promise((resolve, reject) => {
72
+ if (signal?.aborted === true) {
73
+ reject(new AnalysisCancelledError(operation));
74
+ return;
75
+ }
76
+ const request_ = request(url, { method: "GET", signal, headers: { Accept: "application/json" } }, (response) => {
77
+ if (response.statusCode !== 200) {
78
+ response.resume();
79
+ reject(new BrowserObservationError(operation, "invalid_endpoint_response"));
80
+ return;
81
+ }
82
+ const chunks = [];
83
+ let bytes = 0;
84
+ response.on("data", (chunk) => {
85
+ bytes += chunk.length;
86
+ if (bytes > maximumBytes) {
87
+ response.destroy(new BrowserObservationError(operation, "payload_limit"));
88
+ return;
89
+ }
90
+ chunks.push(chunk);
91
+ });
92
+ response.on("end", () => {
93
+ try {
94
+ resolve(JSON.parse(Buffer.concat(chunks).toString("utf8")));
95
+ }
96
+ catch (cause) {
97
+ reject(new BrowserObservationError(operation, "invalid_endpoint_response", { cause }));
98
+ }
99
+ });
100
+ response.on("error", (cause) => reject(endpointFailure(cause, operation, signal)));
101
+ });
102
+ request_.setTimeout(HTTP_TIMEOUT_MS, () => request_.destroy(new AnalysisTimeoutError(operation, HTTP_TIMEOUT_MS)));
103
+ request_.on("error", (cause) => reject(endpointFailure(cause, operation, signal)));
104
+ request_.end();
105
+ });
106
+ const endpointFailure = (cause, operation, signal) => {
107
+ if (cause instanceof AnalysisError)
108
+ return cause;
109
+ if (signal?.aborted === true)
110
+ return new AnalysisCancelledError(operation);
111
+ return new BrowserObservationError(operation, "endpoint_unreachable", {
112
+ cause,
113
+ });
114
+ };
@@ -0,0 +1,15 @@
1
+ import { BrowserObservationError } from "../domain/errors.js";
2
+ import { CdpConnection } from "./CdpConnection.js";
3
+ /** Execute an optional CDP method while preserving transport and cancellation failures. */
4
+ export const optionalCdpCommand = async (context, method, parameters, limitations) => {
5
+ try {
6
+ return await context.connection.send(method, parameters, context.sessionId, context.signal);
7
+ }
8
+ catch (cause) {
9
+ if (!(cause instanceof BrowserObservationError) ||
10
+ cause.reason !== "protocol_error")
11
+ throw cause;
12
+ limitations.push(`${method} was unavailable from this browser target.`);
13
+ return undefined;
14
+ }
15
+ };
@@ -0,0 +1,273 @@
1
+ import { BrowserObservationError } from "../domain/errors.js";
2
+ import { CdpConnection } from "./CdpConnection.js";
3
+ import { CdpCaptureEvents } from "./CdpCaptureEvents.js";
4
+ import { captureStorage } from "./CdpCaptureStorage.js";
5
+ import { optionalCdpCommand } from "./CdpOptionalCommand.js";
6
+ import { captureAccessibility, captureDom, captureFrames, captureResources, mainFrameUrl, } from "./CdpCaptureDocuments.js";
7
+ import { allowedSanitizedUrl, delayWithCancellation, isHttpUrl, recordValue, recordsValue, requiredRecord, sourceExcluded, sourceResult, stringValue, } from "./CdpCaptureValues.js";
8
+ /** Capture and normalize one attached page without evaluating page JavaScript. */
9
+ export const capturePage = async (context) => {
10
+ const allowedOrigins = new Set(context.input.allowed_origins);
11
+ const events = new CdpCaptureEvents(context.input, allowedOrigins);
12
+ const removeListener = context.connection.onEvent((event) => {
13
+ if (event.sessionId !== context.sessionId)
14
+ return;
15
+ events.ingest(event);
16
+ });
17
+ const state = {
18
+ context,
19
+ events,
20
+ allowedOrigins,
21
+ limitations: [
22
+ "Observation starts when REA attaches; prior network and console activity is unavailable.",
23
+ "Network headers, request bodies, response bodies, cookies, storage values, console values, and WebSocket payloads are not retained.",
24
+ "Source maps are reported only as declarative URLs and are not fetched.",
25
+ "URL-less scripts and console events without an allowed source URL are excluded because their origin cannot be proven.",
26
+ ],
27
+ truncatedSections: new Set(),
28
+ startedAt: new Date().toISOString(),
29
+ };
30
+ try {
31
+ return await captureAuthorizedPage(state);
32
+ }
33
+ finally {
34
+ removeListener();
35
+ }
36
+ };
37
+ const captureAuthorizedPage = async (state) => {
38
+ const { context, allowedOrigins, limitations, truncatedSections } = state;
39
+ const { connection, sessionId, input, signal } = context;
40
+ await authorizeObservationWindow(state);
41
+ const frameResult = await authorizedFrameTree(context, allowedOrigins);
42
+ const attachedUrl = mainFrameUrl(frameResult) ?? "";
43
+ const frameCapture = captureFrames(frameResult, allowedOrigins, input.limits.max_frames);
44
+ const frames = frameCapture.items;
45
+ if (frameCapture.total > frames.length)
46
+ truncatedSections.add("frames");
47
+ const captureFrame = frames[0];
48
+ if (captureFrame === undefined)
49
+ throw new BrowserObservationError("inspect_web_page", "target_not_allowed");
50
+ state.events.beginFinalCapture(captureFrame.frame_id);
51
+ const resourceCapture = captureResources(await connection.send("Page.getResourceTree", {}, sessionId, signal), allowedOrigins, input.limits.max_resources);
52
+ if (resourceCapture.total > resourceCapture.items.length)
53
+ truncatedSections.add("resources");
54
+ const dom = await capturePageDom(state);
55
+ if (dom.total > dom.nodes.length)
56
+ truncatedSections.add("dom");
57
+ const accessibility = await accessibilityForFrames(context, frames, limitations);
58
+ if (accessibility.total > accessibility.nodes.length)
59
+ truncatedSections.add("accessibility");
60
+ const scripts = await captureScripts(context, state.events, truncatedSections);
61
+ const workerCapture = await captureWorkers(context, allowedOrigins, new Set(frames.map((frame) => frame.frame_id)), limitations);
62
+ if (workerCapture.total > workerCapture.items.length)
63
+ truncatedSections.add("workers");
64
+ const storageCapture = await captureStorage(context, frames[0]?.origin ?? new URL(attachedUrl).origin, limitations);
65
+ if (storageCapture.truncated)
66
+ truncatedSections.add("storage_keys");
67
+ if (state.events.dropped > 0)
68
+ truncatedSections.add("events");
69
+ const completedFrameResult = await authorizedFrameTree(context, allowedOrigins);
70
+ const completedUrl = mainFrameUrl(completedFrameResult) ?? "";
71
+ if (state.events.originViolation)
72
+ throw new BrowserObservationError("inspect_web_page", "target_not_allowed");
73
+ if (state.events.navigationDuringCapture || completedUrl !== attachedUrl)
74
+ throw new BrowserObservationError("inspect_web_page", "target_changed");
75
+ await report(context.progress, 3, "Normalizing browser evidence");
76
+ return normalizedInspection(state, {
77
+ attachedUrl,
78
+ frames,
79
+ dom,
80
+ accessibility,
81
+ scripts,
82
+ resources: resourceCapture.items,
83
+ workers: workerCapture.items,
84
+ storage: storageCapture.value,
85
+ });
86
+ };
87
+ const authorizeObservationWindow = async (state) => {
88
+ const { context, allowedOrigins, events } = state;
89
+ const { connection, sessionId, signal } = context;
90
+ await report(context.progress, 1, "Enabling passive CDP domains");
91
+ await connection.send("Page.enable", {}, sessionId, signal);
92
+ const initialFrameResult = await authorizedFrameTree(context, allowedOrigins);
93
+ const mainFrame = captureFrames(initialFrameResult, allowedOrigins, 1)
94
+ .items[0];
95
+ if (mainFrame === undefined)
96
+ throw new BrowserObservationError("inspect_web_page", "target_not_allowed");
97
+ events.beginAuthorizedFrame(mainFrame.frame_id);
98
+ await enableObservationDomains(connection, sessionId, signal);
99
+ await report(context.progress, 2, "Observing page events");
100
+ await delayWithCancellation(context.input.observation_ms, signal);
101
+ if (events.originViolation)
102
+ throw new BrowserObservationError("inspect_web_page", "target_not_allowed");
103
+ };
104
+ const capturePageDom = async (state) => {
105
+ const { context, allowedOrigins } = state;
106
+ const result = await context.connection.send("DOMSnapshot.captureSnapshot", {
107
+ computedStyles: [],
108
+ includePaintOrder: false,
109
+ includeDOMRects: false,
110
+ }, context.sessionId, context.signal);
111
+ return captureDom(result, allowedOrigins, context.input);
112
+ };
113
+ const normalizedInspection = (state, captured) => ({
114
+ schema_version: 1,
115
+ browser: state.context.discovery.version,
116
+ target: normalizedTarget(state.context.target, captured.attachedUrl, state.allowedOrigins),
117
+ capture_window: {
118
+ started_at: state.startedAt,
119
+ ended_at: new Date().toISOString(),
120
+ observation_ms: state.context.input.observation_ms,
121
+ },
122
+ completeness: {
123
+ status: state.truncatedSections.size === 0 ? "complete" : "truncated",
124
+ truncated_sections: [...state.truncatedSections].sort(),
125
+ dropped_events: state.events.dropped,
126
+ },
127
+ frames: [...captured.frames],
128
+ dom: { total_nodes: captured.dom.total, nodes: [...captured.dom.nodes] },
129
+ accessibility: {
130
+ total_nodes: captured.accessibility.total,
131
+ nodes: [...captured.accessibility.nodes],
132
+ },
133
+ scripts: captured.scripts,
134
+ resources: [...captured.resources],
135
+ network: {
136
+ requests: [...state.events.network.values()],
137
+ websocket_events: [...state.events.websockets],
138
+ coverage_started_at: state.startedAt,
139
+ prior_activity_available: false,
140
+ },
141
+ console: {
142
+ events: [...state.events.console],
143
+ coverage_started_at: state.startedAt,
144
+ prior_activity_available: false,
145
+ },
146
+ workers: [...captured.workers],
147
+ storage: captured.storage,
148
+ limitations: state.limitations,
149
+ });
150
+ const authorizedFrameTree = async (context, allowedOrigins) => {
151
+ for (let attempt = 0; attempt < 80; attempt += 1) {
152
+ const result = await context.connection.send("Page.getFrameTree", {}, context.sessionId, context.signal);
153
+ const currentUrl = mainFrameUrl(result);
154
+ if (allowedSanitizedUrl(currentUrl, allowedOrigins) !== undefined)
155
+ return result;
156
+ if (isHttpUrl(currentUrl))
157
+ throw new BrowserObservationError("inspect_web_page", "target_not_allowed");
158
+ await delayWithCancellation(25, context.signal);
159
+ }
160
+ throw new BrowserObservationError("inspect_web_page", "target_not_allowed");
161
+ };
162
+ const enableObservationDomains = async (connection, sessionId, signal) => {
163
+ await connection.send("Runtime.enable", {}, sessionId, signal);
164
+ await connection.send("Debugger.enable", { maxScriptsCacheSize: 4 * 1_024 * 1_024 }, sessionId, signal);
165
+ await connection.send("Network.enable", {
166
+ maxTotalBufferSize: 4 * 1_024 * 1_024,
167
+ maxResourceBufferSize: 1_024 * 1_024,
168
+ }, sessionId, signal);
169
+ };
170
+ const accessibilityForFrames = async (context, frames, limitations) => {
171
+ const nodes = [];
172
+ let total = 0;
173
+ for (const frame of frames) {
174
+ const result = await optionalCdpCommand(context, "Accessibility.getFullAXTree", { depth: 32, frameId: frame.frame_id }, limitations);
175
+ if (result === undefined)
176
+ continue;
177
+ const capture = captureAccessibility([result], Math.max(0, context.input.limits.max_ax_nodes - nodes.length));
178
+ total += capture.total;
179
+ nodes.push(...capture.nodes);
180
+ }
181
+ return { total, nodes };
182
+ };
183
+ const captureScripts = async (context, events, truncatedSections) => {
184
+ let totalSourceBytes = 0;
185
+ const items = [];
186
+ for (const script of events.scripts.values()) {
187
+ let source = sourceExcluded("source capture was not approved");
188
+ if (context.input.include_script_sources) {
189
+ if (script.length > context.input.limits.max_script_source_bytes) {
190
+ source = sourceExcluded("declared script length exceeds per-script limit");
191
+ truncatedSections.add("script_sources");
192
+ }
193
+ else if (totalSourceBytes + script.length >
194
+ context.input.limits.max_total_script_source_bytes) {
195
+ source = sourceExcluded("total script source limit reached");
196
+ truncatedSections.add("script_sources");
197
+ }
198
+ else {
199
+ const result = requiredRecord(await context.connection.send("Debugger.getScriptSource", { scriptId: script.scriptId }, context.sessionId, context.signal));
200
+ const content = stringValue(result.scriptSource) ?? "";
201
+ const bytes = Buffer.byteLength(content);
202
+ if (bytes > context.input.limits.max_script_source_bytes ||
203
+ totalSourceBytes + bytes >
204
+ context.input.limits.max_total_script_source_bytes) {
205
+ source = sourceExcluded("actual source exceeds configured byte limits");
206
+ truncatedSections.add("script_sources");
207
+ }
208
+ else {
209
+ source = sourceResult(content);
210
+ totalSourceBytes += bytes;
211
+ }
212
+ }
213
+ }
214
+ items.push({
215
+ script_id: script.scriptId,
216
+ url: script.url,
217
+ origin: script.origin,
218
+ cdp_hash: script.hash,
219
+ length: script.length,
220
+ is_module: script.isModule,
221
+ language: script.language,
222
+ source_map_url: script.sourceMapUrl,
223
+ source,
224
+ });
225
+ }
226
+ return { total: events.scripts.size, items };
227
+ };
228
+ const captureWorkers = async (context, allowedOrigins, frameIds, limitations) => {
229
+ const result = await optionalCdpCommand({ ...context, sessionId: undefined }, "Target.getTargets", {}, limitations);
230
+ const items = [];
231
+ let total = 0;
232
+ for (const target of recordsValue(recordValue(result)?.targetInfos)) {
233
+ const type = stringValue(target.type) ?? "";
234
+ const url = allowedSanitizedUrl(target.url, allowedOrigins);
235
+ const relatedToPage = stringValue(target.openerId) === context.target.id ||
236
+ (stringValue(target.parentFrameId) !== undefined &&
237
+ frameIds.has(stringValue(target.parentFrameId) ?? ""));
238
+ if (!type.includes("worker") || url === undefined || !relatedToPage)
239
+ continue;
240
+ total += 1;
241
+ if (items.length >= context.input.limits.max_workers)
242
+ continue;
243
+ items.push({
244
+ target_id: (stringValue(target.targetId) ?? "").slice(0, 256),
245
+ type: type.slice(0, 100),
246
+ url: url.url,
247
+ origin: url.origin,
248
+ attached: target.attached === true,
249
+ });
250
+ }
251
+ return {
252
+ total,
253
+ items,
254
+ };
255
+ };
256
+ const normalizedTarget = (target, currentUrl, allowedOrigins) => {
257
+ const url = allowedSanitizedUrl(currentUrl, allowedOrigins);
258
+ return {
259
+ target_id: target.id,
260
+ type: target.type,
261
+ title: target.title.slice(0, 16_384),
262
+ url: url?.url ?? "[unsupported-url]",
263
+ origin: url?.origin ?? "",
264
+ attached: target.attached,
265
+ };
266
+ };
267
+ const report = async (progress, completed, message) => await progress?.report({
268
+ phase: "browser_observation",
269
+ completed,
270
+ total: 3,
271
+ message,
272
+ ...(completed === 3 ? { terminal: true } : {}),
273
+ });