@wenathlan/extension 1.1.40 → 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.
@@ -765,7 +765,78 @@ var sessionmemory = class {
765
765
  for (const capture of await this.getcaptures()) runs.set(capture.id, capture.runid);
766
766
  return records.filter((item) => runs.get(item.beforeid) === runid);
767
767
  }
768
+ /** Stores one media record of the 1.1.41 family with its bytes and step linkage, replacing the previous record of that id; the user configured media retention window expires the oldest bytes while the metadata and the recording index always survive. */
769
+ async addmedia(record2) {
770
+ const records = await this.getmediarecords();
771
+ const retention = (await this.getsettings())?.mediaretention;
772
+ const combined = [record2, ...records.filter((item) => item.id !== record2.id)];
773
+ const stored = retention === void 0 ? combined : combined.map((item, index) => index < retention ? item : expiremediabytes(item));
774
+ await this.adapter.set("media", stored);
775
+ }
776
+ /** Returns every stored media record, newest first. */
777
+ async getmediarecords() {
778
+ return await this.adapter.get("media") ?? [];
779
+ }
780
+ /** Returns the media records filtered by run and kind; an absent filter returns every record. */
781
+ async listmedia(filter) {
782
+ const records = await this.getmediarecords();
783
+ return records.filter((item) => (filter.runid === void 0 || item.runid === filter.runid) && (filter.kind === void 0 || mediakindof(item) === filter.kind));
784
+ }
785
+ /** Returns one media record by its id. */
786
+ async getmediarecord(id) {
787
+ return (await this.getmediarecords()).find((item) => item.id === id);
788
+ }
789
+ /** Returns one recording with its file reference and frame index by its id. */
790
+ async getrecording(id) {
791
+ const found = await this.getmediarecord(id);
792
+ return found !== void 0 && "startedat" in found ? found : void 0;
793
+ }
794
+ /** Removes one media record by its id; the audit trail keeps its outcome evidence. */
795
+ async removemedia(id) {
796
+ await this.adapter.set("media", (await this.getmediarecords()).filter((item) => item.id !== id));
797
+ }
798
+ /** Stores one observed image batch of a downloadimages step, replacing the previous batch of that id. */
799
+ async addimagebatch(batch) {
800
+ const records = await this.adapter.get("imagebatches") ?? [];
801
+ await this.adapter.set("imagebatches", [batch, ...records.filter((item) => item.id !== batch.id)]);
802
+ }
803
+ /** Returns every observed image batch with its filter match counts, newest first. */
804
+ async getimagebatches() {
805
+ return await this.adapter.get("imagebatches") ?? [];
806
+ }
807
+ /** Stores one recording consent decision of an origin, replacing the previous record of that id. */
808
+ async setrecordingconsent(record2) {
809
+ const records = (await this.adapter.get("recordingconsents") ?? []).filter((item) => item.id !== record2.id);
810
+ await this.adapter.set("recordingconsents", [record2, ...records]);
811
+ }
812
+ /** Returns every recording consent decision with its prompt and origin, newest first. */
813
+ async getrecordingconsents() {
814
+ return await this.adapter.get("recordingconsents") ?? [];
815
+ }
768
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
+ }
769
840
  function expirecapturebytes(record2) {
770
841
  const { bytes, ...metadata } = record2;
771
842
  void bytes;
@@ -776,12 +847,12 @@ function randomid() {
776
847
  }
777
848
 
778
849
  // policy.ts
779
- var sensitiveactions = /* @__PURE__ */ new Set(["click", "type", "navigate", "select", "presskey", "drag", "drop", "upload", "clear", "check", "uncheck", "toggle", "submit", "reload", "back", "forward", "writestorage", "setattribute", "removeattribute", "evaluate", "tabcreate", "tabactivate", "tabclose", "tabreload", "windowcreate", "windowclose", "windowresize", "downloadfile", "clickpoint", "shiftclick", "dismissdialog", "enterframe", "typetime", "appendtext", "setvalue", "typeedit", "keyhold", "keyrelease", "submitsearch", "selectmulti", "chooseradio", "setslider", "setdate", "setcolor", "openlink", "openprivate", "reloadcache", "stopnav", "followlink", "spanav", "rewritequery", "setfragment", "navlist", "navprofile", "handleauth", "printpdf", "prefetch", "preconnect", "deeplink", "reopentab", "pausenav", "navrate", "openclipboard", "batchopen", "duplicatetab", "closepattern", "pintab", "mutetab", "movetab", "movetabwindow", "grouptabs", "colorgroup", "collapsegroup", "discardtab", "reloadtabs", "zoomin", "zoomout", "switchtab", "maximizewindow", "minimizewindow", "restorewindow", "focuswindow", "scratchwindow", "incognitowindow", "restoretab", "restorelayout", "reopenrun", "badgetab", "fillform", "filllabel", "fillplaceholder", "submitform", "retryform", "runwizard", "selectchain", "picktypeahead", "pickdate", "attachfile", "fillcard", "fillcode", "consentpassword", "exportcsv", "exportjson", "exportexcel", "copytable", "pushsheets", "streamdisk", "paginateextract", "resumeextract", "batchdownload", "pausedownload", "resumedownload", "interceptmime", "readclipboard", "writeclipboard", "copyscreen", "quarantinedownload", "scanvirus", "cleanupartifacts"]);
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"]);
780
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"]);
781
- var readactions = /* @__PURE__ */ new Set(["observe", "inspect", "extract", "wait", "waitfor", "waittext", "readattribute", "readstyle", "readgeometry", "readvalue", "readtext", "readhtml", "countelements", "readtable", "readlinks", "readimages", "readmeta", "readforms", "readstorage", "highlight", "tablist", "windowlist", "tabsnapshot", "mapclicks", "verifyvisible", "verifyenabled", "resolvexpath", "a11ytree", "readvisible", "readertree", "detectlists", "detecttables", "readjson", "watchmutate", "waitquiet", "watchbanner", "detectinfinitescroll", "detectvirtual", "detectlazy", "readscrollpos", "readlang", "readoutline", "countpages", "listshadow", "listframes", "classifypage", "fingerprintsection", "diffsnapshots", "readselection", "watchfocus", "detectsticky", "detectscrolllock", "readopengraph", "detectlanguage", "deriveselector", "waitload", "waiturl", "spawait", "detecthttp", "readredirects", "readfinalurl", "trailaudit", "navintent", "checksafe", "querytabs", "watchtab", "findclones", "searchtabs", "listaudio", "snapshotsession", "savelayout", "attachmeta", "detectfields", "generatevalues", "saveprofiles", "asksubmit", "readerrors", "skiphoneypot", "detectlogin", "detecttemplate", "handoffcaptcha", "scrapetable", "importcsv", "looprows", "transformvalues", "deduperows", "mergepages", "stamplerows", "previewgrid", "logprovenance", "verifydownload", "exportnetlog", "namecaptures", "shotview", "shotfullpage", "shotelement", "shotregion", "contactsheet"]);
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"]);
782
853
  var allowedactions = /* @__PURE__ */ new Set([...sensitiveactions, ...interactionactions, ...readactions]);
783
854
  var watchactions = /* @__PURE__ */ new Set(["watchmutate", "watchbanner", "watchfocus", "watchtab"]);
784
- var targetactions = /* @__PURE__ */ new Set(["inspect", "focus", "click", "type", "scroll", "select", "hover", "clickdeep", "rightclick", "doubleclick", "drag", "drop", "upload", "clear", "check", "uncheck", "toggle", "submit", "readattribute", "readstyle", "readgeometry", "readvalue", "readtext", "readhtml", "countelements", "readtable", "highlight", "setattribute", "removeattribute", "waitfor", "shiftclick", "typetime", "appendtext", "setvalue", "typeedit", "submitsearch", "selectmulti", "chooseradio", "setslider", "setdate", "setcolor", "expanddetails", "verifyvisible", "verifyenabled", "pierceshadow", "deriveselector", "fingerprintsection", "submitform", "retryform", "selectchain", "picktypeahead", "pickdate", "attachfile", "fillcode", "consentpassword", "scrapetable", "paginateextract", "shotelement"]);
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"]);
785
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"]);
786
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"]);
787
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"]);
@@ -789,6 +860,7 @@ var datasetactions = /* @__PURE__ */ new Set(["scrapetable", "exportcsv", "expor
789
860
  var exportactions = /* @__PURE__ */ new Set(["exportcsv", "exportjson", "exportexcel", "copytable", "pushsheets", "streamdisk"]);
790
861
  var filesactions = /* @__PURE__ */ new Set(["batchdownload", "pausedownload", "resumedownload", "verifydownload", "interceptmime", "exportnetlog", "readclipboard", "writeclipboard", "copyscreen", "quarantinedownload", "scanvirus", "namecaptures", "cleanupartifacts"]);
791
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"]);
792
864
  var fieldkinds = ["text", "email", "phone", "date", "number", "select", "check", "radio", "file", "password", "card", "code"];
793
865
  var layoutmutationactions = /* @__PURE__ */ new Set(["grouptabs", "colorgroup", "collapsegroup", "savelayout", "restorelayout"]);
794
866
  var groupcolors = ["grey", "blue", "red", "yellow", "green", "pink", "purple", "cyan", "orange"];
@@ -827,6 +899,7 @@ function requiredcapability(kind) {
827
899
  if (kind === "batchdownload" || kind === "pausedownload" || kind === "resumedownload" || kind === "verifydownload" || kind === "interceptmime" || kind === "quarantinedownload" || kind === "scanvirus") return "downloads";
828
900
  if (kind === "readclipboard") return "clipboardRead";
829
901
  if (kind === "writeclipboard" || kind === "copyscreen") return "clipboardWrite";
902
+ if (kind === "downloadimages") return "downloads";
830
903
  if (kind === "openlink" || kind === "openprivate" || kind === "navlist" || kind === "batchopen" || kind === "reopentab" || kind === "deeplink") return "tabs";
831
904
  if (tabscommandactions.has(kind)) return "tabs";
832
905
  return void 0;
@@ -1514,6 +1587,144 @@ function validatetabsgrammar(step, options) {
1514
1587
  if (kind === "reopenrun" && !isnonempty(options.run)) return { allowed: false, reason: "A reviewed run id is required in options to reopen its tabs." };
1515
1588
  return { allowed: true };
1516
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
+ }
1517
1728
  function validatestep(step, origin) {
1518
1729
  if (!allowedactions.has(step.kind)) return { allowed: false, reason: "Unsupported action kind." };
1519
1730
  if (!step.summary.trim()) return { allowed: false, reason: "A human-readable action summary is required." };
@@ -1735,6 +1946,10 @@ function validatestep(step, origin) {
1735
1946
  const capturecheck = validatecapturegrammar(step, options);
1736
1947
  if (!capturecheck.allowed) return capturecheck;
1737
1948
  }
1949
+ if (ismediakind(step.kind)) {
1950
+ const mediacheck = validatemediagrammar(step, options);
1951
+ if (!mediacheck.allowed) return mediacheck;
1952
+ }
1738
1953
  if (step.kind === "tabcreate") {
1739
1954
  if (options.background !== void 0 && typeof options.background !== "boolean") return { allowed: false, reason: "The reviewed background flag must be a boolean." };
1740
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." };
@@ -1806,6 +2021,14 @@ function canexecute(input) {
1806
2021
  const target = captureoptions.capture?.exporttarget;
1807
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." };
1808
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
+ }
1809
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") {
1810
2033
  let options = {};
1811
2034
  try {
@@ -1927,9 +2150,14 @@ function recordpair(progress, planid, stepid, pair, now) {
1927
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 };
1928
2151
  return recordoutcome(base, planid, outcome, now);
1929
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
+ }
1930
2158
 
1931
2159
  // version.ts
1932
- var packageversion = "1.1.40";
2160
+ var packageversion = "1.1.41";
1933
2161
 
1934
2162
  // types.ts
1935
2163
  var protocolversion = packageversion;
@@ -1993,7 +2221,7 @@ function requestbody(input) {
1993
2221
  return JSON.stringify({ version: protocolversion, objective: input.objective, session: input.session, observation: input.observation, capabilities: input.capabilities });
1994
2222
  }
1995
2223
  function outcomeresponse(input) {
1996
- return JSON.stringify({ version: protocolversion, planid: input.plan.id, planstate: input.plan.state, outcome: input.outcome, ...input.resolvedtarget ? { resolvedtarget: input.resolvedtarget } : {}, ...input.capture ? { capture: input.capture } : {} });
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 } : {} });
1997
2225
  }
1998
2226
  function mapresponse(input) {
1999
2227
  return JSON.stringify({ version: protocolversion, planid: input.plan.id, planstate: input.plan.state, map: input.map });
@@ -2058,6 +2286,9 @@ function quarantinereport(input) {
2058
2286
  function capturereport(input) {
2059
2287
  return { version: protocolversion, records: input.records, pairs: input.pairs };
2060
2288
  }
2289
+ function mediareport(input) {
2290
+ return { version: protocolversion, records: input.records, images: input.images };
2291
+ }
2061
2292
 
2062
2293
  // capture.ts
2063
2294
  var capturekinds = ["shotview", "shotfullpage", "shotelement", "shotregion", "contactsheet"];
@@ -2245,6 +2476,308 @@ function annotationplanof(input) {
2245
2476
  return plan;
2246
2477
  }
2247
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
+ }
2780
+
2248
2781
  // extension/browsertabs.ts
2249
2782
  var browserkinds = /* @__PURE__ */ new Set(["tablist", "tabcreate", "tabactivate", "tabclose", "tabreload", "tabsnapshot", "windowlist", "windowcreate", "windowclose", "zoomset", "windowresize", "downloadfile"]);
2250
2783
  function isbrowserkind(kind) {
@@ -3444,9 +3977,9 @@ function stepoptions2(step) {
3444
3977
  }
3445
3978
  async function refreshcapabilities() {
3446
3979
  const report = await readcapabilities();
3447
- const withcaptures = { ...report, captures: [...capturekinds] };
3448
- await memory.setcapabilities(withcaptures);
3449
- return withcaptures;
3980
+ const withmedia = { ...report, captures: [...capturekinds], media: [...mediakinds] };
3981
+ await memory.setcapabilities(withmedia);
3982
+ return withmedia;
3450
3983
  }
3451
3984
  async function activecontext() {
3452
3985
  const [tab] = await chrome.tabs.query({ active: true, lastFocusedWindow: true });
@@ -3600,6 +4133,7 @@ function stepauditkind(step, ok) {
3600
4133
  if (step.kind === "consentpassword") return "consent";
3601
4134
  if (step.kind === "handoffcaptcha") return "handoff";
3602
4135
  if (iscapturekind(step.kind)) return "capture";
4136
+ if (ismediakind(step.kind)) return "media";
3603
4137
  if (isfileskind(step.kind)) {
3604
4138
  if (step.kind === "interceptmime") return "intercept";
3605
4139
  if (step.kind === "readclipboard" || step.kind === "writeclipboard" || step.kind === "copyscreen") return "clipboard";
@@ -5653,6 +6187,277 @@ async function executecapturestep(step, session, plan, tabid2, origin) {
5653
6187
  }
5654
6188
  throw new Error("Unsupported capture kind.");
5655
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
+ }
5656
6461
  async function enforcewindowreview(step, session, plan) {
5657
6462
  const windowid = step.value && /^\d+$/.test(step.value) ? Number.parseInt(step.value, 10) : 0;
5658
6463
  const progress = plan ? await memory.getprogress() : void 0;
@@ -5692,8 +6497,10 @@ async function refreshbadge() {
5692
6497
  const quarantined = (await memory.getquarantines()).filter((entry) => entry.scan === "pending").length;
5693
6498
  const datasets = (await memory.getdatasets()).length;
5694
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;
5695
6502
  const tasktabs2 = new Set(badges.map((badge) => badge.tabid)).size;
5696
- const total = (queues?.prefetch ?? 0) + (queues?.batchopen ?? 0) + tasktabs2 + prompts + consents + quarantined + datasets + captures;
6503
+ const total = (queues?.prefetch ?? 0) + (queues?.batchopen ?? 0) + tasktabs2 + prompts + consents + quarantined + datasets + captures + media + recordingprompts;
5697
6504
  await chrome.action.setBadgeText({ text: total > 0 ? String(total) : "" }).catch(() => {
5698
6505
  });
5699
6506
  }
@@ -5749,6 +6556,8 @@ async function executestep(stepid) {
5749
6556
  output = await executenavigationkind(step, session, plan, tab.id, origin);
5750
6557
  } else if (iscapturekind(step.kind)) {
5751
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);
5752
6561
  } else {
5753
6562
  if (step.target && freshcheckkinds.has(step.kind)) {
5754
6563
  const fresh = await snapshot(tab.id);
@@ -5798,6 +6607,8 @@ async function executestep(stepid) {
5798
6607
  await updatetaskbadges(plan, tracked);
5799
6608
  await refreshbadge();
5800
6609
  if (iscomplete(tracked, plan) && plan.state === "approved") {
6610
+ await stoprecordingsforrun(plan.id).catch(() => {
6611
+ });
5801
6612
  const done = { ...plan, state: "completed", completedat: Date.now() };
5802
6613
  await memory.setplan(done);
5803
6614
  await audit("complete", "Every reviewed step of the approved plan has executed.", { ...session ? { sessionid: session.id } : {}, planid: done.id });
@@ -5938,6 +6749,13 @@ async function handlerequest(message, sender) {
5938
6749
  return meta;
5939
6750
  });
5940
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();
5941
6759
  const runsettings = await memory.getsettings();
5942
6760
  const scanhooks = [];
5943
6761
  for (const hook of await memory.getscanhooks()) {
@@ -5949,7 +6767,7 @@ async function handlerequest(message, sender) {
5949
6767
  const livetab = session ? await chrome.tabs.get(session.tabid).catch(() => void 0) : void 0;
5950
6768
  const waitprofile = session ? waitprofiles.find((record2) => record2.origin === session.origin) : void 0;
5951
6769
  const livestate = { phase: livetab?.status === "loading" ? "loading" : "complete", ...navrecords[0] ? { finalurl: navrecords[0].finalurl, redirects: navrecords[0].chain } : {} };
5952
- return { config: await memory.getconfig(), session, plan, progress: plan && progress?.planid === plan.id ? progress : void 0, diagnostic: await memory.getdiagnostic(), audit: await memory.getaudit(), capabilities: await refreshcapabilities(), outcomes: await memory.getoutcomes(), holds: heldkeysreport({ tabid: session?.tabid ?? 0, holds }), dialogs: await memory.getdialogs(), retries: await memory.getretries(), ...signals ? { signals: signalsreport({ signals }) } : { signals: signalsreport({}) }, banners: await memory.getbanners(), mutationevents: await memory.getmutationevents(), focusevents: await memory.getfocusevents(), diffs: await memory.getdiffs(), selectors: await memory.getselectors(), ...a11y ? { a11y } : {}, ...reader ? { reader } : {}, ...map ? { map } : {}, trail: trailreport({ ...session ? { sessionid: session.id } : {}, trail }), navrecords, ratestates, safeties, curated, waitprofiles, auths, navcontrol, navqueues, artifacts, navstate: livestate, ...waitprofile ? { waitprofile } : {}, offline: !navigator.onLine, tabs, windows, layouts: layoutreport({ layouts }), tabgroups, tabmetas, badges, snapshots, closedtabs, tabwatchevents, clones, tasktabgauge: taskgauge, ...controltab ? { controltab } : {}, tabreport: report, profiles, tickets, wizards: wizardreport({ ...session ? { sessionid: session.id } : {}, wizards, picks }), picks, errorreports, captchas, detections, ...codeentry !== void 0 ? { codeentry: true } : {}, datasets, imports, extractsessions, streams, exports, provenances, taskrules, sheetendpoints: sheetgrants, downloads, netlogs, clipconsents, clips, quarantines, cleanuprules, cleanupruns, capturecounters, inventory, mimefilters, scanhooks, captures: capturemetadata, capturepairs, capturepolicy: runsettings?.capturepolicy ?? "manual", ...stitchprogress.size > 0 ? { stitchprogress: [...stitchprogress.values()] } : {} };
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()] } : {} };
5953
6771
  }
5954
6772
  case "capabilities":
5955
6773
  return refreshcapabilities();
@@ -5992,7 +6810,8 @@ async function handlerequest(message, sender) {
5992
6810
  if (!outcome) throw new Error("No outcome exists for the reviewed step.");
5993
6811
  const resolved = outcome.details?.resolvedtarget;
5994
6812
  const capture = outcome.details?.capture;
5995
- return JSON.parse(outcomeresponse({ outcome, plan, ...resolved ? { resolvedtarget: resolved } : {}, ...capture ? { capture } : {} }));
6813
+ const media = outcome.details?.media;
6814
+ return JSON.parse(outcomeresponse({ outcome, plan, ...resolved ? { resolvedtarget: resolved } : {}, ...capture ? { capture } : {}, ...media ? { media } : {} }));
5996
6815
  }
5997
6816
  case "map": {
5998
6817
  const plan = await memory.getplan();
@@ -6403,8 +7222,133 @@ async function handlerequest(message, sender) {
6403
7222
  await refreshbadge();
6404
7223
  return { capturepolicy: mode };
6405
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
+ }
6406
7344
  case "stop": {
6407
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
+ }
6408
7352
  if (session) await memory.setsession({ ...session, stoppedat: Date.now() });
6409
7353
  const plan = await memory.getplan();
6410
7354
  if (plan && ["pending", "approved"].includes(plan.state)) await memory.setplan({ ...plan, state: "cancelled" });