1import * as logs from "@distilled.cloud/cloudflare/logs";
2import * as Effect from "effect/Effect";
3import * as Predicate from "effect/Predicate";
4
5import { isResolved } from "../../Diff.ts";
6import * as Provider from "../../Provider.ts";
7import { Resource } from "../../Resource.ts";
8import { CloudflareEnvironment } from "../CloudflareEnvironment.ts";
9import type { Providers } from "../Providers.ts";
10import { listAllZones } from "../Zone/lookup.ts";
11
12const TypeId = "Cloudflare.Logs.RetentionFlag" as const;
13type TypeId = typeof TypeId;
14
15export type LogsRetentionFlagProps = {
16 /**
17 * Zone whose Logpull retention flag is managed. The flag is a zone-level
18 * singleton, so the zone is the resource's identity — changing it
19 * triggers a replacement (the old zone's flag is restored to the value
20 * it had before Alchemy managed it).
21 */
22 zoneId: string;
23 /**
24 * Whether Logpull log retention is enabled for the zone. Mutable —
25 * re-posted in place.
26 */
27 flag: boolean;
28};
29
30export type LogsRetentionFlagAttributes = {
31 /** Zone the retention flag belongs to. */
32 zoneId: string;
33 /** Whether Logpull log retention is enabled. */
34 flag: boolean;
35 /**
36 * The value the flag had before Alchemy first managed it. Restored on
37 * destroy, so deleting the resource puts the zone back the way it was
38 * found.
39 */
40 initialFlag: boolean;
41};
42
43export type LogsRetentionFlag = Resource<
44 TypeId,
45 LogsRetentionFlagProps,
46 LogsRetentionFlagAttributes,
47 never,
48 Providers
49>;
50
51/**
52 * The zone-level Logpull retention flag
53 * (`/zones/{zone_id}/logs/control/retention/flag`) pinned to a desired
54 * value.
55 *
56 * The flag is a singleton that always exists on every zone, so this
57 * resource never creates or deletes anything physical. Reconcile re-posts
58 * the flag when the observed value differs from the desired one; destroy
59 * restores the value the flag had before Alchemy first managed it
60 * (captured as `initialFlag`) — there is no DELETE endpoint.
61 *
62 * Logpull is an Enterprise feature — on unentitled zones every operation
63 * fails with the typed `LogsControlNotAuthorized` error.
64 * ### Managing log retention
65 * **Example:** Enable Logpull retention on a zone
66 * ```typescript
67 * const retention = yield* Cloudflare.LogsControl.LogsRetentionFlag("Retention", {
68 * zoneId: zone.zoneId,
69 * flag: true,
70 * });
71 * ```
72 *
73 * **Example:** Explicitly disable retention
74 * ```typescript
75 * yield* Cloudflare.LogsControl.LogsRetentionFlag("Retention", {
76 * zoneId: zone.zoneId,
77 * flag: false,
78 * });
79 * ```
80 *
81 * @see https://developers.cloudflare.com/logs/logpull/enabling-log-retention/
82 *
83 * @resource
84 * @product Logs
85 * @category Observability & Analytics
86 */
87export const LogsRetentionFlag = Resource<LogsRetentionFlag>(TypeId);
88
89/**
90 * Returns true if the given value is a LogsRetentionFlag resource.
91 */
92export const isLogsRetentionFlag = (
93 value: unknown,
94): value is LogsRetentionFlag =>
95 Predicate.hasProperty(value, "Type") && value.Type === TypeId;
96
97export const LogsRetentionFlagProvider = () =>
98 Provider.succeed(LogsRetentionFlag, {
99 stables: ["zoneId", "initialFlag"],
100
101 list: Effect.fn(function* () {
102 const { accountId } = yield* yield* CloudflareEnvironment;
103 // No account-wide API for this zone singleton — enumerate every
104 // zone in the account and read its retention flag (every zone has
105 // one). The observed value at enumeration time is the zone's
106 // original, so it doubles as `initialFlag`.
107 const allZones = yield* listAllZones(accountId);
108 const rows = yield* Effect.forEach(
109 allZones.map((zone) => zone.id),
110 (zoneId) =>
111 getFlag(zoneId).pipe(
112 Effect.map((flag) =>
113 flag === undefined
114 ? undefined
115 : ({
116 zoneId,
117 flag,
118 initialFlag: flag,
119 } satisfies LogsRetentionFlagAttributes),
120 ),
121 // Logpull is Enterprise-only; unentitled zones reject with the
122 // typed error — skip them rather than fail the whole listing.
123 Effect.catchTag("LogsControlNotAuthorized", () =>
124 Effect.succeed(undefined),
125 ),
126 ),
127 { concurrency: 10 },
128 );
129 return rows.filter(
130 (row): row is LogsRetentionFlagAttributes => row !== undefined,
131 );
132 }),
133
134 diff: Effect.fn(function* ({ news, output }) {
135 if (!isResolved(news)) return undefined;
136 // The zone is the singleton's identity.
137 if (output !== undefined && output.zoneId !== news.zoneId) {
138 return { action: "replace" } as const;
139 }
140 return undefined;
141 }),
142
143 read: Effect.fn(function* ({ output, olds }) {
144 const zoneId =
145 output?.zoneId ??
146 (typeof olds?.zoneId === "string" ? olds.zoneId : undefined);
147 if (zoneId === undefined) return undefined;
148 const observed = yield* getFlag(zoneId);
149 // Zone deleted out-of-band — the flag is gone with it.
150 if (observed === undefined) return undefined;
151 // The flag is a zone singleton that always exists — there is nothing
152 // to "own", so a cold read adopts freely. The observed value at
153 // adoption time becomes the `initialFlag` restored on destroy.
154 const initialFlag = output?.initialFlag ?? observed;
155 return { zoneId, flag: observed, initialFlag };
156 }),
157
158 reconcile: Effect.fn(function* ({ news, output }) {
159 // Inputs were resolved to concrete strings by Plan.
160 const zoneId = news.zoneId as string;
161
162 // 1. Observe — the flag always exists; read its live value.
163 const observed = yield* logs
164 .getControlRetention({ zoneId })
165 .pipe(Effect.map((r) => r.flag ?? false));
166
167 // 2. Capture — the pre-management value, restored on destroy.
168 const initialFlag = output?.initialFlag ?? observed;
169
170 // 3. Sync — re-post only when the observed value differs.
171 if (observed === news.flag) {
172 return { zoneId, flag: observed, initialFlag };
173 }
174 const updated = yield* logs.createControlRetention({
175 zoneId,
176 flag: news.flag,
177 });
178 return { zoneId, flag: updated.flag ?? news.flag, initialFlag };
179 }),
180
181 delete: Effect.fn(function* ({ output }) {
182 const { zoneId, initialFlag } = output;
183 // Observe — if the zone itself is gone, so is the flag.
184 const observed = yield* getFlag(zoneId);
185 if (observed === undefined) return;
186 // Restore the pre-management value; skip the call when it already
187 // matches (idempotent re-delete after a crashed run).
188 if (observed === initialFlag) return;
189 yield* logs
190 .createControlRetention({ zoneId, flag: initialFlag })
191 .pipe(Effect.catchTag("InvalidRoute", () => Effect.void));
192 }),
193 });
194
195/**
196 * Read the retention flag, normalizing a missing zone (`InvalidRoute`,
197 * Cloudflare error code 7003) to `undefined` and a null flag to `false`.
198 */
199const getFlag = (zoneId: string) =>
200 logs.getControlRetention({ zoneId }).pipe(
201 Effect.map((r) => r.flag ?? false),
202 Effect.catchTag("InvalidRoute", () => Effect.succeed(undefined)),
203 );