@effect-agent/platform-cloudflare 0.1.0-beta.51 → 0.1.0-beta.53
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/dist/Alarm.d.mts +45 -6
- package/dist/Alarm.mjs +111 -47
- package/dist/Alarm.mjs.map +1 -1
- package/dist/CloudflareSubscriptions.d.mts +9 -6
- package/dist/CloudflareSubscriptions.mjs +3 -2
- package/dist/CloudflareSubscriptions.mjs.map +1 -1
- package/dist/CloudflareThreadClient.d.mts +24 -24
- package/dist/ProtectedBrowser.d.mts +5 -3
- package/dist/ProtectedBrowser.mjs +180 -36
- package/dist/ProtectedBrowser.mjs.map +1 -1
- package/dist/{ThreadObject-BY8axaWT.mjs → ThreadObject-OuKI0mBU.mjs} +33 -6
- package/dist/ThreadObject-OuKI0mBU.mjs.map +1 -0
- package/dist/{ThreadObject-sDr1JBCj.d.mts → ThreadObject-d-ui3RZ-.d.mts} +26 -15
- package/dist/ThreadObject.d.mts +2 -2
- package/dist/ThreadObject.mjs +1 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +1 -1
- package/package.json +1 -1
- package/src/Alarm.ts +242 -72
- package/src/CloudflareSubscriptions.ts +25 -6
- package/src/ThreadObject.ts +1 -0
- package/src/internal/layers.ts +93 -9
- package/src/protected-browser/binding.ts +2 -1
- package/src/protected-browser/inspect-frame.ts +62 -19
- package/src/protected-browser/native.ts +50 -5
- package/src/protected-browser/policy.ts +138 -13
- 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
|
|
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
|
-
|
|
106
|
+
const principal = yield* caller;
|
|
107
|
+
const rawDecision = yield* access.observation({
|
|
106
108
|
...context,
|
|
107
|
-
caller:
|
|
109
|
+
caller: principal,
|
|
108
110
|
exposures: [...exposures]
|
|
109
|
-
}).pipe(Effect.mapError((error) => fail(error.reason)))
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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 &&
|
|
326
|
-
if (el
|
|
327
|
-
else if (
|
|
328
|
-
if (
|
|
329
|
-
|
|
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
|
|
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}) =>
|
|
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
|
-
|
|
352
|
-
|
|
353
|
-
|
|
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 =
|
|
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
|
-
|
|
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) {
|