@wenathlan/extension 1.1.42 → 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 +6 -4
- package/dist/index.d.ts +5 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1451 -14
- package/dist/index.js.map +4 -4
- package/dist/memory.d.ts +76 -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/netwatch.d.ts +92 -0
- package/dist/netwatch.d.ts.map +1 -0
- package/dist/policy.d.ts +31 -0
- package/dist/policy.d.ts.map +1 -1
- package/dist/protocol.d.ts +59 -3
- package/dist/protocol.d.ts.map +1 -1
- package/dist/socketbus.d.ts +134 -0
- package/dist/socketbus.d.ts.map +1 -0
- package/dist/types.d.ts +338 -3
- package/dist/types.d.ts.map +1 -1
- package/dist/version.d.ts +1 -1
- package/extension/dist/background.js +2890 -630
- package/extension/dist/background.js.map +4 -4
- package/extension/dist/manifest.json +1 -1
- package/extension/dist/pagebridge.js +41 -4
- package/extension/dist/pagebridge.js.map +2 -2
- package/extension/dist/popup.html +1 -1
- package/extension/dist/popup.js +20 -3
- package/extension/dist/popup.js.map +2 -2
- package/extension/dist/sidepanel.html +1 -1
- package/extension/dist/sidepanel.js +212 -3
- 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();
|
|
@@ -886,6 +893,155 @@ var sessionmemory = class {
|
|
|
886
893
|
async getsecret(storageid) {
|
|
887
894
|
return this.adapter.get(storageid);
|
|
888
895
|
}
|
|
896
|
+
/** Stores one channel record of a socket or event stream, replacing the previous record of that id. */
|
|
897
|
+
async addchannel(record2) {
|
|
898
|
+
const records = (await this.adapter.get("channels") ?? []).filter((item) => item.id !== record2.id);
|
|
899
|
+
await this.adapter.set("channels", [record2, ...records]);
|
|
900
|
+
}
|
|
901
|
+
/** Returns every stored channel record, newest first. */
|
|
902
|
+
async getchannels() {
|
|
903
|
+
return await this.adapter.get("channels") ?? [];
|
|
904
|
+
}
|
|
905
|
+
/** Returns one channel record by its id. */
|
|
906
|
+
async getchannel(id) {
|
|
907
|
+
return (await this.getchannels()).find((item) => item.id === id);
|
|
908
|
+
}
|
|
909
|
+
/** Queues one message envelope of a channel stream, keeping the arrival order for the waitmessage matchers. */
|
|
910
|
+
async addmessage(envelope) {
|
|
911
|
+
const records = (await this.adapter.get("messages") ?? []).filter((item) => !(item.channelid === envelope.channelid && item.sequence === envelope.sequence));
|
|
912
|
+
await this.adapter.set("messages", [...records, envelope]);
|
|
913
|
+
}
|
|
914
|
+
/** Returns every queued message envelope, oldest first, optionally filtered by channel and stream. */
|
|
915
|
+
async getmessages(channelid, stream) {
|
|
916
|
+
const records = await this.adapter.get("messages") ?? [];
|
|
917
|
+
return records.filter((item) => (channelid === void 0 || item.channelid === channelid) && (stream === void 0 || item.stream === stream));
|
|
918
|
+
}
|
|
919
|
+
/** Drops the matched message envelopes of one channel from the queue once a waitmessage step consumed them. */
|
|
920
|
+
async drainmessages(sequences) {
|
|
921
|
+
const records = await this.adapter.get("messages") ?? [];
|
|
922
|
+
const kept = records.filter((item) => !sequences.some((match) => match.channelid === item.channelid && match.sequence === item.sequence));
|
|
923
|
+
await this.adapter.set("messages", kept);
|
|
924
|
+
}
|
|
925
|
+
/** Stores one observed exchange record, replacing the previous record of that id. */
|
|
926
|
+
async addexchange(record2) {
|
|
927
|
+
const records = (await this.adapter.get("exchanges") ?? []).filter((item) => item.id !== record2.id);
|
|
928
|
+
await this.adapter.set("exchanges", [record2, ...records]);
|
|
929
|
+
}
|
|
930
|
+
/** Returns every stored exchange record, newest first. */
|
|
931
|
+
async getexchanges() {
|
|
932
|
+
return await this.adapter.get("exchanges") ?? [];
|
|
933
|
+
}
|
|
934
|
+
/** Returns one exchange record by its id. */
|
|
935
|
+
async getexchange(id) {
|
|
936
|
+
return (await this.getexchanges()).find((item) => item.id === id);
|
|
937
|
+
}
|
|
938
|
+
/** Returns the exchange records filtered by run, origin and status; the status filter accepts one code or the failed class of every exchange with an error class. */
|
|
939
|
+
async listexchanges(filter) {
|
|
940
|
+
const records = await this.getexchanges();
|
|
941
|
+
return records.filter((item) => (filter.runid === void 0 || item.runid === filter.runid) && (filter.origin === void 0 || item.origin === filter.origin) && (filter.status === void 0 || (filter.status === "failed" ? item.errorclass !== void 0 : item.status === filter.status)));
|
|
942
|
+
}
|
|
943
|
+
/** Stores one captured response body with its mime type and byte size; the user configured body retention window expires the oldest bodies while the exchange metadata always survives. */
|
|
944
|
+
async addbody(record2) {
|
|
945
|
+
const records = await this.getbodies();
|
|
946
|
+
const retention = (await this.getsettings())?.bodyretention;
|
|
947
|
+
const combined = [record2, ...records.filter((item) => item.ref !== record2.ref)];
|
|
948
|
+
const stored = retention === void 0 ? combined : combined.map((item, index) => index < retention ? item : expirebodybytes(item));
|
|
949
|
+
await this.adapter.set("bodies", stored);
|
|
950
|
+
}
|
|
951
|
+
/** Returns every captured body record, newest first. */
|
|
952
|
+
async getbodies() {
|
|
953
|
+
return await this.adapter.get("bodies") ?? [];
|
|
954
|
+
}
|
|
955
|
+
/** Returns one captured body record with its stored text by its reference. */
|
|
956
|
+
async getbody(ref) {
|
|
957
|
+
return (await this.getbodies()).find((item) => item.ref === ref);
|
|
958
|
+
}
|
|
959
|
+
/** Stores the page api map of one origin, replacing the previous map of that origin. */
|
|
960
|
+
async setapimap(origin, entries) {
|
|
961
|
+
const records = (await this.adapter.get("apimap") ?? []).filter((item) => item.origin !== origin);
|
|
962
|
+
await this.adapter.set("apimap", [...entries, ...records]);
|
|
963
|
+
}
|
|
964
|
+
/** Returns every stored page api map entry, newest first. */
|
|
965
|
+
async getapimap() {
|
|
966
|
+
return await this.adapter.get("apimap") ?? [];
|
|
967
|
+
}
|
|
968
|
+
/** Stores one event stream subscription record, replacing the previous record of that id. */
|
|
969
|
+
async setsubscription(record2) {
|
|
970
|
+
const records = (await this.adapter.get("subscriptions") ?? []).filter((item) => item.id !== record2.id);
|
|
971
|
+
await this.adapter.set("subscriptions", [record2, ...records]);
|
|
972
|
+
}
|
|
973
|
+
/** Returns every stored event stream subscription, newest first. */
|
|
974
|
+
async getsubscriptions() {
|
|
975
|
+
return await this.adapter.get("subscriptions") ?? [];
|
|
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
|
+
}
|
|
889
1045
|
};
|
|
890
1046
|
function mediakindof(record2) {
|
|
891
1047
|
if ("pages" in record2) return "pdf";
|
|
@@ -920,207 +1076,1174 @@ function expirecallbody(record2) {
|
|
|
920
1076
|
void body;
|
|
921
1077
|
return { ...metadata, bodyexpired: true };
|
|
922
1078
|
}
|
|
1079
|
+
function expirebodybytes(record2) {
|
|
1080
|
+
const { body, ...metadata } = record2;
|
|
1081
|
+
void body;
|
|
1082
|
+
return { ...metadata, bodyexpired: true };
|
|
1083
|
+
}
|
|
923
1084
|
function randomid() {
|
|
924
1085
|
return crypto.randomUUID();
|
|
925
1086
|
}
|
|
926
1087
|
|
|
927
|
-
//
|
|
928
|
-
var
|
|
929
|
-
|
|
930
|
-
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"]);
|
|
931
|
-
var allowedactions = /* @__PURE__ */ new Set([...sensitiveactions, ...interactionactions, ...readactions]);
|
|
932
|
-
var watchactions = /* @__PURE__ */ new Set(["watchmutate", "watchbanner", "watchfocus", "watchtab"]);
|
|
933
|
-
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"]);
|
|
934
|
-
var valueactions = /* @__PURE__ */ new Set(["presskey", "drag", "drop", "upload", "readattribute", "removeattribute", "waittext", "evaluate", "zoomset", "tabactivate", "tabclose", "tabreload", "windowclose", "windowresize", "tabcreate", "windowcreate", "downloadfile", "typetime", "appendtext", "setvalue", "typeedit", "keyhold", "keyrelease", "chooseradio", "setslider", "setdate", "setcolor", "followlink", "setfragment", "handleauth", "navintent", "openclipboard", "checksafe", "reopentab", "spanav", "duplicatetab", "pintab", "mutetab", "movetab", "movetabwindow", "searchtabs", "badgetab", "attachmeta", "focuswindow", "maximizewindow", "minimizewindow", "restorewindow", "incognitowindow", "asksubmit", "selectchain", "picktypeahead", "pickdate", "attachfile", "fillcode", "consentpassword", "pausedownload", "resumedownload", "verifydownload", "writeclipboard", "quarantinedownload", "scanvirus"]);
|
|
935
|
-
var tabscommandactions = /* @__PURE__ */ new Set(["querytabs", "duplicatetab", "closepattern", "pintab", "mutetab", "movetab", "movetabwindow", "grouptabs", "colorgroup", "collapsegroup", "discardtab", "reloadtabs", "zoomin", "zoomout", "watchtab", "switchtab", "maximizewindow", "minimizewindow", "restorewindow", "focuswindow", "scratchwindow", "incognitowindow", "restoretab", "savelayout", "restorelayout", "findclones", "searchtabs", "badgetab", "attachmeta", "listaudio", "reopenrun", "snapshotsession"]);
|
|
936
|
-
var formactions = /* @__PURE__ */ new Set(["fillform", "filllabel", "fillplaceholder", "detectfields", "generatevalues", "saveprofiles", "asksubmit", "submitform", "readerrors", "retryform", "runwizard", "selectchain", "picktypeahead", "pickdate", "attachfile", "handoffcaptcha", "fillcard", "fillcode", "consentpassword", "skiphoneypot", "detectlogin", "detecttemplate"]);
|
|
937
|
-
var datasetactions = /* @__PURE__ */ new Set(["scrapetable", "exportcsv", "exportjson", "exportexcel", "copytable", "pushsheets", "importcsv", "looprows", "transformvalues", "deduperows", "paginateextract", "mergepages", "stamplerows", "previewgrid", "streamdisk", "resumeextract", "logprovenance"]);
|
|
938
|
-
var exportactions = /* @__PURE__ */ new Set(["exportcsv", "exportjson", "exportexcel", "copytable", "pushsheets", "streamdisk"]);
|
|
939
|
-
var filesactions = /* @__PURE__ */ new Set(["batchdownload", "pausedownload", "resumedownload", "verifydownload", "interceptmime", "exportnetlog", "readclipboard", "writeclipboard", "copyscreen", "quarantinedownload", "scanvirus", "namecaptures", "cleanupartifacts"]);
|
|
940
|
-
var captureactions = /* @__PURE__ */ new Set(["shotview", "shotfullpage", "shotelement", "shotregion", "contactsheet"]);
|
|
941
|
-
var mediaactions = /* @__PURE__ */ new Set(["capturepdf", "recordscreen", "captureaudio", "captureframe", "downloadimages", "shotcanvas", "probestream", "readmedia", "readassets", "timelapse", "convertimage", "makethumbs"]);
|
|
942
|
-
var httpactions = /* @__PURE__ */ new Set(["fetchurl", "parsejson", "parsehtml", "callrest", "callgraphql"]);
|
|
943
|
-
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"]);
|
|
944
|
-
var fieldkinds = ["text", "email", "phone", "date", "number", "select", "check", "radio", "file", "password", "card", "code"];
|
|
945
|
-
var layoutmutationactions = /* @__PURE__ */ new Set(["grouptabs", "colorgroup", "collapsegroup", "savelayout", "restorelayout"]);
|
|
946
|
-
var groupcolors = ["grey", "blue", "red", "yellow", "green", "pink", "purple", "cyan", "orange"];
|
|
947
|
-
function normalizeendpoint(value) {
|
|
948
|
-
const endpoint = new URL(value.trim());
|
|
949
|
-
if (endpoint.protocol !== "https:") throw new Error("Devthink accepts HTTPS endpoints only.");
|
|
950
|
-
if (endpoint.username || endpoint.password) throw new Error("Endpoint credentials are not allowed in the URL.");
|
|
951
|
-
return { endpoint: endpoint.toString(), origin: endpoint.origin, configuredat: Date.now() };
|
|
952
|
-
}
|
|
953
|
-
function hostpattern(origin) {
|
|
954
|
-
const parsed = new URL(origin);
|
|
955
|
-
if (parsed.protocol !== "https:") throw new Error("Only HTTPS origins can be granted.");
|
|
956
|
-
return `${parsed.origin}/*`;
|
|
957
|
-
}
|
|
958
|
-
function actionrisk(kind) {
|
|
959
|
-
if (!allowedactions.has(kind)) throw new Error("Unsupported browser action.");
|
|
960
|
-
if (sensitiveactions.has(kind)) return "sensitive";
|
|
961
|
-
return interactionactions.has(kind) ? "interaction" : "read";
|
|
962
|
-
}
|
|
963
|
-
function parseoptions(step) {
|
|
964
|
-
if (step.options === void 0) return {};
|
|
965
|
-
let parsed;
|
|
1088
|
+
// socketbus.ts
|
|
1089
|
+
var socketkinds = ["opensocket", "sendmessage", "waitmessage", "subscribesse", "longpoll"];
|
|
1090
|
+
function channelorigin(url) {
|
|
966
1091
|
try {
|
|
967
|
-
parsed =
|
|
1092
|
+
const parsed = new URL(url);
|
|
1093
|
+
const protocol = parsed.protocol === "wss:" ? "https:" : parsed.protocol === "ws:" ? "http:" : parsed.protocol;
|
|
1094
|
+
return `${protocol}//${parsed.host}`;
|
|
968
1095
|
} catch {
|
|
969
|
-
|
|
1096
|
+
return "";
|
|
970
1097
|
}
|
|
971
|
-
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("Step options must be a JSON object.");
|
|
972
|
-
return parsed;
|
|
973
|
-
}
|
|
974
|
-
function requiredcapability(kind) {
|
|
975
|
-
if (kind === "tablist") return "tabs";
|
|
976
|
-
if (kind === "downloadfile") return "downloads";
|
|
977
|
-
if (kind === "openclipboard") return "clipboardRead";
|
|
978
|
-
if (kind === "copytable") return "clipboardWrite";
|
|
979
|
-
if (kind === "batchdownload" || kind === "pausedownload" || kind === "resumedownload" || kind === "verifydownload" || kind === "interceptmime" || kind === "quarantinedownload" || kind === "scanvirus") return "downloads";
|
|
980
|
-
if (kind === "readclipboard") return "clipboardRead";
|
|
981
|
-
if (kind === "writeclipboard" || kind === "copyscreen") return "clipboardWrite";
|
|
982
|
-
if (kind === "downloadimages") return "downloads";
|
|
983
|
-
if (kind === "openlink" || kind === "openprivate" || kind === "navlist" || kind === "batchopen" || kind === "reopentab" || kind === "deeplink") return "tabs";
|
|
984
|
-
if (tabscommandactions.has(kind)) return "tabs";
|
|
985
|
-
return void 0;
|
|
986
|
-
}
|
|
987
|
-
function istabscommandkind(kind) {
|
|
988
|
-
return tabscommandactions.has(kind);
|
|
989
|
-
}
|
|
990
|
-
function islayoutkind(kind) {
|
|
991
|
-
return layoutmutationactions.has(kind);
|
|
992
1098
|
}
|
|
993
|
-
function
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
1099
|
+
function channeloptionsof(value) {
|
|
1100
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
|
|
1101
|
+
const entry = value;
|
|
1102
|
+
if (typeof entry.url !== "string" || !entry.url.trim()) return void 0;
|
|
1103
|
+
const options = {};
|
|
1104
|
+
if (Array.isArray(entry.protocols)) options.protocols = entry.protocols.filter((item) => typeof item === "string" && item.trim().length > 0);
|
|
1105
|
+
if (typeof entry.reconnect === "number" && Number.isFinite(entry.reconnect)) options.reconnect = entry.reconnect;
|
|
1106
|
+
if (typeof entry.backoff === "number" && Number.isFinite(entry.backoff)) options.backoff = entry.backoff;
|
|
1107
|
+
if (typeof entry.backoffceiling === "number" && Number.isFinite(entry.backoffceiling)) options.backoffceiling = entry.backoffceiling;
|
|
1108
|
+
if (typeof entry.lifetime === "number" && Number.isFinite(entry.lifetime)) options.lifetime = entry.lifetime;
|
|
1109
|
+
return { url: entry.url.trim(), options };
|
|
1110
|
+
}
|
|
1111
|
+
function newchannel(input) {
|
|
1112
|
+
return { id: input.id, runid: input.runid, stepid: input.stepid, kind: input.kind, url: input.url, origin: channelorigin(input.url), state: "connecting", openedat: input.at, sent: 0, received: 0, reconnects: 0, ...input.protocols !== void 0 && input.protocols.length > 0 ? { protocols: [...input.protocols] } : {} };
|
|
1113
|
+
}
|
|
1114
|
+
function reconnectwaits(attempts, base, ceiling) {
|
|
1115
|
+
const count = Math.max(0, Math.floor(attempts));
|
|
1116
|
+
const waits = [];
|
|
1117
|
+
let wait = Math.max(0, base);
|
|
1118
|
+
for (let index = 0; index < count; index += 1) {
|
|
1119
|
+
waits.push(wait);
|
|
1120
|
+
const next = wait * 2;
|
|
1121
|
+
wait = ceiling !== void 0 && Number.isFinite(ceiling) && ceiling >= 0 ? Math.min(next, ceiling) : next;
|
|
1122
|
+
}
|
|
1123
|
+
return waits;
|
|
1124
|
+
}
|
|
1125
|
+
async function openchannel(input) {
|
|
1126
|
+
const sleep = input.sleep ?? ((milliseconds) => new Promise((resolve) => setTimeout(resolve, Math.max(0, milliseconds))));
|
|
1127
|
+
const now = input.now ?? Date.now;
|
|
1128
|
+
const attempts = Math.max(1, Math.floor(input.options.reconnect ?? 0) + 1);
|
|
1129
|
+
const waits = reconnectwaits(attempts - 1, input.options.backoff ?? 0, input.options.backoffceiling);
|
|
1130
|
+
let record2 = { ...input.record, state: "connecting" };
|
|
1131
|
+
let lasterror = "";
|
|
1132
|
+
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
|
1133
|
+
try {
|
|
1134
|
+
const result = await input.connect(record2.url, record2.protocols ?? []);
|
|
1135
|
+
if (result.open) return { ...record2, state: "open", openedat: now() };
|
|
1136
|
+
lasterror = result.error ?? `closed with code ${result.code ?? 0}`;
|
|
1137
|
+
} catch (error) {
|
|
1138
|
+
lasterror = error instanceof Error ? error.message : String(error);
|
|
1139
|
+
}
|
|
1140
|
+
if (attempt < attempts - 1) {
|
|
1141
|
+
const wait = waits[attempt] ?? 0;
|
|
1142
|
+
if (wait > 0) await sleep(wait);
|
|
1143
|
+
record2 = { ...record2, reconnects: record2.reconnects + 1 };
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
return { ...record2, state: "failed", error: lasterror };
|
|
998
1147
|
}
|
|
999
|
-
function
|
|
1000
|
-
|
|
1148
|
+
function closechannel(record2, at, error) {
|
|
1149
|
+
const state = error !== void 0 ? "failed" : "closed";
|
|
1150
|
+
return { ...record2, state, closedat: at, ...error !== void 0 ? { error } : {} };
|
|
1001
1151
|
}
|
|
1002
|
-
function
|
|
1003
|
-
|
|
1152
|
+
function tagmessage(state, channelid, stream, payload, at) {
|
|
1153
|
+
const sequence = (state.sequences[channelid] ?? 0) + 1;
|
|
1154
|
+
const envelope = { channelid, stream, payload, sequence, at };
|
|
1155
|
+
return { state: { sequences: { ...state.sequences, [channelid]: sequence }, queue: state.queue }, envelope };
|
|
1004
1156
|
}
|
|
1005
|
-
function
|
|
1006
|
-
return
|
|
1157
|
+
function publishmessage(state, channelid, stream, payload, at) {
|
|
1158
|
+
return tagmessage(state, channelid, stream, payload, at);
|
|
1007
1159
|
}
|
|
1008
|
-
function
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
if (session.pausedat) return { allowed: false, reason: "The browser session is paused and cannot capture." };
|
|
1012
|
-
if (session.tabid !== tabid2) return { allowed: false, reason: `The capture needs the active tab grant of session tab ${session.tabid} and refuses tab ${tabid2}.` };
|
|
1013
|
-
if (!origingranted(session, origin)) return { allowed: false, reason: `The capture of ${origin} needs the session origin grants first.` };
|
|
1014
|
-
return { allowed: true };
|
|
1160
|
+
function receivemessage(state, channelid, stream, payload, at) {
|
|
1161
|
+
const tagged = tagmessage(state, channelid, stream, payload, at);
|
|
1162
|
+
return { state: { ...tagged.state, queue: [...state.queue, tagged.envelope] }, envelope: tagged.envelope };
|
|
1015
1163
|
}
|
|
1016
|
-
function
|
|
1017
|
-
if (
|
|
1018
|
-
if (
|
|
1019
|
-
|
|
1020
|
-
if (options.format !== void 0 && options.format !== "png" && options.format !== "jpeg" && options.format !== "webp") return { allowed: false, reason: "The reviewed capture format must be png, jpeg or webp." };
|
|
1021
|
-
if (options.quality !== void 0 && (typeof options.quality !== "number" || !Number.isFinite(options.quality) || options.quality < 0 || options.quality > 100)) return { allowed: false, reason: "The reviewed capture quality must stay between zero and one hundred; any value in that range is the user choice with no code cap." };
|
|
1022
|
-
if (options.pixelratio !== void 0 && (typeof options.pixelratio !== "number" || !Number.isFinite(options.pixelratio) || options.pixelratio < 1)) return { allowed: false, reason: "The reviewed pixel ratio starts at one and climbs to any user configured ceiling with no code ceiling." };
|
|
1023
|
-
if (options.annotate !== void 0 && typeof options.annotate !== "boolean") return { allowed: false, reason: "The reviewed capture annotation flag must be a boolean." };
|
|
1024
|
-
if (options.exporttarget !== void 0 && options.exporttarget !== "memory" && options.exporttarget !== "download" && options.exporttarget !== "clipboard") return { allowed: false, reason: "The reviewed capture export target must be memory, download or clipboard." };
|
|
1025
|
-
return { allowed: true };
|
|
1164
|
+
function pathstep(current, segment) {
|
|
1165
|
+
if (Array.isArray(current) && /^\d+$/.test(segment)) return current[Number.parseInt(segment, 10)];
|
|
1166
|
+
if (current && typeof current === "object" && !Array.isArray(current)) return current[segment];
|
|
1167
|
+
return void 0;
|
|
1026
1168
|
}
|
|
1027
|
-
function
|
|
1028
|
-
if (!
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1169
|
+
function matchmessage(filter, envelope) {
|
|
1170
|
+
if (!filter) return true;
|
|
1171
|
+
if (filter.stream !== void 0 && filter.stream !== envelope.stream) return false;
|
|
1172
|
+
if (filter.path !== void 0) {
|
|
1173
|
+
try {
|
|
1174
|
+
const parsed = JSON.parse(envelope.payload);
|
|
1175
|
+
let current = parsed;
|
|
1176
|
+
let missing = false;
|
|
1177
|
+
for (const segment of filter.path.split(".")) {
|
|
1178
|
+
const next = pathstep(current, segment);
|
|
1179
|
+
if (next === void 0) {
|
|
1180
|
+
missing = true;
|
|
1181
|
+
break;
|
|
1182
|
+
}
|
|
1183
|
+
current = next;
|
|
1184
|
+
}
|
|
1185
|
+
if (missing) return false;
|
|
1186
|
+
} catch {
|
|
1187
|
+
return false;
|
|
1188
|
+
}
|
|
1032
1189
|
}
|
|
1033
|
-
|
|
1034
|
-
if (rect.width <= 0 || rect.height <= 0) return { allowed: false, reason: "The reviewed regionrect needs positive width and height values." };
|
|
1035
|
-
return { allowed: true };
|
|
1190
|
+
return true;
|
|
1036
1191
|
}
|
|
1037
|
-
function
|
|
1038
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) return {
|
|
1039
|
-
const
|
|
1040
|
-
const
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1192
|
+
function messagefilterof(value) {
|
|
1193
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return {};
|
|
1194
|
+
const entry = value;
|
|
1195
|
+
const filter = {};
|
|
1196
|
+
if (typeof entry.stream === "string" && entry.stream.trim()) filter.stream = entry.stream.trim();
|
|
1197
|
+
if (typeof entry.path === "string" && entry.path.trim()) filter.path = entry.path.trim();
|
|
1198
|
+
if (typeof entry.limit === "number" && Number.isFinite(entry.limit) && entry.limit >= 1) filter.limit = Math.floor(entry.limit);
|
|
1199
|
+
return filter;
|
|
1200
|
+
}
|
|
1201
|
+
function parsessetext(text2) {
|
|
1202
|
+
const separator = text2.lastIndexOf("\n\n");
|
|
1203
|
+
const complete = separator === -1 ? "" : text2.slice(0, separator + 2);
|
|
1204
|
+
const rest = separator === -1 ? text2 : text2.slice(separator + 2);
|
|
1205
|
+
const events = [];
|
|
1206
|
+
for (const block of complete.split(/\n\n/)) {
|
|
1207
|
+
const id = [];
|
|
1208
|
+
const names = [];
|
|
1209
|
+
const data = [];
|
|
1210
|
+
let retry;
|
|
1211
|
+
for (const line of block.split("\n")) {
|
|
1212
|
+
if (line === "" || line.startsWith(":")) continue;
|
|
1213
|
+
const colon = line.indexOf(":");
|
|
1214
|
+
const field = colon === -1 ? line : line.slice(0, colon);
|
|
1215
|
+
let value = colon === -1 ? "" : line.slice(colon + 1);
|
|
1216
|
+
if (value.startsWith(" ")) value = value.slice(1);
|
|
1217
|
+
if (field === "id" && value !== "") id.push(value);
|
|
1218
|
+
if (field === "event" && value !== "") names.push(value);
|
|
1219
|
+
if (field === "data") data.push(value);
|
|
1220
|
+
if (field === "retry" && /^\d+$/.test(value)) retry = Number.parseInt(value, 10);
|
|
1221
|
+
}
|
|
1222
|
+
if (id.length === 0 && names.length === 0 && data.length === 0) continue;
|
|
1223
|
+
events.push({ ...id.length > 0 ? { id: id[id.length - 1] } : {}, ...names.length > 0 ? { event: names[names.length - 1] } : {}, data: data.join("\n"), ...retry !== void 0 ? { retry } : {} });
|
|
1224
|
+
}
|
|
1225
|
+
return { events, rest };
|
|
1226
|
+
}
|
|
1227
|
+
function sserequestheaders(record2) {
|
|
1228
|
+
return { accept: "text/event-stream", ...record2.lasteventid !== void 0 && record2.lasteventid !== "" ? { "last-event-id": record2.lasteventid } : {} };
|
|
1229
|
+
}
|
|
1230
|
+
function subscriptionoptionsof(value) {
|
|
1231
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
|
|
1232
|
+
const entry = value;
|
|
1233
|
+
if (typeof entry.url !== "string" || !entry.url.trim()) return void 0;
|
|
1234
|
+
const cancel = entry.cancel;
|
|
1235
|
+
if (!cancel || typeof cancel !== "object" || Array.isArray(cancel)) return void 0;
|
|
1236
|
+
const cancelrecord = cancel;
|
|
1237
|
+
if (cancelrecord.kind !== "stop" && cancelrecord.kind !== "lifetime") return void 0;
|
|
1238
|
+
if (typeof cancelrecord.value !== "string" && typeof cancelrecord.value !== "number") return void 0;
|
|
1239
|
+
const result = { url: entry.url.trim(), cancel: { kind: cancelrecord.kind, value: cancelrecord.value } };
|
|
1240
|
+
if (typeof entry.lifetime === "number" && Number.isFinite(entry.lifetime) && entry.lifetime > 0) result.lifetime = entry.lifetime;
|
|
1241
|
+
if (typeof entry.lasteventid === "string" && entry.lasteventid.trim()) result.lasteventid = entry.lasteventid.trim();
|
|
1242
|
+
return result;
|
|
1243
|
+
}
|
|
1244
|
+
function pollcursorof(value) {
|
|
1245
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
|
|
1246
|
+
const entry = value;
|
|
1247
|
+
if (typeof entry.url !== "string" || !entry.url.trim()) return void 0;
|
|
1248
|
+
if (typeof entry.cursorfield !== "string" || !entry.cursorfield.trim()) return void 0;
|
|
1249
|
+
if (typeof entry.interval !== "number" || !Number.isFinite(entry.interval) || entry.interval <= 0) return void 0;
|
|
1250
|
+
const stop = entry.stop;
|
|
1251
|
+
if (!stop || typeof stop !== "object" || Array.isArray(stop)) return void 0;
|
|
1252
|
+
const stoprecord = stop;
|
|
1253
|
+
if (typeof stoprecord.field !== "string" || !stoprecord.field.trim()) return void 0;
|
|
1254
|
+
if (typeof stoprecord.equals !== "string") return void 0;
|
|
1255
|
+
const cursor = { url: entry.url.trim(), cursorfield: entry.cursorfield.trim(), interval: entry.interval, stop: { field: stoprecord.field.trim(), equals: stoprecord.equals } };
|
|
1256
|
+
if (typeof entry.maxpolls === "number" && Number.isFinite(entry.maxpolls) && entry.maxpolls >= 1) cursor.maxpolls = Math.floor(entry.maxpolls);
|
|
1257
|
+
if (typeof entry.param === "string" && entry.param.trim()) cursor.param = entry.param.trim();
|
|
1258
|
+
return cursor;
|
|
1259
|
+
}
|
|
1260
|
+
function cursorfrom(response, field) {
|
|
1261
|
+
let current = response;
|
|
1262
|
+
for (const segment of field.split(".")) {
|
|
1263
|
+
const next = pathstep(current, segment);
|
|
1264
|
+
if (next === void 0) return void 0;
|
|
1265
|
+
current = next;
|
|
1266
|
+
}
|
|
1267
|
+
return current === void 0 || current === null ? void 0 : String(current);
|
|
1268
|
+
}
|
|
1269
|
+
function pollurl(cursor, value) {
|
|
1270
|
+
if (cursor.param === void 0 || value === void 0) {
|
|
1271
|
+
return { url: cursor.url, ...value !== void 0 ? { body: JSON.stringify({ [cursor.cursorfield]: value }) } : {} };
|
|
1272
|
+
}
|
|
1273
|
+
const url = new URL(cursor.url);
|
|
1274
|
+
url.searchParams.set(cursor.param, value);
|
|
1275
|
+
return { url: url.toString() };
|
|
1276
|
+
}
|
|
1277
|
+
function polldecision(input) {
|
|
1278
|
+
if (input.cancelled?.() === true) return { continue: false, reason: "The long poll loop was cancelled." };
|
|
1279
|
+
if (input.expiresat !== void 0 && input.now >= input.expiresat) return { continue: false, reason: "The long poll loop stopped at the reviewed plan expiry." };
|
|
1280
|
+
const stopvalue = cursorfrom(input.response, input.cursor.stop.field);
|
|
1281
|
+
if (stopvalue !== void 0 && stopvalue === input.cursor.stop.equals) return { continue: false, reason: `The stop condition matched ${input.cursor.stop.field} ${stopvalue}.` };
|
|
1282
|
+
if (input.cursor.maxpolls !== void 0 && input.polls + 1 >= input.cursor.maxpolls) return { continue: false, reason: `The long poll loop reached the reviewed poll ceiling of ${input.cursor.maxpolls}.` };
|
|
1283
|
+
const value = cursorfrom(input.response, input.cursor.cursorfield);
|
|
1284
|
+
const next = pollurl(input.cursor, value);
|
|
1285
|
+
return { continue: true, reason: "The long poll loop continues.", ...value !== void 0 ? { cursor: value } : {}, next: { ...next, wait: input.cursor.interval } };
|
|
1049
1286
|
}
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1287
|
+
|
|
1288
|
+
// httpclient.ts
|
|
1289
|
+
var httpkinds = ["fetchurl", "parsejson", "parsehtml", "callrest", "callgraphql"];
|
|
1290
|
+
var redirectstatuses = /* @__PURE__ */ new Set([301, 302, 303, 307, 308]);
|
|
1291
|
+
var bodilessmethods = /* @__PURE__ */ new Set(["GET", "HEAD"]);
|
|
1292
|
+
function statusclassof(status) {
|
|
1293
|
+
if (status >= 100 && status < 200) return "informational";
|
|
1294
|
+
if (status >= 200 && status < 300) return "success";
|
|
1295
|
+
if (status >= 300 && status < 400) return "redirect";
|
|
1296
|
+
if (status >= 400 && status < 500) return "clienterror";
|
|
1297
|
+
if (status >= 500 && status < 600) return "servererror";
|
|
1298
|
+
return "unknown";
|
|
1055
1299
|
}
|
|
1056
|
-
function
|
|
1057
|
-
return
|
|
1300
|
+
function templateurl(template, values) {
|
|
1301
|
+
return template.replace(/\{([a-z0-9_]+)\}/gi, (whole, name) => values[name] === void 0 ? whole : String(values[name]));
|
|
1058
1302
|
}
|
|
1059
|
-
function
|
|
1060
|
-
|
|
1061
|
-
const
|
|
1062
|
-
if (!
|
|
1063
|
-
|
|
1064
|
-
if (
|
|
1065
|
-
if (options.
|
|
1066
|
-
|
|
1067
|
-
const
|
|
1068
|
-
|
|
1069
|
-
}
|
|
1070
|
-
if (kind === "shotregion") {
|
|
1071
|
-
const rectcheck = validateregionrect(options.regionrect);
|
|
1072
|
-
if (!rectcheck.allowed) return rectcheck;
|
|
1073
|
-
if (options.reviewed !== true) return { allowed: false, reason: "Every reviewed regionrect needs the explicit reviewed flag before shotregion runs." };
|
|
1074
|
-
if (options.container !== void 0 && !isnonempty(options.container)) return { allowed: false, reason: "The reviewed scrollable container selector must be a non-empty string." };
|
|
1075
|
-
if (options.steps !== void 0 && (typeof options.steps !== "number" || !Number.isInteger(options.steps) || options.steps < 1)) return { allowed: false, reason: "The reviewed container scroll steps must be a positive integer with no code ceiling." };
|
|
1076
|
-
}
|
|
1077
|
-
if (kind === "contactsheet") {
|
|
1078
|
-
const elements = options.elements;
|
|
1079
|
-
if (!Array.isArray(elements) || elements.length === 0 || !elements.every((item) => isnonempty(item))) return { allowed: false, reason: "A reviewed non-empty list of element selectors is required in options for the contact sheet; the cell count stays the user choice." };
|
|
1080
|
-
const layout = options.sheet;
|
|
1081
|
-
if (layout !== void 0) {
|
|
1082
|
-
if (!layout || typeof layout !== "object" || Array.isArray(layout)) return { allowed: false, reason: "The reviewed sheetlayout must be an object with cellsize, columns and label." };
|
|
1083
|
-
const sheet = layout;
|
|
1084
|
-
if (typeof sheet.cellsize !== "number" || !Number.isFinite(sheet.cellsize) || sheet.cellsize <= 0) return { allowed: false, reason: "The reviewed contact sheet cell size must be a positive number of pixels." };
|
|
1085
|
-
if (typeof sheet.columns !== "number" || !Number.isInteger(sheet.columns) || sheet.columns < 1) return { allowed: false, reason: "The reviewed contact sheet column count must be a positive integer with no code ceiling." };
|
|
1086
|
-
if (sheet.label !== void 0 && sheet.label !== "none" && sheet.label !== "index" && sheet.label !== "selector" && sheet.label !== "both") return { allowed: false, reason: "The reviewed contact sheet label style must be none, index, selector or both." };
|
|
1303
|
+
function fetchrequestof(value) {
|
|
1304
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
|
|
1305
|
+
const options = value;
|
|
1306
|
+
if (typeof options.url !== "string" || !options.url.trim()) return void 0;
|
|
1307
|
+
const request = { url: options.url.trim() };
|
|
1308
|
+
if (typeof options.method === "string" && options.method.trim()) request.method = options.method.trim().toUpperCase();
|
|
1309
|
+
if (options.headers && typeof options.headers === "object" && !Array.isArray(options.headers)) {
|
|
1310
|
+
const headers = {};
|
|
1311
|
+
for (const [name, headervalue] of Object.entries(options.headers)) {
|
|
1312
|
+
if (typeof headervalue === "string") headers[name] = headervalue;
|
|
1087
1313
|
}
|
|
1314
|
+
request.headers = headers;
|
|
1088
1315
|
}
|
|
1089
|
-
|
|
1316
|
+
if (typeof options.body === "string") request.body = options.body;
|
|
1317
|
+
if (options.mode === "cors" || options.mode === "no-cors" || options.mode === "same-origin") request.mode = options.mode;
|
|
1318
|
+
return request;
|
|
1090
1319
|
}
|
|
1091
|
-
function
|
|
1092
|
-
if (!
|
|
1093
|
-
|
|
1320
|
+
function fetchoptionsof(value) {
|
|
1321
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return {};
|
|
1322
|
+
const options = value;
|
|
1323
|
+
const normalized = {};
|
|
1324
|
+
if (typeof options.timeout === "number" && Number.isFinite(options.timeout)) normalized.timeout = options.timeout;
|
|
1325
|
+
if (typeof options.retries === "number" && Number.isFinite(options.retries)) normalized.retries = options.retries;
|
|
1326
|
+
if (typeof options.backoff === "number" && Number.isFinite(options.backoff)) normalized.backoff = options.backoff;
|
|
1327
|
+
if (typeof options.follow === "number" && Number.isFinite(options.follow)) normalized.follow = options.follow;
|
|
1328
|
+
return normalized;
|
|
1094
1329
|
}
|
|
1095
|
-
function
|
|
1096
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) return {
|
|
1097
|
-
const
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
return { allowed: true };
|
|
1330
|
+
function streamwindowof(value) {
|
|
1331
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return {};
|
|
1332
|
+
const options = value;
|
|
1333
|
+
const window2 = {};
|
|
1334
|
+
if (typeof options.budget === "number" && Number.isFinite(options.budget)) window2.budget = options.budget;
|
|
1335
|
+
return window2;
|
|
1102
1336
|
}
|
|
1103
|
-
function
|
|
1104
|
-
if (!
|
|
1105
|
-
const
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
for (const item of record2.entries) {
|
|
1109
|
-
if (!item || typeof item !== "object" || Array.isArray(item)) return { allowed: false, reason: "Every reviewed form record entry must be an object." };
|
|
1337
|
+
function jsonpathrulesof(value) {
|
|
1338
|
+
if (!Array.isArray(value)) return [];
|
|
1339
|
+
const rules = [];
|
|
1340
|
+
for (const item of value) {
|
|
1341
|
+
if (!item || typeof item !== "object" || Array.isArray(item)) continue;
|
|
1110
1342
|
const entry = item;
|
|
1111
|
-
|
|
1112
|
-
if (!
|
|
1113
|
-
|
|
1114
|
-
if (
|
|
1115
|
-
if (entry.
|
|
1343
|
+
if (typeof entry.name !== "string" || !entry.name.trim()) continue;
|
|
1344
|
+
if (typeof entry.path !== "string" || !entry.path.trim()) continue;
|
|
1345
|
+
const rule = { name: entry.name.trim(), path: entry.path.trim() };
|
|
1346
|
+
if (entry.kind === "text" || entry.kind === "number" || entry.kind === "boolean" || entry.kind === "json") rule.kind = entry.kind;
|
|
1347
|
+
if (entry.default !== void 0) rule.default = entry.default;
|
|
1348
|
+
rules.push(rule);
|
|
1116
1349
|
}
|
|
1117
|
-
return
|
|
1350
|
+
return rules;
|
|
1118
1351
|
}
|
|
1119
|
-
function
|
|
1120
|
-
if (!
|
|
1121
|
-
const
|
|
1122
|
-
|
|
1123
|
-
|
|
1352
|
+
function htmlqueriesof(value) {
|
|
1353
|
+
if (!Array.isArray(value)) return [];
|
|
1354
|
+
const queries = [];
|
|
1355
|
+
for (const item of value) {
|
|
1356
|
+
if (!item || typeof item !== "object" || Array.isArray(item)) continue;
|
|
1357
|
+
const entry = item;
|
|
1358
|
+
if (typeof entry.selector !== "string" || !entry.selector.trim()) continue;
|
|
1359
|
+
const query = { selector: entry.selector.trim() };
|
|
1360
|
+
if (typeof entry.attribute === "string" && entry.attribute.trim()) query.attribute = entry.attribute.trim();
|
|
1361
|
+
if (entry.multi === true) query.multi = true;
|
|
1362
|
+
queries.push(query);
|
|
1363
|
+
}
|
|
1364
|
+
return queries;
|
|
1365
|
+
}
|
|
1366
|
+
function graphqlrequestof(value) {
|
|
1367
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
|
|
1368
|
+
const options = value;
|
|
1369
|
+
if (typeof options.query !== "string" || !options.query.trim()) return void 0;
|
|
1370
|
+
if (options.operationkind !== "query" && options.operationkind !== "mutation") return void 0;
|
|
1371
|
+
const request = { query: options.query, operationkind: options.operationkind };
|
|
1372
|
+
if (options.variables && typeof options.variables === "object" && !Array.isArray(options.variables)) request.variables = options.variables;
|
|
1373
|
+
if (typeof options.operationname === "string" && options.operationname.trim()) request.operationname = options.operationname.trim();
|
|
1374
|
+
return request;
|
|
1375
|
+
}
|
|
1376
|
+
var realsleep = (milliseconds) => new Promise((resolve) => setTimeout(resolve, Math.max(0, milliseconds)));
|
|
1377
|
+
async function sendfetch(input) {
|
|
1378
|
+
const options = input.options ?? {};
|
|
1379
|
+
const sleep = input.sleep ?? realsleep;
|
|
1380
|
+
const now = input.now ?? Date.now;
|
|
1381
|
+
const attempts = Math.max(1, Math.floor(options.retries ?? 0) + 1);
|
|
1382
|
+
const backoff = options.backoff ?? 0;
|
|
1383
|
+
const follow = options.follow ?? Number.POSITIVE_INFINITY;
|
|
1384
|
+
let url = input.request.url;
|
|
1385
|
+
let method = (input.request.method ?? "GET").toUpperCase();
|
|
1386
|
+
let retries = 0;
|
|
1387
|
+
let redirects = 0;
|
|
1388
|
+
let lastreason = "";
|
|
1389
|
+
for (let attempt = 1; attempt <= attempts; attempt += 1) {
|
|
1390
|
+
let hops = 0;
|
|
1391
|
+
const startedat = now();
|
|
1392
|
+
let response;
|
|
1393
|
+
try {
|
|
1394
|
+
const init = { method, headers: { ...input.request.headers ?? {} }, ...input.request.body !== void 0 && !bodilessmethods.has(method) ? { body: input.request.body } : {}, ...input.request.mode !== void 0 ? { mode: input.request.mode } : {}, redirect: follow <= 0 ? "error" : "follow" };
|
|
1395
|
+
const sent = input.transport(url, init);
|
|
1396
|
+
if (options.timeout !== void 0 && Number.isFinite(options.timeout) && options.timeout >= 0) {
|
|
1397
|
+
let timedout = false;
|
|
1398
|
+
response = await Promise.race([sent, sleep(options.timeout).then(() => {
|
|
1399
|
+
timedout = true;
|
|
1400
|
+
return void 0;
|
|
1401
|
+
})]).then((value) => value ?? (timedout ? (() => {
|
|
1402
|
+
throw new Error(`The request timed out after ${options.timeout} milliseconds.`);
|
|
1403
|
+
})() : value));
|
|
1404
|
+
} else {
|
|
1405
|
+
response = await sent;
|
|
1406
|
+
}
|
|
1407
|
+
while (response !== void 0 && redirectstatuses.has(response.status) && typeof response.location === "string" && response.location) {
|
|
1408
|
+
hops += 1;
|
|
1409
|
+
if (hops > follow) throw new Error(`The redirect chain exceeded the reviewed follow limit of ${follow}.`);
|
|
1410
|
+
url = new URL(response.location, url).toString();
|
|
1411
|
+
if (method === "POST" && [301, 302, 303].includes(response.status)) method = "GET";
|
|
1412
|
+
response = await input.transport(url, { ...init, method });
|
|
1413
|
+
}
|
|
1414
|
+
redirects = hops;
|
|
1415
|
+
} catch (error) {
|
|
1416
|
+
lastreason = error instanceof Error ? error.message : String(error);
|
|
1417
|
+
response = void 0;
|
|
1418
|
+
}
|
|
1419
|
+
if (response !== void 0) {
|
|
1420
|
+
const body = response.body;
|
|
1421
|
+
return { url, status: response.status, statusclass: statusclassof(response.status), headernames: Object.keys(response.headers), body, bytes: body.length, duration: now() - startedat, retries, redirects };
|
|
1422
|
+
}
|
|
1423
|
+
if (attempt < attempts) {
|
|
1424
|
+
const wait = backoff * attempt;
|
|
1425
|
+
if (wait > 0) await sleep(wait);
|
|
1426
|
+
input.onretry?.(attempt, wait, lastreason);
|
|
1427
|
+
retries = attempt;
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1430
|
+
throw new Error(`The request failed after ${attempts} attempt${attempts === 1 ? "" : "s"} with ${retries} retr${retries === 1 ? "y" : "ies"}: ${lastreason}`);
|
|
1431
|
+
}
|
|
1432
|
+
async function readstream(input) {
|
|
1433
|
+
const pull = typeof input.chunks === "function" ? input.chunks : /* @__PURE__ */ ((source) => {
|
|
1434
|
+
let index = 0;
|
|
1435
|
+
return async () => source[index++];
|
|
1436
|
+
})(input.chunks);
|
|
1437
|
+
let count = 0;
|
|
1438
|
+
let total = 0;
|
|
1439
|
+
for (; ; ) {
|
|
1440
|
+
if (input.window.abort?.() === true) return { chunks: count, bytes: total, aborted: true, reason: "The reviewed abort flag stopped the stream." };
|
|
1441
|
+
const chunk = await pull();
|
|
1442
|
+
if (chunk === void 0) return { chunks: count, bytes: total, aborted: false };
|
|
1443
|
+
const next = total + chunk.length;
|
|
1444
|
+
if (input.window.budget !== void 0 && next > input.window.budget) return { chunks: count, bytes: total, aborted: true, reason: `The stream aborted at ${next} bytes past the reviewed byte budget of ${input.window.budget}.` };
|
|
1445
|
+
total = next;
|
|
1446
|
+
count += 1;
|
|
1447
|
+
input.window.onchunk?.(chunk, total);
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
function pathstep2(current, segment) {
|
|
1451
|
+
if (Array.isArray(current) && /^\d+$/.test(segment)) return current[Number.parseInt(segment, 10)];
|
|
1452
|
+
if (current && typeof current === "object" && !Array.isArray(current)) return current[segment];
|
|
1453
|
+
return void 0;
|
|
1454
|
+
}
|
|
1455
|
+
function coerce(value, kind, fallback) {
|
|
1456
|
+
if (value === void 0 || value === null) return { value: fallback, missing: true };
|
|
1457
|
+
if (kind === "text") return { value: String(value), missing: false };
|
|
1458
|
+
if (kind === "number") {
|
|
1459
|
+
const numeric = typeof value === "number" ? value : Number(value);
|
|
1460
|
+
return Number.isFinite(numeric) ? { value: numeric, missing: false } : { value: fallback, missing: true };
|
|
1461
|
+
}
|
|
1462
|
+
if (kind === "boolean") return { value: value === true || value === "true", missing: false };
|
|
1463
|
+
return { value, missing: false };
|
|
1464
|
+
}
|
|
1465
|
+
function readpath(parsed, rules) {
|
|
1466
|
+
const fields = [];
|
|
1467
|
+
for (const rule of rules) {
|
|
1468
|
+
const kind = rule.kind ?? "text";
|
|
1469
|
+
let current = parsed;
|
|
1470
|
+
let missing = false;
|
|
1471
|
+
for (const segment of rule.path.split(".")) {
|
|
1472
|
+
const next = pathstep2(current, segment);
|
|
1473
|
+
if (next === void 0) {
|
|
1474
|
+
missing = true;
|
|
1475
|
+
break;
|
|
1476
|
+
}
|
|
1477
|
+
current = next;
|
|
1478
|
+
}
|
|
1479
|
+
if (missing) fields.push({ name: rule.name, path: rule.path, kind, ...rule.default !== void 0 ? { value: rule.default } : {}, missing: true });
|
|
1480
|
+
else {
|
|
1481
|
+
const resolved = coerce(current, kind, rule.default);
|
|
1482
|
+
fields.push({ name: rule.name, path: rule.path, kind, ...resolved.value !== void 0 ? { value: resolved.value } : {}, ...resolved.missing ? { missing: true } : {} });
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
return fields;
|
|
1486
|
+
}
|
|
1487
|
+
function payloadvalid(payload, schema) {
|
|
1488
|
+
if (!schema) return { ok: false, errors: ["The typed endpoint call needs a reviewed payload schema before it runs."] };
|
|
1489
|
+
const errors = [];
|
|
1490
|
+
for (const field of schema.fields) {
|
|
1491
|
+
const value = payload[field.name];
|
|
1492
|
+
if (value === void 0 || value === null) {
|
|
1493
|
+
if (field.required === true) errors.push(`The required field ${field.name} of kind ${field.kind} is missing.`);
|
|
1494
|
+
continue;
|
|
1495
|
+
}
|
|
1496
|
+
if (field.kind === "string" && typeof value !== "string") errors.push(`The field ${field.name} must be a string.`);
|
|
1497
|
+
if (field.kind === "number" && (typeof value !== "number" || !Number.isFinite(value))) errors.push(`The field ${field.name} must be a finite number.`);
|
|
1498
|
+
if (field.kind === "boolean" && typeof value !== "boolean") errors.push(`The field ${field.name} must be a boolean.`);
|
|
1499
|
+
}
|
|
1500
|
+
return { ok: errors.length === 0, errors };
|
|
1501
|
+
}
|
|
1502
|
+
function payloadwithdefaults(payload, schema) {
|
|
1503
|
+
if (!schema) return payload;
|
|
1504
|
+
const merged = { ...payload };
|
|
1505
|
+
for (const field of schema.fields) {
|
|
1506
|
+
if (merged[field.name] === void 0 && field.default !== void 0) merged[field.name] = field.default;
|
|
1507
|
+
}
|
|
1508
|
+
return merged;
|
|
1509
|
+
}
|
|
1510
|
+
function errorsof(body) {
|
|
1511
|
+
try {
|
|
1512
|
+
const parsed = JSON.parse(body);
|
|
1513
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return [body];
|
|
1514
|
+
const record2 = parsed;
|
|
1515
|
+
for (const key of ["errors", "messages", "error", "message"]) {
|
|
1516
|
+
const value = record2[key];
|
|
1517
|
+
if (Array.isArray(value)) return value.map((item) => typeof item === "string" ? item : item && typeof item === "object" && typeof item.message === "string" ? item.message : String(item));
|
|
1518
|
+
if (typeof value === "string") return [value];
|
|
1519
|
+
}
|
|
1520
|
+
return [body];
|
|
1521
|
+
} catch {
|
|
1522
|
+
return [body];
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
async function callrest(input) {
|
|
1526
|
+
const check = payloadvalid(input.payload, input.endpoint.schema);
|
|
1527
|
+
if (!check.ok) throw new Error(check.errors.join(" "));
|
|
1528
|
+
const payload = payloadwithdefaults(input.payload, input.endpoint.schema);
|
|
1529
|
+
const url = templateurl(input.endpoint.url, payload);
|
|
1530
|
+
const method = input.endpoint.method.toUpperCase();
|
|
1531
|
+
const request = { url, method, ...input.endpoint.headers !== void 0 ? { headers: input.endpoint.headers } : {}, ...bodilessmethods.has(method) ? {} : { body: JSON.stringify(payload) } };
|
|
1532
|
+
const transport = await sendfetch({ request, ...input.options !== void 0 ? { options: input.options } : {}, transport: input.transport, ...input.sleep !== void 0 ? { sleep: input.sleep } : {}, ...input.onretry !== void 0 ? { onretry: input.onretry } : {}, ...input.now !== void 0 ? { now: input.now } : {} });
|
|
1533
|
+
const ok = input.success !== void 0 ? input.success.includes(transport.status) : transport.statusclass === "success";
|
|
1534
|
+
return { transport, url, payload, ok, errors: ok ? [] : errorsof(transport.body) };
|
|
1535
|
+
}
|
|
1536
|
+
function graphqlopenvelope(request) {
|
|
1537
|
+
return JSON.stringify({ query: request.query, ...request.variables !== void 0 ? { variables: request.variables } : {}, ...request.operationname !== void 0 ? { operationName: request.operationname } : {} });
|
|
1538
|
+
}
|
|
1539
|
+
function unwrapgraphql(value) {
|
|
1540
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return { errors: ["The graphql response is not a json object."] };
|
|
1541
|
+
const record2 = value;
|
|
1542
|
+
const errors = Array.isArray(record2.errors) ? record2.errors.map((item) => typeof item === "string" ? item : item && typeof item === "object" && typeof item.message === "string" ? item.message : String(item)) : [];
|
|
1543
|
+
return { ...record2.data !== void 0 ? { data: record2.data } : {}, errors };
|
|
1544
|
+
}
|
|
1545
|
+
async function callgraphql(input) {
|
|
1546
|
+
const request = { url: input.endpoint.url, method: "POST", ...input.endpoint.headers !== void 0 ? { headers: input.endpoint.headers } : {}, body: graphqlopenvelope(input.request) };
|
|
1547
|
+
const transport = await sendfetch({ request, ...input.options !== void 0 ? { options: input.options } : {}, transport: input.transport, ...input.sleep !== void 0 ? { sleep: input.sleep } : {}, ...input.onretry !== void 0 ? { onretry: input.onretry } : {}, ...input.now !== void 0 ? { now: input.now } : {} });
|
|
1548
|
+
try {
|
|
1549
|
+
const unwrapped = unwrapgraphql(JSON.parse(transport.body));
|
|
1550
|
+
return { transport, ...unwrapped.data !== void 0 ? { data: unwrapped.data } : {}, errors: unwrapped.errors };
|
|
1551
|
+
} catch {
|
|
1552
|
+
return { transport, errors: [transport.body] };
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1556
|
+
// netwatch.ts
|
|
1557
|
+
var netwatchkinds = ["watchrequests", "readheaders", "capturebodies", "mapapi", "extractapi"];
|
|
1558
|
+
function resourcefacts(entries) {
|
|
1559
|
+
const facts = [];
|
|
1560
|
+
for (const entry of entries) {
|
|
1561
|
+
const url = typeof entry.name === "string" ? entry.name : "";
|
|
1562
|
+
if (!url) continue;
|
|
1563
|
+
const entrytype = typeof entry.entryType === "string" ? entry.entryType : "resource";
|
|
1564
|
+
if (entrytype !== "resource" && entrytype !== "navigation") continue;
|
|
1565
|
+
facts.push({
|
|
1566
|
+
url,
|
|
1567
|
+
initiator: typeof entry.initiatorType === "string" ? entry.initiatorType : "",
|
|
1568
|
+
entrytype,
|
|
1569
|
+
start: typeof entry.startTime === "number" && Number.isFinite(entry.startTime) ? entry.startTime : 0,
|
|
1570
|
+
duration: typeof entry.duration === "number" && Number.isFinite(entry.duration) ? entry.duration : 0,
|
|
1571
|
+
transfer: typeof entry.transferSize === "number" && Number.isFinite(entry.transferSize) ? entry.transferSize : 0,
|
|
1572
|
+
protocol: typeof entry.nextHopProtocol === "string" ? entry.nextHopProtocol : "",
|
|
1573
|
+
...typeof entry.responseStatus === "number" && Number.isInteger(entry.responseStatus) ? { status: entry.responseStatus } : {},
|
|
1574
|
+
...entry.failed === true ? { failed: true } : {}
|
|
1575
|
+
});
|
|
1576
|
+
}
|
|
1577
|
+
return facts;
|
|
1578
|
+
}
|
|
1579
|
+
function failureclass(fact) {
|
|
1580
|
+
if (fact.status !== void 0 && fact.status >= 400) return { errorclass: "httperror", status: fact.status };
|
|
1581
|
+
if (fact.failed === true) return { errorclass: "networkerror", status: 0 };
|
|
1582
|
+
if ((fact.initiator === "fetch" || fact.initiator === "xmlhttprequest") && fact.duration > 0 && fact.transfer === 0 && fact.protocol === "") return { errorclass: "networkerror", status: 0 };
|
|
1583
|
+
return { status: fact.status ?? 0 };
|
|
1584
|
+
}
|
|
1585
|
+
function correlationid(runid, index) {
|
|
1586
|
+
return `${runid}-${index + 1}`;
|
|
1587
|
+
}
|
|
1588
|
+
function newexchange(input) {
|
|
1589
|
+
const verdict = failureclass(input.fact);
|
|
1590
|
+
let origin = "";
|
|
1591
|
+
try {
|
|
1592
|
+
origin = new URL(input.fact.url).origin;
|
|
1593
|
+
} catch {
|
|
1594
|
+
origin = "";
|
|
1595
|
+
}
|
|
1596
|
+
const method = input.fact.initiator === "fetch" || input.fact.initiator === "xmlhttprequest" ? "?" : "GET";
|
|
1597
|
+
const statusclass = verdict.status >= 100 && verdict.status < 600 ? verdict.status >= 200 && verdict.status < 300 ? "success" : verdict.status >= 300 && verdict.status < 400 ? "redirect" : verdict.status >= 400 && verdict.status < 500 ? "clienterror" : verdict.status >= 500 ? "servererror" : "informational" : "unknown";
|
|
1598
|
+
return { id: input.id, runid: input.runid, stepid: input.stepid, correlationid: input.correlationid, url: input.fact.url, origin, method, status: verdict.status, statusclass, ...verdict.errorclass !== void 0 ? { errorclass: verdict.errorclass } : {}, source: "page", ...input.fact.initiator ? { initiator: input.fact.initiator } : {}, timing: Math.round(input.fact.duration), bytes: input.fact.transfer, at: input.at };
|
|
1599
|
+
}
|
|
1600
|
+
function pairexchange(exchange, response) {
|
|
1601
|
+
if (exchange.correlationid !== response.correlationid) throw new Error(`The response ${response.correlationid} does not pair with the exchange ${exchange.correlationid}.`);
|
|
1602
|
+
return { ...exchange, status: response.status, statusclass: response.status >= 200 && response.status < 300 ? "success" : response.status >= 400 && response.status < 500 ? "clienterror" : response.status >= 500 ? "servererror" : response.status >= 300 && response.status < 400 ? "redirect" : "unknown", bytes: response.bytes, ...response.mime !== void 0 ? { mime: response.mime } : {}, ...response.bodyref !== void 0 ? { bodyref: response.bodyref } : {}, ...Object.keys(response.headers).length > 0 ? { responseheaders: response.headers } : {} };
|
|
1603
|
+
}
|
|
1604
|
+
function headerfilterof(value) {
|
|
1605
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return { allow: [], redact: [] };
|
|
1606
|
+
const entry = value;
|
|
1607
|
+
const names = (source) => Array.isArray(source) ? source.filter((name) => typeof name === "string" && name.trim().length > 0).map((name) => name.trim().toLowerCase()) : [];
|
|
1608
|
+
return { allow: names(entry.allow), redact: names(entry.redact) };
|
|
1609
|
+
}
|
|
1610
|
+
function capturedheaders(headers, filter) {
|
|
1611
|
+
const result = {};
|
|
1612
|
+
for (const [name, value] of Object.entries(headers)) {
|
|
1613
|
+
const key = name.trim().toLowerCase();
|
|
1614
|
+
if (filter.allow.length > 0 && !filter.allow.includes(key)) continue;
|
|
1615
|
+
result[key] = filter.redact.includes(key) ? "[redacted]" : value;
|
|
1616
|
+
}
|
|
1617
|
+
return result;
|
|
1618
|
+
}
|
|
1619
|
+
function bodyfilterof(value) {
|
|
1620
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return {};
|
|
1621
|
+
const entry = value;
|
|
1622
|
+
const filter = {};
|
|
1623
|
+
if (typeof entry.urlpattern === "string" && entry.urlpattern.trim()) filter.urlpattern = entry.urlpattern.trim();
|
|
1624
|
+
if (Array.isArray(entry.mimes)) filter.mimes = entry.mimes.filter((mime) => typeof mime === "string" && mime.trim().length > 0).map((mime) => mime.trim().toLowerCase());
|
|
1625
|
+
if (typeof entry.ceiling === "number" && Number.isFinite(entry.ceiling) && entry.ceiling >= 0) filter.ceiling = entry.ceiling;
|
|
1626
|
+
return filter;
|
|
1627
|
+
}
|
|
1628
|
+
function bodymatches(filter, exchange) {
|
|
1629
|
+
if (filter.urlpattern !== void 0 && !exchange.url.includes(filter.urlpattern)) return false;
|
|
1630
|
+
if (filter.mimes !== void 0 && filter.mimes.length > 0) {
|
|
1631
|
+
const mime = ((exchange.mime ?? "").split(";")[0] ?? "").trim().toLowerCase();
|
|
1632
|
+
if (!filter.mimes.includes(mime)) return false;
|
|
1633
|
+
}
|
|
1634
|
+
return true;
|
|
1635
|
+
}
|
|
1636
|
+
var privatemimes = /* @__PURE__ */ new Set(["text/html", "text/plain", "text/xml", "application/xml", "application/json", "text/json", "application/x-www-form-urlencoded", "application/graphql", "multipart/form-data"]);
|
|
1637
|
+
function privatemime(mime) {
|
|
1638
|
+
return privatemimes.has((mime.split(";")[0] ?? "").trim().toLowerCase());
|
|
1639
|
+
}
|
|
1640
|
+
function capturebody(input) {
|
|
1641
|
+
if (!bodymatches(input.filter, input.exchange)) return { refused: `The exchange ${input.exchange.correlationid} does not match the reviewed body filter.` };
|
|
1642
|
+
const ceiling = input.filter.ceiling;
|
|
1643
|
+
const stored = ceiling !== void 0 && input.body.length > ceiling ? input.body.slice(0, ceiling) : input.body;
|
|
1644
|
+
return { record: { ref: input.ref, runid: input.runid, correlationid: input.exchange.correlationid, url: input.exchange.url, mime: input.mime, bytes: stored.length, body: stored, at: input.at }, truncated: stored.length < input.body.length };
|
|
1645
|
+
}
|
|
1646
|
+
function payloadshapeof(body) {
|
|
1647
|
+
if (body === void 0) return [];
|
|
1648
|
+
try {
|
|
1649
|
+
const parsed = JSON.parse(body);
|
|
1650
|
+
const shape = (value) => value && typeof value === "object" && !Array.isArray(value) ? Object.keys(value) : [];
|
|
1651
|
+
if (Array.isArray(parsed)) return parsed.length > 0 ? shape(parsed[0]) : [];
|
|
1652
|
+
return shape(parsed);
|
|
1653
|
+
} catch {
|
|
1654
|
+
return [];
|
|
1655
|
+
}
|
|
1656
|
+
}
|
|
1657
|
+
function isapicandidate(exchange) {
|
|
1658
|
+
if (exchange.initiator === "fetch" || exchange.initiator === "xmlhttprequest") return true;
|
|
1659
|
+
if (exchange.bodyref !== void 0) return true;
|
|
1660
|
+
try {
|
|
1661
|
+
return /\/api\/|\/graphql|\.json($|\?)|\/v\d+\//i.test(new URL(exchange.url).pathname);
|
|
1662
|
+
} catch {
|
|
1663
|
+
return false;
|
|
1664
|
+
}
|
|
1665
|
+
}
|
|
1666
|
+
function apientries(exchanges, bodies) {
|
|
1667
|
+
const bodybyref = new Map(bodies.map((body) => [body.correlationid, body]));
|
|
1668
|
+
const groups = /* @__PURE__ */ new Map();
|
|
1669
|
+
for (const exchange of exchanges) {
|
|
1670
|
+
if (!isapicandidate(exchange)) continue;
|
|
1671
|
+
let endpoint = exchange.url;
|
|
1672
|
+
let origin = exchange.origin;
|
|
1673
|
+
try {
|
|
1674
|
+
const parsed = new URL(exchange.url);
|
|
1675
|
+
endpoint = `${parsed.origin}${parsed.pathname}`;
|
|
1676
|
+
origin = parsed.origin;
|
|
1677
|
+
} catch {
|
|
1678
|
+
}
|
|
1679
|
+
const key = `${exchange.method} ${endpoint}`;
|
|
1680
|
+
const group = groups.get(key) ?? { endpoint, method: exchange.method, origin, mimes: /* @__PURE__ */ new Map(), frequency: 0, json: 0, captured: 0, shapes: /* @__PURE__ */ new Map(), correlationids: [] };
|
|
1681
|
+
group.frequency += 1;
|
|
1682
|
+
group.correlationids.push(exchange.correlationid);
|
|
1683
|
+
const body = exchange.bodyref !== void 0 ? bodybyref.get(exchange.correlationid) : void 0;
|
|
1684
|
+
const mime = body?.mime ?? exchange.mime ?? "";
|
|
1685
|
+
group.mimes.set(mime, (group.mimes.get(mime) ?? 0) + 1);
|
|
1686
|
+
if (body !== void 0) {
|
|
1687
|
+
group.captured += 1;
|
|
1688
|
+
const shape = payloadshapeof(body.body);
|
|
1689
|
+
if (shape.length > 0) group.json += 1;
|
|
1690
|
+
const shapekey = shape.join(",");
|
|
1691
|
+
group.shapes.set(shapekey, (group.shapes.get(shapekey) ?? 0) + 1);
|
|
1692
|
+
}
|
|
1693
|
+
groups.set(key, group);
|
|
1694
|
+
}
|
|
1695
|
+
return [...groups.values()].map((group) => {
|
|
1696
|
+
const mime = [...group.mimes.entries()].sort((left, right) => right[1] - left[1])[0]?.[0] ?? "";
|
|
1697
|
+
const modalshape = [...group.shapes.entries()].sort((left, right) => right[1] - left[1])[0];
|
|
1698
|
+
return { endpoint: group.endpoint, method: group.method, mime, frequency: group.frequency, payloadshape: (modalshape?.[0] ?? "").split(",").filter(Boolean), jsonshare: group.captured > 0 ? group.json / group.captured : 0, stability: group.captured > 0 ? (modalshape?.[1] ?? 0) / group.captured : 0, origin: group.origin, correlationids: group.correlationids };
|
|
1699
|
+
});
|
|
1700
|
+
}
|
|
1701
|
+
function rankapis(entries) {
|
|
1702
|
+
const score = (entry) => entry.frequency * (1 + entry.jsonshare + entry.stability);
|
|
1703
|
+
return [...entries].sort((left, right) => score(right) - score(left) || right.frequency - left.frequency);
|
|
1704
|
+
}
|
|
1705
|
+
function apireplayspecof(value) {
|
|
1706
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
|
|
1707
|
+
const entry = value;
|
|
1708
|
+
if (typeof entry.endpoint !== "string" || !entry.endpoint.trim()) return void 0;
|
|
1709
|
+
const spec = { endpoint: entry.endpoint.trim() };
|
|
1710
|
+
if (typeof entry.verb === "string" && entry.verb.trim()) spec.verb = entry.verb.trim().toUpperCase();
|
|
1711
|
+
if (entry.overrides !== void 0 && entry.overrides !== null && typeof entry.overrides === "object" && !Array.isArray(entry.overrides)) {
|
|
1712
|
+
const overrides = {};
|
|
1713
|
+
for (const [name, override] of Object.entries(entry.overrides)) {
|
|
1714
|
+
if (typeof override === "string") overrides[name] = override;
|
|
1715
|
+
}
|
|
1716
|
+
spec.overrides = overrides;
|
|
1717
|
+
}
|
|
1718
|
+
if (Array.isArray(entry.paths)) spec.paths = entry.paths.filter((path) => typeof path === "string" && path.trim().length > 0);
|
|
1719
|
+
return spec;
|
|
1720
|
+
}
|
|
1721
|
+
function replayurl(spec) {
|
|
1722
|
+
const url = new URL(spec.endpoint);
|
|
1723
|
+
for (const [name, value] of Object.entries(spec.overrides ?? {})) url.searchParams.set(name, value);
|
|
1724
|
+
return url.toString();
|
|
1725
|
+
}
|
|
1726
|
+
function extractvalues(body, paths) {
|
|
1727
|
+
let parsed;
|
|
1728
|
+
try {
|
|
1729
|
+
parsed = JSON.parse(body);
|
|
1730
|
+
} catch {
|
|
1731
|
+
return paths.map((path) => ({ path, missing: true }));
|
|
1732
|
+
}
|
|
1733
|
+
const fields = readpath(parsed, paths.map((path) => ({ name: path, path, kind: "json" })));
|
|
1734
|
+
return fields.map((field) => ({ path: field.path, ...field.value !== void 0 ? { value: field.value } : {}, ...field.missing ? { missing: true } : {} }));
|
|
1735
|
+
}
|
|
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
|
+
|
|
2046
|
+
// policy.ts
|
|
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"]);
|
|
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"]);
|
|
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"]);
|
|
2050
|
+
var allowedactions = /* @__PURE__ */ new Set([...sensitiveactions, ...interactionactions, ...readactions]);
|
|
2051
|
+
var watchactions = /* @__PURE__ */ new Set(["watchmutate", "watchbanner", "watchfocus", "watchtab"]);
|
|
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"]);
|
|
2053
|
+
var valueactions = /* @__PURE__ */ new Set(["presskey", "drag", "drop", "upload", "readattribute", "removeattribute", "waittext", "evaluate", "zoomset", "tabactivate", "tabclose", "tabreload", "windowclose", "windowresize", "tabcreate", "windowcreate", "downloadfile", "typetime", "appendtext", "setvalue", "typeedit", "keyhold", "keyrelease", "chooseradio", "setslider", "setdate", "setcolor", "followlink", "setfragment", "handleauth", "navintent", "openclipboard", "checksafe", "reopentab", "spanav", "duplicatetab", "pintab", "mutetab", "movetab", "movetabwindow", "searchtabs", "badgetab", "attachmeta", "focuswindow", "maximizewindow", "minimizewindow", "restorewindow", "incognitowindow", "asksubmit", "selectchain", "picktypeahead", "pickdate", "attachfile", "fillcode", "consentpassword", "pausedownload", "resumedownload", "verifydownload", "writeclipboard", "quarantinedownload", "scanvirus"]);
|
|
2054
|
+
var tabscommandactions = /* @__PURE__ */ new Set(["querytabs", "duplicatetab", "closepattern", "pintab", "mutetab", "movetab", "movetabwindow", "grouptabs", "colorgroup", "collapsegroup", "discardtab", "reloadtabs", "zoomin", "zoomout", "watchtab", "switchtab", "maximizewindow", "minimizewindow", "restorewindow", "focuswindow", "scratchwindow", "incognitowindow", "restoretab", "savelayout", "restorelayout", "findclones", "searchtabs", "badgetab", "attachmeta", "listaudio", "reopenrun", "snapshotsession"]);
|
|
2055
|
+
var formactions = /* @__PURE__ */ new Set(["fillform", "filllabel", "fillplaceholder", "detectfields", "generatevalues", "saveprofiles", "asksubmit", "submitform", "readerrors", "retryform", "runwizard", "selectchain", "picktypeahead", "pickdate", "attachfile", "handoffcaptcha", "fillcard", "fillcode", "consentpassword", "skiphoneypot", "detectlogin", "detecttemplate"]);
|
|
2056
|
+
var datasetactions = /* @__PURE__ */ new Set(["scrapetable", "exportcsv", "exportjson", "exportexcel", "copytable", "pushsheets", "importcsv", "looprows", "transformvalues", "deduperows", "paginateextract", "mergepages", "stamplerows", "previewgrid", "streamdisk", "resumeextract", "logprovenance"]);
|
|
2057
|
+
var exportactions = /* @__PURE__ */ new Set(["exportcsv", "exportjson", "exportexcel", "copytable", "pushsheets", "streamdisk"]);
|
|
2058
|
+
var filesactions = /* @__PURE__ */ new Set(["batchdownload", "pausedownload", "resumedownload", "verifydownload", "interceptmime", "exportnetlog", "readclipboard", "writeclipboard", "copyscreen", "quarantinedownload", "scanvirus", "namecaptures", "cleanupartifacts"]);
|
|
2059
|
+
var captureactions = /* @__PURE__ */ new Set(["shotview", "shotfullpage", "shotelement", "shotregion", "contactsheet"]);
|
|
2060
|
+
var mediaactions = /* @__PURE__ */ new Set(["capturepdf", "recordscreen", "captureaudio", "captureframe", "downloadimages", "shotcanvas", "probestream", "readmedia", "readassets", "timelapse", "convertimage", "makethumbs"]);
|
|
2061
|
+
var httpactions = /* @__PURE__ */ new Set(["fetchurl", "parsejson", "parsehtml", "callrest", "callgraphql"]);
|
|
2062
|
+
var socketactions = /* @__PURE__ */ new Set(["opensocket", "sendmessage", "waitmessage", "subscribesse", "longpoll"]);
|
|
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"]);
|
|
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"]);
|
|
2066
|
+
var fieldkinds = ["text", "email", "phone", "date", "number", "select", "check", "radio", "file", "password", "card", "code"];
|
|
2067
|
+
var layoutmutationactions = /* @__PURE__ */ new Set(["grouptabs", "colorgroup", "collapsegroup", "savelayout", "restorelayout"]);
|
|
2068
|
+
var groupcolors = ["grey", "blue", "red", "yellow", "green", "pink", "purple", "cyan", "orange"];
|
|
2069
|
+
function normalizeendpoint(value) {
|
|
2070
|
+
const endpoint = new URL(value.trim());
|
|
2071
|
+
if (endpoint.protocol !== "https:") throw new Error("Devthink accepts HTTPS endpoints only.");
|
|
2072
|
+
if (endpoint.username || endpoint.password) throw new Error("Endpoint credentials are not allowed in the URL.");
|
|
2073
|
+
return { endpoint: endpoint.toString(), origin: endpoint.origin, configuredat: Date.now() };
|
|
2074
|
+
}
|
|
2075
|
+
function hostpattern(origin) {
|
|
2076
|
+
const parsed = new URL(origin);
|
|
2077
|
+
if (parsed.protocol !== "https:") throw new Error("Only HTTPS origins can be granted.");
|
|
2078
|
+
return `${parsed.origin}/*`;
|
|
2079
|
+
}
|
|
2080
|
+
function actionrisk(kind) {
|
|
2081
|
+
if (!allowedactions.has(kind)) throw new Error("Unsupported browser action.");
|
|
2082
|
+
if (sensitiveactions.has(kind)) return "sensitive";
|
|
2083
|
+
return interactionactions.has(kind) ? "interaction" : "read";
|
|
2084
|
+
}
|
|
2085
|
+
function parseoptions(step) {
|
|
2086
|
+
if (step.options === void 0) return {};
|
|
2087
|
+
let parsed;
|
|
2088
|
+
try {
|
|
2089
|
+
parsed = JSON.parse(step.options);
|
|
2090
|
+
} catch {
|
|
2091
|
+
throw new Error("Step options must be a JSON object.");
|
|
2092
|
+
}
|
|
2093
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("Step options must be a JSON object.");
|
|
2094
|
+
return parsed;
|
|
2095
|
+
}
|
|
2096
|
+
function requiredcapability(kind) {
|
|
2097
|
+
if (kind === "tablist") return "tabs";
|
|
2098
|
+
if (kind === "downloadfile") return "downloads";
|
|
2099
|
+
if (kind === "openclipboard") return "clipboardRead";
|
|
2100
|
+
if (kind === "copytable") return "clipboardWrite";
|
|
2101
|
+
if (kind === "batchdownload" || kind === "pausedownload" || kind === "resumedownload" || kind === "verifydownload" || kind === "interceptmime" || kind === "quarantinedownload" || kind === "scanvirus") return "downloads";
|
|
2102
|
+
if (kind === "readclipboard") return "clipboardRead";
|
|
2103
|
+
if (kind === "writeclipboard" || kind === "copyscreen") return "clipboardWrite";
|
|
2104
|
+
if (kind === "downloadimages") return "downloads";
|
|
2105
|
+
if (kind === "authflow") return "tabs";
|
|
2106
|
+
if (kind === "openlink" || kind === "openprivate" || kind === "navlist" || kind === "batchopen" || kind === "reopentab" || kind === "deeplink") return "tabs";
|
|
2107
|
+
if (tabscommandactions.has(kind)) return "tabs";
|
|
2108
|
+
return void 0;
|
|
2109
|
+
}
|
|
2110
|
+
function istabscommandkind(kind) {
|
|
2111
|
+
return tabscommandactions.has(kind);
|
|
2112
|
+
}
|
|
2113
|
+
function islayoutkind(kind) {
|
|
2114
|
+
return layoutmutationactions.has(kind);
|
|
2115
|
+
}
|
|
2116
|
+
function isformkind(kind) {
|
|
2117
|
+
return formactions.has(kind);
|
|
2118
|
+
}
|
|
2119
|
+
function isdatasetkind(kind) {
|
|
2120
|
+
return datasetactions.has(kind);
|
|
2121
|
+
}
|
|
2122
|
+
function isexportkind(kind) {
|
|
2123
|
+
return exportactions.has(kind);
|
|
2124
|
+
}
|
|
2125
|
+
function isfileskind(kind) {
|
|
2126
|
+
return filesactions.has(kind);
|
|
2127
|
+
}
|
|
2128
|
+
function iscapturekind(kind) {
|
|
2129
|
+
return captureactions.has(kind);
|
|
2130
|
+
}
|
|
2131
|
+
function capturegate(session, tabid2, origin, now) {
|
|
2132
|
+
if (!session || session.stoppedat) return { allowed: false, reason: "No active browser session exists for the capture." };
|
|
2133
|
+
if (session.expiresat <= now) return { allowed: false, reason: "The browser session has expired and cannot capture." };
|
|
2134
|
+
if (session.pausedat) return { allowed: false, reason: "The browser session is paused and cannot capture." };
|
|
2135
|
+
if (session.tabid !== tabid2) return { allowed: false, reason: `The capture needs the active tab grant of session tab ${session.tabid} and refuses tab ${tabid2}.` };
|
|
2136
|
+
if (!origingranted(session, origin)) return { allowed: false, reason: `The capture of ${origin} needs the session origin grants first.` };
|
|
2137
|
+
return { allowed: true };
|
|
2138
|
+
}
|
|
2139
|
+
function validatecaptureoptions(value) {
|
|
2140
|
+
if (value === void 0) return { allowed: true };
|
|
2141
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return { allowed: false, reason: "The reviewed capture options must be an object in options.capture." };
|
|
2142
|
+
const options = value;
|
|
2143
|
+
if (options.format !== void 0 && options.format !== "png" && options.format !== "jpeg" && options.format !== "webp") return { allowed: false, reason: "The reviewed capture format must be png, jpeg or webp." };
|
|
2144
|
+
if (options.quality !== void 0 && (typeof options.quality !== "number" || !Number.isFinite(options.quality) || options.quality < 0 || options.quality > 100)) return { allowed: false, reason: "The reviewed capture quality must stay between zero and one hundred; any value in that range is the user choice with no code cap." };
|
|
2145
|
+
if (options.pixelratio !== void 0 && (typeof options.pixelratio !== "number" || !Number.isFinite(options.pixelratio) || options.pixelratio < 1)) return { allowed: false, reason: "The reviewed pixel ratio starts at one and climbs to any user configured ceiling with no code ceiling." };
|
|
2146
|
+
if (options.annotate !== void 0 && typeof options.annotate !== "boolean") return { allowed: false, reason: "The reviewed capture annotation flag must be a boolean." };
|
|
2147
|
+
if (options.exporttarget !== void 0 && options.exporttarget !== "memory" && options.exporttarget !== "download" && options.exporttarget !== "clipboard") return { allowed: false, reason: "The reviewed capture export target must be memory, download or clipboard." };
|
|
2148
|
+
return { allowed: true };
|
|
2149
|
+
}
|
|
2150
|
+
function validateregionrect(value) {
|
|
2151
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return { allowed: false, reason: "A reviewed regionrect with x, y, width and height in css pixels is required in options." };
|
|
2152
|
+
const rect = value;
|
|
2153
|
+
for (const field of ["x", "y", "width", "height"]) {
|
|
2154
|
+
if (typeof rect[field] !== "number" || !Number.isFinite(rect[field])) return { allowed: false, reason: `The reviewed regionrect needs a numeric ${field} in css pixels.` };
|
|
2155
|
+
}
|
|
2156
|
+
if (rect.x < 0 || rect.y < 0) return { allowed: false, reason: "The reviewed regionrect refuses negative coordinates." };
|
|
2157
|
+
if (rect.width <= 0 || rect.height <= 0) return { allowed: false, reason: "The reviewed regionrect needs positive width and height values." };
|
|
2158
|
+
return { allowed: true };
|
|
2159
|
+
}
|
|
2160
|
+
function validatecapturenaming(value) {
|
|
2161
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return { allowed: false, reason: "A reviewed capturenaming rule with run, step, sequence and kind flags is required." };
|
|
2162
|
+
const rule = value;
|
|
2163
|
+
const segments = ["run", "step", "sequence", "kind"];
|
|
2164
|
+
for (const key of Object.keys(rule)) {
|
|
2165
|
+
if (!segments.includes(key)) return { allowed: false, reason: `The reviewed capturenaming rule refuses the unknown ${key} segment; only run, step, sequence and kind participate.` };
|
|
2166
|
+
}
|
|
2167
|
+
for (const segment of segments) {
|
|
2168
|
+
if (rule[segment] !== void 0 && typeof rule[segment] !== "boolean") return { allowed: false, reason: `The reviewed capturenaming ${segment} flag must be a boolean.` };
|
|
2169
|
+
}
|
|
2170
|
+
if (!segments.some((segment) => rule[segment] === true)) return { allowed: false, reason: "The reviewed capturenaming rule needs at least one enabled segment of run, step, sequence and kind." };
|
|
2171
|
+
return { allowed: true };
|
|
2172
|
+
}
|
|
2173
|
+
function stitchbudgetallowed(tiles, settle2, wait) {
|
|
2174
|
+
if (tiles <= 0) return { allowed: false, reason: "The stitch budget needs at least one tile." };
|
|
2175
|
+
if (settle2 < 0 || wait < 0) return { allowed: false, reason: "The reviewed settle and wait windows must be zero or positive milliseconds." };
|
|
2176
|
+
if (tiles * settle2 > wait) return { allowed: false, reason: `The stitching scroll budget of ${tiles} tiles at ${settle2} milliseconds exceeds the reviewed wait window of ${wait} milliseconds; review a wider window or a smaller settle.` };
|
|
2177
|
+
return { allowed: true };
|
|
2178
|
+
}
|
|
2179
|
+
function beforeafterwrapallowed(kind) {
|
|
2180
|
+
return allowedactions.has(kind) && !captureactions.has(kind);
|
|
2181
|
+
}
|
|
2182
|
+
function validatecapturegrammar(step, options) {
|
|
2183
|
+
const kind = step.kind;
|
|
2184
|
+
const optioncheck = validatecaptureoptions(options.capture);
|
|
2185
|
+
if (!optioncheck.allowed) return optioncheck;
|
|
2186
|
+
if (options.settle !== void 0 && (typeof options.settle !== "number" || !Number.isFinite(options.settle) || options.settle < 0)) return { allowed: false, reason: "The reviewed capture settle window must be zero or a positive number of milliseconds." };
|
|
2187
|
+
if (options.overlap !== void 0 && (typeof options.overlap !== "number" || !Number.isInteger(options.overlap) || options.overlap < 0)) return { allowed: false, reason: "The reviewed stitch overlap must be zero or a positive number of rows." };
|
|
2188
|
+
if (options.wait !== void 0 && (typeof options.wait !== "number" || !Number.isFinite(options.wait) || options.wait < 0)) return { allowed: false, reason: "The reviewed capture wait window must be zero or a positive number of milliseconds." };
|
|
2189
|
+
if (options.naming !== void 0) {
|
|
2190
|
+
const namingcheck = validatecapturenaming(options.naming);
|
|
2191
|
+
if (!namingcheck.allowed) return namingcheck;
|
|
2192
|
+
}
|
|
2193
|
+
if (kind === "shotregion") {
|
|
2194
|
+
const rectcheck = validateregionrect(options.regionrect);
|
|
2195
|
+
if (!rectcheck.allowed) return rectcheck;
|
|
2196
|
+
if (options.reviewed !== true) return { allowed: false, reason: "Every reviewed regionrect needs the explicit reviewed flag before shotregion runs." };
|
|
2197
|
+
if (options.container !== void 0 && !isnonempty(options.container)) return { allowed: false, reason: "The reviewed scrollable container selector must be a non-empty string." };
|
|
2198
|
+
if (options.steps !== void 0 && (typeof options.steps !== "number" || !Number.isInteger(options.steps) || options.steps < 1)) return { allowed: false, reason: "The reviewed container scroll steps must be a positive integer with no code ceiling." };
|
|
2199
|
+
}
|
|
2200
|
+
if (kind === "contactsheet") {
|
|
2201
|
+
const elements = options.elements;
|
|
2202
|
+
if (!Array.isArray(elements) || elements.length === 0 || !elements.every((item) => isnonempty(item))) return { allowed: false, reason: "A reviewed non-empty list of element selectors is required in options for the contact sheet; the cell count stays the user choice." };
|
|
2203
|
+
const layout = options.sheet;
|
|
2204
|
+
if (layout !== void 0) {
|
|
2205
|
+
if (!layout || typeof layout !== "object" || Array.isArray(layout)) return { allowed: false, reason: "The reviewed sheetlayout must be an object with cellsize, columns and label." };
|
|
2206
|
+
const sheet = layout;
|
|
2207
|
+
if (typeof sheet.cellsize !== "number" || !Number.isFinite(sheet.cellsize) || sheet.cellsize <= 0) return { allowed: false, reason: "The reviewed contact sheet cell size must be a positive number of pixels." };
|
|
2208
|
+
if (typeof sheet.columns !== "number" || !Number.isInteger(sheet.columns) || sheet.columns < 1) return { allowed: false, reason: "The reviewed contact sheet column count must be a positive integer with no code ceiling." };
|
|
2209
|
+
if (sheet.label !== void 0 && sheet.label !== "none" && sheet.label !== "index" && sheet.label !== "selector" && sheet.label !== "both") return { allowed: false, reason: "The reviewed contact sheet label style must be none, index, selector or both." };
|
|
2210
|
+
}
|
|
2211
|
+
}
|
|
2212
|
+
return { allowed: true };
|
|
2213
|
+
}
|
|
2214
|
+
function exportgranted(session, origin) {
|
|
2215
|
+
if (!origingranted(session, origin)) return { allowed: false, reason: `The export of extracted data from ${origin} needs the session origin grants before it leaves local memory.` };
|
|
2216
|
+
return { allowed: true };
|
|
2217
|
+
}
|
|
2218
|
+
function validatefieldmatch(value) {
|
|
2219
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return { allowed: false, reason: "A reviewed field match is required in options." };
|
|
2220
|
+
const match = value;
|
|
2221
|
+
if (match.mode !== "label" && match.mode !== "placeholder" && match.mode !== "arialabel" && match.mode !== "name") return { allowed: false, reason: "The reviewed field match mode must be label, placeholder, arialabel or name." };
|
|
2222
|
+
const key = match.mode === "label" ? "label" : match.mode === "placeholder" ? "placeholder" : match.mode === "arialabel" ? "arialabel" : "name";
|
|
2223
|
+
if (!isnonempty(match[key])) return { allowed: false, reason: `The reviewed ${match.mode} field match needs a non-empty ${key}.` };
|
|
2224
|
+
return { allowed: true };
|
|
2225
|
+
}
|
|
2226
|
+
function validateformrecord(value) {
|
|
2227
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return { allowed: false, reason: "A reviewed form record with entries is required in options." };
|
|
2228
|
+
const record2 = value;
|
|
2229
|
+
if (record2.form !== void 0 && !isnonempty(record2.form)) return { allowed: false, reason: "The reviewed form record form selector must be a non-empty string." };
|
|
2230
|
+
if (!Array.isArray(record2.entries) || record2.entries.length === 0) return { allowed: false, reason: "The reviewed form record needs a non-empty list of entries." };
|
|
2231
|
+
for (const item of record2.entries) {
|
|
2232
|
+
if (!item || typeof item !== "object" || Array.isArray(item)) return { allowed: false, reason: "Every reviewed form record entry must be an object." };
|
|
2233
|
+
const entry = item;
|
|
2234
|
+
const matchcheck = validatefieldmatch(entry.match);
|
|
2235
|
+
if (!matchcheck.allowed) return matchcheck;
|
|
2236
|
+
if (typeof entry.kind !== "string" || !fieldkinds.includes(entry.kind)) return { allowed: false, reason: "Every reviewed form record entry needs a known field kind." };
|
|
2237
|
+
if (typeof entry.value !== "string") return { allowed: false, reason: "Every reviewed form record entry needs a string value." };
|
|
2238
|
+
if (entry.kind === "password") return { allowed: false, reason: "Password entries are refused inside form records; use consentpassword with a reviewed consent ref." };
|
|
2239
|
+
}
|
|
2240
|
+
return { allowed: true };
|
|
2241
|
+
}
|
|
2242
|
+
function validatevaluegen(value) {
|
|
2243
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return { allowed: false, reason: "A reviewed valuegen rule with a field kind is required in options." };
|
|
2244
|
+
const rule = value;
|
|
2245
|
+
if (typeof rule.kind !== "string" || !fieldkinds.includes(rule.kind)) return { allowed: false, reason: "The reviewed valuegen kind must be a known field kind." };
|
|
2246
|
+
if (rule.locale !== void 0 && !isnonempty(rule.locale)) return { allowed: false, reason: "The reviewed valuegen locale must be a non-empty string." };
|
|
1124
2247
|
if (rule.seed !== void 0 && (typeof rule.seed !== "number" || !Number.isFinite(rule.seed))) return { allowed: false, reason: "The reviewed valuegen seed must be a finite number." };
|
|
1125
2248
|
return { allowed: true };
|
|
1126
2249
|
}
|
|
@@ -1673,6 +2796,72 @@ function ismediakind(kind) {
|
|
|
1673
2796
|
function ishttpkind(kind) {
|
|
1674
2797
|
return httpactions.has(kind);
|
|
1675
2798
|
}
|
|
2799
|
+
function issocketkind(kind) {
|
|
2800
|
+
return socketactions.has(kind);
|
|
2801
|
+
}
|
|
2802
|
+
function isnetwatchkind(kind) {
|
|
2803
|
+
return netwatchactions.has(kind);
|
|
2804
|
+
}
|
|
2805
|
+
function iscontrolkind(kind) {
|
|
2806
|
+
return controlactions.has(kind);
|
|
2807
|
+
}
|
|
2808
|
+
function resolvedrisk(step) {
|
|
2809
|
+
if (step.kind === "capturebodies") {
|
|
2810
|
+
let options = {};
|
|
2811
|
+
try {
|
|
2812
|
+
options = parseoptions(step);
|
|
2813
|
+
} catch {
|
|
2814
|
+
options = {};
|
|
2815
|
+
}
|
|
2816
|
+
const body = options.body;
|
|
2817
|
+
const mimes = body && typeof body === "object" && !Array.isArray(body) ? body.mimes : void 0;
|
|
2818
|
+
if (Array.isArray(mimes) && mimes.some((mime) => typeof mime === "string" && privatemime(mime))) return "sensitive";
|
|
2819
|
+
return "interaction";
|
|
2820
|
+
}
|
|
2821
|
+
if (step.kind === "extractapi") {
|
|
2822
|
+
let options = {};
|
|
2823
|
+
try {
|
|
2824
|
+
options = parseoptions(step);
|
|
2825
|
+
} catch {
|
|
2826
|
+
options = {};
|
|
2827
|
+
}
|
|
2828
|
+
const replay = options.replay;
|
|
2829
|
+
const verb = replay && typeof replay === "object" && !Array.isArray(replay) ? replay.verb : void 0;
|
|
2830
|
+
if (typeof verb === "string" && !["GET", "HEAD", "OPTIONS"].includes(verb.trim().toUpperCase())) return "sensitive";
|
|
2831
|
+
return "read";
|
|
2832
|
+
}
|
|
2833
|
+
return actionrisk(step.kind);
|
|
2834
|
+
}
|
|
2835
|
+
function socketgate(session, url) {
|
|
2836
|
+
let parsed;
|
|
2837
|
+
try {
|
|
2838
|
+
parsed = new URL(url);
|
|
2839
|
+
} catch {
|
|
2840
|
+
return { allowed: false, reason: "The channel needs a valid url before it can be reviewed." };
|
|
2841
|
+
}
|
|
2842
|
+
if (parsed.protocol !== "wss:" && parsed.protocol !== "https:") return { allowed: false, reason: "Channels use wss websocket urls or https event stream urls only." };
|
|
2843
|
+
if (parsed.username || parsed.password) return { allowed: false, reason: "Channel credentials are not allowed in the url." };
|
|
2844
|
+
const origin = channelorigin(url);
|
|
2845
|
+
if (!origingranted(session, origin)) return { allowed: false, reason: `The channel to ${origin} stays outside the session origin grants.` };
|
|
2846
|
+
return { allowed: true };
|
|
2847
|
+
}
|
|
2848
|
+
function watchgate(session, settings, now) {
|
|
2849
|
+
if (!session || session.stoppedat) return { allowed: false, reason: "No active browser session exists for the request watch." };
|
|
2850
|
+
if (session.expiresat <= now) return { allowed: false, reason: "The browser session has expired and cannot watch requests." };
|
|
2851
|
+
if (session.pausedat) return { allowed: false, reason: "The browser session is paused and cannot watch requests." };
|
|
2852
|
+
if (settings?.webrequestgrant !== true) return { allowed: false, reason: "Request watching needs the webrequest grant in the review panel first; the observation derives from the page timing buffers and adds no manifest permission." };
|
|
2853
|
+
return { allowed: true };
|
|
2854
|
+
}
|
|
2855
|
+
function observedorigingranted(session, url) {
|
|
2856
|
+
let origin = "";
|
|
2857
|
+
try {
|
|
2858
|
+
origin = new URL(url).origin;
|
|
2859
|
+
} catch {
|
|
2860
|
+
return { allowed: false, reason: "The observed exchange url does not parse for an origin check." };
|
|
2861
|
+
}
|
|
2862
|
+
if (!origingranted(session, origin)) return { allowed: false, reason: `The observed origin ${origin} stays outside the session origin grants; grant it before reading headers, bodies or replays.` };
|
|
2863
|
+
return { allowed: true };
|
|
2864
|
+
}
|
|
1676
2865
|
function origincheck(session, url) {
|
|
1677
2866
|
let parsed;
|
|
1678
2867
|
try {
|
|
@@ -1890,6 +3079,265 @@ function fetchnumeric(options, key) {
|
|
|
1890
3079
|
function isrecordingkind(kind) {
|
|
1891
3080
|
return kind === "recordscreen" || kind === "captureaudio";
|
|
1892
3081
|
}
|
|
3082
|
+
function validatesocketgrammar(step, options) {
|
|
3083
|
+
const kind = step.kind;
|
|
3084
|
+
if (kind === "opensocket") {
|
|
3085
|
+
const channel = channeloptionsof(options.socket);
|
|
3086
|
+
if (!channel) return { allowed: false, reason: "A reviewed socket with a url is required in options.socket." };
|
|
3087
|
+
if (channel.options.reconnect !== void 0 && !Number.isInteger(channel.options.reconnect)) return { allowed: false, reason: "The reviewed socket reconnect budget must be an integer attempt count with no code ceiling." };
|
|
3088
|
+
for (const label of ["backoff", "backoffceiling"]) {
|
|
3089
|
+
const value = channel.options[label];
|
|
3090
|
+
if (value !== void 0 && (typeof value !== "number" || !Number.isFinite(value) || value < 0)) return { allowed: false, reason: `The reviewed socket ${label} must be zero or a positive number of milliseconds with no code ceiling.` };
|
|
3091
|
+
}
|
|
3092
|
+
if (channel.options.lifetime !== void 0 && (typeof channel.options.lifetime !== "number" || !Number.isFinite(channel.options.lifetime) || channel.options.lifetime <= 0)) return { allowed: false, reason: "The reviewed socket lifetime window must be a positive number of milliseconds." };
|
|
3093
|
+
}
|
|
3094
|
+
if (kind === "sendmessage") {
|
|
3095
|
+
const message = options.message;
|
|
3096
|
+
if (!message || typeof message !== "object" || Array.isArray(message)) return { allowed: false, reason: "A reviewed message with a channel, stream and payload is required in options.message." };
|
|
3097
|
+
const envelope = message;
|
|
3098
|
+
if (!isnonempty(envelope.channel)) return { allowed: false, reason: "The reviewed message needs the open channel id in options.message.channel." };
|
|
3099
|
+
if (envelope.stream !== void 0 && !isnonempty(envelope.stream)) return { allowed: false, reason: "The reviewed message stream name must be a non-empty string." };
|
|
3100
|
+
if (typeof envelope.payload !== "string") return { allowed: false, reason: "The reviewed message payload must be a string." };
|
|
3101
|
+
}
|
|
3102
|
+
if (kind === "waitmessage") {
|
|
3103
|
+
if (options.filter !== void 0) {
|
|
3104
|
+
const filter = options.filter;
|
|
3105
|
+
if (!filter || typeof filter !== "object" || Array.isArray(filter)) return { allowed: false, reason: "The reviewed message filter must be an object of stream, path and limit." };
|
|
3106
|
+
const reviewed = filter;
|
|
3107
|
+
if (reviewed.stream !== void 0 && !isnonempty(reviewed.stream)) return { allowed: false, reason: "The reviewed message filter stream name must be a non-empty string." };
|
|
3108
|
+
if (reviewed.path !== void 0 && (typeof reviewed.path !== "string" || !validpath(reviewed.path.trim()))) return { allowed: false, reason: "The reviewed message filter path must be a dotted path of non-empty segments." };
|
|
3109
|
+
if (reviewed.limit !== void 0 && (typeof reviewed.limit !== "number" || !Number.isInteger(reviewed.limit) || reviewed.limit < 1)) return { allowed: false, reason: "The reviewed message match limit must be a positive integer with no code ceiling." };
|
|
3110
|
+
}
|
|
3111
|
+
if (options.wait !== void 0 && (typeof options.wait !== "number" || !Number.isFinite(options.wait) || options.wait < 0)) return { allowed: false, reason: "The reviewed message wait budget must be zero or a positive number of milliseconds." };
|
|
3112
|
+
}
|
|
3113
|
+
if (kind === "subscribesse") {
|
|
3114
|
+
const subscription = subscriptionoptionsof(options.subscription);
|
|
3115
|
+
if (!subscription) return { allowed: false, reason: "A reviewed subscription with an event stream url and a cancellation path is required in options.subscription." };
|
|
3116
|
+
const rawlifetime = options.subscription && typeof options.subscription === "object" && !Array.isArray(options.subscription) ? options.subscription.lifetime : void 0;
|
|
3117
|
+
if (rawlifetime !== void 0 && (typeof rawlifetime !== "number" || !Number.isFinite(rawlifetime) || rawlifetime <= 0)) return { allowed: false, reason: "The reviewed subscription lifetime window must be a positive number of milliseconds." };
|
|
3118
|
+
}
|
|
3119
|
+
if (kind === "longpoll") {
|
|
3120
|
+
const cursor = pollcursorof(options.poll);
|
|
3121
|
+
if (!cursor) return { allowed: false, reason: "A reviewed poll cursor with a url, cursor field, interval and stop condition is required in options.poll." };
|
|
3122
|
+
const wait = options.wait;
|
|
3123
|
+
if (wait !== void 0 && (typeof wait !== "number" || !Number.isFinite(wait) || wait < 0)) return { allowed: false, reason: "The reviewed long poll wait budget must be zero or a positive number of milliseconds." };
|
|
3124
|
+
if (wait !== void 0 && cursor.interval > wait) return { allowed: false, reason: `The long poll interval of ${cursor.interval} milliseconds exceeds the reviewed wait budget of ${wait} milliseconds; review a wider budget or a shorter interval.` };
|
|
3125
|
+
}
|
|
3126
|
+
return { allowed: true };
|
|
3127
|
+
}
|
|
3128
|
+
function validatenetwatchgrammar(step, options) {
|
|
3129
|
+
const kind = step.kind;
|
|
3130
|
+
if (kind === "watchrequests") {
|
|
3131
|
+
if (options.watch !== void 0) {
|
|
3132
|
+
const watch = options.watch;
|
|
3133
|
+
if (!watch || typeof watch !== "object" || Array.isArray(watch)) return { allowed: false, reason: "The reviewed watch window must be an object." };
|
|
3134
|
+
const reviewed = watch;
|
|
3135
|
+
if (reviewed.window !== void 0 && (typeof reviewed.window !== "number" || !Number.isFinite(reviewed.window) || reviewed.window < 0)) return { allowed: false, reason: "The reviewed watch window must be zero or a positive number of milliseconds." };
|
|
3136
|
+
}
|
|
3137
|
+
if (options.limit !== void 0 && (typeof options.limit !== "number" || !Number.isInteger(options.limit) || options.limit < 1)) return { allowed: false, reason: "The reviewed watch match limit must be a positive integer with no code ceiling." };
|
|
3138
|
+
}
|
|
3139
|
+
if (kind === "readheaders") {
|
|
3140
|
+
const headers = options.headers;
|
|
3141
|
+
if (!headers || typeof headers !== "object" || Array.isArray(headers)) return { allowed: false, reason: "A reviewed header filter with a name allowlist and a redaction list is required in options.headers." };
|
|
3142
|
+
const reviewed = headers;
|
|
3143
|
+
if (!Array.isArray(reviewed.allow) || reviewed.allow.length === 0 || !reviewed.allow.every((name) => isnonempty(name))) return { allowed: false, reason: "The reviewed header allowlist must be a non-empty list of header names." };
|
|
3144
|
+
if (!Array.isArray(reviewed.redact) || reviewed.redact.length === 0 || !reviewed.redact.every((name) => isnonempty(name))) return { allowed: false, reason: "Header capture requires a reviewed redaction list before any header value is stored." };
|
|
3145
|
+
}
|
|
3146
|
+
if (kind === "capturebodies") {
|
|
3147
|
+
const body = options.body;
|
|
3148
|
+
if (!body || typeof body !== "object" || Array.isArray(body)) return { allowed: false, reason: "A reviewed body filter with a url pattern, mime list and byte ceiling is required in options.body." };
|
|
3149
|
+
const reviewed = body;
|
|
3150
|
+
if (reviewed.urlpattern !== void 0 && !isnonempty(reviewed.urlpattern)) return { allowed: false, reason: "The reviewed body url pattern must be a non-empty string." };
|
|
3151
|
+
if (reviewed.mimes !== void 0 && (!Array.isArray(reviewed.mimes) || reviewed.mimes.length === 0 || !reviewed.mimes.every((mime) => isnonempty(mime)))) return { allowed: false, reason: "The reviewed body mime list must be a non-empty list of mime types." };
|
|
3152
|
+
if (reviewed.ceiling !== void 0 && (typeof reviewed.ceiling !== "number" || !Number.isFinite(reviewed.ceiling) || reviewed.ceiling < 0)) return { allowed: false, reason: "The reviewed body byte ceiling must be zero or a positive number of bytes with no code ceiling." };
|
|
3153
|
+
}
|
|
3154
|
+
if (kind === "mapapi") {
|
|
3155
|
+
if (options.limit !== void 0 && (typeof options.limit !== "number" || !Number.isInteger(options.limit) || options.limit < 1)) return { allowed: false, reason: "The reviewed mapapi match limit must be a positive integer with no code ceiling." };
|
|
3156
|
+
}
|
|
3157
|
+
if (kind === "extractapi") {
|
|
3158
|
+
const replay = apireplayspecof(options.replay);
|
|
3159
|
+
if (!replay) return { allowed: false, reason: "A reviewed replay spec with an endpoint is required in options.replay." };
|
|
3160
|
+
if (!ishttpsurl(replay.endpoint)) return { allowed: false, reason: "The reviewed replay endpoint must be an HTTPS url." };
|
|
3161
|
+
if (replay.verb !== void 0 && !["GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"].includes(replay.verb)) return { allowed: false, reason: "The reviewed replay verb must be a known HTTP verb." };
|
|
3162
|
+
for (const path of replay.paths ?? []) {
|
|
3163
|
+
if (!validpath(path.trim())) return { allowed: false, reason: `The reviewed replay extraction path ${path} must be a dotted path of non-empty segments.` };
|
|
3164
|
+
}
|
|
3165
|
+
}
|
|
3166
|
+
return { allowed: true };
|
|
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
|
+
}
|
|
3325
|
+
function sockettarget(step) {
|
|
3326
|
+
let options = {};
|
|
3327
|
+
try {
|
|
3328
|
+
options = parseoptions(step);
|
|
3329
|
+
} catch {
|
|
3330
|
+
options = {};
|
|
3331
|
+
}
|
|
3332
|
+
for (const key of ["socket", "subscription", "poll"]) {
|
|
3333
|
+
const value = options[key];
|
|
3334
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
3335
|
+
const url = value.url;
|
|
3336
|
+
if (typeof url === "string" && url.trim()) return url.trim();
|
|
3337
|
+
}
|
|
3338
|
+
}
|
|
3339
|
+
return void 0;
|
|
3340
|
+
}
|
|
1893
3341
|
function mediagate(session, tabid2, origin, now) {
|
|
1894
3342
|
if (!session || session.stoppedat) return { allowed: false, reason: "No active browser session exists for the media capture." };
|
|
1895
3343
|
if (session.expiresat <= now) return { allowed: false, reason: "The browser session has expired and cannot capture media." };
|
|
@@ -2251,6 +3699,18 @@ function validatestep(step, origin) {
|
|
|
2251
3699
|
const httpcheck = validatehttpgrammar(step, options);
|
|
2252
3700
|
if (!httpcheck.allowed) return httpcheck;
|
|
2253
3701
|
}
|
|
3702
|
+
if (issocketkind(step.kind)) {
|
|
3703
|
+
const socketcheck = validatesocketgrammar(step, options);
|
|
3704
|
+
if (!socketcheck.allowed) return socketcheck;
|
|
3705
|
+
}
|
|
3706
|
+
if (isnetwatchkind(step.kind)) {
|
|
3707
|
+
const netwatchcheck = validatenetwatchgrammar(step, options);
|
|
3708
|
+
if (!netwatchcheck.allowed) return netwatchcheck;
|
|
3709
|
+
}
|
|
3710
|
+
if (iscontrolkind(step.kind)) {
|
|
3711
|
+
const controlcheck = validatecontrolgrammar(step, options);
|
|
3712
|
+
if (!controlcheck.allowed) return controlcheck;
|
|
3713
|
+
}
|
|
2254
3714
|
if (step.kind === "tabcreate") {
|
|
2255
3715
|
if (options.background !== void 0 && typeof options.background !== "boolean") return { allowed: false, reason: "The reviewed background flag must be a boolean." };
|
|
2256
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." };
|
|
@@ -2341,6 +3801,79 @@ function canexecute(input) {
|
|
|
2341
3801
|
if (!consentgate.allowed) return consentgate;
|
|
2342
3802
|
}
|
|
2343
3803
|
}
|
|
3804
|
+
if (issocketkind(input.step.kind)) {
|
|
3805
|
+
const channelurl = sockettarget(input.step);
|
|
3806
|
+
if (channelurl !== void 0) {
|
|
3807
|
+
const channelgate = socketgate(input.session, channelurl);
|
|
3808
|
+
if (!channelgate.allowed) return channelgate;
|
|
3809
|
+
}
|
|
3810
|
+
}
|
|
3811
|
+
if (input.step.kind === "watchrequests") {
|
|
3812
|
+
const watchgatecheck = watchgate(input.session, input.settings, now);
|
|
3813
|
+
if (!watchgatecheck.allowed) return watchgatecheck;
|
|
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
|
+
}
|
|
3864
|
+
if (input.step.kind === "extractapi") {
|
|
3865
|
+
let replayoptions = {};
|
|
3866
|
+
try {
|
|
3867
|
+
replayoptions = parseoptions(input.step);
|
|
3868
|
+
} catch {
|
|
3869
|
+
replayoptions = {};
|
|
3870
|
+
}
|
|
3871
|
+
const replay = apireplayspecof(replayoptions.replay);
|
|
3872
|
+
if (replay !== void 0) {
|
|
3873
|
+
const replaygate = origincheck(input.session, replay.endpoint);
|
|
3874
|
+
if (!replaygate.allowed) return replaygate;
|
|
3875
|
+
}
|
|
3876
|
+
}
|
|
2344
3877
|
if (input.step.kind === "openlink" || input.step.kind === "openprivate" || input.step.kind === "batchopen" || input.step.kind === "prefetch" || input.step.kind === "deeplink" || input.step.kind === "reopentab") {
|
|
2345
3878
|
let options = {};
|
|
2346
3879
|
try {
|
|
@@ -2448,38 +3981,68 @@ function downloadshare(completed, total) {
|
|
|
2448
3981
|
}
|
|
2449
3982
|
function recorddownload(progress, planid, stepid, entry, now) {
|
|
2450
3983
|
const base = progress && progress.planid === planid ? progress : emptyprogress(planid, now);
|
|
2451
|
-
const outcome = { stepid, ok: entry.state === "complete", summary: `Download ${entry.index + 1} of ${entry.url} ended in the ${entry.state} state.`, details: { download: entry }, at: now };
|
|
3984
|
+
const outcome = { stepid, ok: entry.state === "complete", summary: `Download ${entry.index + 1} of ${entry.url} ended in the ${entry.state} state.`, details: { download: entry }, at: now };
|
|
3985
|
+
return recordoutcome(base, planid, outcome, now);
|
|
3986
|
+
}
|
|
3987
|
+
function recordcapture(progress, planid, stepid, capture, now) {
|
|
3988
|
+
const base = progress && progress.planid === planid ? progress : emptyprogress(planid, now);
|
|
3989
|
+
const bytes = capture.bytes?.length ?? 0;
|
|
3990
|
+
const outcome = { stepid, ok: true, summary: `Captured a ${capture.format} ${capture.kind} shot of ${capture.width} by ${capture.height} pixels with ${bytes} character${bytes === 1 ? "" : "s"} of image data.`, details: { capture: { id: capture.id, kind: capture.kind, format: capture.format, width: capture.width, height: capture.height, bytes } }, at: now };
|
|
3991
|
+
return recordoutcome(base, planid, outcome, now);
|
|
3992
|
+
}
|
|
3993
|
+
function recordpair(progress, planid, stepid, pair, now) {
|
|
3994
|
+
const base = progress && progress.planid === planid ? progress : emptyprogress(planid, now);
|
|
3995
|
+
const outcome = { stepid, ok: true, summary: `Paired the before shot ${pair.beforeid} with the after shot ${pair.afterid} around the ${pair.actionkind} action.`, details: { shotpair: { id: pair.id, beforeid: pair.beforeid, afterid: pair.afterid, actionkind: pair.actionkind, ...pair.target !== void 0 ? { target: pair.target } : {}, ...pair.domsnapshotid !== void 0 ? { domsnapshotid: pair.domsnapshotid } : {} } }, at: now };
|
|
3996
|
+
return recordoutcome(base, planid, outcome, now);
|
|
3997
|
+
}
|
|
3998
|
+
function recordmedia(progress, planid, stepid, media, now) {
|
|
3999
|
+
const base = progress && progress.planid === planid ? progress : emptyprogress(planid, now);
|
|
4000
|
+
const outcome = { stepid, ok: true, summary: `Captured a ${media.kind} media record of ${media.scope} scope with ${media.bytes} character${media.bytes === 1 ? "" : "s"} of media data.`, details: { media }, at: now };
|
|
4001
|
+
return recordoutcome(base, planid, outcome, now);
|
|
4002
|
+
}
|
|
4003
|
+
function recordcall(progress, planid, stepid, entry, now) {
|
|
4004
|
+
const base = progress && progress.planid === planid ? progress : emptyprogress(planid, now);
|
|
4005
|
+
const outcome = { stepid, ok: entry.statusclass === "success", summary: `Outbound ${entry.method} ${entry.kind} call to ${entry.origin} ended in the ${entry.status} ${entry.statusclass} class after ${entry.retries} retr${entry.retries === 1 ? "y" : "ies"} and ${entry.bytes} byte${entry.bytes === 1 ? "" : "s"}.`, details: { call: entry }, at: now };
|
|
4006
|
+
return recordoutcome(base, planid, outcome, now);
|
|
4007
|
+
}
|
|
4008
|
+
function recordfetchretry(progress, planid, stepid, retry, now) {
|
|
4009
|
+
const base = progress && progress.planid === planid ? progress : emptyprogress(planid, now);
|
|
4010
|
+
const outcome = { stepid, ok: false, summary: `Fetch attempt ${retry.attempt} of ${retry.url} failed (${retry.reason}); retrying after a ${retry.wait} millisecond backoff.`, details: { fetchretry: retry }, at: now };
|
|
4011
|
+
return recordoutcome(base, planid, outcome, now);
|
|
4012
|
+
}
|
|
4013
|
+
function recordchannel(progress, planid, stepid, entry, now) {
|
|
4014
|
+
const base = progress && progress.planid === planid ? progress : emptyprogress(planid, now);
|
|
4015
|
+
const outcome = { stepid, ok: entry.state !== "failed", summary: `The ${entry.kind} channel of ${entry.url} is ${entry.state} after ${entry.sent} sent and ${entry.received} received message${entry.received === 1 ? "" : "s"}.`, details: { channel: entry }, at: now };
|
|
2452
4016
|
return recordoutcome(base, planid, outcome, now);
|
|
2453
4017
|
}
|
|
2454
|
-
function
|
|
4018
|
+
function recordexchange(progress, planid, stepid, entry, now) {
|
|
2455
4019
|
const base = progress && progress.planid === planid ? progress : emptyprogress(planid, now);
|
|
2456
|
-
const
|
|
2457
|
-
const outcome = { stepid, ok: true, summary: `Captured a ${capture.format} ${capture.kind} shot of ${capture.width} by ${capture.height} pixels with ${bytes} character${bytes === 1 ? "" : "s"} of image data.`, details: { capture: { id: capture.id, kind: capture.kind, format: capture.format, width: capture.width, height: capture.height, bytes } }, at: now };
|
|
4020
|
+
const outcome = { stepid, ok: entry.errorclass === void 0 && entry.statusclass === "success", summary: `Observed the ${entry.method} request of ${entry.url} as exchange ${entry.correlationid} in the ${entry.status} ${entry.statusclass} class over ${entry.duration} millisecond${entry.duration === 1 ? "" : "s"}${entry.errorclass !== void 0 ? ` failing with the ${entry.errorclass} class` : ""}.`, details: { exchange: entry }, at: now };
|
|
2458
4021
|
return recordoutcome(base, planid, outcome, now);
|
|
2459
4022
|
}
|
|
2460
|
-
function
|
|
4023
|
+
function recordevent(progress, planid, stepid, entry, now) {
|
|
2461
4024
|
const base = progress && progress.planid === planid ? progress : emptyprogress(planid, now);
|
|
2462
|
-
const outcome = { stepid, ok: true, summary: `
|
|
4025
|
+
const outcome = { stepid, ok: true, summary: `The event stream of ${entry.url} observed ${entry.events} ${entry.name || "message"} event${entry.events === 1 ? "" : "s"}${entry.lasteventid !== void 0 ? ` resuming from ${entry.lasteventid}` : ""}.`, details: { event: entry }, at: now };
|
|
2463
4026
|
return recordoutcome(base, planid, outcome, now);
|
|
2464
4027
|
}
|
|
2465
|
-
function
|
|
4028
|
+
function recordpoll(progress, planid, stepid, entry, now) {
|
|
2466
4029
|
const base = progress && progress.planid === planid ? progress : emptyprogress(planid, now);
|
|
2467
|
-
const outcome = { stepid, ok: true, summary: `
|
|
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 };
|
|
2468
4031
|
return recordoutcome(base, planid, outcome, now);
|
|
2469
4032
|
}
|
|
2470
|
-
function
|
|
4033
|
+
function recordcontrol(progress, planid, stepid, entry, now) {
|
|
2471
4034
|
const base = progress && progress.planid === planid ? progress : emptyprogress(planid, now);
|
|
2472
|
-
const outcome = { stepid, ok:
|
|
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 };
|
|
2473
4036
|
return recordoutcome(base, planid, outcome, now);
|
|
2474
4037
|
}
|
|
2475
|
-
function
|
|
4038
|
+
function recordupload(progress, planid, stepid, entry, now) {
|
|
2476
4039
|
const base = progress && progress.planid === planid ? progress : emptyprogress(planid, now);
|
|
2477
|
-
const outcome = { stepid, ok:
|
|
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 };
|
|
2478
4041
|
return recordoutcome(base, planid, outcome, now);
|
|
2479
4042
|
}
|
|
2480
4043
|
|
|
2481
4044
|
// version.ts
|
|
2482
|
-
var packageversion = "1.1.
|
|
4045
|
+
var packageversion = "1.1.44";
|
|
2483
4046
|
|
|
2484
4047
|
// types.ts
|
|
2485
4048
|
var protocolversion = packageversion;
|
|
@@ -2500,6 +4063,10 @@ function parseproposal(value, origin, grants) {
|
|
|
2500
4063
|
const planinput = record(root.plan);
|
|
2501
4064
|
const stepsinput = planinput.steps;
|
|
2502
4065
|
if (!Array.isArray(stepsinput) || stepsinput.length === 0) throw new Error("A plan needs at least one step.");
|
|
4066
|
+
const createdat = Date.now();
|
|
4067
|
+
const expiresat = typeof planinput.expiresat === "number" ? planinput.expiresat : createdat + 10 * 60 * 1e3;
|
|
4068
|
+
if (expiresat <= createdat) throw new Error("Plan expiry must be in the future.");
|
|
4069
|
+
const planwindow = expiresat - createdat;
|
|
2503
4070
|
const steps = stepsinput.map((input, index) => {
|
|
2504
4071
|
const candidate = record(input);
|
|
2505
4072
|
const kind = text(candidate.kind, `step ${index + 1} kind`);
|
|
@@ -2507,11 +4074,32 @@ function parseproposal(value, origin, grants) {
|
|
|
2507
4074
|
id: typeof candidate.id === "string" ? candidate.id : crypto.randomUUID(),
|
|
2508
4075
|
kind,
|
|
2509
4076
|
summary: text(candidate.summary, `step ${index + 1} summary`),
|
|
2510
|
-
risk:
|
|
4077
|
+
risk: resolvedrisk(stepof(kind, candidate, index)),
|
|
2511
4078
|
...typeof candidate.target === "string" ? { target: candidate.target } : {},
|
|
2512
4079
|
...typeof candidate.value === "string" ? { value: candidate.value } : {},
|
|
2513
4080
|
...typeof candidate.options === "string" ? { options: candidate.options } : {}
|
|
2514
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
|
+
}
|
|
2515
4103
|
const evaluation = validatestep(step, origin);
|
|
2516
4104
|
if (!evaluation.allowed) throw new Error(evaluation.reason);
|
|
2517
4105
|
const target = outboundtarget(step);
|
|
@@ -2525,6 +4113,29 @@ function parseproposal(value, origin, grants) {
|
|
|
2525
4113
|
});
|
|
2526
4114
|
if (!granted) throw new Error(`The fetch request to ${target} targets an origin outside the grants.`);
|
|
2527
4115
|
}
|
|
4116
|
+
const channelurl = sockettarget(step);
|
|
4117
|
+
if (channelurl !== void 0) {
|
|
4118
|
+
const channeloriginvalue = channeloriginof(channelurl);
|
|
4119
|
+
const granted = covered.some((pattern) => {
|
|
4120
|
+
try {
|
|
4121
|
+
return new URL(channelurl).origin === new URL(pattern).origin || channeloriginvalue === new URL(pattern).origin;
|
|
4122
|
+
} catch {
|
|
4123
|
+
return false;
|
|
4124
|
+
}
|
|
4125
|
+
});
|
|
4126
|
+
if (!granted) throw new Error(`The channel to ${channelurl} targets an origin outside the grants.`);
|
|
4127
|
+
}
|
|
4128
|
+
let lifetime;
|
|
4129
|
+
try {
|
|
4130
|
+
const options = parseoptions(step);
|
|
4131
|
+
for (const key of ["socket", "subscription"]) {
|
|
4132
|
+
const value2 = options[key];
|
|
4133
|
+
if (value2 && typeof value2 === "object" && !Array.isArray(value2) && typeof value2.lifetime === "number") lifetime = value2.lifetime;
|
|
4134
|
+
}
|
|
4135
|
+
} catch {
|
|
4136
|
+
lifetime = void 0;
|
|
4137
|
+
}
|
|
4138
|
+
if (lifetime !== void 0 && lifetime > planwindow) throw new Error(`The channel lifetime of ${lifetime} milliseconds exceeds the reviewed plan window of ${planwindow} milliseconds.`);
|
|
2528
4139
|
return step;
|
|
2529
4140
|
});
|
|
2530
4141
|
for (const step of steps) {
|
|
@@ -2537,8 +4148,6 @@ function parseproposal(value, origin, grants) {
|
|
|
2537
4148
|
const review = submitreviewgranted(steps, step.id);
|
|
2538
4149
|
if (!review.allowed) throw new Error(review.reason);
|
|
2539
4150
|
}
|
|
2540
|
-
const createdat = Date.now();
|
|
2541
|
-
const expiresat = typeof planinput.expiresat === "number" ? planinput.expiresat : createdat + 10 * 60 * 1e3;
|
|
2542
4151
|
const plan = {
|
|
2543
4152
|
id: typeof planinput.id === "string" ? planinput.id : crypto.randomUUID(),
|
|
2544
4153
|
objective: text(planinput.objective, "objective"),
|
|
@@ -2548,14 +4157,24 @@ function parseproposal(value, origin, grants) {
|
|
|
2548
4157
|
expiresat,
|
|
2549
4158
|
state: "pending"
|
|
2550
4159
|
};
|
|
2551
|
-
if (plan.expiresat <= createdat) throw new Error("Plan expiry must be in the future.");
|
|
2552
4160
|
return { version: protocolversion, plan };
|
|
2553
4161
|
}
|
|
4162
|
+
function stepof(kind, candidate, index) {
|
|
4163
|
+
return { id: typeof candidate.id === "string" ? candidate.id : `candidate${index + 1}`, kind, summary: typeof candidate.summary === "string" ? candidate.summary : "", risk: "read", ...typeof candidate.options === "string" ? { options: candidate.options } : {} };
|
|
4164
|
+
}
|
|
4165
|
+
function channeloriginof(url) {
|
|
4166
|
+
try {
|
|
4167
|
+
const parsed = new URL(url);
|
|
4168
|
+
return `${parsed.protocol === "wss:" ? "https:" : parsed.protocol}//${parsed.host}`;
|
|
4169
|
+
} catch {
|
|
4170
|
+
return "";
|
|
4171
|
+
}
|
|
4172
|
+
}
|
|
2554
4173
|
function requestbody(input) {
|
|
2555
4174
|
return JSON.stringify({ version: protocolversion, objective: input.objective, session: input.session, observation: input.observation, capabilities: input.capabilities });
|
|
2556
4175
|
}
|
|
2557
4176
|
function outcomeresponse(input) {
|
|
2558
|
-
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 } : {} });
|
|
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 } : {} });
|
|
2559
4178
|
}
|
|
2560
4179
|
function mapresponse(input) {
|
|
2561
4180
|
return JSON.stringify({ version: protocolversion, planid: input.plan.id, planstate: input.plan.state, map: input.map });
|
|
@@ -2631,6 +4250,26 @@ function callsreport(input) {
|
|
|
2631
4250
|
});
|
|
2632
4251
|
return { version: protocolversion, calls };
|
|
2633
4252
|
}
|
|
4253
|
+
function exchangesreport(input) {
|
|
4254
|
+
return { version: protocolversion, exchanges: input.exchanges, channels: input.channels, subscriptions: input.subscriptions, apimap: input.apimap };
|
|
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
|
+
}
|
|
2634
4273
|
|
|
2635
4274
|
// capture.ts
|
|
2636
4275
|
var capturekinds = ["shotview", "shotfullpage", "shotelement", "shotregion", "contactsheet"];
|
|
@@ -2941,451 +4580,183 @@ endstream`);
|
|
|
2941
4580
|
let document2 = "%PDF-1.4\n";
|
|
2942
4581
|
const offsets = [];
|
|
2943
4582
|
for (let index = 0; index < objects.length; index += 1) {
|
|
2944
|
-
offsets.push(document2.length);
|
|
2945
|
-
document2 += `${index + 1} 0 obj
|
|
2946
|
-
${objects[index]}
|
|
2947
|
-
endobj
|
|
2948
|
-
`;
|
|
2949
|
-
}
|
|
2950
|
-
const xrefstart = document2.length;
|
|
2951
|
-
document2 += `xref
|
|
2952
|
-
0 ${objects.length + 1}
|
|
2953
|
-
0000000000 65535 f
|
|
2954
|
-
`;
|
|
2955
|
-
for (const offset of offsets) document2 += `${String(offset).padStart(10, "0")} 00000 n
|
|
2956
|
-
`;
|
|
2957
|
-
document2 += `trailer
|
|
2958
|
-
<< /Size ${objects.length + 1} /Root 1 0 R >>
|
|
2959
|
-
startxref
|
|
2960
|
-
${xrefstart}
|
|
2961
|
-
%%EOF
|
|
2962
|
-
`;
|
|
2963
|
-
return { document: document2, bytes: document2.length, pages: laidout.length, pagewidth: Math.round(size.width), pageheight: Math.round(size.height) };
|
|
2964
|
-
}
|
|
2965
|
-
function recordingoptionsof(value) {
|
|
2966
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) return {};
|
|
2967
|
-
const options = value;
|
|
2968
|
-
const normalized = {};
|
|
2969
|
-
if (options.scope === "tab" || options.scope === "run") normalized.scope = options.scope;
|
|
2970
|
-
if (typeof options.fps === "number" && Number.isFinite(options.fps)) normalized.fps = options.fps;
|
|
2971
|
-
if (typeof options.bitrate === "number" && Number.isFinite(options.bitrate)) normalized.bitrate = options.bitrate;
|
|
2972
|
-
if (typeof options.audio === "boolean") normalized.audio = options.audio;
|
|
2973
|
-
return normalized;
|
|
2974
|
-
}
|
|
2975
|
-
function newrecording(input) {
|
|
2976
|
-
return {
|
|
2977
|
-
id: input.id,
|
|
2978
|
-
runid: input.runid,
|
|
2979
|
-
stepid: input.stepid,
|
|
2980
|
-
tabid: input.tabid,
|
|
2981
|
-
kind: input.kind,
|
|
2982
|
-
scope: input.options.scope ?? "tab",
|
|
2983
|
-
format: input.kind === "audio" ? "evidence" : "frames",
|
|
2984
|
-
startedat: input.at,
|
|
2985
|
-
at: input.at,
|
|
2986
|
-
...input.options.fps !== void 0 ? { fps: input.options.fps } : {},
|
|
2987
|
-
...input.options.bitrate !== void 0 ? { bitrate: input.options.bitrate } : {},
|
|
2988
|
-
...input.options.audio !== void 0 ? { audio: input.options.audio } : {},
|
|
2989
|
-
frames: []
|
|
2990
|
-
};
|
|
2991
|
-
}
|
|
2992
|
-
function finishrecording(record2, endat) {
|
|
2993
|
-
return { ...record2, endedat: endat, duration: Math.max(0, endat - record2.startedat) };
|
|
2994
|
-
}
|
|
2995
|
-
function frameinterval(fps) {
|
|
2996
|
-
if (!Number.isFinite(fps) || fps <= 0) return 1e3;
|
|
2997
|
-
return Math.max(1, Math.round(1e3 / fps));
|
|
2998
|
-
}
|
|
2999
|
-
function imagefilterof(value) {
|
|
3000
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) return {};
|
|
3001
|
-
const options = value;
|
|
3002
|
-
const normalized = {};
|
|
3003
|
-
if (typeof options.selector === "string" && options.selector.trim()) normalized.selector = options.selector.trim();
|
|
3004
|
-
if (typeof options.minwidth === "number" && Number.isFinite(options.minwidth)) normalized.minwidth = options.minwidth;
|
|
3005
|
-
if (typeof options.minheight === "number" && Number.isFinite(options.minheight)) normalized.minheight = options.minheight;
|
|
3006
|
-
if (Array.isArray(options.formats) && options.formats.every((item) => typeof item === "string" && item.trim())) normalized.formats = options.formats;
|
|
3007
|
-
return normalized;
|
|
3008
|
-
}
|
|
3009
|
-
function imagematches(image, filter) {
|
|
3010
|
-
if (filter.minwidth !== void 0 && image.width < filter.minwidth) return false;
|
|
3011
|
-
if (filter.minheight !== void 0 && image.height < filter.minheight) return false;
|
|
3012
|
-
if (filter.formats !== void 0 && filter.formats.length > 0) {
|
|
3013
|
-
const mime = image.mime.toLowerCase();
|
|
3014
|
-
const matches = filter.formats.some((format) => {
|
|
3015
|
-
const wanted = format.toLowerCase().trim();
|
|
3016
|
-
return mime === wanted || mime === `image/${wanted}` || mime.endsWith(`/${wanted}`);
|
|
3017
|
-
});
|
|
3018
|
-
if (!matches) return false;
|
|
3019
|
-
}
|
|
3020
|
-
return true;
|
|
3021
|
-
}
|
|
3022
|
-
function dedupeimages(images) {
|
|
3023
|
-
const seen = /* @__PURE__ */ new Set();
|
|
3024
|
-
const unique = [];
|
|
3025
|
-
for (const image of images) {
|
|
3026
|
-
if (seen.has(image.url)) continue;
|
|
3027
|
-
seen.add(image.url);
|
|
3028
|
-
unique.push(image);
|
|
3029
|
-
}
|
|
3030
|
-
return unique;
|
|
3031
|
-
}
|
|
3032
|
-
function imagenames(rule, run, step, count, extension) {
|
|
3033
|
-
const names = [];
|
|
3034
|
-
for (let index = 1; index <= Math.max(0, Math.round(count)); index += 1) names.push(buildname(rule, { run, step, sequence: index, kind: "image" }, extension));
|
|
3035
|
-
return names;
|
|
3036
|
-
}
|
|
3037
|
-
function lapseplanof(value) {
|
|
3038
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
|
|
3039
|
-
const options = value;
|
|
3040
|
-
if (typeof options.interval !== "number" || !Number.isFinite(options.interval)) return void 0;
|
|
3041
|
-
if (typeof options.duration !== "number" || !Number.isFinite(options.duration)) return void 0;
|
|
3042
|
-
const format = options.format === "jpeg" || options.format === "webp" ? options.format : "png";
|
|
3043
|
-
return { interval: options.interval, duration: options.duration, format };
|
|
3044
|
-
}
|
|
3045
|
-
function lapseframes(plan) {
|
|
3046
|
-
if (!(plan.interval > 0) || !(plan.duration > 0)) return [];
|
|
3047
|
-
const frames = [];
|
|
3048
|
-
for (let time = 0; time < plan.duration; time += plan.interval) frames.push(Math.round(time));
|
|
3049
|
-
return frames;
|
|
3050
|
-
}
|
|
3051
|
-
function convertdirectiveof(value) {
|
|
3052
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
|
|
3053
|
-
const options = value;
|
|
3054
|
-
if (options.target !== "png" && options.target !== "jpeg" && options.target !== "webp") return void 0;
|
|
3055
|
-
const normalized = { target: options.target };
|
|
3056
|
-
if (options.source === "png" || options.source === "jpeg" || options.source === "webp") normalized.source = options.source;
|
|
3057
|
-
if (typeof options.quality === "number" && Number.isFinite(options.quality)) normalized.quality = options.quality;
|
|
3058
|
-
return normalized;
|
|
3059
|
-
}
|
|
3060
|
-
function thumbdirectiveof(value) {
|
|
3061
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
|
|
3062
|
-
const options = value;
|
|
3063
|
-
if (typeof options.size !== "number" || !Number.isFinite(options.size) || options.size <= 0) return void 0;
|
|
3064
|
-
if (options.fit !== "cover" && options.fit !== "contain") return void 0;
|
|
3065
|
-
if (typeof options.suffix !== "string" || !options.suffix.trim()) return void 0;
|
|
3066
|
-
return { size: options.size, fit: options.fit, suffix: options.suffix.trim() };
|
|
3067
|
-
}
|
|
3068
|
-
function thumbgeometry(source, directive) {
|
|
3069
|
-
const size = Math.max(1, Math.round(directive.size));
|
|
3070
|
-
if (directive.fit === "contain") {
|
|
3071
|
-
const scale2 = Math.min(size / Math.max(1, source.width), size / Math.max(1, source.height));
|
|
3072
|
-
const dw = Math.max(1, Math.round(source.width * scale2));
|
|
3073
|
-
const dh = Math.max(1, Math.round(source.height * scale2));
|
|
3074
|
-
return { sx: 0, sy: 0, sw: source.width, sh: source.height, dx: Math.floor((size - dw) / 2), dy: Math.floor((size - dh) / 2), dw, dh, width: size, height: size };
|
|
3075
|
-
}
|
|
3076
|
-
const scale = Math.max(size / Math.max(1, source.width), size / Math.max(1, source.height));
|
|
3077
|
-
const sw = Math.min(source.width, Math.round(size / scale));
|
|
3078
|
-
const sh = Math.min(source.height, Math.round(size / scale));
|
|
3079
|
-
return { sx: Math.floor((source.width - sw) / 2), sy: Math.floor((source.height - sh) / 2), sw, sh, dx: 0, dy: 0, dw: size, dh: size, width: size, height: size };
|
|
3080
|
-
}
|
|
3081
|
-
function mediaentries(raw) {
|
|
3082
|
-
return raw.map((entry) => ({
|
|
3083
|
-
url: typeof entry.url === "string" ? entry.url : "",
|
|
3084
|
-
mime: typeof entry.mime === "string" ? entry.mime : "",
|
|
3085
|
-
duration: typeof entry.duration === "number" && Number.isFinite(entry.duration) ? entry.duration : 0,
|
|
3086
|
-
width: typeof entry.width === "number" && Number.isFinite(entry.width) ? Math.round(entry.width) : 0,
|
|
3087
|
-
height: typeof entry.height === "number" && Number.isFinite(entry.height) ? Math.round(entry.height) : 0,
|
|
3088
|
-
codecs: typeof entry.codecs === "string" ? entry.codecs : "",
|
|
3089
|
-
tracks: Array.isArray(entry.tracks) ? entry.tracks.filter((item) => typeof item === "string") : []
|
|
3090
|
-
}));
|
|
3091
|
-
}
|
|
3092
|
-
function assetentries(raw) {
|
|
3093
|
-
return raw.map((entry) => ({
|
|
3094
|
-
kind: entry.kind === "logo" ? "logo" : "favicon",
|
|
3095
|
-
url: typeof entry.url === "string" ? entry.url : "",
|
|
3096
|
-
bytes: typeof entry.bytes === "number" && Number.isFinite(entry.bytes) ? entry.bytes : 0,
|
|
3097
|
-
...typeof entry.sizes === "string" && entry.sizes.trim() ? { sizes: entry.sizes.trim() } : {}
|
|
3098
|
-
}));
|
|
3099
|
-
}
|
|
3100
|
-
function streamsummaries(raw) {
|
|
3101
|
-
return raw.map((entry) => {
|
|
3102
|
-
const tracks = Array.isArray(entry.tracks) ? entry.tracks : [];
|
|
3103
|
-
return {
|
|
3104
|
-
kind: typeof entry.kind === "string" ? entry.kind : "stream",
|
|
3105
|
-
tracks: tracks.length,
|
|
3106
|
-
label: typeof entry.label === "string" ? entry.label : "",
|
|
3107
|
-
live: entry.live === true,
|
|
3108
|
-
detail: tracks.map((track) => {
|
|
3109
|
-
const item = track;
|
|
3110
|
-
return {
|
|
3111
|
-
kind: typeof item.kind === "string" ? item.kind : "",
|
|
3112
|
-
label: typeof item.label === "string" ? item.label : "",
|
|
3113
|
-
...typeof item.width === "number" && Number.isFinite(item.width) ? { width: Math.round(item.width) } : {},
|
|
3114
|
-
...typeof item.height === "number" && Number.isFinite(item.height) ? { height: Math.round(item.height) } : {},
|
|
3115
|
-
...typeof item.framerate === "number" && Number.isFinite(item.framerate) ? { framerate: item.framerate } : {},
|
|
3116
|
-
state: typeof item.state === "string" ? item.state : ""
|
|
3117
|
-
};
|
|
3118
|
-
})
|
|
3119
|
-
};
|
|
3120
|
-
});
|
|
3121
|
-
}
|
|
3122
|
-
|
|
3123
|
-
// httpclient.ts
|
|
3124
|
-
var httpkinds = ["fetchurl", "parsejson", "parsehtml", "callrest", "callgraphql"];
|
|
3125
|
-
var redirectstatuses = /* @__PURE__ */ new Set([301, 302, 303, 307, 308]);
|
|
3126
|
-
var bodilessmethods = /* @__PURE__ */ new Set(["GET", "HEAD"]);
|
|
3127
|
-
function statusclassof(status) {
|
|
3128
|
-
if (status >= 100 && status < 200) return "informational";
|
|
3129
|
-
if (status >= 200 && status < 300) return "success";
|
|
3130
|
-
if (status >= 300 && status < 400) return "redirect";
|
|
3131
|
-
if (status >= 400 && status < 500) return "clienterror";
|
|
3132
|
-
if (status >= 500 && status < 600) return "servererror";
|
|
3133
|
-
return "unknown";
|
|
3134
|
-
}
|
|
3135
|
-
function templateurl(template, values) {
|
|
3136
|
-
return template.replace(/\{([a-z0-9_]+)\}/gi, (whole, name) => values[name] === void 0 ? whole : String(values[name]));
|
|
3137
|
-
}
|
|
3138
|
-
function fetchrequestof(value) {
|
|
3139
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
|
|
3140
|
-
const options = value;
|
|
3141
|
-
if (typeof options.url !== "string" || !options.url.trim()) return void 0;
|
|
3142
|
-
const request = { url: options.url.trim() };
|
|
3143
|
-
if (typeof options.method === "string" && options.method.trim()) request.method = options.method.trim().toUpperCase();
|
|
3144
|
-
if (options.headers && typeof options.headers === "object" && !Array.isArray(options.headers)) {
|
|
3145
|
-
const headers = {};
|
|
3146
|
-
for (const [name, headervalue] of Object.entries(options.headers)) {
|
|
3147
|
-
if (typeof headervalue === "string") headers[name] = headervalue;
|
|
3148
|
-
}
|
|
3149
|
-
request.headers = headers;
|
|
4583
|
+
offsets.push(document2.length);
|
|
4584
|
+
document2 += `${index + 1} 0 obj
|
|
4585
|
+
${objects[index]}
|
|
4586
|
+
endobj
|
|
4587
|
+
`;
|
|
3150
4588
|
}
|
|
3151
|
-
|
|
3152
|
-
|
|
3153
|
-
|
|
4589
|
+
const xrefstart = document2.length;
|
|
4590
|
+
document2 += `xref
|
|
4591
|
+
0 ${objects.length + 1}
|
|
4592
|
+
0000000000 65535 f
|
|
4593
|
+
`;
|
|
4594
|
+
for (const offset of offsets) document2 += `${String(offset).padStart(10, "0")} 00000 n
|
|
4595
|
+
`;
|
|
4596
|
+
document2 += `trailer
|
|
4597
|
+
<< /Size ${objects.length + 1} /Root 1 0 R >>
|
|
4598
|
+
startxref
|
|
4599
|
+
${xrefstart}
|
|
4600
|
+
%%EOF
|
|
4601
|
+
`;
|
|
4602
|
+
return { document: document2, bytes: document2.length, pages: laidout.length, pagewidth: Math.round(size.width), pageheight: Math.round(size.height) };
|
|
3154
4603
|
}
|
|
3155
|
-
function
|
|
4604
|
+
function recordingoptionsof(value) {
|
|
3156
4605
|
if (!value || typeof value !== "object" || Array.isArray(value)) return {};
|
|
3157
4606
|
const options = value;
|
|
3158
4607
|
const normalized = {};
|
|
3159
|
-
if (
|
|
3160
|
-
if (typeof options.
|
|
3161
|
-
if (typeof options.
|
|
3162
|
-
if (typeof options.
|
|
4608
|
+
if (options.scope === "tab" || options.scope === "run") normalized.scope = options.scope;
|
|
4609
|
+
if (typeof options.fps === "number" && Number.isFinite(options.fps)) normalized.fps = options.fps;
|
|
4610
|
+
if (typeof options.bitrate === "number" && Number.isFinite(options.bitrate)) normalized.bitrate = options.bitrate;
|
|
4611
|
+
if (typeof options.audio === "boolean") normalized.audio = options.audio;
|
|
3163
4612
|
return normalized;
|
|
3164
4613
|
}
|
|
3165
|
-
function
|
|
3166
|
-
|
|
3167
|
-
|
|
3168
|
-
|
|
3169
|
-
|
|
3170
|
-
|
|
4614
|
+
function newrecording(input) {
|
|
4615
|
+
return {
|
|
4616
|
+
id: input.id,
|
|
4617
|
+
runid: input.runid,
|
|
4618
|
+
stepid: input.stepid,
|
|
4619
|
+
tabid: input.tabid,
|
|
4620
|
+
kind: input.kind,
|
|
4621
|
+
scope: input.options.scope ?? "tab",
|
|
4622
|
+
format: input.kind === "audio" ? "evidence" : "frames",
|
|
4623
|
+
startedat: input.at,
|
|
4624
|
+
at: input.at,
|
|
4625
|
+
...input.options.fps !== void 0 ? { fps: input.options.fps } : {},
|
|
4626
|
+
...input.options.bitrate !== void 0 ? { bitrate: input.options.bitrate } : {},
|
|
4627
|
+
...input.options.audio !== void 0 ? { audio: input.options.audio } : {},
|
|
4628
|
+
frames: []
|
|
4629
|
+
};
|
|
3171
4630
|
}
|
|
3172
|
-
function
|
|
3173
|
-
|
|
3174
|
-
const rules = [];
|
|
3175
|
-
for (const item of value) {
|
|
3176
|
-
if (!item || typeof item !== "object" || Array.isArray(item)) continue;
|
|
3177
|
-
const entry = item;
|
|
3178
|
-
if (typeof entry.name !== "string" || !entry.name.trim()) continue;
|
|
3179
|
-
if (typeof entry.path !== "string" || !entry.path.trim()) continue;
|
|
3180
|
-
const rule = { name: entry.name.trim(), path: entry.path.trim() };
|
|
3181
|
-
if (entry.kind === "text" || entry.kind === "number" || entry.kind === "boolean" || entry.kind === "json") rule.kind = entry.kind;
|
|
3182
|
-
if (entry.default !== void 0) rule.default = entry.default;
|
|
3183
|
-
rules.push(rule);
|
|
3184
|
-
}
|
|
3185
|
-
return rules;
|
|
4631
|
+
function finishrecording(record2, endat) {
|
|
4632
|
+
return { ...record2, endedat: endat, duration: Math.max(0, endat - record2.startedat) };
|
|
3186
4633
|
}
|
|
3187
|
-
function
|
|
3188
|
-
if (!
|
|
3189
|
-
|
|
3190
|
-
for (const item of value) {
|
|
3191
|
-
if (!item || typeof item !== "object" || Array.isArray(item)) continue;
|
|
3192
|
-
const entry = item;
|
|
3193
|
-
if (typeof entry.selector !== "string" || !entry.selector.trim()) continue;
|
|
3194
|
-
const query = { selector: entry.selector.trim() };
|
|
3195
|
-
if (typeof entry.attribute === "string" && entry.attribute.trim()) query.attribute = entry.attribute.trim();
|
|
3196
|
-
if (entry.multi === true) query.multi = true;
|
|
3197
|
-
queries.push(query);
|
|
3198
|
-
}
|
|
3199
|
-
return queries;
|
|
4634
|
+
function frameinterval(fps) {
|
|
4635
|
+
if (!Number.isFinite(fps) || fps <= 0) return 1e3;
|
|
4636
|
+
return Math.max(1, Math.round(1e3 / fps));
|
|
3200
4637
|
}
|
|
3201
|
-
function
|
|
3202
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) return
|
|
4638
|
+
function imagefilterof(value) {
|
|
4639
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return {};
|
|
3203
4640
|
const options = value;
|
|
3204
|
-
|
|
3205
|
-
if (options.
|
|
3206
|
-
|
|
3207
|
-
if (
|
|
3208
|
-
if (
|
|
3209
|
-
return
|
|
4641
|
+
const normalized = {};
|
|
4642
|
+
if (typeof options.selector === "string" && options.selector.trim()) normalized.selector = options.selector.trim();
|
|
4643
|
+
if (typeof options.minwidth === "number" && Number.isFinite(options.minwidth)) normalized.minwidth = options.minwidth;
|
|
4644
|
+
if (typeof options.minheight === "number" && Number.isFinite(options.minheight)) normalized.minheight = options.minheight;
|
|
4645
|
+
if (Array.isArray(options.formats) && options.formats.every((item) => typeof item === "string" && item.trim())) normalized.formats = options.formats;
|
|
4646
|
+
return normalized;
|
|
3210
4647
|
}
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
let retries = 0;
|
|
3222
|
-
let redirects = 0;
|
|
3223
|
-
let lastreason = "";
|
|
3224
|
-
for (let attempt = 1; attempt <= attempts; attempt += 1) {
|
|
3225
|
-
let hops = 0;
|
|
3226
|
-
const startedat = now();
|
|
3227
|
-
let response;
|
|
3228
|
-
try {
|
|
3229
|
-
const init = { method, headers: { ...input.request.headers ?? {} }, ...input.request.body !== void 0 && !bodilessmethods.has(method) ? { body: input.request.body } : {}, ...input.request.mode !== void 0 ? { mode: input.request.mode } : {}, redirect: follow <= 0 ? "error" : "follow" };
|
|
3230
|
-
const sent = input.transport(url, init);
|
|
3231
|
-
if (options.timeout !== void 0 && Number.isFinite(options.timeout) && options.timeout >= 0) {
|
|
3232
|
-
let timedout = false;
|
|
3233
|
-
response = await Promise.race([sent, sleep(options.timeout).then(() => {
|
|
3234
|
-
timedout = true;
|
|
3235
|
-
return void 0;
|
|
3236
|
-
})]).then((value) => value ?? (timedout ? (() => {
|
|
3237
|
-
throw new Error(`The request timed out after ${options.timeout} milliseconds.`);
|
|
3238
|
-
})() : value));
|
|
3239
|
-
} else {
|
|
3240
|
-
response = await sent;
|
|
3241
|
-
}
|
|
3242
|
-
while (response !== void 0 && redirectstatuses.has(response.status) && typeof response.location === "string" && response.location) {
|
|
3243
|
-
hops += 1;
|
|
3244
|
-
if (hops > follow) throw new Error(`The redirect chain exceeded the reviewed follow limit of ${follow}.`);
|
|
3245
|
-
url = new URL(response.location, url).toString();
|
|
3246
|
-
if (method === "POST" && [301, 302, 303].includes(response.status)) method = "GET";
|
|
3247
|
-
response = await input.transport(url, { ...init, method });
|
|
3248
|
-
}
|
|
3249
|
-
redirects = hops;
|
|
3250
|
-
} catch (error) {
|
|
3251
|
-
lastreason = error instanceof Error ? error.message : String(error);
|
|
3252
|
-
response = void 0;
|
|
3253
|
-
}
|
|
3254
|
-
if (response !== void 0) {
|
|
3255
|
-
const body = response.body;
|
|
3256
|
-
return { url, status: response.status, statusclass: statusclassof(response.status), headernames: Object.keys(response.headers), body, bytes: body.length, duration: now() - startedat, retries, redirects };
|
|
3257
|
-
}
|
|
3258
|
-
if (attempt < attempts) {
|
|
3259
|
-
const wait = backoff * attempt;
|
|
3260
|
-
if (wait > 0) await sleep(wait);
|
|
3261
|
-
input.onretry?.(attempt, wait, lastreason);
|
|
3262
|
-
retries = attempt;
|
|
3263
|
-
}
|
|
4648
|
+
function imagematches(image, filter) {
|
|
4649
|
+
if (filter.minwidth !== void 0 && image.width < filter.minwidth) return false;
|
|
4650
|
+
if (filter.minheight !== void 0 && image.height < filter.minheight) return false;
|
|
4651
|
+
if (filter.formats !== void 0 && filter.formats.length > 0) {
|
|
4652
|
+
const mime = image.mime.toLowerCase();
|
|
4653
|
+
const matches = filter.formats.some((format) => {
|
|
4654
|
+
const wanted = format.toLowerCase().trim();
|
|
4655
|
+
return mime === wanted || mime === `image/${wanted}` || mime.endsWith(`/${wanted}`);
|
|
4656
|
+
});
|
|
4657
|
+
if (!matches) return false;
|
|
3264
4658
|
}
|
|
3265
|
-
|
|
4659
|
+
return true;
|
|
3266
4660
|
}
|
|
3267
|
-
|
|
3268
|
-
const
|
|
3269
|
-
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
|
|
3273
|
-
|
|
3274
|
-
for (; ; ) {
|
|
3275
|
-
if (input.window.abort?.() === true) return { chunks: count, bytes: total, aborted: true, reason: "The reviewed abort flag stopped the stream." };
|
|
3276
|
-
const chunk = await pull();
|
|
3277
|
-
if (chunk === void 0) return { chunks: count, bytes: total, aborted: false };
|
|
3278
|
-
const next = total + chunk.length;
|
|
3279
|
-
if (input.window.budget !== void 0 && next > input.window.budget) return { chunks: count, bytes: total, aborted: true, reason: `The stream aborted at ${next} bytes past the reviewed byte budget of ${input.window.budget}.` };
|
|
3280
|
-
total = next;
|
|
3281
|
-
count += 1;
|
|
3282
|
-
input.window.onchunk?.(chunk, total);
|
|
4661
|
+
function dedupeimages(images) {
|
|
4662
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4663
|
+
const unique = [];
|
|
4664
|
+
for (const image of images) {
|
|
4665
|
+
if (seen.has(image.url)) continue;
|
|
4666
|
+
seen.add(image.url);
|
|
4667
|
+
unique.push(image);
|
|
3283
4668
|
}
|
|
4669
|
+
return unique;
|
|
3284
4670
|
}
|
|
3285
|
-
function
|
|
3286
|
-
|
|
3287
|
-
|
|
3288
|
-
return
|
|
4671
|
+
function imagenames(rule, run, step, count, extension) {
|
|
4672
|
+
const names = [];
|
|
4673
|
+
for (let index = 1; index <= Math.max(0, Math.round(count)); index += 1) names.push(buildname(rule, { run, step, sequence: index, kind: "image" }, extension));
|
|
4674
|
+
return names;
|
|
3289
4675
|
}
|
|
3290
|
-
function
|
|
3291
|
-
if (value
|
|
3292
|
-
|
|
3293
|
-
if (
|
|
3294
|
-
|
|
3295
|
-
|
|
3296
|
-
}
|
|
3297
|
-
if (kind === "boolean") return { value: value === true || value === "true", missing: false };
|
|
3298
|
-
return { value, missing: false };
|
|
4676
|
+
function lapseplanof(value) {
|
|
4677
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
|
|
4678
|
+
const options = value;
|
|
4679
|
+
if (typeof options.interval !== "number" || !Number.isFinite(options.interval)) return void 0;
|
|
4680
|
+
if (typeof options.duration !== "number" || !Number.isFinite(options.duration)) return void 0;
|
|
4681
|
+
const format = options.format === "jpeg" || options.format === "webp" ? options.format : "png";
|
|
4682
|
+
return { interval: options.interval, duration: options.duration, format };
|
|
3299
4683
|
}
|
|
3300
|
-
function
|
|
3301
|
-
|
|
3302
|
-
|
|
3303
|
-
|
|
3304
|
-
|
|
3305
|
-
let missing = false;
|
|
3306
|
-
for (const segment of rule.path.split(".")) {
|
|
3307
|
-
const next = pathstep(current, segment);
|
|
3308
|
-
if (next === void 0) {
|
|
3309
|
-
missing = true;
|
|
3310
|
-
break;
|
|
3311
|
-
}
|
|
3312
|
-
current = next;
|
|
3313
|
-
}
|
|
3314
|
-
if (missing) fields.push({ name: rule.name, path: rule.path, kind, ...rule.default !== void 0 ? { value: rule.default } : {}, missing: true });
|
|
3315
|
-
else {
|
|
3316
|
-
const resolved = coerce(current, kind, rule.default);
|
|
3317
|
-
fields.push({ name: rule.name, path: rule.path, kind, ...resolved.value !== void 0 ? { value: resolved.value } : {}, ...resolved.missing ? { missing: true } : {} });
|
|
3318
|
-
}
|
|
3319
|
-
}
|
|
3320
|
-
return fields;
|
|
4684
|
+
function lapseframes(plan) {
|
|
4685
|
+
if (!(plan.interval > 0) || !(plan.duration > 0)) return [];
|
|
4686
|
+
const frames = [];
|
|
4687
|
+
for (let time = 0; time < plan.duration; time += plan.interval) frames.push(Math.round(time));
|
|
4688
|
+
return frames;
|
|
3321
4689
|
}
|
|
3322
|
-
function
|
|
3323
|
-
if (!
|
|
3324
|
-
const
|
|
3325
|
-
|
|
3326
|
-
|
|
3327
|
-
|
|
3328
|
-
|
|
3329
|
-
|
|
3330
|
-
}
|
|
3331
|
-
if (field.kind === "string" && typeof value !== "string") errors.push(`The field ${field.name} must be a string.`);
|
|
3332
|
-
if (field.kind === "number" && (typeof value !== "number" || !Number.isFinite(value))) errors.push(`The field ${field.name} must be a finite number.`);
|
|
3333
|
-
if (field.kind === "boolean" && typeof value !== "boolean") errors.push(`The field ${field.name} must be a boolean.`);
|
|
3334
|
-
}
|
|
3335
|
-
return { ok: errors.length === 0, errors };
|
|
4690
|
+
function convertdirectiveof(value) {
|
|
4691
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
|
|
4692
|
+
const options = value;
|
|
4693
|
+
if (options.target !== "png" && options.target !== "jpeg" && options.target !== "webp") return void 0;
|
|
4694
|
+
const normalized = { target: options.target };
|
|
4695
|
+
if (options.source === "png" || options.source === "jpeg" || options.source === "webp") normalized.source = options.source;
|
|
4696
|
+
if (typeof options.quality === "number" && Number.isFinite(options.quality)) normalized.quality = options.quality;
|
|
4697
|
+
return normalized;
|
|
3336
4698
|
}
|
|
3337
|
-
function
|
|
3338
|
-
if (!
|
|
3339
|
-
const
|
|
3340
|
-
|
|
3341
|
-
|
|
3342
|
-
|
|
3343
|
-
return
|
|
4699
|
+
function thumbdirectiveof(value) {
|
|
4700
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
|
|
4701
|
+
const options = value;
|
|
4702
|
+
if (typeof options.size !== "number" || !Number.isFinite(options.size) || options.size <= 0) return void 0;
|
|
4703
|
+
if (options.fit !== "cover" && options.fit !== "contain") return void 0;
|
|
4704
|
+
if (typeof options.suffix !== "string" || !options.suffix.trim()) return void 0;
|
|
4705
|
+
return { size: options.size, fit: options.fit, suffix: options.suffix.trim() };
|
|
3344
4706
|
}
|
|
3345
|
-
function
|
|
3346
|
-
|
|
3347
|
-
|
|
3348
|
-
|
|
3349
|
-
const
|
|
3350
|
-
|
|
3351
|
-
|
|
3352
|
-
if (Array.isArray(value)) return value.map((item) => typeof item === "string" ? item : item && typeof item === "object" && typeof item.message === "string" ? item.message : String(item));
|
|
3353
|
-
if (typeof value === "string") return [value];
|
|
3354
|
-
}
|
|
3355
|
-
return [body];
|
|
3356
|
-
} catch {
|
|
3357
|
-
return [body];
|
|
4707
|
+
function thumbgeometry(source, directive) {
|
|
4708
|
+
const size = Math.max(1, Math.round(directive.size));
|
|
4709
|
+
if (directive.fit === "contain") {
|
|
4710
|
+
const scale2 = Math.min(size / Math.max(1, source.width), size / Math.max(1, source.height));
|
|
4711
|
+
const dw = Math.max(1, Math.round(source.width * scale2));
|
|
4712
|
+
const dh = Math.max(1, Math.round(source.height * scale2));
|
|
4713
|
+
return { sx: 0, sy: 0, sw: source.width, sh: source.height, dx: Math.floor((size - dw) / 2), dy: Math.floor((size - dh) / 2), dw, dh, width: size, height: size };
|
|
3358
4714
|
}
|
|
4715
|
+
const scale = Math.max(size / Math.max(1, source.width), size / Math.max(1, source.height));
|
|
4716
|
+
const sw = Math.min(source.width, Math.round(size / scale));
|
|
4717
|
+
const sh = Math.min(source.height, Math.round(size / scale));
|
|
4718
|
+
return { sx: Math.floor((source.width - sw) / 2), sy: Math.floor((source.height - sh) / 2), sw, sh, dx: 0, dy: 0, dw: size, dh: size, width: size, height: size };
|
|
3359
4719
|
}
|
|
3360
|
-
|
|
3361
|
-
|
|
3362
|
-
|
|
3363
|
-
|
|
3364
|
-
|
|
3365
|
-
|
|
3366
|
-
|
|
3367
|
-
|
|
3368
|
-
|
|
3369
|
-
|
|
3370
|
-
}
|
|
3371
|
-
function graphqlopenvelope(request) {
|
|
3372
|
-
return JSON.stringify({ query: request.query, ...request.variables !== void 0 ? { variables: request.variables } : {}, ...request.operationname !== void 0 ? { operationName: request.operationname } : {} });
|
|
4720
|
+
function mediaentries(raw) {
|
|
4721
|
+
return raw.map((entry) => ({
|
|
4722
|
+
url: typeof entry.url === "string" ? entry.url : "",
|
|
4723
|
+
mime: typeof entry.mime === "string" ? entry.mime : "",
|
|
4724
|
+
duration: typeof entry.duration === "number" && Number.isFinite(entry.duration) ? entry.duration : 0,
|
|
4725
|
+
width: typeof entry.width === "number" && Number.isFinite(entry.width) ? Math.round(entry.width) : 0,
|
|
4726
|
+
height: typeof entry.height === "number" && Number.isFinite(entry.height) ? Math.round(entry.height) : 0,
|
|
4727
|
+
codecs: typeof entry.codecs === "string" ? entry.codecs : "",
|
|
4728
|
+
tracks: Array.isArray(entry.tracks) ? entry.tracks.filter((item) => typeof item === "string") : []
|
|
4729
|
+
}));
|
|
3373
4730
|
}
|
|
3374
|
-
function
|
|
3375
|
-
|
|
3376
|
-
|
|
3377
|
-
|
|
3378
|
-
|
|
4731
|
+
function assetentries(raw) {
|
|
4732
|
+
return raw.map((entry) => ({
|
|
4733
|
+
kind: entry.kind === "logo" ? "logo" : "favicon",
|
|
4734
|
+
url: typeof entry.url === "string" ? entry.url : "",
|
|
4735
|
+
bytes: typeof entry.bytes === "number" && Number.isFinite(entry.bytes) ? entry.bytes : 0,
|
|
4736
|
+
...typeof entry.sizes === "string" && entry.sizes.trim() ? { sizes: entry.sizes.trim() } : {}
|
|
4737
|
+
}));
|
|
3379
4738
|
}
|
|
3380
|
-
|
|
3381
|
-
|
|
3382
|
-
|
|
3383
|
-
|
|
3384
|
-
|
|
3385
|
-
|
|
3386
|
-
|
|
3387
|
-
|
|
3388
|
-
|
|
4739
|
+
function streamsummaries(raw) {
|
|
4740
|
+
return raw.map((entry) => {
|
|
4741
|
+
const tracks = Array.isArray(entry.tracks) ? entry.tracks : [];
|
|
4742
|
+
return {
|
|
4743
|
+
kind: typeof entry.kind === "string" ? entry.kind : "stream",
|
|
4744
|
+
tracks: tracks.length,
|
|
4745
|
+
label: typeof entry.label === "string" ? entry.label : "",
|
|
4746
|
+
live: entry.live === true,
|
|
4747
|
+
detail: tracks.map((track) => {
|
|
4748
|
+
const item = track;
|
|
4749
|
+
return {
|
|
4750
|
+
kind: typeof item.kind === "string" ? item.kind : "",
|
|
4751
|
+
label: typeof item.label === "string" ? item.label : "",
|
|
4752
|
+
...typeof item.width === "number" && Number.isFinite(item.width) ? { width: Math.round(item.width) } : {},
|
|
4753
|
+
...typeof item.height === "number" && Number.isFinite(item.height) ? { height: Math.round(item.height) } : {},
|
|
4754
|
+
...typeof item.framerate === "number" && Number.isFinite(item.framerate) ? { framerate: item.framerate } : {},
|
|
4755
|
+
state: typeof item.state === "string" ? item.state : ""
|
|
4756
|
+
};
|
|
4757
|
+
})
|
|
4758
|
+
};
|
|
4759
|
+
});
|
|
3389
4760
|
}
|
|
3390
4761
|
|
|
3391
4762
|
// extension/browsertabs.ts
|
|
@@ -4587,7 +5958,7 @@ function stepoptions2(step) {
|
|
|
4587
5958
|
}
|
|
4588
5959
|
async function refreshcapabilities() {
|
|
4589
5960
|
const report = await readcapabilities();
|
|
4590
|
-
const withmedia = { ...report, captures: [...capturekinds], media: [...mediakinds], http: [...httpkinds] };
|
|
5961
|
+
const withmedia = { ...report, captures: [...capturekinds], media: [...mediakinds], http: [...httpkinds], netwatch: [...socketkinds, ...netwatchkinds], control: [...controlkinds] };
|
|
4591
5962
|
await memory.setcapabilities(withmedia);
|
|
4592
5963
|
return withmedia;
|
|
4593
5964
|
}
|
|
@@ -4759,6 +6130,12 @@ function stepauditkind(step, ok) {
|
|
|
4759
6130
|
if (step.kind === "logprovenance") return "provenance";
|
|
4760
6131
|
return "scrape";
|
|
4761
6132
|
}
|
|
6133
|
+
if (issocketkind(step.kind)) return "socket";
|
|
6134
|
+
if (isnetwatchkind(step.kind)) {
|
|
6135
|
+
if (step.kind === "extractapi") return "replay";
|
|
6136
|
+
if (step.kind === "watchrequests") return "watch";
|
|
6137
|
+
return "observation";
|
|
6138
|
+
}
|
|
4762
6139
|
if (formfillkinds.has(step.kind)) return "fill";
|
|
4763
6140
|
if (pointerkinds.has(step.kind)) return "pointer";
|
|
4764
6141
|
if (watchstepkinds.has(step.kind)) return "watch";
|
|
@@ -7146,7 +8523,7 @@ async function executehttpstep(step, session, plan, tabid2, origin) {
|
|
|
7146
8523
|
streamstate.bytes = total;
|
|
7147
8524
|
} } : void 0;
|
|
7148
8525
|
try {
|
|
7149
|
-
const transport = (url, init) =>
|
|
8526
|
+
const transport = (url, init) => controlledfetch(plan.id, url, init, controller, window2, streamstate);
|
|
7150
8527
|
const result = await sendfetch({ request, ...policy !== void 0 && Object.keys(policy).length > 0 ? { options: policy } : {}, transport, onretry: (attempt, wait, reason) => {
|
|
7151
8528
|
void (async () => {
|
|
7152
8529
|
await memory.setprogress(recordfetchretry(await memory.getprogress(), plan.id, step.id, { attempt, url: request.url, wait, reason }, Date.now()));
|
|
@@ -7202,7 +8579,7 @@ async function executehttpstep(step, session, plan, tabid2, origin) {
|
|
|
7202
8579
|
const controller = new AbortController();
|
|
7203
8580
|
activefetches.set(callid, controller);
|
|
7204
8581
|
try {
|
|
7205
|
-
const transport = (url, init) =>
|
|
8582
|
+
const transport = (url, init) => controlledfetch(plan.id, url, init, controller);
|
|
7206
8583
|
const keynames = Array.isArray(options.apikeys) ? options.apikeys.filter((name) => typeof name === "string" && name.trim().length > 0) : [];
|
|
7207
8584
|
if (step.kind === "callrest") {
|
|
7208
8585
|
const methodoverride = typeof options.method === "string" ? options.method.trim().toUpperCase() : void 0;
|
|
@@ -7252,6 +8629,750 @@ async function executehttpstep(step, session, plan, tabid2, origin) {
|
|
|
7252
8629
|
}
|
|
7253
8630
|
throw new Error("Unsupported network observation kind.");
|
|
7254
8631
|
}
|
|
8632
|
+
var activesockets = /* @__PURE__ */ new Map();
|
|
8633
|
+
var channelbuses = /* @__PURE__ */ new Map();
|
|
8634
|
+
var netpoll = 100;
|
|
8635
|
+
function waitsome(milliseconds) {
|
|
8636
|
+
return new Promise((resolve) => setTimeout(resolve, Math.max(0, milliseconds)));
|
|
8637
|
+
}
|
|
8638
|
+
async function queueinboundmessage(channelid, payload) {
|
|
8639
|
+
const stored = await memory.getchannel(channelid);
|
|
8640
|
+
if (!stored) return;
|
|
8641
|
+
const bus = channelbuses.get(channelid) ?? { sequences: {}, queue: [] };
|
|
8642
|
+
const received = receivemessage(bus, channelid, "inbound", payload, Date.now());
|
|
8643
|
+
channelbuses.set(channelid, received.state);
|
|
8644
|
+
await memory.addmessage(received.envelope);
|
|
8645
|
+
await memory.addchannel({ ...stored, received: stored.received + 1 });
|
|
8646
|
+
await refreshbadge().catch(() => {
|
|
8647
|
+
});
|
|
8648
|
+
}
|
|
8649
|
+
function wirewebsocket(channelid, url, protocols, options, budget) {
|
|
8650
|
+
let settled = false;
|
|
8651
|
+
const socket = new WebSocket(url, protocols.length > 0 ? protocols : void 0);
|
|
8652
|
+
const active = activesockets.get(channelid);
|
|
8653
|
+
if (active) activesockets.set(channelid, { ...active, socket });
|
|
8654
|
+
void budget;
|
|
8655
|
+
socket.onmessage = (event) => {
|
|
8656
|
+
void queueinboundmessage(channelid, String(event.data)).catch(() => {
|
|
8657
|
+
});
|
|
8658
|
+
};
|
|
8659
|
+
socket.onopen = () => {
|
|
8660
|
+
void (async () => {
|
|
8661
|
+
const stored = await memory.getchannel(channelid);
|
|
8662
|
+
if (stored) await memory.addchannel({ ...stored, state: "open", openedat: stored.openedat || Date.now() });
|
|
8663
|
+
})().catch(() => {
|
|
8664
|
+
});
|
|
8665
|
+
};
|
|
8666
|
+
socket.onclose = (event) => {
|
|
8667
|
+
if (settled) return;
|
|
8668
|
+
settled = true;
|
|
8669
|
+
void (async () => {
|
|
8670
|
+
const stored = await memory.getchannel(channelid);
|
|
8671
|
+
if (!stored) return;
|
|
8672
|
+
if (stored.state !== "open") return;
|
|
8673
|
+
const remaining = Math.max(0, Math.floor(options.reconnect ?? 0) - budget.reconnects);
|
|
8674
|
+
if (remaining <= 0) {
|
|
8675
|
+
await memory.addchannel(closechannel(stored, Date.now(), `The websocket closed with code ${event.code}.`));
|
|
8676
|
+
activesockets.delete(channelid);
|
|
8677
|
+
channelbuses.delete(channelid);
|
|
8678
|
+
await audit("socket", `Channel ${channelid} of ${stored.origin} closed with code ${event.code} after ${stored.sent} sent and ${stored.received} received message${stored.received === 1 ? "" : "s"}.`, { stepid: stored.stepid });
|
|
8679
|
+
return;
|
|
8680
|
+
}
|
|
8681
|
+
const wait = reconnectwaits(1, options.backoff ?? 0, options.backoffceiling)[0] ?? 0;
|
|
8682
|
+
budget.reconnects += 1;
|
|
8683
|
+
await memory.addchannel({ ...stored, state: "connecting", reconnects: stored.reconnects + 1 });
|
|
8684
|
+
setTimeout(() => wirewebsocket(channelid, url, protocols, options, budget), wait);
|
|
8685
|
+
})().catch(() => {
|
|
8686
|
+
});
|
|
8687
|
+
};
|
|
8688
|
+
socket.onerror = () => {
|
|
8689
|
+
};
|
|
8690
|
+
void settled;
|
|
8691
|
+
}
|
|
8692
|
+
async function runsubscription(subscription, controller) {
|
|
8693
|
+
let buffer = "";
|
|
8694
|
+
let current = subscription;
|
|
8695
|
+
const startedat = Date.now();
|
|
8696
|
+
for (; ; ) {
|
|
8697
|
+
if (controller.signal.aborted) break;
|
|
8698
|
+
if (subscription.lifetime !== void 0 && Date.now() - startedat >= subscription.lifetime) break;
|
|
8699
|
+
try {
|
|
8700
|
+
const response = await fetch(subscription.url, { headers: sserequestheaders(current), credentials: "omit", signal: controller.signal });
|
|
8701
|
+
if (!response.body) throw new Error("The event stream returned no body.");
|
|
8702
|
+
const reader = response.body.getReader();
|
|
8703
|
+
const decoder = new TextDecoder();
|
|
8704
|
+
for (; ; ) {
|
|
8705
|
+
const { done, value } = await reader.read();
|
|
8706
|
+
if (done) break;
|
|
8707
|
+
buffer += decoder.decode(value, { stream: true });
|
|
8708
|
+
const parsed = parsestreamchunk(buffer);
|
|
8709
|
+
buffer = parsed.rest;
|
|
8710
|
+
for (const event of parsed.events) {
|
|
8711
|
+
const names = event.event !== void 0 && !current.names.includes(event.event) ? [...current.names, event.event] : current.names;
|
|
8712
|
+
current = { ...current, events: current.events + 1, names, ...event.id !== void 0 ? { lasteventid: event.id } : {} };
|
|
8713
|
+
await memory.setsubscription(current);
|
|
8714
|
+
await memory.setprogress(recordevent(await memory.getprogress(), current.runid, current.stepid, { url: current.url, name: event.event ?? "", events: current.events, ...current.lasteventid !== void 0 ? { lasteventid: current.lasteventid } : {} }, Date.now())).catch(() => {
|
|
8715
|
+
});
|
|
8716
|
+
}
|
|
8717
|
+
}
|
|
8718
|
+
} catch {
|
|
8719
|
+
if (controller.signal.aborted) break;
|
|
8720
|
+
}
|
|
8721
|
+
if (controller.signal.aborted) break;
|
|
8722
|
+
if (subscription.lifetime !== void 0 && Date.now() - startedat >= subscription.lifetime) break;
|
|
8723
|
+
await waitsome(netpoll * 10);
|
|
8724
|
+
}
|
|
8725
|
+
const closed = { ...current, state: "closed", closedat: Date.now() };
|
|
8726
|
+
await memory.setsubscription(closed);
|
|
8727
|
+
activesockets.delete(subscription.id);
|
|
8728
|
+
await audit("socket", `Event subscription ${subscription.id} of ${subscription.origin} closed after ${closed.events} observed event${closed.events === 1 ? "" : "s"}${closed.lasteventid !== void 0 ? ` at last event id ${closed.lasteventid}` : ""}.`, { stepid: subscription.stepid });
|
|
8729
|
+
}
|
|
8730
|
+
function parsestreamchunk(chunk) {
|
|
8731
|
+
return parsessetext(chunk);
|
|
8732
|
+
}
|
|
8733
|
+
async function closechannelsforrun(runid) {
|
|
8734
|
+
for (const [id, active] of [...activesockets.entries()]) {
|
|
8735
|
+
if (active.runid !== runid) continue;
|
|
8736
|
+
active.cancelled = true;
|
|
8737
|
+
active.controller?.abort();
|
|
8738
|
+
try {
|
|
8739
|
+
active.socket?.close(1e3);
|
|
8740
|
+
} catch {
|
|
8741
|
+
}
|
|
8742
|
+
activesockets.delete(id);
|
|
8743
|
+
if (active.channel) {
|
|
8744
|
+
const closed = closechannel(active.channel, Date.now());
|
|
8745
|
+
await memory.addchannel(closed).catch(() => {
|
|
8746
|
+
});
|
|
8747
|
+
await audit("socket", `Channel ${id} of ${active.channel.origin} closed at the end of the run with ${active.channel.sent} sent and ${active.channel.received} received message${active.channel.received === 1 ? "" : "s"}.`, { stepid: active.channel.stepid }).catch(() => void 0);
|
|
8748
|
+
}
|
|
8749
|
+
if (active.subscription) {
|
|
8750
|
+
const closed = { ...active.subscription, state: "closed", closedat: Date.now() };
|
|
8751
|
+
await memory.setsubscription(closed).catch(() => void 0);
|
|
8752
|
+
await audit("socket", `Event subscription ${id} of ${active.subscription.origin} closed at the end of the run after ${closed.events} observed event${closed.events === 1 ? "" : "s"}.`, { stepid: active.subscription.stepid }).catch(() => void 0);
|
|
8753
|
+
}
|
|
8754
|
+
}
|
|
8755
|
+
channelbuses.clear();
|
|
8756
|
+
}
|
|
8757
|
+
async function executesocketstep(step, session, plan, tabid2, origin) {
|
|
8758
|
+
const options = stepoptions2(step);
|
|
8759
|
+
const extra = { ...session ? { sessionid: session.id } : {}, planid: plan.id, stepid: step.id };
|
|
8760
|
+
if (step.kind === "opensocket") {
|
|
8761
|
+
const channel = channeloptionsof(options.socket);
|
|
8762
|
+
if (!channel) throw new Error("A reviewed socket with a url is required in options.socket.");
|
|
8763
|
+
const gate = socketgate(session, channel.url);
|
|
8764
|
+
if (!gate.allowed) throw new Error(gate.reason ?? "The channel stays outside the session origin grants.");
|
|
8765
|
+
const record2 = newchannel({ id: randomid(), runid: plan.id, stepid: step.id, kind: "websocket", url: channel.url, ...channel.options.protocols !== void 0 ? { protocols: channel.options.protocols } : {}, at: Date.now() });
|
|
8766
|
+
const connect = (url, protocols) => new Promise((resolve) => {
|
|
8767
|
+
const socket = new WebSocket(url, protocols.length > 0 ? protocols : void 0);
|
|
8768
|
+
socket.onopen = () => resolve({ open: true });
|
|
8769
|
+
socket.onerror = () => resolve({ open: false, error: "The websocket reported an error before opening." });
|
|
8770
|
+
socket.onclose = (event) => resolve({ open: false, code: event.code, error: `The websocket closed with code ${event.code}.` });
|
|
8771
|
+
});
|
|
8772
|
+
const opened = await openchannel({ record: record2, options: channel.options, connect });
|
|
8773
|
+
await memory.addchannel(opened);
|
|
8774
|
+
channelbuses.set(opened.id, { sequences: {}, queue: [] });
|
|
8775
|
+
activesockets.set(opened.id, { runid: plan.id, channel: opened, cancelled: false });
|
|
8776
|
+
if (opened.state === "open") wirewebsocket(opened.id, opened.url, opened.protocols ?? [], channel.options, { reconnects: 0 });
|
|
8777
|
+
await memory.setprogress(recordchannel(await memory.getprogress(), plan.id, step.id, { id: opened.id, kind: opened.kind, state: opened.state, url: opened.url, sent: opened.sent, received: opened.received }, Date.now()));
|
|
8778
|
+
await refreshbadge();
|
|
8779
|
+
await audit("socket", `Opened the websocket channel ${opened.id} to ${opened.origin}${(opened.protocols ?? []).length > 0 ? ` with the reviewed protocol${(opened.protocols ?? []).length === 1 ? "" : "s"} ${(opened.protocols ?? []).join(", ")}` : ""} in the ${opened.state} state after ${opened.reconnects} reconnect attempt${opened.reconnects === 1 ? "" : "s"}; payloads stay out of the audit trail.`, extra);
|
|
8780
|
+
return { ok: opened.state === "open", summary: `The websocket channel to ${opened.origin} is ${opened.state}.`, details: { network: { exchanges: 0, channelstate: opened.state, messages: 0 }, channel: { id: opened.id, url: opened.url, state: opened.state, reconnects: opened.reconnects, protocols: opened.protocols ?? [] } } };
|
|
8781
|
+
}
|
|
8782
|
+
if (step.kind === "sendmessage") {
|
|
8783
|
+
const message = options.message;
|
|
8784
|
+
const channelid = message && typeof message === "object" && !Array.isArray(message) && typeof message.channel === "string" ? message.channel : "";
|
|
8785
|
+
const stream = message && typeof message === "object" && !Array.isArray(message) && typeof message.stream === "string" ? message.stream : "outbound";
|
|
8786
|
+
const payload = message && typeof message === "object" && !Array.isArray(message) && typeof message.payload === "string" ? message.payload : void 0;
|
|
8787
|
+
if (!channelid || payload === void 0) throw new Error("A reviewed message with an open channel id and a payload is required in options.message.");
|
|
8788
|
+
const stored = await memory.getchannel(channelid);
|
|
8789
|
+
if (!stored) throw new Error(`No stored channel matches ${channelid}.`);
|
|
8790
|
+
if (stored.state !== "open") throw new Error(`The channel ${channelid} is ${stored.state} and cannot publish.`);
|
|
8791
|
+
const active = activesockets.get(channelid);
|
|
8792
|
+
if (!active?.socket || active.socket.readyState !== WebSocket.OPEN) throw new Error(`The channel ${channelid} has no live websocket to publish on.`);
|
|
8793
|
+
const bus = channelbuses.get(channelid) ?? { sequences: {}, queue: [] };
|
|
8794
|
+
const published = publishmessage(bus, channelid, stream, payload, Date.now());
|
|
8795
|
+
channelbuses.set(channelid, published.state);
|
|
8796
|
+
active.socket.send(payload);
|
|
8797
|
+
const updated = { ...stored, sent: stored.sent + 1 };
|
|
8798
|
+
await memory.addchannel(updated);
|
|
8799
|
+
await memory.addmessage(published.envelope);
|
|
8800
|
+
await memory.setprogress(recordchannel(await memory.getprogress(), plan.id, step.id, { id: updated.id, kind: updated.kind, state: updated.state, url: updated.url, sent: updated.sent, received: updated.received }, Date.now()));
|
|
8801
|
+
await audit("socket", `Published ${payload.length} reviewed character${payload.length === 1 ? "" : "s"} on the ${stream} stream of channel ${channelid} as sequence ${published.envelope.sequence}; the payload value stays out of the audit trail.`, extra);
|
|
8802
|
+
return { ok: true, summary: `Published the reviewed payload on the ${stream} stream of channel ${channelid} as sequence ${published.envelope.sequence}.`, details: { network: { exchanges: 0, channelstate: updated.state, messages: updated.sent + updated.received }, message: { channelid, stream, sequence: published.envelope.sequence, bytes: payload.length } } };
|
|
8803
|
+
}
|
|
8804
|
+
if (step.kind === "waitmessage") {
|
|
8805
|
+
const filter = messagefilterof(options.filter);
|
|
8806
|
+
const channelid = typeof options.channel === "string" ? options.channel : typeof options.filter?.channel === "string" ? options.filter.channel : "";
|
|
8807
|
+
if (!channelid) throw new Error("A reviewed channel id is required in options.channel before a message waits.");
|
|
8808
|
+
const stored = await memory.getchannel(channelid);
|
|
8809
|
+
if (!stored) throw new Error(`No stored channel matches ${channelid}.`);
|
|
8810
|
+
const budget = typeof options.wait === "number" && Number.isFinite(options.wait) && options.wait >= 0 ? options.wait : 0;
|
|
8811
|
+
const deadline = Date.now() + budget;
|
|
8812
|
+
let matched = [];
|
|
8813
|
+
for (; ; ) {
|
|
8814
|
+
const queued = await memory.getmessages(channelid);
|
|
8815
|
+
matched = queued.filter((envelope) => matchmessage(filter, envelope)).slice(0, filter.limit ?? (matched.length || void 0));
|
|
8816
|
+
if (matched.length >= (filter.limit ?? 1) || Date.now() >= deadline) break;
|
|
8817
|
+
await waitsome(netpoll);
|
|
8818
|
+
}
|
|
8819
|
+
await memory.drainmessages(matched.map((envelope) => ({ channelid: envelope.channelid, sequence: envelope.sequence })));
|
|
8820
|
+
await audit("socket", `The message wait on channel ${channelid} matched ${matched.length} envelope${matched.length === 1 ? "" : "s"} of the reviewed filter within the ${budget} millisecond budget; payload values stay out of the audit trail.`, extra);
|
|
8821
|
+
return { ok: matched.length > 0, summary: matched.length > 0 ? `Matched ${matched.length} message${matched.length === 1 ? "" : "s"} on channel ${channelid}.` : `No message of channel ${channelid} matched the reviewed filter within the ${budget} millisecond budget.`, details: { network: { exchanges: 0, channelstate: stored.state, messages: stored.sent + stored.received }, messages: matched.map((envelope) => ({ stream: envelope.stream, sequence: envelope.sequence, payload: envelope.payload })) } };
|
|
8822
|
+
}
|
|
8823
|
+
if (step.kind === "subscribesse") {
|
|
8824
|
+
const subscription = subscriptionoptionsof(options.subscription);
|
|
8825
|
+
if (!subscription) throw new Error("A reviewed subscription with an event stream url and a cancellation path is required in options.subscription.");
|
|
8826
|
+
const gate = socketgate(session, subscription.url);
|
|
8827
|
+
if (!gate.allowed) throw new Error(gate.reason ?? "The event stream stays outside the session origin grants.");
|
|
8828
|
+
const record2 = { id: randomid(), runid: plan.id, stepid: step.id, url: subscription.url, origin: new URL(subscription.url).origin, state: "open", events: 0, names: [], cancel: subscription.cancel, openedat: Date.now(), ...subscription.lasteventid !== void 0 ? { lasteventid: subscription.lasteventid } : {}, ...subscription.lifetime !== void 0 ? { lifetime: subscription.lifetime } : {} };
|
|
8829
|
+
const controller = new AbortController();
|
|
8830
|
+
activesockets.set(record2.id, { runid: plan.id, subscription: record2, controller, cancelled: false });
|
|
8831
|
+
await memory.setsubscription(record2);
|
|
8832
|
+
await memory.setprogress(recordchannel(await memory.getprogress(), plan.id, step.id, { id: record2.id, kind: "sse", state: record2.state, url: record2.url, sent: 0, received: 0 }, Date.now()));
|
|
8833
|
+
await audit("socket", `Subscribed the server sent events stream ${record2.id} of ${record2.origin} with the reviewed ${subscription.cancel.kind} cancellation path${subscription.lifetime !== void 0 ? ` and the ${subscription.lifetime} millisecond lifetime window` : ""}.`, extra);
|
|
8834
|
+
void runsubscription(record2, controller).catch(() => {
|
|
8835
|
+
});
|
|
8836
|
+
return { ok: true, summary: `The event stream subscription of ${record2.origin} is open.`, details: { network: { exchanges: 0, channelstate: "open", messages: 0 }, subscription: { id: record2.id, url: record2.url, cancel: record2.cancel, ...subscription.lifetime !== void 0 ? { lifetime: subscription.lifetime } : {} } } };
|
|
8837
|
+
}
|
|
8838
|
+
if (step.kind === "longpoll") {
|
|
8839
|
+
const cursor = pollcursorof(options.poll);
|
|
8840
|
+
if (!cursor) throw new Error("A reviewed poll cursor with a url, cursor field, interval and stop condition is required in options.poll.");
|
|
8841
|
+
const gate = socketgate(session, cursor.url);
|
|
8842
|
+
if (!gate.allowed) throw new Error(gate.reason ?? "The poll loop stays outside the session origin grants.");
|
|
8843
|
+
const pollid = randomid();
|
|
8844
|
+
const controller = new AbortController();
|
|
8845
|
+
activesockets.set(pollid, { runid: plan.id, controller, cancelled: false });
|
|
8846
|
+
let polls = 0;
|
|
8847
|
+
let next = pollurl(cursor, void 0);
|
|
8848
|
+
let lastreason = "";
|
|
8849
|
+
let stopcursor;
|
|
8850
|
+
try {
|
|
8851
|
+
for (; ; ) {
|
|
8852
|
+
const active = activesockets.get(pollid);
|
|
8853
|
+
if (!active || active.cancelled) {
|
|
8854
|
+
lastreason = "The long poll loop was cancelled.";
|
|
8855
|
+
break;
|
|
8856
|
+
}
|
|
8857
|
+
const started = Date.now();
|
|
8858
|
+
const response = await fetch(next.url, { method: next.body !== void 0 ? "POST" : "GET", ...next.body !== void 0 ? { headers: { "content-type": "application/json" }, body: next.body } : {}, credentials: "omit", signal: controller.signal });
|
|
8859
|
+
const text2 = await response.text();
|
|
8860
|
+
polls += 1;
|
|
8861
|
+
let parsed;
|
|
8862
|
+
try {
|
|
8863
|
+
parsed = JSON.parse(text2);
|
|
8864
|
+
} catch {
|
|
8865
|
+
parsed = void 0;
|
|
8866
|
+
}
|
|
8867
|
+
const mime = response.headers.get("content-type")?.split(";")[0] ?? "";
|
|
8868
|
+
const exchange = { id: randomid(), runid: plan.id, stepid: step.id, correlationid: correlationid(plan.id, polls), url: next.url, origin: new URL(next.url).origin, method: next.body !== void 0 ? "POST" : "GET", status: response.status, statusclass: response.status >= 200 && response.status < 300 ? "success" : response.status >= 400 && response.status < 500 ? "clienterror" : response.status >= 500 ? "servererror" : "unknown", source: "extension", timing: Date.now() - started, bytes: text2.length, ...mime ? { mime } : {}, at: Date.now() };
|
|
8869
|
+
await memory.addexchange(exchange);
|
|
8870
|
+
await memory.setprogress(recordexchange(await memory.getprogress(), plan.id, step.id, { id: exchange.id, correlationid: exchange.correlationid, method: exchange.method, origin: exchange.origin, url: exchange.url, status: exchange.status, statusclass: exchange.statusclass, duration: exchange.timing, bytes: exchange.bytes }, Date.now()));
|
|
8871
|
+
const decision = polldecision({ cursor, polls: polls - 1, response: parsed, cancelled: () => activesockets.get(pollid)?.cancelled === true, expiresat: plan.expiresat, now: Date.now() });
|
|
8872
|
+
await memory.setprogress(recordpoll(await memory.getprogress(), plan.id, step.id, { poll: polls, ...decision.cursor !== void 0 ? { cursor: decision.cursor } : {}, status: response.status, stopped: !decision.continue, reason: decision.reason }, Date.now()));
|
|
8873
|
+
if (!decision.continue) {
|
|
8874
|
+
lastreason = decision.reason;
|
|
8875
|
+
break;
|
|
8876
|
+
}
|
|
8877
|
+
stopcursor = decision.cursor;
|
|
8878
|
+
await waitsome(decision.next?.wait ?? cursor.interval);
|
|
8879
|
+
next = { url: decision.next?.url ?? next.url, ...decision.next?.body !== void 0 ? { body: decision.next.body } : {} };
|
|
8880
|
+
}
|
|
8881
|
+
} finally {
|
|
8882
|
+
activesockets.delete(pollid);
|
|
8883
|
+
}
|
|
8884
|
+
await audit("socket", `The long poll loop of ${cursor.url} ran ${polls} poll${polls === 1 ? "" : "s"} on the ${cursor.cursorfield} cursor and stopped: ${lastreason}.`, extra);
|
|
8885
|
+
return { ok: true, summary: `The long poll loop ran ${polls} poll${polls === 1 ? "" : "s"} and stopped: ${lastreason}`, details: { network: { exchanges: polls, channelstate: "none", messages: 0 }, poll: { url: cursor.url, polls, ...stopcursor !== void 0 ? { cursor: stopcursor } : {}, stop: cursor.stop, reason: lastreason } } };
|
|
8886
|
+
}
|
|
8887
|
+
void tabid2;
|
|
8888
|
+
void origin;
|
|
8889
|
+
throw new Error("Unsupported socket observation kind.");
|
|
8890
|
+
}
|
|
8891
|
+
async function executenetwatchstep(step, session, plan, tabid2, origin) {
|
|
8892
|
+
const options = stepoptions2(step);
|
|
8893
|
+
const extra = { ...session ? { sessionid: session.id } : {}, planid: plan.id, stepid: step.id };
|
|
8894
|
+
if (step.kind === "watchrequests") {
|
|
8895
|
+
const watch = options.watch && typeof options.watch === "object" && !Array.isArray(options.watch) ? options.watch : {};
|
|
8896
|
+
const window2 = typeof watch.window === "number" && Number.isFinite(watch.window) && watch.window >= 0 ? watch.window : 0;
|
|
8897
|
+
const limit = typeof options.limit === "number" && Number.isInteger(options.limit) && options.limit >= 1 ? options.limit : void 0;
|
|
8898
|
+
const before = await bridgecall(tabid2, "resourcerecords");
|
|
8899
|
+
const known = new Set(resourcefacts(before ?? []).map((fact) => `${fact.url}@${fact.start}`));
|
|
8900
|
+
if (window2 > 0) await waitsome(window2);
|
|
8901
|
+
const after = await bridgecall(tabid2, "resourcerecords");
|
|
8902
|
+
const fresh = resourcefacts(after ?? []).filter((fact) => !known.has(`${fact.url}@${fact.start}`));
|
|
8903
|
+
const chosen = limit !== void 0 ? fresh.slice(0, limit) : fresh;
|
|
8904
|
+
const observed = [];
|
|
8905
|
+
for (const [index, fact] of chosen.entries()) {
|
|
8906
|
+
const exchange = newexchange({ id: randomid(), runid: plan.id, stepid: step.id, correlationid: correlationid(plan.id, index + 1), fact, at: Date.now() });
|
|
8907
|
+
await memory.addexchange(exchange);
|
|
8908
|
+
await memory.setprogress(recordexchange(await memory.getprogress(), plan.id, step.id, { id: exchange.id, correlationid: exchange.correlationid, method: exchange.method, origin: exchange.origin, url: exchange.url, status: exchange.status, statusclass: exchange.statusclass, duration: exchange.timing, bytes: exchange.bytes, ...exchange.errorclass !== void 0 ? { errorclass: exchange.errorclass } : {} }, Date.now()));
|
|
8909
|
+
observed.push(exchange);
|
|
8910
|
+
}
|
|
8911
|
+
const failed = observed.filter((exchange) => exchange.errorclass !== void 0).length;
|
|
8912
|
+
await refreshbadge();
|
|
8913
|
+
await audit("watch", `Observed ${observed.length} request${observed.length === 1 ? "" : "s"} of the run tab over the reviewed ${window2} millisecond window, derived from the page timing buffers with ${failed} marked failed; headers and bodies stay out of this observation.`, extra);
|
|
8914
|
+
return { ok: true, summary: `Observed ${observed.length} request${observed.length === 1 ? "" : "s"} of the run tab${failed > 0 ? ` with ${failed} failed` : ""}.`, details: { network: { exchanges: observed.length, channelstate: "none", messages: 0 }, observed: observed.map((exchange) => ({ correlationid: exchange.correlationid, method: exchange.method, url: exchange.url, status: exchange.status, statusclass: exchange.statusclass, ...exchange.errorclass !== void 0 ? { errorclass: exchange.errorclass } : {}, bytes: exchange.bytes, duration: exchange.timing })), derivation: "The request lifecycle derives from the page performance and navigation buffers; the timing buffers expose no header names, body bytes or subresource status codes." } };
|
|
8915
|
+
}
|
|
8916
|
+
if (step.kind === "readheaders") {
|
|
8917
|
+
const filter = headerfilterof(options.headers);
|
|
8918
|
+
const exchanges = typeof options.exchange === "string" ? (await memory.getexchanges()).filter((item) => item.id === options.exchange || item.correlationid === options.exchange) : await memory.listexchanges({ runid: plan.id });
|
|
8919
|
+
if (exchanges.length === 0) throw new Error("No observed exchange of the run is stored yet; watch requests first.");
|
|
8920
|
+
const views = [];
|
|
8921
|
+
for (const exchange of exchanges) {
|
|
8922
|
+
const gate = observedorigingranted(session, exchange.url);
|
|
8923
|
+
if (!gate.allowed) {
|
|
8924
|
+
views.push({ correlationid: exchange.correlationid, request: {}, response: {}, redacted: 0, ...gate.reason !== void 0 ? { refused: gate.reason } : {} });
|
|
8925
|
+
continue;
|
|
8926
|
+
}
|
|
8927
|
+
const request = capturedheaders(exchange.requestheaders ?? {}, filter);
|
|
8928
|
+
const response = capturedheaders(exchange.responseheaders ?? {}, filter);
|
|
8929
|
+
const redacted = [...Object.keys(exchange.requestheaders ?? {}), ...Object.keys(exchange.responseheaders ?? {})].filter((name) => filter.redact.includes(name.trim().toLowerCase())).length;
|
|
8930
|
+
const updated = { ...exchange, ...Object.keys(request).length > 0 ? { requestheaders: request } : {}, ...Object.keys(response).length > 0 ? { responseheaders: response } : {} };
|
|
8931
|
+
if (updated.requestheaders !== exchange.requestheaders || updated.responseheaders !== exchange.responseheaders) await memory.addexchange(updated);
|
|
8932
|
+
views.push({ correlationid: exchange.correlationid, request, response, redacted });
|
|
8933
|
+
}
|
|
8934
|
+
const refused = views.filter((view) => view.refused !== void 0).length;
|
|
8935
|
+
await audit("watch", `Read the captured headers of ${views.length - refused} exchange${views.length - refused === 1 ? "" : "s"} through the reviewed allowlist with ${views.reduce((total, view) => total + view.redacted, 0)} redacted value${views.reduce((total, view) => total + view.redacted, 0) === 1 ? "" : "s"}${refused > 0 ? ` and ${refused} refused exchange${refused === 1 ? "" : "s"} outside the grants` : ""}; derived page exchanges carry no headers.`, extra);
|
|
8936
|
+
return { ok: true, summary: `Read the captured headers of ${views.length - refused} exchange${views.length - refused === 1 ? "" : "s"} with ${views.reduce((total, view) => total + view.redacted, 0)} redacted value${views.reduce((total, view) => total + view.redacted, 0) === 1 ? "" : "s"}.`, details: { network: { exchanges: views.length, channelstate: "none", messages: 0 }, headers: views, derivation: "Header values exist only for exchanges captured through the extension context; derived page exchanges carry none." } };
|
|
8937
|
+
}
|
|
8938
|
+
if (step.kind === "capturebodies") {
|
|
8939
|
+
const filter = bodyfilterof(options.body);
|
|
8940
|
+
const exchanges = await memory.listexchanges({ runid: plan.id });
|
|
8941
|
+
const matched = exchanges.filter((exchange) => bodymatches(filter, exchange));
|
|
8942
|
+
if (matched.length === 0) throw new Error("No observed exchange matches the reviewed body filter yet.");
|
|
8943
|
+
const captures = [];
|
|
8944
|
+
for (const exchange of matched) {
|
|
8945
|
+
const gate = observedorigingranted(session, exchange.url);
|
|
8946
|
+
if (!gate.allowed) {
|
|
8947
|
+
captures.push({ correlationid: exchange.correlationid, ref: "", mime: "", bytes: 0, truncated: false, ...gate.reason !== void 0 ? { refused: gate.reason } : {} });
|
|
8948
|
+
continue;
|
|
8949
|
+
}
|
|
8950
|
+
if (exchange.bodyref !== void 0) {
|
|
8951
|
+
captures.push({ correlationid: exchange.correlationid, ref: exchange.bodyref, mime: exchange.mime ?? "", bytes: exchange.bytes, truncated: false });
|
|
8952
|
+
continue;
|
|
8953
|
+
}
|
|
8954
|
+
const controller = new AbortController();
|
|
8955
|
+
activefetches.set(exchange.id, controller);
|
|
8956
|
+
try {
|
|
8957
|
+
const response = await fetch(exchange.url, { method: "GET", credentials: "omit", signal: controller.signal });
|
|
8958
|
+
const text2 = await response.text();
|
|
8959
|
+
const mime = response.headers.get("content-type")?.split(";")[0] ?? "";
|
|
8960
|
+
const captured = capturebody({ ref: randomid(), runid: plan.id, exchange: { ...exchange, ...mime ? { mime } : {} }, body: text2, mime, filter, at: Date.now() });
|
|
8961
|
+
if ("refused" in captured) {
|
|
8962
|
+
captures.push({ correlationid: exchange.correlationid, ref: "", mime, bytes: 0, truncated: false, refused: captured.refused });
|
|
8963
|
+
continue;
|
|
8964
|
+
}
|
|
8965
|
+
void captures;
|
|
8966
|
+
await memory.addbody(captured.record);
|
|
8967
|
+
const headers = {};
|
|
8968
|
+
response.headers.forEach((value, name) => {
|
|
8969
|
+
headers[name] = value;
|
|
8970
|
+
});
|
|
8971
|
+
const entry = { correlationid: exchange.correlationid, status: response.status, headers: capturedheaders(headers, { allow: filter.mimes !== void 0 ? ["content-type", "content-length"] : ["content-type", "content-length"], redact: [] }), bytes: captured.record.bytes, ...mime ? { mime } : {}, bodyref: captured.record.ref, at: Date.now() };
|
|
8972
|
+
const paired = pairexchange({ ...exchange, ...mime ? { mime } : {} }, entry);
|
|
8973
|
+
await memory.addexchange(paired);
|
|
8974
|
+
captures.push({ correlationid: exchange.correlationid, ref: captured.record.ref, mime, bytes: captured.record.bytes, truncated: captured.truncated });
|
|
8975
|
+
} finally {
|
|
8976
|
+
controller.abort();
|
|
8977
|
+
activefetches.delete(exchange.id);
|
|
8978
|
+
}
|
|
8979
|
+
}
|
|
8980
|
+
const refused = captures.filter((capture) => capture.refused !== void 0).length;
|
|
8981
|
+
const truncated = captures.filter((capture) => capture.truncated).length;
|
|
8982
|
+
await refreshbadge();
|
|
8983
|
+
await audit("watch", `Captured ${captures.length - refused} response bod${captures.length - refused === 1 ? "y" : "ies"} inside the reviewed byte ceiling${filter.ceiling !== void 0 ? ` of ${filter.ceiling} byte${filter.ceiling === 1 ? "" : "s"}` : ""} with ${truncated} truncated and ${refused} refused${refused > 0 ? " outside the grants" : ""}; body bytes stay out of the audit trail and derive from fresh reviewed fetches of the matched urls.`, extra);
|
|
8984
|
+
return { ok: true, summary: `Captured ${captures.length - refused} response bod${captures.length - refused === 1 ? "y" : "ies"} inside the reviewed byte ceiling.`, details: { network: { exchanges: matched.length, channelstate: "none", messages: 0 }, bodies: captures, derivation: "The page timing buffers expose no body bytes, so each captured body comes from a fresh reviewed fetch of the matched url through the extension context." } };
|
|
8985
|
+
}
|
|
8986
|
+
if (step.kind === "mapapi") {
|
|
8987
|
+
const exchanges = await memory.listexchanges({ runid: plan.id });
|
|
8988
|
+
const bodies = await memory.getbodies();
|
|
8989
|
+
const ranked = rankapis(apientries(exchanges, bodies));
|
|
8990
|
+
const limit = typeof options.limit === "number" && Number.isInteger(options.limit) && options.limit >= 1 ? options.limit : void 0;
|
|
8991
|
+
const chosen = limit !== void 0 ? ranked.slice(0, limit) : ranked;
|
|
8992
|
+
for (const entry of chosen) await memory.setapimap(entry.origin, chosen.filter((item) => item.origin === entry.origin));
|
|
8993
|
+
await audit("observation", `Mapped ${chosen.length} page api endpoint${chosen.length === 1 ? "" : "s"} of the run from ${exchanges.length} observed exchange${exchanges.length === 1 ? "" : "s"} ranked by frequency, json share and payload stability.`, extra);
|
|
8994
|
+
return { ok: true, summary: `Mapped ${chosen.length} page api endpoint${chosen.length === 1 ? "" : "s"} of the run.`, details: { network: { exchanges: exchanges.length, channelstate: "none", messages: 0 }, apimap: chosen } };
|
|
8995
|
+
}
|
|
8996
|
+
if (step.kind === "extractapi") {
|
|
8997
|
+
const spec = apireplayspecof(options.replay);
|
|
8998
|
+
if (!spec) throw new Error("A reviewed replay spec with an endpoint is required in options.replay.");
|
|
8999
|
+
const gate = origincheck(session, spec.endpoint);
|
|
9000
|
+
if (!gate.allowed) throw new Error(gate.reason ?? "The replay endpoint stays outside the session origin grants.");
|
|
9001
|
+
const url = replayurl(spec);
|
|
9002
|
+
const verb = spec.verb ?? "GET";
|
|
9003
|
+
const controller = new AbortController();
|
|
9004
|
+
activefetches.set(step.id, controller);
|
|
9005
|
+
try {
|
|
9006
|
+
const started = Date.now();
|
|
9007
|
+
const response = await fetch(url, { method: verb, credentials: "omit", signal: controller.signal });
|
|
9008
|
+
const text2 = await response.text();
|
|
9009
|
+
const mime = response.headers.get("content-type")?.split(";")[0] ?? "";
|
|
9010
|
+
const headers = {};
|
|
9011
|
+
response.headers.forEach((value, name) => {
|
|
9012
|
+
headers[name] = value;
|
|
9013
|
+
});
|
|
9014
|
+
const exchange = { id: randomid(), runid: plan.id, stepid: step.id, correlationid: correlationid(plan.id, (await memory.getexchanges()).length + 1), url, origin: new URL(url).origin, method: verb, requestheaders: {}, responseheaders: headers, status: response.status, statusclass: response.status >= 200 && response.status < 300 ? "success" : response.status >= 400 && response.status < 500 ? "clienterror" : response.status >= 500 ? "servererror" : "unknown", source: "extension", timing: Date.now() - started, bytes: text2.length, ...mime ? { mime } : {}, at: Date.now() };
|
|
9015
|
+
await memory.addexchange(exchange);
|
|
9016
|
+
await memory.setprogress(recordexchange(await memory.getprogress(), plan.id, step.id, { id: exchange.id, correlationid: exchange.correlationid, method: exchange.method, origin: exchange.origin, url: exchange.url, status: exchange.status, statusclass: exchange.statusclass, duration: exchange.timing, bytes: exchange.bytes }, Date.now()));
|
|
9017
|
+
const fields = extractvalues(text2, spec.paths ?? []);
|
|
9018
|
+
await refreshbadge();
|
|
9019
|
+
await audit("replay", `Replayed the captured endpoint ${spec.endpoint} with ${verb} as exchange ${exchange.correlationid} ending in the ${response.status} ${exchange.statusclass} class and mapped ${fields.length} extraction path${fields.length === 1 ? "" : "s"}; body bytes stay out of the audit trail.`, extra);
|
|
9020
|
+
return { ok: exchange.statusclass === "success", summary: `The replay of ${spec.endpoint} ended in the ${response.status} ${exchange.statusclass} class with ${fields.length} extracted field${fields.length === 1 ? "" : "s"}.`, details: { network: { exchanges: 1, channelstate: "none", messages: 0 }, replay: { endpoint: spec.endpoint, verb, url, overrides: spec.overrides ?? {}, fields } } };
|
|
9021
|
+
} finally {
|
|
9022
|
+
controller.abort();
|
|
9023
|
+
activefetches.delete(step.id);
|
|
9024
|
+
}
|
|
9025
|
+
}
|
|
9026
|
+
void origin;
|
|
9027
|
+
throw new Error("Unsupported request observation kind.");
|
|
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
|
+
}
|
|
7255
9376
|
async function enforcewindowreview(step, session, plan) {
|
|
7256
9377
|
const windowid = step.value && /^\d+$/.test(step.value) ? Number.parseInt(step.value, 10) : 0;
|
|
7257
9378
|
const progress = plan ? await memory.getprogress() : void 0;
|
|
@@ -7294,8 +9415,11 @@ async function refreshbadge() {
|
|
|
7294
9415
|
const media = (await memory.getmediarecords()).length + (await memory.getimagebatches()).length;
|
|
7295
9416
|
const recordingprompts = (await memory.getrecordingconsents()).filter((record2) => record2.approved === void 0).length;
|
|
7296
9417
|
const fetchprompts = (await memory.getfetchconsents()).filter((consent) => consent.approved === void 0).length;
|
|
9418
|
+
const observedrequests = (await memory.getexchanges()).length;
|
|
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);
|
|
7297
9421
|
const tasktabs2 = new Set(badges.map((badge) => badge.tabid)).size;
|
|
7298
|
-
const total = (queues?.prefetch ?? 0) + (queues?.batchopen ?? 0) + tasktabs2 + prompts + consents + quarantined + datasets + captures + media + recordingprompts + fetchprompts;
|
|
9422
|
+
const total = (queues?.prefetch ?? 0) + (queues?.batchopen ?? 0) + tasktabs2 + prompts + consents + quarantined + datasets + captures + media + recordingprompts + fetchprompts + observedrequests + livechannels + activerulescount;
|
|
7299
9423
|
await chrome.action.setBadgeText({ text: total > 0 ? String(total) : "" }).catch(() => {
|
|
7300
9424
|
});
|
|
7301
9425
|
}
|
|
@@ -7305,7 +9429,9 @@ async function executestep(stepid) {
|
|
|
7305
9429
|
const { tab, origin } = await activecontext();
|
|
7306
9430
|
const step = plan?.steps.find((candidate) => candidate.id === stepid);
|
|
7307
9431
|
if (!step) throw new Error("Reviewed step was not found.");
|
|
7308
|
-
const
|
|
9432
|
+
const settings = await memory.getsettings();
|
|
9433
|
+
const verdicts = await memory.getsafeties();
|
|
9434
|
+
const gate = canexecute({ session, plan, step, tabid: tab.id, origin, ...verdicts.length > 0 ? { verdicts } : {}, ...settings !== void 0 ? { settings } : {} });
|
|
7309
9435
|
if (!gate.allowed) throw new Error(gate.reason);
|
|
7310
9436
|
const capability = requiredcapability(step.kind);
|
|
7311
9437
|
if (capability) {
|
|
@@ -7355,6 +9481,12 @@ async function executestep(stepid) {
|
|
|
7355
9481
|
output = await executemediastep(step, session, plan, tab.id, origin);
|
|
7356
9482
|
} else if (ishttpkind(step.kind)) {
|
|
7357
9483
|
output = await executehttpstep(step, session, plan, tab.id, origin);
|
|
9484
|
+
} else if (issocketkind(step.kind)) {
|
|
9485
|
+
output = await executesocketstep(step, session, plan, tab.id, origin);
|
|
9486
|
+
} else if (isnetwatchkind(step.kind)) {
|
|
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);
|
|
7358
9490
|
} else {
|
|
7359
9491
|
if (step.target && freshcheckkinds.has(step.kind)) {
|
|
7360
9492
|
const fresh = await snapshot(tab.id);
|
|
@@ -7406,6 +9538,12 @@ async function executestep(stepid) {
|
|
|
7406
9538
|
if (iscomplete(tracked, plan) && plan.state === "approved") {
|
|
7407
9539
|
await stoprecordingsforrun(plan.id).catch(() => {
|
|
7408
9540
|
});
|
|
9541
|
+
await closechannelsforrun(plan.id).catch(() => {
|
|
9542
|
+
});
|
|
9543
|
+
await cancelauthflowsforrun(plan.id, "plan completion").catch(() => {
|
|
9544
|
+
});
|
|
9545
|
+
await revertcontrolsforrun(plan.id, "plan completion").catch(() => {
|
|
9546
|
+
});
|
|
7409
9547
|
const done = { ...plan, state: "completed", completedat: Date.now() };
|
|
7410
9548
|
await memory.setplan(done);
|
|
7411
9549
|
await audit("complete", "Every reviewed step of the approved plan has executed.", { ...session ? { sessionid: session.id } : {}, planid: done.id });
|
|
@@ -7558,9 +9696,17 @@ async function handlerequest(message, sender) {
|
|
|
7558
9696
|
void body;
|
|
7559
9697
|
return metadata;
|
|
7560
9698
|
});
|
|
9699
|
+
const exchanges = await memory.getexchanges();
|
|
9700
|
+
const channels = await memory.getchannels();
|
|
9701
|
+
const subscriptions = await memory.getsubscriptions();
|
|
9702
|
+
const apimap = await memory.getapimap();
|
|
9703
|
+
const messagecount = (await memory.getmessages()).length;
|
|
7561
9704
|
const endpoints = await memory.getendpoints();
|
|
7562
9705
|
const fetchconsents = await memory.getfetchconsents();
|
|
7563
|
-
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" }));
|
|
7564
9710
|
const runsettings = await memory.getsettings();
|
|
7565
9711
|
const scanhooks = [];
|
|
7566
9712
|
for (const hook of await memory.getscanhooks()) {
|
|
@@ -7572,7 +9718,7 @@ async function handlerequest(message, sender) {
|
|
|
7572
9718
|
const livetab = session ? await chrome.tabs.get(session.tabid).catch(() => void 0) : void 0;
|
|
7573
9719
|
const waitprofile = session ? waitprofiles.find((record2) => record2.origin === session.origin) : void 0;
|
|
7574
9720
|
const livestate = { phase: livetab?.status === "loading" ? "loading" : "complete", ...navrecords[0] ? { finalurl: navrecords[0].finalurl, redirects: navrecords[0].chain } : {} };
|
|
7575
|
-
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, ...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()] } : {} };
|
|
7576
9722
|
}
|
|
7577
9723
|
case "capabilities":
|
|
7578
9724
|
return refreshcapabilities();
|
|
@@ -7616,7 +9762,8 @@ async function handlerequest(message, sender) {
|
|
|
7616
9762
|
const resolved = outcome.details?.resolvedtarget;
|
|
7617
9763
|
const capture = outcome.details?.capture;
|
|
7618
9764
|
const media = outcome.details?.media;
|
|
7619
|
-
|
|
9765
|
+
const network = outcome.details?.network;
|
|
9766
|
+
return JSON.parse(outcomeresponse({ outcome, plan, ...resolved ? { resolvedtarget: resolved } : {}, ...capture ? { capture } : {}, ...media ? { media } : {}, ...network ? { network } : {} }));
|
|
7620
9767
|
}
|
|
7621
9768
|
case "map": {
|
|
7622
9769
|
const plan = await memory.getplan();
|
|
@@ -8184,12 +10331,12 @@ async function handlerequest(message, sender) {
|
|
|
8184
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.");
|
|
8185
10332
|
if (!inputkey.header?.trim()) throw new Error("A reviewed header name is required for the api key.");
|
|
8186
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.");
|
|
8187
|
-
const
|
|
8188
|
-
await memory.setapikey(
|
|
8189
|
-
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);
|
|
8190
10337
|
const session = await memory.getsession();
|
|
8191
|
-
await audit("call", `Stored the api key
|
|
8192
|
-
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 };
|
|
8193
10340
|
}
|
|
8194
10341
|
case "deleteapikey": {
|
|
8195
10342
|
const inputdeletekey = message;
|
|
@@ -8212,12 +10359,125 @@ async function handlerequest(message, sender) {
|
|
|
8212
10359
|
await audit("call", `The review panel exported ${report.calls.length} call record${report.calls.length === 1 ? "" : "s"} through the reviewed download flow with every response body held back.`, { sessionid: session.id });
|
|
8213
10360
|
return { exported: report.calls.length };
|
|
8214
10361
|
}
|
|
10362
|
+
case "setwebrequestgrant": {
|
|
10363
|
+
const inputgrant = message;
|
|
10364
|
+
const settings = await memory.getsettings();
|
|
10365
|
+
await memory.setsettings({ ...settings, webrequestgrant: inputgrant.granted === true });
|
|
10366
|
+
await audit("configure", `The user ${inputgrant.granted === true ? "granted" : "revoked"} request watching; the observation derives from the page timing buffers and the manifest permissions stay unchanged.`);
|
|
10367
|
+
return { webrequestgrant: inputgrant.granted === true };
|
|
10368
|
+
}
|
|
10369
|
+
case "setbodyretention": {
|
|
10370
|
+
const inputretention = message;
|
|
10371
|
+
const settings = await memory.getsettings();
|
|
10372
|
+
const retention = typeof inputretention.retention === "number" && Number.isInteger(inputretention.retention) && inputretention.retention >= 0 ? inputretention.retention : void 0;
|
|
10373
|
+
await memory.setsettings({ ...settings, ...retention !== void 0 ? { bodyretention: retention } : {} });
|
|
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.`);
|
|
10375
|
+
return { bodyretention: retention };
|
|
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
|
+
}
|
|
10425
|
+
case "netreport": {
|
|
10426
|
+
const plan = await memory.getplan();
|
|
10427
|
+
if (!plan) throw new Error("No plan is available for a network envelope.");
|
|
10428
|
+
return exchangesreport({ exchanges: await memory.getexchanges(), channels: await memory.getchannels(), subscriptions: await memory.getsubscriptions(), apimap: await memory.getapimap() });
|
|
10429
|
+
}
|
|
10430
|
+
case "exchangebody": {
|
|
10431
|
+
const inputbody = message;
|
|
10432
|
+
const record2 = await memory.getbody(inputbody.ref ?? "");
|
|
10433
|
+
if (!record2) throw new Error(`No captured body matches ${inputbody.ref ?? ""}.`);
|
|
10434
|
+
if (record2.body === void 0 || record2.bodyexpired) throw new Error("The captured body expired from the retention window; capture it again.");
|
|
10435
|
+
return { ref: record2.ref, mime: record2.mime, bytes: record2.bytes, body: record2.body };
|
|
10436
|
+
}
|
|
10437
|
+
case "closesocket": {
|
|
10438
|
+
const inputclose = message;
|
|
10439
|
+
const active = activesockets.get(inputclose.id ?? "");
|
|
10440
|
+
if (!active) throw new Error(`No live socket or stream matches ${inputclose.id ?? ""}.`);
|
|
10441
|
+
active.cancelled = true;
|
|
10442
|
+
active.controller?.abort();
|
|
10443
|
+
try {
|
|
10444
|
+
active.socket?.close(1e3);
|
|
10445
|
+
} catch {
|
|
10446
|
+
}
|
|
10447
|
+
activesockets.delete(inputclose.id ?? "");
|
|
10448
|
+
if (active.channel) {
|
|
10449
|
+
const closed = closechannel(active.channel, Date.now());
|
|
10450
|
+
await memory.addchannel(closed);
|
|
10451
|
+
await audit("socket", `Channel ${active.channel.id} of ${active.channel.origin} closed from the review panel after ${active.channel.sent} sent and ${active.channel.received} received message${active.channel.received === 1 ? "" : "s"}.`, { stepid: active.channel.stepid });
|
|
10452
|
+
}
|
|
10453
|
+
if (active.subscription) {
|
|
10454
|
+
const closed = { ...active.subscription, state: "closed", closedat: Date.now() };
|
|
10455
|
+
await memory.setsubscription(closed);
|
|
10456
|
+
await audit("socket", `Event subscription ${active.subscription.id} of ${active.subscription.origin} cancelled from the review panel after ${closed.events} observed event${closed.events === 1 ? "" : "s"}.`, { stepid: active.subscription.stepid });
|
|
10457
|
+
}
|
|
10458
|
+
await refreshbadge();
|
|
10459
|
+
return { closed: true, id: inputclose.id ?? "" };
|
|
10460
|
+
}
|
|
8215
10461
|
case "stop": {
|
|
8216
10462
|
const session = await memory.getsession();
|
|
8217
10463
|
for (const [id, controller] of [...activefetches.entries()]) {
|
|
8218
10464
|
controller.abort();
|
|
8219
10465
|
activefetches.delete(id);
|
|
8220
10466
|
}
|
|
10467
|
+
const stoppedplan = await memory.getplan();
|
|
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
|
+
}
|
|
8221
10481
|
for (const [id, active] of [...activerecordings.entries()]) {
|
|
8222
10482
|
const finished = finishrecording(active.record, Date.now());
|
|
8223
10483
|
await memory.addmedia(finished).catch(() => {
|