1import * as Effect from "effect/Effect";
2import * as Layer from "effect/Layer";
3import * as Binding from "../../Binding.ts";
4import type { Input } from "../../Input.ts";
5import * as Output from "../../Output.ts";
6import type { PolicyStatement } from "../IAM/Policy.ts";
7import {
8 hostAwsAccess,
9 regionFromArn,
10 withRuntimeCredentials,
11} from "./BindingHttp.ts";
12import type { MicrovmImage } from "./MicrovmImage.ts";
13
14// Shared scaffolding for the MicroVM runtime bindings. Every `*Http` impl is
15// identical except for the distilled operation, the IAM action(s), the policy
16// scope, and whether the bound image's identifier is injected into the request.
17// NOT exported from `index.ts` — it only backs the per-operation `*Http` files.
18
19/** The distilled operation method, yielded to its request callable. */
20type Operation<Res, Err> = Effect.Effect<
21 (req: any) => Effect.Effect<Res, Err, any>,
22 any,
23 any
24>;
25
26export interface ImageBindingOptions<Req, Res, Err, Self> {
27 /** The `Binding.Service` contract this layer implements. */
28 binding: Binding.Service<
29 Self,
30 string,
31 (
32 image: MicrovmImage,
33 ) => Effect.Effect<(req: Req) => Effect.Effect<Res, Err>>
34 >;
35 /** Operation name, used for the SID label and tracing, e.g. `"RunMicrovm"`. */
36 name: string;
37 /** IAM action(s) the host Function needs, e.g. `["lambda:RunMicrovm"]`. */
38 actions: string[];
39 /** The distilled operation, e.g. `microvms.runMicrovm`. */
40 operation: Operation<Res, Err>;
41 /**
42 * Policy scope (always limited to the bound image's identity — never `["*"]`):
43 * - `"image"` (default) scopes to the exact image ARN (e.g. `RunMicrovm`,
44 * image-read ops).
45 * - `"microvm"` scopes to the MicroVM instances launched from this image via
46 * a `microvm:*` glob derived from the image ARN (same partition, region,
47 * and account). MicroVM instance ARNs are minted at runtime, so an exact
48 * ARN can't be known at deploy time, but the action stays bounded to this
49 * account/region's MicroVMs rather than `["*"]`.
50 * - `"account"` uses `["*"]`. Reserved for collection-level list actions
51 * (e.g. `ListMicrovms`) that AWS only authorizes against `*` and cannot be
52 * resource-scoped, analogous to `ec2:DescribeInstances`.
53 */
54 scope?: "image" | "microvm" | "account";
55 /** Inject `imageIdentifier: <imageArn>` into each request. */
56 injectImageIdentifier?: boolean;
57 /**
58 * Also grant `lambda:PassNetworkConnector` on network connectors in the
59 * image's region — both the account's own connectors and the AWS-managed
60 * ones (e.g. `INTERNET_EGRESS`). Required by `RunMicrovm`, which passes a
61 * network connector (defaulting to the managed `INTERNET_EGRESS`) to the
62 * launched MicroVM.
63 */
64 passNetworkConnector?: boolean;
65}
66
67/**
68 * Derive the `microvm:*` instance-ARN glob from an image ARN by swapping the
69 * `microvm-image:<name>` resource segment for `microvm:*`, keeping the same
70 * `arn:<partition>:lambda:<region>:<account>:` prefix.
71 */
72const microvmGlob = (imageArn: string): string =>
73 `${imageArn.replace(/:microvm-image[:/].*$/, "")}:microvm:*`;
74
75/**
76 * Derive the network-connector ARN globs from an image ARN: the account's own
77 * connectors (`arn:<partition>:lambda:<region>:<account>:network-connector:*`)
78 * and the AWS-managed connectors (same prefix but account `aws`).
79 */
80const networkConnectorGlobs = (imageArn: string): string[] => {
81 // arn:<partition>:lambda:<region>:<account>
82 const prefix = imageArn.replace(/:microvm-image[:/].*$/, "");
83 // arn:<partition>:lambda:<region>
84 const regionPrefix = prefix.replace(/:[^:]*$/, "");
85 return [
86 `${prefix}:network-connector:*`,
87 `${regionPrefix}:aws:network-connector:*`,
88 ];
89};
90
91/** The IAM policy statements an image-scoped MicroVM operation requires. */
92const imagePolicyStatements = <Req, Res, Err, Self>(
93 image: MicrovmImage,
94 options: ImageBindingOptions<Req, Res, Err, Self>,
95): Input<PolicyStatement>[] => [
96 {
97 Effect: "Allow",
98 Action: options.actions,
99 Resource:
100 options.scope === "account"
101 ? // Collection-level list actions only authorize on `*`.
102 ["*"]
103 : options.scope === "microvm"
104 ? [
105 // Instance ops (GetMicrovm, TerminateMicrovm, CreateAuthToken,
106 // …) are authorized by AWS against the image ARN as well as the
107 // instance ARN, so grant both: the exact image and the
108 // `microvm:*` instance glob derived from it.
109 Output.interpolate`${image.imageArn}`,
110 image.imageArn.pipe(Output.map(microvmGlob)),
111 ]
112 : [Output.interpolate`${image.imageArn}`],
113 },
114 ...(options.passNetworkConnector
115 ? [
116 {
117 Effect: "Allow" as const,
118 Action: ["lambda:PassNetworkConnector"],
119 Resource: [
120 image.imageArn.pipe(
121 Output.map((a) => networkConnectorGlobs(a)[0]!),
122 ),
123 image.imageArn.pipe(
124 Output.map((a) => networkConnectorGlobs(a)[1]!),
125 ),
126 ],
127 },
128 ]
129 : []),
130];
131
132/**
133 * Build a MicroVM runtime binding bound to a {@link MicrovmImage}. At deploy it
134 * registers the IAM grant on the host — the Lambda execution role directly, or
135 * (for a Cloudflare Worker) a dedicated assume-role Role whose credentials are
136 * bound onto the worker. At runtime it calls the distilled operation with the
137 * host-appropriate credentials.
138 */
139export const makeImageBinding = <Req, Res, Err, Self>(
140 options: ImageBindingOptions<Req, Res, Err, Self>,
141): Layer.Layer<Self> =>
142 Layer.effect(
143 options.binding as any,
144 Effect.gen(function* () {
145 const run = yield* options.operation;
146 return Effect.fn(function* (image: MicrovmImage) {
147 const host = yield* Binding.Host;
148
149 // Accessors (registered on the host at deploy, resolved at runtime).
150 const imageArn = yield* image.imageArn;
151 // Region is derived from the resolved image ARN — via `Effect.map` of
152 // the accessor, NOT a second Output binding (whose env key would embed
153 // the mapper's source text and is brittle).
154 const region = Effect.map(imageArn, regionFromArn);
155
156 const access = yield* hostAwsAccess(host, () => ({
157 label: `Allow(${host?.LogicalId}, AWS.Lambda.${options.name}(${image.LogicalId}))`,
158 policyStatements: imagePolicyStatements(image, options),
159 }));
160
161 return Effect.fn(`AWS.Lambda.${options.name}(${image.LogicalId})`)(
162 function* (request: Req) {
163 return yield* withRuntimeCredentials(
164 access,
165 region,
166 run(
167 options.injectImageIdentifier
168 ? { ...(request as object), imageIdentifier: yield* imageArn }
169 : request,
170 ),
171 );
172 },
173 );
174 });
175 }),
176 ) as unknown as Layer.Layer<Self>;
177
178export interface AccountBindingOptions<Req, Res, Err, Self> {
179 binding: Binding.Service<
180 Self,
181 string,
182 () => Effect.Effect<(req: Req) => Effect.Effect<Res, Err>>
183 >;
184 name: string;
185 actions: string[];
186 operation: Operation<Res, Err>;
187}
188
189/**
190 * Build an account-scoped MicroVM binding (no resource argument), e.g. for
191 * listing AWS-managed base images. IAM `Resource` is `["*"]`.
192 */
193export const makeAccountBinding = <Req, Res, Err, Self>(
194 options: AccountBindingOptions<Req, Res, Err, Self>,
195): Layer.Layer<Self> =>
196 Layer.effect(
197 options.binding as any,
198 Effect.gen(function* () {
199 const run = yield* options.operation;
200 return Effect.fn(function* () {
201 const host = yield* Binding.Host;
202 const access = yield* hostAwsAccess(host, () => ({
203 label: `Allow(${host?.LogicalId}, AWS.Lambda.${options.name}())`,
204 policyStatements: [
205 { Effect: "Allow", Action: options.actions, Resource: ["*"] },
206 ],
207 }));
208
209 return Effect.fn(`AWS.Lambda.${options.name}()`)(function* (
210 request: Req,
211 ) {
212 // Account-level operations are global; default the STS/endpoint region.
213 const region = Effect.succeed("us-east-1");
214 return yield* withRuntimeCredentials(access, region, run(request));
215 });
216 });
217 }),
218 ) as unknown as Layer.Layer<Self>;