@flrande/bak-extension 0.6.15 → 0.6.17

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",
@@ -360,8 +360,8 @@
360
360
  if (source.source.type === "networkResponse") {
361
361
  const requestId = source.source.sourceId.replace(/^networkResponse:/, "");
362
362
  pushRecommendation({
363
- title: `Replay ${requestId} with table schema`,
364
- command: `bak network replay --request-id ${requestId} --mode json --with-schema auto`,
363
+ title: `Clone ${requestId} into a reusable fetch template`,
364
+ command: `bak network clone ${requestId}`,
365
365
  note: `Recent response mapped to ${mapping.tableId} with ${mapping.confidence} confidence.`
366
366
  });
367
367
  continue;
@@ -381,6 +381,96 @@
381
381
  }
382
382
  return recommendations.slice(0, 6);
383
383
  }
384
+ function confidenceRank(confidence) {
385
+ switch (confidence) {
386
+ case "high":
387
+ return 0;
388
+ case "medium":
389
+ return 1;
390
+ case "low":
391
+ return 2;
392
+ default:
393
+ return 3;
394
+ }
395
+ }
396
+ function isSameOrigin(pageUrl, requestUrl) {
397
+ if (!pageUrl) {
398
+ return false;
399
+ }
400
+ try {
401
+ const page = new URL(pageUrl);
402
+ const request = new URL(requestUrl, page);
403
+ return page.origin === request.origin;
404
+ } catch {
405
+ return false;
406
+ }
407
+ }
408
+ function selectPrimaryEndpoint(recentNetwork, mappings, pageUrl) {
409
+ const mapped = mappings.filter((mapping) => mapping.sourceId.startsWith("networkResponse:")).map((mapping) => ({
410
+ mapping,
411
+ entry: recentNetwork.find((entry) => entry.id === mapping.sourceId.replace(/^networkResponse:/, ""))
412
+ })).filter((candidate) => candidate.entry !== void 0).sort((left, right) => {
413
+ return confidenceRank(left.mapping.confidence) - confidenceRank(right.mapping.confidence) || right.entry.ts - left.entry.ts || left.entry.id.localeCompare(right.entry.id);
414
+ })[0];
415
+ if (mapped) {
416
+ return {
417
+ requestId: mapped.entry.id,
418
+ url: mapped.entry.url,
419
+ method: mapped.entry.method,
420
+ status: mapped.entry.status,
421
+ kind: mapped.entry.kind,
422
+ resourceType: mapped.entry.resourceType,
423
+ contentType: mapped.entry.contentType,
424
+ sameOrigin: isSameOrigin(pageUrl, mapped.entry.url),
425
+ matchedTableId: mapped.mapping.tableId,
426
+ matchedSourceId: mapped.mapping.sourceId,
427
+ reason: `Mapped to ${mapped.mapping.tableId} with ${mapped.mapping.confidence} confidence`
428
+ };
429
+ }
430
+ const fallback = recentNetwork.filter((entry) => (entry.kind === "fetch" || entry.kind === "xhr") && entry.status >= 200 && entry.status < 400).sort((left, right) => right.ts - left.ts)[0];
431
+ if (!fallback) {
432
+ return null;
433
+ }
434
+ return {
435
+ requestId: fallback.id,
436
+ url: fallback.url,
437
+ method: fallback.method,
438
+ status: fallback.status,
439
+ kind: fallback.kind,
440
+ resourceType: fallback.resourceType,
441
+ contentType: fallback.contentType,
442
+ sameOrigin: isSameOrigin(pageUrl, fallback.url),
443
+ reason: `Latest successful ${fallback.kind.toUpperCase()} request observed on the page`
444
+ };
445
+ }
446
+ function summarizeAvailableModes(modeGroups) {
447
+ return [...new Set(modeGroups.flatMap((group) => group.options.map((option) => option.label)))];
448
+ }
449
+ function selectCurrentMode(modeGroups) {
450
+ for (const group of modeGroups) {
451
+ const selected = group.options.find((option) => option.selected);
452
+ if (selected) {
453
+ return {
454
+ controlType: group.controlType,
455
+ label: selected.label,
456
+ value: selected.value,
457
+ groupLabel: group.label
458
+ };
459
+ }
460
+ }
461
+ return null;
462
+ }
463
+ function deriveLatestArchiveDate(dateControls) {
464
+ const candidates = dateControls.flatMap((control) => [
465
+ control.value,
466
+ control.min,
467
+ control.max,
468
+ control.dataMaxDate,
469
+ ...Array.isArray(control.options) ? control.options : []
470
+ ]);
471
+ const dated = candidates.filter((value) => typeof value === "string").map((value) => ({ value, parsed: Date.parse(value) })).filter((item) => Number.isFinite(item.parsed)).sort((left, right) => right.parsed - left.parsed);
472
+ return dated[0]?.value ?? null;
473
+ }
384
474
  function buildSourceMappingReport(input) {
385
475
  const now = typeof input.now === "number" ? input.now : Date.now();
386
476
  const windowAnalyses = buildWindowSources(input.windowSources);
@@ -388,14 +478,15 @@
388
478
  const networkAnalyses = buildNetworkAnalyses(input.recentNetwork);
389
479
  const sourceAnalyses = [...windowAnalyses, ...inlineAnalyses, ...networkAnalyses];
390
480
  const sourceMappings = input.tables.flatMap((table) => sourceAnalyses.map((source) => scoreSourceMapping(table, source, now))).filter((mapping) => mapping !== null).sort((left, right) => {
391
- const confidenceRank = { high: 0, medium: 1, low: 2 };
392
- return confidenceRank[left.confidence] - confidenceRank[right.confidence] || left.tableId.localeCompare(right.tableId) || left.sourceId.localeCompare(right.sourceId);
481
+ const confidenceRank2 = { high: 0, medium: 1, low: 2 };
482
+ return confidenceRank2[left.confidence] - confidenceRank2[right.confidence] || left.tableId.localeCompare(right.tableId) || left.sourceId.localeCompare(right.sourceId);
393
483
  });
394
484
  return {
395
485
  dataSources: sourceAnalyses.map((analysis) => analysis.source),
396
486
  sourceMappings,
397
487
  recommendedNextActions: buildRecommendedNextActions(input.tables, sourceMappings, sourceAnalyses),
398
- sourceAnalyses
488
+ sourceAnalyses,
489
+ primaryEndpoint: selectPrimaryEndpoint(input.recentNetwork, sourceMappings, input.pageUrl)
399
490
  };
400
491
  }
401
492
  function mapObjectRowToSchema(row, schema) {
@@ -473,12 +564,17 @@
473
564
  windowSources: input.pageDataCandidates,
474
565
  inlineJsonSources: input.inlineJsonSources,
475
566
  recentNetwork: input.recentNetwork,
567
+ pageUrl: input.pageUrl,
476
568
  now: input.now
477
569
  });
478
570
  return {
479
571
  dataSources: report.dataSources,
480
572
  sourceMappings: report.sourceMappings,
481
- recommendedNextActions: report.recommendedNextActions
573
+ recommendedNextActions: report.recommendedNextActions,
574
+ availableModes: summarizeAvailableModes(input.modeGroups),
575
+ currentMode: selectCurrentMode(input.modeGroups),
576
+ latestArchiveDate: deriveLatestArchiveDate(input.dateControls),
577
+ primaryEndpoint: report.primaryEndpoint
482
578
  };
483
579
  }
484
580
  function buildPageDataProbe(name, resolver, sample) {
@@ -533,9 +629,6 @@
533
629
  function shouldRedactHeader(name) {
534
630
  return SENSITIVE_HEADER_PATTERNS.some((pattern) => pattern.test(name));
535
631
  }
536
- function containsRedactionMarker(raw) {
537
- return typeof raw === "string" && raw.includes("[REDACTED");
538
- }
539
632
  function redactTransportText(raw) {
540
633
  if (!raw) {
541
634
  return "";
@@ -553,10 +646,159 @@
553
646
  return Object.keys(result).length > 0 ? result : void 0;
554
647
  }
555
648
 
649
+ // src/network-tools.ts
650
+ var ROW_CANDIDATE_KEYS2 = ["data", "rows", "results", "items", "records", "entries"];
651
+ var SUMMARY_TEXT_LIMIT = 96;
652
+ function truncateSummaryText(value, limit = SUMMARY_TEXT_LIMIT) {
653
+ const normalized = value.replace(/\s+/g, " ").trim();
654
+ if (normalized.length <= limit) {
655
+ return normalized;
656
+ }
657
+ return `${normalized.slice(0, Math.max(1, limit - 1)).trimEnd()}...`;
658
+ }
659
+ function safeParseUrl(urlText) {
660
+ try {
661
+ return new URL(urlText);
662
+ } catch {
663
+ try {
664
+ return new URL(urlText, "http://127.0.0.1");
665
+ } catch {
666
+ return null;
667
+ }
668
+ }
669
+ }
670
+ function looksLikeFormBody(value) {
671
+ return value.includes("=") && (value.includes("&") || !value.trim().startsWith("{"));
672
+ }
673
+ function summarizeSearchParams(params) {
674
+ const entries = [];
675
+ params.forEach((value, key) => {
676
+ entries.push([key, value]);
677
+ });
678
+ if (entries.length === 0) {
679
+ return void 0;
680
+ }
681
+ const preview = entries.slice(0, 4).map(([key, value]) => `${key}=${truncateSummaryText(value, 32)}`);
682
+ return entries.length > 4 ? `${preview.join(", ")} ...` : preview.join(", ");
683
+ }
684
+ function summarizeJsonValue(value) {
685
+ if (Array.isArray(value)) {
686
+ return `json array(${value.length})`;
687
+ }
688
+ if (!value || typeof value !== "object") {
689
+ return `json ${truncateSummaryText(String(value), 40)}`;
690
+ }
691
+ const record = value;
692
+ for (const key of ROW_CANDIDATE_KEYS2) {
693
+ if (Array.isArray(record[key])) {
694
+ return `json rows(${record[key].length}) via ${key}`;
695
+ }
696
+ }
697
+ const keys = Object.keys(record).slice(0, 5);
698
+ return keys.length > 0 ? `json keys: ${keys.join(", ")}` : "json object";
699
+ }
700
+ function headerValue(headers, name) {
701
+ if (!headers) {
702
+ return void 0;
703
+ }
704
+ const normalizedName = name.toLowerCase();
705
+ for (const [key, value] of Object.entries(headers)) {
706
+ if (key.toLowerCase() === normalizedName) {
707
+ return value;
708
+ }
709
+ }
710
+ return void 0;
711
+ }
712
+ function summarizeNetworkPayload(payload, contentType, truncated = false) {
713
+ if (typeof payload !== "string") {
714
+ return void 0;
715
+ }
716
+ const trimmed = payload.trim();
717
+ if (!trimmed) {
718
+ return void 0;
719
+ }
720
+ const normalizedContentType = contentType?.toLowerCase() ?? "";
721
+ let summary;
722
+ if (normalizedContentType.includes("json") || trimmed.startsWith("{") || trimmed.startsWith("[")) {
723
+ try {
724
+ summary = summarizeJsonValue(JSON.parse(trimmed));
725
+ } catch {
726
+ summary = `json text: ${truncateSummaryText(trimmed)}`;
727
+ }
728
+ } else if (normalizedContentType.includes("x-www-form-urlencoded") || looksLikeFormBody(trimmed)) {
729
+ try {
730
+ const params = new URLSearchParams(trimmed);
731
+ const preview = summarizeSearchParams(params);
732
+ summary = preview ? `form: ${preview}` : "form body";
733
+ } catch {
734
+ summary = `form text: ${truncateSummaryText(trimmed)}`;
735
+ }
736
+ } else {
737
+ summary = `text: ${truncateSummaryText(trimmed)}`;
738
+ }
739
+ return truncated ? `${summary} (truncated)` : summary;
740
+ }
741
+ function buildNetworkEntryDerivedFields(entry) {
742
+ const parsedUrl = safeParseUrl(entry.url);
743
+ const preview = {
744
+ query: parsedUrl ? summarizeSearchParams(parsedUrl.searchParams) : void 0,
745
+ request: summarizeNetworkPayload(
746
+ entry.requestBodyPreview,
747
+ headerValue(entry.requestHeaders, "content-type"),
748
+ entry.requestBodyTruncated === true
749
+ ),
750
+ response: summarizeNetworkPayload(entry.responseBodyPreview, entry.contentType, entry.responseBodyTruncated === true)
751
+ };
752
+ return {
753
+ hostname: parsedUrl?.hostname,
754
+ pathname: parsedUrl?.pathname,
755
+ preview: preview.query || preview.request || preview.response ? preview : void 0
756
+ };
757
+ }
758
+ function clampNetworkListLimit(limit, fallback = 50) {
759
+ return typeof limit === "number" ? Math.max(1, Math.min(500, Math.floor(limit))) : fallback;
760
+ }
761
+ function networkEntryMatchesFilters(entry, filters) {
762
+ const urlIncludes = typeof filters.urlIncludes === "string" ? filters.urlIncludes : "";
763
+ const method = typeof filters.method === "string" ? filters.method.toUpperCase() : "";
764
+ const status = typeof filters.status === "number" ? filters.status : void 0;
765
+ const domain = typeof filters.domain === "string" ? filters.domain.trim().toLowerCase() : "";
766
+ const resourceType = typeof filters.resourceType === "string" ? filters.resourceType.trim().toLowerCase() : "";
767
+ const kind = typeof filters.kind === "string" ? filters.kind : void 0;
768
+ const sinceTs = typeof filters.sinceTs === "number" ? filters.sinceTs : void 0;
769
+ if (typeof sinceTs === "number" && entry.ts < sinceTs) {
770
+ return false;
771
+ }
772
+ if (urlIncludes && !entry.url.includes(urlIncludes)) {
773
+ return false;
774
+ }
775
+ if (method && entry.method.toUpperCase() !== method) {
776
+ return false;
777
+ }
778
+ if (typeof status === "number" && entry.status !== status) {
779
+ return false;
780
+ }
781
+ if (domain) {
782
+ const hostname = (entry.hostname ?? safeParseUrl(entry.url)?.hostname ?? "").toLowerCase();
783
+ if (!hostname || !hostname.includes(domain) && hostname !== domain) {
784
+ return false;
785
+ }
786
+ }
787
+ if (resourceType) {
788
+ if ((entry.resourceType ?? "").toLowerCase() !== resourceType) {
789
+ return false;
790
+ }
791
+ }
792
+ if (kind && entry.kind !== kind) {
793
+ return false;
794
+ }
795
+ return true;
796
+ }
797
+
556
798
  // package.json
557
799
  var package_default = {
558
800
  name: "@flrande/bak-extension",
559
- version: "0.6.15",
801
+ version: "0.6.17",
560
802
  type: "module",
561
803
  scripts: {
562
804
  build: "tsup src/background.ts src/content.ts src/popup.ts --format iife --out-dir dist --clean && node scripts/copy-assets.mjs",
@@ -645,18 +887,6 @@
645
887
  }
646
888
  return Object.keys(result).length > 0 ? result : void 0;
647
889
  }
648
- function headerValue(headers, name) {
649
- if (!headers) {
650
- return void 0;
651
- }
652
- const lower = name.toLowerCase();
653
- for (const [key, value] of Object.entries(headers)) {
654
- if (key.toLowerCase() === lower) {
655
- return value;
656
- }
657
- }
658
- return void 0;
659
- }
660
890
  function isTextualContentType(contentType) {
661
891
  if (!contentType) {
662
892
  return true;
@@ -664,6 +894,18 @@
664
894
  const normalized = contentType.toLowerCase();
665
895
  return normalized.startsWith("text/") || normalized.includes("json") || normalized.includes("javascript") || normalized.includes("xml") || normalized.includes("html") || normalized.includes("urlencoded") || normalized.includes("graphql");
666
896
  }
897
+ function sanitizeEntry(entry) {
898
+ const { rawRequestHeaders, rawRequestBody, rawRequestBodyTruncated, ...publicEntry } = entry;
899
+ void rawRequestHeaders;
900
+ void rawRequestBody;
901
+ void rawRequestBodyTruncated;
902
+ return {
903
+ ...publicEntry,
904
+ requestHeaders: typeof entry.requestHeaders === "object" && entry.requestHeaders !== null ? { ...entry.requestHeaders } : void 0,
905
+ responseHeaders: typeof entry.responseHeaders === "object" && entry.responseHeaders !== null ? { ...entry.responseHeaders } : void 0,
906
+ ...buildNetworkEntryDerivedFields(entry)
907
+ };
908
+ }
667
909
  function pushEntry(state, entry, requestId) {
668
910
  state.entries.push(entry);
669
911
  state.entriesById.set(entry.id, entry);
@@ -737,7 +979,8 @@
737
979
  return;
738
980
  }
739
981
  const request = typeof params.request === "object" && params.request !== null ? params.request : {};
740
- const headers = redactHeaderMap(normalizeHeaders(request.headers));
982
+ const rawHeaders = normalizeHeaders(request.headers);
983
+ const headers = redactHeaderMap(rawHeaders);
741
984
  const truncatedRequest = truncateText(typeof request.postData === "string" ? request.postData : void 0, DEFAULT_BODY_BYTES);
742
985
  const entry = {
743
986
  id: `net_${tabId}_${requestId}`,
@@ -754,6 +997,9 @@
754
997
  requestHeaders: headers,
755
998
  requestBodyPreview: truncatedRequest.text ? redactTransportText(truncatedRequest.text) : void 0,
756
999
  requestBodyTruncated: truncatedRequest.truncated,
1000
+ rawRequestHeaders: rawHeaders,
1001
+ rawRequestBody: typeof request.postData === "string" ? request.postData : void 0,
1002
+ rawRequestBodyTruncated: false,
757
1003
  initiatorUrl: typeof params.initiator === "object" && params.initiator !== null && typeof params.initiator.url === "string" ? String(params.initiator.url) : void 0,
758
1004
  tabId,
759
1005
  source: "debugger"
@@ -839,41 +1085,49 @@
839
1085
  state.requestIdToEntryId.clear();
840
1086
  state.lastTouchedAt = Date.now();
841
1087
  }
842
- function entryMatchesFilters(entry, filters) {
843
- const urlIncludes = typeof filters.urlIncludes === "string" ? filters.urlIncludes : "";
844
- const method = typeof filters.method === "string" ? filters.method.toUpperCase() : "";
845
- const status = typeof filters.status === "number" ? filters.status : void 0;
846
- if (urlIncludes && !entry.url.includes(urlIncludes)) {
847
- return false;
848
- }
849
- if (method && entry.method.toUpperCase() !== method) {
850
- return false;
851
- }
852
- if (typeof status === "number" && entry.status !== status) {
853
- return false;
854
- }
855
- return true;
856
- }
857
1088
  function listNetworkEntries(tabId, filters = {}) {
858
1089
  const state = getState(tabId);
859
- 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 }));
1090
+ const limit = clampNetworkListLimit(filters.limit, 50);
1091
+ const ordered = state.entries.filter((entry) => networkEntryMatchesFilters(entry, filters)).slice(-limit).map((entry) => sanitizeEntry(entry));
1092
+ return filters.tail === true ? ordered : ordered.reverse();
861
1093
  }
862
1094
  function getNetworkEntry(tabId, id) {
863
1095
  const state = getState(tabId);
864
1096
  const entry = state.entriesById.get(id);
865
- return entry ? { ...entry } : null;
1097
+ return entry ? sanitizeEntry(entry) : null;
1098
+ }
1099
+ function getReplayableNetworkRequest(tabId, id) {
1100
+ const state = getState(tabId);
1101
+ const entry = state.entriesById.get(id);
1102
+ if (!entry) {
1103
+ return null;
1104
+ }
1105
+ const publicEntry = sanitizeEntry(entry);
1106
+ if (entry.rawRequestBodyTruncated === true) {
1107
+ return {
1108
+ entry: publicEntry,
1109
+ bodyTruncated: true,
1110
+ degradedReason: "live replay unavailable because the captured request body was truncated in memory"
1111
+ };
1112
+ }
1113
+ return {
1114
+ entry: publicEntry,
1115
+ headers: entry.rawRequestHeaders ? { ...entry.rawRequestHeaders } : void 0,
1116
+ body: entry.rawRequestBody,
1117
+ contentType: headerValue(entry.rawRequestHeaders, "content-type"),
1118
+ bodyTruncated: false
1119
+ };
866
1120
  }
867
1121
  async function waitForNetworkEntry(tabId, filters = {}) {
868
1122
  const timeoutMs = typeof filters.timeoutMs === "number" ? Math.max(1, Math.floor(filters.timeoutMs)) : 5e3;
869
1123
  const deadline = Date.now() + timeoutMs;
870
1124
  const state = getState(tabId);
871
- const seenIds = new Set(state.entries.filter((entry) => entryMatchesFilters(entry, filters)).map((entry) => entry.id));
1125
+ const seenIds = new Set(state.entries.filter((entry) => networkEntryMatchesFilters(entry, filters)).map((entry) => entry.id));
872
1126
  while (Date.now() < deadline) {
873
1127
  const nextState = getState(tabId);
874
- const matched = nextState.entries.find((entry) => !seenIds.has(entry.id) && entryMatchesFilters(entry, filters));
1128
+ const matched = nextState.entries.find((entry) => !seenIds.has(entry.id) && networkEntryMatchesFilters(entry, filters));
875
1129
  if (matched) {
876
- return { ...matched };
1130
+ return sanitizeEntry(matched);
877
1131
  }
878
1132
  await new Promise((resolve) => setTimeout(resolve, 75));
879
1133
  }
@@ -883,14 +1137,30 @@
883
1137
  };
884
1138
  }
885
1139
  function searchNetworkEntries(tabId, pattern, limit = 50) {
1140
+ const state = getState(tabId);
886
1141
  const normalized = pattern.toLowerCase();
887
- return listNetworkEntries(tabId, { limit: Math.max(limit, 1) }).filter((entry) => {
1142
+ const matchedEntries = state.entries.filter((entry) => {
888
1143
  const headerText = JSON.stringify({
889
1144
  requestHeaders: entry.requestHeaders,
890
1145
  responseHeaders: entry.responseHeaders
891
1146
  }).toLowerCase();
892
1147
  return entry.url.toLowerCase().includes(normalized) || (entry.requestBodyPreview ?? "").toLowerCase().includes(normalized) || (entry.responseBodyPreview ?? "").toLowerCase().includes(normalized) || headerText.includes(normalized);
893
1148
  });
1149
+ const scannedEntries = state.entries.filter((entry) => networkEntryMatchesFilters(entry, {}));
1150
+ const toCoverage = (entries, key, truncatedKey) => ({
1151
+ full: entries.filter((entry) => typeof entry[key] === "string" && entry[truncatedKey] !== true).length,
1152
+ partial: entries.filter((entry) => typeof entry[key] === "string" && entry[truncatedKey] === true).length,
1153
+ none: entries.filter((entry) => typeof entry[key] !== "string").length
1154
+ });
1155
+ return {
1156
+ entries: matchedEntries.slice(-Math.max(limit, 1)).reverse().map((entry) => sanitizeEntry(entry)),
1157
+ scanned: scannedEntries.length,
1158
+ matched: matchedEntries.length,
1159
+ bodyCoverage: {
1160
+ request: toCoverage(scannedEntries, "requestBodyPreview", "requestBodyTruncated"),
1161
+ response: toCoverage(scannedEntries, "responseBodyPreview", "responseBodyTruncated")
1162
+ }
1163
+ };
894
1164
  }
895
1165
  function latestNetworkTimestamp(tabId) {
896
1166
  const entries = listNetworkEntries(tabId, { limit: MAX_ENTRIES });
@@ -1931,6 +2201,11 @@
1931
2201
  "referer",
1932
2202
  "set-cookie"
1933
2203
  ]);
2204
+ var CLONE_FORBIDDEN_HEADER_NAMES = /* @__PURE__ */ new Set([
2205
+ ...REPLAY_FORBIDDEN_HEADER_NAMES,
2206
+ "x-csrf-token",
2207
+ "x-xsrf-token"
2208
+ ]);
1934
2209
  var ws = null;
1935
2210
  var reconnectTimer = null;
1936
2211
  var nextReconnectInMs = null;
@@ -2075,6 +2350,12 @@
2075
2350
  async function listSessionBindingStates() {
2076
2351
  return Object.values(await loadSessionBindingStateMap());
2077
2352
  }
2353
+ function quotePowerShellArg(value) {
2354
+ return `'${value.replace(/'/g, "''")}'`;
2355
+ }
2356
+ function renderPowerShellCommand(argv) {
2357
+ return ["bak", ...argv].map((part) => quotePowerShellArg(part)).join(" ");
2358
+ }
2078
2359
  function collectPopupSessionBindingTabIds(state) {
2079
2360
  return [
2080
2361
  ...new Set(state.tabIds.concat([state.activeTabId, state.primaryTabId].filter((value) => typeof value === "number")))
@@ -2507,7 +2788,18 @@
2507
2788
  await new Promise((resolve) => setTimeout(resolve, 80));
2508
2789
  }
2509
2790
  try {
2510
- return await chrome.tabs.captureVisibleTab(tab.windowId, { format: "png" });
2791
+ return {
2792
+ captureStatus: "complete",
2793
+ imageData: await chrome.tabs.captureVisibleTab(tab.windowId, { format: "png" })
2794
+ };
2795
+ } catch (error) {
2796
+ return {
2797
+ captureStatus: "degraded",
2798
+ captureError: {
2799
+ code: "E_CAPTURE_FAILED",
2800
+ message: error instanceof Error ? error.message : String(error)
2801
+ }
2802
+ };
2511
2803
  } finally {
2512
2804
  if (shouldSwitch && typeof activeTab?.id === "number") {
2513
2805
  try {
@@ -2638,7 +2930,9 @@
2638
2930
  contentType: typeof params.contentType === "string" ? params.contentType : void 0,
2639
2931
  mode: params.mode === "json" ? "json" : "raw",
2640
2932
  maxBytes: typeof params.maxBytes === "number" ? params.maxBytes : void 0,
2641
- timeoutMs: typeof params.timeoutMs === "number" ? params.timeoutMs : void 0
2933
+ timeoutMs: typeof params.timeoutMs === "number" ? params.timeoutMs : void 0,
2934
+ fullResponse: params.fullResponse === true,
2935
+ auth: params.auth === "manual" || params.auth === "off" ? params.auth : "auto"
2642
2936
  }
2643
2937
  ],
2644
2938
  func: async (payload) => {
@@ -2761,6 +3055,117 @@
2761
3055
  }
2762
3056
  return { resolver: "lexical", value: readLexical() };
2763
3057
  };
3058
+ const findHeaderName = (headers, name) => Object.keys(headers).find((key) => key.toLowerCase() === name.toLowerCase());
3059
+ const findCookieValue = (cookieString, name) => {
3060
+ const targetName = `${name}=`;
3061
+ for (const segment of cookieString.split(";")) {
3062
+ const trimmed = segment.trim();
3063
+ if (trimmed.toLowerCase().startsWith(targetName.toLowerCase())) {
3064
+ return trimmed.slice(targetName.length);
3065
+ }
3066
+ }
3067
+ return void 0;
3068
+ };
3069
+ const buildJsonSummary = (value) => {
3070
+ const rowsCandidate = (() => {
3071
+ if (Array.isArray(value)) {
3072
+ return value;
3073
+ }
3074
+ if (typeof value !== "object" || value === null) {
3075
+ return null;
3076
+ }
3077
+ const record = value;
3078
+ for (const key of ["data", "rows", "results", "items"]) {
3079
+ if (Array.isArray(record[key])) {
3080
+ return record[key];
3081
+ }
3082
+ }
3083
+ return null;
3084
+ })();
3085
+ if (Array.isArray(rowsCandidate) && rowsCandidate.length > 0) {
3086
+ const objectRows = rowsCandidate.filter((row) => typeof row === "object" && row !== null && !Array.isArray(row)).slice(0, 25);
3087
+ if (objectRows.length > 0) {
3088
+ const columns = [...new Set(objectRows.flatMap((row) => Object.keys(row)))].slice(0, 20);
3089
+ return {
3090
+ schema: {
3091
+ columns: columns.map((label, index) => ({
3092
+ key: `col_${index + 1}`,
3093
+ label
3094
+ }))
3095
+ },
3096
+ mappedRows: objectRows.map((row) => {
3097
+ const mapped = {};
3098
+ for (const column of columns) {
3099
+ mapped[column] = row[column];
3100
+ }
3101
+ return mapped;
3102
+ })
3103
+ };
3104
+ }
3105
+ }
3106
+ if (typeof value === "object" && value !== null && !Array.isArray(value)) {
3107
+ const columns = Object.keys(value).slice(0, 20);
3108
+ if (columns.length > 0) {
3109
+ return {
3110
+ schema: {
3111
+ columns: columns.map((label, index) => ({
3112
+ key: `col_${index + 1}`,
3113
+ label
3114
+ }))
3115
+ }
3116
+ };
3117
+ }
3118
+ }
3119
+ return {};
3120
+ };
3121
+ const utf8ByteLength2 = (value) => {
3122
+ if (typeof TextEncoder === "function") {
3123
+ return new TextEncoder().encode(value).byteLength;
3124
+ }
3125
+ return value.length;
3126
+ };
3127
+ const truncateUtf8Text = (value, limit) => {
3128
+ if (limit <= 0) {
3129
+ return "";
3130
+ }
3131
+ if (typeof TextEncoder !== "function" || typeof TextDecoder !== "function") {
3132
+ return value.slice(0, limit);
3133
+ }
3134
+ const encoded = new TextEncoder().encode(value);
3135
+ if (encoded.byteLength <= limit) {
3136
+ return value;
3137
+ }
3138
+ return new TextDecoder().decode(encoded.subarray(0, limit));
3139
+ };
3140
+ const buildRetryHints = (requestUrl, baseUrl) => {
3141
+ const hints = [];
3142
+ let parsed = null;
3143
+ try {
3144
+ parsed = new URL(requestUrl, baseUrl);
3145
+ } catch {
3146
+ parsed = null;
3147
+ }
3148
+ const keys = (() => {
3149
+ if (!parsed) {
3150
+ return [];
3151
+ }
3152
+ const collected = [];
3153
+ parsed.searchParams.forEach((_value, key) => {
3154
+ collected.push(key.toLowerCase());
3155
+ });
3156
+ return [...new Set(collected)];
3157
+ })();
3158
+ if (keys.some((key) => key.includes("limit"))) {
3159
+ hints.push("reduce the limit parameter and retry");
3160
+ }
3161
+ if (keys.some((key) => /(from|to|start|end|date|time|timestamp)/i.test(key))) {
3162
+ hints.push("narrow the requested time window and retry");
3163
+ }
3164
+ if (keys.some((key) => key.includes("page")) && keys.some((key) => key.includes("limit"))) {
3165
+ hints.push("retry with smaller paginated windows");
3166
+ }
3167
+ return hints;
3168
+ };
2764
3169
  try {
2765
3170
  const targetWindow = payload.scope === "main" ? window : payload.scope === "current" ? resolveFrameWindow(payload.framePath ?? []) : window;
2766
3171
  if (payload.action === "eval") {
@@ -2781,25 +3186,155 @@
2781
3186
  }
2782
3187
  if (payload.action === "fetch") {
2783
3188
  const headers = { ...payload.headers ?? {} };
2784
- if (payload.contentType && !headers["Content-Type"]) {
3189
+ if (payload.contentType && !findHeaderName(headers, "Content-Type")) {
2785
3190
  headers["Content-Type"] = payload.contentType;
2786
3191
  }
3192
+ const fullResponse = payload.fullResponse === true;
3193
+ const authApplied = [];
3194
+ const authSources = /* @__PURE__ */ new Set();
3195
+ const requestUrl = new URL(payload.url, targetWindow.location.href);
3196
+ const sameOrigin = requestUrl.origin === targetWindow.location.origin;
3197
+ const authMode = payload.auth === "manual" || payload.auth === "off" ? payload.auth : "auto";
3198
+ const maybeApplyHeader = (name, value, source) => {
3199
+ if (!value || findHeaderName(headers, name)) {
3200
+ return;
3201
+ }
3202
+ headers[name] = value;
3203
+ authApplied.push(name);
3204
+ authSources.add(source);
3205
+ };
3206
+ if (sameOrigin && authMode === "auto") {
3207
+ const xsrfCookie = findCookieValue(targetWindow.document.cookie ?? "", "XSRF-TOKEN");
3208
+ if (xsrfCookie) {
3209
+ maybeApplyHeader("X-XSRF-TOKEN", decodeURIComponent(xsrfCookie), "cookie:XSRF-TOKEN");
3210
+ }
3211
+ const metaTokens = [
3212
+ {
3213
+ selector: 'meta[name="xsrf-token"], meta[name="x-xsrf-token"]',
3214
+ header: "X-XSRF-TOKEN",
3215
+ source: "meta:xsrf-token"
3216
+ },
3217
+ {
3218
+ selector: 'meta[name="csrf-token"], meta[name="csrf_token"], meta[name="_csrf"]',
3219
+ header: "X-CSRF-TOKEN",
3220
+ source: "meta:csrf-token"
3221
+ }
3222
+ ];
3223
+ for (const token of metaTokens) {
3224
+ const meta = targetWindow.document.querySelector(token.selector);
3225
+ const content = meta?.content?.trim();
3226
+ if (content) {
3227
+ maybeApplyHeader(token.header, content, token.source);
3228
+ }
3229
+ }
3230
+ }
3231
+ const requestHints = buildRetryHints(payload.url, targetWindow.location.href);
3232
+ const diagnosticsBase = {
3233
+ requestSent: false,
3234
+ responseStarted: false,
3235
+ status: void 0,
3236
+ headersReceived: {},
3237
+ bodyBytesRead: 0,
3238
+ partialBodyPreview: "",
3239
+ timing: {
3240
+ startedAt: Date.now()
3241
+ }
3242
+ };
3243
+ const previewLimit = !fullResponse && typeof payload.maxBytes === "number" && payload.maxBytes > 0 ? payload.maxBytes : 8192;
3244
+ let previewBytes = 0;
3245
+ const appendPreviewText = (value) => {
3246
+ if (!value || previewBytes >= previewLimit) {
3247
+ return;
3248
+ }
3249
+ const remaining = previewLimit - previewBytes;
3250
+ const next = truncateUtf8Text(value, remaining);
3251
+ diagnosticsBase.partialBodyPreview += next;
3252
+ previewBytes += utf8ByteLength2(next);
3253
+ };
2787
3254
  const controller = typeof AbortController === "function" ? new AbortController() : null;
2788
3255
  const timeoutId = controller && typeof payload.timeoutMs === "number" && payload.timeoutMs > 0 ? window.setTimeout(() => controller.abort(), payload.timeoutMs) : null;
2789
3256
  let response;
3257
+ let bodyText = "";
2790
3258
  try {
3259
+ diagnosticsBase.requestSent = true;
3260
+ diagnosticsBase.timing.requestSentAt = Date.now();
2791
3261
  response = await targetWindow.fetch(payload.url, {
2792
3262
  method: payload.method || "GET",
2793
3263
  headers,
2794
3264
  body: typeof payload.body === "string" ? payload.body : void 0,
2795
3265
  signal: controller ? controller.signal : void 0
2796
3266
  });
3267
+ diagnosticsBase.responseStarted = true;
3268
+ diagnosticsBase.timing.responseStartedAt = Date.now();
3269
+ diagnosticsBase.status = response.status;
3270
+ response.headers.forEach((value, key) => {
3271
+ diagnosticsBase.headersReceived[key] = value;
3272
+ });
3273
+ const reader = response.body?.getReader?.();
3274
+ if (reader) {
3275
+ const decoder = typeof TextDecoder === "function" ? new TextDecoder() : null;
3276
+ while (true) {
3277
+ const chunk = await reader.read();
3278
+ if (chunk.done) {
3279
+ break;
3280
+ }
3281
+ const value = chunk.value ?? new Uint8Array();
3282
+ diagnosticsBase.bodyBytesRead += value.byteLength;
3283
+ if (decoder) {
3284
+ const decoded = decoder.decode(value, { stream: true });
3285
+ bodyText += decoded;
3286
+ appendPreviewText(decoded);
3287
+ } else {
3288
+ const fallback = String.fromCharCode(...value);
3289
+ bodyText += fallback;
3290
+ appendPreviewText(fallback);
3291
+ }
3292
+ }
3293
+ if (decoder) {
3294
+ const flushed = decoder.decode();
3295
+ if (flushed) {
3296
+ bodyText += flushed;
3297
+ appendPreviewText(flushed);
3298
+ }
3299
+ }
3300
+ } else {
3301
+ bodyText = await response.text();
3302
+ diagnosticsBase.bodyBytesRead = utf8ByteLength2(bodyText);
3303
+ appendPreviewText(bodyText);
3304
+ }
3305
+ diagnosticsBase.timing.completedAt = Date.now();
3306
+ } catch (error) {
3307
+ const abortLike = error instanceof DOMException && error.name === "AbortError" || error instanceof Error && /abort|timeout/i.test(error.message);
3308
+ const where = diagnosticsBase.requestSent !== true ? "dispatch" : diagnosticsBase.responseStarted !== true ? "ttfb" : "body";
3309
+ const diagnostics = {
3310
+ kind: abortLike ? "timeout" : "network",
3311
+ retryable: true,
3312
+ where,
3313
+ timing: {
3314
+ ...diagnosticsBase.timing,
3315
+ ...abortLike ? { timeoutAt: Date.now() } : {}
3316
+ },
3317
+ requestSent: diagnosticsBase.requestSent,
3318
+ responseStarted: diagnosticsBase.responseStarted,
3319
+ status: diagnosticsBase.status,
3320
+ headersReceived: Object.keys(diagnosticsBase.headersReceived).length > 0 ? diagnosticsBase.headersReceived : void 0,
3321
+ bodyBytesRead: diagnosticsBase.bodyBytesRead,
3322
+ partialBodyPreview: diagnosticsBase.partialBodyPreview || void 0,
3323
+ hints: requestHints
3324
+ };
3325
+ throw {
3326
+ code: abortLike ? "E_TIMEOUT" : "E_EXECUTION",
3327
+ message: abortLike ? `page.fetch timeout during ${where}` : error instanceof Error ? error.message : String(error),
3328
+ details: {
3329
+ ...diagnostics,
3330
+ diagnostics
3331
+ }
3332
+ };
2797
3333
  } finally {
2798
3334
  if (timeoutId !== null) {
2799
3335
  window.clearTimeout(timeoutId);
2800
3336
  }
2801
3337
  }
2802
- const bodyText = await response.text();
2803
3338
  const headerMap = {};
2804
3339
  response.headers.forEach((value, key) => {
2805
3340
  headerMap[key] = value;
@@ -2808,34 +3343,88 @@
2808
3343
  url: targetWindow.location.href,
2809
3344
  framePath: payload.scope === "current" ? payload.framePath ?? [] : [],
2810
3345
  value: (() => {
2811
- const encoder = typeof TextEncoder === "function" ? new TextEncoder() : null;
2812
- const decoder = typeof TextDecoder === "function" ? new TextDecoder() : null;
2813
- const previewLimit = typeof payload.maxBytes === "number" && payload.maxBytes > 0 ? payload.maxBytes : 8192;
2814
- const encodedBody = encoder ? encoder.encode(bodyText) : null;
2815
- const bodyBytes = encodedBody ? encodedBody.byteLength : bodyText.length;
2816
- const truncated = bodyBytes > previewLimit;
2817
- if (payload.mode === "json" && truncated) {
2818
- throw {
2819
- code: "E_BODY_TOO_LARGE",
2820
- message: "JSON response exceeds max-bytes",
2821
- details: {
2822
- bytes: bodyBytes,
2823
- maxBytes: previewLimit
2824
- }
2825
- };
2826
- }
2827
- const previewText = encodedBody && decoder ? decoder.decode(encodedBody.subarray(0, Math.min(encodedBody.byteLength, previewLimit))) : truncated ? bodyText.slice(0, previewLimit) : bodyText;
2828
- return {
3346
+ const bodyBytes = diagnosticsBase.bodyBytesRead || utf8ByteLength2(bodyText);
3347
+ const truncated = !fullResponse && bodyBytes > previewLimit;
3348
+ const previewText = fullResponse ? bodyText : truncated ? truncateUtf8Text(bodyText, previewLimit) : bodyText;
3349
+ const result = {
2829
3350
  url: response.url,
2830
3351
  status: response.status,
2831
3352
  ok: response.ok,
2832
3353
  headers: headerMap,
2833
3354
  contentType: response.headers.get("content-type") ?? void 0,
2834
- bodyText: payload.mode === "json" ? void 0 : previewText,
2835
- json: payload.mode === "json" && bodyText ? JSON.parse(bodyText) : void 0,
2836
3355
  bytes: bodyBytes,
2837
- truncated
3356
+ truncated,
3357
+ authApplied: authApplied.length > 0 ? authApplied : void 0,
3358
+ authSources: authSources.size > 0 ? [...authSources] : void 0,
3359
+ diagnostics: {
3360
+ kind: "success",
3361
+ retryable: false,
3362
+ where: "complete",
3363
+ timing: diagnosticsBase.timing,
3364
+ requestSent: diagnosticsBase.requestSent,
3365
+ responseStarted: diagnosticsBase.responseStarted,
3366
+ status: response.status,
3367
+ headersReceived: headerMap,
3368
+ bodyBytesRead: bodyBytes,
3369
+ partialBodyPreview: diagnosticsBase.partialBodyPreview || void 0,
3370
+ hints: requestHints
3371
+ }
2838
3372
  };
3373
+ if (payload.mode === "json") {
3374
+ let parsedJson;
3375
+ try {
3376
+ parsedJson = bodyText ? JSON.parse(bodyText) : void 0;
3377
+ } catch (error) {
3378
+ throw {
3379
+ code: "E_EXECUTION",
3380
+ message: error instanceof Error ? error.message : String(error),
3381
+ details: {
3382
+ kind: "execution",
3383
+ retryable: false,
3384
+ where: "complete",
3385
+ timing: diagnosticsBase.timing,
3386
+ requestSent: diagnosticsBase.requestSent,
3387
+ responseStarted: diagnosticsBase.responseStarted,
3388
+ status: response.status,
3389
+ headersReceived: headerMap,
3390
+ bodyBytesRead: bodyBytes,
3391
+ partialBodyPreview: diagnosticsBase.partialBodyPreview || void 0,
3392
+ hints: requestHints,
3393
+ diagnostics: {
3394
+ kind: "execution",
3395
+ retryable: false,
3396
+ where: "complete",
3397
+ timing: diagnosticsBase.timing,
3398
+ requestSent: diagnosticsBase.requestSent,
3399
+ responseStarted: diagnosticsBase.responseStarted,
3400
+ status: response.status,
3401
+ headersReceived: headerMap,
3402
+ bodyBytesRead: bodyBytes,
3403
+ partialBodyPreview: diagnosticsBase.partialBodyPreview || void 0,
3404
+ hints: requestHints
3405
+ }
3406
+ }
3407
+ };
3408
+ }
3409
+ const summary = buildJsonSummary(parsedJson);
3410
+ if (fullResponse || !truncated) {
3411
+ result.json = parsedJson;
3412
+ } else {
3413
+ result.degradedReason = "response body exceeded max-bytes and was summarized";
3414
+ }
3415
+ if (summary.schema) {
3416
+ result.schema = summary.schema;
3417
+ }
3418
+ if (summary.mappedRows) {
3419
+ result.mappedRows = summary.mappedRows;
3420
+ }
3421
+ } else {
3422
+ result.bodyText = previewText;
3423
+ if (truncated) {
3424
+ result.degradedReason = "response body exceeded max-bytes and was truncated";
3425
+ }
3426
+ }
3427
+ return result;
2839
3428
  })()
2840
3429
  };
2841
3430
  }
@@ -2899,31 +3488,135 @@
2899
3488
  delete clone.requestHeaders;
2900
3489
  delete clone.requestBodyPreview;
2901
3490
  delete clone.requestBodyTruncated;
3491
+ if (clone.preview) {
3492
+ clone.preview = { ...clone.preview };
3493
+ delete clone.preview.request;
3494
+ if (!clone.preview.query && !clone.preview.request && !clone.preview.response) {
3495
+ delete clone.preview;
3496
+ }
3497
+ }
2902
3498
  }
2903
3499
  if (!sections.has("response")) {
2904
3500
  delete clone.responseHeaders;
2905
3501
  delete clone.responseBodyPreview;
2906
3502
  delete clone.responseBodyTruncated;
2907
3503
  delete clone.binary;
3504
+ if (clone.preview) {
3505
+ clone.preview = { ...clone.preview };
3506
+ delete clone.preview.response;
3507
+ if (!clone.preview.query && !clone.preview.request && !clone.preview.response) {
3508
+ delete clone.preview;
3509
+ }
3510
+ }
2908
3511
  }
2909
3512
  return clone;
2910
3513
  }
2911
- function replayHeadersFromEntry(entry) {
2912
- if (!entry.requestHeaders) {
3514
+ function replayHeadersFromRequestHeaders(requestHeaders) {
3515
+ if (!requestHeaders) {
2913
3516
  return void 0;
2914
3517
  }
2915
3518
  const headers = {};
2916
- for (const [name, value] of Object.entries(entry.requestHeaders)) {
3519
+ for (const [name, value] of Object.entries(requestHeaders)) {
2917
3520
  const normalizedName = name.toLowerCase();
2918
3521
  if (REPLAY_FORBIDDEN_HEADER_NAMES.has(normalizedName) || normalizedName.startsWith("sec-")) {
2919
3522
  continue;
2920
3523
  }
2921
- if (containsRedactionMarker(value)) {
3524
+ headers[name] = value;
3525
+ }
3526
+ return Object.keys(headers).length > 0 ? headers : void 0;
3527
+ }
3528
+ function cloneHeadersFromRequestHeaders(requestHeaders) {
3529
+ if (!requestHeaders) {
3530
+ return { omitted: [] };
3531
+ }
3532
+ const headers = {};
3533
+ const omitted = /* @__PURE__ */ new Set();
3534
+ for (const [name, value] of Object.entries(requestHeaders)) {
3535
+ const normalizedName = name.toLowerCase();
3536
+ if (CLONE_FORBIDDEN_HEADER_NAMES.has(normalizedName) || normalizedName.startsWith("sec-")) {
3537
+ omitted.add(normalizedName);
2922
3538
  continue;
2923
3539
  }
2924
3540
  headers[name] = value;
2925
3541
  }
2926
- return Object.keys(headers).length > 0 ? headers : void 0;
3542
+ return {
3543
+ headers: Object.keys(headers).length > 0 ? headers : void 0,
3544
+ omitted: [...omitted].sort()
3545
+ };
3546
+ }
3547
+ function isSameOriginWithTab(tabUrl, requestUrl) {
3548
+ try {
3549
+ if (!tabUrl) {
3550
+ return false;
3551
+ }
3552
+ return new URL(requestUrl).origin === new URL(tabUrl).origin;
3553
+ } catch {
3554
+ return false;
3555
+ }
3556
+ }
3557
+ function buildNetworkCloneResult(requestId, tabUrl, replayable) {
3558
+ const sameOrigin = isSameOriginWithTab(tabUrl, replayable.entry.url);
3559
+ const cloneable = sameOrigin && replayable.bodyTruncated !== true;
3560
+ const notes = [];
3561
+ const sanitizedHeaders = cloneHeadersFromRequestHeaders(replayable.headers);
3562
+ if (sanitizedHeaders.omitted.length > 0) {
3563
+ notes.push(`omitted sensitive headers: ${sanitizedHeaders.omitted.join(", ")}`);
3564
+ }
3565
+ if (!sameOrigin) {
3566
+ notes.push("preferred page.fetch template was skipped because the captured request is not same-origin with the current page");
3567
+ }
3568
+ if (replayable.bodyTruncated) {
3569
+ notes.push("captured request body was truncated, so bak cannot emit a reliable page.fetch clone");
3570
+ }
3571
+ if (!sameOrigin || replayable.bodyTruncated) {
3572
+ notes.push("falling back to network.replay preserves the captured request shape more safely");
3573
+ } else {
3574
+ notes.push("page.fetch clone keeps session cookies and auto-applies same-origin auth helpers");
3575
+ }
3576
+ const pageFetch = sameOrigin && replayable.bodyTruncated !== true ? {
3577
+ url: replayable.entry.url,
3578
+ method: replayable.entry.method,
3579
+ headers: sanitizedHeaders.headers,
3580
+ body: replayable.body,
3581
+ contentType: replayable.contentType,
3582
+ mode: (replayable.entry.contentType ?? replayable.contentType)?.toLowerCase().includes("json") ? "json" : "raw",
3583
+ auth: "auto"
3584
+ } : void 0;
3585
+ const preferredArgv = pageFetch ? [
3586
+ "page",
3587
+ "fetch",
3588
+ "--url",
3589
+ pageFetch.url,
3590
+ "--method",
3591
+ pageFetch.method,
3592
+ "--auth",
3593
+ pageFetch.auth,
3594
+ "--mode",
3595
+ pageFetch.mode ?? "raw",
3596
+ ...pageFetch.contentType ? ["--content-type", pageFetch.contentType] : [],
3597
+ ...Object.entries(pageFetch.headers ?? {}).flatMap(([name, value]) => ["--header", `${name}: ${value}`]),
3598
+ ...typeof pageFetch.body === "string" ? ["--body", pageFetch.body] : []
3599
+ ] : ["network", "replay", "--request-id", requestId, "--auth", "auto"];
3600
+ return {
3601
+ request: {
3602
+ id: replayable.entry.id,
3603
+ url: replayable.entry.url,
3604
+ method: replayable.entry.method,
3605
+ kind: replayable.entry.kind,
3606
+ contentType: replayable.contentType,
3607
+ sameOrigin,
3608
+ bodyPresent: typeof replayable.body === "string" && replayable.body.length > 0,
3609
+ bodyTruncated: replayable.bodyTruncated
3610
+ },
3611
+ cloneable,
3612
+ preferredCommand: {
3613
+ tool: pageFetch ? "page.fetch" : "network.replay",
3614
+ argv: preferredArgv,
3615
+ powershell: renderPowerShellCommand(preferredArgv)
3616
+ },
3617
+ pageFetch,
3618
+ notes
3619
+ };
2927
3620
  }
2928
3621
  function collectTimestampMatchesFromText(text, source, patterns) {
2929
3622
  const regexes = (patterns ?? [
@@ -3051,6 +3744,64 @@
3051
3744
  }
3052
3745
  return "unknown";
3053
3746
  }
3747
+ function freshnessCategoryPriority(category) {
3748
+ switch (category) {
3749
+ case "data":
3750
+ return 0;
3751
+ case "unknown":
3752
+ return 1;
3753
+ case "event":
3754
+ return 2;
3755
+ case "contract":
3756
+ return 3;
3757
+ default:
3758
+ return 4;
3759
+ }
3760
+ }
3761
+ function freshnessSourcePriority(source) {
3762
+ switch (source) {
3763
+ case "network":
3764
+ return 0;
3765
+ case "page-data":
3766
+ return 1;
3767
+ case "visible":
3768
+ return 2;
3769
+ case "inline":
3770
+ return 3;
3771
+ default:
3772
+ return 4;
3773
+ }
3774
+ }
3775
+ function rankFreshnessEvidence(candidates, now = Date.now()) {
3776
+ return candidates.slice().sort((left, right) => {
3777
+ const byCategory = freshnessCategoryPriority(left.category) - freshnessCategoryPriority(right.category);
3778
+ if (byCategory !== 0) {
3779
+ return byCategory;
3780
+ }
3781
+ const bySource = freshnessSourcePriority(left.source) - freshnessSourcePriority(right.source);
3782
+ if (bySource !== 0) {
3783
+ return bySource;
3784
+ }
3785
+ const leftTimestamp = parseTimestampCandidate(left.value, now) ?? Number.NEGATIVE_INFINITY;
3786
+ const rightTimestamp = parseTimestampCandidate(right.value, now) ?? Number.NEGATIVE_INFINITY;
3787
+ if (leftTimestamp !== rightTimestamp) {
3788
+ return rightTimestamp - leftTimestamp;
3789
+ }
3790
+ return left.value.localeCompare(right.value);
3791
+ });
3792
+ }
3793
+ function deriveFreshnessConfidence(primary) {
3794
+ if (!primary) {
3795
+ return "low";
3796
+ }
3797
+ if (primary.category === "data" && (primary.source === "network" || primary.source === "page-data")) {
3798
+ return "high";
3799
+ }
3800
+ if (primary.category === "data") {
3801
+ return "medium";
3802
+ }
3803
+ return "low";
3804
+ }
3054
3805
  async function collectPageInspection(tabId, params = {}) {
3055
3806
  return await forwardContentRpc(tabId, "bak.internal.inspectState", params);
3056
3807
  }
@@ -3172,7 +3923,9 @@
3172
3923
  const domVisibleTimestamp = latestTimestampFromCandidates(visibleCandidates, now);
3173
3924
  const latestNetworkTs = latestNetworkTimestamp(tabId);
3174
3925
  const lastMutationAt = typeof inspection.lastMutationAt === "number" ? inspection.lastMutationAt : null;
3175
- const allCandidates = [...visibleCandidates, ...inlineCandidates, ...pageDataCandidates, ...networkCandidates];
3926
+ const allCandidates = rankFreshnessEvidence([...visibleCandidates, ...inlineCandidates, ...pageDataCandidates, ...networkCandidates], now);
3927
+ const primaryEvidence = allCandidates.find((candidate) => parseTimestampCandidate(candidate.value, now) !== null) ?? null;
3928
+ const primaryTimestamp = primaryEvidence ? parseTimestampCandidate(primaryEvidence.value, now) : null;
3176
3929
  return {
3177
3930
  pageLoadedAt: typeof inspection.pageLoadedAt === "number" ? inspection.pageLoadedAt : null,
3178
3931
  lastMutationAt,
@@ -3181,6 +3934,11 @@
3181
3934
  latestPageDataTimestamp,
3182
3935
  latestNetworkDataTimestamp,
3183
3936
  domVisibleTimestamp,
3937
+ primaryTimestamp,
3938
+ primaryCategory: primaryEvidence?.category ?? null,
3939
+ primarySource: primaryEvidence?.source ?? null,
3940
+ confidence: deriveFreshnessConfidence(primaryEvidence),
3941
+ suppressedEvidenceCount: Math.max(0, allCandidates.length - (primaryEvidence ? 1 : 0)),
3184
3942
  assessment: computeFreshnessAssessment({
3185
3943
  latestInlineDataTimestamp,
3186
3944
  latestPageDataTimestamp,
@@ -3280,6 +4038,7 @@
3280
4038
  const pageDataCandidates = await probePageDataCandidatesForTab(tabId, inspection);
3281
4039
  const recentNetwork = listNetworkEntries(tabId, { limit: 25 });
3282
4040
  const pageDataReport = buildInspectPageDataResult({
4041
+ pageUrl: inspection.url,
3283
4042
  suspiciousGlobals: inspection.suspiciousGlobals ?? [],
3284
4043
  tables: inspection.tables ?? [],
3285
4044
  visibleTimestamps: inspection.visibleTimestamps ?? [],
@@ -3287,7 +4046,9 @@
3287
4046
  pageDataCandidates,
3288
4047
  recentNetwork,
3289
4048
  tableAnalyses: tables,
3290
- inlineJsonSources: Array.isArray(inspection.inlineJsonSources) ? inspection.inlineJsonSources : []
4049
+ inlineJsonSources: Array.isArray(inspection.inlineJsonSources) ? inspection.inlineJsonSources : [],
4050
+ modeGroups: Array.isArray(inspection.modeGroups) ? inspection.modeGroups : [],
4051
+ dateControls: Array.isArray(inspection.dateControls) ? inspection.dateControls : []
3291
4052
  });
3292
4053
  const matched = selectReplaySchemaMatch(response.json, tables, {
3293
4054
  preferredSourceId: `networkResponse:${requestId}`,
@@ -3613,9 +4374,11 @@
3613
4374
  type: "bak.collectElements",
3614
4375
  debugRichText: config.debugRichText
3615
4376
  });
3616
- const imageData = await captureAlignedTabScreenshot(tab);
4377
+ const screenshot = params.capture === false ? { captureStatus: "skipped" } : await captureAlignedTabScreenshot(tab);
3617
4378
  return {
3618
- imageBase64: includeBase64 ? imageData.replace(/^data:image\/png;base64,/, "") : "",
4379
+ captureStatus: screenshot.captureStatus,
4380
+ captureError: screenshot.captureError,
4381
+ imageBase64: includeBase64 && typeof screenshot.imageData === "string" ? screenshot.imageData.replace(/^data:image\/png;base64,/, "") : void 0,
3619
4382
  elements: elements.elements,
3620
4383
  tabId: tab.id,
3621
4384
  url: tab.url ?? ""
@@ -3705,7 +4468,12 @@
3705
4468
  limit: typeof params.limit === "number" ? params.limit : void 0,
3706
4469
  urlIncludes: typeof params.urlIncludes === "string" ? params.urlIncludes : void 0,
3707
4470
  status: typeof params.status === "number" ? params.status : void 0,
3708
- method: typeof params.method === "string" ? params.method : void 0
4471
+ method: typeof params.method === "string" ? params.method : void 0,
4472
+ domain: typeof params.domain === "string" ? params.domain : void 0,
4473
+ resourceType: typeof params.resourceType === "string" ? params.resourceType : void 0,
4474
+ kind: typeof params.kind === "string" ? params.kind : void 0,
4475
+ sinceTs: typeof params.sinceTs === "number" ? params.sinceTs : void 0,
4476
+ tail: params.tail === true
3709
4477
  })
3710
4478
  };
3711
4479
  } catch {
@@ -3739,13 +4507,18 @@
3739
4507
  return await preserveHumanFocus(typeof target.tabId !== "number", async () => {
3740
4508
  const tab = await withTab(target);
3741
4509
  await ensureTabNetworkCapture(tab.id);
3742
- return {
3743
- entries: searchNetworkEntries(
3744
- tab.id,
3745
- String(params.pattern ?? ""),
3746
- typeof params.limit === "number" ? params.limit : 50
3747
- )
3748
- };
4510
+ return searchNetworkEntries(tab.id, String(params.pattern ?? ""), typeof params.limit === "number" ? params.limit : 50);
4511
+ });
4512
+ }
4513
+ case "network.clone": {
4514
+ return await preserveHumanFocus(typeof target.tabId !== "number", async () => {
4515
+ const tab = await withTab(target);
4516
+ await ensureTabNetworkCapture(tab.id);
4517
+ const replayable = getReplayableNetworkRequest(tab.id, String(params.id ?? ""));
4518
+ if (!replayable) {
4519
+ throw toError("E_NOT_FOUND", `network entry not found: ${String(params.id ?? "")}`);
4520
+ }
4521
+ return buildNetworkCloneResult(String(params.id ?? ""), tab.url, replayable);
3749
4522
  });
3750
4523
  }
3751
4524
  case "network.waitFor": {
@@ -3779,34 +4552,32 @@
3779
4552
  return await preserveHumanFocus(typeof target.tabId !== "number", async () => {
3780
4553
  const tab = await withTab(target);
3781
4554
  await ensureTabNetworkCapture(tab.id);
3782
- const entry = getNetworkEntry(tab.id, String(params.id ?? ""));
3783
- if (!entry) {
4555
+ const replayable = getReplayableNetworkRequest(tab.id, String(params.id ?? ""));
4556
+ if (!replayable) {
3784
4557
  throw toError("E_NOT_FOUND", `network entry not found: ${String(params.id ?? "")}`);
3785
4558
  }
3786
- if (entry.requestBodyTruncated === true) {
3787
- throw toError("E_BODY_TOO_LARGE", "captured request body was truncated and cannot be replayed safely", {
3788
- requestId: entry.id,
3789
- requestBytes: entry.requestBytes
3790
- });
3791
- }
3792
- if (containsRedactionMarker(entry.requestBodyPreview)) {
3793
- throw toError("E_EXECUTION", "captured request body was redacted and cannot be replayed safely", {
3794
- requestId: entry.id
3795
- });
4559
+ if (replayable.degradedReason) {
4560
+ return {
4561
+ url: replayable.entry.url,
4562
+ status: 0,
4563
+ ok: false,
4564
+ headers: {},
4565
+ bytes: replayable.entry.requestBytes,
4566
+ truncated: true,
4567
+ degradedReason: replayable.degradedReason
4568
+ };
3796
4569
  }
3797
4570
  const replayed = await executePageWorld(tab.id, "fetch", {
3798
- url: entry.url,
3799
- method: entry.method,
3800
- headers: replayHeadersFromEntry(entry),
3801
- body: entry.requestBodyPreview,
3802
- contentType: (() => {
3803
- const requestHeaders = entry.requestHeaders ?? {};
3804
- const contentTypeHeader = Object.keys(requestHeaders).find((name) => name.toLowerCase() === "content-type");
3805
- return contentTypeHeader ? requestHeaders[contentTypeHeader] : void 0;
3806
- })(),
4571
+ url: replayable.entry.url,
4572
+ method: replayable.entry.method,
4573
+ headers: replayHeadersFromRequestHeaders(replayable.headers),
4574
+ body: replayable.body,
4575
+ contentType: replayable.contentType,
3807
4576
  mode: params.mode,
3808
4577
  timeoutMs: params.timeoutMs,
3809
4578
  maxBytes: params.maxBytes,
4579
+ fullResponse: params.fullResponse === true,
4580
+ auth: params.auth,
3810
4581
  scope: "current"
3811
4582
  });
3812
4583
  const frameResult = replayed.result ?? replayed.results?.find((candidate) => candidate.value || candidate.error);
@@ -3815,7 +4586,13 @@
3815
4586
  }
3816
4587
  const first = frameResult?.value;
3817
4588
  if (!first) {
3818
- throw toError("E_EXECUTION", "network replay returned no response payload");
4589
+ return {
4590
+ url: replayable.entry.url,
4591
+ status: 0,
4592
+ ok: false,
4593
+ headers: {},
4594
+ degradedReason: "network replay returned no response payload"
4595
+ };
3819
4596
  }
3820
4597
  return params.withSchema === "auto" && params.mode === "json" ? await enrichReplayWithSchema(tab.id, String(params.id ?? ""), first) : first;
3821
4598
  });
@@ -3894,9 +4671,10 @@
3894
4671
  await ensureNetworkDebugger(tab.id).catch(() => void 0);
3895
4672
  const inspection = await collectPageInspection(tab.id, params);
3896
4673
  const pageDataCandidates = await probePageDataCandidatesForTab(tab.id, inspection);
3897
- const network = listNetworkEntries(tab.id, { limit: 10 });
4674
+ const network = listNetworkEntries(tab.id, { limit: 25, tail: true });
3898
4675
  const tableAnalyses = await collectTableAnalyses(tab.id);
3899
4676
  const enriched = buildInspectPageDataResult({
4677
+ pageUrl: inspection.url,
3900
4678
  suspiciousGlobals: inspection.suspiciousGlobals ?? [],
3901
4679
  tables: inspection.tables ?? [],
3902
4680
  visibleTimestamps: inspection.visibleTimestamps ?? [],
@@ -3904,7 +4682,9 @@
3904
4682
  pageDataCandidates,
3905
4683
  recentNetwork: network,
3906
4684
  tableAnalyses,
3907
- inlineJsonSources: Array.isArray(inspection.inlineJsonSources) ? inspection.inlineJsonSources : []
4685
+ inlineJsonSources: Array.isArray(inspection.inlineJsonSources) ? inspection.inlineJsonSources : [],
4686
+ modeGroups: Array.isArray(inspection.modeGroups) ? inspection.modeGroups : [],
4687
+ dateControls: Array.isArray(inspection.dateControls) ? inspection.dateControls : []
3908
4688
  });
3909
4689
  const recommendedNextSteps = enriched.recommendedNextActions.map((action) => action.command);
3910
4690
  return {
@@ -3914,6 +4694,12 @@
3914
4694
  inlineTimestamps: inspection.inlineTimestamps ?? [],
3915
4695
  pageDataCandidates,
3916
4696
  recentNetwork: network,
4697
+ modeGroups: Array.isArray(inspection.modeGroups) ? inspection.modeGroups : [],
4698
+ availableModes: enriched.availableModes,
4699
+ currentMode: enriched.currentMode,
4700
+ dateControls: Array.isArray(inspection.dateControls) ? inspection.dateControls : [],
4701
+ latestArchiveDate: enriched.latestArchiveDate,
4702
+ primaryEndpoint: enriched.primaryEndpoint,
3917
4703
  dataSources: enriched.dataSources,
3918
4704
  sourceMappings: enriched.sourceMappings,
3919
4705
  recommendedNextActions: enriched.recommendedNextActions,