@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 +45 -2
- package/dist/cloudTarget.d.ts +100 -0
- package/dist/cloudflare.d.ts +50 -0
- package/dist/cloudflare.js +155 -0
- package/dist/cloudflare.js.map +10 -0
- package/dist/digitalocean.js +145 -99
- package/dist/digitalocean.js.map +5 -4
- package/dist/dns.d.ts +101 -0
- package/dist/dns.js +16 -0
- package/dist/dns.js.map +10 -0
- package/dist/hetzner.js +150 -104
- package/dist/hetzner.js.map +5 -4
- package/package.json +12 -2
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.
|
|
251
|
+
## What v0.4.0 does NOT include
|
|
211
252
|
|
|
212
|
-
-
|
|
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
|
+
}
|