@flrande/bak-extension 0.6.14 → 0.6.16

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.
@@ -302,7 +302,7 @@
302
302
  detail: `Shared sample values: ${distinctOverlappingValues.join(", ")}`
303
303
  });
304
304
  }
305
- const explicitReferenceHit = table.table.name.toLowerCase().includes(source.source.label.toLowerCase()) || (table.table.selector ?? "").toLowerCase().includes(source.source.label.toLowerCase()) || source.source.label.toLowerCase().includes(table.table.name.toLowerCase());
305
+ const explicitReferenceHit = table.table.label.toLowerCase().includes(source.source.label.toLowerCase()) || (table.table.selector ?? "").toLowerCase().includes(source.source.label.toLowerCase()) || source.source.label.toLowerCase().includes(table.table.label.toLowerCase());
306
306
  if (explicitReferenceHit) {
307
307
  basis.push({
308
308
  type: "explicitReference",
@@ -533,9 +533,6 @@
533
533
  function shouldRedactHeader(name) {
534
534
  return SENSITIVE_HEADER_PATTERNS.some((pattern) => pattern.test(name));
535
535
  }
536
- function containsRedactionMarker(raw) {
537
- return typeof raw === "string" && raw.includes("[REDACTED");
538
- }
539
536
  function redactTransportText(raw) {
540
537
  if (!raw) {
541
538
  return "";
@@ -556,7 +553,7 @@
556
553
  // package.json
557
554
  var package_default = {
558
555
  name: "@flrande/bak-extension",
559
- version: "0.6.14",
556
+ version: "0.6.16",
560
557
  type: "module",
561
558
  scripts: {
562
559
  build: "tsup src/background.ts src/content.ts src/popup.ts --format iife --out-dir dist --clean && node scripts/copy-assets.mjs",
@@ -664,6 +661,17 @@
664
661
  const normalized = contentType.toLowerCase();
665
662
  return normalized.startsWith("text/") || normalized.includes("json") || normalized.includes("javascript") || normalized.includes("xml") || normalized.includes("html") || normalized.includes("urlencoded") || normalized.includes("graphql");
666
663
  }
664
+ function sanitizeEntry(entry) {
665
+ const { rawRequestHeaders, rawRequestBody, rawRequestBodyTruncated, ...publicEntry } = entry;
666
+ void rawRequestHeaders;
667
+ void rawRequestBody;
668
+ void rawRequestBodyTruncated;
669
+ return {
670
+ ...publicEntry,
671
+ requestHeaders: typeof entry.requestHeaders === "object" && entry.requestHeaders !== null ? { ...entry.requestHeaders } : void 0,
672
+ responseHeaders: typeof entry.responseHeaders === "object" && entry.responseHeaders !== null ? { ...entry.responseHeaders } : void 0
673
+ };
674
+ }
667
675
  function pushEntry(state, entry, requestId) {
668
676
  state.entries.push(entry);
669
677
  state.entriesById.set(entry.id, entry);
@@ -737,7 +745,8 @@
737
745
  return;
738
746
  }
739
747
  const request = typeof params.request === "object" && params.request !== null ? params.request : {};
740
- const headers = redactHeaderMap(normalizeHeaders(request.headers));
748
+ const rawHeaders = normalizeHeaders(request.headers);
749
+ const headers = redactHeaderMap(rawHeaders);
741
750
  const truncatedRequest = truncateText(typeof request.postData === "string" ? request.postData : void 0, DEFAULT_BODY_BYTES);
742
751
  const entry = {
743
752
  id: `net_${tabId}_${requestId}`,
@@ -754,6 +763,9 @@
754
763
  requestHeaders: headers,
755
764
  requestBodyPreview: truncatedRequest.text ? redactTransportText(truncatedRequest.text) : void 0,
756
765
  requestBodyTruncated: truncatedRequest.truncated,
766
+ rawRequestHeaders: rawHeaders,
767
+ rawRequestBody: typeof request.postData === "string" ? request.postData : void 0,
768
+ rawRequestBodyTruncated: false,
757
769
  initiatorUrl: typeof params.initiator === "object" && params.initiator !== null && typeof params.initiator.url === "string" ? String(params.initiator.url) : void 0,
758
770
  tabId,
759
771
  source: "debugger"
@@ -857,12 +869,32 @@
857
869
  function listNetworkEntries(tabId, filters = {}) {
858
870
  const state = getState(tabId);
859
871
  const limit = typeof filters.limit === "number" ? Math.max(1, Math.min(500, Math.floor(filters.limit))) : 50;
860
- return state.entries.filter((entry) => entryMatchesFilters(entry, filters)).slice(-limit).reverse().map((entry) => ({ ...entry }));
872
+ return state.entries.filter((entry) => entryMatchesFilters(entry, filters)).slice(-limit).reverse().map((entry) => sanitizeEntry(entry));
861
873
  }
862
874
  function getNetworkEntry(tabId, id) {
863
875
  const state = getState(tabId);
864
876
  const entry = state.entriesById.get(id);
865
- return entry ? { ...entry } : null;
877
+ return entry ? sanitizeEntry(entry) : null;
878
+ }
879
+ function getReplayableNetworkRequest(tabId, id) {
880
+ const state = getState(tabId);
881
+ const entry = state.entriesById.get(id);
882
+ if (!entry) {
883
+ return null;
884
+ }
885
+ const publicEntry = sanitizeEntry(entry);
886
+ if (entry.rawRequestBodyTruncated === true) {
887
+ return {
888
+ entry: publicEntry,
889
+ degradedReason: "live replay unavailable because the captured request body was truncated in memory"
890
+ };
891
+ }
892
+ return {
893
+ entry: publicEntry,
894
+ headers: entry.rawRequestHeaders ? { ...entry.rawRequestHeaders } : void 0,
895
+ body: entry.rawRequestBody,
896
+ contentType: headerValue(entry.rawRequestHeaders, "content-type")
897
+ };
866
898
  }
867
899
  async function waitForNetworkEntry(tabId, filters = {}) {
868
900
  const timeoutMs = typeof filters.timeoutMs === "number" ? Math.max(1, Math.floor(filters.timeoutMs)) : 5e3;
@@ -873,7 +905,7 @@
873
905
  const nextState = getState(tabId);
874
906
  const matched = nextState.entries.find((entry) => !seenIds.has(entry.id) && entryMatchesFilters(entry, filters));
875
907
  if (matched) {
876
- return { ...matched };
908
+ return sanitizeEntry(matched);
877
909
  }
878
910
  await new Promise((resolve) => setTimeout(resolve, 75));
879
911
  }
@@ -883,14 +915,30 @@
883
915
  };
884
916
  }
885
917
  function searchNetworkEntries(tabId, pattern, limit = 50) {
918
+ const state = getState(tabId);
886
919
  const normalized = pattern.toLowerCase();
887
- return listNetworkEntries(tabId, { limit: Math.max(limit, 1) }).filter((entry) => {
920
+ const matchedEntries = state.entries.filter((entry) => {
888
921
  const headerText = JSON.stringify({
889
922
  requestHeaders: entry.requestHeaders,
890
923
  responseHeaders: entry.responseHeaders
891
924
  }).toLowerCase();
892
925
  return entry.url.toLowerCase().includes(normalized) || (entry.requestBodyPreview ?? "").toLowerCase().includes(normalized) || (entry.responseBodyPreview ?? "").toLowerCase().includes(normalized) || headerText.includes(normalized);
893
926
  });
927
+ const scannedEntries = state.entries.filter((entry) => entryMatchesFilters(entry, {}));
928
+ const toCoverage = (entries, key, truncatedKey) => ({
929
+ full: entries.filter((entry) => typeof entry[key] === "string" && entry[truncatedKey] !== true).length,
930
+ partial: entries.filter((entry) => typeof entry[key] === "string" && entry[truncatedKey] === true).length,
931
+ none: entries.filter((entry) => typeof entry[key] !== "string").length
932
+ });
933
+ return {
934
+ entries: matchedEntries.slice(-Math.max(limit, 1)).reverse().map((entry) => sanitizeEntry(entry)),
935
+ scanned: scannedEntries.length,
936
+ matched: matchedEntries.length,
937
+ bodyCoverage: {
938
+ request: toCoverage(scannedEntries, "requestBodyPreview", "requestBodyTruncated"),
939
+ response: toCoverage(scannedEntries, "responseBodyPreview", "responseBodyTruncated")
940
+ }
941
+ };
894
942
  }
895
943
  function latestNetworkTimestamp(tabId) {
896
944
  const entries = listNetworkEntries(tabId, { limit: MAX_ENTRIES });
@@ -990,10 +1038,16 @@
990
1038
  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);
991
1039
  }
992
1040
  function cloneSessionBindingRecord(state) {
993
- return {
1041
+ const cloned = {
994
1042
  ...state,
995
1043
  tabIds: [...state.tabIds]
996
1044
  };
1045
+ if (cloned.tabIds.length === 0) {
1046
+ cloned.groupId = null;
1047
+ cloned.activeTabId = null;
1048
+ cloned.primaryTabId = null;
1049
+ }
1050
+ return cloned;
997
1051
  }
998
1052
  function normalizeSessionBindingRecordMap(value) {
999
1053
  if (typeof value !== "object" || value === null || Array.isArray(value)) {
@@ -1376,6 +1430,34 @@
1376
1430
  };
1377
1431
  }
1378
1432
  const tabs = await this.readLooseTrackedTabs(remainingTabIds);
1433
+ if (tabs.length === 0) {
1434
+ const liveWindow = binding.windowId !== null ? await this.waitForWindow(binding.windowId, 300) : null;
1435
+ if (!liveWindow) {
1436
+ await this.storage.delete(binding.id);
1437
+ return {
1438
+ binding: null,
1439
+ closedTabId: resolvedTabId
1440
+ };
1441
+ }
1442
+ const nextState2 = {
1443
+ id: binding.id,
1444
+ label: binding.label,
1445
+ color: binding.color,
1446
+ windowId: liveWindow.id,
1447
+ groupId: null,
1448
+ tabIds: [],
1449
+ activeTabId: null,
1450
+ primaryTabId: null
1451
+ };
1452
+ await this.storage.save(nextState2);
1453
+ return {
1454
+ binding: {
1455
+ ...nextState2,
1456
+ tabs: []
1457
+ },
1458
+ closedTabId: resolvedTabId
1459
+ };
1460
+ }
1379
1461
  const nextPrimaryTabId = binding.primaryTabId === resolvedTabId ? tabs[0]?.id ?? null : binding.primaryTabId;
1380
1462
  const nextActiveTabId = binding.activeTabId === resolvedTabId ? tabs.find((candidate) => candidate.active)?.id ?? nextPrimaryTabId ?? tabs[0]?.id ?? null : binding.activeTabId;
1381
1463
  const nextState = {
@@ -2041,38 +2123,82 @@
2041
2123
  async function listSessionBindingStates() {
2042
2124
  return Object.values(await loadSessionBindingStateMap());
2043
2125
  }
2044
- async function summarizeSessionBindings(states) {
2045
- const items = await Promise.all(
2046
- states.map(async (state) => {
2047
- const detached = state.windowId === null || state.tabIds.length === 0;
2048
- const activeTab = typeof state.activeTabId === "number" ? await sessionBindingBrowser.getTab(state.activeTabId) : null;
2049
- const bindingUpdate = bindingUpdateMetadata.get(state.id);
2050
- return {
2051
- id: state.id,
2052
- label: state.label,
2053
- tabCount: state.tabIds.length,
2054
- activeTabId: state.activeTabId,
2055
- activeTabTitle: activeTab?.title ?? null,
2056
- activeTabUrl: activeTab?.url ?? null,
2057
- windowId: state.windowId,
2058
- groupId: state.groupId,
2059
- detached,
2060
- lastBindingUpdateAt: bindingUpdate?.at ?? null,
2061
- lastBindingUpdateReason: bindingUpdate?.reason ?? null
2062
- };
2126
+ function collectPopupSessionBindingTabIds(state) {
2127
+ return [
2128
+ ...new Set(state.tabIds.concat([state.activeTabId, state.primaryTabId].filter((value) => typeof value === "number")))
2129
+ ];
2130
+ }
2131
+ async function inspectPopupSessionBinding(state) {
2132
+ const trackedTabs = (await Promise.all(
2133
+ collectPopupSessionBindingTabIds(state).map(async (tabId) => {
2134
+ return await sessionBindingBrowser.getTab(tabId);
2063
2135
  })
2064
- );
2136
+ )).filter((tab) => tab !== null);
2137
+ const liveWindow = typeof state.windowId === "number" ? await sessionBindingBrowser.getWindow(state.windowId) : null;
2138
+ const activeTab = trackedTabs.find((tab) => tab.id === state.activeTabId) ?? trackedTabs.find((tab) => tab.active) ?? trackedTabs[0] ?? null;
2139
+ const status = trackedTabs.length > 0 ? "attached" : liveWindow ? "window-only" : "detached";
2140
+ const bindingUpdate = bindingUpdateMetadata.get(state.id);
2141
+ return {
2142
+ summary: {
2143
+ id: state.id,
2144
+ label: state.label,
2145
+ tabCount: trackedTabs.length,
2146
+ activeTabId: activeTab?.id ?? null,
2147
+ activeTabTitle: activeTab?.title ?? null,
2148
+ activeTabUrl: activeTab?.url ?? null,
2149
+ windowId: activeTab?.windowId ?? trackedTabs[0]?.windowId ?? liveWindow?.id ?? state.windowId,
2150
+ groupId: trackedTabs.length > 0 ? activeTab?.groupId ?? trackedTabs.find((tab) => tab.groupId !== null)?.groupId ?? state.groupId : null,
2151
+ status,
2152
+ lastBindingUpdateAt: bindingUpdate?.at ?? null,
2153
+ lastBindingUpdateReason: bindingUpdate?.reason ?? null
2154
+ },
2155
+ prune: status === "detached"
2156
+ };
2157
+ }
2158
+ async function summarizeSessionBindings() {
2159
+ const statusRank = {
2160
+ attached: 0,
2161
+ "window-only": 1,
2162
+ detached: 2
2163
+ };
2164
+ const items = await mutateSessionBindingStateMap(async (stateMap) => {
2165
+ const inspected = await Promise.all(
2166
+ Object.entries(stateMap).map(async ([bindingId, state]) => {
2167
+ return {
2168
+ bindingId,
2169
+ inspected: await inspectPopupSessionBinding(state)
2170
+ };
2171
+ })
2172
+ );
2173
+ for (const entry of inspected) {
2174
+ if (entry.inspected.prune) {
2175
+ delete stateMap[entry.bindingId];
2176
+ }
2177
+ }
2178
+ return inspected.filter((entry) => !entry.inspected.prune).map((entry) => entry.inspected.summary).sort((left, right) => {
2179
+ const byStatus = statusRank[left.status] - statusRank[right.status];
2180
+ if (byStatus !== 0) {
2181
+ return byStatus;
2182
+ }
2183
+ const byUpdate = (right.lastBindingUpdateAt ?? 0) - (left.lastBindingUpdateAt ?? 0);
2184
+ if (byUpdate !== 0) {
2185
+ return byUpdate;
2186
+ }
2187
+ return left.label.localeCompare(right.label);
2188
+ });
2189
+ });
2065
2190
  return {
2066
2191
  count: items.length,
2067
- attachedCount: items.filter((item) => !item.detached).length,
2068
- detachedCount: items.filter((item) => item.detached).length,
2192
+ attachedCount: items.filter((item) => item.status === "attached").length,
2193
+ windowOnlyCount: items.filter((item) => item.status === "window-only").length,
2194
+ detachedCount: items.filter((item) => item.status === "detached").length,
2069
2195
  tabCount: items.reduce((sum, item) => sum + item.tabCount, 0),
2070
2196
  items
2071
2197
  };
2072
2198
  }
2073
2199
  async function buildPopupState() {
2074
2200
  const config = await getConfig();
2075
- const sessionBindings = await summarizeSessionBindings(await listSessionBindingStates());
2201
+ const sessionBindings = await summarizeSessionBindings();
2076
2202
  const reconnectRemainingMs = nextReconnectAt === null ? null : Math.max(0, nextReconnectAt - Date.now());
2077
2203
  let connectionState;
2078
2204
  if (!config.token) {
@@ -2429,7 +2555,18 @@
2429
2555
  await new Promise((resolve) => setTimeout(resolve, 80));
2430
2556
  }
2431
2557
  try {
2432
- return await chrome.tabs.captureVisibleTab(tab.windowId, { format: "png" });
2558
+ return {
2559
+ captureStatus: "complete",
2560
+ imageData: await chrome.tabs.captureVisibleTab(tab.windowId, { format: "png" })
2561
+ };
2562
+ } catch (error) {
2563
+ return {
2564
+ captureStatus: "degraded",
2565
+ captureError: {
2566
+ code: "E_CAPTURE_FAILED",
2567
+ message: error instanceof Error ? error.message : String(error)
2568
+ }
2569
+ };
2433
2570
  } finally {
2434
2571
  if (shouldSwitch && typeof activeTab?.id === "number") {
2435
2572
  try {
@@ -2560,7 +2697,9 @@
2560
2697
  contentType: typeof params.contentType === "string" ? params.contentType : void 0,
2561
2698
  mode: params.mode === "json" ? "json" : "raw",
2562
2699
  maxBytes: typeof params.maxBytes === "number" ? params.maxBytes : void 0,
2563
- timeoutMs: typeof params.timeoutMs === "number" ? params.timeoutMs : void 0
2700
+ timeoutMs: typeof params.timeoutMs === "number" ? params.timeoutMs : void 0,
2701
+ fullResponse: params.fullResponse === true,
2702
+ auth: params.auth === "manual" || params.auth === "off" ? params.auth : "auto"
2564
2703
  }
2565
2704
  ],
2566
2705
  func: async (payload) => {
@@ -2683,6 +2822,69 @@
2683
2822
  }
2684
2823
  return { resolver: "lexical", value: readLexical() };
2685
2824
  };
2825
+ const findHeaderName = (headers, name) => Object.keys(headers).find((key) => key.toLowerCase() === name.toLowerCase());
2826
+ const findCookieValue = (cookieString, name) => {
2827
+ const targetName = `${name}=`;
2828
+ for (const segment of cookieString.split(";")) {
2829
+ const trimmed = segment.trim();
2830
+ if (trimmed.toLowerCase().startsWith(targetName.toLowerCase())) {
2831
+ return trimmed.slice(targetName.length);
2832
+ }
2833
+ }
2834
+ return void 0;
2835
+ };
2836
+ const buildJsonSummary = (value) => {
2837
+ const rowsCandidate = (() => {
2838
+ if (Array.isArray(value)) {
2839
+ return value;
2840
+ }
2841
+ if (typeof value !== "object" || value === null) {
2842
+ return null;
2843
+ }
2844
+ const record = value;
2845
+ for (const key of ["data", "rows", "results", "items"]) {
2846
+ if (Array.isArray(record[key])) {
2847
+ return record[key];
2848
+ }
2849
+ }
2850
+ return null;
2851
+ })();
2852
+ if (Array.isArray(rowsCandidate) && rowsCandidate.length > 0) {
2853
+ const objectRows = rowsCandidate.filter((row) => typeof row === "object" && row !== null && !Array.isArray(row)).slice(0, 25);
2854
+ if (objectRows.length > 0) {
2855
+ const columns = [...new Set(objectRows.flatMap((row) => Object.keys(row)))].slice(0, 20);
2856
+ return {
2857
+ schema: {
2858
+ columns: columns.map((label, index) => ({
2859
+ key: `col_${index + 1}`,
2860
+ label
2861
+ }))
2862
+ },
2863
+ mappedRows: objectRows.map((row) => {
2864
+ const mapped = {};
2865
+ for (const column of columns) {
2866
+ mapped[column] = row[column];
2867
+ }
2868
+ return mapped;
2869
+ })
2870
+ };
2871
+ }
2872
+ }
2873
+ if (typeof value === "object" && value !== null && !Array.isArray(value)) {
2874
+ const columns = Object.keys(value).slice(0, 20);
2875
+ if (columns.length > 0) {
2876
+ return {
2877
+ schema: {
2878
+ columns: columns.map((label, index) => ({
2879
+ key: `col_${index + 1}`,
2880
+ label
2881
+ }))
2882
+ }
2883
+ };
2884
+ }
2885
+ }
2886
+ return {};
2887
+ };
2686
2888
  try {
2687
2889
  const targetWindow = payload.scope === "main" ? window : payload.scope === "current" ? resolveFrameWindow(payload.framePath ?? []) : window;
2688
2890
  if (payload.action === "eval") {
@@ -2703,9 +2905,48 @@
2703
2905
  }
2704
2906
  if (payload.action === "fetch") {
2705
2907
  const headers = { ...payload.headers ?? {} };
2706
- if (payload.contentType && !headers["Content-Type"]) {
2908
+ if (payload.contentType && !findHeaderName(headers, "Content-Type")) {
2707
2909
  headers["Content-Type"] = payload.contentType;
2708
2910
  }
2911
+ const fullResponse = payload.fullResponse === true;
2912
+ const authApplied = [];
2913
+ const authSources = /* @__PURE__ */ new Set();
2914
+ const requestUrl = new URL(payload.url, targetWindow.location.href);
2915
+ const sameOrigin = requestUrl.origin === targetWindow.location.origin;
2916
+ const authMode = payload.auth === "manual" || payload.auth === "off" ? payload.auth : "auto";
2917
+ const maybeApplyHeader = (name, value, source) => {
2918
+ if (!value || findHeaderName(headers, name)) {
2919
+ return;
2920
+ }
2921
+ headers[name] = value;
2922
+ authApplied.push(name);
2923
+ authSources.add(source);
2924
+ };
2925
+ if (sameOrigin && authMode === "auto") {
2926
+ const xsrfCookie = findCookieValue(targetWindow.document.cookie ?? "", "XSRF-TOKEN");
2927
+ if (xsrfCookie) {
2928
+ maybeApplyHeader("X-XSRF-TOKEN", decodeURIComponent(xsrfCookie), "cookie:XSRF-TOKEN");
2929
+ }
2930
+ const metaTokens = [
2931
+ {
2932
+ selector: 'meta[name="xsrf-token"], meta[name="x-xsrf-token"]',
2933
+ header: "X-XSRF-TOKEN",
2934
+ source: "meta:xsrf-token"
2935
+ },
2936
+ {
2937
+ selector: 'meta[name="csrf-token"], meta[name="csrf_token"], meta[name="_csrf"]',
2938
+ header: "X-CSRF-TOKEN",
2939
+ source: "meta:csrf-token"
2940
+ }
2941
+ ];
2942
+ for (const token of metaTokens) {
2943
+ const meta = targetWindow.document.querySelector(token.selector);
2944
+ const content = meta?.content?.trim();
2945
+ if (content) {
2946
+ maybeApplyHeader(token.header, content, token.source);
2947
+ }
2948
+ }
2949
+ }
2709
2950
  const controller = typeof AbortController === "function" ? new AbortController() : null;
2710
2951
  const timeoutId = controller && typeof payload.timeoutMs === "number" && payload.timeoutMs > 0 ? window.setTimeout(() => controller.abort(), payload.timeoutMs) : null;
2711
2952
  let response;
@@ -2732,32 +2973,43 @@
2732
2973
  value: (() => {
2733
2974
  const encoder = typeof TextEncoder === "function" ? new TextEncoder() : null;
2734
2975
  const decoder = typeof TextDecoder === "function" ? new TextDecoder() : null;
2735
- const previewLimit = typeof payload.maxBytes === "number" && payload.maxBytes > 0 ? payload.maxBytes : 8192;
2976
+ const previewLimit = !fullResponse && typeof payload.maxBytes === "number" && payload.maxBytes > 0 ? payload.maxBytes : 8192;
2736
2977
  const encodedBody = encoder ? encoder.encode(bodyText) : null;
2737
2978
  const bodyBytes = encodedBody ? encodedBody.byteLength : bodyText.length;
2738
- const truncated = bodyBytes > previewLimit;
2739
- if (payload.mode === "json" && truncated) {
2740
- throw {
2741
- code: "E_BODY_TOO_LARGE",
2742
- message: "JSON response exceeds max-bytes",
2743
- details: {
2744
- bytes: bodyBytes,
2745
- maxBytes: previewLimit
2746
- }
2747
- };
2748
- }
2749
- const previewText = encodedBody && decoder ? decoder.decode(encodedBody.subarray(0, Math.min(encodedBody.byteLength, previewLimit))) : truncated ? bodyText.slice(0, previewLimit) : bodyText;
2750
- return {
2979
+ const truncated = !fullResponse && bodyBytes > previewLimit;
2980
+ const previewText = fullResponse ? bodyText : encodedBody && decoder ? decoder.decode(encodedBody.subarray(0, Math.min(encodedBody.byteLength, previewLimit))) : truncated ? bodyText.slice(0, previewLimit) : bodyText;
2981
+ const result = {
2751
2982
  url: response.url,
2752
2983
  status: response.status,
2753
2984
  ok: response.ok,
2754
2985
  headers: headerMap,
2755
2986
  contentType: response.headers.get("content-type") ?? void 0,
2756
- bodyText: payload.mode === "json" ? void 0 : previewText,
2757
- json: payload.mode === "json" && bodyText ? JSON.parse(bodyText) : void 0,
2758
2987
  bytes: bodyBytes,
2759
- truncated
2988
+ truncated,
2989
+ authApplied: authApplied.length > 0 ? authApplied : void 0,
2990
+ authSources: authSources.size > 0 ? [...authSources] : void 0
2760
2991
  };
2992
+ if (payload.mode === "json") {
2993
+ const parsedJson = bodyText ? JSON.parse(bodyText) : void 0;
2994
+ const summary = buildJsonSummary(parsedJson);
2995
+ if (fullResponse || !truncated) {
2996
+ result.json = parsedJson;
2997
+ } else {
2998
+ result.degradedReason = "response body exceeded max-bytes and was summarized";
2999
+ }
3000
+ if (summary.schema) {
3001
+ result.schema = summary.schema;
3002
+ }
3003
+ if (summary.mappedRows) {
3004
+ result.mappedRows = summary.mappedRows;
3005
+ }
3006
+ } else {
3007
+ result.bodyText = previewText;
3008
+ if (truncated) {
3009
+ result.degradedReason = "response body exceeded max-bytes and was truncated";
3010
+ }
3011
+ }
3012
+ return result;
2761
3013
  })()
2762
3014
  };
2763
3015
  }
@@ -2830,19 +3082,16 @@
2830
3082
  }
2831
3083
  return clone;
2832
3084
  }
2833
- function replayHeadersFromEntry(entry) {
2834
- if (!entry.requestHeaders) {
3085
+ function replayHeadersFromRequestHeaders(requestHeaders) {
3086
+ if (!requestHeaders) {
2835
3087
  return void 0;
2836
3088
  }
2837
3089
  const headers = {};
2838
- for (const [name, value] of Object.entries(entry.requestHeaders)) {
3090
+ for (const [name, value] of Object.entries(requestHeaders)) {
2839
3091
  const normalizedName = name.toLowerCase();
2840
3092
  if (REPLAY_FORBIDDEN_HEADER_NAMES.has(normalizedName) || normalizedName.startsWith("sec-")) {
2841
3093
  continue;
2842
3094
  }
2843
- if (containsRedactionMarker(value)) {
2844
- continue;
2845
- }
2846
3095
  headers[name] = value;
2847
3096
  }
2848
3097
  return Object.keys(headers).length > 0 ? headers : void 0;
@@ -2973,6 +3222,64 @@
2973
3222
  }
2974
3223
  return "unknown";
2975
3224
  }
3225
+ function freshnessCategoryPriority(category) {
3226
+ switch (category) {
3227
+ case "data":
3228
+ return 0;
3229
+ case "unknown":
3230
+ return 1;
3231
+ case "event":
3232
+ return 2;
3233
+ case "contract":
3234
+ return 3;
3235
+ default:
3236
+ return 4;
3237
+ }
3238
+ }
3239
+ function freshnessSourcePriority(source) {
3240
+ switch (source) {
3241
+ case "network":
3242
+ return 0;
3243
+ case "page-data":
3244
+ return 1;
3245
+ case "visible":
3246
+ return 2;
3247
+ case "inline":
3248
+ return 3;
3249
+ default:
3250
+ return 4;
3251
+ }
3252
+ }
3253
+ function rankFreshnessEvidence(candidates, now = Date.now()) {
3254
+ return candidates.slice().sort((left, right) => {
3255
+ const byCategory = freshnessCategoryPriority(left.category) - freshnessCategoryPriority(right.category);
3256
+ if (byCategory !== 0) {
3257
+ return byCategory;
3258
+ }
3259
+ const bySource = freshnessSourcePriority(left.source) - freshnessSourcePriority(right.source);
3260
+ if (bySource !== 0) {
3261
+ return bySource;
3262
+ }
3263
+ const leftTimestamp = parseTimestampCandidate(left.value, now) ?? Number.NEGATIVE_INFINITY;
3264
+ const rightTimestamp = parseTimestampCandidate(right.value, now) ?? Number.NEGATIVE_INFINITY;
3265
+ if (leftTimestamp !== rightTimestamp) {
3266
+ return rightTimestamp - leftTimestamp;
3267
+ }
3268
+ return left.value.localeCompare(right.value);
3269
+ });
3270
+ }
3271
+ function deriveFreshnessConfidence(primary) {
3272
+ if (!primary) {
3273
+ return "low";
3274
+ }
3275
+ if (primary.category === "data" && (primary.source === "network" || primary.source === "page-data")) {
3276
+ return "high";
3277
+ }
3278
+ if (primary.category === "data") {
3279
+ return "medium";
3280
+ }
3281
+ return "low";
3282
+ }
2976
3283
  async function collectPageInspection(tabId, params = {}) {
2977
3284
  return await forwardContentRpc(tabId, "bak.internal.inspectState", params);
2978
3285
  }
@@ -3094,7 +3401,9 @@
3094
3401
  const domVisibleTimestamp = latestTimestampFromCandidates(visibleCandidates, now);
3095
3402
  const latestNetworkTs = latestNetworkTimestamp(tabId);
3096
3403
  const lastMutationAt = typeof inspection.lastMutationAt === "number" ? inspection.lastMutationAt : null;
3097
- const allCandidates = [...visibleCandidates, ...inlineCandidates, ...pageDataCandidates, ...networkCandidates];
3404
+ const allCandidates = rankFreshnessEvidence([...visibleCandidates, ...inlineCandidates, ...pageDataCandidates, ...networkCandidates], now);
3405
+ const primaryEvidence = allCandidates.find((candidate) => parseTimestampCandidate(candidate.value, now) !== null) ?? null;
3406
+ const primaryTimestamp = primaryEvidence ? parseTimestampCandidate(primaryEvidence.value, now) : null;
3098
3407
  return {
3099
3408
  pageLoadedAt: typeof inspection.pageLoadedAt === "number" ? inspection.pageLoadedAt : null,
3100
3409
  lastMutationAt,
@@ -3103,6 +3412,11 @@
3103
3412
  latestPageDataTimestamp,
3104
3413
  latestNetworkDataTimestamp,
3105
3414
  domVisibleTimestamp,
3415
+ primaryTimestamp,
3416
+ primaryCategory: primaryEvidence?.category ?? null,
3417
+ primarySource: primaryEvidence?.source ?? null,
3418
+ confidence: deriveFreshnessConfidence(primaryEvidence),
3419
+ suppressedEvidenceCount: Math.max(0, allCandidates.length - (primaryEvidence ? 1 : 0)),
3106
3420
  assessment: computeFreshnessAssessment({
3107
3421
  latestInlineDataTimestamp,
3108
3422
  latestPageDataTimestamp,
@@ -3535,9 +3849,11 @@
3535
3849
  type: "bak.collectElements",
3536
3850
  debugRichText: config.debugRichText
3537
3851
  });
3538
- const imageData = await captureAlignedTabScreenshot(tab);
3852
+ const screenshot = params.capture === false ? { captureStatus: "skipped" } : await captureAlignedTabScreenshot(tab);
3539
3853
  return {
3540
- imageBase64: includeBase64 ? imageData.replace(/^data:image\/png;base64,/, "") : "",
3854
+ captureStatus: screenshot.captureStatus,
3855
+ captureError: screenshot.captureError,
3856
+ imageBase64: includeBase64 && typeof screenshot.imageData === "string" ? screenshot.imageData.replace(/^data:image\/png;base64,/, "") : void 0,
3541
3857
  elements: elements.elements,
3542
3858
  tabId: tab.id,
3543
3859
  url: tab.url ?? ""
@@ -3661,13 +3977,7 @@
3661
3977
  return await preserveHumanFocus(typeof target.tabId !== "number", async () => {
3662
3978
  const tab = await withTab(target);
3663
3979
  await ensureTabNetworkCapture(tab.id);
3664
- return {
3665
- entries: searchNetworkEntries(
3666
- tab.id,
3667
- String(params.pattern ?? ""),
3668
- typeof params.limit === "number" ? params.limit : 50
3669
- )
3670
- };
3980
+ return searchNetworkEntries(tab.id, String(params.pattern ?? ""), typeof params.limit === "number" ? params.limit : 50);
3671
3981
  });
3672
3982
  }
3673
3983
  case "network.waitFor": {
@@ -3701,34 +4011,32 @@
3701
4011
  return await preserveHumanFocus(typeof target.tabId !== "number", async () => {
3702
4012
  const tab = await withTab(target);
3703
4013
  await ensureTabNetworkCapture(tab.id);
3704
- const entry = getNetworkEntry(tab.id, String(params.id ?? ""));
3705
- if (!entry) {
4014
+ const replayable = getReplayableNetworkRequest(tab.id, String(params.id ?? ""));
4015
+ if (!replayable) {
3706
4016
  throw toError("E_NOT_FOUND", `network entry not found: ${String(params.id ?? "")}`);
3707
4017
  }
3708
- if (entry.requestBodyTruncated === true) {
3709
- throw toError("E_BODY_TOO_LARGE", "captured request body was truncated and cannot be replayed safely", {
3710
- requestId: entry.id,
3711
- requestBytes: entry.requestBytes
3712
- });
3713
- }
3714
- if (containsRedactionMarker(entry.requestBodyPreview)) {
3715
- throw toError("E_EXECUTION", "captured request body was redacted and cannot be replayed safely", {
3716
- requestId: entry.id
3717
- });
4018
+ if (replayable.degradedReason) {
4019
+ return {
4020
+ url: replayable.entry.url,
4021
+ status: 0,
4022
+ ok: false,
4023
+ headers: {},
4024
+ bytes: replayable.entry.requestBytes,
4025
+ truncated: true,
4026
+ degradedReason: replayable.degradedReason
4027
+ };
3718
4028
  }
3719
4029
  const replayed = await executePageWorld(tab.id, "fetch", {
3720
- url: entry.url,
3721
- method: entry.method,
3722
- headers: replayHeadersFromEntry(entry),
3723
- body: entry.requestBodyPreview,
3724
- contentType: (() => {
3725
- const requestHeaders = entry.requestHeaders ?? {};
3726
- const contentTypeHeader = Object.keys(requestHeaders).find((name) => name.toLowerCase() === "content-type");
3727
- return contentTypeHeader ? requestHeaders[contentTypeHeader] : void 0;
3728
- })(),
4030
+ url: replayable.entry.url,
4031
+ method: replayable.entry.method,
4032
+ headers: replayHeadersFromRequestHeaders(replayable.headers),
4033
+ body: replayable.body,
4034
+ contentType: replayable.contentType,
3729
4035
  mode: params.mode,
3730
4036
  timeoutMs: params.timeoutMs,
3731
4037
  maxBytes: params.maxBytes,
4038
+ fullResponse: params.fullResponse === true,
4039
+ auth: params.auth,
3732
4040
  scope: "current"
3733
4041
  });
3734
4042
  const frameResult = replayed.result ?? replayed.results?.find((candidate) => candidate.value || candidate.error);
@@ -3737,7 +4045,13 @@
3737
4045
  }
3738
4046
  const first = frameResult?.value;
3739
4047
  if (!first) {
3740
- throw toError("E_EXECUTION", "network replay returned no response payload");
4048
+ return {
4049
+ url: replayable.entry.url,
4050
+ status: 0,
4051
+ ok: false,
4052
+ headers: {},
4053
+ degradedReason: "network replay returned no response payload"
4054
+ };
3741
4055
  }
3742
4056
  return params.withSchema === "auto" && params.mode === "json" ? await enrichReplayWithSchema(tab.id, String(params.id ?? ""), first) : first;
3743
4057
  });