@flrande/bak-extension 0.5.0 → 0.6.0
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 +1139 -32
- package/dist/content.global.js +791 -3
- package/dist/manifest.json +2 -2
- package/package.json +2 -2
- package/public/manifest.json +2 -2
- package/src/background.ts +861 -112
- package/src/content.ts +925 -130
- package/src/network-debugger.ts +495 -0
- package/src/privacy.ts +112 -1
|
@@ -1,5 +1,438 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
(() => {
|
|
3
|
+
// src/privacy.ts
|
|
4
|
+
var HIGH_ENTROPY_TOKEN_PATTERN = /^(?=.*\d)(?=.*[a-zA-Z])[A-Za-z0-9~!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?`]{16,}$/;
|
|
5
|
+
var TRANSPORT_SECRET_KEY_SOURCE = "(?:api[_-]?key|authorization|auth|cookie|csrf(?:token)?|nonce|password|passwd|secret|session(?:id)?|token|xsrf(?:token)?)";
|
|
6
|
+
var TRANSPORT_SECRET_PAIR_PATTERN = new RegExp(`((?:^|[?&;,\\s])${TRANSPORT_SECRET_KEY_SOURCE}=)[^&\\r\\n"'>]*`, "gi");
|
|
7
|
+
var JSON_SECRET_VALUE_PATTERN = new RegExp(
|
|
8
|
+
`((?:"|')${TRANSPORT_SECRET_KEY_SOURCE}(?:"|')\\s*:\\s*)(?:"[^"]*"|'[^']*'|true|false|null|-?\\d+(?:\\.\\d+)?)`,
|
|
9
|
+
"gi"
|
|
10
|
+
);
|
|
11
|
+
var ASSIGNMENT_SECRET_VALUE_PATTERN = new RegExp(
|
|
12
|
+
`((?:^|[\\s,{;])${TRANSPORT_SECRET_KEY_SOURCE}\\s*[:=]\\s*)([^,&;}"'\\r\\n]+)`,
|
|
13
|
+
"gi"
|
|
14
|
+
);
|
|
15
|
+
var AUTHORIZATION_VALUE_PATTERN = /\b(Bearer|Basic)\s+[A-Za-z0-9._~+/=-]+\b/gi;
|
|
16
|
+
var SENSITIVE_HEADER_PATTERNS = [
|
|
17
|
+
/^authorization$/i,
|
|
18
|
+
/^proxy-authorization$/i,
|
|
19
|
+
/^cookie$/i,
|
|
20
|
+
/^set-cookie$/i,
|
|
21
|
+
/^x-csrf-token$/i,
|
|
22
|
+
/^x-xsrf-token$/i,
|
|
23
|
+
/^csrf-token$/i,
|
|
24
|
+
/^x-auth-token$/i,
|
|
25
|
+
/^x-api-key$/i,
|
|
26
|
+
/^api-key$/i
|
|
27
|
+
];
|
|
28
|
+
function redactTransportSecrets(text) {
|
|
29
|
+
let output = text;
|
|
30
|
+
output = output.replace(AUTHORIZATION_VALUE_PATTERN, "$1 [REDACTED]");
|
|
31
|
+
output = output.replace(TRANSPORT_SECRET_PAIR_PATTERN, "$1[REDACTED]");
|
|
32
|
+
output = output.replace(JSON_SECRET_VALUE_PATTERN, '$1"[REDACTED]"');
|
|
33
|
+
output = output.replace(ASSIGNMENT_SECRET_VALUE_PATTERN, "$1[REDACTED]");
|
|
34
|
+
if (HIGH_ENTROPY_TOKEN_PATTERN.test(output) && !/[ =&:]/.test(output) && !output.includes("[REDACTED")) {
|
|
35
|
+
return "[REDACTED:secret]";
|
|
36
|
+
}
|
|
37
|
+
return output;
|
|
38
|
+
}
|
|
39
|
+
function shouldRedactHeader(name) {
|
|
40
|
+
return SENSITIVE_HEADER_PATTERNS.some((pattern) => pattern.test(name));
|
|
41
|
+
}
|
|
42
|
+
function containsRedactionMarker(raw) {
|
|
43
|
+
return typeof raw === "string" && raw.includes("[REDACTED");
|
|
44
|
+
}
|
|
45
|
+
function redactTransportText(raw) {
|
|
46
|
+
if (!raw) {
|
|
47
|
+
return "";
|
|
48
|
+
}
|
|
49
|
+
return redactTransportSecrets(String(raw));
|
|
50
|
+
}
|
|
51
|
+
function redactHeaderMap(headers) {
|
|
52
|
+
if (!headers) {
|
|
53
|
+
return void 0;
|
|
54
|
+
}
|
|
55
|
+
const result = {};
|
|
56
|
+
for (const [name, value] of Object.entries(headers)) {
|
|
57
|
+
result[name] = shouldRedactHeader(name) ? `[REDACTED:${name.toLowerCase()}]` : redactTransportText(value);
|
|
58
|
+
}
|
|
59
|
+
return Object.keys(result).length > 0 ? result : void 0;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// src/network-debugger.ts
|
|
63
|
+
var DEBUGGER_VERSION = "1.3";
|
|
64
|
+
var MAX_ENTRIES = 1e3;
|
|
65
|
+
var DEFAULT_BODY_BYTES = 8 * 1024;
|
|
66
|
+
var DEFAULT_TOTAL_BODY_BYTES = 256 * 1024;
|
|
67
|
+
var textEncoder = new TextEncoder();
|
|
68
|
+
var textDecoder = new TextDecoder();
|
|
69
|
+
var captures = /* @__PURE__ */ new Map();
|
|
70
|
+
function getState(tabId) {
|
|
71
|
+
const existing = captures.get(tabId);
|
|
72
|
+
if (existing) {
|
|
73
|
+
return existing;
|
|
74
|
+
}
|
|
75
|
+
const created = {
|
|
76
|
+
attached: false,
|
|
77
|
+
attachError: null,
|
|
78
|
+
entries: [],
|
|
79
|
+
entriesById: /* @__PURE__ */ new Map(),
|
|
80
|
+
requestIdToEntryId: /* @__PURE__ */ new Map(),
|
|
81
|
+
lastTouchedAt: Date.now()
|
|
82
|
+
};
|
|
83
|
+
captures.set(tabId, created);
|
|
84
|
+
return created;
|
|
85
|
+
}
|
|
86
|
+
function debuggerTarget(tabId) {
|
|
87
|
+
return { tabId };
|
|
88
|
+
}
|
|
89
|
+
function utf8ByteLength(value) {
|
|
90
|
+
return textEncoder.encode(value).byteLength;
|
|
91
|
+
}
|
|
92
|
+
function truncateUtf8(value, limit) {
|
|
93
|
+
const encoded = textEncoder.encode(value);
|
|
94
|
+
if (encoded.byteLength <= limit) {
|
|
95
|
+
return value;
|
|
96
|
+
}
|
|
97
|
+
return textDecoder.decode(encoded.subarray(0, limit));
|
|
98
|
+
}
|
|
99
|
+
function decodeBase64Utf8(value) {
|
|
100
|
+
const binary = atob(value);
|
|
101
|
+
const bytes = new Uint8Array(binary.length);
|
|
102
|
+
for (let index = 0; index < binary.length; index += 1) {
|
|
103
|
+
bytes[index] = binary.charCodeAt(index);
|
|
104
|
+
}
|
|
105
|
+
return textDecoder.decode(bytes);
|
|
106
|
+
}
|
|
107
|
+
function truncateText(value, limit = DEFAULT_BODY_BYTES) {
|
|
108
|
+
if (typeof value !== "string") {
|
|
109
|
+
return { truncated: false };
|
|
110
|
+
}
|
|
111
|
+
const bytes = utf8ByteLength(value);
|
|
112
|
+
if (bytes <= limit) {
|
|
113
|
+
return { text: value, truncated: false, bytes };
|
|
114
|
+
}
|
|
115
|
+
const truncatedText = truncateUtf8(value, limit);
|
|
116
|
+
return { text: truncatedText, truncated: true, bytes };
|
|
117
|
+
}
|
|
118
|
+
function normalizeHeaders(headers) {
|
|
119
|
+
if (typeof headers !== "object" || headers === null) {
|
|
120
|
+
return void 0;
|
|
121
|
+
}
|
|
122
|
+
const result = {};
|
|
123
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
124
|
+
if (value === void 0 || value === null) {
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
result[String(key)] = Array.isArray(value) ? value.map(String).join(", ") : String(value);
|
|
128
|
+
}
|
|
129
|
+
return Object.keys(result).length > 0 ? result : void 0;
|
|
130
|
+
}
|
|
131
|
+
function headerValue(headers, name) {
|
|
132
|
+
if (!headers) {
|
|
133
|
+
return void 0;
|
|
134
|
+
}
|
|
135
|
+
const lower = name.toLowerCase();
|
|
136
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
137
|
+
if (key.toLowerCase() === lower) {
|
|
138
|
+
return value;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return void 0;
|
|
142
|
+
}
|
|
143
|
+
function isTextualContentType(contentType) {
|
|
144
|
+
if (!contentType) {
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
const normalized = contentType.toLowerCase();
|
|
148
|
+
return normalized.startsWith("text/") || normalized.includes("json") || normalized.includes("javascript") || normalized.includes("xml") || normalized.includes("html") || normalized.includes("urlencoded") || normalized.includes("graphql");
|
|
149
|
+
}
|
|
150
|
+
function pushEntry(state, entry, requestId) {
|
|
151
|
+
state.entries.push(entry);
|
|
152
|
+
state.entriesById.set(entry.id, entry);
|
|
153
|
+
state.requestIdToEntryId.set(requestId, entry.id);
|
|
154
|
+
state.lastTouchedAt = Date.now();
|
|
155
|
+
while (state.entries.length > MAX_ENTRIES) {
|
|
156
|
+
const removed = state.entries.shift();
|
|
157
|
+
if (!removed) {
|
|
158
|
+
break;
|
|
159
|
+
}
|
|
160
|
+
state.entriesById.delete(removed.id);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
function entryForRequest(tabId, requestId) {
|
|
164
|
+
const state = captures.get(tabId);
|
|
165
|
+
if (!state) {
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
const entryId = state.requestIdToEntryId.get(requestId);
|
|
169
|
+
if (!entryId) {
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
return state.entriesById.get(entryId) ?? null;
|
|
173
|
+
}
|
|
174
|
+
async function sendDebuggerCommand(tabId, method, commandParams) {
|
|
175
|
+
return await chrome.debugger.sendCommand(debuggerTarget(tabId), method, commandParams);
|
|
176
|
+
}
|
|
177
|
+
async function getResponseBodyPreview(tabId, requestId, contentType) {
|
|
178
|
+
try {
|
|
179
|
+
const response = await sendDebuggerCommand(tabId, "Network.getResponseBody", { requestId }) ?? { body: "" };
|
|
180
|
+
const rawBody = typeof response.body === "string" ? response.body : "";
|
|
181
|
+
const base64Encoded = response.base64Encoded === true;
|
|
182
|
+
const binary = base64Encoded && !isTextualContentType(contentType);
|
|
183
|
+
if (binary) {
|
|
184
|
+
return { binary: true };
|
|
185
|
+
}
|
|
186
|
+
const decoded = base64Encoded ? decodeBase64Utf8(rawBody) : rawBody;
|
|
187
|
+
const preview = truncateText(decoded, DEFAULT_BODY_BYTES);
|
|
188
|
+
return {
|
|
189
|
+
responseBodyPreview: preview.text ? redactTransportText(preview.text) : void 0,
|
|
190
|
+
responseBodyTruncated: preview.truncated
|
|
191
|
+
};
|
|
192
|
+
} catch (error) {
|
|
193
|
+
const entry = entryForRequest(tabId, requestId);
|
|
194
|
+
if (entry) {
|
|
195
|
+
entry.failureReason = error instanceof Error ? error.message : String(error);
|
|
196
|
+
}
|
|
197
|
+
return {};
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
async function handleLoadingFinished(tabId, params) {
|
|
201
|
+
const requestId = String(params.requestId ?? "");
|
|
202
|
+
const entry = entryForRequest(tabId, requestId);
|
|
203
|
+
if (!entry) {
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
entry.durationMs = entry.startedAt ? Math.max(0, Date.now() - entry.startedAt) : entry.durationMs;
|
|
207
|
+
if (typeof params.encodedDataLength === "number") {
|
|
208
|
+
entry.responseBytes = Math.max(0, Math.round(params.encodedDataLength));
|
|
209
|
+
}
|
|
210
|
+
const body = await getResponseBodyPreview(tabId, requestId, entry.contentType);
|
|
211
|
+
Object.assign(entry, body);
|
|
212
|
+
if ((entry.requestBytes ?? 0) + (entry.responseBytes ?? 0) > DEFAULT_TOTAL_BODY_BYTES) {
|
|
213
|
+
entry.truncated = true;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
function upsertRequest(tabId, params) {
|
|
217
|
+
const state = getState(tabId);
|
|
218
|
+
const requestId = String(params.requestId ?? "");
|
|
219
|
+
if (!requestId) {
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
const request = typeof params.request === "object" && params.request !== null ? params.request : {};
|
|
223
|
+
const headers = redactHeaderMap(normalizeHeaders(request.headers));
|
|
224
|
+
const truncatedRequest = truncateText(typeof request.postData === "string" ? request.postData : void 0, DEFAULT_BODY_BYTES);
|
|
225
|
+
const entry = {
|
|
226
|
+
id: `net_${tabId}_${requestId}`,
|
|
227
|
+
url: typeof request.url === "string" ? request.url : "",
|
|
228
|
+
method: typeof request.method === "string" ? request.method : "GET",
|
|
229
|
+
status: 0,
|
|
230
|
+
ok: false,
|
|
231
|
+
kind: params.type === "XHR" ? "xhr" : params.type === "Fetch" ? "fetch" : params.type === "Document" ? "navigation" : "resource",
|
|
232
|
+
resourceType: typeof params.type === "string" ? String(params.type) : void 0,
|
|
233
|
+
ts: Date.now(),
|
|
234
|
+
startedAt: Date.now(),
|
|
235
|
+
durationMs: 0,
|
|
236
|
+
requestBytes: truncatedRequest.bytes,
|
|
237
|
+
requestHeaders: headers,
|
|
238
|
+
requestBodyPreview: truncatedRequest.text ? redactTransportText(truncatedRequest.text) : void 0,
|
|
239
|
+
requestBodyTruncated: truncatedRequest.truncated,
|
|
240
|
+
initiatorUrl: typeof params.initiator === "object" && params.initiator !== null && typeof params.initiator.url === "string" ? String(params.initiator.url) : void 0,
|
|
241
|
+
tabId,
|
|
242
|
+
source: "debugger"
|
|
243
|
+
};
|
|
244
|
+
pushEntry(state, entry, requestId);
|
|
245
|
+
}
|
|
246
|
+
function updateResponse(tabId, params) {
|
|
247
|
+
const requestId = String(params.requestId ?? "");
|
|
248
|
+
const entry = entryForRequest(tabId, requestId);
|
|
249
|
+
if (!entry) {
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
const response = typeof params.response === "object" && params.response !== null ? params.response : {};
|
|
253
|
+
const responseHeaders = redactHeaderMap(normalizeHeaders(response.headers));
|
|
254
|
+
entry.status = typeof response.status === "number" ? Math.round(response.status) : entry.status;
|
|
255
|
+
entry.ok = entry.status >= 200 && entry.status < 400;
|
|
256
|
+
entry.contentType = typeof response.mimeType === "string" ? response.mimeType : headerValue(responseHeaders, "content-type");
|
|
257
|
+
entry.responseHeaders = responseHeaders;
|
|
258
|
+
if (typeof response.encodedDataLength === "number") {
|
|
259
|
+
entry.responseBytes = Math.max(0, Math.round(response.encodedDataLength));
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
function updateFailure(tabId, params) {
|
|
263
|
+
const requestId = String(params.requestId ?? "");
|
|
264
|
+
const entry = entryForRequest(tabId, requestId);
|
|
265
|
+
if (!entry) {
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
entry.ok = false;
|
|
269
|
+
entry.failureReason = typeof params.errorText === "string" ? params.errorText : "loading failed";
|
|
270
|
+
entry.durationMs = entry.startedAt ? Math.max(0, Date.now() - entry.startedAt) : entry.durationMs;
|
|
271
|
+
}
|
|
272
|
+
chrome.debugger.onEvent.addListener((source, method, params) => {
|
|
273
|
+
const tabId = typeof source.tabId === "number" ? source.tabId : void 0;
|
|
274
|
+
if (typeof tabId !== "number") {
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
const payload = typeof params === "object" && params !== null ? params : {};
|
|
278
|
+
if (method === "Network.requestWillBeSent") {
|
|
279
|
+
upsertRequest(tabId, payload);
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
if (method === "Network.responseReceived") {
|
|
283
|
+
updateResponse(tabId, payload);
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
if (method === "Network.loadingFailed") {
|
|
287
|
+
updateFailure(tabId, payload);
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
if (method === "Network.loadingFinished") {
|
|
291
|
+
void handleLoadingFinished(tabId, payload);
|
|
292
|
+
}
|
|
293
|
+
});
|
|
294
|
+
chrome.debugger.onDetach.addListener((source, reason) => {
|
|
295
|
+
const tabId = typeof source.tabId === "number" ? source.tabId : void 0;
|
|
296
|
+
if (typeof tabId !== "number") {
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
const state = getState(tabId);
|
|
300
|
+
state.attached = false;
|
|
301
|
+
state.attachError = reason;
|
|
302
|
+
});
|
|
303
|
+
async function ensureNetworkDebugger(tabId) {
|
|
304
|
+
const state = getState(tabId);
|
|
305
|
+
if (state.attached) {
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
try {
|
|
309
|
+
await chrome.debugger.attach(debuggerTarget(tabId), DEBUGGER_VERSION);
|
|
310
|
+
} catch (error) {
|
|
311
|
+
state.attachError = error instanceof Error ? error.message : String(error);
|
|
312
|
+
throw error;
|
|
313
|
+
}
|
|
314
|
+
await sendDebuggerCommand(tabId, "Network.enable");
|
|
315
|
+
state.attached = true;
|
|
316
|
+
state.attachError = null;
|
|
317
|
+
}
|
|
318
|
+
function clearNetworkEntries(tabId) {
|
|
319
|
+
const state = getState(tabId);
|
|
320
|
+
state.entries = [];
|
|
321
|
+
state.entriesById.clear();
|
|
322
|
+
state.requestIdToEntryId.clear();
|
|
323
|
+
state.lastTouchedAt = Date.now();
|
|
324
|
+
}
|
|
325
|
+
function entryMatchesFilters(entry, filters) {
|
|
326
|
+
const urlIncludes = typeof filters.urlIncludes === "string" ? filters.urlIncludes : "";
|
|
327
|
+
const method = typeof filters.method === "string" ? filters.method.toUpperCase() : "";
|
|
328
|
+
const status = typeof filters.status === "number" ? filters.status : void 0;
|
|
329
|
+
if (urlIncludes && !entry.url.includes(urlIncludes)) {
|
|
330
|
+
return false;
|
|
331
|
+
}
|
|
332
|
+
if (method && entry.method.toUpperCase() !== method) {
|
|
333
|
+
return false;
|
|
334
|
+
}
|
|
335
|
+
if (typeof status === "number" && entry.status !== status) {
|
|
336
|
+
return false;
|
|
337
|
+
}
|
|
338
|
+
return true;
|
|
339
|
+
}
|
|
340
|
+
function listNetworkEntries(tabId, filters = {}) {
|
|
341
|
+
const state = getState(tabId);
|
|
342
|
+
const limit = typeof filters.limit === "number" ? Math.max(1, Math.min(500, Math.floor(filters.limit))) : 50;
|
|
343
|
+
return state.entries.filter((entry) => entryMatchesFilters(entry, filters)).slice(-limit).reverse().map((entry) => ({ ...entry }));
|
|
344
|
+
}
|
|
345
|
+
function getNetworkEntry(tabId, id) {
|
|
346
|
+
const state = getState(tabId);
|
|
347
|
+
const entry = state.entriesById.get(id);
|
|
348
|
+
return entry ? { ...entry } : null;
|
|
349
|
+
}
|
|
350
|
+
async function waitForNetworkEntry(tabId, filters = {}) {
|
|
351
|
+
const timeoutMs = typeof filters.timeoutMs === "number" ? Math.max(1, Math.floor(filters.timeoutMs)) : 5e3;
|
|
352
|
+
const deadline = Date.now() + timeoutMs;
|
|
353
|
+
const state = getState(tabId);
|
|
354
|
+
const seenIds = new Set(state.entries.filter((entry) => entryMatchesFilters(entry, filters)).map((entry) => entry.id));
|
|
355
|
+
while (Date.now() < deadline) {
|
|
356
|
+
const nextState = getState(tabId);
|
|
357
|
+
const matched = nextState.entries.find((entry) => !seenIds.has(entry.id) && entryMatchesFilters(entry, filters));
|
|
358
|
+
if (matched) {
|
|
359
|
+
return { ...matched };
|
|
360
|
+
}
|
|
361
|
+
await new Promise((resolve) => setTimeout(resolve, 75));
|
|
362
|
+
}
|
|
363
|
+
throw {
|
|
364
|
+
code: "E_TIMEOUT",
|
|
365
|
+
message: "network.waitFor timeout"
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
function searchNetworkEntries(tabId, pattern, limit = 50) {
|
|
369
|
+
const normalized = pattern.toLowerCase();
|
|
370
|
+
return listNetworkEntries(tabId, { limit: Math.max(limit, 1) }).filter((entry) => {
|
|
371
|
+
const headerText = JSON.stringify({
|
|
372
|
+
requestHeaders: entry.requestHeaders,
|
|
373
|
+
responseHeaders: entry.responseHeaders
|
|
374
|
+
}).toLowerCase();
|
|
375
|
+
return entry.url.toLowerCase().includes(normalized) || (entry.requestBodyPreview ?? "").toLowerCase().includes(normalized) || (entry.responseBodyPreview ?? "").toLowerCase().includes(normalized) || headerText.includes(normalized);
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
function latestNetworkTimestamp(tabId) {
|
|
379
|
+
const entries = listNetworkEntries(tabId, { limit: MAX_ENTRIES });
|
|
380
|
+
return entries.length > 0 ? entries[0].ts : null;
|
|
381
|
+
}
|
|
382
|
+
function recentNetworkSampleIds(tabId, limit = 5) {
|
|
383
|
+
return listNetworkEntries(tabId, { limit }).map((entry) => entry.id);
|
|
384
|
+
}
|
|
385
|
+
function exportHar(tabId, limit = MAX_ENTRIES) {
|
|
386
|
+
const entries = listNetworkEntries(tabId, { limit }).reverse();
|
|
387
|
+
return {
|
|
388
|
+
log: {
|
|
389
|
+
version: "1.2",
|
|
390
|
+
creator: {
|
|
391
|
+
name: "bak",
|
|
392
|
+
version: "0.6.0"
|
|
393
|
+
},
|
|
394
|
+
entries: entries.map((entry) => ({
|
|
395
|
+
startedDateTime: new Date(entry.startedAt ?? entry.ts).toISOString(),
|
|
396
|
+
time: entry.durationMs,
|
|
397
|
+
request: {
|
|
398
|
+
method: entry.method,
|
|
399
|
+
url: entry.url,
|
|
400
|
+
headers: Object.entries(entry.requestHeaders ?? {}).map(([name, value]) => ({ name, value })),
|
|
401
|
+
postData: typeof entry.requestBodyPreview === "string" ? {
|
|
402
|
+
mimeType: headerValue(entry.requestHeaders, "content-type") ?? "",
|
|
403
|
+
text: entry.requestBodyPreview
|
|
404
|
+
} : void 0,
|
|
405
|
+
headersSize: -1,
|
|
406
|
+
bodySize: entry.requestBytes ?? -1
|
|
407
|
+
},
|
|
408
|
+
response: {
|
|
409
|
+
status: entry.status,
|
|
410
|
+
statusText: entry.ok ? "OK" : entry.failureReason ?? "",
|
|
411
|
+
headers: Object.entries(entry.responseHeaders ?? {}).map(([name, value]) => ({ name, value })),
|
|
412
|
+
content: {
|
|
413
|
+
mimeType: entry.contentType ?? "",
|
|
414
|
+
size: entry.responseBytes ?? -1,
|
|
415
|
+
text: entry.binary ? void 0 : entry.responseBodyPreview,
|
|
416
|
+
comment: entry.binary ? "binary body omitted" : void 0
|
|
417
|
+
},
|
|
418
|
+
headersSize: -1,
|
|
419
|
+
bodySize: entry.responseBytes ?? -1
|
|
420
|
+
},
|
|
421
|
+
cache: {},
|
|
422
|
+
timings: {
|
|
423
|
+
send: 0,
|
|
424
|
+
wait: entry.durationMs,
|
|
425
|
+
receive: 0
|
|
426
|
+
},
|
|
427
|
+
_bak: entry
|
|
428
|
+
}))
|
|
429
|
+
}
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
function dropNetworkCapture(tabId) {
|
|
433
|
+
captures.delete(tabId);
|
|
434
|
+
}
|
|
435
|
+
|
|
3
436
|
// src/url-policy.ts
|
|
4
437
|
function isSupportedAutomationUrl(url) {
|
|
5
438
|
if (typeof url !== "string" || !url) {
|
|
@@ -106,19 +539,19 @@
|
|
|
106
539
|
const created = !persisted;
|
|
107
540
|
let state = this.normalizeState(persisted, workspaceId);
|
|
108
541
|
const originalWindowId = state.windowId;
|
|
109
|
-
let
|
|
542
|
+
let window2 = state.windowId !== null ? await this.waitForWindow(state.windowId) : null;
|
|
110
543
|
let tabs = [];
|
|
111
|
-
if (!
|
|
544
|
+
if (!window2) {
|
|
112
545
|
const rebound = await this.rebindWorkspaceWindow(state);
|
|
113
546
|
if (rebound) {
|
|
114
|
-
|
|
547
|
+
window2 = rebound.window;
|
|
115
548
|
tabs = rebound.tabs;
|
|
116
549
|
if (originalWindowId !== rebound.window.id) {
|
|
117
550
|
repairActions.push("rebound-window");
|
|
118
551
|
}
|
|
119
552
|
}
|
|
120
553
|
}
|
|
121
|
-
if (!
|
|
554
|
+
if (!window2) {
|
|
122
555
|
const createdWindow = await this.browser.createWindow({
|
|
123
556
|
url: initialUrl,
|
|
124
557
|
focused: options.focus === true
|
|
@@ -128,7 +561,7 @@
|
|
|
128
561
|
state.tabIds = [];
|
|
129
562
|
state.activeTabId = null;
|
|
130
563
|
state.primaryTabId = null;
|
|
131
|
-
|
|
564
|
+
window2 = createdWindow;
|
|
132
565
|
tabs = await this.waitForWindowTabs(createdWindow.id);
|
|
133
566
|
state.tabIds = tabs.map((tab) => tab.id);
|
|
134
567
|
if (state.primaryTabId === null) {
|
|
@@ -153,7 +586,7 @@
|
|
|
153
586
|
const ownership = await this.inspectWorkspaceWindowOwnership(state, state.windowId);
|
|
154
587
|
if (ownership.foreignTabs.length > 0) {
|
|
155
588
|
const migrated = await this.moveWorkspaceIntoDedicatedWindow(state, ownership, initialUrl);
|
|
156
|
-
|
|
589
|
+
window2 = migrated.window;
|
|
157
590
|
tabs = migrated.tabs;
|
|
158
591
|
state.tabIds = tabs.map((tab) => tab.id);
|
|
159
592
|
repairActions.push("migrated-dirty-window");
|
|
@@ -216,8 +649,8 @@
|
|
|
216
649
|
state.tabIds = [...new Set(tabs.map((tab) => tab.id))];
|
|
217
650
|
if (options.focus === true && state.activeTabId !== null) {
|
|
218
651
|
await this.browser.updateTab(state.activeTabId, { active: true });
|
|
219
|
-
|
|
220
|
-
void
|
|
652
|
+
window2 = await this.browser.updateWindow(state.windowId, { focused: true });
|
|
653
|
+
void window2;
|
|
221
654
|
repairActions.push("focused-window");
|
|
222
655
|
}
|
|
223
656
|
await this.storage.save(state);
|
|
@@ -556,8 +989,8 @@
|
|
|
556
989
|
pushWindowId(tab.windowId);
|
|
557
990
|
}
|
|
558
991
|
for (const candidateWindowId of candidateWindowIds) {
|
|
559
|
-
const
|
|
560
|
-
if (!
|
|
992
|
+
const window2 = await this.waitForWindow(candidateWindowId);
|
|
993
|
+
if (!window2) {
|
|
561
994
|
continue;
|
|
562
995
|
}
|
|
563
996
|
let tabs = await this.readTrackedTabs(this.collectCandidateTabIds(state), candidateWindowId);
|
|
@@ -578,7 +1011,7 @@
|
|
|
578
1011
|
state.activeTabId = tabs.find((tab) => tab.active)?.id ?? state.primaryTabId;
|
|
579
1012
|
}
|
|
580
1013
|
}
|
|
581
|
-
return { window, tabs };
|
|
1014
|
+
return { window: window2, tabs };
|
|
582
1015
|
}
|
|
583
1016
|
return null;
|
|
584
1017
|
}
|
|
@@ -593,11 +1026,11 @@
|
|
|
593
1026
|
async moveWorkspaceIntoDedicatedWindow(state, ownership, initialUrl) {
|
|
594
1027
|
const sourceTabs = this.orderWorkspaceTabsForMigration(state, ownership.workspaceTabs);
|
|
595
1028
|
const seedUrl = sourceTabs[0]?.url ?? initialUrl;
|
|
596
|
-
const
|
|
1029
|
+
const window2 = await this.browser.createWindow({
|
|
597
1030
|
url: seedUrl || DEFAULT_WORKSPACE_URL,
|
|
598
1031
|
focused: false
|
|
599
1032
|
});
|
|
600
|
-
const recreatedTabs = await this.waitForWindowTabs(
|
|
1033
|
+
const recreatedTabs = await this.waitForWindowTabs(window2.id);
|
|
601
1034
|
const firstTab = recreatedTabs[0] ?? null;
|
|
602
1035
|
const tabIdMap = /* @__PURE__ */ new Map();
|
|
603
1036
|
if (sourceTabs[0] && firstTab) {
|
|
@@ -605,7 +1038,7 @@
|
|
|
605
1038
|
}
|
|
606
1039
|
for (const sourceTab of sourceTabs.slice(1)) {
|
|
607
1040
|
const recreated = await this.createWorkspaceTab({
|
|
608
|
-
windowId:
|
|
1041
|
+
windowId: window2.id,
|
|
609
1042
|
url: sourceTab.url,
|
|
610
1043
|
active: false
|
|
611
1044
|
});
|
|
@@ -617,7 +1050,7 @@
|
|
|
617
1050
|
if (nextActiveTabId !== null) {
|
|
618
1051
|
await this.browser.updateTab(nextActiveTabId, { active: true });
|
|
619
1052
|
}
|
|
620
|
-
state.windowId =
|
|
1053
|
+
state.windowId = window2.id;
|
|
621
1054
|
state.groupId = null;
|
|
622
1055
|
state.tabIds = recreatedTabs.map((tab) => tab.id);
|
|
623
1056
|
state.primaryTabId = nextPrimaryTabId;
|
|
@@ -626,7 +1059,7 @@
|
|
|
626
1059
|
await this.browser.closeTab(workspaceTab.id);
|
|
627
1060
|
}
|
|
628
1061
|
return {
|
|
629
|
-
window,
|
|
1062
|
+
window: window2,
|
|
630
1063
|
tabs: await this.readTrackedTabs(state.tabIds, state.windowId)
|
|
631
1064
|
};
|
|
632
1065
|
}
|
|
@@ -756,9 +1189,9 @@
|
|
|
756
1189
|
async waitForWindow(windowId, timeoutMs = 750) {
|
|
757
1190
|
const deadline = Date.now() + timeoutMs;
|
|
758
1191
|
while (Date.now() < deadline) {
|
|
759
|
-
const
|
|
760
|
-
if (
|
|
761
|
-
return
|
|
1192
|
+
const window2 = await this.browser.getWindow(windowId);
|
|
1193
|
+
if (window2) {
|
|
1194
|
+
return window2;
|
|
762
1195
|
}
|
|
763
1196
|
await this.delay(50);
|
|
764
1197
|
}
|
|
@@ -801,6 +1234,20 @@
|
|
|
801
1234
|
var STORAGE_KEY_PORT = "cliPort";
|
|
802
1235
|
var STORAGE_KEY_DEBUG_RICH_TEXT = "debugRichText";
|
|
803
1236
|
var DEFAULT_TAB_LOAD_TIMEOUT_MS = 4e4;
|
|
1237
|
+
var textEncoder2 = new TextEncoder();
|
|
1238
|
+
var textDecoder2 = new TextDecoder();
|
|
1239
|
+
var REPLAY_FORBIDDEN_HEADER_NAMES = /* @__PURE__ */ new Set([
|
|
1240
|
+
"accept-encoding",
|
|
1241
|
+
"authorization",
|
|
1242
|
+
"connection",
|
|
1243
|
+
"content-length",
|
|
1244
|
+
"cookie",
|
|
1245
|
+
"host",
|
|
1246
|
+
"origin",
|
|
1247
|
+
"proxy-authorization",
|
|
1248
|
+
"referer",
|
|
1249
|
+
"set-cookie"
|
|
1250
|
+
]);
|
|
804
1251
|
var ws = null;
|
|
805
1252
|
var reconnectTimer = null;
|
|
806
1253
|
var nextReconnectInMs = null;
|
|
@@ -965,17 +1412,17 @@
|
|
|
965
1412
|
},
|
|
966
1413
|
async getWindow(windowId) {
|
|
967
1414
|
try {
|
|
968
|
-
const
|
|
1415
|
+
const window2 = await chrome.windows.get(windowId);
|
|
969
1416
|
return {
|
|
970
|
-
id:
|
|
971
|
-
focused: Boolean(
|
|
1417
|
+
id: window2.id,
|
|
1418
|
+
focused: Boolean(window2.focused)
|
|
972
1419
|
};
|
|
973
1420
|
} catch {
|
|
974
1421
|
return null;
|
|
975
1422
|
}
|
|
976
1423
|
},
|
|
977
1424
|
async createWindow(options) {
|
|
978
|
-
const previouslyFocusedWindow = options.focused === true ? null : (await chrome.windows.getAll()).find((
|
|
1425
|
+
const previouslyFocusedWindow = options.focused === true ? null : (await chrome.windows.getAll()).find((window2) => window2.focused === true && typeof window2.id === "number") ?? null;
|
|
979
1426
|
const previouslyFocusedTab = previouslyFocusedWindow?.id !== void 0 ? (await chrome.tabs.query({ windowId: previouslyFocusedWindow.id, active: true })).find((tab) => typeof tab.id === "number") ?? null : null;
|
|
980
1427
|
const created = await chrome.windows.create({
|
|
981
1428
|
url: options.url ?? "about:blank",
|
|
@@ -1290,6 +1737,369 @@
|
|
|
1290
1737
|
}
|
|
1291
1738
|
return response.result;
|
|
1292
1739
|
}
|
|
1740
|
+
async function ensureTabNetworkCapture(tabId) {
|
|
1741
|
+
try {
|
|
1742
|
+
await ensureNetworkDebugger(tabId);
|
|
1743
|
+
} catch (error) {
|
|
1744
|
+
throw toError("E_DEBUGGER_NOT_ATTACHED", "Debugger-backed network capture unavailable", {
|
|
1745
|
+
detail: error instanceof Error ? error.message : String(error)
|
|
1746
|
+
});
|
|
1747
|
+
}
|
|
1748
|
+
}
|
|
1749
|
+
function normalizePageExecutionScope(value) {
|
|
1750
|
+
return value === "main" || value === "all-frames" ? value : "current";
|
|
1751
|
+
}
|
|
1752
|
+
async function currentContextFramePath(tabId) {
|
|
1753
|
+
try {
|
|
1754
|
+
const context = await forwardContentRpc(tabId, "context.get", { tabId });
|
|
1755
|
+
return Array.isArray(context.framePath) ? context.framePath.map(String) : [];
|
|
1756
|
+
} catch {
|
|
1757
|
+
return [];
|
|
1758
|
+
}
|
|
1759
|
+
}
|
|
1760
|
+
async function executePageWorld(tabId, action, params) {
|
|
1761
|
+
const scope = normalizePageExecutionScope(params.scope);
|
|
1762
|
+
const framePath = scope === "current" ? await currentContextFramePath(tabId) : [];
|
|
1763
|
+
const target = scope === "all-frames" ? { tabId, allFrames: true } : {
|
|
1764
|
+
tabId,
|
|
1765
|
+
frameIds: [0]
|
|
1766
|
+
};
|
|
1767
|
+
const results = await chrome.scripting.executeScript({
|
|
1768
|
+
target,
|
|
1769
|
+
world: "MAIN",
|
|
1770
|
+
args: [
|
|
1771
|
+
{
|
|
1772
|
+
action,
|
|
1773
|
+
scope,
|
|
1774
|
+
framePath,
|
|
1775
|
+
expr: typeof params.expr === "string" ? params.expr : "",
|
|
1776
|
+
path: typeof params.path === "string" ? params.path : "",
|
|
1777
|
+
url: typeof params.url === "string" ? params.url : "",
|
|
1778
|
+
method: typeof params.method === "string" ? params.method : "GET",
|
|
1779
|
+
headers: typeof params.headers === "object" && params.headers !== null ? params.headers : void 0,
|
|
1780
|
+
body: typeof params.body === "string" ? params.body : void 0,
|
|
1781
|
+
contentType: typeof params.contentType === "string" ? params.contentType : void 0,
|
|
1782
|
+
mode: params.mode === "json" ? "json" : "raw",
|
|
1783
|
+
maxBytes: typeof params.maxBytes === "number" ? params.maxBytes : void 0,
|
|
1784
|
+
timeoutMs: typeof params.timeoutMs === "number" ? params.timeoutMs : void 0
|
|
1785
|
+
}
|
|
1786
|
+
],
|
|
1787
|
+
func: async (payload) => {
|
|
1788
|
+
const serializeValue = (value, maxBytes) => {
|
|
1789
|
+
let cloned;
|
|
1790
|
+
try {
|
|
1791
|
+
cloned = typeof structuredClone === "function" ? structuredClone(value) : JSON.parse(JSON.stringify(value));
|
|
1792
|
+
} catch (error) {
|
|
1793
|
+
throw {
|
|
1794
|
+
code: "E_NOT_SERIALIZABLE",
|
|
1795
|
+
message: error instanceof Error ? error.message : String(error)
|
|
1796
|
+
};
|
|
1797
|
+
}
|
|
1798
|
+
const json = JSON.stringify(cloned);
|
|
1799
|
+
const bytes = typeof json === "string" ? json.length : 0;
|
|
1800
|
+
if (typeof maxBytes === "number" && maxBytes > 0 && bytes > maxBytes) {
|
|
1801
|
+
throw {
|
|
1802
|
+
code: "E_BODY_TOO_LARGE",
|
|
1803
|
+
message: "serialized value exceeds max-bytes",
|
|
1804
|
+
details: { bytes, maxBytes }
|
|
1805
|
+
};
|
|
1806
|
+
}
|
|
1807
|
+
return { value: cloned, bytes };
|
|
1808
|
+
};
|
|
1809
|
+
const parsePath = (path) => {
|
|
1810
|
+
if (typeof path !== "string" || !path.trim()) {
|
|
1811
|
+
throw { code: "E_INVALID_PARAMS", message: "path is required" };
|
|
1812
|
+
}
|
|
1813
|
+
const normalized = path.replace(/^globalThis\.?/, "").replace(/^window\.?/, "").trim();
|
|
1814
|
+
if (!normalized) {
|
|
1815
|
+
return [];
|
|
1816
|
+
}
|
|
1817
|
+
const segments = [];
|
|
1818
|
+
let index = 0;
|
|
1819
|
+
while (index < normalized.length) {
|
|
1820
|
+
if (normalized[index] === ".") {
|
|
1821
|
+
index += 1;
|
|
1822
|
+
continue;
|
|
1823
|
+
}
|
|
1824
|
+
if (normalized[index] === "[") {
|
|
1825
|
+
const bracket = normalized.slice(index).match(/^\[(\d+)\]/);
|
|
1826
|
+
if (!bracket) {
|
|
1827
|
+
throw { code: "E_INVALID_PARAMS", message: "Only numeric bracket paths are supported" };
|
|
1828
|
+
}
|
|
1829
|
+
segments.push(Number(bracket[1]));
|
|
1830
|
+
index += bracket[0].length;
|
|
1831
|
+
continue;
|
|
1832
|
+
}
|
|
1833
|
+
const identifier = normalized.slice(index).match(/^[A-Za-z_$][\w$]*/);
|
|
1834
|
+
if (!identifier) {
|
|
1835
|
+
throw { code: "E_INVALID_PARAMS", message: `Unsupported path token near: ${normalized.slice(index, index + 16)}` };
|
|
1836
|
+
}
|
|
1837
|
+
segments.push(identifier[0]);
|
|
1838
|
+
index += identifier[0].length;
|
|
1839
|
+
}
|
|
1840
|
+
return segments;
|
|
1841
|
+
};
|
|
1842
|
+
const resolveFrameWindow = (frameSelectors) => {
|
|
1843
|
+
let currentWindow = window;
|
|
1844
|
+
let currentDocument = document;
|
|
1845
|
+
for (const selector of frameSelectors) {
|
|
1846
|
+
const frame = currentDocument.querySelector(selector);
|
|
1847
|
+
if (!frame || !("contentWindow" in frame)) {
|
|
1848
|
+
throw { code: "E_NOT_FOUND", message: `frame not found: ${selector}` };
|
|
1849
|
+
}
|
|
1850
|
+
const nextWindow = frame.contentWindow;
|
|
1851
|
+
if (!nextWindow) {
|
|
1852
|
+
throw { code: "E_NOT_READY", message: `frame window unavailable: ${selector}` };
|
|
1853
|
+
}
|
|
1854
|
+
currentWindow = nextWindow;
|
|
1855
|
+
currentDocument = nextWindow.document;
|
|
1856
|
+
}
|
|
1857
|
+
return currentWindow;
|
|
1858
|
+
};
|
|
1859
|
+
try {
|
|
1860
|
+
const targetWindow = payload.scope === "main" ? window : payload.scope === "current" ? resolveFrameWindow(payload.framePath ?? []) : window;
|
|
1861
|
+
if (payload.action === "eval") {
|
|
1862
|
+
const evaluator = targetWindow.eval;
|
|
1863
|
+
const serialized = serializeValue(evaluator(payload.expr), payload.maxBytes);
|
|
1864
|
+
return { url: targetWindow.location.href, framePath: payload.scope === "current" ? payload.framePath ?? [] : [], value: serialized.value, bytes: serialized.bytes };
|
|
1865
|
+
}
|
|
1866
|
+
if (payload.action === "extract") {
|
|
1867
|
+
const segments = parsePath(payload.path);
|
|
1868
|
+
let current = targetWindow;
|
|
1869
|
+
for (const segment of segments) {
|
|
1870
|
+
if (current === null || current === void 0 || !(segment in current)) {
|
|
1871
|
+
throw { code: "E_NOT_FOUND", message: `path not found: ${payload.path}` };
|
|
1872
|
+
}
|
|
1873
|
+
current = current[segment];
|
|
1874
|
+
}
|
|
1875
|
+
const serialized = serializeValue(current, payload.maxBytes);
|
|
1876
|
+
return { url: targetWindow.location.href, framePath: payload.scope === "current" ? payload.framePath ?? [] : [], value: serialized.value, bytes: serialized.bytes };
|
|
1877
|
+
}
|
|
1878
|
+
if (payload.action === "fetch") {
|
|
1879
|
+
const headers = { ...payload.headers ?? {} };
|
|
1880
|
+
if (payload.contentType && !headers["Content-Type"]) {
|
|
1881
|
+
headers["Content-Type"] = payload.contentType;
|
|
1882
|
+
}
|
|
1883
|
+
const controller = typeof AbortController === "function" ? new AbortController() : null;
|
|
1884
|
+
const timeoutId = controller && typeof payload.timeoutMs === "number" && payload.timeoutMs > 0 ? window.setTimeout(() => controller.abort(), payload.timeoutMs) : null;
|
|
1885
|
+
let response;
|
|
1886
|
+
try {
|
|
1887
|
+
response = await targetWindow.fetch(payload.url, {
|
|
1888
|
+
method: payload.method || "GET",
|
|
1889
|
+
headers,
|
|
1890
|
+
body: typeof payload.body === "string" ? payload.body : void 0,
|
|
1891
|
+
signal: controller ? controller.signal : void 0
|
|
1892
|
+
});
|
|
1893
|
+
} finally {
|
|
1894
|
+
if (timeoutId !== null) {
|
|
1895
|
+
window.clearTimeout(timeoutId);
|
|
1896
|
+
}
|
|
1897
|
+
}
|
|
1898
|
+
const bodyText = await response.text();
|
|
1899
|
+
const headerMap = {};
|
|
1900
|
+
response.headers.forEach((value, key) => {
|
|
1901
|
+
headerMap[key] = value;
|
|
1902
|
+
});
|
|
1903
|
+
return {
|
|
1904
|
+
url: targetWindow.location.href,
|
|
1905
|
+
framePath: payload.scope === "current" ? payload.framePath ?? [] : [],
|
|
1906
|
+
value: (() => {
|
|
1907
|
+
const encoder = typeof TextEncoder === "function" ? new TextEncoder() : null;
|
|
1908
|
+
const decoder = typeof TextDecoder === "function" ? new TextDecoder() : null;
|
|
1909
|
+
const previewLimit = typeof payload.maxBytes === "number" && payload.maxBytes > 0 ? payload.maxBytes : 8192;
|
|
1910
|
+
const encodedBody = encoder ? encoder.encode(bodyText) : null;
|
|
1911
|
+
const bodyBytes = encodedBody ? encodedBody.byteLength : bodyText.length;
|
|
1912
|
+
const truncated = bodyBytes > previewLimit;
|
|
1913
|
+
if (payload.mode === "json" && truncated) {
|
|
1914
|
+
throw {
|
|
1915
|
+
code: "E_BODY_TOO_LARGE",
|
|
1916
|
+
message: "JSON response exceeds max-bytes",
|
|
1917
|
+
details: {
|
|
1918
|
+
bytes: bodyBytes,
|
|
1919
|
+
maxBytes: previewLimit
|
|
1920
|
+
}
|
|
1921
|
+
};
|
|
1922
|
+
}
|
|
1923
|
+
const previewText = encodedBody && decoder ? decoder.decode(encodedBody.subarray(0, Math.min(encodedBody.byteLength, previewLimit))) : truncated ? bodyText.slice(0, previewLimit) : bodyText;
|
|
1924
|
+
return {
|
|
1925
|
+
url: response.url,
|
|
1926
|
+
status: response.status,
|
|
1927
|
+
ok: response.ok,
|
|
1928
|
+
headers: headerMap,
|
|
1929
|
+
contentType: response.headers.get("content-type") ?? void 0,
|
|
1930
|
+
bodyText: payload.mode === "json" ? void 0 : previewText,
|
|
1931
|
+
json: payload.mode === "json" && bodyText ? JSON.parse(bodyText) : void 0,
|
|
1932
|
+
bytes: bodyBytes,
|
|
1933
|
+
truncated
|
|
1934
|
+
};
|
|
1935
|
+
})()
|
|
1936
|
+
};
|
|
1937
|
+
}
|
|
1938
|
+
throw { code: "E_NOT_FOUND", message: `Unsupported page world action: ${payload.action}` };
|
|
1939
|
+
} catch (error) {
|
|
1940
|
+
return {
|
|
1941
|
+
url: window.location.href,
|
|
1942
|
+
framePath: payload.scope === "current" ? payload.framePath ?? [] : [],
|
|
1943
|
+
error: typeof error === "object" && error !== null && "code" in error ? error : { code: "E_EXECUTION", message: error instanceof Error ? error.message : String(error) }
|
|
1944
|
+
};
|
|
1945
|
+
}
|
|
1946
|
+
}
|
|
1947
|
+
});
|
|
1948
|
+
if (scope === "all-frames") {
|
|
1949
|
+
return {
|
|
1950
|
+
scope,
|
|
1951
|
+
results: results.map((item) => item.result ?? { url: "", framePath: [] })
|
|
1952
|
+
};
|
|
1953
|
+
}
|
|
1954
|
+
return {
|
|
1955
|
+
scope,
|
|
1956
|
+
result: results[0]?.result ?? { url: "", framePath }
|
|
1957
|
+
};
|
|
1958
|
+
}
|
|
1959
|
+
function truncateNetworkEntry(entry, bodyBytes) {
|
|
1960
|
+
if (typeof bodyBytes !== "number" || !Number.isFinite(bodyBytes) || bodyBytes <= 0) {
|
|
1961
|
+
return entry;
|
|
1962
|
+
}
|
|
1963
|
+
const maxBytes = Math.max(1, Math.floor(bodyBytes));
|
|
1964
|
+
const clone = { ...entry };
|
|
1965
|
+
if (typeof clone.requestBodyPreview === "string") {
|
|
1966
|
+
const requestBytes = textEncoder2.encode(clone.requestBodyPreview);
|
|
1967
|
+
if (requestBytes.byteLength > maxBytes) {
|
|
1968
|
+
clone.requestBodyPreview = textDecoder2.decode(requestBytes.subarray(0, maxBytes));
|
|
1969
|
+
clone.requestBodyTruncated = true;
|
|
1970
|
+
clone.truncated = true;
|
|
1971
|
+
}
|
|
1972
|
+
}
|
|
1973
|
+
if (typeof clone.responseBodyPreview === "string") {
|
|
1974
|
+
const responseBytes = textEncoder2.encode(clone.responseBodyPreview);
|
|
1975
|
+
if (responseBytes.byteLength > maxBytes) {
|
|
1976
|
+
clone.responseBodyPreview = textDecoder2.decode(responseBytes.subarray(0, maxBytes));
|
|
1977
|
+
clone.responseBodyTruncated = true;
|
|
1978
|
+
clone.truncated = true;
|
|
1979
|
+
}
|
|
1980
|
+
}
|
|
1981
|
+
return clone;
|
|
1982
|
+
}
|
|
1983
|
+
function filterNetworkEntrySections(entry, include) {
|
|
1984
|
+
if (!Array.isArray(include)) {
|
|
1985
|
+
return entry;
|
|
1986
|
+
}
|
|
1987
|
+
const sections = new Set(
|
|
1988
|
+
include.map(String).filter((section) => section === "request" || section === "response")
|
|
1989
|
+
);
|
|
1990
|
+
if (sections.size === 0 || sections.size === 2) {
|
|
1991
|
+
return entry;
|
|
1992
|
+
}
|
|
1993
|
+
const clone = { ...entry };
|
|
1994
|
+
if (!sections.has("request")) {
|
|
1995
|
+
delete clone.requestHeaders;
|
|
1996
|
+
delete clone.requestBodyPreview;
|
|
1997
|
+
delete clone.requestBodyTruncated;
|
|
1998
|
+
}
|
|
1999
|
+
if (!sections.has("response")) {
|
|
2000
|
+
delete clone.responseHeaders;
|
|
2001
|
+
delete clone.responseBodyPreview;
|
|
2002
|
+
delete clone.responseBodyTruncated;
|
|
2003
|
+
delete clone.binary;
|
|
2004
|
+
}
|
|
2005
|
+
return clone;
|
|
2006
|
+
}
|
|
2007
|
+
function replayHeadersFromEntry(entry) {
|
|
2008
|
+
if (!entry.requestHeaders) {
|
|
2009
|
+
return void 0;
|
|
2010
|
+
}
|
|
2011
|
+
const headers = {};
|
|
2012
|
+
for (const [name, value] of Object.entries(entry.requestHeaders)) {
|
|
2013
|
+
const normalizedName = name.toLowerCase();
|
|
2014
|
+
if (REPLAY_FORBIDDEN_HEADER_NAMES.has(normalizedName) || normalizedName.startsWith("sec-")) {
|
|
2015
|
+
continue;
|
|
2016
|
+
}
|
|
2017
|
+
if (containsRedactionMarker(value)) {
|
|
2018
|
+
continue;
|
|
2019
|
+
}
|
|
2020
|
+
headers[name] = value;
|
|
2021
|
+
}
|
|
2022
|
+
return Object.keys(headers).length > 0 ? headers : void 0;
|
|
2023
|
+
}
|
|
2024
|
+
function parseTimestampCandidate(value, now = Date.now()) {
|
|
2025
|
+
const normalized = value.trim().toLowerCase();
|
|
2026
|
+
if (!normalized) {
|
|
2027
|
+
return null;
|
|
2028
|
+
}
|
|
2029
|
+
if (normalized === "today") {
|
|
2030
|
+
return now;
|
|
2031
|
+
}
|
|
2032
|
+
if (normalized === "yesterday") {
|
|
2033
|
+
return now - 24 * 60 * 60 * 1e3;
|
|
2034
|
+
}
|
|
2035
|
+
const parsed = Date.parse(value);
|
|
2036
|
+
return Number.isNaN(parsed) ? null : parsed;
|
|
2037
|
+
}
|
|
2038
|
+
function extractLatestTimestamp(values, now = Date.now()) {
|
|
2039
|
+
if (!Array.isArray(values) || values.length === 0) {
|
|
2040
|
+
return null;
|
|
2041
|
+
}
|
|
2042
|
+
let latest = null;
|
|
2043
|
+
for (const value of values) {
|
|
2044
|
+
const parsed = parseTimestampCandidate(value, now);
|
|
2045
|
+
if (parsed === null) {
|
|
2046
|
+
continue;
|
|
2047
|
+
}
|
|
2048
|
+
latest = latest === null ? parsed : Math.max(latest, parsed);
|
|
2049
|
+
}
|
|
2050
|
+
return latest;
|
|
2051
|
+
}
|
|
2052
|
+
function computeFreshnessAssessment(input) {
|
|
2053
|
+
const now = Date.now();
|
|
2054
|
+
const latestDataTimestamp = [input.latestInlineDataTimestamp, input.domVisibleTimestamp].filter((value) => typeof value === "number").sort((left, right) => right - left)[0] ?? null;
|
|
2055
|
+
if (latestDataTimestamp !== null && now - latestDataTimestamp <= input.freshWindowMs) {
|
|
2056
|
+
return "fresh";
|
|
2057
|
+
}
|
|
2058
|
+
const recentSignals = [input.latestNetworkTimestamp, input.lastMutationAt].filter((value) => typeof value === "number").some((value) => now - value <= input.freshWindowMs);
|
|
2059
|
+
if (recentSignals && latestDataTimestamp !== null && now - latestDataTimestamp > input.freshWindowMs) {
|
|
2060
|
+
return "lagged";
|
|
2061
|
+
}
|
|
2062
|
+
const staleSignals = [input.latestNetworkTimestamp, input.lastMutationAt, latestDataTimestamp].filter((value) => typeof value === "number");
|
|
2063
|
+
if (staleSignals.length > 0 && staleSignals.every((value) => now - value > input.staleWindowMs)) {
|
|
2064
|
+
return "stale";
|
|
2065
|
+
}
|
|
2066
|
+
return "unknown";
|
|
2067
|
+
}
|
|
2068
|
+
async function collectPageInspection(tabId, params = {}) {
|
|
2069
|
+
return await forwardContentRpc(tabId, "bak.internal.inspectState", params);
|
|
2070
|
+
}
|
|
2071
|
+
async function buildFreshnessForTab(tabId, params = {}) {
|
|
2072
|
+
const inspection = await collectPageInspection(tabId, params);
|
|
2073
|
+
const visibleTimestamps = Array.isArray(inspection.visibleTimestamps) ? inspection.visibleTimestamps.map(String) : [];
|
|
2074
|
+
const inlineTimestamps = Array.isArray(inspection.inlineTimestamps) ? inspection.inlineTimestamps.map(String) : [];
|
|
2075
|
+
const now = Date.now();
|
|
2076
|
+
const freshWindowMs = typeof params.freshWindowMs === "number" ? Math.max(1, Math.floor(params.freshWindowMs)) : 15 * 60 * 1e3;
|
|
2077
|
+
const staleWindowMs = typeof params.staleWindowMs === "number" ? Math.max(freshWindowMs, Math.floor(params.staleWindowMs)) : 24 * 60 * 60 * 1e3;
|
|
2078
|
+
const latestInlineDataTimestamp = extractLatestTimestamp(inlineTimestamps, now);
|
|
2079
|
+
const domVisibleTimestamp = extractLatestTimestamp(visibleTimestamps, now);
|
|
2080
|
+
const latestNetworkTs = latestNetworkTimestamp(tabId);
|
|
2081
|
+
const lastMutationAt = typeof inspection.lastMutationAt === "number" ? inspection.lastMutationAt : null;
|
|
2082
|
+
return {
|
|
2083
|
+
pageLoadedAt: typeof inspection.pageLoadedAt === "number" ? inspection.pageLoadedAt : null,
|
|
2084
|
+
lastMutationAt,
|
|
2085
|
+
latestNetworkTimestamp: latestNetworkTs,
|
|
2086
|
+
latestInlineDataTimestamp,
|
|
2087
|
+
domVisibleTimestamp,
|
|
2088
|
+
assessment: computeFreshnessAssessment({
|
|
2089
|
+
latestInlineDataTimestamp,
|
|
2090
|
+
latestNetworkTimestamp: latestNetworkTs,
|
|
2091
|
+
domVisibleTimestamp,
|
|
2092
|
+
lastMutationAt,
|
|
2093
|
+
freshWindowMs,
|
|
2094
|
+
staleWindowMs
|
|
2095
|
+
}),
|
|
2096
|
+
evidence: {
|
|
2097
|
+
visibleTimestamps,
|
|
2098
|
+
inlineTimestamps,
|
|
2099
|
+
networkSampleIds: recentNetworkSampleIds(tabId)
|
|
2100
|
+
}
|
|
2101
|
+
};
|
|
2102
|
+
}
|
|
1293
2103
|
async function handleRequest(request) {
|
|
1294
2104
|
const params = request.params ?? {};
|
|
1295
2105
|
const target = {
|
|
@@ -1329,11 +2139,10 @@
|
|
|
1329
2139
|
"context.enterShadow",
|
|
1330
2140
|
"context.exitShadow",
|
|
1331
2141
|
"context.reset",
|
|
1332
|
-
"
|
|
1333
|
-
"
|
|
1334
|
-
"
|
|
1335
|
-
"
|
|
1336
|
-
"debug.dumpState"
|
|
2142
|
+
"table.list",
|
|
2143
|
+
"table.schema",
|
|
2144
|
+
"table.rows",
|
|
2145
|
+
"table.export"
|
|
1337
2146
|
]);
|
|
1338
2147
|
switch (request.method) {
|
|
1339
2148
|
case "session.ping": {
|
|
@@ -1396,11 +2205,15 @@
|
|
|
1396
2205
|
}
|
|
1397
2206
|
case "workspace.ensure": {
|
|
1398
2207
|
return preserveHumanFocus(params.focus !== true, async () => {
|
|
1399
|
-
|
|
2208
|
+
const result = await bindingManager.ensureWorkspace({
|
|
1400
2209
|
workspaceId: String(params.workspaceId ?? ""),
|
|
1401
2210
|
focus: params.focus === true,
|
|
1402
2211
|
initialUrl: typeof params.url === "string" ? params.url : void 0
|
|
1403
2212
|
});
|
|
2213
|
+
for (const tab of result.workspace.tabs) {
|
|
2214
|
+
void ensureNetworkDebugger(tab.id).catch(() => void 0);
|
|
2215
|
+
}
|
|
2216
|
+
return result;
|
|
1404
2217
|
});
|
|
1405
2218
|
}
|
|
1406
2219
|
case "workspace.info": {
|
|
@@ -1418,7 +2231,9 @@
|
|
|
1418
2231
|
focus: params.focus === true
|
|
1419
2232
|
});
|
|
1420
2233
|
});
|
|
1421
|
-
|
|
2234
|
+
const finalized = await finalizeOpenedWorkspaceTab(opened, expectedUrl);
|
|
2235
|
+
void ensureNetworkDebugger(finalized.tab.id).catch(() => void 0);
|
|
2236
|
+
return finalized;
|
|
1422
2237
|
}
|
|
1423
2238
|
case "workspace.listTabs": {
|
|
1424
2239
|
return await bindingManager.listTabs(String(params.workspaceId ?? ""));
|
|
@@ -1427,7 +2242,9 @@
|
|
|
1427
2242
|
return await bindingManager.getActiveTab(String(params.workspaceId ?? ""));
|
|
1428
2243
|
}
|
|
1429
2244
|
case "workspace.setActiveTab": {
|
|
1430
|
-
|
|
2245
|
+
const result = await bindingManager.setActiveTab(Number(params.tabId), String(params.workspaceId ?? ""));
|
|
2246
|
+
void ensureNetworkDebugger(result.tab.id).catch(() => void 0);
|
|
2247
|
+
return result;
|
|
1431
2248
|
}
|
|
1432
2249
|
case "workspace.focus": {
|
|
1433
2250
|
return await bindingManager.focus(String(params.workspaceId ?? ""));
|
|
@@ -1449,6 +2266,7 @@
|
|
|
1449
2266
|
const tab = await withTab(target, {
|
|
1450
2267
|
requireSupportedAutomationUrl: false
|
|
1451
2268
|
});
|
|
2269
|
+
void ensureNetworkDebugger(tab.id).catch(() => void 0);
|
|
1452
2270
|
const url = String(params.url ?? "about:blank");
|
|
1453
2271
|
await chrome.tabs.update(tab.id, { url });
|
|
1454
2272
|
await waitForTabUrl(tab.id, url);
|
|
@@ -1460,6 +2278,7 @@
|
|
|
1460
2278
|
case "page.back": {
|
|
1461
2279
|
return await preserveHumanFocus(typeof target.tabId !== "number", async () => {
|
|
1462
2280
|
const tab = await withTab(target);
|
|
2281
|
+
void ensureNetworkDebugger(tab.id).catch(() => void 0);
|
|
1463
2282
|
await chrome.tabs.goBack(tab.id);
|
|
1464
2283
|
await waitForTabComplete(tab.id);
|
|
1465
2284
|
return { ok: true };
|
|
@@ -1468,6 +2287,7 @@
|
|
|
1468
2287
|
case "page.forward": {
|
|
1469
2288
|
return await preserveHumanFocus(typeof target.tabId !== "number", async () => {
|
|
1470
2289
|
const tab = await withTab(target);
|
|
2290
|
+
void ensureNetworkDebugger(tab.id).catch(() => void 0);
|
|
1471
2291
|
await chrome.tabs.goForward(tab.id);
|
|
1472
2292
|
await waitForTabComplete(tab.id);
|
|
1473
2293
|
return { ok: true };
|
|
@@ -1476,6 +2296,7 @@
|
|
|
1476
2296
|
case "page.reload": {
|
|
1477
2297
|
return await preserveHumanFocus(typeof target.tabId !== "number", async () => {
|
|
1478
2298
|
const tab = await withTab(target);
|
|
2299
|
+
void ensureNetworkDebugger(tab.id).catch(() => void 0);
|
|
1479
2300
|
await chrome.tabs.reload(tab.id);
|
|
1480
2301
|
await waitForTabComplete(tab.id);
|
|
1481
2302
|
return { ok: true };
|
|
@@ -1507,6 +2328,24 @@
|
|
|
1507
2328
|
};
|
|
1508
2329
|
});
|
|
1509
2330
|
}
|
|
2331
|
+
case "page.eval": {
|
|
2332
|
+
return await preserveHumanFocus(typeof target.tabId !== "number", async () => {
|
|
2333
|
+
const tab = await withTab(target);
|
|
2334
|
+
return await executePageWorld(tab.id, "eval", params);
|
|
2335
|
+
});
|
|
2336
|
+
}
|
|
2337
|
+
case "page.extract": {
|
|
2338
|
+
return await preserveHumanFocus(typeof target.tabId !== "number", async () => {
|
|
2339
|
+
const tab = await withTab(target);
|
|
2340
|
+
return await executePageWorld(tab.id, "extract", params);
|
|
2341
|
+
});
|
|
2342
|
+
}
|
|
2343
|
+
case "page.fetch": {
|
|
2344
|
+
return await preserveHumanFocus(typeof target.tabId !== "number", async () => {
|
|
2345
|
+
const tab = await withTab(target);
|
|
2346
|
+
return await executePageWorld(tab.id, "fetch", params);
|
|
2347
|
+
});
|
|
2348
|
+
}
|
|
1510
2349
|
case "page.snapshot": {
|
|
1511
2350
|
return await preserveHumanFocus(typeof target.tabId !== "number", async () => {
|
|
1512
2351
|
const tab = await withTab(target);
|
|
@@ -1601,6 +2440,273 @@
|
|
|
1601
2440
|
return { entries: response.entries };
|
|
1602
2441
|
});
|
|
1603
2442
|
}
|
|
2443
|
+
case "network.list": {
|
|
2444
|
+
return await preserveHumanFocus(typeof target.tabId !== "number", async () => {
|
|
2445
|
+
const tab = await withTab(target);
|
|
2446
|
+
try {
|
|
2447
|
+
await ensureTabNetworkCapture(tab.id);
|
|
2448
|
+
return {
|
|
2449
|
+
entries: listNetworkEntries(tab.id, {
|
|
2450
|
+
limit: typeof params.limit === "number" ? params.limit : void 0,
|
|
2451
|
+
urlIncludes: typeof params.urlIncludes === "string" ? params.urlIncludes : void 0,
|
|
2452
|
+
status: typeof params.status === "number" ? params.status : void 0,
|
|
2453
|
+
method: typeof params.method === "string" ? params.method : void 0
|
|
2454
|
+
})
|
|
2455
|
+
};
|
|
2456
|
+
} catch {
|
|
2457
|
+
return await forwardContentRpc(tab.id, "network.list", params);
|
|
2458
|
+
}
|
|
2459
|
+
});
|
|
2460
|
+
}
|
|
2461
|
+
case "network.get": {
|
|
2462
|
+
return await preserveHumanFocus(typeof target.tabId !== "number", async () => {
|
|
2463
|
+
const tab = await withTab(target);
|
|
2464
|
+
try {
|
|
2465
|
+
await ensureTabNetworkCapture(tab.id);
|
|
2466
|
+
const entry = getNetworkEntry(tab.id, String(params.id ?? ""));
|
|
2467
|
+
if (!entry) {
|
|
2468
|
+
throw toError("E_NOT_FOUND", `network entry not found: ${String(params.id ?? "")}`);
|
|
2469
|
+
}
|
|
2470
|
+
const filtered = filterNetworkEntrySections(
|
|
2471
|
+
truncateNetworkEntry(entry, typeof params.bodyBytes === "number" ? params.bodyBytes : void 0),
|
|
2472
|
+
params.include
|
|
2473
|
+
);
|
|
2474
|
+
return { entry: filtered };
|
|
2475
|
+
} catch (error) {
|
|
2476
|
+
if (error?.code === "E_NOT_FOUND") {
|
|
2477
|
+
throw error;
|
|
2478
|
+
}
|
|
2479
|
+
return await forwardContentRpc(tab.id, "network.get", params);
|
|
2480
|
+
}
|
|
2481
|
+
});
|
|
2482
|
+
}
|
|
2483
|
+
case "network.search": {
|
|
2484
|
+
return await preserveHumanFocus(typeof target.tabId !== "number", async () => {
|
|
2485
|
+
const tab = await withTab(target);
|
|
2486
|
+
await ensureTabNetworkCapture(tab.id);
|
|
2487
|
+
return {
|
|
2488
|
+
entries: searchNetworkEntries(
|
|
2489
|
+
tab.id,
|
|
2490
|
+
String(params.pattern ?? ""),
|
|
2491
|
+
typeof params.limit === "number" ? params.limit : 50
|
|
2492
|
+
)
|
|
2493
|
+
};
|
|
2494
|
+
});
|
|
2495
|
+
}
|
|
2496
|
+
case "network.waitFor": {
|
|
2497
|
+
return await preserveHumanFocus(typeof target.tabId !== "number", async () => {
|
|
2498
|
+
const tab = await withTab(target);
|
|
2499
|
+
try {
|
|
2500
|
+
await ensureTabNetworkCapture(tab.id);
|
|
2501
|
+
} catch {
|
|
2502
|
+
return await forwardContentRpc(tab.id, "network.waitFor", params);
|
|
2503
|
+
}
|
|
2504
|
+
return {
|
|
2505
|
+
entry: await waitForNetworkEntry(tab.id, {
|
|
2506
|
+
limit: 1,
|
|
2507
|
+
urlIncludes: typeof params.urlIncludes === "string" ? params.urlIncludes : void 0,
|
|
2508
|
+
status: typeof params.status === "number" ? params.status : void 0,
|
|
2509
|
+
method: typeof params.method === "string" ? params.method : void 0,
|
|
2510
|
+
timeoutMs: typeof params.timeoutMs === "number" ? params.timeoutMs : void 0
|
|
2511
|
+
})
|
|
2512
|
+
};
|
|
2513
|
+
});
|
|
2514
|
+
}
|
|
2515
|
+
case "network.clear": {
|
|
2516
|
+
return await preserveHumanFocus(typeof target.tabId !== "number", async () => {
|
|
2517
|
+
const tab = await withTab(target);
|
|
2518
|
+
clearNetworkEntries(tab.id);
|
|
2519
|
+
await forwardContentRpc(tab.id, "network.clear", params).catch(() => void 0);
|
|
2520
|
+
return { ok: true };
|
|
2521
|
+
});
|
|
2522
|
+
}
|
|
2523
|
+
case "network.replay": {
|
|
2524
|
+
return await preserveHumanFocus(typeof target.tabId !== "number", async () => {
|
|
2525
|
+
const tab = await withTab(target);
|
|
2526
|
+
await ensureTabNetworkCapture(tab.id);
|
|
2527
|
+
const entry = getNetworkEntry(tab.id, String(params.id ?? ""));
|
|
2528
|
+
if (!entry) {
|
|
2529
|
+
throw toError("E_NOT_FOUND", `network entry not found: ${String(params.id ?? "")}`);
|
|
2530
|
+
}
|
|
2531
|
+
if (entry.requestBodyTruncated === true) {
|
|
2532
|
+
throw toError("E_BODY_TOO_LARGE", "captured request body was truncated and cannot be replayed safely", {
|
|
2533
|
+
requestId: entry.id,
|
|
2534
|
+
requestBytes: entry.requestBytes
|
|
2535
|
+
});
|
|
2536
|
+
}
|
|
2537
|
+
if (containsRedactionMarker(entry.requestBodyPreview)) {
|
|
2538
|
+
throw toError("E_EXECUTION", "captured request body was redacted and cannot be replayed safely", {
|
|
2539
|
+
requestId: entry.id
|
|
2540
|
+
});
|
|
2541
|
+
}
|
|
2542
|
+
const replayed = await executePageWorld(tab.id, "fetch", {
|
|
2543
|
+
url: entry.url,
|
|
2544
|
+
method: entry.method,
|
|
2545
|
+
headers: replayHeadersFromEntry(entry),
|
|
2546
|
+
body: entry.requestBodyPreview,
|
|
2547
|
+
contentType: (() => {
|
|
2548
|
+
const requestHeaders = entry.requestHeaders ?? {};
|
|
2549
|
+
const contentTypeHeader = Object.keys(requestHeaders).find((name) => name.toLowerCase() === "content-type");
|
|
2550
|
+
return contentTypeHeader ? requestHeaders[contentTypeHeader] : void 0;
|
|
2551
|
+
})(),
|
|
2552
|
+
mode: params.mode,
|
|
2553
|
+
timeoutMs: params.timeoutMs,
|
|
2554
|
+
maxBytes: params.maxBytes,
|
|
2555
|
+
scope: "current"
|
|
2556
|
+
});
|
|
2557
|
+
const frameResult = replayed.result ?? replayed.results?.find((candidate) => candidate.value || candidate.error);
|
|
2558
|
+
if (frameResult?.error) {
|
|
2559
|
+
throw toError(frameResult.error.code ?? "E_EXECUTION", frameResult.error.message, frameResult.error.details);
|
|
2560
|
+
}
|
|
2561
|
+
const first = frameResult?.value;
|
|
2562
|
+
if (!first) {
|
|
2563
|
+
throw toError("E_EXECUTION", "network replay returned no response payload");
|
|
2564
|
+
}
|
|
2565
|
+
return first;
|
|
2566
|
+
});
|
|
2567
|
+
}
|
|
2568
|
+
case "page.freshness": {
|
|
2569
|
+
return await preserveHumanFocus(typeof target.tabId !== "number", async () => {
|
|
2570
|
+
const tab = await withTab(target);
|
|
2571
|
+
await ensureNetworkDebugger(tab.id).catch(() => void 0);
|
|
2572
|
+
return await buildFreshnessForTab(tab.id, params);
|
|
2573
|
+
});
|
|
2574
|
+
}
|
|
2575
|
+
case "debug.dumpState": {
|
|
2576
|
+
return await preserveHumanFocus(typeof target.tabId !== "number", async () => {
|
|
2577
|
+
const tab = await withTab(target);
|
|
2578
|
+
await ensureNetworkDebugger(tab.id).catch(() => void 0);
|
|
2579
|
+
const dump = await forwardContentRpc(tab.id, "debug.dumpState", params);
|
|
2580
|
+
const inspection = await collectPageInspection(tab.id, params);
|
|
2581
|
+
const network = listNetworkEntries(tab.id, {
|
|
2582
|
+
limit: typeof params.networkLimit === "number" ? params.networkLimit : 80
|
|
2583
|
+
});
|
|
2584
|
+
const sections = Array.isArray(params.section) ? new Set(params.section.map(String)) : null;
|
|
2585
|
+
const result = {
|
|
2586
|
+
...dump,
|
|
2587
|
+
network,
|
|
2588
|
+
scripts: inspection.scripts,
|
|
2589
|
+
globalsPreview: inspection.globalsPreview,
|
|
2590
|
+
storage: inspection.storage,
|
|
2591
|
+
frames: inspection.frames,
|
|
2592
|
+
networkSummary: {
|
|
2593
|
+
total: network.length,
|
|
2594
|
+
recent: network.slice(0, Math.min(10, network.length))
|
|
2595
|
+
}
|
|
2596
|
+
};
|
|
2597
|
+
if (!sections || sections.size === 0) {
|
|
2598
|
+
return result;
|
|
2599
|
+
}
|
|
2600
|
+
const filtered = {
|
|
2601
|
+
url: result.url,
|
|
2602
|
+
title: result.title,
|
|
2603
|
+
context: result.context
|
|
2604
|
+
};
|
|
2605
|
+
if (sections.has("dom")) {
|
|
2606
|
+
filtered.dom = result.dom;
|
|
2607
|
+
}
|
|
2608
|
+
if (sections.has("visible-text")) {
|
|
2609
|
+
filtered.text = result.text;
|
|
2610
|
+
filtered.elements = result.elements;
|
|
2611
|
+
}
|
|
2612
|
+
if (sections.has("scripts")) {
|
|
2613
|
+
filtered.scripts = result.scripts;
|
|
2614
|
+
}
|
|
2615
|
+
if (sections.has("globals-preview")) {
|
|
2616
|
+
filtered.globalsPreview = result.globalsPreview;
|
|
2617
|
+
}
|
|
2618
|
+
if (sections.has("network-summary")) {
|
|
2619
|
+
filtered.networkSummary = result.networkSummary;
|
|
2620
|
+
}
|
|
2621
|
+
if (sections.has("storage")) {
|
|
2622
|
+
filtered.storage = result.storage;
|
|
2623
|
+
}
|
|
2624
|
+
if (sections.has("frames")) {
|
|
2625
|
+
filtered.frames = result.frames;
|
|
2626
|
+
}
|
|
2627
|
+
if (params.includeAccessibility === true && "accessibility" in result) {
|
|
2628
|
+
filtered.accessibility = result.accessibility;
|
|
2629
|
+
}
|
|
2630
|
+
if ("snapshot" in result) {
|
|
2631
|
+
filtered.snapshot = result.snapshot;
|
|
2632
|
+
}
|
|
2633
|
+
return filtered;
|
|
2634
|
+
});
|
|
2635
|
+
}
|
|
2636
|
+
case "inspect.pageData": {
|
|
2637
|
+
return await preserveHumanFocus(typeof target.tabId !== "number", async () => {
|
|
2638
|
+
const tab = await withTab(target);
|
|
2639
|
+
await ensureNetworkDebugger(tab.id).catch(() => void 0);
|
|
2640
|
+
const inspection = await collectPageInspection(tab.id, params);
|
|
2641
|
+
const network = listNetworkEntries(tab.id, { limit: 10 });
|
|
2642
|
+
return {
|
|
2643
|
+
suspiciousGlobals: inspection.suspiciousGlobals ?? [],
|
|
2644
|
+
tables: inspection.tables ?? [],
|
|
2645
|
+
visibleTimestamps: inspection.visibleTimestamps ?? [],
|
|
2646
|
+
inlineTimestamps: inspection.inlineTimestamps ?? [],
|
|
2647
|
+
recentNetwork: network,
|
|
2648
|
+
recommendedNextSteps: [
|
|
2649
|
+
"bak page extract --path table_data",
|
|
2650
|
+
"bak network search --pattern table_data",
|
|
2651
|
+
"bak page freshness"
|
|
2652
|
+
]
|
|
2653
|
+
};
|
|
2654
|
+
});
|
|
2655
|
+
}
|
|
2656
|
+
case "inspect.liveUpdates": {
|
|
2657
|
+
return await preserveHumanFocus(typeof target.tabId !== "number", async () => {
|
|
2658
|
+
const tab = await withTab(target);
|
|
2659
|
+
await ensureNetworkDebugger(tab.id).catch(() => void 0);
|
|
2660
|
+
const inspection = await collectPageInspection(tab.id, params);
|
|
2661
|
+
const network = listNetworkEntries(tab.id, { limit: 25 });
|
|
2662
|
+
return {
|
|
2663
|
+
lastMutationAt: inspection.lastMutationAt ?? null,
|
|
2664
|
+
timers: inspection.timers ?? { timeouts: 0, intervals: 0 },
|
|
2665
|
+
networkCount: network.length,
|
|
2666
|
+
recentNetwork: network.slice(0, 10)
|
|
2667
|
+
};
|
|
2668
|
+
});
|
|
2669
|
+
}
|
|
2670
|
+
case "inspect.freshness": {
|
|
2671
|
+
return await preserveHumanFocus(typeof target.tabId !== "number", async () => {
|
|
2672
|
+
const tab = await withTab(target);
|
|
2673
|
+
const freshness = await buildFreshnessForTab(tab.id, params);
|
|
2674
|
+
return {
|
|
2675
|
+
...freshness,
|
|
2676
|
+
lagMs: typeof freshness.latestNetworkTimestamp === "number" && typeof freshness.latestInlineDataTimestamp === "number" ? Math.max(0, freshness.latestNetworkTimestamp - freshness.latestInlineDataTimestamp) : null
|
|
2677
|
+
};
|
|
2678
|
+
});
|
|
2679
|
+
}
|
|
2680
|
+
case "capture.snapshot": {
|
|
2681
|
+
return await preserveHumanFocus(typeof target.tabId !== "number", async () => {
|
|
2682
|
+
const tab = await withTab(target);
|
|
2683
|
+
await ensureNetworkDebugger(tab.id).catch(() => void 0);
|
|
2684
|
+
const inspection = await collectPageInspection(tab.id, params);
|
|
2685
|
+
return {
|
|
2686
|
+
url: inspection.url ?? tab.url ?? "",
|
|
2687
|
+
title: inspection.title ?? tab.title ?? "",
|
|
2688
|
+
html: inspection.html ?? "",
|
|
2689
|
+
visibleText: inspection.visibleText ?? [],
|
|
2690
|
+
cookies: inspection.cookies ?? [],
|
|
2691
|
+
storage: inspection.storage ?? { localStorageKeys: [], sessionStorageKeys: [] },
|
|
2692
|
+
context: inspection.context ?? { tabId: tab.id, framePath: [], shadowPath: [] },
|
|
2693
|
+
freshness: await buildFreshnessForTab(tab.id, params),
|
|
2694
|
+
network: listNetworkEntries(tab.id, {
|
|
2695
|
+
limit: typeof params.networkLimit === "number" ? params.networkLimit : 20
|
|
2696
|
+
}),
|
|
2697
|
+
capturedAt: Date.now()
|
|
2698
|
+
};
|
|
2699
|
+
});
|
|
2700
|
+
}
|
|
2701
|
+
case "capture.har": {
|
|
2702
|
+
return await preserveHumanFocus(typeof target.tabId !== "number", async () => {
|
|
2703
|
+
const tab = await withTab(target);
|
|
2704
|
+
await ensureTabNetworkCapture(tab.id);
|
|
2705
|
+
return {
|
|
2706
|
+
har: exportHar(tab.id, typeof params.limit === "number" ? params.limit : void 0)
|
|
2707
|
+
};
|
|
2708
|
+
});
|
|
2709
|
+
}
|
|
1604
2710
|
case "ui.selectCandidate": {
|
|
1605
2711
|
return await preserveHumanFocus(typeof target.tabId !== "number", async () => {
|
|
1606
2712
|
const tab = await withTab(target);
|
|
@@ -1671,7 +2777,7 @@
|
|
|
1671
2777
|
ws?.send(JSON.stringify({
|
|
1672
2778
|
type: "hello",
|
|
1673
2779
|
role: "extension",
|
|
1674
|
-
version: "0.
|
|
2780
|
+
version: "0.6.0",
|
|
1675
2781
|
ts: Date.now()
|
|
1676
2782
|
}));
|
|
1677
2783
|
});
|
|
@@ -1706,6 +2812,7 @@
|
|
|
1706
2812
|
});
|
|
1707
2813
|
}
|
|
1708
2814
|
chrome.tabs.onRemoved.addListener((tabId) => {
|
|
2815
|
+
dropNetworkCapture(tabId);
|
|
1709
2816
|
void listWorkspaceStates().then(async (states) => {
|
|
1710
2817
|
for (const state of states) {
|
|
1711
2818
|
if (!state.tabIds.includes(tabId)) {
|