@wenathlan/extension 1.1.34 → 1.1.36
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 +7 -5
- package/dist/index.js +533 -5
- package/dist/index.js.map +2 -2
- package/dist/memory.d.ts +91 -1
- package/dist/memory.d.ts.map +1 -1
- package/dist/policy.d.ts +20 -1
- package/dist/policy.d.ts.map +1 -1
- package/dist/protocol.d.ts +37 -1
- package/dist/protocol.d.ts.map +1 -1
- package/dist/types.d.ts +271 -3
- package/dist/types.d.ts.map +1 -1
- package/dist/version.d.ts +1 -1
- package/extension/dist/background.js +2040 -17
- 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 +33 -2
- package/extension/dist/popup.js.map +2 -2
- package/extension/dist/sidepanel.html +1 -1
- package/extension/dist/sidepanel.js +396 -8
- package/extension/dist/sidepanel.js.map +4 -4
- package/extension/manifest.json +1 -1
- package/package.json +1 -1
|
@@ -238,19 +238,219 @@ 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
|
+
}
|
|
355
|
+
/** Stores one named tab layout with its window bounds and group states, replacing the previous layout of that name. */
|
|
356
|
+
async setlayout(layout) {
|
|
357
|
+
const records = (await this.getlayouts()).filter((item) => item.name !== layout.name);
|
|
358
|
+
await this.adapter.set("layouts", [layout, ...records]);
|
|
359
|
+
}
|
|
360
|
+
/** Returns one saved tab layout by name with its timestamp. */
|
|
361
|
+
async getlayout(name) {
|
|
362
|
+
return (await this.getlayouts()).find((item) => item.name === name);
|
|
363
|
+
}
|
|
364
|
+
/** Returns every saved tab layout with its window bounds and group states. */
|
|
365
|
+
async getlayouts() {
|
|
366
|
+
return await this.adapter.get("layouts") ?? [];
|
|
367
|
+
}
|
|
368
|
+
/** Stores one tab group definition with its color choice and member tabs, replacing the previous definition of that name. */
|
|
369
|
+
async settabgroup(group) {
|
|
370
|
+
const records = (await this.gettabgroups()).filter((item) => item.name !== group.name);
|
|
371
|
+
await this.adapter.set("tabgroups", [...records, group]);
|
|
372
|
+
}
|
|
373
|
+
/** Returns every stored tab group definition with its color choice, newest first. */
|
|
374
|
+
async gettabgroups() {
|
|
375
|
+
return await this.adapter.get("tabgroups") ?? [];
|
|
376
|
+
}
|
|
377
|
+
/** Records one tabmeta record with task provenance, replacing the previous metadata of that tab. */
|
|
378
|
+
async settabmeta(meta) {
|
|
379
|
+
const records = (await this.gettabmetas()).filter((item) => item.tabid !== meta.tabid);
|
|
380
|
+
await this.adapter.set("tabmetas", [...records, meta]);
|
|
381
|
+
}
|
|
382
|
+
/** Returns every stored tabmeta record with task provenance. */
|
|
383
|
+
async gettabmetas() {
|
|
384
|
+
return await this.adapter.get("tabmetas") ?? [];
|
|
385
|
+
}
|
|
386
|
+
/** Records one session snapshot of tabs and windows for later restore. */
|
|
387
|
+
async addsnapshot(snapshot2) {
|
|
388
|
+
const records = await this.getsnapshots();
|
|
389
|
+
await this.adapter.set("snapshots", [snapshot2, ...records]);
|
|
390
|
+
}
|
|
391
|
+
/** Returns every stored session snapshot, newest first. */
|
|
392
|
+
async getsnapshots() {
|
|
393
|
+
return await this.adapter.get("snapshots") ?? [];
|
|
394
|
+
}
|
|
395
|
+
/** Records one closed tab in the history kept for restoretab and reopenrun. */
|
|
396
|
+
async addclosedtab(tab) {
|
|
397
|
+
const records = await this.getclosedtabs();
|
|
398
|
+
await this.adapter.set("closedtabs", [tab, ...records]);
|
|
399
|
+
}
|
|
400
|
+
/** Returns the closed tab history, newest first. */
|
|
401
|
+
async getclosedtabs() {
|
|
402
|
+
return await this.adapter.get("closedtabs") ?? [];
|
|
403
|
+
}
|
|
404
|
+
/** Stores one badge state per task, replacing the previous badge of that task. */
|
|
405
|
+
async setbadge(badge) {
|
|
406
|
+
const records = (await this.getbadges()).filter((item) => item.taskid !== badge.taskid);
|
|
407
|
+
await this.adapter.set("badges", [...records, badge]);
|
|
408
|
+
}
|
|
409
|
+
/** Returns every stored badge state per task. */
|
|
410
|
+
async getbadges() {
|
|
411
|
+
return await this.adapter.get("badges") ?? [];
|
|
412
|
+
}
|
|
413
|
+
/** Records one tab event observed inside a reviewed watchtab registration. */
|
|
414
|
+
async addtabwatchevent(event) {
|
|
415
|
+
const records = await this.gettabwatchevents();
|
|
416
|
+
await this.adapter.set("tabwatchevents", [event, ...records]);
|
|
417
|
+
}
|
|
418
|
+
/** Returns the tab event stream of every reviewed watchtab registration, newest first. */
|
|
419
|
+
async gettabwatchevents() {
|
|
420
|
+
return await this.adapter.get("tabwatchevents") ?? [];
|
|
421
|
+
}
|
|
422
|
+
/** Returns the ids of the scratch windows opened for split work. */
|
|
423
|
+
async getscratchwindows() {
|
|
424
|
+
return await this.adapter.get("scratchwindows") ?? [];
|
|
425
|
+
}
|
|
426
|
+
/** Replaces the scratch window id list after one scratch window opens or closes. */
|
|
427
|
+
async setscratchwindows(ids) {
|
|
428
|
+
return this.adapter.set("scratchwindows", ids);
|
|
429
|
+
}
|
|
430
|
+
/** Returns the pinned control tab state with the live task feed. */
|
|
431
|
+
async getcontroltab() {
|
|
432
|
+
return this.adapter.get("controltab");
|
|
433
|
+
}
|
|
434
|
+
/** Replaces the pinned control tab state. */
|
|
435
|
+
async setcontroltab(state) {
|
|
436
|
+
return this.adapter.set("controltab", state);
|
|
437
|
+
}
|
|
241
438
|
};
|
|
242
439
|
function randomid() {
|
|
243
440
|
return crypto.randomUUID();
|
|
244
441
|
}
|
|
245
442
|
|
|
246
443
|
// 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"]);
|
|
444
|
+
var sensitiveactions = /* @__PURE__ */ new Set(["click", "type", "navigate", "select", "presskey", "drag", "drop", "upload", "clear", "check", "uncheck", "toggle", "submit", "reload", "back", "forward", "writestorage", "setattribute", "removeattribute", "evaluate", "tabcreate", "tabactivate", "tabclose", "tabreload", "windowcreate", "windowclose", "windowresize", "downloadfile", "clickpoint", "shiftclick", "dismissdialog", "enterframe", "typetime", "appendtext", "setvalue", "typeedit", "keyhold", "keyrelease", "submitsearch", "selectmulti", "chooseradio", "setslider", "setdate", "setcolor", "openlink", "openprivate", "reloadcache", "stopnav", "followlink", "spanav", "rewritequery", "setfragment", "navlist", "navprofile", "handleauth", "printpdf", "prefetch", "preconnect", "deeplink", "reopentab", "pausenav", "navrate", "openclipboard", "batchopen", "duplicatetab", "closepattern", "pintab", "mutetab", "movetab", "movetabwindow", "grouptabs", "colorgroup", "collapsegroup", "discardtab", "reloadtabs", "zoomin", "zoomout", "switchtab", "maximizewindow", "minimizewindow", "restorewindow", "focuswindow", "scratchwindow", "incognitowindow", "restoretab", "restorelayout", "reopenrun", "badgetab"]);
|
|
248
445
|
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"]);
|
|
446
|
+
var readactions = /* @__PURE__ */ new Set(["observe", "inspect", "extract", "wait", "waitfor", "waittext", "readattribute", "readstyle", "readgeometry", "readvalue", "readtext", "readhtml", "countelements", "readtable", "readlinks", "readimages", "readmeta", "readforms", "readstorage", "highlight", "tablist", "windowlist", "tabsnapshot", "mapclicks", "verifyvisible", "verifyenabled", "resolvexpath", "a11ytree", "readvisible", "readertree", "detectlists", "detecttables", "readjson", "watchmutate", "waitquiet", "watchbanner", "detectinfinitescroll", "detectvirtual", "detectlazy", "readscrollpos", "readlang", "readoutline", "countpages", "listshadow", "listframes", "classifypage", "fingerprintsection", "diffsnapshots", "readselection", "watchfocus", "detectsticky", "detectscrolllock", "readopengraph", "detectlanguage", "deriveselector", "waitload", "waiturl", "spawait", "detecthttp", "readredirects", "readfinalurl", "trailaudit", "navintent", "checksafe", "querytabs", "watchtab", "findclones", "searchtabs", "listaudio", "snapshotsession", "savelayout", "attachmeta"]);
|
|
250
447
|
var allowedactions = /* @__PURE__ */ new Set([...sensitiveactions, ...interactionactions, ...readactions]);
|
|
251
|
-
var watchactions = /* @__PURE__ */ new Set(["watchmutate", "watchbanner", "watchfocus"]);
|
|
448
|
+
var watchactions = /* @__PURE__ */ new Set(["watchmutate", "watchbanner", "watchfocus", "watchtab"]);
|
|
252
449
|
var targetactions = /* @__PURE__ */ new Set(["inspect", "focus", "click", "type", "scroll", "select", "hover", "clickdeep", "rightclick", "doubleclick", "drag", "drop", "upload", "clear", "check", "uncheck", "toggle", "submit", "readattribute", "readstyle", "readgeometry", "readvalue", "readtext", "readhtml", "countelements", "readtable", "highlight", "setattribute", "removeattribute", "waitfor", "shiftclick", "typetime", "appendtext", "setvalue", "typeedit", "submitsearch", "selectmulti", "chooseradio", "setslider", "setdate", "setcolor", "expanddetails", "verifyvisible", "verifyenabled", "pierceshadow", "deriveselector", "fingerprintsection"]);
|
|
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"]);
|
|
450
|
+
var valueactions = /* @__PURE__ */ new Set(["presskey", "drag", "drop", "upload", "readattribute", "removeattribute", "waittext", "evaluate", "zoomset", "tabactivate", "tabclose", "tabreload", "windowclose", "windowresize", "tabcreate", "windowcreate", "downloadfile", "typetime", "appendtext", "setvalue", "typeedit", "keyhold", "keyrelease", "chooseradio", "setslider", "setdate", "setcolor", "followlink", "setfragment", "handleauth", "navintent", "openclipboard", "checksafe", "reopentab", "spanav", "duplicatetab", "pintab", "mutetab", "movetab", "movetabwindow", "searchtabs", "badgetab", "attachmeta", "focuswindow", "maximizewindow", "minimizewindow", "restorewindow", "incognitowindow"]);
|
|
451
|
+
var tabscommandactions = /* @__PURE__ */ new Set(["querytabs", "duplicatetab", "closepattern", "pintab", "mutetab", "movetab", "movetabwindow", "grouptabs", "colorgroup", "collapsegroup", "discardtab", "reloadtabs", "zoomin", "zoomout", "watchtab", "switchtab", "maximizewindow", "minimizewindow", "restorewindow", "focuswindow", "scratchwindow", "incognitowindow", "restoretab", "savelayout", "restorelayout", "findclones", "searchtabs", "badgetab", "attachmeta", "listaudio", "reopenrun", "snapshotsession"]);
|
|
452
|
+
var layoutmutationactions = /* @__PURE__ */ new Set(["grouptabs", "colorgroup", "collapsegroup", "savelayout", "restorelayout"]);
|
|
453
|
+
var groupcolors = ["grey", "blue", "red", "yellow", "green", "pink", "purple", "cyan", "orange"];
|
|
254
454
|
function normalizeendpoint(value) {
|
|
255
455
|
const endpoint = new URL(value.trim());
|
|
256
456
|
if (endpoint.protocol !== "https:") throw new Error("Devthink accepts HTTPS endpoints only.");
|
|
@@ -281,8 +481,29 @@ function parseoptions(step) {
|
|
|
281
481
|
function requiredcapability(kind) {
|
|
282
482
|
if (kind === "tablist") return "tabs";
|
|
283
483
|
if (kind === "downloadfile") return "downloads";
|
|
484
|
+
if (kind === "openclipboard") return "clipboardRead";
|
|
485
|
+
if (kind === "openlink" || kind === "openprivate" || kind === "navlist" || kind === "batchopen" || kind === "reopentab" || kind === "deeplink") return "tabs";
|
|
486
|
+
if (tabscommandactions.has(kind)) return "tabs";
|
|
284
487
|
return void 0;
|
|
285
488
|
}
|
|
489
|
+
function istabscommandkind(kind) {
|
|
490
|
+
return tabscommandactions.has(kind);
|
|
491
|
+
}
|
|
492
|
+
function islayoutkind(kind) {
|
|
493
|
+
return layoutmutationactions.has(kind);
|
|
494
|
+
}
|
|
495
|
+
function layoutmutationgranted(session, now) {
|
|
496
|
+
if (!session || session.stoppedat || session.expiresat <= now) return { allowed: false, reason: "Group and layout mutations stay inside the active session." };
|
|
497
|
+
return { allowed: true };
|
|
498
|
+
}
|
|
499
|
+
function windowclosegate(tasktabcount, reviewed) {
|
|
500
|
+
if (tasktabcount > 1 && !reviewed) return { allowed: false, reason: `The window holds ${tasktabcount} task tabs and needs explicit review before it closes.` };
|
|
501
|
+
return { allowed: true };
|
|
502
|
+
}
|
|
503
|
+
function tasktabceiling(settings) {
|
|
504
|
+
const ceiling = settings?.tasktabceiling;
|
|
505
|
+
return typeof ceiling === "number" && Number.isFinite(ceiling) && ceiling >= 0 ? ceiling : void 0;
|
|
506
|
+
}
|
|
286
507
|
function waitduration(step) {
|
|
287
508
|
const requested = step.value ? Number.parseInt(step.value, 10) : 250;
|
|
288
509
|
if (!Number.isFinite(requested) || requested < 0) throw new Error("Wait duration must be zero or a positive number of milliseconds.");
|
|
@@ -331,6 +552,35 @@ function origingranted(session, origin) {
|
|
|
331
552
|
const grants = session.grants ?? [session.origin];
|
|
332
553
|
return grants.includes(origin);
|
|
333
554
|
}
|
|
555
|
+
function originverified(url, grants, verdicts) {
|
|
556
|
+
let origin = "";
|
|
557
|
+
try {
|
|
558
|
+
origin = new URL(url).origin;
|
|
559
|
+
} catch {
|
|
560
|
+
return { allowed: false, reason: "The reviewed navigation URL is invalid." };
|
|
561
|
+
}
|
|
562
|
+
if (grants.includes(origin)) return { allowed: true };
|
|
563
|
+
const covered = verdicts.find((verdict) => verdict.safe && (verdict.url === url || safeorigin(verdict.url) === origin));
|
|
564
|
+
if (covered) return { allowed: true };
|
|
565
|
+
return { allowed: false, reason: `The origin ${origin} is outside the session grants and has no safe checksafe verdict; run checksafe and review it first.` };
|
|
566
|
+
}
|
|
567
|
+
function safeorigin(url) {
|
|
568
|
+
try {
|
|
569
|
+
return new URL(url).origin;
|
|
570
|
+
} catch {
|
|
571
|
+
return "";
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
function navigationgranted(session, url) {
|
|
575
|
+
let origin = "";
|
|
576
|
+
try {
|
|
577
|
+
origin = new URL(url).origin;
|
|
578
|
+
} catch {
|
|
579
|
+
return { allowed: false, reason: "The reviewed navigation URL is invalid." };
|
|
580
|
+
}
|
|
581
|
+
if (origingranted(session, origin)) return { allowed: true };
|
|
582
|
+
return { allowed: false, reason: `Navigation to ${origin} leaves the task tab origins and needs the user consent of a session grant first.` };
|
|
583
|
+
}
|
|
334
584
|
function validateinnerstep(options, origin) {
|
|
335
585
|
const stepid = options.stepid;
|
|
336
586
|
const kind = options.kind;
|
|
@@ -354,6 +604,166 @@ function validateinnerstep(options, origin) {
|
|
|
354
604
|
};
|
|
355
605
|
return validatestep(inner, origin);
|
|
356
606
|
}
|
|
607
|
+
function ishttpsurl(value) {
|
|
608
|
+
if (typeof value !== "string" || !value.trim()) return false;
|
|
609
|
+
try {
|
|
610
|
+
return new URL(value).protocol === "https:";
|
|
611
|
+
} catch {
|
|
612
|
+
return false;
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
function validatenavtarget(value, kind) {
|
|
616
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return { allowed: false, reason: "A reviewed navtarget with a url is required in options." };
|
|
617
|
+
const target = value;
|
|
618
|
+
if (!ishttpsurl(target.url)) return { allowed: false, reason: "The reviewed navtarget url must use HTTPS." };
|
|
619
|
+
const container = target.container ?? "tab";
|
|
620
|
+
if (container !== "current" && container !== "tab" && container !== "window" && container !== "private") return { allowed: false, reason: "The reviewed navtarget container must be current, tab, window or private." };
|
|
621
|
+
if (target.position !== void 0 && target.position !== "adjacent" && target.position !== "end") return { allowed: false, reason: "The reviewed navtarget position must be adjacent or end." };
|
|
622
|
+
if (kind === "openprivate" && container !== "private") return { allowed: false, reason: "The openprivate step requires the private container." };
|
|
623
|
+
if (kind === "openlink" && container === "private") return { allowed: false, reason: "The openlink step cannot open the private container; use openprivate." };
|
|
624
|
+
return { allowed: true };
|
|
625
|
+
}
|
|
626
|
+
function validatewaitprofile(value) {
|
|
627
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return { allowed: false, reason: "A reviewed waitprofile with load signals is required in options." };
|
|
628
|
+
const profile = value;
|
|
629
|
+
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." };
|
|
630
|
+
if (!nonnegativeoption(profile, "idle")) return { allowed: false, reason: "The reviewed waitprofile idle threshold must be zero or a positive number of milliseconds." };
|
|
631
|
+
if (!nonnegativeoption(profile, "timeout")) return { allowed: false, reason: "The reviewed waitprofile timeout must be zero or a positive number of milliseconds." };
|
|
632
|
+
if (profile.overrides !== void 0) {
|
|
633
|
+
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." };
|
|
634
|
+
for (const entry of profile.overrides) {
|
|
635
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) return { allowed: false, reason: "Every reviewed waitprofile override must be an object with an origin." };
|
|
636
|
+
const override = entry;
|
|
637
|
+
if (!ishttpsurl(override.origin)) return { allowed: false, reason: "Every reviewed waitprofile override origin must use HTTPS." };
|
|
638
|
+
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." };
|
|
639
|
+
if (!nonnegativeoption(override, "idle") || !nonnegativeoption(override, "timeout")) return { allowed: false, reason: "The reviewed waitprofile override thresholds must be zero or positive numbers." };
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
return { allowed: true };
|
|
643
|
+
}
|
|
644
|
+
function validateurlpattern(value) {
|
|
645
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return { allowed: false, reason: "A reviewed urlpattern is required in options." };
|
|
646
|
+
const pattern = value;
|
|
647
|
+
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." };
|
|
648
|
+
if (!ishttpsurl(pattern.url)) return { allowed: false, reason: "The reviewed urlpattern url must use HTTPS." };
|
|
649
|
+
if (pattern.query !== void 0) {
|
|
650
|
+
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." };
|
|
651
|
+
for (const item of Object.values(pattern.query)) if (typeof item !== "string") return { allowed: false, reason: "The reviewed urlpattern query values must be strings." };
|
|
652
|
+
}
|
|
653
|
+
if (pattern.fragment !== void 0 && !isnonempty(pattern.fragment)) return { allowed: false, reason: "The reviewed urlpattern fragment must be a non-empty string." };
|
|
654
|
+
return { allowed: true };
|
|
655
|
+
}
|
|
656
|
+
function validateurllist(options, key) {
|
|
657
|
+
const urls = options[key];
|
|
658
|
+
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}.` };
|
|
659
|
+
return { allowed: true };
|
|
660
|
+
}
|
|
661
|
+
function validateratelimit(value) {
|
|
662
|
+
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." };
|
|
663
|
+
const limit = value;
|
|
664
|
+
if (limit.domain !== void 0 && !isnonempty(limit.domain)) return { allowed: false, reason: "The reviewed ratelimit domain must be a non-empty string." };
|
|
665
|
+
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." };
|
|
666
|
+
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." };
|
|
667
|
+
return { allowed: true };
|
|
668
|
+
}
|
|
669
|
+
function validatetabquery(value) {
|
|
670
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return { allowed: false, reason: "A reviewed tabquery with at least one matcher is required in options." };
|
|
671
|
+
const query = value;
|
|
672
|
+
const hasmatcher = query.url !== void 0 || query.title !== void 0 || query.id !== void 0 || query.pattern !== void 0;
|
|
673
|
+
if (!hasmatcher) return { allowed: false, reason: "The reviewed tabquery needs a url, title, id or pattern matcher." };
|
|
674
|
+
if (query.url !== void 0 && !isnonempty(query.url)) return { allowed: false, reason: "The reviewed tabquery url matcher must be a non-empty string." };
|
|
675
|
+
if (query.title !== void 0 && !isnonempty(query.title)) return { allowed: false, reason: "The reviewed tabquery title matcher must be a non-empty string." };
|
|
676
|
+
if (query.pattern !== void 0 && !isnonempty(query.pattern)) return { allowed: false, reason: "The reviewed tabquery pattern matcher must be a non-empty string." };
|
|
677
|
+
if (query.id !== void 0 && (typeof query.id !== "number" || !Number.isInteger(query.id) || query.id < 0)) return { allowed: false, reason: "The reviewed tabquery id matcher must be a non-negative integer tab id." };
|
|
678
|
+
return { allowed: true };
|
|
679
|
+
}
|
|
680
|
+
function validategroupcolor(value) {
|
|
681
|
+
return typeof value === "string" && groupcolors.includes(value);
|
|
682
|
+
}
|
|
683
|
+
function validateidlist(options, key) {
|
|
684
|
+
const ids = options[key];
|
|
685
|
+
return Array.isArray(ids) && ids.length > 0 && ids.every((id) => typeof id === "number" && Number.isInteger(id) && id >= 0);
|
|
686
|
+
}
|
|
687
|
+
function validatetabsgrammar(step, options) {
|
|
688
|
+
const kind = step.kind;
|
|
689
|
+
if (kind === "querytabs" || kind === "closepattern") {
|
|
690
|
+
const querycheck = validatetabquery(options.tabquery);
|
|
691
|
+
if (!querycheck.allowed) return querycheck;
|
|
692
|
+
if (kind === "closepattern" && options.reviewed !== true) return { allowed: false, reason: "The close pattern needs the explicit reviewed flag before any tab closes." };
|
|
693
|
+
}
|
|
694
|
+
if (kind === "duplicatetab" || kind === "pintab" || kind === "mutetab" || kind === "movetab" || kind === "movetabwindow" || kind === "badgetab" || kind === "attachmeta") {
|
|
695
|
+
if (!isnumericid(step.value)) return { allowed: false, reason: "A numeric browser tab id is required." };
|
|
696
|
+
}
|
|
697
|
+
if (kind === "focuswindow" || kind === "maximizewindow" || kind === "minimizewindow" || kind === "restorewindow") {
|
|
698
|
+
if (!isnumericid(step.value)) return { allowed: false, reason: "A numeric browser window id is required." };
|
|
699
|
+
}
|
|
700
|
+
if (kind === "pintab" && typeof options.pinned !== "boolean") return { allowed: false, reason: "A reviewed pinned flag is required in options." };
|
|
701
|
+
if (kind === "mutetab" && typeof options.muted !== "boolean") return { allowed: false, reason: "A reviewed muted flag is required in options." };
|
|
702
|
+
if (kind === "movetab") {
|
|
703
|
+
if (typeof options.index !== "number" || !Number.isInteger(options.index) || options.index < 0) return { allowed: false, reason: "A reviewed non-negative target index is required in options." };
|
|
704
|
+
}
|
|
705
|
+
if (kind === "movetabwindow") {
|
|
706
|
+
if (typeof options.windowid !== "number" || !Number.isInteger(options.windowid) || options.windowid < 0) return { allowed: false, reason: "A reviewed target window id is required in options." };
|
|
707
|
+
}
|
|
708
|
+
if (kind === "grouptabs") {
|
|
709
|
+
const group = options.group;
|
|
710
|
+
if (!group || typeof group !== "object" || Array.isArray(group)) return { allowed: false, reason: "A reviewed group with a name is required in options." };
|
|
711
|
+
const spec = group;
|
|
712
|
+
if (!isnonempty(spec.name)) return { allowed: false, reason: "The reviewed group needs a non-empty name." };
|
|
713
|
+
if (!validategroupcolor(spec.color)) return { allowed: false, reason: "The reviewed group color must be a Chromium tab group color." };
|
|
714
|
+
if (!validateidlist(spec, "tabids")) return { allowed: false, reason: "The reviewed group needs a non-empty list of member tab ids." };
|
|
715
|
+
}
|
|
716
|
+
if (kind === "colorgroup") {
|
|
717
|
+
if (!isnonempty(options.name)) return { allowed: false, reason: "A reviewed group name is required in options." };
|
|
718
|
+
if (!validategroupcolor(options.color)) return { allowed: false, reason: "The reviewed group color must be a Chromium tab group color." };
|
|
719
|
+
}
|
|
720
|
+
if (kind === "collapsegroup") {
|
|
721
|
+
if (!isnonempty(options.name)) return { allowed: false, reason: "A reviewed group name is required in options." };
|
|
722
|
+
if (typeof options.collapsed !== "boolean") return { allowed: false, reason: "A reviewed collapsed flag is required in options." };
|
|
723
|
+
}
|
|
724
|
+
if (kind === "discardtab" || kind === "reloadtabs") {
|
|
725
|
+
if (!isnumericid(step.value) && !validateidlist(options, "tabs")) return { allowed: false, reason: "A numeric tab id or a reviewed list of tab ids is required." };
|
|
726
|
+
}
|
|
727
|
+
if (kind === "zoomin" || kind === "zoomout") {
|
|
728
|
+
if (options.step !== void 0 && (typeof options.step !== "number" || !Number.isFinite(options.step) || options.step <= 0)) return { allowed: false, reason: "The reviewed zoom step must be a positive number with no code ceiling." };
|
|
729
|
+
if (step.value !== void 0 && step.value !== "" && !isnumericid(step.value)) return { allowed: false, reason: "The reviewed zoom target must be a numeric tab id." };
|
|
730
|
+
}
|
|
731
|
+
if (kind === "switchtab") {
|
|
732
|
+
if (options.direction !== "next" && options.direction !== "previous") return { allowed: false, reason: "A reviewed switch direction of next or previous is required in options." };
|
|
733
|
+
}
|
|
734
|
+
if (kind === "restorewindow") {
|
|
735
|
+
const bounds = options.bounds;
|
|
736
|
+
if (bounds !== void 0) {
|
|
737
|
+
if (!bounds || typeof bounds !== "object" || Array.isArray(bounds)) return { allowed: false, reason: "The reviewed window bounds must be an object." };
|
|
738
|
+
const shape = bounds;
|
|
739
|
+
for (const field of ["left", "top", "width", "height"]) {
|
|
740
|
+
if (typeof shape[field] !== "number" || !Number.isFinite(shape[field])) return { allowed: false, reason: "The reviewed window bounds need numeric left, top, width and height." };
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
if (kind === "scratchwindow") {
|
|
745
|
+
if (step.value !== void 0 && step.value !== "" && !ishttpsurl(step.value)) return { allowed: false, reason: "The reviewed scratch window url must use HTTPS." };
|
|
746
|
+
}
|
|
747
|
+
if (kind === "incognitowindow" && !ishttpsurl(step.value)) return { allowed: false, reason: "A reviewed HTTPS url is required to open an incognito window." };
|
|
748
|
+
if (kind === "restoretab" && step.value !== void 0 && step.value !== "" && !ishttpsurl(step.value)) return { allowed: false, reason: "The reviewed restore url must use HTTPS." };
|
|
749
|
+
if (kind === "savelayout" || kind === "restorelayout") {
|
|
750
|
+
if (!isnonempty(options.name)) return { allowed: false, reason: "A reviewed layout name is required in options." };
|
|
751
|
+
}
|
|
752
|
+
if (kind === "badgetab") {
|
|
753
|
+
if (!isnonempty(options.label)) return { allowed: false, reason: "A reviewed badge label is required in options." };
|
|
754
|
+
if (options.taskid !== void 0 && !isnonempty(options.taskid)) return { allowed: false, reason: "The reviewed badge task id must be a non-empty string." };
|
|
755
|
+
}
|
|
756
|
+
if (kind === "attachmeta") {
|
|
757
|
+
const labels = options.labels;
|
|
758
|
+
const taskrefs = options.taskrefs;
|
|
759
|
+
const haslabels = Array.isArray(labels) && labels.length > 0 && labels.every((label) => isnonempty(label));
|
|
760
|
+
const hastaskrefs = Array.isArray(taskrefs) && taskrefs.length > 0 && taskrefs.every((ref) => isnonempty(ref));
|
|
761
|
+
if (!haslabels && !hastaskrefs) return { allowed: false, reason: "Reviewed labels or task refs are required in options to attach metadata." };
|
|
762
|
+
if (options.provenance !== void 0 && !isnonempty(options.provenance)) return { allowed: false, reason: "The reviewed provenance must be a non-empty string." };
|
|
763
|
+
}
|
|
764
|
+
if (kind === "reopenrun" && !isnonempty(options.run)) return { allowed: false, reason: "A reviewed run id is required in options to reopen its tabs." };
|
|
765
|
+
return { allowed: true };
|
|
766
|
+
}
|
|
357
767
|
function validatestep(step, origin) {
|
|
358
768
|
if (!allowedactions.has(step.kind)) return { allowed: false, reason: "Unsupported action kind." };
|
|
359
769
|
if (!step.summary.trim()) return { allowed: false, reason: "A human-readable action summary is required." };
|
|
@@ -491,6 +901,84 @@ function validatestep(step, origin) {
|
|
|
491
901
|
const versions = options.versions;
|
|
492
902
|
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
903
|
}
|
|
904
|
+
if (step.kind === "openlink" || step.kind === "openprivate" || step.kind === "deeplink") {
|
|
905
|
+
const targetcheck = validatenavtarget(options.navtarget, step.kind);
|
|
906
|
+
if (!targetcheck.allowed) return targetcheck;
|
|
907
|
+
if (step.kind === "deeplink") {
|
|
908
|
+
const app = options.app;
|
|
909
|
+
if (!isnonempty(app)) return { allowed: false, reason: "A reviewed deep link app pattern is required in options." };
|
|
910
|
+
const params = options.params;
|
|
911
|
+
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." };
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
if (step.kind === "waitload" && !nonnegativeoption(options, "timeout")) return { allowed: false, reason: "The waitload timeout must be zero or a positive number of milliseconds." };
|
|
915
|
+
if (step.kind === "waiturl" || step.kind === "spawait") {
|
|
916
|
+
if (step.kind === "waiturl") {
|
|
917
|
+
const patterncheck = validateurlpattern(options.urlpattern);
|
|
918
|
+
if (!patterncheck.allowed) return patterncheck;
|
|
919
|
+
}
|
|
920
|
+
if (!nonnegativeoption(options, "timeout")) return { allowed: false, reason: "The wait timeout must be zero or a positive number of milliseconds." };
|
|
921
|
+
if (!nonnegativeoption(options, "poll")) return { allowed: false, reason: "The wait poll interval must be zero or a positive number of milliseconds." };
|
|
922
|
+
}
|
|
923
|
+
if (step.kind === "followlink") {
|
|
924
|
+
if (options.fragment !== void 0 && typeof options.fragment !== "boolean") return { allowed: false, reason: "The reviewed followlink fragment flag must be a boolean." };
|
|
925
|
+
}
|
|
926
|
+
if (step.kind === "spanav") {
|
|
927
|
+
if (options.routepattern !== void 0) {
|
|
928
|
+
const routecheck = validateurlpattern(options.routepattern);
|
|
929
|
+
if (!routecheck.allowed) return routecheck;
|
|
930
|
+
}
|
|
931
|
+
if (!nonnegativeoption(options, "timeout")) return { allowed: false, reason: "The spanav route timeout must be zero or a positive number of milliseconds." };
|
|
932
|
+
}
|
|
933
|
+
if (step.kind === "rewritequery") {
|
|
934
|
+
const set = options.set;
|
|
935
|
+
const remove = options.remove;
|
|
936
|
+
if (set === void 0 && remove === void 0) return { allowed: false, reason: "Reviewed query parameters to set or remove are required in options." };
|
|
937
|
+
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." };
|
|
938
|
+
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." };
|
|
939
|
+
}
|
|
940
|
+
if (step.kind === "navlist") {
|
|
941
|
+
const listcheck = validateurllist(options, "urls");
|
|
942
|
+
if (!listcheck.allowed) return listcheck;
|
|
943
|
+
}
|
|
944
|
+
if (step.kind === "navprofile") {
|
|
945
|
+
const profilecheck = validatewaitprofile(options.waitprofile);
|
|
946
|
+
if (!profilecheck.allowed) return profilecheck;
|
|
947
|
+
}
|
|
948
|
+
if (step.kind === "handleauth" && !ishttpsurl(step.value)) return { allowed: false, reason: "A reviewed HTTPS origin or url is required as the auth target." };
|
|
949
|
+
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." };
|
|
950
|
+
if (step.kind === "prefetch") {
|
|
951
|
+
const listcheck = validateurllist(options, "urls");
|
|
952
|
+
if (!listcheck.allowed) return listcheck;
|
|
953
|
+
}
|
|
954
|
+
if (step.kind === "preconnect") {
|
|
955
|
+
const origins = options.origins;
|
|
956
|
+
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." };
|
|
957
|
+
}
|
|
958
|
+
if (step.kind === "reopentab" && step.value !== void 0 && !ishttpsurl(step.value)) return { allowed: false, reason: "The reviewed reopen url must use HTTPS." };
|
|
959
|
+
if (step.kind === "navrate") {
|
|
960
|
+
const limitcheck = validateratelimit(options.ratelimit);
|
|
961
|
+
if (!limitcheck.allowed) return limitcheck;
|
|
962
|
+
}
|
|
963
|
+
if (step.kind === "checksafe" && !ishttpsurl(step.value)) return { allowed: false, reason: "A reviewed HTTPS url is required for the safety check." };
|
|
964
|
+
if (step.kind === "batchopen") {
|
|
965
|
+
const listcheck = validateurllist(options, "urls");
|
|
966
|
+
if (!listcheck.allowed) return listcheck;
|
|
967
|
+
}
|
|
968
|
+
if (istabscommandkind(step.kind)) {
|
|
969
|
+
const tabscheck = validatetabsgrammar(step, options);
|
|
970
|
+
if (!tabscheck.allowed) return tabscheck;
|
|
971
|
+
}
|
|
972
|
+
if (step.kind === "tabcreate") {
|
|
973
|
+
if (options.background !== void 0 && typeof options.background !== "boolean") return { allowed: false, reason: "The reviewed background flag must be a boolean." };
|
|
974
|
+
if (options.window !== void 0 && (typeof options.window !== "number" || !Number.isInteger(options.window) || options.window < 0)) return { allowed: false, reason: "The reviewed target window id must be a non-negative integer." };
|
|
975
|
+
}
|
|
976
|
+
if (step.kind === "windowcreate") {
|
|
977
|
+
for (const field of ["left", "top", "width", "height"]) {
|
|
978
|
+
if (options[field] !== void 0 && (typeof options[field] !== "number" || !Number.isFinite(options[field]))) return { allowed: false, reason: `The reviewed window ${field} must be a number.` };
|
|
979
|
+
}
|
|
980
|
+
if (options.state !== void 0 && !["normal", "maximized", "minimized", "fullscreen"].includes(options.state)) return { allowed: false, reason: "The reviewed window state must be normal, maximized, minimized or fullscreen." };
|
|
981
|
+
}
|
|
494
982
|
return { allowed: true };
|
|
495
983
|
}
|
|
496
984
|
function sessiongate(input) {
|
|
@@ -508,6 +996,37 @@ function canexecute(input) {
|
|
|
508
996
|
if (input.plan.expiresat <= now) return { allowed: false, reason: "The approved plan has expired." };
|
|
509
997
|
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
998
|
if (input.step.kind === "readjson" && !origingranted(input.session, input.origin)) return { allowed: false, reason: "The json state read is outside the session origin grants." };
|
|
999
|
+
if (input.step.kind === "navlist") {
|
|
1000
|
+
let options = {};
|
|
1001
|
+
try {
|
|
1002
|
+
options = parseoptions(input.step);
|
|
1003
|
+
} catch {
|
|
1004
|
+
options = {};
|
|
1005
|
+
}
|
|
1006
|
+
for (const url of Array.isArray(options.urls) ? options.urls : []) {
|
|
1007
|
+
if (typeof url !== "string") continue;
|
|
1008
|
+
const navigation = navigationgranted(input.session, url);
|
|
1009
|
+
if (!navigation.allowed) return navigation;
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
if (islayoutkind(input.step.kind) && !layoutmutationgranted(input.session, now).allowed) return { allowed: false, reason: "Group and layout mutations stay inside the active session." };
|
|
1013
|
+
if (input.step.kind === "openlink" || input.step.kind === "openprivate" || input.step.kind === "batchopen" || input.step.kind === "prefetch" || input.step.kind === "deeplink" || input.step.kind === "reopentab") {
|
|
1014
|
+
let options = {};
|
|
1015
|
+
try {
|
|
1016
|
+
options = parseoptions(input.step);
|
|
1017
|
+
} catch {
|
|
1018
|
+
options = {};
|
|
1019
|
+
}
|
|
1020
|
+
const grammar = validatestep(input.step, input.origin);
|
|
1021
|
+
if (!grammar.allowed) return grammar;
|
|
1022
|
+
const grants = input.session?.grants ?? [input.session?.origin ?? input.origin];
|
|
1023
|
+
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];
|
|
1024
|
+
for (const target of targets) {
|
|
1025
|
+
if (typeof target !== "string" || !target) continue;
|
|
1026
|
+
const verified = originverified(target, grants, input.verdicts ?? []);
|
|
1027
|
+
if (!verified.allowed) return verified;
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
511
1030
|
return validatestep(input.step, input.origin);
|
|
512
1031
|
}
|
|
513
1032
|
function canpreview(input) {
|
|
@@ -558,9 +1077,23 @@ function recordwatchcompletion(progress, planid, stepid, startedat, lifetime, no
|
|
|
558
1077
|
if (!watchclosed(startedat, lifetime, now)) return base;
|
|
559
1078
|
return recordstep(base, planid, stepid, now);
|
|
560
1079
|
}
|
|
1080
|
+
function recordnaventry(progress, planid, stepid, entry, now) {
|
|
1081
|
+
const base = progress && progress.planid === planid ? progress : emptyprogress(planid, now);
|
|
1082
|
+
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 };
|
|
1083
|
+
return recordoutcome(base, planid, outcome, now);
|
|
1084
|
+
}
|
|
1085
|
+
function assigntasktab(progress, planid, tabid2, now) {
|
|
1086
|
+
const base = progress && progress.planid === planid ? progress : emptyprogress(planid, now);
|
|
1087
|
+
if ((base.tasktabs ?? []).includes(tabid2)) return { ...base, updatedat: now };
|
|
1088
|
+
return { ...base, tasktabs: [...base.tasktabs ?? [], tabid2], updatedat: now };
|
|
1089
|
+
}
|
|
1090
|
+
function tasktabs(progress, planid) {
|
|
1091
|
+
if (!progress || progress.planid !== planid) return [];
|
|
1092
|
+
return progress.tasktabs ?? [];
|
|
1093
|
+
}
|
|
561
1094
|
|
|
562
1095
|
// version.ts
|
|
563
|
-
var packageversion = "1.1.
|
|
1096
|
+
var packageversion = "1.1.36";
|
|
564
1097
|
|
|
565
1098
|
// types.ts
|
|
566
1099
|
var protocolversion = packageversion;
|
|
@@ -640,6 +1173,18 @@ function signalsreport(input) {
|
|
|
640
1173
|
...signals && signals.banner !== void 0 ? { banner: signals.banner } : {}
|
|
641
1174
|
};
|
|
642
1175
|
}
|
|
1176
|
+
function navstateresponse(input) {
|
|
1177
|
+
return JSON.stringify({ version: protocolversion, planid: input.plan.id, planstate: input.plan.state, navstate: input.navstate });
|
|
1178
|
+
}
|
|
1179
|
+
function trailreport(input) {
|
|
1180
|
+
return { version: protocolversion, ...input.sessionid ? { sessionid: input.sessionid } : {}, trail: input.trail };
|
|
1181
|
+
}
|
|
1182
|
+
function safetyresponse(input) {
|
|
1183
|
+
return JSON.stringify({ version: protocolversion, planid: input.plan.id, planstate: input.plan.state, verdicts: input.verdicts });
|
|
1184
|
+
}
|
|
1185
|
+
function layoutreport(input) {
|
|
1186
|
+
return { version: protocolversion, layouts: input.layouts };
|
|
1187
|
+
}
|
|
643
1188
|
|
|
644
1189
|
// extension/browsertabs.ts
|
|
645
1190
|
var browserkinds = /* @__PURE__ */ new Set(["tablist", "tabcreate", "tabactivate", "tabclose", "tabreload", "tabsnapshot", "windowlist", "windowcreate", "windowclose", "zoomset", "windowresize", "downloadfile"]);
|
|
@@ -675,8 +1220,9 @@ async function runbrowseraction(step, sessiontabid, windowid) {
|
|
|
675
1220
|
return { ok: true, summary: `Listed ${tabs.length} open tab${tabs.length === 1 ? "" : "s"}.`, details: { tabs: tabs.map((tab) => ({ id: tab.id ?? 0, index: tab.index, title: tab.title ?? "", url: tab.url ?? "", active: tab.active, pinned: tab.pinned, audible: tab.audible ?? false })) } };
|
|
676
1221
|
}
|
|
677
1222
|
case "tabcreate": {
|
|
678
|
-
const
|
|
679
|
-
|
|
1223
|
+
const targetwindow = typeof options.window === "number" && Number.isFinite(options.window) ? options.window : void 0;
|
|
1224
|
+
const created = await chrome.tabs.create({ url: step.value, active: options.active !== false && options.background !== true, pinned: options.pinned === true, ...targetwindow !== void 0 ? { windowId: targetwindow } : {} });
|
|
1225
|
+
return { ok: true, summary: `Opened a new tab for ${step.value}${options.background === true ? " in the background without activating it" : ""}.`, details: { tabid: created?.id ?? 0, ...targetwindow !== void 0 ? { windowid: targetwindow } : {}, ...options.background === true ? { background: true } : {} } };
|
|
680
1226
|
}
|
|
681
1227
|
case "tabactivate": {
|
|
682
1228
|
await chrome.tabs.update(tabid(step), { active: true });
|
|
@@ -699,8 +1245,11 @@ async function runbrowseraction(step, sessiontabid, windowid) {
|
|
|
699
1245
|
return { ok: true, summary: `Listed ${windows.length} open window${windows.length === 1 ? "" : "s"}.`, details: { windows: windows.map((item) => ({ id: item.id ?? 0, type: item.type, state: item.state ?? "", focused: item.focused })) } };
|
|
700
1246
|
}
|
|
701
1247
|
case "windowcreate": {
|
|
702
|
-
const
|
|
703
|
-
|
|
1248
|
+
const bounds = ["left", "top", "width", "height"].filter((field) => typeof options[field] === "number");
|
|
1249
|
+
const geometry = Object.fromEntries(bounds.map((field) => [field, options[field]]));
|
|
1250
|
+
const state = typeof options.state === "string" && ["normal", "maximized", "minimized", "fullscreen"].includes(options.state) ? options.state : void 0;
|
|
1251
|
+
const created = await chrome.windows.create({ url: step.value ?? "about:blank", ...Object.keys(geometry).length > 0 ? geometry : {}, ...state !== void 0 ? { state } : {} });
|
|
1252
|
+
return { ok: true, summary: `Opened a new window for ${step.value}.`, details: { windowid: created?.id ?? 0, ...Object.keys(geometry).length > 0 ? { bounds: geometry } : {}, ...state !== void 0 ? { state } : {} } };
|
|
704
1253
|
}
|
|
705
1254
|
case "windowclose": {
|
|
706
1255
|
await chrome.windows.remove(tabid(step));
|
|
@@ -724,6 +1273,125 @@ async function runbrowseraction(step, sessiontabid, windowid) {
|
|
|
724
1273
|
}
|
|
725
1274
|
}
|
|
726
1275
|
|
|
1276
|
+
// extension/tabscommand.ts
|
|
1277
|
+
function parsetabquery(step) {
|
|
1278
|
+
let options = {};
|
|
1279
|
+
try {
|
|
1280
|
+
options = parseoptions(step);
|
|
1281
|
+
} catch {
|
|
1282
|
+
options = {};
|
|
1283
|
+
}
|
|
1284
|
+
const value = options.tabquery;
|
|
1285
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
1286
|
+
const query = value;
|
|
1287
|
+
return {
|
|
1288
|
+
...typeof query.url === "string" && query.url ? { url: query.url } : {},
|
|
1289
|
+
...typeof query.title === "string" && query.title ? { title: query.title } : {},
|
|
1290
|
+
...typeof query.id === "number" && Number.isInteger(query.id) && query.id >= 0 ? { id: query.id } : {},
|
|
1291
|
+
...typeof query.pattern === "string" && query.pattern ? { pattern: query.pattern } : {}
|
|
1292
|
+
};
|
|
1293
|
+
}
|
|
1294
|
+
function tabpatternmatches(pattern, url) {
|
|
1295
|
+
const source = pattern.split("**").map((part) => part.split("*").map((piece) => piece.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("[^/]*")).join(".*");
|
|
1296
|
+
return new RegExp(`^${source}$`).test(url);
|
|
1297
|
+
}
|
|
1298
|
+
function querymatches(query, tabs) {
|
|
1299
|
+
return tabs.filter((tab) => {
|
|
1300
|
+
if (query.id !== void 0 && tab.tabid !== query.id) return false;
|
|
1301
|
+
if (query.url !== void 0 && tab.url !== query.url) return false;
|
|
1302
|
+
if (query.title !== void 0 && !tab.title.toLowerCase().includes(query.title.toLowerCase())) return false;
|
|
1303
|
+
if (query.pattern !== void 0 && !tabpatternmatches(query.pattern, tab.url)) return false;
|
|
1304
|
+
return true;
|
|
1305
|
+
});
|
|
1306
|
+
}
|
|
1307
|
+
function normalizedtaburl(url) {
|
|
1308
|
+
let normalized = url;
|
|
1309
|
+
const hash = normalized.indexOf("#");
|
|
1310
|
+
if (hash >= 0) normalized = normalized.slice(0, hash);
|
|
1311
|
+
while (normalized.length > 1 && normalized.endsWith("/")) normalized = normalized.slice(0, -1);
|
|
1312
|
+
return normalized;
|
|
1313
|
+
}
|
|
1314
|
+
function clonetabs(tabs) {
|
|
1315
|
+
const groups = /* @__PURE__ */ new Map();
|
|
1316
|
+
for (const tab of tabs) {
|
|
1317
|
+
if (!tab.url) continue;
|
|
1318
|
+
const key = normalizedtaburl(tab.url);
|
|
1319
|
+
groups.set(key, [...groups.get(key) ?? [], tab.tabid]);
|
|
1320
|
+
}
|
|
1321
|
+
return [...groups.entries()].filter(([, tabids]) => tabids.length > 1).map(([url, tabids]) => ({ url, tabids }));
|
|
1322
|
+
}
|
|
1323
|
+
function searchtabmatches(tabs, text2) {
|
|
1324
|
+
const needle = text2.trim().toLowerCase();
|
|
1325
|
+
if (!needle) return [];
|
|
1326
|
+
return tabs.filter((tab) => tab.title.toLowerCase().includes(needle) || tab.url.toLowerCase().includes(needle));
|
|
1327
|
+
}
|
|
1328
|
+
function audiotabs(tabs) {
|
|
1329
|
+
return tabs.filter((tab) => tab.audible || tab.muted && tab.audible);
|
|
1330
|
+
}
|
|
1331
|
+
function discardcandidates(tabs) {
|
|
1332
|
+
return tabs.filter((tab) => !tab.active && !tab.pinned && !tab.discarded && tab.url.length > 0);
|
|
1333
|
+
}
|
|
1334
|
+
function buildlayout(name, tabs, windows, groups, scratchwindowids, at) {
|
|
1335
|
+
return {
|
|
1336
|
+
name,
|
|
1337
|
+
tabs: tabs.map((tab) => ({ url: tab.url, title: tab.title, pinned: tab.pinned, index: tab.index, windowid: tab.windowid })),
|
|
1338
|
+
groups: groups.map((group) => ({ name: group.name, color: group.color, tabids: group.tabids.filter((tabid2) => tabs.some((tab) => tab.tabid === tabid2)), collapsed: group.collapsed })),
|
|
1339
|
+
windows: windows.map((item) => ({ windowid: item.windowid, state: { bounds: { left: item.left, top: item.top, width: item.width, height: item.height }, maximized: item.state === "maximized", profile: item.incognito ? "incognito" : scratchwindowids.includes(item.windowid) ? "scratch" : "normal" } })),
|
|
1340
|
+
savedat: at
|
|
1341
|
+
};
|
|
1342
|
+
}
|
|
1343
|
+
function layoutrestoreplan(layout, openurls) {
|
|
1344
|
+
const open = new Set(openurls.map((url) => normalizedtaburl(url)));
|
|
1345
|
+
return layout.tabs.map((tab) => tab.url).filter((url) => url.length > 0 && !open.has(normalizedtaburl(url)));
|
|
1346
|
+
}
|
|
1347
|
+
function regroupaftermoves(groups, tabs, at) {
|
|
1348
|
+
const order = new Map(tabs.map((tab) => [tab.tabid, tab.index]));
|
|
1349
|
+
return groups.map((group) => {
|
|
1350
|
+
const members = group.tabids.filter((tabid2) => order.has(tabid2));
|
|
1351
|
+
if (members.length === 0) return group;
|
|
1352
|
+
const ordered = [...members].sort((left, right) => (order.get(left) ?? 0) - (order.get(right) ?? 0));
|
|
1353
|
+
return ordered.length === group.tabids.length && ordered.every((tabid2, index) => tabid2 === group.tabids[index]) ? group : { ...group, tabids: ordered, savedat: at };
|
|
1354
|
+
});
|
|
1355
|
+
}
|
|
1356
|
+
function tasktabsinwindow(tabs, windowid, tasktabids) {
|
|
1357
|
+
const tasks = new Set(tasktabids);
|
|
1358
|
+
return tabs.filter((tab) => tab.windowid === windowid && tasks.has(tab.tabid)).length;
|
|
1359
|
+
}
|
|
1360
|
+
function closeselection(query, tabs, sessiontabid) {
|
|
1361
|
+
const matches = querymatches(query, tabs);
|
|
1362
|
+
return {
|
|
1363
|
+
targets: matches.filter((tab) => tab.tabid !== sessiontabid),
|
|
1364
|
+
refused: matches.filter((tab) => tab.tabid === sessiontabid)
|
|
1365
|
+
};
|
|
1366
|
+
}
|
|
1367
|
+
function zoomstep(current, direction, step) {
|
|
1368
|
+
const next = direction === "in" ? current + step : current - step;
|
|
1369
|
+
return next > 0 ? Number(next.toFixed(4)) : current;
|
|
1370
|
+
}
|
|
1371
|
+
function switchtarget(tabs, direction, currentindex) {
|
|
1372
|
+
if (tabs.length === 0) return void 0;
|
|
1373
|
+
const offset = direction === "next" ? 1 : -1;
|
|
1374
|
+
return (currentindex + offset + tabs.length) % tabs.length;
|
|
1375
|
+
}
|
|
1376
|
+
function watchtabdispatch(events, watchid, filters) {
|
|
1377
|
+
const allowed = filters.length > 0 ? new Set(filters) : void 0;
|
|
1378
|
+
return events.filter((event) => event.watchid === watchid && (allowed === void 0 || allowed.has(event.event)));
|
|
1379
|
+
}
|
|
1380
|
+
function badgefromprogress(completed, total) {
|
|
1381
|
+
if (total <= 0) return { label: "idle", done: false };
|
|
1382
|
+
if (completed >= total) return { label: "done", done: true };
|
|
1383
|
+
return { label: `${completed}/${total}`, done: false };
|
|
1384
|
+
}
|
|
1385
|
+
function tasktabgauge(used, ceiling) {
|
|
1386
|
+
return { used, ceiling, over: ceiling !== void 0 && used > ceiling };
|
|
1387
|
+
}
|
|
1388
|
+
function windowprofilegrants(profile) {
|
|
1389
|
+
return profile !== "incognito";
|
|
1390
|
+
}
|
|
1391
|
+
function trackedtasktabs(progress, planid) {
|
|
1392
|
+
return tasktabs(progress, planid);
|
|
1393
|
+
}
|
|
1394
|
+
|
|
727
1395
|
// extension/pagedialogs.ts
|
|
728
1396
|
function parsedialogpolicy(step) {
|
|
729
1397
|
let options = {};
|
|
@@ -857,12 +1525,332 @@ function heldkeys(holds, tabid2) {
|
|
|
857
1525
|
return holds.filter((hold) => hold.releasedat === void 0 && (tabid2 === void 0 || hold.tabid === void 0 || hold.tabid === tabid2));
|
|
858
1526
|
}
|
|
859
1527
|
|
|
1528
|
+
// extension/pagenav.ts
|
|
1529
|
+
function parsenavtarget(step) {
|
|
1530
|
+
let options = {};
|
|
1531
|
+
try {
|
|
1532
|
+
options = parseoptions(step);
|
|
1533
|
+
} catch {
|
|
1534
|
+
options = {};
|
|
1535
|
+
}
|
|
1536
|
+
const value = options.navtarget;
|
|
1537
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
1538
|
+
const target = value;
|
|
1539
|
+
if (typeof target.url !== "string" || !target.url) return null;
|
|
1540
|
+
const container = target.container === "current" || target.container === "window" || target.container === "private" ? target.container : "tab";
|
|
1541
|
+
return {
|
|
1542
|
+
url: target.url,
|
|
1543
|
+
container,
|
|
1544
|
+
...target.position === "end" ? { position: "end" } : { position: "adjacent" },
|
|
1545
|
+
private: container === "private" || target.private === true
|
|
1546
|
+
};
|
|
1547
|
+
}
|
|
1548
|
+
function resolvecontainer(target, windows) {
|
|
1549
|
+
if (target.container === "current") return { kind: "current", incognito: false, position: target.position ?? "adjacent" };
|
|
1550
|
+
if (target.container === "private" || target.private) return { kind: "private", incognito: true, position: target.position ?? "adjacent" };
|
|
1551
|
+
if (target.container === "window") {
|
|
1552
|
+
const focused = windows.find((item) => item.focused);
|
|
1553
|
+
return { kind: "window", incognito: false, ...focused ? { windowid: focused.id } : {}, position: target.position ?? "adjacent" };
|
|
1554
|
+
}
|
|
1555
|
+
const normal = windows.find((item) => !item.incognito && item.focused) ?? windows.find((item) => !item.incognito);
|
|
1556
|
+
return { kind: "tab", incognito: false, ...normal ? { windowid: normal.id } : {}, position: target.position ?? "adjacent" };
|
|
1557
|
+
}
|
|
1558
|
+
function parsewaitprofile(step) {
|
|
1559
|
+
let options = {};
|
|
1560
|
+
try {
|
|
1561
|
+
options = parseoptions(step);
|
|
1562
|
+
} catch {
|
|
1563
|
+
options = {};
|
|
1564
|
+
}
|
|
1565
|
+
const value = options.waitprofile;
|
|
1566
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
1567
|
+
const profile = value;
|
|
1568
|
+
const signals = Array.isArray(profile.signals) ? profile.signals.filter((item) => typeof item === "string" && item.trim().length > 0) : [];
|
|
1569
|
+
if (signals.length === 0) return null;
|
|
1570
|
+
const overrides = [];
|
|
1571
|
+
if (Array.isArray(profile.overrides)) {
|
|
1572
|
+
for (const item of profile.overrides) {
|
|
1573
|
+
if (!item || typeof item !== "object" || Array.isArray(item)) continue;
|
|
1574
|
+
const override = item;
|
|
1575
|
+
if (typeof override.origin !== "string" || !override.origin) continue;
|
|
1576
|
+
const overridesignals = Array.isArray(override.signals) ? override.signals.filter((entry) => typeof entry === "string" && entry.trim().length > 0) : void 0;
|
|
1577
|
+
overrides.push({
|
|
1578
|
+
origin: override.origin,
|
|
1579
|
+
...overridesignals && overridesignals.length > 0 ? { signals: overridesignals } : {},
|
|
1580
|
+
...typeof override.idle === "number" && Number.isFinite(override.idle) && override.idle >= 0 ? { idle: override.idle } : {},
|
|
1581
|
+
...typeof override.timeout === "number" && Number.isFinite(override.timeout) && override.timeout >= 0 ? { timeout: override.timeout } : {}
|
|
1582
|
+
});
|
|
1583
|
+
}
|
|
1584
|
+
}
|
|
1585
|
+
return {
|
|
1586
|
+
signals,
|
|
1587
|
+
...typeof profile.idle === "number" && Number.isFinite(profile.idle) && profile.idle >= 0 ? { idle: profile.idle } : {},
|
|
1588
|
+
...typeof profile.timeout === "number" && Number.isFinite(profile.timeout) && profile.timeout >= 0 ? { timeout: profile.timeout } : {},
|
|
1589
|
+
...overrides.length > 0 ? { overrides } : {}
|
|
1590
|
+
};
|
|
1591
|
+
}
|
|
1592
|
+
function profilefororigin(profile, origin) {
|
|
1593
|
+
let signals = [...profile.signals];
|
|
1594
|
+
let idle = profile.idle ?? 0;
|
|
1595
|
+
let timeout = profile.timeout ?? 0;
|
|
1596
|
+
for (const override of profile.overrides ?? []) {
|
|
1597
|
+
if (!override.origin || new URL(override.origin).origin !== origin) continue;
|
|
1598
|
+
if (override.signals && override.signals.length > 0) signals = [...override.signals];
|
|
1599
|
+
if (override.idle !== void 0) idle = override.idle;
|
|
1600
|
+
if (override.timeout !== void 0) timeout = override.timeout;
|
|
1601
|
+
}
|
|
1602
|
+
return { signals, idle, timeout };
|
|
1603
|
+
}
|
|
1604
|
+
function deeplinkurl(app, params) {
|
|
1605
|
+
const value = (name) => {
|
|
1606
|
+
const item = params[name];
|
|
1607
|
+
return typeof item === "string" && item.trim() ? item.trim() : void 0;
|
|
1608
|
+
};
|
|
1609
|
+
switch (app.trim().toLowerCase()) {
|
|
1610
|
+
case "github": {
|
|
1611
|
+
const owner = value("owner");
|
|
1612
|
+
const repo = value("repo");
|
|
1613
|
+
if (!owner || !repo) return null;
|
|
1614
|
+
const path = value("path");
|
|
1615
|
+
return `https://github.com/${owner}/${repo}${path ? `/${path.replace(/^\/+/, "")}` : ""}`;
|
|
1616
|
+
}
|
|
1617
|
+
case "youtube": {
|
|
1618
|
+
const id = value("id");
|
|
1619
|
+
if (id) return `https://www.youtube.com/watch?v=${encodeURIComponent(id)}`;
|
|
1620
|
+
const search = value("search");
|
|
1621
|
+
if (search) return `https://www.youtube.com/results?search_query=${encodeURIComponent(search)}`;
|
|
1622
|
+
return null;
|
|
1623
|
+
}
|
|
1624
|
+
case "maps": {
|
|
1625
|
+
const query = value("query");
|
|
1626
|
+
if (!query) return null;
|
|
1627
|
+
return `https://www.google.com/maps/search/${encodeURIComponent(query)}`;
|
|
1628
|
+
}
|
|
1629
|
+
case "wikipedia": {
|
|
1630
|
+
const title = value("title");
|
|
1631
|
+
if (!title) return null;
|
|
1632
|
+
const language = value("language") ?? "en";
|
|
1633
|
+
return `https://${language}.wikipedia.org/wiki/${encodeURIComponent(title.replace(/\s+/g, "_"))}`;
|
|
1634
|
+
}
|
|
1635
|
+
case "amazon": {
|
|
1636
|
+
const search = value("search");
|
|
1637
|
+
if (!search) return null;
|
|
1638
|
+
return `https://www.amazon.com/s?k=${encodeURIComponent(search)}`;
|
|
1639
|
+
}
|
|
1640
|
+
case "x": {
|
|
1641
|
+
const user = value("user");
|
|
1642
|
+
if (!user) return null;
|
|
1643
|
+
return `https://x.com/${user.replace(/^@/, "")}`;
|
|
1644
|
+
}
|
|
1645
|
+
default:
|
|
1646
|
+
return null;
|
|
1647
|
+
}
|
|
1648
|
+
}
|
|
1649
|
+
function pickrecenttab(recenttabs, openurls) {
|
|
1650
|
+
return recenttabs.find((tab) => !openurls.includes(tab.url)) ?? null;
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1653
|
+
// extension/pagenet.ts
|
|
1654
|
+
function buildredirectchain(events) {
|
|
1655
|
+
const hops = [];
|
|
1656
|
+
let startedat = 0;
|
|
1657
|
+
let endedat = 0;
|
|
1658
|
+
let open = false;
|
|
1659
|
+
for (const event of events) {
|
|
1660
|
+
if (event.event === "beforenavigate") {
|
|
1661
|
+
hops.length = 0;
|
|
1662
|
+
startedat = event.timestamp;
|
|
1663
|
+
endedat = event.timestamp;
|
|
1664
|
+
open = true;
|
|
1665
|
+
hops.push({ url: event.url, status: event.status ?? 0, at: event.timestamp });
|
|
1666
|
+
continue;
|
|
1667
|
+
}
|
|
1668
|
+
if (!open) continue;
|
|
1669
|
+
endedat = event.timestamp;
|
|
1670
|
+
if (event.event === "urlchange" || event.redirect || event.event === "committed" && hops[hops.length - 1]?.url !== event.url) {
|
|
1671
|
+
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 };
|
|
1672
|
+
else hops.push({ url: event.url, status: event.status ?? 0, at: event.timestamp });
|
|
1673
|
+
continue;
|
|
1674
|
+
}
|
|
1675
|
+
if (event.event === "committed" && typeof event.status === "number" && hops[hops.length - 1]) {
|
|
1676
|
+
hops[hops.length - 1] = { url: event.url, status: event.status, at: event.timestamp };
|
|
1677
|
+
continue;
|
|
1678
|
+
}
|
|
1679
|
+
if (event.event === "completed" || event.event === "error") {
|
|
1680
|
+
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 });
|
|
1681
|
+
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 };
|
|
1682
|
+
open = false;
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
1685
|
+
return { hops, startedat, endedat: endedat || startedat };
|
|
1686
|
+
}
|
|
1687
|
+
function finalurl(chain) {
|
|
1688
|
+
return chain.hops[chain.hops.length - 1]?.url ?? "";
|
|
1689
|
+
}
|
|
1690
|
+
function classifynavchange(previousurl, currenturl, status) {
|
|
1691
|
+
if (previousurl === currenturl) return status === "loading" ? "reload" : "none";
|
|
1692
|
+
try {
|
|
1693
|
+
if (new URL(previousurl).origin !== new URL(currenturl).origin) return "load";
|
|
1694
|
+
} catch {
|
|
1695
|
+
return "load";
|
|
1696
|
+
}
|
|
1697
|
+
return status === "loading" ? "load" : "route";
|
|
1698
|
+
}
|
|
1699
|
+
function detecthttpstate(input) {
|
|
1700
|
+
const reasons = [];
|
|
1701
|
+
let httperror = false;
|
|
1702
|
+
let certificate = false;
|
|
1703
|
+
const errors = input.errors ?? [];
|
|
1704
|
+
const statuses = input.statuses ?? [];
|
|
1705
|
+
for (const error of errors) {
|
|
1706
|
+
if (/CERT|SSL|TLS|privacy bad|your connection is not private/i.test(error)) {
|
|
1707
|
+
certificate = true;
|
|
1708
|
+
httperror = true;
|
|
1709
|
+
reasons.push(`certificate interstitial: ${error}`);
|
|
1710
|
+
continue;
|
|
1711
|
+
}
|
|
1712
|
+
if (/ERR_NAME_NOT_RESOLVED|ERR_CONNECTION|ERR_TIMED_OUT|ERR_INTERNET_DISCONNECTED|ERR_ADDRESS_UNREACHABLE|ERR_NETWORK/i.test(error)) {
|
|
1713
|
+
httperror = true;
|
|
1714
|
+
reasons.push(`network error: ${error}`);
|
|
1715
|
+
continue;
|
|
1716
|
+
}
|
|
1717
|
+
httperror = true;
|
|
1718
|
+
reasons.push(`navigation error: ${error}`);
|
|
1719
|
+
}
|
|
1720
|
+
for (const status of statuses) {
|
|
1721
|
+
if (status >= 400 && status < 600) {
|
|
1722
|
+
httperror = true;
|
|
1723
|
+
reasons.push(`http status ${status}`);
|
|
1724
|
+
}
|
|
1725
|
+
}
|
|
1726
|
+
if (input.offline) reasons.push("browser reports offline");
|
|
1727
|
+
return { httperror, offline: input.offline, certificate, reasons };
|
|
1728
|
+
}
|
|
1729
|
+
function interstitialpolicy(state) {
|
|
1730
|
+
if (state.certificate) return { interstitial: true, bypass: false, guidance: "A certificate interstitial was detected; Devthink reports it for review and never bypasses it." };
|
|
1731
|
+
if (state.httperror) return { interstitial: true, bypass: false, guidance: "An http error state was detected and is reported for review." };
|
|
1732
|
+
return { interstitial: false, bypass: false, guidance: "No interstitial was detected." };
|
|
1733
|
+
}
|
|
1734
|
+
function parseratelimit(step) {
|
|
1735
|
+
let options = {};
|
|
1736
|
+
try {
|
|
1737
|
+
options = parseoptions(step);
|
|
1738
|
+
} catch {
|
|
1739
|
+
options = {};
|
|
1740
|
+
}
|
|
1741
|
+
const value = options.ratelimit;
|
|
1742
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
1743
|
+
const limit = value;
|
|
1744
|
+
const window2 = typeof limit.window === "number" && Number.isFinite(limit.window) && limit.window > 0 ? limit.window : 0;
|
|
1745
|
+
const ceiling = typeof limit.ceiling === "number" && Number.isInteger(limit.ceiling) && limit.ceiling >= 1 ? limit.ceiling : 0;
|
|
1746
|
+
if (window2 <= 0 || ceiling < 1) return null;
|
|
1747
|
+
const domain = typeof limit.domain === "string" && limit.domain.trim() ? limit.domain.trim() : "";
|
|
1748
|
+
return { domain, window: window2, ceiling };
|
|
1749
|
+
}
|
|
1750
|
+
function domainof(url) {
|
|
1751
|
+
try {
|
|
1752
|
+
return new URL(url).hostname;
|
|
1753
|
+
} catch {
|
|
1754
|
+
return "";
|
|
1755
|
+
}
|
|
1756
|
+
}
|
|
1757
|
+
function ratewindow(state, limit, now) {
|
|
1758
|
+
if (state && state.domain === limit.domain && state.limit.window === limit.window && state.limit.ceiling === limit.ceiling && now < state.openedat + limit.window) return state;
|
|
1759
|
+
return { domain: limit.domain, limit, openedat: now, count: 0 };
|
|
1760
|
+
}
|
|
1761
|
+
function rateallows(state, now) {
|
|
1762
|
+
const elapsed = now - state.openedat;
|
|
1763
|
+
const remaining = Math.max(0, state.limit.ceiling - state.count);
|
|
1764
|
+
const retryafter = Math.max(0, state.limit.window - elapsed);
|
|
1765
|
+
return { allowed: remaining > 0, remaining, retryafter };
|
|
1766
|
+
}
|
|
1767
|
+
function recordratehit(state, now) {
|
|
1768
|
+
return { ...state, count: state.count + 1, ...state.count + 1 === 1 ? { openedat: now } : {} };
|
|
1769
|
+
}
|
|
1770
|
+
function checksafe(url) {
|
|
1771
|
+
const reasons = [];
|
|
1772
|
+
let safe = true;
|
|
1773
|
+
let parsed;
|
|
1774
|
+
try {
|
|
1775
|
+
parsed = new URL(url);
|
|
1776
|
+
} catch {
|
|
1777
|
+
return { url, safe: false, reasons: ["the url does not parse"], at: 0 };
|
|
1778
|
+
}
|
|
1779
|
+
if (parsed.protocol !== "https:") {
|
|
1780
|
+
safe = false;
|
|
1781
|
+
reasons.push("the url must use HTTPS");
|
|
1782
|
+
}
|
|
1783
|
+
if (parsed.username || parsed.password) {
|
|
1784
|
+
safe = false;
|
|
1785
|
+
reasons.push("the url carries embedded credentials");
|
|
1786
|
+
}
|
|
1787
|
+
const host = parsed.hostname.toLowerCase();
|
|
1788
|
+
const privatelist = ["localhost", "127.0.0.1", "0.0.0.0", "::1", "[::1]"];
|
|
1789
|
+
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)) {
|
|
1790
|
+
safe = false;
|
|
1791
|
+
reasons.push(`the host ${host} is a private network target`);
|
|
1792
|
+
}
|
|
1793
|
+
if (/^\d{1,3}(\.\d{1,3}){3}$/.test(host) || /^\[?[0-9a-f:]+\]?$/i.test(host)) {
|
|
1794
|
+
safe = false;
|
|
1795
|
+
reasons.push(`the host ${host} is a raw address without a domain`);
|
|
1796
|
+
}
|
|
1797
|
+
return { url, safe, reasons, at: 0 };
|
|
1798
|
+
}
|
|
1799
|
+
function curatelinks(urls, verifier) {
|
|
1800
|
+
return urls.map((url) => {
|
|
1801
|
+
const verdict = verifier(url);
|
|
1802
|
+
return { url, verdict: verdict.safe ? "safe" : "unsafe", reasons: verdict.reasons };
|
|
1803
|
+
});
|
|
1804
|
+
}
|
|
1805
|
+
function batchopenset(links) {
|
|
1806
|
+
const open = [];
|
|
1807
|
+
const refused = [];
|
|
1808
|
+
for (const link of links) {
|
|
1809
|
+
if (link.verdict === "safe") open.push(link.url);
|
|
1810
|
+
else refused.push({ url: link.url, reasons: link.reasons });
|
|
1811
|
+
}
|
|
1812
|
+
return { open, refused };
|
|
1813
|
+
}
|
|
1814
|
+
function prefetchcandidates(urls, grants) {
|
|
1815
|
+
const allowed = [];
|
|
1816
|
+
const refused = [];
|
|
1817
|
+
for (const url of urls) {
|
|
1818
|
+
let origin = "";
|
|
1819
|
+
try {
|
|
1820
|
+
origin = new URL(url).origin;
|
|
1821
|
+
} catch {
|
|
1822
|
+
refused.push(url);
|
|
1823
|
+
continue;
|
|
1824
|
+
}
|
|
1825
|
+
if (grants.includes(origin)) allowed.push(url);
|
|
1826
|
+
else refused.push(url);
|
|
1827
|
+
}
|
|
1828
|
+
return { allowed, refused };
|
|
1829
|
+
}
|
|
1830
|
+
function preconnectorigins(origins) {
|
|
1831
|
+
return [...new Set(origins.map((origin) => origin.trim()).filter(Boolean))];
|
|
1832
|
+
}
|
|
1833
|
+
function authfor(auths, url) {
|
|
1834
|
+
let origin = "";
|
|
1835
|
+
try {
|
|
1836
|
+
origin = new URL(url).origin;
|
|
1837
|
+
} catch {
|
|
1838
|
+
return void 0;
|
|
1839
|
+
}
|
|
1840
|
+
return auths.find((record2) => record2.origin === origin);
|
|
1841
|
+
}
|
|
1842
|
+
|
|
860
1843
|
// extension/background.ts
|
|
861
1844
|
var sessionduration = 15 * 60 * 1e3;
|
|
862
1845
|
var freshcheckkinds = /* @__PURE__ */ new Set(["focus", "inspect", "click", "type", "scroll", "select", "hover"]);
|
|
863
1846
|
var pointerkinds = /* @__PURE__ */ new Set(["movepointer", "clickpoint", "shiftclick", "clicktext", "clickaria", "clickname", "pierceshadow"]);
|
|
864
1847
|
var watchstepkinds = /* @__PURE__ */ new Set(["watchmutate", "watchbanner", "watchfocus"]);
|
|
865
1848
|
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"]);
|
|
1849
|
+
var navigationstepkinds = /* @__PURE__ */ new Set(["openlink", "openprivate", "reloadcache", "stopnav", "waitload", "waiturl", "followlink", "spanav", "spawait", "rewritequery", "setfragment", "navlist", "navprofile", "detecthttp", "readredirects", "readfinalurl", "handleauth", "printpdf", "prefetch", "preconnect", "deeplink", "reopentab", "trailaudit", "pausenav", "navintent", "navrate", "openclipboard", "checksafe", "batchopen"]);
|
|
1850
|
+
var pausenavkinds = /* @__PURE__ */ new Set(["openlink", "openprivate", "followlink", "spanav", "navlist", "openclipboard", "batchopen", "prefetch", "preconnect", "deeplink", "reopentab"]);
|
|
1851
|
+
var ratecheckedkinds = /* @__PURE__ */ new Set(["openlink", "openprivate", "followlink", "spanav", "navlist", "openclipboard", "batchopen", "deeplink", "reopentab"]);
|
|
1852
|
+
var evidencepoll = 100;
|
|
1853
|
+
var evidencesettle = 5e3;
|
|
866
1854
|
var chromestorage = {
|
|
867
1855
|
async get(key) {
|
|
868
1856
|
return (await chrome.storage.local.get(key))[key];
|
|
@@ -1026,12 +2014,26 @@ function browserauditkind(step) {
|
|
|
1026
2014
|
}
|
|
1027
2015
|
function stepauditkind(step, ok) {
|
|
1028
2016
|
if (isbrowserkind(step.kind)) return browserauditkind(step);
|
|
2017
|
+
if (istabscommandkind(step.kind)) {
|
|
2018
|
+
if (step.kind === "grouptabs" || step.kind === "colorgroup" || step.kind === "collapsegroup") return "group";
|
|
2019
|
+
if (step.kind === "savelayout" || step.kind === "restorelayout" || step.kind === "snapshotsession" || step.kind === "reopenrun") return "layout";
|
|
2020
|
+
if (step.kind === "discardtab") return "discard";
|
|
2021
|
+
if (step.kind === "badgetab") return "badge";
|
|
2022
|
+
if (step.kind === "watchtab") return "watch";
|
|
2023
|
+
if (step.kind === "maximizewindow" || step.kind === "minimizewindow" || step.kind === "restorewindow" || step.kind === "focuswindow" || step.kind === "scratchwindow" || step.kind === "incognitowindow") return "window";
|
|
2024
|
+
return "tab";
|
|
2025
|
+
}
|
|
1029
2026
|
if (step.kind === "dismissdialog") return "dialog";
|
|
1030
2027
|
if (step.kind === "keyhold" || step.kind === "keyrelease") return "hold";
|
|
1031
2028
|
if (step.kind === "retryaction") return "retry";
|
|
1032
2029
|
if (pointerkinds.has(step.kind)) return "pointer";
|
|
1033
2030
|
if (watchstepkinds.has(step.kind)) return "watch";
|
|
1034
2031
|
if (step.kind === "diffsnapshots") return "diff";
|
|
2032
|
+
if (step.kind === "readredirects" || step.kind === "readfinalurl" || step.kind === "detecthttp") return "redirect";
|
|
2033
|
+
if (step.kind === "handleauth") return "auth";
|
|
2034
|
+
if (step.kind === "prefetch" || step.kind === "preconnect") return "prefetch";
|
|
2035
|
+
if (step.kind === "navrate") return "rate";
|
|
2036
|
+
if (navigationstepkinds.has(step.kind)) return "navigation";
|
|
1035
2037
|
if (observationstepkinds.has(step.kind)) return "observation";
|
|
1036
2038
|
return ok ? "action" : "error";
|
|
1037
2039
|
}
|
|
@@ -1254,22 +2256,877 @@ async function recordevidence(step, output, session, plan, origin) {
|
|
|
1254
2256
|
}
|
|
1255
2257
|
await refreshsignals(step, output);
|
|
1256
2258
|
}
|
|
2259
|
+
var navbuffers = /* @__PURE__ */ new Map();
|
|
2260
|
+
var lastknownurls = /* @__PURE__ */ new Map();
|
|
2261
|
+
async function tracktabupdate(tabid2, changeinfo) {
|
|
2262
|
+
const now = Date.now();
|
|
2263
|
+
const url = typeof changeinfo.url === "string" ? changeinfo.url : void 0;
|
|
2264
|
+
const status = changeinfo.status;
|
|
2265
|
+
if (typeof changeinfo.title === "string" && changeinfo.title) {
|
|
2266
|
+
lastknowntitles.set(tabid2, changeinfo.title);
|
|
2267
|
+
await recordtabwatchevent("title", tabid2, changeinfo.title);
|
|
2268
|
+
}
|
|
2269
|
+
const previous = lastknownurls.get(tabid2);
|
|
2270
|
+
if (status === "loading" && url) {
|
|
2271
|
+
navbuffers.set(tabid2, [{ event: "beforenavigate", url, timestamp: now }]);
|
|
2272
|
+
lastknownurls.set(tabid2, url);
|
|
2273
|
+
return;
|
|
2274
|
+
}
|
|
2275
|
+
const buffer = navbuffers.get(tabid2) ?? [];
|
|
2276
|
+
if (url) {
|
|
2277
|
+
const kind = classifynavchange(previous ?? url, url, status);
|
|
2278
|
+
buffer.push({ event: kind === "route" ? "urlchange" : "committed", url, timestamp: now, redirect: kind === "route" });
|
|
2279
|
+
if (kind === "route" && previous) {
|
|
2280
|
+
const session = await memory.getsession();
|
|
2281
|
+
if (session && !session.stoppedat && session.tabid === tabid2) {
|
|
2282
|
+
await memory.addtrailentry(session.id, { url, title: "", at: now });
|
|
2283
|
+
}
|
|
2284
|
+
}
|
|
2285
|
+
lastknownurls.set(tabid2, url);
|
|
2286
|
+
}
|
|
2287
|
+
if (status === "complete") buffer.push({ event: "completed", url: lastknownurls.get(tabid2) ?? "", timestamp: now, status: 200 });
|
|
2288
|
+
navbuffers.set(tabid2, buffer);
|
|
2289
|
+
}
|
|
2290
|
+
chrome.tabs.onUpdated.addListener((tabid2, changeinfo) => {
|
|
2291
|
+
void tracktabupdate(tabid2, changeinfo);
|
|
2292
|
+
});
|
|
2293
|
+
chrome.tabs.onActivated.addListener((activeinfo) => {
|
|
2294
|
+
void recordtabwatchevent("activated", activeinfo.tabId);
|
|
2295
|
+
});
|
|
2296
|
+
chrome.tabs.onRemoved.addListener((tabid2) => {
|
|
2297
|
+
const url = lastknownurls.get(tabid2);
|
|
2298
|
+
const title = lastknowntitles.get(tabid2) ?? "";
|
|
2299
|
+
const windowid = 0;
|
|
2300
|
+
if (url) {
|
|
2301
|
+
void memory.addrecenttab({ url, tabid: tabid2, closedat: Date.now() });
|
|
2302
|
+
void memory.addclosedtab({ url, title, tabid: tabid2, windowid, closedat: Date.now() });
|
|
2303
|
+
}
|
|
2304
|
+
void recordtabwatchevent("closed", tabid2, url);
|
|
2305
|
+
lastknownurls.delete(tabid2);
|
|
2306
|
+
lastknowntitles.delete(tabid2);
|
|
2307
|
+
navbuffers.delete(tabid2);
|
|
2308
|
+
});
|
|
2309
|
+
async function recordnavigation(step, session, tabid2) {
|
|
2310
|
+
const started = Date.now();
|
|
2311
|
+
let tab = await chrome.tabs.get(tabid2).catch(() => void 0);
|
|
2312
|
+
while (tab && tab.status !== "complete" && Date.now() - started < evidencesettle) {
|
|
2313
|
+
await new Promise((resolve) => setTimeout(resolve, evidencepoll));
|
|
2314
|
+
tab = await chrome.tabs.get(tabid2).catch(() => void 0);
|
|
2315
|
+
}
|
|
2316
|
+
const url = tab?.url ?? lastknownurls.get(tabid2) ?? "";
|
|
2317
|
+
const title = tab?.title ?? "";
|
|
2318
|
+
const chain = buildredirectchain(navbuffers.get(tabid2) ?? []);
|
|
2319
|
+
const record2 = {
|
|
2320
|
+
stepid: step.id,
|
|
2321
|
+
...session ? { sessionid: session.id } : {},
|
|
2322
|
+
origin: session?.origin ?? (url ? new URL(url).origin : ""),
|
|
2323
|
+
finalurl: finalurl(chain) || url,
|
|
2324
|
+
chain,
|
|
2325
|
+
at: Date.now()
|
|
2326
|
+
};
|
|
2327
|
+
await memory.addnavrecord(record2);
|
|
2328
|
+
await memory.setnavstate(tabid2, record2);
|
|
2329
|
+
if (session && url) await memory.addtrailentry(session.id, { url, title, stepid: step.id, at: Date.now() });
|
|
2330
|
+
return record2;
|
|
2331
|
+
}
|
|
2332
|
+
function injectallowedorigins(step, session) {
|
|
2333
|
+
const allowedorigins = session?.grants ?? (session ? [session.origin] : []);
|
|
2334
|
+
let options = {};
|
|
2335
|
+
try {
|
|
2336
|
+
options = parseoptions(step);
|
|
2337
|
+
} catch {
|
|
2338
|
+
options = {};
|
|
2339
|
+
}
|
|
2340
|
+
return { ...step, options: JSON.stringify({ ...options, allowedorigins }) };
|
|
2341
|
+
}
|
|
2342
|
+
async function enforceratelimit(url, stepid, sessionid) {
|
|
2343
|
+
const domain = domainof(url);
|
|
2344
|
+
if (!domain) return;
|
|
2345
|
+
const states = await memory.getratestates();
|
|
2346
|
+
const stored = states.find((item) => item.domain === domain);
|
|
2347
|
+
if (!stored) return;
|
|
2348
|
+
const live = ratewindow(stored, stored.limit, Date.now());
|
|
2349
|
+
const decision = rateallows(live, Date.now());
|
|
2350
|
+
if (!decision.allowed) {
|
|
2351
|
+
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 });
|
|
2352
|
+
throw new Error(`The navigation rate limit for ${domain} has been reached; retry after ${Math.ceil(decision.retryafter / 1e3)} seconds.`);
|
|
2353
|
+
}
|
|
2354
|
+
await memory.setratestate(recordratehit(live, Date.now()));
|
|
2355
|
+
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 });
|
|
2356
|
+
}
|
|
2357
|
+
async function refusenavpause(step) {
|
|
2358
|
+
if (!pausenavkinds.has(step.kind)) return;
|
|
2359
|
+
const control = await memory.getnavcontrol();
|
|
2360
|
+
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.");
|
|
2361
|
+
}
|
|
2362
|
+
async function opencontainer(step, session, url, container, position) {
|
|
2363
|
+
const windows = await chrome.windows.getAll().catch(() => []);
|
|
2364
|
+
const plan = resolvecontainer({ url, container, position, private: container === "private" }, windows.map((item) => ({ id: item.id ?? 0, incognito: item.incognito ?? false, focused: item.focused ?? false })));
|
|
2365
|
+
const extra = { ...session ? { sessionid: session.id } : {}, stepid: step.id };
|
|
2366
|
+
if (plan.kind === "current" && session) {
|
|
2367
|
+
const tabid2 = session.tabid;
|
|
2368
|
+
await chrome.tabs.update(tabid2, { url });
|
|
2369
|
+
const record2 = await recordnavigation(step, session, tabid2);
|
|
2370
|
+
await audit("navigation", `Navigated the task tab to ${url}.`, extra);
|
|
2371
|
+
return { ok: true, summary: `Navigated the task tab to ${url}.`, details: { container: plan.kind, url, finalurl: record2.finalurl, hops: record2.chain.hops.length } };
|
|
2372
|
+
}
|
|
2373
|
+
if (plan.kind === "private" || plan.kind === "window") {
|
|
2374
|
+
const created2 = await chrome.windows.create({ url, incognito: plan.incognito });
|
|
2375
|
+
await audit("navigation", `Opened ${plan.kind === "private" ? "a private window" : "a new window"} for ${url}${plan.kind === "private" ? " separated from normal windows" : ""}.`, extra);
|
|
2376
|
+
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 } };
|
|
2377
|
+
}
|
|
2378
|
+
const created = await chrome.tabs.create({ url, ...plan.windowid !== void 0 ? { windowId: plan.windowid } : {}, active: true });
|
|
2379
|
+
await audit("navigation", `Opened a new tab for ${url} without leaving the current page.`, extra);
|
|
2380
|
+
return { ok: true, summary: `Opened a new tab for ${url}.`, details: { container: "tab", url, tabid: created?.id ?? 0, position: plan.position } };
|
|
2381
|
+
}
|
|
2382
|
+
async function verifyopenurl(url, session) {
|
|
2383
|
+
const grants = session?.grants ?? (session ? [session.origin] : []);
|
|
2384
|
+
const verified = originverified(url, grants, await memory.getsafeties());
|
|
2385
|
+
if (!verified.allowed) throw new Error(verified.reason);
|
|
2386
|
+
}
|
|
2387
|
+
async function executeopenlink(step, session) {
|
|
2388
|
+
const target = parsenavtarget(step);
|
|
2389
|
+
if (!target) throw new Error("A reviewed navtarget is required.");
|
|
2390
|
+
await refusenavpause(step);
|
|
2391
|
+
await enforceratelimit(target.url, step.id, session?.id);
|
|
2392
|
+
await verifyopenurl(target.url, session);
|
|
2393
|
+
return opencontainer(step, session, target.url, target.container, target.position ?? "adjacent");
|
|
2394
|
+
}
|
|
2395
|
+
async function executedeeplink(step, session) {
|
|
2396
|
+
let options = {};
|
|
2397
|
+
try {
|
|
2398
|
+
options = parseoptions(step);
|
|
2399
|
+
} catch {
|
|
2400
|
+
options = {};
|
|
2401
|
+
}
|
|
2402
|
+
const params = {};
|
|
2403
|
+
if (options.params && typeof options.params === "object" && !Array.isArray(options.params)) {
|
|
2404
|
+
for (const [name, value] of Object.entries(options.params)) if (typeof value === "string") params[name] = value;
|
|
2405
|
+
}
|
|
2406
|
+
const url = deeplinkurl(typeof options.app === "string" ? options.app : "", params);
|
|
2407
|
+
if (!url) throw new Error("The reviewed deep link pattern is not a known web app.");
|
|
2408
|
+
await refusenavpause(step);
|
|
2409
|
+
await enforceratelimit(url, step.id, session?.id);
|
|
2410
|
+
await verifyopenurl(url, session);
|
|
2411
|
+
const target = parsenavtarget(step);
|
|
2412
|
+
const container = target?.container === "window" ? "window" : target?.container === "private" ? "private" : target?.container === "current" ? "current" : "tab";
|
|
2413
|
+
const output = await opencontainer(step, session, url, container, target?.position ?? "adjacent");
|
|
2414
|
+
return { ...output, details: { ...output.details ?? {}, app: options.app, deeplink: url } };
|
|
2415
|
+
}
|
|
2416
|
+
async function executereopentab(step, session) {
|
|
2417
|
+
await refusenavpause(step);
|
|
2418
|
+
let url = step.value;
|
|
2419
|
+
if (!url) {
|
|
2420
|
+
const tabs = await chrome.tabs.query({}).catch(() => []);
|
|
2421
|
+
const openurls = tabs.map((tab) => tab.url ?? "").filter(Boolean);
|
|
2422
|
+
const recent = pickrecenttab(await memory.getrecenttabs(), openurls);
|
|
2423
|
+
if (!recent) throw new Error("No recently closed tab is available to reopen.");
|
|
2424
|
+
url = recent.url;
|
|
2425
|
+
}
|
|
2426
|
+
await enforceratelimit(url, step.id, session?.id);
|
|
2427
|
+
await verifyopenurl(url, session);
|
|
2428
|
+
const created = await chrome.tabs.create({ url, active: true });
|
|
2429
|
+
await audit("navigation", `Reopened the recently closed tab ${url}.`, { ...session ? { sessionid: session.id } : {}, stepid: step.id });
|
|
2430
|
+
return { ok: true, summary: `Reopened ${url} in a new tab.`, details: { url, tabid: created?.id ?? 0 } };
|
|
2431
|
+
}
|
|
2432
|
+
async function waitforcomplete(tabid2) {
|
|
2433
|
+
const started = Date.now();
|
|
2434
|
+
for (; ; ) {
|
|
2435
|
+
const tab = await chrome.tabs.get(tabid2).catch(() => void 0);
|
|
2436
|
+
if (!tab) return false;
|
|
2437
|
+
if (tab.status === "complete") return true;
|
|
2438
|
+
if (Date.now() - started >= evidencesettle) return false;
|
|
2439
|
+
await new Promise((resolve) => setTimeout(resolve, evidencepoll));
|
|
2440
|
+
}
|
|
2441
|
+
}
|
|
2442
|
+
async function recordnaventryoutcome(step, plan, entry) {
|
|
2443
|
+
const base = await memory.getprogress();
|
|
2444
|
+
await memory.setprogress(recordnaventry(base, plan.id, step.id, entry, Date.now()));
|
|
2445
|
+
}
|
|
2446
|
+
async function executenavlist(step, session, plan, tabid2) {
|
|
2447
|
+
let options = {};
|
|
2448
|
+
try {
|
|
2449
|
+
options = parseoptions(step);
|
|
2450
|
+
} catch {
|
|
2451
|
+
options = {};
|
|
2452
|
+
}
|
|
2453
|
+
const urls = Array.isArray(options.urls) ? options.urls.filter((item) => typeof item === "string" && item.trim().length > 0) : [];
|
|
2454
|
+
if (urls.length === 0) throw new Error("A reviewed list of navigation urls is required.");
|
|
2455
|
+
await refusenavpause(step);
|
|
2456
|
+
let completed = 0;
|
|
2457
|
+
let failed = "";
|
|
2458
|
+
for (let index = 0; index < urls.length; index += 1) {
|
|
2459
|
+
const url = urls[index];
|
|
2460
|
+
const entrygate = navigationgranted(session, url);
|
|
2461
|
+
if (!entrygate.allowed) {
|
|
2462
|
+
failed = entrygate.reason ?? "The navigation list entry was refused.";
|
|
2463
|
+
await recordnaventryoutcome(step, plan, { index, url, ok: false });
|
|
2464
|
+
break;
|
|
2465
|
+
}
|
|
2466
|
+
try {
|
|
2467
|
+
await enforceratelimit(url, step.id, session?.id);
|
|
2468
|
+
await refusenavpause(step);
|
|
2469
|
+
await chrome.tabs.update(tabid2, { url });
|
|
2470
|
+
await waitforcomplete(tabid2);
|
|
2471
|
+
await recordnavigation(step, session, tabid2);
|
|
2472
|
+
completed += 1;
|
|
2473
|
+
await recordnaventryoutcome(step, plan, { index, url, ok: true });
|
|
2474
|
+
} catch (error) {
|
|
2475
|
+
failed = error instanceof Error ? error.message : String(error);
|
|
2476
|
+
await recordnaventryoutcome(step, plan, { index, url, ok: false });
|
|
2477
|
+
break;
|
|
2478
|
+
}
|
|
2479
|
+
}
|
|
2480
|
+
const remaining = urls.length - completed;
|
|
2481
|
+
const ok = failed === "" && completed === urls.length;
|
|
2482
|
+
return {
|
|
2483
|
+
ok,
|
|
2484
|
+
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}`,
|
|
2485
|
+
details: { completed, remaining, urls }
|
|
2486
|
+
};
|
|
2487
|
+
}
|
|
2488
|
+
async function executenavprofile(step, session, origin) {
|
|
2489
|
+
const profile = parsewaitprofile(step);
|
|
2490
|
+
if (!profile) throw new Error("A reviewed waitprofile is required.");
|
|
2491
|
+
const record2 = { origin, profile, at: Date.now() };
|
|
2492
|
+
await memory.setwaitprofile(record2);
|
|
2493
|
+
const effective = profilefororigin(profile, origin);
|
|
2494
|
+
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 });
|
|
2495
|
+
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) } };
|
|
2496
|
+
}
|
|
2497
|
+
async function executedetecthttp(session, tabid2) {
|
|
2498
|
+
const records = await memory.getnavrecords();
|
|
2499
|
+
const buffer = navbuffers.get(tabid2) ?? [];
|
|
2500
|
+
const errors = buffer.filter((event) => event.error !== void 0).map((event) => event.error);
|
|
2501
|
+
const statuses = (records[0]?.chain.hops ?? []).map((hop) => hop.status).filter((status) => status > 0);
|
|
2502
|
+
const state = detecthttpstate({ offline: !navigator.onLine, errors, statuses });
|
|
2503
|
+
const policy = interstitialpolicy(state);
|
|
2504
|
+
return {
|
|
2505
|
+
ok: true,
|
|
2506
|
+
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.",
|
|
2507
|
+
details: { httperror: state.httperror, offline: state.offline, certificate: state.certificate, reasons: state.reasons, interstitial: policy.interstitial, bypass: policy.bypass, guidance: policy.guidance }
|
|
2508
|
+
};
|
|
2509
|
+
}
|
|
2510
|
+
async function executereadredirects(session) {
|
|
2511
|
+
const record2 = (await memory.getnavrecords()).find((item) => !session || item.sessionid === session.id);
|
|
2512
|
+
if (!record2) return { ok: false, summary: "No navigation has been recorded yet.", details: { chain: { hops: [], startedat: 0, endedat: 0 } } };
|
|
2513
|
+
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 } };
|
|
2514
|
+
}
|
|
2515
|
+
async function executereadfinalurl(session) {
|
|
2516
|
+
const record2 = (await memory.getnavrecords()).find((item) => !session || item.sessionid === session.id);
|
|
2517
|
+
if (!record2) return { ok: false, summary: "No navigation has been recorded yet.", details: { finalurl: "" } };
|
|
2518
|
+
return { ok: true, summary: `The final url after redirects is ${record2.finalurl}.`, details: { finalurl: record2.finalurl, hops: record2.chain.hops } };
|
|
2519
|
+
}
|
|
2520
|
+
async function executehandleauth(step, session) {
|
|
2521
|
+
const auths = await memory.getauths();
|
|
2522
|
+
const record2 = authfor(auths, step.value ?? "");
|
|
2523
|
+
if (!record2) throw new Error(`No reviewed basic auth credentials are stored for ${step.value ?? ""}; store them from the side panel first.`);
|
|
2524
|
+
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 });
|
|
2525
|
+
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 } };
|
|
2526
|
+
}
|
|
2527
|
+
async function executeprintpdf(step, session, plan, tabid2, origin) {
|
|
2528
|
+
let options = {};
|
|
2529
|
+
try {
|
|
2530
|
+
options = parseoptions(step);
|
|
2531
|
+
} catch {
|
|
2532
|
+
options = {};
|
|
2533
|
+
}
|
|
2534
|
+
const output = await dispatchpagestep(step, tabid2, origin, plan);
|
|
2535
|
+
const name = typeof options.name === "string" && options.name ? options.name : `${step.id}.pdf`;
|
|
2536
|
+
const artifact = { id: randomid(), kind: "printpdf", name, stepid: step.id, at: Date.now() };
|
|
2537
|
+
await memory.addartifact(artifact);
|
|
2538
|
+
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 });
|
|
2539
|
+
return { ok: output?.ok ?? false, summary: output?.summary ?? "The print pipeline returned no result.", details: { ...output?.details ?? {}, artifact } };
|
|
2540
|
+
}
|
|
2541
|
+
async function executeprefetch(step, session, plan, tabid2, origin) {
|
|
2542
|
+
let options = {};
|
|
2543
|
+
try {
|
|
2544
|
+
options = parseoptions(step);
|
|
2545
|
+
} catch {
|
|
2546
|
+
options = {};
|
|
2547
|
+
}
|
|
2548
|
+
const urls = Array.isArray(options.urls) ? options.urls.filter((item) => typeof item === "string" && item.trim().length > 0) : [];
|
|
2549
|
+
const grants = session?.grants ?? (session ? [session.origin] : []);
|
|
2550
|
+
const verdict = prefetchcandidates(urls, grants);
|
|
2551
|
+
if (verdict.allowed.length === 0) throw new Error("No prefetch candidate is covered by the session grants.");
|
|
2552
|
+
await refusenavpause(step);
|
|
2553
|
+
const derived = { ...step, options: JSON.stringify({ ...options, urls: verdict.allowed }) };
|
|
2554
|
+
const output = await dispatchpagestep(derived, tabid2, origin, plan);
|
|
2555
|
+
await memory.setnavqueues({ prefetch: verdict.allowed.length, batchopen: (await memory.getnavqueues())?.batchopen ?? 0, updatedat: Date.now() });
|
|
2556
|
+
await refreshbadge();
|
|
2557
|
+
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 });
|
|
2558
|
+
return { ok: Boolean(output?.ok), summary: output?.summary ?? "The prefetch step returned no result.", details: { ...output?.details ?? {}, allowed: verdict.allowed, refused: verdict.refused } };
|
|
2559
|
+
}
|
|
2560
|
+
async function executepreconnect(step, session, plan, tabid2, origin) {
|
|
2561
|
+
let options = {};
|
|
2562
|
+
try {
|
|
2563
|
+
options = parseoptions(step);
|
|
2564
|
+
} catch {
|
|
2565
|
+
options = {};
|
|
2566
|
+
}
|
|
2567
|
+
const origins = preconnectorigins(Array.isArray(options.origins) ? options.origins.filter((item) => typeof item === "string") : []);
|
|
2568
|
+
await refusenavpause(step);
|
|
2569
|
+
const output = await dispatchpagestep({ ...step, options: JSON.stringify({ ...options, origins }) }, tabid2, origin, plan);
|
|
2570
|
+
await audit("prefetch", `Preconnected to ${origins.length} expected origin${origins.length === 1 ? "" : "s"}: ${origins.join(", ")}.`, { ...session ? { sessionid: session.id } : {}, stepid: step.id });
|
|
2571
|
+
return { ok: Boolean(output?.ok), summary: output?.summary ?? "The preconnect step returned no result.", details: { ...output?.details ?? {}, origins } };
|
|
2572
|
+
}
|
|
2573
|
+
async function executeopenclipboard(step, session) {
|
|
2574
|
+
await refusenavpause(step);
|
|
2575
|
+
const text2 = await navigator.clipboard.readText();
|
|
2576
|
+
let url = "";
|
|
2577
|
+
try {
|
|
2578
|
+
url = new URL(text2.trim()).toString();
|
|
2579
|
+
} catch {
|
|
2580
|
+
throw new Error("The clipboard does not hold a valid url.");
|
|
2581
|
+
}
|
|
2582
|
+
if (!url.startsWith("https://")) throw new Error("The clipboard url must use HTTPS.");
|
|
2583
|
+
await enforceratelimit(url, step.id, session?.id);
|
|
2584
|
+
await verifyopenurl(url, session);
|
|
2585
|
+
const output = await opencontainer(step, session, url, "tab", "adjacent");
|
|
2586
|
+
await audit("navigation", `Opened the clipboard url ${url} on the explicit consent of the reviewed step.`, { ...session ? { sessionid: session.id } : {}, stepid: step.id });
|
|
2587
|
+
return { ...output, details: { ...output.details ?? {}, url } };
|
|
2588
|
+
}
|
|
2589
|
+
async function executechecksafe(step, session, planid) {
|
|
2590
|
+
const verdict = { ...checksafe(step.value ?? ""), at: Date.now() };
|
|
2591
|
+
await memory.addsafety(verdict);
|
|
2592
|
+
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 });
|
|
2593
|
+
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 } };
|
|
2594
|
+
}
|
|
2595
|
+
async function executebatchopen(step, session) {
|
|
2596
|
+
let options = {};
|
|
2597
|
+
try {
|
|
2598
|
+
options = parseoptions(step);
|
|
2599
|
+
} catch {
|
|
2600
|
+
options = {};
|
|
2601
|
+
}
|
|
2602
|
+
const urls = Array.isArray(options.urls) ? options.urls.filter((item) => typeof item === "string" && item.trim().length > 0) : [];
|
|
2603
|
+
const links = curatelinks(urls, (url) => ({ ...checksafe(url), at: Date.now() }));
|
|
2604
|
+
const curated = { id: randomid(), links, at: Date.now() };
|
|
2605
|
+
await memory.addcurated(curated);
|
|
2606
|
+
const set = batchopenset(links);
|
|
2607
|
+
if (set.refused.length > 0) {
|
|
2608
|
+
await memory.setnavqueues({ prefetch: (await memory.getnavqueues())?.prefetch ?? 0, batchopen: set.refused.length, updatedat: Date.now() });
|
|
2609
|
+
await refreshbadge();
|
|
2610
|
+
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 });
|
|
2611
|
+
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 } };
|
|
2612
|
+
}
|
|
2613
|
+
await refusenavpause(step);
|
|
2614
|
+
const opened = [];
|
|
2615
|
+
for (const url of set.open) {
|
|
2616
|
+
await enforceratelimit(url, step.id, session?.id);
|
|
2617
|
+
const created = await chrome.tabs.create({ url, active: opened.length === 0 });
|
|
2618
|
+
opened.push(created?.id ?? 0);
|
|
2619
|
+
if (session) await memory.addtrailentry(session.id, { url, title: "", stepid: step.id, at: Date.now() });
|
|
2620
|
+
}
|
|
2621
|
+
await memory.addcurated({ ...curated, reviewedat: Date.now() });
|
|
2622
|
+
await memory.setnavqueues({ prefetch: (await memory.getnavqueues())?.prefetch ?? 0, batchopen: 0, updatedat: Date.now() });
|
|
2623
|
+
await refreshbadge();
|
|
2624
|
+
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 });
|
|
2625
|
+
return { ok: true, summary: `Opened ${set.open.length} curated url${set.open.length === 1 ? "" : "s"} after per url safety checks.`, details: { curated, tabids: opened } };
|
|
2626
|
+
}
|
|
2627
|
+
async function executepausenav(step, session) {
|
|
2628
|
+
const control = await memory.getnavcontrol();
|
|
2629
|
+
let options = {};
|
|
2630
|
+
try {
|
|
2631
|
+
options = parseoptions(step);
|
|
2632
|
+
} catch {
|
|
2633
|
+
options = {};
|
|
2634
|
+
}
|
|
2635
|
+
if (control?.pausedat) {
|
|
2636
|
+
await memory.setnavcontrol({ updatedat: Date.now() });
|
|
2637
|
+
await audit("resume", `Navigation resumed after ${Date.now() - control.pausedat} milliseconds of pause.`, { ...session ? { sessionid: session.id } : {}, stepid: step.id });
|
|
2638
|
+
return { ok: true, summary: "Navigation resumed; reviewed navigation steps can run again.", details: { paused: false } };
|
|
2639
|
+
}
|
|
2640
|
+
const reason = typeof options.reason === "string" && options.reason ? options.reason : "a consent prompt is open";
|
|
2641
|
+
await memory.setnavcontrol({ pausedat: Date.now(), reason, updatedat: Date.now() });
|
|
2642
|
+
await audit("pause", `Navigation paused while ${reason}.`, { ...session ? { sessionid: session.id } : {}, stepid: step.id });
|
|
2643
|
+
return { ok: true, summary: `Navigation paused while ${reason}.`, details: { paused: true, reason } };
|
|
2644
|
+
}
|
|
2645
|
+
async function executenavintent(step, session, origin) {
|
|
2646
|
+
const record2 = { id: randomid(), intent: step.value ?? "", origin, ...session ? { sessionid: session.id } : {}, stepid: step.id, at: Date.now() };
|
|
2647
|
+
await memory.addnavintent(record2);
|
|
2648
|
+
await audit("navigation", `Navigation intent "${record2.intent}" recorded for ${origin}.`, { ...session ? { sessionid: session.id } : {}, stepid: step.id });
|
|
2649
|
+
return { ok: true, summary: `Recorded the navigation intent "${record2.intent}".`, details: { intent: record2.intent, origin, at: record2.at } };
|
|
2650
|
+
}
|
|
2651
|
+
async function executenavrate(step, session, origin) {
|
|
2652
|
+
const limit = parseratelimit(step);
|
|
2653
|
+
if (!limit) throw new Error("A reviewed ratelimit is required.");
|
|
2654
|
+
if (!limit.domain) limit.domain = domainof(origin) || origin;
|
|
2655
|
+
const states = await memory.getratestates();
|
|
2656
|
+
const stored = states.find((item) => item.domain === limit.domain);
|
|
2657
|
+
const live = ratewindow(stored, limit, Date.now());
|
|
2658
|
+
const decision = rateallows(live, Date.now());
|
|
2659
|
+
await memory.setratestate(live);
|
|
2660
|
+
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 });
|
|
2661
|
+
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 } };
|
|
2662
|
+
}
|
|
2663
|
+
async function executetrailaudit(session) {
|
|
2664
|
+
if (!session) throw new Error("No active browser session exists.");
|
|
2665
|
+
const trail = await memory.gettrail(session.id);
|
|
2666
|
+
return { ok: true, summary: `The navigation trail of the session holds ${trail.length} visited url${trail.length === 1 ? "" : "s"}.`, details: { trail } };
|
|
2667
|
+
}
|
|
2668
|
+
async function executenavigationkind(step, session, plan, tabid2, origin) {
|
|
2669
|
+
switch (step.kind) {
|
|
2670
|
+
case "openlink":
|
|
2671
|
+
return executeopenlink(step, session);
|
|
2672
|
+
case "openprivate":
|
|
2673
|
+
return executeopenlink(step, session);
|
|
2674
|
+
case "deeplink":
|
|
2675
|
+
return executedeeplink(step, session);
|
|
2676
|
+
case "reopentab":
|
|
2677
|
+
return executereopentab(step, session);
|
|
2678
|
+
case "reloadcache": {
|
|
2679
|
+
await refusenavpause(step);
|
|
2680
|
+
await chrome.tabs.reload(tabid2, { bypassCache: true });
|
|
2681
|
+
const record2 = await recordnavigation(step, session, tabid2);
|
|
2682
|
+
await audit("navigation", `Reloaded the page bypassing the cache.`, { ...session ? { sessionid: session.id } : {}, stepid: step.id });
|
|
2683
|
+
return { ok: true, summary: "Reloaded the page bypassing the cache.", details: { bypasscache: true, finalurl: record2.finalurl } };
|
|
2684
|
+
}
|
|
2685
|
+
case "navlist":
|
|
2686
|
+
return executenavlist(step, session, plan, tabid2);
|
|
2687
|
+
case "navprofile":
|
|
2688
|
+
return executenavprofile(step, session, origin);
|
|
2689
|
+
case "detecthttp":
|
|
2690
|
+
return executedetecthttp(session, tabid2);
|
|
2691
|
+
case "readredirects":
|
|
2692
|
+
return executereadredirects(session);
|
|
2693
|
+
case "readfinalurl":
|
|
2694
|
+
return executereadfinalurl(session);
|
|
2695
|
+
case "handleauth":
|
|
2696
|
+
return executehandleauth(step, session);
|
|
2697
|
+
case "printpdf":
|
|
2698
|
+
return executeprintpdf(step, session, plan, tabid2, origin);
|
|
2699
|
+
case "prefetch":
|
|
2700
|
+
return executeprefetch(step, session, plan, tabid2, origin);
|
|
2701
|
+
case "preconnect":
|
|
2702
|
+
return executepreconnect(step, session, plan, tabid2, origin);
|
|
2703
|
+
case "checksafe":
|
|
2704
|
+
return executechecksafe(step, session, plan.id);
|
|
2705
|
+
case "batchopen":
|
|
2706
|
+
return executebatchopen(step, session);
|
|
2707
|
+
case "pausenav":
|
|
2708
|
+
return executepausenav(step, session);
|
|
2709
|
+
case "navintent":
|
|
2710
|
+
return executenavintent(step, session, origin);
|
|
2711
|
+
case "navrate":
|
|
2712
|
+
return executenavrate(step, session, origin);
|
|
2713
|
+
case "trailaudit":
|
|
2714
|
+
return executetrailaudit(session);
|
|
2715
|
+
case "openclipboard":
|
|
2716
|
+
return executeopenclipboard(step, session);
|
|
2717
|
+
default: {
|
|
2718
|
+
await refusenavpause(step);
|
|
2719
|
+
if (ratecheckedkinds.has(step.kind)) await enforceratelimit(origin, step.id, session?.id);
|
|
2720
|
+
const derived = injectallowedorigins(step, session);
|
|
2721
|
+
const output = await dispatchpagestep(derived, tabid2, origin, plan);
|
|
2722
|
+
if (step.kind === "followlink" || step.kind === "spanav") {
|
|
2723
|
+
const record2 = await recordnavigation(step, session, tabid2);
|
|
2724
|
+
return { ...output ?? { ok: false, summary: "The navigation step returned no result." }, details: { ...(output ?? {}).details ?? {}, finalurl: record2.finalurl, hops: record2.chain.hops.length } };
|
|
2725
|
+
}
|
|
2726
|
+
return output ?? { ok: false, summary: "The navigation step returned no result." };
|
|
2727
|
+
}
|
|
2728
|
+
}
|
|
2729
|
+
}
|
|
2730
|
+
async function livetabs() {
|
|
2731
|
+
const tabs = await chrome.tabs.query({}).catch(() => []);
|
|
2732
|
+
return tabs.map((tab) => ({
|
|
2733
|
+
tabid: tab.id ?? 0,
|
|
2734
|
+
url: tab.url ?? "",
|
|
2735
|
+
title: tab.title ?? "",
|
|
2736
|
+
index: tab.index,
|
|
2737
|
+
windowid: tab.windowId ?? 0,
|
|
2738
|
+
active: tab.active,
|
|
2739
|
+
pinned: tab.pinned,
|
|
2740
|
+
audible: tab.audible ?? false,
|
|
2741
|
+
muted: tab.mutedInfo?.muted ?? false,
|
|
2742
|
+
discarded: tab.discarded ?? false
|
|
2743
|
+
}));
|
|
2744
|
+
}
|
|
2745
|
+
async function livewindows() {
|
|
2746
|
+
const windows = await chrome.windows.getAll().catch(() => []);
|
|
2747
|
+
return windows.map((item) => ({
|
|
2748
|
+
windowid: item.id ?? 0,
|
|
2749
|
+
left: item.left ?? 0,
|
|
2750
|
+
top: item.top ?? 0,
|
|
2751
|
+
width: item.width ?? 0,
|
|
2752
|
+
height: item.height ?? 0,
|
|
2753
|
+
state: item.state === "maximized" || item.state === "minimized" || item.state === "fullscreen" ? item.state : "normal",
|
|
2754
|
+
incognito: item.incognito ?? false,
|
|
2755
|
+
focused: item.focused
|
|
2756
|
+
}));
|
|
2757
|
+
}
|
|
2758
|
+
var tabwatchbuffers = /* @__PURE__ */ new Map();
|
|
2759
|
+
var lastknowntitles = /* @__PURE__ */ new Map();
|
|
2760
|
+
async function recordtabwatchevent(event, tabid2, detail) {
|
|
2761
|
+
const now = Date.now();
|
|
2762
|
+
for (const watch of await memory.getwatches()) {
|
|
2763
|
+
if (watch.closedat !== void 0 || watch.kind !== "watchtab") continue;
|
|
2764
|
+
if (watchclosed(watch.startedat, watch.lifetime, now)) continue;
|
|
2765
|
+
if (watch.events.length > 0 && !watch.events.includes(event)) continue;
|
|
2766
|
+
const record2 = { watchid: watch.watchid, event, tabid: tabid2, ...detail !== void 0 ? { detail } : {}, at: now };
|
|
2767
|
+
await memory.addtabwatchevent(record2);
|
|
2768
|
+
tabwatchbuffers.set(watch.watchid, [...tabwatchbuffers.get(watch.watchid) ?? [], record2]);
|
|
2769
|
+
}
|
|
2770
|
+
}
|
|
2771
|
+
async function buildtabreport(matches) {
|
|
2772
|
+
const [groups, badges, metas] = await Promise.all([memory.gettabgroups(), memory.getbadges(), memory.gettabmetas()]);
|
|
2773
|
+
const metabytab = new Map(metas.map((meta) => [meta.tabid, meta]));
|
|
2774
|
+
const entries = matches.map((tab) => ({
|
|
2775
|
+
tabid: tab.tabid,
|
|
2776
|
+
url: tab.url,
|
|
2777
|
+
title: tab.title,
|
|
2778
|
+
index: tab.index,
|
|
2779
|
+
windowid: tab.windowid,
|
|
2780
|
+
active: tab.active,
|
|
2781
|
+
pinned: tab.pinned,
|
|
2782
|
+
audible: tab.audible,
|
|
2783
|
+
muted: tab.muted,
|
|
2784
|
+
discarded: tab.discarded,
|
|
2785
|
+
...metabytab.has(tab.tabid) ? { meta: metabytab.get(tab.tabid) } : {}
|
|
2786
|
+
}));
|
|
2787
|
+
return { matches: entries, groups: groups.map((group) => ({ name: group.name, color: group.color, tabids: group.tabids, collapsed: group.collapsed })), badges };
|
|
2788
|
+
}
|
|
2789
|
+
function commandtabids(step, options) {
|
|
2790
|
+
const listed = Array.isArray(options.tabs) ? options.tabs.filter((item) => typeof item === "number" && Number.isInteger(item) && item >= 0) : [];
|
|
2791
|
+
const single = step.value && /^\d+$/.test(step.value) ? [Number.parseInt(step.value, 10)] : [];
|
|
2792
|
+
return listed.length > 0 ? listed : single;
|
|
2793
|
+
}
|
|
2794
|
+
async function executetabscommand(step, session, plan, sessiontabid) {
|
|
2795
|
+
const options = stepoptions2(step);
|
|
2796
|
+
const extra = { ...session ? { sessionid: session.id } : {}, planid: plan.id, stepid: step.id };
|
|
2797
|
+
const windowid = step.value && /^\d+$/.test(step.value) ? Number.parseInt(step.value, 10) : 0;
|
|
2798
|
+
const layoutgate = layoutmutationgranted(session, Date.now());
|
|
2799
|
+
if (islayoutkind(step.kind) && !layoutgate.allowed) throw new Error(layoutgate.reason ?? "Group and layout mutations stay inside the active session.");
|
|
2800
|
+
switch (step.kind) {
|
|
2801
|
+
case "querytabs": {
|
|
2802
|
+
const query = parsetabquery(step);
|
|
2803
|
+
if (!query) throw new Error("A reviewed tabquery is required.");
|
|
2804
|
+
const matches = querymatches(query, await livetabs());
|
|
2805
|
+
const report = await buildtabreport(matches);
|
|
2806
|
+
await audit("tab", `Queried the live tab set and matched ${matches.length} tab${matches.length === 1 ? "" : "s"}.`, extra);
|
|
2807
|
+
return { ok: true, summary: `Matched ${matches.length} open tab${matches.length === 1 ? "" : "s"} by the reviewed tabquery.`, details: { report, matches: matches.length } };
|
|
2808
|
+
}
|
|
2809
|
+
case "duplicatetab": {
|
|
2810
|
+
const source = Number.parseInt(step.value ?? "", 10);
|
|
2811
|
+
const created = await chrome.tabs.duplicate(source);
|
|
2812
|
+
await audit("tab", `Duplicated tab ${source} with its history into tab ${created?.id ?? 0}.`, extra);
|
|
2813
|
+
return { ok: true, summary: `Duplicated tab ${source} with its history.`, details: { sourcetab: source, tabid: created?.id ?? 0 } };
|
|
2814
|
+
}
|
|
2815
|
+
case "closepattern": {
|
|
2816
|
+
const query = parsetabquery(step);
|
|
2817
|
+
if (!query) throw new Error("A reviewed tabquery is required.");
|
|
2818
|
+
const tabs = await livetabs();
|
|
2819
|
+
const selection = closeselection(query, tabs, session?.tabid ?? sessiontabid);
|
|
2820
|
+
if (selection.refused.length > 0) throw new Error("The close pattern matches the session tab itself; review the pattern so the session tab survives.");
|
|
2821
|
+
if (selection.targets.length === 0) return { ok: true, summary: "The reviewed close pattern matched no tab outside the session tab.", details: { closed: 0 } };
|
|
2822
|
+
for (const target of selection.targets) await chrome.tabs.remove(target.tabid).catch(() => void 0);
|
|
2823
|
+
await audit("tab", `Closed ${selection.targets.length} tab${selection.targets.length === 1 ? "" : "s"} matching the reviewed close pattern.`, extra);
|
|
2824
|
+
return { ok: true, summary: `Closed ${selection.targets.length} tab${selection.targets.length === 1 ? "" : "s"} matching the reviewed pattern.`, details: { closed: selection.targets.length, urls: selection.targets.map((tab) => tab.url) } };
|
|
2825
|
+
}
|
|
2826
|
+
case "pintab": {
|
|
2827
|
+
const target = Number.parseInt(step.value ?? "", 10);
|
|
2828
|
+
await chrome.tabs.update(target, { pinned: options.pinned === true });
|
|
2829
|
+
await audit("tab", `${options.pinned === true ? "Pinned" : "Unpinned"} tab ${target} by the reviewed flag.`, extra);
|
|
2830
|
+
return { ok: true, summary: `${options.pinned === true ? "Pinned" : "Unpinned"} tab ${target}.`, details: { tabid: target, pinned: options.pinned === true } };
|
|
2831
|
+
}
|
|
2832
|
+
case "mutetab": {
|
|
2833
|
+
const target = Number.parseInt(step.value ?? "", 10);
|
|
2834
|
+
await chrome.tabs.update(target, { muted: options.muted === true });
|
|
2835
|
+
await audit("tab", `${options.muted === true ? "Muted" : "Unmuted"} tab ${target} by the reviewed flag.`, extra);
|
|
2836
|
+
return { ok: true, summary: `${options.muted === true ? "Muted" : "Unmuted"} tab ${target}.`, details: { tabid: target, muted: options.muted === true } };
|
|
2837
|
+
}
|
|
2838
|
+
case "movetab": {
|
|
2839
|
+
const target = Number.parseInt(step.value ?? "", 10);
|
|
2840
|
+
await chrome.tabs.move(target, { index: options.index });
|
|
2841
|
+
const groups = regroupaftermoves(await memory.gettabgroups(), await livetabs(), Date.now());
|
|
2842
|
+
for (const group of groups) await memory.settabgroup(group);
|
|
2843
|
+
await audit("tab", `Moved tab ${target} to index ${options.index} inside its window; group membership is kept.`, extra);
|
|
2844
|
+
return { ok: true, summary: `Moved tab ${target} to index ${options.index}.`, details: { tabid: target, index: options.index } };
|
|
2845
|
+
}
|
|
2846
|
+
case "movetabwindow": {
|
|
2847
|
+
const target = Number.parseInt(step.value ?? "", 10);
|
|
2848
|
+
await chrome.tabs.move(target, { windowId: options.windowid, index: -1 });
|
|
2849
|
+
const groups = regroupaftermoves(await memory.gettabgroups(), await livetabs(), Date.now());
|
|
2850
|
+
for (const group of groups) await memory.settabgroup(group);
|
|
2851
|
+
await audit("tab", `Moved tab ${target} across windows into window ${options.windowid}; group membership is kept.`, extra);
|
|
2852
|
+
return { ok: true, summary: `Moved tab ${target} into window ${options.windowid}.`, details: { tabid: target, windowid: options.windowid } };
|
|
2853
|
+
}
|
|
2854
|
+
case "grouptabs": {
|
|
2855
|
+
const group = options.group;
|
|
2856
|
+
const record2 = { groupid: randomid(), name: String(group.name ?? ""), color: String(group.color ?? "grey"), tabids: Array.isArray(group.tabids) ? group.tabids.filter((item) => typeof item === "number") : [], collapsed: false, savedat: Date.now() };
|
|
2857
|
+
await memory.settabgroup(record2);
|
|
2858
|
+
await audit("group", `Grouped ${record2.tabids.length} tab${record2.tabids.length === 1 ? "" : "s"} under the reviewed name ${record2.name} with color ${record2.color}; membership lives in the Devthink group registry.`, extra);
|
|
2859
|
+
return { ok: true, summary: `Grouped ${record2.tabids.length} tab${record2.tabids.length === 1 ? "" : "s"} under ${record2.name}.`, details: { group: record2 } };
|
|
2860
|
+
}
|
|
2861
|
+
case "colorgroup": {
|
|
2862
|
+
const groups = await memory.gettabgroups();
|
|
2863
|
+
const target = groups.find((group) => group.name === options.name);
|
|
2864
|
+
if (!target) throw new Error(`No tab group named ${options.name} is stored yet.`);
|
|
2865
|
+
const updated = { ...target, color: String(options.color), savedat: Date.now() };
|
|
2866
|
+
await memory.settabgroup(updated);
|
|
2867
|
+
await audit("group", `Set the color of tab group ${updated.name} to ${updated.color}.`, extra);
|
|
2868
|
+
return { ok: true, summary: `Set the color of group ${updated.name} to ${updated.color}.`, details: { group: updated } };
|
|
2869
|
+
}
|
|
2870
|
+
case "collapsegroup": {
|
|
2871
|
+
const groups = await memory.gettabgroups();
|
|
2872
|
+
const target = groups.find((group) => group.name === options.name);
|
|
2873
|
+
if (!target) throw new Error(`No tab group named ${options.name} is stored yet.`);
|
|
2874
|
+
const updated = { ...target, collapsed: options.collapsed === true, savedat: Date.now() };
|
|
2875
|
+
await memory.settabgroup(updated);
|
|
2876
|
+
await audit("group", `${updated.collapsed ? "Collapsed" : "Expanded"} tab group ${updated.name}.`, extra);
|
|
2877
|
+
return { ok: true, summary: `${updated.collapsed ? "Collapsed" : "Expanded"} group ${updated.name}.`, details: { group: updated } };
|
|
2878
|
+
}
|
|
2879
|
+
case "discardtab": {
|
|
2880
|
+
const ids = commandtabids(step, options);
|
|
2881
|
+
if (ids.length === 0) throw new Error("A numeric tab id or a reviewed list of tab ids is required.");
|
|
2882
|
+
const candidates = discardcandidates(await livetabs()).filter((tab) => ids.includes(tab.tabid));
|
|
2883
|
+
const discarded = [];
|
|
2884
|
+
const urls = [];
|
|
2885
|
+
for (const candidate of candidates) {
|
|
2886
|
+
const result = await chrome.tabs.discard(candidate.tabid).catch(() => void 0);
|
|
2887
|
+
if (result) {
|
|
2888
|
+
discarded.push(candidate.tabid);
|
|
2889
|
+
urls.push({ tabid: candidate.tabid, url: candidate.url });
|
|
2890
|
+
}
|
|
2891
|
+
}
|
|
2892
|
+
await audit("discard", `Discarded ${discarded.length} inactive tab${discarded.length === 1 ? "" : "s"} to save memory; the urls survive for on demand restore.`, extra);
|
|
2893
|
+
return { ok: discarded.length > 0, summary: `Discarded ${discarded.length} inactive tab${discarded.length === 1 ? "" : "s"}; their urls stay available for restore.`, details: { discarded, urls, refused: ids.filter((id) => !discarded.includes(id)) } };
|
|
2894
|
+
}
|
|
2895
|
+
case "reloadtabs": {
|
|
2896
|
+
const ids = commandtabids(step, options);
|
|
2897
|
+
if (ids.length === 0) throw new Error("A numeric tab id or a reviewed list of tab ids is required.");
|
|
2898
|
+
for (const id of ids) await chrome.tabs.reload(id).catch(() => void 0);
|
|
2899
|
+
await audit("tab", `Reloaded ${ids.length} reviewed tab${ids.length === 1 ? "" : "s"}.`, extra);
|
|
2900
|
+
return { ok: true, summary: `Reloaded ${ids.length} tab${ids.length === 1 ? "" : "s"}.`, details: { tabs: ids } };
|
|
2901
|
+
}
|
|
2902
|
+
case "zoomin":
|
|
2903
|
+
case "zoomout": {
|
|
2904
|
+
const target = step.value && /^\d+$/.test(step.value) ? Number.parseInt(step.value, 10) : sessiontabid;
|
|
2905
|
+
const current = await chrome.tabs.getZoom(target);
|
|
2906
|
+
const next = zoomstep(current, step.kind === "zoomin" ? "in" : "out", typeof options.step === "number" && options.step > 0 ? options.step : 0.1);
|
|
2907
|
+
await chrome.tabs.setZoom(target, next);
|
|
2908
|
+
await audit("tab", `Zoomed tab ${target} ${step.kind === "zoomin" ? "in" : "out"} from ${current} to ${next} by the reviewed step.`, extra);
|
|
2909
|
+
return { ok: true, summary: `Zoomed tab ${target} ${step.kind === "zoomin" ? "in" : "out"} to ${next}.`, details: { tabid: target, from: current, to: next, step: typeof options.step === "number" ? options.step : 0.1 } };
|
|
2910
|
+
}
|
|
2911
|
+
case "switchtab": {
|
|
2912
|
+
const direction = options.direction === "previous" ? "previous" : "next";
|
|
2913
|
+
const focusedwindow = (await livewindows()).find((item) => item.focused)?.windowid ?? 0;
|
|
2914
|
+
const windowtabs = (await livetabs()).filter((tab) => tab.windowid === focusedwindow);
|
|
2915
|
+
const active = windowtabs.find((tab) => tab.active);
|
|
2916
|
+
const target = switchtarget(windowtabs, direction, active?.index ?? 0);
|
|
2917
|
+
const totab = windowtabs.find((tab) => tab.index === target);
|
|
2918
|
+
if (!totab) throw new Error("No neighbor tab is available to switch to.");
|
|
2919
|
+
await chrome.tabs.update(totab.tabid, { active: true });
|
|
2920
|
+
await audit("tab", `Switched to the ${direction} tab ${totab.tabid}.`, extra);
|
|
2921
|
+
return { ok: true, summary: `Switched to the ${direction} tab.`, details: { tabid: totab.tabid, direction } };
|
|
2922
|
+
}
|
|
2923
|
+
case "maximizewindow": {
|
|
2924
|
+
await chrome.windows.update(windowid, { state: "maximized" });
|
|
2925
|
+
await audit("window", `Maximized window ${windowid}.`, extra);
|
|
2926
|
+
return { ok: true, summary: `Maximized window ${windowid}.`, details: { windowid, state: "maximized" } };
|
|
2927
|
+
}
|
|
2928
|
+
case "minimizewindow": {
|
|
2929
|
+
await chrome.windows.update(windowid, { state: "minimized" });
|
|
2930
|
+
await audit("window", `Minimized window ${windowid}.`, extra);
|
|
2931
|
+
return { ok: true, summary: `Minimized window ${windowid}.`, details: { windowid, state: "minimized" } };
|
|
2932
|
+
}
|
|
2933
|
+
case "restorewindow": {
|
|
2934
|
+
const bounds = options.bounds;
|
|
2935
|
+
await chrome.windows.update(windowid, { state: "normal", ...bounds && typeof bounds.left === "number" ? { left: bounds.left } : {}, ...bounds && typeof bounds.top === "number" ? { top: bounds.top } : {}, ...bounds && typeof bounds.width === "number" ? { width: bounds.width } : {}, ...bounds && typeof bounds.height === "number" ? { height: bounds.height } : {} });
|
|
2936
|
+
await audit("window", `Restored window ${windowid} to its reviewed bounds.`, extra);
|
|
2937
|
+
return { ok: true, summary: `Restored window ${windowid} to its reviewed bounds.`, details: { windowid, bounds: bounds ?? null } };
|
|
2938
|
+
}
|
|
2939
|
+
case "focuswindow": {
|
|
2940
|
+
await chrome.windows.update(windowid, { focused: true });
|
|
2941
|
+
await audit("window", `Focused window ${windowid}.`, extra);
|
|
2942
|
+
return { ok: true, summary: `Focused window ${windowid}.`, details: { windowid } };
|
|
2943
|
+
}
|
|
2944
|
+
case "scratchwindow": {
|
|
2945
|
+
const url = typeof step.value === "string" && step.value ? step.value : "about:blank";
|
|
2946
|
+
const created = await chrome.windows.create({ url });
|
|
2947
|
+
const window2 = created?.id ?? 0;
|
|
2948
|
+
await memory.setscratchwindows([...await memory.getscratchwindows(), window2]);
|
|
2949
|
+
await audit("window", `Opened a scratch window ${window2} for split work.`, extra);
|
|
2950
|
+
return { ok: true, summary: `Opened a scratch window for split work.`, details: { windowid: window2, url } };
|
|
2951
|
+
}
|
|
2952
|
+
case "incognitowindow": {
|
|
2953
|
+
const created = await chrome.windows.create({ url: step.value ?? "", incognito: true });
|
|
2954
|
+
await audit("window", `Opened an incognito window ${created?.id ?? 0} for ${step.value} on the explicit reviewed request; the window stays separated from the session grant inheritance.`, extra);
|
|
2955
|
+
return { ok: true, summary: `Opened an incognito window for ${step.value} on explicit request.`, details: { windowid: created?.id ?? 0, url: step.value, grantsinherited: windowprofilegrants("incognito") } };
|
|
2956
|
+
}
|
|
2957
|
+
case "restoretab": {
|
|
2958
|
+
const open = (await livetabs()).map((tab) => tab.url).filter(Boolean);
|
|
2959
|
+
let url = step.value && /^https:\/\//.test(step.value) ? step.value : void 0;
|
|
2960
|
+
if (!url) {
|
|
2961
|
+
const closed = (await memory.getclosedtabs()).find((entry) => !open.includes(entry.url));
|
|
2962
|
+
if (!closed) throw new Error("No closed tab is available to restore from the session history.");
|
|
2963
|
+
url = closed.url;
|
|
2964
|
+
}
|
|
2965
|
+
const created = await chrome.tabs.create({ url, active: true });
|
|
2966
|
+
await audit("tab", `Restored the closed tab ${url} from the session history.`, extra);
|
|
2967
|
+
return { ok: true, summary: `Restored ${url} from the closed tab history.`, details: { url, tabid: created?.id ?? 0 } };
|
|
2968
|
+
}
|
|
2969
|
+
case "savelayout": {
|
|
2970
|
+
const name = typeof options.name === "string" ? options.name : "";
|
|
2971
|
+
const [tabs, windows, groups, scratch] = await Promise.all([livetabs(), livewindows(), memory.gettabgroups(), memory.getscratchwindows()]);
|
|
2972
|
+
const layout = buildlayout(name, tabs, windows, groups, scratch, Date.now());
|
|
2973
|
+
await memory.setlayout(layout);
|
|
2974
|
+
await audit("layout", `Saved the tab layout ${name} with ${layout.tabs.length} tab${layout.tabs.length === 1 ? "" : "s"}, ${layout.groups.length} group${layout.groups.length === 1 ? "" : "s"} and ${layout.windows.length} window bound${layout.windows.length === 1 ? "" : "s"}.`, extra);
|
|
2975
|
+
return { ok: true, summary: `Saved the tab layout ${name}.`, details: { layout } };
|
|
2976
|
+
}
|
|
2977
|
+
case "restorelayout": {
|
|
2978
|
+
const name = typeof options.name === "string" ? options.name : "";
|
|
2979
|
+
const layout = await memory.getlayout(name);
|
|
2980
|
+
if (!layout) throw new Error(`No tab layout named ${name} is stored yet.`);
|
|
2981
|
+
const open = (await livetabs()).map((tab) => tab.url).filter(Boolean);
|
|
2982
|
+
const urls = layoutrestoreplan(layout, open);
|
|
2983
|
+
const created = [];
|
|
2984
|
+
for (const url of urls) {
|
|
2985
|
+
const tab = await chrome.tabs.create({ url, active: created.length === 0 });
|
|
2986
|
+
created.push(tab?.id ?? 0);
|
|
2987
|
+
}
|
|
2988
|
+
await audit("layout", `Restored the tab layout ${name}: ${created.length} tab${created.length === 1 ? "" : "s"} reopened, ${layout.tabs.length - created.length} already open.`, extra);
|
|
2989
|
+
return { ok: true, summary: `Restored the tab layout ${name}.`, details: { name, reopened: created.length, alreadyopen: layout.tabs.length - created.length, tabs: created } };
|
|
2990
|
+
}
|
|
2991
|
+
case "findclones": {
|
|
2992
|
+
const clones = clonetabs(await livetabs());
|
|
2993
|
+
await audit("tab", `Detected ${clones.length} duplicate url group${clones.length === 1 ? "" : "s"} across the open tabs.`, extra);
|
|
2994
|
+
return { ok: true, summary: clones.length === 0 ? "No duplicate tab was detected by normalized url comparison." : `Detected ${clones.length} duplicate url group${clones.length === 1 ? "" : "s"}.`, details: { clones } };
|
|
2995
|
+
}
|
|
2996
|
+
case "searchtabs": {
|
|
2997
|
+
const matches = searchtabmatches(await livetabs(), step.value ?? "");
|
|
2998
|
+
await audit("tab", `Searched the open tabs and matched ${matches.length} tab${matches.length === 1 ? "" : "s"} for "${step.value}".`, extra);
|
|
2999
|
+
return { ok: true, summary: `Matched ${matches.length} open tab${matches.length === 1 ? "" : "s"} for "${step.value}".`, details: { matches } };
|
|
3000
|
+
}
|
|
3001
|
+
case "badgetab": {
|
|
3002
|
+
const target = Number.parseInt(step.value ?? "", 10);
|
|
3003
|
+
const badge = { tabid: target, taskid: typeof options.taskid === "string" && options.taskid ? options.taskid : plan.id, label: typeof options.label === "string" ? options.label : "", setat: Date.now() };
|
|
3004
|
+
await memory.setbadge(badge);
|
|
3005
|
+
await refreshbadge();
|
|
3006
|
+
await audit("badge", `Set the task badge of tab ${target} to ${badge.label} for task ${badge.taskid}.`, extra);
|
|
3007
|
+
return { ok: true, summary: `Set the badge of tab ${target} to ${badge.label}.`, details: { badge } };
|
|
3008
|
+
}
|
|
3009
|
+
case "attachmeta": {
|
|
3010
|
+
const target = Number.parseInt(step.value ?? "", 10);
|
|
3011
|
+
const meta = {
|
|
3012
|
+
tabid: target,
|
|
3013
|
+
taskrefs: Array.isArray(options.taskrefs) ? options.taskrefs.filter((item) => typeof item === "string" && item.trim().length > 0) : [],
|
|
3014
|
+
provenance: typeof options.provenance === "string" && options.provenance ? options.provenance : "plan step",
|
|
3015
|
+
labels: Array.isArray(options.labels) ? options.labels.filter((item) => typeof item === "string" && item.trim().length > 0) : [],
|
|
3016
|
+
at: Date.now()
|
|
3017
|
+
};
|
|
3018
|
+
await memory.settabmeta(meta);
|
|
3019
|
+
await memory.setprogress(assigntasktab(await memory.getprogress(), plan.id, target, Date.now()));
|
|
3020
|
+
await audit("tab", `Attached metadata to tab ${target} with ${meta.labels.length} label${meta.labels.length === 1 ? "" : "s"} and ${meta.taskrefs.length} task ref${meta.taskrefs.length === 1 ? "" : "s"}; the tab joins the task progress.`, extra);
|
|
3021
|
+
return { ok: true, summary: `Attached metadata to tab ${target} for task routing.`, details: { meta } };
|
|
3022
|
+
}
|
|
3023
|
+
case "listaudio": {
|
|
3024
|
+
const playing = audiotabs(await livetabs());
|
|
3025
|
+
await audit("tab", `Listed ${playing.length} tab${playing.length === 1 ? "" : "s"} that are playing audio.`, extra);
|
|
3026
|
+
return { ok: true, summary: `${playing.length} tab${playing.length === 1 ? " is" : "s are"} playing audio.`, details: { audio: playing } };
|
|
3027
|
+
}
|
|
3028
|
+
case "reopenrun": {
|
|
3029
|
+
const run = typeof options.run === "string" ? options.run : "";
|
|
3030
|
+
const snapshots = await memory.getsnapshots();
|
|
3031
|
+
const snapshot2 = snapshots.find((item) => item.sessionid === run) ?? snapshots.find((item) => item.id === run);
|
|
3032
|
+
if (!snapshot2) throw new Error(`No stored session snapshot exists for the run ${run}.`);
|
|
3033
|
+
const open = (await livetabs()).map((tab) => tab.url).filter(Boolean);
|
|
3034
|
+
const urls = layoutrestoreplan(snapshot2.layout, open);
|
|
3035
|
+
const created = [];
|
|
3036
|
+
for (const url of urls) {
|
|
3037
|
+
const tab = await chrome.tabs.create({ url, active: created.length === 0 });
|
|
3038
|
+
created.push(tab?.id ?? 0);
|
|
3039
|
+
}
|
|
3040
|
+
await audit("layout", `Reopened ${created.length} tab${created.length === 1 ? "" : "s"} of the previous run ${run}.`, extra);
|
|
3041
|
+
return { ok: true, summary: `Reopened ${created.length} tab${created.length === 1 ? "" : "s"} of the previous run.`, details: { run, reopened: created.length, tabs: created } };
|
|
3042
|
+
}
|
|
3043
|
+
case "snapshotsession": {
|
|
3044
|
+
const [tabs, windows, groups, scratch] = await Promise.all([livetabs(), livewindows(), memory.gettabgroups(), memory.getscratchwindows()]);
|
|
3045
|
+
const layout = buildlayout(`session ${(/* @__PURE__ */ new Date()).toISOString()}`, tabs, windows, groups, scratch, Date.now());
|
|
3046
|
+
const snapshot2 = { id: randomid(), ...session ? { sessionid: session.id } : {}, layout, capturedat: Date.now() };
|
|
3047
|
+
await memory.addsnapshot(snapshot2);
|
|
3048
|
+
await audit("layout", `Captured the full session snapshot with ${layout.tabs.length} tab${layout.tabs.length === 1 ? "" : "s"} and ${layout.windows.length} window${layout.windows.length === 1 ? "" : "s"}.`, extra);
|
|
3049
|
+
return { ok: true, summary: `Captured the session snapshot of ${layout.tabs.length} tabs and ${layout.windows.length} windows.`, details: { snapshot: snapshot2 } };
|
|
3050
|
+
}
|
|
3051
|
+
case "watchtab": {
|
|
3052
|
+
const lifetime = typeof options.lifetime === "number" && Number.isFinite(options.lifetime) && options.lifetime > 0 ? options.lifetime : 0;
|
|
3053
|
+
if (lifetime <= 0) throw new Error("A reviewed watch lifetime window in milliseconds is required in options.");
|
|
3054
|
+
const events = Array.isArray(options.events) ? options.events.filter((item) => typeof item === "string" && item.trim().length > 0) : [];
|
|
3055
|
+
const watchid = typeof options.watchid === "string" && options.watchid.trim() ? options.watchid : randomid();
|
|
3056
|
+
const watch = { watchid, kind: "watchtab", stepid: step.id, sessionid: session?.id ?? plan.id, origin: session?.origin ?? plan.origin, scopes: [], events, startedat: Date.now(), lifetime };
|
|
3057
|
+
await memory.addwatch(watch);
|
|
3058
|
+
await audit("watch", `Watchtab registered under id ${watchid} for the reviewed lifetime of ${lifetime} milliseconds${events.length > 0 ? ` over events ${events.join(", ")}` : " over title, activation and closure events"}.`, extra);
|
|
3059
|
+
await new Promise((resolve) => setTimeout(resolve, lifetime));
|
|
3060
|
+
const observed = watchtabdispatch(tabwatchbuffers.get(watchid) ?? [], watchid, events);
|
|
3061
|
+
await memory.closewatch(watchid, Date.now());
|
|
3062
|
+
tabwatchbuffers.delete(watchid);
|
|
3063
|
+
await audit("watch", `Watchtab ${watchid} closed after its reviewed lifetime of ${lifetime} milliseconds with ${observed.length} observed event${observed.length === 1 ? "" : "s"}.`, extra);
|
|
3064
|
+
return { ok: true, summary: `Observed ${observed.length} tab event${observed.length === 1 ? "" : "s"} inside the reviewed lifetime.`, details: { watchid, events: observed } };
|
|
3065
|
+
}
|
|
3066
|
+
default:
|
|
3067
|
+
return { ok: false, summary: "Unsupported tabs and windows command." };
|
|
3068
|
+
}
|
|
3069
|
+
}
|
|
3070
|
+
async function enforcewindowreview(step, session, plan) {
|
|
3071
|
+
const windowid = step.value && /^\d+$/.test(step.value) ? Number.parseInt(step.value, 10) : 0;
|
|
3072
|
+
const progress = plan ? await memory.getprogress() : void 0;
|
|
3073
|
+
const tasktabids = plan ? trackedtasktabs(progress, plan.id) : [];
|
|
3074
|
+
const count = tasktabsinwindow(await livetabs(), windowid, tasktabids);
|
|
3075
|
+
const gate = windowclosegate(count, stepoptions2(step).reviewed === true);
|
|
3076
|
+
if (!gate.allowed) throw new Error(gate.reason ?? "The window close needs explicit review.");
|
|
3077
|
+
if (session && count > 0) await audit("window", `Window ${windowid} closes while holding ${count} task tab${count === 1 ? "" : "s"} under the explicit reviewed flag.`, { ...session ? { sessionid: session.id } : {}, ...plan ? { planid: plan.id } : {}, stepid: step.id });
|
|
3078
|
+
}
|
|
3079
|
+
async function updatetaskbadges(plan, progress) {
|
|
3080
|
+
if (!plan || !progress || progress.planid !== plan.id) return;
|
|
3081
|
+
const state = badgefromprogress(progress.completedsteps.length, plan.steps.length);
|
|
3082
|
+
for (const tabid2 of tasktabs(progress, plan.id)) {
|
|
3083
|
+
await memory.setbadge({ tabid: tabid2, taskid: plan.id, label: state.label, setat: Date.now() });
|
|
3084
|
+
}
|
|
3085
|
+
}
|
|
3086
|
+
async function togglecontroltab(enabled) {
|
|
3087
|
+
const current = await memory.getcontroltab();
|
|
3088
|
+
if (current && current.tabid) await chrome.tabs.remove(current.tabid).catch(() => void 0);
|
|
3089
|
+
if (!enabled) {
|
|
3090
|
+
const closed = { tabid: 0, enabled: false, updatedat: Date.now() };
|
|
3091
|
+
await memory.setcontroltab(closed);
|
|
3092
|
+
await audit("tab", "The pinned control tab was closed.");
|
|
3093
|
+
return closed;
|
|
3094
|
+
}
|
|
3095
|
+
const created = await chrome.tabs.create({ url: chrome.runtime.getURL("sidepanel.html"), pinned: true, active: false });
|
|
3096
|
+
const state = { tabid: created?.id ?? 0, enabled: true, updatedat: Date.now() };
|
|
3097
|
+
await memory.setcontroltab(state);
|
|
3098
|
+
await audit("tab", `The pinned control tab ${state.tabid} was opened with the live task feed.`);
|
|
3099
|
+
return state;
|
|
3100
|
+
}
|
|
3101
|
+
async function refreshbadge() {
|
|
3102
|
+
const queues = await memory.getnavqueues();
|
|
3103
|
+
const badges = await memory.getbadges();
|
|
3104
|
+
const tasktabs2 = new Set(badges.map((badge) => badge.tabid)).size;
|
|
3105
|
+
const total = (queues?.prefetch ?? 0) + (queues?.batchopen ?? 0) + tasktabs2;
|
|
3106
|
+
await chrome.action.setBadgeText({ text: total > 0 ? String(total) : "" }).catch(() => {
|
|
3107
|
+
});
|
|
3108
|
+
}
|
|
1257
3109
|
async function executestep(stepid) {
|
|
1258
3110
|
const session = await memory.getsession();
|
|
1259
3111
|
const plan = await memory.getplan();
|
|
1260
3112
|
const { tab, origin } = await activecontext();
|
|
1261
3113
|
const step = plan?.steps.find((candidate) => candidate.id === stepid);
|
|
1262
3114
|
if (!step) throw new Error("Reviewed step was not found.");
|
|
1263
|
-
const gate = canexecute({ session, plan, step, tabid: tab.id, origin });
|
|
3115
|
+
const gate = canexecute({ session, plan, step, tabid: tab.id, origin, verdicts: await memory.getsafeties() });
|
|
1264
3116
|
if (!gate.allowed) throw new Error(gate.reason);
|
|
3117
|
+
const capability = requiredcapability(step.kind);
|
|
3118
|
+
if (capability) {
|
|
3119
|
+
const granted = await chrome.permissions.contains({ permissions: [capability] });
|
|
3120
|
+
if (!granted) throw new Error(`The ${capability} capability has not been granted; request it from the review panel.`);
|
|
3121
|
+
}
|
|
1265
3122
|
let output;
|
|
1266
3123
|
let watchwindow;
|
|
1267
|
-
if (
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
3124
|
+
if (step.kind === "windowclose") {
|
|
3125
|
+
await enforcewindowreview(step, session, plan);
|
|
3126
|
+
}
|
|
3127
|
+
if (istabscommandkind(step.kind)) {
|
|
3128
|
+
output = await executetabscommand(step, session, plan, tab.id);
|
|
3129
|
+
} else if (isbrowserkind(step.kind)) {
|
|
1273
3130
|
output = await runbrowseraction(step, tab.id, tab.windowId ?? chrome.windows.WINDOW_ID_CURRENT);
|
|
1274
3131
|
} else if (step.kind === "keyhold") {
|
|
1275
3132
|
output = await executekeyhold(step, session, plan, tab.id, origin);
|
|
@@ -1290,6 +3147,8 @@ async function executestep(stepid) {
|
|
|
1290
3147
|
watchwindow = { startedat: watched.watch.startedat, lifetime: watched.watch.lifetime };
|
|
1291
3148
|
} else if (step.kind === "diffsnapshots") {
|
|
1292
3149
|
output = await executediffsnapshots(step, session, plan, tab.id, origin);
|
|
3150
|
+
} else if (navigationstepkinds.has(step.kind)) {
|
|
3151
|
+
output = await executenavigationkind(step, session, plan, tab.id, origin);
|
|
1293
3152
|
} else {
|
|
1294
3153
|
if (step.target && freshcheckkinds.has(step.kind)) {
|
|
1295
3154
|
const fresh = await snapshot(tab.id);
|
|
@@ -1297,7 +3156,11 @@ async function executestep(stepid) {
|
|
|
1297
3156
|
}
|
|
1298
3157
|
output = await dispatchpagestep(step, tab.id, origin, plan);
|
|
1299
3158
|
}
|
|
3159
|
+
if (["navigate", "back", "forward"].includes(step.kind)) await recordnavigation(step, session, tab.id);
|
|
1300
3160
|
await recordevidence(step, output, session, plan, origin);
|
|
3161
|
+
if (output?.ok && plan && typeof output.details?.tabid === "number") {
|
|
3162
|
+
await memory.setprogress(assigntasktab(await memory.getprogress(), plan.id, output.details.tabid, Date.now()));
|
|
3163
|
+
}
|
|
1301
3164
|
const summary = output?.summary ?? "The page action returned no result.";
|
|
1302
3165
|
const resolved = output?.details?.resolvedtarget;
|
|
1303
3166
|
if (resolved) {
|
|
@@ -1312,6 +3175,8 @@ async function executestep(stepid) {
|
|
|
1312
3175
|
const completed = watchwindow ? recordwatchcompletion(base, plan.id, stepid, watchwindow.startedat, watchwindow.lifetime, Date.now()) : recordstep(base, plan.id, stepid, Date.now());
|
|
1313
3176
|
const tracked = recordoutcome(completed, plan.id, outcome, Date.now());
|
|
1314
3177
|
await memory.setprogress(tracked);
|
|
3178
|
+
await updatetaskbadges(plan, tracked);
|
|
3179
|
+
await refreshbadge();
|
|
1315
3180
|
if (iscomplete(tracked, plan) && plan.state === "approved") {
|
|
1316
3181
|
const done = { ...plan, state: "completed", completedat: Date.now() };
|
|
1317
3182
|
await memory.setplan(done);
|
|
@@ -1390,7 +3255,33 @@ async function handlerequest(message, sender) {
|
|
|
1390
3255
|
const a11y = (await memory.geta11ytrees())[0];
|
|
1391
3256
|
const reader = (await memory.getreaderarticles())[0];
|
|
1392
3257
|
const signals = await memory.getsignals();
|
|
1393
|
-
|
|
3258
|
+
const trail = session ? await memory.gettrail(session.id) : [];
|
|
3259
|
+
const navrecords = await memory.getnavrecords();
|
|
3260
|
+
const ratestates = await memory.getratestates();
|
|
3261
|
+
const safeties = await memory.getsafeties();
|
|
3262
|
+
const curated = await memory.getcurateds();
|
|
3263
|
+
const waitprofiles = await memory.getwaitprofiles();
|
|
3264
|
+
const auths = (await memory.getauths()).map((record2) => ({ origin: record2.origin, username: record2.username, reviewedat: record2.reviewedat }));
|
|
3265
|
+
const navcontrol = await memory.getnavcontrol();
|
|
3266
|
+
const navqueues = await memory.getnavqueues();
|
|
3267
|
+
const artifacts = await memory.getartifacts();
|
|
3268
|
+
const tabs = await livetabs().catch(() => []);
|
|
3269
|
+
const windows = await livewindows().catch(() => []);
|
|
3270
|
+
const layouts = await memory.getlayouts();
|
|
3271
|
+
const tabgroups = await memory.gettabgroups();
|
|
3272
|
+
const tabmetas = await memory.gettabmetas();
|
|
3273
|
+
const badges = await memory.getbadges();
|
|
3274
|
+
const snapshots = await memory.getsnapshots();
|
|
3275
|
+
const closedtabs = await memory.getclosedtabs();
|
|
3276
|
+
const controltab = await memory.getcontroltab();
|
|
3277
|
+
const tabwatchevents = await memory.gettabwatchevents();
|
|
3278
|
+
const clones = clonetabs(tabs);
|
|
3279
|
+
const taskgauge = tasktabgauge(tabs.filter((tab) => badges.some((badge) => badge.tabid === tab.tabid)).length, tasktabceiling(await memory.getsettings()));
|
|
3280
|
+
const report = await buildtabreport(tabs);
|
|
3281
|
+
const livetab = session ? await chrome.tabs.get(session.tabid).catch(() => void 0) : void 0;
|
|
3282
|
+
const waitprofile = session ? waitprofiles.find((record2) => record2.origin === session.origin) : void 0;
|
|
3283
|
+
const livestate = { phase: livetab?.status === "loading" ? "loading" : "complete", ...navrecords[0] ? { finalurl: navrecords[0].finalurl, redirects: navrecords[0].chain } : {} };
|
|
3284
|
+
return { config: await memory.getconfig(), session, plan, progress: plan && progress?.planid === plan.id ? progress : void 0, diagnostic: await memory.getdiagnostic(), audit: await memory.getaudit(), capabilities: await refreshcapabilities(), outcomes: await memory.getoutcomes(), holds: heldkeysreport({ tabid: session?.tabid ?? 0, holds }), dialogs: await memory.getdialogs(), retries: await memory.getretries(), ...signals ? { signals: signalsreport({ signals }) } : { signals: signalsreport({}) }, banners: await memory.getbanners(), mutationevents: await memory.getmutationevents(), focusevents: await memory.getfocusevents(), diffs: await memory.getdiffs(), selectors: await memory.getselectors(), ...a11y ? { a11y } : {}, ...reader ? { reader } : {}, ...map ? { map } : {}, trail: trailreport({ ...session ? { sessionid: session.id } : {}, trail }), navrecords, ratestates, safeties, curated, waitprofiles, auths, navcontrol, navqueues, artifacts, navstate: livestate, ...waitprofile ? { waitprofile } : {}, offline: !navigator.onLine, tabs, windows, layouts: layoutreport({ layouts }), tabgroups, tabmetas, badges, snapshots, closedtabs, tabwatchevents, clones, tasktabgauge: taskgauge, ...controltab ? { controltab } : {}, tabreport: report };
|
|
1394
3285
|
}
|
|
1395
3286
|
case "capabilities":
|
|
1396
3287
|
return refreshcapabilities();
|
|
@@ -1454,6 +3345,138 @@ async function handlerequest(message, sender) {
|
|
|
1454
3345
|
return pausesession();
|
|
1455
3346
|
case "resumesession":
|
|
1456
3347
|
return resumesession();
|
|
3348
|
+
case "storeauth": {
|
|
3349
|
+
const session = await memory.getsession();
|
|
3350
|
+
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.");
|
|
3351
|
+
const inputauth = message;
|
|
3352
|
+
const origin = new URL(inputauth.origin ?? "").origin;
|
|
3353
|
+
if (!origin.startsWith("https://")) throw new Error("Basic auth credentials need an HTTPS origin.");
|
|
3354
|
+
if (!inputauth.username?.trim() || !inputauth.password) throw new Error("Basic auth credentials need a username and a password.");
|
|
3355
|
+
const record2 = { origin, username: inputauth.username.trim(), password: inputauth.password, reviewedat: Date.now() };
|
|
3356
|
+
await memory.setauth(record2);
|
|
3357
|
+
await audit("auth", `Basic auth credentials for ${origin} stored after explicit review; the password never leaves local storage.`, { sessionid: session.id });
|
|
3358
|
+
return { origin: record2.origin, username: record2.username, reviewedat: record2.reviewedat };
|
|
3359
|
+
}
|
|
3360
|
+
case "checksafe": {
|
|
3361
|
+
const inputurl = message;
|
|
3362
|
+
const verdict = { ...checksafe(inputurl.url ?? ""), at: Date.now() };
|
|
3363
|
+
await memory.addsafety(verdict);
|
|
3364
|
+
const session = await memory.getsession();
|
|
3365
|
+
await audit("navigation", `Safety check of ${verdict.url} returned ${verdict.safe ? "safe" : "unsafe"}${verdict.reasons.length > 0 ? `: ${verdict.reasons.join("; ")}` : ""}.`, { ...session ? { sessionid: session.id } : {} });
|
|
3366
|
+
return verdict;
|
|
3367
|
+
}
|
|
3368
|
+
case "navstate": {
|
|
3369
|
+
const plan = await memory.getplan();
|
|
3370
|
+
if (!plan) throw new Error("No plan is available for a navstate envelope.");
|
|
3371
|
+
const record2 = (await memory.getnavrecords())[0];
|
|
3372
|
+
const session = await memory.getsession();
|
|
3373
|
+
const livetab = session ? await chrome.tabs.get(session.tabid).catch(() => void 0) : void 0;
|
|
3374
|
+
const state = { phase: livetab?.status === "loading" ? "loading" : "complete", ...record2 ? { finalurl: record2.finalurl, redirects: record2.chain } : {} };
|
|
3375
|
+
return JSON.parse(navstateresponse({ navstate: state, plan }));
|
|
3376
|
+
}
|
|
3377
|
+
case "safeties": {
|
|
3378
|
+
const plan = await memory.getplan();
|
|
3379
|
+
if (!plan) throw new Error("No plan is available for a safety envelope.");
|
|
3380
|
+
return JSON.parse(safetyresponse({ verdicts: await memory.getsafeties(), plan }));
|
|
3381
|
+
}
|
|
3382
|
+
case "jumptotab": {
|
|
3383
|
+
const inputtab = message;
|
|
3384
|
+
if (typeof inputtab.tabid !== "number") throw new Error("A numeric tab id is required to jump.");
|
|
3385
|
+
await chrome.tabs.update(inputtab.tabid, { active: true }).catch(() => {
|
|
3386
|
+
throw new Error("The tab to jump to is no longer open.");
|
|
3387
|
+
});
|
|
3388
|
+
await audit("tab", `The review panel jumped to tab ${inputtab.tabid}.`);
|
|
3389
|
+
return { tabid: inputtab.tabid };
|
|
3390
|
+
}
|
|
3391
|
+
case "tabsearch": {
|
|
3392
|
+
const inputsearch = message;
|
|
3393
|
+
const granted = await chrome.permissions.contains({ permissions: ["tabs"] });
|
|
3394
|
+
if (!granted) throw new Error("The tabs capability has not been granted; request it from the review panel.");
|
|
3395
|
+
const matches = searchtabmatches(await livetabs(), inputsearch.text ?? "");
|
|
3396
|
+
await audit("tab", `The review panel searched the open tabs for "${inputsearch.text ?? ""}" and matched ${matches.length} tab${matches.length === 1 ? "" : "s"}.`);
|
|
3397
|
+
return { matches };
|
|
3398
|
+
}
|
|
3399
|
+
case "savelayout": {
|
|
3400
|
+
const session = await memory.getsession();
|
|
3401
|
+
const gate = layoutmutationgranted(session, Date.now());
|
|
3402
|
+
if (!gate.allowed) throw new Error(gate.reason);
|
|
3403
|
+
const inputlayout = message;
|
|
3404
|
+
if (!inputlayout.name?.trim()) throw new Error("A layout name is required.");
|
|
3405
|
+
const [tabs, windows, groups, scratch] = await Promise.all([livetabs(), livewindows(), memory.gettabgroups(), memory.getscratchwindows()]);
|
|
3406
|
+
const layout = buildlayout(inputlayout.name.trim(), tabs, windows, groups, scratch, Date.now());
|
|
3407
|
+
await memory.setlayout(layout);
|
|
3408
|
+
await audit("layout", `The review panel saved the tab layout ${layout.name} with ${layout.tabs.length} tabs and ${layout.windows.length} window bounds.`, { ...session ? { sessionid: session.id } : {} });
|
|
3409
|
+
return layout;
|
|
3410
|
+
}
|
|
3411
|
+
case "restorelayout": {
|
|
3412
|
+
const session = await memory.getsession();
|
|
3413
|
+
const gate = layoutmutationgranted(session, Date.now());
|
|
3414
|
+
if (!gate.allowed) throw new Error(gate.reason);
|
|
3415
|
+
const inputlayout = message;
|
|
3416
|
+
const layout = await memory.getlayout(inputlayout.name ?? "");
|
|
3417
|
+
if (!layout) throw new Error(`No tab layout named ${inputlayout.name ?? ""} is stored yet.`);
|
|
3418
|
+
const open = (await livetabs()).map((tab) => tab.url).filter(Boolean);
|
|
3419
|
+
const urls = layoutrestoreplan(layout, open);
|
|
3420
|
+
const opened = [];
|
|
3421
|
+
for (const url of urls) {
|
|
3422
|
+
const created = await chrome.tabs.create({ url, active: opened.length === 0 });
|
|
3423
|
+
opened.push(created?.id ?? 0);
|
|
3424
|
+
}
|
|
3425
|
+
await audit("layout", `The review panel restored the tab layout ${layout.name}: ${opened.length} tab${opened.length === 1 ? "" : "s"} reopened.`, { ...session ? { sessionid: session.id } : {} });
|
|
3426
|
+
return { name: layout.name, reopened: opened.length };
|
|
3427
|
+
}
|
|
3428
|
+
case "restoresnapshot": {
|
|
3429
|
+
const session = await memory.getsession();
|
|
3430
|
+
if (!session || session.stoppedat || session.expiresat <= Date.now()) throw new Error("Snapshot restore stays behind the consent gate of an active session.");
|
|
3431
|
+
const inputsnapshot = message;
|
|
3432
|
+
const snapshot2 = (await memory.getsnapshots()).find((item) => item.id === inputsnapshot.id);
|
|
3433
|
+
if (!snapshot2) throw new Error("No stored session snapshot matches the requested id.");
|
|
3434
|
+
const open = (await livetabs()).map((tab) => tab.url).filter(Boolean);
|
|
3435
|
+
const urls = layoutrestoreplan(snapshot2.layout, open);
|
|
3436
|
+
const opened = [];
|
|
3437
|
+
for (const url of urls) {
|
|
3438
|
+
const created = await chrome.tabs.create({ url, active: opened.length === 0 });
|
|
3439
|
+
opened.push(created?.id ?? 0);
|
|
3440
|
+
}
|
|
3441
|
+
await audit("layout", `The review panel restored the session snapshot ${snapshot2.id}: ${opened.length} tab${opened.length === 1 ? "" : "s"} reopened.`, { sessionid: session.id });
|
|
3442
|
+
return { id: snapshot2.id, reopened: opened.length };
|
|
3443
|
+
}
|
|
3444
|
+
case "controltab": {
|
|
3445
|
+
const inputcontrol = message;
|
|
3446
|
+
const state = await togglecontroltab(inputcontrol.enabled === true);
|
|
3447
|
+
const settings = await memory.getsettings();
|
|
3448
|
+
await memory.setsettings({ ...settings, controltab: state.enabled });
|
|
3449
|
+
return state;
|
|
3450
|
+
}
|
|
3451
|
+
case "settasktabceiling": {
|
|
3452
|
+
const inputceiling = message;
|
|
3453
|
+
const settings = await memory.getsettings();
|
|
3454
|
+
const ceiling = typeof inputceiling.ceiling === "number" && Number.isFinite(inputceiling.ceiling) && inputceiling.ceiling >= 0 ? inputceiling.ceiling : void 0;
|
|
3455
|
+
await memory.setsettings({ ...settings, ...ceiling !== void 0 ? { tasktabceiling: ceiling } : {} });
|
|
3456
|
+
await audit("configure", `The user set the concurrent task tab ceiling to ${ceiling === void 0 ? "no ceiling" : ceiling}; the value stays a user choice with no code cap.`);
|
|
3457
|
+
return { tasktabceiling: ceiling };
|
|
3458
|
+
}
|
|
3459
|
+
case "windowstate": {
|
|
3460
|
+
const inputwindow = message;
|
|
3461
|
+
if (typeof inputwindow.windowid !== "number" || !inputwindow.state || !["normal", "maximized", "minimized", "fullscreen"].includes(inputwindow.state)) throw new Error("A numeric window id and a known window state are required.");
|
|
3462
|
+
await chrome.windows.update(inputwindow.windowid, { state: inputwindow.state });
|
|
3463
|
+
await audit("window", `The popup set window ${inputwindow.windowid} to the ${inputwindow.state} state.`);
|
|
3464
|
+
return { windowid: inputwindow.windowid, state: inputwindow.state };
|
|
3465
|
+
}
|
|
3466
|
+
case "closewindow": {
|
|
3467
|
+
const inputclose = message;
|
|
3468
|
+
if (typeof inputclose.windowid !== "number") throw new Error("A numeric window id is required.");
|
|
3469
|
+
const session = await memory.getsession();
|
|
3470
|
+
const plan = await memory.getplan();
|
|
3471
|
+
const progress = plan ? await memory.getprogress() : void 0;
|
|
3472
|
+
const tasktabids = plan && progress ? trackedtasktabs(progress, plan.id) : [];
|
|
3473
|
+
const count = tasktabsinwindow(await livetabs(), inputclose.windowid, tasktabids);
|
|
3474
|
+
const gate = windowclosegate(count, inputclose.reviewed === true);
|
|
3475
|
+
if (!gate.allowed) throw new Error(gate.reason);
|
|
3476
|
+
await chrome.windows.remove(inputclose.windowid);
|
|
3477
|
+
await audit("window", `The review panel closed window ${inputclose.windowid}${count > 0 ? ` while holding ${count} task tab${count === 1 ? "" : "s"} under explicit review` : ""}.`, { ...session ? { sessionid: session.id } : {} });
|
|
3478
|
+
return { windowid: inputclose.windowid, closed: true };
|
|
3479
|
+
}
|
|
1457
3480
|
case "stop": {
|
|
1458
3481
|
const session = await memory.getsession();
|
|
1459
3482
|
if (session) await memory.setsession({ ...session, stoppedat: Date.now() });
|