@wenathlan/extension 1.1.37 → 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.
@@ -520,21 +520,235 @@ var sessionmemory = class {
520
520
  async getcodevalue() {
521
521
  return this.adapter.get("codevalue");
522
522
  }
523
+ /** Stores one dataset with its column specs and rows, replacing the previous record of that id. */
524
+ async setdataset(value) {
525
+ await this.adapter.set(`dataset${value.id}`, value);
526
+ const ids = (await this.adapter.get("datasets") ?? []).filter((id) => id !== value.id);
527
+ await this.adapter.set("datasets", [value.id, ...ids]);
528
+ }
529
+ /** Returns one stored dataset by its id with its column specs and row count. */
530
+ async getdataset(id) {
531
+ return this.adapter.get(`dataset${id}`);
532
+ }
533
+ /** Returns every stored dataset id, newest first. */
534
+ async getdatasets() {
535
+ const ids = await this.adapter.get("datasets") ?? [];
536
+ const records = [];
537
+ for (const id of ids) {
538
+ const record2 = await this.adapter.get(`dataset${id}`);
539
+ if (record2) records.push(record2);
540
+ }
541
+ return records;
542
+ }
543
+ /** Stores one imported csv dataset for fill loops beside the dataset store. */
544
+ async addimport(value) {
545
+ await this.setdataset(value);
546
+ const ids = (await this.adapter.get("imports") ?? []).filter((id) => id !== value.id);
547
+ await this.adapter.set("imports", [value.id, ...ids]);
548
+ }
549
+ /** Returns every imported csv dataset for fill loops, newest first. */
550
+ async getimports() {
551
+ const ids = await this.adapter.get("imports") ?? [];
552
+ const records = [];
553
+ for (const id of ids) {
554
+ const record2 = await this.adapter.get(`dataset${id}`);
555
+ if (record2) records.push(record2);
556
+ }
557
+ return records;
558
+ }
559
+ /** Stores one extraction session with its cursor and page history, replacing the previous session of that id. */
560
+ async setextractsession(value) {
561
+ await this.adapter.set(`extract${value.id}`, value);
562
+ const ids = (await this.adapter.get("extracts") ?? []).filter((id) => id !== value.id);
563
+ await this.adapter.set("extracts", [value.id, ...ids]);
564
+ }
565
+ /** Returns every extraction session with its cursor and page history, newest first. */
566
+ async getextractsessions() {
567
+ const ids = await this.adapter.get("extracts") ?? [];
568
+ const records = [];
569
+ for (const id of ids) {
570
+ const record2 = await this.adapter.get(`extract${id}`);
571
+ if (record2) records.push(record2);
572
+ }
573
+ return records;
574
+ }
575
+ /** Records one provenance record of an exported artifact. */
576
+ async addprovenance(record2) {
577
+ const records = await this.getprovenances();
578
+ await this.adapter.set("provenances", [record2, ...records]);
579
+ }
580
+ /** Returns every provenance record per exported artifact, newest first. */
581
+ async getprovenances() {
582
+ return await this.adapter.get("provenances") ?? [];
583
+ }
584
+ /** Stores the transform rules and dedupe keys of one task, replacing the previous record of that task. */
585
+ async settaskrules(value) {
586
+ const records = (await this.adapter.get("taskrules") ?? []).filter((item) => item.taskid !== value.taskid);
587
+ await this.adapter.set("taskrules", [value, ...records]);
588
+ }
589
+ /** Returns the transform rules and dedupe keys per task, newest first. */
590
+ async gettaskrules() {
591
+ return await this.adapter.get("taskrules") ?? [];
592
+ }
593
+ /** Stores one stream chunk state for resume, replacing the previous state of that dataset. */
594
+ async setstream(state) {
595
+ const records = (await this.adapter.get("streams") ?? []).filter((item) => item.datasetid !== state.datasetid);
596
+ await this.adapter.set("streams", [state, ...records]);
597
+ }
598
+ /** Returns every stream chunk state persisted for resume, newest first. */
599
+ async getstreams() {
600
+ return await this.adapter.get("streams") ?? [];
601
+ }
602
+ /** Stores one reviewed sheet endpoint config behind its origin grant, replacing the previous config of that origin. */
603
+ async setsheetendpoint(config) {
604
+ const records = (await this.adapter.get("sheetendpoints") ?? []).filter((item) => item.origin !== config.origin);
605
+ await this.adapter.set("sheetendpoints", [config, ...records]);
606
+ }
607
+ /** Returns every reviewed sheet endpoint config, newest first. */
608
+ async getsheetendpoints() {
609
+ return await this.adapter.get("sheetendpoints") ?? [];
610
+ }
611
+ /** Records one exported data artifact; artifact retention is a user setting and an absent setting keeps every artifact. */
612
+ async addexport(artifact) {
613
+ const records = await this.getexports();
614
+ const combined = [artifact, ...records.filter((item) => item.id !== artifact.id)];
615
+ const retention = (await this.getsettings())?.artifactretention;
616
+ await this.adapter.set("exports", retention === void 0 ? combined : combined.slice(0, retention));
617
+ }
618
+ /** Returns every exported data artifact with its content and checksum, newest first. */
619
+ async getexports() {
620
+ return await this.adapter.get("exports") ?? [];
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
+ }
523
734
  };
524
735
  function randomid() {
525
736
  return crypto.randomUUID();
526
737
  }
527
738
 
528
739
  // policy.ts
529
- 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"]);
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"]);
530
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"]);
531
- 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"]);
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"]);
532
743
  var allowedactions = /* @__PURE__ */ new Set([...sensitiveactions, ...interactionactions, ...readactions]);
533
744
  var watchactions = /* @__PURE__ */ new Set(["watchmutate", "watchbanner", "watchfocus", "watchtab"]);
534
- 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"]);
535
- 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"]);
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"]);
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"]);
536
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"]);
537
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"]);
749
+ var datasetactions = /* @__PURE__ */ new Set(["scrapetable", "exportcsv", "exportjson", "exportexcel", "copytable", "pushsheets", "importcsv", "looprows", "transformvalues", "deduperows", "paginateextract", "mergepages", "stamplerows", "previewgrid", "streamdisk", "resumeextract", "logprovenance"]);
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"]);
538
752
  var fieldkinds = ["text", "email", "phone", "date", "number", "select", "check", "radio", "file", "password", "card", "code"];
539
753
  var layoutmutationactions = /* @__PURE__ */ new Set(["grouptabs", "colorgroup", "collapsegroup", "savelayout", "restorelayout"]);
540
754
  var groupcolors = ["grey", "blue", "red", "yellow", "green", "pink", "purple", "cyan", "orange"];
@@ -569,6 +783,10 @@ function requiredcapability(kind) {
569
783
  if (kind === "tablist") return "tabs";
570
784
  if (kind === "downloadfile") return "downloads";
571
785
  if (kind === "openclipboard") return "clipboardRead";
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";
572
790
  if (kind === "openlink" || kind === "openprivate" || kind === "navlist" || kind === "batchopen" || kind === "reopentab" || kind === "deeplink") return "tabs";
573
791
  if (tabscommandactions.has(kind)) return "tabs";
574
792
  return void 0;
@@ -582,6 +800,19 @@ function islayoutkind(kind) {
582
800
  function isformkind(kind) {
583
801
  return formactions.has(kind);
584
802
  }
803
+ function isdatasetkind(kind) {
804
+ return datasetactions.has(kind);
805
+ }
806
+ function isexportkind(kind) {
807
+ return exportactions.has(kind);
808
+ }
809
+ function isfileskind(kind) {
810
+ return filesactions.has(kind);
811
+ }
812
+ function exportgranted(session, origin) {
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.` };
814
+ return { allowed: true };
815
+ }
585
816
  function validatefieldmatch(value) {
586
817
  if (!value || typeof value !== "object" || Array.isArray(value)) return { allowed: false, reason: "A reviewed field match is required in options." };
587
818
  const match = value;
@@ -679,6 +910,171 @@ function validateformgrammar(step, options) {
679
910
  if (kind === "consentpassword" && !isnonempty(options.consentref)) return { allowed: false, reason: "A reviewed consent ref is required in options before any password is filled." };
680
911
  return { allowed: true };
681
912
  }
913
+ function validatetransformrule(value) {
914
+ if (!value || typeof value !== "object" || Array.isArray(value)) return { allowed: false, reason: "A reviewed transform rule with an expression, sources and a target is required in options." };
915
+ const rule = value;
916
+ const expression = rule.expression;
917
+ if (typeof expression !== "string" || !/^(trim|upper|lower|number|prefix|suffix|replace)(?::.+)?$/.test(expression)) return { allowed: false, reason: "The reviewed transform expression must be trim, upper, lower, number, prefix, suffix or replace with an optional argument." };
918
+ if (expression.startsWith("replace") && !expression.slice("replace".length).includes("=>")) return { allowed: false, reason: "The reviewed replace expression needs the from=>to separator." };
919
+ if (expression.startsWith("replace") && expression.slice("replace:".length).split("=>")[0] === "") return { allowed: false, reason: "The reviewed replace expression needs a non-empty from part." };
920
+ if (!Array.isArray(rule.sources) || rule.sources.length === 0 || !rule.sources.every((source) => isnonempty(source))) return { allowed: false, reason: "Every reviewed transform rule needs a non-empty list of source columns." };
921
+ if (!isnonempty(rule.target)) return { allowed: false, reason: "Every reviewed transform rule needs a non-empty target column." };
922
+ return { allowed: true };
923
+ }
924
+ function validatedatasetids(options, key) {
925
+ const ids = options[key];
926
+ if (!Array.isArray(ids) || ids.length === 0 || !ids.every((id) => isnonempty(id))) return { allowed: false, reason: `A reviewed non-empty list of dataset ids is required in options as ${key}.` };
927
+ return { allowed: true };
928
+ }
929
+ function validatedatagrammar(step, options, origin) {
930
+ const kind = step.kind;
931
+ if (kind === "scrapetable") {
932
+ if (options.name !== void 0 && !isnonempty(options.name)) return { allowed: false, reason: "The reviewed dataset name must be a non-empty string." };
933
+ if (options.rowlimit !== void 0 && (typeof options.rowlimit !== "number" || !Number.isInteger(options.rowlimit) || options.rowlimit < 1)) return { allowed: false, reason: "The reviewed row limit must be a positive integer with no code ceiling." };
934
+ }
935
+ if (kind === "paginateextract") {
936
+ if (!isnonempty(options.next)) return { allowed: false, reason: "A reviewed next control selector is required in options." };
937
+ if (options.pages !== void 0 && (typeof options.pages !== "number" || !Number.isInteger(options.pages) || options.pages < 1)) return { allowed: false, reason: "The reviewed page count must be a positive integer with no code ceiling." };
938
+ if (!nonnegativeoption(options, "wait")) return { allowed: false, reason: "The reviewed row freshness wait must be zero or a positive number of milliseconds." };
939
+ }
940
+ if (kind === "exportcsv" || kind === "exportjson" || kind === "exportexcel" || kind === "copytable" || kind === "streamdisk") {
941
+ if (!isnonempty(options.dataset)) return { allowed: false, reason: "A reviewed dataset id is required in options." };
942
+ if (options.name !== void 0 && !isnonempty(options.name)) return { allowed: false, reason: "The reviewed artifact name must be a non-empty string." };
943
+ }
944
+ if (kind === "exportcsv" && options.delimiter !== void 0 && (typeof options.delimiter !== "string" || options.delimiter.length !== 1)) return { allowed: false, reason: "The reviewed csv delimiter must be a single character." };
945
+ if (kind === "streamdisk" && (typeof options.chunk !== "number" || !Number.isInteger(options.chunk) || options.chunk < 1)) return { allowed: false, reason: "The reviewed streaming chunk size must be a positive integer with no code ceiling." };
946
+ if (kind === "pushsheets") {
947
+ if (!isnonempty(options.dataset)) return { allowed: false, reason: "A reviewed dataset id is required in options." };
948
+ if (!isnonempty(options.sheet)) return { allowed: false, reason: "A reviewed sheet endpoint url is required in options." };
949
+ if (!ishttpsurl(options.sheet)) return { allowed: false, reason: "The reviewed sheet endpoint url must use HTTPS." };
950
+ if (options.reviewed !== true) return { allowed: false, reason: "The sheet push needs the explicit reviewed flag before any data leaves local memory." };
951
+ }
952
+ if (kind === "importcsv") {
953
+ if (typeof options.csv !== "string" || !options.csv.trim()) return { allowed: false, reason: "Reviewed csv content is required in options." };
954
+ if (options.name !== void 0 && !isnonempty(options.name)) return { allowed: false, reason: "The reviewed dataset name must be a non-empty string." };
955
+ if (options.mapping !== void 0) {
956
+ const mapping = options.mapping;
957
+ if (!mapping || typeof mapping !== "object" || Array.isArray(mapping) || !Object.values(mapping).every((item) => typeof item === "string")) return { allowed: false, reason: "The reviewed csv column mapping must be an object of string values." };
958
+ }
959
+ }
960
+ if (kind === "looprows") {
961
+ if (!isnonempty(options.dataset)) return { allowed: false, reason: "A reviewed dataset id is required in options." };
962
+ if (options.variable !== void 0 && !isnonempty(options.variable)) return { allowed: false, reason: "The reviewed row variable name must be a non-empty string." };
963
+ const inner = validateinnerstep(options, origin);
964
+ if (!inner.allowed) return inner;
965
+ }
966
+ if (kind === "transformvalues") {
967
+ if (!isnonempty(options.dataset)) return { allowed: false, reason: "A reviewed dataset id is required in options." };
968
+ const rules = options.rules;
969
+ if (!Array.isArray(rules) || rules.length === 0) return { allowed: false, reason: "A reviewed non-empty list of transform rules is required in options." };
970
+ for (const item of rules) {
971
+ const rulecheck = validatetransformrule(item);
972
+ if (!rulecheck.allowed) return rulecheck;
973
+ }
974
+ }
975
+ if (kind === "deduperows") {
976
+ if (!isnonempty(options.dataset)) return { allowed: false, reason: "A reviewed dataset id is required in options." };
977
+ const keys = options.keys;
978
+ if (!Array.isArray(keys) || keys.length === 0 || !keys.every((key) => isnonempty(key))) return { allowed: false, reason: "A reviewed non-empty list of dedupe column keys is required in options." };
979
+ }
980
+ if (kind === "mergepages") {
981
+ const listcheck = validatedatasetids(options, "datasets");
982
+ if (!listcheck.allowed) return listcheck;
983
+ }
984
+ if (kind === "stamplerows") {
985
+ if (!isnonempty(options.dataset)) return { allowed: false, reason: "A reviewed dataset id is required in options." };
986
+ if (options.url !== void 0 && !ishttpsurl(options.url)) return { allowed: false, reason: "The reviewed source url must use HTTPS." };
987
+ }
988
+ if (kind === "previewgrid") {
989
+ if (!isnonempty(options.dataset)) return { allowed: false, reason: "A reviewed dataset id is required in options." };
990
+ if (options.sample !== void 0 && (typeof options.sample !== "number" || !Number.isInteger(options.sample) || options.sample < 1)) return { allowed: false, reason: "The reviewed sample row count must be a positive integer with no code ceiling." };
991
+ }
992
+ if (kind === "resumeextract" && !isnonempty(options.session)) return { allowed: false, reason: "A reviewed extract session id is required in options." };
993
+ if (kind === "logprovenance" && !isnonempty(options.artifact)) return { allowed: false, reason: "A reviewed artifact id or name is required in options." };
994
+ return { allowed: true };
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
+ }
682
1078
  function submitreviewgranted(steps, submitid) {
683
1079
  const position = steps.findIndex((candidate) => candidate.id === submitid);
684
1080
  const asked = steps.some((candidate, index) => candidate.kind === "asksubmit" && (position === -1 || index < position));
@@ -817,7 +1213,7 @@ function validateinnerstep(options, origin) {
817
1213
  return { allowed: true };
818
1214
  }
819
1215
  if (typeof kind !== "string" || !kind.trim()) return { allowed: false, reason: "A reviewed step id or inline step kind is required in options." };
820
- if (kind === "retryaction" || kind === "enterframe") return { allowed: false, reason: "The reviewed inner step cannot be another wrapper kind." };
1216
+ if (kind === "retryaction" || kind === "enterframe" || kind === "looprows") return { allowed: false, reason: "The reviewed inner step cannot be another wrapper kind." };
821
1217
  if (!allowedactions.has(kind)) return { allowed: false, reason: "The reviewed inner step kind is unsupported." };
822
1218
  const inneroptions = options.options;
823
1219
  if (inneroptions !== void 0 && (!inneroptions || typeof inneroptions !== "object" || Array.isArray(inneroptions))) return { allowed: false, reason: "The reviewed inner step options must be an object." };
@@ -1201,6 +1597,14 @@ function validatestep(step, origin) {
1201
1597
  const formcheck = validateformgrammar(step, options);
1202
1598
  if (!formcheck.allowed) return formcheck;
1203
1599
  }
1600
+ if (isdatasetkind(step.kind)) {
1601
+ const datacheck = validatedatagrammar(step, options, origin);
1602
+ if (!datacheck.allowed) return datacheck;
1603
+ }
1604
+ if (isfileskind(step.kind)) {
1605
+ const filescheck = validatefilesgrammar(step, options);
1606
+ if (!filescheck.allowed) return filescheck;
1607
+ }
1204
1608
  if (step.kind === "tabcreate") {
1205
1609
  if (options.background !== void 0 && typeof options.background !== "boolean") return { allowed: false, reason: "The reviewed background flag must be a boolean." };
1206
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." };
@@ -1228,6 +1632,10 @@ function canexecute(input) {
1228
1632
  if (input.plan.expiresat <= now) return { allowed: false, reason: "The approved plan has expired." };
1229
1633
  if ((input.step.kind === "pierceshadow" || input.step.kind === "enterframe") && !origingranted(input.session, input.origin)) return { allowed: false, reason: "The shadow or frame step is outside the session origin grants." };
1230
1634
  if (input.step.kind === "readjson" && !origingranted(input.session, input.origin)) return { allowed: false, reason: "The json state read is outside the session origin grants." };
1635
+ if (isexportkind(input.step.kind)) {
1636
+ const exportgate = exportgranted(input.session, input.origin);
1637
+ if (!exportgate.allowed) return exportgate;
1638
+ }
1231
1639
  if (input.step.kind === "navlist") {
1232
1640
  let options = {};
1233
1641
  try {
@@ -1251,6 +1659,11 @@ function canexecute(input) {
1251
1659
  const consentgate = passwordconsentgranted(input.step);
1252
1660
  if (!consentgate.allowed) return consentgate;
1253
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." };
1254
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") {
1255
1668
  let options = {};
1256
1669
  try {
@@ -1336,6 +1749,15 @@ function wizardcompletion(state) {
1336
1749
  if (state.steps <= 0) return 0;
1337
1750
  return Math.min(1, state.completed.filter(Boolean).length / state.steps);
1338
1751
  }
1752
+ function extractionshare(rowscollected, estimatedtotal) {
1753
+ if (!Number.isFinite(estimatedtotal) || estimatedtotal <= 0) return 0;
1754
+ return Math.min(1, Math.max(0, rowscollected) / estimatedtotal);
1755
+ }
1756
+ function recordextraction(progress, planid, stepid, entry, now) {
1757
+ const base = progress && progress.planid === planid ? progress : emptyprogress(planid, now);
1758
+ const outcome = { stepid, ok: true, summary: `Extraction page ${entry.page} collected ${entry.rows} row${entry.rows === 1 ? "" : "s"} at cursor ${entry.cursor}.`, details: { extraction: entry }, at: now };
1759
+ return recordoutcome(base, planid, outcome, now);
1760
+ }
1339
1761
  function recordwizardstep(progress, planid, stepid, state, now) {
1340
1762
  const base = progress && progress.planid === planid ? progress : emptyprogress(planid, now);
1341
1763
  const executed = Math.min(state.index, state.steps);
@@ -1343,9 +1765,18 @@ function recordwizardstep(progress, planid, stepid, state, now) {
1343
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 };
1344
1766
  return recordoutcome(base, planid, outcome, now);
1345
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
+ }
1346
1777
 
1347
1778
  // version.ts
1348
- var packageversion = "1.1.37";
1779
+ var packageversion = "1.1.39";
1349
1780
 
1350
1781
  // types.ts
1351
1782
  var protocolversion = packageversion;
@@ -1382,9 +1813,9 @@ function parseproposal(value, origin) {
1382
1813
  return step;
1383
1814
  });
1384
1815
  for (const step of steps) {
1385
- if (step.kind !== "retryaction" && step.kind !== "enterframe") continue;
1816
+ if (step.kind !== "retryaction" && step.kind !== "enterframe" && step.kind !== "looprows") continue;
1386
1817
  const options = parseoptions(step);
1387
- if (typeof options.stepid === "string" && !steps.some((candidate) => candidate.id === options.stepid)) throw new Error("A retry or frame wrapper references an unknown step id.");
1818
+ if (typeof options.stepid === "string" && !steps.some((candidate) => candidate.id === options.stepid)) throw new Error("A retry, frame or loop wrapper references an unknown step id.");
1388
1819
  }
1389
1820
  for (const step of steps) {
1390
1821
  if (step.kind !== "submitform" && step.kind !== "retryform") continue;
@@ -1451,6 +1882,26 @@ function errorreportresponse(input) {
1451
1882
  function wizardreport(input) {
1452
1883
  return { version: protocolversion, ...input.sessionid ? { sessionid: input.sessionid } : {}, wizards: input.wizards, picks: input.picks };
1453
1884
  }
1885
+ function datasetresponse(input) {
1886
+ const sample = Math.max(0, Math.floor(input.sample ?? 10));
1887
+ const payload = { ...input.dataset, rows: input.dataset.rows.slice(0, sample), totalrows: input.dataset.rows.length };
1888
+ return JSON.stringify({ version: protocolversion, planid: input.plan.id, planstate: input.plan.state, dataset: payload });
1889
+ }
1890
+ function extractionreport(input) {
1891
+ return { version: protocolversion, sessions: input.sessions };
1892
+ }
1893
+ function provenancereport(input) {
1894
+ return { version: protocolversion, records: input.records };
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
+ }
1454
1905
 
1455
1906
  // extension/browsertabs.ts
1456
1907
  var browserkinds = /* @__PURE__ */ new Set(["tablist", "tabcreate", "tabactivate", "tabclose", "tabreload", "tabsnapshot", "windowlist", "windowcreate", "windowclose", "zoomset", "windowresize", "downloadfile"]);
@@ -1779,13 +2230,13 @@ function presshold(holds, hold) {
1779
2230
  return { holds: [...holds, hold], ok: true };
1780
2231
  }
1781
2232
  function releasehold(holds, holdid, releasedat) {
1782
- let released;
2233
+ let released2;
1783
2234
  const next = holds.map((hold) => {
1784
2235
  if (hold.holdid !== holdid || hold.releasedat !== void 0) return hold;
1785
- released = { ...hold, releasedat };
1786
- return released;
2236
+ released2 = { ...hold, releasedat };
2237
+ return released2;
1787
2238
  });
1788
- return { holds: next, ...released ? { released } : {} };
2239
+ return { holds: next, ...released2 ? { released: released2 } : {} };
1789
2240
  }
1790
2241
  function heldkeys(holds, tabid2) {
1791
2242
  return holds.filter((hold) => hold.releasedat === void 0 && (tabid2 === void 0 || hold.tabid === void 0 || hold.tabid === tabid2));
@@ -2213,6 +2664,408 @@ function backoffwaits(attempts, wait, factor) {
2213
2664
  return windows;
2214
2665
  }
2215
2666
 
2667
+ // extension/pagedata.ts
2668
+ function normalizeheader(label) {
2669
+ const slug = label.trim().toLowerCase().replace(/[^\p{L}\p{N}]+/gu, "-").replace(/^-+|-+$/g, "");
2670
+ return slug || "column";
2671
+ }
2672
+ function columnspecof(label, used = /* @__PURE__ */ new Set()) {
2673
+ const base = normalizeheader(label);
2674
+ let key = base;
2675
+ let suffix = 2;
2676
+ while (used.has(key)) {
2677
+ key = `${base}${suffix}`;
2678
+ suffix += 1;
2679
+ }
2680
+ used.add(key);
2681
+ return { key, label: label.trim(), kind: "text", normalized: label.trim().toLowerCase() };
2682
+ }
2683
+ function rowhash(row, keys) {
2684
+ const source = (keys.length > 0 ? keys : Object.keys(row).sort()).map((key) => `${key}=${row[key] ?? ""}`).join("|");
2685
+ let hash = 5381;
2686
+ for (let index = 0; index < source.length; index += 1) hash = (hash * 33 ^ source.charCodeAt(index)) >>> 0;
2687
+ return hash.toString(16);
2688
+ }
2689
+ function dedupebykeys(rows, keys) {
2690
+ const seen = /* @__PURE__ */ new Set();
2691
+ const kept = [];
2692
+ for (const row of rows) {
2693
+ const hash = rowhash(row, keys);
2694
+ if (seen.has(hash)) continue;
2695
+ seen.add(hash);
2696
+ kept.push(row);
2697
+ }
2698
+ return { kept, removed: rows.length - kept.length };
2699
+ }
2700
+ function applyexpression(value, expression) {
2701
+ const split = expression.indexOf(":");
2702
+ const op = split === -1 ? expression : expression.slice(0, split);
2703
+ const argument = split === -1 ? void 0 : expression.slice(split + 1);
2704
+ if (op === "trim") return value.trim();
2705
+ if (op === "upper") return value.toUpperCase();
2706
+ if (op === "lower") return value.toLowerCase();
2707
+ if (op === "number") return value.replace(/[^\d.\-]/g, "");
2708
+ if (op === "prefix") return `${argument ?? ""}${value}`;
2709
+ if (op === "suffix") return `${value}${argument ?? ""}`;
2710
+ if (op === "replace") {
2711
+ const separator = argument?.indexOf("=>") ?? -1;
2712
+ if (separator === -1 || separator === 0) throw new Error(`The reviewed transform expression ${expression} needs the from=>to separator.`);
2713
+ const from = argument.slice(0, separator);
2714
+ const to = argument.slice(separator + 2);
2715
+ return value.split(from).join(to);
2716
+ }
2717
+ throw new Error(`The reviewed transform expression ${op} is not supported.`);
2718
+ }
2719
+ function transformrows(rows, rules) {
2720
+ const errors = [];
2721
+ const output = rows.map((row) => ({ ...row }));
2722
+ for (const rule of rules) {
2723
+ const updated = [];
2724
+ try {
2725
+ for (const row of output) updated.push({ ...row, [rule.target]: applyexpression(rule.sources.map((source) => row[source] ?? "").join(" "), rule.expression) });
2726
+ } catch (error) {
2727
+ errors.push(`${rule.target}: ${error instanceof Error ? error.message : String(error)}`);
2728
+ continue;
2729
+ }
2730
+ output.splice(0, output.length, ...updated);
2731
+ }
2732
+ return { rows: output, errors };
2733
+ }
2734
+ function mergedatasets(datasets) {
2735
+ const columns = [];
2736
+ const seen = /* @__PURE__ */ new Set();
2737
+ for (const dataset of datasets) {
2738
+ for (const column of dataset.columns) {
2739
+ if (seen.has(column.key)) continue;
2740
+ seen.add(column.key);
2741
+ columns.push(column);
2742
+ }
2743
+ }
2744
+ const rows = datasets.flatMap((dataset) => dataset.rows.map((row) => {
2745
+ const merged = {};
2746
+ for (const column of columns) merged[column.key] = row[column.key] ?? "";
2747
+ return merged;
2748
+ }));
2749
+ return { columns, rows };
2750
+ }
2751
+ function samplerows(rows, url, stepid, at) {
2752
+ const stamped = rows.map((row) => ({ ...row, source: url, capturedat: String(at), step: stepid }));
2753
+ const sources = stamped.map((row, index) => ({ row: index, url, at, stepid }));
2754
+ return { rows: stamped, sources };
2755
+ }
2756
+ function csvfield(value, delimiter) {
2757
+ return value.includes(delimiter) || value.includes('"') || value.includes("\n") ? `"${value.replace(/"/g, '""')}"` : value;
2758
+ }
2759
+ function tocsv(columns, rows, delimiter = ",") {
2760
+ const lines = [columns.map((column) => csvfield(column.label || column.key, delimiter)).join(delimiter)];
2761
+ for (const row of rows) lines.push(columns.map((column) => csvfield(row[column.key] ?? "", delimiter)).join(delimiter));
2762
+ return lines.join("\n");
2763
+ }
2764
+ function parsecsv(text2, delimiter = ",") {
2765
+ const records = [];
2766
+ let field = "";
2767
+ let record2 = [];
2768
+ let quoted = false;
2769
+ for (let index = 0; index < text2.length; index += 1) {
2770
+ const character = text2[index];
2771
+ if (quoted) {
2772
+ if (character === '"') {
2773
+ if (text2[index + 1] === '"') {
2774
+ field += '"';
2775
+ index += 1;
2776
+ } else quoted = false;
2777
+ } else field += character;
2778
+ continue;
2779
+ }
2780
+ if (character === '"') {
2781
+ quoted = true;
2782
+ continue;
2783
+ }
2784
+ if (character === delimiter) {
2785
+ record2.push(field);
2786
+ field = "";
2787
+ continue;
2788
+ }
2789
+ if (character === "\n" || character === "\r") {
2790
+ if (character === "\r" && text2[index + 1] === "\n") index += 1;
2791
+ record2.push(field);
2792
+ field = "";
2793
+ if (record2.some((value) => value.length > 0) || record2.length > 1) records.push(record2);
2794
+ record2 = [];
2795
+ continue;
2796
+ }
2797
+ field += character;
2798
+ }
2799
+ record2.push(field);
2800
+ if (record2.some((value) => value.length > 0) || record2.length > 1) records.push(record2);
2801
+ const [headers = [], ...rows] = records;
2802
+ return { headers, rows };
2803
+ }
2804
+ function mapcolumns(headers, mapping = {}) {
2805
+ const used = /* @__PURE__ */ new Set();
2806
+ return headers.map((header) => {
2807
+ const target = mapping[header] ?? mapping[normalizeheader(header)] ?? header;
2808
+ return columnspecof(target, used);
2809
+ });
2810
+ }
2811
+ function tojson(columns, rows) {
2812
+ return JSON.stringify({ columns, rows });
2813
+ }
2814
+ function xmltext(value) {
2815
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
2816
+ }
2817
+ function toexcel(columns, rows, name) {
2818
+ const head = columns.map((column) => `<Cell ss:StyleID="head"><Data ss:Type="String">${xmltext(column.label || column.key)}</Data></Cell>`).join("");
2819
+ const body = rows.map((row) => `<Row>${columns.map((column) => {
2820
+ const value = row[column.key] ?? "";
2821
+ const numeric = column.kind === "number" && value.trim() !== "" && Number.isFinite(Number(value));
2822
+ return numeric ? `<Cell><Data ss:Type="Number">${xmltext(value)}</Data></Cell>` : `<Cell><Data ss:Type="String">${xmltext(value)}</Data></Cell>`;
2823
+ }).join("")}</Row>`).join("");
2824
+ return `<?xml version="1.0"?><?mso-application progid="Excel.Sheet"?><Workbook xmlns="urn:schemas-microsoft-com:office:spreadsheet" xmlns:ss="urn:schemas-microsoft-com:office:spreadsheet"><Styles><Style ss:ID="head"><Font ss:Bold="1"/></Style></Styles><Worksheet ss:Name="${xmltext(name || "dataset").slice(0, 31)}"><Table><Row>${head}</Row>${body}</Table></Worksheet></Workbook>`;
2825
+ }
2826
+
2827
+ // extension/datacommand.ts
2828
+ function builddataset(id, name, grid, at) {
2829
+ return { id, name: name || id, columns: grid.columns, rows: grid.rows, sources: [], at };
2830
+ }
2831
+ function checksum(value) {
2832
+ let hash = 5381;
2833
+ for (let index = 0; index < value.length; index += 1) hash = (hash * 33 ^ value.charCodeAt(index)) >>> 0;
2834
+ return `fnv1a-${hash.toString(16)}`;
2835
+ }
2836
+ function exportcontent(datasetvalue, format, delimiter = ",") {
2837
+ if (format === "json") return tojson(datasetvalue.columns, datasetvalue.rows);
2838
+ if (format === "excel") return toexcel(datasetvalue.columns, datasetvalue.rows, datasetvalue.name);
2839
+ return tocsv(datasetvalue.columns, datasetvalue.rows, delimiter);
2840
+ }
2841
+ function exportartifact(id, datasetvalue, format, stepid, content, at) {
2842
+ const extension = format === "excel" ? "xml" : format;
2843
+ return { id, kind: format, name: `${datasetvalue.name || datasetvalue.id}.${extension}`, stepid, rowcount: datasetvalue.rows.length, content, checksum: checksum(content), at };
2844
+ }
2845
+ function artifactrecordof(artifact) {
2846
+ return { id: artifact.id, kind: artifact.kind, name: artifact.name, stepid: artifact.stepid, at: artifact.at };
2847
+ }
2848
+ function chunkplan(rows, chunk) {
2849
+ const size = Math.max(1, Math.floor(chunk));
2850
+ const chunks = [];
2851
+ for (let from = 0; from < rows || chunks.length === 0; from += size) {
2852
+ const to = Math.min(rows, from + size);
2853
+ chunks.push({ index: chunks.length, from, to });
2854
+ if (to >= rows) break;
2855
+ }
2856
+ return chunks;
2857
+ }
2858
+ function backpressure(written, acknowledged) {
2859
+ return written - acknowledged >= 1;
2860
+ }
2861
+ function advancestream(state, chunk, at, done) {
2862
+ return { datasetid: state.datasetid, name: state.name, chunk: chunk.index + 1, chunks: state.chunks, written: chunk.to, ...done ? { done: true } : {}, at };
2863
+ }
2864
+ function newstream(datasetvalue, chunks, at) {
2865
+ return { datasetid: datasetvalue.id, name: datasetvalue.name, chunk: 0, chunks, written: 0, at };
2866
+ }
2867
+ function advancecursor(sessionvalue, page, rows, at, done) {
2868
+ return {
2869
+ id: sessionvalue.id,
2870
+ datasetid: sessionvalue.datasetid,
2871
+ name: sessionvalue.name,
2872
+ target: sessionvalue.target,
2873
+ next: sessionvalue.next,
2874
+ planned: sessionvalue.planned,
2875
+ pages: [...sessionvalue.pages, page],
2876
+ rows: sessionvalue.rows + rows,
2877
+ cursor: sessionvalue.cursor + 1,
2878
+ ...done || sessionvalue.cursor + 1 >= sessionvalue.planned ? { done: true } : {},
2879
+ startedat: sessionvalue.startedat,
2880
+ updatedat: at
2881
+ };
2882
+ }
2883
+ function newextractsession(id, datasetid, name, target, next, planned, at) {
2884
+ return { id, datasetid, name, target, next, planned, pages: [], rows: 0, cursor: 0, startedat: at, updatedat: at };
2885
+ }
2886
+ function remainingpages(sessionvalue, planned) {
2887
+ if (sessionvalue.done) return 0;
2888
+ return Math.max(0, Math.max(sessionvalue.planned, planned) - sessionvalue.cursor);
2889
+ }
2890
+ function provenancefor(artifact, url, stepid, at) {
2891
+ return { artifact: artifact.id, name: artifact.name, url, stepid, rowstart: artifact.rowcount > 0 ? 1 : 0, rowend: artifact.rowcount, checksum: artifact.checksum, at };
2892
+ }
2893
+ function interpolate(text2, row) {
2894
+ return text2.replace(/\{\{([^}]+)\}\}/g, (_, key) => row[key.trim()] ?? "");
2895
+ }
2896
+ function loopstep(step, row) {
2897
+ return {
2898
+ ...step,
2899
+ ...step.target !== void 0 ? { target: interpolate(step.target, row) } : {},
2900
+ ...step.value !== void 0 ? { value: interpolate(step.value, row) } : {},
2901
+ ...step.options !== void 0 ? { options: interpolate(step.options, row) } : {}
2902
+ };
2903
+ }
2904
+ function loopvariables(row) {
2905
+ return { ...row };
2906
+ }
2907
+ function gridpreview(datasetvalue, sample) {
2908
+ return { datasetid: datasetvalue.id, columns: datasetvalue.columns.map((column) => column.key), rows: datasetvalue.rows.length, sample: datasetvalue.rows.slice(0, Math.max(0, Math.floor(sample))) };
2909
+ }
2910
+ function sheetpayload(datasetvalue, sheet) {
2911
+ return { sheet, columns: datasetvalue.columns.map((column) => column.key), rows: datasetvalue.rows };
2912
+ }
2913
+ function mergetaskrules(existing, taskid, transforms, dedupekeys, at) {
2914
+ return {
2915
+ taskid,
2916
+ transforms: transforms.length > 0 ? transforms : existing?.transforms ?? [],
2917
+ dedupekeys: dedupekeys.length > 0 ? dedupekeys : existing?.dedupekeys ?? [],
2918
+ at
2919
+ };
2920
+ }
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
+
2216
3069
  // extension/background.ts
2217
3070
  var sessionduration = 15 * 60 * 1e3;
2218
3071
  var freshcheckkinds = /* @__PURE__ */ new Set(["focus", "inspect", "click", "type", "scroll", "select", "hover"]);
@@ -2403,6 +3256,21 @@ function stepauditkind(step, ok) {
2403
3256
  if (step.kind === "submitform" || step.kind === "asksubmit") return "submit";
2404
3257
  if (step.kind === "consentpassword") return "consent";
2405
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
+ }
3267
+ if (isdatasetkind(step.kind)) {
3268
+ if (step.kind === "exportcsv" || step.kind === "exportjson" || step.kind === "exportexcel" || step.kind === "copytable" || step.kind === "pushsheets") return "export";
3269
+ if (step.kind === "streamdisk") return "stream";
3270
+ if (step.kind === "resumeextract") return "resume";
3271
+ if (step.kind === "logprovenance") return "provenance";
3272
+ return "scrape";
3273
+ }
2406
3274
  if (formfillkinds.has(step.kind)) return "fill";
2407
3275
  if (pointerkinds.has(step.kind)) return "pointer";
2408
3276
  if (watchstepkinds.has(step.kind)) return "watch";
@@ -2705,8 +3573,18 @@ async function recordnavigation(step, session, tabid2) {
2705
3573
  await memory.addnavrecord(record2);
2706
3574
  await memory.setnavstate(tabid2, record2);
2707
3575
  if (session && url) await memory.addtrailentry(session.id, { url, title, stepid: step.id, at: Date.now() });
3576
+ await collectnetlog(tabid2, step);
2708
3577
  return record2;
2709
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
+ }
2710
3588
  function injectallowedorigins(step, session) {
2711
3589
  const allowedorigins = session?.grants ?? (session ? [session.origin] : []);
2712
3590
  let options = {};
@@ -3585,6 +4463,484 @@ async function executeformstep(step, session, plan, tabid2, origin) {
3585
4463
  }
3586
4464
  }
3587
4465
  }
4466
+ async function loaddataset(datasetid) {
4467
+ const record2 = await memory.getdataset(datasetid);
4468
+ if (!record2) throw new Error(`No dataset ${datasetid} exists yet; scrape or import it first.`);
4469
+ return record2;
4470
+ }
4471
+ function readgridoutput(output) {
4472
+ const grid = output?.details?.grid;
4473
+ if (!grid || typeof grid !== "object") return null;
4474
+ return grid;
4475
+ }
4476
+ async function storeexport(stepid, datasetvalue, format, delimiter, session, planid, origin) {
4477
+ const content = exportcontent(datasetvalue, format, delimiter);
4478
+ const artifact = exportartifact(randomid(), datasetvalue, format, stepid, content, Date.now());
4479
+ await memory.addexport(artifact);
4480
+ await memory.addartifact(artifactrecordof(artifact));
4481
+ await memory.addprovenance(provenancefor(artifact, datasetvalue.sources[0]?.url ?? origin, stepid, Date.now()));
4482
+ await audit("export", `Exported ${artifact.rowcount} row${artifact.rowcount === 1 ? "" : "s"} of dataset ${datasetvalue.name} into the ${format} artifact ${artifact.name} with checksum ${artifact.checksum}; the provenance record keeps the source url, step ref and row range.`, { ...session ? { sessionid: session.id } : {}, planid, stepid });
4483
+ await refreshbadge();
4484
+ return artifact;
4485
+ }
4486
+ async function executedatastep(step, session, plan, tabid2, origin) {
4487
+ const options = stepoptions2(step);
4488
+ const extra = { ...session ? { sessionid: session.id } : {}, planid: plan.id, stepid: step.id };
4489
+ switch (step.kind) {
4490
+ case "scrapetable": {
4491
+ const output = await dispatchpagestep(step, tabid2, origin, plan);
4492
+ const grid = readgridoutput(output);
4493
+ if (!output?.ok || !grid) return output ?? { ok: false, summary: "The table scrape returned no result." };
4494
+ const id = randomid();
4495
+ const name = typeof options.name === "string" && options.name ? options.name : `dataset-${step.id}`;
4496
+ const datasetvalue = builddataset(id, name, grid, Date.now());
4497
+ await memory.setdataset(datasetvalue);
4498
+ const extract = advancecursor(newextractsession(randomid(), id, name, step.target ?? "", "", 1, Date.now()), origin, datasetvalue.rows.length, Date.now(), true);
4499
+ await memory.setextractsession(extract);
4500
+ await memory.setprogress(recordextraction(await memory.getprogress(), plan.id, step.id, { page: 1, rows: datasetvalue.rows.length, cursor: 1 }, Date.now()));
4501
+ await refreshbadge();
4502
+ return { ok: true, summary: output.summary, details: { ...output.details ?? {}, dataset: { id, name, rows: datasetvalue.rows.length, columns: grid.columns.length } } };
4503
+ }
4504
+ case "paginateextract": {
4505
+ const output = await dispatchpagestep(step, tabid2, origin, plan);
4506
+ const grid = readgridoutput(output);
4507
+ if (!output?.ok || !grid) return output ?? { ok: false, summary: "The paginated extraction returned no result." };
4508
+ const id = randomid();
4509
+ const name = typeof options.name === "string" && options.name ? options.name : `dataset-${step.id}`;
4510
+ const datasetvalue = builddataset(id, name, grid, Date.now());
4511
+ await memory.setdataset(datasetvalue);
4512
+ const planned = typeof options.pages === "number" && Number.isInteger(options.pages) && options.pages > 0 ? options.pages : Number(output.details?.pages ?? 1);
4513
+ const next = output.details?.next === true;
4514
+ const extract = advancecursor(newextractsession(randomid(), id, name, step.target ?? "", typeof options.next === "string" ? options.next : "", planned, Date.now()), origin, datasetvalue.rows.length, Date.now(), !next);
4515
+ await memory.setextractsession(extract);
4516
+ await memory.setprogress(recordextraction(await memory.getprogress(), plan.id, step.id, { page: extract.cursor, rows: datasetvalue.rows.length, cursor: extract.cursor }, Date.now()));
4517
+ await refreshbadge();
4518
+ const extractedpages = Number(output.details?.pages ?? 1);
4519
+ const estimated = Math.max(1, Math.round(datasetvalue.rows.length / Math.max(1, extractedpages) * planned));
4520
+ return { ok: true, summary: output.summary, details: { ...output.details ?? {}, dataset: { id, name, rows: datasetvalue.rows.length, columns: grid.columns.length }, extractsession: { id: extract.id, cursor: extract.cursor, planned, done: extract.done === true }, progressshare: extractionshare(datasetvalue.rows.length, estimated) } };
4521
+ }
4522
+ case "resumeextract": {
4523
+ const sessionid = typeof options.session === "string" ? options.session : "";
4524
+ const extract = (await memory.getextractsessions()).find((item) => item.id === sessionid);
4525
+ if (!extract) throw new Error(`No extract session ${sessionid} is stored yet.`);
4526
+ if (extract.done) return { ok: true, summary: `Extraction ${extract.name} already completed at cursor ${extract.cursor}.`, details: { cursor: extract.cursor, done: true } };
4527
+ const datasetvalue = await loaddataset(extract.datasetid);
4528
+ const remaining = remainingpages(extract, extract.planned);
4529
+ const derived = { id: step.id, kind: "paginateextract", target: extract.target, summary: step.summary, risk: "sensitive", options: JSON.stringify({ next: extract.next, pages: remaining, cursor: extract.cursor, name: extract.name }) };
4530
+ const output = await dispatchpagestep(derived, tabid2, origin, plan);
4531
+ const grid = readgridoutput(output);
4532
+ if (!output?.ok || !grid) return output ?? { ok: false, summary: "The resumed extraction returned no result." };
4533
+ const merged = mergedatasets([{ columns: datasetvalue.columns, rows: datasetvalue.rows }, { columns: grid.columns, rows: grid.rows }]);
4534
+ const updated = { ...datasetvalue, columns: merged.columns, rows: merged.rows, at: Date.now() };
4535
+ await memory.setdataset(updated);
4536
+ const next = output.details?.next === true;
4537
+ const resumed = advancecursor({ ...extract, updatedat: Date.now() }, origin, grid.rows.length, Date.now(), !next);
4538
+ await memory.setextractsession(resumed);
4539
+ await memory.setprogress(recordextraction(await memory.getprogress(), plan.id, step.id, { page: resumed.cursor, rows: grid.rows.length, cursor: resumed.cursor }, Date.now()));
4540
+ await audit("resume", `Extraction ${extract.name} resumed from cursor ${extract.cursor} and collected ${grid.rows.length} further row${grid.rows.length === 1 ? "" : "s"} across ${Number(output.details?.pages ?? 1)} page${Number(output.details?.pages ?? 1) === 1 ? "" : "s"}; the dataset now holds ${updated.rows.length} rows.`, extra);
4541
+ return { ok: true, summary: `Resumed extraction ${extract.name} from cursor ${extract.cursor}; the dataset now holds ${updated.rows.length} rows.`, details: { dataset: { id: updated.id, rows: updated.rows.length }, extractsession: { id: resumed.id, cursor: resumed.cursor, planned: resumed.planned, done: resumed.done === true } } };
4542
+ }
4543
+ case "mergepages": {
4544
+ const ids = Array.isArray(options.datasets) ? options.datasets.filter((item) => typeof item === "string" && item.trim().length > 0) : [];
4545
+ const records = [];
4546
+ for (const id2 of ids) records.push(await loaddataset(id2));
4547
+ const merged = mergedatasets(records);
4548
+ const id = randomid();
4549
+ const datasetvalue = { id, name: typeof options.name === "string" && options.name ? options.name : `merged-${step.id}`, columns: merged.columns, rows: merged.rows, sources: records.flatMap((record2) => record2.sources), at: Date.now() };
4550
+ await memory.setdataset(datasetvalue);
4551
+ await refreshbadge();
4552
+ return { ok: true, summary: `Merged ${records.length} dataset${records.length === 1 ? "" : "s"} into ${datasetvalue.name} with ${merged.columns.length} aligned column${merged.columns.length === 1 ? "" : "s"} and ${merged.rows.length} row${merged.rows.length === 1 ? "" : "s"}.`, details: { dataset: { id, name: datasetvalue.name, rows: merged.rows.length, columns: merged.columns.length }, merged: records.map((record2) => record2.id) } };
4553
+ }
4554
+ case "transformvalues": {
4555
+ const datasetvalue = await loaddataset(typeof options.dataset === "string" ? options.dataset : "");
4556
+ const rules = (Array.isArray(options.rules) ? options.rules : []).filter((item) => Boolean(item) && typeof item === "object" && !Array.isArray(item));
4557
+ const applied = transformrows(datasetvalue.rows, rules);
4558
+ await memory.setdataset({ ...datasetvalue, rows: applied.rows, at: Date.now() });
4559
+ const existing = (await memory.gettaskrules()).find((item) => item.taskid === plan.id);
4560
+ await memory.settaskrules(mergetaskrules(existing, plan.id, rules, [], Date.now()));
4561
+ if (applied.errors.length > 0) return { ok: false, summary: `Transform rules surfaced ${applied.errors.length} error${applied.errors.length === 1 ? "" : "s"}: ${applied.errors.join("; ")}.`, details: { errors: applied.errors, rules: rules.length } };
4562
+ return { ok: true, summary: `Applied ${rules.length} reviewed transform rule${rules.length === 1 ? "" : "s"} to ${applied.rows.length} row${applied.rows.length === 1 ? "" : "s"}.`, details: { rules: rules.length, rows: applied.rows.length } };
4563
+ }
4564
+ case "deduperows": {
4565
+ const datasetvalue = await loaddataset(typeof options.dataset === "string" ? options.dataset : "");
4566
+ const keys = Array.isArray(options.keys) ? options.keys.filter((item) => typeof item === "string" && item.trim().length > 0) : [];
4567
+ const result = dedupebykeys(datasetvalue.rows, keys);
4568
+ await memory.setdataset({ ...datasetvalue, rows: result.kept, at: Date.now() });
4569
+ const existing = (await memory.gettaskrules()).find((item) => item.taskid === plan.id);
4570
+ await memory.settaskrules(mergetaskrules(existing, plan.id, [], keys, Date.now()));
4571
+ return { ok: true, summary: `Deduplicated ${datasetvalue.rows.length} row${datasetvalue.rows.length === 1 ? "" : "s"} by ${keys.join(", ")}: removed ${result.removed} duplicate${result.removed === 1 ? "" : "s"}, kept ${result.kept.length}.`, details: { dedupe: { removed: result.removed, kept: result.kept.length, keys } } };
4572
+ }
4573
+ case "stamplerows": {
4574
+ const datasetvalue = await loaddataset(typeof options.dataset === "string" ? options.dataset : "");
4575
+ const url = typeof options.url === "string" && options.url ? options.url : origin;
4576
+ const stamped = samplerows(datasetvalue.rows, url, step.id, Date.now());
4577
+ await memory.setdataset({ ...datasetvalue, rows: stamped.rows, sources: stamped.sources, at: Date.now() });
4578
+ return { ok: true, summary: `Stamped ${stamped.rows.length} row${stamped.rows.length === 1 ? "" : "s"} with the source url, timestamp and step ref.`, details: { rows: stamped.rows.length, url } };
4579
+ }
4580
+ case "previewgrid": {
4581
+ const datasetvalue = await loaddataset(typeof options.dataset === "string" ? options.dataset : "");
4582
+ const sample = typeof options.sample === "number" && Number.isInteger(options.sample) && options.sample > 0 ? options.sample : 10;
4583
+ const preview = gridpreview(datasetvalue, sample);
4584
+ return { ok: true, summary: `Previewed ${preview.rows} row${preview.rows === 1 ? "" : "s"} of dataset ${datasetvalue.name} in the grid with ${preview.columns.length} column${preview.columns.length === 1 ? "" : "s"}.`, details: { preview } };
4585
+ }
4586
+ case "importcsv": {
4587
+ const csv = typeof options.csv === "string" ? options.csv : "";
4588
+ const parsed = parsecsv(csv, typeof options.delimiter === "string" && options.delimiter.length === 1 ? options.delimiter : ",");
4589
+ const mapping = options.mapping && typeof options.mapping === "object" && !Array.isArray(options.mapping) ? options.mapping : {};
4590
+ const columns = mapcolumns(parsed.headers, mapping);
4591
+ const rows = parsed.rows.map((line) => {
4592
+ const row = {};
4593
+ columns.forEach((column, index) => {
4594
+ row[column.key] = line[index] ?? "";
4595
+ });
4596
+ return row;
4597
+ });
4598
+ const id = randomid();
4599
+ const datasetvalue = { id, name: typeof options.name === "string" && options.name ? options.name : `import-${step.id}`, columns, rows, sources: [], at: Date.now() };
4600
+ await memory.addimport(datasetvalue);
4601
+ await refreshbadge();
4602
+ return { ok: true, summary: `Imported ${rows.length} row${rows.length === 1 ? "" : "s"} and ${columns.length} mapped column${columns.length === 1 ? "" : "s"} from the reviewed csv for fill loops.`, details: { dataset: { id, name: datasetvalue.name, rows: rows.length, columns: columns.length } } };
4603
+ }
4604
+ case "looprows": {
4605
+ const datasetvalue = await loaddataset(typeof options.dataset === "string" ? options.dataset : "");
4606
+ const inner = resolvedinnerstep(step, plan);
4607
+ if (!inner) throw new Error("A reviewed inner step or step id is required in options.");
4608
+ const variable = typeof options.variable === "string" && options.variable ? options.variable : "row";
4609
+ let completed = 0;
4610
+ let failed = 0;
4611
+ for (const [index, row] of datasetvalue.rows.entries()) {
4612
+ const derived = loopstep(inner, row);
4613
+ const output = await dispatchpagestep(derived, tabid2, origin, plan);
4614
+ if (output?.ok) completed += 1;
4615
+ else failed += 1;
4616
+ await memory.setprogress(recordoutcome(await memory.getprogress(), plan.id, { stepid: step.id, ok: Boolean(output?.ok), summary: `Iteration ${index + 1} of ${datasetvalue.rows.length}: ${output?.summary ?? "no result"}`, details: { iteration: index + 1, variable, variables: loopvariables(row) }, at: Date.now() }, Date.now()));
4617
+ }
4618
+ return { ok: failed === 0, summary: `Looped ${datasetvalue.rows.length} dataset row${datasetvalue.rows.length === 1 ? "" : "s"} as ${variable} variables: ${completed} iteration${completed === 1 ? "" : "s"} completed${failed > 0 ? `, ${failed} failed` : ""}.`, details: { iterations: datasetvalue.rows.length, completed, failed, variable } };
4619
+ }
4620
+ case "exportcsv":
4621
+ case "exportjson":
4622
+ case "exportexcel": {
4623
+ const datasetvalue = await loaddataset(typeof options.dataset === "string" ? options.dataset : "");
4624
+ const format = step.kind === "exportcsv" ? "csv" : step.kind === "exportjson" ? "json" : "excel";
4625
+ const delimiter = typeof options.delimiter === "string" && options.delimiter.length === 1 ? options.delimiter : ",";
4626
+ const artifact = await storeexport(step.id, { ...datasetvalue, ...typeof options.name === "string" && options.name ? { name: options.name } : {} }, format, delimiter, session, plan.id, origin);
4627
+ return { ok: true, summary: `Exported ${artifact.rowcount} row${artifact.rowcount === 1 ? "" : "s"} of dataset ${datasetvalue.name} to ${artifact.name} with checksum ${artifact.checksum}.`, details: { artifact: { id: artifact.id, name: artifact.name, kind: artifact.kind, checksum: artifact.checksum, rowcount: artifact.rowcount } } };
4628
+ }
4629
+ case "copytable": {
4630
+ const datasetvalue = await loaddataset(typeof options.dataset === "string" ? options.dataset : "");
4631
+ const content = exportcontent(datasetvalue, "csv", typeof options.delimiter === "string" && options.delimiter.length === 1 ? options.delimiter : ",");
4632
+ await navigator.clipboard.writeText(content);
4633
+ await audit("export", `Copied ${datasetvalue.rows.length} row${datasetvalue.rows.length === 1 ? "" : "s"} of dataset ${datasetvalue.name} to the clipboard under the clipboardWrite capability.`, extra);
4634
+ return { ok: true, summary: `Copied ${datasetvalue.rows.length} row${datasetvalue.rows.length === 1 ? "" : "s"} to the clipboard.`, details: { rows: datasetvalue.rows.length } };
4635
+ }
4636
+ case "pushsheets": {
4637
+ const datasetvalue = await loaddataset(typeof options.dataset === "string" ? options.dataset : "");
4638
+ const sheet = typeof options.sheet === "string" ? options.sheet : "";
4639
+ const config = (await memory.getsheetendpoints()).find((item) => item.endpoint === sheet || item.origin === new URL(sheet).origin);
4640
+ if (!config) throw new Error(`The reviewed sheet endpoint has not been configured; configure ${sheet} from the review panel first.`);
4641
+ const granted = await chrome.permissions.contains({ origins: [hostpattern(config.origin)] });
4642
+ if (!granted) throw new Error(`The sheet endpoint origin ${config.origin} has not received optional permission.`);
4643
+ const payload = sheetpayload(datasetvalue, sheet);
4644
+ const response = await fetch(config.endpoint, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(payload) });
4645
+ await audit("export", `Pushed ${datasetvalue.rows.length} row${datasetvalue.rows.length === 1 ? "" : "s"} of dataset ${datasetvalue.name} to the reviewed sheet endpoint ${config.origin} with response ${response.status}.`, extra);
4646
+ return { ok: response.ok, summary: response.ok ? `Pushed ${datasetvalue.rows.length} row${datasetvalue.rows.length === 1 ? "" : "s"} to the reviewed sheet endpoint ${config.origin}.` : `The reviewed sheet endpoint answered ${response.status}; the push failed.`, details: { endpoint: config.endpoint, status: response.status, rows: datasetvalue.rows.length } };
4647
+ }
4648
+ case "streamdisk": {
4649
+ const datasetvalue = await loaddataset(typeof options.dataset === "string" ? options.dataset : "");
4650
+ const chunk = typeof options.chunk === "number" && Number.isInteger(options.chunk) && options.chunk > 0 ? options.chunk : datasetvalue.rows.length || 1;
4651
+ const planchunks = chunkplan(datasetvalue.rows.length, chunk);
4652
+ const previous = (await memory.getstreams()).find((item) => item.datasetid === datasetvalue.id && item.done !== true);
4653
+ let state = previous && previous.chunks === planchunks.length ? previous : newstream(datasetvalue, planchunks.length, Date.now());
4654
+ let written = 0;
4655
+ let acknowledged = 0;
4656
+ for (const part of planchunks.slice(state.chunk)) {
4657
+ written += 1;
4658
+ if (backpressure(written, acknowledged)) await new Promise((resolve) => setTimeout(resolve, 0));
4659
+ state = advancestream(state, part, Date.now(), part.to >= datasetvalue.rows.length);
4660
+ await memory.setstream(state);
4661
+ acknowledged += 1;
4662
+ }
4663
+ const artifact = await storeexport(step.id, datasetvalue, "csv", ",", session, plan.id, origin);
4664
+ await audit("stream", `Streamed ${datasetvalue.rows.length} row${datasetvalue.rows.length === 1 ? "" : "s"} of dataset ${datasetvalue.name} to disk in ${planchunks.length} reviewed chunk${planchunks.length === 1 ? "" : "s"} of ${chunk} row${chunk === 1 ? "" : "s"} with backpressure; the stream state stays persisted for resume.`, extra);
4665
+ return { ok: true, summary: `Streamed ${datasetvalue.rows.length} row${datasetvalue.rows.length === 1 ? "" : "s"} to disk in ${planchunks.length} chunk${planchunks.length === 1 ? "" : "s"} and landed the artifact ${artifact.name}.`, details: { chunks: planchunks.length, chunk, rows: datasetvalue.rows.length, artifact: { id: artifact.id, name: artifact.name, checksum: artifact.checksum } } };
4666
+ }
4667
+ case "logprovenance": {
4668
+ const reference = typeof options.artifact === "string" ? options.artifact : "";
4669
+ const artifact = (await memory.getexports()).find((item) => item.id === reference || item.name === reference);
4670
+ if (!artifact) throw new Error(`No exported artifact ${reference} exists yet.`);
4671
+ const record2 = provenancefor(artifact, origin, step.id, Date.now());
4672
+ await memory.addprovenance(record2);
4673
+ await audit("provenance", `Provenance of artifact ${artifact.name}: rows ${record2.rowstart} to ${record2.rowend}, checksum ${record2.checksum}, source ${record2.url}.`, extra);
4674
+ return { ok: true, summary: `Logged the provenance of ${artifact.name} for audit: rows ${record2.rowstart} to ${record2.rowend} with checksum ${record2.checksum}.`, details: { provenance: record2 } };
4675
+ }
4676
+ default:
4677
+ return { ok: false, summary: "Unsupported forms and data step." };
4678
+ }
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
+ });
3588
4944
  async function enforcewindowreview(step, session, plan) {
3589
4945
  const windowid = step.value && /^\d+$/.test(step.value) ? Number.parseInt(step.value, 10) : 0;
3590
4946
  const progress = plan ? await memory.getprogress() : void 0;
@@ -3620,8 +4976,11 @@ async function refreshbadge() {
3620
4976
  const queues = await memory.getnavqueues();
3621
4977
  const badges = await memory.getbadges();
3622
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;
4981
+ const datasets = (await memory.getdatasets()).length;
3623
4982
  const tasktabs2 = new Set(badges.map((badge) => badge.tabid)).size;
3624
- const total = (queues?.prefetch ?? 0) + (queues?.batchopen ?? 0) + tasktabs2 + prompts;
4983
+ const total = (queues?.prefetch ?? 0) + (queues?.batchopen ?? 0) + tasktabs2 + prompts + consents + quarantined + datasets;
3625
4984
  await chrome.action.setBadgeText({ text: total > 0 ? String(total) : "" }).catch(() => {
3626
4985
  });
3627
4986
  }
@@ -3645,6 +5004,10 @@ async function executestep(stepid) {
3645
5004
  }
3646
5005
  if (istabscommandkind(step.kind)) {
3647
5006
  output = await executetabscommand(step, session, plan, tab.id);
5007
+ } else if (isdatasetkind(step.kind)) {
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);
3648
5011
  } else if (isformkind(step.kind)) {
3649
5012
  output = await executeformstep(step, session, plan, tab.id, origin);
3650
5013
  } else if (isbrowserkind(step.kind)) {
@@ -3749,9 +5112,16 @@ async function grantcapability(permission) {
3749
5112
  if (!["tabs", "downloads", "clipboardRead", "clipboardWrite"].includes(permission)) throw new Error("Unknown capability.");
3750
5113
  const granted = await chrome.permissions.request({ permissions: [permission] });
3751
5114
  if (!granted) throw new Error("The capability grant was declined.");
5115
+ if (permission === "downloads") installmimelistener();
3752
5116
  await audit("capability", `Capability ${permission} granted by the user.`);
3753
5117
  return refreshcapabilities();
3754
5118
  }
5119
+ async function extractionreportValue() {
5120
+ return extractionreport({ sessions: await memory.getextractsessions() });
5121
+ }
5122
+ async function provenancereportValue() {
5123
+ return provenancereport({ records: await memory.getprovenances() });
5124
+ }
3755
5125
  async function handlerequest(message, sender) {
3756
5126
  if (!extensionpage(sender)) throw new Error("Requests are accepted only from Devthink extension pages.");
3757
5127
  const input = message;
@@ -3804,13 +5174,39 @@ async function handlerequest(message, sender) {
3804
5174
  const captchas = await memory.getcaptchas();
3805
5175
  const detections = await memory.getdetections();
3806
5176
  const codeentry = await memory.getcodevalue();
5177
+ const datasets = await memory.getdatasets();
5178
+ const imports = await memory.getimports();
5179
+ const extractsessions = await memory.getextractsessions();
5180
+ const streams = await memory.getstreams();
5181
+ const exports = (await memory.getexports()).map((artifact) => ({ id: artifact.id, kind: artifact.kind, name: artifact.name, stepid: artifact.stepid, rowcount: artifact.rowcount, checksum: artifact.checksum, at: artifact.at }));
5182
+ const provenances = await memory.getprovenances();
5183
+ const taskrules = await memory.gettaskrules();
5184
+ const sheetendpoints = await memory.getsheetendpoints();
5185
+ const sheetgrants = [];
5186
+ for (const config of sheetendpoints) {
5187
+ sheetgrants.push({ ...config, granted: await chrome.permissions.contains({ origins: [hostpattern(config.origin)] }).catch(() => false) });
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
+ }
3807
5203
  const clones = clonetabs(tabs);
3808
5204
  const taskgauge = tasktabgauge(tabs.filter((tab) => badges.some((badge) => badge.tabid === tab.tabid)).length, tasktabceiling(await memory.getsettings()));
3809
5205
  const report = await buildtabreport(tabs);
3810
5206
  const livetab = session ? await chrome.tabs.get(session.tabid).catch(() => void 0) : void 0;
3811
5207
  const waitprofile = session ? waitprofiles.find((record2) => record2.origin === session.origin) : void 0;
3812
5208
  const livestate = { phase: livetab?.status === "loading" ? "loading" : "complete", ...navrecords[0] ? { finalurl: navrecords[0].finalurl, redirects: navrecords[0].chain } : {} };
3813
- 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 } : {} };
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 };
3814
5210
  }
3815
5211
  case "capabilities":
3816
5212
  return refreshcapabilities();
@@ -4086,6 +5482,136 @@ async function handlerequest(message, sender) {
4086
5482
  if (!report) throw new Error("No error report has been collected yet.");
4087
5483
  return JSON.parse(errorreportresponse({ report, plan }));
4088
5484
  }
5485
+ case "configuresheet": {
5486
+ const inputsheet = message;
5487
+ const config = normalizeendpoint(inputsheet.endpoint ?? "");
5488
+ const granted = await chrome.permissions.contains({ origins: [hostpattern(config.origin)] });
5489
+ if (!granted) throw new Error("The sheet endpoint origin has not received optional permission.");
5490
+ const record2 = { endpoint: config.endpoint, origin: config.origin, configuredat: Date.now() };
5491
+ await memory.setsheetendpoint(record2);
5492
+ await audit("configure", `Configured the reviewed sheet endpoint ${config.origin} for data pushes; pushes need the explicit reviewed flag.`);
5493
+ return record2;
5494
+ }
5495
+ case "exportdataset": {
5496
+ const session = await memory.getsession();
5497
+ if (!session || session.stoppedat || session.expiresat <= Date.now()) throw new Error("Data exports stay behind the consent gate of an active session.");
5498
+ const { tab, origin } = await activecontext();
5499
+ const gate = exportgranted(session, origin);
5500
+ if (!gate.allowed) throw new Error(gate.reason);
5501
+ const inputexport = message;
5502
+ const format = inputexport.format === "json" ? "json" : inputexport.format === "excel" ? "excel" : "csv";
5503
+ const datasetvalue = await loaddataset(inputexport.datasetid ?? "");
5504
+ const artifact = await storeexport("panel", { ...datasetvalue, ...inputexport.name?.trim() ? { name: inputexport.name.trim() } : {} }, format, ",", session, "", origin);
5505
+ void tab;
5506
+ return { id: artifact.id, kind: artifact.kind, name: artifact.name, rowcount: artifact.rowcount, checksum: artifact.checksum, at: artifact.at };
5507
+ }
5508
+ case "importcsv": {
5509
+ const session = await memory.getsession();
5510
+ if (!session || session.stoppedat || session.expiresat <= Date.now()) throw new Error("Csv imports stay behind the consent gate of an active session.");
5511
+ const inputimport = message;
5512
+ const parsed = parsecsv(inputimport.csv ?? "");
5513
+ if (parsed.headers.length === 0) throw new Error("The reviewed csv needs a header line.");
5514
+ const columns = mapcolumns(parsed.headers, inputimport.mapping ?? {});
5515
+ const rows = parsed.rows.map((line) => {
5516
+ const row = {};
5517
+ columns.forEach((column, index) => {
5518
+ row[column.key] = line[index] ?? "";
5519
+ });
5520
+ return row;
5521
+ });
5522
+ const datasetvalue = { id: randomid(), name: inputimport.name?.trim() || `import-${Date.now()}`, columns, rows, sources: [], at: Date.now() };
5523
+ await memory.addimport(datasetvalue);
5524
+ await audit("scrape", `The review panel imported ${rows.length} row${rows.length === 1 ? "" : "s"} from the reviewed csv as dataset ${datasetvalue.name} for fill loops.`, { sessionid: session.id });
5525
+ await refreshbadge();
5526
+ return { id: datasetvalue.id, name: datasetvalue.name, rows: rows.length, columns: columns.length };
5527
+ }
5528
+ case "dataset": {
5529
+ const plan = await memory.getplan();
5530
+ if (!plan) throw new Error("No plan is available for a dataset envelope.");
5531
+ const inputdataset = message;
5532
+ const datasetvalue = await memory.getdataset(inputdataset.datasetid ?? "");
5533
+ if (!datasetvalue) throw new Error("No dataset has been captured yet.");
5534
+ return JSON.parse(datasetresponse({ dataset: datasetvalue, plan, ...typeof inputdataset.sample === "number" ? { sample: inputdataset.sample } : {} }));
5535
+ }
5536
+ case "extraction":
5537
+ return extractionreportValue();
5538
+ case "provenance":
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
+ }
4089
5615
  case "stop": {
4090
5616
  const session = await memory.getsession();
4091
5617
  if (session) await memory.setsession({ ...session, stoppedat: Date.now() });