@wenathlan/extension 1.1.43 → 1.1.44
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 +5 -4
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +684 -9
- package/dist/index.js.map +4 -4
- package/dist/memory.d.ts +35 -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 +16 -0
- package/dist/policy.d.ts.map +1 -1
- package/dist/protocol.d.ts +38 -2
- package/dist/protocol.d.ts.map +1 -1
- package/dist/types.d.ts +158 -3
- package/dist/types.d.ts.map +1 -1
- package/dist/version.d.ts +1 -1
- package/extension/dist/background.js +1085 -24
- package/extension/dist/background.js.map +4 -4
- package/extension/dist/manifest.json +1 -1
- package/extension/dist/pagebridge.js +24 -3
- package/extension/dist/pagebridge.js.map +2 -2
- package/extension/dist/popup.html +1 -1
- package/extension/dist/popup.js +11 -2
- package/extension/dist/popup.js.map +2 -2
- package/extension/dist/sidepanel.html +1 -1
- package/extension/dist/sidepanel.js +80 -2
- package/extension/dist/sidepanel.js.map +2 -2
- package/extension/dist/style.css +1 -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,74 @@ 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
|
+
}
|
|
970
1045
|
};
|
|
971
1046
|
function mediakindof(record2) {
|
|
972
1047
|
if ("pages" in record2) return "pdf";
|
|
@@ -1659,10 +1734,319 @@ function extractvalues(body, paths) {
|
|
|
1659
1734
|
return fields.map((field) => ({ path: field.path, ...field.value !== void 0 ? { value: field.value } : {}, ...field.missing ? { missing: true } : {} }));
|
|
1660
1735
|
}
|
|
1661
1736
|
|
|
1737
|
+
// netcontrol.ts
|
|
1738
|
+
var controlkinds = ["blockrequest", "mockresponse", "rewriteheaders", "setcookies", "readcookies", "clearcookies", "authflow", "saveapikey", "routeproxy", "postform", "postfiles"];
|
|
1739
|
+
function patternorigin(pattern) {
|
|
1740
|
+
const trimmed = pattern.trim();
|
|
1741
|
+
if (!trimmed.startsWith("https://")) return void 0;
|
|
1742
|
+
const rest = trimmed.slice("https://".length);
|
|
1743
|
+
const host = rest.split("/")[0] ?? "";
|
|
1744
|
+
if (!host.trim()) return void 0;
|
|
1745
|
+
return `https://${host.toLowerCase()}`;
|
|
1746
|
+
}
|
|
1747
|
+
function matchurlpattern(pattern, url) {
|
|
1748
|
+
const origin = patternorigin(pattern);
|
|
1749
|
+
if (!origin) return false;
|
|
1750
|
+
let parsed;
|
|
1751
|
+
try {
|
|
1752
|
+
parsed = new URL(url);
|
|
1753
|
+
} catch {
|
|
1754
|
+
return false;
|
|
1755
|
+
}
|
|
1756
|
+
if (parsed.origin !== origin) return false;
|
|
1757
|
+
const patternpath = pattern.trim().slice(origin.length);
|
|
1758
|
+
if (patternpath === "" || patternpath === "/") return true;
|
|
1759
|
+
const segments = patternpath.split("/").filter((segment) => segment !== "");
|
|
1760
|
+
if (segments.includes("**")) return true;
|
|
1761
|
+
const pathsegments = parsed.pathname.split("/").filter((segment) => segment !== "");
|
|
1762
|
+
if (segments.length !== pathsegments.length) return false;
|
|
1763
|
+
return segments.every((segment, index) => segment === pathsegments[index] || segment.includes("*") && new RegExp(`^${segment.split("*").map((part) => part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join(".*")}$`).test(pathsegments[index] ?? ""));
|
|
1764
|
+
}
|
|
1765
|
+
function blockruleof(value) {
|
|
1766
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
|
|
1767
|
+
const options = value;
|
|
1768
|
+
if (typeof options.urlpattern !== "string" || !options.urlpattern.trim()) return void 0;
|
|
1769
|
+
const rule = { urlpattern: options.urlpattern.trim() };
|
|
1770
|
+
if (Array.isArray(options.resourcetypes)) {
|
|
1771
|
+
const types = options.resourcetypes.filter((item) => typeof item === "string" && item.trim().length > 0);
|
|
1772
|
+
if (types.length === 0) return void 0;
|
|
1773
|
+
rule.resourcetypes = types;
|
|
1774
|
+
}
|
|
1775
|
+
return rule;
|
|
1776
|
+
}
|
|
1777
|
+
function newblockrule(input) {
|
|
1778
|
+
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 };
|
|
1779
|
+
}
|
|
1780
|
+
function mockspecof(value) {
|
|
1781
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
|
|
1782
|
+
const options = value;
|
|
1783
|
+
if (typeof options.urlpattern !== "string" || !options.urlpattern.trim()) return void 0;
|
|
1784
|
+
if (typeof options.status !== "number" || !Number.isInteger(options.status) || options.status < 100 || options.status > 599) return void 0;
|
|
1785
|
+
const hasbody = typeof options.body === "string";
|
|
1786
|
+
const bodyref = typeof options.bodyref === "string" ? options.bodyref.trim() : "";
|
|
1787
|
+
if (!hasbody && bodyref === "") return void 0;
|
|
1788
|
+
const spec = { urlpattern: options.urlpattern.trim(), status: options.status };
|
|
1789
|
+
if (hasbody) spec.body = options.body;
|
|
1790
|
+
if (bodyref !== "") spec.bodyref = bodyref;
|
|
1791
|
+
if (options.headers && typeof options.headers === "object" && !Array.isArray(options.headers)) spec.headers = options.headers;
|
|
1792
|
+
if (options.reviewed === true) spec.reviewed = true;
|
|
1793
|
+
return spec;
|
|
1794
|
+
}
|
|
1795
|
+
function newmockspec(input) {
|
|
1796
|
+
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 };
|
|
1797
|
+
}
|
|
1798
|
+
function mockfor(url, specs) {
|
|
1799
|
+
return specs.find((spec) => spec.revertedat === void 0 && matchurlpattern(spec.urlpattern, url));
|
|
1800
|
+
}
|
|
1801
|
+
function headeruleof(value) {
|
|
1802
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
|
|
1803
|
+
const options = value;
|
|
1804
|
+
if (typeof options.urlpattern !== "string" || !options.urlpattern.trim()) return void 0;
|
|
1805
|
+
if (typeof options.name !== "string" || !options.name.trim()) return void 0;
|
|
1806
|
+
if (options.operation !== "set" && options.operation !== "append" && options.operation !== "remove") return void 0;
|
|
1807
|
+
if (options.operation === "remove" && options.value !== void 0) return void 0;
|
|
1808
|
+
if (options.operation !== "remove" && typeof options.value !== "string") return void 0;
|
|
1809
|
+
const rule = { urlpattern: options.urlpattern.trim(), name: options.name.trim(), operation: options.operation };
|
|
1810
|
+
if (options.operation !== "remove") rule.value = typeof options.value === "string" ? options.value : "";
|
|
1811
|
+
return rule;
|
|
1812
|
+
}
|
|
1813
|
+
function newheaderule(input) {
|
|
1814
|
+
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 };
|
|
1815
|
+
}
|
|
1816
|
+
function applyheaderules(url, headers, rules) {
|
|
1817
|
+
const rewritten = { ...headers };
|
|
1818
|
+
const applied = [];
|
|
1819
|
+
for (const rule of rules) {
|
|
1820
|
+
if (rule.revertedat !== void 0) continue;
|
|
1821
|
+
if (!matchurlpattern(rule.urlpattern, url)) continue;
|
|
1822
|
+
const name = rule.name;
|
|
1823
|
+
if (rule.operation === "remove") {
|
|
1824
|
+
delete rewritten[name];
|
|
1825
|
+
applied.push(rule);
|
|
1826
|
+
continue;
|
|
1827
|
+
}
|
|
1828
|
+
const value = rule.value ?? "";
|
|
1829
|
+
if (rule.operation === "set") rewritten[name] = value;
|
|
1830
|
+
else rewritten[name] = rewritten[name] !== void 0 ? `${rewritten[name]}, ${value}` : value;
|
|
1831
|
+
applied.push(rule);
|
|
1832
|
+
}
|
|
1833
|
+
return { headers: rewritten, applied };
|
|
1834
|
+
}
|
|
1835
|
+
function revertrule(rule, at) {
|
|
1836
|
+
if (rule.revertedat !== void 0) return rule;
|
|
1837
|
+
return { ...rule, revertedat: at };
|
|
1838
|
+
}
|
|
1839
|
+
function cookierecordof(value) {
|
|
1840
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
|
|
1841
|
+
const options = value;
|
|
1842
|
+
if (typeof options.name !== "string" || !options.name.trim()) return void 0;
|
|
1843
|
+
if (typeof options.domain !== "string" || !options.domain.trim()) return void 0;
|
|
1844
|
+
if (typeof options.path !== "string" || !options.path.trim()) return void 0;
|
|
1845
|
+
if (typeof options.value !== "string") return void 0;
|
|
1846
|
+
const record2 = { name: options.name.trim(), domain: options.domain.trim().toLowerCase(), path: options.path.trim(), value: options.value };
|
|
1847
|
+
if (typeof options.expiresat === "number" && Number.isFinite(options.expiresat)) record2.expiresat = options.expiresat;
|
|
1848
|
+
return record2;
|
|
1849
|
+
}
|
|
1850
|
+
function cookiedomaingranted(domain, grants) {
|
|
1851
|
+
const host = domain.trim().toLowerCase().replace(/^\./, "");
|
|
1852
|
+
return grants.some((grant) => {
|
|
1853
|
+
let granthost = "";
|
|
1854
|
+
try {
|
|
1855
|
+
granthost = new URL(grant).hostname.toLowerCase();
|
|
1856
|
+
} catch {
|
|
1857
|
+
return false;
|
|
1858
|
+
}
|
|
1859
|
+
return host === granthost || host.endsWith(`.${granthost}`);
|
|
1860
|
+
});
|
|
1861
|
+
}
|
|
1862
|
+
function redactedcookies(records) {
|
|
1863
|
+
return records.map((record2) => ({ name: record2.name, domain: record2.domain, path: record2.path, ...record2.expiresat !== void 0 ? { expiresat: record2.expiresat } : {} }));
|
|
1864
|
+
}
|
|
1865
|
+
function proxyrouteof(value) {
|
|
1866
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
|
|
1867
|
+
const options = value;
|
|
1868
|
+
if (options.scheme !== "http" && options.scheme !== "https" && options.scheme !== "socks4" && options.scheme !== "socks5") return void 0;
|
|
1869
|
+
if (typeof options.host !== "string" || !options.host.trim()) return void 0;
|
|
1870
|
+
if (typeof options.port !== "number" || !Number.isInteger(options.port) || options.port < 1 || options.port > 65535) return void 0;
|
|
1871
|
+
if (!Array.isArray(options.bypass) || options.bypass.length === 0 || !options.bypass.every((item) => typeof item === "string" && item.trim().length > 0)) return void 0;
|
|
1872
|
+
return { scheme: options.scheme, host: options.host.trim(), port: options.port, bypass: options.bypass.map((item) => item.trim()) };
|
|
1873
|
+
}
|
|
1874
|
+
function ratelimitreadof(headers, origin, now) {
|
|
1875
|
+
const pick = (name) => {
|
|
1876
|
+
for (const key of Object.keys(headers)) {
|
|
1877
|
+
if (key.toLowerCase() !== name) continue;
|
|
1878
|
+
const value = Number(headers[key]);
|
|
1879
|
+
return Number.isFinite(value) && value >= 0 ? value : void 0;
|
|
1880
|
+
}
|
|
1881
|
+
return void 0;
|
|
1882
|
+
};
|
|
1883
|
+
const remaining = pick("x-ratelimit-remaining");
|
|
1884
|
+
const limit = pick("x-ratelimit-limit");
|
|
1885
|
+
const reset = pick("x-ratelimit-reset");
|
|
1886
|
+
if (remaining === void 0 && limit === void 0 && reset === void 0) return void 0;
|
|
1887
|
+
const read = { origin, ...remaining !== void 0 ? { remaining } : {}, ...limit !== void 0 ? { limit } : {}, resetat: now, at: now };
|
|
1888
|
+
if (reset !== void 0) read.resetat = reset > Math.floor(now / 1e3) ? reset * 1e3 : now + reset * 1e3;
|
|
1889
|
+
return read;
|
|
1890
|
+
}
|
|
1891
|
+
function retryafterof(status, headers) {
|
|
1892
|
+
if (status !== 429 && status !== 503) return void 0;
|
|
1893
|
+
for (const key of Object.keys(headers)) {
|
|
1894
|
+
if (key.toLowerCase() !== "retry-after") continue;
|
|
1895
|
+
const raw = headers[key];
|
|
1896
|
+
if (raw === void 0) continue;
|
|
1897
|
+
const value = raw.trim();
|
|
1898
|
+
const seconds = Number(value);
|
|
1899
|
+
if (Number.isFinite(seconds) && seconds >= 0) return seconds * 1e3;
|
|
1900
|
+
const date = Date.parse(value);
|
|
1901
|
+
if (Number.isFinite(date)) return Math.max(0, date - Date.now());
|
|
1902
|
+
return void 0;
|
|
1903
|
+
}
|
|
1904
|
+
return void 0;
|
|
1905
|
+
}
|
|
1906
|
+
function ratelimitwait(state, now) {
|
|
1907
|
+
if (!state) return 0;
|
|
1908
|
+
return Math.max(0, state.resetat - now);
|
|
1909
|
+
}
|
|
1910
|
+
|
|
1911
|
+
// netauth.ts
|
|
1912
|
+
function oauthflowof(value) {
|
|
1913
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
|
|
1914
|
+
const options = value;
|
|
1915
|
+
if (typeof options.provider !== "string" || !options.provider.trim()) return void 0;
|
|
1916
|
+
if (typeof options.authorizeurl !== "string" || !options.authorizeurl.trim()) return void 0;
|
|
1917
|
+
if (typeof options.tokenurl !== "string" || !options.tokenurl.trim()) return void 0;
|
|
1918
|
+
if (!Array.isArray(options.scopes) || options.scopes.length === 0 || !options.scopes.every((item) => typeof item === "string" && item.trim().length > 0)) return void 0;
|
|
1919
|
+
if (typeof options.redirectorigin !== "string" || !options.redirectorigin.trim()) return void 0;
|
|
1920
|
+
return { provider: options.provider.trim(), authorizeurl: options.authorizeurl.trim(), tokenurl: options.tokenurl.trim(), scopes: options.scopes.map((item) => item.trim()), redirectorigin: options.redirectorigin.trim() };
|
|
1921
|
+
}
|
|
1922
|
+
function authorizeurl(flow, state) {
|
|
1923
|
+
const url = new URL(flow.authorizeurl);
|
|
1924
|
+
url.searchParams.set("response_type", "code");
|
|
1925
|
+
url.searchParams.set("redirect_uri", flow.redirectorigin);
|
|
1926
|
+
url.searchParams.set("scope", flow.scopes.join(" "));
|
|
1927
|
+
url.searchParams.set("state", state);
|
|
1928
|
+
return url.toString();
|
|
1929
|
+
}
|
|
1930
|
+
function capturecode(url, redirectorigin, state) {
|
|
1931
|
+
let parsed;
|
|
1932
|
+
try {
|
|
1933
|
+
parsed = new URL(url);
|
|
1934
|
+
} catch {
|
|
1935
|
+
return { error: "The redirect url does not parse for the code capture." };
|
|
1936
|
+
}
|
|
1937
|
+
const granted = redirectorigin.includes("/", redirectorigin.indexOf("://") + 3) ? `${parsed.origin}${parsed.pathname}`.startsWith(redirectorigin) : parsed.origin === redirectorigin;
|
|
1938
|
+
if (!granted) return { error: `The redirect landed on ${parsed.origin} outside the granted redirect origin ${redirectorigin}.` };
|
|
1939
|
+
const returned = parsed.searchParams.get("state");
|
|
1940
|
+
if (returned !== state) return { error: "The redirect state token does not match the reviewed flow." };
|
|
1941
|
+
const error = parsed.searchParams.get("error");
|
|
1942
|
+
if (error) return { error: `The provider refused the flow: ${error}.` };
|
|
1943
|
+
const code = parsed.searchParams.get("code");
|
|
1944
|
+
if (!code) return { error: "The redirect carries no authorization code." };
|
|
1945
|
+
return { code };
|
|
1946
|
+
}
|
|
1947
|
+
function parsetokens(body) {
|
|
1948
|
+
let parsed;
|
|
1949
|
+
try {
|
|
1950
|
+
parsed = JSON.parse(body);
|
|
1951
|
+
} catch {
|
|
1952
|
+
return void 0;
|
|
1953
|
+
}
|
|
1954
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return void 0;
|
|
1955
|
+
const record2 = parsed;
|
|
1956
|
+
const tokens = {};
|
|
1957
|
+
if (typeof record2.access_token === "string" && record2.access_token) tokens.accesstoken = record2.access_token;
|
|
1958
|
+
if (typeof record2.refresh_token === "string" && record2.refresh_token) tokens.refreshtoken = record2.refresh_token;
|
|
1959
|
+
if (typeof record2.expires_in === "number" && Number.isFinite(record2.expires_in) && record2.expires_in >= 0) tokens.expiresin = record2.expires_in;
|
|
1960
|
+
if (typeof record2.scope === "string" && record2.scope.trim()) tokens.scopes = record2.scope.trim().split(/\s+/);
|
|
1961
|
+
if (tokens.accesstoken === void 0 && tokens.refreshtoken === void 0) return void 0;
|
|
1962
|
+
return tokens;
|
|
1963
|
+
}
|
|
1964
|
+
function tokenrequest(flow, input) {
|
|
1965
|
+
if (input.refreshtoken !== void 0) return { url: flow.tokenurl, body: urlencodeform([{ name: "grant_type", value: "refresh_token" }, { name: "refresh_token", value: input.refreshtoken }]) };
|
|
1966
|
+
return { url: flow.tokenurl, body: urlencodeform([{ name: "grant_type", value: "authorization_code" }, { name: "code", value: input.code ?? "" }, { name: "redirect_uri", value: flow.redirectorigin }]) };
|
|
1967
|
+
}
|
|
1968
|
+
function revocationruleof(value) {
|
|
1969
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
|
|
1970
|
+
const options = value;
|
|
1971
|
+
if (!Array.isArray(options.tokenids) || options.tokenids.length === 0 || !options.tokenids.every((item) => typeof item === "string" && item.trim().length > 0)) return void 0;
|
|
1972
|
+
if (typeof options.reason !== "string" || !options.reason.trim()) return void 0;
|
|
1973
|
+
return { tokenids: options.tokenids.map((item) => item.trim()), reason: options.reason.trim(), revokedat: Date.now() };
|
|
1974
|
+
}
|
|
1975
|
+
function formpayloadof(value) {
|
|
1976
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
|
|
1977
|
+
const options = value;
|
|
1978
|
+
if (typeof options.url !== "string" || !options.url.trim()) return void 0;
|
|
1979
|
+
if (!Array.isArray(options.fields) || options.fields.length === 0) return void 0;
|
|
1980
|
+
const fields = [];
|
|
1981
|
+
for (const item of options.fields) {
|
|
1982
|
+
if (!item || typeof item !== "object" || Array.isArray(item)) return void 0;
|
|
1983
|
+
const field = item;
|
|
1984
|
+
if (typeof field.name !== "string" || !field.name.trim()) return void 0;
|
|
1985
|
+
if (typeof field.value !== "string") return void 0;
|
|
1986
|
+
fields.push({ name: field.name.trim(), value: field.value });
|
|
1987
|
+
}
|
|
1988
|
+
return { url: options.url.trim(), fields };
|
|
1989
|
+
}
|
|
1990
|
+
function urlencodeform(fields) {
|
|
1991
|
+
return fields.map((field) => `${formencode(field.name)}=${formencode(field.value)}`).join("&");
|
|
1992
|
+
}
|
|
1993
|
+
function formencode(value) {
|
|
1994
|
+
const bytes = [...new TextEncoder().encode(value)];
|
|
1995
|
+
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("");
|
|
1996
|
+
}
|
|
1997
|
+
function multipartpayloadof(value) {
|
|
1998
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
|
|
1999
|
+
const options = value;
|
|
2000
|
+
if (typeof options.url !== "string" || !options.url.trim()) return void 0;
|
|
2001
|
+
if (!Array.isArray(options.files) || options.files.length === 0) return void 0;
|
|
2002
|
+
const fields = [];
|
|
2003
|
+
for (const item of Array.isArray(options.fields) ? options.fields : []) {
|
|
2004
|
+
if (!item || typeof item !== "object" || Array.isArray(item)) return void 0;
|
|
2005
|
+
const field = item;
|
|
2006
|
+
if (typeof field.name !== "string" || !field.name.trim()) return void 0;
|
|
2007
|
+
if (typeof field.value !== "string") return void 0;
|
|
2008
|
+
fields.push({ name: field.name.trim(), value: field.value });
|
|
2009
|
+
}
|
|
2010
|
+
const files = [];
|
|
2011
|
+
for (const item of options.files) {
|
|
2012
|
+
if (!item || typeof item !== "object" || Array.isArray(item)) return void 0;
|
|
2013
|
+
const file = item;
|
|
2014
|
+
if (typeof file.name !== "string" || !file.name.trim()) return void 0;
|
|
2015
|
+
if (typeof file.filename !== "string" || !file.filename.trim()) return void 0;
|
|
2016
|
+
if (typeof file.mime !== "string" || !file.mime.trim()) return void 0;
|
|
2017
|
+
if (typeof file.content !== "string") return void 0;
|
|
2018
|
+
if (file.reviewed !== true) return void 0;
|
|
2019
|
+
files.push({ name: file.name.trim(), filename: file.filename.trim(), mime: file.mime.trim(), content: file.content, reviewed: true });
|
|
2020
|
+
}
|
|
2021
|
+
const payload = { url: options.url.trim(), fields, files, ...typeof options.boundary === "string" && options.boundary.trim() ? { boundary: options.boundary.trim() } : {} };
|
|
2022
|
+
return payload;
|
|
2023
|
+
}
|
|
2024
|
+
function newboundary() {
|
|
2025
|
+
return `----devthink${Math.random().toString(16).slice(2)}${Date.now().toString(16)}`;
|
|
2026
|
+
}
|
|
2027
|
+
function multipartchunks(payload) {
|
|
2028
|
+
const boundary = payload.boundary ?? newboundary();
|
|
2029
|
+
const chunks = [];
|
|
2030
|
+
for (const field of payload.fields) chunks.push(`--${boundary}\r
|
|
2031
|
+
content-disposition: form-data; name="${field.name}"\r
|
|
2032
|
+
\r
|
|
2033
|
+
${field.value}\r
|
|
2034
|
+
`);
|
|
2035
|
+
for (const file of payload.files) chunks.push(`--${boundary}\r
|
|
2036
|
+
content-disposition: form-data; name="${file.name}"; filename="${file.filename}"\r
|
|
2037
|
+
content-type: ${file.mime}\r
|
|
2038
|
+
\r
|
|
2039
|
+
${file.content}\r
|
|
2040
|
+
`);
|
|
2041
|
+
chunks.push(`--${boundary}--\r
|
|
2042
|
+
`);
|
|
2043
|
+
return { chunks, boundary, bytes: chunks.reduce((total, chunk) => total + chunk.length, 0) };
|
|
2044
|
+
}
|
|
2045
|
+
|
|
1662
2046
|
// 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"]);
|
|
2047
|
+
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
2048
|
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"]);
|
|
2049
|
+
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"]);
|
|
1666
2050
|
var allowedactions = /* @__PURE__ */ new Set([...sensitiveactions, ...interactionactions, ...readactions]);
|
|
1667
2051
|
var watchactions = /* @__PURE__ */ new Set(["watchmutate", "watchbanner", "watchfocus", "watchtab"]);
|
|
1668
2052
|
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 +2061,7 @@ var mediaactions = /* @__PURE__ */ new Set(["capturepdf", "recordscreen", "captu
|
|
|
1677
2061
|
var httpactions = /* @__PURE__ */ new Set(["fetchurl", "parsejson", "parsehtml", "callrest", "callgraphql"]);
|
|
1678
2062
|
var socketactions = /* @__PURE__ */ new Set(["opensocket", "sendmessage", "waitmessage", "subscribesse", "longpoll"]);
|
|
1679
2063
|
var netwatchactions = /* @__PURE__ */ new Set(["watchrequests", "readheaders", "capturebodies", "mapapi", "extractapi"]);
|
|
2064
|
+
var controlactions = /* @__PURE__ */ new Set(["blockrequest", "mockresponse", "rewriteheaders", "setcookies", "readcookies", "clearcookies", "authflow", "saveapikey", "routeproxy", "postform", "postfiles"]);
|
|
1680
2065
|
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
2066
|
var fieldkinds = ["text", "email", "phone", "date", "number", "select", "check", "radio", "file", "password", "card", "code"];
|
|
1682
2067
|
var layoutmutationactions = /* @__PURE__ */ new Set(["grouptabs", "colorgroup", "collapsegroup", "savelayout", "restorelayout"]);
|
|
@@ -1717,6 +2102,7 @@ function requiredcapability(kind) {
|
|
|
1717
2102
|
if (kind === "readclipboard") return "clipboardRead";
|
|
1718
2103
|
if (kind === "writeclipboard" || kind === "copyscreen") return "clipboardWrite";
|
|
1719
2104
|
if (kind === "downloadimages") return "downloads";
|
|
2105
|
+
if (kind === "authflow") return "tabs";
|
|
1720
2106
|
if (kind === "openlink" || kind === "openprivate" || kind === "navlist" || kind === "batchopen" || kind === "reopentab" || kind === "deeplink") return "tabs";
|
|
1721
2107
|
if (tabscommandactions.has(kind)) return "tabs";
|
|
1722
2108
|
return void 0;
|
|
@@ -2416,6 +2802,9 @@ function issocketkind(kind) {
|
|
|
2416
2802
|
function isnetwatchkind(kind) {
|
|
2417
2803
|
return netwatchactions.has(kind);
|
|
2418
2804
|
}
|
|
2805
|
+
function iscontrolkind(kind) {
|
|
2806
|
+
return controlactions.has(kind);
|
|
2807
|
+
}
|
|
2419
2808
|
function resolvedrisk(step) {
|
|
2420
2809
|
if (step.kind === "capturebodies") {
|
|
2421
2810
|
let options = {};
|
|
@@ -2776,6 +3165,163 @@ function validatenetwatchgrammar(step, options) {
|
|
|
2776
3165
|
}
|
|
2777
3166
|
return { allowed: true };
|
|
2778
3167
|
}
|
|
3168
|
+
function validatecontrolgrammar(step, options) {
|
|
3169
|
+
const kind = step.kind;
|
|
3170
|
+
if (kind === "blockrequest") {
|
|
3171
|
+
const rule = blockruleof(options.block);
|
|
3172
|
+
if (!rule) return { allowed: false, reason: "A reviewed block rule with a url pattern is required in options.block." };
|
|
3173
|
+
if (patternorigin(rule.urlpattern) === void 0) return { allowed: false, reason: "Block rules need an https origin pattern; patterns without a named origin are refused." };
|
|
3174
|
+
if (options.block.reviewed !== true) return { allowed: false, reason: "The block rule carries the explicit reviewed flag before any request is blocked." };
|
|
3175
|
+
}
|
|
3176
|
+
if (kind === "mockresponse") {
|
|
3177
|
+
const spec = mockspecof(options.mock);
|
|
3178
|
+
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." };
|
|
3179
|
+
if (patternorigin(spec.urlpattern) === void 0) return { allowed: false, reason: "Mock fixtures need an https origin pattern; patterns without a named origin are refused." };
|
|
3180
|
+
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." };
|
|
3181
|
+
}
|
|
3182
|
+
if (kind === "rewriteheaders") {
|
|
3183
|
+
const rules = options.rules;
|
|
3184
|
+
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." };
|
|
3185
|
+
for (const item of rules) {
|
|
3186
|
+
const rule = headeruleof(item);
|
|
3187
|
+
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." };
|
|
3188
|
+
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." };
|
|
3189
|
+
}
|
|
3190
|
+
}
|
|
3191
|
+
if (kind === "setcookies") {
|
|
3192
|
+
const cookies = options.cookies;
|
|
3193
|
+
if (!Array.isArray(cookies) || cookies.length === 0) return { allowed: false, reason: "A reviewed non-empty list of cookie records is required in options.cookies." };
|
|
3194
|
+
for (const item of cookies) {
|
|
3195
|
+
if (!cookierecordof(item)) return { allowed: false, reason: "Every cookie record needs a name, domain, path and reviewed string value with an optional expiry." };
|
|
3196
|
+
}
|
|
3197
|
+
}
|
|
3198
|
+
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." };
|
|
3199
|
+
if (kind === "clearcookies") {
|
|
3200
|
+
if (!isnonempty(options.domain)) return { allowed: false, reason: "A reviewed cookie domain is required before cookies are cleared." };
|
|
3201
|
+
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." };
|
|
3202
|
+
}
|
|
3203
|
+
if (kind === "authflow") {
|
|
3204
|
+
const flow = oauthflowof(options.oauth);
|
|
3205
|
+
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." };
|
|
3206
|
+
if (!ishttpsurl(flow.authorizeurl) || !ishttpsurl(flow.tokenurl)) return { allowed: false, reason: "The oauth authorize and token urls must use HTTPS." };
|
|
3207
|
+
if (!ishttpsurl(flow.redirectorigin) && !/^https:\/\/[^/]+\/?$/.test(flow.redirectorigin)) return { allowed: false, reason: "The oauth redirect origin must be an HTTPS origin inside the grants." };
|
|
3208
|
+
const consent = authconsentgranted(step);
|
|
3209
|
+
if (!consent.allowed) return consent;
|
|
3210
|
+
}
|
|
3211
|
+
if (kind === "saveapikey") {
|
|
3212
|
+
const key = options.key;
|
|
3213
|
+
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." };
|
|
3214
|
+
const entry = key;
|
|
3215
|
+
if (!isnonempty(entry.name)) return { allowed: false, reason: "The api key entry needs a reviewed non-empty name." };
|
|
3216
|
+
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." };
|
|
3217
|
+
if (!isnonempty(entry.header)) return { allowed: false, reason: "The api key entry needs a reviewed non-empty header name." };
|
|
3218
|
+
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." };
|
|
3219
|
+
const consent = apikeyconsentgranted(step);
|
|
3220
|
+
if (!consent.allowed) return consent;
|
|
3221
|
+
}
|
|
3222
|
+
if (kind === "routeproxy") {
|
|
3223
|
+
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." };
|
|
3224
|
+
if (!isnonempty(options.consentref)) return { allowed: false, reason: "Proxy routing needs the explicit reviewed consent ref before any route applies." };
|
|
3225
|
+
}
|
|
3226
|
+
if (kind === "postform") {
|
|
3227
|
+
const form = formpayloadof(options.form);
|
|
3228
|
+
if (!form) return { allowed: false, reason: "A reviewed form payload with a url and a non-empty field list is required in options.form." };
|
|
3229
|
+
if (!ishttpsurl(form.url)) return { allowed: false, reason: "The form submission target must use HTTPS." };
|
|
3230
|
+
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." };
|
|
3231
|
+
}
|
|
3232
|
+
if (kind === "postfiles") {
|
|
3233
|
+
const upload = multipartpayloadof(options.upload);
|
|
3234
|
+
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." };
|
|
3235
|
+
if (!ishttpsurl(upload.url)) return { allowed: false, reason: "The multipart upload target must use HTTPS." };
|
|
3236
|
+
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." };
|
|
3237
|
+
}
|
|
3238
|
+
return { allowed: true };
|
|
3239
|
+
}
|
|
3240
|
+
function blockgate(session, step, now) {
|
|
3241
|
+
if (!session || session.stoppedat) return { allowed: false, reason: "No active browser session exists for the request block." };
|
|
3242
|
+
if (session.expiresat <= now) return { allowed: false, reason: "The browser session has expired and cannot block requests." };
|
|
3243
|
+
if (session.pausedat) return { allowed: false, reason: "The browser session is paused and cannot block requests." };
|
|
3244
|
+
let options = {};
|
|
3245
|
+
try {
|
|
3246
|
+
options = parseoptions(step);
|
|
3247
|
+
} catch {
|
|
3248
|
+
options = {};
|
|
3249
|
+
}
|
|
3250
|
+
const rule = options.block;
|
|
3251
|
+
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." };
|
|
3252
|
+
if (!blockruleof(rule)) return { allowed: false, reason: "The block rule needs a url pattern and an optional resource type list." };
|
|
3253
|
+
return { allowed: true };
|
|
3254
|
+
}
|
|
3255
|
+
function cookiegate(session, domain, now) {
|
|
3256
|
+
if (!session || session.stoppedat) return { allowed: false, reason: "No active browser session exists for the cookie operation." };
|
|
3257
|
+
if (session.expiresat <= now) return { allowed: false, reason: "The browser session has expired and cannot touch cookies." };
|
|
3258
|
+
if (session.pausedat) return { allowed: false, reason: "The browser session is paused and cannot touch cookies." };
|
|
3259
|
+
const grants = session.grants ?? [session.origin];
|
|
3260
|
+
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.` };
|
|
3261
|
+
return { allowed: true };
|
|
3262
|
+
}
|
|
3263
|
+
function proxygate(session, step, now) {
|
|
3264
|
+
if (!session || session.stoppedat) return { allowed: false, reason: "No active browser session exists for the proxy route." };
|
|
3265
|
+
if (session.expiresat <= now) return { allowed: false, reason: "The browser session has expired and cannot change routing." };
|
|
3266
|
+
if (session.pausedat) return { allowed: false, reason: "The browser session is paused and cannot change routing." };
|
|
3267
|
+
let options = {};
|
|
3268
|
+
try {
|
|
3269
|
+
options = parseoptions(step);
|
|
3270
|
+
} catch {
|
|
3271
|
+
options = {};
|
|
3272
|
+
}
|
|
3273
|
+
if (!isnonempty(options.consentref)) return { allowed: false, reason: "Proxy routing needs the explicit reviewed consent ref before any route applies." };
|
|
3274
|
+
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." };
|
|
3275
|
+
return { allowed: true };
|
|
3276
|
+
}
|
|
3277
|
+
function authconsentgranted(step) {
|
|
3278
|
+
let options = {};
|
|
3279
|
+
try {
|
|
3280
|
+
options = parseoptions(step);
|
|
3281
|
+
} catch {
|
|
3282
|
+
options = {};
|
|
3283
|
+
}
|
|
3284
|
+
const consentref = options.consentref;
|
|
3285
|
+
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." };
|
|
3286
|
+
return { allowed: true };
|
|
3287
|
+
}
|
|
3288
|
+
function apikeyconsentgranted(step) {
|
|
3289
|
+
let options = {};
|
|
3290
|
+
try {
|
|
3291
|
+
options = parseoptions(step);
|
|
3292
|
+
} catch {
|
|
3293
|
+
options = {};
|
|
3294
|
+
}
|
|
3295
|
+
const consentref = options.consentref;
|
|
3296
|
+
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." };
|
|
3297
|
+
return { allowed: true };
|
|
3298
|
+
}
|
|
3299
|
+
function ratelimitbudgetallowed(wait, budget) {
|
|
3300
|
+
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." };
|
|
3301
|
+
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." };
|
|
3302
|
+
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.` };
|
|
3303
|
+
return { allowed: true };
|
|
3304
|
+
}
|
|
3305
|
+
function controltarget(step) {
|
|
3306
|
+
let options = {};
|
|
3307
|
+
try {
|
|
3308
|
+
options = parseoptions(step);
|
|
3309
|
+
} catch {
|
|
3310
|
+
options = {};
|
|
3311
|
+
}
|
|
3312
|
+
for (const key of ["form", "upload"]) {
|
|
3313
|
+
const value = options[key];
|
|
3314
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
3315
|
+
const url = value.url;
|
|
3316
|
+
if (typeof url === "string" && url.trim()) return url.trim();
|
|
3317
|
+
}
|
|
3318
|
+
}
|
|
3319
|
+
if (step.kind === "authflow") {
|
|
3320
|
+
const flow = oauthflowof(options.oauth);
|
|
3321
|
+
if (flow) return flow.tokenurl;
|
|
3322
|
+
}
|
|
3323
|
+
return void 0;
|
|
3324
|
+
}
|
|
2779
3325
|
function sockettarget(step) {
|
|
2780
3326
|
let options = {};
|
|
2781
3327
|
try {
|
|
@@ -3161,6 +3707,10 @@ function validatestep(step, origin) {
|
|
|
3161
3707
|
const netwatchcheck = validatenetwatchgrammar(step, options);
|
|
3162
3708
|
if (!netwatchcheck.allowed) return netwatchcheck;
|
|
3163
3709
|
}
|
|
3710
|
+
if (iscontrolkind(step.kind)) {
|
|
3711
|
+
const controlcheck = validatecontrolgrammar(step, options);
|
|
3712
|
+
if (!controlcheck.allowed) return controlcheck;
|
|
3713
|
+
}
|
|
3164
3714
|
if (step.kind === "tabcreate") {
|
|
3165
3715
|
if (options.background !== void 0 && typeof options.background !== "boolean") return { allowed: false, reason: "The reviewed background flag must be a boolean." };
|
|
3166
3716
|
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 +3812,55 @@ function canexecute(input) {
|
|
|
3262
3812
|
const watchgatecheck = watchgate(input.session, input.settings, now);
|
|
3263
3813
|
if (!watchgatecheck.allowed) return watchgatecheck;
|
|
3264
3814
|
}
|
|
3815
|
+
if (iscontrolkind(input.step.kind)) {
|
|
3816
|
+
const controlgate = sessiongate({ session: input.session, tabid: input.tabid, origin: input.origin, now, action: "control the network" });
|
|
3817
|
+
if (!controlgate.allowed) return controlgate;
|
|
3818
|
+
let controloptions = {};
|
|
3819
|
+
try {
|
|
3820
|
+
controloptions = parseoptions(input.step);
|
|
3821
|
+
} catch {
|
|
3822
|
+
controloptions = {};
|
|
3823
|
+
}
|
|
3824
|
+
if (input.step.kind === "blockrequest") {
|
|
3825
|
+
const blockgatecheck = blockgate(input.session, input.step, now);
|
|
3826
|
+
if (!blockgatecheck.allowed) return blockgatecheck;
|
|
3827
|
+
const rule = blockruleof(controloptions.block);
|
|
3828
|
+
if (rule) {
|
|
3829
|
+
const blockorigin = origincheck(input.session, rule.urlpattern);
|
|
3830
|
+
if (!blockorigin.allowed) return blockorigin;
|
|
3831
|
+
}
|
|
3832
|
+
}
|
|
3833
|
+
if (input.step.kind === "mockresponse" || input.step.kind === "rewriteheaders") {
|
|
3834
|
+
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 ?? "") : "") : [];
|
|
3835
|
+
for (const pattern of patterns) {
|
|
3836
|
+
const patterngate = origincheck(input.session, pattern);
|
|
3837
|
+
if (!patterngate.allowed) return patterngate;
|
|
3838
|
+
}
|
|
3839
|
+
}
|
|
3840
|
+
if (input.step.kind === "setcookies" || input.step.kind === "readcookies" || input.step.kind === "clearcookies") {
|
|
3841
|
+
const domain = typeof controloptions.domain === "string" && controloptions.domain.trim() ? controloptions.domain : Array.isArray(controloptions.cookies) ? String(controloptions.cookies[0]?.domain ?? "") : "";
|
|
3842
|
+
if (!domain) return { allowed: false, reason: "A reviewed cookie domain is required before cookie control runs." };
|
|
3843
|
+
const cookiegatecheck = cookiegate(input.session, domain, now);
|
|
3844
|
+
if (!cookiegatecheck.allowed) return cookiegatecheck;
|
|
3845
|
+
}
|
|
3846
|
+
if (input.step.kind === "authflow") {
|
|
3847
|
+
const authconsent = authconsentgranted(input.step);
|
|
3848
|
+
if (!authconsent.allowed) return authconsent;
|
|
3849
|
+
}
|
|
3850
|
+
if (input.step.kind === "saveapikey") {
|
|
3851
|
+
const keyconsent = apikeyconsentgranted(input.step);
|
|
3852
|
+
if (!keyconsent.allowed) return keyconsent;
|
|
3853
|
+
}
|
|
3854
|
+
if (input.step.kind === "routeproxy") {
|
|
3855
|
+
const proxygatecheck = proxygate(input.session, input.step, now);
|
|
3856
|
+
if (!proxygatecheck.allowed) return proxygatecheck;
|
|
3857
|
+
}
|
|
3858
|
+
const target = controltarget(input.step);
|
|
3859
|
+
if (target !== void 0) {
|
|
3860
|
+
const targetgate = origincheck(input.session, target);
|
|
3861
|
+
if (!targetgate.allowed) return targetgate;
|
|
3862
|
+
}
|
|
3863
|
+
}
|
|
3265
3864
|
if (input.step.kind === "extractapi") {
|
|
3266
3865
|
let replayoptions = {};
|
|
3267
3866
|
try {
|
|
@@ -3431,9 +4030,19 @@ function recordpoll(progress, planid, stepid, entry, now) {
|
|
|
3431
4030
|
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
4031
|
return recordoutcome(base, planid, outcome, now);
|
|
3433
4032
|
}
|
|
4033
|
+
function recordcontrol(progress, planid, stepid, entry, now) {
|
|
4034
|
+
const base = progress && progress.planid === planid ? progress : emptyprogress(planid, now);
|
|
4035
|
+
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 };
|
|
4036
|
+
return recordoutcome(base, planid, outcome, now);
|
|
4037
|
+
}
|
|
4038
|
+
function recordupload(progress, planid, stepid, entry, now) {
|
|
4039
|
+
const base = progress && progress.planid === planid ? progress : emptyprogress(planid, now);
|
|
4040
|
+
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 };
|
|
4041
|
+
return recordoutcome(base, planid, outcome, now);
|
|
4042
|
+
}
|
|
3434
4043
|
|
|
3435
4044
|
// version.ts
|
|
3436
|
-
var packageversion = "1.1.
|
|
4045
|
+
var packageversion = "1.1.44";
|
|
3437
4046
|
|
|
3438
4047
|
// types.ts
|
|
3439
4048
|
var protocolversion = packageversion;
|
|
@@ -3470,6 +4079,27 @@ function parseproposal(value, origin, grants) {
|
|
|
3470
4079
|
...typeof candidate.value === "string" ? { value: candidate.value } : {},
|
|
3471
4080
|
...typeof candidate.options === "string" ? { options: candidate.options } : {}
|
|
3472
4081
|
};
|
|
4082
|
+
if (step.kind === "blockrequest") {
|
|
4083
|
+
let blockoptions = {};
|
|
4084
|
+
try {
|
|
4085
|
+
blockoptions = parseoptions(step);
|
|
4086
|
+
} catch {
|
|
4087
|
+
blockoptions = {};
|
|
4088
|
+
}
|
|
4089
|
+
const rule = blockruleof(blockoptions.block);
|
|
4090
|
+
if (rule && patternorigin(rule.urlpattern) === void 0) throw new Error("Block rules without a named origin pattern are refused.");
|
|
4091
|
+
}
|
|
4092
|
+
if (step.kind === "routeproxy") {
|
|
4093
|
+
let proxyoptions = {};
|
|
4094
|
+
try {
|
|
4095
|
+
proxyoptions = parseoptions(step);
|
|
4096
|
+
} catch {
|
|
4097
|
+
proxyoptions = {};
|
|
4098
|
+
}
|
|
4099
|
+
const proxy = proxyoptions.proxy;
|
|
4100
|
+
const bypass = proxy && typeof proxy === "object" && !Array.isArray(proxy) ? proxy.bypass : void 0;
|
|
4101
|
+
if (!Array.isArray(bypass) || bypass.length === 0) throw new Error("Proxy routes without a bypass list are refused.");
|
|
4102
|
+
}
|
|
3473
4103
|
const evaluation = validatestep(step, origin);
|
|
3474
4104
|
if (!evaluation.allowed) throw new Error(evaluation.reason);
|
|
3475
4105
|
const target = outboundtarget(step);
|
|
@@ -3544,7 +4174,7 @@ function requestbody(input) {
|
|
|
3544
4174
|
return JSON.stringify({ version: protocolversion, objective: input.objective, session: input.session, observation: input.observation, capabilities: input.capabilities });
|
|
3545
4175
|
}
|
|
3546
4176
|
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 } : {} });
|
|
4177
|
+
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 } : {} });
|
|
3548
4178
|
}
|
|
3549
4179
|
function mapresponse(input) {
|
|
3550
4180
|
return JSON.stringify({ version: protocolversion, planid: input.plan.id, planstate: input.plan.state, map: input.map });
|
|
@@ -3623,6 +4253,23 @@ function callsreport(input) {
|
|
|
3623
4253
|
function exchangesreport(input) {
|
|
3624
4254
|
return { version: protocolversion, exchanges: input.exchanges, channels: input.channels, subscriptions: input.subscriptions, apimap: input.apimap };
|
|
3625
4255
|
}
|
|
4256
|
+
function authreport(input) {
|
|
4257
|
+
const tokens = input.tokens.map((token) => {
|
|
4258
|
+
const { accessstorageid, refreshstorageid, ...metadata } = token;
|
|
4259
|
+
void accessstorageid;
|
|
4260
|
+
void refreshstorageid;
|
|
4261
|
+
return metadata;
|
|
4262
|
+
});
|
|
4263
|
+
return { version: protocolversion, tokens };
|
|
4264
|
+
}
|
|
4265
|
+
function controlreport(input) {
|
|
4266
|
+
const mocks = input.mocks.map((spec) => {
|
|
4267
|
+
const { body, ...metadata } = spec;
|
|
4268
|
+
void body;
|
|
4269
|
+
return metadata;
|
|
4270
|
+
});
|
|
4271
|
+
return { version: protocolversion, blocks: input.blocks, mocks, rewrites: input.rewrites, cookies: input.cookies, proxies: input.proxies, ratelimits: input.ratelimits };
|
|
4272
|
+
}
|
|
3626
4273
|
|
|
3627
4274
|
// capture.ts
|
|
3628
4275
|
var capturekinds = ["shotview", "shotfullpage", "shotelement", "shotregion", "contactsheet"];
|
|
@@ -5311,7 +5958,7 @@ function stepoptions2(step) {
|
|
|
5311
5958
|
}
|
|
5312
5959
|
async function refreshcapabilities() {
|
|
5313
5960
|
const report = await readcapabilities();
|
|
5314
|
-
const withmedia = { ...report, captures: [...capturekinds], media: [...mediakinds], http: [...httpkinds], netwatch: [...socketkinds, ...netwatchkinds] };
|
|
5961
|
+
const withmedia = { ...report, captures: [...capturekinds], media: [...mediakinds], http: [...httpkinds], netwatch: [...socketkinds, ...netwatchkinds], control: [...controlkinds] };
|
|
5315
5962
|
await memory.setcapabilities(withmedia);
|
|
5316
5963
|
return withmedia;
|
|
5317
5964
|
}
|
|
@@ -7876,7 +8523,7 @@ async function executehttpstep(step, session, plan, tabid2, origin) {
|
|
|
7876
8523
|
streamstate.bytes = total;
|
|
7877
8524
|
} } : void 0;
|
|
7878
8525
|
try {
|
|
7879
|
-
const transport = (url, init) =>
|
|
8526
|
+
const transport = (url, init) => controlledfetch(plan.id, url, init, controller, window2, streamstate);
|
|
7880
8527
|
const result = await sendfetch({ request, ...policy !== void 0 && Object.keys(policy).length > 0 ? { options: policy } : {}, transport, onretry: (attempt, wait, reason) => {
|
|
7881
8528
|
void (async () => {
|
|
7882
8529
|
await memory.setprogress(recordfetchretry(await memory.getprogress(), plan.id, step.id, { attempt, url: request.url, wait, reason }, Date.now()));
|
|
@@ -7932,7 +8579,7 @@ async function executehttpstep(step, session, plan, tabid2, origin) {
|
|
|
7932
8579
|
const controller = new AbortController();
|
|
7933
8580
|
activefetches.set(callid, controller);
|
|
7934
8581
|
try {
|
|
7935
|
-
const transport = (url, init) =>
|
|
8582
|
+
const transport = (url, init) => controlledfetch(plan.id, url, init, controller);
|
|
7936
8583
|
const keynames = Array.isArray(options.apikeys) ? options.apikeys.filter((name) => typeof name === "string" && name.trim().length > 0) : [];
|
|
7937
8584
|
if (step.kind === "callrest") {
|
|
7938
8585
|
const methodoverride = typeof options.method === "string" ? options.method.trim().toUpperCase() : void 0;
|
|
@@ -8379,6 +9026,353 @@ async function executenetwatchstep(step, session, plan, tabid2, origin) {
|
|
|
8379
9026
|
void origin;
|
|
8380
9027
|
throw new Error("Unsupported request observation kind.");
|
|
8381
9028
|
}
|
|
9029
|
+
var activerules = /* @__PURE__ */ new Map();
|
|
9030
|
+
var activeauthflows = /* @__PURE__ */ new Map();
|
|
9031
|
+
function rulesetof(runid) {
|
|
9032
|
+
const existing = activerules.get(runid);
|
|
9033
|
+
if (existing) return existing;
|
|
9034
|
+
const created = { blocks: [], mocks: [], rewrites: [] };
|
|
9035
|
+
activerules.set(runid, created);
|
|
9036
|
+
return created;
|
|
9037
|
+
}
|
|
9038
|
+
async function revertcontrolsforrun(runid, reason) {
|
|
9039
|
+
const ruleset = activerules.get(runid);
|
|
9040
|
+
if (!ruleset) return;
|
|
9041
|
+
const at = Date.now();
|
|
9042
|
+
let reverted = 0;
|
|
9043
|
+
for (const rule of ruleset.blocks) {
|
|
9044
|
+
if (rule.revertedat !== void 0) continue;
|
|
9045
|
+
await memory.addblockrule(revertrule(rule, at)).catch(() => void 0);
|
|
9046
|
+
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);
|
|
9047
|
+
reverted += 1;
|
|
9048
|
+
}
|
|
9049
|
+
for (const spec of ruleset.mocks) {
|
|
9050
|
+
if (spec.revertedat !== void 0) continue;
|
|
9051
|
+
await memory.addmockspec(revertrule(spec, at)).catch(() => void 0);
|
|
9052
|
+
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);
|
|
9053
|
+
reverted += 1;
|
|
9054
|
+
}
|
|
9055
|
+
for (const rule of ruleset.rewrites) {
|
|
9056
|
+
if (rule.revertedat !== void 0) continue;
|
|
9057
|
+
await memory.addheaderule(revertrule(rule, at)).catch(() => void 0);
|
|
9058
|
+
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);
|
|
9059
|
+
reverted += 1;
|
|
9060
|
+
}
|
|
9061
|
+
if (ruleset.proxy && ruleset.proxy.revertedat === void 0) {
|
|
9062
|
+
await memory.addproxyroute(revertrule(ruleset.proxy, at)).catch(() => void 0);
|
|
9063
|
+
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);
|
|
9064
|
+
reverted += 1;
|
|
9065
|
+
}
|
|
9066
|
+
activerules.delete(runid);
|
|
9067
|
+
if (reverted > 0) {
|
|
9068
|
+
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);
|
|
9069
|
+
await refreshbadge().catch(() => void 0);
|
|
9070
|
+
}
|
|
9071
|
+
}
|
|
9072
|
+
async function cancelauthflowsforrun(runid, reason) {
|
|
9073
|
+
for (const [state, active] of [...activeauthflows.entries()]) {
|
|
9074
|
+
if (active.runid !== runid) continue;
|
|
9075
|
+
active.cancelled = true;
|
|
9076
|
+
if (active.tabid) await chrome.tabs.remove(active.tabid).catch(() => void 0);
|
|
9077
|
+
activeauthflows.delete(state);
|
|
9078
|
+
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);
|
|
9079
|
+
}
|
|
9080
|
+
}
|
|
9081
|
+
async function controlledfetch(runid, url, init, controller, window2, streamstate) {
|
|
9082
|
+
const ruleset = activerules.get(runid);
|
|
9083
|
+
if (ruleset) {
|
|
9084
|
+
for (const rule of ruleset.blocks) {
|
|
9085
|
+
if (rule.revertedat !== void 0) continue;
|
|
9086
|
+
if (!matchurlpattern(rule.urlpattern, url)) continue;
|
|
9087
|
+
rule.hits += 1;
|
|
9088
|
+
await memory.addblockrule(rule).catch(() => void 0);
|
|
9089
|
+
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);
|
|
9090
|
+
throw new Error(`The request to ${url} was blocked by the reviewed block rule ${rule.id}.`);
|
|
9091
|
+
}
|
|
9092
|
+
const fixture = mockfor(url, ruleset.mocks);
|
|
9093
|
+
if (fixture) {
|
|
9094
|
+
fixture.hits += 1;
|
|
9095
|
+
await memory.addmockspec(fixture).catch(() => void 0);
|
|
9096
|
+
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);
|
|
9097
|
+
return { status: fixture.status, headers: fixture.headers ?? {}, body: fixture.body ?? "" };
|
|
9098
|
+
}
|
|
9099
|
+
const rewritten = applyheaderules(url, init.headers, ruleset.rewrites);
|
|
9100
|
+
if (rewritten.applied.length > 0) {
|
|
9101
|
+
for (const rule of rewritten.applied) {
|
|
9102
|
+
rule.hits += 1;
|
|
9103
|
+
await memory.addheaderule(rule).catch(() => void 0);
|
|
9104
|
+
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);
|
|
9105
|
+
}
|
|
9106
|
+
init = { ...init, headers: rewritten.headers };
|
|
9107
|
+
}
|
|
9108
|
+
if (ruleset.proxy && ruleset.proxy.revertedat === void 0) {
|
|
9109
|
+
const targetorigin = new URL(url).origin;
|
|
9110
|
+
const bypassed = ruleset.proxy.bypass.some((pattern) => {
|
|
9111
|
+
try {
|
|
9112
|
+
return new URL(pattern).origin === targetorigin;
|
|
9113
|
+
} catch {
|
|
9114
|
+
return false;
|
|
9115
|
+
}
|
|
9116
|
+
});
|
|
9117
|
+
if (!bypassed) {
|
|
9118
|
+
const config = await memory.getconfig();
|
|
9119
|
+
if (config?.endpoint) {
|
|
9120
|
+
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);
|
|
9121
|
+
const relayed = await livefetch(config.endpoint, { ...init, headers: { ...init.headers, "x-devthink-target": url } }, controller, window2, streamstate);
|
|
9122
|
+
return relayed;
|
|
9123
|
+
}
|
|
9124
|
+
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);
|
|
9125
|
+
}
|
|
9126
|
+
}
|
|
9127
|
+
}
|
|
9128
|
+
const response = await livefetch(url, init, controller, window2, streamstate);
|
|
9129
|
+
const read = ratelimitreadof(response.headers, new URL(url).origin, Date.now());
|
|
9130
|
+
if (read) await memory.setratelimit(read).catch(() => void 0);
|
|
9131
|
+
return response;
|
|
9132
|
+
}
|
|
9133
|
+
async function executenetcontrolstep(step, session, plan, tabid2, origin) {
|
|
9134
|
+
const options = stepoptions2(step);
|
|
9135
|
+
const extra = { ...session ? { sessionid: session.id } : {}, planid: plan.id, stepid: step.id };
|
|
9136
|
+
const ruleset = rulesetof(plan.id);
|
|
9137
|
+
if (step.kind === "blockrequest") {
|
|
9138
|
+
const rule = blockruleof(options.block);
|
|
9139
|
+
if (!rule) throw new Error("A reviewed block rule with a url pattern is required in options.block.");
|
|
9140
|
+
const registered = newblockrule({ id: randomid(), runid: plan.id, stepid: step.id, urlpattern: rule.urlpattern, ...rule.resourcetypes !== void 0 ? { resourcetypes: rule.resourcetypes } : {}, at: Date.now() });
|
|
9141
|
+
ruleset.blocks = [...ruleset.blocks.filter((item) => item.urlpattern !== registered.urlpattern), registered];
|
|
9142
|
+
await memory.addblockrule(registered);
|
|
9143
|
+
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);
|
|
9144
|
+
await refreshbadge();
|
|
9145
|
+
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." } };
|
|
9146
|
+
}
|
|
9147
|
+
if (step.kind === "mockresponse") {
|
|
9148
|
+
const spec = mockspecof(options.mock);
|
|
9149
|
+
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.");
|
|
9150
|
+
let fixturebody = spec.body ?? "";
|
|
9151
|
+
let bodyref;
|
|
9152
|
+
if (spec.bodyref !== void 0) {
|
|
9153
|
+
const captured = await memory.getbody(spec.bodyref);
|
|
9154
|
+
if (!captured) throw new Error(`No captured body matches ${spec.bodyref}; capture it again before the fixture replays it.`);
|
|
9155
|
+
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.`);
|
|
9156
|
+
fixturebody = captured.body;
|
|
9157
|
+
bodyref = spec.bodyref;
|
|
9158
|
+
}
|
|
9159
|
+
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() });
|
|
9160
|
+
ruleset.mocks = [...ruleset.mocks.filter((item) => item.urlpattern !== registered.urlpattern), registered];
|
|
9161
|
+
await memory.addmockspec(registered);
|
|
9162
|
+
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);
|
|
9163
|
+
await refreshbadge();
|
|
9164
|
+
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 } } };
|
|
9165
|
+
}
|
|
9166
|
+
if (step.kind === "rewriteheaders") {
|
|
9167
|
+
const rules = Array.isArray(options.rules) ? options.rules : [];
|
|
9168
|
+
const registered = [];
|
|
9169
|
+
for (const item of rules) {
|
|
9170
|
+
const rule = headeruleof(item);
|
|
9171
|
+
if (!rule) throw new Error("Every header rewrite rule needs a url pattern, header name, operation and value.");
|
|
9172
|
+
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() });
|
|
9173
|
+
registered.push(record2);
|
|
9174
|
+
}
|
|
9175
|
+
ruleset.rewrites = [...ruleset.rewrites.filter((item) => !registered.some((record2) => record2.urlpattern === item.urlpattern && record2.name === item.name)), ...registered];
|
|
9176
|
+
for (const record2 of registered) await memory.addheaderule(record2);
|
|
9177
|
+
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);
|
|
9178
|
+
await refreshbadge();
|
|
9179
|
+
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 })) } };
|
|
9180
|
+
}
|
|
9181
|
+
if (step.kind === "setcookies" || step.kind === "readcookies" || step.kind === "clearcookies") {
|
|
9182
|
+
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;
|
|
9183
|
+
const gate = cookiegate(session, domain, Date.now());
|
|
9184
|
+
if (!gate.allowed) throw new Error(gate.reason ?? "The cookie domain stays outside the session origin grants.");
|
|
9185
|
+
const pageorigin = `https://${domain.replace(/^\./, "")}`;
|
|
9186
|
+
if (step.kind === "setcookies") {
|
|
9187
|
+
const records = (Array.isArray(options.cookies) ? options.cookies : []).map((item) => cookierecordof(item)).filter((item) => item !== void 0);
|
|
9188
|
+
if (records.length === 0) throw new Error("A reviewed non-empty list of cookie records is required in options.cookies.");
|
|
9189
|
+
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 } : {} })));
|
|
9190
|
+
const operation2 = { id: randomid(), runid: plan.id, stepid: step.id, kind: "write", domain, names: records.map((record2) => record2.name), at: Date.now() };
|
|
9191
|
+
await memory.addcookieop(operation2);
|
|
9192
|
+
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);
|
|
9193
|
+
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 } };
|
|
9194
|
+
}
|
|
9195
|
+
if (step.kind === "readcookies") {
|
|
9196
|
+
const jar = await bridgecall(tabid2, "readcookies");
|
|
9197
|
+
const operation2 = { id: randomid(), runid: plan.id, stepid: step.id, kind: "read", domain, names: jar.map((cookie) => cookie.name), at: Date.now() };
|
|
9198
|
+
await memory.addcookieop(operation2);
|
|
9199
|
+
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);
|
|
9200
|
+
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 } };
|
|
9201
|
+
}
|
|
9202
|
+
const names = Array.isArray(options.names) ? options.names.filter((name) => typeof name === "string" && name.trim().length > 0) : void 0;
|
|
9203
|
+
const cleared = await bridgecall(tabid2, "clearcookies", names);
|
|
9204
|
+
const operation = { id: randomid(), runid: plan.id, stepid: step.id, kind: "clear", domain, names: names ?? [], at: Date.now() };
|
|
9205
|
+
await memory.addcookieop(operation);
|
|
9206
|
+
await audit("control", `Cleared ${cleared.cleared} cookie${cleared.cleared === 1 ? "" : "s"} of the granted domain ${domain} through the page cookie jar.`, extra);
|
|
9207
|
+
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 } : {} } };
|
|
9208
|
+
}
|
|
9209
|
+
if (step.kind === "authflow") {
|
|
9210
|
+
const flow = oauthflowof(options.oauth);
|
|
9211
|
+
if (!flow) throw new Error("A reviewed oauth flow with provider, authorize url, token url, scopes and redirect origin is required in options.oauth.");
|
|
9212
|
+
const tokenorigin = new URL(flow.tokenurl).origin;
|
|
9213
|
+
const tokenorigincheck = origincheck(session, tokenorigin);
|
|
9214
|
+
if (!tokenorigincheck.allowed) throw new Error(tokenorigincheck.reason ?? "The token endpoint stays outside the session origin grants.");
|
|
9215
|
+
const redirectcheck = origincheck(session, flow.redirectorigin);
|
|
9216
|
+
if (!redirectcheck.allowed) throw new Error(redirectcheck.reason ?? "The redirect origin stays outside the session origin grants.");
|
|
9217
|
+
if (options.refresh === true) {
|
|
9218
|
+
const tokenid = typeof options.token === "string" ? options.token : "";
|
|
9219
|
+
const stored = (await memory.listtokens()).find((token) => token.id === tokenid && token.revokedat === void 0);
|
|
9220
|
+
if (!stored) throw new Error(`No stored token record matches ${tokenid}.`);
|
|
9221
|
+
const refreshsecret = await memory.getsecret(stored.refreshstorageid ?? "");
|
|
9222
|
+
if (!refreshsecret) throw new Error("The stored token has no refresh secret; run the flow again.");
|
|
9223
|
+
const controller = new AbortController();
|
|
9224
|
+
activefetches.set(step.id, controller);
|
|
9225
|
+
try {
|
|
9226
|
+
const request = tokenrequest(flow, { refreshtoken: refreshsecret });
|
|
9227
|
+
const response = await controlledfetch(plan.id, request.url, { method: "POST", headers: { "content-type": "application/x-www-form-urlencoded" }, body: request.body, redirect: "error" }, controller);
|
|
9228
|
+
const tokens = parsetokens(response.body);
|
|
9229
|
+
if (!tokens?.accesstoken) throw new Error(`The token refresh failed with the ${response.status} status.`);
|
|
9230
|
+
const expiresat = Date.now() + (tokens.expiresin !== void 0 ? tokens.expiresin * 1e3 : 3600 * 1e3);
|
|
9231
|
+
await memory.setsecret(stored.accessstorageid, tokens.accesstoken);
|
|
9232
|
+
await memory.addtoken({ ...stored, expiresat, refreshedat: Date.now(), ...tokens.refreshtoken !== void 0 ? { refreshstorageid: stored.refreshstorageid } : {} });
|
|
9233
|
+
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);
|
|
9234
|
+
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 } } };
|
|
9235
|
+
} finally {
|
|
9236
|
+
controller.abort();
|
|
9237
|
+
activefetches.delete(step.id);
|
|
9238
|
+
}
|
|
9239
|
+
}
|
|
9240
|
+
if (options.revoke === true) {
|
|
9241
|
+
const rule = revocationruleof(options.revocation);
|
|
9242
|
+
const tokenids = rule ? rule.tokenids : Array.isArray(options.tokenids) ? options.tokenids.filter((item) => typeof item === "string" && item.trim().length > 0) : [];
|
|
9243
|
+
if (tokenids.length === 0) throw new Error("Token revocation needs reviewed token ids in options.");
|
|
9244
|
+
const records = await memory.listtokens();
|
|
9245
|
+
let revoked = 0;
|
|
9246
|
+
for (const tokenid of tokenids) {
|
|
9247
|
+
const stored = records.find((token) => token.id === tokenid);
|
|
9248
|
+
if (!stored || stored.revokedat !== void 0) continue;
|
|
9249
|
+
await memory.setsecret(stored.accessstorageid, "");
|
|
9250
|
+
if (stored.refreshstorageid) await memory.setsecret(stored.refreshstorageid, "");
|
|
9251
|
+
await memory.addtoken({ ...stored, revokedat: Date.now() });
|
|
9252
|
+
revoked += 1;
|
|
9253
|
+
}
|
|
9254
|
+
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);
|
|
9255
|
+
return { ok: true, summary: `Revoked ${revoked} token record${revoked === 1 ? "" : "s"}.`, details: { control: { applied: 0, blocked: 0, mocked: 0 }, revoked, tokenids } };
|
|
9256
|
+
}
|
|
9257
|
+
const open = [...activeauthflows.entries()].find(([, active]) => active.runid === plan.id && active.stepid === step.id && !active.cancelled);
|
|
9258
|
+
if (open) {
|
|
9259
|
+
const [state2, active] = open;
|
|
9260
|
+
if (active.tabid) {
|
|
9261
|
+
const tab = await chrome.tabs.get(active.tabid).catch(() => void 0);
|
|
9262
|
+
const redirected = tab?.url;
|
|
9263
|
+
if (redirected) {
|
|
9264
|
+
const captured = capturecode(redirected, active.flow.redirectorigin, state2);
|
|
9265
|
+
if ("code" in captured) {
|
|
9266
|
+
activeauthflows.delete(state2);
|
|
9267
|
+
const controller = new AbortController();
|
|
9268
|
+
activefetches.set(step.id, controller);
|
|
9269
|
+
try {
|
|
9270
|
+
const request = tokenrequest(active.flow, { code: captured.code });
|
|
9271
|
+
const response = await controlledfetch(plan.id, request.url, { method: "POST", headers: { "content-type": "application/x-www-form-urlencoded" }, body: request.body, redirect: "error" }, controller);
|
|
9272
|
+
const tokens = parsetokens(response.body);
|
|
9273
|
+
if (!tokens?.accesstoken) throw new Error(`The token exchange failed with the ${response.status} status.`);
|
|
9274
|
+
const tokenid = randomid();
|
|
9275
|
+
const expiresat = Date.now() + (tokens.expiresin !== void 0 ? tokens.expiresin * 1e3 : 3600 * 1e3);
|
|
9276
|
+
const accessstorageid = `token-${tokenid}-access`;
|
|
9277
|
+
await memory.setsecret(accessstorageid, tokens.accesstoken);
|
|
9278
|
+
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() };
|
|
9279
|
+
if (tokens.refreshtoken !== void 0) await memory.setsecret(record2.refreshstorageid ?? "", tokens.refreshtoken);
|
|
9280
|
+
await memory.addtoken(record2);
|
|
9281
|
+
await chrome.tabs.remove(active.tabid).catch(() => void 0);
|
|
9282
|
+
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);
|
|
9283
|
+
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" } } };
|
|
9284
|
+
} finally {
|
|
9285
|
+
controller.abort();
|
|
9286
|
+
activefetches.delete(step.id);
|
|
9287
|
+
}
|
|
9288
|
+
}
|
|
9289
|
+
if (captured.error) {
|
|
9290
|
+
activeauthflows.delete(state2);
|
|
9291
|
+
if (active.tabid) await chrome.tabs.remove(active.tabid).catch(() => void 0);
|
|
9292
|
+
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 } };
|
|
9293
|
+
}
|
|
9294
|
+
}
|
|
9295
|
+
}
|
|
9296
|
+
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" } } };
|
|
9297
|
+
}
|
|
9298
|
+
const state = `run-${plan.id.slice(0, 8)}-${randomid().slice(0, 8)}`;
|
|
9299
|
+
const consent = authorizeurl(flow, state);
|
|
9300
|
+
const opened = await chrome.tabs.create({ url: consent, active: true });
|
|
9301
|
+
activeauthflows.set(state, { runid: plan.id, stepid: step.id, flow, ...opened?.id !== void 0 ? { tabid: opened.id } : {}, cancelled: false });
|
|
9302
|
+
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);
|
|
9303
|
+
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." } };
|
|
9304
|
+
}
|
|
9305
|
+
if (step.kind === "saveapikey") {
|
|
9306
|
+
const key = options.key;
|
|
9307
|
+
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.");
|
|
9308
|
+
const entry = key;
|
|
9309
|
+
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() };
|
|
9310
|
+
await memory.setapikey(ref);
|
|
9311
|
+
await memory.setsecret(ref.storageid, String(entry.value ?? ""));
|
|
9312
|
+
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);
|
|
9313
|
+
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 } } };
|
|
9314
|
+
}
|
|
9315
|
+
if (step.kind === "routeproxy") {
|
|
9316
|
+
const route = proxyrouteof(options.proxy);
|
|
9317
|
+
if (!route) throw new Error("A reviewed proxy route with scheme, host, port and bypass list is required in options.proxy.");
|
|
9318
|
+
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() };
|
|
9319
|
+
ruleset.proxy = registered;
|
|
9320
|
+
await memory.addproxyroute(registered);
|
|
9321
|
+
const config = await memory.getconfig();
|
|
9322
|
+
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);
|
|
9323
|
+
await refreshbadge();
|
|
9324
|
+
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." } };
|
|
9325
|
+
}
|
|
9326
|
+
if (step.kind === "postform" || step.kind === "postfiles") {
|
|
9327
|
+
const payload = step.kind === "postform" ? formpayloadof(options.form) : void 0;
|
|
9328
|
+
const upload = step.kind === "postfiles" ? multipartpayloadof(options.upload) : void 0;
|
|
9329
|
+
const url = payload?.url ?? upload?.url;
|
|
9330
|
+
if (!url) throw new Error("A reviewed payload with a url is required in options.");
|
|
9331
|
+
const urlgate = origincheck(session, url);
|
|
9332
|
+
if (!urlgate.allowed) throw new Error(urlgate.reason ?? "The submission target stays outside the session origin grants.");
|
|
9333
|
+
const budget = typeof options.wait === "number" ? options.wait : void 0;
|
|
9334
|
+
const limits = await memory.getratelimits(Date.now());
|
|
9335
|
+
const limit = limits.find((item) => item.origin === new URL(url).origin);
|
|
9336
|
+
const wait = ratelimitwait(limit, Date.now());
|
|
9337
|
+
const budgetcheck = ratelimitbudgetallowed(wait, budget);
|
|
9338
|
+
if (!budgetcheck.allowed) throw new Error(budgetcheck.reason ?? "The rate limit wait exceeds the reviewed budget.");
|
|
9339
|
+
if (wait > 0) {
|
|
9340
|
+
await audit("control", `The rate limiter waits ${wait} milliseconds until the reset window of ${new URL(url).origin} passes before the submission.`, extra);
|
|
9341
|
+
await new Promise((resolve) => setTimeout(resolve, wait));
|
|
9342
|
+
}
|
|
9343
|
+
const controller = new AbortController();
|
|
9344
|
+
activefetches.set(step.id, controller);
|
|
9345
|
+
try {
|
|
9346
|
+
const started = Date.now();
|
|
9347
|
+
let response;
|
|
9348
|
+
if (payload) {
|
|
9349
|
+
const body2 = urlencodeform(payload.fields);
|
|
9350
|
+
response = await controlledfetch(plan.id, url, { method: "POST", headers: { "content-type": "application/x-www-form-urlencoded" }, body: body2, redirect: "error" }, controller);
|
|
9351
|
+
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);
|
|
9352
|
+
const retryafter2 = retryafterof(response.status, response.headers);
|
|
9353
|
+
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);
|
|
9354
|
+
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 } : {} } };
|
|
9355
|
+
}
|
|
9356
|
+
const streamed = multipartchunks(upload ?? { url, fields: [], files: [] });
|
|
9357
|
+
let uploaded = 0;
|
|
9358
|
+
for (let index = 0; index < streamed.chunks.length; index += 1) {
|
|
9359
|
+
uploaded += streamed.chunks[index]?.length ?? 0;
|
|
9360
|
+
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);
|
|
9361
|
+
}
|
|
9362
|
+
const body = streamed.chunks.join("");
|
|
9363
|
+
response = await controlledfetch(plan.id, url, { method: "POST", headers: { "content-type": `multipart/form-data; boundary=${streamed.boundary}` }, body, redirect: "error" }, controller);
|
|
9364
|
+
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);
|
|
9365
|
+
const retryafter = retryafterof(response.status, response.headers);
|
|
9366
|
+
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);
|
|
9367
|
+
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 } : {} } };
|
|
9368
|
+
} finally {
|
|
9369
|
+
controller.abort();
|
|
9370
|
+
activefetches.delete(step.id);
|
|
9371
|
+
}
|
|
9372
|
+
}
|
|
9373
|
+
void tabid2;
|
|
9374
|
+
throw new Error("Unsupported network control kind.");
|
|
9375
|
+
}
|
|
8382
9376
|
async function enforcewindowreview(step, session, plan) {
|
|
8383
9377
|
const windowid = step.value && /^\d+$/.test(step.value) ? Number.parseInt(step.value, 10) : 0;
|
|
8384
9378
|
const progress = plan ? await memory.getprogress() : void 0;
|
|
@@ -8423,8 +9417,9 @@ async function refreshbadge() {
|
|
|
8423
9417
|
const fetchprompts = (await memory.getfetchconsents()).filter((consent) => consent.approved === void 0).length;
|
|
8424
9418
|
const observedrequests = (await memory.getexchanges()).length;
|
|
8425
9419
|
const livechannels = (await memory.getchannels()).filter((channel) => channel.state === "open" || channel.state === "connecting").length;
|
|
9420
|
+
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
9421
|
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;
|
|
9422
|
+
const total = (queues?.prefetch ?? 0) + (queues?.batchopen ?? 0) + tasktabs2 + prompts + consents + quarantined + datasets + captures + media + recordingprompts + fetchprompts + observedrequests + livechannels + activerulescount;
|
|
8428
9423
|
await chrome.action.setBadgeText({ text: total > 0 ? String(total) : "" }).catch(() => {
|
|
8429
9424
|
});
|
|
8430
9425
|
}
|
|
@@ -8490,6 +9485,8 @@ async function executestep(stepid) {
|
|
|
8490
9485
|
output = await executesocketstep(step, session, plan, tab.id, origin);
|
|
8491
9486
|
} else if (isnetwatchkind(step.kind)) {
|
|
8492
9487
|
output = await executenetwatchstep(step, session, plan, tab.id, origin);
|
|
9488
|
+
} else if (iscontrolkind(step.kind)) {
|
|
9489
|
+
output = await executenetcontrolstep(step, session, plan, tab.id, origin);
|
|
8493
9490
|
} else {
|
|
8494
9491
|
if (step.target && freshcheckkinds.has(step.kind)) {
|
|
8495
9492
|
const fresh = await snapshot(tab.id);
|
|
@@ -8543,6 +9540,10 @@ async function executestep(stepid) {
|
|
|
8543
9540
|
});
|
|
8544
9541
|
await closechannelsforrun(plan.id).catch(() => {
|
|
8545
9542
|
});
|
|
9543
|
+
await cancelauthflowsforrun(plan.id, "plan completion").catch(() => {
|
|
9544
|
+
});
|
|
9545
|
+
await revertcontrolsforrun(plan.id, "plan completion").catch(() => {
|
|
9546
|
+
});
|
|
8546
9547
|
const done = { ...plan, state: "completed", completedat: Date.now() };
|
|
8547
9548
|
await memory.setplan(done);
|
|
8548
9549
|
await audit("complete", "Every reviewed step of the approved plan has executed.", { ...session ? { sessionid: session.id } : {}, planid: done.id });
|
|
@@ -8702,7 +9703,10 @@ async function handlerequest(message, sender) {
|
|
|
8702
9703
|
const messagecount = (await memory.getmessages()).length;
|
|
8703
9704
|
const endpoints = await memory.getendpoints();
|
|
8704
9705
|
const fetchconsents = await memory.getfetchconsents();
|
|
8705
|
-
const apikeys = (await memory.getapikeys()).map((ref) => ({ name: ref.name, origins: ref.origins, header: ref.header,
|
|
9706
|
+
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 } : {} }));
|
|
9707
|
+
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()) });
|
|
9708
|
+
const tokens = authreport({ tokens: await memory.listtokens() });
|
|
9709
|
+
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
9710
|
const runsettings = await memory.getsettings();
|
|
8707
9711
|
const scanhooks = [];
|
|
8708
9712
|
for (const hook of await memory.getscanhooks()) {
|
|
@@ -8714,7 +9718,7 @@ async function handlerequest(message, sender) {
|
|
|
8714
9718
|
const livetab = session ? await chrome.tabs.get(session.tabid).catch(() => void 0) : void 0;
|
|
8715
9719
|
const waitprofile = session ? waitprofiles.find((record2) => record2.origin === session.origin) : void 0;
|
|
8716
9720
|
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()] } : {} };
|
|
9721
|
+
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, 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
9722
|
}
|
|
8719
9723
|
case "capabilities":
|
|
8720
9724
|
return refreshcapabilities();
|
|
@@ -9327,12 +10331,12 @@ async function handlerequest(message, sender) {
|
|
|
9327
10331
|
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
10332
|
if (!inputkey.header?.trim()) throw new Error("A reviewed header name is required for the api key.");
|
|
9329
10333
|
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(
|
|
10334
|
+
const entry = { name: inputkey.name.trim(), origins: inputkey.origins, header: inputkey.header.trim(), storageid: `apikey-${inputkey.name.trim()}`, createdat: Date.now() };
|
|
10335
|
+
await memory.setapikey(entry);
|
|
10336
|
+
await memory.setsecret(entry.storageid, inputkey.value);
|
|
9333
10337
|
const session = await memory.getsession();
|
|
9334
|
-
await audit("call", `Stored the api key
|
|
9335
|
-
return { name:
|
|
10338
|
+
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 } : {} });
|
|
10339
|
+
return { name: entry.name, origins: entry.origins, header: entry.header, createdat: entry.createdat };
|
|
9336
10340
|
}
|
|
9337
10341
|
case "deleteapikey": {
|
|
9338
10342
|
const inputdeletekey = message;
|
|
@@ -9370,6 +10374,54 @@ async function handlerequest(message, sender) {
|
|
|
9370
10374
|
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
10375
|
return { bodyretention: retention };
|
|
9372
10376
|
}
|
|
10377
|
+
case "trafficreport": {
|
|
10378
|
+
const plan = await memory.getplan();
|
|
10379
|
+
if (!plan) throw new Error("No plan is available for a traffic control envelope.");
|
|
10380
|
+
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()) });
|
|
10381
|
+
}
|
|
10382
|
+
case "authreport": {
|
|
10383
|
+
return authreport({ tokens: await memory.listtokens() });
|
|
10384
|
+
}
|
|
10385
|
+
case "revoketokens": {
|
|
10386
|
+
const inputrevoke = message;
|
|
10387
|
+
const tokenids = Array.isArray(inputrevoke.tokenids) ? inputrevoke.tokenids.filter((item) => typeof item === "string" && item.trim().length > 0) : [];
|
|
10388
|
+
if (tokenids.length === 0) throw new Error("Token revocation needs the reviewed token ids.");
|
|
10389
|
+
const reason = inputrevoke.reason?.trim() || "user demand from the review panel";
|
|
10390
|
+
const records = await memory.listtokens();
|
|
10391
|
+
let revoked = 0;
|
|
10392
|
+
for (const tokenid of tokenids) {
|
|
10393
|
+
const stored = records.find((token) => token.id === tokenid);
|
|
10394
|
+
if (!stored || stored.revokedat !== void 0) continue;
|
|
10395
|
+
await memory.setsecret(stored.accessstorageid, "");
|
|
10396
|
+
if (stored.refreshstorageid) await memory.setsecret(stored.refreshstorageid, "");
|
|
10397
|
+
await memory.addtoken({ ...stored, revokedat: Date.now() });
|
|
10398
|
+
revoked += 1;
|
|
10399
|
+
}
|
|
10400
|
+
const session = await memory.getsession();
|
|
10401
|
+
await audit("auth", `Revoked ${revoked} stored token record${revoked === 1 ? "" : "s"} on ${reason}; the token material is dropped from storage.`, { ...session ? { sessionid: session.id } : {} });
|
|
10402
|
+
await refreshbadge();
|
|
10403
|
+
return { revoked, tokenids };
|
|
10404
|
+
}
|
|
10405
|
+
case "revertproxyroute": {
|
|
10406
|
+
const inputrevert = message;
|
|
10407
|
+
const plan = await memory.getplan();
|
|
10408
|
+
if (!plan) throw new Error("No plan is available for a proxy revert.");
|
|
10409
|
+
const ruleset = activerules.get(plan.id);
|
|
10410
|
+
if (!ruleset?.proxy) throw new Error("No active proxy route covers this run.");
|
|
10411
|
+
if (inputrevert.id && ruleset.proxy.id !== inputrevert.id) throw new Error(`No active proxy route matches ${inputrevert.id}.`);
|
|
10412
|
+
const reverted = revertrule(ruleset.proxy, Date.now());
|
|
10413
|
+
ruleset.proxy = reverted;
|
|
10414
|
+
await memory.addproxyroute(reverted);
|
|
10415
|
+
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 });
|
|
10416
|
+
await refreshbadge();
|
|
10417
|
+
return { reverted: true, id: reverted.id };
|
|
10418
|
+
}
|
|
10419
|
+
case "revertcontrols": {
|
|
10420
|
+
const plan = await memory.getplan();
|
|
10421
|
+
if (!plan) throw new Error("No plan is available for a traffic rule revert.");
|
|
10422
|
+
await revertcontrolsforrun(plan.id, "review panel demand");
|
|
10423
|
+
return { reverted: true };
|
|
10424
|
+
}
|
|
9373
10425
|
case "netreport": {
|
|
9374
10426
|
const plan = await memory.getplan();
|
|
9375
10427
|
if (!plan) throw new Error("No plan is available for a network envelope.");
|
|
@@ -9413,10 +10465,19 @@ async function handlerequest(message, sender) {
|
|
|
9413
10465
|
activefetches.delete(id);
|
|
9414
10466
|
}
|
|
9415
10467
|
const stoppedplan = await memory.getplan();
|
|
9416
|
-
if (stoppedplan)
|
|
9417
|
-
|
|
9418
|
-
|
|
9419
|
-
|
|
10468
|
+
if (stoppedplan) {
|
|
10469
|
+
await closechannelsforrun(stoppedplan.id).catch(() => {
|
|
10470
|
+
});
|
|
10471
|
+
await cancelauthflowsforrun(stoppedplan.id, "run cancel").catch(() => {
|
|
10472
|
+
});
|
|
10473
|
+
await revertcontrolsforrun(stoppedplan.id, "run cancel").catch(() => {
|
|
10474
|
+
});
|
|
10475
|
+
} else {
|
|
10476
|
+
await closechannelsforrun("none").catch(() => {
|
|
10477
|
+
});
|
|
10478
|
+
await revertcontrolsforrun("none", "run cancel").catch(() => {
|
|
10479
|
+
});
|
|
10480
|
+
}
|
|
9420
10481
|
for (const [id, active] of [...activerecordings.entries()]) {
|
|
9421
10482
|
const finished = finishrecording(active.record, Date.now());
|
|
9422
10483
|
await memory.addmedia(finished).catch(() => {
|