@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.
- package/README.md +6 -4
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1119 -10
- package/dist/index.js.map +4 -4
- package/dist/memory.d.ts +73 -5
- package/dist/memory.d.ts.map +1 -1
- package/dist/netauth.d.ts +46 -0
- package/dist/netauth.d.ts.map +1 -0
- package/dist/netcontrol.d.ts +96 -0
- package/dist/netcontrol.d.ts.map +1 -0
- package/dist/policy.d.ts +39 -1
- package/dist/policy.d.ts.map +1 -1
- package/dist/protocol.d.ts +67 -2
- package/dist/protocol.d.ts.map +1 -1
- package/dist/runtimeline.d.ts +122 -0
- package/dist/runtimeline.d.ts.map +1 -0
- package/dist/types.d.ts +289 -3
- package/dist/types.d.ts.map +1 -1
- package/dist/version.d.ts +1 -1
- package/extension/dist/background.js +1567 -25
- package/extension/dist/background.js.map +4 -4
- package/extension/dist/manifest.json +1 -1
- package/extension/dist/pagebridge.js +228 -7
- package/extension/dist/pagebridge.js.map +4 -4
- package/extension/dist/popup.html +1 -1
- package/extension/dist/popup.js +22 -2
- package/extension/dist/popup.js.map +2 -2
- package/extension/dist/sidepanel.html +1 -1
- package/extension/dist/sidepanel.js +231 -2
- package/extension/dist/sidepanel.js.map +2 -2
- package/extension/dist/style.css +5 -0
- package/extension/manifest.json +1 -1
- package/package.json +1 -1
|
@@ -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
|
|
866
|
-
async setapikey(
|
|
867
|
-
const records = (await this.adapter.get("apikeys") ?? []).filter((item) => item.name !==
|
|
868
|
-
await this.adapter.set("apikeys", [
|
|
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
|
|
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.
|
|
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) =>
|
|
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) =>
|
|
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,
|
|
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
|
-
|
|
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
|
|
9331
|
-
await memory.setapikey(
|
|
9332
|
-
await memory.setsecret(
|
|
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
|
|
9335
|
-
return { name:
|
|
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)
|
|
9417
|
-
|
|
9418
|
-
|
|
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(() => {
|