1import * as lcc from "@distilled.cloud/cloudflare/leaked-credential-checks";
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 =
12 "Cloudflare.LeakedCredentialCheck.LeakedCredentialCheck" as const;
13type TypeId = typeof TypeId;
14
15export interface Props {
16 /**
17 * Zone whose Leaked Credential Checks setting is managed. Stable —
18 * changing the zone triggers a replacement (the old zone's setting is
19 * restored to the value it had before Alchemy managed it).
20 */
21 zoneId: string;
22 /**
23 * Whether Leaked Credential Checks are enabled on the zone. Mutable —
24 * set in place via the API's POST upsert.
25 * @default true
26 */
27 enabled?: boolean;
28}
29
30export interface Attributes {
31 /** Zone the setting belongs to. */
32 zoneId: string;
33 /** Whether Leaked Credential Checks are currently enabled. */
34 enabled: boolean;
35 /**
36 * The value the setting had before Alchemy first managed it. Restored
37 * on destroy, so deleting the resource puts the zone back the way it
38 * was found.
39 */
40 initialEnabled: boolean;
41}
42
43export type LeakedCredentialCheck = Resource<
44 TypeId,
45 Props,
46 Attributes,
47 never,
48 Providers
49>;
50
51/**
52 * The Leaked Credential Checks setting of a Cloudflare zone
53 * (`/zones/{zone_id}/leaked-credential-checks`).
54 *
55 * Leaked credential detection scans incoming requests for authentication
56 * credentials previously seen in known breach compilations, populating the
57 * `cf.waf.credential_check.*` ruleset fields that WAF rules can act on
58 * (e.g. force a password reset on a leaked-credential login). The check is
59 * a zone **singleton** — it always exists (default `enabled: false`), so
60 * this resource never creates or deletes anything physical. Reconcile sets
61 * the flag when the observed value differs from the desired one; destroy
62 * restores the value the setting had before Alchemy first managed it
63 * (captured as `initialEnabled`).
64 *
65 * Leaked-credential detection is available on all plans. Custom detection
66 * locations (see {@link LeakedCredentialDetection}) are plan-gated
67 * separately.
68 *
69 * Only one `LeakedCredentialCheck` resource per zone makes sense — two
70 * instances managing the same zone would fight over the singleton.
71 * ### Managing the check
72 * **Example:** Enable Leaked Credential Checks on a zone
73 * ```typescript
74 * const zone = yield* Cloudflare.Zone.Zone("Site", { name: "example.com" });
75 *
76 * yield* Cloudflare.LeakedCredentialCheck.LeakedCredentialCheck("Lcc", {
77 * zoneId: zone.zoneId,
78 * });
79 * ```
80 *
81 * **Example:** Explicitly pin the check off
82 * ```typescript
83 * yield* Cloudflare.LeakedCredentialCheck.LeakedCredentialCheck("Lcc", {
84 * zoneId: zone.zoneId,
85 * enabled: false,
86 * });
87 * ```
88 *
89 * @see https://developers.cloudflare.com/waf/detections/leaked-credentials/
90 *
91 * @resource
92 * @product Leaked Credential Checks
93 * @category Application Security
94 */
95export const LeakedCredentialCheck = Resource<LeakedCredentialCheck>(TypeId, {
96 aliases: ["Cloudflare.LeakedCredentialCheck"],
97});
98
99/**
100 * Returns true if the given value is a LeakedCredentialCheck resource.
101 */
102export const isLeakedCredentialCheck = (
103 value: unknown,
104): value is LeakedCredentialCheck =>
105 Predicate.hasProperty(value, "Type") && value.Type === TypeId;
106
107const desiredEnabled = (props: Props): boolean => props.enabled ?? true;
108
109export const LeakedCredentialCheckProvider = () =>
110 Provider.succeed(LeakedCredentialCheck, {
111 nuke: { singleton: true },
112 stables: ["zoneId", "initialEnabled"],
113
114 list: Effect.fn(function* () {
115 const { accountId } = yield* yield* CloudflareEnvironment;
116 // No account-wide API for this zone singleton — enumerate every
117 // zone in the account and read its setting (every zone has one).
118 const allZones = yield* listAllZones(accountId);
119 const rows = yield* Effect.forEach(
120 allZones.map((zone) => zone.id),
121 (zoneId) =>
122 lcc.getLeakedCredentialCheck({ zoneId }).pipe(
123 Effect.map((observed) => {
124 const enabled = observed.enabled ?? false;
125 // The singleton always exists; with no managed history its
126 // observed value is also its pre-management value.
127 return {
128 zoneId,
129 enabled,
130 initialEnabled: enabled,
131 } satisfies Attributes;
132 }),
133 // Zone deleted out-of-band or plan-gated route; skip it.
134 Effect.catchTag("InvalidRoute", () => Effect.succeed(undefined)),
135 ),
136 { concurrency: 10 },
137 );
138 return rows.filter((row): row is Attributes => row !== undefined);
139 }),
140
141 diff: Effect.fn(function* ({ olds = {}, news, output }) {
142 const o = olds as Props;
143 const n = news as Props;
144 // zoneId is Input<string>; compare only once both sides are concrete.
145 const oldZoneId =
146 output?.zoneId ?? (typeof o.zoneId === "string" ? o.zoneId : undefined);
147 if (
148 oldZoneId !== undefined &&
149 typeof n.zoneId === "string" &&
150 oldZoneId !== n.zoneId
151 ) {
152 return { action: "replace" } as const;
153 }
154 return undefined;
155 }),
156
157 read: Effect.fn(function* ({ output, olds }) {
158 const zoneId = output?.zoneId ?? (olds?.zoneId as string | undefined);
159 if (!zoneId) return undefined;
160 const observed = yield* lcc.getLeakedCredentialCheck({ zoneId });
161 // The setting is a singleton that always exists with a Cloudflare
162 // default — there is nothing to "own", so a cold read adopts
163 // freely (never `Unowned`). The observed value at adoption time
164 // becomes the `initialEnabled` restored on destroy.
165 const enabled = observed.enabled ?? false;
166 const initialEnabled =
167 output !== undefined ? output.initialEnabled : enabled;
168 return { zoneId, enabled, initialEnabled };
169 }),
170
171 reconcile: Effect.fn(function* ({ news, output }) {
172 // Inputs have been resolved to concrete strings by Plan.
173 const zoneId = news.zoneId as string;
174
175 // 1. Observe — the setting always exists; read its live value.
176 const observed = yield* lcc.getLeakedCredentialCheck({ zoneId });
177 const observedEnabled = observed.enabled ?? false;
178
179 // 2. Capture — the pre-management value, restored on destroy.
180 // `output` (including an adoption read) already carries it;
181 // otherwise this is our first touch and the observed value is
182 // the zone's original.
183 const initialEnabled =
184 output !== undefined ? output.initialEnabled : observedEnabled;
185
186 // 3. Sync — POST (the API's set/upsert) only when it differs.
187 const desired = desiredEnabled(news);
188 if (observedEnabled === desired) {
189 return { zoneId, enabled: observedEnabled, initialEnabled };
190 }
191 const set = yield* lcc.createLeakedCredentialCheck({
192 zoneId,
193 enabled: desired,
194 });
195 return { zoneId, enabled: set.enabled ?? desired, initialEnabled };
196 }),
197
198 delete: Effect.fn(function* ({ output }) {
199 const { zoneId, initialEnabled } = output;
200 // Observe, then restore the pre-management value; skip the call
201 // when it already matches (idempotent re-delete after a crashed
202 // run). The singleton always exists, so nothing here can 404.
203 const observed = yield* lcc.getLeakedCredentialCheck({ zoneId });
204 if ((observed.enabled ?? false) === initialEnabled) return;
205 yield* lcc.createLeakedCredentialCheck({
206 zoneId,
207 enabled: initialEnabled,
208 });
209 }),
210 });