1import * as ssl from "@distilled.cloud/cloudflare/ssl";
2import * as Effect from "effect/Effect";
3import * as Predicate from "effect/Predicate";
4import * as Schedule from "effect/Schedule";
5
6import { isResolved } from "../../Diff.ts";
7import * as Provider from "../../Provider.ts";
8import { Resource } from "../../Resource.ts";
9import { CloudflareEnvironment } from "../CloudflareEnvironment.ts";
10import type { Providers } from "../Providers.ts";
11import { listAllZones } from "../Zone/lookup.ts";
12
13const TypeId = "Cloudflare.Ssl.UniversalSsl" as const;
14type TypeId = typeof TypeId;
15
16export type UniversalSslProps = {
17 /**
18 * Zone whose Universal SSL setting is managed. Stable — changing the
19 * zone triggers a replacement (the old zone's setting is restored to
20 * the value it had before Alchemy managed it).
21 */
22 zoneId: string;
23 /**
24 * Whether Universal SSL certificates are issued for the zone.
25 *
26 * Disabling removes any currently active Universal SSL certificates
27 * for the zone from the edge and prevents future Universal SSL
28 * certificates from being ordered — visitors will see TLS errors
29 * unless the zone has advanced/custom certificates covering its
30 * hostnames.
31 *
32 * Mutable — patched in place.
33 */
34 enabled: boolean;
35};
36
37export type UniversalSslAttributes = {
38 /** Zone the setting belongs to. */
39 zoneId: string;
40 /** Whether Universal SSL is currently enabled for the zone. */
41 enabled: boolean;
42 /**
43 * The value the setting had before Alchemy first patched it. Restored
44 * on destroy, so deleting the resource puts the zone back the way it
45 * was found.
46 */
47 initialEnabled: boolean;
48};
49
50export type UniversalSsl = Resource<
51 TypeId,
52 UniversalSslProps,
53 UniversalSslAttributes,
54 never,
55 Providers
56>;
57
58/**
59 * The Universal SSL setting of a Cloudflare zone
60 * (`/zones/{zone_id}/ssl/universal/settings`).
61 *
62 * Universal SSL is a zone singleton — the setting always exists (Cloudflare
63 * defaults it to enabled), so this resource never creates or deletes
64 * anything physical. Reconcile patches the setting when the observed value
65 * differs from the desired one; destroy restores the value the setting had
66 * before Alchemy first managed it (captured as `initialEnabled`).
67 *
68 * **Warning:** disabling Universal SSL removes any active Universal SSL
69 * certificates for the zone from the edge. Visitors will see TLS errors
70 * unless advanced/custom certificates cover the zone's hostnames.
71 * ### Managing Universal SSL
72 * **Example:** Disable Universal SSL for a zone
73 * ```typescript
74 * yield* Cloudflare.Ssl.UniversalSsl("UniversalSsl", {
75 * zoneId: zone.zoneId,
76 * enabled: false,
77 * });
78 * ```
79 *
80 * **Example:** Pin Universal SSL enabled
81 * ```typescript
82 * yield* Cloudflare.Ssl.UniversalSsl("UniversalSsl", {
83 * zoneId: zone.zoneId,
84 * enabled: true,
85 * });
86 * ```
87 *
88 * @see https://developers.cloudflare.com/api/resources/ssl/subresources/universal/subresources/settings/
89 *
90 * @resource
91 * @product SSL/TLS
92 * @category SSL/TLS & Certificates
93 */
94export const UniversalSsl = Resource<UniversalSsl>(TypeId);
95
96/**
97 * Returns true if the given value is a UniversalSsl resource.
98 */
99export const isUniversalSsl = (value: unknown): value is UniversalSsl =>
100 Predicate.hasProperty(value, "Type") && value.Type === TypeId;
101
102/**
103 * The Universal SSL "edit setting" endpoint enforces its OWN per-zone,
104 * per-operation rate limit ("Rate limit reached for the update operation.
105 * Please try again in a minute") — much longer than the engine's blanket
106 * retry budget (~40s). Ride it out patiently on the typed `TooManyRequests`
107 * within a bounded number of attempts (~120s total) so a concurrent or
108 * recent patch on the same zone's singleton doesn't fail the operation.
109 */
110const patchUniversal = (input: { zoneId: string; enabled: boolean }) =>
111 ssl.patchUniversalSetting(input).pipe(
112 Effect.retry({
113 while: (e) => e._tag === "TooManyRequests",
114 schedule: Schedule.spaced("20 seconds"),
115 times: 6,
116 }),
117 );
118
119export const UniversalSslProvider = () =>
120 Provider.succeed(UniversalSsl, {
121 nuke: { singleton: true },
122 stables: ["zoneId", "initialEnabled"],
123
124 list: Effect.fn(function* () {
125 const { accountId } = yield* yield* CloudflareEnvironment;
126 // No account-wide API for this zone singleton — enumerate every
127 // zone in the account and read its setting (every zone has one).
128 const allZones = yield* listAllZones(accountId);
129 const rows = yield* Effect.forEach(
130 allZones.map((zone) => zone.id),
131 (zoneId) =>
132 ssl.getUniversalSetting({ zoneId }).pipe(
133 Effect.map((observed) => {
134 const enabled = observedEnabled(observed);
135 // Observed value is the pre-management value at adoption.
136 return { zoneId, enabled, initialEnabled: enabled };
137 }),
138 // Plan-gated or partial zones reject the route; skip them.
139 Effect.catchTag("InvalidRoute", () => Effect.succeed(undefined)),
140 ),
141 { concurrency: 10 },
142 );
143 return rows.filter(
144 (row): row is UniversalSslAttributes => row !== undefined,
145 );
146 }),
147
148 diff: Effect.fn(function* ({ olds, news, output }) {
149 // news is Input<Props> during plan — only compare once resolved.
150 if (!isResolved(news)) return undefined;
151 // zoneId is the resource's identity; it is Input<string>, so
152 // compare only once both sides are concrete.
153 const oldZoneId =
154 output?.zoneId ??
155 (typeof olds?.zoneId === "string" ? olds.zoneId : undefined);
156 if (
157 oldZoneId !== undefined &&
158 typeof news.zoneId === "string" &&
159 oldZoneId !== news.zoneId
160 ) {
161 return { action: "replace" } as const;
162 }
163 return undefined;
164 }),
165
166 read: Effect.fn(function* ({ output, olds }) {
167 const zoneId = output?.zoneId ?? (olds?.zoneId as string | undefined);
168 if (!zoneId) return undefined;
169 const observed = yield* ssl.getUniversalSetting({ zoneId }).pipe(
170 // Zone deleted out-of-band — the setting is gone with it.
171 Effect.catchTag("InvalidRoute", () => Effect.succeed(undefined)),
172 );
173 if (observed === undefined) return undefined;
174 const enabled = observedEnabled(observed);
175 // Universal SSL is a singleton that always exists with a
176 // Cloudflare default — there is nothing to "own", so a cold read
177 // adopts freely (never `Unowned`). The observed value at adoption
178 // time becomes the `initialEnabled` restored on destroy.
179 const initialEnabled =
180 output !== undefined ? output.initialEnabled : enabled;
181 return { zoneId, enabled, initialEnabled };
182 }),
183
184 reconcile: Effect.fn(function* ({ news, output }) {
185 // Inputs have been resolved to concrete strings by Plan.
186 const zoneId = news.zoneId as string;
187
188 // 1. Observe — the setting always exists; read its live value.
189 const observed = yield* ssl.getUniversalSetting({ zoneId });
190 const enabled = observedEnabled(observed);
191
192 // 2. Capture — the pre-management value, restored on destroy.
193 // `output` (including an adoption read) already carries it;
194 // otherwise this is our first touch and the observed value is
195 // the zone's original.
196 const initialEnabled =
197 output !== undefined ? output.initialEnabled : enabled;
198
199 // 3. Sync — patch only when the observed value differs.
200 if (enabled === news.enabled) {
201 return { zoneId, enabled, initialEnabled };
202 }
203 const patched = yield* patchUniversal({
204 zoneId,
205 enabled: news.enabled,
206 });
207 return { zoneId, enabled: observedEnabled(patched), initialEnabled };
208 }),
209
210 delete: Effect.fn(function* ({ output }) {
211 const { zoneId, initialEnabled } = output;
212 // Observe — if the zone itself is gone, so is the setting.
213 const observed = yield* ssl
214 .getUniversalSetting({ zoneId })
215 .pipe(Effect.catchTag("InvalidRoute", () => Effect.succeed(undefined)));
216 if (observed === undefined) return;
217 // Restore the pre-management value; skip the call when it already
218 // matches (idempotent re-delete after a crashed run).
219 if (observedEnabled(observed) === initialEnabled) return;
220 yield* patchUniversal({ zoneId, enabled: initialEnabled }).pipe(
221 Effect.catchTag("InvalidRoute", () => Effect.void),
222 );
223 }),
224 });
225
226/**
227 * Normalize the distilled response's `enabled?: boolean | null` to a
228 * concrete boolean — Cloudflare's default for Universal SSL is enabled.
229 */
230const observedEnabled = (
231 setting: ssl.GetUniversalSettingResponse | ssl.PatchUniversalSettingResponse,
232): boolean => setting.enabled ?? true;