imprint-mcp 0.7.0 → 0.7.2

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 (58) hide show
  1. package/README.md +66 -2
  2. package/package.json +1 -1
  3. package/prompts/auth-compile-agent.md +173 -22
  4. package/prompts/compile-agent.md +45 -8
  5. package/prompts/component-recovery.md +40 -0
  6. package/prompts/master-teach-decision.md +28 -3
  7. package/prompts/master-teach-focused-planner.md +18 -5
  8. package/src/cli.ts +26 -18
  9. package/src/imprint/agent-tool.ts +15 -0
  10. package/src/imprint/agent.ts +2 -11
  11. package/src/imprint/audit.ts +35 -13
  12. package/src/imprint/auth-browser-discovery.ts +276 -0
  13. package/src/imprint/auth-compile-agent.ts +513 -224
  14. package/src/imprint/auth-compile-tools.ts +213 -25
  15. package/src/imprint/auth-discovery-evidence.ts +81 -0
  16. package/src/imprint/auth-plan.ts +28 -0
  17. package/src/imprint/auth-verifier.ts +234 -20
  18. package/src/imprint/authentication-interaction.ts +137 -0
  19. package/src/imprint/backend-ladder.ts +271 -43
  20. package/src/imprint/build-plan.ts +2 -22
  21. package/src/imprint/cdp-browser-fetch.ts +242 -60
  22. package/src/imprint/chromium.ts +93 -29
  23. package/src/imprint/claude-cli-compile.ts +2 -0
  24. package/src/imprint/codex-cli-compile.ts +99 -46
  25. package/src/imprint/compile-agent-types.ts +9 -5
  26. package/src/imprint/compile-agent.ts +33 -0
  27. package/src/imprint/compile-tool-executor.ts +783 -0
  28. package/src/imprint/compile-tools.ts +42 -9
  29. package/src/imprint/compile-verification.ts +2 -0
  30. package/src/imprint/compile.ts +3 -0
  31. package/src/imprint/compiler-log.ts +16 -0
  32. package/src/imprint/component-recovery.ts +120 -0
  33. package/src/imprint/cookie-jar.ts +117 -7
  34. package/src/imprint/credential-extract.ts +9 -2
  35. package/src/imprint/credential-lock.ts +147 -0
  36. package/src/imprint/credential-observation.ts +261 -0
  37. package/src/imprint/credential-store.ts +284 -44
  38. package/src/imprint/live-component-recovery.ts +73 -0
  39. package/src/imprint/llm.ts +4 -3
  40. package/src/imprint/log.ts +16 -1
  41. package/src/imprint/login.ts +9 -0
  42. package/src/imprint/master-teach-agent-contracts.ts +40 -3
  43. package/src/imprint/master-teach-agents.ts +229 -14
  44. package/src/imprint/master-teach-checks.ts +8 -1
  45. package/src/imprint/master-teach-controller.ts +681 -138
  46. package/src/imprint/master-teach-prompt-projections.ts +2 -0
  47. package/src/imprint/mcp-cdp-sessions.ts +7 -2
  48. package/src/imprint/mcp-compile-server.ts +62 -850
  49. package/src/imprint/mcp-server.ts +667 -91
  50. package/src/imprint/mcp-session-recovery.ts +167 -0
  51. package/src/imprint/native-teach-agents.ts +232 -108
  52. package/src/imprint/runtime.ts +416 -47
  53. package/src/imprint/stealth-chromium.ts +67 -7
  54. package/src/imprint/teach-browser-resources.ts +88 -0
  55. package/src/imprint/teach-stage-progress.ts +38 -0
  56. package/src/imprint/terminal-user-prompt.ts +42 -0
  57. package/src/imprint/types.ts +51 -18
  58. package/src/imprint/user-interaction.ts +836 -0
@@ -28,8 +28,14 @@
28
28
  * trusted browser session.
29
29
  */
30
30
 
31
+ import { randomUUID } from 'node:crypto';
31
32
  import CDP from 'chrome-remote-interface';
32
33
  import { getDomain } from 'tldts';
34
+ import {
35
+ type AuthBrowserAction,
36
+ AuthBrowserDiscovery,
37
+ type AuthBrowserObservation,
38
+ } from './auth-browser-discovery.ts';
33
39
  import { AKAMAI_SENSOR_COOKIE, abckFlag, isAbckValidated } from './bot-defense.ts';
34
40
  import {
35
41
  __registerChromePid,
@@ -37,7 +43,9 @@ import {
37
43
  launchChromium,
38
44
  proxyUrl,
39
45
  } from './chromium.ts';
46
+ import { recordBrowserCredentialObservation } from './credential-observation.ts';
40
47
  import { createLog } from './log.ts';
48
+ import type { BrowserRequestInit } from './types.ts';
41
49
  import type { NavigationNetworkResponseMatcher } from './types.ts';
42
50
 
43
51
  const log = createLog('cdp-browser');
@@ -53,7 +61,10 @@ export interface MintedJar {
53
61
  httpOnly?: boolean;
54
62
  secure?: boolean;
55
63
  sameSite?: string;
64
+ hostOnly?: boolean;
56
65
  }>;
66
+ /** Factual outcome of the initial cookie planting; never contains values. */
67
+ seedCookieDiagnostics?: CookieSeedDiagnostics;
57
68
  /** The exact UA the bootstrap browser presented (HeadlessChrome stripped) —
58
69
  * replay fetches MUST send this verbatim or Akamai drops the jar. */
59
70
  ua: string;
@@ -83,6 +94,9 @@ export interface MintedJar {
83
94
  * seeded from the user's recorded session. Used only for accurate diagnostics
84
95
  * (both now carry `html`, so emptiness no longer distinguishes them). */
85
96
  source?: 'mint' | 'recording';
97
+ /** Durable credential input for a retained replay snapshot. A new credential
98
+ * state supersedes the entire snapshot, including cookies it had deleted. */
99
+ credentialStateHash?: string;
86
100
  }
87
101
 
88
102
  /** A session is replay-safe when `_abck` is validated (`~0~`) OR the Akamai
@@ -188,9 +202,14 @@ export interface CdpBrowserFetch {
188
202
  /** Snapshot every cookie in this dedicated browser, including cookies minted
189
203
  * on origins reached by redirects rather than the bootstrap origin. */
190
204
  snapshotCookies?(): Promise<MintedJar['cookies']>;
205
+ /** Read diagnostic state only when a client already exists. Unlike ordinary
206
+ * snapshotCookies, this never launches or bootstraps a replacement browser. */
207
+ snapshotExistingCookies?(): Promise<MintedJar['cookies'] | undefined>;
191
208
  /** Inspect the currently rendered page without navigating or mutating browser
192
209
  * state. Cookie values are intentionally excluded from this diagnostic view. */
193
210
  inspectPage?(): Promise<CdpPageSnapshot>;
211
+ /** Agent-selected discovery on this same browser, with ongoing network capture. */
212
+ interactBrowser?(action: AuthBrowserAction): Promise<AuthBrowserObservation>;
194
213
  /** Force the bootstrap navigation + `_abck` validation now; returns the
195
214
  * session cookies so callers can read session tokens (CSRF) for `${state.X}`. */
196
215
  ensureBootstrapped(): Promise<Array<{ name: string; value: string }>>;
@@ -215,6 +234,7 @@ export interface CdpPageSnapshot {
215
234
  httpOnly?: boolean;
216
235
  secure?: boolean;
217
236
  sameSite?: string;
237
+ hostOnly?: boolean;
218
238
  }>;
219
239
  }
220
240
 
@@ -489,6 +509,7 @@ export interface CdpBrowserFetchOptions {
489
509
  httpOnly?: boolean;
490
510
  sameSite?: string;
491
511
  expires?: number;
512
+ hostOnly?: boolean;
492
513
  }>;
493
514
  /** Browser storage to restore before page scripts run. Entries are scoped to
494
515
  * their recorded origin and applied on every matching document navigation. */
@@ -509,6 +530,101 @@ export interface CdpBrowserFetchOptions {
509
530
  }
510
531
 
511
532
  type CdpClient = Awaited<ReturnType<typeof CDP>>;
533
+
534
+ /** Read the target's complete cookie partition, rather than current-page URLs. */
535
+ export async function readCdpCookieSnapshot(
536
+ network: Pick<CdpClient['Network'], 'getAllCookies'>,
537
+ timeoutMs: number,
538
+ ): Promise<MintedJar['cookies']> {
539
+ // Network.getCookies({}) filters by the page and subframes. Keeping this
540
+ // command on the existing target also avoids reading another browser context.
541
+ const { cookies } = await withTimeout(
542
+ network.getAllCookies(),
543
+ 'CDP Network.getAllCookies',
544
+ timeoutMs,
545
+ );
546
+ return cookies.map((cookie) => ({
547
+ name: cookie.name,
548
+ value: cookie.value,
549
+ domain: cookie.domain,
550
+ path: cookie.path,
551
+ expires: cookie.expires > 0 ? cookie.expires : undefined,
552
+ httpOnly: cookie.httpOnly,
553
+ secure: cookie.secure,
554
+ sameSite: cookie.sameSite,
555
+ hostOnly: !cookie.domain.startsWith('.'),
556
+ }));
557
+ }
558
+
559
+ export interface CookieSeedDiagnostics {
560
+ attempted: number;
561
+ accepted: number;
562
+ rejected: number;
563
+ failed: number;
564
+ /** Positions identify input scopes without exposing names, paths or values. */
565
+ issues: Array<{ index: number; outcome: 'rejected' | 'error' }>;
566
+ issuesTruncated: boolean;
567
+ }
568
+
569
+ /** Preserve browser cookie scope and distinguish a rejected CDP command from
570
+ * a fulfilled one. A stale optional cookie does not make navigation fail. */
571
+ export async function seedCdpCookies(
572
+ network: Pick<CdpClient['Network'], 'setCookie'>,
573
+ cookies: NonNullable<CdpBrowserFetchOptions['seedCookies']>,
574
+ timeoutMs: number,
575
+ ): Promise<CookieSeedDiagnostics> {
576
+ const diagnostics: CookieSeedDiagnostics = {
577
+ attempted: cookies.length,
578
+ accepted: 0,
579
+ rejected: 0,
580
+ failed: 0,
581
+ issues: [],
582
+ issuesTruncated: false,
583
+ };
584
+ const issue = (index: number, outcome: 'rejected' | 'error'): void => {
585
+ if (diagnostics.issues.length < 50) diagnostics.issues.push({ index, outcome });
586
+ else diagnostics.issuesTruncated = true;
587
+ };
588
+ for (const [index, cookie] of cookies.entries()) {
589
+ try {
590
+ const hostOnly = cookie.hostOnly ?? !cookie.domain.startsWith('.');
591
+ const url = new URL(
592
+ `${cookie.secure ? 'https' : 'http'}://${cookie.domain.replace(/^\./, '')}/`,
593
+ );
594
+ url.pathname = cookie.path ?? '/';
595
+ const result = await withTimeout(
596
+ network.setCookie({
597
+ name: cookie.name,
598
+ value: cookie.value,
599
+ url: url.href,
600
+ ...(hostOnly ? {} : { domain: `.${cookie.domain.replace(/^\./, '')}` }),
601
+ path: cookie.path ?? '/',
602
+ secure: cookie.secure ?? false,
603
+ httpOnly: cookie.httpOnly ?? false,
604
+ ...(cookie.sameSite ? { sameSite: normalizeSameSite(cookie.sameSite) } : {}),
605
+ ...(typeof cookie.expires === 'number' && cookie.expires > 0
606
+ ? { expires: cookie.expires }
607
+ : {}),
608
+ }),
609
+ `CDP Network.setCookie(${index})`,
610
+ timeoutMs,
611
+ );
612
+ if (result.success === true) diagnostics.accepted++;
613
+ else if (result.success === false) {
614
+ diagnostics.rejected++;
615
+ issue(index, 'rejected');
616
+ } else {
617
+ diagnostics.failed++;
618
+ issue(index, 'error');
619
+ }
620
+ } catch {
621
+ diagnostics.failed++;
622
+ issue(index, 'error');
623
+ }
624
+ }
625
+ return diagnostics;
626
+ }
627
+
512
628
  type LaunchedChromium = Awaited<ReturnType<typeof launchChromium>>;
513
629
  type ChromiumLauncher = (opts: Parameters<typeof launchChromium>[0]) => Promise<LaunchedChromium>;
514
630
  type CdpConnector = (port: number) => Promise<CdpClient>;
@@ -991,6 +1107,7 @@ export function buildInPageFetchExpr(
991
1107
  body: string | null,
992
1108
  reqTimeoutMs: number,
993
1109
  sameOriginIframeSrc?: string,
1110
+ allowAmbiguousRetry = true,
994
1111
  ): string {
995
1112
  const iframeNativeFetchSetup = `
996
1113
  ifr = document.createElement('iframe');
@@ -1042,10 +1159,12 @@ export function buildInPageFetchExpr(
1042
1159
  if (ifr) try { ifr.remove(); } catch (_) {}
1043
1160
  return { ok: false, error: String(e) };
1044
1161
  }`;
1162
+ const primarySetup =
1163
+ !allowAmbiguousRetry && !sameOriginIframeSrc ? iframeNativeFetchSetup : iframeSetup;
1045
1164
  return `(async () => {
1046
- const primary = await (async () => {${fetchOnce(iframeSetup)}
1165
+ const primary = await (async () => {${fetchOnce(primarySetup)}
1047
1166
  })();
1048
- if (${sameOriginIframeSrc ? 'false' : 'true'} && !primary.ok) {
1167
+ if (${allowAmbiguousRetry && !sameOriginIframeSrc} && !primary.ok) {
1049
1168
  const fallback = await (async () => {${fetchOnce(iframeNativeFetchSetup)}
1050
1169
  })();
1051
1170
  return JSON.stringify(fallback);
@@ -1106,6 +1225,7 @@ function buildUaOverride(rawUa: string): {
1106
1225
 
1107
1226
  /** Create a CDP-browser-backed fetch. Lazily launches Chrome on first use. */
1108
1227
  export function createCdpBrowserFetch(opts: CdpBrowserFetchOptions): CdpBrowserFetch {
1228
+ const diagnosticBrowserId = randomUUID();
1109
1229
  const baseOrigin = new URL(opts.baseUrl).origin;
1110
1230
  // Navigate the bootstrap page when declared; otherwise the base URL — but
1111
1231
  // never an obvious API/.act endpoint (opening one cold yields an error page
@@ -1123,6 +1243,7 @@ export function createCdpBrowserFetch(opts: CdpBrowserFetchOptions): CdpBrowserF
1123
1243
  let bootstrapped = false;
1124
1244
  let appliedUa: string | undefined;
1125
1245
  let bootstrapResponseHeaders: Record<string, string> = {};
1246
+ let seedCookieDiagnostics: CookieSeedDiagnostics | undefined;
1126
1247
  let currentPageUrl = navUrl;
1127
1248
  let mainFrameId: string | undefined;
1128
1249
  let lastDocumentResponse: {
@@ -1141,8 +1262,14 @@ export function createCdpBrowserFetch(opts: CdpBrowserFetchOptions): CdpBrowserF
1141
1262
  const activeNetworkBodyReads = new Set<Promise<void>>();
1142
1263
  let finishActiveNetworkResponse: ((requestId: string) => void) | undefined;
1143
1264
  let navigationInProgress = false;
1265
+ let lifecycle = 0;
1266
+ let discoveryRequested = false;
1267
+ let discovery: AuthBrowserDiscovery | undefined;
1144
1268
 
1145
1269
  async function close(): Promise<void> {
1270
+ lifecycle++;
1271
+ discovery?.close();
1272
+ discovery = undefined;
1146
1273
  const c = client;
1147
1274
  const ch = chrome;
1148
1275
  const chromePid = chrome?.process.pid;
@@ -1151,6 +1278,7 @@ export function createCdpBrowserFetch(opts: CdpBrowserFetchOptions): CdpBrowserF
1151
1278
  bootstrapped = false;
1152
1279
  appliedUa = undefined;
1153
1280
  bootstrapResponseHeaders = {};
1281
+ seedCookieDiagnostics = undefined;
1154
1282
  currentPageUrl = navUrl;
1155
1283
  mainFrameId = undefined;
1156
1284
  lastDocumentResponse = null;
@@ -1178,6 +1306,10 @@ export function createCdpBrowserFetch(opts: CdpBrowserFetchOptions): CdpBrowserF
1178
1306
 
1179
1307
  async function ensure(): Promise<CdpClient> {
1180
1308
  if (client && bootstrapped) return client;
1309
+ const startedIn = lifecycle;
1310
+ const checkOpen = () => {
1311
+ if (lifecycle !== startedIn) throw new Error('Browser closed during CDP startup.');
1312
+ };
1181
1313
  try {
1182
1314
  const headed = opts.headed ?? false;
1183
1315
  if (!chrome) {
@@ -1187,27 +1319,57 @@ export function createCdpBrowserFetch(opts: CdpBrowserFetchOptions): CdpBrowserF
1187
1319
  // Page.navigate AFTER the override rather than passing the URL at launch.
1188
1320
  // headless renders offscreen (no display); headed needs one (Xvfb on Linux).
1189
1321
  const launch = chromiumLauncherForTest ?? launchChromium;
1190
- chrome = await withTimeout(
1322
+ const launched = await withTimeout<LaunchedChromium>(
1191
1323
  launch({
1192
1324
  headless: !headed,
1193
1325
  extraArgs: gpuLaunchArgs(),
1194
1326
  ...(headed ? { display: opts.display } : {}),
1327
+ }).then(async (launched) => {
1328
+ // Closing a late handle rejects readiness even if no caller adopted it.
1329
+ void launched.ready.catch(() => {});
1330
+ if (lifecycle === startedIn) return launched;
1331
+ await launched.close();
1332
+ throw new Error('Browser closed during Chromium launch.');
1195
1333
  }),
1196
1334
  'Chromium launch',
1197
1335
  cdpCommandTimeoutMs,
1198
1336
  );
1199
- await withTimeout(chrome.ready, 'Chromium CDP readiness', cdpCommandTimeoutMs);
1337
+ // Close can run after the launch promise resolves and before this await
1338
+ // resumes. Keep the handle local until ownership has been checked.
1339
+ if (lifecycle !== startedIn) {
1340
+ await launched.close();
1341
+ checkOpen();
1342
+ }
1343
+ chrome = launched;
1344
+ await withTimeout(launched.ready, 'Chromium CDP readiness', cdpCommandTimeoutMs);
1345
+ checkOpen();
1200
1346
  // Register the Chrome PID for exit-time cleanup (leak prevention for
1201
1347
  // probe subprocesses that exit before the idle-close timer fires).
1202
1348
  if (chrome.process.pid !== undefined) __registerChromePid(chrome.process.pid);
1203
1349
  }
1204
1350
  if (!client) {
1205
1351
  const connectCdp = cdpConnectorForTest ?? ((port: number) => CDP({ port }));
1206
- client = await withTimeout(connectCdp(chrome.port), 'CDP connect', cdpCommandTimeoutMs);
1352
+ const connected = await withTimeout<CdpClient>(
1353
+ connectCdp(chrome.port).then(async (connected) => {
1354
+ if (lifecycle === startedIn) return connected;
1355
+ await connected.close();
1356
+ throw new Error('Browser closed during CDP connect.');
1357
+ }),
1358
+ 'CDP connect',
1359
+ cdpCommandTimeoutMs,
1360
+ );
1361
+ if (lifecycle !== startedIn) {
1362
+ await connected.close();
1363
+ checkOpen();
1364
+ }
1365
+ client = connected;
1207
1366
  }
1367
+ checkOpen();
1208
1368
  const { Runtime, Network, Input, Page } = client;
1209
1369
  await withTimeout(Runtime.enable(), 'CDP Runtime.enable', cdpCommandTimeoutMs);
1210
1370
  await withTimeout(Network.enable(), 'CDP Network.enable', cdpCommandTimeoutMs);
1371
+ if (discoveryRequested && !discovery)
1372
+ discovery = new AuthBrowserDiscovery(client, cdpCommandTimeoutMs);
1211
1373
  await withTimeout(Page.enable(), 'CDP Page.enable', cdpCommandTimeoutMs);
1212
1374
  if (typeof Page.getFrameTree === 'function') {
1213
1375
  const tree = await withTimeout(
@@ -1248,6 +1410,12 @@ export function createCdpBrowserFetch(opts: CdpBrowserFetchOptions): CdpBrowserF
1248
1410
  request?: { method?: string; url?: string };
1249
1411
  }) => {
1250
1412
  if (!event.requestId) return;
1413
+ recordBrowserCredentialObservation({
1414
+ event: 'browser-request',
1415
+ browserId: diagnosticBrowserId,
1416
+ requestId: event.requestId,
1417
+ request: event.request,
1418
+ });
1251
1419
  requestSequence++;
1252
1420
  requestFacts.set(event.requestId, {
1253
1421
  method: event.request?.method ?? '',
@@ -1268,6 +1436,22 @@ export function createCdpBrowserFetch(opts: CdpBrowserFetchOptions): CdpBrowserF
1268
1436
  }
1269
1437
  },
1270
1438
  );
1439
+ if (
1440
+ process.env.IMPRINT_CREDENTIAL_DIAGNOSTICS_PATH &&
1441
+ typeof Network.requestWillBeSentExtraInfo === 'function'
1442
+ ) {
1443
+ Network.requestWillBeSentExtraInfo(
1444
+ (event: { requestId?: string; headers?: Record<string, unknown> }) => {
1445
+ if (!event.requestId) return;
1446
+ recordBrowserCredentialObservation({
1447
+ event: 'browser-wire',
1448
+ browserId: diagnosticBrowserId,
1449
+ requestId: event.requestId,
1450
+ headers: event.headers,
1451
+ });
1452
+ },
1453
+ );
1454
+ }
1271
1455
  const onResponseReceived = (event: {
1272
1456
  requestId?: string;
1273
1457
  loaderId?: string;
@@ -1283,6 +1467,12 @@ export function createCdpBrowserFetch(opts: CdpBrowserFetchOptions): CdpBrowserF
1283
1467
  if (!url) return;
1284
1468
  const headers = normalizeCdpResponseHeaders(event.response?.headers ?? {});
1285
1469
  if (event.requestId) {
1470
+ recordBrowserCredentialObservation({
1471
+ event: 'browser-response',
1472
+ browserId: diagnosticBrowserId,
1473
+ requestId: event.requestId,
1474
+ status: event.response?.status,
1475
+ });
1286
1476
  const facts = requestFacts.get(event.requestId);
1287
1477
  const observed = {
1288
1478
  requestId: event.requestId,
@@ -1362,29 +1552,10 @@ export function createCdpBrowserFetch(opts: CdpBrowserFetchOptions): CdpBrowserF
1362
1552
  // the in-page protected POSTs succeed (the live bmak sensor then keeps it
1363
1553
  // re-validated between calls).
1364
1554
  if (opts.seedCookies && opts.seedCookies.length > 0) {
1365
- let planted = 0;
1366
- for (const c of opts.seedCookies) {
1367
- try {
1368
- await withTimeout(
1369
- Network.setCookie({
1370
- name: c.name,
1371
- value: c.value,
1372
- domain: c.domain,
1373
- path: c.path ?? '/',
1374
- secure: c.secure ?? false,
1375
- httpOnly: c.httpOnly ?? false,
1376
- ...(c.sameSite ? { sameSite: normalizeSameSite(c.sameSite) } : {}),
1377
- ...(typeof c.expires === 'number' && c.expires > 0 ? { expires: c.expires } : {}),
1378
- }),
1379
- `CDP Network.setCookie(${c.name})`,
1380
- shortCdpTimeoutMs,
1381
- );
1382
- planted++;
1383
- } catch {
1384
- // best-effort — a cookie Akamai re-issues on navigate isn't fatal
1385
- }
1386
- }
1387
- log(`seeded ${planted}/${opts.seedCookies.length} high-trust cookies before navigate`);
1555
+ seedCookieDiagnostics = await seedCdpCookies(Network, opts.seedCookies, shortCdpTimeoutMs);
1556
+ log(
1557
+ `seeded ${seedCookieDiagnostics.accepted}/${seedCookieDiagnostics.attempted} cookies before navigate; rejected=${seedCookieDiagnostics.rejected} failed=${seedCookieDiagnostics.failed}`,
1558
+ );
1388
1559
  }
1389
1560
  // Strip the `HeadlessChrome` UA token (Akamai's only headless edge-tell) and
1390
1561
  // send matching client hints — BEFORE navigating to the protected origin.
@@ -1552,10 +1723,11 @@ export function createCdpBrowserFetch(opts: CdpBrowserFetchOptions): CdpBrowserF
1552
1723
  i++;
1553
1724
  }
1554
1725
  log(`_abck status after interaction: ~${status}~`);
1726
+ checkOpen();
1555
1727
  bootstrapped = true;
1556
1728
  return client;
1557
1729
  } catch (err) {
1558
- await close();
1730
+ if (lifecycle === startedIn) await close();
1559
1731
  throw err;
1560
1732
  }
1561
1733
  }
@@ -1580,7 +1752,7 @@ export function createCdpBrowserFetch(opts: CdpBrowserFetchOptions): CdpBrowserF
1580
1752
 
1581
1753
  const fetchImpl: typeof fetch = (async (
1582
1754
  input: string | URL | Request,
1583
- init?: RequestInit,
1755
+ init?: BrowserRequestInit,
1584
1756
  ): Promise<Response> => {
1585
1757
  const c = await ensure();
1586
1758
  const url =
@@ -1633,6 +1805,7 @@ export function createCdpBrowserFetch(opts: CdpBrowserFetchOptions): CdpBrowserF
1633
1805
  body,
1634
1806
  reqTimeoutMs,
1635
1807
  sameOriginIframeSrc,
1808
+ init?.allowAmbiguousRetry,
1636
1809
  );
1637
1810
  const { result } = await withTimeout(
1638
1811
  c.Runtime.evaluate({
@@ -1677,6 +1850,21 @@ export function createCdpBrowserFetch(opts: CdpBrowserFetchOptions): CdpBrowserF
1677
1850
  // Route the cross-origin plain fetch through the SAME proxy as the browser
1678
1851
  // (Bun fetch `proxy` opt) so its egress IP matches the in-page traffic.
1679
1852
  const proxy = proxyUrl();
1853
+ const diagnosticRequestId = randomUUID();
1854
+ recordBrowserCredentialObservation({
1855
+ event: 'browser-request',
1856
+ browserId: diagnosticBrowserId,
1857
+ requestId: diagnosticRequestId,
1858
+ transport: 'browser-http',
1859
+ request: { method, url: fullUrl },
1860
+ });
1861
+ recordBrowserCredentialObservation({
1862
+ event: 'browser-wire',
1863
+ browserId: diagnosticBrowserId,
1864
+ requestId: diagnosticRequestId,
1865
+ transport: 'browser-http',
1866
+ headers: outHeaders,
1867
+ });
1680
1868
  const xResp = await globalThis.fetch(fullUrl, {
1681
1869
  method,
1682
1870
  headers: outHeaders,
@@ -1684,6 +1872,13 @@ export function createCdpBrowserFetch(opts: CdpBrowserFetchOptions): CdpBrowserF
1684
1872
  signal: init?.signal ?? undefined,
1685
1873
  ...(proxy ? { proxy } : {}),
1686
1874
  } as RequestInit);
1875
+ recordBrowserCredentialObservation({
1876
+ event: 'browser-response',
1877
+ browserId: diagnosticBrowserId,
1878
+ requestId: diagnosticRequestId,
1879
+ transport: 'browser-http',
1880
+ status: xResp.status,
1881
+ });
1687
1882
  // Re-inject the cross-origin response's Set-Cookie back into the browser's
1688
1883
  // cookie jar — ONLY when the auth compile agent opted in (the recorded login
1689
1884
  // carries its session through a cross-origin Set-Cookie). The plain fetch
@@ -1753,6 +1948,9 @@ export function createCdpBrowserFetch(opts: CdpBrowserFetchOptions): CdpBrowserF
1753
1948
  headers: new Headers(payload.headers),
1754
1949
  });
1755
1950
  }
1951
+ if (init?.allowAmbiguousRetry === false) {
1952
+ throw new Error('The browser could not confirm the request outcome. No retry was sent.');
1953
+ }
1756
1954
  // The in-page sibling fetch failed — almost always a genuine CORS block
1757
1955
  // (the page can't read the cross-origin response). Fall back to plain
1758
1956
  // fetch so the request still goes out (it just won't carry the live
@@ -1778,35 +1976,8 @@ export function createCdpBrowserFetch(opts: CdpBrowserFetchOptions): CdpBrowserF
1778
1976
  });
1779
1977
  }) as typeof fetch;
1780
1978
 
1781
- const snapshotAllCookies = async (c: CdpClient): Promise<MintedJar['cookies']> => {
1782
- const cookies: MintedJar['cookies'] = [];
1783
- try {
1784
- // With no URL filter CDP returns the dedicated browser's complete jar.
1785
- // This includes host-only cookies minted on relying-party origins reached
1786
- // through redirects, which a bootstrap-origin-only query cannot see.
1787
- const res = await withTimeout(
1788
- c.Network.getCookies({}),
1789
- 'CDP Network.getCookies(all origins)',
1790
- cdpCommandTimeoutMs,
1791
- );
1792
- for (const ck of res.cookies as unknown as Array<Record<string, unknown>>) {
1793
- cookies.push({
1794
- name: ck.name as string,
1795
- value: ck.value as string,
1796
- domain: ck.domain as string,
1797
- path: (ck.path as string) ?? '/',
1798
- expires:
1799
- typeof ck.expires === 'number' && ck.expires > 0 ? (ck.expires as number) : undefined,
1800
- httpOnly: ck.httpOnly as boolean | undefined,
1801
- secure: ck.secure as boolean | undefined,
1802
- sameSite: ck.sameSite as string | undefined,
1803
- });
1804
- }
1805
- } catch {
1806
- // best-effort
1807
- }
1808
- return cookies;
1809
- };
1979
+ const snapshotAllCookies = (c: CdpClient): Promise<MintedJar['cookies']> =>
1980
+ readCdpCookieSnapshot(c.Network, cdpCommandTimeoutMs);
1810
1981
 
1811
1982
  const readCurrentPageUrl = async (c: CdpClient): Promise<string> => {
1812
1983
  try {
@@ -2385,6 +2556,12 @@ export function createCdpBrowserFetch(opts: CdpBrowserFetchOptions): CdpBrowserF
2385
2556
  return {
2386
2557
  fetchImpl,
2387
2558
  navigate,
2559
+ async interactBrowser(action) {
2560
+ discoveryRequested = true;
2561
+ const c = await ensure();
2562
+ discovery ??= new AuthBrowserDiscovery(c, cdpCommandTimeoutMs);
2563
+ return discovery.interact(action, buildNavigationClickTargetExpression);
2564
+ },
2388
2565
  async inspectPage(): Promise<CdpPageSnapshot> {
2389
2566
  const c = await ensure();
2390
2567
  const evaluation = await withTimeout(
@@ -2409,12 +2586,16 @@ export function createCdpBrowserFetch(opts: CdpBrowserFetchOptions): CdpBrowserF
2409
2586
  httpOnly: cookie.httpOnly,
2410
2587
  secure: cookie.secure,
2411
2588
  sameSite: cookie.sameSite,
2589
+ hostOnly: cookie.hostOnly,
2412
2590
  })),
2413
2591
  };
2414
2592
  },
2415
2593
  async snapshotCookies() {
2416
2594
  return snapshotAllCookies(await ensure());
2417
2595
  },
2596
+ async snapshotExistingCookies() {
2597
+ return client ? snapshotAllCookies(client) : undefined;
2598
+ },
2418
2599
  async ensureBootstrapped() {
2419
2600
  const c = await ensure();
2420
2601
  try {
@@ -2458,6 +2639,7 @@ export function createCdpBrowserFetch(opts: CdpBrowserFetchOptions): CdpBrowserF
2458
2639
  abckFlag: abckFlag(abck),
2459
2640
  validated: jarCookiesValidated(cookies),
2460
2641
  source: 'mint',
2642
+ ...(seedCookieDiagnostics ? { seedCookieDiagnostics } : {}),
2461
2643
  };
2462
2644
  },
2463
2645
  close,
@@ -2561,7 +2743,7 @@ export function parseSetCookieForCdp(setCookie: string, requestUrl: string): Cdp
2561
2743
  const i = seg.indexOf('=');
2562
2744
  const k = (i < 0 ? seg : seg.slice(0, i)).trim().toLowerCase();
2563
2745
  const v = i < 0 ? '' : seg.slice(i + 1).trim();
2564
- if (k === 'domain' && v) ck.domain = v;
2746
+ if (k === 'domain' && v) ck.domain = `.${v.replace(/^\./, '')}`;
2565
2747
  else if (k === 'path' && v) ck.path = v;
2566
2748
  else if (k === 'secure') ck.secure = true;
2567
2749
  else if (k === 'httponly') ck.httpOnly = true;