@wenathlan/extension 1.1.38 → 1.1.39

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.
@@ -619,23 +619,136 @@ 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
+ }
622
734
  };
623
735
  function randomid() {
624
736
  return crypto.randomUUID();
625
737
  }
626
738
 
627
739
  // 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"]);
740
+ 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
741
  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"]);
742
+ 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"]);
631
743
  var allowedactions = /* @__PURE__ */ new Set([...sensitiveactions, ...interactionactions, ...readactions]);
632
744
  var watchactions = /* @__PURE__ */ new Set(["watchmutate", "watchbanner", "watchfocus", "watchtab"]);
633
745
  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"]);
746
+ 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
747
  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
748
  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
749
  var datasetactions = /* @__PURE__ */ new Set(["scrapetable", "exportcsv", "exportjson", "exportexcel", "copytable", "pushsheets", "importcsv", "looprows", "transformvalues", "deduperows", "paginateextract", "mergepages", "stamplerows", "previewgrid", "streamdisk", "resumeextract", "logprovenance"]);
638
750
  var exportactions = /* @__PURE__ */ new Set(["exportcsv", "exportjson", "exportexcel", "copytable", "pushsheets", "streamdisk"]);
751
+ var filesactions = /* @__PURE__ */ new Set(["batchdownload", "pausedownload", "resumedownload", "verifydownload", "interceptmime", "exportnetlog", "readclipboard", "writeclipboard", "copyscreen", "quarantinedownload", "scanvirus", "namecaptures", "cleanupartifacts"]);
639
752
  var fieldkinds = ["text", "email", "phone", "date", "number", "select", "check", "radio", "file", "password", "card", "code"];
640
753
  var layoutmutationactions = /* @__PURE__ */ new Set(["grouptabs", "colorgroup", "collapsegroup", "savelayout", "restorelayout"]);
641
754
  var groupcolors = ["grey", "blue", "red", "yellow", "green", "pink", "purple", "cyan", "orange"];
@@ -671,6 +784,9 @@ function requiredcapability(kind) {
671
784
  if (kind === "downloadfile") return "downloads";
672
785
  if (kind === "openclipboard") return "clipboardRead";
673
786
  if (kind === "copytable") return "clipboardWrite";
787
+ if (kind === "batchdownload" || kind === "pausedownload" || kind === "resumedownload" || kind === "verifydownload" || kind === "interceptmime" || kind === "quarantinedownload" || kind === "scanvirus") return "downloads";
788
+ if (kind === "readclipboard") return "clipboardRead";
789
+ if (kind === "writeclipboard" || kind === "copyscreen") return "clipboardWrite";
674
790
  if (kind === "openlink" || kind === "openprivate" || kind === "navlist" || kind === "batchopen" || kind === "reopentab" || kind === "deeplink") return "tabs";
675
791
  if (tabscommandactions.has(kind)) return "tabs";
676
792
  return void 0;
@@ -690,6 +806,9 @@ function isdatasetkind(kind) {
690
806
  function isexportkind(kind) {
691
807
  return exportactions.has(kind);
692
808
  }
809
+ function isfileskind(kind) {
810
+ return filesactions.has(kind);
811
+ }
693
812
  function exportgranted(session, origin) {
694
813
  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
814
  return { allowed: true };
@@ -874,6 +993,88 @@ function validatedatagrammar(step, options, origin) {
874
993
  if (kind === "logprovenance" && !isnonempty(options.artifact)) return { allowed: false, reason: "A reviewed artifact id or name is required in options." };
875
994
  return { allowed: true };
876
995
  }
996
+ function validatedownloadspec(value) {
997
+ if (!value || typeof value !== "object" || Array.isArray(value)) return { allowed: false, reason: "A reviewed downloadspec with a url list is required in options." };
998
+ const spec = value;
999
+ 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." };
1000
+ if (spec.filename !== void 0 && !isnonempty(spec.filename)) return { allowed: false, reason: "The reviewed downloadspec filename rule must be a non-empty string." };
1001
+ 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." };
1002
+ return { allowed: true };
1003
+ }
1004
+ function validatemimefilter(value) {
1005
+ if (!value || typeof value !== "object" || Array.isArray(value)) return { allowed: false, reason: "A reviewed mimefilter with include and exclude patterns is required in options." };
1006
+ const filter = value;
1007
+ 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." };
1008
+ 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." };
1009
+ if (filter.default !== "deny" && filter.default !== "allow") return { allowed: false, reason: "The reviewed mimefilter needs the deny or allow default for unlisted mime types." };
1010
+ return { allowed: true };
1011
+ }
1012
+ function validatecleanuprule(value) {
1013
+ 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." };
1014
+ const rule = value;
1015
+ 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." };
1016
+ if (!isnonempty(rule.kind)) return { allowed: false, reason: "The reviewed cleanup rule needs a non-empty artifact kind, or any to match every kind." };
1017
+ 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." };
1018
+ return { allowed: true };
1019
+ }
1020
+ function validatefilesgrammar(step, options) {
1021
+ const kind = step.kind;
1022
+ if (kind === "batchdownload") {
1023
+ const speccheck = validatedownloadspec(options.downloadspec);
1024
+ if (!speccheck.allowed) return speccheck;
1025
+ 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." };
1026
+ }
1027
+ if (kind === "pausedownload" || kind === "resumedownload" || kind === "verifydownload" || kind === "quarantinedownload" || kind === "scanvirus") {
1028
+ if (!isnonempty(step.value)) return { allowed: false, reason: "A reviewed download or quarantine reference is required." };
1029
+ if (kind === "verifydownload") {
1030
+ if (options.checksum !== void 0 && !isnonempty(options.checksum)) return { allowed: false, reason: "The reviewed expected checksum must be a non-empty string." };
1031
+ 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." };
1032
+ }
1033
+ if (kind === "scanvirus" && options.scanner !== void 0 && !isnonempty(options.scanner)) return { allowed: false, reason: "The reviewed scanner name must be a non-empty string." };
1034
+ if (kind === "quarantinedownload" && options.reason !== void 0 && !isnonempty(options.reason)) return { allowed: false, reason: "The reviewed quarantine reason must be a non-empty string." };
1035
+ }
1036
+ if (kind === "interceptmime") {
1037
+ const filtercheck = validatemimefilter(options.mimefilter);
1038
+ if (!filtercheck.allowed) return filtercheck;
1039
+ }
1040
+ if (kind === "readclipboard") {
1041
+ if (!isnonempty(options.consentref)) return { allowed: false, reason: "A clipboard read requires a reviewed consent ref of an approved consent prompt in options." };
1042
+ if (options.prompt !== void 0 && !isnonempty(options.prompt)) return { allowed: false, reason: "The reviewed clipboard consent prompt must be a non-empty string." };
1043
+ }
1044
+ 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." };
1045
+ if (kind === "namecaptures") {
1046
+ if (!isnonempty(options.task)) return { allowed: false, reason: "A reviewed task id is required in options for capture naming." };
1047
+ 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." };
1048
+ if (options.extension !== void 0 && !isnonempty(options.extension)) return { allowed: false, reason: "The reviewed capture extension must be a non-empty string." };
1049
+ }
1050
+ if (kind === "cleanupartifacts" && options.rules !== void 0) {
1051
+ const rules = options.rules;
1052
+ if (!Array.isArray(rules) || rules.length === 0) return { allowed: false, reason: "The reviewed cleanup rules must be a non-empty list when present." };
1053
+ for (const item of rules) {
1054
+ const rulecheck = validatecleanuprule(item);
1055
+ if (!rulecheck.allowed) return rulecheck;
1056
+ }
1057
+ }
1058
+ return { allowed: true };
1059
+ }
1060
+ function clipboardconsentgranted(step) {
1061
+ let options = {};
1062
+ try {
1063
+ options = parseoptions(step);
1064
+ } catch {
1065
+ options = {};
1066
+ }
1067
+ const consentref = options.consentref;
1068
+ if (typeof consentref !== "string" || !consentref.trim()) return { allowed: false, reason: "A clipboard read requires a reviewed consent ref in options." };
1069
+ return { allowed: true };
1070
+ }
1071
+ function quarantinereleasegranted(entry) {
1072
+ 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.` };
1073
+ return { allowed: true };
1074
+ }
1075
+ function maskclipboard(payload) {
1076
+ return `[clipboard payload of ${payload.length} character${payload.length === 1 ? "" : "s"}]`;
1077
+ }
877
1078
  function submitreviewgranted(steps, submitid) {
878
1079
  const position = steps.findIndex((candidate) => candidate.id === submitid);
879
1080
  const asked = steps.some((candidate, index) => candidate.kind === "asksubmit" && (position === -1 || index < position));
@@ -1400,6 +1601,10 @@ function validatestep(step, origin) {
1400
1601
  const datacheck = validatedatagrammar(step, options, origin);
1401
1602
  if (!datacheck.allowed) return datacheck;
1402
1603
  }
1604
+ if (isfileskind(step.kind)) {
1605
+ const filescheck = validatefilesgrammar(step, options);
1606
+ if (!filescheck.allowed) return filescheck;
1607
+ }
1403
1608
  if (step.kind === "tabcreate") {
1404
1609
  if (options.background !== void 0 && typeof options.background !== "boolean") return { allowed: false, reason: "The reviewed background flag must be a boolean." };
1405
1610
  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 +1659,11 @@ function canexecute(input) {
1454
1659
  const consentgate = passwordconsentgranted(input.step);
1455
1660
  if (!consentgate.allowed) return consentgate;
1456
1661
  }
1662
+ if (input.step.kind === "readclipboard") {
1663
+ const clipgate = clipboardconsentgranted(input.step);
1664
+ if (!clipgate.allowed) return clipgate;
1665
+ }
1666
+ if (input.step.kind === "interceptmime" && !origingranted(input.session, input.origin)) return { allowed: false, reason: "The download interception is outside the session origin grants." };
1457
1667
  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
1668
  let options = {};
1459
1669
  try {
@@ -1555,9 +1765,18 @@ function recordwizardstep(progress, planid, stepid, state, now) {
1555
1765
  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
1766
  return recordoutcome(base, planid, outcome, now);
1557
1767
  }
1768
+ function downloadshare(completed, total) {
1769
+ if (!Number.isFinite(total) || total <= 0) return 0;
1770
+ return Math.min(1, Math.max(0, completed) / total);
1771
+ }
1772
+ function recorddownload(progress, planid, stepid, entry, now) {
1773
+ const base = progress && progress.planid === planid ? progress : emptyprogress(planid, now);
1774
+ 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 };
1775
+ return recordoutcome(base, planid, outcome, now);
1776
+ }
1558
1777
 
1559
1778
  // version.ts
1560
- var packageversion = "1.1.38";
1779
+ var packageversion = "1.1.39";
1561
1780
 
1562
1781
  // types.ts
1563
1782
  var protocolversion = packageversion;
@@ -1674,6 +1893,15 @@ function extractionreport(input) {
1674
1893
  function provenancereport(input) {
1675
1894
  return { version: protocolversion, records: input.records };
1676
1895
  }
1896
+ function downloadreport(input) {
1897
+ return JSON.stringify({ version: protocolversion, planid: input.plan.id, planstate: input.plan.state, downloads: input.downloads });
1898
+ }
1899
+ function netlogreport(input) {
1900
+ return { version: protocolversion, records: input.records };
1901
+ }
1902
+ function quarantinereport(input) {
1903
+ return { version: protocolversion, entries: input.entries };
1904
+ }
1677
1905
 
1678
1906
  // extension/browsertabs.ts
1679
1907
  var browserkinds = /* @__PURE__ */ new Set(["tablist", "tabcreate", "tabactivate", "tabclose", "tabreload", "tabsnapshot", "windowlist", "windowcreate", "windowclose", "zoomset", "windowresize", "downloadfile"]);
@@ -2002,13 +2230,13 @@ function presshold(holds, hold) {
2002
2230
  return { holds: [...holds, hold], ok: true };
2003
2231
  }
2004
2232
  function releasehold(holds, holdid, releasedat) {
2005
- let released;
2233
+ let released2;
2006
2234
  const next = holds.map((hold) => {
2007
2235
  if (hold.holdid !== holdid || hold.releasedat !== void 0) return hold;
2008
- released = { ...hold, releasedat };
2009
- return released;
2236
+ released2 = { ...hold, releasedat };
2237
+ return released2;
2010
2238
  });
2011
- return { holds: next, ...released ? { released } : {} };
2239
+ return { holds: next, ...released2 ? { released: released2 } : {} };
2012
2240
  }
2013
2241
  function heldkeys(holds, tabid2) {
2014
2242
  return holds.filter((hold) => hold.releasedat === void 0 && (tabid2 === void 0 || hold.tabid === void 0 || hold.tabid === tabid2));
@@ -2691,6 +2919,153 @@ function mergetaskrules(existing, taskid, transforms, dedupekeys, at) {
2691
2919
  };
2692
2920
  }
2693
2921
 
2922
+ // extension/filescommand.ts
2923
+ var downloadtransitions = {
2924
+ queued: ["running", "complete", "failed"],
2925
+ running: ["paused", "complete", "failed"],
2926
+ paused: ["running", "failed"],
2927
+ complete: [],
2928
+ failed: []
2929
+ };
2930
+ function transitionallowed(from, to) {
2931
+ return downloadtransitions[from].includes(to);
2932
+ }
2933
+ function advancedownload(record2, state, at, evidence) {
2934
+ if (!transitionallowed(record2.state, state)) return record2;
2935
+ return {
2936
+ ...record2,
2937
+ state,
2938
+ ...evidence?.path !== void 0 ? { path: evidence.path } : record2.path !== void 0 ? { path: record2.path } : {},
2939
+ ...evidence?.bytes !== void 0 ? { bytes: evidence.bytes } : record2.bytes !== void 0 ? { bytes: record2.bytes } : {},
2940
+ ...evidence?.checksum !== void 0 ? { checksum: evidence.checksum } : record2.checksum !== void 0 ? { checksum: record2.checksum } : {},
2941
+ ...evidence?.downloadid !== void 0 ? { downloadid: evidence.downloadid } : record2.downloadid !== void 0 ? { downloadid: record2.downloadid } : {},
2942
+ updatedat: at
2943
+ };
2944
+ }
2945
+ function concurrentwindow(running, ceiling) {
2946
+ return ceiling === void 0 || running < ceiling;
2947
+ }
2948
+ function conflictfree(filename, taken) {
2949
+ if (!taken.includes(filename)) return filename;
2950
+ const dot = filename.lastIndexOf(".");
2951
+ const base = dot > 0 ? filename.slice(0, dot) : filename;
2952
+ const extension = dot > 0 ? filename.slice(dot) : "";
2953
+ let sequence = 2;
2954
+ while (taken.includes(`${base}-${sequence}${extension}`)) sequence += 1;
2955
+ return `${base}-${sequence}${extension}`;
2956
+ }
2957
+ function downloadfilename(url, rule) {
2958
+ if (rule && rule.trim()) return rule.trim();
2959
+ let name = "";
2960
+ try {
2961
+ const parsed = new URL(url);
2962
+ name = decodeURIComponent(parsed.pathname.split("/").filter(Boolean).pop() ?? parsed.hostname);
2963
+ } catch {
2964
+ name = url;
2965
+ }
2966
+ return name || "download";
2967
+ }
2968
+ function verifybytes(record2, expected) {
2969
+ const statematch = record2.state === "complete";
2970
+ const sizematch = expected.bytes === void 0 ? true : record2.bytes === expected.bytes;
2971
+ const checksummatch = expected.checksum === void 0 ? true : record2.checksum === expected.checksum;
2972
+ const ok = statematch && sizematch && checksummatch;
2973
+ const parts = [`state ${record2.state}${statematch ? " matches" : " does not match the completed expectation"}`];
2974
+ if (expected.bytes !== void 0) parts.push(`size ${record2.bytes ?? "unknown"} of ${expected.bytes} bytes ${sizematch ? "matches" : "differs"}`);
2975
+ if (expected.checksum !== void 0) parts.push(`checksum ${record2.checksum ?? "unknown"} ${checksummatch ? "matches" : "differs from"} the reviewed ${expected.checksum}`);
2976
+ return { ok, summary: `${ok ? "Verified" : "Failed to verify"} the download of ${record2.filename}: ${parts.join("; ")}.`, matches: { state: statematch, size: sizematch, checksum: checksummatch } };
2977
+ }
2978
+ function mimepatternmatches(pattern, mime) {
2979
+ if (!pattern.endsWith("*")) return pattern === mime;
2980
+ return mime.startsWith(pattern.slice(0, -1));
2981
+ }
2982
+ function mimeallowed(filter, mime) {
2983
+ if (filter.exclude.some((pattern) => mimepatternmatches(pattern, mime))) return false;
2984
+ if (filter.include.some((pattern) => mimepatternmatches(pattern, mime))) return true;
2985
+ return filter.default === "allow";
2986
+ }
2987
+ function redactheaders(headers) {
2988
+ return Object.fromEntries(Object.entries(headers).map(([name]) => [name, "[redacted]"]));
2989
+ }
2990
+ function netlogentry(input) {
2991
+ return { url: input.url, method: input.method, status: input.status, timing: input.timing, requestid: input.requestid, stepid: input.stepid, at: input.at };
2992
+ }
2993
+ function netlogforstep(records, stepid) {
2994
+ return records.filter((record2) => record2.stepid === stepid);
2995
+ }
2996
+ function clipentryof(kind, payload, origin, stepid, at) {
2997
+ return { kind, hash: payload.hash, length: payload.length, origin, stepid, at };
2998
+ }
2999
+ function cliphash(payload) {
3000
+ return checksum(payload);
3001
+ }
3002
+ function quarantinedpath(filename) {
3003
+ return `devthink-quarantine/${filename.replace(/^\/+/, "")}`;
3004
+ }
3005
+ function newquarantine(id, filename, reason, at) {
3006
+ const path = quarantinedpath(filename);
3007
+ return { id, path, reason, scan: "pending", at, updatedat: at };
3008
+ }
3009
+ function scanresult(entry, verdict, at) {
3010
+ return { ...entry, scan: verdict, updatedat: at };
3011
+ }
3012
+ function scanverdictof(response) {
3013
+ if (!response || typeof response !== "object") return "pending";
3014
+ const verdict = response.verdict;
3015
+ if (verdict === "clean" || verdict === "flagged" || verdict === "error") return verdict;
3016
+ return "pending";
3017
+ }
3018
+ function released(entry, ref, at) {
3019
+ return { ...entry, release: ref, updatedat: at };
3020
+ }
3021
+ function capturepart(value) {
3022
+ return value.replace(/[^a-z0-9-]+/gi, "-").replace(/^-+|-+$/g, "").toLowerCase() || "capture";
3023
+ }
3024
+ function capturefilename(name, extension) {
3025
+ const safeextension = extension.replace(/^\.+/, "").toLowerCase() || "png";
3026
+ return `${capturepart(name.task)}-${capturepart(name.step)}-${name.sequence}.${safeextension}`;
3027
+ }
3028
+ function advancecounter(counters, base) {
3029
+ const sequence = (counters[base] ?? 0) + 1;
3030
+ return { sequence, counters: { ...counters, [base]: sequence } };
3031
+ }
3032
+ function capturenames(counters, task, steps, extension) {
3033
+ let current = { ...counters };
3034
+ const names = steps.map((step) => {
3035
+ const advanced = advancecounter(current, step);
3036
+ current = advanced.counters;
3037
+ return capturefilename({ task, step, sequence: advanced.sequence }, extension);
3038
+ });
3039
+ return { names, counters: current };
3040
+ }
3041
+ function referencedartifacts(plan, completed) {
3042
+ if (!plan) return [];
3043
+ return plan.steps.filter((step) => step.kind === "attachfile" && !completed.includes(step.id)).map((step) => step.value ?? "").filter((value) => value.trim().length > 0);
3044
+ }
3045
+ function sweepplan(entries, rules, now, keeprefs) {
3046
+ const remove = /* @__PURE__ */ new Set();
3047
+ for (const rule of rules) {
3048
+ const matching = entries.filter((entry) => rule.kind === "any" || entry.kind === rule.kind);
3049
+ const aged = matching.filter((entry) => now - entry.at >= rule.age);
3050
+ const kept = [];
3051
+ if (rule.keep === "all") kept.push(...aged);
3052
+ else if (rule.keep === "latest") {
3053
+ const newest = [...aged].sort((left, right) => right.at - left.at)[0];
3054
+ if (newest) kept.push(newest);
3055
+ }
3056
+ for (const entry of aged) {
3057
+ if (kept.some((item) => item.id === entry.id)) continue;
3058
+ if (keeprefs.includes(entry.id) || keeprefs.includes(entry.name)) continue;
3059
+ remove.add(entry.id);
3060
+ }
3061
+ }
3062
+ return { remove: [...remove], keep: entries.filter((entry) => !remove.has(entry.id)).map((entry) => entry.id) };
3063
+ }
3064
+ function capturesteps(options, plan) {
3065
+ const listed = Array.isArray(options.steps) ? options.steps.filter((item) => typeof item === "string" && item.trim().length > 0) : [];
3066
+ return listed.length > 0 ? listed : plan.steps.map((step) => step.id);
3067
+ }
3068
+
2694
3069
  // extension/background.ts
2695
3070
  var sessionduration = 15 * 60 * 1e3;
2696
3071
  var freshcheckkinds = /* @__PURE__ */ new Set(["focus", "inspect", "click", "type", "scroll", "select", "hover"]);
@@ -2881,6 +3256,14 @@ function stepauditkind(step, ok) {
2881
3256
  if (step.kind === "submitform" || step.kind === "asksubmit") return "submit";
2882
3257
  if (step.kind === "consentpassword") return "consent";
2883
3258
  if (step.kind === "handoffcaptcha") return "handoff";
3259
+ if (isfileskind(step.kind)) {
3260
+ if (step.kind === "interceptmime") return "intercept";
3261
+ if (step.kind === "readclipboard" || step.kind === "writeclipboard" || step.kind === "copyscreen") return "clipboard";
3262
+ if (step.kind === "quarantinedownload" || step.kind === "scanvirus") return "quarantine";
3263
+ if (step.kind === "cleanupartifacts") return "cleanup";
3264
+ if (step.kind === "verifydownload" || step.kind === "exportnetlog" || step.kind === "namecaptures") return "observation";
3265
+ return "download";
3266
+ }
2884
3267
  if (isdatasetkind(step.kind)) {
2885
3268
  if (step.kind === "exportcsv" || step.kind === "exportjson" || step.kind === "exportexcel" || step.kind === "copytable" || step.kind === "pushsheets") return "export";
2886
3269
  if (step.kind === "streamdisk") return "stream";
@@ -3190,8 +3573,18 @@ async function recordnavigation(step, session, tabid2) {
3190
3573
  await memory.addnavrecord(record2);
3191
3574
  await memory.setnavstate(tabid2, record2);
3192
3575
  if (session && url) await memory.addtrailentry(session.id, { url, title, stepid: step.id, at: Date.now() });
3576
+ await collectnetlog(tabid2, step);
3193
3577
  return record2;
3194
3578
  }
3579
+ async function collectnetlog(tabid2, step) {
3580
+ const events = navbuffers.get(tabid2) ?? [];
3581
+ if (events.length === 0) return;
3582
+ const first = events[0]?.timestamp ?? Date.now();
3583
+ for (const [index, event] of events.entries()) {
3584
+ const status = event.status ?? (event.event === "completed" ? 200 : event.event === "error" ? 0 : 0);
3585
+ 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 }));
3586
+ }
3587
+ }
3195
3588
  function injectallowedorigins(step, session) {
3196
3589
  const allowedorigins = session?.grants ?? (session ? [session.origin] : []);
3197
3590
  let options = {};
@@ -4284,6 +4677,270 @@ async function executedatastep(step, session, plan, tabid2, origin) {
4284
4677
  return { ok: false, summary: "Unsupported forms and data step." };
4285
4678
  }
4286
4679
  }
4680
+ async function loadownload(reference) {
4681
+ const records = await memory.getdownloads();
4682
+ return records.find((item) => item.id === reference || item.filename === reference || item.url === reference);
4683
+ }
4684
+ async function settlesdownload(record2) {
4685
+ const started = Date.now();
4686
+ for (; ; ) {
4687
+ const items = record2.downloadid === void 0 ? [] : await chrome.downloads.search({ id: record2.downloadid }).catch(() => []);
4688
+ const item = items[0];
4689
+ if (item?.state === "complete") return { state: "complete", evidence: { path: item.filename, bytes: item.fileSize ?? item.totalBytes ?? 0 } };
4690
+ if (item?.state === "interrupted") return { state: "failed" };
4691
+ if (Date.now() - started >= evidencesettle) return { state: "running" };
4692
+ await new Promise((resolve) => setTimeout(resolve, evidencepoll));
4693
+ }
4694
+ }
4695
+ async function pauseonerecord(record2, extra) {
4696
+ if (!transitionallowed(record2.state, "paused")) throw new Error(`A ${record2.state} download cannot pause.`);
4697
+ if (record2.downloadid !== void 0) await chrome.downloads.pause(record2.downloadid).catch(() => void 0);
4698
+ const paused = advancedownload(record2, "paused", Date.now());
4699
+ await memory.setdownload(paused);
4700
+ await audit("download", `Paused the download of ${record2.filename} from ${record2.url}.`, extra);
4701
+ return { ok: true, summary: `Paused the download of ${record2.filename}.`, details: { download: paused } };
4702
+ }
4703
+ async function resumeonerecord(record2, extra) {
4704
+ if (!transitionallowed(record2.state, "running")) throw new Error(`A ${record2.state} download cannot resume.`);
4705
+ if (record2.downloadid !== void 0) await chrome.downloads.resume(record2.downloadid).catch(() => void 0);
4706
+ const resumed = advancedownload(record2, "running", Date.now());
4707
+ await memory.setdownload(resumed);
4708
+ await audit("download", `Resumed the paused download of ${record2.filename} from ${record2.url}.`, extra);
4709
+ return { ok: true, summary: `Resumed the download of ${record2.filename}.`, details: { download: resumed } };
4710
+ }
4711
+ async function verifyonerecord(record2, expected, extra) {
4712
+ const verification = verifybytes(record2, expected);
4713
+ await audit("observation", `Verified the download of ${record2.filename}: ${verification.summary}`, extra);
4714
+ return { ok: verification.ok, summary: verification.summary, details: { verification: { ...verification.matches, state: record2.state, path: record2.path, checksum: record2.checksum, bytes: record2.bytes } } };
4715
+ }
4716
+ async function executefilesstep(step, session, plan, tabid2, origin) {
4717
+ const options = stepoptions2(step);
4718
+ const extra = { ...session ? { sessionid: session.id } : {}, planid: plan.id, stepid: step.id };
4719
+ switch (step.kind) {
4720
+ case "batchdownload": {
4721
+ const spec = options.downloadspec;
4722
+ const urls = Array.isArray(spec?.urls) ? (spec?.urls).filter((item) => typeof item === "string" && item.trim().length > 0) : [];
4723
+ const concurrent = typeof options.concurrent === "number" && Number.isInteger(options.concurrent) && options.concurrent > 0 ? options.concurrent : void 0;
4724
+ const taken = (await memory.getdownloads()).map((record2) => record2.filename);
4725
+ const records = urls.map((url) => {
4726
+ const filename = conflictfree(downloadfilename(url, spec?.filename), taken);
4727
+ taken.push(filename);
4728
+ return { id: randomid(), url, filename, state: "queued", at: Date.now(), updatedat: Date.now() };
4729
+ });
4730
+ let completed = 0;
4731
+ let failed = 0;
4732
+ let queued = 0;
4733
+ let index = 0;
4734
+ while (index < records.length) {
4735
+ const wave = [];
4736
+ while (index < records.length && concurrentwindow(wave.length, concurrent)) {
4737
+ const record2 = records[index];
4738
+ index += 1;
4739
+ const downloadid = await chrome.downloads.download({ url: record2.url, filename: record2.filename }).catch(() => void 0);
4740
+ const started = downloadid === void 0 ? advancedownload(record2, "failed", Date.now()) : advancedownload(record2, "running", Date.now(), { downloadid });
4741
+ await memory.setdownload(started);
4742
+ wave.push(started);
4743
+ }
4744
+ for (const started of wave) {
4745
+ if (started.state === "failed") {
4746
+ failed += 1;
4747
+ continue;
4748
+ }
4749
+ const settled = await settlesdownload(started);
4750
+ const final = advancedownload(started, settled.state, Date.now(), settled.evidence);
4751
+ await memory.setdownload(final);
4752
+ if (final.state === "complete") completed += 1;
4753
+ else if (final.state === "failed") failed += 1;
4754
+ else queued += 1;
4755
+ await memory.setprogress(recorddownload(await memory.getprogress(), plan.id, step.id, { index: records.indexOf(started), url: started.url, state: final.state }, Date.now()));
4756
+ }
4757
+ }
4758
+ await refreshbadge();
4759
+ 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);
4760
+ 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 } };
4761
+ }
4762
+ case "pausedownload": {
4763
+ const record2 = await loadownload(step.value ?? "");
4764
+ if (!record2) throw new Error(`No stored download matches ${step.value ?? ""}.`);
4765
+ return pauseonerecord(record2, extra);
4766
+ }
4767
+ case "resumedownload": {
4768
+ const record2 = await loadownload(step.value ?? "");
4769
+ if (!record2) throw new Error(`No stored download matches ${step.value ?? ""}.`);
4770
+ return resumeonerecord(record2, extra);
4771
+ }
4772
+ case "verifydownload": {
4773
+ const record2 = await loadownload(step.value ?? "");
4774
+ if (!record2) throw new Error(`No stored download matches ${step.value ?? ""}.`);
4775
+ return verifyonerecord(record2, { ...typeof options.bytes === "number" ? { bytes: options.bytes } : {}, ...typeof options.checksum === "string" ? { checksum: options.checksum } : {} }, extra);
4776
+ }
4777
+ case "interceptmime": {
4778
+ const filter = options.mimefilter;
4779
+ if (!filter) throw new Error("A reviewed mimefilter is required in options.");
4780
+ const filters = await memory.getmimefilters();
4781
+ await memory.setmimefilters([filter, ...filters]);
4782
+ armedmimefilter = filter;
4783
+ installmimelistener();
4784
+ 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);
4785
+ return { ok: true, summary: `Armed the mime interception filter with the ${filter.default} default for unlisted mime types.`, details: { mimefilter: filter } };
4786
+ }
4787
+ case "exportnetlog": {
4788
+ const records = await memory.getnetlog();
4789
+ const stepfilter = typeof options.stepid === "string" && options.stepid ? options.stepid : void 0;
4790
+ const filtered = (stepfilter ? netlogforstep(records, stepfilter) : records).map((record2) => ({ ...record2, headers: redactheaders(record2.headers ?? {}) }));
4791
+ 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);
4792
+ 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" } };
4793
+ }
4794
+ case "readclipboard": {
4795
+ const consentref = typeof options.consentref === "string" ? options.consentref : "";
4796
+ const consents = await memory.getclipconsents();
4797
+ const consent = consents.find((item) => item.id === consentref && item.approved === true && item.usedat === void 0);
4798
+ if (!consent) {
4799
+ const pending = { id: consentref || randomid(), prompt: typeof options.prompt === "string" && options.prompt ? options.prompt : step.summary, origin, stepid: step.id, at: Date.now() };
4800
+ await memory.setclipconsent(pending);
4801
+ await refreshbadge();
4802
+ 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);
4803
+ 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 } } };
4804
+ }
4805
+ const text2 = await navigator.clipboard.readText();
4806
+ const entry = clipentryof("read", { hash: cliphash(text2), length: text2.length }, origin, step.id, Date.now());
4807
+ await memory.addclip(entry);
4808
+ await memory.setclipconsent({ ...consent, usedat: Date.now() });
4809
+ await refreshbadge();
4810
+ 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);
4811
+ 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) } };
4812
+ }
4813
+ case "writeclipboard": {
4814
+ const output = await dispatchpagestep(step, tabid2, origin, plan);
4815
+ const hash = typeof output?.details?.hash === "string" ? output.details.hash : cliphash(step.value ?? "");
4816
+ const length = typeof output?.details?.length === "number" ? output.details.length : (step.value ?? "").length;
4817
+ const entry = clipentryof("write", { hash, length }, origin, step.id, Date.now());
4818
+ await memory.addclip(entry);
4819
+ 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);
4820
+ return { ok: Boolean(output?.ok), summary: output?.summary ?? "The clipboard write returned no result.", details: { ...output?.details ?? {}, clip: entry } };
4821
+ }
4822
+ case "copyscreen": {
4823
+ const windowid = chrome.windows.WINDOW_ID_CURRENT;
4824
+ const shot = await chrome.tabs.captureVisibleTab(windowid, { format: "png" });
4825
+ let destination = "clipboard";
4826
+ try {
4827
+ const blob = await (await fetch(shot)).blob();
4828
+ await navigator.clipboard.write([new ClipboardItem({ "image/png": blob })]);
4829
+ } catch {
4830
+ destination = "clipboard unavailable";
4831
+ }
4832
+ const entry = clipentryof("screen", { hash: cliphash(shot), length: shot.length }, origin, step.id, Date.now());
4833
+ await memory.addclip(entry);
4834
+ 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);
4835
+ return { ok: destination === "clipboard", summary: `Copied the visible tab screenshot with payload hash ${entry.hash} to the clipboard.`, details: { clip: entry, destination } };
4836
+ }
4837
+ case "quarantinedownload": {
4838
+ const record2 = await loadownload(step.value ?? "");
4839
+ if (!record2) throw new Error(`No stored download matches ${step.value ?? ""}.`);
4840
+ const reason = typeof options.reason === "string" && options.reason ? options.reason : `moved from ${record2.url} by the reviewed quarantine step`;
4841
+ const entry = newquarantine(randomid(), record2.path ?? record2.filename, reason, Date.now());
4842
+ await memory.setquarantine(entry);
4843
+ await refreshbadge();
4844
+ 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);
4845
+ return { ok: true, summary: `Quarantined ${record2.filename} at ${entry.path} with a pending scan verdict.`, details: { quarantine: entry } };
4846
+ }
4847
+ case "scanvirus": {
4848
+ const reference = step.value ?? "";
4849
+ const entry = (await memory.getquarantines()).find((item) => item.id === reference || item.path === reference);
4850
+ if (!entry) throw new Error(`No quarantined file matches ${reference}.`);
4851
+ const scanner = typeof options.scanner === "string" && options.scanner ? options.scanner : void 0;
4852
+ const hooks = await memory.getscanhooks();
4853
+ const hook = scanner ? hooks.find((item) => item.scanner === scanner) : hooks[0];
4854
+ let verdict = "pending";
4855
+ if (hook) {
4856
+ const granted = await chrome.permissions.contains({ origins: [hostpattern(hook.origin)] }).catch(() => false);
4857
+ if (granted) {
4858
+ try {
4859
+ 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 }) });
4860
+ verdict = scanverdictof(await response.json().catch(() => void 0));
4861
+ } catch {
4862
+ verdict = "pending";
4863
+ }
4864
+ }
4865
+ }
4866
+ const scanned = scanresult(entry, verdict, Date.now());
4867
+ await memory.setquarantine(scanned);
4868
+ await refreshbadge();
4869
+ 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);
4870
+ return { ok: true, summary: `Scan verdict ${verdict} recorded for ${entry.path}.`, details: { quarantine: scanned, verdict } };
4871
+ }
4872
+ case "namecaptures": {
4873
+ const task = typeof options.task === "string" && options.task ? options.task : plan.id;
4874
+ const steps = capturesteps(options, plan);
4875
+ const extension = typeof options.extension === "string" && options.extension ? options.extension : "png";
4876
+ const existing = (await memory.getcapturecounters()).find((item) => item.taskid === task);
4877
+ const stamped = capturenames(existing?.counters ?? {}, task, steps, extension);
4878
+ await memory.setcapturecounter({ taskid: task, counters: stamped.counters, at: Date.now() });
4879
+ 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);
4880
+ 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 } };
4881
+ }
4882
+ case "cleanupartifacts": {
4883
+ const inlinrules = (Array.isArray(options.rules) ? options.rules : []).filter((item) => Boolean(item) && typeof item === "object" && !Array.isArray(item));
4884
+ const rules = inlinrules.length > 0 ? inlinrules : await memory.getcleanuprules();
4885
+ 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.");
4886
+ if (inlinrules.length > 0) await memory.setcleanuprules(inlinrules);
4887
+ const exportrecords = await memory.getexports();
4888
+ const artifacts = await memory.getartifacts();
4889
+ const inventory = [
4890
+ ...exportrecords.map((artifact) => ({ id: artifact.id, kind: `export-${artifact.kind}`, name: artifact.name, size: artifact.content.length, at: artifact.at })),
4891
+ ...artifacts.map((artifact) => ({ id: artifact.id, kind: artifact.kind, name: artifact.name, size: 0, at: artifact.at }))
4892
+ ];
4893
+ await memory.setinventory(inventory);
4894
+ const progress = await memory.getprogress();
4895
+ const keeprefs = referencedartifacts(plan, progress?.planid === plan.id ? progress.completedsteps : []);
4896
+ const sweep = sweepplan(inventory, rules, Date.now(), keeprefs);
4897
+ let removed = 0;
4898
+ for (const id of sweep.remove) {
4899
+ if (await memory.removeexport(id)) removed += 1;
4900
+ else if (await memory.removeartifact(id)) removed += 1;
4901
+ }
4902
+ const run = { id: randomid(), rules: rules.length, removed, kept: sweep.keep.length, at: Date.now() };
4903
+ await memory.addcleanuprun(run);
4904
+ await refreshbadge();
4905
+ 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);
4906
+ 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 } };
4907
+ }
4908
+ default:
4909
+ return { ok: false, summary: "Unsupported files, clipboard and downloads step." };
4910
+ }
4911
+ }
4912
+ var armedmimefilter;
4913
+ var mimelistenerinstalled = false;
4914
+ function installmimelistener() {
4915
+ if (mimelistenerinstalled || typeof chrome.downloads?.onDeterminingFilename?.addListener !== "function") return;
4916
+ mimelistenerinstalled = true;
4917
+ chrome.downloads.onDeterminingFilename.addListener((item, suggest) => {
4918
+ const filter = armedmimefilter;
4919
+ if (!filter || item.byExtensionId === chrome.runtime.id) {
4920
+ suggest({ filename: item.filename, conflictAction: "uniquify" });
4921
+ return;
4922
+ }
4923
+ const mime = item.mime ?? "";
4924
+ if (mimeallowed(filter, mime)) {
4925
+ suggest({ filename: `devthink-quarantine/${item.filename}`, conflictAction: "uniquify" });
4926
+ 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);
4927
+ return;
4928
+ }
4929
+ if (filter.default === "deny") {
4930
+ void chrome.downloads.cancel(item.id).catch(() => void 0);
4931
+ 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);
4932
+ }
4933
+ suggest({ filename: item.filename, conflictAction: "uniquify" });
4934
+ });
4935
+ }
4936
+ installmimelistener();
4937
+ async function reconcilmimefilter() {
4938
+ const filters = await memory.getmimefilters();
4939
+ armedmimefilter = filters[0];
4940
+ installmimelistener();
4941
+ }
4942
+ reconcilmimefilter().catch(() => {
4943
+ });
4287
4944
  async function enforcewindowreview(step, session, plan) {
4288
4945
  const windowid = step.value && /^\d+$/.test(step.value) ? Number.parseInt(step.value, 10) : 0;
4289
4946
  const progress = plan ? await memory.getprogress() : void 0;
@@ -4319,9 +4976,11 @@ async function refreshbadge() {
4319
4976
  const queues = await memory.getnavqueues();
4320
4977
  const badges = await memory.getbadges();
4321
4978
  const prompts = (await memory.gettickets()).filter((ticket) => ticket.approved === void 0).length;
4979
+ const consents = (await memory.getclipconsents()).filter((record2) => record2.approved === void 0).length;
4980
+ const quarantined = (await memory.getquarantines()).filter((entry) => entry.scan === "pending").length;
4322
4981
  const datasets = (await memory.getdatasets()).length;
4323
4982
  const tasktabs2 = new Set(badges.map((badge) => badge.tabid)).size;
4324
- const total = (queues?.prefetch ?? 0) + (queues?.batchopen ?? 0) + tasktabs2 + prompts + datasets;
4983
+ const total = (queues?.prefetch ?? 0) + (queues?.batchopen ?? 0) + tasktabs2 + prompts + consents + quarantined + datasets;
4325
4984
  await chrome.action.setBadgeText({ text: total > 0 ? String(total) : "" }).catch(() => {
4326
4985
  });
4327
4986
  }
@@ -4347,6 +5006,8 @@ async function executestep(stepid) {
4347
5006
  output = await executetabscommand(step, session, plan, tab.id);
4348
5007
  } else if (isdatasetkind(step.kind)) {
4349
5008
  output = await executedatastep(step, session, plan, tab.id, origin);
5009
+ } else if (isfileskind(step.kind)) {
5010
+ output = await executefilesstep(step, session, plan, tab.id, origin);
4350
5011
  } else if (isformkind(step.kind)) {
4351
5012
  output = await executeformstep(step, session, plan, tab.id, origin);
4352
5013
  } else if (isbrowserkind(step.kind)) {
@@ -4451,6 +5112,7 @@ async function grantcapability(permission) {
4451
5112
  if (!["tabs", "downloads", "clipboardRead", "clipboardWrite"].includes(permission)) throw new Error("Unknown capability.");
4452
5113
  const granted = await chrome.permissions.request({ permissions: [permission] });
4453
5114
  if (!granted) throw new Error("The capability grant was declined.");
5115
+ if (permission === "downloads") installmimelistener();
4454
5116
  await audit("capability", `Capability ${permission} granted by the user.`);
4455
5117
  return refreshcapabilities();
4456
5118
  }
@@ -4524,13 +5186,27 @@ async function handlerequest(message, sender) {
4524
5186
  for (const config of sheetendpoints) {
4525
5187
  sheetgrants.push({ ...config, granted: await chrome.permissions.contains({ origins: [hostpattern(config.origin)] }).catch(() => false) });
4526
5188
  }
5189
+ const downloads = await memory.getdownloads();
5190
+ const netlogs = await memory.getnetlog();
5191
+ const clipconsents = await memory.getclipconsents();
5192
+ const clips = await memory.getclips();
5193
+ const quarantines = await memory.getquarantines();
5194
+ const cleanuprules = await memory.getcleanuprules();
5195
+ const cleanupruns = await memory.getcleanupruns();
5196
+ const capturecounters = await memory.getcapturecounters();
5197
+ const inventory = await memory.getinventory();
5198
+ const mimefilters = await memory.getmimefilters();
5199
+ const scanhooks = [];
5200
+ for (const hook of await memory.getscanhooks()) {
5201
+ scanhooks.push({ ...hook, granted: await chrome.permissions.contains({ origins: [hostpattern(hook.origin)] }).catch(() => false) });
5202
+ }
4527
5203
  const clones = clonetabs(tabs);
4528
5204
  const taskgauge = tasktabgauge(tabs.filter((tab) => badges.some((badge) => badge.tabid === tab.tabid)).length, tasktabceiling(await memory.getsettings()));
4529
5205
  const report = await buildtabreport(tabs);
4530
5206
  const livetab = session ? await chrome.tabs.get(session.tabid).catch(() => void 0) : void 0;
4531
5207
  const waitprofile = session ? waitprofiles.find((record2) => record2.origin === session.origin) : void 0;
4532
5208
  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 };
5209
+ 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 };
4534
5210
  }
4535
5211
  case "capabilities":
4536
5212
  return refreshcapabilities();
@@ -4861,6 +5537,81 @@ async function handlerequest(message, sender) {
4861
5537
  return extractionreportValue();
4862
5538
  case "provenance":
4863
5539
  return provenancereportValue();
5540
+ case "downloadreport": {
5541
+ const plan = await memory.getplan();
5542
+ if (!plan) throw new Error("No plan is available for a download report envelope.");
5543
+ return JSON.parse(downloadreport({ downloads: await memory.getdownloads(), plan }));
5544
+ }
5545
+ case "quarantine":
5546
+ return quarantinereport({ entries: await memory.getquarantines() });
5547
+ case "netlog":
5548
+ return netlogreport({ records: await memory.getnetlog() });
5549
+ case "downloadaction": {
5550
+ const inputdownload = message;
5551
+ const session = await memory.getsession();
5552
+ if (!session || session.stoppedat || session.expiresat <= Date.now()) throw new Error("Download actions stay behind the consent gate of an active session.");
5553
+ const record2 = await loadownload(inputdownload.id ?? "");
5554
+ if (!record2) throw new Error(`No stored download matches ${inputdownload.id ?? ""}.`);
5555
+ const extra = { sessionid: session.id };
5556
+ if (inputdownload.action === "pause") return pauseonerecord(record2, extra);
5557
+ if (inputdownload.action === "resume") return resumeonerecord(record2, extra);
5558
+ if (inputdownload.action === "verify") return verifyonerecord(record2, {}, extra);
5559
+ throw new Error("A pause, resume or verify action is required.");
5560
+ }
5561
+ case "approveclipconsent": {
5562
+ const inputconsent = message;
5563
+ const record2 = (await memory.getclipconsents()).find((item) => item.id === inputconsent.id);
5564
+ if (!record2) throw new Error("No clipboard consent prompt matches the requested id.");
5565
+ await memory.setclipconsent({ ...record2, approved: inputconsent.approved !== false });
5566
+ const session = await memory.getsession();
5567
+ 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 } : {} });
5568
+ await refreshbadge();
5569
+ return { id: record2.id, approved: inputconsent.approved !== false };
5570
+ }
5571
+ case "releasequarantine": {
5572
+ const inputquarantine = message;
5573
+ const entry = (await memory.getquarantines()).find((item) => item.id === inputquarantine.id);
5574
+ if (!entry) throw new Error("No quarantined file matches the requested id.");
5575
+ if (entry.release !== void 0) throw new Error(`The quarantined file ${entry.path} was already released.`);
5576
+ const gate = quarantinereleasegranted(entry);
5577
+ if (!gate.allowed) throw new Error(gate.reason);
5578
+ const releasedentry = released(entry, `user-${Date.now()}`, Date.now());
5579
+ await memory.setquarantine(releasedentry);
5580
+ const session = await memory.getsession();
5581
+ 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 } : {} });
5582
+ await refreshbadge();
5583
+ return releasedentry;
5584
+ }
5585
+ case "setcleanuprules": {
5586
+ const inputrules = message;
5587
+ const rules = (Array.isArray(inputrules.rules) ? inputrules.rules : []).filter((item) => Boolean(item) && typeof item === "object" && !Array.isArray(item));
5588
+ for (const rule of rules) {
5589
+ const gate = validatecleanuprule(rule);
5590
+ if (!gate.allowed) throw new Error(gate.reason);
5591
+ }
5592
+ await memory.setcleanuprules(rules);
5593
+ const session = await memory.getsession();
5594
+ 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 } : {} });
5595
+ return { rules: rules.length };
5596
+ }
5597
+ case "configurescanhook": {
5598
+ const inputhook = message;
5599
+ const config = normalizeendpoint(inputhook.endpoint ?? "");
5600
+ const granted = await chrome.permissions.contains({ origins: [hostpattern(config.origin)] });
5601
+ if (!granted) throw new Error("The scan hook origin has not received optional permission.");
5602
+ if (!inputhook.scanner?.trim()) throw new Error("A scanner name is required for the scan hook.");
5603
+ const hook = { scanner: inputhook.scanner.trim(), endpoint: config.endpoint, origin: config.origin, configuredat: Date.now() };
5604
+ await memory.setscanhook(hook);
5605
+ 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.`, {});
5606
+ return hook;
5607
+ }
5608
+ case "exportnetlog": {
5609
+ const session = await memory.getsession();
5610
+ if (!session || session.stoppedat || session.expiresat <= Date.now()) throw new Error("Netlog exports stay behind the consent gate of an active session.");
5611
+ const records = (await memory.getnetlog()).map((record2) => ({ ...record2, headers: redactheaders(record2.headers ?? {}) }));
5612
+ 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 });
5613
+ return { records, redacted: true, redaction: "every header value is redacted from exported netlogs" };
5614
+ }
4864
5615
  case "stop": {
4865
5616
  const session = await memory.getsession();
4866
5617
  if (session) await memory.setsession({ ...session, stoppedat: Date.now() });