1import * as cni from "@distilled.cloud/cloudflare/network-interconnects";
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";
9
10const TypeId = "Cloudflare.NetworkInterconnects.Settings" as const;
11type TypeId = typeof TypeId;
12
13export interface NetworkInterconnectSettingsProps {
14 /**
15 * The default ASN (Autonomous System Number) used for new CNI BGP
16 * sessions on this account when a CNI does not specify its own
17 * `customerAsn`.
18 *
19 * Mutable — updated in place via `PUT /accounts/{account_id}/cni/settings`.
20 */
21 defaultAsn: number;
22}
23
24export interface NetworkInterconnectSettingsAttributes {
25 /** Account the CNI settings belong to. */
26 accountId: string;
27 /** The currently configured default ASN. */
28 defaultAsn: number;
29 /**
30 * The default ASN observed before Alchemy first managed this
31 * singleton. Restored on destroy, so deleting the resource puts the
32 * account back the way it was found.
33 */
34 initialDefaultAsn: number;
35}
36
37export type NetworkInterconnectSettings = Resource<
38 TypeId,
39 NetworkInterconnectSettingsProps,
40 NetworkInterconnectSettingsAttributes,
41 never,
42 Providers
43>;
44
45/**
46 * Account-level settings for Cloudflare Network Interconnect (CNI v2) —
47 * currently the default ASN applied to new CNI BGP configurations
48 * (`/accounts/{account_id}/cni/settings`).
49 *
50 * The settings object is an **account singleton** — it always exists and
51 * can never be created or deleted. Reconcile PUTs the desired value when
52 * the observed value differs; destroy restores the value the setting had
53 * before Alchemy first managed it (captured as `initialDefaultAsn`).
54 *
55 * CNI is an enterprise feature — on accounts without the Network
56 * Interconnect entitlement the endpoint fails with the typed `Forbidden`
57 * error.
58 * ### Managing the default ASN
59 * **Example:** Pin the account's default ASN
60 * ```typescript
61 * yield* Cloudflare.NetworkInterconnects.NetworkInterconnectSettings("CniSettings", {
62 * defaultAsn: 65000,
63 * });
64 * ```
65 *
66 * **Example:** Use a private 32-bit ASN
67 * ```typescript
68 * yield* Cloudflare.NetworkInterconnects.NetworkInterconnectSettings("CniSettings", {
69 * defaultAsn: 4200000001,
70 * });
71 * ```
72 *
73 * @see https://developers.cloudflare.com/network-interconnect/
74 *
75 * @resource
76 * @product Network Interconnects
77 * @category Network
78 */
79export const NetworkInterconnectSettings =
80 Resource<NetworkInterconnectSettings>(TypeId);
81
82/**
83 * Returns true if the given value is a NetworkInterconnectSettings resource.
84 */
85export const isNetworkInterconnectSettings = (
86 value: unknown,
87): value is NetworkInterconnectSettings =>
88 Predicate.hasProperty(value, "Type") && value.Type === TypeId;
89
90export const NetworkInterconnectSettingsProvider = () =>
91 Provider.succeed(NetworkInterconnectSettings, {
92 nuke: { singleton: true },
93 stables: ["accountId", "initialDefaultAsn"],
94
95 // Account singleton — there is no enumeration API, just the single
96 // `/accounts/{account_id}/cni/settings` object. Read it and return a
97 // one-element array (mirroring `read` with no prior output, so
98 // `initialDefaultAsn` is the observed value). CNI is an enterprise
99 // feature: accounts without the entitlement reject the route with the
100 // typed `Forbidden` error — treat that as "unset" and return `[]`.
101 list: Effect.fn(function* () {
102 const { accountId } = yield* yield* CloudflareEnvironment;
103 const observed = yield* cni
104 .getSetting({ accountId })
105 .pipe(Effect.catchTag("Forbidden", () => Effect.succeed(undefined)));
106 if (observed === undefined) return [];
107 return [
108 {
109 accountId,
110 defaultAsn: observed.defaultAsn,
111 initialDefaultAsn: observed.defaultAsn,
112 },
113 ];
114 }),
115
116 read: Effect.fn(function* ({ output }) {
117 const { accountId } = yield* yield* CloudflareEnvironment;
118 // The settings singleton always exists with a Cloudflare default —
119 // there is nothing to "own", so a cold read adopts freely (never
120 // `Unowned`). The observed value at adoption time becomes the
121 // `initialDefaultAsn` restored on destroy.
122 const observed = yield* cni.getSetting({ accountId });
123 const initialDefaultAsn =
124 output !== undefined ? output.initialDefaultAsn : observed.defaultAsn;
125 return {
126 accountId,
127 defaultAsn: observed.defaultAsn,
128 initialDefaultAsn,
129 };
130 }),
131
132 reconcile: Effect.fn(function* ({ news, output }) {
133 const { accountId } = yield* yield* CloudflareEnvironment;
134
135 // 1. Observe — the singleton always exists; read its live value.
136 const observed = yield* cni.getSetting({ accountId });
137
138 // 2. Capture — the pre-management value, restored on destroy.
139 // `output` (including an adoption read) already carries it;
140 // otherwise this is our first touch and the observed value is
141 // the account's original.
142 const initialDefaultAsn =
143 output !== undefined ? output.initialDefaultAsn : observed.defaultAsn;
144
145 // 3. Sync — PUT only when the observed value differs.
146 if (observed.defaultAsn === news.defaultAsn) {
147 return {
148 accountId,
149 defaultAsn: observed.defaultAsn,
150 initialDefaultAsn,
151 };
152 }
153 const updated = yield* cni.putSetting({
154 accountId,
155 defaultAsn: news.defaultAsn,
156 });
157 return {
158 accountId,
159 defaultAsn: updated.defaultAsn,
160 initialDefaultAsn,
161 };
162 }),
163
164 delete: Effect.fn(function* ({ output }) {
165 const { accountId, initialDefaultAsn } = output;
166 // Observe — restore the pre-management value; skip the call when
167 // it already matches (idempotent re-delete after a crashed run).
168 const observed = yield* cni.getSetting({ accountId });
169 if (observed.defaultAsn === initialDefaultAsn) return;
170 yield* cni.putSetting({ accountId, defaultAsn: initialDefaultAsn });
171 }),
172 });