1import type { Credentials } from "@distilled.cloud/aws/Credentials";
2import { Region as AwsRegion } from "@distilled.cloud/aws/Region";
3import * as Effect from "effect/Effect";
4import type * as HttpClient from "effect/http/HttpClient";
5import * as Binding from "../../Binding.ts";
6import { isBindingHost } from "../Lambda/Function.ts";
7import type { Certificate } from "./Certificate.ts";
8
9/**
10 * Shared HTTP scaffolding for the ACM runtime bindings.
11 *
12 * Every ACM capability follows the same shape — resolve the distilled
13 * operation, register an IAM policy statement on the binding host, and return
14 * a runtime callable that injects the certificate ARN. The only variation is
15 * the operation, the IAM action(s), and whether the binding is scoped to one
16 * certificate or to the whole account, so those are the only inputs.
17 *
18 * All calls are pinned to `us-east-1`, matching where the
19 * {@link Certificate} resource provider requests certificates (the region
20 * required for CloudFront viewer certificates).
21 *
22 * @internal — not exported from `index.ts`.
23 */
24
25const ACM_REGION = "us-east-1" as const;
26
27type AcmRequirements = Credentials | AwsRegion | HttpClient.HttpClient;
28
29const withAcmRegion = <A, E, R>(effect: Effect.Effect<A, E, R>) =>
30 // `AwsRegion`'s service value is an `Effect<RegionName>` (see
31 // `@distilled.cloud/aws/Region`), so it must be provided as an effect, not
32 // a bare string.
33 effect.pipe(Effect.provideService(AwsRegion, Effect.succeed(ACM_REGION)));
34
35export interface AcmHttpBindingConfig<Req extends object, Out, Err> {
36 /**
37 * Short capability name used in the binding sid and runtime span, e.g.
38 * `"DescribeCertificate"`.
39 */
40 capability: string;
41 /**
42 * IAM actions granted to the binding host, e.g. `["acm:GetCertificate"]`.
43 */
44 iamActions: readonly string[];
45 /**
46 * The distilled ACM operation implementing the capability.
47 */
48 operation: Effect.Effect<
49 (input: Req & { CertificateArn: string }) => Effect.Effect<Out, Err>,
50 never,
51 AcmRequirements
52 >;
53}
54
55/**
56 * Build the implementation effect for a certificate-scoped ACM capability:
57 * `Layer.effect(Cap, makeAcmCertificateHttpBinding({ ... }))`.
58 *
59 * `Req` infers as the operation's *full* distilled request (inference against
60 * `Req & { CertificateArn: string }` captures the whole source type). The
61 * runtime callable injects the bound certificate's ARN, so its request type
62 * is `Omit<Req, "CertificateArn">` — matching the capability contracts, which
63 * declare `Omit<acm.XRequest, "CertificateArn">`.
64 */
65export const makeAcmCertificateHttpBinding = <Req extends object, Out, Err>(
66 config: AcmHttpBindingConfig<Req, Out, Err>,
67) =>
68 Effect.gen(function* () {
69 const op = yield* withAcmRegion(config.operation);
70
71 return Effect.fn(function* (certificate: Certificate) {
72 const CertificateArn = yield* certificate.certificateArn;
73 if (!globalThis.__ALCHEMY_RUNTIME__) {
74 const host = yield* Binding.Host;
75 if (isBindingHost(host)) {
76 yield* host.bind`Allow(${host}, AWS.ACM.${config.capability}(${certificate}))`(
77 {
78 policyStatements: [
79 {
80 Effect: "Allow",
81 Action: [...config.iamActions],
82 Resource: [certificate.certificateArn],
83 },
84 ],
85 },
86 );
87 }
88 }
89 return Effect.fn(
90 `AWS.ACM.${config.capability}(${certificate.LogicalId})`,
91 )(function* (request?: Omit<Req, "CertificateArn">) {
92 // Sound: at instantiation `Req` always contains `CertificateArn: string`
93 // (every certificate-scoped distilled request does), so
94 // `Omit<Req, "CertificateArn"> & { CertificateArn: string }` is exactly
95 // `Req & { CertificateArn: string }`. TypeScript cannot prove this for
96 // an unresolved type parameter, hence the precise assertion.
97 //
98 // The region must be pinned HERE, at the call site: the yield-time
99 // snapshot is only a fallback — the calling fiber's ambient Region
100 // (the host Function's own region) wins over it, so pinning only at
101 // the yield would silently route the call to the wrong region.
102 return yield* withAcmRegion(
103 op({
104 ...request,
105 CertificateArn: yield* CertificateArn,
106 } as Req & { CertificateArn: string }),
107 );
108 });
109 });
110 });
111
112export interface AcmAccountHttpBindingConfig<Req extends object, Out, Err> {
113 /**
114 * Short capability name used in the binding sid and runtime span, e.g.
115 * `"ListCertificates"`.
116 */
117 capability: string;
118 /**
119 * IAM actions granted to the binding host on `Resource: ["*"]` (ACM
120 * account-level operations are not resource-scoped).
121 */
122 iamActions: readonly string[];
123 /**
124 * The distilled ACM operation implementing the capability.
125 */
126 operation: Effect.Effect<
127 (input: Req) => Effect.Effect<Out, Err>,
128 never,
129 AcmRequirements
130 >;
131}
132
133/**
134 * Build the implementation effect for an account-level ACM capability (no
135 * certificate argument): `Layer.effect(Cap, makeAcmAccountHttpBinding({ ... }))`.
136 */
137export const makeAcmAccountHttpBinding = <Req extends object, Out, Err>(
138 config: AcmAccountHttpBindingConfig<Req, Out, Err>,
139) =>
140 Effect.gen(function* () {
141 const op = yield* withAcmRegion(config.operation);
142
143 return Effect.fn(function* () {
144 if (!globalThis.__ALCHEMY_RUNTIME__) {
145 const host = yield* Binding.Host;
146 if (isBindingHost(host)) {
147 yield* host.bind`Allow(${host}, AWS.ACM.${config.capability}())`({
148 policyStatements: [
149 {
150 Effect: "Allow",
151 Action: [...config.iamActions],
152 Resource: ["*"],
153 },
154 ],
155 });
156 }
157 }
158 return Effect.fn(`AWS.ACM.${config.capability}`)(function* (
159 request?: Req,
160 ) {
161 // Call-site pin — see makeAcmCertificateHttpBinding above.
162 return yield* withAcmRegion(op(request ?? ({} as Req)));
163 });
164 });
165 });