@wenathlan/extension 1.1.44 → 1.1.45

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.
@@ -1042,6 +1042,103 @@ var sessionmemory = class {
1042
1042
  if (live.length !== records.length) await this.adapter.set("ratelimits", live);
1043
1043
  return live;
1044
1044
  }
1045
+ /** Stores one run timeline entry; the user configured timeline retention window expires the oldest entries while their level counts survive in the per run level summaries. */
1046
+ async addtimelineentry(entry) {
1047
+ const records = await this.gettimeline();
1048
+ const combined = [entry, ...records];
1049
+ const retention = (await this.getsettings())?.timelineretention;
1050
+ if (retention === void 0) {
1051
+ await this.adapter.set("timelineentries", combined);
1052
+ return;
1053
+ }
1054
+ const kept = combined.slice(0, retention);
1055
+ const expired = combined.slice(retention);
1056
+ if (expired.length > 0) {
1057
+ const expiredcounts = /* @__PURE__ */ new Map();
1058
+ for (const item of expired) {
1059
+ const counts = expiredcounts.get(item.runid) ?? {};
1060
+ counts[item.level] = (counts[item.level] ?? 0) + 1;
1061
+ expiredcounts.set(item.runid, counts);
1062
+ }
1063
+ for (const [runid, counts] of expiredcounts) await this.mergelevelsummary(runid, counts, Date.now());
1064
+ }
1065
+ await this.adapter.set("timelineentries", kept);
1066
+ }
1067
+ /** Returns every stored run timeline entry, newest first. */
1068
+ async gettimeline() {
1069
+ return await this.adapter.get("timelineentries") ?? [];
1070
+ }
1071
+ /** Returns the run timeline entries filtered by run, level and step id. */
1072
+ async listtimeline(filter) {
1073
+ const records = await this.gettimeline();
1074
+ return records.filter((item) => (filter.runid === void 0 || item.runid === filter.runid) && (filter.level === void 0 || item.level === filter.level) && (filter.stepid === void 0 || item.stepid === filter.stepid));
1075
+ }
1076
+ /** Stores one captured javascript error record with its stack frames, source url and line. */
1077
+ async adderrorrecord(record2) {
1078
+ const records = await this.adapter.get("errorrecords") ?? [];
1079
+ await this.adapter.set("errorrecords", [record2, ...records]);
1080
+ }
1081
+ /** Returns every stored error record, newest first. */
1082
+ async geterrorrecords() {
1083
+ return await this.adapter.get("errorrecords") ?? [];
1084
+ }
1085
+ /** Stores one captured unhandled rejection record with its reason and stack frames. */
1086
+ async addrejectionrecord(record2) {
1087
+ const records = await this.adapter.get("rejectionrecords") ?? [];
1088
+ await this.adapter.set("rejectionrecords", [record2, ...records]);
1089
+ }
1090
+ /** Returns every stored rejection record, newest first. */
1091
+ async getrejectionrecords() {
1092
+ return await this.adapter.get("rejectionrecords") ?? [];
1093
+ }
1094
+ /** Stores one captured long task entry with its duration, start time and attribution names. */
1095
+ async addlongtask(record2) {
1096
+ const records = await this.adapter.get("longtasks") ?? [];
1097
+ await this.adapter.set("longtasks", [record2, ...records]);
1098
+ }
1099
+ /** Returns every stored long task entry, newest first. */
1100
+ async getlongtasks() {
1101
+ return await this.adapter.get("longtasks") ?? [];
1102
+ }
1103
+ /** Stores one console diff result between two runs, replacing the previous one. */
1104
+ async addconsolediff(diff) {
1105
+ return this.adapter.set("consolediff", diff);
1106
+ }
1107
+ /** Returns the one stored console diff result. */
1108
+ async getdiff() {
1109
+ return this.adapter.get("consolediff");
1110
+ }
1111
+ /** Stores one log rotation target record with its overflow entry counts, replacing the previous record of that target and run. */
1112
+ async addrotationtarget(record2) {
1113
+ const records = (await this.adapter.get("rotationtargets") ?? []).filter((item) => !(item.target === record2.target && item.runid === record2.runid));
1114
+ await this.adapter.set("rotationtargets", [record2, ...records]);
1115
+ }
1116
+ /** Returns every stored rotation target record with its overflow entry counts, newest first. */
1117
+ async getrotationtargets() {
1118
+ return await this.adapter.get("rotationtargets") ?? [];
1119
+ }
1120
+ /** Stores one console capture consent decision per origin; the approved decision persists so console watching on that origin prompts once. */
1121
+ async setconsoleconsent(consent) {
1122
+ const records = (await this.adapter.get("consoleconsents") ?? []).filter((item) => item.id !== consent.id);
1123
+ await this.adapter.set("consoleconsents", [consent, ...records]);
1124
+ }
1125
+ /** Returns every console capture consent decision, newest first. */
1126
+ async getconsoleconsents() {
1127
+ return await this.adapter.get("consoleconsents") ?? [];
1128
+ }
1129
+ /** Merges expired entry counts into the per run level count summary that survives the retention window. */
1130
+ async mergelevelsummary(runid, counts, now) {
1131
+ const records = await this.getlevelsummaries();
1132
+ const existing = records.find((item) => item.runid === runid);
1133
+ const merged = { ...existing?.counts ?? {} };
1134
+ for (const [level, count] of Object.entries(counts)) merged[level] = (merged[level] ?? 0) + count;
1135
+ const updated = { runid, counts: merged, at: now };
1136
+ await this.adapter.set("levelsummaries", [updated, ...records.filter((item) => item.runid !== runid)]);
1137
+ }
1138
+ /** Returns every per run level count summary, newest first. */
1139
+ async getlevelsummaries() {
1140
+ return await this.adapter.get("levelsummaries") ?? [];
1141
+ }
1045
1142
  };
1046
1143
  function mediakindof(record2) {
1047
1144
  if ("pages" in record2) return "pdf";
@@ -2043,10 +2140,107 @@ ${file.content}\r
2043
2140
  return { chunks, boundary, bytes: chunks.reduce((total, chunk) => total + chunk.length, 0) };
2044
2141
  }
2045
2142
 
2143
+ // runtimeline.ts
2144
+ var timelinekinds = ["watchconsole", "watcherrors", "watchtasks"];
2145
+ var loglevels = ["error", "warn", "info", "log", "debug", "trace"];
2146
+ var timelinesources = ["console", "error", "rejection", "resource", "longtask", "network"];
2147
+ function attachtimeline(input) {
2148
+ return { runid: input.runid, origin: input.origin, stepids: [...input.stepids], attachedat: input.now, entries: [] };
2149
+ }
2150
+ function spamdetect(entries, rule) {
2151
+ const collapsed = [];
2152
+ const counts = /* @__PURE__ */ new Map();
2153
+ for (const entry of entries) {
2154
+ if (rule.pattern !== "" && !entry.message.includes(rule.pattern)) {
2155
+ collapsed.push({ ...entry, repeat: 1 });
2156
+ continue;
2157
+ }
2158
+ const key = `${entry.level}|${entry.source}|${entry.message}`;
2159
+ const previous = collapsed[collapsed.length - 1];
2160
+ if (previous && previous.repeat !== void 0 && `${previous.level}|${previous.source}|${previous.message}` === key && entry.time - previous.time <= rule.windowsize) {
2161
+ previous.repeat += 1;
2162
+ continue;
2163
+ }
2164
+ collapsed.push({ ...entry, repeat: 1 });
2165
+ }
2166
+ for (const entry of collapsed) {
2167
+ if (entry.repeat > 1) counts.set(`${entry.level}|${entry.source}|${entry.message}`, entry.repeat);
2168
+ }
2169
+ const flagged = [...counts.entries()].filter(([, count]) => count > rule.collapse).map(([key, count]) => ({ message: key.split("|").slice(2).join("|"), count }));
2170
+ return { entries: collapsed, flagged };
2171
+ }
2172
+ function rotatelogs(entries, rule) {
2173
+ if (entries.length <= rule.maxentries) return { kept: [...entries], overflow: [] };
2174
+ const kept = entries.slice(entries.length - rule.maxentries);
2175
+ const overflow = entries.slice(0, entries.length - rule.maxentries);
2176
+ return { kept, overflow };
2177
+ }
2178
+ function timelinecounts(entries) {
2179
+ const counts = {};
2180
+ for (const level of loglevels) counts[level] = 0;
2181
+ for (const entry of entries) counts[entry.level] = (counts[entry.level] ?? 0) + 1;
2182
+ return counts;
2183
+ }
2184
+ function blockingduration(tasks, stepid, window2) {
2185
+ const inside = tasks.filter((task) => task.starttime >= window2.startedat && task.starttime <= window2.endedat);
2186
+ return { stepid, blocking: inside.reduce((total, task) => total + task.duration, 0), tasks: inside.length };
2187
+ }
2188
+ function netfailureentryof(input) {
2189
+ const exchange = input.exchange;
2190
+ if (exchange.errorclass === void 0 && exchange.status < 400) return null;
2191
+ return { id: input.id, runid: exchange.runid, stepid: exchange.stepid, url: exchange.url, status: exchange.status, errorclass: exchange.errorclass ?? "httperror", correlationid: exchange.correlationid, at: input.at };
2192
+ }
2193
+ function watcherdetached(input) {
2194
+ for (const navigation of input.navigations) {
2195
+ if (navigation >= input.startedat && navigation <= input.startedat + input.lifetime) return { detached: true, at: navigation };
2196
+ }
2197
+ return { detached: false };
2198
+ }
2199
+ function consolediff(input) {
2200
+ const base = input.baselines;
2201
+ const target = input.targetlines;
2202
+ const basemap = /* @__PURE__ */ new Map();
2203
+ for (const line of base) basemap.set(line, (basemap.get(line) ?? 0) + 1);
2204
+ const targetmap = /* @__PURE__ */ new Map();
2205
+ for (const line of target) targetmap.set(line, (targetmap.get(line) ?? 0) + 1);
2206
+ const lines = [];
2207
+ const added = [];
2208
+ const removed = [];
2209
+ const repeated = [];
2210
+ for (const [line, count] of targetmap) {
2211
+ const basecount = basemap.get(line) ?? 0;
2212
+ if (basecount === 0) {
2213
+ for (let index = 0; index < count; index += 1) {
2214
+ lines.push({ kind: "added", text: line });
2215
+ added.push(line);
2216
+ }
2217
+ continue;
2218
+ }
2219
+ const share = Math.min(basecount, count);
2220
+ for (let index = 0; index < share; index += 1) {
2221
+ lines.push({ kind: "repeated", text: line, count: share });
2222
+ repeated.push(line);
2223
+ }
2224
+ for (let index = share; index < count; index += 1) {
2225
+ lines.push({ kind: "added", text: line });
2226
+ added.push(line);
2227
+ }
2228
+ }
2229
+ for (const [line, count] of basemap) {
2230
+ const targetcount = targetmap.get(line) ?? 0;
2231
+ const missing = Math.max(0, count - targetcount);
2232
+ for (let index = 0; index < missing; index += 1) {
2233
+ lines.push({ kind: "removed", text: line });
2234
+ removed.push(line);
2235
+ }
2236
+ }
2237
+ return { base: input.baseid, target: input.targetid, lines, added: added.length, removed: removed.length, repeated: repeated.length, at: input.now };
2238
+ }
2239
+
2046
2240
  // policy.ts
2047
2241
  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", "callrest", "callgraphql", "sendmessage", "blockrequest", "mockresponse", "rewriteheaders", "setcookies", "clearcookies", "authflow", "saveapikey", "routeproxy", "postform", "postfiles"]);
2048
2242
  var interactionactions = /* @__PURE__ */ new Set(["focus", "scroll", "hover", "clickdeep", "rightclick", "doubleclick", "scrollpage", "scrollby", "scrollend", "scrolltop", "fullscreen", "zoomset", "movepointer", "clicktext", "clickaria", "clickname", "expanddetails", "pierceshadow", "retryaction", "capturebodies"]);
2049
- 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", "fetchurl", "parsejson", "parsehtml", "opensocket", "waitmessage", "watchrequests", "readheaders", "mapapi", "subscribesse", "longpoll", "extractapi", "readcookies"]);
2243
+ 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", "fetchurl", "parsejson", "parsehtml", "opensocket", "waitmessage", "watchrequests", "readheaders", "mapapi", "subscribesse", "longpoll", "extractapi", "readcookies", "watchconsole", "watcherrors", "watchtasks"]);
2050
2244
  var allowedactions = /* @__PURE__ */ new Set([...sensitiveactions, ...interactionactions, ...readactions]);
2051
2245
  var watchactions = /* @__PURE__ */ new Set(["watchmutate", "watchbanner", "watchfocus", "watchtab"]);
2052
2246
  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"]);
@@ -2062,6 +2256,7 @@ var httpactions = /* @__PURE__ */ new Set(["fetchurl", "parsejson", "parsehtml",
2062
2256
  var socketactions = /* @__PURE__ */ new Set(["opensocket", "sendmessage", "waitmessage", "subscribesse", "longpoll"]);
2063
2257
  var netwatchactions = /* @__PURE__ */ new Set(["watchrequests", "readheaders", "capturebodies", "mapapi", "extractapi"]);
2064
2258
  var controlactions = /* @__PURE__ */ new Set(["blockrequest", "mockresponse", "rewriteheaders", "setcookies", "readcookies", "clearcookies", "authflow", "saveapikey", "routeproxy", "postform", "postfiles"]);
2259
+ var debugactions = /* @__PURE__ */ new Set(["watchconsole", "watcherrors", "watchtasks"]);
2065
2260
  var credentialheaders = /* @__PURE__ */ new Set(["authorization", "proxy-authorization", "cookie", "cookie2", "set-cookie", "api-key", "x-api-key", "x-auth-token", "x-session-token", "proxy-authorization"]);
2066
2261
  var fieldkinds = ["text", "email", "phone", "date", "number", "select", "check", "radio", "file", "password", "card", "code"];
2067
2262
  var layoutmutationactions = /* @__PURE__ */ new Set(["grouptabs", "colorgroup", "collapsegroup", "savelayout", "restorelayout"]);
@@ -2077,6 +2272,9 @@ function hostpattern(origin) {
2077
2272
  if (parsed.protocol !== "https:") throw new Error("Only HTTPS origins can be granted.");
2078
2273
  return `${parsed.origin}/*`;
2079
2274
  }
2275
+ function isdebugkind(kind) {
2276
+ return debugactions.has(kind);
2277
+ }
2080
2278
  function actionrisk(kind) {
2081
2279
  if (!allowedactions.has(kind)) throw new Error("Unsupported browser action.");
2082
2280
  if (sensitiveactions.has(kind)) return "sensitive";
@@ -3302,6 +3500,81 @@ function ratelimitbudgetallowed(wait, budget) {
3302
3500
  if (wait !== void 0 && budget !== void 0 && wait > budget) return { allowed: false, reason: `The rate limit wait of ${wait} milliseconds exceeds the reviewed budget of ${budget} milliseconds; review a wider budget or submit later.` };
3303
3501
  return { allowed: true };
3304
3502
  }
3503
+ function timelinegate(session, tabid2, origin, now) {
3504
+ if (!session || session.stoppedat) return { allowed: false, reason: "No active browser session exists for the timeline capture." };
3505
+ if (session.expiresat <= now) return { allowed: false, reason: "The browser session has expired and cannot capture the timeline." };
3506
+ if (session.pausedat) return { allowed: false, reason: "The browser session is paused and cannot capture the timeline." };
3507
+ if (session.tabid !== tabid2) return { allowed: false, reason: `The timeline capture needs the run tab ${session.tabid} and refuses tab ${tabid2}.` };
3508
+ if (!origingranted(session, origin)) return { allowed: false, reason: `The timeline capture of ${origin} needs the session origin grants first.` };
3509
+ return { allowed: true };
3510
+ }
3511
+ function consoleconsentcovers(origin, consents) {
3512
+ if (consents.some((consent) => consent.origin === origin && consent.approved === true)) return { allowed: true };
3513
+ return { allowed: false, reason: `Console capture on ${origin} needs the reviewed console consent first; approve the prompt in the review panel.` };
3514
+ }
3515
+ function stackgate(session, origin) {
3516
+ if (!origingranted(session, origin)) return { allowed: false, reason: `Stack capture of ${origin} stays outside the session origin grants.` };
3517
+ return { allowed: true };
3518
+ }
3519
+ function debugwaitbudgetallowed(watchwindow, wait) {
3520
+ if (watchwindow !== void 0 && (typeof watchwindow !== "number" || !Number.isFinite(watchwindow) || watchwindow < 0)) return { allowed: false, reason: "The debug watch window must be zero or a positive number of milliseconds." };
3521
+ if (wait !== void 0 && (typeof wait !== "number" || !Number.isFinite(wait) || wait < 0)) return { allowed: false, reason: "The reviewed debug wait budget must be zero or a positive number of milliseconds." };
3522
+ if (watchwindow !== void 0 && wait !== void 0 && watchwindow > wait) return { allowed: false, reason: `The debug watch window of ${watchwindow} milliseconds exceeds the reviewed wait budget of ${wait} milliseconds; review a wider budget or a shorter window.` };
3523
+ return { allowed: true };
3524
+ }
3525
+ function validatetimelinegrammar(step, options) {
3526
+ const kind = step.kind;
3527
+ let watchwindow;
3528
+ if (options.watch !== void 0) {
3529
+ const watch = options.watch;
3530
+ if (!watch || typeof watch !== "object" || Array.isArray(watch)) return { allowed: false, reason: "The reviewed debug watch window must be an object." };
3531
+ const reviewed = watch;
3532
+ if (reviewed.window !== void 0) {
3533
+ if (typeof reviewed.window !== "number" || !Number.isFinite(reviewed.window) || reviewed.window < 0) return { allowed: false, reason: "The reviewed debug watch window must be zero or a positive number of milliseconds." };
3534
+ watchwindow = reviewed.window;
3535
+ }
3536
+ }
3537
+ const budgetcheck = debugwaitbudgetallowed(watchwindow, typeof options.wait === "number" ? options.wait : void 0);
3538
+ if (!budgetcheck.allowed) return budgetcheck;
3539
+ if (options.level !== void 0 && !loglevels.includes(options.level)) return { allowed: false, reason: `The reviewed level floor must be one of ${loglevels.join(", ")}.` };
3540
+ if (options.sources !== void 0) {
3541
+ if (!Array.isArray(options.sources) || options.sources.length === 0 || !options.sources.every((source) => timelinesources.includes(source))) return { allowed: false, reason: `The reviewed source filters must be a non-empty list of the reviewed timeline sources: ${timelinesources.join(", ")}.` };
3542
+ }
3543
+ if (kind === "watchconsole") {
3544
+ if (options.redact === void 0 || !Array.isArray(options.redact) || options.redact.length === 0 || !options.redact.every((pattern) => isnonempty(pattern))) return { allowed: false, reason: "Console capture requires a reviewed non-empty redaction pattern list before any console text is captured." };
3545
+ if (options.depth !== void 0 && (typeof options.depth !== "number" || !Number.isInteger(options.depth) || options.depth < 1)) return { allowed: false, reason: "The reviewed serialization depth bound must be a positive integer with no code ceiling." };
3546
+ if (options.spam !== void 0) {
3547
+ const rule = spamruleof(options.spam);
3548
+ if (!rule) return { allowed: false, reason: "The reviewed spam rule needs a pattern, a window size and a collapse threshold." };
3549
+ if (rule.collapse < 1) return { allowed: false, reason: "The reviewed spam collapse threshold must be a positive integer of user configured value with no code ceiling." };
3550
+ }
3551
+ if (options.rotation !== void 0) {
3552
+ const rule = rotationruleof(options.rotation);
3553
+ if (!rule) return { allowed: false, reason: "The reviewed rotation rule needs a max entry count and an overflow target." };
3554
+ }
3555
+ }
3556
+ if (kind === "watchtasks") {
3557
+ if (options.threshold !== void 0 && (typeof options.threshold !== "number" || !Number.isFinite(options.threshold) || options.threshold < 0)) return { allowed: false, reason: "The reviewed long task threshold must be zero or a positive number of milliseconds with no code ceiling." };
3558
+ }
3559
+ return { allowed: true };
3560
+ }
3561
+ function spamruleof(value) {
3562
+ if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
3563
+ const entry = value;
3564
+ const pattern = typeof entry.pattern === "string" ? entry.pattern : "";
3565
+ const windowsize = typeof entry.windowsize === "number" && Number.isFinite(entry.windowsize) && entry.windowsize >= 0 ? entry.windowsize : void 0;
3566
+ const collapse = typeof entry.collapse === "number" && Number.isInteger(entry.collapse) ? entry.collapse : void 0;
3567
+ if (windowsize === void 0 || collapse === void 0) return void 0;
3568
+ return { pattern, windowsize, collapse };
3569
+ }
3570
+ function rotationruleof(value) {
3571
+ if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
3572
+ const entry = value;
3573
+ const maxentries = typeof entry.maxentries === "number" && Number.isInteger(entry.maxentries) && entry.maxentries >= 1 ? entry.maxentries : void 0;
3574
+ const overflowtarget = typeof entry.overflowtarget === "string" && entry.overflowtarget.trim() ? entry.overflowtarget.trim() : void 0;
3575
+ if (maxentries === void 0 || overflowtarget === void 0) return void 0;
3576
+ return { maxentries, overflowtarget };
3577
+ }
3305
3578
  function controltarget(step) {
3306
3579
  let options = {};
3307
3580
  try {
@@ -3711,6 +3984,10 @@ function validatestep(step, origin) {
3711
3984
  const controlcheck = validatecontrolgrammar(step, options);
3712
3985
  if (!controlcheck.allowed) return controlcheck;
3713
3986
  }
3987
+ if (isdebugkind(step.kind)) {
3988
+ const timelinecheck = validatetimelinegrammar(step, options);
3989
+ if (!timelinecheck.allowed) return timelinecheck;
3990
+ }
3714
3991
  if (step.kind === "tabcreate") {
3715
3992
  if (options.background !== void 0 && typeof options.background !== "boolean") return { allowed: false, reason: "The reviewed background flag must be a boolean." };
3716
3993
  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." };
@@ -3812,6 +4089,10 @@ function canexecute(input) {
3812
4089
  const watchgatecheck = watchgate(input.session, input.settings, now);
3813
4090
  if (!watchgatecheck.allowed) return watchgatecheck;
3814
4091
  }
4092
+ if (isdebugkind(input.step.kind)) {
4093
+ const timelinegatecheck = timelinegate(input.session, input.tabid, input.origin, now);
4094
+ if (!timelinegatecheck.allowed) return timelinegatecheck;
4095
+ }
3815
4096
  if (iscontrolkind(input.step.kind)) {
3816
4097
  const controlgate = sessiongate({ session: input.session, tabid: input.tabid, origin: input.origin, now, action: "control the network" });
3817
4098
  if (!controlgate.allowed) return controlgate;
@@ -4040,9 +4321,14 @@ function recordupload(progress, planid, stepid, entry, now) {
4040
4321
  const outcome = { stepid, ok: true, summary: `The multipart upload moved chunk ${entry.chunk} of ${entry.chunks} with ${entry.uploaded} of ${entry.bytes} bytes sent.`, details: { upload: entry }, at: now };
4041
4322
  return recordoutcome(base, planid, outcome, now);
4042
4323
  }
4324
+ function recordtimeline(progress, planid, stepid, entry, now) {
4325
+ const base = progress && progress.planid === planid ? progress : emptyprogress(planid, now);
4326
+ const outcome = { stepid, ok: true, summary: `Captured ${entry.entries} timeline entr${entry.entries === 1 ? "y" : "ies"} with ${entry.collapsed} collapsed repeat${entry.collapsed === 1 ? "" : "s"}, ${entry.errors} error${entry.errors === 1 ? "" : "s"}, ${entry.rejections} rejection${entry.rejections === 1 ? "" : "s"} and ${entry.longtasks} long task${entry.longtasks === 1 ? "" : "s"}.`, details: { timeline: entry }, at: now };
4327
+ return recordoutcome(base, planid, outcome, now);
4328
+ }
4043
4329
 
4044
4330
  // version.ts
4045
- var packageversion = "1.1.44";
4331
+ var packageversion = "1.1.45";
4046
4332
 
4047
4333
  // types.ts
4048
4334
  var protocolversion = packageversion;
@@ -4100,6 +4386,23 @@ function parseproposal(value, origin, grants) {
4100
4386
  const bypass = proxy && typeof proxy === "object" && !Array.isArray(proxy) ? proxy.bypass : void 0;
4101
4387
  if (!Array.isArray(bypass) || bypass.length === 0) throw new Error("Proxy routes without a bypass list are refused.");
4102
4388
  }
4389
+ if (step.kind === "watchconsole" || step.kind === "watcherrors" || step.kind === "watchtasks") {
4390
+ let debugoptions = {};
4391
+ try {
4392
+ debugoptions = parseoptions(step);
4393
+ } catch {
4394
+ debugoptions = {};
4395
+ }
4396
+ const granted = covered.some((pattern) => {
4397
+ try {
4398
+ return new URL(origin).origin === new URL(pattern).origin;
4399
+ } catch {
4400
+ return false;
4401
+ }
4402
+ });
4403
+ if (!granted) throw new Error(`The ${step.kind} capture of ${origin} targets an origin outside the grants.`);
4404
+ if (debugoptions.level !== void 0 && !loglevels.includes(debugoptions.level)) throw new Error(`The reviewed level floor must be one of ${loglevels.join(", ")}.`);
4405
+ }
4103
4406
  const evaluation = validatestep(step, origin);
4104
4407
  if (!evaluation.allowed) throw new Error(evaluation.reason);
4105
4408
  const target = outboundtarget(step);
@@ -4174,7 +4477,7 @@ function requestbody(input) {
4174
4477
  return JSON.stringify({ version: protocolversion, objective: input.objective, session: input.session, observation: input.observation, capabilities: input.capabilities });
4175
4478
  }
4176
4479
  function outcomeresponse(input) {
4177
- 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 } : {}, ...input.transport ? { transport: input.transport } : {}, ...input.network ? { network: input.network } : {}, ...input.control ? { control: input.control } : {} });
4480
+ 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 } : {}, ...input.transport ? { transport: input.transport } : {}, ...input.network ? { network: input.network } : {}, ...input.control ? { control: input.control } : {}, ...input.timeline ? { timeline: input.timeline } : {} });
4178
4481
  }
4179
4482
  function mapresponse(input) {
4180
4483
  return JSON.stringify({ version: protocolversion, planid: input.plan.id, planstate: input.plan.state, map: input.map });
@@ -4270,6 +4573,12 @@ function controlreport(input) {
4270
4573
  });
4271
4574
  return { version: protocolversion, blocks: input.blocks, mocks, rewrites: input.rewrites, cookies: input.cookies, proxies: input.proxies, ratelimits: input.ratelimits };
4272
4575
  }
4576
+ function timelinereport(input) {
4577
+ return { version: protocolversion, entries: input.entries, errors: input.errors, rejections: input.rejections, longtasks: input.longtasks, levelcounts: input.levelcounts };
4578
+ }
4579
+ function consolediffreport(input) {
4580
+ return { version: protocolversion, diff: input.diff };
4581
+ }
4273
4582
 
4274
4583
  // capture.ts
4275
4584
  var capturekinds = ["shotview", "shotfullpage", "shotelement", "shotregion", "contactsheet"];
@@ -5958,7 +6267,7 @@ function stepoptions2(step) {
5958
6267
  }
5959
6268
  async function refreshcapabilities() {
5960
6269
  const report = await readcapabilities();
5961
- const withmedia = { ...report, captures: [...capturekinds], media: [...mediakinds], http: [...httpkinds], netwatch: [...socketkinds, ...netwatchkinds], control: [...controlkinds] };
6270
+ const withmedia = { ...report, captures: [...capturekinds], media: [...mediakinds], http: [...httpkinds], netwatch: [...socketkinds, ...netwatchkinds], control: [...controlkinds], debug: [...timelinekinds] };
5962
6271
  await memory.setcapabilities(withmedia);
5963
6272
  return withmedia;
5964
6273
  }
@@ -6381,6 +6690,17 @@ async function tracktabupdate(tabid2, changeinfo) {
6381
6690
  if (status === "loading" && url) {
6382
6691
  navbuffers.set(tabid2, [{ event: "beforenavigate", url, timestamp: now }]);
6383
6692
  lastknownurls.set(tabid2, url);
6693
+ for (const [id, watcher] of [...activetimelinewatchers.entries()]) {
6694
+ const detached = watcherdetached({ startedat: watcher.startedat, lifetime: watcher.lifetime, navigations: [now] });
6695
+ if (!detached.detached) continue;
6696
+ const session = await memory.getsession();
6697
+ if (!session || session.tabid !== tabid2) continue;
6698
+ watcher.cancelled = true;
6699
+ await memory.closewatch(id, now).catch(() => {
6700
+ });
6701
+ await audit("timeline", `Watcher ${id} detached when the run tab navigated to ${url} at ${detached.at}; every page hook of the destroyed context is gone with it.`, { sessionid: session.id });
6702
+ activetimelinewatchers.delete(id);
6703
+ }
6384
6704
  return;
6385
6705
  }
6386
6706
  const buffer = navbuffers.get(tabid2) ?? [];
@@ -8909,6 +9229,11 @@ async function executenetwatchstep(step, session, plan, tabid2, origin) {
8909
9229
  observed.push(exchange);
8910
9230
  }
8911
9231
  const failed = observed.filter((exchange) => exchange.errorclass !== void 0).length;
9232
+ for (const exchange of observed) {
9233
+ const failure = netfailureentryof({ id: randomid(), exchange, at: Date.now() });
9234
+ if (!failure) continue;
9235
+ await memory.addtimelineentry({ id: failure.id, runid: failure.runid, stepid: failure.stepid, time: failure.at, level: "error", source: "network", message: `Request ${failure.correlationid} of ${failure.url} failed with the ${failure.errorclass} class${failure.status > 0 ? ` at status ${failure.status}` : ""}.` });
9236
+ }
8912
9237
  await refreshbadge();
8913
9238
  await audit("watch", `Observed ${observed.length} request${observed.length === 1 ? "" : "s"} of the run tab over the reviewed ${window2} millisecond window, derived from the page timing buffers with ${failed} marked failed; headers and bodies stay out of this observation.`, extra);
8914
9239
  return { ok: true, summary: `Observed ${observed.length} request${observed.length === 1 ? "" : "s"} of the run tab${failed > 0 ? ` with ${failed} failed` : ""}.`, details: { network: { exchanges: observed.length, channelstate: "none", messages: 0 }, observed: observed.map((exchange) => ({ correlationid: exchange.correlationid, method: exchange.method, url: exchange.url, status: exchange.status, statusclass: exchange.statusclass, ...exchange.errorclass !== void 0 ? { errorclass: exchange.errorclass } : {}, bytes: exchange.bytes, duration: exchange.timing })), derivation: "The request lifecycle derives from the page performance and navigation buffers; the timing buffers expose no header names, body bytes or subresource status codes." } };
@@ -9026,6 +9351,115 @@ async function executenetwatchstep(step, session, plan, tabid2, origin) {
9026
9351
  void origin;
9027
9352
  throw new Error("Unsupported request observation kind.");
9028
9353
  }
9354
+ var activetimelinewatchers = /* @__PURE__ */ new Map();
9355
+ function timelinedetail(entry, runid) {
9356
+ if (!entry || typeof entry !== "object") return null;
9357
+ const record2 = entry;
9358
+ if (typeof record2.stepid !== "string" || typeof record2.time !== "number" || typeof record2.message !== "string") return null;
9359
+ if (typeof record2.level !== "string" || !loglevels.includes(record2.level)) return null;
9360
+ if (typeof record2.source !== "string" || !timelinesources.includes(record2.source)) return null;
9361
+ return { id: randomid(), runid, stepid: record2.stepid, time: record2.time, level: record2.level, source: record2.source, message: record2.message };
9362
+ }
9363
+ async function executetimelinestep(step, session, plan, tabid2, origin) {
9364
+ const options = stepoptions2(step);
9365
+ const watch = options.watch && typeof options.watch === "object" && !Array.isArray(options.watch) ? options.watch : {};
9366
+ const watchwindow = typeof watch.window === "number" && Number.isFinite(watch.window) && watch.window >= 0 ? watch.window : 0;
9367
+ const extra = { sessionid: session.id, planid: plan.id, stepid: step.id };
9368
+ if (step.kind === "watchconsole") {
9369
+ const consents = await memory.getconsoleconsents();
9370
+ const consent = consoleconsentcovers(origin, consents);
9371
+ if (!consent.allowed) {
9372
+ const record2 = { id: randomid(), prompt: `Console capture on ${origin} for the reviewed ${watchwindow} millisecond window of step ${step.id}.`, origin, stepid: step.id, at: Date.now() };
9373
+ await memory.setconsoleconsent(record2);
9374
+ await refreshbadge();
9375
+ throw new Error(`${consent.reason} The prompt is open in the review panel; approve it and run the step again.`);
9376
+ }
9377
+ }
9378
+ if (step.kind === "watcherrors") {
9379
+ const stackgatecheck = stackgate(session, origin);
9380
+ if (!stackgatecheck.allowed) throw new Error(stackgatecheck.reason ?? "Stack capture stays outside the session origin grants.");
9381
+ }
9382
+ const startedat = Date.now();
9383
+ const bound = attachtimeline({ runid: plan.id, origin, stepids: plan.steps.map((item) => item.id), now: startedat });
9384
+ const watchid = randomid();
9385
+ const registration = { watchid, kind: step.kind, stepid: step.id, sessionid: session.id, origin, scopes: [], events: [], startedat, lifetime: watchwindow };
9386
+ await memory.addwatch(registration);
9387
+ await audit("timeline", `Watcher ${step.kind} attached under id ${watchid} on ${origin} for the reviewed window of ${watchwindow} milliseconds inside the run tab ${tabid2}.`, extra);
9388
+ activetimelinewatchers.set(watchid, { runid: plan.id, startedat, lifetime: watchwindow, cancelled: false });
9389
+ let output;
9390
+ try {
9391
+ output = await dispatchpagestep(step, tabid2, origin, plan) ?? { ok: false, summary: "The watch returned no result." };
9392
+ } catch (error) {
9393
+ activetimelinewatchers.delete(watchid);
9394
+ await memory.closewatch(watchid, Date.now());
9395
+ await audit("timeline", `Watcher ${watchid} detached when the run tab navigated or closed inside the reviewed window of ${watchwindow} milliseconds.`, extra);
9396
+ return { ok: false, summary: `The ${step.kind} watcher detached when the tab navigated or closed inside the reviewed window; run it again on the settled page (${error instanceof Error ? error.message : String(error)}).` };
9397
+ }
9398
+ const watcherstate = activetimelinewatchers.get(watchid);
9399
+ activetimelinewatchers.delete(watchid);
9400
+ await memory.closewatch(watchid, Date.now());
9401
+ await audit("timeline", `Watcher ${watchid} detached cleanly after its reviewed window of ${watchwindow} milliseconds.`, extra);
9402
+ if (watcherstate?.cancelled) {
9403
+ await audit("timeline", `Watcher ${watchid} cancelled on run cancel or the killswitch; the captured window is discarded.`, extra);
9404
+ return { ok: false, summary: `The ${step.kind} watcher was cancelled with the run; the captured window is discarded.` };
9405
+ }
9406
+ const captured = detailarray(output.details, "entries").map((entry) => timelinedetail(entry, plan.id)).filter((entry) => entry !== null);
9407
+ bound.entries.push(...captured);
9408
+ let collapsed = 0;
9409
+ let flagged = [];
9410
+ let stored = bound.entries;
9411
+ if (step.kind === "watchconsole") {
9412
+ const rule = spamruleof(options.spam);
9413
+ if (rule) {
9414
+ const outcome = spamdetect(bound.entries, rule);
9415
+ stored = outcome.entries;
9416
+ collapsed = bound.entries.length - stored.length;
9417
+ flagged = outcome.flagged;
9418
+ }
9419
+ }
9420
+ const rotation = rotationruleof(options.rotation);
9421
+ let rotationtarget;
9422
+ if (rotation) {
9423
+ const rotated = rotatelogs(stored, rotation);
9424
+ stored = rotated.kept;
9425
+ rotationtarget = { target: rotation.overflowtarget, runid: plan.id, entries: rotated.overflow.length, at: Date.now() };
9426
+ }
9427
+ const errorids = [];
9428
+ const rejectionids = [];
9429
+ const longtasks = [];
9430
+ for (const entry of detailarray(output.details, "errors")) {
9431
+ if (!entry || typeof entry !== "object") continue;
9432
+ const record2 = entry;
9433
+ const id = randomid();
9434
+ errorids.push(id);
9435
+ const capturederror = { id, runid: plan.id, stepid: step.id, message: typeof record2.message === "string" ? record2.message : "", frames: Array.isArray(record2.frames) ? record2.frames : [], sourceurl: typeof record2.sourceurl === "string" ? record2.sourceurl : "", line: typeof record2.line === "number" ? record2.line : 0, at: Date.now() };
9436
+ await memory.adderrorrecord(capturederror);
9437
+ }
9438
+ for (const entry of detailarray(output.details, "rejections")) {
9439
+ if (!entry || typeof entry !== "object") continue;
9440
+ const record2 = entry;
9441
+ const id = randomid();
9442
+ rejectionids.push(id);
9443
+ const capturedrejection = { id, runid: plan.id, stepid: step.id, reason: typeof record2.reason === "string" ? record2.reason : "", frames: Array.isArray(record2.frames) ? record2.frames : [], at: Date.now() };
9444
+ await memory.addrejectionrecord(capturedrejection);
9445
+ }
9446
+ for (const entry of detailarray(output.details, "longtasks")) {
9447
+ if (!entry || typeof entry !== "object") continue;
9448
+ const record2 = entry;
9449
+ const capturedtask = { id: randomid(), runid: plan.id, stepid: step.id, duration: typeof record2.duration === "number" ? record2.duration : 0, starttime: typeof record2.starttime === "number" ? record2.starttime : 0, attributions: Array.isArray(record2.attributions) ? record2.attributions.filter((name) => typeof name === "string") : [], at: Date.now() };
9450
+ longtasks.push(capturedtask);
9451
+ await memory.addlongtask(capturedtask);
9452
+ }
9453
+ for (const entry of stored) await memory.addtimelineentry(entry);
9454
+ if (rotationtarget) await memory.addrotationtarget(rotationtarget);
9455
+ const blocking = blockingduration(longtasks, step.id, { startedat, endedat: Date.now() });
9456
+ const evidence = { entries: stored.length, collapsed, errors: errorids.length, rejections: rejectionids.length, longtasks: longtasks.length };
9457
+ await memory.setprogress(recordtimeline(await memory.getprogress(), plan.id, step.id, evidence, Date.now()));
9458
+ await refreshbadge();
9459
+ const counts = timelinecounts(stored);
9460
+ await audit("timeline", `Captured ${stored.length} timeline entr${stored.length === 1 ? "y" : "ies"} of ${origin} for the reviewed window of ${watchwindow} milliseconds${collapsed > 0 ? ` with ${collapsed} collapsed repeat${collapsed === 1 ? "" : "s"}` : ""}${errorids.length > 0 ? `, ${errorids.length} error${errorids.length === 1 ? "" : "s"}` : ""}${rejectionids.length > 0 ? `, ${rejectionids.length} rejection${rejectionids.length === 1 ? "" : "s"}` : ""}${longtasks.length > 0 ? ` and ${longtasks.length} long task${longtasks.length === 1 ? "" : "s"}` : ""}; console, error and task watching derives from page-injected listeners and the performance buffers.`, extra);
9461
+ return { ok: Boolean(output.ok), summary: output.summary, details: { ...output.details ?? {}, timeline: { entries: stored.length, levels: counts, collapsed }, entries: stored, errorids, rejectionids, longtasks: longtasks.map((task) => ({ ...task, blocking: blocking.blocking })), ...flagged.length > 0 ? { flagged } : {}, ...rotationtarget ? { rotation: rotationtarget } : {} } };
9462
+ }
9029
9463
  var activerules = /* @__PURE__ */ new Map();
9030
9464
  var activeauthflows = /* @__PURE__ */ new Map();
9031
9465
  function rulesetof(runid) {
@@ -9415,11 +9849,12 @@ async function refreshbadge() {
9415
9849
  const media = (await memory.getmediarecords()).length + (await memory.getimagebatches()).length;
9416
9850
  const recordingprompts = (await memory.getrecordingconsents()).filter((record2) => record2.approved === void 0).length;
9417
9851
  const fetchprompts = (await memory.getfetchconsents()).filter((consent) => consent.approved === void 0).length;
9852
+ const consoleprompts = (await memory.getconsoleconsents()).filter((consent) => consent.approved === void 0).length;
9418
9853
  const observedrequests = (await memory.getexchanges()).length;
9419
9854
  const livechannels = (await memory.getchannels()).filter((channel) => channel.state === "open" || channel.state === "connecting").length;
9420
9855
  const activerulescount = [...activerules.values()].reduce((total2, ruleset) => total2 + ruleset.blocks.filter((rule) => rule.revertedat === void 0).length + ruleset.mocks.filter((rule) => rule.revertedat === void 0).length + ruleset.rewrites.filter((rule) => rule.revertedat === void 0).length + (ruleset.proxy !== void 0 && ruleset.proxy.revertedat === void 0 ? 1 : 0), 0);
9421
9856
  const tasktabs2 = new Set(badges.map((badge) => badge.tabid)).size;
9422
- const total = (queues?.prefetch ?? 0) + (queues?.batchopen ?? 0) + tasktabs2 + prompts + consents + quarantined + datasets + captures + media + recordingprompts + fetchprompts + observedrequests + livechannels + activerulescount;
9857
+ const total = (queues?.prefetch ?? 0) + (queues?.batchopen ?? 0) + tasktabs2 + prompts + consents + quarantined + datasets + captures + media + recordingprompts + fetchprompts + consoleprompts + observedrequests + livechannels + activerulescount;
9423
9858
  await chrome.action.setBadgeText({ text: total > 0 ? String(total) : "" }).catch(() => {
9424
9859
  });
9425
9860
  }
@@ -9487,6 +9922,9 @@ async function executestep(stepid) {
9487
9922
  output = await executenetwatchstep(step, session, plan, tab.id, origin);
9488
9923
  } else if (iscontrolkind(step.kind)) {
9489
9924
  output = await executenetcontrolstep(step, session, plan, tab.id, origin);
9925
+ } else if (isdebugkind(step.kind)) {
9926
+ if (!session || !plan || plan.state !== "approved") throw new Error("Debugging kinds refuse to run outside an approved session plan.");
9927
+ output = await executetimelinestep(step, session, plan, tab.id, origin);
9490
9928
  } else {
9491
9929
  if (step.target && freshcheckkinds.has(step.kind)) {
9492
9930
  const fresh = await snapshot(tab.id);
@@ -9706,6 +10144,8 @@ async function handlerequest(message, sender) {
9706
10144
  const apikeys = (await memory.getapikeys()).map((ref) => ({ name: ref.name, origins: ref.origins, header: ref.header, createdat: ref.createdat, ...ref.lastuse !== void 0 ? { lastuse: ref.lastuse } : {} }));
9707
10145
  const traffic = controlreport({ blocks: await memory.getblockrules(), mocks: await memory.getmockspecs(), rewrites: await memory.getheaderules(), cookies: await memory.getcookieops(), proxies: await memory.getproxyroutes(), ratelimits: await memory.getratelimits(Date.now()) });
9708
10146
  const tokens = authreport({ tokens: await memory.listtokens() });
10147
+ const timelineentries = await memory.gettimeline();
10148
+ const timeline = timelinereport({ entries: timelineentries, errors: await memory.geterrorrecords(), rejections: await memory.getrejectionrecords(), longtasks: await memory.getlongtasks(), levelcounts: timelinecounts(timelineentries) });
9709
10149
  const authflows = [...activeauthflows.values()].map((active) => ({ provider: active.flow.provider, redirectorigin: active.flow.redirectorigin, scopes: active.flow.scopes, stepid: active.stepid, tabid: active.tabid ?? 0, stage: active.cancelled ? "cancelled" : "consent" }));
9710
10150
  const runsettings = await memory.getsettings();
9711
10151
  const scanhooks = [];
@@ -9718,7 +10158,7 @@ async function handlerequest(message, sender) {
9718
10158
  const livetab = session ? await chrome.tabs.get(session.tabid).catch(() => void 0) : void 0;
9719
10159
  const waitprofile = session ? waitprofiles.find((record2) => record2.origin === session.origin) : void 0;
9720
10160
  const livestate = { phase: livetab?.status === "loading" ? "loading" : "complete", ...navrecords[0] ? { finalurl: navrecords[0].finalurl, redirects: navrecords[0].chain } : {} };
9721
- 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, calls, endpoints, fetchconsents, apikeys, callretention: runsettings?.callretention, fetchesactive: activefetches.size, exchanges, channels, subscriptions, apimap, messages: messagecount, webrequestgrant: runsettings?.webrequestgrant === true, bodyretention: runsettings?.bodyretention, socketsactive: activesockets.size, traffic, tokens, authflows, activerules: [...activerules.values()].reduce((total, ruleset) => total + ruleset.blocks.filter((rule) => rule.revertedat === void 0).length + ruleset.mocks.filter((rule) => rule.revertedat === void 0).length + ruleset.rewrites.filter((rule) => rule.revertedat === void 0).length + (ruleset.proxy !== void 0 && ruleset.proxy.revertedat === void 0 ? 1 : 0), 0), ...stitchprogress.size > 0 ? { stitchprogress: [...stitchprogress.values()] } : {} };
10161
+ 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, calls, endpoints, fetchconsents, apikeys, callretention: runsettings?.callretention, fetchesactive: activefetches.size, exchanges, channels, subscriptions, apimap, messages: messagecount, webrequestgrant: runsettings?.webrequestgrant === true, bodyretention: runsettings?.bodyretention, timelineretention: runsettings?.timelineretention, timeline, consoleconsents: await memory.getconsoleconsents(), rotationtargets: await memory.getrotationtargets(), levelsummaries: await memory.getlevelsummaries(), socketsactive: activesockets.size, traffic, tokens, authflows, activerules: [...activerules.values()].reduce((total, ruleset) => total + ruleset.blocks.filter((rule) => rule.revertedat === void 0).length + ruleset.mocks.filter((rule) => rule.revertedat === void 0).length + ruleset.rewrites.filter((rule) => rule.revertedat === void 0).length + (ruleset.proxy !== void 0 && ruleset.proxy.revertedat === void 0 ? 1 : 0), 0), ...stitchprogress.size > 0 ? { stitchprogress: [...stitchprogress.values()] } : {} };
9722
10162
  }
9723
10163
  case "capabilities":
9724
10164
  return refreshcapabilities();
@@ -9763,7 +10203,8 @@ async function handlerequest(message, sender) {
9763
10203
  const capture = outcome.details?.capture;
9764
10204
  const media = outcome.details?.media;
9765
10205
  const network = outcome.details?.network;
9766
- return JSON.parse(outcomeresponse({ outcome, plan, ...resolved ? { resolvedtarget: resolved } : {}, ...capture ? { capture } : {}, ...media ? { media } : {}, ...network ? { network } : {} }));
10206
+ const timeline = outcome.details?.timeline;
10207
+ return JSON.parse(outcomeresponse({ outcome, plan, ...resolved ? { resolvedtarget: resolved } : {}, ...capture ? { capture } : {}, ...media ? { media } : {}, ...network ? { network } : {}, ...timeline ? { timeline } : {} }));
9767
10208
  }
9768
10209
  case "map": {
9769
10210
  const plan = await memory.getplan();
@@ -10374,6 +10815,39 @@ async function handlerequest(message, sender) {
10374
10815
  await audit("configure", `The user set the captured body retention to ${retention === void 0 ? "keep every body" : retention} record${retention === 1 ? "" : "s"}; the exchange metadata always survives.`);
10375
10816
  return { bodyretention: retention };
10376
10817
  }
10818
+ case "settimelineretention": {
10819
+ const inputretention = message;
10820
+ const settings = await memory.getsettings();
10821
+ const retention = typeof inputretention.retention === "number" && Number.isInteger(inputretention.retention) && inputretention.retention >= 0 ? inputretention.retention : void 0;
10822
+ await memory.setsettings({ ...settings, ...retention !== void 0 ? { timelineretention: retention } : {} });
10823
+ await audit("configure", `The user set the timeline retention to ${retention === void 0 ? "keep every entry" : retention} entr${retention === 1 ? "y" : "ies"}; the level count summaries always survive.`);
10824
+ return { timelineretention: retention };
10825
+ }
10826
+ case "approveconsoleconsent": {
10827
+ const inputapprove = message;
10828
+ const records = await memory.getconsoleconsents();
10829
+ const record2 = records.find((item) => item.id === inputapprove.id);
10830
+ if (!record2) throw new Error("No console capture prompt matches the id.");
10831
+ const decided = { ...record2, approved: true, usedat: Date.now() };
10832
+ await memory.setconsoleconsent(decided);
10833
+ await audit("consent", `Console capture on ${record2.origin} approved from the review panel; the decision persists for that origin.`, { stepid: record2.stepid });
10834
+ await refreshbadge();
10835
+ return { approved: true, origin: record2.origin };
10836
+ }
10837
+ case "consolediff": {
10838
+ const inputdiff = message;
10839
+ const base = inputdiff.base?.trim();
10840
+ const target = inputdiff.target?.trim();
10841
+ if (!base || !target) throw new Error("Console diffing needs the two reviewed run ids.");
10842
+ if (base === target) throw new Error("Console diffing needs two different run ids.");
10843
+ const baselines = (await memory.listtimeline({ runid: base })).filter((entry) => entry.source === "console").map((entry) => entry.message);
10844
+ const targetlines = (await memory.listtimeline({ runid: target })).filter((entry) => entry.source === "console").map((entry) => entry.message);
10845
+ if (baselines.length === 0 && targetlines.length === 0) throw new Error("Neither run stored console output yet; run watchconsole on both runs first.");
10846
+ const diff = consolediff({ baseid: base, targetid: target, baselines, targetlines, now: Date.now() });
10847
+ await memory.addconsolediff(diff);
10848
+ await audit("diff", `Diffed the console output of runs ${base} and ${target}: ${diff.added} added, ${diff.removed} removed and ${diff.repeated} repeated line${diff.added + diff.removed + diff.repeated === 1 ? "" : "s"}.`);
10849
+ return consolediffreport({ diff });
10850
+ }
10377
10851
  case "trafficreport": {
10378
10852
  const plan = await memory.getplan();
10379
10853
  if (!plan) throw new Error("No plan is available for a traffic control envelope.");
@@ -10464,6 +10938,13 @@ async function handlerequest(message, sender) {
10464
10938
  controller.abort();
10465
10939
  activefetches.delete(id);
10466
10940
  }
10941
+ for (const [id, watcher] of [...activetimelinewatchers.entries()]) {
10942
+ watcher.cancelled = true;
10943
+ await memory.closewatch(id, Date.now()).catch(() => {
10944
+ });
10945
+ await audit("timeline", `Watcher ${id} cancelled on run cancel or the killswitch; the captured window is discarded.`, { ...session ? { sessionid: session.id } : {}, ...watcher.runid ? { planid: watcher.runid } : {} });
10946
+ activetimelinewatchers.delete(id);
10947
+ }
10467
10948
  const stoppedplan = await memory.getplan();
10468
10949
  if (stoppedplan) {
10469
10950
  await closechannelsforrun(stoppedplan.id).catch(() => {