@wenathlan/extension 1.1.43 → 1.1.45

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.
@@ -862,15 +862,22 @@ var sessionmemory = class {
862
862
  async getfetchconsents() {
863
863
  return await this.adapter.get("fetchconsents") ?? [];
864
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]);
865
+ /** Stores one api key entry with its origin scope and created time, replacing the previous entry of that name; the key material stays behind its storage id. */
866
+ async setapikey(entry) {
867
+ const records = (await this.adapter.get("apikeys") ?? []).filter((item) => item.name !== entry.name);
868
+ await this.adapter.set("apikeys", [entry, ...records]);
869
869
  }
870
- /** Returns every stored api key reference with its origin scope, header name and storage id; key material never loads here. */
870
+ /** Returns every stored api key entry with its origin scope, header name, storage id and last use timestamp; key material never loads here. */
871
871
  async getapikeys() {
872
872
  return await this.adapter.get("apikeys") ?? [];
873
873
  }
874
+ /** Stamps the last use timestamp of one stored api key entry without ever loading the key material. */
875
+ async touchapikey(name, at) {
876
+ const records = await this.getapikeys();
877
+ const entry = records.find((item) => item.name === name);
878
+ if (!entry) return;
879
+ await this.adapter.set("apikeys", [{ ...entry, lastuse: at }, ...records.filter((item) => item.name !== name)]);
880
+ }
874
881
  /** Removes one api key reference and its stored secret together. */
875
882
  async removeapikey(name) {
876
883
  const records = await this.getapikeys();
@@ -967,6 +974,171 @@ var sessionmemory = class {
967
974
  async getsubscriptions() {
968
975
  return await this.adapter.get("subscriptions") ?? [];
969
976
  }
977
+ /** Stores one registered request block rule of a run, replacing the previous rule of that id; every rule reverts and stays auditable after the run ends. */
978
+ async addblockrule(rule) {
979
+ const records = (await this.adapter.get("blockrules") ?? []).filter((item) => item.id !== rule.id);
980
+ await this.adapter.set("blockrules", [rule, ...records]);
981
+ }
982
+ /** Returns every stored block rule, newest first. */
983
+ async getblockrules() {
984
+ return await this.adapter.get("blockrules") ?? [];
985
+ }
986
+ /** Stores one registered response mock fixture of a run, replacing the previous fixture of that id; the fixture body stays out of every audit trail. */
987
+ async addmockspec(spec) {
988
+ const records = (await this.adapter.get("mockspecs") ?? []).filter((item) => item.id !== spec.id);
989
+ await this.adapter.set("mockspecs", [spec, ...records]);
990
+ }
991
+ /** Returns every stored mock fixture, newest first. */
992
+ async getmockspecs() {
993
+ return await this.adapter.get("mockspecs") ?? [];
994
+ }
995
+ /** Stores one registered header rewrite rule of a run, replacing the previous rule of that id; the provenance of every applied rule stays auditable. */
996
+ async addheaderule(rule) {
997
+ const records = (await this.adapter.get("headerules") ?? []).filter((item) => item.id !== rule.id);
998
+ await this.adapter.set("headerules", [rule, ...records]);
999
+ }
1000
+ /** Returns every stored header rewrite rule, newest first. */
1001
+ async getheaderules() {
1002
+ return await this.adapter.get("headerules") ?? [];
1003
+ }
1004
+ /** Stores one cookie operation of a run per domain with its timestamp; cookie values never enter the operation record. */
1005
+ async addcookieop(operation) {
1006
+ const records = await this.adapter.get("cookieops") ?? [];
1007
+ await this.adapter.set("cookieops", [operation, ...records]);
1008
+ }
1009
+ /** Returns every stored cookie operation, newest first, optionally filtered by domain. */
1010
+ async getcookieops(domain) {
1011
+ const records = await this.adapter.get("cookieops") ?? [];
1012
+ return records.filter((item) => domain === void 0 || item.domain === domain);
1013
+ }
1014
+ /** Stores one token record of a provider with scopes, origin scope and expiry; the token values stay behind their storage ids. */
1015
+ async addtoken(record2) {
1016
+ const records = (await this.adapter.get("tokens") ?? []).filter((item) => item.id !== record2.id);
1017
+ await this.adapter.set("tokens", [record2, ...records]);
1018
+ }
1019
+ /** Returns every stored token record, newest first, optionally filtered by provider; token values never load here. */
1020
+ async listtokens(provider) {
1021
+ const records = await this.adapter.get("tokens") ?? [];
1022
+ return records.filter((item) => provider === void 0 || item.provider === provider);
1023
+ }
1024
+ /** Stores one applied proxy route of a run with its apply time, replacing the previous route of that id; the history keeps the revert times. */
1025
+ async addproxyroute(route) {
1026
+ const records = (await this.adapter.get("proxyroutes") ?? []).filter((item) => item.id !== route.id);
1027
+ await this.adapter.set("proxyroutes", [route, ...records]);
1028
+ }
1029
+ /** Returns every stored proxy route with apply and revert times, newest first. */
1030
+ async getproxyroutes() {
1031
+ return await this.adapter.get("proxyroutes") ?? [];
1032
+ }
1033
+ /** Stores one parsed rate limit read per origin, replacing the previous read of that origin. */
1034
+ async setratelimit(read) {
1035
+ const records = (await this.adapter.get("ratelimits") ?? []).filter((item) => item.origin !== read.origin);
1036
+ await this.adapter.set("ratelimits", [read, ...records]);
1037
+ }
1038
+ /** Returns every stored rate limit read whose reset window has not passed yet; expired states drop out at their reset windows. */
1039
+ async getratelimits(now) {
1040
+ const records = await this.adapter.get("ratelimits") ?? [];
1041
+ const live = records.filter((item) => item.resetat > now);
1042
+ if (live.length !== records.length) await this.adapter.set("ratelimits", live);
1043
+ return live;
1044
+ }
1045
+ /** Stores one run timeline entry; the user configured timeline retention window expires the oldest entries while their level counts survive in the per run level summaries. */
1046
+ async addtimelineentry(entry) {
1047
+ const records = await this.gettimeline();
1048
+ const combined = [entry, ...records];
1049
+ const retention = (await this.getsettings())?.timelineretention;
1050
+ if (retention === void 0) {
1051
+ await this.adapter.set("timelineentries", combined);
1052
+ return;
1053
+ }
1054
+ const kept = combined.slice(0, retention);
1055
+ const expired = combined.slice(retention);
1056
+ if (expired.length > 0) {
1057
+ const expiredcounts = /* @__PURE__ */ new Map();
1058
+ for (const item of expired) {
1059
+ const counts = expiredcounts.get(item.runid) ?? {};
1060
+ counts[item.level] = (counts[item.level] ?? 0) + 1;
1061
+ expiredcounts.set(item.runid, counts);
1062
+ }
1063
+ for (const [runid, counts] of expiredcounts) await this.mergelevelsummary(runid, counts, Date.now());
1064
+ }
1065
+ await this.adapter.set("timelineentries", kept);
1066
+ }
1067
+ /** Returns every stored run timeline entry, newest first. */
1068
+ async gettimeline() {
1069
+ return await this.adapter.get("timelineentries") ?? [];
1070
+ }
1071
+ /** Returns the run timeline entries filtered by run, level and step id. */
1072
+ async listtimeline(filter) {
1073
+ const records = await this.gettimeline();
1074
+ return records.filter((item) => (filter.runid === void 0 || item.runid === filter.runid) && (filter.level === void 0 || item.level === filter.level) && (filter.stepid === void 0 || item.stepid === filter.stepid));
1075
+ }
1076
+ /** Stores one captured javascript error record with its stack frames, source url and line. */
1077
+ async adderrorrecord(record2) {
1078
+ const records = await this.adapter.get("errorrecords") ?? [];
1079
+ await this.adapter.set("errorrecords", [record2, ...records]);
1080
+ }
1081
+ /** Returns every stored error record, newest first. */
1082
+ async geterrorrecords() {
1083
+ return await this.adapter.get("errorrecords") ?? [];
1084
+ }
1085
+ /** Stores one captured unhandled rejection record with its reason and stack frames. */
1086
+ async addrejectionrecord(record2) {
1087
+ const records = await this.adapter.get("rejectionrecords") ?? [];
1088
+ await this.adapter.set("rejectionrecords", [record2, ...records]);
1089
+ }
1090
+ /** Returns every stored rejection record, newest first. */
1091
+ async getrejectionrecords() {
1092
+ return await this.adapter.get("rejectionrecords") ?? [];
1093
+ }
1094
+ /** Stores one captured long task entry with its duration, start time and attribution names. */
1095
+ async addlongtask(record2) {
1096
+ const records = await this.adapter.get("longtasks") ?? [];
1097
+ await this.adapter.set("longtasks", [record2, ...records]);
1098
+ }
1099
+ /** Returns every stored long task entry, newest first. */
1100
+ async getlongtasks() {
1101
+ return await this.adapter.get("longtasks") ?? [];
1102
+ }
1103
+ /** Stores one console diff result between two runs, replacing the previous one. */
1104
+ async addconsolediff(diff) {
1105
+ return this.adapter.set("consolediff", diff);
1106
+ }
1107
+ /** Returns the one stored console diff result. */
1108
+ async getdiff() {
1109
+ return this.adapter.get("consolediff");
1110
+ }
1111
+ /** Stores one log rotation target record with its overflow entry counts, replacing the previous record of that target and run. */
1112
+ async addrotationtarget(record2) {
1113
+ const records = (await this.adapter.get("rotationtargets") ?? []).filter((item) => !(item.target === record2.target && item.runid === record2.runid));
1114
+ await this.adapter.set("rotationtargets", [record2, ...records]);
1115
+ }
1116
+ /** Returns every stored rotation target record with its overflow entry counts, newest first. */
1117
+ async getrotationtargets() {
1118
+ return await this.adapter.get("rotationtargets") ?? [];
1119
+ }
1120
+ /** Stores one console capture consent decision per origin; the approved decision persists so console watching on that origin prompts once. */
1121
+ async setconsoleconsent(consent) {
1122
+ const records = (await this.adapter.get("consoleconsents") ?? []).filter((item) => item.id !== consent.id);
1123
+ await this.adapter.set("consoleconsents", [consent, ...records]);
1124
+ }
1125
+ /** Returns every console capture consent decision, newest first. */
1126
+ async getconsoleconsents() {
1127
+ return await this.adapter.get("consoleconsents") ?? [];
1128
+ }
1129
+ /** Merges expired entry counts into the per run level count summary that survives the retention window. */
1130
+ async mergelevelsummary(runid, counts, now) {
1131
+ const records = await this.getlevelsummaries();
1132
+ const existing = records.find((item) => item.runid === runid);
1133
+ const merged = { ...existing?.counts ?? {} };
1134
+ for (const [level, count] of Object.entries(counts)) merged[level] = (merged[level] ?? 0) + count;
1135
+ const updated = { runid, counts: merged, at: now };
1136
+ await this.adapter.set("levelsummaries", [updated, ...records.filter((item) => item.runid !== runid)]);
1137
+ }
1138
+ /** Returns every per run level count summary, newest first. */
1139
+ async getlevelsummaries() {
1140
+ return await this.adapter.get("levelsummaries") ?? [];
1141
+ }
970
1142
  };
971
1143
  function mediakindof(record2) {
972
1144
  if ("pages" in record2) return "pdf";
@@ -1659,10 +1831,416 @@ function extractvalues(body, paths) {
1659
1831
  return fields.map((field) => ({ path: field.path, ...field.value !== void 0 ? { value: field.value } : {}, ...field.missing ? { missing: true } : {} }));
1660
1832
  }
1661
1833
 
1834
+ // netcontrol.ts
1835
+ var controlkinds = ["blockrequest", "mockresponse", "rewriteheaders", "setcookies", "readcookies", "clearcookies", "authflow", "saveapikey", "routeproxy", "postform", "postfiles"];
1836
+ function patternorigin(pattern) {
1837
+ const trimmed = pattern.trim();
1838
+ if (!trimmed.startsWith("https://")) return void 0;
1839
+ const rest = trimmed.slice("https://".length);
1840
+ const host = rest.split("/")[0] ?? "";
1841
+ if (!host.trim()) return void 0;
1842
+ return `https://${host.toLowerCase()}`;
1843
+ }
1844
+ function matchurlpattern(pattern, url) {
1845
+ const origin = patternorigin(pattern);
1846
+ if (!origin) return false;
1847
+ let parsed;
1848
+ try {
1849
+ parsed = new URL(url);
1850
+ } catch {
1851
+ return false;
1852
+ }
1853
+ if (parsed.origin !== origin) return false;
1854
+ const patternpath = pattern.trim().slice(origin.length);
1855
+ if (patternpath === "" || patternpath === "/") return true;
1856
+ const segments = patternpath.split("/").filter((segment) => segment !== "");
1857
+ if (segments.includes("**")) return true;
1858
+ const pathsegments = parsed.pathname.split("/").filter((segment) => segment !== "");
1859
+ if (segments.length !== pathsegments.length) return false;
1860
+ return segments.every((segment, index) => segment === pathsegments[index] || segment.includes("*") && new RegExp(`^${segment.split("*").map((part) => part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join(".*")}$`).test(pathsegments[index] ?? ""));
1861
+ }
1862
+ function blockruleof(value) {
1863
+ if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
1864
+ const options = value;
1865
+ if (typeof options.urlpattern !== "string" || !options.urlpattern.trim()) return void 0;
1866
+ const rule = { urlpattern: options.urlpattern.trim() };
1867
+ if (Array.isArray(options.resourcetypes)) {
1868
+ const types = options.resourcetypes.filter((item) => typeof item === "string" && item.trim().length > 0);
1869
+ if (types.length === 0) return void 0;
1870
+ rule.resourcetypes = types;
1871
+ }
1872
+ return rule;
1873
+ }
1874
+ function newblockrule(input) {
1875
+ return { id: input.id, runid: input.runid, stepid: input.stepid, urlpattern: input.urlpattern, ...input.resourcetypes !== void 0 ? { resourcetypes: input.resourcetypes } : {}, hits: 0, registeredat: input.at };
1876
+ }
1877
+ function mockspecof(value) {
1878
+ if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
1879
+ const options = value;
1880
+ if (typeof options.urlpattern !== "string" || !options.urlpattern.trim()) return void 0;
1881
+ if (typeof options.status !== "number" || !Number.isInteger(options.status) || options.status < 100 || options.status > 599) return void 0;
1882
+ const hasbody = typeof options.body === "string";
1883
+ const bodyref = typeof options.bodyref === "string" ? options.bodyref.trim() : "";
1884
+ if (!hasbody && bodyref === "") return void 0;
1885
+ const spec = { urlpattern: options.urlpattern.trim(), status: options.status };
1886
+ if (hasbody) spec.body = options.body;
1887
+ if (bodyref !== "") spec.bodyref = bodyref;
1888
+ if (options.headers && typeof options.headers === "object" && !Array.isArray(options.headers)) spec.headers = options.headers;
1889
+ if (options.reviewed === true) spec.reviewed = true;
1890
+ return spec;
1891
+ }
1892
+ function newmockspec(input) {
1893
+ return { id: input.id, runid: input.runid, stepid: input.stepid, urlpattern: input.urlpattern, status: input.status, ...input.headers !== void 0 ? { headers: input.headers } : {}, ...input.body !== void 0 ? { body: input.body } : {}, ...input.bodyref !== void 0 ? { bodyref: input.bodyref } : {}, reviewed: input.reviewed, hits: 0, registeredat: input.at };
1894
+ }
1895
+ function mockfor(url, specs) {
1896
+ return specs.find((spec) => spec.revertedat === void 0 && matchurlpattern(spec.urlpattern, url));
1897
+ }
1898
+ function headeruleof(value) {
1899
+ if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
1900
+ const options = value;
1901
+ if (typeof options.urlpattern !== "string" || !options.urlpattern.trim()) return void 0;
1902
+ if (typeof options.name !== "string" || !options.name.trim()) return void 0;
1903
+ if (options.operation !== "set" && options.operation !== "append" && options.operation !== "remove") return void 0;
1904
+ if (options.operation === "remove" && options.value !== void 0) return void 0;
1905
+ if (options.operation !== "remove" && typeof options.value !== "string") return void 0;
1906
+ const rule = { urlpattern: options.urlpattern.trim(), name: options.name.trim(), operation: options.operation };
1907
+ if (options.operation !== "remove") rule.value = typeof options.value === "string" ? options.value : "";
1908
+ return rule;
1909
+ }
1910
+ function newheaderule(input) {
1911
+ return { id: input.id, runid: input.runid, stepid: input.stepid, urlpattern: input.urlpattern, name: input.name, operation: input.operation, ...input.value !== void 0 ? { value: input.value } : {}, hits: 0, registeredat: input.at };
1912
+ }
1913
+ function applyheaderules(url, headers, rules) {
1914
+ const rewritten = { ...headers };
1915
+ const applied = [];
1916
+ for (const rule of rules) {
1917
+ if (rule.revertedat !== void 0) continue;
1918
+ if (!matchurlpattern(rule.urlpattern, url)) continue;
1919
+ const name = rule.name;
1920
+ if (rule.operation === "remove") {
1921
+ delete rewritten[name];
1922
+ applied.push(rule);
1923
+ continue;
1924
+ }
1925
+ const value = rule.value ?? "";
1926
+ if (rule.operation === "set") rewritten[name] = value;
1927
+ else rewritten[name] = rewritten[name] !== void 0 ? `${rewritten[name]}, ${value}` : value;
1928
+ applied.push(rule);
1929
+ }
1930
+ return { headers: rewritten, applied };
1931
+ }
1932
+ function revertrule(rule, at) {
1933
+ if (rule.revertedat !== void 0) return rule;
1934
+ return { ...rule, revertedat: at };
1935
+ }
1936
+ function cookierecordof(value) {
1937
+ if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
1938
+ const options = value;
1939
+ if (typeof options.name !== "string" || !options.name.trim()) return void 0;
1940
+ if (typeof options.domain !== "string" || !options.domain.trim()) return void 0;
1941
+ if (typeof options.path !== "string" || !options.path.trim()) return void 0;
1942
+ if (typeof options.value !== "string") return void 0;
1943
+ const record2 = { name: options.name.trim(), domain: options.domain.trim().toLowerCase(), path: options.path.trim(), value: options.value };
1944
+ if (typeof options.expiresat === "number" && Number.isFinite(options.expiresat)) record2.expiresat = options.expiresat;
1945
+ return record2;
1946
+ }
1947
+ function cookiedomaingranted(domain, grants) {
1948
+ const host = domain.trim().toLowerCase().replace(/^\./, "");
1949
+ return grants.some((grant) => {
1950
+ let granthost = "";
1951
+ try {
1952
+ granthost = new URL(grant).hostname.toLowerCase();
1953
+ } catch {
1954
+ return false;
1955
+ }
1956
+ return host === granthost || host.endsWith(`.${granthost}`);
1957
+ });
1958
+ }
1959
+ function redactedcookies(records) {
1960
+ return records.map((record2) => ({ name: record2.name, domain: record2.domain, path: record2.path, ...record2.expiresat !== void 0 ? { expiresat: record2.expiresat } : {} }));
1961
+ }
1962
+ function proxyrouteof(value) {
1963
+ if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
1964
+ const options = value;
1965
+ if (options.scheme !== "http" && options.scheme !== "https" && options.scheme !== "socks4" && options.scheme !== "socks5") return void 0;
1966
+ if (typeof options.host !== "string" || !options.host.trim()) return void 0;
1967
+ if (typeof options.port !== "number" || !Number.isInteger(options.port) || options.port < 1 || options.port > 65535) return void 0;
1968
+ if (!Array.isArray(options.bypass) || options.bypass.length === 0 || !options.bypass.every((item) => typeof item === "string" && item.trim().length > 0)) return void 0;
1969
+ return { scheme: options.scheme, host: options.host.trim(), port: options.port, bypass: options.bypass.map((item) => item.trim()) };
1970
+ }
1971
+ function ratelimitreadof(headers, origin, now) {
1972
+ const pick = (name) => {
1973
+ for (const key of Object.keys(headers)) {
1974
+ if (key.toLowerCase() !== name) continue;
1975
+ const value = Number(headers[key]);
1976
+ return Number.isFinite(value) && value >= 0 ? value : void 0;
1977
+ }
1978
+ return void 0;
1979
+ };
1980
+ const remaining = pick("x-ratelimit-remaining");
1981
+ const limit = pick("x-ratelimit-limit");
1982
+ const reset = pick("x-ratelimit-reset");
1983
+ if (remaining === void 0 && limit === void 0 && reset === void 0) return void 0;
1984
+ const read = { origin, ...remaining !== void 0 ? { remaining } : {}, ...limit !== void 0 ? { limit } : {}, resetat: now, at: now };
1985
+ if (reset !== void 0) read.resetat = reset > Math.floor(now / 1e3) ? reset * 1e3 : now + reset * 1e3;
1986
+ return read;
1987
+ }
1988
+ function retryafterof(status, headers) {
1989
+ if (status !== 429 && status !== 503) return void 0;
1990
+ for (const key of Object.keys(headers)) {
1991
+ if (key.toLowerCase() !== "retry-after") continue;
1992
+ const raw = headers[key];
1993
+ if (raw === void 0) continue;
1994
+ const value = raw.trim();
1995
+ const seconds = Number(value);
1996
+ if (Number.isFinite(seconds) && seconds >= 0) return seconds * 1e3;
1997
+ const date = Date.parse(value);
1998
+ if (Number.isFinite(date)) return Math.max(0, date - Date.now());
1999
+ return void 0;
2000
+ }
2001
+ return void 0;
2002
+ }
2003
+ function ratelimitwait(state, now) {
2004
+ if (!state) return 0;
2005
+ return Math.max(0, state.resetat - now);
2006
+ }
2007
+
2008
+ // netauth.ts
2009
+ function oauthflowof(value) {
2010
+ if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
2011
+ const options = value;
2012
+ if (typeof options.provider !== "string" || !options.provider.trim()) return void 0;
2013
+ if (typeof options.authorizeurl !== "string" || !options.authorizeurl.trim()) return void 0;
2014
+ if (typeof options.tokenurl !== "string" || !options.tokenurl.trim()) return void 0;
2015
+ if (!Array.isArray(options.scopes) || options.scopes.length === 0 || !options.scopes.every((item) => typeof item === "string" && item.trim().length > 0)) return void 0;
2016
+ if (typeof options.redirectorigin !== "string" || !options.redirectorigin.trim()) return void 0;
2017
+ return { provider: options.provider.trim(), authorizeurl: options.authorizeurl.trim(), tokenurl: options.tokenurl.trim(), scopes: options.scopes.map((item) => item.trim()), redirectorigin: options.redirectorigin.trim() };
2018
+ }
2019
+ function authorizeurl(flow, state) {
2020
+ const url = new URL(flow.authorizeurl);
2021
+ url.searchParams.set("response_type", "code");
2022
+ url.searchParams.set("redirect_uri", flow.redirectorigin);
2023
+ url.searchParams.set("scope", flow.scopes.join(" "));
2024
+ url.searchParams.set("state", state);
2025
+ return url.toString();
2026
+ }
2027
+ function capturecode(url, redirectorigin, state) {
2028
+ let parsed;
2029
+ try {
2030
+ parsed = new URL(url);
2031
+ } catch {
2032
+ return { error: "The redirect url does not parse for the code capture." };
2033
+ }
2034
+ const granted = redirectorigin.includes("/", redirectorigin.indexOf("://") + 3) ? `${parsed.origin}${parsed.pathname}`.startsWith(redirectorigin) : parsed.origin === redirectorigin;
2035
+ if (!granted) return { error: `The redirect landed on ${parsed.origin} outside the granted redirect origin ${redirectorigin}.` };
2036
+ const returned = parsed.searchParams.get("state");
2037
+ if (returned !== state) return { error: "The redirect state token does not match the reviewed flow." };
2038
+ const error = parsed.searchParams.get("error");
2039
+ if (error) return { error: `The provider refused the flow: ${error}.` };
2040
+ const code = parsed.searchParams.get("code");
2041
+ if (!code) return { error: "The redirect carries no authorization code." };
2042
+ return { code };
2043
+ }
2044
+ function parsetokens(body) {
2045
+ let parsed;
2046
+ try {
2047
+ parsed = JSON.parse(body);
2048
+ } catch {
2049
+ return void 0;
2050
+ }
2051
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return void 0;
2052
+ const record2 = parsed;
2053
+ const tokens = {};
2054
+ if (typeof record2.access_token === "string" && record2.access_token) tokens.accesstoken = record2.access_token;
2055
+ if (typeof record2.refresh_token === "string" && record2.refresh_token) tokens.refreshtoken = record2.refresh_token;
2056
+ if (typeof record2.expires_in === "number" && Number.isFinite(record2.expires_in) && record2.expires_in >= 0) tokens.expiresin = record2.expires_in;
2057
+ if (typeof record2.scope === "string" && record2.scope.trim()) tokens.scopes = record2.scope.trim().split(/\s+/);
2058
+ if (tokens.accesstoken === void 0 && tokens.refreshtoken === void 0) return void 0;
2059
+ return tokens;
2060
+ }
2061
+ function tokenrequest(flow, input) {
2062
+ if (input.refreshtoken !== void 0) return { url: flow.tokenurl, body: urlencodeform([{ name: "grant_type", value: "refresh_token" }, { name: "refresh_token", value: input.refreshtoken }]) };
2063
+ return { url: flow.tokenurl, body: urlencodeform([{ name: "grant_type", value: "authorization_code" }, { name: "code", value: input.code ?? "" }, { name: "redirect_uri", value: flow.redirectorigin }]) };
2064
+ }
2065
+ function revocationruleof(value) {
2066
+ if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
2067
+ const options = value;
2068
+ if (!Array.isArray(options.tokenids) || options.tokenids.length === 0 || !options.tokenids.every((item) => typeof item === "string" && item.trim().length > 0)) return void 0;
2069
+ if (typeof options.reason !== "string" || !options.reason.trim()) return void 0;
2070
+ return { tokenids: options.tokenids.map((item) => item.trim()), reason: options.reason.trim(), revokedat: Date.now() };
2071
+ }
2072
+ function formpayloadof(value) {
2073
+ if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
2074
+ const options = value;
2075
+ if (typeof options.url !== "string" || !options.url.trim()) return void 0;
2076
+ if (!Array.isArray(options.fields) || options.fields.length === 0) return void 0;
2077
+ const fields = [];
2078
+ for (const item of options.fields) {
2079
+ if (!item || typeof item !== "object" || Array.isArray(item)) return void 0;
2080
+ const field = item;
2081
+ if (typeof field.name !== "string" || !field.name.trim()) return void 0;
2082
+ if (typeof field.value !== "string") return void 0;
2083
+ fields.push({ name: field.name.trim(), value: field.value });
2084
+ }
2085
+ return { url: options.url.trim(), fields };
2086
+ }
2087
+ function urlencodeform(fields) {
2088
+ return fields.map((field) => `${formencode(field.name)}=${formencode(field.value)}`).join("&");
2089
+ }
2090
+ function formencode(value) {
2091
+ const bytes = [...new TextEncoder().encode(value)];
2092
+ return bytes.map((byte) => byte >= 65 && byte <= 90 || byte >= 97 && byte <= 122 || byte >= 48 && byte <= 57 || byte === 45 || byte === 95 || byte === 46 || byte === 126 ? String.fromCharCode(byte) : `%${byte.toString(16).toUpperCase().padStart(2, "0")}`).join("");
2093
+ }
2094
+ function multipartpayloadof(value) {
2095
+ if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
2096
+ const options = value;
2097
+ if (typeof options.url !== "string" || !options.url.trim()) return void 0;
2098
+ if (!Array.isArray(options.files) || options.files.length === 0) return void 0;
2099
+ const fields = [];
2100
+ for (const item of Array.isArray(options.fields) ? options.fields : []) {
2101
+ if (!item || typeof item !== "object" || Array.isArray(item)) return void 0;
2102
+ const field = item;
2103
+ if (typeof field.name !== "string" || !field.name.trim()) return void 0;
2104
+ if (typeof field.value !== "string") return void 0;
2105
+ fields.push({ name: field.name.trim(), value: field.value });
2106
+ }
2107
+ const files = [];
2108
+ for (const item of options.files) {
2109
+ if (!item || typeof item !== "object" || Array.isArray(item)) return void 0;
2110
+ const file = item;
2111
+ if (typeof file.name !== "string" || !file.name.trim()) return void 0;
2112
+ if (typeof file.filename !== "string" || !file.filename.trim()) return void 0;
2113
+ if (typeof file.mime !== "string" || !file.mime.trim()) return void 0;
2114
+ if (typeof file.content !== "string") return void 0;
2115
+ if (file.reviewed !== true) return void 0;
2116
+ files.push({ name: file.name.trim(), filename: file.filename.trim(), mime: file.mime.trim(), content: file.content, reviewed: true });
2117
+ }
2118
+ const payload = { url: options.url.trim(), fields, files, ...typeof options.boundary === "string" && options.boundary.trim() ? { boundary: options.boundary.trim() } : {} };
2119
+ return payload;
2120
+ }
2121
+ function newboundary() {
2122
+ return `----devthink${Math.random().toString(16).slice(2)}${Date.now().toString(16)}`;
2123
+ }
2124
+ function multipartchunks(payload) {
2125
+ const boundary = payload.boundary ?? newboundary();
2126
+ const chunks = [];
2127
+ for (const field of payload.fields) chunks.push(`--${boundary}\r
2128
+ content-disposition: form-data; name="${field.name}"\r
2129
+ \r
2130
+ ${field.value}\r
2131
+ `);
2132
+ for (const file of payload.files) chunks.push(`--${boundary}\r
2133
+ content-disposition: form-data; name="${file.name}"; filename="${file.filename}"\r
2134
+ content-type: ${file.mime}\r
2135
+ \r
2136
+ ${file.content}\r
2137
+ `);
2138
+ chunks.push(`--${boundary}--\r
2139
+ `);
2140
+ return { chunks, boundary, bytes: chunks.reduce((total, chunk) => total + chunk.length, 0) };
2141
+ }
2142
+
2143
+ // runtimeline.ts
2144
+ var timelinekinds = ["watchconsole", "watcherrors", "watchtasks"];
2145
+ var loglevels = ["error", "warn", "info", "log", "debug", "trace"];
2146
+ var timelinesources = ["console", "error", "rejection", "resource", "longtask", "network"];
2147
+ function attachtimeline(input) {
2148
+ return { runid: input.runid, origin: input.origin, stepids: [...input.stepids], attachedat: input.now, entries: [] };
2149
+ }
2150
+ function spamdetect(entries, rule) {
2151
+ const collapsed = [];
2152
+ const counts = /* @__PURE__ */ new Map();
2153
+ for (const entry of entries) {
2154
+ if (rule.pattern !== "" && !entry.message.includes(rule.pattern)) {
2155
+ collapsed.push({ ...entry, repeat: 1 });
2156
+ continue;
2157
+ }
2158
+ const key = `${entry.level}|${entry.source}|${entry.message}`;
2159
+ const previous = collapsed[collapsed.length - 1];
2160
+ if (previous && previous.repeat !== void 0 && `${previous.level}|${previous.source}|${previous.message}` === key && entry.time - previous.time <= rule.windowsize) {
2161
+ previous.repeat += 1;
2162
+ continue;
2163
+ }
2164
+ collapsed.push({ ...entry, repeat: 1 });
2165
+ }
2166
+ for (const entry of collapsed) {
2167
+ if (entry.repeat > 1) counts.set(`${entry.level}|${entry.source}|${entry.message}`, entry.repeat);
2168
+ }
2169
+ const flagged = [...counts.entries()].filter(([, count]) => count > rule.collapse).map(([key, count]) => ({ message: key.split("|").slice(2).join("|"), count }));
2170
+ return { entries: collapsed, flagged };
2171
+ }
2172
+ function rotatelogs(entries, rule) {
2173
+ if (entries.length <= rule.maxentries) return { kept: [...entries], overflow: [] };
2174
+ const kept = entries.slice(entries.length - rule.maxentries);
2175
+ const overflow = entries.slice(0, entries.length - rule.maxentries);
2176
+ return { kept, overflow };
2177
+ }
2178
+ function timelinecounts(entries) {
2179
+ const counts = {};
2180
+ for (const level of loglevels) counts[level] = 0;
2181
+ for (const entry of entries) counts[entry.level] = (counts[entry.level] ?? 0) + 1;
2182
+ return counts;
2183
+ }
2184
+ function blockingduration(tasks, stepid, window2) {
2185
+ const inside = tasks.filter((task) => task.starttime >= window2.startedat && task.starttime <= window2.endedat);
2186
+ return { stepid, blocking: inside.reduce((total, task) => total + task.duration, 0), tasks: inside.length };
2187
+ }
2188
+ function netfailureentryof(input) {
2189
+ const exchange = input.exchange;
2190
+ if (exchange.errorclass === void 0 && exchange.status < 400) return null;
2191
+ return { id: input.id, runid: exchange.runid, stepid: exchange.stepid, url: exchange.url, status: exchange.status, errorclass: exchange.errorclass ?? "httperror", correlationid: exchange.correlationid, at: input.at };
2192
+ }
2193
+ function watcherdetached(input) {
2194
+ for (const navigation of input.navigations) {
2195
+ if (navigation >= input.startedat && navigation <= input.startedat + input.lifetime) return { detached: true, at: navigation };
2196
+ }
2197
+ return { detached: false };
2198
+ }
2199
+ function consolediff(input) {
2200
+ const base = input.baselines;
2201
+ const target = input.targetlines;
2202
+ const basemap = /* @__PURE__ */ new Map();
2203
+ for (const line of base) basemap.set(line, (basemap.get(line) ?? 0) + 1);
2204
+ const targetmap = /* @__PURE__ */ new Map();
2205
+ for (const line of target) targetmap.set(line, (targetmap.get(line) ?? 0) + 1);
2206
+ const lines = [];
2207
+ const added = [];
2208
+ const removed = [];
2209
+ const repeated = [];
2210
+ for (const [line, count] of targetmap) {
2211
+ const basecount = basemap.get(line) ?? 0;
2212
+ if (basecount === 0) {
2213
+ for (let index = 0; index < count; index += 1) {
2214
+ lines.push({ kind: "added", text: line });
2215
+ added.push(line);
2216
+ }
2217
+ continue;
2218
+ }
2219
+ const share = Math.min(basecount, count);
2220
+ for (let index = 0; index < share; index += 1) {
2221
+ lines.push({ kind: "repeated", text: line, count: share });
2222
+ repeated.push(line);
2223
+ }
2224
+ for (let index = share; index < count; index += 1) {
2225
+ lines.push({ kind: "added", text: line });
2226
+ added.push(line);
2227
+ }
2228
+ }
2229
+ for (const [line, count] of basemap) {
2230
+ const targetcount = targetmap.get(line) ?? 0;
2231
+ const missing = Math.max(0, count - targetcount);
2232
+ for (let index = 0; index < missing; index += 1) {
2233
+ lines.push({ kind: "removed", text: line });
2234
+ removed.push(line);
2235
+ }
2236
+ }
2237
+ return { base: input.baseid, target: input.targetid, lines, added: added.length, removed: removed.length, repeated: repeated.length, at: input.now };
2238
+ }
2239
+
1662
2240
  // policy.ts
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"]);
2241
+ 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", "blockrequest", "mockresponse", "rewriteheaders", "setcookies", "clearcookies", "authflow", "saveapikey", "routeproxy", "postform", "postfiles"]);
1664
2242
  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"]);
2243
+ 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", "readcookies", "watchconsole", "watcherrors", "watchtasks"]);
1666
2244
  var allowedactions = /* @__PURE__ */ new Set([...sensitiveactions, ...interactionactions, ...readactions]);
1667
2245
  var watchactions = /* @__PURE__ */ new Set(["watchmutate", "watchbanner", "watchfocus", "watchtab"]);
1668
2246
  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"]);
@@ -1677,6 +2255,8 @@ var mediaactions = /* @__PURE__ */ new Set(["capturepdf", "recordscreen", "captu
1677
2255
  var httpactions = /* @__PURE__ */ new Set(["fetchurl", "parsejson", "parsehtml", "callrest", "callgraphql"]);
1678
2256
  var socketactions = /* @__PURE__ */ new Set(["opensocket", "sendmessage", "waitmessage", "subscribesse", "longpoll"]);
1679
2257
  var netwatchactions = /* @__PURE__ */ new Set(["watchrequests", "readheaders", "capturebodies", "mapapi", "extractapi"]);
2258
+ var controlactions = /* @__PURE__ */ new Set(["blockrequest", "mockresponse", "rewriteheaders", "setcookies", "readcookies", "clearcookies", "authflow", "saveapikey", "routeproxy", "postform", "postfiles"]);
2259
+ var debugactions = /* @__PURE__ */ new Set(["watchconsole", "watcherrors", "watchtasks"]);
1680
2260
  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"]);
1681
2261
  var fieldkinds = ["text", "email", "phone", "date", "number", "select", "check", "radio", "file", "password", "card", "code"];
1682
2262
  var layoutmutationactions = /* @__PURE__ */ new Set(["grouptabs", "colorgroup", "collapsegroup", "savelayout", "restorelayout"]);
@@ -1692,6 +2272,9 @@ function hostpattern(origin) {
1692
2272
  if (parsed.protocol !== "https:") throw new Error("Only HTTPS origins can be granted.");
1693
2273
  return `${parsed.origin}/*`;
1694
2274
  }
2275
+ function isdebugkind(kind) {
2276
+ return debugactions.has(kind);
2277
+ }
1695
2278
  function actionrisk(kind) {
1696
2279
  if (!allowedactions.has(kind)) throw new Error("Unsupported browser action.");
1697
2280
  if (sensitiveactions.has(kind)) return "sensitive";
@@ -1717,6 +2300,7 @@ function requiredcapability(kind) {
1717
2300
  if (kind === "readclipboard") return "clipboardRead";
1718
2301
  if (kind === "writeclipboard" || kind === "copyscreen") return "clipboardWrite";
1719
2302
  if (kind === "downloadimages") return "downloads";
2303
+ if (kind === "authflow") return "tabs";
1720
2304
  if (kind === "openlink" || kind === "openprivate" || kind === "navlist" || kind === "batchopen" || kind === "reopentab" || kind === "deeplink") return "tabs";
1721
2305
  if (tabscommandactions.has(kind)) return "tabs";
1722
2306
  return void 0;
@@ -2416,6 +3000,9 @@ function issocketkind(kind) {
2416
3000
  function isnetwatchkind(kind) {
2417
3001
  return netwatchactions.has(kind);
2418
3002
  }
3003
+ function iscontrolkind(kind) {
3004
+ return controlactions.has(kind);
3005
+ }
2419
3006
  function resolvedrisk(step) {
2420
3007
  if (step.kind === "capturebodies") {
2421
3008
  let options = {};
@@ -2776,6 +3363,238 @@ function validatenetwatchgrammar(step, options) {
2776
3363
  }
2777
3364
  return { allowed: true };
2778
3365
  }
3366
+ function validatecontrolgrammar(step, options) {
3367
+ const kind = step.kind;
3368
+ if (kind === "blockrequest") {
3369
+ const rule = blockruleof(options.block);
3370
+ if (!rule) return { allowed: false, reason: "A reviewed block rule with a url pattern is required in options.block." };
3371
+ if (patternorigin(rule.urlpattern) === void 0) return { allowed: false, reason: "Block rules need an https origin pattern; patterns without a named origin are refused." };
3372
+ if (options.block.reviewed !== true) return { allowed: false, reason: "The block rule carries the explicit reviewed flag before any request is blocked." };
3373
+ }
3374
+ if (kind === "mockresponse") {
3375
+ const spec = mockspecof(options.mock);
3376
+ if (!spec) return { allowed: false, reason: "A reviewed mock fixture with a url pattern, status and its reviewed body or a captured body ref is required in options.mock." };
3377
+ if (patternorigin(spec.urlpattern) === void 0) return { allowed: false, reason: "Mock fixtures need an https origin pattern; patterns without a named origin are refused." };
3378
+ if (spec.reviewed !== true) return { allowed: false, reason: "Every mock fixture is reviewed with its full body or the referenced captured body through the explicit reviewed flag before it serves." };
3379
+ }
3380
+ if (kind === "rewriteheaders") {
3381
+ const rules = options.rules;
3382
+ if (!Array.isArray(rules) || rules.length === 0) return { allowed: false, reason: "A reviewed non-empty list of header rewrite rules is required in options.rules." };
3383
+ for (const item of rules) {
3384
+ const rule = headeruleof(item);
3385
+ if (!rule) return { allowed: false, reason: "Every header rewrite rule needs a url pattern, header name, a set, append or remove operation and its value." };
3386
+ if (patternorigin(rule.urlpattern) === void 0) return { allowed: false, reason: "Header rewrite rules must name their origin pattern explicitly; patterns without a named origin are refused." };
3387
+ }
3388
+ }
3389
+ if (kind === "setcookies") {
3390
+ const cookies = options.cookies;
3391
+ if (!Array.isArray(cookies) || cookies.length === 0) return { allowed: false, reason: "A reviewed non-empty list of cookie records is required in options.cookies." };
3392
+ for (const item of cookies) {
3393
+ if (!cookierecordof(item)) return { allowed: false, reason: "Every cookie record needs a name, domain, path and reviewed string value with an optional expiry." };
3394
+ }
3395
+ }
3396
+ if (kind === "readcookies" && options.domain !== void 0 && !isnonempty(options.domain)) return { allowed: false, reason: "The reviewed cookie read domain must be a non-empty host." };
3397
+ if (kind === "clearcookies") {
3398
+ if (!isnonempty(options.domain)) return { allowed: false, reason: "A reviewed cookie domain is required before cookies are cleared." };
3399
+ if (options.names !== void 0 && (!Array.isArray(options.names) || options.names.length === 0 || !options.names.every((name) => isnonempty(name)))) return { allowed: false, reason: "The reviewed cookie clear list must be a non-empty list of cookie names when present." };
3400
+ }
3401
+ if (kind === "authflow") {
3402
+ const flow = oauthflowof(options.oauth);
3403
+ if (!flow) return { allowed: false, reason: "A reviewed oauth flow with provider, authorize url, token url, scopes and redirect origin is required in options.oauth." };
3404
+ if (!ishttpsurl(flow.authorizeurl) || !ishttpsurl(flow.tokenurl)) return { allowed: false, reason: "The oauth authorize and token urls must use HTTPS." };
3405
+ if (!ishttpsurl(flow.redirectorigin) && !/^https:\/\/[^/]+\/?$/.test(flow.redirectorigin)) return { allowed: false, reason: "The oauth redirect origin must be an HTTPS origin inside the grants." };
3406
+ const consent = authconsentgranted(step);
3407
+ if (!consent.allowed) return consent;
3408
+ }
3409
+ if (kind === "saveapikey") {
3410
+ const key = options.key;
3411
+ if (!key || typeof key !== "object" || Array.isArray(key)) return { allowed: false, reason: "A reviewed api key entry with name, origin scopes and header is required in options.key." };
3412
+ const entry = key;
3413
+ if (!isnonempty(entry.name)) return { allowed: false, reason: "The api key entry needs a reviewed non-empty name." };
3414
+ if (!Array.isArray(entry.origins) || entry.origins.length === 0 || !entry.origins.every((item) => ishttpsurl(item))) return { allowed: false, reason: "The api key needs a reviewed non-empty list of HTTPS origin scopes." };
3415
+ if (!isnonempty(entry.header)) return { allowed: false, reason: "The api key entry needs a reviewed non-empty header name." };
3416
+ if (typeof entry.value !== "string" || !entry.value) return { allowed: false, reason: "The api key needs its secret value in the reviewed options; it never enters the audit trail." };
3417
+ const consent = apikeyconsentgranted(step);
3418
+ if (!consent.allowed) return consent;
3419
+ }
3420
+ if (kind === "routeproxy") {
3421
+ if (!proxyrouteof(options.proxy)) return { allowed: false, reason: "A reviewed proxy route with scheme, host, port and a non-empty bypass list is required in options.proxy." };
3422
+ if (!isnonempty(options.consentref)) return { allowed: false, reason: "Proxy routing needs the explicit reviewed consent ref before any route applies." };
3423
+ }
3424
+ if (kind === "postform") {
3425
+ const form = formpayloadof(options.form);
3426
+ if (!form) return { allowed: false, reason: "A reviewed form payload with a url and a non-empty field list is required in options.form." };
3427
+ if (!ishttpsurl(form.url)) return { allowed: false, reason: "The form submission target must use HTTPS." };
3428
+ if (options.wait !== void 0 && (typeof options.wait !== "number" || !Number.isFinite(options.wait) || options.wait < 0)) return { allowed: false, reason: "The reviewed rate limit wait budget must be zero or a positive number of milliseconds." };
3429
+ }
3430
+ if (kind === "postfiles") {
3431
+ const upload = multipartpayloadof(options.upload);
3432
+ if (!upload) return { allowed: false, reason: "A reviewed multipart upload with a url and reviewed files is required in options.upload; every file carries the explicit reviewed flag." };
3433
+ if (!ishttpsurl(upload.url)) return { allowed: false, reason: "The multipart upload target must use HTTPS." };
3434
+ if (options.wait !== void 0 && (typeof options.wait !== "number" || !Number.isFinite(options.wait) || options.wait < 0)) return { allowed: false, reason: "The reviewed rate limit wait budget must be zero or a positive number of milliseconds." };
3435
+ }
3436
+ return { allowed: true };
3437
+ }
3438
+ function blockgate(session, step, now) {
3439
+ if (!session || session.stoppedat) return { allowed: false, reason: "No active browser session exists for the request block." };
3440
+ if (session.expiresat <= now) return { allowed: false, reason: "The browser session has expired and cannot block requests." };
3441
+ if (session.pausedat) return { allowed: false, reason: "The browser session is paused and cannot block requests." };
3442
+ let options = {};
3443
+ try {
3444
+ options = parseoptions(step);
3445
+ } catch {
3446
+ options = {};
3447
+ }
3448
+ const rule = options.block;
3449
+ if (!rule || typeof rule !== "object" || Array.isArray(rule) || rule.reviewed !== true) return { allowed: false, reason: "Request blocking needs its reviewed block rule with the explicit reviewed flag before any rule applies." };
3450
+ if (!blockruleof(rule)) return { allowed: false, reason: "The block rule needs a url pattern and an optional resource type list." };
3451
+ return { allowed: true };
3452
+ }
3453
+ function cookiegate(session, domain, now) {
3454
+ if (!session || session.stoppedat) return { allowed: false, reason: "No active browser session exists for the cookie operation." };
3455
+ if (session.expiresat <= now) return { allowed: false, reason: "The browser session has expired and cannot touch cookies." };
3456
+ if (session.pausedat) return { allowed: false, reason: "The browser session is paused and cannot touch cookies." };
3457
+ const grants = session.grants ?? [session.origin];
3458
+ if (!cookiedomaingranted(domain, grants)) return { allowed: false, reason: `The cookie domain ${domain} stays outside the session origin grants; cookie control refuses domains beyond the grants.` };
3459
+ return { allowed: true };
3460
+ }
3461
+ function proxygate(session, step, now) {
3462
+ if (!session || session.stoppedat) return { allowed: false, reason: "No active browser session exists for the proxy route." };
3463
+ if (session.expiresat <= now) return { allowed: false, reason: "The browser session has expired and cannot change routing." };
3464
+ if (session.pausedat) return { allowed: false, reason: "The browser session is paused and cannot change routing." };
3465
+ let options = {};
3466
+ try {
3467
+ options = parseoptions(step);
3468
+ } catch {
3469
+ options = {};
3470
+ }
3471
+ if (!isnonempty(options.consentref)) return { allowed: false, reason: "Proxy routing needs the explicit reviewed consent ref before any route applies." };
3472
+ if (!proxyrouteof(options.proxy)) return { allowed: false, reason: "The proxy route needs a scheme, host, port and a non-empty bypass list of origins that stay direct." };
3473
+ return { allowed: true };
3474
+ }
3475
+ function authconsentgranted(step) {
3476
+ let options = {};
3477
+ try {
3478
+ options = parseoptions(step);
3479
+ } catch {
3480
+ options = {};
3481
+ }
3482
+ const consentref = options.consentref;
3483
+ if (typeof consentref !== "string" || !consentref.trim()) return { allowed: false, reason: "An oauth flow requires the reviewed provider consent prompt ref in options before it starts." };
3484
+ return { allowed: true };
3485
+ }
3486
+ function apikeyconsentgranted(step) {
3487
+ let options = {};
3488
+ try {
3489
+ options = parseoptions(step);
3490
+ } catch {
3491
+ options = {};
3492
+ }
3493
+ const consentref = options.consentref;
3494
+ if (typeof consentref !== "string" || !consentref.trim()) return { allowed: false, reason: "Storing an api key requires the explicit reviewed consent prompt ref in options before anything is stored." };
3495
+ return { allowed: true };
3496
+ }
3497
+ function ratelimitbudgetallowed(wait, budget) {
3498
+ if (wait !== void 0 && (typeof wait !== "number" || !Number.isFinite(wait) || wait < 0)) return { allowed: false, reason: "The rate limit wait must be zero or a positive number of milliseconds." };
3499
+ if (budget !== void 0 && (typeof budget !== "number" || !Number.isFinite(budget) || budget < 0)) return { allowed: false, reason: "The reviewed rate limit budget must be zero or a positive number of milliseconds." };
3500
+ if (wait !== void 0 && budget !== void 0 && wait > budget) return { allowed: false, reason: `The rate limit wait of ${wait} milliseconds exceeds the reviewed budget of ${budget} milliseconds; review a wider budget or submit later.` };
3501
+ return { allowed: true };
3502
+ }
3503
+ function timelinegate(session, tabid2, origin, now) {
3504
+ if (!session || session.stoppedat) return { allowed: false, reason: "No active browser session exists for the timeline capture." };
3505
+ if (session.expiresat <= now) return { allowed: false, reason: "The browser session has expired and cannot capture the timeline." };
3506
+ if (session.pausedat) return { allowed: false, reason: "The browser session is paused and cannot capture the timeline." };
3507
+ if (session.tabid !== tabid2) return { allowed: false, reason: `The timeline capture needs the run tab ${session.tabid} and refuses tab ${tabid2}.` };
3508
+ if (!origingranted(session, origin)) return { allowed: false, reason: `The timeline capture of ${origin} needs the session origin grants first.` };
3509
+ return { allowed: true };
3510
+ }
3511
+ function consoleconsentcovers(origin, consents) {
3512
+ if (consents.some((consent) => consent.origin === origin && consent.approved === true)) return { allowed: true };
3513
+ return { allowed: false, reason: `Console capture on ${origin} needs the reviewed console consent first; approve the prompt in the review panel.` };
3514
+ }
3515
+ function stackgate(session, origin) {
3516
+ if (!origingranted(session, origin)) return { allowed: false, reason: `Stack capture of ${origin} stays outside the session origin grants.` };
3517
+ return { allowed: true };
3518
+ }
3519
+ function debugwaitbudgetallowed(watchwindow, wait) {
3520
+ if (watchwindow !== void 0 && (typeof watchwindow !== "number" || !Number.isFinite(watchwindow) || watchwindow < 0)) return { allowed: false, reason: "The debug watch window must be zero or a positive number of milliseconds." };
3521
+ if (wait !== void 0 && (typeof wait !== "number" || !Number.isFinite(wait) || wait < 0)) return { allowed: false, reason: "The reviewed debug wait budget must be zero or a positive number of milliseconds." };
3522
+ if (watchwindow !== void 0 && wait !== void 0 && watchwindow > wait) return { allowed: false, reason: `The debug watch window of ${watchwindow} milliseconds exceeds the reviewed wait budget of ${wait} milliseconds; review a wider budget or a shorter window.` };
3523
+ return { allowed: true };
3524
+ }
3525
+ function validatetimelinegrammar(step, options) {
3526
+ const kind = step.kind;
3527
+ let watchwindow;
3528
+ if (options.watch !== void 0) {
3529
+ const watch = options.watch;
3530
+ if (!watch || typeof watch !== "object" || Array.isArray(watch)) return { allowed: false, reason: "The reviewed debug watch window must be an object." };
3531
+ const reviewed = watch;
3532
+ if (reviewed.window !== void 0) {
3533
+ if (typeof reviewed.window !== "number" || !Number.isFinite(reviewed.window) || reviewed.window < 0) return { allowed: false, reason: "The reviewed debug watch window must be zero or a positive number of milliseconds." };
3534
+ watchwindow = reviewed.window;
3535
+ }
3536
+ }
3537
+ const budgetcheck = debugwaitbudgetallowed(watchwindow, typeof options.wait === "number" ? options.wait : void 0);
3538
+ if (!budgetcheck.allowed) return budgetcheck;
3539
+ if (options.level !== void 0 && !loglevels.includes(options.level)) return { allowed: false, reason: `The reviewed level floor must be one of ${loglevels.join(", ")}.` };
3540
+ if (options.sources !== void 0) {
3541
+ if (!Array.isArray(options.sources) || options.sources.length === 0 || !options.sources.every((source) => timelinesources.includes(source))) return { allowed: false, reason: `The reviewed source filters must be a non-empty list of the reviewed timeline sources: ${timelinesources.join(", ")}.` };
3542
+ }
3543
+ if (kind === "watchconsole") {
3544
+ if (options.redact === void 0 || !Array.isArray(options.redact) || options.redact.length === 0 || !options.redact.every((pattern) => isnonempty(pattern))) return { allowed: false, reason: "Console capture requires a reviewed non-empty redaction pattern list before any console text is captured." };
3545
+ if (options.depth !== void 0 && (typeof options.depth !== "number" || !Number.isInteger(options.depth) || options.depth < 1)) return { allowed: false, reason: "The reviewed serialization depth bound must be a positive integer with no code ceiling." };
3546
+ if (options.spam !== void 0) {
3547
+ const rule = spamruleof(options.spam);
3548
+ if (!rule) return { allowed: false, reason: "The reviewed spam rule needs a pattern, a window size and a collapse threshold." };
3549
+ if (rule.collapse < 1) return { allowed: false, reason: "The reviewed spam collapse threshold must be a positive integer of user configured value with no code ceiling." };
3550
+ }
3551
+ if (options.rotation !== void 0) {
3552
+ const rule = rotationruleof(options.rotation);
3553
+ if (!rule) return { allowed: false, reason: "The reviewed rotation rule needs a max entry count and an overflow target." };
3554
+ }
3555
+ }
3556
+ if (kind === "watchtasks") {
3557
+ if (options.threshold !== void 0 && (typeof options.threshold !== "number" || !Number.isFinite(options.threshold) || options.threshold < 0)) return { allowed: false, reason: "The reviewed long task threshold must be zero or a positive number of milliseconds with no code ceiling." };
3558
+ }
3559
+ return { allowed: true };
3560
+ }
3561
+ function spamruleof(value) {
3562
+ if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
3563
+ const entry = value;
3564
+ const pattern = typeof entry.pattern === "string" ? entry.pattern : "";
3565
+ const windowsize = typeof entry.windowsize === "number" && Number.isFinite(entry.windowsize) && entry.windowsize >= 0 ? entry.windowsize : void 0;
3566
+ const collapse = typeof entry.collapse === "number" && Number.isInteger(entry.collapse) ? entry.collapse : void 0;
3567
+ if (windowsize === void 0 || collapse === void 0) return void 0;
3568
+ return { pattern, windowsize, collapse };
3569
+ }
3570
+ function rotationruleof(value) {
3571
+ if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
3572
+ const entry = value;
3573
+ const maxentries = typeof entry.maxentries === "number" && Number.isInteger(entry.maxentries) && entry.maxentries >= 1 ? entry.maxentries : void 0;
3574
+ const overflowtarget = typeof entry.overflowtarget === "string" && entry.overflowtarget.trim() ? entry.overflowtarget.trim() : void 0;
3575
+ if (maxentries === void 0 || overflowtarget === void 0) return void 0;
3576
+ return { maxentries, overflowtarget };
3577
+ }
3578
+ function controltarget(step) {
3579
+ let options = {};
3580
+ try {
3581
+ options = parseoptions(step);
3582
+ } catch {
3583
+ options = {};
3584
+ }
3585
+ for (const key of ["form", "upload"]) {
3586
+ const value = options[key];
3587
+ if (value && typeof value === "object" && !Array.isArray(value)) {
3588
+ const url = value.url;
3589
+ if (typeof url === "string" && url.trim()) return url.trim();
3590
+ }
3591
+ }
3592
+ if (step.kind === "authflow") {
3593
+ const flow = oauthflowof(options.oauth);
3594
+ if (flow) return flow.tokenurl;
3595
+ }
3596
+ return void 0;
3597
+ }
2779
3598
  function sockettarget(step) {
2780
3599
  let options = {};
2781
3600
  try {
@@ -3161,6 +3980,14 @@ function validatestep(step, origin) {
3161
3980
  const netwatchcheck = validatenetwatchgrammar(step, options);
3162
3981
  if (!netwatchcheck.allowed) return netwatchcheck;
3163
3982
  }
3983
+ if (iscontrolkind(step.kind)) {
3984
+ const controlcheck = validatecontrolgrammar(step, options);
3985
+ if (!controlcheck.allowed) return controlcheck;
3986
+ }
3987
+ if (isdebugkind(step.kind)) {
3988
+ const timelinecheck = validatetimelinegrammar(step, options);
3989
+ if (!timelinecheck.allowed) return timelinecheck;
3990
+ }
3164
3991
  if (step.kind === "tabcreate") {
3165
3992
  if (options.background !== void 0 && typeof options.background !== "boolean") return { allowed: false, reason: "The reviewed background flag must be a boolean." };
3166
3993
  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." };
@@ -3262,6 +4089,59 @@ function canexecute(input) {
3262
4089
  const watchgatecheck = watchgate(input.session, input.settings, now);
3263
4090
  if (!watchgatecheck.allowed) return watchgatecheck;
3264
4091
  }
4092
+ if (isdebugkind(input.step.kind)) {
4093
+ const timelinegatecheck = timelinegate(input.session, input.tabid, input.origin, now);
4094
+ if (!timelinegatecheck.allowed) return timelinegatecheck;
4095
+ }
4096
+ if (iscontrolkind(input.step.kind)) {
4097
+ const controlgate = sessiongate({ session: input.session, tabid: input.tabid, origin: input.origin, now, action: "control the network" });
4098
+ if (!controlgate.allowed) return controlgate;
4099
+ let controloptions = {};
4100
+ try {
4101
+ controloptions = parseoptions(input.step);
4102
+ } catch {
4103
+ controloptions = {};
4104
+ }
4105
+ if (input.step.kind === "blockrequest") {
4106
+ const blockgatecheck = blockgate(input.session, input.step, now);
4107
+ if (!blockgatecheck.allowed) return blockgatecheck;
4108
+ const rule = blockruleof(controloptions.block);
4109
+ if (rule) {
4110
+ const blockorigin = origincheck(input.session, rule.urlpattern);
4111
+ if (!blockorigin.allowed) return blockorigin;
4112
+ }
4113
+ }
4114
+ if (input.step.kind === "mockresponse" || input.step.kind === "rewriteheaders") {
4115
+ const patterns = input.step.kind === "mockresponse" ? [mockspecof(controloptions.mock)?.urlpattern ?? ""] : Array.isArray(controloptions.rules) ? controloptions.rules.map((item) => item && typeof item === "object" && !Array.isArray(item) ? String(item.urlpattern ?? "") : "") : [];
4116
+ for (const pattern of patterns) {
4117
+ const patterngate = origincheck(input.session, pattern);
4118
+ if (!patterngate.allowed) return patterngate;
4119
+ }
4120
+ }
4121
+ if (input.step.kind === "setcookies" || input.step.kind === "readcookies" || input.step.kind === "clearcookies") {
4122
+ const domain = typeof controloptions.domain === "string" && controloptions.domain.trim() ? controloptions.domain : Array.isArray(controloptions.cookies) ? String(controloptions.cookies[0]?.domain ?? "") : "";
4123
+ if (!domain) return { allowed: false, reason: "A reviewed cookie domain is required before cookie control runs." };
4124
+ const cookiegatecheck = cookiegate(input.session, domain, now);
4125
+ if (!cookiegatecheck.allowed) return cookiegatecheck;
4126
+ }
4127
+ if (input.step.kind === "authflow") {
4128
+ const authconsent = authconsentgranted(input.step);
4129
+ if (!authconsent.allowed) return authconsent;
4130
+ }
4131
+ if (input.step.kind === "saveapikey") {
4132
+ const keyconsent = apikeyconsentgranted(input.step);
4133
+ if (!keyconsent.allowed) return keyconsent;
4134
+ }
4135
+ if (input.step.kind === "routeproxy") {
4136
+ const proxygatecheck = proxygate(input.session, input.step, now);
4137
+ if (!proxygatecheck.allowed) return proxygatecheck;
4138
+ }
4139
+ const target = controltarget(input.step);
4140
+ if (target !== void 0) {
4141
+ const targetgate = origincheck(input.session, target);
4142
+ if (!targetgate.allowed) return targetgate;
4143
+ }
4144
+ }
3265
4145
  if (input.step.kind === "extractapi") {
3266
4146
  let replayoptions = {};
3267
4147
  try {
@@ -3431,9 +4311,24 @@ function recordpoll(progress, planid, stepid, entry, now) {
3431
4311
  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
4312
  return recordoutcome(base, planid, outcome, now);
3433
4313
  }
4314
+ function recordcontrol(progress, planid, stepid, entry, now) {
4315
+ const base = progress && progress.planid === planid ? progress : emptyprogress(planid, now);
4316
+ const outcome = { stepid, ok: true, summary: `${entry.reason}: ${entry.applied} applied rule${entry.applied === 1 ? "" : "s"}, ${entry.blocked} blocked request${entry.blocked === 1 ? "" : "s"}, ${entry.mocked} mocked response${entry.mocked === 1 ? "" : "s"} and ${entry.reverts} reverted rule${entry.reverts === 1 ? "" : "s"}.`, details: { control: entry }, at: now };
4317
+ return recordoutcome(base, planid, outcome, now);
4318
+ }
4319
+ function recordupload(progress, planid, stepid, entry, now) {
4320
+ const base = progress && progress.planid === planid ? progress : emptyprogress(planid, now);
4321
+ const outcome = { stepid, ok: true, summary: `The multipart upload moved chunk ${entry.chunk} of ${entry.chunks} with ${entry.uploaded} of ${entry.bytes} bytes sent.`, details: { upload: entry }, at: now };
4322
+ return recordoutcome(base, planid, outcome, now);
4323
+ }
4324
+ function recordtimeline(progress, planid, stepid, entry, now) {
4325
+ const base = progress && progress.planid === planid ? progress : emptyprogress(planid, now);
4326
+ const outcome = { stepid, ok: true, summary: `Captured ${entry.entries} timeline entr${entry.entries === 1 ? "y" : "ies"} with ${entry.collapsed} collapsed repeat${entry.collapsed === 1 ? "" : "s"}, ${entry.errors} error${entry.errors === 1 ? "" : "s"}, ${entry.rejections} rejection${entry.rejections === 1 ? "" : "s"} and ${entry.longtasks} long task${entry.longtasks === 1 ? "" : "s"}.`, details: { timeline: entry }, at: now };
4327
+ return recordoutcome(base, planid, outcome, now);
4328
+ }
3434
4329
 
3435
4330
  // version.ts
3436
- var packageversion = "1.1.43";
4331
+ var packageversion = "1.1.45";
3437
4332
 
3438
4333
  // types.ts
3439
4334
  var protocolversion = packageversion;
@@ -3470,6 +4365,44 @@ function parseproposal(value, origin, grants) {
3470
4365
  ...typeof candidate.value === "string" ? { value: candidate.value } : {},
3471
4366
  ...typeof candidate.options === "string" ? { options: candidate.options } : {}
3472
4367
  };
4368
+ if (step.kind === "blockrequest") {
4369
+ let blockoptions = {};
4370
+ try {
4371
+ blockoptions = parseoptions(step);
4372
+ } catch {
4373
+ blockoptions = {};
4374
+ }
4375
+ const rule = blockruleof(blockoptions.block);
4376
+ if (rule && patternorigin(rule.urlpattern) === void 0) throw new Error("Block rules without a named origin pattern are refused.");
4377
+ }
4378
+ if (step.kind === "routeproxy") {
4379
+ let proxyoptions = {};
4380
+ try {
4381
+ proxyoptions = parseoptions(step);
4382
+ } catch {
4383
+ proxyoptions = {};
4384
+ }
4385
+ const proxy = proxyoptions.proxy;
4386
+ const bypass = proxy && typeof proxy === "object" && !Array.isArray(proxy) ? proxy.bypass : void 0;
4387
+ if (!Array.isArray(bypass) || bypass.length === 0) throw new Error("Proxy routes without a bypass list are refused.");
4388
+ }
4389
+ if (step.kind === "watchconsole" || step.kind === "watcherrors" || step.kind === "watchtasks") {
4390
+ let debugoptions = {};
4391
+ try {
4392
+ debugoptions = parseoptions(step);
4393
+ } catch {
4394
+ debugoptions = {};
4395
+ }
4396
+ const granted = covered.some((pattern) => {
4397
+ try {
4398
+ return new URL(origin).origin === new URL(pattern).origin;
4399
+ } catch {
4400
+ return false;
4401
+ }
4402
+ });
4403
+ if (!granted) throw new Error(`The ${step.kind} capture of ${origin} targets an origin outside the grants.`);
4404
+ if (debugoptions.level !== void 0 && !loglevels.includes(debugoptions.level)) throw new Error(`The reviewed level floor must be one of ${loglevels.join(", ")}.`);
4405
+ }
3473
4406
  const evaluation = validatestep(step, origin);
3474
4407
  if (!evaluation.allowed) throw new Error(evaluation.reason);
3475
4408
  const target = outboundtarget(step);
@@ -3544,7 +4477,7 @@ function requestbody(input) {
3544
4477
  return JSON.stringify({ version: protocolversion, objective: input.objective, session: input.session, observation: input.observation, capabilities: input.capabilities });
3545
4478
  }
3546
4479
  function outcomeresponse(input) {
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 } : {} });
4480
+ 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 } : {}, ...input.control ? { control: input.control } : {}, ...input.timeline ? { timeline: input.timeline } : {} });
3548
4481
  }
3549
4482
  function mapresponse(input) {
3550
4483
  return JSON.stringify({ version: protocolversion, planid: input.plan.id, planstate: input.plan.state, map: input.map });
@@ -3623,6 +4556,29 @@ function callsreport(input) {
3623
4556
  function exchangesreport(input) {
3624
4557
  return { version: protocolversion, exchanges: input.exchanges, channels: input.channels, subscriptions: input.subscriptions, apimap: input.apimap };
3625
4558
  }
4559
+ function authreport(input) {
4560
+ const tokens = input.tokens.map((token) => {
4561
+ const { accessstorageid, refreshstorageid, ...metadata } = token;
4562
+ void accessstorageid;
4563
+ void refreshstorageid;
4564
+ return metadata;
4565
+ });
4566
+ return { version: protocolversion, tokens };
4567
+ }
4568
+ function controlreport(input) {
4569
+ const mocks = input.mocks.map((spec) => {
4570
+ const { body, ...metadata } = spec;
4571
+ void body;
4572
+ return metadata;
4573
+ });
4574
+ return { version: protocolversion, blocks: input.blocks, mocks, rewrites: input.rewrites, cookies: input.cookies, proxies: input.proxies, ratelimits: input.ratelimits };
4575
+ }
4576
+ function timelinereport(input) {
4577
+ return { version: protocolversion, entries: input.entries, errors: input.errors, rejections: input.rejections, longtasks: input.longtasks, levelcounts: input.levelcounts };
4578
+ }
4579
+ function consolediffreport(input) {
4580
+ return { version: protocolversion, diff: input.diff };
4581
+ }
3626
4582
 
3627
4583
  // capture.ts
3628
4584
  var capturekinds = ["shotview", "shotfullpage", "shotelement", "shotregion", "contactsheet"];
@@ -5311,7 +6267,7 @@ function stepoptions2(step) {
5311
6267
  }
5312
6268
  async function refreshcapabilities() {
5313
6269
  const report = await readcapabilities();
5314
- const withmedia = { ...report, captures: [...capturekinds], media: [...mediakinds], http: [...httpkinds], netwatch: [...socketkinds, ...netwatchkinds] };
6270
+ const withmedia = { ...report, captures: [...capturekinds], media: [...mediakinds], http: [...httpkinds], netwatch: [...socketkinds, ...netwatchkinds], control: [...controlkinds], debug: [...timelinekinds] };
5315
6271
  await memory.setcapabilities(withmedia);
5316
6272
  return withmedia;
5317
6273
  }
@@ -5734,6 +6690,17 @@ async function tracktabupdate(tabid2, changeinfo) {
5734
6690
  if (status === "loading" && url) {
5735
6691
  navbuffers.set(tabid2, [{ event: "beforenavigate", url, timestamp: now }]);
5736
6692
  lastknownurls.set(tabid2, url);
6693
+ for (const [id, watcher] of [...activetimelinewatchers.entries()]) {
6694
+ const detached = watcherdetached({ startedat: watcher.startedat, lifetime: watcher.lifetime, navigations: [now] });
6695
+ if (!detached.detached) continue;
6696
+ const session = await memory.getsession();
6697
+ if (!session || session.tabid !== tabid2) continue;
6698
+ watcher.cancelled = true;
6699
+ await memory.closewatch(id, now).catch(() => {
6700
+ });
6701
+ await audit("timeline", `Watcher ${id} detached when the run tab navigated to ${url} at ${detached.at}; every page hook of the destroyed context is gone with it.`, { sessionid: session.id });
6702
+ activetimelinewatchers.delete(id);
6703
+ }
5737
6704
  return;
5738
6705
  }
5739
6706
  const buffer = navbuffers.get(tabid2) ?? [];
@@ -7876,7 +8843,7 @@ async function executehttpstep(step, session, plan, tabid2, origin) {
7876
8843
  streamstate.bytes = total;
7877
8844
  } } : void 0;
7878
8845
  try {
7879
- const transport = (url, init) => livefetch(url, init, controller, window2, streamstate);
8846
+ const transport = (url, init) => controlledfetch(plan.id, url, init, controller, window2, streamstate);
7880
8847
  const result = await sendfetch({ request, ...policy !== void 0 && Object.keys(policy).length > 0 ? { options: policy } : {}, transport, onretry: (attempt, wait, reason) => {
7881
8848
  void (async () => {
7882
8849
  await memory.setprogress(recordfetchretry(await memory.getprogress(), plan.id, step.id, { attempt, url: request.url, wait, reason }, Date.now()));
@@ -7932,7 +8899,7 @@ async function executehttpstep(step, session, plan, tabid2, origin) {
7932
8899
  const controller = new AbortController();
7933
8900
  activefetches.set(callid, controller);
7934
8901
  try {
7935
- const transport = (url, init) => livefetch(url, init, controller);
8902
+ const transport = (url, init) => controlledfetch(plan.id, url, init, controller);
7936
8903
  const keynames = Array.isArray(options.apikeys) ? options.apikeys.filter((name) => typeof name === "string" && name.trim().length > 0) : [];
7937
8904
  if (step.kind === "callrest") {
7938
8905
  const methodoverride = typeof options.method === "string" ? options.method.trim().toUpperCase() : void 0;
@@ -8262,6 +9229,11 @@ async function executenetwatchstep(step, session, plan, tabid2, origin) {
8262
9229
  observed.push(exchange);
8263
9230
  }
8264
9231
  const failed = observed.filter((exchange) => exchange.errorclass !== void 0).length;
9232
+ for (const exchange of observed) {
9233
+ const failure = netfailureentryof({ id: randomid(), exchange, at: Date.now() });
9234
+ if (!failure) continue;
9235
+ await memory.addtimelineentry({ id: failure.id, runid: failure.runid, stepid: failure.stepid, time: failure.at, level: "error", source: "network", message: `Request ${failure.correlationid} of ${failure.url} failed with the ${failure.errorclass} class${failure.status > 0 ? ` at status ${failure.status}` : ""}.` });
9236
+ }
8265
9237
  await refreshbadge();
8266
9238
  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
9239
  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." } };
@@ -8379,6 +9351,462 @@ async function executenetwatchstep(step, session, plan, tabid2, origin) {
8379
9351
  void origin;
8380
9352
  throw new Error("Unsupported request observation kind.");
8381
9353
  }
9354
+ var activetimelinewatchers = /* @__PURE__ */ new Map();
9355
+ function timelinedetail(entry, runid) {
9356
+ if (!entry || typeof entry !== "object") return null;
9357
+ const record2 = entry;
9358
+ if (typeof record2.stepid !== "string" || typeof record2.time !== "number" || typeof record2.message !== "string") return null;
9359
+ if (typeof record2.level !== "string" || !loglevels.includes(record2.level)) return null;
9360
+ if (typeof record2.source !== "string" || !timelinesources.includes(record2.source)) return null;
9361
+ return { id: randomid(), runid, stepid: record2.stepid, time: record2.time, level: record2.level, source: record2.source, message: record2.message };
9362
+ }
9363
+ async function executetimelinestep(step, session, plan, tabid2, origin) {
9364
+ const options = stepoptions2(step);
9365
+ const watch = options.watch && typeof options.watch === "object" && !Array.isArray(options.watch) ? options.watch : {};
9366
+ const watchwindow = typeof watch.window === "number" && Number.isFinite(watch.window) && watch.window >= 0 ? watch.window : 0;
9367
+ const extra = { sessionid: session.id, planid: plan.id, stepid: step.id };
9368
+ if (step.kind === "watchconsole") {
9369
+ const consents = await memory.getconsoleconsents();
9370
+ const consent = consoleconsentcovers(origin, consents);
9371
+ if (!consent.allowed) {
9372
+ const record2 = { id: randomid(), prompt: `Console capture on ${origin} for the reviewed ${watchwindow} millisecond window of step ${step.id}.`, origin, stepid: step.id, at: Date.now() };
9373
+ await memory.setconsoleconsent(record2);
9374
+ await refreshbadge();
9375
+ throw new Error(`${consent.reason} The prompt is open in the review panel; approve it and run the step again.`);
9376
+ }
9377
+ }
9378
+ if (step.kind === "watcherrors") {
9379
+ const stackgatecheck = stackgate(session, origin);
9380
+ if (!stackgatecheck.allowed) throw new Error(stackgatecheck.reason ?? "Stack capture stays outside the session origin grants.");
9381
+ }
9382
+ const startedat = Date.now();
9383
+ const bound = attachtimeline({ runid: plan.id, origin, stepids: plan.steps.map((item) => item.id), now: startedat });
9384
+ const watchid = randomid();
9385
+ const registration = { watchid, kind: step.kind, stepid: step.id, sessionid: session.id, origin, scopes: [], events: [], startedat, lifetime: watchwindow };
9386
+ await memory.addwatch(registration);
9387
+ await audit("timeline", `Watcher ${step.kind} attached under id ${watchid} on ${origin} for the reviewed window of ${watchwindow} milliseconds inside the run tab ${tabid2}.`, extra);
9388
+ activetimelinewatchers.set(watchid, { runid: plan.id, startedat, lifetime: watchwindow, cancelled: false });
9389
+ let output;
9390
+ try {
9391
+ output = await dispatchpagestep(step, tabid2, origin, plan) ?? { ok: false, summary: "The watch returned no result." };
9392
+ } catch (error) {
9393
+ activetimelinewatchers.delete(watchid);
9394
+ await memory.closewatch(watchid, Date.now());
9395
+ await audit("timeline", `Watcher ${watchid} detached when the run tab navigated or closed inside the reviewed window of ${watchwindow} milliseconds.`, extra);
9396
+ return { ok: false, summary: `The ${step.kind} watcher detached when the tab navigated or closed inside the reviewed window; run it again on the settled page (${error instanceof Error ? error.message : String(error)}).` };
9397
+ }
9398
+ const watcherstate = activetimelinewatchers.get(watchid);
9399
+ activetimelinewatchers.delete(watchid);
9400
+ await memory.closewatch(watchid, Date.now());
9401
+ await audit("timeline", `Watcher ${watchid} detached cleanly after its reviewed window of ${watchwindow} milliseconds.`, extra);
9402
+ if (watcherstate?.cancelled) {
9403
+ await audit("timeline", `Watcher ${watchid} cancelled on run cancel or the killswitch; the captured window is discarded.`, extra);
9404
+ return { ok: false, summary: `The ${step.kind} watcher was cancelled with the run; the captured window is discarded.` };
9405
+ }
9406
+ const captured = detailarray(output.details, "entries").map((entry) => timelinedetail(entry, plan.id)).filter((entry) => entry !== null);
9407
+ bound.entries.push(...captured);
9408
+ let collapsed = 0;
9409
+ let flagged = [];
9410
+ let stored = bound.entries;
9411
+ if (step.kind === "watchconsole") {
9412
+ const rule = spamruleof(options.spam);
9413
+ if (rule) {
9414
+ const outcome = spamdetect(bound.entries, rule);
9415
+ stored = outcome.entries;
9416
+ collapsed = bound.entries.length - stored.length;
9417
+ flagged = outcome.flagged;
9418
+ }
9419
+ }
9420
+ const rotation = rotationruleof(options.rotation);
9421
+ let rotationtarget;
9422
+ if (rotation) {
9423
+ const rotated = rotatelogs(stored, rotation);
9424
+ stored = rotated.kept;
9425
+ rotationtarget = { target: rotation.overflowtarget, runid: plan.id, entries: rotated.overflow.length, at: Date.now() };
9426
+ }
9427
+ const errorids = [];
9428
+ const rejectionids = [];
9429
+ const longtasks = [];
9430
+ for (const entry of detailarray(output.details, "errors")) {
9431
+ if (!entry || typeof entry !== "object") continue;
9432
+ const record2 = entry;
9433
+ const id = randomid();
9434
+ errorids.push(id);
9435
+ const capturederror = { id, runid: plan.id, stepid: step.id, message: typeof record2.message === "string" ? record2.message : "", frames: Array.isArray(record2.frames) ? record2.frames : [], sourceurl: typeof record2.sourceurl === "string" ? record2.sourceurl : "", line: typeof record2.line === "number" ? record2.line : 0, at: Date.now() };
9436
+ await memory.adderrorrecord(capturederror);
9437
+ }
9438
+ for (const entry of detailarray(output.details, "rejections")) {
9439
+ if (!entry || typeof entry !== "object") continue;
9440
+ const record2 = entry;
9441
+ const id = randomid();
9442
+ rejectionids.push(id);
9443
+ const capturedrejection = { id, runid: plan.id, stepid: step.id, reason: typeof record2.reason === "string" ? record2.reason : "", frames: Array.isArray(record2.frames) ? record2.frames : [], at: Date.now() };
9444
+ await memory.addrejectionrecord(capturedrejection);
9445
+ }
9446
+ for (const entry of detailarray(output.details, "longtasks")) {
9447
+ if (!entry || typeof entry !== "object") continue;
9448
+ const record2 = entry;
9449
+ const capturedtask = { id: randomid(), runid: plan.id, stepid: step.id, duration: typeof record2.duration === "number" ? record2.duration : 0, starttime: typeof record2.starttime === "number" ? record2.starttime : 0, attributions: Array.isArray(record2.attributions) ? record2.attributions.filter((name) => typeof name === "string") : [], at: Date.now() };
9450
+ longtasks.push(capturedtask);
9451
+ await memory.addlongtask(capturedtask);
9452
+ }
9453
+ for (const entry of stored) await memory.addtimelineentry(entry);
9454
+ if (rotationtarget) await memory.addrotationtarget(rotationtarget);
9455
+ const blocking = blockingduration(longtasks, step.id, { startedat, endedat: Date.now() });
9456
+ const evidence = { entries: stored.length, collapsed, errors: errorids.length, rejections: rejectionids.length, longtasks: longtasks.length };
9457
+ await memory.setprogress(recordtimeline(await memory.getprogress(), plan.id, step.id, evidence, Date.now()));
9458
+ await refreshbadge();
9459
+ const counts = timelinecounts(stored);
9460
+ await audit("timeline", `Captured ${stored.length} timeline entr${stored.length === 1 ? "y" : "ies"} of ${origin} for the reviewed window of ${watchwindow} milliseconds${collapsed > 0 ? ` with ${collapsed} collapsed repeat${collapsed === 1 ? "" : "s"}` : ""}${errorids.length > 0 ? `, ${errorids.length} error${errorids.length === 1 ? "" : "s"}` : ""}${rejectionids.length > 0 ? `, ${rejectionids.length} rejection${rejectionids.length === 1 ? "" : "s"}` : ""}${longtasks.length > 0 ? ` and ${longtasks.length} long task${longtasks.length === 1 ? "" : "s"}` : ""}; console, error and task watching derives from page-injected listeners and the performance buffers.`, extra);
9461
+ return { ok: Boolean(output.ok), summary: output.summary, details: { ...output.details ?? {}, timeline: { entries: stored.length, levels: counts, collapsed }, entries: stored, errorids, rejectionids, longtasks: longtasks.map((task) => ({ ...task, blocking: blocking.blocking })), ...flagged.length > 0 ? { flagged } : {}, ...rotationtarget ? { rotation: rotationtarget } : {} } };
9462
+ }
9463
+ var activerules = /* @__PURE__ */ new Map();
9464
+ var activeauthflows = /* @__PURE__ */ new Map();
9465
+ function rulesetof(runid) {
9466
+ const existing = activerules.get(runid);
9467
+ if (existing) return existing;
9468
+ const created = { blocks: [], mocks: [], rewrites: [] };
9469
+ activerules.set(runid, created);
9470
+ return created;
9471
+ }
9472
+ async function revertcontrolsforrun(runid, reason) {
9473
+ const ruleset = activerules.get(runid);
9474
+ if (!ruleset) return;
9475
+ const at = Date.now();
9476
+ let reverted = 0;
9477
+ for (const rule of ruleset.blocks) {
9478
+ if (rule.revertedat !== void 0) continue;
9479
+ await memory.addblockrule(revertrule(rule, at)).catch(() => void 0);
9480
+ await audit("control", `Reverted the block rule ${rule.id} of ${rule.urlpattern} with ${rule.hits} blocked request${rule.hits === 1 ? "" : "s"} at ${reason}.`, { stepid: rule.stepid }).catch(() => void 0);
9481
+ reverted += 1;
9482
+ }
9483
+ for (const spec of ruleset.mocks) {
9484
+ if (spec.revertedat !== void 0) continue;
9485
+ await memory.addmockspec(revertrule(spec, at)).catch(() => void 0);
9486
+ await audit("control", `Reverted the mock fixture ${spec.id} of ${spec.urlpattern} with ${spec.hits} served response${spec.hits === 1 ? "" : "s"} at ${reason}.`, { stepid: spec.stepid }).catch(() => void 0);
9487
+ reverted += 1;
9488
+ }
9489
+ for (const rule of ruleset.rewrites) {
9490
+ if (rule.revertedat !== void 0) continue;
9491
+ await memory.addheaderule(revertrule(rule, at)).catch(() => void 0);
9492
+ await audit("control", `Reverted the header rewrite rule ${rule.id} of ${rule.urlpattern} with ${rule.hits} applied request${rule.hits === 1 ? "" : "s"} at ${reason}.`, { stepid: rule.stepid }).catch(() => void 0);
9493
+ reverted += 1;
9494
+ }
9495
+ if (ruleset.proxy && ruleset.proxy.revertedat === void 0) {
9496
+ await memory.addproxyroute(revertrule(ruleset.proxy, at)).catch(() => void 0);
9497
+ await audit("control", `Reverted the proxy route ${ruleset.proxy.id} of ${ruleset.proxy.scheme}://${ruleset.proxy.host}:${ruleset.proxy.port} at ${reason}; the previous routing state is restored.`, { stepid: ruleset.proxy.stepid }).catch(() => void 0);
9498
+ reverted += 1;
9499
+ }
9500
+ activerules.delete(runid);
9501
+ if (reverted > 0) {
9502
+ await memory.setprogress(recordcontrol(await memory.getprogress(), runid, ruleset.blocks[0]?.stepid ?? ruleset.rewrites[0]?.stepid ?? ruleset.mocks[0]?.stepid ?? ruleset.proxy?.stepid ?? "control", { applied: 0, blocked: 0, mocked: 0, reverts: reverted, reason: `The traffic rules reverted at ${reason}` }, at)).catch(() => void 0);
9503
+ await refreshbadge().catch(() => void 0);
9504
+ }
9505
+ }
9506
+ async function cancelauthflowsforrun(runid, reason) {
9507
+ for (const [state, active] of [...activeauthflows.entries()]) {
9508
+ if (active.runid !== runid) continue;
9509
+ active.cancelled = true;
9510
+ if (active.tabid) await chrome.tabs.remove(active.tabid).catch(() => void 0);
9511
+ activeauthflows.delete(state);
9512
+ await audit("auth", `Cancelled the oauth flow of provider ${active.flow.provider} at ${reason} before any token was exchanged.`, { stepid: active.stepid }).catch(() => void 0);
9513
+ }
9514
+ }
9515
+ async function controlledfetch(runid, url, init, controller, window2, streamstate) {
9516
+ const ruleset = activerules.get(runid);
9517
+ if (ruleset) {
9518
+ for (const rule of ruleset.blocks) {
9519
+ if (rule.revertedat !== void 0) continue;
9520
+ if (!matchurlpattern(rule.urlpattern, url)) continue;
9521
+ rule.hits += 1;
9522
+ await memory.addblockrule(rule).catch(() => void 0);
9523
+ await audit("control", `Blocked the ${init.method} request to ${url} by the reviewed rule ${rule.id} of ${rule.urlpattern}; blocking applies to extension initiated traffic and page requests stay visible in the watch buffers.`, { stepid: rule.stepid }).catch(() => void 0);
9524
+ throw new Error(`The request to ${url} was blocked by the reviewed block rule ${rule.id}.`);
9525
+ }
9526
+ const fixture = mockfor(url, ruleset.mocks);
9527
+ if (fixture) {
9528
+ fixture.hits += 1;
9529
+ await memory.addmockspec(fixture).catch(() => void 0);
9530
+ await audit("control", `Served the reviewed mock fixture ${fixture.id} with the ${fixture.status} status for ${url} instead of the network; the fixture body stays out of the audit trail.`, { stepid: fixture.stepid }).catch(() => void 0);
9531
+ return { status: fixture.status, headers: fixture.headers ?? {}, body: fixture.body ?? "" };
9532
+ }
9533
+ const rewritten = applyheaderules(url, init.headers, ruleset.rewrites);
9534
+ if (rewritten.applied.length > 0) {
9535
+ for (const rule of rewritten.applied) {
9536
+ rule.hits += 1;
9537
+ await memory.addheaderule(rule).catch(() => void 0);
9538
+ await audit("control", `Applied the header rewrite rule ${rule.id} of ${rule.operation} ${rule.name} on ${url} from step ${rule.stepid}; the provenance of every applied rule is recorded here.`, { stepid: rule.stepid }).catch(() => void 0);
9539
+ }
9540
+ init = { ...init, headers: rewritten.headers };
9541
+ }
9542
+ if (ruleset.proxy && ruleset.proxy.revertedat === void 0) {
9543
+ const targetorigin = new URL(url).origin;
9544
+ const bypassed = ruleset.proxy.bypass.some((pattern) => {
9545
+ try {
9546
+ return new URL(pattern).origin === targetorigin;
9547
+ } catch {
9548
+ return false;
9549
+ }
9550
+ });
9551
+ if (!bypassed) {
9552
+ const config = await memory.getconfig();
9553
+ if (config?.endpoint) {
9554
+ await audit("control", `Routed the ${init.method} request to ${url} through the reviewed proxy route ${ruleset.proxy.id} of ${ruleset.proxy.scheme}://${ruleset.proxy.host}:${ruleset.proxy.port} via the reviewed relay endpoint ${config.endpoint}.`, { stepid: ruleset.proxy.stepid }).catch(() => void 0);
9555
+ const relayed = await livefetch(config.endpoint, { ...init, headers: { ...init.headers, "x-devthink-target": url } }, controller, window2, streamstate);
9556
+ return relayed;
9557
+ }
9558
+ await audit("control", `The proxy route ${ruleset.proxy.id} of ${ruleset.proxy.host}:${ruleset.proxy.port} covers ${url} but no reviewed relay endpoint is configured, so the request sends direct; the route stays recorded for review.`, { stepid: ruleset.proxy.stepid }).catch(() => void 0);
9559
+ }
9560
+ }
9561
+ }
9562
+ const response = await livefetch(url, init, controller, window2, streamstate);
9563
+ const read = ratelimitreadof(response.headers, new URL(url).origin, Date.now());
9564
+ if (read) await memory.setratelimit(read).catch(() => void 0);
9565
+ return response;
9566
+ }
9567
+ async function executenetcontrolstep(step, session, plan, tabid2, origin) {
9568
+ const options = stepoptions2(step);
9569
+ const extra = { ...session ? { sessionid: session.id } : {}, planid: plan.id, stepid: step.id };
9570
+ const ruleset = rulesetof(plan.id);
9571
+ if (step.kind === "blockrequest") {
9572
+ const rule = blockruleof(options.block);
9573
+ if (!rule) throw new Error("A reviewed block rule with a url pattern is required in options.block.");
9574
+ const registered = newblockrule({ id: randomid(), runid: plan.id, stepid: step.id, urlpattern: rule.urlpattern, ...rule.resourcetypes !== void 0 ? { resourcetypes: rule.resourcetypes } : {}, at: Date.now() });
9575
+ ruleset.blocks = [...ruleset.blocks.filter((item) => item.urlpattern !== registered.urlpattern), registered];
9576
+ await memory.addblockrule(registered);
9577
+ await audit("control", `Registered the reviewed block rule ${registered.id} of ${registered.urlpattern}${registered.resourcetypes !== void 0 ? ` for the resource types ${registered.resourcetypes.join(", ")}` : ""} for the run only; every rule reverts at run end and blocking applies to extension initiated traffic.`, extra);
9578
+ await refreshbadge();
9579
+ return { ok: true, summary: `The block rule of ${registered.urlpattern} applies for this run; matched extension requests are refused.`, details: { control: { applied: ruleset.blocks.filter((item) => item.revertedat === void 0).length, blocked: ruleset.blocks.reduce((total, item) => total + item.hits, 0), mocked: ruleset.mocks.reduce((total, item) => total + item.hits, 0) }, rule: { id: registered.id, urlpattern: registered.urlpattern, ...registered.resourcetypes !== void 0 ? { resourcetypes: registered.resourcetypes } : {} }, derivation: "The manifest keeps webRequest out, so blocking refuses extension initiated requests to the pattern and page requests stay visible in the watch buffers." } };
9580
+ }
9581
+ if (step.kind === "mockresponse") {
9582
+ const spec = mockspecof(options.mock);
9583
+ if (!spec || spec.reviewed !== true) throw new Error("A reviewed mock fixture with its full body or captured body ref reviewed is required in options.mock.");
9584
+ let fixturebody = spec.body ?? "";
9585
+ let bodyref;
9586
+ if (spec.bodyref !== void 0) {
9587
+ const captured = await memory.getbody(spec.bodyref);
9588
+ if (!captured) throw new Error(`No captured body matches ${spec.bodyref}; capture it again before the fixture replays it.`);
9589
+ if (captured.body === void 0 || captured.bodyexpired) throw new Error(`The captured body ${spec.bodyref} expired from the retention window; capture it again before the fixture replays it.`);
9590
+ fixturebody = captured.body;
9591
+ bodyref = spec.bodyref;
9592
+ }
9593
+ const registered = newmockspec({ id: randomid(), runid: plan.id, stepid: step.id, urlpattern: spec.urlpattern, status: spec.status, ...spec.headers !== void 0 ? { headers: spec.headers } : {}, body: fixturebody, ...bodyref !== void 0 ? { bodyref } : {}, reviewed: true, at: Date.now() });
9594
+ ruleset.mocks = [...ruleset.mocks.filter((item) => item.urlpattern !== registered.urlpattern), registered];
9595
+ await memory.addmockspec(registered);
9596
+ await audit("control", `Registered the reviewed mock fixture ${registered.id} of ${registered.urlpattern} serving the ${registered.status} status for the run only${bodyref !== void 0 ? ` replaying the captured body ${bodyref} of the body store` : ""}; the fixture body stays out of the audit trail.`, extra);
9597
+ await refreshbadge();
9598
+ return { ok: true, summary: `The mock fixture of ${registered.urlpattern} serves the ${registered.status} status for this run.`, details: { control: { applied: ruleset.mocks.filter((item) => item.revertedat === void 0).length, blocked: ruleset.blocks.reduce((total, item) => total + item.hits, 0), mocked: ruleset.mocks.reduce((total, item) => total + item.hits, 0) }, mock: { id: registered.id, urlpattern: registered.urlpattern, status: registered.status, bytes: fixturebody.length } } };
9599
+ }
9600
+ if (step.kind === "rewriteheaders") {
9601
+ const rules = Array.isArray(options.rules) ? options.rules : [];
9602
+ const registered = [];
9603
+ for (const item of rules) {
9604
+ const rule = headeruleof(item);
9605
+ if (!rule) throw new Error("Every header rewrite rule needs a url pattern, header name, operation and value.");
9606
+ const record2 = newheaderule({ id: randomid(), runid: plan.id, stepid: step.id, urlpattern: rule.urlpattern, name: rule.name, operation: rule.operation, ...rule.value !== void 0 ? { value: rule.value } : {}, at: Date.now() });
9607
+ registered.push(record2);
9608
+ }
9609
+ ruleset.rewrites = [...ruleset.rewrites.filter((item) => !registered.some((record2) => record2.urlpattern === item.urlpattern && record2.name === item.name)), ...registered];
9610
+ for (const record2 of registered) await memory.addheaderule(record2);
9611
+ await audit("control", `Registered ${registered.length} reviewed header rewrite rule${registered.length === 1 ? "" : "s"} of ${registered.map((rule) => `${rule.operation} ${rule.name} on ${rule.urlpattern}`).join("; ")} for the run only; the provenance of every applied rule is audited per request.`, extra);
9612
+ await refreshbadge();
9613
+ return { ok: true, summary: `${registered.length} header rewrite rule${registered.length === 1 ? "" : "s"} apply for this run.`, details: { control: { applied: ruleset.rewrites.filter((item) => item.revertedat === void 0).length, blocked: ruleset.blocks.reduce((total, item) => total + item.hits, 0), mocked: ruleset.mocks.reduce((total, item) => total + item.hits, 0) }, rules: registered.map((rule) => ({ id: rule.id, urlpattern: rule.urlpattern, name: rule.name, operation: rule.operation })) } };
9614
+ }
9615
+ if (step.kind === "setcookies" || step.kind === "readcookies" || step.kind === "clearcookies") {
9616
+ const domain = typeof options.domain === "string" && options.domain.trim() ? options.domain.trim() : step.kind === "setcookies" && Array.isArray(options.cookies) ? String(options.cookies[0]?.domain ?? "") : origin;
9617
+ const gate = cookiegate(session, domain, Date.now());
9618
+ if (!gate.allowed) throw new Error(gate.reason ?? "The cookie domain stays outside the session origin grants.");
9619
+ const pageorigin = `https://${domain.replace(/^\./, "")}`;
9620
+ if (step.kind === "setcookies") {
9621
+ const records = (Array.isArray(options.cookies) ? options.cookies : []).map((item) => cookierecordof(item)).filter((item) => item !== void 0);
9622
+ if (records.length === 0) throw new Error("A reviewed non-empty list of cookie records is required in options.cookies.");
9623
+ const written = await bridgecall(tabid2, "writecookies", records.map((record2) => ({ name: record2.name, value: record2.value, path: record2.path, ...record2.expiresat !== void 0 ? { expiresat: record2.expiresat } : {} })));
9624
+ const operation2 = { id: randomid(), runid: plan.id, stepid: step.id, kind: "write", domain, names: records.map((record2) => record2.name), at: Date.now() };
9625
+ await memory.addcookieop(operation2);
9626
+ await audit("control", `Wrote ${written.written} reviewed cookie${written.written === 1 ? "" : "s"} for the granted domain ${domain} through the page cookie jar; cookie values stay out of the audit trail.`, extra);
9627
+ return { ok: true, summary: `Wrote ${written.written} cookie${written.written === 1 ? "" : "s"} for ${domain}.`, details: { control: { applied: 0, blocked: 0, mocked: 0 }, cookies: redactedcookies(records), domain } };
9628
+ }
9629
+ if (step.kind === "readcookies") {
9630
+ const jar = await bridgecall(tabid2, "readcookies");
9631
+ const operation2 = { id: randomid(), runid: plan.id, stepid: step.id, kind: "read", domain, names: jar.map((cookie) => cookie.name), at: Date.now() };
9632
+ await memory.addcookieop(operation2);
9633
+ await audit("control", `Read ${jar.length} cookie${jar.length === 1 ? "" : "s"} of the granted domain ${domain} through the page cookie jar; the values return to the step result and stay out of the audit trail.`, extra);
9634
+ return { ok: true, summary: `Read ${jar.length} cookie${jar.length === 1 ? "" : "s"} of ${domain}.`, details: { control: { applied: 0, blocked: 0, mocked: 0 }, cookies: jar, domain } };
9635
+ }
9636
+ const names = Array.isArray(options.names) ? options.names.filter((name) => typeof name === "string" && name.trim().length > 0) : void 0;
9637
+ const cleared = await bridgecall(tabid2, "clearcookies", names);
9638
+ const operation = { id: randomid(), runid: plan.id, stepid: step.id, kind: "clear", domain, names: names ?? [], at: Date.now() };
9639
+ await memory.addcookieop(operation);
9640
+ await audit("control", `Cleared ${cleared.cleared} cookie${cleared.cleared === 1 ? "" : "s"} of the granted domain ${domain} through the page cookie jar.`, extra);
9641
+ return { ok: true, summary: `Cleared ${cleared.cleared} cookie${cleared.cleared === 1 ? "" : "s"} of ${domain}.`, details: { control: { applied: 0, blocked: 0, mocked: 0 }, cleared: cleared.cleared, domain, ...names !== void 0 ? { names } : {} } };
9642
+ }
9643
+ if (step.kind === "authflow") {
9644
+ const flow = oauthflowof(options.oauth);
9645
+ if (!flow) throw new Error("A reviewed oauth flow with provider, authorize url, token url, scopes and redirect origin is required in options.oauth.");
9646
+ const tokenorigin = new URL(flow.tokenurl).origin;
9647
+ const tokenorigincheck = origincheck(session, tokenorigin);
9648
+ if (!tokenorigincheck.allowed) throw new Error(tokenorigincheck.reason ?? "The token endpoint stays outside the session origin grants.");
9649
+ const redirectcheck = origincheck(session, flow.redirectorigin);
9650
+ if (!redirectcheck.allowed) throw new Error(redirectcheck.reason ?? "The redirect origin stays outside the session origin grants.");
9651
+ if (options.refresh === true) {
9652
+ const tokenid = typeof options.token === "string" ? options.token : "";
9653
+ const stored = (await memory.listtokens()).find((token) => token.id === tokenid && token.revokedat === void 0);
9654
+ if (!stored) throw new Error(`No stored token record matches ${tokenid}.`);
9655
+ const refreshsecret = await memory.getsecret(stored.refreshstorageid ?? "");
9656
+ if (!refreshsecret) throw new Error("The stored token has no refresh secret; run the flow again.");
9657
+ const controller = new AbortController();
9658
+ activefetches.set(step.id, controller);
9659
+ try {
9660
+ const request = tokenrequest(flow, { refreshtoken: refreshsecret });
9661
+ const response = await controlledfetch(plan.id, request.url, { method: "POST", headers: { "content-type": "application/x-www-form-urlencoded" }, body: request.body, redirect: "error" }, controller);
9662
+ const tokens = parsetokens(response.body);
9663
+ if (!tokens?.accesstoken) throw new Error(`The token refresh failed with the ${response.status} status.`);
9664
+ const expiresat = Date.now() + (tokens.expiresin !== void 0 ? tokens.expiresin * 1e3 : 3600 * 1e3);
9665
+ await memory.setsecret(stored.accessstorageid, tokens.accesstoken);
9666
+ await memory.addtoken({ ...stored, expiresat, refreshedat: Date.now(), ...tokens.refreshtoken !== void 0 ? { refreshstorageid: stored.refreshstorageid } : {} });
9667
+ await audit("auth", `Refreshed the token of provider ${stored.provider} scoped to ${stored.origin} for the scopes ${stored.scopes.join(", ")} through the reviewed refresh flow; token values never enter the audit trail.`, extra);
9668
+ return { ok: true, summary: `Refreshed the token of ${stored.provider} expiring at ${new Date(expiresat).toISOString()}.`, details: { control: { applied: 0, blocked: 0, mocked: 0 }, token: { id: stored.id, provider: stored.provider, scopes: stored.scopes, origin: stored.origin, expiresat, refreshed: true } } };
9669
+ } finally {
9670
+ controller.abort();
9671
+ activefetches.delete(step.id);
9672
+ }
9673
+ }
9674
+ if (options.revoke === true) {
9675
+ const rule = revocationruleof(options.revocation);
9676
+ const tokenids = rule ? rule.tokenids : Array.isArray(options.tokenids) ? options.tokenids.filter((item) => typeof item === "string" && item.trim().length > 0) : [];
9677
+ if (tokenids.length === 0) throw new Error("Token revocation needs reviewed token ids in options.");
9678
+ const records = await memory.listtokens();
9679
+ let revoked = 0;
9680
+ for (const tokenid of tokenids) {
9681
+ const stored = records.find((token) => token.id === tokenid);
9682
+ if (!stored || stored.revokedat !== void 0) continue;
9683
+ await memory.setsecret(stored.accessstorageid, "");
9684
+ if (stored.refreshstorageid) await memory.setsecret(stored.refreshstorageid, "");
9685
+ await memory.addtoken({ ...stored, revokedat: Date.now() });
9686
+ revoked += 1;
9687
+ }
9688
+ await audit("auth", `Revoked ${revoked} stored token record${revoked === 1 ? "" : "s"} of the run${rule ? ` with the reason ${rule.reason}` : ""}; the token material is dropped from storage.`, extra);
9689
+ return { ok: true, summary: `Revoked ${revoked} token record${revoked === 1 ? "" : "s"}.`, details: { control: { applied: 0, blocked: 0, mocked: 0 }, revoked, tokenids } };
9690
+ }
9691
+ const open = [...activeauthflows.entries()].find(([, active]) => active.runid === plan.id && active.stepid === step.id && !active.cancelled);
9692
+ if (open) {
9693
+ const [state2, active] = open;
9694
+ if (active.tabid) {
9695
+ const tab = await chrome.tabs.get(active.tabid).catch(() => void 0);
9696
+ const redirected = tab?.url;
9697
+ if (redirected) {
9698
+ const captured = capturecode(redirected, active.flow.redirectorigin, state2);
9699
+ if ("code" in captured) {
9700
+ activeauthflows.delete(state2);
9701
+ const controller = new AbortController();
9702
+ activefetches.set(step.id, controller);
9703
+ try {
9704
+ const request = tokenrequest(active.flow, { code: captured.code });
9705
+ const response = await controlledfetch(plan.id, request.url, { method: "POST", headers: { "content-type": "application/x-www-form-urlencoded" }, body: request.body, redirect: "error" }, controller);
9706
+ const tokens = parsetokens(response.body);
9707
+ if (!tokens?.accesstoken) throw new Error(`The token exchange failed with the ${response.status} status.`);
9708
+ const tokenid = randomid();
9709
+ const expiresat = Date.now() + (tokens.expiresin !== void 0 ? tokens.expiresin * 1e3 : 3600 * 1e3);
9710
+ const accessstorageid = `token-${tokenid}-access`;
9711
+ await memory.setsecret(accessstorageid, tokens.accesstoken);
9712
+ const record2 = { id: tokenid, provider: active.flow.provider, origin: new URL(active.flow.tokenurl).origin, scopes: tokens.scopes ?? active.flow.scopes, accessstorageid, ...tokens.refreshtoken !== void 0 ? { refreshstorageid: `token-${tokenid}-refresh` } : {}, expiresat, at: Date.now() };
9713
+ if (tokens.refreshtoken !== void 0) await memory.setsecret(record2.refreshstorageid ?? "", tokens.refreshtoken);
9714
+ await memory.addtoken(record2);
9715
+ await chrome.tabs.remove(active.tabid).catch(() => void 0);
9716
+ await audit("auth", `Exchanged the captured code of provider ${active.flow.provider} for a token scoped to ${record2.origin} with the scopes ${record2.scopes.join(", ")} expiring at ${new Date(expiresat).toISOString()}; token values stay behind storage ids and never enter the audit trail.`, extra);
9717
+ return { ok: true, summary: `The oauth flow of ${active.flow.provider} captured the redirect code and exchanged it for a token expiring at ${new Date(expiresat).toISOString()}.`, details: { control: { applied: 0, blocked: 0, mocked: 0 }, token: { id: record2.id, provider: record2.provider, scopes: record2.scopes, origin: record2.origin, expiresat }, auth: { provider: record2.provider, state: state2, redirectorigin: active.flow.redirectorigin, scopes: record2.scopes, stage: "exchanged" } } };
9718
+ } finally {
9719
+ controller.abort();
9720
+ activefetches.delete(step.id);
9721
+ }
9722
+ }
9723
+ if (captured.error) {
9724
+ activeauthflows.delete(state2);
9725
+ if (active.tabid) await chrome.tabs.remove(active.tabid).catch(() => void 0);
9726
+ return { ok: false, summary: `The oauth flow of ${active.flow.provider} failed: ${captured.error}`, details: { control: { applied: 0, blocked: 0, mocked: 0 }, auth: { provider: active.flow.provider, state: state2, redirectorigin: active.flow.redirectorigin, scopes: active.flow.scopes, stage: "failed" }, error: captured.error } };
9727
+ }
9728
+ }
9729
+ }
9730
+ return { ok: true, summary: `The oauth flow of ${active.flow.provider} still waits for the redirect on ${active.flow.redirectorigin}; finish the provider consent and rerun the step.`, details: { control: { applied: 0, blocked: 0, mocked: 0 }, auth: { provider: active.flow.provider, state: state2, redirectorigin: active.flow.redirectorigin, scopes: active.flow.scopes, stage: "consent" } } };
9731
+ }
9732
+ const state = `run-${plan.id.slice(0, 8)}-${randomid().slice(0, 8)}`;
9733
+ const consent = authorizeurl(flow, state);
9734
+ const opened = await chrome.tabs.create({ url: consent, active: true });
9735
+ activeauthflows.set(state, { runid: plan.id, stepid: step.id, flow, ...opened?.id !== void 0 ? { tabid: opened.id } : {}, cancelled: false });
9736
+ await audit("auth", `Opened the provider consent page of ${flow.provider} in reviewed tab ${opened?.id ?? 0} for the scopes ${flow.scopes.join(", ")} with the redirect origin ${flow.redirectorigin}; the extension captures the code on the granted redirect origin only and run cancel closes the flow.`, extra);
9737
+ return { ok: true, summary: `The oauth flow of ${flow.provider} waits for your consent in the opened tab; the code capture watches the granted redirect origin.`, details: { control: { applied: 0, blocked: 0, mocked: 0 }, auth: { provider: flow.provider, state, redirectorigin: flow.redirectorigin, scopes: flow.scopes, tabid: opened?.id ?? 0, stage: "consent" }, consenturl: consent, hint: "Finish the provider consent, then rerun the step to capture the redirect code through the granted redirect origin." } };
9738
+ }
9739
+ if (step.kind === "saveapikey") {
9740
+ const key = options.key;
9741
+ if (!key || typeof key !== "object" || Array.isArray(key)) throw new Error("A reviewed api key entry with name, origin scopes, header and value is required in options.key.");
9742
+ const entry = key;
9743
+ const ref = { name: String(entry.name ?? "").trim(), origins: Array.isArray(entry.origins) ? entry.origins.map((item) => String(item)) : [], header: String(entry.header ?? "").trim(), storageid: `apikey-${String(entry.name ?? "").trim()}`, createdat: Date.now() };
9744
+ await memory.setapikey(ref);
9745
+ await memory.setsecret(ref.storageid, String(entry.value ?? ""));
9746
+ await audit("control", `Stored the api key entry ${ref.name} for ${ref.origins.join(", ")} attaching header ${ref.header} behind the reviewed consent; the platform exposes no extension key store, so the material stays behind its storage id and never enters the audit trail.`, extra);
9747
+ return { ok: true, summary: `Stored the api key ${ref.name} for ${ref.origins.join(", ")}.`, details: { control: { applied: 0, blocked: 0, mocked: 0 }, apikey: { name: ref.name, origins: ref.origins, header: ref.header, createdat: ref.createdat } } };
9748
+ }
9749
+ if (step.kind === "routeproxy") {
9750
+ const route = proxyrouteof(options.proxy);
9751
+ if (!route) throw new Error("A reviewed proxy route with scheme, host, port and bypass list is required in options.proxy.");
9752
+ const registered = { id: randomid(), runid: plan.id, stepid: step.id, scheme: route.scheme, host: route.host, port: route.port, bypass: route.bypass, appliedat: Date.now() };
9753
+ ruleset.proxy = registered;
9754
+ await memory.addproxyroute(registered);
9755
+ const config = await memory.getconfig();
9756
+ await audit("control", `Applied the reviewed proxy route ${registered.id} of ${registered.scheme}://${registered.host}:${registered.port} for the run only with the bypassed origins ${registered.bypass.join(", ")}; non bypassed extension traffic routes through the reviewed relay endpoint${config?.endpoint ? ` ${config.endpoint}` : " once one is configured"} and the route reverts at run end.`, extra);
9757
+ await refreshbadge();
9758
+ return { ok: true, summary: `The proxy route of ${registered.host}:${registered.port} applies for this run with ${registered.bypass.length} bypassed origin${registered.bypass.length === 1 ? "" : "s"}.`, details: { control: { applied: 1, blocked: 0, mocked: 0 }, proxy: { id: registered.id, scheme: registered.scheme, host: registered.host, port: registered.port, bypass: registered.bypass, appliedat: registered.appliedat }, relay: config?.endpoint ?? "", derivation: "The manifest keeps proxy control out, so the route steers extension initiated traffic through the reviewed relay endpoint pattern and reverts at run end." } };
9759
+ }
9760
+ if (step.kind === "postform" || step.kind === "postfiles") {
9761
+ const payload = step.kind === "postform" ? formpayloadof(options.form) : void 0;
9762
+ const upload = step.kind === "postfiles" ? multipartpayloadof(options.upload) : void 0;
9763
+ const url = payload?.url ?? upload?.url;
9764
+ if (!url) throw new Error("A reviewed payload with a url is required in options.");
9765
+ const urlgate = origincheck(session, url);
9766
+ if (!urlgate.allowed) throw new Error(urlgate.reason ?? "The submission target stays outside the session origin grants.");
9767
+ const budget = typeof options.wait === "number" ? options.wait : void 0;
9768
+ const limits = await memory.getratelimits(Date.now());
9769
+ const limit = limits.find((item) => item.origin === new URL(url).origin);
9770
+ const wait = ratelimitwait(limit, Date.now());
9771
+ const budgetcheck = ratelimitbudgetallowed(wait, budget);
9772
+ if (!budgetcheck.allowed) throw new Error(budgetcheck.reason ?? "The rate limit wait exceeds the reviewed budget.");
9773
+ if (wait > 0) {
9774
+ await audit("control", `The rate limiter waits ${wait} milliseconds until the reset window of ${new URL(url).origin} passes before the submission.`, extra);
9775
+ await new Promise((resolve) => setTimeout(resolve, wait));
9776
+ }
9777
+ const controller = new AbortController();
9778
+ activefetches.set(step.id, controller);
9779
+ try {
9780
+ const started = Date.now();
9781
+ let response;
9782
+ if (payload) {
9783
+ const body2 = urlencodeform(payload.fields);
9784
+ response = await controlledfetch(plan.id, url, { method: "POST", headers: { "content-type": "application/x-www-form-urlencoded" }, body: body2, redirect: "error" }, controller);
9785
+ await audit("control", `Posted the reviewed urlencoded form of ${payload.fields.length} field${payload.fields.length === 1 ? "" : "s"} to ${url} ending in the ${response.status} class after the rate limiter pass; field values stay out of the audit trail.`, extra);
9786
+ const retryafter2 = retryafterof(response.status, response.headers);
9787
+ if (retryafter2 !== void 0) await audit("control", `The submission endpoint answered ${response.status} with a retry after window of ${retryafter2} milliseconds; the next submission waits for it.`, extra);
9788
+ return { ok: response.status >= 200 && response.status < 300, summary: `The form post to ${url} ended in the ${response.status} class.`, details: { control: { applied: 0, blocked: 0, mocked: 0 }, transport: { status: response.status, headers: Object.keys(response.headers), bytes: response.body.length, duration: Date.now() - started }, fields: payload.fields.map((field) => field.name), ...retryafter2 !== void 0 ? { retryafter: retryafter2 } : {} } };
9789
+ }
9790
+ const streamed = multipartchunks(upload ?? { url, fields: [], files: [] });
9791
+ let uploaded = 0;
9792
+ for (let index = 0; index < streamed.chunks.length; index += 1) {
9793
+ uploaded += streamed.chunks[index]?.length ?? 0;
9794
+ await memory.setprogress(recordupload(await memory.getprogress(), plan.id, step.id, { chunk: index + 1, chunks: streamed.chunks.length, uploaded, bytes: streamed.bytes }, Date.now())).catch(() => void 0);
9795
+ }
9796
+ const body = streamed.chunks.join("");
9797
+ response = await controlledfetch(plan.id, url, { method: "POST", headers: { "content-type": `multipart/form-data; boundary=${streamed.boundary}` }, body, redirect: "error" }, controller);
9798
+ await audit("control", `Streamed the reviewed multipart upload of ${(upload?.files ?? []).length} reviewed file${(upload?.files ?? []).length === 1 ? "" : "s"} in ${streamed.chunks.length} chunk${streamed.chunks.length === 1 ? "" : "s"} of ${streamed.bytes} bytes to ${url} ending in the ${response.status} class; file contents stay out of the audit trail.`, extra);
9799
+ const retryafter = retryafterof(response.status, response.headers);
9800
+ if (retryafter !== void 0) await audit("control", `The upload endpoint answered ${response.status} with a retry after window of ${retryafter} milliseconds; the next submission waits for it.`, extra);
9801
+ return { ok: response.status >= 200 && response.status < 300, summary: `The multipart upload to ${url} ended in the ${response.status} class.`, details: { control: { applied: 0, blocked: 0, mocked: 0 }, transport: { status: response.status, headers: Object.keys(response.headers), bytes: response.body.length, duration: Date.now() - started }, upload: { chunks: streamed.chunks.length, bytes: streamed.bytes, files: (upload?.files ?? []).map((file) => file.filename) }, ...retryafter !== void 0 ? { retryafter } : {} } };
9802
+ } finally {
9803
+ controller.abort();
9804
+ activefetches.delete(step.id);
9805
+ }
9806
+ }
9807
+ void tabid2;
9808
+ throw new Error("Unsupported network control kind.");
9809
+ }
8382
9810
  async function enforcewindowreview(step, session, plan) {
8383
9811
  const windowid = step.value && /^\d+$/.test(step.value) ? Number.parseInt(step.value, 10) : 0;
8384
9812
  const progress = plan ? await memory.getprogress() : void 0;
@@ -8421,10 +9849,12 @@ async function refreshbadge() {
8421
9849
  const media = (await memory.getmediarecords()).length + (await memory.getimagebatches()).length;
8422
9850
  const recordingprompts = (await memory.getrecordingconsents()).filter((record2) => record2.approved === void 0).length;
8423
9851
  const fetchprompts = (await memory.getfetchconsents()).filter((consent) => consent.approved === void 0).length;
9852
+ const consoleprompts = (await memory.getconsoleconsents()).filter((consent) => consent.approved === void 0).length;
8424
9853
  const observedrequests = (await memory.getexchanges()).length;
8425
9854
  const livechannels = (await memory.getchannels()).filter((channel) => channel.state === "open" || channel.state === "connecting").length;
9855
+ const activerulescount = [...activerules.values()].reduce((total2, ruleset) => total2 + ruleset.blocks.filter((rule) => rule.revertedat === void 0).length + ruleset.mocks.filter((rule) => rule.revertedat === void 0).length + ruleset.rewrites.filter((rule) => rule.revertedat === void 0).length + (ruleset.proxy !== void 0 && ruleset.proxy.revertedat === void 0 ? 1 : 0), 0);
8426
9856
  const tasktabs2 = new Set(badges.map((badge) => badge.tabid)).size;
8427
- const total = (queues?.prefetch ?? 0) + (queues?.batchopen ?? 0) + tasktabs2 + prompts + consents + quarantined + datasets + captures + media + recordingprompts + fetchprompts + observedrequests + livechannels;
9857
+ const total = (queues?.prefetch ?? 0) + (queues?.batchopen ?? 0) + tasktabs2 + prompts + consents + quarantined + datasets + captures + media + recordingprompts + fetchprompts + consoleprompts + observedrequests + livechannels + activerulescount;
8428
9858
  await chrome.action.setBadgeText({ text: total > 0 ? String(total) : "" }).catch(() => {
8429
9859
  });
8430
9860
  }
@@ -8490,6 +9920,11 @@ async function executestep(stepid) {
8490
9920
  output = await executesocketstep(step, session, plan, tab.id, origin);
8491
9921
  } else if (isnetwatchkind(step.kind)) {
8492
9922
  output = await executenetwatchstep(step, session, plan, tab.id, origin);
9923
+ } else if (iscontrolkind(step.kind)) {
9924
+ output = await executenetcontrolstep(step, session, plan, tab.id, origin);
9925
+ } else if (isdebugkind(step.kind)) {
9926
+ if (!session || !plan || plan.state !== "approved") throw new Error("Debugging kinds refuse to run outside an approved session plan.");
9927
+ output = await executetimelinestep(step, session, plan, tab.id, origin);
8493
9928
  } else {
8494
9929
  if (step.target && freshcheckkinds.has(step.kind)) {
8495
9930
  const fresh = await snapshot(tab.id);
@@ -8543,6 +9978,10 @@ async function executestep(stepid) {
8543
9978
  });
8544
9979
  await closechannelsforrun(plan.id).catch(() => {
8545
9980
  });
9981
+ await cancelauthflowsforrun(plan.id, "plan completion").catch(() => {
9982
+ });
9983
+ await revertcontrolsforrun(plan.id, "plan completion").catch(() => {
9984
+ });
8546
9985
  const done = { ...plan, state: "completed", completedat: Date.now() };
8547
9986
  await memory.setplan(done);
8548
9987
  await audit("complete", "Every reviewed step of the approved plan has executed.", { ...session ? { sessionid: session.id } : {}, planid: done.id });
@@ -8702,7 +10141,12 @@ async function handlerequest(message, sender) {
8702
10141
  const messagecount = (await memory.getmessages()).length;
8703
10142
  const endpoints = await memory.getendpoints();
8704
10143
  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 }));
10144
+ const apikeys = (await memory.getapikeys()).map((ref) => ({ name: ref.name, origins: ref.origins, header: ref.header, createdat: ref.createdat, ...ref.lastuse !== void 0 ? { lastuse: ref.lastuse } : {} }));
10145
+ const traffic = controlreport({ blocks: await memory.getblockrules(), mocks: await memory.getmockspecs(), rewrites: await memory.getheaderules(), cookies: await memory.getcookieops(), proxies: await memory.getproxyroutes(), ratelimits: await memory.getratelimits(Date.now()) });
10146
+ const tokens = authreport({ tokens: await memory.listtokens() });
10147
+ const timelineentries = await memory.gettimeline();
10148
+ const timeline = timelinereport({ entries: timelineentries, errors: await memory.geterrorrecords(), rejections: await memory.getrejectionrecords(), longtasks: await memory.getlongtasks(), levelcounts: timelinecounts(timelineentries) });
10149
+ const authflows = [...activeauthflows.values()].map((active) => ({ provider: active.flow.provider, redirectorigin: active.flow.redirectorigin, scopes: active.flow.scopes, stepid: active.stepid, tabid: active.tabid ?? 0, stage: active.cancelled ? "cancelled" : "consent" }));
8706
10150
  const runsettings = await memory.getsettings();
8707
10151
  const scanhooks = [];
8708
10152
  for (const hook of await memory.getscanhooks()) {
@@ -8714,7 +10158,7 @@ async function handlerequest(message, sender) {
8714
10158
  const livetab = session ? await chrome.tabs.get(session.tabid).catch(() => void 0) : void 0;
8715
10159
  const waitprofile = session ? waitprofiles.find((record2) => record2.origin === session.origin) : void 0;
8716
10160
  const livestate = { phase: livetab?.status === "loading" ? "loading" : "complete", ...navrecords[0] ? { finalurl: navrecords[0].finalurl, redirects: navrecords[0].chain } : {} };
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()] } : {} };
10161
+ 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, timelineretention: runsettings?.timelineretention, timeline, consoleconsents: await memory.getconsoleconsents(), rotationtargets: await memory.getrotationtargets(), levelsummaries: await memory.getlevelsummaries(), socketsactive: activesockets.size, traffic, tokens, authflows, activerules: [...activerules.values()].reduce((total, ruleset) => total + ruleset.blocks.filter((rule) => rule.revertedat === void 0).length + ruleset.mocks.filter((rule) => rule.revertedat === void 0).length + ruleset.rewrites.filter((rule) => rule.revertedat === void 0).length + (ruleset.proxy !== void 0 && ruleset.proxy.revertedat === void 0 ? 1 : 0), 0), ...stitchprogress.size > 0 ? { stitchprogress: [...stitchprogress.values()] } : {} };
8718
10162
  }
8719
10163
  case "capabilities":
8720
10164
  return refreshcapabilities();
@@ -8759,7 +10203,8 @@ async function handlerequest(message, sender) {
8759
10203
  const capture = outcome.details?.capture;
8760
10204
  const media = outcome.details?.media;
8761
10205
  const network = outcome.details?.network;
8762
- return JSON.parse(outcomeresponse({ outcome, plan, ...resolved ? { resolvedtarget: resolved } : {}, ...capture ? { capture } : {}, ...media ? { media } : {}, ...network ? { network } : {} }));
10206
+ const timeline = outcome.details?.timeline;
10207
+ return JSON.parse(outcomeresponse({ outcome, plan, ...resolved ? { resolvedtarget: resolved } : {}, ...capture ? { capture } : {}, ...media ? { media } : {}, ...network ? { network } : {}, ...timeline ? { timeline } : {} }));
8763
10208
  }
8764
10209
  case "map": {
8765
10210
  const plan = await memory.getplan();
@@ -9327,12 +10772,12 @@ async function handlerequest(message, sender) {
9327
10772
  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
10773
  if (!inputkey.header?.trim()) throw new Error("A reviewed header name is required for the api key.");
9329
10774
  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);
10775
+ const entry = { name: inputkey.name.trim(), origins: inputkey.origins, header: inputkey.header.trim(), storageid: `apikey-${inputkey.name.trim()}`, createdat: Date.now() };
10776
+ await memory.setapikey(entry);
10777
+ await memory.setsecret(entry.storageid, inputkey.value);
9333
10778
  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 };
10779
+ await audit("call", `Stored the api key entry ${entry.name} for ${entry.origins.join(", ")} attaching header ${entry.header}; the key material itself never enters the audit trail.`, { ...session ? { sessionid: session.id } : {} });
10780
+ return { name: entry.name, origins: entry.origins, header: entry.header, createdat: entry.createdat };
9336
10781
  }
9337
10782
  case "deleteapikey": {
9338
10783
  const inputdeletekey = message;
@@ -9370,6 +10815,87 @@ async function handlerequest(message, sender) {
9370
10815
  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
10816
  return { bodyretention: retention };
9372
10817
  }
10818
+ case "settimelineretention": {
10819
+ const inputretention = message;
10820
+ const settings = await memory.getsettings();
10821
+ const retention = typeof inputretention.retention === "number" && Number.isInteger(inputretention.retention) && inputretention.retention >= 0 ? inputretention.retention : void 0;
10822
+ await memory.setsettings({ ...settings, ...retention !== void 0 ? { timelineretention: retention } : {} });
10823
+ await audit("configure", `The user set the timeline retention to ${retention === void 0 ? "keep every entry" : retention} entr${retention === 1 ? "y" : "ies"}; the level count summaries always survive.`);
10824
+ return { timelineretention: retention };
10825
+ }
10826
+ case "approveconsoleconsent": {
10827
+ const inputapprove = message;
10828
+ const records = await memory.getconsoleconsents();
10829
+ const record2 = records.find((item) => item.id === inputapprove.id);
10830
+ if (!record2) throw new Error("No console capture prompt matches the id.");
10831
+ const decided = { ...record2, approved: true, usedat: Date.now() };
10832
+ await memory.setconsoleconsent(decided);
10833
+ await audit("consent", `Console capture on ${record2.origin} approved from the review panel; the decision persists for that origin.`, { stepid: record2.stepid });
10834
+ await refreshbadge();
10835
+ return { approved: true, origin: record2.origin };
10836
+ }
10837
+ case "consolediff": {
10838
+ const inputdiff = message;
10839
+ const base = inputdiff.base?.trim();
10840
+ const target = inputdiff.target?.trim();
10841
+ if (!base || !target) throw new Error("Console diffing needs the two reviewed run ids.");
10842
+ if (base === target) throw new Error("Console diffing needs two different run ids.");
10843
+ const baselines = (await memory.listtimeline({ runid: base })).filter((entry) => entry.source === "console").map((entry) => entry.message);
10844
+ const targetlines = (await memory.listtimeline({ runid: target })).filter((entry) => entry.source === "console").map((entry) => entry.message);
10845
+ if (baselines.length === 0 && targetlines.length === 0) throw new Error("Neither run stored console output yet; run watchconsole on both runs first.");
10846
+ const diff = consolediff({ baseid: base, targetid: target, baselines, targetlines, now: Date.now() });
10847
+ await memory.addconsolediff(diff);
10848
+ await audit("diff", `Diffed the console output of runs ${base} and ${target}: ${diff.added} added, ${diff.removed} removed and ${diff.repeated} repeated line${diff.added + diff.removed + diff.repeated === 1 ? "" : "s"}.`);
10849
+ return consolediffreport({ diff });
10850
+ }
10851
+ case "trafficreport": {
10852
+ const plan = await memory.getplan();
10853
+ if (!plan) throw new Error("No plan is available for a traffic control envelope.");
10854
+ return controlreport({ blocks: await memory.getblockrules(), mocks: await memory.getmockspecs(), rewrites: await memory.getheaderules(), cookies: await memory.getcookieops(), proxies: await memory.getproxyroutes(), ratelimits: await memory.getratelimits(Date.now()) });
10855
+ }
10856
+ case "authreport": {
10857
+ return authreport({ tokens: await memory.listtokens() });
10858
+ }
10859
+ case "revoketokens": {
10860
+ const inputrevoke = message;
10861
+ const tokenids = Array.isArray(inputrevoke.tokenids) ? inputrevoke.tokenids.filter((item) => typeof item === "string" && item.trim().length > 0) : [];
10862
+ if (tokenids.length === 0) throw new Error("Token revocation needs the reviewed token ids.");
10863
+ const reason = inputrevoke.reason?.trim() || "user demand from the review panel";
10864
+ const records = await memory.listtokens();
10865
+ let revoked = 0;
10866
+ for (const tokenid of tokenids) {
10867
+ const stored = records.find((token) => token.id === tokenid);
10868
+ if (!stored || stored.revokedat !== void 0) continue;
10869
+ await memory.setsecret(stored.accessstorageid, "");
10870
+ if (stored.refreshstorageid) await memory.setsecret(stored.refreshstorageid, "");
10871
+ await memory.addtoken({ ...stored, revokedat: Date.now() });
10872
+ revoked += 1;
10873
+ }
10874
+ const session = await memory.getsession();
10875
+ await audit("auth", `Revoked ${revoked} stored token record${revoked === 1 ? "" : "s"} on ${reason}; the token material is dropped from storage.`, { ...session ? { sessionid: session.id } : {} });
10876
+ await refreshbadge();
10877
+ return { revoked, tokenids };
10878
+ }
10879
+ case "revertproxyroute": {
10880
+ const inputrevert = message;
10881
+ const plan = await memory.getplan();
10882
+ if (!plan) throw new Error("No plan is available for a proxy revert.");
10883
+ const ruleset = activerules.get(plan.id);
10884
+ if (!ruleset?.proxy) throw new Error("No active proxy route covers this run.");
10885
+ if (inputrevert.id && ruleset.proxy.id !== inputrevert.id) throw new Error(`No active proxy route matches ${inputrevert.id}.`);
10886
+ const reverted = revertrule(ruleset.proxy, Date.now());
10887
+ ruleset.proxy = reverted;
10888
+ await memory.addproxyroute(reverted);
10889
+ await audit("control", `Reverted the proxy route ${reverted.id} of ${reverted.scheme}://${reverted.host}:${reverted.port} from the review panel; the previous routing state is restored.`, { planid: plan.id, stepid: reverted.stepid });
10890
+ await refreshbadge();
10891
+ return { reverted: true, id: reverted.id };
10892
+ }
10893
+ case "revertcontrols": {
10894
+ const plan = await memory.getplan();
10895
+ if (!plan) throw new Error("No plan is available for a traffic rule revert.");
10896
+ await revertcontrolsforrun(plan.id, "review panel demand");
10897
+ return { reverted: true };
10898
+ }
9373
10899
  case "netreport": {
9374
10900
  const plan = await memory.getplan();
9375
10901
  if (!plan) throw new Error("No plan is available for a network envelope.");
@@ -9412,11 +10938,27 @@ async function handlerequest(message, sender) {
9412
10938
  controller.abort();
9413
10939
  activefetches.delete(id);
9414
10940
  }
10941
+ for (const [id, watcher] of [...activetimelinewatchers.entries()]) {
10942
+ watcher.cancelled = true;
10943
+ await memory.closewatch(id, Date.now()).catch(() => {
10944
+ });
10945
+ await audit("timeline", `Watcher ${id} cancelled on run cancel or the killswitch; the captured window is discarded.`, { ...session ? { sessionid: session.id } : {}, ...watcher.runid ? { planid: watcher.runid } : {} });
10946
+ activetimelinewatchers.delete(id);
10947
+ }
9415
10948
  const stoppedplan = await memory.getplan();
9416
- if (stoppedplan) await closechannelsforrun(stoppedplan.id).catch(() => {
9417
- });
9418
- else await closechannelsforrun("none").catch(() => {
9419
- });
10949
+ if (stoppedplan) {
10950
+ await closechannelsforrun(stoppedplan.id).catch(() => {
10951
+ });
10952
+ await cancelauthflowsforrun(stoppedplan.id, "run cancel").catch(() => {
10953
+ });
10954
+ await revertcontrolsforrun(stoppedplan.id, "run cancel").catch(() => {
10955
+ });
10956
+ } else {
10957
+ await closechannelsforrun("none").catch(() => {
10958
+ });
10959
+ await revertcontrolsforrun("none", "run cancel").catch(() => {
10960
+ });
10961
+ }
9420
10962
  for (const [id, active] of [...activerecordings.entries()]) {
9421
10963
  const finished = finishrecording(active.record, Date.now());
9422
10964
  await memory.addmedia(finished).catch(() => {