@wenathlan/extension 1.1.39 → 1.1.41

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.
@@ -731,24 +731,136 @@ var sessionmemory = class {
731
731
  async getmimefilters() {
732
732
  return await this.adapter.get("mimefilters") ?? [];
733
733
  }
734
+ /** Stores one capture record with its bytes and step linkage, replacing the previous record of that id; the user configured capture retention window expires the oldest bytes while the metadata always survives for the audit trail. */
735
+ async addcapture(record2) {
736
+ const records = await this.getcaptures();
737
+ const retention = (await this.getsettings())?.captureretention;
738
+ const combined = [record2, ...records.filter((item) => item.id !== record2.id)];
739
+ const stored = retention === void 0 ? combined : combined.map((item, index) => index < retention ? item : expirecapturebytes(item));
740
+ await this.adapter.set("captures", stored);
741
+ }
742
+ /** Returns every stored capture record with its metadata, newest first. */
743
+ async getcaptures() {
744
+ return await this.adapter.get("captures") ?? [];
745
+ }
746
+ /** Returns one capture record with its bytes by its id. */
747
+ async getcapture(id) {
748
+ return (await this.getcaptures()).find((item) => item.id === id);
749
+ }
750
+ /** Returns the capture records filtered by run, step and kind. */
751
+ async listcaptures(filter) {
752
+ const records = await this.getcaptures();
753
+ return records.filter((item) => (filter.runid === void 0 || item.runid === filter.runid) && (filter.stepid === void 0 || item.stepid === filter.stepid) && (filter.kind === void 0 || item.kind === filter.kind));
754
+ }
755
+ /** Records one before and after shotpair of the run with its action context. */
756
+ async addpair(pair) {
757
+ const records = await this.getpairs();
758
+ await this.adapter.set("capturepairs", [pair, ...records.filter((item) => item.id !== pair.id)]);
759
+ }
760
+ /** Returns the shotpairs of one run resolved through their before records, newest first; an absent run returns every pair. */
761
+ async getpairs(runid) {
762
+ const records = await this.adapter.get("capturepairs") ?? [];
763
+ if (runid === void 0) return records;
764
+ const runs = /* @__PURE__ */ new Map();
765
+ for (const capture of await this.getcaptures()) runs.set(capture.id, capture.runid);
766
+ return records.filter((item) => runs.get(item.beforeid) === runid);
767
+ }
768
+ /** Stores one media record of the 1.1.41 family with its bytes and step linkage, replacing the previous record of that id; the user configured media retention window expires the oldest bytes while the metadata and the recording index always survive. */
769
+ async addmedia(record2) {
770
+ const records = await this.getmediarecords();
771
+ const retention = (await this.getsettings())?.mediaretention;
772
+ const combined = [record2, ...records.filter((item) => item.id !== record2.id)];
773
+ const stored = retention === void 0 ? combined : combined.map((item, index) => index < retention ? item : expiremediabytes(item));
774
+ await this.adapter.set("media", stored);
775
+ }
776
+ /** Returns every stored media record, newest first. */
777
+ async getmediarecords() {
778
+ return await this.adapter.get("media") ?? [];
779
+ }
780
+ /** Returns the media records filtered by run and kind; an absent filter returns every record. */
781
+ async listmedia(filter) {
782
+ const records = await this.getmediarecords();
783
+ return records.filter((item) => (filter.runid === void 0 || item.runid === filter.runid) && (filter.kind === void 0 || mediakindof(item) === filter.kind));
784
+ }
785
+ /** Returns one media record by its id. */
786
+ async getmediarecord(id) {
787
+ return (await this.getmediarecords()).find((item) => item.id === id);
788
+ }
789
+ /** Returns one recording with its file reference and frame index by its id. */
790
+ async getrecording(id) {
791
+ const found = await this.getmediarecord(id);
792
+ return found !== void 0 && "startedat" in found ? found : void 0;
793
+ }
794
+ /** Removes one media record by its id; the audit trail keeps its outcome evidence. */
795
+ async removemedia(id) {
796
+ await this.adapter.set("media", (await this.getmediarecords()).filter((item) => item.id !== id));
797
+ }
798
+ /** Stores one observed image batch of a downloadimages step, replacing the previous batch of that id. */
799
+ async addimagebatch(batch) {
800
+ const records = await this.adapter.get("imagebatches") ?? [];
801
+ await this.adapter.set("imagebatches", [batch, ...records.filter((item) => item.id !== batch.id)]);
802
+ }
803
+ /** Returns every observed image batch with its filter match counts, newest first. */
804
+ async getimagebatches() {
805
+ return await this.adapter.get("imagebatches") ?? [];
806
+ }
807
+ /** Stores one recording consent decision of an origin, replacing the previous record of that id. */
808
+ async setrecordingconsent(record2) {
809
+ const records = (await this.adapter.get("recordingconsents") ?? []).filter((item) => item.id !== record2.id);
810
+ await this.adapter.set("recordingconsents", [record2, ...records]);
811
+ }
812
+ /** Returns every recording consent decision with its prompt and origin, newest first. */
813
+ async getrecordingconsents() {
814
+ return await this.adapter.get("recordingconsents") ?? [];
815
+ }
734
816
  };
817
+ function mediakindof(record2) {
818
+ if ("pages" in record2) return "pdf";
819
+ if ("startedat" in record2) return "recording";
820
+ if ("timestamp" in record2) return "frame";
821
+ if ("context" in record2) return "canvas";
822
+ if ("tracks" in record2) return "stream";
823
+ return "asset";
824
+ }
825
+ function expiremediabytes(record2) {
826
+ if ("dataurl" in record2) {
827
+ const source = record2;
828
+ const copy = { ...source };
829
+ delete copy.dataurl;
830
+ return { ...copy, bytesexpired: true };
831
+ }
832
+ if ("startedat" in record2) {
833
+ const source = record2;
834
+ const copy = { ...source };
835
+ delete copy.bytes;
836
+ return { ...copy, bytesexpired: true };
837
+ }
838
+ return record2;
839
+ }
840
+ function expirecapturebytes(record2) {
841
+ const { bytes, ...metadata } = record2;
842
+ void bytes;
843
+ return { ...metadata, bytesexpired: true };
844
+ }
735
845
  function randomid() {
736
846
  return crypto.randomUUID();
737
847
  }
738
848
 
739
849
  // policy.ts
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"]);
850
+ var sensitiveactions = /* @__PURE__ */ new Set(["click", "type", "navigate", "select", "presskey", "drag", "drop", "upload", "clear", "check", "uncheck", "toggle", "submit", "reload", "back", "forward", "writestorage", "setattribute", "removeattribute", "evaluate", "tabcreate", "tabactivate", "tabclose", "tabreload", "windowcreate", "windowclose", "windowresize", "downloadfile", "clickpoint", "shiftclick", "dismissdialog", "enterframe", "typetime", "appendtext", "setvalue", "typeedit", "keyhold", "keyrelease", "submitsearch", "selectmulti", "chooseradio", "setslider", "setdate", "setcolor", "openlink", "openprivate", "reloadcache", "stopnav", "followlink", "spanav", "rewritequery", "setfragment", "navlist", "navprofile", "handleauth", "printpdf", "prefetch", "preconnect", "deeplink", "reopentab", "pausenav", "navrate", "openclipboard", "batchopen", "duplicatetab", "closepattern", "pintab", "mutetab", "movetab", "movetabwindow", "grouptabs", "colorgroup", "collapsegroup", "discardtab", "reloadtabs", "zoomin", "zoomout", "switchtab", "maximizewindow", "minimizewindow", "restorewindow", "focuswindow", "scratchwindow", "incognitowindow", "restoretab", "restorelayout", "reopenrun", "badgetab", "fillform", "filllabel", "fillplaceholder", "submitform", "retryform", "runwizard", "selectchain", "picktypeahead", "pickdate", "attachfile", "fillcard", "fillcode", "consentpassword", "exportcsv", "exportjson", "exportexcel", "copytable", "pushsheets", "streamdisk", "paginateextract", "resumeextract", "batchdownload", "pausedownload", "resumedownload", "interceptmime", "readclipboard", "writeclipboard", "copyscreen", "quarantinedownload", "scanvirus", "cleanupartifacts", "recordscreen", "captureaudio", "downloadimages"]);
741
851
  var interactionactions = /* @__PURE__ */ new Set(["focus", "scroll", "hover", "clickdeep", "rightclick", "doubleclick", "scrollpage", "scrollby", "scrollend", "scrolltop", "fullscreen", "zoomset", "movepointer", "clicktext", "clickaria", "clickname", "expanddetails", "pierceshadow", "retryaction"]);
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"]);
852
+ var readactions = /* @__PURE__ */ new Set(["observe", "inspect", "extract", "wait", "waitfor", "waittext", "readattribute", "readstyle", "readgeometry", "readvalue", "readtext", "readhtml", "countelements", "readtable", "readlinks", "readimages", "readmeta", "readforms", "readstorage", "highlight", "tablist", "windowlist", "tabsnapshot", "mapclicks", "verifyvisible", "verifyenabled", "resolvexpath", "a11ytree", "readvisible", "readertree", "detectlists", "detecttables", "readjson", "watchmutate", "waitquiet", "watchbanner", "detectinfinitescroll", "detectvirtual", "detectlazy", "readscrollpos", "readlang", "readoutline", "countpages", "listshadow", "listframes", "classifypage", "fingerprintsection", "diffsnapshots", "readselection", "watchfocus", "detectsticky", "detectscrolllock", "readopengraph", "detectlanguage", "deriveselector", "waitload", "waiturl", "spawait", "detecthttp", "readredirects", "readfinalurl", "trailaudit", "navintent", "checksafe", "querytabs", "watchtab", "findclones", "searchtabs", "listaudio", "snapshotsession", "savelayout", "attachmeta", "detectfields", "generatevalues", "saveprofiles", "asksubmit", "readerrors", "skiphoneypot", "detectlogin", "detecttemplate", "handoffcaptcha", "scrapetable", "importcsv", "looprows", "transformvalues", "deduperows", "mergepages", "stamplerows", "previewgrid", "logprovenance", "verifydownload", "exportnetlog", "namecaptures", "shotview", "shotfullpage", "shotelement", "shotregion", "contactsheet", "capturepdf", "captureframe", "readmedia", "readassets", "probestream", "timelapse", "shotcanvas", "convertimage", "makethumbs"]);
743
853
  var allowedactions = /* @__PURE__ */ new Set([...sensitiveactions, ...interactionactions, ...readactions]);
744
854
  var watchactions = /* @__PURE__ */ new Set(["watchmutate", "watchbanner", "watchfocus", "watchtab"]);
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"]);
855
+ var targetactions = /* @__PURE__ */ new Set(["inspect", "focus", "click", "type", "scroll", "select", "hover", "clickdeep", "rightclick", "doubleclick", "drag", "drop", "upload", "clear", "check", "uncheck", "toggle", "submit", "readattribute", "readstyle", "readgeometry", "readvalue", "readtext", "readhtml", "countelements", "readtable", "highlight", "setattribute", "removeattribute", "waitfor", "shiftclick", "typetime", "appendtext", "setvalue", "typeedit", "submitsearch", "selectmulti", "chooseradio", "setslider", "setdate", "setcolor", "expanddetails", "verifyvisible", "verifyenabled", "pierceshadow", "deriveselector", "fingerprintsection", "submitform", "retryform", "selectchain", "picktypeahead", "pickdate", "attachfile", "fillcode", "consentpassword", "scrapetable", "paginateextract", "shotelement", "captureframe", "shotcanvas"]);
746
856
  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"]);
747
857
  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"]);
748
858
  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
859
  var datasetactions = /* @__PURE__ */ new Set(["scrapetable", "exportcsv", "exportjson", "exportexcel", "copytable", "pushsheets", "importcsv", "looprows", "transformvalues", "deduperows", "paginateextract", "mergepages", "stamplerows", "previewgrid", "streamdisk", "resumeextract", "logprovenance"]);
750
860
  var exportactions = /* @__PURE__ */ new Set(["exportcsv", "exportjson", "exportexcel", "copytable", "pushsheets", "streamdisk"]);
751
861
  var filesactions = /* @__PURE__ */ new Set(["batchdownload", "pausedownload", "resumedownload", "verifydownload", "interceptmime", "exportnetlog", "readclipboard", "writeclipboard", "copyscreen", "quarantinedownload", "scanvirus", "namecaptures", "cleanupartifacts"]);
862
+ var captureactions = /* @__PURE__ */ new Set(["shotview", "shotfullpage", "shotelement", "shotregion", "contactsheet"]);
863
+ var mediaactions = /* @__PURE__ */ new Set(["capturepdf", "recordscreen", "captureaudio", "captureframe", "downloadimages", "shotcanvas", "probestream", "readmedia", "readassets", "timelapse", "convertimage", "makethumbs"]);
752
864
  var fieldkinds = ["text", "email", "phone", "date", "number", "select", "check", "radio", "file", "password", "card", "code"];
753
865
  var layoutmutationactions = /* @__PURE__ */ new Set(["grouptabs", "colorgroup", "collapsegroup", "savelayout", "restorelayout"]);
754
866
  var groupcolors = ["grey", "blue", "red", "yellow", "green", "pink", "purple", "cyan", "orange"];
@@ -787,6 +899,7 @@ function requiredcapability(kind) {
787
899
  if (kind === "batchdownload" || kind === "pausedownload" || kind === "resumedownload" || kind === "verifydownload" || kind === "interceptmime" || kind === "quarantinedownload" || kind === "scanvirus") return "downloads";
788
900
  if (kind === "readclipboard") return "clipboardRead";
789
901
  if (kind === "writeclipboard" || kind === "copyscreen") return "clipboardWrite";
902
+ if (kind === "downloadimages") return "downloads";
790
903
  if (kind === "openlink" || kind === "openprivate" || kind === "navlist" || kind === "batchopen" || kind === "reopentab" || kind === "deeplink") return "tabs";
791
904
  if (tabscommandactions.has(kind)) return "tabs";
792
905
  return void 0;
@@ -809,6 +922,92 @@ function isexportkind(kind) {
809
922
  function isfileskind(kind) {
810
923
  return filesactions.has(kind);
811
924
  }
925
+ function iscapturekind(kind) {
926
+ return captureactions.has(kind);
927
+ }
928
+ function capturegate(session, tabid2, origin, now) {
929
+ if (!session || session.stoppedat) return { allowed: false, reason: "No active browser session exists for the capture." };
930
+ if (session.expiresat <= now) return { allowed: false, reason: "The browser session has expired and cannot capture." };
931
+ if (session.pausedat) return { allowed: false, reason: "The browser session is paused and cannot capture." };
932
+ if (session.tabid !== tabid2) return { allowed: false, reason: `The capture needs the active tab grant of session tab ${session.tabid} and refuses tab ${tabid2}.` };
933
+ if (!origingranted(session, origin)) return { allowed: false, reason: `The capture of ${origin} needs the session origin grants first.` };
934
+ return { allowed: true };
935
+ }
936
+ function validatecaptureoptions(value) {
937
+ if (value === void 0) return { allowed: true };
938
+ if (!value || typeof value !== "object" || Array.isArray(value)) return { allowed: false, reason: "The reviewed capture options must be an object in options.capture." };
939
+ const options = value;
940
+ if (options.format !== void 0 && options.format !== "png" && options.format !== "jpeg" && options.format !== "webp") return { allowed: false, reason: "The reviewed capture format must be png, jpeg or webp." };
941
+ if (options.quality !== void 0 && (typeof options.quality !== "number" || !Number.isFinite(options.quality) || options.quality < 0 || options.quality > 100)) return { allowed: false, reason: "The reviewed capture quality must stay between zero and one hundred; any value in that range is the user choice with no code cap." };
942
+ if (options.pixelratio !== void 0 && (typeof options.pixelratio !== "number" || !Number.isFinite(options.pixelratio) || options.pixelratio < 1)) return { allowed: false, reason: "The reviewed pixel ratio starts at one and climbs to any user configured ceiling with no code ceiling." };
943
+ if (options.annotate !== void 0 && typeof options.annotate !== "boolean") return { allowed: false, reason: "The reviewed capture annotation flag must be a boolean." };
944
+ if (options.exporttarget !== void 0 && options.exporttarget !== "memory" && options.exporttarget !== "download" && options.exporttarget !== "clipboard") return { allowed: false, reason: "The reviewed capture export target must be memory, download or clipboard." };
945
+ return { allowed: true };
946
+ }
947
+ function validateregionrect(value) {
948
+ if (!value || typeof value !== "object" || Array.isArray(value)) return { allowed: false, reason: "A reviewed regionrect with x, y, width and height in css pixels is required in options." };
949
+ const rect = value;
950
+ for (const field of ["x", "y", "width", "height"]) {
951
+ if (typeof rect[field] !== "number" || !Number.isFinite(rect[field])) return { allowed: false, reason: `The reviewed regionrect needs a numeric ${field} in css pixels.` };
952
+ }
953
+ if (rect.x < 0 || rect.y < 0) return { allowed: false, reason: "The reviewed regionrect refuses negative coordinates." };
954
+ if (rect.width <= 0 || rect.height <= 0) return { allowed: false, reason: "The reviewed regionrect needs positive width and height values." };
955
+ return { allowed: true };
956
+ }
957
+ function validatecapturenaming(value) {
958
+ if (!value || typeof value !== "object" || Array.isArray(value)) return { allowed: false, reason: "A reviewed capturenaming rule with run, step, sequence and kind flags is required." };
959
+ const rule = value;
960
+ const segments = ["run", "step", "sequence", "kind"];
961
+ for (const key of Object.keys(rule)) {
962
+ if (!segments.includes(key)) return { allowed: false, reason: `The reviewed capturenaming rule refuses the unknown ${key} segment; only run, step, sequence and kind participate.` };
963
+ }
964
+ for (const segment of segments) {
965
+ if (rule[segment] !== void 0 && typeof rule[segment] !== "boolean") return { allowed: false, reason: `The reviewed capturenaming ${segment} flag must be a boolean.` };
966
+ }
967
+ if (!segments.some((segment) => rule[segment] === true)) return { allowed: false, reason: "The reviewed capturenaming rule needs at least one enabled segment of run, step, sequence and kind." };
968
+ return { allowed: true };
969
+ }
970
+ function stitchbudgetallowed(tiles, settle2, wait) {
971
+ if (tiles <= 0) return { allowed: false, reason: "The stitch budget needs at least one tile." };
972
+ if (settle2 < 0 || wait < 0) return { allowed: false, reason: "The reviewed settle and wait windows must be zero or positive milliseconds." };
973
+ if (tiles * settle2 > wait) return { allowed: false, reason: `The stitching scroll budget of ${tiles} tiles at ${settle2} milliseconds exceeds the reviewed wait window of ${wait} milliseconds; review a wider window or a smaller settle.` };
974
+ return { allowed: true };
975
+ }
976
+ function beforeafterwrapallowed(kind) {
977
+ return allowedactions.has(kind) && !captureactions.has(kind);
978
+ }
979
+ function validatecapturegrammar(step, options) {
980
+ const kind = step.kind;
981
+ const optioncheck = validatecaptureoptions(options.capture);
982
+ if (!optioncheck.allowed) return optioncheck;
983
+ if (options.settle !== void 0 && (typeof options.settle !== "number" || !Number.isFinite(options.settle) || options.settle < 0)) return { allowed: false, reason: "The reviewed capture settle window must be zero or a positive number of milliseconds." };
984
+ if (options.overlap !== void 0 && (typeof options.overlap !== "number" || !Number.isInteger(options.overlap) || options.overlap < 0)) return { allowed: false, reason: "The reviewed stitch overlap must be zero or a positive number of rows." };
985
+ if (options.wait !== void 0 && (typeof options.wait !== "number" || !Number.isFinite(options.wait) || options.wait < 0)) return { allowed: false, reason: "The reviewed capture wait window must be zero or a positive number of milliseconds." };
986
+ if (options.naming !== void 0) {
987
+ const namingcheck = validatecapturenaming(options.naming);
988
+ if (!namingcheck.allowed) return namingcheck;
989
+ }
990
+ if (kind === "shotregion") {
991
+ const rectcheck = validateregionrect(options.regionrect);
992
+ if (!rectcheck.allowed) return rectcheck;
993
+ if (options.reviewed !== true) return { allowed: false, reason: "Every reviewed regionrect needs the explicit reviewed flag before shotregion runs." };
994
+ if (options.container !== void 0 && !isnonempty(options.container)) return { allowed: false, reason: "The reviewed scrollable container selector must be a non-empty string." };
995
+ if (options.steps !== void 0 && (typeof options.steps !== "number" || !Number.isInteger(options.steps) || options.steps < 1)) return { allowed: false, reason: "The reviewed container scroll steps must be a positive integer with no code ceiling." };
996
+ }
997
+ if (kind === "contactsheet") {
998
+ const elements = options.elements;
999
+ if (!Array.isArray(elements) || elements.length === 0 || !elements.every((item) => isnonempty(item))) return { allowed: false, reason: "A reviewed non-empty list of element selectors is required in options for the contact sheet; the cell count stays the user choice." };
1000
+ const layout = options.sheet;
1001
+ if (layout !== void 0) {
1002
+ if (!layout || typeof layout !== "object" || Array.isArray(layout)) return { allowed: false, reason: "The reviewed sheetlayout must be an object with cellsize, columns and label." };
1003
+ const sheet = layout;
1004
+ if (typeof sheet.cellsize !== "number" || !Number.isFinite(sheet.cellsize) || sheet.cellsize <= 0) return { allowed: false, reason: "The reviewed contact sheet cell size must be a positive number of pixels." };
1005
+ if (typeof sheet.columns !== "number" || !Number.isInteger(sheet.columns) || sheet.columns < 1) return { allowed: false, reason: "The reviewed contact sheet column count must be a positive integer with no code ceiling." };
1006
+ if (sheet.label !== void 0 && sheet.label !== "none" && sheet.label !== "index" && sheet.label !== "selector" && sheet.label !== "both") return { allowed: false, reason: "The reviewed contact sheet label style must be none, index, selector or both." };
1007
+ }
1008
+ }
1009
+ return { allowed: true };
1010
+ }
812
1011
  function exportgranted(session, origin) {
813
1012
  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
1013
  return { allowed: true };
@@ -1388,6 +1587,144 @@ function validatetabsgrammar(step, options) {
1388
1587
  if (kind === "reopenrun" && !isnonempty(options.run)) return { allowed: false, reason: "A reviewed run id is required in options to reopen its tabs." };
1389
1588
  return { allowed: true };
1390
1589
  }
1590
+ function ismediakind(kind) {
1591
+ return mediaactions.has(kind);
1592
+ }
1593
+ function isrecordingkind(kind) {
1594
+ return kind === "recordscreen" || kind === "captureaudio";
1595
+ }
1596
+ function mediagate(session, tabid2, origin, now) {
1597
+ if (!session || session.stoppedat) return { allowed: false, reason: "No active browser session exists for the media capture." };
1598
+ if (session.expiresat <= now) return { allowed: false, reason: "The browser session has expired and cannot capture media." };
1599
+ if (session.pausedat) return { allowed: false, reason: "The browser session is paused and cannot capture media." };
1600
+ if (session.tabid !== tabid2) return { allowed: false, reason: `The media capture needs the active tab grant of session tab ${session.tabid} and refuses tab ${tabid2}.` };
1601
+ if (!origingranted(session, origin)) return { allowed: false, reason: `The media capture of ${origin} needs the session origin grants first.` };
1602
+ return { allowed: true };
1603
+ }
1604
+ function recordingconsentgranted(step) {
1605
+ let options = {};
1606
+ try {
1607
+ options = parseoptions(step);
1608
+ } catch {
1609
+ options = {};
1610
+ }
1611
+ const consentref = options.consentref;
1612
+ if (typeof consentref !== "string" || !consentref.trim()) return { allowed: false, reason: "A recording of user activity requires a reviewed consent ref in options before it starts." };
1613
+ return { allowed: true };
1614
+ }
1615
+ function recordingwindow(settings) {
1616
+ const window2 = settings?.recordingwindow;
1617
+ return typeof window2 === "number" && Number.isFinite(window2) && window2 > 0 ? window2 : void 0;
1618
+ }
1619
+ function lapsebudgetallowed(interval, duration, wait) {
1620
+ if (!(interval > 0)) return { allowed: false, reason: "The reviewed lapse interval must be a positive number of milliseconds." };
1621
+ if (!(duration > 0)) return { allowed: false, reason: "The reviewed lapse duration must be a positive number of milliseconds." };
1622
+ if (wait !== void 0 && !(wait >= 0)) return { allowed: false, reason: "The reviewed wait budget must be zero or a positive number of milliseconds." };
1623
+ if (wait !== void 0 && duration > wait) return { allowed: false, reason: `The lapse duration of ${duration} milliseconds exceeds the reviewed wait budget of ${wait} milliseconds; review a wider budget or a shorter duration.` };
1624
+ return { allowed: true };
1625
+ }
1626
+ function validatemediagrammar(step, options) {
1627
+ const kind = step.kind;
1628
+ if (kind === "capturepdf") {
1629
+ const pdf = options.pdf;
1630
+ if (pdf !== void 0) {
1631
+ if (!pdf || typeof pdf !== "object" || Array.isArray(pdf)) return { allowed: false, reason: "The reviewed pdf options must be an object in options.pdf." };
1632
+ const pdfoptions = pdf;
1633
+ if (pdfoptions.paperwidth !== void 0 && (typeof pdfoptions.paperwidth !== "number" || !Number.isFinite(pdfoptions.paperwidth) || pdfoptions.paperwidth <= 0)) return { allowed: false, reason: "The reviewed pdf paper width must be a positive number of inches with no code cap." };
1634
+ if (pdfoptions.paperheight !== void 0 && (typeof pdfoptions.paperheight !== "number" || !Number.isFinite(pdfoptions.paperheight) || pdfoptions.paperheight <= 0)) return { allowed: false, reason: "The reviewed pdf paper height must be a positive number of inches with no code cap." };
1635
+ if (pdfoptions.margins !== void 0) {
1636
+ const margins = pdfoptions.margins;
1637
+ if (!margins || typeof margins !== "object" || Array.isArray(margins)) return { allowed: false, reason: "The reviewed pdf margins must be an object with top, right, bottom and left inches." };
1638
+ for (const side of ["top", "right", "bottom", "left"]) {
1639
+ const value = margins[side];
1640
+ if (value === void 0) continue;
1641
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0) return { allowed: false, reason: `The reviewed pdf ${side} margin must be zero or a positive number of inches; negative margins are refused.` };
1642
+ }
1643
+ }
1644
+ if (pdfoptions.scale !== void 0 && (typeof pdfoptions.scale !== "number" || !Number.isFinite(pdfoptions.scale) || pdfoptions.scale <= 0)) return { allowed: false, reason: "The reviewed pdf scale must be a positive number with no code cap." };
1645
+ if (pdfoptions.landscape !== void 0 && typeof pdfoptions.landscape !== "boolean") return { allowed: false, reason: "The reviewed pdf landscape flag must be a boolean." };
1646
+ if (pdfoptions.paginate !== void 0 && typeof pdfoptions.paginate !== "boolean") return { allowed: false, reason: "The reviewed pdf paginate flag must be a boolean." };
1647
+ }
1648
+ if (options.breakpoints !== void 0 && (!Array.isArray(options.breakpoints) || options.breakpoints.length === 0 || !options.breakpoints.every((item) => isnonempty(item)))) return { allowed: false, reason: "The reviewed pdf break points must be a non-empty list of selectors when present." };
1649
+ if (options.exporttarget !== void 0 && options.exporttarget !== "memory" && options.exporttarget !== "download") return { allowed: false, reason: "The reviewed pdf export target must be memory or download; pdf documents do not route to the clipboard." };
1650
+ if (options.name !== void 0 && !isnonempty(options.name)) return { allowed: false, reason: "The reviewed pdf artifact name must be a non-empty string." };
1651
+ }
1652
+ if (kind === "recordscreen" || kind === "captureaudio") {
1653
+ const recording = options.recording;
1654
+ if (recording !== void 0) {
1655
+ if (!recording || typeof recording !== "object" || Array.isArray(recording)) return { allowed: false, reason: "The reviewed recording options must be an object in options.recording." };
1656
+ const recordoptions = recording;
1657
+ if (recordoptions.scope !== void 0 && recordoptions.scope !== "tab" && recordoptions.scope !== "run") return { allowed: false, reason: "The reviewed recording scope must be tab or run." };
1658
+ if (recordoptions.fps !== void 0 && (typeof recordoptions.fps !== "number" || !Number.isFinite(recordoptions.fps) || recordoptions.fps <= 0)) return { allowed: false, reason: "The reviewed recording fps must be a positive number with no code ceiling." };
1659
+ if (recordoptions.bitrate !== void 0 && (typeof recordoptions.bitrate !== "number" || !Number.isFinite(recordoptions.bitrate) || recordoptions.bitrate <= 0)) return { allowed: false, reason: "The reviewed recording bitrate must be a positive number with no code ceiling." };
1660
+ if (recordoptions.audio !== void 0 && typeof recordoptions.audio !== "boolean") return { allowed: false, reason: "The reviewed recording audio flag must be a boolean." };
1661
+ }
1662
+ if (options.duration !== void 0 && (typeof options.duration !== "number" || !Number.isFinite(options.duration) || options.duration <= 0)) return { allowed: false, reason: "The reviewed recording duration must be a positive number of milliseconds with no code ceiling." };
1663
+ const consent = recordingconsentgranted(step);
1664
+ if (!consent.allowed) return consent;
1665
+ }
1666
+ if (kind === "captureframe") {
1667
+ if (options.timestamp !== void 0 && (typeof options.timestamp !== "number" || !Number.isFinite(options.timestamp) || options.timestamp < 0)) return { allowed: false, reason: "The reviewed frame timestamp must be zero or a positive number of seconds." };
1668
+ if (options.poster !== void 0 && typeof options.poster !== "boolean") return { allowed: false, reason: "The reviewed poster flag must be a boolean." };
1669
+ const capturecheck = validatecaptureoptions(options.capture);
1670
+ if (!capturecheck.allowed) return capturecheck;
1671
+ }
1672
+ if (kind === "downloadimages") {
1673
+ const filter = options.imagefilter;
1674
+ if (!filter || typeof filter !== "object" || Array.isArray(filter)) return { allowed: false, reason: "A reviewed imagefilter is required in options before any image downloads." };
1675
+ const imagefilter = filter;
1676
+ if (imagefilter.selector !== void 0 && !isnonempty(imagefilter.selector)) return { allowed: false, reason: "The reviewed imagefilter selector must be a non-empty selector from the reviewed selector grammar." };
1677
+ if (imagefilter.minwidth !== void 0 && (typeof imagefilter.minwidth !== "number" || !Number.isFinite(imagefilter.minwidth) || imagefilter.minwidth < 0)) return { allowed: false, reason: "The reviewed imagefilter minimum width must be zero or a positive number of pixels." };
1678
+ if (imagefilter.minheight !== void 0 && (typeof imagefilter.minheight !== "number" || !Number.isFinite(imagefilter.minheight) || imagefilter.minheight < 0)) return { allowed: false, reason: "The reviewed imagefilter minimum height must be zero or a positive number of pixels." };
1679
+ if (imagefilter.formats !== void 0 && (!Array.isArray(imagefilter.formats) || imagefilter.formats.length === 0 || !imagefilter.formats.every((item) => isnonempty(item)))) return { allowed: false, reason: "The reviewed imagefilter format list must be a non-empty list of mime or extension patterns when present." };
1680
+ if (options.naming !== void 0) {
1681
+ const namingcheck = validatecapturenaming(options.naming);
1682
+ if (!namingcheck.allowed) return namingcheck;
1683
+ }
1684
+ }
1685
+ if (kind === "shotcanvas") {
1686
+ const capturecheck = validatecaptureoptions(options.capture);
1687
+ if (!capturecheck.allowed) return capturecheck;
1688
+ }
1689
+ if (kind === "probestream" && options.selector !== void 0 && !isnonempty(options.selector)) return { allowed: false, reason: "The reviewed stream probe scope selector must be a non-empty string." };
1690
+ if (kind === "timelapse") {
1691
+ const lapse = options.lapse;
1692
+ if (!lapse || typeof lapse !== "object" || Array.isArray(lapse)) return { allowed: false, reason: "A reviewed lapse plan with interval, duration and format is required in options." };
1693
+ const plan = lapse;
1694
+ if (typeof plan.interval !== "number" || !Number.isFinite(plan.interval) || plan.interval <= 0) return { allowed: false, reason: "The reviewed lapse interval must be a positive number of milliseconds with no code ceiling." };
1695
+ if (typeof plan.duration !== "number" || !Number.isFinite(plan.duration) || plan.duration <= 0) return { allowed: false, reason: "The reviewed lapse duration must be a positive number of milliseconds with no code ceiling." };
1696
+ if (plan.format !== void 0 && plan.format !== "png" && plan.format !== "jpeg" && plan.format !== "webp") return { allowed: false, reason: "The reviewed lapse format must be png, jpeg or webp." };
1697
+ const budget = lapsebudgetallowed(plan.interval, plan.duration, typeof options.wait === "number" ? options.wait : void 0);
1698
+ if (!budget.allowed) return budget;
1699
+ const capturecheck = validatecaptureoptions(options.capture);
1700
+ if (!capturecheck.allowed) return capturecheck;
1701
+ }
1702
+ if (kind === "convertimage" || kind === "makethumbs") {
1703
+ const single = options.capture;
1704
+ const list = options.captures;
1705
+ const hasone = isnonempty(single);
1706
+ const haslist = Array.isArray(list) && list.length > 0 && list.every((item) => isnonempty(item));
1707
+ if (!hasone && !haslist) return { allowed: false, reason: "A reviewed capture id or a reviewed non-empty capture id list is required in options." };
1708
+ if (hasone && haslist) return { allowed: false, reason: "The reviewed step needs one capture id or a capture id list, not both." };
1709
+ }
1710
+ if (kind === "convertimage") {
1711
+ const convert = options.convert;
1712
+ if (!convert || typeof convert !== "object" || Array.isArray(convert)) return { allowed: false, reason: "A reviewed convert directive with a target format is required in options." };
1713
+ const directive = convert;
1714
+ if (directive.target !== "png" && directive.target !== "jpeg" && directive.target !== "webp") return { allowed: false, reason: "The reviewed conversion target must be png, jpeg or webp." };
1715
+ if (directive.source !== void 0 && directive.source !== "png" && directive.source !== "jpeg" && directive.source !== "webp") return { allowed: false, reason: "The reviewed conversion source must be png, jpeg or webp." };
1716
+ if (directive.quality !== void 0 && (typeof directive.quality !== "number" || !Number.isFinite(directive.quality) || directive.quality < 0 || directive.quality > 100)) return { allowed: false, reason: "The reviewed conversion quality must stay between zero and one hundred with no code cap inside that range." };
1717
+ }
1718
+ if (kind === "makethumbs") {
1719
+ const thumb = options.thumb;
1720
+ if (!thumb || typeof thumb !== "object" || Array.isArray(thumb)) return { allowed: false, reason: "A reviewed thumb directive with size, fit and suffix is required in options." };
1721
+ const directive = thumb;
1722
+ if (typeof directive.size !== "number" || !Number.isFinite(directive.size) || directive.size <= 0) return { allowed: false, reason: "The reviewed thumbnail size must be a positive number of pixels with no fixed set." };
1723
+ if (directive.fit !== "cover" && directive.fit !== "contain") return { allowed: false, reason: "The reviewed thumbnail fit must be cover or contain." };
1724
+ if (!isnonempty(directive.suffix)) return { allowed: false, reason: "The reviewed thumbnail naming suffix must be a non-empty string." };
1725
+ }
1726
+ return { allowed: true };
1727
+ }
1391
1728
  function validatestep(step, origin) {
1392
1729
  if (!allowedactions.has(step.kind)) return { allowed: false, reason: "Unsupported action kind." };
1393
1730
  if (!step.summary.trim()) return { allowed: false, reason: "A human-readable action summary is required." };
@@ -1605,6 +1942,14 @@ function validatestep(step, origin) {
1605
1942
  const filescheck = validatefilesgrammar(step, options);
1606
1943
  if (!filescheck.allowed) return filescheck;
1607
1944
  }
1945
+ if (iscapturekind(step.kind)) {
1946
+ const capturecheck = validatecapturegrammar(step, options);
1947
+ if (!capturecheck.allowed) return capturecheck;
1948
+ }
1949
+ if (ismediakind(step.kind)) {
1950
+ const mediacheck = validatemediagrammar(step, options);
1951
+ if (!mediacheck.allowed) return mediacheck;
1952
+ }
1608
1953
  if (step.kind === "tabcreate") {
1609
1954
  if (options.background !== void 0 && typeof options.background !== "boolean") return { allowed: false, reason: "The reviewed background flag must be a boolean." };
1610
1955
  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." };
@@ -1664,6 +2009,26 @@ function canexecute(input) {
1664
2009
  if (!clipgate.allowed) return clipgate;
1665
2010
  }
1666
2011
  if (input.step.kind === "interceptmime" && !origingranted(input.session, input.origin)) return { allowed: false, reason: "The download interception is outside the session origin grants." };
2012
+ if (iscapturekind(input.step.kind)) {
2013
+ const capturegatecheck = capturegate(input.session, input.tabid, input.origin, now);
2014
+ if (!capturegatecheck.allowed) return capturegatecheck;
2015
+ let captureoptions = {};
2016
+ try {
2017
+ captureoptions = parseoptions(input.step);
2018
+ } catch {
2019
+ captureoptions = {};
2020
+ }
2021
+ const target = captureoptions.capture?.exporttarget;
2022
+ if (target !== void 0 && target !== "memory" && target !== "download" && target !== "clipboard") return { allowed: false, reason: "The capture export target must be memory, download or clipboard." };
2023
+ }
2024
+ if (ismediakind(input.step.kind)) {
2025
+ const mediagatecheck = mediagate(input.session, input.tabid, input.origin, now);
2026
+ if (!mediagatecheck.allowed) return mediagatecheck;
2027
+ }
2028
+ if (isrecordingkind(input.step.kind)) {
2029
+ const recordinggate = recordingconsentgranted(input.step);
2030
+ if (!recordinggate.allowed) return recordinggate;
2031
+ }
1667
2032
  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") {
1668
2033
  let options = {};
1669
2034
  try {
@@ -1774,9 +2139,25 @@ function recorddownload(progress, planid, stepid, entry, now) {
1774
2139
  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
2140
  return recordoutcome(base, planid, outcome, now);
1776
2141
  }
2142
+ function recordcapture(progress, planid, stepid, capture, now) {
2143
+ const base = progress && progress.planid === planid ? progress : emptyprogress(planid, now);
2144
+ const bytes = capture.bytes?.length ?? 0;
2145
+ const outcome = { stepid, ok: true, summary: `Captured a ${capture.format} ${capture.kind} shot of ${capture.width} by ${capture.height} pixels with ${bytes} character${bytes === 1 ? "" : "s"} of image data.`, details: { capture: { id: capture.id, kind: capture.kind, format: capture.format, width: capture.width, height: capture.height, bytes } }, at: now };
2146
+ return recordoutcome(base, planid, outcome, now);
2147
+ }
2148
+ function recordpair(progress, planid, stepid, pair, now) {
2149
+ const base = progress && progress.planid === planid ? progress : emptyprogress(planid, now);
2150
+ const outcome = { stepid, ok: true, summary: `Paired the before shot ${pair.beforeid} with the after shot ${pair.afterid} around the ${pair.actionkind} action.`, details: { shotpair: { id: pair.id, beforeid: pair.beforeid, afterid: pair.afterid, actionkind: pair.actionkind, ...pair.target !== void 0 ? { target: pair.target } : {}, ...pair.domsnapshotid !== void 0 ? { domsnapshotid: pair.domsnapshotid } : {} } }, at: now };
2151
+ return recordoutcome(base, planid, outcome, now);
2152
+ }
2153
+ function recordmedia(progress, planid, stepid, media, now) {
2154
+ const base = progress && progress.planid === planid ? progress : emptyprogress(planid, now);
2155
+ const outcome = { stepid, ok: true, summary: `Captured a ${media.kind} media record of ${media.scope} scope with ${media.bytes} character${media.bytes === 1 ? "" : "s"} of media data.`, details: { media }, at: now };
2156
+ return recordoutcome(base, planid, outcome, now);
2157
+ }
1777
2158
 
1778
2159
  // version.ts
1779
- var packageversion = "1.1.39";
2160
+ var packageversion = "1.1.41";
1780
2161
 
1781
2162
  // types.ts
1782
2163
  var protocolversion = packageversion;
@@ -1840,7 +2221,7 @@ function requestbody(input) {
1840
2221
  return JSON.stringify({ version: protocolversion, objective: input.objective, session: input.session, observation: input.observation, capabilities: input.capabilities });
1841
2222
  }
1842
2223
  function outcomeresponse(input) {
1843
- return JSON.stringify({ version: protocolversion, planid: input.plan.id, planstate: input.plan.state, outcome: input.outcome, ...input.resolvedtarget ? { resolvedtarget: input.resolvedtarget } : {} });
2224
+ return JSON.stringify({ version: protocolversion, planid: input.plan.id, planstate: input.plan.state, outcome: input.outcome, ...input.resolvedtarget ? { resolvedtarget: input.resolvedtarget } : {}, ...input.capture ? { capture: input.capture } : {}, ...input.media ? { media: input.media } : {} });
1844
2225
  }
1845
2226
  function mapresponse(input) {
1846
2227
  return JSON.stringify({ version: protocolversion, planid: input.plan.id, planstate: input.plan.state, map: input.map });
@@ -1902,6 +2283,500 @@ function netlogreport(input) {
1902
2283
  function quarantinereport(input) {
1903
2284
  return { version: protocolversion, entries: input.entries };
1904
2285
  }
2286
+ function capturereport(input) {
2287
+ return { version: protocolversion, records: input.records, pairs: input.pairs };
2288
+ }
2289
+ function mediareport(input) {
2290
+ return { version: protocolversion, records: input.records, images: input.images };
2291
+ }
2292
+
2293
+ // capture.ts
2294
+ var capturekinds = ["shotview", "shotfullpage", "shotelement", "shotregion", "contactsheet"];
2295
+ function captureoptionsof(value) {
2296
+ if (!value || typeof value !== "object" || Array.isArray(value)) return {};
2297
+ const options = value;
2298
+ const normalized = {};
2299
+ if (options.format === "png" || options.format === "jpeg" || options.format === "webp") normalized.format = options.format;
2300
+ if (typeof options.quality === "number" && Number.isFinite(options.quality)) normalized.quality = options.quality;
2301
+ if (typeof options.pixelratio === "number" && Number.isFinite(options.pixelratio)) normalized.pixelratio = options.pixelratio;
2302
+ if (typeof options.annotate === "boolean") normalized.annotate = options.annotate;
2303
+ if (options.exporttarget === "memory" || options.exporttarget === "download" || options.exporttarget === "clipboard") normalized.exporttarget = options.exporttarget;
2304
+ return normalized;
2305
+ }
2306
+ function capturevisible(input) {
2307
+ const ratio = input.options.pixelratio ?? 1;
2308
+ return {
2309
+ id: input.id,
2310
+ runid: input.runid,
2311
+ stepid: input.stepid,
2312
+ kind: "shotview",
2313
+ format: input.options.format ?? "png",
2314
+ width: Math.round(input.viewport.width * ratio),
2315
+ height: Math.round(input.viewport.height * ratio),
2316
+ capturedat: input.at,
2317
+ bytes: input.dataurl,
2318
+ ...input.name !== void 0 ? { name: input.name } : {},
2319
+ ...input.options.annotate === true ? { annotated: true } : {},
2320
+ ...input.options.exporttarget !== void 0 ? { exporttarget: input.options.exporttarget } : {},
2321
+ ...input.target !== void 0 ? { target: input.target } : {}
2322
+ };
2323
+ }
2324
+ function capturestitched(input) {
2325
+ const ratio = input.options.pixelratio ?? 1;
2326
+ return {
2327
+ id: input.id,
2328
+ runid: input.runid,
2329
+ stepid: input.stepid,
2330
+ kind: "shotfullpage",
2331
+ format: input.options.format ?? "png",
2332
+ width: Math.round(input.plan.scrollwidth * ratio),
2333
+ height: Math.round(input.plan.scrollheight * ratio),
2334
+ capturedat: input.at,
2335
+ bytes: input.dataurl,
2336
+ ...input.name !== void 0 ? { name: input.name } : {},
2337
+ ...input.options.annotate === true ? { annotated: true } : {},
2338
+ ...input.options.exporttarget !== void 0 ? { exporttarget: input.options.exporttarget } : {}
2339
+ };
2340
+ }
2341
+ function captureelement(input) {
2342
+ const ratio = input.options.pixelratio ?? 1;
2343
+ const scaled = scaledrect(input.rect, ratio);
2344
+ return {
2345
+ id: input.id,
2346
+ runid: input.runid,
2347
+ stepid: input.stepid,
2348
+ kind: "shotelement",
2349
+ format: input.options.format ?? "png",
2350
+ width: scaled.width,
2351
+ height: scaled.height,
2352
+ capturedat: input.at,
2353
+ bytes: input.dataurl,
2354
+ ...input.name !== void 0 ? { name: input.name } : {},
2355
+ ...input.options.annotate === true ? { annotated: true } : {},
2356
+ ...input.options.exporttarget !== void 0 ? { exporttarget: input.options.exporttarget } : {},
2357
+ ...input.target !== void 0 ? { target: input.target } : {}
2358
+ };
2359
+ }
2360
+ function captureregion(input) {
2361
+ const ratio = input.options.pixelratio ?? 1;
2362
+ const scaled = scaledrect(input.rect, ratio);
2363
+ return {
2364
+ id: input.id,
2365
+ runid: input.runid,
2366
+ stepid: input.stepid,
2367
+ kind: "shotregion",
2368
+ format: input.options.format ?? "png",
2369
+ width: scaled.width,
2370
+ height: scaled.height,
2371
+ capturedat: input.at,
2372
+ bytes: input.dataurl,
2373
+ ...input.name !== void 0 ? { name: input.name } : {},
2374
+ ...input.options.annotate === true ? { annotated: true } : {},
2375
+ ...input.options.exporttarget !== void 0 ? { exporttarget: input.options.exporttarget } : {},
2376
+ ...input.target !== void 0 ? { target: input.target } : {}
2377
+ };
2378
+ }
2379
+ function pairstates(before, after, action, at, id) {
2380
+ if (!before) return { skipped: "before", reason: "The before shot was not captured, so no state pair exists." };
2381
+ if (!after) return { skipped: "after", reason: `The action of kind ${action.kind} failed before the after shot, so the state pair is skipped.` };
2382
+ return {
2383
+ pair: {
2384
+ id,
2385
+ beforeid: before.id,
2386
+ afterid: after.id,
2387
+ actionkind: action.kind,
2388
+ ...action.target !== void 0 ? { target: action.target } : {},
2389
+ ...action.domsnapshotid !== void 0 ? { domsnapshotid: action.domsnapshotid } : {},
2390
+ at
2391
+ },
2392
+ reason: `Paired the before shot ${before.id} with the after shot ${after.id} around the ${action.kind} action.`
2393
+ };
2394
+ }
2395
+ function capturestates(input) {
2396
+ if (input.policy !== "beforeafter") return { reason: `The ${input.policy} capture policy takes no state pair around the ${input.actionkind} action.` };
2397
+ return pairstates(input.before, input.after, { kind: input.actionkind, ...input.target !== void 0 ? { target: input.target } : {}, ...input.domsnapshotid !== void 0 ? { domsnapshotid: input.domsnapshotid } : {} }, input.at, input.id);
2398
+ }
2399
+ function buildstitchplan(input) {
2400
+ const overlap = Math.max(0, Math.round(input.overlap ?? 0));
2401
+ const stepy = Math.max(1, input.viewportheight - overlap);
2402
+ const columns = Math.max(1, Math.ceil(input.scrollwidth / input.viewportwidth));
2403
+ const rows = input.scrollheight <= input.viewportheight ? 1 : Math.max(1, Math.ceil((input.scrollheight - overlap) / stepy));
2404
+ const tiles = [];
2405
+ for (let column = 0; column < columns; column += 1) {
2406
+ for (let row = 0; row < rows; row += 1) {
2407
+ const x = Math.min(column * input.viewportwidth, Math.max(0, input.scrollwidth - input.viewportwidth));
2408
+ const y = rows === 1 ? 0 : Math.min(row * stepy, Math.max(0, input.scrollheight - input.viewportheight));
2409
+ tiles.push({ x: Math.round(x), y: Math.round(y) });
2410
+ }
2411
+ }
2412
+ return { columns, rows, tiles, overlap, scrollwidth: Math.round(input.scrollwidth), scrollheight: Math.round(input.scrollheight), viewportwidth: Math.round(input.viewportwidth), viewportheight: Math.round(input.viewportheight) };
2413
+ }
2414
+ function seamweights(overlap) {
2415
+ if (overlap <= 0) return [];
2416
+ const weights = [];
2417
+ for (let index = 0; index < overlap; index += 1) weights.push((index + 1) / (overlap + 1));
2418
+ return weights;
2419
+ }
2420
+ function fixedheadermatch(band, firstband) {
2421
+ if (band.length === 0 || band.length !== firstband.length) return false;
2422
+ return band.every((value, index) => value === firstband[index]);
2423
+ }
2424
+ function scaledrect(rect, pixelratio) {
2425
+ const ratio = pixelratio >= 1 ? pixelratio : 1;
2426
+ return { x: Math.round(rect.x * ratio), y: Math.round(rect.y * ratio), width: Math.round(rect.width * ratio), height: Math.round(rect.height * ratio) };
2427
+ }
2428
+ function croprect(rect, viewport) {
2429
+ const x = Math.max(0, rect.x);
2430
+ const y = Math.max(0, rect.y);
2431
+ return { x: Math.round(x), y: Math.round(y), width: Math.round(Math.max(0, Math.min(rect.width, viewport.width - x))), height: Math.round(Math.max(0, Math.min(rect.height, viewport.height - y))) };
2432
+ }
2433
+ function regionsteps(containerheight, viewportstep) {
2434
+ if (containerheight <= 0 || viewportstep <= 0) return [0];
2435
+ const steps = [];
2436
+ for (let top = 0; top < containerheight; top += viewportstep) {
2437
+ const clamped = Math.min(top, Math.max(0, containerheight - viewportstep));
2438
+ if (!steps.includes(clamped)) steps.push(clamped);
2439
+ }
2440
+ return steps;
2441
+ }
2442
+ function buildsheet(cells, layout) {
2443
+ const columns = Math.max(1, Math.round(layout.columns));
2444
+ const rows = Math.max(1, Math.ceil(cells.length / columns));
2445
+ const placed = cells.map((cell, index) => {
2446
+ const column = index % columns;
2447
+ const row = Math.floor(index / columns);
2448
+ const label = cell.label ?? "";
2449
+ const caption = layout.label === "none" ? "" : layout.label === "index" ? `${index + 1}` : layout.label === "selector" ? cell.selector : label ? `${index + 1} \xB7 ${cell.selector} \xB7 ${label}` : `${index + 1} \xB7 ${cell.selector}`;
2450
+ return { index, column, row, selector: cell.selector, label, caption };
2451
+ });
2452
+ return { columns, rows, cells: placed };
2453
+ }
2454
+ function capturepart(value) {
2455
+ return value.replace(/[^a-z0-9-]+/gi, "-").replace(/^-+|-+$/g, "").toLowerCase() || "capture";
2456
+ }
2457
+ function buildname(rule, parts, extension) {
2458
+ const segments = [];
2459
+ if (rule.run) segments.push(capturepart(parts.run));
2460
+ if (rule.step) segments.push(capturepart(parts.step));
2461
+ if (rule.sequence) segments.push(String(Math.max(0, Math.round(parts.sequence))));
2462
+ if (rule.kind) segments.push(capturepart(parts.kind));
2463
+ const safeextension = extension.replace(/^\.+/, "").toLowerCase() || "png";
2464
+ return `${(segments.length > 0 ? segments : ["capture"]).join("-")}.${safeextension}`;
2465
+ }
2466
+ function annotationplanof(input) {
2467
+ const inset = Math.min(24, Math.max(8, Math.round(Math.min(input.width, input.height) / 12)));
2468
+ const plan = {
2469
+ marker: { x: inset, y: inset, number: Math.max(1, Math.round(input.step)) },
2470
+ footer: `${new Date(input.at).toISOString()} \xB7 ${input.url}`
2471
+ };
2472
+ if (input.rect !== void 0) {
2473
+ const expansion = 2;
2474
+ plan.outline = { x: Math.round(input.rect.x - expansion), y: Math.round(input.rect.y - expansion), width: Math.round(input.rect.width + expansion * 2), height: Math.round(input.rect.height + expansion * 2) };
2475
+ }
2476
+ return plan;
2477
+ }
2478
+
2479
+ // media.ts
2480
+ var mediakinds = ["capturepdf", "recordscreen", "captureaudio", "captureframe", "downloadimages", "shotcanvas", "probestream", "readmedia", "readassets", "timelapse", "convertimage", "makethumbs"];
2481
+ var defaultpaperwidth = 8.5;
2482
+ var defaultpaperheight = 11;
2483
+ var defaultmargins = { top: 0.4, right: 0.4, bottom: 0.4, left: 0.4 };
2484
+ var pdfpointsperinch = 72;
2485
+ var basefontsize = 11;
2486
+ function pdfoptionsof(value) {
2487
+ if (!value || typeof value !== "object" || Array.isArray(value)) return {};
2488
+ const options = value;
2489
+ const normalized = {};
2490
+ if (typeof options.paperwidth === "number" && Number.isFinite(options.paperwidth)) normalized.paperwidth = options.paperwidth;
2491
+ if (typeof options.paperheight === "number" && Number.isFinite(options.paperheight)) normalized.paperheight = options.paperheight;
2492
+ if (options.margins && typeof options.margins === "object" && !Array.isArray(options.margins)) {
2493
+ const margins = options.margins;
2494
+ const top = typeof margins.top === "number" ? margins.top : defaultmargins.top;
2495
+ const right = typeof margins.right === "number" ? margins.right : defaultmargins.right;
2496
+ const bottom = typeof margins.bottom === "number" ? margins.bottom : defaultmargins.bottom;
2497
+ const left = typeof margins.left === "number" ? margins.left : defaultmargins.left;
2498
+ normalized.margins = { top, right, bottom, left };
2499
+ }
2500
+ if (typeof options.scale === "number" && Number.isFinite(options.scale)) normalized.scale = options.scale;
2501
+ if (typeof options.landscape === "boolean") normalized.landscape = options.landscape;
2502
+ if (typeof options.paginate === "boolean") normalized.paginate = options.paginate;
2503
+ return normalized;
2504
+ }
2505
+ function pdfpagesize(options) {
2506
+ const width = (options.paperwidth ?? defaultpaperwidth) * pdfpointsperinch;
2507
+ const height = (options.paperheight ?? defaultpaperheight) * pdfpointsperinch;
2508
+ return options.landscape === true ? { width: height, height: width } : { width, height };
2509
+ }
2510
+ function pdfmargins(options) {
2511
+ const margins = options.margins ?? defaultmargins;
2512
+ return { top: margins.top * pdfpointsperinch, right: margins.right * pdfpointsperinch, bottom: margins.bottom * pdfpointsperinch, left: margins.left * pdfpointsperinch };
2513
+ }
2514
+ function pdffontsize(options) {
2515
+ return basefontsize * (options.scale ?? 1);
2516
+ }
2517
+ function pdftextlayout(text2, options) {
2518
+ const size = pdfpagesize(options);
2519
+ const margins = pdfmargins(options);
2520
+ const fontsize = pdffontsize(options);
2521
+ const leading = fontsize * 1.35;
2522
+ const linesperpage = Math.max(1, Math.floor((size.height - margins.top - margins.bottom) / leading));
2523
+ const columns = Math.max(1, Math.floor((size.width - margins.left - margins.right) / (fontsize * 0.5)));
2524
+ const wrapped = [];
2525
+ for (const paragraph of text2.split(/\r?\n/)) {
2526
+ let line = "";
2527
+ for (const word of paragraph.split(/\s+/).filter(Boolean)) {
2528
+ const candidate = line ? `${line} ${word}` : word;
2529
+ if (candidate.length <= columns) {
2530
+ line = candidate;
2531
+ continue;
2532
+ }
2533
+ if (line) wrapped.push(line);
2534
+ if (word.length <= columns) {
2535
+ line = word;
2536
+ continue;
2537
+ }
2538
+ for (let index = 0; index < word.length; index += columns) wrapped.push(word.slice(index, index + columns));
2539
+ line = "";
2540
+ }
2541
+ wrapped.push(line);
2542
+ if (wrapped.length >= linesperpage) break;
2543
+ }
2544
+ return wrapped.slice(0, linesperpage);
2545
+ }
2546
+ function pdfsegments(scrollheight, viewportheight, breaks) {
2547
+ if (scrollheight <= 0) return [];
2548
+ const step = viewportheight > 0 ? viewportheight : scrollheight;
2549
+ const cuts = [0, ...breaks.filter((top) => Number.isFinite(top) && top > 0 && top < scrollheight).map((top) => Math.round(top))].filter((top, index2, list) => list.indexOf(top) === index2).sort((left, right) => left - right);
2550
+ const segments = [];
2551
+ let index = 0;
2552
+ let cursor = 0;
2553
+ while (cursor < scrollheight) {
2554
+ while (index < cuts.length && (cuts[index] ?? 0) <= cursor) index += 1;
2555
+ const nextcut = index < cuts.length ? cuts[index] : void 0;
2556
+ const next = nextcut !== void 0 ? Math.min(nextcut, scrollheight) : Math.min(cursor + step, scrollheight);
2557
+ if (next <= cursor) break;
2558
+ segments.push({ top: cursor, height: next - cursor });
2559
+ cursor = next;
2560
+ }
2561
+ return segments.length > 0 ? segments : [{ top: 0, height: scrollheight }];
2562
+ }
2563
+ function pdfescape(text2) {
2564
+ let escaped = "";
2565
+ for (const character of text2) {
2566
+ const code = character.charCodeAt(0);
2567
+ if (character === "(" || character === ")" || character === "\\") escaped += `\\${character}`;
2568
+ else if (code >= 32 && code <= 255) escaped += character;
2569
+ else escaped += "?";
2570
+ }
2571
+ return escaped;
2572
+ }
2573
+ function buildpdf(pages, options) {
2574
+ const size = pdfpagesize(options);
2575
+ const margins = pdfmargins(options);
2576
+ const fontsize = pdffontsize(options);
2577
+ const leading = fontsize * 1.35;
2578
+ const laidout = (pages.length > 0 ? pages : [""]).map((text2) => pdftextlayout(text2, options));
2579
+ const objects = [];
2580
+ const kids = laidout.map((_, index) => `${4 + index * 2} 0 R`).join(" ");
2581
+ objects.push(`<< /Type /Catalog /Pages 2 0 R >>`);
2582
+ objects.push(`<< /Type /Pages /Kids [${kids}] /Count ${laidout.length} >>`);
2583
+ objects.push(`<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>`);
2584
+ for (let pageindex = 0; pageindex < laidout.length; pageindex += 1) {
2585
+ const lines = laidout[pageindex] ?? [];
2586
+ const operators = ["BT", `/F1 ${fontsize} Tf`, `${leading.toFixed(2)} TL`, `${margins.left.toFixed(2)} ${(size.height - margins.top - fontsize).toFixed(2)} Td`];
2587
+ for (let lineindex = 0; lineindex < lines.length; lineindex += 1) {
2588
+ if (lineindex > 0) operators.push("T*");
2589
+ operators.push(`(${pdfescape(lines[lineindex] ?? "")}) Tj`);
2590
+ }
2591
+ operators.push("ET");
2592
+ const content = operators.join("\n");
2593
+ objects.push(`<< /Type /Page /Parent 2 0 R /MediaBox [0 0 ${size.width.toFixed(2)} ${size.height.toFixed(2)}] /Resources << /Font << /F1 3 0 R >> >> /Contents ${5 + pageindex * 2} 0 R >>`);
2594
+ objects.push(`<< /Length ${content.length} >>
2595
+ stream
2596
+ ${content}
2597
+ endstream`);
2598
+ }
2599
+ let document2 = "%PDF-1.4\n";
2600
+ const offsets = [];
2601
+ for (let index = 0; index < objects.length; index += 1) {
2602
+ offsets.push(document2.length);
2603
+ document2 += `${index + 1} 0 obj
2604
+ ${objects[index]}
2605
+ endobj
2606
+ `;
2607
+ }
2608
+ const xrefstart = document2.length;
2609
+ document2 += `xref
2610
+ 0 ${objects.length + 1}
2611
+ 0000000000 65535 f
2612
+ `;
2613
+ for (const offset of offsets) document2 += `${String(offset).padStart(10, "0")} 00000 n
2614
+ `;
2615
+ document2 += `trailer
2616
+ << /Size ${objects.length + 1} /Root 1 0 R >>
2617
+ startxref
2618
+ ${xrefstart}
2619
+ %%EOF
2620
+ `;
2621
+ return { document: document2, bytes: document2.length, pages: laidout.length, pagewidth: Math.round(size.width), pageheight: Math.round(size.height) };
2622
+ }
2623
+ function recordingoptionsof(value) {
2624
+ if (!value || typeof value !== "object" || Array.isArray(value)) return {};
2625
+ const options = value;
2626
+ const normalized = {};
2627
+ if (options.scope === "tab" || options.scope === "run") normalized.scope = options.scope;
2628
+ if (typeof options.fps === "number" && Number.isFinite(options.fps)) normalized.fps = options.fps;
2629
+ if (typeof options.bitrate === "number" && Number.isFinite(options.bitrate)) normalized.bitrate = options.bitrate;
2630
+ if (typeof options.audio === "boolean") normalized.audio = options.audio;
2631
+ return normalized;
2632
+ }
2633
+ function newrecording(input) {
2634
+ return {
2635
+ id: input.id,
2636
+ runid: input.runid,
2637
+ stepid: input.stepid,
2638
+ tabid: input.tabid,
2639
+ kind: input.kind,
2640
+ scope: input.options.scope ?? "tab",
2641
+ format: input.kind === "audio" ? "evidence" : "frames",
2642
+ startedat: input.at,
2643
+ at: input.at,
2644
+ ...input.options.fps !== void 0 ? { fps: input.options.fps } : {},
2645
+ ...input.options.bitrate !== void 0 ? { bitrate: input.options.bitrate } : {},
2646
+ ...input.options.audio !== void 0 ? { audio: input.options.audio } : {},
2647
+ frames: []
2648
+ };
2649
+ }
2650
+ function finishrecording(record2, endat) {
2651
+ return { ...record2, endedat: endat, duration: Math.max(0, endat - record2.startedat) };
2652
+ }
2653
+ function frameinterval(fps) {
2654
+ if (!Number.isFinite(fps) || fps <= 0) return 1e3;
2655
+ return Math.max(1, Math.round(1e3 / fps));
2656
+ }
2657
+ function imagefilterof(value) {
2658
+ if (!value || typeof value !== "object" || Array.isArray(value)) return {};
2659
+ const options = value;
2660
+ const normalized = {};
2661
+ if (typeof options.selector === "string" && options.selector.trim()) normalized.selector = options.selector.trim();
2662
+ if (typeof options.minwidth === "number" && Number.isFinite(options.minwidth)) normalized.minwidth = options.minwidth;
2663
+ if (typeof options.minheight === "number" && Number.isFinite(options.minheight)) normalized.minheight = options.minheight;
2664
+ if (Array.isArray(options.formats) && options.formats.every((item) => typeof item === "string" && item.trim())) normalized.formats = options.formats;
2665
+ return normalized;
2666
+ }
2667
+ function imagematches(image, filter) {
2668
+ if (filter.minwidth !== void 0 && image.width < filter.minwidth) return false;
2669
+ if (filter.minheight !== void 0 && image.height < filter.minheight) return false;
2670
+ if (filter.formats !== void 0 && filter.formats.length > 0) {
2671
+ const mime = image.mime.toLowerCase();
2672
+ const matches = filter.formats.some((format) => {
2673
+ const wanted = format.toLowerCase().trim();
2674
+ return mime === wanted || mime === `image/${wanted}` || mime.endsWith(`/${wanted}`);
2675
+ });
2676
+ if (!matches) return false;
2677
+ }
2678
+ return true;
2679
+ }
2680
+ function dedupeimages(images) {
2681
+ const seen = /* @__PURE__ */ new Set();
2682
+ const unique = [];
2683
+ for (const image of images) {
2684
+ if (seen.has(image.url)) continue;
2685
+ seen.add(image.url);
2686
+ unique.push(image);
2687
+ }
2688
+ return unique;
2689
+ }
2690
+ function imagenames(rule, run, step, count, extension) {
2691
+ const names = [];
2692
+ for (let index = 1; index <= Math.max(0, Math.round(count)); index += 1) names.push(buildname(rule, { run, step, sequence: index, kind: "image" }, extension));
2693
+ return names;
2694
+ }
2695
+ function lapseplanof(value) {
2696
+ if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
2697
+ const options = value;
2698
+ if (typeof options.interval !== "number" || !Number.isFinite(options.interval)) return void 0;
2699
+ if (typeof options.duration !== "number" || !Number.isFinite(options.duration)) return void 0;
2700
+ const format = options.format === "jpeg" || options.format === "webp" ? options.format : "png";
2701
+ return { interval: options.interval, duration: options.duration, format };
2702
+ }
2703
+ function lapseframes(plan) {
2704
+ if (!(plan.interval > 0) || !(plan.duration > 0)) return [];
2705
+ const frames = [];
2706
+ for (let time = 0; time < plan.duration; time += plan.interval) frames.push(Math.round(time));
2707
+ return frames;
2708
+ }
2709
+ function convertdirectiveof(value) {
2710
+ if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
2711
+ const options = value;
2712
+ if (options.target !== "png" && options.target !== "jpeg" && options.target !== "webp") return void 0;
2713
+ const normalized = { target: options.target };
2714
+ if (options.source === "png" || options.source === "jpeg" || options.source === "webp") normalized.source = options.source;
2715
+ if (typeof options.quality === "number" && Number.isFinite(options.quality)) normalized.quality = options.quality;
2716
+ return normalized;
2717
+ }
2718
+ function thumbdirectiveof(value) {
2719
+ if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
2720
+ const options = value;
2721
+ if (typeof options.size !== "number" || !Number.isFinite(options.size) || options.size <= 0) return void 0;
2722
+ if (options.fit !== "cover" && options.fit !== "contain") return void 0;
2723
+ if (typeof options.suffix !== "string" || !options.suffix.trim()) return void 0;
2724
+ return { size: options.size, fit: options.fit, suffix: options.suffix.trim() };
2725
+ }
2726
+ function thumbgeometry(source, directive) {
2727
+ const size = Math.max(1, Math.round(directive.size));
2728
+ if (directive.fit === "contain") {
2729
+ const scale2 = Math.min(size / Math.max(1, source.width), size / Math.max(1, source.height));
2730
+ const dw = Math.max(1, Math.round(source.width * scale2));
2731
+ const dh = Math.max(1, Math.round(source.height * scale2));
2732
+ return { sx: 0, sy: 0, sw: source.width, sh: source.height, dx: Math.floor((size - dw) / 2), dy: Math.floor((size - dh) / 2), dw, dh, width: size, height: size };
2733
+ }
2734
+ const scale = Math.max(size / Math.max(1, source.width), size / Math.max(1, source.height));
2735
+ const sw = Math.min(source.width, Math.round(size / scale));
2736
+ const sh = Math.min(source.height, Math.round(size / scale));
2737
+ return { sx: Math.floor((source.width - sw) / 2), sy: Math.floor((source.height - sh) / 2), sw, sh, dx: 0, dy: 0, dw: size, dh: size, width: size, height: size };
2738
+ }
2739
+ function mediaentries(raw) {
2740
+ return raw.map((entry) => ({
2741
+ url: typeof entry.url === "string" ? entry.url : "",
2742
+ mime: typeof entry.mime === "string" ? entry.mime : "",
2743
+ duration: typeof entry.duration === "number" && Number.isFinite(entry.duration) ? entry.duration : 0,
2744
+ width: typeof entry.width === "number" && Number.isFinite(entry.width) ? Math.round(entry.width) : 0,
2745
+ height: typeof entry.height === "number" && Number.isFinite(entry.height) ? Math.round(entry.height) : 0,
2746
+ codecs: typeof entry.codecs === "string" ? entry.codecs : "",
2747
+ tracks: Array.isArray(entry.tracks) ? entry.tracks.filter((item) => typeof item === "string") : []
2748
+ }));
2749
+ }
2750
+ function assetentries(raw) {
2751
+ return raw.map((entry) => ({
2752
+ kind: entry.kind === "logo" ? "logo" : "favicon",
2753
+ url: typeof entry.url === "string" ? entry.url : "",
2754
+ bytes: typeof entry.bytes === "number" && Number.isFinite(entry.bytes) ? entry.bytes : 0,
2755
+ ...typeof entry.sizes === "string" && entry.sizes.trim() ? { sizes: entry.sizes.trim() } : {}
2756
+ }));
2757
+ }
2758
+ function streamsummaries(raw) {
2759
+ return raw.map((entry) => {
2760
+ const tracks = Array.isArray(entry.tracks) ? entry.tracks : [];
2761
+ return {
2762
+ kind: typeof entry.kind === "string" ? entry.kind : "stream",
2763
+ tracks: tracks.length,
2764
+ label: typeof entry.label === "string" ? entry.label : "",
2765
+ live: entry.live === true,
2766
+ detail: tracks.map((track) => {
2767
+ const item = track;
2768
+ return {
2769
+ kind: typeof item.kind === "string" ? item.kind : "",
2770
+ label: typeof item.label === "string" ? item.label : "",
2771
+ ...typeof item.width === "number" && Number.isFinite(item.width) ? { width: Math.round(item.width) } : {},
2772
+ ...typeof item.height === "number" && Number.isFinite(item.height) ? { height: Math.round(item.height) } : {},
2773
+ ...typeof item.framerate === "number" && Number.isFinite(item.framerate) ? { framerate: item.framerate } : {},
2774
+ state: typeof item.state === "string" ? item.state : ""
2775
+ };
2776
+ })
2777
+ };
2778
+ });
2779
+ }
1905
2780
 
1906
2781
  // extension/browsertabs.ts
1907
2782
  var browserkinds = /* @__PURE__ */ new Set(["tablist", "tabcreate", "tabactivate", "tabclose", "tabreload", "tabsnapshot", "windowlist", "windowcreate", "windowclose", "zoomset", "windowresize", "downloadfile"]);
@@ -3018,12 +3893,12 @@ function scanverdictof(response) {
3018
3893
  function released(entry, ref, at) {
3019
3894
  return { ...entry, release: ref, updatedat: at };
3020
3895
  }
3021
- function capturepart(value) {
3896
+ function capturepart2(value) {
3022
3897
  return value.replace(/[^a-z0-9-]+/gi, "-").replace(/^-+|-+$/g, "").toLowerCase() || "capture";
3023
3898
  }
3024
3899
  function capturefilename(name, extension) {
3025
3900
  const safeextension = extension.replace(/^\.+/, "").toLowerCase() || "png";
3026
- return `${capturepart(name.task)}-${capturepart(name.step)}-${name.sequence}.${safeextension}`;
3901
+ return `${capturepart2(name.task)}-${capturepart2(name.step)}-${name.sequence}.${safeextension}`;
3027
3902
  }
3028
3903
  function advancecounter(counters, base) {
3029
3904
  const sequence = (counters[base] ?? 0) + 1;
@@ -3102,8 +3977,9 @@ function stepoptions2(step) {
3102
3977
  }
3103
3978
  async function refreshcapabilities() {
3104
3979
  const report = await readcapabilities();
3105
- await memory.setcapabilities(report);
3106
- return report;
3980
+ const withmedia = { ...report, captures: [...capturekinds], media: [...mediakinds] };
3981
+ await memory.setcapabilities(withmedia);
3982
+ return withmedia;
3107
3983
  }
3108
3984
  async function activecontext() {
3109
3985
  const [tab] = await chrome.tabs.query({ active: true, lastFocusedWindow: true });
@@ -3256,6 +4132,8 @@ function stepauditkind(step, ok) {
3256
4132
  if (step.kind === "submitform" || step.kind === "asksubmit") return "submit";
3257
4133
  if (step.kind === "consentpassword") return "consent";
3258
4134
  if (step.kind === "handoffcaptcha") return "handoff";
4135
+ if (iscapturekind(step.kind)) return "capture";
4136
+ if (ismediakind(step.kind)) return "media";
3259
4137
  if (isfileskind(step.kind)) {
3260
4138
  if (step.kind === "interceptmime") return "intercept";
3261
4139
  if (step.kind === "readclipboard" || step.kind === "writeclipboard" || step.kind === "copyscreen") return "clipboard";
@@ -4941,6 +5819,645 @@ async function reconcilmimefilter() {
4941
5819
  }
4942
5820
  reconcilmimefilter().catch(() => {
4943
5821
  });
5822
+ var stitchprogress = /* @__PURE__ */ new Map();
5823
+ function stepcaptureoptions(step) {
5824
+ return captureoptionsof(stepoptions2(step).capture);
5825
+ }
5826
+ async function blobtodataurl(blob) {
5827
+ const buffer = new Uint8Array(await blob.arrayBuffer());
5828
+ let binary = "";
5829
+ const chunk = 32768;
5830
+ for (let index = 0; index < buffer.length; index += chunk) binary += String.fromCharCode(...buffer.subarray(index, index + chunk));
5831
+ return `data:${blob.type};base64,${btoa(binary)}`;
5832
+ }
5833
+ async function tabshot(format, quality) {
5834
+ const shot = { format: format === "jpeg" ? "jpeg" : "png" };
5835
+ if (format === "jpeg" && quality !== void 0) shot.quality = Math.max(0, Math.min(100, Math.round(quality)));
5836
+ return chrome.tabs.captureVisibleTab(chrome.windows.WINDOW_ID_CURRENT, shot);
5837
+ }
5838
+ async function bridgecall(tabid2, name, ...args) {
5839
+ await chrome.scripting.executeScript({ target: { tabId: tabid2 }, files: ["pagebridge.js"] });
5840
+ const result = await chrome.scripting.executeScript({ target: { tabId: tabid2 }, func: (bridgeName, bridgeArgs) => {
5841
+ const bridge = globalThis.devthinkbridge;
5842
+ if (!bridge) throw new Error("Devthink page bridge is unavailable.");
5843
+ const seam = bridge[bridgeName];
5844
+ if (typeof seam !== "function") throw new Error(`The page bridge has no ${bridgeName} capture seam.`);
5845
+ return seam(...bridgeArgs);
5846
+ }, args: [name, args] });
5847
+ return result[0]?.result;
5848
+ }
5849
+ function tileband(bitmap, rows) {
5850
+ if (rows <= 0 || bitmap.height <= 0) return [];
5851
+ const canvas = new OffscreenCanvas(bitmap.width, Math.min(rows, bitmap.height));
5852
+ const context = canvas.getContext("2d");
5853
+ if (!context) return [];
5854
+ context.drawImage(bitmap, 0, 0);
5855
+ const data = context.getImageData(0, 0, canvas.width, canvas.height).data;
5856
+ const band = [];
5857
+ const pixels = canvas.width * canvas.height;
5858
+ const stride = Math.max(1, Math.floor(pixels / 64));
5859
+ for (let pixel = 0; pixel < pixels; pixel += stride) {
5860
+ const index = pixel * 4;
5861
+ band.push((data[index] ?? 0) + (data[index + 1] ?? 0) * 256 + (data[index + 2] ?? 0) * 65536 + (data[index + 3] ?? 0) * 16777216);
5862
+ }
5863
+ return band;
5864
+ }
5865
+ function drawannotations(context, plan, width, height, ratio) {
5866
+ context.save();
5867
+ context.scale(ratio, ratio);
5868
+ context.fillStyle = "rgba(47,128,237,.92)";
5869
+ context.beginPath();
5870
+ context.arc(plan.marker.x, plan.marker.y, 14, 0, Math.PI * 2);
5871
+ context.fill();
5872
+ context.fillStyle = "#ffffff";
5873
+ context.font = "bold 14px sans-serif";
5874
+ context.textBaseline = "middle";
5875
+ context.textAlign = "center";
5876
+ context.fillText(String(plan.marker.number), plan.marker.x, plan.marker.y);
5877
+ if (plan.outline !== void 0) {
5878
+ context.strokeStyle = "#2f80ed";
5879
+ context.lineWidth = 2;
5880
+ context.strokeRect(plan.outline.x, plan.outline.y, plan.outline.width, plan.outline.height);
5881
+ }
5882
+ const footery = Math.max(12, height - 22);
5883
+ context.fillStyle = "rgba(23,32,44,.86)";
5884
+ context.fillRect(0, footery, width, 22);
5885
+ context.fillStyle = "#ffffff";
5886
+ context.font = "12px sans-serif";
5887
+ context.textAlign = "left";
5888
+ context.fillText(plan.footer.slice(0, 160), 8, footery + 11);
5889
+ context.restore();
5890
+ }
5891
+ async function canvasdataurl(canvas, format, quality) {
5892
+ const type = format === "jpeg" ? "image/jpeg" : format === "webp" ? "image/webp" : "image/png";
5893
+ const encoded = format === "png" ? await canvas.convertToBlob({ type }) : await canvas.convertToBlob({ type, quality: Math.max(0, Math.min(1, (quality ?? 100) / 100)) });
5894
+ return blobtodataurl(encoded);
5895
+ }
5896
+ async function composestitched(tiles, width, height, overlap, ratio, options) {
5897
+ const canvas = new OffscreenCanvas(Math.max(1, Math.round(width * ratio)), Math.max(1, Math.round(height * ratio)));
5898
+ const context = canvas.getContext("2d");
5899
+ if (!context) throw new Error("The stitcher could not create a canvas context.");
5900
+ const weights = seamweights(overlap);
5901
+ const firstband = overlap > 0 && tiles[0] ? tileband(tiles[0].bitmap, overlap) : [];
5902
+ for (const tile of tiles) {
5903
+ const band = overlap > 0 ? tileband(tile.bitmap, overlap) : [];
5904
+ const repeated = overlap > 0 && !tile.firstofcolumn && fixedheadermatch(band, firstband);
5905
+ if (repeated) {
5906
+ const skipped = Math.min(overlap, tile.bitmap.height);
5907
+ context.drawImage(tile.bitmap, 0, skipped, tile.bitmap.width, tile.bitmap.height - skipped, tile.x, tile.y + skipped, tile.bitmap.width, tile.bitmap.height - skipped);
5908
+ continue;
5909
+ }
5910
+ if (weights.length > 0 && !tile.firstofcolumn) {
5911
+ for (let row = 0; row < weights.length && row < tile.bitmap.height; row += 1) {
5912
+ context.globalAlpha = weights[row] ?? 1;
5913
+ context.drawImage(tile.bitmap, 0, row, tile.bitmap.width, 1, tile.x, tile.y + row, tile.bitmap.width, 1);
5914
+ }
5915
+ context.globalAlpha = 1;
5916
+ const below = Math.min(overlap, tile.bitmap.height);
5917
+ context.drawImage(tile.bitmap, 0, below, tile.bitmap.width, tile.bitmap.height - below, tile.x, tile.y + below, tile.bitmap.width, tile.bitmap.height - below);
5918
+ continue;
5919
+ }
5920
+ context.drawImage(tile.bitmap, tile.x, tile.y);
5921
+ }
5922
+ if (options.annotate) drawannotations(context, options.annotate, width, height, ratio);
5923
+ return canvasdataurl(canvas, options.format, options.quality);
5924
+ }
5925
+ async function decodeTile(dataurl, x, y, firstofcolumn) {
5926
+ const blob = await (await fetch(dataurl)).blob();
5927
+ return { bitmap: await createImageBitmap(blob), x, y, firstofcolumn };
5928
+ }
5929
+ async function croptoRect(dataurl, rect, ratio, options) {
5930
+ const blob = await (await fetch(dataurl)).blob();
5931
+ const bitmap = await createImageBitmap(blob);
5932
+ const canvas = new OffscreenCanvas(Math.max(1, Math.round(rect.width * ratio)), Math.max(1, Math.round(rect.height * ratio)));
5933
+ const context = canvas.getContext("2d");
5934
+ if (!context) throw new Error("The crop could not create a canvas context.");
5935
+ context.drawImage(bitmap, rect.x, rect.y, rect.width, rect.height, 0, 0, canvas.width, canvas.height);
5936
+ if (options.annotate) drawannotations(context, options.annotate, rect.width, rect.height, ratio);
5937
+ return canvasdataurl(canvas, options.format, options.quality);
5938
+ }
5939
+ async function encodecanvas(width, height, draw, options) {
5940
+ const canvas = new OffscreenCanvas(Math.max(1, Math.round(width)), Math.max(1, Math.round(height)));
5941
+ const context = canvas.getContext("2d");
5942
+ if (!context) throw new Error("The capture canvas could not create a context.");
5943
+ await draw(context);
5944
+ if (options.annotate) drawannotations(context, options.annotate, canvas.width / (options.annotateratio ?? 1), canvas.height / (options.annotateratio ?? 1), options.annotateratio ?? 1);
5945
+ return canvasdataurl(canvas, options.format, options.quality);
5946
+ }
5947
+ async function capturenamefor(step, plan, kind, format) {
5948
+ const naming = stepoptions2(step).naming;
5949
+ const rule = naming && typeof naming === "object" && !Array.isArray(naming) ? naming : { run: true, step: true, sequence: true, kind: true };
5950
+ const counters = (await memory.getcapturecounters()).find((item) => item.taskid === plan.id);
5951
+ const advanced = advancecounter(counters?.counters ?? {}, step.id);
5952
+ await memory.setcapturecounter({ taskid: plan.id, counters: advanced.counters, at: Date.now() });
5953
+ return buildname(rule, { run: plan.id, step: step.id, sequence: advanced.sequence, kind }, format);
5954
+ }
5955
+ async function routecapture(record2, session, plan, step, origin) {
5956
+ const target = record2.exporttarget ?? "memory";
5957
+ if (target === "clipboard") {
5958
+ const granted = await chrome.permissions.contains({ permissions: ["clipboardWrite"] }).catch(() => false);
5959
+ if (!granted) throw new Error("The clipboard capture export needs the clipboardwrite capability; request it from the review panel.");
5960
+ const blob = await (await fetch(record2.bytes ?? "")).blob();
5961
+ await navigator.clipboard.write([new ClipboardItem({ [blob.type]: blob })]);
5962
+ await memory.addclip(clipentryof("screen", { hash: cliphash(record2.bytes ?? ""), length: (record2.bytes ?? "").length }, origin, step.id, Date.now()));
5963
+ void session;
5964
+ void plan;
5965
+ return { target, destination: "clipboard" };
5966
+ }
5967
+ if (target === "download") {
5968
+ const granted = await chrome.permissions.contains({ permissions: ["downloads"] }).catch(() => false);
5969
+ if (!granted) throw new Error("The download capture export needs the downloads capability; request it from the review panel.");
5970
+ await chrome.downloads.download({ url: record2.bytes ?? "", filename: record2.name ?? `capture.${record2.format}` });
5971
+ return { target, destination: "reviewed download flow" };
5972
+ }
5973
+ return { target, destination: "session memory" };
5974
+ }
5975
+ async function runcapturepolicy() {
5976
+ return (await memory.getsettings())?.capturepolicy ?? "manual";
5977
+ }
5978
+ async function grabstateshot(step, session, plan, tabid2, phase) {
5979
+ const options = stepcaptureoptions(step);
5980
+ const format = options.format ?? "png";
5981
+ const shot = await tabshot(format, options.quality);
5982
+ const measured = await bridgecall(tabid2, "measurepage");
5983
+ const record2 = capturevisible({ runid: plan.id, stepid: step.id, options: { ...options, annotate: options.annotate ?? true }, viewport: { width: measured.viewportwidth, height: measured.viewportheight }, dataurl: shot, at: Date.now(), id: randomid(), name: `${phase}-${await capturenamefor(step, plan, "state", format)}` });
5984
+ await memory.addcapture(record2);
5985
+ return record2;
5986
+ }
5987
+ async function storecapture(record2, session, plan, step, origin) {
5988
+ await memory.addcapture(record2);
5989
+ const routed = await routecapture(record2, session, plan, step, origin);
5990
+ await memory.setprogress(recordcapture(await memory.getprogress(), plan.id, step.id, record2, Date.now()));
5991
+ await refreshbadge();
5992
+ return { record: record2, routed };
5993
+ }
5994
+ async function executecapturestep(step, session, plan, tabid2, origin) {
5995
+ const options = stepcaptureoptions(step);
5996
+ const format = options.format ?? "png";
5997
+ const ratio = options.pixelratio ?? 1;
5998
+ const runpolicy = await runcapturepolicy();
5999
+ const annotate = options.annotate ?? runpolicy === "annotated";
6000
+ const stepnumber = plan.steps.findIndex((candidate) => candidate.id === step.id) + 1;
6001
+ const extra = { ...session ? { sessionid: session.id } : {}, planid: plan.id, stepid: step.id };
6002
+ const pageurl = await chrome.tabs.get(tabid2).then((tab) => tab.url ?? origin).catch(() => origin);
6003
+ if (step.kind === "shotview") {
6004
+ const measured = await bridgecall(tabid2, "measurepage");
6005
+ const raw = await tabshot(format, options.quality);
6006
+ const marker = annotate ? annotationplanof({ step: Math.max(1, stepnumber), width: measured.viewportwidth, height: measured.viewportheight, url: pageurl, at: Date.now() }) : void 0;
6007
+ const dataurl = format === "png" && ratio === 1 && !marker ? raw : await encodecanvas(measured.viewportwidth * ratio, measured.viewportheight * ratio, async (context) => {
6008
+ const bitmap = await createImageBitmap(await (await fetch(raw)).blob());
6009
+ context.drawImage(bitmap, 0, 0, context.canvas.width, context.canvas.height);
6010
+ }, { format, quality: options.quality, annotate: marker, annotateratio: ratio });
6011
+ const record2 = capturevisible({ runid: plan.id, stepid: step.id, options: { ...options, ...annotate ? { annotate: true } : {} }, viewport: { width: measured.viewportwidth, height: measured.viewportheight }, dataurl, at: Date.now(), id: randomid(), name: await capturenamefor(step, plan, "shotview", format) });
6012
+ const stored = await storecapture(record2, session, plan, step, origin);
6013
+ await audit("capture", `Captured the visible viewport at ${record2.width} by ${record2.height} pixels in ${format} for step ${step.id}, routed to ${stored.routed.destination}.`, extra);
6014
+ return { ok: true, summary: `Captured the visible viewport at ${record2.width} by ${record2.height} pixels.`, details: { capture: { id: record2.id, kind: record2.kind, format: record2.format, width: record2.width, height: record2.height, bytes: (record2.bytes ?? "").length, exporttarget: stored.routed.target } } };
6015
+ }
6016
+ if (step.kind === "shotfullpage") {
6017
+ const rawoptions = stepoptions2(step);
6018
+ const settle2 = typeof rawoptions.settle === "number" ? rawoptions.settle : 150;
6019
+ const overlap = typeof rawoptions.overlap === "number" ? rawoptions.overlap : 0;
6020
+ const wait = typeof rawoptions.wait === "number" ? rawoptions.wait : void 0;
6021
+ const state = await bridgecall(tabid2, "preparecapture");
6022
+ try {
6023
+ const measured = await bridgecall(tabid2, "measurepage");
6024
+ const stitch = buildstitchplan({ scrollwidth: measured.scrollwidth, scrollheight: measured.scrollheight, viewportwidth: measured.viewportwidth, viewportheight: measured.viewportheight, overlap });
6025
+ if (wait !== void 0) {
6026
+ const budget = stitchbudgetallowed(stitch.tiles.length, settle2, wait);
6027
+ if (!budget.allowed) throw new Error(budget.reason ?? "The stitching scroll budget exceeds the reviewed wait window.");
6028
+ }
6029
+ const tiles = [];
6030
+ let done = 0;
6031
+ for (let index = 0; index < stitch.tiles.length; index += 1) {
6032
+ const tile = stitch.tiles[index];
6033
+ if (!tile) continue;
6034
+ await bridgecall(tabid2, "scrollcapture", tile.x, tile.y);
6035
+ await bridgecall(tabid2, "waitsettle", settle2);
6036
+ const shot = await tabshot("png", void 0);
6037
+ tiles.push(await decodeTile(shot, tile.x, tile.y, index % stitch.rows === 0));
6038
+ done += 1;
6039
+ stitchprogress.set(step.id, { stepid: step.id, done, total: stitch.tiles.length });
6040
+ }
6041
+ const annotation = annotate ? annotationplanof({ step: Math.max(1, stepnumber), width: stitch.scrollwidth, height: stitch.scrollheight, url: pageurl, at: Date.now() }) : void 0;
6042
+ const dataurl = await composestitched(tiles, stitch.scrollwidth, stitch.scrollheight, overlap, ratio, { format, quality: options.quality, annotate: annotation });
6043
+ const record2 = capturestitched({ runid: plan.id, stepid: step.id, options: { ...options, ...annotate ? { annotate: true } : {} }, plan: stitch, dataurl, at: Date.now(), id: randomid(), name: await capturenamefor(step, plan, "shotfullpage", format) });
6044
+ const stored = await storecapture(record2, session, plan, step, origin);
6045
+ await audit("capture", `Captured the full page of ${stitch.scrollwidth} by ${stitch.scrollheight} css pixels from ${stitch.tiles.length} stitched tile${stitch.tiles.length === 1 ? "" : "s"} with ${overlap} overlap row${overlap === 1 ? "" : "s"}, routed to ${stored.routed.destination}.`, extra);
6046
+ return { ok: true, summary: `Captured the full page from ${stitch.tiles.length} stitched tiles.`, details: { capture: { id: record2.id, kind: record2.kind, format: record2.format, width: record2.width, height: record2.height, bytes: (record2.bytes ?? "").length, exporttarget: stored.routed.target }, tiles: stitch.tiles.length, columns: stitch.columns, rows: stitch.rows, overlap } };
6047
+ } finally {
6048
+ stitchprogress.delete(step.id);
6049
+ await bridgecall(tabid2, "restorecapture", state).catch(() => void 0);
6050
+ }
6051
+ }
6052
+ if (step.kind === "shotelement") {
6053
+ const selector = step.target ?? "";
6054
+ const settle2 = typeof stepoptions2(step).settle === "number" ? stepoptions2(step).settle : 150;
6055
+ const measured = await bridgecall(tabid2, "measurepage");
6056
+ const targetinfo = await bridgecall(tabid2, "elementrect", selector);
6057
+ if (!targetinfo.ok || !targetinfo.rect) throw new Error(targetinfo.summary);
6058
+ const rect = targetinfo.rect;
6059
+ const crossing = targetinfo.crossesviewport === true;
6060
+ const state = await bridgecall(tabid2, "preparecapture");
6061
+ try {
6062
+ let dataurl = "";
6063
+ if (!crossing) {
6064
+ const scrollto = Math.max(0, rect.y - Math.floor((measured.viewportheight - rect.height) / 2));
6065
+ await bridgecall(tabid2, "scrollcapture", 0, scrollto);
6066
+ await bridgecall(tabid2, "waitsettle", settle2);
6067
+ const shot = await tabshot("png", void 0);
6068
+ const crop = croprect({ x: rect.x, y: rect.y - scrollto, width: rect.width, height: rect.height }, { width: measured.viewportwidth, height: measured.viewportheight });
6069
+ const annotation = annotate ? annotationplanof({ step: Math.max(1, stepnumber), width: crop.width, height: crop.height, rect: { x: 0, y: 0, width: crop.width, height: crop.height }, url: pageurl, at: Date.now() }) : void 0;
6070
+ dataurl = await croptoRect(shot, crop, ratio, { format, quality: options.quality, annotate: annotation });
6071
+ } else {
6072
+ const tops = regionsteps(rect.height, measured.viewportheight);
6073
+ const slices = [];
6074
+ for (let index = 0; index < tops.length; index += 1) {
6075
+ const top = tops[index] ?? 0;
6076
+ await bridgecall(tabid2, "scrollcapture", 0, rect.y + top);
6077
+ await bridgecall(tabid2, "waitsettle", settle2);
6078
+ const shot = await tabshot("png", void 0);
6079
+ const crop = croprect({ x: rect.x, y: 0, width: rect.width, height: Math.min(measured.viewportheight, rect.height - top) }, { width: measured.viewportwidth, height: measured.viewportheight });
6080
+ const slicedata = await croptoRect(shot, crop, 1, { format: "png" });
6081
+ slices.push(await decodeTile(slicedata, 0, top, index === 0));
6082
+ }
6083
+ const annotation = annotate ? annotationplanof({ step: Math.max(1, stepnumber), width: Math.min(rect.width, measured.viewportwidth), height: rect.height, rect: { x: 0, y: 0, width: Math.min(rect.width, measured.viewportwidth), height: rect.height }, url: pageurl, at: Date.now() }) : void 0;
6084
+ dataurl = await composestitched(slices, Math.min(rect.width, measured.viewportwidth), rect.height, 0, ratio, { format, quality: options.quality, annotate: annotation });
6085
+ }
6086
+ const record2 = captureelement({ runid: plan.id, stepid: step.id, options: { ...options, ...annotate ? { annotate: true } : {} }, rect, dataurl, at: Date.now(), id: randomid(), name: await capturenamefor(step, plan, "shotelement", format), target: selector });
6087
+ const stored = await storecapture(record2, session, plan, step, origin);
6088
+ await audit("capture", `Captured the element ${selector} at ${rect.width} by ${rect.height} css pixels${crossing ? " through the tiled fallback" : ""}, routed to ${stored.routed.destination}.`, extra);
6089
+ return { ok: true, summary: `Captured the element ${selector}.`, details: { capture: { id: record2.id, kind: record2.kind, format: record2.format, width: record2.width, height: record2.height, bytes: (record2.bytes ?? "").length, exporttarget: stored.routed.target }, rect, tiledfallback: crossing, target: selector } };
6090
+ } finally {
6091
+ await bridgecall(tabid2, "restorecapture", state).catch(() => void 0);
6092
+ }
6093
+ }
6094
+ if (step.kind === "shotregion") {
6095
+ const rawoptions = stepoptions2(step);
6096
+ const rect = rawoptions.regionrect;
6097
+ if (!rect) throw new Error("A reviewed regionrect is required in options.");
6098
+ const container = typeof rawoptions.container === "string" ? rawoptions.container : void 0;
6099
+ const measured = await bridgecall(tabid2, "measurepage");
6100
+ const state = await bridgecall(tabid2, "preparecapture");
6101
+ try {
6102
+ let dataurl = "";
6103
+ let captured = rect;
6104
+ if (container) {
6105
+ const info = await bridgecall(tabid2, "scrollcontainercapture", container, 0);
6106
+ if (!info.ok || info.height === void 0 || info.viewportheight === void 0) throw new Error(info.summary);
6107
+ const steps = regionsteps(info.height, info.viewportheight);
6108
+ const strips = [];
6109
+ for (let index = 0; index < steps.length; index += 1) {
6110
+ await bridgecall(tabid2, "scrollcontainercapture", container, steps[index] ?? 0);
6111
+ await bridgecall(tabid2, "waitsettle", typeof rawoptions.settle === "number" ? rawoptions.settle : 150);
6112
+ const shot = await tabshot("png", void 0);
6113
+ strips.push(await decodeTile(shot, 0, steps[index] ?? 0, index === 0));
6114
+ }
6115
+ const annotation = annotate ? annotationplanof({ step: Math.max(1, stepnumber), width: rect.width, height: info.height, rect, url: pageurl, at: Date.now() }) : void 0;
6116
+ dataurl = await composestitched(strips, rect.width, info.height, 0, ratio, { format, quality: options.quality, annotate: annotation });
6117
+ captured = { x: rect.x, y: rect.y, width: rect.width, height: info.height };
6118
+ } else {
6119
+ await bridgecall(tabid2, "scrollcapture", 0, Math.max(0, rect.y - Math.floor((measured.viewportheight - rect.height) / 2)));
6120
+ await bridgecall(tabid2, "waitsettle", typeof rawoptions.settle === "number" ? rawoptions.settle : 150);
6121
+ const shot = await tabshot("png", void 0);
6122
+ const crop = croprect({ x: rect.x, y: rect.y - Math.max(0, rect.y - Math.floor((measured.viewportheight - rect.height) / 2)), width: rect.width, height: rect.height }, { width: measured.viewportwidth, height: measured.viewportheight });
6123
+ const annotation = annotate ? annotationplanof({ step: Math.max(1, stepnumber), width: crop.width, height: crop.height, rect: { x: 0, y: 0, width: crop.width, height: crop.height }, url: pageurl, at: Date.now() }) : void 0;
6124
+ dataurl = await croptoRect(shot, crop, ratio, { format, quality: options.quality, annotate: annotation });
6125
+ captured = crop;
6126
+ }
6127
+ const record2 = captureregion({ runid: plan.id, stepid: step.id, options: { ...options, ...annotate ? { annotate: true } : {} }, rect: captured, dataurl, at: Date.now(), id: randomid(), name: await capturenamefor(step, plan, "shotregion", format), ...container ? { target: container } : {} });
6128
+ const stored = await storecapture(record2, session, plan, step, origin);
6129
+ await audit("capture", `Captured the reviewed region at ${captured.width} by ${captured.height} css pixels${container ? ` of the scrollable container ${container}` : ""}, routed to ${stored.routed.destination}.`, extra);
6130
+ return { ok: true, summary: `Captured the reviewed region${container ? ` of container ${container}` : ""}.`, details: { capture: { id: record2.id, kind: record2.kind, format: record2.format, width: record2.width, height: record2.height, bytes: (record2.bytes ?? "").length, exporttarget: stored.routed.target }, rect: captured, ...container ? { container } : {} } };
6131
+ } finally {
6132
+ await bridgecall(tabid2, "restorecapture", state).catch(() => void 0);
6133
+ }
6134
+ }
6135
+ if (step.kind === "contactsheet") {
6136
+ const rawoptions = stepoptions2(step);
6137
+ const elements = (Array.isArray(rawoptions.elements) ? rawoptions.elements : []).filter((item) => typeof item === "string" && item.trim().length > 0);
6138
+ const layout = rawoptions.sheet && typeof rawoptions.sheet === "object" && !Array.isArray(rawoptions.sheet) ? rawoptions.sheet : { cellsize: 240, columns: 3, label: "both" };
6139
+ const measured = await bridgecall(tabid2, "measurepage");
6140
+ const state = await bridgecall(tabid2, "preparecapture");
6141
+ try {
6142
+ const cells = [];
6143
+ for (const selector of elements) {
6144
+ const targetinfo = await bridgecall(tabid2, "elementrect", selector);
6145
+ if (!targetinfo.ok || !targetinfo.rect) throw new Error(targetinfo.summary);
6146
+ await bridgecall(tabid2, "scrollcapture", 0, Math.max(0, targetinfo.rect.y - Math.floor((measured.viewportheight - targetinfo.rect.height) / 2)));
6147
+ await bridgecall(tabid2, "waitsettle", typeof rawoptions.settle === "number" ? rawoptions.settle : 150);
6148
+ const shot = await tabshot("png", void 0);
6149
+ const crop = croprect({ x: 0, y: targetinfo.rect.y - Math.max(0, targetinfo.rect.y - Math.floor((measured.viewportheight - targetinfo.rect.height) / 2)), width: targetinfo.rect.width, height: targetinfo.rect.height }, { width: measured.viewportwidth, height: measured.viewportheight });
6150
+ const celldata = await croptoRect(shot, crop, 1, { format: "png" });
6151
+ cells.push({ selector, dataurl: celldata, width: crop.width, height: crop.height });
6152
+ }
6153
+ const placed = buildsheet(cells.map((cell) => ({ selector: cell.selector })), layout);
6154
+ const width = placed.columns * layout.cellsize;
6155
+ const rows = placed.rows;
6156
+ const height = rows * (layout.cellsize + 28);
6157
+ const annotation = annotate ? annotationplanof({ step: Math.max(1, stepnumber), width, height, url: pageurl, at: Date.now() }) : void 0;
6158
+ const dataurl = await encodecanvas(width * ratio, height * ratio, async (context) => {
6159
+ context.scale(ratio, ratio);
6160
+ for (let index = 0; index < cells.length; index += 1) {
6161
+ const cell = cells[index];
6162
+ const place = placed.cells[index];
6163
+ if (!cell || !place) continue;
6164
+ const bitmap = await createImageBitmap(await (await fetch(cell.dataurl)).blob());
6165
+ const scaled = Math.min(layout.cellsize / Math.max(1, bitmap.width), layout.cellsize / Math.max(1, bitmap.height));
6166
+ const drawwidth = Math.max(1, Math.round(bitmap.width * scaled));
6167
+ const drawheight = Math.max(1, Math.round(bitmap.height * scaled));
6168
+ context.drawImage(bitmap, place.column * layout.cellsize + Math.floor((layout.cellsize - drawwidth) / 2), place.row * (layout.cellsize + 28) + Math.floor((layout.cellsize - drawheight) / 2), drawwidth, drawheight);
6169
+ if (place.caption) {
6170
+ context.fillStyle = "rgba(23,32,44,.86)";
6171
+ context.fillRect(place.column * layout.cellsize, place.row * (layout.cellsize + 28) + layout.cellsize, layout.cellsize, 28);
6172
+ context.fillStyle = "#ffffff";
6173
+ context.font = "12px sans-serif";
6174
+ context.textAlign = "left";
6175
+ context.textBaseline = "middle";
6176
+ context.fillText(place.caption.slice(0, Math.floor(layout.cellsize / 7)), place.column * layout.cellsize + 6, place.row * (layout.cellsize + 28) + layout.cellsize + 14);
6177
+ }
6178
+ }
6179
+ }, { format, quality: options.quality, annotate: annotation, annotateratio: ratio });
6180
+ const record2 = { id: randomid(), runid: plan.id, stepid: step.id, kind: "contactsheet", format, width: Math.round(width * ratio), height: Math.round(height * ratio), capturedat: Date.now(), bytes: dataurl, name: await capturenamefor(step, plan, "contactsheet", format), ...annotate ? { annotated: true } : {}, ...options.exporttarget !== void 0 ? { exporttarget: options.exporttarget } : {} };
6181
+ const stored = await storecapture(record2, session, plan, step, origin);
6182
+ await audit("capture", `Tiled ${cells.length} element capture${cells.length === 1 ? "" : "s"} into one labeled ${placed.columns} column contact sheet, routed to ${stored.routed.destination}.`, extra);
6183
+ return { ok: true, summary: `Tiled ${cells.length} element captures into one contact sheet.`, details: { capture: { id: record2.id, kind: record2.kind, format: record2.format, width: record2.width, height: record2.height, bytes: (record2.bytes ?? "").length, exporttarget: stored.routed.target }, cells: placed.cells, columns: placed.columns, rows: placed.rows } };
6184
+ } finally {
6185
+ await bridgecall(tabid2, "restorecapture", state).catch(() => void 0);
6186
+ }
6187
+ }
6188
+ throw new Error("Unsupported capture kind.");
6189
+ }
6190
+ var activerecordings = /* @__PURE__ */ new Map();
6191
+ async function stoprecordingsforrun(runid) {
6192
+ for (const [id, active] of [...activerecordings.entries()]) {
6193
+ if (active.record.runid !== runid) continue;
6194
+ const finished = finishrecording(active.record, Date.now());
6195
+ await memory.addmedia(finished);
6196
+ activerecordings.delete(id);
6197
+ await audit("media", `Recording ${finished.id} of kind ${finished.kind} stopped cleanly at run end after ${finished.duration ?? 0} milliseconds.`, { planid: runid, stepid: finished.stepid });
6198
+ }
6199
+ }
6200
+ async function recordingwriter(record2, exporttarget) {
6201
+ const manifest = {
6202
+ id: record2.id,
6203
+ runid: record2.runid,
6204
+ stepid: record2.stepid,
6205
+ kind: record2.kind,
6206
+ scope: record2.scope,
6207
+ fps: record2.fps,
6208
+ bitrate: record2.bitrate,
6209
+ startedat: record2.startedat,
6210
+ endedat: record2.endedat,
6211
+ duration: record2.duration,
6212
+ derivation: "tab screencast apis stay outside the reviewed manifest, so recordscreen derives an ordered frame sequence from viewport captures and captureaudio derives audio element evidence; no encoded video or audio bytes exist",
6213
+ frames: (record2.frames ?? []).map((id) => ({ id }))
6214
+ };
6215
+ const file = `${(record2.file ?? record2.id).replace(/\.[a-z0-9]+$/i, "")}.json`;
6216
+ const bytes = JSON.stringify(manifest).length;
6217
+ if (exporttarget === "download") {
6218
+ const granted = await chrome.permissions.contains({ permissions: ["downloads"] }).catch(() => false);
6219
+ if (!granted) throw new Error("The recording download needs the downloads capability; request it from the review panel.");
6220
+ await chrome.downloads.download({ url: `data:application/json;base64,${btoa(JSON.stringify(manifest))}`, filename: file });
6221
+ }
6222
+ return { written: exporttarget === "download", file, bytes };
6223
+ }
6224
+ async function convertonecapture(source, directive, plan, step) {
6225
+ if (!source.bytes) throw new Error(`The capture bytes of ${source.id} expired from the retention window; conversions need live bytes.`);
6226
+ const blob = await (await fetch(source.bytes)).blob();
6227
+ const bitmap = await createImageBitmap(blob);
6228
+ const dataurl = await encodecanvas(bitmap.width, bitmap.height, (context) => {
6229
+ context.drawImage(bitmap, 0, 0);
6230
+ }, { format: directive.target, quality: directive.quality });
6231
+ const name = await capturenamefor(step, plan, "convertimage", directive.target);
6232
+ return { id: randomid(), runid: plan.id, stepid: step.id, kind: "convertimage", format: directive.target, width: bitmap.width, height: bitmap.height, capturedat: Date.now(), bytes: dataurl, name, target: source.id };
6233
+ }
6234
+ async function thumbonecapture(source, directive, plan, step) {
6235
+ if (!source.bytes) throw new Error(`The capture bytes of ${source.id} expired from the retention window; thumbnails need live bytes.`);
6236
+ const blob = await (await fetch(source.bytes)).blob();
6237
+ const bitmap = await createImageBitmap(blob);
6238
+ const geometry = thumbgeometry({ width: bitmap.width, height: bitmap.height }, directive);
6239
+ const format = source.format === "jpeg" || source.format === "webp" ? source.format : "png";
6240
+ const dataurl = await encodecanvas(geometry.width, geometry.height, (context) => {
6241
+ context.drawImage(bitmap, geometry.sx, geometry.sy, geometry.sw, geometry.sh, geometry.dx, geometry.dy, geometry.dw, geometry.dh);
6242
+ }, { format });
6243
+ const name = (await capturenamefor(step, plan, "makethumbs", format)).replace(/(\.[a-z0-9]+)?$/, `-${directive.suffix}$1`);
6244
+ return { id: randomid(), runid: plan.id, stepid: step.id, kind: "makethumbs", format, width: geometry.width, height: geometry.height, capturedat: Date.now(), bytes: dataurl, name, target: source.id };
6245
+ }
6246
+ async function executemediastep(step, session, plan, tabid2, origin) {
6247
+ const options = stepoptions2(step);
6248
+ const extra = { ...session ? { sessionid: session.id } : {}, planid: plan.id, stepid: step.id };
6249
+ const gate = mediagate(session, tabid2, origin, Date.now());
6250
+ if (!gate.allowed) throw new Error(gate.reason ?? "The media capture needs the active session tab grant.");
6251
+ if (step.kind === "capturepdf") {
6252
+ const pdfoptions = pdfoptionsof(options.pdf);
6253
+ const paginate = pdfoptions.paginate === true;
6254
+ const measured = await bridgecall(tabid2, "measurepage");
6255
+ const breakpoints = (Array.isArray(options.breakpoints) ? options.breakpoints : []).filter((item) => typeof item === "string" && item.trim().length > 0);
6256
+ const resolvedbreaks = breakpoints.length > 0 ? (await bridgecall(tabid2, "pdfbreaks", breakpoints)).map((entry) => entry.top) : [];
6257
+ const segments = paginate ? pdfsegments(measured.scrollheight, measured.viewportheight, resolvedbreaks) : [{ top: 0, height: measured.scrollheight }];
6258
+ const texts = [];
6259
+ for (const segment of segments) {
6260
+ const collected = await bridgecall(tabid2, "pdfsegment", segment.top, segment.height);
6261
+ if (!collected.ok) throw new Error(collected.summary);
6262
+ texts.push(collected.text || " ");
6263
+ }
6264
+ const composed = buildpdf(texts, pdfoptions);
6265
+ const dataurl = `data:application/pdf;base64,${btoa(composed.document)}`;
6266
+ const name = await capturenamefor(step, plan, "capturepdf", "pdf");
6267
+ const record2 = { id: randomid(), runid: plan.id, stepid: step.id, pages: composed.pages, pagewidth: composed.pagewidth, pageheight: composed.pageheight, margins: pdfoptions.margins ?? { top: 0.4, right: 0.4, bottom: 0.4, left: 0.4 }, landscape: pdfoptions.landscape === true, scale: pdfoptions.scale ?? 1, bytes: composed.bytes, at: Date.now(), dataurl, name };
6268
+ await memory.addmedia(record2);
6269
+ await memory.setprogress(recordmedia(await memory.getprogress(), plan.id, step.id, { id: record2.id, kind: "pdf", scope: "tab", bytes: record2.bytes }, Date.now()));
6270
+ const exporttarget = options.exporttarget === "download" ? "download" : "memory";
6271
+ if (exporttarget === "download") {
6272
+ const granted = await chrome.permissions.contains({ permissions: ["downloads"] }).catch(() => false);
6273
+ if (!granted) throw new Error("The pdf download export needs the downloads capability; request it from the review panel.");
6274
+ await chrome.downloads.download({ url: dataurl, filename: name });
6275
+ }
6276
+ await refreshbadge();
6277
+ await audit("media", `Composed a derived pdf report of ${record2.pages} page${record2.pages === 1 ? "" : "s"} at ${record2.pagewidth} by ${record2.pageheight} points from ${segments.length} report segment${segments.length === 1 ? "" : "s"}, routed to ${exporttarget === "download" ? "the reviewed download flow" : "session memory"}.`, extra);
6278
+ return { ok: true, summary: `Composed a derived pdf report of ${record2.pages} page${record2.pages === 1 ? "" : "s"}.`, details: { media: { id: record2.id, kind: "pdf", bytes: record2.bytes }, pages: record2.pages, pagewidth: record2.pagewidth, pageheight: record2.pageheight, landscape: record2.landscape, scale: record2.scale, segments: segments.length, exporttarget, derivation: "the pdf is a text layer composed from the page segments because browser print apis stay outside the reviewed manifest" } };
6279
+ }
6280
+ if (step.kind === "recordscreen" || step.kind === "captureaudio") {
6281
+ const recordingoptions = recordingoptionsof(options.recording);
6282
+ const consentref = typeof options.consentref === "string" ? options.consentref : "";
6283
+ const consents = await memory.getrecordingconsents();
6284
+ const consent = consents.find((item) => item.id === consentref && item.approved === true && item.usedat === void 0);
6285
+ if (!consent) {
6286
+ const pending = { id: consentref || randomid(), prompt: typeof options.prompt === "string" && options.prompt ? options.prompt : step.summary, origin, stepid: step.id, at: Date.now() };
6287
+ await memory.setrecordingconsent(pending);
6288
+ await refreshbadge();
6289
+ await audit("media", `Recording consent prompt ${pending.id} opened for step ${step.id} on ${origin}; the recording waits for the user approval and every start needs its own prompt.`, extra);
6290
+ return { ok: false, summary: `The ${step.kind} waits for your recording consent approval${consentref ? ` under ref ${consentref}` : ""}; approve it in the review panel and run the step again.`, details: { consent: { id: pending.id, prompt: pending.prompt, origin, stepid: step.id, approved: false } } };
6291
+ }
6292
+ await memory.setrecordingconsent({ ...consent, usedat: Date.now() });
6293
+ const duration = typeof options.duration === "number" ? options.duration : recordingwindow(await memory.getsettings());
6294
+ if (duration === void 0) throw new Error("The recording needs a reviewed duration window in options or the user configured recording window before it starts.");
6295
+ const kind = step.kind === "recordscreen" ? "screen" : "audio";
6296
+ const recordoptions = { ...recordingoptions };
6297
+ if (kind === "audio" && recordoptions.audio === void 0) recordoptions.audio = true;
6298
+ const record2 = newrecording({ id: randomid(), runid: plan.id, stepid: step.id, tabid: tabid2, kind, options: recordoptions, at: Date.now() });
6299
+ activerecordings.set(record2.id, { record: record2, tabid: tabid2, stopat: Date.now() + duration });
6300
+ const interval = frameinterval(recordingoptions.fps ?? 1);
6301
+ const frames = [];
6302
+ let missed = 0;
6303
+ const deadline = Date.now() + duration;
6304
+ try {
6305
+ while (Date.now() < deadline && activerecordings.has(record2.id) && Date.now() < (activerecordings.get(record2.id)?.stopat ?? deadline)) {
6306
+ if (kind === "screen") {
6307
+ try {
6308
+ const shot = await tabshot("png", void 0);
6309
+ const measured = await bridgecall(tabid2, "measurepage");
6310
+ const frame = { id: randomid(), runid: plan.id, stepid: step.id, kind: "recordscreen", format: "png", width: measured.viewportwidth, height: measured.viewportheight, capturedat: Date.now(), bytes: shot, name: await capturenamefor(step, plan, "recordscreen", "png") };
6311
+ await memory.addcapture(frame);
6312
+ frames.push(frame.id);
6313
+ } catch {
6314
+ missed += 1;
6315
+ }
6316
+ } else {
6317
+ const elementstate = await bridgecall(tabid2, "mediaelements").catch(() => []);
6318
+ frames.push(`${Date.now()}:${elementstate.length}`);
6319
+ }
6320
+ await new Promise((resolve) => setTimeout(resolve, interval));
6321
+ }
6322
+ const finished = finishrecording({ ...record2, frames }, Date.now());
6323
+ const written = await recordingwriter(finished, options.exporttarget === "download" ? "download" : "memory");
6324
+ const stored = { ...finished, file: written.file, bytes: written.bytes };
6325
+ await memory.addmedia(stored);
6326
+ await memory.setprogress(recordmedia(await memory.getprogress(), plan.id, step.id, { id: stored.id, kind: stored.kind === "screen" ? "recording" : "audio", scope: stored.scope, bytes: stored.bytes ?? 0 }, Date.now()));
6327
+ await refreshbadge();
6328
+ await audit("media", `Recorded ${stored.duration ?? 0} milliseconds of ${stored.kind} activity of ${stored.scope} scope with ${frames.length - missed} frame${frames.length - missed === 1 ? "" : "s"}${missed > 0 ? ` and ${missed} missed capture${missed === 1 ? "" : "s"}` : ""} at ${stored.fps ?? 1} fps, stored as derived evidence with ${stored.bytes ?? 0} manifest bytes.`, extra);
6329
+ return { ok: true, summary: `Recorded ${stored.duration ?? 0} milliseconds of ${stored.kind} activity with ${frames.length - missed} frames.`, details: { media: { id: stored.id, kind: stored.kind === "screen" ? "recording" : "audio", bytes: stored.bytes ?? 0 }, recording: { id: stored.id, duration: stored.duration, scope: stored.scope, fps: stored.fps }, frames: frames.length, missed, derivation: "tab screencast stays outside the reviewed manifest, so the recording derives an ordered frame sequence and an honest manifest instead of encoded media bytes" } };
6330
+ } finally {
6331
+ activerecordings.delete(record2.id);
6332
+ }
6333
+ }
6334
+ if (step.kind === "captureframe") {
6335
+ const timestamp = typeof options.timestamp === "number" ? options.timestamp : void 0;
6336
+ const poster = options.poster === true;
6337
+ const grabbed = await bridgecall(tabid2, "videoframe", step.target ?? "", timestamp, poster);
6338
+ if (!grabbed.ok || !grabbed.dataurl) throw new Error(grabbed.summary);
6339
+ const capture = captureoptionsof(options.capture);
6340
+ const format = capture.format ?? "png";
6341
+ const record2 = { id: randomid(), runid: plan.id, stepid: step.id, source: step.target ?? "", timestamp: timestamp ?? 0, poster, format, width: grabbed.width ?? 0, height: grabbed.height ?? 0, at: Date.now(), dataurl: grabbed.dataurl, name: await capturenamefor(step, plan, "captureframe", format) };
6342
+ await memory.addmedia(record2);
6343
+ await memory.setprogress(recordmedia(await memory.getprogress(), plan.id, step.id, { id: record2.id, kind: "frame", scope: step.target ?? "", bytes: (record2.dataurl ?? "").length }, Date.now()));
6344
+ await refreshbadge();
6345
+ await audit("media", `Grabbed a video frame of ${record2.width} by ${record2.height} pixels at ${record2.timestamp} seconds from ${step.target ?? "the video element"}${poster ? " as the poster frame" : ""}.`, extra);
6346
+ return { ok: true, summary: `Grabbed the video frame at ${record2.timestamp} seconds.`, details: { media: { id: record2.id, kind: "frame", bytes: (record2.dataurl ?? "").length }, source: record2.source, timestamp: record2.timestamp, poster, width: record2.width, height: record2.height } };
6347
+ }
6348
+ if (step.kind === "downloadimages") {
6349
+ const filter = imagefilterof(options.imagefilter);
6350
+ const observed = await bridgecall(tabid2, "pageimages", filter.selector);
6351
+ const descriptors = observed.map((entry) => ({ url: typeof entry.url === "string" ? entry.url : "", alt: typeof entry.alt === "string" ? entry.alt : "", width: typeof entry.width === "number" ? entry.width : 0, height: typeof entry.height === "number" ? entry.height : 0, bytes: typeof entry.bytes === "number" ? entry.bytes : 0, mime: typeof entry.mime === "string" ? entry.mime : "" }));
6352
+ const matched = descriptors.filter((image) => imagematches(image, filter));
6353
+ const unique = dedupeimages(matched);
6354
+ const naming = options.naming && typeof options.naming === "object" && !Array.isArray(options.naming) ? options.naming : { run: true, step: true, sequence: true, kind: true };
6355
+ const names = imagenames(naming, plan.id, step.id, unique.length, "png");
6356
+ let downloaded = 0;
6357
+ for (let index = 0; index < unique.length; index += 1) {
6358
+ const image = unique[index];
6359
+ if (!image) continue;
6360
+ const rawextension = image.url.split("?")[0]?.split(".").pop()?.toLowerCase() ?? "";
6361
+ const extension = /^[a-z0-9]{2,5}$/.test(rawextension) ? rawextension : "png";
6362
+ const name = `${(names[index] ?? `image-${index + 1}`).replace(/\.[a-z0-9]+$/i, "")}.${extension}`;
6363
+ const downloadid = await chrome.downloads.download({ url: image.url, filename: name }).catch(() => void 0);
6364
+ if (downloadid !== void 0) downloaded += 1;
6365
+ }
6366
+ const batch = { id: randomid(), runid: plan.id, stepid: step.id, images: descriptors, matched: matched.length, downloaded, at: Date.now() };
6367
+ await memory.addimagebatch(batch);
6368
+ await memory.setprogress(recordmedia(await memory.getprogress(), plan.id, step.id, { id: batch.id, kind: "images", scope: filter.selector ?? "page", bytes: descriptors.reduce((total, image) => total + image.bytes, 0) }, Date.now()));
6369
+ await refreshbadge();
6370
+ await audit("media", `Detected ${descriptors.length} image${descriptors.length === 1 ? "" : "s"} on the page, matched ${matched.length} against the reviewed filter, deduplicated to ${unique.length} urls and downloaded ${downloaded} through the reviewed download flow.`, extra);
6371
+ return { ok: true, summary: `Downloaded ${downloaded} of ${unique.length} deduplicated matching images.`, details: { media: { id: batch.id, kind: "images", bytes: descriptors.reduce((total, image) => total + image.bytes, 0) }, observed: descriptors.length, matched: matched.length, unique: unique.length, downloaded, filter } };
6372
+ }
6373
+ if (step.kind === "shotcanvas") {
6374
+ const read = await bridgecall(tabid2, "canvasdata", step.target ?? "");
6375
+ if (!read.ok || !read.dataurl) throw new Error(read.summary);
6376
+ const capture = captureoptionsof(options.capture);
6377
+ const format = capture.format ?? "png";
6378
+ const record2 = { id: randomid(), runid: plan.id, stepid: step.id, element: step.target ?? "", context: read.context, width: read.width ?? 0, height: read.height ?? 0, format, at: Date.now(), dataurl: read.dataurl, name: await capturenamefor(step, plan, "shotcanvas", format) };
6379
+ await memory.addmedia(record2);
6380
+ await memory.setprogress(recordmedia(await memory.getprogress(), plan.id, step.id, { id: record2.id, kind: "canvas", scope: record2.element, bytes: (record2.dataurl ?? "").length }, Date.now()));
6381
+ await refreshbadge();
6382
+ await audit("media", `Read the ${record2.context} canvas buffer of ${step.target ?? "the canvas"} at ${record2.width} by ${record2.height} pixels${read.preserved === true ? " through a preserved readPixels pass" : ""}.`, extra);
6383
+ return { ok: true, summary: `Captured the ${record2.context} canvas content.`, details: { media: { id: record2.id, kind: "canvas", bytes: (record2.dataurl ?? "").length }, context: record2.context, preserved: read.preserved === true, element: record2.element, width: record2.width, height: record2.height } };
6384
+ }
6385
+ if (step.kind === "probestream") {
6386
+ const selector = typeof options.selector === "string" && options.selector ? options.selector : void 0;
6387
+ const raw = await bridgecall(tabid2, "streamelements", selector);
6388
+ const summaries = streamsummaries(raw);
6389
+ const records = summaries.map((summary) => ({ id: randomid(), runid: plan.id, stepid: step.id, kind: summary.kind, tracks: summary.tracks, label: summary.label, live: summary.live, at: Date.now(), ...summary.detail !== void 0 ? { detail: summary.detail } : {} }));
6390
+ for (const record2 of records) await memory.addmedia(record2);
6391
+ if (records.length > 0) await memory.setprogress(recordmedia(await memory.getprogress(), plan.id, step.id, { id: records[0]?.id ?? "", kind: "stream", scope: selector ?? "page", bytes: 0 }, Date.now()));
6392
+ await refreshbadge();
6393
+ await audit("media", `Probed ${records.length} media stream${records.length === 1 ? "" : "s"} of the page with ${records.reduce((total, record2) => total + record2.tracks, 0)} tracks; peer connection statistics stay outside the isolated world bridge.`, extra);
6394
+ return { ok: true, summary: `Probed ${records.length} media stream${records.length === 1 ? "" : "s"} with track details.`, details: { media: { id: records[0]?.id ?? "", kind: "stream", bytes: 0 }, streams: records, note: "peer connection statistics such as round trip time and frame drops need main world access that stays outside the reviewed bridge" } };
6395
+ }
6396
+ if (step.kind === "readmedia") {
6397
+ const raw = await bridgecall(tabid2, "mediaelements");
6398
+ const media = mediaentries(raw);
6399
+ await audit("media", `Read ${media.length} embedded media source${media.length === 1 ? "" : "s"} with formats, durations, dimensions, codecs and track lists.`, extra);
6400
+ return { ok: true, summary: `Read ${media.length} embedded media source${media.length === 1 ? "" : "s"}.`, details: { media: { id: step.id, kind: "media", bytes: 0 }, sources: media } };
6401
+ }
6402
+ if (step.kind === "readassets") {
6403
+ const raw = await bridgecall(tabid2, "pageassets");
6404
+ const entries = assetentries(raw);
6405
+ const records = entries.map((entry) => ({ id: randomid(), runid: plan.id, stepid: step.id, kind: entry.kind, url: entry.url, bytes: entry.bytes, at: Date.now(), ...entry.sizes !== void 0 ? { sizes: entry.sizes } : {} }));
6406
+ for (const record2 of records) await memory.addmedia(record2);
6407
+ if (records.length > 0) await memory.setprogress(recordmedia(await memory.getprogress(), plan.id, step.id, { id: records[0]?.id ?? "", kind: "asset", scope: "page", bytes: 0 }, Date.now()));
6408
+ await refreshbadge();
6409
+ await audit("media", `Collected ${records.length} page asset${records.length === 1 ? "" : "s"}: ${records.filter((record2) => record2.kind === "favicon").length} favicon and ${records.filter((record2) => record2.kind === "logo").length} logo entries with declared icon sizes.`, extra);
6410
+ return { ok: true, summary: `Collected ${records.length} page asset${records.length === 1 ? "" : "s"}.`, details: { media: { id: records[0]?.id ?? "", kind: "asset", bytes: 0 }, assets: records } };
6411
+ }
6412
+ if (step.kind === "timelapse") {
6413
+ const lapse = lapseplanof(options.lapse);
6414
+ if (!lapse) throw new Error("A reviewed lapse plan with interval, duration and format is required in options.");
6415
+ const timestamps = lapseframes(lapse);
6416
+ const capture = captureoptionsof(options.capture);
6417
+ const format = lapse.format ?? capture.format ?? "png";
6418
+ const frameids = [];
6419
+ for (let index = 0; index < timestamps.length; index += 1) {
6420
+ if (index > 0) await new Promise((resolve) => setTimeout(resolve, lapse.interval));
6421
+ const shot = await tabshot(format === "png" || format === "jpeg" ? format : "png", capture.quality);
6422
+ const measured = await bridgecall(tabid2, "measurepage");
6423
+ const frame = { id: randomid(), runid: plan.id, stepid: step.id, kind: "timelapse", format, width: measured.viewportwidth, height: measured.viewportheight, capturedat: Date.now(), bytes: shot, name: await capturenamefor(step, plan, "timelapse", format), target: `frame-${index + 1}` };
6424
+ await memory.addcapture(frame);
6425
+ frameids.push(frame.id);
6426
+ }
6427
+ await memory.setprogress(recordmedia(await memory.getprogress(), plan.id, step.id, { id: frameids[0] ?? "", kind: "timelapse", scope: "page", bytes: 0 }, Date.now()));
6428
+ await refreshbadge();
6429
+ await audit("media", `Captured a time lapse of ${frameids.length} frame${frameids.length === 1 ? "" : "s"} at ${lapse.interval} millisecond intervals over ${lapse.duration} milliseconds in ${format}.`, extra);
6430
+ return { ok: true, summary: `Captured a time lapse of ${frameids.length} frames.`, details: { media: { id: frameids[0] ?? "", kind: "timelapse", bytes: 0 }, frames: frameids, interval: lapse.interval, duration: lapse.duration, format } };
6431
+ }
6432
+ if (step.kind === "convertimage" || step.kind === "makethumbs") {
6433
+ const single = typeof options.capture === "string" ? options.capture : void 0;
6434
+ const list = Array.isArray(options.captures) ? options.captures.filter((item) => typeof item === "string" && item.trim().length > 0) : [];
6435
+ const ids = single !== void 0 ? [single] : list;
6436
+ const converted = [];
6437
+ for (const id of ids) {
6438
+ const source = await memory.getcapture(id);
6439
+ if (!source) throw new Error(`No stored capture matches ${id}.`);
6440
+ if (step.kind === "convertimage") {
6441
+ const directive = convertdirectiveof(options.convert);
6442
+ if (!directive) throw new Error("A reviewed convert directive with a target format is required in options.");
6443
+ const record2 = await convertonecapture(source, directive, plan, step);
6444
+ await memory.addcapture(record2);
6445
+ converted.push({ id: record2.id, format: record2.format, width: record2.width, height: record2.height, name: record2.name ?? "" });
6446
+ } else {
6447
+ const directive = thumbdirectiveof(options.thumb);
6448
+ if (!directive) throw new Error("A reviewed thumb directive with size, fit and suffix is required in options.");
6449
+ const record2 = await thumbonecapture(source, directive, plan, step);
6450
+ await memory.addcapture(record2);
6451
+ converted.push({ id: record2.id, format: record2.format, width: record2.width, height: record2.height, name: record2.name ?? "" });
6452
+ }
6453
+ }
6454
+ await memory.setprogress(recordmedia(await memory.getprogress(), plan.id, step.id, { id: converted[0]?.id ?? "", kind: step.kind === "convertimage" ? "convert" : "thumb", scope: "store", bytes: 0 }, Date.now()));
6455
+ await refreshbadge();
6456
+ await audit("media", `${step.kind === "convertimage" ? "Converted" : "Thumbnailed"} ${converted.length} stored capture${converted.length === 1 ? "" : "s"} into new derived capture records.`, extra);
6457
+ return { ok: true, summary: `${step.kind === "convertimage" ? "Converted" : "Thumbnailed"} ${converted.length} stored capture${converted.length === 1 ? "" : "s"}.`, details: { media: { id: converted[0]?.id ?? "", kind: step.kind === "convertimage" ? "convert" : "thumb", bytes: 0 }, converted } };
6458
+ }
6459
+ throw new Error("Unsupported media kind.");
6460
+ }
4944
6461
  async function enforcewindowreview(step, session, plan) {
4945
6462
  const windowid = step.value && /^\d+$/.test(step.value) ? Number.parseInt(step.value, 10) : 0;
4946
6463
  const progress = plan ? await memory.getprogress() : void 0;
@@ -4979,8 +6496,11 @@ async function refreshbadge() {
4979
6496
  const consents = (await memory.getclipconsents()).filter((record2) => record2.approved === void 0).length;
4980
6497
  const quarantined = (await memory.getquarantines()).filter((entry) => entry.scan === "pending").length;
4981
6498
  const datasets = (await memory.getdatasets()).length;
6499
+ const captures = (await memory.getcaptures()).length;
6500
+ const media = (await memory.getmediarecords()).length + (await memory.getimagebatches()).length;
6501
+ const recordingprompts = (await memory.getrecordingconsents()).filter((record2) => record2.approved === void 0).length;
4982
6502
  const tasktabs2 = new Set(badges.map((badge) => badge.tabid)).size;
4983
- const total = (queues?.prefetch ?? 0) + (queues?.batchopen ?? 0) + tasktabs2 + prompts + consents + quarantined + datasets;
6503
+ const total = (queues?.prefetch ?? 0) + (queues?.batchopen ?? 0) + tasktabs2 + prompts + consents + quarantined + datasets + captures + media + recordingprompts;
4984
6504
  await chrome.action.setBadgeText({ text: total > 0 ? String(total) : "" }).catch(() => {
4985
6505
  });
4986
6506
  }
@@ -4999,46 +6519,71 @@ async function executestep(stepid) {
4999
6519
  }
5000
6520
  let output;
5001
6521
  let watchwindow;
5002
- if (step.kind === "windowclose") {
5003
- await enforcewindowreview(step, session, plan);
5004
- }
5005
- if (istabscommandkind(step.kind)) {
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);
5011
- } else if (isformkind(step.kind)) {
5012
- output = await executeformstep(step, session, plan, tab.id, origin);
5013
- } else if (isbrowserkind(step.kind)) {
5014
- output = await runbrowseraction(step, tab.id, tab.windowId ?? chrome.windows.WINDOW_ID_CURRENT);
5015
- } else if (step.kind === "keyhold") {
5016
- output = await executekeyhold(step, session, plan, tab.id, origin);
5017
- } else if (step.kind === "keyrelease") {
5018
- output = await executekeyrelease(step, session, plan, tab.id, origin);
5019
- } else if (step.kind === "dismissdialog") {
5020
- output = await executedismissdialog(step, session, plan, tab.id, origin);
5021
- } else if (step.kind === "retryaction") {
5022
- output = await executeretryaction(step, session, plan, tab.id, origin);
5023
- } else if (step.kind === "mapclicks") {
5024
- output = await executemapclicks(step, plan, tab.id, origin);
5025
- } else if (step.kind === "enterframe") {
5026
- output = await executeenterframe(step, plan, tab.id, origin);
5027
- } else if (watchstepkinds.has(step.kind)) {
5028
- if (!session || !plan || plan.state !== "approved") throw new Error("Watch kinds refuse to run outside an approved session plan.");
5029
- const watched = await executewatchstep(step, session, plan, tab.id, origin);
5030
- output = watched.output;
5031
- watchwindow = { startedat: watched.watch.startedat, lifetime: watched.watch.lifetime };
5032
- } else if (step.kind === "diffsnapshots") {
5033
- output = await executediffsnapshots(step, session, plan, tab.id, origin);
5034
- } else if (navigationstepkinds.has(step.kind)) {
5035
- output = await executenavigationkind(step, session, plan, tab.id, origin);
5036
- } else {
5037
- if (step.target && freshcheckkinds.has(step.kind)) {
5038
- const fresh = await snapshot(tab.id);
5039
- if (!fresh.interactive.some((item) => item.selector === step.target)) throw new Error("The page changed and the target must be reviewed again.");
6522
+ const dispatchreviewedstep = async () => {
6523
+ if (step.kind === "windowclose") {
6524
+ await enforcewindowreview(step, session, plan);
6525
+ }
6526
+ if (istabscommandkind(step.kind)) {
6527
+ output = await executetabscommand(step, session, plan, tab.id);
6528
+ } else if (isdatasetkind(step.kind)) {
6529
+ output = await executedatastep(step, session, plan, tab.id, origin);
6530
+ } else if (isfileskind(step.kind)) {
6531
+ output = await executefilesstep(step, session, plan, tab.id, origin);
6532
+ } else if (isformkind(step.kind)) {
6533
+ output = await executeformstep(step, session, plan, tab.id, origin);
6534
+ } else if (isbrowserkind(step.kind)) {
6535
+ output = await runbrowseraction(step, tab.id, tab.windowId ?? chrome.windows.WINDOW_ID_CURRENT);
6536
+ } else if (step.kind === "keyhold") {
6537
+ output = await executekeyhold(step, session, plan, tab.id, origin);
6538
+ } else if (step.kind === "keyrelease") {
6539
+ output = await executekeyrelease(step, session, plan, tab.id, origin);
6540
+ } else if (step.kind === "dismissdialog") {
6541
+ output = await executedismissdialog(step, session, plan, tab.id, origin);
6542
+ } else if (step.kind === "retryaction") {
6543
+ output = await executeretryaction(step, session, plan, tab.id, origin);
6544
+ } else if (step.kind === "mapclicks") {
6545
+ output = await executemapclicks(step, plan, tab.id, origin);
6546
+ } else if (step.kind === "enterframe") {
6547
+ output = await executeenterframe(step, plan, tab.id, origin);
6548
+ } else if (watchstepkinds.has(step.kind)) {
6549
+ if (!session || !plan || plan.state !== "approved") throw new Error("Watch kinds refuse to run outside an approved session plan.");
6550
+ const watched = await executewatchstep(step, session, plan, tab.id, origin);
6551
+ output = watched.output;
6552
+ watchwindow = { startedat: watched.watch.startedat, lifetime: watched.watch.lifetime };
6553
+ } else if (step.kind === "diffsnapshots") {
6554
+ output = await executediffsnapshots(step, session, plan, tab.id, origin);
6555
+ } else if (navigationstepkinds.has(step.kind)) {
6556
+ output = await executenavigationkind(step, session, plan, tab.id, origin);
6557
+ } else if (iscapturekind(step.kind)) {
6558
+ output = await executecapturestep(step, session, plan, tab.id, origin);
6559
+ } else if (ismediakind(step.kind)) {
6560
+ output = await executemediastep(step, session, plan, tab.id, origin);
6561
+ } else {
6562
+ if (step.target && freshcheckkinds.has(step.kind)) {
6563
+ const fresh = await snapshot(tab.id);
6564
+ if (!fresh.interactive.some((item) => item.selector === step.target)) throw new Error("The page changed and the target must be reviewed again.");
6565
+ }
6566
+ output = await dispatchpagestep(step, tab.id, origin, plan);
5040
6567
  }
5041
- output = await dispatchpagestep(step, tab.id, origin, plan);
6568
+ return output;
6569
+ };
6570
+ const runplan = plan;
6571
+ const capturepolicystate = await runcapturepolicy();
6572
+ if (capturepolicystate === "beforeafter" && session && step.risk !== "read" && beforeafterwrapallowed(step.kind)) {
6573
+ const before = await grabstateshot(step, session, runplan, tab.id, "before");
6574
+ output = await dispatchreviewedstep();
6575
+ if (output?.ok) {
6576
+ const after = await grabstateshot(step, session, runplan, tab.id, "after");
6577
+ const domversion = await memory.getobservationversion();
6578
+ const paired = capturestates({ policy: "beforeafter", before, after, actionkind: step.kind, ...step.target ? { target: step.target } : {}, ...domversion !== void 0 ? { domsnapshotid: String(domversion) } : {}, at: Date.now(), id: randomid() });
6579
+ if (paired.pair) {
6580
+ await memory.addpair(paired.pair);
6581
+ await memory.setprogress(recordpair(await memory.getprogress(), runplan.id, step.id, paired.pair, Date.now()));
6582
+ await audit("capture", `Paired the before shot ${paired.pair.beforeid} with the after shot ${paired.pair.afterid} around the ${step.kind} action${paired.pair.domsnapshotid !== void 0 ? ` with dom snapshot ${paired.pair.domsnapshotid}` : ""}.`, { sessionid: session.id, planid: runplan.id, stepid: step.id });
6583
+ }
6584
+ }
6585
+ } else {
6586
+ output = await dispatchreviewedstep();
5042
6587
  }
5043
6588
  if (["navigate", "back", "forward"].includes(step.kind)) await recordnavigation(step, session, tab.id);
5044
6589
  await recordevidence(step, output, session, plan, origin);
@@ -5062,6 +6607,8 @@ async function executestep(stepid) {
5062
6607
  await updatetaskbadges(plan, tracked);
5063
6608
  await refreshbadge();
5064
6609
  if (iscomplete(tracked, plan) && plan.state === "approved") {
6610
+ await stoprecordingsforrun(plan.id).catch(() => {
6611
+ });
5065
6612
  const done = { ...plan, state: "completed", completedat: Date.now() };
5066
6613
  await memory.setplan(done);
5067
6614
  await audit("complete", "Every reviewed step of the approved plan has executed.", { ...session ? { sessionid: session.id } : {}, planid: done.id });
@@ -5196,6 +6743,20 @@ async function handlerequest(message, sender) {
5196
6743
  const capturecounters = await memory.getcapturecounters();
5197
6744
  const inventory = await memory.getinventory();
5198
6745
  const mimefilters = await memory.getmimefilters();
6746
+ const capturemetadata = (await memory.getcaptures()).map((record2) => {
6747
+ const { bytes, ...meta } = record2;
6748
+ void bytes;
6749
+ return meta;
6750
+ });
6751
+ const capturepairs = await memory.getpairs();
6752
+ const mediarecords = (await memory.getmediarecords()).map((record2) => {
6753
+ const { dataurl, ...meta } = record2;
6754
+ void dataurl;
6755
+ return meta;
6756
+ });
6757
+ const imagebatches = await memory.getimagebatches();
6758
+ const recordingconsents = await memory.getrecordingconsents();
6759
+ const runsettings = await memory.getsettings();
5199
6760
  const scanhooks = [];
5200
6761
  for (const hook of await memory.getscanhooks()) {
5201
6762
  scanhooks.push({ ...hook, granted: await chrome.permissions.contains({ origins: [hostpattern(hook.origin)] }).catch(() => false) });
@@ -5206,7 +6767,7 @@ async function handlerequest(message, sender) {
5206
6767
  const livetab = session ? await chrome.tabs.get(session.tabid).catch(() => void 0) : void 0;
5207
6768
  const waitprofile = session ? waitprofiles.find((record2) => record2.origin === session.origin) : void 0;
5208
6769
  const livestate = { phase: livetab?.status === "loading" ? "loading" : "complete", ...navrecords[0] ? { finalurl: navrecords[0].finalurl, redirects: navrecords[0].chain } : {} };
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 };
6770
+ return { config: await memory.getconfig(), session, plan, progress: plan && progress?.planid === plan.id ? progress : void 0, diagnostic: await memory.getdiagnostic(), audit: await memory.getaudit(), capabilities: await refreshcapabilities(), outcomes: await memory.getoutcomes(), holds: heldkeysreport({ tabid: session?.tabid ?? 0, holds }), dialogs: await memory.getdialogs(), retries: await memory.getretries(), ...signals ? { signals: signalsreport({ signals }) } : { signals: signalsreport({}) }, banners: await memory.getbanners(), mutationevents: await memory.getmutationevents(), focusevents: await memory.getfocusevents(), diffs: await memory.getdiffs(), selectors: await memory.getselectors(), ...a11y ? { a11y } : {}, ...reader ? { reader } : {}, ...map ? { map } : {}, trail: trailreport({ ...session ? { sessionid: session.id } : {}, trail }), navrecords, ratestates, safeties, curated, waitprofiles, auths, navcontrol, navqueues, artifacts, navstate: livestate, ...waitprofile ? { waitprofile } : {}, offline: !navigator.onLine, tabs, windows, layouts: layoutreport({ layouts }), tabgroups, tabmetas, badges, snapshots, closedtabs, tabwatchevents, clones, tasktabgauge: taskgauge, ...controltab ? { controltab } : {}, tabreport: report, profiles, tickets, wizards: wizardreport({ ...session ? { sessionid: session.id } : {}, wizards, picks }), picks, errorreports, captchas, detections, ...codeentry !== void 0 ? { codeentry: true } : {}, datasets, imports, extractsessions, streams, exports, provenances, taskrules, sheetendpoints: sheetgrants, downloads, netlogs, clipconsents, clips, quarantines, cleanuprules, cleanupruns, capturecounters, inventory, mimefilters, scanhooks, captures: capturemetadata, capturepairs, capturepolicy: runsettings?.capturepolicy ?? "manual", media: mediarecords, imagebatches, recordingconsents, recordingactive: [...activerecordings.values()].map((active) => ({ id: active.record.id, kind: active.record.kind, scope: active.record.scope, startedat: active.record.startedat, stopat: active.stopat })), recordingwindow: runsettings?.recordingwindow, ...stitchprogress.size > 0 ? { stitchprogress: [...stitchprogress.values()] } : {} };
5210
6771
  }
5211
6772
  case "capabilities":
5212
6773
  return refreshcapabilities();
@@ -5248,7 +6809,9 @@ async function handlerequest(message, sender) {
5248
6809
  const outcome = (await memory.getoutcomes()).find((candidate) => candidate.stepid === (input.stepid ?? ""));
5249
6810
  if (!outcome) throw new Error("No outcome exists for the reviewed step.");
5250
6811
  const resolved = outcome.details?.resolvedtarget;
5251
- return JSON.parse(outcomeresponse({ outcome, plan, ...resolved ? { resolvedtarget: resolved } : {} }));
6812
+ const capture = outcome.details?.capture;
6813
+ const media = outcome.details?.media;
6814
+ return JSON.parse(outcomeresponse({ outcome, plan, ...resolved ? { resolvedtarget: resolved } : {}, ...capture ? { capture } : {}, ...media ? { media } : {} }));
5252
6815
  }
5253
6816
  case "map": {
5254
6817
  const plan = await memory.getplan();
@@ -5612,8 +7175,180 @@ async function handlerequest(message, sender) {
5612
7175
  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
7176
  return { records, redacted: true, redaction: "every header value is redacted from exported netlogs" };
5614
7177
  }
7178
+ case "capturebytes": {
7179
+ const inputcapture = message;
7180
+ const record2 = await memory.getcapture(inputcapture.id ?? "");
7181
+ if (!record2) throw new Error("No stored capture matches the requested id.");
7182
+ if (!record2.bytes) throw new Error("The capture bytes expired from the retention window; the metadata stays for the audit trail.");
7183
+ return { id: record2.id, runid: record2.runid, kind: record2.kind, format: record2.format, width: record2.width, height: record2.height, capturedat: record2.capturedat, name: record2.name, annotated: record2.annotated === true, bytes: record2.bytes };
7184
+ }
7185
+ case "capturereport":
7186
+ return capturereport({ records: await memory.getcaptures(), pairs: await memory.getpairs() });
7187
+ case "copycapture": {
7188
+ const inputcopy = message;
7189
+ const session = await memory.getsession();
7190
+ if (!session || session.stoppedat || session.expiresat <= Date.now()) throw new Error("Capture clipboard copies stay behind the consent gate of an active session.");
7191
+ const record2 = await memory.getcapture(inputcopy.id ?? "");
7192
+ if (!record2) throw new Error("No stored capture matches the requested id.");
7193
+ if (!record2.bytes) throw new Error("The capture bytes expired from the retention window; the metadata stays for the audit trail.");
7194
+ const granted = await chrome.permissions.contains({ permissions: ["clipboardWrite"] }).catch(() => false);
7195
+ if (!granted) throw new Error("The capture copy needs the clipboardwrite capability; request it from the review panel.");
7196
+ const blob = await (await fetch(record2.bytes)).blob();
7197
+ await navigator.clipboard.write([new ClipboardItem({ [blob.type]: blob })]);
7198
+ await memory.addclip(clipentryof("screen", { hash: cliphash(record2.bytes), length: record2.bytes.length }, session.origin, record2.stepid, Date.now()));
7199
+ await audit("capture", `The review panel copied capture ${record2.id} of kind ${record2.kind} to the clipboard with payload hash ${cliphash(record2.bytes)}.`, { sessionid: session.id });
7200
+ return { id: record2.id, copied: true };
7201
+ }
7202
+ case "downloadcapture": {
7203
+ const inputdownloadcapture = message;
7204
+ const session = await memory.getsession();
7205
+ if (!session || session.stoppedat || session.expiresat <= Date.now()) throw new Error("Capture downloads stay behind the consent gate of an active session.");
7206
+ const record2 = await memory.getcapture(inputdownloadcapture.id ?? "");
7207
+ if (!record2) throw new Error("No stored capture matches the requested id.");
7208
+ if (!record2.bytes) throw new Error("The capture bytes expired from the retention window; the metadata stays for the audit trail.");
7209
+ const granted = await chrome.permissions.contains({ permissions: ["downloads"] }).catch(() => false);
7210
+ if (!granted) throw new Error("The capture download needs the downloads capability; request it from the review panel.");
7211
+ await chrome.downloads.download({ url: record2.bytes, filename: record2.name ?? `capture.${record2.format}` });
7212
+ await audit("capture", `The review panel downloaded capture ${record2.id} of kind ${record2.kind} through the reviewed download flow.`, { sessionid: session.id });
7213
+ return { id: record2.id, downloaded: true };
7214
+ }
7215
+ case "setcapturepolicy": {
7216
+ const inputpolicy = message;
7217
+ const mode = inputpolicy.mode === "off" || inputpolicy.mode === "manual" || inputpolicy.mode === "annotated" || inputpolicy.mode === "beforeafter" ? inputpolicy.mode : void 0;
7218
+ if (!mode) throw new Error("The capture policy must be off, manual, annotated or beforeafter.");
7219
+ const settings = await memory.getsettings();
7220
+ await memory.setsettings({ ...settings, capturepolicy: mode });
7221
+ await audit("configure", `The user set the capture policy of the run to ${mode}; beforeafter wraps page moving actions with state pairs.`);
7222
+ await refreshbadge();
7223
+ return { capturepolicy: mode };
7224
+ }
7225
+ case "mediareport": {
7226
+ const records = (await memory.getmediarecords()).map((record2) => {
7227
+ const { dataurl, ...meta } = record2;
7228
+ void dataurl;
7229
+ return meta;
7230
+ });
7231
+ return mediareport({ records, images: await memory.getimagebatches() });
7232
+ }
7233
+ case "mediabytes": {
7234
+ const inputmedia = message;
7235
+ const record2 = await memory.getmediarecord(inputmedia.id ?? "");
7236
+ if (!record2) throw new Error("No stored media record matches the requested id.");
7237
+ const bytes = "dataurl" in record2 ? record2.dataurl : void 0;
7238
+ if (!bytes) throw new Error("The media bytes expired from the retention window; the metadata stays for the audit trail.");
7239
+ const kind = "pages" in record2 ? "pdf" : "timestamp" in record2 ? "frame" : "canvas";
7240
+ return { id: record2.id, kind, bytes };
7241
+ }
7242
+ case "downloadmedia": {
7243
+ const inputmedia = message;
7244
+ const session = await memory.getsession();
7245
+ if (!session || session.stoppedat || session.expiresat <= Date.now()) throw new Error("Media downloads stay behind the consent gate of an active session.");
7246
+ const record2 = await memory.getmediarecord(inputmedia.id ?? "");
7247
+ if (!record2) throw new Error("No stored media record matches the requested id.");
7248
+ const bytes = "dataurl" in record2 ? record2.dataurl : void 0;
7249
+ if (!bytes) throw new Error("The media bytes expired from the retention window; the metadata stays for the audit trail.");
7250
+ const granted = await chrome.permissions.contains({ permissions: ["downloads"] }).catch(() => false);
7251
+ if (!granted) throw new Error("The media download needs the downloads capability; request it from the review panel.");
7252
+ const name = "name" in record2 ? record2.name ?? `media-${record2.id}` : `media-${record2.id}`;
7253
+ await chrome.downloads.download({ url: bytes, filename: name });
7254
+ await audit("media", `The review panel downloaded media record ${record2.id} through the reviewed download flow.`, { sessionid: session.id });
7255
+ return { id: record2.id, downloaded: true };
7256
+ }
7257
+ case "approverecordingconsent": {
7258
+ const inputrecordingconsent = message;
7259
+ const record2 = (await memory.getrecordingconsents()).find((item) => item.id === inputrecordingconsent.id);
7260
+ if (!record2) throw new Error("No recording consent prompt matches the requested id.");
7261
+ await memory.setrecordingconsent({ ...record2, approved: inputrecordingconsent.approved !== false });
7262
+ const session = await memory.getsession();
7263
+ await audit("media", `Recording consent prompt ${record2.id} for step ${record2.stepid} on ${record2.origin} ${inputrecordingconsent.approved !== false ? "approved" : "declined"} by the user; every recording start consumes its own prompt.`, { ...session ? { sessionid: session.id } : {} });
7264
+ await refreshbadge();
7265
+ return { id: record2.id, approved: inputrecordingconsent.approved !== false };
7266
+ }
7267
+ case "stoprecording": {
7268
+ const inputstop = message;
7269
+ const active = activerecordings.get(inputstop.id ?? "");
7270
+ if (!active) throw new Error("No active recording matches the requested id.");
7271
+ active.stopat = Date.now();
7272
+ const session = await memory.getsession();
7273
+ await audit("media", `The review panel requested a clean stop of recording ${active.record.id} of kind ${active.record.kind}.`, { ...session ? { sessionid: session.id } : {} });
7274
+ return { id: active.record.id, stopping: true };
7275
+ }
7276
+ case "recordingframes": {
7277
+ const inputframes = message;
7278
+ const record2 = await memory.getrecording(inputframes.id ?? "");
7279
+ if (!record2) throw new Error("No stored recording matches the requested id.");
7280
+ return { id: record2.id, kind: record2.kind, frames: record2.frames ?? [], interval: frameinterval(record2.fps ?? 1), scope: record2.scope, duration: record2.duration };
7281
+ }
7282
+ case "downloadrecording": {
7283
+ const inputrecording = message;
7284
+ const session = await memory.getsession();
7285
+ if (!session || session.stoppedat || session.expiresat <= Date.now()) throw new Error("Recording downloads stay behind the consent gate of an active session.");
7286
+ const record2 = await memory.getrecording(inputrecording.id ?? "");
7287
+ if (!record2) throw new Error("No stored recording matches the requested id.");
7288
+ const written = await recordingwriter(record2, "download");
7289
+ await memory.addmedia({ ...record2, file: written.file, bytes: written.bytes });
7290
+ await audit("media", `The review panel downloaded the recording manifest ${record2.id} of kind ${record2.kind} through the reviewed download flow.`, { sessionid: session.id });
7291
+ return { id: record2.id, downloaded: true, file: written.file };
7292
+ }
7293
+ case "deleterecording": {
7294
+ const inputdelete = message;
7295
+ const session = await memory.getsession();
7296
+ if (!session || session.stoppedat || session.expiresat <= Date.now()) throw new Error("Recording deletion stays behind the consent gate of an active session.");
7297
+ const record2 = await memory.getrecording(inputdelete.id ?? "");
7298
+ if (!record2) throw new Error("No stored recording matches the requested id.");
7299
+ await memory.removemedia(record2.id);
7300
+ await audit("media", `The review panel deleted the recording ${record2.id} of kind ${record2.kind}; the outcome evidence stays in the audit trail.`, { sessionid: session.id });
7301
+ await refreshbadge();
7302
+ return { id: record2.id, deleted: true };
7303
+ }
7304
+ case "convertcapture": {
7305
+ const inputconvert = message;
7306
+ const session = await memory.getsession();
7307
+ if (!session || session.stoppedat || session.expiresat <= Date.now()) throw new Error("Capture conversions stay behind the consent gate of an active session.");
7308
+ const plan = await memory.getplan();
7309
+ if (!plan) throw new Error("No plan is available for the capture conversion.");
7310
+ const source = await memory.getcapture(inputconvert.id ?? "");
7311
+ if (!source) throw new Error("No stored capture matches the requested id.");
7312
+ if (inputconvert.target !== "png" && inputconvert.target !== "jpeg" && inputconvert.target !== "webp") throw new Error("The conversion target must be png, jpeg or webp.");
7313
+ const record2 = await convertonecapture(source, { target: inputconvert.target, ...typeof inputconvert.quality === "number" ? { quality: inputconvert.quality } : {} }, plan, { id: `panel-${source.id}`, kind: "convertimage", summary: `Panel conversion of capture ${source.id}.`, risk: "read" });
7314
+ await memory.addcapture(record2);
7315
+ await audit("media", `The review panel converted capture ${source.id} from ${source.format} to ${record2.format}.`, { sessionid: session.id });
7316
+ await refreshbadge();
7317
+ return { id: record2.id, format: record2.format, name: record2.name };
7318
+ }
7319
+ case "thumbcapture": {
7320
+ const inputthumb = message;
7321
+ const session = await memory.getsession();
7322
+ if (!session || session.stoppedat || session.expiresat <= Date.now()) throw new Error("Capture thumbnails stay behind the consent gate of an active session.");
7323
+ const plan = await memory.getplan();
7324
+ if (!plan) throw new Error("No plan is available for the capture thumbnail.");
7325
+ const source = await memory.getcapture(inputthumb.id ?? "");
7326
+ if (!source) throw new Error("No stored capture matches the requested id.");
7327
+ const directive = thumbdirectiveof({ size: inputthumb.size, fit: inputthumb.fit, suffix: inputthumb.suffix });
7328
+ if (!directive) throw new Error("The thumbnail needs a reviewed size, a cover or contain fit and a naming suffix.");
7329
+ const record2 = await thumbonecapture(source, directive, plan, { id: `panel-${source.id}`, kind: "makethumbs", summary: `Panel thumbnail of capture ${source.id}.`, risk: "read" });
7330
+ await memory.addcapture(record2);
7331
+ await audit("media", `The review panel thumbnailed capture ${source.id} to ${record2.width} by ${record2.height} pixels with the ${directive.fit} fit.`, { sessionid: session.id });
7332
+ await refreshbadge();
7333
+ return { id: record2.id, width: record2.width, height: record2.height, name: record2.name };
7334
+ }
7335
+ case "setrecordingwindow": {
7336
+ const inputwindow = message;
7337
+ const window2 = inputwindow.window;
7338
+ if (typeof window2 !== "number" || !Number.isFinite(window2) || window2 <= 0) throw new Error("The recording window must be a positive number of milliseconds with no code ceiling.");
7339
+ const settings = await memory.getsettings();
7340
+ await memory.setsettings({ ...settings, recordingwindow: window2 });
7341
+ await audit("configure", `The user set the recording duration window of the run to ${window2} milliseconds; recordings never run past it.`);
7342
+ return { recordingwindow: window2 };
7343
+ }
5615
7344
  case "stop": {
5616
7345
  const session = await memory.getsession();
7346
+ for (const [id, active] of [...activerecordings.entries()]) {
7347
+ const finished = finishrecording(active.record, Date.now());
7348
+ await memory.addmedia(finished).catch(() => {
7349
+ });
7350
+ activerecordings.delete(id);
7351
+ }
5617
7352
  if (session) await memory.setsession({ ...session, stoppedat: Date.now() });
5618
7353
  const plan = await memory.getplan();
5619
7354
  if (plan && ["pending", "approved"].includes(plan.state)) await memory.setplan({ ...plan, state: "cancelled" });