imprint-mcp 0.3.1 → 0.4.1

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.
@@ -109,6 +109,9 @@ export interface CdpBrowserFetchOptions {
109
109
  abckWaitSeconds?: number;
110
110
  /** Per-request in-page timeout (ms). Default 60000. */
111
111
  requestTimeoutMs?: number;
112
+ /** Per-CDP-command timeout (ms). Default 20000. Prevents a wedged browser
113
+ * or CDP socket from hanging an MCP tool call forever. */
114
+ cdpCommandTimeoutMs?: number;
112
115
  /** Launch a visible window instead of headless. Default false (headless). Only
113
116
  * needed as a fallback on a GPU-less host where headless WebGL falls back to
114
117
  * SwiftShader and the site fingerprints it — pair with `display`/Xvfb. */
@@ -135,6 +138,19 @@ export interface CdpBrowserFetchOptions {
135
138
  }
136
139
 
137
140
  type CdpClient = Awaited<ReturnType<typeof CDP>>;
141
+ type LaunchedChromium = Awaited<ReturnType<typeof launchChromium>>;
142
+ type ChromiumLauncher = (opts: Parameters<typeof launchChromium>[0]) => Promise<LaunchedChromium>;
143
+ type CdpConnector = (port: number) => Promise<CdpClient>;
144
+
145
+ let chromiumLauncherForTest: ChromiumLauncher | null = null;
146
+ let cdpConnectorForTest: CdpConnector | null = null;
147
+
148
+ export function __setCdpBrowserFetchHooksForTest(
149
+ hooks: { launchChromium?: ChromiumLauncher; connectCdp?: CdpConnector } | null,
150
+ ): void {
151
+ chromiumLauncherForTest = hooks?.launchChromium ?? null;
152
+ cdpConnectorForTest = hooks?.connectCdp ?? null;
153
+ }
138
154
 
139
155
  function abckIsValidated(v: string | undefined): boolean {
140
156
  return !!v && v.split('~')[1] === '0';
@@ -201,168 +217,234 @@ export function createCdpBrowserFetch(opts: CdpBrowserFetchOptions): CdpBrowserF
201
217
  const navUrl = opts.bootstrapUrl ?? (baseLooksLikeApi ? `${baseOrigin}/` : opts.baseUrl);
202
218
  const abckWaitMs = (opts.abckWaitSeconds ?? 25) * 1000;
203
219
  const reqTimeoutMs = opts.requestTimeoutMs ?? 60_000;
220
+ const cdpCommandTimeoutMs = opts.cdpCommandTimeoutMs ?? 20_000;
221
+ const shortCdpTimeoutMs = Math.max(1, Math.min(cdpCommandTimeoutMs, 2_000));
204
222
 
205
- let chrome: Awaited<ReturnType<typeof launchChromium>> | null = null;
223
+ let chrome: LaunchedChromium | null = null;
206
224
  let client: CdpClient | null = null;
207
225
  let bootstrapped = false;
208
226
  let appliedUa: string | undefined;
209
227
 
210
- async function ensure(): Promise<CdpClient> {
211
- if (client && bootstrapped) return client;
212
- const headed = opts.headed ?? false;
213
- if (!chrome) {
214
- log(`launching real ${headed ? 'headed' : 'headless'} Chrome (will navigate ${navUrl})`);
215
- // Launch at about:blank — we MUST attach CDP and override the UA before the
216
- // first request to the protected origin fires, so we navigate via
217
- // Page.navigate AFTER the override rather than passing the URL at launch.
218
- // headless renders offscreen (no display); headed needs one (Xvfb on Linux).
219
- chrome = await launchChromium({
220
- headless: !headed,
221
- extraArgs: gpuLaunchArgs(),
222
- ...(headed ? { display: opts.display } : {}),
223
- });
224
- await chrome.ready;
225
- }
226
- if (!client) client = await CDP({ port: chrome.port });
227
- const { Runtime, Network, Input, Page } = client;
228
- await Runtime.enable();
229
- await Network.enable();
230
- await Page.enable();
231
- // Plant the high-trust seed cookies (the recording's validated Akamai jar)
232
- // BEFORE navigating, so the first request to the protected origin carries the
233
- // trusted session. A synthetic mint can reach `_abck~0~` yet still get its
234
- // `.act` tarpitted; starting from the recording's earned trust is what makes
235
- // the in-page protected POSTs succeed (the live bmak sensor then keeps it
236
- // re-validated between calls).
237
- if (opts.seedCookies && opts.seedCookies.length > 0) {
238
- let planted = 0;
239
- for (const c of opts.seedCookies) {
240
- try {
241
- await Network.setCookie({
242
- name: c.name,
243
- value: c.value,
244
- domain: c.domain,
245
- path: c.path ?? '/',
246
- secure: c.secure ?? false,
247
- httpOnly: c.httpOnly ?? false,
248
- ...(c.sameSite ? { sameSite: normalizeSameSite(c.sameSite) } : {}),
249
- ...(typeof c.expires === 'number' && c.expires > 0 ? { expires: c.expires } : {}),
250
- });
251
- planted++;
252
- } catch {
253
- // best-effort — a cookie Akamai re-issues on navigate isn't fatal
254
- }
255
- }
256
- log(`seeded ${planted}/${opts.seedCookies.length} high-trust cookies before navigate`);
257
- }
258
- // Strip the `HeadlessChrome` UA token (Akamai's only headless edge-tell) and
259
- // send matching client hints — BEFORE navigating to the protected origin.
228
+ async function close(): Promise<void> {
229
+ const c = client;
230
+ const ch = chrome;
231
+ client = null;
232
+ chrome = null;
233
+ bootstrapped = false;
234
+ appliedUa = undefined;
260
235
  try {
261
- const { result } = await Runtime.evaluate({
262
- expression: 'navigator.userAgent',
263
- returnByValue: true,
264
- });
265
- const rawUa = String(result.value ?? '');
266
- if (rawUa) {
267
- const override = buildUaOverride(rawUa);
268
- await Network.setUserAgentOverride(override);
269
- appliedUa = override.userAgent;
270
- log(`UA override: ${override.userAgent}`);
271
- }
236
+ await withTimeout(Promise.resolve(c?.close()), 'CDP client close', 2_000);
272
237
  } catch {
273
- // best-effort — a headed launch already has a clean UA
238
+ /* ignore */
274
239
  }
275
- // Navigate now (post-override). Page.navigate stalls forever on an Akamai
276
- // origin ONLY when the UA still says HeadlessChrome; with the override it
277
- // loads normally. Race a timeout and proceed regardless — _abck polling below
278
- // tolerates a partial load.
279
240
  try {
280
- await Promise.race([
281
- Page.navigate({ url: navUrl }),
282
- sleep(Math.min(abckWaitMs, 25_000)).then(() => {
283
- throw new Error('navigate timeout');
284
- }),
285
- ]);
286
- await Promise.race([
287
- Page.loadEventFired(),
288
- sleep(Math.min(abckWaitMs, 5000)).then(() => undefined),
289
- ]).catch(() => {});
290
- } catch (err) {
291
- log(`navigation issue (continuing): ${err instanceof Error ? err.message : String(err)}`);
241
+ await withTimeout(Promise.resolve(ch?.close()), 'Chromium close', 3_000);
242
+ } catch {
243
+ /* ignore */
292
244
  }
293
- // Give the sensor JS time to start.
294
- await sleep(3000);
295
- // Drive HUMAN-LIKE interaction until _abck validates (or budget expires).
296
- // Akamai's bmak grades the behavioral SHAPE of trusted input, not just that
297
- // it exists: a robotic linear lattice + a programmatic `window.scrollBy`
298
- // (which is isTrusted=FALSE — a real bot tell) score low. Instead we move the
299
- // cursor along Bezier paths with variable velocity + sub-pixel jitter, scroll
300
- // via a TRUSTED CDP mouseWheel, and emit occasional key events — all through
301
- // CDP Input (isTrusted=true). Note: this raises the behavioral score that
302
- // sits ON TOP of IP reputation; it does NOT overcome a datacenter egress
303
- // (Akamai serves a 200 empty-shell to a datacenter ASN regardless), which is
304
- // what IMPRINT_PROXY (residential egress) is for.
305
- const start = Date.now();
306
- let i = 0;
307
- let status = '?';
308
- let pos = { x: rand(120, 1100), y: rand(120, 600) };
309
- while (Date.now() - start < abckWaitMs) {
310
- try {
311
- const target = { x: rand(60, 1200), y: rand(80, 680) };
312
- for (const p of bezierPoints(pos, target, Math.round(rand(8, 20)))) {
313
- await Input.dispatchMouseEvent({
314
- type: 'mouseMoved',
315
- x: Math.round(p.x),
316
- y: Math.round(p.y),
317
- timestamp: Date.now() / 1000,
318
- });
319
- await sleep(rand(8, 28)); // variable velocity, not a fixed cadence
320
- }
321
- pos = target;
322
- if (i % 3 === 0) {
323
- // TRUSTED wheel scroll via CDP Input (replaces the isTrusted=false
324
- // programmatic window.scrollBy).
325
- await Input.dispatchMouseEvent({
326
- type: 'mouseWheel',
327
- x: Math.round(pos.x),
328
- y: Math.round(pos.y),
329
- deltaX: 0,
330
- deltaY: rand(80, 260),
331
- });
245
+ }
246
+
247
+ async function ensure(): Promise<CdpClient> {
248
+ if (client && bootstrapped) return client;
249
+ try {
250
+ const headed = opts.headed ?? false;
251
+ if (!chrome) {
252
+ log(`launching real ${headed ? 'headed' : 'headless'} Chrome (will navigate ${navUrl})`);
253
+ // Launch at about:blank we MUST attach CDP and override the UA before the
254
+ // first request to the protected origin fires, so we navigate via
255
+ // Page.navigate AFTER the override rather than passing the URL at launch.
256
+ // headless renders offscreen (no display); headed needs one (Xvfb on Linux).
257
+ const launch = chromiumLauncherForTest ?? launchChromium;
258
+ chrome = await withTimeout(
259
+ launch({
260
+ headless: !headed,
261
+ extraArgs: gpuLaunchArgs(),
262
+ ...(headed ? { display: opts.display } : {}),
263
+ }),
264
+ 'Chromium launch',
265
+ cdpCommandTimeoutMs,
266
+ );
267
+ await withTimeout(chrome.ready, 'Chromium CDP readiness', cdpCommandTimeoutMs);
268
+ }
269
+ if (!client) {
270
+ const connectCdp = cdpConnectorForTest ?? ((port: number) => CDP({ port }));
271
+ client = await withTimeout(connectCdp(chrome.port), 'CDP connect', cdpCommandTimeoutMs);
272
+ }
273
+ const { Runtime, Network, Input, Page } = client;
274
+ await withTimeout(Runtime.enable(), 'CDP Runtime.enable', cdpCommandTimeoutMs);
275
+ await withTimeout(Network.enable(), 'CDP Network.enable', cdpCommandTimeoutMs);
276
+ await withTimeout(Page.enable(), 'CDP Page.enable', cdpCommandTimeoutMs);
277
+ // Plant the high-trust seed cookies (the recording's validated Akamai jar)
278
+ // BEFORE navigating, so the first request to the protected origin carries the
279
+ // trusted session. A synthetic mint can reach `_abck~0~` yet still get its
280
+ // `.act` tarpitted; starting from the recording's earned trust is what makes
281
+ // the in-page protected POSTs succeed (the live bmak sensor then keeps it
282
+ // re-validated between calls).
283
+ if (opts.seedCookies && opts.seedCookies.length > 0) {
284
+ let planted = 0;
285
+ for (const c of opts.seedCookies) {
286
+ try {
287
+ await withTimeout(
288
+ Network.setCookie({
289
+ name: c.name,
290
+ value: c.value,
291
+ domain: c.domain,
292
+ path: c.path ?? '/',
293
+ secure: c.secure ?? false,
294
+ httpOnly: c.httpOnly ?? false,
295
+ ...(c.sameSite ? { sameSite: normalizeSameSite(c.sameSite) } : {}),
296
+ ...(typeof c.expires === 'number' && c.expires > 0 ? { expires: c.expires } : {}),
297
+ }),
298
+ `CDP Network.setCookie(${c.name})`,
299
+ shortCdpTimeoutMs,
300
+ );
301
+ planted++;
302
+ } catch {
303
+ // best-effort — a cookie Akamai re-issues on navigate isn't fatal
304
+ }
332
305
  }
333
- if (i % 5 === 2) {
334
- // A keystroke broadens the behavioral feature vector beyond mouse-only.
335
- await Input.dispatchKeyEvent({
336
- type: 'keyDown',
337
- key: 'ArrowDown',
338
- code: 'ArrowDown',
339
- windowsVirtualKeyCode: 40,
340
- });
341
- await sleep(rand(30, 90));
342
- await Input.dispatchKeyEvent({
343
- type: 'keyUp',
344
- key: 'ArrowDown',
345
- code: 'ArrowDown',
346
- windowsVirtualKeyCode: 40,
347
- });
306
+ log(`seeded ${planted}/${opts.seedCookies.length} high-trust cookies before navigate`);
307
+ }
308
+ // Strip the `HeadlessChrome` UA token (Akamai's only headless edge-tell) and
309
+ // send matching client hints — BEFORE navigating to the protected origin.
310
+ try {
311
+ const { result } = await withTimeout(
312
+ Runtime.evaluate({
313
+ expression: 'navigator.userAgent',
314
+ returnByValue: true,
315
+ }),
316
+ 'CDP Runtime.evaluate(navigator.userAgent)',
317
+ cdpCommandTimeoutMs,
318
+ );
319
+ const rawUa = String(result.value ?? '');
320
+ if (rawUa) {
321
+ const override = buildUaOverride(rawUa);
322
+ await withTimeout(
323
+ Network.setUserAgentOverride(override),
324
+ 'CDP Network.setUserAgentOverride',
325
+ cdpCommandTimeoutMs,
326
+ );
327
+ appliedUa = override.userAgent;
328
+ log(`UA override: ${override.userAgent}`);
348
329
  }
349
330
  } catch {
350
- // non-fatal
331
+ // best-effort — a headed launch already has a clean UA
351
332
  }
352
- await sleep(rand(180, 520)); // non-uniform dwell between interaction bursts
353
- const abck = await getCookie(client, '_abck');
354
- status = abck?.split('~')[1] ?? '?';
355
- if (abckIsValidated(abck)) break;
356
- i++;
333
+ // Navigate now (post-override). Page.navigate stalls forever on an Akamai
334
+ // origin ONLY when the UA still says HeadlessChrome; with the override it
335
+ // loads normally. Bound the CDP command and proceed regardless — _abck
336
+ // polling below tolerates a partial load.
337
+ try {
338
+ await withTimeout(
339
+ Page.navigate({ url: navUrl }),
340
+ 'CDP Page.navigate',
341
+ Math.max(1, Math.min(abckWaitMs, 25_000)),
342
+ );
343
+ await withTimeout(
344
+ Page.loadEventFired(),
345
+ 'CDP Page.loadEventFired',
346
+ Math.max(1, Math.min(abckWaitMs, 5_000)),
347
+ ).catch(() => {});
348
+ } catch (err) {
349
+ log(`navigation issue (continuing): ${err instanceof Error ? err.message : String(err)}`);
350
+ }
351
+ // Give the sensor JS time to start.
352
+ await sleep(3000);
353
+ // Drive HUMAN-LIKE interaction until _abck validates (or budget expires).
354
+ // Akamai's bmak grades the behavioral SHAPE of trusted input, not just that
355
+ // it exists: a robotic linear lattice + a programmatic `window.scrollBy`
356
+ // (which is isTrusted=FALSE — a real bot tell) score low. Instead we move the
357
+ // cursor along Bezier paths with variable velocity + sub-pixel jitter, scroll
358
+ // via a TRUSTED CDP mouseWheel, and emit occasional key events — all through
359
+ // CDP Input (isTrusted=true). Note: this raises the behavioral score that
360
+ // sits ON TOP of IP reputation; it does NOT overcome a datacenter egress
361
+ // (Akamai serves a 200 empty-shell to a datacenter ASN regardless), which is
362
+ // what IMPRINT_PROXY (residential egress) is for.
363
+ const start = Date.now();
364
+ let i = 0;
365
+ let status = '?';
366
+ let pos = { x: rand(120, 1100), y: rand(120, 600) };
367
+ while (Date.now() - start < abckWaitMs) {
368
+ try {
369
+ const target = { x: rand(60, 1200), y: rand(80, 680) };
370
+ for (const p of bezierPoints(pos, target, Math.round(rand(8, 20)))) {
371
+ await withTimeout(
372
+ Input.dispatchMouseEvent({
373
+ type: 'mouseMoved',
374
+ x: Math.round(p.x),
375
+ y: Math.round(p.y),
376
+ timestamp: Date.now() / 1000,
377
+ }),
378
+ 'CDP Input.dispatchMouseEvent(mouseMoved)',
379
+ shortCdpTimeoutMs,
380
+ );
381
+ await sleep(rand(8, 28)); // variable velocity, not a fixed cadence
382
+ }
383
+ pos = target;
384
+ if (i % 3 === 0) {
385
+ // TRUSTED wheel scroll via CDP Input (replaces the isTrusted=false
386
+ // programmatic window.scrollBy).
387
+ await withTimeout(
388
+ Input.dispatchMouseEvent({
389
+ type: 'mouseWheel',
390
+ x: Math.round(pos.x),
391
+ y: Math.round(pos.y),
392
+ deltaX: 0,
393
+ deltaY: rand(80, 260),
394
+ }),
395
+ 'CDP Input.dispatchMouseEvent(mouseWheel)',
396
+ shortCdpTimeoutMs,
397
+ );
398
+ }
399
+ if (i % 5 === 2) {
400
+ // A keystroke broadens the behavioral feature vector beyond mouse-only.
401
+ await withTimeout(
402
+ Input.dispatchKeyEvent({
403
+ type: 'keyDown',
404
+ key: 'ArrowDown',
405
+ code: 'ArrowDown',
406
+ windowsVirtualKeyCode: 40,
407
+ }),
408
+ 'CDP Input.dispatchKeyEvent(keyDown)',
409
+ shortCdpTimeoutMs,
410
+ );
411
+ await sleep(rand(30, 90));
412
+ await withTimeout(
413
+ Input.dispatchKeyEvent({
414
+ type: 'keyUp',
415
+ key: 'ArrowDown',
416
+ code: 'ArrowDown',
417
+ windowsVirtualKeyCode: 40,
418
+ }),
419
+ 'CDP Input.dispatchKeyEvent(keyUp)',
420
+ shortCdpTimeoutMs,
421
+ );
422
+ }
423
+ } catch {
424
+ // non-fatal
425
+ }
426
+ await sleep(rand(180, 520)); // non-uniform dwell between interaction bursts
427
+ const abck = await getCookie(client, '_abck');
428
+ status = abck?.split('~')[1] ?? '?';
429
+ if (abckIsValidated(abck)) break;
430
+ i++;
431
+ }
432
+ log(`_abck status after interaction: ~${status}~`);
433
+ bootstrapped = true;
434
+ return client;
435
+ } catch (err) {
436
+ await close();
437
+ throw err;
357
438
  }
358
- log(`_abck status after interaction: ~${status}~`);
359
- bootstrapped = true;
360
- return client;
361
439
  }
362
440
 
363
441
  async function getCookie(c: CdpClient, name: string): Promise<string | undefined> {
364
442
  try {
365
- const { cookies } = await c.Network.getCookies({ urls: [baseOrigin] });
443
+ const { cookies } = await withTimeout(
444
+ c.Network.getCookies({ urls: [baseOrigin] }),
445
+ 'CDP Network.getCookies',
446
+ shortCdpTimeoutMs,
447
+ );
366
448
  return cookies.find((ck: { name: string; value: string }) => ck.name === name)?.value;
367
449
  } catch (err) {
368
450
  // A failed CDP call (dead/crashed browser, closed target) is
@@ -405,7 +487,11 @@ export function createCdpBrowserFetch(opts: CdpBrowserFetchOptions): CdpBrowserF
405
487
  if (requestOrigin !== baseOrigin) {
406
488
  let cookieHeader: string | undefined;
407
489
  try {
408
- const { cookies } = await c.Network.getCookies({ urls: [requestOrigin] });
490
+ const { cookies } = await withTimeout(
491
+ c.Network.getCookies({ urls: [requestOrigin] }),
492
+ 'CDP Network.getCookies(cross-origin)',
493
+ cdpCommandTimeoutMs,
494
+ );
409
495
  if (cookies.length) {
410
496
  cookieHeader = cookies
411
497
  .map((ck: { name: string; value: string }) => `${ck.name}=${ck.value}`)
@@ -453,11 +539,15 @@ export function createCdpBrowserFetch(opts: CdpBrowserFetchOptions): CdpBrowserF
453
539
  return JSON.stringify({ ok: false, error: String(e) });
454
540
  }
455
541
  })()`;
456
- const { result } = await c.Runtime.evaluate({
457
- expression: expr,
458
- awaitPromise: true,
459
- returnByValue: true,
460
- });
542
+ const { result } = await withTimeout(
543
+ c.Runtime.evaluate({
544
+ expression: expr,
545
+ awaitPromise: true,
546
+ returnByValue: true,
547
+ }),
548
+ 'CDP Runtime.evaluate(fetch)',
549
+ Math.max(cdpCommandTimeoutMs, reqTimeoutMs + 5_000),
550
+ );
461
551
  const payload = JSON.parse(result.value as string) as
462
552
  | { ok: true; status: number; body: string; headers: Record<string, string> }
463
553
  | { ok: false; error: string };
@@ -476,7 +566,11 @@ export function createCdpBrowserFetch(opts: CdpBrowserFetchOptions): CdpBrowserF
476
566
  async ensureBootstrapped() {
477
567
  const c = await ensure();
478
568
  try {
479
- const { cookies } = await c.Network.getCookies({ urls: [baseOrigin] });
569
+ const { cookies } = await withTimeout(
570
+ c.Network.getCookies({ urls: [baseOrigin] }),
571
+ 'CDP Network.getCookies',
572
+ cdpCommandTimeoutMs,
573
+ );
480
574
  return cookies.map((ck: { name: string; value: string }) => ({
481
575
  name: ck.name,
482
576
  value: ck.value,
@@ -489,7 +583,11 @@ export function createCdpBrowserFetch(opts: CdpBrowserFetchOptions): CdpBrowserF
489
583
  const c = await ensure();
490
584
  const cookies: MintedJar['cookies'] = [];
491
585
  try {
492
- const res = await c.Network.getCookies({ urls: [baseOrigin] });
586
+ const res = await withTimeout(
587
+ c.Network.getCookies({ urls: [baseOrigin] }),
588
+ 'CDP Network.getCookies',
589
+ cdpCommandTimeoutMs,
590
+ );
493
591
  for (const ck of res.cookies as unknown as Array<Record<string, unknown>>) {
494
592
  cookies.push({
495
593
  name: ck.name as string,
@@ -508,10 +606,14 @@ export function createCdpBrowserFetch(opts: CdpBrowserFetchOptions): CdpBrowserF
508
606
  }
509
607
  let html = '';
510
608
  try {
511
- const { result } = await c.Runtime.evaluate({
512
- expression: 'document.documentElement.outerHTML',
513
- returnByValue: true,
514
- });
609
+ const { result } = await withTimeout(
610
+ c.Runtime.evaluate({
611
+ expression: 'document.documentElement.outerHTML',
612
+ returnByValue: true,
613
+ }),
614
+ 'CDP Runtime.evaluate(document HTML)',
615
+ cdpCommandTimeoutMs,
616
+ );
515
617
  html = String(result.value ?? '');
516
618
  } catch {
517
619
  // best-effort — html_regex captures will miss
@@ -527,21 +629,7 @@ export function createCdpBrowserFetch(opts: CdpBrowserFetchOptions): CdpBrowserF
527
629
  source: 'mint',
528
630
  };
529
631
  },
530
- async close() {
531
- try {
532
- await client?.close();
533
- } catch {
534
- /* ignore */
535
- }
536
- try {
537
- await chrome?.close();
538
- } catch {
539
- /* ignore */
540
- }
541
- client = null;
542
- chrome = null;
543
- bootstrapped = false;
544
- },
632
+ close,
545
633
  };
546
634
  }
547
635
 
@@ -549,6 +637,25 @@ function sleep(ms: number): Promise<void> {
549
637
  return new Promise((r) => setTimeout(r, ms));
550
638
  }
551
639
 
640
+ async function withTimeout<T>(promise: Promise<T>, label: string, timeoutMs: number): Promise<T> {
641
+ if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) return promise;
642
+ let timer: ReturnType<typeof setTimeout> | undefined;
643
+ try {
644
+ return await Promise.race([
645
+ promise,
646
+ new Promise<never>((_, reject) => {
647
+ timer = setTimeout(
648
+ () => reject(new Error(`${label} timed out after ${timeoutMs}ms`)),
649
+ timeoutMs,
650
+ );
651
+ (timer as unknown as { unref?: () => void }).unref?.();
652
+ }),
653
+ ]);
654
+ } finally {
655
+ if (timer) clearTimeout(timer);
656
+ }
657
+ }
658
+
552
659
  /** Uniform random in [min, max). Used to humanize interaction timing/geometry —
553
660
  * bmak flags fixed cadences and uniform step sizes as synthetic. */
554
661
  function rand(min: number, max: number): number {