| 1 | import * as Effect from "effect/Effect"; |
| 2 | import * as Binding from "../../Binding.ts"; |
| 3 | import { isBindingHost } from "../Lambda/Function.ts"; |
| 4 | import { Region } from "../Region.ts"; |
| 5 | import type { PublicRepository } from "./Repository.ts"; |
| 6 | |
| 7 | /** |
| 8 | * Shared HTTP scaffolding for the Amazon ECR Public runtime bindings. |
| 9 | * |
| 10 | * NOT exported from `index.ts` — every `{Op}Http.ts` in this service is a |
| 11 | * thin `Layer.effect(Cap, make…HttpBinding({ … }))` over one of the builders |
| 12 | * below. Everything except the operation, the IAM action list, and (for the |
| 13 | * repository-scoped builder) the injected repository name is boilerplate. |
| 14 | * |
| 15 | * Amazon ECR Public is a single global registry served exclusively from the |
| 16 | * `us-east-1` endpoint, so every operation is resolved with the Region |
| 17 | * pinned via {@link pinEcrPublic} — exactly like the resource provider in |
| 18 | * `Repository.ts`. |
| 19 | * |
| 20 | * Per the `ecr-public` service authorization reference, repository-scoped |
| 21 | * actions authorize on the repository ARN, while registry-level actions |
| 22 | * (`GetAuthorizationToken`, `DescribeRegistries`, `GetRegistryCatalogData`) |
| 23 | * support no repository resource type, so the registry builder grants on |
| 24 | * `Resource: ["*"]`. |
| 25 | */ |
| 26 | |
| 27 | const US_EAST_1 = "us-east-1"; |
| 28 | |
| 29 | /** Pin a distilled `ecr-public` effect to the service's home region. */ |
| 30 | export const pinEcrPublic = <A, E, R>(effect: Effect.Effect<A, E, R>) => |
| 31 | effect.pipe(Effect.provideService(Region, Effect.succeed(US_EAST_1))); |
| 32 | |
| 33 | /** |
| 34 | * Build the impl Effect for an operation scoped to one |
| 35 | * {@link PublicRepository}. The runtime callable injects the bound |
| 36 | * repository's name as the request's `repositoryName`; the deploy-time half |
| 37 | * grants `iamActions` on the repository's ARN. |
| 38 | */ |
| 39 | export const makePublicRepositoryHttpBinding = < |
| 40 | I extends { repositoryName: string }, |
| 41 | A, |
| 42 | E, |
| 43 | R, |
| 44 | >(options: { |
| 45 | /** |
| 46 | * Short capability name used in the binding sid and runtime span, e.g. |
| 47 | * `"DescribeImages"`. |
| 48 | */ |
| 49 | capability: string; |
| 50 | /** IAM actions granted on the repository ARN. */ |
| 51 | iamActions: readonly string[]; |
| 52 | /** The distilled operation; `repositoryName` is injected. */ |
| 53 | operation: Effect.Effect<(input: I) => Effect.Effect<A, E>, never, R>; |
| 54 | }) => |
| 55 | Effect.gen(function* () { |
| 56 | const op = yield* pinEcrPublic(options.operation); |
| 57 | |
| 58 | return Effect.fn(function* (repository: PublicRepository) { |
| 59 | const RepositoryName = yield* repository.repositoryName; |
| 60 | if (!globalThis.__ALCHEMY_RUNTIME__) { |
| 61 | const host = yield* Binding.Host; |
| 62 | if (isBindingHost(host)) { |
| 63 | yield* host.bind`Allow(${host}, AWS.ECRPublic.${options.capability}(${repository}))`( |
| 64 | { |
| 65 | policyStatements: [ |
| 66 | { |
| 67 | Effect: "Allow", |
| 68 | Action: [...options.iamActions], |
| 69 | Resource: [repository.repositoryArn], |
| 70 | }, |
| 71 | ], |
| 72 | }, |
| 73 | ); |
| 74 | } |
| 75 | } |
| 76 | return Effect.fn( |
| 77 | `AWS.ECRPublic.${options.capability}(${repository.LogicalId})`, |
| 78 | )(function* (request?: Omit<I, "repositoryName">) { |
| 79 | // The region must also be pinned at the call site: the yield-time |
| 80 | // snapshot is only a fallback — the calling fiber's ambient Region |
| 81 | // (the host Function's own region) wins over it. |
| 82 | return yield* pinEcrPublic( |
| 83 | op({ |
| 84 | ...request, |
| 85 | repositoryName: yield* RepositoryName, |
| 86 | } as unknown as I), |
| 87 | ); |
| 88 | }); |
| 89 | }); |
| 90 | }); |
| 91 | |
| 92 | /** |
| 93 | * Build the impl Effect for a registry-level ECR Public operation |
| 94 | * (`GetAuthorizationToken`, `DescribeRegistries`, `GetRegistryCatalogData` — |
| 95 | * none of which are repository-scoped, so the grant is on |
| 96 | * `Resource: ["*"]`). |
| 97 | */ |
| 98 | export const makePublicRegistryHttpBinding = < |
| 99 | I extends object, |
| 100 | A, |
| 101 | E, |
| 102 | R, |
| 103 | >(options: { |
| 104 | /** |
| 105 | * Short capability name used in the binding sid and runtime span, e.g. |
| 106 | * `"GetAuthorizationToken"`. |
| 107 | */ |
| 108 | capability: string; |
| 109 | /** IAM actions granted on `Resource: ["*"]`. */ |
| 110 | iamActions: readonly string[]; |
| 111 | /** The distilled operation implementing the capability. */ |
| 112 | operation: Effect.Effect<(input: I) => Effect.Effect<A, E>, never, R>; |
| 113 | }) => |
| 114 | Effect.gen(function* () { |
| 115 | const op = yield* pinEcrPublic(options.operation); |
| 116 | |
| 117 | return Effect.fn(function* () { |
| 118 | if (!globalThis.__ALCHEMY_RUNTIME__) { |
| 119 | const host = yield* Binding.Host; |
| 120 | if (isBindingHost(host)) { |
| 121 | yield* host.bind`Allow(${host}, AWS.ECRPublic.${options.capability}())`( |
| 122 | { |
| 123 | policyStatements: [ |
| 124 | { |
| 125 | Effect: "Allow", |
| 126 | Action: [...options.iamActions], |
| 127 | Resource: ["*"], |
| 128 | }, |
| 129 | ], |
| 130 | }, |
| 131 | ); |
| 132 | } |
| 133 | } |
| 134 | return Effect.fn(`AWS.ECRPublic.${options.capability}`)(function* ( |
| 135 | request?: I, |
| 136 | ) { |
| 137 | // Call-site region pin — see makePublicRepositoryHttpBinding above. |
| 138 | return yield* pinEcrPublic(op((request ?? {}) as I)); |
| 139 | }); |
| 140 | }); |
| 141 | }); |