1import * as Effect from "effect/Effect";
2import * as Binding from "../../Binding.ts";
3import { isBindingHost } from "../Lambda/Function.ts";
4import type { Repository } from "./Repository.ts";
5
6/**
7 * Shared scaffolding for the ECR HTTP bindings.
8 *
9 * NOT exported from `index.ts` — every thin `{Op}Http.ts` in this service is
10 * a `Layer.effect(Cap, makeEcrRepositoryHttpBinding({ … }))`. Everything
11 * except the operation and the IAM action list is boilerplate: per the ECR
12 * service authorization reference every repository-scoped action authorizes
13 * against the repository ARN, and the runtime callable injects the bound
14 * {@link Repository}'s `repositoryName` into every request.
15 */
16
17/** The repository-scoping fields every repository-scoped ECR operation takes. */
18export interface RepositoryScopedRequest {
19 registryId?: string;
20 repositoryName: string;
21}
22
23/**
24 * Build the impl Effect for a repository-scoped ECR operation. The
25 * deploy-time half grants `iamActions` on the bound repository's ARN; the
26 * runtime callable injects the repository's `repositoryName` into every
27 * request (the registry defaults to the caller's own account).
28 *
29 * Registry-level operations (`ecr:GetAuthorizationToken`, which authorizes
30 * only on `Resource: ["*"]`) stay bespoke in their own `{Op}Http.ts`.
31 */
32export const makeEcrRepositoryHttpBinding = <
33 I extends RepositoryScopedRequest,
34 A,
35 E,
36 R,
37>(options: {
38 /**
39 * Short capability name used in the binding sid and runtime span, e.g.
40 * `"DescribeImages"`.
41 */
42 capability: string;
43 /** The distilled operation implementing the capability. */
44 operation: Effect.Effect<(input: I) => Effect.Effect<A, E>, never, R>;
45 /** IAM actions granted on the bound repository's ARN. */
46 iamActions: readonly string[];
47}) =>
48 Effect.gen(function* () {
49 const op = yield* options.operation;
50
51 return Effect.fn(function* (repository: Repository) {
52 const RepositoryName = yield* repository.repositoryName;
53 if (!globalThis.__ALCHEMY_RUNTIME__) {
54 const host = yield* Binding.Host;
55 if (isBindingHost(host)) {
56 yield* host.bind`Allow(${host}, AWS.ECR.${options.capability}(${repository}))`(
57 {
58 policyStatements: [
59 {
60 Effect: "Allow",
61 Action: [...options.iamActions],
62 Resource: [repository.repositoryArn],
63 },
64 ],
65 },
66 );
67 }
68 }
69 // The request is optional at the impl level so contracts whose
70 // remaining fields are all optional (DescribeImages, ListImages, …) can
71 // declare `(request?: …)`; contracts with required fields keep the
72 // parameter required and narrow this signature.
73 return Effect.fn(
74 `AWS.ECR.${options.capability}(${repository.LogicalId})`,
75 )(function* (request?: Omit<I, keyof RepositoryScopedRequest>) {
76 return yield* op({
77 ...(request ?? {}),
78 repositoryName: yield* RepositoryName,
79 } as unknown as I);
80 });
81 });
82 });