@deeeed/metamask-harness 0.79.0 → 0.80.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 (210) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/README.md +1 -1
  3. package/adapters/extension/ensure-browser.sh +2 -2
  4. package/adapters/extension/inject.mjs +0 -1
  5. package/adapters/extension/launch-browser.cjs +39 -613
  6. package/adapters/extension/readiness.mjs +2 -5
  7. package/adapters/extension/reattach.sh +2 -2
  8. package/adapters/extension/stamp-runtime-title.cjs +1 -1
  9. package/adapters/manifest.json +2 -34
  10. package/adapters/mobile/bridge-runtime/cdp-bridge.cjs +57 -1358
  11. package/adapters/mobile/bridge-runtime/teaching.cjs +20 -0
  12. package/adapters/mobile/inject.sh +16 -0
  13. package/adapters/mobile/launch-console-forwarder.cjs +1 -2
  14. package/adapters/mobile/open-device.sh +2 -2
  15. package/adapters/mobile/reload-app.mjs +4 -4
  16. package/adapters/mobile/reset-app-data.sh +1 -1
  17. package/adapters/mobile/start-console-forwarder.sh +3 -2
  18. package/adapters/mobile/start-metro.sh +17 -12
  19. package/adapters/mobile/wait-for-bridge.cjs +2 -2
  20. package/adapters/shared/harness-source-fingerprint.mjs +13 -8
  21. package/adapters/shared/open-debug.mjs +2 -2
  22. package/adapters/shared/sync-wallet-fixture.sh +8 -5
  23. package/adapters/terminal/cleanup.mjs +2 -1
  24. package/adapters/terminal/inject.mjs +2 -1
  25. package/bin/mm-harness +9 -28
  26. package/dist/adapters/checkout-files.js +16 -0
  27. package/dist/adapters/console-capture.js +14 -1
  28. package/dist/adapters/core/actions.js +4 -3
  29. package/dist/adapters/core/overlay.js +1 -1
  30. package/dist/adapters/core/readiness.js +28 -1
  31. package/dist/adapters/core/run.js +2 -2
  32. package/dist/adapters/core/surface.js +16 -1
  33. package/dist/adapters/extension/browser-pid.js +56 -0
  34. package/dist/adapters/extension/console-capture.js +1 -1
  35. package/dist/adapters/extension/doctor-checks.js +1 -1
  36. package/dist/adapters/extension/ensure-ready.js +5 -4
  37. package/dist/adapters/extension/extension-id.js +1 -1
  38. package/dist/adapters/extension/feature-flags.js +1 -1
  39. package/dist/adapters/extension/fixtures.js +1 -2
  40. package/dist/adapters/extension/launch.js +4 -5
  41. package/dist/adapters/extension/launched-browser.js +1 -2
  42. package/dist/adapters/extension/network-observer.js +1 -1
  43. package/dist/adapters/extension/overlay.js +1 -1
  44. package/dist/adapters/extension/product-config.js +1 -1
  45. package/dist/adapters/extension/readiness.js +1 -1
  46. package/dist/adapters/extension/reload.js +1 -1
  47. package/dist/adapters/extension/run.js +24 -10
  48. package/dist/adapters/extension/runtime-decision.js +2 -1
  49. package/dist/adapters/extension/runtime.js +9 -5
  50. package/dist/adapters/extension/surface.js +15 -2
  51. package/dist/adapters/extension/verify.js +2 -2
  52. package/dist/adapters/harness-freshness.js +2 -1
  53. package/dist/adapters/mobile/android-gfxinfo-performance.js +1 -3
  54. package/dist/adapters/mobile/app-identity.js +10 -0
  55. package/dist/adapters/mobile/doctor-checks.js +1 -1
  56. package/dist/adapters/mobile/fixtures.js +1 -1
  57. package/dist/adapters/mobile/launch.js +27 -28
  58. package/dist/adapters/mobile/lifecycle.js +1 -1
  59. package/dist/adapters/mobile/metro-env.js +9 -59
  60. package/dist/adapters/mobile/performance-observer.js +1 -1
  61. package/dist/adapters/mobile/prepare-target.js +1 -1
  62. package/dist/adapters/mobile/prepare.js +3 -4
  63. package/dist/adapters/mobile/provision.js +1 -1
  64. package/dist/adapters/mobile/readiness.js +1 -1
  65. package/dist/adapters/mobile/recording-target.js +15 -0
  66. package/dist/adapters/mobile/release-artifact-state.js +1 -1
  67. package/dist/adapters/mobile/reload.js +1 -1
  68. package/dist/adapters/mobile/run.js +8 -7
  69. package/dist/adapters/mobile/runtime-decision.js +2 -4
  70. package/dist/adapters/mobile/source-freshness.js +22 -126
  71. package/dist/adapters/mobile/surface.js +26 -3
  72. package/dist/adapters/mobile/ui.js +16 -5
  73. package/dist/adapters/mobile/verify.js +2 -3
  74. package/dist/adapters/performance/cdp-trace.js +1 -1
  75. package/dist/adapters/performance/device-platform.js +1 -1
  76. package/dist/adapters/slot-ports.js +1 -1
  77. package/dist/adapters/surface.js +1 -0
  78. package/dist/adapters/terminal/console-capture.js +1 -1
  79. package/dist/adapters/terminal/launch.js +1 -1
  80. package/dist/adapters/terminal/run.js +25 -2
  81. package/dist/adapters/terminal/surface.js +7 -1
  82. package/dist/adapters.js +8 -4
  83. package/dist/advisor-policy.js +1 -1
  84. package/dist/checkout-view.js +1 -2
  85. package/dist/cli.js +28 -15
  86. package/dist/command-contract.js +1 -26
  87. package/dist/commands/call.js +10 -24
  88. package/dist/commands/check.js +3 -4
  89. package/dist/commands/checklist.js +1 -1
  90. package/dist/commands/completion-candidates.js +2 -10
  91. package/dist/commands/config.js +2 -2
  92. package/dist/commands/core-readiness.js +12 -7
  93. package/dist/commands/debug.js +3 -3
  94. package/dist/commands/device-target.js +5 -5
  95. package/dist/commands/doctor.js +2 -14
  96. package/dist/commands/domain.js +2 -2
  97. package/dist/commands/ensure-ready.js +1 -7
  98. package/dist/commands/fixtures.js +5 -6
  99. package/dist/commands/help.js +3 -4
  100. package/dist/commands/launch/device.js +10 -17
  101. package/dist/commands/launch/extension.js +2 -8
  102. package/dist/commands/launch/mobile.js +1 -1
  103. package/dist/commands/list-executables.js +1 -3
  104. package/dist/commands/logs.js +3 -3
  105. package/dist/commands/manifest.js +1 -12
  106. package/dist/commands/mobile-device-view.js +4 -2
  107. package/dist/commands/overlay.js +110 -0
  108. package/dist/commands/pin.js +1 -1
  109. package/dist/commands/pr-body.js +2 -2
  110. package/dist/commands/prepare.js +4 -9
  111. package/dist/commands/provision.js +3 -12
  112. package/dist/commands/recipe-advice.js +1 -2
  113. package/dist/commands/recipe-quality.js +2 -3
  114. package/dist/commands/resolve-extension.js +1 -7
  115. package/dist/commands/review.js +4 -4
  116. package/dist/commands/run-engine.js +12 -51
  117. package/dist/commands/run.js +4 -25
  118. package/dist/commands/runtime-decision.js +1 -6
  119. package/dist/commands/runtime-health.js +1 -7
  120. package/dist/commands/runtime-launch.js +1 -13
  121. package/dist/commands/self-test.js +2 -6
  122. package/dist/commands/status-watch.js +1 -2
  123. package/dist/commands/status.js +2 -5
  124. package/dist/commands/task-init.js +2 -3
  125. package/dist/commands/tutorial.js +2 -2
  126. package/dist/commands/update.js +1 -1
  127. package/dist/completions-cache.js +1 -1
  128. package/dist/console-classifier.js +19 -0
  129. package/dist/doctor.js +2 -3
  130. package/dist/funding-execution-context.js +15 -1
  131. package/dist/host.js +32 -0
  132. package/dist/index.js +1 -6
  133. package/dist/live-tsx.js +49 -0
  134. package/dist/mm-harness-cli.js +2 -4
  135. package/dist/network-observation.js +4 -4
  136. package/dist/paths.js +34 -64
  137. package/dist/performance-observation.js +2 -2
  138. package/dist/progress.js +1 -1
  139. package/dist/recipe-assistance-context.js +1 -3
  140. package/dist/recipe-security.js +2 -59
  141. package/dist/review/knowledge.js +2 -2
  142. package/dist/runner.js +5 -3
  143. package/dist/runtime-context.js +1 -1
  144. package/dist/task-view.js +2 -3
  145. package/docs/CONTRIBUTING.md +1 -1
  146. package/docs/cli-contract.md +2 -2
  147. package/docs/feature-guard.md +90 -0
  148. package/library/README.md +3 -1
  149. package/library/actions/core/perps/_controller.mjs +153 -130
  150. package/library/actions/core/perps/quote_fees.mjs +58 -0
  151. package/library/actions/core/wallet/_fixture-accounts.mjs +69 -0
  152. package/library/actions/core/wallet/list_accounts.mjs +14 -57
  153. package/library/actions/extension/feature_flags/state.mjs +1 -1
  154. package/library/actions/extension/platform/cdp.mjs +2 -8
  155. package/library/actions/harness-exports.mjs +1 -6
  156. package/library/actions/mobile/app/network-control.mjs +6 -8
  157. package/library/actions/mobile/perps/prepare_local_snapshot_endpoint.mjs +1 -1
  158. package/library/actions/mobile/platform/bridge.mjs +8 -8
  159. package/library/actions/mobile/platform/native-session-name.mjs +21 -1
  160. package/library/actions/mobile/platform/native-session.mjs +3 -4
  161. package/library/actions/mobile/platform/observe-ui.mjs +12 -6
  162. package/library/actions/mobile/ui/native-navigation.mjs +4 -3
  163. package/library/actions/mobile/wallet/native-ui.mjs +3 -2
  164. package/library/actions/shared/analytics/_adapter.mjs +1 -1
  165. package/library/actions/shared/console-findings.mjs +1 -1
  166. package/library/actions/terminal/platform/runtime.mjs +2 -1
  167. package/library/manifests/core.action-manifest.json +83 -13
  168. package/package.json +8 -9
  169. package/adapters/extension/lib/slot-title.cjs +0 -292
  170. package/adapters/mobile/bridge-runtime/console-forwarder.cjs +0 -472
  171. package/adapters/mobile/bridge-runtime/lib/bridge-errors.cjs +0 -125
  172. package/adapters/mobile/bridge-runtime/lib/cdp-broker.cjs +0 -1055
  173. package/adapters/mobile/bridge-runtime/lib/cdp-eval.cjs +0 -110
  174. package/adapters/mobile/bridge-runtime/lib/config.cjs +0 -61
  175. package/adapters/mobile/bridge-runtime/lib/console-format.cjs +0 -53
  176. package/adapters/mobile/bridge-runtime/lib/devtools-proxy.cjs +0 -177
  177. package/adapters/mobile/bridge-runtime/lib/issue-capture.cjs +0 -446
  178. package/adapters/mobile/bridge-runtime/lib/match-bridge-target.cjs +0 -84
  179. package/adapters/mobile/bridge-runtime/lib/target-discovery.cjs +0 -403
  180. package/adapters/mobile/bridge-runtime/lib/ws-client.cjs +0 -116
  181. package/adapters/mobile/coalesce-metro-log.cjs +0 -24
  182. package/adapters/mobile/launch-metro.cjs +0 -77
  183. package/adapters/mobile/metro-config.cjs +0 -98
  184. package/adapters/mobile/metro-log-generation.cjs +0 -106
  185. package/dist/adapters/mobile/frame-metrics.js +0 -118
  186. package/dist/adapters/mobile/video-recorder.js +0 -289
  187. package/dist/artifact-files.js +0 -92
  188. package/dist/checkout-lock.js +0 -149
  189. package/dist/cli-color.js +0 -98
  190. package/dist/command-journal.js +0 -308
  191. package/dist/commands/last.js +0 -60
  192. package/dist/commands/launch/index.js +0 -109
  193. package/dist/commands/parse-args.js +0 -279
  194. package/dist/commands/reload.js +0 -41
  195. package/dist/commands/run-report.js +0 -241
  196. package/dist/commands/shared.js +0 -323
  197. package/dist/commands/stop.js +0 -60
  198. package/dist/devices.js +0 -100
  199. package/dist/execution-provenance.js +0 -332
  200. package/dist/harness.js +0 -451
  201. package/dist/heal-bounds.js +0 -137
  202. package/dist/json-stream.js +0 -60
  203. package/dist/leaf-invoke.js +0 -19
  204. package/dist/library-provenance.js +0 -33
  205. package/dist/live-adapter-contract.js +0 -380
  206. package/dist/recording-target.js +0 -120
  207. package/dist/run-diagnostics.js +0 -364
  208. package/dist/run-recording.js +0 -481
  209. package/dist/runtime-proof.js +0 -90
  210. package/library/actions/mobile/platform/tool-paths.mjs +0 -123
@@ -1,1055 +0,0 @@
1
- 'use strict';
2
-
3
- const fs = require('node:fs');
4
- const { createHash, randomUUID } = require('node:crypto');
5
- const net = require('node:net');
6
- const path = require('node:path');
7
-
8
- const DEFAULT_MAX_REQUESTS = 1000;
9
- const DEFAULT_MAX_DURATION_MS = 5 * 60 * 1000;
10
- const MAX_CAPTURE_DURATION_MS = 60 * 60 * 1000;
11
- const MAX_ACTIVE_CAPTURES = 16;
12
- const MAX_COMPLETED_CAPTURES = 16;
13
- const MAX_CAPTURE_BYTES = 4 * 1024 * 1024;
14
- const MAX_POST_DATA_BYTES = 64 * 1024;
15
- const MAX_REQUEST_FRAME_BYTES = 8 * 1024 * 1024;
16
- const MAX_RESPONSE_FRAME_BYTES = 16 * 1024 * 1024;
17
- const MAX_HUD_STEP_BYTES = 16 * 1024;
18
- const MAX_HUD_UPDATES = 64;
19
- const MAX_RETAINED_STRING_LENGTH = 256;
20
- const MAX_BROKER_TIMEOUT_MS = 60_000;
21
- const SENSITIVE_FIELD =
22
- /(?:address|authorization|cookie|key|password|secret|token|user|account)/iu;
23
- const SENSITIVE_VALUE =
24
- /(?:0x[a-f0-9]{40,}|[a-f0-9]{64,}|eyJ[a-z0-9_-]{20,}\.[a-z0-9_-]{20,})/iu;
25
- const APPLY_HUD_UPDATE_FUNCTION = `function(step) {
26
- const bridge = this.__AGENTIC__;
27
- if (step === null) {
28
- if (typeof bridge?.hideStep !== 'function') return false;
29
- setTimeout(() => bridge.hideStep(), 0);
30
- return true;
31
- }
32
- if (typeof bridge?.showStep !== 'function') return false;
33
- setTimeout(() => bridge.showStep(step), 0);
34
- return true;
35
- }`;
36
-
37
- function brokerSocketPath(runtimeDir, endpointIdentity) {
38
- const runtimePath = path.resolve(
39
- runtimeDir ||
40
- process.env.RECIPE_RUNTIME_DIR ||
41
- path.join('temp', 'recipe', 'runtime'),
42
- );
43
- const socketIdentity =
44
- endpointIdentity === undefined || endpointIdentity === null
45
- ? runtimePath
46
- : `${runtimePath}\u0000${String(endpointIdentity)}`;
47
- const identity = createHash('sha256')
48
- .update(socketIdentity)
49
- .digest('hex')
50
- .slice(0, 20);
51
- return path.join('/tmp', `mmh-cdp-${identity}.sock`);
52
- }
53
-
54
- function deviceIdFromUrl(wsUrl) {
55
- const match = /[?&]device=([^&]+)/u.exec(wsUrl || '');
56
- return match ? match[1] : wsUrl;
57
- }
58
-
59
- function safePrimitive(value) {
60
- if (value === null || ['number', 'boolean'].includes(typeof value)) {
61
- return value;
62
- }
63
- if (
64
- typeof value !== 'string' ||
65
- value.length > MAX_RETAINED_STRING_LENGTH ||
66
- SENSITIVE_VALUE.test(value)
67
- ) {
68
- return undefined;
69
- }
70
- return value;
71
- }
72
-
73
- function sanitizePathname(pathname) {
74
- const segments = String(pathname)
75
- .split('/')
76
- .map((segment) =>
77
- segment.length > 64 || SENSITIVE_VALUE.test(segment)
78
- ? '<redacted>'
79
- : segment,
80
- );
81
- return segments.join('/').slice(0, 1024);
82
- }
83
-
84
- function boundedError(error) {
85
- return String(error?.message || error || 'unknown error').slice(
86
- 0,
87
- MAX_RETAINED_STRING_LENGTH,
88
- );
89
- }
90
-
91
- function boundedTimeout(value, fallback = 10_000) {
92
- const timeout = Number(value);
93
- return Number.isFinite(timeout) && timeout > 0
94
- ? Math.min(MAX_BROKER_TIMEOUT_MS, timeout)
95
- : fallback;
96
- }
97
-
98
- // State recovery is event-driven. Pick a fixed bound below the caller's RPC
99
- // deadline so the broker always answers before the client gives up. Returning
100
- // literals also keeps the timer independent from untrusted request values.
101
- function stateWaitTimeout(value) {
102
- const timeout = boundedTimeout(value);
103
- if (timeout <= 500) return 1;
104
- if (timeout <= 1_000) return 400;
105
- if (timeout <= 2_000) return 900;
106
- if (timeout <= 5_000) return 1_900;
107
- if (timeout <= 10_000) return 4_500;
108
- if (timeout <= 15_000) return 9_500;
109
- if (timeout <= 30_000) return 14_500;
110
- if (timeout <= 45_000) return 29_500;
111
- if (timeout <= 55_000) return 44_500;
112
- return 54_500;
113
- }
114
-
115
- function readField(value, field) {
116
- let current = value;
117
- for (const segment of String(field).split('.')) {
118
- if (
119
- !current ||
120
- typeof current !== 'object' ||
121
- !Object.hasOwn(current, segment)
122
- ) {
123
- return { found: false };
124
- }
125
- current = current[segment];
126
- }
127
- return { found: true, value: safePrimitive(current) };
128
- }
129
-
130
- function sanitizeNetworkRequest(request, capture, now) {
131
- let url;
132
- try {
133
- url = new URL(request.url);
134
- } catch {
135
- return null;
136
- }
137
- if (
138
- capture.urlIncludes.length > 0 &&
139
- !capture.urlIncludes.some((value) => request.url.includes(value))
140
- ) {
141
- return null;
142
- }
143
- if (
144
- capture.methods.length > 0 &&
145
- !capture.methods.includes(String(request.method).toUpperCase())
146
- ) {
147
- return null;
148
- }
149
-
150
- let body = null;
151
- let bodyInspectable = capture.bodyJsonFields.length === 0;
152
- if (
153
- capture.bodyJsonFields.length > 0 &&
154
- request.postData &&
155
- Buffer.byteLength(request.postData) <= MAX_POST_DATA_BYTES
156
- ) {
157
- try {
158
- const parsed = JSON.parse(request.postData);
159
- bodyInspectable = true;
160
- const entries = [];
161
- for (const field of capture.bodyJsonFields) {
162
- const retained = readField(parsed, field);
163
- if (!retained.found) continue;
164
- if (retained.value === undefined) {
165
- bodyInspectable = false;
166
- continue;
167
- }
168
- entries.push([field, retained.value]);
169
- }
170
- body = Object.fromEntries(entries);
171
- } catch {
172
- body = null;
173
- }
174
- }
175
-
176
- return {
177
- record: {
178
- elapsedMs: now - capture.startedAtEpochMs,
179
- host: url.hostname,
180
- path: sanitizePathname(url.pathname),
181
- method: String(request.method || 'GET').toUpperCase(),
182
- ...(body && Object.keys(body).length > 0 ? { body } : {}),
183
- },
184
- bodyInspectable,
185
- };
186
- }
187
-
188
- function countsBy(records, selector) {
189
- const result = Object.create(null);
190
- for (const record of records) {
191
- const key = String(selector(record) ?? 'unknown');
192
- result[key] = (result[key] || 0) + 1;
193
- }
194
- return Object.fromEntries(Object.entries(result).sort(([a], [b]) => a.localeCompare(b)));
195
- }
196
-
197
- function normalizeCapture(input) {
198
- const id = String(input.id || '').trim();
199
- if (!/^[a-z0-9._-]{1,100}$/iu.test(id)) {
200
- throw new Error('Network capture id is invalid');
201
- }
202
- const normalizeList = (value, name) => {
203
- const values = Array.isArray(value)
204
- ? [...new Set(value.map(String).filter(Boolean))]
205
- : [];
206
- if (values.length > 20 || values.some((entry) => entry.length > 256)) {
207
- throw new Error(`Network capture ${name} exceeds its bound`);
208
- }
209
- return values;
210
- };
211
- const bodyJsonFields = normalizeList(
212
- input.bodyJsonFields,
213
- 'bodyJsonFields',
214
- );
215
- if (bodyJsonFields.some((field) => SENSITIVE_FIELD.test(field))) {
216
- throw new Error('Network capture body field is sensitive');
217
- }
218
- const maxRequests = Number(input.maxRequests ?? DEFAULT_MAX_REQUESTS);
219
- if (!Number.isInteger(maxRequests) || maxRequests < 1 || maxRequests > 10_000) {
220
- throw new Error('Network capture maxRequests must be between 1 and 10000');
221
- }
222
- const maxDurationMs = Number(
223
- input.maxDurationMs ?? DEFAULT_MAX_DURATION_MS,
224
- );
225
- if (
226
- !Number.isInteger(maxDurationMs) ||
227
- maxDurationMs < 1 ||
228
- maxDurationMs > MAX_CAPTURE_DURATION_MS
229
- ) {
230
- throw new Error('Network capture maxDurationMs is invalid');
231
- }
232
- return {
233
- id,
234
- startedAtEpochMs: Date.now(),
235
- urlIncludes: normalizeList(input.urlIncludes, 'urlIncludes'),
236
- methods: normalizeList(input.methods, 'methods').map((value) =>
237
- value.toUpperCase(),
238
- ),
239
- bodyJsonFields,
240
- maxRequests,
241
- maxDurationMs,
242
- requests: [],
243
- retainedBytes: 0,
244
- uninspectableBodyRequests: 0,
245
- droppedRequests: 0,
246
- reconnects: 0,
247
- partial: false,
248
- networkEnabled: false,
249
- enableErrors: [],
250
- endedAtEpochMs: null,
251
- };
252
- }
253
-
254
- function brokerOwnerPath(socketPath) {
255
- return `${socketPath}.owner`;
256
- }
257
-
258
- function readBrokerOwner(ownerPath) {
259
- let source;
260
- try {
261
- source = fs.readFileSync(ownerPath, 'utf8');
262
- } catch (error) {
263
- if (error.code !== 'EISDIR') return null;
264
- try {
265
- source = fs.readFileSync(path.join(ownerPath, 'owner.json'), 'utf8');
266
- } catch {
267
- return null;
268
- }
269
- }
270
- try {
271
- const value = JSON.parse(source);
272
- return Number.isInteger(value.pid) && typeof value.token === 'string'
273
- ? value
274
- : null;
275
- } catch {
276
- return null;
277
- }
278
- }
279
-
280
- function processIsAlive(pid) {
281
- if (!Number.isInteger(pid) || pid < 1) return false;
282
- try {
283
- process.kill(pid, 0);
284
- return true;
285
- } catch {
286
- return false;
287
- }
288
- }
289
-
290
- function claimBrokerSocket(socketPath) {
291
- const ownerPath = brokerOwnerPath(socketPath);
292
- const owner = { pid: process.pid, token: randomUUID() };
293
- const candidatePath = `${ownerPath}.claim-${owner.pid}-${owner.token}`;
294
- fs.mkdirSync(candidatePath, { mode: 0o700 });
295
- fs.writeFileSync(
296
- path.join(candidatePath, 'owner.json'),
297
- `${JSON.stringify(owner)}\n`,
298
- { flag: 'wx', mode: 0o600 },
299
- );
300
- let claimed = false;
301
- for (let attempt = 0; attempt < 10 && !claimed; attempt += 1) {
302
- try {
303
- fs.renameSync(candidatePath, ownerPath);
304
- claimed = true;
305
- } catch (error) {
306
- if (!['EEXIST', 'ENOTEMPTY', 'ENOTDIR', 'EISDIR'].includes(error.code)) {
307
- fs.rmSync(candidatePath, { recursive: true, force: true });
308
- throw error;
309
- }
310
- const previousOwner = readBrokerOwner(ownerPath);
311
- if (previousOwner && processIsAlive(previousOwner.pid)) {
312
- fs.rmSync(candidatePath, { recursive: true, force: true });
313
- throw new Error(
314
- `CDP broker socket is owned by process ${previousOwner.pid}`,
315
- );
316
- }
317
- const stalePath = `${ownerPath}.stale-${randomUUID()}`;
318
- try {
319
- fs.renameSync(ownerPath, stalePath);
320
- fs.rmSync(stalePath, { recursive: true, force: true });
321
- } catch (staleError) {
322
- if (staleError.code !== 'ENOENT') {
323
- fs.rmSync(candidatePath, { recursive: true, force: true });
324
- throw staleError;
325
- }
326
- }
327
- }
328
- }
329
- if (!claimed) {
330
- fs.rmSync(candidatePath, { recursive: true, force: true });
331
- throw new Error('CDP broker socket ownership could not be claimed');
332
- }
333
- try {
334
- fs.unlinkSync(socketPath);
335
- } catch (error) {
336
- if (error.code !== 'ENOENT') {
337
- fs.rmSync(ownerPath, { recursive: true, force: true });
338
- throw error;
339
- }
340
- }
341
- return { ownerPath, owner };
342
- }
343
-
344
- function releaseBrokerSocket(socketPath, ownership) {
345
- const currentOwner = readBrokerOwner(ownership.ownerPath);
346
- if (
347
- currentOwner?.pid !== ownership.owner.pid ||
348
- currentOwner?.token !== ownership.owner.token
349
- ) {
350
- return;
351
- }
352
- try {
353
- fs.unlinkSync(socketPath);
354
- } catch {}
355
- try {
356
- fs.rmSync(ownership.ownerPath, { recursive: true, force: true });
357
- } catch {}
358
- }
359
-
360
- function createCdpBroker({
361
- socketPath,
362
- sessions,
363
- sendCommand,
364
- requestDiscovery,
365
- onClientActivity,
366
- }) {
367
- const captures = new Map();
368
- const completedCaptures = new Map();
369
- const hudUpdates = new Map();
370
- const appliedHudVersions = new Map();
371
- const hudApplyChains = new Map();
372
- const clients = new Set();
373
- const subscriptions = new Map();
374
- const knownTargets = new Map(
375
- [...sessions.entries()].flatMap(([deviceId, session]) =>
376
- session?.brokerReady
377
- ? [[deviceId, {
378
- deviceId,
379
- generation: 1,
380
- name: String(session.name || ''),
381
- ready: true,
382
- }]]
383
- : [],
384
- ),
385
- );
386
- const sessionWaiters = new Map();
387
- const targetWaiters = new Set();
388
- const observedSessions = new Map(
389
- [...sessions.entries()].flatMap(([deviceId, session]) =>
390
- session?.brokerReady ? [[deviceId, session]] : [],
391
- ),
392
- );
393
-
394
- const capturesFor = (deviceId) => {
395
- if (!captures.has(deviceId)) captures.set(deviceId, new Map());
396
- return captures.get(deviceId);
397
- };
398
-
399
- const completedCapturesFor = (deviceId) => {
400
- if (!completedCaptures.has(deviceId)) {
401
- completedCaptures.set(deviceId, new Map());
402
- }
403
- return completedCaptures.get(deviceId);
404
- };
405
-
406
- function targetList({ nameIncludes = '', readyOnly = false } = {}) {
407
- const pin = String(nameIncludes).trim().toLowerCase();
408
- return [...knownTargets.values()]
409
- .filter((target) => !readyOnly || target.ready)
410
- .filter((target) => !pin || target.name.toLowerCase().includes(pin))
411
- .map((target) => ({ ...target }))
412
- .sort((a, b) => a.name.localeCompare(b.name));
413
- }
414
-
415
- function settleTargetWaiters() {
416
- for (const waiter of [...targetWaiters]) {
417
- const targets = targetList({
418
- nameIncludes: waiter.nameIncludes,
419
- readyOnly: true,
420
- });
421
- if (targets.length === 0) continue;
422
- targetWaiters.delete(waiter);
423
- clearTimeout(waiter.timer);
424
- waiter.resolve(targets);
425
- }
426
- }
427
-
428
- function resolveTargets(params, timeoutMs, socket) {
429
- const existing = targetList({
430
- nameIncludes: params.nameIncludes,
431
- readyOnly: true,
432
- });
433
- if (existing.length > 0) return Promise.resolve(existing);
434
- const retained = targetList({ nameIncludes: params.nameIncludes });
435
- requestDiscovery?.(
436
- String(params.nameIncludes || '').trim() && retained.length === 1
437
- ? retained[0].deviceId
438
- : '',
439
- );
440
- return new Promise((resolve, reject) => {
441
- const waiter = {
442
- nameIncludes: String(params.nameIncludes || ''),
443
- resolve,
444
- reject,
445
- socket,
446
- timer: null,
447
- };
448
- waiter.timer = setTimeout(() => {
449
- targetWaiters.delete(waiter);
450
- reject(new Error('CDP broker never observed the requested target'));
451
- }, stateWaitTimeout(timeoutMs));
452
- targetWaiters.add(waiter);
453
- settleTargetWaiters();
454
- });
455
- }
456
-
457
- function waitForSession(deviceId, timeoutMs, socket) {
458
- if (sessions.get(deviceId)?.brokerReady) return sessions.get(deviceId);
459
- requestDiscovery?.(deviceId);
460
- return new Promise((resolve, reject) => {
461
- const waiters = sessionWaiters.get(deviceId) || new Set();
462
- const waiter = {
463
- resolve,
464
- reject,
465
- socket,
466
- timer: setTimeout(() => {
467
- waiters.delete(waiter);
468
- if (waiters.size === 0) sessionWaiters.delete(deviceId);
469
- reject(new Error(`CDP broker target unavailable: ${deviceId}`));
470
- }, stateWaitTimeout(timeoutMs)),
471
- };
472
- waiters.add(waiter);
473
- sessionWaiters.set(deviceId, waiters);
474
- const session = sessions.get(deviceId);
475
- if (session?.brokerReady) {
476
- waiters.delete(waiter);
477
- if (waiters.size === 0) sessionWaiters.delete(deviceId);
478
- clearTimeout(waiter.timer);
479
- resolve(session);
480
- }
481
- });
482
- }
483
-
484
- async function enableNetwork(
485
- deviceId,
486
- capture,
487
- timeoutMs = 10_000,
488
- socket,
489
- ) {
490
- try {
491
- const budgetMs = boundedTimeout(timeoutMs);
492
- const deadline = Date.now() + budgetMs;
493
- const session = await waitForSession(deviceId, budgetMs, socket);
494
- const remainingMs = Math.max(1, deadline - Date.now());
495
- await sendCommand(session, 'Network.enable', {}, remainingMs);
496
- if (capture.enableErrors.length > 0) capture.partial = true;
497
- capture.networkEnabled = true;
498
- } catch (error) {
499
- if (capture.networkEnabled) capture.partial = true;
500
- if (capture.enableErrors.length < 20) {
501
- capture.enableErrors.push(boundedError(error));
502
- }
503
- }
504
- }
505
-
506
- function finishCapture(deviceId, capture, partial = false) {
507
- capture.partial ||= partial;
508
- capture.endedAtEpochMs ??= Date.now();
509
- capturesFor(deviceId).delete(capture.id);
510
- }
511
-
512
- async function applyHudUpdate(deviceId, timeoutMs, socket) {
513
- if (!hudUpdates.has(deviceId)) return { status: 'none' };
514
- const budgetMs = boundedTimeout(timeoutMs);
515
- const deadline = Date.now() + budgetMs;
516
- const session = await waitForSession(deviceId, budgetMs, socket);
517
- const update = hudUpdates.get(deviceId);
518
- if (!update) return { status: 'none' };
519
- if (appliedHudVersions.get(deviceId) === update.version) {
520
- return { status: 'applied', version: update.version };
521
- }
522
- let applied = false;
523
- while (Date.now() < deadline) {
524
- const globalObject = await sendCommand(
525
- session,
526
- 'Runtime.evaluate',
527
- { expression: 'globalThis', returnByValue: false, awaitPromise: false },
528
- Math.max(1, deadline - Date.now()),
529
- );
530
- const objectId = globalObject?.result?.objectId;
531
- if (!objectId) {
532
- throw new Error('Mobile HUD runtime did not expose globalThis');
533
- }
534
- const evaluation = await sendCommand(
535
- session,
536
- 'Runtime.callFunctionOn',
537
- {
538
- objectId,
539
- functionDeclaration: APPLY_HUD_UPDATE_FUNCTION,
540
- arguments: [{ value: update.step }],
541
- returnByValue: true,
542
- awaitPromise: false,
543
- },
544
- Math.max(1, deadline - Date.now()),
545
- );
546
- if (evaluation?.result?.value === true) {
547
- applied = true;
548
- break;
549
- }
550
- await new Promise((resolve) => setTimeout(resolve, 100));
551
- }
552
- if (!applied) {
553
- throw new Error('Mobile HUD bridge was not installed before the update deadline');
554
- }
555
- if (hudUpdates.get(deviceId)?.version === update.version) {
556
- appliedHudVersions.set(deviceId, update.version);
557
- }
558
- return { status: 'applied', version: update.version };
559
- }
560
-
561
- function scheduleHudUpdate(deviceId, timeoutMs, socket) {
562
- const previous = hudApplyChains.get(deviceId) || Promise.resolve();
563
- const next = previous
564
- .catch(() => undefined)
565
- .then(() => applyHudUpdate(deviceId, timeoutMs, socket));
566
- hudApplyChains.set(deviceId, next);
567
- void next.finally(() => {
568
- if (hudApplyChains.get(deviceId) === next) hudApplyChains.delete(deviceId);
569
- }).catch(() => undefined);
570
- return next;
571
- }
572
-
573
- function expireCapture(deviceId, capture) {
574
- if (capturesFor(deviceId).get(capture.id) !== capture) return;
575
- finishCapture(deviceId, capture, true);
576
- const completed = completedCapturesFor(deviceId);
577
- completed.set(capture.id, capture);
578
- while (completed.size > MAX_COMPLETED_CAPTURES) {
579
- completed.delete(completed.keys().next().value);
580
- }
581
- }
582
-
583
- function captureSummary(capture) {
584
- const bodyType = (record) => record.body?.type ?? 'unknown';
585
- const endedAtEpochMs = capture.endedAtEpochMs ?? Date.now();
586
- const exceededMaxDuration =
587
- endedAtEpochMs - capture.startedAtEpochMs > capture.maxDurationMs;
588
- return {
589
- schemaVersion: 1,
590
- id: capture.id,
591
- status:
592
- !capture.networkEnabled
593
- ? 'unavailable'
594
- : capture.partial || exceededMaxDuration
595
- ? 'partial'
596
- : 'complete',
597
- startedAtEpochMs: capture.startedAtEpochMs,
598
- endedAtEpochMs,
599
- maxDurationMs: capture.maxDurationMs,
600
- reconnects: capture.reconnects,
601
- droppedRequests: capture.droppedRequests,
602
- unavailableReasons: capture.networkEnabled ? [] : capture.enableErrors,
603
- coverageGapReasons: capture.networkEnabled ? capture.enableErrors : [],
604
- uninspectableBodyRequests: capture.uninspectableBodyRequests,
605
- projectedBodyFields: capture.bodyJsonFields,
606
- totalRequests: capture.requests.length,
607
- requestsByMethod: countsBy(capture.requests, (record) => record.method),
608
- requestsByHost: countsBy(capture.requests, (record) => record.host),
609
- requestsByType: countsBy(capture.requests, bodyType),
610
- requests: capture.requests,
611
- };
612
- }
613
-
614
- async function control(
615
- deviceId,
616
- action,
617
- params,
618
- timeoutMs = 10_000,
619
- socket,
620
- ) {
621
- if (action === 'list-targets') {
622
- return targetList({ readyOnly: true }).map(({ deviceId, name }) => ({
623
- deviceId,
624
- name,
625
- }));
626
- }
627
- if (action === 'list-target-identities') {
628
- return targetList({ readyOnly: true }).map(
629
- ({ deviceId, generation, name }) => ({ deviceId, generation, name }),
630
- );
631
- }
632
- if (action === 'resolve-targets') {
633
- return resolveTargets(params, timeoutMs, socket);
634
- }
635
- if (action === 'hud-update') {
636
- const step = params.step ?? null;
637
- if (step !== null && (typeof step !== 'object' || Array.isArray(step))) {
638
- throw new Error('HUD update requires an object step or null');
639
- }
640
- if (Buffer.byteLength(JSON.stringify(step)) > MAX_HUD_STEP_BYTES) {
641
- throw new Error('HUD update exceeds its bound');
642
- }
643
- const previousVersion = hudUpdates.get(deviceId)?.version || 0;
644
- if (hudUpdates.has(deviceId)) {
645
- hudUpdates.delete(deviceId);
646
- }
647
- while (hudUpdates.size >= MAX_HUD_UPDATES) {
648
- const oldestDeviceId = hudUpdates.keys().next().value;
649
- hudUpdates.delete(oldestDeviceId);
650
- appliedHudVersions.delete(oldestDeviceId);
651
- }
652
- const version = previousVersion + 1;
653
- hudUpdates.set(deviceId, { step, version });
654
- requestDiscovery?.(deviceId);
655
- void scheduleHudUpdate(deviceId, 10_000).catch(() => undefined);
656
- return { status: 'queued', version };
657
- }
658
- if (action === 'capture-start') {
659
- const capture = normalizeCapture(params);
660
- const deviceCaptures = capturesFor(deviceId);
661
- if (deviceCaptures.size >= MAX_ACTIVE_CAPTURES) {
662
- throw new Error('Too many active Network captures');
663
- }
664
- if (deviceCaptures.has(capture.id)) {
665
- throw new Error(`Network capture already active: ${capture.id}`);
666
- }
667
- completedCapturesFor(deviceId).delete(capture.id);
668
- deviceCaptures.set(capture.id, capture);
669
- await enableNetwork(
670
- deviceId,
671
- capture,
672
- Math.max(1, timeoutMs - 500),
673
- socket,
674
- );
675
- return { id: capture.id, status: 'started' };
676
- }
677
- if (action === 'capture-end') {
678
- const id = String(params.id || '').trim();
679
- const deviceCaptures = capturesFor(deviceId);
680
- const completed = completedCapturesFor(deviceId);
681
- const capture = deviceCaptures.get(id) || completed.get(id);
682
- if (!capture) throw new Error(`Network capture not active: ${id}`);
683
- if (deviceCaptures.has(id)) finishCapture(deviceId, capture);
684
- completed.delete(id);
685
- return captureSummary(capture);
686
- }
687
- throw new Error(`Unknown CDP broker control: ${action}`);
688
- }
689
-
690
- function writeMessage(socket, message) {
691
- if (!socket.destroyed) socket.write(`${JSON.stringify(message)}\n`);
692
- }
693
-
694
- async function handleRequest(socket, request) {
695
- try {
696
- onClientActivity?.();
697
- if (request.type === 'command') {
698
- const budgetMs = boundedTimeout(request.timeoutMs);
699
- const deadline = Date.now() + budgetMs;
700
- const session = await waitForSession(
701
- request.deviceId,
702
- budgetMs,
703
- socket,
704
- );
705
- const result = await sendCommand(
706
- session,
707
- request.method,
708
- request.params || {},
709
- Math.max(1, deadline - Date.now()),
710
- );
711
- writeMessage(socket, { id: request.id, result });
712
- return;
713
- }
714
- if (request.type === 'control') {
715
- const result = await control(
716
- request.deviceId,
717
- request.action,
718
- request.params || {},
719
- boundedTimeout(request.timeoutMs),
720
- socket,
721
- );
722
- writeMessage(socket, { id: request.id, result });
723
- return;
724
- }
725
- if (request.type === 'subscribe') {
726
- const entries = subscriptions.get(socket) || new Set();
727
- entries.add(`${request.deviceId}\u0000${request.method}`);
728
- subscriptions.set(socket, entries);
729
- writeMessage(socket, { id: request.id, result: {} });
730
- return;
731
- }
732
- if (request.type === 'unsubscribe') {
733
- subscriptions
734
- .get(socket)
735
- ?.delete(`${request.deviceId}\u0000${request.method}`);
736
- writeMessage(socket, { id: request.id, result: {} });
737
- return;
738
- }
739
- throw new Error('Unknown CDP broker request');
740
- } catch (error) {
741
- writeMessage(socket, {
742
- id: request.id,
743
- error: String(error.message || error),
744
- });
745
- }
746
- }
747
-
748
- fs.mkdirSync(path.dirname(socketPath), { recursive: true });
749
- const ownership = claimBrokerSocket(socketPath);
750
- const expirySweep = setInterval(() => {
751
- const now = Date.now();
752
- for (const [deviceId, deviceCaptures] of captures) {
753
- for (const capture of deviceCaptures.values()) {
754
- if (now - capture.startedAtEpochMs >= capture.maxDurationMs) {
755
- expireCapture(deviceId, capture);
756
- }
757
- }
758
- }
759
- }, 100);
760
- const server = net.createServer((socket) => {
761
- clients.add(socket);
762
- subscriptions.set(socket, new Set());
763
- let buffer = '';
764
- socket.setEncoding('utf8');
765
- socket.on('data', (chunk) => {
766
- buffer += chunk;
767
- if (Buffer.byteLength(buffer) > MAX_REQUEST_FRAME_BYTES) {
768
- writeMessage(socket, { error: 'CDP broker request exceeds its bound' });
769
- socket.destroy();
770
- return;
771
- }
772
- let newline;
773
- while ((newline = buffer.indexOf('\n')) >= 0) {
774
- const line = buffer.slice(0, newline);
775
- buffer = buffer.slice(newline + 1);
776
- if (!line.trim()) continue;
777
- try {
778
- const request = JSON.parse(line);
779
- if (!request || typeof request !== 'object' || Array.isArray(request)) {
780
- throw new Error('Invalid CDP broker request');
781
- }
782
- void handleRequest(socket, request);
783
- } catch {
784
- writeMessage(socket, { error: 'Invalid CDP broker request' });
785
- }
786
- }
787
- });
788
- const drop = () => {
789
- clients.delete(socket);
790
- subscriptions.delete(socket);
791
- for (const waiter of [...targetWaiters]) {
792
- if (waiter.socket !== socket) continue;
793
- targetWaiters.delete(waiter);
794
- clearTimeout(waiter.timer);
795
- waiter.reject(new Error('CDP broker client closed'));
796
- }
797
- for (const [deviceId, waiters] of sessionWaiters) {
798
- for (const waiter of [...waiters]) {
799
- if (waiter.socket !== socket) continue;
800
- waiters.delete(waiter);
801
- clearTimeout(waiter.timer);
802
- waiter.reject(new Error('CDP broker client closed'));
803
- }
804
- if (waiters.size === 0) sessionWaiters.delete(deviceId);
805
- }
806
- };
807
- socket.on('close', drop);
808
- socket.on('error', drop);
809
- });
810
- server.on('error', (error) => {
811
- releaseBrokerSocket(socketPath, ownership);
812
- process.stderr.write(`cdp-broker: ${boundedError(error)}\n`);
813
- });
814
- server.listen(socketPath, () => fs.chmodSync(socketPath, 0o600));
815
-
816
- return {
817
- onSessionOpen(deviceId) {
818
- const session = sessions.get(deviceId);
819
- if (!session?.brokerReady) return;
820
- const previous = knownTargets.get(deviceId);
821
- const name = String(session.name || previous?.name || '');
822
- for (const [otherDeviceId, target] of knownTargets) {
823
- if (
824
- otherDeviceId !== deviceId &&
825
- name &&
826
- target.name === name &&
827
- target.ready &&
828
- !sessions.get(otherDeviceId)?.brokerReady
829
- ) {
830
- knownTargets.set(otherDeviceId, { ...target, ready: false });
831
- }
832
- }
833
- const sessionChanged = observedSessions.get(deviceId) !== session;
834
- knownTargets.set(deviceId, {
835
- deviceId,
836
- generation:
837
- previous?.generation === undefined
838
- ? 1
839
- : previous.generation + (!previous.ready || sessionChanged ? 1 : 0),
840
- name,
841
- ready: true,
842
- });
843
- observedSessions.set(deviceId, session);
844
- const waiters = sessionWaiters.get(deviceId);
845
- if (waiters) {
846
- sessionWaiters.delete(deviceId);
847
- for (const waiter of waiters) {
848
- clearTimeout(waiter.timer);
849
- waiter.resolve(session);
850
- }
851
- }
852
- settleTargetWaiters();
853
- for (const capture of capturesFor(deviceId).values()) {
854
- if (capture.reconnects > 0) capture.partial = true;
855
- void enableNetwork(deviceId, capture);
856
- }
857
- void scheduleHudUpdate(deviceId, 10_000).catch(() => undefined);
858
- },
859
- onSessionClose(deviceId) {
860
- const previous = knownTargets.get(deviceId);
861
- if (previous) knownTargets.set(deviceId, { ...previous, ready: false });
862
- observedSessions.delete(deviceId);
863
- appliedHudVersions.delete(deviceId);
864
- for (const capture of capturesFor(deviceId).values()) {
865
- capture.reconnects += 1;
866
- capture.partial = true;
867
- }
868
- },
869
- onCdpEvent(deviceId, method, params) {
870
- if (method === 'Runtime.executionContextsCleared') {
871
- const target = knownTargets.get(deviceId);
872
- if (target) knownTargets.set(deviceId, { ...target, generation: target.generation + 1 });
873
- }
874
- for (const socket of clients) {
875
- if (subscriptions.get(socket)?.has(`${deviceId}\u0000${method}`)) {
876
- writeMessage(socket, { type: 'event', deviceId, method, params });
877
- }
878
- }
879
- if (method !== 'Network.requestWillBeSent') return;
880
- const now = Date.now();
881
- for (const capture of capturesFor(deviceId).values()) {
882
- const sanitized = sanitizeNetworkRequest(
883
- params.request || {},
884
- capture,
885
- now,
886
- );
887
- if (!sanitized) continue;
888
- const { record } = sanitized;
889
- if (!sanitized.bodyInspectable) {
890
- capture.uninspectableBodyRequests += 1;
891
- capture.partial = true;
892
- }
893
- if (now - capture.startedAtEpochMs > capture.maxDurationMs) {
894
- capture.droppedRequests += 1;
895
- expireCapture(deviceId, capture);
896
- continue;
897
- }
898
- if (capture.requests.length >= capture.maxRequests) {
899
- capture.droppedRequests += 1;
900
- capture.partial = true;
901
- continue;
902
- }
903
- const recordBytes = Buffer.byteLength(JSON.stringify(record));
904
- if (capture.retainedBytes + recordBytes > MAX_CAPTURE_BYTES) {
905
- capture.droppedRequests += 1;
906
- capture.partial = true;
907
- continue;
908
- }
909
- capture.retainedBytes += recordBytes;
910
- capture.requests.push(record);
911
- }
912
- },
913
- close() {
914
- clearInterval(expirySweep);
915
- for (const waiters of sessionWaiters.values()) {
916
- for (const waiter of waiters) {
917
- clearTimeout(waiter.timer);
918
- waiter.reject(new Error('CDP broker closed'));
919
- }
920
- }
921
- sessionWaiters.clear();
922
- for (const waiter of targetWaiters) {
923
- clearTimeout(waiter.timer);
924
- waiter.reject(new Error('CDP broker closed'));
925
- }
926
- targetWaiters.clear();
927
- for (const socket of clients) socket.destroy();
928
- try {
929
- server.close();
930
- } catch {}
931
- releaseBrokerSocket(socketPath, ownership);
932
- },
933
- };
934
- }
935
-
936
- function createBrokerClient(socketPath, deviceId, timeout) {
937
- return new Promise((resolve, reject) => {
938
- const clientTimeout = boundedTimeout(timeout);
939
- const socket = net.createConnection(socketPath);
940
- socket.setEncoding('utf8');
941
- let nextId = 0;
942
- let buffer = '';
943
- const pending = new Map();
944
- const eventHandlers = new Map();
945
- const connectTimer = setTimeout(() => {
946
- socket.destroy();
947
- reject(new Error('CDP broker connection timeout'));
948
- }, clientTimeout);
949
-
950
- const request = (type, value, requestTimeout = clientTimeout) =>
951
- new Promise((requestResolve, requestReject) => {
952
- const timeoutMs = boundedTimeout(requestTimeout, clientTimeout);
953
- const id = ++nextId;
954
- const timer = setTimeout(() => {
955
- pending.delete(id);
956
- requestReject(new Error('CDP broker request timeout'));
957
- }, timeoutMs);
958
- pending.set(id, {
959
- resolve: (result) => {
960
- clearTimeout(timer);
961
- requestResolve(result);
962
- },
963
- reject: (error) => {
964
- clearTimeout(timer);
965
- requestReject(error);
966
- },
967
- });
968
- socket.write(
969
- `${JSON.stringify({ id, deviceId, timeoutMs, type, ...value })}\n`,
970
- );
971
- });
972
-
973
- socket.on('connect', () => {
974
- clearTimeout(connectTimer);
975
- resolve({
976
- send(method, params = {}, requestTimeout = clientTimeout) {
977
- return request('command', { method, params }, requestTimeout);
978
- },
979
- control(action, params = {}, requestTimeout = clientTimeout) {
980
- return request('control', { action, params }, requestTimeout);
981
- },
982
- on(method, handler) {
983
- const handlers = eventHandlers.get(method) || new Set();
984
- handlers.add(handler);
985
- eventHandlers.set(method, handlers);
986
- void request('subscribe', { method });
987
- return () => {
988
- handlers.delete(handler);
989
- if (handlers.size === 0) {
990
- eventHandlers.delete(method);
991
- void request('unsubscribe', { method }).catch(() => undefined);
992
- }
993
- };
994
- },
995
- close() {
996
- socket.destroy();
997
- },
998
- });
999
- });
1000
- socket.on('data', (chunk) => {
1001
- buffer += chunk;
1002
- if (Buffer.byteLength(buffer) > MAX_RESPONSE_FRAME_BYTES) {
1003
- socket.destroy();
1004
- for (const entry of pending.values()) {
1005
- entry.reject(new Error('CDP broker response exceeds its bound'));
1006
- }
1007
- pending.clear();
1008
- return;
1009
- }
1010
- let newline;
1011
- while ((newline = buffer.indexOf('\n')) >= 0) {
1012
- const line = buffer.slice(0, newline);
1013
- buffer = buffer.slice(newline + 1);
1014
- if (!line.trim()) continue;
1015
- let message;
1016
- try {
1017
- message = JSON.parse(line);
1018
- } catch {
1019
- continue;
1020
- }
1021
- if (message.id && pending.has(message.id)) {
1022
- const entry = pending.get(message.id);
1023
- pending.delete(message.id);
1024
- if (message.error) entry.reject(new Error(message.error));
1025
- else entry.resolve(message.result);
1026
- continue;
1027
- }
1028
- if (message.type !== 'event') continue;
1029
- for (const handler of eventHandlers.get(message.method) || []) {
1030
- try {
1031
- handler(message.params || {});
1032
- } catch {}
1033
- }
1034
- }
1035
- });
1036
- socket.on('error', (error) => {
1037
- clearTimeout(connectTimer);
1038
- reject(error);
1039
- });
1040
- socket.on('close', () => {
1041
- for (const entry of pending.values()) {
1042
- entry.reject(new Error('CDP broker closed'));
1043
- }
1044
- pending.clear();
1045
- eventHandlers.clear();
1046
- });
1047
- });
1048
- }
1049
-
1050
- module.exports = {
1051
- brokerSocketPath,
1052
- createBrokerClient,
1053
- createCdpBroker,
1054
- deviceIdFromUrl,
1055
- };