@wenathlan/extension 1.1.39 → 1.1.40
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -4
- package/dist/capture.d.ts +161 -0
- package/dist/capture.d.ts.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +360 -4
- package/dist/index.js.map +3 -3
- package/dist/memory.d.ts +17 -1
- package/dist/memory.d.ts.map +1 -1
- package/dist/policy.d.ts +19 -1
- package/dist/policy.d.ts.map +1 -1
- package/dist/protocol.d.ts +18 -2
- package/dist/protocol.d.ts.map +1 -1
- package/dist/types.d.ts +89 -4
- package/dist/types.d.ts.map +1 -1
- package/dist/version.d.ts +1 -1
- package/extension/dist/background.js +841 -50
- package/extension/dist/background.js.map +4 -4
- package/extension/dist/manifest.json +1 -1
- package/extension/dist/pagebridge.js +53 -2
- package/extension/dist/pagebridge.js.map +2 -2
- package/extension/dist/popup.html +1 -1
- package/extension/dist/popup.js +9 -1
- package/extension/dist/popup.js.map +2 -2
- package/extension/dist/sidepanel.html +1 -1
- package/extension/dist/sidepanel.js +138 -1
- package/extension/dist/sidepanel.js.map +2 -2
- package/extension/dist/style.css +2 -0
- package/extension/manifest.json +1 -1
- package/package.json +1 -1
|
@@ -731,7 +731,46 @@ var sessionmemory = class {
|
|
|
731
731
|
async getmimefilters() {
|
|
732
732
|
return await this.adapter.get("mimefilters") ?? [];
|
|
733
733
|
}
|
|
734
|
+
/** Stores one capture record with its bytes and step linkage, replacing the previous record of that id; the user configured capture retention window expires the oldest bytes while the metadata always survives for the audit trail. */
|
|
735
|
+
async addcapture(record2) {
|
|
736
|
+
const records = await this.getcaptures();
|
|
737
|
+
const retention = (await this.getsettings())?.captureretention;
|
|
738
|
+
const combined = [record2, ...records.filter((item) => item.id !== record2.id)];
|
|
739
|
+
const stored = retention === void 0 ? combined : combined.map((item, index) => index < retention ? item : expirecapturebytes(item));
|
|
740
|
+
await this.adapter.set("captures", stored);
|
|
741
|
+
}
|
|
742
|
+
/** Returns every stored capture record with its metadata, newest first. */
|
|
743
|
+
async getcaptures() {
|
|
744
|
+
return await this.adapter.get("captures") ?? [];
|
|
745
|
+
}
|
|
746
|
+
/** Returns one capture record with its bytes by its id. */
|
|
747
|
+
async getcapture(id) {
|
|
748
|
+
return (await this.getcaptures()).find((item) => item.id === id);
|
|
749
|
+
}
|
|
750
|
+
/** Returns the capture records filtered by run, step and kind. */
|
|
751
|
+
async listcaptures(filter) {
|
|
752
|
+
const records = await this.getcaptures();
|
|
753
|
+
return records.filter((item) => (filter.runid === void 0 || item.runid === filter.runid) && (filter.stepid === void 0 || item.stepid === filter.stepid) && (filter.kind === void 0 || item.kind === filter.kind));
|
|
754
|
+
}
|
|
755
|
+
/** Records one before and after shotpair of the run with its action context. */
|
|
756
|
+
async addpair(pair) {
|
|
757
|
+
const records = await this.getpairs();
|
|
758
|
+
await this.adapter.set("capturepairs", [pair, ...records.filter((item) => item.id !== pair.id)]);
|
|
759
|
+
}
|
|
760
|
+
/** Returns the shotpairs of one run resolved through their before records, newest first; an absent run returns every pair. */
|
|
761
|
+
async getpairs(runid) {
|
|
762
|
+
const records = await this.adapter.get("capturepairs") ?? [];
|
|
763
|
+
if (runid === void 0) return records;
|
|
764
|
+
const runs = /* @__PURE__ */ new Map();
|
|
765
|
+
for (const capture of await this.getcaptures()) runs.set(capture.id, capture.runid);
|
|
766
|
+
return records.filter((item) => runs.get(item.beforeid) === runid);
|
|
767
|
+
}
|
|
734
768
|
};
|
|
769
|
+
function expirecapturebytes(record2) {
|
|
770
|
+
const { bytes, ...metadata } = record2;
|
|
771
|
+
void bytes;
|
|
772
|
+
return { ...metadata, bytesexpired: true };
|
|
773
|
+
}
|
|
735
774
|
function randomid() {
|
|
736
775
|
return crypto.randomUUID();
|
|
737
776
|
}
|
|
@@ -739,16 +778,17 @@ function randomid() {
|
|
|
739
778
|
// policy.ts
|
|
740
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"]);
|
|
741
780
|
var interactionactions = /* @__PURE__ */ new Set(["focus", "scroll", "hover", "clickdeep", "rightclick", "doubleclick", "scrollpage", "scrollby", "scrollend", "scrolltop", "fullscreen", "zoomset", "movepointer", "clicktext", "clickaria", "clickname", "expanddetails", "pierceshadow", "retryaction"]);
|
|
742
|
-
var readactions = /* @__PURE__ */ new Set(["observe", "inspect", "extract", "wait", "waitfor", "waittext", "readattribute", "readstyle", "readgeometry", "readvalue", "readtext", "readhtml", "countelements", "readtable", "readlinks", "readimages", "readmeta", "readforms", "readstorage", "highlight", "tablist", "windowlist", "tabsnapshot", "mapclicks", "verifyvisible", "verifyenabled", "resolvexpath", "a11ytree", "readvisible", "readertree", "detectlists", "detecttables", "readjson", "watchmutate", "waitquiet", "watchbanner", "detectinfinitescroll", "detectvirtual", "detectlazy", "readscrollpos", "readlang", "readoutline", "countpages", "listshadow", "listframes", "classifypage", "fingerprintsection", "diffsnapshots", "readselection", "watchfocus", "detectsticky", "detectscrolllock", "readopengraph", "detectlanguage", "deriveselector", "waitload", "waiturl", "spawait", "detecthttp", "readredirects", "readfinalurl", "trailaudit", "navintent", "checksafe", "querytabs", "watchtab", "findclones", "searchtabs", "listaudio", "snapshotsession", "savelayout", "attachmeta", "detectfields", "generatevalues", "saveprofiles", "asksubmit", "readerrors", "skiphoneypot", "detectlogin", "detecttemplate", "handoffcaptcha", "scrapetable", "importcsv", "looprows", "transformvalues", "deduperows", "mergepages", "stamplerows", "previewgrid", "logprovenance", "verifydownload", "exportnetlog", "namecaptures"]);
|
|
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"]);
|
|
743
782
|
var allowedactions = /* @__PURE__ */ new Set([...sensitiveactions, ...interactionactions, ...readactions]);
|
|
744
783
|
var watchactions = /* @__PURE__ */ new Set(["watchmutate", "watchbanner", "watchfocus", "watchtab"]);
|
|
745
|
-
var targetactions = /* @__PURE__ */ new Set(["inspect", "focus", "click", "type", "scroll", "select", "hover", "clickdeep", "rightclick", "doubleclick", "drag", "drop", "upload", "clear", "check", "uncheck", "toggle", "submit", "readattribute", "readstyle", "readgeometry", "readvalue", "readtext", "readhtml", "countelements", "readtable", "highlight", "setattribute", "removeattribute", "waitfor", "shiftclick", "typetime", "appendtext", "setvalue", "typeedit", "submitsearch", "selectmulti", "chooseradio", "setslider", "setdate", "setcolor", "expanddetails", "verifyvisible", "verifyenabled", "pierceshadow", "deriveselector", "fingerprintsection", "submitform", "retryform", "selectchain", "picktypeahead", "pickdate", "attachfile", "fillcode", "consentpassword", "scrapetable", "paginateextract"]);
|
|
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"]);
|
|
746
785
|
var valueactions = /* @__PURE__ */ new Set(["presskey", "drag", "drop", "upload", "readattribute", "removeattribute", "waittext", "evaluate", "zoomset", "tabactivate", "tabclose", "tabreload", "windowclose", "windowresize", "tabcreate", "windowcreate", "downloadfile", "typetime", "appendtext", "setvalue", "typeedit", "keyhold", "keyrelease", "chooseradio", "setslider", "setdate", "setcolor", "followlink", "setfragment", "handleauth", "navintent", "openclipboard", "checksafe", "reopentab", "spanav", "duplicatetab", "pintab", "mutetab", "movetab", "movetabwindow", "searchtabs", "badgetab", "attachmeta", "focuswindow", "maximizewindow", "minimizewindow", "restorewindow", "incognitowindow", "asksubmit", "selectchain", "picktypeahead", "pickdate", "attachfile", "fillcode", "consentpassword", "pausedownload", "resumedownload", "verifydownload", "writeclipboard", "quarantinedownload", "scanvirus"]);
|
|
747
786
|
var tabscommandactions = /* @__PURE__ */ new Set(["querytabs", "duplicatetab", "closepattern", "pintab", "mutetab", "movetab", "movetabwindow", "grouptabs", "colorgroup", "collapsegroup", "discardtab", "reloadtabs", "zoomin", "zoomout", "watchtab", "switchtab", "maximizewindow", "minimizewindow", "restorewindow", "focuswindow", "scratchwindow", "incognitowindow", "restoretab", "savelayout", "restorelayout", "findclones", "searchtabs", "badgetab", "attachmeta", "listaudio", "reopenrun", "snapshotsession"]);
|
|
748
787
|
var formactions = /* @__PURE__ */ new Set(["fillform", "filllabel", "fillplaceholder", "detectfields", "generatevalues", "saveprofiles", "asksubmit", "submitform", "readerrors", "retryform", "runwizard", "selectchain", "picktypeahead", "pickdate", "attachfile", "handoffcaptcha", "fillcard", "fillcode", "consentpassword", "skiphoneypot", "detectlogin", "detecttemplate"]);
|
|
749
788
|
var datasetactions = /* @__PURE__ */ new Set(["scrapetable", "exportcsv", "exportjson", "exportexcel", "copytable", "pushsheets", "importcsv", "looprows", "transformvalues", "deduperows", "paginateextract", "mergepages", "stamplerows", "previewgrid", "streamdisk", "resumeextract", "logprovenance"]);
|
|
750
789
|
var exportactions = /* @__PURE__ */ new Set(["exportcsv", "exportjson", "exportexcel", "copytable", "pushsheets", "streamdisk"]);
|
|
751
790
|
var filesactions = /* @__PURE__ */ new Set(["batchdownload", "pausedownload", "resumedownload", "verifydownload", "interceptmime", "exportnetlog", "readclipboard", "writeclipboard", "copyscreen", "quarantinedownload", "scanvirus", "namecaptures", "cleanupartifacts"]);
|
|
791
|
+
var captureactions = /* @__PURE__ */ new Set(["shotview", "shotfullpage", "shotelement", "shotregion", "contactsheet"]);
|
|
752
792
|
var fieldkinds = ["text", "email", "phone", "date", "number", "select", "check", "radio", "file", "password", "card", "code"];
|
|
753
793
|
var layoutmutationactions = /* @__PURE__ */ new Set(["grouptabs", "colorgroup", "collapsegroup", "savelayout", "restorelayout"]);
|
|
754
794
|
var groupcolors = ["grey", "blue", "red", "yellow", "green", "pink", "purple", "cyan", "orange"];
|
|
@@ -809,6 +849,92 @@ function isexportkind(kind) {
|
|
|
809
849
|
function isfileskind(kind) {
|
|
810
850
|
return filesactions.has(kind);
|
|
811
851
|
}
|
|
852
|
+
function iscapturekind(kind) {
|
|
853
|
+
return captureactions.has(kind);
|
|
854
|
+
}
|
|
855
|
+
function capturegate(session, tabid2, origin, now) {
|
|
856
|
+
if (!session || session.stoppedat) return { allowed: false, reason: "No active browser session exists for the capture." };
|
|
857
|
+
if (session.expiresat <= now) return { allowed: false, reason: "The browser session has expired and cannot capture." };
|
|
858
|
+
if (session.pausedat) return { allowed: false, reason: "The browser session is paused and cannot capture." };
|
|
859
|
+
if (session.tabid !== tabid2) return { allowed: false, reason: `The capture needs the active tab grant of session tab ${session.tabid} and refuses tab ${tabid2}.` };
|
|
860
|
+
if (!origingranted(session, origin)) return { allowed: false, reason: `The capture of ${origin} needs the session origin grants first.` };
|
|
861
|
+
return { allowed: true };
|
|
862
|
+
}
|
|
863
|
+
function validatecaptureoptions(value) {
|
|
864
|
+
if (value === void 0) return { allowed: true };
|
|
865
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return { allowed: false, reason: "The reviewed capture options must be an object in options.capture." };
|
|
866
|
+
const options = value;
|
|
867
|
+
if (options.format !== void 0 && options.format !== "png" && options.format !== "jpeg" && options.format !== "webp") return { allowed: false, reason: "The reviewed capture format must be png, jpeg or webp." };
|
|
868
|
+
if (options.quality !== void 0 && (typeof options.quality !== "number" || !Number.isFinite(options.quality) || options.quality < 0 || options.quality > 100)) return { allowed: false, reason: "The reviewed capture quality must stay between zero and one hundred; any value in that range is the user choice with no code cap." };
|
|
869
|
+
if (options.pixelratio !== void 0 && (typeof options.pixelratio !== "number" || !Number.isFinite(options.pixelratio) || options.pixelratio < 1)) return { allowed: false, reason: "The reviewed pixel ratio starts at one and climbs to any user configured ceiling with no code ceiling." };
|
|
870
|
+
if (options.annotate !== void 0 && typeof options.annotate !== "boolean") return { allowed: false, reason: "The reviewed capture annotation flag must be a boolean." };
|
|
871
|
+
if (options.exporttarget !== void 0 && options.exporttarget !== "memory" && options.exporttarget !== "download" && options.exporttarget !== "clipboard") return { allowed: false, reason: "The reviewed capture export target must be memory, download or clipboard." };
|
|
872
|
+
return { allowed: true };
|
|
873
|
+
}
|
|
874
|
+
function validateregionrect(value) {
|
|
875
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return { allowed: false, reason: "A reviewed regionrect with x, y, width and height in css pixels is required in options." };
|
|
876
|
+
const rect = value;
|
|
877
|
+
for (const field of ["x", "y", "width", "height"]) {
|
|
878
|
+
if (typeof rect[field] !== "number" || !Number.isFinite(rect[field])) return { allowed: false, reason: `The reviewed regionrect needs a numeric ${field} in css pixels.` };
|
|
879
|
+
}
|
|
880
|
+
if (rect.x < 0 || rect.y < 0) return { allowed: false, reason: "The reviewed regionrect refuses negative coordinates." };
|
|
881
|
+
if (rect.width <= 0 || rect.height <= 0) return { allowed: false, reason: "The reviewed regionrect needs positive width and height values." };
|
|
882
|
+
return { allowed: true };
|
|
883
|
+
}
|
|
884
|
+
function validatecapturenaming(value) {
|
|
885
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return { allowed: false, reason: "A reviewed capturenaming rule with run, step, sequence and kind flags is required." };
|
|
886
|
+
const rule = value;
|
|
887
|
+
const segments = ["run", "step", "sequence", "kind"];
|
|
888
|
+
for (const key of Object.keys(rule)) {
|
|
889
|
+
if (!segments.includes(key)) return { allowed: false, reason: `The reviewed capturenaming rule refuses the unknown ${key} segment; only run, step, sequence and kind participate.` };
|
|
890
|
+
}
|
|
891
|
+
for (const segment of segments) {
|
|
892
|
+
if (rule[segment] !== void 0 && typeof rule[segment] !== "boolean") return { allowed: false, reason: `The reviewed capturenaming ${segment} flag must be a boolean.` };
|
|
893
|
+
}
|
|
894
|
+
if (!segments.some((segment) => rule[segment] === true)) return { allowed: false, reason: "The reviewed capturenaming rule needs at least one enabled segment of run, step, sequence and kind." };
|
|
895
|
+
return { allowed: true };
|
|
896
|
+
}
|
|
897
|
+
function stitchbudgetallowed(tiles, settle2, wait) {
|
|
898
|
+
if (tiles <= 0) return { allowed: false, reason: "The stitch budget needs at least one tile." };
|
|
899
|
+
if (settle2 < 0 || wait < 0) return { allowed: false, reason: "The reviewed settle and wait windows must be zero or positive milliseconds." };
|
|
900
|
+
if (tiles * settle2 > wait) return { allowed: false, reason: `The stitching scroll budget of ${tiles} tiles at ${settle2} milliseconds exceeds the reviewed wait window of ${wait} milliseconds; review a wider window or a smaller settle.` };
|
|
901
|
+
return { allowed: true };
|
|
902
|
+
}
|
|
903
|
+
function beforeafterwrapallowed(kind) {
|
|
904
|
+
return allowedactions.has(kind) && !captureactions.has(kind);
|
|
905
|
+
}
|
|
906
|
+
function validatecapturegrammar(step, options) {
|
|
907
|
+
const kind = step.kind;
|
|
908
|
+
const optioncheck = validatecaptureoptions(options.capture);
|
|
909
|
+
if (!optioncheck.allowed) return optioncheck;
|
|
910
|
+
if (options.settle !== void 0 && (typeof options.settle !== "number" || !Number.isFinite(options.settle) || options.settle < 0)) return { allowed: false, reason: "The reviewed capture settle window must be zero or a positive number of milliseconds." };
|
|
911
|
+
if (options.overlap !== void 0 && (typeof options.overlap !== "number" || !Number.isInteger(options.overlap) || options.overlap < 0)) return { allowed: false, reason: "The reviewed stitch overlap must be zero or a positive number of rows." };
|
|
912
|
+
if (options.wait !== void 0 && (typeof options.wait !== "number" || !Number.isFinite(options.wait) || options.wait < 0)) return { allowed: false, reason: "The reviewed capture wait window must be zero or a positive number of milliseconds." };
|
|
913
|
+
if (options.naming !== void 0) {
|
|
914
|
+
const namingcheck = validatecapturenaming(options.naming);
|
|
915
|
+
if (!namingcheck.allowed) return namingcheck;
|
|
916
|
+
}
|
|
917
|
+
if (kind === "shotregion") {
|
|
918
|
+
const rectcheck = validateregionrect(options.regionrect);
|
|
919
|
+
if (!rectcheck.allowed) return rectcheck;
|
|
920
|
+
if (options.reviewed !== true) return { allowed: false, reason: "Every reviewed regionrect needs the explicit reviewed flag before shotregion runs." };
|
|
921
|
+
if (options.container !== void 0 && !isnonempty(options.container)) return { allowed: false, reason: "The reviewed scrollable container selector must be a non-empty string." };
|
|
922
|
+
if (options.steps !== void 0 && (typeof options.steps !== "number" || !Number.isInteger(options.steps) || options.steps < 1)) return { allowed: false, reason: "The reviewed container scroll steps must be a positive integer with no code ceiling." };
|
|
923
|
+
}
|
|
924
|
+
if (kind === "contactsheet") {
|
|
925
|
+
const elements = options.elements;
|
|
926
|
+
if (!Array.isArray(elements) || elements.length === 0 || !elements.every((item) => isnonempty(item))) return { allowed: false, reason: "A reviewed non-empty list of element selectors is required in options for the contact sheet; the cell count stays the user choice." };
|
|
927
|
+
const layout = options.sheet;
|
|
928
|
+
if (layout !== void 0) {
|
|
929
|
+
if (!layout || typeof layout !== "object" || Array.isArray(layout)) return { allowed: false, reason: "The reviewed sheetlayout must be an object with cellsize, columns and label." };
|
|
930
|
+
const sheet = layout;
|
|
931
|
+
if (typeof sheet.cellsize !== "number" || !Number.isFinite(sheet.cellsize) || sheet.cellsize <= 0) return { allowed: false, reason: "The reviewed contact sheet cell size must be a positive number of pixels." };
|
|
932
|
+
if (typeof sheet.columns !== "number" || !Number.isInteger(sheet.columns) || sheet.columns < 1) return { allowed: false, reason: "The reviewed contact sheet column count must be a positive integer with no code ceiling." };
|
|
933
|
+
if (sheet.label !== void 0 && sheet.label !== "none" && sheet.label !== "index" && sheet.label !== "selector" && sheet.label !== "both") return { allowed: false, reason: "The reviewed contact sheet label style must be none, index, selector or both." };
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
return { allowed: true };
|
|
937
|
+
}
|
|
812
938
|
function exportgranted(session, origin) {
|
|
813
939
|
if (!origingranted(session, origin)) return { allowed: false, reason: `The export of extracted data from ${origin} needs the session origin grants before it leaves local memory.` };
|
|
814
940
|
return { allowed: true };
|
|
@@ -1605,6 +1731,10 @@ function validatestep(step, origin) {
|
|
|
1605
1731
|
const filescheck = validatefilesgrammar(step, options);
|
|
1606
1732
|
if (!filescheck.allowed) return filescheck;
|
|
1607
1733
|
}
|
|
1734
|
+
if (iscapturekind(step.kind)) {
|
|
1735
|
+
const capturecheck = validatecapturegrammar(step, options);
|
|
1736
|
+
if (!capturecheck.allowed) return capturecheck;
|
|
1737
|
+
}
|
|
1608
1738
|
if (step.kind === "tabcreate") {
|
|
1609
1739
|
if (options.background !== void 0 && typeof options.background !== "boolean") return { allowed: false, reason: "The reviewed background flag must be a boolean." };
|
|
1610
1740
|
if (options.window !== void 0 && (typeof options.window !== "number" || !Number.isInteger(options.window) || options.window < 0)) return { allowed: false, reason: "The reviewed target window id must be a non-negative integer." };
|
|
@@ -1664,6 +1794,18 @@ function canexecute(input) {
|
|
|
1664
1794
|
if (!clipgate.allowed) return clipgate;
|
|
1665
1795
|
}
|
|
1666
1796
|
if (input.step.kind === "interceptmime" && !origingranted(input.session, input.origin)) return { allowed: false, reason: "The download interception is outside the session origin grants." };
|
|
1797
|
+
if (iscapturekind(input.step.kind)) {
|
|
1798
|
+
const capturegatecheck = capturegate(input.session, input.tabid, input.origin, now);
|
|
1799
|
+
if (!capturegatecheck.allowed) return capturegatecheck;
|
|
1800
|
+
let captureoptions = {};
|
|
1801
|
+
try {
|
|
1802
|
+
captureoptions = parseoptions(input.step);
|
|
1803
|
+
} catch {
|
|
1804
|
+
captureoptions = {};
|
|
1805
|
+
}
|
|
1806
|
+
const target = captureoptions.capture?.exporttarget;
|
|
1807
|
+
if (target !== void 0 && target !== "memory" && target !== "download" && target !== "clipboard") return { allowed: false, reason: "The capture export target must be memory, download or clipboard." };
|
|
1808
|
+
}
|
|
1667
1809
|
if (input.step.kind === "openlink" || input.step.kind === "openprivate" || input.step.kind === "batchopen" || input.step.kind === "prefetch" || input.step.kind === "deeplink" || input.step.kind === "reopentab") {
|
|
1668
1810
|
let options = {};
|
|
1669
1811
|
try {
|
|
@@ -1774,9 +1916,20 @@ function recorddownload(progress, planid, stepid, entry, now) {
|
|
|
1774
1916
|
const outcome = { stepid, ok: entry.state === "complete", summary: `Download ${entry.index + 1} of ${entry.url} ended in the ${entry.state} state.`, details: { download: entry }, at: now };
|
|
1775
1917
|
return recordoutcome(base, planid, outcome, now);
|
|
1776
1918
|
}
|
|
1919
|
+
function recordcapture(progress, planid, stepid, capture, now) {
|
|
1920
|
+
const base = progress && progress.planid === planid ? progress : emptyprogress(planid, now);
|
|
1921
|
+
const bytes = capture.bytes?.length ?? 0;
|
|
1922
|
+
const outcome = { stepid, ok: true, summary: `Captured a ${capture.format} ${capture.kind} shot of ${capture.width} by ${capture.height} pixels with ${bytes} character${bytes === 1 ? "" : "s"} of image data.`, details: { capture: { id: capture.id, kind: capture.kind, format: capture.format, width: capture.width, height: capture.height, bytes } }, at: now };
|
|
1923
|
+
return recordoutcome(base, planid, outcome, now);
|
|
1924
|
+
}
|
|
1925
|
+
function recordpair(progress, planid, stepid, pair, now) {
|
|
1926
|
+
const base = progress && progress.planid === planid ? progress : emptyprogress(planid, now);
|
|
1927
|
+
const outcome = { stepid, ok: true, summary: `Paired the before shot ${pair.beforeid} with the after shot ${pair.afterid} around the ${pair.actionkind} action.`, details: { shotpair: { id: pair.id, beforeid: pair.beforeid, afterid: pair.afterid, actionkind: pair.actionkind, ...pair.target !== void 0 ? { target: pair.target } : {}, ...pair.domsnapshotid !== void 0 ? { domsnapshotid: pair.domsnapshotid } : {} } }, at: now };
|
|
1928
|
+
return recordoutcome(base, planid, outcome, now);
|
|
1929
|
+
}
|
|
1777
1930
|
|
|
1778
1931
|
// version.ts
|
|
1779
|
-
var packageversion = "1.1.
|
|
1932
|
+
var packageversion = "1.1.40";
|
|
1780
1933
|
|
|
1781
1934
|
// types.ts
|
|
1782
1935
|
var protocolversion = packageversion;
|
|
@@ -1840,7 +1993,7 @@ function requestbody(input) {
|
|
|
1840
1993
|
return JSON.stringify({ version: protocolversion, objective: input.objective, session: input.session, observation: input.observation, capabilities: input.capabilities });
|
|
1841
1994
|
}
|
|
1842
1995
|
function outcomeresponse(input) {
|
|
1843
|
-
return JSON.stringify({ version: protocolversion, planid: input.plan.id, planstate: input.plan.state, outcome: input.outcome, ...input.resolvedtarget ? { resolvedtarget: input.resolvedtarget } : {} });
|
|
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 } : {} });
|
|
1844
1997
|
}
|
|
1845
1998
|
function mapresponse(input) {
|
|
1846
1999
|
return JSON.stringify({ version: protocolversion, planid: input.plan.id, planstate: input.plan.state, map: input.map });
|
|
@@ -1902,6 +2055,195 @@ function netlogreport(input) {
|
|
|
1902
2055
|
function quarantinereport(input) {
|
|
1903
2056
|
return { version: protocolversion, entries: input.entries };
|
|
1904
2057
|
}
|
|
2058
|
+
function capturereport(input) {
|
|
2059
|
+
return { version: protocolversion, records: input.records, pairs: input.pairs };
|
|
2060
|
+
}
|
|
2061
|
+
|
|
2062
|
+
// capture.ts
|
|
2063
|
+
var capturekinds = ["shotview", "shotfullpage", "shotelement", "shotregion", "contactsheet"];
|
|
2064
|
+
function captureoptionsof(value) {
|
|
2065
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return {};
|
|
2066
|
+
const options = value;
|
|
2067
|
+
const normalized = {};
|
|
2068
|
+
if (options.format === "png" || options.format === "jpeg" || options.format === "webp") normalized.format = options.format;
|
|
2069
|
+
if (typeof options.quality === "number" && Number.isFinite(options.quality)) normalized.quality = options.quality;
|
|
2070
|
+
if (typeof options.pixelratio === "number" && Number.isFinite(options.pixelratio)) normalized.pixelratio = options.pixelratio;
|
|
2071
|
+
if (typeof options.annotate === "boolean") normalized.annotate = options.annotate;
|
|
2072
|
+
if (options.exporttarget === "memory" || options.exporttarget === "download" || options.exporttarget === "clipboard") normalized.exporttarget = options.exporttarget;
|
|
2073
|
+
return normalized;
|
|
2074
|
+
}
|
|
2075
|
+
function capturevisible(input) {
|
|
2076
|
+
const ratio = input.options.pixelratio ?? 1;
|
|
2077
|
+
return {
|
|
2078
|
+
id: input.id,
|
|
2079
|
+
runid: input.runid,
|
|
2080
|
+
stepid: input.stepid,
|
|
2081
|
+
kind: "shotview",
|
|
2082
|
+
format: input.options.format ?? "png",
|
|
2083
|
+
width: Math.round(input.viewport.width * ratio),
|
|
2084
|
+
height: Math.round(input.viewport.height * ratio),
|
|
2085
|
+
capturedat: input.at,
|
|
2086
|
+
bytes: input.dataurl,
|
|
2087
|
+
...input.name !== void 0 ? { name: input.name } : {},
|
|
2088
|
+
...input.options.annotate === true ? { annotated: true } : {},
|
|
2089
|
+
...input.options.exporttarget !== void 0 ? { exporttarget: input.options.exporttarget } : {},
|
|
2090
|
+
...input.target !== void 0 ? { target: input.target } : {}
|
|
2091
|
+
};
|
|
2092
|
+
}
|
|
2093
|
+
function capturestitched(input) {
|
|
2094
|
+
const ratio = input.options.pixelratio ?? 1;
|
|
2095
|
+
return {
|
|
2096
|
+
id: input.id,
|
|
2097
|
+
runid: input.runid,
|
|
2098
|
+
stepid: input.stepid,
|
|
2099
|
+
kind: "shotfullpage",
|
|
2100
|
+
format: input.options.format ?? "png",
|
|
2101
|
+
width: Math.round(input.plan.scrollwidth * ratio),
|
|
2102
|
+
height: Math.round(input.plan.scrollheight * ratio),
|
|
2103
|
+
capturedat: input.at,
|
|
2104
|
+
bytes: input.dataurl,
|
|
2105
|
+
...input.name !== void 0 ? { name: input.name } : {},
|
|
2106
|
+
...input.options.annotate === true ? { annotated: true } : {},
|
|
2107
|
+
...input.options.exporttarget !== void 0 ? { exporttarget: input.options.exporttarget } : {}
|
|
2108
|
+
};
|
|
2109
|
+
}
|
|
2110
|
+
function captureelement(input) {
|
|
2111
|
+
const ratio = input.options.pixelratio ?? 1;
|
|
2112
|
+
const scaled = scaledrect(input.rect, ratio);
|
|
2113
|
+
return {
|
|
2114
|
+
id: input.id,
|
|
2115
|
+
runid: input.runid,
|
|
2116
|
+
stepid: input.stepid,
|
|
2117
|
+
kind: "shotelement",
|
|
2118
|
+
format: input.options.format ?? "png",
|
|
2119
|
+
width: scaled.width,
|
|
2120
|
+
height: scaled.height,
|
|
2121
|
+
capturedat: input.at,
|
|
2122
|
+
bytes: input.dataurl,
|
|
2123
|
+
...input.name !== void 0 ? { name: input.name } : {},
|
|
2124
|
+
...input.options.annotate === true ? { annotated: true } : {},
|
|
2125
|
+
...input.options.exporttarget !== void 0 ? { exporttarget: input.options.exporttarget } : {},
|
|
2126
|
+
...input.target !== void 0 ? { target: input.target } : {}
|
|
2127
|
+
};
|
|
2128
|
+
}
|
|
2129
|
+
function captureregion(input) {
|
|
2130
|
+
const ratio = input.options.pixelratio ?? 1;
|
|
2131
|
+
const scaled = scaledrect(input.rect, ratio);
|
|
2132
|
+
return {
|
|
2133
|
+
id: input.id,
|
|
2134
|
+
runid: input.runid,
|
|
2135
|
+
stepid: input.stepid,
|
|
2136
|
+
kind: "shotregion",
|
|
2137
|
+
format: input.options.format ?? "png",
|
|
2138
|
+
width: scaled.width,
|
|
2139
|
+
height: scaled.height,
|
|
2140
|
+
capturedat: input.at,
|
|
2141
|
+
bytes: input.dataurl,
|
|
2142
|
+
...input.name !== void 0 ? { name: input.name } : {},
|
|
2143
|
+
...input.options.annotate === true ? { annotated: true } : {},
|
|
2144
|
+
...input.options.exporttarget !== void 0 ? { exporttarget: input.options.exporttarget } : {},
|
|
2145
|
+
...input.target !== void 0 ? { target: input.target } : {}
|
|
2146
|
+
};
|
|
2147
|
+
}
|
|
2148
|
+
function pairstates(before, after, action, at, id) {
|
|
2149
|
+
if (!before) return { skipped: "before", reason: "The before shot was not captured, so no state pair exists." };
|
|
2150
|
+
if (!after) return { skipped: "after", reason: `The action of kind ${action.kind} failed before the after shot, so the state pair is skipped.` };
|
|
2151
|
+
return {
|
|
2152
|
+
pair: {
|
|
2153
|
+
id,
|
|
2154
|
+
beforeid: before.id,
|
|
2155
|
+
afterid: after.id,
|
|
2156
|
+
actionkind: action.kind,
|
|
2157
|
+
...action.target !== void 0 ? { target: action.target } : {},
|
|
2158
|
+
...action.domsnapshotid !== void 0 ? { domsnapshotid: action.domsnapshotid } : {},
|
|
2159
|
+
at
|
|
2160
|
+
},
|
|
2161
|
+
reason: `Paired the before shot ${before.id} with the after shot ${after.id} around the ${action.kind} action.`
|
|
2162
|
+
};
|
|
2163
|
+
}
|
|
2164
|
+
function capturestates(input) {
|
|
2165
|
+
if (input.policy !== "beforeafter") return { reason: `The ${input.policy} capture policy takes no state pair around the ${input.actionkind} action.` };
|
|
2166
|
+
return pairstates(input.before, input.after, { kind: input.actionkind, ...input.target !== void 0 ? { target: input.target } : {}, ...input.domsnapshotid !== void 0 ? { domsnapshotid: input.domsnapshotid } : {} }, input.at, input.id);
|
|
2167
|
+
}
|
|
2168
|
+
function buildstitchplan(input) {
|
|
2169
|
+
const overlap = Math.max(0, Math.round(input.overlap ?? 0));
|
|
2170
|
+
const stepy = Math.max(1, input.viewportheight - overlap);
|
|
2171
|
+
const columns = Math.max(1, Math.ceil(input.scrollwidth / input.viewportwidth));
|
|
2172
|
+
const rows = input.scrollheight <= input.viewportheight ? 1 : Math.max(1, Math.ceil((input.scrollheight - overlap) / stepy));
|
|
2173
|
+
const tiles = [];
|
|
2174
|
+
for (let column = 0; column < columns; column += 1) {
|
|
2175
|
+
for (let row = 0; row < rows; row += 1) {
|
|
2176
|
+
const x = Math.min(column * input.viewportwidth, Math.max(0, input.scrollwidth - input.viewportwidth));
|
|
2177
|
+
const y = rows === 1 ? 0 : Math.min(row * stepy, Math.max(0, input.scrollheight - input.viewportheight));
|
|
2178
|
+
tiles.push({ x: Math.round(x), y: Math.round(y) });
|
|
2179
|
+
}
|
|
2180
|
+
}
|
|
2181
|
+
return { columns, rows, tiles, overlap, scrollwidth: Math.round(input.scrollwidth), scrollheight: Math.round(input.scrollheight), viewportwidth: Math.round(input.viewportwidth), viewportheight: Math.round(input.viewportheight) };
|
|
2182
|
+
}
|
|
2183
|
+
function seamweights(overlap) {
|
|
2184
|
+
if (overlap <= 0) return [];
|
|
2185
|
+
const weights = [];
|
|
2186
|
+
for (let index = 0; index < overlap; index += 1) weights.push((index + 1) / (overlap + 1));
|
|
2187
|
+
return weights;
|
|
2188
|
+
}
|
|
2189
|
+
function fixedheadermatch(band, firstband) {
|
|
2190
|
+
if (band.length === 0 || band.length !== firstband.length) return false;
|
|
2191
|
+
return band.every((value, index) => value === firstband[index]);
|
|
2192
|
+
}
|
|
2193
|
+
function scaledrect(rect, pixelratio) {
|
|
2194
|
+
const ratio = pixelratio >= 1 ? pixelratio : 1;
|
|
2195
|
+
return { x: Math.round(rect.x * ratio), y: Math.round(rect.y * ratio), width: Math.round(rect.width * ratio), height: Math.round(rect.height * ratio) };
|
|
2196
|
+
}
|
|
2197
|
+
function croprect(rect, viewport) {
|
|
2198
|
+
const x = Math.max(0, rect.x);
|
|
2199
|
+
const y = Math.max(0, rect.y);
|
|
2200
|
+
return { x: Math.round(x), y: Math.round(y), width: Math.round(Math.max(0, Math.min(rect.width, viewport.width - x))), height: Math.round(Math.max(0, Math.min(rect.height, viewport.height - y))) };
|
|
2201
|
+
}
|
|
2202
|
+
function regionsteps(containerheight, viewportstep) {
|
|
2203
|
+
if (containerheight <= 0 || viewportstep <= 0) return [0];
|
|
2204
|
+
const steps = [];
|
|
2205
|
+
for (let top = 0; top < containerheight; top += viewportstep) {
|
|
2206
|
+
const clamped = Math.min(top, Math.max(0, containerheight - viewportstep));
|
|
2207
|
+
if (!steps.includes(clamped)) steps.push(clamped);
|
|
2208
|
+
}
|
|
2209
|
+
return steps;
|
|
2210
|
+
}
|
|
2211
|
+
function buildsheet(cells, layout) {
|
|
2212
|
+
const columns = Math.max(1, Math.round(layout.columns));
|
|
2213
|
+
const rows = Math.max(1, Math.ceil(cells.length / columns));
|
|
2214
|
+
const placed = cells.map((cell, index) => {
|
|
2215
|
+
const column = index % columns;
|
|
2216
|
+
const row = Math.floor(index / columns);
|
|
2217
|
+
const label = cell.label ?? "";
|
|
2218
|
+
const caption = layout.label === "none" ? "" : layout.label === "index" ? `${index + 1}` : layout.label === "selector" ? cell.selector : label ? `${index + 1} \xB7 ${cell.selector} \xB7 ${label}` : `${index + 1} \xB7 ${cell.selector}`;
|
|
2219
|
+
return { index, column, row, selector: cell.selector, label, caption };
|
|
2220
|
+
});
|
|
2221
|
+
return { columns, rows, cells: placed };
|
|
2222
|
+
}
|
|
2223
|
+
function capturepart(value) {
|
|
2224
|
+
return value.replace(/[^a-z0-9-]+/gi, "-").replace(/^-+|-+$/g, "").toLowerCase() || "capture";
|
|
2225
|
+
}
|
|
2226
|
+
function buildname(rule, parts, extension) {
|
|
2227
|
+
const segments = [];
|
|
2228
|
+
if (rule.run) segments.push(capturepart(parts.run));
|
|
2229
|
+
if (rule.step) segments.push(capturepart(parts.step));
|
|
2230
|
+
if (rule.sequence) segments.push(String(Math.max(0, Math.round(parts.sequence))));
|
|
2231
|
+
if (rule.kind) segments.push(capturepart(parts.kind));
|
|
2232
|
+
const safeextension = extension.replace(/^\.+/, "").toLowerCase() || "png";
|
|
2233
|
+
return `${(segments.length > 0 ? segments : ["capture"]).join("-")}.${safeextension}`;
|
|
2234
|
+
}
|
|
2235
|
+
function annotationplanof(input) {
|
|
2236
|
+
const inset = Math.min(24, Math.max(8, Math.round(Math.min(input.width, input.height) / 12)));
|
|
2237
|
+
const plan = {
|
|
2238
|
+
marker: { x: inset, y: inset, number: Math.max(1, Math.round(input.step)) },
|
|
2239
|
+
footer: `${new Date(input.at).toISOString()} \xB7 ${input.url}`
|
|
2240
|
+
};
|
|
2241
|
+
if (input.rect !== void 0) {
|
|
2242
|
+
const expansion = 2;
|
|
2243
|
+
plan.outline = { x: Math.round(input.rect.x - expansion), y: Math.round(input.rect.y - expansion), width: Math.round(input.rect.width + expansion * 2), height: Math.round(input.rect.height + expansion * 2) };
|
|
2244
|
+
}
|
|
2245
|
+
return plan;
|
|
2246
|
+
}
|
|
1905
2247
|
|
|
1906
2248
|
// extension/browsertabs.ts
|
|
1907
2249
|
var browserkinds = /* @__PURE__ */ new Set(["tablist", "tabcreate", "tabactivate", "tabclose", "tabreload", "tabsnapshot", "windowlist", "windowcreate", "windowclose", "zoomset", "windowresize", "downloadfile"]);
|
|
@@ -3018,12 +3360,12 @@ function scanverdictof(response) {
|
|
|
3018
3360
|
function released(entry, ref, at) {
|
|
3019
3361
|
return { ...entry, release: ref, updatedat: at };
|
|
3020
3362
|
}
|
|
3021
|
-
function
|
|
3363
|
+
function capturepart2(value) {
|
|
3022
3364
|
return value.replace(/[^a-z0-9-]+/gi, "-").replace(/^-+|-+$/g, "").toLowerCase() || "capture";
|
|
3023
3365
|
}
|
|
3024
3366
|
function capturefilename(name, extension) {
|
|
3025
3367
|
const safeextension = extension.replace(/^\.+/, "").toLowerCase() || "png";
|
|
3026
|
-
return `${
|
|
3368
|
+
return `${capturepart2(name.task)}-${capturepart2(name.step)}-${name.sequence}.${safeextension}`;
|
|
3027
3369
|
}
|
|
3028
3370
|
function advancecounter(counters, base) {
|
|
3029
3371
|
const sequence = (counters[base] ?? 0) + 1;
|
|
@@ -3102,8 +3444,9 @@ function stepoptions2(step) {
|
|
|
3102
3444
|
}
|
|
3103
3445
|
async function refreshcapabilities() {
|
|
3104
3446
|
const report = await readcapabilities();
|
|
3105
|
-
|
|
3106
|
-
|
|
3447
|
+
const withcaptures = { ...report, captures: [...capturekinds] };
|
|
3448
|
+
await memory.setcapabilities(withcaptures);
|
|
3449
|
+
return withcaptures;
|
|
3107
3450
|
}
|
|
3108
3451
|
async function activecontext() {
|
|
3109
3452
|
const [tab] = await chrome.tabs.query({ active: true, lastFocusedWindow: true });
|
|
@@ -3256,6 +3599,7 @@ function stepauditkind(step, ok) {
|
|
|
3256
3599
|
if (step.kind === "submitform" || step.kind === "asksubmit") return "submit";
|
|
3257
3600
|
if (step.kind === "consentpassword") return "consent";
|
|
3258
3601
|
if (step.kind === "handoffcaptcha") return "handoff";
|
|
3602
|
+
if (iscapturekind(step.kind)) return "capture";
|
|
3259
3603
|
if (isfileskind(step.kind)) {
|
|
3260
3604
|
if (step.kind === "interceptmime") return "intercept";
|
|
3261
3605
|
if (step.kind === "readclipboard" || step.kind === "writeclipboard" || step.kind === "copyscreen") return "clipboard";
|
|
@@ -4941,6 +5285,374 @@ async function reconcilmimefilter() {
|
|
|
4941
5285
|
}
|
|
4942
5286
|
reconcilmimefilter().catch(() => {
|
|
4943
5287
|
});
|
|
5288
|
+
var stitchprogress = /* @__PURE__ */ new Map();
|
|
5289
|
+
function stepcaptureoptions(step) {
|
|
5290
|
+
return captureoptionsof(stepoptions2(step).capture);
|
|
5291
|
+
}
|
|
5292
|
+
async function blobtodataurl(blob) {
|
|
5293
|
+
const buffer = new Uint8Array(await blob.arrayBuffer());
|
|
5294
|
+
let binary = "";
|
|
5295
|
+
const chunk = 32768;
|
|
5296
|
+
for (let index = 0; index < buffer.length; index += chunk) binary += String.fromCharCode(...buffer.subarray(index, index + chunk));
|
|
5297
|
+
return `data:${blob.type};base64,${btoa(binary)}`;
|
|
5298
|
+
}
|
|
5299
|
+
async function tabshot(format, quality) {
|
|
5300
|
+
const shot = { format: format === "jpeg" ? "jpeg" : "png" };
|
|
5301
|
+
if (format === "jpeg" && quality !== void 0) shot.quality = Math.max(0, Math.min(100, Math.round(quality)));
|
|
5302
|
+
return chrome.tabs.captureVisibleTab(chrome.windows.WINDOW_ID_CURRENT, shot);
|
|
5303
|
+
}
|
|
5304
|
+
async function bridgecall(tabid2, name, ...args) {
|
|
5305
|
+
await chrome.scripting.executeScript({ target: { tabId: tabid2 }, files: ["pagebridge.js"] });
|
|
5306
|
+
const result = await chrome.scripting.executeScript({ target: { tabId: tabid2 }, func: (bridgeName, bridgeArgs) => {
|
|
5307
|
+
const bridge = globalThis.devthinkbridge;
|
|
5308
|
+
if (!bridge) throw new Error("Devthink page bridge is unavailable.");
|
|
5309
|
+
const seam = bridge[bridgeName];
|
|
5310
|
+
if (typeof seam !== "function") throw new Error(`The page bridge has no ${bridgeName} capture seam.`);
|
|
5311
|
+
return seam(...bridgeArgs);
|
|
5312
|
+
}, args: [name, args] });
|
|
5313
|
+
return result[0]?.result;
|
|
5314
|
+
}
|
|
5315
|
+
function tileband(bitmap, rows) {
|
|
5316
|
+
if (rows <= 0 || bitmap.height <= 0) return [];
|
|
5317
|
+
const canvas = new OffscreenCanvas(bitmap.width, Math.min(rows, bitmap.height));
|
|
5318
|
+
const context = canvas.getContext("2d");
|
|
5319
|
+
if (!context) return [];
|
|
5320
|
+
context.drawImage(bitmap, 0, 0);
|
|
5321
|
+
const data = context.getImageData(0, 0, canvas.width, canvas.height).data;
|
|
5322
|
+
const band = [];
|
|
5323
|
+
const pixels = canvas.width * canvas.height;
|
|
5324
|
+
const stride = Math.max(1, Math.floor(pixels / 64));
|
|
5325
|
+
for (let pixel = 0; pixel < pixels; pixel += stride) {
|
|
5326
|
+
const index = pixel * 4;
|
|
5327
|
+
band.push((data[index] ?? 0) + (data[index + 1] ?? 0) * 256 + (data[index + 2] ?? 0) * 65536 + (data[index + 3] ?? 0) * 16777216);
|
|
5328
|
+
}
|
|
5329
|
+
return band;
|
|
5330
|
+
}
|
|
5331
|
+
function drawannotations(context, plan, width, height, ratio) {
|
|
5332
|
+
context.save();
|
|
5333
|
+
context.scale(ratio, ratio);
|
|
5334
|
+
context.fillStyle = "rgba(47,128,237,.92)";
|
|
5335
|
+
context.beginPath();
|
|
5336
|
+
context.arc(plan.marker.x, plan.marker.y, 14, 0, Math.PI * 2);
|
|
5337
|
+
context.fill();
|
|
5338
|
+
context.fillStyle = "#ffffff";
|
|
5339
|
+
context.font = "bold 14px sans-serif";
|
|
5340
|
+
context.textBaseline = "middle";
|
|
5341
|
+
context.textAlign = "center";
|
|
5342
|
+
context.fillText(String(plan.marker.number), plan.marker.x, plan.marker.y);
|
|
5343
|
+
if (plan.outline !== void 0) {
|
|
5344
|
+
context.strokeStyle = "#2f80ed";
|
|
5345
|
+
context.lineWidth = 2;
|
|
5346
|
+
context.strokeRect(plan.outline.x, plan.outline.y, plan.outline.width, plan.outline.height);
|
|
5347
|
+
}
|
|
5348
|
+
const footery = Math.max(12, height - 22);
|
|
5349
|
+
context.fillStyle = "rgba(23,32,44,.86)";
|
|
5350
|
+
context.fillRect(0, footery, width, 22);
|
|
5351
|
+
context.fillStyle = "#ffffff";
|
|
5352
|
+
context.font = "12px sans-serif";
|
|
5353
|
+
context.textAlign = "left";
|
|
5354
|
+
context.fillText(plan.footer.slice(0, 160), 8, footery + 11);
|
|
5355
|
+
context.restore();
|
|
5356
|
+
}
|
|
5357
|
+
async function canvasdataurl(canvas, format, quality) {
|
|
5358
|
+
const type = format === "jpeg" ? "image/jpeg" : format === "webp" ? "image/webp" : "image/png";
|
|
5359
|
+
const encoded = format === "png" ? await canvas.convertToBlob({ type }) : await canvas.convertToBlob({ type, quality: Math.max(0, Math.min(1, (quality ?? 100) / 100)) });
|
|
5360
|
+
return blobtodataurl(encoded);
|
|
5361
|
+
}
|
|
5362
|
+
async function composestitched(tiles, width, height, overlap, ratio, options) {
|
|
5363
|
+
const canvas = new OffscreenCanvas(Math.max(1, Math.round(width * ratio)), Math.max(1, Math.round(height * ratio)));
|
|
5364
|
+
const context = canvas.getContext("2d");
|
|
5365
|
+
if (!context) throw new Error("The stitcher could not create a canvas context.");
|
|
5366
|
+
const weights = seamweights(overlap);
|
|
5367
|
+
const firstband = overlap > 0 && tiles[0] ? tileband(tiles[0].bitmap, overlap) : [];
|
|
5368
|
+
for (const tile of tiles) {
|
|
5369
|
+
const band = overlap > 0 ? tileband(tile.bitmap, overlap) : [];
|
|
5370
|
+
const repeated = overlap > 0 && !tile.firstofcolumn && fixedheadermatch(band, firstband);
|
|
5371
|
+
if (repeated) {
|
|
5372
|
+
const skipped = Math.min(overlap, tile.bitmap.height);
|
|
5373
|
+
context.drawImage(tile.bitmap, 0, skipped, tile.bitmap.width, tile.bitmap.height - skipped, tile.x, tile.y + skipped, tile.bitmap.width, tile.bitmap.height - skipped);
|
|
5374
|
+
continue;
|
|
5375
|
+
}
|
|
5376
|
+
if (weights.length > 0 && !tile.firstofcolumn) {
|
|
5377
|
+
for (let row = 0; row < weights.length && row < tile.bitmap.height; row += 1) {
|
|
5378
|
+
context.globalAlpha = weights[row] ?? 1;
|
|
5379
|
+
context.drawImage(tile.bitmap, 0, row, tile.bitmap.width, 1, tile.x, tile.y + row, tile.bitmap.width, 1);
|
|
5380
|
+
}
|
|
5381
|
+
context.globalAlpha = 1;
|
|
5382
|
+
const below = Math.min(overlap, tile.bitmap.height);
|
|
5383
|
+
context.drawImage(tile.bitmap, 0, below, tile.bitmap.width, tile.bitmap.height - below, tile.x, tile.y + below, tile.bitmap.width, tile.bitmap.height - below);
|
|
5384
|
+
continue;
|
|
5385
|
+
}
|
|
5386
|
+
context.drawImage(tile.bitmap, tile.x, tile.y);
|
|
5387
|
+
}
|
|
5388
|
+
if (options.annotate) drawannotations(context, options.annotate, width, height, ratio);
|
|
5389
|
+
return canvasdataurl(canvas, options.format, options.quality);
|
|
5390
|
+
}
|
|
5391
|
+
async function decodeTile(dataurl, x, y, firstofcolumn) {
|
|
5392
|
+
const blob = await (await fetch(dataurl)).blob();
|
|
5393
|
+
return { bitmap: await createImageBitmap(blob), x, y, firstofcolumn };
|
|
5394
|
+
}
|
|
5395
|
+
async function croptoRect(dataurl, rect, ratio, options) {
|
|
5396
|
+
const blob = await (await fetch(dataurl)).blob();
|
|
5397
|
+
const bitmap = await createImageBitmap(blob);
|
|
5398
|
+
const canvas = new OffscreenCanvas(Math.max(1, Math.round(rect.width * ratio)), Math.max(1, Math.round(rect.height * ratio)));
|
|
5399
|
+
const context = canvas.getContext("2d");
|
|
5400
|
+
if (!context) throw new Error("The crop could not create a canvas context.");
|
|
5401
|
+
context.drawImage(bitmap, rect.x, rect.y, rect.width, rect.height, 0, 0, canvas.width, canvas.height);
|
|
5402
|
+
if (options.annotate) drawannotations(context, options.annotate, rect.width, rect.height, ratio);
|
|
5403
|
+
return canvasdataurl(canvas, options.format, options.quality);
|
|
5404
|
+
}
|
|
5405
|
+
async function encodecanvas(width, height, draw, options) {
|
|
5406
|
+
const canvas = new OffscreenCanvas(Math.max(1, Math.round(width)), Math.max(1, Math.round(height)));
|
|
5407
|
+
const context = canvas.getContext("2d");
|
|
5408
|
+
if (!context) throw new Error("The capture canvas could not create a context.");
|
|
5409
|
+
await draw(context);
|
|
5410
|
+
if (options.annotate) drawannotations(context, options.annotate, canvas.width / (options.annotateratio ?? 1), canvas.height / (options.annotateratio ?? 1), options.annotateratio ?? 1);
|
|
5411
|
+
return canvasdataurl(canvas, options.format, options.quality);
|
|
5412
|
+
}
|
|
5413
|
+
async function capturenamefor(step, plan, kind, format) {
|
|
5414
|
+
const naming = stepoptions2(step).naming;
|
|
5415
|
+
const rule = naming && typeof naming === "object" && !Array.isArray(naming) ? naming : { run: true, step: true, sequence: true, kind: true };
|
|
5416
|
+
const counters = (await memory.getcapturecounters()).find((item) => item.taskid === plan.id);
|
|
5417
|
+
const advanced = advancecounter(counters?.counters ?? {}, step.id);
|
|
5418
|
+
await memory.setcapturecounter({ taskid: plan.id, counters: advanced.counters, at: Date.now() });
|
|
5419
|
+
return buildname(rule, { run: plan.id, step: step.id, sequence: advanced.sequence, kind }, format);
|
|
5420
|
+
}
|
|
5421
|
+
async function routecapture(record2, session, plan, step, origin) {
|
|
5422
|
+
const target = record2.exporttarget ?? "memory";
|
|
5423
|
+
if (target === "clipboard") {
|
|
5424
|
+
const granted = await chrome.permissions.contains({ permissions: ["clipboardWrite"] }).catch(() => false);
|
|
5425
|
+
if (!granted) throw new Error("The clipboard capture export needs the clipboardwrite capability; request it from the review panel.");
|
|
5426
|
+
const blob = await (await fetch(record2.bytes ?? "")).blob();
|
|
5427
|
+
await navigator.clipboard.write([new ClipboardItem({ [blob.type]: blob })]);
|
|
5428
|
+
await memory.addclip(clipentryof("screen", { hash: cliphash(record2.bytes ?? ""), length: (record2.bytes ?? "").length }, origin, step.id, Date.now()));
|
|
5429
|
+
void session;
|
|
5430
|
+
void plan;
|
|
5431
|
+
return { target, destination: "clipboard" };
|
|
5432
|
+
}
|
|
5433
|
+
if (target === "download") {
|
|
5434
|
+
const granted = await chrome.permissions.contains({ permissions: ["downloads"] }).catch(() => false);
|
|
5435
|
+
if (!granted) throw new Error("The download capture export needs the downloads capability; request it from the review panel.");
|
|
5436
|
+
await chrome.downloads.download({ url: record2.bytes ?? "", filename: record2.name ?? `capture.${record2.format}` });
|
|
5437
|
+
return { target, destination: "reviewed download flow" };
|
|
5438
|
+
}
|
|
5439
|
+
return { target, destination: "session memory" };
|
|
5440
|
+
}
|
|
5441
|
+
async function runcapturepolicy() {
|
|
5442
|
+
return (await memory.getsettings())?.capturepolicy ?? "manual";
|
|
5443
|
+
}
|
|
5444
|
+
async function grabstateshot(step, session, plan, tabid2, phase) {
|
|
5445
|
+
const options = stepcaptureoptions(step);
|
|
5446
|
+
const format = options.format ?? "png";
|
|
5447
|
+
const shot = await tabshot(format, options.quality);
|
|
5448
|
+
const measured = await bridgecall(tabid2, "measurepage");
|
|
5449
|
+
const record2 = capturevisible({ runid: plan.id, stepid: step.id, options: { ...options, annotate: options.annotate ?? true }, viewport: { width: measured.viewportwidth, height: measured.viewportheight }, dataurl: shot, at: Date.now(), id: randomid(), name: `${phase}-${await capturenamefor(step, plan, "state", format)}` });
|
|
5450
|
+
await memory.addcapture(record2);
|
|
5451
|
+
return record2;
|
|
5452
|
+
}
|
|
5453
|
+
async function storecapture(record2, session, plan, step, origin) {
|
|
5454
|
+
await memory.addcapture(record2);
|
|
5455
|
+
const routed = await routecapture(record2, session, plan, step, origin);
|
|
5456
|
+
await memory.setprogress(recordcapture(await memory.getprogress(), plan.id, step.id, record2, Date.now()));
|
|
5457
|
+
await refreshbadge();
|
|
5458
|
+
return { record: record2, routed };
|
|
5459
|
+
}
|
|
5460
|
+
async function executecapturestep(step, session, plan, tabid2, origin) {
|
|
5461
|
+
const options = stepcaptureoptions(step);
|
|
5462
|
+
const format = options.format ?? "png";
|
|
5463
|
+
const ratio = options.pixelratio ?? 1;
|
|
5464
|
+
const runpolicy = await runcapturepolicy();
|
|
5465
|
+
const annotate = options.annotate ?? runpolicy === "annotated";
|
|
5466
|
+
const stepnumber = plan.steps.findIndex((candidate) => candidate.id === step.id) + 1;
|
|
5467
|
+
const extra = { ...session ? { sessionid: session.id } : {}, planid: plan.id, stepid: step.id };
|
|
5468
|
+
const pageurl = await chrome.tabs.get(tabid2).then((tab) => tab.url ?? origin).catch(() => origin);
|
|
5469
|
+
if (step.kind === "shotview") {
|
|
5470
|
+
const measured = await bridgecall(tabid2, "measurepage");
|
|
5471
|
+
const raw = await tabshot(format, options.quality);
|
|
5472
|
+
const marker = annotate ? annotationplanof({ step: Math.max(1, stepnumber), width: measured.viewportwidth, height: measured.viewportheight, url: pageurl, at: Date.now() }) : void 0;
|
|
5473
|
+
const dataurl = format === "png" && ratio === 1 && !marker ? raw : await encodecanvas(measured.viewportwidth * ratio, measured.viewportheight * ratio, async (context) => {
|
|
5474
|
+
const bitmap = await createImageBitmap(await (await fetch(raw)).blob());
|
|
5475
|
+
context.drawImage(bitmap, 0, 0, context.canvas.width, context.canvas.height);
|
|
5476
|
+
}, { format, quality: options.quality, annotate: marker, annotateratio: ratio });
|
|
5477
|
+
const record2 = capturevisible({ runid: plan.id, stepid: step.id, options: { ...options, ...annotate ? { annotate: true } : {} }, viewport: { width: measured.viewportwidth, height: measured.viewportheight }, dataurl, at: Date.now(), id: randomid(), name: await capturenamefor(step, plan, "shotview", format) });
|
|
5478
|
+
const stored = await storecapture(record2, session, plan, step, origin);
|
|
5479
|
+
await audit("capture", `Captured the visible viewport at ${record2.width} by ${record2.height} pixels in ${format} for step ${step.id}, routed to ${stored.routed.destination}.`, extra);
|
|
5480
|
+
return { ok: true, summary: `Captured the visible viewport at ${record2.width} by ${record2.height} pixels.`, details: { capture: { id: record2.id, kind: record2.kind, format: record2.format, width: record2.width, height: record2.height, bytes: (record2.bytes ?? "").length, exporttarget: stored.routed.target } } };
|
|
5481
|
+
}
|
|
5482
|
+
if (step.kind === "shotfullpage") {
|
|
5483
|
+
const rawoptions = stepoptions2(step);
|
|
5484
|
+
const settle2 = typeof rawoptions.settle === "number" ? rawoptions.settle : 150;
|
|
5485
|
+
const overlap = typeof rawoptions.overlap === "number" ? rawoptions.overlap : 0;
|
|
5486
|
+
const wait = typeof rawoptions.wait === "number" ? rawoptions.wait : void 0;
|
|
5487
|
+
const state = await bridgecall(tabid2, "preparecapture");
|
|
5488
|
+
try {
|
|
5489
|
+
const measured = await bridgecall(tabid2, "measurepage");
|
|
5490
|
+
const stitch = buildstitchplan({ scrollwidth: measured.scrollwidth, scrollheight: measured.scrollheight, viewportwidth: measured.viewportwidth, viewportheight: measured.viewportheight, overlap });
|
|
5491
|
+
if (wait !== void 0) {
|
|
5492
|
+
const budget = stitchbudgetallowed(stitch.tiles.length, settle2, wait);
|
|
5493
|
+
if (!budget.allowed) throw new Error(budget.reason ?? "The stitching scroll budget exceeds the reviewed wait window.");
|
|
5494
|
+
}
|
|
5495
|
+
const tiles = [];
|
|
5496
|
+
let done = 0;
|
|
5497
|
+
for (let index = 0; index < stitch.tiles.length; index += 1) {
|
|
5498
|
+
const tile = stitch.tiles[index];
|
|
5499
|
+
if (!tile) continue;
|
|
5500
|
+
await bridgecall(tabid2, "scrollcapture", tile.x, tile.y);
|
|
5501
|
+
await bridgecall(tabid2, "waitsettle", settle2);
|
|
5502
|
+
const shot = await tabshot("png", void 0);
|
|
5503
|
+
tiles.push(await decodeTile(shot, tile.x, tile.y, index % stitch.rows === 0));
|
|
5504
|
+
done += 1;
|
|
5505
|
+
stitchprogress.set(step.id, { stepid: step.id, done, total: stitch.tiles.length });
|
|
5506
|
+
}
|
|
5507
|
+
const annotation = annotate ? annotationplanof({ step: Math.max(1, stepnumber), width: stitch.scrollwidth, height: stitch.scrollheight, url: pageurl, at: Date.now() }) : void 0;
|
|
5508
|
+
const dataurl = await composestitched(tiles, stitch.scrollwidth, stitch.scrollheight, overlap, ratio, { format, quality: options.quality, annotate: annotation });
|
|
5509
|
+
const record2 = capturestitched({ runid: plan.id, stepid: step.id, options: { ...options, ...annotate ? { annotate: true } : {} }, plan: stitch, dataurl, at: Date.now(), id: randomid(), name: await capturenamefor(step, plan, "shotfullpage", format) });
|
|
5510
|
+
const stored = await storecapture(record2, session, plan, step, origin);
|
|
5511
|
+
await audit("capture", `Captured the full page of ${stitch.scrollwidth} by ${stitch.scrollheight} css pixels from ${stitch.tiles.length} stitched tile${stitch.tiles.length === 1 ? "" : "s"} with ${overlap} overlap row${overlap === 1 ? "" : "s"}, routed to ${stored.routed.destination}.`, extra);
|
|
5512
|
+
return { ok: true, summary: `Captured the full page from ${stitch.tiles.length} stitched tiles.`, details: { capture: { id: record2.id, kind: record2.kind, format: record2.format, width: record2.width, height: record2.height, bytes: (record2.bytes ?? "").length, exporttarget: stored.routed.target }, tiles: stitch.tiles.length, columns: stitch.columns, rows: stitch.rows, overlap } };
|
|
5513
|
+
} finally {
|
|
5514
|
+
stitchprogress.delete(step.id);
|
|
5515
|
+
await bridgecall(tabid2, "restorecapture", state).catch(() => void 0);
|
|
5516
|
+
}
|
|
5517
|
+
}
|
|
5518
|
+
if (step.kind === "shotelement") {
|
|
5519
|
+
const selector = step.target ?? "";
|
|
5520
|
+
const settle2 = typeof stepoptions2(step).settle === "number" ? stepoptions2(step).settle : 150;
|
|
5521
|
+
const measured = await bridgecall(tabid2, "measurepage");
|
|
5522
|
+
const targetinfo = await bridgecall(tabid2, "elementrect", selector);
|
|
5523
|
+
if (!targetinfo.ok || !targetinfo.rect) throw new Error(targetinfo.summary);
|
|
5524
|
+
const rect = targetinfo.rect;
|
|
5525
|
+
const crossing = targetinfo.crossesviewport === true;
|
|
5526
|
+
const state = await bridgecall(tabid2, "preparecapture");
|
|
5527
|
+
try {
|
|
5528
|
+
let dataurl = "";
|
|
5529
|
+
if (!crossing) {
|
|
5530
|
+
const scrollto = Math.max(0, rect.y - Math.floor((measured.viewportheight - rect.height) / 2));
|
|
5531
|
+
await bridgecall(tabid2, "scrollcapture", 0, scrollto);
|
|
5532
|
+
await bridgecall(tabid2, "waitsettle", settle2);
|
|
5533
|
+
const shot = await tabshot("png", void 0);
|
|
5534
|
+
const crop = croprect({ x: rect.x, y: rect.y - scrollto, width: rect.width, height: rect.height }, { width: measured.viewportwidth, height: measured.viewportheight });
|
|
5535
|
+
const annotation = annotate ? annotationplanof({ step: Math.max(1, stepnumber), width: crop.width, height: crop.height, rect: { x: 0, y: 0, width: crop.width, height: crop.height }, url: pageurl, at: Date.now() }) : void 0;
|
|
5536
|
+
dataurl = await croptoRect(shot, crop, ratio, { format, quality: options.quality, annotate: annotation });
|
|
5537
|
+
} else {
|
|
5538
|
+
const tops = regionsteps(rect.height, measured.viewportheight);
|
|
5539
|
+
const slices = [];
|
|
5540
|
+
for (let index = 0; index < tops.length; index += 1) {
|
|
5541
|
+
const top = tops[index] ?? 0;
|
|
5542
|
+
await bridgecall(tabid2, "scrollcapture", 0, rect.y + top);
|
|
5543
|
+
await bridgecall(tabid2, "waitsettle", settle2);
|
|
5544
|
+
const shot = await tabshot("png", void 0);
|
|
5545
|
+
const crop = croprect({ x: rect.x, y: 0, width: rect.width, height: Math.min(measured.viewportheight, rect.height - top) }, { width: measured.viewportwidth, height: measured.viewportheight });
|
|
5546
|
+
const slicedata = await croptoRect(shot, crop, 1, { format: "png" });
|
|
5547
|
+
slices.push(await decodeTile(slicedata, 0, top, index === 0));
|
|
5548
|
+
}
|
|
5549
|
+
const annotation = annotate ? annotationplanof({ step: Math.max(1, stepnumber), width: Math.min(rect.width, measured.viewportwidth), height: rect.height, rect: { x: 0, y: 0, width: Math.min(rect.width, measured.viewportwidth), height: rect.height }, url: pageurl, at: Date.now() }) : void 0;
|
|
5550
|
+
dataurl = await composestitched(slices, Math.min(rect.width, measured.viewportwidth), rect.height, 0, ratio, { format, quality: options.quality, annotate: annotation });
|
|
5551
|
+
}
|
|
5552
|
+
const record2 = captureelement({ runid: plan.id, stepid: step.id, options: { ...options, ...annotate ? { annotate: true } : {} }, rect, dataurl, at: Date.now(), id: randomid(), name: await capturenamefor(step, plan, "shotelement", format), target: selector });
|
|
5553
|
+
const stored = await storecapture(record2, session, plan, step, origin);
|
|
5554
|
+
await audit("capture", `Captured the element ${selector} at ${rect.width} by ${rect.height} css pixels${crossing ? " through the tiled fallback" : ""}, routed to ${stored.routed.destination}.`, extra);
|
|
5555
|
+
return { ok: true, summary: `Captured the element ${selector}.`, details: { capture: { id: record2.id, kind: record2.kind, format: record2.format, width: record2.width, height: record2.height, bytes: (record2.bytes ?? "").length, exporttarget: stored.routed.target }, rect, tiledfallback: crossing, target: selector } };
|
|
5556
|
+
} finally {
|
|
5557
|
+
await bridgecall(tabid2, "restorecapture", state).catch(() => void 0);
|
|
5558
|
+
}
|
|
5559
|
+
}
|
|
5560
|
+
if (step.kind === "shotregion") {
|
|
5561
|
+
const rawoptions = stepoptions2(step);
|
|
5562
|
+
const rect = rawoptions.regionrect;
|
|
5563
|
+
if (!rect) throw new Error("A reviewed regionrect is required in options.");
|
|
5564
|
+
const container = typeof rawoptions.container === "string" ? rawoptions.container : void 0;
|
|
5565
|
+
const measured = await bridgecall(tabid2, "measurepage");
|
|
5566
|
+
const state = await bridgecall(tabid2, "preparecapture");
|
|
5567
|
+
try {
|
|
5568
|
+
let dataurl = "";
|
|
5569
|
+
let captured = rect;
|
|
5570
|
+
if (container) {
|
|
5571
|
+
const info = await bridgecall(tabid2, "scrollcontainercapture", container, 0);
|
|
5572
|
+
if (!info.ok || info.height === void 0 || info.viewportheight === void 0) throw new Error(info.summary);
|
|
5573
|
+
const steps = regionsteps(info.height, info.viewportheight);
|
|
5574
|
+
const strips = [];
|
|
5575
|
+
for (let index = 0; index < steps.length; index += 1) {
|
|
5576
|
+
await bridgecall(tabid2, "scrollcontainercapture", container, steps[index] ?? 0);
|
|
5577
|
+
await bridgecall(tabid2, "waitsettle", typeof rawoptions.settle === "number" ? rawoptions.settle : 150);
|
|
5578
|
+
const shot = await tabshot("png", void 0);
|
|
5579
|
+
strips.push(await decodeTile(shot, 0, steps[index] ?? 0, index === 0));
|
|
5580
|
+
}
|
|
5581
|
+
const annotation = annotate ? annotationplanof({ step: Math.max(1, stepnumber), width: rect.width, height: info.height, rect, url: pageurl, at: Date.now() }) : void 0;
|
|
5582
|
+
dataurl = await composestitched(strips, rect.width, info.height, 0, ratio, { format, quality: options.quality, annotate: annotation });
|
|
5583
|
+
captured = { x: rect.x, y: rect.y, width: rect.width, height: info.height };
|
|
5584
|
+
} else {
|
|
5585
|
+
await bridgecall(tabid2, "scrollcapture", 0, Math.max(0, rect.y - Math.floor((measured.viewportheight - rect.height) / 2)));
|
|
5586
|
+
await bridgecall(tabid2, "waitsettle", typeof rawoptions.settle === "number" ? rawoptions.settle : 150);
|
|
5587
|
+
const shot = await tabshot("png", void 0);
|
|
5588
|
+
const crop = croprect({ x: rect.x, y: rect.y - Math.max(0, rect.y - Math.floor((measured.viewportheight - rect.height) / 2)), width: rect.width, height: rect.height }, { width: measured.viewportwidth, height: measured.viewportheight });
|
|
5589
|
+
const annotation = annotate ? annotationplanof({ step: Math.max(1, stepnumber), width: crop.width, height: crop.height, rect: { x: 0, y: 0, width: crop.width, height: crop.height }, url: pageurl, at: Date.now() }) : void 0;
|
|
5590
|
+
dataurl = await croptoRect(shot, crop, ratio, { format, quality: options.quality, annotate: annotation });
|
|
5591
|
+
captured = crop;
|
|
5592
|
+
}
|
|
5593
|
+
const record2 = captureregion({ runid: plan.id, stepid: step.id, options: { ...options, ...annotate ? { annotate: true } : {} }, rect: captured, dataurl, at: Date.now(), id: randomid(), name: await capturenamefor(step, plan, "shotregion", format), ...container ? { target: container } : {} });
|
|
5594
|
+
const stored = await storecapture(record2, session, plan, step, origin);
|
|
5595
|
+
await audit("capture", `Captured the reviewed region at ${captured.width} by ${captured.height} css pixels${container ? ` of the scrollable container ${container}` : ""}, routed to ${stored.routed.destination}.`, extra);
|
|
5596
|
+
return { ok: true, summary: `Captured the reviewed region${container ? ` of container ${container}` : ""}.`, details: { capture: { id: record2.id, kind: record2.kind, format: record2.format, width: record2.width, height: record2.height, bytes: (record2.bytes ?? "").length, exporttarget: stored.routed.target }, rect: captured, ...container ? { container } : {} } };
|
|
5597
|
+
} finally {
|
|
5598
|
+
await bridgecall(tabid2, "restorecapture", state).catch(() => void 0);
|
|
5599
|
+
}
|
|
5600
|
+
}
|
|
5601
|
+
if (step.kind === "contactsheet") {
|
|
5602
|
+
const rawoptions = stepoptions2(step);
|
|
5603
|
+
const elements = (Array.isArray(rawoptions.elements) ? rawoptions.elements : []).filter((item) => typeof item === "string" && item.trim().length > 0);
|
|
5604
|
+
const layout = rawoptions.sheet && typeof rawoptions.sheet === "object" && !Array.isArray(rawoptions.sheet) ? rawoptions.sheet : { cellsize: 240, columns: 3, label: "both" };
|
|
5605
|
+
const measured = await bridgecall(tabid2, "measurepage");
|
|
5606
|
+
const state = await bridgecall(tabid2, "preparecapture");
|
|
5607
|
+
try {
|
|
5608
|
+
const cells = [];
|
|
5609
|
+
for (const selector of elements) {
|
|
5610
|
+
const targetinfo = await bridgecall(tabid2, "elementrect", selector);
|
|
5611
|
+
if (!targetinfo.ok || !targetinfo.rect) throw new Error(targetinfo.summary);
|
|
5612
|
+
await bridgecall(tabid2, "scrollcapture", 0, Math.max(0, targetinfo.rect.y - Math.floor((measured.viewportheight - targetinfo.rect.height) / 2)));
|
|
5613
|
+
await bridgecall(tabid2, "waitsettle", typeof rawoptions.settle === "number" ? rawoptions.settle : 150);
|
|
5614
|
+
const shot = await tabshot("png", void 0);
|
|
5615
|
+
const crop = croprect({ x: 0, y: targetinfo.rect.y - Math.max(0, targetinfo.rect.y - Math.floor((measured.viewportheight - targetinfo.rect.height) / 2)), width: targetinfo.rect.width, height: targetinfo.rect.height }, { width: measured.viewportwidth, height: measured.viewportheight });
|
|
5616
|
+
const celldata = await croptoRect(shot, crop, 1, { format: "png" });
|
|
5617
|
+
cells.push({ selector, dataurl: celldata, width: crop.width, height: crop.height });
|
|
5618
|
+
}
|
|
5619
|
+
const placed = buildsheet(cells.map((cell) => ({ selector: cell.selector })), layout);
|
|
5620
|
+
const width = placed.columns * layout.cellsize;
|
|
5621
|
+
const rows = placed.rows;
|
|
5622
|
+
const height = rows * (layout.cellsize + 28);
|
|
5623
|
+
const annotation = annotate ? annotationplanof({ step: Math.max(1, stepnumber), width, height, url: pageurl, at: Date.now() }) : void 0;
|
|
5624
|
+
const dataurl = await encodecanvas(width * ratio, height * ratio, async (context) => {
|
|
5625
|
+
context.scale(ratio, ratio);
|
|
5626
|
+
for (let index = 0; index < cells.length; index += 1) {
|
|
5627
|
+
const cell = cells[index];
|
|
5628
|
+
const place = placed.cells[index];
|
|
5629
|
+
if (!cell || !place) continue;
|
|
5630
|
+
const bitmap = await createImageBitmap(await (await fetch(cell.dataurl)).blob());
|
|
5631
|
+
const scaled = Math.min(layout.cellsize / Math.max(1, bitmap.width), layout.cellsize / Math.max(1, bitmap.height));
|
|
5632
|
+
const drawwidth = Math.max(1, Math.round(bitmap.width * scaled));
|
|
5633
|
+
const drawheight = Math.max(1, Math.round(bitmap.height * scaled));
|
|
5634
|
+
context.drawImage(bitmap, place.column * layout.cellsize + Math.floor((layout.cellsize - drawwidth) / 2), place.row * (layout.cellsize + 28) + Math.floor((layout.cellsize - drawheight) / 2), drawwidth, drawheight);
|
|
5635
|
+
if (place.caption) {
|
|
5636
|
+
context.fillStyle = "rgba(23,32,44,.86)";
|
|
5637
|
+
context.fillRect(place.column * layout.cellsize, place.row * (layout.cellsize + 28) + layout.cellsize, layout.cellsize, 28);
|
|
5638
|
+
context.fillStyle = "#ffffff";
|
|
5639
|
+
context.font = "12px sans-serif";
|
|
5640
|
+
context.textAlign = "left";
|
|
5641
|
+
context.textBaseline = "middle";
|
|
5642
|
+
context.fillText(place.caption.slice(0, Math.floor(layout.cellsize / 7)), place.column * layout.cellsize + 6, place.row * (layout.cellsize + 28) + layout.cellsize + 14);
|
|
5643
|
+
}
|
|
5644
|
+
}
|
|
5645
|
+
}, { format, quality: options.quality, annotate: annotation, annotateratio: ratio });
|
|
5646
|
+
const record2 = { id: randomid(), runid: plan.id, stepid: step.id, kind: "contactsheet", format, width: Math.round(width * ratio), height: Math.round(height * ratio), capturedat: Date.now(), bytes: dataurl, name: await capturenamefor(step, plan, "contactsheet", format), ...annotate ? { annotated: true } : {}, ...options.exporttarget !== void 0 ? { exporttarget: options.exporttarget } : {} };
|
|
5647
|
+
const stored = await storecapture(record2, session, plan, step, origin);
|
|
5648
|
+
await audit("capture", `Tiled ${cells.length} element capture${cells.length === 1 ? "" : "s"} into one labeled ${placed.columns} column contact sheet, routed to ${stored.routed.destination}.`, extra);
|
|
5649
|
+
return { ok: true, summary: `Tiled ${cells.length} element captures into one contact sheet.`, details: { capture: { id: record2.id, kind: record2.kind, format: record2.format, width: record2.width, height: record2.height, bytes: (record2.bytes ?? "").length, exporttarget: stored.routed.target }, cells: placed.cells, columns: placed.columns, rows: placed.rows } };
|
|
5650
|
+
} finally {
|
|
5651
|
+
await bridgecall(tabid2, "restorecapture", state).catch(() => void 0);
|
|
5652
|
+
}
|
|
5653
|
+
}
|
|
5654
|
+
throw new Error("Unsupported capture kind.");
|
|
5655
|
+
}
|
|
4944
5656
|
async function enforcewindowreview(step, session, plan) {
|
|
4945
5657
|
const windowid = step.value && /^\d+$/.test(step.value) ? Number.parseInt(step.value, 10) : 0;
|
|
4946
5658
|
const progress = plan ? await memory.getprogress() : void 0;
|
|
@@ -4979,8 +5691,9 @@ async function refreshbadge() {
|
|
|
4979
5691
|
const consents = (await memory.getclipconsents()).filter((record2) => record2.approved === void 0).length;
|
|
4980
5692
|
const quarantined = (await memory.getquarantines()).filter((entry) => entry.scan === "pending").length;
|
|
4981
5693
|
const datasets = (await memory.getdatasets()).length;
|
|
5694
|
+
const captures = (await memory.getcaptures()).length;
|
|
4982
5695
|
const tasktabs2 = new Set(badges.map((badge) => badge.tabid)).size;
|
|
4983
|
-
const total = (queues?.prefetch ?? 0) + (queues?.batchopen ?? 0) + tasktabs2 + prompts + consents + quarantined + datasets;
|
|
5696
|
+
const total = (queues?.prefetch ?? 0) + (queues?.batchopen ?? 0) + tasktabs2 + prompts + consents + quarantined + datasets + captures;
|
|
4984
5697
|
await chrome.action.setBadgeText({ text: total > 0 ? String(total) : "" }).catch(() => {
|
|
4985
5698
|
});
|
|
4986
5699
|
}
|
|
@@ -4999,46 +5712,69 @@ async function executestep(stepid) {
|
|
|
4999
5712
|
}
|
|
5000
5713
|
let output;
|
|
5001
5714
|
let watchwindow;
|
|
5002
|
-
|
|
5003
|
-
|
|
5004
|
-
|
|
5005
|
-
|
|
5006
|
-
|
|
5007
|
-
|
|
5008
|
-
|
|
5009
|
-
|
|
5010
|
-
|
|
5011
|
-
|
|
5012
|
-
|
|
5013
|
-
|
|
5014
|
-
|
|
5015
|
-
|
|
5016
|
-
|
|
5017
|
-
|
|
5018
|
-
|
|
5019
|
-
|
|
5020
|
-
|
|
5021
|
-
|
|
5022
|
-
|
|
5023
|
-
|
|
5024
|
-
|
|
5025
|
-
|
|
5026
|
-
|
|
5027
|
-
|
|
5028
|
-
if (
|
|
5029
|
-
|
|
5030
|
-
|
|
5031
|
-
|
|
5032
|
-
|
|
5033
|
-
|
|
5034
|
-
|
|
5035
|
-
|
|
5036
|
-
|
|
5037
|
-
|
|
5038
|
-
|
|
5039
|
-
|
|
5715
|
+
const dispatchreviewedstep = async () => {
|
|
5716
|
+
if (step.kind === "windowclose") {
|
|
5717
|
+
await enforcewindowreview(step, session, plan);
|
|
5718
|
+
}
|
|
5719
|
+
if (istabscommandkind(step.kind)) {
|
|
5720
|
+
output = await executetabscommand(step, session, plan, tab.id);
|
|
5721
|
+
} else if (isdatasetkind(step.kind)) {
|
|
5722
|
+
output = await executedatastep(step, session, plan, tab.id, origin);
|
|
5723
|
+
} else if (isfileskind(step.kind)) {
|
|
5724
|
+
output = await executefilesstep(step, session, plan, tab.id, origin);
|
|
5725
|
+
} else if (isformkind(step.kind)) {
|
|
5726
|
+
output = await executeformstep(step, session, plan, tab.id, origin);
|
|
5727
|
+
} else if (isbrowserkind(step.kind)) {
|
|
5728
|
+
output = await runbrowseraction(step, tab.id, tab.windowId ?? chrome.windows.WINDOW_ID_CURRENT);
|
|
5729
|
+
} else if (step.kind === "keyhold") {
|
|
5730
|
+
output = await executekeyhold(step, session, plan, tab.id, origin);
|
|
5731
|
+
} else if (step.kind === "keyrelease") {
|
|
5732
|
+
output = await executekeyrelease(step, session, plan, tab.id, origin);
|
|
5733
|
+
} else if (step.kind === "dismissdialog") {
|
|
5734
|
+
output = await executedismissdialog(step, session, plan, tab.id, origin);
|
|
5735
|
+
} else if (step.kind === "retryaction") {
|
|
5736
|
+
output = await executeretryaction(step, session, plan, tab.id, origin);
|
|
5737
|
+
} else if (step.kind === "mapclicks") {
|
|
5738
|
+
output = await executemapclicks(step, plan, tab.id, origin);
|
|
5739
|
+
} else if (step.kind === "enterframe") {
|
|
5740
|
+
output = await executeenterframe(step, plan, tab.id, origin);
|
|
5741
|
+
} else if (watchstepkinds.has(step.kind)) {
|
|
5742
|
+
if (!session || !plan || plan.state !== "approved") throw new Error("Watch kinds refuse to run outside an approved session plan.");
|
|
5743
|
+
const watched = await executewatchstep(step, session, plan, tab.id, origin);
|
|
5744
|
+
output = watched.output;
|
|
5745
|
+
watchwindow = { startedat: watched.watch.startedat, lifetime: watched.watch.lifetime };
|
|
5746
|
+
} else if (step.kind === "diffsnapshots") {
|
|
5747
|
+
output = await executediffsnapshots(step, session, plan, tab.id, origin);
|
|
5748
|
+
} else if (navigationstepkinds.has(step.kind)) {
|
|
5749
|
+
output = await executenavigationkind(step, session, plan, tab.id, origin);
|
|
5750
|
+
} else if (iscapturekind(step.kind)) {
|
|
5751
|
+
output = await executecapturestep(step, session, plan, tab.id, origin);
|
|
5752
|
+
} else {
|
|
5753
|
+
if (step.target && freshcheckkinds.has(step.kind)) {
|
|
5754
|
+
const fresh = await snapshot(tab.id);
|
|
5755
|
+
if (!fresh.interactive.some((item) => item.selector === step.target)) throw new Error("The page changed and the target must be reviewed again.");
|
|
5756
|
+
}
|
|
5757
|
+
output = await dispatchpagestep(step, tab.id, origin, plan);
|
|
5040
5758
|
}
|
|
5041
|
-
output
|
|
5759
|
+
return output;
|
|
5760
|
+
};
|
|
5761
|
+
const runplan = plan;
|
|
5762
|
+
const capturepolicystate = await runcapturepolicy();
|
|
5763
|
+
if (capturepolicystate === "beforeafter" && session && step.risk !== "read" && beforeafterwrapallowed(step.kind)) {
|
|
5764
|
+
const before = await grabstateshot(step, session, runplan, tab.id, "before");
|
|
5765
|
+
output = await dispatchreviewedstep();
|
|
5766
|
+
if (output?.ok) {
|
|
5767
|
+
const after = await grabstateshot(step, session, runplan, tab.id, "after");
|
|
5768
|
+
const domversion = await memory.getobservationversion();
|
|
5769
|
+
const paired = capturestates({ policy: "beforeafter", before, after, actionkind: step.kind, ...step.target ? { target: step.target } : {}, ...domversion !== void 0 ? { domsnapshotid: String(domversion) } : {}, at: Date.now(), id: randomid() });
|
|
5770
|
+
if (paired.pair) {
|
|
5771
|
+
await memory.addpair(paired.pair);
|
|
5772
|
+
await memory.setprogress(recordpair(await memory.getprogress(), runplan.id, step.id, paired.pair, Date.now()));
|
|
5773
|
+
await audit("capture", `Paired the before shot ${paired.pair.beforeid} with the after shot ${paired.pair.afterid} around the ${step.kind} action${paired.pair.domsnapshotid !== void 0 ? ` with dom snapshot ${paired.pair.domsnapshotid}` : ""}.`, { sessionid: session.id, planid: runplan.id, stepid: step.id });
|
|
5774
|
+
}
|
|
5775
|
+
}
|
|
5776
|
+
} else {
|
|
5777
|
+
output = await dispatchreviewedstep();
|
|
5042
5778
|
}
|
|
5043
5779
|
if (["navigate", "back", "forward"].includes(step.kind)) await recordnavigation(step, session, tab.id);
|
|
5044
5780
|
await recordevidence(step, output, session, plan, origin);
|
|
@@ -5196,6 +5932,13 @@ async function handlerequest(message, sender) {
|
|
|
5196
5932
|
const capturecounters = await memory.getcapturecounters();
|
|
5197
5933
|
const inventory = await memory.getinventory();
|
|
5198
5934
|
const mimefilters = await memory.getmimefilters();
|
|
5935
|
+
const capturemetadata = (await memory.getcaptures()).map((record2) => {
|
|
5936
|
+
const { bytes, ...meta } = record2;
|
|
5937
|
+
void bytes;
|
|
5938
|
+
return meta;
|
|
5939
|
+
});
|
|
5940
|
+
const capturepairs = await memory.getpairs();
|
|
5941
|
+
const runsettings = await memory.getsettings();
|
|
5199
5942
|
const scanhooks = [];
|
|
5200
5943
|
for (const hook of await memory.getscanhooks()) {
|
|
5201
5944
|
scanhooks.push({ ...hook, granted: await chrome.permissions.contains({ origins: [hostpattern(hook.origin)] }).catch(() => false) });
|
|
@@ -5206,7 +5949,7 @@ async function handlerequest(message, sender) {
|
|
|
5206
5949
|
const livetab = session ? await chrome.tabs.get(session.tabid).catch(() => void 0) : void 0;
|
|
5207
5950
|
const waitprofile = session ? waitprofiles.find((record2) => record2.origin === session.origin) : void 0;
|
|
5208
5951
|
const livestate = { phase: livetab?.status === "loading" ? "loading" : "complete", ...navrecords[0] ? { finalurl: navrecords[0].finalurl, redirects: navrecords[0].chain } : {} };
|
|
5209
|
-
return { config: await memory.getconfig(), session, plan, progress: plan && progress?.planid === plan.id ? progress : void 0, diagnostic: await memory.getdiagnostic(), audit: await memory.getaudit(), capabilities: await refreshcapabilities(), outcomes: await memory.getoutcomes(), holds: heldkeysreport({ tabid: session?.tabid ?? 0, holds }), dialogs: await memory.getdialogs(), retries: await memory.getretries(), ...signals ? { signals: signalsreport({ signals }) } : { signals: signalsreport({}) }, banners: await memory.getbanners(), mutationevents: await memory.getmutationevents(), focusevents: await memory.getfocusevents(), diffs: await memory.getdiffs(), selectors: await memory.getselectors(), ...a11y ? { a11y } : {}, ...reader ? { reader } : {}, ...map ? { map } : {}, trail: trailreport({ ...session ? { sessionid: session.id } : {}, trail }), navrecords, ratestates, safeties, curated, waitprofiles, auths, navcontrol, navqueues, artifacts, navstate: livestate, ...waitprofile ? { waitprofile } : {}, offline: !navigator.onLine, tabs, windows, layouts: layoutreport({ layouts }), tabgroups, tabmetas, badges, snapshots, closedtabs, tabwatchevents, clones, tasktabgauge: taskgauge, ...controltab ? { controltab } : {}, tabreport: report, profiles, tickets, wizards: wizardreport({ ...session ? { sessionid: session.id } : {}, wizards, picks }), picks, errorreports, captchas, detections, ...codeentry !== void 0 ? { codeentry: true } : {}, datasets, imports, extractsessions, streams, exports, provenances, taskrules, sheetendpoints: sheetgrants, downloads, netlogs, clipconsents, clips, quarantines, cleanuprules, cleanupruns, capturecounters, inventory, mimefilters, scanhooks };
|
|
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()] } : {} };
|
|
5210
5953
|
}
|
|
5211
5954
|
case "capabilities":
|
|
5212
5955
|
return refreshcapabilities();
|
|
@@ -5248,7 +5991,8 @@ async function handlerequest(message, sender) {
|
|
|
5248
5991
|
const outcome = (await memory.getoutcomes()).find((candidate) => candidate.stepid === (input.stepid ?? ""));
|
|
5249
5992
|
if (!outcome) throw new Error("No outcome exists for the reviewed step.");
|
|
5250
5993
|
const resolved = outcome.details?.resolvedtarget;
|
|
5251
|
-
|
|
5994
|
+
const capture = outcome.details?.capture;
|
|
5995
|
+
return JSON.parse(outcomeresponse({ outcome, plan, ...resolved ? { resolvedtarget: resolved } : {}, ...capture ? { capture } : {} }));
|
|
5252
5996
|
}
|
|
5253
5997
|
case "map": {
|
|
5254
5998
|
const plan = await memory.getplan();
|
|
@@ -5612,6 +6356,53 @@ async function handlerequest(message, sender) {
|
|
|
5612
6356
|
await audit("observation", `The review panel exported ${records.length} netlog record${records.length === 1 ? "" : "s"} of the run with every header value redacted.`, { sessionid: session.id });
|
|
5613
6357
|
return { records, redacted: true, redaction: "every header value is redacted from exported netlogs" };
|
|
5614
6358
|
}
|
|
6359
|
+
case "capturebytes": {
|
|
6360
|
+
const inputcapture = message;
|
|
6361
|
+
const record2 = await memory.getcapture(inputcapture.id ?? "");
|
|
6362
|
+
if (!record2) throw new Error("No stored capture matches the requested id.");
|
|
6363
|
+
if (!record2.bytes) throw new Error("The capture bytes expired from the retention window; the metadata stays for the audit trail.");
|
|
6364
|
+
return { id: record2.id, runid: record2.runid, kind: record2.kind, format: record2.format, width: record2.width, height: record2.height, capturedat: record2.capturedat, name: record2.name, annotated: record2.annotated === true, bytes: record2.bytes };
|
|
6365
|
+
}
|
|
6366
|
+
case "capturereport":
|
|
6367
|
+
return capturereport({ records: await memory.getcaptures(), pairs: await memory.getpairs() });
|
|
6368
|
+
case "copycapture": {
|
|
6369
|
+
const inputcopy = message;
|
|
6370
|
+
const session = await memory.getsession();
|
|
6371
|
+
if (!session || session.stoppedat || session.expiresat <= Date.now()) throw new Error("Capture clipboard copies stay behind the consent gate of an active session.");
|
|
6372
|
+
const record2 = await memory.getcapture(inputcopy.id ?? "");
|
|
6373
|
+
if (!record2) throw new Error("No stored capture matches the requested id.");
|
|
6374
|
+
if (!record2.bytes) throw new Error("The capture bytes expired from the retention window; the metadata stays for the audit trail.");
|
|
6375
|
+
const granted = await chrome.permissions.contains({ permissions: ["clipboardWrite"] }).catch(() => false);
|
|
6376
|
+
if (!granted) throw new Error("The capture copy needs the clipboardwrite capability; request it from the review panel.");
|
|
6377
|
+
const blob = await (await fetch(record2.bytes)).blob();
|
|
6378
|
+
await navigator.clipboard.write([new ClipboardItem({ [blob.type]: blob })]);
|
|
6379
|
+
await memory.addclip(clipentryof("screen", { hash: cliphash(record2.bytes), length: record2.bytes.length }, session.origin, record2.stepid, Date.now()));
|
|
6380
|
+
await audit("capture", `The review panel copied capture ${record2.id} of kind ${record2.kind} to the clipboard with payload hash ${cliphash(record2.bytes)}.`, { sessionid: session.id });
|
|
6381
|
+
return { id: record2.id, copied: true };
|
|
6382
|
+
}
|
|
6383
|
+
case "downloadcapture": {
|
|
6384
|
+
const inputdownloadcapture = message;
|
|
6385
|
+
const session = await memory.getsession();
|
|
6386
|
+
if (!session || session.stoppedat || session.expiresat <= Date.now()) throw new Error("Capture downloads stay behind the consent gate of an active session.");
|
|
6387
|
+
const record2 = await memory.getcapture(inputdownloadcapture.id ?? "");
|
|
6388
|
+
if (!record2) throw new Error("No stored capture matches the requested id.");
|
|
6389
|
+
if (!record2.bytes) throw new Error("The capture bytes expired from the retention window; the metadata stays for the audit trail.");
|
|
6390
|
+
const granted = await chrome.permissions.contains({ permissions: ["downloads"] }).catch(() => false);
|
|
6391
|
+
if (!granted) throw new Error("The capture download needs the downloads capability; request it from the review panel.");
|
|
6392
|
+
await chrome.downloads.download({ url: record2.bytes, filename: record2.name ?? `capture.${record2.format}` });
|
|
6393
|
+
await audit("capture", `The review panel downloaded capture ${record2.id} of kind ${record2.kind} through the reviewed download flow.`, { sessionid: session.id });
|
|
6394
|
+
return { id: record2.id, downloaded: true };
|
|
6395
|
+
}
|
|
6396
|
+
case "setcapturepolicy": {
|
|
6397
|
+
const inputpolicy = message;
|
|
6398
|
+
const mode = inputpolicy.mode === "off" || inputpolicy.mode === "manual" || inputpolicy.mode === "annotated" || inputpolicy.mode === "beforeafter" ? inputpolicy.mode : void 0;
|
|
6399
|
+
if (!mode) throw new Error("The capture policy must be off, manual, annotated or beforeafter.");
|
|
6400
|
+
const settings = await memory.getsettings();
|
|
6401
|
+
await memory.setsettings({ ...settings, capturepolicy: mode });
|
|
6402
|
+
await audit("configure", `The user set the capture policy of the run to ${mode}; beforeafter wraps page moving actions with state pairs.`);
|
|
6403
|
+
await refreshbadge();
|
|
6404
|
+
return { capturepolicy: mode };
|
|
6405
|
+
}
|
|
5615
6406
|
case "stop": {
|
|
5616
6407
|
const session = await memory.getsession();
|
|
5617
6408
|
if (session) await memory.setsession({ ...session, stoppedat: Date.now() });
|