@flrande/bak-extension 0.6.15 → 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 +304 -68
- package/dist/content.global.js +9 -3
- package/dist/manifest.json +1 -1
- package/package.json +2 -2
- package/src/background.ts +430 -209
- package/src/content.ts +9 -3
- package/src/dynamic-data-tools.ts +2 -2
- package/src/network-debugger.ts +90 -12
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,
|
|
@@ -838,10 +838,18 @@ async function withTab(target: { tabId?: number; bindingId?: string } = {}, opti
|
|
|
838
838
|
return validate(tab);
|
|
839
839
|
}
|
|
840
840
|
|
|
841
|
-
async function captureAlignedTabScreenshot(tab: chrome.tabs.Tab): Promise<
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
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
|
+
}
|
|
845
853
|
|
|
846
854
|
const activeTabs = await chrome.tabs.query({ windowId: tab.windowId, active: true });
|
|
847
855
|
const activeTab = activeTabs[0];
|
|
@@ -851,13 +859,24 @@ async function captureAlignedTabScreenshot(tab: chrome.tabs.Tab): Promise<string
|
|
|
851
859
|
await chrome.tabs.update(tab.id, { active: true });
|
|
852
860
|
await new Promise((resolve) => setTimeout(resolve, 80));
|
|
853
861
|
}
|
|
854
|
-
|
|
855
|
-
try {
|
|
856
|
-
return
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
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 });
|
|
861
880
|
} catch {
|
|
862
881
|
// Ignore restore errors if the original tab no longer exists.
|
|
863
882
|
}
|
|
@@ -1012,10 +1031,10 @@ async function executePageWorld<T>(
|
|
|
1012
1031
|
target,
|
|
1013
1032
|
world: 'MAIN',
|
|
1014
1033
|
args: [
|
|
1015
|
-
{
|
|
1016
|
-
action,
|
|
1017
|
-
scope,
|
|
1018
|
-
framePath,
|
|
1034
|
+
{
|
|
1035
|
+
action,
|
|
1036
|
+
scope,
|
|
1037
|
+
framePath,
|
|
1019
1038
|
expr: typeof params.expr === 'string' ? params.expr : '',
|
|
1020
1039
|
path: typeof params.path === 'string' ? params.path : '',
|
|
1021
1040
|
resolver: typeof params.resolver === 'string' ? params.resolver : undefined,
|
|
@@ -1023,13 +1042,15 @@ async function executePageWorld<T>(
|
|
|
1023
1042
|
method: typeof params.method === 'string' ? params.method : 'GET',
|
|
1024
1043
|
headers: typeof params.headers === 'object' && params.headers !== null ? params.headers : undefined,
|
|
1025
1044
|
body: typeof params.body === 'string' ? params.body : undefined,
|
|
1026
|
-
contentType: typeof params.contentType === 'string' ? params.contentType : undefined,
|
|
1027
|
-
mode: params.mode === 'json' ? 'json' : 'raw',
|
|
1028
|
-
maxBytes: typeof params.maxBytes === 'number' ? params.maxBytes : undefined,
|
|
1029
|
-
timeoutMs: typeof params.timeoutMs === 'number' ? params.timeoutMs : undefined
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
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) => {
|
|
1033
1054
|
const serializeValue = (value: unknown, maxBytes?: number) => {
|
|
1034
1055
|
let cloned: unknown;
|
|
1035
1056
|
try {
|
|
@@ -1129,10 +1150,10 @@ async function executePageWorld<T>(
|
|
|
1129
1150
|
return current;
|
|
1130
1151
|
};
|
|
1131
1152
|
|
|
1132
|
-
const resolveExtractValue = (
|
|
1133
|
-
targetWindow: Window & { eval: (expr: string) => unknown },
|
|
1134
|
-
path: string,
|
|
1135
|
-
resolver: unknown
|
|
1153
|
+
const resolveExtractValue = (
|
|
1154
|
+
targetWindow: Window & { eval: (expr: string) => unknown },
|
|
1155
|
+
path: string,
|
|
1156
|
+
resolver: unknown
|
|
1136
1157
|
): { resolver: 'globalThis' | 'lexical'; value: unknown } => {
|
|
1137
1158
|
const strategy = resolver === 'globalThis' || resolver === 'lexical' ? resolver : 'auto';
|
|
1138
1159
|
const lexicalExpression = buildPathExpression(path);
|
|
@@ -1159,11 +1180,82 @@ async function executePageWorld<T>(
|
|
|
1159
1180
|
throw error;
|
|
1160
1181
|
}
|
|
1161
1182
|
}
|
|
1162
|
-
return { resolver: 'lexical', value: readLexical() };
|
|
1163
|
-
};
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
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;
|
|
1167
1259
|
if (payload.action === 'eval') {
|
|
1168
1260
|
const evaluator = (targetWindow as Window & { eval: (expr: string) => unknown }).eval;
|
|
1169
1261
|
const serialized = serializeValue(evaluator(payload.expr), payload.maxBytes);
|
|
@@ -1179,14 +1271,53 @@ async function executePageWorld<T>(
|
|
|
1179
1271
|
bytes: serialized.bytes,
|
|
1180
1272
|
resolver: extracted.resolver
|
|
1181
1273
|
};
|
|
1182
|
-
}
|
|
1183
|
-
if (payload.action === 'fetch') {
|
|
1184
|
-
const headers = { ...(payload.headers ?? {}) } as Record<string, string>;
|
|
1185
|
-
if (payload.contentType && !headers
|
|
1186
|
-
headers['Content-Type'] = payload.contentType;
|
|
1187
|
-
}
|
|
1188
|
-
const
|
|
1189
|
-
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 =
|
|
1190
1321
|
controller && typeof payload.timeoutMs === 'number' && payload.timeoutMs > 0
|
|
1191
1322
|
? window.setTimeout(() => controller.abort(), payload.timeoutMs)
|
|
1192
1323
|
: null;
|
|
@@ -1211,43 +1342,56 @@ async function executePageWorld<T>(
|
|
|
1211
1342
|
return {
|
|
1212
1343
|
url: targetWindow.location.href,
|
|
1213
1344
|
framePath: payload.scope === 'current' ? payload.framePath ?? [] : [],
|
|
1214
|
-
value: (() => {
|
|
1215
|
-
const encoder = typeof TextEncoder === 'function' ? new TextEncoder() : null;
|
|
1216
|
-
const decoder = typeof TextDecoder === 'function' ? new TextDecoder() : null;
|
|
1217
|
-
const previewLimit = typeof payload.maxBytes === 'number' && payload.maxBytes > 0 ? payload.maxBytes : 8192;
|
|
1218
|
-
const encodedBody = encoder ? encoder.encode(bodyText) : null;
|
|
1219
|
-
const bodyBytes = encodedBody ? encodedBody.byteLength : bodyText.length;
|
|
1220
|
-
const truncated = bodyBytes > previewLimit;
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
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
|
+
}
|
|
1251
1395
|
throw { code: 'E_NOT_FOUND', message: `Unsupported page world action: ${payload.action}` };
|
|
1252
1396
|
} catch (error) {
|
|
1253
1397
|
return {
|
|
@@ -1327,23 +1471,20 @@ function filterNetworkEntrySections(entry: NetworkEntry, include: unknown): Netw
|
|
|
1327
1471
|
return clone;
|
|
1328
1472
|
}
|
|
1329
1473
|
|
|
1330
|
-
function
|
|
1331
|
-
if (!
|
|
1332
|
-
return undefined;
|
|
1333
|
-
}
|
|
1334
|
-
const headers: Record<string, string> = {};
|
|
1335
|
-
for (const [name, value] of Object.entries(
|
|
1336
|
-
const normalizedName = name.toLowerCase();
|
|
1337
|
-
if (REPLAY_FORBIDDEN_HEADER_NAMES.has(normalizedName) || normalizedName.startsWith('sec-')) {
|
|
1338
|
-
continue;
|
|
1339
|
-
}
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
}
|
|
1345
|
-
return Object.keys(headers).length > 0 ? headers : undefined;
|
|
1346
|
-
}
|
|
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
|
+
}
|
|
1347
1488
|
|
|
1348
1489
|
function collectTimestampMatchesFromText(text: string, source: TimestampEvidenceCandidate['source'], patterns?: string[]): TimestampEvidenceCandidate[] {
|
|
1349
1490
|
const regexes = (
|
|
@@ -1461,7 +1602,7 @@ function latestTimestampFromCandidates(
|
|
|
1461
1602
|
return latest;
|
|
1462
1603
|
}
|
|
1463
1604
|
|
|
1464
|
-
function computeFreshnessAssessment(input: {
|
|
1605
|
+
function computeFreshnessAssessment(input: {
|
|
1465
1606
|
latestInlineDataTimestamp: number | null;
|
|
1466
1607
|
latestPageDataTimestamp: number | null;
|
|
1467
1608
|
latestNetworkDataTimestamp: number | null;
|
|
@@ -1498,10 +1639,79 @@ function computeFreshnessAssessment(input: {
|
|
|
1498
1639
|
.filter((value): value is number => typeof value === 'number');
|
|
1499
1640
|
if (staleSignals.length > 0 && staleSignals.every((value) => now - value > input.staleWindowMs)) {
|
|
1500
1641
|
return 'stale';
|
|
1501
|
-
}
|
|
1502
|
-
return 'unknown';
|
|
1503
|
-
}
|
|
1504
|
-
|
|
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
|
+
|
|
1505
1715
|
async function collectPageInspection(tabId: number, params: Record<string, unknown> = {}): Promise<PageInspectionState> {
|
|
1506
1716
|
return (await forwardContentRpc(tabId, 'bak.internal.inspectState', params)) as PageInspectionState;
|
|
1507
1717
|
}
|
|
@@ -1647,24 +1857,32 @@ async function buildFreshnessForTab(tabId: number, params: Record<string, unknow
|
|
|
1647
1857
|
now
|
|
1648
1858
|
);
|
|
1649
1859
|
const latestInlineDataTimestamp = latestTimestampFromCandidates(inlineCandidates, now);
|
|
1650
|
-
const latestPageDataTimestamp = latestTimestampFromCandidates(pageDataCandidates, now);
|
|
1651
|
-
const latestNetworkDataTimestamp = latestTimestampFromCandidates(networkCandidates, now);
|
|
1652
|
-
const domVisibleTimestamp = latestTimestampFromCandidates(visibleCandidates, now);
|
|
1653
|
-
const latestNetworkTs = latestNetworkTimestamp(tabId);
|
|
1654
|
-
const lastMutationAt = typeof inspection.lastMutationAt === 'number' ? inspection.lastMutationAt : null;
|
|
1655
|
-
const allCandidates = [...visibleCandidates, ...inlineCandidates, ...pageDataCandidates, ...networkCandidates];
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
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,
|
|
1668
1886
|
latestNetworkTimestamp: latestNetworkTs,
|
|
1669
1887
|
domVisibleTimestamp,
|
|
1670
1888
|
lastMutationAt,
|
|
@@ -1673,10 +1891,10 @@ async function buildFreshnessForTab(tabId: number, params: Record<string, unknow
|
|
|
1673
1891
|
}),
|
|
1674
1892
|
evidence: {
|
|
1675
1893
|
visibleTimestamps: visibleCandidates.map((candidate) => candidate.value),
|
|
1676
|
-
inlineTimestamps: inlineCandidates.map((candidate) => candidate.value),
|
|
1677
|
-
pageDataTimestamps: pageDataCandidates.map((candidate) => candidate.value),
|
|
1678
|
-
networkDataTimestamps: networkCandidates.map((candidate) => candidate.value),
|
|
1679
|
-
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,
|
|
1680
1898
|
networkSampleIds: recentNetworkSampleIds(tabId)
|
|
1681
1899
|
}
|
|
1682
1900
|
};
|
|
@@ -2109,32 +2327,37 @@ async function handleRequest(request: CliRequest): Promise<unknown> {
|
|
|
2109
2327
|
return await executePageWorld(tab.id!, 'extract', params);
|
|
2110
2328
|
});
|
|
2111
2329
|
}
|
|
2112
|
-
case 'page.fetch': {
|
|
2113
|
-
return await preserveHumanFocus(typeof target.tabId !== 'number', async () => {
|
|
2114
|
-
const tab = await withTab(target);
|
|
2115
|
-
return await executePageWorld<PageFetchResponse>(tab.id!, 'fetch', params);
|
|
2116
|
-
});
|
|
2117
|
-
}
|
|
2118
|
-
case 'page.snapshot': {
|
|
2119
|
-
return await preserveHumanFocus(typeof target.tabId !== 'number', async () => {
|
|
2120
|
-
const tab = await withTab(target);
|
|
2121
|
-
if (typeof tab.id !== 'number' || typeof tab.windowId !== 'number') {
|
|
2122
|
-
throw toError('E_NOT_FOUND', 'Tab missing id');
|
|
2123
|
-
}
|
|
2124
|
-
const includeBase64 = params.includeBase64 !== false;
|
|
2125
|
-
const config = await getConfig();
|
|
2126
|
-
const elements = await sendToContent<{ elements: unknown[] }>(tab.id, {
|
|
2127
|
-
type: 'bak.collectElements',
|
|
2128
|
-
debugRichText: config.debugRichText
|
|
2129
|
-
});
|
|
2130
|
-
const
|
|
2131
|
-
return {
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
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
|
+
});
|
|
2138
2361
|
}
|
|
2139
2362
|
case 'element.click': {
|
|
2140
2363
|
return await preserveHumanFocus(typeof target.tabId !== 'number', async () => {
|
|
@@ -2249,19 +2472,13 @@ async function handleRequest(request: CliRequest): Promise<unknown> {
|
|
|
2249
2472
|
}
|
|
2250
2473
|
});
|
|
2251
2474
|
}
|
|
2252
|
-
case 'network.search': {
|
|
2253
|
-
return await preserveHumanFocus(typeof target.tabId !== 'number', async () => {
|
|
2254
|
-
const tab = await withTab(target);
|
|
2255
|
-
await ensureTabNetworkCapture(tab.id!);
|
|
2256
|
-
return
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
String(params.pattern ?? ''),
|
|
2260
|
-
typeof params.limit === 'number' ? params.limit : 50
|
|
2261
|
-
)
|
|
2262
|
-
};
|
|
2263
|
-
});
|
|
2264
|
-
}
|
|
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
|
+
}
|
|
2265
2482
|
case 'network.waitFor': {
|
|
2266
2483
|
return await preserveHumanFocus(typeof target.tabId !== 'number', async () => {
|
|
2267
2484
|
const tab = await withTab(target);
|
|
@@ -2289,48 +2506,52 @@ async function handleRequest(request: CliRequest): Promise<unknown> {
|
|
|
2289
2506
|
return { ok: true };
|
|
2290
2507
|
});
|
|
2291
2508
|
}
|
|
2292
|
-
case 'network.replay': {
|
|
2293
|
-
return await preserveHumanFocus(typeof target.tabId !== 'number', async () => {
|
|
2294
|
-
const tab = await withTab(target);
|
|
2295
|
-
await ensureTabNetworkCapture(tab.id!);
|
|
2296
|
-
const
|
|
2297
|
-
if (!
|
|
2298
|
-
throw toError('E_NOT_FOUND', `network entry not found: ${String(params.id ?? '')}`);
|
|
2299
|
-
}
|
|
2300
|
-
if (
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
}
|
|
2310
|
-
}
|
|
2311
|
-
const replayed = await executePageWorld<PageFetchResponse>(tab.id!, 'fetch', {
|
|
2312
|
-
url: entry.url,
|
|
2313
|
-
method: entry.method,
|
|
2314
|
-
headers:
|
|
2315
|
-
body:
|
|
2316
|
-
contentType:
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
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
|
+
}
|
|
2334
2555
|
return params.withSchema === 'auto' && params.mode === 'json'
|
|
2335
2556
|
? await enrichReplayWithSchema(tab.id!, String(params.id ?? ''), first)
|
|
2336
2557
|
: first;
|