@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.
@@ -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 window = state.windowId !== null ? await this.waitForWindow(state.windowId) : null;
542
+ let window2 = state.windowId !== null ? await this.waitForWindow(state.windowId) : null;
110
543
  let tabs = [];
111
- if (!window) {
544
+ if (!window2) {
112
545
  const rebound = await this.rebindWorkspaceWindow(state);
113
546
  if (rebound) {
114
- window = rebound.window;
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 (!window) {
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
- window = createdWindow;
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
- window = migrated.window;
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
- window = await this.browser.updateWindow(state.windowId, { focused: true });
220
- void window;
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 window = await this.waitForWindow(candidateWindowId);
560
- if (!window) {
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 window = await this.browser.createWindow({
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(window.id);
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: window.id,
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 = window.id;
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 window = await this.browser.getWindow(windowId);
760
- if (window) {
761
- return window;
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 window = await chrome.windows.get(windowId);
1415
+ const window2 = await chrome.windows.get(windowId);
969
1416
  return {
970
- id: window.id,
971
- focused: Boolean(window.focused)
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((window) => window.focused === true && typeof window.id === "number") ?? null;
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
- "network.list",
1333
- "network.get",
1334
- "network.waitFor",
1335
- "network.clear",
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
- return await bindingManager.ensureWorkspace({
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
- return await finalizeOpenedWorkspaceTab(opened, expectedUrl);
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
- return await bindingManager.setActiveTab(Number(params.tabId), String(params.workspaceId ?? ""));
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.5.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)) {