@flrande/bak-extension 0.6.16 → 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.
@@ -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) {
@@ -550,10 +646,159 @@
550
646
  return Object.keys(result).length > 0 ? result : void 0;
551
647
  }
552
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
+
553
798
  // package.json
554
799
  var package_default = {
555
800
  name: "@flrande/bak-extension",
556
- version: "0.6.16",
801
+ version: "0.6.17",
557
802
  type: "module",
558
803
  scripts: {
559
804
  build: "tsup src/background.ts src/content.ts src/popup.ts --format iife --out-dir dist --clean && node scripts/copy-assets.mjs",
@@ -642,18 +887,6 @@
642
887
  }
643
888
  return Object.keys(result).length > 0 ? result : void 0;
644
889
  }
645
- function headerValue(headers, name) {
646
- if (!headers) {
647
- return void 0;
648
- }
649
- const lower = name.toLowerCase();
650
- for (const [key, value] of Object.entries(headers)) {
651
- if (key.toLowerCase() === lower) {
652
- return value;
653
- }
654
- }
655
- return void 0;
656
- }
657
890
  function isTextualContentType(contentType) {
658
891
  if (!contentType) {
659
892
  return true;
@@ -669,7 +902,8 @@
669
902
  return {
670
903
  ...publicEntry,
671
904
  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
905
+ responseHeaders: typeof entry.responseHeaders === "object" && entry.responseHeaders !== null ? { ...entry.responseHeaders } : void 0,
906
+ ...buildNetworkEntryDerivedFields(entry)
673
907
  };
674
908
  }
675
909
  function pushEntry(state, entry, requestId) {
@@ -851,25 +1085,11 @@
851
1085
  state.requestIdToEntryId.clear();
852
1086
  state.lastTouchedAt = Date.now();
853
1087
  }
854
- function entryMatchesFilters(entry, filters) {
855
- const urlIncludes = typeof filters.urlIncludes === "string" ? filters.urlIncludes : "";
856
- const method = typeof filters.method === "string" ? filters.method.toUpperCase() : "";
857
- const status = typeof filters.status === "number" ? filters.status : void 0;
858
- if (urlIncludes && !entry.url.includes(urlIncludes)) {
859
- return false;
860
- }
861
- if (method && entry.method.toUpperCase() !== method) {
862
- return false;
863
- }
864
- if (typeof status === "number" && entry.status !== status) {
865
- return false;
866
- }
867
- return true;
868
- }
869
1088
  function listNetworkEntries(tabId, filters = {}) {
870
1089
  const state = getState(tabId);
871
- const limit = typeof filters.limit === "number" ? Math.max(1, Math.min(500, Math.floor(filters.limit))) : 50;
872
- return state.entries.filter((entry) => entryMatchesFilters(entry, filters)).slice(-limit).reverse().map((entry) => sanitizeEntry(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();
873
1093
  }
874
1094
  function getNetworkEntry(tabId, id) {
875
1095
  const state = getState(tabId);
@@ -886,6 +1106,7 @@
886
1106
  if (entry.rawRequestBodyTruncated === true) {
887
1107
  return {
888
1108
  entry: publicEntry,
1109
+ bodyTruncated: true,
889
1110
  degradedReason: "live replay unavailable because the captured request body was truncated in memory"
890
1111
  };
891
1112
  }
@@ -893,17 +1114,18 @@
893
1114
  entry: publicEntry,
894
1115
  headers: entry.rawRequestHeaders ? { ...entry.rawRequestHeaders } : void 0,
895
1116
  body: entry.rawRequestBody,
896
- contentType: headerValue(entry.rawRequestHeaders, "content-type")
1117
+ contentType: headerValue(entry.rawRequestHeaders, "content-type"),
1118
+ bodyTruncated: false
897
1119
  };
898
1120
  }
899
1121
  async function waitForNetworkEntry(tabId, filters = {}) {
900
1122
  const timeoutMs = typeof filters.timeoutMs === "number" ? Math.max(1, Math.floor(filters.timeoutMs)) : 5e3;
901
1123
  const deadline = Date.now() + timeoutMs;
902
1124
  const state = getState(tabId);
903
- 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));
904
1126
  while (Date.now() < deadline) {
905
1127
  const nextState = getState(tabId);
906
- 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));
907
1129
  if (matched) {
908
1130
  return sanitizeEntry(matched);
909
1131
  }
@@ -924,7 +1146,7 @@
924
1146
  }).toLowerCase();
925
1147
  return entry.url.toLowerCase().includes(normalized) || (entry.requestBodyPreview ?? "").toLowerCase().includes(normalized) || (entry.responseBodyPreview ?? "").toLowerCase().includes(normalized) || headerText.includes(normalized);
926
1148
  });
927
- const scannedEntries = state.entries.filter((entry) => entryMatchesFilters(entry, {}));
1149
+ const scannedEntries = state.entries.filter((entry) => networkEntryMatchesFilters(entry, {}));
928
1150
  const toCoverage = (entries, key, truncatedKey) => ({
929
1151
  full: entries.filter((entry) => typeof entry[key] === "string" && entry[truncatedKey] !== true).length,
930
1152
  partial: entries.filter((entry) => typeof entry[key] === "string" && entry[truncatedKey] === true).length,
@@ -1979,6 +2201,11 @@
1979
2201
  "referer",
1980
2202
  "set-cookie"
1981
2203
  ]);
2204
+ var CLONE_FORBIDDEN_HEADER_NAMES = /* @__PURE__ */ new Set([
2205
+ ...REPLAY_FORBIDDEN_HEADER_NAMES,
2206
+ "x-csrf-token",
2207
+ "x-xsrf-token"
2208
+ ]);
1982
2209
  var ws = null;
1983
2210
  var reconnectTimer = null;
1984
2211
  var nextReconnectInMs = null;
@@ -2123,6 +2350,12 @@
2123
2350
  async function listSessionBindingStates() {
2124
2351
  return Object.values(await loadSessionBindingStateMap());
2125
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
+ }
2126
2359
  function collectPopupSessionBindingTabIds(state) {
2127
2360
  return [
2128
2361
  ...new Set(state.tabIds.concat([state.activeTabId, state.primaryTabId].filter((value) => typeof value === "number")))
@@ -2885,6 +3118,54 @@
2885
3118
  }
2886
3119
  return {};
2887
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
+ };
2888
3169
  try {
2889
3170
  const targetWindow = payload.scope === "main" ? window : payload.scope === "current" ? resolveFrameWindow(payload.framePath ?? []) : window;
2890
3171
  if (payload.action === "eval") {
@@ -2947,22 +3228,113 @@
2947
3228
  }
2948
3229
  }
2949
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
+ };
2950
3254
  const controller = typeof AbortController === "function" ? new AbortController() : null;
2951
3255
  const timeoutId = controller && typeof payload.timeoutMs === "number" && payload.timeoutMs > 0 ? window.setTimeout(() => controller.abort(), payload.timeoutMs) : null;
2952
3256
  let response;
3257
+ let bodyText = "";
2953
3258
  try {
3259
+ diagnosticsBase.requestSent = true;
3260
+ diagnosticsBase.timing.requestSentAt = Date.now();
2954
3261
  response = await targetWindow.fetch(payload.url, {
2955
3262
  method: payload.method || "GET",
2956
3263
  headers,
2957
3264
  body: typeof payload.body === "string" ? payload.body : void 0,
2958
3265
  signal: controller ? controller.signal : void 0
2959
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
+ };
2960
3333
  } finally {
2961
3334
  if (timeoutId !== null) {
2962
3335
  window.clearTimeout(timeoutId);
2963
3336
  }
2964
3337
  }
2965
- const bodyText = await response.text();
2966
3338
  const headerMap = {};
2967
3339
  response.headers.forEach((value, key) => {
2968
3340
  headerMap[key] = value;
@@ -2971,13 +3343,9 @@
2971
3343
  url: targetWindow.location.href,
2972
3344
  framePath: payload.scope === "current" ? payload.framePath ?? [] : [],
2973
3345
  value: (() => {
2974
- const encoder = typeof TextEncoder === "function" ? new TextEncoder() : null;
2975
- const decoder = typeof TextDecoder === "function" ? new TextDecoder() : null;
2976
- const previewLimit = !fullResponse && typeof payload.maxBytes === "number" && payload.maxBytes > 0 ? payload.maxBytes : 8192;
2977
- const encodedBody = encoder ? encoder.encode(bodyText) : null;
2978
- const bodyBytes = encodedBody ? encodedBody.byteLength : bodyText.length;
3346
+ const bodyBytes = diagnosticsBase.bodyBytesRead || utf8ByteLength2(bodyText);
2979
3347
  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;
3348
+ const previewText = fullResponse ? bodyText : truncated ? truncateUtf8Text(bodyText, previewLimit) : bodyText;
2981
3349
  const result = {
2982
3350
  url: response.url,
2983
3351
  status: response.status,
@@ -2987,10 +3355,57 @@
2987
3355
  bytes: bodyBytes,
2988
3356
  truncated,
2989
3357
  authApplied: authApplied.length > 0 ? authApplied : void 0,
2990
- authSources: authSources.size > 0 ? [...authSources] : 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
+ }
2991
3372
  };
2992
3373
  if (payload.mode === "json") {
2993
- const parsedJson = bodyText ? JSON.parse(bodyText) : void 0;
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
+ }
2994
3409
  const summary = buildJsonSummary(parsedJson);
2995
3410
  if (fullResponse || !truncated) {
2996
3411
  result.json = parsedJson;
@@ -3073,12 +3488,26 @@
3073
3488
  delete clone.requestHeaders;
3074
3489
  delete clone.requestBodyPreview;
3075
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
+ }
3076
3498
  }
3077
3499
  if (!sections.has("response")) {
3078
3500
  delete clone.responseHeaders;
3079
3501
  delete clone.responseBodyPreview;
3080
3502
  delete clone.responseBodyTruncated;
3081
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
+ }
3082
3511
  }
3083
3512
  return clone;
3084
3513
  }
@@ -3096,6 +3525,99 @@
3096
3525
  }
3097
3526
  return Object.keys(headers).length > 0 ? headers : void 0;
3098
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);
3538
+ continue;
3539
+ }
3540
+ headers[name] = value;
3541
+ }
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
+ };
3620
+ }
3099
3621
  function collectTimestampMatchesFromText(text, source, patterns) {
3100
3622
  const regexes = (patterns ?? [
3101
3623
  String.raw`\b20\d{2}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:?\d{2})?\b`,
@@ -3516,6 +4038,7 @@
3516
4038
  const pageDataCandidates = await probePageDataCandidatesForTab(tabId, inspection);
3517
4039
  const recentNetwork = listNetworkEntries(tabId, { limit: 25 });
3518
4040
  const pageDataReport = buildInspectPageDataResult({
4041
+ pageUrl: inspection.url,
3519
4042
  suspiciousGlobals: inspection.suspiciousGlobals ?? [],
3520
4043
  tables: inspection.tables ?? [],
3521
4044
  visibleTimestamps: inspection.visibleTimestamps ?? [],
@@ -3523,7 +4046,9 @@
3523
4046
  pageDataCandidates,
3524
4047
  recentNetwork,
3525
4048
  tableAnalyses: tables,
3526
- 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 : []
3527
4052
  });
3528
4053
  const matched = selectReplaySchemaMatch(response.json, tables, {
3529
4054
  preferredSourceId: `networkResponse:${requestId}`,
@@ -3943,7 +4468,12 @@
3943
4468
  limit: typeof params.limit === "number" ? params.limit : void 0,
3944
4469
  urlIncludes: typeof params.urlIncludes === "string" ? params.urlIncludes : void 0,
3945
4470
  status: typeof params.status === "number" ? params.status : void 0,
3946
- 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
3947
4477
  })
3948
4478
  };
3949
4479
  } catch {
@@ -3980,6 +4510,17 @@
3980
4510
  return searchNetworkEntries(tab.id, String(params.pattern ?? ""), typeof params.limit === "number" ? params.limit : 50);
3981
4511
  });
3982
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);
4522
+ });
4523
+ }
3983
4524
  case "network.waitFor": {
3984
4525
  return await preserveHumanFocus(typeof target.tabId !== "number", async () => {
3985
4526
  const tab = await withTab(target);
@@ -4130,9 +4671,10 @@
4130
4671
  await ensureNetworkDebugger(tab.id).catch(() => void 0);
4131
4672
  const inspection = await collectPageInspection(tab.id, params);
4132
4673
  const pageDataCandidates = await probePageDataCandidatesForTab(tab.id, inspection);
4133
- const network = listNetworkEntries(tab.id, { limit: 10 });
4674
+ const network = listNetworkEntries(tab.id, { limit: 25, tail: true });
4134
4675
  const tableAnalyses = await collectTableAnalyses(tab.id);
4135
4676
  const enriched = buildInspectPageDataResult({
4677
+ pageUrl: inspection.url,
4136
4678
  suspiciousGlobals: inspection.suspiciousGlobals ?? [],
4137
4679
  tables: inspection.tables ?? [],
4138
4680
  visibleTimestamps: inspection.visibleTimestamps ?? [],
@@ -4140,7 +4682,9 @@
4140
4682
  pageDataCandidates,
4141
4683
  recentNetwork: network,
4142
4684
  tableAnalyses,
4143
- 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 : []
4144
4688
  });
4145
4689
  const recommendedNextSteps = enriched.recommendedNextActions.map((action) => action.command);
4146
4690
  return {
@@ -4150,6 +4694,12 @@
4150
4694
  inlineTimestamps: inspection.inlineTimestamps ?? [],
4151
4695
  pageDataCandidates,
4152
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,
4153
4703
  dataSources: enriched.dataSources,
4154
4704
  sourceMappings: enriched.sourceMappings,
4155
4705
  recommendedNextActions: enriched.recommendedNextActions,