@flrande/bak-extension 0.3.8 → 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 +1305 -140
- package/dist/content.global.js +820 -3
- package/dist/manifest.json +2 -2
- package/package.json +2 -2
- package/public/manifest.json +2 -2
- package/src/background.ts +1762 -992
- package/src/content.ts +2419 -1593
- package/src/network-debugger.ts +495 -0
- package/src/privacy.ts +112 -1
- package/src/session-binding-storage.ts +68 -0
- package/src/workspace.ts +912 -917
|
@@ -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) {
|
|
@@ -30,42 +463,95 @@
|
|
|
30
463
|
return Math.min(maxDelayMs, baseDelayMs * 2 ** safeAttempt);
|
|
31
464
|
}
|
|
32
465
|
|
|
466
|
+
// src/session-binding-storage.ts
|
|
467
|
+
var STORAGE_KEY_SESSION_BINDINGS = "sessionBindings";
|
|
468
|
+
var LEGACY_STORAGE_KEY_WORKSPACES = "agentWorkspaces";
|
|
469
|
+
var LEGACY_STORAGE_KEY_WORKSPACE = "agentWorkspace";
|
|
470
|
+
function isWorkspaceRecord(value) {
|
|
471
|
+
if (typeof value !== "object" || value === null) {
|
|
472
|
+
return false;
|
|
473
|
+
}
|
|
474
|
+
const candidate = value;
|
|
475
|
+
return typeof candidate.id === "string" && Array.isArray(candidate.tabIds) && (typeof candidate.windowId === "number" || candidate.windowId === null) && (typeof candidate.groupId === "number" || candidate.groupId === null) && (typeof candidate.activeTabId === "number" || candidate.activeTabId === null) && (typeof candidate.primaryTabId === "number" || candidate.primaryTabId === null);
|
|
476
|
+
}
|
|
477
|
+
function cloneWorkspaceRecord(state) {
|
|
478
|
+
return {
|
|
479
|
+
...state,
|
|
480
|
+
tabIds: [...state.tabIds]
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
function normalizeWorkspaceRecordMap(value) {
|
|
484
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
485
|
+
return {
|
|
486
|
+
found: false,
|
|
487
|
+
map: {}
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
const normalizedEntries = [];
|
|
491
|
+
for (const [workspaceId, entry] of Object.entries(value)) {
|
|
492
|
+
if (!isWorkspaceRecord(entry)) {
|
|
493
|
+
continue;
|
|
494
|
+
}
|
|
495
|
+
normalizedEntries.push([workspaceId, cloneWorkspaceRecord(entry)]);
|
|
496
|
+
}
|
|
497
|
+
return {
|
|
498
|
+
found: true,
|
|
499
|
+
map: Object.fromEntries(normalizedEntries)
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
function resolveSessionBindingStateMap(stored) {
|
|
503
|
+
const current = normalizeWorkspaceRecordMap(stored[STORAGE_KEY_SESSION_BINDINGS]);
|
|
504
|
+
if (current.found) {
|
|
505
|
+
return current.map;
|
|
506
|
+
}
|
|
507
|
+
const legacyMap = normalizeWorkspaceRecordMap(stored[LEGACY_STORAGE_KEY_WORKSPACES]);
|
|
508
|
+
if (legacyMap.found) {
|
|
509
|
+
return legacyMap.map;
|
|
510
|
+
}
|
|
511
|
+
const legacySingle = stored[LEGACY_STORAGE_KEY_WORKSPACE];
|
|
512
|
+
if (isWorkspaceRecord(legacySingle)) {
|
|
513
|
+
return {
|
|
514
|
+
[legacySingle.id]: cloneWorkspaceRecord(legacySingle)
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
return {};
|
|
518
|
+
}
|
|
519
|
+
|
|
33
520
|
// src/workspace.ts
|
|
34
|
-
var DEFAULT_WORKSPACE_ID = "default";
|
|
35
521
|
var DEFAULT_WORKSPACE_LABEL = "bak agent";
|
|
36
522
|
var DEFAULT_WORKSPACE_COLOR = "blue";
|
|
37
523
|
var DEFAULT_WORKSPACE_URL = "about:blank";
|
|
38
|
-
var
|
|
524
|
+
var SessionBindingManager = class {
|
|
39
525
|
storage;
|
|
40
526
|
browser;
|
|
41
527
|
constructor(storage, browser) {
|
|
42
528
|
this.storage = storage;
|
|
43
529
|
this.browser = browser;
|
|
44
530
|
}
|
|
45
|
-
async getWorkspaceInfo(workspaceId
|
|
531
|
+
async getWorkspaceInfo(workspaceId) {
|
|
46
532
|
return this.inspectWorkspace(workspaceId);
|
|
47
533
|
}
|
|
48
534
|
async ensureWorkspace(options = {}) {
|
|
49
535
|
const workspaceId = this.normalizeWorkspaceId(options.workspaceId);
|
|
50
536
|
const repairActions = [];
|
|
51
537
|
const initialUrl = options.initialUrl ?? DEFAULT_WORKSPACE_URL;
|
|
52
|
-
const persisted = await this.storage.load();
|
|
538
|
+
const persisted = await this.storage.load(workspaceId);
|
|
53
539
|
const created = !persisted;
|
|
54
540
|
let state = this.normalizeState(persisted, workspaceId);
|
|
55
541
|
const originalWindowId = state.windowId;
|
|
56
|
-
let
|
|
542
|
+
let window2 = state.windowId !== null ? await this.waitForWindow(state.windowId) : null;
|
|
57
543
|
let tabs = [];
|
|
58
|
-
if (!
|
|
544
|
+
if (!window2) {
|
|
59
545
|
const rebound = await this.rebindWorkspaceWindow(state);
|
|
60
546
|
if (rebound) {
|
|
61
|
-
|
|
547
|
+
window2 = rebound.window;
|
|
62
548
|
tabs = rebound.tabs;
|
|
63
549
|
if (originalWindowId !== rebound.window.id) {
|
|
64
550
|
repairActions.push("rebound-window");
|
|
65
551
|
}
|
|
66
552
|
}
|
|
67
553
|
}
|
|
68
|
-
if (!
|
|
554
|
+
if (!window2) {
|
|
69
555
|
const createdWindow = await this.browser.createWindow({
|
|
70
556
|
url: initialUrl,
|
|
71
557
|
focused: options.focus === true
|
|
@@ -75,7 +561,7 @@
|
|
|
75
561
|
state.tabIds = [];
|
|
76
562
|
state.activeTabId = null;
|
|
77
563
|
state.primaryTabId = null;
|
|
78
|
-
|
|
564
|
+
window2 = createdWindow;
|
|
79
565
|
tabs = await this.waitForWindowTabs(createdWindow.id);
|
|
80
566
|
state.tabIds = tabs.map((tab) => tab.id);
|
|
81
567
|
if (state.primaryTabId === null) {
|
|
@@ -100,7 +586,7 @@
|
|
|
100
586
|
const ownership = await this.inspectWorkspaceWindowOwnership(state, state.windowId);
|
|
101
587
|
if (ownership.foreignTabs.length > 0) {
|
|
102
588
|
const migrated = await this.moveWorkspaceIntoDedicatedWindow(state, ownership, initialUrl);
|
|
103
|
-
|
|
589
|
+
window2 = migrated.window;
|
|
104
590
|
tabs = migrated.tabs;
|
|
105
591
|
state.tabIds = tabs.map((tab) => tab.id);
|
|
106
592
|
repairActions.push("migrated-dirty-window");
|
|
@@ -163,8 +649,8 @@
|
|
|
163
649
|
state.tabIds = [...new Set(tabs.map((tab) => tab.id))];
|
|
164
650
|
if (options.focus === true && state.activeTabId !== null) {
|
|
165
651
|
await this.browser.updateTab(state.activeTabId, { active: true });
|
|
166
|
-
|
|
167
|
-
void
|
|
652
|
+
window2 = await this.browser.updateWindow(state.windowId, { focused: true });
|
|
653
|
+
void window2;
|
|
168
654
|
repairActions.push("focused-window");
|
|
169
655
|
}
|
|
170
656
|
await this.storage.save(state);
|
|
@@ -263,7 +749,7 @@
|
|
|
263
749
|
tab
|
|
264
750
|
};
|
|
265
751
|
}
|
|
266
|
-
async listTabs(workspaceId
|
|
752
|
+
async listTabs(workspaceId) {
|
|
267
753
|
const ensured = await this.inspectWorkspace(workspaceId);
|
|
268
754
|
if (!ensured) {
|
|
269
755
|
throw new Error(`Workspace ${workspaceId} does not exist`);
|
|
@@ -273,7 +759,7 @@
|
|
|
273
759
|
tabs: ensured.tabs
|
|
274
760
|
};
|
|
275
761
|
}
|
|
276
|
-
async getActiveTab(workspaceId
|
|
762
|
+
async getActiveTab(workspaceId) {
|
|
277
763
|
const ensured = await this.inspectWorkspace(workspaceId);
|
|
278
764
|
if (!ensured) {
|
|
279
765
|
const normalizedWorkspaceId = this.normalizeWorkspaceId(workspaceId);
|
|
@@ -290,7 +776,7 @@
|
|
|
290
776
|
tab: ensured.tabs.find((tab) => tab.id === ensured.activeTabId) ?? null
|
|
291
777
|
};
|
|
292
778
|
}
|
|
293
|
-
async setActiveTab(tabId, workspaceId
|
|
779
|
+
async setActiveTab(tabId, workspaceId) {
|
|
294
780
|
const ensured = await this.ensureWorkspace({ workspaceId });
|
|
295
781
|
if (!ensured.workspace.tabIds.includes(tabId)) {
|
|
296
782
|
throw new Error(`Tab ${tabId} does not belong to workspace ${workspaceId}`);
|
|
@@ -319,7 +805,7 @@
|
|
|
319
805
|
tab
|
|
320
806
|
};
|
|
321
807
|
}
|
|
322
|
-
async focus(workspaceId
|
|
808
|
+
async focus(workspaceId) {
|
|
323
809
|
const ensured = await this.ensureWorkspace({ workspaceId, focus: false });
|
|
324
810
|
if (ensured.workspace.activeTabId !== null) {
|
|
325
811
|
await this.browser.updateTab(ensured.workspace.activeTabId, { active: true });
|
|
@@ -331,16 +817,20 @@
|
|
|
331
817
|
return { ok: true, workspace: refreshed.workspace };
|
|
332
818
|
}
|
|
333
819
|
async reset(options = {}) {
|
|
334
|
-
|
|
335
|
-
|
|
820
|
+
const workspaceId = this.normalizeWorkspaceId(options.workspaceId);
|
|
821
|
+
await this.close(workspaceId);
|
|
822
|
+
return this.ensureWorkspace({
|
|
823
|
+
...options,
|
|
824
|
+
workspaceId
|
|
825
|
+
});
|
|
336
826
|
}
|
|
337
|
-
async close(workspaceId
|
|
338
|
-
const state = await this.
|
|
339
|
-
if (!state
|
|
340
|
-
await this.storage.
|
|
827
|
+
async close(workspaceId) {
|
|
828
|
+
const state = await this.loadWorkspaceRecord(workspaceId);
|
|
829
|
+
if (!state) {
|
|
830
|
+
await this.storage.delete(workspaceId);
|
|
341
831
|
return { ok: true };
|
|
342
832
|
}
|
|
343
|
-
await this.storage.
|
|
833
|
+
await this.storage.delete(workspaceId);
|
|
344
834
|
if (state.windowId !== null) {
|
|
345
835
|
const existingWindow = await this.browser.getWindow(state.windowId);
|
|
346
836
|
if (existingWindow) {
|
|
@@ -366,19 +856,11 @@
|
|
|
366
856
|
}
|
|
367
857
|
const explicitWorkspaceId = typeof options.workspaceId === "string" ? this.normalizeWorkspaceId(options.workspaceId) : void 0;
|
|
368
858
|
if (explicitWorkspaceId) {
|
|
369
|
-
const
|
|
859
|
+
const ensured = await this.ensureWorkspace({
|
|
370
860
|
workspaceId: explicitWorkspaceId,
|
|
371
861
|
focus: false
|
|
372
862
|
});
|
|
373
|
-
return this.buildWorkspaceResolution(
|
|
374
|
-
}
|
|
375
|
-
const existingWorkspace = await this.loadWorkspaceRecord(DEFAULT_WORKSPACE_ID);
|
|
376
|
-
if (existingWorkspace) {
|
|
377
|
-
const ensured2 = await this.ensureWorkspace({
|
|
378
|
-
workspaceId: existingWorkspace.id,
|
|
379
|
-
focus: false
|
|
380
|
-
});
|
|
381
|
-
return this.buildWorkspaceResolution(ensured2, "default-workspace");
|
|
863
|
+
return this.buildWorkspaceResolution(ensured, "explicit-workspace");
|
|
382
864
|
}
|
|
383
865
|
if (options.createIfMissing !== true) {
|
|
384
866
|
const activeTab = await this.browser.getActiveTab();
|
|
@@ -394,19 +876,12 @@
|
|
|
394
876
|
repairActions: []
|
|
395
877
|
};
|
|
396
878
|
}
|
|
397
|
-
|
|
398
|
-
workspaceId: DEFAULT_WORKSPACE_ID,
|
|
399
|
-
focus: false
|
|
400
|
-
});
|
|
401
|
-
return this.buildWorkspaceResolution(ensured, "default-workspace");
|
|
879
|
+
throw new Error("workspaceId is required when createIfMissing is true");
|
|
402
880
|
}
|
|
403
881
|
normalizeWorkspaceId(workspaceId) {
|
|
404
882
|
const candidate = workspaceId?.trim();
|
|
405
883
|
if (!candidate) {
|
|
406
|
-
|
|
407
|
-
}
|
|
408
|
-
if (candidate !== DEFAULT_WORKSPACE_ID) {
|
|
409
|
-
throw new Error(`Unsupported workspace id: ${candidate}`);
|
|
884
|
+
throw new Error("workspaceId is required");
|
|
410
885
|
}
|
|
411
886
|
return candidate;
|
|
412
887
|
}
|
|
@@ -422,9 +897,12 @@
|
|
|
422
897
|
primaryTabId: state?.primaryTabId ?? null
|
|
423
898
|
};
|
|
424
899
|
}
|
|
425
|
-
async
|
|
900
|
+
async listWorkspaceRecords() {
|
|
901
|
+
return await this.storage.list();
|
|
902
|
+
}
|
|
903
|
+
async loadWorkspaceRecord(workspaceId) {
|
|
426
904
|
const normalizedWorkspaceId = this.normalizeWorkspaceId(workspaceId);
|
|
427
|
-
const state = await this.storage.load();
|
|
905
|
+
const state = await this.storage.load(normalizedWorkspaceId);
|
|
428
906
|
if (!state || state.id !== normalizedWorkspaceId) {
|
|
429
907
|
return null;
|
|
430
908
|
}
|
|
@@ -511,8 +989,8 @@
|
|
|
511
989
|
pushWindowId(tab.windowId);
|
|
512
990
|
}
|
|
513
991
|
for (const candidateWindowId of candidateWindowIds) {
|
|
514
|
-
const
|
|
515
|
-
if (!
|
|
992
|
+
const window2 = await this.waitForWindow(candidateWindowId);
|
|
993
|
+
if (!window2) {
|
|
516
994
|
continue;
|
|
517
995
|
}
|
|
518
996
|
let tabs = await this.readTrackedTabs(this.collectCandidateTabIds(state), candidateWindowId);
|
|
@@ -533,7 +1011,7 @@
|
|
|
533
1011
|
state.activeTabId = tabs.find((tab) => tab.active)?.id ?? state.primaryTabId;
|
|
534
1012
|
}
|
|
535
1013
|
}
|
|
536
|
-
return { window, tabs };
|
|
1014
|
+
return { window: window2, tabs };
|
|
537
1015
|
}
|
|
538
1016
|
return null;
|
|
539
1017
|
}
|
|
@@ -548,11 +1026,11 @@
|
|
|
548
1026
|
async moveWorkspaceIntoDedicatedWindow(state, ownership, initialUrl) {
|
|
549
1027
|
const sourceTabs = this.orderWorkspaceTabsForMigration(state, ownership.workspaceTabs);
|
|
550
1028
|
const seedUrl = sourceTabs[0]?.url ?? initialUrl;
|
|
551
|
-
const
|
|
1029
|
+
const window2 = await this.browser.createWindow({
|
|
552
1030
|
url: seedUrl || DEFAULT_WORKSPACE_URL,
|
|
553
1031
|
focused: false
|
|
554
1032
|
});
|
|
555
|
-
const recreatedTabs = await this.waitForWindowTabs(
|
|
1033
|
+
const recreatedTabs = await this.waitForWindowTabs(window2.id);
|
|
556
1034
|
const firstTab = recreatedTabs[0] ?? null;
|
|
557
1035
|
const tabIdMap = /* @__PURE__ */ new Map();
|
|
558
1036
|
if (sourceTabs[0] && firstTab) {
|
|
@@ -560,7 +1038,7 @@
|
|
|
560
1038
|
}
|
|
561
1039
|
for (const sourceTab of sourceTabs.slice(1)) {
|
|
562
1040
|
const recreated = await this.createWorkspaceTab({
|
|
563
|
-
windowId:
|
|
1041
|
+
windowId: window2.id,
|
|
564
1042
|
url: sourceTab.url,
|
|
565
1043
|
active: false
|
|
566
1044
|
});
|
|
@@ -572,7 +1050,7 @@
|
|
|
572
1050
|
if (nextActiveTabId !== null) {
|
|
573
1051
|
await this.browser.updateTab(nextActiveTabId, { active: true });
|
|
574
1052
|
}
|
|
575
|
-
state.windowId =
|
|
1053
|
+
state.windowId = window2.id;
|
|
576
1054
|
state.groupId = null;
|
|
577
1055
|
state.tabIds = recreatedTabs.map((tab) => tab.id);
|
|
578
1056
|
state.primaryTabId = nextPrimaryTabId;
|
|
@@ -581,7 +1059,7 @@
|
|
|
581
1059
|
await this.browser.closeTab(workspaceTab.id);
|
|
582
1060
|
}
|
|
583
1061
|
return {
|
|
584
|
-
window,
|
|
1062
|
+
window: window2,
|
|
585
1063
|
tabs: await this.readTrackedTabs(state.tabIds, state.windowId)
|
|
586
1064
|
};
|
|
587
1065
|
}
|
|
@@ -659,7 +1137,7 @@
|
|
|
659
1137
|
}
|
|
660
1138
|
throw lastError2 ?? new Error(`No window with id: ${options.windowId}.`);
|
|
661
1139
|
}
|
|
662
|
-
async inspectWorkspace(workspaceId
|
|
1140
|
+
async inspectWorkspace(workspaceId) {
|
|
663
1141
|
const state = await this.loadWorkspaceRecord(workspaceId);
|
|
664
1142
|
if (!state) {
|
|
665
1143
|
return null;
|
|
@@ -711,9 +1189,9 @@
|
|
|
711
1189
|
async waitForWindow(windowId, timeoutMs = 750) {
|
|
712
1190
|
const deadline = Date.now() + timeoutMs;
|
|
713
1191
|
while (Date.now() < deadline) {
|
|
714
|
-
const
|
|
715
|
-
if (
|
|
716
|
-
return
|
|
1192
|
+
const window2 = await this.browser.getWindow(windowId);
|
|
1193
|
+
if (window2) {
|
|
1194
|
+
return window2;
|
|
717
1195
|
}
|
|
718
1196
|
await this.delay(50);
|
|
719
1197
|
}
|
|
@@ -755,8 +1233,21 @@
|
|
|
755
1233
|
var STORAGE_KEY_TOKEN = "pairToken";
|
|
756
1234
|
var STORAGE_KEY_PORT = "cliPort";
|
|
757
1235
|
var STORAGE_KEY_DEBUG_RICH_TEXT = "debugRichText";
|
|
758
|
-
var STORAGE_KEY_WORKSPACE = "agentWorkspace";
|
|
759
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
|
+
]);
|
|
760
1251
|
var ws = null;
|
|
761
1252
|
var reconnectTimer = null;
|
|
762
1253
|
var nextReconnectInMs = null;
|
|
@@ -817,6 +1308,12 @@
|
|
|
817
1308
|
if (lower.includes("no tab with id") || lower.includes("no window with id")) {
|
|
818
1309
|
return toError("E_NOT_FOUND", message);
|
|
819
1310
|
}
|
|
1311
|
+
if (lower.includes("workspace") && lower.includes("does not exist")) {
|
|
1312
|
+
return toError("E_NOT_FOUND", message);
|
|
1313
|
+
}
|
|
1314
|
+
if (lower.includes("does not belong to workspace") || lower.includes("is missing from workspace")) {
|
|
1315
|
+
return toError("E_NOT_FOUND", message);
|
|
1316
|
+
}
|
|
820
1317
|
if (lower.includes("invalid url") || lower.includes("url is invalid")) {
|
|
821
1318
|
return toError("E_INVALID_PARAMS", message);
|
|
822
1319
|
}
|
|
@@ -838,20 +1335,36 @@
|
|
|
838
1335
|
groupId: typeof tab.groupId === "number" && tab.groupId >= 0 ? tab.groupId : null
|
|
839
1336
|
};
|
|
840
1337
|
}
|
|
841
|
-
async function
|
|
842
|
-
const stored = await chrome.storage.local.get(
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
return
|
|
1338
|
+
async function loadWorkspaceStateMap() {
|
|
1339
|
+
const stored = await chrome.storage.local.get([
|
|
1340
|
+
STORAGE_KEY_SESSION_BINDINGS,
|
|
1341
|
+
LEGACY_STORAGE_KEY_WORKSPACES,
|
|
1342
|
+
LEGACY_STORAGE_KEY_WORKSPACE
|
|
1343
|
+
]);
|
|
1344
|
+
return resolveSessionBindingStateMap(stored);
|
|
1345
|
+
}
|
|
1346
|
+
async function loadWorkspaceState(workspaceId) {
|
|
1347
|
+
const stateMap = await loadWorkspaceStateMap();
|
|
1348
|
+
return stateMap[workspaceId] ?? null;
|
|
1349
|
+
}
|
|
1350
|
+
async function listWorkspaceStates() {
|
|
1351
|
+
return Object.values(await loadWorkspaceStateMap());
|
|
848
1352
|
}
|
|
849
1353
|
async function saveWorkspaceState(state) {
|
|
850
|
-
|
|
851
|
-
|
|
1354
|
+
const stateMap = await loadWorkspaceStateMap();
|
|
1355
|
+
stateMap[state.id] = state;
|
|
1356
|
+
await chrome.storage.local.set({ [STORAGE_KEY_SESSION_BINDINGS]: stateMap });
|
|
1357
|
+
await chrome.storage.local.remove([LEGACY_STORAGE_KEY_WORKSPACES, LEGACY_STORAGE_KEY_WORKSPACE]);
|
|
1358
|
+
}
|
|
1359
|
+
async function deleteWorkspaceState(workspaceId) {
|
|
1360
|
+
const stateMap = await loadWorkspaceStateMap();
|
|
1361
|
+
delete stateMap[workspaceId];
|
|
1362
|
+
if (Object.keys(stateMap).length === 0) {
|
|
1363
|
+
await chrome.storage.local.remove([STORAGE_KEY_SESSION_BINDINGS, LEGACY_STORAGE_KEY_WORKSPACES, LEGACY_STORAGE_KEY_WORKSPACE]);
|
|
852
1364
|
return;
|
|
853
1365
|
}
|
|
854
|
-
await chrome.storage.local.set({ [
|
|
1366
|
+
await chrome.storage.local.set({ [STORAGE_KEY_SESSION_BINDINGS]: stateMap });
|
|
1367
|
+
await chrome.storage.local.remove([LEGACY_STORAGE_KEY_WORKSPACES, LEGACY_STORAGE_KEY_WORKSPACE]);
|
|
855
1368
|
}
|
|
856
1369
|
var workspaceBrowser = {
|
|
857
1370
|
async getTab(tabId) {
|
|
@@ -899,17 +1412,17 @@
|
|
|
899
1412
|
},
|
|
900
1413
|
async getWindow(windowId) {
|
|
901
1414
|
try {
|
|
902
|
-
const
|
|
1415
|
+
const window2 = await chrome.windows.get(windowId);
|
|
903
1416
|
return {
|
|
904
|
-
id:
|
|
905
|
-
focused: Boolean(
|
|
1417
|
+
id: window2.id,
|
|
1418
|
+
focused: Boolean(window2.focused)
|
|
906
1419
|
};
|
|
907
1420
|
} catch {
|
|
908
1421
|
return null;
|
|
909
1422
|
}
|
|
910
1423
|
},
|
|
911
1424
|
async createWindow(options) {
|
|
912
|
-
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;
|
|
913
1426
|
const previouslyFocusedTab = previouslyFocusedWindow?.id !== void 0 ? (await chrome.tabs.query({ windowId: previouslyFocusedWindow.id, active: true })).find((tab) => typeof tab.id === "number") ?? null : null;
|
|
914
1427
|
const created = await chrome.windows.create({
|
|
915
1428
|
url: options.url ?? "about:blank",
|
|
@@ -983,10 +1496,12 @@
|
|
|
983
1496
|
};
|
|
984
1497
|
}
|
|
985
1498
|
};
|
|
986
|
-
var
|
|
1499
|
+
var bindingManager = new SessionBindingManager(
|
|
987
1500
|
{
|
|
988
1501
|
load: loadWorkspaceState,
|
|
989
|
-
save: saveWorkspaceState
|
|
1502
|
+
save: saveWorkspaceState,
|
|
1503
|
+
delete: deleteWorkspaceState,
|
|
1504
|
+
list: listWorkspaceStates
|
|
990
1505
|
},
|
|
991
1506
|
workspaceBrowser
|
|
992
1507
|
);
|
|
@@ -1094,7 +1609,7 @@
|
|
|
1094
1609
|
} catch {
|
|
1095
1610
|
refreshedTab = await workspaceBrowser.getTab(opened.tab.id) ?? opened.tab;
|
|
1096
1611
|
}
|
|
1097
|
-
const refreshedWorkspace = await
|
|
1612
|
+
const refreshedWorkspace = await bindingManager.getWorkspaceInfo(opened.workspace.id) ?? {
|
|
1098
1613
|
...opened.workspace,
|
|
1099
1614
|
tabs: opened.workspace.tabs.map((tab) => tab.id === refreshedTab.id ? refreshedTab : tab)
|
|
1100
1615
|
};
|
|
@@ -1121,7 +1636,7 @@
|
|
|
1121
1636
|
const tab2 = await chrome.tabs.get(target.tabId);
|
|
1122
1637
|
return validate(tab2);
|
|
1123
1638
|
}
|
|
1124
|
-
const resolved = await
|
|
1639
|
+
const resolved = await bindingManager.resolveTarget({
|
|
1125
1640
|
tabId: target.tabId,
|
|
1126
1641
|
workspaceId: typeof target.workspaceId === "string" ? target.workspaceId : void 0,
|
|
1127
1642
|
createIfMissing: false
|
|
@@ -1222,6 +1737,369 @@
|
|
|
1222
1737
|
}
|
|
1223
1738
|
return response.result;
|
|
1224
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
|
+
}
|
|
1225
2103
|
async function handleRequest(request) {
|
|
1226
2104
|
const params = request.params ?? {};
|
|
1227
2105
|
const target = {
|
|
@@ -1254,16 +2132,17 @@
|
|
|
1254
2132
|
"mouse.click",
|
|
1255
2133
|
"mouse.wheel",
|
|
1256
2134
|
"file.upload",
|
|
2135
|
+
"context.get",
|
|
2136
|
+
"context.set",
|
|
1257
2137
|
"context.enterFrame",
|
|
1258
2138
|
"context.exitFrame",
|
|
1259
2139
|
"context.enterShadow",
|
|
1260
2140
|
"context.exitShadow",
|
|
1261
2141
|
"context.reset",
|
|
1262
|
-
"
|
|
1263
|
-
"
|
|
1264
|
-
"
|
|
1265
|
-
"
|
|
1266
|
-
"debug.dumpState"
|
|
2142
|
+
"table.list",
|
|
2143
|
+
"table.schema",
|
|
2144
|
+
"table.rows",
|
|
2145
|
+
"table.export"
|
|
1267
2146
|
]);
|
|
1268
2147
|
switch (request.method) {
|
|
1269
2148
|
case "session.ping": {
|
|
@@ -1301,24 +2180,6 @@
|
|
|
1301
2180
|
return { ok: true };
|
|
1302
2181
|
}
|
|
1303
2182
|
case "tabs.new": {
|
|
1304
|
-
if (typeof params.workspaceId === "string" || params.windowId === void 0) {
|
|
1305
|
-
const expectedUrl = params.url ?? "about:blank";
|
|
1306
|
-
const opened = await preserveHumanFocus(true, async () => {
|
|
1307
|
-
return await workspaceManager.openTab({
|
|
1308
|
-
workspaceId: typeof params.workspaceId === "string" ? params.workspaceId : DEFAULT_WORKSPACE_ID,
|
|
1309
|
-
url: expectedUrl,
|
|
1310
|
-
active: params.active === true,
|
|
1311
|
-
focus: false
|
|
1312
|
-
});
|
|
1313
|
-
});
|
|
1314
|
-
const stabilized = await finalizeOpenedWorkspaceTab(opened, expectedUrl);
|
|
1315
|
-
return {
|
|
1316
|
-
tabId: stabilized.tab.id,
|
|
1317
|
-
windowId: stabilized.tab.windowId,
|
|
1318
|
-
groupId: stabilized.workspace.groupId,
|
|
1319
|
-
workspaceId: stabilized.workspace.id
|
|
1320
|
-
};
|
|
1321
|
-
}
|
|
1322
2183
|
const tab = await chrome.tabs.create({
|
|
1323
2184
|
url: params.url ?? "about:blank",
|
|
1324
2185
|
windowId: typeof params.windowId === "number" ? params.windowId : void 0,
|
|
@@ -1344,59 +2205,68 @@
|
|
|
1344
2205
|
}
|
|
1345
2206
|
case "workspace.ensure": {
|
|
1346
2207
|
return preserveHumanFocus(params.focus !== true, async () => {
|
|
1347
|
-
|
|
1348
|
-
workspaceId:
|
|
2208
|
+
const result = await bindingManager.ensureWorkspace({
|
|
2209
|
+
workspaceId: String(params.workspaceId ?? ""),
|
|
1349
2210
|
focus: params.focus === true,
|
|
1350
2211
|
initialUrl: typeof params.url === "string" ? params.url : void 0
|
|
1351
2212
|
});
|
|
2213
|
+
for (const tab of result.workspace.tabs) {
|
|
2214
|
+
void ensureNetworkDebugger(tab.id).catch(() => void 0);
|
|
2215
|
+
}
|
|
2216
|
+
return result;
|
|
1352
2217
|
});
|
|
1353
2218
|
}
|
|
1354
2219
|
case "workspace.info": {
|
|
1355
2220
|
return {
|
|
1356
|
-
workspace: await
|
|
2221
|
+
workspace: await bindingManager.getWorkspaceInfo(String(params.workspaceId ?? ""))
|
|
1357
2222
|
};
|
|
1358
2223
|
}
|
|
1359
2224
|
case "workspace.openTab": {
|
|
1360
2225
|
const expectedUrl = typeof params.url === "string" ? params.url : void 0;
|
|
1361
2226
|
const opened = await preserveHumanFocus(params.focus !== true, async () => {
|
|
1362
|
-
return await
|
|
1363
|
-
workspaceId:
|
|
2227
|
+
return await bindingManager.openTab({
|
|
2228
|
+
workspaceId: String(params.workspaceId ?? ""),
|
|
1364
2229
|
url: expectedUrl,
|
|
1365
2230
|
active: params.active === true,
|
|
1366
2231
|
focus: params.focus === true
|
|
1367
2232
|
});
|
|
1368
2233
|
});
|
|
1369
|
-
|
|
2234
|
+
const finalized = await finalizeOpenedWorkspaceTab(opened, expectedUrl);
|
|
2235
|
+
void ensureNetworkDebugger(finalized.tab.id).catch(() => void 0);
|
|
2236
|
+
return finalized;
|
|
1370
2237
|
}
|
|
1371
2238
|
case "workspace.listTabs": {
|
|
1372
|
-
return await
|
|
2239
|
+
return await bindingManager.listTabs(String(params.workspaceId ?? ""));
|
|
1373
2240
|
}
|
|
1374
2241
|
case "workspace.getActiveTab": {
|
|
1375
|
-
return await
|
|
2242
|
+
return await bindingManager.getActiveTab(String(params.workspaceId ?? ""));
|
|
1376
2243
|
}
|
|
1377
2244
|
case "workspace.setActiveTab": {
|
|
1378
|
-
|
|
2245
|
+
const result = await bindingManager.setActiveTab(Number(params.tabId), String(params.workspaceId ?? ""));
|
|
2246
|
+
void ensureNetworkDebugger(result.tab.id).catch(() => void 0);
|
|
2247
|
+
return result;
|
|
1379
2248
|
}
|
|
1380
2249
|
case "workspace.focus": {
|
|
1381
|
-
return await
|
|
2250
|
+
return await bindingManager.focus(String(params.workspaceId ?? ""));
|
|
1382
2251
|
}
|
|
1383
2252
|
case "workspace.reset": {
|
|
1384
2253
|
return await preserveHumanFocus(params.focus !== true, async () => {
|
|
1385
|
-
return await
|
|
1386
|
-
workspaceId:
|
|
2254
|
+
return await bindingManager.reset({
|
|
2255
|
+
workspaceId: String(params.workspaceId ?? ""),
|
|
1387
2256
|
focus: params.focus === true,
|
|
1388
2257
|
initialUrl: typeof params.url === "string" ? params.url : void 0
|
|
1389
2258
|
});
|
|
1390
2259
|
});
|
|
1391
2260
|
}
|
|
1392
2261
|
case "workspace.close": {
|
|
1393
|
-
return await
|
|
2262
|
+
return await bindingManager.close(String(params.workspaceId ?? ""));
|
|
1394
2263
|
}
|
|
1395
2264
|
case "page.goto": {
|
|
1396
2265
|
return await preserveHumanFocus(typeof target.tabId !== "number", async () => {
|
|
1397
2266
|
const tab = await withTab(target, {
|
|
1398
2267
|
requireSupportedAutomationUrl: false
|
|
1399
2268
|
});
|
|
2269
|
+
void ensureNetworkDebugger(tab.id).catch(() => void 0);
|
|
1400
2270
|
const url = String(params.url ?? "about:blank");
|
|
1401
2271
|
await chrome.tabs.update(tab.id, { url });
|
|
1402
2272
|
await waitForTabUrl(tab.id, url);
|
|
@@ -1408,6 +2278,7 @@
|
|
|
1408
2278
|
case "page.back": {
|
|
1409
2279
|
return await preserveHumanFocus(typeof target.tabId !== "number", async () => {
|
|
1410
2280
|
const tab = await withTab(target);
|
|
2281
|
+
void ensureNetworkDebugger(tab.id).catch(() => void 0);
|
|
1411
2282
|
await chrome.tabs.goBack(tab.id);
|
|
1412
2283
|
await waitForTabComplete(tab.id);
|
|
1413
2284
|
return { ok: true };
|
|
@@ -1416,6 +2287,7 @@
|
|
|
1416
2287
|
case "page.forward": {
|
|
1417
2288
|
return await preserveHumanFocus(typeof target.tabId !== "number", async () => {
|
|
1418
2289
|
const tab = await withTab(target);
|
|
2290
|
+
void ensureNetworkDebugger(tab.id).catch(() => void 0);
|
|
1419
2291
|
await chrome.tabs.goForward(tab.id);
|
|
1420
2292
|
await waitForTabComplete(tab.id);
|
|
1421
2293
|
return { ok: true };
|
|
@@ -1424,6 +2296,7 @@
|
|
|
1424
2296
|
case "page.reload": {
|
|
1425
2297
|
return await preserveHumanFocus(typeof target.tabId !== "number", async () => {
|
|
1426
2298
|
const tab = await withTab(target);
|
|
2299
|
+
void ensureNetworkDebugger(tab.id).catch(() => void 0);
|
|
1427
2300
|
await chrome.tabs.reload(tab.id);
|
|
1428
2301
|
await waitForTabComplete(tab.id);
|
|
1429
2302
|
return { ok: true };
|
|
@@ -1455,6 +2328,24 @@
|
|
|
1455
2328
|
};
|
|
1456
2329
|
});
|
|
1457
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
|
+
}
|
|
1458
2349
|
case "page.snapshot": {
|
|
1459
2350
|
return await preserveHumanFocus(typeof target.tabId !== "number", async () => {
|
|
1460
2351
|
const tab = await withTab(target);
|
|
@@ -1549,6 +2440,273 @@
|
|
|
1549
2440
|
return { entries: response.entries };
|
|
1550
2441
|
});
|
|
1551
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
|
+
}
|
|
1552
2710
|
case "ui.selectCandidate": {
|
|
1553
2711
|
return await preserveHumanFocus(typeof target.tabId !== "number", async () => {
|
|
1554
2712
|
const tab = await withTab(target);
|
|
@@ -1619,7 +2777,7 @@
|
|
|
1619
2777
|
ws?.send(JSON.stringify({
|
|
1620
2778
|
type: "hello",
|
|
1621
2779
|
role: "extension",
|
|
1622
|
-
version: "0.
|
|
2780
|
+
version: "0.6.0",
|
|
1623
2781
|
ts: Date.now()
|
|
1624
2782
|
}));
|
|
1625
2783
|
});
|
|
@@ -1654,43 +2812,50 @@
|
|
|
1654
2812
|
});
|
|
1655
2813
|
}
|
|
1656
2814
|
chrome.tabs.onRemoved.addListener((tabId) => {
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
2815
|
+
dropNetworkCapture(tabId);
|
|
2816
|
+
void listWorkspaceStates().then(async (states) => {
|
|
2817
|
+
for (const state of states) {
|
|
2818
|
+
if (!state.tabIds.includes(tabId)) {
|
|
2819
|
+
continue;
|
|
2820
|
+
}
|
|
2821
|
+
const nextTabIds = state.tabIds.filter((id) => id !== tabId);
|
|
2822
|
+
await saveWorkspaceState({
|
|
2823
|
+
...state,
|
|
2824
|
+
tabIds: nextTabIds,
|
|
2825
|
+
activeTabId: state.activeTabId === tabId ? null : state.activeTabId,
|
|
2826
|
+
primaryTabId: state.primaryTabId === tabId ? null : state.primaryTabId
|
|
2827
|
+
});
|
|
1660
2828
|
}
|
|
1661
|
-
const nextTabIds = state.tabIds.filter((id) => id !== tabId);
|
|
1662
|
-
await saveWorkspaceState({
|
|
1663
|
-
...state,
|
|
1664
|
-
tabIds: nextTabIds,
|
|
1665
|
-
activeTabId: state.activeTabId === tabId ? null : state.activeTabId,
|
|
1666
|
-
primaryTabId: state.primaryTabId === tabId ? null : state.primaryTabId
|
|
1667
|
-
});
|
|
1668
2829
|
});
|
|
1669
2830
|
});
|
|
1670
2831
|
chrome.tabs.onActivated.addListener((activeInfo) => {
|
|
1671
|
-
void
|
|
1672
|
-
|
|
1673
|
-
|
|
2832
|
+
void listWorkspaceStates().then(async (states) => {
|
|
2833
|
+
for (const state of states) {
|
|
2834
|
+
if (state.windowId !== activeInfo.windowId || !state.tabIds.includes(activeInfo.tabId)) {
|
|
2835
|
+
continue;
|
|
2836
|
+
}
|
|
2837
|
+
await saveWorkspaceState({
|
|
2838
|
+
...state,
|
|
2839
|
+
activeTabId: activeInfo.tabId
|
|
2840
|
+
});
|
|
1674
2841
|
}
|
|
1675
|
-
await saveWorkspaceState({
|
|
1676
|
-
...state,
|
|
1677
|
-
activeTabId: activeInfo.tabId
|
|
1678
|
-
});
|
|
1679
2842
|
});
|
|
1680
2843
|
});
|
|
1681
2844
|
chrome.windows.onRemoved.addListener((windowId) => {
|
|
1682
|
-
void
|
|
1683
|
-
|
|
1684
|
-
|
|
2845
|
+
void listWorkspaceStates().then(async (states) => {
|
|
2846
|
+
for (const state of states) {
|
|
2847
|
+
if (state.windowId !== windowId) {
|
|
2848
|
+
continue;
|
|
2849
|
+
}
|
|
2850
|
+
await saveWorkspaceState({
|
|
2851
|
+
...state,
|
|
2852
|
+
windowId: null,
|
|
2853
|
+
groupId: null,
|
|
2854
|
+
tabIds: [],
|
|
2855
|
+
activeTabId: null,
|
|
2856
|
+
primaryTabId: null
|
|
2857
|
+
});
|
|
1685
2858
|
}
|
|
1686
|
-
await saveWorkspaceState({
|
|
1687
|
-
...state,
|
|
1688
|
-
windowId: null,
|
|
1689
|
-
groupId: null,
|
|
1690
|
-
tabIds: [],
|
|
1691
|
-
activeTabId: null,
|
|
1692
|
-
primaryTabId: null
|
|
1693
|
-
});
|
|
1694
2859
|
});
|
|
1695
2860
|
});
|
|
1696
2861
|
chrome.runtime.onInstalled.addListener(() => {
|