@flrande/bak-extension 0.6.16 → 0.6.17
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 +604 -54
- package/dist/content.global.js +356 -20
- package/dist/manifest.json +1 -1
- package/package.json +2 -2
- package/src/background.ts +445 -74
- package/src/content.ts +257 -24
- package/src/dynamic-data-tools.ts +139 -5
- package/src/network-debugger.ts +19 -47
- package/src/network-tools.ts +184 -0
package/src/background.ts
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
ConsoleEntry,
|
|
3
|
+
FetchDiagnostics,
|
|
3
4
|
DebugDumpSection,
|
|
4
5
|
InspectFreshnessResult,
|
|
5
6
|
InspectLiveUpdatesResult,
|
|
6
7
|
InspectPageDataCandidateProbe,
|
|
8
|
+
InspectPageDateControl,
|
|
7
9
|
InspectPageDataResult,
|
|
10
|
+
InspectPageModeGroup,
|
|
8
11
|
Locator,
|
|
12
|
+
NetworkCloneResult,
|
|
9
13
|
NetworkEntry,
|
|
10
14
|
PageExecutionScope,
|
|
11
15
|
PageFetchResponse,
|
|
@@ -153,6 +157,8 @@ interface PageInspectionState {
|
|
|
153
157
|
inlineTimestampCandidates?: Array<Record<string, unknown>>;
|
|
154
158
|
tables?: TableHandle[];
|
|
155
159
|
inlineJsonSources?: InlineJsonInspectionSource[];
|
|
160
|
+
modeGroups?: InspectPageModeGroup[];
|
|
161
|
+
dateControls?: InspectPageDateControl[];
|
|
156
162
|
cookies?: Array<{ name: string }>;
|
|
157
163
|
lastMutationAt?: number;
|
|
158
164
|
pageLoadedAt?: number;
|
|
@@ -174,18 +180,23 @@ interface PageInspectionState {
|
|
|
174
180
|
intervals: number;
|
|
175
181
|
};
|
|
176
182
|
}
|
|
177
|
-
const REPLAY_FORBIDDEN_HEADER_NAMES = new Set([
|
|
178
|
-
'accept-encoding',
|
|
179
|
-
'authorization',
|
|
180
|
-
'connection',
|
|
183
|
+
const REPLAY_FORBIDDEN_HEADER_NAMES = new Set([
|
|
184
|
+
'accept-encoding',
|
|
185
|
+
'authorization',
|
|
186
|
+
'connection',
|
|
181
187
|
'content-length',
|
|
182
188
|
'cookie',
|
|
183
189
|
'host',
|
|
184
190
|
'origin',
|
|
185
191
|
'proxy-authorization',
|
|
186
192
|
'referer',
|
|
187
|
-
'set-cookie'
|
|
188
|
-
]);
|
|
193
|
+
'set-cookie'
|
|
194
|
+
]);
|
|
195
|
+
const CLONE_FORBIDDEN_HEADER_NAMES = new Set([
|
|
196
|
+
...REPLAY_FORBIDDEN_HEADER_NAMES,
|
|
197
|
+
'x-csrf-token',
|
|
198
|
+
'x-xsrf-token'
|
|
199
|
+
]);
|
|
189
200
|
|
|
190
201
|
let ws: WebSocket | null = null;
|
|
191
202
|
let reconnectTimer: number | null = null;
|
|
@@ -248,7 +259,7 @@ function sendResponse(payload: CliResponse): void {
|
|
|
248
259
|
}
|
|
249
260
|
}
|
|
250
261
|
|
|
251
|
-
function sendEvent(event: string, data: Record<string, unknown>): void {
|
|
262
|
+
function sendEvent(event: string, data: Record<string, unknown>): void {
|
|
252
263
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
253
264
|
ws.send(
|
|
254
265
|
JSON.stringify({
|
|
@@ -351,6 +362,14 @@ async function listSessionBindingStates(): Promise<SessionBindingRecord[]> {
|
|
|
351
362
|
return Object.values(await loadSessionBindingStateMap());
|
|
352
363
|
}
|
|
353
364
|
|
|
365
|
+
function quotePowerShellArg(value: string): string {
|
|
366
|
+
return `'${value.replace(/'/g, "''")}'`;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function renderPowerShellCommand(argv: string[]): string {
|
|
370
|
+
return ['bak', ...argv].map((part) => quotePowerShellArg(part)).join(' ');
|
|
371
|
+
}
|
|
372
|
+
|
|
354
373
|
function collectPopupSessionBindingTabIds(state: SessionBindingRecord): number[] {
|
|
355
374
|
return [
|
|
356
375
|
...new Set(state.tabIds.concat([state.activeTabId, state.primaryTabId].filter((value): value is number => typeof value === 'number')))
|
|
@@ -1254,6 +1273,57 @@ async function executePageWorld<T>(
|
|
|
1254
1273
|
return {};
|
|
1255
1274
|
};
|
|
1256
1275
|
|
|
1276
|
+
const utf8ByteLength = (value: string): number => {
|
|
1277
|
+
if (typeof TextEncoder === 'function') {
|
|
1278
|
+
return new TextEncoder().encode(value).byteLength;
|
|
1279
|
+
}
|
|
1280
|
+
return value.length;
|
|
1281
|
+
};
|
|
1282
|
+
|
|
1283
|
+
const truncateUtf8Text = (value: string, limit: number): string => {
|
|
1284
|
+
if (limit <= 0) {
|
|
1285
|
+
return '';
|
|
1286
|
+
}
|
|
1287
|
+
if (typeof TextEncoder !== 'function' || typeof TextDecoder !== 'function') {
|
|
1288
|
+
return value.slice(0, limit);
|
|
1289
|
+
}
|
|
1290
|
+
const encoded = new TextEncoder().encode(value);
|
|
1291
|
+
if (encoded.byteLength <= limit) {
|
|
1292
|
+
return value;
|
|
1293
|
+
}
|
|
1294
|
+
return new TextDecoder().decode(encoded.subarray(0, limit));
|
|
1295
|
+
};
|
|
1296
|
+
|
|
1297
|
+
const buildRetryHints = (requestUrl: string, baseUrl: string): string[] => {
|
|
1298
|
+
const hints: string[] = [];
|
|
1299
|
+
let parsed: URL | null = null;
|
|
1300
|
+
try {
|
|
1301
|
+
parsed = new URL(requestUrl, baseUrl);
|
|
1302
|
+
} catch {
|
|
1303
|
+
parsed = null;
|
|
1304
|
+
}
|
|
1305
|
+
const keys = (() => {
|
|
1306
|
+
if (!parsed) {
|
|
1307
|
+
return [];
|
|
1308
|
+
}
|
|
1309
|
+
const collected: string[] = [];
|
|
1310
|
+
parsed.searchParams.forEach((_value, key) => {
|
|
1311
|
+
collected.push(key.toLowerCase());
|
|
1312
|
+
});
|
|
1313
|
+
return [...new Set(collected)];
|
|
1314
|
+
})();
|
|
1315
|
+
if (keys.some((key) => key.includes('limit'))) {
|
|
1316
|
+
hints.push('reduce the limit parameter and retry');
|
|
1317
|
+
}
|
|
1318
|
+
if (keys.some((key) => /(from|to|start|end|date|time|timestamp)/i.test(key))) {
|
|
1319
|
+
hints.push('narrow the requested time window and retry');
|
|
1320
|
+
}
|
|
1321
|
+
if (keys.some((key) => key.includes('page')) && keys.some((key) => key.includes('limit'))) {
|
|
1322
|
+
hints.push('retry with smaller paginated windows');
|
|
1323
|
+
}
|
|
1324
|
+
return hints;
|
|
1325
|
+
};
|
|
1326
|
+
|
|
1257
1327
|
try {
|
|
1258
1328
|
const targetWindow = payload.scope === 'main' ? window : payload.scope === 'current' ? resolveFrameWindow(payload.framePath ?? []) : window;
|
|
1259
1329
|
if (payload.action === 'eval') {
|
|
@@ -1316,46 +1386,143 @@ async function executePageWorld<T>(
|
|
|
1316
1386
|
}
|
|
1317
1387
|
}
|
|
1318
1388
|
}
|
|
1389
|
+
|
|
1390
|
+
const requestHints = buildRetryHints(payload.url, targetWindow.location.href);
|
|
1391
|
+
const diagnosticsBase = {
|
|
1392
|
+
requestSent: false,
|
|
1393
|
+
responseStarted: false,
|
|
1394
|
+
status: undefined as number | undefined,
|
|
1395
|
+
headersReceived: {} as Record<string, string>,
|
|
1396
|
+
bodyBytesRead: 0,
|
|
1397
|
+
partialBodyPreview: '',
|
|
1398
|
+
timing: {
|
|
1399
|
+
startedAt: Date.now()
|
|
1400
|
+
} as FetchDiagnostics['timing']
|
|
1401
|
+
};
|
|
1402
|
+
const previewLimit =
|
|
1403
|
+
!fullResponse && typeof payload.maxBytes === 'number' && payload.maxBytes > 0 ? payload.maxBytes : 8192;
|
|
1404
|
+
let previewBytes = 0;
|
|
1405
|
+
const appendPreviewText = (value: string): void => {
|
|
1406
|
+
if (!value || previewBytes >= previewLimit) {
|
|
1407
|
+
return;
|
|
1408
|
+
}
|
|
1409
|
+
const remaining = previewLimit - previewBytes;
|
|
1410
|
+
const next = truncateUtf8Text(value, remaining);
|
|
1411
|
+
diagnosticsBase.partialBodyPreview += next;
|
|
1412
|
+
previewBytes += utf8ByteLength(next);
|
|
1413
|
+
};
|
|
1414
|
+
|
|
1319
1415
|
const controller = typeof AbortController === 'function' ? new AbortController() : null;
|
|
1320
1416
|
const timeoutId =
|
|
1321
|
-
controller && typeof payload.timeoutMs === 'number' && payload.timeoutMs > 0
|
|
1322
|
-
? window.setTimeout(() => controller.abort(), payload.timeoutMs)
|
|
1323
|
-
: null;
|
|
1324
|
-
let response: Response;
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
framePath: payload.scope === 'current' ? payload.framePath ?? [] : [],
|
|
1345
|
-
value: (() => {
|
|
1346
|
-
const encoder = typeof TextEncoder === 'function' ? new TextEncoder() : null;
|
|
1417
|
+
controller && typeof payload.timeoutMs === 'number' && payload.timeoutMs > 0
|
|
1418
|
+
? window.setTimeout(() => controller.abort(), payload.timeoutMs)
|
|
1419
|
+
: null;
|
|
1420
|
+
let response: Response;
|
|
1421
|
+
let bodyText = '';
|
|
1422
|
+
try {
|
|
1423
|
+
diagnosticsBase.requestSent = true;
|
|
1424
|
+
diagnosticsBase.timing.requestSentAt = Date.now();
|
|
1425
|
+
response = await targetWindow.fetch(payload.url, {
|
|
1426
|
+
method: payload.method || 'GET',
|
|
1427
|
+
headers,
|
|
1428
|
+
body: typeof payload.body === 'string' ? payload.body : undefined,
|
|
1429
|
+
signal: controller ? controller.signal : undefined
|
|
1430
|
+
});
|
|
1431
|
+
diagnosticsBase.responseStarted = true;
|
|
1432
|
+
diagnosticsBase.timing.responseStartedAt = Date.now();
|
|
1433
|
+
diagnosticsBase.status = response.status;
|
|
1434
|
+
response.headers.forEach((value, key) => {
|
|
1435
|
+
diagnosticsBase.headersReceived[key] = value;
|
|
1436
|
+
});
|
|
1437
|
+
|
|
1438
|
+
const reader = response.body?.getReader?.();
|
|
1439
|
+
if (reader) {
|
|
1347
1440
|
const decoder = typeof TextDecoder === 'function' ? new TextDecoder() : null;
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1441
|
+
while (true) {
|
|
1442
|
+
const chunk = await reader.read();
|
|
1443
|
+
if (chunk.done) {
|
|
1444
|
+
break;
|
|
1445
|
+
}
|
|
1446
|
+
const value = chunk.value ?? new Uint8Array();
|
|
1447
|
+
diagnosticsBase.bodyBytesRead += value.byteLength;
|
|
1448
|
+
if (decoder) {
|
|
1449
|
+
const decoded = decoder.decode(value, { stream: true });
|
|
1450
|
+
bodyText += decoded;
|
|
1451
|
+
appendPreviewText(decoded);
|
|
1452
|
+
} else {
|
|
1453
|
+
const fallback = String.fromCharCode(...value);
|
|
1454
|
+
bodyText += fallback;
|
|
1455
|
+
appendPreviewText(fallback);
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
if (decoder) {
|
|
1459
|
+
const flushed = decoder.decode();
|
|
1460
|
+
if (flushed) {
|
|
1461
|
+
bodyText += flushed;
|
|
1462
|
+
appendPreviewText(flushed);
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
} else {
|
|
1466
|
+
bodyText = await response.text();
|
|
1467
|
+
diagnosticsBase.bodyBytesRead = utf8ByteLength(bodyText);
|
|
1468
|
+
appendPreviewText(bodyText);
|
|
1469
|
+
}
|
|
1470
|
+
diagnosticsBase.timing.completedAt = Date.now();
|
|
1471
|
+
} catch (error) {
|
|
1472
|
+
const abortLike =
|
|
1473
|
+
(error instanceof DOMException && error.name === 'AbortError') ||
|
|
1474
|
+
(error instanceof Error && /abort|timeout/i.test(error.message));
|
|
1475
|
+
const where: FetchDiagnostics['where'] =
|
|
1476
|
+
diagnosticsBase.requestSent !== true
|
|
1477
|
+
? 'dispatch'
|
|
1478
|
+
: diagnosticsBase.responseStarted !== true
|
|
1479
|
+
? 'ttfb'
|
|
1480
|
+
: 'body';
|
|
1481
|
+
const diagnostics: FetchDiagnostics = {
|
|
1482
|
+
kind: abortLike ? 'timeout' : 'network',
|
|
1483
|
+
retryable: true,
|
|
1484
|
+
where,
|
|
1485
|
+
timing: {
|
|
1486
|
+
...diagnosticsBase.timing,
|
|
1487
|
+
...(abortLike ? { timeoutAt: Date.now() } : {})
|
|
1488
|
+
},
|
|
1489
|
+
requestSent: diagnosticsBase.requestSent,
|
|
1490
|
+
responseStarted: diagnosticsBase.responseStarted,
|
|
1491
|
+
status: diagnosticsBase.status,
|
|
1492
|
+
headersReceived:
|
|
1493
|
+
Object.keys(diagnosticsBase.headersReceived).length > 0 ? diagnosticsBase.headersReceived : undefined,
|
|
1494
|
+
bodyBytesRead: diagnosticsBase.bodyBytesRead,
|
|
1495
|
+
partialBodyPreview: diagnosticsBase.partialBodyPreview || undefined,
|
|
1496
|
+
hints: requestHints
|
|
1497
|
+
};
|
|
1498
|
+
throw {
|
|
1499
|
+
code: abortLike ? 'E_TIMEOUT' : 'E_EXECUTION',
|
|
1500
|
+
message: abortLike ? `page.fetch timeout during ${where}` : error instanceof Error ? error.message : String(error),
|
|
1501
|
+
details: {
|
|
1502
|
+
...diagnostics,
|
|
1503
|
+
diagnostics
|
|
1504
|
+
}
|
|
1505
|
+
};
|
|
1506
|
+
} finally {
|
|
1507
|
+
if (timeoutId !== null) {
|
|
1508
|
+
window.clearTimeout(timeoutId);
|
|
1509
|
+
}
|
|
1510
|
+
}
|
|
1511
|
+
const headerMap: Record<string, string> = {};
|
|
1512
|
+
response.headers.forEach((value, key) => {
|
|
1513
|
+
headerMap[key] = value;
|
|
1514
|
+
});
|
|
1515
|
+
return {
|
|
1516
|
+
url: targetWindow.location.href,
|
|
1517
|
+
framePath: payload.scope === 'current' ? payload.framePath ?? [] : [],
|
|
1518
|
+
value: (() => {
|
|
1519
|
+
const bodyBytes = diagnosticsBase.bodyBytesRead || utf8ByteLength(bodyText);
|
|
1351
1520
|
const truncated = !fullResponse && bodyBytes > previewLimit;
|
|
1352
1521
|
const previewText =
|
|
1353
1522
|
fullResponse
|
|
1354
1523
|
? bodyText
|
|
1355
|
-
: encodedBody && decoder
|
|
1356
|
-
? decoder.decode(encodedBody.subarray(0, Math.min(encodedBody.byteLength, previewLimit)))
|
|
1357
1524
|
: truncated
|
|
1358
|
-
? bodyText
|
|
1525
|
+
? truncateUtf8Text(bodyText, previewLimit)
|
|
1359
1526
|
: bodyText;
|
|
1360
1527
|
const result: Record<string, unknown> = {
|
|
1361
1528
|
url: response.url,
|
|
@@ -1366,10 +1533,57 @@ async function executePageWorld<T>(
|
|
|
1366
1533
|
bytes: bodyBytes,
|
|
1367
1534
|
truncated,
|
|
1368
1535
|
authApplied: authApplied.length > 0 ? authApplied : undefined,
|
|
1369
|
-
authSources: authSources.size > 0 ? [...authSources] : undefined
|
|
1536
|
+
authSources: authSources.size > 0 ? [...authSources] : undefined,
|
|
1537
|
+
diagnostics: {
|
|
1538
|
+
kind: 'success',
|
|
1539
|
+
retryable: false,
|
|
1540
|
+
where: 'complete',
|
|
1541
|
+
timing: diagnosticsBase.timing,
|
|
1542
|
+
requestSent: diagnosticsBase.requestSent,
|
|
1543
|
+
responseStarted: diagnosticsBase.responseStarted,
|
|
1544
|
+
status: response.status,
|
|
1545
|
+
headersReceived: headerMap,
|
|
1546
|
+
bodyBytesRead: bodyBytes,
|
|
1547
|
+
partialBodyPreview: diagnosticsBase.partialBodyPreview || undefined,
|
|
1548
|
+
hints: requestHints
|
|
1549
|
+
} satisfies FetchDiagnostics
|
|
1370
1550
|
};
|
|
1371
1551
|
if (payload.mode === 'json') {
|
|
1372
|
-
|
|
1552
|
+
let parsedJson: unknown;
|
|
1553
|
+
try {
|
|
1554
|
+
parsedJson = bodyText ? JSON.parse(bodyText) : undefined;
|
|
1555
|
+
} catch (error) {
|
|
1556
|
+
throw {
|
|
1557
|
+
code: 'E_EXECUTION',
|
|
1558
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1559
|
+
details: {
|
|
1560
|
+
kind: 'execution',
|
|
1561
|
+
retryable: false,
|
|
1562
|
+
where: 'complete',
|
|
1563
|
+
timing: diagnosticsBase.timing,
|
|
1564
|
+
requestSent: diagnosticsBase.requestSent,
|
|
1565
|
+
responseStarted: diagnosticsBase.responseStarted,
|
|
1566
|
+
status: response.status,
|
|
1567
|
+
headersReceived: headerMap,
|
|
1568
|
+
bodyBytesRead: bodyBytes,
|
|
1569
|
+
partialBodyPreview: diagnosticsBase.partialBodyPreview || undefined,
|
|
1570
|
+
hints: requestHints,
|
|
1571
|
+
diagnostics: {
|
|
1572
|
+
kind: 'execution',
|
|
1573
|
+
retryable: false,
|
|
1574
|
+
where: 'complete',
|
|
1575
|
+
timing: diagnosticsBase.timing,
|
|
1576
|
+
requestSent: diagnosticsBase.requestSent,
|
|
1577
|
+
responseStarted: diagnosticsBase.responseStarted,
|
|
1578
|
+
status: response.status,
|
|
1579
|
+
headersReceived: headerMap,
|
|
1580
|
+
bodyBytesRead: bodyBytes,
|
|
1581
|
+
partialBodyPreview: diagnosticsBase.partialBodyPreview || undefined,
|
|
1582
|
+
hints: requestHints
|
|
1583
|
+
} satisfies FetchDiagnostics
|
|
1584
|
+
}
|
|
1585
|
+
};
|
|
1586
|
+
}
|
|
1373
1587
|
const summary = buildJsonSummary(parsedJson);
|
|
1374
1588
|
if (fullResponse || !truncated) {
|
|
1375
1589
|
result.json = parsedJson;
|
|
@@ -1444,10 +1658,10 @@ function truncateNetworkEntry(entry: NetworkEntry, bodyBytes?: number): NetworkE
|
|
|
1444
1658
|
return clone;
|
|
1445
1659
|
}
|
|
1446
1660
|
|
|
1447
|
-
function filterNetworkEntrySections(entry: NetworkEntry, include: unknown): NetworkEntry {
|
|
1448
|
-
if (!Array.isArray(include)) {
|
|
1449
|
-
return entry;
|
|
1450
|
-
}
|
|
1661
|
+
function filterNetworkEntrySections(entry: NetworkEntry, include: unknown): NetworkEntry {
|
|
1662
|
+
if (!Array.isArray(include)) {
|
|
1663
|
+
return entry;
|
|
1664
|
+
}
|
|
1451
1665
|
const sections = new Set(
|
|
1452
1666
|
include
|
|
1453
1667
|
.map(String)
|
|
@@ -1456,20 +1670,34 @@ function filterNetworkEntrySections(entry: NetworkEntry, include: unknown): Netw
|
|
|
1456
1670
|
if (sections.size === 0 || sections.size === 2) {
|
|
1457
1671
|
return entry;
|
|
1458
1672
|
}
|
|
1459
|
-
const clone: NetworkEntry = { ...entry };
|
|
1460
|
-
if (!sections.has('request')) {
|
|
1461
|
-
delete clone.requestHeaders;
|
|
1462
|
-
delete clone.requestBodyPreview;
|
|
1463
|
-
delete clone.requestBodyTruncated;
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1673
|
+
const clone: NetworkEntry = { ...entry };
|
|
1674
|
+
if (!sections.has('request')) {
|
|
1675
|
+
delete clone.requestHeaders;
|
|
1676
|
+
delete clone.requestBodyPreview;
|
|
1677
|
+
delete clone.requestBodyTruncated;
|
|
1678
|
+
if (clone.preview) {
|
|
1679
|
+
clone.preview = { ...clone.preview };
|
|
1680
|
+
delete clone.preview.request;
|
|
1681
|
+
if (!clone.preview.query && !clone.preview.request && !clone.preview.response) {
|
|
1682
|
+
delete clone.preview;
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
1685
|
+
}
|
|
1686
|
+
if (!sections.has('response')) {
|
|
1687
|
+
delete clone.responseHeaders;
|
|
1688
|
+
delete clone.responseBodyPreview;
|
|
1689
|
+
delete clone.responseBodyTruncated;
|
|
1690
|
+
delete clone.binary;
|
|
1691
|
+
if (clone.preview) {
|
|
1692
|
+
clone.preview = { ...clone.preview };
|
|
1693
|
+
delete clone.preview.response;
|
|
1694
|
+
if (!clone.preview.query && !clone.preview.request && !clone.preview.response) {
|
|
1695
|
+
delete clone.preview;
|
|
1696
|
+
}
|
|
1697
|
+
}
|
|
1698
|
+
}
|
|
1699
|
+
return clone;
|
|
1700
|
+
}
|
|
1473
1701
|
|
|
1474
1702
|
function replayHeadersFromRequestHeaders(requestHeaders: Record<string, string> | undefined): Record<string, string> | undefined {
|
|
1475
1703
|
if (!requestHeaders) {
|
|
@@ -1485,6 +1713,121 @@ function replayHeadersFromRequestHeaders(requestHeaders: Record<string, string>
|
|
|
1485
1713
|
}
|
|
1486
1714
|
return Object.keys(headers).length > 0 ? headers : undefined;
|
|
1487
1715
|
}
|
|
1716
|
+
|
|
1717
|
+
function cloneHeadersFromRequestHeaders(requestHeaders: Record<string, string> | undefined): {
|
|
1718
|
+
headers?: Record<string, string>;
|
|
1719
|
+
omitted: string[];
|
|
1720
|
+
} {
|
|
1721
|
+
if (!requestHeaders) {
|
|
1722
|
+
return { omitted: [] };
|
|
1723
|
+
}
|
|
1724
|
+
const headers: Record<string, string> = {};
|
|
1725
|
+
const omitted = new Set<string>();
|
|
1726
|
+
for (const [name, value] of Object.entries(requestHeaders)) {
|
|
1727
|
+
const normalizedName = name.toLowerCase();
|
|
1728
|
+
if (CLONE_FORBIDDEN_HEADER_NAMES.has(normalizedName) || normalizedName.startsWith('sec-')) {
|
|
1729
|
+
omitted.add(normalizedName);
|
|
1730
|
+
continue;
|
|
1731
|
+
}
|
|
1732
|
+
headers[name] = value;
|
|
1733
|
+
}
|
|
1734
|
+
return {
|
|
1735
|
+
headers: Object.keys(headers).length > 0 ? headers : undefined,
|
|
1736
|
+
omitted: [...omitted].sort()
|
|
1737
|
+
};
|
|
1738
|
+
}
|
|
1739
|
+
|
|
1740
|
+
function isSameOriginWithTab(tabUrl: string | undefined, requestUrl: string): boolean {
|
|
1741
|
+
try {
|
|
1742
|
+
if (!tabUrl) {
|
|
1743
|
+
return false;
|
|
1744
|
+
}
|
|
1745
|
+
return new URL(requestUrl).origin === new URL(tabUrl).origin;
|
|
1746
|
+
} catch {
|
|
1747
|
+
return false;
|
|
1748
|
+
}
|
|
1749
|
+
}
|
|
1750
|
+
|
|
1751
|
+
function buildNetworkCloneResult(
|
|
1752
|
+
requestId: string,
|
|
1753
|
+
tabUrl: string | undefined,
|
|
1754
|
+
replayable: NonNullable<ReturnType<typeof getReplayableNetworkRequest>>
|
|
1755
|
+
): NetworkCloneResult {
|
|
1756
|
+
const sameOrigin = isSameOriginWithTab(tabUrl, replayable.entry.url);
|
|
1757
|
+
const cloneable = sameOrigin && replayable.bodyTruncated !== true;
|
|
1758
|
+
const notes: string[] = [];
|
|
1759
|
+
const sanitizedHeaders = cloneHeadersFromRequestHeaders(replayable.headers);
|
|
1760
|
+
if (sanitizedHeaders.omitted.length > 0) {
|
|
1761
|
+
notes.push(`omitted sensitive headers: ${sanitizedHeaders.omitted.join(', ')}`);
|
|
1762
|
+
}
|
|
1763
|
+
if (!sameOrigin) {
|
|
1764
|
+
notes.push('preferred page.fetch template was skipped because the captured request is not same-origin with the current page');
|
|
1765
|
+
}
|
|
1766
|
+
if (replayable.bodyTruncated) {
|
|
1767
|
+
notes.push('captured request body was truncated, so bak cannot emit a reliable page.fetch clone');
|
|
1768
|
+
}
|
|
1769
|
+
if (!sameOrigin || replayable.bodyTruncated) {
|
|
1770
|
+
notes.push('falling back to network.replay preserves the captured request shape more safely');
|
|
1771
|
+
} else {
|
|
1772
|
+
notes.push('page.fetch clone keeps session cookies and auto-applies same-origin auth helpers');
|
|
1773
|
+
}
|
|
1774
|
+
|
|
1775
|
+
const pageFetch =
|
|
1776
|
+
sameOrigin && replayable.bodyTruncated !== true
|
|
1777
|
+
? {
|
|
1778
|
+
url: replayable.entry.url,
|
|
1779
|
+
method: replayable.entry.method,
|
|
1780
|
+
headers: sanitizedHeaders.headers,
|
|
1781
|
+
body: replayable.body,
|
|
1782
|
+
contentType: replayable.contentType,
|
|
1783
|
+
mode:
|
|
1784
|
+
(replayable.entry.contentType ?? replayable.contentType)?.toLowerCase().includes('json')
|
|
1785
|
+
? ('json' as const)
|
|
1786
|
+
: ('raw' as const),
|
|
1787
|
+
auth: 'auto' as const
|
|
1788
|
+
}
|
|
1789
|
+
: undefined;
|
|
1790
|
+
|
|
1791
|
+
const preferredArgv =
|
|
1792
|
+
pageFetch
|
|
1793
|
+
? [
|
|
1794
|
+
'page',
|
|
1795
|
+
'fetch',
|
|
1796
|
+
'--url',
|
|
1797
|
+
pageFetch.url,
|
|
1798
|
+
'--method',
|
|
1799
|
+
pageFetch.method,
|
|
1800
|
+
'--auth',
|
|
1801
|
+
pageFetch.auth,
|
|
1802
|
+
'--mode',
|
|
1803
|
+
pageFetch.mode ?? 'raw',
|
|
1804
|
+
...(pageFetch.contentType ? ['--content-type', pageFetch.contentType] : []),
|
|
1805
|
+
...Object.entries(pageFetch.headers ?? {}).flatMap(([name, value]) => ['--header', `${name}: ${value}`]),
|
|
1806
|
+
...(typeof pageFetch.body === 'string' ? ['--body', pageFetch.body] : [])
|
|
1807
|
+
]
|
|
1808
|
+
: ['network', 'replay', '--request-id', requestId, '--auth', 'auto'];
|
|
1809
|
+
|
|
1810
|
+
return {
|
|
1811
|
+
request: {
|
|
1812
|
+
id: replayable.entry.id,
|
|
1813
|
+
url: replayable.entry.url,
|
|
1814
|
+
method: replayable.entry.method,
|
|
1815
|
+
kind: replayable.entry.kind,
|
|
1816
|
+
contentType: replayable.contentType,
|
|
1817
|
+
sameOrigin,
|
|
1818
|
+
bodyPresent: typeof replayable.body === 'string' && replayable.body.length > 0,
|
|
1819
|
+
bodyTruncated: replayable.bodyTruncated
|
|
1820
|
+
},
|
|
1821
|
+
cloneable,
|
|
1822
|
+
preferredCommand: {
|
|
1823
|
+
tool: pageFetch ? 'page.fetch' : 'network.replay',
|
|
1824
|
+
argv: preferredArgv,
|
|
1825
|
+
powershell: renderPowerShellCommand(preferredArgv)
|
|
1826
|
+
},
|
|
1827
|
+
pageFetch,
|
|
1828
|
+
notes
|
|
1829
|
+
};
|
|
1830
|
+
}
|
|
1488
1831
|
|
|
1489
1832
|
function collectTimestampMatchesFromText(text: string, source: TimestampEvidenceCandidate['source'], patterns?: string[]): TimestampEvidenceCandidate[] {
|
|
1490
1833
|
const regexes = (
|
|
@@ -2000,6 +2343,7 @@ async function enrichReplayWithSchema(tabId: number, requestId: string, response
|
|
|
2000
2343
|
const pageDataCandidates = await probePageDataCandidatesForTab(tabId, inspection);
|
|
2001
2344
|
const recentNetwork = listNetworkEntries(tabId, { limit: 25 });
|
|
2002
2345
|
const pageDataReport = buildInspectPageDataResult({
|
|
2346
|
+
pageUrl: inspection.url,
|
|
2003
2347
|
suspiciousGlobals: inspection.suspiciousGlobals ?? [],
|
|
2004
2348
|
tables: inspection.tables ?? [],
|
|
2005
2349
|
visibleTimestamps: inspection.visibleTimestamps ?? [],
|
|
@@ -2007,7 +2351,9 @@ async function enrichReplayWithSchema(tabId: number, requestId: string, response
|
|
|
2007
2351
|
pageDataCandidates,
|
|
2008
2352
|
recentNetwork,
|
|
2009
2353
|
tableAnalyses: tables,
|
|
2010
|
-
inlineJsonSources: Array.isArray(inspection.inlineJsonSources) ? inspection.inlineJsonSources : []
|
|
2354
|
+
inlineJsonSources: Array.isArray(inspection.inlineJsonSources) ? inspection.inlineJsonSources : [],
|
|
2355
|
+
modeGroups: Array.isArray(inspection.modeGroups) ? inspection.modeGroups : [],
|
|
2356
|
+
dateControls: Array.isArray(inspection.dateControls) ? inspection.dateControls : []
|
|
2011
2357
|
});
|
|
2012
2358
|
const matched = selectReplaySchemaMatch(response.json, tables, {
|
|
2013
2359
|
preferredSourceId: `networkResponse:${requestId}`,
|
|
@@ -2437,14 +2783,19 @@ async function handleRequest(request: CliRequest): Promise<unknown> {
|
|
|
2437
2783
|
const tab = await withTab(target);
|
|
2438
2784
|
try {
|
|
2439
2785
|
await ensureTabNetworkCapture(tab.id!);
|
|
2440
|
-
return {
|
|
2441
|
-
entries: listNetworkEntries(tab.id!, {
|
|
2442
|
-
limit: typeof params.limit === 'number' ? params.limit : undefined,
|
|
2443
|
-
urlIncludes: typeof params.urlIncludes === 'string' ? params.urlIncludes : undefined,
|
|
2444
|
-
status: typeof params.status === 'number' ? params.status : undefined,
|
|
2445
|
-
method: typeof params.method === 'string' ? params.method : undefined
|
|
2446
|
-
|
|
2447
|
-
|
|
2786
|
+
return {
|
|
2787
|
+
entries: listNetworkEntries(tab.id!, {
|
|
2788
|
+
limit: typeof params.limit === 'number' ? params.limit : undefined,
|
|
2789
|
+
urlIncludes: typeof params.urlIncludes === 'string' ? params.urlIncludes : undefined,
|
|
2790
|
+
status: typeof params.status === 'number' ? params.status : undefined,
|
|
2791
|
+
method: typeof params.method === 'string' ? params.method : undefined,
|
|
2792
|
+
domain: typeof params.domain === 'string' ? params.domain : undefined,
|
|
2793
|
+
resourceType: typeof params.resourceType === 'string' ? params.resourceType : undefined,
|
|
2794
|
+
kind: typeof params.kind === 'string' ? (params.kind as NetworkEntry['kind']) : undefined,
|
|
2795
|
+
sinceTs: typeof params.sinceTs === 'number' ? params.sinceTs : undefined,
|
|
2796
|
+
tail: params.tail === true
|
|
2797
|
+
})
|
|
2798
|
+
};
|
|
2448
2799
|
} catch {
|
|
2449
2800
|
return await forwardContentRpc(tab.id!, 'network.list', params);
|
|
2450
2801
|
}
|
|
@@ -2479,10 +2830,21 @@ async function handleRequest(request: CliRequest): Promise<unknown> {
|
|
|
2479
2830
|
return searchNetworkEntries(tab.id!, String(params.pattern ?? ''), typeof params.limit === 'number' ? params.limit : 50);
|
|
2480
2831
|
});
|
|
2481
2832
|
}
|
|
2482
|
-
case 'network.
|
|
2483
|
-
return await preserveHumanFocus(typeof target.tabId !== 'number', async () => {
|
|
2484
|
-
const tab = await withTab(target);
|
|
2485
|
-
|
|
2833
|
+
case 'network.clone': {
|
|
2834
|
+
return await preserveHumanFocus(typeof target.tabId !== 'number', async () => {
|
|
2835
|
+
const tab = await withTab(target);
|
|
2836
|
+
await ensureTabNetworkCapture(tab.id!);
|
|
2837
|
+
const replayable = getReplayableNetworkRequest(tab.id!, String(params.id ?? ''));
|
|
2838
|
+
if (!replayable) {
|
|
2839
|
+
throw toError('E_NOT_FOUND', `network entry not found: ${String(params.id ?? '')}`);
|
|
2840
|
+
}
|
|
2841
|
+
return buildNetworkCloneResult(String(params.id ?? ''), tab.url, replayable);
|
|
2842
|
+
});
|
|
2843
|
+
}
|
|
2844
|
+
case 'network.waitFor': {
|
|
2845
|
+
return await preserveHumanFocus(typeof target.tabId !== 'number', async () => {
|
|
2846
|
+
const tab = await withTab(target);
|
|
2847
|
+
try {
|
|
2486
2848
|
await ensureTabNetworkCapture(tab.id!);
|
|
2487
2849
|
} catch {
|
|
2488
2850
|
return await forwardContentRpc(tab.id!, 'network.waitFor', params);
|
|
@@ -2631,9 +2993,10 @@ async function handleRequest(request: CliRequest): Promise<unknown> {
|
|
|
2631
2993
|
await ensureNetworkDebugger(tab.id!).catch(() => undefined);
|
|
2632
2994
|
const inspection = await collectPageInspection(tab.id!, params);
|
|
2633
2995
|
const pageDataCandidates = await probePageDataCandidatesForTab(tab.id!, inspection);
|
|
2634
|
-
const network = listNetworkEntries(tab.id!, { limit:
|
|
2996
|
+
const network = listNetworkEntries(tab.id!, { limit: 25, tail: true });
|
|
2635
2997
|
const tableAnalyses = await collectTableAnalyses(tab.id!);
|
|
2636
2998
|
const enriched = buildInspectPageDataResult({
|
|
2999
|
+
pageUrl: inspection.url,
|
|
2637
3000
|
suspiciousGlobals: inspection.suspiciousGlobals ?? [],
|
|
2638
3001
|
tables: inspection.tables ?? [],
|
|
2639
3002
|
visibleTimestamps: inspection.visibleTimestamps ?? [],
|
|
@@ -2641,7 +3004,9 @@ async function handleRequest(request: CliRequest): Promise<unknown> {
|
|
|
2641
3004
|
pageDataCandidates,
|
|
2642
3005
|
recentNetwork: network,
|
|
2643
3006
|
tableAnalyses,
|
|
2644
|
-
inlineJsonSources: Array.isArray(inspection.inlineJsonSources) ? inspection.inlineJsonSources : []
|
|
3007
|
+
inlineJsonSources: Array.isArray(inspection.inlineJsonSources) ? inspection.inlineJsonSources : [],
|
|
3008
|
+
modeGroups: Array.isArray(inspection.modeGroups) ? inspection.modeGroups : [],
|
|
3009
|
+
dateControls: Array.isArray(inspection.dateControls) ? inspection.dateControls : []
|
|
2645
3010
|
});
|
|
2646
3011
|
const recommendedNextSteps = enriched.recommendedNextActions.map((action) => action.command);
|
|
2647
3012
|
return {
|
|
@@ -2651,6 +3016,12 @@ async function handleRequest(request: CliRequest): Promise<unknown> {
|
|
|
2651
3016
|
inlineTimestamps: inspection.inlineTimestamps ?? [],
|
|
2652
3017
|
pageDataCandidates,
|
|
2653
3018
|
recentNetwork: network,
|
|
3019
|
+
modeGroups: Array.isArray(inspection.modeGroups) ? inspection.modeGroups : [],
|
|
3020
|
+
availableModes: enriched.availableModes,
|
|
3021
|
+
currentMode: enriched.currentMode,
|
|
3022
|
+
dateControls: Array.isArray(inspection.dateControls) ? inspection.dateControls : [],
|
|
3023
|
+
latestArchiveDate: enriched.latestArchiveDate,
|
|
3024
|
+
primaryEndpoint: enriched.primaryEndpoint,
|
|
2654
3025
|
dataSources: enriched.dataSources,
|
|
2655
3026
|
sourceMappings: enriched.sourceMappings,
|
|
2656
3027
|
recommendedNextActions: enriched.recommendedNextActions,
|