@absolutejs/deploy 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -188,6 +188,47 @@ happen. Admin helpers: `listHetznerServers({ token, labelSelector?
188
188
  })` (Hetzner's `'env=prod'` / `'env in (prod,staging)'` syntax);
189
189
  `destroyHetznerServer({ token, id })` (404 idempotent success).
190
190
 
191
+ ## `@absolutejs/deploy/cloudflare` — DNS automation (0.4.0)
192
+
193
+ After provisioning a Target, point a hostname at its IP without
194
+ leaving the deploy script. `cloudflareProvider({ token, zoneId })`
195
+ implements the shared `DnsProvider` contract from
196
+ `@absolutejs/deploy/dns`.
197
+
198
+ ```ts
199
+ import { hetznerTarget } from '@absolutejs/deploy/hetzner';
200
+ import { cloudflareProvider } from '@absolutejs/deploy/cloudflare';
201
+ import { ensureDnsForTarget } from '@absolutejs/deploy/dns';
202
+
203
+ const target = await hetznerTarget({ /* … */ });
204
+ const dns = cloudflareProvider({
205
+ token: process.env.CLOUDFLARE_TOKEN!,
206
+ zoneId: process.env.CLOUDFLARE_ZONE_ID!,
207
+ });
208
+
209
+ // Idempotent — create or update so the A record points at target.ipv4.
210
+ await ensureDnsForTarget(dns, {
211
+ name: 'api.example.com',
212
+ target,
213
+ ttl: 60,
214
+ proxied: false,
215
+ });
216
+ ```
217
+
218
+ `upsert` is the canonical entry: finds by exact (name, type); skips
219
+ the API call entirely when the existing record already matches the
220
+ spec (no churn on TTL / proxied / comment agreement). Multiple
221
+ records sharing the same (name, type) throw with a "resolve
222
+ manually" message instead of silently picking one.
223
+
224
+ Auth uses Cloudflare API tokens with `Zone:DNS:Edit` scope (global
225
+ keys not supported). Pair with `provider.list({ name?, type? })`
226
+ for inventory, `provider.delete(id)` for tear-down (404 idempotent
227
+ success).
228
+
229
+ The same `DnsProvider` contract applies to other providers — Route
230
+ 53 / DigitalOcean DNS / etc. follow next.
231
+
191
232
  ## DigitalOcean Droplet — first deploy (manual)
192
233
 
193
234
  Assuming a fresh Ubuntu/Debian Droplet:
@@ -207,9 +248,11 @@ bun run my-deploy-script.ts
207
248
 
208
249
  The first run creates `/srv/<appName>/releases/<id>/`, drops a systemd unit at `/etc/systemd/system/<appName>.service` (if you're using `systemdManager`), starts the service, and probes. Subsequent runs just add a new release dir and swap the symlink.
209
250
 
210
- ## What v0.3.0 does NOT include
251
+ ## What v0.4.0 does NOT include
211
252
 
212
- - Provider-specific HTTP-API adapters beyond DigitalOcean + Hetzner (Cloudflare Workers, Fly Machines, AWS Fargate, GCP Cloud Run). Linode / Vultr follow the same shape and are next on the list.
253
+ - TLS certificate automation. ACME / Let's Encrypt is a separate concern coming next.
254
+ - Cloud-provider compute targets beyond DigitalOcean + Hetzner. Linode / Vultr / Fly Machines follow the same shape and are next on the list.
255
+ - DNS providers beyond Cloudflare. Route 53 / DigitalOcean DNS / Hetzner DNS slot into the same `DnsProvider` contract.
213
256
  - Bun installation on the remote — caller does it once, out of band.
214
257
  - Multi-target / fan-out deploys (caller iterates).
215
258
  - Zero-downtime port-swap (start new release on a fresh port, then nginx-reload). The default pipeline does stop-then-start; for true zero-downtime, replace the `restart` step.
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Shared "cloud-provider Target" plumbing used by the
3
+ * provider-specific adapters (`./digitalocean`, `./hetzner`, future
4
+ * `./linode`, `./vultr`, etc.).
5
+ *
6
+ * The provider supplies a small `CloudTargetHooks` bag that knows
7
+ * the provider's:
8
+ *
9
+ * - find-by-name lookup
10
+ * - create call (closure over create params)
11
+ * - fetch-by-id (used to poll for `active`)
12
+ * - destroy-by-id
13
+ * - status + ipv4 + id extraction from the provider's Server shape
14
+ * - readiness predicate (status reached the terminal "running" value)
15
+ *
16
+ * `createCloudTarget()` does the universal machinery: provision-or-
17
+ * reuse, poll until ready + IPv4, wait for SSH probe, build
18
+ * `sshTarget` against the IPv4, return the Target wrapped with
19
+ * `{ id, ipv4, destroy() }`.
20
+ *
21
+ * The public adapter (e.g. `digitalOceanTarget`) is a 30-line facade
22
+ * that wires its provider-specific bits and renames `id` → `dropletId`
23
+ * on the way out.
24
+ */
25
+ import type { Target } from './targets';
26
+ /** Provider-specific hooks. Keep these pure of network IO timing — the helper schedules. */
27
+ export type CloudTargetHooks<Server> = {
28
+ /** Find a server by name. Returns undefined if absent. */
29
+ findByName: (name: string) => Promise<Server | undefined>;
30
+ /** Create the server. Closure over provider-specific create params. */
31
+ create: () => Promise<Server>;
32
+ /** Fetch a fresh copy of the server by id. Used to poll. */
33
+ fetch: (id: number) => Promise<Server>;
34
+ /** Destroy a server by id. 404 should be treated as idempotent success. */
35
+ destroy: (id: number) => Promise<void>;
36
+ /** True when the server has reached its terminal "running" status. */
37
+ isReady: (server: Server) => boolean;
38
+ /** Extract the numeric id. */
39
+ getId: (server: Server) => number;
40
+ /** Extract the public IPv4. Returns undefined while one is being assigned. */
41
+ getIpv4: (server: Server) => string | undefined;
42
+ /** Extract the current status as a string (for log lines). */
43
+ getStatus: (server: Server) => string;
44
+ };
45
+ export type CloudTargetOptions = {
46
+ /** Provider's idempotency key (server name). */
47
+ name: string;
48
+ /** Region / location label — used in the "creating" log line. */
49
+ region: string;
50
+ /** SSH login user. Default `'root'`. */
51
+ user?: string;
52
+ /** SSH identity file. */
53
+ identity?: string;
54
+ /** SSH port. Default 22. */
55
+ port?: number;
56
+ /** Default 5 min. */
57
+ provisionTimeoutMs?: number;
58
+ /** Default 2 min. */
59
+ sshReadinessTimeoutMs?: number;
60
+ /** Default 5 s. */
61
+ pollIntervalMs?: number;
62
+ /** Called with status updates. */
63
+ onLog?: (line: string) => void;
64
+ /** Override SSH probe — tests skip real TCP IO. */
65
+ probeSsh?: (host: string, port: number) => Promise<boolean>;
66
+ /** Override sleep — tests skip real waits. */
67
+ sleep?: (ms: number) => Promise<void>;
68
+ /** Override clock — tests inject deterministic timestamps. */
69
+ now?: () => number;
70
+ /**
71
+ * Short log prefix, e.g. `'[do]'` or `'[hetzner]'`. Threaded through
72
+ * every log line so multi-provider deploys distinguish output.
73
+ */
74
+ logPrefix: string;
75
+ /**
76
+ * Provider's word for the entity in log copy — `'droplet'` for DO,
77
+ * `'server'` for Hetzner. Preserves provider-accurate output.
78
+ */
79
+ entityWord: string;
80
+ /**
81
+ * Build the Target's `description` field. Receives the resolved
82
+ * IPv4 + the wrapped sshTarget description.
83
+ */
84
+ describeTarget: (sshDescription: string) => string;
85
+ };
86
+ export type CloudTargetResult = {
87
+ id: number;
88
+ ipv4: string;
89
+ description: string;
90
+ exec: Target['exec'];
91
+ upload: Target['upload'];
92
+ close?: Target['close'];
93
+ destroy: () => Promise<void>;
94
+ };
95
+ /**
96
+ * The shared provision-or-reuse + wait-for-ready + wait-for-SSH
97
+ * pipeline. Provider-specific adapters wire their `CloudTargetHooks`
98
+ * + their option-shape mapping and return a typed result.
99
+ */
100
+ export declare const createCloudTarget: <Server>(hooks: CloudTargetHooks<Server>, options: CloudTargetOptions) => Promise<CloudTargetResult>;
@@ -0,0 +1,50 @@
1
+ /**
2
+ * @absolutejs/deploy/cloudflare — Cloudflare DNS adapter implementing
3
+ * the {@link DnsProvider} contract from `./dns`.
4
+ *
5
+ * Scope: one provider instance is bound to one Cloudflare zone. Multi-
6
+ * zone callers construct one provider per zone.
7
+ *
8
+ * Auth: Cloudflare API tokens with `Zone:DNS:Edit` scope. Global API
9
+ * keys are intentionally not supported — tokens are the modern path.
10
+ *
11
+ * Narrow CloudflareClientLike interface keeps `cloudflare` SDK out as
12
+ * a hard dep. Default client uses `fetch` against
13
+ * `api.cloudflare.com/client/v4`.
14
+ */
15
+ import type { DnsProvider } from './dns';
16
+ /**
17
+ * Minimal subset of Cloudflare API calls. Lets callers BYO a client
18
+ * with retry / observability / etc.
19
+ */
20
+ export type CloudflareClientLike = {
21
+ request: <T = unknown>(method: 'GET' | 'POST' | 'PUT' | 'DELETE', path: string, body?: unknown) => Promise<T>;
22
+ };
23
+ export declare class CloudflareError extends Error {
24
+ readonly status: number;
25
+ readonly body: unknown;
26
+ constructor(message: string, status: number, body: unknown);
27
+ }
28
+ /**
29
+ * fetch-backed default client. Throws CloudflareError on non-2xx OR
30
+ * on a 2xx with `success: false` (Cloudflare returns 200 + an errors
31
+ * array for some validation failures, hence the second check).
32
+ */
33
+ export declare const createCloudflareClient: (token: string, options?: {
34
+ baseUrl?: string;
35
+ fetch?: typeof fetch;
36
+ }) => CloudflareClientLike;
37
+ export type CloudflareProviderOptions = {
38
+ /** API token with `Zone:DNS:Edit` permission. Required unless `client` is set. */
39
+ token?: string;
40
+ /** Custom client. Overrides token-built default. */
41
+ client?: CloudflareClientLike;
42
+ /** Cloudflare Zone ID this provider operates on. */
43
+ zoneId: string;
44
+ /** Optional zone name for log copy. Default uses `zoneId` truncated. */
45
+ zoneName?: string;
46
+ };
47
+ /**
48
+ * Build a {@link DnsProvider} bound to one Cloudflare zone.
49
+ */
50
+ export declare const cloudflareProvider: (options: CloudflareProviderOptions) => DnsProvider;
@@ -0,0 +1,155 @@
1
+ // @bun
2
+ // src/cloudflare.ts
3
+ var CLOUDFLARE_API_BASE = "https://api.cloudflare.com/client/v4";
4
+
5
+ class CloudflareError extends Error {
6
+ status;
7
+ body;
8
+ constructor(message, status, body) {
9
+ super(message);
10
+ this.name = "CloudflareError";
11
+ this.status = status;
12
+ this.body = body;
13
+ }
14
+ }
15
+ var createCloudflareClient = (token, options = {}) => {
16
+ const base = options.baseUrl ?? CLOUDFLARE_API_BASE;
17
+ const f = options.fetch ?? fetch;
18
+ return {
19
+ request: async (method, path, body) => {
20
+ const init = {
21
+ headers: {
22
+ authorization: `Bearer ${token}`,
23
+ "content-type": "application/json"
24
+ },
25
+ method
26
+ };
27
+ if (body !== undefined)
28
+ init.body = JSON.stringify(body);
29
+ const response = await f(`${base}${path}`, init);
30
+ const text = await response.text();
31
+ const parsed = text.length > 0 ? JSON.parse(text) : undefined;
32
+ if (!response.ok) {
33
+ throw new CloudflareError(`Cloudflare API ${method} ${path} failed: ${response.status} ${response.statusText}`, response.status, parsed);
34
+ }
35
+ return parsed;
36
+ }
37
+ };
38
+ };
39
+ var resolveClient = (options) => {
40
+ if (options.client !== undefined)
41
+ return options.client;
42
+ if (options.token !== undefined && options.token.length > 0) {
43
+ return createCloudflareClient(options.token);
44
+ }
45
+ throw new Error("[deploy/cloudflare] either `token` or `client` must be provided");
46
+ };
47
+ var toDnsRecord = (raw) => ({
48
+ content: raw.content,
49
+ id: raw.id,
50
+ name: raw.name,
51
+ type: raw.type,
52
+ ...raw.ttl !== undefined ? { ttl: raw.ttl } : {},
53
+ ...raw.proxied !== undefined ? { proxied: raw.proxied } : {},
54
+ ...raw.comment !== undefined ? { comment: raw.comment } : {}
55
+ });
56
+ var specEqualsRecord = (spec, record) => {
57
+ if (record.content !== spec.content)
58
+ return false;
59
+ if (spec.ttl !== undefined && record.ttl !== spec.ttl)
60
+ return false;
61
+ if (spec.proxied !== undefined && record.proxied !== spec.proxied)
62
+ return false;
63
+ if (spec.comment !== undefined && record.comment !== spec.comment)
64
+ return false;
65
+ return true;
66
+ };
67
+ var buildQuery = (filter) => {
68
+ const params = [];
69
+ if (filter?.name !== undefined) {
70
+ params.push(`name=${encodeURIComponent(filter.name)}`);
71
+ }
72
+ if (filter?.type !== undefined) {
73
+ params.push(`type=${encodeURIComponent(filter.type)}`);
74
+ }
75
+ return params.length > 0 ? `?${params.join("&")}` : "";
76
+ };
77
+ var cloudflareProvider = (options) => {
78
+ const client = resolveClient(options);
79
+ const { zoneId } = options;
80
+ const zoneLabel = options.zoneName ?? zoneId.slice(0, 8);
81
+ const recordsPath = `/zones/${zoneId}/dns_records`;
82
+ const list = async (filter) => {
83
+ const response = await client.request("GET", `${recordsPath}${buildQuery(filter)}`);
84
+ return response.result.map(toDnsRecord);
85
+ };
86
+ const find = async (key) => {
87
+ const matches = await list(key);
88
+ const exact = matches.filter((record) => record.name === key.name || record.name === `${key.name}.`);
89
+ if (exact.length === 0)
90
+ return;
91
+ if (exact.length > 1) {
92
+ throw new Error(`[deploy/cloudflare] multiple ${key.type} records for "${key.name}" in zone ${zoneLabel} \u2014 drifted state; resolve manually before upsert.`);
93
+ }
94
+ return exact[0];
95
+ };
96
+ const create = async (spec) => {
97
+ const response = await client.request("POST", recordsPath, {
98
+ content: spec.content,
99
+ name: spec.name,
100
+ type: spec.type,
101
+ ...spec.ttl !== undefined ? { ttl: spec.ttl } : {},
102
+ ...spec.proxied !== undefined ? { proxied: spec.proxied } : {},
103
+ ...spec.comment !== undefined ? { comment: spec.comment } : {}
104
+ });
105
+ return toDnsRecord(response.result);
106
+ };
107
+ const update = async (id, spec) => {
108
+ const response = await client.request("PUT", `${recordsPath}/${id}`, {
109
+ content: spec.content,
110
+ name: spec.name,
111
+ type: spec.type,
112
+ ...spec.ttl !== undefined ? { ttl: spec.ttl } : {},
113
+ ...spec.proxied !== undefined ? { proxied: spec.proxied } : {},
114
+ ...spec.comment !== undefined ? { comment: spec.comment } : {}
115
+ });
116
+ return toDnsRecord(response.result);
117
+ };
118
+ const deleteRecord = async (id) => {
119
+ try {
120
+ await client.request("DELETE", `${recordsPath}/${id}`);
121
+ } catch (error) {
122
+ if (error instanceof CloudflareError && error.status === 404) {
123
+ return;
124
+ }
125
+ throw error;
126
+ }
127
+ };
128
+ const upsert = async (spec) => {
129
+ const existing = await find({ name: spec.name, type: spec.type });
130
+ if (existing === undefined) {
131
+ return create(spec);
132
+ }
133
+ if (specEqualsRecord(spec, existing)) {
134
+ return existing;
135
+ }
136
+ return update(existing.id, spec);
137
+ };
138
+ return {
139
+ create,
140
+ delete: deleteRecord,
141
+ description: `cloudflare zone "${zoneLabel}"`,
142
+ find,
143
+ list,
144
+ update,
145
+ upsert
146
+ };
147
+ };
148
+ export {
149
+ createCloudflareClient,
150
+ cloudflareProvider,
151
+ CloudflareError
152
+ };
153
+
154
+ //# debugId=CC538CE8BF2FCBDB64756E2164756E21
155
+ //# sourceMappingURL=cloudflare.js.map
@@ -0,0 +1,10 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/cloudflare.ts"],
4
+ "sourcesContent": [
5
+ "/**\n * @absolutejs/deploy/cloudflare — Cloudflare DNS adapter implementing\n * the {@link DnsProvider} contract from `./dns`.\n *\n * Scope: one provider instance is bound to one Cloudflare zone. Multi-\n * zone callers construct one provider per zone.\n *\n * Auth: Cloudflare API tokens with `Zone:DNS:Edit` scope. Global API\n * keys are intentionally not supported — tokens are the modern path.\n *\n * Narrow CloudflareClientLike interface keeps `cloudflare` SDK out as\n * a hard dep. Default client uses `fetch` against\n * `api.cloudflare.com/client/v4`.\n */\n\nimport type {\n\tDnsProvider,\n\tDnsRecord,\n\tDnsRecordFilter,\n\tDnsRecordSpec,\n\tDnsRecordType\n} from './dns';\n\nconst CLOUDFLARE_API_BASE = 'https://api.cloudflare.com/client/v4';\n\n/**\n * Minimal subset of Cloudflare API calls. Lets callers BYO a client\n * with retry / observability / etc.\n */\nexport type CloudflareClientLike = {\n\trequest: <T = unknown>(\n\t\tmethod: 'GET' | 'POST' | 'PUT' | 'DELETE',\n\t\tpath: string,\n\t\tbody?: unknown\n\t) => Promise<T>;\n};\n\n/** Cloudflare's standard envelope on every response. */\ntype CloudflareResponse<T> = {\n\tsuccess: boolean;\n\tresult: T;\n\terrors?: Array<{ code: number; message: string }>;\n\tmessages?: Array<{ code: number; message: string }>;\n};\n\n/** Raw record from Cloudflare's API. */\ntype CloudflareDnsRecord = {\n\tid: string;\n\tname: string;\n\ttype: string;\n\tcontent: string;\n\tttl?: number;\n\tproxied?: boolean;\n\tcomment?: string;\n\tzone_id?: string;\n\tzone_name?: string;\n};\n\nexport class CloudflareError extends Error {\n\treadonly status: number;\n\treadonly body: unknown;\n\tconstructor(message: string, status: number, body: unknown) {\n\t\tsuper(message);\n\t\tthis.name = 'CloudflareError';\n\t\tthis.status = status;\n\t\tthis.body = body;\n\t}\n}\n\n/**\n * fetch-backed default client. Throws CloudflareError on non-2xx OR\n * on a 2xx with `success: false` (Cloudflare returns 200 + an errors\n * array for some validation failures, hence the second check).\n */\nexport const createCloudflareClient = (\n\ttoken: string,\n\toptions: { baseUrl?: string; fetch?: typeof fetch } = {}\n): CloudflareClientLike => {\n\tconst base = options.baseUrl ?? CLOUDFLARE_API_BASE;\n\tconst f = options.fetch ?? fetch;\n\treturn {\n\t\trequest: async <T>(\n\t\t\tmethod: 'GET' | 'POST' | 'PUT' | 'DELETE',\n\t\t\tpath: string,\n\t\t\tbody?: unknown\n\t\t): Promise<T> => {\n\t\t\tconst init: RequestInit = {\n\t\t\t\theaders: {\n\t\t\t\t\tauthorization: `Bearer ${token}`,\n\t\t\t\t\t'content-type': 'application/json'\n\t\t\t\t},\n\t\t\t\tmethod\n\t\t\t};\n\t\t\tif (body !== undefined) init.body = JSON.stringify(body);\n\t\t\tconst response = await f(`${base}${path}`, init);\n\t\t\tconst text = await response.text();\n\t\t\tconst parsed = text.length > 0 ? JSON.parse(text) : undefined;\n\t\t\tif (!response.ok) {\n\t\t\t\tthrow new CloudflareError(\n\t\t\t\t\t`Cloudflare API ${method} ${path} failed: ${response.status} ${response.statusText}`,\n\t\t\t\t\tresponse.status,\n\t\t\t\t\tparsed\n\t\t\t\t);\n\t\t\t}\n\t\t\treturn parsed as T;\n\t\t}\n\t};\n};\n\nexport type CloudflareProviderOptions = {\n\t/** API token with `Zone:DNS:Edit` permission. Required unless `client` is set. */\n\ttoken?: string;\n\t/** Custom client. Overrides token-built default. */\n\tclient?: CloudflareClientLike;\n\t/** Cloudflare Zone ID this provider operates on. */\n\tzoneId: string;\n\t/** Optional zone name for log copy. Default uses `zoneId` truncated. */\n\tzoneName?: string;\n};\n\nconst resolveClient = (\n\toptions: Pick<CloudflareProviderOptions, 'client' | 'token'>\n): CloudflareClientLike => {\n\tif (options.client !== undefined) return options.client;\n\tif (options.token !== undefined && options.token.length > 0) {\n\t\treturn createCloudflareClient(options.token);\n\t}\n\tthrow new Error(\n\t\t'[deploy/cloudflare] either `token` or `client` must be provided'\n\t);\n};\n\nconst toDnsRecord = (raw: CloudflareDnsRecord): DnsRecord => ({\n\tcontent: raw.content,\n\tid: raw.id,\n\tname: raw.name,\n\ttype: raw.type as DnsRecordType,\n\t...(raw.ttl !== undefined ? { ttl: raw.ttl } : {}),\n\t...(raw.proxied !== undefined ? { proxied: raw.proxied } : {}),\n\t...(raw.comment !== undefined ? { comment: raw.comment } : {})\n});\n\nconst specEqualsRecord = (spec: DnsRecordSpec, record: DnsRecord): boolean => {\n\tif (record.content !== spec.content) return false;\n\tif (spec.ttl !== undefined && record.ttl !== spec.ttl) return false;\n\tif (spec.proxied !== undefined && record.proxied !== spec.proxied)\n\t\treturn false;\n\tif (spec.comment !== undefined && record.comment !== spec.comment)\n\t\treturn false;\n\treturn true;\n};\n\nconst buildQuery = (filter?: DnsRecordFilter): string => {\n\tconst params: string[] = [];\n\tif (filter?.name !== undefined) {\n\t\tparams.push(`name=${encodeURIComponent(filter.name)}`);\n\t}\n\tif (filter?.type !== undefined) {\n\t\tparams.push(`type=${encodeURIComponent(filter.type)}`);\n\t}\n\treturn params.length > 0 ? `?${params.join('&')}` : '';\n};\n\n/**\n * Build a {@link DnsProvider} bound to one Cloudflare zone.\n */\nexport const cloudflareProvider = (\n\toptions: CloudflareProviderOptions\n): DnsProvider => {\n\tconst client = resolveClient(options);\n\tconst { zoneId } = options;\n\tconst zoneLabel = options.zoneName ?? zoneId.slice(0, 8);\n\tconst recordsPath = `/zones/${zoneId}/dns_records`;\n\n\tconst list = async (filter?: DnsRecordFilter): Promise<DnsRecord[]> => {\n\t\tconst response = await client.request<\n\t\t\tCloudflareResponse<CloudflareDnsRecord[]>\n\t\t>('GET', `${recordsPath}${buildQuery(filter)}`);\n\t\treturn response.result.map(toDnsRecord);\n\t};\n\n\tconst find = async (key: {\n\t\tname: string;\n\t\ttype: DnsRecordType;\n\t}): Promise<DnsRecord | undefined> => {\n\t\tconst matches = await list(key);\n\t\t// Cloudflare's filter is sometimes substring-loose on `name`; pin to\n\t\t// an exact match here so a `'api.example.com'` lookup never returns\n\t\t// `'api.example.com.staging'`.\n\t\tconst exact = matches.filter(\n\t\t\t(record) => record.name === key.name || record.name === `${key.name}.`\n\t\t);\n\t\tif (exact.length === 0) return undefined;\n\t\tif (exact.length > 1) {\n\t\t\tthrow new Error(\n\t\t\t\t`[deploy/cloudflare] multiple ${key.type} records for \"${key.name}\" in zone ${zoneLabel} — drifted state; resolve manually before upsert.`\n\t\t\t);\n\t\t}\n\t\treturn exact[0];\n\t};\n\n\tconst create = async (spec: DnsRecordSpec): Promise<DnsRecord> => {\n\t\tconst response = await client.request<\n\t\t\tCloudflareResponse<CloudflareDnsRecord>\n\t\t>('POST', recordsPath, {\n\t\t\tcontent: spec.content,\n\t\t\tname: spec.name,\n\t\t\ttype: spec.type,\n\t\t\t...(spec.ttl !== undefined ? { ttl: spec.ttl } : {}),\n\t\t\t...(spec.proxied !== undefined ? { proxied: spec.proxied } : {}),\n\t\t\t...(spec.comment !== undefined ? { comment: spec.comment } : {})\n\t\t});\n\t\treturn toDnsRecord(response.result);\n\t};\n\n\tconst update = async (\n\t\tid: string,\n\t\tspec: DnsRecordSpec\n\t): Promise<DnsRecord> => {\n\t\tconst response = await client.request<\n\t\t\tCloudflareResponse<CloudflareDnsRecord>\n\t\t>('PUT', `${recordsPath}/${id}`, {\n\t\t\tcontent: spec.content,\n\t\t\tname: spec.name,\n\t\t\ttype: spec.type,\n\t\t\t...(spec.ttl !== undefined ? { ttl: spec.ttl } : {}),\n\t\t\t...(spec.proxied !== undefined ? { proxied: spec.proxied } : {}),\n\t\t\t...(spec.comment !== undefined ? { comment: spec.comment } : {})\n\t\t});\n\t\treturn toDnsRecord(response.result);\n\t};\n\n\tconst deleteRecord = async (id: string): Promise<void> => {\n\t\ttry {\n\t\t\tawait client.request('DELETE', `${recordsPath}/${id}`);\n\t\t} catch (error) {\n\t\t\tif (error instanceof CloudflareError && error.status === 404) {\n\t\t\t\treturn; // already gone — idempotent\n\t\t\t}\n\t\t\tthrow error;\n\t\t}\n\t};\n\n\tconst upsert = async (spec: DnsRecordSpec): Promise<DnsRecord> => {\n\t\tconst existing = await find({ name: spec.name, type: spec.type });\n\t\tif (existing === undefined) {\n\t\t\treturn create(spec);\n\t\t}\n\t\tif (specEqualsRecord(spec, existing)) {\n\t\t\treturn existing;\n\t\t}\n\t\treturn update(existing.id, spec);\n\t};\n\n\treturn {\n\t\tcreate,\n\t\tdelete: deleteRecord,\n\t\tdescription: `cloudflare zone \"${zoneLabel}\"`,\n\t\tfind,\n\t\tlist,\n\t\tupdate,\n\t\tupsert\n\t};\n};\n"
6
+ ],
7
+ "mappings": "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",
8
+ "debugId": "CC538CE8BF2FCBDB64756E2164756E21",
9
+ "names": []
10
+ }