@wenathlan/extension 1.1.36 → 1.1.38

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.
@@ -435,20 +435,208 @@ var sessionmemory = class {
435
435
  async setcontroltab(state) {
436
436
  return this.adapter.set("controltab", state);
437
437
  }
438
+ /** Stores one saved form profile under its reviewed name, replacing the previous profile of that name. */
439
+ async setprofile(profile) {
440
+ const records = (await this.getprofiles()).filter((item) => item.name !== profile.name);
441
+ await this.adapter.set("formprofiles", [profile, ...records]);
442
+ }
443
+ /** Returns one saved form profile by its reviewed name. */
444
+ async getprofile(name) {
445
+ return (await this.getprofiles()).find((item) => item.name === name);
446
+ }
447
+ /** Returns every saved form profile with its origin grants, newest first. */
448
+ async getprofiles() {
449
+ return await this.adapter.get("formprofiles") ?? [];
450
+ }
451
+ /** Removes one saved form profile by its reviewed name. */
452
+ async removeprofile(name) {
453
+ const records = (await this.getprofiles()).filter((item) => item.name !== name);
454
+ await this.adapter.set("formprofiles", records);
455
+ }
456
+ /** Records one wizard state with its step history. */
457
+ async addwizard(state) {
458
+ const records = await this.getwizards();
459
+ await this.adapter.set("wizards", [state, ...records]);
460
+ }
461
+ /** Returns every stored wizard state with its step history, newest first. */
462
+ async getwizards() {
463
+ return await this.adapter.get("wizards") ?? [];
464
+ }
465
+ /** Stores one submission ticket with its values hash, replacing the previous ticket of that id. */
466
+ async setticket(ticket) {
467
+ const records = (await this.gettickets()).filter((item) => item.id !== ticket.id);
468
+ await this.adapter.set("submittickets", [ticket, ...records]);
469
+ }
470
+ /** Returns every stored submission ticket with its values hash, newest first. */
471
+ async gettickets() {
472
+ return await this.adapter.get("submittickets") ?? [];
473
+ }
474
+ /** Records one collected error report for correction loops. */
475
+ async adderrorreport(report) {
476
+ const records = await this.geterrorreports();
477
+ await this.adapter.set("errorreports", [report, ...records]);
478
+ }
479
+ /** Returns every stored error report, newest first. */
480
+ async geterrorreports() {
481
+ return await this.adapter.get("errorreports") ?? [];
482
+ }
483
+ /** Records one typeahead pick observed when a reviewed suggestion entry was chosen. */
484
+ async addpick(pick) {
485
+ const records = await this.getpicks();
486
+ await this.adapter.set("typeaheadpicks", [pick, ...records]);
487
+ }
488
+ /** Returns every recorded typeahead pick, newest first. */
489
+ async getpicks() {
490
+ return await this.adapter.get("typeaheadpicks") ?? [];
491
+ }
492
+ /** Records one captcha handoff while the plan waits for the user. */
493
+ async addcaptcha(handoff) {
494
+ const records = await this.getcaptchas();
495
+ await this.adapter.set("captchas", [handoff, ...records]);
496
+ }
497
+ /** Returns every captcha handoff record with its resolution state, newest first. */
498
+ async getcaptchas() {
499
+ return await this.adapter.get("captchas") ?? [];
500
+ }
501
+ /** Resolves one captcha handoff by id once the user finished it. */
502
+ async resolvecaptcha(id, resolvedat) {
503
+ const records = await this.getcaptchas();
504
+ await this.adapter.set("captchas", records.map((handoff) => handoff.id === id && !handoff.resolved ? { ...handoff, resolved: true, resolvedat } : handoff));
505
+ }
506
+ /** Records one login or template detection for its origin. */
507
+ async adddetection(record2) {
508
+ const records = await this.getdetections();
509
+ await this.adapter.set("detections", [record2, ...records]);
510
+ }
511
+ /** Returns every stored login and template detection per origin, newest first. */
512
+ async getdetections() {
513
+ return await this.adapter.get("detections") ?? [];
514
+ }
515
+ /** Stores the reviewed one time code behind the consent gate of an active session. */
516
+ async setcodevalue(value) {
517
+ return this.adapter.set("codevalue", value);
518
+ }
519
+ /** Returns the reviewed one time code, if the user stored one behind the consent gate. */
520
+ async getcodevalue() {
521
+ return this.adapter.get("codevalue");
522
+ }
523
+ /** Stores one dataset with its column specs and rows, replacing the previous record of that id. */
524
+ async setdataset(value) {
525
+ await this.adapter.set(`dataset${value.id}`, value);
526
+ const ids = (await this.adapter.get("datasets") ?? []).filter((id) => id !== value.id);
527
+ await this.adapter.set("datasets", [value.id, ...ids]);
528
+ }
529
+ /** Returns one stored dataset by its id with its column specs and row count. */
530
+ async getdataset(id) {
531
+ return this.adapter.get(`dataset${id}`);
532
+ }
533
+ /** Returns every stored dataset id, newest first. */
534
+ async getdatasets() {
535
+ const ids = await this.adapter.get("datasets") ?? [];
536
+ const records = [];
537
+ for (const id of ids) {
538
+ const record2 = await this.adapter.get(`dataset${id}`);
539
+ if (record2) records.push(record2);
540
+ }
541
+ return records;
542
+ }
543
+ /** Stores one imported csv dataset for fill loops beside the dataset store. */
544
+ async addimport(value) {
545
+ await this.setdataset(value);
546
+ const ids = (await this.adapter.get("imports") ?? []).filter((id) => id !== value.id);
547
+ await this.adapter.set("imports", [value.id, ...ids]);
548
+ }
549
+ /** Returns every imported csv dataset for fill loops, newest first. */
550
+ async getimports() {
551
+ const ids = await this.adapter.get("imports") ?? [];
552
+ const records = [];
553
+ for (const id of ids) {
554
+ const record2 = await this.adapter.get(`dataset${id}`);
555
+ if (record2) records.push(record2);
556
+ }
557
+ return records;
558
+ }
559
+ /** Stores one extraction session with its cursor and page history, replacing the previous session of that id. */
560
+ async setextractsession(value) {
561
+ await this.adapter.set(`extract${value.id}`, value);
562
+ const ids = (await this.adapter.get("extracts") ?? []).filter((id) => id !== value.id);
563
+ await this.adapter.set("extracts", [value.id, ...ids]);
564
+ }
565
+ /** Returns every extraction session with its cursor and page history, newest first. */
566
+ async getextractsessions() {
567
+ const ids = await this.adapter.get("extracts") ?? [];
568
+ const records = [];
569
+ for (const id of ids) {
570
+ const record2 = await this.adapter.get(`extract${id}`);
571
+ if (record2) records.push(record2);
572
+ }
573
+ return records;
574
+ }
575
+ /** Records one provenance record of an exported artifact. */
576
+ async addprovenance(record2) {
577
+ const records = await this.getprovenances();
578
+ await this.adapter.set("provenances", [record2, ...records]);
579
+ }
580
+ /** Returns every provenance record per exported artifact, newest first. */
581
+ async getprovenances() {
582
+ return await this.adapter.get("provenances") ?? [];
583
+ }
584
+ /** Stores the transform rules and dedupe keys of one task, replacing the previous record of that task. */
585
+ async settaskrules(value) {
586
+ const records = (await this.adapter.get("taskrules") ?? []).filter((item) => item.taskid !== value.taskid);
587
+ await this.adapter.set("taskrules", [value, ...records]);
588
+ }
589
+ /** Returns the transform rules and dedupe keys per task, newest first. */
590
+ async gettaskrules() {
591
+ return await this.adapter.get("taskrules") ?? [];
592
+ }
593
+ /** Stores one stream chunk state for resume, replacing the previous state of that dataset. */
594
+ async setstream(state) {
595
+ const records = (await this.adapter.get("streams") ?? []).filter((item) => item.datasetid !== state.datasetid);
596
+ await this.adapter.set("streams", [state, ...records]);
597
+ }
598
+ /** Returns every stream chunk state persisted for resume, newest first. */
599
+ async getstreams() {
600
+ return await this.adapter.get("streams") ?? [];
601
+ }
602
+ /** Stores one reviewed sheet endpoint config behind its origin grant, replacing the previous config of that origin. */
603
+ async setsheetendpoint(config) {
604
+ const records = (await this.adapter.get("sheetendpoints") ?? []).filter((item) => item.origin !== config.origin);
605
+ await this.adapter.set("sheetendpoints", [config, ...records]);
606
+ }
607
+ /** Returns every reviewed sheet endpoint config, newest first. */
608
+ async getsheetendpoints() {
609
+ return await this.adapter.get("sheetendpoints") ?? [];
610
+ }
611
+ /** Records one exported data artifact; artifact retention is a user setting and an absent setting keeps every artifact. */
612
+ async addexport(artifact) {
613
+ const records = await this.getexports();
614
+ const combined = [artifact, ...records.filter((item) => item.id !== artifact.id)];
615
+ const retention = (await this.getsettings())?.artifactretention;
616
+ await this.adapter.set("exports", retention === void 0 ? combined : combined.slice(0, retention));
617
+ }
618
+ /** Returns every exported data artifact with its content and checksum, newest first. */
619
+ async getexports() {
620
+ return await this.adapter.get("exports") ?? [];
621
+ }
438
622
  };
439
623
  function randomid() {
440
624
  return crypto.randomUUID();
441
625
  }
442
626
 
443
627
  // policy.ts
444
- 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"]);
628
+ 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"]);
445
629
  var interactionactions = /* @__PURE__ */ new Set(["focus", "scroll", "hover", "clickdeep", "rightclick", "doubleclick", "scrollpage", "scrollby", "scrollend", "scrolltop", "fullscreen", "zoomset", "movepointer", "clicktext", "clickaria", "clickname", "expanddetails", "pierceshadow", "retryaction"]);
446
- 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"]);
630
+ 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"]);
447
631
  var allowedactions = /* @__PURE__ */ new Set([...sensitiveactions, ...interactionactions, ...readactions]);
448
632
  var watchactions = /* @__PURE__ */ new Set(["watchmutate", "watchbanner", "watchfocus", "watchtab"]);
449
- 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"]);
450
- var valueactions = /* @__PURE__ */ new Set(["presskey", "drag", "drop", "upload", "readattribute", "removeattribute", "waittext", "evaluate", "zoomset", "tabactivate", "tabclose", "tabreload", "windowclose", "windowresize", "tabcreate", "windowcreate", "downloadfile", "typetime", "appendtext", "setvalue", "typeedit", "keyhold", "keyrelease", "chooseradio", "setslider", "setdate", "setcolor", "followlink", "setfragment", "handleauth", "navintent", "openclipboard", "checksafe", "reopentab", "spanav", "duplicatetab", "pintab", "mutetab", "movetab", "movetabwindow", "searchtabs", "badgetab", "attachmeta", "focuswindow", "maximizewindow", "minimizewindow", "restorewindow", "incognitowindow"]);
633
+ 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"]);
634
+ var valueactions = /* @__PURE__ */ new Set(["presskey", "drag", "drop", "upload", "readattribute", "removeattribute", "waittext", "evaluate", "zoomset", "tabactivate", "tabclose", "tabreload", "windowclose", "windowresize", "tabcreate", "windowcreate", "downloadfile", "typetime", "appendtext", "setvalue", "typeedit", "keyhold", "keyrelease", "chooseradio", "setslider", "setdate", "setcolor", "followlink", "setfragment", "handleauth", "navintent", "openclipboard", "checksafe", "reopentab", "spanav", "duplicatetab", "pintab", "mutetab", "movetab", "movetabwindow", "searchtabs", "badgetab", "attachmeta", "focuswindow", "maximizewindow", "minimizewindow", "restorewindow", "incognitowindow", "asksubmit", "selectchain", "picktypeahead", "pickdate", "attachfile", "fillcode", "consentpassword"]);
451
635
  var tabscommandactions = /* @__PURE__ */ new Set(["querytabs", "duplicatetab", "closepattern", "pintab", "mutetab", "movetab", "movetabwindow", "grouptabs", "colorgroup", "collapsegroup", "discardtab", "reloadtabs", "zoomin", "zoomout", "watchtab", "switchtab", "maximizewindow", "minimizewindow", "restorewindow", "focuswindow", "scratchwindow", "incognitowindow", "restoretab", "savelayout", "restorelayout", "findclones", "searchtabs", "badgetab", "attachmeta", "listaudio", "reopenrun", "snapshotsession"]);
636
+ var formactions = /* @__PURE__ */ new Set(["fillform", "filllabel", "fillplaceholder", "detectfields", "generatevalues", "saveprofiles", "asksubmit", "submitform", "readerrors", "retryform", "runwizard", "selectchain", "picktypeahead", "pickdate", "attachfile", "handoffcaptcha", "fillcard", "fillcode", "consentpassword", "skiphoneypot", "detectlogin", "detecttemplate"]);
637
+ var datasetactions = /* @__PURE__ */ new Set(["scrapetable", "exportcsv", "exportjson", "exportexcel", "copytable", "pushsheets", "importcsv", "looprows", "transformvalues", "deduperows", "paginateextract", "mergepages", "stamplerows", "previewgrid", "streamdisk", "resumeextract", "logprovenance"]);
638
+ var exportactions = /* @__PURE__ */ new Set(["exportcsv", "exportjson", "exportexcel", "copytable", "pushsheets", "streamdisk"]);
639
+ var fieldkinds = ["text", "email", "phone", "date", "number", "select", "check", "radio", "file", "password", "card", "code"];
452
640
  var layoutmutationactions = /* @__PURE__ */ new Set(["grouptabs", "colorgroup", "collapsegroup", "savelayout", "restorelayout"]);
453
641
  var groupcolors = ["grey", "blue", "red", "yellow", "green", "pink", "purple", "cyan", "orange"];
454
642
  function normalizeendpoint(value) {
@@ -482,6 +670,7 @@ function requiredcapability(kind) {
482
670
  if (kind === "tablist") return "tabs";
483
671
  if (kind === "downloadfile") return "downloads";
484
672
  if (kind === "openclipboard") return "clipboardRead";
673
+ if (kind === "copytable") return "clipboardWrite";
485
674
  if (kind === "openlink" || kind === "openprivate" || kind === "navlist" || kind === "batchopen" || kind === "reopentab" || kind === "deeplink") return "tabs";
486
675
  if (tabscommandactions.has(kind)) return "tabs";
487
676
  return void 0;
@@ -492,6 +681,240 @@ function istabscommandkind(kind) {
492
681
  function islayoutkind(kind) {
493
682
  return layoutmutationactions.has(kind);
494
683
  }
684
+ function isformkind(kind) {
685
+ return formactions.has(kind);
686
+ }
687
+ function isdatasetkind(kind) {
688
+ return datasetactions.has(kind);
689
+ }
690
+ function isexportkind(kind) {
691
+ return exportactions.has(kind);
692
+ }
693
+ function exportgranted(session, origin) {
694
+ if (!origingranted(session, origin)) return { allowed: false, reason: `The export of extracted data from ${origin} needs the session origin grants before it leaves local memory.` };
695
+ return { allowed: true };
696
+ }
697
+ function validatefieldmatch(value) {
698
+ if (!value || typeof value !== "object" || Array.isArray(value)) return { allowed: false, reason: "A reviewed field match is required in options." };
699
+ const match = value;
700
+ if (match.mode !== "label" && match.mode !== "placeholder" && match.mode !== "arialabel" && match.mode !== "name") return { allowed: false, reason: "The reviewed field match mode must be label, placeholder, arialabel or name." };
701
+ const key = match.mode === "label" ? "label" : match.mode === "placeholder" ? "placeholder" : match.mode === "arialabel" ? "arialabel" : "name";
702
+ if (!isnonempty(match[key])) return { allowed: false, reason: `The reviewed ${match.mode} field match needs a non-empty ${key}.` };
703
+ return { allowed: true };
704
+ }
705
+ function validateformrecord(value) {
706
+ if (!value || typeof value !== "object" || Array.isArray(value)) return { allowed: false, reason: "A reviewed form record with entries is required in options." };
707
+ const record2 = value;
708
+ if (record2.form !== void 0 && !isnonempty(record2.form)) return { allowed: false, reason: "The reviewed form record form selector must be a non-empty string." };
709
+ if (!Array.isArray(record2.entries) || record2.entries.length === 0) return { allowed: false, reason: "The reviewed form record needs a non-empty list of entries." };
710
+ for (const item of record2.entries) {
711
+ if (!item || typeof item !== "object" || Array.isArray(item)) return { allowed: false, reason: "Every reviewed form record entry must be an object." };
712
+ const entry = item;
713
+ const matchcheck = validatefieldmatch(entry.match);
714
+ if (!matchcheck.allowed) return matchcheck;
715
+ if (typeof entry.kind !== "string" || !fieldkinds.includes(entry.kind)) return { allowed: false, reason: "Every reviewed form record entry needs a known field kind." };
716
+ if (typeof entry.value !== "string") return { allowed: false, reason: "Every reviewed form record entry needs a string value." };
717
+ if (entry.kind === "password") return { allowed: false, reason: "Password entries are refused inside form records; use consentpassword with a reviewed consent ref." };
718
+ }
719
+ return { allowed: true };
720
+ }
721
+ function validatevaluegen(value) {
722
+ if (!value || typeof value !== "object" || Array.isArray(value)) return { allowed: false, reason: "A reviewed valuegen rule with a field kind is required in options." };
723
+ const rule = value;
724
+ if (typeof rule.kind !== "string" || !fieldkinds.includes(rule.kind)) return { allowed: false, reason: "The reviewed valuegen kind must be a known field kind." };
725
+ if (rule.locale !== void 0 && !isnonempty(rule.locale)) return { allowed: false, reason: "The reviewed valuegen locale must be a non-empty string." };
726
+ if (rule.seed !== void 0 && (typeof rule.seed !== "number" || !Number.isFinite(rule.seed))) return { allowed: false, reason: "The reviewed valuegen seed must be a finite number." };
727
+ return { allowed: true };
728
+ }
729
+ function validatefieldpairs(options, mode) {
730
+ const pairs = options.fields;
731
+ if (!Array.isArray(pairs) || pairs.length === 0) return { allowed: false, reason: "A reviewed non-empty list of field pairs is required in options." };
732
+ for (const item of pairs) {
733
+ if (!item || typeof item !== "object" || Array.isArray(item)) return { allowed: false, reason: "Every reviewed field pair must be an object." };
734
+ const pair = item;
735
+ if (!isnonempty(pair[mode])) return { allowed: false, reason: `Every reviewed field pair needs a non-empty ${mode}.` };
736
+ if (typeof pair.value !== "string" || !pair.value.trim()) return { allowed: false, reason: "Every reviewed field pair needs a non-empty value." };
737
+ }
738
+ return { allowed: true };
739
+ }
740
+ function validatecardsegments(value) {
741
+ if (!Array.isArray(value) || value.length === 0) return { allowed: false, reason: "A reviewed non-empty list of card segments is required in options." };
742
+ for (const item of value) {
743
+ if (!item || typeof item !== "object" || Array.isArray(item)) return { allowed: false, reason: "Every reviewed card segment must be an object." };
744
+ const segment = item;
745
+ const matchcheck = validatefieldmatch(segment.match);
746
+ if (!matchcheck.allowed) return matchcheck;
747
+ if (typeof segment.value !== "string" || !segment.value.trim()) return { allowed: false, reason: "Every reviewed card segment needs a non-empty value." };
748
+ }
749
+ return { allowed: true };
750
+ }
751
+ function validateformgrammar(step, options) {
752
+ const kind = step.kind;
753
+ if (kind === "fillform" || kind === "saveprofiles" && options.formrecord !== void 0) {
754
+ const recordcheck = validateformrecord(options.formrecord);
755
+ if (!recordcheck.allowed) return recordcheck;
756
+ }
757
+ if (kind === "filllabel" || kind === "fillplaceholder") {
758
+ const paircheck = validatefieldpairs(options, kind === "filllabel" ? "label" : "placeholder");
759
+ if (!paircheck.allowed) return paircheck;
760
+ }
761
+ if (kind === "generatevalues" && options.valuegen !== void 0) {
762
+ const rulecheck = validatevaluegen(options.valuegen);
763
+ if (!rulecheck.allowed) return rulecheck;
764
+ }
765
+ if (kind === "saveprofiles" && !isnonempty(options.name)) return { allowed: false, reason: "A reviewed profile name is required in options." };
766
+ if (kind === "submitform" && !isnonempty(options.consentref)) return { allowed: false, reason: "A reviewed consent ref of an approved asksubmit ticket is required in options." };
767
+ if (kind === "retryform") {
768
+ const backoff = options.backoff;
769
+ if (!backoff || typeof backoff !== "object" || Array.isArray(backoff)) return { allowed: false, reason: "A reviewed backoff rule with wait and factor is required in options." };
770
+ const rule = backoff;
771
+ if (typeof rule.wait !== "number" || !Number.isFinite(rule.wait) || rule.wait <= 0) return { allowed: false, reason: "The reviewed retry backoff wait must be a positive number of milliseconds with no code ceiling." };
772
+ if (typeof rule.factor !== "number" || !Number.isFinite(rule.factor) || rule.factor < 1) return { allowed: false, reason: "The reviewed retry backoff factor must be one or greater with no code ceiling." };
773
+ if (options.attempts !== void 0 && (typeof options.attempts !== "number" || !Number.isInteger(options.attempts) || options.attempts < 1)) return { allowed: false, reason: "The reviewed retry attempts must be a positive integer with no code ceiling." };
774
+ }
775
+ if (kind === "runwizard" && options.steps !== void 0 && (typeof options.steps !== "number" || !Number.isInteger(options.steps) || options.steps < 1)) return { allowed: false, reason: "The reviewed wizard step count must be a positive integer with no code ceiling." };
776
+ if (kind === "selectchain") {
777
+ if (!isnonempty(options.child)) return { allowed: false, reason: "A reviewed child selector of the dependent control is required in options." };
778
+ if (!nonnegativeoption(options, "wait")) return { allowed: false, reason: "The reviewed dependent wait must be zero or a positive number of milliseconds." };
779
+ }
780
+ if (kind === "picktypeahead") {
781
+ if (!isnonempty(options.pick)) return { allowed: false, reason: "A reviewed suggestion entry to pick is required in options." };
782
+ if (!nonnegativeoption(options, "timeout")) return { allowed: false, reason: "The reviewed typeahead timeout must be zero or a positive number of milliseconds." };
783
+ }
784
+ if (kind === "pickdate" && !/^\d{4}-\d{2}-\d{2}$/.test(step.value ?? "")) return { allowed: false, reason: "The reviewed date must use the yyyy-mm-dd form." };
785
+ if (kind === "fillcard") {
786
+ const segmentcheck = validatecardsegments(options.segments);
787
+ if (!segmentcheck.allowed) return segmentcheck;
788
+ if (!nonnegativeoption(options, "pause")) return { allowed: false, reason: "The reviewed card typing pause must be zero or a positive number of milliseconds." };
789
+ }
790
+ if (kind === "fillcode" && !isnonempty(options.source)) return { allowed: false, reason: "A reviewed one time code source is required in options." };
791
+ if (kind === "consentpassword" && !isnonempty(options.consentref)) return { allowed: false, reason: "A reviewed consent ref is required in options before any password is filled." };
792
+ return { allowed: true };
793
+ }
794
+ function validatetransformrule(value) {
795
+ if (!value || typeof value !== "object" || Array.isArray(value)) return { allowed: false, reason: "A reviewed transform rule with an expression, sources and a target is required in options." };
796
+ const rule = value;
797
+ const expression = rule.expression;
798
+ if (typeof expression !== "string" || !/^(trim|upper|lower|number|prefix|suffix|replace)(?::.+)?$/.test(expression)) return { allowed: false, reason: "The reviewed transform expression must be trim, upper, lower, number, prefix, suffix or replace with an optional argument." };
799
+ if (expression.startsWith("replace") && !expression.slice("replace".length).includes("=>")) return { allowed: false, reason: "The reviewed replace expression needs the from=>to separator." };
800
+ if (expression.startsWith("replace") && expression.slice("replace:".length).split("=>")[0] === "") return { allowed: false, reason: "The reviewed replace expression needs a non-empty from part." };
801
+ if (!Array.isArray(rule.sources) || rule.sources.length === 0 || !rule.sources.every((source) => isnonempty(source))) return { allowed: false, reason: "Every reviewed transform rule needs a non-empty list of source columns." };
802
+ if (!isnonempty(rule.target)) return { allowed: false, reason: "Every reviewed transform rule needs a non-empty target column." };
803
+ return { allowed: true };
804
+ }
805
+ function validatedatasetids(options, key) {
806
+ const ids = options[key];
807
+ if (!Array.isArray(ids) || ids.length === 0 || !ids.every((id) => isnonempty(id))) return { allowed: false, reason: `A reviewed non-empty list of dataset ids is required in options as ${key}.` };
808
+ return { allowed: true };
809
+ }
810
+ function validatedatagrammar(step, options, origin) {
811
+ const kind = step.kind;
812
+ if (kind === "scrapetable") {
813
+ if (options.name !== void 0 && !isnonempty(options.name)) return { allowed: false, reason: "The reviewed dataset name must be a non-empty string." };
814
+ if (options.rowlimit !== void 0 && (typeof options.rowlimit !== "number" || !Number.isInteger(options.rowlimit) || options.rowlimit < 1)) return { allowed: false, reason: "The reviewed row limit must be a positive integer with no code ceiling." };
815
+ }
816
+ if (kind === "paginateextract") {
817
+ if (!isnonempty(options.next)) return { allowed: false, reason: "A reviewed next control selector is required in options." };
818
+ if (options.pages !== void 0 && (typeof options.pages !== "number" || !Number.isInteger(options.pages) || options.pages < 1)) return { allowed: false, reason: "The reviewed page count must be a positive integer with no code ceiling." };
819
+ if (!nonnegativeoption(options, "wait")) return { allowed: false, reason: "The reviewed row freshness wait must be zero or a positive number of milliseconds." };
820
+ }
821
+ if (kind === "exportcsv" || kind === "exportjson" || kind === "exportexcel" || kind === "copytable" || kind === "streamdisk") {
822
+ if (!isnonempty(options.dataset)) return { allowed: false, reason: "A reviewed dataset id is required in options." };
823
+ if (options.name !== void 0 && !isnonempty(options.name)) return { allowed: false, reason: "The reviewed artifact name must be a non-empty string." };
824
+ }
825
+ if (kind === "exportcsv" && options.delimiter !== void 0 && (typeof options.delimiter !== "string" || options.delimiter.length !== 1)) return { allowed: false, reason: "The reviewed csv delimiter must be a single character." };
826
+ if (kind === "streamdisk" && (typeof options.chunk !== "number" || !Number.isInteger(options.chunk) || options.chunk < 1)) return { allowed: false, reason: "The reviewed streaming chunk size must be a positive integer with no code ceiling." };
827
+ if (kind === "pushsheets") {
828
+ if (!isnonempty(options.dataset)) return { allowed: false, reason: "A reviewed dataset id is required in options." };
829
+ if (!isnonempty(options.sheet)) return { allowed: false, reason: "A reviewed sheet endpoint url is required in options." };
830
+ if (!ishttpsurl(options.sheet)) return { allowed: false, reason: "The reviewed sheet endpoint url must use HTTPS." };
831
+ if (options.reviewed !== true) return { allowed: false, reason: "The sheet push needs the explicit reviewed flag before any data leaves local memory." };
832
+ }
833
+ if (kind === "importcsv") {
834
+ if (typeof options.csv !== "string" || !options.csv.trim()) return { allowed: false, reason: "Reviewed csv content is required in options." };
835
+ if (options.name !== void 0 && !isnonempty(options.name)) return { allowed: false, reason: "The reviewed dataset name must be a non-empty string." };
836
+ if (options.mapping !== void 0) {
837
+ const mapping = options.mapping;
838
+ if (!mapping || typeof mapping !== "object" || Array.isArray(mapping) || !Object.values(mapping).every((item) => typeof item === "string")) return { allowed: false, reason: "The reviewed csv column mapping must be an object of string values." };
839
+ }
840
+ }
841
+ if (kind === "looprows") {
842
+ if (!isnonempty(options.dataset)) return { allowed: false, reason: "A reviewed dataset id is required in options." };
843
+ if (options.variable !== void 0 && !isnonempty(options.variable)) return { allowed: false, reason: "The reviewed row variable name must be a non-empty string." };
844
+ const inner = validateinnerstep(options, origin);
845
+ if (!inner.allowed) return inner;
846
+ }
847
+ if (kind === "transformvalues") {
848
+ if (!isnonempty(options.dataset)) return { allowed: false, reason: "A reviewed dataset id is required in options." };
849
+ const rules = options.rules;
850
+ if (!Array.isArray(rules) || rules.length === 0) return { allowed: false, reason: "A reviewed non-empty list of transform rules is required in options." };
851
+ for (const item of rules) {
852
+ const rulecheck = validatetransformrule(item);
853
+ if (!rulecheck.allowed) return rulecheck;
854
+ }
855
+ }
856
+ if (kind === "deduperows") {
857
+ if (!isnonempty(options.dataset)) return { allowed: false, reason: "A reviewed dataset id is required in options." };
858
+ const keys = options.keys;
859
+ if (!Array.isArray(keys) || keys.length === 0 || !keys.every((key) => isnonempty(key))) return { allowed: false, reason: "A reviewed non-empty list of dedupe column keys is required in options." };
860
+ }
861
+ if (kind === "mergepages") {
862
+ const listcheck = validatedatasetids(options, "datasets");
863
+ if (!listcheck.allowed) return listcheck;
864
+ }
865
+ if (kind === "stamplerows") {
866
+ if (!isnonempty(options.dataset)) return { allowed: false, reason: "A reviewed dataset id is required in options." };
867
+ if (options.url !== void 0 && !ishttpsurl(options.url)) return { allowed: false, reason: "The reviewed source url must use HTTPS." };
868
+ }
869
+ if (kind === "previewgrid") {
870
+ if (!isnonempty(options.dataset)) return { allowed: false, reason: "A reviewed dataset id is required in options." };
871
+ if (options.sample !== void 0 && (typeof options.sample !== "number" || !Number.isInteger(options.sample) || options.sample < 1)) return { allowed: false, reason: "The reviewed sample row count must be a positive integer with no code ceiling." };
872
+ }
873
+ if (kind === "resumeextract" && !isnonempty(options.session)) return { allowed: false, reason: "A reviewed extract session id is required in options." };
874
+ if (kind === "logprovenance" && !isnonempty(options.artifact)) return { allowed: false, reason: "A reviewed artifact id or name is required in options." };
875
+ return { allowed: true };
876
+ }
877
+ function submitreviewgranted(steps, submitid) {
878
+ const position = steps.findIndex((candidate) => candidate.id === submitid);
879
+ const asked = steps.some((candidate, index) => candidate.kind === "asksubmit" && (position === -1 || index < position));
880
+ return asked ? { allowed: true } : { allowed: false, reason: "Form submission requires an asksubmit review step before it." };
881
+ }
882
+ function passwordconsentgranted(step) {
883
+ let options = {};
884
+ try {
885
+ options = parseoptions(step);
886
+ } catch {
887
+ options = {};
888
+ }
889
+ const consentref = options.consentref;
890
+ if (typeof consentref !== "string" || !consentref.trim()) return { allowed: false, reason: "A password fill requires a reviewed consent ref in options." };
891
+ return { allowed: true };
892
+ }
893
+ function luhnvalid(digits) {
894
+ let sum = 0;
895
+ let double = false;
896
+ for (let index = digits.length - 1; index >= 0; index -= 1) {
897
+ let value = Number.parseInt(digits[index] ?? "", 10);
898
+ if (!Number.isFinite(value)) return false;
899
+ if (double) {
900
+ value *= 2;
901
+ if (value > 9) value -= 9;
902
+ }
903
+ sum += value;
904
+ double = !double;
905
+ }
906
+ return sum % 10 === 0;
907
+ }
908
+ function generatedvalueallowed(value) {
909
+ const compact = value.replace(/[\s-]/g, "");
910
+ if (/^\d{13,19}$/.test(compact) && luhnvalid(compact) && !compact.startsWith("4111")) return { allowed: false, reason: "The generated value looks like a real card number and is refused; generated card values use the 4111 test prefix." };
911
+ if (/^\d{3}-\d{2}-\d{4}$/.test(value.trim())) return { allowed: false, reason: "The generated value looks like a personal identifier and is refused." };
912
+ return { allowed: true };
913
+ }
914
+ function profilegrantgranted(profile, origin) {
915
+ if (!profile.grants.includes(origin)) return { allowed: false, reason: `The saved profile ${profile.name} is not granted to ${origin}; add the origin to the profile grants first.` };
916
+ return { allowed: true };
917
+ }
495
918
  function layoutmutationgranted(session, now) {
496
919
  if (!session || session.stoppedat || session.expiresat <= now) return { allowed: false, reason: "Group and layout mutations stay inside the active session." };
497
920
  return { allowed: true };
@@ -589,7 +1012,7 @@ function validateinnerstep(options, origin) {
589
1012
  return { allowed: true };
590
1013
  }
591
1014
  if (typeof kind !== "string" || !kind.trim()) return { allowed: false, reason: "A reviewed step id or inline step kind is required in options." };
592
- if (kind === "retryaction" || kind === "enterframe") return { allowed: false, reason: "The reviewed inner step cannot be another wrapper kind." };
1015
+ if (kind === "retryaction" || kind === "enterframe" || kind === "looprows") return { allowed: false, reason: "The reviewed inner step cannot be another wrapper kind." };
593
1016
  if (!allowedactions.has(kind)) return { allowed: false, reason: "The reviewed inner step kind is unsupported." };
594
1017
  const inneroptions = options.options;
595
1018
  if (inneroptions !== void 0 && (!inneroptions || typeof inneroptions !== "object" || Array.isArray(inneroptions))) return { allowed: false, reason: "The reviewed inner step options must be an object." };
@@ -969,6 +1392,14 @@ function validatestep(step, origin) {
969
1392
  const tabscheck = validatetabsgrammar(step, options);
970
1393
  if (!tabscheck.allowed) return tabscheck;
971
1394
  }
1395
+ if (isformkind(step.kind)) {
1396
+ const formcheck = validateformgrammar(step, options);
1397
+ if (!formcheck.allowed) return formcheck;
1398
+ }
1399
+ if (isdatasetkind(step.kind)) {
1400
+ const datacheck = validatedatagrammar(step, options, origin);
1401
+ if (!datacheck.allowed) return datacheck;
1402
+ }
972
1403
  if (step.kind === "tabcreate") {
973
1404
  if (options.background !== void 0 && typeof options.background !== "boolean") return { allowed: false, reason: "The reviewed background flag must be a boolean." };
974
1405
  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." };
@@ -996,6 +1427,10 @@ function canexecute(input) {
996
1427
  if (input.plan.expiresat <= now) return { allowed: false, reason: "The approved plan has expired." };
997
1428
  if ((input.step.kind === "pierceshadow" || input.step.kind === "enterframe") && !origingranted(input.session, input.origin)) return { allowed: false, reason: "The shadow or frame step is outside the session origin grants." };
998
1429
  if (input.step.kind === "readjson" && !origingranted(input.session, input.origin)) return { allowed: false, reason: "The json state read is outside the session origin grants." };
1430
+ if (isexportkind(input.step.kind)) {
1431
+ const exportgate = exportgranted(input.session, input.origin);
1432
+ if (!exportgate.allowed) return exportgate;
1433
+ }
999
1434
  if (input.step.kind === "navlist") {
1000
1435
  let options = {};
1001
1436
  try {
@@ -1010,6 +1445,15 @@ function canexecute(input) {
1010
1445
  }
1011
1446
  }
1012
1447
  if (islayoutkind(input.step.kind) && !layoutmutationgranted(input.session, now).allowed) return { allowed: false, reason: "Group and layout mutations stay inside the active session." };
1448
+ if (input.step.kind === "submitform" || input.step.kind === "retryform") {
1449
+ if (!input.plan) return { allowed: false, reason: "Form submission requires an asksubmit review step before it." };
1450
+ const reviewgate = submitreviewgranted(input.plan.steps, input.step.id);
1451
+ if (!reviewgate.allowed) return reviewgate;
1452
+ }
1453
+ if (input.step.kind === "consentpassword") {
1454
+ const consentgate = passwordconsentgranted(input.step);
1455
+ if (!consentgate.allowed) return consentgate;
1456
+ }
1013
1457
  if (input.step.kind === "openlink" || input.step.kind === "openprivate" || input.step.kind === "batchopen" || input.step.kind === "prefetch" || input.step.kind === "deeplink" || input.step.kind === "reopentab") {
1014
1458
  let options = {};
1015
1459
  try {
@@ -1091,9 +1535,29 @@ function tasktabs(progress, planid) {
1091
1535
  if (!progress || progress.planid !== planid) return [];
1092
1536
  return progress.tasktabs ?? [];
1093
1537
  }
1538
+ function wizardcompletion(state) {
1539
+ if (state.steps <= 0) return 0;
1540
+ return Math.min(1, state.completed.filter(Boolean).length / state.steps);
1541
+ }
1542
+ function extractionshare(rowscollected, estimatedtotal) {
1543
+ if (!Number.isFinite(estimatedtotal) || estimatedtotal <= 0) return 0;
1544
+ return Math.min(1, Math.max(0, rowscollected) / estimatedtotal);
1545
+ }
1546
+ function recordextraction(progress, planid, stepid, entry, now) {
1547
+ const base = progress && progress.planid === planid ? progress : emptyprogress(planid, now);
1548
+ const outcome = { stepid, ok: true, summary: `Extraction page ${entry.page} collected ${entry.rows} row${entry.rows === 1 ? "" : "s"} at cursor ${entry.cursor}.`, details: { extraction: entry }, at: now };
1549
+ return recordoutcome(base, planid, outcome, now);
1550
+ }
1551
+ function recordwizardstep(progress, planid, stepid, state, now) {
1552
+ const base = progress && progress.planid === planid ? progress : emptyprogress(planid, now);
1553
+ const executed = Math.min(state.index, state.steps);
1554
+ const done = executed >= state.steps;
1555
+ const outcome = { stepid, ok: done, summary: `Wizard step ${executed} of ${state.steps} ${done ? "completed the wizard" : "executed"}.`, details: { wizard: { index: state.index, steps: state.steps, completed: [...state.completed] } }, at: now };
1556
+ return recordoutcome(base, planid, outcome, now);
1557
+ }
1094
1558
 
1095
1559
  // version.ts
1096
- var packageversion = "1.1.36";
1560
+ var packageversion = "1.1.38";
1097
1561
 
1098
1562
  // types.ts
1099
1563
  var protocolversion = packageversion;
@@ -1130,9 +1594,14 @@ function parseproposal(value, origin) {
1130
1594
  return step;
1131
1595
  });
1132
1596
  for (const step of steps) {
1133
- if (step.kind !== "retryaction" && step.kind !== "enterframe") continue;
1597
+ if (step.kind !== "retryaction" && step.kind !== "enterframe" && step.kind !== "looprows") continue;
1134
1598
  const options = parseoptions(step);
1135
- if (typeof options.stepid === "string" && !steps.some((candidate) => candidate.id === options.stepid)) throw new Error("A retry or frame wrapper references an unknown step id.");
1599
+ if (typeof options.stepid === "string" && !steps.some((candidate) => candidate.id === options.stepid)) throw new Error("A retry, frame or loop wrapper references an unknown step id.");
1600
+ }
1601
+ for (const step of steps) {
1602
+ if (step.kind !== "submitform" && step.kind !== "retryform") continue;
1603
+ const review = submitreviewgranted(steps, step.id);
1604
+ if (!review.allowed) throw new Error(review.reason);
1136
1605
  }
1137
1606
  const createdat = Date.now();
1138
1607
  const expiresat = typeof planinput.expiresat === "number" ? planinput.expiresat : createdat + 10 * 60 * 1e3;
@@ -1185,6 +1654,26 @@ function safetyresponse(input) {
1185
1654
  function layoutreport(input) {
1186
1655
  return { version: protocolversion, layouts: input.layouts };
1187
1656
  }
1657
+ function formreportresponse(input) {
1658
+ return JSON.stringify({ version: protocolversion, planid: input.plan.id, planstate: input.plan.state, report: input.report });
1659
+ }
1660
+ function errorreportresponse(input) {
1661
+ return JSON.stringify({ version: protocolversion, planid: input.plan.id, planstate: input.plan.state, report: input.report });
1662
+ }
1663
+ function wizardreport(input) {
1664
+ return { version: protocolversion, ...input.sessionid ? { sessionid: input.sessionid } : {}, wizards: input.wizards, picks: input.picks };
1665
+ }
1666
+ function datasetresponse(input) {
1667
+ const sample = Math.max(0, Math.floor(input.sample ?? 10));
1668
+ const payload = { ...input.dataset, rows: input.dataset.rows.slice(0, sample), totalrows: input.dataset.rows.length };
1669
+ return JSON.stringify({ version: protocolversion, planid: input.plan.id, planstate: input.plan.state, dataset: payload });
1670
+ }
1671
+ function extractionreport(input) {
1672
+ return { version: protocolversion, sessions: input.sessions };
1673
+ }
1674
+ function provenancereport(input) {
1675
+ return { version: protocolversion, records: input.records };
1676
+ }
1188
1677
 
1189
1678
  // extension/browsertabs.ts
1190
1679
  var browserkinds = /* @__PURE__ */ new Set(["tablist", "tabcreate", "tabactivate", "tabclose", "tabreload", "tabsnapshot", "windowlist", "windowcreate", "windowclose", "zoomset", "windowresize", "downloadfile"]);
@@ -1840,6 +2329,368 @@ function authfor(auths, url) {
1840
2329
  return auths.find((record2) => record2.origin === origin);
1841
2330
  }
1842
2331
 
2332
+ // extension/pageforms.ts
2333
+ var firstnames = { en: ["alex", "jordan", "taylor", "morgan", "casey"], pt: ["ana", "bruno", "carla", "diego", "helena"] };
2334
+ var lastnames = { en: ["brooks", "carter", "diaz", "evans", "reyes"], pt: ["alves", "costa", "lima", "souza", "moraes"] };
2335
+ function localekey(locale) {
2336
+ const normalized = locale.toLowerCase();
2337
+ if (normalized.startsWith("pt")) return "pt";
2338
+ return "en";
2339
+ }
2340
+ function generatevalue(kind, rule) {
2341
+ const seed = typeof rule.seed === "number" && Number.isFinite(rule.seed) ? Math.abs(Math.floor(rule.seed)) : 1;
2342
+ const names = firstnames[localekey(rule.locale ?? "en")] ?? firstnames.en ?? ["alex"];
2343
+ const surnames = lastnames[localekey(rule.locale ?? "en")] ?? lastnames.en ?? ["brooks"];
2344
+ let state = seed * 1103515245 + 12345;
2345
+ const next = () => {
2346
+ state = (state * 1103515245 + 12345) % 2147483648;
2347
+ return state / 2147483648;
2348
+ };
2349
+ const pick = (items) => items[Math.floor(next() * items.length) % items.length] ?? items[0];
2350
+ const digits = (count) => Array.from({ length: count }, () => String(Math.floor(next() * 10))).join("");
2351
+ const person = `${pick(names)} ${pick(surnames)}`;
2352
+ switch (kind) {
2353
+ case "email":
2354
+ return `${person.replace(" ", ".")}${digits(2)}@example.com`;
2355
+ case "phone":
2356
+ return localekey(rule.locale ?? "en") === "pt" ? `+55 (11) 9${digits(4)}-${digits(4)}` : `+1 (555) 010-${digits(4)}`;
2357
+ case "date":
2358
+ return `${2024 + Math.floor(next() * 2)}-${String(1 + Math.floor(next() * 12)).padStart(2, "0")}-${String(1 + Math.floor(next() * 28)).padStart(2, "0")}`;
2359
+ case "number":
2360
+ return String(Math.floor(next() * 1e3));
2361
+ case "select":
2362
+ return `option ${1 + Math.floor(next() * 5)}`;
2363
+ case "check":
2364
+ return next() > 0.5 ? "true" : "false";
2365
+ case "radio":
2366
+ return `choice ${1 + Math.floor(next() * 4)}`;
2367
+ case "file":
2368
+ return `sample${digits(2)}.pdf`;
2369
+ case "password":
2370
+ return `pw-${digits(6)}-${pick(names)}`;
2371
+ case "card":
2372
+ return `4111 ${digits(4)} ${digits(4)} ${digits(4)}`;
2373
+ case "code":
2374
+ return digits(6);
2375
+ default:
2376
+ return person;
2377
+ }
2378
+ }
2379
+ function valueshash(values) {
2380
+ const source = values.map((entry) => `${entry.label}=${entry.value}`).join("|");
2381
+ let hash = 5381;
2382
+ for (let index = 0; index < source.length; index += 1) hash = (hash * 33 ^ source.charCodeAt(index)) >>> 0;
2383
+ return hash.toString(16);
2384
+ }
2385
+ function parseformrecord(value) {
2386
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
2387
+ const record2 = value;
2388
+ if (!Array.isArray(record2.entries)) return null;
2389
+ const entries = [];
2390
+ for (const item of record2.entries) {
2391
+ if (!item || typeof item !== "object" || Array.isArray(item)) continue;
2392
+ const entry = item;
2393
+ const match = entry.match;
2394
+ if (!match || typeof match !== "object" || Array.isArray(match)) continue;
2395
+ const shapes = match;
2396
+ if (typeof shapes.mode !== "string") continue;
2397
+ const fieldmatch = {
2398
+ mode: shapes.mode,
2399
+ ...typeof shapes.label === "string" ? { label: shapes.label } : {},
2400
+ ...typeof shapes.placeholder === "string" ? { placeholder: shapes.placeholder } : {},
2401
+ ...typeof shapes.arialabel === "string" ? { arialabel: shapes.arialabel } : {},
2402
+ ...typeof shapes.name === "string" ? { name: shapes.name } : {}
2403
+ };
2404
+ if (typeof entry.kind !== "string" || typeof entry.value !== "string") continue;
2405
+ entries.push({ match: fieldmatch, kind: entry.kind, value: entry.value });
2406
+ }
2407
+ if (entries.length === 0) return null;
2408
+ return { ...typeof record2.form === "string" && record2.form ? { form: record2.form } : {}, entries };
2409
+ }
2410
+
2411
+ // extension/pagewizards.ts
2412
+ function parsebackoff(step) {
2413
+ let options = {};
2414
+ try {
2415
+ options = parseoptions(step);
2416
+ } catch {
2417
+ options = {};
2418
+ }
2419
+ const backoff = options.backoff;
2420
+ if (!backoff || typeof backoff !== "object" || Array.isArray(backoff)) return null;
2421
+ const rule = backoff;
2422
+ const wait = rule.wait;
2423
+ const factor = rule.factor;
2424
+ if (typeof wait !== "number" || !Number.isFinite(wait) || wait <= 0) return null;
2425
+ if (typeof factor !== "number" || !Number.isFinite(factor) || factor < 1) return null;
2426
+ const attempts = typeof options.attempts === "number" && Number.isInteger(options.attempts) && options.attempts >= 1 ? options.attempts : 2;
2427
+ return { attempts, wait, factor };
2428
+ }
2429
+ function backoffwaits(attempts, wait, factor) {
2430
+ const windows = [];
2431
+ let current = wait;
2432
+ for (let index = 1; index < attempts; index += 1) {
2433
+ windows.push(current);
2434
+ current *= factor;
2435
+ }
2436
+ return windows;
2437
+ }
2438
+
2439
+ // extension/pagedata.ts
2440
+ function normalizeheader(label) {
2441
+ const slug = label.trim().toLowerCase().replace(/[^\p{L}\p{N}]+/gu, "-").replace(/^-+|-+$/g, "");
2442
+ return slug || "column";
2443
+ }
2444
+ function columnspecof(label, used = /* @__PURE__ */ new Set()) {
2445
+ const base = normalizeheader(label);
2446
+ let key = base;
2447
+ let suffix = 2;
2448
+ while (used.has(key)) {
2449
+ key = `${base}${suffix}`;
2450
+ suffix += 1;
2451
+ }
2452
+ used.add(key);
2453
+ return { key, label: label.trim(), kind: "text", normalized: label.trim().toLowerCase() };
2454
+ }
2455
+ function rowhash(row, keys) {
2456
+ const source = (keys.length > 0 ? keys : Object.keys(row).sort()).map((key) => `${key}=${row[key] ?? ""}`).join("|");
2457
+ let hash = 5381;
2458
+ for (let index = 0; index < source.length; index += 1) hash = (hash * 33 ^ source.charCodeAt(index)) >>> 0;
2459
+ return hash.toString(16);
2460
+ }
2461
+ function dedupebykeys(rows, keys) {
2462
+ const seen = /* @__PURE__ */ new Set();
2463
+ const kept = [];
2464
+ for (const row of rows) {
2465
+ const hash = rowhash(row, keys);
2466
+ if (seen.has(hash)) continue;
2467
+ seen.add(hash);
2468
+ kept.push(row);
2469
+ }
2470
+ return { kept, removed: rows.length - kept.length };
2471
+ }
2472
+ function applyexpression(value, expression) {
2473
+ const split = expression.indexOf(":");
2474
+ const op = split === -1 ? expression : expression.slice(0, split);
2475
+ const argument = split === -1 ? void 0 : expression.slice(split + 1);
2476
+ if (op === "trim") return value.trim();
2477
+ if (op === "upper") return value.toUpperCase();
2478
+ if (op === "lower") return value.toLowerCase();
2479
+ if (op === "number") return value.replace(/[^\d.\-]/g, "");
2480
+ if (op === "prefix") return `${argument ?? ""}${value}`;
2481
+ if (op === "suffix") return `${value}${argument ?? ""}`;
2482
+ if (op === "replace") {
2483
+ const separator = argument?.indexOf("=>") ?? -1;
2484
+ if (separator === -1 || separator === 0) throw new Error(`The reviewed transform expression ${expression} needs the from=>to separator.`);
2485
+ const from = argument.slice(0, separator);
2486
+ const to = argument.slice(separator + 2);
2487
+ return value.split(from).join(to);
2488
+ }
2489
+ throw new Error(`The reviewed transform expression ${op} is not supported.`);
2490
+ }
2491
+ function transformrows(rows, rules) {
2492
+ const errors = [];
2493
+ const output = rows.map((row) => ({ ...row }));
2494
+ for (const rule of rules) {
2495
+ const updated = [];
2496
+ try {
2497
+ for (const row of output) updated.push({ ...row, [rule.target]: applyexpression(rule.sources.map((source) => row[source] ?? "").join(" "), rule.expression) });
2498
+ } catch (error) {
2499
+ errors.push(`${rule.target}: ${error instanceof Error ? error.message : String(error)}`);
2500
+ continue;
2501
+ }
2502
+ output.splice(0, output.length, ...updated);
2503
+ }
2504
+ return { rows: output, errors };
2505
+ }
2506
+ function mergedatasets(datasets) {
2507
+ const columns = [];
2508
+ const seen = /* @__PURE__ */ new Set();
2509
+ for (const dataset of datasets) {
2510
+ for (const column of dataset.columns) {
2511
+ if (seen.has(column.key)) continue;
2512
+ seen.add(column.key);
2513
+ columns.push(column);
2514
+ }
2515
+ }
2516
+ const rows = datasets.flatMap((dataset) => dataset.rows.map((row) => {
2517
+ const merged = {};
2518
+ for (const column of columns) merged[column.key] = row[column.key] ?? "";
2519
+ return merged;
2520
+ }));
2521
+ return { columns, rows };
2522
+ }
2523
+ function samplerows(rows, url, stepid, at) {
2524
+ const stamped = rows.map((row) => ({ ...row, source: url, capturedat: String(at), step: stepid }));
2525
+ const sources = stamped.map((row, index) => ({ row: index, url, at, stepid }));
2526
+ return { rows: stamped, sources };
2527
+ }
2528
+ function csvfield(value, delimiter) {
2529
+ return value.includes(delimiter) || value.includes('"') || value.includes("\n") ? `"${value.replace(/"/g, '""')}"` : value;
2530
+ }
2531
+ function tocsv(columns, rows, delimiter = ",") {
2532
+ const lines = [columns.map((column) => csvfield(column.label || column.key, delimiter)).join(delimiter)];
2533
+ for (const row of rows) lines.push(columns.map((column) => csvfield(row[column.key] ?? "", delimiter)).join(delimiter));
2534
+ return lines.join("\n");
2535
+ }
2536
+ function parsecsv(text2, delimiter = ",") {
2537
+ const records = [];
2538
+ let field = "";
2539
+ let record2 = [];
2540
+ let quoted = false;
2541
+ for (let index = 0; index < text2.length; index += 1) {
2542
+ const character = text2[index];
2543
+ if (quoted) {
2544
+ if (character === '"') {
2545
+ if (text2[index + 1] === '"') {
2546
+ field += '"';
2547
+ index += 1;
2548
+ } else quoted = false;
2549
+ } else field += character;
2550
+ continue;
2551
+ }
2552
+ if (character === '"') {
2553
+ quoted = true;
2554
+ continue;
2555
+ }
2556
+ if (character === delimiter) {
2557
+ record2.push(field);
2558
+ field = "";
2559
+ continue;
2560
+ }
2561
+ if (character === "\n" || character === "\r") {
2562
+ if (character === "\r" && text2[index + 1] === "\n") index += 1;
2563
+ record2.push(field);
2564
+ field = "";
2565
+ if (record2.some((value) => value.length > 0) || record2.length > 1) records.push(record2);
2566
+ record2 = [];
2567
+ continue;
2568
+ }
2569
+ field += character;
2570
+ }
2571
+ record2.push(field);
2572
+ if (record2.some((value) => value.length > 0) || record2.length > 1) records.push(record2);
2573
+ const [headers = [], ...rows] = records;
2574
+ return { headers, rows };
2575
+ }
2576
+ function mapcolumns(headers, mapping = {}) {
2577
+ const used = /* @__PURE__ */ new Set();
2578
+ return headers.map((header) => {
2579
+ const target = mapping[header] ?? mapping[normalizeheader(header)] ?? header;
2580
+ return columnspecof(target, used);
2581
+ });
2582
+ }
2583
+ function tojson(columns, rows) {
2584
+ return JSON.stringify({ columns, rows });
2585
+ }
2586
+ function xmltext(value) {
2587
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
2588
+ }
2589
+ function toexcel(columns, rows, name) {
2590
+ const head = columns.map((column) => `<Cell ss:StyleID="head"><Data ss:Type="String">${xmltext(column.label || column.key)}</Data></Cell>`).join("");
2591
+ const body = rows.map((row) => `<Row>${columns.map((column) => {
2592
+ const value = row[column.key] ?? "";
2593
+ const numeric = column.kind === "number" && value.trim() !== "" && Number.isFinite(Number(value));
2594
+ return numeric ? `<Cell><Data ss:Type="Number">${xmltext(value)}</Data></Cell>` : `<Cell><Data ss:Type="String">${xmltext(value)}</Data></Cell>`;
2595
+ }).join("")}</Row>`).join("");
2596
+ return `<?xml version="1.0"?><?mso-application progid="Excel.Sheet"?><Workbook xmlns="urn:schemas-microsoft-com:office:spreadsheet" xmlns:ss="urn:schemas-microsoft-com:office:spreadsheet"><Styles><Style ss:ID="head"><Font ss:Bold="1"/></Style></Styles><Worksheet ss:Name="${xmltext(name || "dataset").slice(0, 31)}"><Table><Row>${head}</Row>${body}</Table></Worksheet></Workbook>`;
2597
+ }
2598
+
2599
+ // extension/datacommand.ts
2600
+ function builddataset(id, name, grid, at) {
2601
+ return { id, name: name || id, columns: grid.columns, rows: grid.rows, sources: [], at };
2602
+ }
2603
+ function checksum(value) {
2604
+ let hash = 5381;
2605
+ for (let index = 0; index < value.length; index += 1) hash = (hash * 33 ^ value.charCodeAt(index)) >>> 0;
2606
+ return `fnv1a-${hash.toString(16)}`;
2607
+ }
2608
+ function exportcontent(datasetvalue, format, delimiter = ",") {
2609
+ if (format === "json") return tojson(datasetvalue.columns, datasetvalue.rows);
2610
+ if (format === "excel") return toexcel(datasetvalue.columns, datasetvalue.rows, datasetvalue.name);
2611
+ return tocsv(datasetvalue.columns, datasetvalue.rows, delimiter);
2612
+ }
2613
+ function exportartifact(id, datasetvalue, format, stepid, content, at) {
2614
+ const extension = format === "excel" ? "xml" : format;
2615
+ return { id, kind: format, name: `${datasetvalue.name || datasetvalue.id}.${extension}`, stepid, rowcount: datasetvalue.rows.length, content, checksum: checksum(content), at };
2616
+ }
2617
+ function artifactrecordof(artifact) {
2618
+ return { id: artifact.id, kind: artifact.kind, name: artifact.name, stepid: artifact.stepid, at: artifact.at };
2619
+ }
2620
+ function chunkplan(rows, chunk) {
2621
+ const size = Math.max(1, Math.floor(chunk));
2622
+ const chunks = [];
2623
+ for (let from = 0; from < rows || chunks.length === 0; from += size) {
2624
+ const to = Math.min(rows, from + size);
2625
+ chunks.push({ index: chunks.length, from, to });
2626
+ if (to >= rows) break;
2627
+ }
2628
+ return chunks;
2629
+ }
2630
+ function backpressure(written, acknowledged) {
2631
+ return written - acknowledged >= 1;
2632
+ }
2633
+ function advancestream(state, chunk, at, done) {
2634
+ return { datasetid: state.datasetid, name: state.name, chunk: chunk.index + 1, chunks: state.chunks, written: chunk.to, ...done ? { done: true } : {}, at };
2635
+ }
2636
+ function newstream(datasetvalue, chunks, at) {
2637
+ return { datasetid: datasetvalue.id, name: datasetvalue.name, chunk: 0, chunks, written: 0, at };
2638
+ }
2639
+ function advancecursor(sessionvalue, page, rows, at, done) {
2640
+ return {
2641
+ id: sessionvalue.id,
2642
+ datasetid: sessionvalue.datasetid,
2643
+ name: sessionvalue.name,
2644
+ target: sessionvalue.target,
2645
+ next: sessionvalue.next,
2646
+ planned: sessionvalue.planned,
2647
+ pages: [...sessionvalue.pages, page],
2648
+ rows: sessionvalue.rows + rows,
2649
+ cursor: sessionvalue.cursor + 1,
2650
+ ...done || sessionvalue.cursor + 1 >= sessionvalue.planned ? { done: true } : {},
2651
+ startedat: sessionvalue.startedat,
2652
+ updatedat: at
2653
+ };
2654
+ }
2655
+ function newextractsession(id, datasetid, name, target, next, planned, at) {
2656
+ return { id, datasetid, name, target, next, planned, pages: [], rows: 0, cursor: 0, startedat: at, updatedat: at };
2657
+ }
2658
+ function remainingpages(sessionvalue, planned) {
2659
+ if (sessionvalue.done) return 0;
2660
+ return Math.max(0, Math.max(sessionvalue.planned, planned) - sessionvalue.cursor);
2661
+ }
2662
+ function provenancefor(artifact, url, stepid, at) {
2663
+ return { artifact: artifact.id, name: artifact.name, url, stepid, rowstart: artifact.rowcount > 0 ? 1 : 0, rowend: artifact.rowcount, checksum: artifact.checksum, at };
2664
+ }
2665
+ function interpolate(text2, row) {
2666
+ return text2.replace(/\{\{([^}]+)\}\}/g, (_, key) => row[key.trim()] ?? "");
2667
+ }
2668
+ function loopstep(step, row) {
2669
+ return {
2670
+ ...step,
2671
+ ...step.target !== void 0 ? { target: interpolate(step.target, row) } : {},
2672
+ ...step.value !== void 0 ? { value: interpolate(step.value, row) } : {},
2673
+ ...step.options !== void 0 ? { options: interpolate(step.options, row) } : {}
2674
+ };
2675
+ }
2676
+ function loopvariables(row) {
2677
+ return { ...row };
2678
+ }
2679
+ function gridpreview(datasetvalue, sample) {
2680
+ return { datasetid: datasetvalue.id, columns: datasetvalue.columns.map((column) => column.key), rows: datasetvalue.rows.length, sample: datasetvalue.rows.slice(0, Math.max(0, Math.floor(sample))) };
2681
+ }
2682
+ function sheetpayload(datasetvalue, sheet) {
2683
+ return { sheet, columns: datasetvalue.columns.map((column) => column.key), rows: datasetvalue.rows };
2684
+ }
2685
+ function mergetaskrules(existing, taskid, transforms, dedupekeys, at) {
2686
+ return {
2687
+ taskid,
2688
+ transforms: transforms.length > 0 ? transforms : existing?.transforms ?? [],
2689
+ dedupekeys: dedupekeys.length > 0 ? dedupekeys : existing?.dedupekeys ?? [],
2690
+ at
2691
+ };
2692
+ }
2693
+
1843
2694
  // extension/background.ts
1844
2695
  var sessionduration = 15 * 60 * 1e3;
1845
2696
  var freshcheckkinds = /* @__PURE__ */ new Set(["focus", "inspect", "click", "type", "scroll", "select", "hover"]);
@@ -1849,6 +2700,7 @@ var observationstepkinds = /* @__PURE__ */ new Set(["a11ytree", "readvisible", "
1849
2700
  var navigationstepkinds = /* @__PURE__ */ new Set(["openlink", "openprivate", "reloadcache", "stopnav", "waitload", "waiturl", "followlink", "spanav", "spawait", "rewritequery", "setfragment", "navlist", "navprofile", "detecthttp", "readredirects", "readfinalurl", "handleauth", "printpdf", "prefetch", "preconnect", "deeplink", "reopentab", "trailaudit", "pausenav", "navintent", "navrate", "openclipboard", "checksafe", "batchopen"]);
1850
2701
  var pausenavkinds = /* @__PURE__ */ new Set(["openlink", "openprivate", "followlink", "spanav", "navlist", "openclipboard", "batchopen", "prefetch", "preconnect", "deeplink", "reopentab"]);
1851
2702
  var ratecheckedkinds = /* @__PURE__ */ new Set(["openlink", "openprivate", "followlink", "spanav", "navlist", "openclipboard", "batchopen", "deeplink", "reopentab"]);
2703
+ var formfillkinds = /* @__PURE__ */ new Set(["fillform", "filllabel", "fillplaceholder", "fillcard", "fillcode", "attachfile", "saveprofiles", "runwizard", "selectchain", "picktypeahead", "pickdate"]);
1852
2704
  var evidencepoll = 100;
1853
2705
  var evidencesettle = 5e3;
1854
2706
  var chromestorage = {
@@ -2025,7 +2877,18 @@ function stepauditkind(step, ok) {
2025
2877
  }
2026
2878
  if (step.kind === "dismissdialog") return "dialog";
2027
2879
  if (step.kind === "keyhold" || step.kind === "keyrelease") return "hold";
2028
- if (step.kind === "retryaction") return "retry";
2880
+ if (step.kind === "retryaction" || step.kind === "retryform") return "retry";
2881
+ if (step.kind === "submitform" || step.kind === "asksubmit") return "submit";
2882
+ if (step.kind === "consentpassword") return "consent";
2883
+ if (step.kind === "handoffcaptcha") return "handoff";
2884
+ if (isdatasetkind(step.kind)) {
2885
+ if (step.kind === "exportcsv" || step.kind === "exportjson" || step.kind === "exportexcel" || step.kind === "copytable" || step.kind === "pushsheets") return "export";
2886
+ if (step.kind === "streamdisk") return "stream";
2887
+ if (step.kind === "resumeextract") return "resume";
2888
+ if (step.kind === "logprovenance") return "provenance";
2889
+ return "scrape";
2890
+ }
2891
+ if (formfillkinds.has(step.kind)) return "fill";
2029
2892
  if (pointerkinds.has(step.kind)) return "pointer";
2030
2893
  if (watchstepkinds.has(step.kind)) return "watch";
2031
2894
  if (step.kind === "diffsnapshots") return "diff";
@@ -3067,6 +3930,360 @@ async function executetabscommand(step, session, plan, sessiontabid) {
3067
3930
  return { ok: false, summary: "Unsupported tabs and windows command." };
3068
3931
  }
3069
3932
  }
3933
+ async function executesaveprofiles(step, session, origin) {
3934
+ const options = stepoptions2(step);
3935
+ const record2 = parseformrecord(options.formrecord);
3936
+ const name = typeof options.name === "string" ? options.name : "";
3937
+ if (!name || !record2) throw new Error("A reviewed profile name and form record are required.");
3938
+ const grants = session?.grants ?? (session ? [session.origin] : [origin]);
3939
+ const profile = { name, fields: record2.entries, grants, savedat: Date.now() };
3940
+ await memory.setprofile(profile);
3941
+ await audit("fill", `Form profile ${name} stored locally with ${profile.fields.length} field entries behind the origin grants of ${grants.join(", ")}; password entries are refused.`, { ...session ? { sessionid: session.id } : {} });
3942
+ return { ok: true, summary: `Stored the form profile ${name} locally with ${profile.fields.length} field entries.`, details: { profile: { name: profile.name, fields: profile.fields.length, grants: profile.grants } } };
3943
+ }
3944
+ async function executeasksubmit(step, session, plan, tabid2, origin) {
3945
+ const output = await dispatchpagestep(step, tabid2, origin, plan);
3946
+ const values = Array.isArray(output?.details?.values) ? output?.details?.values : [];
3947
+ const ticket = { id: randomid(), form: step.value ?? "", valueshash: valueshash(values), consentref: step.id, at: Date.now() };
3948
+ await memory.setticket(ticket);
3949
+ await audit("submit", `Asksubmit prompt opened for form ${ticket.form || "the reviewed form"} with the values hash ${ticket.valueshash}; the submission waits for the user approval.`, { ...session ? { sessionid: session.id } : {}, planid: plan.id, stepid: step.id });
3950
+ await refreshbadge();
3951
+ return { ok: true, summary: `Asksubmit prompt opened for form ${ticket.form || "the reviewed form"}; the submission waits for your approval.`, details: { ticket, values } };
3952
+ }
3953
+ async function executesubmitform(step, session, plan, tabid2, origin) {
3954
+ const consentref = typeof stepoptions2(step).consentref === "string" ? stepoptions2(step).consentref : "";
3955
+ const ticket = (await memory.gettickets()).find((item) => item.approved === true && (item.consentref === consentref || item.id === consentref));
3956
+ if (!ticket) throw new Error("No approved asksubmit ticket matches the reviewed consent ref; approve the submission in the review panel first.");
3957
+ const output = await dispatchpagestep(step, tabid2, origin, plan) ?? { ok: false, summary: "The form submission returned no result." };
3958
+ await audit("submit", `Form ${ticket.form || "the reviewed form"} submitted through its owning form under ticket ${ticket.id} with values hash ${ticket.valueshash} and outcome ${output.ok ? "delivered" : "refused"}.`, { ...session ? { sessionid: session.id } : {}, planid: plan.id, stepid: step.id });
3959
+ return { ...output, details: { ...output.details ?? {}, ticket: { id: ticket.id, valueshash: ticket.valueshash, consentref: ticket.consentref } } };
3960
+ }
3961
+ async function executeretryform(step, session, plan, tabid2, origin) {
3962
+ const rule = parsebackoff(step);
3963
+ if (!rule) throw new Error("A reviewed backoff rule with wait and factor is required.");
3964
+ const windows = backoffwaits(rule.attempts, rule.wait, rule.factor);
3965
+ let attempts = 0;
3966
+ let output;
3967
+ while (attempts < rule.attempts) {
3968
+ attempts += 1;
3969
+ output = await dispatchpagestep(step, tabid2, origin, plan) ?? { ok: false, summary: "The retried submission returned no result." };
3970
+ if (output.ok) break;
3971
+ const waitwindow = windows[attempts - 1];
3972
+ if (waitwindow !== void 0 && attempts < rule.attempts) await new Promise((resolve) => setTimeout(resolve, waitwindow));
3973
+ }
3974
+ await audit("retry", `Form submission retried ${attempts} time${attempts === 1 ? "" : "s"} with the reviewed backoff windows ${windows.join(", ") || "none"} milliseconds; outcome ${output?.ok ? "delivered" : "refused"}.`, { ...session ? { sessionid: session.id } : {}, planid: plan.id, stepid: step.id });
3975
+ return { ok: Boolean(output?.ok), summary: output?.summary ?? "The retried submission returned no result.", details: { attempts, windows, ok: Boolean(output?.ok) } };
3976
+ }
3977
+ async function executeconsentpassword(step, session, plan, tabid2, origin) {
3978
+ const consentref = typeof stepoptions2(step).consentref === "string" ? stepoptions2(step).consentref : "";
3979
+ const gate = passwordconsentgranted(step);
3980
+ if (!gate.allowed) throw new Error(gate.reason ?? "A password fill requires a reviewed consent ref.");
3981
+ const output = await dispatchpagestep(step, tabid2, origin, plan);
3982
+ await audit("consent", `Password field filled after the explicit consent ref ${consentref}; the value never appears in the audit trail.`, { ...session ? { sessionid: session.id } : {}, planid: plan.id, stepid: step.id });
3983
+ return output ?? { ok: false, summary: "The password fill returned no result." };
3984
+ }
3985
+ async function executeattachfile(step, session, plan, tabid2, origin) {
3986
+ const name = step.value ?? "";
3987
+ const artifacts = await memory.getartifacts();
3988
+ const artifact = artifacts.find((item) => item.name === name || item.id === name);
3989
+ if (!artifact) throw new Error(`No generated artifact named ${name} exists in the run store yet.`);
3990
+ const derived = { ...step, options: JSON.stringify({ ...stepoptions2(step), artifact: artifact.id, artifactname: artifact.name }) };
3991
+ const output = await dispatchpagestep(derived, tabid2, origin, plan);
3992
+ await audit("fill", `Artifact ${artifact.name} of kind ${artifact.kind} attached to the reviewed file input inside the form submission.`, { ...session ? { sessionid: session.id } : {}, planid: plan.id, stepid: step.id });
3993
+ return { ...output ?? { ok: false, summary: "The artifact attachment returned no result." }, details: { ...output?.details ?? {}, artifact } };
3994
+ }
3995
+ async function executecaptchahandoff(step, session, plan, tabid2, origin) {
3996
+ const output = await dispatchpagestep(step, tabid2, origin, plan) ?? { ok: false, summary: "The captcha probe returned no result." };
3997
+ if (output.details?.captcha !== true) return { ok: true, summary: "No captcha was detected; the plan continues.", details: { captcha: false } };
3998
+ const handoff = { id: randomid(), origin, resolved: false, openedat: Date.now() };
3999
+ await memory.addcaptcha(handoff);
4000
+ if (session && !session.pausedat && !session.stoppedat) await memory.setsession({ ...session, pausedat: Date.now() });
4001
+ await audit("handoff", `Captcha detected on ${origin}; control handed back to the user and the plan pauses until the handoff ${handoff.id} resolves.`, { ...session ? { sessionid: session.id } : {}, planid: plan.id, stepid: step.id });
4002
+ await refreshbadge();
4003
+ return { ok: true, summary: "Captcha detected; control is yours and the plan waits until you resolve the handoff.", details: { captcha: true, handoff } };
4004
+ }
4005
+ async function executeformstep(step, session, plan, tabid2, origin) {
4006
+ switch (step.kind) {
4007
+ case "saveprofiles":
4008
+ return executesaveprofiles(step, session, origin);
4009
+ case "asksubmit":
4010
+ return executeasksubmit(step, session, plan, tabid2, origin);
4011
+ case "submitform":
4012
+ return executesubmitform(step, session, plan, tabid2, origin);
4013
+ case "retryform":
4014
+ return executeretryform(step, session, plan, tabid2, origin);
4015
+ case "consentpassword":
4016
+ return executeconsentpassword(step, session, plan, tabid2, origin);
4017
+ case "attachfile":
4018
+ return executeattachfile(step, session, plan, tabid2, origin);
4019
+ case "handoffcaptcha":
4020
+ return executecaptchahandoff(step, session, plan, tabid2, origin);
4021
+ case "fillcode": {
4022
+ const stored = await memory.getcodevalue();
4023
+ const source = typeof stepoptions2(step).source === "string" ? stepoptions2(step).source : "";
4024
+ const derived = stored !== void 0 && source === "reviewed" ? { ...step, value: stored } : step;
4025
+ const output = await dispatchpagestep(derived, tabid2, origin, plan);
4026
+ await audit("fill", `One time code typed from the reviewed source ${source}${stored !== void 0 ? " through the consent gated code entry" : ""}.`, { ...session ? { sessionid: session.id } : {}, planid: plan.id, stepid: step.id });
4027
+ return output ?? { ok: false, summary: "The one time code fill returned no result." };
4028
+ }
4029
+ default: {
4030
+ const output = await dispatchpagestep(step, tabid2, origin, plan);
4031
+ const extra = { ...session ? { sessionid: session.id } : {}, planid: plan.id, stepid: step.id };
4032
+ if (step.kind === "runwizard" && output?.details?.wizard && typeof output.details.wizard === "object") {
4033
+ const state = output.details.wizard;
4034
+ await memory.addwizard(state);
4035
+ await memory.setprogress(recordwizardstep(await memory.getprogress(), plan.id, step.id, state, Date.now()));
4036
+ await audit("fill", `Wizard advanced to step ${Math.min(state.index, state.steps)} of ${state.steps} with a completion share of ${Math.round(wizardcompletion(state) * 100)} percent.`, extra);
4037
+ }
4038
+ if (step.kind === "picktypeahead" && typeof output?.details?.pick === "string") {
4039
+ const pick = { field: step.target ?? "", query: step.value ?? "", pick: output.details.pick, at: Date.now() };
4040
+ await memory.addpick(pick);
4041
+ }
4042
+ if (step.kind === "readerrors" && Array.isArray(output?.details?.errors)) {
4043
+ const report = { form: step.target ?? "", errors: output?.details?.errors, at: Date.now() };
4044
+ await memory.adderrorreport(report);
4045
+ await audit("fill", `Collected ${report.errors.length} inline validation message${report.errors.length === 1 ? "" : "s"} for the correction loop.`, extra);
4046
+ }
4047
+ if ((step.kind === "detectlogin" || step.kind === "detecttemplate") && output?.ok) {
4048
+ const detected = step.kind === "detectlogin" ? output.details?.login === true : output.details?.template === "signup" || output.details?.template === "checkout";
4049
+ if (detected) {
4050
+ const kind = step.kind === "detectlogin" ? "login" : output.details?.template === "checkout" ? "checkout" : "signup";
4051
+ const markers = Array.isArray(output.details?.markers) ? output.details?.markers : [];
4052
+ const record2 = { origin, kind, markers, at: Date.now() };
4053
+ await memory.adddetection(record2);
4054
+ await audit("fill", `${kind} shape detected on ${origin} with markers ${markers.join(", ") || "none"}; sensitive work stays behind the consent gates.`, extra);
4055
+ }
4056
+ }
4057
+ if (step.kind === "skiphoneypot" && Array.isArray(output?.details?.skipped)) {
4058
+ await audit("fill", `Honeypot survey flagged ${output.details.skipped.length} field${output.details.skipped.length === 1 ? "" : "s"} as skipped so fills never trip them.`, extra);
4059
+ }
4060
+ if (step.kind === "fillform" || step.kind === "filllabel" || step.kind === "fillplaceholder") {
4061
+ const filled = typeof output?.details?.filled === "number" ? output.details.filled : 0;
4062
+ const skipped = Array.isArray(output?.details?.skipped) ? output.details.skipped.length : 0;
4063
+ await audit("fill", `Filled ${filled} reviewed field${filled === 1 ? "" : "s"}${skipped > 0 ? ` and skipped ${skipped} honeypot field${skipped === 1 ? "" : "s"}` : ""}.`, extra);
4064
+ }
4065
+ if (step.kind === "generatevalues") {
4066
+ const count = Array.isArray(output?.details?.values) ? output.details.values.length : 0;
4067
+ await audit("fill", `Generated ${count} realistic value${count === 1 ? "" : "s"} with the reviewed seed and locale; real looking card numbers and personal identifiers are refused.`, extra);
4068
+ }
4069
+ return output ?? { ok: false, summary: "The forms and data step returned no result." };
4070
+ }
4071
+ }
4072
+ }
4073
+ async function loaddataset(datasetid) {
4074
+ const record2 = await memory.getdataset(datasetid);
4075
+ if (!record2) throw new Error(`No dataset ${datasetid} exists yet; scrape or import it first.`);
4076
+ return record2;
4077
+ }
4078
+ function readgridoutput(output) {
4079
+ const grid = output?.details?.grid;
4080
+ if (!grid || typeof grid !== "object") return null;
4081
+ return grid;
4082
+ }
4083
+ async function storeexport(stepid, datasetvalue, format, delimiter, session, planid, origin) {
4084
+ const content = exportcontent(datasetvalue, format, delimiter);
4085
+ const artifact = exportartifact(randomid(), datasetvalue, format, stepid, content, Date.now());
4086
+ await memory.addexport(artifact);
4087
+ await memory.addartifact(artifactrecordof(artifact));
4088
+ await memory.addprovenance(provenancefor(artifact, datasetvalue.sources[0]?.url ?? origin, stepid, Date.now()));
4089
+ await audit("export", `Exported ${artifact.rowcount} row${artifact.rowcount === 1 ? "" : "s"} of dataset ${datasetvalue.name} into the ${format} artifact ${artifact.name} with checksum ${artifact.checksum}; the provenance record keeps the source url, step ref and row range.`, { ...session ? { sessionid: session.id } : {}, planid, stepid });
4090
+ await refreshbadge();
4091
+ return artifact;
4092
+ }
4093
+ async function executedatastep(step, session, plan, tabid2, origin) {
4094
+ const options = stepoptions2(step);
4095
+ const extra = { ...session ? { sessionid: session.id } : {}, planid: plan.id, stepid: step.id };
4096
+ switch (step.kind) {
4097
+ case "scrapetable": {
4098
+ const output = await dispatchpagestep(step, tabid2, origin, plan);
4099
+ const grid = readgridoutput(output);
4100
+ if (!output?.ok || !grid) return output ?? { ok: false, summary: "The table scrape returned no result." };
4101
+ const id = randomid();
4102
+ const name = typeof options.name === "string" && options.name ? options.name : `dataset-${step.id}`;
4103
+ const datasetvalue = builddataset(id, name, grid, Date.now());
4104
+ await memory.setdataset(datasetvalue);
4105
+ const extract = advancecursor(newextractsession(randomid(), id, name, step.target ?? "", "", 1, Date.now()), origin, datasetvalue.rows.length, Date.now(), true);
4106
+ await memory.setextractsession(extract);
4107
+ await memory.setprogress(recordextraction(await memory.getprogress(), plan.id, step.id, { page: 1, rows: datasetvalue.rows.length, cursor: 1 }, Date.now()));
4108
+ await refreshbadge();
4109
+ return { ok: true, summary: output.summary, details: { ...output.details ?? {}, dataset: { id, name, rows: datasetvalue.rows.length, columns: grid.columns.length } } };
4110
+ }
4111
+ case "paginateextract": {
4112
+ const output = await dispatchpagestep(step, tabid2, origin, plan);
4113
+ const grid = readgridoutput(output);
4114
+ if (!output?.ok || !grid) return output ?? { ok: false, summary: "The paginated extraction returned no result." };
4115
+ const id = randomid();
4116
+ const name = typeof options.name === "string" && options.name ? options.name : `dataset-${step.id}`;
4117
+ const datasetvalue = builddataset(id, name, grid, Date.now());
4118
+ await memory.setdataset(datasetvalue);
4119
+ const planned = typeof options.pages === "number" && Number.isInteger(options.pages) && options.pages > 0 ? options.pages : Number(output.details?.pages ?? 1);
4120
+ const next = output.details?.next === true;
4121
+ const extract = advancecursor(newextractsession(randomid(), id, name, step.target ?? "", typeof options.next === "string" ? options.next : "", planned, Date.now()), origin, datasetvalue.rows.length, Date.now(), !next);
4122
+ await memory.setextractsession(extract);
4123
+ await memory.setprogress(recordextraction(await memory.getprogress(), plan.id, step.id, { page: extract.cursor, rows: datasetvalue.rows.length, cursor: extract.cursor }, Date.now()));
4124
+ await refreshbadge();
4125
+ const extractedpages = Number(output.details?.pages ?? 1);
4126
+ const estimated = Math.max(1, Math.round(datasetvalue.rows.length / Math.max(1, extractedpages) * planned));
4127
+ return { ok: true, summary: output.summary, details: { ...output.details ?? {}, dataset: { id, name, rows: datasetvalue.rows.length, columns: grid.columns.length }, extractsession: { id: extract.id, cursor: extract.cursor, planned, done: extract.done === true }, progressshare: extractionshare(datasetvalue.rows.length, estimated) } };
4128
+ }
4129
+ case "resumeextract": {
4130
+ const sessionid = typeof options.session === "string" ? options.session : "";
4131
+ const extract = (await memory.getextractsessions()).find((item) => item.id === sessionid);
4132
+ if (!extract) throw new Error(`No extract session ${sessionid} is stored yet.`);
4133
+ if (extract.done) return { ok: true, summary: `Extraction ${extract.name} already completed at cursor ${extract.cursor}.`, details: { cursor: extract.cursor, done: true } };
4134
+ const datasetvalue = await loaddataset(extract.datasetid);
4135
+ const remaining = remainingpages(extract, extract.planned);
4136
+ const derived = { id: step.id, kind: "paginateextract", target: extract.target, summary: step.summary, risk: "sensitive", options: JSON.stringify({ next: extract.next, pages: remaining, cursor: extract.cursor, name: extract.name }) };
4137
+ const output = await dispatchpagestep(derived, tabid2, origin, plan);
4138
+ const grid = readgridoutput(output);
4139
+ if (!output?.ok || !grid) return output ?? { ok: false, summary: "The resumed extraction returned no result." };
4140
+ const merged = mergedatasets([{ columns: datasetvalue.columns, rows: datasetvalue.rows }, { columns: grid.columns, rows: grid.rows }]);
4141
+ const updated = { ...datasetvalue, columns: merged.columns, rows: merged.rows, at: Date.now() };
4142
+ await memory.setdataset(updated);
4143
+ const next = output.details?.next === true;
4144
+ const resumed = advancecursor({ ...extract, updatedat: Date.now() }, origin, grid.rows.length, Date.now(), !next);
4145
+ await memory.setextractsession(resumed);
4146
+ await memory.setprogress(recordextraction(await memory.getprogress(), plan.id, step.id, { page: resumed.cursor, rows: grid.rows.length, cursor: resumed.cursor }, Date.now()));
4147
+ await audit("resume", `Extraction ${extract.name} resumed from cursor ${extract.cursor} and collected ${grid.rows.length} further row${grid.rows.length === 1 ? "" : "s"} across ${Number(output.details?.pages ?? 1)} page${Number(output.details?.pages ?? 1) === 1 ? "" : "s"}; the dataset now holds ${updated.rows.length} rows.`, extra);
4148
+ return { ok: true, summary: `Resumed extraction ${extract.name} from cursor ${extract.cursor}; the dataset now holds ${updated.rows.length} rows.`, details: { dataset: { id: updated.id, rows: updated.rows.length }, extractsession: { id: resumed.id, cursor: resumed.cursor, planned: resumed.planned, done: resumed.done === true } } };
4149
+ }
4150
+ case "mergepages": {
4151
+ const ids = Array.isArray(options.datasets) ? options.datasets.filter((item) => typeof item === "string" && item.trim().length > 0) : [];
4152
+ const records = [];
4153
+ for (const id2 of ids) records.push(await loaddataset(id2));
4154
+ const merged = mergedatasets(records);
4155
+ const id = randomid();
4156
+ const datasetvalue = { id, name: typeof options.name === "string" && options.name ? options.name : `merged-${step.id}`, columns: merged.columns, rows: merged.rows, sources: records.flatMap((record2) => record2.sources), at: Date.now() };
4157
+ await memory.setdataset(datasetvalue);
4158
+ await refreshbadge();
4159
+ return { ok: true, summary: `Merged ${records.length} dataset${records.length === 1 ? "" : "s"} into ${datasetvalue.name} with ${merged.columns.length} aligned column${merged.columns.length === 1 ? "" : "s"} and ${merged.rows.length} row${merged.rows.length === 1 ? "" : "s"}.`, details: { dataset: { id, name: datasetvalue.name, rows: merged.rows.length, columns: merged.columns.length }, merged: records.map((record2) => record2.id) } };
4160
+ }
4161
+ case "transformvalues": {
4162
+ const datasetvalue = await loaddataset(typeof options.dataset === "string" ? options.dataset : "");
4163
+ const rules = (Array.isArray(options.rules) ? options.rules : []).filter((item) => Boolean(item) && typeof item === "object" && !Array.isArray(item));
4164
+ const applied = transformrows(datasetvalue.rows, rules);
4165
+ await memory.setdataset({ ...datasetvalue, rows: applied.rows, at: Date.now() });
4166
+ const existing = (await memory.gettaskrules()).find((item) => item.taskid === plan.id);
4167
+ await memory.settaskrules(mergetaskrules(existing, plan.id, rules, [], Date.now()));
4168
+ if (applied.errors.length > 0) return { ok: false, summary: `Transform rules surfaced ${applied.errors.length} error${applied.errors.length === 1 ? "" : "s"}: ${applied.errors.join("; ")}.`, details: { errors: applied.errors, rules: rules.length } };
4169
+ return { ok: true, summary: `Applied ${rules.length} reviewed transform rule${rules.length === 1 ? "" : "s"} to ${applied.rows.length} row${applied.rows.length === 1 ? "" : "s"}.`, details: { rules: rules.length, rows: applied.rows.length } };
4170
+ }
4171
+ case "deduperows": {
4172
+ const datasetvalue = await loaddataset(typeof options.dataset === "string" ? options.dataset : "");
4173
+ const keys = Array.isArray(options.keys) ? options.keys.filter((item) => typeof item === "string" && item.trim().length > 0) : [];
4174
+ const result = dedupebykeys(datasetvalue.rows, keys);
4175
+ await memory.setdataset({ ...datasetvalue, rows: result.kept, at: Date.now() });
4176
+ const existing = (await memory.gettaskrules()).find((item) => item.taskid === plan.id);
4177
+ await memory.settaskrules(mergetaskrules(existing, plan.id, [], keys, Date.now()));
4178
+ return { ok: true, summary: `Deduplicated ${datasetvalue.rows.length} row${datasetvalue.rows.length === 1 ? "" : "s"} by ${keys.join(", ")}: removed ${result.removed} duplicate${result.removed === 1 ? "" : "s"}, kept ${result.kept.length}.`, details: { dedupe: { removed: result.removed, kept: result.kept.length, keys } } };
4179
+ }
4180
+ case "stamplerows": {
4181
+ const datasetvalue = await loaddataset(typeof options.dataset === "string" ? options.dataset : "");
4182
+ const url = typeof options.url === "string" && options.url ? options.url : origin;
4183
+ const stamped = samplerows(datasetvalue.rows, url, step.id, Date.now());
4184
+ await memory.setdataset({ ...datasetvalue, rows: stamped.rows, sources: stamped.sources, at: Date.now() });
4185
+ return { ok: true, summary: `Stamped ${stamped.rows.length} row${stamped.rows.length === 1 ? "" : "s"} with the source url, timestamp and step ref.`, details: { rows: stamped.rows.length, url } };
4186
+ }
4187
+ case "previewgrid": {
4188
+ const datasetvalue = await loaddataset(typeof options.dataset === "string" ? options.dataset : "");
4189
+ const sample = typeof options.sample === "number" && Number.isInteger(options.sample) && options.sample > 0 ? options.sample : 10;
4190
+ const preview = gridpreview(datasetvalue, sample);
4191
+ return { ok: true, summary: `Previewed ${preview.rows} row${preview.rows === 1 ? "" : "s"} of dataset ${datasetvalue.name} in the grid with ${preview.columns.length} column${preview.columns.length === 1 ? "" : "s"}.`, details: { preview } };
4192
+ }
4193
+ case "importcsv": {
4194
+ const csv = typeof options.csv === "string" ? options.csv : "";
4195
+ const parsed = parsecsv(csv, typeof options.delimiter === "string" && options.delimiter.length === 1 ? options.delimiter : ",");
4196
+ const mapping = options.mapping && typeof options.mapping === "object" && !Array.isArray(options.mapping) ? options.mapping : {};
4197
+ const columns = mapcolumns(parsed.headers, mapping);
4198
+ const rows = parsed.rows.map((line) => {
4199
+ const row = {};
4200
+ columns.forEach((column, index) => {
4201
+ row[column.key] = line[index] ?? "";
4202
+ });
4203
+ return row;
4204
+ });
4205
+ const id = randomid();
4206
+ const datasetvalue = { id, name: typeof options.name === "string" && options.name ? options.name : `import-${step.id}`, columns, rows, sources: [], at: Date.now() };
4207
+ await memory.addimport(datasetvalue);
4208
+ await refreshbadge();
4209
+ return { ok: true, summary: `Imported ${rows.length} row${rows.length === 1 ? "" : "s"} and ${columns.length} mapped column${columns.length === 1 ? "" : "s"} from the reviewed csv for fill loops.`, details: { dataset: { id, name: datasetvalue.name, rows: rows.length, columns: columns.length } } };
4210
+ }
4211
+ case "looprows": {
4212
+ const datasetvalue = await loaddataset(typeof options.dataset === "string" ? options.dataset : "");
4213
+ const inner = resolvedinnerstep(step, plan);
4214
+ if (!inner) throw new Error("A reviewed inner step or step id is required in options.");
4215
+ const variable = typeof options.variable === "string" && options.variable ? options.variable : "row";
4216
+ let completed = 0;
4217
+ let failed = 0;
4218
+ for (const [index, row] of datasetvalue.rows.entries()) {
4219
+ const derived = loopstep(inner, row);
4220
+ const output = await dispatchpagestep(derived, tabid2, origin, plan);
4221
+ if (output?.ok) completed += 1;
4222
+ else failed += 1;
4223
+ await memory.setprogress(recordoutcome(await memory.getprogress(), plan.id, { stepid: step.id, ok: Boolean(output?.ok), summary: `Iteration ${index + 1} of ${datasetvalue.rows.length}: ${output?.summary ?? "no result"}`, details: { iteration: index + 1, variable, variables: loopvariables(row) }, at: Date.now() }, Date.now()));
4224
+ }
4225
+ return { ok: failed === 0, summary: `Looped ${datasetvalue.rows.length} dataset row${datasetvalue.rows.length === 1 ? "" : "s"} as ${variable} variables: ${completed} iteration${completed === 1 ? "" : "s"} completed${failed > 0 ? `, ${failed} failed` : ""}.`, details: { iterations: datasetvalue.rows.length, completed, failed, variable } };
4226
+ }
4227
+ case "exportcsv":
4228
+ case "exportjson":
4229
+ case "exportexcel": {
4230
+ const datasetvalue = await loaddataset(typeof options.dataset === "string" ? options.dataset : "");
4231
+ const format = step.kind === "exportcsv" ? "csv" : step.kind === "exportjson" ? "json" : "excel";
4232
+ const delimiter = typeof options.delimiter === "string" && options.delimiter.length === 1 ? options.delimiter : ",";
4233
+ const artifact = await storeexport(step.id, { ...datasetvalue, ...typeof options.name === "string" && options.name ? { name: options.name } : {} }, format, delimiter, session, plan.id, origin);
4234
+ return { ok: true, summary: `Exported ${artifact.rowcount} row${artifact.rowcount === 1 ? "" : "s"} of dataset ${datasetvalue.name} to ${artifact.name} with checksum ${artifact.checksum}.`, details: { artifact: { id: artifact.id, name: artifact.name, kind: artifact.kind, checksum: artifact.checksum, rowcount: artifact.rowcount } } };
4235
+ }
4236
+ case "copytable": {
4237
+ const datasetvalue = await loaddataset(typeof options.dataset === "string" ? options.dataset : "");
4238
+ const content = exportcontent(datasetvalue, "csv", typeof options.delimiter === "string" && options.delimiter.length === 1 ? options.delimiter : ",");
4239
+ await navigator.clipboard.writeText(content);
4240
+ await audit("export", `Copied ${datasetvalue.rows.length} row${datasetvalue.rows.length === 1 ? "" : "s"} of dataset ${datasetvalue.name} to the clipboard under the clipboardWrite capability.`, extra);
4241
+ return { ok: true, summary: `Copied ${datasetvalue.rows.length} row${datasetvalue.rows.length === 1 ? "" : "s"} to the clipboard.`, details: { rows: datasetvalue.rows.length } };
4242
+ }
4243
+ case "pushsheets": {
4244
+ const datasetvalue = await loaddataset(typeof options.dataset === "string" ? options.dataset : "");
4245
+ const sheet = typeof options.sheet === "string" ? options.sheet : "";
4246
+ const config = (await memory.getsheetendpoints()).find((item) => item.endpoint === sheet || item.origin === new URL(sheet).origin);
4247
+ if (!config) throw new Error(`The reviewed sheet endpoint has not been configured; configure ${sheet} from the review panel first.`);
4248
+ const granted = await chrome.permissions.contains({ origins: [hostpattern(config.origin)] });
4249
+ if (!granted) throw new Error(`The sheet endpoint origin ${config.origin} has not received optional permission.`);
4250
+ const payload = sheetpayload(datasetvalue, sheet);
4251
+ const response = await fetch(config.endpoint, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(payload) });
4252
+ await audit("export", `Pushed ${datasetvalue.rows.length} row${datasetvalue.rows.length === 1 ? "" : "s"} of dataset ${datasetvalue.name} to the reviewed sheet endpoint ${config.origin} with response ${response.status}.`, extra);
4253
+ return { ok: response.ok, summary: response.ok ? `Pushed ${datasetvalue.rows.length} row${datasetvalue.rows.length === 1 ? "" : "s"} to the reviewed sheet endpoint ${config.origin}.` : `The reviewed sheet endpoint answered ${response.status}; the push failed.`, details: { endpoint: config.endpoint, status: response.status, rows: datasetvalue.rows.length } };
4254
+ }
4255
+ case "streamdisk": {
4256
+ const datasetvalue = await loaddataset(typeof options.dataset === "string" ? options.dataset : "");
4257
+ const chunk = typeof options.chunk === "number" && Number.isInteger(options.chunk) && options.chunk > 0 ? options.chunk : datasetvalue.rows.length || 1;
4258
+ const planchunks = chunkplan(datasetvalue.rows.length, chunk);
4259
+ const previous = (await memory.getstreams()).find((item) => item.datasetid === datasetvalue.id && item.done !== true);
4260
+ let state = previous && previous.chunks === planchunks.length ? previous : newstream(datasetvalue, planchunks.length, Date.now());
4261
+ let written = 0;
4262
+ let acknowledged = 0;
4263
+ for (const part of planchunks.slice(state.chunk)) {
4264
+ written += 1;
4265
+ if (backpressure(written, acknowledged)) await new Promise((resolve) => setTimeout(resolve, 0));
4266
+ state = advancestream(state, part, Date.now(), part.to >= datasetvalue.rows.length);
4267
+ await memory.setstream(state);
4268
+ acknowledged += 1;
4269
+ }
4270
+ const artifact = await storeexport(step.id, datasetvalue, "csv", ",", session, plan.id, origin);
4271
+ await audit("stream", `Streamed ${datasetvalue.rows.length} row${datasetvalue.rows.length === 1 ? "" : "s"} of dataset ${datasetvalue.name} to disk in ${planchunks.length} reviewed chunk${planchunks.length === 1 ? "" : "s"} of ${chunk} row${chunk === 1 ? "" : "s"} with backpressure; the stream state stays persisted for resume.`, extra);
4272
+ return { ok: true, summary: `Streamed ${datasetvalue.rows.length} row${datasetvalue.rows.length === 1 ? "" : "s"} to disk in ${planchunks.length} chunk${planchunks.length === 1 ? "" : "s"} and landed the artifact ${artifact.name}.`, details: { chunks: planchunks.length, chunk, rows: datasetvalue.rows.length, artifact: { id: artifact.id, name: artifact.name, checksum: artifact.checksum } } };
4273
+ }
4274
+ case "logprovenance": {
4275
+ const reference = typeof options.artifact === "string" ? options.artifact : "";
4276
+ const artifact = (await memory.getexports()).find((item) => item.id === reference || item.name === reference);
4277
+ if (!artifact) throw new Error(`No exported artifact ${reference} exists yet.`);
4278
+ const record2 = provenancefor(artifact, origin, step.id, Date.now());
4279
+ await memory.addprovenance(record2);
4280
+ await audit("provenance", `Provenance of artifact ${artifact.name}: rows ${record2.rowstart} to ${record2.rowend}, checksum ${record2.checksum}, source ${record2.url}.`, extra);
4281
+ return { ok: true, summary: `Logged the provenance of ${artifact.name} for audit: rows ${record2.rowstart} to ${record2.rowend} with checksum ${record2.checksum}.`, details: { provenance: record2 } };
4282
+ }
4283
+ default:
4284
+ return { ok: false, summary: "Unsupported forms and data step." };
4285
+ }
4286
+ }
3070
4287
  async function enforcewindowreview(step, session, plan) {
3071
4288
  const windowid = step.value && /^\d+$/.test(step.value) ? Number.parseInt(step.value, 10) : 0;
3072
4289
  const progress = plan ? await memory.getprogress() : void 0;
@@ -3101,8 +4318,10 @@ async function togglecontroltab(enabled) {
3101
4318
  async function refreshbadge() {
3102
4319
  const queues = await memory.getnavqueues();
3103
4320
  const badges = await memory.getbadges();
4321
+ const prompts = (await memory.gettickets()).filter((ticket) => ticket.approved === void 0).length;
4322
+ const datasets = (await memory.getdatasets()).length;
3104
4323
  const tasktabs2 = new Set(badges.map((badge) => badge.tabid)).size;
3105
- const total = (queues?.prefetch ?? 0) + (queues?.batchopen ?? 0) + tasktabs2;
4324
+ const total = (queues?.prefetch ?? 0) + (queues?.batchopen ?? 0) + tasktabs2 + prompts + datasets;
3106
4325
  await chrome.action.setBadgeText({ text: total > 0 ? String(total) : "" }).catch(() => {
3107
4326
  });
3108
4327
  }
@@ -3126,6 +4345,10 @@ async function executestep(stepid) {
3126
4345
  }
3127
4346
  if (istabscommandkind(step.kind)) {
3128
4347
  output = await executetabscommand(step, session, plan, tab.id);
4348
+ } else if (isdatasetkind(step.kind)) {
4349
+ output = await executedatastep(step, session, plan, tab.id, origin);
4350
+ } else if (isformkind(step.kind)) {
4351
+ output = await executeformstep(step, session, plan, tab.id, origin);
3129
4352
  } else if (isbrowserkind(step.kind)) {
3130
4353
  output = await runbrowseraction(step, tab.id, tab.windowId ?? chrome.windows.WINDOW_ID_CURRENT);
3131
4354
  } else if (step.kind === "keyhold") {
@@ -3231,6 +4454,12 @@ async function grantcapability(permission) {
3231
4454
  await audit("capability", `Capability ${permission} granted by the user.`);
3232
4455
  return refreshcapabilities();
3233
4456
  }
4457
+ async function extractionreportValue() {
4458
+ return extractionreport({ sessions: await memory.getextractsessions() });
4459
+ }
4460
+ async function provenancereportValue() {
4461
+ return provenancereport({ records: await memory.getprovenances() });
4462
+ }
3234
4463
  async function handlerequest(message, sender) {
3235
4464
  if (!extensionpage(sender)) throw new Error("Requests are accepted only from Devthink extension pages.");
3236
4465
  const input = message;
@@ -3275,13 +4504,33 @@ async function handlerequest(message, sender) {
3275
4504
  const closedtabs = await memory.getclosedtabs();
3276
4505
  const controltab = await memory.getcontroltab();
3277
4506
  const tabwatchevents = await memory.gettabwatchevents();
4507
+ const profiles = await memory.getprofiles();
4508
+ const tickets = await memory.gettickets();
4509
+ const wizards = await memory.getwizards();
4510
+ const picks = await memory.getpicks();
4511
+ const errorreports = await memory.geterrorreports();
4512
+ const captchas = await memory.getcaptchas();
4513
+ const detections = await memory.getdetections();
4514
+ const codeentry = await memory.getcodevalue();
4515
+ const datasets = await memory.getdatasets();
4516
+ const imports = await memory.getimports();
4517
+ const extractsessions = await memory.getextractsessions();
4518
+ const streams = await memory.getstreams();
4519
+ const exports = (await memory.getexports()).map((artifact) => ({ id: artifact.id, kind: artifact.kind, name: artifact.name, stepid: artifact.stepid, rowcount: artifact.rowcount, checksum: artifact.checksum, at: artifact.at }));
4520
+ const provenances = await memory.getprovenances();
4521
+ const taskrules = await memory.gettaskrules();
4522
+ const sheetendpoints = await memory.getsheetendpoints();
4523
+ const sheetgrants = [];
4524
+ for (const config of sheetendpoints) {
4525
+ sheetgrants.push({ ...config, granted: await chrome.permissions.contains({ origins: [hostpattern(config.origin)] }).catch(() => false) });
4526
+ }
3278
4527
  const clones = clonetabs(tabs);
3279
4528
  const taskgauge = tasktabgauge(tabs.filter((tab) => badges.some((badge) => badge.tabid === tab.tabid)).length, tasktabceiling(await memory.getsettings()));
3280
4529
  const report = await buildtabreport(tabs);
3281
4530
  const livetab = session ? await chrome.tabs.get(session.tabid).catch(() => void 0) : void 0;
3282
4531
  const waitprofile = session ? waitprofiles.find((record2) => record2.origin === session.origin) : void 0;
3283
4532
  const livestate = { phase: livetab?.status === "loading" ? "loading" : "complete", ...navrecords[0] ? { finalurl: navrecords[0].finalurl, redirects: navrecords[0].chain } : {} };
3284
- 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 };
4533
+ 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 };
3285
4534
  }
3286
4535
  case "capabilities":
3287
4536
  return refreshcapabilities();
@@ -3477,6 +4726,141 @@ async function handlerequest(message, sender) {
3477
4726
  await audit("window", `The review panel closed window ${inputclose.windowid}${count > 0 ? ` while holding ${count} task tab${count === 1 ? "" : "s"} under explicit review` : ""}.`, { ...session ? { sessionid: session.id } : {} });
3478
4727
  return { windowid: inputclose.windowid, closed: true };
3479
4728
  }
4729
+ case "applyprofile": {
4730
+ const session = await memory.getsession();
4731
+ if (!session || session.stoppedat || session.expiresat <= Date.now()) throw new Error("Profile application stays inside the consent gate of an active session.");
4732
+ const inputprofile = message;
4733
+ const profile = await memory.getprofile(inputprofile.name ?? "");
4734
+ if (!profile) throw new Error(`No form profile named ${inputprofile.name ?? ""} is stored yet.`);
4735
+ const gate = profilegrantgranted(profile, session.origin);
4736
+ if (!gate.allowed) throw new Error(gate.reason);
4737
+ await audit("fill", `Form profile ${profile.name} applied as the reviewed field map for ${session.origin} under its origin grants.`, { sessionid: session.id });
4738
+ return { profile: { name: profile.name, fields: profile.fields, grants: profile.grants } };
4739
+ }
4740
+ case "approvesubmit": {
4741
+ const inputticket = message;
4742
+ const tickets = await memory.gettickets();
4743
+ const ticket = tickets.find((item) => item.id === inputticket.id);
4744
+ if (!ticket) throw new Error("No submission ticket matches the requested id.");
4745
+ const updated = { ...ticket, approved: inputticket.approved !== false };
4746
+ await memory.setticket(updated);
4747
+ const session = await memory.getsession();
4748
+ await audit("submit", `Submission ticket ${ticket.id} for form ${ticket.form} ${updated.approved ? "approved" : "declined"} by the user with values hash ${ticket.valueshash}.`, { ...session ? { sessionid: session.id } : {} });
4749
+ await refreshbadge();
4750
+ return updated;
4751
+ }
4752
+ case "resolvecaptcha": {
4753
+ const open = (await memory.getcaptchas()).find((item) => !item.resolved);
4754
+ if (!open) throw new Error("No open captcha handoff exists.");
4755
+ await memory.resolvecaptcha(open.id, Date.now());
4756
+ const session = await memory.getsession();
4757
+ if (session?.pausedat && !session.stoppedat) {
4758
+ const resumed = { id: session.id, tabid: session.tabid, origin: session.origin, startedat: session.startedat, expiresat: session.expiresat, ...session.grants ? { grants: session.grants } : {} };
4759
+ await memory.setsession(resumed);
4760
+ }
4761
+ await audit("handoff", `Captcha handoff ${open.id} resolved by the user after ${Date.now() - open.openedat} milliseconds; the plan continues.`, { ...session ? { sessionid: session.id } : {} });
4762
+ await refreshbadge();
4763
+ return { resolved: true, id: open.id };
4764
+ }
4765
+ case "storecode": {
4766
+ const session = await memory.getsession();
4767
+ if (!session || session.stoppedat || session.expiresat <= Date.now()) throw new Error("The one time code entry stays behind the consent gate of an active session.");
4768
+ const inputcode = message;
4769
+ if (!inputcode.code?.trim()) throw new Error("A non-empty one time code is required.");
4770
+ await memory.setcodevalue(inputcode.code.trim());
4771
+ await audit("consent", `A one time code was stored behind the consent gate of the active session; the value never appears in the audit trail.`, { sessionid: session.id });
4772
+ return { stored: true };
4773
+ }
4774
+ case "regeneratevalue": {
4775
+ const inputvalue = message;
4776
+ if (typeof inputvalue.field !== "string" || !inputvalue.field) throw new Error("A field kind is required to regenerate a value.");
4777
+ const rule = {};
4778
+ if (typeof inputvalue.locale === "string" && inputvalue.locale) rule.locale = inputvalue.locale;
4779
+ if (typeof inputvalue.seed === "number" && Number.isFinite(inputvalue.seed)) rule.seed = inputvalue.seed;
4780
+ const value = generatevalue(inputvalue.field, rule);
4781
+ const verdict = generatedvalueallowed(value);
4782
+ if (!verdict.allowed) throw new Error(verdict.reason ?? "The regenerated value was refused.");
4783
+ return { kind: inputvalue.field, value, locale: rule.locale ?? "en", seed: rule.seed ?? 1 };
4784
+ }
4785
+ case "removeprofile": {
4786
+ const inputprofile = message;
4787
+ const profile = await memory.getprofile(inputprofile.name ?? "");
4788
+ if (!profile) throw new Error(`No form profile named ${inputprofile.name ?? ""} is stored yet.`);
4789
+ await memory.removeprofile(profile.name);
4790
+ const session = await memory.getsession();
4791
+ await audit("fill", `Form profile ${profile.name} removed from local memory by the user.`, { ...session ? { sessionid: session.id } : {} });
4792
+ return { removed: true, name: profile.name };
4793
+ }
4794
+ case "formreport": {
4795
+ const plan = await memory.getplan();
4796
+ if (!plan) throw new Error("No plan is available for a form report envelope.");
4797
+ const outcome = (await memory.getoutcomes()).find((candidate) => plan.steps.some((step) => step.id === candidate.stepid && step.kind === "detectfields"));
4798
+ const report = outcome?.details?.report;
4799
+ if (!report) throw new Error("No form report has been captured yet.");
4800
+ return JSON.parse(formreportresponse({ report, plan }));
4801
+ }
4802
+ case "errorreport": {
4803
+ const plan = await memory.getplan();
4804
+ if (!plan) throw new Error("No plan is available for an error report envelope.");
4805
+ const report = (await memory.geterrorreports())[0];
4806
+ if (!report) throw new Error("No error report has been collected yet.");
4807
+ return JSON.parse(errorreportresponse({ report, plan }));
4808
+ }
4809
+ case "configuresheet": {
4810
+ const inputsheet = message;
4811
+ const config = normalizeendpoint(inputsheet.endpoint ?? "");
4812
+ const granted = await chrome.permissions.contains({ origins: [hostpattern(config.origin)] });
4813
+ if (!granted) throw new Error("The sheet endpoint origin has not received optional permission.");
4814
+ const record2 = { endpoint: config.endpoint, origin: config.origin, configuredat: Date.now() };
4815
+ await memory.setsheetendpoint(record2);
4816
+ await audit("configure", `Configured the reviewed sheet endpoint ${config.origin} for data pushes; pushes need the explicit reviewed flag.`);
4817
+ return record2;
4818
+ }
4819
+ case "exportdataset": {
4820
+ const session = await memory.getsession();
4821
+ if (!session || session.stoppedat || session.expiresat <= Date.now()) throw new Error("Data exports stay behind the consent gate of an active session.");
4822
+ const { tab, origin } = await activecontext();
4823
+ const gate = exportgranted(session, origin);
4824
+ if (!gate.allowed) throw new Error(gate.reason);
4825
+ const inputexport = message;
4826
+ const format = inputexport.format === "json" ? "json" : inputexport.format === "excel" ? "excel" : "csv";
4827
+ const datasetvalue = await loaddataset(inputexport.datasetid ?? "");
4828
+ const artifact = await storeexport("panel", { ...datasetvalue, ...inputexport.name?.trim() ? { name: inputexport.name.trim() } : {} }, format, ",", session, "", origin);
4829
+ void tab;
4830
+ return { id: artifact.id, kind: artifact.kind, name: artifact.name, rowcount: artifact.rowcount, checksum: artifact.checksum, at: artifact.at };
4831
+ }
4832
+ case "importcsv": {
4833
+ const session = await memory.getsession();
4834
+ if (!session || session.stoppedat || session.expiresat <= Date.now()) throw new Error("Csv imports stay behind the consent gate of an active session.");
4835
+ const inputimport = message;
4836
+ const parsed = parsecsv(inputimport.csv ?? "");
4837
+ if (parsed.headers.length === 0) throw new Error("The reviewed csv needs a header line.");
4838
+ const columns = mapcolumns(parsed.headers, inputimport.mapping ?? {});
4839
+ const rows = parsed.rows.map((line) => {
4840
+ const row = {};
4841
+ columns.forEach((column, index) => {
4842
+ row[column.key] = line[index] ?? "";
4843
+ });
4844
+ return row;
4845
+ });
4846
+ const datasetvalue = { id: randomid(), name: inputimport.name?.trim() || `import-${Date.now()}`, columns, rows, sources: [], at: Date.now() };
4847
+ await memory.addimport(datasetvalue);
4848
+ await audit("scrape", `The review panel imported ${rows.length} row${rows.length === 1 ? "" : "s"} from the reviewed csv as dataset ${datasetvalue.name} for fill loops.`, { sessionid: session.id });
4849
+ await refreshbadge();
4850
+ return { id: datasetvalue.id, name: datasetvalue.name, rows: rows.length, columns: columns.length };
4851
+ }
4852
+ case "dataset": {
4853
+ const plan = await memory.getplan();
4854
+ if (!plan) throw new Error("No plan is available for a dataset envelope.");
4855
+ const inputdataset = message;
4856
+ const datasetvalue = await memory.getdataset(inputdataset.datasetid ?? "");
4857
+ if (!datasetvalue) throw new Error("No dataset has been captured yet.");
4858
+ return JSON.parse(datasetresponse({ dataset: datasetvalue, plan, ...typeof inputdataset.sample === "number" ? { sample: inputdataset.sample } : {} }));
4859
+ }
4860
+ case "extraction":
4861
+ return extractionreportValue();
4862
+ case "provenance":
4863
+ return provenancereportValue();
3480
4864
  case "stop": {
3481
4865
  const session = await memory.getsession();
3482
4866
  if (session) await memory.setsession({ ...session, stoppedat: Date.now() });