1import * as AWS from "@/AWS";
2import * as Cloudflare from "@/Cloudflare";
3import * as Effect from "effect/Effect";
4import * as Layer from "effect/Layer";
5import * as Schedule from "effect/Schedule";
6import * as FetchHttpClient from "effect/http/FetchHttpClient";
7import * as HttpClient from "effect/http/HttpClient";
8import { HttpServerRequest } from "effect/http/HttpServerRequest";
9import * as HttpServerResponse from "effect/http/HttpServerResponse";
10import { Sandbox } from "./sandbox.ts";
11
12/**
13 * Cloudflare Worker orchestrator — the cross-cloud analog of the Lambda
14 * {@link import("./orchestrator.ts")} fixture. It binds the AWS MicroVM
15 * instance operations to the {@link Sandbox} image and exposes one HTTP route
16 * per operation.
17 *
18 * Because a Worker has no AWS execution role, binding these operations causes
19 * Alchemy to provision an IAM User + AccessKey + assume-role Role (once for the
20 * worker) and assume that role at runtime — see `MicrovmBinding.ts`.
21 */
22export default Cloudflare.Worker(
23 "MicrovmWorker",
24 { main: import.meta.filename },
25 Effect.gen(function* () {
26 const runMicrovm = yield* AWS.Lambda.RunMicrovm(Sandbox);
27 const getMicrovm = yield* AWS.Lambda.GetMicrovm(Sandbox);
28 const listMicrovms = yield* AWS.Lambda.ListMicrovms(Sandbox);
29 const terminateMicrovm = yield* AWS.Lambda.TerminateMicrovm(Sandbox);
30 const createAuthToken = yield* AWS.Lambda.CreateAuthToken(Sandbox);
31
32 return {
33 fetch: Effect.gen(function* () {
34 const request = yield* HttpServerRequest;
35 const url = new URL(request.url, "http://microvm");
36 const pathname = url.pathname;
37
38 if (request.method === "POST" && pathname === "/run") {
39 const vm = yield* runMicrovm({
40 idlePolicy: {
41 maxIdleDurationSeconds: 900,
42 suspendedDurationSeconds: 300,
43 autoResumeEnabled: true,
44 },
45 });
46 return yield* HttpServerResponse.json({
47 microvmId: vm.microvmId,
48 endpoint: vm.endpoint,
49 state: vm.state,
50 });
51 }
52
53 if (request.method === "GET" && pathname === "/get") {
54 const id = url.searchParams.get("id")!;
55 const vm = yield* getMicrovm({ microvmIdentifier: id });
56 return yield* HttpServerResponse.json({ state: vm.state });
57 }
58
59 if (request.method === "GET" && pathname === "/list") {
60 const { items } = yield* listMicrovms({});
61 return yield* HttpServerResponse.json({ count: items.length });
62 }
63
64 if (request.method === "POST" && pathname === "/auth-token") {
65 const id = url.searchParams.get("id")!;
66 const { authToken } = yield* createAuthToken({
67 microvmIdentifier: id,
68 expirationInMinutes: 5,
69 allowedPorts: [{ port: 8080 }],
70 });
71 return yield* HttpServerResponse.json({
72 hasToken: Object.keys(authToken).length > 0,
73 });
74 }
75
76 if (request.method === "POST" && pathname === "/terminate") {
77 const id = url.searchParams.get("id")!;
78 yield* terminateMicrovm({ microvmIdentifier: id });
79 return yield* HttpServerResponse.json({ ok: true });
80 }
81
82 // Run a MicroVM, then exercise BOTH the in-VM tagged-RPC server (the
83 // `hello` method) and its raw `fetch` handler (the `/echo` route)
84 // end-to-end over the MicroVM endpoint — entirely from a Worker, using
85 // assume-role credentials.
86 if (request.method === "POST" && pathname === "/rpc") {
87 const message = url.searchParams.get("message") ?? "world";
88 const vm = yield* runMicrovm({
89 idlePolicy: {
90 maxIdleDurationSeconds: 900,
91 suspendedDurationSeconds: 300,
92 autoResumeEnabled: true,
93 },
94 });
95 // Always terminate the MicroVM we launched — on success OR failure.
96 return yield* Effect.gen(function* () {
97 yield* getMicrovm({ microvmIdentifier: vm.microvmId }).pipe(
98 Effect.flatMap((m) =>
99 m.state === "RUNNING"
100 ? Effect.void
101 : Effect.fail(new Error(`microvm ${m.state}`)),
102 ),
103 Effect.retry({
104 schedule: Schedule.spaced("2 seconds"),
105 times: 30,
106 }),
107 Effect.orDie,
108 );
109 const { authToken } = yield* createAuthToken({
110 microvmIdentifier: vm.microvmId,
111 expirationInMinutes: 5,
112 allowedPorts: [{ port: 8080 }],
113 });
114
115 // RPC path: typed stub → `hello`.
116 const sandbox = yield* AWS.Lambda.connectMicrovm(Sandbox, {
117 endpoint: vm.endpoint,
118 authToken,
119 });
120 const reply = yield* sandbox.hello(message).pipe(
121 Effect.retry({
122 schedule: Schedule.exponential("500 millis"),
123 times: 8,
124 }),
125 Effect.orDie,
126 );
127
128 // fetch path: raw HTTPS GET to the in-VM `/echo` route.
129 const client = yield* HttpClient.HttpClient;
130 const headers = AWS.Lambda.microvmAuthHeaders(authToken);
131 const echoRes = yield* client
132 .get(
133 `https://${vm.endpoint}/echo?message=${encodeURIComponent(message)}`,
134 { headers },
135 )
136 .pipe(
137 Effect.retry({
138 schedule: Schedule.exponential("500 millis"),
139 times: 8,
140 }),
141 Effect.orDie,
142 );
143 const echo = (yield* echoRes.json.pipe(Effect.orDie)) as {
144 message: string;
145 };
146
147 return yield* HttpServerResponse.json({
148 reply,
149 echo: echo.message,
150 });
151 }).pipe(
152 Effect.ensuring(
153 terminateMicrovm({ microvmIdentifier: vm.microvmId }).pipe(
154 Effect.ignore,
155 ),
156 ),
157 Effect.provide(FetchHttpClient.layer),
158 );
159 }
160
161 return HttpServerResponse.text("ok");
162 }).pipe(Effect.orDie),
163 };
164 }).pipe(
165 Effect.provide(
166 Layer.mergeAll(
167 AWS.Lambda.RunMicrovmHttp,
168 AWS.Lambda.GetMicrovmHttp,
169 AWS.Lambda.ListMicrovmsHttp,
170 AWS.Lambda.TerminateMicrovmHttp,
171 AWS.Lambda.CreateAuthTokenHttp,
172 ),
173 ),
174 ),
175);