@wenathlan/extension 1.1.41 → 1.1.42
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -4
- package/dist/httpclient.d.ts +158 -0
- package/dist/httpclient.d.ts.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +591 -5
- package/dist/index.js.map +4 -4
- package/dist/memory.d.ts +32 -1
- package/dist/memory.d.ts.map +1 -1
- package/dist/policy.d.ts +25 -0
- package/dist/policy.d.ts.map +1 -1
- package/dist/protocol.d.ts +21 -4
- package/dist/protocol.d.ts.map +1 -1
- package/dist/types.d.ts +131 -3
- package/dist/types.d.ts.map +1 -1
- package/dist/version.d.ts +1 -1
- package/extension/dist/background.js +884 -9
- package/extension/dist/background.js.map +4 -4
- package/extension/dist/manifest.json +1 -1
- package/extension/dist/pagebridge.js +12 -3
- package/extension/dist/pagebridge.js.map +2 -2
- package/extension/dist/popup.html +1 -1
- package/extension/dist/popup.js +10 -2
- package/extension/dist/popup.js.map +2 -2
- package/extension/dist/sidepanel.html +1 -1
- package/extension/dist/sidepanel.js +149 -2
- package/extension/dist/sidepanel.js.map +3 -3
- package/extension/dist/style.css +2 -0
- package/extension/manifest.json +1 -1
- package/package.json +1 -1
|
@@ -813,6 +813,79 @@ var sessionmemory = class {
|
|
|
813
813
|
async getrecordingconsents() {
|
|
814
814
|
return await this.adapter.get("recordingconsents") ?? [];
|
|
815
815
|
}
|
|
816
|
+
/** Stores one outbound call record with its transport facts and body, replacing the previous record of that id; the user configured call retention window expires the oldest bodies while the metadata always survives. */
|
|
817
|
+
async addcall(record2) {
|
|
818
|
+
const records = await this.getcalls();
|
|
819
|
+
const retention = (await this.getsettings())?.callretention;
|
|
820
|
+
const combined = [record2, ...records.filter((item) => item.id !== record2.id)];
|
|
821
|
+
const stored = retention === void 0 ? combined : combined.map((item, index) => index < retention ? item : expirecallbody(item));
|
|
822
|
+
await this.adapter.set("calls", stored);
|
|
823
|
+
}
|
|
824
|
+
/** Returns every stored outbound call record, newest first. */
|
|
825
|
+
async getcalls() {
|
|
826
|
+
return await this.adapter.get("calls") ?? [];
|
|
827
|
+
}
|
|
828
|
+
/** Returns one outbound call record with its body by its id. */
|
|
829
|
+
async getcall(id) {
|
|
830
|
+
return (await this.getcalls()).find((item) => item.id === id);
|
|
831
|
+
}
|
|
832
|
+
/** Returns the outbound call records filtered by run and origin; an absent filter returns every call. */
|
|
833
|
+
async listcalls(filter) {
|
|
834
|
+
const records = await this.getcalls();
|
|
835
|
+
return records.filter((item) => (filter.runid === void 0 || item.runid === filter.runid) && (filter.origin === void 0 || item.origin === filter.origin));
|
|
836
|
+
}
|
|
837
|
+
/** Stores one typed endpoint definition version, appending to the version history of that endpoint name. */
|
|
838
|
+
async setendpoint(record2) {
|
|
839
|
+
const records = await this.adapter.get("endpoints") ?? [];
|
|
840
|
+
const prior = records.filter((item) => item.name === record2.name);
|
|
841
|
+
const version = prior.length > 0 ? Math.max(...prior.map((item) => item.version)) + 1 : 1;
|
|
842
|
+
await this.adapter.set("endpoints", [{ ...record2, version, at: record2.at }, ...records]);
|
|
843
|
+
}
|
|
844
|
+
/** Returns the newest endpointrecord definition of one name with its payload schema and version. */
|
|
845
|
+
async getendpoint(name) {
|
|
846
|
+
const records = await this.adapter.get("endpoints") ?? [];
|
|
847
|
+
return records.find((item) => item.name === name);
|
|
848
|
+
}
|
|
849
|
+
/** Returns the newest definition of every typed endpoint name with its schema and version history. */
|
|
850
|
+
async getendpoints() {
|
|
851
|
+
const records = await this.adapter.get("endpoints") ?? [];
|
|
852
|
+
const latest = /* @__PURE__ */ new Map();
|
|
853
|
+
for (const record2 of records) if (!latest.has(record2.name)) latest.set(record2.name, record2);
|
|
854
|
+
return [...latest.values()];
|
|
855
|
+
}
|
|
856
|
+
/** Stores one fetch consent decision per origin with its reviewed header names and values and its expiry window. */
|
|
857
|
+
async setfetchconsent(consent) {
|
|
858
|
+
const records = (await this.adapter.get("fetchconsents") ?? []).filter((item) => item.id !== consent.id);
|
|
859
|
+
await this.adapter.set("fetchconsents", [consent, ...records]);
|
|
860
|
+
}
|
|
861
|
+
/** Returns every fetch consent decision with its origin, header names and expiry window, newest first. */
|
|
862
|
+
async getfetchconsents() {
|
|
863
|
+
return await this.adapter.get("fetchconsents") ?? [];
|
|
864
|
+
}
|
|
865
|
+
/** Stores one api key reference record without any key material; the secret value stays behind its storage id. */
|
|
866
|
+
async setapikey(ref) {
|
|
867
|
+
const records = (await this.adapter.get("apikeys") ?? []).filter((item) => item.name !== ref.name);
|
|
868
|
+
await this.adapter.set("apikeys", [ref, ...records]);
|
|
869
|
+
}
|
|
870
|
+
/** Returns every stored api key reference with its origin scope, header name and storage id; key material never loads here. */
|
|
871
|
+
async getapikeys() {
|
|
872
|
+
return await this.adapter.get("apikeys") ?? [];
|
|
873
|
+
}
|
|
874
|
+
/** Removes one api key reference and its stored secret together. */
|
|
875
|
+
async removeapikey(name) {
|
|
876
|
+
const records = await this.getapikeys();
|
|
877
|
+
const ref = records.find((item) => item.name === name);
|
|
878
|
+
if (ref) await this.adapter.set(ref.storageid, void 0);
|
|
879
|
+
await this.adapter.set("apikeys", records.filter((item) => item.name !== name));
|
|
880
|
+
}
|
|
881
|
+
/** Stores one api key secret under its storage id; the value never appears in reports, outcomes or the audit trail. */
|
|
882
|
+
async setsecret(storageid, value) {
|
|
883
|
+
return this.adapter.set(storageid, value);
|
|
884
|
+
}
|
|
885
|
+
/** Loads one api key secret under its storage id for the executor only. */
|
|
886
|
+
async getsecret(storageid) {
|
|
887
|
+
return this.adapter.get(storageid);
|
|
888
|
+
}
|
|
816
889
|
};
|
|
817
890
|
function mediakindof(record2) {
|
|
818
891
|
if ("pages" in record2) return "pdf";
|
|
@@ -842,14 +915,19 @@ function expirecapturebytes(record2) {
|
|
|
842
915
|
void bytes;
|
|
843
916
|
return { ...metadata, bytesexpired: true };
|
|
844
917
|
}
|
|
918
|
+
function expirecallbody(record2) {
|
|
919
|
+
const { body, ...metadata } = record2;
|
|
920
|
+
void body;
|
|
921
|
+
return { ...metadata, bodyexpired: true };
|
|
922
|
+
}
|
|
845
923
|
function randomid() {
|
|
846
924
|
return crypto.randomUUID();
|
|
847
925
|
}
|
|
848
926
|
|
|
849
927
|
// policy.ts
|
|
850
|
-
var sensitiveactions = /* @__PURE__ */ new Set(["click", "type", "navigate", "select", "presskey", "drag", "drop", "upload", "clear", "check", "uncheck", "toggle", "submit", "reload", "back", "forward", "writestorage", "setattribute", "removeattribute", "evaluate", "tabcreate", "tabactivate", "tabclose", "tabreload", "windowcreate", "windowclose", "windowresize", "downloadfile", "clickpoint", "shiftclick", "dismissdialog", "enterframe", "typetime", "appendtext", "setvalue", "typeedit", "keyhold", "keyrelease", "submitsearch", "selectmulti", "chooseradio", "setslider", "setdate", "setcolor", "openlink", "openprivate", "reloadcache", "stopnav", "followlink", "spanav", "rewritequery", "setfragment", "navlist", "navprofile", "handleauth", "printpdf", "prefetch", "preconnect", "deeplink", "reopentab", "pausenav", "navrate", "openclipboard", "batchopen", "duplicatetab", "closepattern", "pintab", "mutetab", "movetab", "movetabwindow", "grouptabs", "colorgroup", "collapsegroup", "discardtab", "reloadtabs", "zoomin", "zoomout", "switchtab", "maximizewindow", "minimizewindow", "restorewindow", "focuswindow", "scratchwindow", "incognitowindow", "restoretab", "restorelayout", "reopenrun", "badgetab", "fillform", "filllabel", "fillplaceholder", "submitform", "retryform", "runwizard", "selectchain", "picktypeahead", "pickdate", "attachfile", "fillcard", "fillcode", "consentpassword", "exportcsv", "exportjson", "exportexcel", "copytable", "pushsheets", "streamdisk", "paginateextract", "resumeextract", "batchdownload", "pausedownload", "resumedownload", "interceptmime", "readclipboard", "writeclipboard", "copyscreen", "quarantinedownload", "scanvirus", "cleanupartifacts", "recordscreen", "captureaudio", "downloadimages"]);
|
|
928
|
+
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"]);
|
|
851
929
|
var interactionactions = /* @__PURE__ */ new Set(["focus", "scroll", "hover", "clickdeep", "rightclick", "doubleclick", "scrollpage", "scrollby", "scrollend", "scrolltop", "fullscreen", "zoomset", "movepointer", "clicktext", "clickaria", "clickname", "expanddetails", "pierceshadow", "retryaction"]);
|
|
852
|
-
var readactions = /* @__PURE__ */ new Set(["observe", "inspect", "extract", "wait", "waitfor", "waittext", "readattribute", "readstyle", "readgeometry", "readvalue", "readtext", "readhtml", "countelements", "readtable", "readlinks", "readimages", "readmeta", "readforms", "readstorage", "highlight", "tablist", "windowlist", "tabsnapshot", "mapclicks", "verifyvisible", "verifyenabled", "resolvexpath", "a11ytree", "readvisible", "readertree", "detectlists", "detecttables", "readjson", "watchmutate", "waitquiet", "watchbanner", "detectinfinitescroll", "detectvirtual", "detectlazy", "readscrollpos", "readlang", "readoutline", "countpages", "listshadow", "listframes", "classifypage", "fingerprintsection", "diffsnapshots", "readselection", "watchfocus", "detectsticky", "detectscrolllock", "readopengraph", "detectlanguage", "deriveselector", "waitload", "waiturl", "spawait", "detecthttp", "readredirects", "readfinalurl", "trailaudit", "navintent", "checksafe", "querytabs", "watchtab", "findclones", "searchtabs", "listaudio", "snapshotsession", "savelayout", "attachmeta", "detectfields", "generatevalues", "saveprofiles", "asksubmit", "readerrors", "skiphoneypot", "detectlogin", "detecttemplate", "handoffcaptcha", "scrapetable", "importcsv", "looprows", "transformvalues", "deduperows", "mergepages", "stamplerows", "previewgrid", "logprovenance", "verifydownload", "exportnetlog", "namecaptures", "shotview", "shotfullpage", "shotelement", "shotregion", "contactsheet", "capturepdf", "captureframe", "readmedia", "readassets", "probestream", "timelapse", "shotcanvas", "convertimage", "makethumbs"]);
|
|
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"]);
|
|
853
931
|
var allowedactions = /* @__PURE__ */ new Set([...sensitiveactions, ...interactionactions, ...readactions]);
|
|
854
932
|
var watchactions = /* @__PURE__ */ new Set(["watchmutate", "watchbanner", "watchfocus", "watchtab"]);
|
|
855
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"]);
|
|
@@ -861,6 +939,8 @@ var exportactions = /* @__PURE__ */ new Set(["exportcsv", "exportjson", "exporte
|
|
|
861
939
|
var filesactions = /* @__PURE__ */ new Set(["batchdownload", "pausedownload", "resumedownload", "verifydownload", "interceptmime", "exportnetlog", "readclipboard", "writeclipboard", "copyscreen", "quarantinedownload", "scanvirus", "namecaptures", "cleanupartifacts"]);
|
|
862
940
|
var captureactions = /* @__PURE__ */ new Set(["shotview", "shotfullpage", "shotelement", "shotregion", "contactsheet"]);
|
|
863
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"]);
|
|
864
944
|
var fieldkinds = ["text", "email", "phone", "date", "number", "select", "check", "radio", "file", "password", "card", "code"];
|
|
865
945
|
var layoutmutationactions = /* @__PURE__ */ new Set(["grouptabs", "colorgroup", "collapsegroup", "savelayout", "restorelayout"]);
|
|
866
946
|
var groupcolors = ["grey", "blue", "red", "yellow", "green", "pink", "purple", "cyan", "orange"];
|
|
@@ -1590,6 +1670,223 @@ function validatetabsgrammar(step, options) {
|
|
|
1590
1670
|
function ismediakind(kind) {
|
|
1591
1671
|
return mediaactions.has(kind);
|
|
1592
1672
|
}
|
|
1673
|
+
function ishttpkind(kind) {
|
|
1674
|
+
return httpactions.has(kind);
|
|
1675
|
+
}
|
|
1676
|
+
function origincheck(session, url) {
|
|
1677
|
+
let parsed;
|
|
1678
|
+
try {
|
|
1679
|
+
parsed = new URL(url);
|
|
1680
|
+
} catch {
|
|
1681
|
+
return { allowed: false, reason: "The outbound request needs a valid url before it can be reviewed." };
|
|
1682
|
+
}
|
|
1683
|
+
if (parsed.protocol !== "https:") return { allowed: false, reason: "Outbound requests use HTTPS urls only." };
|
|
1684
|
+
if (parsed.username || parsed.password) return { allowed: false, reason: "Endpoint credentials are not allowed in the url." };
|
|
1685
|
+
if (!origingranted(session, parsed.origin)) return { allowed: false, reason: `The outbound request to ${parsed.origin} stays outside the session origin grants.` };
|
|
1686
|
+
return { allowed: true };
|
|
1687
|
+
}
|
|
1688
|
+
function credentialheadername(name) {
|
|
1689
|
+
return credentialheaders.has(name.trim().toLowerCase());
|
|
1690
|
+
}
|
|
1691
|
+
function fetchconsentrefgranted(step) {
|
|
1692
|
+
let options = {};
|
|
1693
|
+
try {
|
|
1694
|
+
options = parseoptions(step);
|
|
1695
|
+
} catch {
|
|
1696
|
+
options = {};
|
|
1697
|
+
}
|
|
1698
|
+
const request = options.fetch;
|
|
1699
|
+
const headers = request && typeof request === "object" && !Array.isArray(request) ? request.headers : void 0;
|
|
1700
|
+
const names = headers && typeof headers === "object" && !Array.isArray(headers) ? Object.keys(headers) : [];
|
|
1701
|
+
if (names.length === 0) return { allowed: true };
|
|
1702
|
+
const empty = names.some((name) => !name.trim());
|
|
1703
|
+
if (empty) return { allowed: false, reason: "Header allowlists with empty names are refused." };
|
|
1704
|
+
const credential = names.find((name) => credentialheadername(name));
|
|
1705
|
+
if (credential !== void 0 && !isnonempty(options.consentref)) return { allowed: false, reason: `The credential bearing header ${credential} needs the explicit reviewed consent that names it before it is sent.` };
|
|
1706
|
+
if (!isnonempty(options.consentref)) return { allowed: false, reason: `The ${names.length} reviewed custom header${names.length === 1 ? "" : "s"} need a reviewed consent ref in options before any send.` };
|
|
1707
|
+
return { allowed: true };
|
|
1708
|
+
}
|
|
1709
|
+
function fetchconsentcovers(consent, origin, headernames, now) {
|
|
1710
|
+
if (consent.approved !== true) return false;
|
|
1711
|
+
if (consent.expiresat <= now) return false;
|
|
1712
|
+
if (consent.origin !== origin) return false;
|
|
1713
|
+
const covered = new Set(consent.headers.map((header) => header.name.trim().toLowerCase()));
|
|
1714
|
+
return headernames.every((name) => covered.has(name.trim().toLowerCase()));
|
|
1715
|
+
}
|
|
1716
|
+
function mutationcallof(step) {
|
|
1717
|
+
let options = {};
|
|
1718
|
+
try {
|
|
1719
|
+
options = parseoptions(step);
|
|
1720
|
+
} catch {
|
|
1721
|
+
options = {};
|
|
1722
|
+
}
|
|
1723
|
+
if (step.kind === "callgraphql") {
|
|
1724
|
+
const request = options.graphql;
|
|
1725
|
+
return Boolean(request && typeof request === "object" && !Array.isArray(request) && request.operationkind === "mutation");
|
|
1726
|
+
}
|
|
1727
|
+
if (step.kind === "callrest") {
|
|
1728
|
+
const method = typeof options.method === "string" ? options.method.trim().toUpperCase() : void 0;
|
|
1729
|
+
if (method !== void 0) return !["GET", "HEAD", "OPTIONS"].includes(method);
|
|
1730
|
+
}
|
|
1731
|
+
return false;
|
|
1732
|
+
}
|
|
1733
|
+
function fetchbudgetallowed(timeout, retries, backoff, wait) {
|
|
1734
|
+
for (const [label, value] of [["timeout", timeout], ["retries", retries], ["backoff", backoff]]) {
|
|
1735
|
+
if (value !== void 0 && (typeof value !== "number" || !Number.isFinite(value) || value < 0)) return { allowed: false, reason: `The reviewed fetch ${label} must be zero or a positive number with no code ceiling.` };
|
|
1736
|
+
}
|
|
1737
|
+
if (wait !== void 0 && (typeof wait !== "number" || !Number.isFinite(wait) || wait < 0)) return { allowed: false, reason: "The reviewed fetch wait budget must be zero or a positive number of milliseconds." };
|
|
1738
|
+
if (wait === void 0 || timeout === void 0) return { allowed: true };
|
|
1739
|
+
const attempts = Math.max(1, Math.floor(retries ?? 0) + 1);
|
|
1740
|
+
const waits = (backoff ?? 0) * (attempts * (attempts - 1)) / 2;
|
|
1741
|
+
const worstcase = timeout * attempts + waits;
|
|
1742
|
+
if (worstcase > wait) return { allowed: false, reason: `The fetch worst case of ${worstcase} milliseconds exceeds the reviewed wait budget of ${wait} milliseconds; review a wider budget or fewer retries.` };
|
|
1743
|
+
return { allowed: true };
|
|
1744
|
+
}
|
|
1745
|
+
function outboundtarget(step) {
|
|
1746
|
+
let options = {};
|
|
1747
|
+
try {
|
|
1748
|
+
options = parseoptions(step);
|
|
1749
|
+
} catch {
|
|
1750
|
+
options = {};
|
|
1751
|
+
}
|
|
1752
|
+
const request = options.fetch;
|
|
1753
|
+
if (request && typeof request === "object" && !Array.isArray(request)) {
|
|
1754
|
+
const url = request.url;
|
|
1755
|
+
if (typeof url === "string" && url.trim()) return url.trim();
|
|
1756
|
+
}
|
|
1757
|
+
return void 0;
|
|
1758
|
+
}
|
|
1759
|
+
function validateendpointrecord(value) {
|
|
1760
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return { allowed: false, reason: "A reviewed endpoint record is required." };
|
|
1761
|
+
const record2 = value;
|
|
1762
|
+
if (!isnonempty(record2.name)) return { allowed: false, reason: "The endpoint record needs a reviewed non-empty name." };
|
|
1763
|
+
if (!isnonempty(record2.method)) return { allowed: false, reason: "The endpoint record needs a reviewed method." };
|
|
1764
|
+
if (!ishttpsurl(record2.url)) return { allowed: false, reason: "The endpoint record url template must be an HTTPS url." };
|
|
1765
|
+
if (record2.headers !== void 0) {
|
|
1766
|
+
if (!record2.headers || typeof record2.headers !== "object" || Array.isArray(record2.headers)) return { allowed: false, reason: "The endpoint header allowlist must be an object of reviewed headers." };
|
|
1767
|
+
for (const name of Object.keys(record2.headers)) {
|
|
1768
|
+
if (!name.trim()) return { allowed: false, reason: "Endpoint header allowlists with empty names are refused." };
|
|
1769
|
+
const headervalue = record2.headers[name];
|
|
1770
|
+
if (typeof headervalue !== "string") return { allowed: false, reason: `The endpoint header ${name} needs a reviewed string value.` };
|
|
1771
|
+
}
|
|
1772
|
+
}
|
|
1773
|
+
const schema = record2.schema;
|
|
1774
|
+
if (!schema || typeof schema !== "object" || Array.isArray(schema)) return { allowed: false, reason: "Every typed endpoint call needs a reviewed payload schema; endpoint records without schemas are refused." };
|
|
1775
|
+
const fields = schema.fields;
|
|
1776
|
+
if (!Array.isArray(fields) || fields.length === 0) return { allowed: false, reason: "The endpoint payload schema needs a non-empty field list." };
|
|
1777
|
+
for (const item of fields) {
|
|
1778
|
+
if (!item || typeof item !== "object" || Array.isArray(item)) return { allowed: false, reason: "Every payload schema field must be an object." };
|
|
1779
|
+
const field = item;
|
|
1780
|
+
if (!isnonempty(field.name)) return { allowed: false, reason: "Every payload schema field needs a non-empty name." };
|
|
1781
|
+
if (field.kind !== "string" && field.kind !== "number" && field.kind !== "boolean") return { allowed: false, reason: `The payload schema field ${field.name} must be a string, number or boolean kind.` };
|
|
1782
|
+
if (field.required !== void 0 && typeof field.required !== "boolean") return { allowed: false, reason: `The payload schema field ${field.name} required flag must be a boolean.` };
|
|
1783
|
+
if (field.default !== void 0 && typeof field.default !== "string" && typeof field.default !== "number" && typeof field.default !== "boolean") return { allowed: false, reason: `The payload schema field ${field.name} default must match its kind.` };
|
|
1784
|
+
}
|
|
1785
|
+
return { allowed: true };
|
|
1786
|
+
}
|
|
1787
|
+
function validpath(path) {
|
|
1788
|
+
return path.split(".").every((segment) => /^[A-Za-z0-9_-]+$/.test(segment));
|
|
1789
|
+
}
|
|
1790
|
+
function validatehttpgrammar(step, options) {
|
|
1791
|
+
const kind = step.kind;
|
|
1792
|
+
if (kind === "fetchurl") {
|
|
1793
|
+
const request = options.fetch;
|
|
1794
|
+
if (!request || typeof request !== "object" || Array.isArray(request)) return { allowed: false, reason: "A reviewed fetch request with a url is required in options.fetch." };
|
|
1795
|
+
const fetchrequest = request;
|
|
1796
|
+
if (typeof fetchrequest.url !== "string" || !fetchrequest.url.trim()) return { allowed: false, reason: "The reviewed fetch request needs a non-empty url." };
|
|
1797
|
+
if (fetchrequest.method !== void 0 && (typeof fetchrequest.method !== "string" || !["GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"].includes(fetchrequest.method.trim().toUpperCase()))) return { allowed: false, reason: "The reviewed fetch method must be a known HTTP verb." };
|
|
1798
|
+
if (fetchrequest.headers !== void 0) {
|
|
1799
|
+
if (!fetchrequest.headers || typeof fetchrequest.headers !== "object" || Array.isArray(fetchrequest.headers)) return { allowed: false, reason: "The reviewed header allowlist must be an object of custom headers." };
|
|
1800
|
+
for (const name of Object.keys(fetchrequest.headers)) {
|
|
1801
|
+
if (!name.trim()) return { allowed: false, reason: "Header allowlists with empty names are refused." };
|
|
1802
|
+
if (typeof fetchrequest.headers[name] !== "string") return { allowed: false, reason: `The reviewed header ${name} needs a string value.` };
|
|
1803
|
+
}
|
|
1804
|
+
}
|
|
1805
|
+
if (fetchrequest.body !== void 0 && typeof fetchrequest.body !== "string") return { allowed: false, reason: "The reviewed fetch body must be a string." };
|
|
1806
|
+
if (fetchrequest.mode !== void 0 && fetchrequest.mode !== "cors" && fetchrequest.mode !== "no-cors" && fetchrequest.mode !== "same-origin") return { allowed: false, reason: "The reviewed fetch mode must be cors, no-cors or same-origin." };
|
|
1807
|
+
const consentgate = fetchconsentrefgranted(step);
|
|
1808
|
+
if (!consentgate.allowed) return consentgate;
|
|
1809
|
+
const policycheck = validatefetchoptions(options.fetchoptions);
|
|
1810
|
+
if (!policycheck.allowed) return policycheck;
|
|
1811
|
+
const fetchpolicy = fetchoptionsvalues(options.fetchoptions);
|
|
1812
|
+
const budget = fetchbudgetallowed(fetchpolicy.timeout, fetchpolicy.retries, fetchpolicy.backoff, fetchnumeric(options, "wait"));
|
|
1813
|
+
if (!budget.allowed) return budget;
|
|
1814
|
+
if (options.stream !== void 0) {
|
|
1815
|
+
if (!options.stream || typeof options.stream !== "object" || Array.isArray(options.stream)) return { allowed: false, reason: "The reviewed stream window must be an object with an optional byte budget." };
|
|
1816
|
+
const streambudget = options.stream.budget;
|
|
1817
|
+
if (streambudget !== void 0 && (typeof streambudget !== "number" || !Number.isFinite(streambudget) || streambudget < 0)) return { allowed: false, reason: "The reviewed stream byte budget must be zero or a positive number of bytes with no code ceiling." };
|
|
1818
|
+
}
|
|
1819
|
+
}
|
|
1820
|
+
if (kind === "parsejson") {
|
|
1821
|
+
if (!isnonempty(options.call)) return { allowed: false, reason: "A reviewed stored call id is required in options.call before the body parses." };
|
|
1822
|
+
const fields = options.fields;
|
|
1823
|
+
if (!Array.isArray(fields) || fields.length === 0) return { allowed: false, reason: "A reviewed non-empty list of json path rules is required in options.fields." };
|
|
1824
|
+
for (const item of fields) {
|
|
1825
|
+
if (!item || typeof item !== "object" || Array.isArray(item)) return { allowed: false, reason: "Every json path rule must be an object." };
|
|
1826
|
+
const rule = item;
|
|
1827
|
+
if (!isnonempty(rule.name)) return { allowed: false, reason: "Every json path rule needs a non-empty field name." };
|
|
1828
|
+
if (typeof rule.path !== "string" || !rule.path.trim() || !validpath(rule.path.trim())) return { allowed: false, reason: `The json path of ${rule.name} must be a dotted path of non-empty segments.` };
|
|
1829
|
+
if (rule.kind !== void 0 && rule.kind !== "text" && rule.kind !== "number" && rule.kind !== "boolean" && rule.kind !== "json") return { allowed: false, reason: `The json path kind of ${rule.name} must be text, number, boolean or json.` };
|
|
1830
|
+
}
|
|
1831
|
+
}
|
|
1832
|
+
if (kind === "parsehtml") {
|
|
1833
|
+
if (!isnonempty(options.call)) return { allowed: false, reason: "A reviewed stored call id is required in options.call before the markup parses." };
|
|
1834
|
+
const queries = options.queries;
|
|
1835
|
+
if (!Array.isArray(queries) || queries.length === 0) return { allowed: false, reason: "A reviewed non-empty list of html queries is required in options.queries." };
|
|
1836
|
+
for (const item of queries) {
|
|
1837
|
+
if (!item || typeof item !== "object" || Array.isArray(item)) return { allowed: false, reason: "Every html query must be an object." };
|
|
1838
|
+
const query = item;
|
|
1839
|
+
if (!isnonempty(query.selector)) return { allowed: false, reason: "Every html query needs a selector from the reviewed selector grammar." };
|
|
1840
|
+
if (query.attribute !== void 0 && !isnonempty(query.attribute)) return { allowed: false, reason: "The reviewed html query attribute must be a non-empty attribute name." };
|
|
1841
|
+
if (query.multi !== void 0 && typeof query.multi !== "boolean") return { allowed: false, reason: "The reviewed html query multi flag must be a boolean." };
|
|
1842
|
+
}
|
|
1843
|
+
}
|
|
1844
|
+
if (kind === "callrest" || kind === "callgraphql") {
|
|
1845
|
+
if (!isnonempty(options.endpoint)) return { allowed: false, reason: "A reviewed typed endpoint name is required in options.endpoint." };
|
|
1846
|
+
if (kind === "callrest") {
|
|
1847
|
+
if (options.payload !== void 0 && (!options.payload || typeof options.payload !== "object" || Array.isArray(options.payload))) return { allowed: false, reason: "The reviewed rest payload must be an object of reviewed values." };
|
|
1848
|
+
if (options.method !== void 0 && (typeof options.method !== "string" || !["GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"].includes(options.method.trim().toUpperCase()))) return { allowed: false, reason: "The reviewed endpoint method override must be a known HTTP verb." };
|
|
1849
|
+
if (options.success !== void 0 && (!Array.isArray(options.success) || !options.success.every((code) => typeof code === "number" && Number.isInteger(code)))) return { allowed: false, reason: "The reviewed success status list must be a list of integer status codes." };
|
|
1850
|
+
}
|
|
1851
|
+
if (kind === "callgraphql") {
|
|
1852
|
+
const request = options.graphql;
|
|
1853
|
+
if (!request || typeof request !== "object" || Array.isArray(request)) return { allowed: false, reason: "A reviewed graphql request with an operation is required in options.graphql." };
|
|
1854
|
+
const graphql = request;
|
|
1855
|
+
if (typeof graphql.query !== "string" || !graphql.query.trim()) return { allowed: false, reason: "The reviewed graphql operation text must be a non-empty string." };
|
|
1856
|
+
if (graphql.operationkind !== "query" && graphql.operationkind !== "mutation") return { allowed: false, reason: "The reviewed graphql operation kind must be query or mutation; unknown operation kinds are refused." };
|
|
1857
|
+
if (graphql.variables !== void 0 && (!graphql.variables || typeof graphql.variables !== "object" || Array.isArray(graphql.variables))) return { allowed: false, reason: "The reviewed graphql variables must be an object of reviewed values." };
|
|
1858
|
+
if (graphql.operationname !== void 0 && !isnonempty(graphql.operationname)) return { allowed: false, reason: "The reviewed graphql operation name must be a non-empty string." };
|
|
1859
|
+
}
|
|
1860
|
+
if (options.apikeys !== void 0 && (!Array.isArray(options.apikeys) || !options.apikeys.every((name) => isnonempty(name)))) return { allowed: false, reason: "The reviewed api key reference list must be a list of non-empty stored names." };
|
|
1861
|
+
const policycheck = validatefetchoptions(options.fetchoptions);
|
|
1862
|
+
if (!policycheck.allowed) return policycheck;
|
|
1863
|
+
const fetchpolicy = fetchoptionsvalues(options.fetchoptions);
|
|
1864
|
+
const budget = fetchbudgetallowed(fetchpolicy.timeout, fetchpolicy.retries, fetchpolicy.backoff, fetchnumeric(options, "wait"));
|
|
1865
|
+
if (!budget.allowed) return budget;
|
|
1866
|
+
}
|
|
1867
|
+
return { allowed: true };
|
|
1868
|
+
}
|
|
1869
|
+
function validatefetchoptions(value) {
|
|
1870
|
+
if (value === void 0) return { allowed: true };
|
|
1871
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return { allowed: false, reason: "The reviewed fetch options must be an object with timeout, retries, backoff and follow." };
|
|
1872
|
+
const options = value;
|
|
1873
|
+
for (const key of ["timeout", "backoff"]) {
|
|
1874
|
+
if (options[key] !== void 0 && (typeof options[key] !== "number" || !Number.isFinite(options[key]) || options[key] < 0)) return { allowed: false, reason: `The reviewed fetch ${key} must be zero or a positive number with no code ceiling.` };
|
|
1875
|
+
}
|
|
1876
|
+
for (const key of ["retries", "follow"]) {
|
|
1877
|
+
if (options[key] !== void 0 && (typeof options[key] !== "number" || !Number.isInteger(options[key]) || options[key] < 0)) return { allowed: false, reason: `The reviewed fetch ${key} must be zero or a positive integer with no code ceiling.` };
|
|
1878
|
+
}
|
|
1879
|
+
return { allowed: true };
|
|
1880
|
+
}
|
|
1881
|
+
function fetchoptionsvalues(value) {
|
|
1882
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return {};
|
|
1883
|
+
const options = value;
|
|
1884
|
+
return { timeout: fetchnumeric(options, "timeout"), retries: fetchnumeric(options, "retries"), backoff: fetchnumeric(options, "backoff") };
|
|
1885
|
+
}
|
|
1886
|
+
function fetchnumeric(options, key) {
|
|
1887
|
+
const value = options[key];
|
|
1888
|
+
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
1889
|
+
}
|
|
1593
1890
|
function isrecordingkind(kind) {
|
|
1594
1891
|
return kind === "recordscreen" || kind === "captureaudio";
|
|
1595
1892
|
}
|
|
@@ -1950,6 +2247,10 @@ function validatestep(step, origin) {
|
|
|
1950
2247
|
const mediacheck = validatemediagrammar(step, options);
|
|
1951
2248
|
if (!mediacheck.allowed) return mediacheck;
|
|
1952
2249
|
}
|
|
2250
|
+
if (ishttpkind(step.kind)) {
|
|
2251
|
+
const httpcheck = validatehttpgrammar(step, options);
|
|
2252
|
+
if (!httpcheck.allowed) return httpcheck;
|
|
2253
|
+
}
|
|
1953
2254
|
if (step.kind === "tabcreate") {
|
|
1954
2255
|
if (options.background !== void 0 && typeof options.background !== "boolean") return { allowed: false, reason: "The reviewed background flag must be a boolean." };
|
|
1955
2256
|
if (options.window !== void 0 && (typeof options.window !== "number" || !Number.isInteger(options.window) || options.window < 0)) return { allowed: false, reason: "The reviewed target window id must be a non-negative integer." };
|
|
@@ -2029,6 +2330,17 @@ function canexecute(input) {
|
|
|
2029
2330
|
const recordinggate = recordingconsentgranted(input.step);
|
|
2030
2331
|
if (!recordinggate.allowed) return recordinggate;
|
|
2031
2332
|
}
|
|
2333
|
+
if (ishttpkind(input.step.kind)) {
|
|
2334
|
+
const target = outboundtarget(input.step);
|
|
2335
|
+
if (target !== void 0) {
|
|
2336
|
+
const outboundgate = origincheck(input.session, target);
|
|
2337
|
+
if (!outboundgate.allowed) return outboundgate;
|
|
2338
|
+
}
|
|
2339
|
+
if (input.step.kind === "fetchurl" || input.step.kind === "callrest" || input.step.kind === "callgraphql") {
|
|
2340
|
+
const consentgate = fetchconsentrefgranted(input.step);
|
|
2341
|
+
if (!consentgate.allowed) return consentgate;
|
|
2342
|
+
}
|
|
2343
|
+
}
|
|
2032
2344
|
if (input.step.kind === "openlink" || input.step.kind === "openprivate" || input.step.kind === "batchopen" || input.step.kind === "prefetch" || input.step.kind === "deeplink" || input.step.kind === "reopentab") {
|
|
2033
2345
|
let options = {};
|
|
2034
2346
|
try {
|
|
@@ -2155,9 +2467,19 @@ function recordmedia(progress, planid, stepid, media, now) {
|
|
|
2155
2467
|
const outcome = { stepid, ok: true, summary: `Captured a ${media.kind} media record of ${media.scope} scope with ${media.bytes} character${media.bytes === 1 ? "" : "s"} of media data.`, details: { media }, at: now };
|
|
2156
2468
|
return recordoutcome(base, planid, outcome, now);
|
|
2157
2469
|
}
|
|
2470
|
+
function recordcall(progress, planid, stepid, entry, now) {
|
|
2471
|
+
const base = progress && progress.planid === planid ? progress : emptyprogress(planid, now);
|
|
2472
|
+
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 };
|
|
2473
|
+
return recordoutcome(base, planid, outcome, now);
|
|
2474
|
+
}
|
|
2475
|
+
function recordfetchretry(progress, planid, stepid, retry, now) {
|
|
2476
|
+
const base = progress && progress.planid === planid ? progress : emptyprogress(planid, now);
|
|
2477
|
+
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 };
|
|
2478
|
+
return recordoutcome(base, planid, outcome, now);
|
|
2479
|
+
}
|
|
2158
2480
|
|
|
2159
2481
|
// version.ts
|
|
2160
|
-
var packageversion = "1.1.
|
|
2482
|
+
var packageversion = "1.1.42";
|
|
2161
2483
|
|
|
2162
2484
|
// types.ts
|
|
2163
2485
|
var protocolversion = packageversion;
|
|
@@ -2171,9 +2493,10 @@ function text(value, field) {
|
|
|
2171
2493
|
if (typeof value !== "string" || !value.trim()) throw new Error(`${field} must be a non-empty string.`);
|
|
2172
2494
|
return value.trim();
|
|
2173
2495
|
}
|
|
2174
|
-
function parseproposal(value, origin) {
|
|
2496
|
+
function parseproposal(value, origin, grants) {
|
|
2175
2497
|
const root = record(value);
|
|
2176
2498
|
if (root.version !== protocolversion) throw new Error("Unsupported protocol version.");
|
|
2499
|
+
const covered = grants !== void 0 && grants.length > 0 ? grants : [origin];
|
|
2177
2500
|
const planinput = record(root.plan);
|
|
2178
2501
|
const stepsinput = planinput.steps;
|
|
2179
2502
|
if (!Array.isArray(stepsinput) || stepsinput.length === 0) throw new Error("A plan needs at least one step.");
|
|
@@ -2191,6 +2514,17 @@ function parseproposal(value, origin) {
|
|
|
2191
2514
|
};
|
|
2192
2515
|
const evaluation = validatestep(step, origin);
|
|
2193
2516
|
if (!evaluation.allowed) throw new Error(evaluation.reason);
|
|
2517
|
+
const target = outboundtarget(step);
|
|
2518
|
+
if (target !== void 0) {
|
|
2519
|
+
const granted = covered.some((pattern) => {
|
|
2520
|
+
try {
|
|
2521
|
+
return new URL(target).origin === new URL(pattern).origin;
|
|
2522
|
+
} catch {
|
|
2523
|
+
return false;
|
|
2524
|
+
}
|
|
2525
|
+
});
|
|
2526
|
+
if (!granted) throw new Error(`The fetch request to ${target} targets an origin outside the grants.`);
|
|
2527
|
+
}
|
|
2194
2528
|
return step;
|
|
2195
2529
|
});
|
|
2196
2530
|
for (const step of steps) {
|
|
@@ -2221,7 +2555,7 @@ function requestbody(input) {
|
|
|
2221
2555
|
return JSON.stringify({ version: protocolversion, objective: input.objective, session: input.session, observation: input.observation, capabilities: input.capabilities });
|
|
2222
2556
|
}
|
|
2223
2557
|
function outcomeresponse(input) {
|
|
2224
|
-
return JSON.stringify({ version: protocolversion, planid: input.plan.id, planstate: input.plan.state, outcome: input.outcome, ...input.resolvedtarget ? { resolvedtarget: input.resolvedtarget } : {}, ...input.capture ? { capture: input.capture } : {}, ...input.media ? { media: input.media } : {} });
|
|
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 } : {} });
|
|
2225
2559
|
}
|
|
2226
2560
|
function mapresponse(input) {
|
|
2227
2561
|
return JSON.stringify({ version: protocolversion, planid: input.plan.id, planstate: input.plan.state, map: input.map });
|
|
@@ -2289,6 +2623,14 @@ function capturereport(input) {
|
|
|
2289
2623
|
function mediareport(input) {
|
|
2290
2624
|
return { version: protocolversion, records: input.records, images: input.images };
|
|
2291
2625
|
}
|
|
2626
|
+
function callsreport(input) {
|
|
2627
|
+
const calls = input.calls.map((call) => {
|
|
2628
|
+
const { body, ...metadata } = call;
|
|
2629
|
+
void body;
|
|
2630
|
+
return metadata;
|
|
2631
|
+
});
|
|
2632
|
+
return { version: protocolversion, calls };
|
|
2633
|
+
}
|
|
2292
2634
|
|
|
2293
2635
|
// capture.ts
|
|
2294
2636
|
var capturekinds = ["shotview", "shotfullpage", "shotelement", "shotregion", "contactsheet"];
|
|
@@ -2778,6 +3120,274 @@ function streamsummaries(raw) {
|
|
|
2778
3120
|
});
|
|
2779
3121
|
}
|
|
2780
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;
|
|
3150
|
+
}
|
|
3151
|
+
if (typeof options.body === "string") request.body = options.body;
|
|
3152
|
+
if (options.mode === "cors" || options.mode === "no-cors" || options.mode === "same-origin") request.mode = options.mode;
|
|
3153
|
+
return request;
|
|
3154
|
+
}
|
|
3155
|
+
function fetchoptionsof(value) {
|
|
3156
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return {};
|
|
3157
|
+
const options = value;
|
|
3158
|
+
const normalized = {};
|
|
3159
|
+
if (typeof options.timeout === "number" && Number.isFinite(options.timeout)) normalized.timeout = options.timeout;
|
|
3160
|
+
if (typeof options.retries === "number" && Number.isFinite(options.retries)) normalized.retries = options.retries;
|
|
3161
|
+
if (typeof options.backoff === "number" && Number.isFinite(options.backoff)) normalized.backoff = options.backoff;
|
|
3162
|
+
if (typeof options.follow === "number" && Number.isFinite(options.follow)) normalized.follow = options.follow;
|
|
3163
|
+
return normalized;
|
|
3164
|
+
}
|
|
3165
|
+
function streamwindowof(value) {
|
|
3166
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return {};
|
|
3167
|
+
const options = value;
|
|
3168
|
+
const window2 = {};
|
|
3169
|
+
if (typeof options.budget === "number" && Number.isFinite(options.budget)) window2.budget = options.budget;
|
|
3170
|
+
return window2;
|
|
3171
|
+
}
|
|
3172
|
+
function jsonpathrulesof(value) {
|
|
3173
|
+
if (!Array.isArray(value)) return [];
|
|
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;
|
|
3186
|
+
}
|
|
3187
|
+
function htmlqueriesof(value) {
|
|
3188
|
+
if (!Array.isArray(value)) return [];
|
|
3189
|
+
const queries = [];
|
|
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;
|
|
3200
|
+
}
|
|
3201
|
+
function graphqlrequestof(value) {
|
|
3202
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
|
|
3203
|
+
const options = value;
|
|
3204
|
+
if (typeof options.query !== "string" || !options.query.trim()) return void 0;
|
|
3205
|
+
if (options.operationkind !== "query" && options.operationkind !== "mutation") return void 0;
|
|
3206
|
+
const request = { query: options.query, operationkind: options.operationkind };
|
|
3207
|
+
if (options.variables && typeof options.variables === "object" && !Array.isArray(options.variables)) request.variables = options.variables;
|
|
3208
|
+
if (typeof options.operationname === "string" && options.operationname.trim()) request.operationname = options.operationname.trim();
|
|
3209
|
+
return request;
|
|
3210
|
+
}
|
|
3211
|
+
var realsleep = (milliseconds) => new Promise((resolve) => setTimeout(resolve, Math.max(0, milliseconds)));
|
|
3212
|
+
async function sendfetch(input) {
|
|
3213
|
+
const options = input.options ?? {};
|
|
3214
|
+
const sleep = input.sleep ?? realsleep;
|
|
3215
|
+
const now = input.now ?? Date.now;
|
|
3216
|
+
const attempts = Math.max(1, Math.floor(options.retries ?? 0) + 1);
|
|
3217
|
+
const backoff = options.backoff ?? 0;
|
|
3218
|
+
const follow = options.follow ?? Number.POSITIVE_INFINITY;
|
|
3219
|
+
let url = input.request.url;
|
|
3220
|
+
let method = (input.request.method ?? "GET").toUpperCase();
|
|
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
|
+
}
|
|
3264
|
+
}
|
|
3265
|
+
throw new Error(`The request failed after ${attempts} attempt${attempts === 1 ? "" : "s"} with ${retries} retr${retries === 1 ? "y" : "ies"}: ${lastreason}`);
|
|
3266
|
+
}
|
|
3267
|
+
async function readstream(input) {
|
|
3268
|
+
const pull = typeof input.chunks === "function" ? input.chunks : /* @__PURE__ */ ((source) => {
|
|
3269
|
+
let index = 0;
|
|
3270
|
+
return async () => source[index++];
|
|
3271
|
+
})(input.chunks);
|
|
3272
|
+
let count = 0;
|
|
3273
|
+
let total = 0;
|
|
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);
|
|
3283
|
+
}
|
|
3284
|
+
}
|
|
3285
|
+
function pathstep(current, segment) {
|
|
3286
|
+
if (Array.isArray(current) && /^\d+$/.test(segment)) return current[Number.parseInt(segment, 10)];
|
|
3287
|
+
if (current && typeof current === "object" && !Array.isArray(current)) return current[segment];
|
|
3288
|
+
return void 0;
|
|
3289
|
+
}
|
|
3290
|
+
function coerce(value, kind, fallback) {
|
|
3291
|
+
if (value === void 0 || value === null) return { value: fallback, missing: true };
|
|
3292
|
+
if (kind === "text") return { value: String(value), missing: false };
|
|
3293
|
+
if (kind === "number") {
|
|
3294
|
+
const numeric = typeof value === "number" ? value : Number(value);
|
|
3295
|
+
return Number.isFinite(numeric) ? { value: numeric, missing: false } : { value: fallback, missing: true };
|
|
3296
|
+
}
|
|
3297
|
+
if (kind === "boolean") return { value: value === true || value === "true", missing: false };
|
|
3298
|
+
return { value, missing: false };
|
|
3299
|
+
}
|
|
3300
|
+
function readpath(parsed, rules) {
|
|
3301
|
+
const fields = [];
|
|
3302
|
+
for (const rule of rules) {
|
|
3303
|
+
const kind = rule.kind ?? "text";
|
|
3304
|
+
let current = parsed;
|
|
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;
|
|
3321
|
+
}
|
|
3322
|
+
function payloadvalid(payload, schema) {
|
|
3323
|
+
if (!schema) return { ok: false, errors: ["The typed endpoint call needs a reviewed payload schema before it runs."] };
|
|
3324
|
+
const errors = [];
|
|
3325
|
+
for (const field of schema.fields) {
|
|
3326
|
+
const value = payload[field.name];
|
|
3327
|
+
if (value === void 0 || value === null) {
|
|
3328
|
+
if (field.required === true) errors.push(`The required field ${field.name} of kind ${field.kind} is missing.`);
|
|
3329
|
+
continue;
|
|
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 };
|
|
3336
|
+
}
|
|
3337
|
+
function payloadwithdefaults(payload, schema) {
|
|
3338
|
+
if (!schema) return payload;
|
|
3339
|
+
const merged = { ...payload };
|
|
3340
|
+
for (const field of schema.fields) {
|
|
3341
|
+
if (merged[field.name] === void 0 && field.default !== void 0) merged[field.name] = field.default;
|
|
3342
|
+
}
|
|
3343
|
+
return merged;
|
|
3344
|
+
}
|
|
3345
|
+
function errorsof(body) {
|
|
3346
|
+
try {
|
|
3347
|
+
const parsed = JSON.parse(body);
|
|
3348
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return [body];
|
|
3349
|
+
const record2 = parsed;
|
|
3350
|
+
for (const key of ["errors", "messages", "error", "message"]) {
|
|
3351
|
+
const value = record2[key];
|
|
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];
|
|
3358
|
+
}
|
|
3359
|
+
}
|
|
3360
|
+
async function callrest(input) {
|
|
3361
|
+
const check = payloadvalid(input.payload, input.endpoint.schema);
|
|
3362
|
+
if (!check.ok) throw new Error(check.errors.join(" "));
|
|
3363
|
+
const payload = payloadwithdefaults(input.payload, input.endpoint.schema);
|
|
3364
|
+
const url = templateurl(input.endpoint.url, payload);
|
|
3365
|
+
const method = input.endpoint.method.toUpperCase();
|
|
3366
|
+
const request = { url, method, ...input.endpoint.headers !== void 0 ? { headers: input.endpoint.headers } : {}, ...bodilessmethods.has(method) ? {} : { body: JSON.stringify(payload) } };
|
|
3367
|
+
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 } : {} });
|
|
3368
|
+
const ok = input.success !== void 0 ? input.success.includes(transport.status) : transport.statusclass === "success";
|
|
3369
|
+
return { transport, url, payload, ok, errors: ok ? [] : errorsof(transport.body) };
|
|
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 } : {} });
|
|
3373
|
+
}
|
|
3374
|
+
function unwrapgraphql(value) {
|
|
3375
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return { errors: ["The graphql response is not a json object."] };
|
|
3376
|
+
const record2 = value;
|
|
3377
|
+
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)) : [];
|
|
3378
|
+
return { ...record2.data !== void 0 ? { data: record2.data } : {}, errors };
|
|
3379
|
+
}
|
|
3380
|
+
async function callgraphql(input) {
|
|
3381
|
+
const request = { url: input.endpoint.url, method: "POST", ...input.endpoint.headers !== void 0 ? { headers: input.endpoint.headers } : {}, body: graphqlopenvelope(input.request) };
|
|
3382
|
+
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 } : {} });
|
|
3383
|
+
try {
|
|
3384
|
+
const unwrapped = unwrapgraphql(JSON.parse(transport.body));
|
|
3385
|
+
return { transport, ...unwrapped.data !== void 0 ? { data: unwrapped.data } : {}, errors: unwrapped.errors };
|
|
3386
|
+
} catch {
|
|
3387
|
+
return { transport, errors: [transport.body] };
|
|
3388
|
+
}
|
|
3389
|
+
}
|
|
3390
|
+
|
|
2781
3391
|
// extension/browsertabs.ts
|
|
2782
3392
|
var browserkinds = /* @__PURE__ */ new Set(["tablist", "tabcreate", "tabactivate", "tabclose", "tabreload", "tabsnapshot", "windowlist", "windowcreate", "windowclose", "zoomset", "windowresize", "downloadfile"]);
|
|
2783
3393
|
function isbrowserkind(kind) {
|
|
@@ -3977,7 +4587,7 @@ function stepoptions2(step) {
|
|
|
3977
4587
|
}
|
|
3978
4588
|
async function refreshcapabilities() {
|
|
3979
4589
|
const report = await readcapabilities();
|
|
3980
|
-
const withmedia = { ...report, captures: [...capturekinds], media: [...mediakinds] };
|
|
4590
|
+
const withmedia = { ...report, captures: [...capturekinds], media: [...mediakinds], http: [...httpkinds] };
|
|
3981
4591
|
await memory.setcapabilities(withmedia);
|
|
3982
4592
|
return withmedia;
|
|
3983
4593
|
}
|
|
@@ -4103,7 +4713,7 @@ async function propose(objective, remote) {
|
|
|
4103
4713
|
if (!config) throw new Error("Configure an approved HTTPS endpoint before requesting a remote proposal.");
|
|
4104
4714
|
const response = await fetch(config.endpoint, { method: "POST", headers: { "content-type": "application/json" }, credentials: "omit", body: requestbody({ objective: objective.trim(), session, observation, capabilities }) });
|
|
4105
4715
|
if (!response.ok) throw new Error(`Proposal endpoint returned ${response.status}.`);
|
|
4106
|
-
plan = parseproposal(await response.json(), session.origin).plan;
|
|
4716
|
+
plan = parseproposal(await response.json(), session.origin, session.grants ?? [session.origin]).plan;
|
|
4107
4717
|
}
|
|
4108
4718
|
await memory.setplan(plan);
|
|
4109
4719
|
await memory.setprogress(resetforplan(await memory.getprogress(), plan, Date.now()));
|
|
@@ -6458,6 +7068,190 @@ async function executemediastep(step, session, plan, tabid2, origin) {
|
|
|
6458
7068
|
}
|
|
6459
7069
|
throw new Error("Unsupported media kind.");
|
|
6460
7070
|
}
|
|
7071
|
+
var activefetches = /* @__PURE__ */ new Map();
|
|
7072
|
+
var defaultconsentwindow = 10 * 60 * 1e3;
|
|
7073
|
+
async function livefetch(url, init, controller, window2, streamstate) {
|
|
7074
|
+
const response = await fetch(url, { method: init.method, headers: init.headers, ...init.body !== void 0 ? { body: init.body } : {}, ...init.mode !== void 0 ? { mode: init.mode } : {}, redirect: init.redirect, credentials: "omit", signal: controller.signal });
|
|
7075
|
+
const headers = {};
|
|
7076
|
+
response.headers.forEach((value, name) => {
|
|
7077
|
+
headers[name] = value;
|
|
7078
|
+
});
|
|
7079
|
+
let body = "";
|
|
7080
|
+
if (window2 !== void 0 && response.body !== null) {
|
|
7081
|
+
const reader = response.body.getReader();
|
|
7082
|
+
const decoder = new TextDecoder();
|
|
7083
|
+
const pulled = await readstream({ chunks: async () => {
|
|
7084
|
+
const { done, value } = await reader.read();
|
|
7085
|
+
return done ? void 0 : decoder.decode(value, { stream: true });
|
|
7086
|
+
}, window: window2 });
|
|
7087
|
+
if (streamstate) {
|
|
7088
|
+
streamstate.bytes = pulled.bytes;
|
|
7089
|
+
streamstate.chunks = pulled.chunks;
|
|
7090
|
+
streamstate.aborted = pulled.aborted;
|
|
7091
|
+
streamstate.reason = pulled.reason;
|
|
7092
|
+
}
|
|
7093
|
+
if (pulled.aborted) controller.abort();
|
|
7094
|
+
body = decoder.decode();
|
|
7095
|
+
} else {
|
|
7096
|
+
body = await response.text();
|
|
7097
|
+
}
|
|
7098
|
+
return { status: response.status, headers, body, ...response.redirected ? { redirected: true } : {} };
|
|
7099
|
+
}
|
|
7100
|
+
async function attachapikeys(names, origin) {
|
|
7101
|
+
const headers = {};
|
|
7102
|
+
const attached = [];
|
|
7103
|
+
const refs = await memory.getapikeys();
|
|
7104
|
+
for (const name of names) {
|
|
7105
|
+
const ref = refs.find((item) => item.name === name);
|
|
7106
|
+
if (!ref) throw new Error(`No stored api key reference matches ${name}.`);
|
|
7107
|
+
if (!ref.origins.includes(origin)) throw new Error(`The api key ${name} is not scoped to ${origin}.`);
|
|
7108
|
+
const secret = await memory.getsecret(ref.storageid);
|
|
7109
|
+
if (secret === void 0) throw new Error(`The api key ${name} has no stored secret; set it from the review panel first.`);
|
|
7110
|
+
headers[ref.header] = secret;
|
|
7111
|
+
attached.push(name);
|
|
7112
|
+
}
|
|
7113
|
+
return { headers, keys: attached };
|
|
7114
|
+
}
|
|
7115
|
+
async function executehttpstep(step, session, plan, tabid2, origin) {
|
|
7116
|
+
const options = stepoptions2(step);
|
|
7117
|
+
const extra = { ...session ? { sessionid: session.id } : {}, planid: plan.id, stepid: step.id };
|
|
7118
|
+
if (step.kind === "fetchurl") {
|
|
7119
|
+
const request = fetchrequestof(options.fetch);
|
|
7120
|
+
if (!request) throw new Error("A reviewed fetch request with a url is required in options.fetch.");
|
|
7121
|
+
const gate = origincheck(session, request.url);
|
|
7122
|
+
if (!gate.allowed) throw new Error(gate.reason ?? "The outbound request stays outside the session origin grants.");
|
|
7123
|
+
const callorigin = new URL(request.url).origin;
|
|
7124
|
+
const names = Object.keys(request.headers ?? {});
|
|
7125
|
+
if (names.length > 0) {
|
|
7126
|
+
const consents = await memory.getfetchconsents();
|
|
7127
|
+
const covering = consents.find((consent) => fetchconsentcovers(consent, callorigin, names, Date.now()));
|
|
7128
|
+
if (!covering) {
|
|
7129
|
+
const consentref = typeof options.consentref === "string" ? options.consentref : "";
|
|
7130
|
+
const pending = consents.find((consent) => consent.approved !== true && consent.origin === callorigin && names.every((name) => consent.headers.some((header) => header.name.toLowerCase() === name.toLowerCase())));
|
|
7131
|
+
const prompt = pending ?? { id: consentref || randomid(), origin: callorigin, headers: names.map((name) => ({ name, value: (request.headers ?? {})[name] ?? "" })), expiresat: Date.now() + defaultconsentwindow, at: Date.now() };
|
|
7132
|
+
await memory.setfetchconsent(prompt);
|
|
7133
|
+
await refreshbadge();
|
|
7134
|
+
await audit("consent", `Fetch consent prompt ${prompt.id} opened for ${callorigin} with the header name${names.length === 1 ? "" : "s"} ${names.join(", ")}; header values stay out of the audit trail and appear only in the review prompt.`, extra);
|
|
7135
|
+
return { ok: false, summary: `The fetch waits for your header consent approval${consentref ? ` under ref ${consentref}` : ""}; approve it in the review panel and run the step again.`, details: { fetchconsent: { id: prompt.id, origin: callorigin, headers: prompt.headers, stepid: step.id, approved: false, expirywindow: prompt.expiresat - prompt.at } } };
|
|
7136
|
+
}
|
|
7137
|
+
}
|
|
7138
|
+
const policy = fetchoptionsof(options.fetchoptions);
|
|
7139
|
+
const callid = randomid();
|
|
7140
|
+
const controller = new AbortController();
|
|
7141
|
+
activefetches.set(callid, controller);
|
|
7142
|
+
const stream = options.stream !== void 0 ? streamwindowof(options.stream) : void 0;
|
|
7143
|
+
const streamstate = { bytes: 0, chunks: 0, aborted: false };
|
|
7144
|
+
const window2 = stream !== void 0 ? { ...stream, onchunk: (chunk, total) => {
|
|
7145
|
+
streamstate.chunks += 1;
|
|
7146
|
+
streamstate.bytes = total;
|
|
7147
|
+
} } : void 0;
|
|
7148
|
+
try {
|
|
7149
|
+
const transport = (url, init) => livefetch(url, init, controller, window2, streamstate);
|
|
7150
|
+
const result = await sendfetch({ request, ...policy !== void 0 && Object.keys(policy).length > 0 ? { options: policy } : {}, transport, onretry: (attempt, wait, reason) => {
|
|
7151
|
+
void (async () => {
|
|
7152
|
+
await memory.setprogress(recordfetchretry(await memory.getprogress(), plan.id, step.id, { attempt, url: request.url, wait, reason }, Date.now()));
|
|
7153
|
+
})().catch(() => {
|
|
7154
|
+
});
|
|
7155
|
+
} });
|
|
7156
|
+
const record2 = { id: callid, runid: plan.id, stepid: step.id, kind: "fetch", url: result.url, origin: new URL(result.url).origin, method: (request.method ?? "GET").toUpperCase(), status: result.status, statusclass: result.statusclass, duration: result.duration, retries: result.retries, bytes: result.bytes, headernames: names, body: result.body, ...streamstate.bytes > 0 ? { streambytes: streamstate.bytes } : {}, at: Date.now() };
|
|
7157
|
+
await memory.addcall(record2);
|
|
7158
|
+
await memory.setprogress(recordcall(await memory.getprogress(), plan.id, step.id, { id: record2.id, kind: record2.kind, origin: record2.origin, method: record2.method, status: record2.status, statusclass: record2.statusclass, duration: record2.duration, retries: record2.retries, bytes: record2.bytes }, Date.now()));
|
|
7159
|
+
await refreshbadge();
|
|
7160
|
+
await audit("call", `Fetched ${record2.method} ${record2.url} from the extension context ending in the ${record2.status} ${record2.statusclass} class after ${record2.retries} retr${record2.retries === 1 ? "y" : "ies"} with ${record2.bytes} body byte${record2.bytes === 1 ? "" : "s"}${names.length > 0 ? ` and ${names.length} consented header name${names.length === 1 ? "" : "s"}` : ""}; header values and body bytes stay out of the audit trail.`, extra);
|
|
7161
|
+
return { ok: true, summary: `Fetched ${record2.url} ending in the ${record2.status} ${record2.statusclass} class.`, details: { transport: { status: record2.status, headers: result.headernames, bytes: record2.bytes, duration: record2.duration }, call: record2.id, retries: record2.retries, redirects: result.redirects, ...streamstate.bytes > 0 ? { stream: { bytes: streamstate.bytes, chunks: streamstate.chunks, budget: window2?.budget, aborted: streamstate.aborted, ...streamstate.reason !== void 0 ? { reason: streamstate.reason } : {} } } : {}, credentialcall: names.some((name) => credentialheadername(name)) } };
|
|
7162
|
+
} finally {
|
|
7163
|
+
controller.abort();
|
|
7164
|
+
activefetches.delete(callid);
|
|
7165
|
+
}
|
|
7166
|
+
}
|
|
7167
|
+
if (step.kind === "parsejson") {
|
|
7168
|
+
const callid = typeof options.call === "string" ? options.call : "";
|
|
7169
|
+
const stored = await memory.getcall(callid);
|
|
7170
|
+
if (!stored) throw new Error(`No stored call matches ${callid}.`);
|
|
7171
|
+
if (stored.body === void 0 || stored.bodyexpired) throw new Error("The stored call body expired from the retention window; fetch it again before parsing.");
|
|
7172
|
+
let parsed;
|
|
7173
|
+
try {
|
|
7174
|
+
parsed = JSON.parse(stored.body);
|
|
7175
|
+
} catch {
|
|
7176
|
+
return { ok: false, summary: `The stored body of call ${callid} is not valid json.`, details: { call: callid, parseerror: true } };
|
|
7177
|
+
}
|
|
7178
|
+
const rules = jsonpathrulesof(options.fields);
|
|
7179
|
+
const fields = readpath(parsed, rules);
|
|
7180
|
+
const misses = fields.filter((field) => field.missing);
|
|
7181
|
+
await memory.addcall({ ...stored, fields });
|
|
7182
|
+
await audit("call", `Parsed the stored ${stored.method} body of ${stored.origin} into ${fields.length} named field${fields.length === 1 ? "" : "s"} with ${misses.length} path mis${misses.length === 1 ? "s" : "ses"} reported as outcomes.`, extra);
|
|
7183
|
+
return { ok: true, summary: `Extracted ${fields.length} field${fields.length === 1 ? "" : "s"} from the stored body${misses.length > 0 ? ` with ${misses.length} path mis${misses.length === 1 ? "s" : "ses"} filled by the reviewed defaults` : ""}.`, details: { call: callid, fields, misses: misses.map((field) => field.name), transport: { status: stored.status, headers: [], bytes: stored.bytes, duration: stored.duration } } };
|
|
7184
|
+
}
|
|
7185
|
+
if (step.kind === "parsehtml") {
|
|
7186
|
+
const callid = typeof options.call === "string" ? options.call : "";
|
|
7187
|
+
const stored = await memory.getcall(callid);
|
|
7188
|
+
if (!stored) throw new Error(`No stored call matches ${callid}.`);
|
|
7189
|
+
if (stored.body === void 0 || stored.bodyexpired) throw new Error("The stored call body expired from the retention window; fetch it again before parsing.");
|
|
7190
|
+
const queries = htmlqueriesof(options.queries);
|
|
7191
|
+
const results = await bridgecall(tabid2, "parsehtmlmarkup", stored.body, queries);
|
|
7192
|
+
await audit("call", `Parsed the stored ${stored.method} markup of ${stored.origin} through the page bridge domparser with ${queries.length} reviewed html quer${queries.length === 1 ? "y" : "ies"} over ${results.reduce((total, result) => total + result.count, 0)} matched element${results.reduce((total, result) => total + result.count, 0) === 1 ? "" : "s"}.`, extra);
|
|
7193
|
+
return { ok: true, summary: `Ran ${queries.length} html quer${queries.length === 1 ? "y" : "ies"} over the fetched markup.`, details: { call: callid, queries: results, transport: { status: stored.status, headers: [], bytes: stored.bytes, duration: stored.duration } } };
|
|
7194
|
+
}
|
|
7195
|
+
if (step.kind === "callrest" || step.kind === "callgraphql") {
|
|
7196
|
+
const endpointname = typeof options.endpoint === "string" ? options.endpoint : "";
|
|
7197
|
+
const stored = await memory.getendpoint(endpointname);
|
|
7198
|
+
if (!stored) throw new Error(`No stored typed endpoint matches ${endpointname}.`);
|
|
7199
|
+
if (!stored.schema) throw new Error("Every typed endpoint call needs a reviewed payload schema; store the endpoint with one from the review panel.");
|
|
7200
|
+
const policy = fetchoptionsof(options.fetchoptions);
|
|
7201
|
+
const callid = randomid();
|
|
7202
|
+
const controller = new AbortController();
|
|
7203
|
+
activefetches.set(callid, controller);
|
|
7204
|
+
try {
|
|
7205
|
+
const transport = (url, init) => livefetch(url, init, controller);
|
|
7206
|
+
const keynames = Array.isArray(options.apikeys) ? options.apikeys.filter((name) => typeof name === "string" && name.trim().length > 0) : [];
|
|
7207
|
+
if (step.kind === "callrest") {
|
|
7208
|
+
const methodoverride = typeof options.method === "string" ? options.method.trim().toUpperCase() : void 0;
|
|
7209
|
+
const endpoint = { ...stored, ...methodoverride !== void 0 ? { method: methodoverride } : {} };
|
|
7210
|
+
const payload = options.payload && typeof options.payload === "object" && !Array.isArray(options.payload) ? options.payload : {};
|
|
7211
|
+
const resolvedurl = templateurl(endpoint.url, payloadwithdefaults(payload, endpoint.schema));
|
|
7212
|
+
const urlgate2 = origincheck(session, resolvedurl);
|
|
7213
|
+
if (!urlgate2.allowed) throw new Error(urlgate2.reason ?? "The typed call stays outside the session origin grants.");
|
|
7214
|
+
const keys2 = keynames.length > 0 ? await attachapikeys(keynames, new URL(resolvedurl).origin) : { headers: {}, keys: [] };
|
|
7215
|
+
const headers2 = { ...endpoint.headers ?? {}, ...keys2.headers };
|
|
7216
|
+
const success = Array.isArray(options.success) ? options.success.filter((code) => typeof code === "number" && Number.isInteger(code)) : void 0;
|
|
7217
|
+
const result2 = await callrest({ endpoint: { ...endpoint, ...Object.keys(headers2).length > 0 ? { headers: headers2 } : {} }, payload, ...policy !== void 0 && Object.keys(policy).length > 0 ? { options: policy } : {}, transport, ...success !== void 0 ? { success } : {}, onretry: (attempt, wait, reason) => {
|
|
7218
|
+
void (async () => {
|
|
7219
|
+
await memory.setprogress(recordfetchretry(await memory.getprogress(), plan.id, step.id, { attempt, url: resolvedurl, wait, reason }, Date.now()));
|
|
7220
|
+
})().catch(() => {
|
|
7221
|
+
});
|
|
7222
|
+
} });
|
|
7223
|
+
const record3 = { id: callid, runid: plan.id, stepid: step.id, kind: "rest", url: result2.url, origin: new URL(result2.url).origin, method: endpoint.method.toUpperCase(), status: result2.transport.status, statusclass: result2.transport.statusclass, duration: result2.transport.duration, retries: result2.transport.retries, bytes: result2.transport.bytes, headernames: [...Object.keys(endpoint.headers ?? {}), ...keynames.map((name) => `apikey:${name}`)], endpoint: endpointname, body: result2.transport.body, ...result2.errors.length > 0 ? { errors: result2.errors } : {}, at: Date.now() };
|
|
7224
|
+
await memory.addcall(record3);
|
|
7225
|
+
await memory.setprogress(recordcall(await memory.getprogress(), plan.id, step.id, { id: record3.id, kind: record3.kind, origin: record3.origin, method: record3.method, status: record3.status, statusclass: record3.statusclass, duration: record3.duration, retries: record3.retries, bytes: record3.bytes }, Date.now()));
|
|
7226
|
+
await refreshbadge();
|
|
7227
|
+
await audit("call", `Called the typed rest endpoint ${endpointname} with ${record3.method} on ${record3.origin} ending in the ${record3.status} ${record3.statusclass} class after ${record3.retries} retr${record3.retries === 1 ? "y" : "ies"}${mutationcallof(step) ? " as a reviewed mutating verb" : ""}${keys2.keys.length > 0 ? ` with ${keys2.keys.length} attached api key reference${keys2.keys.length === 1 ? "" : "s"}` : ""}; payload values, header values and body bytes stay out of the audit trail.`, extra);
|
|
7228
|
+
return { ok: result2.ok, summary: `The typed rest call ${endpointname} ended in the ${record3.status} ${record3.statusclass} class.`, details: { transport: { status: record3.status, headers: result2.transport.headernames, bytes: record3.bytes, duration: record3.duration }, call: record3.id, endpoint: endpointname, payload: result2.payload, retries: record3.retries, ...result2.errors.length > 0 ? { errors: result2.errors } : {}, mutation: mutationcallof(step), credentialcall: keys2.keys.length > 0 } };
|
|
7229
|
+
}
|
|
7230
|
+
const request = graphqlrequestof(options.graphql);
|
|
7231
|
+
if (!request) throw new Error("A reviewed graphql request with an operation and its kind is required in options.graphql.");
|
|
7232
|
+
const urlgate = origincheck(session, stored.url);
|
|
7233
|
+
if (!urlgate.allowed) throw new Error(urlgate.reason ?? "The typed call stays outside the session origin grants.");
|
|
7234
|
+
const keys = keynames.length > 0 ? await attachapikeys(keynames, new URL(stored.url).origin) : { headers: {}, keys: [] };
|
|
7235
|
+
const headers = { ...stored.headers ?? {}, ...keys.headers };
|
|
7236
|
+
const result = await callgraphql({ endpoint: { ...stored, ...Object.keys(headers).length > 0 ? { headers } : {} }, request, ...policy !== void 0 && Object.keys(policy).length > 0 ? { options: policy } : {}, transport, onretry: (attempt, wait, reason) => {
|
|
7237
|
+
void (async () => {
|
|
7238
|
+
await memory.setprogress(recordfetchretry(await memory.getprogress(), plan.id, step.id, { attempt, url: stored.url, wait, reason }, Date.now()));
|
|
7239
|
+
})().catch(() => {
|
|
7240
|
+
});
|
|
7241
|
+
} });
|
|
7242
|
+
const record2 = { id: callid, runid: plan.id, stepid: step.id, kind: "graphql", url: result.transport.url, origin: new URL(result.transport.url).origin, method: "POST", status: result.transport.status, statusclass: result.transport.statusclass, duration: result.transport.duration, retries: result.transport.retries, bytes: result.transport.bytes, headernames: [...Object.keys(stored.headers ?? {}), ...keynames.map((name) => `apikey:${name}`)], endpoint: endpointname, body: result.transport.body, ...result.errors.length > 0 ? { errors: result.errors } : {}, at: Date.now() };
|
|
7243
|
+
await memory.addcall(record2);
|
|
7244
|
+
await memory.setprogress(recordcall(await memory.getprogress(), plan.id, step.id, { id: record2.id, kind: record2.kind, origin: record2.origin, method: record2.method, status: record2.status, statusclass: record2.statusclass, duration: record2.duration, retries: record2.retries, bytes: record2.bytes }, Date.now()));
|
|
7245
|
+
await refreshbadge();
|
|
7246
|
+
await audit("call", `Called the typed graphql endpoint ${endpointname} with a reviewed ${request.operationkind} on ${record2.origin} ending in the ${record2.status} ${record2.statusclass} class with ${result.errors.length} returned error${result.errors.length === 1 ? "" : "s"}${keys.keys.length > 0 ? ` and ${keys.keys.length} attached api key reference${keys.keys.length === 1 ? "" : "s"}` : ""}; variables, header values and body bytes stay out of the audit trail.`, extra);
|
|
7247
|
+
return { ok: result.errors.length === 0 && result.transport.statusclass === "success", summary: `The graphql ${request.operationkind} ended in the ${record2.status} ${record2.statusclass} class with ${result.errors.length} error${result.errors.length === 1 ? "" : "s"}.`, details: { transport: { status: record2.status, headers: result.transport.headernames, bytes: record2.bytes, duration: record2.duration }, call: record2.id, endpoint: endpointname, operationkind: request.operationkind, retries: record2.retries, ...result.errors.length > 0 ? { errors: result.errors } : {}, ...result.data !== void 0 ? { datapaths: result.data && typeof result.data === "object" && !Array.isArray(result.data) ? Object.keys(result.data) : [] } : {}, mutation: request.operationkind === "mutation", credentialcall: keys.keys.length > 0 } };
|
|
7248
|
+
} finally {
|
|
7249
|
+
controller.abort();
|
|
7250
|
+
activefetches.delete(callid);
|
|
7251
|
+
}
|
|
7252
|
+
}
|
|
7253
|
+
throw new Error("Unsupported network observation kind.");
|
|
7254
|
+
}
|
|
6461
7255
|
async function enforcewindowreview(step, session, plan) {
|
|
6462
7256
|
const windowid = step.value && /^\d+$/.test(step.value) ? Number.parseInt(step.value, 10) : 0;
|
|
6463
7257
|
const progress = plan ? await memory.getprogress() : void 0;
|
|
@@ -6499,8 +7293,9 @@ async function refreshbadge() {
|
|
|
6499
7293
|
const captures = (await memory.getcaptures()).length;
|
|
6500
7294
|
const media = (await memory.getmediarecords()).length + (await memory.getimagebatches()).length;
|
|
6501
7295
|
const recordingprompts = (await memory.getrecordingconsents()).filter((record2) => record2.approved === void 0).length;
|
|
7296
|
+
const fetchprompts = (await memory.getfetchconsents()).filter((consent) => consent.approved === void 0).length;
|
|
6502
7297
|
const tasktabs2 = new Set(badges.map((badge) => badge.tabid)).size;
|
|
6503
|
-
const total = (queues?.prefetch ?? 0) + (queues?.batchopen ?? 0) + tasktabs2 + prompts + consents + quarantined + datasets + captures + media + recordingprompts;
|
|
7298
|
+
const total = (queues?.prefetch ?? 0) + (queues?.batchopen ?? 0) + tasktabs2 + prompts + consents + quarantined + datasets + captures + media + recordingprompts + fetchprompts;
|
|
6504
7299
|
await chrome.action.setBadgeText({ text: total > 0 ? String(total) : "" }).catch(() => {
|
|
6505
7300
|
});
|
|
6506
7301
|
}
|
|
@@ -6558,6 +7353,8 @@ async function executestep(stepid) {
|
|
|
6558
7353
|
output = await executecapturestep(step, session, plan, tab.id, origin);
|
|
6559
7354
|
} else if (ismediakind(step.kind)) {
|
|
6560
7355
|
output = await executemediastep(step, session, plan, tab.id, origin);
|
|
7356
|
+
} else if (ishttpkind(step.kind)) {
|
|
7357
|
+
output = await executehttpstep(step, session, plan, tab.id, origin);
|
|
6561
7358
|
} else {
|
|
6562
7359
|
if (step.target && freshcheckkinds.has(step.kind)) {
|
|
6563
7360
|
const fresh = await snapshot(tab.id);
|
|
@@ -6756,6 +7553,14 @@ async function handlerequest(message, sender) {
|
|
|
6756
7553
|
});
|
|
6757
7554
|
const imagebatches = await memory.getimagebatches();
|
|
6758
7555
|
const recordingconsents = await memory.getrecordingconsents();
|
|
7556
|
+
const calls = (await memory.getcalls()).map((call) => {
|
|
7557
|
+
const { body, ...metadata } = call;
|
|
7558
|
+
void body;
|
|
7559
|
+
return metadata;
|
|
7560
|
+
});
|
|
7561
|
+
const endpoints = await memory.getendpoints();
|
|
7562
|
+
const fetchconsents = await memory.getfetchconsents();
|
|
7563
|
+
const apikeys = (await memory.getapikeys()).map((ref) => ({ name: ref.name, origins: ref.origins, header: ref.header, configuredat: ref.configuredat }));
|
|
6759
7564
|
const runsettings = await memory.getsettings();
|
|
6760
7565
|
const scanhooks = [];
|
|
6761
7566
|
for (const hook of await memory.getscanhooks()) {
|
|
@@ -6767,7 +7572,7 @@ async function handlerequest(message, sender) {
|
|
|
6767
7572
|
const livetab = session ? await chrome.tabs.get(session.tabid).catch(() => void 0) : void 0;
|
|
6768
7573
|
const waitprofile = session ? waitprofiles.find((record2) => record2.origin === session.origin) : void 0;
|
|
6769
7574
|
const livestate = { phase: livetab?.status === "loading" ? "loading" : "complete", ...navrecords[0] ? { finalurl: navrecords[0].finalurl, redirects: navrecords[0].chain } : {} };
|
|
6770
|
-
return { config: await memory.getconfig(), session, plan, progress: plan && progress?.planid === plan.id ? progress : void 0, diagnostic: await memory.getdiagnostic(), audit: await memory.getaudit(), capabilities: await refreshcapabilities(), outcomes: await memory.getoutcomes(), holds: heldkeysreport({ tabid: session?.tabid ?? 0, holds }), dialogs: await memory.getdialogs(), retries: await memory.getretries(), ...signals ? { signals: signalsreport({ signals }) } : { signals: signalsreport({}) }, banners: await memory.getbanners(), mutationevents: await memory.getmutationevents(), focusevents: await memory.getfocusevents(), diffs: await memory.getdiffs(), selectors: await memory.getselectors(), ...a11y ? { a11y } : {}, ...reader ? { reader } : {}, ...map ? { map } : {}, trail: trailreport({ ...session ? { sessionid: session.id } : {}, trail }), navrecords, ratestates, safeties, curated, waitprofiles, auths, navcontrol, navqueues, artifacts, navstate: livestate, ...waitprofile ? { waitprofile } : {}, offline: !navigator.onLine, tabs, windows, layouts: layoutreport({ layouts }), tabgroups, tabmetas, badges, snapshots, closedtabs, tabwatchevents, clones, tasktabgauge: taskgauge, ...controltab ? { controltab } : {}, tabreport: report, profiles, tickets, wizards: wizardreport({ ...session ? { sessionid: session.id } : {}, wizards, picks }), picks, errorreports, captchas, detections, ...codeentry !== void 0 ? { codeentry: true } : {}, datasets, imports, extractsessions, streams, exports, provenances, taskrules, sheetendpoints: sheetgrants, downloads, netlogs, clipconsents, clips, quarantines, cleanuprules, cleanupruns, capturecounters, inventory, mimefilters, scanhooks, captures: capturemetadata, capturepairs, capturepolicy: runsettings?.capturepolicy ?? "manual", media: mediarecords, imagebatches, recordingconsents, recordingactive: [...activerecordings.values()].map((active) => ({ id: active.record.id, kind: active.record.kind, scope: active.record.scope, startedat: active.record.startedat, stopat: active.stopat })), recordingwindow: runsettings?.recordingwindow, ...stitchprogress.size > 0 ? { stitchprogress: [...stitchprogress.values()] } : {} };
|
|
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()] } : {} };
|
|
6771
7576
|
}
|
|
6772
7577
|
case "capabilities":
|
|
6773
7578
|
return refreshcapabilities();
|
|
@@ -7341,8 +8146,78 @@ async function handlerequest(message, sender) {
|
|
|
7341
8146
|
await audit("configure", `The user set the recording duration window of the run to ${window2} milliseconds; recordings never run past it.`);
|
|
7342
8147
|
return { recordingwindow: window2 };
|
|
7343
8148
|
}
|
|
8149
|
+
case "setcallretention": {
|
|
8150
|
+
const inputretention = message;
|
|
8151
|
+
const retention = inputretention.retention;
|
|
8152
|
+
if (retention !== void 0 && (typeof retention !== "number" || !Number.isFinite(retention) || retention < 0)) throw new Error("The call body retention window must be zero or a positive number of records with no code ceiling.");
|
|
8153
|
+
const settings = await memory.getsettings();
|
|
8154
|
+
await memory.setsettings({ ...settings, ...retention !== void 0 ? { callretention: retention } : {} });
|
|
8155
|
+
await audit("configure", `The user set the outbound call body retention window to ${retention === void 0 ? "keep every body" : `${retention} record${retention === 1 ? "" : "s"}`}; the call metadata always survives for the audit trail.`);
|
|
8156
|
+
return { callretention: retention };
|
|
8157
|
+
}
|
|
8158
|
+
case "approvefetchconsent": {
|
|
8159
|
+
const inputconsent = message;
|
|
8160
|
+
const record2 = (await memory.getfetchconsents()).find((item) => item.id === inputconsent.id);
|
|
8161
|
+
if (!record2) throw new Error("No fetch consent prompt matches the requested id.");
|
|
8162
|
+
await memory.setfetchconsent({ ...record2, approved: inputconsent.approved !== false });
|
|
8163
|
+
const session = await memory.getsession();
|
|
8164
|
+
await audit("consent", `Fetch consent prompt ${record2.id} for ${record2.origin} with the header name${record2.headers.length === 1 ? "" : "s"} ${record2.headers.map((header) => header.name).join(", ")} ${inputconsent.approved !== false ? "approved" : "declined"} by the user; the prompt appears once per origin and expires on the reviewed window.`, { ...session ? { sessionid: session.id } : {} });
|
|
8165
|
+
await refreshbadge();
|
|
8166
|
+
return { id: record2.id, approved: inputconsent.approved !== false };
|
|
8167
|
+
}
|
|
8168
|
+
case "configureendpoint": {
|
|
8169
|
+
const inputendpoint = message;
|
|
8170
|
+
const candidate = { name: inputendpoint.name ?? "", method: inputendpoint.method ?? "", url: inputendpoint.url ?? "", ...inputendpoint.headers !== void 0 ? { headers: inputendpoint.headers } : {}, ...inputendpoint.schema !== void 0 ? { schema: inputendpoint.schema } : {}, version: 1, at: Date.now() };
|
|
8171
|
+
const gate = validateendpointrecord(candidate);
|
|
8172
|
+
if (!gate.allowed) throw new Error(gate.reason);
|
|
8173
|
+
const normalized = normalizeendpoint(candidate.url);
|
|
8174
|
+
const record2 = { name: candidate.name.trim(), method: candidate.method.trim().toUpperCase(), url: normalized.endpoint, ...candidate.headers !== void 0 ? { headers: candidate.headers } : {}, ...inputendpoint.schema !== void 0 ? { schema: inputendpoint.schema } : {}, version: 1, at: Date.now() };
|
|
8175
|
+
await memory.setendpoint(record2);
|
|
8176
|
+
const session = await memory.getsession();
|
|
8177
|
+
await audit("call", `Stored the typed endpoint ${record2.name} version as ${record2.method} ${normalized.endpoint} with a reviewed payload schema and header allowlist; every stored version stays in the history.`, { ...session ? { sessionid: session.id } : {} });
|
|
8178
|
+
const stored = await memory.getendpoint(record2.name);
|
|
8179
|
+
return stored;
|
|
8180
|
+
}
|
|
8181
|
+
case "setapikey": {
|
|
8182
|
+
const inputkey = message;
|
|
8183
|
+
if (!inputkey.name?.trim()) throw new Error("A reviewed api key name is required.");
|
|
8184
|
+
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
|
+
if (!inputkey.header?.trim()) throw new Error("A reviewed header name is required for the api key.");
|
|
8186
|
+
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 ref = { name: inputkey.name.trim(), origins: inputkey.origins, header: inputkey.header.trim(), storageid: `apikey-${inputkey.name.trim()}`, configuredat: Date.now() };
|
|
8188
|
+
await memory.setapikey(ref);
|
|
8189
|
+
await memory.setsecret(ref.storageid, inputkey.value);
|
|
8190
|
+
const session = await memory.getsession();
|
|
8191
|
+
await audit("call", `Stored the api key reference ${ref.name} for ${ref.origins.join(", ")} attaching header ${ref.header}; the key material itself never enters the audit trail.`, { ...session ? { sessionid: session.id } : {} });
|
|
8192
|
+
return { name: ref.name, origins: ref.origins, header: ref.header, configuredat: ref.configuredat };
|
|
8193
|
+
}
|
|
8194
|
+
case "deleteapikey": {
|
|
8195
|
+
const inputdeletekey = message;
|
|
8196
|
+
if (!inputdeletekey.name?.trim()) throw new Error("A reviewed api key name is required.");
|
|
8197
|
+
await memory.removeapikey(inputdeletekey.name.trim());
|
|
8198
|
+
const session = await memory.getsession();
|
|
8199
|
+
await audit("call", `Removed the api key reference ${inputdeletekey.name.trim()} and its stored secret.`, { ...session ? { sessionid: session.id } : {} });
|
|
8200
|
+
return { name: inputdeletekey.name.trim(), deleted: true };
|
|
8201
|
+
}
|
|
8202
|
+
case "callsreport":
|
|
8203
|
+
return callsreport({ calls: await memory.getcalls() });
|
|
8204
|
+
case "exportcalls": {
|
|
8205
|
+
const session = await memory.getsession();
|
|
8206
|
+
if (!session || session.stoppedat || session.expiresat <= Date.now()) throw new Error("Call list exports stay behind the consent gate of an active session.");
|
|
8207
|
+
const granted = await chrome.permissions.contains({ permissions: ["downloads"] }).catch(() => false);
|
|
8208
|
+
if (!granted) throw new Error("The call list export needs the downloads capability; request it from the review panel.");
|
|
8209
|
+
const report = callsreport({ calls: await memory.getcalls() });
|
|
8210
|
+
const dataurl = `data:application/json;base64,${btoa(JSON.stringify(report, null, 2))}`;
|
|
8211
|
+
await chrome.downloads.download({ url: dataurl, filename: `devthink-calls-${Date.now()}.json` });
|
|
8212
|
+
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
|
+
return { exported: report.calls.length };
|
|
8214
|
+
}
|
|
7344
8215
|
case "stop": {
|
|
7345
8216
|
const session = await memory.getsession();
|
|
8217
|
+
for (const [id, controller] of [...activefetches.entries()]) {
|
|
8218
|
+
controller.abort();
|
|
8219
|
+
activefetches.delete(id);
|
|
8220
|
+
}
|
|
7346
8221
|
for (const [id, active] of [...activerecordings.entries()]) {
|
|
7347
8222
|
const finished = finishrecording(active.record, Date.now());
|
|
7348
8223
|
await memory.addmedia(finished).catch(() => {
|