@effect-agent/platform-cloudflare 0.1.0-beta.88 → 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.
@@ -1,8 +1,8 @@
1
- import { n as BrowserRunSessionLifecycle } from "./browser-session-lifecycle-CmbQABHC.mjs";
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
- * 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;
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
- 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.decodeEffect(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.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.authorizeAction({
109
+ const rawDecision = yield* access.observation({
110
+ ...context,
135
111
  caller: principal,
136
- action,
137
- exposures: [...exposures]
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
- 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");
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
- }, 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"));
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
- 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");
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* 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(Effect.fromOption(() => 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.decodeEffect(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.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: control.role
229
+ role: "link",
230
+ url: control.url
234
231
  };
235
- yield* authorizeAction(action);
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
- 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.decodeEffect(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.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
- 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.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
- yield* authorize;
335
- yield* validate;
336
- const raw = yield* access.resolve(authorization).pipe(Effect.mapError((error) => fail(error.reason)));
337
- const material = yield* Schema.decodeEffect(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);
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* 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";
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* 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";
324
+ const submit = yield* target(decoded.submit);
325
+ if (submit.role !== "submit" || !sameTarget(offer.target, submit.target)) return yield* fail("denied");
365
326
  }
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)) };
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 browserRunProtectedBindingLayer = (options) => Layer.effect(BrowserRunProtectedTransport)(Effect.gen(function* () {
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
- const page = await (await browser.createBrowserContext()).newPage();
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
- return driver;
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