1import * as cloudfront from "@distilled.cloud/aws/cloudfront";
2import * as Effect from "effect/Effect";
3import { isResolved } from "../../Diff.ts";
4import { createPhysicalName } from "../../PhysicalName.ts";
5import * as Provider from "../../Provider.ts";
6import { Resource } from "../../Resource.ts";
7import type { Providers } from "../Providers.ts";
8
9export interface OriginRequestPolicyProps {
10 /**
11 * Name of the origin request policy. If omitted, a deterministic name is
12 * generated. Names must be unique per AWS account. Changing the name
13 * triggers a replacement.
14 */
15 name?: string;
16 /**
17 * Optional comment describing the policy.
18 */
19 comment?: string;
20 /**
21 * Headers to forward to the origin (in addition to the cache key).
22 */
23 headersConfig: cloudfront.OriginRequestPolicyHeadersConfig;
24 /**
25 * Cookies to forward to the origin (in addition to the cache key).
26 */
27 cookiesConfig: cloudfront.OriginRequestPolicyCookiesConfig;
28 /**
29 * Query strings to forward to the origin (in addition to the cache key).
30 */
31 queryStringsConfig: cloudfront.OriginRequestPolicyQueryStringsConfig;
32}
33
34export interface OriginRequestPolicy extends Resource<
35 "AWS.CloudFront.OriginRequestPolicy",
36 OriginRequestPolicyProps,
37 {
38 /**
39 * CloudFront-assigned origin request policy identifier.
40 */
41 originRequestPolicyId: string;
42 /**
43 * Name of the origin request policy.
44 */
45 name: string;
46 /**
47 * Most recent entity tag for update/delete operations.
48 */
49 etag: string | undefined;
50 /**
51 * Current comment on the policy.
52 */
53 comment: string | undefined;
54 /**
55 * Current headers configuration.
56 */
57 headersConfig: cloudfront.OriginRequestPolicyHeadersConfig;
58 /**
59 * Current cookies configuration.
60 */
61 cookiesConfig: cloudfront.OriginRequestPolicyCookiesConfig;
62 /**
63 * Current query strings configuration.
64 */
65 queryStringsConfig: cloudfront.OriginRequestPolicyQueryStringsConfig;
66 },
67 never,
68 Providers
69> {}
70
71/**
72 * A CloudFront origin request policy.
73 *
74 * Origin request policies control which values from the viewer request (in
75 * addition to those used in the cache key) CloudFront includes when sending
76 * a request to the origin. They are referenced by ID on a Distribution's
77 * default behavior or per-path cache behaviors.
78 * ### Creating Origin Request Policies
79 * **Example:** Forward all viewer headers and cookies
80 * ```typescript
81 * const originRequestPolicy = yield* OriginRequestPolicy("AppOriginRequest", {
82 * comment: "Forward auth + locale",
83 * headersConfig: {
84 * HeaderBehavior: "whitelist",
85 * Headers: { Quantity: 2, Items: ["Authorization", "Accept-Language"] },
86 * },
87 * cookiesConfig: { CookieBehavior: "all" },
88 * queryStringsConfig: { QueryStringBehavior: "all" },
89 * });
90 * ```
91 *
92 * @resource
93 */
94export const OriginRequestPolicy = Resource<OriginRequestPolicy>(
95 "AWS.CloudFront.OriginRequestPolicy",
96);
97
98export const OriginRequestPolicyProvider = () =>
99 Provider.effect(
100 OriginRequestPolicy,
101 Effect.gen(function* () {
102 const getById = Effect.fn(function* (id: string) {
103 const config = yield* cloudfront
104 .getOriginRequestPolicyConfig({ Id: id })
105 .pipe(
106 Effect.catchTag("NoSuchOriginRequestPolicy", () =>
107 Effect.succeed(undefined),
108 ),
109 );
110 if (!config?.OriginRequestPolicyConfig) return undefined;
111 return { config: config.OriginRequestPolicyConfig, etag: config.ETag };
112 });
113
114 const getByName = Effect.fn(function* (name: string) {
115 const listed = yield* cloudfront.listOriginRequestPolicies({
116 Type: "custom",
117 });
118 const summary = listed.OriginRequestPolicyList?.Items?.find(
119 (item) =>
120 item.OriginRequestPolicy?.OriginRequestPolicyConfig?.Name === name,
121 );
122 if (!summary?.OriginRequestPolicy?.Id) return undefined;
123 return yield* getById(summary.OriginRequestPolicy.Id).pipe(
124 Effect.map((found) =>
125 found
126 ? { id: summary.OriginRequestPolicy.Id, ...found }
127 : undefined,
128 ),
129 );
130 });
131
132 const buildConfig = (
133 name: string,
134 props: OriginRequestPolicyProps,
135 ): cloudfront.OriginRequestPolicyConfig => ({
136 Name: name,
137 Comment: props.comment,
138 HeadersConfig: props.headersConfig,
139 CookiesConfig: props.cookiesConfig,
140 QueryStringsConfig: props.queryStringsConfig,
141 });
142
143 const toAttrs = (
144 id: string,
145 config: cloudfront.OriginRequestPolicyConfig,
146 etag: string | undefined,
147 ) => ({
148 originRequestPolicyId: id,
149 name: config.Name,
150 etag,
151 comment: config.Comment,
152 headersConfig: config.HeadersConfig,
153 cookiesConfig: config.CookiesConfig,
154 queryStringsConfig: config.QueryStringsConfig,
155 });
156
157 return {
158 stables: ["originRequestPolicyId"],
159 diff: Effect.fn(function* ({ id, news, olds }) {
160 if (!isResolved(news)) return undefined;
161 if (
162 (yield* createName(id, olds ?? {})) !==
163 (yield* createName(id, news))
164 ) {
165 return { action: "replace" } as const;
166 }
167 }),
168 read: Effect.fn(function* ({ id, olds, output }) {
169 if (output?.originRequestPolicyId) {
170 const found = yield* getById(output.originRequestPolicyId);
171 if (found) {
172 return toAttrs(
173 output.originRequestPolicyId,
174 found.config,
175 found.etag,
176 );
177 }
178 }
179 const name = yield* createName(id, olds ?? {});
180 const found = yield* getByName(name);
181 if (!found) return undefined;
182 return toAttrs(found.id, found.config, found.etag);
183 }),
184 // CloudFront is global (no region). `listOriginRequestPolicies`
185 // returns both AWS-managed and custom policies; we filter to
186 // `Type: "custom"` since those are the only ones we create/delete.
187 // The op is marker-paginated (no `.pages`), so we loop until
188 // `NextMarker` is exhausted and hydrate each summary's ETag via
189 // `getById` so every row matches read().
190 list: () =>
191 Effect.gen(function* () {
192 const items: ReturnType<typeof toAttrs>[] = [];
193 let marker: string | undefined = undefined;
194 do {
195 const listed: cloudfront.ListOriginRequestPoliciesResult =
196 yield* cloudfront.listOriginRequestPolicies({
197 Type: "custom",
198 Marker: marker,
199 });
200 for (const summary of listed.OriginRequestPolicyList?.Items ??
201 []) {
202 if (summary.Type !== "custom") continue;
203 const id = summary.OriginRequestPolicy?.Id;
204 const config =
205 summary.OriginRequestPolicy?.OriginRequestPolicyConfig;
206 if (!id || !config) continue;
207 const found = yield* getById(id);
208 items.push(toAttrs(id, found?.config ?? config, found?.etag));
209 }
210 marker = listed.OriginRequestPolicyList?.NextMarker;
211 } while (marker);
212 return items;
213 }),
214 reconcile: Effect.fn(function* ({ id, news, output, session }) {
215 const name = yield* createName(id, news);
216
217 // Observe — locate the policy by id (cached on `output`) or
218 // by name. Trust observed cloud state, not stale `olds`.
219 let observed = output?.originRequestPolicyId
220 ? yield* getById(output.originRequestPolicyId).pipe(
221 Effect.map((found) =>
222 found
223 ? { id: output.originRequestPolicyId, ...found }
224 : undefined,
225 ),
226 )
227 : undefined;
228 if (!observed) {
229 observed = yield* getByName(name);
230 }
231
232 // Ensure — create the policy if it's missing. Tolerate
233 // `OriginRequestPolicyAlreadyExists` (race with a peer
234 // reconciler).
235 if (!observed) {
236 const created = yield* cloudfront
237 .createOriginRequestPolicy({
238 OriginRequestPolicyConfig: buildConfig(name, news),
239 })
240 .pipe(
241 Effect.catchTag("OriginRequestPolicyAlreadyExists", () =>
242 getByName(name).pipe(
243 Effect.flatMap((existing) =>
244 existing
245 ? Effect.succeed({
246 OriginRequestPolicy: {
247 Id: existing.id,
248 LastModifiedTime: new Date(),
249 OriginRequestPolicyConfig: existing.config,
250 },
251 ETag: existing.etag,
252 Location: undefined,
253 })
254 : Effect.fail(
255 new Error(
256 `Origin request policy '${name}' already exists but could not be recovered`,
257 ),
258 ),
259 ),
260 ),
261 ),
262 );
263 if (!created.OriginRequestPolicy?.Id) {
264 return yield* Effect.fail(
265 new Error("createOriginRequestPolicy returned no identifier"),
266 );
267 }
268 yield* session.note(created.OriginRequestPolicy.Id);
269 return toAttrs(
270 created.OriginRequestPolicy.Id,
271 created.OriginRequestPolicy.OriginRequestPolicyConfig,
272 created.ETag,
273 );
274 }
275
276 // Sync — patch the observed config to the desired state. The
277 // freshly observed ETag handles optimistic concurrency.
278 const updated = yield* cloudfront.updateOriginRequestPolicy({
279 Id: observed.id,
280 IfMatch: observed.etag,
281 OriginRequestPolicyConfig: buildConfig(observed.config.Name, news),
282 });
283 if (!updated.OriginRequestPolicy?.Id) {
284 return yield* Effect.fail(
285 new Error("updateOriginRequestPolicy returned no identifier"),
286 );
287 }
288 yield* session.note(observed.id);
289 return toAttrs(
290 updated.OriginRequestPolicy.Id,
291 updated.OriginRequestPolicy.OriginRequestPolicyConfig,
292 updated.ETag,
293 );
294 }),
295 delete: Effect.fn(function* ({ output }) {
296 const current = yield* getById(output.originRequestPolicyId);
297 if (!current) return;
298 yield* cloudfront
299 .deleteOriginRequestPolicy({
300 Id: output.originRequestPolicyId,
301 IfMatch: current.etag,
302 })
303 .pipe(
304 Effect.catchTag("NoSuchOriginRequestPolicy", () => Effect.void),
305 );
306 }),
307 };
308 }),
309 );
310
311const createName = (id: string, props: OriginRequestPolicyProps) =>
312 props.name
313 ? Effect.succeed(props.name)
314 : createPhysicalName({ id, maxLength: 128, lowercase: true });