@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.
- package/README.md +5 -4
- package/dist/index.js +315 -4
- package/dist/index.js.map +2 -2
- package/dist/memory.d.ts +53 -1
- package/dist/memory.d.ts.map +1 -1
- package/dist/policy.d.ts +8 -1
- package/dist/policy.d.ts.map +1 -1
- package/dist/protocol.d.ts +23 -1
- package/dist/protocol.d.ts.map +1 -1
- package/dist/types.d.ts +146 -3
- package/dist/types.d.ts.map +1 -1
- package/dist/version.d.ts +1 -1
- package/extension/dist/background.js +1164 -11
- package/extension/dist/background.js.map +4 -4
- package/extension/dist/manifest.json +1 -1
- package/extension/dist/pagebridge.js +331 -12
- package/extension/dist/pagebridge.js.map +4 -4
- package/extension/dist/popup.html +1 -1
- package/extension/dist/popup.js +12 -2
- package/extension/dist/popup.js.map +2 -2
- package/extension/dist/sidepanel.html +1 -1
- package/extension/dist/sidepanel.js +189 -8
- package/extension/dist/sidepanel.js.map +3 -3
- package/extension/manifest.json +1 -1
- package/package.json +1 -1
|
@@ -238,19 +238,133 @@ var sessionmemory = class {
|
|
|
238
238
|
async setsignals(signals) {
|
|
239
239
|
return this.adapter.set("signals", signals);
|
|
240
240
|
}
|
|
241
|
+
/** Appends one navigation trail entry of a session with its url, title, step ref and timestamp. */
|
|
242
|
+
async addtrailentry(sessionid, entry) {
|
|
243
|
+
const records = await this.gettrail(sessionid);
|
|
244
|
+
await this.adapter.set(`trail${sessionid}`, [...records, entry]);
|
|
245
|
+
}
|
|
246
|
+
/** Returns the navigation trail of a session, oldest first. */
|
|
247
|
+
async gettrail(sessionid) {
|
|
248
|
+
return await this.adapter.get(`trail${sessionid}`) ?? [];
|
|
249
|
+
}
|
|
250
|
+
/** Stores one wait profile for an origin with user configured values, replacing the previous profile of that origin. */
|
|
251
|
+
async setwaitprofile(record2) {
|
|
252
|
+
const records = (await this.getwaitprofiles()).filter((item) => item.origin !== record2.origin);
|
|
253
|
+
await this.adapter.set("waitprofiles", [...records, record2]);
|
|
254
|
+
}
|
|
255
|
+
/** Returns every stored wait profile with its origin and user configured values, newest first. */
|
|
256
|
+
async getwaitprofiles() {
|
|
257
|
+
return await this.adapter.get("waitprofiles") ?? [];
|
|
258
|
+
}
|
|
259
|
+
/** Records one navigation step with its redirect chain and final url. */
|
|
260
|
+
async addnavrecord(record2) {
|
|
261
|
+
const records = await this.getnavrecords();
|
|
262
|
+
await this.adapter.set("navrecords", [record2, ...records]);
|
|
263
|
+
}
|
|
264
|
+
/** Returns every stored navigation record with redirect chains and final urls, newest first. */
|
|
265
|
+
async getnavrecords() {
|
|
266
|
+
return await this.adapter.get("navrecords") ?? [];
|
|
267
|
+
}
|
|
268
|
+
/** Records one navigation intent detected from a plan for audit review. */
|
|
269
|
+
async addnavintent(record2) {
|
|
270
|
+
const records = await this.getnavintents();
|
|
271
|
+
await this.adapter.set("navintents", [record2, ...records]);
|
|
272
|
+
}
|
|
273
|
+
/** Returns every stored navigation intent record, newest first. */
|
|
274
|
+
async getnavintents() {
|
|
275
|
+
return await this.adapter.get("navintents") ?? [];
|
|
276
|
+
}
|
|
277
|
+
/** Replaces the rate limit window state of one domain. */
|
|
278
|
+
async setratestate(state) {
|
|
279
|
+
const records = (await this.getratestates()).filter((item) => item.domain !== state.domain);
|
|
280
|
+
await this.adapter.set("ratestates", [...records, state]);
|
|
281
|
+
}
|
|
282
|
+
/** Returns every rate limit window state per domain. */
|
|
283
|
+
async getratestates() {
|
|
284
|
+
return await this.adapter.get("ratestates") ?? [];
|
|
285
|
+
}
|
|
286
|
+
/** Records one curated link list with its review state before batch opening. */
|
|
287
|
+
async addcurated(list) {
|
|
288
|
+
const records = await this.getcurateds();
|
|
289
|
+
await this.adapter.set("curated", [list, ...records]);
|
|
290
|
+
}
|
|
291
|
+
/** Returns every stored curated link list, newest first. */
|
|
292
|
+
async getcurateds() {
|
|
293
|
+
return await this.adapter.get("curated") ?? [];
|
|
294
|
+
}
|
|
295
|
+
/** Stores reviewed basic auth credentials for one origin, replacing the previous record of that origin. */
|
|
296
|
+
async setauth(record2) {
|
|
297
|
+
const records = (await this.getauths()).filter((item) => item.origin !== record2.origin);
|
|
298
|
+
await this.adapter.set("auths", [...records, record2]);
|
|
299
|
+
}
|
|
300
|
+
/** Returns every stored reviewed basic auth record per origin. */
|
|
301
|
+
async getauths() {
|
|
302
|
+
return await this.adapter.get("auths") ?? [];
|
|
303
|
+
}
|
|
304
|
+
/** Records one task artifact routed into the artifact store. */
|
|
305
|
+
async addartifact(record2) {
|
|
306
|
+
const records = await this.getartifacts();
|
|
307
|
+
await this.adapter.set("artifacts", [record2, ...records]);
|
|
308
|
+
}
|
|
309
|
+
/** Returns every stored task artifact, newest first. */
|
|
310
|
+
async getartifacts() {
|
|
311
|
+
return await this.adapter.get("artifacts") ?? [];
|
|
312
|
+
}
|
|
313
|
+
/** Returns the navigation control state of paused navigation. */
|
|
314
|
+
async getnavcontrol() {
|
|
315
|
+
return this.adapter.get("navcontrol");
|
|
316
|
+
}
|
|
317
|
+
/** Replaces the navigation control state after a pause or resume transition. */
|
|
318
|
+
async setnavcontrol(control) {
|
|
319
|
+
return this.adapter.set("navcontrol", control);
|
|
320
|
+
}
|
|
321
|
+
/** Records one url safety verdict produced by a checksafe verification. */
|
|
322
|
+
async addsafety(verdict) {
|
|
323
|
+
const records = await this.getsafeties();
|
|
324
|
+
await this.adapter.set("safeties", [verdict, ...records]);
|
|
325
|
+
}
|
|
326
|
+
/** Returns every stored url safety verdict, newest first. */
|
|
327
|
+
async getsafeties() {
|
|
328
|
+
return await this.adapter.get("safeties") ?? [];
|
|
329
|
+
}
|
|
330
|
+
/** Records one recently closed tab so a reopentab step can restore it. */
|
|
331
|
+
async addrecenttab(tab) {
|
|
332
|
+
const records = await this.getrecenttabs();
|
|
333
|
+
await this.adapter.set("recenttabs", [tab, ...records]);
|
|
334
|
+
}
|
|
335
|
+
/** Returns every recently closed tab, newest first. */
|
|
336
|
+
async getrecenttabs() {
|
|
337
|
+
return await this.adapter.get("recenttabs") ?? [];
|
|
338
|
+
}
|
|
339
|
+
/** Returns the queued prefetch and batch open target counts shown in the popup badge. */
|
|
340
|
+
async getnavqueues() {
|
|
341
|
+
return this.adapter.get("navqueues");
|
|
342
|
+
}
|
|
343
|
+
/** Replaces the queued prefetch and batch open target counts. */
|
|
344
|
+
async setnavqueues(queues) {
|
|
345
|
+
return this.adapter.set("navqueues", queues);
|
|
346
|
+
}
|
|
347
|
+
/** Returns the last known navigation state of a tab, kept across service worker restarts. */
|
|
348
|
+
async getnavstate(tabid2) {
|
|
349
|
+
return this.adapter.get(`navstate${tabid2}`);
|
|
350
|
+
}
|
|
351
|
+
/** Replaces the last known navigation state of a tab. */
|
|
352
|
+
async setnavstate(tabid2, state) {
|
|
353
|
+
return this.adapter.set(`navstate${tabid2}`, state);
|
|
354
|
+
}
|
|
241
355
|
};
|
|
242
356
|
function randomid() {
|
|
243
357
|
return crypto.randomUUID();
|
|
244
358
|
}
|
|
245
359
|
|
|
246
360
|
// policy.ts
|
|
247
|
-
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"]);
|
|
361
|
+
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"]);
|
|
248
362
|
var interactionactions = /* @__PURE__ */ new Set(["focus", "scroll", "hover", "clickdeep", "rightclick", "doubleclick", "scrollpage", "scrollby", "scrollend", "scrolltop", "fullscreen", "zoomset", "movepointer", "clicktext", "clickaria", "clickname", "expanddetails", "pierceshadow", "retryaction"]);
|
|
249
|
-
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"]);
|
|
363
|
+
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"]);
|
|
250
364
|
var allowedactions = /* @__PURE__ */ new Set([...sensitiveactions, ...interactionactions, ...readactions]);
|
|
251
365
|
var watchactions = /* @__PURE__ */ new Set(["watchmutate", "watchbanner", "watchfocus"]);
|
|
252
366
|
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"]);
|
|
253
|
-
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"]);
|
|
367
|
+
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"]);
|
|
254
368
|
function normalizeendpoint(value) {
|
|
255
369
|
const endpoint = new URL(value.trim());
|
|
256
370
|
if (endpoint.protocol !== "https:") throw new Error("Devthink accepts HTTPS endpoints only.");
|
|
@@ -281,6 +395,8 @@ function parseoptions(step) {
|
|
|
281
395
|
function requiredcapability(kind) {
|
|
282
396
|
if (kind === "tablist") return "tabs";
|
|
283
397
|
if (kind === "downloadfile") return "downloads";
|
|
398
|
+
if (kind === "openclipboard") return "clipboardRead";
|
|
399
|
+
if (kind === "openlink" || kind === "openprivate" || kind === "navlist" || kind === "batchopen" || kind === "reopentab" || kind === "deeplink") return "tabs";
|
|
284
400
|
return void 0;
|
|
285
401
|
}
|
|
286
402
|
function waitduration(step) {
|
|
@@ -331,6 +447,35 @@ function origingranted(session, origin) {
|
|
|
331
447
|
const grants = session.grants ?? [session.origin];
|
|
332
448
|
return grants.includes(origin);
|
|
333
449
|
}
|
|
450
|
+
function originverified(url, grants, verdicts) {
|
|
451
|
+
let origin = "";
|
|
452
|
+
try {
|
|
453
|
+
origin = new URL(url).origin;
|
|
454
|
+
} catch {
|
|
455
|
+
return { allowed: false, reason: "The reviewed navigation URL is invalid." };
|
|
456
|
+
}
|
|
457
|
+
if (grants.includes(origin)) return { allowed: true };
|
|
458
|
+
const covered = verdicts.find((verdict) => verdict.safe && (verdict.url === url || safeorigin(verdict.url) === origin));
|
|
459
|
+
if (covered) return { allowed: true };
|
|
460
|
+
return { allowed: false, reason: `The origin ${origin} is outside the session grants and has no safe checksafe verdict; run checksafe and review it first.` };
|
|
461
|
+
}
|
|
462
|
+
function safeorigin(url) {
|
|
463
|
+
try {
|
|
464
|
+
return new URL(url).origin;
|
|
465
|
+
} catch {
|
|
466
|
+
return "";
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
function navigationgranted(session, url) {
|
|
470
|
+
let origin = "";
|
|
471
|
+
try {
|
|
472
|
+
origin = new URL(url).origin;
|
|
473
|
+
} catch {
|
|
474
|
+
return { allowed: false, reason: "The reviewed navigation URL is invalid." };
|
|
475
|
+
}
|
|
476
|
+
if (origingranted(session, origin)) return { allowed: true };
|
|
477
|
+
return { allowed: false, reason: `Navigation to ${origin} leaves the task tab origins and needs the user consent of a session grant first.` };
|
|
478
|
+
}
|
|
334
479
|
function validateinnerstep(options, origin) {
|
|
335
480
|
const stepid = options.stepid;
|
|
336
481
|
const kind = options.kind;
|
|
@@ -354,6 +499,68 @@ function validateinnerstep(options, origin) {
|
|
|
354
499
|
};
|
|
355
500
|
return validatestep(inner, origin);
|
|
356
501
|
}
|
|
502
|
+
function ishttpsurl(value) {
|
|
503
|
+
if (typeof value !== "string" || !value.trim()) return false;
|
|
504
|
+
try {
|
|
505
|
+
return new URL(value).protocol === "https:";
|
|
506
|
+
} catch {
|
|
507
|
+
return false;
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
function validatenavtarget(value, kind) {
|
|
511
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return { allowed: false, reason: "A reviewed navtarget with a url is required in options." };
|
|
512
|
+
const target = value;
|
|
513
|
+
if (!ishttpsurl(target.url)) return { allowed: false, reason: "The reviewed navtarget url must use HTTPS." };
|
|
514
|
+
const container = target.container ?? "tab";
|
|
515
|
+
if (container !== "current" && container !== "tab" && container !== "window" && container !== "private") return { allowed: false, reason: "The reviewed navtarget container must be current, tab, window or private." };
|
|
516
|
+
if (target.position !== void 0 && target.position !== "adjacent" && target.position !== "end") return { allowed: false, reason: "The reviewed navtarget position must be adjacent or end." };
|
|
517
|
+
if (kind === "openprivate" && container !== "private") return { allowed: false, reason: "The openprivate step requires the private container." };
|
|
518
|
+
if (kind === "openlink" && container === "private") return { allowed: false, reason: "The openlink step cannot open the private container; use openprivate." };
|
|
519
|
+
return { allowed: true };
|
|
520
|
+
}
|
|
521
|
+
function validatewaitprofile(value) {
|
|
522
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return { allowed: false, reason: "A reviewed waitprofile with load signals is required in options." };
|
|
523
|
+
const profile = value;
|
|
524
|
+
if (!Array.isArray(profile.signals) || profile.signals.length === 0 || !profile.signals.every((signal) => isnonempty(signal))) return { allowed: false, reason: "The reviewed waitprofile needs a non-empty list of load signals." };
|
|
525
|
+
if (!nonnegativeoption(profile, "idle")) return { allowed: false, reason: "The reviewed waitprofile idle threshold must be zero or a positive number of milliseconds." };
|
|
526
|
+
if (!nonnegativeoption(profile, "timeout")) return { allowed: false, reason: "The reviewed waitprofile timeout must be zero or a positive number of milliseconds." };
|
|
527
|
+
if (profile.overrides !== void 0) {
|
|
528
|
+
if (!Array.isArray(profile.overrides) || profile.overrides.length === 0) return { allowed: false, reason: "The reviewed waitprofile overrides must be a non-empty list when present." };
|
|
529
|
+
for (const entry of profile.overrides) {
|
|
530
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) return { allowed: false, reason: "Every reviewed waitprofile override must be an object with an origin." };
|
|
531
|
+
const override = entry;
|
|
532
|
+
if (!ishttpsurl(override.origin)) return { allowed: false, reason: "Every reviewed waitprofile override origin must use HTTPS." };
|
|
533
|
+
if (override.signals !== void 0 && (!Array.isArray(override.signals) || !override.signals.every((signal) => isnonempty(signal)))) return { allowed: false, reason: "The reviewed waitprofile override signals must be a list of non-empty strings." };
|
|
534
|
+
if (!nonnegativeoption(override, "idle") || !nonnegativeoption(override, "timeout")) return { allowed: false, reason: "The reviewed waitprofile override thresholds must be zero or positive numbers." };
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
return { allowed: true };
|
|
538
|
+
}
|
|
539
|
+
function validateurlpattern(value) {
|
|
540
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return { allowed: false, reason: "A reviewed urlpattern is required in options." };
|
|
541
|
+
const pattern = value;
|
|
542
|
+
if (pattern.mode !== "exact" && pattern.mode !== "prefix" && pattern.mode !== "host" && pattern.mode !== "pattern") return { allowed: false, reason: "The reviewed urlpattern mode must be exact, prefix, host or pattern." };
|
|
543
|
+
if (!ishttpsurl(pattern.url)) return { allowed: false, reason: "The reviewed urlpattern url must use HTTPS." };
|
|
544
|
+
if (pattern.query !== void 0) {
|
|
545
|
+
if (!pattern.query || typeof pattern.query !== "object" || Array.isArray(pattern.query)) return { allowed: false, reason: "The reviewed urlpattern query part must be an object of parameter names and values." };
|
|
546
|
+
for (const item of Object.values(pattern.query)) if (typeof item !== "string") return { allowed: false, reason: "The reviewed urlpattern query values must be strings." };
|
|
547
|
+
}
|
|
548
|
+
if (pattern.fragment !== void 0 && !isnonempty(pattern.fragment)) return { allowed: false, reason: "The reviewed urlpattern fragment must be a non-empty string." };
|
|
549
|
+
return { allowed: true };
|
|
550
|
+
}
|
|
551
|
+
function validateurllist(options, key) {
|
|
552
|
+
const urls = options[key];
|
|
553
|
+
if (!Array.isArray(urls) || urls.length === 0 || !urls.every((url) => ishttpsurl(url))) return { allowed: false, reason: `A reviewed non-empty list of HTTPS urls is required in options as ${key}.` };
|
|
554
|
+
return { allowed: true };
|
|
555
|
+
}
|
|
556
|
+
function validateratelimit(value) {
|
|
557
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return { allowed: false, reason: "A reviewed ratelimit with a window and a ceiling is required in options." };
|
|
558
|
+
const limit = value;
|
|
559
|
+
if (limit.domain !== void 0 && !isnonempty(limit.domain)) return { allowed: false, reason: "The reviewed ratelimit domain must be a non-empty string." };
|
|
560
|
+
if (typeof limit.window !== "number" || !Number.isFinite(limit.window) || limit.window <= 0) return { allowed: false, reason: "The reviewed ratelimit window must be a positive number of milliseconds with no code ceiling." };
|
|
561
|
+
if (typeof limit.ceiling !== "number" || !Number.isInteger(limit.ceiling) || limit.ceiling < 1) return { allowed: false, reason: "The reviewed ratelimit ceiling must be a positive integer with no code ceiling." };
|
|
562
|
+
return { allowed: true };
|
|
563
|
+
}
|
|
357
564
|
function validatestep(step, origin) {
|
|
358
565
|
if (!allowedactions.has(step.kind)) return { allowed: false, reason: "Unsupported action kind." };
|
|
359
566
|
if (!step.summary.trim()) return { allowed: false, reason: "A human-readable action summary is required." };
|
|
@@ -491,6 +698,70 @@ function validatestep(step, origin) {
|
|
|
491
698
|
const versions = options.versions;
|
|
492
699
|
if (!Array.isArray(versions) || versions.length !== 2 || !versions.every((version) => typeof version === "number" && Number.isInteger(version) && version >= 1)) return { allowed: false, reason: "Two reviewed observation version numbers are required in options." };
|
|
493
700
|
}
|
|
701
|
+
if (step.kind === "openlink" || step.kind === "openprivate" || step.kind === "deeplink") {
|
|
702
|
+
const targetcheck = validatenavtarget(options.navtarget, step.kind);
|
|
703
|
+
if (!targetcheck.allowed) return targetcheck;
|
|
704
|
+
if (step.kind === "deeplink") {
|
|
705
|
+
const app = options.app;
|
|
706
|
+
if (!isnonempty(app)) return { allowed: false, reason: "A reviewed deep link app pattern is required in options." };
|
|
707
|
+
const params = options.params;
|
|
708
|
+
if (params !== void 0 && (!params || typeof params !== "object" || Array.isArray(params) || !Object.values(params).every((item) => typeof item === "string"))) return { allowed: false, reason: "The reviewed deep link params must be an object of string values." };
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
if (step.kind === "waitload" && !nonnegativeoption(options, "timeout")) return { allowed: false, reason: "The waitload timeout must be zero or a positive number of milliseconds." };
|
|
712
|
+
if (step.kind === "waiturl" || step.kind === "spawait") {
|
|
713
|
+
if (step.kind === "waiturl") {
|
|
714
|
+
const patterncheck = validateurlpattern(options.urlpattern);
|
|
715
|
+
if (!patterncheck.allowed) return patterncheck;
|
|
716
|
+
}
|
|
717
|
+
if (!nonnegativeoption(options, "timeout")) return { allowed: false, reason: "The wait timeout must be zero or a positive number of milliseconds." };
|
|
718
|
+
if (!nonnegativeoption(options, "poll")) return { allowed: false, reason: "The wait poll interval must be zero or a positive number of milliseconds." };
|
|
719
|
+
}
|
|
720
|
+
if (step.kind === "followlink") {
|
|
721
|
+
if (options.fragment !== void 0 && typeof options.fragment !== "boolean") return { allowed: false, reason: "The reviewed followlink fragment flag must be a boolean." };
|
|
722
|
+
}
|
|
723
|
+
if (step.kind === "spanav") {
|
|
724
|
+
if (options.routepattern !== void 0) {
|
|
725
|
+
const routecheck = validateurlpattern(options.routepattern);
|
|
726
|
+
if (!routecheck.allowed) return routecheck;
|
|
727
|
+
}
|
|
728
|
+
if (!nonnegativeoption(options, "timeout")) return { allowed: false, reason: "The spanav route timeout must be zero or a positive number of milliseconds." };
|
|
729
|
+
}
|
|
730
|
+
if (step.kind === "rewritequery") {
|
|
731
|
+
const set = options.set;
|
|
732
|
+
const remove = options.remove;
|
|
733
|
+
if (set === void 0 && remove === void 0) return { allowed: false, reason: "Reviewed query parameters to set or remove are required in options." };
|
|
734
|
+
if (set !== void 0 && (!set || typeof set !== "object" || Array.isArray(set) || !Object.values(set).every((item) => typeof item === "string"))) return { allowed: false, reason: "The reviewed query parameters to set must be an object of string values." };
|
|
735
|
+
if (remove !== void 0 && (!Array.isArray(remove) || !remove.every((item) => isnonempty(item)))) return { allowed: false, reason: "The reviewed query parameters to remove must be a list of non-empty names." };
|
|
736
|
+
}
|
|
737
|
+
if (step.kind === "navlist") {
|
|
738
|
+
const listcheck = validateurllist(options, "urls");
|
|
739
|
+
if (!listcheck.allowed) return listcheck;
|
|
740
|
+
}
|
|
741
|
+
if (step.kind === "navprofile") {
|
|
742
|
+
const profilecheck = validatewaitprofile(options.waitprofile);
|
|
743
|
+
if (!profilecheck.allowed) return profilecheck;
|
|
744
|
+
}
|
|
745
|
+
if (step.kind === "handleauth" && !ishttpsurl(step.value)) return { allowed: false, reason: "A reviewed HTTPS origin or url is required as the auth target." };
|
|
746
|
+
if (step.kind === "printpdf" && options.name !== void 0 && !isnonempty(options.name)) return { allowed: false, reason: "The reviewed artifact name must be a non-empty string." };
|
|
747
|
+
if (step.kind === "prefetch") {
|
|
748
|
+
const listcheck = validateurllist(options, "urls");
|
|
749
|
+
if (!listcheck.allowed) return listcheck;
|
|
750
|
+
}
|
|
751
|
+
if (step.kind === "preconnect") {
|
|
752
|
+
const origins = options.origins;
|
|
753
|
+
if (!Array.isArray(origins) || origins.length === 0 || !origins.every((originurl) => ishttpsurl(originurl))) return { allowed: false, reason: "A reviewed non-empty list of HTTPS origins is required in options." };
|
|
754
|
+
}
|
|
755
|
+
if (step.kind === "reopentab" && step.value !== void 0 && !ishttpsurl(step.value)) return { allowed: false, reason: "The reviewed reopen url must use HTTPS." };
|
|
756
|
+
if (step.kind === "navrate") {
|
|
757
|
+
const limitcheck = validateratelimit(options.ratelimit);
|
|
758
|
+
if (!limitcheck.allowed) return limitcheck;
|
|
759
|
+
}
|
|
760
|
+
if (step.kind === "checksafe" && !ishttpsurl(step.value)) return { allowed: false, reason: "A reviewed HTTPS url is required for the safety check." };
|
|
761
|
+
if (step.kind === "batchopen") {
|
|
762
|
+
const listcheck = validateurllist(options, "urls");
|
|
763
|
+
if (!listcheck.allowed) return listcheck;
|
|
764
|
+
}
|
|
494
765
|
return { allowed: true };
|
|
495
766
|
}
|
|
496
767
|
function sessiongate(input) {
|
|
@@ -508,6 +779,36 @@ function canexecute(input) {
|
|
|
508
779
|
if (input.plan.expiresat <= now) return { allowed: false, reason: "The approved plan has expired." };
|
|
509
780
|
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." };
|
|
510
781
|
if (input.step.kind === "readjson" && !origingranted(input.session, input.origin)) return { allowed: false, reason: "The json state read is outside the session origin grants." };
|
|
782
|
+
if (input.step.kind === "navlist") {
|
|
783
|
+
let options = {};
|
|
784
|
+
try {
|
|
785
|
+
options = parseoptions(input.step);
|
|
786
|
+
} catch {
|
|
787
|
+
options = {};
|
|
788
|
+
}
|
|
789
|
+
for (const url of Array.isArray(options.urls) ? options.urls : []) {
|
|
790
|
+
if (typeof url !== "string") continue;
|
|
791
|
+
const navigation = navigationgranted(input.session, url);
|
|
792
|
+
if (!navigation.allowed) return navigation;
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
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") {
|
|
796
|
+
let options = {};
|
|
797
|
+
try {
|
|
798
|
+
options = parseoptions(input.step);
|
|
799
|
+
} catch {
|
|
800
|
+
options = {};
|
|
801
|
+
}
|
|
802
|
+
const grammar = validatestep(input.step, input.origin);
|
|
803
|
+
if (!grammar.allowed) return grammar;
|
|
804
|
+
const grants = input.session?.grants ?? [input.session?.origin ?? input.origin];
|
|
805
|
+
const targets = input.step.kind === "batchopen" || input.step.kind === "prefetch" ? Array.isArray(options.urls) ? options.urls : [] : input.step.kind === "reopentab" ? [input.step.value] : [options.navtarget?.url];
|
|
806
|
+
for (const target of targets) {
|
|
807
|
+
if (typeof target !== "string" || !target) continue;
|
|
808
|
+
const verified = originverified(target, grants, input.verdicts ?? []);
|
|
809
|
+
if (!verified.allowed) return verified;
|
|
810
|
+
}
|
|
811
|
+
}
|
|
511
812
|
return validatestep(input.step, input.origin);
|
|
512
813
|
}
|
|
513
814
|
function canpreview(input) {
|
|
@@ -558,9 +859,14 @@ function recordwatchcompletion(progress, planid, stepid, startedat, lifetime, no
|
|
|
558
859
|
if (!watchclosed(startedat, lifetime, now)) return base;
|
|
559
860
|
return recordstep(base, planid, stepid, now);
|
|
560
861
|
}
|
|
862
|
+
function recordnaventry(progress, planid, stepid, entry, now) {
|
|
863
|
+
const base = progress && progress.planid === planid ? progress : emptyprogress(planid, now);
|
|
864
|
+
const outcome = { stepid, ok: entry.ok, summary: `Navigation list entry ${entry.index + 1} of ${entry.url} ${entry.ok ? "completed" : "failed"}.`, details: { naventry: entry }, at: now };
|
|
865
|
+
return recordoutcome(base, planid, outcome, now);
|
|
866
|
+
}
|
|
561
867
|
|
|
562
868
|
// version.ts
|
|
563
|
-
var packageversion = "1.1.
|
|
869
|
+
var packageversion = "1.1.35";
|
|
564
870
|
|
|
565
871
|
// types.ts
|
|
566
872
|
var protocolversion = packageversion;
|
|
@@ -640,6 +946,15 @@ function signalsreport(input) {
|
|
|
640
946
|
...signals && signals.banner !== void 0 ? { banner: signals.banner } : {}
|
|
641
947
|
};
|
|
642
948
|
}
|
|
949
|
+
function navstateresponse(input) {
|
|
950
|
+
return JSON.stringify({ version: protocolversion, planid: input.plan.id, planstate: input.plan.state, navstate: input.navstate });
|
|
951
|
+
}
|
|
952
|
+
function trailreport(input) {
|
|
953
|
+
return { version: protocolversion, ...input.sessionid ? { sessionid: input.sessionid } : {}, trail: input.trail };
|
|
954
|
+
}
|
|
955
|
+
function safetyresponse(input) {
|
|
956
|
+
return JSON.stringify({ version: protocolversion, planid: input.plan.id, planstate: input.plan.state, verdicts: input.verdicts });
|
|
957
|
+
}
|
|
643
958
|
|
|
644
959
|
// extension/browsertabs.ts
|
|
645
960
|
var browserkinds = /* @__PURE__ */ new Set(["tablist", "tabcreate", "tabactivate", "tabclose", "tabreload", "tabsnapshot", "windowlist", "windowcreate", "windowclose", "zoomset", "windowresize", "downloadfile"]);
|
|
@@ -857,12 +1172,332 @@ function heldkeys(holds, tabid2) {
|
|
|
857
1172
|
return holds.filter((hold) => hold.releasedat === void 0 && (tabid2 === void 0 || hold.tabid === void 0 || hold.tabid === tabid2));
|
|
858
1173
|
}
|
|
859
1174
|
|
|
1175
|
+
// extension/pagenav.ts
|
|
1176
|
+
function parsenavtarget(step) {
|
|
1177
|
+
let options = {};
|
|
1178
|
+
try {
|
|
1179
|
+
options = parseoptions(step);
|
|
1180
|
+
} catch {
|
|
1181
|
+
options = {};
|
|
1182
|
+
}
|
|
1183
|
+
const value = options.navtarget;
|
|
1184
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
1185
|
+
const target = value;
|
|
1186
|
+
if (typeof target.url !== "string" || !target.url) return null;
|
|
1187
|
+
const container = target.container === "current" || target.container === "window" || target.container === "private" ? target.container : "tab";
|
|
1188
|
+
return {
|
|
1189
|
+
url: target.url,
|
|
1190
|
+
container,
|
|
1191
|
+
...target.position === "end" ? { position: "end" } : { position: "adjacent" },
|
|
1192
|
+
private: container === "private" || target.private === true
|
|
1193
|
+
};
|
|
1194
|
+
}
|
|
1195
|
+
function resolvecontainer(target, windows) {
|
|
1196
|
+
if (target.container === "current") return { kind: "current", incognito: false, position: target.position ?? "adjacent" };
|
|
1197
|
+
if (target.container === "private" || target.private) return { kind: "private", incognito: true, position: target.position ?? "adjacent" };
|
|
1198
|
+
if (target.container === "window") {
|
|
1199
|
+
const focused = windows.find((item) => item.focused);
|
|
1200
|
+
return { kind: "window", incognito: false, ...focused ? { windowid: focused.id } : {}, position: target.position ?? "adjacent" };
|
|
1201
|
+
}
|
|
1202
|
+
const normal = windows.find((item) => !item.incognito && item.focused) ?? windows.find((item) => !item.incognito);
|
|
1203
|
+
return { kind: "tab", incognito: false, ...normal ? { windowid: normal.id } : {}, position: target.position ?? "adjacent" };
|
|
1204
|
+
}
|
|
1205
|
+
function parsewaitprofile(step) {
|
|
1206
|
+
let options = {};
|
|
1207
|
+
try {
|
|
1208
|
+
options = parseoptions(step);
|
|
1209
|
+
} catch {
|
|
1210
|
+
options = {};
|
|
1211
|
+
}
|
|
1212
|
+
const value = options.waitprofile;
|
|
1213
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
1214
|
+
const profile = value;
|
|
1215
|
+
const signals = Array.isArray(profile.signals) ? profile.signals.filter((item) => typeof item === "string" && item.trim().length > 0) : [];
|
|
1216
|
+
if (signals.length === 0) return null;
|
|
1217
|
+
const overrides = [];
|
|
1218
|
+
if (Array.isArray(profile.overrides)) {
|
|
1219
|
+
for (const item of profile.overrides) {
|
|
1220
|
+
if (!item || typeof item !== "object" || Array.isArray(item)) continue;
|
|
1221
|
+
const override = item;
|
|
1222
|
+
if (typeof override.origin !== "string" || !override.origin) continue;
|
|
1223
|
+
const overridesignals = Array.isArray(override.signals) ? override.signals.filter((entry) => typeof entry === "string" && entry.trim().length > 0) : void 0;
|
|
1224
|
+
overrides.push({
|
|
1225
|
+
origin: override.origin,
|
|
1226
|
+
...overridesignals && overridesignals.length > 0 ? { signals: overridesignals } : {},
|
|
1227
|
+
...typeof override.idle === "number" && Number.isFinite(override.idle) && override.idle >= 0 ? { idle: override.idle } : {},
|
|
1228
|
+
...typeof override.timeout === "number" && Number.isFinite(override.timeout) && override.timeout >= 0 ? { timeout: override.timeout } : {}
|
|
1229
|
+
});
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
return {
|
|
1233
|
+
signals,
|
|
1234
|
+
...typeof profile.idle === "number" && Number.isFinite(profile.idle) && profile.idle >= 0 ? { idle: profile.idle } : {},
|
|
1235
|
+
...typeof profile.timeout === "number" && Number.isFinite(profile.timeout) && profile.timeout >= 0 ? { timeout: profile.timeout } : {},
|
|
1236
|
+
...overrides.length > 0 ? { overrides } : {}
|
|
1237
|
+
};
|
|
1238
|
+
}
|
|
1239
|
+
function profilefororigin(profile, origin) {
|
|
1240
|
+
let signals = [...profile.signals];
|
|
1241
|
+
let idle = profile.idle ?? 0;
|
|
1242
|
+
let timeout = profile.timeout ?? 0;
|
|
1243
|
+
for (const override of profile.overrides ?? []) {
|
|
1244
|
+
if (!override.origin || new URL(override.origin).origin !== origin) continue;
|
|
1245
|
+
if (override.signals && override.signals.length > 0) signals = [...override.signals];
|
|
1246
|
+
if (override.idle !== void 0) idle = override.idle;
|
|
1247
|
+
if (override.timeout !== void 0) timeout = override.timeout;
|
|
1248
|
+
}
|
|
1249
|
+
return { signals, idle, timeout };
|
|
1250
|
+
}
|
|
1251
|
+
function deeplinkurl(app, params) {
|
|
1252
|
+
const value = (name) => {
|
|
1253
|
+
const item = params[name];
|
|
1254
|
+
return typeof item === "string" && item.trim() ? item.trim() : void 0;
|
|
1255
|
+
};
|
|
1256
|
+
switch (app.trim().toLowerCase()) {
|
|
1257
|
+
case "github": {
|
|
1258
|
+
const owner = value("owner");
|
|
1259
|
+
const repo = value("repo");
|
|
1260
|
+
if (!owner || !repo) return null;
|
|
1261
|
+
const path = value("path");
|
|
1262
|
+
return `https://github.com/${owner}/${repo}${path ? `/${path.replace(/^\/+/, "")}` : ""}`;
|
|
1263
|
+
}
|
|
1264
|
+
case "youtube": {
|
|
1265
|
+
const id = value("id");
|
|
1266
|
+
if (id) return `https://www.youtube.com/watch?v=${encodeURIComponent(id)}`;
|
|
1267
|
+
const search = value("search");
|
|
1268
|
+
if (search) return `https://www.youtube.com/results?search_query=${encodeURIComponent(search)}`;
|
|
1269
|
+
return null;
|
|
1270
|
+
}
|
|
1271
|
+
case "maps": {
|
|
1272
|
+
const query = value("query");
|
|
1273
|
+
if (!query) return null;
|
|
1274
|
+
return `https://www.google.com/maps/search/${encodeURIComponent(query)}`;
|
|
1275
|
+
}
|
|
1276
|
+
case "wikipedia": {
|
|
1277
|
+
const title = value("title");
|
|
1278
|
+
if (!title) return null;
|
|
1279
|
+
const language = value("language") ?? "en";
|
|
1280
|
+
return `https://${language}.wikipedia.org/wiki/${encodeURIComponent(title.replace(/\s+/g, "_"))}`;
|
|
1281
|
+
}
|
|
1282
|
+
case "amazon": {
|
|
1283
|
+
const search = value("search");
|
|
1284
|
+
if (!search) return null;
|
|
1285
|
+
return `https://www.amazon.com/s?k=${encodeURIComponent(search)}`;
|
|
1286
|
+
}
|
|
1287
|
+
case "x": {
|
|
1288
|
+
const user = value("user");
|
|
1289
|
+
if (!user) return null;
|
|
1290
|
+
return `https://x.com/${user.replace(/^@/, "")}`;
|
|
1291
|
+
}
|
|
1292
|
+
default:
|
|
1293
|
+
return null;
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
function pickrecenttab(recenttabs, openurls) {
|
|
1297
|
+
return recenttabs.find((tab) => !openurls.includes(tab.url)) ?? null;
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
// extension/pagenet.ts
|
|
1301
|
+
function buildredirectchain(events) {
|
|
1302
|
+
const hops = [];
|
|
1303
|
+
let startedat = 0;
|
|
1304
|
+
let endedat = 0;
|
|
1305
|
+
let open = false;
|
|
1306
|
+
for (const event of events) {
|
|
1307
|
+
if (event.event === "beforenavigate") {
|
|
1308
|
+
hops.length = 0;
|
|
1309
|
+
startedat = event.timestamp;
|
|
1310
|
+
endedat = event.timestamp;
|
|
1311
|
+
open = true;
|
|
1312
|
+
hops.push({ url: event.url, status: event.status ?? 0, at: event.timestamp });
|
|
1313
|
+
continue;
|
|
1314
|
+
}
|
|
1315
|
+
if (!open) continue;
|
|
1316
|
+
endedat = event.timestamp;
|
|
1317
|
+
if (event.event === "urlchange" || event.redirect || event.event === "committed" && hops[hops.length - 1]?.url !== event.url) {
|
|
1318
|
+
if (hops[hops.length - 1]?.url === event.url && typeof event.status === "number") hops[hops.length - 1] = { url: event.url, status: event.status, at: event.timestamp };
|
|
1319
|
+
else hops.push({ url: event.url, status: event.status ?? 0, at: event.timestamp });
|
|
1320
|
+
continue;
|
|
1321
|
+
}
|
|
1322
|
+
if (event.event === "committed" && typeof event.status === "number" && hops[hops.length - 1]) {
|
|
1323
|
+
hops[hops.length - 1] = { url: event.url, status: event.status, at: event.timestamp };
|
|
1324
|
+
continue;
|
|
1325
|
+
}
|
|
1326
|
+
if (event.event === "completed" || event.event === "error") {
|
|
1327
|
+
if (event.event === "completed" && hops[hops.length - 1] && hops[hops.length - 1]?.url !== event.url) hops.push({ url: event.url, status: event.status ?? 200, at: event.timestamp });
|
|
1328
|
+
if (event.event === "completed" && typeof event.status === "number" && hops[hops.length - 1]) hops[hops.length - 1] = { url: event.url, status: event.status, at: event.timestamp };
|
|
1329
|
+
open = false;
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
return { hops, startedat, endedat: endedat || startedat };
|
|
1333
|
+
}
|
|
1334
|
+
function finalurl(chain) {
|
|
1335
|
+
return chain.hops[chain.hops.length - 1]?.url ?? "";
|
|
1336
|
+
}
|
|
1337
|
+
function classifynavchange(previousurl, currenturl, status) {
|
|
1338
|
+
if (previousurl === currenturl) return status === "loading" ? "reload" : "none";
|
|
1339
|
+
try {
|
|
1340
|
+
if (new URL(previousurl).origin !== new URL(currenturl).origin) return "load";
|
|
1341
|
+
} catch {
|
|
1342
|
+
return "load";
|
|
1343
|
+
}
|
|
1344
|
+
return status === "loading" ? "load" : "route";
|
|
1345
|
+
}
|
|
1346
|
+
function detecthttpstate(input) {
|
|
1347
|
+
const reasons = [];
|
|
1348
|
+
let httperror = false;
|
|
1349
|
+
let certificate = false;
|
|
1350
|
+
const errors = input.errors ?? [];
|
|
1351
|
+
const statuses = input.statuses ?? [];
|
|
1352
|
+
for (const error of errors) {
|
|
1353
|
+
if (/CERT|SSL|TLS|privacy bad|your connection is not private/i.test(error)) {
|
|
1354
|
+
certificate = true;
|
|
1355
|
+
httperror = true;
|
|
1356
|
+
reasons.push(`certificate interstitial: ${error}`);
|
|
1357
|
+
continue;
|
|
1358
|
+
}
|
|
1359
|
+
if (/ERR_NAME_NOT_RESOLVED|ERR_CONNECTION|ERR_TIMED_OUT|ERR_INTERNET_DISCONNECTED|ERR_ADDRESS_UNREACHABLE|ERR_NETWORK/i.test(error)) {
|
|
1360
|
+
httperror = true;
|
|
1361
|
+
reasons.push(`network error: ${error}`);
|
|
1362
|
+
continue;
|
|
1363
|
+
}
|
|
1364
|
+
httperror = true;
|
|
1365
|
+
reasons.push(`navigation error: ${error}`);
|
|
1366
|
+
}
|
|
1367
|
+
for (const status of statuses) {
|
|
1368
|
+
if (status >= 400 && status < 600) {
|
|
1369
|
+
httperror = true;
|
|
1370
|
+
reasons.push(`http status ${status}`);
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
if (input.offline) reasons.push("browser reports offline");
|
|
1374
|
+
return { httperror, offline: input.offline, certificate, reasons };
|
|
1375
|
+
}
|
|
1376
|
+
function interstitialpolicy(state) {
|
|
1377
|
+
if (state.certificate) return { interstitial: true, bypass: false, guidance: "A certificate interstitial was detected; Devthink reports it for review and never bypasses it." };
|
|
1378
|
+
if (state.httperror) return { interstitial: true, bypass: false, guidance: "An http error state was detected and is reported for review." };
|
|
1379
|
+
return { interstitial: false, bypass: false, guidance: "No interstitial was detected." };
|
|
1380
|
+
}
|
|
1381
|
+
function parseratelimit(step) {
|
|
1382
|
+
let options = {};
|
|
1383
|
+
try {
|
|
1384
|
+
options = parseoptions(step);
|
|
1385
|
+
} catch {
|
|
1386
|
+
options = {};
|
|
1387
|
+
}
|
|
1388
|
+
const value = options.ratelimit;
|
|
1389
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
1390
|
+
const limit = value;
|
|
1391
|
+
const window2 = typeof limit.window === "number" && Number.isFinite(limit.window) && limit.window > 0 ? limit.window : 0;
|
|
1392
|
+
const ceiling = typeof limit.ceiling === "number" && Number.isInteger(limit.ceiling) && limit.ceiling >= 1 ? limit.ceiling : 0;
|
|
1393
|
+
if (window2 <= 0 || ceiling < 1) return null;
|
|
1394
|
+
const domain = typeof limit.domain === "string" && limit.domain.trim() ? limit.domain.trim() : "";
|
|
1395
|
+
return { domain, window: window2, ceiling };
|
|
1396
|
+
}
|
|
1397
|
+
function domainof(url) {
|
|
1398
|
+
try {
|
|
1399
|
+
return new URL(url).hostname;
|
|
1400
|
+
} catch {
|
|
1401
|
+
return "";
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
function ratewindow(state, limit, now) {
|
|
1405
|
+
if (state && state.domain === limit.domain && state.limit.window === limit.window && state.limit.ceiling === limit.ceiling && now < state.openedat + limit.window) return state;
|
|
1406
|
+
return { domain: limit.domain, limit, openedat: now, count: 0 };
|
|
1407
|
+
}
|
|
1408
|
+
function rateallows(state, now) {
|
|
1409
|
+
const elapsed = now - state.openedat;
|
|
1410
|
+
const remaining = Math.max(0, state.limit.ceiling - state.count);
|
|
1411
|
+
const retryafter = Math.max(0, state.limit.window - elapsed);
|
|
1412
|
+
return { allowed: remaining > 0, remaining, retryafter };
|
|
1413
|
+
}
|
|
1414
|
+
function recordratehit(state, now) {
|
|
1415
|
+
return { ...state, count: state.count + 1, ...state.count + 1 === 1 ? { openedat: now } : {} };
|
|
1416
|
+
}
|
|
1417
|
+
function checksafe(url) {
|
|
1418
|
+
const reasons = [];
|
|
1419
|
+
let safe = true;
|
|
1420
|
+
let parsed;
|
|
1421
|
+
try {
|
|
1422
|
+
parsed = new URL(url);
|
|
1423
|
+
} catch {
|
|
1424
|
+
return { url, safe: false, reasons: ["the url does not parse"], at: 0 };
|
|
1425
|
+
}
|
|
1426
|
+
if (parsed.protocol !== "https:") {
|
|
1427
|
+
safe = false;
|
|
1428
|
+
reasons.push("the url must use HTTPS");
|
|
1429
|
+
}
|
|
1430
|
+
if (parsed.username || parsed.password) {
|
|
1431
|
+
safe = false;
|
|
1432
|
+
reasons.push("the url carries embedded credentials");
|
|
1433
|
+
}
|
|
1434
|
+
const host = parsed.hostname.toLowerCase();
|
|
1435
|
+
const privatelist = ["localhost", "127.0.0.1", "0.0.0.0", "::1", "[::1]"];
|
|
1436
|
+
if (privatelist.includes(host) || /^10\./.test(host) || /^192\.168\./.test(host) || /^172\.(1[6-9]|2\d|3[01])\./.test(host) || /^169\.254\./.test(host)) {
|
|
1437
|
+
safe = false;
|
|
1438
|
+
reasons.push(`the host ${host} is a private network target`);
|
|
1439
|
+
}
|
|
1440
|
+
if (/^\d{1,3}(\.\d{1,3}){3}$/.test(host) || /^\[?[0-9a-f:]+\]?$/i.test(host)) {
|
|
1441
|
+
safe = false;
|
|
1442
|
+
reasons.push(`the host ${host} is a raw address without a domain`);
|
|
1443
|
+
}
|
|
1444
|
+
return { url, safe, reasons, at: 0 };
|
|
1445
|
+
}
|
|
1446
|
+
function curatelinks(urls, verifier) {
|
|
1447
|
+
return urls.map((url) => {
|
|
1448
|
+
const verdict = verifier(url);
|
|
1449
|
+
return { url, verdict: verdict.safe ? "safe" : "unsafe", reasons: verdict.reasons };
|
|
1450
|
+
});
|
|
1451
|
+
}
|
|
1452
|
+
function batchopenset(links) {
|
|
1453
|
+
const open = [];
|
|
1454
|
+
const refused = [];
|
|
1455
|
+
for (const link of links) {
|
|
1456
|
+
if (link.verdict === "safe") open.push(link.url);
|
|
1457
|
+
else refused.push({ url: link.url, reasons: link.reasons });
|
|
1458
|
+
}
|
|
1459
|
+
return { open, refused };
|
|
1460
|
+
}
|
|
1461
|
+
function prefetchcandidates(urls, grants) {
|
|
1462
|
+
const allowed = [];
|
|
1463
|
+
const refused = [];
|
|
1464
|
+
for (const url of urls) {
|
|
1465
|
+
let origin = "";
|
|
1466
|
+
try {
|
|
1467
|
+
origin = new URL(url).origin;
|
|
1468
|
+
} catch {
|
|
1469
|
+
refused.push(url);
|
|
1470
|
+
continue;
|
|
1471
|
+
}
|
|
1472
|
+
if (grants.includes(origin)) allowed.push(url);
|
|
1473
|
+
else refused.push(url);
|
|
1474
|
+
}
|
|
1475
|
+
return { allowed, refused };
|
|
1476
|
+
}
|
|
1477
|
+
function preconnectorigins(origins) {
|
|
1478
|
+
return [...new Set(origins.map((origin) => origin.trim()).filter(Boolean))];
|
|
1479
|
+
}
|
|
1480
|
+
function authfor(auths, url) {
|
|
1481
|
+
let origin = "";
|
|
1482
|
+
try {
|
|
1483
|
+
origin = new URL(url).origin;
|
|
1484
|
+
} catch {
|
|
1485
|
+
return void 0;
|
|
1486
|
+
}
|
|
1487
|
+
return auths.find((record2) => record2.origin === origin);
|
|
1488
|
+
}
|
|
1489
|
+
|
|
860
1490
|
// extension/background.ts
|
|
861
1491
|
var sessionduration = 15 * 60 * 1e3;
|
|
862
1492
|
var freshcheckkinds = /* @__PURE__ */ new Set(["focus", "inspect", "click", "type", "scroll", "select", "hover"]);
|
|
863
1493
|
var pointerkinds = /* @__PURE__ */ new Set(["movepointer", "clickpoint", "shiftclick", "clicktext", "clickaria", "clickname", "pierceshadow"]);
|
|
864
1494
|
var watchstepkinds = /* @__PURE__ */ new Set(["watchmutate", "watchbanner", "watchfocus"]);
|
|
865
1495
|
var observationstepkinds = /* @__PURE__ */ new Set(["a11ytree", "readvisible", "readertree", "detectlists", "detecttables", "readjson", "detectinfinitescroll", "detectvirtual", "detectlazy", "readscrollpos", "readlang", "readoutline", "countpages", "listshadow", "listframes", "classifypage", "fingerprintsection", "readselection", "detectsticky", "detectscrolllock", "readopengraph", "detectlanguage", "deriveselector"]);
|
|
1496
|
+
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"]);
|
|
1497
|
+
var pausenavkinds = /* @__PURE__ */ new Set(["openlink", "openprivate", "followlink", "spanav", "navlist", "openclipboard", "batchopen", "prefetch", "preconnect", "deeplink", "reopentab"]);
|
|
1498
|
+
var ratecheckedkinds = /* @__PURE__ */ new Set(["openlink", "openprivate", "followlink", "spanav", "navlist", "openclipboard", "batchopen", "deeplink", "reopentab"]);
|
|
1499
|
+
var evidencepoll = 100;
|
|
1500
|
+
var evidencesettle = 5e3;
|
|
866
1501
|
var chromestorage = {
|
|
867
1502
|
async get(key) {
|
|
868
1503
|
return (await chrome.storage.local.get(key))[key];
|
|
@@ -1032,6 +1667,11 @@ function stepauditkind(step, ok) {
|
|
|
1032
1667
|
if (pointerkinds.has(step.kind)) return "pointer";
|
|
1033
1668
|
if (watchstepkinds.has(step.kind)) return "watch";
|
|
1034
1669
|
if (step.kind === "diffsnapshots") return "diff";
|
|
1670
|
+
if (step.kind === "readredirects" || step.kind === "readfinalurl" || step.kind === "detecthttp") return "redirect";
|
|
1671
|
+
if (step.kind === "handleauth") return "auth";
|
|
1672
|
+
if (step.kind === "prefetch" || step.kind === "preconnect") return "prefetch";
|
|
1673
|
+
if (step.kind === "navrate") return "rate";
|
|
1674
|
+
if (navigationstepkinds.has(step.kind)) return "navigation";
|
|
1035
1675
|
if (observationstepkinds.has(step.kind)) return "observation";
|
|
1036
1676
|
return ok ? "action" : "error";
|
|
1037
1677
|
}
|
|
@@ -1254,22 +1894,485 @@ async function recordevidence(step, output, session, plan, origin) {
|
|
|
1254
1894
|
}
|
|
1255
1895
|
await refreshsignals(step, output);
|
|
1256
1896
|
}
|
|
1897
|
+
var navbuffers = /* @__PURE__ */ new Map();
|
|
1898
|
+
var lastknownurls = /* @__PURE__ */ new Map();
|
|
1899
|
+
async function tracktabupdate(tabid2, changeinfo) {
|
|
1900
|
+
const now = Date.now();
|
|
1901
|
+
const url = typeof changeinfo.url === "string" ? changeinfo.url : void 0;
|
|
1902
|
+
const status = changeinfo.status;
|
|
1903
|
+
const previous = lastknownurls.get(tabid2);
|
|
1904
|
+
if (status === "loading" && url) {
|
|
1905
|
+
navbuffers.set(tabid2, [{ event: "beforenavigate", url, timestamp: now }]);
|
|
1906
|
+
lastknownurls.set(tabid2, url);
|
|
1907
|
+
return;
|
|
1908
|
+
}
|
|
1909
|
+
const buffer = navbuffers.get(tabid2) ?? [];
|
|
1910
|
+
if (url) {
|
|
1911
|
+
const kind = classifynavchange(previous ?? url, url, status);
|
|
1912
|
+
buffer.push({ event: kind === "route" ? "urlchange" : "committed", url, timestamp: now, redirect: kind === "route" });
|
|
1913
|
+
if (kind === "route" && previous) {
|
|
1914
|
+
const session = await memory.getsession();
|
|
1915
|
+
if (session && !session.stoppedat && session.tabid === tabid2) {
|
|
1916
|
+
await memory.addtrailentry(session.id, { url, title: "", at: now });
|
|
1917
|
+
}
|
|
1918
|
+
}
|
|
1919
|
+
lastknownurls.set(tabid2, url);
|
|
1920
|
+
}
|
|
1921
|
+
if (status === "complete") buffer.push({ event: "completed", url: lastknownurls.get(tabid2) ?? "", timestamp: now, status: 200 });
|
|
1922
|
+
navbuffers.set(tabid2, buffer);
|
|
1923
|
+
}
|
|
1924
|
+
chrome.tabs.onUpdated.addListener((tabid2, changeinfo) => {
|
|
1925
|
+
void tracktabupdate(tabid2, changeinfo);
|
|
1926
|
+
});
|
|
1927
|
+
chrome.tabs.onRemoved.addListener((tabid2) => {
|
|
1928
|
+
const url = lastknownurls.get(tabid2);
|
|
1929
|
+
if (url) void memory.addrecenttab({ url, tabid: tabid2, closedat: Date.now() });
|
|
1930
|
+
lastknownurls.delete(tabid2);
|
|
1931
|
+
navbuffers.delete(tabid2);
|
|
1932
|
+
});
|
|
1933
|
+
async function recordnavigation(step, session, tabid2) {
|
|
1934
|
+
const started = Date.now();
|
|
1935
|
+
let tab = await chrome.tabs.get(tabid2).catch(() => void 0);
|
|
1936
|
+
while (tab && tab.status !== "complete" && Date.now() - started < evidencesettle) {
|
|
1937
|
+
await new Promise((resolve) => setTimeout(resolve, evidencepoll));
|
|
1938
|
+
tab = await chrome.tabs.get(tabid2).catch(() => void 0);
|
|
1939
|
+
}
|
|
1940
|
+
const url = tab?.url ?? lastknownurls.get(tabid2) ?? "";
|
|
1941
|
+
const title = tab?.title ?? "";
|
|
1942
|
+
const chain = buildredirectchain(navbuffers.get(tabid2) ?? []);
|
|
1943
|
+
const record2 = {
|
|
1944
|
+
stepid: step.id,
|
|
1945
|
+
...session ? { sessionid: session.id } : {},
|
|
1946
|
+
origin: session?.origin ?? (url ? new URL(url).origin : ""),
|
|
1947
|
+
finalurl: finalurl(chain) || url,
|
|
1948
|
+
chain,
|
|
1949
|
+
at: Date.now()
|
|
1950
|
+
};
|
|
1951
|
+
await memory.addnavrecord(record2);
|
|
1952
|
+
await memory.setnavstate(tabid2, record2);
|
|
1953
|
+
if (session && url) await memory.addtrailentry(session.id, { url, title, stepid: step.id, at: Date.now() });
|
|
1954
|
+
return record2;
|
|
1955
|
+
}
|
|
1956
|
+
function injectallowedorigins(step, session) {
|
|
1957
|
+
const allowedorigins = session?.grants ?? (session ? [session.origin] : []);
|
|
1958
|
+
let options = {};
|
|
1959
|
+
try {
|
|
1960
|
+
options = parseoptions(step);
|
|
1961
|
+
} catch {
|
|
1962
|
+
options = {};
|
|
1963
|
+
}
|
|
1964
|
+
return { ...step, options: JSON.stringify({ ...options, allowedorigins }) };
|
|
1965
|
+
}
|
|
1966
|
+
async function enforceratelimit(url, stepid, sessionid) {
|
|
1967
|
+
const domain = domainof(url);
|
|
1968
|
+
if (!domain) return;
|
|
1969
|
+
const states = await memory.getratestates();
|
|
1970
|
+
const stored = states.find((item) => item.domain === domain);
|
|
1971
|
+
if (!stored) return;
|
|
1972
|
+
const live = ratewindow(stored, stored.limit, Date.now());
|
|
1973
|
+
const decision = rateallows(live, Date.now());
|
|
1974
|
+
if (!decision.allowed) {
|
|
1975
|
+
await audit("rate", `The navigation rate limit of ${live.limit.ceiling} per ${live.limit.window} milliseconds for ${domain} was exceeded; the navigation was refused.`, { ...sessionid ? { sessionid } : {}, stepid });
|
|
1976
|
+
throw new Error(`The navigation rate limit for ${domain} has been reached; retry after ${Math.ceil(decision.retryafter / 1e3)} seconds.`);
|
|
1977
|
+
}
|
|
1978
|
+
await memory.setratestate(recordratehit(live, Date.now()));
|
|
1979
|
+
await audit("rate", `Navigation to ${url} counted against the reviewed rate limit of ${live.limit.ceiling} per ${live.limit.window} milliseconds for ${domain}; ${decision.remaining - 1} remaining.`, { ...sessionid ? { sessionid } : {}, stepid });
|
|
1980
|
+
}
|
|
1981
|
+
async function refusenavpause(step) {
|
|
1982
|
+
if (!pausenavkinds.has(step.kind)) return;
|
|
1983
|
+
const control = await memory.getnavcontrol();
|
|
1984
|
+
if (control?.pausedat) throw new Error(control.reason ? `Navigation is paused: ${control.reason}` : "Navigation is paused while a consent prompt is open; resume navigation first.");
|
|
1985
|
+
}
|
|
1986
|
+
async function opencontainer(step, session, url, container, position) {
|
|
1987
|
+
const windows = await chrome.windows.getAll().catch(() => []);
|
|
1988
|
+
const plan = resolvecontainer({ url, container, position, private: container === "private" }, windows.map((item) => ({ id: item.id ?? 0, incognito: item.incognito ?? false, focused: item.focused ?? false })));
|
|
1989
|
+
const extra = { ...session ? { sessionid: session.id } : {}, stepid: step.id };
|
|
1990
|
+
if (plan.kind === "current" && session) {
|
|
1991
|
+
const tabid2 = session.tabid;
|
|
1992
|
+
await chrome.tabs.update(tabid2, { url });
|
|
1993
|
+
const record2 = await recordnavigation(step, session, tabid2);
|
|
1994
|
+
await audit("navigation", `Navigated the task tab to ${url}.`, extra);
|
|
1995
|
+
return { ok: true, summary: `Navigated the task tab to ${url}.`, details: { container: plan.kind, url, finalurl: record2.finalurl, hops: record2.chain.hops.length } };
|
|
1996
|
+
}
|
|
1997
|
+
if (plan.kind === "private" || plan.kind === "window") {
|
|
1998
|
+
const created2 = await chrome.windows.create({ url, incognito: plan.incognito });
|
|
1999
|
+
await audit("navigation", `Opened ${plan.kind === "private" ? "a private window" : "a new window"} for ${url}${plan.kind === "private" ? " separated from normal windows" : ""}.`, extra);
|
|
2000
|
+
return { ok: true, summary: `Opened ${plan.kind === "private" ? "a private window" : "a new window"} for ${url}.`, details: { container: plan.kind, url, windowid: created2?.id ?? 0, incognito: plan.incognito } };
|
|
2001
|
+
}
|
|
2002
|
+
const created = await chrome.tabs.create({ url, ...plan.windowid !== void 0 ? { windowId: plan.windowid } : {}, active: true });
|
|
2003
|
+
await audit("navigation", `Opened a new tab for ${url} without leaving the current page.`, extra);
|
|
2004
|
+
return { ok: true, summary: `Opened a new tab for ${url}.`, details: { container: "tab", url, tabid: created?.id ?? 0, position: plan.position } };
|
|
2005
|
+
}
|
|
2006
|
+
async function verifyopenurl(url, session) {
|
|
2007
|
+
const grants = session?.grants ?? (session ? [session.origin] : []);
|
|
2008
|
+
const verified = originverified(url, grants, await memory.getsafeties());
|
|
2009
|
+
if (!verified.allowed) throw new Error(verified.reason);
|
|
2010
|
+
}
|
|
2011
|
+
async function executeopenlink(step, session) {
|
|
2012
|
+
const target = parsenavtarget(step);
|
|
2013
|
+
if (!target) throw new Error("A reviewed navtarget is required.");
|
|
2014
|
+
await refusenavpause(step);
|
|
2015
|
+
await enforceratelimit(target.url, step.id, session?.id);
|
|
2016
|
+
await verifyopenurl(target.url, session);
|
|
2017
|
+
return opencontainer(step, session, target.url, target.container, target.position ?? "adjacent");
|
|
2018
|
+
}
|
|
2019
|
+
async function executedeeplink(step, session) {
|
|
2020
|
+
let options = {};
|
|
2021
|
+
try {
|
|
2022
|
+
options = parseoptions(step);
|
|
2023
|
+
} catch {
|
|
2024
|
+
options = {};
|
|
2025
|
+
}
|
|
2026
|
+
const params = {};
|
|
2027
|
+
if (options.params && typeof options.params === "object" && !Array.isArray(options.params)) {
|
|
2028
|
+
for (const [name, value] of Object.entries(options.params)) if (typeof value === "string") params[name] = value;
|
|
2029
|
+
}
|
|
2030
|
+
const url = deeplinkurl(typeof options.app === "string" ? options.app : "", params);
|
|
2031
|
+
if (!url) throw new Error("The reviewed deep link pattern is not a known web app.");
|
|
2032
|
+
await refusenavpause(step);
|
|
2033
|
+
await enforceratelimit(url, step.id, session?.id);
|
|
2034
|
+
await verifyopenurl(url, session);
|
|
2035
|
+
const target = parsenavtarget(step);
|
|
2036
|
+
const container = target?.container === "window" ? "window" : target?.container === "private" ? "private" : target?.container === "current" ? "current" : "tab";
|
|
2037
|
+
const output = await opencontainer(step, session, url, container, target?.position ?? "adjacent");
|
|
2038
|
+
return { ...output, details: { ...output.details ?? {}, app: options.app, deeplink: url } };
|
|
2039
|
+
}
|
|
2040
|
+
async function executereopentab(step, session) {
|
|
2041
|
+
await refusenavpause(step);
|
|
2042
|
+
let url = step.value;
|
|
2043
|
+
if (!url) {
|
|
2044
|
+
const tabs = await chrome.tabs.query({}).catch(() => []);
|
|
2045
|
+
const openurls = tabs.map((tab) => tab.url ?? "").filter(Boolean);
|
|
2046
|
+
const recent = pickrecenttab(await memory.getrecenttabs(), openurls);
|
|
2047
|
+
if (!recent) throw new Error("No recently closed tab is available to reopen.");
|
|
2048
|
+
url = recent.url;
|
|
2049
|
+
}
|
|
2050
|
+
await enforceratelimit(url, step.id, session?.id);
|
|
2051
|
+
await verifyopenurl(url, session);
|
|
2052
|
+
const created = await chrome.tabs.create({ url, active: true });
|
|
2053
|
+
await audit("navigation", `Reopened the recently closed tab ${url}.`, { ...session ? { sessionid: session.id } : {}, stepid: step.id });
|
|
2054
|
+
return { ok: true, summary: `Reopened ${url} in a new tab.`, details: { url, tabid: created?.id ?? 0 } };
|
|
2055
|
+
}
|
|
2056
|
+
async function waitforcomplete(tabid2) {
|
|
2057
|
+
const started = Date.now();
|
|
2058
|
+
for (; ; ) {
|
|
2059
|
+
const tab = await chrome.tabs.get(tabid2).catch(() => void 0);
|
|
2060
|
+
if (!tab) return false;
|
|
2061
|
+
if (tab.status === "complete") return true;
|
|
2062
|
+
if (Date.now() - started >= evidencesettle) return false;
|
|
2063
|
+
await new Promise((resolve) => setTimeout(resolve, evidencepoll));
|
|
2064
|
+
}
|
|
2065
|
+
}
|
|
2066
|
+
async function recordnaventryoutcome(step, plan, entry) {
|
|
2067
|
+
const base = await memory.getprogress();
|
|
2068
|
+
await memory.setprogress(recordnaventry(base, plan.id, step.id, entry, Date.now()));
|
|
2069
|
+
}
|
|
2070
|
+
async function executenavlist(step, session, plan, tabid2) {
|
|
2071
|
+
let options = {};
|
|
2072
|
+
try {
|
|
2073
|
+
options = parseoptions(step);
|
|
2074
|
+
} catch {
|
|
2075
|
+
options = {};
|
|
2076
|
+
}
|
|
2077
|
+
const urls = Array.isArray(options.urls) ? options.urls.filter((item) => typeof item === "string" && item.trim().length > 0) : [];
|
|
2078
|
+
if (urls.length === 0) throw new Error("A reviewed list of navigation urls is required.");
|
|
2079
|
+
await refusenavpause(step);
|
|
2080
|
+
let completed = 0;
|
|
2081
|
+
let failed = "";
|
|
2082
|
+
for (let index = 0; index < urls.length; index += 1) {
|
|
2083
|
+
const url = urls[index];
|
|
2084
|
+
const entrygate = navigationgranted(session, url);
|
|
2085
|
+
if (!entrygate.allowed) {
|
|
2086
|
+
failed = entrygate.reason ?? "The navigation list entry was refused.";
|
|
2087
|
+
await recordnaventryoutcome(step, plan, { index, url, ok: false });
|
|
2088
|
+
break;
|
|
2089
|
+
}
|
|
2090
|
+
try {
|
|
2091
|
+
await enforceratelimit(url, step.id, session?.id);
|
|
2092
|
+
await refusenavpause(step);
|
|
2093
|
+
await chrome.tabs.update(tabid2, { url });
|
|
2094
|
+
await waitforcomplete(tabid2);
|
|
2095
|
+
await recordnavigation(step, session, tabid2);
|
|
2096
|
+
completed += 1;
|
|
2097
|
+
await recordnaventryoutcome(step, plan, { index, url, ok: true });
|
|
2098
|
+
} catch (error) {
|
|
2099
|
+
failed = error instanceof Error ? error.message : String(error);
|
|
2100
|
+
await recordnaventryoutcome(step, plan, { index, url, ok: false });
|
|
2101
|
+
break;
|
|
2102
|
+
}
|
|
2103
|
+
}
|
|
2104
|
+
const remaining = urls.length - completed;
|
|
2105
|
+
const ok = failed === "" && completed === urls.length;
|
|
2106
|
+
return {
|
|
2107
|
+
ok,
|
|
2108
|
+
summary: ok ? `Navigated the reviewed list of ${urls.length} url${urls.length === 1 ? "" : "s"} sequentially.` : `The navigation list stopped after ${completed} of ${urls.length} entries: ${failed}`,
|
|
2109
|
+
details: { completed, remaining, urls }
|
|
2110
|
+
};
|
|
2111
|
+
}
|
|
2112
|
+
async function executenavprofile(step, session, origin) {
|
|
2113
|
+
const profile = parsewaitprofile(step);
|
|
2114
|
+
if (!profile) throw new Error("A reviewed waitprofile is required.");
|
|
2115
|
+
const record2 = { origin, profile, at: Date.now() };
|
|
2116
|
+
await memory.setwaitprofile(record2);
|
|
2117
|
+
const effective = profilefororigin(profile, origin);
|
|
2118
|
+
await audit("navigation", `Wait profile applied for ${origin} with signals ${effective.signals.join(", ")}${effective.idle > 0 ? `, idle ${effective.idle}ms` : ""}${effective.timeout > 0 ? ` and timeout ${effective.timeout}ms` : ""}.`, { ...session ? { sessionid: session.id } : {}, stepid: step.id });
|
|
2119
|
+
return { ok: true, summary: `Applied the reviewed wait profile for ${origin} during navigation.`, details: { origin, signals: effective.signals, idle: effective.idle, timeout: effective.timeout, overrides: (profile.overrides ?? []).map((override) => override.origin) } };
|
|
2120
|
+
}
|
|
2121
|
+
async function executedetecthttp(session, tabid2) {
|
|
2122
|
+
const records = await memory.getnavrecords();
|
|
2123
|
+
const buffer = navbuffers.get(tabid2) ?? [];
|
|
2124
|
+
const errors = buffer.filter((event) => event.error !== void 0).map((event) => event.error);
|
|
2125
|
+
const statuses = (records[0]?.chain.hops ?? []).map((hop) => hop.status).filter((status) => status > 0);
|
|
2126
|
+
const state = detecthttpstate({ offline: !navigator.onLine, errors, statuses });
|
|
2127
|
+
const policy = interstitialpolicy(state);
|
|
2128
|
+
return {
|
|
2129
|
+
ok: true,
|
|
2130
|
+
summary: state.httperror || state.offline ? `Detected ${state.reasons.length} navigation problem${state.reasons.length === 1 ? "" : "s"}: ${state.reasons.join("; ")}.` : "No http error, offline state or certificate interstitial was detected.",
|
|
2131
|
+
details: { httperror: state.httperror, offline: state.offline, certificate: state.certificate, reasons: state.reasons, interstitial: policy.interstitial, bypass: policy.bypass, guidance: policy.guidance }
|
|
2132
|
+
};
|
|
2133
|
+
}
|
|
2134
|
+
async function executereadredirects(session) {
|
|
2135
|
+
const record2 = (await memory.getnavrecords()).find((item) => !session || item.sessionid === session.id);
|
|
2136
|
+
if (!record2) return { ok: false, summary: "No navigation has been recorded yet.", details: { chain: { hops: [], startedat: 0, endedat: 0 } } };
|
|
2137
|
+
return { ok: true, summary: `The latest navigation travelled ${Math.max(0, record2.chain.hops.length - 1)} redirect${record2.chain.hops.length - 1 === 1 ? "" : "s"} to ${record2.finalurl}.`, details: { chain: record2.chain, finalurl: record2.finalurl, duration: record2.chain.endedat - record2.chain.startedat } };
|
|
2138
|
+
}
|
|
2139
|
+
async function executereadfinalurl(session) {
|
|
2140
|
+
const record2 = (await memory.getnavrecords()).find((item) => !session || item.sessionid === session.id);
|
|
2141
|
+
if (!record2) return { ok: false, summary: "No navigation has been recorded yet.", details: { finalurl: "" } };
|
|
2142
|
+
return { ok: true, summary: `The final url after redirects is ${record2.finalurl}.`, details: { finalurl: record2.finalurl, hops: record2.chain.hops } };
|
|
2143
|
+
}
|
|
2144
|
+
async function executehandleauth(step, session) {
|
|
2145
|
+
const auths = await memory.getauths();
|
|
2146
|
+
const record2 = authfor(auths, step.value ?? "");
|
|
2147
|
+
if (!record2) throw new Error(`No reviewed basic auth credentials are stored for ${step.value ?? ""}; store them from the side panel first.`);
|
|
2148
|
+
await audit("auth", `Basic auth credentials for ${record2.origin} reviewed as ${record2.username} were armed for the auth prompt; the password stays out of every step detail.`, { ...session ? { sessionid: session.id } : {}, stepid: step.id });
|
|
2149
|
+
return { ok: true, summary: `Armed the reviewed basic auth credentials of ${record2.username} for ${record2.origin}.`, details: { origin: record2.origin, username: record2.username, reviewedat: record2.reviewedat, armed: true } };
|
|
2150
|
+
}
|
|
2151
|
+
async function executeprintpdf(step, session, plan, tabid2, origin) {
|
|
2152
|
+
let options = {};
|
|
2153
|
+
try {
|
|
2154
|
+
options = parseoptions(step);
|
|
2155
|
+
} catch {
|
|
2156
|
+
options = {};
|
|
2157
|
+
}
|
|
2158
|
+
const output = await dispatchpagestep(step, tabid2, origin, plan);
|
|
2159
|
+
const name = typeof options.name === "string" && options.name ? options.name : `${step.id}.pdf`;
|
|
2160
|
+
const artifact = { id: randomid(), kind: "printpdf", name, stepid: step.id, at: Date.now() };
|
|
2161
|
+
await memory.addartifact(artifact);
|
|
2162
|
+
await audit("navigation", `Printed the page to pdf through the browser print pipeline and routed the artifact ${name} into the task artifact store.`, { ...session ? { sessionid: session.id } : {}, stepid: step.id });
|
|
2163
|
+
return { ok: output?.ok ?? false, summary: output?.summary ?? "The print pipeline returned no result.", details: { ...output?.details ?? {}, artifact } };
|
|
2164
|
+
}
|
|
2165
|
+
async function executeprefetch(step, session, plan, tabid2, origin) {
|
|
2166
|
+
let options = {};
|
|
2167
|
+
try {
|
|
2168
|
+
options = parseoptions(step);
|
|
2169
|
+
} catch {
|
|
2170
|
+
options = {};
|
|
2171
|
+
}
|
|
2172
|
+
const urls = Array.isArray(options.urls) ? options.urls.filter((item) => typeof item === "string" && item.trim().length > 0) : [];
|
|
2173
|
+
const grants = session?.grants ?? (session ? [session.origin] : []);
|
|
2174
|
+
const verdict = prefetchcandidates(urls, grants);
|
|
2175
|
+
if (verdict.allowed.length === 0) throw new Error("No prefetch candidate is covered by the session grants.");
|
|
2176
|
+
await refusenavpause(step);
|
|
2177
|
+
const derived = { ...step, options: JSON.stringify({ ...options, urls: verdict.allowed }) };
|
|
2178
|
+
const output = await dispatchpagestep(derived, tabid2, origin, plan);
|
|
2179
|
+
await memory.setnavqueues({ prefetch: verdict.allowed.length, batchopen: (await memory.getnavqueues())?.batchopen ?? 0, updatedat: Date.now() });
|
|
2180
|
+
await refreshbadge();
|
|
2181
|
+
await audit("prefetch", `Prefetched ${verdict.allowed.length} predicted next page${verdict.allowed.length === 1 ? "" : "s"} verified against the session grants${verdict.refused.length > 0 ? ` and refused ${verdict.refused.length} candidate${verdict.refused.length === 1 ? "" : "s"} outside the grants` : ""}.`, { ...session ? { sessionid: session.id } : {}, stepid: step.id });
|
|
2182
|
+
return { ok: Boolean(output?.ok), summary: output?.summary ?? "The prefetch step returned no result.", details: { ...output?.details ?? {}, allowed: verdict.allowed, refused: verdict.refused } };
|
|
2183
|
+
}
|
|
2184
|
+
async function executepreconnect(step, session, plan, tabid2, origin) {
|
|
2185
|
+
let options = {};
|
|
2186
|
+
try {
|
|
2187
|
+
options = parseoptions(step);
|
|
2188
|
+
} catch {
|
|
2189
|
+
options = {};
|
|
2190
|
+
}
|
|
2191
|
+
const origins = preconnectorigins(Array.isArray(options.origins) ? options.origins.filter((item) => typeof item === "string") : []);
|
|
2192
|
+
await refusenavpause(step);
|
|
2193
|
+
const output = await dispatchpagestep({ ...step, options: JSON.stringify({ ...options, origins }) }, tabid2, origin, plan);
|
|
2194
|
+
await audit("prefetch", `Preconnected to ${origins.length} expected origin${origins.length === 1 ? "" : "s"}: ${origins.join(", ")}.`, { ...session ? { sessionid: session.id } : {}, stepid: step.id });
|
|
2195
|
+
return { ok: Boolean(output?.ok), summary: output?.summary ?? "The preconnect step returned no result.", details: { ...output?.details ?? {}, origins } };
|
|
2196
|
+
}
|
|
2197
|
+
async function executeopenclipboard(step, session) {
|
|
2198
|
+
await refusenavpause(step);
|
|
2199
|
+
const text2 = await navigator.clipboard.readText();
|
|
2200
|
+
let url = "";
|
|
2201
|
+
try {
|
|
2202
|
+
url = new URL(text2.trim()).toString();
|
|
2203
|
+
} catch {
|
|
2204
|
+
throw new Error("The clipboard does not hold a valid url.");
|
|
2205
|
+
}
|
|
2206
|
+
if (!url.startsWith("https://")) throw new Error("The clipboard url must use HTTPS.");
|
|
2207
|
+
await enforceratelimit(url, step.id, session?.id);
|
|
2208
|
+
await verifyopenurl(url, session);
|
|
2209
|
+
const output = await opencontainer(step, session, url, "tab", "adjacent");
|
|
2210
|
+
await audit("navigation", `Opened the clipboard url ${url} on the explicit consent of the reviewed step.`, { ...session ? { sessionid: session.id } : {}, stepid: step.id });
|
|
2211
|
+
return { ...output, details: { ...output.details ?? {}, url } };
|
|
2212
|
+
}
|
|
2213
|
+
async function executechecksafe(step, session, planid) {
|
|
2214
|
+
const verdict = { ...checksafe(step.value ?? ""), at: Date.now() };
|
|
2215
|
+
await memory.addsafety(verdict);
|
|
2216
|
+
await audit("navigation", `Safety check of ${verdict.url} returned ${verdict.safe ? "safe" : "unsafe"}${verdict.reasons.length > 0 ? `: ${verdict.reasons.join("; ")}` : ""}.`, { ...session ? { sessionid: session.id } : {}, ...planid ? { planid } : {}, stepid: step.id });
|
|
2217
|
+
return { ok: true, summary: verdict.safe ? `The url ${verdict.url} passed every safety check.` : `The url ${verdict.url} is unsafe: ${verdict.reasons.join("; ")}.`, details: { verdict } };
|
|
2218
|
+
}
|
|
2219
|
+
async function executebatchopen(step, session) {
|
|
2220
|
+
let options = {};
|
|
2221
|
+
try {
|
|
2222
|
+
options = parseoptions(step);
|
|
2223
|
+
} catch {
|
|
2224
|
+
options = {};
|
|
2225
|
+
}
|
|
2226
|
+
const urls = Array.isArray(options.urls) ? options.urls.filter((item) => typeof item === "string" && item.trim().length > 0) : [];
|
|
2227
|
+
const links = curatelinks(urls, (url) => ({ ...checksafe(url), at: Date.now() }));
|
|
2228
|
+
const curated = { id: randomid(), links, at: Date.now() };
|
|
2229
|
+
await memory.addcurated(curated);
|
|
2230
|
+
const set = batchopenset(links);
|
|
2231
|
+
if (set.refused.length > 0) {
|
|
2232
|
+
await memory.setnavqueues({ prefetch: (await memory.getnavqueues())?.prefetch ?? 0, batchopen: set.refused.length, updatedat: Date.now() });
|
|
2233
|
+
await refreshbadge();
|
|
2234
|
+
await audit("navigation", `Batch open refused for ${set.refused.length} unsafe url${set.refused.length === 1 ? "" : "s"}; the curated list waits for review.`, { ...session ? { sessionid: session.id } : {}, stepid: step.id });
|
|
2235
|
+
return { ok: false, summary: `The batch was refused because ${set.refused.length} of ${links.length} urls are unsafe; review the curated list before opening.`, details: { curated, refused: set.refused } };
|
|
2236
|
+
}
|
|
2237
|
+
await refusenavpause(step);
|
|
2238
|
+
const opened = [];
|
|
2239
|
+
for (const url of set.open) {
|
|
2240
|
+
await enforceratelimit(url, step.id, session?.id);
|
|
2241
|
+
const created = await chrome.tabs.create({ url, active: opened.length === 0 });
|
|
2242
|
+
opened.push(created?.id ?? 0);
|
|
2243
|
+
if (session) await memory.addtrailentry(session.id, { url, title: "", stepid: step.id, at: Date.now() });
|
|
2244
|
+
}
|
|
2245
|
+
await memory.addcurated({ ...curated, reviewedat: Date.now() });
|
|
2246
|
+
await memory.setnavqueues({ prefetch: (await memory.getnavqueues())?.prefetch ?? 0, batchopen: 0, updatedat: Date.now() });
|
|
2247
|
+
await refreshbadge();
|
|
2248
|
+
await audit("navigation", `Batch opened ${set.open.length} curated url${set.open.length === 1 ? "" : "s"} after per url safety checks.`, { ...session ? { sessionid: session.id } : {}, stepid: step.id });
|
|
2249
|
+
return { ok: true, summary: `Opened ${set.open.length} curated url${set.open.length === 1 ? "" : "s"} after per url safety checks.`, details: { curated, tabids: opened } };
|
|
2250
|
+
}
|
|
2251
|
+
async function executepausenav(step, session) {
|
|
2252
|
+
const control = await memory.getnavcontrol();
|
|
2253
|
+
let options = {};
|
|
2254
|
+
try {
|
|
2255
|
+
options = parseoptions(step);
|
|
2256
|
+
} catch {
|
|
2257
|
+
options = {};
|
|
2258
|
+
}
|
|
2259
|
+
if (control?.pausedat) {
|
|
2260
|
+
await memory.setnavcontrol({ updatedat: Date.now() });
|
|
2261
|
+
await audit("resume", `Navigation resumed after ${Date.now() - control.pausedat} milliseconds of pause.`, { ...session ? { sessionid: session.id } : {}, stepid: step.id });
|
|
2262
|
+
return { ok: true, summary: "Navigation resumed; reviewed navigation steps can run again.", details: { paused: false } };
|
|
2263
|
+
}
|
|
2264
|
+
const reason = typeof options.reason === "string" && options.reason ? options.reason : "a consent prompt is open";
|
|
2265
|
+
await memory.setnavcontrol({ pausedat: Date.now(), reason, updatedat: Date.now() });
|
|
2266
|
+
await audit("pause", `Navigation paused while ${reason}.`, { ...session ? { sessionid: session.id } : {}, stepid: step.id });
|
|
2267
|
+
return { ok: true, summary: `Navigation paused while ${reason}.`, details: { paused: true, reason } };
|
|
2268
|
+
}
|
|
2269
|
+
async function executenavintent(step, session, origin) {
|
|
2270
|
+
const record2 = { id: randomid(), intent: step.value ?? "", origin, ...session ? { sessionid: session.id } : {}, stepid: step.id, at: Date.now() };
|
|
2271
|
+
await memory.addnavintent(record2);
|
|
2272
|
+
await audit("navigation", `Navigation intent "${record2.intent}" recorded for ${origin}.`, { ...session ? { sessionid: session.id } : {}, stepid: step.id });
|
|
2273
|
+
return { ok: true, summary: `Recorded the navigation intent "${record2.intent}".`, details: { intent: record2.intent, origin, at: record2.at } };
|
|
2274
|
+
}
|
|
2275
|
+
async function executenavrate(step, session, origin) {
|
|
2276
|
+
const limit = parseratelimit(step);
|
|
2277
|
+
if (!limit) throw new Error("A reviewed ratelimit is required.");
|
|
2278
|
+
if (!limit.domain) limit.domain = domainof(origin) || origin;
|
|
2279
|
+
const states = await memory.getratestates();
|
|
2280
|
+
const stored = states.find((item) => item.domain === limit.domain);
|
|
2281
|
+
const live = ratewindow(stored, limit, Date.now());
|
|
2282
|
+
const decision = rateallows(live, Date.now());
|
|
2283
|
+
await memory.setratestate(live);
|
|
2284
|
+
await audit("rate", `Navigation rate limit of ${limit.ceiling} per ${limit.window} milliseconds applied for ${limit.domain}; ${decision.remaining} navigation${decision.remaining === 1 ? "" : "s"} remaining in the window.`, { ...session ? { sessionid: session.id } : {}, stepid: step.id });
|
|
2285
|
+
return { ok: true, summary: `Applied the reviewed navigation rate limit for ${limit.domain}.`, details: { domain: limit.domain, window: limit.window, ceiling: limit.ceiling, count: live.count, remaining: decision.remaining, retryafter: decision.retryafter } };
|
|
2286
|
+
}
|
|
2287
|
+
async function executetrailaudit(session) {
|
|
2288
|
+
if (!session) throw new Error("No active browser session exists.");
|
|
2289
|
+
const trail = await memory.gettrail(session.id);
|
|
2290
|
+
return { ok: true, summary: `The navigation trail of the session holds ${trail.length} visited url${trail.length === 1 ? "" : "s"}.`, details: { trail } };
|
|
2291
|
+
}
|
|
2292
|
+
async function executenavigationkind(step, session, plan, tabid2, origin) {
|
|
2293
|
+
switch (step.kind) {
|
|
2294
|
+
case "openlink":
|
|
2295
|
+
return executeopenlink(step, session);
|
|
2296
|
+
case "openprivate":
|
|
2297
|
+
return executeopenlink(step, session);
|
|
2298
|
+
case "deeplink":
|
|
2299
|
+
return executedeeplink(step, session);
|
|
2300
|
+
case "reopentab":
|
|
2301
|
+
return executereopentab(step, session);
|
|
2302
|
+
case "reloadcache": {
|
|
2303
|
+
await refusenavpause(step);
|
|
2304
|
+
await chrome.tabs.reload(tabid2, { bypassCache: true });
|
|
2305
|
+
const record2 = await recordnavigation(step, session, tabid2);
|
|
2306
|
+
await audit("navigation", `Reloaded the page bypassing the cache.`, { ...session ? { sessionid: session.id } : {}, stepid: step.id });
|
|
2307
|
+
return { ok: true, summary: "Reloaded the page bypassing the cache.", details: { bypasscache: true, finalurl: record2.finalurl } };
|
|
2308
|
+
}
|
|
2309
|
+
case "navlist":
|
|
2310
|
+
return executenavlist(step, session, plan, tabid2);
|
|
2311
|
+
case "navprofile":
|
|
2312
|
+
return executenavprofile(step, session, origin);
|
|
2313
|
+
case "detecthttp":
|
|
2314
|
+
return executedetecthttp(session, tabid2);
|
|
2315
|
+
case "readredirects":
|
|
2316
|
+
return executereadredirects(session);
|
|
2317
|
+
case "readfinalurl":
|
|
2318
|
+
return executereadfinalurl(session);
|
|
2319
|
+
case "handleauth":
|
|
2320
|
+
return executehandleauth(step, session);
|
|
2321
|
+
case "printpdf":
|
|
2322
|
+
return executeprintpdf(step, session, plan, tabid2, origin);
|
|
2323
|
+
case "prefetch":
|
|
2324
|
+
return executeprefetch(step, session, plan, tabid2, origin);
|
|
2325
|
+
case "preconnect":
|
|
2326
|
+
return executepreconnect(step, session, plan, tabid2, origin);
|
|
2327
|
+
case "checksafe":
|
|
2328
|
+
return executechecksafe(step, session, plan.id);
|
|
2329
|
+
case "batchopen":
|
|
2330
|
+
return executebatchopen(step, session);
|
|
2331
|
+
case "pausenav":
|
|
2332
|
+
return executepausenav(step, session);
|
|
2333
|
+
case "navintent":
|
|
2334
|
+
return executenavintent(step, session, origin);
|
|
2335
|
+
case "navrate":
|
|
2336
|
+
return executenavrate(step, session, origin);
|
|
2337
|
+
case "trailaudit":
|
|
2338
|
+
return executetrailaudit(session);
|
|
2339
|
+
case "openclipboard":
|
|
2340
|
+
return executeopenclipboard(step, session);
|
|
2341
|
+
default: {
|
|
2342
|
+
await refusenavpause(step);
|
|
2343
|
+
if (ratecheckedkinds.has(step.kind)) await enforceratelimit(origin, step.id, session?.id);
|
|
2344
|
+
const derived = injectallowedorigins(step, session);
|
|
2345
|
+
const output = await dispatchpagestep(derived, tabid2, origin, plan);
|
|
2346
|
+
if (step.kind === "followlink" || step.kind === "spanav") {
|
|
2347
|
+
const record2 = await recordnavigation(step, session, tabid2);
|
|
2348
|
+
return { ...output ?? { ok: false, summary: "The navigation step returned no result." }, details: { ...(output ?? {}).details ?? {}, finalurl: record2.finalurl, hops: record2.chain.hops.length } };
|
|
2349
|
+
}
|
|
2350
|
+
return output ?? { ok: false, summary: "The navigation step returned no result." };
|
|
2351
|
+
}
|
|
2352
|
+
}
|
|
2353
|
+
}
|
|
2354
|
+
async function refreshbadge() {
|
|
2355
|
+
const queues = await memory.getnavqueues();
|
|
2356
|
+
const total = (queues?.prefetch ?? 0) + (queues?.batchopen ?? 0);
|
|
2357
|
+
await chrome.action.setBadgeText({ text: total > 0 ? String(total) : "" }).catch(() => {
|
|
2358
|
+
});
|
|
2359
|
+
}
|
|
1257
2360
|
async function executestep(stepid) {
|
|
1258
2361
|
const session = await memory.getsession();
|
|
1259
2362
|
const plan = await memory.getplan();
|
|
1260
2363
|
const { tab, origin } = await activecontext();
|
|
1261
2364
|
const step = plan?.steps.find((candidate) => candidate.id === stepid);
|
|
1262
2365
|
if (!step) throw new Error("Reviewed step was not found.");
|
|
1263
|
-
const gate = canexecute({ session, plan, step, tabid: tab.id, origin });
|
|
2366
|
+
const gate = canexecute({ session, plan, step, tabid: tab.id, origin, verdicts: await memory.getsafeties() });
|
|
1264
2367
|
if (!gate.allowed) throw new Error(gate.reason);
|
|
2368
|
+
const capability = requiredcapability(step.kind);
|
|
2369
|
+
if (capability) {
|
|
2370
|
+
const granted = await chrome.permissions.contains({ permissions: [capability] });
|
|
2371
|
+
if (!granted) throw new Error(`The ${capability} capability has not been granted; request it from the review panel.`);
|
|
2372
|
+
}
|
|
1265
2373
|
let output;
|
|
1266
2374
|
let watchwindow;
|
|
1267
2375
|
if (isbrowserkind(step.kind)) {
|
|
1268
|
-
const capability = requiredcapability(step.kind);
|
|
1269
|
-
if (capability) {
|
|
1270
|
-
const granted = await chrome.permissions.contains({ permissions: [capability] });
|
|
1271
|
-
if (!granted) throw new Error(`The ${capability} capability has not been granted; request it from the review panel.`);
|
|
1272
|
-
}
|
|
1273
2376
|
output = await runbrowseraction(step, tab.id, tab.windowId ?? chrome.windows.WINDOW_ID_CURRENT);
|
|
1274
2377
|
} else if (step.kind === "keyhold") {
|
|
1275
2378
|
output = await executekeyhold(step, session, plan, tab.id, origin);
|
|
@@ -1290,6 +2393,8 @@ async function executestep(stepid) {
|
|
|
1290
2393
|
watchwindow = { startedat: watched.watch.startedat, lifetime: watched.watch.lifetime };
|
|
1291
2394
|
} else if (step.kind === "diffsnapshots") {
|
|
1292
2395
|
output = await executediffsnapshots(step, session, plan, tab.id, origin);
|
|
2396
|
+
} else if (navigationstepkinds.has(step.kind)) {
|
|
2397
|
+
output = await executenavigationkind(step, session, plan, tab.id, origin);
|
|
1293
2398
|
} else {
|
|
1294
2399
|
if (step.target && freshcheckkinds.has(step.kind)) {
|
|
1295
2400
|
const fresh = await snapshot(tab.id);
|
|
@@ -1297,6 +2402,7 @@ async function executestep(stepid) {
|
|
|
1297
2402
|
}
|
|
1298
2403
|
output = await dispatchpagestep(step, tab.id, origin, plan);
|
|
1299
2404
|
}
|
|
2405
|
+
if (["navigate", "back", "forward"].includes(step.kind)) await recordnavigation(step, session, tab.id);
|
|
1300
2406
|
await recordevidence(step, output, session, plan, origin);
|
|
1301
2407
|
const summary = output?.summary ?? "The page action returned no result.";
|
|
1302
2408
|
const resolved = output?.details?.resolvedtarget;
|
|
@@ -1390,7 +2496,20 @@ async function handlerequest(message, sender) {
|
|
|
1390
2496
|
const a11y = (await memory.geta11ytrees())[0];
|
|
1391
2497
|
const reader = (await memory.getreaderarticles())[0];
|
|
1392
2498
|
const signals = await memory.getsignals();
|
|
1393
|
-
|
|
2499
|
+
const trail = session ? await memory.gettrail(session.id) : [];
|
|
2500
|
+
const navrecords = await memory.getnavrecords();
|
|
2501
|
+
const ratestates = await memory.getratestates();
|
|
2502
|
+
const safeties = await memory.getsafeties();
|
|
2503
|
+
const curated = await memory.getcurateds();
|
|
2504
|
+
const waitprofiles = await memory.getwaitprofiles();
|
|
2505
|
+
const auths = (await memory.getauths()).map((record2) => ({ origin: record2.origin, username: record2.username, reviewedat: record2.reviewedat }));
|
|
2506
|
+
const navcontrol = await memory.getnavcontrol();
|
|
2507
|
+
const navqueues = await memory.getnavqueues();
|
|
2508
|
+
const artifacts = await memory.getartifacts();
|
|
2509
|
+
const livetab = session ? await chrome.tabs.get(session.tabid).catch(() => void 0) : void 0;
|
|
2510
|
+
const waitprofile = session ? waitprofiles.find((record2) => record2.origin === session.origin) : void 0;
|
|
2511
|
+
const livestate = { phase: livetab?.status === "loading" ? "loading" : "complete", ...navrecords[0] ? { finalurl: navrecords[0].finalurl, redirects: navrecords[0].chain } : {} };
|
|
2512
|
+
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 };
|
|
1394
2513
|
}
|
|
1395
2514
|
case "capabilities":
|
|
1396
2515
|
return refreshcapabilities();
|
|
@@ -1454,6 +2573,40 @@ async function handlerequest(message, sender) {
|
|
|
1454
2573
|
return pausesession();
|
|
1455
2574
|
case "resumesession":
|
|
1456
2575
|
return resumesession();
|
|
2576
|
+
case "storeauth": {
|
|
2577
|
+
const session = await memory.getsession();
|
|
2578
|
+
if (!session || session.stoppedat || session.expiresat <= Date.now()) throw new Error("Basic auth credentials are stored only behind the consent gate of an active session.");
|
|
2579
|
+
const inputauth = message;
|
|
2580
|
+
const origin = new URL(inputauth.origin ?? "").origin;
|
|
2581
|
+
if (!origin.startsWith("https://")) throw new Error("Basic auth credentials need an HTTPS origin.");
|
|
2582
|
+
if (!inputauth.username?.trim() || !inputauth.password) throw new Error("Basic auth credentials need a username and a password.");
|
|
2583
|
+
const record2 = { origin, username: inputauth.username.trim(), password: inputauth.password, reviewedat: Date.now() };
|
|
2584
|
+
await memory.setauth(record2);
|
|
2585
|
+
await audit("auth", `Basic auth credentials for ${origin} stored after explicit review; the password never leaves local storage.`, { sessionid: session.id });
|
|
2586
|
+
return { origin: record2.origin, username: record2.username, reviewedat: record2.reviewedat };
|
|
2587
|
+
}
|
|
2588
|
+
case "checksafe": {
|
|
2589
|
+
const inputurl = message;
|
|
2590
|
+
const verdict = { ...checksafe(inputurl.url ?? ""), at: Date.now() };
|
|
2591
|
+
await memory.addsafety(verdict);
|
|
2592
|
+
const session = await memory.getsession();
|
|
2593
|
+
await audit("navigation", `Safety check of ${verdict.url} returned ${verdict.safe ? "safe" : "unsafe"}${verdict.reasons.length > 0 ? `: ${verdict.reasons.join("; ")}` : ""}.`, { ...session ? { sessionid: session.id } : {} });
|
|
2594
|
+
return verdict;
|
|
2595
|
+
}
|
|
2596
|
+
case "navstate": {
|
|
2597
|
+
const plan = await memory.getplan();
|
|
2598
|
+
if (!plan) throw new Error("No plan is available for a navstate envelope.");
|
|
2599
|
+
const record2 = (await memory.getnavrecords())[0];
|
|
2600
|
+
const session = await memory.getsession();
|
|
2601
|
+
const livetab = session ? await chrome.tabs.get(session.tabid).catch(() => void 0) : void 0;
|
|
2602
|
+
const state = { phase: livetab?.status === "loading" ? "loading" : "complete", ...record2 ? { finalurl: record2.finalurl, redirects: record2.chain } : {} };
|
|
2603
|
+
return JSON.parse(navstateresponse({ navstate: state, plan }));
|
|
2604
|
+
}
|
|
2605
|
+
case "safeties": {
|
|
2606
|
+
const plan = await memory.getplan();
|
|
2607
|
+
if (!plan) throw new Error("No plan is available for a safety envelope.");
|
|
2608
|
+
return JSON.parse(safetyresponse({ verdicts: await memory.getsafeties(), plan }));
|
|
2609
|
+
}
|
|
1457
2610
|
case "stop": {
|
|
1458
2611
|
const session = await memory.getsession();
|
|
1459
2612
|
if (session) await memory.setsession({ ...session, stoppedat: Date.now() });
|