1import * as addressing from "@distilled.cloud/cloudflare/addressing";
2import * as Effect from "effect/Effect";
3import * as Predicate from "effect/Predicate";
4import * as Stream from "effect/Stream";
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";
11
12const TypeId = "Cloudflare.Addressing.PrefixDelegation" as const;
13type TypeId = typeof TypeId;
14
15export interface PrefixDelegationProps {
16 /**
17 * Identifier of the parent BYOIP prefix being delegated from. Changing it
18 * forces a replacement.
19 */
20 prefixId: string;
21 /**
22 * IP Prefix in Classless Inter-Domain Routing format to delegate. Must be
23 * contained in the parent prefix. Changing it forces a replacement.
24 */
25 cidr: string;
26 /**
27 * Identifier of the Cloudflare account the prefix is delegated to.
28 * Changing it forces a replacement.
29 */
30 delegatedAccountId: string;
31}
32
33export interface PrefixDelegationAttributes {
34 /** Cloudflare-assigned identifier of the delegation. */
35 delegationId: string;
36 /** Identifier of the parent BYOIP prefix. */
37 prefixId: string;
38 /** The Cloudflare account that owns the parent prefix. */
39 accountId: string;
40 /** Delegated IP Prefix in CIDR format. */
41 cidr: string;
42 /** The account the prefix portion is delegated to. */
43 delegatedAccountId: string;
44 /** ISO8601 creation timestamp. */
45 createdAt: string | undefined;
46}
47
48export type PrefixDelegation = Resource<
49 TypeId,
50 PrefixDelegationProps,
51 PrefixDelegationAttributes,
52 never,
53 Providers
54>;
55
56/**
57 * Delegates part of a BYOIP prefix to another Cloudflare account, allowing
58 * that account to use the delegated CIDR (e.g. for its own service
59 * bindings).
60 *
61 * Delegations are create/delete only — every prop change forces a
62 * replacement.
63 * ### Delegating a Prefix
64 * **Example:** Delegate a /26 to another account
65 * ```typescript
66 * const delegation = yield* Cloudflare.Addressing.PrefixDelegation("share", {
67 * prefixId: prefix.prefixId,
68 * cidr: "192.0.2.0/26",
69 * delegatedAccountId: "023e105f4ecef8ad9ca31a8372d0c353",
70 * });
71 * ```
72 *
73 * @see https://developers.cloudflare.com/byoip/
74 *
75 * @resource
76 * @product Addressing
77 * @category Network
78 */
79export const PrefixDelegation = Resource<PrefixDelegation>(TypeId);
80
81/**
82 * Returns true if the given value is an PrefixDelegation resource.
83 */
84export const isPrefixDelegation = (value: unknown): value is PrefixDelegation =>
85 Predicate.hasProperty(value, "Type") && value.Type === TypeId;
86
87export const PrefixDelegationProvider = () =>
88 Provider.succeed(PrefixDelegation, {
89 stables: [
90 "delegationId",
91 "prefixId",
92 "accountId",
93 "cidr",
94 "delegatedAccountId",
95 "createdAt",
96 ],
97
98 diff: Effect.fn(function* ({ olds, news, output }) {
99 if (olds === undefined) return undefined;
100 if (!isResolved(news) || !isResolved(olds)) return undefined;
101 // Create/delete only — any change forces a replacement.
102 const oldPrefixId = output?.prefixId ?? olds.prefixId;
103 if (
104 typeof oldPrefixId === "string" &&
105 typeof news.prefixId === "string" &&
106 news.prefixId !== oldPrefixId
107 ) {
108 return { action: "replace" } as const;
109 }
110 if (news.cidr !== (output?.cidr ?? olds.cidr)) {
111 return { action: "replace" } as const;
112 }
113 const oldDelegated =
114 output?.delegatedAccountId ?? olds.delegatedAccountId;
115 if (
116 typeof oldDelegated === "string" &&
117 typeof news.delegatedAccountId === "string" &&
118 news.delegatedAccountId !== oldDelegated
119 ) {
120 return { action: "replace" } as const;
121 }
122 return undefined;
123 }),
124
125 read: Effect.fn(function* ({ output, olds }) {
126 const { accountId } = yield* yield* CloudflareEnvironment;
127 const acct = output?.accountId ?? accountId;
128 const prefixId =
129 output?.prefixId ??
130 (typeof olds?.prefixId === "string" ? olds.prefixId : undefined);
131 if (!prefixId) return undefined;
132
133 // There is no get-by-id op — list and filter. Cold reads match on
134 // (cidr, delegatedAccountId), which identify a delegation uniquely.
135 const delegations = yield* listDelegations(acct, prefixId);
136 const match = output?.delegationId
137 ? delegations.find((d) => d.id === output.delegationId)
138 : delegations.find(
139 (d) =>
140 d.cidr === (output?.cidr ?? olds?.cidr) &&
141 d.delegatedAccountId ===
142 (output?.delegatedAccountId ??
143 (typeof olds?.delegatedAccountId === "string"
144 ? olds.delegatedAccountId
145 : undefined)),
146 );
147 return match ? toAttributes(match, prefixId, acct) : undefined;
148 }),
149
150 reconcile: Effect.fn(function* ({ news, output }) {
151 const { accountId } = yield* yield* CloudflareEnvironment;
152 const acct = output?.accountId ?? accountId;
153 const prefixId = news.prefixId as string;
154 const delegatedAccountId = news.delegatedAccountId as string;
155
156 // 1. Observe — find an existing delegation with the same identity.
157 const delegations = yield* listDelegations(acct, prefixId);
158 const observed =
159 (output?.delegationId
160 ? delegations.find((d) => d.id === output.delegationId)
161 : undefined) ??
162 delegations.find(
163 (d) =>
164 d.cidr === news.cidr && d.delegatedAccountId === delegatedAccountId,
165 );
166 if (observed) {
167 // Nothing is mutable — converged.
168 return toAttributes(observed, prefixId, acct);
169 }
170
171 // 2. Ensure — create the delegation.
172 const created = yield* addressing.createPrefixDelegation({
173 accountId: acct,
174 prefixId,
175 cidr: news.cidr,
176 delegatedAccountId,
177 });
178 return toAttributes(created, prefixId, acct);
179 }),
180
181 delete: Effect.fn(function* ({ output }) {
182 yield* addressing
183 .deletePrefixDelegation({
184 accountId: output.accountId,
185 prefixId: output.prefixId,
186 delegationId: output.delegationId,
187 })
188 .pipe(Effect.catchTag("DelegationNotFound", () => Effect.void));
189 }),
190
191 // Delegations are children of BYOIP prefixes and have no account-wide
192 // enumeration API. Fan out: list every account prefix, then list the
193 // delegations on each prefix (bounded concurrency). A prefix that has
194 // gone away between the two calls reads as an empty list (typed
195 // `PrefixNotFound`, handled in `listDelegations`).
196 list: Effect.fn(function* () {
197 const { accountId } = yield* yield* CloudflareEnvironment;
198
199 const prefixIds = yield* addressing.listPrefixes
200 .pages({ accountId })
201 .pipe(
202 Stream.runCollect,
203 Effect.map((chunk) =>
204 Array.from(chunk).flatMap((page) =>
205 (page.result ?? [])
206 .map((p) => p.id)
207 .filter((id): id is string => typeof id === "string"),
208 ),
209 ),
210 );
211
212 const rows = yield* Effect.forEach(
213 prefixIds,
214 (prefixId) =>
215 listDelegations(accountId, prefixId).pipe(
216 Effect.map((delegations) =>
217 delegations.map((d) => toAttributes(d, prefixId, accountId)),
218 ),
219 ),
220 { concurrency: 10 },
221 );
222
223 return rows.flat();
224 }),
225 });
226
227interface ObservedDelegation {
228 readonly id?: string | null;
229 readonly cidr?: string | null;
230 readonly createdAt?: string | null;
231 readonly delegatedAccountId?: string | null;
232 readonly parentPrefixId?: string | null;
233}
234
235/**
236 * List all delegations on a prefix. A missing parent prefix reads as an
237 * empty list.
238 */
239const listDelegations = (accountId: string, prefixId: string) =>
240 addressing.listPrefixDelegations.items({ accountId, prefixId }).pipe(
241 Stream.runCollect,
242 Effect.map((chunk) => Array.from(chunk) as ObservedDelegation[]),
243 Effect.catchTag("PrefixNotFound", () =>
244 Effect.succeed([] as ObservedDelegation[]),
245 ),
246 );
247
248const toAttributes = (
249 delegation: ObservedDelegation,
250 prefixId: string,
251 accountId: string,
252): PrefixDelegationAttributes => ({
253 delegationId: delegation.id ?? "",
254 prefixId,
255 accountId,
256 cidr: delegation.cidr ?? "",
257 delegatedAccountId: delegation.delegatedAccountId ?? "",
258 createdAt: delegation.createdAt ?? undefined,
259});