1import * as cloudfront from "@distilled.cloud/aws/cloudfront";
2import * as Effect from "effect/Effect";
3import * as Option from "effect/Option";
4import * as Stream from "effect/Stream";
5import { isResolved } from "../../Diff.ts";
6import { createPhysicalName } from "../../PhysicalName.ts";
7import * as Provider from "../../Provider.ts";
8import { Resource } from "../../Resource.ts";
9import type { Providers } from "../Providers.ts";
10
11export interface OriginAccessControlProps {
12 /**
13 * Name of the origin access control. If omitted, a deterministic name is generated.
14 */
15 name?: string;
16 /**
17 * Optional description of the origin access control.
18 */
19 description?: string;
20 /**
21 * The origin type this OAC signs for.
22 * @default "s3"
23 */
24 originType?: cloudfront.OriginAccessControlOriginTypes;
25 /**
26 * How CloudFront should apply signing.
27 * @default "always"
28 */
29 signingBehavior?: cloudfront.OriginAccessControlSigningBehaviors;
30 /**
31 * Signing protocol used by CloudFront.
32 * @default "sigv4"
33 */
34 signingProtocol?: cloudfront.OriginAccessControlSigningProtocols;
35}
36
37export interface OriginAccessControl extends Resource<
38 "AWS.CloudFront.OriginAccessControl",
39 OriginAccessControlProps,
40 {
41 /**
42 * CloudFront-assigned OAC identifier.
43 */
44 originAccessControlId: string;
45 /**
46 * Name of the OAC.
47 */
48 name: string;
49 /**
50 * Current description of the OAC.
51 */
52 description: string | undefined;
53 /**
54 * Origin type configured for the OAC.
55 */
56 originType: cloudfront.OriginAccessControlOriginTypes;
57 /**
58 * Signing behavior configured for the OAC.
59 */
60 signingBehavior: cloudfront.OriginAccessControlSigningBehaviors;
61 /**
62 * Signing protocol configured for the OAC.
63 */
64 signingProtocol: cloudfront.OriginAccessControlSigningProtocols;
65 /**
66 * Most recent entity tag for update/delete operations.
67 */
68 etag: string | undefined;
69 },
70 never,
71 Providers
72> {}
73
74/**
75 * A CloudFront Origin Access Control for private origins.
76 *
77 * `OriginAccessControl` is the recommended CloudFront access model for private
78 * S3 origins and newer signed-origin integrations.
79 * ### Creating Origin Access Controls
80 * **Example:** S3 Origin Access Control
81 * ```typescript
82 * const oac = yield* OriginAccessControl("SiteOriginAccess", {
83 * originType: "s3",
84 * });
85 * ```
86 *
87 * @resource
88 */
89export const OriginAccessControl = Resource<OriginAccessControl>(
90 "AWS.CloudFront.OriginAccessControl",
91);
92
93export const OriginAccessControlProvider = () =>
94 Provider.effect(
95 OriginAccessControl,
96 Effect.gen(function* () {
97 const getByName = Effect.fn(function* (name: string) {
98 const summary = yield* cloudfront.listOriginAccessControls
99 .pages({})
100 .pipe(
101 Stream.map((page) => page.OriginAccessControlList?.Items ?? []),
102 Stream.flattenIterable,
103 Stream.filter((item) => item.Name === name),
104 Stream.runHead,
105 Effect.map(Option.getOrUndefined),
106 );
107 if (!summary?.Id) {
108 return undefined;
109 }
110 const config = yield* cloudfront.getOriginAccessControlConfig({
111 Id: summary.Id,
112 });
113 return {
114 OriginAccessControl: {
115 Id: summary.Id,
116 OriginAccessControlConfig: config.OriginAccessControlConfig,
117 },
118 ETag: config.ETag,
119 };
120 });
121
122 const getCurrent = Effect.fn(function* (
123 output: OriginAccessControl["Attributes"] | undefined,
124 ) {
125 if (!output?.originAccessControlId) {
126 return undefined;
127 }
128 const config = yield* cloudfront
129 .getOriginAccessControlConfig({
130 Id: output.originAccessControlId,
131 })
132 .pipe(
133 Effect.catchTag("NoSuchOriginAccessControl", () =>
134 Effect.succeed(undefined),
135 ),
136 );
137
138 if (!config?.OriginAccessControlConfig) {
139 return undefined;
140 }
141
142 return {
143 OriginAccessControl: {
144 Id: output.originAccessControlId,
145 OriginAccessControlConfig: config.OriginAccessControlConfig,
146 },
147 ETag: config.ETag,
148 };
149 });
150
151 return {
152 stables: ["originAccessControlId"],
153 list: () =>
154 cloudfront.listOriginAccessControls.pages({}).pipe(
155 Stream.map((page) => page.OriginAccessControlList?.Items ?? []),
156 Stream.flattenIterable,
157 Stream.filter((summary) => summary.Id !== undefined),
158 Stream.mapEffect((summary) =>
159 Effect.gen(function* () {
160 // Fetch per-item config for the fresh ETag (the list summary
161 // omits it). Tolerate a concurrent delete between list and get.
162 const config = yield* cloudfront
163 .getOriginAccessControlConfig({ Id: summary.Id! })
164 .pipe(
165 Effect.catchTag("NoSuchOriginAccessControl", () =>
166 Effect.succeed(undefined),
167 ),
168 );
169 return toAttrs(
170 {
171 Id: summary.Id!,
172 OriginAccessControlConfig:
173 config?.OriginAccessControlConfig ?? {
174 Name: summary.Name,
175 Description: summary.Description,
176 OriginAccessControlOriginType:
177 summary.OriginAccessControlOriginType,
178 SigningBehavior: summary.SigningBehavior,
179 SigningProtocol: summary.SigningProtocol,
180 },
181 },
182 config?.ETag,
183 summary.Name,
184 );
185 }),
186 ),
187 Stream.runCollect,
188 Effect.map((chunk) => Array.from(chunk)),
189 ),
190 diff: Effect.fn(function* ({ id, olds, news: _news }) {
191 if (!isResolved(_news)) return undefined;
192 const news = _news as typeof olds;
193 if (
194 (yield* createName(id, olds ?? {})) !==
195 (yield* createName(id, news))
196 ) {
197 return { action: "replace" } as const;
198 }
199 }),
200 read: Effect.fn(function* ({ id, olds, output }) {
201 const existing =
202 (yield* getCurrent(output)) ??
203 (yield* getByName(yield* createName(id, olds ?? {})));
204
205 if (!existing?.OriginAccessControl?.Id) {
206 return undefined;
207 }
208
209 return toAttrs(
210 existing.OriginAccessControl,
211 existing.ETag,
212 yield* createName(id, olds ?? {}),
213 );
214 }),
215 reconcile: Effect.fn(function* ({ id, news, output, session }) {
216 const name = yield* createName(id, news);
217
218 // Observe — locate the OAC by id (cached on `output`) or by
219 // name. Trust observed cloud state, not stale `olds`.
220 const observed =
221 (yield* getCurrent(output)) ?? (yield* getByName(name));
222
223 // Ensure — create the OAC if it's missing. Tolerate
224 // `OriginAccessControlAlreadyExists` (race with a peer
225 // reconciler).
226 if (!observed?.OriginAccessControl?.Id) {
227 const created = yield* cloudfront
228 .createOriginAccessControl({
229 OriginAccessControlConfig: {
230 Name: name,
231 Description: news.description,
232 OriginAccessControlOriginType: news.originType ?? "s3",
233 SigningBehavior: news.signingBehavior ?? "always",
234 SigningProtocol: news.signingProtocol ?? "sigv4",
235 },
236 })
237 .pipe(
238 Effect.catchTag("OriginAccessControlAlreadyExists", () =>
239 getByName(name).pipe(
240 Effect.flatMap((existing) =>
241 existing
242 ? Effect.succeed(existing)
243 : Effect.fail(
244 new Error(
245 `Origin access control '${name}' already exists but could not be recovered`,
246 ),
247 ),
248 ),
249 ),
250 ),
251 );
252
253 if (!created.OriginAccessControl?.Id) {
254 return yield* Effect.fail(
255 new Error("createOriginAccessControl returned no identifier"),
256 );
257 }
258
259 yield* session.note(created.OriginAccessControl.Id);
260 return toAttrs(created.OriginAccessControl, created.ETag, name);
261 }
262
263 // Sync — patch the observed config to the desired state. The
264 // freshly observed ETag handles optimistic concurrency.
265 const observedConfig =
266 observed.OriginAccessControl.OriginAccessControlConfig;
267 const updated = yield* cloudfront.updateOriginAccessControl({
268 Id: observed.OriginAccessControl.Id,
269 IfMatch: observed.ETag,
270 OriginAccessControlConfig: {
271 Name: observedConfig?.Name ?? name,
272 Description: news.description,
273 OriginAccessControlOriginType:
274 news.originType ??
275 observedConfig?.OriginAccessControlOriginType ??
276 "s3",
277 SigningBehavior:
278 news.signingBehavior ??
279 observedConfig?.SigningBehavior ??
280 "always",
281 SigningProtocol:
282 news.signingProtocol ??
283 observedConfig?.SigningProtocol ??
284 "sigv4",
285 },
286 });
287
288 if (!updated.OriginAccessControl?.Id) {
289 return yield* Effect.fail(
290 new Error("updateOriginAccessControl returned no identifier"),
291 );
292 }
293
294 yield* session.note(observed.OriginAccessControl.Id);
295 return toAttrs(
296 updated.OriginAccessControl,
297 updated.ETag,
298 observedConfig?.Name ?? name,
299 );
300 }),
301 delete: Effect.fn(function* ({ output }) {
302 yield* cloudfront
303 .deleteOriginAccessControl({
304 Id: output.originAccessControlId,
305 IfMatch: output.etag,
306 })
307 .pipe(
308 Effect.catchTag("NoSuchOriginAccessControl", () => Effect.void),
309 );
310 }),
311 };
312 }),
313 );
314
315const createName = (id: string, props: OriginAccessControlProps) =>
316 props.name
317 ? Effect.succeed(props.name)
318 : createPhysicalName({
319 id,
320 maxLength: 64,
321 lowercase: true,
322 });
323
324const toAttrs = (
325 oac: cloudfront.OriginAccessControl,
326 etag: string | undefined,
327 fallbackName: string,
328) => ({
329 originAccessControlId: oac.Id,
330 name: oac.OriginAccessControlConfig?.Name ?? fallbackName,
331 description: oac.OriginAccessControlConfig?.Description,
332 originType:
333 oac.OriginAccessControlConfig?.OriginAccessControlOriginType ?? "s3",
334 signingBehavior: oac.OriginAccessControlConfig?.SigningBehavior ?? "always",
335 signingProtocol: oac.OriginAccessControlConfig?.SigningProtocol ?? "sigv4",
336 etag,
337});