@effect-agent/platform-cloudflare 0.1.0-beta.8 → 0.1.0-beta.81
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 +184 -0
- package/dist/Alarm.mjs +451 -0
- package/dist/Alarm.mjs.map +1 -0
- package/dist/BrowserRestCapture.d.mts +35 -0
- package/dist/BrowserRestCapture.mjs +238 -0
- package/dist/BrowserRestCapture.mjs.map +1 -0
- package/dist/BrowserRestCrawl.d.mts +17 -0
- package/dist/BrowserRestCrawl.mjs +244 -0
- package/dist/BrowserRestCrawl.mjs.map +1 -0
- package/dist/CloudflareAiGateway.d.mts +64 -0
- package/dist/CloudflareAiGateway.mjs +70 -0
- package/dist/CloudflareAiGateway.mjs.map +1 -0
- package/dist/CloudflareBindings.d.mts +112 -0
- package/dist/CloudflareBindings.mjs +101 -0
- package/dist/CloudflareBindings.mjs.map +1 -0
- package/dist/CloudflareBrowser-D4YqaTAl.mjs +495 -0
- package/dist/CloudflareBrowser-D4YqaTAl.mjs.map +1 -0
- package/dist/CloudflareBrowser-DJq53niJ.d.mts +80 -0
- package/dist/CloudflareBrowser.d.mts +2 -0
- package/dist/CloudflareBrowser.mjs +2 -0
- package/dist/CloudflareCodeMode.d.mts +44 -0
- package/dist/CloudflareCodeMode.mjs +616 -0
- package/dist/CloudflareCodeMode.mjs.map +1 -0
- package/dist/CloudflareConfig-f3CqTel1.d.mts +101 -0
- package/dist/CloudflareConfig.d.mts +2 -0
- package/dist/CloudflareConfig.mjs +122 -0
- package/dist/CloudflareConfig.mjs.map +1 -0
- package/dist/CloudflareMemory.d.mts +142 -0
- package/dist/CloudflareMemory.mjs +195 -0
- package/dist/CloudflareMemory.mjs.map +1 -0
- package/dist/CloudflareScheduling.d.mts +52 -0
- package/dist/CloudflareScheduling.mjs +389 -0
- package/dist/CloudflareScheduling.mjs.map +1 -0
- package/dist/CloudflareSubscriptions.d.mts +87 -0
- package/dist/CloudflareSubscriptions.mjs +524 -0
- package/dist/CloudflareSubscriptions.mjs.map +1 -0
- package/dist/CloudflareThreadClient.d.mts +1286 -0
- package/dist/CloudflareThreadClient.mjs +386 -0
- package/dist/CloudflareThreadClient.mjs.map +1 -0
- package/dist/InteractiveBrowser.d.mts +144 -0
- package/dist/InteractiveBrowser.mjs +1090 -0
- package/dist/InteractiveBrowser.mjs.map +1 -0
- package/dist/ProtectedBrowser.d.mts +65 -0
- package/dist/ProtectedBrowser.mjs +859 -0
- package/dist/ProtectedBrowser.mjs.map +1 -0
- package/dist/ThreadObject-BpwkEc5E.mjs +836 -0
- package/dist/ThreadObject-BpwkEc5E.mjs.map +1 -0
- package/dist/ThreadObject-NbFOWK1I.d.mts +816 -0
- package/dist/ThreadObject.d.mts +2 -0
- package/dist/ThreadObject.mjs +3 -0
- package/dist/WakeScheduler.d.mts +23 -0
- package/dist/WakeScheduler.mjs +60 -0
- package/dist/WakeScheduler.mjs.map +1 -0
- package/dist/boundary-BguazVkh.mjs +42 -0
- package/dist/boundary-BguazVkh.mjs.map +1 -0
- package/dist/browser-session-lifecycle-CUbVfEgP.mjs +84 -0
- package/dist/browser-session-lifecycle-CUbVfEgP.mjs.map +1 -0
- package/dist/browser-session-lifecycle-DJpqLXy_.d.mts +21 -0
- package/dist/index.d.mts +13 -1547
- package/dist/index.mjs +13 -1636
- package/dist/prepared-admission-DSi55W-7.mjs +73 -0
- package/dist/prepared-admission-DSi55W-7.mjs.map +1 -0
- package/dist/rolldown-runtime-D7D4PA-g.mjs +13 -0
- package/package.json +1 -53
- package/src/Alarm.ts +967 -0
- package/src/BrowserRestCapture.ts +477 -0
- package/src/BrowserRestCrawl.ts +540 -0
- package/src/CloudflareAiGateway.ts +170 -0
- package/src/CloudflareBindings.ts +180 -0
- package/src/CloudflareBrowser.ts +15 -0
- package/src/{code-mode-executor.ts → CloudflareCodeMode.ts} +372 -204
- package/src/{config.ts → CloudflareConfig.ts} +9 -8
- package/src/CloudflareMemory.ts +406 -0
- package/src/CloudflareScheduling.ts +733 -0
- package/src/CloudflareSubscriptions.ts +1045 -0
- package/src/CloudflareThreadClient.ts +807 -0
- package/src/InteractiveBrowser.ts +2357 -0
- package/src/ProtectedBrowser.ts +2 -0
- package/src/ThreadObject.ts +1179 -0
- package/src/{wake-scheduler.ts → WakeScheduler.ts} +23 -23
- package/src/index.ts +12 -23
- package/src/internal/boundary.ts +55 -0
- package/src/internal/browser-quick-action.ts +857 -0
- package/src/internal/browser-session-lifecycle.ts +160 -0
- package/src/internal/layers.ts +751 -0
- package/src/internal/message-delivery.ts +148 -0
- package/src/internal/prepared-admission.ts +116 -0
- package/src/internal/progress-wait.ts +121 -0
- package/src/internal/transport.ts +42 -0
- package/src/protected-browser/binding.ts +187 -0
- package/src/protected-browser/inspect-frame.ts +125 -0
- package/src/protected-browser/native.ts +429 -0
- package/src/protected-browser/policy.ts +719 -0
- package/dist/index.mjs.map +0 -1
- package/src/alarm.ts +0 -334
- package/src/bindings.ts +0 -145
- package/src/client.ts +0 -646
- package/src/conversation-object.ts +0 -768
- package/src/layers.ts +0 -376
- package/src/transport.ts +0 -38
|
@@ -0,0 +1,859 @@
|
|
|
1
|
+
import { n as BrowserRunSessionLifecycle } from "./browser-session-lifecycle-CUbVfEgP.mjs";
|
|
2
|
+
import { Clock, Context, Crypto, Effect, Layer, Option, Redacted, Schema, Semaphore } from "effect";
|
|
3
|
+
import puppeteer from "@cloudflare/puppeteer";
|
|
4
|
+
import { InteractiveBrowserPolicy } from "@effect-agent/sandbox/InteractiveBrowser";
|
|
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
|
+
//#region src/protected-browser/policy.ts
|
|
7
|
+
var ProtectedTransportError = class extends Schema.TaggedError()("ProtectedTransportError", { reason: Schema.Literals([
|
|
8
|
+
"stale-reference",
|
|
9
|
+
"needs-attention",
|
|
10
|
+
"unsupported",
|
|
11
|
+
"provider"
|
|
12
|
+
]) }) {};
|
|
13
|
+
/** Per-write evidence authority, provided by the policy immediately around a transport fill. */
|
|
14
|
+
var ProtectedBrowserDispatch = class extends Context.Service()("@effect-agent/platform-cloudflare/ProtectedBrowserDispatch") {};
|
|
15
|
+
/** Adapter-private injection seam for deterministic tests, not a model capability. */
|
|
16
|
+
var BrowserRunProtectedTransport = class extends Context.Service()("@effect-agent/platform-cloudflare/BrowserRunProtectedTransport") {};
|
|
17
|
+
const ProtectedPageContext = Schema.Struct({
|
|
18
|
+
document: Schema.String.check(Schema.isUUID()),
|
|
19
|
+
topOrigin: CredentialOrigin,
|
|
20
|
+
frameOrigins: Schema.Array(CredentialOrigin).check(Schema.isMaxLength(16))
|
|
21
|
+
});
|
|
22
|
+
const ProtectedDiscovery = Schema.Struct({
|
|
23
|
+
...ProtectedPageContext.fields,
|
|
24
|
+
text: Schema.String.check(Schema.isMaxLength(65536)),
|
|
25
|
+
controls: Schema.Array(ProtectedBrowserControl).check(Schema.isMaxLength(64)),
|
|
26
|
+
truncated: Schema.Boolean
|
|
27
|
+
});
|
|
28
|
+
const sameTarget = (a, b) => a.topOrigin === b.topOrigin && a.frameOrigin === b.frameOrigin && a.recipientOrigin === b.recipientOrigin && a.document === b.document && a.frame === b.frame && a.form === b.form;
|
|
29
|
+
const kindFor = (role) => role === "username" || role === "password" ? "login" : role.startsWith("card-") ? "card" : void 0;
|
|
30
|
+
const secretFor = (material, role) => {
|
|
31
|
+
if (material._tag === "LoginCredential") return role === "username" ? material.username : role === "password" ? material.password : void 0;
|
|
32
|
+
switch (role) {
|
|
33
|
+
case "card-name": return material.name;
|
|
34
|
+
case "card-number": return material.number;
|
|
35
|
+
case "card-expiry": return material.expiry;
|
|
36
|
+
case "card-expiry-month": return material.expiryMonth;
|
|
37
|
+
case "card-expiry-year": return material.expiryYear;
|
|
38
|
+
case "card-security-code": return material.securityCode;
|
|
39
|
+
case "username":
|
|
40
|
+
case "password": return;
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* Fresh private passes only. Account administrators and Browser Rendering token holders are
|
|
45
|
+
* trusted operators. No viewer, handoff, raw JavaScript, screenshot or plaintext credential API exists.
|
|
46
|
+
* Hosts explicitly authorize post-exposure observations for recipients they trust not to echo.
|
|
47
|
+
*/
|
|
48
|
+
const browserRunProtectedLayer = () => Layer.effect(ProtectedBrowser)(Effect.gen(function* () {
|
|
49
|
+
const transport = yield* BrowserRunProtectedTransport;
|
|
50
|
+
const crypto = yield* Crypto.Crypto;
|
|
51
|
+
return { open: Effect.fn("ProtectedBrowser.open")(function* (input) {
|
|
52
|
+
const initialError = (reason) => new ProtectedBrowserError({
|
|
53
|
+
reason,
|
|
54
|
+
dispatch: "not-dispatched",
|
|
55
|
+
milestone: "none",
|
|
56
|
+
observation: "closed",
|
|
57
|
+
cleanup: "not-requested"
|
|
58
|
+
});
|
|
59
|
+
const decodedPolicy = yield* Schema.decodeUnknownEffect(InteractiveBrowserPolicy)(input).pipe(Effect.mapError(() => initialError("denied")));
|
|
60
|
+
if (decodedPolicy.network._tag === "PublicWeb") return yield* initialError("unsupported");
|
|
61
|
+
const policy = InteractiveBrowserPolicy.make({
|
|
62
|
+
...decodedPolicy,
|
|
63
|
+
network: decodedPolicy.network._tag === "ExactHosts" ? {
|
|
64
|
+
_tag: "ExactHosts",
|
|
65
|
+
allowedHosts: [...decodedPolicy.network.allowedHosts]
|
|
66
|
+
} : { _tag: "Unrestricted" }
|
|
67
|
+
});
|
|
68
|
+
const access = yield* BrowserCredentialAccess;
|
|
69
|
+
const started = yield* Clock.currentTimeMillis;
|
|
70
|
+
const driver = yield* transport.open(policy);
|
|
71
|
+
const lock = yield* Semaphore.make(1);
|
|
72
|
+
let observation = "before-exposure";
|
|
73
|
+
let cleanup = "not-requested";
|
|
74
|
+
let actions = 0;
|
|
75
|
+
let dispatch = "not-dispatched";
|
|
76
|
+
let milestone = "none";
|
|
77
|
+
const exposures = [];
|
|
78
|
+
const offers = /* @__PURE__ */ new Map();
|
|
79
|
+
const fail = (reason) => new ProtectedBrowserError({
|
|
80
|
+
reason,
|
|
81
|
+
dispatch,
|
|
82
|
+
milestone,
|
|
83
|
+
observation,
|
|
84
|
+
cleanup
|
|
85
|
+
});
|
|
86
|
+
const close = yield* Effect.cached(Effect.uninterruptible(Effect.gen(function* () {
|
|
87
|
+
observation = "closed";
|
|
88
|
+
offers.clear();
|
|
89
|
+
driver.invalidate();
|
|
90
|
+
cleanup = yield* driver.close.pipe(Effect.interruptible, Effect.timeoutOrElse({
|
|
91
|
+
duration: "10 seconds",
|
|
92
|
+
orElse: () => Effect.succeed("unconfirmed")
|
|
93
|
+
}), Effect.catchCause(() => Effect.succeed("unconfirmed")));
|
|
94
|
+
return cleanup;
|
|
95
|
+
})));
|
|
96
|
+
yield* Effect.addFinalizer(() => close.pipe(Effect.flatMap((result) => result === "unconfirmed" ? Effect.logWarning("Protected browser exact-session closure unconfirmed") : Effect.void)));
|
|
97
|
+
const remote = (effect) => effect.pipe(Effect.mapError((error) => fail(error.reason)));
|
|
98
|
+
const decode = (schema, value) => Schema.decodeUnknownEffect(schema)(value).pipe(Effect.mapError(() => fail("provider")));
|
|
99
|
+
const caller = access.caller.pipe(Effect.mapError((error) => fail(error.reason)));
|
|
100
|
+
const pageContext = remote(driver.context);
|
|
101
|
+
const permitObservation = Effect.gen(function* () {
|
|
102
|
+
const context = yield* pageContext;
|
|
103
|
+
let origins;
|
|
104
|
+
if (exposures.length > 0) {
|
|
105
|
+
observation = "protected";
|
|
106
|
+
const principal = yield* caller;
|
|
107
|
+
const rawDecision = yield* access.observation({
|
|
108
|
+
...context,
|
|
109
|
+
caller: principal,
|
|
110
|
+
exposures: [...exposures]
|
|
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
|
+
}
|
|
119
|
+
observation = "approved-after-exposure";
|
|
120
|
+
}
|
|
121
|
+
yield* remote(driver.restrictObservation(origins));
|
|
122
|
+
return {
|
|
123
|
+
...context,
|
|
124
|
+
frameOrigins: context.frameOrigins.filter((origin) => origins === void 0 || origins.includes(origin))
|
|
125
|
+
};
|
|
126
|
+
});
|
|
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));
|
|
145
|
+
const bounded = (result) => {
|
|
146
|
+
return new TextEncoder().encode(JSON.stringify(result)).byteLength <= policy.maxReturnedBytes ? Effect.succeed(result) : Effect.fail(fail("limit"));
|
|
147
|
+
};
|
|
148
|
+
const run = (effect) => lock.withPermitsIfAvailable(1)(Effect.gen(function* () {
|
|
149
|
+
dispatch = "not-dispatched";
|
|
150
|
+
milestone = "none";
|
|
151
|
+
if (observation === "closed") return yield* fail("closed");
|
|
152
|
+
if (++actions > policy.maxActions) return yield* fail("limit");
|
|
153
|
+
const remaining = policy.maxElapsedMillis - ((yield* Clock.currentTimeMillis) - started);
|
|
154
|
+
if (remaining <= 0) {
|
|
155
|
+
yield* close;
|
|
156
|
+
return yield* fail("timeout");
|
|
157
|
+
}
|
|
158
|
+
return yield* effect.pipe(Effect.timeoutOrElse({
|
|
159
|
+
duration: remaining,
|
|
160
|
+
orElse: () => Effect.fail(fail("timeout"))
|
|
161
|
+
}), Effect.catchCause((cause) => Effect.gen(function* () {
|
|
162
|
+
const error = cause.reasons.find((reason) => reason._tag === "Fail" && Schema.is(ProtectedBrowserError)(reason.error));
|
|
163
|
+
const reason = error?._tag === "Fail" && Schema.is(ProtectedBrowserError)(error.error) ? error.error.reason : "provider";
|
|
164
|
+
const interrupt = cause.reasons.some((reason) => reason._tag === "Interrupt");
|
|
165
|
+
if (dispatch !== "not-dispatched" || interrupt || reason === "provider" || reason === "timeout") yield* close;
|
|
166
|
+
if (interrupt) return yield* Effect.interrupt;
|
|
167
|
+
return yield* fail(dispatch === "possibly-dispatched" && reason === "provider" ? "outcome-unknown" : reason);
|
|
168
|
+
})), Effect.onInterrupt(() => close), Effect.withTracerEnabled(false));
|
|
169
|
+
})).pipe(Effect.flatMap((result) => Option.isSome(result) ? Effect.succeed(result.value) : Effect.fail(new ProtectedBrowserError({
|
|
170
|
+
reason: "busy",
|
|
171
|
+
dispatch: "not-dispatched",
|
|
172
|
+
milestone: "none",
|
|
173
|
+
observation,
|
|
174
|
+
cleanup
|
|
175
|
+
}))));
|
|
176
|
+
return {
|
|
177
|
+
close,
|
|
178
|
+
navigate: (request) => run(Effect.gen(function* () {
|
|
179
|
+
const decoded = yield* Schema.decodeUnknownEffect(ProtectedBrowserNavigate)(request).pipe(Effect.mapError(() => fail("denied")));
|
|
180
|
+
let url;
|
|
181
|
+
try {
|
|
182
|
+
url = new URL(decoded.url);
|
|
183
|
+
} catch {
|
|
184
|
+
return yield* fail("denied");
|
|
185
|
+
}
|
|
186
|
+
if (url.protocol !== "https:" || url.username || url.password || policy.network._tag === "ExactHosts" && !policy.network.allowedHosts.includes(url.host)) return yield* fail("denied");
|
|
187
|
+
if (exposures.length > 0) yield* permitObservation;
|
|
188
|
+
yield* authorizeAction({
|
|
189
|
+
_tag: "Navigate",
|
|
190
|
+
url: decoded.url
|
|
191
|
+
});
|
|
192
|
+
offers.clear();
|
|
193
|
+
dispatch = "possibly-dispatched";
|
|
194
|
+
yield* remote(driver.navigate(decoded.url));
|
|
195
|
+
dispatch = "dispatched";
|
|
196
|
+
yield* permitObservation;
|
|
197
|
+
})),
|
|
198
|
+
observe: run(Effect.gen(function* () {
|
|
199
|
+
const before = yield* permitObservation;
|
|
200
|
+
const result = yield* remote(driver.discover);
|
|
201
|
+
const after = yield* permitObservation;
|
|
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");
|
|
204
|
+
return yield* bounded(ProtectedBrowserObservation.make({
|
|
205
|
+
...result,
|
|
206
|
+
observation: exposures.length > 0 ? "approved-after-exposure" : "before-exposure"
|
|
207
|
+
}));
|
|
208
|
+
})),
|
|
209
|
+
click: (request) => run(Effect.gen(function* () {
|
|
210
|
+
const decoded = yield* Schema.decodeUnknownEffect(ProtectedBrowserClick)(request).pipe(Effect.mapError(() => fail("denied")));
|
|
211
|
+
yield* permitObservation;
|
|
212
|
+
const control = yield* target(decoded.ref);
|
|
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);
|
|
236
|
+
dispatch = "possibly-dispatched";
|
|
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
|
+
}) }));
|
|
259
|
+
dispatch = "dispatched";
|
|
260
|
+
milestone = "filled";
|
|
261
|
+
yield* permitObservation;
|
|
262
|
+
})),
|
|
263
|
+
listCredentialOffers: (request) => run(Effect.gen(function* () {
|
|
264
|
+
const decoded = yield* Schema.decodeUnknownEffect(ListCredentialOffers)(request).pipe(Effect.mapError(() => fail("denied")));
|
|
265
|
+
yield* permitObservation;
|
|
266
|
+
const control = yield* target(decoded.target);
|
|
267
|
+
if (kindFor(control.role) !== decoded.kind) return yield* fail("unsupported");
|
|
268
|
+
const principal = yield* caller;
|
|
269
|
+
const candidates = yield* access.list({
|
|
270
|
+
caller: principal,
|
|
271
|
+
kind: decoded.kind,
|
|
272
|
+
target: control.target
|
|
273
|
+
}).pipe(Effect.mapError((error) => fail(error.reason)));
|
|
274
|
+
const now = yield* Clock.currentTimeMillis;
|
|
275
|
+
for (const [ref, offer] of offers) if (offer.expires <= now) offers.delete(ref);
|
|
276
|
+
if (candidates.length > 16 || offers.size + candidates.length > 64) return yield* fail("limit");
|
|
277
|
+
if (!sameTarget(control.target, (yield* target(decoded.target)).target)) return yield* fail("stale-reference");
|
|
278
|
+
const expires = now + 6e4;
|
|
279
|
+
const result = [];
|
|
280
|
+
const pending = /* @__PURE__ */ new Map();
|
|
281
|
+
for (const candidate of candidates) {
|
|
282
|
+
const metadata = yield* decode(CredentialOfferMetadata, candidate.metadata);
|
|
283
|
+
const ref = yield* crypto.randomUUIDv4.pipe(Effect.mapError(() => fail("provider")));
|
|
284
|
+
pending.set(ref, {
|
|
285
|
+
caller: principal,
|
|
286
|
+
key: candidate.key,
|
|
287
|
+
target: control.target,
|
|
288
|
+
targetRef: decoded.target,
|
|
289
|
+
kind: decoded.kind,
|
|
290
|
+
expires
|
|
291
|
+
});
|
|
292
|
+
result.push(CredentialOffer.make({
|
|
293
|
+
ref,
|
|
294
|
+
kind: decoded.kind,
|
|
295
|
+
metadata
|
|
296
|
+
}));
|
|
297
|
+
}
|
|
298
|
+
yield* bounded(result);
|
|
299
|
+
for (const [ref, offer] of pending) offers.set(ref, offer);
|
|
300
|
+
return result;
|
|
301
|
+
})),
|
|
302
|
+
useCredential: (request) => run(Effect.gen(function* () {
|
|
303
|
+
const decoded = yield* Schema.decodeUnknownEffect(UseCredential)(request).pipe(Effect.mapError(() => fail("denied")));
|
|
304
|
+
const offer = offers.get(decoded.offer);
|
|
305
|
+
if (offer === void 0 || offer.expires <= (yield* Clock.currentTimeMillis)) return yield* fail("stale-reference");
|
|
306
|
+
const principal = yield* caller;
|
|
307
|
+
if (Redacted.value(principal) !== Redacted.value(offer.caller)) return yield* fail("denied");
|
|
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");
|
|
309
|
+
const validate = Effect.gen(function* () {
|
|
310
|
+
if (!sameTarget(offer.target, (yield* target(offer.targetRef)).target)) return yield* fail("stale-reference");
|
|
311
|
+
for (const field of decoded.fields) {
|
|
312
|
+
const current = yield* target(field.ref);
|
|
313
|
+
if (field.role !== current.role || kindFor(field.role) !== offer.kind || !sameTarget(offer.target, current.target)) return yield* fail("denied");
|
|
314
|
+
}
|
|
315
|
+
if (decoded.submit !== void 0) {
|
|
316
|
+
const submit = yield* target(decoded.submit);
|
|
317
|
+
if (submit.role !== "submit" || !sameTarget(offer.target, submit.target)) return yield* fail("denied");
|
|
318
|
+
}
|
|
319
|
+
});
|
|
320
|
+
yield* validate;
|
|
321
|
+
const authorization = {
|
|
322
|
+
caller: principal,
|
|
323
|
+
key: offer.key,
|
|
324
|
+
kind: offer.kind,
|
|
325
|
+
target: offer.target,
|
|
326
|
+
roles: decoded.fields.map((field) => field.role),
|
|
327
|
+
submit: decoded.submit !== void 0
|
|
328
|
+
};
|
|
329
|
+
const authorize = Effect.gen(function* () {
|
|
330
|
+
if (Redacted.value(yield* caller) !== Redacted.value(principal)) return yield* fail("denied");
|
|
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");
|
|
333
|
+
});
|
|
334
|
+
yield* authorize;
|
|
335
|
+
yield* validate;
|
|
336
|
+
const raw = yield* access.resolve(authorization).pipe(Effect.mapError((error) => fail(error.reason)));
|
|
337
|
+
const material = yield* Schema.decodeUnknownEffect(offer.kind === "login" ? LoginCredential : CardCredential)(raw).pipe(Effect.mapError(() => fail("resolver")));
|
|
338
|
+
for (const field of decoded.fields) if (secretFor(material, field.role) === void 0) return yield* fail("missing-credential");
|
|
339
|
+
offers.delete(decoded.offer);
|
|
340
|
+
for (const field of decoded.fields) {
|
|
341
|
+
yield* validate;
|
|
342
|
+
yield* authorize;
|
|
343
|
+
const value = secretFor(material, field.role);
|
|
344
|
+
if (value === void 0) return yield* fail("missing-credential");
|
|
345
|
+
observation = "protected";
|
|
346
|
+
yield* remote(driver.fill(field.ref, field.role, value)).pipe(Effect.provideService(ProtectedBrowserDispatch, { mark: Effect.sync(() => {
|
|
347
|
+
dispatch = "possibly-dispatched";
|
|
348
|
+
if (!exposures.some((target) => sameTarget(target, offer.target))) exposures.push(offer.target);
|
|
349
|
+
}) }));
|
|
350
|
+
dispatch = "dispatched";
|
|
351
|
+
milestone = "partial-fill";
|
|
352
|
+
}
|
|
353
|
+
milestone = "filled";
|
|
354
|
+
if (decoded.submit !== void 0) {
|
|
355
|
+
yield* validate;
|
|
356
|
+
yield* authorize;
|
|
357
|
+
const ref = decoded.submit;
|
|
358
|
+
dispatch = "possibly-dispatched";
|
|
359
|
+
yield* remote(driver.click(ref)).pipe(Effect.catch((error) => {
|
|
360
|
+
if (error.reason === "needs-attention") dispatch = "dispatched";
|
|
361
|
+
return Effect.fail(error);
|
|
362
|
+
}));
|
|
363
|
+
dispatch = "dispatched";
|
|
364
|
+
milestone = "submission-dispatched";
|
|
365
|
+
}
|
|
366
|
+
yield* permitObservation;
|
|
367
|
+
return CredentialUseResult.make({
|
|
368
|
+
dispatch,
|
|
369
|
+
milestone,
|
|
370
|
+
observation,
|
|
371
|
+
cleanup,
|
|
372
|
+
authentication: "unverified"
|
|
373
|
+
});
|
|
374
|
+
}))
|
|
375
|
+
};
|
|
376
|
+
}, Effect.withTracerEnabled(false)) };
|
|
377
|
+
}));
|
|
378
|
+
//#endregion
|
|
379
|
+
//#region src/protected-browser/inspect-frame.ts
|
|
380
|
+
const maxAttributeLength = 2048;
|
|
381
|
+
const inspectFrame = `(() => {
|
|
382
|
+
const doc = document;
|
|
383
|
+
const forms = [];
|
|
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
|
+
};
|
|
403
|
+
const describe = (el) => {
|
|
404
|
+
if (doc !== document || !el.isConnected || el.ownerDocument !== doc || !available(el)) return null;
|
|
405
|
+
const form = el.form ?? null;
|
|
406
|
+
let formIndex = forms.indexOf(form);
|
|
407
|
+
if (formIndex < 0) { formIndex = forms.length; forms.push(form); }
|
|
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;
|
|
410
|
+
const method = form ? (el.hasAttribute('formmethod') ? el.formMethod : form.method) : '';
|
|
411
|
+
const enctype = form?.enctype ?? '';
|
|
412
|
+
const name = el.name ?? '';
|
|
413
|
+
const completion = el.getAttribute('autocomplete') ?? '';
|
|
414
|
+
const inputType = el.type ?? '';
|
|
415
|
+
const choiceValue = isChoice(el) ? el.value : '';
|
|
416
|
+
// Reject before parsing, fingerprinting or CDP transfer. Truncation could hide a target change.
|
|
417
|
+
if ([action, method, enctype, name, completion, inputType, choiceValue].some(value => value.length > ${maxAttributeLength})) return null;
|
|
418
|
+
const type = inputType.toLowerCase();
|
|
419
|
+
const autocomplete = completion.trim().toLowerCase().split(/\\s+/).at(-1);
|
|
420
|
+
const cardRoles = { 'cc-name':'card-name', 'cc-number':'card-number', 'cc-exp':'card-expiry',
|
|
421
|
+
'cc-exp-month':'card-expiry-month', 'cc-exp-year':'card-expiry-year', 'cc-csc':'card-security-code' };
|
|
422
|
+
let role = 'unsupported';
|
|
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'; }
|
|
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';
|
|
434
|
+
}
|
|
435
|
+
} else if (el instanceof HTMLButtonElement || (el instanceof HTMLInputElement && ['submit','button'].includes(type))) {
|
|
436
|
+
if (!el.matches(':disabled')) role = type === 'submit' && form ? 'submit' : type === 'button' ? 'button' : 'unsupported';
|
|
437
|
+
} else if (el instanceof HTMLAnchorElement) role = 'link';
|
|
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) };
|
|
441
|
+
};
|
|
442
|
+
const expose = ({role, formIndex, action, label, checked}) =>
|
|
443
|
+
({role, formIndex, action, label, ...(checked === undefined ? {} : {checked})});
|
|
444
|
+
const original = elements.map(describe);
|
|
445
|
+
const validate = (index) => {
|
|
446
|
+
const current = describe(elements[index]);
|
|
447
|
+
if (!current || !original[index] || current.fingerprint !== original[index].fingerprint ||
|
|
448
|
+
current.formIndex !== original[index].formIndex) return null;
|
|
449
|
+
return expose(current);
|
|
450
|
+
};
|
|
451
|
+
return {
|
|
452
|
+
doc, elements, original: original.map(current => current && expose(current)), validate,
|
|
453
|
+
text: () => {
|
|
454
|
+
if (doc !== document) return null;
|
|
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);
|
|
468
|
+
},
|
|
469
|
+
fill: (index, role, value) => {
|
|
470
|
+
const current = validate(index);
|
|
471
|
+
if (!current || current.role !== role) return false;
|
|
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
|
+
}
|
|
481
|
+
let prototype = Object.getPrototypeOf(el);
|
|
482
|
+
let setter;
|
|
483
|
+
while (prototype && !setter) { setter = Object.getOwnPropertyDescriptor(prototype,'value')?.set; prototype = Object.getPrototypeOf(prototype); }
|
|
484
|
+
if (!setter) return false;
|
|
485
|
+
// Validate and mutate in one isolated-world task, without focusing first and running
|
|
486
|
+
// page handlers between validation and assignment. Native setters support controlled inputs.
|
|
487
|
+
setter.call(el, value);
|
|
488
|
+
if (el.value !== value) return 'unsupported';
|
|
489
|
+
el.dispatchEvent(new Event('input', {bubbles:true}));
|
|
490
|
+
el.dispatchEvent(new Event('change', {bubbles:true}));
|
|
491
|
+
return true;
|
|
492
|
+
},
|
|
493
|
+
click: (index) => {
|
|
494
|
+
const current = validate(index);
|
|
495
|
+
if (!current || !['button','submit','link','radio','checkbox'].includes(current.role)) return false;
|
|
496
|
+
if (current.role === 'submit' && elements[index].form && !elements[index].form.matches(':valid')) return 'needs-attention';
|
|
497
|
+
elements[index].click();
|
|
498
|
+
return true;
|
|
499
|
+
}
|
|
500
|
+
};
|
|
501
|
+
})()`;
|
|
502
|
+
//#endregion
|
|
503
|
+
//#region src/protected-browser/native.ts
|
|
504
|
+
const Description = Schema.Struct({
|
|
505
|
+
role: ProtectedBrowserControl.fields.role,
|
|
506
|
+
formIndex: Schema.Natural,
|
|
507
|
+
action: Schema.String.check(Schema.isMaxLength(maxAttributeLength)),
|
|
508
|
+
label: Schema.String.check(Schema.isMaxLength(200)),
|
|
509
|
+
checked: Schema.optionalKey(Schema.Boolean)
|
|
510
|
+
});
|
|
511
|
+
const Descriptions = Schema.Array(Schema.NullOr(Description)).check(Schema.isMaxLength(65));
|
|
512
|
+
/** One host-private acquired session. The SDK handles and exact-session cleanup have one owner. */
|
|
513
|
+
var ProtectedNativeSession = class extends Context.Service()("@effect-agent/platform-cloudflare/ProtectedNativeSession") {};
|
|
514
|
+
const transportError = (reason) => new ProtectedTransportError({ reason });
|
|
515
|
+
const decode = (schema, value) => Schema.decodeUnknownEffect(schema)(value).pipe(Effect.mapError(() => transportError("provider")));
|
|
516
|
+
const remote = (run) => Effect.tryPromise({
|
|
517
|
+
try: async (signal) => {
|
|
518
|
+
try {
|
|
519
|
+
return {
|
|
520
|
+
ok: true,
|
|
521
|
+
value: await run(signal)
|
|
522
|
+
};
|
|
523
|
+
} catch {
|
|
524
|
+
return { ok: false };
|
|
525
|
+
}
|
|
526
|
+
},
|
|
527
|
+
catch: () => transportError("provider")
|
|
528
|
+
}).pipe(Effect.flatMap((result) => result.ok ? Effect.succeed(result.value) : Effect.fail(transportError("provider"))));
|
|
529
|
+
/** Host-private scoped transport. Session capabilities are requirements; policy is per-pass data. */
|
|
530
|
+
const makeProtectedNativeTransport = Effect.fn("ProtectedNativeTransport.make")(function* (policy) {
|
|
531
|
+
const session = yield* ProtectedNativeSession;
|
|
532
|
+
const { browser, page } = session;
|
|
533
|
+
const crypto = yield* Crypto.Crypto;
|
|
534
|
+
const clock = yield* Clock.Clock;
|
|
535
|
+
const uuid = crypto.randomUUIDv4.pipe(Effect.mapError(() => transportError("provider")));
|
|
536
|
+
let closed = false;
|
|
537
|
+
let violation = false;
|
|
538
|
+
let documentRef;
|
|
539
|
+
let observationOrigins;
|
|
540
|
+
const frames = /* @__PURE__ */ new Map();
|
|
541
|
+
const controls = /* @__PURE__ */ new Map();
|
|
542
|
+
const origin = (value) => Effect.try({
|
|
543
|
+
try: () => {
|
|
544
|
+
const url = new URL(value);
|
|
545
|
+
if (url.username || url.password) throw transportError("stale-reference");
|
|
546
|
+
if (!Schema.is(CredentialOrigin)(url.origin)) throw transportError("unsupported");
|
|
547
|
+
if (policy.network._tag === "ExactHosts" && !policy.network.allowedHosts.includes(url.host)) throw transportError("stale-reference");
|
|
548
|
+
return url.origin;
|
|
549
|
+
},
|
|
550
|
+
catch: (cause) => Schema.is(ProtectedTransportError)(cause) ? cause : transportError("provider")
|
|
551
|
+
});
|
|
552
|
+
const check = Effect.suspend(() => closed || violation || !browser.isConnected() ? Effect.fail(transportError("stale-reference")) : Effect.void);
|
|
553
|
+
const clear = () => {
|
|
554
|
+
controls.clear();
|
|
555
|
+
documentRef = void 0;
|
|
556
|
+
};
|
|
557
|
+
const onNavigated = () => {
|
|
558
|
+
clear();
|
|
559
|
+
};
|
|
560
|
+
const onTarget = (target) => {
|
|
561
|
+
if (target.type() !== "page") return;
|
|
562
|
+
violation = true;
|
|
563
|
+
clear();
|
|
564
|
+
};
|
|
565
|
+
const invalidate = () => {
|
|
566
|
+
closed = true;
|
|
567
|
+
clear();
|
|
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
|
+
});
|
|
577
|
+
const context = Effect.gen(function* () {
|
|
578
|
+
yield* check;
|
|
579
|
+
const list = yield* observationFrames;
|
|
580
|
+
const topOrigin = yield* origin(page.url());
|
|
581
|
+
const frameOrigins = [...new Set(yield* Effect.forEach(list, (frame) => origin(frame.url())))];
|
|
582
|
+
if (frameOrigins.length > 16) return yield* transportError("unsupported");
|
|
583
|
+
if (documentRef === void 0) documentRef = yield* uuid;
|
|
584
|
+
return yield* decode(ProtectedPageContext, {
|
|
585
|
+
document: documentRef,
|
|
586
|
+
topOrigin,
|
|
587
|
+
frameOrigins
|
|
588
|
+
});
|
|
589
|
+
});
|
|
590
|
+
const isCurrent = Effect.fn("ProtectedNativeTransport.isCurrent")(function* (state) {
|
|
591
|
+
if (state.frame.detached) return false;
|
|
592
|
+
return yield* remote(() => state.handle.evaluate((held) => {
|
|
593
|
+
if (typeof held !== "object" || held === null) return false;
|
|
594
|
+
return Reflect.get(held, "doc") === Reflect.get(globalThis, "document");
|
|
595
|
+
}));
|
|
596
|
+
});
|
|
597
|
+
const get = Effect.fn("ProtectedNativeTransport.get")(function* (ref) {
|
|
598
|
+
yield* check;
|
|
599
|
+
const state = controls.get(ref);
|
|
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");
|
|
601
|
+
const current = yield* remote(() => state.frame.handle.evaluate((held, index) => {
|
|
602
|
+
if (typeof held !== "object" || held === null) return null;
|
|
603
|
+
return Reflect.apply(Reflect.get(held, "validate"), held, [index]);
|
|
604
|
+
}, state.index));
|
|
605
|
+
const description = yield* decode(Schema.NullOr(Description), current);
|
|
606
|
+
if (description === null) return yield* transportError("stale-reference");
|
|
607
|
+
if ((yield* context).topOrigin !== state.control.target.topOrigin || (yield* origin(state.frame.frame.url())) !== state.control.target.frameOrigin || (yield* origin(description.action)) !== state.control.target.recipientOrigin || description.role !== state.control.role) return yield* transportError("stale-reference");
|
|
608
|
+
return state;
|
|
609
|
+
});
|
|
610
|
+
const close = yield* Effect.cached(Effect.gen(function* () {
|
|
611
|
+
invalidate();
|
|
612
|
+
return yield* session.close;
|
|
613
|
+
}).pipe(Effect.ensuring(Effect.sync(() => {
|
|
614
|
+
page.off("framenavigated", onNavigated);
|
|
615
|
+
page.off("framedetached", onNavigated);
|
|
616
|
+
browser.off("targetcreated", onTarget);
|
|
617
|
+
frames.clear();
|
|
618
|
+
}))));
|
|
619
|
+
yield* Effect.acquireRelease(Effect.sync(() => {
|
|
620
|
+
page.on("framenavigated", onNavigated);
|
|
621
|
+
page.on("framedetached", onNavigated);
|
|
622
|
+
browser.on("targetcreated", onTarget);
|
|
623
|
+
}), () => close);
|
|
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
|
+
}),
|
|
630
|
+
context,
|
|
631
|
+
invalidate,
|
|
632
|
+
close,
|
|
633
|
+
navigate: Effect.fn("ProtectedNativeTransport.navigate")(function* (url) {
|
|
634
|
+
yield* check;
|
|
635
|
+
yield* origin(url);
|
|
636
|
+
clear();
|
|
637
|
+
yield* remote(() => page.goto(url, { waitUntil: "load" }));
|
|
638
|
+
yield* context;
|
|
639
|
+
}),
|
|
640
|
+
target: (ref) => get(ref).pipe(Effect.map((state) => state.control)),
|
|
641
|
+
discover: Effect.gen(function* () {
|
|
642
|
+
const before = yield* context;
|
|
643
|
+
controls.clear();
|
|
644
|
+
for (const state of frames.values()) yield* remote(() => state.handle.dispose());
|
|
645
|
+
frames.clear();
|
|
646
|
+
const discovered = [];
|
|
647
|
+
let text = "";
|
|
648
|
+
let truncated = false;
|
|
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);
|
|
654
|
+
if (typeof frame.isolatedRealm !== "function") return yield* transportError("unsupported");
|
|
655
|
+
const handle = yield* remote(() => frame.isolatedRealm().evaluateHandle(inspectFrame));
|
|
656
|
+
const state = {
|
|
657
|
+
frame,
|
|
658
|
+
handle,
|
|
659
|
+
ref: yield* uuid,
|
|
660
|
+
document: yield* uuid,
|
|
661
|
+
forms: /* @__PURE__ */ new Map()
|
|
662
|
+
};
|
|
663
|
+
frames.set(frame, state);
|
|
664
|
+
const raw = yield* remote(() => handle.evaluate((held) => {
|
|
665
|
+
if (typeof held !== "object" || held === null) return null;
|
|
666
|
+
return Reflect.get(held, "original");
|
|
667
|
+
}));
|
|
668
|
+
const descriptions = yield* decode(Descriptions, raw);
|
|
669
|
+
for (let index = 0; index < descriptions.length; index++) {
|
|
670
|
+
const desc = descriptions[index];
|
|
671
|
+
if (!desc) continue;
|
|
672
|
+
if (discovered.length === 64) {
|
|
673
|
+
truncated = true;
|
|
674
|
+
break;
|
|
675
|
+
}
|
|
676
|
+
let form = state.forms.get(desc.formIndex);
|
|
677
|
+
if (form === void 0) {
|
|
678
|
+
form = yield* uuid;
|
|
679
|
+
state.forms.set(desc.formIndex, form);
|
|
680
|
+
}
|
|
681
|
+
const recipient = yield* origin(desc.action).pipe(Effect.result);
|
|
682
|
+
if (recipient._tag === "Failure") continue;
|
|
683
|
+
const control = ProtectedBrowserControl.make({
|
|
684
|
+
ref: yield* uuid,
|
|
685
|
+
role: desc.role,
|
|
686
|
+
label: desc.label,
|
|
687
|
+
...desc.checked === void 0 ? {} : { checked: desc.checked },
|
|
688
|
+
...desc.role === "link" ? { url: desc.action } : {},
|
|
689
|
+
target: CredentialTarget.make({
|
|
690
|
+
topOrigin: before.topOrigin,
|
|
691
|
+
frameOrigin: yield* origin(frame.url()),
|
|
692
|
+
recipientOrigin: recipient.success,
|
|
693
|
+
document: state.document,
|
|
694
|
+
frame: state.ref,
|
|
695
|
+
form
|
|
696
|
+
})
|
|
697
|
+
});
|
|
698
|
+
controls.set(control.ref, {
|
|
699
|
+
frame: state,
|
|
700
|
+
index,
|
|
701
|
+
control,
|
|
702
|
+
expires: (yield* clock.currentTimeMillis) + 6e4
|
|
703
|
+
});
|
|
704
|
+
discovered.push(control);
|
|
705
|
+
}
|
|
706
|
+
const rawText = yield* remote(() => handle.evaluate((held) => {
|
|
707
|
+
if (typeof held !== "object" || held === null) return null;
|
|
708
|
+
return Reflect.apply(Reflect.get(held, "text"), held, []);
|
|
709
|
+
}));
|
|
710
|
+
text += yield* decode(Schema.String.check(Schema.isMaxLength(65536)), rawText);
|
|
711
|
+
if (text.length > 65536) {
|
|
712
|
+
text = text.slice(0, 65536);
|
|
713
|
+
truncated = true;
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
if ((yield* context).document !== before.document) return yield* transportError("stale-reference");
|
|
717
|
+
return yield* decode(ProtectedDiscovery, {
|
|
718
|
+
...before,
|
|
719
|
+
frameOrigins: [...frameOrigins],
|
|
720
|
+
text,
|
|
721
|
+
controls: discovered,
|
|
722
|
+
truncated
|
|
723
|
+
});
|
|
724
|
+
}),
|
|
725
|
+
fill: Effect.fn("ProtectedNativeTransport.fill")(function* (ref, role, value) {
|
|
726
|
+
const dispatch = yield* ProtectedBrowserDispatch;
|
|
727
|
+
const state = yield* get(ref);
|
|
728
|
+
yield* dispatch.mark;
|
|
729
|
+
const filled = yield* remote(() => state.frame.handle.evaluate((held, index, expectedRole, secret) => {
|
|
730
|
+
if (typeof held !== "object" || held === null) return false;
|
|
731
|
+
return Reflect.apply(Reflect.get(held, "fill"), held, [
|
|
732
|
+
index,
|
|
733
|
+
expectedRole,
|
|
734
|
+
secret
|
|
735
|
+
]);
|
|
736
|
+
}, state.index, role, Redacted.value(value)));
|
|
737
|
+
if (filled === "unsupported") return yield* transportError("unsupported");
|
|
738
|
+
if (filled !== true) return yield* transportError("stale-reference");
|
|
739
|
+
}),
|
|
740
|
+
click: Effect.fn("ProtectedNativeTransport.click")(function* (ref) {
|
|
741
|
+
const state = yield* get(ref);
|
|
742
|
+
const clicked = yield* remote(() => state.frame.handle.evaluate((held, index) => {
|
|
743
|
+
if (typeof held !== "object" || held === null) return false;
|
|
744
|
+
return Reflect.apply(Reflect.get(held, "click"), held, [index]);
|
|
745
|
+
}, state.index));
|
|
746
|
+
if (clicked === "needs-attention") return yield* transportError("needs-attention");
|
|
747
|
+
if (clicked !== true) return yield* transportError("stale-reference");
|
|
748
|
+
})
|
|
749
|
+
};
|
|
750
|
+
}, Effect.withTracerEnabled(false));
|
|
751
|
+
//#endregion
|
|
752
|
+
//#region src/protected-browser/binding.ts
|
|
753
|
+
/**
|
|
754
|
+
* Acquires through BROWSER with recording=false explicitly on the wire. The trusted provider's
|
|
755
|
+
* documented opt-in semantics are the recording guarantee; no recording-status API exists.
|
|
756
|
+
* Account operators remain trusted, and must not attach external observers to private passes.
|
|
757
|
+
*/
|
|
758
|
+
const browserRunProtectedBindingLayer = (options) => Layer.effect(BrowserRunProtectedTransport)(Effect.gen(function* () {
|
|
759
|
+
const lifecycle = yield* BrowserRunSessionLifecycle;
|
|
760
|
+
const crypto = yield* Crypto.Crypto;
|
|
761
|
+
return { open: Effect.fn("BrowserRunProtectedTransport.open")(function* (policy) {
|
|
762
|
+
const failure = () => new ProtectedBrowserError({
|
|
763
|
+
reason: "provider",
|
|
764
|
+
dispatch: "not-dispatched",
|
|
765
|
+
milestone: "none",
|
|
766
|
+
observation: "closed",
|
|
767
|
+
cleanup: "unconfirmed"
|
|
768
|
+
});
|
|
769
|
+
let sessionId;
|
|
770
|
+
let browser;
|
|
771
|
+
let driver;
|
|
772
|
+
let invalid = false;
|
|
773
|
+
const runCleanup = Effect.runPromiseWith(Context.make(Clock.Clock, yield* Clock.Clock));
|
|
774
|
+
const terminate = Effect.gen(function* () {
|
|
775
|
+
invalid = true;
|
|
776
|
+
driver?.invalidate();
|
|
777
|
+
if (sessionId === void 0) return "unconfirmed";
|
|
778
|
+
const cleanup = yield* lifecycle.close(sessionId).pipe(Effect.as("confirmed"), Effect.catchCause(() => Effect.succeed("unconfirmed")), Effect.interruptible, Effect.timeoutOrElse({
|
|
779
|
+
duration: "10 seconds",
|
|
780
|
+
orElse: () => Effect.succeed("unconfirmed")
|
|
781
|
+
}));
|
|
782
|
+
const connected = browser;
|
|
783
|
+
if (connected !== void 0) yield* Effect.promise(() => connected.disconnect()).pipe(Effect.catchCause(() => Effect.void), Effect.interruptible, Effect.timeoutOrElse({
|
|
784
|
+
duration: "1 second",
|
|
785
|
+
orElse: () => Effect.void
|
|
786
|
+
}));
|
|
787
|
+
return cleanup;
|
|
788
|
+
});
|
|
789
|
+
const close = yield* Effect.cached(terminate);
|
|
790
|
+
yield* Effect.addFinalizer(() => close.pipe(Effect.flatMap((state) => state === "confirmed" ? Effect.void : Effect.logWarning("Protected browser exact-session closure unconfirmed"))));
|
|
791
|
+
const acquired = yield* Effect.tryPromise({
|
|
792
|
+
try: async (signal) => {
|
|
793
|
+
let recordingDisabled = false;
|
|
794
|
+
const acquired = await puppeteer.acquire({ fetch: async (input, init) => {
|
|
795
|
+
const request = new Request(input, init);
|
|
796
|
+
const url = new URL(request.url);
|
|
797
|
+
if (url.pathname === "/v1/devtools/browser" && request.method === "POST") {
|
|
798
|
+
url.searchParams.set("recording", "false");
|
|
799
|
+
recordingDisabled = url.searchParams.get("recording") === "false";
|
|
800
|
+
return options.browser.fetch(new Request(url, request));
|
|
801
|
+
}
|
|
802
|
+
return options.browser.fetch(request);
|
|
803
|
+
} }, {
|
|
804
|
+
recording: false,
|
|
805
|
+
keep_alive: Math.min(6e5, Math.max(1e4, policy.maxElapsedMillis))
|
|
806
|
+
});
|
|
807
|
+
sessionId = Redacted.make(Schema.decodeUnknownSync(Schema.String.check(Schema.isUUID()))(acquired.sessionId));
|
|
808
|
+
if (!recordingDisabled || signal.aborted || invalid) {
|
|
809
|
+
await runCleanup(terminate);
|
|
810
|
+
throw new ProtectedTransportError({ reason: "stale-reference" });
|
|
811
|
+
}
|
|
812
|
+
browser = await puppeteer.connect(options.browser, acquired.sessionId);
|
|
813
|
+
if (signal.aborted || invalid) {
|
|
814
|
+
await runCleanup(terminate);
|
|
815
|
+
throw new ProtectedTransportError({ reason: "stale-reference" });
|
|
816
|
+
}
|
|
817
|
+
const page = await (await browser.createBrowserContext()).newPage();
|
|
818
|
+
await page.setBypassServiceWorker(true);
|
|
819
|
+
await page.setRequestInterception(true);
|
|
820
|
+
page.on("request", (request) => {
|
|
821
|
+
let allowed = policy.network._tag === "Unrestricted";
|
|
822
|
+
try {
|
|
823
|
+
const url = new URL(request.url());
|
|
824
|
+
allowed ||= policy.network._tag === "ExactHosts" && url.protocol === "https:" && !url.username && !url.password && policy.network.allowedHosts.includes(url.host);
|
|
825
|
+
} catch {}
|
|
826
|
+
(allowed && !invalid ? request.continue() : request.abort("blockedbyclient")).catch(() => {
|
|
827
|
+
invalid = true;
|
|
828
|
+
driver?.invalidate();
|
|
829
|
+
});
|
|
830
|
+
});
|
|
831
|
+
if (signal.aborted || invalid) {
|
|
832
|
+
await runCleanup(terminate);
|
|
833
|
+
throw new ProtectedTransportError({ reason: "stale-reference" });
|
|
834
|
+
}
|
|
835
|
+
return ProtectedNativeSession.of({
|
|
836
|
+
browser,
|
|
837
|
+
page,
|
|
838
|
+
close
|
|
839
|
+
});
|
|
840
|
+
},
|
|
841
|
+
catch: failure
|
|
842
|
+
}).pipe(Effect.timeoutOrElse({
|
|
843
|
+
duration: Math.min(policy.maxElapsedMillis, 3e4),
|
|
844
|
+
orElse: () => Effect.fail(new ProtectedBrowserError({
|
|
845
|
+
...failure(),
|
|
846
|
+
reason: "timeout"
|
|
847
|
+
}))
|
|
848
|
+
}), Effect.catch((error) => close.pipe(Effect.flatMap((cleanup) => Effect.fail(new ProtectedBrowserError({
|
|
849
|
+
...error,
|
|
850
|
+
cleanup
|
|
851
|
+
}))))));
|
|
852
|
+
driver = yield* makeProtectedNativeTransport(policy).pipe(Effect.provideService(ProtectedNativeSession, acquired), Effect.provideService(Crypto.Crypto, crypto));
|
|
853
|
+
return driver;
|
|
854
|
+
}, Effect.withTracerEnabled(false)) };
|
|
855
|
+
}));
|
|
856
|
+
//#endregion
|
|
857
|
+
export { browserRunProtectedBindingLayer, browserRunProtectedLayer };
|
|
858
|
+
|
|
859
|
+
//# sourceMappingURL=ProtectedBrowser.mjs.map
|