@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 CHANGED
@@ -2,17 +2,20 @@
2
2
 
3
3
  Devthink is a **consent-first browser-agent bridge** distributed as a TypeScript library and a Chromium Manifest V3 extension. It turns a user-provided browser objective into a bounded, reviewable plan. The user must start the active-tab session and approve the plan before any page action reaches the browser.
4
4
 
5
- Version: **1.1.33**. License: **GPL-3.0-only**. The repository is `wenathlan/extension`; the npm-compatible scoped package identifier is `@wenathlan/extension`.
5
+ Version: **1.1.35**. License: **GPL-3.0-only**. The repository is `wenathlan/extension`; the npm-compatible scoped package identifier is `@wenathlan/extension`.
6
6
 
7
7
  ## What it does
8
8
 
9
- | Capability | Behavior in 1.1.33 |
9
+ | Capability | Behavior in 1.1.35 |
10
10
  | --- | --- |
11
11
  | Active-tab session | The user starts a short-lived session for one HTTPS tab and one origin; the session records its origin grants. |
12
12
  | Page observation | The extension captures the complete semantic inventory: every interactive element, every form control, every select option and the full page text. |
13
13
  | Plan proposal | A local plan can be created immediately; an optional user-configured HTTPS endpoint can return a typed plan proposal of any length. |
14
14
  | Review gate | Every remote proposal starts in `pending`; it cannot reach the page bridge before explicit approval. |
15
- | Browser tools | Reviewed plans cover ninety action kinds: pointer paths and coordinate clicks, text, aria, name, xpath, index and point target resolution, timed typing, key holds, sliders, dates, colors, shadow dom piercing, iframe entry, dialog answering, retry rules, a complete read vocabulary with clickable maps and verification reads, page mutation under review, and browser-level tab, window, zoom, snapshot and download commands. |
15
+ | Browser tools | Reviewed plans cover one hundred forty seven action kinds: pointer paths and coordinate clicks, text, aria, name, xpath, index and point target resolution, timed typing, key holds, sliders, dates, colors, shadow dom piercing, iframe entry, dialog answering, retry rules, a complete read vocabulary with clickable maps and verification reads, page mutation under review, and browser-level tab, window, zoom, snapshot and download commands. |
16
+ | Navigation mastery | The agent moves anywhere with review: `openlink`, `openprivate`, `reopentab`, `deeplink` and `openclipboard` open reviewed targets in resolved containers, `followlink`, `spanav`, `spawait`, `waitload` and `waiturl` navigate pages and single page app routes, `rewritequery` and `setfragment` edit the current url, `navlist` walks a reviewed url list with per entry progress, `navprofile` applies per site wait profiles, `checksafe` verifies urls before unreviewed origins open, `batchopen` opens curated lists with per url safety states, `navrate` enforces per domain rate limits with user configured windows and ceilings, and the navigation trail, redirect chains and final urls stay recorded for audit. |
17
+ | Observation depth | The agent sees the whole page read only: the accessibility tree beside the dom snapshot, reader views, visible text, outlines, selections, open graph and embedded json state, plus detected lists, tables, pagination, infinite scroll, virtualization, lazy images, sticky overlays and scroll locks offered as plan suggestions. |
18
+ | Watch vocabulary | `watchmutate`, `watchfocus` and `watchbanner` observe the page across a reviewed lifetime window with batched event records, `waitquiet` waits for network quiet under a reviewed threshold, `diffsnapshots` diffs two stored observation versions into added, removed and changed rows, and `deriveselector` ranks stable selector candidates with stability scores; registrations persist across service worker restarts. |
16
19
  | Target resolution | Steps address elements through reviewed `targetref` modes: css selector, visible text, aria role and name, accessible name, xpath, clickable map index or viewport point; ambiguous matches are refused with candidate lists and every resolution returns a matched element summary for review. |
17
20
  | Optional capabilities | `tabs`, `downloads`, `clipboardRead` and `clipboardWrite` are optional permissions; browser kinds check their capability, the review panel can request a grant and every grant is audited. |
18
21
  | Unlimited by choice | Wait durations, plan size, plan expiry, audit retention and outcome retention carry no code ceilings; every bound is a user choice. |
@@ -46,10 +49,10 @@ The endpoint field intentionally has no default URL. Enter a URL such as `https:
46
49
 
47
50
  ```json
48
51
  {
49
- "version": "1.1.33",
52
+ "version": "1.1.35",
50
53
  "objective": "User supplied objective",
51
54
  "session": { "id": "uuid", "tabid": 1, "origin": "https://example.com" },
52
- "observation": { "schemaversion": 2, "url": "https://example.com/path", "interactive": [] },
55
+ "observation": { "schemaversion": 3, "url": "https://example.com/path", "interactive": [] },
53
56
  "capabilities": { "tabs": false, "downloads": false, "clipboardread": false, "clipboardwrite": false }
54
57
  }
55
58
  ```
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export * from "./memory.js";
2
- export { actionrisk as deriveactionrisk, canexecute, hostpattern, normalizeendpoint, resolutionverdict, validatestep, validatetargetref } from "./policy.js";
2
+ export { actionrisk as deriveactionrisk, canexecute, hostpattern, iswatchkind, normalizeendpoint, observationmodeof, resolutionverdict, validatestep, validatetargetref } from "./policy.js";
3
3
  export * from "./protocol.js";
4
4
  export * from "./types.js";
5
5
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAC;AAC5B,OAAO,EAAE,UAAU,IAAI,gBAAgB,EAAE,UAAU,EAAE,WAAW,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,YAAY,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAC7J,cAAc,eAAe,CAAC;AAC9B,cAAc,YAAY,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAC;AAC5B,OAAO,EAAE,UAAU,IAAI,gBAAgB,EAAE,UAAU,EAAE,WAAW,EAAE,WAAW,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,YAAY,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAC7L,cAAc,eAAe,CAAC;AAC9B,cAAc,YAAY,CAAC"}
package/dist/index.js CHANGED
@@ -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(tabid) {
349
+ return this.adapter.get(`navstate${tabid}`);
350
+ }
351
+ /** Replaces the last known navigation state of a tab. */
352
+ async setnavstate(tabid, state) {
353
+ return this.adapter.set(`navstate${tabid}`, 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 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"]);
142
- 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"]);
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.");
@@ -151,6 +376,14 @@ function hostpattern(origin) {
151
376
  if (parsed.protocol !== "https:") throw new Error("Only HTTPS origins can be granted.");
152
377
  return `${parsed.origin}/*`;
153
378
  }
379
+ function iswatchkind(kind) {
380
+ return watchactions.has(kind);
381
+ }
382
+ function observationmodeof(kind) {
383
+ if (watchactions.has(kind) || kind === "waitquiet") return "watching";
384
+ if (kind === "diffsnapshots") return "diffing";
385
+ return "passive";
386
+ }
154
387
  function actionrisk(kind) {
155
388
  if (!allowedactions.has(kind)) throw new Error("Unsupported browser action.");
156
389
  if (sensitiveactions.has(kind)) return "sensitive";
@@ -219,6 +452,35 @@ function origingranted(session, origin) {
219
452
  const grants = session.grants ?? [session.origin];
220
453
  return grants.includes(origin);
221
454
  }
455
+ function originverified(url, grants, verdicts) {
456
+ let origin = "";
457
+ try {
458
+ origin = new URL(url).origin;
459
+ } catch {
460
+ return { allowed: false, reason: "The reviewed navigation URL is invalid." };
461
+ }
462
+ if (grants.includes(origin)) return { allowed: true };
463
+ const covered = verdicts.find((verdict) => verdict.safe && (verdict.url === url || safeorigin(verdict.url) === origin));
464
+ if (covered) return { allowed: true };
465
+ return { allowed: false, reason: `The origin ${origin} is outside the session grants and has no safe checksafe verdict; run checksafe and review it first.` };
466
+ }
467
+ function safeorigin(url) {
468
+ try {
469
+ return new URL(url).origin;
470
+ } catch {
471
+ return "";
472
+ }
473
+ }
474
+ function navigationgranted(session, url) {
475
+ let origin = "";
476
+ try {
477
+ origin = new URL(url).origin;
478
+ } catch {
479
+ return { allowed: false, reason: "The reviewed navigation URL is invalid." };
480
+ }
481
+ if (origingranted(session, origin)) return { allowed: true };
482
+ return { allowed: false, reason: `Navigation to ${origin} leaves the task tab origins and needs the user consent of a session grant first.` };
483
+ }
222
484
  function validateinnerstep(options, origin) {
223
485
  const stepid = options.stepid;
224
486
  const kind = options.kind;
@@ -242,6 +504,68 @@ function validateinnerstep(options, origin) {
242
504
  };
243
505
  return validatestep(inner, origin);
244
506
  }
507
+ function ishttpsurl(value) {
508
+ if (typeof value !== "string" || !value.trim()) return false;
509
+ try {
510
+ return new URL(value).protocol === "https:";
511
+ } catch {
512
+ return false;
513
+ }
514
+ }
515
+ function validatenavtarget(value, kind) {
516
+ if (!value || typeof value !== "object" || Array.isArray(value)) return { allowed: false, reason: "A reviewed navtarget with a url is required in options." };
517
+ const target = value;
518
+ if (!ishttpsurl(target.url)) return { allowed: false, reason: "The reviewed navtarget url must use HTTPS." };
519
+ const container = target.container ?? "tab";
520
+ if (container !== "current" && container !== "tab" && container !== "window" && container !== "private") return { allowed: false, reason: "The reviewed navtarget container must be current, tab, window or private." };
521
+ if (target.position !== void 0 && target.position !== "adjacent" && target.position !== "end") return { allowed: false, reason: "The reviewed navtarget position must be adjacent or end." };
522
+ if (kind === "openprivate" && container !== "private") return { allowed: false, reason: "The openprivate step requires the private container." };
523
+ if (kind === "openlink" && container === "private") return { allowed: false, reason: "The openlink step cannot open the private container; use openprivate." };
524
+ return { allowed: true };
525
+ }
526
+ function validatewaitprofile(value) {
527
+ if (!value || typeof value !== "object" || Array.isArray(value)) return { allowed: false, reason: "A reviewed waitprofile with load signals is required in options." };
528
+ const profile = value;
529
+ 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." };
530
+ if (!nonnegativeoption(profile, "idle")) return { allowed: false, reason: "The reviewed waitprofile idle threshold must be zero or a positive number of milliseconds." };
531
+ if (!nonnegativeoption(profile, "timeout")) return { allowed: false, reason: "The reviewed waitprofile timeout must be zero or a positive number of milliseconds." };
532
+ if (profile.overrides !== void 0) {
533
+ 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." };
534
+ for (const entry of profile.overrides) {
535
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) return { allowed: false, reason: "Every reviewed waitprofile override must be an object with an origin." };
536
+ const override = entry;
537
+ if (!ishttpsurl(override.origin)) return { allowed: false, reason: "Every reviewed waitprofile override origin must use HTTPS." };
538
+ 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." };
539
+ if (!nonnegativeoption(override, "idle") || !nonnegativeoption(override, "timeout")) return { allowed: false, reason: "The reviewed waitprofile override thresholds must be zero or positive numbers." };
540
+ }
541
+ }
542
+ return { allowed: true };
543
+ }
544
+ function validateurlpattern(value) {
545
+ if (!value || typeof value !== "object" || Array.isArray(value)) return { allowed: false, reason: "A reviewed urlpattern is required in options." };
546
+ const pattern = value;
547
+ 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." };
548
+ if (!ishttpsurl(pattern.url)) return { allowed: false, reason: "The reviewed urlpattern url must use HTTPS." };
549
+ if (pattern.query !== void 0) {
550
+ 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." };
551
+ for (const item of Object.values(pattern.query)) if (typeof item !== "string") return { allowed: false, reason: "The reviewed urlpattern query values must be strings." };
552
+ }
553
+ if (pattern.fragment !== void 0 && !isnonempty(pattern.fragment)) return { allowed: false, reason: "The reviewed urlpattern fragment must be a non-empty string." };
554
+ return { allowed: true };
555
+ }
556
+ function validateurllist(options, key) {
557
+ const urls = options[key];
558
+ 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}.` };
559
+ return { allowed: true };
560
+ }
561
+ function validateratelimit(value) {
562
+ 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." };
563
+ const limit = value;
564
+ if (limit.domain !== void 0 && !isnonempty(limit.domain)) return { allowed: false, reason: "The reviewed ratelimit domain must be a non-empty string." };
565
+ 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." };
566
+ 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." };
567
+ return { allowed: true };
568
+ }
245
569
  function validatestep(step, origin) {
246
570
  if (!allowedactions.has(step.kind)) return { allowed: false, reason: "Unsupported action kind." };
247
571
  if (!step.summary.trim()) return { allowed: false, reason: "A human-readable action summary is required." };
@@ -361,6 +685,88 @@ function validatestep(step, origin) {
361
685
  if (!nonnegativeoption(retry, "settle")) return { allowed: false, reason: "The reviewed retry settle window must be zero or a positive number of milliseconds." };
362
686
  if (!nonnegativeoption(retry, "tolerance")) return { allowed: false, reason: "The reviewed retry movement tolerance must be zero or a positive number of pixels." };
363
687
  }
688
+ if (watchactions.has(step.kind)) {
689
+ 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." };
690
+ 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." };
691
+ 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." };
692
+ if (!nonnegativeoption(options, "poll")) return { allowed: false, reason: "The reviewed watch poll interval must be zero or a positive number of milliseconds." };
693
+ }
694
+ if (step.kind === "waitquiet") {
695
+ const rule = options.quietrule;
696
+ if (!rule || typeof rule !== "object" || Array.isArray(rule)) return { allowed: false, reason: "A reviewed quietrule with an idle threshold is required in options." };
697
+ const quiet = rule;
698
+ 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." };
699
+ if (!nonnegativeoption(quiet, "poll")) return { allowed: false, reason: "The reviewed quiet poll interval must be zero or a positive number of milliseconds." };
700
+ if (!nonnegativeoption(quiet, "timeout")) return { allowed: false, reason: "The reviewed quiet timeout must be zero or a positive number of milliseconds." };
701
+ }
702
+ if (step.kind === "diffsnapshots") {
703
+ const versions = options.versions;
704
+ 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." };
705
+ }
706
+ if (step.kind === "openlink" || step.kind === "openprivate" || step.kind === "deeplink") {
707
+ const targetcheck = validatenavtarget(options.navtarget, step.kind);
708
+ if (!targetcheck.allowed) return targetcheck;
709
+ if (step.kind === "deeplink") {
710
+ const app = options.app;
711
+ if (!isnonempty(app)) return { allowed: false, reason: "A reviewed deep link app pattern is required in options." };
712
+ const params = options.params;
713
+ 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." };
714
+ }
715
+ }
716
+ if (step.kind === "waitload" && !nonnegativeoption(options, "timeout")) return { allowed: false, reason: "The waitload timeout must be zero or a positive number of milliseconds." };
717
+ if (step.kind === "waiturl" || step.kind === "spawait") {
718
+ if (step.kind === "waiturl") {
719
+ const patterncheck = validateurlpattern(options.urlpattern);
720
+ if (!patterncheck.allowed) return patterncheck;
721
+ }
722
+ if (!nonnegativeoption(options, "timeout")) return { allowed: false, reason: "The wait timeout must be zero or a positive number of milliseconds." };
723
+ if (!nonnegativeoption(options, "poll")) return { allowed: false, reason: "The wait poll interval must be zero or a positive number of milliseconds." };
724
+ }
725
+ if (step.kind === "followlink") {
726
+ if (options.fragment !== void 0 && typeof options.fragment !== "boolean") return { allowed: false, reason: "The reviewed followlink fragment flag must be a boolean." };
727
+ }
728
+ if (step.kind === "spanav") {
729
+ if (options.routepattern !== void 0) {
730
+ const routecheck = validateurlpattern(options.routepattern);
731
+ if (!routecheck.allowed) return routecheck;
732
+ }
733
+ if (!nonnegativeoption(options, "timeout")) return { allowed: false, reason: "The spanav route timeout must be zero or a positive number of milliseconds." };
734
+ }
735
+ if (step.kind === "rewritequery") {
736
+ const set = options.set;
737
+ const remove = options.remove;
738
+ if (set === void 0 && remove === void 0) return { allowed: false, reason: "Reviewed query parameters to set or remove are required in options." };
739
+ 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." };
740
+ 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." };
741
+ }
742
+ if (step.kind === "navlist") {
743
+ const listcheck = validateurllist(options, "urls");
744
+ if (!listcheck.allowed) return listcheck;
745
+ }
746
+ if (step.kind === "navprofile") {
747
+ const profilecheck = validatewaitprofile(options.waitprofile);
748
+ if (!profilecheck.allowed) return profilecheck;
749
+ }
750
+ if (step.kind === "handleauth" && !ishttpsurl(step.value)) return { allowed: false, reason: "A reviewed HTTPS origin or url is required as the auth target." };
751
+ 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." };
752
+ if (step.kind === "prefetch") {
753
+ const listcheck = validateurllist(options, "urls");
754
+ if (!listcheck.allowed) return listcheck;
755
+ }
756
+ if (step.kind === "preconnect") {
757
+ const origins = options.origins;
758
+ 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." };
759
+ }
760
+ if (step.kind === "reopentab" && step.value !== void 0 && !ishttpsurl(step.value)) return { allowed: false, reason: "The reviewed reopen url must use HTTPS." };
761
+ if (step.kind === "navrate") {
762
+ const limitcheck = validateratelimit(options.ratelimit);
763
+ if (!limitcheck.allowed) return limitcheck;
764
+ }
765
+ if (step.kind === "checksafe" && !ishttpsurl(step.value)) return { allowed: false, reason: "A reviewed HTTPS url is required for the safety check." };
766
+ if (step.kind === "batchopen") {
767
+ const listcheck = validateurllist(options, "urls");
768
+ if (!listcheck.allowed) return listcheck;
769
+ }
364
770
  return { allowed: true };
365
771
  }
366
772
  function sessiongate(input) {
@@ -377,11 +783,42 @@ function canexecute(input) {
377
783
  if (!input.plan || input.plan.state !== "approved") return { allowed: false, reason: "The plan has not received explicit approval." };
378
784
  if (input.plan.expiresat <= now) return { allowed: false, reason: "The approved plan has expired." };
379
785
  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." };
786
+ if (input.step.kind === "readjson" && !origingranted(input.session, input.origin)) return { allowed: false, reason: "The json state read is outside the session origin grants." };
787
+ if (input.step.kind === "navlist") {
788
+ let options = {};
789
+ try {
790
+ options = parseoptions(input.step);
791
+ } catch {
792
+ options = {};
793
+ }
794
+ for (const url of Array.isArray(options.urls) ? options.urls : []) {
795
+ if (typeof url !== "string") continue;
796
+ const navigation = navigationgranted(input.session, url);
797
+ if (!navigation.allowed) return navigation;
798
+ }
799
+ }
800
+ 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") {
801
+ let options = {};
802
+ try {
803
+ options = parseoptions(input.step);
804
+ } catch {
805
+ options = {};
806
+ }
807
+ const grammar = validatestep(input.step, input.origin);
808
+ if (!grammar.allowed) return grammar;
809
+ const grants = input.session?.grants ?? [input.session?.origin ?? input.origin];
810
+ 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];
811
+ for (const target of targets) {
812
+ if (typeof target !== "string" || !target) continue;
813
+ const verified = originverified(target, grants, input.verdicts ?? []);
814
+ if (!verified.allowed) return verified;
815
+ }
816
+ }
380
817
  return validatestep(input.step, input.origin);
381
818
  }
382
819
 
383
820
  // version.ts
384
- var packageversion = "1.1.33";
821
+ var packageversion = "1.1.35";
385
822
 
386
823
  // types.ts
387
824
  var protocolversion = packageversion;
@@ -448,20 +885,61 @@ function mapresponse(input) {
448
885
  function heldkeysreport(input) {
449
886
  return { version: protocolversion, tabid: input.tabid, heldkeys: input.holds };
450
887
  }
888
+ function observationresponse(input) {
889
+ return JSON.stringify({ version: protocolversion, planid: input.plan.id, planstate: input.plan.state, observation: input.observation });
890
+ }
891
+ function eventresponse(input) {
892
+ return JSON.stringify({ version: protocolversion, planid: input.plan.id, planstate: input.plan.state, events: input.events });
893
+ }
894
+ function diffresponse(input) {
895
+ return JSON.stringify({ version: protocolversion, planid: input.plan.id, planstate: input.plan.state, diff: input.diff });
896
+ }
897
+ function signalsreport(input) {
898
+ const signals = input.signals;
899
+ return {
900
+ version: protocolversion,
901
+ ...signals && signals.language !== void 0 ? { language: signals.language } : {},
902
+ ...signals && signals.template !== void 0 ? { template: signals.template } : {},
903
+ ...signals && signals.scrolllocked !== void 0 ? { scrolllocked: signals.scrolllocked } : {},
904
+ ...signals && signals.banner !== void 0 ? { banner: signals.banner } : {}
905
+ };
906
+ }
907
+ function selectorresponse(input) {
908
+ return JSON.stringify({ version: protocolversion, planid: input.plan.id, planstate: input.plan.state, candidates: input.candidates });
909
+ }
910
+ function navstateresponse(input) {
911
+ return JSON.stringify({ version: protocolversion, planid: input.plan.id, planstate: input.plan.state, navstate: input.navstate });
912
+ }
913
+ function trailreport(input) {
914
+ return { version: protocolversion, ...input.sessionid ? { sessionid: input.sessionid } : {}, trail: input.trail };
915
+ }
916
+ function safetyresponse(input) {
917
+ return JSON.stringify({ version: protocolversion, planid: input.plan.id, planstate: input.plan.state, verdicts: input.verdicts });
918
+ }
451
919
  export {
452
920
  canexecute,
453
921
  actionrisk as deriveactionrisk,
922
+ diffresponse,
923
+ eventresponse,
454
924
  heldkeysreport,
455
925
  hostpattern,
926
+ iswatchkind,
456
927
  mapresponse,
928
+ navstateresponse,
457
929
  normalizeendpoint,
930
+ observationmodeof,
931
+ observationresponse,
458
932
  outcomeresponse,
459
933
  parseproposal,
460
934
  protocolversion,
461
935
  randomid,
462
936
  requestbody,
463
937
  resolutionverdict,
938
+ safetyresponse,
939
+ selectorresponse,
464
940
  sessionmemory,
941
+ signalsreport,
942
+ trailreport,
465
943
  validatestep,
466
944
  validatetargetref
467
945
  };