@wenathlan/extension 1.1.34 → 1.1.35

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.
@@ -2,7 +2,7 @@
2
2
  "manifest_version": 3,
3
3
  "key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnOEjO8Z0PDgQyfvawGcaO2j+o0GLCFTLNj7TkYC/Avo9l2NenMRq7gp90Nfd7E9MViv/OMcCKSYZ5unv12QPRtv31C+a5UQWDFAOP/cH5mwMd6hsayElrSoW8ta+FwFqmr9dIFkn7cQEU3YhZr4Gcbs+ycUHOxVgDA4NBKB0rQ6e9VW5LvTw0isRYUrqM+M72vKxHk9zUIYYn/LGPvottKBYi2GLr0PHSeC2UE+Shmq7vcFIXj6hDjvD4kLJ5sKoUllEcZ1TPuBcnHUQ9ndKA5iktXDQOIJCUJmi7a0YJ2PGg7fvpYfT9k0ai/qZ+pIoRfoOEwE01bPoDn7NjeYnNQIDAQAB",
4
4
  "name": "Devthink",
5
- "version": "1.1.34",
5
+ "version": "1.1.35",
6
6
  "description": "A consent-first bridge for reviewed browser-agent tasks.",
7
7
  "permissions": [
8
8
  "activeTab",
@@ -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"]);
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"]);
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"]);
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"]);
7
7
  var allowedactions = /* @__PURE__ */ new Set([...sensitiveactions, ...interactionactions, ...readactions]);
8
8
  function parseoptions(step) {
9
9
  if (step.options === void 0) return {};
@@ -1135,6 +1135,323 @@
1135
1135
  return { ok: false, summary: "Unsupported interaction action." };
1136
1136
  }
1137
1137
 
1138
+ // extension/pagenav.ts
1139
+ function loadphase(readystate) {
1140
+ if (readystate === "interactive") return "interactive";
1141
+ if (readystate === "complete") return "complete";
1142
+ return "loading";
1143
+ }
1144
+ function parseurlpattern(step, key = "urlpattern") {
1145
+ let options = {};
1146
+ try {
1147
+ options = parseoptions(step);
1148
+ } catch {
1149
+ options = {};
1150
+ }
1151
+ const value = options[key];
1152
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
1153
+ const pattern = value;
1154
+ if (typeof pattern.url !== "string" || !pattern.url) return null;
1155
+ const mode = pattern.mode === "exact" || pattern.mode === "host" || pattern.mode === "pattern" ? pattern.mode : "prefix";
1156
+ const query = {};
1157
+ if (pattern.query && typeof pattern.query === "object" && !Array.isArray(pattern.query)) {
1158
+ for (const [name, item] of Object.entries(pattern.query)) if (typeof item === "string") query[name] = item;
1159
+ }
1160
+ return {
1161
+ mode,
1162
+ url: pattern.url,
1163
+ ...Object.keys(query).length > 0 ? { query } : {},
1164
+ ...typeof pattern.fragment === "string" && pattern.fragment ? { fragment: pattern.fragment } : {}
1165
+ };
1166
+ }
1167
+ function segmentmatches(pattern, actual) {
1168
+ if (pattern === "*" || pattern === "**") return true;
1169
+ if (!pattern.includes("*")) return pattern === actual;
1170
+ const parts = pattern.split("*");
1171
+ let index = 0;
1172
+ for (let position = 0; position < parts.length; position += 1) {
1173
+ const part = parts[position];
1174
+ if (part === "") continue;
1175
+ const found = actual.indexOf(part, index);
1176
+ if (found < 0) return false;
1177
+ if (position === 0 && found !== 0) return false;
1178
+ index = found + part.length;
1179
+ }
1180
+ const last = parts[parts.length - 1];
1181
+ return last === "" || actual.endsWith(last);
1182
+ }
1183
+ function urlmatches(url, pattern) {
1184
+ let parsed;
1185
+ try {
1186
+ parsed = new URL(url);
1187
+ } catch {
1188
+ return false;
1189
+ }
1190
+ let expected;
1191
+ try {
1192
+ expected = new URL(pattern.url);
1193
+ } catch {
1194
+ return false;
1195
+ }
1196
+ if (pattern.mode === "exact" && parsed.toString() !== expected.toString()) return false;
1197
+ if (pattern.mode === "prefix" && !parsed.toString().startsWith(pattern.url)) return false;
1198
+ if (pattern.mode === "host" && parsed.origin !== expected.origin) return false;
1199
+ if (pattern.mode === "pattern") {
1200
+ if (parsed.origin !== expected.origin) return false;
1201
+ const expectedsegments = expected.pathname.split("/").filter((segment) => segment !== "");
1202
+ const actualsegments = parsed.pathname.split("/").filter((segment) => segment !== "");
1203
+ if (expectedsegments.includes("**")) {
1204
+ const cut = expectedsegments.indexOf("**");
1205
+ const head = expectedsegments.slice(0, cut);
1206
+ const tail = expectedsegments.slice(cut + 1);
1207
+ if (actualsegments.length < head.length + tail.length) return false;
1208
+ if (!head.every((segment, position) => segmentmatches(segment, actualsegments[position] ?? ""))) return false;
1209
+ if (!tail.every((segment, position) => segmentmatches(segment, actualsegments[actualsegments.length - tail.length + position] ?? ""))) return false;
1210
+ } else if (expectedsegments.length !== actualsegments.length || !expectedsegments.every((segment, position) => segmentmatches(segment, actualsegments[position] ?? ""))) return false;
1211
+ }
1212
+ const values = parsed.searchParams;
1213
+ for (const [name, value] of Object.entries(pattern.query ?? {})) {
1214
+ if (!values.has(name)) return false;
1215
+ if (value !== "*" && values.get(name) !== value) return false;
1216
+ }
1217
+ if (pattern.fragment !== void 0 && parsed.hash.slice(1) !== pattern.fragment) return false;
1218
+ return true;
1219
+ }
1220
+ function readquery(url) {
1221
+ const values = {};
1222
+ try {
1223
+ for (const [name, value] of new URL(url).searchParams.entries()) values[name] = value;
1224
+ } catch {
1225
+ }
1226
+ return values;
1227
+ }
1228
+ function rewritequeryurl(url, set, remove) {
1229
+ const parsed = new URL(url);
1230
+ const before = readquery(url);
1231
+ for (const name of remove) parsed.searchParams.delete(name);
1232
+ for (const [name, value] of Object.entries(set)) parsed.searchParams.set(name, value);
1233
+ parsed.hash = "";
1234
+ return { url: parsed.toString(), before, after: readquery(parsed.toString()), set: Object.keys(set), removed: [...remove] };
1235
+ }
1236
+ function fragmenturl(url, fragment) {
1237
+ const parsed = new URL(url);
1238
+ parsed.hash = fragment.replace(/^#/, "");
1239
+ return parsed.toString();
1240
+ }
1241
+ function matchlinktext(links, text) {
1242
+ const wanted = text.trim().toLowerCase();
1243
+ return links.filter((link) => link.text.trim().toLowerCase() === wanted);
1244
+ }
1245
+ function matchlinkfragment(links, fragment) {
1246
+ const wanted = fragment.trim().replace(/^#/, "");
1247
+ return links.filter((link) => {
1248
+ try {
1249
+ return new URL(link.href, "https://example.invalid").hash.replace(/^#/, "") === wanted;
1250
+ } catch {
1251
+ return false;
1252
+ }
1253
+ });
1254
+ }
1255
+ function spauroutechanged(previousurl, currenturl) {
1256
+ if (previousurl === currenturl) return false;
1257
+ try {
1258
+ return new URL(previousurl).origin === new URL(currenturl).origin;
1259
+ } catch {
1260
+ return false;
1261
+ }
1262
+ }
1263
+ function wait2(ms) {
1264
+ return new Promise((resolve) => window.setTimeout(resolve, ms));
1265
+ }
1266
+ function stepoptions(step) {
1267
+ try {
1268
+ return parseoptions(step);
1269
+ } catch {
1270
+ return {};
1271
+ }
1272
+ }
1273
+ function collectlinks(root) {
1274
+ return [...root.querySelectorAll("a[href]")].map((element) => ({
1275
+ text: element.textContent?.trim() ?? "",
1276
+ href: element instanceof HTMLAnchorElement ? element.href : element.getAttribute("href") ?? "",
1277
+ selector: element.getAttribute("href") ?? ""
1278
+ }));
1279
+ }
1280
+ function resolvelink(step, root) {
1281
+ const options = stepoptions(step);
1282
+ const allowedorigins = Array.isArray(options.allowedorigins) ? options.allowedorigins.filter((item) => typeof item === "string") : [];
1283
+ const links = collectlinks(root);
1284
+ const matches = options.fragment === true ? matchlinkfragment(links, step.value ?? "") : matchlinktext(links, step.value ?? "");
1285
+ if (matches.length === 0) return { ok: false, summary: `No link matches the reviewed reference "${step.value ?? ""}".` };
1286
+ if (matches.length > 1) return { ok: false, summary: `The reviewed link reference matched ${matches.length} links; review a unique one.` };
1287
+ const element = [...root.querySelectorAll("a[href]")].find((candidate) => (candidate instanceof HTMLAnchorElement ? candidate.href : candidate.getAttribute("href") ?? "") === matches[0]?.href);
1288
+ if (!(element instanceof HTMLAnchorElement)) return { ok: false, summary: "The reviewed link is no longer available." };
1289
+ if (allowedorigins.length > 0) {
1290
+ let origin = "";
1291
+ try {
1292
+ origin = new URL(element.href).origin;
1293
+ } catch {
1294
+ origin = "";
1295
+ }
1296
+ if (!allowedorigins.includes(origin)) return { ok: false, summary: `The reviewed link leaves the session origin grants for ${origin}.` };
1297
+ }
1298
+ return { ok: true, element };
1299
+ }
1300
+ async function runwaitload(step) {
1301
+ const options = stepoptions(step);
1302
+ const timeout = typeof options.timeout === "number" && Number.isFinite(options.timeout) && options.timeout > 0 ? options.timeout : 0;
1303
+ const started = Date.now();
1304
+ for (; ; ) {
1305
+ const phase = loadphase(document.readyState);
1306
+ if (phase === "complete") return { ok: true, summary: `The page load event fired and the document is complete after ${Date.now() - started} milliseconds.`, details: { phase, readystate: document.readyState, waited: Date.now() - started } };
1307
+ if (timeout > 0 && Date.now() - started >= timeout) return { ok: false, summary: `The page did not reach the complete load phase within the reviewed timeout of ${timeout} milliseconds.`, details: { phase, readystate: document.readyState, waited: Date.now() - started } };
1308
+ await wait2(50);
1309
+ }
1310
+ }
1311
+ async function runwaiturl(step) {
1312
+ const options = stepoptions(step);
1313
+ const pattern = parseurlpattern(step);
1314
+ if (!pattern) return { ok: false, summary: "A reviewed urlpattern is required." };
1315
+ const timeout = typeof options.timeout === "number" && Number.isFinite(options.timeout) && options.timeout > 0 ? options.timeout : 0;
1316
+ const poll2 = typeof options.poll === "number" && Number.isFinite(options.poll) && options.poll > 0 ? options.poll : 100;
1317
+ const started = Date.now();
1318
+ for (; ; ) {
1319
+ if (urlmatches(location.href, pattern)) return { ok: true, summary: `The url matched the reviewed ${pattern.mode} pattern after ${Date.now() - started} milliseconds.`, details: { url: location.href, mode: pattern.mode, waited: Date.now() - started } };
1320
+ if (timeout > 0 && Date.now() - started >= timeout) return { ok: false, summary: `The url did not match the reviewed ${pattern.mode} pattern within the reviewed timeout of ${timeout} milliseconds.`, details: { url: location.href, mode: pattern.mode, waited: Date.now() - started } };
1321
+ await wait2(poll2);
1322
+ }
1323
+ }
1324
+ async function runfollowlink(step, root) {
1325
+ const resolution = resolvelink(step, root);
1326
+ if (!resolution.ok) return { ok: false, summary: resolution.summary };
1327
+ const href = resolution.element.href;
1328
+ resolution.element.click();
1329
+ return { ok: true, summary: `Followed the reviewed link to ${href}.`, details: { href } };
1330
+ }
1331
+ async function runspanav(step, root) {
1332
+ const options = stepoptions(step);
1333
+ const resolution = resolvelink(step, root);
1334
+ if (!resolution.ok) return { ok: false, summary: resolution.summary };
1335
+ const timeout = typeof options.timeout === "number" && Number.isFinite(options.timeout) && options.timeout > 0 ? options.timeout : 0;
1336
+ const pattern = parseurlpattern(step, "routepattern");
1337
+ const before = location.href;
1338
+ resolution.element.click();
1339
+ const started = Date.now();
1340
+ for (; ; ) {
1341
+ const changed = spauroutechanged(before, location.href);
1342
+ const matched = pattern ? urlmatches(location.href, pattern) : changed;
1343
+ if (matched) return { ok: true, summary: `The single page app route changed to ${location.href} without a reload.`, details: { from: before, to: location.href, waited: Date.now() - started } };
1344
+ if (timeout > 0 && Date.now() - started >= timeout) return { ok: false, summary: `The single page app route did not change within the reviewed timeout of ${timeout} milliseconds.`, details: { from: before, to: location.href, waited: Date.now() - started } };
1345
+ await wait2(50);
1346
+ }
1347
+ }
1348
+ async function runspawait(step) {
1349
+ const options = stepoptions(step);
1350
+ const timeout = typeof options.timeout === "number" && Number.isFinite(options.timeout) && options.timeout > 0 ? options.timeout : 0;
1351
+ const poll2 = typeof options.poll === "number" && Number.isFinite(options.poll) && options.poll > 0 ? options.poll : 100;
1352
+ const pattern = parseurlpattern(step);
1353
+ const before = location.href;
1354
+ const started = Date.now();
1355
+ let detected = false;
1356
+ const onroute = () => {
1357
+ if (spauroutechanged(before, location.href)) detected = true;
1358
+ };
1359
+ window.addEventListener("popstate", onroute);
1360
+ window.addEventListener("hashchange", onroute);
1361
+ try {
1362
+ for (; ; ) {
1363
+ if (pattern ? urlmatches(location.href, pattern) : detected || spauroutechanged(before, location.href)) {
1364
+ return { ok: true, summary: `The single page app url changed to ${location.href} without a reload.`, details: { from: before, to: location.href, waited: Date.now() - started } };
1365
+ }
1366
+ if (timeout > 0 && Date.now() - started >= timeout) return { ok: false, summary: `The single page app url did not change within the reviewed timeout of ${timeout} milliseconds.`, details: { from: before, to: location.href, waited: Date.now() - started } };
1367
+ await wait2(poll2);
1368
+ }
1369
+ } finally {
1370
+ window.removeEventListener("popstate", onroute);
1371
+ window.removeEventListener("hashchange", onroute);
1372
+ }
1373
+ }
1374
+ function runrewritequery(step) {
1375
+ const options = stepoptions(step);
1376
+ const set = {};
1377
+ if (options.set && typeof options.set === "object" && !Array.isArray(options.set)) {
1378
+ for (const [name, value] of Object.entries(options.set)) if (typeof value === "string") set[name] = value;
1379
+ }
1380
+ const remove = Array.isArray(options.remove) ? options.remove.filter((item) => typeof item === "string" && item.trim().length > 0) : [];
1381
+ const outcome = rewritequeryurl(location.href, set, remove);
1382
+ history.pushState(history.state, document.title, outcome.url);
1383
+ return { ok: true, summary: `Rewrote ${outcome.set.length + outcome.removed.length} query parameter${outcome.set.length + outcome.removed.length === 1 ? "" : "s"}; the url is now ${outcome.url}.`, details: { url: outcome.url, before: outcome.before, after: outcome.after, set: outcome.set, removed: outcome.removed } };
1384
+ }
1385
+ async function runsetfragment(step) {
1386
+ const fragment = (step.value ?? "").replace(/^#/, "");
1387
+ if (!fragment) return { ok: false, summary: "A reviewed fragment is required." };
1388
+ const url = fragmenturl(location.href, fragment);
1389
+ history.pushState(history.state, document.title, url);
1390
+ const anchor = document.getElementById(fragment);
1391
+ anchor?.scrollIntoView({ behavior: "smooth", block: "start" });
1392
+ return { ok: true, summary: `Set the url fragment to ${fragment} and scrolled to its anchor.`, details: { url, fragment, anchored: Boolean(anchor) } };
1393
+ }
1394
+ function runstopnav() {
1395
+ window.stop();
1396
+ return { ok: true, summary: "Stopped the pending navigation of the page." };
1397
+ }
1398
+ function runprefetch(step) {
1399
+ const options = stepoptions(step);
1400
+ const urls = Array.isArray(options.urls) ? options.urls.filter((item) => typeof item === "string" && item.trim().length > 0) : [];
1401
+ if (urls.length === 0) return { ok: false, summary: "A reviewed list of prefetch urls is required." };
1402
+ for (const url of urls) {
1403
+ const hint = document.createElement("link");
1404
+ hint.rel = "prefetch";
1405
+ hint.href = url;
1406
+ document.head.append(hint);
1407
+ }
1408
+ return { ok: true, summary: `Queued ${urls.length} prefetch hint${urls.length === 1 ? "" : "s"}.`, details: { urls } };
1409
+ }
1410
+ function runpreconnect(step) {
1411
+ const options = stepoptions(step);
1412
+ const origins = Array.isArray(options.origins) ? options.origins.filter((item) => typeof item === "string" && item.trim().length > 0) : [];
1413
+ if (origins.length === 0) return { ok: false, summary: "A reviewed list of preconnect origins is required." };
1414
+ for (const origin of origins) {
1415
+ const hint = document.createElement("link");
1416
+ hint.rel = "preconnect";
1417
+ hint.href = origin;
1418
+ document.head.append(hint);
1419
+ }
1420
+ return { ok: true, summary: `Opened ${origins.length} preconnect hint${origins.length === 1 ? "" : "s"}.`, details: { origins } };
1421
+ }
1422
+ function runprintpdf() {
1423
+ window.print();
1424
+ return { ok: true, summary: "Sent the page to the browser print pipeline." };
1425
+ }
1426
+ function runpagenav(step, root = document) {
1427
+ switch (step.kind) {
1428
+ case "waitload":
1429
+ return runwaitload(step);
1430
+ case "waiturl":
1431
+ return runwaiturl(step);
1432
+ case "followlink":
1433
+ return runfollowlink(step, root);
1434
+ case "spanav":
1435
+ return runspanav(step, root);
1436
+ case "spawait":
1437
+ return runspawait(step);
1438
+ case "rewritequery":
1439
+ return runrewritequery(step);
1440
+ case "setfragment":
1441
+ return runsetfragment(step);
1442
+ case "stopnav":
1443
+ return runstopnav();
1444
+ case "prefetch":
1445
+ return runprefetch(step);
1446
+ case "preconnect":
1447
+ return runpreconnect(step);
1448
+ case "printpdf":
1449
+ return runprintpdf();
1450
+ default:
1451
+ return { ok: false, summary: "Unsupported navigation action." };
1452
+ }
1453
+ }
1454
+
1138
1455
  // extension/pagedialogs.ts
1139
1456
  function harvestdialoglog(root) {
1140
1457
  const raw = root.documentElement.dataset.devthinkdialoglog;
@@ -1405,7 +1722,7 @@
1405
1722
  return { index, origin, sameorigin: origin !== "" && origin === location.origin, width: Math.round(rect.width), height: Math.round(rect.height) };
1406
1723
  });
1407
1724
  }
1408
- function stepoptions(step) {
1725
+ function stepoptions2(step) {
1409
1726
  try {
1410
1727
  return parseoptions(step);
1411
1728
  } catch {
@@ -1449,7 +1766,7 @@
1449
1766
  return { ok: true, summary: `Detected page language ${outcome.language || "unknown"} from the ${outcome.source} signal.`, details: { language: outcome.language, source: outcome.source } };
1450
1767
  }
1451
1768
  case "detectlanguage": {
1452
- const options = stepoptions(step);
1769
+ const options = stepoptions2(step);
1453
1770
  const text = typeof options.text === "string" && options.text ? options.text : target?.textContent ?? root.body?.innerText ?? "";
1454
1771
  const routed = taglanguage(clean(text));
1455
1772
  return { ok: routed.language !== "", summary: routed.language ? `Detected language ${routed.language} for the extracted text.` : "The extracted text language is undetermined.", details: { language: routed.language, routed } };
@@ -1878,7 +2195,7 @@
1878
2195
  if (shape.index > 0) candidates.push({ selector: `${shape.tag}:nth-of-type(${shape.index})`, strategy: "structural", score: 40 });
1879
2196
  return candidates.sort((left, right) => right.score - left.score);
1880
2197
  }
1881
- function wait2(ms) {
2198
+ function wait3(ms) {
1882
2199
  return new Promise((resolve) => window.setTimeout(resolve, ms));
1883
2200
  }
1884
2201
  function quietruleof(step) {
@@ -1912,7 +2229,7 @@
1912
2229
  }
1913
2230
  });
1914
2231
  for (const scope of roots) observer.observe(scope, { childList: true, attributes: true, characterData: true, subtree: true });
1915
- await wait2(options.lifetime);
2232
+ await wait3(options.lifetime);
1916
2233
  observer.disconnect();
1917
2234
  const batches = batchmutations(collected, options.poll);
1918
2235
  return { ok: true, summary: `Watched ${collected.length} mutation${collected.length === 1 ? "" : "s"} in ${batches.length} batch${batches.length === 1 ? "" : "es"} for the reviewed lifetime of ${options.lifetime} milliseconds.`, details: { events: collected, batches: batches.length, watchid: options.watchid, lifetime: options.lifetime, scopes: options.scopes ?? [] } };
@@ -1929,7 +2246,7 @@
1929
2246
  const onblur = record("blur");
1930
2247
  root.addEventListener("focusin", onfocus, true);
1931
2248
  root.addEventListener("focusout", onblur, true);
1932
- await wait2(options.lifetime);
2249
+ await wait3(options.lifetime);
1933
2250
  root.removeEventListener("focusin", onfocus, true);
1934
2251
  root.removeEventListener("focusout", onblur, true);
1935
2252
  return { ok: true, summary: `Watched ${collected.length} focus change${collected.length === 1 ? "" : "s"} for the reviewed lifetime of ${options.lifetime} milliseconds.`, details: { events: collected, watchid: options.watchid, lifetime: options.lifetime } };
@@ -1944,7 +2261,7 @@
1944
2261
  for (const report of bannermatches(collectbannercandidates(root), at)) {
1945
2262
  if (!seen.has(report.selector)) seen.set(report.selector, report);
1946
2263
  }
1947
- await wait2(options.poll);
2264
+ await wait3(options.poll);
1948
2265
  }
1949
2266
  const reports = [...seen.values()];
1950
2267
  return { ok: true, summary: `Watched for consent banners for the reviewed lifetime of ${options.lifetime} milliseconds and observed ${reports.length} banner${reports.length === 1 ? "" : "s"}.`, details: { banners: reports, watchid: options.watchid, lifetime: options.lifetime } };
@@ -1963,7 +2280,7 @@
1963
2280
  const latest = samples[samples.length - 1];
1964
2281
  if (latest && latest.quietfor >= rule.idle) break;
1965
2282
  if (timeout > 0 && now - started >= timeout) break;
1966
- await wait2(poll2);
2283
+ await wait3(poll2);
1967
2284
  }
1968
2285
  const outcome = quietresolution(samples, rule.idle, timeout);
1969
2286
  return {
@@ -2042,7 +2359,7 @@
2042
2359
  }
2043
2360
 
2044
2361
  // extension/pagebridge.ts
2045
- function stepoptions2(step) {
2362
+ function stepoptions3(step) {
2046
2363
  if (!step.options) return {};
2047
2364
  try {
2048
2365
  const parsed = JSON.parse(step.options);
@@ -2147,6 +2464,7 @@
2147
2464
  var observationkinds = /* @__PURE__ */ new Set(["a11ytree", "readvisible", "readertree", "readoutline", "readselection", "readopengraph", "readlang", "detectlanguage", "listshadow", "listframes"]);
2148
2465
  var detectionkinds = /* @__PURE__ */ new Set(["detectlists", "detecttables", "detectinfinitescroll", "detectvirtual", "detectlazy", "detectsticky", "detectscrolllock", "countpages", "classifypage", "fingerprintsection", "readscrollpos"]);
2149
2466
  var watchstepkinds = /* @__PURE__ */ new Set(["watchmutate", "watchbanner", "watchfocus", "waitquiet", "readjson", "diffsnapshots", "deriveselector"]);
2467
+ var navstepkinds = /* @__PURE__ */ new Set(["waitload", "waiturl", "followlink", "spanav", "spawait", "rewritequery", "setfragment", "stopnav", "prefetch", "preconnect", "printpdf"]);
2150
2468
  async function performstep(step, expectedorigin, rootdocument = document) {
2151
2469
  if (location.origin !== expectedorigin) return { ok: false, summary: "Page origin changed before action." };
2152
2470
  if (step.kind === "observe") return { ok: true, summary: "Observation completed." };
@@ -2173,8 +2491,9 @@
2173
2491
  history.forward();
2174
2492
  return { ok: true, summary: "History forward requested." };
2175
2493
  }
2494
+ if (navstepkinds.has(step.kind)) return await runpagenav(step, rootdocument);
2176
2495
  if (step.kind === "scrollpage") {
2177
- const options = stepoptions2(step);
2496
+ const options = stepoptions3(step);
2178
2497
  window.scrollBy({ left: typeof options.x === "number" ? options.x : 0, top: typeof options.y === "number" ? options.y : 600, behavior: "auto" });
2179
2498
  return { ok: true, summary: "Window scrolled by the reviewed amounts." };
2180
2499
  }
@@ -2203,7 +2522,7 @@
2203
2522
  else {
2204
2523
  if (!element) return { ok: false, summary: "Action target is no longer available." };
2205
2524
  if (step.kind === "scrollby") {
2206
- const options = stepoptions2(step);
2525
+ const options = stepoptions3(step);
2207
2526
  element.scrollBy({ left: typeof options.x === "number" ? options.x : 0, top: typeof options.y === "number" ? options.y : 600, behavior: "auto" });
2208
2527
  result = { ok: true, summary: "Container scrolled by the reviewed amounts." };
2209
2528
  } else if (step.kind === "focus") {