1import * as Effect from "effect/Effect";
2import * as Binding from "../../Binding.ts";
3import * as Output from "../../Output.ts";
4import { isBindingHost } from "../Lambda/Function.ts";
5import type { HealthCheck } from "./HealthCheck.ts";
6import type { HostedZone } from "./HostedZone.ts";
7
8/**
9 * Shared scaffolding for the Route 53 runtime bindings.
10 *
11 * NOT exported from `index.ts` — every `{Op}Http.ts` in this service is a
12 * thin `Layer.effect(Cap, makeRoute53…HttpBinding({ … }))` over one of the
13 * three builders below. Everything except the operation, the IAM action
14 * list, and (for scoped operations) the injected zone / health check id is
15 * boilerplate.
16 *
17 * Route 53 ARNs are global and account-less:
18 * `arn:aws:route53:::hostedzone/{Id}` / `arn:aws:route53:::healthcheck/{Id}`.
19 */
20
21/**
22 * Build the impl Effect for a hosted-zone-scoped Route 53 operation: the
23 * runtime callable injects the bound {@link HostedZone}'s id under
24 * `requestKey` and the deploy-time half grants `actions` on the zone's ARN
25 * (or `*` when the action does not support resource-level permissions).
26 */
27export const makeRoute53ZoneHttpBinding = <I, A, E, R>(options: {
28 /** Fully-qualified binding tag, e.g. `AWS.Route53.ListResourceRecordSets`. */
29 tag: string;
30 /** The distilled operation; the zone id is injected under `requestKey`. */
31 operation: Effect.Effect<(input: I) => Effect.Effect<A, E>, never, R>;
32 /** IAM actions granted on the hosted zone ARN. */
33 actions: readonly string[];
34 /**
35 * Request key the zone id is injected under (`getHostedZone` uses `Id`,
36 * everything else `HostedZoneId`).
37 * @default "HostedZoneId"
38 */
39 requestKey?: "HostedZoneId" | "Id";
40 /**
41 * Grant on `*` instead of the zone ARN — for actions without
42 * resource-level permission support (e.g. `route53:TestDNSAnswer`).
43 */
44 wildcardIam?: boolean;
45}) =>
46 Effect.gen(function* () {
47 const op = yield* options.operation;
48 const requestKey = options.requestKey ?? "HostedZoneId";
49
50 return Effect.fn(function* (zone: HostedZone) {
51 const zoneId = yield* zone.id;
52 if (!globalThis.__ALCHEMY_RUNTIME__) {
53 const host = yield* Binding.Host;
54 if (isBindingHost(host)) {
55 yield* host.bind`Allow(${host}, ${options.tag}(${zone}))`({
56 policyStatements: [
57 {
58 Effect: "Allow",
59 Action: [...options.actions],
60 Resource: options.wildcardIam
61 ? ["*"]
62 : [
63 Output.interpolate`arn:aws:route53:::hostedzone/${zone.id}`,
64 ],
65 },
66 ],
67 });
68 }
69 }
70 return Effect.fn(`${options.tag}(${zone.LogicalId})`)(function* (
71 request?: Omit<I, "HostedZoneId" | "Id">,
72 ) {
73 return yield* op({
74 ...request,
75 [requestKey]: yield* zoneId,
76 } as I);
77 });
78 });
79 });
80
81/**
82 * Build the impl Effect for a health-check-scoped Route 53 operation: the
83 * runtime callable injects the bound {@link HealthCheck}'s id as
84 * `HealthCheckId` and the deploy-time half grants `actions` on the health
85 * check's ARN.
86 */
87export const makeRoute53HealthCheckHttpBinding = <
88 I extends { HealthCheckId: string },
89 A,
90 E,
91 R,
92>(options: {
93 /** Fully-qualified binding tag, e.g. `AWS.Route53.GetHealthCheckStatus`. */
94 tag: string;
95 /** The distilled operation; `HealthCheckId` is injected from the check. */
96 operation: Effect.Effect<(input: I) => Effect.Effect<A, E>, never, R>;
97 /** IAM actions granted on the health check ARN. */
98 actions: readonly string[];
99}) =>
100 Effect.gen(function* () {
101 const op = yield* options.operation;
102
103 return Effect.fn(function* (healthCheck: HealthCheck) {
104 const healthCheckId = yield* healthCheck.id;
105 if (!globalThis.__ALCHEMY_RUNTIME__) {
106 const host = yield* Binding.Host;
107 if (isBindingHost(host)) {
108 yield* host.bind`Allow(${host}, ${options.tag}(${healthCheck}))`({
109 policyStatements: [
110 {
111 Effect: "Allow",
112 Action: [...options.actions],
113 Resource: [
114 Output.interpolate`arn:aws:route53:::healthcheck/${healthCheck.id}`,
115 ],
116 },
117 ],
118 });
119 }
120 }
121 return Effect.fn(`${options.tag}(${healthCheck.LogicalId})`)(function* (
122 request?: Omit<I, "HealthCheckId">,
123 ) {
124 return yield* op({
125 ...request,
126 HealthCheckId: yield* healthCheckId,
127 } as I);
128 });
129 });
130 });
131
132/**
133 * Build the impl Effect for an account-level Route 53 operation (no resource
134 * argument): the deploy-time half grants `actions` on `resources`
135 * (default `*`).
136 */
137export const makeRoute53AccountHttpBinding = <I, A, E, R>(options: {
138 /** Fully-qualified binding tag, e.g. `AWS.Route53.ListHostedZones`. */
139 tag: string;
140 /** The distilled operation, invoked with the caller's request as-is. */
141 operation: Effect.Effect<(input: I) => Effect.Effect<A, E>, never, R>;
142 /** IAM actions granted on `resources`. */
143 actions: readonly string[];
144 /**
145 * IAM resources the actions are granted on.
146 * @default ["*"]
147 */
148 resources?: readonly string[];
149}) =>
150 Effect.gen(function* () {
151 const op = yield* options.operation;
152
153 return Effect.fn(function* () {
154 if (!globalThis.__ALCHEMY_RUNTIME__) {
155 const host = yield* Binding.Host;
156 if (isBindingHost(host)) {
157 yield* host.bind`Allow(${host}, ${options.tag}())`({
158 policyStatements: [
159 {
160 Effect: "Allow",
161 Action: [...options.actions],
162 Resource: [...(options.resources ?? ["*"])],
163 },
164 ],
165 });
166 }
167 }
168 return Effect.fn(options.tag)(function* (request?: I) {
169 return yield* op((request ?? {}) as I);
170 });
171 });
172 });