@absolutejs/deploy 0.3.1 → 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/cloudflare.d.ts +50 -0
- package/dist/cloudflare.js +155 -0
- package/dist/cloudflare.js.map +10 -0
- package/dist/dns.d.ts +101 -0
- package/dist/dns.js +16 -0
- package/dist/dns.js.map +10 -0
- 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,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
|
+
}
|
package/dist/dns.d.ts
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DNS provider contract for `@absolutejs/deploy`. Provider-specific
|
|
3
|
+
* adapters (Cloudflare, Route 53, etc.) implement `DnsProvider`; the
|
|
4
|
+
* deploy pipeline composes them with a cloud `Target` to point a
|
|
5
|
+
* hostname at a freshly-provisioned IP.
|
|
6
|
+
*
|
|
7
|
+
* Operations are zone-scoped: a `DnsProvider` instance is bound to
|
|
8
|
+
* one zone at construction time. Multi-zone hosts construct one
|
|
9
|
+
* provider per zone.
|
|
10
|
+
*/
|
|
11
|
+
/** Record types we care about for deploy workflows. */
|
|
12
|
+
export type DnsRecordType = 'A' | 'AAAA' | 'CNAME' | 'TXT' | 'MX';
|
|
13
|
+
/** A DNS record as it exists in the provider. */
|
|
14
|
+
export type DnsRecord = {
|
|
15
|
+
/** Provider-assigned id (Cloudflare uuid, Route 53 set name, etc.). */
|
|
16
|
+
id: string;
|
|
17
|
+
/** Fully-qualified record name (`api.example.com.`). */
|
|
18
|
+
name: string;
|
|
19
|
+
type: DnsRecordType;
|
|
20
|
+
/** Record value — IP for A/AAAA, hostname for CNAME, text for TXT, etc. */
|
|
21
|
+
content: string;
|
|
22
|
+
ttl?: number;
|
|
23
|
+
/**
|
|
24
|
+
* Cloudflare's orange-cloud proxy flag. Other providers ignore this
|
|
25
|
+
* field. Default behavior is provider-specific: Cloudflare defaults
|
|
26
|
+
* to `true` (proxied) for new records when omitted.
|
|
27
|
+
*/
|
|
28
|
+
proxied?: boolean;
|
|
29
|
+
comment?: string;
|
|
30
|
+
};
|
|
31
|
+
/** Desired state for an upsert. */
|
|
32
|
+
export type DnsRecordSpec = {
|
|
33
|
+
/** Record name. May be `'@'` for the zone apex or relative ('api'). */
|
|
34
|
+
name: string;
|
|
35
|
+
type: DnsRecordType;
|
|
36
|
+
content: string;
|
|
37
|
+
ttl?: number;
|
|
38
|
+
proxied?: boolean;
|
|
39
|
+
comment?: string;
|
|
40
|
+
};
|
|
41
|
+
export type DnsRecordFilter = {
|
|
42
|
+
name?: string;
|
|
43
|
+
type?: DnsRecordType;
|
|
44
|
+
};
|
|
45
|
+
/**
|
|
46
|
+
* Zone-scoped DNS operations. Implementations: `cloudflareProvider`,
|
|
47
|
+
* future `route53Provider`, etc.
|
|
48
|
+
*/
|
|
49
|
+
export type DnsProvider = {
|
|
50
|
+
/** Human-readable description for logs (`'cloudflare zone "example.com"'`). */
|
|
51
|
+
readonly description: string;
|
|
52
|
+
/** List records, optionally filtered by name + type. */
|
|
53
|
+
list: (filter?: DnsRecordFilter) => Promise<DnsRecord[]>;
|
|
54
|
+
/**
|
|
55
|
+
* Find one record by exact (name, type). Returns undefined if absent.
|
|
56
|
+
* Throws if multiple records share that key (drifted state — multiple
|
|
57
|
+
* A records pointing at different IPs, etc.).
|
|
58
|
+
*/
|
|
59
|
+
find: (key: {
|
|
60
|
+
name: string;
|
|
61
|
+
type: DnsRecordType;
|
|
62
|
+
}) => Promise<DnsRecord | undefined>;
|
|
63
|
+
create: (spec: DnsRecordSpec) => Promise<DnsRecord>;
|
|
64
|
+
update: (id: string, spec: DnsRecordSpec) => Promise<DnsRecord>;
|
|
65
|
+
delete: (id: string) => Promise<void>;
|
|
66
|
+
/**
|
|
67
|
+
* Create or update so the (name, type) record matches `spec`. The
|
|
68
|
+
* canonical "point this DNS at this IP" entry point — idempotent.
|
|
69
|
+
*/
|
|
70
|
+
upsert: (spec: DnsRecordSpec) => Promise<DnsRecord>;
|
|
71
|
+
};
|
|
72
|
+
/**
|
|
73
|
+
* Compose a DNS provider with a cloud Target's IPv4. Idempotently
|
|
74
|
+
* ensures an A record (name → target.ipv4) exists. Returns the
|
|
75
|
+
* resulting DNS record.
|
|
76
|
+
*
|
|
77
|
+
* Usage:
|
|
78
|
+
*
|
|
79
|
+
* const target = await digitalOceanTarget({ ... });
|
|
80
|
+
* const dns = cloudflareProvider({ token, zoneId });
|
|
81
|
+
* await ensureDnsForTarget(dns, {
|
|
82
|
+
* name: 'api.example.com',
|
|
83
|
+
* target,
|
|
84
|
+
* ttl: 60,
|
|
85
|
+
* proxied: false,
|
|
86
|
+
* });
|
|
87
|
+
*/
|
|
88
|
+
export declare const ensureDnsForTarget: (provider: DnsProvider, options: {
|
|
89
|
+
/** Record name (FQDN, relative, or `'@'` for zone apex). */
|
|
90
|
+
name: string;
|
|
91
|
+
/** Cloud Target with an `ipv4` field. */
|
|
92
|
+
target: {
|
|
93
|
+
ipv4: string;
|
|
94
|
+
};
|
|
95
|
+
/** Default 300s. */
|
|
96
|
+
ttl?: number;
|
|
97
|
+
/** Cloudflare orange-cloud. Other providers ignore. */
|
|
98
|
+
proxied?: boolean;
|
|
99
|
+
/** Optional record comment for audit trails. */
|
|
100
|
+
comment?: string;
|
|
101
|
+
}) => Promise<DnsRecord>;
|
package/dist/dns.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// src/dns.ts
|
|
3
|
+
var ensureDnsForTarget = async (provider, options) => provider.upsert({
|
|
4
|
+
content: options.target.ipv4,
|
|
5
|
+
name: options.name,
|
|
6
|
+
type: "A",
|
|
7
|
+
...options.ttl !== undefined ? { ttl: options.ttl } : {},
|
|
8
|
+
...options.proxied !== undefined ? { proxied: options.proxied } : {},
|
|
9
|
+
...options.comment !== undefined ? { comment: options.comment } : {}
|
|
10
|
+
});
|
|
11
|
+
export {
|
|
12
|
+
ensureDnsForTarget
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
//# debugId=9927EACBFED2D7E864756E2164756E21
|
|
16
|
+
//# sourceMappingURL=dns.js.map
|
package/dist/dns.js.map
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/dns.ts"],
|
|
4
|
+
"sourcesContent": [
|
|
5
|
+
"/**\n * DNS provider contract for `@absolutejs/deploy`. Provider-specific\n * adapters (Cloudflare, Route 53, etc.) implement `DnsProvider`; the\n * deploy pipeline composes them with a cloud `Target` to point a\n * hostname at a freshly-provisioned IP.\n *\n * Operations are zone-scoped: a `DnsProvider` instance is bound to\n * one zone at construction time. Multi-zone hosts construct one\n * provider per zone.\n */\n\n/** Record types we care about for deploy workflows. */\nexport type DnsRecordType = 'A' | 'AAAA' | 'CNAME' | 'TXT' | 'MX';\n\n/** A DNS record as it exists in the provider. */\nexport type DnsRecord = {\n\t/** Provider-assigned id (Cloudflare uuid, Route 53 set name, etc.). */\n\tid: string;\n\t/** Fully-qualified record name (`api.example.com.`). */\n\tname: string;\n\ttype: DnsRecordType;\n\t/** Record value — IP for A/AAAA, hostname for CNAME, text for TXT, etc. */\n\tcontent: string;\n\tttl?: number;\n\t/**\n\t * Cloudflare's orange-cloud proxy flag. Other providers ignore this\n\t * field. Default behavior is provider-specific: Cloudflare defaults\n\t * to `true` (proxied) for new records when omitted.\n\t */\n\tproxied?: boolean;\n\tcomment?: string;\n};\n\n/** Desired state for an upsert. */\nexport type DnsRecordSpec = {\n\t/** Record name. May be `'@'` for the zone apex or relative ('api'). */\n\tname: string;\n\ttype: DnsRecordType;\n\tcontent: string;\n\tttl?: number;\n\tproxied?: boolean;\n\tcomment?: string;\n};\n\nexport type DnsRecordFilter = {\n\tname?: string;\n\ttype?: DnsRecordType;\n};\n\n/**\n * Zone-scoped DNS operations. Implementations: `cloudflareProvider`,\n * future `route53Provider`, etc.\n */\nexport type DnsProvider = {\n\t/** Human-readable description for logs (`'cloudflare zone \"example.com\"'`). */\n\treadonly description: string;\n\t/** List records, optionally filtered by name + type. */\n\tlist: (filter?: DnsRecordFilter) => Promise<DnsRecord[]>;\n\t/**\n\t * Find one record by exact (name, type). Returns undefined if absent.\n\t * Throws if multiple records share that key (drifted state — multiple\n\t * A records pointing at different IPs, etc.).\n\t */\n\tfind: (key: {\n\t\tname: string;\n\t\ttype: DnsRecordType;\n\t}) => Promise<DnsRecord | undefined>;\n\tcreate: (spec: DnsRecordSpec) => Promise<DnsRecord>;\n\tupdate: (id: string, spec: DnsRecordSpec) => Promise<DnsRecord>;\n\tdelete: (id: string) => Promise<void>;\n\t/**\n\t * Create or update so the (name, type) record matches `spec`. The\n\t * canonical \"point this DNS at this IP\" entry point — idempotent.\n\t */\n\tupsert: (spec: DnsRecordSpec) => Promise<DnsRecord>;\n};\n\n/**\n * Compose a DNS provider with a cloud Target's IPv4. Idempotently\n * ensures an A record (name → target.ipv4) exists. Returns the\n * resulting DNS record.\n *\n * Usage:\n *\n * const target = await digitalOceanTarget({ ... });\n * const dns = cloudflareProvider({ token, zoneId });\n * await ensureDnsForTarget(dns, {\n * name: 'api.example.com',\n * target,\n * ttl: 60,\n * proxied: false,\n * });\n */\nexport const ensureDnsForTarget = async (\n\tprovider: DnsProvider,\n\toptions: {\n\t\t/** Record name (FQDN, relative, or `'@'` for zone apex). */\n\t\tname: string;\n\t\t/** Cloud Target with an `ipv4` field. */\n\t\ttarget: { ipv4: string };\n\t\t/** Default 300s. */\n\t\tttl?: number;\n\t\t/** Cloudflare orange-cloud. Other providers ignore. */\n\t\tproxied?: boolean;\n\t\t/** Optional record comment for audit trails. */\n\t\tcomment?: string;\n\t}\n): Promise<DnsRecord> =>\n\tprovider.upsert({\n\t\tcontent: options.target.ipv4,\n\t\tname: options.name,\n\t\ttype: 'A',\n\t\t...(options.ttl !== undefined ? { ttl: options.ttl } : {}),\n\t\t...(options.proxied !== undefined ? { proxied: options.proxied } : {}),\n\t\t...(options.comment !== undefined ? { comment: options.comment } : {})\n\t});\n"
|
|
6
|
+
],
|
|
7
|
+
"mappings": ";;AA6FO,IAAM,qBAAqB,OACjC,UACA,YAaA,SAAS,OAAO;AAAA,EACf,SAAS,QAAQ,OAAO;AAAA,EACxB,MAAM,QAAQ;AAAA,EACd,MAAM;AAAA,KACF,QAAQ,QAAQ,YAAY,EAAE,KAAK,QAAQ,IAAI,IAAI,CAAC;AAAA,KACpD,QAAQ,YAAY,YAAY,EAAE,SAAS,QAAQ,QAAQ,IAAI,CAAC;AAAA,KAChE,QAAQ,YAAY,YAAY,EAAE,SAAS,QAAQ,QAAQ,IAAI,CAAC;AACrE,CAAC;",
|
|
8
|
+
"debugId": "9927EACBFED2D7E864756E2164756E21",
|
|
9
|
+
"names": []
|
|
10
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@absolutejs/deploy",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Generic Bun-project deploy pipeline. A Target (localTarget / sshTarget) is anywhere you can exec + upload — DigitalOcean droplets, Linode, Hetzner, Vultr, your own boxes. Bundled pipeline: prepare → upload → install → build → link → restart → verify. Atomic symlink swap, release history, prune, hooks. SSH shells out to system ssh/rsync — zero ssh2 deps.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"release"
|
|
27
27
|
],
|
|
28
28
|
"scripts": {
|
|
29
|
-
"build": "rm -rf dist && bun build src/index.ts src/digitalocean.ts src/hetzner.ts --outdir dist --sourcemap --target=bun && tsc --project tsconfig.build.json",
|
|
29
|
+
"build": "rm -rf dist && bun build src/index.ts src/digitalocean.ts src/hetzner.ts src/dns.ts src/cloudflare.ts --outdir dist --sourcemap --target=bun && tsc --project tsconfig.build.json",
|
|
30
30
|
"test": "bun test tests/",
|
|
31
31
|
"typecheck": "tsc --noEmit",
|
|
32
32
|
"format": "prettier --write \"./**/*.{ts,json,md}\"",
|
|
@@ -56,6 +56,16 @@
|
|
|
56
56
|
"types": "./dist/hetzner.d.ts",
|
|
57
57
|
"import": "./dist/hetzner.js",
|
|
58
58
|
"default": "./dist/hetzner.js"
|
|
59
|
+
},
|
|
60
|
+
"./dns": {
|
|
61
|
+
"types": "./dist/dns.d.ts",
|
|
62
|
+
"import": "./dist/dns.js",
|
|
63
|
+
"default": "./dist/dns.js"
|
|
64
|
+
},
|
|
65
|
+
"./cloudflare": {
|
|
66
|
+
"types": "./dist/cloudflare.d.ts",
|
|
67
|
+
"import": "./dist/cloudflare.js",
|
|
68
|
+
"default": "./dist/cloudflare.js"
|
|
59
69
|
}
|
|
60
70
|
},
|
|
61
71
|
"files": ["dist", "README.md"]
|