imprint-mcp 0.7.1 → 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 +2 -0
- package/package.json +1 -1
- package/prompts/auth-compile-agent.md +6 -0
- package/src/imprint/backend-ladder.ts +59 -15
- package/src/imprint/cdp-browser-fetch.ts +50 -7
- package/src/imprint/credential-lock.ts +2 -1
- package/src/imprint/credential-store.ts +41 -4
- package/src/imprint/mcp-cdp-sessions.ts +7 -2
- package/src/imprint/mcp-server.ts +667 -91
- package/src/imprint/mcp-session-recovery.ts +167 -0
- package/src/imprint/runtime.ts +60 -12
- package/src/imprint/types.ts +5 -0
package/README.md
CHANGED
|
@@ -34,6 +34,8 @@ renewed cookies for later calls without overwriting newer saved authentication.
|
|
|
34
34
|
Same-name host-only and domain cookies keep their distinct scopes across replay
|
|
35
35
|
and browser restoration.
|
|
36
36
|
That continuity still needs verification against the requested data operation.
|
|
37
|
+
Local stdio MCP servers can opt in to [session renewal](docs/getting-started.md#renew-authenticated-mcp-sessions)
|
|
38
|
+
for read-only tools, with client approval before sending a new sign-in challenge.
|
|
37
39
|
|
|
38
40
|
## Quick Start
|
|
39
41
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "imprint-mcp",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.2",
|
|
4
4
|
"description": "Teach an AI agent how to use any website. Once. Records a real browser session + narration; generates a deterministic MCP tool plus a DOM-replay playbook fallback.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -217,6 +217,12 @@ semantics.
|
|
|
217
217
|
Verify the session-reuse path and each login/MFA path actually offered by the
|
|
218
218
|
observed flow separately. Do not restart login just to repeat an already
|
|
219
219
|
passing reuse check, and do not claim an untested branch works.
|
|
220
|
+
- For a pause during managed MCP recovery, use `resume: "automatic"` only if the next action can begin
|
|
221
|
+
without waiting for input through the auth tool. A bounded poll for an
|
|
222
|
+
already sent approval request can start while the user approves it with the
|
|
223
|
+
provider. Keep each new approval request in its own action and classify
|
|
224
|
+
its dispatch effect. The MCP host asks for consent before unsafe actions.
|
|
225
|
+
After handing off to the auth tool, callers follow one declared action per call.
|
|
220
226
|
- `persist` names durable credential interfaces that data tools consume later as
|
|
221
227
|
`${credential.NAME}`. Cookies persist automatically. These interface names do
|
|
222
228
|
not constrain your internal capture names. When they differ, set
|
|
@@ -350,6 +350,8 @@ export async function runWithLadder(
|
|
|
350
350
|
/** Per-site CDP browser pool so cdp-replay reuses a live Chrome across
|
|
351
351
|
* calls (~2-5s) instead of launching a fresh one each time (~33s). */
|
|
352
352
|
cdpPool?: Map<string, CdpBrowserFetch>;
|
|
353
|
+
/** A managed continuation must use the browser that produced it. */
|
|
354
|
+
authBrowser?: CdpBrowserFetch;
|
|
353
355
|
/** Per-session memo of the backend that last served each tool. Once set, the
|
|
354
356
|
* next call starts at that backend instead of re-walking the doomed early
|
|
355
357
|
* rungs — the runtime analog of the compile path's `compileWinningBackend`.
|
|
@@ -492,6 +494,7 @@ export async function runWithLadder(
|
|
|
492
494
|
options?.signal,
|
|
493
495
|
options?.onResponse ? onResponse : undefined,
|
|
494
496
|
options?.onPreparedRequest ? onPreparedRequest : undefined,
|
|
497
|
+
options?.authBrowser,
|
|
495
498
|
);
|
|
496
499
|
break;
|
|
497
500
|
case 'stealth-fetch': {
|
|
@@ -1131,6 +1134,7 @@ async function runCdpReplay(
|
|
|
1131
1134
|
signal?: AbortSignal,
|
|
1132
1135
|
onResponse?: (observation: ResponseObservation) => void,
|
|
1133
1136
|
onPreparedRequest?: (observation: PreparedRequestObservation) => void,
|
|
1137
|
+
authBrowser?: CdpBrowserFetch,
|
|
1134
1138
|
): Promise<ToolResult> {
|
|
1135
1139
|
const credentials = credentialOverride ??
|
|
1136
1140
|
(await loadCredentialStore(tool.site, signal)) ?? {
|
|
@@ -1162,7 +1166,31 @@ async function runCdpReplay(
|
|
|
1162
1166
|
.update(bootstrapUrl ?? baseUrl)
|
|
1163
1167
|
.digest('hex'),
|
|
1164
1168
|
);
|
|
1165
|
-
const
|
|
1169
|
+
const pinned =
|
|
1170
|
+
authBrowser && cdpPool
|
|
1171
|
+
? [...cdpPool].find(([, browser]) => browser === authBrowser)?.[0]
|
|
1172
|
+
: undefined;
|
|
1173
|
+
if (authBrowser && pinned === undefined) {
|
|
1174
|
+
return {
|
|
1175
|
+
ok: false,
|
|
1176
|
+
error: 'AUTH_EXPIRED',
|
|
1177
|
+
message: 'The browser for this sign-in has closed.',
|
|
1178
|
+
};
|
|
1179
|
+
}
|
|
1180
|
+
const requestedKey = cdpReplayPoolKey(tool.site, tool.workflow.toolName, bootstrapUrl ?? baseUrl);
|
|
1181
|
+
if (
|
|
1182
|
+
pinned &&
|
|
1183
|
+
pinned !== requestedKey &&
|
|
1184
|
+
tool.workflow.bootstrap?.captures?.some((capture) => capture.source !== 'cookie')
|
|
1185
|
+
) {
|
|
1186
|
+
return {
|
|
1187
|
+
ok: false,
|
|
1188
|
+
error: 'STATE_MISSING',
|
|
1189
|
+
message:
|
|
1190
|
+
'This sign-in action needs captures from a different bootstrap page. Use standalone sign-in.',
|
|
1191
|
+
};
|
|
1192
|
+
}
|
|
1193
|
+
const poolKey = pinned ?? requestedKey;
|
|
1166
1194
|
if (tool.workflow.toolKind !== 'authenticate') await prepareTeachBrowserContext(cdpPool, poolKey);
|
|
1167
1195
|
if (signal?.aborted) throw abortSignalError(signal);
|
|
1168
1196
|
const credentialHash = credentialStateHash(credentials);
|
|
@@ -1247,8 +1275,11 @@ async function runCdpReplay(
|
|
|
1247
1275
|
if (cdpPool) cdpPool.set(poolKey, cf);
|
|
1248
1276
|
}
|
|
1249
1277
|
|
|
1278
|
+
// Existing callers keep their normal timing. Managed auth owns this browser
|
|
1279
|
+
// and closes it when the recovery deadline or client cancellation fires.
|
|
1280
|
+
const authSignal = credentials.authCommitRevision === undefined ? undefined : signal;
|
|
1250
1281
|
try {
|
|
1251
|
-
const jar = await cf.mintJar();
|
|
1282
|
+
const jar = await withAbortSignal(() => cf.mintJar(), authSignal);
|
|
1252
1283
|
if (signal?.aborted) throw abortSignalError(signal);
|
|
1253
1284
|
const observedCookies = jar.cookies.map((c) => ({
|
|
1254
1285
|
name: c.name,
|
|
@@ -1283,16 +1314,26 @@ async function runCdpReplay(
|
|
|
1283
1314
|
return captureResult.result;
|
|
1284
1315
|
}
|
|
1285
1316
|
|
|
1317
|
+
const navigate = cf.navigate?.bind(cf);
|
|
1318
|
+
const snapshotCookies = cf.snapshotCookies?.bind(cf);
|
|
1319
|
+
const fetchImpl: typeof fetch = authSignal
|
|
1320
|
+
? Object.assign(
|
|
1321
|
+
(input: Parameters<typeof fetch>[0], init?: RequestInit) =>
|
|
1322
|
+
withAbortSignal(() => cf.fetchImpl(input, init), authSignal),
|
|
1323
|
+
{ preconnect: cf.fetchImpl.preconnect },
|
|
1324
|
+
)
|
|
1325
|
+
: cf.fetchImpl;
|
|
1286
1326
|
let runtimeCookies: CredentialStore['cookies'] | undefined;
|
|
1287
1327
|
let result = await tool.toolFn(paramsWithDefaults, {
|
|
1288
1328
|
credentials: bootstrappedCredentials,
|
|
1289
1329
|
initialState: { ...callerState, ...captureResult.state },
|
|
1290
|
-
fetchImpl
|
|
1330
|
+
fetchImpl,
|
|
1291
1331
|
browser:
|
|
1292
|
-
|
|
1332
|
+
navigate && snapshotCookies
|
|
1293
1333
|
? {
|
|
1294
|
-
navigate:
|
|
1295
|
-
|
|
1334
|
+
navigate: (...args: Parameters<NonNullable<CdpBrowserFetch['navigate']>>) =>
|
|
1335
|
+
withAbortSignal(() => navigate(...args), authSignal),
|
|
1336
|
+
snapshotCookies: () => withAbortSignal(snapshotCookies, authSignal),
|
|
1296
1337
|
initialCookies: observedCookies,
|
|
1297
1338
|
}
|
|
1298
1339
|
: undefined,
|
|
@@ -1327,9 +1368,10 @@ async function runCdpReplay(
|
|
|
1327
1368
|
}
|
|
1328
1369
|
if (jarLikelyStale(result)) clearJar(cacheDir);
|
|
1329
1370
|
if (result.ok) {
|
|
1371
|
+
if (authSignal) return result;
|
|
1330
1372
|
if (cdpPool && ownsSession) cdpPool.set(poolKey, cf);
|
|
1331
1373
|
try {
|
|
1332
|
-
const postJar = await cf.mintJar();
|
|
1374
|
+
const postJar = await withAbortSignal(() => cf.mintJar(), authSignal);
|
|
1333
1375
|
recordBrowserCookieState(
|
|
1334
1376
|
{ ...credentials, cookies: postJar.cookies },
|
|
1335
1377
|
'cdp-after-request',
|
|
@@ -1390,14 +1432,16 @@ async function runCdpReplay(
|
|
|
1390
1432
|
// A paused auth action is healthy progress. Retain the browser so the next
|
|
1391
1433
|
// declared action sees the same cookies, page, and browser-owned state.
|
|
1392
1434
|
if (ownsSession) cdpPool.set(poolKey, cf);
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1435
|
+
if (credentials.authCommitRevision === undefined) {
|
|
1436
|
+
try {
|
|
1437
|
+
const postJar = await cf.mintJar();
|
|
1438
|
+
saveJar(cacheDir, { ...postJar, credentialStateHash: credentialHash });
|
|
1439
|
+
} catch (err) {
|
|
1440
|
+
clearJar(cacheDir);
|
|
1441
|
+
log(
|
|
1442
|
+
`cdp-replay paused cookie snapshot unavailable: ${err instanceof Error ? err.message : String(err)}`,
|
|
1443
|
+
);
|
|
1444
|
+
}
|
|
1401
1445
|
}
|
|
1402
1446
|
// Deliberately do NOT close cf — the pool retains it for the completion phase.
|
|
1403
1447
|
} else if (cdpPool) {
|
|
@@ -45,6 +45,7 @@ import {
|
|
|
45
45
|
} from './chromium.ts';
|
|
46
46
|
import { recordBrowserCredentialObservation } from './credential-observation.ts';
|
|
47
47
|
import { createLog } from './log.ts';
|
|
48
|
+
import type { BrowserRequestInit } from './types.ts';
|
|
48
49
|
import type { NavigationNetworkResponseMatcher } from './types.ts';
|
|
49
50
|
|
|
50
51
|
const log = createLog('cdp-browser');
|
|
@@ -1106,6 +1107,7 @@ export function buildInPageFetchExpr(
|
|
|
1106
1107
|
body: string | null,
|
|
1107
1108
|
reqTimeoutMs: number,
|
|
1108
1109
|
sameOriginIframeSrc?: string,
|
|
1110
|
+
allowAmbiguousRetry = true,
|
|
1109
1111
|
): string {
|
|
1110
1112
|
const iframeNativeFetchSetup = `
|
|
1111
1113
|
ifr = document.createElement('iframe');
|
|
@@ -1157,10 +1159,12 @@ export function buildInPageFetchExpr(
|
|
|
1157
1159
|
if (ifr) try { ifr.remove(); } catch (_) {}
|
|
1158
1160
|
return { ok: false, error: String(e) };
|
|
1159
1161
|
}`;
|
|
1162
|
+
const primarySetup =
|
|
1163
|
+
!allowAmbiguousRetry && !sameOriginIframeSrc ? iframeNativeFetchSetup : iframeSetup;
|
|
1160
1164
|
return `(async () => {
|
|
1161
|
-
const primary = await (async () => {${fetchOnce(
|
|
1165
|
+
const primary = await (async () => {${fetchOnce(primarySetup)}
|
|
1162
1166
|
})();
|
|
1163
|
-
if (${
|
|
1167
|
+
if (${allowAmbiguousRetry && !sameOriginIframeSrc} && !primary.ok) {
|
|
1164
1168
|
const fallback = await (async () => {${fetchOnce(iframeNativeFetchSetup)}
|
|
1165
1169
|
})();
|
|
1166
1170
|
return JSON.stringify(fallback);
|
|
@@ -1258,10 +1262,12 @@ export function createCdpBrowserFetch(opts: CdpBrowserFetchOptions): CdpBrowserF
|
|
|
1258
1262
|
const activeNetworkBodyReads = new Set<Promise<void>>();
|
|
1259
1263
|
let finishActiveNetworkResponse: ((requestId: string) => void) | undefined;
|
|
1260
1264
|
let navigationInProgress = false;
|
|
1265
|
+
let lifecycle = 0;
|
|
1261
1266
|
let discoveryRequested = false;
|
|
1262
1267
|
let discovery: AuthBrowserDiscovery | undefined;
|
|
1263
1268
|
|
|
1264
1269
|
async function close(): Promise<void> {
|
|
1270
|
+
lifecycle++;
|
|
1265
1271
|
discovery?.close();
|
|
1266
1272
|
discovery = undefined;
|
|
1267
1273
|
const c = client;
|
|
@@ -1300,6 +1306,10 @@ export function createCdpBrowserFetch(opts: CdpBrowserFetchOptions): CdpBrowserF
|
|
|
1300
1306
|
|
|
1301
1307
|
async function ensure(): Promise<CdpClient> {
|
|
1302
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
|
+
};
|
|
1303
1313
|
try {
|
|
1304
1314
|
const headed = opts.headed ?? false;
|
|
1305
1315
|
if (!chrome) {
|
|
@@ -1309,24 +1319,52 @@ export function createCdpBrowserFetch(opts: CdpBrowserFetchOptions): CdpBrowserF
|
|
|
1309
1319
|
// Page.navigate AFTER the override rather than passing the URL at launch.
|
|
1310
1320
|
// headless renders offscreen (no display); headed needs one (Xvfb on Linux).
|
|
1311
1321
|
const launch = chromiumLauncherForTest ?? launchChromium;
|
|
1312
|
-
|
|
1322
|
+
const launched = await withTimeout<LaunchedChromium>(
|
|
1313
1323
|
launch({
|
|
1314
1324
|
headless: !headed,
|
|
1315
1325
|
extraArgs: gpuLaunchArgs(),
|
|
1316
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.');
|
|
1317
1333
|
}),
|
|
1318
1334
|
'Chromium launch',
|
|
1319
1335
|
cdpCommandTimeoutMs,
|
|
1320
1336
|
);
|
|
1321
|
-
|
|
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();
|
|
1322
1346
|
// Register the Chrome PID for exit-time cleanup (leak prevention for
|
|
1323
1347
|
// probe subprocesses that exit before the idle-close timer fires).
|
|
1324
1348
|
if (chrome.process.pid !== undefined) __registerChromePid(chrome.process.pid);
|
|
1325
1349
|
}
|
|
1326
1350
|
if (!client) {
|
|
1327
1351
|
const connectCdp = cdpConnectorForTest ?? ((port: number) => CDP({ port }));
|
|
1328
|
-
|
|
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;
|
|
1329
1366
|
}
|
|
1367
|
+
checkOpen();
|
|
1330
1368
|
const { Runtime, Network, Input, Page } = client;
|
|
1331
1369
|
await withTimeout(Runtime.enable(), 'CDP Runtime.enable', cdpCommandTimeoutMs);
|
|
1332
1370
|
await withTimeout(Network.enable(), 'CDP Network.enable', cdpCommandTimeoutMs);
|
|
@@ -1685,10 +1723,11 @@ export function createCdpBrowserFetch(opts: CdpBrowserFetchOptions): CdpBrowserF
|
|
|
1685
1723
|
i++;
|
|
1686
1724
|
}
|
|
1687
1725
|
log(`_abck status after interaction: ~${status}~`);
|
|
1726
|
+
checkOpen();
|
|
1688
1727
|
bootstrapped = true;
|
|
1689
1728
|
return client;
|
|
1690
1729
|
} catch (err) {
|
|
1691
|
-
await close();
|
|
1730
|
+
if (lifecycle === startedIn) await close();
|
|
1692
1731
|
throw err;
|
|
1693
1732
|
}
|
|
1694
1733
|
}
|
|
@@ -1713,7 +1752,7 @@ export function createCdpBrowserFetch(opts: CdpBrowserFetchOptions): CdpBrowserF
|
|
|
1713
1752
|
|
|
1714
1753
|
const fetchImpl: typeof fetch = (async (
|
|
1715
1754
|
input: string | URL | Request,
|
|
1716
|
-
init?:
|
|
1755
|
+
init?: BrowserRequestInit,
|
|
1717
1756
|
): Promise<Response> => {
|
|
1718
1757
|
const c = await ensure();
|
|
1719
1758
|
const url =
|
|
@@ -1766,6 +1805,7 @@ export function createCdpBrowserFetch(opts: CdpBrowserFetchOptions): CdpBrowserF
|
|
|
1766
1805
|
body,
|
|
1767
1806
|
reqTimeoutMs,
|
|
1768
1807
|
sameOriginIframeSrc,
|
|
1808
|
+
init?.allowAmbiguousRetry,
|
|
1769
1809
|
);
|
|
1770
1810
|
const { result } = await withTimeout(
|
|
1771
1811
|
c.Runtime.evaluate({
|
|
@@ -1908,6 +1948,9 @@ export function createCdpBrowserFetch(opts: CdpBrowserFetchOptions): CdpBrowserF
|
|
|
1908
1948
|
headers: new Headers(payload.headers),
|
|
1909
1949
|
});
|
|
1910
1950
|
}
|
|
1951
|
+
if (init?.allowAmbiguousRetry === false) {
|
|
1952
|
+
throw new Error('The browser could not confirm the request outcome. No retry was sent.');
|
|
1953
|
+
}
|
|
1911
1954
|
// The in-page sibling fetch failed — almost always a genuine CORS block
|
|
1912
1955
|
// (the page can't read the cross-origin response). Fall back to plain
|
|
1913
1956
|
// fetch so the request still goes out (it just won't carry the live
|
|
@@ -95,6 +95,7 @@ export async function withCredentialLock<T>(
|
|
|
95
95
|
directory: string,
|
|
96
96
|
signal: AbortSignal | undefined,
|
|
97
97
|
work: () => Promise<T>,
|
|
98
|
+
pollMs = 20,
|
|
98
99
|
): Promise<T> {
|
|
99
100
|
signal?.throwIfAborted();
|
|
100
101
|
mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
@@ -123,7 +124,7 @@ export async function withCredentialLock<T>(
|
|
|
123
124
|
(other.ticket === owner.ticket && other.nonce < nonce)),
|
|
124
125
|
);
|
|
125
126
|
if (!ahead) break;
|
|
126
|
-
await abortableDelay(
|
|
127
|
+
await abortableDelay(pollMs, signal);
|
|
127
128
|
}
|
|
128
129
|
signal?.throwIfAborted();
|
|
129
130
|
result = await work();
|
|
@@ -694,6 +694,7 @@ export interface SiteCredentialView {
|
|
|
694
694
|
storage: StorageRecord[];
|
|
695
695
|
/** Raw durable material identity, before runtime normalization or env input. */
|
|
696
696
|
revision: string;
|
|
697
|
+
legacy?: true;
|
|
697
698
|
}
|
|
698
699
|
|
|
699
700
|
function credentialRevision(material: Omit<SiteCredentialView, 'revision'>): string {
|
|
@@ -739,6 +740,34 @@ async function withSiteCredentialLock<T>(
|
|
|
739
740
|
});
|
|
740
741
|
}
|
|
741
742
|
|
|
743
|
+
/** Serializes a complete sign-in independently of the credential reads and commits it performs. */
|
|
744
|
+
export async function acquireSiteAuthRecoveryLock(
|
|
745
|
+
site: string,
|
|
746
|
+
signal: AbortSignal,
|
|
747
|
+
): Promise<() => Promise<void>> {
|
|
748
|
+
signal.throwIfAborted();
|
|
749
|
+
const backend = await getCredentialBackend();
|
|
750
|
+
const directory = pathJoin(
|
|
751
|
+
`${credentialLockDirectory(site, backend)}-auth-recovery`,
|
|
752
|
+
createHash('sha256').update(site).digest('hex'),
|
|
753
|
+
);
|
|
754
|
+
return new Promise((resolve, reject) => {
|
|
755
|
+
const held = withCredentialLock(
|
|
756
|
+
directory,
|
|
757
|
+
signal,
|
|
758
|
+
() =>
|
|
759
|
+
new Promise<void>((release) => {
|
|
760
|
+
resolve(() => {
|
|
761
|
+
release();
|
|
762
|
+
return held;
|
|
763
|
+
});
|
|
764
|
+
}),
|
|
765
|
+
250,
|
|
766
|
+
);
|
|
767
|
+
void held.catch(reject);
|
|
768
|
+
});
|
|
769
|
+
}
|
|
770
|
+
|
|
742
771
|
/** Loads everything we know about a site, falling back through the backends.
|
|
743
772
|
* Used at request time by runtime.executeWorkflow. */
|
|
744
773
|
export async function loadSiteCredentials(
|
|
@@ -779,7 +808,7 @@ async function loadSiteCredentialsUnlocked(
|
|
|
779
808
|
values: legacy.values,
|
|
780
809
|
storage: [],
|
|
781
810
|
});
|
|
782
|
-
const result = { ...material, revision: credentialRevision(material) };
|
|
811
|
+
const result = { ...material, revision: credentialRevision(material), legacy: true as const };
|
|
783
812
|
const observation = credentialObservation(result, 'legacy-json');
|
|
784
813
|
if (observation) recordCredentialEvent('load', observation);
|
|
785
814
|
return result;
|
|
@@ -881,14 +910,22 @@ export async function commitSiteAuthState(
|
|
|
881
910
|
site: string,
|
|
882
911
|
cookies: CookieRecord[],
|
|
883
912
|
secrets: Record<string, string>,
|
|
884
|
-
options?: { signal?: AbortSignal },
|
|
913
|
+
options?: { signal?: AbortSignal; expectedRevision?: string },
|
|
885
914
|
): Promise<void> {
|
|
886
915
|
const signal = options?.signal;
|
|
887
916
|
await withSiteCredentialLock(site, signal, async (backend) => {
|
|
888
917
|
signal?.throwIfAborted();
|
|
918
|
+
let nextSecrets = secrets;
|
|
919
|
+
if (options?.expectedRevision !== undefined) {
|
|
920
|
+
const current = await loadSiteCredentialsUnlocked(site, backend);
|
|
921
|
+
if (current.revision !== options.expectedRevision) {
|
|
922
|
+
throw new Error('the saved session changed while this sign-in was running');
|
|
923
|
+
}
|
|
924
|
+
if (current.legacy) nextSecrets = { ...current.values, ...secrets };
|
|
925
|
+
}
|
|
889
926
|
const previousCookies = structuredClone(await backend.getCookies(site));
|
|
890
927
|
const previousSecrets = new Map<string, string | null>();
|
|
891
|
-
for (const name of Object.keys(
|
|
928
|
+
for (const name of Object.keys(nextSecrets)) {
|
|
892
929
|
signal?.throwIfAborted();
|
|
893
930
|
previousSecrets.set(name, await backend.getSecret(site, name));
|
|
894
931
|
}
|
|
@@ -897,7 +934,7 @@ export async function commitSiteAuthState(
|
|
|
897
934
|
signal?.throwIfAborted();
|
|
898
935
|
await backend.setCookies(site, cookies);
|
|
899
936
|
signal?.throwIfAborted();
|
|
900
|
-
for (const [name, value] of Object.entries(
|
|
937
|
+
for (const [name, value] of Object.entries(nextSecrets)) {
|
|
901
938
|
await backend.setSecret(site, name, value);
|
|
902
939
|
signal?.throwIfAborted();
|
|
903
940
|
}
|
|
@@ -19,7 +19,11 @@ export class McpCdpSessions {
|
|
|
19
19
|
}
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
finishCall(
|
|
22
|
+
finishCall(
|
|
23
|
+
site: string,
|
|
24
|
+
toolName: string,
|
|
25
|
+
onIdle?: (browser: CdpBrowserFetch, closing: Promise<void>) => void,
|
|
26
|
+
): void {
|
|
23
27
|
this.beginCall(site, toolName);
|
|
24
28
|
const prefix = cdpReplayPoolKey(site, toolName, '');
|
|
25
29
|
for (const [key, session] of this.pool) {
|
|
@@ -29,7 +33,8 @@ export class McpCdpSessions {
|
|
|
29
33
|
if (this.pool.get(key) !== session) return;
|
|
30
34
|
this.pool.delete(key);
|
|
31
35
|
this.winners.delete(`${site}:${toolName}`);
|
|
32
|
-
|
|
36
|
+
const closing = session.close().catch(() => {});
|
|
37
|
+
onIdle?.(session, closing);
|
|
33
38
|
}, this.idleMs);
|
|
34
39
|
timer.unref();
|
|
35
40
|
this.timers.set(key, timer);
|