@effect-agent/platform-cloudflare 0.1.0-beta.87 → 0.1.0-beta.89
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/InteractiveBrowser-DUE_m12-.d.mts +161 -0
- package/dist/InteractiveBrowser-DupDtfw2.mjs +1169 -0
- package/dist/InteractiveBrowser-DupDtfw2.mjs.map +1 -0
- package/dist/InteractiveBrowser.d.mts +2 -144
- package/dist/InteractiveBrowser.mjs +1 -1089
- package/dist/ProtectedBrowser.d.mts +70 -14
- package/dist/ProtectedBrowser.mjs +599 -328
- package/dist/ProtectedBrowser.mjs.map +1 -1
- package/package.json +1 -1
- package/src/ProtectedBrowser.ts +7 -0
- package/src/protected-browser/binding.ts +151 -25
- package/src/protected-browser/host.ts +328 -0
- package/src/protected-browser/native.ts +17 -1
- package/src/protected-browser/policy.ts +640 -536
- package/dist/InteractiveBrowser.mjs.map +0 -1
- package/dist/browser-session-lifecycle-CmbQABHC.mjs +0 -84
- package/dist/browser-session-lifecycle-CmbQABHC.mjs.map +0 -1
- package/dist/browser-session-lifecycle-DJpqLXy_.d.mts +0 -21
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { h as BrowserRunSessionLifecycle, o as BrowserRunLiveViewRequest, r as BrowserRunHandoffState, s as BrowserRunLiveViewResult, t as BrowserRunHandoffRequest } from "./InteractiveBrowser-DupDtfw2.mjs";
|
|
2
2
|
import { Clock, Context, Crypto, Effect, Layer, Redacted, Schema, Semaphore } from "effect";
|
|
3
3
|
import puppeteer from "@cloudflare/puppeteer";
|
|
4
4
|
import { InteractiveBrowserPolicy } from "effect-agent/interactive-browser";
|
|
5
|
-
import { BrowserCredentialAccess, CardCredential, CredentialObservationDecision, CredentialOffer, CredentialOfferMetadata, CredentialOrigin, CredentialTarget, CredentialUseResult, ListCredentialOffers, LoginCredential, ProtectedBrowser, ProtectedBrowserClick, ProtectedBrowserControl, ProtectedBrowserError, ProtectedBrowserFill, ProtectedBrowserNavigate, ProtectedBrowserObservation, UseCredential } from "effect-agent/protected-browser";
|
|
5
|
+
import { BrowserCredentialAccess, CardCredential, CredentialObservationDecision, CredentialOffer, CredentialOfferMetadata, CredentialOrigin, CredentialTarget, CredentialUseResult, ListCredentialOffers, LoginCredential, ProtectedBrowser, ProtectedBrowserCheckpoint, ProtectedBrowserClick, ProtectedBrowserControl, ProtectedBrowserError, ProtectedBrowserFill, ProtectedBrowserNavigate, ProtectedBrowserObservation, UseCredential } from "effect-agent/protected-browser";
|
|
6
6
|
//#region src/protected-browser/policy.ts
|
|
7
7
|
var ProtectedTransportError = class extends Schema.TaggedError()("ProtectedTransportError", { reason: Schema.Literals([
|
|
8
8
|
"stale-reference",
|
|
@@ -40,340 +40,403 @@ const secretFor = (material, role) => {
|
|
|
40
40
|
case "password": return;
|
|
41
41
|
}
|
|
42
42
|
};
|
|
43
|
-
/**
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
43
|
+
/** Shared protected policy implementation for ephemeral and host-owned passes. */
|
|
44
|
+
const makeProtectedBrowserPolicy = Effect.fn("ProtectedBrowser.open")(function* (input, checkpoint) {
|
|
45
|
+
const initialError = (reason) => new ProtectedBrowserError({
|
|
46
|
+
reason,
|
|
47
|
+
dispatch: "not-dispatched",
|
|
48
|
+
milestone: "none",
|
|
49
|
+
observation: "closed",
|
|
50
|
+
cleanup: "not-requested"
|
|
51
|
+
});
|
|
52
|
+
const decodedPolicy = yield* Schema.decodeEffect(InteractiveBrowserPolicy)(input).pipe(Effect.mapError(() => initialError("denied")));
|
|
53
|
+
if (decodedPolicy.network._tag === "PublicWeb") return yield* initialError("unsupported");
|
|
54
|
+
const policy = InteractiveBrowserPolicy.make({
|
|
55
|
+
...decodedPolicy,
|
|
56
|
+
network: decodedPolicy.network._tag === "ExactHosts" ? {
|
|
57
|
+
_tag: "ExactHosts",
|
|
58
|
+
allowedHosts: [...decodedPolicy.network.allowedHosts]
|
|
59
|
+
} : { _tag: "Unrestricted" }
|
|
60
|
+
});
|
|
61
|
+
const access = yield* BrowserCredentialAccess;
|
|
62
|
+
const restored = checkpoint === void 0 ? void 0 : yield* Schema.decodeEffect(ProtectedBrowserCheckpoint)(checkpoint).pipe(Effect.mapError(() => initialError("denied")));
|
|
63
|
+
const now = yield* Clock.currentTimeMillis;
|
|
64
|
+
if (restored !== void 0 && (restored.startedAt > now || restored.actions > policy.maxActions || now - restored.startedAt >= policy.maxElapsedMillis)) return yield* initialError("timeout");
|
|
65
|
+
const started = restored?.startedAt ?? now;
|
|
66
|
+
const driver = yield* (yield* BrowserRunProtectedTransport).open(policy);
|
|
50
67
|
const crypto = yield* Crypto.Crypto;
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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.decodeEffect(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
|
-
}
|
|
68
|
+
let suspended = restored !== void 0;
|
|
69
|
+
let detached = false;
|
|
70
|
+
let needsObservation = restored !== void 0;
|
|
71
|
+
let humanExposure = restored?.humanExposure ?? false;
|
|
72
|
+
let humanOrigins = [...restored?.humanOrigins ?? []];
|
|
73
|
+
const lock = yield* Semaphore.make(1);
|
|
74
|
+
let observation = "before-exposure";
|
|
75
|
+
let cleanup = "not-requested";
|
|
76
|
+
let actions = restored?.actions ?? 0;
|
|
77
|
+
let dispatch = restored?.dispatch ?? "not-dispatched";
|
|
78
|
+
let milestone = restored?.milestone ?? "none";
|
|
79
|
+
const exposures = [...restored?.exposures ?? []];
|
|
80
|
+
const offers = /* @__PURE__ */ new Map();
|
|
81
|
+
const fail = (reason) => new ProtectedBrowserError({
|
|
82
|
+
reason,
|
|
83
|
+
dispatch,
|
|
84
|
+
milestone,
|
|
85
|
+
observation,
|
|
86
|
+
cleanup
|
|
87
|
+
});
|
|
88
|
+
const close = yield* Effect.cached(Effect.uninterruptible(Effect.gen(function* () {
|
|
89
|
+
observation = "closed";
|
|
90
|
+
offers.clear();
|
|
91
|
+
driver.invalidate();
|
|
92
|
+
cleanup = yield* driver.close.pipe(Effect.interruptible, Effect.timeoutOrElse({
|
|
93
|
+
duration: "10 seconds",
|
|
94
|
+
orElse: () => Effect.succeed("unconfirmed")
|
|
95
|
+
}), Effect.catchCause(() => Effect.succeed("unconfirmed")));
|
|
96
|
+
return cleanup;
|
|
97
|
+
})));
|
|
98
|
+
yield* Effect.addFinalizer(() => detached ? Effect.void : close.pipe(Effect.flatMap((result) => result === "unconfirmed" ? Effect.logWarning("Protected browser exact-session closure unconfirmed") : Effect.void)));
|
|
99
|
+
const remote = (effect) => effect.pipe(Effect.mapError((error) => fail(error.reason)));
|
|
100
|
+
const decode = (schema, value) => Schema.decodeUnknownEffect(schema)(value).pipe(Effect.mapError(() => fail("provider")));
|
|
101
|
+
const caller = access.caller.pipe(Effect.mapError((error) => fail(error.reason)));
|
|
102
|
+
const pageContext = remote(driver.context);
|
|
103
|
+
const permitObservation = Effect.gen(function* () {
|
|
104
|
+
const context = yield* pageContext;
|
|
105
|
+
let origins;
|
|
106
|
+
if (exposures.length > 0 || humanExposure) {
|
|
107
|
+
observation = "protected";
|
|
133
108
|
const principal = yield* caller;
|
|
134
|
-
yield* access.
|
|
109
|
+
const rawDecision = yield* access.observation({
|
|
110
|
+
...context,
|
|
135
111
|
caller: principal,
|
|
136
|
-
|
|
137
|
-
|
|
112
|
+
exposures: [...exposures],
|
|
113
|
+
humanExposure,
|
|
114
|
+
humanOrigins
|
|
138
115
|
}).pipe(Effect.mapError((error) => fail(error.reason)));
|
|
139
116
|
if (Redacted.value(yield* caller) !== Redacted.value(principal)) return yield* fail("denied");
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
117
|
+
const decision = yield* Schema.decodeEffect(CredentialObservationDecision)(rawDecision).pipe(Effect.mapError(() => fail("observation-blocked")));
|
|
118
|
+
if (decision === "deny") return yield* fail("observation-blocked");
|
|
119
|
+
if (typeof decision !== "string") {
|
|
120
|
+
origins = [...decision.origins];
|
|
121
|
+
if (!origins.includes(context.topOrigin)) return yield* fail("observation-blocked");
|
|
143
122
|
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
123
|
+
observation = "approved-after-exposure";
|
|
124
|
+
}
|
|
125
|
+
yield* remote(driver.restrictObservation(origins));
|
|
126
|
+
return {
|
|
127
|
+
...context,
|
|
128
|
+
frameOrigins: context.frameOrigins.filter((origin) => origins === void 0 || origins.includes(origin))
|
|
147
129
|
};
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
if (
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
130
|
+
});
|
|
131
|
+
const target = (ref) => remote(driver.target(ref));
|
|
132
|
+
const authorizeAction = Effect.fn("ProtectedBrowser.authorizeAction")(function* (action) {
|
|
133
|
+
if (access.authorizeAction === void 0) {
|
|
134
|
+
if (action._tag === "Submit") return yield* fail("unsupported");
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
const principal = yield* caller;
|
|
138
|
+
yield* access.authorizeAction({
|
|
139
|
+
caller: principal,
|
|
140
|
+
action,
|
|
141
|
+
exposures: [...exposures]
|
|
142
|
+
}).pipe(Effect.mapError((error) => fail(error.reason)));
|
|
143
|
+
if (Redacted.value(yield* caller) !== Redacted.value(principal)) return yield* fail("denied");
|
|
144
|
+
if (action._tag !== "Navigate") {
|
|
145
|
+
const current = yield* target(action.ref);
|
|
146
|
+
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");
|
|
147
|
+
}
|
|
148
|
+
}, Effect.withTracerEnabled(false));
|
|
149
|
+
const bounded = (result) => {
|
|
150
|
+
return new TextEncoder().encode(JSON.stringify(result)).byteLength <= policy.maxReturnedBytes ? Effect.succeed(result) : Effect.fail(fail("limit"));
|
|
151
|
+
};
|
|
152
|
+
const run = (effect, observing = false) => lock.withPermitsIfAvailable(1)(Effect.gen(function* () {
|
|
153
|
+
if (suspended || detached) return yield* fail("busy");
|
|
154
|
+
if (needsObservation && !observing) return yield* fail("stale-reference");
|
|
155
|
+
dispatch = "not-dispatched";
|
|
156
|
+
milestone = "none";
|
|
157
|
+
if (observation === "closed") return yield* fail("closed");
|
|
158
|
+
if (++actions > policy.maxActions) return yield* fail("limit");
|
|
159
|
+
const remaining = policy.maxElapsedMillis - ((yield* Clock.currentTimeMillis) - started);
|
|
160
|
+
if (remaining <= 0) {
|
|
161
|
+
yield* close;
|
|
162
|
+
return yield* fail("timeout");
|
|
163
|
+
}
|
|
164
|
+
return yield* effect.pipe(Effect.timeoutOrElse({
|
|
165
|
+
duration: remaining,
|
|
166
|
+
orElse: () => Effect.fail(fail("timeout"))
|
|
167
|
+
}), Effect.catchCause((cause) => Effect.gen(function* () {
|
|
168
|
+
const error = cause.reasons.find((reason) => reason._tag === "Fail" && Schema.is(ProtectedBrowserError)(reason.error));
|
|
169
|
+
const reason = error?._tag === "Fail" && Schema.is(ProtectedBrowserError)(error.error) ? error.error.reason : "provider";
|
|
170
|
+
const interrupt = cause.reasons.some((reason) => reason._tag === "Interrupt");
|
|
171
|
+
if (dispatch !== "not-dispatched" || interrupt || reason === "provider" || reason === "timeout") yield* close;
|
|
172
|
+
if (interrupt) return yield* Effect.interrupt;
|
|
173
|
+
return yield* fail(dispatch === "possibly-dispatched" && reason === "provider" ? "outcome-unknown" : reason);
|
|
174
|
+
})), Effect.onInterrupt(() => close), Effect.withTracerEnabled(false));
|
|
175
|
+
})).pipe(Effect.flatMap(Effect.fromOption(() => new ProtectedBrowserError({
|
|
176
|
+
reason: "busy",
|
|
177
|
+
dispatch: "not-dispatched",
|
|
178
|
+
milestone: "none",
|
|
179
|
+
observation,
|
|
180
|
+
cleanup
|
|
181
|
+
}))));
|
|
182
|
+
const handle = {
|
|
183
|
+
close: Effect.suspend(() => detached ? Effect.succeed("not-requested") : close),
|
|
184
|
+
navigate: (request) => run(Effect.gen(function* () {
|
|
185
|
+
const decoded = yield* Schema.decodeEffect(ProtectedBrowserNavigate)(request).pipe(Effect.mapError(() => fail("denied")));
|
|
186
|
+
let url;
|
|
187
|
+
try {
|
|
188
|
+
url = new URL(decoded.url);
|
|
189
|
+
} catch {
|
|
190
|
+
return yield* fail("denied");
|
|
157
191
|
}
|
|
158
|
-
return yield*
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
}))
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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.decodeEffect(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 = {
|
|
192
|
+
if (url.protocol !== "https:" || url.username || url.password || policy.network._tag === "ExactHosts" && !policy.network.allowedHosts.includes(url.host)) return yield* fail("denied");
|
|
193
|
+
if (exposures.length > 0 || humanExposure) yield* permitObservation;
|
|
194
|
+
yield* authorizeAction({
|
|
195
|
+
_tag: "Navigate",
|
|
196
|
+
url: decoded.url
|
|
197
|
+
});
|
|
198
|
+
offers.clear();
|
|
199
|
+
dispatch = "possibly-dispatched";
|
|
200
|
+
yield* remote(driver.navigate(decoded.url));
|
|
201
|
+
dispatch = "dispatched";
|
|
202
|
+
yield* permitObservation;
|
|
203
|
+
})),
|
|
204
|
+
observe: run(Effect.gen(function* () {
|
|
205
|
+
const before = yield* permitObservation;
|
|
206
|
+
const result = yield* remote(driver.discover);
|
|
207
|
+
const after = yield* permitObservation;
|
|
208
|
+
if (before.document !== after.document || result.document !== after.document || result.topOrigin !== after.topOrigin) return yield* fail("stale-reference");
|
|
209
|
+
if (result.frameOrigins.some((origin) => !after.frameOrigins.includes(origin))) return yield* fail("observation-blocked");
|
|
210
|
+
const observation = yield* bounded(ProtectedBrowserObservation.make({
|
|
211
|
+
...result,
|
|
212
|
+
observation: exposures.length > 0 || humanExposure ? "approved-after-exposure" : "before-exposure"
|
|
213
|
+
}));
|
|
214
|
+
needsObservation = false;
|
|
215
|
+
return observation;
|
|
216
|
+
}), true),
|
|
217
|
+
click: (request) => run(Effect.gen(function* () {
|
|
218
|
+
const decoded = yield* Schema.decodeEffect(ProtectedBrowserClick)(request).pipe(Effect.mapError(() => fail("denied")));
|
|
219
|
+
yield* permitObservation;
|
|
220
|
+
const control = yield* target(decoded.ref);
|
|
221
|
+
if (control.role !== "link" && control.role !== "button" && control.role !== "radio" && control.role !== "checkbox" && control.role !== "submit") return yield* fail("unsupported");
|
|
222
|
+
let action;
|
|
223
|
+
if (control.role === "link") {
|
|
224
|
+
if (control.url === void 0) return yield* fail("unsupported");
|
|
225
|
+
action = {
|
|
230
226
|
_tag: "Click",
|
|
231
227
|
ref: decoded.ref,
|
|
232
228
|
target: control.target,
|
|
233
|
-
role:
|
|
229
|
+
role: "link",
|
|
230
|
+
url: control.url
|
|
234
231
|
};
|
|
235
|
-
|
|
232
|
+
} else if (control.role === "submit") action = {
|
|
233
|
+
_tag: "Submit",
|
|
234
|
+
ref: decoded.ref,
|
|
235
|
+
target: control.target
|
|
236
|
+
};
|
|
237
|
+
else action = {
|
|
238
|
+
_tag: "Click",
|
|
239
|
+
ref: decoded.ref,
|
|
240
|
+
target: control.target,
|
|
241
|
+
role: control.role
|
|
242
|
+
};
|
|
243
|
+
yield* authorizeAction(action);
|
|
244
|
+
dispatch = "possibly-dispatched";
|
|
245
|
+
yield* remote(driver.click(decoded.ref)).pipe(Effect.catch((error) => {
|
|
246
|
+
if (error.reason === "needs-attention") dispatch = "not-dispatched";
|
|
247
|
+
return Effect.fail(error);
|
|
248
|
+
}));
|
|
249
|
+
dispatch = "dispatched";
|
|
250
|
+
if (control.role === "submit") milestone = "submission-dispatched";
|
|
251
|
+
yield* permitObservation;
|
|
252
|
+
})),
|
|
253
|
+
fill: (request) => run(Effect.gen(function* () {
|
|
254
|
+
const decoded = yield* Schema.decodeEffect(ProtectedBrowserFill)(request).pipe(Effect.mapError(() => fail("denied")));
|
|
255
|
+
yield* permitObservation;
|
|
256
|
+
const control = yield* target(decoded.ref);
|
|
257
|
+
if (control.role !== "text" && control.role !== "select") return yield* fail("unsupported");
|
|
258
|
+
yield* authorizeAction({
|
|
259
|
+
_tag: "Fill",
|
|
260
|
+
ref: decoded.ref,
|
|
261
|
+
target: control.target,
|
|
262
|
+
role: control.role
|
|
263
|
+
});
|
|
264
|
+
yield* remote(driver.fill(decoded.ref, control.role, Redacted.make(decoded.value))).pipe(Effect.provideService(ProtectedBrowserDispatch, { mark: Effect.sync(() => {
|
|
236
265
|
dispatch = "possibly-dispatched";
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
yield*
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
const decoded = yield* Schema.decodeEffect(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({
|
|
266
|
+
}) }));
|
|
267
|
+
dispatch = "dispatched";
|
|
268
|
+
milestone = "filled";
|
|
269
|
+
yield* permitObservation;
|
|
270
|
+
})),
|
|
271
|
+
listCredentialOffers: (request) => run(Effect.gen(function* () {
|
|
272
|
+
const decoded = yield* Schema.decodeEffect(ListCredentialOffers)(request).pipe(Effect.mapError(() => fail("denied")));
|
|
273
|
+
yield* permitObservation;
|
|
274
|
+
const control = yield* target(decoded.target);
|
|
275
|
+
if (kindFor(control.role) !== decoded.kind) return yield* fail("unsupported");
|
|
276
|
+
const principal = yield* caller;
|
|
277
|
+
const candidates = yield* access.list({
|
|
278
|
+
caller: principal,
|
|
279
|
+
kind: decoded.kind,
|
|
280
|
+
target: control.target
|
|
281
|
+
}).pipe(Effect.mapError((error) => fail(error.reason)));
|
|
282
|
+
const now = yield* Clock.currentTimeMillis;
|
|
283
|
+
for (const [ref, offer] of offers) if (offer.expires <= now) offers.delete(ref);
|
|
284
|
+
if (candidates.length > 16 || offers.size + candidates.length > 64) return yield* fail("limit");
|
|
285
|
+
if (!sameTarget(control.target, (yield* target(decoded.target)).target)) return yield* fail("stale-reference");
|
|
286
|
+
const expires = now + 6e4;
|
|
287
|
+
const result = [];
|
|
288
|
+
const pending = /* @__PURE__ */ new Map();
|
|
289
|
+
for (const candidate of candidates) {
|
|
290
|
+
const metadata = yield* decode(CredentialOfferMetadata, candidate.metadata);
|
|
291
|
+
const ref = yield* crypto.randomUUIDv4.pipe(Effect.mapError(() => fail("provider")));
|
|
292
|
+
pending.set(ref, {
|
|
270
293
|
caller: principal,
|
|
294
|
+
key: candidate.key,
|
|
295
|
+
target: control.target,
|
|
296
|
+
targetRef: decoded.target,
|
|
271
297
|
kind: decoded.kind,
|
|
272
|
-
|
|
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.decodeEffect(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");
|
|
298
|
+
expires
|
|
333
299
|
});
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
300
|
+
result.push(CredentialOffer.make({
|
|
301
|
+
ref,
|
|
302
|
+
kind: decoded.kind,
|
|
303
|
+
metadata
|
|
304
|
+
}));
|
|
305
|
+
}
|
|
306
|
+
yield* bounded(result);
|
|
307
|
+
for (const [ref, offer] of pending) offers.set(ref, offer);
|
|
308
|
+
return result;
|
|
309
|
+
})),
|
|
310
|
+
useCredential: (request) => run(Effect.gen(function* () {
|
|
311
|
+
const decoded = yield* Schema.decodeEffect(UseCredential)(request).pipe(Effect.mapError(() => fail("denied")));
|
|
312
|
+
const offer = offers.get(decoded.offer);
|
|
313
|
+
if (offer === void 0 || offer.expires <= (yield* Clock.currentTimeMillis)) return yield* fail("stale-reference");
|
|
314
|
+
const principal = yield* caller;
|
|
315
|
+
if (Redacted.value(principal) !== Redacted.value(offer.caller)) return yield* fail("denied");
|
|
316
|
+
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");
|
|
317
|
+
const validate = Effect.gen(function* () {
|
|
318
|
+
if (!sameTarget(offer.target, (yield* target(offer.targetRef)).target)) return yield* fail("stale-reference");
|
|
340
319
|
for (const field of decoded.fields) {
|
|
341
|
-
yield*
|
|
342
|
-
yield*
|
|
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";
|
|
320
|
+
const current = yield* target(field.ref);
|
|
321
|
+
if (field.role !== current.role || kindFor(field.role) !== offer.kind || !sameTarget(offer.target, current.target)) return yield* fail("denied");
|
|
352
322
|
}
|
|
353
|
-
milestone = "filled";
|
|
354
323
|
if (decoded.submit !== void 0) {
|
|
355
|
-
yield*
|
|
356
|
-
yield*
|
|
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";
|
|
324
|
+
const submit = yield* target(decoded.submit);
|
|
325
|
+
if (submit.role !== "submit" || !sameTarget(offer.target, submit.target)) return yield* fail("denied");
|
|
365
326
|
}
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
327
|
+
});
|
|
328
|
+
yield* validate;
|
|
329
|
+
const authorization = {
|
|
330
|
+
caller: principal,
|
|
331
|
+
key: offer.key,
|
|
332
|
+
kind: offer.kind,
|
|
333
|
+
target: offer.target,
|
|
334
|
+
roles: decoded.fields.map((field) => field.role),
|
|
335
|
+
submit: decoded.submit !== void 0
|
|
336
|
+
};
|
|
337
|
+
const authorize = Effect.gen(function* () {
|
|
338
|
+
if (Redacted.value(yield* caller) !== Redacted.value(principal)) return yield* fail("denied");
|
|
339
|
+
yield* access.authorize(authorization).pipe(Effect.mapError((error) => fail(error.reason)));
|
|
340
|
+
if (Redacted.value(yield* caller) !== Redacted.value(principal)) return yield* fail("denied");
|
|
341
|
+
});
|
|
342
|
+
yield* authorize;
|
|
343
|
+
yield* validate;
|
|
344
|
+
const raw = yield* access.resolve(authorization).pipe(Effect.mapError((error) => fail(error.reason)));
|
|
345
|
+
const material = yield* Schema.decodeEffect(offer.kind === "login" ? LoginCredential : CardCredential)(raw).pipe(Effect.mapError(() => fail("resolver")));
|
|
346
|
+
for (const field of decoded.fields) if (secretFor(material, field.role) === void 0) return yield* fail("missing-credential");
|
|
347
|
+
offers.delete(decoded.offer);
|
|
348
|
+
for (const field of decoded.fields) {
|
|
349
|
+
yield* validate;
|
|
350
|
+
yield* authorize;
|
|
351
|
+
const value = secretFor(material, field.role);
|
|
352
|
+
if (value === void 0) return yield* fail("missing-credential");
|
|
353
|
+
observation = "protected";
|
|
354
|
+
yield* remote(driver.fill(field.ref, field.role, value)).pipe(Effect.provideService(ProtectedBrowserDispatch, { mark: Effect.sync(() => {
|
|
355
|
+
dispatch = "possibly-dispatched";
|
|
356
|
+
if (!exposures.some((target) => sameTarget(target, offer.target))) exposures.push(offer.target);
|
|
357
|
+
}) }));
|
|
358
|
+
dispatch = "dispatched";
|
|
359
|
+
milestone = "partial-fill";
|
|
360
|
+
}
|
|
361
|
+
milestone = "filled";
|
|
362
|
+
if (decoded.submit !== void 0) {
|
|
363
|
+
yield* validate;
|
|
364
|
+
yield* authorize;
|
|
365
|
+
const ref = decoded.submit;
|
|
366
|
+
dispatch = "possibly-dispatched";
|
|
367
|
+
yield* remote(driver.click(ref)).pipe(Effect.catch((error) => {
|
|
368
|
+
if (error.reason === "needs-attention") dispatch = "dispatched";
|
|
369
|
+
return Effect.fail(error);
|
|
370
|
+
}));
|
|
371
|
+
dispatch = "dispatched";
|
|
372
|
+
milestone = "submission-dispatched";
|
|
373
|
+
}
|
|
374
|
+
yield* permitObservation;
|
|
375
|
+
return CredentialUseResult.make({
|
|
376
|
+
dispatch,
|
|
377
|
+
milestone,
|
|
378
|
+
observation,
|
|
379
|
+
cleanup,
|
|
380
|
+
authentication: "unverified"
|
|
381
|
+
});
|
|
382
|
+
}))
|
|
383
|
+
};
|
|
384
|
+
const locked = (effect) => lock.withPermitsIfAvailable(1)(effect).pipe(Effect.flatMap(Effect.fromOption(() => fail("busy"))));
|
|
385
|
+
return {
|
|
386
|
+
handle,
|
|
387
|
+
suspend: (human = false) => locked(Effect.gen(function* () {
|
|
388
|
+
if (detached || observation === "closed") return yield* fail("closed");
|
|
389
|
+
if (dispatch === "possibly-dispatched") return yield* fail("outcome-unknown");
|
|
390
|
+
if ((yield* Clock.currentTimeMillis) - started >= policy.maxElapsedMillis) return yield* fail("timeout");
|
|
391
|
+
suspended = true;
|
|
392
|
+
if (human) {
|
|
393
|
+
const context = yield* pageContext;
|
|
394
|
+
const origins = yield* Schema.decodeEffect(ProtectedBrowserCheckpoint.fields.humanOrigins)([.../* @__PURE__ */ new Set([
|
|
395
|
+
...humanOrigins,
|
|
396
|
+
context.topOrigin,
|
|
397
|
+
...context.frameOrigins
|
|
398
|
+
])]).pipe(Effect.catch(() => fail("needs-attention")));
|
|
399
|
+
humanExposure = true;
|
|
400
|
+
humanOrigins = [...origins];
|
|
401
|
+
observation = "protected";
|
|
402
|
+
}
|
|
403
|
+
driver.resetReferences();
|
|
404
|
+
offers.clear();
|
|
405
|
+
return ProtectedBrowserCheckpoint.make({
|
|
406
|
+
policy,
|
|
407
|
+
startedAt: started,
|
|
408
|
+
actions,
|
|
409
|
+
exposures: [...exposures],
|
|
410
|
+
humanExposure,
|
|
411
|
+
humanOrigins,
|
|
412
|
+
dispatch,
|
|
413
|
+
milestone
|
|
414
|
+
});
|
|
415
|
+
})),
|
|
416
|
+
returnControl: locked(Effect.gen(function* () {
|
|
417
|
+
if (detached || observation === "closed") return yield* fail("closed");
|
|
418
|
+
if (!suspended) return yield* fail("denied");
|
|
419
|
+
yield* permitObservation;
|
|
420
|
+
offers.clear();
|
|
421
|
+
driver.resetReferences();
|
|
422
|
+
needsObservation = true;
|
|
423
|
+
suspended = false;
|
|
424
|
+
})),
|
|
425
|
+
detach: locked(Effect.gen(function* () {
|
|
426
|
+
if (!suspended || observation === "closed") return yield* fail("denied");
|
|
427
|
+
if (driver.detach === void 0) return yield* fail("unsupported");
|
|
428
|
+
yield* driver.detach.pipe(Effect.onError(() => close), Effect.onInterrupt(() => close));
|
|
429
|
+
detached = true;
|
|
430
|
+
offers.clear();
|
|
431
|
+
driver.invalidate();
|
|
432
|
+
}))
|
|
433
|
+
};
|
|
434
|
+
}, Effect.withTracerEnabled(false));
|
|
435
|
+
/** Fresh private passes; scoped release confirms exact-session termination. */
|
|
436
|
+
const browserRunProtectedLayer = () => Layer.effect(ProtectedBrowser)(Effect.gen(function* () {
|
|
437
|
+
const transport = yield* BrowserRunProtectedTransport;
|
|
438
|
+
const crypto = yield* Crypto.Crypto;
|
|
439
|
+
return { open: (policy) => makeProtectedBrowserPolicy(policy).pipe(Effect.map((session) => session.handle), Effect.provideService(BrowserRunProtectedTransport, transport), Effect.provideService(Crypto.Crypto, crypto)) };
|
|
377
440
|
}));
|
|
378
441
|
//#endregion
|
|
379
442
|
//#region src/protected-browser/inspect-frame.ts
|
|
@@ -620,7 +683,13 @@ const makeProtectedNativeTransport = Effect.fn("ProtectedNativeTransport.make")(
|
|
|
620
683
|
page.on("framenavigated", onNavigated);
|
|
621
684
|
page.on("framedetached", onNavigated);
|
|
622
685
|
browser.on("targetcreated", onTarget);
|
|
623
|
-
}), () => close)
|
|
686
|
+
}), () => session.release === void 0 ? close : session.release.pipe(Effect.ensuring(Effect.sync(() => {
|
|
687
|
+
invalidate();
|
|
688
|
+
page.off("framenavigated", onNavigated);
|
|
689
|
+
page.off("framedetached", onNavigated);
|
|
690
|
+
browser.off("targetcreated", onTarget);
|
|
691
|
+
frames.clear();
|
|
692
|
+
}))));
|
|
624
693
|
return {
|
|
625
694
|
restrictObservation: Effect.fn("ProtectedNativeTransport.restrictObservation")(function* (origins) {
|
|
626
695
|
yield* check;
|
|
@@ -629,6 +698,7 @@ const makeProtectedNativeTransport = Effect.fn("ProtectedNativeTransport.make")(
|
|
|
629
698
|
}),
|
|
630
699
|
context,
|
|
631
700
|
invalidate,
|
|
701
|
+
resetReferences: clear,
|
|
632
702
|
close,
|
|
633
703
|
navigate: Effect.fn("ProtectedNativeTransport.navigate")(function* (url) {
|
|
634
704
|
yield* check;
|
|
@@ -750,15 +820,22 @@ const makeProtectedNativeTransport = Effect.fn("ProtectedNativeTransport.make")(
|
|
|
750
820
|
}, Effect.withTracerEnabled(false));
|
|
751
821
|
//#endregion
|
|
752
822
|
//#region src/protected-browser/binding.ts
|
|
823
|
+
/** Host-private exact page identity; never include it in model-visible results. */
|
|
824
|
+
const ProtectedProviderIdentity = Schema.Struct({
|
|
825
|
+
sessionId: Schema.Redacted(Schema.String.check(Schema.isUUID())),
|
|
826
|
+
contextId: Schema.Redacted(Schema.NonEmptyString.check(Schema.isMaxLength(256))),
|
|
827
|
+
targetId: Schema.Redacted(Schema.NonEmptyString.check(Schema.isMaxLength(256)))
|
|
828
|
+
});
|
|
829
|
+
var BrowserRunProtectedBinding = class extends Context.Service()("@effect-agent/platform-cloudflare/BrowserRunProtectedBinding") {};
|
|
753
830
|
/**
|
|
754
831
|
* Acquires through BROWSER with recording=false explicitly on the wire. The trusted provider's
|
|
755
832
|
* documented opt-in semantics are the recording guarantee; no recording-status API exists.
|
|
756
833
|
* Account operators remain trusted, and must not attach external observers to private passes.
|
|
757
834
|
*/
|
|
758
|
-
const
|
|
835
|
+
const protectedBindingLayer = (options) => Layer.effect(BrowserRunProtectedBinding)(Effect.gen(function* () {
|
|
759
836
|
const lifecycle = yield* BrowserRunSessionLifecycle;
|
|
760
837
|
const crypto = yield* Crypto.Crypto;
|
|
761
|
-
return { open: Effect.fn("BrowserRunProtectedTransport.open")(function* (policy) {
|
|
838
|
+
return { open: Effect.fn("BrowserRunProtectedTransport.open")(function* (policy, identity) {
|
|
762
839
|
const failure = () => new ProtectedBrowserError({
|
|
763
840
|
reason: "provider",
|
|
764
841
|
dispatch: "not-dispatched",
|
|
@@ -766,7 +843,10 @@ const browserRunProtectedBindingLayer = (options) => Layer.effect(BrowserRunProt
|
|
|
766
843
|
observation: "closed",
|
|
767
844
|
cleanup: "unconfirmed"
|
|
768
845
|
});
|
|
769
|
-
let sessionId;
|
|
846
|
+
let sessionId = identity?.sessionId;
|
|
847
|
+
let detached = false;
|
|
848
|
+
let control;
|
|
849
|
+
let providerIdentity;
|
|
770
850
|
let browser;
|
|
771
851
|
let driver;
|
|
772
852
|
let invalid = false;
|
|
@@ -787,11 +867,11 @@ const browserRunProtectedBindingLayer = (options) => Layer.effect(BrowserRunProt
|
|
|
787
867
|
return cleanup;
|
|
788
868
|
});
|
|
789
869
|
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"))));
|
|
870
|
+
yield* Effect.addFinalizer(() => detached ? Effect.void : close.pipe(Effect.flatMap((state) => state === "confirmed" ? Effect.void : Effect.logWarning("Protected browser exact-session closure unconfirmed"))));
|
|
791
871
|
const acquired = yield* Effect.tryPromise({
|
|
792
872
|
try: async (signal) => {
|
|
793
873
|
let recordingDisabled = false;
|
|
794
|
-
const acquired = await puppeteer.acquire({ fetch: async (input, init) => {
|
|
874
|
+
const acquired = identity === void 0 ? await puppeteer.acquire({ fetch: async (input, init) => {
|
|
795
875
|
const request = new Request(input, init);
|
|
796
876
|
const url = new URL(request.url);
|
|
797
877
|
if (url.pathname === "/v1/devtools/browser" && request.method === "POST") {
|
|
@@ -803,9 +883,9 @@ const browserRunProtectedBindingLayer = (options) => Layer.effect(BrowserRunProt
|
|
|
803
883
|
} }, {
|
|
804
884
|
recording: false,
|
|
805
885
|
keep_alive: Math.min(6e5, Math.max(1e4, policy.maxElapsedMillis))
|
|
806
|
-
});
|
|
886
|
+
}) : { sessionId: Redacted.value(identity.sessionId) };
|
|
807
887
|
sessionId = Redacted.make(Schema.decodeSync(Schema.String.check(Schema.isUUID()))(acquired.sessionId));
|
|
808
|
-
if (!recordingDisabled || signal.aborted || invalid) {
|
|
888
|
+
if (identity === void 0 && !recordingDisabled || signal.aborted || invalid) {
|
|
809
889
|
await runCleanup(terminate);
|
|
810
890
|
throw new ProtectedTransportError({ reason: "stale-reference" });
|
|
811
891
|
}
|
|
@@ -814,7 +894,29 @@ const browserRunProtectedBindingLayer = (options) => Layer.effect(BrowserRunProt
|
|
|
814
894
|
await runCleanup(terminate);
|
|
815
895
|
throw new ProtectedTransportError({ reason: "stale-reference" });
|
|
816
896
|
}
|
|
817
|
-
|
|
897
|
+
let page;
|
|
898
|
+
if (identity === void 0) page = await (await browser.createBrowserContext()).newPage();
|
|
899
|
+
else {
|
|
900
|
+
const context = browser.browserContexts().find((candidate) => candidate.id === Redacted.value(identity.contextId));
|
|
901
|
+
if (context === void 0) throw new ProtectedTransportError({ reason: "stale-reference" });
|
|
902
|
+
const pages = await context.pages();
|
|
903
|
+
let found;
|
|
904
|
+
for (const candidate of pages) {
|
|
905
|
+
const client = await candidate.createCDPSession();
|
|
906
|
+
const info = await client.send("Target.getTargetInfo");
|
|
907
|
+
await client.detach();
|
|
908
|
+
if (info.targetInfo.targetId === Redacted.value(identity.targetId)) found = candidate;
|
|
909
|
+
}
|
|
910
|
+
if (found === void 0 || pages.length !== 1) throw new ProtectedTransportError({ reason: "stale-reference" });
|
|
911
|
+
page = found;
|
|
912
|
+
}
|
|
913
|
+
control = await page.createCDPSession();
|
|
914
|
+
const info = await control.send("Target.getTargetInfo");
|
|
915
|
+
providerIdentity = Schema.decodeSync(ProtectedProviderIdentity)({
|
|
916
|
+
sessionId,
|
|
917
|
+
contextId: Redacted.make(page.browserContext().id ?? ""),
|
|
918
|
+
targetId: Redacted.make(info.targetInfo.targetId)
|
|
919
|
+
});
|
|
818
920
|
await page.setBypassServiceWorker(true);
|
|
819
921
|
await page.setRequestInterception(true);
|
|
820
922
|
page.on("request", (request) => {
|
|
@@ -835,7 +937,8 @@ const browserRunProtectedBindingLayer = (options) => Layer.effect(BrowserRunProt
|
|
|
835
937
|
return ProtectedNativeSession.of({
|
|
836
938
|
browser,
|
|
837
939
|
page,
|
|
838
|
-
close
|
|
940
|
+
close,
|
|
941
|
+
release: Effect.suspend(() => detached ? Effect.void : close.pipe(Effect.asVoid))
|
|
839
942
|
});
|
|
840
943
|
},
|
|
841
944
|
catch: failure
|
|
@@ -850,10 +953,178 @@ const browserRunProtectedBindingLayer = (options) => Layer.effect(BrowserRunProt
|
|
|
850
953
|
cleanup
|
|
851
954
|
}))))));
|
|
852
955
|
driver = yield* makeProtectedNativeTransport(policy).pipe(Effect.provideService(ProtectedNativeSession, acquired), Effect.provideService(Crypto.Crypto, crypto));
|
|
853
|
-
|
|
956
|
+
const exact = providerIdentity;
|
|
957
|
+
if (exact === void 0) return yield* failure();
|
|
958
|
+
return {
|
|
959
|
+
identity: exact,
|
|
960
|
+
driver,
|
|
961
|
+
command: (method, parameters) => Effect.tryPromise({
|
|
962
|
+
try: async () => {
|
|
963
|
+
if (invalid || detached || control === void 0) throw void 0;
|
|
964
|
+
const send = Reflect.get(control, "send");
|
|
965
|
+
return await Reflect.apply(send, control, [method, parameters]);
|
|
966
|
+
},
|
|
967
|
+
catch: failure
|
|
968
|
+
}).pipe(Effect.timeoutOrElse({
|
|
969
|
+
duration: "10 seconds",
|
|
970
|
+
orElse: () => Effect.fail(failure())
|
|
971
|
+
})),
|
|
972
|
+
detach: Effect.tryPromise({
|
|
973
|
+
try: async () => {
|
|
974
|
+
if (invalid || detached || browser === void 0) throw void 0;
|
|
975
|
+
await browser.disconnect();
|
|
976
|
+
detached = true;
|
|
977
|
+
driver?.invalidate();
|
|
978
|
+
},
|
|
979
|
+
catch: failure
|
|
980
|
+
}).pipe(Effect.timeoutOrElse({
|
|
981
|
+
duration: "10 seconds",
|
|
982
|
+
orElse: () => Effect.fail(failure())
|
|
983
|
+
}))
|
|
984
|
+
};
|
|
854
985
|
}, Effect.withTracerEnabled(false)) };
|
|
855
986
|
}));
|
|
987
|
+
/** One binding implementation serves scoped tools and host-managed protected handoffs. */
|
|
988
|
+
const browserRunProtectedBindingLayer = (options) => Layer.effect(BrowserRunProtectedTransport)(Effect.gen(function* () {
|
|
989
|
+
const binding = yield* BrowserRunProtectedBinding;
|
|
990
|
+
return { open: (policy) => binding.open(policy).pipe(Effect.map((session) => session.driver)) };
|
|
991
|
+
})).pipe(Layer.provideMerge(protectedBindingLayer(options)));
|
|
992
|
+
//#endregion
|
|
993
|
+
//#region src/protected-browser/host.ts
|
|
994
|
+
/** Host-only receipt. Store atomically with the controller lease; never return it to a model. */
|
|
995
|
+
var BrowserRunProtectedCheckpoint = class extends Schema.Class("BrowserRunProtectedCheckpoint")({
|
|
996
|
+
...ProtectedProviderIdentity.fields,
|
|
997
|
+
protected: ProtectedBrowserCheckpoint,
|
|
998
|
+
handoffId: Schema.optionalKey(Schema.Redacted(Schema.NonEmptyString.check(Schema.isMaxLength(1024))))
|
|
999
|
+
}) {};
|
|
1000
|
+
/**
|
|
1001
|
+
* Host authority for one protected page. The consumer owns durable generation fencing, authorized
|
|
1002
|
+
* human recipients, checkpoint integrity, vault exposure metadata, and expiry cleanup. Resume only
|
|
1003
|
+
* the latest committed suspended receipt; it never replays browser mutations or creates a page.
|
|
1004
|
+
* Hosted passes require Unrestricted network policy: attachment-local request interception
|
|
1005
|
+
* cannot enforce an ExactHosts policy during detachment. Current credential grants still apply.
|
|
1006
|
+
* Provider inactivity/session deadlines still apply while the host is detached.
|
|
1007
|
+
*/
|
|
1008
|
+
var BrowserRunProtectedHost = class extends Context.Service()("@effect-agent/platform-cloudflare/BrowserRunProtectedHost") {};
|
|
1009
|
+
const failure = (reason) => new ProtectedBrowserError({
|
|
1010
|
+
reason,
|
|
1011
|
+
dispatch: "not-dispatched",
|
|
1012
|
+
milestone: "none",
|
|
1013
|
+
observation: "protected",
|
|
1014
|
+
cleanup: "not-requested"
|
|
1015
|
+
});
|
|
1016
|
+
const Handoff = Schema.Struct({ handoffId: Schema.NonEmptyString.check(Schema.isMaxLength(1024)) });
|
|
1017
|
+
const HandoffState = Schema.Struct({
|
|
1018
|
+
active: Schema.Boolean,
|
|
1019
|
+
handoffId: Schema.optionalKey(Schema.NonEmptyString.check(Schema.isMaxLength(1024))),
|
|
1020
|
+
durationMs: Schema.optionalKey(Schema.Natural)
|
|
1021
|
+
});
|
|
1022
|
+
const LiveView = Schema.Struct({ devtoolsFrontendUrl: Schema.String });
|
|
1023
|
+
/** Compose with browserRunProtectedBindingLayer and BrowserRunSessionLifecycle. */
|
|
1024
|
+
const browserRunProtectedHostLayer = () => Layer.effect(BrowserRunProtectedHost)(Effect.gen(function* () {
|
|
1025
|
+
const binding = yield* BrowserRunProtectedBinding;
|
|
1026
|
+
const lifecycle = yield* BrowserRunSessionLifecycle;
|
|
1027
|
+
const crypto = yield* Crypto.Crypto;
|
|
1028
|
+
const open = Effect.fn("BrowserRunProtectedHost.open")(function* (input, checkpoint) {
|
|
1029
|
+
const policy = yield* Schema.decodeEffect(InteractiveBrowserPolicy)(input).pipe(Effect.mapError(() => failure("denied")));
|
|
1030
|
+
if (policy.network._tag !== "Unrestricted") return yield* failure("unsupported");
|
|
1031
|
+
if (policy.maxElapsedMillis - ((yield* Clock.currentTimeMillis) - (checkpoint?.protected.startedAt ?? (yield* Clock.currentTimeMillis))) <= 0 || checkpoint !== void 0 && checkpoint.protected.startedAt > (yield* Clock.currentTimeMillis)) return yield* failure("timeout");
|
|
1032
|
+
if (checkpoint !== void 0 && checkpoint.protected.actions > policy.maxActions) return yield* failure("denied");
|
|
1033
|
+
const provider = yield* binding.open(policy, checkpoint);
|
|
1034
|
+
const state = yield* makeProtectedBrowserPolicy(policy, checkpoint?.protected).pipe(Effect.provideService(BrowserRunProtectedTransport, { open: () => Effect.succeed({
|
|
1035
|
+
...provider.driver,
|
|
1036
|
+
detach: provider.detach
|
|
1037
|
+
}) }), Effect.provideService(Crypto.Crypto, crypto));
|
|
1038
|
+
const lock = yield* Semaphore.make(1);
|
|
1039
|
+
let handoff = checkpoint;
|
|
1040
|
+
let detached = false;
|
|
1041
|
+
const control = (effect) => lock.withPermitsIfAvailable(1)(Effect.suspend(() => detached ? Effect.fail(failure("closed")) : effect)).pipe(Effect.flatMap(Effect.fromOption(() => failure("busy"))));
|
|
1042
|
+
const fits = (millis) => Effect.gen(function* () {
|
|
1043
|
+
const started = handoff?.protected.startedAt ?? startedAt;
|
|
1044
|
+
if (millis > policy.maxElapsedMillis - ((yield* Clock.currentTimeMillis) - started)) return yield* failure("timeout");
|
|
1045
|
+
});
|
|
1046
|
+
const startedAt = checkpoint?.protected.startedAt ?? (yield* Clock.currentTimeMillis);
|
|
1047
|
+
const getHandoffState = Effect.gen(function* () {
|
|
1048
|
+
if (handoff?.handoffId === void 0) return yield* failure("denied");
|
|
1049
|
+
yield* fits(0);
|
|
1050
|
+
const raw = yield* provider.command("Cloudflare.getHandoffState", {});
|
|
1051
|
+
const result = yield* Schema.decodeUnknownEffect(HandoffState)(raw).pipe(Effect.mapError(() => failure("provider")));
|
|
1052
|
+
if (result.active && result.handoffId === void 0 || result.handoffId !== void 0 && result.handoffId !== Redacted.value(handoff.handoffId)) return yield* failure("denied");
|
|
1053
|
+
return BrowserRunHandoffState.make({
|
|
1054
|
+
active: result.active,
|
|
1055
|
+
...result.durationMs === void 0 ? {} : { durationMs: result.durationMs },
|
|
1056
|
+
...result.handoffId === void 0 ? {} : { handoffId: Redacted.make(result.handoffId) }
|
|
1057
|
+
});
|
|
1058
|
+
});
|
|
1059
|
+
return {
|
|
1060
|
+
handle: state.handle,
|
|
1061
|
+
sessionId: provider.identity.sessionId,
|
|
1062
|
+
suspend: control(Effect.gen(function* () {
|
|
1063
|
+
if (handoff !== void 0) {
|
|
1064
|
+
yield* state.suspend();
|
|
1065
|
+
return handoff;
|
|
1066
|
+
}
|
|
1067
|
+
const suspended = yield* state.suspend();
|
|
1068
|
+
handoff = BrowserRunProtectedCheckpoint.make({
|
|
1069
|
+
...provider.identity,
|
|
1070
|
+
protected: suspended
|
|
1071
|
+
});
|
|
1072
|
+
return handoff;
|
|
1073
|
+
})),
|
|
1074
|
+
handoff: (request) => control(Effect.gen(function* () {
|
|
1075
|
+
if (handoff?.handoffId !== void 0) return yield* failure("busy");
|
|
1076
|
+
const decoded = yield* Schema.decodeEffect(BrowserRunHandoffRequest)(request).pipe(Effect.mapError(() => failure("denied")));
|
|
1077
|
+
yield* fits(decoded.timeout);
|
|
1078
|
+
const suspended = yield* state.suspend(true);
|
|
1079
|
+
const result = yield* provider.command("Cloudflare.handoff", {
|
|
1080
|
+
instructions: decoded.instructions,
|
|
1081
|
+
timeout: decoded.timeout
|
|
1082
|
+
}).pipe(Effect.flatMap(Schema.decodeUnknownEffect(Handoff)), Effect.catch(() => state.handle.close.pipe(Effect.flatMap((cleanup) => Effect.fail(new ProtectedBrowserError({
|
|
1083
|
+
reason: "outcome-unknown",
|
|
1084
|
+
dispatch: "possibly-dispatched",
|
|
1085
|
+
milestone: suspended.milestone,
|
|
1086
|
+
observation: "closed",
|
|
1087
|
+
cleanup
|
|
1088
|
+
}))))), Effect.onInterrupt(() => state.handle.close));
|
|
1089
|
+
handoff = BrowserRunProtectedCheckpoint.make({
|
|
1090
|
+
...provider.identity,
|
|
1091
|
+
protected: suspended,
|
|
1092
|
+
handoffId: Redacted.make(result.handoffId)
|
|
1093
|
+
});
|
|
1094
|
+
return handoff;
|
|
1095
|
+
})),
|
|
1096
|
+
getLiveView: (request) => control(Effect.gen(function* () {
|
|
1097
|
+
if (handoff?.handoffId === void 0) return yield* failure("denied");
|
|
1098
|
+
const decoded = yield* Schema.decodeEffect(BrowserRunLiveViewRequest)(request).pipe(Effect.mapError(() => failure("denied")));
|
|
1099
|
+
yield* fits(decoded.expiresInMs);
|
|
1100
|
+
const raw = yield* provider.command("Cloudflare.getLiveView", {
|
|
1101
|
+
mode: decoded.mode,
|
|
1102
|
+
expiresInMs: decoded.expiresInMs
|
|
1103
|
+
});
|
|
1104
|
+
const result = yield* Schema.decodeUnknownEffect(LiveView)(raw).pipe(Effect.mapError(() => failure("provider")));
|
|
1105
|
+
return yield* Schema.decodeEffect(BrowserRunLiveViewResult)({ devtoolsFrontendUrl: Redacted.make(result.devtoolsFrontendUrl) }).pipe(Effect.mapError(() => failure("provider")));
|
|
1106
|
+
})),
|
|
1107
|
+
getHandoffState: control(getHandoffState),
|
|
1108
|
+
returnControl: control(Effect.gen(function* () {
|
|
1109
|
+
if ((yield* getHandoffState).active) return yield* failure("busy");
|
|
1110
|
+
yield* state.returnControl;
|
|
1111
|
+
handoff = void 0;
|
|
1112
|
+
})),
|
|
1113
|
+
detach: control(Effect.gen(function* () {
|
|
1114
|
+
if (handoff === void 0) return yield* failure("denied");
|
|
1115
|
+
yield* state.detach;
|
|
1116
|
+
detached = true;
|
|
1117
|
+
})),
|
|
1118
|
+
close: state.handle.close
|
|
1119
|
+
};
|
|
1120
|
+
}, Effect.withTracerEnabled(false));
|
|
1121
|
+
return {
|
|
1122
|
+
open: (policy) => open(policy),
|
|
1123
|
+
resume: (checkpoint) => Schema.decodeEffect(BrowserRunProtectedCheckpoint)(checkpoint).pipe(Effect.mapError(() => failure("denied")), Effect.flatMap((decoded) => open(decoded.protected.policy, decoded))),
|
|
1124
|
+
closeSession: (id) => lifecycle.close(id).pipe(Effect.as("confirmed"), Effect.catchCause(() => Effect.succeed("unconfirmed")))
|
|
1125
|
+
};
|
|
1126
|
+
}));
|
|
856
1127
|
//#endregion
|
|
857
|
-
export { browserRunProtectedBindingLayer, browserRunProtectedLayer };
|
|
1128
|
+
export { BrowserRunProtectedCheckpoint, BrowserRunProtectedHost, browserRunProtectedBindingLayer, browserRunProtectedHostLayer, browserRunProtectedLayer };
|
|
858
1129
|
|
|
859
1130
|
//# sourceMappingURL=ProtectedBrowser.mjs.map
|