@wenathlan/extension 1.1.38 → 1.1.40
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/capture.d.ts +161 -0
- package/dist/capture.d.ts.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +574 -6
- package/dist/index.js.map +3 -3
- package/dist/memory.d.ts +65 -1
- package/dist/memory.d.ts.map +1 -1
- package/dist/policy.d.ts +35 -1
- package/dist/policy.d.ts.map +1 -1
- package/dist/protocol.d.ts +39 -2
- package/dist/protocol.d.ts.map +1 -1
- package/dist/types.d.ts +202 -4
- package/dist/types.d.ts.map +1 -1
- package/dist/version.d.ts +1 -1
- package/extension/dist/background.js +1594 -52
- package/extension/dist/background.js.map +4 -4
- package/extension/dist/manifest.json +1 -1
- package/extension/dist/pagebridge.js +66 -3
- package/extension/dist/pagebridge.js.map +3 -3
- package/extension/dist/popup.html +1 -1
- package/extension/dist/popup.js +39 -2
- package/extension/dist/popup.js.map +2 -2
- package/extension/dist/sidepanel.html +1 -1
- package/extension/dist/sidepanel.js +306 -2
- package/extension/dist/sidepanel.js.map +2 -2
- package/extension/dist/style.css +2 -0
- package/extension/manifest.json +1 -1
- package/package.json +1 -1
|
@@ -619,23 +619,176 @@ var sessionmemory = class {
|
|
|
619
619
|
async getexports() {
|
|
620
620
|
return await this.adapter.get("exports") ?? [];
|
|
621
621
|
}
|
|
622
|
+
/** Removes one exported data artifact by id and reports whether it existed. */
|
|
623
|
+
async removeexport(id) {
|
|
624
|
+
const records = await this.getexports();
|
|
625
|
+
const remaining = records.filter((item) => item.id !== id);
|
|
626
|
+
await this.adapter.set("exports", remaining);
|
|
627
|
+
return remaining.length !== records.length;
|
|
628
|
+
}
|
|
629
|
+
/** Removes one run store artifact by id and reports whether it existed. */
|
|
630
|
+
async removeartifact(id) {
|
|
631
|
+
const records = await this.getartifacts();
|
|
632
|
+
const remaining = records.filter((item) => item.id !== id);
|
|
633
|
+
await this.adapter.set("artifacts", remaining);
|
|
634
|
+
return remaining.length !== records.length;
|
|
635
|
+
}
|
|
636
|
+
/** Stores one batch download file record with its state, path and checksum, replacing the previous record of that id. */
|
|
637
|
+
async setdownload(record2) {
|
|
638
|
+
const records = (await this.adapter.get("downloads") ?? []).filter((item) => item.id !== record2.id);
|
|
639
|
+
await this.adapter.set("downloads", [record2, ...records]);
|
|
640
|
+
}
|
|
641
|
+
/** Returns every batch download file record with its state, path and checksum, newest first. */
|
|
642
|
+
async getdownloads() {
|
|
643
|
+
return await this.adapter.get("downloads") ?? [];
|
|
644
|
+
}
|
|
645
|
+
/** Records one captured network log record; netlog retention is a user setting and an absent value keeps every record. */
|
|
646
|
+
async addnetlog(record2) {
|
|
647
|
+
const records = await this.getnetlog();
|
|
648
|
+
const combined = [record2, ...records];
|
|
649
|
+
const retention = (await this.getsettings())?.netlogretention;
|
|
650
|
+
await this.adapter.set("netlog", retention === void 0 ? combined : combined.slice(0, retention));
|
|
651
|
+
}
|
|
652
|
+
/** Returns the captured network log of the run with its step correlation, newest first. */
|
|
653
|
+
async getnetlog() {
|
|
654
|
+
return await this.adapter.get("netlog") ?? [];
|
|
655
|
+
}
|
|
656
|
+
/** Stores one clipboard consent record with its prompt and origin, replacing the previous record of that id. */
|
|
657
|
+
async setclipconsent(record2) {
|
|
658
|
+
const records = (await this.adapter.get("clipconsents") ?? []).filter((item) => item.id !== record2.id);
|
|
659
|
+
await this.adapter.set("clipconsents", [record2, ...records]);
|
|
660
|
+
}
|
|
661
|
+
/** Returns every clipboard consent record with its prompt and origin, newest first. */
|
|
662
|
+
async getclipconsents() {
|
|
663
|
+
return await this.adapter.get("clipconsents") ?? [];
|
|
664
|
+
}
|
|
665
|
+
/** Records one clipboard entry hash with its origin provenance; the payload text itself never persists. */
|
|
666
|
+
async addclip(entry) {
|
|
667
|
+
const records = await this.getclips();
|
|
668
|
+
await this.adapter.set("clips", [entry, ...records]);
|
|
669
|
+
}
|
|
670
|
+
/** Returns every clipboard entry hash with its kind and origin provenance, newest first. */
|
|
671
|
+
async getclips() {
|
|
672
|
+
return await this.adapter.get("clips") ?? [];
|
|
673
|
+
}
|
|
674
|
+
/** Stores one quarantine entry with its scan verdict, replacing the previous entry of that id. */
|
|
675
|
+
async setquarantine(entry) {
|
|
676
|
+
const records = (await this.adapter.get("quarantines") ?? []).filter((item) => item.id !== entry.id);
|
|
677
|
+
await this.adapter.set("quarantines", [entry, ...records]);
|
|
678
|
+
}
|
|
679
|
+
/** Returns every quarantine entry with its scan verdict and release ref, newest first. */
|
|
680
|
+
async getquarantines() {
|
|
681
|
+
return await this.adapter.get("quarantines") ?? [];
|
|
682
|
+
}
|
|
683
|
+
/** Stores the reviewed cleanup rule set of the run, replacing the previous set. */
|
|
684
|
+
async setcleanuprules(rules) {
|
|
685
|
+
return this.adapter.set("cleanuprules", rules);
|
|
686
|
+
}
|
|
687
|
+
/** Returns the reviewed cleanup rule set of the run. */
|
|
688
|
+
async getcleanuprules() {
|
|
689
|
+
return await this.adapter.get("cleanuprules") ?? [];
|
|
690
|
+
}
|
|
691
|
+
/** Records one cleanup run in the run history. */
|
|
692
|
+
async addcleanuprun(run) {
|
|
693
|
+
const records = await this.getcleanupruns();
|
|
694
|
+
await this.adapter.set("cleanupruns", [run, ...records]);
|
|
695
|
+
}
|
|
696
|
+
/** Returns every cleanup run history record with removed and kept counts, newest first. */
|
|
697
|
+
async getcleanupruns() {
|
|
698
|
+
return await this.adapter.get("cleanupruns") ?? [];
|
|
699
|
+
}
|
|
700
|
+
/** Stores the capture naming counters of one task, replacing the previous counters of that task. */
|
|
701
|
+
async setcapturecounter(counter) {
|
|
702
|
+
const records = (await this.adapter.get("capturecounters") ?? []).filter((item) => item.taskid !== counter.taskid);
|
|
703
|
+
await this.adapter.set("capturecounters", [counter, ...records]);
|
|
704
|
+
}
|
|
705
|
+
/** Returns every stored capture naming counter per task, newest first. */
|
|
706
|
+
async getcapturecounters() {
|
|
707
|
+
return await this.adapter.get("capturecounters") ?? [];
|
|
708
|
+
}
|
|
709
|
+
/** Replaces the artifact inventory the cleanup sweeper plans against. */
|
|
710
|
+
async setinventory(entries) {
|
|
711
|
+
return this.adapter.set("inventory", entries);
|
|
712
|
+
}
|
|
713
|
+
/** Returns the artifact inventory with sizes and ages for the cleanup sweeper. */
|
|
714
|
+
async getinventory() {
|
|
715
|
+
return await this.adapter.get("inventory") ?? [];
|
|
716
|
+
}
|
|
717
|
+
/** Stores one user configured virus scanning hook, replacing the previous hook of that scanner name. */
|
|
718
|
+
async setscanhook(config) {
|
|
719
|
+
const records = (await this.adapter.get("scanhooks") ?? []).filter((item) => item.scanner !== config.scanner);
|
|
720
|
+
await this.adapter.set("scanhooks", [config, ...records]);
|
|
721
|
+
}
|
|
722
|
+
/** Returns every configured virus scanning hook, newest first. */
|
|
723
|
+
async getscanhooks() {
|
|
724
|
+
return await this.adapter.get("scanhooks") ?? [];
|
|
725
|
+
}
|
|
726
|
+
/** Stores the armed mime interception filters of the run, newest first. */
|
|
727
|
+
async setmimefilters(filters) {
|
|
728
|
+
return this.adapter.set("mimefilters", filters);
|
|
729
|
+
}
|
|
730
|
+
/** Returns the armed mime interception filters of the run, newest first. */
|
|
731
|
+
async getmimefilters() {
|
|
732
|
+
return await this.adapter.get("mimefilters") ?? [];
|
|
733
|
+
}
|
|
734
|
+
/** Stores one capture record with its bytes and step linkage, replacing the previous record of that id; the user configured capture retention window expires the oldest bytes while the metadata always survives for the audit trail. */
|
|
735
|
+
async addcapture(record2) {
|
|
736
|
+
const records = await this.getcaptures();
|
|
737
|
+
const retention = (await this.getsettings())?.captureretention;
|
|
738
|
+
const combined = [record2, ...records.filter((item) => item.id !== record2.id)];
|
|
739
|
+
const stored = retention === void 0 ? combined : combined.map((item, index) => index < retention ? item : expirecapturebytes(item));
|
|
740
|
+
await this.adapter.set("captures", stored);
|
|
741
|
+
}
|
|
742
|
+
/** Returns every stored capture record with its metadata, newest first. */
|
|
743
|
+
async getcaptures() {
|
|
744
|
+
return await this.adapter.get("captures") ?? [];
|
|
745
|
+
}
|
|
746
|
+
/** Returns one capture record with its bytes by its id. */
|
|
747
|
+
async getcapture(id) {
|
|
748
|
+
return (await this.getcaptures()).find((item) => item.id === id);
|
|
749
|
+
}
|
|
750
|
+
/** Returns the capture records filtered by run, step and kind. */
|
|
751
|
+
async listcaptures(filter) {
|
|
752
|
+
const records = await this.getcaptures();
|
|
753
|
+
return records.filter((item) => (filter.runid === void 0 || item.runid === filter.runid) && (filter.stepid === void 0 || item.stepid === filter.stepid) && (filter.kind === void 0 || item.kind === filter.kind));
|
|
754
|
+
}
|
|
755
|
+
/** Records one before and after shotpair of the run with its action context. */
|
|
756
|
+
async addpair(pair) {
|
|
757
|
+
const records = await this.getpairs();
|
|
758
|
+
await this.adapter.set("capturepairs", [pair, ...records.filter((item) => item.id !== pair.id)]);
|
|
759
|
+
}
|
|
760
|
+
/** Returns the shotpairs of one run resolved through their before records, newest first; an absent run returns every pair. */
|
|
761
|
+
async getpairs(runid) {
|
|
762
|
+
const records = await this.adapter.get("capturepairs") ?? [];
|
|
763
|
+
if (runid === void 0) return records;
|
|
764
|
+
const runs = /* @__PURE__ */ new Map();
|
|
765
|
+
for (const capture of await this.getcaptures()) runs.set(capture.id, capture.runid);
|
|
766
|
+
return records.filter((item) => runs.get(item.beforeid) === runid);
|
|
767
|
+
}
|
|
622
768
|
};
|
|
769
|
+
function expirecapturebytes(record2) {
|
|
770
|
+
const { bytes, ...metadata } = record2;
|
|
771
|
+
void bytes;
|
|
772
|
+
return { ...metadata, bytesexpired: true };
|
|
773
|
+
}
|
|
623
774
|
function randomid() {
|
|
624
775
|
return crypto.randomUUID();
|
|
625
776
|
}
|
|
626
777
|
|
|
627
778
|
// policy.ts
|
|
628
|
-
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"]);
|
|
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"]);
|
|
629
780
|
var interactionactions = /* @__PURE__ */ new Set(["focus", "scroll", "hover", "clickdeep", "rightclick", "doubleclick", "scrollpage", "scrollby", "scrollend", "scrolltop", "fullscreen", "zoomset", "movepointer", "clicktext", "clickaria", "clickname", "expanddetails", "pierceshadow", "retryaction"]);
|
|
630
|
-
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"]);
|
|
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"]);
|
|
631
782
|
var allowedactions = /* @__PURE__ */ new Set([...sensitiveactions, ...interactionactions, ...readactions]);
|
|
632
783
|
var watchactions = /* @__PURE__ */ new Set(["watchmutate", "watchbanner", "watchfocus", "watchtab"]);
|
|
633
|
-
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"]);
|
|
634
|
-
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"]);
|
|
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"]);
|
|
785
|
+
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"]);
|
|
635
786
|
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"]);
|
|
636
787
|
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"]);
|
|
637
788
|
var datasetactions = /* @__PURE__ */ new Set(["scrapetable", "exportcsv", "exportjson", "exportexcel", "copytable", "pushsheets", "importcsv", "looprows", "transformvalues", "deduperows", "paginateextract", "mergepages", "stamplerows", "previewgrid", "streamdisk", "resumeextract", "logprovenance"]);
|
|
638
789
|
var exportactions = /* @__PURE__ */ new Set(["exportcsv", "exportjson", "exportexcel", "copytable", "pushsheets", "streamdisk"]);
|
|
790
|
+
var filesactions = /* @__PURE__ */ new Set(["batchdownload", "pausedownload", "resumedownload", "verifydownload", "interceptmime", "exportnetlog", "readclipboard", "writeclipboard", "copyscreen", "quarantinedownload", "scanvirus", "namecaptures", "cleanupartifacts"]);
|
|
791
|
+
var captureactions = /* @__PURE__ */ new Set(["shotview", "shotfullpage", "shotelement", "shotregion", "contactsheet"]);
|
|
639
792
|
var fieldkinds = ["text", "email", "phone", "date", "number", "select", "check", "radio", "file", "password", "card", "code"];
|
|
640
793
|
var layoutmutationactions = /* @__PURE__ */ new Set(["grouptabs", "colorgroup", "collapsegroup", "savelayout", "restorelayout"]);
|
|
641
794
|
var groupcolors = ["grey", "blue", "red", "yellow", "green", "pink", "purple", "cyan", "orange"];
|
|
@@ -671,6 +824,9 @@ function requiredcapability(kind) {
|
|
|
671
824
|
if (kind === "downloadfile") return "downloads";
|
|
672
825
|
if (kind === "openclipboard") return "clipboardRead";
|
|
673
826
|
if (kind === "copytable") return "clipboardWrite";
|
|
827
|
+
if (kind === "batchdownload" || kind === "pausedownload" || kind === "resumedownload" || kind === "verifydownload" || kind === "interceptmime" || kind === "quarantinedownload" || kind === "scanvirus") return "downloads";
|
|
828
|
+
if (kind === "readclipboard") return "clipboardRead";
|
|
829
|
+
if (kind === "writeclipboard" || kind === "copyscreen") return "clipboardWrite";
|
|
674
830
|
if (kind === "openlink" || kind === "openprivate" || kind === "navlist" || kind === "batchopen" || kind === "reopentab" || kind === "deeplink") return "tabs";
|
|
675
831
|
if (tabscommandactions.has(kind)) return "tabs";
|
|
676
832
|
return void 0;
|
|
@@ -690,6 +846,95 @@ function isdatasetkind(kind) {
|
|
|
690
846
|
function isexportkind(kind) {
|
|
691
847
|
return exportactions.has(kind);
|
|
692
848
|
}
|
|
849
|
+
function isfileskind(kind) {
|
|
850
|
+
return filesactions.has(kind);
|
|
851
|
+
}
|
|
852
|
+
function iscapturekind(kind) {
|
|
853
|
+
return captureactions.has(kind);
|
|
854
|
+
}
|
|
855
|
+
function capturegate(session, tabid2, origin, now) {
|
|
856
|
+
if (!session || session.stoppedat) return { allowed: false, reason: "No active browser session exists for the capture." };
|
|
857
|
+
if (session.expiresat <= now) return { allowed: false, reason: "The browser session has expired and cannot capture." };
|
|
858
|
+
if (session.pausedat) return { allowed: false, reason: "The browser session is paused and cannot capture." };
|
|
859
|
+
if (session.tabid !== tabid2) return { allowed: false, reason: `The capture needs the active tab grant of session tab ${session.tabid} and refuses tab ${tabid2}.` };
|
|
860
|
+
if (!origingranted(session, origin)) return { allowed: false, reason: `The capture of ${origin} needs the session origin grants first.` };
|
|
861
|
+
return { allowed: true };
|
|
862
|
+
}
|
|
863
|
+
function validatecaptureoptions(value) {
|
|
864
|
+
if (value === void 0) return { allowed: true };
|
|
865
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return { allowed: false, reason: "The reviewed capture options must be an object in options.capture." };
|
|
866
|
+
const options = value;
|
|
867
|
+
if (options.format !== void 0 && options.format !== "png" && options.format !== "jpeg" && options.format !== "webp") return { allowed: false, reason: "The reviewed capture format must be png, jpeg or webp." };
|
|
868
|
+
if (options.quality !== void 0 && (typeof options.quality !== "number" || !Number.isFinite(options.quality) || options.quality < 0 || options.quality > 100)) return { allowed: false, reason: "The reviewed capture quality must stay between zero and one hundred; any value in that range is the user choice with no code cap." };
|
|
869
|
+
if (options.pixelratio !== void 0 && (typeof options.pixelratio !== "number" || !Number.isFinite(options.pixelratio) || options.pixelratio < 1)) return { allowed: false, reason: "The reviewed pixel ratio starts at one and climbs to any user configured ceiling with no code ceiling." };
|
|
870
|
+
if (options.annotate !== void 0 && typeof options.annotate !== "boolean") return { allowed: false, reason: "The reviewed capture annotation flag must be a boolean." };
|
|
871
|
+
if (options.exporttarget !== void 0 && options.exporttarget !== "memory" && options.exporttarget !== "download" && options.exporttarget !== "clipboard") return { allowed: false, reason: "The reviewed capture export target must be memory, download or clipboard." };
|
|
872
|
+
return { allowed: true };
|
|
873
|
+
}
|
|
874
|
+
function validateregionrect(value) {
|
|
875
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return { allowed: false, reason: "A reviewed regionrect with x, y, width and height in css pixels is required in options." };
|
|
876
|
+
const rect = value;
|
|
877
|
+
for (const field of ["x", "y", "width", "height"]) {
|
|
878
|
+
if (typeof rect[field] !== "number" || !Number.isFinite(rect[field])) return { allowed: false, reason: `The reviewed regionrect needs a numeric ${field} in css pixels.` };
|
|
879
|
+
}
|
|
880
|
+
if (rect.x < 0 || rect.y < 0) return { allowed: false, reason: "The reviewed regionrect refuses negative coordinates." };
|
|
881
|
+
if (rect.width <= 0 || rect.height <= 0) return { allowed: false, reason: "The reviewed regionrect needs positive width and height values." };
|
|
882
|
+
return { allowed: true };
|
|
883
|
+
}
|
|
884
|
+
function validatecapturenaming(value) {
|
|
885
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return { allowed: false, reason: "A reviewed capturenaming rule with run, step, sequence and kind flags is required." };
|
|
886
|
+
const rule = value;
|
|
887
|
+
const segments = ["run", "step", "sequence", "kind"];
|
|
888
|
+
for (const key of Object.keys(rule)) {
|
|
889
|
+
if (!segments.includes(key)) return { allowed: false, reason: `The reviewed capturenaming rule refuses the unknown ${key} segment; only run, step, sequence and kind participate.` };
|
|
890
|
+
}
|
|
891
|
+
for (const segment of segments) {
|
|
892
|
+
if (rule[segment] !== void 0 && typeof rule[segment] !== "boolean") return { allowed: false, reason: `The reviewed capturenaming ${segment} flag must be a boolean.` };
|
|
893
|
+
}
|
|
894
|
+
if (!segments.some((segment) => rule[segment] === true)) return { allowed: false, reason: "The reviewed capturenaming rule needs at least one enabled segment of run, step, sequence and kind." };
|
|
895
|
+
return { allowed: true };
|
|
896
|
+
}
|
|
897
|
+
function stitchbudgetallowed(tiles, settle2, wait) {
|
|
898
|
+
if (tiles <= 0) return { allowed: false, reason: "The stitch budget needs at least one tile." };
|
|
899
|
+
if (settle2 < 0 || wait < 0) return { allowed: false, reason: "The reviewed settle and wait windows must be zero or positive milliseconds." };
|
|
900
|
+
if (tiles * settle2 > wait) return { allowed: false, reason: `The stitching scroll budget of ${tiles} tiles at ${settle2} milliseconds exceeds the reviewed wait window of ${wait} milliseconds; review a wider window or a smaller settle.` };
|
|
901
|
+
return { allowed: true };
|
|
902
|
+
}
|
|
903
|
+
function beforeafterwrapallowed(kind) {
|
|
904
|
+
return allowedactions.has(kind) && !captureactions.has(kind);
|
|
905
|
+
}
|
|
906
|
+
function validatecapturegrammar(step, options) {
|
|
907
|
+
const kind = step.kind;
|
|
908
|
+
const optioncheck = validatecaptureoptions(options.capture);
|
|
909
|
+
if (!optioncheck.allowed) return optioncheck;
|
|
910
|
+
if (options.settle !== void 0 && (typeof options.settle !== "number" || !Number.isFinite(options.settle) || options.settle < 0)) return { allowed: false, reason: "The reviewed capture settle window must be zero or a positive number of milliseconds." };
|
|
911
|
+
if (options.overlap !== void 0 && (typeof options.overlap !== "number" || !Number.isInteger(options.overlap) || options.overlap < 0)) return { allowed: false, reason: "The reviewed stitch overlap must be zero or a positive number of rows." };
|
|
912
|
+
if (options.wait !== void 0 && (typeof options.wait !== "number" || !Number.isFinite(options.wait) || options.wait < 0)) return { allowed: false, reason: "The reviewed capture wait window must be zero or a positive number of milliseconds." };
|
|
913
|
+
if (options.naming !== void 0) {
|
|
914
|
+
const namingcheck = validatecapturenaming(options.naming);
|
|
915
|
+
if (!namingcheck.allowed) return namingcheck;
|
|
916
|
+
}
|
|
917
|
+
if (kind === "shotregion") {
|
|
918
|
+
const rectcheck = validateregionrect(options.regionrect);
|
|
919
|
+
if (!rectcheck.allowed) return rectcheck;
|
|
920
|
+
if (options.reviewed !== true) return { allowed: false, reason: "Every reviewed regionrect needs the explicit reviewed flag before shotregion runs." };
|
|
921
|
+
if (options.container !== void 0 && !isnonempty(options.container)) return { allowed: false, reason: "The reviewed scrollable container selector must be a non-empty string." };
|
|
922
|
+
if (options.steps !== void 0 && (typeof options.steps !== "number" || !Number.isInteger(options.steps) || options.steps < 1)) return { allowed: false, reason: "The reviewed container scroll steps must be a positive integer with no code ceiling." };
|
|
923
|
+
}
|
|
924
|
+
if (kind === "contactsheet") {
|
|
925
|
+
const elements = options.elements;
|
|
926
|
+
if (!Array.isArray(elements) || elements.length === 0 || !elements.every((item) => isnonempty(item))) return { allowed: false, reason: "A reviewed non-empty list of element selectors is required in options for the contact sheet; the cell count stays the user choice." };
|
|
927
|
+
const layout = options.sheet;
|
|
928
|
+
if (layout !== void 0) {
|
|
929
|
+
if (!layout || typeof layout !== "object" || Array.isArray(layout)) return { allowed: false, reason: "The reviewed sheetlayout must be an object with cellsize, columns and label." };
|
|
930
|
+
const sheet = layout;
|
|
931
|
+
if (typeof sheet.cellsize !== "number" || !Number.isFinite(sheet.cellsize) || sheet.cellsize <= 0) return { allowed: false, reason: "The reviewed contact sheet cell size must be a positive number of pixels." };
|
|
932
|
+
if (typeof sheet.columns !== "number" || !Number.isInteger(sheet.columns) || sheet.columns < 1) return { allowed: false, reason: "The reviewed contact sheet column count must be a positive integer with no code ceiling." };
|
|
933
|
+
if (sheet.label !== void 0 && sheet.label !== "none" && sheet.label !== "index" && sheet.label !== "selector" && sheet.label !== "both") return { allowed: false, reason: "The reviewed contact sheet label style must be none, index, selector or both." };
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
return { allowed: true };
|
|
937
|
+
}
|
|
693
938
|
function exportgranted(session, origin) {
|
|
694
939
|
if (!origingranted(session, origin)) return { allowed: false, reason: `The export of extracted data from ${origin} needs the session origin grants before it leaves local memory.` };
|
|
695
940
|
return { allowed: true };
|
|
@@ -874,6 +1119,88 @@ function validatedatagrammar(step, options, origin) {
|
|
|
874
1119
|
if (kind === "logprovenance" && !isnonempty(options.artifact)) return { allowed: false, reason: "A reviewed artifact id or name is required in options." };
|
|
875
1120
|
return { allowed: true };
|
|
876
1121
|
}
|
|
1122
|
+
function validatedownloadspec(value) {
|
|
1123
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return { allowed: false, reason: "A reviewed downloadspec with a url list is required in options." };
|
|
1124
|
+
const spec = value;
|
|
1125
|
+
if (!Array.isArray(spec.urls) || spec.urls.length === 0 || !spec.urls.every((url) => ishttpsurl(url))) return { allowed: false, reason: "The reviewed downloadspec needs a non-empty list of HTTPS urls." };
|
|
1126
|
+
if (spec.filename !== void 0 && !isnonempty(spec.filename)) return { allowed: false, reason: "The reviewed downloadspec filename rule must be a non-empty string." };
|
|
1127
|
+
if (spec.complete !== void 0 && spec.complete !== "size" && spec.complete !== "checksum") return { allowed: false, reason: "The reviewed downloadspec completion criterion must be size or checksum." };
|
|
1128
|
+
return { allowed: true };
|
|
1129
|
+
}
|
|
1130
|
+
function validatemimefilter(value) {
|
|
1131
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return { allowed: false, reason: "A reviewed mimefilter with include and exclude patterns is required in options." };
|
|
1132
|
+
const filter = value;
|
|
1133
|
+
if (!Array.isArray(filter.include) || filter.include.length === 0 || !filter.include.every((pattern) => isnonempty(pattern))) return { allowed: false, reason: "The reviewed mimefilter needs a non-empty list of include patterns." };
|
|
1134
|
+
if (filter.exclude !== void 0 && (!Array.isArray(filter.exclude) || !filter.exclude.every((pattern) => isnonempty(pattern)))) return { allowed: false, reason: "The reviewed mimefilter exclude patterns must be a list of non-empty strings." };
|
|
1135
|
+
if (filter.default !== "deny" && filter.default !== "allow") return { allowed: false, reason: "The reviewed mimefilter needs the deny or allow default for unlisted mime types." };
|
|
1136
|
+
return { allowed: true };
|
|
1137
|
+
}
|
|
1138
|
+
function validatecleanuprule(value) {
|
|
1139
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return { allowed: false, reason: "A reviewed cleanuprule with an age, a kind and a keep policy is required." };
|
|
1140
|
+
const rule = value;
|
|
1141
|
+
if (typeof rule.age !== "number" || !Number.isFinite(rule.age) || rule.age <= 0) return { allowed: false, reason: "The reviewed cleanup age window must be a positive number of milliseconds with no code ceiling." };
|
|
1142
|
+
if (!isnonempty(rule.kind)) return { allowed: false, reason: "The reviewed cleanup rule needs a non-empty artifact kind, or any to match every kind." };
|
|
1143
|
+
if (rule.keep !== "none" && rule.keep !== "latest" && rule.keep !== "all") return { allowed: false, reason: "The reviewed cleanup keep policy must be none, latest or all." };
|
|
1144
|
+
return { allowed: true };
|
|
1145
|
+
}
|
|
1146
|
+
function validatefilesgrammar(step, options) {
|
|
1147
|
+
const kind = step.kind;
|
|
1148
|
+
if (kind === "batchdownload") {
|
|
1149
|
+
const speccheck = validatedownloadspec(options.downloadspec);
|
|
1150
|
+
if (!speccheck.allowed) return speccheck;
|
|
1151
|
+
if (options.concurrent !== void 0 && (typeof options.concurrent !== "number" || !Number.isInteger(options.concurrent) || options.concurrent < 1)) return { allowed: false, reason: "The reviewed concurrent download window must be a positive integer with no code ceiling." };
|
|
1152
|
+
}
|
|
1153
|
+
if (kind === "pausedownload" || kind === "resumedownload" || kind === "verifydownload" || kind === "quarantinedownload" || kind === "scanvirus") {
|
|
1154
|
+
if (!isnonempty(step.value)) return { allowed: false, reason: "A reviewed download or quarantine reference is required." };
|
|
1155
|
+
if (kind === "verifydownload") {
|
|
1156
|
+
if (options.checksum !== void 0 && !isnonempty(options.checksum)) return { allowed: false, reason: "The reviewed expected checksum must be a non-empty string." };
|
|
1157
|
+
if (options.bytes !== void 0 && (typeof options.bytes !== "number" || !Number.isFinite(options.bytes) || options.bytes < 0)) return { allowed: false, reason: "The reviewed expected size must be zero or a positive number of bytes." };
|
|
1158
|
+
}
|
|
1159
|
+
if (kind === "scanvirus" && options.scanner !== void 0 && !isnonempty(options.scanner)) return { allowed: false, reason: "The reviewed scanner name must be a non-empty string." };
|
|
1160
|
+
if (kind === "quarantinedownload" && options.reason !== void 0 && !isnonempty(options.reason)) return { allowed: false, reason: "The reviewed quarantine reason must be a non-empty string." };
|
|
1161
|
+
}
|
|
1162
|
+
if (kind === "interceptmime") {
|
|
1163
|
+
const filtercheck = validatemimefilter(options.mimefilter);
|
|
1164
|
+
if (!filtercheck.allowed) return filtercheck;
|
|
1165
|
+
}
|
|
1166
|
+
if (kind === "readclipboard") {
|
|
1167
|
+
if (!isnonempty(options.consentref)) return { allowed: false, reason: "A clipboard read requires a reviewed consent ref of an approved consent prompt in options." };
|
|
1168
|
+
if (options.prompt !== void 0 && !isnonempty(options.prompt)) return { allowed: false, reason: "The reviewed clipboard consent prompt must be a non-empty string." };
|
|
1169
|
+
}
|
|
1170
|
+
if (kind === "exportnetlog" && options.stepid !== void 0 && !isnonempty(options.stepid)) return { allowed: false, reason: "The reviewed netlog step filter must be a non-empty step id." };
|
|
1171
|
+
if (kind === "namecaptures") {
|
|
1172
|
+
if (!isnonempty(options.task)) return { allowed: false, reason: "A reviewed task id is required in options for capture naming." };
|
|
1173
|
+
if (options.steps !== void 0 && (!Array.isArray(options.steps) || options.steps.length === 0 || !options.steps.every((item) => isnonempty(item)))) return { allowed: false, reason: "The reviewed capture steps must be a non-empty list of step ids when present." };
|
|
1174
|
+
if (options.extension !== void 0 && !isnonempty(options.extension)) return { allowed: false, reason: "The reviewed capture extension must be a non-empty string." };
|
|
1175
|
+
}
|
|
1176
|
+
if (kind === "cleanupartifacts" && options.rules !== void 0) {
|
|
1177
|
+
const rules = options.rules;
|
|
1178
|
+
if (!Array.isArray(rules) || rules.length === 0) return { allowed: false, reason: "The reviewed cleanup rules must be a non-empty list when present." };
|
|
1179
|
+
for (const item of rules) {
|
|
1180
|
+
const rulecheck = validatecleanuprule(item);
|
|
1181
|
+
if (!rulecheck.allowed) return rulecheck;
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
return { allowed: true };
|
|
1185
|
+
}
|
|
1186
|
+
function clipboardconsentgranted(step) {
|
|
1187
|
+
let options = {};
|
|
1188
|
+
try {
|
|
1189
|
+
options = parseoptions(step);
|
|
1190
|
+
} catch {
|
|
1191
|
+
options = {};
|
|
1192
|
+
}
|
|
1193
|
+
const consentref = options.consentref;
|
|
1194
|
+
if (typeof consentref !== "string" || !consentref.trim()) return { allowed: false, reason: "A clipboard read requires a reviewed consent ref in options." };
|
|
1195
|
+
return { allowed: true };
|
|
1196
|
+
}
|
|
1197
|
+
function quarantinereleasegranted(entry) {
|
|
1198
|
+
if (entry.scan !== "clean") return { allowed: false, reason: `The quarantined file ${entry.path} cannot leave quarantine with the ${entry.scan} scan verdict; only a clean verdict releases it.` };
|
|
1199
|
+
return { allowed: true };
|
|
1200
|
+
}
|
|
1201
|
+
function maskclipboard(payload) {
|
|
1202
|
+
return `[clipboard payload of ${payload.length} character${payload.length === 1 ? "" : "s"}]`;
|
|
1203
|
+
}
|
|
877
1204
|
function submitreviewgranted(steps, submitid) {
|
|
878
1205
|
const position = steps.findIndex((candidate) => candidate.id === submitid);
|
|
879
1206
|
const asked = steps.some((candidate, index) => candidate.kind === "asksubmit" && (position === -1 || index < position));
|
|
@@ -1400,6 +1727,14 @@ function validatestep(step, origin) {
|
|
|
1400
1727
|
const datacheck = validatedatagrammar(step, options, origin);
|
|
1401
1728
|
if (!datacheck.allowed) return datacheck;
|
|
1402
1729
|
}
|
|
1730
|
+
if (isfileskind(step.kind)) {
|
|
1731
|
+
const filescheck = validatefilesgrammar(step, options);
|
|
1732
|
+
if (!filescheck.allowed) return filescheck;
|
|
1733
|
+
}
|
|
1734
|
+
if (iscapturekind(step.kind)) {
|
|
1735
|
+
const capturecheck = validatecapturegrammar(step, options);
|
|
1736
|
+
if (!capturecheck.allowed) return capturecheck;
|
|
1737
|
+
}
|
|
1403
1738
|
if (step.kind === "tabcreate") {
|
|
1404
1739
|
if (options.background !== void 0 && typeof options.background !== "boolean") return { allowed: false, reason: "The reviewed background flag must be a boolean." };
|
|
1405
1740
|
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." };
|
|
@@ -1454,6 +1789,23 @@ function canexecute(input) {
|
|
|
1454
1789
|
const consentgate = passwordconsentgranted(input.step);
|
|
1455
1790
|
if (!consentgate.allowed) return consentgate;
|
|
1456
1791
|
}
|
|
1792
|
+
if (input.step.kind === "readclipboard") {
|
|
1793
|
+
const clipgate = clipboardconsentgranted(input.step);
|
|
1794
|
+
if (!clipgate.allowed) return clipgate;
|
|
1795
|
+
}
|
|
1796
|
+
if (input.step.kind === "interceptmime" && !origingranted(input.session, input.origin)) return { allowed: false, reason: "The download interception is outside the session origin grants." };
|
|
1797
|
+
if (iscapturekind(input.step.kind)) {
|
|
1798
|
+
const capturegatecheck = capturegate(input.session, input.tabid, input.origin, now);
|
|
1799
|
+
if (!capturegatecheck.allowed) return capturegatecheck;
|
|
1800
|
+
let captureoptions = {};
|
|
1801
|
+
try {
|
|
1802
|
+
captureoptions = parseoptions(input.step);
|
|
1803
|
+
} catch {
|
|
1804
|
+
captureoptions = {};
|
|
1805
|
+
}
|
|
1806
|
+
const target = captureoptions.capture?.exporttarget;
|
|
1807
|
+
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
|
+
}
|
|
1457
1809
|
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") {
|
|
1458
1810
|
let options = {};
|
|
1459
1811
|
try {
|
|
@@ -1555,9 +1907,29 @@ function recordwizardstep(progress, planid, stepid, state, now) {
|
|
|
1555
1907
|
const outcome = { stepid, ok: done, summary: `Wizard step ${executed} of ${state.steps} ${done ? "completed the wizard" : "executed"}.`, details: { wizard: { index: state.index, steps: state.steps, completed: [...state.completed] } }, at: now };
|
|
1556
1908
|
return recordoutcome(base, planid, outcome, now);
|
|
1557
1909
|
}
|
|
1910
|
+
function downloadshare(completed, total) {
|
|
1911
|
+
if (!Number.isFinite(total) || total <= 0) return 0;
|
|
1912
|
+
return Math.min(1, Math.max(0, completed) / total);
|
|
1913
|
+
}
|
|
1914
|
+
function recorddownload(progress, planid, stepid, entry, now) {
|
|
1915
|
+
const base = progress && progress.planid === planid ? progress : emptyprogress(planid, now);
|
|
1916
|
+
const outcome = { stepid, ok: entry.state === "complete", summary: `Download ${entry.index + 1} of ${entry.url} ended in the ${entry.state} state.`, details: { download: entry }, at: now };
|
|
1917
|
+
return recordoutcome(base, planid, outcome, now);
|
|
1918
|
+
}
|
|
1919
|
+
function recordcapture(progress, planid, stepid, capture, now) {
|
|
1920
|
+
const base = progress && progress.planid === planid ? progress : emptyprogress(planid, now);
|
|
1921
|
+
const bytes = capture.bytes?.length ?? 0;
|
|
1922
|
+
const outcome = { stepid, ok: true, summary: `Captured a ${capture.format} ${capture.kind} shot of ${capture.width} by ${capture.height} pixels with ${bytes} character${bytes === 1 ? "" : "s"} of image data.`, details: { capture: { id: capture.id, kind: capture.kind, format: capture.format, width: capture.width, height: capture.height, bytes } }, at: now };
|
|
1923
|
+
return recordoutcome(base, planid, outcome, now);
|
|
1924
|
+
}
|
|
1925
|
+
function recordpair(progress, planid, stepid, pair, now) {
|
|
1926
|
+
const base = progress && progress.planid === planid ? progress : emptyprogress(planid, now);
|
|
1927
|
+
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
|
+
return recordoutcome(base, planid, outcome, now);
|
|
1929
|
+
}
|
|
1558
1930
|
|
|
1559
1931
|
// version.ts
|
|
1560
|
-
var packageversion = "1.1.
|
|
1932
|
+
var packageversion = "1.1.40";
|
|
1561
1933
|
|
|
1562
1934
|
// types.ts
|
|
1563
1935
|
var protocolversion = packageversion;
|
|
@@ -1621,7 +1993,7 @@ function requestbody(input) {
|
|
|
1621
1993
|
return JSON.stringify({ version: protocolversion, objective: input.objective, session: input.session, observation: input.observation, capabilities: input.capabilities });
|
|
1622
1994
|
}
|
|
1623
1995
|
function outcomeresponse(input) {
|
|
1624
|
-
return JSON.stringify({ version: protocolversion, planid: input.plan.id, planstate: input.plan.state, outcome: input.outcome, ...input.resolvedtarget ? { resolvedtarget: input.resolvedtarget } : {} });
|
|
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 } : {} });
|
|
1625
1997
|
}
|
|
1626
1998
|
function mapresponse(input) {
|
|
1627
1999
|
return JSON.stringify({ version: protocolversion, planid: input.plan.id, planstate: input.plan.state, map: input.map });
|
|
@@ -1674,6 +2046,204 @@ function extractionreport(input) {
|
|
|
1674
2046
|
function provenancereport(input) {
|
|
1675
2047
|
return { version: protocolversion, records: input.records };
|
|
1676
2048
|
}
|
|
2049
|
+
function downloadreport(input) {
|
|
2050
|
+
return JSON.stringify({ version: protocolversion, planid: input.plan.id, planstate: input.plan.state, downloads: input.downloads });
|
|
2051
|
+
}
|
|
2052
|
+
function netlogreport(input) {
|
|
2053
|
+
return { version: protocolversion, records: input.records };
|
|
2054
|
+
}
|
|
2055
|
+
function quarantinereport(input) {
|
|
2056
|
+
return { version: protocolversion, entries: input.entries };
|
|
2057
|
+
}
|
|
2058
|
+
function capturereport(input) {
|
|
2059
|
+
return { version: protocolversion, records: input.records, pairs: input.pairs };
|
|
2060
|
+
}
|
|
2061
|
+
|
|
2062
|
+
// capture.ts
|
|
2063
|
+
var capturekinds = ["shotview", "shotfullpage", "shotelement", "shotregion", "contactsheet"];
|
|
2064
|
+
function captureoptionsof(value) {
|
|
2065
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return {};
|
|
2066
|
+
const options = value;
|
|
2067
|
+
const normalized = {};
|
|
2068
|
+
if (options.format === "png" || options.format === "jpeg" || options.format === "webp") normalized.format = options.format;
|
|
2069
|
+
if (typeof options.quality === "number" && Number.isFinite(options.quality)) normalized.quality = options.quality;
|
|
2070
|
+
if (typeof options.pixelratio === "number" && Number.isFinite(options.pixelratio)) normalized.pixelratio = options.pixelratio;
|
|
2071
|
+
if (typeof options.annotate === "boolean") normalized.annotate = options.annotate;
|
|
2072
|
+
if (options.exporttarget === "memory" || options.exporttarget === "download" || options.exporttarget === "clipboard") normalized.exporttarget = options.exporttarget;
|
|
2073
|
+
return normalized;
|
|
2074
|
+
}
|
|
2075
|
+
function capturevisible(input) {
|
|
2076
|
+
const ratio = input.options.pixelratio ?? 1;
|
|
2077
|
+
return {
|
|
2078
|
+
id: input.id,
|
|
2079
|
+
runid: input.runid,
|
|
2080
|
+
stepid: input.stepid,
|
|
2081
|
+
kind: "shotview",
|
|
2082
|
+
format: input.options.format ?? "png",
|
|
2083
|
+
width: Math.round(input.viewport.width * ratio),
|
|
2084
|
+
height: Math.round(input.viewport.height * ratio),
|
|
2085
|
+
capturedat: input.at,
|
|
2086
|
+
bytes: input.dataurl,
|
|
2087
|
+
...input.name !== void 0 ? { name: input.name } : {},
|
|
2088
|
+
...input.options.annotate === true ? { annotated: true } : {},
|
|
2089
|
+
...input.options.exporttarget !== void 0 ? { exporttarget: input.options.exporttarget } : {},
|
|
2090
|
+
...input.target !== void 0 ? { target: input.target } : {}
|
|
2091
|
+
};
|
|
2092
|
+
}
|
|
2093
|
+
function capturestitched(input) {
|
|
2094
|
+
const ratio = input.options.pixelratio ?? 1;
|
|
2095
|
+
return {
|
|
2096
|
+
id: input.id,
|
|
2097
|
+
runid: input.runid,
|
|
2098
|
+
stepid: input.stepid,
|
|
2099
|
+
kind: "shotfullpage",
|
|
2100
|
+
format: input.options.format ?? "png",
|
|
2101
|
+
width: Math.round(input.plan.scrollwidth * ratio),
|
|
2102
|
+
height: Math.round(input.plan.scrollheight * ratio),
|
|
2103
|
+
capturedat: input.at,
|
|
2104
|
+
bytes: input.dataurl,
|
|
2105
|
+
...input.name !== void 0 ? { name: input.name } : {},
|
|
2106
|
+
...input.options.annotate === true ? { annotated: true } : {},
|
|
2107
|
+
...input.options.exporttarget !== void 0 ? { exporttarget: input.options.exporttarget } : {}
|
|
2108
|
+
};
|
|
2109
|
+
}
|
|
2110
|
+
function captureelement(input) {
|
|
2111
|
+
const ratio = input.options.pixelratio ?? 1;
|
|
2112
|
+
const scaled = scaledrect(input.rect, ratio);
|
|
2113
|
+
return {
|
|
2114
|
+
id: input.id,
|
|
2115
|
+
runid: input.runid,
|
|
2116
|
+
stepid: input.stepid,
|
|
2117
|
+
kind: "shotelement",
|
|
2118
|
+
format: input.options.format ?? "png",
|
|
2119
|
+
width: scaled.width,
|
|
2120
|
+
height: scaled.height,
|
|
2121
|
+
capturedat: input.at,
|
|
2122
|
+
bytes: input.dataurl,
|
|
2123
|
+
...input.name !== void 0 ? { name: input.name } : {},
|
|
2124
|
+
...input.options.annotate === true ? { annotated: true } : {},
|
|
2125
|
+
...input.options.exporttarget !== void 0 ? { exporttarget: input.options.exporttarget } : {},
|
|
2126
|
+
...input.target !== void 0 ? { target: input.target } : {}
|
|
2127
|
+
};
|
|
2128
|
+
}
|
|
2129
|
+
function captureregion(input) {
|
|
2130
|
+
const ratio = input.options.pixelratio ?? 1;
|
|
2131
|
+
const scaled = scaledrect(input.rect, ratio);
|
|
2132
|
+
return {
|
|
2133
|
+
id: input.id,
|
|
2134
|
+
runid: input.runid,
|
|
2135
|
+
stepid: input.stepid,
|
|
2136
|
+
kind: "shotregion",
|
|
2137
|
+
format: input.options.format ?? "png",
|
|
2138
|
+
width: scaled.width,
|
|
2139
|
+
height: scaled.height,
|
|
2140
|
+
capturedat: input.at,
|
|
2141
|
+
bytes: input.dataurl,
|
|
2142
|
+
...input.name !== void 0 ? { name: input.name } : {},
|
|
2143
|
+
...input.options.annotate === true ? { annotated: true } : {},
|
|
2144
|
+
...input.options.exporttarget !== void 0 ? { exporttarget: input.options.exporttarget } : {},
|
|
2145
|
+
...input.target !== void 0 ? { target: input.target } : {}
|
|
2146
|
+
};
|
|
2147
|
+
}
|
|
2148
|
+
function pairstates(before, after, action, at, id) {
|
|
2149
|
+
if (!before) return { skipped: "before", reason: "The before shot was not captured, so no state pair exists." };
|
|
2150
|
+
if (!after) return { skipped: "after", reason: `The action of kind ${action.kind} failed before the after shot, so the state pair is skipped.` };
|
|
2151
|
+
return {
|
|
2152
|
+
pair: {
|
|
2153
|
+
id,
|
|
2154
|
+
beforeid: before.id,
|
|
2155
|
+
afterid: after.id,
|
|
2156
|
+
actionkind: action.kind,
|
|
2157
|
+
...action.target !== void 0 ? { target: action.target } : {},
|
|
2158
|
+
...action.domsnapshotid !== void 0 ? { domsnapshotid: action.domsnapshotid } : {},
|
|
2159
|
+
at
|
|
2160
|
+
},
|
|
2161
|
+
reason: `Paired the before shot ${before.id} with the after shot ${after.id} around the ${action.kind} action.`
|
|
2162
|
+
};
|
|
2163
|
+
}
|
|
2164
|
+
function capturestates(input) {
|
|
2165
|
+
if (input.policy !== "beforeafter") return { reason: `The ${input.policy} capture policy takes no state pair around the ${input.actionkind} action.` };
|
|
2166
|
+
return pairstates(input.before, input.after, { kind: input.actionkind, ...input.target !== void 0 ? { target: input.target } : {}, ...input.domsnapshotid !== void 0 ? { domsnapshotid: input.domsnapshotid } : {} }, input.at, input.id);
|
|
2167
|
+
}
|
|
2168
|
+
function buildstitchplan(input) {
|
|
2169
|
+
const overlap = Math.max(0, Math.round(input.overlap ?? 0));
|
|
2170
|
+
const stepy = Math.max(1, input.viewportheight - overlap);
|
|
2171
|
+
const columns = Math.max(1, Math.ceil(input.scrollwidth / input.viewportwidth));
|
|
2172
|
+
const rows = input.scrollheight <= input.viewportheight ? 1 : Math.max(1, Math.ceil((input.scrollheight - overlap) / stepy));
|
|
2173
|
+
const tiles = [];
|
|
2174
|
+
for (let column = 0; column < columns; column += 1) {
|
|
2175
|
+
for (let row = 0; row < rows; row += 1) {
|
|
2176
|
+
const x = Math.min(column * input.viewportwidth, Math.max(0, input.scrollwidth - input.viewportwidth));
|
|
2177
|
+
const y = rows === 1 ? 0 : Math.min(row * stepy, Math.max(0, input.scrollheight - input.viewportheight));
|
|
2178
|
+
tiles.push({ x: Math.round(x), y: Math.round(y) });
|
|
2179
|
+
}
|
|
2180
|
+
}
|
|
2181
|
+
return { columns, rows, tiles, overlap, scrollwidth: Math.round(input.scrollwidth), scrollheight: Math.round(input.scrollheight), viewportwidth: Math.round(input.viewportwidth), viewportheight: Math.round(input.viewportheight) };
|
|
2182
|
+
}
|
|
2183
|
+
function seamweights(overlap) {
|
|
2184
|
+
if (overlap <= 0) return [];
|
|
2185
|
+
const weights = [];
|
|
2186
|
+
for (let index = 0; index < overlap; index += 1) weights.push((index + 1) / (overlap + 1));
|
|
2187
|
+
return weights;
|
|
2188
|
+
}
|
|
2189
|
+
function fixedheadermatch(band, firstband) {
|
|
2190
|
+
if (band.length === 0 || band.length !== firstband.length) return false;
|
|
2191
|
+
return band.every((value, index) => value === firstband[index]);
|
|
2192
|
+
}
|
|
2193
|
+
function scaledrect(rect, pixelratio) {
|
|
2194
|
+
const ratio = pixelratio >= 1 ? pixelratio : 1;
|
|
2195
|
+
return { x: Math.round(rect.x * ratio), y: Math.round(rect.y * ratio), width: Math.round(rect.width * ratio), height: Math.round(rect.height * ratio) };
|
|
2196
|
+
}
|
|
2197
|
+
function croprect(rect, viewport) {
|
|
2198
|
+
const x = Math.max(0, rect.x);
|
|
2199
|
+
const y = Math.max(0, rect.y);
|
|
2200
|
+
return { x: Math.round(x), y: Math.round(y), width: Math.round(Math.max(0, Math.min(rect.width, viewport.width - x))), height: Math.round(Math.max(0, Math.min(rect.height, viewport.height - y))) };
|
|
2201
|
+
}
|
|
2202
|
+
function regionsteps(containerheight, viewportstep) {
|
|
2203
|
+
if (containerheight <= 0 || viewportstep <= 0) return [0];
|
|
2204
|
+
const steps = [];
|
|
2205
|
+
for (let top = 0; top < containerheight; top += viewportstep) {
|
|
2206
|
+
const clamped = Math.min(top, Math.max(0, containerheight - viewportstep));
|
|
2207
|
+
if (!steps.includes(clamped)) steps.push(clamped);
|
|
2208
|
+
}
|
|
2209
|
+
return steps;
|
|
2210
|
+
}
|
|
2211
|
+
function buildsheet(cells, layout) {
|
|
2212
|
+
const columns = Math.max(1, Math.round(layout.columns));
|
|
2213
|
+
const rows = Math.max(1, Math.ceil(cells.length / columns));
|
|
2214
|
+
const placed = cells.map((cell, index) => {
|
|
2215
|
+
const column = index % columns;
|
|
2216
|
+
const row = Math.floor(index / columns);
|
|
2217
|
+
const label = cell.label ?? "";
|
|
2218
|
+
const caption = layout.label === "none" ? "" : layout.label === "index" ? `${index + 1}` : layout.label === "selector" ? cell.selector : label ? `${index + 1} \xB7 ${cell.selector} \xB7 ${label}` : `${index + 1} \xB7 ${cell.selector}`;
|
|
2219
|
+
return { index, column, row, selector: cell.selector, label, caption };
|
|
2220
|
+
});
|
|
2221
|
+
return { columns, rows, cells: placed };
|
|
2222
|
+
}
|
|
2223
|
+
function capturepart(value) {
|
|
2224
|
+
return value.replace(/[^a-z0-9-]+/gi, "-").replace(/^-+|-+$/g, "").toLowerCase() || "capture";
|
|
2225
|
+
}
|
|
2226
|
+
function buildname(rule, parts, extension) {
|
|
2227
|
+
const segments = [];
|
|
2228
|
+
if (rule.run) segments.push(capturepart(parts.run));
|
|
2229
|
+
if (rule.step) segments.push(capturepart(parts.step));
|
|
2230
|
+
if (rule.sequence) segments.push(String(Math.max(0, Math.round(parts.sequence))));
|
|
2231
|
+
if (rule.kind) segments.push(capturepart(parts.kind));
|
|
2232
|
+
const safeextension = extension.replace(/^\.+/, "").toLowerCase() || "png";
|
|
2233
|
+
return `${(segments.length > 0 ? segments : ["capture"]).join("-")}.${safeextension}`;
|
|
2234
|
+
}
|
|
2235
|
+
function annotationplanof(input) {
|
|
2236
|
+
const inset = Math.min(24, Math.max(8, Math.round(Math.min(input.width, input.height) / 12)));
|
|
2237
|
+
const plan = {
|
|
2238
|
+
marker: { x: inset, y: inset, number: Math.max(1, Math.round(input.step)) },
|
|
2239
|
+
footer: `${new Date(input.at).toISOString()} \xB7 ${input.url}`
|
|
2240
|
+
};
|
|
2241
|
+
if (input.rect !== void 0) {
|
|
2242
|
+
const expansion = 2;
|
|
2243
|
+
plan.outline = { x: Math.round(input.rect.x - expansion), y: Math.round(input.rect.y - expansion), width: Math.round(input.rect.width + expansion * 2), height: Math.round(input.rect.height + expansion * 2) };
|
|
2244
|
+
}
|
|
2245
|
+
return plan;
|
|
2246
|
+
}
|
|
1677
2247
|
|
|
1678
2248
|
// extension/browsertabs.ts
|
|
1679
2249
|
var browserkinds = /* @__PURE__ */ new Set(["tablist", "tabcreate", "tabactivate", "tabclose", "tabreload", "tabsnapshot", "windowlist", "windowcreate", "windowclose", "zoomset", "windowresize", "downloadfile"]);
|
|
@@ -2002,13 +2572,13 @@ function presshold(holds, hold) {
|
|
|
2002
2572
|
return { holds: [...holds, hold], ok: true };
|
|
2003
2573
|
}
|
|
2004
2574
|
function releasehold(holds, holdid, releasedat) {
|
|
2005
|
-
let
|
|
2575
|
+
let released2;
|
|
2006
2576
|
const next = holds.map((hold) => {
|
|
2007
2577
|
if (hold.holdid !== holdid || hold.releasedat !== void 0) return hold;
|
|
2008
|
-
|
|
2009
|
-
return
|
|
2578
|
+
released2 = { ...hold, releasedat };
|
|
2579
|
+
return released2;
|
|
2010
2580
|
});
|
|
2011
|
-
return { holds: next, ...
|
|
2581
|
+
return { holds: next, ...released2 ? { released: released2 } : {} };
|
|
2012
2582
|
}
|
|
2013
2583
|
function heldkeys(holds, tabid2) {
|
|
2014
2584
|
return holds.filter((hold) => hold.releasedat === void 0 && (tabid2 === void 0 || hold.tabid === void 0 || hold.tabid === tabid2));
|
|
@@ -2691,6 +3261,153 @@ function mergetaskrules(existing, taskid, transforms, dedupekeys, at) {
|
|
|
2691
3261
|
};
|
|
2692
3262
|
}
|
|
2693
3263
|
|
|
3264
|
+
// extension/filescommand.ts
|
|
3265
|
+
var downloadtransitions = {
|
|
3266
|
+
queued: ["running", "complete", "failed"],
|
|
3267
|
+
running: ["paused", "complete", "failed"],
|
|
3268
|
+
paused: ["running", "failed"],
|
|
3269
|
+
complete: [],
|
|
3270
|
+
failed: []
|
|
3271
|
+
};
|
|
3272
|
+
function transitionallowed(from, to) {
|
|
3273
|
+
return downloadtransitions[from].includes(to);
|
|
3274
|
+
}
|
|
3275
|
+
function advancedownload(record2, state, at, evidence) {
|
|
3276
|
+
if (!transitionallowed(record2.state, state)) return record2;
|
|
3277
|
+
return {
|
|
3278
|
+
...record2,
|
|
3279
|
+
state,
|
|
3280
|
+
...evidence?.path !== void 0 ? { path: evidence.path } : record2.path !== void 0 ? { path: record2.path } : {},
|
|
3281
|
+
...evidence?.bytes !== void 0 ? { bytes: evidence.bytes } : record2.bytes !== void 0 ? { bytes: record2.bytes } : {},
|
|
3282
|
+
...evidence?.checksum !== void 0 ? { checksum: evidence.checksum } : record2.checksum !== void 0 ? { checksum: record2.checksum } : {},
|
|
3283
|
+
...evidence?.downloadid !== void 0 ? { downloadid: evidence.downloadid } : record2.downloadid !== void 0 ? { downloadid: record2.downloadid } : {},
|
|
3284
|
+
updatedat: at
|
|
3285
|
+
};
|
|
3286
|
+
}
|
|
3287
|
+
function concurrentwindow(running, ceiling) {
|
|
3288
|
+
return ceiling === void 0 || running < ceiling;
|
|
3289
|
+
}
|
|
3290
|
+
function conflictfree(filename, taken) {
|
|
3291
|
+
if (!taken.includes(filename)) return filename;
|
|
3292
|
+
const dot = filename.lastIndexOf(".");
|
|
3293
|
+
const base = dot > 0 ? filename.slice(0, dot) : filename;
|
|
3294
|
+
const extension = dot > 0 ? filename.slice(dot) : "";
|
|
3295
|
+
let sequence = 2;
|
|
3296
|
+
while (taken.includes(`${base}-${sequence}${extension}`)) sequence += 1;
|
|
3297
|
+
return `${base}-${sequence}${extension}`;
|
|
3298
|
+
}
|
|
3299
|
+
function downloadfilename(url, rule) {
|
|
3300
|
+
if (rule && rule.trim()) return rule.trim();
|
|
3301
|
+
let name = "";
|
|
3302
|
+
try {
|
|
3303
|
+
const parsed = new URL(url);
|
|
3304
|
+
name = decodeURIComponent(parsed.pathname.split("/").filter(Boolean).pop() ?? parsed.hostname);
|
|
3305
|
+
} catch {
|
|
3306
|
+
name = url;
|
|
3307
|
+
}
|
|
3308
|
+
return name || "download";
|
|
3309
|
+
}
|
|
3310
|
+
function verifybytes(record2, expected) {
|
|
3311
|
+
const statematch = record2.state === "complete";
|
|
3312
|
+
const sizematch = expected.bytes === void 0 ? true : record2.bytes === expected.bytes;
|
|
3313
|
+
const checksummatch = expected.checksum === void 0 ? true : record2.checksum === expected.checksum;
|
|
3314
|
+
const ok = statematch && sizematch && checksummatch;
|
|
3315
|
+
const parts = [`state ${record2.state}${statematch ? " matches" : " does not match the completed expectation"}`];
|
|
3316
|
+
if (expected.bytes !== void 0) parts.push(`size ${record2.bytes ?? "unknown"} of ${expected.bytes} bytes ${sizematch ? "matches" : "differs"}`);
|
|
3317
|
+
if (expected.checksum !== void 0) parts.push(`checksum ${record2.checksum ?? "unknown"} ${checksummatch ? "matches" : "differs from"} the reviewed ${expected.checksum}`);
|
|
3318
|
+
return { ok, summary: `${ok ? "Verified" : "Failed to verify"} the download of ${record2.filename}: ${parts.join("; ")}.`, matches: { state: statematch, size: sizematch, checksum: checksummatch } };
|
|
3319
|
+
}
|
|
3320
|
+
function mimepatternmatches(pattern, mime) {
|
|
3321
|
+
if (!pattern.endsWith("*")) return pattern === mime;
|
|
3322
|
+
return mime.startsWith(pattern.slice(0, -1));
|
|
3323
|
+
}
|
|
3324
|
+
function mimeallowed(filter, mime) {
|
|
3325
|
+
if (filter.exclude.some((pattern) => mimepatternmatches(pattern, mime))) return false;
|
|
3326
|
+
if (filter.include.some((pattern) => mimepatternmatches(pattern, mime))) return true;
|
|
3327
|
+
return filter.default === "allow";
|
|
3328
|
+
}
|
|
3329
|
+
function redactheaders(headers) {
|
|
3330
|
+
return Object.fromEntries(Object.entries(headers).map(([name]) => [name, "[redacted]"]));
|
|
3331
|
+
}
|
|
3332
|
+
function netlogentry(input) {
|
|
3333
|
+
return { url: input.url, method: input.method, status: input.status, timing: input.timing, requestid: input.requestid, stepid: input.stepid, at: input.at };
|
|
3334
|
+
}
|
|
3335
|
+
function netlogforstep(records, stepid) {
|
|
3336
|
+
return records.filter((record2) => record2.stepid === stepid);
|
|
3337
|
+
}
|
|
3338
|
+
function clipentryof(kind, payload, origin, stepid, at) {
|
|
3339
|
+
return { kind, hash: payload.hash, length: payload.length, origin, stepid, at };
|
|
3340
|
+
}
|
|
3341
|
+
function cliphash(payload) {
|
|
3342
|
+
return checksum(payload);
|
|
3343
|
+
}
|
|
3344
|
+
function quarantinedpath(filename) {
|
|
3345
|
+
return `devthink-quarantine/${filename.replace(/^\/+/, "")}`;
|
|
3346
|
+
}
|
|
3347
|
+
function newquarantine(id, filename, reason, at) {
|
|
3348
|
+
const path = quarantinedpath(filename);
|
|
3349
|
+
return { id, path, reason, scan: "pending", at, updatedat: at };
|
|
3350
|
+
}
|
|
3351
|
+
function scanresult(entry, verdict, at) {
|
|
3352
|
+
return { ...entry, scan: verdict, updatedat: at };
|
|
3353
|
+
}
|
|
3354
|
+
function scanverdictof(response) {
|
|
3355
|
+
if (!response || typeof response !== "object") return "pending";
|
|
3356
|
+
const verdict = response.verdict;
|
|
3357
|
+
if (verdict === "clean" || verdict === "flagged" || verdict === "error") return verdict;
|
|
3358
|
+
return "pending";
|
|
3359
|
+
}
|
|
3360
|
+
function released(entry, ref, at) {
|
|
3361
|
+
return { ...entry, release: ref, updatedat: at };
|
|
3362
|
+
}
|
|
3363
|
+
function capturepart2(value) {
|
|
3364
|
+
return value.replace(/[^a-z0-9-]+/gi, "-").replace(/^-+|-+$/g, "").toLowerCase() || "capture";
|
|
3365
|
+
}
|
|
3366
|
+
function capturefilename(name, extension) {
|
|
3367
|
+
const safeextension = extension.replace(/^\.+/, "").toLowerCase() || "png";
|
|
3368
|
+
return `${capturepart2(name.task)}-${capturepart2(name.step)}-${name.sequence}.${safeextension}`;
|
|
3369
|
+
}
|
|
3370
|
+
function advancecounter(counters, base) {
|
|
3371
|
+
const sequence = (counters[base] ?? 0) + 1;
|
|
3372
|
+
return { sequence, counters: { ...counters, [base]: sequence } };
|
|
3373
|
+
}
|
|
3374
|
+
function capturenames(counters, task, steps, extension) {
|
|
3375
|
+
let current = { ...counters };
|
|
3376
|
+
const names = steps.map((step) => {
|
|
3377
|
+
const advanced = advancecounter(current, step);
|
|
3378
|
+
current = advanced.counters;
|
|
3379
|
+
return capturefilename({ task, step, sequence: advanced.sequence }, extension);
|
|
3380
|
+
});
|
|
3381
|
+
return { names, counters: current };
|
|
3382
|
+
}
|
|
3383
|
+
function referencedartifacts(plan, completed) {
|
|
3384
|
+
if (!plan) return [];
|
|
3385
|
+
return plan.steps.filter((step) => step.kind === "attachfile" && !completed.includes(step.id)).map((step) => step.value ?? "").filter((value) => value.trim().length > 0);
|
|
3386
|
+
}
|
|
3387
|
+
function sweepplan(entries, rules, now, keeprefs) {
|
|
3388
|
+
const remove = /* @__PURE__ */ new Set();
|
|
3389
|
+
for (const rule of rules) {
|
|
3390
|
+
const matching = entries.filter((entry) => rule.kind === "any" || entry.kind === rule.kind);
|
|
3391
|
+
const aged = matching.filter((entry) => now - entry.at >= rule.age);
|
|
3392
|
+
const kept = [];
|
|
3393
|
+
if (rule.keep === "all") kept.push(...aged);
|
|
3394
|
+
else if (rule.keep === "latest") {
|
|
3395
|
+
const newest = [...aged].sort((left, right) => right.at - left.at)[0];
|
|
3396
|
+
if (newest) kept.push(newest);
|
|
3397
|
+
}
|
|
3398
|
+
for (const entry of aged) {
|
|
3399
|
+
if (kept.some((item) => item.id === entry.id)) continue;
|
|
3400
|
+
if (keeprefs.includes(entry.id) || keeprefs.includes(entry.name)) continue;
|
|
3401
|
+
remove.add(entry.id);
|
|
3402
|
+
}
|
|
3403
|
+
}
|
|
3404
|
+
return { remove: [...remove], keep: entries.filter((entry) => !remove.has(entry.id)).map((entry) => entry.id) };
|
|
3405
|
+
}
|
|
3406
|
+
function capturesteps(options, plan) {
|
|
3407
|
+
const listed = Array.isArray(options.steps) ? options.steps.filter((item) => typeof item === "string" && item.trim().length > 0) : [];
|
|
3408
|
+
return listed.length > 0 ? listed : plan.steps.map((step) => step.id);
|
|
3409
|
+
}
|
|
3410
|
+
|
|
2694
3411
|
// extension/background.ts
|
|
2695
3412
|
var sessionduration = 15 * 60 * 1e3;
|
|
2696
3413
|
var freshcheckkinds = /* @__PURE__ */ new Set(["focus", "inspect", "click", "type", "scroll", "select", "hover"]);
|
|
@@ -2727,8 +3444,9 @@ function stepoptions2(step) {
|
|
|
2727
3444
|
}
|
|
2728
3445
|
async function refreshcapabilities() {
|
|
2729
3446
|
const report = await readcapabilities();
|
|
2730
|
-
|
|
2731
|
-
|
|
3447
|
+
const withcaptures = { ...report, captures: [...capturekinds] };
|
|
3448
|
+
await memory.setcapabilities(withcaptures);
|
|
3449
|
+
return withcaptures;
|
|
2732
3450
|
}
|
|
2733
3451
|
async function activecontext() {
|
|
2734
3452
|
const [tab] = await chrome.tabs.query({ active: true, lastFocusedWindow: true });
|
|
@@ -2881,6 +3599,15 @@ function stepauditkind(step, ok) {
|
|
|
2881
3599
|
if (step.kind === "submitform" || step.kind === "asksubmit") return "submit";
|
|
2882
3600
|
if (step.kind === "consentpassword") return "consent";
|
|
2883
3601
|
if (step.kind === "handoffcaptcha") return "handoff";
|
|
3602
|
+
if (iscapturekind(step.kind)) return "capture";
|
|
3603
|
+
if (isfileskind(step.kind)) {
|
|
3604
|
+
if (step.kind === "interceptmime") return "intercept";
|
|
3605
|
+
if (step.kind === "readclipboard" || step.kind === "writeclipboard" || step.kind === "copyscreen") return "clipboard";
|
|
3606
|
+
if (step.kind === "quarantinedownload" || step.kind === "scanvirus") return "quarantine";
|
|
3607
|
+
if (step.kind === "cleanupartifacts") return "cleanup";
|
|
3608
|
+
if (step.kind === "verifydownload" || step.kind === "exportnetlog" || step.kind === "namecaptures") return "observation";
|
|
3609
|
+
return "download";
|
|
3610
|
+
}
|
|
2884
3611
|
if (isdatasetkind(step.kind)) {
|
|
2885
3612
|
if (step.kind === "exportcsv" || step.kind === "exportjson" || step.kind === "exportexcel" || step.kind === "copytable" || step.kind === "pushsheets") return "export";
|
|
2886
3613
|
if (step.kind === "streamdisk") return "stream";
|
|
@@ -3190,8 +3917,18 @@ async function recordnavigation(step, session, tabid2) {
|
|
|
3190
3917
|
await memory.addnavrecord(record2);
|
|
3191
3918
|
await memory.setnavstate(tabid2, record2);
|
|
3192
3919
|
if (session && url) await memory.addtrailentry(session.id, { url, title, stepid: step.id, at: Date.now() });
|
|
3920
|
+
await collectnetlog(tabid2, step);
|
|
3193
3921
|
return record2;
|
|
3194
3922
|
}
|
|
3923
|
+
async function collectnetlog(tabid2, step) {
|
|
3924
|
+
const events = navbuffers.get(tabid2) ?? [];
|
|
3925
|
+
if (events.length === 0) return;
|
|
3926
|
+
const first = events[0]?.timestamp ?? Date.now();
|
|
3927
|
+
for (const [index, event] of events.entries()) {
|
|
3928
|
+
const status = event.status ?? (event.event === "completed" ? 200 : event.event === "error" ? 0 : 0);
|
|
3929
|
+
await memory.addnetlog(netlogentry({ url: event.url, method: "GET", status, timing: Math.max(0, event.timestamp - first), requestid: `${step.id}-${index + 1}`, stepid: step.id, at: event.timestamp }));
|
|
3930
|
+
}
|
|
3931
|
+
}
|
|
3195
3932
|
function injectallowedorigins(step, session) {
|
|
3196
3933
|
const allowedorigins = session?.grants ?? (session ? [session.origin] : []);
|
|
3197
3934
|
let options = {};
|
|
@@ -4284,6 +5021,638 @@ async function executedatastep(step, session, plan, tabid2, origin) {
|
|
|
4284
5021
|
return { ok: false, summary: "Unsupported forms and data step." };
|
|
4285
5022
|
}
|
|
4286
5023
|
}
|
|
5024
|
+
async function loadownload(reference) {
|
|
5025
|
+
const records = await memory.getdownloads();
|
|
5026
|
+
return records.find((item) => item.id === reference || item.filename === reference || item.url === reference);
|
|
5027
|
+
}
|
|
5028
|
+
async function settlesdownload(record2) {
|
|
5029
|
+
const started = Date.now();
|
|
5030
|
+
for (; ; ) {
|
|
5031
|
+
const items = record2.downloadid === void 0 ? [] : await chrome.downloads.search({ id: record2.downloadid }).catch(() => []);
|
|
5032
|
+
const item = items[0];
|
|
5033
|
+
if (item?.state === "complete") return { state: "complete", evidence: { path: item.filename, bytes: item.fileSize ?? item.totalBytes ?? 0 } };
|
|
5034
|
+
if (item?.state === "interrupted") return { state: "failed" };
|
|
5035
|
+
if (Date.now() - started >= evidencesettle) return { state: "running" };
|
|
5036
|
+
await new Promise((resolve) => setTimeout(resolve, evidencepoll));
|
|
5037
|
+
}
|
|
5038
|
+
}
|
|
5039
|
+
async function pauseonerecord(record2, extra) {
|
|
5040
|
+
if (!transitionallowed(record2.state, "paused")) throw new Error(`A ${record2.state} download cannot pause.`);
|
|
5041
|
+
if (record2.downloadid !== void 0) await chrome.downloads.pause(record2.downloadid).catch(() => void 0);
|
|
5042
|
+
const paused = advancedownload(record2, "paused", Date.now());
|
|
5043
|
+
await memory.setdownload(paused);
|
|
5044
|
+
await audit("download", `Paused the download of ${record2.filename} from ${record2.url}.`, extra);
|
|
5045
|
+
return { ok: true, summary: `Paused the download of ${record2.filename}.`, details: { download: paused } };
|
|
5046
|
+
}
|
|
5047
|
+
async function resumeonerecord(record2, extra) {
|
|
5048
|
+
if (!transitionallowed(record2.state, "running")) throw new Error(`A ${record2.state} download cannot resume.`);
|
|
5049
|
+
if (record2.downloadid !== void 0) await chrome.downloads.resume(record2.downloadid).catch(() => void 0);
|
|
5050
|
+
const resumed = advancedownload(record2, "running", Date.now());
|
|
5051
|
+
await memory.setdownload(resumed);
|
|
5052
|
+
await audit("download", `Resumed the paused download of ${record2.filename} from ${record2.url}.`, extra);
|
|
5053
|
+
return { ok: true, summary: `Resumed the download of ${record2.filename}.`, details: { download: resumed } };
|
|
5054
|
+
}
|
|
5055
|
+
async function verifyonerecord(record2, expected, extra) {
|
|
5056
|
+
const verification = verifybytes(record2, expected);
|
|
5057
|
+
await audit("observation", `Verified the download of ${record2.filename}: ${verification.summary}`, extra);
|
|
5058
|
+
return { ok: verification.ok, summary: verification.summary, details: { verification: { ...verification.matches, state: record2.state, path: record2.path, checksum: record2.checksum, bytes: record2.bytes } } };
|
|
5059
|
+
}
|
|
5060
|
+
async function executefilesstep(step, session, plan, tabid2, origin) {
|
|
5061
|
+
const options = stepoptions2(step);
|
|
5062
|
+
const extra = { ...session ? { sessionid: session.id } : {}, planid: plan.id, stepid: step.id };
|
|
5063
|
+
switch (step.kind) {
|
|
5064
|
+
case "batchdownload": {
|
|
5065
|
+
const spec = options.downloadspec;
|
|
5066
|
+
const urls = Array.isArray(spec?.urls) ? (spec?.urls).filter((item) => typeof item === "string" && item.trim().length > 0) : [];
|
|
5067
|
+
const concurrent = typeof options.concurrent === "number" && Number.isInteger(options.concurrent) && options.concurrent > 0 ? options.concurrent : void 0;
|
|
5068
|
+
const taken = (await memory.getdownloads()).map((record2) => record2.filename);
|
|
5069
|
+
const records = urls.map((url) => {
|
|
5070
|
+
const filename = conflictfree(downloadfilename(url, spec?.filename), taken);
|
|
5071
|
+
taken.push(filename);
|
|
5072
|
+
return { id: randomid(), url, filename, state: "queued", at: Date.now(), updatedat: Date.now() };
|
|
5073
|
+
});
|
|
5074
|
+
let completed = 0;
|
|
5075
|
+
let failed = 0;
|
|
5076
|
+
let queued = 0;
|
|
5077
|
+
let index = 0;
|
|
5078
|
+
while (index < records.length) {
|
|
5079
|
+
const wave = [];
|
|
5080
|
+
while (index < records.length && concurrentwindow(wave.length, concurrent)) {
|
|
5081
|
+
const record2 = records[index];
|
|
5082
|
+
index += 1;
|
|
5083
|
+
const downloadid = await chrome.downloads.download({ url: record2.url, filename: record2.filename }).catch(() => void 0);
|
|
5084
|
+
const started = downloadid === void 0 ? advancedownload(record2, "failed", Date.now()) : advancedownload(record2, "running", Date.now(), { downloadid });
|
|
5085
|
+
await memory.setdownload(started);
|
|
5086
|
+
wave.push(started);
|
|
5087
|
+
}
|
|
5088
|
+
for (const started of wave) {
|
|
5089
|
+
if (started.state === "failed") {
|
|
5090
|
+
failed += 1;
|
|
5091
|
+
continue;
|
|
5092
|
+
}
|
|
5093
|
+
const settled = await settlesdownload(started);
|
|
5094
|
+
const final = advancedownload(started, settled.state, Date.now(), settled.evidence);
|
|
5095
|
+
await memory.setdownload(final);
|
|
5096
|
+
if (final.state === "complete") completed += 1;
|
|
5097
|
+
else if (final.state === "failed") failed += 1;
|
|
5098
|
+
else queued += 1;
|
|
5099
|
+
await memory.setprogress(recorddownload(await memory.getprogress(), plan.id, step.id, { index: records.indexOf(started), url: started.url, state: final.state }, Date.now()));
|
|
5100
|
+
}
|
|
5101
|
+
}
|
|
5102
|
+
await refreshbadge();
|
|
5103
|
+
await audit("download", `Batch downloaded ${records.length} reviewed file${records.length === 1 ? "" : "s"}${concurrent !== void 0 ? ` under the user configured concurrent window of ${concurrent}` : ""}: ${completed} completed, ${failed} failed${queued > 0 ? `, ${queued} still running` : ""}.`, extra);
|
|
5104
|
+
return { ok: failed === 0, summary: `Batch downloaded ${records.length} file${records.length === 1 ? "" : "s"}: ${completed} completed, ${failed} failed${queued > 0 ? `, ${queued} still running` : ""}.`, details: { downloads: records, completed, failed, running: queued, total: records.length, share: downloadshare(completed, records.length), concurrent } };
|
|
5105
|
+
}
|
|
5106
|
+
case "pausedownload": {
|
|
5107
|
+
const record2 = await loadownload(step.value ?? "");
|
|
5108
|
+
if (!record2) throw new Error(`No stored download matches ${step.value ?? ""}.`);
|
|
5109
|
+
return pauseonerecord(record2, extra);
|
|
5110
|
+
}
|
|
5111
|
+
case "resumedownload": {
|
|
5112
|
+
const record2 = await loadownload(step.value ?? "");
|
|
5113
|
+
if (!record2) throw new Error(`No stored download matches ${step.value ?? ""}.`);
|
|
5114
|
+
return resumeonerecord(record2, extra);
|
|
5115
|
+
}
|
|
5116
|
+
case "verifydownload": {
|
|
5117
|
+
const record2 = await loadownload(step.value ?? "");
|
|
5118
|
+
if (!record2) throw new Error(`No stored download matches ${step.value ?? ""}.`);
|
|
5119
|
+
return verifyonerecord(record2, { ...typeof options.bytes === "number" ? { bytes: options.bytes } : {}, ...typeof options.checksum === "string" ? { checksum: options.checksum } : {} }, extra);
|
|
5120
|
+
}
|
|
5121
|
+
case "interceptmime": {
|
|
5122
|
+
const filter = options.mimefilter;
|
|
5123
|
+
if (!filter) throw new Error("A reviewed mimefilter is required in options.");
|
|
5124
|
+
const filters = await memory.getmimefilters();
|
|
5125
|
+
await memory.setmimefilters([filter, ...filters]);
|
|
5126
|
+
armedmimefilter = filter;
|
|
5127
|
+
installmimelistener();
|
|
5128
|
+
await audit("intercept", `Armed the reviewed mime interception filter: include ${filter.include.join(", ")}, exclude ${filter.exclude.join(", ") || "none"} and the ${filter.default} default for unlisted mime types; matching downloads reroute into quarantine.`, extra);
|
|
5129
|
+
return { ok: true, summary: `Armed the mime interception filter with the ${filter.default} default for unlisted mime types.`, details: { mimefilter: filter } };
|
|
5130
|
+
}
|
|
5131
|
+
case "exportnetlog": {
|
|
5132
|
+
const records = await memory.getnetlog();
|
|
5133
|
+
const stepfilter = typeof options.stepid === "string" && options.stepid ? options.stepid : void 0;
|
|
5134
|
+
const filtered = (stepfilter ? netlogforstep(records, stepfilter) : records).map((record2) => ({ ...record2, headers: redactheaders(record2.headers ?? {}) }));
|
|
5135
|
+
await audit("observation", `Exported ${filtered.length} netlog record${filtered.length === 1 ? "" : "s"} of the run${stepfilter ? ` correlated with step ${stepfilter}` : ""} with every header value redacted.`, extra);
|
|
5136
|
+
return { ok: true, summary: `Exported ${filtered.length} netlog record${filtered.length === 1 ? "" : "s"} with header values redacted.`, details: { netlog: filtered, count: filtered.length, redacted: true, redaction: "every header value is redacted from exported netlogs" } };
|
|
5137
|
+
}
|
|
5138
|
+
case "readclipboard": {
|
|
5139
|
+
const consentref = typeof options.consentref === "string" ? options.consentref : "";
|
|
5140
|
+
const consents = await memory.getclipconsents();
|
|
5141
|
+
const consent = consents.find((item) => item.id === consentref && item.approved === true && item.usedat === void 0);
|
|
5142
|
+
if (!consent) {
|
|
5143
|
+
const pending = { id: consentref || randomid(), prompt: typeof options.prompt === "string" && options.prompt ? options.prompt : step.summary, origin, stepid: step.id, at: Date.now() };
|
|
5144
|
+
await memory.setclipconsent(pending);
|
|
5145
|
+
await refreshbadge();
|
|
5146
|
+
await audit("clipboard", `Clipboard read consent prompt ${pending.id} opened for step ${step.id} on ${origin}; the read waits for the user approval and every read needs its own prompt.`, extra);
|
|
5147
|
+
return { ok: false, summary: `The clipboard read waits for your 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 } } };
|
|
5148
|
+
}
|
|
5149
|
+
const text2 = await navigator.clipboard.readText();
|
|
5150
|
+
const entry = clipentryof("read", { hash: cliphash(text2), length: text2.length }, origin, step.id, Date.now());
|
|
5151
|
+
await memory.addclip(entry);
|
|
5152
|
+
await memory.setclipconsent({ ...consent, usedat: Date.now() });
|
|
5153
|
+
await refreshbadge();
|
|
5154
|
+
await audit("clipboard", `Read ${entry.length} clipboard character${entry.length === 1 ? "" : "s"} on the approved consent ${consent.id} with payload hash ${entry.hash}; the payload text never appears in logs or memory.`, extra);
|
|
5155
|
+
return { ok: true, summary: `Read ${entry.length} clipboard character${entry.length === 1 ? "" : "s"} on the approved consent ${consent.id} with payload hash ${entry.hash}.`, details: { clip: entry, masked: maskclipboard(text2) } };
|
|
5156
|
+
}
|
|
5157
|
+
case "writeclipboard": {
|
|
5158
|
+
const output = await dispatchpagestep(step, tabid2, origin, plan);
|
|
5159
|
+
const hash = typeof output?.details?.hash === "string" ? output.details.hash : cliphash(step.value ?? "");
|
|
5160
|
+
const length = typeof output?.details?.length === "number" ? output.details.length : (step.value ?? "").length;
|
|
5161
|
+
const entry = clipentryof("write", { hash, length }, origin, step.id, Date.now());
|
|
5162
|
+
await memory.addclip(entry);
|
|
5163
|
+
await audit("clipboard", `Wrote ${length} reviewed character${length === 1 ? "" : "s"} to the clipboard with payload hash ${hash}; the payload text never appears in logs or memory.`, extra);
|
|
5164
|
+
return { ok: Boolean(output?.ok), summary: output?.summary ?? "The clipboard write returned no result.", details: { ...output?.details ?? {}, clip: entry } };
|
|
5165
|
+
}
|
|
5166
|
+
case "copyscreen": {
|
|
5167
|
+
const windowid = chrome.windows.WINDOW_ID_CURRENT;
|
|
5168
|
+
const shot = await chrome.tabs.captureVisibleTab(windowid, { format: "png" });
|
|
5169
|
+
let destination = "clipboard";
|
|
5170
|
+
try {
|
|
5171
|
+
const blob = await (await fetch(shot)).blob();
|
|
5172
|
+
await navigator.clipboard.write([new ClipboardItem({ "image/png": blob })]);
|
|
5173
|
+
} catch {
|
|
5174
|
+
destination = "clipboard unavailable";
|
|
5175
|
+
}
|
|
5176
|
+
const entry = clipentryof("screen", { hash: cliphash(shot), length: shot.length }, origin, step.id, Date.now());
|
|
5177
|
+
await memory.addclip(entry);
|
|
5178
|
+
await audit("clipboard", `Copied a screenshot of the visible tab (${entry.length} characters of png data) to the clipboard with payload hash ${entry.hash}; destination ${destination}.`, extra);
|
|
5179
|
+
return { ok: destination === "clipboard", summary: `Copied the visible tab screenshot with payload hash ${entry.hash} to the clipboard.`, details: { clip: entry, destination } };
|
|
5180
|
+
}
|
|
5181
|
+
case "quarantinedownload": {
|
|
5182
|
+
const record2 = await loadownload(step.value ?? "");
|
|
5183
|
+
if (!record2) throw new Error(`No stored download matches ${step.value ?? ""}.`);
|
|
5184
|
+
const reason = typeof options.reason === "string" && options.reason ? options.reason : `moved from ${record2.url} by the reviewed quarantine step`;
|
|
5185
|
+
const entry = newquarantine(randomid(), record2.path ?? record2.filename, reason, Date.now());
|
|
5186
|
+
await memory.setquarantine(entry);
|
|
5187
|
+
await refreshbadge();
|
|
5188
|
+
await audit("quarantine", `Quarantined the download ${record2.filename} outside the downloads folder at ${entry.path} with reason ${reason}; the file stays there until a clean scan verdict releases it.`, extra);
|
|
5189
|
+
return { ok: true, summary: `Quarantined ${record2.filename} at ${entry.path} with a pending scan verdict.`, details: { quarantine: entry } };
|
|
5190
|
+
}
|
|
5191
|
+
case "scanvirus": {
|
|
5192
|
+
const reference = step.value ?? "";
|
|
5193
|
+
const entry = (await memory.getquarantines()).find((item) => item.id === reference || item.path === reference);
|
|
5194
|
+
if (!entry) throw new Error(`No quarantined file matches ${reference}.`);
|
|
5195
|
+
const scanner = typeof options.scanner === "string" && options.scanner ? options.scanner : void 0;
|
|
5196
|
+
const hooks = await memory.getscanhooks();
|
|
5197
|
+
const hook = scanner ? hooks.find((item) => item.scanner === scanner) : hooks[0];
|
|
5198
|
+
let verdict = "pending";
|
|
5199
|
+
if (hook) {
|
|
5200
|
+
const granted = await chrome.permissions.contains({ origins: [hostpattern(hook.origin)] }).catch(() => false);
|
|
5201
|
+
if (granted) {
|
|
5202
|
+
try {
|
|
5203
|
+
const response = await fetch(hook.endpoint, { method: "POST", headers: { "content-type": "application/json" }, credentials: "omit", body: JSON.stringify({ quarantine: entry.id, path: entry.path, reason: entry.reason }) });
|
|
5204
|
+
verdict = scanverdictof(await response.json().catch(() => void 0));
|
|
5205
|
+
} catch {
|
|
5206
|
+
verdict = "pending";
|
|
5207
|
+
}
|
|
5208
|
+
}
|
|
5209
|
+
}
|
|
5210
|
+
const scanned = scanresult(entry, verdict, Date.now());
|
|
5211
|
+
await memory.setquarantine(scanned);
|
|
5212
|
+
await refreshbadge();
|
|
5213
|
+
await audit("quarantine", `Scan hook ${hook ? hook.scanner : scanner ?? "none configured"} ${hook ? `returned the ${verdict} verdict` : "is not configured or granted; the verdict stays pending"} for the quarantined file ${entry.path}; hook failures never release a file.`, extra);
|
|
5214
|
+
return { ok: true, summary: `Scan verdict ${verdict} recorded for ${entry.path}.`, details: { quarantine: scanned, verdict } };
|
|
5215
|
+
}
|
|
5216
|
+
case "namecaptures": {
|
|
5217
|
+
const task = typeof options.task === "string" && options.task ? options.task : plan.id;
|
|
5218
|
+
const steps = capturesteps(options, plan);
|
|
5219
|
+
const extension = typeof options.extension === "string" && options.extension ? options.extension : "png";
|
|
5220
|
+
const existing = (await memory.getcapturecounters()).find((item) => item.taskid === task);
|
|
5221
|
+
const stamped = capturenames(existing?.counters ?? {}, task, steps, extension);
|
|
5222
|
+
await memory.setcapturecounter({ taskid: task, counters: stamped.counters, at: Date.now() });
|
|
5223
|
+
await audit("observation", `Stamped ${stamped.names.length} consistent capture name${stamped.names.length === 1 ? "" : "s"} for task ${task} from task, step and sequence parts.`, extra);
|
|
5224
|
+
return { ok: true, summary: `Stamped ${stamped.names.length} capture name${stamped.names.length === 1 ? "" : "s"} for task ${task}.`, details: { task, names: stamped.names, counters: stamped.counters } };
|
|
5225
|
+
}
|
|
5226
|
+
case "cleanupartifacts": {
|
|
5227
|
+
const inlinrules = (Array.isArray(options.rules) ? options.rules : []).filter((item) => Boolean(item) && typeof item === "object" && !Array.isArray(item));
|
|
5228
|
+
const rules = inlinrules.length > 0 ? inlinrules : await memory.getcleanuprules();
|
|
5229
|
+
if (rules.length === 0) throw new Error("No reviewed cleanup rules are present; pass rules in options or store a rule set from the review panel.");
|
|
5230
|
+
if (inlinrules.length > 0) await memory.setcleanuprules(inlinrules);
|
|
5231
|
+
const exportrecords = await memory.getexports();
|
|
5232
|
+
const artifacts = await memory.getartifacts();
|
|
5233
|
+
const inventory = [
|
|
5234
|
+
...exportrecords.map((artifact) => ({ id: artifact.id, kind: `export-${artifact.kind}`, name: artifact.name, size: artifact.content.length, at: artifact.at })),
|
|
5235
|
+
...artifacts.map((artifact) => ({ id: artifact.id, kind: artifact.kind, name: artifact.name, size: 0, at: artifact.at }))
|
|
5236
|
+
];
|
|
5237
|
+
await memory.setinventory(inventory);
|
|
5238
|
+
const progress = await memory.getprogress();
|
|
5239
|
+
const keeprefs = referencedartifacts(plan, progress?.planid === plan.id ? progress.completedsteps : []);
|
|
5240
|
+
const sweep = sweepplan(inventory, rules, Date.now(), keeprefs);
|
|
5241
|
+
let removed = 0;
|
|
5242
|
+
for (const id of sweep.remove) {
|
|
5243
|
+
if (await memory.removeexport(id)) removed += 1;
|
|
5244
|
+
else if (await memory.removeartifact(id)) removed += 1;
|
|
5245
|
+
}
|
|
5246
|
+
const run = { id: randomid(), rules: rules.length, removed, kept: sweep.keep.length, at: Date.now() };
|
|
5247
|
+
await memory.addcleanuprun(run);
|
|
5248
|
+
await refreshbadge();
|
|
5249
|
+
await audit("cleanup", `Cleanup sweep applied ${rules.length} reviewed rule${rules.length === 1 ? "" : "s"} by age and kind: removed ${removed} artifact${removed === 1 ? "" : "s"}, kept ${sweep.keep.length}${keeprefs.length > 0 ? ` while holding every artifact referenced by open review cards` : ""}.`, extra);
|
|
5250
|
+
return { ok: true, summary: `Cleanup sweep removed ${removed} artifact${removed === 1 ? "" : "s"} and kept ${sweep.keep.length} under the reviewed rules.`, details: { run: { id: run.id, rules: run.rules, removed: run.removed, kept: run.kept }, remove: sweep.remove, inventory: inventory.length } };
|
|
5251
|
+
}
|
|
5252
|
+
default:
|
|
5253
|
+
return { ok: false, summary: "Unsupported files, clipboard and downloads step." };
|
|
5254
|
+
}
|
|
5255
|
+
}
|
|
5256
|
+
var armedmimefilter;
|
|
5257
|
+
var mimelistenerinstalled = false;
|
|
5258
|
+
function installmimelistener() {
|
|
5259
|
+
if (mimelistenerinstalled || typeof chrome.downloads?.onDeterminingFilename?.addListener !== "function") return;
|
|
5260
|
+
mimelistenerinstalled = true;
|
|
5261
|
+
chrome.downloads.onDeterminingFilename.addListener((item, suggest) => {
|
|
5262
|
+
const filter = armedmimefilter;
|
|
5263
|
+
if (!filter || item.byExtensionId === chrome.runtime.id) {
|
|
5264
|
+
suggest({ filename: item.filename, conflictAction: "uniquify" });
|
|
5265
|
+
return;
|
|
5266
|
+
}
|
|
5267
|
+
const mime = item.mime ?? "";
|
|
5268
|
+
if (mimeallowed(filter, mime)) {
|
|
5269
|
+
suggest({ filename: `devthink-quarantine/${item.filename}`, conflictAction: "uniquify" });
|
|
5270
|
+
void memory.setquarantine(newquarantine(randomid(), item.filename, `mime ${mime || "unknown"} matched the reviewed include patterns`, Date.now())).then(() => audit("intercept", `Intercepted the download of ${item.filename} (${mime || "unknown mime"}) into quarantine under the reviewed mime filter.`, {})).catch(() => void 0);
|
|
5271
|
+
return;
|
|
5272
|
+
}
|
|
5273
|
+
if (filter.default === "deny") {
|
|
5274
|
+
void chrome.downloads.cancel(item.id).catch(() => void 0);
|
|
5275
|
+
void audit("intercept", `Denied the unlisted download of ${item.filename} (${mime || "unknown mime"}) under the deny default of the reviewed mime filter.`, {}).catch(() => void 0);
|
|
5276
|
+
}
|
|
5277
|
+
suggest({ filename: item.filename, conflictAction: "uniquify" });
|
|
5278
|
+
});
|
|
5279
|
+
}
|
|
5280
|
+
installmimelistener();
|
|
5281
|
+
async function reconcilmimefilter() {
|
|
5282
|
+
const filters = await memory.getmimefilters();
|
|
5283
|
+
armedmimefilter = filters[0];
|
|
5284
|
+
installmimelistener();
|
|
5285
|
+
}
|
|
5286
|
+
reconcilmimefilter().catch(() => {
|
|
5287
|
+
});
|
|
5288
|
+
var stitchprogress = /* @__PURE__ */ new Map();
|
|
5289
|
+
function stepcaptureoptions(step) {
|
|
5290
|
+
return captureoptionsof(stepoptions2(step).capture);
|
|
5291
|
+
}
|
|
5292
|
+
async function blobtodataurl(blob) {
|
|
5293
|
+
const buffer = new Uint8Array(await blob.arrayBuffer());
|
|
5294
|
+
let binary = "";
|
|
5295
|
+
const chunk = 32768;
|
|
5296
|
+
for (let index = 0; index < buffer.length; index += chunk) binary += String.fromCharCode(...buffer.subarray(index, index + chunk));
|
|
5297
|
+
return `data:${blob.type};base64,${btoa(binary)}`;
|
|
5298
|
+
}
|
|
5299
|
+
async function tabshot(format, quality) {
|
|
5300
|
+
const shot = { format: format === "jpeg" ? "jpeg" : "png" };
|
|
5301
|
+
if (format === "jpeg" && quality !== void 0) shot.quality = Math.max(0, Math.min(100, Math.round(quality)));
|
|
5302
|
+
return chrome.tabs.captureVisibleTab(chrome.windows.WINDOW_ID_CURRENT, shot);
|
|
5303
|
+
}
|
|
5304
|
+
async function bridgecall(tabid2, name, ...args) {
|
|
5305
|
+
await chrome.scripting.executeScript({ target: { tabId: tabid2 }, files: ["pagebridge.js"] });
|
|
5306
|
+
const result = await chrome.scripting.executeScript({ target: { tabId: tabid2 }, func: (bridgeName, bridgeArgs) => {
|
|
5307
|
+
const bridge = globalThis.devthinkbridge;
|
|
5308
|
+
if (!bridge) throw new Error("Devthink page bridge is unavailable.");
|
|
5309
|
+
const seam = bridge[bridgeName];
|
|
5310
|
+
if (typeof seam !== "function") throw new Error(`The page bridge has no ${bridgeName} capture seam.`);
|
|
5311
|
+
return seam(...bridgeArgs);
|
|
5312
|
+
}, args: [name, args] });
|
|
5313
|
+
return result[0]?.result;
|
|
5314
|
+
}
|
|
5315
|
+
function tileband(bitmap, rows) {
|
|
5316
|
+
if (rows <= 0 || bitmap.height <= 0) return [];
|
|
5317
|
+
const canvas = new OffscreenCanvas(bitmap.width, Math.min(rows, bitmap.height));
|
|
5318
|
+
const context = canvas.getContext("2d");
|
|
5319
|
+
if (!context) return [];
|
|
5320
|
+
context.drawImage(bitmap, 0, 0);
|
|
5321
|
+
const data = context.getImageData(0, 0, canvas.width, canvas.height).data;
|
|
5322
|
+
const band = [];
|
|
5323
|
+
const pixels = canvas.width * canvas.height;
|
|
5324
|
+
const stride = Math.max(1, Math.floor(pixels / 64));
|
|
5325
|
+
for (let pixel = 0; pixel < pixels; pixel += stride) {
|
|
5326
|
+
const index = pixel * 4;
|
|
5327
|
+
band.push((data[index] ?? 0) + (data[index + 1] ?? 0) * 256 + (data[index + 2] ?? 0) * 65536 + (data[index + 3] ?? 0) * 16777216);
|
|
5328
|
+
}
|
|
5329
|
+
return band;
|
|
5330
|
+
}
|
|
5331
|
+
function drawannotations(context, plan, width, height, ratio) {
|
|
5332
|
+
context.save();
|
|
5333
|
+
context.scale(ratio, ratio);
|
|
5334
|
+
context.fillStyle = "rgba(47,128,237,.92)";
|
|
5335
|
+
context.beginPath();
|
|
5336
|
+
context.arc(plan.marker.x, plan.marker.y, 14, 0, Math.PI * 2);
|
|
5337
|
+
context.fill();
|
|
5338
|
+
context.fillStyle = "#ffffff";
|
|
5339
|
+
context.font = "bold 14px sans-serif";
|
|
5340
|
+
context.textBaseline = "middle";
|
|
5341
|
+
context.textAlign = "center";
|
|
5342
|
+
context.fillText(String(plan.marker.number), plan.marker.x, plan.marker.y);
|
|
5343
|
+
if (plan.outline !== void 0) {
|
|
5344
|
+
context.strokeStyle = "#2f80ed";
|
|
5345
|
+
context.lineWidth = 2;
|
|
5346
|
+
context.strokeRect(plan.outline.x, plan.outline.y, plan.outline.width, plan.outline.height);
|
|
5347
|
+
}
|
|
5348
|
+
const footery = Math.max(12, height - 22);
|
|
5349
|
+
context.fillStyle = "rgba(23,32,44,.86)";
|
|
5350
|
+
context.fillRect(0, footery, width, 22);
|
|
5351
|
+
context.fillStyle = "#ffffff";
|
|
5352
|
+
context.font = "12px sans-serif";
|
|
5353
|
+
context.textAlign = "left";
|
|
5354
|
+
context.fillText(plan.footer.slice(0, 160), 8, footery + 11);
|
|
5355
|
+
context.restore();
|
|
5356
|
+
}
|
|
5357
|
+
async function canvasdataurl(canvas, format, quality) {
|
|
5358
|
+
const type = format === "jpeg" ? "image/jpeg" : format === "webp" ? "image/webp" : "image/png";
|
|
5359
|
+
const encoded = format === "png" ? await canvas.convertToBlob({ type }) : await canvas.convertToBlob({ type, quality: Math.max(0, Math.min(1, (quality ?? 100) / 100)) });
|
|
5360
|
+
return blobtodataurl(encoded);
|
|
5361
|
+
}
|
|
5362
|
+
async function composestitched(tiles, width, height, overlap, ratio, options) {
|
|
5363
|
+
const canvas = new OffscreenCanvas(Math.max(1, Math.round(width * ratio)), Math.max(1, Math.round(height * ratio)));
|
|
5364
|
+
const context = canvas.getContext("2d");
|
|
5365
|
+
if (!context) throw new Error("The stitcher could not create a canvas context.");
|
|
5366
|
+
const weights = seamweights(overlap);
|
|
5367
|
+
const firstband = overlap > 0 && tiles[0] ? tileband(tiles[0].bitmap, overlap) : [];
|
|
5368
|
+
for (const tile of tiles) {
|
|
5369
|
+
const band = overlap > 0 ? tileband(tile.bitmap, overlap) : [];
|
|
5370
|
+
const repeated = overlap > 0 && !tile.firstofcolumn && fixedheadermatch(band, firstband);
|
|
5371
|
+
if (repeated) {
|
|
5372
|
+
const skipped = Math.min(overlap, tile.bitmap.height);
|
|
5373
|
+
context.drawImage(tile.bitmap, 0, skipped, tile.bitmap.width, tile.bitmap.height - skipped, tile.x, tile.y + skipped, tile.bitmap.width, tile.bitmap.height - skipped);
|
|
5374
|
+
continue;
|
|
5375
|
+
}
|
|
5376
|
+
if (weights.length > 0 && !tile.firstofcolumn) {
|
|
5377
|
+
for (let row = 0; row < weights.length && row < tile.bitmap.height; row += 1) {
|
|
5378
|
+
context.globalAlpha = weights[row] ?? 1;
|
|
5379
|
+
context.drawImage(tile.bitmap, 0, row, tile.bitmap.width, 1, tile.x, tile.y + row, tile.bitmap.width, 1);
|
|
5380
|
+
}
|
|
5381
|
+
context.globalAlpha = 1;
|
|
5382
|
+
const below = Math.min(overlap, tile.bitmap.height);
|
|
5383
|
+
context.drawImage(tile.bitmap, 0, below, tile.bitmap.width, tile.bitmap.height - below, tile.x, tile.y + below, tile.bitmap.width, tile.bitmap.height - below);
|
|
5384
|
+
continue;
|
|
5385
|
+
}
|
|
5386
|
+
context.drawImage(tile.bitmap, tile.x, tile.y);
|
|
5387
|
+
}
|
|
5388
|
+
if (options.annotate) drawannotations(context, options.annotate, width, height, ratio);
|
|
5389
|
+
return canvasdataurl(canvas, options.format, options.quality);
|
|
5390
|
+
}
|
|
5391
|
+
async function decodeTile(dataurl, x, y, firstofcolumn) {
|
|
5392
|
+
const blob = await (await fetch(dataurl)).blob();
|
|
5393
|
+
return { bitmap: await createImageBitmap(blob), x, y, firstofcolumn };
|
|
5394
|
+
}
|
|
5395
|
+
async function croptoRect(dataurl, rect, ratio, options) {
|
|
5396
|
+
const blob = await (await fetch(dataurl)).blob();
|
|
5397
|
+
const bitmap = await createImageBitmap(blob);
|
|
5398
|
+
const canvas = new OffscreenCanvas(Math.max(1, Math.round(rect.width * ratio)), Math.max(1, Math.round(rect.height * ratio)));
|
|
5399
|
+
const context = canvas.getContext("2d");
|
|
5400
|
+
if (!context) throw new Error("The crop could not create a canvas context.");
|
|
5401
|
+
context.drawImage(bitmap, rect.x, rect.y, rect.width, rect.height, 0, 0, canvas.width, canvas.height);
|
|
5402
|
+
if (options.annotate) drawannotations(context, options.annotate, rect.width, rect.height, ratio);
|
|
5403
|
+
return canvasdataurl(canvas, options.format, options.quality);
|
|
5404
|
+
}
|
|
5405
|
+
async function encodecanvas(width, height, draw, options) {
|
|
5406
|
+
const canvas = new OffscreenCanvas(Math.max(1, Math.round(width)), Math.max(1, Math.round(height)));
|
|
5407
|
+
const context = canvas.getContext("2d");
|
|
5408
|
+
if (!context) throw new Error("The capture canvas could not create a context.");
|
|
5409
|
+
await draw(context);
|
|
5410
|
+
if (options.annotate) drawannotations(context, options.annotate, canvas.width / (options.annotateratio ?? 1), canvas.height / (options.annotateratio ?? 1), options.annotateratio ?? 1);
|
|
5411
|
+
return canvasdataurl(canvas, options.format, options.quality);
|
|
5412
|
+
}
|
|
5413
|
+
async function capturenamefor(step, plan, kind, format) {
|
|
5414
|
+
const naming = stepoptions2(step).naming;
|
|
5415
|
+
const rule = naming && typeof naming === "object" && !Array.isArray(naming) ? naming : { run: true, step: true, sequence: true, kind: true };
|
|
5416
|
+
const counters = (await memory.getcapturecounters()).find((item) => item.taskid === plan.id);
|
|
5417
|
+
const advanced = advancecounter(counters?.counters ?? {}, step.id);
|
|
5418
|
+
await memory.setcapturecounter({ taskid: plan.id, counters: advanced.counters, at: Date.now() });
|
|
5419
|
+
return buildname(rule, { run: plan.id, step: step.id, sequence: advanced.sequence, kind }, format);
|
|
5420
|
+
}
|
|
5421
|
+
async function routecapture(record2, session, plan, step, origin) {
|
|
5422
|
+
const target = record2.exporttarget ?? "memory";
|
|
5423
|
+
if (target === "clipboard") {
|
|
5424
|
+
const granted = await chrome.permissions.contains({ permissions: ["clipboardWrite"] }).catch(() => false);
|
|
5425
|
+
if (!granted) throw new Error("The clipboard capture export needs the clipboardwrite capability; request it from the review panel.");
|
|
5426
|
+
const blob = await (await fetch(record2.bytes ?? "")).blob();
|
|
5427
|
+
await navigator.clipboard.write([new ClipboardItem({ [blob.type]: blob })]);
|
|
5428
|
+
await memory.addclip(clipentryof("screen", { hash: cliphash(record2.bytes ?? ""), length: (record2.bytes ?? "").length }, origin, step.id, Date.now()));
|
|
5429
|
+
void session;
|
|
5430
|
+
void plan;
|
|
5431
|
+
return { target, destination: "clipboard" };
|
|
5432
|
+
}
|
|
5433
|
+
if (target === "download") {
|
|
5434
|
+
const granted = await chrome.permissions.contains({ permissions: ["downloads"] }).catch(() => false);
|
|
5435
|
+
if (!granted) throw new Error("The download capture export needs the downloads capability; request it from the review panel.");
|
|
5436
|
+
await chrome.downloads.download({ url: record2.bytes ?? "", filename: record2.name ?? `capture.${record2.format}` });
|
|
5437
|
+
return { target, destination: "reviewed download flow" };
|
|
5438
|
+
}
|
|
5439
|
+
return { target, destination: "session memory" };
|
|
5440
|
+
}
|
|
5441
|
+
async function runcapturepolicy() {
|
|
5442
|
+
return (await memory.getsettings())?.capturepolicy ?? "manual";
|
|
5443
|
+
}
|
|
5444
|
+
async function grabstateshot(step, session, plan, tabid2, phase) {
|
|
5445
|
+
const options = stepcaptureoptions(step);
|
|
5446
|
+
const format = options.format ?? "png";
|
|
5447
|
+
const shot = await tabshot(format, options.quality);
|
|
5448
|
+
const measured = await bridgecall(tabid2, "measurepage");
|
|
5449
|
+
const record2 = capturevisible({ runid: plan.id, stepid: step.id, options: { ...options, annotate: options.annotate ?? true }, viewport: { width: measured.viewportwidth, height: measured.viewportheight }, dataurl: shot, at: Date.now(), id: randomid(), name: `${phase}-${await capturenamefor(step, plan, "state", format)}` });
|
|
5450
|
+
await memory.addcapture(record2);
|
|
5451
|
+
return record2;
|
|
5452
|
+
}
|
|
5453
|
+
async function storecapture(record2, session, plan, step, origin) {
|
|
5454
|
+
await memory.addcapture(record2);
|
|
5455
|
+
const routed = await routecapture(record2, session, plan, step, origin);
|
|
5456
|
+
await memory.setprogress(recordcapture(await memory.getprogress(), plan.id, step.id, record2, Date.now()));
|
|
5457
|
+
await refreshbadge();
|
|
5458
|
+
return { record: record2, routed };
|
|
5459
|
+
}
|
|
5460
|
+
async function executecapturestep(step, session, plan, tabid2, origin) {
|
|
5461
|
+
const options = stepcaptureoptions(step);
|
|
5462
|
+
const format = options.format ?? "png";
|
|
5463
|
+
const ratio = options.pixelratio ?? 1;
|
|
5464
|
+
const runpolicy = await runcapturepolicy();
|
|
5465
|
+
const annotate = options.annotate ?? runpolicy === "annotated";
|
|
5466
|
+
const stepnumber = plan.steps.findIndex((candidate) => candidate.id === step.id) + 1;
|
|
5467
|
+
const extra = { ...session ? { sessionid: session.id } : {}, planid: plan.id, stepid: step.id };
|
|
5468
|
+
const pageurl = await chrome.tabs.get(tabid2).then((tab) => tab.url ?? origin).catch(() => origin);
|
|
5469
|
+
if (step.kind === "shotview") {
|
|
5470
|
+
const measured = await bridgecall(tabid2, "measurepage");
|
|
5471
|
+
const raw = await tabshot(format, options.quality);
|
|
5472
|
+
const marker = annotate ? annotationplanof({ step: Math.max(1, stepnumber), width: measured.viewportwidth, height: measured.viewportheight, url: pageurl, at: Date.now() }) : void 0;
|
|
5473
|
+
const dataurl = format === "png" && ratio === 1 && !marker ? raw : await encodecanvas(measured.viewportwidth * ratio, measured.viewportheight * ratio, async (context) => {
|
|
5474
|
+
const bitmap = await createImageBitmap(await (await fetch(raw)).blob());
|
|
5475
|
+
context.drawImage(bitmap, 0, 0, context.canvas.width, context.canvas.height);
|
|
5476
|
+
}, { format, quality: options.quality, annotate: marker, annotateratio: ratio });
|
|
5477
|
+
const record2 = capturevisible({ runid: plan.id, stepid: step.id, options: { ...options, ...annotate ? { annotate: true } : {} }, viewport: { width: measured.viewportwidth, height: measured.viewportheight }, dataurl, at: Date.now(), id: randomid(), name: await capturenamefor(step, plan, "shotview", format) });
|
|
5478
|
+
const stored = await storecapture(record2, session, plan, step, origin);
|
|
5479
|
+
await audit("capture", `Captured the visible viewport at ${record2.width} by ${record2.height} pixels in ${format} for step ${step.id}, routed to ${stored.routed.destination}.`, extra);
|
|
5480
|
+
return { ok: true, summary: `Captured the visible viewport at ${record2.width} by ${record2.height} pixels.`, details: { capture: { id: record2.id, kind: record2.kind, format: record2.format, width: record2.width, height: record2.height, bytes: (record2.bytes ?? "").length, exporttarget: stored.routed.target } } };
|
|
5481
|
+
}
|
|
5482
|
+
if (step.kind === "shotfullpage") {
|
|
5483
|
+
const rawoptions = stepoptions2(step);
|
|
5484
|
+
const settle2 = typeof rawoptions.settle === "number" ? rawoptions.settle : 150;
|
|
5485
|
+
const overlap = typeof rawoptions.overlap === "number" ? rawoptions.overlap : 0;
|
|
5486
|
+
const wait = typeof rawoptions.wait === "number" ? rawoptions.wait : void 0;
|
|
5487
|
+
const state = await bridgecall(tabid2, "preparecapture");
|
|
5488
|
+
try {
|
|
5489
|
+
const measured = await bridgecall(tabid2, "measurepage");
|
|
5490
|
+
const stitch = buildstitchplan({ scrollwidth: measured.scrollwidth, scrollheight: measured.scrollheight, viewportwidth: measured.viewportwidth, viewportheight: measured.viewportheight, overlap });
|
|
5491
|
+
if (wait !== void 0) {
|
|
5492
|
+
const budget = stitchbudgetallowed(stitch.tiles.length, settle2, wait);
|
|
5493
|
+
if (!budget.allowed) throw new Error(budget.reason ?? "The stitching scroll budget exceeds the reviewed wait window.");
|
|
5494
|
+
}
|
|
5495
|
+
const tiles = [];
|
|
5496
|
+
let done = 0;
|
|
5497
|
+
for (let index = 0; index < stitch.tiles.length; index += 1) {
|
|
5498
|
+
const tile = stitch.tiles[index];
|
|
5499
|
+
if (!tile) continue;
|
|
5500
|
+
await bridgecall(tabid2, "scrollcapture", tile.x, tile.y);
|
|
5501
|
+
await bridgecall(tabid2, "waitsettle", settle2);
|
|
5502
|
+
const shot = await tabshot("png", void 0);
|
|
5503
|
+
tiles.push(await decodeTile(shot, tile.x, tile.y, index % stitch.rows === 0));
|
|
5504
|
+
done += 1;
|
|
5505
|
+
stitchprogress.set(step.id, { stepid: step.id, done, total: stitch.tiles.length });
|
|
5506
|
+
}
|
|
5507
|
+
const annotation = annotate ? annotationplanof({ step: Math.max(1, stepnumber), width: stitch.scrollwidth, height: stitch.scrollheight, url: pageurl, at: Date.now() }) : void 0;
|
|
5508
|
+
const dataurl = await composestitched(tiles, stitch.scrollwidth, stitch.scrollheight, overlap, ratio, { format, quality: options.quality, annotate: annotation });
|
|
5509
|
+
const record2 = capturestitched({ runid: plan.id, stepid: step.id, options: { ...options, ...annotate ? { annotate: true } : {} }, plan: stitch, dataurl, at: Date.now(), id: randomid(), name: await capturenamefor(step, plan, "shotfullpage", format) });
|
|
5510
|
+
const stored = await storecapture(record2, session, plan, step, origin);
|
|
5511
|
+
await audit("capture", `Captured the full page of ${stitch.scrollwidth} by ${stitch.scrollheight} css pixels from ${stitch.tiles.length} stitched tile${stitch.tiles.length === 1 ? "" : "s"} with ${overlap} overlap row${overlap === 1 ? "" : "s"}, routed to ${stored.routed.destination}.`, extra);
|
|
5512
|
+
return { ok: true, summary: `Captured the full page from ${stitch.tiles.length} stitched tiles.`, details: { capture: { id: record2.id, kind: record2.kind, format: record2.format, width: record2.width, height: record2.height, bytes: (record2.bytes ?? "").length, exporttarget: stored.routed.target }, tiles: stitch.tiles.length, columns: stitch.columns, rows: stitch.rows, overlap } };
|
|
5513
|
+
} finally {
|
|
5514
|
+
stitchprogress.delete(step.id);
|
|
5515
|
+
await bridgecall(tabid2, "restorecapture", state).catch(() => void 0);
|
|
5516
|
+
}
|
|
5517
|
+
}
|
|
5518
|
+
if (step.kind === "shotelement") {
|
|
5519
|
+
const selector = step.target ?? "";
|
|
5520
|
+
const settle2 = typeof stepoptions2(step).settle === "number" ? stepoptions2(step).settle : 150;
|
|
5521
|
+
const measured = await bridgecall(tabid2, "measurepage");
|
|
5522
|
+
const targetinfo = await bridgecall(tabid2, "elementrect", selector);
|
|
5523
|
+
if (!targetinfo.ok || !targetinfo.rect) throw new Error(targetinfo.summary);
|
|
5524
|
+
const rect = targetinfo.rect;
|
|
5525
|
+
const crossing = targetinfo.crossesviewport === true;
|
|
5526
|
+
const state = await bridgecall(tabid2, "preparecapture");
|
|
5527
|
+
try {
|
|
5528
|
+
let dataurl = "";
|
|
5529
|
+
if (!crossing) {
|
|
5530
|
+
const scrollto = Math.max(0, rect.y - Math.floor((measured.viewportheight - rect.height) / 2));
|
|
5531
|
+
await bridgecall(tabid2, "scrollcapture", 0, scrollto);
|
|
5532
|
+
await bridgecall(tabid2, "waitsettle", settle2);
|
|
5533
|
+
const shot = await tabshot("png", void 0);
|
|
5534
|
+
const crop = croprect({ x: rect.x, y: rect.y - scrollto, width: rect.width, height: rect.height }, { width: measured.viewportwidth, height: measured.viewportheight });
|
|
5535
|
+
const annotation = annotate ? annotationplanof({ step: Math.max(1, stepnumber), width: crop.width, height: crop.height, rect: { x: 0, y: 0, width: crop.width, height: crop.height }, url: pageurl, at: Date.now() }) : void 0;
|
|
5536
|
+
dataurl = await croptoRect(shot, crop, ratio, { format, quality: options.quality, annotate: annotation });
|
|
5537
|
+
} else {
|
|
5538
|
+
const tops = regionsteps(rect.height, measured.viewportheight);
|
|
5539
|
+
const slices = [];
|
|
5540
|
+
for (let index = 0; index < tops.length; index += 1) {
|
|
5541
|
+
const top = tops[index] ?? 0;
|
|
5542
|
+
await bridgecall(tabid2, "scrollcapture", 0, rect.y + top);
|
|
5543
|
+
await bridgecall(tabid2, "waitsettle", settle2);
|
|
5544
|
+
const shot = await tabshot("png", void 0);
|
|
5545
|
+
const crop = croprect({ x: rect.x, y: 0, width: rect.width, height: Math.min(measured.viewportheight, rect.height - top) }, { width: measured.viewportwidth, height: measured.viewportheight });
|
|
5546
|
+
const slicedata = await croptoRect(shot, crop, 1, { format: "png" });
|
|
5547
|
+
slices.push(await decodeTile(slicedata, 0, top, index === 0));
|
|
5548
|
+
}
|
|
5549
|
+
const annotation = annotate ? annotationplanof({ step: Math.max(1, stepnumber), width: Math.min(rect.width, measured.viewportwidth), height: rect.height, rect: { x: 0, y: 0, width: Math.min(rect.width, measured.viewportwidth), height: rect.height }, url: pageurl, at: Date.now() }) : void 0;
|
|
5550
|
+
dataurl = await composestitched(slices, Math.min(rect.width, measured.viewportwidth), rect.height, 0, ratio, { format, quality: options.quality, annotate: annotation });
|
|
5551
|
+
}
|
|
5552
|
+
const record2 = captureelement({ runid: plan.id, stepid: step.id, options: { ...options, ...annotate ? { annotate: true } : {} }, rect, dataurl, at: Date.now(), id: randomid(), name: await capturenamefor(step, plan, "shotelement", format), target: selector });
|
|
5553
|
+
const stored = await storecapture(record2, session, plan, step, origin);
|
|
5554
|
+
await audit("capture", `Captured the element ${selector} at ${rect.width} by ${rect.height} css pixels${crossing ? " through the tiled fallback" : ""}, routed to ${stored.routed.destination}.`, extra);
|
|
5555
|
+
return { ok: true, summary: `Captured the element ${selector}.`, details: { capture: { id: record2.id, kind: record2.kind, format: record2.format, width: record2.width, height: record2.height, bytes: (record2.bytes ?? "").length, exporttarget: stored.routed.target }, rect, tiledfallback: crossing, target: selector } };
|
|
5556
|
+
} finally {
|
|
5557
|
+
await bridgecall(tabid2, "restorecapture", state).catch(() => void 0);
|
|
5558
|
+
}
|
|
5559
|
+
}
|
|
5560
|
+
if (step.kind === "shotregion") {
|
|
5561
|
+
const rawoptions = stepoptions2(step);
|
|
5562
|
+
const rect = rawoptions.regionrect;
|
|
5563
|
+
if (!rect) throw new Error("A reviewed regionrect is required in options.");
|
|
5564
|
+
const container = typeof rawoptions.container === "string" ? rawoptions.container : void 0;
|
|
5565
|
+
const measured = await bridgecall(tabid2, "measurepage");
|
|
5566
|
+
const state = await bridgecall(tabid2, "preparecapture");
|
|
5567
|
+
try {
|
|
5568
|
+
let dataurl = "";
|
|
5569
|
+
let captured = rect;
|
|
5570
|
+
if (container) {
|
|
5571
|
+
const info = await bridgecall(tabid2, "scrollcontainercapture", container, 0);
|
|
5572
|
+
if (!info.ok || info.height === void 0 || info.viewportheight === void 0) throw new Error(info.summary);
|
|
5573
|
+
const steps = regionsteps(info.height, info.viewportheight);
|
|
5574
|
+
const strips = [];
|
|
5575
|
+
for (let index = 0; index < steps.length; index += 1) {
|
|
5576
|
+
await bridgecall(tabid2, "scrollcontainercapture", container, steps[index] ?? 0);
|
|
5577
|
+
await bridgecall(tabid2, "waitsettle", typeof rawoptions.settle === "number" ? rawoptions.settle : 150);
|
|
5578
|
+
const shot = await tabshot("png", void 0);
|
|
5579
|
+
strips.push(await decodeTile(shot, 0, steps[index] ?? 0, index === 0));
|
|
5580
|
+
}
|
|
5581
|
+
const annotation = annotate ? annotationplanof({ step: Math.max(1, stepnumber), width: rect.width, height: info.height, rect, url: pageurl, at: Date.now() }) : void 0;
|
|
5582
|
+
dataurl = await composestitched(strips, rect.width, info.height, 0, ratio, { format, quality: options.quality, annotate: annotation });
|
|
5583
|
+
captured = { x: rect.x, y: rect.y, width: rect.width, height: info.height };
|
|
5584
|
+
} else {
|
|
5585
|
+
await bridgecall(tabid2, "scrollcapture", 0, Math.max(0, rect.y - Math.floor((measured.viewportheight - rect.height) / 2)));
|
|
5586
|
+
await bridgecall(tabid2, "waitsettle", typeof rawoptions.settle === "number" ? rawoptions.settle : 150);
|
|
5587
|
+
const shot = await tabshot("png", void 0);
|
|
5588
|
+
const crop = croprect({ x: rect.x, y: rect.y - Math.max(0, rect.y - Math.floor((measured.viewportheight - rect.height) / 2)), width: rect.width, height: rect.height }, { width: measured.viewportwidth, height: measured.viewportheight });
|
|
5589
|
+
const annotation = annotate ? annotationplanof({ step: Math.max(1, stepnumber), width: crop.width, height: crop.height, rect: { x: 0, y: 0, width: crop.width, height: crop.height }, url: pageurl, at: Date.now() }) : void 0;
|
|
5590
|
+
dataurl = await croptoRect(shot, crop, ratio, { format, quality: options.quality, annotate: annotation });
|
|
5591
|
+
captured = crop;
|
|
5592
|
+
}
|
|
5593
|
+
const record2 = captureregion({ runid: plan.id, stepid: step.id, options: { ...options, ...annotate ? { annotate: true } : {} }, rect: captured, dataurl, at: Date.now(), id: randomid(), name: await capturenamefor(step, plan, "shotregion", format), ...container ? { target: container } : {} });
|
|
5594
|
+
const stored = await storecapture(record2, session, plan, step, origin);
|
|
5595
|
+
await audit("capture", `Captured the reviewed region at ${captured.width} by ${captured.height} css pixels${container ? ` of the scrollable container ${container}` : ""}, routed to ${stored.routed.destination}.`, extra);
|
|
5596
|
+
return { ok: true, summary: `Captured the reviewed region${container ? ` of container ${container}` : ""}.`, details: { capture: { id: record2.id, kind: record2.kind, format: record2.format, width: record2.width, height: record2.height, bytes: (record2.bytes ?? "").length, exporttarget: stored.routed.target }, rect: captured, ...container ? { container } : {} } };
|
|
5597
|
+
} finally {
|
|
5598
|
+
await bridgecall(tabid2, "restorecapture", state).catch(() => void 0);
|
|
5599
|
+
}
|
|
5600
|
+
}
|
|
5601
|
+
if (step.kind === "contactsheet") {
|
|
5602
|
+
const rawoptions = stepoptions2(step);
|
|
5603
|
+
const elements = (Array.isArray(rawoptions.elements) ? rawoptions.elements : []).filter((item) => typeof item === "string" && item.trim().length > 0);
|
|
5604
|
+
const layout = rawoptions.sheet && typeof rawoptions.sheet === "object" && !Array.isArray(rawoptions.sheet) ? rawoptions.sheet : { cellsize: 240, columns: 3, label: "both" };
|
|
5605
|
+
const measured = await bridgecall(tabid2, "measurepage");
|
|
5606
|
+
const state = await bridgecall(tabid2, "preparecapture");
|
|
5607
|
+
try {
|
|
5608
|
+
const cells = [];
|
|
5609
|
+
for (const selector of elements) {
|
|
5610
|
+
const targetinfo = await bridgecall(tabid2, "elementrect", selector);
|
|
5611
|
+
if (!targetinfo.ok || !targetinfo.rect) throw new Error(targetinfo.summary);
|
|
5612
|
+
await bridgecall(tabid2, "scrollcapture", 0, Math.max(0, targetinfo.rect.y - Math.floor((measured.viewportheight - targetinfo.rect.height) / 2)));
|
|
5613
|
+
await bridgecall(tabid2, "waitsettle", typeof rawoptions.settle === "number" ? rawoptions.settle : 150);
|
|
5614
|
+
const shot = await tabshot("png", void 0);
|
|
5615
|
+
const crop = croprect({ x: 0, y: targetinfo.rect.y - Math.max(0, targetinfo.rect.y - Math.floor((measured.viewportheight - targetinfo.rect.height) / 2)), width: targetinfo.rect.width, height: targetinfo.rect.height }, { width: measured.viewportwidth, height: measured.viewportheight });
|
|
5616
|
+
const celldata = await croptoRect(shot, crop, 1, { format: "png" });
|
|
5617
|
+
cells.push({ selector, dataurl: celldata, width: crop.width, height: crop.height });
|
|
5618
|
+
}
|
|
5619
|
+
const placed = buildsheet(cells.map((cell) => ({ selector: cell.selector })), layout);
|
|
5620
|
+
const width = placed.columns * layout.cellsize;
|
|
5621
|
+
const rows = placed.rows;
|
|
5622
|
+
const height = rows * (layout.cellsize + 28);
|
|
5623
|
+
const annotation = annotate ? annotationplanof({ step: Math.max(1, stepnumber), width, height, url: pageurl, at: Date.now() }) : void 0;
|
|
5624
|
+
const dataurl = await encodecanvas(width * ratio, height * ratio, async (context) => {
|
|
5625
|
+
context.scale(ratio, ratio);
|
|
5626
|
+
for (let index = 0; index < cells.length; index += 1) {
|
|
5627
|
+
const cell = cells[index];
|
|
5628
|
+
const place = placed.cells[index];
|
|
5629
|
+
if (!cell || !place) continue;
|
|
5630
|
+
const bitmap = await createImageBitmap(await (await fetch(cell.dataurl)).blob());
|
|
5631
|
+
const scaled = Math.min(layout.cellsize / Math.max(1, bitmap.width), layout.cellsize / Math.max(1, bitmap.height));
|
|
5632
|
+
const drawwidth = Math.max(1, Math.round(bitmap.width * scaled));
|
|
5633
|
+
const drawheight = Math.max(1, Math.round(bitmap.height * scaled));
|
|
5634
|
+
context.drawImage(bitmap, place.column * layout.cellsize + Math.floor((layout.cellsize - drawwidth) / 2), place.row * (layout.cellsize + 28) + Math.floor((layout.cellsize - drawheight) / 2), drawwidth, drawheight);
|
|
5635
|
+
if (place.caption) {
|
|
5636
|
+
context.fillStyle = "rgba(23,32,44,.86)";
|
|
5637
|
+
context.fillRect(place.column * layout.cellsize, place.row * (layout.cellsize + 28) + layout.cellsize, layout.cellsize, 28);
|
|
5638
|
+
context.fillStyle = "#ffffff";
|
|
5639
|
+
context.font = "12px sans-serif";
|
|
5640
|
+
context.textAlign = "left";
|
|
5641
|
+
context.textBaseline = "middle";
|
|
5642
|
+
context.fillText(place.caption.slice(0, Math.floor(layout.cellsize / 7)), place.column * layout.cellsize + 6, place.row * (layout.cellsize + 28) + layout.cellsize + 14);
|
|
5643
|
+
}
|
|
5644
|
+
}
|
|
5645
|
+
}, { format, quality: options.quality, annotate: annotation, annotateratio: ratio });
|
|
5646
|
+
const record2 = { id: randomid(), runid: plan.id, stepid: step.id, kind: "contactsheet", format, width: Math.round(width * ratio), height: Math.round(height * ratio), capturedat: Date.now(), bytes: dataurl, name: await capturenamefor(step, plan, "contactsheet", format), ...annotate ? { annotated: true } : {}, ...options.exporttarget !== void 0 ? { exporttarget: options.exporttarget } : {} };
|
|
5647
|
+
const stored = await storecapture(record2, session, plan, step, origin);
|
|
5648
|
+
await audit("capture", `Tiled ${cells.length} element capture${cells.length === 1 ? "" : "s"} into one labeled ${placed.columns} column contact sheet, routed to ${stored.routed.destination}.`, extra);
|
|
5649
|
+
return { ok: true, summary: `Tiled ${cells.length} element captures into one contact sheet.`, details: { capture: { id: record2.id, kind: record2.kind, format: record2.format, width: record2.width, height: record2.height, bytes: (record2.bytes ?? "").length, exporttarget: stored.routed.target }, cells: placed.cells, columns: placed.columns, rows: placed.rows } };
|
|
5650
|
+
} finally {
|
|
5651
|
+
await bridgecall(tabid2, "restorecapture", state).catch(() => void 0);
|
|
5652
|
+
}
|
|
5653
|
+
}
|
|
5654
|
+
throw new Error("Unsupported capture kind.");
|
|
5655
|
+
}
|
|
4287
5656
|
async function enforcewindowreview(step, session, plan) {
|
|
4288
5657
|
const windowid = step.value && /^\d+$/.test(step.value) ? Number.parseInt(step.value, 10) : 0;
|
|
4289
5658
|
const progress = plan ? await memory.getprogress() : void 0;
|
|
@@ -4319,9 +5688,12 @@ async function refreshbadge() {
|
|
|
4319
5688
|
const queues = await memory.getnavqueues();
|
|
4320
5689
|
const badges = await memory.getbadges();
|
|
4321
5690
|
const prompts = (await memory.gettickets()).filter((ticket) => ticket.approved === void 0).length;
|
|
5691
|
+
const consents = (await memory.getclipconsents()).filter((record2) => record2.approved === void 0).length;
|
|
5692
|
+
const quarantined = (await memory.getquarantines()).filter((entry) => entry.scan === "pending").length;
|
|
4322
5693
|
const datasets = (await memory.getdatasets()).length;
|
|
5694
|
+
const captures = (await memory.getcaptures()).length;
|
|
4323
5695
|
const tasktabs2 = new Set(badges.map((badge) => badge.tabid)).size;
|
|
4324
|
-
const total = (queues?.prefetch ?? 0) + (queues?.batchopen ?? 0) + tasktabs2 + prompts + datasets;
|
|
5696
|
+
const total = (queues?.prefetch ?? 0) + (queues?.batchopen ?? 0) + tasktabs2 + prompts + consents + quarantined + datasets + captures;
|
|
4325
5697
|
await chrome.action.setBadgeText({ text: total > 0 ? String(total) : "" }).catch(() => {
|
|
4326
5698
|
});
|
|
4327
5699
|
}
|
|
@@ -4340,44 +5712,69 @@ async function executestep(stepid) {
|
|
|
4340
5712
|
}
|
|
4341
5713
|
let output;
|
|
4342
5714
|
let watchwindow;
|
|
4343
|
-
|
|
4344
|
-
|
|
4345
|
-
|
|
4346
|
-
|
|
4347
|
-
|
|
4348
|
-
|
|
4349
|
-
|
|
4350
|
-
|
|
4351
|
-
|
|
4352
|
-
|
|
4353
|
-
|
|
4354
|
-
|
|
4355
|
-
|
|
4356
|
-
|
|
4357
|
-
|
|
4358
|
-
|
|
4359
|
-
|
|
4360
|
-
|
|
4361
|
-
|
|
4362
|
-
|
|
4363
|
-
|
|
4364
|
-
|
|
4365
|
-
|
|
4366
|
-
|
|
4367
|
-
if (
|
|
4368
|
-
|
|
4369
|
-
|
|
4370
|
-
|
|
4371
|
-
|
|
4372
|
-
|
|
4373
|
-
|
|
4374
|
-
|
|
4375
|
-
|
|
4376
|
-
if (
|
|
4377
|
-
|
|
4378
|
-
|
|
5715
|
+
const dispatchreviewedstep = async () => {
|
|
5716
|
+
if (step.kind === "windowclose") {
|
|
5717
|
+
await enforcewindowreview(step, session, plan);
|
|
5718
|
+
}
|
|
5719
|
+
if (istabscommandkind(step.kind)) {
|
|
5720
|
+
output = await executetabscommand(step, session, plan, tab.id);
|
|
5721
|
+
} else if (isdatasetkind(step.kind)) {
|
|
5722
|
+
output = await executedatastep(step, session, plan, tab.id, origin);
|
|
5723
|
+
} else if (isfileskind(step.kind)) {
|
|
5724
|
+
output = await executefilesstep(step, session, plan, tab.id, origin);
|
|
5725
|
+
} else if (isformkind(step.kind)) {
|
|
5726
|
+
output = await executeformstep(step, session, plan, tab.id, origin);
|
|
5727
|
+
} else if (isbrowserkind(step.kind)) {
|
|
5728
|
+
output = await runbrowseraction(step, tab.id, tab.windowId ?? chrome.windows.WINDOW_ID_CURRENT);
|
|
5729
|
+
} else if (step.kind === "keyhold") {
|
|
5730
|
+
output = await executekeyhold(step, session, plan, tab.id, origin);
|
|
5731
|
+
} else if (step.kind === "keyrelease") {
|
|
5732
|
+
output = await executekeyrelease(step, session, plan, tab.id, origin);
|
|
5733
|
+
} else if (step.kind === "dismissdialog") {
|
|
5734
|
+
output = await executedismissdialog(step, session, plan, tab.id, origin);
|
|
5735
|
+
} else if (step.kind === "retryaction") {
|
|
5736
|
+
output = await executeretryaction(step, session, plan, tab.id, origin);
|
|
5737
|
+
} else if (step.kind === "mapclicks") {
|
|
5738
|
+
output = await executemapclicks(step, plan, tab.id, origin);
|
|
5739
|
+
} else if (step.kind === "enterframe") {
|
|
5740
|
+
output = await executeenterframe(step, plan, tab.id, origin);
|
|
5741
|
+
} else if (watchstepkinds.has(step.kind)) {
|
|
5742
|
+
if (!session || !plan || plan.state !== "approved") throw new Error("Watch kinds refuse to run outside an approved session plan.");
|
|
5743
|
+
const watched = await executewatchstep(step, session, plan, tab.id, origin);
|
|
5744
|
+
output = watched.output;
|
|
5745
|
+
watchwindow = { startedat: watched.watch.startedat, lifetime: watched.watch.lifetime };
|
|
5746
|
+
} else if (step.kind === "diffsnapshots") {
|
|
5747
|
+
output = await executediffsnapshots(step, session, plan, tab.id, origin);
|
|
5748
|
+
} else if (navigationstepkinds.has(step.kind)) {
|
|
5749
|
+
output = await executenavigationkind(step, session, plan, tab.id, origin);
|
|
5750
|
+
} else if (iscapturekind(step.kind)) {
|
|
5751
|
+
output = await executecapturestep(step, session, plan, tab.id, origin);
|
|
5752
|
+
} else {
|
|
5753
|
+
if (step.target && freshcheckkinds.has(step.kind)) {
|
|
5754
|
+
const fresh = await snapshot(tab.id);
|
|
5755
|
+
if (!fresh.interactive.some((item) => item.selector === step.target)) throw new Error("The page changed and the target must be reviewed again.");
|
|
5756
|
+
}
|
|
5757
|
+
output = await dispatchpagestep(step, tab.id, origin, plan);
|
|
4379
5758
|
}
|
|
4380
|
-
output
|
|
5759
|
+
return output;
|
|
5760
|
+
};
|
|
5761
|
+
const runplan = plan;
|
|
5762
|
+
const capturepolicystate = await runcapturepolicy();
|
|
5763
|
+
if (capturepolicystate === "beforeafter" && session && step.risk !== "read" && beforeafterwrapallowed(step.kind)) {
|
|
5764
|
+
const before = await grabstateshot(step, session, runplan, tab.id, "before");
|
|
5765
|
+
output = await dispatchreviewedstep();
|
|
5766
|
+
if (output?.ok) {
|
|
5767
|
+
const after = await grabstateshot(step, session, runplan, tab.id, "after");
|
|
5768
|
+
const domversion = await memory.getobservationversion();
|
|
5769
|
+
const paired = capturestates({ policy: "beforeafter", before, after, actionkind: step.kind, ...step.target ? { target: step.target } : {}, ...domversion !== void 0 ? { domsnapshotid: String(domversion) } : {}, at: Date.now(), id: randomid() });
|
|
5770
|
+
if (paired.pair) {
|
|
5771
|
+
await memory.addpair(paired.pair);
|
|
5772
|
+
await memory.setprogress(recordpair(await memory.getprogress(), runplan.id, step.id, paired.pair, Date.now()));
|
|
5773
|
+
await audit("capture", `Paired the before shot ${paired.pair.beforeid} with the after shot ${paired.pair.afterid} around the ${step.kind} action${paired.pair.domsnapshotid !== void 0 ? ` with dom snapshot ${paired.pair.domsnapshotid}` : ""}.`, { sessionid: session.id, planid: runplan.id, stepid: step.id });
|
|
5774
|
+
}
|
|
5775
|
+
}
|
|
5776
|
+
} else {
|
|
5777
|
+
output = await dispatchreviewedstep();
|
|
4381
5778
|
}
|
|
4382
5779
|
if (["navigate", "back", "forward"].includes(step.kind)) await recordnavigation(step, session, tab.id);
|
|
4383
5780
|
await recordevidence(step, output, session, plan, origin);
|
|
@@ -4451,6 +5848,7 @@ async function grantcapability(permission) {
|
|
|
4451
5848
|
if (!["tabs", "downloads", "clipboardRead", "clipboardWrite"].includes(permission)) throw new Error("Unknown capability.");
|
|
4452
5849
|
const granted = await chrome.permissions.request({ permissions: [permission] });
|
|
4453
5850
|
if (!granted) throw new Error("The capability grant was declined.");
|
|
5851
|
+
if (permission === "downloads") installmimelistener();
|
|
4454
5852
|
await audit("capability", `Capability ${permission} granted by the user.`);
|
|
4455
5853
|
return refreshcapabilities();
|
|
4456
5854
|
}
|
|
@@ -4524,13 +5922,34 @@ async function handlerequest(message, sender) {
|
|
|
4524
5922
|
for (const config of sheetendpoints) {
|
|
4525
5923
|
sheetgrants.push({ ...config, granted: await chrome.permissions.contains({ origins: [hostpattern(config.origin)] }).catch(() => false) });
|
|
4526
5924
|
}
|
|
5925
|
+
const downloads = await memory.getdownloads();
|
|
5926
|
+
const netlogs = await memory.getnetlog();
|
|
5927
|
+
const clipconsents = await memory.getclipconsents();
|
|
5928
|
+
const clips = await memory.getclips();
|
|
5929
|
+
const quarantines = await memory.getquarantines();
|
|
5930
|
+
const cleanuprules = await memory.getcleanuprules();
|
|
5931
|
+
const cleanupruns = await memory.getcleanupruns();
|
|
5932
|
+
const capturecounters = await memory.getcapturecounters();
|
|
5933
|
+
const inventory = await memory.getinventory();
|
|
5934
|
+
const mimefilters = await memory.getmimefilters();
|
|
5935
|
+
const capturemetadata = (await memory.getcaptures()).map((record2) => {
|
|
5936
|
+
const { bytes, ...meta } = record2;
|
|
5937
|
+
void bytes;
|
|
5938
|
+
return meta;
|
|
5939
|
+
});
|
|
5940
|
+
const capturepairs = await memory.getpairs();
|
|
5941
|
+
const runsettings = await memory.getsettings();
|
|
5942
|
+
const scanhooks = [];
|
|
5943
|
+
for (const hook of await memory.getscanhooks()) {
|
|
5944
|
+
scanhooks.push({ ...hook, granted: await chrome.permissions.contains({ origins: [hostpattern(hook.origin)] }).catch(() => false) });
|
|
5945
|
+
}
|
|
4527
5946
|
const clones = clonetabs(tabs);
|
|
4528
5947
|
const taskgauge = tasktabgauge(tabs.filter((tab) => badges.some((badge) => badge.tabid === tab.tabid)).length, tasktabceiling(await memory.getsettings()));
|
|
4529
5948
|
const report = await buildtabreport(tabs);
|
|
4530
5949
|
const livetab = session ? await chrome.tabs.get(session.tabid).catch(() => void 0) : void 0;
|
|
4531
5950
|
const waitprofile = session ? waitprofiles.find((record2) => record2.origin === session.origin) : void 0;
|
|
4532
5951
|
const livestate = { phase: livetab?.status === "loading" ? "loading" : "complete", ...navrecords[0] ? { finalurl: navrecords[0].finalurl, redirects: navrecords[0].chain } : {} };
|
|
4533
|
-
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 };
|
|
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()] } : {} };
|
|
4534
5953
|
}
|
|
4535
5954
|
case "capabilities":
|
|
4536
5955
|
return refreshcapabilities();
|
|
@@ -4572,7 +5991,8 @@ async function handlerequest(message, sender) {
|
|
|
4572
5991
|
const outcome = (await memory.getoutcomes()).find((candidate) => candidate.stepid === (input.stepid ?? ""));
|
|
4573
5992
|
if (!outcome) throw new Error("No outcome exists for the reviewed step.");
|
|
4574
5993
|
const resolved = outcome.details?.resolvedtarget;
|
|
4575
|
-
|
|
5994
|
+
const capture = outcome.details?.capture;
|
|
5995
|
+
return JSON.parse(outcomeresponse({ outcome, plan, ...resolved ? { resolvedtarget: resolved } : {}, ...capture ? { capture } : {} }));
|
|
4576
5996
|
}
|
|
4577
5997
|
case "map": {
|
|
4578
5998
|
const plan = await memory.getplan();
|
|
@@ -4861,6 +6281,128 @@ async function handlerequest(message, sender) {
|
|
|
4861
6281
|
return extractionreportValue();
|
|
4862
6282
|
case "provenance":
|
|
4863
6283
|
return provenancereportValue();
|
|
6284
|
+
case "downloadreport": {
|
|
6285
|
+
const plan = await memory.getplan();
|
|
6286
|
+
if (!plan) throw new Error("No plan is available for a download report envelope.");
|
|
6287
|
+
return JSON.parse(downloadreport({ downloads: await memory.getdownloads(), plan }));
|
|
6288
|
+
}
|
|
6289
|
+
case "quarantine":
|
|
6290
|
+
return quarantinereport({ entries: await memory.getquarantines() });
|
|
6291
|
+
case "netlog":
|
|
6292
|
+
return netlogreport({ records: await memory.getnetlog() });
|
|
6293
|
+
case "downloadaction": {
|
|
6294
|
+
const inputdownload = message;
|
|
6295
|
+
const session = await memory.getsession();
|
|
6296
|
+
if (!session || session.stoppedat || session.expiresat <= Date.now()) throw new Error("Download actions stay behind the consent gate of an active session.");
|
|
6297
|
+
const record2 = await loadownload(inputdownload.id ?? "");
|
|
6298
|
+
if (!record2) throw new Error(`No stored download matches ${inputdownload.id ?? ""}.`);
|
|
6299
|
+
const extra = { sessionid: session.id };
|
|
6300
|
+
if (inputdownload.action === "pause") return pauseonerecord(record2, extra);
|
|
6301
|
+
if (inputdownload.action === "resume") return resumeonerecord(record2, extra);
|
|
6302
|
+
if (inputdownload.action === "verify") return verifyonerecord(record2, {}, extra);
|
|
6303
|
+
throw new Error("A pause, resume or verify action is required.");
|
|
6304
|
+
}
|
|
6305
|
+
case "approveclipconsent": {
|
|
6306
|
+
const inputconsent = message;
|
|
6307
|
+
const record2 = (await memory.getclipconsents()).find((item) => item.id === inputconsent.id);
|
|
6308
|
+
if (!record2) throw new Error("No clipboard consent prompt matches the requested id.");
|
|
6309
|
+
await memory.setclipconsent({ ...record2, approved: inputconsent.approved !== false });
|
|
6310
|
+
const session = await memory.getsession();
|
|
6311
|
+
await audit("clipboard", `Clipboard consent prompt ${record2.id} for step ${record2.stepid} on ${record2.origin} ${inputconsent.approved !== false ? "approved" : "declined"} by the user; every read consumes its own prompt.`, { ...session ? { sessionid: session.id } : {} });
|
|
6312
|
+
await refreshbadge();
|
|
6313
|
+
return { id: record2.id, approved: inputconsent.approved !== false };
|
|
6314
|
+
}
|
|
6315
|
+
case "releasequarantine": {
|
|
6316
|
+
const inputquarantine = message;
|
|
6317
|
+
const entry = (await memory.getquarantines()).find((item) => item.id === inputquarantine.id);
|
|
6318
|
+
if (!entry) throw new Error("No quarantined file matches the requested id.");
|
|
6319
|
+
if (entry.release !== void 0) throw new Error(`The quarantined file ${entry.path} was already released.`);
|
|
6320
|
+
const gate = quarantinereleasegranted(entry);
|
|
6321
|
+
if (!gate.allowed) throw new Error(gate.reason);
|
|
6322
|
+
const releasedentry = released(entry, `user-${Date.now()}`, Date.now());
|
|
6323
|
+
await memory.setquarantine(releasedentry);
|
|
6324
|
+
const session = await memory.getsession();
|
|
6325
|
+
await audit("quarantine", `Quarantine release of ${entry.path} approved with the ${entry.scan} scan verdict under ref ${releasedentry.release}; every release is audited with its verdict.`, { ...session ? { sessionid: session.id } : {} });
|
|
6326
|
+
await refreshbadge();
|
|
6327
|
+
return releasedentry;
|
|
6328
|
+
}
|
|
6329
|
+
case "setcleanuprules": {
|
|
6330
|
+
const inputrules = message;
|
|
6331
|
+
const rules = (Array.isArray(inputrules.rules) ? inputrules.rules : []).filter((item) => Boolean(item) && typeof item === "object" && !Array.isArray(item));
|
|
6332
|
+
for (const rule of rules) {
|
|
6333
|
+
const gate = validatecleanuprule(rule);
|
|
6334
|
+
if (!gate.allowed) throw new Error(gate.reason);
|
|
6335
|
+
}
|
|
6336
|
+
await memory.setcleanuprules(rules);
|
|
6337
|
+
const session = await memory.getsession();
|
|
6338
|
+
await audit("cleanup", `The review panel stored ${rules.length} reviewed cleanup rule${rules.length === 1 ? "" : "s"} with age windows and keep policies; ages stay user configured with no code ceiling.`, { ...session ? { sessionid: session.id } : {} });
|
|
6339
|
+
return { rules: rules.length };
|
|
6340
|
+
}
|
|
6341
|
+
case "configurescanhook": {
|
|
6342
|
+
const inputhook = message;
|
|
6343
|
+
const config = normalizeendpoint(inputhook.endpoint ?? "");
|
|
6344
|
+
const granted = await chrome.permissions.contains({ origins: [hostpattern(config.origin)] });
|
|
6345
|
+
if (!granted) throw new Error("The scan hook origin has not received optional permission.");
|
|
6346
|
+
if (!inputhook.scanner?.trim()) throw new Error("A scanner name is required for the scan hook.");
|
|
6347
|
+
const hook = { scanner: inputhook.scanner.trim(), endpoint: config.endpoint, origin: config.origin, configuredat: Date.now() };
|
|
6348
|
+
await memory.setscanhook(hook);
|
|
6349
|
+
await audit("quarantine", `Configured the virus scanning hook ${hook.scanner} at ${hook.origin}; scan verdicts arrive from the endpoint and hook failures stay pending verdicts.`, {});
|
|
6350
|
+
return hook;
|
|
6351
|
+
}
|
|
6352
|
+
case "exportnetlog": {
|
|
6353
|
+
const session = await memory.getsession();
|
|
6354
|
+
if (!session || session.stoppedat || session.expiresat <= Date.now()) throw new Error("Netlog exports stay behind the consent gate of an active session.");
|
|
6355
|
+
const records = (await memory.getnetlog()).map((record2) => ({ ...record2, headers: redactheaders(record2.headers ?? {}) }));
|
|
6356
|
+
await audit("observation", `The review panel exported ${records.length} netlog record${records.length === 1 ? "" : "s"} of the run with every header value redacted.`, { sessionid: session.id });
|
|
6357
|
+
return { records, redacted: true, redaction: "every header value is redacted from exported netlogs" };
|
|
6358
|
+
}
|
|
6359
|
+
case "capturebytes": {
|
|
6360
|
+
const inputcapture = message;
|
|
6361
|
+
const record2 = await memory.getcapture(inputcapture.id ?? "");
|
|
6362
|
+
if (!record2) throw new Error("No stored capture matches the requested id.");
|
|
6363
|
+
if (!record2.bytes) throw new Error("The capture bytes expired from the retention window; the metadata stays for the audit trail.");
|
|
6364
|
+
return { id: record2.id, runid: record2.runid, kind: record2.kind, format: record2.format, width: record2.width, height: record2.height, capturedat: record2.capturedat, name: record2.name, annotated: record2.annotated === true, bytes: record2.bytes };
|
|
6365
|
+
}
|
|
6366
|
+
case "capturereport":
|
|
6367
|
+
return capturereport({ records: await memory.getcaptures(), pairs: await memory.getpairs() });
|
|
6368
|
+
case "copycapture": {
|
|
6369
|
+
const inputcopy = message;
|
|
6370
|
+
const session = await memory.getsession();
|
|
6371
|
+
if (!session || session.stoppedat || session.expiresat <= Date.now()) throw new Error("Capture clipboard copies stay behind the consent gate of an active session.");
|
|
6372
|
+
const record2 = await memory.getcapture(inputcopy.id ?? "");
|
|
6373
|
+
if (!record2) throw new Error("No stored capture matches the requested id.");
|
|
6374
|
+
if (!record2.bytes) throw new Error("The capture bytes expired from the retention window; the metadata stays for the audit trail.");
|
|
6375
|
+
const granted = await chrome.permissions.contains({ permissions: ["clipboardWrite"] }).catch(() => false);
|
|
6376
|
+
if (!granted) throw new Error("The capture copy needs the clipboardwrite capability; request it from the review panel.");
|
|
6377
|
+
const blob = await (await fetch(record2.bytes)).blob();
|
|
6378
|
+
await navigator.clipboard.write([new ClipboardItem({ [blob.type]: blob })]);
|
|
6379
|
+
await memory.addclip(clipentryof("screen", { hash: cliphash(record2.bytes), length: record2.bytes.length }, session.origin, record2.stepid, Date.now()));
|
|
6380
|
+
await audit("capture", `The review panel copied capture ${record2.id} of kind ${record2.kind} to the clipboard with payload hash ${cliphash(record2.bytes)}.`, { sessionid: session.id });
|
|
6381
|
+
return { id: record2.id, copied: true };
|
|
6382
|
+
}
|
|
6383
|
+
case "downloadcapture": {
|
|
6384
|
+
const inputdownloadcapture = message;
|
|
6385
|
+
const session = await memory.getsession();
|
|
6386
|
+
if (!session || session.stoppedat || session.expiresat <= Date.now()) throw new Error("Capture downloads stay behind the consent gate of an active session.");
|
|
6387
|
+
const record2 = await memory.getcapture(inputdownloadcapture.id ?? "");
|
|
6388
|
+
if (!record2) throw new Error("No stored capture matches the requested id.");
|
|
6389
|
+
if (!record2.bytes) throw new Error("The capture bytes expired from the retention window; the metadata stays for the audit trail.");
|
|
6390
|
+
const granted = await chrome.permissions.contains({ permissions: ["downloads"] }).catch(() => false);
|
|
6391
|
+
if (!granted) throw new Error("The capture download needs the downloads capability; request it from the review panel.");
|
|
6392
|
+
await chrome.downloads.download({ url: record2.bytes, filename: record2.name ?? `capture.${record2.format}` });
|
|
6393
|
+
await audit("capture", `The review panel downloaded capture ${record2.id} of kind ${record2.kind} through the reviewed download flow.`, { sessionid: session.id });
|
|
6394
|
+
return { id: record2.id, downloaded: true };
|
|
6395
|
+
}
|
|
6396
|
+
case "setcapturepolicy": {
|
|
6397
|
+
const inputpolicy = message;
|
|
6398
|
+
const mode = inputpolicy.mode === "off" || inputpolicy.mode === "manual" || inputpolicy.mode === "annotated" || inputpolicy.mode === "beforeafter" ? inputpolicy.mode : void 0;
|
|
6399
|
+
if (!mode) throw new Error("The capture policy must be off, manual, annotated or beforeafter.");
|
|
6400
|
+
const settings = await memory.getsettings();
|
|
6401
|
+
await memory.setsettings({ ...settings, capturepolicy: mode });
|
|
6402
|
+
await audit("configure", `The user set the capture policy of the run to ${mode}; beforeafter wraps page moving actions with state pairs.`);
|
|
6403
|
+
await refreshbadge();
|
|
6404
|
+
return { capturepolicy: mode };
|
|
6405
|
+
}
|
|
4864
6406
|
case "stop": {
|
|
4865
6407
|
const session = await memory.getsession();
|
|
4866
6408
|
if (session) await memory.setsession({ ...session, stoppedat: Date.now() });
|