@flrande/bak-extension 0.6.14 → 0.6.16
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/dist/.bak-e2e-build-stamp +1 -1
- package/dist/background.global.js +406 -92
- package/dist/content.global.js +9 -3
- package/dist/manifest.json +1 -1
- package/dist/popup.global.js +11 -8
- package/dist/popup.html +13 -3
- package/package.json +2 -2
- package/public/popup.html +13 -3
- package/src/background.ts +537 -258
- package/src/content.ts +9 -3
- package/src/dynamic-data-tools.ts +2 -2
- package/src/network-debugger.ts +90 -12
- package/src/popup.ts +27 -9
- package/src/session-binding-storage.ts +7 -1
- package/src/session-binding.ts +41 -11
package/src/background.ts
CHANGED
|
@@ -25,18 +25,18 @@ import {
|
|
|
25
25
|
clearNetworkEntries,
|
|
26
26
|
dropNetworkCapture,
|
|
27
27
|
ensureNetworkDebugger,
|
|
28
|
-
exportHar,
|
|
29
|
-
getNetworkEntry,
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
28
|
+
exportHar,
|
|
29
|
+
getNetworkEntry,
|
|
30
|
+
getReplayableNetworkRequest,
|
|
31
|
+
latestNetworkTimestamp,
|
|
32
|
+
listNetworkEntries,
|
|
33
|
+
recentNetworkSampleIds,
|
|
34
|
+
searchNetworkEntries,
|
|
35
|
+
waitForNetworkEntry
|
|
35
36
|
} from './network-debugger.js';
|
|
36
37
|
import { isSupportedAutomationUrl } from './url-policy.js';
|
|
37
38
|
import { computeReconnectDelayMs } from './reconnect.js';
|
|
38
39
|
import { resolveSessionBindingStateMap, STORAGE_KEY_SESSION_BINDINGS } from './session-binding-storage.js';
|
|
39
|
-
import { containsRedactionMarker } from './privacy.js';
|
|
40
40
|
import {
|
|
41
41
|
type SessionBindingBrowser,
|
|
42
42
|
type SessionBindingColor,
|
|
@@ -70,12 +70,14 @@ interface ExtensionConfig {
|
|
|
70
70
|
debugRichText: boolean;
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
-
interface RuntimeErrorDetails {
|
|
74
|
-
message: string;
|
|
75
|
-
context: 'config' | 'socket' | 'request' | 'parse';
|
|
76
|
-
at: number;
|
|
77
|
-
}
|
|
78
|
-
|
|
73
|
+
interface RuntimeErrorDetails {
|
|
74
|
+
message: string;
|
|
75
|
+
context: 'config' | 'socket' | 'request' | 'parse';
|
|
76
|
+
at: number;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
type PopupSessionBindingStatus = 'attached' | 'window-only' | 'detached';
|
|
80
|
+
|
|
79
81
|
interface PopupSessionBindingSummary {
|
|
80
82
|
id: string;
|
|
81
83
|
label: string;
|
|
@@ -85,7 +87,7 @@ interface PopupSessionBindingSummary {
|
|
|
85
87
|
activeTabUrl: string | null;
|
|
86
88
|
windowId: number | null;
|
|
87
89
|
groupId: number | null;
|
|
88
|
-
|
|
90
|
+
status: PopupSessionBindingStatus;
|
|
89
91
|
lastBindingUpdateAt: number | null;
|
|
90
92
|
lastBindingUpdateReason: string | null;
|
|
91
93
|
}
|
|
@@ -107,14 +109,15 @@ interface PopupState {
|
|
|
107
109
|
extensionVersion: string;
|
|
108
110
|
lastBindingUpdateAt: number | null;
|
|
109
111
|
lastBindingUpdateReason: string | null;
|
|
110
|
-
sessionBindings: {
|
|
111
|
-
count: number;
|
|
112
|
-
attachedCount: number;
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
}
|
|
112
|
+
sessionBindings: {
|
|
113
|
+
count: number;
|
|
114
|
+
attachedCount: number;
|
|
115
|
+
windowOnlyCount: number;
|
|
116
|
+
detachedCount: number;
|
|
117
|
+
tabCount: number;
|
|
118
|
+
items: PopupSessionBindingSummary[];
|
|
119
|
+
};
|
|
120
|
+
}
|
|
118
121
|
|
|
119
122
|
const DEFAULT_PORT = 17373;
|
|
120
123
|
const STORAGE_KEY_TOKEN = 'pairToken';
|
|
@@ -344,44 +347,99 @@ async function loadSessionBindingState(bindingId: string): Promise<SessionBindin
|
|
|
344
347
|
return stateMap[bindingId] ?? null;
|
|
345
348
|
}
|
|
346
349
|
|
|
347
|
-
async function listSessionBindingStates(): Promise<SessionBindingRecord[]> {
|
|
348
|
-
return Object.values(await loadSessionBindingStateMap());
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
350
|
+
async function listSessionBindingStates(): Promise<SessionBindingRecord[]> {
|
|
351
|
+
return Object.values(await loadSessionBindingStateMap());
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function collectPopupSessionBindingTabIds(state: SessionBindingRecord): number[] {
|
|
355
|
+
return [
|
|
356
|
+
...new Set(state.tabIds.concat([state.activeTabId, state.primaryTabId].filter((value): value is number => typeof value === 'number')))
|
|
357
|
+
];
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
async function inspectPopupSessionBinding(
|
|
361
|
+
state: SessionBindingRecord
|
|
362
|
+
): Promise<{ summary: PopupSessionBindingSummary; prune: boolean }> {
|
|
363
|
+
const trackedTabs = (
|
|
364
|
+
await Promise.all(
|
|
365
|
+
collectPopupSessionBindingTabIds(state).map(async (tabId) => {
|
|
366
|
+
return await sessionBindingBrowser.getTab(tabId);
|
|
367
|
+
})
|
|
368
|
+
)
|
|
369
|
+
).filter((tab): tab is SessionBindingTab => tab !== null);
|
|
370
|
+
const liveWindow = typeof state.windowId === 'number' ? await sessionBindingBrowser.getWindow(state.windowId) : null;
|
|
371
|
+
const activeTab = trackedTabs.find((tab) => tab.id === state.activeTabId) ?? trackedTabs.find((tab) => tab.active) ?? trackedTabs[0] ?? null;
|
|
372
|
+
const status: PopupSessionBindingStatus = trackedTabs.length > 0 ? 'attached' : liveWindow ? 'window-only' : 'detached';
|
|
373
|
+
const bindingUpdate = bindingUpdateMetadata.get(state.id);
|
|
374
|
+
return {
|
|
375
|
+
summary: {
|
|
376
|
+
id: state.id,
|
|
377
|
+
label: state.label,
|
|
378
|
+
tabCount: trackedTabs.length,
|
|
379
|
+
activeTabId: activeTab?.id ?? null,
|
|
380
|
+
activeTabTitle: activeTab?.title ?? null,
|
|
381
|
+
activeTabUrl: activeTab?.url ?? null,
|
|
382
|
+
windowId: activeTab?.windowId ?? trackedTabs[0]?.windowId ?? liveWindow?.id ?? state.windowId,
|
|
383
|
+
groupId:
|
|
384
|
+
trackedTabs.length > 0
|
|
385
|
+
? activeTab?.groupId ?? trackedTabs.find((tab) => tab.groupId !== null)?.groupId ?? state.groupId
|
|
386
|
+
: null,
|
|
387
|
+
status,
|
|
388
|
+
lastBindingUpdateAt: bindingUpdate?.at ?? null,
|
|
389
|
+
lastBindingUpdateReason: bindingUpdate?.reason ?? null
|
|
390
|
+
},
|
|
391
|
+
prune: status === 'detached'
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
async function summarizeSessionBindings(): Promise<PopupState['sessionBindings']> {
|
|
396
|
+
const statusRank: Record<PopupSessionBindingStatus, number> = {
|
|
397
|
+
attached: 0,
|
|
398
|
+
'window-only': 1,
|
|
399
|
+
detached: 2
|
|
400
|
+
};
|
|
401
|
+
const items = await mutateSessionBindingStateMap(async (stateMap) => {
|
|
402
|
+
const inspected = await Promise.all(
|
|
403
|
+
Object.entries(stateMap).map(async ([bindingId, state]) => {
|
|
404
|
+
return {
|
|
405
|
+
bindingId,
|
|
406
|
+
inspected: await inspectPopupSessionBinding(state)
|
|
407
|
+
};
|
|
408
|
+
})
|
|
409
|
+
);
|
|
410
|
+
for (const entry of inspected) {
|
|
411
|
+
if (entry.inspected.prune) {
|
|
412
|
+
delete stateMap[entry.bindingId];
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
return inspected
|
|
416
|
+
.filter((entry) => !entry.inspected.prune)
|
|
417
|
+
.map((entry) => entry.inspected.summary)
|
|
418
|
+
.sort((left, right) => {
|
|
419
|
+
const byStatus = statusRank[left.status] - statusRank[right.status];
|
|
420
|
+
if (byStatus !== 0) {
|
|
421
|
+
return byStatus;
|
|
422
|
+
}
|
|
423
|
+
const byUpdate = (right.lastBindingUpdateAt ?? 0) - (left.lastBindingUpdateAt ?? 0);
|
|
424
|
+
if (byUpdate !== 0) {
|
|
425
|
+
return byUpdate;
|
|
426
|
+
}
|
|
427
|
+
return left.label.localeCompare(right.label);
|
|
428
|
+
});
|
|
429
|
+
});
|
|
373
430
|
return {
|
|
374
431
|
count: items.length,
|
|
375
|
-
attachedCount: items.filter((item) =>
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
items
|
|
379
|
-
|
|
380
|
-
}
|
|
381
|
-
|
|
432
|
+
attachedCount: items.filter((item) => item.status === 'attached').length,
|
|
433
|
+
windowOnlyCount: items.filter((item) => item.status === 'window-only').length,
|
|
434
|
+
detachedCount: items.filter((item) => item.status === 'detached').length,
|
|
435
|
+
tabCount: items.reduce((sum, item) => sum + item.tabCount, 0),
|
|
436
|
+
items
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
|
|
382
440
|
async function buildPopupState(): Promise<PopupState> {
|
|
383
441
|
const config = await getConfig();
|
|
384
|
-
const sessionBindings = await summarizeSessionBindings(
|
|
442
|
+
const sessionBindings = await summarizeSessionBindings();
|
|
385
443
|
const reconnectRemainingMs = nextReconnectAt === null ? null : Math.max(0, nextReconnectAt - Date.now());
|
|
386
444
|
let connectionState: PopupState['connectionState'];
|
|
387
445
|
if (!config.token) {
|
|
@@ -780,10 +838,18 @@ async function withTab(target: { tabId?: number; bindingId?: string } = {}, opti
|
|
|
780
838
|
return validate(tab);
|
|
781
839
|
}
|
|
782
840
|
|
|
783
|
-
async function captureAlignedTabScreenshot(tab: chrome.tabs.Tab): Promise<
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
841
|
+
async function captureAlignedTabScreenshot(tab: chrome.tabs.Tab): Promise<{
|
|
842
|
+
captureStatus: 'complete' | 'degraded' | 'skipped';
|
|
843
|
+
imageData?: string;
|
|
844
|
+
captureError?: {
|
|
845
|
+
code?: string;
|
|
846
|
+
message: string;
|
|
847
|
+
details?: Record<string, unknown>;
|
|
848
|
+
};
|
|
849
|
+
}> {
|
|
850
|
+
if (typeof tab.id !== 'number' || typeof tab.windowId !== 'number') {
|
|
851
|
+
throw toError('E_NOT_FOUND', 'Tab screenshot requires tab id and window id');
|
|
852
|
+
}
|
|
787
853
|
|
|
788
854
|
const activeTabs = await chrome.tabs.query({ windowId: tab.windowId, active: true });
|
|
789
855
|
const activeTab = activeTabs[0];
|
|
@@ -793,13 +859,24 @@ async function captureAlignedTabScreenshot(tab: chrome.tabs.Tab): Promise<string
|
|
|
793
859
|
await chrome.tabs.update(tab.id, { active: true });
|
|
794
860
|
await new Promise((resolve) => setTimeout(resolve, 80));
|
|
795
861
|
}
|
|
796
|
-
|
|
797
|
-
try {
|
|
798
|
-
return
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
862
|
+
|
|
863
|
+
try {
|
|
864
|
+
return {
|
|
865
|
+
captureStatus: 'complete',
|
|
866
|
+
imageData: await chrome.tabs.captureVisibleTab(tab.windowId, { format: 'png' })
|
|
867
|
+
};
|
|
868
|
+
} catch (error) {
|
|
869
|
+
return {
|
|
870
|
+
captureStatus: 'degraded',
|
|
871
|
+
captureError: {
|
|
872
|
+
code: 'E_CAPTURE_FAILED',
|
|
873
|
+
message: error instanceof Error ? error.message : String(error)
|
|
874
|
+
}
|
|
875
|
+
};
|
|
876
|
+
} finally {
|
|
877
|
+
if (shouldSwitch && typeof activeTab?.id === 'number') {
|
|
878
|
+
try {
|
|
879
|
+
await chrome.tabs.update(activeTab.id, { active: true });
|
|
803
880
|
} catch {
|
|
804
881
|
// Ignore restore errors if the original tab no longer exists.
|
|
805
882
|
}
|
|
@@ -954,10 +1031,10 @@ async function executePageWorld<T>(
|
|
|
954
1031
|
target,
|
|
955
1032
|
world: 'MAIN',
|
|
956
1033
|
args: [
|
|
957
|
-
{
|
|
958
|
-
action,
|
|
959
|
-
scope,
|
|
960
|
-
framePath,
|
|
1034
|
+
{
|
|
1035
|
+
action,
|
|
1036
|
+
scope,
|
|
1037
|
+
framePath,
|
|
961
1038
|
expr: typeof params.expr === 'string' ? params.expr : '',
|
|
962
1039
|
path: typeof params.path === 'string' ? params.path : '',
|
|
963
1040
|
resolver: typeof params.resolver === 'string' ? params.resolver : undefined,
|
|
@@ -965,13 +1042,15 @@ async function executePageWorld<T>(
|
|
|
965
1042
|
method: typeof params.method === 'string' ? params.method : 'GET',
|
|
966
1043
|
headers: typeof params.headers === 'object' && params.headers !== null ? params.headers : undefined,
|
|
967
1044
|
body: typeof params.body === 'string' ? params.body : undefined,
|
|
968
|
-
contentType: typeof params.contentType === 'string' ? params.contentType : undefined,
|
|
969
|
-
mode: params.mode === 'json' ? 'json' : 'raw',
|
|
970
|
-
maxBytes: typeof params.maxBytes === 'number' ? params.maxBytes : undefined,
|
|
971
|
-
timeoutMs: typeof params.timeoutMs === 'number' ? params.timeoutMs : undefined
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
1045
|
+
contentType: typeof params.contentType === 'string' ? params.contentType : undefined,
|
|
1046
|
+
mode: params.mode === 'json' ? 'json' : 'raw',
|
|
1047
|
+
maxBytes: typeof params.maxBytes === 'number' ? params.maxBytes : undefined,
|
|
1048
|
+
timeoutMs: typeof params.timeoutMs === 'number' ? params.timeoutMs : undefined,
|
|
1049
|
+
fullResponse: params.fullResponse === true,
|
|
1050
|
+
auth: params.auth === 'manual' || params.auth === 'off' ? params.auth : 'auto'
|
|
1051
|
+
}
|
|
1052
|
+
],
|
|
1053
|
+
func: async (payload) => {
|
|
975
1054
|
const serializeValue = (value: unknown, maxBytes?: number) => {
|
|
976
1055
|
let cloned: unknown;
|
|
977
1056
|
try {
|
|
@@ -1071,10 +1150,10 @@ async function executePageWorld<T>(
|
|
|
1071
1150
|
return current;
|
|
1072
1151
|
};
|
|
1073
1152
|
|
|
1074
|
-
const resolveExtractValue = (
|
|
1075
|
-
targetWindow: Window & { eval: (expr: string) => unknown },
|
|
1076
|
-
path: string,
|
|
1077
|
-
resolver: unknown
|
|
1153
|
+
const resolveExtractValue = (
|
|
1154
|
+
targetWindow: Window & { eval: (expr: string) => unknown },
|
|
1155
|
+
path: string,
|
|
1156
|
+
resolver: unknown
|
|
1078
1157
|
): { resolver: 'globalThis' | 'lexical'; value: unknown } => {
|
|
1079
1158
|
const strategy = resolver === 'globalThis' || resolver === 'lexical' ? resolver : 'auto';
|
|
1080
1159
|
const lexicalExpression = buildPathExpression(path);
|
|
@@ -1101,11 +1180,82 @@ async function executePageWorld<T>(
|
|
|
1101
1180
|
throw error;
|
|
1102
1181
|
}
|
|
1103
1182
|
}
|
|
1104
|
-
return { resolver: 'lexical', value: readLexical() };
|
|
1105
|
-
};
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1183
|
+
return { resolver: 'lexical', value: readLexical() };
|
|
1184
|
+
};
|
|
1185
|
+
|
|
1186
|
+
const findHeaderName = (headers: Record<string, string>, name: string): string | undefined =>
|
|
1187
|
+
Object.keys(headers).find((key) => key.toLowerCase() === name.toLowerCase());
|
|
1188
|
+
|
|
1189
|
+
const findCookieValue = (cookieString: string, name: string): string | undefined => {
|
|
1190
|
+
const targetName = `${name}=`;
|
|
1191
|
+
for (const segment of cookieString.split(';')) {
|
|
1192
|
+
const trimmed = segment.trim();
|
|
1193
|
+
if (trimmed.toLowerCase().startsWith(targetName.toLowerCase())) {
|
|
1194
|
+
return trimmed.slice(targetName.length);
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
return undefined;
|
|
1198
|
+
};
|
|
1199
|
+
|
|
1200
|
+
const buildJsonSummary = (
|
|
1201
|
+
value: unknown
|
|
1202
|
+
): { schema?: { columns: Array<{ key: string; label: string }> }; mappedRows?: Array<Record<string, unknown>> } => {
|
|
1203
|
+
const rowsCandidate = (() => {
|
|
1204
|
+
if (Array.isArray(value)) {
|
|
1205
|
+
return value;
|
|
1206
|
+
}
|
|
1207
|
+
if (typeof value !== 'object' || value === null) {
|
|
1208
|
+
return null;
|
|
1209
|
+
}
|
|
1210
|
+
const record = value as Record<string, unknown>;
|
|
1211
|
+
for (const key of ['data', 'rows', 'results', 'items']) {
|
|
1212
|
+
if (Array.isArray(record[key])) {
|
|
1213
|
+
return record[key] as unknown[];
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
return null;
|
|
1217
|
+
})();
|
|
1218
|
+
if (Array.isArray(rowsCandidate) && rowsCandidate.length > 0) {
|
|
1219
|
+
const objectRows = rowsCandidate
|
|
1220
|
+
.filter((row): row is Record<string, unknown> => typeof row === 'object' && row !== null && !Array.isArray(row))
|
|
1221
|
+
.slice(0, 25);
|
|
1222
|
+
if (objectRows.length > 0) {
|
|
1223
|
+
const columns = [...new Set(objectRows.flatMap((row) => Object.keys(row)))].slice(0, 20);
|
|
1224
|
+
return {
|
|
1225
|
+
schema: {
|
|
1226
|
+
columns: columns.map((label, index) => ({
|
|
1227
|
+
key: `col_${index + 1}`,
|
|
1228
|
+
label
|
|
1229
|
+
}))
|
|
1230
|
+
},
|
|
1231
|
+
mappedRows: objectRows.map((row) => {
|
|
1232
|
+
const mapped: Record<string, unknown> = {};
|
|
1233
|
+
for (const column of columns) {
|
|
1234
|
+
mapped[column] = row[column];
|
|
1235
|
+
}
|
|
1236
|
+
return mapped;
|
|
1237
|
+
})
|
|
1238
|
+
};
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
|
1242
|
+
const columns = Object.keys(value as Record<string, unknown>).slice(0, 20);
|
|
1243
|
+
if (columns.length > 0) {
|
|
1244
|
+
return {
|
|
1245
|
+
schema: {
|
|
1246
|
+
columns: columns.map((label, index) => ({
|
|
1247
|
+
key: `col_${index + 1}`,
|
|
1248
|
+
label
|
|
1249
|
+
}))
|
|
1250
|
+
}
|
|
1251
|
+
};
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
return {};
|
|
1255
|
+
};
|
|
1256
|
+
|
|
1257
|
+
try {
|
|
1258
|
+
const targetWindow = payload.scope === 'main' ? window : payload.scope === 'current' ? resolveFrameWindow(payload.framePath ?? []) : window;
|
|
1109
1259
|
if (payload.action === 'eval') {
|
|
1110
1260
|
const evaluator = (targetWindow as Window & { eval: (expr: string) => unknown }).eval;
|
|
1111
1261
|
const serialized = serializeValue(evaluator(payload.expr), payload.maxBytes);
|
|
@@ -1121,14 +1271,53 @@ async function executePageWorld<T>(
|
|
|
1121
1271
|
bytes: serialized.bytes,
|
|
1122
1272
|
resolver: extracted.resolver
|
|
1123
1273
|
};
|
|
1124
|
-
}
|
|
1125
|
-
if (payload.action === 'fetch') {
|
|
1126
|
-
const headers = { ...(payload.headers ?? {}) } as Record<string, string>;
|
|
1127
|
-
if (payload.contentType && !headers
|
|
1128
|
-
headers['Content-Type'] = payload.contentType;
|
|
1129
|
-
}
|
|
1130
|
-
const
|
|
1131
|
-
const
|
|
1274
|
+
}
|
|
1275
|
+
if (payload.action === 'fetch') {
|
|
1276
|
+
const headers = { ...(payload.headers ?? {}) } as Record<string, string>;
|
|
1277
|
+
if (payload.contentType && !findHeaderName(headers, 'Content-Type')) {
|
|
1278
|
+
headers['Content-Type'] = payload.contentType;
|
|
1279
|
+
}
|
|
1280
|
+
const fullResponse = payload.fullResponse === true;
|
|
1281
|
+
const authApplied: string[] = [];
|
|
1282
|
+
const authSources = new Set<string>();
|
|
1283
|
+
const requestUrl = new URL(payload.url, targetWindow.location.href);
|
|
1284
|
+
const sameOrigin = requestUrl.origin === targetWindow.location.origin;
|
|
1285
|
+
const authMode = payload.auth === 'manual' || payload.auth === 'off' ? payload.auth : 'auto';
|
|
1286
|
+
const maybeApplyHeader = (name: string, value: string | undefined, source: string): void => {
|
|
1287
|
+
if (!value || findHeaderName(headers, name)) {
|
|
1288
|
+
return;
|
|
1289
|
+
}
|
|
1290
|
+
headers[name] = value;
|
|
1291
|
+
authApplied.push(name);
|
|
1292
|
+
authSources.add(source);
|
|
1293
|
+
};
|
|
1294
|
+
if (sameOrigin && authMode === 'auto') {
|
|
1295
|
+
const xsrfCookie = findCookieValue(targetWindow.document.cookie ?? '', 'XSRF-TOKEN');
|
|
1296
|
+
if (xsrfCookie) {
|
|
1297
|
+
maybeApplyHeader('X-XSRF-TOKEN', decodeURIComponent(xsrfCookie), 'cookie:XSRF-TOKEN');
|
|
1298
|
+
}
|
|
1299
|
+
const metaTokens = [
|
|
1300
|
+
{
|
|
1301
|
+
selector: 'meta[name="xsrf-token"], meta[name="x-xsrf-token"]',
|
|
1302
|
+
header: 'X-XSRF-TOKEN',
|
|
1303
|
+
source: 'meta:xsrf-token'
|
|
1304
|
+
},
|
|
1305
|
+
{
|
|
1306
|
+
selector: 'meta[name="csrf-token"], meta[name="csrf_token"], meta[name="_csrf"]',
|
|
1307
|
+
header: 'X-CSRF-TOKEN',
|
|
1308
|
+
source: 'meta:csrf-token'
|
|
1309
|
+
}
|
|
1310
|
+
];
|
|
1311
|
+
for (const token of metaTokens) {
|
|
1312
|
+
const meta = targetWindow.document.querySelector<HTMLMetaElement>(token.selector);
|
|
1313
|
+
const content = meta?.content?.trim();
|
|
1314
|
+
if (content) {
|
|
1315
|
+
maybeApplyHeader(token.header, content, token.source);
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
const controller = typeof AbortController === 'function' ? new AbortController() : null;
|
|
1320
|
+
const timeoutId =
|
|
1132
1321
|
controller && typeof payload.timeoutMs === 'number' && payload.timeoutMs > 0
|
|
1133
1322
|
? window.setTimeout(() => controller.abort(), payload.timeoutMs)
|
|
1134
1323
|
: null;
|
|
@@ -1153,43 +1342,56 @@ async function executePageWorld<T>(
|
|
|
1153
1342
|
return {
|
|
1154
1343
|
url: targetWindow.location.href,
|
|
1155
1344
|
framePath: payload.scope === 'current' ? payload.framePath ?? [] : [],
|
|
1156
|
-
value: (() => {
|
|
1157
|
-
const encoder = typeof TextEncoder === 'function' ? new TextEncoder() : null;
|
|
1158
|
-
const decoder = typeof TextDecoder === 'function' ? new TextDecoder() : null;
|
|
1159
|
-
const previewLimit = typeof payload.maxBytes === 'number' && payload.maxBytes > 0 ? payload.maxBytes : 8192;
|
|
1160
|
-
const encodedBody = encoder ? encoder.encode(bodyText) : null;
|
|
1161
|
-
const bodyBytes = encodedBody ? encodedBody.byteLength : bodyText.length;
|
|
1162
|
-
const truncated = bodyBytes > previewLimit;
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1345
|
+
value: (() => {
|
|
1346
|
+
const encoder = typeof TextEncoder === 'function' ? new TextEncoder() : null;
|
|
1347
|
+
const decoder = typeof TextDecoder === 'function' ? new TextDecoder() : null;
|
|
1348
|
+
const previewLimit = !fullResponse && typeof payload.maxBytes === 'number' && payload.maxBytes > 0 ? payload.maxBytes : 8192;
|
|
1349
|
+
const encodedBody = encoder ? encoder.encode(bodyText) : null;
|
|
1350
|
+
const bodyBytes = encodedBody ? encodedBody.byteLength : bodyText.length;
|
|
1351
|
+
const truncated = !fullResponse && bodyBytes > previewLimit;
|
|
1352
|
+
const previewText =
|
|
1353
|
+
fullResponse
|
|
1354
|
+
? bodyText
|
|
1355
|
+
: encodedBody && decoder
|
|
1356
|
+
? decoder.decode(encodedBody.subarray(0, Math.min(encodedBody.byteLength, previewLimit)))
|
|
1357
|
+
: truncated
|
|
1358
|
+
? bodyText.slice(0, previewLimit)
|
|
1359
|
+
: bodyText;
|
|
1360
|
+
const result: Record<string, unknown> = {
|
|
1361
|
+
url: response.url,
|
|
1362
|
+
status: response.status,
|
|
1363
|
+
ok: response.ok,
|
|
1364
|
+
headers: headerMap,
|
|
1365
|
+
contentType: response.headers.get('content-type') ?? undefined,
|
|
1366
|
+
bytes: bodyBytes,
|
|
1367
|
+
truncated,
|
|
1368
|
+
authApplied: authApplied.length > 0 ? authApplied : undefined,
|
|
1369
|
+
authSources: authSources.size > 0 ? [...authSources] : undefined
|
|
1370
|
+
};
|
|
1371
|
+
if (payload.mode === 'json') {
|
|
1372
|
+
const parsedJson = bodyText ? JSON.parse(bodyText) : undefined;
|
|
1373
|
+
const summary = buildJsonSummary(parsedJson);
|
|
1374
|
+
if (fullResponse || !truncated) {
|
|
1375
|
+
result.json = parsedJson;
|
|
1376
|
+
} else {
|
|
1377
|
+
result.degradedReason = 'response body exceeded max-bytes and was summarized';
|
|
1378
|
+
}
|
|
1379
|
+
if (summary.schema) {
|
|
1380
|
+
result.schema = summary.schema;
|
|
1381
|
+
}
|
|
1382
|
+
if (summary.mappedRows) {
|
|
1383
|
+
result.mappedRows = summary.mappedRows;
|
|
1384
|
+
}
|
|
1385
|
+
} else {
|
|
1386
|
+
result.bodyText = previewText;
|
|
1387
|
+
if (truncated) {
|
|
1388
|
+
result.degradedReason = 'response body exceeded max-bytes and was truncated';
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
return result;
|
|
1392
|
+
})()
|
|
1393
|
+
};
|
|
1394
|
+
}
|
|
1193
1395
|
throw { code: 'E_NOT_FOUND', message: `Unsupported page world action: ${payload.action}` };
|
|
1194
1396
|
} catch (error) {
|
|
1195
1397
|
return {
|
|
@@ -1269,23 +1471,20 @@ function filterNetworkEntrySections(entry: NetworkEntry, include: unknown): Netw
|
|
|
1269
1471
|
return clone;
|
|
1270
1472
|
}
|
|
1271
1473
|
|
|
1272
|
-
function
|
|
1273
|
-
if (!
|
|
1274
|
-
return undefined;
|
|
1275
|
-
}
|
|
1276
|
-
const headers: Record<string, string> = {};
|
|
1277
|
-
for (const [name, value] of Object.entries(
|
|
1278
|
-
const normalizedName = name.toLowerCase();
|
|
1279
|
-
if (REPLAY_FORBIDDEN_HEADER_NAMES.has(normalizedName) || normalizedName.startsWith('sec-')) {
|
|
1280
|
-
continue;
|
|
1281
|
-
}
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
}
|
|
1287
|
-
return Object.keys(headers).length > 0 ? headers : undefined;
|
|
1288
|
-
}
|
|
1474
|
+
function replayHeadersFromRequestHeaders(requestHeaders: Record<string, string> | undefined): Record<string, string> | undefined {
|
|
1475
|
+
if (!requestHeaders) {
|
|
1476
|
+
return undefined;
|
|
1477
|
+
}
|
|
1478
|
+
const headers: Record<string, string> = {};
|
|
1479
|
+
for (const [name, value] of Object.entries(requestHeaders)) {
|
|
1480
|
+
const normalizedName = name.toLowerCase();
|
|
1481
|
+
if (REPLAY_FORBIDDEN_HEADER_NAMES.has(normalizedName) || normalizedName.startsWith('sec-')) {
|
|
1482
|
+
continue;
|
|
1483
|
+
}
|
|
1484
|
+
headers[name] = value;
|
|
1485
|
+
}
|
|
1486
|
+
return Object.keys(headers).length > 0 ? headers : undefined;
|
|
1487
|
+
}
|
|
1289
1488
|
|
|
1290
1489
|
function collectTimestampMatchesFromText(text: string, source: TimestampEvidenceCandidate['source'], patterns?: string[]): TimestampEvidenceCandidate[] {
|
|
1291
1490
|
const regexes = (
|
|
@@ -1403,7 +1602,7 @@ function latestTimestampFromCandidates(
|
|
|
1403
1602
|
return latest;
|
|
1404
1603
|
}
|
|
1405
1604
|
|
|
1406
|
-
function computeFreshnessAssessment(input: {
|
|
1605
|
+
function computeFreshnessAssessment(input: {
|
|
1407
1606
|
latestInlineDataTimestamp: number | null;
|
|
1408
1607
|
latestPageDataTimestamp: number | null;
|
|
1409
1608
|
latestNetworkDataTimestamp: number | null;
|
|
@@ -1440,10 +1639,79 @@ function computeFreshnessAssessment(input: {
|
|
|
1440
1639
|
.filter((value): value is number => typeof value === 'number');
|
|
1441
1640
|
if (staleSignals.length > 0 && staleSignals.every((value) => now - value > input.staleWindowMs)) {
|
|
1442
1641
|
return 'stale';
|
|
1443
|
-
}
|
|
1444
|
-
return 'unknown';
|
|
1445
|
-
}
|
|
1446
|
-
|
|
1642
|
+
}
|
|
1643
|
+
return 'unknown';
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1646
|
+
function freshnessCategoryPriority(category: PageFreshnessResult['evidence']['classifiedTimestamps'][number]['category']): number {
|
|
1647
|
+
switch (category) {
|
|
1648
|
+
case 'data':
|
|
1649
|
+
return 0;
|
|
1650
|
+
case 'unknown':
|
|
1651
|
+
return 1;
|
|
1652
|
+
case 'event':
|
|
1653
|
+
return 2;
|
|
1654
|
+
case 'contract':
|
|
1655
|
+
return 3;
|
|
1656
|
+
default:
|
|
1657
|
+
return 4;
|
|
1658
|
+
}
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1661
|
+
function freshnessSourcePriority(source: PageFreshnessResult['evidence']['classifiedTimestamps'][number]['source']): number {
|
|
1662
|
+
switch (source) {
|
|
1663
|
+
case 'network':
|
|
1664
|
+
return 0;
|
|
1665
|
+
case 'page-data':
|
|
1666
|
+
return 1;
|
|
1667
|
+
case 'visible':
|
|
1668
|
+
return 2;
|
|
1669
|
+
case 'inline':
|
|
1670
|
+
return 3;
|
|
1671
|
+
default:
|
|
1672
|
+
return 4;
|
|
1673
|
+
}
|
|
1674
|
+
}
|
|
1675
|
+
|
|
1676
|
+
function rankFreshnessEvidence(
|
|
1677
|
+
candidates: PageFreshnessResult['evidence']['classifiedTimestamps'],
|
|
1678
|
+
now = Date.now()
|
|
1679
|
+
): PageFreshnessResult['evidence']['classifiedTimestamps'] {
|
|
1680
|
+
return candidates
|
|
1681
|
+
.slice()
|
|
1682
|
+
.sort((left, right) => {
|
|
1683
|
+
const byCategory = freshnessCategoryPriority(left.category) - freshnessCategoryPriority(right.category);
|
|
1684
|
+
if (byCategory !== 0) {
|
|
1685
|
+
return byCategory;
|
|
1686
|
+
}
|
|
1687
|
+
const bySource = freshnessSourcePriority(left.source) - freshnessSourcePriority(right.source);
|
|
1688
|
+
if (bySource !== 0) {
|
|
1689
|
+
return bySource;
|
|
1690
|
+
}
|
|
1691
|
+
const leftTimestamp = parseTimestampCandidate(left.value, now) ?? Number.NEGATIVE_INFINITY;
|
|
1692
|
+
const rightTimestamp = parseTimestampCandidate(right.value, now) ?? Number.NEGATIVE_INFINITY;
|
|
1693
|
+
if (leftTimestamp !== rightTimestamp) {
|
|
1694
|
+
return rightTimestamp - leftTimestamp;
|
|
1695
|
+
}
|
|
1696
|
+
return left.value.localeCompare(right.value);
|
|
1697
|
+
});
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1700
|
+
function deriveFreshnessConfidence(
|
|
1701
|
+
primary: PageFreshnessResult['evidence']['classifiedTimestamps'][number] | null
|
|
1702
|
+
): PageFreshnessResult['confidence'] {
|
|
1703
|
+
if (!primary) {
|
|
1704
|
+
return 'low';
|
|
1705
|
+
}
|
|
1706
|
+
if (primary.category === 'data' && (primary.source === 'network' || primary.source === 'page-data')) {
|
|
1707
|
+
return 'high';
|
|
1708
|
+
}
|
|
1709
|
+
if (primary.category === 'data') {
|
|
1710
|
+
return 'medium';
|
|
1711
|
+
}
|
|
1712
|
+
return 'low';
|
|
1713
|
+
}
|
|
1714
|
+
|
|
1447
1715
|
async function collectPageInspection(tabId: number, params: Record<string, unknown> = {}): Promise<PageInspectionState> {
|
|
1448
1716
|
return (await forwardContentRpc(tabId, 'bak.internal.inspectState', params)) as PageInspectionState;
|
|
1449
1717
|
}
|
|
@@ -1589,24 +1857,32 @@ async function buildFreshnessForTab(tabId: number, params: Record<string, unknow
|
|
|
1589
1857
|
now
|
|
1590
1858
|
);
|
|
1591
1859
|
const latestInlineDataTimestamp = latestTimestampFromCandidates(inlineCandidates, now);
|
|
1592
|
-
const latestPageDataTimestamp = latestTimestampFromCandidates(pageDataCandidates, now);
|
|
1593
|
-
const latestNetworkDataTimestamp = latestTimestampFromCandidates(networkCandidates, now);
|
|
1594
|
-
const domVisibleTimestamp = latestTimestampFromCandidates(visibleCandidates, now);
|
|
1595
|
-
const latestNetworkTs = latestNetworkTimestamp(tabId);
|
|
1596
|
-
const lastMutationAt = typeof inspection.lastMutationAt === 'number' ? inspection.lastMutationAt : null;
|
|
1597
|
-
const allCandidates = [...visibleCandidates, ...inlineCandidates, ...pageDataCandidates, ...networkCandidates];
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1860
|
+
const latestPageDataTimestamp = latestTimestampFromCandidates(pageDataCandidates, now);
|
|
1861
|
+
const latestNetworkDataTimestamp = latestTimestampFromCandidates(networkCandidates, now);
|
|
1862
|
+
const domVisibleTimestamp = latestTimestampFromCandidates(visibleCandidates, now);
|
|
1863
|
+
const latestNetworkTs = latestNetworkTimestamp(tabId);
|
|
1864
|
+
const lastMutationAt = typeof inspection.lastMutationAt === 'number' ? inspection.lastMutationAt : null;
|
|
1865
|
+
const allCandidates = rankFreshnessEvidence([...visibleCandidates, ...inlineCandidates, ...pageDataCandidates, ...networkCandidates], now);
|
|
1866
|
+
const primaryEvidence =
|
|
1867
|
+
allCandidates.find((candidate) => parseTimestampCandidate(candidate.value, now) !== null) ?? null;
|
|
1868
|
+
const primaryTimestamp = primaryEvidence ? parseTimestampCandidate(primaryEvidence.value, now) : null;
|
|
1869
|
+
return {
|
|
1870
|
+
pageLoadedAt: typeof inspection.pageLoadedAt === 'number' ? inspection.pageLoadedAt : null,
|
|
1871
|
+
lastMutationAt,
|
|
1872
|
+
latestNetworkTimestamp: latestNetworkTs,
|
|
1873
|
+
latestInlineDataTimestamp,
|
|
1874
|
+
latestPageDataTimestamp,
|
|
1875
|
+
latestNetworkDataTimestamp,
|
|
1876
|
+
domVisibleTimestamp,
|
|
1877
|
+
primaryTimestamp,
|
|
1878
|
+
primaryCategory: primaryEvidence?.category ?? null,
|
|
1879
|
+
primarySource: primaryEvidence?.source ?? null,
|
|
1880
|
+
confidence: deriveFreshnessConfidence(primaryEvidence),
|
|
1881
|
+
suppressedEvidenceCount: Math.max(0, allCandidates.length - (primaryEvidence ? 1 : 0)),
|
|
1882
|
+
assessment: computeFreshnessAssessment({
|
|
1883
|
+
latestInlineDataTimestamp,
|
|
1884
|
+
latestPageDataTimestamp,
|
|
1885
|
+
latestNetworkDataTimestamp,
|
|
1610
1886
|
latestNetworkTimestamp: latestNetworkTs,
|
|
1611
1887
|
domVisibleTimestamp,
|
|
1612
1888
|
lastMutationAt,
|
|
@@ -1615,10 +1891,10 @@ async function buildFreshnessForTab(tabId: number, params: Record<string, unknow
|
|
|
1615
1891
|
}),
|
|
1616
1892
|
evidence: {
|
|
1617
1893
|
visibleTimestamps: visibleCandidates.map((candidate) => candidate.value),
|
|
1618
|
-
inlineTimestamps: inlineCandidates.map((candidate) => candidate.value),
|
|
1619
|
-
pageDataTimestamps: pageDataCandidates.map((candidate) => candidate.value),
|
|
1620
|
-
networkDataTimestamps: networkCandidates.map((candidate) => candidate.value),
|
|
1621
|
-
classifiedTimestamps: allCandidates,
|
|
1894
|
+
inlineTimestamps: inlineCandidates.map((candidate) => candidate.value),
|
|
1895
|
+
pageDataTimestamps: pageDataCandidates.map((candidate) => candidate.value),
|
|
1896
|
+
networkDataTimestamps: networkCandidates.map((candidate) => candidate.value),
|
|
1897
|
+
classifiedTimestamps: allCandidates,
|
|
1622
1898
|
networkSampleIds: recentNetworkSampleIds(tabId)
|
|
1623
1899
|
}
|
|
1624
1900
|
};
|
|
@@ -2051,32 +2327,37 @@ async function handleRequest(request: CliRequest): Promise<unknown> {
|
|
|
2051
2327
|
return await executePageWorld(tab.id!, 'extract', params);
|
|
2052
2328
|
});
|
|
2053
2329
|
}
|
|
2054
|
-
case 'page.fetch': {
|
|
2055
|
-
return await preserveHumanFocus(typeof target.tabId !== 'number', async () => {
|
|
2056
|
-
const tab = await withTab(target);
|
|
2057
|
-
return await executePageWorld<PageFetchResponse>(tab.id!, 'fetch', params);
|
|
2058
|
-
});
|
|
2059
|
-
}
|
|
2060
|
-
case 'page.snapshot': {
|
|
2061
|
-
return await preserveHumanFocus(typeof target.tabId !== 'number', async () => {
|
|
2062
|
-
const tab = await withTab(target);
|
|
2063
|
-
if (typeof tab.id !== 'number' || typeof tab.windowId !== 'number') {
|
|
2064
|
-
throw toError('E_NOT_FOUND', 'Tab missing id');
|
|
2065
|
-
}
|
|
2066
|
-
const includeBase64 = params.includeBase64 !== false;
|
|
2067
|
-
const config = await getConfig();
|
|
2068
|
-
const elements = await sendToContent<{ elements: unknown[] }>(tab.id, {
|
|
2069
|
-
type: 'bak.collectElements',
|
|
2070
|
-
debugRichText: config.debugRichText
|
|
2071
|
-
});
|
|
2072
|
-
const
|
|
2073
|
-
return {
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2330
|
+
case 'page.fetch': {
|
|
2331
|
+
return await preserveHumanFocus(typeof target.tabId !== 'number', async () => {
|
|
2332
|
+
const tab = await withTab(target);
|
|
2333
|
+
return await executePageWorld<PageFetchResponse>(tab.id!, 'fetch', params);
|
|
2334
|
+
});
|
|
2335
|
+
}
|
|
2336
|
+
case 'page.snapshot': {
|
|
2337
|
+
return await preserveHumanFocus(typeof target.tabId !== 'number', async () => {
|
|
2338
|
+
const tab = await withTab(target);
|
|
2339
|
+
if (typeof tab.id !== 'number' || typeof tab.windowId !== 'number') {
|
|
2340
|
+
throw toError('E_NOT_FOUND', 'Tab missing id');
|
|
2341
|
+
}
|
|
2342
|
+
const includeBase64 = params.includeBase64 !== false;
|
|
2343
|
+
const config = await getConfig();
|
|
2344
|
+
const elements = await sendToContent<{ elements: unknown[] }>(tab.id, {
|
|
2345
|
+
type: 'bak.collectElements',
|
|
2346
|
+
debugRichText: config.debugRichText
|
|
2347
|
+
});
|
|
2348
|
+
const screenshot = params.capture === false ? { captureStatus: 'skipped' as const } : await captureAlignedTabScreenshot(tab);
|
|
2349
|
+
return {
|
|
2350
|
+
captureStatus: screenshot.captureStatus,
|
|
2351
|
+
captureError: screenshot.captureError,
|
|
2352
|
+
imageBase64:
|
|
2353
|
+
includeBase64 && typeof screenshot.imageData === 'string'
|
|
2354
|
+
? screenshot.imageData.replace(/^data:image\/png;base64,/, '')
|
|
2355
|
+
: undefined,
|
|
2356
|
+
elements: elements.elements,
|
|
2357
|
+
tabId: tab.id,
|
|
2358
|
+
url: tab.url ?? ''
|
|
2359
|
+
};
|
|
2360
|
+
});
|
|
2080
2361
|
}
|
|
2081
2362
|
case 'element.click': {
|
|
2082
2363
|
return await preserveHumanFocus(typeof target.tabId !== 'number', async () => {
|
|
@@ -2191,19 +2472,13 @@ async function handleRequest(request: CliRequest): Promise<unknown> {
|
|
|
2191
2472
|
}
|
|
2192
2473
|
});
|
|
2193
2474
|
}
|
|
2194
|
-
case 'network.search': {
|
|
2195
|
-
return await preserveHumanFocus(typeof target.tabId !== 'number', async () => {
|
|
2196
|
-
const tab = await withTab(target);
|
|
2197
|
-
await ensureTabNetworkCapture(tab.id!);
|
|
2198
|
-
return
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
String(params.pattern ?? ''),
|
|
2202
|
-
typeof params.limit === 'number' ? params.limit : 50
|
|
2203
|
-
)
|
|
2204
|
-
};
|
|
2205
|
-
});
|
|
2206
|
-
}
|
|
2475
|
+
case 'network.search': {
|
|
2476
|
+
return await preserveHumanFocus(typeof target.tabId !== 'number', async () => {
|
|
2477
|
+
const tab = await withTab(target);
|
|
2478
|
+
await ensureTabNetworkCapture(tab.id!);
|
|
2479
|
+
return searchNetworkEntries(tab.id!, String(params.pattern ?? ''), typeof params.limit === 'number' ? params.limit : 50);
|
|
2480
|
+
});
|
|
2481
|
+
}
|
|
2207
2482
|
case 'network.waitFor': {
|
|
2208
2483
|
return await preserveHumanFocus(typeof target.tabId !== 'number', async () => {
|
|
2209
2484
|
const tab = await withTab(target);
|
|
@@ -2231,48 +2506,52 @@ async function handleRequest(request: CliRequest): Promise<unknown> {
|
|
|
2231
2506
|
return { ok: true };
|
|
2232
2507
|
});
|
|
2233
2508
|
}
|
|
2234
|
-
case 'network.replay': {
|
|
2235
|
-
return await preserveHumanFocus(typeof target.tabId !== 'number', async () => {
|
|
2236
|
-
const tab = await withTab(target);
|
|
2237
|
-
await ensureTabNetworkCapture(tab.id!);
|
|
2238
|
-
const
|
|
2239
|
-
if (!
|
|
2240
|
-
throw toError('E_NOT_FOUND', `network entry not found: ${String(params.id ?? '')}`);
|
|
2241
|
-
}
|
|
2242
|
-
if (
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
}
|
|
2252
|
-
}
|
|
2253
|
-
const replayed = await executePageWorld<PageFetchResponse>(tab.id!, 'fetch', {
|
|
2254
|
-
url: entry.url,
|
|
2255
|
-
method: entry.method,
|
|
2256
|
-
headers:
|
|
2257
|
-
body:
|
|
2258
|
-
contentType:
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2509
|
+
case 'network.replay': {
|
|
2510
|
+
return await preserveHumanFocus(typeof target.tabId !== 'number', async () => {
|
|
2511
|
+
const tab = await withTab(target);
|
|
2512
|
+
await ensureTabNetworkCapture(tab.id!);
|
|
2513
|
+
const replayable = getReplayableNetworkRequest(tab.id!, String(params.id ?? ''));
|
|
2514
|
+
if (!replayable) {
|
|
2515
|
+
throw toError('E_NOT_FOUND', `network entry not found: ${String(params.id ?? '')}`);
|
|
2516
|
+
}
|
|
2517
|
+
if (replayable.degradedReason) {
|
|
2518
|
+
return {
|
|
2519
|
+
url: replayable.entry.url,
|
|
2520
|
+
status: 0,
|
|
2521
|
+
ok: false,
|
|
2522
|
+
headers: {},
|
|
2523
|
+
bytes: replayable.entry.requestBytes,
|
|
2524
|
+
truncated: true,
|
|
2525
|
+
degradedReason: replayable.degradedReason
|
|
2526
|
+
} satisfies PageFetchResponse;
|
|
2527
|
+
}
|
|
2528
|
+
const replayed = await executePageWorld<PageFetchResponse>(tab.id!, 'fetch', {
|
|
2529
|
+
url: replayable.entry.url,
|
|
2530
|
+
method: replayable.entry.method,
|
|
2531
|
+
headers: replayHeadersFromRequestHeaders(replayable.headers),
|
|
2532
|
+
body: replayable.body,
|
|
2533
|
+
contentType: replayable.contentType,
|
|
2534
|
+
mode: params.mode,
|
|
2535
|
+
timeoutMs: params.timeoutMs,
|
|
2536
|
+
maxBytes: params.maxBytes,
|
|
2537
|
+
fullResponse: params.fullResponse === true,
|
|
2538
|
+
auth: params.auth,
|
|
2539
|
+
scope: 'current'
|
|
2540
|
+
});
|
|
2541
|
+
const frameResult = replayed.result ?? replayed.results?.find((candidate) => candidate.value || candidate.error);
|
|
2542
|
+
if (frameResult?.error) {
|
|
2543
|
+
throw toError(frameResult.error.code ?? 'E_EXECUTION', frameResult.error.message, frameResult.error.details);
|
|
2544
|
+
}
|
|
2545
|
+
const first = frameResult?.value;
|
|
2546
|
+
if (!first) {
|
|
2547
|
+
return {
|
|
2548
|
+
url: replayable.entry.url,
|
|
2549
|
+
status: 0,
|
|
2550
|
+
ok: false,
|
|
2551
|
+
headers: {},
|
|
2552
|
+
degradedReason: 'network replay returned no response payload'
|
|
2553
|
+
} satisfies PageFetchResponse;
|
|
2554
|
+
}
|
|
2276
2555
|
return params.withSchema === 'auto' && params.mode === 'json'
|
|
2277
2556
|
? await enrichReplayWithSchema(tab.id!, String(params.id ?? ''), first)
|
|
2278
2557
|
: first;
|