@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.
@@ -1,9 +1,9 @@
1
1
  "use strict";
2
2
  (() => {
3
3
  // policy.ts
4
- 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"]);
4
+ 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"]);
5
5
  var interactionactions = /* @__PURE__ */ new Set(["focus", "scroll", "hover", "clickdeep", "rightclick", "doubleclick", "scrollpage", "scrollby", "scrollend", "scrolltop", "fullscreen", "zoomset", "movepointer", "clicktext", "clickaria", "clickname", "expanddetails", "pierceshadow", "retryaction"]);
6
- 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"]);
6
+ 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"]);
7
7
  var allowedactions = /* @__PURE__ */ new Set([...sensitiveactions, ...interactionactions, ...readactions]);
8
8
  function parseoptions(step) {
9
9
  if (step.options === void 0) return {};
@@ -2107,12 +2107,12 @@
2107
2107
  options = {};
2108
2108
  }
2109
2109
  const scopes = Array.isArray(options.scopes) ? options.scopes.filter((item) => typeof item === "string" && item.trim().length > 0) : void 0;
2110
- const events3 = Array.isArray(options.events) ? options.events.filter((item) => typeof item === "string" && item.trim().length > 0) : void 0;
2110
+ const events4 = Array.isArray(options.events) ? options.events.filter((item) => typeof item === "string" && item.trim().length > 0) : void 0;
2111
2111
  const lifetime = typeof options.lifetime === "number" && Number.isFinite(options.lifetime) && options.lifetime > 0 ? options.lifetime : 0;
2112
2112
  return {
2113
2113
  watchid: typeof options.watchid === "string" && options.watchid.trim() ? options.watchid : fallbackid,
2114
2114
  ...scopes ? { scopes } : {},
2115
- ...events3 ? { events: events3 } : {},
2115
+ ...events4 ? { events: events4 } : {},
2116
2116
  lifetime,
2117
2117
  poll: typeof options.poll === "number" && Number.isFinite(options.poll) && options.poll >= 0 ? options.poll : defaultpoll
2118
2118
  };
@@ -2358,6 +2358,765 @@
2358
2358
  }
2359
2359
  }
2360
2360
 
2361
+ // extension/pageforms.ts
2362
+ function matchfield(fields, match) {
2363
+ const key = match.mode === "label" ? "label" : match.mode === "placeholder" ? "placeholder" : match.mode === "arialabel" ? "arialabel" : "name";
2364
+ const needle = (match[key] ?? "").trim().toLowerCase();
2365
+ if (!needle) return [];
2366
+ return fields.filter((field) => {
2367
+ const primary = field[key].toLowerCase();
2368
+ const secondary = match.mode === "label" || match.mode === "name" ? field.name.toLowerCase() : match.mode === "placeholder" ? field.arialabel.toLowerCase() : field.placeholder.toLowerCase();
2369
+ return primary.includes(needle) || secondary.includes(needle);
2370
+ });
2371
+ }
2372
+ function classifyfield(input) {
2373
+ const type = input.type.toLowerCase();
2374
+ const autocomplete = input.autocomplete.toLowerCase();
2375
+ const label = input.label.toLowerCase();
2376
+ if (type === "password") return "password";
2377
+ if (autocomplete.startsWith("cc-") || label.includes("card number") || label.includes("credit card") || label.includes("cardholder")) return "card";
2378
+ if (autocomplete.includes("one-time-code") || autocomplete.includes("otp") || label.includes("one time code") || label.includes("verification code") || label.includes("otp")) return "code";
2379
+ if (type === "email" || autocomplete.includes("email") || label.includes("email")) return "email";
2380
+ if (type === "tel" || autocomplete.includes("tel") || label.includes("phone") || label.includes("telephone")) return "phone";
2381
+ if (type === "date") return "date";
2382
+ if (type === "number") return "number";
2383
+ if (type === "checkbox") return "check";
2384
+ if (type === "radio") return "radio";
2385
+ if (type === "file") return "file";
2386
+ if (type === "select" || type === "select-one") return "select";
2387
+ return "text";
2388
+ }
2389
+ var firstnames = { en: ["alex", "jordan", "taylor", "morgan", "casey"], pt: ["ana", "bruno", "carla", "diego", "helena"] };
2390
+ var lastnames = { en: ["brooks", "carter", "diaz", "evans", "reyes"], pt: ["alves", "costa", "lima", "souza", "moraes"] };
2391
+ function localekey(locale) {
2392
+ const normalized = locale.toLowerCase();
2393
+ if (normalized.startsWith("pt")) return "pt";
2394
+ return "en";
2395
+ }
2396
+ function generatevalue(kind, rule) {
2397
+ const seed = typeof rule.seed === "number" && Number.isFinite(rule.seed) ? Math.abs(Math.floor(rule.seed)) : 1;
2398
+ const names = firstnames[localekey(rule.locale ?? "en")] ?? firstnames.en ?? ["alex"];
2399
+ const surnames = lastnames[localekey(rule.locale ?? "en")] ?? lastnames.en ?? ["brooks"];
2400
+ let state = seed * 1103515245 + 12345;
2401
+ const next = () => {
2402
+ state = (state * 1103515245 + 12345) % 2147483648;
2403
+ return state / 2147483648;
2404
+ };
2405
+ const pick = (items) => items[Math.floor(next() * items.length) % items.length] ?? items[0];
2406
+ const digits = (count) => Array.from({ length: count }, () => String(Math.floor(next() * 10))).join("");
2407
+ const person = `${pick(names)} ${pick(surnames)}`;
2408
+ switch (kind) {
2409
+ case "email":
2410
+ return `${person.replace(" ", ".")}${digits(2)}@example.com`;
2411
+ case "phone":
2412
+ return localekey(rule.locale ?? "en") === "pt" ? `+55 (11) 9${digits(4)}-${digits(4)}` : `+1 (555) 010-${digits(4)}`;
2413
+ case "date":
2414
+ return `${2024 + Math.floor(next() * 2)}-${String(1 + Math.floor(next() * 12)).padStart(2, "0")}-${String(1 + Math.floor(next() * 28)).padStart(2, "0")}`;
2415
+ case "number":
2416
+ return String(Math.floor(next() * 1e3));
2417
+ case "select":
2418
+ return `option ${1 + Math.floor(next() * 5)}`;
2419
+ case "check":
2420
+ return next() > 0.5 ? "true" : "false";
2421
+ case "radio":
2422
+ return `choice ${1 + Math.floor(next() * 4)}`;
2423
+ case "file":
2424
+ return `sample${digits(2)}.pdf`;
2425
+ case "password":
2426
+ return `pw-${digits(6)}-${pick(names)}`;
2427
+ case "card":
2428
+ return `4111 ${digits(4)} ${digits(4)} ${digits(4)}`;
2429
+ case "code":
2430
+ return digits(6);
2431
+ default:
2432
+ return person;
2433
+ }
2434
+ }
2435
+ function parseformrecord(value) {
2436
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
2437
+ const record = value;
2438
+ if (!Array.isArray(record.entries)) return null;
2439
+ const entries = [];
2440
+ for (const item of record.entries) {
2441
+ if (!item || typeof item !== "object" || Array.isArray(item)) continue;
2442
+ const entry = item;
2443
+ const match = entry.match;
2444
+ if (!match || typeof match !== "object" || Array.isArray(match)) continue;
2445
+ const shapes = match;
2446
+ if (typeof shapes.mode !== "string") continue;
2447
+ const fieldmatch = {
2448
+ mode: shapes.mode,
2449
+ ...typeof shapes.label === "string" ? { label: shapes.label } : {},
2450
+ ...typeof shapes.placeholder === "string" ? { placeholder: shapes.placeholder } : {},
2451
+ ...typeof shapes.arialabel === "string" ? { arialabel: shapes.arialabel } : {},
2452
+ ...typeof shapes.name === "string" ? { name: shapes.name } : {}
2453
+ };
2454
+ if (typeof entry.kind !== "string" || typeof entry.value !== "string") continue;
2455
+ entries.push({ match: fieldmatch, kind: entry.kind, value: entry.value });
2456
+ }
2457
+ if (entries.length === 0) return null;
2458
+ return { ...typeof record.form === "string" && record.form ? { form: record.form } : {}, entries };
2459
+ }
2460
+ function pairentries(pairs, mode) {
2461
+ return { entries: pairs.map((pair) => ({ match: mode === "label" ? { mode, label: pair.label ?? "" } : { mode, placeholder: pair.placeholder ?? "" }, kind: "text", value: pair.value })) };
2462
+ }
2463
+ function filloperations(record, fields, skippedselectors = []) {
2464
+ return record.entries.map((entry) => {
2465
+ const matches = matchfield(fields, entry.match);
2466
+ if (matches.length === 0) return { entry, reason: "unmatched" };
2467
+ if (matches.length > 1) return { entry, reason: "ambiguous" };
2468
+ const matched = matches[0];
2469
+ if (skippedselectors.includes(matched.selector)) return { entry, matched, skipped: true };
2470
+ return { entry, matched };
2471
+ });
2472
+ }
2473
+ function cardmask(value) {
2474
+ const trimmed = value.trim();
2475
+ if (/^\d[\d\s-]{11,18}$/.test(trimmed)) {
2476
+ const compact = trimmed.replace(/[\s-]/g, "");
2477
+ const last = compact.slice(-4);
2478
+ return `${"\u2022".repeat(Math.max(0, compact.length - 4))}${last}`;
2479
+ }
2480
+ return "\u2022".repeat(trimmed.length);
2481
+ }
2482
+ function detecthoneypots(surveys, loadedat) {
2483
+ const traps = [];
2484
+ for (const field of surveys) {
2485
+ if (field.hidden) traps.push({ selector: field.selector, reason: "hidden" });
2486
+ else if (field.offscreen) traps.push({ selector: field.selector, reason: "offscreen" });
2487
+ else if (field.createdat !== void 0 && loadedat > 0 && field.createdat > loadedat) traps.push({ selector: field.selector, reason: "timetrap" });
2488
+ }
2489
+ return traps;
2490
+ }
2491
+ function detectlogin(fields, links) {
2492
+ const markers = [];
2493
+ const password = fields.find((field) => classifyfield(field) === "password");
2494
+ if (password) markers.push("password field");
2495
+ const identifier = fields.find((field) => {
2496
+ const kind = classifyfield(field);
2497
+ return kind === "email" || kind === "text" && /user|login|account|identifier/i.test(`${field.name} ${field.label}`);
2498
+ });
2499
+ if (identifier) markers.push("identifier field");
2500
+ const sessionlink = links.some((link) => /sign in|log in|log on|forgot|create account|sign up/i.test(link));
2501
+ if (sessionlink) markers.push("session link");
2502
+ return { login: Boolean(password && identifier && sessionlink), markers };
2503
+ }
2504
+ var signupmarkers = ["sign up", "create account", "register", "confirm password", "terms"];
2505
+ var checkoutmarkers = ["checkout", "payment", "billing", "shipping", "card number", "place order", "cart"];
2506
+ function detecttemplate(fields, text) {
2507
+ const corpus = [text, ...fields.map((field) => `${field.label} ${field.name} ${field.placeholder} ${field.arialabel} ${field.autocomplete}`)].join(" ").toLowerCase();
2508
+ const signup = signupmarkers.filter((marker) => corpus.includes(marker));
2509
+ const checkout = checkoutmarkers.filter((marker) => corpus.includes(marker));
2510
+ if (signup.length >= 2 && signup.length >= checkout.length) return { template: "signup", markers: signup };
2511
+ if (checkout.length >= 2) return { template: "checkout", markers: checkout };
2512
+ return { template: "unknown", markers: [...signup, ...checkout] };
2513
+ }
2514
+ function associateerrors(contexts, messages) {
2515
+ const errors = [];
2516
+ for (const field of contexts) {
2517
+ const byref = field.describedby ? messages.find((message) => message.id === field.describedby && message.text.trim()) : void 0;
2518
+ if (byref) {
2519
+ errors.push({ field: field.selector, message: byref.text.trim() });
2520
+ continue;
2521
+ }
2522
+ const sibling = field.siblings.map((text) => text.trim()).find((text) => text.length > 0);
2523
+ if (sibling) errors.push({ field: field.selector, message: sibling });
2524
+ }
2525
+ return errors;
2526
+ }
2527
+ var captchamarkers = ['iframe[src*="recaptcha"]', 'iframe[title*="recaptcha" i]', ".g-recaptcha", "[data-sitekey]", 'iframe[title*="captcha" i]', ".h-captcha"];
2528
+ function captchadetected(matched) {
2529
+ return matched.length > 0;
2530
+ }
2531
+ function events3(target) {
2532
+ target.dispatchEvent(new Event("input", { bubbles: true }));
2533
+ target.dispatchEvent(new Event("change", { bubbles: true }));
2534
+ }
2535
+ function fillcontrol(element, entry) {
2536
+ if (element instanceof HTMLInputElement && (entry.kind === "check" || element.type === "checkbox")) {
2537
+ element.checked = entry.value === "true" || entry.value === "on" || entry.value === "checked";
2538
+ events3(element);
2539
+ return true;
2540
+ }
2541
+ if (element instanceof HTMLInputElement && (entry.kind === "radio" || element.type === "radio")) {
2542
+ element.checked = true;
2543
+ events3(element);
2544
+ return true;
2545
+ }
2546
+ if (element instanceof HTMLSelectElement) {
2547
+ const option = [...element.options].find((candidate) => candidate.value === entry.value || candidate.textContent?.trim() === entry.value);
2548
+ if (!option) return false;
2549
+ element.value = option.value;
2550
+ events3(element);
2551
+ return true;
2552
+ }
2553
+ if (element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement) {
2554
+ if (element instanceof HTMLInputElement && element.type === "file") return false;
2555
+ element.focus();
2556
+ const setter = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(element), "value")?.set;
2557
+ if (setter) setter.call(element, entry.value);
2558
+ else element.value = entry.value;
2559
+ events3(element);
2560
+ return true;
2561
+ }
2562
+ return false;
2563
+ }
2564
+ function controlshape(element) {
2565
+ return {
2566
+ selector: elementselector(element),
2567
+ tag: element.tagName.toLowerCase(),
2568
+ type: element instanceof HTMLSelectElement ? "select" : element.getAttribute("type") || "text",
2569
+ name: element.getAttribute("name") || "",
2570
+ label: elementlabel(element),
2571
+ placeholder: element.getAttribute("placeholder") || "",
2572
+ arialabel: element.getAttribute("aria-label") || "",
2573
+ autocomplete: element.getAttribute("autocomplete") || "",
2574
+ ...element instanceof HTMLSelectElement ? { options: [...element.options].map((option) => option.value) } : {}
2575
+ };
2576
+ }
2577
+ function collectfields(root, formscope) {
2578
+ const scope = formscope ? root.querySelector(formscope) : root;
2579
+ if (!scope) return [];
2580
+ const controls = [...scope.querySelectorAll("input, select, textarea")];
2581
+ return controls.filter((element) => element.type !== "hidden").map((element) => ({ shape: controlshape(element), element }));
2582
+ }
2583
+ function surveyfields(root, formscope) {
2584
+ const viewport = { left: 0, top: 0, right: window.innerWidth || 0, bottom: window.innerHeight || 0 };
2585
+ return collectfields(root, formscope).map(({ shape, element }) => {
2586
+ const rect = element.getBoundingClientRect();
2587
+ const hidden = element.getAttribute("aria-hidden") === "true" || element.tabIndex < 0 && element.offsetParent === null || element.offsetParent === null && rect.width === 0 && rect.height === 0;
2588
+ const offscreen = rect.width > 0 && rect.height > 0 && (rect.bottom < viewport.top || rect.top > viewport.bottom || rect.right < viewport.left || rect.left > viewport.right);
2589
+ return { survey: { ...shape, hidden, offscreen }, element };
2590
+ });
2591
+ }
2592
+ function collecterrorcontext(root, formscope) {
2593
+ return collectfields(root, formscope).map(({ shape, element }) => {
2594
+ const siblings = [];
2595
+ let neighbor = element.nextElementSibling;
2596
+ for (let index = 0; neighbor && index < 3; index += 1) {
2597
+ const text = clean(neighbor.textContent || "");
2598
+ if (text && text !== shape.label) siblings.push(text);
2599
+ neighbor = neighbor.nextElementSibling;
2600
+ }
2601
+ const describedby = element.getAttribute("aria-describedby");
2602
+ return { context: { ...shape, ...describedby ? { describedby } : {}, siblings }, element };
2603
+ });
2604
+ }
2605
+ function runpageform(step, target, root = document) {
2606
+ let options = {};
2607
+ try {
2608
+ options = parseoptions(step);
2609
+ } catch {
2610
+ options = {};
2611
+ }
2612
+ const formscope = typeof options.form === "string" && options.form ? options.form : step.target;
2613
+ switch (step.kind) {
2614
+ case "fillform": {
2615
+ const record = parseformrecord(options.formrecord);
2616
+ if (!record) return { ok: false, summary: "A reviewed form record with entries is required in options." };
2617
+ const surveys = surveyfields(root, record.form);
2618
+ const honeypots = detecthoneypots(surveys.map((entry) => entry.survey), 0);
2619
+ const operations = filloperations(record, surveys.map((entry) => entry.survey), honeypots.map((trap) => trap.selector));
2620
+ let filled = 0;
2621
+ const skipped = [];
2622
+ const failures = [];
2623
+ const values = [];
2624
+ for (const operation of operations) {
2625
+ if (operation.skipped && operation.matched) {
2626
+ skipped.push(operation.matched.selector);
2627
+ continue;
2628
+ }
2629
+ if (!operation.matched) {
2630
+ failures.push(`${operation.reason}: ${operation.entry.match.label ?? operation.entry.match.name ?? operation.entry.match.placeholder ?? "field"}`);
2631
+ continue;
2632
+ }
2633
+ const element = surveys.find((entry) => entry.survey.selector === operation.matched?.selector)?.element;
2634
+ if (!element || !fillcontrol(element, operation.entry)) {
2635
+ failures.push(`unfillable: ${operation.matched.selector}`);
2636
+ continue;
2637
+ }
2638
+ filled += 1;
2639
+ values.push({ label: operation.matched.label || operation.matched.name, value: operation.entry.kind === "password" ? "" : operation.entry.value });
2640
+ }
2641
+ const report = { form: record.form ?? "", fields: operations.map((operation) => ({ selector: operation.matched?.selector ?? "", label: operation.matched?.label ?? operation.entry.match.label ?? "", kind: operation.entry.kind, matched: Boolean(operation.matched) })) };
2642
+ return {
2643
+ ok: failures.length === 0,
2644
+ summary: failures.length === 0 ? `Filled ${filled} reviewed field${filled === 1 ? "" : "s"} from the structured record${skipped.length > 0 ? ` and skipped ${skipped.length} honeypot field${skipped.length === 1 ? "" : "s"}` : ""}.` : `Filled ${filled} of ${record.entries.length} reviewed fields; ${failures.length} refusals: ${failures.join("; ")}.`,
2645
+ details: { filled, skipped, failures, values, report }
2646
+ };
2647
+ }
2648
+ case "filllabel":
2649
+ case "fillplaceholder": {
2650
+ const mode = step.kind === "filllabel" ? "label" : "placeholder";
2651
+ const pairs = Array.isArray(options.fields) ? options.fields.filter((item) => item && typeof item === "object") : [];
2652
+ const record = pairentries(pairs.map((pair) => ({ label: typeof pair.label === "string" ? pair.label : "", placeholder: typeof pair.placeholder === "string" ? pair.placeholder : "", value: typeof pair.value === "string" ? pair.value : "" })), mode);
2653
+ if (record.entries.length === 0) return { ok: false, summary: "A reviewed non-empty list of field pairs is required in options." };
2654
+ const surveys = surveyfields(root, formscope);
2655
+ const honeypots = detecthoneypots(surveys.map((entry) => entry.survey), 0);
2656
+ const operations = filloperations(record, surveys.map((entry) => entry.survey), honeypots.map((trap) => trap.selector));
2657
+ let filled = 0;
2658
+ const failures = [];
2659
+ for (const operation of operations) {
2660
+ if (operation.skipped) continue;
2661
+ if (!operation.matched) {
2662
+ failures.push(`${operation.reason}: ${mode === "label" ? operation.entry.match.label : operation.entry.match.placeholder}`);
2663
+ continue;
2664
+ }
2665
+ const element = surveys.find((entry) => entry.survey.selector === operation.matched?.selector)?.element;
2666
+ const refined = { ...operation.entry, kind: classifyfield(operation.matched) };
2667
+ if (!element || !fillcontrol(element, refined)) {
2668
+ failures.push(`unfillable: ${operation.matched.selector}`);
2669
+ continue;
2670
+ }
2671
+ filled += 1;
2672
+ }
2673
+ return { ok: failures.length === 0, summary: failures.length === 0 ? `Filled ${filled} field${filled === 1 ? "" : "s"} matched by ${mode}.` : `Filled ${filled} of ${record.entries.length} fields matched by ${mode}; ${failures.join("; ")}.`, details: { filled, failures, mode } };
2674
+ }
2675
+ case "detectfields": {
2676
+ const collected = collectfields(root, formscope);
2677
+ const report = { form: formscope ?? "", fields: collected.map((entry) => ({ selector: entry.shape.selector, label: entry.shape.label || entry.shape.name, kind: classifyfield(entry.shape), matched: Boolean(entry.shape.label || entry.shape.name) })) };
2678
+ return { ok: true, summary: `Detected ${collected.length} form field${collected.length === 1 ? "" : "s"} with their kinds.`, details: { report, count: collected.length } };
2679
+ }
2680
+ case "generatevalues": {
2681
+ const rule = options.valuegen && typeof options.valuegen === "object" && !Array.isArray(options.valuegen) ? options.valuegen : {};
2682
+ const locale = typeof rule.locale === "string" ? rule.locale : "en";
2683
+ const seed = typeof rule.seed === "number" && Number.isFinite(rule.seed) ? rule.seed : 1;
2684
+ const surveys = surveyfields(root, formscope);
2685
+ const honeypots = detecthoneypots(surveys.map((entry) => entry.survey), 0);
2686
+ const skippedselectors = new Set(honeypots.map((trap) => trap.selector));
2687
+ const candidates = surveys.filter((entry) => !skippedselectors.has(entry.survey.selector));
2688
+ const values = candidates.map((entry) => ({ label: entry.survey.label || entry.survey.name || entry.survey.selector, kind: classifyfield(entry.survey), value: generatevalue(classifyfield(entry.survey), { locale, seed }) }));
2689
+ const single = values.length === 0 && typeof rule.kind === "string" ? [{ label: rule.kind, kind: rule.kind, value: generatevalue(rule.kind, { locale, seed }) }] : values;
2690
+ return { ok: true, summary: `Generated ${single.length} realistic value${single.length === 1 ? "" : "s"} for the detected field kinds.`, details: { values: single, locale, seed } };
2691
+ }
2692
+ case "readerrors": {
2693
+ const contexts = collecterrorcontext(root, formscope);
2694
+ const messages = [...root.querySelectorAll("[id]")].map((element) => ({ id: element.id, text: clean(element.textContent || "") })).filter((message) => message.text.length > 0);
2695
+ const errors = associateerrors(contexts.map((entry) => entry.context), messages);
2696
+ return { ok: true, summary: errors.length === 0 ? "No validation error was found next to the reviewed fields." : `Collected ${errors.length} inline validation message${errors.length === 1 ? "" : "s"}.`, details: { errors, form: formscope ?? "" } };
2697
+ }
2698
+ case "skiphoneypot": {
2699
+ const surveys = surveyfields(root, formscope);
2700
+ const traps = detecthoneypots(surveys.map((entry) => entry.survey), 0);
2701
+ return { ok: true, summary: traps.length === 0 ? "No honeypot field was detected." : `Skipped ${traps.length} honeypot field${traps.length === 1 ? "" : "s"}: ${traps.map((trap) => `${trap.selector} (${trap.reason})`).join(", ")}.`, details: { skipped: traps } };
2702
+ }
2703
+ case "detectlogin": {
2704
+ const collected = collectfields(root, formscope);
2705
+ const links = [...formscope ? root.querySelectorAll(formscope) : [root]].flatMap((scope) => [...scope.querySelectorAll("a[href], button")]).map((element) => clean(element.textContent || ""));
2706
+ const detection = detectlogin(collected.map((entry) => entry.shape), links);
2707
+ return { ok: true, summary: detection.login ? `Login form detected with ${detection.markers.join(", ")}.` : "No login form was detected.", details: { login: detection.login, markers: detection.markers } };
2708
+ }
2709
+ case "detecttemplate": {
2710
+ const collected = collectfields(root, formscope);
2711
+ const text = clean(root.body?.innerText || "");
2712
+ const detection = detecttemplate(collected.map((entry) => entry.shape), text);
2713
+ return { ok: true, summary: detection.template === "unknown" ? "No signup or checkout template was detected." : `${detection.template} template detected with markers ${detection.markers.join(", ")}.`, details: { template: detection.template, markers: detection.markers } };
2714
+ }
2715
+ case "handoffcaptcha": {
2716
+ const matched = captchamarkers.filter((marker) => root.querySelector(marker) !== null);
2717
+ return { ok: true, summary: captchadetected(matched) ? `Captcha presence detected (${matched.join(", ")}); control hands back to the user.` : "No captcha was detected.", details: { captcha: captchadetected(matched), markers: matched } };
2718
+ }
2719
+ case "asksubmit": {
2720
+ const collected = collectfields(root, step.value || void 0);
2721
+ const values = collected.map((entry) => ({ label: entry.shape.label || entry.shape.name || entry.shape.selector, value: entry.element instanceof HTMLSelectElement ? entry.element.value : entry.element.value }));
2722
+ return { ok: true, summary: `Read ${values.length} field value${values.length === 1 ? "" : "s"} for the submission review.`, details: { values } };
2723
+ }
2724
+ case "submitform": {
2725
+ const form = target instanceof HTMLFormElement ? target : target instanceof HTMLElement ? target.closest("form") : null;
2726
+ if (!form) return { ok: false, summary: "No owning form was found for the reviewed submission." };
2727
+ form.requestSubmit();
2728
+ return { ok: true, summary: "Form submitted programmatically through its owning form." };
2729
+ }
2730
+ case "consentpassword": {
2731
+ if (!(target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement)) return { ok: false, summary: "The reviewed password target cannot receive text." };
2732
+ const entry = { match: { mode: "name", name: target.name || target.getAttribute("id") || "" }, kind: "password", value: step.value ?? "" };
2733
+ if (!fillcontrol(target, entry)) return { ok: false, summary: "The password field refused the native setter fill." };
2734
+ return { ok: true, summary: "Password field filled after the reviewed consent; the value never appears in the audit trail." };
2735
+ }
2736
+ case "attachfile": {
2737
+ if (!(target instanceof HTMLInputElement) || target.type !== "file") return { ok: false, summary: "The reviewed target is not a file input." };
2738
+ const artifactname = typeof options.artifactname === "string" && options.artifactname ? options.artifactname : "artifact";
2739
+ try {
2740
+ const file = new File([new Blob(["devthink artifact"], { type: "application/octet-stream" })], artifactname);
2741
+ const transfer = new DataTransfer();
2742
+ transfer.items.add(file);
2743
+ target.files = transfer.files;
2744
+ events3(target);
2745
+ return { ok: true, summary: `Artifact ${artifactname} attached to the reviewed file input.`, details: { artifact: options.artifact, artifactname } };
2746
+ } catch {
2747
+ return { ok: false, summary: "The reviewed file input refused the artifact attachment." };
2748
+ }
2749
+ }
2750
+ default:
2751
+ return { ok: false, summary: "Unsupported forms and data action." };
2752
+ }
2753
+ }
2754
+
2755
+ // extension/pagewizards.ts
2756
+ function wizardadvance(state, completed, at) {
2757
+ const flags = [...state.completed];
2758
+ while (flags.length < state.index + 1) flags.push(false);
2759
+ flags[state.index] = completed;
2760
+ return { ...state, index: Math.min(state.index + 1, state.steps), completed: flags, at };
2761
+ }
2762
+ function dependentloaded(previouscount, currentcount) {
2763
+ return currentcount !== previouscount;
2764
+ }
2765
+ function typeaheadpick(suggestions, pick) {
2766
+ const needle = pick.trim().toLowerCase();
2767
+ return suggestions.find((suggestion) => suggestion.trim().toLowerCase() === needle);
2768
+ }
2769
+ function parsedateparts(value) {
2770
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) return null;
2771
+ const parts = value.split("-").map((part) => Number.parseInt(part, 10));
2772
+ const year = parts[0] ?? 0;
2773
+ const month = parts[1] ?? 0;
2774
+ const day = parts[2] ?? 0;
2775
+ if (month < 1 || month > 12 || day < 1 || day > 31) return null;
2776
+ return { year, month, day };
2777
+ }
2778
+ function calendarplan(view, date) {
2779
+ return { months: (date.year - view.year) * 12 + (date.month - view.month), day: date.day };
2780
+ }
2781
+ function cardgroups(number) {
2782
+ const groups = number.replace(/[-\s]+/g, " ").trim().split(" ");
2783
+ return groups.filter((group) => group.length > 0);
2784
+ }
2785
+ function codeready(source, value) {
2786
+ if (source === "reviewed") return typeof value === "string" && value.trim().length > 0;
2787
+ return false;
2788
+ }
2789
+ function wait4(delay) {
2790
+ return new Promise((resolve) => window.setTimeout(resolve, delay));
2791
+ }
2792
+ function pollfor2(predicate, description, timeout) {
2793
+ return new Promise((resolve) => {
2794
+ const started = Date.now();
2795
+ const check = () => {
2796
+ if (predicate()) {
2797
+ resolve({ ok: true, summary: `${description} is now present on the page.` });
2798
+ return;
2799
+ }
2800
+ if (timeout > 0 && Date.now() - started >= timeout) {
2801
+ resolve({ ok: false, summary: `${description} did not appear within ${timeout} milliseconds.` });
2802
+ return;
2803
+ }
2804
+ window.setTimeout(check, 100);
2805
+ };
2806
+ check();
2807
+ });
2808
+ }
2809
+ function runpagewizard(step, target, root = document) {
2810
+ let options = {};
2811
+ try {
2812
+ options = parseoptions(step);
2813
+ } catch {
2814
+ options = {};
2815
+ }
2816
+ switch (step.kind) {
2817
+ case "runwizard": {
2818
+ const scope = target instanceof HTMLElement ? target : root.body ?? root.documentElement;
2819
+ const steps = typeof options.steps === "number" && Number.isInteger(options.steps) && options.steps > 0 ? options.steps : 1;
2820
+ const state = { index: 0, steps, completed: [], at: Date.now() };
2821
+ const next = scope.querySelector("button[type=submit], button[name=next], [data-next]");
2822
+ if (!next) return { ok: false, summary: "No next step control was found inside the wizard scope.", details: { wizard: state } };
2823
+ next.click();
2824
+ const advanced = wizardadvance(state, true, Date.now());
2825
+ return { ok: advanced.index >= steps, summary: `Wizard advanced to step ${advanced.index + 1} of ${steps}${advanced.index >= steps ? " and completed" : ""}.`, details: { wizard: advanced } };
2826
+ }
2827
+ case "selectchain": {
2828
+ if (!(target instanceof HTMLSelectElement)) return { ok: false, summary: "The reviewed parent target is not a select element." };
2829
+ const childselector = typeof options.child === "string" ? options.child : "";
2830
+ const child = root.querySelector(childselector);
2831
+ if (!child) return { ok: false, summary: "The reviewed dependent child control was not found." };
2832
+ const previouscount = child.options.length;
2833
+ const option = [...target.options].find((candidate) => candidate.value === step.value || candidate.textContent?.trim() === step.value);
2834
+ if (!option) return { ok: false, summary: "The reviewed parent option is not part of the select element." };
2835
+ target.value = option.value;
2836
+ target.dispatchEvent(new Event("input", { bubbles: true }));
2837
+ target.dispatchEvent(new Event("change", { bubbles: true }));
2838
+ const waitwindow = typeof options.wait === "number" && options.wait > 0 ? options.wait : 0;
2839
+ return pollfor2(() => dependentloaded(previouscount, child.options.length), "Dependent options of the child control", waitwindow);
2840
+ }
2841
+ case "picktypeahead": {
2842
+ const field = target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement ? target : null;
2843
+ if (!field) return { ok: false, summary: "The reviewed typeahead target cannot receive text." };
2844
+ const pick = typeof options.pick === "string" ? options.pick : "";
2845
+ const timeout = typeof options.timeout === "number" && options.timeout > 0 ? options.timeout : 0;
2846
+ field.focus();
2847
+ field.value = step.value ?? "";
2848
+ field.dispatchEvent(new Event("input", { bubbles: true }));
2849
+ const list = root.querySelector("[role=listbox], .suggestions, ul.autocomplete, [data-typeahead]");
2850
+ const suggestions = list ? [...list.querySelectorAll("[role=option], li, .suggestion")].map((entry2) => clean(entry2.textContent || "")) : [];
2851
+ const chosen = typeaheadpick(suggestions, pick);
2852
+ if (chosen === void 0) return { ok: false, summary: `The reviewed suggestion "${pick}" is not part of the typeahead list.`, details: { suggestions } };
2853
+ const entry = [...list?.querySelectorAll("[role=option], li, .suggestion") ?? []].find((item) => clean(item.textContent || "").trim().toLowerCase() === chosen.trim().toLowerCase());
2854
+ entry?.click();
2855
+ field.dispatchEvent(new Event("change", { bubbles: true }));
2856
+ return { ok: Boolean(entry), summary: `Picked the reviewed typeahead entry "${chosen}".`, details: { pick: chosen, query: step.value ?? "" } };
2857
+ }
2858
+ case "pickdate": {
2859
+ const calendar = target instanceof HTMLElement ? target : root.body ?? root.documentElement;
2860
+ const date = parsedateparts(step.value ?? "");
2861
+ if (!date) return { ok: false, summary: "The reviewed date must use the yyyy-mm-dd form." };
2862
+ const header = clean(calendar.querySelector("[data-calendar-title], .calendar-title, header")?.textContent || "");
2863
+ const match = /(\d{4})/.exec(header);
2864
+ const monthnames = ["january", "february", "march", "april", "may", "june", "july", "august", "september", "october", "november", "december"];
2865
+ const viewyear = match ? Number.parseInt(match[1] ?? "0", 10) : (/* @__PURE__ */ new Date()).getFullYear();
2866
+ const viewmonth = monthnames.findIndex((name) => header.toLowerCase().includes(name)) >= 0 ? monthnames.findIndex((name) => header.toLowerCase().includes(name)) : (/* @__PURE__ */ new Date()).getMonth() + 1;
2867
+ const plan = calendarplan({ year: viewyear, month: viewmonth }, date);
2868
+ const forward = plan.months >= 0;
2869
+ for (let index = 0; index < Math.abs(plan.months); index += 1) {
2870
+ calendar.querySelector(forward ? "[data-next-month], .next-month, [aria-label=next]" : "[data-prev-month], .prev-month, [aria-label=previous]")?.click();
2871
+ }
2872
+ const day = [...calendar.querySelectorAll("[role=gridcell], [data-day], td")].find((cell) => Number.parseInt(clean(cell.textContent || ""), 10) === plan.day);
2873
+ if (!day) return { ok: false, summary: `The reviewed day cell ${plan.day} was not found in the calendar widget.` };
2874
+ day.click();
2875
+ return { ok: true, summary: `Picked ${step.value} from the calendar widget after ${Math.abs(plan.months)} month navigation${Math.abs(plan.months) === 1 ? "" : "s"}.`, details: { months: plan.months, day: plan.day } };
2876
+ }
2877
+ case "fillcard": {
2878
+ const segments = Array.isArray(options.segments) ? options.segments.filter((item) => item && typeof item === "object") : [];
2879
+ if (segments.length === 0) return { ok: false, summary: "A reviewed non-empty list of card segments is required in options." };
2880
+ const pause = typeof options.pause === "number" && options.pause > 0 ? options.pause : 0;
2881
+ const filled = [];
2882
+ const failures = [];
2883
+ const fillsegment = async (segment) => {
2884
+ const match = segment.match;
2885
+ const value = typeof segment.value === "string" ? segment.value : "";
2886
+ if (!match || typeof match !== "object") {
2887
+ failures.push("segment without a reviewed match");
2888
+ return;
2889
+ }
2890
+ const scope = root.body ?? root.documentElement;
2891
+ const candidates = [...scope.querySelectorAll("input, select")];
2892
+ const key = match.mode === "label" ? "label" : match.mode === "placeholder" ? "placeholder" : match.mode === "arialabel" ? "arialabel" : "name";
2893
+ const needle = String(match[key] ?? "").toLowerCase();
2894
+ const element = candidates.find((input) => (key === "label" ? input.name.toLowerCase() || input.getAttribute("aria-label")?.toLowerCase() || "" : (input.getAttribute(key) ?? input.name).toLowerCase()).includes(needle) || (input.getAttribute("aria-label") ?? "").toLowerCase().includes(needle));
2895
+ if (!element) {
2896
+ failures.push(`unmatched: ${needle}`);
2897
+ return;
2898
+ }
2899
+ element.focus();
2900
+ for (const group of cardgroups(value)) {
2901
+ element.value = group;
2902
+ element.dispatchEvent(new Event("input", { bubbles: true }));
2903
+ if (pause > 0) await wait4(pause);
2904
+ }
2905
+ element.dispatchEvent(new Event("change", { bubbles: true }));
2906
+ filled.push({ label: String(match[key] ?? ""), masked: cardmask(value) });
2907
+ };
2908
+ return (async () => {
2909
+ for (const segment of segments) await fillsegment(segment);
2910
+ return {
2911
+ ok: failures.length === 0,
2912
+ summary: failures.length === 0 ? `Filled ${filled.length} card segment${filled.length === 1 ? "" : "s"} with pauses between card number groups.` : `Filled ${filled.length} of ${segments.length} card segments; ${failures.join("; ")}.`,
2913
+ details: { segments: filled, failures, pause }
2914
+ };
2915
+ })();
2916
+ }
2917
+ case "fillcode": {
2918
+ const field = target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement ? target : null;
2919
+ if (!field) return { ok: false, summary: "The reviewed one time code target cannot receive text." };
2920
+ const source = typeof options.source === "string" ? options.source : "";
2921
+ const timeout = typeof options.timeout === "number" && options.timeout > 0 ? options.timeout : 0;
2922
+ const code = step.value ?? "";
2923
+ return pollfor2(() => codeready(source, code), "The reviewed one time code source", timeout).then((result) => {
2924
+ if (!codeready(source, code)) return { ok: false, summary: `The reviewed code source ${source} is not ready to deliver the code yet.` };
2925
+ field.focus();
2926
+ field.value = code;
2927
+ field.dispatchEvent(new Event("input", { bubbles: true }));
2928
+ field.dispatchEvent(new Event("change", { bubbles: true }));
2929
+ return { ok: true, summary: `One time code typed from the reviewed source ${source}.`, details: { source } };
2930
+ });
2931
+ }
2932
+ default:
2933
+ return { ok: false, summary: "Unsupported wizard action." };
2934
+ }
2935
+ }
2936
+
2937
+ // extension/pagedata.ts
2938
+ function normalizeheader(label) {
2939
+ const slug = label.trim().toLowerCase().replace(/[^\p{L}\p{N}]+/gu, "-").replace(/^-+|-+$/g, "");
2940
+ return slug || "column";
2941
+ }
2942
+ function columnspecof(label, used = /* @__PURE__ */ new Set()) {
2943
+ const base = normalizeheader(label);
2944
+ let key = base;
2945
+ let suffix = 2;
2946
+ while (used.has(key)) {
2947
+ key = `${base}${suffix}`;
2948
+ suffix += 1;
2949
+ }
2950
+ used.add(key);
2951
+ return { key, label: label.trim(), kind: "text", normalized: label.trim().toLowerCase() };
2952
+ }
2953
+ function classifycolumn(values) {
2954
+ const present = values.filter((value) => value.trim().length > 0);
2955
+ if (present.length === 0) return "text";
2956
+ return present.every((value) => Number.isFinite(Number(value.replace(/,/g, ".")))) ? "number" : "text";
2957
+ }
2958
+ function expandspans(rows) {
2959
+ const filled = [];
2960
+ const pending = /* @__PURE__ */ new Map();
2961
+ for (let index = 0; index < rows.length; index += 1) {
2962
+ const row = filled[index] ?? (filled[index] = []);
2963
+ let column = 0;
2964
+ for (const cell of rows[index].cells) {
2965
+ while (row[column] !== void 0 || pending.has(`${index},${column}`)) {
2966
+ if (row[column] === void 0) row[column] = pending.get(`${index},${column}`);
2967
+ column += 1;
2968
+ }
2969
+ row[column] = cell.text;
2970
+ for (let spanrow = 0; spanrow < Math.max(1, cell.rowspan); spanrow += 1) {
2971
+ for (let spancol = 0; spancol < Math.max(1, cell.colspan); spancol += 1) {
2972
+ if (spanrow === 0 && spancol === 0) continue;
2973
+ pending.set(`${index + spanrow},${column + spancol}`, cell.text);
2974
+ }
2975
+ }
2976
+ column += Math.max(1, cell.colspan);
2977
+ }
2978
+ }
2979
+ for (const [key, value] of pending) {
2980
+ const [rowpart, columnpart] = key.split(",");
2981
+ const rowindex = Number.parseInt(rowpart ?? "0", 10);
2982
+ const columnindex = Number.parseInt(columnpart ?? "0", 10);
2983
+ const target = filled[rowindex] ?? (filled[rowindex] = []);
2984
+ if (target[columnindex] === void 0) target[columnindex] = value;
2985
+ }
2986
+ const width = filled.reduce((largest, row) => Math.max(largest, row.length), 0);
2987
+ return filled.map((row) => Array.from({ length: width }, (_, column) => row[column] ?? ""));
2988
+ }
2989
+ function readgrid(rows) {
2990
+ const headerindex = rows.findIndex((row) => row.header || (row.cells[0]?.header ?? false));
2991
+ const useheader = headerindex !== -1 ? headerindex : 0;
2992
+ const hasheader = headerindex !== -1 || rows.length > 0;
2993
+ const grid = expandspans(rows);
2994
+ const headercells = hasheader ? grid[useheader] ?? [] : [];
2995
+ const used = /* @__PURE__ */ new Set();
2996
+ const columns = Array.from({ length: headercells.length || (grid[0]?.length ?? 0) }, (_, index) => columnspecof(headercells[index] ?? `column${index + 1}`, used));
2997
+ const bodyrows = grid.filter((_, index) => hasheader ? index !== useheader : true).filter((line) => line.some((value) => value.trim().length > 0)).map((line) => {
2998
+ const row = {};
2999
+ columns.forEach((column, index) => {
3000
+ row[column.key] = line[index] ?? "";
3001
+ });
3002
+ return row;
3003
+ });
3004
+ for (const column of columns) column.kind = classifycolumn(bodyrows.map((row) => row[column.key] ?? ""));
3005
+ const children = [];
3006
+ rows.forEach((row, rowindex) => {
3007
+ if (hasheader && rowindex === useheader) return;
3008
+ const parentrow = hasheader && rowindex > useheader ? rowindex - 1 : rowindex;
3009
+ row.cells.forEach((cell) => {
3010
+ if (!cell.nested) return;
3011
+ const child = readgrid(cell.nested.rows);
3012
+ children.push({ parentrow, selector: cell.nested.selector, columns: child.columns, rows: child.rows });
3013
+ });
3014
+ });
3015
+ return { columns, rows: bodyrows, children };
3016
+ }
3017
+ function rowsfresh(previous, current) {
3018
+ if (current.length === 0) return false;
3019
+ const known = new Set(previous.map((row) => JSON.stringify(row)));
3020
+ return current.some((row) => !known.has(JSON.stringify(row)));
3021
+ }
3022
+ function mergedatasets(datasets) {
3023
+ const columns = [];
3024
+ const seen = /* @__PURE__ */ new Set();
3025
+ for (const dataset of datasets) {
3026
+ for (const column of dataset.columns) {
3027
+ if (seen.has(column.key)) continue;
3028
+ seen.add(column.key);
3029
+ columns.push(column);
3030
+ }
3031
+ }
3032
+ const rows = datasets.flatMap((dataset) => dataset.rows.map((row) => {
3033
+ const merged = {};
3034
+ for (const column of columns) merged[column.key] = row[column.key] ?? "";
3035
+ return merged;
3036
+ }));
3037
+ return { columns, rows };
3038
+ }
3039
+ function collectrowshapes(table) {
3040
+ return [...table.rows].map((row) => ({
3041
+ header: [...row.cells].every((cell) => cell.tagName === "TH") && row.cells.length > 0,
3042
+ cells: [...row.cells].map((cell) => {
3043
+ const nested = cell.querySelector("table");
3044
+ return {
3045
+ text: clean(nested ? `${nested.rows.length} rows` : cell.textContent ?? ""),
3046
+ header: cell.tagName === "TH",
3047
+ rowspan: cell.rowSpan,
3048
+ colspan: cell.colSpan,
3049
+ ...nested instanceof HTMLTableElement ? { nested: { selector: elementselector(nested), rows: collectrowshapes(nested) } } : {}
3050
+ };
3051
+ })
3052
+ }));
3053
+ }
3054
+ function readtable(target, root, fallbackselector) {
3055
+ const table = target instanceof HTMLTableElement ? target : fallbackselector ? root.querySelector(fallbackselector) : root.querySelector("table");
3056
+ if (!table) return null;
3057
+ return readgrid(collectrowshapes(table));
3058
+ }
3059
+ async function runpagedata(step, target, root = document) {
3060
+ let options = {};
3061
+ try {
3062
+ options = parseoptions(step);
3063
+ } catch {
3064
+ options = {};
3065
+ }
3066
+ if (step.kind === "scrapetable") {
3067
+ const grid = readtable(target, root, step.target);
3068
+ if (!grid) return { ok: false, summary: "The reviewed table selector matches no table element." };
3069
+ const rowlimit = typeof options.rowlimit === "number" && Number.isInteger(options.rowlimit) && options.rowlimit > 0 ? options.rowlimit : grid.rows.length;
3070
+ const limited = grid.rows.slice(0, rowlimit);
3071
+ return {
3072
+ ok: true,
3073
+ summary: `Scraped ${limited.length} row${limited.length === 1 ? "" : "s"} into ${grid.columns.length} normalized column${grid.columns.length === 1 ? "" : "s"}${grid.children.length > 0 ? ` with ${grid.children.length} nested child table${grid.children.length === 1 ? "" : "s"}` : ""}.`,
3074
+ details: { grid: { columns: grid.columns, rows: limited, children: grid.children }, rows: limited.length, columns: grid.columns.length }
3075
+ };
3076
+ }
3077
+ if (step.kind === "paginateextract") {
3078
+ const nextselector = typeof options.next === "string" ? options.next : "";
3079
+ const pages = typeof options.pages === "number" && Number.isInteger(options.pages) && options.pages > 0 ? options.pages : 1;
3080
+ const wait5 = typeof options.wait === "number" && Number.isFinite(options.wait) && options.wait > 0 ? options.wait : 0;
3081
+ const cursor = typeof options.cursor === "number" && Number.isInteger(options.cursor) && options.cursor > 0 ? options.cursor : 0;
3082
+ const grids = [];
3083
+ let previous = [];
3084
+ for (let page = 0; page < pages + cursor; page += 1) {
3085
+ if (page < cursor) {
3086
+ const control = root.querySelector(nextselector);
3087
+ if (!control) break;
3088
+ control.click();
3089
+ await new Promise((resolve) => window.setTimeout(resolve, 0));
3090
+ continue;
3091
+ }
3092
+ if (page > cursor) {
3093
+ const control = root.querySelector(nextselector);
3094
+ if (!control) break;
3095
+ control.click();
3096
+ const deadline = Date.now() + wait5;
3097
+ let fresh = false;
3098
+ while (!fresh && Date.now() < deadline) {
3099
+ await new Promise((resolve) => window.setTimeout(resolve, Math.min(100, Math.max(16, deadline - Date.now()))));
3100
+ const probe = readtable(target, root, step.target);
3101
+ fresh = probe !== null && rowsfresh(previous, probe.rows);
3102
+ }
3103
+ }
3104
+ const grid = readtable(target, root, step.target);
3105
+ if (!grid) return { ok: false, summary: "The reviewed table selector matches no table element." };
3106
+ grids.push(grid);
3107
+ previous = grid.rows;
3108
+ }
3109
+ const merged = mergedatasets(grids);
3110
+ const hasnext = Boolean(root.querySelector(nextselector));
3111
+ return {
3112
+ ok: grids.length > 0,
3113
+ summary: grids.length > 0 ? `Followed ${grids.length} page${grids.length === 1 ? "" : "s"} of the reviewed table into ${merged.rows.length} row${merged.rows.length === 1 ? "" : "s"}${hasnext ? "; a next control remains" : ""}.` : "No page of the reviewed table was extracted.",
3114
+ details: { grid: { columns: merged.columns, rows: merged.rows, children: grids.flatMap((grid) => grid.children) }, pages: grids.length, rows: merged.rows.length, next: hasnext }
3115
+ };
3116
+ }
3117
+ return { ok: false, summary: "Unsupported data step." };
3118
+ }
3119
+
2361
3120
  // extension/pagebridge.ts
2362
3121
  function stepoptions3(step) {
2363
3122
  if (!step.options) return {};
@@ -2465,6 +3224,9 @@
2465
3224
  var detectionkinds = /* @__PURE__ */ new Set(["detectlists", "detecttables", "detectinfinitescroll", "detectvirtual", "detectlazy", "detectsticky", "detectscrolllock", "countpages", "classifypage", "fingerprintsection", "readscrollpos"]);
2466
3225
  var watchstepkinds = /* @__PURE__ */ new Set(["watchmutate", "watchbanner", "watchfocus", "waitquiet", "readjson", "diffsnapshots", "deriveselector"]);
2467
3226
  var navstepkinds = /* @__PURE__ */ new Set(["waitload", "waiturl", "followlink", "spanav", "spawait", "rewritequery", "setfragment", "stopnav", "prefetch", "preconnect", "printpdf"]);
3227
+ var formkinds = /* @__PURE__ */ new Set(["fillform", "filllabel", "fillplaceholder", "detectfields", "generatevalues", "readerrors", "skiphoneypot", "detectlogin", "detecttemplate", "handoffcaptcha", "asksubmit", "submitform", "consentpassword", "attachfile"]);
3228
+ var wizardkinds = /* @__PURE__ */ new Set(["runwizard", "selectchain", "picktypeahead", "pickdate", "fillcard", "fillcode"]);
3229
+ var datastepkinds = /* @__PURE__ */ new Set(["scrapetable", "paginateextract"]);
2468
3230
  async function performstep(step, expectedorigin, rootdocument = document) {
2469
3231
  if (location.origin !== expectedorigin) return { ok: false, summary: "Page origin changed before action." };
2470
3232
  if (step.kind === "observe") return { ok: true, summary: "Observation completed." };
@@ -2511,7 +3273,10 @@
2511
3273
  }
2512
3274
  const element = resolution.status === "resolved" ? resolution.element : null;
2513
3275
  let result;
2514
- if (readkinds.has(step.kind)) result = runpageread(step, element, rootdocument);
3276
+ if (formkinds.has(step.kind)) result = runpageform(step, element, rootdocument);
3277
+ else if (wizardkinds.has(step.kind)) result = runpagewizard(step, element, rootdocument);
3278
+ else if (datastepkinds.has(step.kind)) result = runpagedata(step, element, rootdocument);
3279
+ else if (readkinds.has(step.kind)) result = runpageread(step, element, rootdocument);
2515
3280
  else if (controlkinds.has(step.kind)) result = runpagecontrol(step, element, rootdocument);
2516
3281
  else if (interactkinds.has(step.kind)) return await runinteractstep(step, expectedorigin, performstep);
2517
3282
  else if (pointerkinds.has(step.kind)) result = runpointerstep(step, resolution);