@wenathlan/extension 1.1.40 → 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 +6 -4
- package/dist/httpclient.d.ts +158 -0
- package/dist/httpclient.d.ts.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1138 -6
- package/dist/index.js.map +4 -4
- package/dist/media.d.ts +96 -0
- package/dist/media.d.ts.map +1 -0
- package/dist/memory.d.ts +55 -1
- package/dist/memory.d.ts.map +1 -1
- package/dist/policy.d.ts +37 -0
- package/dist/policy.d.ts.map +1 -1
- package/dist/protocol.d.ts +37 -4
- package/dist/protocol.d.ts.map +1 -1
- package/dist/types.d.ts +336 -3
- package/dist/types.d.ts.map +1 -1
- package/dist/version.d.ts +1 -1
- package/extension/dist/background.js +1832 -13
- package/extension/dist/background.js.map +4 -4
- package/extension/dist/manifest.json +1 -1
- package/extension/dist/pagebridge.js +203 -3
- package/extension/dist/pagebridge.js.map +3 -3
- package/extension/dist/popup.html +1 -1
- package/extension/dist/popup.js +22 -2
- package/extension/dist/popup.js.map +2 -2
- package/extension/dist/sidepanel.html +1 -1
- package/extension/dist/sidepanel.js +400 -2
- package/extension/dist/sidepanel.js.map +3 -3
- package/extension/dist/style.css +4 -0
- package/extension/manifest.json +1 -1
- package/package.json +1 -1
|
@@ -765,23 +765,172 @@ var sessionmemory = class {
|
|
|
765
765
|
for (const capture of await this.getcaptures()) runs.set(capture.id, capture.runid);
|
|
766
766
|
return records.filter((item) => runs.get(item.beforeid) === runid);
|
|
767
767
|
}
|
|
768
|
+
/** Stores one media record of the 1.1.41 family with its bytes and step linkage, replacing the previous record of that id; the user configured media retention window expires the oldest bytes while the metadata and the recording index always survive. */
|
|
769
|
+
async addmedia(record2) {
|
|
770
|
+
const records = await this.getmediarecords();
|
|
771
|
+
const retention = (await this.getsettings())?.mediaretention;
|
|
772
|
+
const combined = [record2, ...records.filter((item) => item.id !== record2.id)];
|
|
773
|
+
const stored = retention === void 0 ? combined : combined.map((item, index) => index < retention ? item : expiremediabytes(item));
|
|
774
|
+
await this.adapter.set("media", stored);
|
|
775
|
+
}
|
|
776
|
+
/** Returns every stored media record, newest first. */
|
|
777
|
+
async getmediarecords() {
|
|
778
|
+
return await this.adapter.get("media") ?? [];
|
|
779
|
+
}
|
|
780
|
+
/** Returns the media records filtered by run and kind; an absent filter returns every record. */
|
|
781
|
+
async listmedia(filter) {
|
|
782
|
+
const records = await this.getmediarecords();
|
|
783
|
+
return records.filter((item) => (filter.runid === void 0 || item.runid === filter.runid) && (filter.kind === void 0 || mediakindof(item) === filter.kind));
|
|
784
|
+
}
|
|
785
|
+
/** Returns one media record by its id. */
|
|
786
|
+
async getmediarecord(id) {
|
|
787
|
+
return (await this.getmediarecords()).find((item) => item.id === id);
|
|
788
|
+
}
|
|
789
|
+
/** Returns one recording with its file reference and frame index by its id. */
|
|
790
|
+
async getrecording(id) {
|
|
791
|
+
const found = await this.getmediarecord(id);
|
|
792
|
+
return found !== void 0 && "startedat" in found ? found : void 0;
|
|
793
|
+
}
|
|
794
|
+
/** Removes one media record by its id; the audit trail keeps its outcome evidence. */
|
|
795
|
+
async removemedia(id) {
|
|
796
|
+
await this.adapter.set("media", (await this.getmediarecords()).filter((item) => item.id !== id));
|
|
797
|
+
}
|
|
798
|
+
/** Stores one observed image batch of a downloadimages step, replacing the previous batch of that id. */
|
|
799
|
+
async addimagebatch(batch) {
|
|
800
|
+
const records = await this.adapter.get("imagebatches") ?? [];
|
|
801
|
+
await this.adapter.set("imagebatches", [batch, ...records.filter((item) => item.id !== batch.id)]);
|
|
802
|
+
}
|
|
803
|
+
/** Returns every observed image batch with its filter match counts, newest first. */
|
|
804
|
+
async getimagebatches() {
|
|
805
|
+
return await this.adapter.get("imagebatches") ?? [];
|
|
806
|
+
}
|
|
807
|
+
/** Stores one recording consent decision of an origin, replacing the previous record of that id. */
|
|
808
|
+
async setrecordingconsent(record2) {
|
|
809
|
+
const records = (await this.adapter.get("recordingconsents") ?? []).filter((item) => item.id !== record2.id);
|
|
810
|
+
await this.adapter.set("recordingconsents", [record2, ...records]);
|
|
811
|
+
}
|
|
812
|
+
/** Returns every recording consent decision with its prompt and origin, newest first. */
|
|
813
|
+
async getrecordingconsents() {
|
|
814
|
+
return await this.adapter.get("recordingconsents") ?? [];
|
|
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
|
+
}
|
|
768
889
|
};
|
|
890
|
+
function mediakindof(record2) {
|
|
891
|
+
if ("pages" in record2) return "pdf";
|
|
892
|
+
if ("startedat" in record2) return "recording";
|
|
893
|
+
if ("timestamp" in record2) return "frame";
|
|
894
|
+
if ("context" in record2) return "canvas";
|
|
895
|
+
if ("tracks" in record2) return "stream";
|
|
896
|
+
return "asset";
|
|
897
|
+
}
|
|
898
|
+
function expiremediabytes(record2) {
|
|
899
|
+
if ("dataurl" in record2) {
|
|
900
|
+
const source = record2;
|
|
901
|
+
const copy = { ...source };
|
|
902
|
+
delete copy.dataurl;
|
|
903
|
+
return { ...copy, bytesexpired: true };
|
|
904
|
+
}
|
|
905
|
+
if ("startedat" in record2) {
|
|
906
|
+
const source = record2;
|
|
907
|
+
const copy = { ...source };
|
|
908
|
+
delete copy.bytes;
|
|
909
|
+
return { ...copy, bytesexpired: true };
|
|
910
|
+
}
|
|
911
|
+
return record2;
|
|
912
|
+
}
|
|
769
913
|
function expirecapturebytes(record2) {
|
|
770
914
|
const { bytes, ...metadata } = record2;
|
|
771
915
|
void bytes;
|
|
772
916
|
return { ...metadata, bytesexpired: true };
|
|
773
917
|
}
|
|
918
|
+
function expirecallbody(record2) {
|
|
919
|
+
const { body, ...metadata } = record2;
|
|
920
|
+
void body;
|
|
921
|
+
return { ...metadata, bodyexpired: true };
|
|
922
|
+
}
|
|
774
923
|
function randomid() {
|
|
775
924
|
return crypto.randomUUID();
|
|
776
925
|
}
|
|
777
926
|
|
|
778
927
|
// policy.ts
|
|
779
|
-
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"]);
|
|
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"]);
|
|
780
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"]);
|
|
781
|
-
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"]);
|
|
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"]);
|
|
782
931
|
var allowedactions = /* @__PURE__ */ new Set([...sensitiveactions, ...interactionactions, ...readactions]);
|
|
783
932
|
var watchactions = /* @__PURE__ */ new Set(["watchmutate", "watchbanner", "watchfocus", "watchtab"]);
|
|
784
|
-
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"]);
|
|
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"]);
|
|
785
934
|
var valueactions = /* @__PURE__ */ new Set(["presskey", "drag", "drop", "upload", "readattribute", "removeattribute", "waittext", "evaluate", "zoomset", "tabactivate", "tabclose", "tabreload", "windowclose", "windowresize", "tabcreate", "windowcreate", "downloadfile", "typetime", "appendtext", "setvalue", "typeedit", "keyhold", "keyrelease", "chooseradio", "setslider", "setdate", "setcolor", "followlink", "setfragment", "handleauth", "navintent", "openclipboard", "checksafe", "reopentab", "spanav", "duplicatetab", "pintab", "mutetab", "movetab", "movetabwindow", "searchtabs", "badgetab", "attachmeta", "focuswindow", "maximizewindow", "minimizewindow", "restorewindow", "incognitowindow", "asksubmit", "selectchain", "picktypeahead", "pickdate", "attachfile", "fillcode", "consentpassword", "pausedownload", "resumedownload", "verifydownload", "writeclipboard", "quarantinedownload", "scanvirus"]);
|
|
786
935
|
var tabscommandactions = /* @__PURE__ */ new Set(["querytabs", "duplicatetab", "closepattern", "pintab", "mutetab", "movetab", "movetabwindow", "grouptabs", "colorgroup", "collapsegroup", "discardtab", "reloadtabs", "zoomin", "zoomout", "watchtab", "switchtab", "maximizewindow", "minimizewindow", "restorewindow", "focuswindow", "scratchwindow", "incognitowindow", "restoretab", "savelayout", "restorelayout", "findclones", "searchtabs", "badgetab", "attachmeta", "listaudio", "reopenrun", "snapshotsession"]);
|
|
787
936
|
var formactions = /* @__PURE__ */ new Set(["fillform", "filllabel", "fillplaceholder", "detectfields", "generatevalues", "saveprofiles", "asksubmit", "submitform", "readerrors", "retryform", "runwizard", "selectchain", "picktypeahead", "pickdate", "attachfile", "handoffcaptcha", "fillcard", "fillcode", "consentpassword", "skiphoneypot", "detectlogin", "detecttemplate"]);
|
|
@@ -789,6 +938,9 @@ var datasetactions = /* @__PURE__ */ new Set(["scrapetable", "exportcsv", "expor
|
|
|
789
938
|
var exportactions = /* @__PURE__ */ new Set(["exportcsv", "exportjson", "exportexcel", "copytable", "pushsheets", "streamdisk"]);
|
|
790
939
|
var filesactions = /* @__PURE__ */ new Set(["batchdownload", "pausedownload", "resumedownload", "verifydownload", "interceptmime", "exportnetlog", "readclipboard", "writeclipboard", "copyscreen", "quarantinedownload", "scanvirus", "namecaptures", "cleanupartifacts"]);
|
|
791
940
|
var captureactions = /* @__PURE__ */ new Set(["shotview", "shotfullpage", "shotelement", "shotregion", "contactsheet"]);
|
|
941
|
+
var mediaactions = /* @__PURE__ */ new Set(["capturepdf", "recordscreen", "captureaudio", "captureframe", "downloadimages", "shotcanvas", "probestream", "readmedia", "readassets", "timelapse", "convertimage", "makethumbs"]);
|
|
942
|
+
var httpactions = /* @__PURE__ */ new Set(["fetchurl", "parsejson", "parsehtml", "callrest", "callgraphql"]);
|
|
943
|
+
var credentialheaders = /* @__PURE__ */ new Set(["authorization", "proxy-authorization", "cookie", "cookie2", "set-cookie", "api-key", "x-api-key", "x-auth-token", "x-session-token", "proxy-authorization"]);
|
|
792
944
|
var fieldkinds = ["text", "email", "phone", "date", "number", "select", "check", "radio", "file", "password", "card", "code"];
|
|
793
945
|
var layoutmutationactions = /* @__PURE__ */ new Set(["grouptabs", "colorgroup", "collapsegroup", "savelayout", "restorelayout"]);
|
|
794
946
|
var groupcolors = ["grey", "blue", "red", "yellow", "green", "pink", "purple", "cyan", "orange"];
|
|
@@ -827,6 +979,7 @@ function requiredcapability(kind) {
|
|
|
827
979
|
if (kind === "batchdownload" || kind === "pausedownload" || kind === "resumedownload" || kind === "verifydownload" || kind === "interceptmime" || kind === "quarantinedownload" || kind === "scanvirus") return "downloads";
|
|
828
980
|
if (kind === "readclipboard") return "clipboardRead";
|
|
829
981
|
if (kind === "writeclipboard" || kind === "copyscreen") return "clipboardWrite";
|
|
982
|
+
if (kind === "downloadimages") return "downloads";
|
|
830
983
|
if (kind === "openlink" || kind === "openprivate" || kind === "navlist" || kind === "batchopen" || kind === "reopentab" || kind === "deeplink") return "tabs";
|
|
831
984
|
if (tabscommandactions.has(kind)) return "tabs";
|
|
832
985
|
return void 0;
|
|
@@ -1514,6 +1667,361 @@ function validatetabsgrammar(step, options) {
|
|
|
1514
1667
|
if (kind === "reopenrun" && !isnonempty(options.run)) return { allowed: false, reason: "A reviewed run id is required in options to reopen its tabs." };
|
|
1515
1668
|
return { allowed: true };
|
|
1516
1669
|
}
|
|
1670
|
+
function ismediakind(kind) {
|
|
1671
|
+
return mediaactions.has(kind);
|
|
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
|
+
}
|
|
1890
|
+
function isrecordingkind(kind) {
|
|
1891
|
+
return kind === "recordscreen" || kind === "captureaudio";
|
|
1892
|
+
}
|
|
1893
|
+
function mediagate(session, tabid2, origin, now) {
|
|
1894
|
+
if (!session || session.stoppedat) return { allowed: false, reason: "No active browser session exists for the media capture." };
|
|
1895
|
+
if (session.expiresat <= now) return { allowed: false, reason: "The browser session has expired and cannot capture media." };
|
|
1896
|
+
if (session.pausedat) return { allowed: false, reason: "The browser session is paused and cannot capture media." };
|
|
1897
|
+
if (session.tabid !== tabid2) return { allowed: false, reason: `The media capture needs the active tab grant of session tab ${session.tabid} and refuses tab ${tabid2}.` };
|
|
1898
|
+
if (!origingranted(session, origin)) return { allowed: false, reason: `The media capture of ${origin} needs the session origin grants first.` };
|
|
1899
|
+
return { allowed: true };
|
|
1900
|
+
}
|
|
1901
|
+
function recordingconsentgranted(step) {
|
|
1902
|
+
let options = {};
|
|
1903
|
+
try {
|
|
1904
|
+
options = parseoptions(step);
|
|
1905
|
+
} catch {
|
|
1906
|
+
options = {};
|
|
1907
|
+
}
|
|
1908
|
+
const consentref = options.consentref;
|
|
1909
|
+
if (typeof consentref !== "string" || !consentref.trim()) return { allowed: false, reason: "A recording of user activity requires a reviewed consent ref in options before it starts." };
|
|
1910
|
+
return { allowed: true };
|
|
1911
|
+
}
|
|
1912
|
+
function recordingwindow(settings) {
|
|
1913
|
+
const window2 = settings?.recordingwindow;
|
|
1914
|
+
return typeof window2 === "number" && Number.isFinite(window2) && window2 > 0 ? window2 : void 0;
|
|
1915
|
+
}
|
|
1916
|
+
function lapsebudgetallowed(interval, duration, wait) {
|
|
1917
|
+
if (!(interval > 0)) return { allowed: false, reason: "The reviewed lapse interval must be a positive number of milliseconds." };
|
|
1918
|
+
if (!(duration > 0)) return { allowed: false, reason: "The reviewed lapse duration must be a positive number of milliseconds." };
|
|
1919
|
+
if (wait !== void 0 && !(wait >= 0)) return { allowed: false, reason: "The reviewed wait budget must be zero or a positive number of milliseconds." };
|
|
1920
|
+
if (wait !== void 0 && duration > wait) return { allowed: false, reason: `The lapse duration of ${duration} milliseconds exceeds the reviewed wait budget of ${wait} milliseconds; review a wider budget or a shorter duration.` };
|
|
1921
|
+
return { allowed: true };
|
|
1922
|
+
}
|
|
1923
|
+
function validatemediagrammar(step, options) {
|
|
1924
|
+
const kind = step.kind;
|
|
1925
|
+
if (kind === "capturepdf") {
|
|
1926
|
+
const pdf = options.pdf;
|
|
1927
|
+
if (pdf !== void 0) {
|
|
1928
|
+
if (!pdf || typeof pdf !== "object" || Array.isArray(pdf)) return { allowed: false, reason: "The reviewed pdf options must be an object in options.pdf." };
|
|
1929
|
+
const pdfoptions = pdf;
|
|
1930
|
+
if (pdfoptions.paperwidth !== void 0 && (typeof pdfoptions.paperwidth !== "number" || !Number.isFinite(pdfoptions.paperwidth) || pdfoptions.paperwidth <= 0)) return { allowed: false, reason: "The reviewed pdf paper width must be a positive number of inches with no code cap." };
|
|
1931
|
+
if (pdfoptions.paperheight !== void 0 && (typeof pdfoptions.paperheight !== "number" || !Number.isFinite(pdfoptions.paperheight) || pdfoptions.paperheight <= 0)) return { allowed: false, reason: "The reviewed pdf paper height must be a positive number of inches with no code cap." };
|
|
1932
|
+
if (pdfoptions.margins !== void 0) {
|
|
1933
|
+
const margins = pdfoptions.margins;
|
|
1934
|
+
if (!margins || typeof margins !== "object" || Array.isArray(margins)) return { allowed: false, reason: "The reviewed pdf margins must be an object with top, right, bottom and left inches." };
|
|
1935
|
+
for (const side of ["top", "right", "bottom", "left"]) {
|
|
1936
|
+
const value = margins[side];
|
|
1937
|
+
if (value === void 0) continue;
|
|
1938
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0) return { allowed: false, reason: `The reviewed pdf ${side} margin must be zero or a positive number of inches; negative margins are refused.` };
|
|
1939
|
+
}
|
|
1940
|
+
}
|
|
1941
|
+
if (pdfoptions.scale !== void 0 && (typeof pdfoptions.scale !== "number" || !Number.isFinite(pdfoptions.scale) || pdfoptions.scale <= 0)) return { allowed: false, reason: "The reviewed pdf scale must be a positive number with no code cap." };
|
|
1942
|
+
if (pdfoptions.landscape !== void 0 && typeof pdfoptions.landscape !== "boolean") return { allowed: false, reason: "The reviewed pdf landscape flag must be a boolean." };
|
|
1943
|
+
if (pdfoptions.paginate !== void 0 && typeof pdfoptions.paginate !== "boolean") return { allowed: false, reason: "The reviewed pdf paginate flag must be a boolean." };
|
|
1944
|
+
}
|
|
1945
|
+
if (options.breakpoints !== void 0 && (!Array.isArray(options.breakpoints) || options.breakpoints.length === 0 || !options.breakpoints.every((item) => isnonempty(item)))) return { allowed: false, reason: "The reviewed pdf break points must be a non-empty list of selectors when present." };
|
|
1946
|
+
if (options.exporttarget !== void 0 && options.exporttarget !== "memory" && options.exporttarget !== "download") return { allowed: false, reason: "The reviewed pdf export target must be memory or download; pdf documents do not route to the clipboard." };
|
|
1947
|
+
if (options.name !== void 0 && !isnonempty(options.name)) return { allowed: false, reason: "The reviewed pdf artifact name must be a non-empty string." };
|
|
1948
|
+
}
|
|
1949
|
+
if (kind === "recordscreen" || kind === "captureaudio") {
|
|
1950
|
+
const recording = options.recording;
|
|
1951
|
+
if (recording !== void 0) {
|
|
1952
|
+
if (!recording || typeof recording !== "object" || Array.isArray(recording)) return { allowed: false, reason: "The reviewed recording options must be an object in options.recording." };
|
|
1953
|
+
const recordoptions = recording;
|
|
1954
|
+
if (recordoptions.scope !== void 0 && recordoptions.scope !== "tab" && recordoptions.scope !== "run") return { allowed: false, reason: "The reviewed recording scope must be tab or run." };
|
|
1955
|
+
if (recordoptions.fps !== void 0 && (typeof recordoptions.fps !== "number" || !Number.isFinite(recordoptions.fps) || recordoptions.fps <= 0)) return { allowed: false, reason: "The reviewed recording fps must be a positive number with no code ceiling." };
|
|
1956
|
+
if (recordoptions.bitrate !== void 0 && (typeof recordoptions.bitrate !== "number" || !Number.isFinite(recordoptions.bitrate) || recordoptions.bitrate <= 0)) return { allowed: false, reason: "The reviewed recording bitrate must be a positive number with no code ceiling." };
|
|
1957
|
+
if (recordoptions.audio !== void 0 && typeof recordoptions.audio !== "boolean") return { allowed: false, reason: "The reviewed recording audio flag must be a boolean." };
|
|
1958
|
+
}
|
|
1959
|
+
if (options.duration !== void 0 && (typeof options.duration !== "number" || !Number.isFinite(options.duration) || options.duration <= 0)) return { allowed: false, reason: "The reviewed recording duration must be a positive number of milliseconds with no code ceiling." };
|
|
1960
|
+
const consent = recordingconsentgranted(step);
|
|
1961
|
+
if (!consent.allowed) return consent;
|
|
1962
|
+
}
|
|
1963
|
+
if (kind === "captureframe") {
|
|
1964
|
+
if (options.timestamp !== void 0 && (typeof options.timestamp !== "number" || !Number.isFinite(options.timestamp) || options.timestamp < 0)) return { allowed: false, reason: "The reviewed frame timestamp must be zero or a positive number of seconds." };
|
|
1965
|
+
if (options.poster !== void 0 && typeof options.poster !== "boolean") return { allowed: false, reason: "The reviewed poster flag must be a boolean." };
|
|
1966
|
+
const capturecheck = validatecaptureoptions(options.capture);
|
|
1967
|
+
if (!capturecheck.allowed) return capturecheck;
|
|
1968
|
+
}
|
|
1969
|
+
if (kind === "downloadimages") {
|
|
1970
|
+
const filter = options.imagefilter;
|
|
1971
|
+
if (!filter || typeof filter !== "object" || Array.isArray(filter)) return { allowed: false, reason: "A reviewed imagefilter is required in options before any image downloads." };
|
|
1972
|
+
const imagefilter = filter;
|
|
1973
|
+
if (imagefilter.selector !== void 0 && !isnonempty(imagefilter.selector)) return { allowed: false, reason: "The reviewed imagefilter selector must be a non-empty selector from the reviewed selector grammar." };
|
|
1974
|
+
if (imagefilter.minwidth !== void 0 && (typeof imagefilter.minwidth !== "number" || !Number.isFinite(imagefilter.minwidth) || imagefilter.minwidth < 0)) return { allowed: false, reason: "The reviewed imagefilter minimum width must be zero or a positive number of pixels." };
|
|
1975
|
+
if (imagefilter.minheight !== void 0 && (typeof imagefilter.minheight !== "number" || !Number.isFinite(imagefilter.minheight) || imagefilter.minheight < 0)) return { allowed: false, reason: "The reviewed imagefilter minimum height must be zero or a positive number of pixels." };
|
|
1976
|
+
if (imagefilter.formats !== void 0 && (!Array.isArray(imagefilter.formats) || imagefilter.formats.length === 0 || !imagefilter.formats.every((item) => isnonempty(item)))) return { allowed: false, reason: "The reviewed imagefilter format list must be a non-empty list of mime or extension patterns when present." };
|
|
1977
|
+
if (options.naming !== void 0) {
|
|
1978
|
+
const namingcheck = validatecapturenaming(options.naming);
|
|
1979
|
+
if (!namingcheck.allowed) return namingcheck;
|
|
1980
|
+
}
|
|
1981
|
+
}
|
|
1982
|
+
if (kind === "shotcanvas") {
|
|
1983
|
+
const capturecheck = validatecaptureoptions(options.capture);
|
|
1984
|
+
if (!capturecheck.allowed) return capturecheck;
|
|
1985
|
+
}
|
|
1986
|
+
if (kind === "probestream" && options.selector !== void 0 && !isnonempty(options.selector)) return { allowed: false, reason: "The reviewed stream probe scope selector must be a non-empty string." };
|
|
1987
|
+
if (kind === "timelapse") {
|
|
1988
|
+
const lapse = options.lapse;
|
|
1989
|
+
if (!lapse || typeof lapse !== "object" || Array.isArray(lapse)) return { allowed: false, reason: "A reviewed lapse plan with interval, duration and format is required in options." };
|
|
1990
|
+
const plan = lapse;
|
|
1991
|
+
if (typeof plan.interval !== "number" || !Number.isFinite(plan.interval) || plan.interval <= 0) return { allowed: false, reason: "The reviewed lapse interval must be a positive number of milliseconds with no code ceiling." };
|
|
1992
|
+
if (typeof plan.duration !== "number" || !Number.isFinite(plan.duration) || plan.duration <= 0) return { allowed: false, reason: "The reviewed lapse duration must be a positive number of milliseconds with no code ceiling." };
|
|
1993
|
+
if (plan.format !== void 0 && plan.format !== "png" && plan.format !== "jpeg" && plan.format !== "webp") return { allowed: false, reason: "The reviewed lapse format must be png, jpeg or webp." };
|
|
1994
|
+
const budget = lapsebudgetallowed(plan.interval, plan.duration, typeof options.wait === "number" ? options.wait : void 0);
|
|
1995
|
+
if (!budget.allowed) return budget;
|
|
1996
|
+
const capturecheck = validatecaptureoptions(options.capture);
|
|
1997
|
+
if (!capturecheck.allowed) return capturecheck;
|
|
1998
|
+
}
|
|
1999
|
+
if (kind === "convertimage" || kind === "makethumbs") {
|
|
2000
|
+
const single = options.capture;
|
|
2001
|
+
const list = options.captures;
|
|
2002
|
+
const hasone = isnonempty(single);
|
|
2003
|
+
const haslist = Array.isArray(list) && list.length > 0 && list.every((item) => isnonempty(item));
|
|
2004
|
+
if (!hasone && !haslist) return { allowed: false, reason: "A reviewed capture id or a reviewed non-empty capture id list is required in options." };
|
|
2005
|
+
if (hasone && haslist) return { allowed: false, reason: "The reviewed step needs one capture id or a capture id list, not both." };
|
|
2006
|
+
}
|
|
2007
|
+
if (kind === "convertimage") {
|
|
2008
|
+
const convert = options.convert;
|
|
2009
|
+
if (!convert || typeof convert !== "object" || Array.isArray(convert)) return { allowed: false, reason: "A reviewed convert directive with a target format is required in options." };
|
|
2010
|
+
const directive = convert;
|
|
2011
|
+
if (directive.target !== "png" && directive.target !== "jpeg" && directive.target !== "webp") return { allowed: false, reason: "The reviewed conversion target must be png, jpeg or webp." };
|
|
2012
|
+
if (directive.source !== void 0 && directive.source !== "png" && directive.source !== "jpeg" && directive.source !== "webp") return { allowed: false, reason: "The reviewed conversion source must be png, jpeg or webp." };
|
|
2013
|
+
if (directive.quality !== void 0 && (typeof directive.quality !== "number" || !Number.isFinite(directive.quality) || directive.quality < 0 || directive.quality > 100)) return { allowed: false, reason: "The reviewed conversion quality must stay between zero and one hundred with no code cap inside that range." };
|
|
2014
|
+
}
|
|
2015
|
+
if (kind === "makethumbs") {
|
|
2016
|
+
const thumb = options.thumb;
|
|
2017
|
+
if (!thumb || typeof thumb !== "object" || Array.isArray(thumb)) return { allowed: false, reason: "A reviewed thumb directive with size, fit and suffix is required in options." };
|
|
2018
|
+
const directive = thumb;
|
|
2019
|
+
if (typeof directive.size !== "number" || !Number.isFinite(directive.size) || directive.size <= 0) return { allowed: false, reason: "The reviewed thumbnail size must be a positive number of pixels with no fixed set." };
|
|
2020
|
+
if (directive.fit !== "cover" && directive.fit !== "contain") return { allowed: false, reason: "The reviewed thumbnail fit must be cover or contain." };
|
|
2021
|
+
if (!isnonempty(directive.suffix)) return { allowed: false, reason: "The reviewed thumbnail naming suffix must be a non-empty string." };
|
|
2022
|
+
}
|
|
2023
|
+
return { allowed: true };
|
|
2024
|
+
}
|
|
1517
2025
|
function validatestep(step, origin) {
|
|
1518
2026
|
if (!allowedactions.has(step.kind)) return { allowed: false, reason: "Unsupported action kind." };
|
|
1519
2027
|
if (!step.summary.trim()) return { allowed: false, reason: "A human-readable action summary is required." };
|
|
@@ -1735,6 +2243,14 @@ function validatestep(step, origin) {
|
|
|
1735
2243
|
const capturecheck = validatecapturegrammar(step, options);
|
|
1736
2244
|
if (!capturecheck.allowed) return capturecheck;
|
|
1737
2245
|
}
|
|
2246
|
+
if (ismediakind(step.kind)) {
|
|
2247
|
+
const mediacheck = validatemediagrammar(step, options);
|
|
2248
|
+
if (!mediacheck.allowed) return mediacheck;
|
|
2249
|
+
}
|
|
2250
|
+
if (ishttpkind(step.kind)) {
|
|
2251
|
+
const httpcheck = validatehttpgrammar(step, options);
|
|
2252
|
+
if (!httpcheck.allowed) return httpcheck;
|
|
2253
|
+
}
|
|
1738
2254
|
if (step.kind === "tabcreate") {
|
|
1739
2255
|
if (options.background !== void 0 && typeof options.background !== "boolean") return { allowed: false, reason: "The reviewed background flag must be a boolean." };
|
|
1740
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." };
|
|
@@ -1806,6 +2322,25 @@ function canexecute(input) {
|
|
|
1806
2322
|
const target = captureoptions.capture?.exporttarget;
|
|
1807
2323
|
if (target !== void 0 && target !== "memory" && target !== "download" && target !== "clipboard") return { allowed: false, reason: "The capture export target must be memory, download or clipboard." };
|
|
1808
2324
|
}
|
|
2325
|
+
if (ismediakind(input.step.kind)) {
|
|
2326
|
+
const mediagatecheck = mediagate(input.session, input.tabid, input.origin, now);
|
|
2327
|
+
if (!mediagatecheck.allowed) return mediagatecheck;
|
|
2328
|
+
}
|
|
2329
|
+
if (isrecordingkind(input.step.kind)) {
|
|
2330
|
+
const recordinggate = recordingconsentgranted(input.step);
|
|
2331
|
+
if (!recordinggate.allowed) return recordinggate;
|
|
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
|
+
}
|
|
1809
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") {
|
|
1810
2345
|
let options = {};
|
|
1811
2346
|
try {
|
|
@@ -1927,9 +2462,24 @@ function recordpair(progress, planid, stepid, pair, now) {
|
|
|
1927
2462
|
const outcome = { stepid, ok: true, summary: `Paired the before shot ${pair.beforeid} with the after shot ${pair.afterid} around the ${pair.actionkind} action.`, details: { shotpair: { id: pair.id, beforeid: pair.beforeid, afterid: pair.afterid, actionkind: pair.actionkind, ...pair.target !== void 0 ? { target: pair.target } : {}, ...pair.domsnapshotid !== void 0 ? { domsnapshotid: pair.domsnapshotid } : {} } }, at: now };
|
|
1928
2463
|
return recordoutcome(base, planid, outcome, now);
|
|
1929
2464
|
}
|
|
2465
|
+
function recordmedia(progress, planid, stepid, media, now) {
|
|
2466
|
+
const base = progress && progress.planid === planid ? progress : emptyprogress(planid, now);
|
|
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 };
|
|
2468
|
+
return recordoutcome(base, planid, outcome, now);
|
|
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
|
+
}
|
|
1930
2480
|
|
|
1931
2481
|
// version.ts
|
|
1932
|
-
var packageversion = "1.1.
|
|
2482
|
+
var packageversion = "1.1.42";
|
|
1933
2483
|
|
|
1934
2484
|
// types.ts
|
|
1935
2485
|
var protocolversion = packageversion;
|
|
@@ -1943,9 +2493,10 @@ function text(value, field) {
|
|
|
1943
2493
|
if (typeof value !== "string" || !value.trim()) throw new Error(`${field} must be a non-empty string.`);
|
|
1944
2494
|
return value.trim();
|
|
1945
2495
|
}
|
|
1946
|
-
function parseproposal(value, origin) {
|
|
2496
|
+
function parseproposal(value, origin, grants) {
|
|
1947
2497
|
const root = record(value);
|
|
1948
2498
|
if (root.version !== protocolversion) throw new Error("Unsupported protocol version.");
|
|
2499
|
+
const covered = grants !== void 0 && grants.length > 0 ? grants : [origin];
|
|
1949
2500
|
const planinput = record(root.plan);
|
|
1950
2501
|
const stepsinput = planinput.steps;
|
|
1951
2502
|
if (!Array.isArray(stepsinput) || stepsinput.length === 0) throw new Error("A plan needs at least one step.");
|
|
@@ -1963,6 +2514,17 @@ function parseproposal(value, origin) {
|
|
|
1963
2514
|
};
|
|
1964
2515
|
const evaluation = validatestep(step, origin);
|
|
1965
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
|
+
}
|
|
1966
2528
|
return step;
|
|
1967
2529
|
});
|
|
1968
2530
|
for (const step of steps) {
|
|
@@ -1993,7 +2555,7 @@ function requestbody(input) {
|
|
|
1993
2555
|
return JSON.stringify({ version: protocolversion, objective: input.objective, session: input.session, observation: input.observation, capabilities: input.capabilities });
|
|
1994
2556
|
}
|
|
1995
2557
|
function outcomeresponse(input) {
|
|
1996
|
-
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 } : {} });
|
|
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 } : {} });
|
|
1997
2559
|
}
|
|
1998
2560
|
function mapresponse(input) {
|
|
1999
2561
|
return JSON.stringify({ version: protocolversion, planid: input.plan.id, planstate: input.plan.state, map: input.map });
|
|
@@ -2058,6 +2620,17 @@ function quarantinereport(input) {
|
|
|
2058
2620
|
function capturereport(input) {
|
|
2059
2621
|
return { version: protocolversion, records: input.records, pairs: input.pairs };
|
|
2060
2622
|
}
|
|
2623
|
+
function mediareport(input) {
|
|
2624
|
+
return { version: protocolversion, records: input.records, images: input.images };
|
|
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
|
+
}
|
|
2061
2634
|
|
|
2062
2635
|
// capture.ts
|
|
2063
2636
|
var capturekinds = ["shotview", "shotfullpage", "shotelement", "shotregion", "contactsheet"];
|
|
@@ -2245,6 +2818,576 @@ function annotationplanof(input) {
|
|
|
2245
2818
|
return plan;
|
|
2246
2819
|
}
|
|
2247
2820
|
|
|
2821
|
+
// media.ts
|
|
2822
|
+
var mediakinds = ["capturepdf", "recordscreen", "captureaudio", "captureframe", "downloadimages", "shotcanvas", "probestream", "readmedia", "readassets", "timelapse", "convertimage", "makethumbs"];
|
|
2823
|
+
var defaultpaperwidth = 8.5;
|
|
2824
|
+
var defaultpaperheight = 11;
|
|
2825
|
+
var defaultmargins = { top: 0.4, right: 0.4, bottom: 0.4, left: 0.4 };
|
|
2826
|
+
var pdfpointsperinch = 72;
|
|
2827
|
+
var basefontsize = 11;
|
|
2828
|
+
function pdfoptionsof(value) {
|
|
2829
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return {};
|
|
2830
|
+
const options = value;
|
|
2831
|
+
const normalized = {};
|
|
2832
|
+
if (typeof options.paperwidth === "number" && Number.isFinite(options.paperwidth)) normalized.paperwidth = options.paperwidth;
|
|
2833
|
+
if (typeof options.paperheight === "number" && Number.isFinite(options.paperheight)) normalized.paperheight = options.paperheight;
|
|
2834
|
+
if (options.margins && typeof options.margins === "object" && !Array.isArray(options.margins)) {
|
|
2835
|
+
const margins = options.margins;
|
|
2836
|
+
const top = typeof margins.top === "number" ? margins.top : defaultmargins.top;
|
|
2837
|
+
const right = typeof margins.right === "number" ? margins.right : defaultmargins.right;
|
|
2838
|
+
const bottom = typeof margins.bottom === "number" ? margins.bottom : defaultmargins.bottom;
|
|
2839
|
+
const left = typeof margins.left === "number" ? margins.left : defaultmargins.left;
|
|
2840
|
+
normalized.margins = { top, right, bottom, left };
|
|
2841
|
+
}
|
|
2842
|
+
if (typeof options.scale === "number" && Number.isFinite(options.scale)) normalized.scale = options.scale;
|
|
2843
|
+
if (typeof options.landscape === "boolean") normalized.landscape = options.landscape;
|
|
2844
|
+
if (typeof options.paginate === "boolean") normalized.paginate = options.paginate;
|
|
2845
|
+
return normalized;
|
|
2846
|
+
}
|
|
2847
|
+
function pdfpagesize(options) {
|
|
2848
|
+
const width = (options.paperwidth ?? defaultpaperwidth) * pdfpointsperinch;
|
|
2849
|
+
const height = (options.paperheight ?? defaultpaperheight) * pdfpointsperinch;
|
|
2850
|
+
return options.landscape === true ? { width: height, height: width } : { width, height };
|
|
2851
|
+
}
|
|
2852
|
+
function pdfmargins(options) {
|
|
2853
|
+
const margins = options.margins ?? defaultmargins;
|
|
2854
|
+
return { top: margins.top * pdfpointsperinch, right: margins.right * pdfpointsperinch, bottom: margins.bottom * pdfpointsperinch, left: margins.left * pdfpointsperinch };
|
|
2855
|
+
}
|
|
2856
|
+
function pdffontsize(options) {
|
|
2857
|
+
return basefontsize * (options.scale ?? 1);
|
|
2858
|
+
}
|
|
2859
|
+
function pdftextlayout(text2, options) {
|
|
2860
|
+
const size = pdfpagesize(options);
|
|
2861
|
+
const margins = pdfmargins(options);
|
|
2862
|
+
const fontsize = pdffontsize(options);
|
|
2863
|
+
const leading = fontsize * 1.35;
|
|
2864
|
+
const linesperpage = Math.max(1, Math.floor((size.height - margins.top - margins.bottom) / leading));
|
|
2865
|
+
const columns = Math.max(1, Math.floor((size.width - margins.left - margins.right) / (fontsize * 0.5)));
|
|
2866
|
+
const wrapped = [];
|
|
2867
|
+
for (const paragraph of text2.split(/\r?\n/)) {
|
|
2868
|
+
let line = "";
|
|
2869
|
+
for (const word of paragraph.split(/\s+/).filter(Boolean)) {
|
|
2870
|
+
const candidate = line ? `${line} ${word}` : word;
|
|
2871
|
+
if (candidate.length <= columns) {
|
|
2872
|
+
line = candidate;
|
|
2873
|
+
continue;
|
|
2874
|
+
}
|
|
2875
|
+
if (line) wrapped.push(line);
|
|
2876
|
+
if (word.length <= columns) {
|
|
2877
|
+
line = word;
|
|
2878
|
+
continue;
|
|
2879
|
+
}
|
|
2880
|
+
for (let index = 0; index < word.length; index += columns) wrapped.push(word.slice(index, index + columns));
|
|
2881
|
+
line = "";
|
|
2882
|
+
}
|
|
2883
|
+
wrapped.push(line);
|
|
2884
|
+
if (wrapped.length >= linesperpage) break;
|
|
2885
|
+
}
|
|
2886
|
+
return wrapped.slice(0, linesperpage);
|
|
2887
|
+
}
|
|
2888
|
+
function pdfsegments(scrollheight, viewportheight, breaks) {
|
|
2889
|
+
if (scrollheight <= 0) return [];
|
|
2890
|
+
const step = viewportheight > 0 ? viewportheight : scrollheight;
|
|
2891
|
+
const cuts = [0, ...breaks.filter((top) => Number.isFinite(top) && top > 0 && top < scrollheight).map((top) => Math.round(top))].filter((top, index2, list) => list.indexOf(top) === index2).sort((left, right) => left - right);
|
|
2892
|
+
const segments = [];
|
|
2893
|
+
let index = 0;
|
|
2894
|
+
let cursor = 0;
|
|
2895
|
+
while (cursor < scrollheight) {
|
|
2896
|
+
while (index < cuts.length && (cuts[index] ?? 0) <= cursor) index += 1;
|
|
2897
|
+
const nextcut = index < cuts.length ? cuts[index] : void 0;
|
|
2898
|
+
const next = nextcut !== void 0 ? Math.min(nextcut, scrollheight) : Math.min(cursor + step, scrollheight);
|
|
2899
|
+
if (next <= cursor) break;
|
|
2900
|
+
segments.push({ top: cursor, height: next - cursor });
|
|
2901
|
+
cursor = next;
|
|
2902
|
+
}
|
|
2903
|
+
return segments.length > 0 ? segments : [{ top: 0, height: scrollheight }];
|
|
2904
|
+
}
|
|
2905
|
+
function pdfescape(text2) {
|
|
2906
|
+
let escaped = "";
|
|
2907
|
+
for (const character of text2) {
|
|
2908
|
+
const code = character.charCodeAt(0);
|
|
2909
|
+
if (character === "(" || character === ")" || character === "\\") escaped += `\\${character}`;
|
|
2910
|
+
else if (code >= 32 && code <= 255) escaped += character;
|
|
2911
|
+
else escaped += "?";
|
|
2912
|
+
}
|
|
2913
|
+
return escaped;
|
|
2914
|
+
}
|
|
2915
|
+
function buildpdf(pages, options) {
|
|
2916
|
+
const size = pdfpagesize(options);
|
|
2917
|
+
const margins = pdfmargins(options);
|
|
2918
|
+
const fontsize = pdffontsize(options);
|
|
2919
|
+
const leading = fontsize * 1.35;
|
|
2920
|
+
const laidout = (pages.length > 0 ? pages : [""]).map((text2) => pdftextlayout(text2, options));
|
|
2921
|
+
const objects = [];
|
|
2922
|
+
const kids = laidout.map((_, index) => `${4 + index * 2} 0 R`).join(" ");
|
|
2923
|
+
objects.push(`<< /Type /Catalog /Pages 2 0 R >>`);
|
|
2924
|
+
objects.push(`<< /Type /Pages /Kids [${kids}] /Count ${laidout.length} >>`);
|
|
2925
|
+
objects.push(`<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>`);
|
|
2926
|
+
for (let pageindex = 0; pageindex < laidout.length; pageindex += 1) {
|
|
2927
|
+
const lines = laidout[pageindex] ?? [];
|
|
2928
|
+
const operators = ["BT", `/F1 ${fontsize} Tf`, `${leading.toFixed(2)} TL`, `${margins.left.toFixed(2)} ${(size.height - margins.top - fontsize).toFixed(2)} Td`];
|
|
2929
|
+
for (let lineindex = 0; lineindex < lines.length; lineindex += 1) {
|
|
2930
|
+
if (lineindex > 0) operators.push("T*");
|
|
2931
|
+
operators.push(`(${pdfescape(lines[lineindex] ?? "")}) Tj`);
|
|
2932
|
+
}
|
|
2933
|
+
operators.push("ET");
|
|
2934
|
+
const content = operators.join("\n");
|
|
2935
|
+
objects.push(`<< /Type /Page /Parent 2 0 R /MediaBox [0 0 ${size.width.toFixed(2)} ${size.height.toFixed(2)}] /Resources << /Font << /F1 3 0 R >> >> /Contents ${5 + pageindex * 2} 0 R >>`);
|
|
2936
|
+
objects.push(`<< /Length ${content.length} >>
|
|
2937
|
+
stream
|
|
2938
|
+
${content}
|
|
2939
|
+
endstream`);
|
|
2940
|
+
}
|
|
2941
|
+
let document2 = "%PDF-1.4\n";
|
|
2942
|
+
const offsets = [];
|
|
2943
|
+
for (let index = 0; index < objects.length; index += 1) {
|
|
2944
|
+
offsets.push(document2.length);
|
|
2945
|
+
document2 += `${index + 1} 0 obj
|
|
2946
|
+
${objects[index]}
|
|
2947
|
+
endobj
|
|
2948
|
+
`;
|
|
2949
|
+
}
|
|
2950
|
+
const xrefstart = document2.length;
|
|
2951
|
+
document2 += `xref
|
|
2952
|
+
0 ${objects.length + 1}
|
|
2953
|
+
0000000000 65535 f
|
|
2954
|
+
`;
|
|
2955
|
+
for (const offset of offsets) document2 += `${String(offset).padStart(10, "0")} 00000 n
|
|
2956
|
+
`;
|
|
2957
|
+
document2 += `trailer
|
|
2958
|
+
<< /Size ${objects.length + 1} /Root 1 0 R >>
|
|
2959
|
+
startxref
|
|
2960
|
+
${xrefstart}
|
|
2961
|
+
%%EOF
|
|
2962
|
+
`;
|
|
2963
|
+
return { document: document2, bytes: document2.length, pages: laidout.length, pagewidth: Math.round(size.width), pageheight: Math.round(size.height) };
|
|
2964
|
+
}
|
|
2965
|
+
function recordingoptionsof(value) {
|
|
2966
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return {};
|
|
2967
|
+
const options = value;
|
|
2968
|
+
const normalized = {};
|
|
2969
|
+
if (options.scope === "tab" || options.scope === "run") normalized.scope = options.scope;
|
|
2970
|
+
if (typeof options.fps === "number" && Number.isFinite(options.fps)) normalized.fps = options.fps;
|
|
2971
|
+
if (typeof options.bitrate === "number" && Number.isFinite(options.bitrate)) normalized.bitrate = options.bitrate;
|
|
2972
|
+
if (typeof options.audio === "boolean") normalized.audio = options.audio;
|
|
2973
|
+
return normalized;
|
|
2974
|
+
}
|
|
2975
|
+
function newrecording(input) {
|
|
2976
|
+
return {
|
|
2977
|
+
id: input.id,
|
|
2978
|
+
runid: input.runid,
|
|
2979
|
+
stepid: input.stepid,
|
|
2980
|
+
tabid: input.tabid,
|
|
2981
|
+
kind: input.kind,
|
|
2982
|
+
scope: input.options.scope ?? "tab",
|
|
2983
|
+
format: input.kind === "audio" ? "evidence" : "frames",
|
|
2984
|
+
startedat: input.at,
|
|
2985
|
+
at: input.at,
|
|
2986
|
+
...input.options.fps !== void 0 ? { fps: input.options.fps } : {},
|
|
2987
|
+
...input.options.bitrate !== void 0 ? { bitrate: input.options.bitrate } : {},
|
|
2988
|
+
...input.options.audio !== void 0 ? { audio: input.options.audio } : {},
|
|
2989
|
+
frames: []
|
|
2990
|
+
};
|
|
2991
|
+
}
|
|
2992
|
+
function finishrecording(record2, endat) {
|
|
2993
|
+
return { ...record2, endedat: endat, duration: Math.max(0, endat - record2.startedat) };
|
|
2994
|
+
}
|
|
2995
|
+
function frameinterval(fps) {
|
|
2996
|
+
if (!Number.isFinite(fps) || fps <= 0) return 1e3;
|
|
2997
|
+
return Math.max(1, Math.round(1e3 / fps));
|
|
2998
|
+
}
|
|
2999
|
+
function imagefilterof(value) {
|
|
3000
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return {};
|
|
3001
|
+
const options = value;
|
|
3002
|
+
const normalized = {};
|
|
3003
|
+
if (typeof options.selector === "string" && options.selector.trim()) normalized.selector = options.selector.trim();
|
|
3004
|
+
if (typeof options.minwidth === "number" && Number.isFinite(options.minwidth)) normalized.minwidth = options.minwidth;
|
|
3005
|
+
if (typeof options.minheight === "number" && Number.isFinite(options.minheight)) normalized.minheight = options.minheight;
|
|
3006
|
+
if (Array.isArray(options.formats) && options.formats.every((item) => typeof item === "string" && item.trim())) normalized.formats = options.formats;
|
|
3007
|
+
return normalized;
|
|
3008
|
+
}
|
|
3009
|
+
function imagematches(image, filter) {
|
|
3010
|
+
if (filter.minwidth !== void 0 && image.width < filter.minwidth) return false;
|
|
3011
|
+
if (filter.minheight !== void 0 && image.height < filter.minheight) return false;
|
|
3012
|
+
if (filter.formats !== void 0 && filter.formats.length > 0) {
|
|
3013
|
+
const mime = image.mime.toLowerCase();
|
|
3014
|
+
const matches = filter.formats.some((format) => {
|
|
3015
|
+
const wanted = format.toLowerCase().trim();
|
|
3016
|
+
return mime === wanted || mime === `image/${wanted}` || mime.endsWith(`/${wanted}`);
|
|
3017
|
+
});
|
|
3018
|
+
if (!matches) return false;
|
|
3019
|
+
}
|
|
3020
|
+
return true;
|
|
3021
|
+
}
|
|
3022
|
+
function dedupeimages(images) {
|
|
3023
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3024
|
+
const unique = [];
|
|
3025
|
+
for (const image of images) {
|
|
3026
|
+
if (seen.has(image.url)) continue;
|
|
3027
|
+
seen.add(image.url);
|
|
3028
|
+
unique.push(image);
|
|
3029
|
+
}
|
|
3030
|
+
return unique;
|
|
3031
|
+
}
|
|
3032
|
+
function imagenames(rule, run, step, count, extension) {
|
|
3033
|
+
const names = [];
|
|
3034
|
+
for (let index = 1; index <= Math.max(0, Math.round(count)); index += 1) names.push(buildname(rule, { run, step, sequence: index, kind: "image" }, extension));
|
|
3035
|
+
return names;
|
|
3036
|
+
}
|
|
3037
|
+
function lapseplanof(value) {
|
|
3038
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
|
|
3039
|
+
const options = value;
|
|
3040
|
+
if (typeof options.interval !== "number" || !Number.isFinite(options.interval)) return void 0;
|
|
3041
|
+
if (typeof options.duration !== "number" || !Number.isFinite(options.duration)) return void 0;
|
|
3042
|
+
const format = options.format === "jpeg" || options.format === "webp" ? options.format : "png";
|
|
3043
|
+
return { interval: options.interval, duration: options.duration, format };
|
|
3044
|
+
}
|
|
3045
|
+
function lapseframes(plan) {
|
|
3046
|
+
if (!(plan.interval > 0) || !(plan.duration > 0)) return [];
|
|
3047
|
+
const frames = [];
|
|
3048
|
+
for (let time = 0; time < plan.duration; time += plan.interval) frames.push(Math.round(time));
|
|
3049
|
+
return frames;
|
|
3050
|
+
}
|
|
3051
|
+
function convertdirectiveof(value) {
|
|
3052
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
|
|
3053
|
+
const options = value;
|
|
3054
|
+
if (options.target !== "png" && options.target !== "jpeg" && options.target !== "webp") return void 0;
|
|
3055
|
+
const normalized = { target: options.target };
|
|
3056
|
+
if (options.source === "png" || options.source === "jpeg" || options.source === "webp") normalized.source = options.source;
|
|
3057
|
+
if (typeof options.quality === "number" && Number.isFinite(options.quality)) normalized.quality = options.quality;
|
|
3058
|
+
return normalized;
|
|
3059
|
+
}
|
|
3060
|
+
function thumbdirectiveof(value) {
|
|
3061
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
|
|
3062
|
+
const options = value;
|
|
3063
|
+
if (typeof options.size !== "number" || !Number.isFinite(options.size) || options.size <= 0) return void 0;
|
|
3064
|
+
if (options.fit !== "cover" && options.fit !== "contain") return void 0;
|
|
3065
|
+
if (typeof options.suffix !== "string" || !options.suffix.trim()) return void 0;
|
|
3066
|
+
return { size: options.size, fit: options.fit, suffix: options.suffix.trim() };
|
|
3067
|
+
}
|
|
3068
|
+
function thumbgeometry(source, directive) {
|
|
3069
|
+
const size = Math.max(1, Math.round(directive.size));
|
|
3070
|
+
if (directive.fit === "contain") {
|
|
3071
|
+
const scale2 = Math.min(size / Math.max(1, source.width), size / Math.max(1, source.height));
|
|
3072
|
+
const dw = Math.max(1, Math.round(source.width * scale2));
|
|
3073
|
+
const dh = Math.max(1, Math.round(source.height * scale2));
|
|
3074
|
+
return { sx: 0, sy: 0, sw: source.width, sh: source.height, dx: Math.floor((size - dw) / 2), dy: Math.floor((size - dh) / 2), dw, dh, width: size, height: size };
|
|
3075
|
+
}
|
|
3076
|
+
const scale = Math.max(size / Math.max(1, source.width), size / Math.max(1, source.height));
|
|
3077
|
+
const sw = Math.min(source.width, Math.round(size / scale));
|
|
3078
|
+
const sh = Math.min(source.height, Math.round(size / scale));
|
|
3079
|
+
return { sx: Math.floor((source.width - sw) / 2), sy: Math.floor((source.height - sh) / 2), sw, sh, dx: 0, dy: 0, dw: size, dh: size, width: size, height: size };
|
|
3080
|
+
}
|
|
3081
|
+
function mediaentries(raw) {
|
|
3082
|
+
return raw.map((entry) => ({
|
|
3083
|
+
url: typeof entry.url === "string" ? entry.url : "",
|
|
3084
|
+
mime: typeof entry.mime === "string" ? entry.mime : "",
|
|
3085
|
+
duration: typeof entry.duration === "number" && Number.isFinite(entry.duration) ? entry.duration : 0,
|
|
3086
|
+
width: typeof entry.width === "number" && Number.isFinite(entry.width) ? Math.round(entry.width) : 0,
|
|
3087
|
+
height: typeof entry.height === "number" && Number.isFinite(entry.height) ? Math.round(entry.height) : 0,
|
|
3088
|
+
codecs: typeof entry.codecs === "string" ? entry.codecs : "",
|
|
3089
|
+
tracks: Array.isArray(entry.tracks) ? entry.tracks.filter((item) => typeof item === "string") : []
|
|
3090
|
+
}));
|
|
3091
|
+
}
|
|
3092
|
+
function assetentries(raw) {
|
|
3093
|
+
return raw.map((entry) => ({
|
|
3094
|
+
kind: entry.kind === "logo" ? "logo" : "favicon",
|
|
3095
|
+
url: typeof entry.url === "string" ? entry.url : "",
|
|
3096
|
+
bytes: typeof entry.bytes === "number" && Number.isFinite(entry.bytes) ? entry.bytes : 0,
|
|
3097
|
+
...typeof entry.sizes === "string" && entry.sizes.trim() ? { sizes: entry.sizes.trim() } : {}
|
|
3098
|
+
}));
|
|
3099
|
+
}
|
|
3100
|
+
function streamsummaries(raw) {
|
|
3101
|
+
return raw.map((entry) => {
|
|
3102
|
+
const tracks = Array.isArray(entry.tracks) ? entry.tracks : [];
|
|
3103
|
+
return {
|
|
3104
|
+
kind: typeof entry.kind === "string" ? entry.kind : "stream",
|
|
3105
|
+
tracks: tracks.length,
|
|
3106
|
+
label: typeof entry.label === "string" ? entry.label : "",
|
|
3107
|
+
live: entry.live === true,
|
|
3108
|
+
detail: tracks.map((track) => {
|
|
3109
|
+
const item = track;
|
|
3110
|
+
return {
|
|
3111
|
+
kind: typeof item.kind === "string" ? item.kind : "",
|
|
3112
|
+
label: typeof item.label === "string" ? item.label : "",
|
|
3113
|
+
...typeof item.width === "number" && Number.isFinite(item.width) ? { width: Math.round(item.width) } : {},
|
|
3114
|
+
...typeof item.height === "number" && Number.isFinite(item.height) ? { height: Math.round(item.height) } : {},
|
|
3115
|
+
...typeof item.framerate === "number" && Number.isFinite(item.framerate) ? { framerate: item.framerate } : {},
|
|
3116
|
+
state: typeof item.state === "string" ? item.state : ""
|
|
3117
|
+
};
|
|
3118
|
+
})
|
|
3119
|
+
};
|
|
3120
|
+
});
|
|
3121
|
+
}
|
|
3122
|
+
|
|
3123
|
+
// httpclient.ts
|
|
3124
|
+
var httpkinds = ["fetchurl", "parsejson", "parsehtml", "callrest", "callgraphql"];
|
|
3125
|
+
var redirectstatuses = /* @__PURE__ */ new Set([301, 302, 303, 307, 308]);
|
|
3126
|
+
var bodilessmethods = /* @__PURE__ */ new Set(["GET", "HEAD"]);
|
|
3127
|
+
function statusclassof(status) {
|
|
3128
|
+
if (status >= 100 && status < 200) return "informational";
|
|
3129
|
+
if (status >= 200 && status < 300) return "success";
|
|
3130
|
+
if (status >= 300 && status < 400) return "redirect";
|
|
3131
|
+
if (status >= 400 && status < 500) return "clienterror";
|
|
3132
|
+
if (status >= 500 && status < 600) return "servererror";
|
|
3133
|
+
return "unknown";
|
|
3134
|
+
}
|
|
3135
|
+
function templateurl(template, values) {
|
|
3136
|
+
return template.replace(/\{([a-z0-9_]+)\}/gi, (whole, name) => values[name] === void 0 ? whole : String(values[name]));
|
|
3137
|
+
}
|
|
3138
|
+
function fetchrequestof(value) {
|
|
3139
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
|
|
3140
|
+
const options = value;
|
|
3141
|
+
if (typeof options.url !== "string" || !options.url.trim()) return void 0;
|
|
3142
|
+
const request = { url: options.url.trim() };
|
|
3143
|
+
if (typeof options.method === "string" && options.method.trim()) request.method = options.method.trim().toUpperCase();
|
|
3144
|
+
if (options.headers && typeof options.headers === "object" && !Array.isArray(options.headers)) {
|
|
3145
|
+
const headers = {};
|
|
3146
|
+
for (const [name, headervalue] of Object.entries(options.headers)) {
|
|
3147
|
+
if (typeof headervalue === "string") headers[name] = headervalue;
|
|
3148
|
+
}
|
|
3149
|
+
request.headers = headers;
|
|
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
|
+
|
|
2248
3391
|
// extension/browsertabs.ts
|
|
2249
3392
|
var browserkinds = /* @__PURE__ */ new Set(["tablist", "tabcreate", "tabactivate", "tabclose", "tabreload", "tabsnapshot", "windowlist", "windowcreate", "windowclose", "zoomset", "windowresize", "downloadfile"]);
|
|
2250
3393
|
function isbrowserkind(kind) {
|
|
@@ -3444,9 +4587,9 @@ function stepoptions2(step) {
|
|
|
3444
4587
|
}
|
|
3445
4588
|
async function refreshcapabilities() {
|
|
3446
4589
|
const report = await readcapabilities();
|
|
3447
|
-
const
|
|
3448
|
-
await memory.setcapabilities(
|
|
3449
|
-
return
|
|
4590
|
+
const withmedia = { ...report, captures: [...capturekinds], media: [...mediakinds], http: [...httpkinds] };
|
|
4591
|
+
await memory.setcapabilities(withmedia);
|
|
4592
|
+
return withmedia;
|
|
3450
4593
|
}
|
|
3451
4594
|
async function activecontext() {
|
|
3452
4595
|
const [tab] = await chrome.tabs.query({ active: true, lastFocusedWindow: true });
|
|
@@ -3570,7 +4713,7 @@ async function propose(objective, remote) {
|
|
|
3570
4713
|
if (!config) throw new Error("Configure an approved HTTPS endpoint before requesting a remote proposal.");
|
|
3571
4714
|
const response = await fetch(config.endpoint, { method: "POST", headers: { "content-type": "application/json" }, credentials: "omit", body: requestbody({ objective: objective.trim(), session, observation, capabilities }) });
|
|
3572
4715
|
if (!response.ok) throw new Error(`Proposal endpoint returned ${response.status}.`);
|
|
3573
|
-
plan = parseproposal(await response.json(), session.origin).plan;
|
|
4716
|
+
plan = parseproposal(await response.json(), session.origin, session.grants ?? [session.origin]).plan;
|
|
3574
4717
|
}
|
|
3575
4718
|
await memory.setplan(plan);
|
|
3576
4719
|
await memory.setprogress(resetforplan(await memory.getprogress(), plan, Date.now()));
|
|
@@ -3600,6 +4743,7 @@ function stepauditkind(step, ok) {
|
|
|
3600
4743
|
if (step.kind === "consentpassword") return "consent";
|
|
3601
4744
|
if (step.kind === "handoffcaptcha") return "handoff";
|
|
3602
4745
|
if (iscapturekind(step.kind)) return "capture";
|
|
4746
|
+
if (ismediakind(step.kind)) return "media";
|
|
3603
4747
|
if (isfileskind(step.kind)) {
|
|
3604
4748
|
if (step.kind === "interceptmime") return "intercept";
|
|
3605
4749
|
if (step.kind === "readclipboard" || step.kind === "writeclipboard" || step.kind === "copyscreen") return "clipboard";
|
|
@@ -5653,6 +6797,461 @@ async function executecapturestep(step, session, plan, tabid2, origin) {
|
|
|
5653
6797
|
}
|
|
5654
6798
|
throw new Error("Unsupported capture kind.");
|
|
5655
6799
|
}
|
|
6800
|
+
var activerecordings = /* @__PURE__ */ new Map();
|
|
6801
|
+
async function stoprecordingsforrun(runid) {
|
|
6802
|
+
for (const [id, active] of [...activerecordings.entries()]) {
|
|
6803
|
+
if (active.record.runid !== runid) continue;
|
|
6804
|
+
const finished = finishrecording(active.record, Date.now());
|
|
6805
|
+
await memory.addmedia(finished);
|
|
6806
|
+
activerecordings.delete(id);
|
|
6807
|
+
await audit("media", `Recording ${finished.id} of kind ${finished.kind} stopped cleanly at run end after ${finished.duration ?? 0} milliseconds.`, { planid: runid, stepid: finished.stepid });
|
|
6808
|
+
}
|
|
6809
|
+
}
|
|
6810
|
+
async function recordingwriter(record2, exporttarget) {
|
|
6811
|
+
const manifest = {
|
|
6812
|
+
id: record2.id,
|
|
6813
|
+
runid: record2.runid,
|
|
6814
|
+
stepid: record2.stepid,
|
|
6815
|
+
kind: record2.kind,
|
|
6816
|
+
scope: record2.scope,
|
|
6817
|
+
fps: record2.fps,
|
|
6818
|
+
bitrate: record2.bitrate,
|
|
6819
|
+
startedat: record2.startedat,
|
|
6820
|
+
endedat: record2.endedat,
|
|
6821
|
+
duration: record2.duration,
|
|
6822
|
+
derivation: "tab screencast apis stay outside the reviewed manifest, so recordscreen derives an ordered frame sequence from viewport captures and captureaudio derives audio element evidence; no encoded video or audio bytes exist",
|
|
6823
|
+
frames: (record2.frames ?? []).map((id) => ({ id }))
|
|
6824
|
+
};
|
|
6825
|
+
const file = `${(record2.file ?? record2.id).replace(/\.[a-z0-9]+$/i, "")}.json`;
|
|
6826
|
+
const bytes = JSON.stringify(manifest).length;
|
|
6827
|
+
if (exporttarget === "download") {
|
|
6828
|
+
const granted = await chrome.permissions.contains({ permissions: ["downloads"] }).catch(() => false);
|
|
6829
|
+
if (!granted) throw new Error("The recording download needs the downloads capability; request it from the review panel.");
|
|
6830
|
+
await chrome.downloads.download({ url: `data:application/json;base64,${btoa(JSON.stringify(manifest))}`, filename: file });
|
|
6831
|
+
}
|
|
6832
|
+
return { written: exporttarget === "download", file, bytes };
|
|
6833
|
+
}
|
|
6834
|
+
async function convertonecapture(source, directive, plan, step) {
|
|
6835
|
+
if (!source.bytes) throw new Error(`The capture bytes of ${source.id} expired from the retention window; conversions need live bytes.`);
|
|
6836
|
+
const blob = await (await fetch(source.bytes)).blob();
|
|
6837
|
+
const bitmap = await createImageBitmap(blob);
|
|
6838
|
+
const dataurl = await encodecanvas(bitmap.width, bitmap.height, (context) => {
|
|
6839
|
+
context.drawImage(bitmap, 0, 0);
|
|
6840
|
+
}, { format: directive.target, quality: directive.quality });
|
|
6841
|
+
const name = await capturenamefor(step, plan, "convertimage", directive.target);
|
|
6842
|
+
return { id: randomid(), runid: plan.id, stepid: step.id, kind: "convertimage", format: directive.target, width: bitmap.width, height: bitmap.height, capturedat: Date.now(), bytes: dataurl, name, target: source.id };
|
|
6843
|
+
}
|
|
6844
|
+
async function thumbonecapture(source, directive, plan, step) {
|
|
6845
|
+
if (!source.bytes) throw new Error(`The capture bytes of ${source.id} expired from the retention window; thumbnails need live bytes.`);
|
|
6846
|
+
const blob = await (await fetch(source.bytes)).blob();
|
|
6847
|
+
const bitmap = await createImageBitmap(blob);
|
|
6848
|
+
const geometry = thumbgeometry({ width: bitmap.width, height: bitmap.height }, directive);
|
|
6849
|
+
const format = source.format === "jpeg" || source.format === "webp" ? source.format : "png";
|
|
6850
|
+
const dataurl = await encodecanvas(geometry.width, geometry.height, (context) => {
|
|
6851
|
+
context.drawImage(bitmap, geometry.sx, geometry.sy, geometry.sw, geometry.sh, geometry.dx, geometry.dy, geometry.dw, geometry.dh);
|
|
6852
|
+
}, { format });
|
|
6853
|
+
const name = (await capturenamefor(step, plan, "makethumbs", format)).replace(/(\.[a-z0-9]+)?$/, `-${directive.suffix}$1`);
|
|
6854
|
+
return { id: randomid(), runid: plan.id, stepid: step.id, kind: "makethumbs", format, width: geometry.width, height: geometry.height, capturedat: Date.now(), bytes: dataurl, name, target: source.id };
|
|
6855
|
+
}
|
|
6856
|
+
async function executemediastep(step, session, plan, tabid2, origin) {
|
|
6857
|
+
const options = stepoptions2(step);
|
|
6858
|
+
const extra = { ...session ? { sessionid: session.id } : {}, planid: plan.id, stepid: step.id };
|
|
6859
|
+
const gate = mediagate(session, tabid2, origin, Date.now());
|
|
6860
|
+
if (!gate.allowed) throw new Error(gate.reason ?? "The media capture needs the active session tab grant.");
|
|
6861
|
+
if (step.kind === "capturepdf") {
|
|
6862
|
+
const pdfoptions = pdfoptionsof(options.pdf);
|
|
6863
|
+
const paginate = pdfoptions.paginate === true;
|
|
6864
|
+
const measured = await bridgecall(tabid2, "measurepage");
|
|
6865
|
+
const breakpoints = (Array.isArray(options.breakpoints) ? options.breakpoints : []).filter((item) => typeof item === "string" && item.trim().length > 0);
|
|
6866
|
+
const resolvedbreaks = breakpoints.length > 0 ? (await bridgecall(tabid2, "pdfbreaks", breakpoints)).map((entry) => entry.top) : [];
|
|
6867
|
+
const segments = paginate ? pdfsegments(measured.scrollheight, measured.viewportheight, resolvedbreaks) : [{ top: 0, height: measured.scrollheight }];
|
|
6868
|
+
const texts = [];
|
|
6869
|
+
for (const segment of segments) {
|
|
6870
|
+
const collected = await bridgecall(tabid2, "pdfsegment", segment.top, segment.height);
|
|
6871
|
+
if (!collected.ok) throw new Error(collected.summary);
|
|
6872
|
+
texts.push(collected.text || " ");
|
|
6873
|
+
}
|
|
6874
|
+
const composed = buildpdf(texts, pdfoptions);
|
|
6875
|
+
const dataurl = `data:application/pdf;base64,${btoa(composed.document)}`;
|
|
6876
|
+
const name = await capturenamefor(step, plan, "capturepdf", "pdf");
|
|
6877
|
+
const record2 = { id: randomid(), runid: plan.id, stepid: step.id, pages: composed.pages, pagewidth: composed.pagewidth, pageheight: composed.pageheight, margins: pdfoptions.margins ?? { top: 0.4, right: 0.4, bottom: 0.4, left: 0.4 }, landscape: pdfoptions.landscape === true, scale: pdfoptions.scale ?? 1, bytes: composed.bytes, at: Date.now(), dataurl, name };
|
|
6878
|
+
await memory.addmedia(record2);
|
|
6879
|
+
await memory.setprogress(recordmedia(await memory.getprogress(), plan.id, step.id, { id: record2.id, kind: "pdf", scope: "tab", bytes: record2.bytes }, Date.now()));
|
|
6880
|
+
const exporttarget = options.exporttarget === "download" ? "download" : "memory";
|
|
6881
|
+
if (exporttarget === "download") {
|
|
6882
|
+
const granted = await chrome.permissions.contains({ permissions: ["downloads"] }).catch(() => false);
|
|
6883
|
+
if (!granted) throw new Error("The pdf download export needs the downloads capability; request it from the review panel.");
|
|
6884
|
+
await chrome.downloads.download({ url: dataurl, filename: name });
|
|
6885
|
+
}
|
|
6886
|
+
await refreshbadge();
|
|
6887
|
+
await audit("media", `Composed a derived pdf report of ${record2.pages} page${record2.pages === 1 ? "" : "s"} at ${record2.pagewidth} by ${record2.pageheight} points from ${segments.length} report segment${segments.length === 1 ? "" : "s"}, routed to ${exporttarget === "download" ? "the reviewed download flow" : "session memory"}.`, extra);
|
|
6888
|
+
return { ok: true, summary: `Composed a derived pdf report of ${record2.pages} page${record2.pages === 1 ? "" : "s"}.`, details: { media: { id: record2.id, kind: "pdf", bytes: record2.bytes }, pages: record2.pages, pagewidth: record2.pagewidth, pageheight: record2.pageheight, landscape: record2.landscape, scale: record2.scale, segments: segments.length, exporttarget, derivation: "the pdf is a text layer composed from the page segments because browser print apis stay outside the reviewed manifest" } };
|
|
6889
|
+
}
|
|
6890
|
+
if (step.kind === "recordscreen" || step.kind === "captureaudio") {
|
|
6891
|
+
const recordingoptions = recordingoptionsof(options.recording);
|
|
6892
|
+
const consentref = typeof options.consentref === "string" ? options.consentref : "";
|
|
6893
|
+
const consents = await memory.getrecordingconsents();
|
|
6894
|
+
const consent = consents.find((item) => item.id === consentref && item.approved === true && item.usedat === void 0);
|
|
6895
|
+
if (!consent) {
|
|
6896
|
+
const pending = { id: consentref || randomid(), prompt: typeof options.prompt === "string" && options.prompt ? options.prompt : step.summary, origin, stepid: step.id, at: Date.now() };
|
|
6897
|
+
await memory.setrecordingconsent(pending);
|
|
6898
|
+
await refreshbadge();
|
|
6899
|
+
await audit("media", `Recording consent prompt ${pending.id} opened for step ${step.id} on ${origin}; the recording waits for the user approval and every start needs its own prompt.`, extra);
|
|
6900
|
+
return { ok: false, summary: `The ${step.kind} waits for your recording consent approval${consentref ? ` under ref ${consentref}` : ""}; approve it in the review panel and run the step again.`, details: { consent: { id: pending.id, prompt: pending.prompt, origin, stepid: step.id, approved: false } } };
|
|
6901
|
+
}
|
|
6902
|
+
await memory.setrecordingconsent({ ...consent, usedat: Date.now() });
|
|
6903
|
+
const duration = typeof options.duration === "number" ? options.duration : recordingwindow(await memory.getsettings());
|
|
6904
|
+
if (duration === void 0) throw new Error("The recording needs a reviewed duration window in options or the user configured recording window before it starts.");
|
|
6905
|
+
const kind = step.kind === "recordscreen" ? "screen" : "audio";
|
|
6906
|
+
const recordoptions = { ...recordingoptions };
|
|
6907
|
+
if (kind === "audio" && recordoptions.audio === void 0) recordoptions.audio = true;
|
|
6908
|
+
const record2 = newrecording({ id: randomid(), runid: plan.id, stepid: step.id, tabid: tabid2, kind, options: recordoptions, at: Date.now() });
|
|
6909
|
+
activerecordings.set(record2.id, { record: record2, tabid: tabid2, stopat: Date.now() + duration });
|
|
6910
|
+
const interval = frameinterval(recordingoptions.fps ?? 1);
|
|
6911
|
+
const frames = [];
|
|
6912
|
+
let missed = 0;
|
|
6913
|
+
const deadline = Date.now() + duration;
|
|
6914
|
+
try {
|
|
6915
|
+
while (Date.now() < deadline && activerecordings.has(record2.id) && Date.now() < (activerecordings.get(record2.id)?.stopat ?? deadline)) {
|
|
6916
|
+
if (kind === "screen") {
|
|
6917
|
+
try {
|
|
6918
|
+
const shot = await tabshot("png", void 0);
|
|
6919
|
+
const measured = await bridgecall(tabid2, "measurepage");
|
|
6920
|
+
const frame = { id: randomid(), runid: plan.id, stepid: step.id, kind: "recordscreen", format: "png", width: measured.viewportwidth, height: measured.viewportheight, capturedat: Date.now(), bytes: shot, name: await capturenamefor(step, plan, "recordscreen", "png") };
|
|
6921
|
+
await memory.addcapture(frame);
|
|
6922
|
+
frames.push(frame.id);
|
|
6923
|
+
} catch {
|
|
6924
|
+
missed += 1;
|
|
6925
|
+
}
|
|
6926
|
+
} else {
|
|
6927
|
+
const elementstate = await bridgecall(tabid2, "mediaelements").catch(() => []);
|
|
6928
|
+
frames.push(`${Date.now()}:${elementstate.length}`);
|
|
6929
|
+
}
|
|
6930
|
+
await new Promise((resolve) => setTimeout(resolve, interval));
|
|
6931
|
+
}
|
|
6932
|
+
const finished = finishrecording({ ...record2, frames }, Date.now());
|
|
6933
|
+
const written = await recordingwriter(finished, options.exporttarget === "download" ? "download" : "memory");
|
|
6934
|
+
const stored = { ...finished, file: written.file, bytes: written.bytes };
|
|
6935
|
+
await memory.addmedia(stored);
|
|
6936
|
+
await memory.setprogress(recordmedia(await memory.getprogress(), plan.id, step.id, { id: stored.id, kind: stored.kind === "screen" ? "recording" : "audio", scope: stored.scope, bytes: stored.bytes ?? 0 }, Date.now()));
|
|
6937
|
+
await refreshbadge();
|
|
6938
|
+
await audit("media", `Recorded ${stored.duration ?? 0} milliseconds of ${stored.kind} activity of ${stored.scope} scope with ${frames.length - missed} frame${frames.length - missed === 1 ? "" : "s"}${missed > 0 ? ` and ${missed} missed capture${missed === 1 ? "" : "s"}` : ""} at ${stored.fps ?? 1} fps, stored as derived evidence with ${stored.bytes ?? 0} manifest bytes.`, extra);
|
|
6939
|
+
return { ok: true, summary: `Recorded ${stored.duration ?? 0} milliseconds of ${stored.kind} activity with ${frames.length - missed} frames.`, details: { media: { id: stored.id, kind: stored.kind === "screen" ? "recording" : "audio", bytes: stored.bytes ?? 0 }, recording: { id: stored.id, duration: stored.duration, scope: stored.scope, fps: stored.fps }, frames: frames.length, missed, derivation: "tab screencast stays outside the reviewed manifest, so the recording derives an ordered frame sequence and an honest manifest instead of encoded media bytes" } };
|
|
6940
|
+
} finally {
|
|
6941
|
+
activerecordings.delete(record2.id);
|
|
6942
|
+
}
|
|
6943
|
+
}
|
|
6944
|
+
if (step.kind === "captureframe") {
|
|
6945
|
+
const timestamp = typeof options.timestamp === "number" ? options.timestamp : void 0;
|
|
6946
|
+
const poster = options.poster === true;
|
|
6947
|
+
const grabbed = await bridgecall(tabid2, "videoframe", step.target ?? "", timestamp, poster);
|
|
6948
|
+
if (!grabbed.ok || !grabbed.dataurl) throw new Error(grabbed.summary);
|
|
6949
|
+
const capture = captureoptionsof(options.capture);
|
|
6950
|
+
const format = capture.format ?? "png";
|
|
6951
|
+
const record2 = { id: randomid(), runid: plan.id, stepid: step.id, source: step.target ?? "", timestamp: timestamp ?? 0, poster, format, width: grabbed.width ?? 0, height: grabbed.height ?? 0, at: Date.now(), dataurl: grabbed.dataurl, name: await capturenamefor(step, plan, "captureframe", format) };
|
|
6952
|
+
await memory.addmedia(record2);
|
|
6953
|
+
await memory.setprogress(recordmedia(await memory.getprogress(), plan.id, step.id, { id: record2.id, kind: "frame", scope: step.target ?? "", bytes: (record2.dataurl ?? "").length }, Date.now()));
|
|
6954
|
+
await refreshbadge();
|
|
6955
|
+
await audit("media", `Grabbed a video frame of ${record2.width} by ${record2.height} pixels at ${record2.timestamp} seconds from ${step.target ?? "the video element"}${poster ? " as the poster frame" : ""}.`, extra);
|
|
6956
|
+
return { ok: true, summary: `Grabbed the video frame at ${record2.timestamp} seconds.`, details: { media: { id: record2.id, kind: "frame", bytes: (record2.dataurl ?? "").length }, source: record2.source, timestamp: record2.timestamp, poster, width: record2.width, height: record2.height } };
|
|
6957
|
+
}
|
|
6958
|
+
if (step.kind === "downloadimages") {
|
|
6959
|
+
const filter = imagefilterof(options.imagefilter);
|
|
6960
|
+
const observed = await bridgecall(tabid2, "pageimages", filter.selector);
|
|
6961
|
+
const descriptors = observed.map((entry) => ({ url: typeof entry.url === "string" ? entry.url : "", alt: typeof entry.alt === "string" ? entry.alt : "", width: typeof entry.width === "number" ? entry.width : 0, height: typeof entry.height === "number" ? entry.height : 0, bytes: typeof entry.bytes === "number" ? entry.bytes : 0, mime: typeof entry.mime === "string" ? entry.mime : "" }));
|
|
6962
|
+
const matched = descriptors.filter((image) => imagematches(image, filter));
|
|
6963
|
+
const unique = dedupeimages(matched);
|
|
6964
|
+
const naming = options.naming && typeof options.naming === "object" && !Array.isArray(options.naming) ? options.naming : { run: true, step: true, sequence: true, kind: true };
|
|
6965
|
+
const names = imagenames(naming, plan.id, step.id, unique.length, "png");
|
|
6966
|
+
let downloaded = 0;
|
|
6967
|
+
for (let index = 0; index < unique.length; index += 1) {
|
|
6968
|
+
const image = unique[index];
|
|
6969
|
+
if (!image) continue;
|
|
6970
|
+
const rawextension = image.url.split("?")[0]?.split(".").pop()?.toLowerCase() ?? "";
|
|
6971
|
+
const extension = /^[a-z0-9]{2,5}$/.test(rawextension) ? rawextension : "png";
|
|
6972
|
+
const name = `${(names[index] ?? `image-${index + 1}`).replace(/\.[a-z0-9]+$/i, "")}.${extension}`;
|
|
6973
|
+
const downloadid = await chrome.downloads.download({ url: image.url, filename: name }).catch(() => void 0);
|
|
6974
|
+
if (downloadid !== void 0) downloaded += 1;
|
|
6975
|
+
}
|
|
6976
|
+
const batch = { id: randomid(), runid: plan.id, stepid: step.id, images: descriptors, matched: matched.length, downloaded, at: Date.now() };
|
|
6977
|
+
await memory.addimagebatch(batch);
|
|
6978
|
+
await memory.setprogress(recordmedia(await memory.getprogress(), plan.id, step.id, { id: batch.id, kind: "images", scope: filter.selector ?? "page", bytes: descriptors.reduce((total, image) => total + image.bytes, 0) }, Date.now()));
|
|
6979
|
+
await refreshbadge();
|
|
6980
|
+
await audit("media", `Detected ${descriptors.length} image${descriptors.length === 1 ? "" : "s"} on the page, matched ${matched.length} against the reviewed filter, deduplicated to ${unique.length} urls and downloaded ${downloaded} through the reviewed download flow.`, extra);
|
|
6981
|
+
return { ok: true, summary: `Downloaded ${downloaded} of ${unique.length} deduplicated matching images.`, details: { media: { id: batch.id, kind: "images", bytes: descriptors.reduce((total, image) => total + image.bytes, 0) }, observed: descriptors.length, matched: matched.length, unique: unique.length, downloaded, filter } };
|
|
6982
|
+
}
|
|
6983
|
+
if (step.kind === "shotcanvas") {
|
|
6984
|
+
const read = await bridgecall(tabid2, "canvasdata", step.target ?? "");
|
|
6985
|
+
if (!read.ok || !read.dataurl) throw new Error(read.summary);
|
|
6986
|
+
const capture = captureoptionsof(options.capture);
|
|
6987
|
+
const format = capture.format ?? "png";
|
|
6988
|
+
const record2 = { id: randomid(), runid: plan.id, stepid: step.id, element: step.target ?? "", context: read.context, width: read.width ?? 0, height: read.height ?? 0, format, at: Date.now(), dataurl: read.dataurl, name: await capturenamefor(step, plan, "shotcanvas", format) };
|
|
6989
|
+
await memory.addmedia(record2);
|
|
6990
|
+
await memory.setprogress(recordmedia(await memory.getprogress(), plan.id, step.id, { id: record2.id, kind: "canvas", scope: record2.element, bytes: (record2.dataurl ?? "").length }, Date.now()));
|
|
6991
|
+
await refreshbadge();
|
|
6992
|
+
await audit("media", `Read the ${record2.context} canvas buffer of ${step.target ?? "the canvas"} at ${record2.width} by ${record2.height} pixels${read.preserved === true ? " through a preserved readPixels pass" : ""}.`, extra);
|
|
6993
|
+
return { ok: true, summary: `Captured the ${record2.context} canvas content.`, details: { media: { id: record2.id, kind: "canvas", bytes: (record2.dataurl ?? "").length }, context: record2.context, preserved: read.preserved === true, element: record2.element, width: record2.width, height: record2.height } };
|
|
6994
|
+
}
|
|
6995
|
+
if (step.kind === "probestream") {
|
|
6996
|
+
const selector = typeof options.selector === "string" && options.selector ? options.selector : void 0;
|
|
6997
|
+
const raw = await bridgecall(tabid2, "streamelements", selector);
|
|
6998
|
+
const summaries = streamsummaries(raw);
|
|
6999
|
+
const records = summaries.map((summary) => ({ id: randomid(), runid: plan.id, stepid: step.id, kind: summary.kind, tracks: summary.tracks, label: summary.label, live: summary.live, at: Date.now(), ...summary.detail !== void 0 ? { detail: summary.detail } : {} }));
|
|
7000
|
+
for (const record2 of records) await memory.addmedia(record2);
|
|
7001
|
+
if (records.length > 0) await memory.setprogress(recordmedia(await memory.getprogress(), plan.id, step.id, { id: records[0]?.id ?? "", kind: "stream", scope: selector ?? "page", bytes: 0 }, Date.now()));
|
|
7002
|
+
await refreshbadge();
|
|
7003
|
+
await audit("media", `Probed ${records.length} media stream${records.length === 1 ? "" : "s"} of the page with ${records.reduce((total, record2) => total + record2.tracks, 0)} tracks; peer connection statistics stay outside the isolated world bridge.`, extra);
|
|
7004
|
+
return { ok: true, summary: `Probed ${records.length} media stream${records.length === 1 ? "" : "s"} with track details.`, details: { media: { id: records[0]?.id ?? "", kind: "stream", bytes: 0 }, streams: records, note: "peer connection statistics such as round trip time and frame drops need main world access that stays outside the reviewed bridge" } };
|
|
7005
|
+
}
|
|
7006
|
+
if (step.kind === "readmedia") {
|
|
7007
|
+
const raw = await bridgecall(tabid2, "mediaelements");
|
|
7008
|
+
const media = mediaentries(raw);
|
|
7009
|
+
await audit("media", `Read ${media.length} embedded media source${media.length === 1 ? "" : "s"} with formats, durations, dimensions, codecs and track lists.`, extra);
|
|
7010
|
+
return { ok: true, summary: `Read ${media.length} embedded media source${media.length === 1 ? "" : "s"}.`, details: { media: { id: step.id, kind: "media", bytes: 0 }, sources: media } };
|
|
7011
|
+
}
|
|
7012
|
+
if (step.kind === "readassets") {
|
|
7013
|
+
const raw = await bridgecall(tabid2, "pageassets");
|
|
7014
|
+
const entries = assetentries(raw);
|
|
7015
|
+
const records = entries.map((entry) => ({ id: randomid(), runid: plan.id, stepid: step.id, kind: entry.kind, url: entry.url, bytes: entry.bytes, at: Date.now(), ...entry.sizes !== void 0 ? { sizes: entry.sizes } : {} }));
|
|
7016
|
+
for (const record2 of records) await memory.addmedia(record2);
|
|
7017
|
+
if (records.length > 0) await memory.setprogress(recordmedia(await memory.getprogress(), plan.id, step.id, { id: records[0]?.id ?? "", kind: "asset", scope: "page", bytes: 0 }, Date.now()));
|
|
7018
|
+
await refreshbadge();
|
|
7019
|
+
await audit("media", `Collected ${records.length} page asset${records.length === 1 ? "" : "s"}: ${records.filter((record2) => record2.kind === "favicon").length} favicon and ${records.filter((record2) => record2.kind === "logo").length} logo entries with declared icon sizes.`, extra);
|
|
7020
|
+
return { ok: true, summary: `Collected ${records.length} page asset${records.length === 1 ? "" : "s"}.`, details: { media: { id: records[0]?.id ?? "", kind: "asset", bytes: 0 }, assets: records } };
|
|
7021
|
+
}
|
|
7022
|
+
if (step.kind === "timelapse") {
|
|
7023
|
+
const lapse = lapseplanof(options.lapse);
|
|
7024
|
+
if (!lapse) throw new Error("A reviewed lapse plan with interval, duration and format is required in options.");
|
|
7025
|
+
const timestamps = lapseframes(lapse);
|
|
7026
|
+
const capture = captureoptionsof(options.capture);
|
|
7027
|
+
const format = lapse.format ?? capture.format ?? "png";
|
|
7028
|
+
const frameids = [];
|
|
7029
|
+
for (let index = 0; index < timestamps.length; index += 1) {
|
|
7030
|
+
if (index > 0) await new Promise((resolve) => setTimeout(resolve, lapse.interval));
|
|
7031
|
+
const shot = await tabshot(format === "png" || format === "jpeg" ? format : "png", capture.quality);
|
|
7032
|
+
const measured = await bridgecall(tabid2, "measurepage");
|
|
7033
|
+
const frame = { id: randomid(), runid: plan.id, stepid: step.id, kind: "timelapse", format, width: measured.viewportwidth, height: measured.viewportheight, capturedat: Date.now(), bytes: shot, name: await capturenamefor(step, plan, "timelapse", format), target: `frame-${index + 1}` };
|
|
7034
|
+
await memory.addcapture(frame);
|
|
7035
|
+
frameids.push(frame.id);
|
|
7036
|
+
}
|
|
7037
|
+
await memory.setprogress(recordmedia(await memory.getprogress(), plan.id, step.id, { id: frameids[0] ?? "", kind: "timelapse", scope: "page", bytes: 0 }, Date.now()));
|
|
7038
|
+
await refreshbadge();
|
|
7039
|
+
await audit("media", `Captured a time lapse of ${frameids.length} frame${frameids.length === 1 ? "" : "s"} at ${lapse.interval} millisecond intervals over ${lapse.duration} milliseconds in ${format}.`, extra);
|
|
7040
|
+
return { ok: true, summary: `Captured a time lapse of ${frameids.length} frames.`, details: { media: { id: frameids[0] ?? "", kind: "timelapse", bytes: 0 }, frames: frameids, interval: lapse.interval, duration: lapse.duration, format } };
|
|
7041
|
+
}
|
|
7042
|
+
if (step.kind === "convertimage" || step.kind === "makethumbs") {
|
|
7043
|
+
const single = typeof options.capture === "string" ? options.capture : void 0;
|
|
7044
|
+
const list = Array.isArray(options.captures) ? options.captures.filter((item) => typeof item === "string" && item.trim().length > 0) : [];
|
|
7045
|
+
const ids = single !== void 0 ? [single] : list;
|
|
7046
|
+
const converted = [];
|
|
7047
|
+
for (const id of ids) {
|
|
7048
|
+
const source = await memory.getcapture(id);
|
|
7049
|
+
if (!source) throw new Error(`No stored capture matches ${id}.`);
|
|
7050
|
+
if (step.kind === "convertimage") {
|
|
7051
|
+
const directive = convertdirectiveof(options.convert);
|
|
7052
|
+
if (!directive) throw new Error("A reviewed convert directive with a target format is required in options.");
|
|
7053
|
+
const record2 = await convertonecapture(source, directive, plan, step);
|
|
7054
|
+
await memory.addcapture(record2);
|
|
7055
|
+
converted.push({ id: record2.id, format: record2.format, width: record2.width, height: record2.height, name: record2.name ?? "" });
|
|
7056
|
+
} else {
|
|
7057
|
+
const directive = thumbdirectiveof(options.thumb);
|
|
7058
|
+
if (!directive) throw new Error("A reviewed thumb directive with size, fit and suffix is required in options.");
|
|
7059
|
+
const record2 = await thumbonecapture(source, directive, plan, step);
|
|
7060
|
+
await memory.addcapture(record2);
|
|
7061
|
+
converted.push({ id: record2.id, format: record2.format, width: record2.width, height: record2.height, name: record2.name ?? "" });
|
|
7062
|
+
}
|
|
7063
|
+
}
|
|
7064
|
+
await memory.setprogress(recordmedia(await memory.getprogress(), plan.id, step.id, { id: converted[0]?.id ?? "", kind: step.kind === "convertimage" ? "convert" : "thumb", scope: "store", bytes: 0 }, Date.now()));
|
|
7065
|
+
await refreshbadge();
|
|
7066
|
+
await audit("media", `${step.kind === "convertimage" ? "Converted" : "Thumbnailed"} ${converted.length} stored capture${converted.length === 1 ? "" : "s"} into new derived capture records.`, extra);
|
|
7067
|
+
return { ok: true, summary: `${step.kind === "convertimage" ? "Converted" : "Thumbnailed"} ${converted.length} stored capture${converted.length === 1 ? "" : "s"}.`, details: { media: { id: converted[0]?.id ?? "", kind: step.kind === "convertimage" ? "convert" : "thumb", bytes: 0 }, converted } };
|
|
7068
|
+
}
|
|
7069
|
+
throw new Error("Unsupported media kind.");
|
|
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
|
+
}
|
|
5656
7255
|
async function enforcewindowreview(step, session, plan) {
|
|
5657
7256
|
const windowid = step.value && /^\d+$/.test(step.value) ? Number.parseInt(step.value, 10) : 0;
|
|
5658
7257
|
const progress = plan ? await memory.getprogress() : void 0;
|
|
@@ -5692,8 +7291,11 @@ async function refreshbadge() {
|
|
|
5692
7291
|
const quarantined = (await memory.getquarantines()).filter((entry) => entry.scan === "pending").length;
|
|
5693
7292
|
const datasets = (await memory.getdatasets()).length;
|
|
5694
7293
|
const captures = (await memory.getcaptures()).length;
|
|
7294
|
+
const media = (await memory.getmediarecords()).length + (await memory.getimagebatches()).length;
|
|
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;
|
|
5695
7297
|
const tasktabs2 = new Set(badges.map((badge) => badge.tabid)).size;
|
|
5696
|
-
const total = (queues?.prefetch ?? 0) + (queues?.batchopen ?? 0) + tasktabs2 + prompts + consents + quarantined + datasets + captures;
|
|
7298
|
+
const total = (queues?.prefetch ?? 0) + (queues?.batchopen ?? 0) + tasktabs2 + prompts + consents + quarantined + datasets + captures + media + recordingprompts + fetchprompts;
|
|
5697
7299
|
await chrome.action.setBadgeText({ text: total > 0 ? String(total) : "" }).catch(() => {
|
|
5698
7300
|
});
|
|
5699
7301
|
}
|
|
@@ -5749,6 +7351,10 @@ async function executestep(stepid) {
|
|
|
5749
7351
|
output = await executenavigationkind(step, session, plan, tab.id, origin);
|
|
5750
7352
|
} else if (iscapturekind(step.kind)) {
|
|
5751
7353
|
output = await executecapturestep(step, session, plan, tab.id, origin);
|
|
7354
|
+
} else if (ismediakind(step.kind)) {
|
|
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);
|
|
5752
7358
|
} else {
|
|
5753
7359
|
if (step.target && freshcheckkinds.has(step.kind)) {
|
|
5754
7360
|
const fresh = await snapshot(tab.id);
|
|
@@ -5798,6 +7404,8 @@ async function executestep(stepid) {
|
|
|
5798
7404
|
await updatetaskbadges(plan, tracked);
|
|
5799
7405
|
await refreshbadge();
|
|
5800
7406
|
if (iscomplete(tracked, plan) && plan.state === "approved") {
|
|
7407
|
+
await stoprecordingsforrun(plan.id).catch(() => {
|
|
7408
|
+
});
|
|
5801
7409
|
const done = { ...plan, state: "completed", completedat: Date.now() };
|
|
5802
7410
|
await memory.setplan(done);
|
|
5803
7411
|
await audit("complete", "Every reviewed step of the approved plan has executed.", { ...session ? { sessionid: session.id } : {}, planid: done.id });
|
|
@@ -5938,6 +7546,21 @@ async function handlerequest(message, sender) {
|
|
|
5938
7546
|
return meta;
|
|
5939
7547
|
});
|
|
5940
7548
|
const capturepairs = await memory.getpairs();
|
|
7549
|
+
const mediarecords = (await memory.getmediarecords()).map((record2) => {
|
|
7550
|
+
const { dataurl, ...meta } = record2;
|
|
7551
|
+
void dataurl;
|
|
7552
|
+
return meta;
|
|
7553
|
+
});
|
|
7554
|
+
const imagebatches = await memory.getimagebatches();
|
|
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 }));
|
|
5941
7564
|
const runsettings = await memory.getsettings();
|
|
5942
7565
|
const scanhooks = [];
|
|
5943
7566
|
for (const hook of await memory.getscanhooks()) {
|
|
@@ -5949,7 +7572,7 @@ async function handlerequest(message, sender) {
|
|
|
5949
7572
|
const livetab = session ? await chrome.tabs.get(session.tabid).catch(() => void 0) : void 0;
|
|
5950
7573
|
const waitprofile = session ? waitprofiles.find((record2) => record2.origin === session.origin) : void 0;
|
|
5951
7574
|
const livestate = { phase: livetab?.status === "loading" ? "loading" : "complete", ...navrecords[0] ? { finalurl: navrecords[0].finalurl, redirects: navrecords[0].chain } : {} };
|
|
5952
|
-
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", ...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()] } : {} };
|
|
5953
7576
|
}
|
|
5954
7577
|
case "capabilities":
|
|
5955
7578
|
return refreshcapabilities();
|
|
@@ -5992,7 +7615,8 @@ async function handlerequest(message, sender) {
|
|
|
5992
7615
|
if (!outcome) throw new Error("No outcome exists for the reviewed step.");
|
|
5993
7616
|
const resolved = outcome.details?.resolvedtarget;
|
|
5994
7617
|
const capture = outcome.details?.capture;
|
|
5995
|
-
|
|
7618
|
+
const media = outcome.details?.media;
|
|
7619
|
+
return JSON.parse(outcomeresponse({ outcome, plan, ...resolved ? { resolvedtarget: resolved } : {}, ...capture ? { capture } : {}, ...media ? { media } : {} }));
|
|
5996
7620
|
}
|
|
5997
7621
|
case "map": {
|
|
5998
7622
|
const plan = await memory.getplan();
|
|
@@ -6403,8 +8027,203 @@ async function handlerequest(message, sender) {
|
|
|
6403
8027
|
await refreshbadge();
|
|
6404
8028
|
return { capturepolicy: mode };
|
|
6405
8029
|
}
|
|
8030
|
+
case "mediareport": {
|
|
8031
|
+
const records = (await memory.getmediarecords()).map((record2) => {
|
|
8032
|
+
const { dataurl, ...meta } = record2;
|
|
8033
|
+
void dataurl;
|
|
8034
|
+
return meta;
|
|
8035
|
+
});
|
|
8036
|
+
return mediareport({ records, images: await memory.getimagebatches() });
|
|
8037
|
+
}
|
|
8038
|
+
case "mediabytes": {
|
|
8039
|
+
const inputmedia = message;
|
|
8040
|
+
const record2 = await memory.getmediarecord(inputmedia.id ?? "");
|
|
8041
|
+
if (!record2) throw new Error("No stored media record matches the requested id.");
|
|
8042
|
+
const bytes = "dataurl" in record2 ? record2.dataurl : void 0;
|
|
8043
|
+
if (!bytes) throw new Error("The media bytes expired from the retention window; the metadata stays for the audit trail.");
|
|
8044
|
+
const kind = "pages" in record2 ? "pdf" : "timestamp" in record2 ? "frame" : "canvas";
|
|
8045
|
+
return { id: record2.id, kind, bytes };
|
|
8046
|
+
}
|
|
8047
|
+
case "downloadmedia": {
|
|
8048
|
+
const inputmedia = message;
|
|
8049
|
+
const session = await memory.getsession();
|
|
8050
|
+
if (!session || session.stoppedat || session.expiresat <= Date.now()) throw new Error("Media downloads stay behind the consent gate of an active session.");
|
|
8051
|
+
const record2 = await memory.getmediarecord(inputmedia.id ?? "");
|
|
8052
|
+
if (!record2) throw new Error("No stored media record matches the requested id.");
|
|
8053
|
+
const bytes = "dataurl" in record2 ? record2.dataurl : void 0;
|
|
8054
|
+
if (!bytes) throw new Error("The media bytes expired from the retention window; the metadata stays for the audit trail.");
|
|
8055
|
+
const granted = await chrome.permissions.contains({ permissions: ["downloads"] }).catch(() => false);
|
|
8056
|
+
if (!granted) throw new Error("The media download needs the downloads capability; request it from the review panel.");
|
|
8057
|
+
const name = "name" in record2 ? record2.name ?? `media-${record2.id}` : `media-${record2.id}`;
|
|
8058
|
+
await chrome.downloads.download({ url: bytes, filename: name });
|
|
8059
|
+
await audit("media", `The review panel downloaded media record ${record2.id} through the reviewed download flow.`, { sessionid: session.id });
|
|
8060
|
+
return { id: record2.id, downloaded: true };
|
|
8061
|
+
}
|
|
8062
|
+
case "approverecordingconsent": {
|
|
8063
|
+
const inputrecordingconsent = message;
|
|
8064
|
+
const record2 = (await memory.getrecordingconsents()).find((item) => item.id === inputrecordingconsent.id);
|
|
8065
|
+
if (!record2) throw new Error("No recording consent prompt matches the requested id.");
|
|
8066
|
+
await memory.setrecordingconsent({ ...record2, approved: inputrecordingconsent.approved !== false });
|
|
8067
|
+
const session = await memory.getsession();
|
|
8068
|
+
await audit("media", `Recording consent prompt ${record2.id} for step ${record2.stepid} on ${record2.origin} ${inputrecordingconsent.approved !== false ? "approved" : "declined"} by the user; every recording start consumes its own prompt.`, { ...session ? { sessionid: session.id } : {} });
|
|
8069
|
+
await refreshbadge();
|
|
8070
|
+
return { id: record2.id, approved: inputrecordingconsent.approved !== false };
|
|
8071
|
+
}
|
|
8072
|
+
case "stoprecording": {
|
|
8073
|
+
const inputstop = message;
|
|
8074
|
+
const active = activerecordings.get(inputstop.id ?? "");
|
|
8075
|
+
if (!active) throw new Error("No active recording matches the requested id.");
|
|
8076
|
+
active.stopat = Date.now();
|
|
8077
|
+
const session = await memory.getsession();
|
|
8078
|
+
await audit("media", `The review panel requested a clean stop of recording ${active.record.id} of kind ${active.record.kind}.`, { ...session ? { sessionid: session.id } : {} });
|
|
8079
|
+
return { id: active.record.id, stopping: true };
|
|
8080
|
+
}
|
|
8081
|
+
case "recordingframes": {
|
|
8082
|
+
const inputframes = message;
|
|
8083
|
+
const record2 = await memory.getrecording(inputframes.id ?? "");
|
|
8084
|
+
if (!record2) throw new Error("No stored recording matches the requested id.");
|
|
8085
|
+
return { id: record2.id, kind: record2.kind, frames: record2.frames ?? [], interval: frameinterval(record2.fps ?? 1), scope: record2.scope, duration: record2.duration };
|
|
8086
|
+
}
|
|
8087
|
+
case "downloadrecording": {
|
|
8088
|
+
const inputrecording = message;
|
|
8089
|
+
const session = await memory.getsession();
|
|
8090
|
+
if (!session || session.stoppedat || session.expiresat <= Date.now()) throw new Error("Recording downloads stay behind the consent gate of an active session.");
|
|
8091
|
+
const record2 = await memory.getrecording(inputrecording.id ?? "");
|
|
8092
|
+
if (!record2) throw new Error("No stored recording matches the requested id.");
|
|
8093
|
+
const written = await recordingwriter(record2, "download");
|
|
8094
|
+
await memory.addmedia({ ...record2, file: written.file, bytes: written.bytes });
|
|
8095
|
+
await audit("media", `The review panel downloaded the recording manifest ${record2.id} of kind ${record2.kind} through the reviewed download flow.`, { sessionid: session.id });
|
|
8096
|
+
return { id: record2.id, downloaded: true, file: written.file };
|
|
8097
|
+
}
|
|
8098
|
+
case "deleterecording": {
|
|
8099
|
+
const inputdelete = message;
|
|
8100
|
+
const session = await memory.getsession();
|
|
8101
|
+
if (!session || session.stoppedat || session.expiresat <= Date.now()) throw new Error("Recording deletion stays behind the consent gate of an active session.");
|
|
8102
|
+
const record2 = await memory.getrecording(inputdelete.id ?? "");
|
|
8103
|
+
if (!record2) throw new Error("No stored recording matches the requested id.");
|
|
8104
|
+
await memory.removemedia(record2.id);
|
|
8105
|
+
await audit("media", `The review panel deleted the recording ${record2.id} of kind ${record2.kind}; the outcome evidence stays in the audit trail.`, { sessionid: session.id });
|
|
8106
|
+
await refreshbadge();
|
|
8107
|
+
return { id: record2.id, deleted: true };
|
|
8108
|
+
}
|
|
8109
|
+
case "convertcapture": {
|
|
8110
|
+
const inputconvert = message;
|
|
8111
|
+
const session = await memory.getsession();
|
|
8112
|
+
if (!session || session.stoppedat || session.expiresat <= Date.now()) throw new Error("Capture conversions stay behind the consent gate of an active session.");
|
|
8113
|
+
const plan = await memory.getplan();
|
|
8114
|
+
if (!plan) throw new Error("No plan is available for the capture conversion.");
|
|
8115
|
+
const source = await memory.getcapture(inputconvert.id ?? "");
|
|
8116
|
+
if (!source) throw new Error("No stored capture matches the requested id.");
|
|
8117
|
+
if (inputconvert.target !== "png" && inputconvert.target !== "jpeg" && inputconvert.target !== "webp") throw new Error("The conversion target must be png, jpeg or webp.");
|
|
8118
|
+
const record2 = await convertonecapture(source, { target: inputconvert.target, ...typeof inputconvert.quality === "number" ? { quality: inputconvert.quality } : {} }, plan, { id: `panel-${source.id}`, kind: "convertimage", summary: `Panel conversion of capture ${source.id}.`, risk: "read" });
|
|
8119
|
+
await memory.addcapture(record2);
|
|
8120
|
+
await audit("media", `The review panel converted capture ${source.id} from ${source.format} to ${record2.format}.`, { sessionid: session.id });
|
|
8121
|
+
await refreshbadge();
|
|
8122
|
+
return { id: record2.id, format: record2.format, name: record2.name };
|
|
8123
|
+
}
|
|
8124
|
+
case "thumbcapture": {
|
|
8125
|
+
const inputthumb = message;
|
|
8126
|
+
const session = await memory.getsession();
|
|
8127
|
+
if (!session || session.stoppedat || session.expiresat <= Date.now()) throw new Error("Capture thumbnails stay behind the consent gate of an active session.");
|
|
8128
|
+
const plan = await memory.getplan();
|
|
8129
|
+
if (!plan) throw new Error("No plan is available for the capture thumbnail.");
|
|
8130
|
+
const source = await memory.getcapture(inputthumb.id ?? "");
|
|
8131
|
+
if (!source) throw new Error("No stored capture matches the requested id.");
|
|
8132
|
+
const directive = thumbdirectiveof({ size: inputthumb.size, fit: inputthumb.fit, suffix: inputthumb.suffix });
|
|
8133
|
+
if (!directive) throw new Error("The thumbnail needs a reviewed size, a cover or contain fit and a naming suffix.");
|
|
8134
|
+
const record2 = await thumbonecapture(source, directive, plan, { id: `panel-${source.id}`, kind: "makethumbs", summary: `Panel thumbnail of capture ${source.id}.`, risk: "read" });
|
|
8135
|
+
await memory.addcapture(record2);
|
|
8136
|
+
await audit("media", `The review panel thumbnailed capture ${source.id} to ${record2.width} by ${record2.height} pixels with the ${directive.fit} fit.`, { sessionid: session.id });
|
|
8137
|
+
await refreshbadge();
|
|
8138
|
+
return { id: record2.id, width: record2.width, height: record2.height, name: record2.name };
|
|
8139
|
+
}
|
|
8140
|
+
case "setrecordingwindow": {
|
|
8141
|
+
const inputwindow = message;
|
|
8142
|
+
const window2 = inputwindow.window;
|
|
8143
|
+
if (typeof window2 !== "number" || !Number.isFinite(window2) || window2 <= 0) throw new Error("The recording window must be a positive number of milliseconds with no code ceiling.");
|
|
8144
|
+
const settings = await memory.getsettings();
|
|
8145
|
+
await memory.setsettings({ ...settings, recordingwindow: window2 });
|
|
8146
|
+
await audit("configure", `The user set the recording duration window of the run to ${window2} milliseconds; recordings never run past it.`);
|
|
8147
|
+
return { recordingwindow: window2 };
|
|
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
|
+
}
|
|
6406
8215
|
case "stop": {
|
|
6407
8216
|
const session = await memory.getsession();
|
|
8217
|
+
for (const [id, controller] of [...activefetches.entries()]) {
|
|
8218
|
+
controller.abort();
|
|
8219
|
+
activefetches.delete(id);
|
|
8220
|
+
}
|
|
8221
|
+
for (const [id, active] of [...activerecordings.entries()]) {
|
|
8222
|
+
const finished = finishrecording(active.record, Date.now());
|
|
8223
|
+
await memory.addmedia(finished).catch(() => {
|
|
8224
|
+
});
|
|
8225
|
+
activerecordings.delete(id);
|
|
8226
|
+
}
|
|
6408
8227
|
if (session) await memory.setsession({ ...session, stoppedat: Date.now() });
|
|
6409
8228
|
const plan = await memory.getplan();
|
|
6410
8229
|
if (plan && ["pending", "approved"].includes(plan.state)) await memory.setplan({ ...plan, state: "cancelled" });
|