@wenathlan/extension 1.1.33 → 1.1.35
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -5
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +483 -5
- package/dist/index.js.map +3 -3
- package/dist/memory.d.ts +101 -1
- package/dist/memory.d.ts.map +1 -1
- package/dist/policy.d.ts +12 -1
- package/dist/policy.d.ts.map +1 -1
- package/dist/protocol.d.ts +57 -1
- package/dist/protocol.d.ts.map +1 -1
- package/dist/types.d.ts +320 -3
- package/dist/types.d.ts.map +1 -1
- package/dist/version.d.ts +1 -1
- package/extension/dist/background.js +1492 -14
- package/extension/dist/background.js.map +4 -4
- package/extension/dist/manifest.json +1 -1
- package/extension/dist/pagebridge.js +1255 -6
- package/extension/dist/pagebridge.js.map +4 -4
- package/extension/dist/popup.html +1 -1
- package/extension/dist/popup.js +16 -5
- package/extension/dist/popup.js.map +2 -2
- package/extension/dist/sidepanel.html +1 -1
- package/extension/dist/sidepanel.js +379 -10
- package/extension/dist/sidepanel.js.map +3 -3
- package/extension/dist/style.css +2 -1
- package/extension/manifest.json +1 -1
- package/package.json +1 -1
|
@@ -128,18 +128,243 @@ var sessionmemory = class {
|
|
|
128
128
|
async setdialogpolicy(policy) {
|
|
129
129
|
return this.adapter.set("dialogpolicy", policy);
|
|
130
130
|
}
|
|
131
|
+
/** Stores one observation capture under its version so every observation version stays available. */
|
|
132
|
+
async setobservation(record2) {
|
|
133
|
+
return this.adapter.set(`observation${record2.version}`, record2);
|
|
134
|
+
}
|
|
135
|
+
/** Returns one stored observation version. */
|
|
136
|
+
async getobservation(version) {
|
|
137
|
+
return this.adapter.get(`observation${version}`);
|
|
138
|
+
}
|
|
139
|
+
/** Returns the observation retention window; an absent setting keeps every capture. */
|
|
140
|
+
async observationretention() {
|
|
141
|
+
return (await this.getsettings())?.observationretention;
|
|
142
|
+
}
|
|
143
|
+
/** Stores one accessibility tree capture; retention is a user setting and an absent setting keeps every tree. */
|
|
144
|
+
async adda11ytree(capture) {
|
|
145
|
+
const records = await this.geta11ytrees();
|
|
146
|
+
const combined = [capture, ...records];
|
|
147
|
+
const retention = await this.observationretention();
|
|
148
|
+
await this.adapter.set("a11ytrees", retention === void 0 ? combined : combined.slice(0, retention));
|
|
149
|
+
}
|
|
150
|
+
/** Returns every stored accessibility tree capture, newest first. */
|
|
151
|
+
async geta11ytrees() {
|
|
152
|
+
return await this.adapter.get("a11ytrees") ?? [];
|
|
153
|
+
}
|
|
154
|
+
/** Stores one reader article capture; retention is a user setting and an absent setting keeps every article. */
|
|
155
|
+
async addreaderarticle(capture) {
|
|
156
|
+
const records = await this.getreaderarticles();
|
|
157
|
+
const combined = [capture, ...records];
|
|
158
|
+
const retention = await this.observationretention();
|
|
159
|
+
await this.adapter.set("readerarticles", retention === void 0 ? combined : combined.slice(0, retention));
|
|
160
|
+
}
|
|
161
|
+
/** Returns every stored reader article capture, newest first. */
|
|
162
|
+
async getreaderarticles() {
|
|
163
|
+
return await this.adapter.get("readerarticles") ?? [];
|
|
164
|
+
}
|
|
165
|
+
/** Records one dom mutation observed inside a reviewed watch. */
|
|
166
|
+
async addmutationevent(event) {
|
|
167
|
+
const records = await this.getmutationevents();
|
|
168
|
+
await this.adapter.set("mutationevents", [event, ...records]);
|
|
169
|
+
}
|
|
170
|
+
/** Returns the mutation event stream of every reviewed watch. */
|
|
171
|
+
async getmutationevents() {
|
|
172
|
+
return await this.adapter.get("mutationevents") ?? [];
|
|
173
|
+
}
|
|
174
|
+
/** Records one focus change observed inside a reviewed watch. */
|
|
175
|
+
async addfocusevent(event) {
|
|
176
|
+
const records = await this.getfocusevents();
|
|
177
|
+
await this.adapter.set("focusevents", [event, ...records]);
|
|
178
|
+
}
|
|
179
|
+
/** Returns the focus event stream of every reviewed watch. */
|
|
180
|
+
async getfocusevents() {
|
|
181
|
+
return await this.adapter.get("focusevents") ?? [];
|
|
182
|
+
}
|
|
183
|
+
/** Records one consent banner observed by a reviewed banner watch. */
|
|
184
|
+
async addbanner(event) {
|
|
185
|
+
const records = await this.getbanners();
|
|
186
|
+
await this.adapter.set("banners", [event, ...records]);
|
|
187
|
+
}
|
|
188
|
+
/** Returns every consent banner report observed so far. */
|
|
189
|
+
async getbanners() {
|
|
190
|
+
return await this.adapter.get("banners") ?? [];
|
|
191
|
+
}
|
|
192
|
+
/** Records one snapshot diff between two observation versions. */
|
|
193
|
+
async adddiff(diff) {
|
|
194
|
+
const records = await this.getdiffs();
|
|
195
|
+
await this.adapter.set("diffs", [diff, ...records]);
|
|
196
|
+
}
|
|
197
|
+
/** Returns every stored snapshot diff, newest first. */
|
|
198
|
+
async getdiffs() {
|
|
199
|
+
return await this.adapter.get("diffs") ?? [];
|
|
200
|
+
}
|
|
201
|
+
/** Records one derived selector with its stability score for reuse. */
|
|
202
|
+
async addselector(selector) {
|
|
203
|
+
const records = await this.getselectors();
|
|
204
|
+
await this.adapter.set("selectors", [selector, ...records]);
|
|
205
|
+
}
|
|
206
|
+
/** Returns every stored derived selector with its stability score, newest first. */
|
|
207
|
+
async getselectors() {
|
|
208
|
+
return await this.adapter.get("selectors") ?? [];
|
|
209
|
+
}
|
|
210
|
+
/** Records one detected template class or section fingerprint for its origin. */
|
|
211
|
+
async addtemplate(profile) {
|
|
212
|
+
const records = await this.gettemplates();
|
|
213
|
+
await this.adapter.set("templates", [profile, ...records]);
|
|
214
|
+
}
|
|
215
|
+
/** Returns every stored template class and section fingerprint, newest first. */
|
|
216
|
+
async gettemplates() {
|
|
217
|
+
return await this.adapter.get("templates") ?? [];
|
|
218
|
+
}
|
|
219
|
+
/** Records one watch registration so it survives service worker restarts. */
|
|
220
|
+
async addwatch(watch) {
|
|
221
|
+
const records = await this.getwatches();
|
|
222
|
+
await this.adapter.set("watches", [watch, ...records]);
|
|
223
|
+
}
|
|
224
|
+
/** Returns every watch registration, newest first, including closed windows. */
|
|
225
|
+
async getwatches() {
|
|
226
|
+
return await this.adapter.get("watches") ?? [];
|
|
227
|
+
}
|
|
228
|
+
/** Closes one watch registration by watch id once its reviewed lifetime window ends. */
|
|
229
|
+
async closewatch(watchid, closedat) {
|
|
230
|
+
const records = await this.getwatches();
|
|
231
|
+
await this.adapter.set("watches", records.map((watch) => watch.watchid === watchid && watch.closedat === void 0 ? { ...watch, closedat } : watch));
|
|
232
|
+
}
|
|
233
|
+
/** Returns the live page signals of language, template, scroll lock and banner state. */
|
|
234
|
+
async getsignals() {
|
|
235
|
+
return this.adapter.get("signals");
|
|
236
|
+
}
|
|
237
|
+
/** Replaces the live page signals after an observation step refreshes them. */
|
|
238
|
+
async setsignals(signals) {
|
|
239
|
+
return this.adapter.set("signals", signals);
|
|
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
|
+
}
|
|
131
355
|
};
|
|
132
356
|
function randomid() {
|
|
133
357
|
return crypto.randomUUID();
|
|
134
358
|
}
|
|
135
359
|
|
|
136
360
|
// policy.ts
|
|
137
|
-
var sensitiveactions = /* @__PURE__ */ new Set(["click", "type", "navigate", "select", "presskey", "drag", "drop", "upload", "clear", "check", "uncheck", "toggle", "submit", "reload", "back", "forward", "writestorage", "setattribute", "removeattribute", "evaluate", "tabcreate", "tabactivate", "tabclose", "tabreload", "windowcreate", "windowclose", "windowresize", "downloadfile", "clickpoint", "shiftclick", "dismissdialog", "enterframe", "typetime", "appendtext", "setvalue", "typeedit", "keyhold", "keyrelease", "submitsearch", "selectmulti", "chooseradio", "setslider", "setdate", "setcolor"]);
|
|
361
|
+
var sensitiveactions = /* @__PURE__ */ new Set(["click", "type", "navigate", "select", "presskey", "drag", "drop", "upload", "clear", "check", "uncheck", "toggle", "submit", "reload", "back", "forward", "writestorage", "setattribute", "removeattribute", "evaluate", "tabcreate", "tabactivate", "tabclose", "tabreload", "windowcreate", "windowclose", "windowresize", "downloadfile", "clickpoint", "shiftclick", "dismissdialog", "enterframe", "typetime", "appendtext", "setvalue", "typeedit", "keyhold", "keyrelease", "submitsearch", "selectmulti", "chooseradio", "setslider", "setdate", "setcolor", "openlink", "openprivate", "reloadcache", "stopnav", "followlink", "spanav", "rewritequery", "setfragment", "navlist", "navprofile", "handleauth", "printpdf", "prefetch", "preconnect", "deeplink", "reopentab", "pausenav", "navrate", "openclipboard", "batchopen"]);
|
|
138
362
|
var interactionactions = /* @__PURE__ */ new Set(["focus", "scroll", "hover", "clickdeep", "rightclick", "doubleclick", "scrollpage", "scrollby", "scrollend", "scrolltop", "fullscreen", "zoomset", "movepointer", "clicktext", "clickaria", "clickname", "expanddetails", "pierceshadow", "retryaction"]);
|
|
139
|
-
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"]);
|
|
363
|
+
var readactions = /* @__PURE__ */ new Set(["observe", "inspect", "extract", "wait", "waitfor", "waittext", "readattribute", "readstyle", "readgeometry", "readvalue", "readtext", "readhtml", "countelements", "readtable", "readlinks", "readimages", "readmeta", "readforms", "readstorage", "highlight", "tablist", "windowlist", "tabsnapshot", "mapclicks", "verifyvisible", "verifyenabled", "resolvexpath", "a11ytree", "readvisible", "readertree", "detectlists", "detecttables", "readjson", "watchmutate", "waitquiet", "watchbanner", "detectinfinitescroll", "detectvirtual", "detectlazy", "readscrollpos", "readlang", "readoutline", "countpages", "listshadow", "listframes", "classifypage", "fingerprintsection", "diffsnapshots", "readselection", "watchfocus", "detectsticky", "detectscrolllock", "readopengraph", "detectlanguage", "deriveselector", "waitload", "waiturl", "spawait", "detecthttp", "readredirects", "readfinalurl", "trailaudit", "navintent", "checksafe"]);
|
|
140
364
|
var allowedactions = /* @__PURE__ */ new Set([...sensitiveactions, ...interactionactions, ...readactions]);
|
|
141
|
-
var
|
|
142
|
-
var
|
|
365
|
+
var watchactions = /* @__PURE__ */ new Set(["watchmutate", "watchbanner", "watchfocus"]);
|
|
366
|
+
var targetactions = /* @__PURE__ */ new Set(["inspect", "focus", "click", "type", "scroll", "select", "hover", "clickdeep", "rightclick", "doubleclick", "drag", "drop", "upload", "clear", "check", "uncheck", "toggle", "submit", "readattribute", "readstyle", "readgeometry", "readvalue", "readtext", "readhtml", "countelements", "readtable", "highlight", "setattribute", "removeattribute", "waitfor", "shiftclick", "typetime", "appendtext", "setvalue", "typeedit", "submitsearch", "selectmulti", "chooseradio", "setslider", "setdate", "setcolor", "expanddetails", "verifyvisible", "verifyenabled", "pierceshadow", "deriveselector", "fingerprintsection"]);
|
|
367
|
+
var valueactions = /* @__PURE__ */ new Set(["presskey", "drag", "drop", "upload", "readattribute", "removeattribute", "waittext", "evaluate", "zoomset", "tabactivate", "tabclose", "tabreload", "windowclose", "windowresize", "tabcreate", "windowcreate", "downloadfile", "typetime", "appendtext", "setvalue", "typeedit", "keyhold", "keyrelease", "chooseradio", "setslider", "setdate", "setcolor", "followlink", "setfragment", "handleauth", "navintent", "openclipboard", "checksafe", "reopentab", "spanav"]);
|
|
143
368
|
function normalizeendpoint(value) {
|
|
144
369
|
const endpoint = new URL(value.trim());
|
|
145
370
|
if (endpoint.protocol !== "https:") throw new Error("Devthink accepts HTTPS endpoints only.");
|
|
@@ -170,6 +395,8 @@ function parseoptions(step) {
|
|
|
170
395
|
function requiredcapability(kind) {
|
|
171
396
|
if (kind === "tablist") return "tabs";
|
|
172
397
|
if (kind === "downloadfile") return "downloads";
|
|
398
|
+
if (kind === "openclipboard") return "clipboardRead";
|
|
399
|
+
if (kind === "openlink" || kind === "openprivate" || kind === "navlist" || kind === "batchopen" || kind === "reopentab" || kind === "deeplink") return "tabs";
|
|
173
400
|
return void 0;
|
|
174
401
|
}
|
|
175
402
|
function waitduration(step) {
|
|
@@ -220,6 +447,35 @@ function origingranted(session, origin) {
|
|
|
220
447
|
const grants = session.grants ?? [session.origin];
|
|
221
448
|
return grants.includes(origin);
|
|
222
449
|
}
|
|
450
|
+
function originverified(url, grants, verdicts) {
|
|
451
|
+
let origin = "";
|
|
452
|
+
try {
|
|
453
|
+
origin = new URL(url).origin;
|
|
454
|
+
} catch {
|
|
455
|
+
return { allowed: false, reason: "The reviewed navigation URL is invalid." };
|
|
456
|
+
}
|
|
457
|
+
if (grants.includes(origin)) return { allowed: true };
|
|
458
|
+
const covered = verdicts.find((verdict) => verdict.safe && (verdict.url === url || safeorigin(verdict.url) === origin));
|
|
459
|
+
if (covered) return { allowed: true };
|
|
460
|
+
return { allowed: false, reason: `The origin ${origin} is outside the session grants and has no safe checksafe verdict; run checksafe and review it first.` };
|
|
461
|
+
}
|
|
462
|
+
function safeorigin(url) {
|
|
463
|
+
try {
|
|
464
|
+
return new URL(url).origin;
|
|
465
|
+
} catch {
|
|
466
|
+
return "";
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
function navigationgranted(session, url) {
|
|
470
|
+
let origin = "";
|
|
471
|
+
try {
|
|
472
|
+
origin = new URL(url).origin;
|
|
473
|
+
} catch {
|
|
474
|
+
return { allowed: false, reason: "The reviewed navigation URL is invalid." };
|
|
475
|
+
}
|
|
476
|
+
if (origingranted(session, origin)) return { allowed: true };
|
|
477
|
+
return { allowed: false, reason: `Navigation to ${origin} leaves the task tab origins and needs the user consent of a session grant first.` };
|
|
478
|
+
}
|
|
223
479
|
function validateinnerstep(options, origin) {
|
|
224
480
|
const stepid = options.stepid;
|
|
225
481
|
const kind = options.kind;
|
|
@@ -243,6 +499,68 @@ function validateinnerstep(options, origin) {
|
|
|
243
499
|
};
|
|
244
500
|
return validatestep(inner, origin);
|
|
245
501
|
}
|
|
502
|
+
function ishttpsurl(value) {
|
|
503
|
+
if (typeof value !== "string" || !value.trim()) return false;
|
|
504
|
+
try {
|
|
505
|
+
return new URL(value).protocol === "https:";
|
|
506
|
+
} catch {
|
|
507
|
+
return false;
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
function validatenavtarget(value, kind) {
|
|
511
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return { allowed: false, reason: "A reviewed navtarget with a url is required in options." };
|
|
512
|
+
const target = value;
|
|
513
|
+
if (!ishttpsurl(target.url)) return { allowed: false, reason: "The reviewed navtarget url must use HTTPS." };
|
|
514
|
+
const container = target.container ?? "tab";
|
|
515
|
+
if (container !== "current" && container !== "tab" && container !== "window" && container !== "private") return { allowed: false, reason: "The reviewed navtarget container must be current, tab, window or private." };
|
|
516
|
+
if (target.position !== void 0 && target.position !== "adjacent" && target.position !== "end") return { allowed: false, reason: "The reviewed navtarget position must be adjacent or end." };
|
|
517
|
+
if (kind === "openprivate" && container !== "private") return { allowed: false, reason: "The openprivate step requires the private container." };
|
|
518
|
+
if (kind === "openlink" && container === "private") return { allowed: false, reason: "The openlink step cannot open the private container; use openprivate." };
|
|
519
|
+
return { allowed: true };
|
|
520
|
+
}
|
|
521
|
+
function validatewaitprofile(value) {
|
|
522
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return { allowed: false, reason: "A reviewed waitprofile with load signals is required in options." };
|
|
523
|
+
const profile = value;
|
|
524
|
+
if (!Array.isArray(profile.signals) || profile.signals.length === 0 || !profile.signals.every((signal) => isnonempty(signal))) return { allowed: false, reason: "The reviewed waitprofile needs a non-empty list of load signals." };
|
|
525
|
+
if (!nonnegativeoption(profile, "idle")) return { allowed: false, reason: "The reviewed waitprofile idle threshold must be zero or a positive number of milliseconds." };
|
|
526
|
+
if (!nonnegativeoption(profile, "timeout")) return { allowed: false, reason: "The reviewed waitprofile timeout must be zero or a positive number of milliseconds." };
|
|
527
|
+
if (profile.overrides !== void 0) {
|
|
528
|
+
if (!Array.isArray(profile.overrides) || profile.overrides.length === 0) return { allowed: false, reason: "The reviewed waitprofile overrides must be a non-empty list when present." };
|
|
529
|
+
for (const entry of profile.overrides) {
|
|
530
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) return { allowed: false, reason: "Every reviewed waitprofile override must be an object with an origin." };
|
|
531
|
+
const override = entry;
|
|
532
|
+
if (!ishttpsurl(override.origin)) return { allowed: false, reason: "Every reviewed waitprofile override origin must use HTTPS." };
|
|
533
|
+
if (override.signals !== void 0 && (!Array.isArray(override.signals) || !override.signals.every((signal) => isnonempty(signal)))) return { allowed: false, reason: "The reviewed waitprofile override signals must be a list of non-empty strings." };
|
|
534
|
+
if (!nonnegativeoption(override, "idle") || !nonnegativeoption(override, "timeout")) return { allowed: false, reason: "The reviewed waitprofile override thresholds must be zero or positive numbers." };
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
return { allowed: true };
|
|
538
|
+
}
|
|
539
|
+
function validateurlpattern(value) {
|
|
540
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return { allowed: false, reason: "A reviewed urlpattern is required in options." };
|
|
541
|
+
const pattern = value;
|
|
542
|
+
if (pattern.mode !== "exact" && pattern.mode !== "prefix" && pattern.mode !== "host" && pattern.mode !== "pattern") return { allowed: false, reason: "The reviewed urlpattern mode must be exact, prefix, host or pattern." };
|
|
543
|
+
if (!ishttpsurl(pattern.url)) return { allowed: false, reason: "The reviewed urlpattern url must use HTTPS." };
|
|
544
|
+
if (pattern.query !== void 0) {
|
|
545
|
+
if (!pattern.query || typeof pattern.query !== "object" || Array.isArray(pattern.query)) return { allowed: false, reason: "The reviewed urlpattern query part must be an object of parameter names and values." };
|
|
546
|
+
for (const item of Object.values(pattern.query)) if (typeof item !== "string") return { allowed: false, reason: "The reviewed urlpattern query values must be strings." };
|
|
547
|
+
}
|
|
548
|
+
if (pattern.fragment !== void 0 && !isnonempty(pattern.fragment)) return { allowed: false, reason: "The reviewed urlpattern fragment must be a non-empty string." };
|
|
549
|
+
return { allowed: true };
|
|
550
|
+
}
|
|
551
|
+
function validateurllist(options, key) {
|
|
552
|
+
const urls = options[key];
|
|
553
|
+
if (!Array.isArray(urls) || urls.length === 0 || !urls.every((url) => ishttpsurl(url))) return { allowed: false, reason: `A reviewed non-empty list of HTTPS urls is required in options as ${key}.` };
|
|
554
|
+
return { allowed: true };
|
|
555
|
+
}
|
|
556
|
+
function validateratelimit(value) {
|
|
557
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return { allowed: false, reason: "A reviewed ratelimit with a window and a ceiling is required in options." };
|
|
558
|
+
const limit = value;
|
|
559
|
+
if (limit.domain !== void 0 && !isnonempty(limit.domain)) return { allowed: false, reason: "The reviewed ratelimit domain must be a non-empty string." };
|
|
560
|
+
if (typeof limit.window !== "number" || !Number.isFinite(limit.window) || limit.window <= 0) return { allowed: false, reason: "The reviewed ratelimit window must be a positive number of milliseconds with no code ceiling." };
|
|
561
|
+
if (typeof limit.ceiling !== "number" || !Number.isInteger(limit.ceiling) || limit.ceiling < 1) return { allowed: false, reason: "The reviewed ratelimit ceiling must be a positive integer with no code ceiling." };
|
|
562
|
+
return { allowed: true };
|
|
563
|
+
}
|
|
246
564
|
function validatestep(step, origin) {
|
|
247
565
|
if (!allowedactions.has(step.kind)) return { allowed: false, reason: "Unsupported action kind." };
|
|
248
566
|
if (!step.summary.trim()) return { allowed: false, reason: "A human-readable action summary is required." };
|
|
@@ -362,6 +680,88 @@ function validatestep(step, origin) {
|
|
|
362
680
|
if (!nonnegativeoption(retry, "settle")) return { allowed: false, reason: "The reviewed retry settle window must be zero or a positive number of milliseconds." };
|
|
363
681
|
if (!nonnegativeoption(retry, "tolerance")) return { allowed: false, reason: "The reviewed retry movement tolerance must be zero or a positive number of pixels." };
|
|
364
682
|
}
|
|
683
|
+
if (watchactions.has(step.kind)) {
|
|
684
|
+
if (typeof options.lifetime !== "number" || !Number.isFinite(options.lifetime) || options.lifetime <= 0) return { allowed: false, reason: "A reviewed watch lifetime window in milliseconds is required in options." };
|
|
685
|
+
if (options.scopes !== void 0 && (!Array.isArray(options.scopes) || !options.scopes.every((scope) => isnonempty(scope)))) return { allowed: false, reason: "The reviewed watch scopes must be a list of non-empty selectors." };
|
|
686
|
+
if (options.events !== void 0 && (!Array.isArray(options.events) || !options.events.every((event) => isnonempty(event)))) return { allowed: false, reason: "The reviewed watch event kinds must be a list of non-empty strings." };
|
|
687
|
+
if (!nonnegativeoption(options, "poll")) return { allowed: false, reason: "The reviewed watch poll interval must be zero or a positive number of milliseconds." };
|
|
688
|
+
}
|
|
689
|
+
if (step.kind === "waitquiet") {
|
|
690
|
+
const rule = options.quietrule;
|
|
691
|
+
if (!rule || typeof rule !== "object" || Array.isArray(rule)) return { allowed: false, reason: "A reviewed quietrule with an idle threshold is required in options." };
|
|
692
|
+
const quiet = rule;
|
|
693
|
+
if (typeof quiet.idle !== "number" || !Number.isFinite(quiet.idle) || quiet.idle <= 0) return { allowed: false, reason: "The reviewed quiet idle threshold must be a positive number of milliseconds with no code ceiling." };
|
|
694
|
+
if (!nonnegativeoption(quiet, "poll")) return { allowed: false, reason: "The reviewed quiet poll interval must be zero or a positive number of milliseconds." };
|
|
695
|
+
if (!nonnegativeoption(quiet, "timeout")) return { allowed: false, reason: "The reviewed quiet timeout must be zero or a positive number of milliseconds." };
|
|
696
|
+
}
|
|
697
|
+
if (step.kind === "diffsnapshots") {
|
|
698
|
+
const versions = options.versions;
|
|
699
|
+
if (!Array.isArray(versions) || versions.length !== 2 || !versions.every((version) => typeof version === "number" && Number.isInteger(version) && version >= 1)) return { allowed: false, reason: "Two reviewed observation version numbers are required in options." };
|
|
700
|
+
}
|
|
701
|
+
if (step.kind === "openlink" || step.kind === "openprivate" || step.kind === "deeplink") {
|
|
702
|
+
const targetcheck = validatenavtarget(options.navtarget, step.kind);
|
|
703
|
+
if (!targetcheck.allowed) return targetcheck;
|
|
704
|
+
if (step.kind === "deeplink") {
|
|
705
|
+
const app = options.app;
|
|
706
|
+
if (!isnonempty(app)) return { allowed: false, reason: "A reviewed deep link app pattern is required in options." };
|
|
707
|
+
const params = options.params;
|
|
708
|
+
if (params !== void 0 && (!params || typeof params !== "object" || Array.isArray(params) || !Object.values(params).every((item) => typeof item === "string"))) return { allowed: false, reason: "The reviewed deep link params must be an object of string values." };
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
if (step.kind === "waitload" && !nonnegativeoption(options, "timeout")) return { allowed: false, reason: "The waitload timeout must be zero or a positive number of milliseconds." };
|
|
712
|
+
if (step.kind === "waiturl" || step.kind === "spawait") {
|
|
713
|
+
if (step.kind === "waiturl") {
|
|
714
|
+
const patterncheck = validateurlpattern(options.urlpattern);
|
|
715
|
+
if (!patterncheck.allowed) return patterncheck;
|
|
716
|
+
}
|
|
717
|
+
if (!nonnegativeoption(options, "timeout")) return { allowed: false, reason: "The wait timeout must be zero or a positive number of milliseconds." };
|
|
718
|
+
if (!nonnegativeoption(options, "poll")) return { allowed: false, reason: "The wait poll interval must be zero or a positive number of milliseconds." };
|
|
719
|
+
}
|
|
720
|
+
if (step.kind === "followlink") {
|
|
721
|
+
if (options.fragment !== void 0 && typeof options.fragment !== "boolean") return { allowed: false, reason: "The reviewed followlink fragment flag must be a boolean." };
|
|
722
|
+
}
|
|
723
|
+
if (step.kind === "spanav") {
|
|
724
|
+
if (options.routepattern !== void 0) {
|
|
725
|
+
const routecheck = validateurlpattern(options.routepattern);
|
|
726
|
+
if (!routecheck.allowed) return routecheck;
|
|
727
|
+
}
|
|
728
|
+
if (!nonnegativeoption(options, "timeout")) return { allowed: false, reason: "The spanav route timeout must be zero or a positive number of milliseconds." };
|
|
729
|
+
}
|
|
730
|
+
if (step.kind === "rewritequery") {
|
|
731
|
+
const set = options.set;
|
|
732
|
+
const remove = options.remove;
|
|
733
|
+
if (set === void 0 && remove === void 0) return { allowed: false, reason: "Reviewed query parameters to set or remove are required in options." };
|
|
734
|
+
if (set !== void 0 && (!set || typeof set !== "object" || Array.isArray(set) || !Object.values(set).every((item) => typeof item === "string"))) return { allowed: false, reason: "The reviewed query parameters to set must be an object of string values." };
|
|
735
|
+
if (remove !== void 0 && (!Array.isArray(remove) || !remove.every((item) => isnonempty(item)))) return { allowed: false, reason: "The reviewed query parameters to remove must be a list of non-empty names." };
|
|
736
|
+
}
|
|
737
|
+
if (step.kind === "navlist") {
|
|
738
|
+
const listcheck = validateurllist(options, "urls");
|
|
739
|
+
if (!listcheck.allowed) return listcheck;
|
|
740
|
+
}
|
|
741
|
+
if (step.kind === "navprofile") {
|
|
742
|
+
const profilecheck = validatewaitprofile(options.waitprofile);
|
|
743
|
+
if (!profilecheck.allowed) return profilecheck;
|
|
744
|
+
}
|
|
745
|
+
if (step.kind === "handleauth" && !ishttpsurl(step.value)) return { allowed: false, reason: "A reviewed HTTPS origin or url is required as the auth target." };
|
|
746
|
+
if (step.kind === "printpdf" && options.name !== void 0 && !isnonempty(options.name)) return { allowed: false, reason: "The reviewed artifact name must be a non-empty string." };
|
|
747
|
+
if (step.kind === "prefetch") {
|
|
748
|
+
const listcheck = validateurllist(options, "urls");
|
|
749
|
+
if (!listcheck.allowed) return listcheck;
|
|
750
|
+
}
|
|
751
|
+
if (step.kind === "preconnect") {
|
|
752
|
+
const origins = options.origins;
|
|
753
|
+
if (!Array.isArray(origins) || origins.length === 0 || !origins.every((originurl) => ishttpsurl(originurl))) return { allowed: false, reason: "A reviewed non-empty list of HTTPS origins is required in options." };
|
|
754
|
+
}
|
|
755
|
+
if (step.kind === "reopentab" && step.value !== void 0 && !ishttpsurl(step.value)) return { allowed: false, reason: "The reviewed reopen url must use HTTPS." };
|
|
756
|
+
if (step.kind === "navrate") {
|
|
757
|
+
const limitcheck = validateratelimit(options.ratelimit);
|
|
758
|
+
if (!limitcheck.allowed) return limitcheck;
|
|
759
|
+
}
|
|
760
|
+
if (step.kind === "checksafe" && !ishttpsurl(step.value)) return { allowed: false, reason: "A reviewed HTTPS url is required for the safety check." };
|
|
761
|
+
if (step.kind === "batchopen") {
|
|
762
|
+
const listcheck = validateurllist(options, "urls");
|
|
763
|
+
if (!listcheck.allowed) return listcheck;
|
|
764
|
+
}
|
|
365
765
|
return { allowed: true };
|
|
366
766
|
}
|
|
367
767
|
function sessiongate(input) {
|
|
@@ -378,6 +778,37 @@ function canexecute(input) {
|
|
|
378
778
|
if (!input.plan || input.plan.state !== "approved") return { allowed: false, reason: "The plan has not received explicit approval." };
|
|
379
779
|
if (input.plan.expiresat <= now) return { allowed: false, reason: "The approved plan has expired." };
|
|
380
780
|
if ((input.step.kind === "pierceshadow" || input.step.kind === "enterframe") && !origingranted(input.session, input.origin)) return { allowed: false, reason: "The shadow or frame step is outside the session origin grants." };
|
|
781
|
+
if (input.step.kind === "readjson" && !origingranted(input.session, input.origin)) return { allowed: false, reason: "The json state read is outside the session origin grants." };
|
|
782
|
+
if (input.step.kind === "navlist") {
|
|
783
|
+
let options = {};
|
|
784
|
+
try {
|
|
785
|
+
options = parseoptions(input.step);
|
|
786
|
+
} catch {
|
|
787
|
+
options = {};
|
|
788
|
+
}
|
|
789
|
+
for (const url of Array.isArray(options.urls) ? options.urls : []) {
|
|
790
|
+
if (typeof url !== "string") continue;
|
|
791
|
+
const navigation = navigationgranted(input.session, url);
|
|
792
|
+
if (!navigation.allowed) return navigation;
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
if (input.step.kind === "openlink" || input.step.kind === "openprivate" || input.step.kind === "batchopen" || input.step.kind === "prefetch" || input.step.kind === "deeplink" || input.step.kind === "reopentab") {
|
|
796
|
+
let options = {};
|
|
797
|
+
try {
|
|
798
|
+
options = parseoptions(input.step);
|
|
799
|
+
} catch {
|
|
800
|
+
options = {};
|
|
801
|
+
}
|
|
802
|
+
const grammar = validatestep(input.step, input.origin);
|
|
803
|
+
if (!grammar.allowed) return grammar;
|
|
804
|
+
const grants = input.session?.grants ?? [input.session?.origin ?? input.origin];
|
|
805
|
+
const targets = input.step.kind === "batchopen" || input.step.kind === "prefetch" ? Array.isArray(options.urls) ? options.urls : [] : input.step.kind === "reopentab" ? [input.step.value] : [options.navtarget?.url];
|
|
806
|
+
for (const target of targets) {
|
|
807
|
+
if (typeof target !== "string" || !target) continue;
|
|
808
|
+
const verified = originverified(target, grants, input.verdicts ?? []);
|
|
809
|
+
if (!verified.allowed) return verified;
|
|
810
|
+
}
|
|
811
|
+
}
|
|
381
812
|
return validatestep(input.step, input.origin);
|
|
382
813
|
}
|
|
383
814
|
function canpreview(input) {
|
|
@@ -420,9 +851,22 @@ function resetforplan(progress, plan, now) {
|
|
|
420
851
|
const snapshot2 = { planid: progress.planid, completedsteps: progress.completedsteps, ...progress.outcomes ? { outcomes: progress.outcomes } : {}, updatedat: progress.updatedat };
|
|
421
852
|
return { planid: plan.id, completedsteps: [], outcomes: [], prior: [...progress.prior ?? [], snapshot2], updatedat: now };
|
|
422
853
|
}
|
|
854
|
+
function watchclosed(startedat, lifetime, now) {
|
|
855
|
+
return now >= startedat + lifetime;
|
|
856
|
+
}
|
|
857
|
+
function recordwatchcompletion(progress, planid, stepid, startedat, lifetime, now) {
|
|
858
|
+
const base = progress && progress.planid === planid ? progress : emptyprogress(planid, now);
|
|
859
|
+
if (!watchclosed(startedat, lifetime, now)) return base;
|
|
860
|
+
return recordstep(base, planid, stepid, now);
|
|
861
|
+
}
|
|
862
|
+
function recordnaventry(progress, planid, stepid, entry, now) {
|
|
863
|
+
const base = progress && progress.planid === planid ? progress : emptyprogress(planid, now);
|
|
864
|
+
const outcome = { stepid, ok: entry.ok, summary: `Navigation list entry ${entry.index + 1} of ${entry.url} ${entry.ok ? "completed" : "failed"}.`, details: { naventry: entry }, at: now };
|
|
865
|
+
return recordoutcome(base, planid, outcome, now);
|
|
866
|
+
}
|
|
423
867
|
|
|
424
868
|
// version.ts
|
|
425
|
-
var packageversion = "1.1.
|
|
869
|
+
var packageversion = "1.1.35";
|
|
426
870
|
|
|
427
871
|
// types.ts
|
|
428
872
|
var protocolversion = packageversion;
|
|
@@ -489,6 +933,28 @@ function mapresponse(input) {
|
|
|
489
933
|
function heldkeysreport(input) {
|
|
490
934
|
return { version: protocolversion, tabid: input.tabid, heldkeys: input.holds };
|
|
491
935
|
}
|
|
936
|
+
function observationresponse(input) {
|
|
937
|
+
return JSON.stringify({ version: protocolversion, planid: input.plan.id, planstate: input.plan.state, observation: input.observation });
|
|
938
|
+
}
|
|
939
|
+
function signalsreport(input) {
|
|
940
|
+
const signals = input.signals;
|
|
941
|
+
return {
|
|
942
|
+
version: protocolversion,
|
|
943
|
+
...signals && signals.language !== void 0 ? { language: signals.language } : {},
|
|
944
|
+
...signals && signals.template !== void 0 ? { template: signals.template } : {},
|
|
945
|
+
...signals && signals.scrolllocked !== void 0 ? { scrolllocked: signals.scrolllocked } : {},
|
|
946
|
+
...signals && signals.banner !== void 0 ? { banner: signals.banner } : {}
|
|
947
|
+
};
|
|
948
|
+
}
|
|
949
|
+
function navstateresponse(input) {
|
|
950
|
+
return JSON.stringify({ version: protocolversion, planid: input.plan.id, planstate: input.plan.state, navstate: input.navstate });
|
|
951
|
+
}
|
|
952
|
+
function trailreport(input) {
|
|
953
|
+
return { version: protocolversion, ...input.sessionid ? { sessionid: input.sessionid } : {}, trail: input.trail };
|
|
954
|
+
}
|
|
955
|
+
function safetyresponse(input) {
|
|
956
|
+
return JSON.stringify({ version: protocolversion, planid: input.plan.id, planstate: input.plan.state, verdicts: input.verdicts });
|
|
957
|
+
}
|
|
492
958
|
|
|
493
959
|
// extension/browsertabs.ts
|
|
494
960
|
var browserkinds = /* @__PURE__ */ new Set(["tablist", "tabcreate", "tabactivate", "tabclose", "tabreload", "tabsnapshot", "windowlist", "windowcreate", "windowclose", "zoomset", "windowresize", "downloadfile"]);
|
|
@@ -706,10 +1172,332 @@ function heldkeys(holds, tabid2) {
|
|
|
706
1172
|
return holds.filter((hold) => hold.releasedat === void 0 && (tabid2 === void 0 || hold.tabid === void 0 || hold.tabid === tabid2));
|
|
707
1173
|
}
|
|
708
1174
|
|
|
1175
|
+
// extension/pagenav.ts
|
|
1176
|
+
function parsenavtarget(step) {
|
|
1177
|
+
let options = {};
|
|
1178
|
+
try {
|
|
1179
|
+
options = parseoptions(step);
|
|
1180
|
+
} catch {
|
|
1181
|
+
options = {};
|
|
1182
|
+
}
|
|
1183
|
+
const value = options.navtarget;
|
|
1184
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
1185
|
+
const target = value;
|
|
1186
|
+
if (typeof target.url !== "string" || !target.url) return null;
|
|
1187
|
+
const container = target.container === "current" || target.container === "window" || target.container === "private" ? target.container : "tab";
|
|
1188
|
+
return {
|
|
1189
|
+
url: target.url,
|
|
1190
|
+
container,
|
|
1191
|
+
...target.position === "end" ? { position: "end" } : { position: "adjacent" },
|
|
1192
|
+
private: container === "private" || target.private === true
|
|
1193
|
+
};
|
|
1194
|
+
}
|
|
1195
|
+
function resolvecontainer(target, windows) {
|
|
1196
|
+
if (target.container === "current") return { kind: "current", incognito: false, position: target.position ?? "adjacent" };
|
|
1197
|
+
if (target.container === "private" || target.private) return { kind: "private", incognito: true, position: target.position ?? "adjacent" };
|
|
1198
|
+
if (target.container === "window") {
|
|
1199
|
+
const focused = windows.find((item) => item.focused);
|
|
1200
|
+
return { kind: "window", incognito: false, ...focused ? { windowid: focused.id } : {}, position: target.position ?? "adjacent" };
|
|
1201
|
+
}
|
|
1202
|
+
const normal = windows.find((item) => !item.incognito && item.focused) ?? windows.find((item) => !item.incognito);
|
|
1203
|
+
return { kind: "tab", incognito: false, ...normal ? { windowid: normal.id } : {}, position: target.position ?? "adjacent" };
|
|
1204
|
+
}
|
|
1205
|
+
function parsewaitprofile(step) {
|
|
1206
|
+
let options = {};
|
|
1207
|
+
try {
|
|
1208
|
+
options = parseoptions(step);
|
|
1209
|
+
} catch {
|
|
1210
|
+
options = {};
|
|
1211
|
+
}
|
|
1212
|
+
const value = options.waitprofile;
|
|
1213
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
1214
|
+
const profile = value;
|
|
1215
|
+
const signals = Array.isArray(profile.signals) ? profile.signals.filter((item) => typeof item === "string" && item.trim().length > 0) : [];
|
|
1216
|
+
if (signals.length === 0) return null;
|
|
1217
|
+
const overrides = [];
|
|
1218
|
+
if (Array.isArray(profile.overrides)) {
|
|
1219
|
+
for (const item of profile.overrides) {
|
|
1220
|
+
if (!item || typeof item !== "object" || Array.isArray(item)) continue;
|
|
1221
|
+
const override = item;
|
|
1222
|
+
if (typeof override.origin !== "string" || !override.origin) continue;
|
|
1223
|
+
const overridesignals = Array.isArray(override.signals) ? override.signals.filter((entry) => typeof entry === "string" && entry.trim().length > 0) : void 0;
|
|
1224
|
+
overrides.push({
|
|
1225
|
+
origin: override.origin,
|
|
1226
|
+
...overridesignals && overridesignals.length > 0 ? { signals: overridesignals } : {},
|
|
1227
|
+
...typeof override.idle === "number" && Number.isFinite(override.idle) && override.idle >= 0 ? { idle: override.idle } : {},
|
|
1228
|
+
...typeof override.timeout === "number" && Number.isFinite(override.timeout) && override.timeout >= 0 ? { timeout: override.timeout } : {}
|
|
1229
|
+
});
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
return {
|
|
1233
|
+
signals,
|
|
1234
|
+
...typeof profile.idle === "number" && Number.isFinite(profile.idle) && profile.idle >= 0 ? { idle: profile.idle } : {},
|
|
1235
|
+
...typeof profile.timeout === "number" && Number.isFinite(profile.timeout) && profile.timeout >= 0 ? { timeout: profile.timeout } : {},
|
|
1236
|
+
...overrides.length > 0 ? { overrides } : {}
|
|
1237
|
+
};
|
|
1238
|
+
}
|
|
1239
|
+
function profilefororigin(profile, origin) {
|
|
1240
|
+
let signals = [...profile.signals];
|
|
1241
|
+
let idle = profile.idle ?? 0;
|
|
1242
|
+
let timeout = profile.timeout ?? 0;
|
|
1243
|
+
for (const override of profile.overrides ?? []) {
|
|
1244
|
+
if (!override.origin || new URL(override.origin).origin !== origin) continue;
|
|
1245
|
+
if (override.signals && override.signals.length > 0) signals = [...override.signals];
|
|
1246
|
+
if (override.idle !== void 0) idle = override.idle;
|
|
1247
|
+
if (override.timeout !== void 0) timeout = override.timeout;
|
|
1248
|
+
}
|
|
1249
|
+
return { signals, idle, timeout };
|
|
1250
|
+
}
|
|
1251
|
+
function deeplinkurl(app, params) {
|
|
1252
|
+
const value = (name) => {
|
|
1253
|
+
const item = params[name];
|
|
1254
|
+
return typeof item === "string" && item.trim() ? item.trim() : void 0;
|
|
1255
|
+
};
|
|
1256
|
+
switch (app.trim().toLowerCase()) {
|
|
1257
|
+
case "github": {
|
|
1258
|
+
const owner = value("owner");
|
|
1259
|
+
const repo = value("repo");
|
|
1260
|
+
if (!owner || !repo) return null;
|
|
1261
|
+
const path = value("path");
|
|
1262
|
+
return `https://github.com/${owner}/${repo}${path ? `/${path.replace(/^\/+/, "")}` : ""}`;
|
|
1263
|
+
}
|
|
1264
|
+
case "youtube": {
|
|
1265
|
+
const id = value("id");
|
|
1266
|
+
if (id) return `https://www.youtube.com/watch?v=${encodeURIComponent(id)}`;
|
|
1267
|
+
const search = value("search");
|
|
1268
|
+
if (search) return `https://www.youtube.com/results?search_query=${encodeURIComponent(search)}`;
|
|
1269
|
+
return null;
|
|
1270
|
+
}
|
|
1271
|
+
case "maps": {
|
|
1272
|
+
const query = value("query");
|
|
1273
|
+
if (!query) return null;
|
|
1274
|
+
return `https://www.google.com/maps/search/${encodeURIComponent(query)}`;
|
|
1275
|
+
}
|
|
1276
|
+
case "wikipedia": {
|
|
1277
|
+
const title = value("title");
|
|
1278
|
+
if (!title) return null;
|
|
1279
|
+
const language = value("language") ?? "en";
|
|
1280
|
+
return `https://${language}.wikipedia.org/wiki/${encodeURIComponent(title.replace(/\s+/g, "_"))}`;
|
|
1281
|
+
}
|
|
1282
|
+
case "amazon": {
|
|
1283
|
+
const search = value("search");
|
|
1284
|
+
if (!search) return null;
|
|
1285
|
+
return `https://www.amazon.com/s?k=${encodeURIComponent(search)}`;
|
|
1286
|
+
}
|
|
1287
|
+
case "x": {
|
|
1288
|
+
const user = value("user");
|
|
1289
|
+
if (!user) return null;
|
|
1290
|
+
return `https://x.com/${user.replace(/^@/, "")}`;
|
|
1291
|
+
}
|
|
1292
|
+
default:
|
|
1293
|
+
return null;
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
function pickrecenttab(recenttabs, openurls) {
|
|
1297
|
+
return recenttabs.find((tab) => !openurls.includes(tab.url)) ?? null;
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
// extension/pagenet.ts
|
|
1301
|
+
function buildredirectchain(events) {
|
|
1302
|
+
const hops = [];
|
|
1303
|
+
let startedat = 0;
|
|
1304
|
+
let endedat = 0;
|
|
1305
|
+
let open = false;
|
|
1306
|
+
for (const event of events) {
|
|
1307
|
+
if (event.event === "beforenavigate") {
|
|
1308
|
+
hops.length = 0;
|
|
1309
|
+
startedat = event.timestamp;
|
|
1310
|
+
endedat = event.timestamp;
|
|
1311
|
+
open = true;
|
|
1312
|
+
hops.push({ url: event.url, status: event.status ?? 0, at: event.timestamp });
|
|
1313
|
+
continue;
|
|
1314
|
+
}
|
|
1315
|
+
if (!open) continue;
|
|
1316
|
+
endedat = event.timestamp;
|
|
1317
|
+
if (event.event === "urlchange" || event.redirect || event.event === "committed" && hops[hops.length - 1]?.url !== event.url) {
|
|
1318
|
+
if (hops[hops.length - 1]?.url === event.url && typeof event.status === "number") hops[hops.length - 1] = { url: event.url, status: event.status, at: event.timestamp };
|
|
1319
|
+
else hops.push({ url: event.url, status: event.status ?? 0, at: event.timestamp });
|
|
1320
|
+
continue;
|
|
1321
|
+
}
|
|
1322
|
+
if (event.event === "committed" && typeof event.status === "number" && hops[hops.length - 1]) {
|
|
1323
|
+
hops[hops.length - 1] = { url: event.url, status: event.status, at: event.timestamp };
|
|
1324
|
+
continue;
|
|
1325
|
+
}
|
|
1326
|
+
if (event.event === "completed" || event.event === "error") {
|
|
1327
|
+
if (event.event === "completed" && hops[hops.length - 1] && hops[hops.length - 1]?.url !== event.url) hops.push({ url: event.url, status: event.status ?? 200, at: event.timestamp });
|
|
1328
|
+
if (event.event === "completed" && typeof event.status === "number" && hops[hops.length - 1]) hops[hops.length - 1] = { url: event.url, status: event.status, at: event.timestamp };
|
|
1329
|
+
open = false;
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
return { hops, startedat, endedat: endedat || startedat };
|
|
1333
|
+
}
|
|
1334
|
+
function finalurl(chain) {
|
|
1335
|
+
return chain.hops[chain.hops.length - 1]?.url ?? "";
|
|
1336
|
+
}
|
|
1337
|
+
function classifynavchange(previousurl, currenturl, status) {
|
|
1338
|
+
if (previousurl === currenturl) return status === "loading" ? "reload" : "none";
|
|
1339
|
+
try {
|
|
1340
|
+
if (new URL(previousurl).origin !== new URL(currenturl).origin) return "load";
|
|
1341
|
+
} catch {
|
|
1342
|
+
return "load";
|
|
1343
|
+
}
|
|
1344
|
+
return status === "loading" ? "load" : "route";
|
|
1345
|
+
}
|
|
1346
|
+
function detecthttpstate(input) {
|
|
1347
|
+
const reasons = [];
|
|
1348
|
+
let httperror = false;
|
|
1349
|
+
let certificate = false;
|
|
1350
|
+
const errors = input.errors ?? [];
|
|
1351
|
+
const statuses = input.statuses ?? [];
|
|
1352
|
+
for (const error of errors) {
|
|
1353
|
+
if (/CERT|SSL|TLS|privacy bad|your connection is not private/i.test(error)) {
|
|
1354
|
+
certificate = true;
|
|
1355
|
+
httperror = true;
|
|
1356
|
+
reasons.push(`certificate interstitial: ${error}`);
|
|
1357
|
+
continue;
|
|
1358
|
+
}
|
|
1359
|
+
if (/ERR_NAME_NOT_RESOLVED|ERR_CONNECTION|ERR_TIMED_OUT|ERR_INTERNET_DISCONNECTED|ERR_ADDRESS_UNREACHABLE|ERR_NETWORK/i.test(error)) {
|
|
1360
|
+
httperror = true;
|
|
1361
|
+
reasons.push(`network error: ${error}`);
|
|
1362
|
+
continue;
|
|
1363
|
+
}
|
|
1364
|
+
httperror = true;
|
|
1365
|
+
reasons.push(`navigation error: ${error}`);
|
|
1366
|
+
}
|
|
1367
|
+
for (const status of statuses) {
|
|
1368
|
+
if (status >= 400 && status < 600) {
|
|
1369
|
+
httperror = true;
|
|
1370
|
+
reasons.push(`http status ${status}`);
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
if (input.offline) reasons.push("browser reports offline");
|
|
1374
|
+
return { httperror, offline: input.offline, certificate, reasons };
|
|
1375
|
+
}
|
|
1376
|
+
function interstitialpolicy(state) {
|
|
1377
|
+
if (state.certificate) return { interstitial: true, bypass: false, guidance: "A certificate interstitial was detected; Devthink reports it for review and never bypasses it." };
|
|
1378
|
+
if (state.httperror) return { interstitial: true, bypass: false, guidance: "An http error state was detected and is reported for review." };
|
|
1379
|
+
return { interstitial: false, bypass: false, guidance: "No interstitial was detected." };
|
|
1380
|
+
}
|
|
1381
|
+
function parseratelimit(step) {
|
|
1382
|
+
let options = {};
|
|
1383
|
+
try {
|
|
1384
|
+
options = parseoptions(step);
|
|
1385
|
+
} catch {
|
|
1386
|
+
options = {};
|
|
1387
|
+
}
|
|
1388
|
+
const value = options.ratelimit;
|
|
1389
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
1390
|
+
const limit = value;
|
|
1391
|
+
const window2 = typeof limit.window === "number" && Number.isFinite(limit.window) && limit.window > 0 ? limit.window : 0;
|
|
1392
|
+
const ceiling = typeof limit.ceiling === "number" && Number.isInteger(limit.ceiling) && limit.ceiling >= 1 ? limit.ceiling : 0;
|
|
1393
|
+
if (window2 <= 0 || ceiling < 1) return null;
|
|
1394
|
+
const domain = typeof limit.domain === "string" && limit.domain.trim() ? limit.domain.trim() : "";
|
|
1395
|
+
return { domain, window: window2, ceiling };
|
|
1396
|
+
}
|
|
1397
|
+
function domainof(url) {
|
|
1398
|
+
try {
|
|
1399
|
+
return new URL(url).hostname;
|
|
1400
|
+
} catch {
|
|
1401
|
+
return "";
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
function ratewindow(state, limit, now) {
|
|
1405
|
+
if (state && state.domain === limit.domain && state.limit.window === limit.window && state.limit.ceiling === limit.ceiling && now < state.openedat + limit.window) return state;
|
|
1406
|
+
return { domain: limit.domain, limit, openedat: now, count: 0 };
|
|
1407
|
+
}
|
|
1408
|
+
function rateallows(state, now) {
|
|
1409
|
+
const elapsed = now - state.openedat;
|
|
1410
|
+
const remaining = Math.max(0, state.limit.ceiling - state.count);
|
|
1411
|
+
const retryafter = Math.max(0, state.limit.window - elapsed);
|
|
1412
|
+
return { allowed: remaining > 0, remaining, retryafter };
|
|
1413
|
+
}
|
|
1414
|
+
function recordratehit(state, now) {
|
|
1415
|
+
return { ...state, count: state.count + 1, ...state.count + 1 === 1 ? { openedat: now } : {} };
|
|
1416
|
+
}
|
|
1417
|
+
function checksafe(url) {
|
|
1418
|
+
const reasons = [];
|
|
1419
|
+
let safe = true;
|
|
1420
|
+
let parsed;
|
|
1421
|
+
try {
|
|
1422
|
+
parsed = new URL(url);
|
|
1423
|
+
} catch {
|
|
1424
|
+
return { url, safe: false, reasons: ["the url does not parse"], at: 0 };
|
|
1425
|
+
}
|
|
1426
|
+
if (parsed.protocol !== "https:") {
|
|
1427
|
+
safe = false;
|
|
1428
|
+
reasons.push("the url must use HTTPS");
|
|
1429
|
+
}
|
|
1430
|
+
if (parsed.username || parsed.password) {
|
|
1431
|
+
safe = false;
|
|
1432
|
+
reasons.push("the url carries embedded credentials");
|
|
1433
|
+
}
|
|
1434
|
+
const host = parsed.hostname.toLowerCase();
|
|
1435
|
+
const privatelist = ["localhost", "127.0.0.1", "0.0.0.0", "::1", "[::1]"];
|
|
1436
|
+
if (privatelist.includes(host) || /^10\./.test(host) || /^192\.168\./.test(host) || /^172\.(1[6-9]|2\d|3[01])\./.test(host) || /^169\.254\./.test(host)) {
|
|
1437
|
+
safe = false;
|
|
1438
|
+
reasons.push(`the host ${host} is a private network target`);
|
|
1439
|
+
}
|
|
1440
|
+
if (/^\d{1,3}(\.\d{1,3}){3}$/.test(host) || /^\[?[0-9a-f:]+\]?$/i.test(host)) {
|
|
1441
|
+
safe = false;
|
|
1442
|
+
reasons.push(`the host ${host} is a raw address without a domain`);
|
|
1443
|
+
}
|
|
1444
|
+
return { url, safe, reasons, at: 0 };
|
|
1445
|
+
}
|
|
1446
|
+
function curatelinks(urls, verifier) {
|
|
1447
|
+
return urls.map((url) => {
|
|
1448
|
+
const verdict = verifier(url);
|
|
1449
|
+
return { url, verdict: verdict.safe ? "safe" : "unsafe", reasons: verdict.reasons };
|
|
1450
|
+
});
|
|
1451
|
+
}
|
|
1452
|
+
function batchopenset(links) {
|
|
1453
|
+
const open = [];
|
|
1454
|
+
const refused = [];
|
|
1455
|
+
for (const link of links) {
|
|
1456
|
+
if (link.verdict === "safe") open.push(link.url);
|
|
1457
|
+
else refused.push({ url: link.url, reasons: link.reasons });
|
|
1458
|
+
}
|
|
1459
|
+
return { open, refused };
|
|
1460
|
+
}
|
|
1461
|
+
function prefetchcandidates(urls, grants) {
|
|
1462
|
+
const allowed = [];
|
|
1463
|
+
const refused = [];
|
|
1464
|
+
for (const url of urls) {
|
|
1465
|
+
let origin = "";
|
|
1466
|
+
try {
|
|
1467
|
+
origin = new URL(url).origin;
|
|
1468
|
+
} catch {
|
|
1469
|
+
refused.push(url);
|
|
1470
|
+
continue;
|
|
1471
|
+
}
|
|
1472
|
+
if (grants.includes(origin)) allowed.push(url);
|
|
1473
|
+
else refused.push(url);
|
|
1474
|
+
}
|
|
1475
|
+
return { allowed, refused };
|
|
1476
|
+
}
|
|
1477
|
+
function preconnectorigins(origins) {
|
|
1478
|
+
return [...new Set(origins.map((origin) => origin.trim()).filter(Boolean))];
|
|
1479
|
+
}
|
|
1480
|
+
function authfor(auths, url) {
|
|
1481
|
+
let origin = "";
|
|
1482
|
+
try {
|
|
1483
|
+
origin = new URL(url).origin;
|
|
1484
|
+
} catch {
|
|
1485
|
+
return void 0;
|
|
1486
|
+
}
|
|
1487
|
+
return auths.find((record2) => record2.origin === origin);
|
|
1488
|
+
}
|
|
1489
|
+
|
|
709
1490
|
// extension/background.ts
|
|
710
1491
|
var sessionduration = 15 * 60 * 1e3;
|
|
711
1492
|
var freshcheckkinds = /* @__PURE__ */ new Set(["focus", "inspect", "click", "type", "scroll", "select", "hover"]);
|
|
712
1493
|
var pointerkinds = /* @__PURE__ */ new Set(["movepointer", "clickpoint", "shiftclick", "clicktext", "clickaria", "clickname", "pierceshadow"]);
|
|
1494
|
+
var watchstepkinds = /* @__PURE__ */ new Set(["watchmutate", "watchbanner", "watchfocus"]);
|
|
1495
|
+
var observationstepkinds = /* @__PURE__ */ new Set(["a11ytree", "readvisible", "readertree", "detectlists", "detecttables", "readjson", "detectinfinitescroll", "detectvirtual", "detectlazy", "readscrollpos", "readlang", "readoutline", "countpages", "listshadow", "listframes", "classifypage", "fingerprintsection", "readselection", "detectsticky", "detectscrolllock", "readopengraph", "detectlanguage", "deriveselector"]);
|
|
1496
|
+
var navigationstepkinds = /* @__PURE__ */ new Set(["openlink", "openprivate", "reloadcache", "stopnav", "waitload", "waiturl", "followlink", "spanav", "spawait", "rewritequery", "setfragment", "navlist", "navprofile", "detecthttp", "readredirects", "readfinalurl", "handleauth", "printpdf", "prefetch", "preconnect", "deeplink", "reopentab", "trailaudit", "pausenav", "navintent", "navrate", "openclipboard", "checksafe", "batchopen"]);
|
|
1497
|
+
var pausenavkinds = /* @__PURE__ */ new Set(["openlink", "openprivate", "followlink", "spanav", "navlist", "openclipboard", "batchopen", "prefetch", "preconnect", "deeplink", "reopentab"]);
|
|
1498
|
+
var ratecheckedkinds = /* @__PURE__ */ new Set(["openlink", "openprivate", "followlink", "spanav", "navlist", "openclipboard", "batchopen", "deeplink", "reopentab"]);
|
|
1499
|
+
var evidencepoll = 100;
|
|
1500
|
+
var evidencesettle = 5e3;
|
|
713
1501
|
var chromestorage = {
|
|
714
1502
|
async get(key) {
|
|
715
1503
|
return (await chrome.storage.local.get(key))[key];
|
|
@@ -753,7 +1541,10 @@ async function snapshot(tabid2) {
|
|
|
753
1541
|
} });
|
|
754
1542
|
const value = result[0]?.result;
|
|
755
1543
|
if (!value) throw new Error("The page did not return an observation.");
|
|
756
|
-
|
|
1544
|
+
const observationcapture = value;
|
|
1545
|
+
const version = await memory.nextobservationversion();
|
|
1546
|
+
await memory.setobservation({ version, observation: observationcapture });
|
|
1547
|
+
return observationcapture;
|
|
757
1548
|
}
|
|
758
1549
|
function plandialogpolicy(plan) {
|
|
759
1550
|
const step = plan?.steps.find((candidate) => candidate.kind === "dismissdialog");
|
|
@@ -874,6 +1665,14 @@ function stepauditkind(step, ok) {
|
|
|
874
1665
|
if (step.kind === "keyhold" || step.kind === "keyrelease") return "hold";
|
|
875
1666
|
if (step.kind === "retryaction") return "retry";
|
|
876
1667
|
if (pointerkinds.has(step.kind)) return "pointer";
|
|
1668
|
+
if (watchstepkinds.has(step.kind)) return "watch";
|
|
1669
|
+
if (step.kind === "diffsnapshots") return "diff";
|
|
1670
|
+
if (step.kind === "readredirects" || step.kind === "readfinalurl" || step.kind === "detecthttp") return "redirect";
|
|
1671
|
+
if (step.kind === "handleauth") return "auth";
|
|
1672
|
+
if (step.kind === "prefetch" || step.kind === "preconnect") return "prefetch";
|
|
1673
|
+
if (step.kind === "navrate") return "rate";
|
|
1674
|
+
if (navigationstepkinds.has(step.kind)) return "navigation";
|
|
1675
|
+
if (observationstepkinds.has(step.kind)) return "observation";
|
|
877
1676
|
return ok ? "action" : "error";
|
|
878
1677
|
}
|
|
879
1678
|
function resolvedinnerstep(step, plan) {
|
|
@@ -960,21 +1759,620 @@ async function executeenterframe(step, plan, tabid2, origin) {
|
|
|
960
1759
|
const derived = { ...step, options: JSON.stringify({ ...options, kind: inner.kind, ...inner.target ? { target: inner.target } : {}, ...inner.value ? { value: inner.value } : {}, ...inneroptions ? { options: inneroptions } : {} }) };
|
|
961
1760
|
return dispatchpagestep(derived, tabid2, origin, plan);
|
|
962
1761
|
}
|
|
1762
|
+
function observationnodes(observationcapture) {
|
|
1763
|
+
return observationcapture.interactive.map((entry) => ({ selector: entry.selector, tag: entry.role, text: entry.label, attributes: {} }));
|
|
1764
|
+
}
|
|
1765
|
+
function detailarray(details, key) {
|
|
1766
|
+
const value = details?.[key];
|
|
1767
|
+
return Array.isArray(value) ? value : [];
|
|
1768
|
+
}
|
|
1769
|
+
async function executediffsnapshots(step, session, plan, tabid2, origin) {
|
|
1770
|
+
const options = stepoptions2(step);
|
|
1771
|
+
const versions = Array.isArray(options.versions) ? options.versions.filter((item) => typeof item === "number") : [];
|
|
1772
|
+
const baseversion = versions[0];
|
|
1773
|
+
const targetversion = versions[1];
|
|
1774
|
+
if (baseversion === void 0 || targetversion === void 0) throw new Error("Two reviewed observation versions are required.");
|
|
1775
|
+
const base = await memory.getobservation(baseversion);
|
|
1776
|
+
const target = await memory.getobservation(targetversion);
|
|
1777
|
+
if (!base || !target) throw new Error("A reviewed observation version has not been captured yet.");
|
|
1778
|
+
const derived = { ...step, options: JSON.stringify({ ...options, base: observationnodes(base.observation), target: observationnodes(target.observation) }) };
|
|
1779
|
+
const output = await dispatchpagestep(derived, tabid2, origin, plan) ?? { ok: false, summary: "The snapshot diff returned no result." };
|
|
1780
|
+
const diff = {
|
|
1781
|
+
baseversion,
|
|
1782
|
+
targetversion,
|
|
1783
|
+
added: detailarray(output.details, "added"),
|
|
1784
|
+
removed: detailarray(output.details, "removed"),
|
|
1785
|
+
changed: detailarray(output.details, "changed"),
|
|
1786
|
+
at: Date.now()
|
|
1787
|
+
};
|
|
1788
|
+
await memory.adddiff(diff);
|
|
1789
|
+
await audit("diff", `Diffed observation versions ${baseversion} and ${targetversion}: ${diff.added.length} added, ${diff.removed.length} removed and ${diff.changed.length} changed nodes.`, { ...session ? { sessionid: session.id } : {}, planid: plan.id, stepid: step.id });
|
|
1790
|
+
return { ok: Boolean(output.ok), summary: output.summary, details: { ...output.details ?? {}, versions: [baseversion, targetversion] } };
|
|
1791
|
+
}
|
|
1792
|
+
async function executewatchstep(step, session, plan, tabid2, origin) {
|
|
1793
|
+
const options = stepoptions2(step);
|
|
1794
|
+
const watchid = typeof options.watchid === "string" && options.watchid.trim() ? options.watchid : randomid();
|
|
1795
|
+
const scopes = Array.isArray(options.scopes) ? options.scopes.filter((item) => typeof item === "string" && item.trim().length > 0) : [];
|
|
1796
|
+
const events = Array.isArray(options.events) ? options.events.filter((item) => typeof item === "string" && item.trim().length > 0) : [];
|
|
1797
|
+
const lifetime = typeof options.lifetime === "number" && Number.isFinite(options.lifetime) && options.lifetime > 0 ? options.lifetime : 0;
|
|
1798
|
+
const watch = { watchid, kind: step.kind, stepid: step.id, sessionid: session.id, origin, scopes, events, startedat: Date.now(), lifetime };
|
|
1799
|
+
await memory.addwatch(watch);
|
|
1800
|
+
await audit("watch", `Watch ${step.kind} registered under id ${watchid} for the reviewed lifetime of ${lifetime} milliseconds inside ${scopes.length > 0 ? `scopes ${scopes.join(", ")}` : "the whole document"}.`, { sessionid: session.id, planid: plan.id, stepid: step.id });
|
|
1801
|
+
const output = await dispatchpagestep(step, tabid2, origin, plan) ?? { ok: false, summary: "The watch returned no result." };
|
|
1802
|
+
await memory.closewatch(watchid, Date.now());
|
|
1803
|
+
if (step.kind === "watchmutate") {
|
|
1804
|
+
for (const entry of detailarray(output.details, "events")) {
|
|
1805
|
+
if (!entry || typeof entry !== "object") continue;
|
|
1806
|
+
const record2 = entry;
|
|
1807
|
+
await memory.addmutationevent({ ...record2, sessionid: session.id });
|
|
1808
|
+
}
|
|
1809
|
+
}
|
|
1810
|
+
if (step.kind === "watchfocus") {
|
|
1811
|
+
for (const entry of detailarray(output.details, "events")) {
|
|
1812
|
+
if (!entry || typeof entry !== "object") continue;
|
|
1813
|
+
const record2 = entry;
|
|
1814
|
+
await memory.addfocusevent({ ...record2, sessionid: session.id });
|
|
1815
|
+
}
|
|
1816
|
+
}
|
|
1817
|
+
if (step.kind === "watchbanner") {
|
|
1818
|
+
for (const entry of detailarray(output.details, "banners")) {
|
|
1819
|
+
if (!entry || typeof entry !== "object") continue;
|
|
1820
|
+
const record2 = entry;
|
|
1821
|
+
await memory.addbanner({ ...record2, sessionid: session.id });
|
|
1822
|
+
}
|
|
1823
|
+
}
|
|
1824
|
+
await audit("watch", `Watch ${watchid} closed after its reviewed lifetime of ${lifetime} milliseconds.`, { sessionid: session.id, planid: plan.id, stepid: step.id });
|
|
1825
|
+
return { output, watch };
|
|
1826
|
+
}
|
|
1827
|
+
async function refreshsignals(step, output) {
|
|
1828
|
+
const current = await memory.getsignals();
|
|
1829
|
+
const next = current ? { ...current, refreshedat: Date.now() } : { refreshedat: Date.now() };
|
|
1830
|
+
let changed = false;
|
|
1831
|
+
if (step.kind === "readlang" || step.kind === "detectlanguage") {
|
|
1832
|
+
const language = output?.details?.language;
|
|
1833
|
+
if (typeof language === "string" && language) {
|
|
1834
|
+
next.language = language;
|
|
1835
|
+
changed = true;
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1838
|
+
if (step.kind === "classifypage") {
|
|
1839
|
+
const template = output?.details?.template;
|
|
1840
|
+
if (typeof template === "string" && template) {
|
|
1841
|
+
next.template = template;
|
|
1842
|
+
changed = true;
|
|
1843
|
+
}
|
|
1844
|
+
}
|
|
1845
|
+
if (step.kind === "detectscrolllock") {
|
|
1846
|
+
const locked = output?.details?.locked;
|
|
1847
|
+
if (typeof locked === "boolean") {
|
|
1848
|
+
next.scrolllocked = locked;
|
|
1849
|
+
changed = true;
|
|
1850
|
+
}
|
|
1851
|
+
}
|
|
1852
|
+
if (step.kind === "watchbanner") {
|
|
1853
|
+
const banners = detailarray(output?.details, "banners");
|
|
1854
|
+
const first = banners[0];
|
|
1855
|
+
if (first && typeof first.kind === "string") {
|
|
1856
|
+
next.banner = first.kind;
|
|
1857
|
+
changed = true;
|
|
1858
|
+
}
|
|
1859
|
+
}
|
|
1860
|
+
if (changed) await memory.setsignals(next);
|
|
1861
|
+
}
|
|
1862
|
+
async function recordevidence(step, output, session, plan, origin) {
|
|
1863
|
+
const extra = { ...session ? { sessionid: session.id } : {}, ...plan ? { planid: plan.id } : {}, stepid: step.id };
|
|
1864
|
+
if (step.kind === "a11ytree" && output?.details?.tree && typeof output.details.tree === "object") {
|
|
1865
|
+
const version = await memory.nextobservationversion();
|
|
1866
|
+
await memory.adda11ytree({ version, tree: output.details.tree, capturedat: Date.now() });
|
|
1867
|
+
await audit("observation", `Accessibility tree captured under observation version ${version} with ${output.details.nodecount ?? "an unknown count of"} nodes.`, extra);
|
|
1868
|
+
}
|
|
1869
|
+
if (step.kind === "readertree" && output?.details?.article && typeof output.details.article === "object") {
|
|
1870
|
+
const version = await memory.nextobservationversion();
|
|
1871
|
+
const article = output.details.article;
|
|
1872
|
+
await memory.addreaderarticle({ version, article, capturedat: Date.now() });
|
|
1873
|
+
await audit("observation", `Reader article captured under observation version ${version} with ${article.blocks.length} blocks and ${article.words} words.`, extra);
|
|
1874
|
+
}
|
|
1875
|
+
if (step.kind === "classifypage" && typeof output?.details?.template === "string") {
|
|
1876
|
+
const fingerprint = typeof output.details.fingerprint === "string" ? output.details.fingerprint : "";
|
|
1877
|
+
const profile = { origin, template: output.details.template, fingerprint, at: Date.now() };
|
|
1878
|
+
await memory.addtemplate(profile);
|
|
1879
|
+
await audit("observation", `Page template ${profile.template} classified for ${origin}${fingerprint ? ` with fingerprint ${fingerprint}` : ""}.`, extra);
|
|
1880
|
+
}
|
|
1881
|
+
if (step.kind === "fingerprintsection" && typeof output?.details?.fingerprint === "string") {
|
|
1882
|
+
const profile = { origin, template: "", fingerprint: output.details.fingerprint, ...typeof output.details.section === "string" ? { section: output.details.section } : {}, at: Date.now() };
|
|
1883
|
+
await memory.addtemplate(profile);
|
|
1884
|
+
await audit("observation", `Section fingerprint ${profile.fingerprint} computed for ${origin}.`, extra);
|
|
1885
|
+
}
|
|
1886
|
+
if (step.kind === "deriveselector") {
|
|
1887
|
+
const candidates = detailarray(output?.details, "candidates");
|
|
1888
|
+
const best = candidates[0];
|
|
1889
|
+
if (best && typeof best.selector === "string" && best.selector) {
|
|
1890
|
+
const record2 = { stepid: step.id, selector: best.selector, strategy: best.strategy, score: best.score, at: Date.now() };
|
|
1891
|
+
await memory.addselector(record2);
|
|
1892
|
+
await audit("observation", `Derived selector ${record2.selector} through the ${record2.strategy} strategy with stability ${record2.score}.`, extra);
|
|
1893
|
+
}
|
|
1894
|
+
}
|
|
1895
|
+
await refreshsignals(step, output);
|
|
1896
|
+
}
|
|
1897
|
+
var navbuffers = /* @__PURE__ */ new Map();
|
|
1898
|
+
var lastknownurls = /* @__PURE__ */ new Map();
|
|
1899
|
+
async function tracktabupdate(tabid2, changeinfo) {
|
|
1900
|
+
const now = Date.now();
|
|
1901
|
+
const url = typeof changeinfo.url === "string" ? changeinfo.url : void 0;
|
|
1902
|
+
const status = changeinfo.status;
|
|
1903
|
+
const previous = lastknownurls.get(tabid2);
|
|
1904
|
+
if (status === "loading" && url) {
|
|
1905
|
+
navbuffers.set(tabid2, [{ event: "beforenavigate", url, timestamp: now }]);
|
|
1906
|
+
lastknownurls.set(tabid2, url);
|
|
1907
|
+
return;
|
|
1908
|
+
}
|
|
1909
|
+
const buffer = navbuffers.get(tabid2) ?? [];
|
|
1910
|
+
if (url) {
|
|
1911
|
+
const kind = classifynavchange(previous ?? url, url, status);
|
|
1912
|
+
buffer.push({ event: kind === "route" ? "urlchange" : "committed", url, timestamp: now, redirect: kind === "route" });
|
|
1913
|
+
if (kind === "route" && previous) {
|
|
1914
|
+
const session = await memory.getsession();
|
|
1915
|
+
if (session && !session.stoppedat && session.tabid === tabid2) {
|
|
1916
|
+
await memory.addtrailentry(session.id, { url, title: "", at: now });
|
|
1917
|
+
}
|
|
1918
|
+
}
|
|
1919
|
+
lastknownurls.set(tabid2, url);
|
|
1920
|
+
}
|
|
1921
|
+
if (status === "complete") buffer.push({ event: "completed", url: lastknownurls.get(tabid2) ?? "", timestamp: now, status: 200 });
|
|
1922
|
+
navbuffers.set(tabid2, buffer);
|
|
1923
|
+
}
|
|
1924
|
+
chrome.tabs.onUpdated.addListener((tabid2, changeinfo) => {
|
|
1925
|
+
void tracktabupdate(tabid2, changeinfo);
|
|
1926
|
+
});
|
|
1927
|
+
chrome.tabs.onRemoved.addListener((tabid2) => {
|
|
1928
|
+
const url = lastknownurls.get(tabid2);
|
|
1929
|
+
if (url) void memory.addrecenttab({ url, tabid: tabid2, closedat: Date.now() });
|
|
1930
|
+
lastknownurls.delete(tabid2);
|
|
1931
|
+
navbuffers.delete(tabid2);
|
|
1932
|
+
});
|
|
1933
|
+
async function recordnavigation(step, session, tabid2) {
|
|
1934
|
+
const started = Date.now();
|
|
1935
|
+
let tab = await chrome.tabs.get(tabid2).catch(() => void 0);
|
|
1936
|
+
while (tab && tab.status !== "complete" && Date.now() - started < evidencesettle) {
|
|
1937
|
+
await new Promise((resolve) => setTimeout(resolve, evidencepoll));
|
|
1938
|
+
tab = await chrome.tabs.get(tabid2).catch(() => void 0);
|
|
1939
|
+
}
|
|
1940
|
+
const url = tab?.url ?? lastknownurls.get(tabid2) ?? "";
|
|
1941
|
+
const title = tab?.title ?? "";
|
|
1942
|
+
const chain = buildredirectchain(navbuffers.get(tabid2) ?? []);
|
|
1943
|
+
const record2 = {
|
|
1944
|
+
stepid: step.id,
|
|
1945
|
+
...session ? { sessionid: session.id } : {},
|
|
1946
|
+
origin: session?.origin ?? (url ? new URL(url).origin : ""),
|
|
1947
|
+
finalurl: finalurl(chain) || url,
|
|
1948
|
+
chain,
|
|
1949
|
+
at: Date.now()
|
|
1950
|
+
};
|
|
1951
|
+
await memory.addnavrecord(record2);
|
|
1952
|
+
await memory.setnavstate(tabid2, record2);
|
|
1953
|
+
if (session && url) await memory.addtrailentry(session.id, { url, title, stepid: step.id, at: Date.now() });
|
|
1954
|
+
return record2;
|
|
1955
|
+
}
|
|
1956
|
+
function injectallowedorigins(step, session) {
|
|
1957
|
+
const allowedorigins = session?.grants ?? (session ? [session.origin] : []);
|
|
1958
|
+
let options = {};
|
|
1959
|
+
try {
|
|
1960
|
+
options = parseoptions(step);
|
|
1961
|
+
} catch {
|
|
1962
|
+
options = {};
|
|
1963
|
+
}
|
|
1964
|
+
return { ...step, options: JSON.stringify({ ...options, allowedorigins }) };
|
|
1965
|
+
}
|
|
1966
|
+
async function enforceratelimit(url, stepid, sessionid) {
|
|
1967
|
+
const domain = domainof(url);
|
|
1968
|
+
if (!domain) return;
|
|
1969
|
+
const states = await memory.getratestates();
|
|
1970
|
+
const stored = states.find((item) => item.domain === domain);
|
|
1971
|
+
if (!stored) return;
|
|
1972
|
+
const live = ratewindow(stored, stored.limit, Date.now());
|
|
1973
|
+
const decision = rateallows(live, Date.now());
|
|
1974
|
+
if (!decision.allowed) {
|
|
1975
|
+
await audit("rate", `The navigation rate limit of ${live.limit.ceiling} per ${live.limit.window} milliseconds for ${domain} was exceeded; the navigation was refused.`, { ...sessionid ? { sessionid } : {}, stepid });
|
|
1976
|
+
throw new Error(`The navigation rate limit for ${domain} has been reached; retry after ${Math.ceil(decision.retryafter / 1e3)} seconds.`);
|
|
1977
|
+
}
|
|
1978
|
+
await memory.setratestate(recordratehit(live, Date.now()));
|
|
1979
|
+
await audit("rate", `Navigation to ${url} counted against the reviewed rate limit of ${live.limit.ceiling} per ${live.limit.window} milliseconds for ${domain}; ${decision.remaining - 1} remaining.`, { ...sessionid ? { sessionid } : {}, stepid });
|
|
1980
|
+
}
|
|
1981
|
+
async function refusenavpause(step) {
|
|
1982
|
+
if (!pausenavkinds.has(step.kind)) return;
|
|
1983
|
+
const control = await memory.getnavcontrol();
|
|
1984
|
+
if (control?.pausedat) throw new Error(control.reason ? `Navigation is paused: ${control.reason}` : "Navigation is paused while a consent prompt is open; resume navigation first.");
|
|
1985
|
+
}
|
|
1986
|
+
async function opencontainer(step, session, url, container, position) {
|
|
1987
|
+
const windows = await chrome.windows.getAll().catch(() => []);
|
|
1988
|
+
const plan = resolvecontainer({ url, container, position, private: container === "private" }, windows.map((item) => ({ id: item.id ?? 0, incognito: item.incognito ?? false, focused: item.focused ?? false })));
|
|
1989
|
+
const extra = { ...session ? { sessionid: session.id } : {}, stepid: step.id };
|
|
1990
|
+
if (plan.kind === "current" && session) {
|
|
1991
|
+
const tabid2 = session.tabid;
|
|
1992
|
+
await chrome.tabs.update(tabid2, { url });
|
|
1993
|
+
const record2 = await recordnavigation(step, session, tabid2);
|
|
1994
|
+
await audit("navigation", `Navigated the task tab to ${url}.`, extra);
|
|
1995
|
+
return { ok: true, summary: `Navigated the task tab to ${url}.`, details: { container: plan.kind, url, finalurl: record2.finalurl, hops: record2.chain.hops.length } };
|
|
1996
|
+
}
|
|
1997
|
+
if (plan.kind === "private" || plan.kind === "window") {
|
|
1998
|
+
const created2 = await chrome.windows.create({ url, incognito: plan.incognito });
|
|
1999
|
+
await audit("navigation", `Opened ${plan.kind === "private" ? "a private window" : "a new window"} for ${url}${plan.kind === "private" ? " separated from normal windows" : ""}.`, extra);
|
|
2000
|
+
return { ok: true, summary: `Opened ${plan.kind === "private" ? "a private window" : "a new window"} for ${url}.`, details: { container: plan.kind, url, windowid: created2?.id ?? 0, incognito: plan.incognito } };
|
|
2001
|
+
}
|
|
2002
|
+
const created = await chrome.tabs.create({ url, ...plan.windowid !== void 0 ? { windowId: plan.windowid } : {}, active: true });
|
|
2003
|
+
await audit("navigation", `Opened a new tab for ${url} without leaving the current page.`, extra);
|
|
2004
|
+
return { ok: true, summary: `Opened a new tab for ${url}.`, details: { container: "tab", url, tabid: created?.id ?? 0, position: plan.position } };
|
|
2005
|
+
}
|
|
2006
|
+
async function verifyopenurl(url, session) {
|
|
2007
|
+
const grants = session?.grants ?? (session ? [session.origin] : []);
|
|
2008
|
+
const verified = originverified(url, grants, await memory.getsafeties());
|
|
2009
|
+
if (!verified.allowed) throw new Error(verified.reason);
|
|
2010
|
+
}
|
|
2011
|
+
async function executeopenlink(step, session) {
|
|
2012
|
+
const target = parsenavtarget(step);
|
|
2013
|
+
if (!target) throw new Error("A reviewed navtarget is required.");
|
|
2014
|
+
await refusenavpause(step);
|
|
2015
|
+
await enforceratelimit(target.url, step.id, session?.id);
|
|
2016
|
+
await verifyopenurl(target.url, session);
|
|
2017
|
+
return opencontainer(step, session, target.url, target.container, target.position ?? "adjacent");
|
|
2018
|
+
}
|
|
2019
|
+
async function executedeeplink(step, session) {
|
|
2020
|
+
let options = {};
|
|
2021
|
+
try {
|
|
2022
|
+
options = parseoptions(step);
|
|
2023
|
+
} catch {
|
|
2024
|
+
options = {};
|
|
2025
|
+
}
|
|
2026
|
+
const params = {};
|
|
2027
|
+
if (options.params && typeof options.params === "object" && !Array.isArray(options.params)) {
|
|
2028
|
+
for (const [name, value] of Object.entries(options.params)) if (typeof value === "string") params[name] = value;
|
|
2029
|
+
}
|
|
2030
|
+
const url = deeplinkurl(typeof options.app === "string" ? options.app : "", params);
|
|
2031
|
+
if (!url) throw new Error("The reviewed deep link pattern is not a known web app.");
|
|
2032
|
+
await refusenavpause(step);
|
|
2033
|
+
await enforceratelimit(url, step.id, session?.id);
|
|
2034
|
+
await verifyopenurl(url, session);
|
|
2035
|
+
const target = parsenavtarget(step);
|
|
2036
|
+
const container = target?.container === "window" ? "window" : target?.container === "private" ? "private" : target?.container === "current" ? "current" : "tab";
|
|
2037
|
+
const output = await opencontainer(step, session, url, container, target?.position ?? "adjacent");
|
|
2038
|
+
return { ...output, details: { ...output.details ?? {}, app: options.app, deeplink: url } };
|
|
2039
|
+
}
|
|
2040
|
+
async function executereopentab(step, session) {
|
|
2041
|
+
await refusenavpause(step);
|
|
2042
|
+
let url = step.value;
|
|
2043
|
+
if (!url) {
|
|
2044
|
+
const tabs = await chrome.tabs.query({}).catch(() => []);
|
|
2045
|
+
const openurls = tabs.map((tab) => tab.url ?? "").filter(Boolean);
|
|
2046
|
+
const recent = pickrecenttab(await memory.getrecenttabs(), openurls);
|
|
2047
|
+
if (!recent) throw new Error("No recently closed tab is available to reopen.");
|
|
2048
|
+
url = recent.url;
|
|
2049
|
+
}
|
|
2050
|
+
await enforceratelimit(url, step.id, session?.id);
|
|
2051
|
+
await verifyopenurl(url, session);
|
|
2052
|
+
const created = await chrome.tabs.create({ url, active: true });
|
|
2053
|
+
await audit("navigation", `Reopened the recently closed tab ${url}.`, { ...session ? { sessionid: session.id } : {}, stepid: step.id });
|
|
2054
|
+
return { ok: true, summary: `Reopened ${url} in a new tab.`, details: { url, tabid: created?.id ?? 0 } };
|
|
2055
|
+
}
|
|
2056
|
+
async function waitforcomplete(tabid2) {
|
|
2057
|
+
const started = Date.now();
|
|
2058
|
+
for (; ; ) {
|
|
2059
|
+
const tab = await chrome.tabs.get(tabid2).catch(() => void 0);
|
|
2060
|
+
if (!tab) return false;
|
|
2061
|
+
if (tab.status === "complete") return true;
|
|
2062
|
+
if (Date.now() - started >= evidencesettle) return false;
|
|
2063
|
+
await new Promise((resolve) => setTimeout(resolve, evidencepoll));
|
|
2064
|
+
}
|
|
2065
|
+
}
|
|
2066
|
+
async function recordnaventryoutcome(step, plan, entry) {
|
|
2067
|
+
const base = await memory.getprogress();
|
|
2068
|
+
await memory.setprogress(recordnaventry(base, plan.id, step.id, entry, Date.now()));
|
|
2069
|
+
}
|
|
2070
|
+
async function executenavlist(step, session, plan, tabid2) {
|
|
2071
|
+
let options = {};
|
|
2072
|
+
try {
|
|
2073
|
+
options = parseoptions(step);
|
|
2074
|
+
} catch {
|
|
2075
|
+
options = {};
|
|
2076
|
+
}
|
|
2077
|
+
const urls = Array.isArray(options.urls) ? options.urls.filter((item) => typeof item === "string" && item.trim().length > 0) : [];
|
|
2078
|
+
if (urls.length === 0) throw new Error("A reviewed list of navigation urls is required.");
|
|
2079
|
+
await refusenavpause(step);
|
|
2080
|
+
let completed = 0;
|
|
2081
|
+
let failed = "";
|
|
2082
|
+
for (let index = 0; index < urls.length; index += 1) {
|
|
2083
|
+
const url = urls[index];
|
|
2084
|
+
const entrygate = navigationgranted(session, url);
|
|
2085
|
+
if (!entrygate.allowed) {
|
|
2086
|
+
failed = entrygate.reason ?? "The navigation list entry was refused.";
|
|
2087
|
+
await recordnaventryoutcome(step, plan, { index, url, ok: false });
|
|
2088
|
+
break;
|
|
2089
|
+
}
|
|
2090
|
+
try {
|
|
2091
|
+
await enforceratelimit(url, step.id, session?.id);
|
|
2092
|
+
await refusenavpause(step);
|
|
2093
|
+
await chrome.tabs.update(tabid2, { url });
|
|
2094
|
+
await waitforcomplete(tabid2);
|
|
2095
|
+
await recordnavigation(step, session, tabid2);
|
|
2096
|
+
completed += 1;
|
|
2097
|
+
await recordnaventryoutcome(step, plan, { index, url, ok: true });
|
|
2098
|
+
} catch (error) {
|
|
2099
|
+
failed = error instanceof Error ? error.message : String(error);
|
|
2100
|
+
await recordnaventryoutcome(step, plan, { index, url, ok: false });
|
|
2101
|
+
break;
|
|
2102
|
+
}
|
|
2103
|
+
}
|
|
2104
|
+
const remaining = urls.length - completed;
|
|
2105
|
+
const ok = failed === "" && completed === urls.length;
|
|
2106
|
+
return {
|
|
2107
|
+
ok,
|
|
2108
|
+
summary: ok ? `Navigated the reviewed list of ${urls.length} url${urls.length === 1 ? "" : "s"} sequentially.` : `The navigation list stopped after ${completed} of ${urls.length} entries: ${failed}`,
|
|
2109
|
+
details: { completed, remaining, urls }
|
|
2110
|
+
};
|
|
2111
|
+
}
|
|
2112
|
+
async function executenavprofile(step, session, origin) {
|
|
2113
|
+
const profile = parsewaitprofile(step);
|
|
2114
|
+
if (!profile) throw new Error("A reviewed waitprofile is required.");
|
|
2115
|
+
const record2 = { origin, profile, at: Date.now() };
|
|
2116
|
+
await memory.setwaitprofile(record2);
|
|
2117
|
+
const effective = profilefororigin(profile, origin);
|
|
2118
|
+
await audit("navigation", `Wait profile applied for ${origin} with signals ${effective.signals.join(", ")}${effective.idle > 0 ? `, idle ${effective.idle}ms` : ""}${effective.timeout > 0 ? ` and timeout ${effective.timeout}ms` : ""}.`, { ...session ? { sessionid: session.id } : {}, stepid: step.id });
|
|
2119
|
+
return { ok: true, summary: `Applied the reviewed wait profile for ${origin} during navigation.`, details: { origin, signals: effective.signals, idle: effective.idle, timeout: effective.timeout, overrides: (profile.overrides ?? []).map((override) => override.origin) } };
|
|
2120
|
+
}
|
|
2121
|
+
async function executedetecthttp(session, tabid2) {
|
|
2122
|
+
const records = await memory.getnavrecords();
|
|
2123
|
+
const buffer = navbuffers.get(tabid2) ?? [];
|
|
2124
|
+
const errors = buffer.filter((event) => event.error !== void 0).map((event) => event.error);
|
|
2125
|
+
const statuses = (records[0]?.chain.hops ?? []).map((hop) => hop.status).filter((status) => status > 0);
|
|
2126
|
+
const state = detecthttpstate({ offline: !navigator.onLine, errors, statuses });
|
|
2127
|
+
const policy = interstitialpolicy(state);
|
|
2128
|
+
return {
|
|
2129
|
+
ok: true,
|
|
2130
|
+
summary: state.httperror || state.offline ? `Detected ${state.reasons.length} navigation problem${state.reasons.length === 1 ? "" : "s"}: ${state.reasons.join("; ")}.` : "No http error, offline state or certificate interstitial was detected.",
|
|
2131
|
+
details: { httperror: state.httperror, offline: state.offline, certificate: state.certificate, reasons: state.reasons, interstitial: policy.interstitial, bypass: policy.bypass, guidance: policy.guidance }
|
|
2132
|
+
};
|
|
2133
|
+
}
|
|
2134
|
+
async function executereadredirects(session) {
|
|
2135
|
+
const record2 = (await memory.getnavrecords()).find((item) => !session || item.sessionid === session.id);
|
|
2136
|
+
if (!record2) return { ok: false, summary: "No navigation has been recorded yet.", details: { chain: { hops: [], startedat: 0, endedat: 0 } } };
|
|
2137
|
+
return { ok: true, summary: `The latest navigation travelled ${Math.max(0, record2.chain.hops.length - 1)} redirect${record2.chain.hops.length - 1 === 1 ? "" : "s"} to ${record2.finalurl}.`, details: { chain: record2.chain, finalurl: record2.finalurl, duration: record2.chain.endedat - record2.chain.startedat } };
|
|
2138
|
+
}
|
|
2139
|
+
async function executereadfinalurl(session) {
|
|
2140
|
+
const record2 = (await memory.getnavrecords()).find((item) => !session || item.sessionid === session.id);
|
|
2141
|
+
if (!record2) return { ok: false, summary: "No navigation has been recorded yet.", details: { finalurl: "" } };
|
|
2142
|
+
return { ok: true, summary: `The final url after redirects is ${record2.finalurl}.`, details: { finalurl: record2.finalurl, hops: record2.chain.hops } };
|
|
2143
|
+
}
|
|
2144
|
+
async function executehandleauth(step, session) {
|
|
2145
|
+
const auths = await memory.getauths();
|
|
2146
|
+
const record2 = authfor(auths, step.value ?? "");
|
|
2147
|
+
if (!record2) throw new Error(`No reviewed basic auth credentials are stored for ${step.value ?? ""}; store them from the side panel first.`);
|
|
2148
|
+
await audit("auth", `Basic auth credentials for ${record2.origin} reviewed as ${record2.username} were armed for the auth prompt; the password stays out of every step detail.`, { ...session ? { sessionid: session.id } : {}, stepid: step.id });
|
|
2149
|
+
return { ok: true, summary: `Armed the reviewed basic auth credentials of ${record2.username} for ${record2.origin}.`, details: { origin: record2.origin, username: record2.username, reviewedat: record2.reviewedat, armed: true } };
|
|
2150
|
+
}
|
|
2151
|
+
async function executeprintpdf(step, session, plan, tabid2, origin) {
|
|
2152
|
+
let options = {};
|
|
2153
|
+
try {
|
|
2154
|
+
options = parseoptions(step);
|
|
2155
|
+
} catch {
|
|
2156
|
+
options = {};
|
|
2157
|
+
}
|
|
2158
|
+
const output = await dispatchpagestep(step, tabid2, origin, plan);
|
|
2159
|
+
const name = typeof options.name === "string" && options.name ? options.name : `${step.id}.pdf`;
|
|
2160
|
+
const artifact = { id: randomid(), kind: "printpdf", name, stepid: step.id, at: Date.now() };
|
|
2161
|
+
await memory.addartifact(artifact);
|
|
2162
|
+
await audit("navigation", `Printed the page to pdf through the browser print pipeline and routed the artifact ${name} into the task artifact store.`, { ...session ? { sessionid: session.id } : {}, stepid: step.id });
|
|
2163
|
+
return { ok: output?.ok ?? false, summary: output?.summary ?? "The print pipeline returned no result.", details: { ...output?.details ?? {}, artifact } };
|
|
2164
|
+
}
|
|
2165
|
+
async function executeprefetch(step, session, plan, tabid2, origin) {
|
|
2166
|
+
let options = {};
|
|
2167
|
+
try {
|
|
2168
|
+
options = parseoptions(step);
|
|
2169
|
+
} catch {
|
|
2170
|
+
options = {};
|
|
2171
|
+
}
|
|
2172
|
+
const urls = Array.isArray(options.urls) ? options.urls.filter((item) => typeof item === "string" && item.trim().length > 0) : [];
|
|
2173
|
+
const grants = session?.grants ?? (session ? [session.origin] : []);
|
|
2174
|
+
const verdict = prefetchcandidates(urls, grants);
|
|
2175
|
+
if (verdict.allowed.length === 0) throw new Error("No prefetch candidate is covered by the session grants.");
|
|
2176
|
+
await refusenavpause(step);
|
|
2177
|
+
const derived = { ...step, options: JSON.stringify({ ...options, urls: verdict.allowed }) };
|
|
2178
|
+
const output = await dispatchpagestep(derived, tabid2, origin, plan);
|
|
2179
|
+
await memory.setnavqueues({ prefetch: verdict.allowed.length, batchopen: (await memory.getnavqueues())?.batchopen ?? 0, updatedat: Date.now() });
|
|
2180
|
+
await refreshbadge();
|
|
2181
|
+
await audit("prefetch", `Prefetched ${verdict.allowed.length} predicted next page${verdict.allowed.length === 1 ? "" : "s"} verified against the session grants${verdict.refused.length > 0 ? ` and refused ${verdict.refused.length} candidate${verdict.refused.length === 1 ? "" : "s"} outside the grants` : ""}.`, { ...session ? { sessionid: session.id } : {}, stepid: step.id });
|
|
2182
|
+
return { ok: Boolean(output?.ok), summary: output?.summary ?? "The prefetch step returned no result.", details: { ...output?.details ?? {}, allowed: verdict.allowed, refused: verdict.refused } };
|
|
2183
|
+
}
|
|
2184
|
+
async function executepreconnect(step, session, plan, tabid2, origin) {
|
|
2185
|
+
let options = {};
|
|
2186
|
+
try {
|
|
2187
|
+
options = parseoptions(step);
|
|
2188
|
+
} catch {
|
|
2189
|
+
options = {};
|
|
2190
|
+
}
|
|
2191
|
+
const origins = preconnectorigins(Array.isArray(options.origins) ? options.origins.filter((item) => typeof item === "string") : []);
|
|
2192
|
+
await refusenavpause(step);
|
|
2193
|
+
const output = await dispatchpagestep({ ...step, options: JSON.stringify({ ...options, origins }) }, tabid2, origin, plan);
|
|
2194
|
+
await audit("prefetch", `Preconnected to ${origins.length} expected origin${origins.length === 1 ? "" : "s"}: ${origins.join(", ")}.`, { ...session ? { sessionid: session.id } : {}, stepid: step.id });
|
|
2195
|
+
return { ok: Boolean(output?.ok), summary: output?.summary ?? "The preconnect step returned no result.", details: { ...output?.details ?? {}, origins } };
|
|
2196
|
+
}
|
|
2197
|
+
async function executeopenclipboard(step, session) {
|
|
2198
|
+
await refusenavpause(step);
|
|
2199
|
+
const text2 = await navigator.clipboard.readText();
|
|
2200
|
+
let url = "";
|
|
2201
|
+
try {
|
|
2202
|
+
url = new URL(text2.trim()).toString();
|
|
2203
|
+
} catch {
|
|
2204
|
+
throw new Error("The clipboard does not hold a valid url.");
|
|
2205
|
+
}
|
|
2206
|
+
if (!url.startsWith("https://")) throw new Error("The clipboard url must use HTTPS.");
|
|
2207
|
+
await enforceratelimit(url, step.id, session?.id);
|
|
2208
|
+
await verifyopenurl(url, session);
|
|
2209
|
+
const output = await opencontainer(step, session, url, "tab", "adjacent");
|
|
2210
|
+
await audit("navigation", `Opened the clipboard url ${url} on the explicit consent of the reviewed step.`, { ...session ? { sessionid: session.id } : {}, stepid: step.id });
|
|
2211
|
+
return { ...output, details: { ...output.details ?? {}, url } };
|
|
2212
|
+
}
|
|
2213
|
+
async function executechecksafe(step, session, planid) {
|
|
2214
|
+
const verdict = { ...checksafe(step.value ?? ""), at: Date.now() };
|
|
2215
|
+
await memory.addsafety(verdict);
|
|
2216
|
+
await audit("navigation", `Safety check of ${verdict.url} returned ${verdict.safe ? "safe" : "unsafe"}${verdict.reasons.length > 0 ? `: ${verdict.reasons.join("; ")}` : ""}.`, { ...session ? { sessionid: session.id } : {}, ...planid ? { planid } : {}, stepid: step.id });
|
|
2217
|
+
return { ok: true, summary: verdict.safe ? `The url ${verdict.url} passed every safety check.` : `The url ${verdict.url} is unsafe: ${verdict.reasons.join("; ")}.`, details: { verdict } };
|
|
2218
|
+
}
|
|
2219
|
+
async function executebatchopen(step, session) {
|
|
2220
|
+
let options = {};
|
|
2221
|
+
try {
|
|
2222
|
+
options = parseoptions(step);
|
|
2223
|
+
} catch {
|
|
2224
|
+
options = {};
|
|
2225
|
+
}
|
|
2226
|
+
const urls = Array.isArray(options.urls) ? options.urls.filter((item) => typeof item === "string" && item.trim().length > 0) : [];
|
|
2227
|
+
const links = curatelinks(urls, (url) => ({ ...checksafe(url), at: Date.now() }));
|
|
2228
|
+
const curated = { id: randomid(), links, at: Date.now() };
|
|
2229
|
+
await memory.addcurated(curated);
|
|
2230
|
+
const set = batchopenset(links);
|
|
2231
|
+
if (set.refused.length > 0) {
|
|
2232
|
+
await memory.setnavqueues({ prefetch: (await memory.getnavqueues())?.prefetch ?? 0, batchopen: set.refused.length, updatedat: Date.now() });
|
|
2233
|
+
await refreshbadge();
|
|
2234
|
+
await audit("navigation", `Batch open refused for ${set.refused.length} unsafe url${set.refused.length === 1 ? "" : "s"}; the curated list waits for review.`, { ...session ? { sessionid: session.id } : {}, stepid: step.id });
|
|
2235
|
+
return { ok: false, summary: `The batch was refused because ${set.refused.length} of ${links.length} urls are unsafe; review the curated list before opening.`, details: { curated, refused: set.refused } };
|
|
2236
|
+
}
|
|
2237
|
+
await refusenavpause(step);
|
|
2238
|
+
const opened = [];
|
|
2239
|
+
for (const url of set.open) {
|
|
2240
|
+
await enforceratelimit(url, step.id, session?.id);
|
|
2241
|
+
const created = await chrome.tabs.create({ url, active: opened.length === 0 });
|
|
2242
|
+
opened.push(created?.id ?? 0);
|
|
2243
|
+
if (session) await memory.addtrailentry(session.id, { url, title: "", stepid: step.id, at: Date.now() });
|
|
2244
|
+
}
|
|
2245
|
+
await memory.addcurated({ ...curated, reviewedat: Date.now() });
|
|
2246
|
+
await memory.setnavqueues({ prefetch: (await memory.getnavqueues())?.prefetch ?? 0, batchopen: 0, updatedat: Date.now() });
|
|
2247
|
+
await refreshbadge();
|
|
2248
|
+
await audit("navigation", `Batch opened ${set.open.length} curated url${set.open.length === 1 ? "" : "s"} after per url safety checks.`, { ...session ? { sessionid: session.id } : {}, stepid: step.id });
|
|
2249
|
+
return { ok: true, summary: `Opened ${set.open.length} curated url${set.open.length === 1 ? "" : "s"} after per url safety checks.`, details: { curated, tabids: opened } };
|
|
2250
|
+
}
|
|
2251
|
+
async function executepausenav(step, session) {
|
|
2252
|
+
const control = await memory.getnavcontrol();
|
|
2253
|
+
let options = {};
|
|
2254
|
+
try {
|
|
2255
|
+
options = parseoptions(step);
|
|
2256
|
+
} catch {
|
|
2257
|
+
options = {};
|
|
2258
|
+
}
|
|
2259
|
+
if (control?.pausedat) {
|
|
2260
|
+
await memory.setnavcontrol({ updatedat: Date.now() });
|
|
2261
|
+
await audit("resume", `Navigation resumed after ${Date.now() - control.pausedat} milliseconds of pause.`, { ...session ? { sessionid: session.id } : {}, stepid: step.id });
|
|
2262
|
+
return { ok: true, summary: "Navigation resumed; reviewed navigation steps can run again.", details: { paused: false } };
|
|
2263
|
+
}
|
|
2264
|
+
const reason = typeof options.reason === "string" && options.reason ? options.reason : "a consent prompt is open";
|
|
2265
|
+
await memory.setnavcontrol({ pausedat: Date.now(), reason, updatedat: Date.now() });
|
|
2266
|
+
await audit("pause", `Navigation paused while ${reason}.`, { ...session ? { sessionid: session.id } : {}, stepid: step.id });
|
|
2267
|
+
return { ok: true, summary: `Navigation paused while ${reason}.`, details: { paused: true, reason } };
|
|
2268
|
+
}
|
|
2269
|
+
async function executenavintent(step, session, origin) {
|
|
2270
|
+
const record2 = { id: randomid(), intent: step.value ?? "", origin, ...session ? { sessionid: session.id } : {}, stepid: step.id, at: Date.now() };
|
|
2271
|
+
await memory.addnavintent(record2);
|
|
2272
|
+
await audit("navigation", `Navigation intent "${record2.intent}" recorded for ${origin}.`, { ...session ? { sessionid: session.id } : {}, stepid: step.id });
|
|
2273
|
+
return { ok: true, summary: `Recorded the navigation intent "${record2.intent}".`, details: { intent: record2.intent, origin, at: record2.at } };
|
|
2274
|
+
}
|
|
2275
|
+
async function executenavrate(step, session, origin) {
|
|
2276
|
+
const limit = parseratelimit(step);
|
|
2277
|
+
if (!limit) throw new Error("A reviewed ratelimit is required.");
|
|
2278
|
+
if (!limit.domain) limit.domain = domainof(origin) || origin;
|
|
2279
|
+
const states = await memory.getratestates();
|
|
2280
|
+
const stored = states.find((item) => item.domain === limit.domain);
|
|
2281
|
+
const live = ratewindow(stored, limit, Date.now());
|
|
2282
|
+
const decision = rateallows(live, Date.now());
|
|
2283
|
+
await memory.setratestate(live);
|
|
2284
|
+
await audit("rate", `Navigation rate limit of ${limit.ceiling} per ${limit.window} milliseconds applied for ${limit.domain}; ${decision.remaining} navigation${decision.remaining === 1 ? "" : "s"} remaining in the window.`, { ...session ? { sessionid: session.id } : {}, stepid: step.id });
|
|
2285
|
+
return { ok: true, summary: `Applied the reviewed navigation rate limit for ${limit.domain}.`, details: { domain: limit.domain, window: limit.window, ceiling: limit.ceiling, count: live.count, remaining: decision.remaining, retryafter: decision.retryafter } };
|
|
2286
|
+
}
|
|
2287
|
+
async function executetrailaudit(session) {
|
|
2288
|
+
if (!session) throw new Error("No active browser session exists.");
|
|
2289
|
+
const trail = await memory.gettrail(session.id);
|
|
2290
|
+
return { ok: true, summary: `The navigation trail of the session holds ${trail.length} visited url${trail.length === 1 ? "" : "s"}.`, details: { trail } };
|
|
2291
|
+
}
|
|
2292
|
+
async function executenavigationkind(step, session, plan, tabid2, origin) {
|
|
2293
|
+
switch (step.kind) {
|
|
2294
|
+
case "openlink":
|
|
2295
|
+
return executeopenlink(step, session);
|
|
2296
|
+
case "openprivate":
|
|
2297
|
+
return executeopenlink(step, session);
|
|
2298
|
+
case "deeplink":
|
|
2299
|
+
return executedeeplink(step, session);
|
|
2300
|
+
case "reopentab":
|
|
2301
|
+
return executereopentab(step, session);
|
|
2302
|
+
case "reloadcache": {
|
|
2303
|
+
await refusenavpause(step);
|
|
2304
|
+
await chrome.tabs.reload(tabid2, { bypassCache: true });
|
|
2305
|
+
const record2 = await recordnavigation(step, session, tabid2);
|
|
2306
|
+
await audit("navigation", `Reloaded the page bypassing the cache.`, { ...session ? { sessionid: session.id } : {}, stepid: step.id });
|
|
2307
|
+
return { ok: true, summary: "Reloaded the page bypassing the cache.", details: { bypasscache: true, finalurl: record2.finalurl } };
|
|
2308
|
+
}
|
|
2309
|
+
case "navlist":
|
|
2310
|
+
return executenavlist(step, session, plan, tabid2);
|
|
2311
|
+
case "navprofile":
|
|
2312
|
+
return executenavprofile(step, session, origin);
|
|
2313
|
+
case "detecthttp":
|
|
2314
|
+
return executedetecthttp(session, tabid2);
|
|
2315
|
+
case "readredirects":
|
|
2316
|
+
return executereadredirects(session);
|
|
2317
|
+
case "readfinalurl":
|
|
2318
|
+
return executereadfinalurl(session);
|
|
2319
|
+
case "handleauth":
|
|
2320
|
+
return executehandleauth(step, session);
|
|
2321
|
+
case "printpdf":
|
|
2322
|
+
return executeprintpdf(step, session, plan, tabid2, origin);
|
|
2323
|
+
case "prefetch":
|
|
2324
|
+
return executeprefetch(step, session, plan, tabid2, origin);
|
|
2325
|
+
case "preconnect":
|
|
2326
|
+
return executepreconnect(step, session, plan, tabid2, origin);
|
|
2327
|
+
case "checksafe":
|
|
2328
|
+
return executechecksafe(step, session, plan.id);
|
|
2329
|
+
case "batchopen":
|
|
2330
|
+
return executebatchopen(step, session);
|
|
2331
|
+
case "pausenav":
|
|
2332
|
+
return executepausenav(step, session);
|
|
2333
|
+
case "navintent":
|
|
2334
|
+
return executenavintent(step, session, origin);
|
|
2335
|
+
case "navrate":
|
|
2336
|
+
return executenavrate(step, session, origin);
|
|
2337
|
+
case "trailaudit":
|
|
2338
|
+
return executetrailaudit(session);
|
|
2339
|
+
case "openclipboard":
|
|
2340
|
+
return executeopenclipboard(step, session);
|
|
2341
|
+
default: {
|
|
2342
|
+
await refusenavpause(step);
|
|
2343
|
+
if (ratecheckedkinds.has(step.kind)) await enforceratelimit(origin, step.id, session?.id);
|
|
2344
|
+
const derived = injectallowedorigins(step, session);
|
|
2345
|
+
const output = await dispatchpagestep(derived, tabid2, origin, plan);
|
|
2346
|
+
if (step.kind === "followlink" || step.kind === "spanav") {
|
|
2347
|
+
const record2 = await recordnavigation(step, session, tabid2);
|
|
2348
|
+
return { ...output ?? { ok: false, summary: "The navigation step returned no result." }, details: { ...(output ?? {}).details ?? {}, finalurl: record2.finalurl, hops: record2.chain.hops.length } };
|
|
2349
|
+
}
|
|
2350
|
+
return output ?? { ok: false, summary: "The navigation step returned no result." };
|
|
2351
|
+
}
|
|
2352
|
+
}
|
|
2353
|
+
}
|
|
2354
|
+
async function refreshbadge() {
|
|
2355
|
+
const queues = await memory.getnavqueues();
|
|
2356
|
+
const total = (queues?.prefetch ?? 0) + (queues?.batchopen ?? 0);
|
|
2357
|
+
await chrome.action.setBadgeText({ text: total > 0 ? String(total) : "" }).catch(() => {
|
|
2358
|
+
});
|
|
2359
|
+
}
|
|
963
2360
|
async function executestep(stepid) {
|
|
964
2361
|
const session = await memory.getsession();
|
|
965
2362
|
const plan = await memory.getplan();
|
|
966
2363
|
const { tab, origin } = await activecontext();
|
|
967
2364
|
const step = plan?.steps.find((candidate) => candidate.id === stepid);
|
|
968
2365
|
if (!step) throw new Error("Reviewed step was not found.");
|
|
969
|
-
const gate = canexecute({ session, plan, step, tabid: tab.id, origin });
|
|
2366
|
+
const gate = canexecute({ session, plan, step, tabid: tab.id, origin, verdicts: await memory.getsafeties() });
|
|
970
2367
|
if (!gate.allowed) throw new Error(gate.reason);
|
|
2368
|
+
const capability = requiredcapability(step.kind);
|
|
2369
|
+
if (capability) {
|
|
2370
|
+
const granted = await chrome.permissions.contains({ permissions: [capability] });
|
|
2371
|
+
if (!granted) throw new Error(`The ${capability} capability has not been granted; request it from the review panel.`);
|
|
2372
|
+
}
|
|
971
2373
|
let output;
|
|
2374
|
+
let watchwindow;
|
|
972
2375
|
if (isbrowserkind(step.kind)) {
|
|
973
|
-
const capability = requiredcapability(step.kind);
|
|
974
|
-
if (capability) {
|
|
975
|
-
const granted = await chrome.permissions.contains({ permissions: [capability] });
|
|
976
|
-
if (!granted) throw new Error(`The ${capability} capability has not been granted; request it from the review panel.`);
|
|
977
|
-
}
|
|
978
2376
|
output = await runbrowseraction(step, tab.id, tab.windowId ?? chrome.windows.WINDOW_ID_CURRENT);
|
|
979
2377
|
} else if (step.kind === "keyhold") {
|
|
980
2378
|
output = await executekeyhold(step, session, plan, tab.id, origin);
|
|
@@ -988,6 +2386,15 @@ async function executestep(stepid) {
|
|
|
988
2386
|
output = await executemapclicks(step, plan, tab.id, origin);
|
|
989
2387
|
} else if (step.kind === "enterframe") {
|
|
990
2388
|
output = await executeenterframe(step, plan, tab.id, origin);
|
|
2389
|
+
} else if (watchstepkinds.has(step.kind)) {
|
|
2390
|
+
if (!session || !plan || plan.state !== "approved") throw new Error("Watch kinds refuse to run outside an approved session plan.");
|
|
2391
|
+
const watched = await executewatchstep(step, session, plan, tab.id, origin);
|
|
2392
|
+
output = watched.output;
|
|
2393
|
+
watchwindow = { startedat: watched.watch.startedat, lifetime: watched.watch.lifetime };
|
|
2394
|
+
} else if (step.kind === "diffsnapshots") {
|
|
2395
|
+
output = await executediffsnapshots(step, session, plan, tab.id, origin);
|
|
2396
|
+
} else if (navigationstepkinds.has(step.kind)) {
|
|
2397
|
+
output = await executenavigationkind(step, session, plan, tab.id, origin);
|
|
991
2398
|
} else {
|
|
992
2399
|
if (step.target && freshcheckkinds.has(step.kind)) {
|
|
993
2400
|
const fresh = await snapshot(tab.id);
|
|
@@ -995,6 +2402,8 @@ async function executestep(stepid) {
|
|
|
995
2402
|
}
|
|
996
2403
|
output = await dispatchpagestep(step, tab.id, origin, plan);
|
|
997
2404
|
}
|
|
2405
|
+
if (["navigate", "back", "forward"].includes(step.kind)) await recordnavigation(step, session, tab.id);
|
|
2406
|
+
await recordevidence(step, output, session, plan, origin);
|
|
998
2407
|
const summary = output?.summary ?? "The page action returned no result.";
|
|
999
2408
|
const resolved = output?.details?.resolvedtarget;
|
|
1000
2409
|
if (resolved) {
|
|
@@ -1005,7 +2414,8 @@ async function executestep(stepid) {
|
|
|
1005
2414
|
await audit(auditkind, summary, { ...session ? { sessionid: session.id } : {}, ...plan ? { planid: plan.id } : {}, stepid });
|
|
1006
2415
|
await memory.addoutcome(outcome);
|
|
1007
2416
|
if (output?.ok && plan) {
|
|
1008
|
-
const
|
|
2417
|
+
const base = await memory.getprogress();
|
|
2418
|
+
const completed = watchwindow ? recordwatchcompletion(base, plan.id, stepid, watchwindow.startedat, watchwindow.lifetime, Date.now()) : recordstep(base, plan.id, stepid, Date.now());
|
|
1009
2419
|
const tracked = recordoutcome(completed, plan.id, outcome, Date.now());
|
|
1010
2420
|
await memory.setprogress(tracked);
|
|
1011
2421
|
if (iscomplete(tracked, plan) && plan.state === "approved") {
|
|
@@ -1083,7 +2493,23 @@ async function handlerequest(message, sender) {
|
|
|
1083
2493
|
const holds = heldkeys(await memory.getholds());
|
|
1084
2494
|
const observationversion = await memory.getobservationversion();
|
|
1085
2495
|
const map = observationversion !== void 0 && observationversion > 0 ? await memory.getmap(observationversion) : void 0;
|
|
1086
|
-
|
|
2496
|
+
const a11y = (await memory.geta11ytrees())[0];
|
|
2497
|
+
const reader = (await memory.getreaderarticles())[0];
|
|
2498
|
+
const signals = await memory.getsignals();
|
|
2499
|
+
const trail = session ? await memory.gettrail(session.id) : [];
|
|
2500
|
+
const navrecords = await memory.getnavrecords();
|
|
2501
|
+
const ratestates = await memory.getratestates();
|
|
2502
|
+
const safeties = await memory.getsafeties();
|
|
2503
|
+
const curated = await memory.getcurateds();
|
|
2504
|
+
const waitprofiles = await memory.getwaitprofiles();
|
|
2505
|
+
const auths = (await memory.getauths()).map((record2) => ({ origin: record2.origin, username: record2.username, reviewedat: record2.reviewedat }));
|
|
2506
|
+
const navcontrol = await memory.getnavcontrol();
|
|
2507
|
+
const navqueues = await memory.getnavqueues();
|
|
2508
|
+
const artifacts = await memory.getartifacts();
|
|
2509
|
+
const livetab = session ? await chrome.tabs.get(session.tabid).catch(() => void 0) : void 0;
|
|
2510
|
+
const waitprofile = session ? waitprofiles.find((record2) => record2.origin === session.origin) : void 0;
|
|
2511
|
+
const livestate = { phase: livetab?.status === "loading" ? "loading" : "complete", ...navrecords[0] ? { finalurl: navrecords[0].finalurl, redirects: navrecords[0].chain } : {} };
|
|
2512
|
+
return { config: await memory.getconfig(), session, plan, progress: plan && progress?.planid === plan.id ? progress : void 0, diagnostic: await memory.getdiagnostic(), audit: await memory.getaudit(), capabilities: await refreshcapabilities(), outcomes: await memory.getoutcomes(), holds: heldkeysreport({ tabid: session?.tabid ?? 0, holds }), dialogs: await memory.getdialogs(), retries: await memory.getretries(), ...signals ? { signals: signalsreport({ signals }) } : { signals: signalsreport({}) }, banners: await memory.getbanners(), mutationevents: await memory.getmutationevents(), focusevents: await memory.getfocusevents(), diffs: await memory.getdiffs(), selectors: await memory.getselectors(), ...a11y ? { a11y } : {}, ...reader ? { reader } : {}, ...map ? { map } : {}, trail: trailreport({ ...session ? { sessionid: session.id } : {}, trail }), navrecords, ratestates, safeties, curated, waitprofiles, auths, navcontrol, navqueues, artifacts, navstate: livestate, ...waitprofile ? { waitprofile } : {}, offline: !navigator.onLine };
|
|
1087
2513
|
}
|
|
1088
2514
|
case "capabilities":
|
|
1089
2515
|
return refreshcapabilities();
|
|
@@ -1135,10 +2561,52 @@ async function handlerequest(message, sender) {
|
|
|
1135
2561
|
if (!map) throw new Error("No clickable map has been captured yet.");
|
|
1136
2562
|
return JSON.parse(mapresponse({ map, plan }));
|
|
1137
2563
|
}
|
|
2564
|
+
case "observation": {
|
|
2565
|
+
const plan = await memory.getplan();
|
|
2566
|
+
if (!plan) throw new Error("No plan is available for an observation envelope.");
|
|
2567
|
+
const version = await memory.getobservationversion();
|
|
2568
|
+
const record2 = version !== void 0 ? await memory.getobservation(version) : void 0;
|
|
2569
|
+
if (!record2) throw new Error("No observation has been captured yet.");
|
|
2570
|
+
return JSON.parse(observationresponse({ observation: record2.observation, plan }));
|
|
2571
|
+
}
|
|
1138
2572
|
case "pausesession":
|
|
1139
2573
|
return pausesession();
|
|
1140
2574
|
case "resumesession":
|
|
1141
2575
|
return resumesession();
|
|
2576
|
+
case "storeauth": {
|
|
2577
|
+
const session = await memory.getsession();
|
|
2578
|
+
if (!session || session.stoppedat || session.expiresat <= Date.now()) throw new Error("Basic auth credentials are stored only behind the consent gate of an active session.");
|
|
2579
|
+
const inputauth = message;
|
|
2580
|
+
const origin = new URL(inputauth.origin ?? "").origin;
|
|
2581
|
+
if (!origin.startsWith("https://")) throw new Error("Basic auth credentials need an HTTPS origin.");
|
|
2582
|
+
if (!inputauth.username?.trim() || !inputauth.password) throw new Error("Basic auth credentials need a username and a password.");
|
|
2583
|
+
const record2 = { origin, username: inputauth.username.trim(), password: inputauth.password, reviewedat: Date.now() };
|
|
2584
|
+
await memory.setauth(record2);
|
|
2585
|
+
await audit("auth", `Basic auth credentials for ${origin} stored after explicit review; the password never leaves local storage.`, { sessionid: session.id });
|
|
2586
|
+
return { origin: record2.origin, username: record2.username, reviewedat: record2.reviewedat };
|
|
2587
|
+
}
|
|
2588
|
+
case "checksafe": {
|
|
2589
|
+
const inputurl = message;
|
|
2590
|
+
const verdict = { ...checksafe(inputurl.url ?? ""), at: Date.now() };
|
|
2591
|
+
await memory.addsafety(verdict);
|
|
2592
|
+
const session = await memory.getsession();
|
|
2593
|
+
await audit("navigation", `Safety check of ${verdict.url} returned ${verdict.safe ? "safe" : "unsafe"}${verdict.reasons.length > 0 ? `: ${verdict.reasons.join("; ")}` : ""}.`, { ...session ? { sessionid: session.id } : {} });
|
|
2594
|
+
return verdict;
|
|
2595
|
+
}
|
|
2596
|
+
case "navstate": {
|
|
2597
|
+
const plan = await memory.getplan();
|
|
2598
|
+
if (!plan) throw new Error("No plan is available for a navstate envelope.");
|
|
2599
|
+
const record2 = (await memory.getnavrecords())[0];
|
|
2600
|
+
const session = await memory.getsession();
|
|
2601
|
+
const livetab = session ? await chrome.tabs.get(session.tabid).catch(() => void 0) : void 0;
|
|
2602
|
+
const state = { phase: livetab?.status === "loading" ? "loading" : "complete", ...record2 ? { finalurl: record2.finalurl, redirects: record2.chain } : {} };
|
|
2603
|
+
return JSON.parse(navstateresponse({ navstate: state, plan }));
|
|
2604
|
+
}
|
|
2605
|
+
case "safeties": {
|
|
2606
|
+
const plan = await memory.getplan();
|
|
2607
|
+
if (!plan) throw new Error("No plan is available for a safety envelope.");
|
|
2608
|
+
return JSON.parse(safetyresponse({ verdicts: await memory.getsafeties(), plan }));
|
|
2609
|
+
}
|
|
1142
2610
|
case "stop": {
|
|
1143
2611
|
const session = await memory.getsession();
|
|
1144
2612
|
if (session) await memory.setsession({ ...session, stoppedat: Date.now() });
|
|
@@ -1155,6 +2623,16 @@ chrome.runtime.onMessage.addListener((message, sender, sendresponse) => {
|
|
|
1155
2623
|
handlerequest(message, sender).then((value) => sendresponse({ ok: true, value })).catch((error) => sendresponse({ ok: false, error: error instanceof Error ? error.message : String(error) }));
|
|
1156
2624
|
return true;
|
|
1157
2625
|
});
|
|
2626
|
+
async function reconcilewatches() {
|
|
2627
|
+
for (const watch of await memory.getwatches()) {
|
|
2628
|
+
if (watch.closedat !== void 0) continue;
|
|
2629
|
+
if (!watchclosed(watch.startedat, watch.lifetime, Date.now())) continue;
|
|
2630
|
+
await memory.closewatch(watch.watchid, Date.now());
|
|
2631
|
+
await audit("watch", `Watch ${watch.watchid} of ${watch.kind} closed on service worker restart after its reviewed lifetime of ${watch.lifetime} milliseconds.`, { sessionid: watch.sessionid, stepid: watch.stepid });
|
|
2632
|
+
}
|
|
2633
|
+
}
|
|
2634
|
+
reconcilewatches().catch(() => {
|
|
2635
|
+
});
|
|
1158
2636
|
chrome.runtime.onConnect.addListener((port) => {
|
|
1159
2637
|
if (port.name !== "devthinksidepanel" || port.sender?.id !== chrome.runtime.id || !port.sender.url?.startsWith(chrome.runtime.getURL(""))) return port.disconnect();
|
|
1160
2638
|
port.onMessage.addListener((message) => {
|