@wenathlan/extension 1.1.41 → 1.1.43

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.
@@ -813,6 +813,160 @@ var sessionmemory = class {
813
813
  async getrecordingconsents() {
814
814
  return await this.adapter.get("recordingconsents") ?? [];
815
815
  }
816
+ /** Stores one outbound call record with its transport facts and body, replacing the previous record of that id; the user configured call retention window expires the oldest bodies while the metadata always survives. */
817
+ async addcall(record2) {
818
+ const records = await this.getcalls();
819
+ const retention = (await this.getsettings())?.callretention;
820
+ const combined = [record2, ...records.filter((item) => item.id !== record2.id)];
821
+ const stored = retention === void 0 ? combined : combined.map((item, index) => index < retention ? item : expirecallbody(item));
822
+ await this.adapter.set("calls", stored);
823
+ }
824
+ /** Returns every stored outbound call record, newest first. */
825
+ async getcalls() {
826
+ return await this.adapter.get("calls") ?? [];
827
+ }
828
+ /** Returns one outbound call record with its body by its id. */
829
+ async getcall(id) {
830
+ return (await this.getcalls()).find((item) => item.id === id);
831
+ }
832
+ /** Returns the outbound call records filtered by run and origin; an absent filter returns every call. */
833
+ async listcalls(filter) {
834
+ const records = await this.getcalls();
835
+ return records.filter((item) => (filter.runid === void 0 || item.runid === filter.runid) && (filter.origin === void 0 || item.origin === filter.origin));
836
+ }
837
+ /** Stores one typed endpoint definition version, appending to the version history of that endpoint name. */
838
+ async setendpoint(record2) {
839
+ const records = await this.adapter.get("endpoints") ?? [];
840
+ const prior = records.filter((item) => item.name === record2.name);
841
+ const version = prior.length > 0 ? Math.max(...prior.map((item) => item.version)) + 1 : 1;
842
+ await this.adapter.set("endpoints", [{ ...record2, version, at: record2.at }, ...records]);
843
+ }
844
+ /** Returns the newest endpointrecord definition of one name with its payload schema and version. */
845
+ async getendpoint(name) {
846
+ const records = await this.adapter.get("endpoints") ?? [];
847
+ return records.find((item) => item.name === name);
848
+ }
849
+ /** Returns the newest definition of every typed endpoint name with its schema and version history. */
850
+ async getendpoints() {
851
+ const records = await this.adapter.get("endpoints") ?? [];
852
+ const latest = /* @__PURE__ */ new Map();
853
+ for (const record2 of records) if (!latest.has(record2.name)) latest.set(record2.name, record2);
854
+ return [...latest.values()];
855
+ }
856
+ /** Stores one fetch consent decision per origin with its reviewed header names and values and its expiry window. */
857
+ async setfetchconsent(consent) {
858
+ const records = (await this.adapter.get("fetchconsents") ?? []).filter((item) => item.id !== consent.id);
859
+ await this.adapter.set("fetchconsents", [consent, ...records]);
860
+ }
861
+ /** Returns every fetch consent decision with its origin, header names and expiry window, newest first. */
862
+ async getfetchconsents() {
863
+ return await this.adapter.get("fetchconsents") ?? [];
864
+ }
865
+ /** Stores one api key reference record without any key material; the secret value stays behind its storage id. */
866
+ async setapikey(ref) {
867
+ const records = (await this.adapter.get("apikeys") ?? []).filter((item) => item.name !== ref.name);
868
+ await this.adapter.set("apikeys", [ref, ...records]);
869
+ }
870
+ /** Returns every stored api key reference with its origin scope, header name and storage id; key material never loads here. */
871
+ async getapikeys() {
872
+ return await this.adapter.get("apikeys") ?? [];
873
+ }
874
+ /** Removes one api key reference and its stored secret together. */
875
+ async removeapikey(name) {
876
+ const records = await this.getapikeys();
877
+ const ref = records.find((item) => item.name === name);
878
+ if (ref) await this.adapter.set(ref.storageid, void 0);
879
+ await this.adapter.set("apikeys", records.filter((item) => item.name !== name));
880
+ }
881
+ /** Stores one api key secret under its storage id; the value never appears in reports, outcomes or the audit trail. */
882
+ async setsecret(storageid, value) {
883
+ return this.adapter.set(storageid, value);
884
+ }
885
+ /** Loads one api key secret under its storage id for the executor only. */
886
+ async getsecret(storageid) {
887
+ return this.adapter.get(storageid);
888
+ }
889
+ /** Stores one channel record of a socket or event stream, replacing the previous record of that id. */
890
+ async addchannel(record2) {
891
+ const records = (await this.adapter.get("channels") ?? []).filter((item) => item.id !== record2.id);
892
+ await this.adapter.set("channels", [record2, ...records]);
893
+ }
894
+ /** Returns every stored channel record, newest first. */
895
+ async getchannels() {
896
+ return await this.adapter.get("channels") ?? [];
897
+ }
898
+ /** Returns one channel record by its id. */
899
+ async getchannel(id) {
900
+ return (await this.getchannels()).find((item) => item.id === id);
901
+ }
902
+ /** Queues one message envelope of a channel stream, keeping the arrival order for the waitmessage matchers. */
903
+ async addmessage(envelope) {
904
+ const records = (await this.adapter.get("messages") ?? []).filter((item) => !(item.channelid === envelope.channelid && item.sequence === envelope.sequence));
905
+ await this.adapter.set("messages", [...records, envelope]);
906
+ }
907
+ /** Returns every queued message envelope, oldest first, optionally filtered by channel and stream. */
908
+ async getmessages(channelid, stream) {
909
+ const records = await this.adapter.get("messages") ?? [];
910
+ return records.filter((item) => (channelid === void 0 || item.channelid === channelid) && (stream === void 0 || item.stream === stream));
911
+ }
912
+ /** Drops the matched message envelopes of one channel from the queue once a waitmessage step consumed them. */
913
+ async drainmessages(sequences) {
914
+ const records = await this.adapter.get("messages") ?? [];
915
+ const kept = records.filter((item) => !sequences.some((match) => match.channelid === item.channelid && match.sequence === item.sequence));
916
+ await this.adapter.set("messages", kept);
917
+ }
918
+ /** Stores one observed exchange record, replacing the previous record of that id. */
919
+ async addexchange(record2) {
920
+ const records = (await this.adapter.get("exchanges") ?? []).filter((item) => item.id !== record2.id);
921
+ await this.adapter.set("exchanges", [record2, ...records]);
922
+ }
923
+ /** Returns every stored exchange record, newest first. */
924
+ async getexchanges() {
925
+ return await this.adapter.get("exchanges") ?? [];
926
+ }
927
+ /** Returns one exchange record by its id. */
928
+ async getexchange(id) {
929
+ return (await this.getexchanges()).find((item) => item.id === id);
930
+ }
931
+ /** Returns the exchange records filtered by run, origin and status; the status filter accepts one code or the failed class of every exchange with an error class. */
932
+ async listexchanges(filter) {
933
+ const records = await this.getexchanges();
934
+ return records.filter((item) => (filter.runid === void 0 || item.runid === filter.runid) && (filter.origin === void 0 || item.origin === filter.origin) && (filter.status === void 0 || (filter.status === "failed" ? item.errorclass !== void 0 : item.status === filter.status)));
935
+ }
936
+ /** Stores one captured response body with its mime type and byte size; the user configured body retention window expires the oldest bodies while the exchange metadata always survives. */
937
+ async addbody(record2) {
938
+ const records = await this.getbodies();
939
+ const retention = (await this.getsettings())?.bodyretention;
940
+ const combined = [record2, ...records.filter((item) => item.ref !== record2.ref)];
941
+ const stored = retention === void 0 ? combined : combined.map((item, index) => index < retention ? item : expirebodybytes(item));
942
+ await this.adapter.set("bodies", stored);
943
+ }
944
+ /** Returns every captured body record, newest first. */
945
+ async getbodies() {
946
+ return await this.adapter.get("bodies") ?? [];
947
+ }
948
+ /** Returns one captured body record with its stored text by its reference. */
949
+ async getbody(ref) {
950
+ return (await this.getbodies()).find((item) => item.ref === ref);
951
+ }
952
+ /** Stores the page api map of one origin, replacing the previous map of that origin. */
953
+ async setapimap(origin, entries) {
954
+ const records = (await this.adapter.get("apimap") ?? []).filter((item) => item.origin !== origin);
955
+ await this.adapter.set("apimap", [...entries, ...records]);
956
+ }
957
+ /** Returns every stored page api map entry, newest first. */
958
+ async getapimap() {
959
+ return await this.adapter.get("apimap") ?? [];
960
+ }
961
+ /** Stores one event stream subscription record, replacing the previous record of that id. */
962
+ async setsubscription(record2) {
963
+ const records = (await this.adapter.get("subscriptions") ?? []).filter((item) => item.id !== record2.id);
964
+ await this.adapter.set("subscriptions", [record2, ...records]);
965
+ }
966
+ /** Returns every stored event stream subscription, newest first. */
967
+ async getsubscriptions() {
968
+ return await this.adapter.get("subscriptions") ?? [];
969
+ }
816
970
  };
817
971
  function mediakindof(record2) {
818
972
  if ("pages" in record2) return "pdf";
@@ -842,14 +996,673 @@ function expirecapturebytes(record2) {
842
996
  void bytes;
843
997
  return { ...metadata, bytesexpired: true };
844
998
  }
999
+ function expirecallbody(record2) {
1000
+ const { body, ...metadata } = record2;
1001
+ void body;
1002
+ return { ...metadata, bodyexpired: true };
1003
+ }
1004
+ function expirebodybytes(record2) {
1005
+ const { body, ...metadata } = record2;
1006
+ void body;
1007
+ return { ...metadata, bodyexpired: true };
1008
+ }
845
1009
  function randomid() {
846
1010
  return crypto.randomUUID();
847
1011
  }
848
1012
 
1013
+ // socketbus.ts
1014
+ var socketkinds = ["opensocket", "sendmessage", "waitmessage", "subscribesse", "longpoll"];
1015
+ function channelorigin(url) {
1016
+ try {
1017
+ const parsed = new URL(url);
1018
+ const protocol = parsed.protocol === "wss:" ? "https:" : parsed.protocol === "ws:" ? "http:" : parsed.protocol;
1019
+ return `${protocol}//${parsed.host}`;
1020
+ } catch {
1021
+ return "";
1022
+ }
1023
+ }
1024
+ function channeloptionsof(value) {
1025
+ if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
1026
+ const entry = value;
1027
+ if (typeof entry.url !== "string" || !entry.url.trim()) return void 0;
1028
+ const options = {};
1029
+ if (Array.isArray(entry.protocols)) options.protocols = entry.protocols.filter((item) => typeof item === "string" && item.trim().length > 0);
1030
+ if (typeof entry.reconnect === "number" && Number.isFinite(entry.reconnect)) options.reconnect = entry.reconnect;
1031
+ if (typeof entry.backoff === "number" && Number.isFinite(entry.backoff)) options.backoff = entry.backoff;
1032
+ if (typeof entry.backoffceiling === "number" && Number.isFinite(entry.backoffceiling)) options.backoffceiling = entry.backoffceiling;
1033
+ if (typeof entry.lifetime === "number" && Number.isFinite(entry.lifetime)) options.lifetime = entry.lifetime;
1034
+ return { url: entry.url.trim(), options };
1035
+ }
1036
+ function newchannel(input) {
1037
+ return { id: input.id, runid: input.runid, stepid: input.stepid, kind: input.kind, url: input.url, origin: channelorigin(input.url), state: "connecting", openedat: input.at, sent: 0, received: 0, reconnects: 0, ...input.protocols !== void 0 && input.protocols.length > 0 ? { protocols: [...input.protocols] } : {} };
1038
+ }
1039
+ function reconnectwaits(attempts, base, ceiling) {
1040
+ const count = Math.max(0, Math.floor(attempts));
1041
+ const waits = [];
1042
+ let wait = Math.max(0, base);
1043
+ for (let index = 0; index < count; index += 1) {
1044
+ waits.push(wait);
1045
+ const next = wait * 2;
1046
+ wait = ceiling !== void 0 && Number.isFinite(ceiling) && ceiling >= 0 ? Math.min(next, ceiling) : next;
1047
+ }
1048
+ return waits;
1049
+ }
1050
+ async function openchannel(input) {
1051
+ const sleep = input.sleep ?? ((milliseconds) => new Promise((resolve) => setTimeout(resolve, Math.max(0, milliseconds))));
1052
+ const now = input.now ?? Date.now;
1053
+ const attempts = Math.max(1, Math.floor(input.options.reconnect ?? 0) + 1);
1054
+ const waits = reconnectwaits(attempts - 1, input.options.backoff ?? 0, input.options.backoffceiling);
1055
+ let record2 = { ...input.record, state: "connecting" };
1056
+ let lasterror = "";
1057
+ for (let attempt = 0; attempt < attempts; attempt += 1) {
1058
+ try {
1059
+ const result = await input.connect(record2.url, record2.protocols ?? []);
1060
+ if (result.open) return { ...record2, state: "open", openedat: now() };
1061
+ lasterror = result.error ?? `closed with code ${result.code ?? 0}`;
1062
+ } catch (error) {
1063
+ lasterror = error instanceof Error ? error.message : String(error);
1064
+ }
1065
+ if (attempt < attempts - 1) {
1066
+ const wait = waits[attempt] ?? 0;
1067
+ if (wait > 0) await sleep(wait);
1068
+ record2 = { ...record2, reconnects: record2.reconnects + 1 };
1069
+ }
1070
+ }
1071
+ return { ...record2, state: "failed", error: lasterror };
1072
+ }
1073
+ function closechannel(record2, at, error) {
1074
+ const state = error !== void 0 ? "failed" : "closed";
1075
+ return { ...record2, state, closedat: at, ...error !== void 0 ? { error } : {} };
1076
+ }
1077
+ function tagmessage(state, channelid, stream, payload, at) {
1078
+ const sequence = (state.sequences[channelid] ?? 0) + 1;
1079
+ const envelope = { channelid, stream, payload, sequence, at };
1080
+ return { state: { sequences: { ...state.sequences, [channelid]: sequence }, queue: state.queue }, envelope };
1081
+ }
1082
+ function publishmessage(state, channelid, stream, payload, at) {
1083
+ return tagmessage(state, channelid, stream, payload, at);
1084
+ }
1085
+ function receivemessage(state, channelid, stream, payload, at) {
1086
+ const tagged = tagmessage(state, channelid, stream, payload, at);
1087
+ return { state: { ...tagged.state, queue: [...state.queue, tagged.envelope] }, envelope: tagged.envelope };
1088
+ }
1089
+ function pathstep(current, segment) {
1090
+ if (Array.isArray(current) && /^\d+$/.test(segment)) return current[Number.parseInt(segment, 10)];
1091
+ if (current && typeof current === "object" && !Array.isArray(current)) return current[segment];
1092
+ return void 0;
1093
+ }
1094
+ function matchmessage(filter, envelope) {
1095
+ if (!filter) return true;
1096
+ if (filter.stream !== void 0 && filter.stream !== envelope.stream) return false;
1097
+ if (filter.path !== void 0) {
1098
+ try {
1099
+ const parsed = JSON.parse(envelope.payload);
1100
+ let current = parsed;
1101
+ let missing = false;
1102
+ for (const segment of filter.path.split(".")) {
1103
+ const next = pathstep(current, segment);
1104
+ if (next === void 0) {
1105
+ missing = true;
1106
+ break;
1107
+ }
1108
+ current = next;
1109
+ }
1110
+ if (missing) return false;
1111
+ } catch {
1112
+ return false;
1113
+ }
1114
+ }
1115
+ return true;
1116
+ }
1117
+ function messagefilterof(value) {
1118
+ if (!value || typeof value !== "object" || Array.isArray(value)) return {};
1119
+ const entry = value;
1120
+ const filter = {};
1121
+ if (typeof entry.stream === "string" && entry.stream.trim()) filter.stream = entry.stream.trim();
1122
+ if (typeof entry.path === "string" && entry.path.trim()) filter.path = entry.path.trim();
1123
+ if (typeof entry.limit === "number" && Number.isFinite(entry.limit) && entry.limit >= 1) filter.limit = Math.floor(entry.limit);
1124
+ return filter;
1125
+ }
1126
+ function parsessetext(text2) {
1127
+ const separator = text2.lastIndexOf("\n\n");
1128
+ const complete = separator === -1 ? "" : text2.slice(0, separator + 2);
1129
+ const rest = separator === -1 ? text2 : text2.slice(separator + 2);
1130
+ const events = [];
1131
+ for (const block of complete.split(/\n\n/)) {
1132
+ const id = [];
1133
+ const names = [];
1134
+ const data = [];
1135
+ let retry;
1136
+ for (const line of block.split("\n")) {
1137
+ if (line === "" || line.startsWith(":")) continue;
1138
+ const colon = line.indexOf(":");
1139
+ const field = colon === -1 ? line : line.slice(0, colon);
1140
+ let value = colon === -1 ? "" : line.slice(colon + 1);
1141
+ if (value.startsWith(" ")) value = value.slice(1);
1142
+ if (field === "id" && value !== "") id.push(value);
1143
+ if (field === "event" && value !== "") names.push(value);
1144
+ if (field === "data") data.push(value);
1145
+ if (field === "retry" && /^\d+$/.test(value)) retry = Number.parseInt(value, 10);
1146
+ }
1147
+ if (id.length === 0 && names.length === 0 && data.length === 0) continue;
1148
+ events.push({ ...id.length > 0 ? { id: id[id.length - 1] } : {}, ...names.length > 0 ? { event: names[names.length - 1] } : {}, data: data.join("\n"), ...retry !== void 0 ? { retry } : {} });
1149
+ }
1150
+ return { events, rest };
1151
+ }
1152
+ function sserequestheaders(record2) {
1153
+ return { accept: "text/event-stream", ...record2.lasteventid !== void 0 && record2.lasteventid !== "" ? { "last-event-id": record2.lasteventid } : {} };
1154
+ }
1155
+ function subscriptionoptionsof(value) {
1156
+ if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
1157
+ const entry = value;
1158
+ if (typeof entry.url !== "string" || !entry.url.trim()) return void 0;
1159
+ const cancel = entry.cancel;
1160
+ if (!cancel || typeof cancel !== "object" || Array.isArray(cancel)) return void 0;
1161
+ const cancelrecord = cancel;
1162
+ if (cancelrecord.kind !== "stop" && cancelrecord.kind !== "lifetime") return void 0;
1163
+ if (typeof cancelrecord.value !== "string" && typeof cancelrecord.value !== "number") return void 0;
1164
+ const result = { url: entry.url.trim(), cancel: { kind: cancelrecord.kind, value: cancelrecord.value } };
1165
+ if (typeof entry.lifetime === "number" && Number.isFinite(entry.lifetime) && entry.lifetime > 0) result.lifetime = entry.lifetime;
1166
+ if (typeof entry.lasteventid === "string" && entry.lasteventid.trim()) result.lasteventid = entry.lasteventid.trim();
1167
+ return result;
1168
+ }
1169
+ function pollcursorof(value) {
1170
+ if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
1171
+ const entry = value;
1172
+ if (typeof entry.url !== "string" || !entry.url.trim()) return void 0;
1173
+ if (typeof entry.cursorfield !== "string" || !entry.cursorfield.trim()) return void 0;
1174
+ if (typeof entry.interval !== "number" || !Number.isFinite(entry.interval) || entry.interval <= 0) return void 0;
1175
+ const stop = entry.stop;
1176
+ if (!stop || typeof stop !== "object" || Array.isArray(stop)) return void 0;
1177
+ const stoprecord = stop;
1178
+ if (typeof stoprecord.field !== "string" || !stoprecord.field.trim()) return void 0;
1179
+ if (typeof stoprecord.equals !== "string") return void 0;
1180
+ const cursor = { url: entry.url.trim(), cursorfield: entry.cursorfield.trim(), interval: entry.interval, stop: { field: stoprecord.field.trim(), equals: stoprecord.equals } };
1181
+ if (typeof entry.maxpolls === "number" && Number.isFinite(entry.maxpolls) && entry.maxpolls >= 1) cursor.maxpolls = Math.floor(entry.maxpolls);
1182
+ if (typeof entry.param === "string" && entry.param.trim()) cursor.param = entry.param.trim();
1183
+ return cursor;
1184
+ }
1185
+ function cursorfrom(response, field) {
1186
+ let current = response;
1187
+ for (const segment of field.split(".")) {
1188
+ const next = pathstep(current, segment);
1189
+ if (next === void 0) return void 0;
1190
+ current = next;
1191
+ }
1192
+ return current === void 0 || current === null ? void 0 : String(current);
1193
+ }
1194
+ function pollurl(cursor, value) {
1195
+ if (cursor.param === void 0 || value === void 0) {
1196
+ return { url: cursor.url, ...value !== void 0 ? { body: JSON.stringify({ [cursor.cursorfield]: value }) } : {} };
1197
+ }
1198
+ const url = new URL(cursor.url);
1199
+ url.searchParams.set(cursor.param, value);
1200
+ return { url: url.toString() };
1201
+ }
1202
+ function polldecision(input) {
1203
+ if (input.cancelled?.() === true) return { continue: false, reason: "The long poll loop was cancelled." };
1204
+ if (input.expiresat !== void 0 && input.now >= input.expiresat) return { continue: false, reason: "The long poll loop stopped at the reviewed plan expiry." };
1205
+ const stopvalue = cursorfrom(input.response, input.cursor.stop.field);
1206
+ if (stopvalue !== void 0 && stopvalue === input.cursor.stop.equals) return { continue: false, reason: `The stop condition matched ${input.cursor.stop.field} ${stopvalue}.` };
1207
+ if (input.cursor.maxpolls !== void 0 && input.polls + 1 >= input.cursor.maxpolls) return { continue: false, reason: `The long poll loop reached the reviewed poll ceiling of ${input.cursor.maxpolls}.` };
1208
+ const value = cursorfrom(input.response, input.cursor.cursorfield);
1209
+ const next = pollurl(input.cursor, value);
1210
+ return { continue: true, reason: "The long poll loop continues.", ...value !== void 0 ? { cursor: value } : {}, next: { ...next, wait: input.cursor.interval } };
1211
+ }
1212
+
1213
+ // httpclient.ts
1214
+ var httpkinds = ["fetchurl", "parsejson", "parsehtml", "callrest", "callgraphql"];
1215
+ var redirectstatuses = /* @__PURE__ */ new Set([301, 302, 303, 307, 308]);
1216
+ var bodilessmethods = /* @__PURE__ */ new Set(["GET", "HEAD"]);
1217
+ function statusclassof(status) {
1218
+ if (status >= 100 && status < 200) return "informational";
1219
+ if (status >= 200 && status < 300) return "success";
1220
+ if (status >= 300 && status < 400) return "redirect";
1221
+ if (status >= 400 && status < 500) return "clienterror";
1222
+ if (status >= 500 && status < 600) return "servererror";
1223
+ return "unknown";
1224
+ }
1225
+ function templateurl(template, values) {
1226
+ return template.replace(/\{([a-z0-9_]+)\}/gi, (whole, name) => values[name] === void 0 ? whole : String(values[name]));
1227
+ }
1228
+ function fetchrequestof(value) {
1229
+ if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
1230
+ const options = value;
1231
+ if (typeof options.url !== "string" || !options.url.trim()) return void 0;
1232
+ const request = { url: options.url.trim() };
1233
+ if (typeof options.method === "string" && options.method.trim()) request.method = options.method.trim().toUpperCase();
1234
+ if (options.headers && typeof options.headers === "object" && !Array.isArray(options.headers)) {
1235
+ const headers = {};
1236
+ for (const [name, headervalue] of Object.entries(options.headers)) {
1237
+ if (typeof headervalue === "string") headers[name] = headervalue;
1238
+ }
1239
+ request.headers = headers;
1240
+ }
1241
+ if (typeof options.body === "string") request.body = options.body;
1242
+ if (options.mode === "cors" || options.mode === "no-cors" || options.mode === "same-origin") request.mode = options.mode;
1243
+ return request;
1244
+ }
1245
+ function fetchoptionsof(value) {
1246
+ if (!value || typeof value !== "object" || Array.isArray(value)) return {};
1247
+ const options = value;
1248
+ const normalized = {};
1249
+ if (typeof options.timeout === "number" && Number.isFinite(options.timeout)) normalized.timeout = options.timeout;
1250
+ if (typeof options.retries === "number" && Number.isFinite(options.retries)) normalized.retries = options.retries;
1251
+ if (typeof options.backoff === "number" && Number.isFinite(options.backoff)) normalized.backoff = options.backoff;
1252
+ if (typeof options.follow === "number" && Number.isFinite(options.follow)) normalized.follow = options.follow;
1253
+ return normalized;
1254
+ }
1255
+ function streamwindowof(value) {
1256
+ if (!value || typeof value !== "object" || Array.isArray(value)) return {};
1257
+ const options = value;
1258
+ const window2 = {};
1259
+ if (typeof options.budget === "number" && Number.isFinite(options.budget)) window2.budget = options.budget;
1260
+ return window2;
1261
+ }
1262
+ function jsonpathrulesof(value) {
1263
+ if (!Array.isArray(value)) return [];
1264
+ const rules = [];
1265
+ for (const item of value) {
1266
+ if (!item || typeof item !== "object" || Array.isArray(item)) continue;
1267
+ const entry = item;
1268
+ if (typeof entry.name !== "string" || !entry.name.trim()) continue;
1269
+ if (typeof entry.path !== "string" || !entry.path.trim()) continue;
1270
+ const rule = { name: entry.name.trim(), path: entry.path.trim() };
1271
+ if (entry.kind === "text" || entry.kind === "number" || entry.kind === "boolean" || entry.kind === "json") rule.kind = entry.kind;
1272
+ if (entry.default !== void 0) rule.default = entry.default;
1273
+ rules.push(rule);
1274
+ }
1275
+ return rules;
1276
+ }
1277
+ function htmlqueriesof(value) {
1278
+ if (!Array.isArray(value)) return [];
1279
+ const queries = [];
1280
+ for (const item of value) {
1281
+ if (!item || typeof item !== "object" || Array.isArray(item)) continue;
1282
+ const entry = item;
1283
+ if (typeof entry.selector !== "string" || !entry.selector.trim()) continue;
1284
+ const query = { selector: entry.selector.trim() };
1285
+ if (typeof entry.attribute === "string" && entry.attribute.trim()) query.attribute = entry.attribute.trim();
1286
+ if (entry.multi === true) query.multi = true;
1287
+ queries.push(query);
1288
+ }
1289
+ return queries;
1290
+ }
1291
+ function graphqlrequestof(value) {
1292
+ if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
1293
+ const options = value;
1294
+ if (typeof options.query !== "string" || !options.query.trim()) return void 0;
1295
+ if (options.operationkind !== "query" && options.operationkind !== "mutation") return void 0;
1296
+ const request = { query: options.query, operationkind: options.operationkind };
1297
+ if (options.variables && typeof options.variables === "object" && !Array.isArray(options.variables)) request.variables = options.variables;
1298
+ if (typeof options.operationname === "string" && options.operationname.trim()) request.operationname = options.operationname.trim();
1299
+ return request;
1300
+ }
1301
+ var realsleep = (milliseconds) => new Promise((resolve) => setTimeout(resolve, Math.max(0, milliseconds)));
1302
+ async function sendfetch(input) {
1303
+ const options = input.options ?? {};
1304
+ const sleep = input.sleep ?? realsleep;
1305
+ const now = input.now ?? Date.now;
1306
+ const attempts = Math.max(1, Math.floor(options.retries ?? 0) + 1);
1307
+ const backoff = options.backoff ?? 0;
1308
+ const follow = options.follow ?? Number.POSITIVE_INFINITY;
1309
+ let url = input.request.url;
1310
+ let method = (input.request.method ?? "GET").toUpperCase();
1311
+ let retries = 0;
1312
+ let redirects = 0;
1313
+ let lastreason = "";
1314
+ for (let attempt = 1; attempt <= attempts; attempt += 1) {
1315
+ let hops = 0;
1316
+ const startedat = now();
1317
+ let response;
1318
+ try {
1319
+ const init = { method, headers: { ...input.request.headers ?? {} }, ...input.request.body !== void 0 && !bodilessmethods.has(method) ? { body: input.request.body } : {}, ...input.request.mode !== void 0 ? { mode: input.request.mode } : {}, redirect: follow <= 0 ? "error" : "follow" };
1320
+ const sent = input.transport(url, init);
1321
+ if (options.timeout !== void 0 && Number.isFinite(options.timeout) && options.timeout >= 0) {
1322
+ let timedout = false;
1323
+ response = await Promise.race([sent, sleep(options.timeout).then(() => {
1324
+ timedout = true;
1325
+ return void 0;
1326
+ })]).then((value) => value ?? (timedout ? (() => {
1327
+ throw new Error(`The request timed out after ${options.timeout} milliseconds.`);
1328
+ })() : value));
1329
+ } else {
1330
+ response = await sent;
1331
+ }
1332
+ while (response !== void 0 && redirectstatuses.has(response.status) && typeof response.location === "string" && response.location) {
1333
+ hops += 1;
1334
+ if (hops > follow) throw new Error(`The redirect chain exceeded the reviewed follow limit of ${follow}.`);
1335
+ url = new URL(response.location, url).toString();
1336
+ if (method === "POST" && [301, 302, 303].includes(response.status)) method = "GET";
1337
+ response = await input.transport(url, { ...init, method });
1338
+ }
1339
+ redirects = hops;
1340
+ } catch (error) {
1341
+ lastreason = error instanceof Error ? error.message : String(error);
1342
+ response = void 0;
1343
+ }
1344
+ if (response !== void 0) {
1345
+ const body = response.body;
1346
+ return { url, status: response.status, statusclass: statusclassof(response.status), headernames: Object.keys(response.headers), body, bytes: body.length, duration: now() - startedat, retries, redirects };
1347
+ }
1348
+ if (attempt < attempts) {
1349
+ const wait = backoff * attempt;
1350
+ if (wait > 0) await sleep(wait);
1351
+ input.onretry?.(attempt, wait, lastreason);
1352
+ retries = attempt;
1353
+ }
1354
+ }
1355
+ throw new Error(`The request failed after ${attempts} attempt${attempts === 1 ? "" : "s"} with ${retries} retr${retries === 1 ? "y" : "ies"}: ${lastreason}`);
1356
+ }
1357
+ async function readstream(input) {
1358
+ const pull = typeof input.chunks === "function" ? input.chunks : /* @__PURE__ */ ((source) => {
1359
+ let index = 0;
1360
+ return async () => source[index++];
1361
+ })(input.chunks);
1362
+ let count = 0;
1363
+ let total = 0;
1364
+ for (; ; ) {
1365
+ if (input.window.abort?.() === true) return { chunks: count, bytes: total, aborted: true, reason: "The reviewed abort flag stopped the stream." };
1366
+ const chunk = await pull();
1367
+ if (chunk === void 0) return { chunks: count, bytes: total, aborted: false };
1368
+ const next = total + chunk.length;
1369
+ if (input.window.budget !== void 0 && next > input.window.budget) return { chunks: count, bytes: total, aborted: true, reason: `The stream aborted at ${next} bytes past the reviewed byte budget of ${input.window.budget}.` };
1370
+ total = next;
1371
+ count += 1;
1372
+ input.window.onchunk?.(chunk, total);
1373
+ }
1374
+ }
1375
+ function pathstep2(current, segment) {
1376
+ if (Array.isArray(current) && /^\d+$/.test(segment)) return current[Number.parseInt(segment, 10)];
1377
+ if (current && typeof current === "object" && !Array.isArray(current)) return current[segment];
1378
+ return void 0;
1379
+ }
1380
+ function coerce(value, kind, fallback) {
1381
+ if (value === void 0 || value === null) return { value: fallback, missing: true };
1382
+ if (kind === "text") return { value: String(value), missing: false };
1383
+ if (kind === "number") {
1384
+ const numeric = typeof value === "number" ? value : Number(value);
1385
+ return Number.isFinite(numeric) ? { value: numeric, missing: false } : { value: fallback, missing: true };
1386
+ }
1387
+ if (kind === "boolean") return { value: value === true || value === "true", missing: false };
1388
+ return { value, missing: false };
1389
+ }
1390
+ function readpath(parsed, rules) {
1391
+ const fields = [];
1392
+ for (const rule of rules) {
1393
+ const kind = rule.kind ?? "text";
1394
+ let current = parsed;
1395
+ let missing = false;
1396
+ for (const segment of rule.path.split(".")) {
1397
+ const next = pathstep2(current, segment);
1398
+ if (next === void 0) {
1399
+ missing = true;
1400
+ break;
1401
+ }
1402
+ current = next;
1403
+ }
1404
+ if (missing) fields.push({ name: rule.name, path: rule.path, kind, ...rule.default !== void 0 ? { value: rule.default } : {}, missing: true });
1405
+ else {
1406
+ const resolved = coerce(current, kind, rule.default);
1407
+ fields.push({ name: rule.name, path: rule.path, kind, ...resolved.value !== void 0 ? { value: resolved.value } : {}, ...resolved.missing ? { missing: true } : {} });
1408
+ }
1409
+ }
1410
+ return fields;
1411
+ }
1412
+ function payloadvalid(payload, schema) {
1413
+ if (!schema) return { ok: false, errors: ["The typed endpoint call needs a reviewed payload schema before it runs."] };
1414
+ const errors = [];
1415
+ for (const field of schema.fields) {
1416
+ const value = payload[field.name];
1417
+ if (value === void 0 || value === null) {
1418
+ if (field.required === true) errors.push(`The required field ${field.name} of kind ${field.kind} is missing.`);
1419
+ continue;
1420
+ }
1421
+ if (field.kind === "string" && typeof value !== "string") errors.push(`The field ${field.name} must be a string.`);
1422
+ if (field.kind === "number" && (typeof value !== "number" || !Number.isFinite(value))) errors.push(`The field ${field.name} must be a finite number.`);
1423
+ if (field.kind === "boolean" && typeof value !== "boolean") errors.push(`The field ${field.name} must be a boolean.`);
1424
+ }
1425
+ return { ok: errors.length === 0, errors };
1426
+ }
1427
+ function payloadwithdefaults(payload, schema) {
1428
+ if (!schema) return payload;
1429
+ const merged = { ...payload };
1430
+ for (const field of schema.fields) {
1431
+ if (merged[field.name] === void 0 && field.default !== void 0) merged[field.name] = field.default;
1432
+ }
1433
+ return merged;
1434
+ }
1435
+ function errorsof(body) {
1436
+ try {
1437
+ const parsed = JSON.parse(body);
1438
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return [body];
1439
+ const record2 = parsed;
1440
+ for (const key of ["errors", "messages", "error", "message"]) {
1441
+ const value = record2[key];
1442
+ if (Array.isArray(value)) return value.map((item) => typeof item === "string" ? item : item && typeof item === "object" && typeof item.message === "string" ? item.message : String(item));
1443
+ if (typeof value === "string") return [value];
1444
+ }
1445
+ return [body];
1446
+ } catch {
1447
+ return [body];
1448
+ }
1449
+ }
1450
+ async function callrest(input) {
1451
+ const check = payloadvalid(input.payload, input.endpoint.schema);
1452
+ if (!check.ok) throw new Error(check.errors.join(" "));
1453
+ const payload = payloadwithdefaults(input.payload, input.endpoint.schema);
1454
+ const url = templateurl(input.endpoint.url, payload);
1455
+ const method = input.endpoint.method.toUpperCase();
1456
+ const request = { url, method, ...input.endpoint.headers !== void 0 ? { headers: input.endpoint.headers } : {}, ...bodilessmethods.has(method) ? {} : { body: JSON.stringify(payload) } };
1457
+ const transport = await sendfetch({ request, ...input.options !== void 0 ? { options: input.options } : {}, transport: input.transport, ...input.sleep !== void 0 ? { sleep: input.sleep } : {}, ...input.onretry !== void 0 ? { onretry: input.onretry } : {}, ...input.now !== void 0 ? { now: input.now } : {} });
1458
+ const ok = input.success !== void 0 ? input.success.includes(transport.status) : transport.statusclass === "success";
1459
+ return { transport, url, payload, ok, errors: ok ? [] : errorsof(transport.body) };
1460
+ }
1461
+ function graphqlopenvelope(request) {
1462
+ return JSON.stringify({ query: request.query, ...request.variables !== void 0 ? { variables: request.variables } : {}, ...request.operationname !== void 0 ? { operationName: request.operationname } : {} });
1463
+ }
1464
+ function unwrapgraphql(value) {
1465
+ if (!value || typeof value !== "object" || Array.isArray(value)) return { errors: ["The graphql response is not a json object."] };
1466
+ const record2 = value;
1467
+ const errors = Array.isArray(record2.errors) ? record2.errors.map((item) => typeof item === "string" ? item : item && typeof item === "object" && typeof item.message === "string" ? item.message : String(item)) : [];
1468
+ return { ...record2.data !== void 0 ? { data: record2.data } : {}, errors };
1469
+ }
1470
+ async function callgraphql(input) {
1471
+ const request = { url: input.endpoint.url, method: "POST", ...input.endpoint.headers !== void 0 ? { headers: input.endpoint.headers } : {}, body: graphqlopenvelope(input.request) };
1472
+ const transport = await sendfetch({ request, ...input.options !== void 0 ? { options: input.options } : {}, transport: input.transport, ...input.sleep !== void 0 ? { sleep: input.sleep } : {}, ...input.onretry !== void 0 ? { onretry: input.onretry } : {}, ...input.now !== void 0 ? { now: input.now } : {} });
1473
+ try {
1474
+ const unwrapped = unwrapgraphql(JSON.parse(transport.body));
1475
+ return { transport, ...unwrapped.data !== void 0 ? { data: unwrapped.data } : {}, errors: unwrapped.errors };
1476
+ } catch {
1477
+ return { transport, errors: [transport.body] };
1478
+ }
1479
+ }
1480
+
1481
+ // netwatch.ts
1482
+ var netwatchkinds = ["watchrequests", "readheaders", "capturebodies", "mapapi", "extractapi"];
1483
+ function resourcefacts(entries) {
1484
+ const facts = [];
1485
+ for (const entry of entries) {
1486
+ const url = typeof entry.name === "string" ? entry.name : "";
1487
+ if (!url) continue;
1488
+ const entrytype = typeof entry.entryType === "string" ? entry.entryType : "resource";
1489
+ if (entrytype !== "resource" && entrytype !== "navigation") continue;
1490
+ facts.push({
1491
+ url,
1492
+ initiator: typeof entry.initiatorType === "string" ? entry.initiatorType : "",
1493
+ entrytype,
1494
+ start: typeof entry.startTime === "number" && Number.isFinite(entry.startTime) ? entry.startTime : 0,
1495
+ duration: typeof entry.duration === "number" && Number.isFinite(entry.duration) ? entry.duration : 0,
1496
+ transfer: typeof entry.transferSize === "number" && Number.isFinite(entry.transferSize) ? entry.transferSize : 0,
1497
+ protocol: typeof entry.nextHopProtocol === "string" ? entry.nextHopProtocol : "",
1498
+ ...typeof entry.responseStatus === "number" && Number.isInteger(entry.responseStatus) ? { status: entry.responseStatus } : {},
1499
+ ...entry.failed === true ? { failed: true } : {}
1500
+ });
1501
+ }
1502
+ return facts;
1503
+ }
1504
+ function failureclass(fact) {
1505
+ if (fact.status !== void 0 && fact.status >= 400) return { errorclass: "httperror", status: fact.status };
1506
+ if (fact.failed === true) return { errorclass: "networkerror", status: 0 };
1507
+ if ((fact.initiator === "fetch" || fact.initiator === "xmlhttprequest") && fact.duration > 0 && fact.transfer === 0 && fact.protocol === "") return { errorclass: "networkerror", status: 0 };
1508
+ return { status: fact.status ?? 0 };
1509
+ }
1510
+ function correlationid(runid, index) {
1511
+ return `${runid}-${index + 1}`;
1512
+ }
1513
+ function newexchange(input) {
1514
+ const verdict = failureclass(input.fact);
1515
+ let origin = "";
1516
+ try {
1517
+ origin = new URL(input.fact.url).origin;
1518
+ } catch {
1519
+ origin = "";
1520
+ }
1521
+ const method = input.fact.initiator === "fetch" || input.fact.initiator === "xmlhttprequest" ? "?" : "GET";
1522
+ const statusclass = verdict.status >= 100 && verdict.status < 600 ? verdict.status >= 200 && verdict.status < 300 ? "success" : verdict.status >= 300 && verdict.status < 400 ? "redirect" : verdict.status >= 400 && verdict.status < 500 ? "clienterror" : verdict.status >= 500 ? "servererror" : "informational" : "unknown";
1523
+ return { id: input.id, runid: input.runid, stepid: input.stepid, correlationid: input.correlationid, url: input.fact.url, origin, method, status: verdict.status, statusclass, ...verdict.errorclass !== void 0 ? { errorclass: verdict.errorclass } : {}, source: "page", ...input.fact.initiator ? { initiator: input.fact.initiator } : {}, timing: Math.round(input.fact.duration), bytes: input.fact.transfer, at: input.at };
1524
+ }
1525
+ function pairexchange(exchange, response) {
1526
+ if (exchange.correlationid !== response.correlationid) throw new Error(`The response ${response.correlationid} does not pair with the exchange ${exchange.correlationid}.`);
1527
+ return { ...exchange, status: response.status, statusclass: response.status >= 200 && response.status < 300 ? "success" : response.status >= 400 && response.status < 500 ? "clienterror" : response.status >= 500 ? "servererror" : response.status >= 300 && response.status < 400 ? "redirect" : "unknown", bytes: response.bytes, ...response.mime !== void 0 ? { mime: response.mime } : {}, ...response.bodyref !== void 0 ? { bodyref: response.bodyref } : {}, ...Object.keys(response.headers).length > 0 ? { responseheaders: response.headers } : {} };
1528
+ }
1529
+ function headerfilterof(value) {
1530
+ if (!value || typeof value !== "object" || Array.isArray(value)) return { allow: [], redact: [] };
1531
+ const entry = value;
1532
+ const names = (source) => Array.isArray(source) ? source.filter((name) => typeof name === "string" && name.trim().length > 0).map((name) => name.trim().toLowerCase()) : [];
1533
+ return { allow: names(entry.allow), redact: names(entry.redact) };
1534
+ }
1535
+ function capturedheaders(headers, filter) {
1536
+ const result = {};
1537
+ for (const [name, value] of Object.entries(headers)) {
1538
+ const key = name.trim().toLowerCase();
1539
+ if (filter.allow.length > 0 && !filter.allow.includes(key)) continue;
1540
+ result[key] = filter.redact.includes(key) ? "[redacted]" : value;
1541
+ }
1542
+ return result;
1543
+ }
1544
+ function bodyfilterof(value) {
1545
+ if (!value || typeof value !== "object" || Array.isArray(value)) return {};
1546
+ const entry = value;
1547
+ const filter = {};
1548
+ if (typeof entry.urlpattern === "string" && entry.urlpattern.trim()) filter.urlpattern = entry.urlpattern.trim();
1549
+ if (Array.isArray(entry.mimes)) filter.mimes = entry.mimes.filter((mime) => typeof mime === "string" && mime.trim().length > 0).map((mime) => mime.trim().toLowerCase());
1550
+ if (typeof entry.ceiling === "number" && Number.isFinite(entry.ceiling) && entry.ceiling >= 0) filter.ceiling = entry.ceiling;
1551
+ return filter;
1552
+ }
1553
+ function bodymatches(filter, exchange) {
1554
+ if (filter.urlpattern !== void 0 && !exchange.url.includes(filter.urlpattern)) return false;
1555
+ if (filter.mimes !== void 0 && filter.mimes.length > 0) {
1556
+ const mime = ((exchange.mime ?? "").split(";")[0] ?? "").trim().toLowerCase();
1557
+ if (!filter.mimes.includes(mime)) return false;
1558
+ }
1559
+ return true;
1560
+ }
1561
+ var privatemimes = /* @__PURE__ */ new Set(["text/html", "text/plain", "text/xml", "application/xml", "application/json", "text/json", "application/x-www-form-urlencoded", "application/graphql", "multipart/form-data"]);
1562
+ function privatemime(mime) {
1563
+ return privatemimes.has((mime.split(";")[0] ?? "").trim().toLowerCase());
1564
+ }
1565
+ function capturebody(input) {
1566
+ if (!bodymatches(input.filter, input.exchange)) return { refused: `The exchange ${input.exchange.correlationid} does not match the reviewed body filter.` };
1567
+ const ceiling = input.filter.ceiling;
1568
+ const stored = ceiling !== void 0 && input.body.length > ceiling ? input.body.slice(0, ceiling) : input.body;
1569
+ return { record: { ref: input.ref, runid: input.runid, correlationid: input.exchange.correlationid, url: input.exchange.url, mime: input.mime, bytes: stored.length, body: stored, at: input.at }, truncated: stored.length < input.body.length };
1570
+ }
1571
+ function payloadshapeof(body) {
1572
+ if (body === void 0) return [];
1573
+ try {
1574
+ const parsed = JSON.parse(body);
1575
+ const shape = (value) => value && typeof value === "object" && !Array.isArray(value) ? Object.keys(value) : [];
1576
+ if (Array.isArray(parsed)) return parsed.length > 0 ? shape(parsed[0]) : [];
1577
+ return shape(parsed);
1578
+ } catch {
1579
+ return [];
1580
+ }
1581
+ }
1582
+ function isapicandidate(exchange) {
1583
+ if (exchange.initiator === "fetch" || exchange.initiator === "xmlhttprequest") return true;
1584
+ if (exchange.bodyref !== void 0) return true;
1585
+ try {
1586
+ return /\/api\/|\/graphql|\.json($|\?)|\/v\d+\//i.test(new URL(exchange.url).pathname);
1587
+ } catch {
1588
+ return false;
1589
+ }
1590
+ }
1591
+ function apientries(exchanges, bodies) {
1592
+ const bodybyref = new Map(bodies.map((body) => [body.correlationid, body]));
1593
+ const groups = /* @__PURE__ */ new Map();
1594
+ for (const exchange of exchanges) {
1595
+ if (!isapicandidate(exchange)) continue;
1596
+ let endpoint = exchange.url;
1597
+ let origin = exchange.origin;
1598
+ try {
1599
+ const parsed = new URL(exchange.url);
1600
+ endpoint = `${parsed.origin}${parsed.pathname}`;
1601
+ origin = parsed.origin;
1602
+ } catch {
1603
+ }
1604
+ const key = `${exchange.method} ${endpoint}`;
1605
+ const group = groups.get(key) ?? { endpoint, method: exchange.method, origin, mimes: /* @__PURE__ */ new Map(), frequency: 0, json: 0, captured: 0, shapes: /* @__PURE__ */ new Map(), correlationids: [] };
1606
+ group.frequency += 1;
1607
+ group.correlationids.push(exchange.correlationid);
1608
+ const body = exchange.bodyref !== void 0 ? bodybyref.get(exchange.correlationid) : void 0;
1609
+ const mime = body?.mime ?? exchange.mime ?? "";
1610
+ group.mimes.set(mime, (group.mimes.get(mime) ?? 0) + 1);
1611
+ if (body !== void 0) {
1612
+ group.captured += 1;
1613
+ const shape = payloadshapeof(body.body);
1614
+ if (shape.length > 0) group.json += 1;
1615
+ const shapekey = shape.join(",");
1616
+ group.shapes.set(shapekey, (group.shapes.get(shapekey) ?? 0) + 1);
1617
+ }
1618
+ groups.set(key, group);
1619
+ }
1620
+ return [...groups.values()].map((group) => {
1621
+ const mime = [...group.mimes.entries()].sort((left, right) => right[1] - left[1])[0]?.[0] ?? "";
1622
+ const modalshape = [...group.shapes.entries()].sort((left, right) => right[1] - left[1])[0];
1623
+ return { endpoint: group.endpoint, method: group.method, mime, frequency: group.frequency, payloadshape: (modalshape?.[0] ?? "").split(",").filter(Boolean), jsonshare: group.captured > 0 ? group.json / group.captured : 0, stability: group.captured > 0 ? (modalshape?.[1] ?? 0) / group.captured : 0, origin: group.origin, correlationids: group.correlationids };
1624
+ });
1625
+ }
1626
+ function rankapis(entries) {
1627
+ const score = (entry) => entry.frequency * (1 + entry.jsonshare + entry.stability);
1628
+ return [...entries].sort((left, right) => score(right) - score(left) || right.frequency - left.frequency);
1629
+ }
1630
+ function apireplayspecof(value) {
1631
+ if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
1632
+ const entry = value;
1633
+ if (typeof entry.endpoint !== "string" || !entry.endpoint.trim()) return void 0;
1634
+ const spec = { endpoint: entry.endpoint.trim() };
1635
+ if (typeof entry.verb === "string" && entry.verb.trim()) spec.verb = entry.verb.trim().toUpperCase();
1636
+ if (entry.overrides !== void 0 && entry.overrides !== null && typeof entry.overrides === "object" && !Array.isArray(entry.overrides)) {
1637
+ const overrides = {};
1638
+ for (const [name, override] of Object.entries(entry.overrides)) {
1639
+ if (typeof override === "string") overrides[name] = override;
1640
+ }
1641
+ spec.overrides = overrides;
1642
+ }
1643
+ if (Array.isArray(entry.paths)) spec.paths = entry.paths.filter((path) => typeof path === "string" && path.trim().length > 0);
1644
+ return spec;
1645
+ }
1646
+ function replayurl(spec) {
1647
+ const url = new URL(spec.endpoint);
1648
+ for (const [name, value] of Object.entries(spec.overrides ?? {})) url.searchParams.set(name, value);
1649
+ return url.toString();
1650
+ }
1651
+ function extractvalues(body, paths) {
1652
+ let parsed;
1653
+ try {
1654
+ parsed = JSON.parse(body);
1655
+ } catch {
1656
+ return paths.map((path) => ({ path, missing: true }));
1657
+ }
1658
+ const fields = readpath(parsed, paths.map((path) => ({ name: path, path, kind: "json" })));
1659
+ return fields.map((field) => ({ path: field.path, ...field.value !== void 0 ? { value: field.value } : {}, ...field.missing ? { missing: true } : {} }));
1660
+ }
1661
+
849
1662
  // policy.ts
850
- var sensitiveactions = /* @__PURE__ */ new Set(["click", "type", "navigate", "select", "presskey", "drag", "drop", "upload", "clear", "check", "uncheck", "toggle", "submit", "reload", "back", "forward", "writestorage", "setattribute", "removeattribute", "evaluate", "tabcreate", "tabactivate", "tabclose", "tabreload", "windowcreate", "windowclose", "windowresize", "downloadfile", "clickpoint", "shiftclick", "dismissdialog", "enterframe", "typetime", "appendtext", "setvalue", "typeedit", "keyhold", "keyrelease", "submitsearch", "selectmulti", "chooseradio", "setslider", "setdate", "setcolor", "openlink", "openprivate", "reloadcache", "stopnav", "followlink", "spanav", "rewritequery", "setfragment", "navlist", "navprofile", "handleauth", "printpdf", "prefetch", "preconnect", "deeplink", "reopentab", "pausenav", "navrate", "openclipboard", "batchopen", "duplicatetab", "closepattern", "pintab", "mutetab", "movetab", "movetabwindow", "grouptabs", "colorgroup", "collapsegroup", "discardtab", "reloadtabs", "zoomin", "zoomout", "switchtab", "maximizewindow", "minimizewindow", "restorewindow", "focuswindow", "scratchwindow", "incognitowindow", "restoretab", "restorelayout", "reopenrun", "badgetab", "fillform", "filllabel", "fillplaceholder", "submitform", "retryform", "runwizard", "selectchain", "picktypeahead", "pickdate", "attachfile", "fillcard", "fillcode", "consentpassword", "exportcsv", "exportjson", "exportexcel", "copytable", "pushsheets", "streamdisk", "paginateextract", "resumeextract", "batchdownload", "pausedownload", "resumedownload", "interceptmime", "readclipboard", "writeclipboard", "copyscreen", "quarantinedownload", "scanvirus", "cleanupartifacts", "recordscreen", "captureaudio", "downloadimages"]);
851
- var interactionactions = /* @__PURE__ */ new Set(["focus", "scroll", "hover", "clickdeep", "rightclick", "doubleclick", "scrollpage", "scrollby", "scrollend", "scrolltop", "fullscreen", "zoomset", "movepointer", "clicktext", "clickaria", "clickname", "expanddetails", "pierceshadow", "retryaction"]);
852
- var readactions = /* @__PURE__ */ new Set(["observe", "inspect", "extract", "wait", "waitfor", "waittext", "readattribute", "readstyle", "readgeometry", "readvalue", "readtext", "readhtml", "countelements", "readtable", "readlinks", "readimages", "readmeta", "readforms", "readstorage", "highlight", "tablist", "windowlist", "tabsnapshot", "mapclicks", "verifyvisible", "verifyenabled", "resolvexpath", "a11ytree", "readvisible", "readertree", "detectlists", "detecttables", "readjson", "watchmutate", "waitquiet", "watchbanner", "detectinfinitescroll", "detectvirtual", "detectlazy", "readscrollpos", "readlang", "readoutline", "countpages", "listshadow", "listframes", "classifypage", "fingerprintsection", "diffsnapshots", "readselection", "watchfocus", "detectsticky", "detectscrolllock", "readopengraph", "detectlanguage", "deriveselector", "waitload", "waiturl", "spawait", "detecthttp", "readredirects", "readfinalurl", "trailaudit", "navintent", "checksafe", "querytabs", "watchtab", "findclones", "searchtabs", "listaudio", "snapshotsession", "savelayout", "attachmeta", "detectfields", "generatevalues", "saveprofiles", "asksubmit", "readerrors", "skiphoneypot", "detectlogin", "detecttemplate", "handoffcaptcha", "scrapetable", "importcsv", "looprows", "transformvalues", "deduperows", "mergepages", "stamplerows", "previewgrid", "logprovenance", "verifydownload", "exportnetlog", "namecaptures", "shotview", "shotfullpage", "shotelement", "shotregion", "contactsheet", "capturepdf", "captureframe", "readmedia", "readassets", "probestream", "timelapse", "shotcanvas", "convertimage", "makethumbs"]);
1663
+ var sensitiveactions = /* @__PURE__ */ new Set(["click", "type", "navigate", "select", "presskey", "drag", "drop", "upload", "clear", "check", "uncheck", "toggle", "submit", "reload", "back", "forward", "writestorage", "setattribute", "removeattribute", "evaluate", "tabcreate", "tabactivate", "tabclose", "tabreload", "windowcreate", "windowclose", "windowresize", "downloadfile", "clickpoint", "shiftclick", "dismissdialog", "enterframe", "typetime", "appendtext", "setvalue", "typeedit", "keyhold", "keyrelease", "submitsearch", "selectmulti", "chooseradio", "setslider", "setdate", "setcolor", "openlink", "openprivate", "reloadcache", "stopnav", "followlink", "spanav", "rewritequery", "setfragment", "navlist", "navprofile", "handleauth", "printpdf", "prefetch", "preconnect", "deeplink", "reopentab", "pausenav", "navrate", "openclipboard", "batchopen", "duplicatetab", "closepattern", "pintab", "mutetab", "movetab", "movetabwindow", "grouptabs", "colorgroup", "collapsegroup", "discardtab", "reloadtabs", "zoomin", "zoomout", "switchtab", "maximizewindow", "minimizewindow", "restorewindow", "focuswindow", "scratchwindow", "incognitowindow", "restoretab", "restorelayout", "reopenrun", "badgetab", "fillform", "filllabel", "fillplaceholder", "submitform", "retryform", "runwizard", "selectchain", "picktypeahead", "pickdate", "attachfile", "fillcard", "fillcode", "consentpassword", "exportcsv", "exportjson", "exportexcel", "copytable", "pushsheets", "streamdisk", "paginateextract", "resumeextract", "batchdownload", "pausedownload", "resumedownload", "interceptmime", "readclipboard", "writeclipboard", "copyscreen", "quarantinedownload", "scanvirus", "cleanupartifacts", "recordscreen", "captureaudio", "downloadimages", "callrest", "callgraphql", "sendmessage"]);
1664
+ var interactionactions = /* @__PURE__ */ new Set(["focus", "scroll", "hover", "clickdeep", "rightclick", "doubleclick", "scrollpage", "scrollby", "scrollend", "scrolltop", "fullscreen", "zoomset", "movepointer", "clicktext", "clickaria", "clickname", "expanddetails", "pierceshadow", "retryaction", "capturebodies"]);
1665
+ var readactions = /* @__PURE__ */ new Set(["observe", "inspect", "extract", "wait", "waitfor", "waittext", "readattribute", "readstyle", "readgeometry", "readvalue", "readtext", "readhtml", "countelements", "readtable", "readlinks", "readimages", "readmeta", "readforms", "readstorage", "highlight", "tablist", "windowlist", "tabsnapshot", "mapclicks", "verifyvisible", "verifyenabled", "resolvexpath", "a11ytree", "readvisible", "readertree", "detectlists", "detecttables", "readjson", "watchmutate", "waitquiet", "watchbanner", "detectinfinitescroll", "detectvirtual", "detectlazy", "readscrollpos", "readlang", "readoutline", "countpages", "listshadow", "listframes", "classifypage", "fingerprintsection", "diffsnapshots", "readselection", "watchfocus", "detectsticky", "detectscrolllock", "readopengraph", "detectlanguage", "deriveselector", "waitload", "waiturl", "spawait", "detecthttp", "readredirects", "readfinalurl", "trailaudit", "navintent", "checksafe", "querytabs", "watchtab", "findclones", "searchtabs", "listaudio", "snapshotsession", "savelayout", "attachmeta", "detectfields", "generatevalues", "saveprofiles", "asksubmit", "readerrors", "skiphoneypot", "detectlogin", "detecttemplate", "handoffcaptcha", "scrapetable", "importcsv", "looprows", "transformvalues", "deduperows", "mergepages", "stamplerows", "previewgrid", "logprovenance", "verifydownload", "exportnetlog", "namecaptures", "shotview", "shotfullpage", "shotelement", "shotregion", "contactsheet", "capturepdf", "captureframe", "readmedia", "readassets", "probestream", "timelapse", "shotcanvas", "convertimage", "makethumbs", "fetchurl", "parsejson", "parsehtml", "opensocket", "waitmessage", "watchrequests", "readheaders", "mapapi", "subscribesse", "longpoll", "extractapi"]);
853
1666
  var allowedactions = /* @__PURE__ */ new Set([...sensitiveactions, ...interactionactions, ...readactions]);
854
1667
  var watchactions = /* @__PURE__ */ new Set(["watchmutate", "watchbanner", "watchfocus", "watchtab"]);
855
1668
  var targetactions = /* @__PURE__ */ new Set(["inspect", "focus", "click", "type", "scroll", "select", "hover", "clickdeep", "rightclick", "doubleclick", "drag", "drop", "upload", "clear", "check", "uncheck", "toggle", "submit", "readattribute", "readstyle", "readgeometry", "readvalue", "readtext", "readhtml", "countelements", "readtable", "highlight", "setattribute", "removeattribute", "waitfor", "shiftclick", "typetime", "appendtext", "setvalue", "typeedit", "submitsearch", "selectmulti", "chooseradio", "setslider", "setdate", "setcolor", "expanddetails", "verifyvisible", "verifyenabled", "pierceshadow", "deriveselector", "fingerprintsection", "submitform", "retryform", "selectchain", "picktypeahead", "pickdate", "attachfile", "fillcode", "consentpassword", "scrapetable", "paginateextract", "shotelement", "captureframe", "shotcanvas"]);
@@ -861,6 +1674,10 @@ var exportactions = /* @__PURE__ */ new Set(["exportcsv", "exportjson", "exporte
861
1674
  var filesactions = /* @__PURE__ */ new Set(["batchdownload", "pausedownload", "resumedownload", "verifydownload", "interceptmime", "exportnetlog", "readclipboard", "writeclipboard", "copyscreen", "quarantinedownload", "scanvirus", "namecaptures", "cleanupartifacts"]);
862
1675
  var captureactions = /* @__PURE__ */ new Set(["shotview", "shotfullpage", "shotelement", "shotregion", "contactsheet"]);
863
1676
  var mediaactions = /* @__PURE__ */ new Set(["capturepdf", "recordscreen", "captureaudio", "captureframe", "downloadimages", "shotcanvas", "probestream", "readmedia", "readassets", "timelapse", "convertimage", "makethumbs"]);
1677
+ var httpactions = /* @__PURE__ */ new Set(["fetchurl", "parsejson", "parsehtml", "callrest", "callgraphql"]);
1678
+ var socketactions = /* @__PURE__ */ new Set(["opensocket", "sendmessage", "waitmessage", "subscribesse", "longpoll"]);
1679
+ var netwatchactions = /* @__PURE__ */ new Set(["watchrequests", "readheaders", "capturebodies", "mapapi", "extractapi"]);
1680
+ var credentialheaders = /* @__PURE__ */ new Set(["authorization", "proxy-authorization", "cookie", "cookie2", "set-cookie", "api-key", "x-api-key", "x-auth-token", "x-session-token", "proxy-authorization"]);
864
1681
  var fieldkinds = ["text", "email", "phone", "date", "number", "select", "check", "radio", "file", "password", "card", "code"];
865
1682
  var layoutmutationactions = /* @__PURE__ */ new Set(["grouptabs", "colorgroup", "collapsegroup", "savelayout", "restorelayout"]);
866
1683
  var groupcolors = ["grey", "blue", "red", "yellow", "green", "pink", "purple", "cyan", "orange"];
@@ -1590,9 +2407,391 @@ function validatetabsgrammar(step, options) {
1590
2407
  function ismediakind(kind) {
1591
2408
  return mediaactions.has(kind);
1592
2409
  }
2410
+ function ishttpkind(kind) {
2411
+ return httpactions.has(kind);
2412
+ }
2413
+ function issocketkind(kind) {
2414
+ return socketactions.has(kind);
2415
+ }
2416
+ function isnetwatchkind(kind) {
2417
+ return netwatchactions.has(kind);
2418
+ }
2419
+ function resolvedrisk(step) {
2420
+ if (step.kind === "capturebodies") {
2421
+ let options = {};
2422
+ try {
2423
+ options = parseoptions(step);
2424
+ } catch {
2425
+ options = {};
2426
+ }
2427
+ const body = options.body;
2428
+ const mimes = body && typeof body === "object" && !Array.isArray(body) ? body.mimes : void 0;
2429
+ if (Array.isArray(mimes) && mimes.some((mime) => typeof mime === "string" && privatemime(mime))) return "sensitive";
2430
+ return "interaction";
2431
+ }
2432
+ if (step.kind === "extractapi") {
2433
+ let options = {};
2434
+ try {
2435
+ options = parseoptions(step);
2436
+ } catch {
2437
+ options = {};
2438
+ }
2439
+ const replay = options.replay;
2440
+ const verb = replay && typeof replay === "object" && !Array.isArray(replay) ? replay.verb : void 0;
2441
+ if (typeof verb === "string" && !["GET", "HEAD", "OPTIONS"].includes(verb.trim().toUpperCase())) return "sensitive";
2442
+ return "read";
2443
+ }
2444
+ return actionrisk(step.kind);
2445
+ }
2446
+ function socketgate(session, url) {
2447
+ let parsed;
2448
+ try {
2449
+ parsed = new URL(url);
2450
+ } catch {
2451
+ return { allowed: false, reason: "The channel needs a valid url before it can be reviewed." };
2452
+ }
2453
+ if (parsed.protocol !== "wss:" && parsed.protocol !== "https:") return { allowed: false, reason: "Channels use wss websocket urls or https event stream urls only." };
2454
+ if (parsed.username || parsed.password) return { allowed: false, reason: "Channel credentials are not allowed in the url." };
2455
+ const origin = channelorigin(url);
2456
+ if (!origingranted(session, origin)) return { allowed: false, reason: `The channel to ${origin} stays outside the session origin grants.` };
2457
+ return { allowed: true };
2458
+ }
2459
+ function watchgate(session, settings, now) {
2460
+ if (!session || session.stoppedat) return { allowed: false, reason: "No active browser session exists for the request watch." };
2461
+ if (session.expiresat <= now) return { allowed: false, reason: "The browser session has expired and cannot watch requests." };
2462
+ if (session.pausedat) return { allowed: false, reason: "The browser session is paused and cannot watch requests." };
2463
+ if (settings?.webrequestgrant !== true) return { allowed: false, reason: "Request watching needs the webrequest grant in the review panel first; the observation derives from the page timing buffers and adds no manifest permission." };
2464
+ return { allowed: true };
2465
+ }
2466
+ function observedorigingranted(session, url) {
2467
+ let origin = "";
2468
+ try {
2469
+ origin = new URL(url).origin;
2470
+ } catch {
2471
+ return { allowed: false, reason: "The observed exchange url does not parse for an origin check." };
2472
+ }
2473
+ if (!origingranted(session, origin)) return { allowed: false, reason: `The observed origin ${origin} stays outside the session origin grants; grant it before reading headers, bodies or replays.` };
2474
+ return { allowed: true };
2475
+ }
2476
+ function origincheck(session, url) {
2477
+ let parsed;
2478
+ try {
2479
+ parsed = new URL(url);
2480
+ } catch {
2481
+ return { allowed: false, reason: "The outbound request needs a valid url before it can be reviewed." };
2482
+ }
2483
+ if (parsed.protocol !== "https:") return { allowed: false, reason: "Outbound requests use HTTPS urls only." };
2484
+ if (parsed.username || parsed.password) return { allowed: false, reason: "Endpoint credentials are not allowed in the url." };
2485
+ if (!origingranted(session, parsed.origin)) return { allowed: false, reason: `The outbound request to ${parsed.origin} stays outside the session origin grants.` };
2486
+ return { allowed: true };
2487
+ }
2488
+ function credentialheadername(name) {
2489
+ return credentialheaders.has(name.trim().toLowerCase());
2490
+ }
2491
+ function fetchconsentrefgranted(step) {
2492
+ let options = {};
2493
+ try {
2494
+ options = parseoptions(step);
2495
+ } catch {
2496
+ options = {};
2497
+ }
2498
+ const request = options.fetch;
2499
+ const headers = request && typeof request === "object" && !Array.isArray(request) ? request.headers : void 0;
2500
+ const names = headers && typeof headers === "object" && !Array.isArray(headers) ? Object.keys(headers) : [];
2501
+ if (names.length === 0) return { allowed: true };
2502
+ const empty = names.some((name) => !name.trim());
2503
+ if (empty) return { allowed: false, reason: "Header allowlists with empty names are refused." };
2504
+ const credential = names.find((name) => credentialheadername(name));
2505
+ if (credential !== void 0 && !isnonempty(options.consentref)) return { allowed: false, reason: `The credential bearing header ${credential} needs the explicit reviewed consent that names it before it is sent.` };
2506
+ if (!isnonempty(options.consentref)) return { allowed: false, reason: `The ${names.length} reviewed custom header${names.length === 1 ? "" : "s"} need a reviewed consent ref in options before any send.` };
2507
+ return { allowed: true };
2508
+ }
2509
+ function fetchconsentcovers(consent, origin, headernames, now) {
2510
+ if (consent.approved !== true) return false;
2511
+ if (consent.expiresat <= now) return false;
2512
+ if (consent.origin !== origin) return false;
2513
+ const covered = new Set(consent.headers.map((header) => header.name.trim().toLowerCase()));
2514
+ return headernames.every((name) => covered.has(name.trim().toLowerCase()));
2515
+ }
2516
+ function mutationcallof(step) {
2517
+ let options = {};
2518
+ try {
2519
+ options = parseoptions(step);
2520
+ } catch {
2521
+ options = {};
2522
+ }
2523
+ if (step.kind === "callgraphql") {
2524
+ const request = options.graphql;
2525
+ return Boolean(request && typeof request === "object" && !Array.isArray(request) && request.operationkind === "mutation");
2526
+ }
2527
+ if (step.kind === "callrest") {
2528
+ const method = typeof options.method === "string" ? options.method.trim().toUpperCase() : void 0;
2529
+ if (method !== void 0) return !["GET", "HEAD", "OPTIONS"].includes(method);
2530
+ }
2531
+ return false;
2532
+ }
2533
+ function fetchbudgetallowed(timeout, retries, backoff, wait) {
2534
+ for (const [label, value] of [["timeout", timeout], ["retries", retries], ["backoff", backoff]]) {
2535
+ if (value !== void 0 && (typeof value !== "number" || !Number.isFinite(value) || value < 0)) return { allowed: false, reason: `The reviewed fetch ${label} must be zero or a positive number with no code ceiling.` };
2536
+ }
2537
+ if (wait !== void 0 && (typeof wait !== "number" || !Number.isFinite(wait) || wait < 0)) return { allowed: false, reason: "The reviewed fetch wait budget must be zero or a positive number of milliseconds." };
2538
+ if (wait === void 0 || timeout === void 0) return { allowed: true };
2539
+ const attempts = Math.max(1, Math.floor(retries ?? 0) + 1);
2540
+ const waits = (backoff ?? 0) * (attempts * (attempts - 1)) / 2;
2541
+ const worstcase = timeout * attempts + waits;
2542
+ if (worstcase > wait) return { allowed: false, reason: `The fetch worst case of ${worstcase} milliseconds exceeds the reviewed wait budget of ${wait} milliseconds; review a wider budget or fewer retries.` };
2543
+ return { allowed: true };
2544
+ }
2545
+ function outboundtarget(step) {
2546
+ let options = {};
2547
+ try {
2548
+ options = parseoptions(step);
2549
+ } catch {
2550
+ options = {};
2551
+ }
2552
+ const request = options.fetch;
2553
+ if (request && typeof request === "object" && !Array.isArray(request)) {
2554
+ const url = request.url;
2555
+ if (typeof url === "string" && url.trim()) return url.trim();
2556
+ }
2557
+ return void 0;
2558
+ }
2559
+ function validateendpointrecord(value) {
2560
+ if (!value || typeof value !== "object" || Array.isArray(value)) return { allowed: false, reason: "A reviewed endpoint record is required." };
2561
+ const record2 = value;
2562
+ if (!isnonempty(record2.name)) return { allowed: false, reason: "The endpoint record needs a reviewed non-empty name." };
2563
+ if (!isnonempty(record2.method)) return { allowed: false, reason: "The endpoint record needs a reviewed method." };
2564
+ if (!ishttpsurl(record2.url)) return { allowed: false, reason: "The endpoint record url template must be an HTTPS url." };
2565
+ if (record2.headers !== void 0) {
2566
+ if (!record2.headers || typeof record2.headers !== "object" || Array.isArray(record2.headers)) return { allowed: false, reason: "The endpoint header allowlist must be an object of reviewed headers." };
2567
+ for (const name of Object.keys(record2.headers)) {
2568
+ if (!name.trim()) return { allowed: false, reason: "Endpoint header allowlists with empty names are refused." };
2569
+ const headervalue = record2.headers[name];
2570
+ if (typeof headervalue !== "string") return { allowed: false, reason: `The endpoint header ${name} needs a reviewed string value.` };
2571
+ }
2572
+ }
2573
+ const schema = record2.schema;
2574
+ if (!schema || typeof schema !== "object" || Array.isArray(schema)) return { allowed: false, reason: "Every typed endpoint call needs a reviewed payload schema; endpoint records without schemas are refused." };
2575
+ const fields = schema.fields;
2576
+ if (!Array.isArray(fields) || fields.length === 0) return { allowed: false, reason: "The endpoint payload schema needs a non-empty field list." };
2577
+ for (const item of fields) {
2578
+ if (!item || typeof item !== "object" || Array.isArray(item)) return { allowed: false, reason: "Every payload schema field must be an object." };
2579
+ const field = item;
2580
+ if (!isnonempty(field.name)) return { allowed: false, reason: "Every payload schema field needs a non-empty name." };
2581
+ if (field.kind !== "string" && field.kind !== "number" && field.kind !== "boolean") return { allowed: false, reason: `The payload schema field ${field.name} must be a string, number or boolean kind.` };
2582
+ if (field.required !== void 0 && typeof field.required !== "boolean") return { allowed: false, reason: `The payload schema field ${field.name} required flag must be a boolean.` };
2583
+ if (field.default !== void 0 && typeof field.default !== "string" && typeof field.default !== "number" && typeof field.default !== "boolean") return { allowed: false, reason: `The payload schema field ${field.name} default must match its kind.` };
2584
+ }
2585
+ return { allowed: true };
2586
+ }
2587
+ function validpath(path) {
2588
+ return path.split(".").every((segment) => /^[A-Za-z0-9_-]+$/.test(segment));
2589
+ }
2590
+ function validatehttpgrammar(step, options) {
2591
+ const kind = step.kind;
2592
+ if (kind === "fetchurl") {
2593
+ const request = options.fetch;
2594
+ if (!request || typeof request !== "object" || Array.isArray(request)) return { allowed: false, reason: "A reviewed fetch request with a url is required in options.fetch." };
2595
+ const fetchrequest = request;
2596
+ if (typeof fetchrequest.url !== "string" || !fetchrequest.url.trim()) return { allowed: false, reason: "The reviewed fetch request needs a non-empty url." };
2597
+ if (fetchrequest.method !== void 0 && (typeof fetchrequest.method !== "string" || !["GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"].includes(fetchrequest.method.trim().toUpperCase()))) return { allowed: false, reason: "The reviewed fetch method must be a known HTTP verb." };
2598
+ if (fetchrequest.headers !== void 0) {
2599
+ if (!fetchrequest.headers || typeof fetchrequest.headers !== "object" || Array.isArray(fetchrequest.headers)) return { allowed: false, reason: "The reviewed header allowlist must be an object of custom headers." };
2600
+ for (const name of Object.keys(fetchrequest.headers)) {
2601
+ if (!name.trim()) return { allowed: false, reason: "Header allowlists with empty names are refused." };
2602
+ if (typeof fetchrequest.headers[name] !== "string") return { allowed: false, reason: `The reviewed header ${name} needs a string value.` };
2603
+ }
2604
+ }
2605
+ if (fetchrequest.body !== void 0 && typeof fetchrequest.body !== "string") return { allowed: false, reason: "The reviewed fetch body must be a string." };
2606
+ if (fetchrequest.mode !== void 0 && fetchrequest.mode !== "cors" && fetchrequest.mode !== "no-cors" && fetchrequest.mode !== "same-origin") return { allowed: false, reason: "The reviewed fetch mode must be cors, no-cors or same-origin." };
2607
+ const consentgate = fetchconsentrefgranted(step);
2608
+ if (!consentgate.allowed) return consentgate;
2609
+ const policycheck = validatefetchoptions(options.fetchoptions);
2610
+ if (!policycheck.allowed) return policycheck;
2611
+ const fetchpolicy = fetchoptionsvalues(options.fetchoptions);
2612
+ const budget = fetchbudgetallowed(fetchpolicy.timeout, fetchpolicy.retries, fetchpolicy.backoff, fetchnumeric(options, "wait"));
2613
+ if (!budget.allowed) return budget;
2614
+ if (options.stream !== void 0) {
2615
+ if (!options.stream || typeof options.stream !== "object" || Array.isArray(options.stream)) return { allowed: false, reason: "The reviewed stream window must be an object with an optional byte budget." };
2616
+ const streambudget = options.stream.budget;
2617
+ if (streambudget !== void 0 && (typeof streambudget !== "number" || !Number.isFinite(streambudget) || streambudget < 0)) return { allowed: false, reason: "The reviewed stream byte budget must be zero or a positive number of bytes with no code ceiling." };
2618
+ }
2619
+ }
2620
+ if (kind === "parsejson") {
2621
+ if (!isnonempty(options.call)) return { allowed: false, reason: "A reviewed stored call id is required in options.call before the body parses." };
2622
+ const fields = options.fields;
2623
+ if (!Array.isArray(fields) || fields.length === 0) return { allowed: false, reason: "A reviewed non-empty list of json path rules is required in options.fields." };
2624
+ for (const item of fields) {
2625
+ if (!item || typeof item !== "object" || Array.isArray(item)) return { allowed: false, reason: "Every json path rule must be an object." };
2626
+ const rule = item;
2627
+ if (!isnonempty(rule.name)) return { allowed: false, reason: "Every json path rule needs a non-empty field name." };
2628
+ if (typeof rule.path !== "string" || !rule.path.trim() || !validpath(rule.path.trim())) return { allowed: false, reason: `The json path of ${rule.name} must be a dotted path of non-empty segments.` };
2629
+ if (rule.kind !== void 0 && rule.kind !== "text" && rule.kind !== "number" && rule.kind !== "boolean" && rule.kind !== "json") return { allowed: false, reason: `The json path kind of ${rule.name} must be text, number, boolean or json.` };
2630
+ }
2631
+ }
2632
+ if (kind === "parsehtml") {
2633
+ if (!isnonempty(options.call)) return { allowed: false, reason: "A reviewed stored call id is required in options.call before the markup parses." };
2634
+ const queries = options.queries;
2635
+ if (!Array.isArray(queries) || queries.length === 0) return { allowed: false, reason: "A reviewed non-empty list of html queries is required in options.queries." };
2636
+ for (const item of queries) {
2637
+ if (!item || typeof item !== "object" || Array.isArray(item)) return { allowed: false, reason: "Every html query must be an object." };
2638
+ const query = item;
2639
+ if (!isnonempty(query.selector)) return { allowed: false, reason: "Every html query needs a selector from the reviewed selector grammar." };
2640
+ if (query.attribute !== void 0 && !isnonempty(query.attribute)) return { allowed: false, reason: "The reviewed html query attribute must be a non-empty attribute name." };
2641
+ if (query.multi !== void 0 && typeof query.multi !== "boolean") return { allowed: false, reason: "The reviewed html query multi flag must be a boolean." };
2642
+ }
2643
+ }
2644
+ if (kind === "callrest" || kind === "callgraphql") {
2645
+ if (!isnonempty(options.endpoint)) return { allowed: false, reason: "A reviewed typed endpoint name is required in options.endpoint." };
2646
+ if (kind === "callrest") {
2647
+ if (options.payload !== void 0 && (!options.payload || typeof options.payload !== "object" || Array.isArray(options.payload))) return { allowed: false, reason: "The reviewed rest payload must be an object of reviewed values." };
2648
+ if (options.method !== void 0 && (typeof options.method !== "string" || !["GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"].includes(options.method.trim().toUpperCase()))) return { allowed: false, reason: "The reviewed endpoint method override must be a known HTTP verb." };
2649
+ if (options.success !== void 0 && (!Array.isArray(options.success) || !options.success.every((code) => typeof code === "number" && Number.isInteger(code)))) return { allowed: false, reason: "The reviewed success status list must be a list of integer status codes." };
2650
+ }
2651
+ if (kind === "callgraphql") {
2652
+ const request = options.graphql;
2653
+ if (!request || typeof request !== "object" || Array.isArray(request)) return { allowed: false, reason: "A reviewed graphql request with an operation is required in options.graphql." };
2654
+ const graphql = request;
2655
+ if (typeof graphql.query !== "string" || !graphql.query.trim()) return { allowed: false, reason: "The reviewed graphql operation text must be a non-empty string." };
2656
+ if (graphql.operationkind !== "query" && graphql.operationkind !== "mutation") return { allowed: false, reason: "The reviewed graphql operation kind must be query or mutation; unknown operation kinds are refused." };
2657
+ if (graphql.variables !== void 0 && (!graphql.variables || typeof graphql.variables !== "object" || Array.isArray(graphql.variables))) return { allowed: false, reason: "The reviewed graphql variables must be an object of reviewed values." };
2658
+ if (graphql.operationname !== void 0 && !isnonempty(graphql.operationname)) return { allowed: false, reason: "The reviewed graphql operation name must be a non-empty string." };
2659
+ }
2660
+ if (options.apikeys !== void 0 && (!Array.isArray(options.apikeys) || !options.apikeys.every((name) => isnonempty(name)))) return { allowed: false, reason: "The reviewed api key reference list must be a list of non-empty stored names." };
2661
+ const policycheck = validatefetchoptions(options.fetchoptions);
2662
+ if (!policycheck.allowed) return policycheck;
2663
+ const fetchpolicy = fetchoptionsvalues(options.fetchoptions);
2664
+ const budget = fetchbudgetallowed(fetchpolicy.timeout, fetchpolicy.retries, fetchpolicy.backoff, fetchnumeric(options, "wait"));
2665
+ if (!budget.allowed) return budget;
2666
+ }
2667
+ return { allowed: true };
2668
+ }
2669
+ function validatefetchoptions(value) {
2670
+ if (value === void 0) return { allowed: true };
2671
+ if (!value || typeof value !== "object" || Array.isArray(value)) return { allowed: false, reason: "The reviewed fetch options must be an object with timeout, retries, backoff and follow." };
2672
+ const options = value;
2673
+ for (const key of ["timeout", "backoff"]) {
2674
+ if (options[key] !== void 0 && (typeof options[key] !== "number" || !Number.isFinite(options[key]) || options[key] < 0)) return { allowed: false, reason: `The reviewed fetch ${key} must be zero or a positive number with no code ceiling.` };
2675
+ }
2676
+ for (const key of ["retries", "follow"]) {
2677
+ if (options[key] !== void 0 && (typeof options[key] !== "number" || !Number.isInteger(options[key]) || options[key] < 0)) return { allowed: false, reason: `The reviewed fetch ${key} must be zero or a positive integer with no code ceiling.` };
2678
+ }
2679
+ return { allowed: true };
2680
+ }
2681
+ function fetchoptionsvalues(value) {
2682
+ if (!value || typeof value !== "object" || Array.isArray(value)) return {};
2683
+ const options = value;
2684
+ return { timeout: fetchnumeric(options, "timeout"), retries: fetchnumeric(options, "retries"), backoff: fetchnumeric(options, "backoff") };
2685
+ }
2686
+ function fetchnumeric(options, key) {
2687
+ const value = options[key];
2688
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
2689
+ }
1593
2690
  function isrecordingkind(kind) {
1594
2691
  return kind === "recordscreen" || kind === "captureaudio";
1595
2692
  }
2693
+ function validatesocketgrammar(step, options) {
2694
+ const kind = step.kind;
2695
+ if (kind === "opensocket") {
2696
+ const channel = channeloptionsof(options.socket);
2697
+ if (!channel) return { allowed: false, reason: "A reviewed socket with a url is required in options.socket." };
2698
+ if (channel.options.reconnect !== void 0 && !Number.isInteger(channel.options.reconnect)) return { allowed: false, reason: "The reviewed socket reconnect budget must be an integer attempt count with no code ceiling." };
2699
+ for (const label of ["backoff", "backoffceiling"]) {
2700
+ const value = channel.options[label];
2701
+ if (value !== void 0 && (typeof value !== "number" || !Number.isFinite(value) || value < 0)) return { allowed: false, reason: `The reviewed socket ${label} must be zero or a positive number of milliseconds with no code ceiling.` };
2702
+ }
2703
+ if (channel.options.lifetime !== void 0 && (typeof channel.options.lifetime !== "number" || !Number.isFinite(channel.options.lifetime) || channel.options.lifetime <= 0)) return { allowed: false, reason: "The reviewed socket lifetime window must be a positive number of milliseconds." };
2704
+ }
2705
+ if (kind === "sendmessage") {
2706
+ const message = options.message;
2707
+ if (!message || typeof message !== "object" || Array.isArray(message)) return { allowed: false, reason: "A reviewed message with a channel, stream and payload is required in options.message." };
2708
+ const envelope = message;
2709
+ if (!isnonempty(envelope.channel)) return { allowed: false, reason: "The reviewed message needs the open channel id in options.message.channel." };
2710
+ if (envelope.stream !== void 0 && !isnonempty(envelope.stream)) return { allowed: false, reason: "The reviewed message stream name must be a non-empty string." };
2711
+ if (typeof envelope.payload !== "string") return { allowed: false, reason: "The reviewed message payload must be a string." };
2712
+ }
2713
+ if (kind === "waitmessage") {
2714
+ if (options.filter !== void 0) {
2715
+ const filter = options.filter;
2716
+ if (!filter || typeof filter !== "object" || Array.isArray(filter)) return { allowed: false, reason: "The reviewed message filter must be an object of stream, path and limit." };
2717
+ const reviewed = filter;
2718
+ if (reviewed.stream !== void 0 && !isnonempty(reviewed.stream)) return { allowed: false, reason: "The reviewed message filter stream name must be a non-empty string." };
2719
+ if (reviewed.path !== void 0 && (typeof reviewed.path !== "string" || !validpath(reviewed.path.trim()))) return { allowed: false, reason: "The reviewed message filter path must be a dotted path of non-empty segments." };
2720
+ if (reviewed.limit !== void 0 && (typeof reviewed.limit !== "number" || !Number.isInteger(reviewed.limit) || reviewed.limit < 1)) return { allowed: false, reason: "The reviewed message match limit must be a positive integer with no code ceiling." };
2721
+ }
2722
+ if (options.wait !== void 0 && (typeof options.wait !== "number" || !Number.isFinite(options.wait) || options.wait < 0)) return { allowed: false, reason: "The reviewed message wait budget must be zero or a positive number of milliseconds." };
2723
+ }
2724
+ if (kind === "subscribesse") {
2725
+ const subscription = subscriptionoptionsof(options.subscription);
2726
+ if (!subscription) return { allowed: false, reason: "A reviewed subscription with an event stream url and a cancellation path is required in options.subscription." };
2727
+ const rawlifetime = options.subscription && typeof options.subscription === "object" && !Array.isArray(options.subscription) ? options.subscription.lifetime : void 0;
2728
+ if (rawlifetime !== void 0 && (typeof rawlifetime !== "number" || !Number.isFinite(rawlifetime) || rawlifetime <= 0)) return { allowed: false, reason: "The reviewed subscription lifetime window must be a positive number of milliseconds." };
2729
+ }
2730
+ if (kind === "longpoll") {
2731
+ const cursor = pollcursorof(options.poll);
2732
+ if (!cursor) return { allowed: false, reason: "A reviewed poll cursor with a url, cursor field, interval and stop condition is required in options.poll." };
2733
+ const wait = options.wait;
2734
+ if (wait !== void 0 && (typeof wait !== "number" || !Number.isFinite(wait) || wait < 0)) return { allowed: false, reason: "The reviewed long poll wait budget must be zero or a positive number of milliseconds." };
2735
+ if (wait !== void 0 && cursor.interval > wait) return { allowed: false, reason: `The long poll interval of ${cursor.interval} milliseconds exceeds the reviewed wait budget of ${wait} milliseconds; review a wider budget or a shorter interval.` };
2736
+ }
2737
+ return { allowed: true };
2738
+ }
2739
+ function validatenetwatchgrammar(step, options) {
2740
+ const kind = step.kind;
2741
+ if (kind === "watchrequests") {
2742
+ if (options.watch !== void 0) {
2743
+ const watch = options.watch;
2744
+ if (!watch || typeof watch !== "object" || Array.isArray(watch)) return { allowed: false, reason: "The reviewed watch window must be an object." };
2745
+ const reviewed = watch;
2746
+ if (reviewed.window !== void 0 && (typeof reviewed.window !== "number" || !Number.isFinite(reviewed.window) || reviewed.window < 0)) return { allowed: false, reason: "The reviewed watch window must be zero or a positive number of milliseconds." };
2747
+ }
2748
+ if (options.limit !== void 0 && (typeof options.limit !== "number" || !Number.isInteger(options.limit) || options.limit < 1)) return { allowed: false, reason: "The reviewed watch match limit must be a positive integer with no code ceiling." };
2749
+ }
2750
+ if (kind === "readheaders") {
2751
+ const headers = options.headers;
2752
+ if (!headers || typeof headers !== "object" || Array.isArray(headers)) return { allowed: false, reason: "A reviewed header filter with a name allowlist and a redaction list is required in options.headers." };
2753
+ const reviewed = headers;
2754
+ if (!Array.isArray(reviewed.allow) || reviewed.allow.length === 0 || !reviewed.allow.every((name) => isnonempty(name))) return { allowed: false, reason: "The reviewed header allowlist must be a non-empty list of header names." };
2755
+ if (!Array.isArray(reviewed.redact) || reviewed.redact.length === 0 || !reviewed.redact.every((name) => isnonempty(name))) return { allowed: false, reason: "Header capture requires a reviewed redaction list before any header value is stored." };
2756
+ }
2757
+ if (kind === "capturebodies") {
2758
+ const body = options.body;
2759
+ if (!body || typeof body !== "object" || Array.isArray(body)) return { allowed: false, reason: "A reviewed body filter with a url pattern, mime list and byte ceiling is required in options.body." };
2760
+ const reviewed = body;
2761
+ if (reviewed.urlpattern !== void 0 && !isnonempty(reviewed.urlpattern)) return { allowed: false, reason: "The reviewed body url pattern must be a non-empty string." };
2762
+ if (reviewed.mimes !== void 0 && (!Array.isArray(reviewed.mimes) || reviewed.mimes.length === 0 || !reviewed.mimes.every((mime) => isnonempty(mime)))) return { allowed: false, reason: "The reviewed body mime list must be a non-empty list of mime types." };
2763
+ if (reviewed.ceiling !== void 0 && (typeof reviewed.ceiling !== "number" || !Number.isFinite(reviewed.ceiling) || reviewed.ceiling < 0)) return { allowed: false, reason: "The reviewed body byte ceiling must be zero or a positive number of bytes with no code ceiling." };
2764
+ }
2765
+ if (kind === "mapapi") {
2766
+ if (options.limit !== void 0 && (typeof options.limit !== "number" || !Number.isInteger(options.limit) || options.limit < 1)) return { allowed: false, reason: "The reviewed mapapi match limit must be a positive integer with no code ceiling." };
2767
+ }
2768
+ if (kind === "extractapi") {
2769
+ const replay = apireplayspecof(options.replay);
2770
+ if (!replay) return { allowed: false, reason: "A reviewed replay spec with an endpoint is required in options.replay." };
2771
+ if (!ishttpsurl(replay.endpoint)) return { allowed: false, reason: "The reviewed replay endpoint must be an HTTPS url." };
2772
+ if (replay.verb !== void 0 && !["GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"].includes(replay.verb)) return { allowed: false, reason: "The reviewed replay verb must be a known HTTP verb." };
2773
+ for (const path of replay.paths ?? []) {
2774
+ if (!validpath(path.trim())) return { allowed: false, reason: `The reviewed replay extraction path ${path} must be a dotted path of non-empty segments.` };
2775
+ }
2776
+ }
2777
+ return { allowed: true };
2778
+ }
2779
+ function sockettarget(step) {
2780
+ let options = {};
2781
+ try {
2782
+ options = parseoptions(step);
2783
+ } catch {
2784
+ options = {};
2785
+ }
2786
+ for (const key of ["socket", "subscription", "poll"]) {
2787
+ const value = options[key];
2788
+ if (value && typeof value === "object" && !Array.isArray(value)) {
2789
+ const url = value.url;
2790
+ if (typeof url === "string" && url.trim()) return url.trim();
2791
+ }
2792
+ }
2793
+ return void 0;
2794
+ }
1596
2795
  function mediagate(session, tabid2, origin, now) {
1597
2796
  if (!session || session.stoppedat) return { allowed: false, reason: "No active browser session exists for the media capture." };
1598
2797
  if (session.expiresat <= now) return { allowed: false, reason: "The browser session has expired and cannot capture media." };
@@ -1950,6 +3149,18 @@ function validatestep(step, origin) {
1950
3149
  const mediacheck = validatemediagrammar(step, options);
1951
3150
  if (!mediacheck.allowed) return mediacheck;
1952
3151
  }
3152
+ if (ishttpkind(step.kind)) {
3153
+ const httpcheck = validatehttpgrammar(step, options);
3154
+ if (!httpcheck.allowed) return httpcheck;
3155
+ }
3156
+ if (issocketkind(step.kind)) {
3157
+ const socketcheck = validatesocketgrammar(step, options);
3158
+ if (!socketcheck.allowed) return socketcheck;
3159
+ }
3160
+ if (isnetwatchkind(step.kind)) {
3161
+ const netwatchcheck = validatenetwatchgrammar(step, options);
3162
+ if (!netwatchcheck.allowed) return netwatchcheck;
3163
+ }
1953
3164
  if (step.kind === "tabcreate") {
1954
3165
  if (options.background !== void 0 && typeof options.background !== "boolean") return { allowed: false, reason: "The reviewed background flag must be a boolean." };
1955
3166
  if (options.window !== void 0 && (typeof options.window !== "number" || !Number.isInteger(options.window) || options.window < 0)) return { allowed: false, reason: "The reviewed target window id must be a non-negative integer." };
@@ -2029,6 +3240,41 @@ function canexecute(input) {
2029
3240
  const recordinggate = recordingconsentgranted(input.step);
2030
3241
  if (!recordinggate.allowed) return recordinggate;
2031
3242
  }
3243
+ if (ishttpkind(input.step.kind)) {
3244
+ const target = outboundtarget(input.step);
3245
+ if (target !== void 0) {
3246
+ const outboundgate = origincheck(input.session, target);
3247
+ if (!outboundgate.allowed) return outboundgate;
3248
+ }
3249
+ if (input.step.kind === "fetchurl" || input.step.kind === "callrest" || input.step.kind === "callgraphql") {
3250
+ const consentgate = fetchconsentrefgranted(input.step);
3251
+ if (!consentgate.allowed) return consentgate;
3252
+ }
3253
+ }
3254
+ if (issocketkind(input.step.kind)) {
3255
+ const channelurl = sockettarget(input.step);
3256
+ if (channelurl !== void 0) {
3257
+ const channelgate = socketgate(input.session, channelurl);
3258
+ if (!channelgate.allowed) return channelgate;
3259
+ }
3260
+ }
3261
+ if (input.step.kind === "watchrequests") {
3262
+ const watchgatecheck = watchgate(input.session, input.settings, now);
3263
+ if (!watchgatecheck.allowed) return watchgatecheck;
3264
+ }
3265
+ if (input.step.kind === "extractapi") {
3266
+ let replayoptions = {};
3267
+ try {
3268
+ replayoptions = parseoptions(input.step);
3269
+ } catch {
3270
+ replayoptions = {};
3271
+ }
3272
+ const replay = apireplayspecof(replayoptions.replay);
3273
+ if (replay !== void 0) {
3274
+ const replaygate = origincheck(input.session, replay.endpoint);
3275
+ if (!replaygate.allowed) return replaygate;
3276
+ }
3277
+ }
2032
3278
  if (input.step.kind === "openlink" || input.step.kind === "openprivate" || input.step.kind === "batchopen" || input.step.kind === "prefetch" || input.step.kind === "deeplink" || input.step.kind === "reopentab") {
2033
3279
  let options = {};
2034
3280
  try {
@@ -2155,9 +3401,39 @@ function recordmedia(progress, planid, stepid, media, now) {
2155
3401
  const outcome = { stepid, ok: true, summary: `Captured a ${media.kind} media record of ${media.scope} scope with ${media.bytes} character${media.bytes === 1 ? "" : "s"} of media data.`, details: { media }, at: now };
2156
3402
  return recordoutcome(base, planid, outcome, now);
2157
3403
  }
3404
+ function recordcall(progress, planid, stepid, entry, now) {
3405
+ const base = progress && progress.planid === planid ? progress : emptyprogress(planid, now);
3406
+ const outcome = { stepid, ok: entry.statusclass === "success", summary: `Outbound ${entry.method} ${entry.kind} call to ${entry.origin} ended in the ${entry.status} ${entry.statusclass} class after ${entry.retries} retr${entry.retries === 1 ? "y" : "ies"} and ${entry.bytes} byte${entry.bytes === 1 ? "" : "s"}.`, details: { call: entry }, at: now };
3407
+ return recordoutcome(base, planid, outcome, now);
3408
+ }
3409
+ function recordfetchretry(progress, planid, stepid, retry, now) {
3410
+ const base = progress && progress.planid === planid ? progress : emptyprogress(planid, now);
3411
+ const outcome = { stepid, ok: false, summary: `Fetch attempt ${retry.attempt} of ${retry.url} failed (${retry.reason}); retrying after a ${retry.wait} millisecond backoff.`, details: { fetchretry: retry }, at: now };
3412
+ return recordoutcome(base, planid, outcome, now);
3413
+ }
3414
+ function recordchannel(progress, planid, stepid, entry, now) {
3415
+ const base = progress && progress.planid === planid ? progress : emptyprogress(planid, now);
3416
+ const outcome = { stepid, ok: entry.state !== "failed", summary: `The ${entry.kind} channel of ${entry.url} is ${entry.state} after ${entry.sent} sent and ${entry.received} received message${entry.received === 1 ? "" : "s"}.`, details: { channel: entry }, at: now };
3417
+ return recordoutcome(base, planid, outcome, now);
3418
+ }
3419
+ function recordexchange(progress, planid, stepid, entry, now) {
3420
+ const base = progress && progress.planid === planid ? progress : emptyprogress(planid, now);
3421
+ const outcome = { stepid, ok: entry.errorclass === void 0 && entry.statusclass === "success", summary: `Observed the ${entry.method} request of ${entry.url} as exchange ${entry.correlationid} in the ${entry.status} ${entry.statusclass} class over ${entry.duration} millisecond${entry.duration === 1 ? "" : "s"}${entry.errorclass !== void 0 ? ` failing with the ${entry.errorclass} class` : ""}.`, details: { exchange: entry }, at: now };
3422
+ return recordoutcome(base, planid, outcome, now);
3423
+ }
3424
+ function recordevent(progress, planid, stepid, entry, now) {
3425
+ const base = progress && progress.planid === planid ? progress : emptyprogress(planid, now);
3426
+ const outcome = { stepid, ok: true, summary: `The event stream of ${entry.url} observed ${entry.events} ${entry.name || "message"} event${entry.events === 1 ? "" : "s"}${entry.lasteventid !== void 0 ? ` resuming from ${entry.lasteventid}` : ""}.`, details: { event: entry }, at: now };
3427
+ return recordoutcome(base, planid, outcome, now);
3428
+ }
3429
+ function recordpoll(progress, planid, stepid, entry, now) {
3430
+ const base = progress && progress.planid === planid ? progress : emptyprogress(planid, now);
3431
+ const outcome = { stepid, ok: true, summary: `Long poll ${entry.poll} returned the ${entry.status} status${entry.cursor !== void 0 ? ` at cursor ${entry.cursor}` : ""} and ${entry.stopped ? `stopped: ${entry.reason}` : "continues"}.`, details: { poll: entry }, at: now };
3432
+ return recordoutcome(base, planid, outcome, now);
3433
+ }
2158
3434
 
2159
3435
  // version.ts
2160
- var packageversion = "1.1.41";
3436
+ var packageversion = "1.1.43";
2161
3437
 
2162
3438
  // types.ts
2163
3439
  var protocolversion = packageversion;
@@ -2171,12 +3447,17 @@ function text(value, field) {
2171
3447
  if (typeof value !== "string" || !value.trim()) throw new Error(`${field} must be a non-empty string.`);
2172
3448
  return value.trim();
2173
3449
  }
2174
- function parseproposal(value, origin) {
3450
+ function parseproposal(value, origin, grants) {
2175
3451
  const root = record(value);
2176
3452
  if (root.version !== protocolversion) throw new Error("Unsupported protocol version.");
3453
+ const covered = grants !== void 0 && grants.length > 0 ? grants : [origin];
2177
3454
  const planinput = record(root.plan);
2178
3455
  const stepsinput = planinput.steps;
2179
3456
  if (!Array.isArray(stepsinput) || stepsinput.length === 0) throw new Error("A plan needs at least one step.");
3457
+ const createdat = Date.now();
3458
+ const expiresat = typeof planinput.expiresat === "number" ? planinput.expiresat : createdat + 10 * 60 * 1e3;
3459
+ if (expiresat <= createdat) throw new Error("Plan expiry must be in the future.");
3460
+ const planwindow = expiresat - createdat;
2180
3461
  const steps = stepsinput.map((input, index) => {
2181
3462
  const candidate = record(input);
2182
3463
  const kind = text(candidate.kind, `step ${index + 1} kind`);
@@ -2184,13 +3465,47 @@ function parseproposal(value, origin) {
2184
3465
  id: typeof candidate.id === "string" ? candidate.id : crypto.randomUUID(),
2185
3466
  kind,
2186
3467
  summary: text(candidate.summary, `step ${index + 1} summary`),
2187
- risk: actionrisk(kind),
3468
+ risk: resolvedrisk(stepof(kind, candidate, index)),
2188
3469
  ...typeof candidate.target === "string" ? { target: candidate.target } : {},
2189
3470
  ...typeof candidate.value === "string" ? { value: candidate.value } : {},
2190
3471
  ...typeof candidate.options === "string" ? { options: candidate.options } : {}
2191
3472
  };
2192
3473
  const evaluation = validatestep(step, origin);
2193
3474
  if (!evaluation.allowed) throw new Error(evaluation.reason);
3475
+ const target = outboundtarget(step);
3476
+ if (target !== void 0) {
3477
+ const granted = covered.some((pattern) => {
3478
+ try {
3479
+ return new URL(target).origin === new URL(pattern).origin;
3480
+ } catch {
3481
+ return false;
3482
+ }
3483
+ });
3484
+ if (!granted) throw new Error(`The fetch request to ${target} targets an origin outside the grants.`);
3485
+ }
3486
+ const channelurl = sockettarget(step);
3487
+ if (channelurl !== void 0) {
3488
+ const channeloriginvalue = channeloriginof(channelurl);
3489
+ const granted = covered.some((pattern) => {
3490
+ try {
3491
+ return new URL(channelurl).origin === new URL(pattern).origin || channeloriginvalue === new URL(pattern).origin;
3492
+ } catch {
3493
+ return false;
3494
+ }
3495
+ });
3496
+ if (!granted) throw new Error(`The channel to ${channelurl} targets an origin outside the grants.`);
3497
+ }
3498
+ let lifetime;
3499
+ try {
3500
+ const options = parseoptions(step);
3501
+ for (const key of ["socket", "subscription"]) {
3502
+ const value2 = options[key];
3503
+ if (value2 && typeof value2 === "object" && !Array.isArray(value2) && typeof value2.lifetime === "number") lifetime = value2.lifetime;
3504
+ }
3505
+ } catch {
3506
+ lifetime = void 0;
3507
+ }
3508
+ if (lifetime !== void 0 && lifetime > planwindow) throw new Error(`The channel lifetime of ${lifetime} milliseconds exceeds the reviewed plan window of ${planwindow} milliseconds.`);
2194
3509
  return step;
2195
3510
  });
2196
3511
  for (const step of steps) {
@@ -2203,8 +3518,6 @@ function parseproposal(value, origin) {
2203
3518
  const review = submitreviewgranted(steps, step.id);
2204
3519
  if (!review.allowed) throw new Error(review.reason);
2205
3520
  }
2206
- const createdat = Date.now();
2207
- const expiresat = typeof planinput.expiresat === "number" ? planinput.expiresat : createdat + 10 * 60 * 1e3;
2208
3521
  const plan = {
2209
3522
  id: typeof planinput.id === "string" ? planinput.id : crypto.randomUUID(),
2210
3523
  objective: text(planinput.objective, "objective"),
@@ -2214,14 +3527,24 @@ function parseproposal(value, origin) {
2214
3527
  expiresat,
2215
3528
  state: "pending"
2216
3529
  };
2217
- if (plan.expiresat <= createdat) throw new Error("Plan expiry must be in the future.");
2218
3530
  return { version: protocolversion, plan };
2219
3531
  }
3532
+ function stepof(kind, candidate, index) {
3533
+ return { id: typeof candidate.id === "string" ? candidate.id : `candidate${index + 1}`, kind, summary: typeof candidate.summary === "string" ? candidate.summary : "", risk: "read", ...typeof candidate.options === "string" ? { options: candidate.options } : {} };
3534
+ }
3535
+ function channeloriginof(url) {
3536
+ try {
3537
+ const parsed = new URL(url);
3538
+ return `${parsed.protocol === "wss:" ? "https:" : parsed.protocol}//${parsed.host}`;
3539
+ } catch {
3540
+ return "";
3541
+ }
3542
+ }
2220
3543
  function requestbody(input) {
2221
3544
  return JSON.stringify({ version: protocolversion, objective: input.objective, session: input.session, observation: input.observation, capabilities: input.capabilities });
2222
3545
  }
2223
3546
  function outcomeresponse(input) {
2224
- return JSON.stringify({ version: protocolversion, planid: input.plan.id, planstate: input.plan.state, outcome: input.outcome, ...input.resolvedtarget ? { resolvedtarget: input.resolvedtarget } : {}, ...input.capture ? { capture: input.capture } : {}, ...input.media ? { media: input.media } : {} });
3547
+ return JSON.stringify({ version: protocolversion, planid: input.plan.id, planstate: input.plan.state, outcome: input.outcome, ...input.resolvedtarget ? { resolvedtarget: input.resolvedtarget } : {}, ...input.capture ? { capture: input.capture } : {}, ...input.media ? { media: input.media } : {}, ...input.transport ? { transport: input.transport } : {}, ...input.network ? { network: input.network } : {} });
2225
3548
  }
2226
3549
  function mapresponse(input) {
2227
3550
  return JSON.stringify({ version: protocolversion, planid: input.plan.id, planstate: input.plan.state, map: input.map });
@@ -2289,6 +3612,17 @@ function capturereport(input) {
2289
3612
  function mediareport(input) {
2290
3613
  return { version: protocolversion, records: input.records, images: input.images };
2291
3614
  }
3615
+ function callsreport(input) {
3616
+ const calls = input.calls.map((call) => {
3617
+ const { body, ...metadata } = call;
3618
+ void body;
3619
+ return metadata;
3620
+ });
3621
+ return { version: protocolversion, calls };
3622
+ }
3623
+ function exchangesreport(input) {
3624
+ return { version: protocolversion, exchanges: input.exchanges, channels: input.channels, subscriptions: input.subscriptions, apimap: input.apimap };
3625
+ }
2292
3626
 
2293
3627
  // capture.ts
2294
3628
  var capturekinds = ["shotview", "shotfullpage", "shotelement", "shotregion", "contactsheet"];
@@ -3977,7 +5311,7 @@ function stepoptions2(step) {
3977
5311
  }
3978
5312
  async function refreshcapabilities() {
3979
5313
  const report = await readcapabilities();
3980
- const withmedia = { ...report, captures: [...capturekinds], media: [...mediakinds] };
5314
+ const withmedia = { ...report, captures: [...capturekinds], media: [...mediakinds], http: [...httpkinds], netwatch: [...socketkinds, ...netwatchkinds] };
3981
5315
  await memory.setcapabilities(withmedia);
3982
5316
  return withmedia;
3983
5317
  }
@@ -4103,7 +5437,7 @@ async function propose(objective, remote) {
4103
5437
  if (!config) throw new Error("Configure an approved HTTPS endpoint before requesting a remote proposal.");
4104
5438
  const response = await fetch(config.endpoint, { method: "POST", headers: { "content-type": "application/json" }, credentials: "omit", body: requestbody({ objective: objective.trim(), session, observation, capabilities }) });
4105
5439
  if (!response.ok) throw new Error(`Proposal endpoint returned ${response.status}.`);
4106
- plan = parseproposal(await response.json(), session.origin).plan;
5440
+ plan = parseproposal(await response.json(), session.origin, session.grants ?? [session.origin]).plan;
4107
5441
  }
4108
5442
  await memory.setplan(plan);
4109
5443
  await memory.setprogress(resetforplan(await memory.getprogress(), plan, Date.now()));
@@ -4149,6 +5483,12 @@ function stepauditkind(step, ok) {
4149
5483
  if (step.kind === "logprovenance") return "provenance";
4150
5484
  return "scrape";
4151
5485
  }
5486
+ if (issocketkind(step.kind)) return "socket";
5487
+ if (isnetwatchkind(step.kind)) {
5488
+ if (step.kind === "extractapi") return "replay";
5489
+ if (step.kind === "watchrequests") return "watch";
5490
+ return "observation";
5491
+ }
4152
5492
  if (formfillkinds.has(step.kind)) return "fill";
4153
5493
  if (pointerkinds.has(step.kind)) return "pointer";
4154
5494
  if (watchstepkinds.has(step.kind)) return "watch";
@@ -6458,6 +7798,587 @@ async function executemediastep(step, session, plan, tabid2, origin) {
6458
7798
  }
6459
7799
  throw new Error("Unsupported media kind.");
6460
7800
  }
7801
+ var activefetches = /* @__PURE__ */ new Map();
7802
+ var defaultconsentwindow = 10 * 60 * 1e3;
7803
+ async function livefetch(url, init, controller, window2, streamstate) {
7804
+ const response = await fetch(url, { method: init.method, headers: init.headers, ...init.body !== void 0 ? { body: init.body } : {}, ...init.mode !== void 0 ? { mode: init.mode } : {}, redirect: init.redirect, credentials: "omit", signal: controller.signal });
7805
+ const headers = {};
7806
+ response.headers.forEach((value, name) => {
7807
+ headers[name] = value;
7808
+ });
7809
+ let body = "";
7810
+ if (window2 !== void 0 && response.body !== null) {
7811
+ const reader = response.body.getReader();
7812
+ const decoder = new TextDecoder();
7813
+ const pulled = await readstream({ chunks: async () => {
7814
+ const { done, value } = await reader.read();
7815
+ return done ? void 0 : decoder.decode(value, { stream: true });
7816
+ }, window: window2 });
7817
+ if (streamstate) {
7818
+ streamstate.bytes = pulled.bytes;
7819
+ streamstate.chunks = pulled.chunks;
7820
+ streamstate.aborted = pulled.aborted;
7821
+ streamstate.reason = pulled.reason;
7822
+ }
7823
+ if (pulled.aborted) controller.abort();
7824
+ body = decoder.decode();
7825
+ } else {
7826
+ body = await response.text();
7827
+ }
7828
+ return { status: response.status, headers, body, ...response.redirected ? { redirected: true } : {} };
7829
+ }
7830
+ async function attachapikeys(names, origin) {
7831
+ const headers = {};
7832
+ const attached = [];
7833
+ const refs = await memory.getapikeys();
7834
+ for (const name of names) {
7835
+ const ref = refs.find((item) => item.name === name);
7836
+ if (!ref) throw new Error(`No stored api key reference matches ${name}.`);
7837
+ if (!ref.origins.includes(origin)) throw new Error(`The api key ${name} is not scoped to ${origin}.`);
7838
+ const secret = await memory.getsecret(ref.storageid);
7839
+ if (secret === void 0) throw new Error(`The api key ${name} has no stored secret; set it from the review panel first.`);
7840
+ headers[ref.header] = secret;
7841
+ attached.push(name);
7842
+ }
7843
+ return { headers, keys: attached };
7844
+ }
7845
+ async function executehttpstep(step, session, plan, tabid2, origin) {
7846
+ const options = stepoptions2(step);
7847
+ const extra = { ...session ? { sessionid: session.id } : {}, planid: plan.id, stepid: step.id };
7848
+ if (step.kind === "fetchurl") {
7849
+ const request = fetchrequestof(options.fetch);
7850
+ if (!request) throw new Error("A reviewed fetch request with a url is required in options.fetch.");
7851
+ const gate = origincheck(session, request.url);
7852
+ if (!gate.allowed) throw new Error(gate.reason ?? "The outbound request stays outside the session origin grants.");
7853
+ const callorigin = new URL(request.url).origin;
7854
+ const names = Object.keys(request.headers ?? {});
7855
+ if (names.length > 0) {
7856
+ const consents = await memory.getfetchconsents();
7857
+ const covering = consents.find((consent) => fetchconsentcovers(consent, callorigin, names, Date.now()));
7858
+ if (!covering) {
7859
+ const consentref = typeof options.consentref === "string" ? options.consentref : "";
7860
+ const pending = consents.find((consent) => consent.approved !== true && consent.origin === callorigin && names.every((name) => consent.headers.some((header) => header.name.toLowerCase() === name.toLowerCase())));
7861
+ const prompt = pending ?? { id: consentref || randomid(), origin: callorigin, headers: names.map((name) => ({ name, value: (request.headers ?? {})[name] ?? "" })), expiresat: Date.now() + defaultconsentwindow, at: Date.now() };
7862
+ await memory.setfetchconsent(prompt);
7863
+ await refreshbadge();
7864
+ await audit("consent", `Fetch consent prompt ${prompt.id} opened for ${callorigin} with the header name${names.length === 1 ? "" : "s"} ${names.join(", ")}; header values stay out of the audit trail and appear only in the review prompt.`, extra);
7865
+ return { ok: false, summary: `The fetch waits for your header consent approval${consentref ? ` under ref ${consentref}` : ""}; approve it in the review panel and run the step again.`, details: { fetchconsent: { id: prompt.id, origin: callorigin, headers: prompt.headers, stepid: step.id, approved: false, expirywindow: prompt.expiresat - prompt.at } } };
7866
+ }
7867
+ }
7868
+ const policy = fetchoptionsof(options.fetchoptions);
7869
+ const callid = randomid();
7870
+ const controller = new AbortController();
7871
+ activefetches.set(callid, controller);
7872
+ const stream = options.stream !== void 0 ? streamwindowof(options.stream) : void 0;
7873
+ const streamstate = { bytes: 0, chunks: 0, aborted: false };
7874
+ const window2 = stream !== void 0 ? { ...stream, onchunk: (chunk, total) => {
7875
+ streamstate.chunks += 1;
7876
+ streamstate.bytes = total;
7877
+ } } : void 0;
7878
+ try {
7879
+ const transport = (url, init) => livefetch(url, init, controller, window2, streamstate);
7880
+ const result = await sendfetch({ request, ...policy !== void 0 && Object.keys(policy).length > 0 ? { options: policy } : {}, transport, onretry: (attempt, wait, reason) => {
7881
+ void (async () => {
7882
+ await memory.setprogress(recordfetchretry(await memory.getprogress(), plan.id, step.id, { attempt, url: request.url, wait, reason }, Date.now()));
7883
+ })().catch(() => {
7884
+ });
7885
+ } });
7886
+ const record2 = { id: callid, runid: plan.id, stepid: step.id, kind: "fetch", url: result.url, origin: new URL(result.url).origin, method: (request.method ?? "GET").toUpperCase(), status: result.status, statusclass: result.statusclass, duration: result.duration, retries: result.retries, bytes: result.bytes, headernames: names, body: result.body, ...streamstate.bytes > 0 ? { streambytes: streamstate.bytes } : {}, at: Date.now() };
7887
+ await memory.addcall(record2);
7888
+ await memory.setprogress(recordcall(await memory.getprogress(), plan.id, step.id, { id: record2.id, kind: record2.kind, origin: record2.origin, method: record2.method, status: record2.status, statusclass: record2.statusclass, duration: record2.duration, retries: record2.retries, bytes: record2.bytes }, Date.now()));
7889
+ await refreshbadge();
7890
+ await audit("call", `Fetched ${record2.method} ${record2.url} from the extension context ending in the ${record2.status} ${record2.statusclass} class after ${record2.retries} retr${record2.retries === 1 ? "y" : "ies"} with ${record2.bytes} body byte${record2.bytes === 1 ? "" : "s"}${names.length > 0 ? ` and ${names.length} consented header name${names.length === 1 ? "" : "s"}` : ""}; header values and body bytes stay out of the audit trail.`, extra);
7891
+ return { ok: true, summary: `Fetched ${record2.url} ending in the ${record2.status} ${record2.statusclass} class.`, details: { transport: { status: record2.status, headers: result.headernames, bytes: record2.bytes, duration: record2.duration }, call: record2.id, retries: record2.retries, redirects: result.redirects, ...streamstate.bytes > 0 ? { stream: { bytes: streamstate.bytes, chunks: streamstate.chunks, budget: window2?.budget, aborted: streamstate.aborted, ...streamstate.reason !== void 0 ? { reason: streamstate.reason } : {} } } : {}, credentialcall: names.some((name) => credentialheadername(name)) } };
7892
+ } finally {
7893
+ controller.abort();
7894
+ activefetches.delete(callid);
7895
+ }
7896
+ }
7897
+ if (step.kind === "parsejson") {
7898
+ const callid = typeof options.call === "string" ? options.call : "";
7899
+ const stored = await memory.getcall(callid);
7900
+ if (!stored) throw new Error(`No stored call matches ${callid}.`);
7901
+ if (stored.body === void 0 || stored.bodyexpired) throw new Error("The stored call body expired from the retention window; fetch it again before parsing.");
7902
+ let parsed;
7903
+ try {
7904
+ parsed = JSON.parse(stored.body);
7905
+ } catch {
7906
+ return { ok: false, summary: `The stored body of call ${callid} is not valid json.`, details: { call: callid, parseerror: true } };
7907
+ }
7908
+ const rules = jsonpathrulesof(options.fields);
7909
+ const fields = readpath(parsed, rules);
7910
+ const misses = fields.filter((field) => field.missing);
7911
+ await memory.addcall({ ...stored, fields });
7912
+ await audit("call", `Parsed the stored ${stored.method} body of ${stored.origin} into ${fields.length} named field${fields.length === 1 ? "" : "s"} with ${misses.length} path mis${misses.length === 1 ? "s" : "ses"} reported as outcomes.`, extra);
7913
+ return { ok: true, summary: `Extracted ${fields.length} field${fields.length === 1 ? "" : "s"} from the stored body${misses.length > 0 ? ` with ${misses.length} path mis${misses.length === 1 ? "s" : "ses"} filled by the reviewed defaults` : ""}.`, details: { call: callid, fields, misses: misses.map((field) => field.name), transport: { status: stored.status, headers: [], bytes: stored.bytes, duration: stored.duration } } };
7914
+ }
7915
+ if (step.kind === "parsehtml") {
7916
+ const callid = typeof options.call === "string" ? options.call : "";
7917
+ const stored = await memory.getcall(callid);
7918
+ if (!stored) throw new Error(`No stored call matches ${callid}.`);
7919
+ if (stored.body === void 0 || stored.bodyexpired) throw new Error("The stored call body expired from the retention window; fetch it again before parsing.");
7920
+ const queries = htmlqueriesof(options.queries);
7921
+ const results = await bridgecall(tabid2, "parsehtmlmarkup", stored.body, queries);
7922
+ await audit("call", `Parsed the stored ${stored.method} markup of ${stored.origin} through the page bridge domparser with ${queries.length} reviewed html quer${queries.length === 1 ? "y" : "ies"} over ${results.reduce((total, result) => total + result.count, 0)} matched element${results.reduce((total, result) => total + result.count, 0) === 1 ? "" : "s"}.`, extra);
7923
+ return { ok: true, summary: `Ran ${queries.length} html quer${queries.length === 1 ? "y" : "ies"} over the fetched markup.`, details: { call: callid, queries: results, transport: { status: stored.status, headers: [], bytes: stored.bytes, duration: stored.duration } } };
7924
+ }
7925
+ if (step.kind === "callrest" || step.kind === "callgraphql") {
7926
+ const endpointname = typeof options.endpoint === "string" ? options.endpoint : "";
7927
+ const stored = await memory.getendpoint(endpointname);
7928
+ if (!stored) throw new Error(`No stored typed endpoint matches ${endpointname}.`);
7929
+ if (!stored.schema) throw new Error("Every typed endpoint call needs a reviewed payload schema; store the endpoint with one from the review panel.");
7930
+ const policy = fetchoptionsof(options.fetchoptions);
7931
+ const callid = randomid();
7932
+ const controller = new AbortController();
7933
+ activefetches.set(callid, controller);
7934
+ try {
7935
+ const transport = (url, init) => livefetch(url, init, controller);
7936
+ const keynames = Array.isArray(options.apikeys) ? options.apikeys.filter((name) => typeof name === "string" && name.trim().length > 0) : [];
7937
+ if (step.kind === "callrest") {
7938
+ const methodoverride = typeof options.method === "string" ? options.method.trim().toUpperCase() : void 0;
7939
+ const endpoint = { ...stored, ...methodoverride !== void 0 ? { method: methodoverride } : {} };
7940
+ const payload = options.payload && typeof options.payload === "object" && !Array.isArray(options.payload) ? options.payload : {};
7941
+ const resolvedurl = templateurl(endpoint.url, payloadwithdefaults(payload, endpoint.schema));
7942
+ const urlgate2 = origincheck(session, resolvedurl);
7943
+ if (!urlgate2.allowed) throw new Error(urlgate2.reason ?? "The typed call stays outside the session origin grants.");
7944
+ const keys2 = keynames.length > 0 ? await attachapikeys(keynames, new URL(resolvedurl).origin) : { headers: {}, keys: [] };
7945
+ const headers2 = { ...endpoint.headers ?? {}, ...keys2.headers };
7946
+ const success = Array.isArray(options.success) ? options.success.filter((code) => typeof code === "number" && Number.isInteger(code)) : void 0;
7947
+ const result2 = await callrest({ endpoint: { ...endpoint, ...Object.keys(headers2).length > 0 ? { headers: headers2 } : {} }, payload, ...policy !== void 0 && Object.keys(policy).length > 0 ? { options: policy } : {}, transport, ...success !== void 0 ? { success } : {}, onretry: (attempt, wait, reason) => {
7948
+ void (async () => {
7949
+ await memory.setprogress(recordfetchretry(await memory.getprogress(), plan.id, step.id, { attempt, url: resolvedurl, wait, reason }, Date.now()));
7950
+ })().catch(() => {
7951
+ });
7952
+ } });
7953
+ const record3 = { id: callid, runid: plan.id, stepid: step.id, kind: "rest", url: result2.url, origin: new URL(result2.url).origin, method: endpoint.method.toUpperCase(), status: result2.transport.status, statusclass: result2.transport.statusclass, duration: result2.transport.duration, retries: result2.transport.retries, bytes: result2.transport.bytes, headernames: [...Object.keys(endpoint.headers ?? {}), ...keynames.map((name) => `apikey:${name}`)], endpoint: endpointname, body: result2.transport.body, ...result2.errors.length > 0 ? { errors: result2.errors } : {}, at: Date.now() };
7954
+ await memory.addcall(record3);
7955
+ await memory.setprogress(recordcall(await memory.getprogress(), plan.id, step.id, { id: record3.id, kind: record3.kind, origin: record3.origin, method: record3.method, status: record3.status, statusclass: record3.statusclass, duration: record3.duration, retries: record3.retries, bytes: record3.bytes }, Date.now()));
7956
+ await refreshbadge();
7957
+ await audit("call", `Called the typed rest endpoint ${endpointname} with ${record3.method} on ${record3.origin} ending in the ${record3.status} ${record3.statusclass} class after ${record3.retries} retr${record3.retries === 1 ? "y" : "ies"}${mutationcallof(step) ? " as a reviewed mutating verb" : ""}${keys2.keys.length > 0 ? ` with ${keys2.keys.length} attached api key reference${keys2.keys.length === 1 ? "" : "s"}` : ""}; payload values, header values and body bytes stay out of the audit trail.`, extra);
7958
+ return { ok: result2.ok, summary: `The typed rest call ${endpointname} ended in the ${record3.status} ${record3.statusclass} class.`, details: { transport: { status: record3.status, headers: result2.transport.headernames, bytes: record3.bytes, duration: record3.duration }, call: record3.id, endpoint: endpointname, payload: result2.payload, retries: record3.retries, ...result2.errors.length > 0 ? { errors: result2.errors } : {}, mutation: mutationcallof(step), credentialcall: keys2.keys.length > 0 } };
7959
+ }
7960
+ const request = graphqlrequestof(options.graphql);
7961
+ if (!request) throw new Error("A reviewed graphql request with an operation and its kind is required in options.graphql.");
7962
+ const urlgate = origincheck(session, stored.url);
7963
+ if (!urlgate.allowed) throw new Error(urlgate.reason ?? "The typed call stays outside the session origin grants.");
7964
+ const keys = keynames.length > 0 ? await attachapikeys(keynames, new URL(stored.url).origin) : { headers: {}, keys: [] };
7965
+ const headers = { ...stored.headers ?? {}, ...keys.headers };
7966
+ const result = await callgraphql({ endpoint: { ...stored, ...Object.keys(headers).length > 0 ? { headers } : {} }, request, ...policy !== void 0 && Object.keys(policy).length > 0 ? { options: policy } : {}, transport, onretry: (attempt, wait, reason) => {
7967
+ void (async () => {
7968
+ await memory.setprogress(recordfetchretry(await memory.getprogress(), plan.id, step.id, { attempt, url: stored.url, wait, reason }, Date.now()));
7969
+ })().catch(() => {
7970
+ });
7971
+ } });
7972
+ const record2 = { id: callid, runid: plan.id, stepid: step.id, kind: "graphql", url: result.transport.url, origin: new URL(result.transport.url).origin, method: "POST", status: result.transport.status, statusclass: result.transport.statusclass, duration: result.transport.duration, retries: result.transport.retries, bytes: result.transport.bytes, headernames: [...Object.keys(stored.headers ?? {}), ...keynames.map((name) => `apikey:${name}`)], endpoint: endpointname, body: result.transport.body, ...result.errors.length > 0 ? { errors: result.errors } : {}, at: Date.now() };
7973
+ await memory.addcall(record2);
7974
+ await memory.setprogress(recordcall(await memory.getprogress(), plan.id, step.id, { id: record2.id, kind: record2.kind, origin: record2.origin, method: record2.method, status: record2.status, statusclass: record2.statusclass, duration: record2.duration, retries: record2.retries, bytes: record2.bytes }, Date.now()));
7975
+ await refreshbadge();
7976
+ await audit("call", `Called the typed graphql endpoint ${endpointname} with a reviewed ${request.operationkind} on ${record2.origin} ending in the ${record2.status} ${record2.statusclass} class with ${result.errors.length} returned error${result.errors.length === 1 ? "" : "s"}${keys.keys.length > 0 ? ` and ${keys.keys.length} attached api key reference${keys.keys.length === 1 ? "" : "s"}` : ""}; variables, header values and body bytes stay out of the audit trail.`, extra);
7977
+ return { ok: result.errors.length === 0 && result.transport.statusclass === "success", summary: `The graphql ${request.operationkind} ended in the ${record2.status} ${record2.statusclass} class with ${result.errors.length} error${result.errors.length === 1 ? "" : "s"}.`, details: { transport: { status: record2.status, headers: result.transport.headernames, bytes: record2.bytes, duration: record2.duration }, call: record2.id, endpoint: endpointname, operationkind: request.operationkind, retries: record2.retries, ...result.errors.length > 0 ? { errors: result.errors } : {}, ...result.data !== void 0 ? { datapaths: result.data && typeof result.data === "object" && !Array.isArray(result.data) ? Object.keys(result.data) : [] } : {}, mutation: request.operationkind === "mutation", credentialcall: keys.keys.length > 0 } };
7978
+ } finally {
7979
+ controller.abort();
7980
+ activefetches.delete(callid);
7981
+ }
7982
+ }
7983
+ throw new Error("Unsupported network observation kind.");
7984
+ }
7985
+ var activesockets = /* @__PURE__ */ new Map();
7986
+ var channelbuses = /* @__PURE__ */ new Map();
7987
+ var netpoll = 100;
7988
+ function waitsome(milliseconds) {
7989
+ return new Promise((resolve) => setTimeout(resolve, Math.max(0, milliseconds)));
7990
+ }
7991
+ async function queueinboundmessage(channelid, payload) {
7992
+ const stored = await memory.getchannel(channelid);
7993
+ if (!stored) return;
7994
+ const bus = channelbuses.get(channelid) ?? { sequences: {}, queue: [] };
7995
+ const received = receivemessage(bus, channelid, "inbound", payload, Date.now());
7996
+ channelbuses.set(channelid, received.state);
7997
+ await memory.addmessage(received.envelope);
7998
+ await memory.addchannel({ ...stored, received: stored.received + 1 });
7999
+ await refreshbadge().catch(() => {
8000
+ });
8001
+ }
8002
+ function wirewebsocket(channelid, url, protocols, options, budget) {
8003
+ let settled = false;
8004
+ const socket = new WebSocket(url, protocols.length > 0 ? protocols : void 0);
8005
+ const active = activesockets.get(channelid);
8006
+ if (active) activesockets.set(channelid, { ...active, socket });
8007
+ void budget;
8008
+ socket.onmessage = (event) => {
8009
+ void queueinboundmessage(channelid, String(event.data)).catch(() => {
8010
+ });
8011
+ };
8012
+ socket.onopen = () => {
8013
+ void (async () => {
8014
+ const stored = await memory.getchannel(channelid);
8015
+ if (stored) await memory.addchannel({ ...stored, state: "open", openedat: stored.openedat || Date.now() });
8016
+ })().catch(() => {
8017
+ });
8018
+ };
8019
+ socket.onclose = (event) => {
8020
+ if (settled) return;
8021
+ settled = true;
8022
+ void (async () => {
8023
+ const stored = await memory.getchannel(channelid);
8024
+ if (!stored) return;
8025
+ if (stored.state !== "open") return;
8026
+ const remaining = Math.max(0, Math.floor(options.reconnect ?? 0) - budget.reconnects);
8027
+ if (remaining <= 0) {
8028
+ await memory.addchannel(closechannel(stored, Date.now(), `The websocket closed with code ${event.code}.`));
8029
+ activesockets.delete(channelid);
8030
+ channelbuses.delete(channelid);
8031
+ await audit("socket", `Channel ${channelid} of ${stored.origin} closed with code ${event.code} after ${stored.sent} sent and ${stored.received} received message${stored.received === 1 ? "" : "s"}.`, { stepid: stored.stepid });
8032
+ return;
8033
+ }
8034
+ const wait = reconnectwaits(1, options.backoff ?? 0, options.backoffceiling)[0] ?? 0;
8035
+ budget.reconnects += 1;
8036
+ await memory.addchannel({ ...stored, state: "connecting", reconnects: stored.reconnects + 1 });
8037
+ setTimeout(() => wirewebsocket(channelid, url, protocols, options, budget), wait);
8038
+ })().catch(() => {
8039
+ });
8040
+ };
8041
+ socket.onerror = () => {
8042
+ };
8043
+ void settled;
8044
+ }
8045
+ async function runsubscription(subscription, controller) {
8046
+ let buffer = "";
8047
+ let current = subscription;
8048
+ const startedat = Date.now();
8049
+ for (; ; ) {
8050
+ if (controller.signal.aborted) break;
8051
+ if (subscription.lifetime !== void 0 && Date.now() - startedat >= subscription.lifetime) break;
8052
+ try {
8053
+ const response = await fetch(subscription.url, { headers: sserequestheaders(current), credentials: "omit", signal: controller.signal });
8054
+ if (!response.body) throw new Error("The event stream returned no body.");
8055
+ const reader = response.body.getReader();
8056
+ const decoder = new TextDecoder();
8057
+ for (; ; ) {
8058
+ const { done, value } = await reader.read();
8059
+ if (done) break;
8060
+ buffer += decoder.decode(value, { stream: true });
8061
+ const parsed = parsestreamchunk(buffer);
8062
+ buffer = parsed.rest;
8063
+ for (const event of parsed.events) {
8064
+ const names = event.event !== void 0 && !current.names.includes(event.event) ? [...current.names, event.event] : current.names;
8065
+ current = { ...current, events: current.events + 1, names, ...event.id !== void 0 ? { lasteventid: event.id } : {} };
8066
+ await memory.setsubscription(current);
8067
+ await memory.setprogress(recordevent(await memory.getprogress(), current.runid, current.stepid, { url: current.url, name: event.event ?? "", events: current.events, ...current.lasteventid !== void 0 ? { lasteventid: current.lasteventid } : {} }, Date.now())).catch(() => {
8068
+ });
8069
+ }
8070
+ }
8071
+ } catch {
8072
+ if (controller.signal.aborted) break;
8073
+ }
8074
+ if (controller.signal.aborted) break;
8075
+ if (subscription.lifetime !== void 0 && Date.now() - startedat >= subscription.lifetime) break;
8076
+ await waitsome(netpoll * 10);
8077
+ }
8078
+ const closed = { ...current, state: "closed", closedat: Date.now() };
8079
+ await memory.setsubscription(closed);
8080
+ activesockets.delete(subscription.id);
8081
+ await audit("socket", `Event subscription ${subscription.id} of ${subscription.origin} closed after ${closed.events} observed event${closed.events === 1 ? "" : "s"}${closed.lasteventid !== void 0 ? ` at last event id ${closed.lasteventid}` : ""}.`, { stepid: subscription.stepid });
8082
+ }
8083
+ function parsestreamchunk(chunk) {
8084
+ return parsessetext(chunk);
8085
+ }
8086
+ async function closechannelsforrun(runid) {
8087
+ for (const [id, active] of [...activesockets.entries()]) {
8088
+ if (active.runid !== runid) continue;
8089
+ active.cancelled = true;
8090
+ active.controller?.abort();
8091
+ try {
8092
+ active.socket?.close(1e3);
8093
+ } catch {
8094
+ }
8095
+ activesockets.delete(id);
8096
+ if (active.channel) {
8097
+ const closed = closechannel(active.channel, Date.now());
8098
+ await memory.addchannel(closed).catch(() => {
8099
+ });
8100
+ await audit("socket", `Channel ${id} of ${active.channel.origin} closed at the end of the run with ${active.channel.sent} sent and ${active.channel.received} received message${active.channel.received === 1 ? "" : "s"}.`, { stepid: active.channel.stepid }).catch(() => void 0);
8101
+ }
8102
+ if (active.subscription) {
8103
+ const closed = { ...active.subscription, state: "closed", closedat: Date.now() };
8104
+ await memory.setsubscription(closed).catch(() => void 0);
8105
+ await audit("socket", `Event subscription ${id} of ${active.subscription.origin} closed at the end of the run after ${closed.events} observed event${closed.events === 1 ? "" : "s"}.`, { stepid: active.subscription.stepid }).catch(() => void 0);
8106
+ }
8107
+ }
8108
+ channelbuses.clear();
8109
+ }
8110
+ async function executesocketstep(step, session, plan, tabid2, origin) {
8111
+ const options = stepoptions2(step);
8112
+ const extra = { ...session ? { sessionid: session.id } : {}, planid: plan.id, stepid: step.id };
8113
+ if (step.kind === "opensocket") {
8114
+ const channel = channeloptionsof(options.socket);
8115
+ if (!channel) throw new Error("A reviewed socket with a url is required in options.socket.");
8116
+ const gate = socketgate(session, channel.url);
8117
+ if (!gate.allowed) throw new Error(gate.reason ?? "The channel stays outside the session origin grants.");
8118
+ const record2 = newchannel({ id: randomid(), runid: plan.id, stepid: step.id, kind: "websocket", url: channel.url, ...channel.options.protocols !== void 0 ? { protocols: channel.options.protocols } : {}, at: Date.now() });
8119
+ const connect = (url, protocols) => new Promise((resolve) => {
8120
+ const socket = new WebSocket(url, protocols.length > 0 ? protocols : void 0);
8121
+ socket.onopen = () => resolve({ open: true });
8122
+ socket.onerror = () => resolve({ open: false, error: "The websocket reported an error before opening." });
8123
+ socket.onclose = (event) => resolve({ open: false, code: event.code, error: `The websocket closed with code ${event.code}.` });
8124
+ });
8125
+ const opened = await openchannel({ record: record2, options: channel.options, connect });
8126
+ await memory.addchannel(opened);
8127
+ channelbuses.set(opened.id, { sequences: {}, queue: [] });
8128
+ activesockets.set(opened.id, { runid: plan.id, channel: opened, cancelled: false });
8129
+ if (opened.state === "open") wirewebsocket(opened.id, opened.url, opened.protocols ?? [], channel.options, { reconnects: 0 });
8130
+ await memory.setprogress(recordchannel(await memory.getprogress(), plan.id, step.id, { id: opened.id, kind: opened.kind, state: opened.state, url: opened.url, sent: opened.sent, received: opened.received }, Date.now()));
8131
+ await refreshbadge();
8132
+ await audit("socket", `Opened the websocket channel ${opened.id} to ${opened.origin}${(opened.protocols ?? []).length > 0 ? ` with the reviewed protocol${(opened.protocols ?? []).length === 1 ? "" : "s"} ${(opened.protocols ?? []).join(", ")}` : ""} in the ${opened.state} state after ${opened.reconnects} reconnect attempt${opened.reconnects === 1 ? "" : "s"}; payloads stay out of the audit trail.`, extra);
8133
+ return { ok: opened.state === "open", summary: `The websocket channel to ${opened.origin} is ${opened.state}.`, details: { network: { exchanges: 0, channelstate: opened.state, messages: 0 }, channel: { id: opened.id, url: opened.url, state: opened.state, reconnects: opened.reconnects, protocols: opened.protocols ?? [] } } };
8134
+ }
8135
+ if (step.kind === "sendmessage") {
8136
+ const message = options.message;
8137
+ const channelid = message && typeof message === "object" && !Array.isArray(message) && typeof message.channel === "string" ? message.channel : "";
8138
+ const stream = message && typeof message === "object" && !Array.isArray(message) && typeof message.stream === "string" ? message.stream : "outbound";
8139
+ const payload = message && typeof message === "object" && !Array.isArray(message) && typeof message.payload === "string" ? message.payload : void 0;
8140
+ if (!channelid || payload === void 0) throw new Error("A reviewed message with an open channel id and a payload is required in options.message.");
8141
+ const stored = await memory.getchannel(channelid);
8142
+ if (!stored) throw new Error(`No stored channel matches ${channelid}.`);
8143
+ if (stored.state !== "open") throw new Error(`The channel ${channelid} is ${stored.state} and cannot publish.`);
8144
+ const active = activesockets.get(channelid);
8145
+ if (!active?.socket || active.socket.readyState !== WebSocket.OPEN) throw new Error(`The channel ${channelid} has no live websocket to publish on.`);
8146
+ const bus = channelbuses.get(channelid) ?? { sequences: {}, queue: [] };
8147
+ const published = publishmessage(bus, channelid, stream, payload, Date.now());
8148
+ channelbuses.set(channelid, published.state);
8149
+ active.socket.send(payload);
8150
+ const updated = { ...stored, sent: stored.sent + 1 };
8151
+ await memory.addchannel(updated);
8152
+ await memory.addmessage(published.envelope);
8153
+ await memory.setprogress(recordchannel(await memory.getprogress(), plan.id, step.id, { id: updated.id, kind: updated.kind, state: updated.state, url: updated.url, sent: updated.sent, received: updated.received }, Date.now()));
8154
+ await audit("socket", `Published ${payload.length} reviewed character${payload.length === 1 ? "" : "s"} on the ${stream} stream of channel ${channelid} as sequence ${published.envelope.sequence}; the payload value stays out of the audit trail.`, extra);
8155
+ return { ok: true, summary: `Published the reviewed payload on the ${stream} stream of channel ${channelid} as sequence ${published.envelope.sequence}.`, details: { network: { exchanges: 0, channelstate: updated.state, messages: updated.sent + updated.received }, message: { channelid, stream, sequence: published.envelope.sequence, bytes: payload.length } } };
8156
+ }
8157
+ if (step.kind === "waitmessage") {
8158
+ const filter = messagefilterof(options.filter);
8159
+ const channelid = typeof options.channel === "string" ? options.channel : typeof options.filter?.channel === "string" ? options.filter.channel : "";
8160
+ if (!channelid) throw new Error("A reviewed channel id is required in options.channel before a message waits.");
8161
+ const stored = await memory.getchannel(channelid);
8162
+ if (!stored) throw new Error(`No stored channel matches ${channelid}.`);
8163
+ const budget = typeof options.wait === "number" && Number.isFinite(options.wait) && options.wait >= 0 ? options.wait : 0;
8164
+ const deadline = Date.now() + budget;
8165
+ let matched = [];
8166
+ for (; ; ) {
8167
+ const queued = await memory.getmessages(channelid);
8168
+ matched = queued.filter((envelope) => matchmessage(filter, envelope)).slice(0, filter.limit ?? (matched.length || void 0));
8169
+ if (matched.length >= (filter.limit ?? 1) || Date.now() >= deadline) break;
8170
+ await waitsome(netpoll);
8171
+ }
8172
+ await memory.drainmessages(matched.map((envelope) => ({ channelid: envelope.channelid, sequence: envelope.sequence })));
8173
+ await audit("socket", `The message wait on channel ${channelid} matched ${matched.length} envelope${matched.length === 1 ? "" : "s"} of the reviewed filter within the ${budget} millisecond budget; payload values stay out of the audit trail.`, extra);
8174
+ return { ok: matched.length > 0, summary: matched.length > 0 ? `Matched ${matched.length} message${matched.length === 1 ? "" : "s"} on channel ${channelid}.` : `No message of channel ${channelid} matched the reviewed filter within the ${budget} millisecond budget.`, details: { network: { exchanges: 0, channelstate: stored.state, messages: stored.sent + stored.received }, messages: matched.map((envelope) => ({ stream: envelope.stream, sequence: envelope.sequence, payload: envelope.payload })) } };
8175
+ }
8176
+ if (step.kind === "subscribesse") {
8177
+ const subscription = subscriptionoptionsof(options.subscription);
8178
+ if (!subscription) throw new Error("A reviewed subscription with an event stream url and a cancellation path is required in options.subscription.");
8179
+ const gate = socketgate(session, subscription.url);
8180
+ if (!gate.allowed) throw new Error(gate.reason ?? "The event stream stays outside the session origin grants.");
8181
+ const record2 = { id: randomid(), runid: plan.id, stepid: step.id, url: subscription.url, origin: new URL(subscription.url).origin, state: "open", events: 0, names: [], cancel: subscription.cancel, openedat: Date.now(), ...subscription.lasteventid !== void 0 ? { lasteventid: subscription.lasteventid } : {}, ...subscription.lifetime !== void 0 ? { lifetime: subscription.lifetime } : {} };
8182
+ const controller = new AbortController();
8183
+ activesockets.set(record2.id, { runid: plan.id, subscription: record2, controller, cancelled: false });
8184
+ await memory.setsubscription(record2);
8185
+ await memory.setprogress(recordchannel(await memory.getprogress(), plan.id, step.id, { id: record2.id, kind: "sse", state: record2.state, url: record2.url, sent: 0, received: 0 }, Date.now()));
8186
+ await audit("socket", `Subscribed the server sent events stream ${record2.id} of ${record2.origin} with the reviewed ${subscription.cancel.kind} cancellation path${subscription.lifetime !== void 0 ? ` and the ${subscription.lifetime} millisecond lifetime window` : ""}.`, extra);
8187
+ void runsubscription(record2, controller).catch(() => {
8188
+ });
8189
+ return { ok: true, summary: `The event stream subscription of ${record2.origin} is open.`, details: { network: { exchanges: 0, channelstate: "open", messages: 0 }, subscription: { id: record2.id, url: record2.url, cancel: record2.cancel, ...subscription.lifetime !== void 0 ? { lifetime: subscription.lifetime } : {} } } };
8190
+ }
8191
+ if (step.kind === "longpoll") {
8192
+ const cursor = pollcursorof(options.poll);
8193
+ if (!cursor) throw new Error("A reviewed poll cursor with a url, cursor field, interval and stop condition is required in options.poll.");
8194
+ const gate = socketgate(session, cursor.url);
8195
+ if (!gate.allowed) throw new Error(gate.reason ?? "The poll loop stays outside the session origin grants.");
8196
+ const pollid = randomid();
8197
+ const controller = new AbortController();
8198
+ activesockets.set(pollid, { runid: plan.id, controller, cancelled: false });
8199
+ let polls = 0;
8200
+ let next = pollurl(cursor, void 0);
8201
+ let lastreason = "";
8202
+ let stopcursor;
8203
+ try {
8204
+ for (; ; ) {
8205
+ const active = activesockets.get(pollid);
8206
+ if (!active || active.cancelled) {
8207
+ lastreason = "The long poll loop was cancelled.";
8208
+ break;
8209
+ }
8210
+ const started = Date.now();
8211
+ const response = await fetch(next.url, { method: next.body !== void 0 ? "POST" : "GET", ...next.body !== void 0 ? { headers: { "content-type": "application/json" }, body: next.body } : {}, credentials: "omit", signal: controller.signal });
8212
+ const text2 = await response.text();
8213
+ polls += 1;
8214
+ let parsed;
8215
+ try {
8216
+ parsed = JSON.parse(text2);
8217
+ } catch {
8218
+ parsed = void 0;
8219
+ }
8220
+ const mime = response.headers.get("content-type")?.split(";")[0] ?? "";
8221
+ const exchange = { id: randomid(), runid: plan.id, stepid: step.id, correlationid: correlationid(plan.id, polls), url: next.url, origin: new URL(next.url).origin, method: next.body !== void 0 ? "POST" : "GET", status: response.status, statusclass: response.status >= 200 && response.status < 300 ? "success" : response.status >= 400 && response.status < 500 ? "clienterror" : response.status >= 500 ? "servererror" : "unknown", source: "extension", timing: Date.now() - started, bytes: text2.length, ...mime ? { mime } : {}, at: Date.now() };
8222
+ await memory.addexchange(exchange);
8223
+ await memory.setprogress(recordexchange(await memory.getprogress(), plan.id, step.id, { id: exchange.id, correlationid: exchange.correlationid, method: exchange.method, origin: exchange.origin, url: exchange.url, status: exchange.status, statusclass: exchange.statusclass, duration: exchange.timing, bytes: exchange.bytes }, Date.now()));
8224
+ const decision = polldecision({ cursor, polls: polls - 1, response: parsed, cancelled: () => activesockets.get(pollid)?.cancelled === true, expiresat: plan.expiresat, now: Date.now() });
8225
+ await memory.setprogress(recordpoll(await memory.getprogress(), plan.id, step.id, { poll: polls, ...decision.cursor !== void 0 ? { cursor: decision.cursor } : {}, status: response.status, stopped: !decision.continue, reason: decision.reason }, Date.now()));
8226
+ if (!decision.continue) {
8227
+ lastreason = decision.reason;
8228
+ break;
8229
+ }
8230
+ stopcursor = decision.cursor;
8231
+ await waitsome(decision.next?.wait ?? cursor.interval);
8232
+ next = { url: decision.next?.url ?? next.url, ...decision.next?.body !== void 0 ? { body: decision.next.body } : {} };
8233
+ }
8234
+ } finally {
8235
+ activesockets.delete(pollid);
8236
+ }
8237
+ await audit("socket", `The long poll loop of ${cursor.url} ran ${polls} poll${polls === 1 ? "" : "s"} on the ${cursor.cursorfield} cursor and stopped: ${lastreason}.`, extra);
8238
+ return { ok: true, summary: `The long poll loop ran ${polls} poll${polls === 1 ? "" : "s"} and stopped: ${lastreason}`, details: { network: { exchanges: polls, channelstate: "none", messages: 0 }, poll: { url: cursor.url, polls, ...stopcursor !== void 0 ? { cursor: stopcursor } : {}, stop: cursor.stop, reason: lastreason } } };
8239
+ }
8240
+ void tabid2;
8241
+ void origin;
8242
+ throw new Error("Unsupported socket observation kind.");
8243
+ }
8244
+ async function executenetwatchstep(step, session, plan, tabid2, origin) {
8245
+ const options = stepoptions2(step);
8246
+ const extra = { ...session ? { sessionid: session.id } : {}, planid: plan.id, stepid: step.id };
8247
+ if (step.kind === "watchrequests") {
8248
+ const watch = options.watch && typeof options.watch === "object" && !Array.isArray(options.watch) ? options.watch : {};
8249
+ const window2 = typeof watch.window === "number" && Number.isFinite(watch.window) && watch.window >= 0 ? watch.window : 0;
8250
+ const limit = typeof options.limit === "number" && Number.isInteger(options.limit) && options.limit >= 1 ? options.limit : void 0;
8251
+ const before = await bridgecall(tabid2, "resourcerecords");
8252
+ const known = new Set(resourcefacts(before ?? []).map((fact) => `${fact.url}@${fact.start}`));
8253
+ if (window2 > 0) await waitsome(window2);
8254
+ const after = await bridgecall(tabid2, "resourcerecords");
8255
+ const fresh = resourcefacts(after ?? []).filter((fact) => !known.has(`${fact.url}@${fact.start}`));
8256
+ const chosen = limit !== void 0 ? fresh.slice(0, limit) : fresh;
8257
+ const observed = [];
8258
+ for (const [index, fact] of chosen.entries()) {
8259
+ const exchange = newexchange({ id: randomid(), runid: plan.id, stepid: step.id, correlationid: correlationid(plan.id, index + 1), fact, at: Date.now() });
8260
+ await memory.addexchange(exchange);
8261
+ await memory.setprogress(recordexchange(await memory.getprogress(), plan.id, step.id, { id: exchange.id, correlationid: exchange.correlationid, method: exchange.method, origin: exchange.origin, url: exchange.url, status: exchange.status, statusclass: exchange.statusclass, duration: exchange.timing, bytes: exchange.bytes, ...exchange.errorclass !== void 0 ? { errorclass: exchange.errorclass } : {} }, Date.now()));
8262
+ observed.push(exchange);
8263
+ }
8264
+ const failed = observed.filter((exchange) => exchange.errorclass !== void 0).length;
8265
+ await refreshbadge();
8266
+ await audit("watch", `Observed ${observed.length} request${observed.length === 1 ? "" : "s"} of the run tab over the reviewed ${window2} millisecond window, derived from the page timing buffers with ${failed} marked failed; headers and bodies stay out of this observation.`, extra);
8267
+ return { ok: true, summary: `Observed ${observed.length} request${observed.length === 1 ? "" : "s"} of the run tab${failed > 0 ? ` with ${failed} failed` : ""}.`, details: { network: { exchanges: observed.length, channelstate: "none", messages: 0 }, observed: observed.map((exchange) => ({ correlationid: exchange.correlationid, method: exchange.method, url: exchange.url, status: exchange.status, statusclass: exchange.statusclass, ...exchange.errorclass !== void 0 ? { errorclass: exchange.errorclass } : {}, bytes: exchange.bytes, duration: exchange.timing })), derivation: "The request lifecycle derives from the page performance and navigation buffers; the timing buffers expose no header names, body bytes or subresource status codes." } };
8268
+ }
8269
+ if (step.kind === "readheaders") {
8270
+ const filter = headerfilterof(options.headers);
8271
+ const exchanges = typeof options.exchange === "string" ? (await memory.getexchanges()).filter((item) => item.id === options.exchange || item.correlationid === options.exchange) : await memory.listexchanges({ runid: plan.id });
8272
+ if (exchanges.length === 0) throw new Error("No observed exchange of the run is stored yet; watch requests first.");
8273
+ const views = [];
8274
+ for (const exchange of exchanges) {
8275
+ const gate = observedorigingranted(session, exchange.url);
8276
+ if (!gate.allowed) {
8277
+ views.push({ correlationid: exchange.correlationid, request: {}, response: {}, redacted: 0, ...gate.reason !== void 0 ? { refused: gate.reason } : {} });
8278
+ continue;
8279
+ }
8280
+ const request = capturedheaders(exchange.requestheaders ?? {}, filter);
8281
+ const response = capturedheaders(exchange.responseheaders ?? {}, filter);
8282
+ const redacted = [...Object.keys(exchange.requestheaders ?? {}), ...Object.keys(exchange.responseheaders ?? {})].filter((name) => filter.redact.includes(name.trim().toLowerCase())).length;
8283
+ const updated = { ...exchange, ...Object.keys(request).length > 0 ? { requestheaders: request } : {}, ...Object.keys(response).length > 0 ? { responseheaders: response } : {} };
8284
+ if (updated.requestheaders !== exchange.requestheaders || updated.responseheaders !== exchange.responseheaders) await memory.addexchange(updated);
8285
+ views.push({ correlationid: exchange.correlationid, request, response, redacted });
8286
+ }
8287
+ const refused = views.filter((view) => view.refused !== void 0).length;
8288
+ await audit("watch", `Read the captured headers of ${views.length - refused} exchange${views.length - refused === 1 ? "" : "s"} through the reviewed allowlist with ${views.reduce((total, view) => total + view.redacted, 0)} redacted value${views.reduce((total, view) => total + view.redacted, 0) === 1 ? "" : "s"}${refused > 0 ? ` and ${refused} refused exchange${refused === 1 ? "" : "s"} outside the grants` : ""}; derived page exchanges carry no headers.`, extra);
8289
+ return { ok: true, summary: `Read the captured headers of ${views.length - refused} exchange${views.length - refused === 1 ? "" : "s"} with ${views.reduce((total, view) => total + view.redacted, 0)} redacted value${views.reduce((total, view) => total + view.redacted, 0) === 1 ? "" : "s"}.`, details: { network: { exchanges: views.length, channelstate: "none", messages: 0 }, headers: views, derivation: "Header values exist only for exchanges captured through the extension context; derived page exchanges carry none." } };
8290
+ }
8291
+ if (step.kind === "capturebodies") {
8292
+ const filter = bodyfilterof(options.body);
8293
+ const exchanges = await memory.listexchanges({ runid: plan.id });
8294
+ const matched = exchanges.filter((exchange) => bodymatches(filter, exchange));
8295
+ if (matched.length === 0) throw new Error("No observed exchange matches the reviewed body filter yet.");
8296
+ const captures = [];
8297
+ for (const exchange of matched) {
8298
+ const gate = observedorigingranted(session, exchange.url);
8299
+ if (!gate.allowed) {
8300
+ captures.push({ correlationid: exchange.correlationid, ref: "", mime: "", bytes: 0, truncated: false, ...gate.reason !== void 0 ? { refused: gate.reason } : {} });
8301
+ continue;
8302
+ }
8303
+ if (exchange.bodyref !== void 0) {
8304
+ captures.push({ correlationid: exchange.correlationid, ref: exchange.bodyref, mime: exchange.mime ?? "", bytes: exchange.bytes, truncated: false });
8305
+ continue;
8306
+ }
8307
+ const controller = new AbortController();
8308
+ activefetches.set(exchange.id, controller);
8309
+ try {
8310
+ const response = await fetch(exchange.url, { method: "GET", credentials: "omit", signal: controller.signal });
8311
+ const text2 = await response.text();
8312
+ const mime = response.headers.get("content-type")?.split(";")[0] ?? "";
8313
+ const captured = capturebody({ ref: randomid(), runid: plan.id, exchange: { ...exchange, ...mime ? { mime } : {} }, body: text2, mime, filter, at: Date.now() });
8314
+ if ("refused" in captured) {
8315
+ captures.push({ correlationid: exchange.correlationid, ref: "", mime, bytes: 0, truncated: false, refused: captured.refused });
8316
+ continue;
8317
+ }
8318
+ void captures;
8319
+ await memory.addbody(captured.record);
8320
+ const headers = {};
8321
+ response.headers.forEach((value, name) => {
8322
+ headers[name] = value;
8323
+ });
8324
+ const entry = { correlationid: exchange.correlationid, status: response.status, headers: capturedheaders(headers, { allow: filter.mimes !== void 0 ? ["content-type", "content-length"] : ["content-type", "content-length"], redact: [] }), bytes: captured.record.bytes, ...mime ? { mime } : {}, bodyref: captured.record.ref, at: Date.now() };
8325
+ const paired = pairexchange({ ...exchange, ...mime ? { mime } : {} }, entry);
8326
+ await memory.addexchange(paired);
8327
+ captures.push({ correlationid: exchange.correlationid, ref: captured.record.ref, mime, bytes: captured.record.bytes, truncated: captured.truncated });
8328
+ } finally {
8329
+ controller.abort();
8330
+ activefetches.delete(exchange.id);
8331
+ }
8332
+ }
8333
+ const refused = captures.filter((capture) => capture.refused !== void 0).length;
8334
+ const truncated = captures.filter((capture) => capture.truncated).length;
8335
+ await refreshbadge();
8336
+ await audit("watch", `Captured ${captures.length - refused} response bod${captures.length - refused === 1 ? "y" : "ies"} inside the reviewed byte ceiling${filter.ceiling !== void 0 ? ` of ${filter.ceiling} byte${filter.ceiling === 1 ? "" : "s"}` : ""} with ${truncated} truncated and ${refused} refused${refused > 0 ? " outside the grants" : ""}; body bytes stay out of the audit trail and derive from fresh reviewed fetches of the matched urls.`, extra);
8337
+ return { ok: true, summary: `Captured ${captures.length - refused} response bod${captures.length - refused === 1 ? "y" : "ies"} inside the reviewed byte ceiling.`, details: { network: { exchanges: matched.length, channelstate: "none", messages: 0 }, bodies: captures, derivation: "The page timing buffers expose no body bytes, so each captured body comes from a fresh reviewed fetch of the matched url through the extension context." } };
8338
+ }
8339
+ if (step.kind === "mapapi") {
8340
+ const exchanges = await memory.listexchanges({ runid: plan.id });
8341
+ const bodies = await memory.getbodies();
8342
+ const ranked = rankapis(apientries(exchanges, bodies));
8343
+ const limit = typeof options.limit === "number" && Number.isInteger(options.limit) && options.limit >= 1 ? options.limit : void 0;
8344
+ const chosen = limit !== void 0 ? ranked.slice(0, limit) : ranked;
8345
+ for (const entry of chosen) await memory.setapimap(entry.origin, chosen.filter((item) => item.origin === entry.origin));
8346
+ await audit("observation", `Mapped ${chosen.length} page api endpoint${chosen.length === 1 ? "" : "s"} of the run from ${exchanges.length} observed exchange${exchanges.length === 1 ? "" : "s"} ranked by frequency, json share and payload stability.`, extra);
8347
+ return { ok: true, summary: `Mapped ${chosen.length} page api endpoint${chosen.length === 1 ? "" : "s"} of the run.`, details: { network: { exchanges: exchanges.length, channelstate: "none", messages: 0 }, apimap: chosen } };
8348
+ }
8349
+ if (step.kind === "extractapi") {
8350
+ const spec = apireplayspecof(options.replay);
8351
+ if (!spec) throw new Error("A reviewed replay spec with an endpoint is required in options.replay.");
8352
+ const gate = origincheck(session, spec.endpoint);
8353
+ if (!gate.allowed) throw new Error(gate.reason ?? "The replay endpoint stays outside the session origin grants.");
8354
+ const url = replayurl(spec);
8355
+ const verb = spec.verb ?? "GET";
8356
+ const controller = new AbortController();
8357
+ activefetches.set(step.id, controller);
8358
+ try {
8359
+ const started = Date.now();
8360
+ const response = await fetch(url, { method: verb, credentials: "omit", signal: controller.signal });
8361
+ const text2 = await response.text();
8362
+ const mime = response.headers.get("content-type")?.split(";")[0] ?? "";
8363
+ const headers = {};
8364
+ response.headers.forEach((value, name) => {
8365
+ headers[name] = value;
8366
+ });
8367
+ const exchange = { id: randomid(), runid: plan.id, stepid: step.id, correlationid: correlationid(plan.id, (await memory.getexchanges()).length + 1), url, origin: new URL(url).origin, method: verb, requestheaders: {}, responseheaders: headers, status: response.status, statusclass: response.status >= 200 && response.status < 300 ? "success" : response.status >= 400 && response.status < 500 ? "clienterror" : response.status >= 500 ? "servererror" : "unknown", source: "extension", timing: Date.now() - started, bytes: text2.length, ...mime ? { mime } : {}, at: Date.now() };
8368
+ await memory.addexchange(exchange);
8369
+ await memory.setprogress(recordexchange(await memory.getprogress(), plan.id, step.id, { id: exchange.id, correlationid: exchange.correlationid, method: exchange.method, origin: exchange.origin, url: exchange.url, status: exchange.status, statusclass: exchange.statusclass, duration: exchange.timing, bytes: exchange.bytes }, Date.now()));
8370
+ const fields = extractvalues(text2, spec.paths ?? []);
8371
+ await refreshbadge();
8372
+ await audit("replay", `Replayed the captured endpoint ${spec.endpoint} with ${verb} as exchange ${exchange.correlationid} ending in the ${response.status} ${exchange.statusclass} class and mapped ${fields.length} extraction path${fields.length === 1 ? "" : "s"}; body bytes stay out of the audit trail.`, extra);
8373
+ return { ok: exchange.statusclass === "success", summary: `The replay of ${spec.endpoint} ended in the ${response.status} ${exchange.statusclass} class with ${fields.length} extracted field${fields.length === 1 ? "" : "s"}.`, details: { network: { exchanges: 1, channelstate: "none", messages: 0 }, replay: { endpoint: spec.endpoint, verb, url, overrides: spec.overrides ?? {}, fields } } };
8374
+ } finally {
8375
+ controller.abort();
8376
+ activefetches.delete(step.id);
8377
+ }
8378
+ }
8379
+ void origin;
8380
+ throw new Error("Unsupported request observation kind.");
8381
+ }
6461
8382
  async function enforcewindowreview(step, session, plan) {
6462
8383
  const windowid = step.value && /^\d+$/.test(step.value) ? Number.parseInt(step.value, 10) : 0;
6463
8384
  const progress = plan ? await memory.getprogress() : void 0;
@@ -6499,8 +8420,11 @@ async function refreshbadge() {
6499
8420
  const captures = (await memory.getcaptures()).length;
6500
8421
  const media = (await memory.getmediarecords()).length + (await memory.getimagebatches()).length;
6501
8422
  const recordingprompts = (await memory.getrecordingconsents()).filter((record2) => record2.approved === void 0).length;
8423
+ const fetchprompts = (await memory.getfetchconsents()).filter((consent) => consent.approved === void 0).length;
8424
+ const observedrequests = (await memory.getexchanges()).length;
8425
+ const livechannels = (await memory.getchannels()).filter((channel) => channel.state === "open" || channel.state === "connecting").length;
6502
8426
  const tasktabs2 = new Set(badges.map((badge) => badge.tabid)).size;
6503
- const total = (queues?.prefetch ?? 0) + (queues?.batchopen ?? 0) + tasktabs2 + prompts + consents + quarantined + datasets + captures + media + recordingprompts;
8427
+ const total = (queues?.prefetch ?? 0) + (queues?.batchopen ?? 0) + tasktabs2 + prompts + consents + quarantined + datasets + captures + media + recordingprompts + fetchprompts + observedrequests + livechannels;
6504
8428
  await chrome.action.setBadgeText({ text: total > 0 ? String(total) : "" }).catch(() => {
6505
8429
  });
6506
8430
  }
@@ -6510,7 +8434,9 @@ async function executestep(stepid) {
6510
8434
  const { tab, origin } = await activecontext();
6511
8435
  const step = plan?.steps.find((candidate) => candidate.id === stepid);
6512
8436
  if (!step) throw new Error("Reviewed step was not found.");
6513
- const gate = canexecute({ session, plan, step, tabid: tab.id, origin, verdicts: await memory.getsafeties() });
8437
+ const settings = await memory.getsettings();
8438
+ const verdicts = await memory.getsafeties();
8439
+ const gate = canexecute({ session, plan, step, tabid: tab.id, origin, ...verdicts.length > 0 ? { verdicts } : {}, ...settings !== void 0 ? { settings } : {} });
6514
8440
  if (!gate.allowed) throw new Error(gate.reason);
6515
8441
  const capability = requiredcapability(step.kind);
6516
8442
  if (capability) {
@@ -6558,6 +8484,12 @@ async function executestep(stepid) {
6558
8484
  output = await executecapturestep(step, session, plan, tab.id, origin);
6559
8485
  } else if (ismediakind(step.kind)) {
6560
8486
  output = await executemediastep(step, session, plan, tab.id, origin);
8487
+ } else if (ishttpkind(step.kind)) {
8488
+ output = await executehttpstep(step, session, plan, tab.id, origin);
8489
+ } else if (issocketkind(step.kind)) {
8490
+ output = await executesocketstep(step, session, plan, tab.id, origin);
8491
+ } else if (isnetwatchkind(step.kind)) {
8492
+ output = await executenetwatchstep(step, session, plan, tab.id, origin);
6561
8493
  } else {
6562
8494
  if (step.target && freshcheckkinds.has(step.kind)) {
6563
8495
  const fresh = await snapshot(tab.id);
@@ -6609,6 +8541,8 @@ async function executestep(stepid) {
6609
8541
  if (iscomplete(tracked, plan) && plan.state === "approved") {
6610
8542
  await stoprecordingsforrun(plan.id).catch(() => {
6611
8543
  });
8544
+ await closechannelsforrun(plan.id).catch(() => {
8545
+ });
6612
8546
  const done = { ...plan, state: "completed", completedat: Date.now() };
6613
8547
  await memory.setplan(done);
6614
8548
  await audit("complete", "Every reviewed step of the approved plan has executed.", { ...session ? { sessionid: session.id } : {}, planid: done.id });
@@ -6756,6 +8690,19 @@ async function handlerequest(message, sender) {
6756
8690
  });
6757
8691
  const imagebatches = await memory.getimagebatches();
6758
8692
  const recordingconsents = await memory.getrecordingconsents();
8693
+ const calls = (await memory.getcalls()).map((call) => {
8694
+ const { body, ...metadata } = call;
8695
+ void body;
8696
+ return metadata;
8697
+ });
8698
+ const exchanges = await memory.getexchanges();
8699
+ const channels = await memory.getchannels();
8700
+ const subscriptions = await memory.getsubscriptions();
8701
+ const apimap = await memory.getapimap();
8702
+ const messagecount = (await memory.getmessages()).length;
8703
+ const endpoints = await memory.getendpoints();
8704
+ const fetchconsents = await memory.getfetchconsents();
8705
+ const apikeys = (await memory.getapikeys()).map((ref) => ({ name: ref.name, origins: ref.origins, header: ref.header, configuredat: ref.configuredat }));
6759
8706
  const runsettings = await memory.getsettings();
6760
8707
  const scanhooks = [];
6761
8708
  for (const hook of await memory.getscanhooks()) {
@@ -6767,7 +8714,7 @@ async function handlerequest(message, sender) {
6767
8714
  const livetab = session ? await chrome.tabs.get(session.tabid).catch(() => void 0) : void 0;
6768
8715
  const waitprofile = session ? waitprofiles.find((record2) => record2.origin === session.origin) : void 0;
6769
8716
  const livestate = { phase: livetab?.status === "loading" ? "loading" : "complete", ...navrecords[0] ? { finalurl: navrecords[0].finalurl, redirects: navrecords[0].chain } : {} };
6770
- return { config: await memory.getconfig(), session, plan, progress: plan && progress?.planid === plan.id ? progress : void 0, diagnostic: await memory.getdiagnostic(), audit: await memory.getaudit(), capabilities: await refreshcapabilities(), outcomes: await memory.getoutcomes(), holds: heldkeysreport({ tabid: session?.tabid ?? 0, holds }), dialogs: await memory.getdialogs(), retries: await memory.getretries(), ...signals ? { signals: signalsreport({ signals }) } : { signals: signalsreport({}) }, banners: await memory.getbanners(), mutationevents: await memory.getmutationevents(), focusevents: await memory.getfocusevents(), diffs: await memory.getdiffs(), selectors: await memory.getselectors(), ...a11y ? { a11y } : {}, ...reader ? { reader } : {}, ...map ? { map } : {}, trail: trailreport({ ...session ? { sessionid: session.id } : {}, trail }), navrecords, ratestates, safeties, curated, waitprofiles, auths, navcontrol, navqueues, artifacts, navstate: livestate, ...waitprofile ? { waitprofile } : {}, offline: !navigator.onLine, tabs, windows, layouts: layoutreport({ layouts }), tabgroups, tabmetas, badges, snapshots, closedtabs, tabwatchevents, clones, tasktabgauge: taskgauge, ...controltab ? { controltab } : {}, tabreport: report, profiles, tickets, wizards: wizardreport({ ...session ? { sessionid: session.id } : {}, wizards, picks }), picks, errorreports, captchas, detections, ...codeentry !== void 0 ? { codeentry: true } : {}, datasets, imports, extractsessions, streams, exports, provenances, taskrules, sheetendpoints: sheetgrants, downloads, netlogs, clipconsents, clips, quarantines, cleanuprules, cleanupruns, capturecounters, inventory, mimefilters, scanhooks, captures: capturemetadata, capturepairs, capturepolicy: runsettings?.capturepolicy ?? "manual", media: mediarecords, imagebatches, recordingconsents, recordingactive: [...activerecordings.values()].map((active) => ({ id: active.record.id, kind: active.record.kind, scope: active.record.scope, startedat: active.record.startedat, stopat: active.stopat })), recordingwindow: runsettings?.recordingwindow, ...stitchprogress.size > 0 ? { stitchprogress: [...stitchprogress.values()] } : {} };
8717
+ return { config: await memory.getconfig(), session, plan, progress: plan && progress?.planid === plan.id ? progress : void 0, diagnostic: await memory.getdiagnostic(), audit: await memory.getaudit(), capabilities: await refreshcapabilities(), outcomes: await memory.getoutcomes(), holds: heldkeysreport({ tabid: session?.tabid ?? 0, holds }), dialogs: await memory.getdialogs(), retries: await memory.getretries(), ...signals ? { signals: signalsreport({ signals }) } : { signals: signalsreport({}) }, banners: await memory.getbanners(), mutationevents: await memory.getmutationevents(), focusevents: await memory.getfocusevents(), diffs: await memory.getdiffs(), selectors: await memory.getselectors(), ...a11y ? { a11y } : {}, ...reader ? { reader } : {}, ...map ? { map } : {}, trail: trailreport({ ...session ? { sessionid: session.id } : {}, trail }), navrecords, ratestates, safeties, curated, waitprofiles, auths, navcontrol, navqueues, artifacts, navstate: livestate, ...waitprofile ? { waitprofile } : {}, offline: !navigator.onLine, tabs, windows, layouts: layoutreport({ layouts }), tabgroups, tabmetas, badges, snapshots, closedtabs, tabwatchevents, clones, tasktabgauge: taskgauge, ...controltab ? { controltab } : {}, tabreport: report, profiles, tickets, wizards: wizardreport({ ...session ? { sessionid: session.id } : {}, wizards, picks }), picks, errorreports, captchas, detections, ...codeentry !== void 0 ? { codeentry: true } : {}, datasets, imports, extractsessions, streams, exports, provenances, taskrules, sheetendpoints: sheetgrants, downloads, netlogs, clipconsents, clips, quarantines, cleanuprules, cleanupruns, capturecounters, inventory, mimefilters, scanhooks, captures: capturemetadata, capturepairs, capturepolicy: runsettings?.capturepolicy ?? "manual", media: mediarecords, imagebatches, recordingconsents, recordingactive: [...activerecordings.values()].map((active) => ({ id: active.record.id, kind: active.record.kind, scope: active.record.scope, startedat: active.record.startedat, stopat: active.stopat })), recordingwindow: runsettings?.recordingwindow, calls, endpoints, fetchconsents, apikeys, callretention: runsettings?.callretention, fetchesactive: activefetches.size, exchanges, channels, subscriptions, apimap, messages: messagecount, webrequestgrant: runsettings?.webrequestgrant === true, bodyretention: runsettings?.bodyretention, socketsactive: activesockets.size, ...stitchprogress.size > 0 ? { stitchprogress: [...stitchprogress.values()] } : {} };
6771
8718
  }
6772
8719
  case "capabilities":
6773
8720
  return refreshcapabilities();
@@ -6811,7 +8758,8 @@ async function handlerequest(message, sender) {
6811
8758
  const resolved = outcome.details?.resolvedtarget;
6812
8759
  const capture = outcome.details?.capture;
6813
8760
  const media = outcome.details?.media;
6814
- return JSON.parse(outcomeresponse({ outcome, plan, ...resolved ? { resolvedtarget: resolved } : {}, ...capture ? { capture } : {}, ...media ? { media } : {} }));
8761
+ const network = outcome.details?.network;
8762
+ return JSON.parse(outcomeresponse({ outcome, plan, ...resolved ? { resolvedtarget: resolved } : {}, ...capture ? { capture } : {}, ...media ? { media } : {}, ...network ? { network } : {} }));
6815
8763
  }
6816
8764
  case "map": {
6817
8765
  const plan = await memory.getplan();
@@ -7341,8 +9289,134 @@ async function handlerequest(message, sender) {
7341
9289
  await audit("configure", `The user set the recording duration window of the run to ${window2} milliseconds; recordings never run past it.`);
7342
9290
  return { recordingwindow: window2 };
7343
9291
  }
9292
+ case "setcallretention": {
9293
+ const inputretention = message;
9294
+ const retention = inputretention.retention;
9295
+ if (retention !== void 0 && (typeof retention !== "number" || !Number.isFinite(retention) || retention < 0)) throw new Error("The call body retention window must be zero or a positive number of records with no code ceiling.");
9296
+ const settings = await memory.getsettings();
9297
+ await memory.setsettings({ ...settings, ...retention !== void 0 ? { callretention: retention } : {} });
9298
+ await audit("configure", `The user set the outbound call body retention window to ${retention === void 0 ? "keep every body" : `${retention} record${retention === 1 ? "" : "s"}`}; the call metadata always survives for the audit trail.`);
9299
+ return { callretention: retention };
9300
+ }
9301
+ case "approvefetchconsent": {
9302
+ const inputconsent = message;
9303
+ const record2 = (await memory.getfetchconsents()).find((item) => item.id === inputconsent.id);
9304
+ if (!record2) throw new Error("No fetch consent prompt matches the requested id.");
9305
+ await memory.setfetchconsent({ ...record2, approved: inputconsent.approved !== false });
9306
+ const session = await memory.getsession();
9307
+ await audit("consent", `Fetch consent prompt ${record2.id} for ${record2.origin} with the header name${record2.headers.length === 1 ? "" : "s"} ${record2.headers.map((header) => header.name).join(", ")} ${inputconsent.approved !== false ? "approved" : "declined"} by the user; the prompt appears once per origin and expires on the reviewed window.`, { ...session ? { sessionid: session.id } : {} });
9308
+ await refreshbadge();
9309
+ return { id: record2.id, approved: inputconsent.approved !== false };
9310
+ }
9311
+ case "configureendpoint": {
9312
+ const inputendpoint = message;
9313
+ const candidate = { name: inputendpoint.name ?? "", method: inputendpoint.method ?? "", url: inputendpoint.url ?? "", ...inputendpoint.headers !== void 0 ? { headers: inputendpoint.headers } : {}, ...inputendpoint.schema !== void 0 ? { schema: inputendpoint.schema } : {}, version: 1, at: Date.now() };
9314
+ const gate = validateendpointrecord(candidate);
9315
+ if (!gate.allowed) throw new Error(gate.reason);
9316
+ const normalized = normalizeendpoint(candidate.url);
9317
+ const record2 = { name: candidate.name.trim(), method: candidate.method.trim().toUpperCase(), url: normalized.endpoint, ...candidate.headers !== void 0 ? { headers: candidate.headers } : {}, ...inputendpoint.schema !== void 0 ? { schema: inputendpoint.schema } : {}, version: 1, at: Date.now() };
9318
+ await memory.setendpoint(record2);
9319
+ const session = await memory.getsession();
9320
+ await audit("call", `Stored the typed endpoint ${record2.name} version as ${record2.method} ${normalized.endpoint} with a reviewed payload schema and header allowlist; every stored version stays in the history.`, { ...session ? { sessionid: session.id } : {} });
9321
+ const stored = await memory.getendpoint(record2.name);
9322
+ return stored;
9323
+ }
9324
+ case "setapikey": {
9325
+ const inputkey = message;
9326
+ if (!inputkey.name?.trim()) throw new Error("A reviewed api key name is required.");
9327
+ if (!Array.isArray(inputkey.origins) || inputkey.origins.length === 0 || !inputkey.origins.every((item) => typeof item === "string" && /^https:\/\//.test(item))) throw new Error("The api key needs a reviewed non-empty list of HTTPS origin scopes.");
9328
+ if (!inputkey.header?.trim()) throw new Error("A reviewed header name is required for the api key.");
9329
+ if (typeof inputkey.value !== "string" || !inputkey.value) throw new Error("The api key needs its secret value; it stays out of every audit trail.");
9330
+ const ref = { name: inputkey.name.trim(), origins: inputkey.origins, header: inputkey.header.trim(), storageid: `apikey-${inputkey.name.trim()}`, configuredat: Date.now() };
9331
+ await memory.setapikey(ref);
9332
+ await memory.setsecret(ref.storageid, inputkey.value);
9333
+ const session = await memory.getsession();
9334
+ await audit("call", `Stored the api key reference ${ref.name} for ${ref.origins.join(", ")} attaching header ${ref.header}; the key material itself never enters the audit trail.`, { ...session ? { sessionid: session.id } : {} });
9335
+ return { name: ref.name, origins: ref.origins, header: ref.header, configuredat: ref.configuredat };
9336
+ }
9337
+ case "deleteapikey": {
9338
+ const inputdeletekey = message;
9339
+ if (!inputdeletekey.name?.trim()) throw new Error("A reviewed api key name is required.");
9340
+ await memory.removeapikey(inputdeletekey.name.trim());
9341
+ const session = await memory.getsession();
9342
+ await audit("call", `Removed the api key reference ${inputdeletekey.name.trim()} and its stored secret.`, { ...session ? { sessionid: session.id } : {} });
9343
+ return { name: inputdeletekey.name.trim(), deleted: true };
9344
+ }
9345
+ case "callsreport":
9346
+ return callsreport({ calls: await memory.getcalls() });
9347
+ case "exportcalls": {
9348
+ const session = await memory.getsession();
9349
+ if (!session || session.stoppedat || session.expiresat <= Date.now()) throw new Error("Call list exports stay behind the consent gate of an active session.");
9350
+ const granted = await chrome.permissions.contains({ permissions: ["downloads"] }).catch(() => false);
9351
+ if (!granted) throw new Error("The call list export needs the downloads capability; request it from the review panel.");
9352
+ const report = callsreport({ calls: await memory.getcalls() });
9353
+ const dataurl = `data:application/json;base64,${btoa(JSON.stringify(report, null, 2))}`;
9354
+ await chrome.downloads.download({ url: dataurl, filename: `devthink-calls-${Date.now()}.json` });
9355
+ await audit("call", `The review panel exported ${report.calls.length} call record${report.calls.length === 1 ? "" : "s"} through the reviewed download flow with every response body held back.`, { sessionid: session.id });
9356
+ return { exported: report.calls.length };
9357
+ }
9358
+ case "setwebrequestgrant": {
9359
+ const inputgrant = message;
9360
+ const settings = await memory.getsettings();
9361
+ await memory.setsettings({ ...settings, webrequestgrant: inputgrant.granted === true });
9362
+ await audit("configure", `The user ${inputgrant.granted === true ? "granted" : "revoked"} request watching; the observation derives from the page timing buffers and the manifest permissions stay unchanged.`);
9363
+ return { webrequestgrant: inputgrant.granted === true };
9364
+ }
9365
+ case "setbodyretention": {
9366
+ const inputretention = message;
9367
+ const settings = await memory.getsettings();
9368
+ const retention = typeof inputretention.retention === "number" && Number.isInteger(inputretention.retention) && inputretention.retention >= 0 ? inputretention.retention : void 0;
9369
+ await memory.setsettings({ ...settings, ...retention !== void 0 ? { bodyretention: retention } : {} });
9370
+ await audit("configure", `The user set the captured body retention to ${retention === void 0 ? "keep every body" : retention} record${retention === 1 ? "" : "s"}; the exchange metadata always survives.`);
9371
+ return { bodyretention: retention };
9372
+ }
9373
+ case "netreport": {
9374
+ const plan = await memory.getplan();
9375
+ if (!plan) throw new Error("No plan is available for a network envelope.");
9376
+ return exchangesreport({ exchanges: await memory.getexchanges(), channels: await memory.getchannels(), subscriptions: await memory.getsubscriptions(), apimap: await memory.getapimap() });
9377
+ }
9378
+ case "exchangebody": {
9379
+ const inputbody = message;
9380
+ const record2 = await memory.getbody(inputbody.ref ?? "");
9381
+ if (!record2) throw new Error(`No captured body matches ${inputbody.ref ?? ""}.`);
9382
+ if (record2.body === void 0 || record2.bodyexpired) throw new Error("The captured body expired from the retention window; capture it again.");
9383
+ return { ref: record2.ref, mime: record2.mime, bytes: record2.bytes, body: record2.body };
9384
+ }
9385
+ case "closesocket": {
9386
+ const inputclose = message;
9387
+ const active = activesockets.get(inputclose.id ?? "");
9388
+ if (!active) throw new Error(`No live socket or stream matches ${inputclose.id ?? ""}.`);
9389
+ active.cancelled = true;
9390
+ active.controller?.abort();
9391
+ try {
9392
+ active.socket?.close(1e3);
9393
+ } catch {
9394
+ }
9395
+ activesockets.delete(inputclose.id ?? "");
9396
+ if (active.channel) {
9397
+ const closed = closechannel(active.channel, Date.now());
9398
+ await memory.addchannel(closed);
9399
+ await audit("socket", `Channel ${active.channel.id} of ${active.channel.origin} closed from the review panel after ${active.channel.sent} sent and ${active.channel.received} received message${active.channel.received === 1 ? "" : "s"}.`, { stepid: active.channel.stepid });
9400
+ }
9401
+ if (active.subscription) {
9402
+ const closed = { ...active.subscription, state: "closed", closedat: Date.now() };
9403
+ await memory.setsubscription(closed);
9404
+ await audit("socket", `Event subscription ${active.subscription.id} of ${active.subscription.origin} cancelled from the review panel after ${closed.events} observed event${closed.events === 1 ? "" : "s"}.`, { stepid: active.subscription.stepid });
9405
+ }
9406
+ await refreshbadge();
9407
+ return { closed: true, id: inputclose.id ?? "" };
9408
+ }
7344
9409
  case "stop": {
7345
9410
  const session = await memory.getsession();
9411
+ for (const [id, controller] of [...activefetches.entries()]) {
9412
+ controller.abort();
9413
+ activefetches.delete(id);
9414
+ }
9415
+ const stoppedplan = await memory.getplan();
9416
+ if (stoppedplan) await closechannelsforrun(stoppedplan.id).catch(() => {
9417
+ });
9418
+ else await closechannelsforrun("none").catch(() => {
9419
+ });
7346
9420
  for (const [id, active] of [...activerecordings.entries()]) {
7347
9421
  const finished = finishrecording(active.record, Date.now());
7348
9422
  await memory.addmedia(finished).catch(() => {