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.
- package/README.md +66 -2
- package/package.json +1 -1
- package/prompts/auth-compile-agent.md +173 -22
- package/prompts/compile-agent.md +45 -8
- package/prompts/component-recovery.md +40 -0
- package/prompts/master-teach-decision.md +28 -3
- package/prompts/master-teach-focused-planner.md +18 -5
- package/src/cli.ts +26 -18
- package/src/imprint/agent-tool.ts +15 -0
- package/src/imprint/agent.ts +2 -11
- package/src/imprint/audit.ts +35 -13
- package/src/imprint/auth-browser-discovery.ts +276 -0
- package/src/imprint/auth-compile-agent.ts +513 -224
- package/src/imprint/auth-compile-tools.ts +213 -25
- package/src/imprint/auth-discovery-evidence.ts +81 -0
- package/src/imprint/auth-plan.ts +28 -0
- package/src/imprint/auth-verifier.ts +234 -20
- package/src/imprint/authentication-interaction.ts +137 -0
- package/src/imprint/backend-ladder.ts +271 -43
- package/src/imprint/build-plan.ts +2 -22
- package/src/imprint/cdp-browser-fetch.ts +242 -60
- package/src/imprint/chromium.ts +93 -29
- package/src/imprint/claude-cli-compile.ts +2 -0
- package/src/imprint/codex-cli-compile.ts +99 -46
- package/src/imprint/compile-agent-types.ts +9 -5
- package/src/imprint/compile-agent.ts +33 -0
- package/src/imprint/compile-tool-executor.ts +783 -0
- package/src/imprint/compile-tools.ts +42 -9
- package/src/imprint/compile-verification.ts +2 -0
- package/src/imprint/compile.ts +3 -0
- package/src/imprint/compiler-log.ts +16 -0
- package/src/imprint/component-recovery.ts +120 -0
- package/src/imprint/cookie-jar.ts +117 -7
- package/src/imprint/credential-extract.ts +9 -2
- package/src/imprint/credential-lock.ts +147 -0
- package/src/imprint/credential-observation.ts +261 -0
- package/src/imprint/credential-store.ts +284 -44
- package/src/imprint/live-component-recovery.ts +73 -0
- package/src/imprint/llm.ts +4 -3
- package/src/imprint/log.ts +16 -1
- package/src/imprint/login.ts +9 -0
- package/src/imprint/master-teach-agent-contracts.ts +40 -3
- package/src/imprint/master-teach-agents.ts +229 -14
- package/src/imprint/master-teach-checks.ts +8 -1
- package/src/imprint/master-teach-controller.ts +681 -138
- package/src/imprint/master-teach-prompt-projections.ts +2 -0
- package/src/imprint/mcp-cdp-sessions.ts +7 -2
- package/src/imprint/mcp-compile-server.ts +62 -850
- package/src/imprint/mcp-server.ts +667 -91
- package/src/imprint/mcp-session-recovery.ts +167 -0
- package/src/imprint/native-teach-agents.ts +232 -108
- package/src/imprint/runtime.ts +416 -47
- package/src/imprint/stealth-chromium.ts +67 -7
- package/src/imprint/teach-browser-resources.ts +88 -0
- package/src/imprint/teach-stage-progress.ts +38 -0
- package/src/imprint/terminal-user-prompt.ts +42 -0
- package/src/imprint/types.ts +51 -18
- 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(
|
|
1165
|
+
const primary = await (async () => {${fetchOnce(primarySetup)}
|
|
1047
1166
|
})();
|
|
1048
|
-
if (${
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
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?:
|
|
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 =
|
|
1782
|
-
|
|
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;
|