@wenathlan/extension 1.1.48 → 1.1.50

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.
@@ -374,6 +374,212 @@ function locationconsentcovers(origin, latitude, longitude, consents) {
374
374
  return consents.some((consent) => consent.origin === origin && consent.approved === true && consent.revokedat === void 0 && consent.latitude === latitude && consent.longitude === longitude);
375
375
  }
376
376
 
377
+ // sessions.ts
378
+ var sessionkinds = ["persiststate", "capturesession", "restoresession", "namedsessions", "diffsessions", "searchsessions", "exportsessions", "importsessions"];
379
+ var sessionfileversion = 1;
380
+ var snapshotsections = ["tabs", "scroll", "forms", "storage", "cookies"];
381
+ var searchfields = ["urls", "titles", "names", "text"];
382
+ function checksumtext(payload) {
383
+ let hash = 2166136261;
384
+ for (let index = 0; index < payload.length; index += 1) {
385
+ hash ^= payload.charCodeAt(index);
386
+ hash = Math.imul(hash, 16777619) >>> 0;
387
+ }
388
+ return hash.toString(16).padStart(8, "0");
389
+ }
390
+ function taskstatechecksum(runid, stepcursor, outputs) {
391
+ return checksumtext(`${runid}:${stepcursor}:${outputs.length}:${outputs.map((output) => `${output.stepid}:${output.ok}:${output.summary.length}`).join("|")}`);
392
+ }
393
+ function taskstateof(input) {
394
+ return { runid: input.runid, stepcursor: input.stepcursor, outputs: input.outputs, checkpointat: input.checkpointat, checksum: taskstatechecksum(input.runid, input.stepcursor, input.outputs) };
395
+ }
396
+ function taskstatevalid(state) {
397
+ if (!state || typeof state.runid !== "string" || !state.runid.trim()) return false;
398
+ if (typeof state.stepcursor !== "number" || !Number.isInteger(state.stepcursor) || state.stepcursor < 0) return false;
399
+ if (typeof state.checkpointat !== "number" || !Number.isFinite(state.checkpointat)) return false;
400
+ if (!Array.isArray(state.outputs)) return false;
401
+ return state.checksum === taskstatechecksum(state.runid, state.stepcursor, state.outputs);
402
+ }
403
+ function sessiontabof(value) {
404
+ if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
405
+ const candidate = value;
406
+ if (typeof candidate.url !== "string" || !candidate.url.trim()) return void 0;
407
+ if (typeof candidate.title !== "string") return void 0;
408
+ if (typeof candidate.index !== "number" || !Number.isInteger(candidate.index) || candidate.index < 0) return void 0;
409
+ const scrollx = typeof candidate.scrollx === "number" && Number.isFinite(candidate.scrollx) ? candidate.scrollx : 0;
410
+ const scrolly = typeof candidate.scrolly === "number" && Number.isFinite(candidate.scrolly) ? candidate.scrolly : 0;
411
+ const forms = Array.isArray(candidate.forms) ? candidate.forms.flatMap((entry) => {
412
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) return [];
413
+ const form = entry;
414
+ if (typeof form.selector !== "string" || !form.selector.trim()) return [];
415
+ return [{ selector: form.selector, value: typeof form.value === "string" ? form.value : "" }];
416
+ }) : [];
417
+ return { url: candidate.url, title: candidate.title, index: candidate.index, scrollx, scrolly, forms };
418
+ }
419
+ function autointervalof(value) {
420
+ if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
421
+ const candidate = value;
422
+ if (typeof candidate.period !== "number" || !Number.isFinite(candidate.period) || candidate.period <= 0) return void 0;
423
+ if (typeof candidate.maxsnapshots !== "number" || !Number.isInteger(candidate.maxsnapshots) || candidate.maxsnapshots < 1) return void 0;
424
+ if (typeof candidate.expiry !== "number" || !Number.isFinite(candidate.expiry) || candidate.expiry < 0) return void 0;
425
+ return { period: candidate.period, maxsnapshots: candidate.maxsnapshots, expiry: candidate.expiry };
426
+ }
427
+ function snapshotplanof(value) {
428
+ if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
429
+ const candidate = value;
430
+ if (candidate.scope !== "tab" && candidate.scope !== "run" && candidate.scope !== "all") return void 0;
431
+ const sections = Array.isArray(candidate.sections) ? candidate.sections.flatMap((section) => typeof section === "string" && snapshotsections.includes(section) ? [section] : []) : [];
432
+ if (sections.length === 0) return void 0;
433
+ if (typeof candidate.captures !== "boolean") return void 0;
434
+ const auto = candidate.auto === void 0 ? void 0 : autointervalof(candidate.auto);
435
+ if (candidate.auto !== void 0 && auto === void 0) return void 0;
436
+ return { scope: candidate.scope, sections, captures: candidate.captures, ...auto !== void 0 ? { auto } : {} };
437
+ }
438
+ function restoreplanof(value) {
439
+ if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
440
+ const candidate = value;
441
+ if (candidate.tabpolicy !== "reopen" && candidate.tabpolicy !== "skip") return void 0;
442
+ if (candidate.formpolicy !== "restore" && candidate.formpolicy !== "skip") return void 0;
443
+ if (candidate.capturepolicy !== "link" && candidate.capturepolicy !== "skip") return void 0;
444
+ return { tabpolicy: candidate.tabpolicy, formpolicy: candidate.formpolicy, capturepolicy: candidate.capturepolicy };
445
+ }
446
+ function searchqueryof(value) {
447
+ if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
448
+ const candidate = value;
449
+ const terms = Array.isArray(candidate.terms) ? candidate.terms.flatMap((term) => typeof term === "string" && term.trim() ? [term.trim()] : []) : [];
450
+ if (terms.length === 0) return void 0;
451
+ const fields = Array.isArray(candidate.fields) ? candidate.fields.flatMap((field) => typeof field === "string" && searchfields.includes(field) ? [field] : []) : [...searchfields];
452
+ if (fields.length === 0) return void 0;
453
+ const from = typeof candidate.from === "number" && Number.isFinite(candidate.from) ? candidate.from : void 0;
454
+ const to = typeof candidate.to === "number" && Number.isFinite(candidate.to) ? candidate.to : void 0;
455
+ if (from !== void 0 && to !== void 0 && from > to) return void 0;
456
+ return { terms, fields, ...from !== void 0 ? { from } : {}, ...to !== void 0 ? { to } : {} };
457
+ }
458
+ function sessionfolderof(value) {
459
+ if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
460
+ const candidate = value;
461
+ if (typeof candidate.name !== "string" || !candidate.name.trim()) return void 0;
462
+ const parent = typeof candidate.parent === "string" && candidate.parent.trim() ? candidate.parent : void 0;
463
+ const tags = Array.isArray(candidate.tags) ? candidate.tags.flatMap((tag) => typeof tag === "string" && tag.trim() ? [tag] : []) : [];
464
+ return { name: candidate.name, ...parent !== void 0 ? { parent } : {}, tags };
465
+ }
466
+ function newsessionrecord(input) {
467
+ return { id: input.id, name: input.name, createdat: input.createdat, tabs: input.tabs, captures: input.captures, storage: input.storage, cookies: input.cookies, ...input.folder !== void 0 ? { folder: input.folder } : {}, tags: input.tags ?? [], ...input.auto === true ? { auto: true } : {} };
468
+ }
469
+ function diffsessionrecords(left, right) {
470
+ const changes = [];
471
+ const leftbyindex = new Map(left.tabs.map((tab) => [tab.index, tab]));
472
+ const rightbyindex = new Map(right.tabs.map((tab) => [tab.index, tab]));
473
+ for (const tab of right.tabs) {
474
+ const prior = leftbyindex.get(tab.index);
475
+ if (!prior) {
476
+ changes.push({ class: "added", subject: "tab", detail: `Tab ${tab.index} added: ${tab.url}` });
477
+ continue;
478
+ }
479
+ if (prior.url !== tab.url) changes.push({ class: "changed", subject: "url", detail: `Tab ${tab.index} moved from ${prior.url} to ${tab.url}` });
480
+ if (prior.title !== tab.title) changes.push({ class: "changed", subject: "tab", detail: `Tab ${tab.index} title changed from "${prior.title}" to "${tab.title}"` });
481
+ const priorforms = new Map(prior.forms.map((form) => [form.selector, form.value]));
482
+ for (const form of tab.forms) {
483
+ const before = priorforms.get(form.selector);
484
+ if (before === void 0) {
485
+ changes.push({ class: "added", subject: "form", detail: `Form field ${form.selector} of tab ${tab.index} added with a value` });
486
+ continue;
487
+ }
488
+ if (before !== form.value) changes.push({ class: "changed", subject: "form", detail: `Form field ${form.selector} of tab ${tab.index} changed its captured value` });
489
+ }
490
+ for (const form of prior.forms) if (!tab.forms.some((entry) => entry.selector === form.selector)) changes.push({ class: "removed", subject: "form", detail: `Form field ${form.selector} of tab ${tab.index} removed` });
491
+ }
492
+ for (const tab of left.tabs) if (!rightbyindex.has(tab.index)) changes.push({ class: "removed", subject: "tab", detail: `Tab ${tab.index} removed: ${tab.url}` });
493
+ const leftstorage = new Map(left.storage.map((entry) => [entry.origin, entry]));
494
+ for (const entry of right.storage) {
495
+ const prior = leftstorage.get(entry.origin);
496
+ if (!prior) {
497
+ changes.push({ class: "added", subject: "storage", detail: `Local storage of ${entry.origin} captured with ${entry.keys.length} keys` });
498
+ continue;
499
+ }
500
+ if (prior.keys.join("|") !== entry.keys.join("|") || prior.values.join("|") !== entry.values.join("|")) changes.push({ class: "changed", subject: "storage", detail: `Local storage of ${entry.origin} changed its captured keys or values` });
501
+ }
502
+ for (const entry of left.storage) if (!right.storage.some((candidate) => candidate.origin === entry.origin)) changes.push({ class: "removed", subject: "storage", detail: `Local storage of ${entry.origin} left the capture` });
503
+ return changes;
504
+ }
505
+ function newsessiondiff(input) {
506
+ return { id: input.id, leftid: input.left.id, rightid: input.right.id, changes: diffsessionrecords(input.left, input.right), at: input.at };
507
+ }
508
+ function searchsessionrecords(query, records) {
509
+ const matches = [];
510
+ for (const record2 of records) {
511
+ if (query.from !== void 0 && record2.createdat < query.from) continue;
512
+ if (query.to !== void 0 && record2.createdat > query.to) continue;
513
+ const haystacks = [
514
+ { field: "urls", text: record2.tabs.map((tab) => tab.url).join(" ") },
515
+ { field: "titles", text: record2.tabs.map((tab) => tab.title).join(" ") },
516
+ { field: "names", text: [record2.name, record2.folder ?? "", ...record2.tags].join(" ") },
517
+ { field: "text", text: record2.tabs.flatMap((tab) => tab.forms.map((form) => form.value)).join(" ") }
518
+ ];
519
+ for (const haystack of haystacks) {
520
+ if (!query.fields.includes(haystack.field)) continue;
521
+ const lower = haystack.text.toLowerCase();
522
+ for (const term of query.terms) {
523
+ const at = lower.indexOf(term.toLowerCase());
524
+ if (at < 0) continue;
525
+ const start = Math.max(0, at - 30);
526
+ matches.push({ sessionid: record2.id, field: haystack.field, term, at: record2.createdat, excerpt: haystack.text.slice(start, start + 80).trim() });
527
+ }
528
+ }
529
+ }
530
+ return matches;
531
+ }
532
+ function exportsessionfile(records, now) {
533
+ const recordids = records.map((record2) => record2.id);
534
+ const payload = JSON.stringify(records);
535
+ return { formatversion: sessionfileversion, records, recordids, bytesize: payload.length, checksum: checksumtext(`${sessionfileversion}:${recordids.join(",")}:${payload.length}`), exportedat: now };
536
+ }
537
+ function importsessionfile(value) {
538
+ if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
539
+ const candidate = value;
540
+ if (candidate.formatversion !== sessionfileversion) return void 0;
541
+ const records = Array.isArray(candidate.records) ? candidate.records.flatMap((record2) => sessionrecordvalid(record2) ? [record2] : []) : [];
542
+ if (records.length === 0) return void 0;
543
+ if (!Array.isArray(candidate.recordids) || candidate.recordids.length !== records.length || !candidate.recordids.every((id, index) => id === records[index]?.id)) return void 0;
544
+ const bytesize = typeof candidate.bytesize === "number" && Number.isFinite(candidate.bytesize) ? candidate.bytesize : -1;
545
+ if (bytesize < 0) return void 0;
546
+ const checksum2 = typeof candidate.checksum === "string" ? candidate.checksum : "";
547
+ if (checksum2 !== checksumtext(`${sessionfileversion}:${candidate.recordids.join(",")}:${bytesize}`)) return void 0;
548
+ return { formatversion: sessionfileversion, records, recordids: candidate.recordids, bytesize, checksum: checksum2, exportedat: typeof candidate.exportedat === "number" && Number.isFinite(candidate.exportedat) ? candidate.exportedat : 0 };
549
+ }
550
+ function sessionrecordvalid(value) {
551
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
552
+ const candidate = value;
553
+ if (typeof candidate.id !== "string" || !candidate.id.trim()) return false;
554
+ if (typeof candidate.name !== "string" || !candidate.name.trim()) return false;
555
+ if (typeof candidate.createdat !== "number" || !Number.isFinite(candidate.createdat)) return false;
556
+ if (!Array.isArray(candidate.tabs) || !candidate.tabs.every((tab) => sessiontabof(tab) !== void 0)) return false;
557
+ if (!Array.isArray(candidate.captures) || !candidate.captures.every((id) => typeof id === "string")) return false;
558
+ if (!Array.isArray(candidate.tags) || !candidate.tags.every((tag) => typeof tag === "string")) return false;
559
+ return true;
560
+ }
561
+ function expiresessions(records, retention, now) {
562
+ if (retention === void 0 || !Number.isFinite(retention)) return records;
563
+ return records.map((record2) => {
564
+ if (record2.sectionsexpired || now - record2.createdat < retention) return record2;
565
+ return { id: record2.id, name: record2.name, createdat: record2.createdat, tabs: [], captures: record2.captures, storage: [], cookies: [], ...record2.folder !== void 0 ? { folder: record2.folder } : {}, tags: record2.tags, ...record2.auto === true ? { auto: true } : {}, ...record2.restoredat !== void 0 ? { restoredat: record2.restoredat } : {}, sectionsexpired: true };
566
+ });
567
+ }
568
+ function filteredsessions(records, filter) {
569
+ return records.filter((record2) => {
570
+ if (filter.name !== void 0 && !record2.name.toLowerCase().includes(filter.name.toLowerCase())) return false;
571
+ if (filter.folder !== void 0 && record2.folder !== filter.folder) return false;
572
+ if (filter.from !== void 0 && record2.createdat < filter.from) return false;
573
+ if (filter.to !== void 0 && record2.createdat > filter.to) return false;
574
+ return true;
575
+ });
576
+ }
577
+ function crashinterrupted(state, plansteps, now) {
578
+ if (!state) return void 0;
579
+ if (state.stepcursor >= plansteps) return state;
580
+ return { ...state, interrupted: true, crashat: state.crashat ?? now };
581
+ }
582
+
377
583
  // memory.ts
378
584
  var sessionmemory = class {
379
585
  constructor(adapter) {
@@ -1821,6 +2027,170 @@ var sessionmemory = class {
1821
2027
  async getlocationconsents() {
1822
2028
  return await this.adapter.get("locationconsents") ?? [];
1823
2029
  }
2030
+ /** Returns the persisted task state checkpoint of one run so the run resumes after a service worker restart. */
2031
+ async gettaskstate(runid) {
2032
+ return this.adapter.get(`taskstate${runid}`);
2033
+ }
2034
+ /** Persists one task state checkpoint per run with its corruption checksum. */
2035
+ async settaskstate(state) {
2036
+ return this.adapter.set(`taskstate${state.runid}`, state);
2037
+ }
2038
+ /** Returns the session event history with timestamps, newest first. */
2039
+ async getsessionevents() {
2040
+ return await this.adapter.get("sessionevents") ?? [];
2041
+ }
2042
+ /** Records one session event of the run with its timestamp and detail. */
2043
+ async addsessionevent(event) {
2044
+ const records = await this.getsessionevents();
2045
+ await this.adapter.set("sessionevents", [event, ...records]);
2046
+ }
2047
+ /** Returns every saved session record with its sections, newest first. */
2048
+ async getsessionrecords() {
2049
+ return await this.adapter.get("sessionrecords") ?? [];
2050
+ }
2051
+ /** Adds one saved session record to the library. */
2052
+ async addsessionrecord(record2) {
2053
+ const records = await this.getsessionrecords();
2054
+ await this.adapter.set("sessionrecords", [record2, ...records]);
2055
+ }
2056
+ /** Replaces one saved session record by its id after a filing or restore touches it. */
2057
+ async updatesessionrecord(record2) {
2058
+ const records = await this.getsessionrecords();
2059
+ await this.adapter.set("sessionrecords", records.map((item) => item.id === record2.id ? record2 : item));
2060
+ }
2061
+ /** Lists saved sessions filtered by name substring, folder and time window; the filter stays a user choice with no result cap. */
2062
+ async listsessions(filter) {
2063
+ return filteredsessions(await this.getsessionrecords(), filter);
2064
+ }
2065
+ /** Returns one saved session with every section; an expired record carries its metadata only. */
2066
+ async getsessionrecord(id) {
2067
+ return (await this.getsessionrecords()).find((record2) => record2.id === id);
2068
+ }
2069
+ /** Runs the reviewed search query across every stored session and returns the matches with their session ids and time windows. */
2070
+ async searchmemory(query) {
2071
+ return searchsessionrecords(query, await this.getsessionrecords());
2072
+ }
2073
+ /** Returns the folder tree of the session library. */
2074
+ async getsessionfolders() {
2075
+ return await this.adapter.get("sessionfolders") ?? [];
2076
+ }
2077
+ /** Replaces the folder tree after a reviewed filing adds or moves one folder. */
2078
+ async setsessionfolders(folders) {
2079
+ return this.adapter.set("sessionfolders", folders);
2080
+ }
2081
+ /** Returns every stored session diff result, newest first. */
2082
+ async getsessiondiffs() {
2083
+ return await this.adapter.get("sessiondiffs") ?? [];
2084
+ }
2085
+ /** Stores one session diff result for later review. */
2086
+ async addsessiondiff(diff) {
2087
+ const records = await this.getsessiondiffs();
2088
+ await this.adapter.set("sessiondiffs", [diff, ...records]);
2089
+ }
2090
+ /** Returns the persisted auto snapshot state with the reviewed interval, the last snapshot time and the snapshot count. */
2091
+ async getautosnapshot() {
2092
+ return await this.adapter.get("autosnapshot") ?? void 0;
2093
+ }
2094
+ /** Stores the auto snapshot state of the reviewed interval. */
2095
+ async setautosnapshot(state) {
2096
+ return this.adapter.set("autosnapshot", state);
2097
+ }
2098
+ /** Clears the auto snapshot interval so on demand captures stay the only source of records. */
2099
+ async clearautosnapshot() {
2100
+ return this.adapter.set("autosnapshot", null);
2101
+ }
2102
+ /** Expires the heavy sections of saved sessions after the reviewed retention window while the record metadata survives. */
2103
+ async applysessionexpiry(retention, now) {
2104
+ const records = expiresessions(await this.getsessionrecords(), retention, now);
2105
+ await this.adapter.set("sessionrecords", records);
2106
+ return records;
2107
+ }
2108
+ /** Returns the crash marker of a run interrupted by a browser restart. */
2109
+ async getcrashflag() {
2110
+ return await this.adapter.get("crashed") ?? false;
2111
+ }
2112
+ /** Sets the crash marker so the sessions view offers the crash restore inside the consent model. */
2113
+ async setcrashflag(value) {
2114
+ return this.adapter.set("crashed", value);
2115
+ }
2116
+ /** Stores one composed workflow record version with its timestamp; re-composing the same version replaces it while older versions survive for the audit trail. */
2117
+ async addworkflowrecord(record2) {
2118
+ const records = await this.getworkflowrecordversions();
2119
+ const remaining = records.filter((entry) => !(entry.id === record2.id && entry.version === record2.version));
2120
+ await this.adapter.set("workflowrecords", [record2, ...remaining]);
2121
+ }
2122
+ /** Returns every stored workflow record version, newest first. */
2123
+ async getworkflowrecordversions() {
2124
+ return await this.adapter.get("workflowrecords") ?? [];
2125
+ }
2126
+ /** Returns the latest stored version of one workflow record. */
2127
+ async getworkflowrecord(id) {
2128
+ return (await this.getworkflowrecordversions()).find((entry) => entry.id === id);
2129
+ }
2130
+ /** Lists the saved workflow records, the latest version of each, newest first. */
2131
+ async listworkflows() {
2132
+ const seen = /* @__PURE__ */ new Set();
2133
+ const latest = [];
2134
+ for (const entry of await this.getworkflowrecordversions()) {
2135
+ if (seen.has(entry.id)) continue;
2136
+ seen.add(entry.id);
2137
+ latest.push(entry);
2138
+ }
2139
+ return latest;
2140
+ }
2141
+ /** Stores one workflow run with its state transition; a run replace keeps the full runlog of the same id. */
2142
+ async setworkflowrun(run) {
2143
+ const runs = await this.listworkflowruns();
2144
+ const remaining = runs.filter((entry) => entry.id !== run.id);
2145
+ await this.adapter.set("workflowruns", [run, ...remaining]);
2146
+ }
2147
+ /** Returns every stored workflow run, newest first. */
2148
+ async listworkflowruns() {
2149
+ return await this.adapter.get("workflowruns") ?? [];
2150
+ }
2151
+ /** Returns one run with its full step outcome list so the panel shows the timeline after and during a run. */
2152
+ async getrun(id) {
2153
+ const run = (await this.listworkflowruns()).find((entry) => entry.id === id);
2154
+ if (!run) return void 0;
2155
+ return { run, log: await this.getrunlog(id) };
2156
+ }
2157
+ /** Records one runlog entry of a run; the runlog retention window is a user setting and an absent window keeps every entry. */
2158
+ async addrunlogentry(runid, entry) {
2159
+ const entries = await this.getrunlog(runid);
2160
+ const combined = [...entries, entry];
2161
+ const retention = (await this.getsettings())?.runlogretention;
2162
+ await this.adapter.set(`runlog${runid}`, retention === void 0 ? combined : combined.slice(-retention));
2163
+ }
2164
+ /** Returns the runlog of one run, oldest first. */
2165
+ async getrunlog(runid) {
2166
+ return await this.adapter.get(`runlog${runid}`) ?? [];
2167
+ }
2168
+ /** Stores the variable values per scope of one run for inspection after the run. */
2169
+ async setrunscopes(runid, scopes) {
2170
+ return this.adapter.set(`runscopes${runid}`, scopes);
2171
+ }
2172
+ /** Returns the variable scopes of one run, oldest first. */
2173
+ async getrunscopes(runid) {
2174
+ return await this.adapter.get(`runscopes${runid}`) ?? [];
2175
+ }
2176
+ /** Records one provenance entry of a run: an expression result or a regex capture with its name, value and time. */
2177
+ async addworkflowprovenance(runid, entry) {
2178
+ const entries = await this.getworkflowprovenance(runid);
2179
+ await this.adapter.set(`workflowprovenance${runid}`, [...entries, entry]);
2180
+ }
2181
+ /** Returns every provenance entry of one run, oldest first. */
2182
+ async getworkflowprovenance(runid) {
2183
+ return await this.adapter.get(`workflowprovenance${runid}`) ?? [];
2184
+ }
2185
+ /** Stores one shareable step template under its unique name. */
2186
+ async addsteptemplate(template) {
2187
+ const templates = (await this.getsteptemplates()).filter((entry) => entry.name !== template.name);
2188
+ await this.adapter.set("steptemplates", [template, ...templates]);
2189
+ }
2190
+ /** Returns every stored step template, newest first. */
2191
+ async getsteptemplates() {
2192
+ return await this.adapter.get("steptemplates") ?? [];
2193
+ }
1824
2194
  };
1825
2195
  function mediakindof(record2) {
1826
2196
  if ("pages" in record2) return "pdf";
@@ -2807,6 +3177,511 @@ function teardowncdpsession(input) {
2807
3177
  };
2808
3178
  }
2809
3179
 
3180
+ // workflow.ts
3181
+ var workflowkinds = ["composeworkflow", "savetemplate", "runworkflow", "dryrun", "delay", "waitelement", "compute", "extractvars"];
3182
+ function workflowstepof(value) {
3183
+ if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
3184
+ const candidate = value;
3185
+ if (typeof candidate.id !== "string" || !candidate.id.trim()) return void 0;
3186
+ if (typeof candidate.kind !== "string" || !/^[a-z]+$/.test(candidate.kind)) return void 0;
3187
+ if (typeof candidate.label !== "string" || !candidate.label.trim()) return void 0;
3188
+ if (candidate.target !== void 0 && (typeof candidate.target !== "string" || !candidate.target)) return void 0;
3189
+ if (candidate.value !== void 0 && typeof candidate.value !== "string") return void 0;
3190
+ if (candidate.options !== void 0 && typeof candidate.options !== "string") return void 0;
3191
+ const bindings = Array.isArray(candidate.bindings) ? candidate.bindings.flatMap((binding) => bindingof(binding) !== void 0 ? [bindingof(binding)] : []) : void 0;
3192
+ if (candidate.bindings !== void 0 && bindings === void 0) return void 0;
3193
+ if (Array.isArray(candidate.bindings) && bindings !== void 0 && bindings.length !== candidate.bindings.length) return void 0;
3194
+ const expression = candidate.expression === void 0 ? void 0 : expressionof(candidate.expression);
3195
+ if (candidate.expression !== void 0 && expression === void 0) return void 0;
3196
+ const extract = candidate.extract === void 0 ? void 0 : regexruleof(candidate.extract);
3197
+ if (candidate.extract !== void 0 && extract === void 0) return void 0;
3198
+ return { id: candidate.id, kind: candidate.kind, label: candidate.label, ...candidate.target !== void 0 ? { target: candidate.target } : {}, ...candidate.value !== void 0 ? { value: candidate.value } : {}, ...candidate.options !== void 0 ? { options: candidate.options } : {}, ...bindings !== void 0 && bindings.length > 0 ? { bindings } : {}, ...expression !== void 0 ? { expression } : {}, ...extract !== void 0 ? { extract } : {} };
3199
+ }
3200
+ function blockinvocationof(value) {
3201
+ if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
3202
+ const candidate = value;
3203
+ if (typeof candidate.block !== "string" || !candidate.block.trim()) return void 0;
3204
+ if (typeof candidate.label !== "string" || !candidate.label.trim()) return void 0;
3205
+ return { block: candidate.block, label: candidate.label };
3206
+ }
3207
+ function workflowblockof(value) {
3208
+ if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
3209
+ const candidate = value;
3210
+ if (typeof candidate.name !== "string" || !/^[a-z][a-z0-9]*$/.test(candidate.name)) return void 0;
3211
+ if (typeof candidate.label !== "string" || !candidate.label.trim()) return void 0;
3212
+ if (!Array.isArray(candidate.steps)) return void 0;
3213
+ const steps = [];
3214
+ for (const entry of candidate.steps) {
3215
+ const step = workflowstepof(entry);
3216
+ if (step) {
3217
+ steps.push(step);
3218
+ continue;
3219
+ }
3220
+ const invocation = blockinvocationof(entry);
3221
+ if (invocation) {
3222
+ steps.push(invocation);
3223
+ continue;
3224
+ }
3225
+ return void 0;
3226
+ }
3227
+ return { name: candidate.name, label: candidate.label, steps };
3228
+ }
3229
+ function steptemplateof(value) {
3230
+ if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
3231
+ const candidate = value;
3232
+ if (typeof candidate.id !== "string" || !candidate.id.trim()) return void 0;
3233
+ if (typeof candidate.name !== "string" || !candidate.name.trim()) return void 0;
3234
+ if (typeof candidate.origin !== "string" || !candidate.origin.trim()) return void 0;
3235
+ const step = workflowstepof(candidate.step);
3236
+ if (!step) return void 0;
3237
+ if (typeof candidate.sharedat !== "number" || !Number.isFinite(candidate.sharedat)) return void 0;
3238
+ return { id: candidate.id, name: candidate.name, origin: candidate.origin, step, sharedat: candidate.sharedat };
3239
+ }
3240
+ function bindingof(value) {
3241
+ if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
3242
+ const candidate = value;
3243
+ if (typeof candidate.variable !== "string" || !/^[a-z][a-z0-9]*$/.test(candidate.variable)) return void 0;
3244
+ if (!variablekinds.includes(candidate.kind)) return void 0;
3245
+ if (typeof candidate.stepid !== "string" || !candidate.stepid.trim()) return void 0;
3246
+ if (candidate.path !== void 0 && (typeof candidate.path !== "string" || !candidate.path.trim())) return void 0;
3247
+ return { variable: candidate.variable, kind: candidate.kind, stepid: candidate.stepid, ...candidate.path !== void 0 ? { path: candidate.path } : {} };
3248
+ }
3249
+ var variablekinds = ["string", "number", "boolean", "list", "element"];
3250
+ var expressionoperators = ["add", "subtract", "multiply", "divide", "modulo", "equal", "notequal", "less", "greater", "lessequal", "greaterequal", "and", "or", "not", "concat", "contains", "length"];
3251
+ function expressionof(value) {
3252
+ if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
3253
+ const candidate = value;
3254
+ const left = operandof(candidate.left);
3255
+ if (!left) return void 0;
3256
+ const right = candidate.right === void 0 ? void 0 : operandof(candidate.right);
3257
+ if (candidate.right !== void 0 && right === void 0) return void 0;
3258
+ if (typeof candidate.operator !== "string" || !expressionoperators.includes(candidate.operator)) return void 0;
3259
+ if (typeof candidate.result !== "string" || !/^[a-z][a-z0-9]*$/.test(candidate.result)) return void 0;
3260
+ if (!variablekinds.includes(candidate.resultkind)) return void 0;
3261
+ return { left, ...right !== void 0 ? { right } : {}, operator: candidate.operator, result: candidate.result, resultkind: candidate.resultkind };
3262
+ }
3263
+ function operandof(value) {
3264
+ if (value === void 0) return void 0;
3265
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") return { literal: value };
3266
+ if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
3267
+ const candidate = value;
3268
+ if (typeof candidate.ref === "string" && /^[a-z][a-z0-9]*$/.test(candidate.ref)) return { ref: candidate.ref };
3269
+ if (typeof candidate.literal === "string" || typeof candidate.literal === "number" || typeof candidate.literal === "boolean") return { literal: candidate.literal };
3270
+ return void 0;
3271
+ }
3272
+ function regexruleof(value) {
3273
+ if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
3274
+ const candidate = value;
3275
+ if (typeof candidate.pattern !== "string" || !candidate.pattern.trim()) return void 0;
3276
+ if (typeof candidate.flags !== "string" || !/^[dgimsuvy]*$/.test(candidate.flags)) return void 0;
3277
+ const groups = Array.isArray(candidate.groups) ? candidate.groups.flatMap((group) => typeof group === "string" && /^[a-z][a-z0-9]*$/.test(group) ? [group] : []) : [];
3278
+ if (candidate.groups !== void 0 && groups.length !== candidate.groups.length) return void 0;
3279
+ return { pattern: candidate.pattern, flags: candidate.flags, groups };
3280
+ }
3281
+ function expandblocks(steps, blocks) {
3282
+ const byname = new Map(blocks.map((block) => [block.name, block]));
3283
+ const expanded = [];
3284
+ const visit = (entries, path, inside) => {
3285
+ for (const entry of entries) {
3286
+ if ("kind" in entry && "label" in entry && !("block" in entry)) {
3287
+ expanded.push(inside === void 0 ? entry : { ...entry, block: inside });
3288
+ continue;
3289
+ }
3290
+ const invocation = blockinvocationof(entry);
3291
+ if (!invocation) throw new Error("The step list entry is neither a reviewed step nor a block invocation.");
3292
+ if (path.includes(invocation.block)) throw new Error(`The block ${invocation.block} recurs inside itself and cannot expand.`);
3293
+ const block = byname.get(invocation.block);
3294
+ if (!block) throw new Error(`The block ${invocation.block} is not defined in the workflow.`);
3295
+ visit(block.steps, [...path, invocation.block], invocation.block);
3296
+ }
3297
+ };
3298
+ visit(steps, [], void 0);
3299
+ if (expanded.length === 0) throw new Error("A workflow needs at least one executable step after block expansion.");
3300
+ return expanded;
3301
+ }
3302
+ function composeworkflow(input) {
3303
+ if (typeof input.name !== "string" || !input.name.trim()) throw new Error("The workflow name must be a non-empty string.");
3304
+ if (typeof input.version !== "number" || !Number.isInteger(input.version) || input.version < 1) throw new Error("The workflow version must be a positive integer.");
3305
+ if (!Array.isArray(input.origins) || input.origins.length === 0) throw new Error("A workflow needs at least one granted HTTPS origin.");
3306
+ const origins = input.origins.map((origin) => {
3307
+ try {
3308
+ return new URL(origin).origin;
3309
+ } catch {
3310
+ throw new Error(`The workflow origin ${origin} is not a valid url.`);
3311
+ }
3312
+ });
3313
+ if (origins.some((origin) => !origin.startsWith("https://"))) throw new Error("Workflow origins must use HTTPS.");
3314
+ const blocks = input.blocks ?? [];
3315
+ if (blocks.some((block, index) => blocks.findIndex((other) => other.name === block.name) !== index)) throw new Error("Workflow block names must stay unique.");
3316
+ for (const entry of input.steps) {
3317
+ if ("kind" in entry && "label" in entry && !("block" in entry)) {
3318
+ if (input.kindallowed && !input.kindallowed(entry.kind)) throw new Error(`The workflow step kind ${entry.kind} is not a reviewed action kind.`);
3319
+ }
3320
+ }
3321
+ for (const block of blocks) for (const entry of block.steps) {
3322
+ if ("kind" in entry && "label" in entry && !("block" in entry) && input.kindallowed && !input.kindallowed(entry.kind)) throw new Error(`The workflow step kind ${entry.kind} inside block ${block.name} is not a reviewed action kind.`);
3323
+ }
3324
+ const steps = expandblocks(input.steps, blocks);
3325
+ for (const step of steps) {
3326
+ if (input.kindallowed && !input.kindallowed(step.kind)) throw new Error(`The workflow step kind ${step.kind} is not a reviewed action kind.`);
3327
+ if (step.bindings) for (const binding of step.bindings) {
3328
+ if (!steps.some((other) => other.id === binding.stepid)) throw new Error(`The binding of ${binding.variable} references the unknown step ${binding.stepid}.`);
3329
+ }
3330
+ }
3331
+ const riskof = input.riskof ?? (() => "sensitive");
3332
+ const risk = steps.some((step) => riskof(step.kind) === "sensitive") ? "sensitive" : steps.some((step) => riskof(step.kind) === "interaction") ? "interaction" : "read";
3333
+ const record2 = { id: input.id ?? crypto.randomUUID(), name: input.name, version: input.version, origins: [...new Set(origins)], steps, blocks, risk, createdat: input.now };
3334
+ return deepfreeze(record2);
3335
+ }
3336
+ function deepfreeze(record2) {
3337
+ for (const step of record2.steps) Object.freeze(step);
3338
+ for (const block of record2.blocks) for (const entry of block.steps) if ("kind" in entry && "label" in entry && !("block" in entry)) Object.freeze(entry);
3339
+ Object.freeze(record2.blocks);
3340
+ Object.freeze(record2.steps);
3341
+ return Object.freeze(record2);
3342
+ }
3343
+ function validateworkflow(record2, options) {
3344
+ if (record2.steps.length === 0) return { allowed: false, reason: "A workflow needs at least one reviewed step." };
3345
+ const defined = new Set(options?.inputs ?? []);
3346
+ const byid = new Map(record2.steps.map((step, index) => [step.id, { step, index }]));
3347
+ for (let index = 0; index < record2.steps.length; index += 1) {
3348
+ const step = record2.steps[index];
3349
+ if (options?.kindallowed && !options.kindallowed(step.kind)) return { allowed: false, reason: `The workflow step kind ${step.kind} is not a reviewed action kind.` };
3350
+ if (step.bindings) for (const binding of step.bindings) {
3351
+ const source = byid.get(binding.stepid);
3352
+ if (!source) return { allowed: false, reason: `The binding of ${binding.variable} references the unknown step ${binding.stepid}.` };
3353
+ if (source.index >= index) return { allowed: false, reason: `The binding of ${binding.variable} must link an earlier step than ${step.id}.` };
3354
+ defined.add(binding.variable);
3355
+ }
3356
+ if (step.expression) {
3357
+ for (const operand of [step.expression.left, step.expression.right]) {
3358
+ if (operand?.ref && !defined.has(operand.ref)) return { allowed: false, reason: `The expression of step ${step.id} references the undefined variable ${operand.ref}.` };
3359
+ }
3360
+ defined.add(step.expression.result);
3361
+ }
3362
+ if (step.extract) for (const group of step.extract.groups) defined.add(group);
3363
+ }
3364
+ return { allowed: true };
3365
+ }
3366
+ function pushscope(scopes, name, parent) {
3367
+ return [...scopes, { name, variables: [], ...parent !== void 0 ? { parent } : {} }];
3368
+ }
3369
+ function popscope(scopes) {
3370
+ if (scopes.length === 0) return scopes;
3371
+ return scopes.slice(0, -1);
3372
+ }
3373
+ function resolvevariable(scopes, name) {
3374
+ for (let index = scopes.length - 1; index >= 0; index -= 1) {
3375
+ const scope = scopes[index];
3376
+ const found = scope.variables.find((variable) => variable.name === name);
3377
+ if (found) return found;
3378
+ if (scope.parent === void 0) continue;
3379
+ const parentindex = scopes.findIndex((candidate) => candidate.name === scope.parent);
3380
+ if (parentindex >= 0 && parentindex < index) {
3381
+ const inherited = resolvevariable([scopes[parentindex]], name);
3382
+ if (inherited) return inherited;
3383
+ }
3384
+ }
3385
+ return void 0;
3386
+ }
3387
+ function setvariable(scopes, name, kind, value, now) {
3388
+ if (scopes.length === 0) scopes = [{ name: "root", variables: [] }];
3389
+ const target = scopes[scopes.length - 1];
3390
+ const variables = [...target.variables.filter((variable) => variable.name !== name), { name, kind, value, setat: now }];
3391
+ return [...scopes.slice(0, -1), { ...target, variables }];
3392
+ }
3393
+ function coercevariable(value, kind) {
3394
+ if (kind === "number") {
3395
+ const parsed = typeof value === "number" ? value : typeof value === "string" && value.trim() !== "" ? Number(value) : NaN;
3396
+ if (!Number.isFinite(parsed)) throw new Error("The bound value is not a finite number.");
3397
+ return parsed;
3398
+ }
3399
+ if (kind === "boolean") {
3400
+ if (typeof value === "boolean") return value;
3401
+ if (value === "true") return true;
3402
+ if (value === "false") return false;
3403
+ throw new Error("The bound value is not a boolean.");
3404
+ }
3405
+ if (kind === "list") {
3406
+ if (Array.isArray(value)) return value.map((item) => String(item));
3407
+ if (typeof value === "string") return value.length === 0 ? [] : value.split(",");
3408
+ throw new Error("The bound value is not a list.");
3409
+ }
3410
+ if (kind === "element") {
3411
+ if (typeof value === "string" && value.trim()) return value;
3412
+ throw new Error("The bound value is not an element reference.");
3413
+ }
3414
+ if (typeof value === "string") return value;
3415
+ if (typeof value === "number" || typeof value === "boolean") return String(value);
3416
+ throw new Error("The bound value is not a string.");
3417
+ }
3418
+ function outcomedetail(outcome, path) {
3419
+ if (!path) return outcome.summary;
3420
+ let current = outcome.details ?? {};
3421
+ for (const segment of path.split(".")) {
3422
+ if (!current || typeof current !== "object" || Array.isArray(current)) return void 0;
3423
+ current = current[segment];
3424
+ }
3425
+ return current;
3426
+ }
3427
+ function bindvariables(scopes, bindings, outputs, now) {
3428
+ let current = scopes;
3429
+ const produced = [];
3430
+ for (const binding of bindings) {
3431
+ const outcome = outputs[binding.stepid];
3432
+ if (!outcome) continue;
3433
+ const raw = outcomedetail(outcome, binding.path);
3434
+ if (raw === void 0) throw new Error(`The binding of ${binding.variable} found no value at ${binding.path ?? "the summary"} of step ${binding.stepid}.`);
3435
+ current = setvariable(current, binding.variable, binding.kind, coercevariable(raw, binding.kind), now);
3436
+ produced.push(binding.variable);
3437
+ }
3438
+ return { scopes: current, produced };
3439
+ }
3440
+ function operandvalue(operand, scopes) {
3441
+ if (operand.ref !== void 0) {
3442
+ const resolved = resolvevariable(scopes, operand.ref);
3443
+ if (!resolved) throw new Error(`The expression references the undefined variable ${operand.ref}.`);
3444
+ return resolved.value;
3445
+ }
3446
+ if (operand.literal === void 0) throw new Error("The expression operand needs a variable reference or a literal.");
3447
+ return operand.literal;
3448
+ }
3449
+ function expressioneval(expression, scopes) {
3450
+ const left = operandvalue(expression.left, scopes);
3451
+ const right = expression.right === void 0 ? void 0 : operandvalue(expression.right, scopes);
3452
+ const operand = (value) => {
3453
+ if (Array.isArray(value)) throw new Error("The expression operand is a list and needs the contains or length operator.");
3454
+ if (value === void 0) throw new Error("The expression operand is missing.");
3455
+ return value;
3456
+ };
3457
+ const numbervalue = (value) => {
3458
+ const primitive = operand(value);
3459
+ if (typeof primitive === "number") return primitive;
3460
+ if (typeof primitive === "string" && primitive.trim() !== "") {
3461
+ const parsed = Number(primitive);
3462
+ if (Number.isFinite(parsed)) return parsed;
3463
+ }
3464
+ throw new Error("The arithmetic operand is not a number.");
3465
+ };
3466
+ const booleanvalue = (value) => {
3467
+ const primitive = operand(value);
3468
+ if (typeof primitive === "boolean") return primitive;
3469
+ throw new Error("The logic operand is not a boolean.");
3470
+ };
3471
+ const stringvalue = (value) => {
3472
+ const primitive = operand(value);
3473
+ if (typeof primitive === "string") return primitive;
3474
+ if (typeof primitive === "number" || typeof primitive === "boolean") return String(primitive);
3475
+ throw new Error("The text operand is not a string.");
3476
+ };
3477
+ switch (expression.operator) {
3478
+ case "add":
3479
+ return numbervalue(left) + numbervalue(right);
3480
+ case "subtract":
3481
+ return numbervalue(left) - numbervalue(right);
3482
+ case "multiply":
3483
+ return numbervalue(left) * numbervalue(right);
3484
+ case "divide": {
3485
+ const divisor = numbervalue(right);
3486
+ if (divisor === 0) throw new Error("The expression divides by zero.");
3487
+ return numbervalue(left) / divisor;
3488
+ }
3489
+ case "modulo": {
3490
+ const divisor = numbervalue(right);
3491
+ if (divisor === 0) throw new Error("The expression divides by zero.");
3492
+ return numbervalue(left) % divisor;
3493
+ }
3494
+ case "equal":
3495
+ return left === right;
3496
+ case "notequal":
3497
+ return left !== right;
3498
+ case "less":
3499
+ return numbervalue(left) < numbervalue(right);
3500
+ case "greater":
3501
+ return numbervalue(left) > numbervalue(right);
3502
+ case "lessequal":
3503
+ return numbervalue(left) <= numbervalue(right);
3504
+ case "greaterequal":
3505
+ return numbervalue(left) >= numbervalue(right);
3506
+ case "and":
3507
+ return booleanvalue(left) && booleanvalue(right);
3508
+ case "or":
3509
+ return booleanvalue(left) || booleanvalue(right);
3510
+ case "not":
3511
+ return !booleanvalue(left);
3512
+ case "concat":
3513
+ return `${stringvalue(left)}${stringvalue(right)}`;
3514
+ case "contains": {
3515
+ if (Array.isArray(left)) return left.includes(stringvalue(right));
3516
+ return stringvalue(left).includes(stringvalue(right));
3517
+ }
3518
+ case "length": {
3519
+ if (Array.isArray(left)) return left.length;
3520
+ return stringvalue(left).length;
3521
+ }
3522
+ default:
3523
+ throw new Error("The reviewed expression operator is unknown.");
3524
+ }
3525
+ }
3526
+ function regexextract(rule, text2, now) {
3527
+ const pattern = new RegExp(rule.pattern, rule.flags);
3528
+ const match = pattern.exec(text2);
3529
+ if (!match) return { matched: false, variables: [] };
3530
+ const variables = [];
3531
+ for (const group of rule.groups) {
3532
+ const value = match.groups?.[group];
3533
+ variables.push({ name: group, kind: "string", value: typeof value === "string" ? value : "", setat: now });
3534
+ }
3535
+ return { matched: true, variables };
3536
+ }
3537
+ function waitelementplan(wait) {
3538
+ if (wait.timeout <= 0 || wait.poll <= 0) return { probes: 1, lastwait: 0 };
3539
+ const probes = Math.floor(wait.timeout / wait.poll) + 1;
3540
+ return { probes, lastwait: wait.timeout % wait.poll };
3541
+ }
3542
+ function delayjitter(delay, seed) {
3543
+ if (delay.jitter <= 0) return Math.max(0, delay.base);
3544
+ const sample = seededrandom(seed);
3545
+ return Math.max(0, delay.base - delay.jitter / 2 + sample * delay.jitter);
3546
+ }
3547
+ function seededrandom(seed) {
3548
+ let state = seed >>> 0;
3549
+ state ^= state >>> 16;
3550
+ state = Math.imul(state, 2246822507);
3551
+ state ^= state >>> 13;
3552
+ state = Math.imul(state, 3266489909);
3553
+ state ^= state >>> 16;
3554
+ state = state >>> 0 || 1;
3555
+ state ^= state << 13;
3556
+ state >>>= 0;
3557
+ state ^= state >> 17;
3558
+ state ^= state << 5;
3559
+ state >>>= 0;
3560
+ return state / 4294967296;
3561
+ }
3562
+ function newworkflowrun(input) {
3563
+ return { id: input.id ?? crypto.randomUUID(), workflowid: input.workflowid, state: "pending", cursor: 0, startedat: input.now, ...input.dryrun === true ? { dryrun: true } : {} };
3564
+ }
3565
+ function pauserun(run, now) {
3566
+ if (run.state !== "running") throw new Error("Only a running workflow can pause.");
3567
+ return { ...run, state: "paused", pausedat: now };
3568
+ }
3569
+ function cancelrun(run, reason, now) {
3570
+ if (run.state === "done" || run.state === "cancelled") return run;
3571
+ return { ...run, state: "cancelled", cancelreason: reason, endedat: now };
3572
+ }
3573
+ function interpolate(text2, scopes) {
3574
+ const consumed = [];
3575
+ const resolved = text2.replace(/\$\{([a-z][a-z0-9]*)\}/g, (_whole, name) => {
3576
+ const variable = resolvevariable(scopes, name);
3577
+ if (!variable) throw new Error(`The step references the undefined variable ${name}.`);
3578
+ consumed.push(name);
3579
+ return Array.isArray(variable.value) ? variable.value.join(",") : String(variable.value);
3580
+ });
3581
+ return { text: resolved, consumed };
3582
+ }
3583
+ function runlogof(step, state, startedat, duration, summary, extra) {
3584
+ return { stepid: step.id, label: step.label, state, startedat, duration, summary, ...extra.block !== void 0 ? { block: extra.block } : {}, ...extra.consumed !== void 0 && extra.consumed.length > 0 ? { consumed: extra.consumed } : {}, ...extra.produced !== void 0 && extra.produced.length > 0 ? { produced: extra.produced } : {}, ...extra.checkpoint === true ? { checkpoint: true } : {}, ...extra.details !== void 0 ? { details: extra.details } : {} };
3585
+ }
3586
+ async function runstep(input) {
3587
+ const startedat = input.now;
3588
+ let scopes = input.scopes;
3589
+ const consumed = [];
3590
+ if (input.step.bindings) {
3591
+ const bound = bindvariables(scopes, input.step.bindings.filter((binding) => input.outputs[binding.stepid] !== void 0), input.outputs, input.now);
3592
+ scopes = bound.scopes;
3593
+ }
3594
+ let produced = [];
3595
+ try {
3596
+ if (input.step.expression) {
3597
+ const value2 = expressioneval(input.step.expression, scopes);
3598
+ scopes = setvariable(scopes, input.step.expression.result, input.step.expression.resultkind, coercevariable(value2, input.step.expression.resultkind), input.now);
3599
+ produced = [...produced, input.step.expression.result];
3600
+ }
3601
+ let stepvalue = input.step.value;
3602
+ if (input.step.extract) {
3603
+ const text2 = stepvalue ?? "";
3604
+ const interpolated = interpolate(text2, scopes);
3605
+ consumed.push(...interpolated.consumed);
3606
+ const extraction = regexextract(input.step.extract, interpolated.text, input.now);
3607
+ if (extraction.matched) {
3608
+ for (const variable of extraction.variables) scopes = setvariable(scopes, variable.name, "string", variable.value, input.now);
3609
+ produced = [...produced, ...extraction.variables.map((variable) => variable.name)];
3610
+ }
3611
+ stepvalue = interpolated.text;
3612
+ }
3613
+ const target = input.step.target !== void 0 ? interpolate(input.step.target, scopes) : void 0;
3614
+ if (target) consumed.push(...target.consumed);
3615
+ const value = stepvalue !== void 0 ? interpolate(stepvalue, scopes) : void 0;
3616
+ if (value) consumed.push(...value.consumed);
3617
+ const options = input.step.options !== void 0 ? interpolate(input.step.options, scopes) : void 0;
3618
+ if (options) consumed.push(...options.consumed);
3619
+ const dispatchable = { ...input.step, ...target !== void 0 ? { target: target.text } : {}, ...value !== void 0 ? { value: value.text } : {}, ...options !== void 0 ? { options: options.text } : {} };
3620
+ const output = await input.execute(dispatchable, { scopes, ...input.block !== void 0 ? { block: input.block } : {} });
3621
+ if (input.step.bindings) {
3622
+ const bound = bindvariables(scopes, input.step.bindings, { ...input.outputs, [input.step.id]: { stepid: input.step.id, ok: output.ok, summary: output.summary, ...output.details !== void 0 ? { details: output.details } : {}, at: input.now } }, input.now);
3623
+ scopes = bound.scopes;
3624
+ produced = [.../* @__PURE__ */ new Set([...produced, ...bound.produced])];
3625
+ }
3626
+ const duration = Date.now() - startedat;
3627
+ return { scopes, log: runlogof(input.step, output.ok ? "done" : "failed", startedat, duration, output.summary, { ...input.block !== void 0 ? { block: input.block } : {}, ...consumed.length > 0 ? { consumed } : {}, ...produced.length > 0 ? { produced } : {}, ...output.details !== void 0 ? { details: output.details } : {}, ...output.ok ? { checkpoint: true } : {} }), output };
3628
+ } catch (error) {
3629
+ const duration = Date.now() - startedat;
3630
+ const summary = error instanceof Error ? error.message : String(error);
3631
+ return { scopes, log: runlogof(input.step, "failed", startedat, duration, summary, { ...input.block !== void 0 ? { block: input.block } : {}, ...consumed.length > 0 ? { consumed } : {} }), output: { ok: false, summary } };
3632
+ }
3633
+ }
3634
+ async function runworkflow(input) {
3635
+ if (input.gates && !input.gates.sessionactive) throw new Error("The workflow refuses to run outside an approved session.");
3636
+ if (input.gates && !input.gates.planapproved) throw new Error("The workflow refuses to run without the approved plan review.");
3637
+ if (input.gates) for (const origin of input.record.origins) {
3638
+ if (!input.gates.origingranted(origin)) throw new Error(`The workflow origin ${origin} falls outside the session grants.`);
3639
+ }
3640
+ if (input.run.state === "done" || input.run.state === "failed" || input.run.state === "cancelled") throw new Error(`The workflow run is already ${input.run.state}.`);
3641
+ const { pausedat, ...resumed } = input.run;
3642
+ void pausedat;
3643
+ let run = input.run.state === "paused" ? { ...resumed, state: "running" } : { ...input.run, state: "running" };
3644
+ let scopes = input.scopes ?? [{ name: "root", variables: [] }];
3645
+ const log = [...input.log ?? []];
3646
+ const outputs = { ...input.outputs ?? {} };
3647
+ let activeblock;
3648
+ for (let index = run.cursor; index < input.record.steps.length; index += 1) {
3649
+ const step = input.record.steps[index];
3650
+ if (step.block !== void 0 && step.block !== activeblock) {
3651
+ scopes = pushscope(scopes, step.block, scopes[scopes.length - 1].name);
3652
+ activeblock = step.block;
3653
+ } else if (step.block === void 0 && activeblock !== void 0) {
3654
+ while (scopes.length > 1) scopes = popscope(scopes);
3655
+ activeblock = void 0;
3656
+ }
3657
+ const executed = await runstep({ step, scopes, outputs, execute: input.execute, now: Date.now(), ...step.block !== void 0 ? { block: step.block } : {} });
3658
+ scopes = executed.scopes;
3659
+ log.push(executed.log);
3660
+ outputs[step.id] = { stepid: step.id, ok: executed.output.ok, summary: executed.output.summary, ...executed.output.details !== void 0 ? { details: executed.output.details } : {}, at: Date.now() };
3661
+ if (!executed.output.ok) {
3662
+ run = { ...run, state: "failed", endedat: Date.now(), failreason: executed.output.summary };
3663
+ return { run, scopes, log, outputs };
3664
+ }
3665
+ run = { ...run, cursor: index + 1 };
3666
+ if (input.oncheckpoint) await input.oncheckpoint({ run, scopes, log });
3667
+ }
3668
+ run = { ...run, state: "done", endedat: Date.now() };
3669
+ return { run, scopes, log, outputs };
3670
+ }
3671
+ function dryrunworkflow(input) {
3672
+ const run = { ...input.run, state: "running", ...input.run.dryrun === true ? { dryrun: true } : { dryrun: true } };
3673
+ let scopes = input.scopes ?? [{ name: "root", variables: [] }];
3674
+ const log = [...input.log ?? []];
3675
+ for (let index = run.cursor; index < input.record.steps.length; index += 1) {
3676
+ const step = input.record.steps[index];
3677
+ const summary = input.projection(step);
3678
+ const entry = summary === void 0 ? runlogof(step, "refused", input.now, 0, `The ${step.kind} step has no read only projection and the dry run refuses it.`, { ...step.block !== void 0 ? { block: step.block } : {} }) : runlogof(step, "done", input.now, 0, summary, { ...step.block !== void 0 ? { block: step.block } : {} });
3679
+ log.push(entry);
3680
+ scopes = setvariable(scopes, `${step.id}outcome`, "boolean", entry.state === "done", input.now);
3681
+ }
3682
+ return { run: { ...run, state: "done", cursor: input.record.steps.length, endedat: input.now }, scopes, log };
3683
+ }
3684
+
2810
3685
  // netauth.ts
2811
3686
  function oauthflowof(value) {
2812
3687
  if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
@@ -3040,9 +3915,9 @@ function consolediff(input) {
3040
3915
  }
3041
3916
 
3042
3917
  // policy.ts
3043
- 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", "attachcdp", "detachcdp", "cdpcmd", "overridescript", "heapshot", "profilecpu", "capturesourcemaps", "emulatedevice", "emulatenetwork", "emulatelocate", "setuseragent", "overridepermission"]);
3918
+ 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", "attachcdp", "detachcdp", "cdpcmd", "overridescript", "heapshot", "profilecpu", "capturesourcemaps", "emulatedevice", "emulatenetwork", "emulatelocate", "setuseragent", "overridepermission", "restoresession", "exportsessions", "importsessions", "runworkflow"]);
3044
3919
  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", "setbreakpoint", "stepcode", "watchexpr"]);
3045
- 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", "watchcdp", "measureflow", "trackmemory", "watchshifts", "traceload", "annotatetrace", "replaytrace", "blackboxscripts"]);
3920
+ 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", "watchcdp", "measureflow", "trackmemory", "watchshifts", "traceload", "annotatetrace", "replaytrace", "blackboxscripts", "persiststate", "capturesession", "namedsessions", "diffsessions", "searchsessions", "composeworkflow", "savetemplate", "dryrun", "delay", "waitelement", "compute", "extractvars"]);
3046
3921
  var allowedactions = /* @__PURE__ */ new Set([...sensitiveactions, ...interactionactions, ...readactions]);
3047
3922
  var watchactions = /* @__PURE__ */ new Set(["watchmutate", "watchbanner", "watchfocus", "watchtab"]);
3048
3923
  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"]);
@@ -3062,6 +3937,8 @@ var debugactions = /* @__PURE__ */ new Set(["watchconsole", "watcherrors", "watc
3062
3937
  var cdpactions = /* @__PURE__ */ new Set(["attachcdp", "detachcdp", "cdpcmd", "watchcdp", "setbreakpoint", "stepcode", "watchexpr", "overridescript"]);
3063
3938
  var profileractions = /* @__PURE__ */ new Set(["measureflow", "heapshot", "trackmemory", "profilecpu", "watchshifts", "traceload", "annotatetrace", "replaytrace", "capturesourcemaps"]);
3064
3939
  var emulationactions = /* @__PURE__ */ new Set(["emulatedevice", "emulatenetwork", "emulatelocate", "setuseragent", "overridepermission", "blackboxscripts"]);
3940
+ var sessionactions = /* @__PURE__ */ new Set(["persiststate", "capturesession", "restoresession", "namedsessions", "diffsessions", "searchsessions", "exportsessions", "importsessions"]);
3941
+ var workflowactions = /* @__PURE__ */ new Set(["composeworkflow", "savetemplate", "runworkflow", "dryrun", "delay", "waitelement", "compute", "extractvars"]);
3065
3942
  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"]);
3066
3943
  var fieldkinds = ["text", "email", "phone", "date", "number", "select", "check", "radio", "file", "password", "card", "code"];
3067
3944
  var layoutmutationactions = /* @__PURE__ */ new Set(["grouptabs", "colorgroup", "collapsegroup", "savelayout", "restorelayout"]);
@@ -3077,6 +3954,12 @@ function hostpattern(origin) {
3077
3954
  if (parsed.protocol !== "https:") throw new Error("Only HTTPS origins can be granted.");
3078
3955
  return `${parsed.origin}/*`;
3079
3956
  }
3957
+ function issessionkind(kind) {
3958
+ return sessionactions.has(kind);
3959
+ }
3960
+ function isworkflowkind(kind) {
3961
+ return workflowactions.has(kind);
3962
+ }
3080
3963
  function isdebugkind(kind) {
3081
3964
  return debugactions.has(kind);
3082
3965
  }
@@ -3115,6 +3998,8 @@ function requiredcapability(kind) {
3115
3998
  if (kind === "writeclipboard" || kind === "copyscreen") return "clipboardWrite";
3116
3999
  if (kind === "downloadimages") return "downloads";
3117
4000
  if (kind === "authflow") return "tabs";
4001
+ if (kind === "capturesession" || kind === "restoresession") return "tabs";
4002
+ if (kind === "exportsessions") return "downloads";
3118
4003
  if (kind === "openlink" || kind === "openprivate" || kind === "navlist" || kind === "batchopen" || kind === "reopentab" || kind === "deeplink") return "tabs";
3119
4004
  if (tabscommandactions.has(kind)) return "tabs";
3120
4005
  return void 0;
@@ -4520,6 +5405,253 @@ function validateemulationgrammar(step, options) {
4520
5405
  }
4521
5406
  return { allowed: true };
4522
5407
  }
5408
+ function validatesessiongrammar(step, options) {
5409
+ const kind = step.kind;
5410
+ if (kind === "persiststate") {
5411
+ if (options.resume !== void 0 && typeof options.resume !== "boolean") return { allowed: false, reason: "The reviewed resume flag must be a boolean." };
5412
+ return { allowed: true };
5413
+ }
5414
+ if (kind === "capturesession") {
5415
+ const plan = snapshotplanof(options.snapshot);
5416
+ if (!plan) return { allowed: false, reason: "The session capture needs a reviewed snapshot plan with its scope, a non-empty section list of the reviewed grammar (tabs, scroll, forms, storage, cookies) and the capture link flag." };
5417
+ if (plan.auto !== void 0) {
5418
+ const interval = autointervalof(options.snapshot.auto);
5419
+ if (interval === void 0) return { allowed: false, reason: "The reviewed auto snapshot interval needs a positive period, a positive maximum snapshot count and a zero or positive expiry window with no code ceiling." };
5420
+ }
5421
+ return { allowed: true };
5422
+ }
5423
+ if (kind === "restoresession") {
5424
+ if (typeof options.sessionid !== "string" || !options.sessionid.trim()) return { allowed: false, reason: "The session restore needs the reviewed session id of the saved record." };
5425
+ if (restoreplanof(options.restore) === void 0) return { allowed: false, reason: "The session restore needs a reviewed restore plan with its tab, form and capture policies." };
5426
+ if (options.reviewed !== true) return { allowed: false, reason: "Every session restore needs the explicit restore review with its tabs, form state and captures listed before it reopens anything." };
5427
+ return { allowed: true };
5428
+ }
5429
+ if (kind === "namedsessions") {
5430
+ if (typeof options.sessionid !== "string" || !options.sessionid.trim()) return { allowed: false, reason: "The session filing needs the reviewed session id of the saved record." };
5431
+ if (typeof options.name !== "string" || !options.name.trim()) return { allowed: false, reason: "The session filing needs a reviewed non-empty session name." };
5432
+ if (options.folder !== void 0 && (typeof options.folder !== "string" || !options.folder.trim())) return { allowed: false, reason: "The reviewed folder name must be a non-empty string." };
5433
+ if (options.tags !== void 0 && (!Array.isArray(options.tags) || !options.tags.every((tag) => typeof tag === "string" && tag.trim()))) return { allowed: false, reason: "The reviewed tag list must be a list of non-empty strings." };
5434
+ return { allowed: true };
5435
+ }
5436
+ if (kind === "diffsessions") {
5437
+ if (typeof options.left !== "string" || !options.left.trim() || typeof options.right !== "string" || !options.right.trim()) return { allowed: false, reason: "The session diff needs the reviewed ids of both saved sessions." };
5438
+ return { allowed: true };
5439
+ }
5440
+ if (kind === "searchsessions") {
5441
+ if (searchqueryof(options.query) === void 0) return { allowed: false, reason: "The session search needs a reviewed query with a non-empty term list, fields of the reviewed grammar (urls, titles, names, text) and an optional time window." };
5442
+ return { allowed: true };
5443
+ }
5444
+ if (kind === "exportsessions") {
5445
+ if (options.reviewed !== true) return { allowed: false, reason: "Session exports need the explicit export review before any session file leaves the device." };
5446
+ if (options.ids !== void 0 && (!Array.isArray(options.ids) || options.ids.length === 0 || !options.ids.every((id) => typeof id === "string" && id.trim()))) return { allowed: false, reason: "The reviewed export id list must be a non-empty list of saved session ids." };
5447
+ return { allowed: true };
5448
+ }
5449
+ if (kind === "importsessions") {
5450
+ if (options.reviewed !== true) return { allowed: false, reason: "Session imports need the explicit full record review before any record joins the library." };
5451
+ if (importsessionfile(options.file) === void 0) return { allowed: false, reason: "The session import needs a reviewed file of the known format version with an intact checksum." };
5452
+ return { allowed: true };
5453
+ }
5454
+ return { allowed: true };
5455
+ }
5456
+ function restorereviewgranted(step) {
5457
+ let options = {};
5458
+ try {
5459
+ options = parseoptions(step);
5460
+ } catch {
5461
+ options = {};
5462
+ }
5463
+ if (restoreplanof(options.restore) === void 0) return { allowed: false, reason: "Every session restore needs a reviewed restore plan with its tab, form and capture policies." };
5464
+ if (options.reviewed !== true) return { allowed: false, reason: "The session restore needs the explicit restore review of its tabs, form state and captures before it reopens anything." };
5465
+ return { allowed: true };
5466
+ }
5467
+ function sessionrestoregate(input) {
5468
+ const gate = sessiongate({ session: input.session, tabid: input.tabid, origin: input.origin, now: input.now, action: "run the session memory step" });
5469
+ if (!gate.allowed) return gate;
5470
+ if (!input.plan || input.plan.state !== "approved") return { allowed: false, reason: "Session memory steps need an approved plan before they run." };
5471
+ if (input.step.kind === "restoresession") return restorereviewgranted(input.step);
5472
+ return { allowed: true };
5473
+ }
5474
+ function restoreoriginsgranted(urls, grants) {
5475
+ const covered = new Set(grants);
5476
+ const skippedorigins = [];
5477
+ for (const url of urls) {
5478
+ let origin = "";
5479
+ try {
5480
+ origin = new URL(url).origin;
5481
+ } catch {
5482
+ origin = "";
5483
+ }
5484
+ if (!origin || !covered.has(origin)) skippedorigins.push(origin || url);
5485
+ }
5486
+ return { allowed: skippedorigins.length === 0, skippedorigins: [...new Set(skippedorigins)] };
5487
+ }
5488
+ function sessionnameunique(name, records, recordid) {
5489
+ if (records.some((record2) => record2.name === name && record2.id !== recordid)) return { allowed: false, reason: `The session name ${name} already exists in the library; review a unique name.` };
5490
+ return { allowed: true };
5491
+ }
5492
+ function sessionfolderunique(name, folders) {
5493
+ if (folders.some((folder) => folder.name === name)) return { allowed: false, reason: `The folder name ${name} already exists in the library; review a unique folder name.` };
5494
+ return { allowed: true };
5495
+ }
5496
+ function snapshotretentionwindow(settings) {
5497
+ return settings?.sessionretention;
5498
+ }
5499
+ function validateworkflowgrammar(step, options) {
5500
+ const kind = step.kind;
5501
+ if (kind === "composeworkflow") {
5502
+ const payload = options.workflow;
5503
+ if (!payload || typeof payload !== "object" || Array.isArray(payload)) return { allowed: false, reason: "The workflow composition needs the reviewed workflow payload with its name, version, origins, steps and blocks." };
5504
+ const candidate = payload;
5505
+ if (typeof candidate.name !== "string" || !candidate.name.trim()) return { allowed: false, reason: "The workflow composition needs a reviewed non-empty name." };
5506
+ if (typeof candidate.version !== "number" || !Number.isInteger(candidate.version) || candidate.version < 1) return { allowed: false, reason: "The workflow version must be a positive integer." };
5507
+ if (!Array.isArray(candidate.origins) || candidate.origins.length === 0 || !candidate.origins.every((origin) => typeof origin === "string" && origin.startsWith("https://"))) return { allowed: false, reason: "The workflow needs at least one granted HTTPS origin so every step stays inside the grants." };
5508
+ if (!Array.isArray(candidate.steps) || candidate.steps.length === 0 || !candidate.steps.every((entry) => workflowstepof(entry) !== void 0 || entry && typeof entry === "object" && typeof entry.block === "string")) return { allowed: false, reason: "The workflow needs a non-empty reviewed step list of the workflow step grammar or block invocations." };
5509
+ const blocks = Array.isArray(candidate.blocks) ? candidate.blocks.flatMap((block) => {
5510
+ const parsed = workflowblockof(block);
5511
+ return parsed !== void 0 ? [parsed] : [];
5512
+ }) : [];
5513
+ if (Array.isArray(candidate.blocks) && blocks.length !== candidate.blocks.length) return { allowed: false, reason: "The reviewed block list must carry unique lowercase names, labels and valid child steps." };
5514
+ try {
5515
+ const record2 = composeworkflow({ name: candidate.name, version: candidate.version, origins: candidate.origins, steps: candidate.steps.map((entry) => "block" in entry ? { block: entry.block, label: typeof entry.label === "string" ? entry.label : entry.block } : workflowstepof(entry)), blocks, now: 0, kindallowed: (candidatekind) => {
5516
+ try {
5517
+ actionrisk(candidatekind);
5518
+ return true;
5519
+ } catch {
5520
+ return false;
5521
+ }
5522
+ }, riskof: (candidatekind) => actionrisk(candidatekind) });
5523
+ const inputs = Array.isArray(candidate.inputs) ? candidate.inputs.flatMap((name) => typeof name === "string" ? [name] : []) : void 0;
5524
+ const checked = validateworkflow(record2, { kindallowed: (workflowkind) => {
5525
+ try {
5526
+ actionrisk(workflowkind);
5527
+ return true;
5528
+ } catch {
5529
+ return false;
5530
+ }
5531
+ }, ...inputs !== void 0 ? { inputs } : {} });
5532
+ if (!checked.allowed) return checked;
5533
+ } catch (error) {
5534
+ return { allowed: false, reason: error instanceof Error ? error.message : "The workflow payload failed its composition validation." };
5535
+ }
5536
+ return { allowed: true };
5537
+ }
5538
+ if (kind === "savetemplate") {
5539
+ const payload = options.template && typeof options.template === "object" && !Array.isArray(options.template) ? options.template : {};
5540
+ const template = steptemplateof({ id: "templatereview", origin: "https://example.com", sharedat: 0, ...payload });
5541
+ if (!template) return { allowed: false, reason: "The step template needs a reviewed name and a valid workflow step it shares across workflows." };
5542
+ return { allowed: true };
5543
+ }
5544
+ if (kind === "runworkflow") {
5545
+ if (typeof options.workflowid !== "string" || !options.workflowid.trim()) return { allowed: false, reason: "The workflow run needs the reviewed id of the composed workflow." };
5546
+ if (options.reviewed !== true) return { allowed: false, reason: "Every real workflow run needs the explicit run review with its expanded step list shown before the first step executes." };
5547
+ if (options.variables !== void 0 && (!options.variables || typeof options.variables !== "object" || Array.isArray(options.variables) || !Object.values(options.variables).every((value) => typeof value === "string" || typeof value === "number" || typeof value === "boolean"))) return { allowed: false, reason: "The reviewed run variables must be an object of string, number or boolean values." };
5548
+ return { allowed: true };
5549
+ }
5550
+ if (kind === "dryrun") {
5551
+ if (typeof options.workflowid !== "string" || !options.workflowid.trim()) return { allowed: false, reason: "The dry run needs the reviewed id of the composed workflow." };
5552
+ return { allowed: true };
5553
+ }
5554
+ if (kind === "delay") {
5555
+ const delay = options.delay;
5556
+ if (!delay || typeof delay !== "object" || Array.isArray(delay)) return { allowed: false, reason: "The delay needs a reviewed base and jitter window in options." };
5557
+ const reviewed = delay;
5558
+ if (typeof reviewed.base !== "number" || !Number.isFinite(reviewed.base) || reviewed.base < 0) return { allowed: false, reason: "The reviewed delay base must be zero or a positive number of milliseconds." };
5559
+ if (typeof reviewed.jitter !== "number" || !Number.isFinite(reviewed.jitter) || reviewed.jitter < 0) return { allowed: false, reason: "The reviewed delay jitter window must be zero or a positive number of milliseconds with no code ceiling." };
5560
+ return { allowed: true };
5561
+ }
5562
+ if (kind === "waitelement") {
5563
+ const wait = options.wait;
5564
+ if (!wait || typeof wait !== "object" || Array.isArray(wait)) return { allowed: false, reason: "The element wait needs a reviewed selector, timeout and poll interval in options." };
5565
+ const reviewed = wait;
5566
+ if (typeof reviewed.selector !== "string" || !reviewed.selector.trim()) return { allowed: false, reason: "The element wait needs a reviewed non-empty selector." };
5567
+ if (typeof reviewed.timeout !== "number" || !Number.isFinite(reviewed.timeout) || reviewed.timeout < 0) return { allowed: false, reason: "The reviewed element wait timeout must be zero or a positive number of milliseconds with no code ceiling." };
5568
+ if (typeof reviewed.poll !== "number" || !Number.isFinite(reviewed.poll) || reviewed.poll < 0) return { allowed: false, reason: "The reviewed element wait poll interval must be zero or a positive number of milliseconds with no code ceiling." };
5569
+ return { allowed: true };
5570
+ }
5571
+ if (kind === "compute") {
5572
+ const expression = expressionof(options.expression);
5573
+ if (!expression) return { allowed: false, reason: `The expression step needs a reviewed expression with operands, an operator of the reviewed set (${expressionoperators.join(", ")}) and a result variable of a reviewed kind.` };
5574
+ const operatorcheck = validatexpressionoperators(expression);
5575
+ if (!operatorcheck.allowed) return operatorcheck;
5576
+ return { allowed: true };
5577
+ }
5578
+ if (kind === "extractvars") {
5579
+ const rule = regexruleof(options.rule);
5580
+ if (!rule) return { allowed: false, reason: "The variable extraction needs a reviewed regex rule with its pattern, flags and named capture groups." };
5581
+ const shapecheck = validateregexrule(rule.pattern);
5582
+ if (!shapecheck.allowed) return shapecheck;
5583
+ if (typeof options.text !== "string") return { allowed: false, reason: "The variable extraction needs the reviewed text the regex rule applies to." };
5584
+ return { allowed: true };
5585
+ }
5586
+ return { allowed: true };
5587
+ }
5588
+ function validateregexrule(pattern) {
5589
+ try {
5590
+ new RegExp(pattern);
5591
+ } catch {
5592
+ return { allowed: false, reason: "The reviewed regex pattern does not compile." };
5593
+ }
5594
+ const nestedquantifier = /\((?:[^()\\]|\\.)*[+*}]\)[+*{]/.test(pattern) || /\(\)[+*{]/.test(pattern);
5595
+ if (nestedquantifier) return { allowed: false, reason: "The reviewed regex pattern nests an unbounded quantifier inside a quantified group and is refused because adversarial text could explode the backtracking." };
5596
+ const unboundedrepeat = /\{\d+,\}/.test(pattern);
5597
+ if (unboundedrepeat && /\([^)]*\{\d+,\}[^)]*\)[+*{]/.test(pattern)) return { allowed: false, reason: "The reviewed regex pattern repeats an unbounded group and is refused because adversarial text could explode the backtracking." };
5598
+ return { allowed: true };
5599
+ }
5600
+ function validatexpressionoperators(expression) {
5601
+ const numeric = /* @__PURE__ */ new Set(["add", "subtract", "multiply", "divide", "modulo"]);
5602
+ const logic = /* @__PURE__ */ new Set(["and", "or", "not"]);
5603
+ const comparison = /* @__PURE__ */ new Set(["less", "greater", "lessequal", "greaterequal"]);
5604
+ const text2 = /* @__PURE__ */ new Set(["concat", "contains"]);
5605
+ const operator = expression.operator;
5606
+ if (numeric.has(operator)) {
5607
+ for (const operand of [expression.left, expression.right]) {
5608
+ if (operand === void 0) continue;
5609
+ if (operand.literal !== void 0 && typeof operand.literal === "boolean") return { allowed: false, reason: `The ${operator} operator needs numeric operands; boolean literals are refused.` };
5610
+ }
5611
+ if (expression.resultkind !== "number" && expression.resultkind !== "string") return { allowed: false, reason: `The ${operator} operator needs a number result kind.` };
5612
+ }
5613
+ if (logic.has(operator)) {
5614
+ for (const operand of [expression.left, expression.right]) {
5615
+ if (operand === void 0) continue;
5616
+ if (operand.literal !== void 0 && typeof operand.literal !== "boolean") return { allowed: false, reason: `The ${operator} operator needs boolean operands; non boolean literals are refused.` };
5617
+ }
5618
+ if (expression.resultkind !== "boolean") return { allowed: false, reason: `The ${operator} operator needs a boolean result kind.` };
5619
+ if (operator === "not" && expression.right !== void 0) return { allowed: false, reason: "The not operator takes one operand only." };
5620
+ }
5621
+ if (comparison.has(operator) && expression.resultkind !== "boolean") return { allowed: false, reason: `The ${operator} operator needs a boolean result kind.` };
5622
+ if (text2.has(operator) && expression.resultkind !== "boolean" && expression.resultkind !== "string") return { allowed: false, reason: `The ${operator} operator needs a string or boolean result kind.` };
5623
+ if (operator === "contains" && expression.resultkind !== "boolean") return { allowed: false, reason: "The contains operator needs a boolean result kind." };
5624
+ if (operator === "length") {
5625
+ if (expression.right !== void 0) return { allowed: false, reason: "The length operator takes one operand only." };
5626
+ if (expression.resultkind !== "number") return { allowed: false, reason: "The length operator needs a number result kind." };
5627
+ }
5628
+ if ((operator === "equal" || operator === "notequal") && !(/* @__PURE__ */ new Set(["boolean", "string", "number"])).has(expression.resultkind)) return { allowed: false, reason: "The equality operator needs a primitive result kind." };
5629
+ return { allowed: true };
5630
+ }
5631
+ function workflowgate(input) {
5632
+ const gate = sessiongate({ session: input.session, tabid: input.tabid, origin: input.origin, now: input.now, action: "run the workflow step" });
5633
+ if (!gate.allowed) return gate;
5634
+ if (!input.plan || input.plan.state !== "approved") return { allowed: false, reason: "Workflow steps need the approved plan review before they run." };
5635
+ if (input.step.kind === "runworkflow") {
5636
+ let runoptions = {};
5637
+ try {
5638
+ runoptions = parseoptions(input.step);
5639
+ } catch {
5640
+ runoptions = {};
5641
+ }
5642
+ if (runoptions.reviewed !== true) return { allowed: false, reason: "Every real workflow run needs the explicit run review with its expanded step list shown before the first step executes." };
5643
+ }
5644
+ return { allowed: true };
5645
+ }
5646
+ function dryrunprojection(step) {
5647
+ const risk = resolvedrisk({ id: step.id, kind: step.kind, summary: step.label, risk: "read", ...step.target !== void 0 ? { target: step.target } : {}, ...step.value !== void 0 ? { value: step.value } : {}, ...step.options !== void 0 ? { options: step.options } : {} });
5648
+ if (risk !== "read") return void 0;
5649
+ if (step.kind === "delay") return `The delay step would sleep its reviewed base inside the jitter window.`;
5650
+ if (step.kind === "waitelement") return `The element wait step would poll ${step.target ?? "the reviewed selector"} until appearance or the reviewed timeout.`;
5651
+ if (step.kind === "compute") return `The compute step would evaluate its reviewed expression into the result variable.`;
5652
+ if (step.kind === "extractvars") return `The variable extraction step would apply its reviewed regex rule and store the named captures.`;
5653
+ return `The ${step.kind} step would run read only and mutate nothing.`;
5654
+ }
4523
5655
  function validatecdpgrammar(step, options) {
4524
5656
  const kind = step.kind;
4525
5657
  if (kind === "attachcdp") {
@@ -5085,6 +6217,14 @@ function validatestep(step, origin) {
5085
6217
  const emulationcheck = validateemulationgrammar(step, options);
5086
6218
  if (!emulationcheck.allowed) return emulationcheck;
5087
6219
  }
6220
+ if (issessionkind(step.kind)) {
6221
+ const sessioncheck = validatesessiongrammar(step, options);
6222
+ if (!sessioncheck.allowed) return sessioncheck;
6223
+ }
6224
+ if (isworkflowkind(step.kind)) {
6225
+ const workflowcheck = validateworkflowgrammar(step, options);
6226
+ if (!workflowcheck.allowed) return workflowcheck;
6227
+ }
5088
6228
  if (step.kind === "tabcreate") {
5089
6229
  if (options.background !== void 0 && typeof options.background !== "boolean") return { allowed: false, reason: "The reviewed background flag must be a boolean." };
5090
6230
  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." };
@@ -5258,6 +6398,27 @@ function canexecute(input) {
5258
6398
  const emugatecheck = emugate({ session: input.session, plan: input.plan, step: input.step, tabid: input.tabid, origin: input.origin, now });
5259
6399
  if (!emugatecheck.allowed) return emugatecheck;
5260
6400
  }
6401
+ if (issessionkind(input.step.kind)) {
6402
+ const sessiongatecheck = sessionrestoregate({ session: input.session, plan: input.plan, step: input.step, tabid: input.tabid, origin: input.origin, now });
6403
+ if (!sessiongatecheck.allowed) return sessiongatecheck;
6404
+ if (input.step.kind === "restoresession") {
6405
+ let restoreoptions = {};
6406
+ try {
6407
+ restoreoptions = parseoptions(input.step);
6408
+ } catch {
6409
+ restoreoptions = {};
6410
+ }
6411
+ for (const url of Array.isArray(restoreoptions.origins) ? restoreoptions.origins : []) {
6412
+ if (typeof url !== "string" || !url) continue;
6413
+ const origingate = origincheck(input.session, url);
6414
+ if (!origingate.allowed) return { allowed: false, reason: `The session restore reopens ${url} outside the session origin grants; review the restore record or grant the origin.` };
6415
+ }
6416
+ }
6417
+ }
6418
+ if (isworkflowkind(input.step.kind)) {
6419
+ const workflowgatecheck = workflowgate({ session: input.session, plan: input.plan, step: input.step, tabid: input.tabid, origin: input.origin, now });
6420
+ if (!workflowgatecheck.allowed) return workflowgatecheck;
6421
+ }
5261
6422
  if (iscontrolkind(input.step.kind)) {
5262
6423
  const controlgate = sessiongate({ session: input.session, tabid: input.tabid, origin: input.origin, now, action: "control the network" });
5263
6424
  if (!controlgate.allowed) return controlgate;
@@ -5508,9 +6669,21 @@ function recordemulation(progress, planid, stepid, entry, now) {
5508
6669
  const outcome = { stepid, ok: true, summary: `${entry.reason}: ${entry.applied.length} applied layer${entry.applied.length === 1 ? "" : "s"}${entry.applied.length > 0 ? ` (${entry.applied.join(", ")})` : ""} and ${entry.reverted.length} reverted layer${entry.reverted.length === 1 ? "" : "s"}${entry.reverted.length > 0 ? ` (${entry.reverted.join(", ")})` : ""}.`, details: { emulation: entry }, at: now };
5509
6670
  return recordoutcome(base, planid, outcome, now);
5510
6671
  }
6672
+ function recordsession(progress, planid, stepid, entry, now) {
6673
+ const base = progress && progress.planid === planid ? progress : emptyprogress(planid, now);
6674
+ const counts = `${entry.sections !== void 0 ? `${entry.sections} section${entry.sections === 1 ? "" : "s"}, ` : ""}${entry.matches !== void 0 ? `${entry.matches} match${entry.matches === 1 ? "" : "es"}, ` : ""}${entry.restored !== void 0 ? `${entry.restored} restored tab${entry.restored === 1 ? "" : "s"}, ` : ""}${entry.skipped !== void 0 ? `${entry.skipped} skipped origin${entry.skipped === 1 ? "" : "s"}, ` : ""}${entry.cursor !== void 0 ? `cursor ${entry.cursor}, ` : ""}${entry.bytes !== void 0 ? `${entry.bytes} byte${entry.bytes === 1 ? "" : "s"}, ` : ""}`.replace(/, $/, "");
6675
+ const outcome = { stepid, ok: true, summary: `${entry.detail}${counts.length > 0 ? ` with ${counts}` : ""}.`, details: { session: entry }, at: now };
6676
+ return recordoutcome(base, planid, outcome, now);
6677
+ }
6678
+ function recordworkflow(progress, planid, stepid, entry, now) {
6679
+ const base = progress && progress.planid === planid ? progress : emptyprogress(planid, now);
6680
+ const counts = `${entry.executed !== void 0 ? `${entry.executed} executed step${entry.executed === 1 ? "" : "s"}, ` : ""}${entry.refused !== void 0 ? `${entry.refused} refused step${entry.refused === 1 ? "" : "s"}, ` : ""}${entry.total !== void 0 ? `${entry.total} total step${entry.total === 1 ? "" : "s"}, ` : ""}`.replace(/, $/, "");
6681
+ const outcome = { stepid, ok: true, summary: `${entry.detail}${counts.length > 0 ? ` with ${counts}` : ""}.`, details: { workflow: entry }, at: now };
6682
+ return recordoutcome(base, planid, outcome, now);
6683
+ }
5511
6684
 
5512
6685
  // version.ts
5513
- var packageversion = "1.1.48";
6686
+ var packageversion = "1.1.50";
5514
6687
 
5515
6688
  // types.ts
5516
6689
  var protocolversion = packageversion;
@@ -5704,6 +6877,51 @@ function parseproposal(value, origin, grants) {
5704
6877
  }
5705
6878
  if (step.kind === "overridepermission" && permissiongrantof(emulationoptions.permission) === void 0) throw new Error("Permission overrides of unknown permission names are refused.");
5706
6879
  }
6880
+ if (issessionkind(step.kind)) {
6881
+ let sessionoptions = {};
6882
+ try {
6883
+ sessionoptions = parseoptions(step);
6884
+ } catch {
6885
+ sessionoptions = {};
6886
+ }
6887
+ if (step.kind === "restoresession") {
6888
+ for (const url of Array.isArray(sessionoptions.origins) ? sessionoptions.origins : []) {
6889
+ if (typeof url !== "string" || !url) continue;
6890
+ const granted = covered.some((pattern) => {
6891
+ try {
6892
+ return new URL(url).origin === new URL(pattern).origin;
6893
+ } catch {
6894
+ return false;
6895
+ }
6896
+ });
6897
+ if (!granted) throw new Error(`The session restore reopens ${url} outside the grants.`);
6898
+ }
6899
+ }
6900
+ if (step.kind === "importsessions" && importsessionfile(sessionoptions.file) === void 0) throw new Error("Session import files of unknown format versions are refused.");
6901
+ }
6902
+ if (isworkflowkind(step.kind)) {
6903
+ let workflowoptions = {};
6904
+ try {
6905
+ workflowoptions = parseoptions(step);
6906
+ } catch {
6907
+ workflowoptions = {};
6908
+ }
6909
+ if (step.kind === "composeworkflow") {
6910
+ const payload = workflowoptions.workflow && typeof workflowoptions.workflow === "object" && !Array.isArray(workflowoptions.workflow) ? workflowoptions.workflow : void 0;
6911
+ const origins = payload && Array.isArray(payload.origins) ? payload.origins.filter((originvalue) => typeof originvalue === "string") : [];
6912
+ for (const workfloworigin of origins) {
6913
+ const granted = covered.some((pattern) => {
6914
+ try {
6915
+ return new URL(workfloworigin).origin === new URL(pattern).origin;
6916
+ } catch {
6917
+ return false;
6918
+ }
6919
+ });
6920
+ if (!granted) throw new Error(`The workflow origin ${workfloworigin} stays outside the grants.`);
6921
+ }
6922
+ }
6923
+ if (step.kind === "runworkflow" && workflowoptions.reviewed !== true) throw new Error("Workflow runs without the explicit run review of the expanded step list are refused.");
6924
+ }
5707
6925
  const evaluation = validatestep(step, origin);
5708
6926
  if (!evaluation.allowed) throw new Error(evaluation.reason);
5709
6927
  const target = outboundtarget(step);
@@ -5763,6 +6981,11 @@ function parseproposal(value, origin, grants) {
5763
6981
  };
5764
6982
  return { version: protocolversion, plan };
5765
6983
  }
6984
+ function workflowoutcome(input) {
6985
+ const selected = input.stepid !== void 0 ? input.entries.filter((entry) => entry.stepid === input.stepid) : input.entries;
6986
+ const steps = selected.map((entry) => ({ stepid: entry.stepid, label: entry.label, state: entry.state, duration: entry.duration, summary: entry.summary, ...entry.block !== void 0 ? { block: entry.block } : {}, ...entry.produced !== void 0 ? { produced: entry.produced } : {}, ...entry.consumed !== void 0 ? { consumed: entry.consumed } : {}, ...entry.checkpoint === true ? { checkpoint: true } : {} }));
6987
+ return { version: protocolversion, runid: input.run.id, workflowid: input.run.workflowid, state: input.run.state, ...input.run.dryrun === true ? { dryrun: true } : {}, steps };
6988
+ }
5766
6989
  function stepof(kind, candidate, index) {
5767
6990
  return { id: typeof candidate.id === "string" ? candidate.id : `candidate${index + 1}`, kind, summary: typeof candidate.summary === "string" ? candidate.summary : "", risk: "read", ...typeof candidate.options === "string" ? { options: candidate.options } : {} };
5768
6991
  }
@@ -5778,7 +7001,7 @@ function requestbody(input) {
5778
7001
  return JSON.stringify({ version: protocolversion, objective: input.objective, session: input.session, observation: input.observation, capabilities: input.capabilities });
5779
7002
  }
5780
7003
  function outcomeresponse(input) {
5781
- 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 } : {}, ...input.cdp ? { cdp: input.cdp } : {}, ...input.profile ? { profile: input.profile } : {}, ...input.emulation ? { emulation: input.emulation } : {} });
7004
+ 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 } : {}, ...input.cdp ? { cdp: input.cdp } : {}, ...input.profile ? { profile: input.profile } : {}, ...input.emulation ? { emulation: input.emulation } : {}, ...input.session ? { session: input.session } : {}, ...input.workflow ? { workflow: { runid: input.workflow.runid, state: input.workflow.state, ...input.workflow.dryrun === true ? { dryrun: true } : {}, produced: input.workflow.produced, consumed: input.workflow.consumed } } : {} });
5782
7005
  }
5783
7006
  function mapresponse(input) {
5784
7007
  return JSON.stringify({ version: protocolversion, planid: input.plan.id, planstate: input.plan.state, map: input.map });
@@ -5909,6 +7132,12 @@ function emulationreport(input) {
5909
7132
  });
5910
7133
  return { version: protocolversion, ...input.state !== void 0 ? { state: input.state } : {}, layers: input.state?.layers ?? [], devices: input.devices, networks: input.networks, locations: input.locations, agents: input.agents, blackbox: input.blackbox, permissions: input.permissions, consents };
5911
7134
  }
7135
+ function sessionreport(input) {
7136
+ return { version: protocolversion, records: input.records, events: input.events, folders: input.folders, diffs: input.diffs, ...input.auto !== void 0 ? { auto: input.auto } : {}, ...input.crashed === true ? { crashed: true } : {} };
7137
+ }
7138
+ function workflowreport(input) {
7139
+ return { version: protocolversion, workflows: input.workflows, runs: input.runs, templates: input.templates, log: input.log ?? [], scopes: input.scopes ?? [], provenance: input.provenance ?? [] };
7140
+ }
5912
7141
 
5913
7142
  // capture.ts
5914
7143
  var capturekinds = ["shotview", "shotfullpage", "shotelement", "shotregion", "contactsheet"];
@@ -6485,6 +7714,46 @@ async function runbrowseraction(step, sessiontabid, windowid) {
6485
7714
  }
6486
7715
  }
6487
7716
 
7717
+ // extension/pagesession.ts
7718
+ function capturepagestate(sections) {
7719
+ const wants = (section) => sections.includes(section);
7720
+ const forms = [];
7721
+ if (wants("forms")) {
7722
+ const elements = Array.from(document.querySelectorAll("input, textarea, select"));
7723
+ elements.forEach((element, index) => {
7724
+ if (element.type === "password") return;
7725
+ const selector = element.id ? `#${element.id}` : element.name ? `[name="${element.name}"]` : `${element.tagName.toLowerCase()}:nth-of-type(${index + 1})`;
7726
+ forms.push({ selector, value: element.value });
7727
+ });
7728
+ }
7729
+ const storagekeys = [];
7730
+ const storagevalues = [];
7731
+ if (wants("storage")) {
7732
+ for (let index = 0; index < localStorage.length; index += 1) {
7733
+ const key = localStorage.key(index);
7734
+ if (key === null) continue;
7735
+ storagekeys.push(key);
7736
+ storagevalues.push(localStorage.getItem(key) ?? "");
7737
+ }
7738
+ }
7739
+ const cookienames = wants("cookies") ? document.cookie.split(";").map((part) => part.split("=")[0]?.trim() ?? "").filter((name) => name.length > 0) : [];
7740
+ return { scrollx: window.scrollX, scrolly: window.scrollY, forms, storagekeys, storagevalues, cookienames };
7741
+ }
7742
+ function restorepagestate(state) {
7743
+ window.scrollTo(state.scrollx, state.scrolly);
7744
+ let restored = 0;
7745
+ for (const form of Array.isArray(state.forms) ? state.forms : []) {
7746
+ const element = document.querySelector(form.selector);
7747
+ if (element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement || element instanceof HTMLSelectElement) {
7748
+ element.value = form.value;
7749
+ element.dispatchEvent(new Event("input", { bubbles: true }));
7750
+ element.dispatchEvent(new Event("change", { bubbles: true }));
7751
+ restored += 1;
7752
+ }
7753
+ }
7754
+ return { restored, summary: `Restored the scroll position and ${restored} form field${restored === 1 ? "" : "s"} of the reopened tab.` };
7755
+ }
7756
+
6488
7757
  // extension/tabscommand.ts
6489
7758
  function parsetabquery(step) {
6490
7759
  let options = {};
@@ -7385,15 +8654,15 @@ function remainingpages(sessionvalue, planned) {
7385
8654
  function provenancefor(artifact, url, stepid, at) {
7386
8655
  return { artifact: artifact.id, name: artifact.name, url, stepid, rowstart: artifact.rowcount > 0 ? 1 : 0, rowend: artifact.rowcount, checksum: artifact.checksum, at };
7387
8656
  }
7388
- function interpolate(text2, row) {
8657
+ function interpolate2(text2, row) {
7389
8658
  return text2.replace(/\{\{([^}]+)\}\}/g, (_, key) => row[key.trim()] ?? "");
7390
8659
  }
7391
8660
  function loopstep(step, row) {
7392
8661
  return {
7393
8662
  ...step,
7394
- ...step.target !== void 0 ? { target: interpolate(step.target, row) } : {},
7395
- ...step.value !== void 0 ? { value: interpolate(step.value, row) } : {},
7396
- ...step.options !== void 0 ? { options: interpolate(step.options, row) } : {}
8663
+ ...step.target !== void 0 ? { target: interpolate2(step.target, row) } : {},
8664
+ ...step.value !== void 0 ? { value: interpolate2(step.value, row) } : {},
8665
+ ...step.options !== void 0 ? { options: interpolate2(step.options, row) } : {}
7397
8666
  };
7398
8667
  }
7399
8668
  function loopvariables(row) {
@@ -7597,7 +8866,7 @@ function stepoptions2(step) {
7597
8866
  }
7598
8867
  async function refreshcapabilities() {
7599
8868
  const report = await readcapabilities();
7600
- const withmedia = { ...report, captures: [...capturekinds], media: [...mediakinds], http: [...httpkinds], netwatch: [...socketkinds, ...netwatchkinds], control: [...controlkinds], debug: [...timelinekinds, ...cdpkinds], profile: [...profilerkinds], emulation: [...emulationkinds] };
8869
+ const withmedia = { ...report, captures: [...capturekinds], media: [...mediakinds], http: [...httpkinds], netwatch: [...socketkinds, ...netwatchkinds], control: [...controlkinds], debug: [...timelinekinds, ...cdpkinds], profile: [...profilerkinds], emulation: [...emulationkinds], sessions: [...sessionkinds], workflow: [...workflowkinds] };
7601
8870
  await memory.setcapabilities(withmedia);
7602
8871
  return withmedia;
7603
8872
  }
@@ -7736,6 +9005,7 @@ function browserauditkind(step) {
7736
9005
  return "tab";
7737
9006
  }
7738
9007
  function stepauditkind(step, ok) {
9008
+ if (issessionkind(step.kind)) return "session";
7739
9009
  if (isbrowserkind(step.kind)) return browserauditkind(step);
7740
9010
  if (istabscommandkind(step.kind)) {
7741
9011
  if (step.kind === "grouptabs" || step.kind === "colorgroup" || step.kind === "collapsegroup") return "group";
@@ -11711,7 +12981,8 @@ async function refreshbadge() {
11711
12981
  const locationprompts = (await memory.getlocationconsents()).filter((consent) => consent.approved === void 0).length;
11712
12982
  const emulatedlayers = [...activeemulation.values()].reduce((total2, state) => total2 + activelayers(state).length, 0);
11713
12983
  const tasktabs2 = new Set(badges.map((badge) => badge.tabid)).size;
11714
- const total = (queues?.prefetch ?? 0) + (queues?.batchopen ?? 0) + tasktabs2 + prompts + consents + quarantined + datasets + captures + media + recordingprompts + fetchprompts + consoleprompts + debuggerprompts + locationprompts + emulatedlayers + observedrequests + livechannels + activerulescount;
12984
+ const runningworkflows = (await memory.listworkflowruns()).filter((run) => run.state === "running").length + activeworkflowruns.size;
12985
+ const total = (queues?.prefetch ?? 0) + (queues?.batchopen ?? 0) + tasktabs2 + prompts + consents + quarantined + datasets + captures + media + recordingprompts + fetchprompts + consoleprompts + debuggerprompts + locationprompts + emulatedlayers + observedrequests + livechannels + activerulescount + runningworkflows;
11715
12986
  await chrome.action.setBadgeText({ text: total > 0 ? String(total) : "" }).catch(() => {
11716
12987
  });
11717
12988
  }
@@ -11818,6 +13089,429 @@ async function executeemulationstep(step, session, plan, tabid2, origin) {
11818
13089
  await refreshbadge();
11819
13090
  return { ok: true, summary: output.summary, details: { ...output.details ?? {}, emulation: { applied: [name], reverted: [] } } };
11820
13091
  }
13092
+ var restoreloadwindow = 4e3;
13093
+ var restorepollstep = 200;
13094
+ async function waittabloaded(tabid2) {
13095
+ const started = Date.now();
13096
+ while (Date.now() - started < restoreloadwindow) {
13097
+ const tab = await chrome.tabs.get(tabid2).catch(() => void 0);
13098
+ if (!tab || tab.status === "complete") return;
13099
+ await new Promise((resolve) => setTimeout(resolve, restorepollstep));
13100
+ }
13101
+ }
13102
+ async function capturesessionrecord(plan, session, runid) {
13103
+ const grants = session.grants ?? [session.origin];
13104
+ const query = plan.scope === "all" ? {} : plan.scope === "run" ? { currentWindow: true } : { active: true, currentWindow: true };
13105
+ const tabs = await chrome.tabs.query(query).catch(() => []);
13106
+ const capturedtabs = [];
13107
+ const storage = [];
13108
+ const cookies = [];
13109
+ for (const tab of tabs) {
13110
+ const url = tab.url ?? "";
13111
+ if (!url.startsWith("http")) continue;
13112
+ let taborigin = "";
13113
+ try {
13114
+ taborigin = new URL(url).origin;
13115
+ } catch {
13116
+ taborigin = "";
13117
+ }
13118
+ let state;
13119
+ if (tab.id !== void 0 && (plan.sections.includes("scroll") || plan.sections.includes("forms") || plan.sections.includes("storage") || plan.sections.includes("cookies"))) {
13120
+ state = await chrome.scripting.executeScript({ target: { tabId: tab.id }, func: capturepagestate, args: [plan.sections] }).then((result) => result[0]?.result).catch(() => void 0);
13121
+ }
13122
+ capturedtabs.push({ url, title: tab.title ?? "", index: tab.index ?? 0, scrollx: state?.scrollx ?? 0, scrolly: state?.scrolly ?? 0, forms: plan.sections.includes("forms") ? state?.forms ?? [] : [] });
13123
+ if (state && taborigin && grants.includes(taborigin)) {
13124
+ if (plan.sections.includes("storage") && state.storagekeys.length > 0) storage.push({ origin: taborigin, keys: state.storagekeys, values: state.storagevalues });
13125
+ if (plan.sections.includes("cookies") && state.cookienames.length > 0) cookies.push({ origin: taborigin, names: state.cookienames });
13126
+ }
13127
+ }
13128
+ const captures = plan.captures ? (await memory.getcaptures()).filter((record2) => record2.runid === runid).map((record2) => record2.id) : [];
13129
+ return newsessionrecord({ id: randomid(), name: `session ${new Date(Date.now()).toISOString()}`, createdat: Date.now(), tabs: capturedtabs.sort((left, right) => left.index - right.index), captures, storage, cookies });
13130
+ }
13131
+ async function performrestore(record2, restore, session) {
13132
+ const grants = session.grants ?? [session.origin];
13133
+ const grantscheck = restoreoriginsgranted(record2.tabs.map((tab) => tab.url), grants);
13134
+ const restored = [];
13135
+ for (const tab of [...record2.tabs].sort((left, right) => left.index - right.index)) {
13136
+ if (restore.tabpolicy !== "reopen") break;
13137
+ let taborigin = "";
13138
+ try {
13139
+ taborigin = new URL(tab.url).origin;
13140
+ } catch {
13141
+ taborigin = "";
13142
+ }
13143
+ if (!taborigin || !grants.includes(taborigin)) continue;
13144
+ const created = await chrome.tabs.create({ url: tab.url, index: tab.index, active: false }).catch(() => void 0);
13145
+ if (!created?.id) continue;
13146
+ await waittabloaded(created.id);
13147
+ if (restore.formpolicy === "restore") {
13148
+ await chrome.scripting.executeScript({ target: { tabId: created.id }, func: restorepagestate, args: [{ scrollx: tab.scrollx, scrolly: tab.scrolly, forms: tab.forms }] }).catch(() => {
13149
+ });
13150
+ }
13151
+ restored.push(tab);
13152
+ }
13153
+ await memory.updatesessionrecord({ ...record2, restoredat: Date.now() });
13154
+ return { restored, skippedorigins: grantscheck.skippedorigins };
13155
+ }
13156
+ async function executesessionstep(step, session, plan, tabid2, origin) {
13157
+ const options = stepoptions2(step);
13158
+ const extra = { sessionid: session.id, planid: plan.id, stepid: step.id };
13159
+ if (step.kind === "persiststate") {
13160
+ const progress = await memory.getprogress();
13161
+ const tracked = progress && progress.planid === plan.id ? progress : void 0;
13162
+ const state = taskstateof({ runid: plan.id, stepcursor: tracked?.completedsteps.length ?? 0, outputs: tracked?.outcomes ?? [], checkpointat: Date.now() });
13163
+ await memory.settaskstate(state);
13164
+ await memory.addsessionevent({ id: randomid(), kind: "persist", at: Date.now(), tabid: tabid2, detail: `Checkpointed the run at step cursor ${state.stepcursor} of ${plan.steps.length}.` });
13165
+ await memory.setprogress(recordsession(await memory.getprogress(), plan.id, step.id, { family: "persist", detail: "Checkpointed the task state of the run", cursor: state.stepcursor }, Date.now()));
13166
+ await audit("session", `Persisted the task state checkpoint of run ${plan.id} at step cursor ${state.stepcursor} of ${plan.steps.length}; the checksum detects corruption before any resume and the run resumes from it after a service worker restart.`, extra);
13167
+ return { ok: true, summary: `Task state checkpointed at step cursor ${state.stepcursor}.`, details: { session: { family: "persist", detail: "Task state checkpoint", cursor: state.stepcursor } } };
13168
+ }
13169
+ if (step.kind === "capturesession") {
13170
+ const snapshot2 = snapshotplanof(options.snapshot);
13171
+ if (!snapshot2) throw new Error("A reviewed snapshot plan is required before the session capture runs.");
13172
+ const record2 = await capturesessionrecord(snapshot2, session, plan.id);
13173
+ await memory.addsessionrecord(record2);
13174
+ if (snapshot2.auto) {
13175
+ const current = await memory.getautosnapshot();
13176
+ await memory.setautosnapshot({ interval: snapshot2.auto, lastat: Date.now(), count: current?.count ?? 0 });
13177
+ }
13178
+ await memory.addsessionevent({ id: randomid(), kind: "capture", at: Date.now(), tabid: tabid2, detail: `Captured ${record2.tabs.length} tabs with the ${snapshot2.sections.join(", ")} sections into record ${record2.id}.` });
13179
+ await memory.setprogress(recordsession(await memory.getprogress(), plan.id, step.id, { family: "capture", detail: `Captured the browsing session record ${record2.id}`, recordid: record2.id, sections: snapshot2.sections.length }, Date.now()));
13180
+ await audit("session", `Captured the browsing session record ${record2.id} of ${record2.tabs.length} tab${record2.tabs.length === 1 ? "" : "s"} with the ${snapshot2.sections.join(", ")} sections${snapshot2.captures ? ` linking ${record2.captures.length} capture record${record2.captures.length === 1 ? "" : "s"}` : ""}${snapshot2.auto ? ` and the reviewed auto interval of ${snapshot2.auto.period} milliseconds with at most ${snapshot2.auto.maxsnapshots} snapshot${snapshot2.auto.maxsnapshots === 1 ? "" : "s"}` : ""}; the local storage and cookie names of ungranted origins stayed out of the capture.`, extra);
13181
+ return { ok: true, summary: `Captured ${record2.tabs.length} tabs into the session record ${record2.id}.`, details: { snapshotid: record2.id, session: { family: "capture", detail: `Session record ${record2.id}`, recordid: record2.id, sections: snapshot2.sections.length } } };
13182
+ }
13183
+ if (step.kind === "restoresession") {
13184
+ const restore = restoreplanof(options.restore);
13185
+ if (!restore) throw new Error("A reviewed restore plan is required before the session restore runs.");
13186
+ const record2 = await memory.getsessionrecord(String(options.sessionid ?? ""));
13187
+ if (!record2) throw new Error(`No saved session matches ${String(options.sessionid ?? "")}.`);
13188
+ if (record2.sectionsexpired) throw new Error("The saved session sections expired after the retention window; only the record metadata survives for review.");
13189
+ const outcome = await performrestore(record2, restore, session);
13190
+ await memory.addsessionevent({ id: randomid(), kind: "restore", at: Date.now(), tabid: tabid2, detail: `Restored ${outcome.restored.length} tabs of record ${record2.id}${outcome.skippedorigins.length > 0 ? ` and skipped ${outcome.skippedorigins.join(", ")}` : ""}.` });
13191
+ await memory.setprogress(recordsession(await memory.getprogress(), plan.id, step.id, { family: "restore", detail: `Restored the session record ${record2.id}`, recordid: record2.id, sections: record2.tabs.length, restored: outcome.restored.length, skipped: outcome.skippedorigins.length }, Date.now()));
13192
+ await audit("session", `Restored the saved session ${record2.id} on demand: ${outcome.restored.length} tab${outcome.restored.length === 1 ? "" : "s"} reopened in their recorded order with the ${restore.formpolicy} form policy and the ${restore.capturepolicy} capture policy${outcome.skippedorigins.length > 0 ? ` while ${outcome.skippedorigins.join(", ")} stayed skipped because their grants expired` : ""}.`, extra);
13193
+ return { ok: true, summary: `Restored ${outcome.restored.length} of ${record2.tabs.length} tabs${outcome.skippedorigins.length > 0 ? `; skipped ${outcome.skippedorigins.join(", ")} outside the grants` : ""}.`, details: { restoreid: record2.id, skippedorigins: outcome.skippedorigins, session: { family: "restore", detail: `Session restore of ${record2.id}`, recordid: record2.id, sections: record2.tabs.length, restored: outcome.restored.length, skipped: outcome.skippedorigins.length } } };
13194
+ }
13195
+ if (step.kind === "namedsessions") {
13196
+ const record2 = await memory.getsessionrecord(String(options.sessionid ?? ""));
13197
+ if (!record2) throw new Error(`No saved session matches ${String(options.sessionid ?? "")}.`);
13198
+ const name = String(options.name ?? "");
13199
+ const records = await memory.getsessionrecords();
13200
+ const unique = sessionnameunique(name, records, record2.id);
13201
+ if (!unique.allowed) throw new Error(unique.reason);
13202
+ const folder = typeof options.folder === "string" && options.folder.trim() ? options.folder : record2.folder;
13203
+ const tags = Array.isArray(options.tags) ? options.tags.filter((tag) => typeof tag === "string" && tag.trim()) : record2.tags;
13204
+ await memory.updatesessionrecord({ ...record2, name, ...folder !== void 0 ? { folder } : {}, tags });
13205
+ if (folder !== void 0) {
13206
+ const folders = await memory.getsessionfolders();
13207
+ if (sessionfolderunique(folder, folders).allowed) await memory.setsessionfolders([...folders, ...sessionfolderof({ name: folder, tags }) !== void 0 ? [sessionfolderof({ name: folder, tags })] : []]);
13208
+ }
13209
+ await memory.addsessionevent({ id: randomid(), kind: "name", at: Date.now(), tabid: tabid2, detail: `Filed the session record ${record2.id} as ${name}${folder !== void 0 ? ` under ${folder}` : ""}.` });
13210
+ await memory.setprogress(recordsession(await memory.getprogress(), plan.id, step.id, { family: "name", detail: `Filed the session record ${record2.id} as ${name}`, recordid: record2.id }, Date.now()));
13211
+ await audit("session", `Filed the saved session ${record2.id} under the reviewed name ${name}${folder !== void 0 ? ` inside the ${folder} folder` : ""}${tags.length > 0 ? ` with the tags ${tags.join(", ")}` : ""}; the filing stays read only organization.`, extra);
13212
+ return { ok: true, summary: `Filed the session record ${record2.id} as ${name}.`, details: { session: { family: "name", detail: `Session filed as ${name}`, recordid: record2.id } } };
13213
+ }
13214
+ if (step.kind === "diffsessions") {
13215
+ const left = await memory.getsessionrecord(String(options.left ?? ""));
13216
+ const right = await memory.getsessionrecord(String(options.right ?? ""));
13217
+ if (!left || !right) throw new Error("The session diff needs both saved sessions in the library.");
13218
+ const diff = newsessiondiff({ id: randomid(), left, right, at: Date.now() });
13219
+ await memory.addsessiondiff(diff);
13220
+ await memory.addsessionevent({ id: randomid(), kind: "diff", at: Date.now(), tabid: tabid2, detail: `Diffed ${left.id} and ${right.id} with ${diff.changes.length} changes.` });
13221
+ await memory.setprogress(recordsession(await memory.getprogress(), plan.id, step.id, { family: "diff", detail: `Diffed the sessions ${left.id} and ${right.id}`, recordid: diff.id, sections: diff.changes.length }, Date.now()));
13222
+ await audit("session", `Compared the saved sessions ${left.id} and ${right.id}: ${diff.changes.length} tab, url, form and storage change${diff.changes.length === 1 ? "" : "s"} classified as read only comparison evidence.`, extra);
13223
+ return { ok: true, summary: `Diffed the sessions: ${diff.changes.length} change${diff.changes.length === 1 ? "" : "s"}.`, details: { diffid: diff.id, changes: diff.changes, session: { family: "diff", detail: `Session diff ${diff.id}`, recordid: diff.id, sections: diff.changes.length } } };
13224
+ }
13225
+ if (step.kind === "searchsessions") {
13226
+ const query = searchqueryof(options.query);
13227
+ if (!query) throw new Error("A reviewed search query is required before the session search runs.");
13228
+ const matches = await memory.searchmemory(query);
13229
+ await memory.addsessionevent({ id: randomid(), kind: "search", at: Date.now(), tabid: tabid2, detail: `Searched ${query.terms.join(", ")} across the saved sessions with ${matches.length} matches.` });
13230
+ await memory.setprogress(recordsession(await memory.getprogress(), plan.id, step.id, { family: "search", detail: `Searched ${query.terms.join(", ")} across the saved sessions`, matches: matches.length }, Date.now()));
13231
+ await audit("session", `Searched the terms ${query.terms.join(", ")} across the saved sessions on the ${query.fields.join(", ")} fields${query.from !== void 0 || query.to !== void 0 ? ` inside the reviewed time window` : ""}: ${matches.length} match${matches.length === 1 ? "" : "es"} returned with their session ids.`, extra);
13232
+ return { ok: true, summary: `Found ${matches.length} match${matches.length === 1 ? "" : "es"} across the saved sessions.`, details: { matches, matchcount: matches.length, session: { family: "search", detail: `Session search of ${query.terms.join(", ")}`, matches: matches.length } } };
13233
+ }
13234
+ if (step.kind === "exportsessions") {
13235
+ if (options.reviewed !== true) throw new Error("Session exports need the explicit export review before any session file leaves the device.");
13236
+ const records = await memory.getsessionrecords();
13237
+ const selected = Array.isArray(options.ids) && options.ids.length > 0 ? records.filter((record2) => options.ids.includes(record2.id)) : records;
13238
+ if (selected.length === 0) throw new Error("No saved session matches the reviewed export ids.");
13239
+ const file = exportsessionfile(selected, Date.now());
13240
+ const payload = JSON.stringify(file);
13241
+ await chrome.downloads.download({ url: `data:application/json;charset=utf-8,${encodeURIComponent(payload)}`, filename: `devthink-sessions-${Date.now()}.json` }).catch(() => {
13242
+ throw new Error("The session file download was refused by the browser.");
13243
+ });
13244
+ await memory.addsessionevent({ id: randomid(), kind: "export", at: Date.now(), tabid: tabid2, detail: `Exported ${file.records.length} session records as a ${file.bytesize} byte file.` });
13245
+ await memory.setprogress(recordsession(await memory.getprogress(), plan.id, step.id, { family: "export", detail: `Exported ${file.records.length} session records`, bytes: file.bytesize }, Date.now()));
13246
+ await audit("export", `Exported ${file.records.length} saved session record${file.records.length === 1 ? "" : "s"} as one ${file.bytesize} byte session file of format version ${file.formatversion} with its checksum, through the reviewed download flow.`, extra);
13247
+ return { ok: true, summary: `Exported ${file.records.length} session record${file.records.length === 1 ? "" : "s"} as a ${file.bytesize} byte file.`, details: { session: { family: "export", detail: `Session export of ${file.records.length} records`, bytes: file.bytesize } } };
13248
+ }
13249
+ if (step.kind === "importsessions") {
13250
+ if (options.reviewed !== true) throw new Error("Session imports need the explicit full record review before any record joins the library.");
13251
+ const file = importsessionfile(options.file);
13252
+ if (!file) throw new Error(`The reviewed session file failed its format version or checksum validation; only format version ${sessionfileversion} imports.`);
13253
+ const records = await memory.getsessionrecords();
13254
+ let added = 0;
13255
+ for (const record2 of file.records) {
13256
+ if (records.some((existing) => existing.id === record2.id)) continue;
13257
+ const unique = sessionnameunique(record2.name, [...records, ...file.records.filter((candidate) => candidate.id !== record2.id).map((candidate) => ({ id: candidate.id, name: candidate.name }))]);
13258
+ const name = unique.allowed ? record2.name : `${record2.name} (${record2.id.slice(0, 6)})`;
13259
+ await memory.addsessionrecord({ ...record2, name });
13260
+ added += 1;
13261
+ }
13262
+ await memory.addsessionevent({ id: randomid(), kind: "import", at: Date.now(), tabid: tabid2, detail: `Imported ${added} session records from a reviewed file of format version ${file.formatversion}.` });
13263
+ await memory.setprogress(recordsession(await memory.getprogress(), plan.id, step.id, { family: "import", detail: `Imported ${added} session records`, sections: added }, Date.now()));
13264
+ await audit("session", `Imported ${added} of ${file.records.length} reviewed session record${file.records.length === 1 ? "" : "s"} from a session file of format version ${file.formatversion}; every record was listed in the full record review before it joined the library.`, extra);
13265
+ return { ok: true, summary: `Imported ${added} session record${added === 1 ? "" : "s"} after review.`, details: { session: { family: "import", detail: `Session import of ${added} records`, sections: added } } };
13266
+ }
13267
+ throw new Error(`The ${step.kind} step has no session memory executor.`);
13268
+ }
13269
+ var activeworkflowruns = /* @__PURE__ */ new Map();
13270
+ function runscopes(variables) {
13271
+ const root = { name: "root", variables: [] };
13272
+ if (!variables || typeof variables !== "object" || Array.isArray(variables)) return [root];
13273
+ for (const [name, value] of Object.entries(variables)) {
13274
+ if (typeof value === "number") root.variables.push({ name, kind: "number", value, setat: Date.now() });
13275
+ else if (typeof value === "boolean") root.variables.push({ name, kind: "boolean", value, setat: Date.now() });
13276
+ else if (typeof value === "string") root.variables.push({ name, kind: "string", value, setat: Date.now() });
13277
+ }
13278
+ return [root];
13279
+ }
13280
+ async function dispatchworkflowstep(step, context) {
13281
+ const settings = await memory.getsettings();
13282
+ const verdicts = await memory.getsafeties();
13283
+ const action = {
13284
+ id: step.id,
13285
+ kind: step.kind,
13286
+ summary: step.label,
13287
+ risk: actionrisk(step.kind),
13288
+ ...step.target !== void 0 ? { target: step.target } : {},
13289
+ ...step.value !== void 0 ? { value: step.value } : {},
13290
+ ...step.options !== void 0 ? { options: step.options } : {}
13291
+ };
13292
+ const gate = canexecute({ session: context.session, plan: context.plan, step: action, tabid: context.tabid, origin: context.origin, ...verdicts.length > 0 ? { verdicts } : {}, ...settings !== void 0 ? { settings } : {} });
13293
+ if (!gate.allowed) return { ok: false, summary: `The workflow step ${step.label} was refused: ${gate.reason}` };
13294
+ if (step.kind === "delay") return await executedelaystep(step);
13295
+ if (step.kind === "waitelement") return await executewaitelement(step, context.tabid);
13296
+ if (step.kind === "compute") return await executecomputestep(step, context.session);
13297
+ if (step.kind === "extractvars") return await executeextractvarsstep(step, context.session);
13298
+ const output = await executeaction(action, context.session, context.plan, context.tabid, context.origin, settings, verdicts.length > 0 ? verdicts : void 0, "run");
13299
+ return { ok: Boolean(output.ok), summary: output.summary, ...output.details !== void 0 ? { details: output.details } : {} };
13300
+ }
13301
+ async function executedelaystep(step) {
13302
+ const options = stepoptions2({ id: step.id, kind: step.kind, summary: step.label, risk: "read", ...step.options !== void 0 ? { options: step.options } : {} });
13303
+ const delay = delayof(options.delay);
13304
+ const sampled = delayjitter(delay, hashseed(`${step.id}:${Date.now()}`));
13305
+ const transport = await sleepreviewed(sampled, step.id);
13306
+ return { ok: true, summary: `Slept ${Math.round(sampled)} milliseconds inside the reviewed window of base ${delay.base} and jitter ${delay.jitter}${transport === "alarm" ? " through the alarms api" : ""}.`, details: { sampled: Math.round(sampled), base: delay.base, jitter: delay.jitter, transport } };
13307
+ }
13308
+ function delayof(value) {
13309
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("The delay needs a reviewed base and jitter window.");
13310
+ const candidate = value;
13311
+ if (typeof candidate.base !== "number" || !Number.isFinite(candidate.base) || candidate.base < 0) throw new Error("The reviewed delay base must be zero or a positive number of milliseconds.");
13312
+ if (typeof candidate.jitter !== "number" || !Number.isFinite(candidate.jitter) || candidate.jitter < 0) throw new Error("The reviewed delay jitter window must be zero or a positive number of milliseconds.");
13313
+ return { base: candidate.base, jitter: candidate.jitter };
13314
+ }
13315
+ function hashseed(text2) {
13316
+ let hash = 2166136261;
13317
+ for (let index = 0; index < text2.length; index += 1) {
13318
+ hash ^= text2.charCodeAt(index);
13319
+ hash = Math.imul(hash, 16777619) >>> 0;
13320
+ }
13321
+ return hash >>> 0;
13322
+ }
13323
+ async function sleepreviewed(sampled, stepid) {
13324
+ const alarmname = `devthinkdelay${stepid}`;
13325
+ if (sampled > 3e4 && typeof chrome.alarms?.create === "function" && typeof chrome.alarms.onAlarm?.addListener === "function") {
13326
+ try {
13327
+ await new Promise((resolve) => {
13328
+ const fallback = setTimeout(() => {
13329
+ chrome.alarms.onAlarm.removeListener(listener);
13330
+ resolve();
13331
+ }, sampled + 5e3);
13332
+ const listener = (alarm) => {
13333
+ if (alarm.name !== alarmname) return;
13334
+ clearTimeout(fallback);
13335
+ chrome.alarms.onAlarm.removeListener(listener);
13336
+ resolve();
13337
+ };
13338
+ chrome.alarms.onAlarm.addListener(listener);
13339
+ void chrome.alarms.create(alarmname, { when: Date.now() + sampled });
13340
+ });
13341
+ return "alarm";
13342
+ } catch {
13343
+ }
13344
+ }
13345
+ await waitsome(sampled);
13346
+ return "timer";
13347
+ }
13348
+ async function executewaitelement(step, tabid2) {
13349
+ const options = stepoptions2({ id: step.id, kind: step.kind, summary: step.label, risk: "read", ...step.target !== void 0 ? { target: step.target } : {}, ...step.options !== void 0 ? { options: step.options } : {} });
13350
+ const wait = waitof(options.wait, step.target);
13351
+ const startedat = Date.now();
13352
+ const starttab = await chrome.tabs.get(tabid2).catch(() => void 0);
13353
+ const starturl = starttab?.url ?? "";
13354
+ const plan = waitelementplan(wait);
13355
+ const singlepass = plan.probes === 1;
13356
+ const deadline = startedat + wait.timeout;
13357
+ for (let pass = 0; pass < plan.probes; pass += 1) {
13358
+ const tab = await chrome.tabs.get(tabid2).catch(() => void 0);
13359
+ if (!tab || starturl !== "" && (tab.url ?? "") !== starturl) return { ok: false, summary: `The tab navigated away and the element wait for ${wait.selector} aborted cleanly after ${Date.now() - startedat} milliseconds.` };
13360
+ const probe = await bridgecall(tabid2, "elementrect", wait.selector).catch(() => void 0);
13361
+ if (probe?.ok) return { ok: true, summary: `Selector ${wait.selector} appeared after ${Date.now() - startedat} milliseconds of polling every ${wait.poll} milliseconds.`, details: { selector: wait.selector, waited: Date.now() - startedat, poll: wait.poll } };
13362
+ if (singlepass || Date.now() >= deadline) break;
13363
+ await waitsome(pass + 1 < plan.probes ? wait.poll : plan.lastwait);
13364
+ }
13365
+ return { ok: false, summary: `Selector ${wait.selector} did not appear within the reviewed timeout of ${wait.timeout} milliseconds.`, details: { selector: wait.selector, waited: Date.now() - startedat } };
13366
+ }
13367
+ function waitof(value, target) {
13368
+ const candidate = value && typeof value === "object" && !Array.isArray(value) ? value : {};
13369
+ const selector = typeof candidate.selector === "string" && candidate.selector.trim() ? candidate.selector : target;
13370
+ if (!selector || !selector.trim()) throw new Error("The element wait needs a reviewed non-empty selector.");
13371
+ const timeout = typeof candidate.timeout === "number" && Number.isFinite(candidate.timeout) && candidate.timeout >= 0 ? candidate.timeout : -1;
13372
+ const poll = typeof candidate.poll === "number" && Number.isFinite(candidate.poll) && candidate.poll >= 0 ? candidate.poll : -1;
13373
+ if (timeout < 0 || poll < 0) throw new Error("The element wait needs a reviewed timeout and poll interval of zero or more milliseconds.");
13374
+ return { selector, timeout, poll };
13375
+ }
13376
+ async function executecomputestep(step, session) {
13377
+ const options = stepoptions2({ id: step.id, kind: step.kind, summary: step.label, risk: "read", ...step.options !== void 0 ? { options: step.options } : {} });
13378
+ const expression = options.expression;
13379
+ if (!expression || typeof expression !== "object") throw new Error("The compute step needs a reviewed expression.");
13380
+ const scopes = runscopes(options.variables);
13381
+ const value = expressioneval(expression, scopes);
13382
+ await audit("workflow", `Evaluated the reviewed expression into ${expression.result} with the value ${typeof value === "string" ? `"${value}"` : String(value)}.`, { ...session ? { sessionid: session.id } : {}, stepid: step.id });
13383
+ return { ok: true, summary: `Computed ${expression.result} = ${typeof value === "string" ? `"${value}"` : String(value)} through the ${expression.operator} operator.`, details: { result: expression.result, kind: expression.resultkind, value } };
13384
+ }
13385
+ async function executeextractvarsstep(step, session) {
13386
+ const options = stepoptions2({ id: step.id, kind: step.kind, summary: step.label, risk: "read", ...step.value !== void 0 ? { value: step.value } : {}, ...step.options !== void 0 ? { options: step.options } : {} });
13387
+ const rule = options.rule;
13388
+ if (!rule || typeof rule !== "object" || typeof rule.pattern !== "string") throw new Error("The variable extraction needs a reviewed regex rule.");
13389
+ const text2 = typeof options.text === "string" ? options.text : step.value ?? "";
13390
+ const extraction = regexextract(rule, text2, Date.now());
13391
+ if (!extraction.matched) {
13392
+ await audit("workflow", `The reviewed regex rule of the ${step.id} step matched nothing; no variable was stored.`, { ...session ? { sessionid: session.id } : {}, stepid: step.id });
13393
+ return { ok: true, summary: "The reviewed regex rule matched nothing; no variable was stored.", details: { matched: false, groups: rule.groups } };
13394
+ }
13395
+ await audit("workflow", `The reviewed regex rule captured ${extraction.variables.map((variable) => variable.name).join(", ")} as variables.`, { ...session ? { sessionid: session.id } : {}, stepid: step.id });
13396
+ return { ok: true, summary: `Captured ${extraction.variables.length} variable${extraction.variables.length === 1 ? "" : "s"} from the reviewed text.`, details: { matched: true, variables: extraction.variables } };
13397
+ }
13398
+ async function executeworkflowstep(step, session, plan, tabid2, origin) {
13399
+ const options = stepoptions2(step);
13400
+ if (step.kind === "composeworkflow") {
13401
+ const payload = options.workflow;
13402
+ if (!payload || typeof payload !== "object" || Array.isArray(payload)) throw new Error("The workflow composition needs the reviewed workflow payload.");
13403
+ const candidate = payload;
13404
+ const record2 = composeworkflow({
13405
+ ...typeof candidate.id === "string" && candidate.id.trim() ? { id: candidate.id } : {},
13406
+ name: String(candidate.name ?? ""),
13407
+ version: Number(candidate.version ?? 0),
13408
+ origins: Array.isArray(candidate.origins) ? candidate.origins.filter((entry) => typeof entry === "string") : [],
13409
+ steps: (Array.isArray(candidate.steps) ? candidate.steps : []).flatMap((entry) => {
13410
+ const parsed = workflowstepofentry(entry);
13411
+ return parsed !== void 0 ? [parsed] : [];
13412
+ }),
13413
+ blocks: (Array.isArray(candidate.blocks) ? candidate.blocks : []).flatMap((block) => {
13414
+ const parsed = workflowblockof(block);
13415
+ return parsed !== void 0 ? [parsed] : [];
13416
+ }),
13417
+ now: Date.now(),
13418
+ kindallowed: (kind) => {
13419
+ try {
13420
+ actionrisk(kind);
13421
+ return true;
13422
+ } catch {
13423
+ return false;
13424
+ }
13425
+ },
13426
+ riskof: (kind) => actionrisk(kind)
13427
+ });
13428
+ await memory.addworkflowrecord(record2);
13429
+ await memory.setprogress(recordworkflow(await memory.getprogress(), plan.id, step.id, { family: "compose", detail: `Composed the workflow ${record2.name} version ${record2.version}`, total: record2.steps.length }, Date.now()));
13430
+ await audit("workflow", `Composed the workflow ${record2.name} version ${record2.version} with ${record2.steps.length} expanded step${record2.steps.length === 1 ? "" : "s"} graded ${record2.risk} for review; blocks expanded so no step stayed hidden.`, { sessionid: session.id, planid: plan.id, stepid: step.id });
13431
+ return { ok: true, summary: `Composed the workflow ${record2.name} version ${record2.version} with ${record2.steps.length} expanded steps graded ${record2.risk}.`, details: { workflow: { recordid: record2.id, steps: record2.steps.length, risk: record2.risk } } };
13432
+ }
13433
+ if (step.kind === "savetemplate") {
13434
+ const template = steptemplateof({ ...options.template ?? {}, id: randomid(), origin, sharedat: Date.now() });
13435
+ if (!template) throw new Error("The step template needs a reviewed name and a valid workflow step.");
13436
+ await memory.addsteptemplate(template);
13437
+ await memory.setprogress(recordworkflow(await memory.getprogress(), plan.id, step.id, { family: "template", detail: `Shared the step template ${template.name}` }, Date.now()));
13438
+ await audit("workflow", `Shared the step template ${template.name} of the ${template.step.kind} kind for reuse across workflows.`, { sessionid: session.id, planid: plan.id, stepid: step.id });
13439
+ return { ok: true, summary: `Shared the step template ${template.name}.`, details: { workflow: { recordid: template.name, steps: 1 } } };
13440
+ }
13441
+ if (step.kind === "runworkflow") return await executeworkflowrun(step, session, plan, tabid2, origin, false);
13442
+ if (step.kind === "dryrun") return await executeworkflowrun(step, session, plan, tabid2, origin, true);
13443
+ if (step.kind === "delay") return await executedelaystep({ id: step.id, kind: "delay", label: step.summary });
13444
+ if (step.kind === "waitelement") return await executewaitelement({ id: step.id, kind: "waitelement", label: step.summary, ...step.target !== void 0 ? { target: step.target } : {}, ...step.options !== void 0 ? { options: step.options } : {} }, tabid2);
13445
+ if (step.kind === "compute") return await executecomputestep({ id: step.id, kind: "compute", label: step.summary, ...step.options !== void 0 ? { options: step.options } : {} }, session);
13446
+ if (step.kind === "extractvars") return await executeextractvarsstep({ id: step.id, kind: "extractvars", label: step.summary, ...step.value !== void 0 ? { value: step.value } : {}, ...step.options !== void 0 ? { options: step.options } : {} }, session);
13447
+ throw new Error(`The ${step.kind} step has no workflow executor.`);
13448
+ }
13449
+ function workflowstepofentry(value) {
13450
+ const parsed = workflowstepof(value);
13451
+ if (parsed) return parsed;
13452
+ return blockinvocationof(value);
13453
+ }
13454
+ async function executeworkflowrun(step, session, plan, tabid2, origin, dry) {
13455
+ const options = stepoptions2(step);
13456
+ const workflowid = typeof options.workflowid === "string" ? options.workflowid : "";
13457
+ const record2 = await memory.getworkflowrecord(workflowid);
13458
+ if (!record2) throw new Error(`No composed workflow matches ${workflowid || "the reviewed id"}.`);
13459
+ for (const workfloworigin of record2.origins) {
13460
+ if (!origingranted(session, workfloworigin)) throw new Error(`The workflow origin ${workfloworigin} falls outside the session grants.`);
13461
+ }
13462
+ const run = newworkflowrun({ workflowid: record2.id, dryrun: dry, now: Date.now() });
13463
+ await memory.setworkflowrun(run);
13464
+ await memory.setrunscopes(run.id, runscopes(options.variables));
13465
+ if (dry) {
13466
+ const evaluated = dryrunworkflow({ record: record2, run, now: Date.now(), projection: dryrunprojection });
13467
+ for (const entry of evaluated.log) await memory.addrunlogentry(run.id, entry);
13468
+ await memory.setworkflowrun(evaluated.run);
13469
+ const refused = evaluated.log.filter((entry) => entry.state === "refused").length;
13470
+ await memory.setprogress(recordworkflow(await memory.getprogress(), plan.id, step.id, { family: "dryrun", detail: `Dry ran the workflow ${record2.name}`, runid: run.id, executed: evaluated.log.length - refused, refused, total: record2.steps.length }, Date.now()));
13471
+ await audit("workflow", `The dry run of the workflow ${record2.name} evaluated ${evaluated.log.length} step${evaluated.log.length === 1 ? "" : "s"} read only; ${refused} step${refused === 1 ? "" : "s"} refused for lacking a read only projection and nothing was mutated.`, { sessionid: session.id, planid: plan.id, stepid: step.id });
13472
+ return { ok: true, summary: `The dry run evaluated ${evaluated.log.length} steps read only; ${refused} refused for lacking a read only projection.`, details: { runid: run.id, state: evaluated.run.state, dryrun: true, executed: evaluated.log.length - refused, refused, total: record2.steps.length } };
13473
+ }
13474
+ const guards = { cancelled: false };
13475
+ activeworkflowruns.set(run.id, guards);
13476
+ await audit("workflow", `Started the run ${run.id} of the workflow ${record2.name} version ${record2.version} with ${record2.steps.length} reviewed steps; every step passes the session, plan review and origin gates.`, { sessionid: session.id, planid: plan.id, stepid: step.id });
13477
+ const context = { session, plan, tabid: tabid2, origin };
13478
+ const execute = async (workflowstep) => {
13479
+ if (guards.cancelled) return { ok: false, summary: `The run was cancelled before the ${workflowstep.label} step dispatched.` };
13480
+ return await dispatchworkflowstep(workflowstep, context);
13481
+ };
13482
+ let result;
13483
+ try {
13484
+ result = await runworkflow({
13485
+ record: record2,
13486
+ run,
13487
+ scopes: runscopes(options.variables),
13488
+ execute,
13489
+ now: Date.now(),
13490
+ gates: { sessionactive: Boolean(session && !session.stoppedat && !session.pausedat && session.expiresat > Date.now()), planapproved: plan.state === "approved", origingranted: (workfloworigin) => origingranted(session, workfloworigin) },
13491
+ oncheckpoint: async (state) => {
13492
+ await memory.setworkflowrun(state.run);
13493
+ const last = state.log[state.log.length - 1];
13494
+ if (last) await memory.addrunlogentry(state.run.id, last);
13495
+ await memory.setrunscopes(state.run.id, state.scopes);
13496
+ for (const scope of state.scopes) for (const variable of scope.variables) await memory.addworkflowprovenance(state.run.id, { runid: state.run.id, kind: "binding", name: variable.name, value: variable.value, at: variable.setat });
13497
+ await refreshbadge();
13498
+ }
13499
+ });
13500
+ } finally {
13501
+ activeworkflowruns.delete(run.id);
13502
+ }
13503
+ await memory.setworkflowrun(result.run);
13504
+ for (const entry of result.log) {
13505
+ const stored = await memory.getrunlog(run.id);
13506
+ if (!stored.some((candidate) => candidate.stepid === entry.stepid && candidate.startedat === entry.startedat)) await memory.addrunlogentry(run.id, entry);
13507
+ }
13508
+ await memory.setrunscopes(run.id, result.scopes);
13509
+ const executed = result.run.cursor;
13510
+ await memory.setprogress(recordworkflow(await memory.getprogress(), plan.id, step.id, { family: "run", detail: `Ran the workflow ${record2.name}`, runid: run.id, executed, total: record2.steps.length }, Date.now()));
13511
+ await audit("workflow", `The run ${run.id} of the workflow ${record2.name} ended ${result.run.state} after ${executed} of ${record2.steps.length} steps${result.run.failreason !== void 0 ? ` with the failure ${result.run.failreason}` : ""}.`, { sessionid: session.id, planid: plan.id, stepid: step.id });
13512
+ await refreshbadge();
13513
+ return { ok: result.run.state === "done", summary: `The workflow run ended ${result.run.state} after ${executed} of ${record2.steps.length} steps.`, details: { runid: run.id, state: result.run.state, executed, total: record2.steps.length, ...result.run.failreason !== void 0 ? { failreason: result.run.failreason } : {} } };
13514
+ }
11821
13515
  async function executestep(stepid) {
11822
13516
  const session = await memory.getsession();
11823
13517
  const plan = await memory.getplan();
@@ -11826,7 +13520,10 @@ async function executestep(stepid) {
11826
13520
  if (!step) throw new Error("Reviewed step was not found.");
11827
13521
  const settings = await memory.getsettings();
11828
13522
  const verdicts = await memory.getsafeties();
11829
- const gate = canexecute({ session, plan, step, tabid: tab.id, origin, ...verdicts.length > 0 ? { verdicts } : {}, ...settings !== void 0 ? { settings } : {} });
13523
+ return executeaction(step, session, plan, tab.id, origin, settings, verdicts.length > 0 ? verdicts : void 0, "plan");
13524
+ }
13525
+ async function executeaction(step, session, plan, tabid2, origin, settings, verdicts, mode) {
13526
+ const gate = canexecute({ session, plan, step, tabid: tabid2, origin, ...verdicts !== void 0 && verdicts.length > 0 ? { verdicts } : {}, ...settings !== void 0 ? { settings } : {} });
11830
13527
  if (!gate.allowed) throw new Error(gate.reason);
11831
13528
  const capability = requiredcapability(step.kind);
11832
13529
  if (capability) {
@@ -11840,76 +13537,82 @@ async function executestep(stepid) {
11840
13537
  await enforcewindowreview(step, session, plan);
11841
13538
  }
11842
13539
  if (istabscommandkind(step.kind)) {
11843
- output = await executetabscommand(step, session, plan, tab.id);
13540
+ output = await executetabscommand(step, session, plan, tabid2);
11844
13541
  } else if (isdatasetkind(step.kind)) {
11845
- output = await executedatastep(step, session, plan, tab.id, origin);
13542
+ output = await executedatastep(step, session, plan, tabid2, origin);
11846
13543
  } else if (isfileskind(step.kind)) {
11847
- output = await executefilesstep(step, session, plan, tab.id, origin);
13544
+ output = await executefilesstep(step, session, plan, tabid2, origin);
11848
13545
  } else if (isformkind(step.kind)) {
11849
- output = await executeformstep(step, session, plan, tab.id, origin);
13546
+ output = await executeformstep(step, session, plan, tabid2, origin);
11850
13547
  } else if (isbrowserkind(step.kind)) {
11851
- output = await runbrowseraction(step, tab.id, tab.windowId ?? chrome.windows.WINDOW_ID_CURRENT);
13548
+ output = await runbrowseraction(step, tabid2, chrome.windows.WINDOW_ID_CURRENT);
11852
13549
  } else if (step.kind === "keyhold") {
11853
- output = await executekeyhold(step, session, plan, tab.id, origin);
13550
+ output = await executekeyhold(step, session, plan, tabid2, origin);
11854
13551
  } else if (step.kind === "keyrelease") {
11855
- output = await executekeyrelease(step, session, plan, tab.id, origin);
13552
+ output = await executekeyrelease(step, session, plan, tabid2, origin);
11856
13553
  } else if (step.kind === "dismissdialog") {
11857
- output = await executedismissdialog(step, session, plan, tab.id, origin);
13554
+ output = await executedismissdialog(step, session, plan, tabid2, origin);
11858
13555
  } else if (step.kind === "retryaction") {
11859
- output = await executeretryaction(step, session, plan, tab.id, origin);
13556
+ output = await executeretryaction(step, session, plan, tabid2, origin);
11860
13557
  } else if (step.kind === "mapclicks") {
11861
- output = await executemapclicks(step, plan, tab.id, origin);
13558
+ output = await executemapclicks(step, plan, tabid2, origin);
11862
13559
  } else if (step.kind === "enterframe") {
11863
- output = await executeenterframe(step, plan, tab.id, origin);
13560
+ output = await executeenterframe(step, plan, tabid2, origin);
11864
13561
  } else if (watchstepkinds.has(step.kind)) {
11865
13562
  if (!session || !plan || plan.state !== "approved") throw new Error("Watch kinds refuse to run outside an approved session plan.");
11866
- const watched = await executewatchstep(step, session, plan, tab.id, origin);
13563
+ const watched = await executewatchstep(step, session, plan, tabid2, origin);
11867
13564
  output = watched.output;
11868
13565
  watchwindow = { startedat: watched.watch.startedat, lifetime: watched.watch.lifetime };
11869
13566
  } else if (step.kind === "diffsnapshots") {
11870
- output = await executediffsnapshots(step, session, plan, tab.id, origin);
13567
+ output = await executediffsnapshots(step, session, plan, tabid2, origin);
11871
13568
  } else if (navigationstepkinds.has(step.kind)) {
11872
- output = await executenavigationkind(step, session, plan, tab.id, origin);
13569
+ output = await executenavigationkind(step, session, plan, tabid2, origin);
11873
13570
  } else if (iscapturekind(step.kind)) {
11874
- output = await executecapturestep(step, session, plan, tab.id, origin);
13571
+ output = await executecapturestep(step, session, plan, tabid2, origin);
11875
13572
  } else if (ismediakind(step.kind)) {
11876
- output = await executemediastep(step, session, plan, tab.id, origin);
13573
+ output = await executemediastep(step, session, plan, tabid2, origin);
11877
13574
  } else if (ishttpkind(step.kind)) {
11878
- output = await executehttpstep(step, session, plan, tab.id, origin);
13575
+ output = await executehttpstep(step, session, plan, tabid2, origin);
11879
13576
  } else if (issocketkind(step.kind)) {
11880
- output = await executesocketstep(step, session, plan, tab.id, origin);
13577
+ output = await executesocketstep(step, session, plan, tabid2, origin);
11881
13578
  } else if (isnetwatchkind(step.kind)) {
11882
- output = await executenetwatchstep(step, session, plan, tab.id, origin);
13579
+ output = await executenetwatchstep(step, session, plan, tabid2, origin);
11883
13580
  } else if (iscontrolkind(step.kind)) {
11884
- output = await executenetcontrolstep(step, session, plan, tab.id, origin);
13581
+ output = await executenetcontrolstep(step, session, plan, tabid2, origin);
11885
13582
  } else if (isdebugkind(step.kind)) {
11886
13583
  if (!session || !plan || plan.state !== "approved") throw new Error("Debugging kinds refuse to run outside an approved session plan.");
11887
- output = await executetimelinestep(step, session, plan, tab.id, origin);
13584
+ output = await executetimelinestep(step, session, plan, tabid2, origin);
11888
13585
  } else if (iscdpkind(step.kind)) {
11889
13586
  if (!session || !plan || plan.state !== "approved") throw new Error("Devtools protocol kinds refuse to run outside an approved session plan.");
11890
- output = await executecdpstep(step, session, plan, tab.id, origin);
13587
+ output = await executecdpstep(step, session, plan, tabid2, origin);
11891
13588
  } else if (isprofilekind(step.kind)) {
11892
13589
  if (!session || !plan || plan.state !== "approved") throw new Error("Profiling kinds refuse to run outside an approved session plan.");
11893
- output = await executeprofilestep(step, session, plan, tab.id, origin);
13590
+ output = await executeprofilestep(step, session, plan, tabid2, origin);
11894
13591
  } else if (isemulationkind(step.kind)) {
11895
13592
  if (!session || !plan || plan.state !== "approved") throw new Error("Emulation kinds refuse to run outside an approved session plan.");
11896
- output = await executeemulationstep(step, session, plan, tab.id, origin);
13593
+ output = await executeemulationstep(step, session, plan, tabid2, origin);
13594
+ } else if (issessionkind(step.kind)) {
13595
+ if (!session || !plan || plan.state !== "approved") throw new Error("Session memory kinds refuse to run outside an approved session plan.");
13596
+ output = await executesessionstep(step, session, plan, tabid2, origin);
13597
+ } else if (isworkflowkind(step.kind)) {
13598
+ if (!session || !plan || plan.state !== "approved") throw new Error("Workflow kinds refuse to run outside an approved session plan.");
13599
+ output = await executeworkflowstep(step, session, plan, tabid2, origin);
11897
13600
  } else {
11898
13601
  if (step.target && freshcheckkinds.has(step.kind)) {
11899
- const fresh = await snapshot(tab.id);
13602
+ const fresh = await snapshot(tabid2);
11900
13603
  if (!fresh.interactive.some((item) => item.selector === step.target)) throw new Error("The page changed and the target must be reviewed again.");
11901
13604
  }
11902
- output = await dispatchpagestep(step, tab.id, origin, plan);
13605
+ output = await dispatchpagestep(step, tabid2, origin, plan);
11903
13606
  }
11904
13607
  return output;
11905
13608
  };
11906
13609
  const runplan = plan;
11907
13610
  const capturepolicystate = await runcapturepolicy();
11908
13611
  if (capturepolicystate === "beforeafter" && session && step.risk !== "read" && beforeafterwrapallowed(step.kind)) {
11909
- const before = await grabstateshot(step, session, runplan, tab.id, "before");
13612
+ const before = await grabstateshot(step, session, runplan, tabid2, "before");
11910
13613
  output = await dispatchreviewedstep();
11911
13614
  if (output?.ok) {
11912
- const after = await grabstateshot(step, session, runplan, tab.id, "after");
13615
+ const after = await grabstateshot(step, session, runplan, tabid2, "after");
11913
13616
  const domversion = await memory.getobservationversion();
11914
13617
  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() });
11915
13618
  if (paired.pair) {
@@ -11921,7 +13624,7 @@ async function executestep(stepid) {
11921
13624
  } else {
11922
13625
  output = await dispatchreviewedstep();
11923
13626
  }
11924
- if (["navigate", "back", "forward"].includes(step.kind)) await recordnavigation(step, session, tab.id);
13627
+ if (["navigate", "back", "forward"].includes(step.kind)) await recordnavigation(step, session, tabid2);
11925
13628
  await recordevidence(step, output, session, plan, origin);
11926
13629
  if (output?.ok && plan && typeof output.details?.tabid === "number") {
11927
13630
  await memory.setprogress(assigntasktab(await memory.getprogress(), plan.id, output.details.tabid, Date.now()));
@@ -11929,19 +13632,20 @@ async function executestep(stepid) {
11929
13632
  const summary = output?.summary ?? "The page action returned no result.";
11930
13633
  const resolved = output?.details?.resolvedtarget;
11931
13634
  if (resolved) {
11932
- await memory.addresolution({ stepid, mode: resolved.mode, selector: resolved.selector, label: resolved.label, at: Date.now() });
13635
+ await memory.addresolution({ stepid: step.id, mode: resolved.mode, selector: resolved.selector, label: resolved.label, at: Date.now() });
11933
13636
  }
11934
- const outcome = { stepid, ok: Boolean(output?.ok), summary, ...output?.details ? { details: output.details } : {}, at: Date.now() };
13637
+ const outcome = { stepid: step.id, ok: Boolean(output?.ok), summary, ...output?.details ? { details: output.details } : {}, at: Date.now() };
11935
13638
  const auditkind = stepauditkind(step, Boolean(output?.ok));
11936
- await audit(auditkind, summary, { ...session ? { sessionid: session.id } : {}, ...plan ? { planid: plan.id } : {}, stepid });
13639
+ await audit(auditkind, summary, { ...session ? { sessionid: session.id } : {}, ...plan ? { planid: plan.id } : {}, stepid: step.id });
11937
13640
  await memory.addoutcome(outcome);
11938
- if (output?.ok && plan) {
13641
+ if (output?.ok && plan && mode === "plan") {
11939
13642
  const base = await memory.getprogress();
11940
- const completed = watchwindow ? recordwatchcompletion(base, plan.id, stepid, watchwindow.startedat, watchwindow.lifetime, Date.now()) : recordstep(base, plan.id, stepid, Date.now());
13643
+ const completed = watchwindow ? recordwatchcompletion(base, plan.id, step.id, watchwindow.startedat, watchwindow.lifetime, Date.now()) : recordstep(base, plan.id, step.id, Date.now());
11941
13644
  const tracked = recordoutcome(completed, plan.id, outcome, Date.now());
11942
13645
  await memory.setprogress(tracked);
13646
+ await memory.settaskstate(taskstateof({ runid: plan.id, stepcursor: tracked.completedsteps.length, outputs: tracked.outcomes ?? [], checkpointat: Date.now() }));
11943
13647
  const tracker = activememorytrackers.get(plan.id);
11944
- if (tracker) await sampleheapforstep(tracker, stepid, tab.id, origin, plan).catch(() => {
13648
+ if (tracker) await sampleheapforstep(tracker, step.id, tabid2, origin, plan).catch(() => {
11945
13649
  });
11946
13650
  await updatetaskbadges(plan, tracked);
11947
13651
  await refreshbadge();
@@ -12133,10 +13837,15 @@ async function handlerequest(message, sender) {
12133
13837
  const clones = clonetabs(tabs);
12134
13838
  const taskgauge = tasktabgauge(tabs.filter((tab) => badges.some((badge) => badge.tabid === tab.tabid)).length, tasktabceiling(await memory.getsettings()));
12135
13839
  const report = await buildtabreport(tabs);
13840
+ const autosnapshot = await memory.getautosnapshot();
13841
+ const crashed = await memory.getcrashflag();
13842
+ const sessionrecords = await memory.applysessionexpiry(snapshotretentionwindow(runsettings), Date.now());
13843
+ const newestworkflowrun = (await memory.listworkflowruns())[0];
13844
+ const taskstate = plan ? await memory.gettaskstate(plan.id) : void 0;
12136
13845
  const livetab = session ? await chrome.tabs.get(session.tabid).catch(() => void 0) : void 0;
12137
13846
  const waitprofile = session ? waitprofiles.find((record2) => record2.origin === session.origin) : void 0;
12138
13847
  const livestate = { phase: livetab?.status === "loading" ? "loading" : "complete", ...navrecords[0] ? { finalurl: navrecords[0].finalurl, redirects: navrecords[0].chain } : {} };
12139
- 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(), cdpsessions: await memory.getcdpsessions(), cdpcommands: await memory.getcdpcommands(), cdpeventrules: await memory.getcdpeventrules(), breakpoints: await memory.getbreakpoints(), pauses: await memory.getpauses(), watchexpressions: await memory.getwatchexpressions(), scriptoverrides: await memory.getscriptoverrides(), debuggergrants: await memory.getdebuggergrants(), pauseretention: runsettings?.pauseretention, breakpointceiling: runsettings?.breakpointceiling, cdpattached: [...activecdpsessions.values()].filter((active) => active.session.detachedat === void 0).length, profileretention: runsettings?.profileretention, traceceiling: runsettings?.traceceiling, profile: profilereport({ flows: await memory.getflowmetrics(), heaps: await memory.getheaprecords(), samples: await memory.getgrowsamples(), trends: await memory.gettrends(), profiles: await memory.getcpuprofiles(), shifts: await memory.getshiftentries(), traces: await memory.gettracerecords(), sourcemaps: await memory.getsourcemaps(), consents: await memory.getsourcemapconsents() }), profileactive: activememorytrackers.size + activeprofiletargets.size, profiletargets: [...activeprofiletargets.values()].flatMap((entry) => entry.targets), socketsactive: activesockets.size, emulation: emulationreport({ ...plan && await loademulationstate(plan.id) !== void 0 ? { state: await loademulationstate(plan.id) } : {}, devices: await memory.getdevicepresets(), networks: await memory.getnetworkpresets(), locations: await memory.getlocationpresets(), agents: await memory.getagentpresets(), blackbox: await memory.getblackboxrules(), permissions: await memory.getpermissionoverrides(), consents: await memory.getlocationconsents() }), emulatedlayers: plan ? layernames(await loademulationstate(plan.id)) : [], emulationretention: runsettings?.emulationretention, 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()] } : {} };
13848
+ 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(), cdpsessions: await memory.getcdpsessions(), cdpcommands: await memory.getcdpcommands(), cdpeventrules: await memory.getcdpeventrules(), breakpoints: await memory.getbreakpoints(), pauses: await memory.getpauses(), watchexpressions: await memory.getwatchexpressions(), scriptoverrides: await memory.getscriptoverrides(), debuggergrants: await memory.getdebuggergrants(), pauseretention: runsettings?.pauseretention, breakpointceiling: runsettings?.breakpointceiling, cdpattached: [...activecdpsessions.values()].filter((active) => active.session.detachedat === void 0).length, profileretention: runsettings?.profileretention, traceceiling: runsettings?.traceceiling, profile: profilereport({ flows: await memory.getflowmetrics(), heaps: await memory.getheaprecords(), samples: await memory.getgrowsamples(), trends: await memory.gettrends(), profiles: await memory.getcpuprofiles(), shifts: await memory.getshiftentries(), traces: await memory.gettracerecords(), sourcemaps: await memory.getsourcemaps(), consents: await memory.getsourcemapconsents() }), profileactive: activememorytrackers.size + activeprofiletargets.size, profiletargets: [...activeprofiletargets.values()].flatMap((entry) => entry.targets), socketsactive: activesockets.size, emulation: emulationreport({ ...plan && await loademulationstate(plan.id) !== void 0 ? { state: await loademulationstate(plan.id) } : {}, devices: await memory.getdevicepresets(), networks: await memory.getnetworkpresets(), locations: await memory.getlocationpresets(), agents: await memory.getagentpresets(), blackbox: await memory.getblackboxrules(), permissions: await memory.getpermissionoverrides(), consents: await memory.getlocationconsents() }), emulatedlayers: plan ? layernames(await loademulationstate(plan.id)) : [], emulationretention: runsettings?.emulationretention, 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), sessionmemory: sessionreport({ records: sessionrecords, events: await memory.getsessionevents(), folders: await memory.getsessionfolders(), diffs: await memory.getsessiondiffs(), ...autosnapshot !== void 0 ? { auto: autosnapshot.interval } : {}, ...crashed ? { crashed: true } : {} }), autosnapshotstate: autosnapshot, sessionretention: runsettings?.sessionretention, workflow: workflowreport({ workflows: await memory.listworkflows(), runs: await memory.listworkflowruns(), templates: await memory.getsteptemplates(), ...newestworkflowrun !== void 0 ? { log: await memory.getrunlog(newestworkflowrun.id), scopes: await memory.getrunscopes(newestworkflowrun.id), provenance: await memory.getworkflowprovenance(newestworkflowrun.id) } : {} }), runlogretention: runsettings?.runlogretention, ...taskstate !== void 0 ? { taskstate } : {}, ...stitchprogress.size > 0 ? { stitchprogress: [...stitchprogress.values()] } : {} };
12140
13849
  }
12141
13850
  case "capabilities":
12142
13851
  return refreshcapabilities();
@@ -12182,7 +13891,8 @@ async function handlerequest(message, sender) {
12182
13891
  const media = outcome.details?.media;
12183
13892
  const network = outcome.details?.network;
12184
13893
  const timeline = outcome.details?.timeline;
12185
- return JSON.parse(outcomeresponse({ outcome, plan, ...resolved ? { resolvedtarget: resolved } : {}, ...capture ? { capture } : {}, ...media ? { media } : {}, ...network ? { network } : {}, ...timeline ? { timeline } : {} }));
13894
+ const sessionblock = outcome.details?.session;
13895
+ return JSON.parse(outcomeresponse({ outcome, plan, ...resolved ? { resolvedtarget: resolved } : {}, ...capture ? { capture } : {}, ...media ? { media } : {}, ...network ? { network } : {}, ...timeline ? { timeline } : {}, ...sessionblock !== void 0 ? { session: { recordid: sessionblock.recordid ?? "", sections: sessionblock.sections ?? 0, ...sessionblock.matches !== void 0 ? { matches: sessionblock.matches } : {}, ...sessionblock.restored !== void 0 ? { restored: sessionblock.restored } : {}, ...sessionblock.skipped !== void 0 ? { skipped: sessionblock.skipped } : {}, ...sessionblock.cursor !== void 0 ? { cursor: sessionblock.cursor } : {}, ...sessionblock.bytes !== void 0 ? { bytes: sessionblock.bytes } : {} } } : {} }));
12186
13896
  }
12187
13897
  case "map": {
12188
13898
  const plan = await memory.getplan();
@@ -13195,6 +14905,204 @@ async function handlerequest(message, sender) {
13195
14905
  await audit("stop", "The user stopped the browser session.", { ...session ? { sessionid: session.id } : {}, ...plan ? { planid: plan.id } : {} });
13196
14906
  return { stopped: true };
13197
14907
  }
14908
+ case "sessionreview": {
14909
+ const inputreview = message;
14910
+ const record2 = await memory.getsessionrecord(inputreview.sessionid?.trim() ?? "");
14911
+ if (!record2) throw new Error(`No saved session matches ${inputreview.sessionid ?? ""}.`);
14912
+ return { record: record2, tabs: record2.tabs.map((tab) => ({ url: tab.url, title: tab.title, index: tab.index, forms: tab.forms.length })), captures: record2.captures, storage: record2.storage.map((entry) => ({ origin: entry.origin, keys: entry.keys.length })), cookies: record2.cookies };
14913
+ }
14914
+ case "approverestore": {
14915
+ const inputrestore = message;
14916
+ const session = await memory.getsession();
14917
+ if (!session || session.stoppedat || session.expiresat <= Date.now()) throw new Error("The crash restore needs an active browser session before it reopens anything.");
14918
+ const record2 = await memory.getsessionrecord(inputrestore.sessionid?.trim() ?? "");
14919
+ if (!record2) throw new Error(`No saved session matches ${inputrestore.sessionid ?? ""}.`);
14920
+ if (record2.sectionsexpired) throw new Error("The saved session sections expired after the retention window; only the record metadata survives for review.");
14921
+ const restore = { tabpolicy: "reopen", formpolicy: "restore", capturepolicy: "link" };
14922
+ const outcome = await performrestore(record2, restore, session);
14923
+ await memory.addsessionevent({ id: randomid(), kind: "restore", at: Date.now(), tabid: session.tabid, detail: `Crash restore reopened ${outcome.restored.length} tabs of record ${record2.id}${outcome.skippedorigins.length > 0 ? ` and skipped ${outcome.skippedorigins.join(", ")}` : ""}.` });
14924
+ await audit("session", `The user approved the crash restore of the saved session ${record2.id}: ${outcome.restored.length} tab${outcome.restored.length === 1 ? "" : "s"} reopened in their recorded order with the scroll and form state restored${outcome.skippedorigins.length > 0 ? ` while ${outcome.skippedorigins.join(", ")} stayed skipped because their grants expired` : ""}.`, { sessionid: session.id });
14925
+ await refreshbadge();
14926
+ return { restored: outcome.restored.length, skippedorigins: outcome.skippedorigins };
14927
+ }
14928
+ case "sessiondiff": {
14929
+ const inputdiff = message;
14930
+ const left = await memory.getsessionrecord(inputdiff.left?.trim() ?? "");
14931
+ const right = await memory.getsessionrecord(inputdiff.right?.trim() ?? "");
14932
+ if (!left || !right) throw new Error("The session diff needs both saved sessions in the library.");
14933
+ return { changes: diffsessionrecords(left, right), leftid: left.id, rightid: right.id };
14934
+ }
14935
+ case "loadsessionfile": {
14936
+ const inputfile = message;
14937
+ let parsed;
14938
+ try {
14939
+ parsed = JSON.parse(inputfile.content ?? "");
14940
+ } catch {
14941
+ throw new Error("The selected file is not a valid session file.");
14942
+ }
14943
+ const file = importsessionfile(parsed);
14944
+ if (!file) throw new Error(`The selected session file failed its format version or checksum validation; only format version ${sessionfileversion} imports.`);
14945
+ return { formatversion: file.formatversion, records: file.records.map((record2) => ({ id: record2.id, name: record2.name, tabs: record2.tabs.length, folder: record2.folder, tags: record2.tags })), bytesize: file.bytesize };
14946
+ }
14947
+ case "importsessionrecords": {
14948
+ const session = await memory.getsession();
14949
+ if (!session || session.stoppedat || session.expiresat <= Date.now()) throw new Error("Session imports need an active browser session behind the consent gates.");
14950
+ const inputimport = message;
14951
+ const file = importsessionfile(inputimport.file);
14952
+ if (!file) throw new Error(`The reviewed session file failed its format version or checksum validation; only format version ${sessionfileversion} imports.`);
14953
+ const records = await memory.getsessionrecords();
14954
+ let added = 0;
14955
+ for (const record2 of file.records) {
14956
+ if (records.some((existing) => existing.id === record2.id)) continue;
14957
+ const unique = sessionnameunique(record2.name, [...records, ...file.records.filter((candidate) => candidate.id !== record2.id).map((candidate) => ({ id: candidate.id, name: candidate.name }))]);
14958
+ const name = unique.allowed ? record2.name : `${record2.name} (${record2.id.slice(0, 6)})`;
14959
+ await memory.addsessionrecord({ ...record2, name });
14960
+ added += 1;
14961
+ }
14962
+ await memory.addsessionevent({ id: randomid(), kind: "import", at: Date.now(), tabid: session.tabid, detail: `Imported ${added} session records from a reviewed file of format version ${file.formatversion}.` });
14963
+ await audit("session", `The review panel imported ${added} of ${file.records.length} reviewed session record${file.records.length === 1 ? "" : "s"} from a session file of format version ${file.formatversion}; every record was listed before it joined the library.`, { sessionid: session.id });
14964
+ return { imported: added };
14965
+ }
14966
+ case "resumerun": {
14967
+ const plan = await memory.getplan();
14968
+ const session = await memory.getsession();
14969
+ if (!session || session.stoppedat || session.expiresat <= Date.now()) throw new Error("The run resume needs an active browser session.");
14970
+ if (!plan || plan.state !== "approved") throw new Error("The run resume needs the approved plan of the interrupted run.");
14971
+ const state = await memory.gettaskstate(plan.id);
14972
+ if (!state || !taskstatevalid(state)) throw new Error("The persisted task state is missing or corrupted; the checksum refused the resume.");
14973
+ const remaining = plan.steps.slice(state.stepcursor);
14974
+ await memory.addsessionevent({ id: randomid(), kind: "resume", at: Date.now(), tabid: session.tabid, detail: `Resumed the run ${plan.id} from step cursor ${state.stepcursor} with ${remaining.length} remaining reviewed steps.` });
14975
+ await audit("resume", `Resumed the run ${plan.id} from the persisted task state checkpoint at step cursor ${state.stepcursor}; ${remaining.length} reviewed step${remaining.length === 1 ? "" : "s"} remain and every one still passes the consent gates.`, { sessionid: session.id, planid: plan.id });
14976
+ await memory.setcrashflag(false);
14977
+ let executed = 0;
14978
+ for (const step of remaining) {
14979
+ const output = await executestep(step.id).catch(() => void 0);
14980
+ if (output === void 0) break;
14981
+ executed += 1;
14982
+ }
14983
+ return { resumed: true, stepcursor: state.stepcursor, remaining: remaining.length, executed };
14984
+ }
14985
+ case "setsessionretention": {
14986
+ const inputretention = message;
14987
+ const settings = await memory.getsettings();
14988
+ const retention = typeof inputretention.retention === "number" && Number.isInteger(inputretention.retention) && inputretention.retention >= 0 ? inputretention.retention : void 0;
14989
+ await memory.setsettings({ ...settings, ...retention !== void 0 ? { sessionretention: retention } : {} });
14990
+ await memory.applysessionexpiry(retention, Date.now());
14991
+ await audit("configure", `The user set the session retention to ${retention === void 0 ? "keep every section" : `${retention} millisecond${retention === 1 ? "" : "s"}`}; the record metadata always survives and no code ceiling applies.`);
14992
+ return { sessionretention: retention };
14993
+ }
14994
+ case "exportsessionfile": {
14995
+ const session = await memory.getsession();
14996
+ if (!session || session.stoppedat || session.expiresat <= Date.now()) throw new Error("Session exports need an active browser session behind the consent gates.");
14997
+ const inputexport = message;
14998
+ const records = await memory.getsessionrecords();
14999
+ const selected = Array.isArray(inputexport.ids) && inputexport.ids.length > 0 ? records.filter((record2) => inputexport.ids.includes(record2.id)) : records;
15000
+ if (selected.length === 0) throw new Error("No saved session matches the reviewed export ids.");
15001
+ const file = exportsessionfile(selected, Date.now());
15002
+ const payload = JSON.stringify(file);
15003
+ await chrome.downloads.download({ url: `data:application/json;charset=utf-8,${encodeURIComponent(payload)}`, filename: `devthink-sessions-${Date.now()}.json` }).catch(() => {
15004
+ throw new Error("The session file download was refused by the browser.");
15005
+ });
15006
+ await memory.addsessionevent({ id: randomid(), kind: "export", at: Date.now(), tabid: session.tabid, detail: `The review panel exported ${file.records.length} session records as a ${file.bytesize} byte file.` });
15007
+ await audit("export", `The review panel exported ${file.records.length} saved session record${file.records.length === 1 ? "" : "s"} as one ${file.bytesize} byte session file of format version ${file.formatversion} after the explicit export review.`, { sessionid: session.id });
15008
+ return { exported: file.records.length, bytes: file.bytesize };
15009
+ }
15010
+ case "clearautosnapshot": {
15011
+ await memory.clearautosnapshot();
15012
+ await audit("session", "The user cleared the reviewed auto snapshot interval; on demand captures stay the only source of session records.");
15013
+ return { cleared: true };
15014
+ }
15015
+ case "workflowreview": {
15016
+ const inputreview = message;
15017
+ const record2 = await memory.getworkflowrecord(inputreview.workflowid?.trim() ?? "");
15018
+ if (!record2) throw new Error(`No composed workflow matches ${inputreview.workflowid ?? ""}.`);
15019
+ return { record: record2, steps: record2.steps.map((entry) => ({ id: entry.id, kind: entry.kind, label: entry.label, ...entry.target !== void 0 ? { target: entry.target } : {}, ...entry.value !== void 0 ? { value: entry.value } : {}, ...entry.block !== void 0 ? { block: entry.block } : {}, ...entry.bindings !== void 0 ? { bindings: entry.bindings } : {}, ...entry.expression !== void 0 ? { expression: entry.expression } : {}, ...entry.extract !== void 0 ? { extract: entry.extract } : {} })), blocks: record2.blocks, risk: record2.risk, origins: record2.origins };
15020
+ }
15021
+ case "approveworkflowrun": {
15022
+ const inputapprove = message;
15023
+ const session = await memory.getsession();
15024
+ if (!session || session.stoppedat || session.expiresat <= Date.now()) throw new Error("Workflow runs need an active browser session behind the consent gates.");
15025
+ const record2 = await memory.getworkflowrecord(inputapprove.workflowid?.trim() ?? "");
15026
+ if (!record2) throw new Error(`No composed workflow matches ${inputapprove.workflowid ?? ""}.`);
15027
+ await audit("workflow", `The user approved the run review of the workflow ${record2.name} version ${record2.version} with its ${record2.steps.length} expanded step${record2.steps.length === 1 ? "" : "s"} shown; the run still passes every consent gate per step.`, { sessionid: session.id });
15028
+ return { approved: true, steps: record2.steps.length, risk: record2.risk };
15029
+ }
15030
+ case "executeworkflowstep": {
15031
+ const inputsingle = message;
15032
+ const session = await memory.getsession();
15033
+ const plan = await memory.getplan();
15034
+ if (!session || !plan || plan.state !== "approved") throw new Error("Single step execution needs an approved session plan.");
15035
+ const stored = await memory.getrun(inputsingle.runid?.trim() ?? "");
15036
+ if (!stored) throw new Error(`No workflow run matches ${inputsingle.runid ?? ""}.`);
15037
+ const record2 = await memory.getworkflowrecord(stored.run.workflowid);
15038
+ if (!record2) throw new Error("The workflow of the run is no longer composed in the library.");
15039
+ const step = record2.steps.find((entry) => entry.id === (inputsingle.stepid ?? ""));
15040
+ if (!step) throw new Error(`No step of the workflow matches ${inputsingle.stepid ?? ""}.`);
15041
+ const { tab, origin } = await activecontext();
15042
+ const executed = await runstep({ step, scopes: await memory.getrunscopes(stored.run.id), outputs: {}, execute: async (dispatched) => dispatchworkflowstep(dispatched, { session, plan, tabid: tab.id, origin }), now: Date.now(), ...step.block !== void 0 ? { block: step.block } : {} });
15043
+ await memory.addrunlogentry(stored.run.id, executed.log);
15044
+ await memory.setrunscopes(stored.run.id, executed.scopes);
15045
+ const stepindex = record2.steps.findIndex((entry) => entry.id === step.id);
15046
+ const isthenext = stepindex === stored.run.cursor && executed.output.ok;
15047
+ if (isthenext) await memory.setworkflowrun({ ...stored.run, cursor: stored.run.cursor + 1 });
15048
+ await audit("workflow", `Executed the single step ${step.label} of the run ${stored.run.id} outside the run loop${isthenext ? " and advanced its checkpoint" : ""}.`, { sessionid: session.id, planid: plan.id, stepid: step.id });
15049
+ return workflowoutcome({ run: { ...stored.run, ...isthenext ? { cursor: stored.run.cursor + 1 } : {} }, entries: await memory.getrunlog(stored.run.id), stepid: step.id });
15050
+ }
15051
+ case "pauseworkflowrun": {
15052
+ const inputpause = message;
15053
+ const stored = (await memory.listworkflowruns()).find((entry) => entry.id === (inputpause.runid ?? ""));
15054
+ if (!stored) throw new Error(`No workflow run matches ${inputpause.runid ?? ""}.`);
15055
+ const guards = activeworkflowruns.get(stored.id);
15056
+ if (guards) guards.cancelled = true;
15057
+ const paused = pauserun(stored, Date.now());
15058
+ await memory.setworkflowrun(paused);
15059
+ await audit("workflow", `Paused the workflow run ${paused.id} at the checkpoint of step cursor ${paused.cursor}; the resume continues exactly there.`, {});
15060
+ await refreshbadge();
15061
+ return { runid: paused.id, state: paused.state, cursor: paused.cursor };
15062
+ }
15063
+ case "resumeworkflowrun": {
15064
+ const inputresume = message;
15065
+ const session = await memory.getsession();
15066
+ const plan = await memory.getplan();
15067
+ if (!session || !plan || plan.state !== "approved") throw new Error("The run resume needs an approved session plan.");
15068
+ const stored = await memory.getrun(inputresume.runid?.trim() ?? "");
15069
+ if (!stored) throw new Error(`No workflow run matches ${inputresume.runid ?? ""}.`);
15070
+ if (stored.run.state !== "paused" && stored.run.state !== "running") throw new Error(`The workflow run is already ${stored.run.state}.`);
15071
+ const record2 = await memory.getworkflowrecord(stored.run.workflowid);
15072
+ if (!record2) throw new Error("The workflow of the run is no longer composed in the library.");
15073
+ const scopes = await memory.getrunscopes(stored.run.id);
15074
+ const log = await memory.getrunlog(stored.run.id);
15075
+ const { tab, origin } = await activecontext();
15076
+ const resumed = await runworkflow({ record: record2, run: stored.run, scopes, log, execute: async (dispatched) => dispatchworkflowstep(dispatched, { session, plan, tabid: tab.id, origin }), now: Date.now(), gates: { sessionactive: Boolean(session && !session.stoppedat && !session.pausedat && session.expiresat > Date.now()), planapproved: plan.state === "approved", origingranted: (workfloworigin) => origingranted(session, workfloworigin) }, oncheckpoint: async (state) => {
15077
+ await memory.setworkflowrun(state.run);
15078
+ const last = state.log[state.log.length - 1];
15079
+ if (last) await memory.addrunlogentry(state.run.id, last);
15080
+ await memory.setrunscopes(state.run.id, state.scopes);
15081
+ } });
15082
+ await memory.setworkflowrun(resumed.run);
15083
+ await memory.setrunscopes(stored.run.id, resumed.scopes);
15084
+ await audit("workflow", `Resumed the workflow run ${stored.run.id} from the checkpoint at step cursor ${stored.run.cursor}; the run ended ${resumed.run.state} at cursor ${resumed.run.cursor}.`, { sessionid: session.id, planid: plan.id });
15085
+ await refreshbadge();
15086
+ return { runid: resumed.run.id, state: resumed.run.state, cursor: resumed.run.cursor };
15087
+ }
15088
+ case "cancelworkflowrun": {
15089
+ const inputcancel = message;
15090
+ const stored = (await memory.listworkflowruns()).find((entry) => entry.id === (inputcancel.runid ?? ""));
15091
+ if (!stored) throw new Error(`No workflow run matches ${inputcancel.runid ?? ""}.`);
15092
+ const guards = activeworkflowruns.get(stored.id);
15093
+ if (guards) guards.cancelled = true;
15094
+ const cancelled = cancelrun(stored, typeof inputcancel.reason === "string" && inputcancel.reason.trim() ? inputcancel.reason.trim() : "user cancel", Date.now());
15095
+ await memory.setworkflowrun(cancelled);
15096
+ await audit("workflow", `Cancelled the workflow run ${cancelled.id} with the reason ${cancelled.cancelreason ?? "user cancel"} at step cursor ${cancelled.cursor}.`, {});
15097
+ await refreshbadge();
15098
+ return { runid: cancelled.id, state: cancelled.state, cancelreason: cancelled.cancelreason };
15099
+ }
15100
+ case "workflowoutcome": {
15101
+ const inputoutcome = message;
15102
+ const stored = await memory.getrun(inputoutcome.runid?.trim() ?? "");
15103
+ if (!stored) throw new Error(`No workflow run matches ${inputoutcome.runid ?? ""}.`);
15104
+ return workflowoutcome({ run: stored.run, entries: stored.log, ...inputoutcome.stepid !== void 0 && inputoutcome.stepid !== "" ? { stepid: inputoutcome.stepid } : {} });
15105
+ }
13198
15106
  default:
13199
15107
  throw new Error("Unknown Devthink request.");
13200
15108
  }
@@ -13203,6 +15111,75 @@ chrome.runtime.onMessage.addListener((message, sender, sendresponse) => {
13203
15111
  handlerequest(message, sender).then((value) => sendresponse({ ok: true, value })).catch((error) => sendresponse({ ok: false, error: error instanceof Error ? error.message : String(error) }));
13204
15112
  return true;
13205
15113
  });
15114
+ async function detectcrash() {
15115
+ const plan = await memory.getplan();
15116
+ if (!plan || plan.state !== "approved") return;
15117
+ const state = await memory.gettaskstate(plan.id);
15118
+ if (!state || !taskstatevalid(state)) return;
15119
+ const marked = crashinterrupted(state, plan.steps.length, Date.now());
15120
+ if (marked === state || marked === void 0) return;
15121
+ await memory.settaskstate(marked);
15122
+ await memory.setcrashflag(true);
15123
+ await memory.addsessionevent({ id: randomid(), kind: "crash", at: Date.now(), detail: `Run ${plan.id} was interrupted by a browser restart at step cursor ${state.stepcursor} of ${plan.steps.length}.` });
15124
+ await audit("session", `The crash detector marked the run ${plan.id} interrupted by a browser restart at step cursor ${state.stepcursor} of ${plan.steps.length}; the crash restore prompt stays inside the session consent model.`, { planid: plan.id });
15125
+ await refreshbadge().catch(() => {
15126
+ });
15127
+ }
15128
+ chrome.runtime.onStartup.addListener(() => {
15129
+ void detectcrash();
15130
+ void pauseinterruptedworkflowruns();
15131
+ });
15132
+ async function pauseinterruptedworkflowruns() {
15133
+ for (const run of await memory.listworkflowruns()) {
15134
+ if (run.state !== "running") continue;
15135
+ await memory.setworkflowrun({ ...run, state: "paused", pausedat: Date.now() });
15136
+ await audit("workflow", `The service worker restart paused the workflow run ${run.id} at its last checkpoint of step cursor ${run.cursor}; the resume continues exactly there.`, {});
15137
+ }
15138
+ }
15139
+ async function maybeautosnapshot() {
15140
+ const state = await memory.getautosnapshot();
15141
+ if (!state || !Number.isFinite(state.interval.period)) return;
15142
+ const now = Date.now();
15143
+ if (now - state.lastat < state.interval.period) return;
15144
+ const session = await memory.getsession();
15145
+ const plan = await memory.getplan();
15146
+ if (!session || session.stoppedat || session.expiresat <= now || session.pausedat || !plan || plan.state !== "approved") return;
15147
+ const step = plan.steps.find((candidate) => candidate.kind === "capturesession");
15148
+ if (!step) return;
15149
+ const autorecords = (await memory.getsessionrecords()).filter((record3) => record3.auto);
15150
+ if (autorecords.length >= state.interval.maxsnapshots) {
15151
+ await memory.clearautosnapshot();
15152
+ await audit("session", `The reviewed auto snapshot interval stopped after ${state.interval.maxsnapshots} snapshot${state.interval.maxsnapshots === 1 ? "" : "s"}; the retention window of ${state.interval.expiry} millisecond${state.interval.expiry === 1 ? "" : "s"} expires them by user choice.`, { sessionid: session.id, planid: plan.id });
15153
+ return;
15154
+ }
15155
+ const options = stepoptions2(step);
15156
+ const snapshot2 = snapshotplanof(options.snapshot);
15157
+ if (!snapshot2) return;
15158
+ const record2 = await capturesessionrecord({ ...snapshot2, ...snapshot2.auto !== void 0 ? { auto: snapshot2.auto } : {} }, session, plan.id).catch(() => void 0);
15159
+ if (!record2) return;
15160
+ const auto = { ...record2, auto: true };
15161
+ await memory.addsessionrecord(auto);
15162
+ await memory.setautosnapshot({ interval: state.interval, lastat: now, count: state.count + 1 });
15163
+ await memory.addsessionevent({ id: randomid(), kind: "auto", at: now, tabid: session.tabid, detail: `Auto snapshot ${record2.id} captured ${record2.tabs.length} tabs on the reviewed interval.` });
15164
+ await memory.applysessionexpiry(state.interval.expiry > 0 ? state.interval.expiry : snapshotretentionwindow(await memory.getsettings()), now);
15165
+ await audit("session", `The reviewed auto snapshot interval of ${state.interval.period} milliseconds captured the session record ${record2.id} of ${record2.tabs.length} tab${record2.tabs.length === 1 ? "" : "s"}; snapshot ${state.count + 1} of the reviewed maximum of ${state.interval.maxsnapshots}.`, { sessionid: session.id, planid: plan.id });
15166
+ }
15167
+ setInterval(() => {
15168
+ void maybeautosnapshot().catch(() => {
15169
+ });
15170
+ }, 3e4);
15171
+ function schedulewakes() {
15172
+ const alarms = chrome.alarms;
15173
+ try {
15174
+ alarms?.create("devthinkautosnapshot", { periodInMinutes: 1 });
15175
+ alarms?.onAlarm?.addListener(() => {
15176
+ void maybeautosnapshot().catch(() => {
15177
+ });
15178
+ });
15179
+ } catch {
15180
+ }
15181
+ }
15182
+ schedulewakes();
13206
15183
  async function reconcilewatches() {
13207
15184
  for (const watch of await memory.getwatches()) {
13208
15185
  if (watch.closedat !== void 0) continue;