@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.
@@ -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 WorkspaceManager = class {
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 = DEFAULT_WORKSPACE_ID) {
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 window = state.windowId !== null ? await this.waitForWindow(state.windowId) : null;
542
+ let window2 = state.windowId !== null ? await this.waitForWindow(state.windowId) : null;
57
543
  let tabs = [];
58
- if (!window) {
544
+ if (!window2) {
59
545
  const rebound = await this.rebindWorkspaceWindow(state);
60
546
  if (rebound) {
61
- window = rebound.window;
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 (!window) {
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
- window = createdWindow;
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
- window = migrated.window;
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
- window = await this.browser.updateWindow(state.windowId, { focused: true });
167
- void window;
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 = DEFAULT_WORKSPACE_ID) {
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 = DEFAULT_WORKSPACE_ID) {
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 = DEFAULT_WORKSPACE_ID) {
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 = DEFAULT_WORKSPACE_ID) {
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
- await this.close(options.workspaceId);
335
- return this.ensureWorkspace(options);
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 = DEFAULT_WORKSPACE_ID) {
338
- const state = await this.storage.load();
339
- if (!state || state.id !== workspaceId) {
340
- await this.storage.save(null);
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.save(null);
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 ensured2 = await this.ensureWorkspace({
859
+ const ensured = await this.ensureWorkspace({
370
860
  workspaceId: explicitWorkspaceId,
371
861
  focus: false
372
862
  });
373
- return this.buildWorkspaceResolution(ensured2, "explicit-workspace");
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
- const ensured = await this.ensureWorkspace({
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
- return DEFAULT_WORKSPACE_ID;
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 loadWorkspaceRecord(workspaceId = DEFAULT_WORKSPACE_ID) {
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 window = await this.waitForWindow(candidateWindowId);
515
- if (!window) {
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 window = await this.browser.createWindow({
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(window.id);
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: window.id,
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 = window.id;
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 = DEFAULT_WORKSPACE_ID) {
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 window = await this.browser.getWindow(windowId);
715
- if (window) {
716
- return window;
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 loadWorkspaceState() {
842
- const stored = await chrome.storage.local.get(STORAGE_KEY_WORKSPACE);
843
- const state = stored[STORAGE_KEY_WORKSPACE];
844
- if (!state || typeof state !== "object") {
845
- return null;
846
- }
847
- return state;
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
- if (state === null) {
851
- await chrome.storage.local.remove(STORAGE_KEY_WORKSPACE);
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({ [STORAGE_KEY_WORKSPACE]: state });
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 window = await chrome.windows.get(windowId);
1415
+ const window2 = await chrome.windows.get(windowId);
903
1416
  return {
904
- id: window.id,
905
- focused: Boolean(window.focused)
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((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;
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 workspaceManager = new WorkspaceManager(
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 workspaceManager.getWorkspaceInfo(opened.workspace.id) ?? {
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 workspaceManager.resolveTarget({
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
- "network.list",
1263
- "network.get",
1264
- "network.waitFor",
1265
- "network.clear",
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
- return await workspaceManager.ensureWorkspace({
1348
- workspaceId: typeof params.workspaceId === "string" ? params.workspaceId : void 0,
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 workspaceManager.getWorkspaceInfo(typeof params.workspaceId === "string" ? params.workspaceId : void 0)
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 workspaceManager.openTab({
1363
- workspaceId: typeof params.workspaceId === "string" ? params.workspaceId : void 0,
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
- return await finalizeOpenedWorkspaceTab(opened, expectedUrl);
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 workspaceManager.listTabs(typeof params.workspaceId === "string" ? params.workspaceId : void 0);
2239
+ return await bindingManager.listTabs(String(params.workspaceId ?? ""));
1373
2240
  }
1374
2241
  case "workspace.getActiveTab": {
1375
- return await workspaceManager.getActiveTab(typeof params.workspaceId === "string" ? params.workspaceId : void 0);
2242
+ return await bindingManager.getActiveTab(String(params.workspaceId ?? ""));
1376
2243
  }
1377
2244
  case "workspace.setActiveTab": {
1378
- return await workspaceManager.setActiveTab(Number(params.tabId), typeof params.workspaceId === "string" ? params.workspaceId : void 0);
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 workspaceManager.focus(typeof params.workspaceId === "string" ? params.workspaceId : void 0);
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 workspaceManager.reset({
1386
- workspaceId: typeof params.workspaceId === "string" ? params.workspaceId : void 0,
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 workspaceManager.close(typeof params.workspaceId === "string" ? params.workspaceId : void 0);
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.3.8",
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
- void loadWorkspaceState().then(async (state) => {
1658
- if (!state || !state.tabIds.includes(tabId)) {
1659
- return;
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 loadWorkspaceState().then(async (state) => {
1672
- if (!state || state.windowId !== activeInfo.windowId || !state.tabIds.includes(activeInfo.tabId)) {
1673
- return;
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 loadWorkspaceState().then(async (state) => {
1683
- if (!state || state.windowId !== windowId) {
1684
- return;
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(() => {