@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.
@@ -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.41";
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(() => {