| 1 | import * as Effect from "effect/Effect"; |
| 2 | import * as Layer from "effect/Layer"; |
| 3 | import * as Binding from "../../Binding.ts"; |
| 4 | import type { Input } from "../../Input.ts"; |
| 5 | import * as Output from "../../Output.ts"; |
| 6 | import type { PolicyStatement } from "../IAM/Policy.ts"; |
| 7 | import { |
| 8 | hostAwsAccess, |
| 9 | regionFromArn, |
| 10 | withRuntimeCredentials, |
| 11 | } from "./BindingHttp.ts"; |
| 12 | import 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. */ |
| 20 | type Operation<Res, Err> = Effect.Effect< |
| 21 | (req: any) => Effect.Effect<Res, Err, any>, |
| 22 | any, |
| 23 | any |
| 24 | >; |
| 25 | |
| 26 | export 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 | */ |
| 72 | const 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 | */ |
| 80 | const 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. */ |
| 92 | const 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 | */ |
| 139 | export 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 | |
| 178 | export 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 | */ |
| 193 | export 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>; |