@effect-agent/platform-cloudflare 0.1.0-beta.52 → 0.1.0-beta.54

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.
Files changed (33) hide show
  1. package/dist/Alarm.d.mts +45 -6
  2. package/dist/Alarm.mjs +111 -47
  3. package/dist/Alarm.mjs.map +1 -1
  4. package/dist/BrowserRestCapture.mjs +1 -1
  5. package/dist/{CloudflareBrowser-1jeZx3_g.mjs → CloudflareBrowser-Bj22nNUT.mjs} +12 -13
  6. package/dist/CloudflareBrowser-Bj22nNUT.mjs.map +1 -0
  7. package/dist/CloudflareBrowser.mjs +1 -1
  8. package/dist/CloudflareThreadClient.d.mts +18 -17
  9. package/dist/CloudflareThreadClient.mjs +2 -1
  10. package/dist/CloudflareThreadClient.mjs.map +1 -1
  11. package/dist/ProtectedBrowser.d.mts +5 -3
  12. package/dist/ProtectedBrowser.mjs +180 -36
  13. package/dist/ProtectedBrowser.mjs.map +1 -1
  14. package/dist/{ThreadObject-BSRVxtPI.d.mts → ThreadObject-6_j7YgMV.d.mts} +48 -37
  15. package/dist/{ThreadObject-BY8axaWT.mjs → ThreadObject-D2eP-tEO.mjs} +44 -20
  16. package/dist/ThreadObject-D2eP-tEO.mjs.map +1 -0
  17. package/dist/ThreadObject.d.mts +2 -2
  18. package/dist/ThreadObject.mjs +1 -1
  19. package/dist/index.d.mts +1 -1
  20. package/dist/index.mjs +2 -2
  21. package/package.json +1 -1
  22. package/src/Alarm.ts +242 -72
  23. package/src/CloudflareThreadClient.ts +6 -0
  24. package/src/ThreadObject.ts +1 -0
  25. package/src/internal/browser-quick-action.ts +14 -11
  26. package/src/internal/layers.ts +93 -9
  27. package/src/internal/progress-wait.ts +17 -13
  28. package/src/protected-browser/binding.ts +2 -1
  29. package/src/protected-browser/inspect-frame.ts +62 -19
  30. package/src/protected-browser/native.ts +50 -5
  31. package/src/protected-browser/policy.ts +138 -13
  32. package/dist/CloudflareBrowser-1jeZx3_g.mjs.map +0 -1
  33. package/dist/ThreadObject-BY8axaWT.mjs.map +0 -1
@@ -2,7 +2,7 @@ import { n as BrowserRunSessionLifecycle } from "./browser-session-lifecycle-CUb
2
2
  import { Clock, Context, Crypto, Effect, Layer, Option, Redacted, Schema, Semaphore } from "effect";
3
3
  import puppeteer from "@cloudflare/puppeteer";
4
4
  import { InteractiveBrowserPolicy } from "@effect-agent/sandbox/InteractiveBrowser";
5
- import { BrowserCredentialAccess, CardCredential, CredentialOffer, CredentialOfferMetadata, CredentialOrigin, CredentialTarget, CredentialUseResult, ListCredentialOffers, LoginCredential, ProtectedBrowser, ProtectedBrowserClick, ProtectedBrowserControl, ProtectedBrowserError, ProtectedBrowserNavigate, ProtectedBrowserObservation, UseCredential } from "@effect-agent/sandbox/ProtectedBrowser";
5
+ import { BrowserCredentialAccess, CardCredential, CredentialObservationDecision, CredentialOffer, CredentialOfferMetadata, CredentialOrigin, CredentialTarget, CredentialUseResult, ListCredentialOffers, LoginCredential, ProtectedBrowser, ProtectedBrowserClick, ProtectedBrowserControl, ProtectedBrowserError, ProtectedBrowserFill, ProtectedBrowserNavigate, ProtectedBrowserObservation, UseCredential } from "@effect-agent/sandbox/ProtectedBrowser";
6
6
  //#region src/protected-browser/policy.ts
7
7
  var ProtectedTransportError = class extends Schema.TaggedError()("ProtectedTransportError", { reason: Schema.Literals([
8
8
  "stale-reference",
@@ -42,7 +42,7 @@ const secretFor = (material, role) => {
42
42
  };
43
43
  /**
44
44
  * Fresh private passes only. Account administrators and Browser Rendering token holders are
45
- * trusted operators. No viewer, handoff, raw JavaScript, screenshot or plaintext-fill API exists.
45
+ * trusted operators. No viewer, handoff, raw JavaScript, screenshot or plaintext credential API exists.
46
46
  * Hosts explicitly authorize post-exposure observations for recipients they trust not to echo.
47
47
  */
48
48
  const browserRunProtectedLayer = () => Layer.effect(ProtectedBrowser)(Effect.gen(function* () {
@@ -100,18 +100,48 @@ const browserRunProtectedLayer = () => Layer.effect(ProtectedBrowser)(Effect.gen
100
100
  const pageContext = remote(driver.context);
101
101
  const permitObservation = Effect.gen(function* () {
102
102
  const context = yield* pageContext;
103
+ let origins;
103
104
  if (exposures.length > 0) {
104
105
  observation = "protected";
105
- if ((yield* access.observation({
106
+ const principal = yield* caller;
107
+ const rawDecision = yield* access.observation({
106
108
  ...context,
107
- caller: yield* caller,
109
+ caller: principal,
108
110
  exposures: [...exposures]
109
- }).pipe(Effect.mapError((error) => fail(error.reason)))) !== "trust-recipient-no-credential-echo") return yield* fail("observation-blocked");
111
+ }).pipe(Effect.mapError((error) => fail(error.reason)));
112
+ if (Redacted.value(yield* caller) !== Redacted.value(principal)) return yield* fail("denied");
113
+ const decision = yield* Schema.decodeUnknownEffect(CredentialObservationDecision)(rawDecision).pipe(Effect.mapError(() => fail("observation-blocked")));
114
+ if (decision === "deny") return yield* fail("observation-blocked");
115
+ if (typeof decision !== "string") {
116
+ origins = [...decision.origins];
117
+ if (!origins.includes(context.topOrigin)) return yield* fail("observation-blocked");
118
+ }
110
119
  observation = "approved-after-exposure";
111
120
  }
112
- return context;
121
+ yield* remote(driver.restrictObservation(origins));
122
+ return {
123
+ ...context,
124
+ frameOrigins: context.frameOrigins.filter((origin) => origins === void 0 || origins.includes(origin))
125
+ };
113
126
  });
114
127
  const target = (ref) => remote(driver.target(ref));
128
+ const authorizeAction = Effect.fn("ProtectedBrowser.authorizeAction")(function* (action) {
129
+ if (access.authorizeAction === void 0) {
130
+ if (action._tag === "Submit") return yield* fail("unsupported");
131
+ return;
132
+ }
133
+ const principal = yield* caller;
134
+ yield* access.authorizeAction({
135
+ caller: principal,
136
+ action,
137
+ exposures: [...exposures]
138
+ }).pipe(Effect.mapError((error) => fail(error.reason)));
139
+ if (Redacted.value(yield* caller) !== Redacted.value(principal)) return yield* fail("denied");
140
+ if (action._tag !== "Navigate") {
141
+ const current = yield* target(action.ref);
142
+ if (!sameTarget(current.target, action.target) || current.role !== (action._tag === "Submit" ? "submit" : action.role) || action._tag === "Click" && action.role === "link" && current.url !== action.url) return yield* fail("stale-reference");
143
+ }
144
+ }, Effect.withTracerEnabled(false));
115
145
  const bounded = (result) => {
116
146
  return new TextEncoder().encode(JSON.stringify(result)).byteLength <= policy.maxReturnedBytes ? Effect.succeed(result) : Effect.fail(fail("limit"));
117
147
  };
@@ -155,6 +185,10 @@ const browserRunProtectedLayer = () => Layer.effect(ProtectedBrowser)(Effect.gen
155
185
  }
156
186
  if (url.protocol !== "https:" || url.username || url.password || policy.network._tag === "ExactHosts" && !policy.network.allowedHosts.includes(url.host)) return yield* fail("denied");
157
187
  if (exposures.length > 0) yield* permitObservation;
188
+ yield* authorizeAction({
189
+ _tag: "Navigate",
190
+ url: decoded.url
191
+ });
158
192
  offers.clear();
159
193
  dispatch = "possibly-dispatched";
160
194
  yield* remote(driver.navigate(decoded.url));
@@ -165,7 +199,8 @@ const browserRunProtectedLayer = () => Layer.effect(ProtectedBrowser)(Effect.gen
165
199
  const before = yield* permitObservation;
166
200
  const result = yield* remote(driver.discover);
167
201
  const after = yield* permitObservation;
168
- if (before.document !== after.document || result.document !== after.document) return yield* fail("stale-reference");
202
+ if (before.document !== after.document || result.document !== after.document || result.topOrigin !== after.topOrigin) return yield* fail("stale-reference");
203
+ if (result.frameOrigins.some((origin) => !after.frameOrigins.includes(origin))) return yield* fail("observation-blocked");
169
204
  return yield* bounded(ProtectedBrowserObservation.make({
170
205
  ...result,
171
206
  observation: exposures.length > 0 ? "approved-after-exposure" : "before-exposure"
@@ -175,10 +210,54 @@ const browserRunProtectedLayer = () => Layer.effect(ProtectedBrowser)(Effect.gen
175
210
  const decoded = yield* Schema.decodeUnknownEffect(ProtectedBrowserClick)(request).pipe(Effect.mapError(() => fail("denied")));
176
211
  yield* permitObservation;
177
212
  const control = yield* target(decoded.ref);
178
- if (control.role !== "link" && control.role !== "button") return yield* fail("unsupported");
213
+ if (control.role !== "link" && control.role !== "button" && control.role !== "radio" && control.role !== "checkbox" && control.role !== "submit") return yield* fail("unsupported");
214
+ let action;
215
+ if (control.role === "link") {
216
+ if (control.url === void 0) return yield* fail("unsupported");
217
+ action = {
218
+ _tag: "Click",
219
+ ref: decoded.ref,
220
+ target: control.target,
221
+ role: "link",
222
+ url: control.url
223
+ };
224
+ } else if (control.role === "submit") action = {
225
+ _tag: "Submit",
226
+ ref: decoded.ref,
227
+ target: control.target
228
+ };
229
+ else action = {
230
+ _tag: "Click",
231
+ ref: decoded.ref,
232
+ target: control.target,
233
+ role: control.role
234
+ };
235
+ yield* authorizeAction(action);
179
236
  dispatch = "possibly-dispatched";
180
- yield* remote(driver.click(decoded.ref));
237
+ yield* remote(driver.click(decoded.ref)).pipe(Effect.catch((error) => {
238
+ if (error.reason === "needs-attention") dispatch = "not-dispatched";
239
+ return Effect.fail(error);
240
+ }));
241
+ dispatch = "dispatched";
242
+ if (control.role === "submit") milestone = "submission-dispatched";
243
+ yield* permitObservation;
244
+ })),
245
+ fill: (request) => run(Effect.gen(function* () {
246
+ const decoded = yield* Schema.decodeUnknownEffect(ProtectedBrowserFill)(request).pipe(Effect.mapError(() => fail("denied")));
247
+ yield* permitObservation;
248
+ const control = yield* target(decoded.ref);
249
+ if (control.role !== "text" && control.role !== "select") return yield* fail("unsupported");
250
+ yield* authorizeAction({
251
+ _tag: "Fill",
252
+ ref: decoded.ref,
253
+ target: control.target,
254
+ role: control.role
255
+ });
256
+ yield* remote(driver.fill(decoded.ref, control.role, Redacted.make(decoded.value))).pipe(Effect.provideService(ProtectedBrowserDispatch, { mark: Effect.sync(() => {
257
+ dispatch = "possibly-dispatched";
258
+ }) }));
181
259
  dispatch = "dispatched";
260
+ milestone = "filled";
182
261
  yield* permitObservation;
183
262
  })),
184
263
  listCredentialOffers: (request) => run(Effect.gen(function* () {
@@ -226,7 +305,6 @@ const browserRunProtectedLayer = () => Layer.effect(ProtectedBrowser)(Effect.gen
226
305
  if (offer === void 0 || offer.expires <= (yield* Clock.currentTimeMillis)) return yield* fail("stale-reference");
227
306
  const principal = yield* caller;
228
307
  if (Redacted.value(principal) !== Redacted.value(offer.caller)) return yield* fail("denied");
229
- if (offer.kind === "card" && decoded.submit !== void 0) return yield* fail("unsupported");
230
308
  if (new Set(decoded.fields.map((field) => field.ref)).size !== decoded.fields.length || new Set(decoded.fields.map((field) => field.role)).size !== decoded.fields.length) return yield* fail("denied");
231
309
  const validate = Effect.gen(function* () {
232
310
  if (!sameTarget(offer.target, (yield* target(offer.targetRef)).target)) return yield* fail("stale-reference");
@@ -251,6 +329,7 @@ const browserRunProtectedLayer = () => Layer.effect(ProtectedBrowser)(Effect.gen
251
329
  const authorize = Effect.gen(function* () {
252
330
  if (Redacted.value(yield* caller) !== Redacted.value(principal)) return yield* fail("denied");
253
331
  yield* access.authorize(authorization).pipe(Effect.mapError((error) => fail(error.reason)));
332
+ if (Redacted.value(yield* caller) !== Redacted.value(principal)) return yield* fail("denied");
254
333
  });
255
334
  yield* authorize;
256
335
  yield* validate;
@@ -302,41 +381,66 @@ const maxAttributeLength = 2048;
302
381
  const inspectFrame = `(() => {
303
382
  const doc = document;
304
383
  const forms = [];
305
- const elements = [...doc.querySelectorAll('input,select,button,a[href]')].slice(0, 65);
384
+ const isChoice = el => el instanceof HTMLInputElement && ['radio','checkbox'].includes(el.type);
385
+ const presented = el => {
386
+ if (el.closest('[hidden],[aria-hidden="true"],[inert]') ||
387
+ ['hidden','collapse'].includes(getComputedStyle(el).visibility)) return false;
388
+ for (let parent = el; parent; parent = parent.parentElement) {
389
+ if (getComputedStyle(parent).opacity === '0') return false;
390
+ }
391
+ return true;
392
+ };
393
+ const hasLayout = el => presented(el) &&
394
+ [...el.getClientRects()].some(rect => rect.width > 0 && rect.height > 0);
395
+ const available = el => el.type !== 'hidden' && !el.closest('[hidden],[aria-hidden="true"],[inert]') &&
396
+ (hasLayout(el) || (isChoice(el) && [...(el.labels ?? [])].some(hasLayout)));
397
+ const elements = [...doc.querySelectorAll('input,textarea,select,button,a[href]')].filter(available).slice(0, 65);
398
+ const labelText = el => {
399
+ const label = el.labels?.[0]?.cloneNode(true);
400
+ label?.querySelectorAll('input,textarea,select,script,style,noscript').forEach(child => child.remove());
401
+ return label?.textContent ?? el.getAttribute('aria-label');
402
+ };
306
403
  const describe = (el) => {
307
- if (doc !== document || !el.isConnected || el.ownerDocument !== doc) return null;
404
+ if (doc !== document || !el.isConnected || el.ownerDocument !== doc || !available(el)) return null;
308
405
  const form = el.form ?? null;
309
406
  let formIndex = forms.indexOf(form);
310
407
  if (formIndex < 0) { formIndex = forms.length; forms.push(form); }
311
- const action = form ? (el.hasAttribute('formaction') ? el.formAction : form.action || doc.URL) : doc.URL;
408
+ // Resolved href (including path/query/fragment and base-URL changes) is part of the fingerprint.
409
+ const action = el instanceof HTMLAnchorElement ? el.href : form ? (el.hasAttribute('formaction') ? el.formAction : form.action || doc.URL) : doc.URL;
312
410
  const method = form ? (el.hasAttribute('formmethod') ? el.formMethod : form.method) : '';
313
411
  const enctype = form?.enctype ?? '';
314
412
  const name = el.name ?? '';
315
413
  const completion = el.getAttribute('autocomplete') ?? '';
316
414
  const inputType = el.type ?? '';
415
+ const choiceValue = isChoice(el) ? el.value : '';
317
416
  // Reject before parsing, fingerprinting or CDP transfer. Truncation could hide a target change.
318
- if ([action, method, enctype, name, completion, inputType].some(value => value.length > ${maxAttributeLength})) return null;
417
+ if ([action, method, enctype, name, completion, inputType, choiceValue].some(value => value.length > ${maxAttributeLength})) return null;
319
418
  const type = inputType.toLowerCase();
320
419
  const autocomplete = completion.trim().toLowerCase().split(/\\s+/).at(-1);
321
420
  const cardRoles = { 'cc-name':'card-name', 'cc-number':'card-number', 'cc-exp':'card-expiry',
322
421
  'cc-exp-month':'card-expiry-month', 'cc-exp-year':'card-expiry-year', 'cc-csc':'card-security-code' };
323
422
  let role = 'unsupported';
324
- const nativeField = el instanceof HTMLInputElement || el instanceof HTMLSelectElement;
325
- if (nativeField && form && !['submit','button'].includes(type) && !el.disabled && !el.readOnly && el.getClientRects().length > 0) {
326
- if (el instanceof HTMLInputElement && type === 'password') role = 'password';
327
- else if (['text','email','tel','number','month',''].includes(type) || el instanceof HTMLSelectElement) {
328
- if (cardRoles[autocomplete]) role = cardRoles[autocomplete];
329
- else if (autocomplete === 'username' || autocomplete === 'email') role = 'username';
423
+ const nativeField = el instanceof HTMLInputElement || el instanceof HTMLSelectElement || el instanceof HTMLTextAreaElement;
424
+ if (nativeField && !['submit','button'].includes(type) && !el.matches(':disabled') && !el.readOnly && !(el instanceof HTMLSelectElement && el.multiple)) {
425
+ if (isChoice(el)) role = type;
426
+ else if (type === 'password' || ['current-password','new-password','one-time-code'].includes(autocomplete)) {
427
+ if (type === 'password' && form) role = 'password';
428
+ } else if (['text','email','tel','number','month','search','url','date','time','week','datetime-local','textarea',''].includes(type) || el instanceof HTMLSelectElement) {
429
+ if (cardRoles[autocomplete]) { if (form) role = cardRoles[autocomplete]; }
430
+ else if (autocomplete === 'username' || autocomplete === 'email') { if (form) role = 'username'; }
330
431
  else if (['text','email'].includes(type) && form && form.querySelector('input[type="password"]')) role = 'username';
432
+ else if (el instanceof HTMLSelectElement) { if (!el.multiple) role = 'select'; }
433
+ else role = 'text';
331
434
  }
332
435
  } else if (el instanceof HTMLButtonElement || (el instanceof HTMLInputElement && ['submit','button'].includes(type))) {
333
- if (!el.disabled) role = type === 'submit' && form ? 'submit' : type === 'button' ? 'button' : 'unsupported';
436
+ if (!el.matches(':disabled')) role = type === 'submit' && form ? 'submit' : type === 'button' ? 'button' : 'unsupported';
334
437
  } else if (el instanceof HTMLAnchorElement) role = 'link';
335
- const fingerprint = JSON.stringify([role, action, method, enctype, name, completion, type]);
336
- return { role, formIndex, action, fingerprint,
337
- label: (el.labels?.[0]?.textContent ?? el.getAttribute('aria-label') ?? el.textContent ?? '').slice(0,200) };
438
+ const fingerprint = JSON.stringify([role, action, method, enctype, name, completion, type, choiceValue]);
439
+ return { role, formIndex, action, fingerprint, ...(isChoice(el) ? {checked: el.checked} : {}),
440
+ label: (labelText(el) ?? (nativeField ? '' : el.textContent) ?? '').slice(0,200) };
338
441
  };
339
- const expose = ({role, formIndex, action, label}) => ({role, formIndex, action, label});
442
+ const expose = ({role, formIndex, action, label, checked}) =>
443
+ ({role, formIndex, action, label, ...(checked === undefined ? {} : {checked})});
340
444
  const original = elements.map(describe);
341
445
  const validate = (index) => {
342
446
  const current = describe(elements[index]);
@@ -348,14 +452,32 @@ const inspectFrame = `(() => {
348
452
  doc, elements, original: original.map(current => current && expose(current)), validate,
349
453
  text: () => {
350
454
  if (doc !== document) return null;
351
- const clone = doc.body?.cloneNode(true);
352
- clone?.querySelectorAll('input,textarea,select,script,style,noscript,iframe,object,embed').forEach(el => el.remove());
353
- return (clone?.textContent ?? '').slice(0,65536);
455
+ if (!doc.body) return '';
456
+ const walker = doc.createTreeWalker(doc.body, NodeFilter.SHOW_TEXT);
457
+ const range = doc.createRange();
458
+ let text = '';
459
+ for (let node = walker.nextNode(); node && text.length < 65536; node = walker.nextNode()) {
460
+ const parent = node.parentElement;
461
+ if (!parent || parent.closest('input,textarea,select,script,style,noscript,iframe,object,embed')) continue;
462
+ if (!presented(parent)) continue;
463
+ range.selectNodeContents(node);
464
+ if (![...range.getClientRects()].some(rect => rect.width > 0 && rect.height > 0)) continue;
465
+ text += (node.textContent ?? '').replace(/\\s+/g, ' ') + ' ';
466
+ }
467
+ return text.slice(0,65536);
354
468
  },
355
469
  fill: (index, role, value) => {
356
470
  const current = validate(index);
357
471
  if (!current || current.role !== role) return false;
358
472
  const el = elements[index];
473
+ if (role === 'select') {
474
+ const options = [...el.options].filter(option => !option.matches(':disabled'));
475
+ const values = options.filter(option => option.value === value);
476
+ const matches = values.length > 0 ? values : options.filter(option => option.textContent?.trim() === value);
477
+ if (matches.length !== 1) return 'unsupported';
478
+ value = matches[0].value;
479
+ if ([...el.options].filter(option => option.value === value).length !== 1) return 'unsupported';
480
+ }
359
481
  let prototype = Object.getPrototypeOf(el);
360
482
  let setter;
361
483
  while (prototype && !setter) { setter = Object.getOwnPropertyDescriptor(prototype,'value')?.set; prototype = Object.getPrototypeOf(prototype); }
@@ -370,7 +492,7 @@ const inspectFrame = `(() => {
370
492
  },
371
493
  click: (index) => {
372
494
  const current = validate(index);
373
- if (!current || !['button','submit','link'].includes(current.role)) return false;
495
+ if (!current || !['button','submit','link','radio','checkbox'].includes(current.role)) return false;
374
496
  if (current.role === 'submit' && elements[index].form && !elements[index].form.matches(':valid')) return 'needs-attention';
375
497
  elements[index].click();
376
498
  return true;
@@ -383,7 +505,8 @@ const Description = Schema.Struct({
383
505
  role: ProtectedBrowserControl.fields.role,
384
506
  formIndex: Schema.Natural,
385
507
  action: Schema.String.check(Schema.isMaxLength(maxAttributeLength)),
386
- label: Schema.String.check(Schema.isMaxLength(200))
508
+ label: Schema.String.check(Schema.isMaxLength(200)),
509
+ checked: Schema.optionalKey(Schema.Boolean)
387
510
  });
388
511
  const Descriptions = Schema.Array(Schema.NullOr(Description)).check(Schema.isMaxLength(65));
389
512
  /** One host-private acquired session. The SDK handles and exact-session cleanup have one owner. */
@@ -413,6 +536,7 @@ const makeProtectedNativeTransport = Effect.fn("ProtectedNativeTransport.make")(
413
536
  let closed = false;
414
537
  let violation = false;
415
538
  let documentRef;
539
+ let observationOrigins;
416
540
  const frames = /* @__PURE__ */ new Map();
417
541
  const controls = /* @__PURE__ */ new Map();
418
542
  const origin = (value) => Effect.try({
@@ -442,12 +566,20 @@ const makeProtectedNativeTransport = Effect.fn("ProtectedNativeTransport.make")(
442
566
  closed = true;
443
567
  clear();
444
568
  };
569
+ const observationFrames = Effect.gen(function* () {
570
+ const list = page.frames();
571
+ if (list.length > 32) return yield* transportError("unsupported");
572
+ return yield* Effect.filter(list, (frame) => frame === page.mainFrame() ? Effect.succeed(true) : Effect.try({
573
+ try: () => frame.url() !== "" && new URL(frame.url()).protocol === "https:",
574
+ catch: () => transportError("provider")
575
+ }));
576
+ });
445
577
  const context = Effect.gen(function* () {
446
578
  yield* check;
447
- const list = page.frames();
448
- if (list.length > 16) return yield* transportError("unsupported");
579
+ const list = yield* observationFrames;
449
580
  const topOrigin = yield* origin(page.url());
450
- const frameOrigins = yield* Effect.forEach(list, (frame) => origin(frame.url()));
581
+ const frameOrigins = [...new Set(yield* Effect.forEach(list, (frame) => origin(frame.url())))];
582
+ if (frameOrigins.length > 16) return yield* transportError("unsupported");
451
583
  if (documentRef === void 0) documentRef = yield* uuid;
452
584
  return yield* decode(ProtectedPageContext, {
453
585
  document: documentRef,
@@ -465,7 +597,7 @@ const makeProtectedNativeTransport = Effect.fn("ProtectedNativeTransport.make")(
465
597
  const get = Effect.fn("ProtectedNativeTransport.get")(function* (ref) {
466
598
  yield* check;
467
599
  const state = controls.get(ref);
468
- if (!state || state.expires <= (yield* clock.currentTimeMillis) || !(yield* isCurrent(state.frame))) return yield* transportError("stale-reference");
600
+ if (!state || observationOrigins !== void 0 && !observationOrigins.has(state.control.target.frameOrigin) || state.expires <= (yield* clock.currentTimeMillis) || !(yield* isCurrent(state.frame))) return yield* transportError("stale-reference");
469
601
  const current = yield* remote(() => state.frame.handle.evaluate((held, index) => {
470
602
  if (typeof held !== "object" || held === null) return null;
471
603
  return Reflect.apply(Reflect.get(held, "validate"), held, [index]);
@@ -490,6 +622,11 @@ const makeProtectedNativeTransport = Effect.fn("ProtectedNativeTransport.make")(
490
622
  browser.on("targetcreated", onTarget);
491
623
  }), () => close);
492
624
  return {
625
+ restrictObservation: Effect.fn("ProtectedNativeTransport.restrictObservation")(function* (origins) {
626
+ yield* check;
627
+ observationOrigins = origins === void 0 ? void 0 : new Set(yield* decode(Schema.Array(CredentialOrigin), origins));
628
+ for (const [ref, state] of controls) if (observationOrigins !== void 0 && !observationOrigins.has(state.control.target.frameOrigin)) controls.delete(ref);
629
+ }),
493
630
  context,
494
631
  invalidate,
495
632
  close,
@@ -509,7 +646,11 @@ const makeProtectedNativeTransport = Effect.fn("ProtectedNativeTransport.make")(
509
646
  const discovered = [];
510
647
  let text = "";
511
648
  let truncated = false;
512
- for (const frame of page.frames()) {
649
+ const frameOrigins = /* @__PURE__ */ new Set();
650
+ for (const frame of yield* observationFrames) {
651
+ const frameOrigin = yield* origin(frame.url());
652
+ if (observationOrigins !== void 0 && !observationOrigins.has(frameOrigin)) continue;
653
+ frameOrigins.add(frameOrigin);
513
654
  if (typeof frame.isolatedRealm !== "function") return yield* transportError("unsupported");
514
655
  const handle = yield* remote(() => frame.isolatedRealm().evaluateHandle(inspectFrame));
515
656
  const state = {
@@ -543,6 +684,8 @@ const makeProtectedNativeTransport = Effect.fn("ProtectedNativeTransport.make")(
543
684
  ref: yield* uuid,
544
685
  role: desc.role,
545
686
  label: desc.label,
687
+ ...desc.checked === void 0 ? {} : { checked: desc.checked },
688
+ ...desc.role === "link" ? { url: desc.action } : {},
546
689
  target: CredentialTarget.make({
547
690
  topOrigin: before.topOrigin,
548
691
  frameOrigin: yield* origin(frame.url()),
@@ -573,6 +716,7 @@ const makeProtectedNativeTransport = Effect.fn("ProtectedNativeTransport.make")(
573
716
  if ((yield* context).document !== before.document) return yield* transportError("stale-reference");
574
717
  return yield* decode(ProtectedDiscovery, {
575
718
  ...before,
719
+ frameOrigins: [...frameOrigins],
576
720
  text,
577
721
  controls: discovered,
578
722
  truncated
@@ -658,7 +802,7 @@ const browserRunProtectedBindingLayer = (options) => Layer.effect(BrowserRunProt
658
802
  return options.browser.fetch(request);
659
803
  } }, {
660
804
  recording: false,
661
- keep_alive: Math.max(1e4, policy.maxElapsedMillis)
805
+ keep_alive: Math.min(6e5, Math.max(1e4, policy.maxElapsedMillis))
662
806
  });
663
807
  sessionId = Redacted.make(Schema.decodeUnknownSync(Schema.String.check(Schema.isUUID()))(acquired.sessionId));
664
808
  if (!recordingDisabled || signal.aborted || invalid) {