1import * as managedTransforms from "@distilled.cloud/cloudflare/managed-transforms";
2import * as Effect from "effect/Effect";
3import * as Predicate from "effect/Predicate";
4
5import * as Provider from "../../Provider.ts";
6import { Resource } from "../../Resource.ts";
7import { CloudflareEnvironment } from "../CloudflareEnvironment.ts";
8import type { Providers } from "../Providers.ts";
9import { listAllZones } from "../Zone/lookup.ts";
10
11const TypeId = "Cloudflare.ManagedTransforms.ManagedTransforms" as const;
12type TypeId = typeof TypeId;
13
14/**
15 * Identifier of a Cloudflare managed request transform — the built-in
16 * request-header transforms Cloudflare offers on
17 * `/zones/{zone_id}/managed_headers`. The open `(string & {})` tail keeps
18 * the type forward-compatible with transforms Cloudflare adds later.
19 */
20export type ManagedRequestTransformId =
21 | "add_bot_protection_headers"
22 | "add_client_certificate_headers"
23 | "add_true_client_ip_headers"
24 | "add_visitor_location_headers"
25 | "add_waf_credential_check_status_header"
26 | "remove_visitor_ip_headers"
27 | (string & {});
28
29/**
30 * Identifier of a Cloudflare managed response transform — the built-in
31 * response-header transforms Cloudflare offers on
32 * `/zones/{zone_id}/managed_headers`. The open `(string & {})` tail keeps
33 * the type forward-compatible with transforms Cloudflare adds later.
34 */
35export type ManagedResponseTransformId =
36 | "add_security_headers"
37 | "remove_x-powered-by_header"
38 | (string & {});
39
40export interface Props {
41 /**
42 * Zone whose managed transforms are managed. Stable — changing the zone
43 * triggers a replacement (the old zone's managed transforms are restored
44 * to the enabled states they had before Alchemy managed them).
45 */
46 zoneId: string;
47 /**
48 * Desired enabled state per managed **request** transform id (e.g.
49 * `{ add_visitor_location_headers: true }`). Only the ids you name here
50 * are managed — every other transform on the zone is left untouched.
51 *
52 * Mutable — patched in place.
53 *
54 * @default {}
55 */
56 requestHeaders?: Partial<Record<ManagedRequestTransformId, boolean>>;
57 /**
58 * Desired enabled state per managed **response** transform id (e.g.
59 * `{ "remove_x-powered-by_header": true }`). Only the ids you name here
60 * are managed — every other transform on the zone is left untouched.
61 *
62 * Mutable — patched in place.
63 *
64 * @default {}
65 */
66 responseHeaders?: Partial<Record<ManagedResponseTransformId, boolean>>;
67}
68
69/**
70 * Observed state of a single managed transform on the zone.
71 */
72export interface ManagedTransformState {
73 /** The transform's identifier (e.g. `add_visitor_location_headers`). */
74 id: string;
75 /** Whether the transform is currently enabled on the zone. */
76 enabled: boolean;
77 /** Whether the transform conflicts with another enabled feature. */
78 hasConflict: boolean;
79 /** Ids of the transforms this one conflicts with, when Cloudflare reports them. */
80 conflictsWith: string[] | undefined;
81}
82
83export interface Attributes {
84 /** Zone that owns these managed transforms. */
85 zoneId: string;
86 /** Observed state of every managed request transform on the zone. */
87 requestHeaders: ManagedTransformState[];
88 /** Observed state of every managed response transform on the zone. */
89 responseHeaders: ManagedTransformState[];
90 /**
91 * Snapshot of every request transform's enabled state observed **before**
92 * this resource first wrote to the zone. `delete` restores the ids this
93 * resource managed to these values.
94 */
95 initialRequestHeaders: Record<string, boolean>;
96 /**
97 * Snapshot of every response transform's enabled state observed
98 * **before** this resource first wrote to the zone. `delete` restores the
99 * ids this resource managed to these values.
100 */
101 initialResponseHeaders: Record<string, boolean>;
102}
103
104export type ManagedTransforms = Resource<
105 TypeId,
106 Props,
107 Attributes,
108 never,
109 Providers
110>;
111
112/**
113 * The managed request/response header transforms of a Cloudflare zone
114 * (`/zones/{zone_id}/managed_headers`) — a zone-scoped **singleton**: every
115 * zone always carries the full catalog of managed transforms (each with an
116 * enabled flag), so there is no create or delete on the Cloudflare side.
117 *
118 * Reconciling this resource adopts the singleton and patches **only the
119 * transform ids you name** in `requestHeaders` / `responseHeaders` — every
120 * other transform is left exactly as found (dashboard- or otherwise-managed
121 * toggles are never clobbered).
122 *
123 * On destroy, the resource restores the ids it managed to the enabled
124 * states observed before its first write (the `initialRequestHeaders` /
125 * `initialResponseHeaders` snapshots). Transforms that were never named are
126 * not touched.
127 *
128 * Some transforms are plan-gated (e.g. `add_bot_protection_headers`
129 * requires Bot Management) — enabling those fails server-side on
130 * unentitled zones.
131 * ### Request transforms
132 * **Example:** Add visitor location headers
133 * ```typescript
134 * yield* Cloudflare.ManagedTransforms.ManagedTransforms("Transforms", {
135 * zoneId: zone.zoneId,
136 * requestHeaders: { add_visitor_location_headers: true },
137 * });
138 * ```
139 *
140 * **Example:** Remove visitor IP headers
141 * ```typescript
142 * yield* Cloudflare.ManagedTransforms.ManagedTransforms("Transforms", {
143 * zoneId: zone.zoneId,
144 * requestHeaders: { remove_visitor_ip_headers: true },
145 * });
146 * ```
147 *
148 * ### Response transforms
149 * **Example:** Harden responses
150 * ```typescript
151 * yield* Cloudflare.ManagedTransforms.ManagedTransforms("Transforms", {
152 * zoneId: zone.zoneId,
153 * responseHeaders: {
154 * add_security_headers: true,
155 * "remove_x-powered-by_header": true,
156 * },
157 * });
158 * ```
159 *
160 * ### Mixed
161 * **Example:** Manage request and response transforms together
162 * ```typescript
163 * yield* Cloudflare.ManagedTransforms.ManagedTransforms("Transforms", {
164 * zoneId: zone.zoneId,
165 * requestHeaders: { add_true_client_ip_headers: true },
166 * responseHeaders: { "remove_x-powered-by_header": false },
167 * });
168 * ```
169 *
170 * @see https://developers.cloudflare.com/rules/transform/managed-transforms/
171 *
172 * @resource
173 * @product Managed Transforms
174 * @category Rules & Configuration
175 */
176export const ManagedTransforms = Resource<ManagedTransforms>(TypeId, {
177 aliases: ["Cloudflare.ManagedTransforms"],
178});
179
180/**
181 * Returns true if the given value is a ManagedTransforms resource.
182 */
183export const isManagedTransforms = (
184 value: unknown,
185): value is ManagedTransforms =>
186 Predicate.hasProperty(value, "Type") && value.Type === TypeId;
187
188export const ManagedTransformsProvider = () =>
189 Provider.succeed(ManagedTransforms, {
190 nuke: { singleton: true },
191 stables: ["zoneId", "initialRequestHeaders", "initialResponseHeaders"],
192
193 list: Effect.fn(function* () {
194 const { accountId } = yield* yield* CloudflareEnvironment;
195 // The managed-transforms catalog is a per-zone singleton with no
196 // account-wide list — enumerate every zone and read its catalog.
197 const allZones = yield* listAllZones(accountId);
198 const rows = yield* Effect.forEach(
199 allZones.map((zone) => zone.id),
200 (zoneId) =>
201 observe(zoneId).pipe(
202 Effect.map((observed) =>
203 observed === undefined
204 ? undefined
205 : // Cold enumeration adopts the singleton freely — the
206 // observed enabled states become the snapshot baseline,
207 // matching the `read` cold-adopt path.
208 toAttributes(
209 zoneId,
210 observed,
211 snapshot(observed.managedRequestHeaders),
212 snapshot(observed.managedResponseHeaders),
213 ),
214 ),
215 ),
216 { concurrency: 10 },
217 );
218 return rows.filter((row): row is Attributes => row !== undefined);
219 }),
220
221 diff: Effect.fn(function* ({ olds = {}, news, output }) {
222 const o = olds as Props;
223 const n = news as Props;
224 // zoneId is Input<string>; compare only when both sides are concrete.
225 const oldZone = output?.zoneId ?? o.zoneId;
226 if (
227 typeof oldZone === "string" &&
228 typeof n.zoneId === "string" &&
229 oldZone !== n.zoneId
230 ) {
231 return { action: "replace" } as const;
232 }
233 }),
234
235 read: Effect.fn(function* ({ output, olds }) {
236 // The transform catalog is a singleton — it exists iff the zone does.
237 const zoneId = output?.zoneId ?? (olds?.zoneId as string | undefined);
238 if (!zoneId) return undefined;
239 const observed = yield* observe(zoneId);
240 if (!observed) return undefined;
241 // Singletons always exist with Cloudflare defaults — there is nothing
242 // to "own", so a cold read adopts freely (never `Unowned`). The
243 // enabled states observed at adoption time become the snapshot
244 // restored on destroy.
245 return toAttributes(
246 zoneId,
247 observed,
248 output?.initialRequestHeaders ??
249 snapshot(observed.managedRequestHeaders),
250 output?.initialResponseHeaders ??
251 snapshot(observed.managedResponseHeaders),
252 );
253 }),
254
255 reconcile: Effect.fn(function* ({ news, output }) {
256 // Inputs are resolved to concrete strings by Plan.
257 const zoneId = news.zoneId as string;
258
259 // 1. Observe — the singleton always exists for a live zone.
260 let observed = normalize(
261 yield* managedTransforms.listManagedTransforms({ zoneId }),
262 );
263
264 // 2. Snapshot — capture pre-management enabled states once; `output`
265 // acts as the cache that keeps the very first observation sticky.
266 const initialRequestHeaders =
267 output?.initialRequestHeaders ??
268 snapshot(observed.managedRequestHeaders);
269 const initialResponseHeaders =
270 output?.initialResponseHeaders ??
271 snapshot(observed.managedResponseHeaders);
272
273 // 3. Sync — diff the managed ids' desired enabled flags against the
274 // observed states and PATCH only the deltas. Unnamed transforms
275 // are never sent, so they stay exactly as found.
276 const requestDelta = delta(
277 news.requestHeaders ?? {},
278 observed.managedRequestHeaders,
279 );
280 const responseDelta = delta(
281 news.responseHeaders ?? {},
282 observed.managedResponseHeaders,
283 );
284 if (requestDelta.length > 0 || responseDelta.length > 0) {
285 observed = normalize(
286 yield* managedTransforms.patchManagedTransform({
287 zoneId,
288 managedRequestHeaders: requestDelta,
289 managedResponseHeaders: responseDelta,
290 }),
291 );
292 }
293
294 // 4. Return fresh attributes.
295 return toAttributes(
296 zoneId,
297 observed,
298 initialRequestHeaders,
299 initialResponseHeaders,
300 );
301 }),
302
303 delete: Effect.fn(function* ({ output, olds }) {
304 // Singleton — nothing to delete on the Cloudflare side. Restore the
305 // ids this resource managed (i.e. the ones named in the last-applied
306 // props) to their pre-management snapshot values. Ids missing from
307 // the snapshot (added by Cloudflare after adoption) are left as-is.
308 const observed = yield* observe(output.zoneId);
309 if (!observed) return; // zone is gone — nothing to restore
310 const o = (olds ?? {}) as Props;
311 const requestRestore = restoreDelta(
312 o.requestHeaders ?? {},
313 output.initialRequestHeaders ?? {},
314 observed.managedRequestHeaders,
315 );
316 const responseRestore = restoreDelta(
317 o.responseHeaders ?? {},
318 output.initialResponseHeaders ?? {},
319 observed.managedResponseHeaders,
320 );
321 if (requestRestore.length > 0 || responseRestore.length > 0) {
322 yield* managedTransforms
323 .patchManagedTransform({
324 zoneId: output.zoneId,
325 managedRequestHeaders: requestRestore,
326 managedResponseHeaders: responseRestore,
327 })
328 .pipe(Effect.catchTag("InvalidRoute", () => Effect.void));
329 }
330 }),
331 });
332
333interface ObservedTransform {
334 id: string;
335 enabled: boolean;
336 hasConflict: boolean;
337 conflictsWith?: string[] | null;
338}
339
340interface ObservedTransforms {
341 managedRequestHeaders: readonly ObservedTransform[];
342 managedResponseHeaders: readonly ObservedTransform[];
343}
344
345/**
346 * Cloudflare returns `null` (not `[]`) for a transform list when the zone's
347 * plan offers no transforms of that kind (e.g. all managed request
348 * transforms are plan-gated on free zones). Normalize to empty arrays so the
349 * rest of the provider only deals in lists.
350 */
351const normalize = (
352 response:
353 | managedTransforms.ListManagedTransformsResponse
354 | managedTransforms.PatchManagedTransformResponse,
355): ObservedTransforms => ({
356 managedRequestHeaders: response.managedRequestHeaders ?? [],
357 managedResponseHeaders: response.managedResponseHeaders ?? [],
358});
359
360/**
361 * Read the zone's managed transforms, mapping a dead zone (`InvalidRoute`,
362 * Cloudflare code 7003) to `undefined`.
363 */
364const observe = (zoneId: string) =>
365 managedTransforms.listManagedTransforms({ zoneId }).pipe(
366 Effect.map(normalize),
367 Effect.catchTag("InvalidRoute", () => Effect.succeed(undefined)),
368 );
369
370/**
371 * Project an observed transform list onto an id → enabled snapshot map.
372 */
373const snapshot = (
374 transforms: readonly ObservedTransform[],
375): Record<string, boolean> => {
376 const out: Record<string, boolean> = {};
377 for (const t of transforms) out[t.id] = t.enabled;
378 return out;
379};
380
381/**
382 * The PATCH entries needed to bring the managed ids to their desired
383 * enabled states — only ids whose observed state differs are included, so
384 * a no-op reconcile sends nothing.
385 */
386const delta = (
387 desired: Partial<Record<string, boolean>>,
388 observed: readonly ObservedTransform[],
389): { id: string; enabled: boolean }[] => {
390 const current = snapshot(observed);
391 const out: { id: string; enabled: boolean }[] = [];
392 for (const [id, enabled] of Object.entries(desired)) {
393 if (enabled !== undefined && current[id] !== enabled) {
394 out.push({ id, enabled });
395 }
396 }
397 return out;
398};
399
400/**
401 * The PATCH entries needed to restore the managed ids to their snapshot
402 * values — only ids that were managed, have a snapshot value, and currently
403 * differ from it are included (idempotent re-delete after a crashed run).
404 */
405const restoreDelta = (
406 managed: Partial<Record<string, boolean>>,
407 initial: Record<string, boolean>,
408 observed: readonly ObservedTransform[],
409): { id: string; enabled: boolean }[] => {
410 const current = snapshot(observed);
411 const out: { id: string; enabled: boolean }[] = [];
412 for (const id of Object.keys(managed)) {
413 if (managed[id] === undefined) continue;
414 const original = initial[id];
415 if (original !== undefined && current[id] !== original) {
416 out.push({ id, enabled: original });
417 }
418 }
419 return out;
420};
421
422const toState = (t: ObservedTransform): ManagedTransformState => ({
423 id: t.id,
424 enabled: t.enabled,
425 hasConflict: t.hasConflict,
426 conflictsWith: t.conflictsWith == null ? undefined : [...t.conflictsWith],
427});
428
429const toAttributes = (
430 zoneId: string,
431 observed: ObservedTransforms,
432 initialRequestHeaders: Record<string, boolean>,
433 initialResponseHeaders: Record<string, boolean>,
434): Attributes => ({
435 zoneId,
436 requestHeaders: observed.managedRequestHeaders.map(toState),
437 responseHeaders: observed.managedResponseHeaders.map(toState),
438 initialRequestHeaders,
439 initialResponseHeaders,
440});