@absolutejs/deploy 0.4.0 → 0.5.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 +60 -2
- package/dist/tls.d.ts +107 -0
- package/dist/tls.js +444 -0
- package/dist/tls.js.map +10 -0
- package/package.json +7 -2
package/README.md
CHANGED
|
@@ -229,6 +229,64 @@ success).
|
|
|
229
229
|
The same `DnsProvider` contract applies to other providers — Route
|
|
230
230
|
53 / DigitalOcean DNS / etc. follow next.
|
|
231
231
|
|
|
232
|
+
## `@absolutejs/deploy/tls` — Let's Encrypt automation (0.5.0)
|
|
233
|
+
|
|
234
|
+
The last step. After provisioning a Target and pointing DNS at it,
|
|
235
|
+
`issueCertificate(...)` drives the full ACME-DNS-01 flow against
|
|
236
|
+
Let's Encrypt: account registration, new order, DNS-01 challenge
|
|
237
|
+
via the same `DnsProvider` you used for DNS, polling, CSR
|
|
238
|
+
finalize, cert download. Then `installCertificateOnTarget(...)`
|
|
239
|
+
uploads the PEM files to the box.
|
|
240
|
+
|
|
241
|
+
Zero third-party ACME / JOSE deps — RFC 8555 implemented directly
|
|
242
|
+
against Bun's `crypto.subtle`. The audit surface stays in this
|
|
243
|
+
repo.
|
|
244
|
+
|
|
245
|
+
```ts
|
|
246
|
+
import { hetznerTarget } from '@absolutejs/deploy/hetzner';
|
|
247
|
+
import { cloudflareProvider } from '@absolutejs/deploy/cloudflare';
|
|
248
|
+
import { ensureDnsForTarget } from '@absolutejs/deploy/dns';
|
|
249
|
+
import {
|
|
250
|
+
issueCertificate,
|
|
251
|
+
installCertificateOnTarget,
|
|
252
|
+
LETSENCRYPT_PRODUCTION,
|
|
253
|
+
} from '@absolutejs/deploy/tls';
|
|
254
|
+
|
|
255
|
+
const target = await hetznerTarget({ /* … */ });
|
|
256
|
+
const dns = cloudflareProvider({
|
|
257
|
+
token: process.env.CLOUDFLARE_TOKEN!,
|
|
258
|
+
zoneId: process.env.CLOUDFLARE_ZONE_ID!,
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
// 1. Point DNS at the box.
|
|
262
|
+
await ensureDnsForTarget(dns, { name: 'api.example.com', target, ttl: 60 });
|
|
263
|
+
|
|
264
|
+
// 2. Issue a cert via DNS-01.
|
|
265
|
+
const cert = await issueCertificate({
|
|
266
|
+
domains: ['api.example.com'],
|
|
267
|
+
dnsProvider: dns,
|
|
268
|
+
email: 'ops@example.com',
|
|
269
|
+
directoryUrl: LETSENCRYPT_PRODUCTION,
|
|
270
|
+
onLog: (line) => console.log(line),
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
// 3. Install on the box.
|
|
274
|
+
await installCertificateOnTarget(target, cert, {
|
|
275
|
+
reload: 'systemctl reload nginx',
|
|
276
|
+
});
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
`generateAccountKey()` / `exportAccount()` / `importAccount()`
|
|
280
|
+
round-trip the ECDSA P-256 keypair + `kid` so cert renewals reuse
|
|
281
|
+
the same account (avoids Let's Encrypt's account-creation rate
|
|
282
|
+
limit). Persist the JSON; pass `account` back to subsequent
|
|
283
|
+
`issueCertificate` calls.
|
|
284
|
+
|
|
285
|
+
`installCertificateOnTarget`'s defaults:
|
|
286
|
+
`/etc/ssl/<domain>/fullchain.pem` + `/etc/ssl/<domain>/privkey.pem`,
|
|
287
|
+
mode `600`. Override `certPath` / `keyPath` / `mode` / `owner` /
|
|
288
|
+
`reload` as needed.
|
|
289
|
+
|
|
232
290
|
## DigitalOcean Droplet — first deploy (manual)
|
|
233
291
|
|
|
234
292
|
Assuming a fresh Ubuntu/Debian Droplet:
|
|
@@ -248,11 +306,11 @@ bun run my-deploy-script.ts
|
|
|
248
306
|
|
|
249
307
|
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.
|
|
250
308
|
|
|
251
|
-
## What v0.
|
|
309
|
+
## What v0.5.0 does NOT include
|
|
252
310
|
|
|
253
|
-
- TLS certificate automation. ACME / Let's Encrypt is a separate concern — coming next.
|
|
254
311
|
- Cloud-provider compute targets beyond DigitalOcean + Hetzner. Linode / Vultr / Fly Machines follow the same shape and are next on the list.
|
|
255
312
|
- DNS providers beyond Cloudflare. Route 53 / DigitalOcean DNS / Hetzner DNS slot into the same `DnsProvider` contract.
|
|
313
|
+
- Cert renewal scheduling — `issueCertificate` is one-shot; wire it to a cron / scheduled-function and pass the persisted account JSON back in to renew. (Convention: renew at 30 days remaining.)
|
|
256
314
|
- Bun installation on the remote — caller does it once, out of band.
|
|
257
315
|
- Multi-target / fan-out deploys (caller iterates).
|
|
258
316
|
- 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.
|
package/dist/tls.d.ts
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @absolutejs/deploy/tls — ACME (RFC 8555) client for Let's Encrypt and
|
|
3
|
+
* compatible CAs. DNS-01 challenges only — uses the {@link DnsProvider}
|
|
4
|
+
* abstraction from `./dns`, so the same Cloudflare / Route 53 /
|
|
5
|
+
* Hetzner-DNS providers that point hostnames at boxes also satisfy
|
|
6
|
+
* ACME's challenge requirement.
|
|
7
|
+
*
|
|
8
|
+
* Zero peer deps. Bun's `crypto.subtle` covers JWS signing and ECDSA
|
|
9
|
+
* key generation; a small DER encoder builds the CSR.
|
|
10
|
+
*
|
|
11
|
+
* Public API:
|
|
12
|
+
*
|
|
13
|
+
* issueCertificate({ domains, dnsProvider, email })
|
|
14
|
+
* → { certificatePem, privateKeyPem, account, domains }
|
|
15
|
+
*
|
|
16
|
+
* installCertificateOnTarget(target, cert, { certPath, keyPath, reload? })
|
|
17
|
+
* → uploads PEM files via Target.upload + optional reload exec
|
|
18
|
+
*
|
|
19
|
+
* exportAccountKey / importAccountKey for persistence between runs
|
|
20
|
+
* (reuse the same account across cert renewals — cheaper, doesn't
|
|
21
|
+
* hit Let's Encrypt's account-creation rate limit)
|
|
22
|
+
*/
|
|
23
|
+
import type { Target } from './targets';
|
|
24
|
+
import type { DnsProvider } from './dns';
|
|
25
|
+
export declare const LETSENCRYPT_PRODUCTION = "https://acme-v02.api.letsencrypt.org/directory";
|
|
26
|
+
export declare const LETSENCRYPT_STAGING = "https://acme-staging-v02.api.letsencrypt.org/directory";
|
|
27
|
+
export type AcmeAccount = {
|
|
28
|
+
key: CryptoKeyPair;
|
|
29
|
+
/** Account URL — set after first registration; persisted for renewals. */
|
|
30
|
+
kid?: string;
|
|
31
|
+
};
|
|
32
|
+
export type AcmeAccountJson = {
|
|
33
|
+
publicJwk: JsonWebKey;
|
|
34
|
+
privateJwk: JsonWebKey;
|
|
35
|
+
kid?: string;
|
|
36
|
+
};
|
|
37
|
+
export declare const generateAccountKey: () => Promise<AcmeAccount>;
|
|
38
|
+
export declare const exportAccount: (account: AcmeAccount) => Promise<AcmeAccountJson>;
|
|
39
|
+
export declare const importAccount: (json: AcmeAccountJson) => Promise<AcmeAccount>;
|
|
40
|
+
export type IssueCertificateOptions = {
|
|
41
|
+
/** Domain(s) to include. First is the CN; all are SANs. */
|
|
42
|
+
domains: string[];
|
|
43
|
+
/** DNS provider used for DNS-01 challenges (must own the zone). */
|
|
44
|
+
dnsProvider: DnsProvider;
|
|
45
|
+
/** Contact email for the ACME account. */
|
|
46
|
+
email: string;
|
|
47
|
+
/** ACME directory URL. Default Let's Encrypt production. */
|
|
48
|
+
directoryUrl?: string;
|
|
49
|
+
/** Reuse an existing account. Pass `exportAccount`'s output via `importAccount`. */
|
|
50
|
+
account?: AcmeAccount;
|
|
51
|
+
/** Override fetch (tests). Default global fetch. */
|
|
52
|
+
fetch?: typeof fetch;
|
|
53
|
+
/** Poll interval. Default 3 s. */
|
|
54
|
+
pollIntervalMs?: number;
|
|
55
|
+
/**
|
|
56
|
+
* Max wait before notifying ACME that the DNS challenge is ready.
|
|
57
|
+
* Set ~30-60s for Cloudflare; longer for slower providers. Default 30 s.
|
|
58
|
+
*/
|
|
59
|
+
dnsPropagationDelayMs?: number;
|
|
60
|
+
/** Max wait for the order to become valid. Default 5 min. */
|
|
61
|
+
orderTimeoutMs?: number;
|
|
62
|
+
/** Status log lines. */
|
|
63
|
+
onLog?: (line: string) => void;
|
|
64
|
+
/** Override sleep (tests). */
|
|
65
|
+
sleep?: (ms: number) => Promise<void>;
|
|
66
|
+
/**
|
|
67
|
+
* Optional pre-check: returns true when DNS has propagated globally.
|
|
68
|
+
* Default: just wait `dnsPropagationDelayMs` then proceed. Override
|
|
69
|
+
* for production to actually verify (e.g. resolve from multiple
|
|
70
|
+
* public resolvers).
|
|
71
|
+
*/
|
|
72
|
+
checkDnsPropagated?: (recordName: string, expectedValue: string) => Promise<boolean>;
|
|
73
|
+
};
|
|
74
|
+
export type IssuedCertificate = {
|
|
75
|
+
certificatePem: string;
|
|
76
|
+
privateKeyPem: string;
|
|
77
|
+
account: AcmeAccount;
|
|
78
|
+
domains: string[];
|
|
79
|
+
};
|
|
80
|
+
export declare class AcmeError extends Error {
|
|
81
|
+
readonly status: number;
|
|
82
|
+
readonly body: unknown;
|
|
83
|
+
constructor(message: string, status: number, body: unknown);
|
|
84
|
+
}
|
|
85
|
+
export declare const issueCertificate: (options: IssueCertificateOptions) => Promise<IssuedCertificate>;
|
|
86
|
+
export type InstallCertificateOptions = {
|
|
87
|
+
/** Remote path for the cert chain. Default `/etc/ssl/<firstDomain>/fullchain.pem`. */
|
|
88
|
+
certPath?: string;
|
|
89
|
+
/** Remote path for the private key. Default `/etc/ssl/<firstDomain>/privkey.pem`. */
|
|
90
|
+
keyPath?: string;
|
|
91
|
+
/** Mode (chmod) for the cert + key. Default `600`. */
|
|
92
|
+
mode?: string;
|
|
93
|
+
/** Owner (chown) for the cert + key. Default unchanged. */
|
|
94
|
+
owner?: string;
|
|
95
|
+
/** Optional reload command run after install (e.g. `'systemctl reload nginx'`). */
|
|
96
|
+
reload?: string;
|
|
97
|
+
/** Override the writeTo helper (tests). */
|
|
98
|
+
writeFile?: (target: Target, path: string, contents: string) => Promise<void>;
|
|
99
|
+
};
|
|
100
|
+
/**
|
|
101
|
+
* Upload cert + private key to the target. Composable with the deploy
|
|
102
|
+
* pipeline as a verify-step or post-deploy hook.
|
|
103
|
+
*/
|
|
104
|
+
export declare const installCertificateOnTarget: (target: Target, cert: IssuedCertificate, options?: InstallCertificateOptions) => Promise<{
|
|
105
|
+
certPath: string;
|
|
106
|
+
keyPath: string;
|
|
107
|
+
}>;
|
package/dist/tls.js
ADDED
|
@@ -0,0 +1,444 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// src/tls.ts
|
|
3
|
+
var LETSENCRYPT_PRODUCTION = "https://acme-v02.api.letsencrypt.org/directory";
|
|
4
|
+
var LETSENCRYPT_STAGING = "https://acme-staging-v02.api.letsencrypt.org/directory";
|
|
5
|
+
var base64UrlEncode = (bytes) => {
|
|
6
|
+
const buf = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);
|
|
7
|
+
let binary = "";
|
|
8
|
+
for (const byte of buf)
|
|
9
|
+
binary += String.fromCharCode(byte);
|
|
10
|
+
return btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replaceAll("=", "");
|
|
11
|
+
};
|
|
12
|
+
var base64UrlEncodeString = (value) => base64UrlEncode(new TextEncoder().encode(value));
|
|
13
|
+
var derLength = (length) => {
|
|
14
|
+
if (length < 128)
|
|
15
|
+
return Uint8Array.from([length]);
|
|
16
|
+
const bytes = [];
|
|
17
|
+
let remaining = length;
|
|
18
|
+
while (remaining > 0) {
|
|
19
|
+
bytes.unshift(remaining & 255);
|
|
20
|
+
remaining >>= 8;
|
|
21
|
+
}
|
|
22
|
+
return Uint8Array.from([128 | bytes.length, ...bytes]);
|
|
23
|
+
};
|
|
24
|
+
var derTag = (tag, payload) => {
|
|
25
|
+
const length = derLength(payload.length);
|
|
26
|
+
const out = new Uint8Array(1 + length.length + payload.length);
|
|
27
|
+
out[0] = tag;
|
|
28
|
+
out.set(length, 1);
|
|
29
|
+
out.set(payload, 1 + length.length);
|
|
30
|
+
return out;
|
|
31
|
+
};
|
|
32
|
+
var derSeq = (children) => {
|
|
33
|
+
const total = children.reduce((sum, child) => sum + child.length, 0);
|
|
34
|
+
const payload = new Uint8Array(total);
|
|
35
|
+
let offset = 0;
|
|
36
|
+
for (const child of children) {
|
|
37
|
+
payload.set(child, offset);
|
|
38
|
+
offset += child.length;
|
|
39
|
+
}
|
|
40
|
+
return derTag(48, payload);
|
|
41
|
+
};
|
|
42
|
+
var derSet = (children) => {
|
|
43
|
+
const total = children.reduce((sum, child) => sum + child.length, 0);
|
|
44
|
+
const payload = new Uint8Array(total);
|
|
45
|
+
let offset = 0;
|
|
46
|
+
for (const child of children) {
|
|
47
|
+
payload.set(child, offset);
|
|
48
|
+
offset += child.length;
|
|
49
|
+
}
|
|
50
|
+
return derTag(49, payload);
|
|
51
|
+
};
|
|
52
|
+
var derInt = (value) => {
|
|
53
|
+
if (typeof value === "number") {
|
|
54
|
+
if (value === 0)
|
|
55
|
+
return derTag(2, Uint8Array.from([0]));
|
|
56
|
+
const bytes = [];
|
|
57
|
+
let remaining = value;
|
|
58
|
+
while (remaining > 0) {
|
|
59
|
+
bytes.unshift(remaining & 255);
|
|
60
|
+
remaining >>= 8;
|
|
61
|
+
}
|
|
62
|
+
if (bytes[0] & 128)
|
|
63
|
+
bytes.unshift(0);
|
|
64
|
+
return derTag(2, Uint8Array.from(bytes));
|
|
65
|
+
}
|
|
66
|
+
let start = 0;
|
|
67
|
+
while (start < value.length - 1 && value[start] === 0)
|
|
68
|
+
start += 1;
|
|
69
|
+
let payload = value.subarray(start);
|
|
70
|
+
if (payload[0] & 128) {
|
|
71
|
+
const padded = new Uint8Array(payload.length + 1);
|
|
72
|
+
padded.set(payload, 1);
|
|
73
|
+
payload = padded;
|
|
74
|
+
}
|
|
75
|
+
return derTag(2, payload);
|
|
76
|
+
};
|
|
77
|
+
var derOid = (oid) => {
|
|
78
|
+
const parts = oid.split(".").map((part) => Number.parseInt(part, 10));
|
|
79
|
+
const first = parts[0];
|
|
80
|
+
const second = parts[1];
|
|
81
|
+
if (first === undefined || second === undefined) {
|
|
82
|
+
throw new Error(`[deploy/tls] invalid OID: ${oid}`);
|
|
83
|
+
}
|
|
84
|
+
const bytes = [first * 40 + second];
|
|
85
|
+
for (let index = 2;index < parts.length; index += 1) {
|
|
86
|
+
const value = parts[index];
|
|
87
|
+
const chunks = [];
|
|
88
|
+
let remaining = value;
|
|
89
|
+
do {
|
|
90
|
+
chunks.unshift(remaining & 127);
|
|
91
|
+
remaining >>= 7;
|
|
92
|
+
} while (remaining > 0);
|
|
93
|
+
for (let chunkIndex = 0;chunkIndex < chunks.length - 1; chunkIndex += 1) {
|
|
94
|
+
chunks[chunkIndex] = chunks[chunkIndex] | 128;
|
|
95
|
+
}
|
|
96
|
+
bytes.push(...chunks);
|
|
97
|
+
}
|
|
98
|
+
return derTag(6, Uint8Array.from(bytes));
|
|
99
|
+
};
|
|
100
|
+
var derUtf8String = (value) => derTag(12, new TextEncoder().encode(value));
|
|
101
|
+
var derOctetString = (payload) => derTag(4, payload);
|
|
102
|
+
var derBitString = (payload) => {
|
|
103
|
+
const bits = new Uint8Array(payload.length + 1);
|
|
104
|
+
bits[0] = 0;
|
|
105
|
+
bits.set(payload, 1);
|
|
106
|
+
return derTag(3, bits);
|
|
107
|
+
};
|
|
108
|
+
var derContextTag = (tag, payload, constructed = true) => derTag(160 + tag + (constructed ? 0 : -32), payload);
|
|
109
|
+
var ecdsaRawToDer = (rawSig) => {
|
|
110
|
+
const half = rawSig.length / 2;
|
|
111
|
+
const r = rawSig.subarray(0, half);
|
|
112
|
+
const s = rawSig.subarray(half);
|
|
113
|
+
return derSeq([derInt(r), derInt(s)]);
|
|
114
|
+
};
|
|
115
|
+
var signJws = async (privateKey, header, payload) => {
|
|
116
|
+
const protectedHeader = base64UrlEncodeString(JSON.stringify(header));
|
|
117
|
+
const payloadEncoded = payload === "" ? "" : base64UrlEncodeString(JSON.stringify(payload));
|
|
118
|
+
const signingInput = new TextEncoder().encode(`${protectedHeader}.${payloadEncoded}`);
|
|
119
|
+
const rawSig = new Uint8Array(await crypto.subtle.sign({ hash: "SHA-256", name: "ECDSA" }, privateKey, signingInput));
|
|
120
|
+
return {
|
|
121
|
+
payload: payloadEncoded,
|
|
122
|
+
protected: protectedHeader,
|
|
123
|
+
signature: base64UrlEncode(rawSig)
|
|
124
|
+
};
|
|
125
|
+
};
|
|
126
|
+
var exportPublicJwk = async (publicKey) => {
|
|
127
|
+
const jwk = await crypto.subtle.exportKey("jwk", publicKey);
|
|
128
|
+
return {
|
|
129
|
+
crv: jwk.crv,
|
|
130
|
+
kty: jwk.kty,
|
|
131
|
+
x: jwk.x,
|
|
132
|
+
y: jwk.y
|
|
133
|
+
};
|
|
134
|
+
};
|
|
135
|
+
var jwkThumbprint = async (publicJwk) => {
|
|
136
|
+
const canonical = JSON.stringify({
|
|
137
|
+
crv: publicJwk.crv,
|
|
138
|
+
kty: publicJwk.kty,
|
|
139
|
+
x: publicJwk.x,
|
|
140
|
+
y: publicJwk.y
|
|
141
|
+
});
|
|
142
|
+
const hash = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(canonical));
|
|
143
|
+
return base64UrlEncode(hash);
|
|
144
|
+
};
|
|
145
|
+
var generateAccountKey = async () => {
|
|
146
|
+
const key = await crypto.subtle.generateKey({ name: "ECDSA", namedCurve: "P-256" }, true, ["sign", "verify"]);
|
|
147
|
+
return { key };
|
|
148
|
+
};
|
|
149
|
+
var exportAccount = async (account) => {
|
|
150
|
+
const publicJwk = await crypto.subtle.exportKey("jwk", account.key.publicKey);
|
|
151
|
+
const privateJwk = await crypto.subtle.exportKey("jwk", account.key.privateKey);
|
|
152
|
+
return {
|
|
153
|
+
privateJwk,
|
|
154
|
+
publicJwk,
|
|
155
|
+
...account.kid !== undefined ? { kid: account.kid } : {}
|
|
156
|
+
};
|
|
157
|
+
};
|
|
158
|
+
var importAccount = async (json) => {
|
|
159
|
+
const publicKey = await crypto.subtle.importKey("jwk", json.publicJwk, { name: "ECDSA", namedCurve: "P-256" }, true, ["verify"]);
|
|
160
|
+
const privateKey = await crypto.subtle.importKey("jwk", json.privateJwk, { name: "ECDSA", namedCurve: "P-256" }, true, ["sign"]);
|
|
161
|
+
return {
|
|
162
|
+
key: { privateKey, publicKey },
|
|
163
|
+
...json.kid !== undefined ? { kid: json.kid } : {}
|
|
164
|
+
};
|
|
165
|
+
};
|
|
166
|
+
var buildSanExtension = (domains) => {
|
|
167
|
+
const generalNames = domains.map((domain) => derTag(130, new TextEncoder().encode(domain)));
|
|
168
|
+
const sanSequence = derSeq(generalNames);
|
|
169
|
+
const extensionValue = derOctetString(sanSequence);
|
|
170
|
+
return derSeq([derOid("2.5.29.17"), extensionValue]);
|
|
171
|
+
};
|
|
172
|
+
var buildExtensionRequestAttribute = (domains) => {
|
|
173
|
+
const extensions = derSeq([buildSanExtension(domains)]);
|
|
174
|
+
return derSeq([
|
|
175
|
+
derOid("1.2.840.113549.1.9.14"),
|
|
176
|
+
derSet([extensions])
|
|
177
|
+
]);
|
|
178
|
+
};
|
|
179
|
+
var generateCertKeyPair = async () => crypto.subtle.generateKey({ name: "ECDSA", namedCurve: "P-256" }, true, ["sign", "verify"]);
|
|
180
|
+
var buildCsr = async (domains, keypair) => {
|
|
181
|
+
if (domains.length === 0) {
|
|
182
|
+
throw new Error("[deploy/tls] CSR requires at least one domain");
|
|
183
|
+
}
|
|
184
|
+
const commonName = domains[0];
|
|
185
|
+
const spkiDer = new Uint8Array(await crypto.subtle.exportKey("spki", keypair.publicKey));
|
|
186
|
+
const version = derInt(0);
|
|
187
|
+
const subject = derSeq([
|
|
188
|
+
derSet([
|
|
189
|
+
derSeq([
|
|
190
|
+
derOid("2.5.4.3"),
|
|
191
|
+
derUtf8String(commonName)
|
|
192
|
+
])
|
|
193
|
+
])
|
|
194
|
+
]);
|
|
195
|
+
const attributes = derContextTag(0, buildExtensionRequestAttribute(domains));
|
|
196
|
+
const certificationRequestInfo = derSeq([
|
|
197
|
+
version,
|
|
198
|
+
subject,
|
|
199
|
+
spkiDer,
|
|
200
|
+
attributes
|
|
201
|
+
]);
|
|
202
|
+
const rawSig = new Uint8Array(await crypto.subtle.sign({ hash: "SHA-256", name: "ECDSA" }, keypair.privateKey, certificationRequestInfo));
|
|
203
|
+
const sigDer = ecdsaRawToDer(rawSig);
|
|
204
|
+
const sigAlgorithm = derSeq([derOid("1.2.840.10045.4.3.2")]);
|
|
205
|
+
const signatureBits = derBitString(sigDer);
|
|
206
|
+
return derSeq([certificationRequestInfo, sigAlgorithm, signatureBits]);
|
|
207
|
+
};
|
|
208
|
+
var derToPem = (label, der) => {
|
|
209
|
+
const b64 = btoa(String.fromCharCode(...der));
|
|
210
|
+
const lines = b64.match(/.{1,64}/g) ?? [];
|
|
211
|
+
return `-----BEGIN ${label}-----
|
|
212
|
+
${lines.join(`
|
|
213
|
+
`)}
|
|
214
|
+
-----END ${label}-----
|
|
215
|
+
`;
|
|
216
|
+
};
|
|
217
|
+
var exportEcPrivateKeyPem = async (keypair) => {
|
|
218
|
+
const pkcs8 = new Uint8Array(await crypto.subtle.exportKey("pkcs8", keypair.privateKey));
|
|
219
|
+
return derToPem("PRIVATE KEY", pkcs8);
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
class AcmeError extends Error {
|
|
223
|
+
status;
|
|
224
|
+
body;
|
|
225
|
+
constructor(message, status, body) {
|
|
226
|
+
super(message);
|
|
227
|
+
this.name = "AcmeError";
|
|
228
|
+
this.status = status;
|
|
229
|
+
this.body = body;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
var defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
233
|
+
var issueCertificate = async (options) => {
|
|
234
|
+
const log = options.onLog ?? (() => {});
|
|
235
|
+
const sleep = options.sleep ?? defaultSleep;
|
|
236
|
+
const fetcher = options.fetch ?? fetch;
|
|
237
|
+
const directoryUrl = options.directoryUrl ?? LETSENCRYPT_PRODUCTION;
|
|
238
|
+
const pollMs = options.pollIntervalMs ?? 3000;
|
|
239
|
+
const dnsDelay = options.dnsPropagationDelayMs ?? 30000;
|
|
240
|
+
const orderTimeout = options.orderTimeoutMs ?? 5 * 60000;
|
|
241
|
+
const account = options.account ?? await generateAccountKey();
|
|
242
|
+
if (options.domains.length === 0) {
|
|
243
|
+
throw new Error("[deploy/tls] at least one domain is required");
|
|
244
|
+
}
|
|
245
|
+
log(`[tls] fetching ACME directory ${directoryUrl}`);
|
|
246
|
+
const directoryResponse = await fetcher(directoryUrl);
|
|
247
|
+
if (!directoryResponse.ok) {
|
|
248
|
+
throw new AcmeError(`failed to fetch ACME directory: ${directoryResponse.status}`, directoryResponse.status, await directoryResponse.text());
|
|
249
|
+
}
|
|
250
|
+
const directory = await directoryResponse.json();
|
|
251
|
+
let nonce = await fetchNonce(fetcher, directory.newNonce);
|
|
252
|
+
const post = async (url, payload, identification) => {
|
|
253
|
+
const header = {
|
|
254
|
+
alg: "ES256",
|
|
255
|
+
nonce,
|
|
256
|
+
url,
|
|
257
|
+
...identification.kid !== undefined ? { kid: identification.kid } : {},
|
|
258
|
+
...identification.jwk !== undefined ? { jwk: identification.jwk } : {}
|
|
259
|
+
};
|
|
260
|
+
const jws = await signJws(account.key.privateKey, header, payload);
|
|
261
|
+
const response = await fetcher(url, {
|
|
262
|
+
body: JSON.stringify(jws),
|
|
263
|
+
headers: { "content-type": "application/jose+json" },
|
|
264
|
+
method: "POST"
|
|
265
|
+
});
|
|
266
|
+
const newNonce = response.headers.get("replay-nonce");
|
|
267
|
+
if (newNonce !== null)
|
|
268
|
+
nonce = newNonce;
|
|
269
|
+
const text = await response.text();
|
|
270
|
+
const body = text.length > 0 && response.headers.get("content-type")?.includes("json") ? JSON.parse(text) : text;
|
|
271
|
+
if (!response.ok) {
|
|
272
|
+
throw new AcmeError(`ACME ${url} \u2192 ${response.status}`, response.status, body);
|
|
273
|
+
}
|
|
274
|
+
return { body, headers: response.headers, status: response.status };
|
|
275
|
+
};
|
|
276
|
+
const publicJwk = await exportPublicJwk(account.key.publicKey);
|
|
277
|
+
if (account.kid === undefined) {
|
|
278
|
+
log(`[tls] registering ACME account for ${options.email}`);
|
|
279
|
+
const result = await post(directory.newAccount, {
|
|
280
|
+
contact: [`mailto:${options.email}`],
|
|
281
|
+
termsOfServiceAgreed: true
|
|
282
|
+
}, { jwk: publicJwk });
|
|
283
|
+
account.kid = result.headers.get("location") ?? undefined;
|
|
284
|
+
if (account.kid === undefined) {
|
|
285
|
+
throw new Error("[deploy/tls] ACME newAccount response missing Location header");
|
|
286
|
+
}
|
|
287
|
+
log(`[tls] account registered: ${account.kid}`);
|
|
288
|
+
} else {
|
|
289
|
+
log(`[tls] reusing account ${account.kid}`);
|
|
290
|
+
}
|
|
291
|
+
const accountKid = account.kid;
|
|
292
|
+
log(`[tls] submitting order for ${options.domains.join(", ")}`);
|
|
293
|
+
const orderResult = await post(directory.newOrder, {
|
|
294
|
+
identifiers: options.domains.map((domain) => ({
|
|
295
|
+
type: "dns",
|
|
296
|
+
value: domain
|
|
297
|
+
}))
|
|
298
|
+
}, { kid: accountKid });
|
|
299
|
+
let order = orderResult.body;
|
|
300
|
+
const orderUrl = orderResult.headers.get("location");
|
|
301
|
+
if (orderUrl === null) {
|
|
302
|
+
throw new Error("[deploy/tls] ACME newOrder response missing Location header");
|
|
303
|
+
}
|
|
304
|
+
const cleanups = [];
|
|
305
|
+
const dnsCreated = [];
|
|
306
|
+
try {
|
|
307
|
+
const thumbprint = await jwkThumbprint(publicJwk);
|
|
308
|
+
for (const authUrl of order.authorizations) {
|
|
309
|
+
const authResult = await post(authUrl, "", { kid: accountKid });
|
|
310
|
+
const auth = authResult.body;
|
|
311
|
+
const challenge = auth.challenges.find((c) => c.type === "dns-01");
|
|
312
|
+
if (challenge === undefined) {
|
|
313
|
+
throw new Error(`[deploy/tls] no dns-01 challenge for ${auth.identifier.value}`);
|
|
314
|
+
}
|
|
315
|
+
const keyAuthorization = `${challenge.token}.${thumbprint}`;
|
|
316
|
+
const txtBytes = new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(keyAuthorization)));
|
|
317
|
+
const txtValue = base64UrlEncode(txtBytes);
|
|
318
|
+
const recordName = `_acme-challenge.${auth.identifier.value}`;
|
|
319
|
+
log(`[tls] DNS-01: setting ${recordName} = "${txtValue}"`);
|
|
320
|
+
const record = await options.dnsProvider.upsert({
|
|
321
|
+
content: txtValue,
|
|
322
|
+
name: recordName,
|
|
323
|
+
ttl: 60,
|
|
324
|
+
type: "TXT"
|
|
325
|
+
});
|
|
326
|
+
dnsCreated.push({
|
|
327
|
+
recordId: record.id,
|
|
328
|
+
recordName,
|
|
329
|
+
recordValue: txtValue
|
|
330
|
+
});
|
|
331
|
+
cleanups.push(() => options.dnsProvider.delete(record.id));
|
|
332
|
+
if (options.checkDnsPropagated !== undefined) {
|
|
333
|
+
log("[tls] waiting for DNS propagation (custom checker)\u2026");
|
|
334
|
+
const deadline = Date.now() + dnsDelay;
|
|
335
|
+
while (Date.now() < deadline) {
|
|
336
|
+
if (await options.checkDnsPropagated(recordName, txtValue))
|
|
337
|
+
break;
|
|
338
|
+
await sleep(pollMs);
|
|
339
|
+
}
|
|
340
|
+
} else {
|
|
341
|
+
log(`[tls] sleeping ${dnsDelay}ms for DNS propagation`);
|
|
342
|
+
await sleep(dnsDelay);
|
|
343
|
+
}
|
|
344
|
+
log(`[tls] notifying ACME of dns-01 readiness for ${auth.identifier.value}`);
|
|
345
|
+
await post(challenge.url, {}, { kid: accountKid });
|
|
346
|
+
const authDeadline = Date.now() + orderTimeout;
|
|
347
|
+
while (Date.now() < authDeadline) {
|
|
348
|
+
const polledResult = await post(authUrl, "", { kid: accountKid });
|
|
349
|
+
const polled = polledResult.body;
|
|
350
|
+
if (polled.status === "valid")
|
|
351
|
+
break;
|
|
352
|
+
if (polled.status === "invalid") {
|
|
353
|
+
throw new Error(`[deploy/tls] authorization failed for ${auth.identifier.value}: ${JSON.stringify(polled.challenges)}`);
|
|
354
|
+
}
|
|
355
|
+
await sleep(pollMs);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
log("[tls] generating cert keypair + CSR");
|
|
359
|
+
const certKey = await generateCertKeyPair();
|
|
360
|
+
const csrDer = await buildCsr(options.domains, certKey);
|
|
361
|
+
const csr = base64UrlEncode(csrDer);
|
|
362
|
+
await post(order.finalize, { csr }, { kid: accountKid });
|
|
363
|
+
const orderDeadline = Date.now() + orderTimeout;
|
|
364
|
+
while (Date.now() < orderDeadline) {
|
|
365
|
+
const refreshed = await post(orderUrl, "", { kid: accountKid });
|
|
366
|
+
order = refreshed.body;
|
|
367
|
+
if (order.status === "valid" && order.certificate !== undefined)
|
|
368
|
+
break;
|
|
369
|
+
if (order.status === "invalid") {
|
|
370
|
+
throw new Error(`[deploy/tls] order failed: ${JSON.stringify(order)}`);
|
|
371
|
+
}
|
|
372
|
+
await sleep(pollMs);
|
|
373
|
+
}
|
|
374
|
+
if (order.certificate === undefined) {
|
|
375
|
+
throw new Error("[deploy/tls] order timed out before issuing certificate");
|
|
376
|
+
}
|
|
377
|
+
log(`[tls] downloading certificate from ${order.certificate}`);
|
|
378
|
+
const certResult = await post(order.certificate, "", { kid: accountKid });
|
|
379
|
+
const certificatePem = typeof certResult.body === "string" ? certResult.body : String(certResult.body);
|
|
380
|
+
const privateKeyPem = await exportEcPrivateKeyPem(certKey);
|
|
381
|
+
return {
|
|
382
|
+
account,
|
|
383
|
+
certificatePem,
|
|
384
|
+
domains: options.domains,
|
|
385
|
+
privateKeyPem
|
|
386
|
+
};
|
|
387
|
+
} finally {
|
|
388
|
+
for (const cleanup of cleanups) {
|
|
389
|
+
try {
|
|
390
|
+
await cleanup();
|
|
391
|
+
} catch (error) {
|
|
392
|
+
log(`[tls] cleanup failed (continuing): ${String(error)}`);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
};
|
|
397
|
+
var fetchNonce = async (fetcher, url) => {
|
|
398
|
+
const response = await fetcher(url, { method: "HEAD" });
|
|
399
|
+
const nonce = response.headers.get("replay-nonce");
|
|
400
|
+
if (nonce === null) {
|
|
401
|
+
throw new Error("[deploy/tls] newNonce response missing Replay-Nonce header");
|
|
402
|
+
}
|
|
403
|
+
return nonce;
|
|
404
|
+
};
|
|
405
|
+
var defaultWriteFile = async (target, remotePath, contents) => {
|
|
406
|
+
const escaped = contents.replaceAll("'", "'\\''");
|
|
407
|
+
await target.exec(`mkdir -p "$(dirname '${remotePath}')"`);
|
|
408
|
+
await target.exec(`cat > '${remotePath}' <<'__ABS_TLS_EOF__'
|
|
409
|
+
${contents}__ABS_TLS_EOF__
|
|
410
|
+
`);
|
|
411
|
+
};
|
|
412
|
+
var installCertificateOnTarget = async (target, cert, options = {}) => {
|
|
413
|
+
const domain = cert.domains[0];
|
|
414
|
+
if (domain === undefined) {
|
|
415
|
+
throw new Error("[deploy/tls] certificate has no domains");
|
|
416
|
+
}
|
|
417
|
+
const certPath = options.certPath ?? `/etc/ssl/${domain}/fullchain.pem`;
|
|
418
|
+
const keyPath = options.keyPath ?? `/etc/ssl/${domain}/privkey.pem`;
|
|
419
|
+
const mode = options.mode ?? "600";
|
|
420
|
+
const writer = options.writeFile ?? defaultWriteFile;
|
|
421
|
+
await writer(target, certPath, cert.certificatePem);
|
|
422
|
+
await writer(target, keyPath, cert.privateKeyPem);
|
|
423
|
+
await target.exec(`chmod ${mode} '${certPath}' '${keyPath}'`);
|
|
424
|
+
if (options.owner !== undefined) {
|
|
425
|
+
await target.exec(`chown ${options.owner} '${certPath}' '${keyPath}'`);
|
|
426
|
+
}
|
|
427
|
+
if (options.reload !== undefined) {
|
|
428
|
+
await target.exec(options.reload);
|
|
429
|
+
}
|
|
430
|
+
return { certPath, keyPath };
|
|
431
|
+
};
|
|
432
|
+
export {
|
|
433
|
+
issueCertificate,
|
|
434
|
+
installCertificateOnTarget,
|
|
435
|
+
importAccount,
|
|
436
|
+
generateAccountKey,
|
|
437
|
+
exportAccount,
|
|
438
|
+
LETSENCRYPT_STAGING,
|
|
439
|
+
LETSENCRYPT_PRODUCTION,
|
|
440
|
+
AcmeError
|
|
441
|
+
};
|
|
442
|
+
|
|
443
|
+
//# debugId=54D17E1F39C4D88964756E2164756E21
|
|
444
|
+
//# sourceMappingURL=tls.js.map
|
package/dist/tls.js.map
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/tls.ts"],
|
|
4
|
+
"sourcesContent": [
|
|
5
|
+
"/**\n * @absolutejs/deploy/tls — ACME (RFC 8555) client for Let's Encrypt and\n * compatible CAs. DNS-01 challenges only — uses the {@link DnsProvider}\n * abstraction from `./dns`, so the same Cloudflare / Route 53 /\n * Hetzner-DNS providers that point hostnames at boxes also satisfy\n * ACME's challenge requirement.\n *\n * Zero peer deps. Bun's `crypto.subtle` covers JWS signing and ECDSA\n * key generation; a small DER encoder builds the CSR.\n *\n * Public API:\n *\n * issueCertificate({ domains, dnsProvider, email })\n * → { certificatePem, privateKeyPem, account, domains }\n *\n * installCertificateOnTarget(target, cert, { certPath, keyPath, reload? })\n * → uploads PEM files via Target.upload + optional reload exec\n *\n * exportAccountKey / importAccountKey for persistence between runs\n * (reuse the same account across cert renewals — cheaper, doesn't\n * hit Let's Encrypt's account-creation rate limit)\n */\n\nimport type { Target } from './targets';\nimport type { DnsProvider } from './dns';\n\n// =============================================================================\n// Constants\n// =============================================================================\n\nexport const LETSENCRYPT_PRODUCTION =\n\t'https://acme-v02.api.letsencrypt.org/directory';\nexport const LETSENCRYPT_STAGING =\n\t'https://acme-staging-v02.api.letsencrypt.org/directory';\n\n// =============================================================================\n// base64url\n// =============================================================================\n\nconst base64UrlEncode = (bytes: Uint8Array | ArrayBuffer): string => {\n\tconst buf = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);\n\tlet binary = '';\n\tfor (const byte of buf) binary += String.fromCharCode(byte);\n\treturn btoa(binary).replaceAll('+', '-').replaceAll('/', '_').replaceAll('=', '');\n};\n\nconst base64UrlEncodeString = (value: string): string =>\n\tbase64UrlEncode(new TextEncoder().encode(value));\n\nconst base64UrlDecode = (value: string): Uint8Array => {\n\tconst padded = value.replaceAll('-', '+').replaceAll('_', '/').padEnd(\n\t\tMath.ceil(value.length / 4) * 4,\n\t\t'='\n\t);\n\tconst binary = atob(padded);\n\tconst bytes = new Uint8Array(binary.length);\n\tfor (let index = 0; index < binary.length; index += 1) {\n\t\tbytes[index] = binary.charCodeAt(index);\n\t}\n\treturn bytes;\n};\n\n// =============================================================================\n// DER encoding (minimal subset for CSR)\n// =============================================================================\n\nconst derLength = (length: number): Uint8Array => {\n\tif (length < 0x80) return Uint8Array.from([length]);\n\tconst bytes: number[] = [];\n\tlet remaining = length;\n\twhile (remaining > 0) {\n\t\tbytes.unshift(remaining & 0xff);\n\t\tremaining >>= 8;\n\t}\n\treturn Uint8Array.from([0x80 | bytes.length, ...bytes]);\n};\n\nconst derTag = (tag: number, payload: Uint8Array): Uint8Array => {\n\tconst length = derLength(payload.length);\n\tconst out = new Uint8Array(1 + length.length + payload.length);\n\tout[0] = tag;\n\tout.set(length, 1);\n\tout.set(payload, 1 + length.length);\n\treturn out;\n};\n\nconst derSeq = (children: Uint8Array[]): Uint8Array => {\n\tconst total = children.reduce((sum, child) => sum + child.length, 0);\n\tconst payload = new Uint8Array(total);\n\tlet offset = 0;\n\tfor (const child of children) {\n\t\tpayload.set(child, offset);\n\t\toffset += child.length;\n\t}\n\treturn derTag(0x30, payload);\n};\n\nconst derSet = (children: Uint8Array[]): Uint8Array => {\n\tconst total = children.reduce((sum, child) => sum + child.length, 0);\n\tconst payload = new Uint8Array(total);\n\tlet offset = 0;\n\tfor (const child of children) {\n\t\tpayload.set(child, offset);\n\t\toffset += child.length;\n\t}\n\treturn derTag(0x31, payload);\n};\n\nconst derInt = (value: number | Uint8Array): Uint8Array => {\n\tif (typeof value === 'number') {\n\t\t// Small unsigned int — used for the CSR version field (0).\n\t\tif (value === 0) return derTag(0x02, Uint8Array.from([0]));\n\t\tconst bytes: number[] = [];\n\t\tlet remaining = value;\n\t\twhile (remaining > 0) {\n\t\t\tbytes.unshift(remaining & 0xff);\n\t\t\tremaining >>= 8;\n\t\t}\n\t\tif ((bytes[0] as number) & 0x80) bytes.unshift(0); // ensure positive\n\t\treturn derTag(0x02, Uint8Array.from(bytes));\n\t}\n\t// Big-endian unsigned bytes — strip leading zeros, then prepend 0x00 if\n\t// the high bit is set (DER INTEGER is signed two's complement).\n\tlet start = 0;\n\twhile (start < value.length - 1 && value[start] === 0) start += 1;\n\tlet payload = value.subarray(start);\n\tif ((payload[0] as number) & 0x80) {\n\t\tconst padded = new Uint8Array(payload.length + 1);\n\t\tpadded.set(payload, 1);\n\t\tpayload = padded;\n\t}\n\treturn derTag(0x02, payload);\n};\n\nconst derOid = (oid: string): Uint8Array => {\n\tconst parts = oid.split('.').map((part) => Number.parseInt(part, 10));\n\tconst first = parts[0];\n\tconst second = parts[1];\n\tif (first === undefined || second === undefined) {\n\t\tthrow new Error(`[deploy/tls] invalid OID: ${oid}`);\n\t}\n\tconst bytes: number[] = [first * 40 + second];\n\tfor (let index = 2; index < parts.length; index += 1) {\n\t\tconst value = parts[index] as number;\n\t\tconst chunks: number[] = [];\n\t\tlet remaining = value;\n\t\tdo {\n\t\t\tchunks.unshift(remaining & 0x7f);\n\t\t\tremaining >>= 7;\n\t\t} while (remaining > 0);\n\t\tfor (let chunkIndex = 0; chunkIndex < chunks.length - 1; chunkIndex += 1) {\n\t\t\tchunks[chunkIndex] = (chunks[chunkIndex] as number) | 0x80;\n\t\t}\n\t\tbytes.push(...chunks);\n\t}\n\treturn derTag(0x06, Uint8Array.from(bytes));\n};\n\nconst derPrintableString = (value: string): Uint8Array =>\n\tderTag(0x13, new TextEncoder().encode(value));\n\nconst derUtf8String = (value: string): Uint8Array =>\n\tderTag(0x0c, new TextEncoder().encode(value));\n\nconst derIa5String = (value: string): Uint8Array =>\n\tderTag(0x16, new TextEncoder().encode(value));\n\nconst derOctetString = (payload: Uint8Array): Uint8Array =>\n\tderTag(0x04, payload);\n\nconst derBitString = (payload: Uint8Array): Uint8Array => {\n\t// 0x00 prefix = number of unused bits in the final byte; for byte-aligned\n\t// signatures + keys this is always zero.\n\tconst bits = new Uint8Array(payload.length + 1);\n\tbits[0] = 0;\n\tbits.set(payload, 1);\n\treturn derTag(0x03, bits);\n};\n\nconst derContextTag = (\n\ttag: number,\n\tpayload: Uint8Array,\n\tconstructed = true\n): Uint8Array => derTag(0xa0 + tag + (constructed ? 0 : -0x20), payload);\n\n// =============================================================================\n// ECDSA signature: raw r||s ↔ DER SEQUENCE\n// =============================================================================\n\nconst ecdsaRawToDer = (rawSig: Uint8Array): Uint8Array => {\n\tconst half = rawSig.length / 2;\n\tconst r = rawSig.subarray(0, half);\n\tconst s = rawSig.subarray(half);\n\treturn derSeq([derInt(r), derInt(s)]);\n};\n\n// =============================================================================\n// JWS (RFC 7515) Flattened JSON Serialization for ACME\n// =============================================================================\n\ntype JwsHeader = {\n\talg: 'ES256';\n\tnonce: string;\n\turl: string;\n\tjwk?: JsonWebKey;\n\tkid?: string;\n};\n\ntype JwsBody = {\n\tprotected: string;\n\tpayload: string;\n\tsignature: string;\n};\n\nconst signJws = async (\n\tprivateKey: CryptoKey,\n\theader: JwsHeader,\n\tpayload: object | string\n): Promise<JwsBody> => {\n\tconst protectedHeader = base64UrlEncodeString(JSON.stringify(header));\n\tconst payloadEncoded =\n\t\tpayload === ''\n\t\t\t? '' // POST-as-GET: empty string, NOT empty object\n\t\t\t: base64UrlEncodeString(JSON.stringify(payload));\n\tconst signingInput = new TextEncoder().encode(\n\t\t`${protectedHeader}.${payloadEncoded}`\n\t);\n\tconst rawSig = new Uint8Array(\n\t\tawait crypto.subtle.sign(\n\t\t\t{ hash: 'SHA-256', name: 'ECDSA' },\n\t\t\tprivateKey,\n\t\t\tsigningInput\n\t\t)\n\t);\n\treturn {\n\t\tpayload: payloadEncoded,\n\t\tprotected: protectedHeader,\n\t\tsignature: base64UrlEncode(rawSig)\n\t};\n};\n\n// =============================================================================\n// JWK helpers — canonical thumbprint, public-key extraction\n// =============================================================================\n\nconst exportPublicJwk = async (publicKey: CryptoKey): Promise<JsonWebKey> => {\n\tconst jwk = await crypto.subtle.exportKey('jwk', publicKey);\n\t// Strip private fields if the export included them (shouldn't, on a\n\t// public key, but be defensive).\n\treturn {\n\t\tcrv: jwk.crv,\n\t\tkty: jwk.kty,\n\t\tx: jwk.x,\n\t\ty: jwk.y\n\t};\n};\n\nconst jwkThumbprint = async (publicJwk: JsonWebKey): Promise<string> => {\n\t// RFC 7638 — canonical JSON: required fields only, lex order.\n\tconst canonical = JSON.stringify({\n\t\tcrv: publicJwk.crv,\n\t\tkty: publicJwk.kty,\n\t\tx: publicJwk.x,\n\t\ty: publicJwk.y\n\t});\n\tconst hash = await crypto.subtle.digest(\n\t\t'SHA-256',\n\t\tnew TextEncoder().encode(canonical)\n\t);\n\treturn base64UrlEncode(hash);\n};\n\n// =============================================================================\n// Account key — generation + JSON export/import for persistence\n// =============================================================================\n\nexport type AcmeAccount = {\n\tkey: CryptoKeyPair;\n\t/** Account URL — set after first registration; persisted for renewals. */\n\tkid?: string;\n};\n\nexport type AcmeAccountJson = {\n\tpublicJwk: JsonWebKey;\n\tprivateJwk: JsonWebKey;\n\tkid?: string;\n};\n\nexport const generateAccountKey = async (): Promise<AcmeAccount> => {\n\tconst key = await crypto.subtle.generateKey(\n\t\t{ name: 'ECDSA', namedCurve: 'P-256' },\n\t\ttrue,\n\t\t['sign', 'verify']\n\t);\n\treturn { key };\n};\n\nexport const exportAccount = async (\n\taccount: AcmeAccount\n): Promise<AcmeAccountJson> => {\n\tconst publicJwk = await crypto.subtle.exportKey('jwk', account.key.publicKey);\n\tconst privateJwk = await crypto.subtle.exportKey(\n\t\t'jwk',\n\t\taccount.key.privateKey\n\t);\n\treturn {\n\t\tprivateJwk,\n\t\tpublicJwk,\n\t\t...(account.kid !== undefined ? { kid: account.kid } : {})\n\t};\n};\n\nexport const importAccount = async (\n\tjson: AcmeAccountJson\n): Promise<AcmeAccount> => {\n\tconst publicKey = await crypto.subtle.importKey(\n\t\t'jwk',\n\t\tjson.publicJwk,\n\t\t{ name: 'ECDSA', namedCurve: 'P-256' },\n\t\ttrue,\n\t\t['verify']\n\t);\n\tconst privateKey = await crypto.subtle.importKey(\n\t\t'jwk',\n\t\tjson.privateJwk,\n\t\t{ name: 'ECDSA', namedCurve: 'P-256' },\n\t\ttrue,\n\t\t['sign']\n\t);\n\treturn {\n\t\tkey: { privateKey, publicKey },\n\t\t...(json.kid !== undefined ? { kid: json.kid } : {})\n\t};\n};\n\n// =============================================================================\n// CSR — Certificate Signing Request\n// =============================================================================\n\nconst buildSanExtension = (domains: string[]): Uint8Array => {\n\t// SubjectAltName extension value: SEQUENCE OF GeneralName\n\tconst generalNames = domains.map((domain) =>\n\t\tderTag(0x82, new TextEncoder().encode(domain))\n\t);\n\tconst sanSequence = derSeq(generalNames);\n\tconst extensionValue = derOctetString(sanSequence);\n\treturn derSeq([derOid('2.5.29.17'), extensionValue]);\n};\n\nconst buildExtensionRequestAttribute = (domains: string[]): Uint8Array => {\n\tconst extensions = derSeq([buildSanExtension(domains)]);\n\treturn derSeq([\n\t\tderOid('1.2.840.113549.1.9.14'), // PKCS#9 extensionRequest\n\t\tderSet([extensions])\n\t]);\n};\n\nconst generateCertKeyPair = async (): Promise<CryptoKeyPair> =>\n\tcrypto.subtle.generateKey(\n\t\t{ name: 'ECDSA', namedCurve: 'P-256' },\n\t\ttrue,\n\t\t['sign', 'verify']\n\t);\n\nconst buildCsr = async (\n\tdomains: string[],\n\tkeypair: CryptoKeyPair\n): Promise<Uint8Array> => {\n\tif (domains.length === 0) {\n\t\tthrow new Error('[deploy/tls] CSR requires at least one domain');\n\t}\n\tconst commonName = domains[0] as string;\n\n\tconst spkiDer = new Uint8Array(\n\t\tawait crypto.subtle.exportKey('spki', keypair.publicKey)\n\t);\n\n\tconst version = derInt(0);\n\tconst subject = derSeq([\n\t\tderSet([\n\t\t\tderSeq([\n\t\t\t\tderOid('2.5.4.3'), // commonName\n\t\t\t\tderUtf8String(commonName)\n\t\t\t])\n\t\t])\n\t]);\n\n\tconst attributes = derContextTag(0, buildExtensionRequestAttribute(domains));\n\n\tconst certificationRequestInfo = derSeq([\n\t\tversion,\n\t\tsubject,\n\t\tspkiDer,\n\t\tattributes\n\t]);\n\n\tconst rawSig = new Uint8Array(\n\t\tawait crypto.subtle.sign(\n\t\t\t{ hash: 'SHA-256', name: 'ECDSA' },\n\t\t\tkeypair.privateKey,\n\t\t\tcertificationRequestInfo as BufferSource\n\t\t)\n\t);\n\tconst sigDer = ecdsaRawToDer(rawSig);\n\n\tconst sigAlgorithm = derSeq([derOid('1.2.840.10045.4.3.2')]);\n\t// ecdsa-with-SHA256\n\tconst signatureBits = derBitString(sigDer);\n\n\treturn derSeq([certificationRequestInfo, sigAlgorithm, signatureBits]);\n};\n\n// =============================================================================\n// PEM\n// =============================================================================\n\nconst derToPem = (label: string, der: Uint8Array): string => {\n\tconst b64 = btoa(String.fromCharCode(...der));\n\tconst lines = b64.match(/.{1,64}/g) ?? [];\n\treturn `-----BEGIN ${label}-----\\n${lines.join('\\n')}\\n-----END ${label}-----\\n`;\n};\n\nconst exportEcPrivateKeyPem = async (\n\tkeypair: CryptoKeyPair\n): Promise<string> => {\n\tconst pkcs8 = new Uint8Array(\n\t\tawait crypto.subtle.exportKey('pkcs8', keypair.privateKey)\n\t);\n\treturn derToPem('PRIVATE KEY', pkcs8);\n};\n\n// =============================================================================\n// ACME client\n// =============================================================================\n\ntype AcmeDirectory = {\n\tnewNonce: string;\n\tnewAccount: string;\n\tnewOrder: string;\n};\n\ntype AcmeOrder = {\n\tstatus: string;\n\tidentifiers: Array<{ type: string; value: string }>;\n\tauthorizations: string[];\n\tfinalize: string;\n\tcertificate?: string;\n};\n\ntype AcmeAuthorization = {\n\tstatus: string;\n\tidentifier: { type: string; value: string };\n\tchallenges: Array<{\n\t\ttype: string;\n\t\tstatus: string;\n\t\turl: string;\n\t\ttoken: string;\n\t}>;\n};\n\nexport type IssueCertificateOptions = {\n\t/** Domain(s) to include. First is the CN; all are SANs. */\n\tdomains: string[];\n\t/** DNS provider used for DNS-01 challenges (must own the zone). */\n\tdnsProvider: DnsProvider;\n\t/** Contact email for the ACME account. */\n\temail: string;\n\t/** ACME directory URL. Default Let's Encrypt production. */\n\tdirectoryUrl?: string;\n\t/** Reuse an existing account. Pass `exportAccount`'s output via `importAccount`. */\n\taccount?: AcmeAccount;\n\t/** Override fetch (tests). Default global fetch. */\n\tfetch?: typeof fetch;\n\t/** Poll interval. Default 3 s. */\n\tpollIntervalMs?: number;\n\t/**\n\t * Max wait before notifying ACME that the DNS challenge is ready.\n\t * Set ~30-60s for Cloudflare; longer for slower providers. Default 30 s.\n\t */\n\tdnsPropagationDelayMs?: number;\n\t/** Max wait for the order to become valid. Default 5 min. */\n\torderTimeoutMs?: number;\n\t/** Status log lines. */\n\tonLog?: (line: string) => void;\n\t/** Override sleep (tests). */\n\tsleep?: (ms: number) => Promise<void>;\n\t/**\n\t * Optional pre-check: returns true when DNS has propagated globally.\n\t * Default: just wait `dnsPropagationDelayMs` then proceed. Override\n\t * for production to actually verify (e.g. resolve from multiple\n\t * public resolvers).\n\t */\n\tcheckDnsPropagated?: (\n\t\trecordName: string,\n\t\texpectedValue: string\n\t) => Promise<boolean>;\n};\n\nexport type IssuedCertificate = {\n\tcertificatePem: string;\n\tprivateKeyPem: string;\n\taccount: AcmeAccount;\n\tdomains: string[];\n};\n\nexport class AcmeError 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 = 'AcmeError';\n\t\tthis.status = status;\n\t\tthis.body = body;\n\t}\n}\n\nconst defaultSleep = (ms: number): Promise<void> =>\n\tnew Promise((resolve) => setTimeout(resolve, ms));\n\nexport const issueCertificate = async (\n\toptions: IssueCertificateOptions\n): Promise<IssuedCertificate> => {\n\tconst log = options.onLog ?? (() => {});\n\tconst sleep = options.sleep ?? defaultSleep;\n\tconst fetcher = options.fetch ?? fetch;\n\tconst directoryUrl = options.directoryUrl ?? LETSENCRYPT_PRODUCTION;\n\tconst pollMs = options.pollIntervalMs ?? 3_000;\n\tconst dnsDelay = options.dnsPropagationDelayMs ?? 30_000;\n\tconst orderTimeout = options.orderTimeoutMs ?? 5 * 60_000;\n\tconst account = options.account ?? (await generateAccountKey());\n\n\tif (options.domains.length === 0) {\n\t\tthrow new Error('[deploy/tls] at least one domain is required');\n\t}\n\n\t// 1. Fetch directory.\n\tlog(`[tls] fetching ACME directory ${directoryUrl}`);\n\tconst directoryResponse = await fetcher(directoryUrl);\n\tif (!directoryResponse.ok) {\n\t\tthrow new AcmeError(\n\t\t\t`failed to fetch ACME directory: ${directoryResponse.status}`,\n\t\t\tdirectoryResponse.status,\n\t\t\tawait directoryResponse.text()\n\t\t);\n\t}\n\tconst directory = (await directoryResponse.json()) as AcmeDirectory;\n\n\t// 2. Get initial nonce.\n\tlet nonce = await fetchNonce(fetcher, directory.newNonce);\n\n\t// Helper: signed POST. Tracks nonce; returns parsed JSON body + headers.\n\tconst post = async (\n\t\turl: string,\n\t\tpayload: object | string,\n\t\tidentification: { kid?: string; jwk?: JsonWebKey }\n\t): Promise<{ status: number; body: unknown; headers: Headers }> => {\n\t\tconst header: JwsHeader = {\n\t\t\talg: 'ES256',\n\t\t\tnonce,\n\t\t\turl,\n\t\t\t...(identification.kid !== undefined ? { kid: identification.kid } : {}),\n\t\t\t...(identification.jwk !== undefined ? { jwk: identification.jwk } : {})\n\t\t};\n\t\tconst jws = await signJws(account.key.privateKey, header, payload);\n\t\tconst response = await fetcher(url, {\n\t\t\tbody: JSON.stringify(jws),\n\t\t\theaders: { 'content-type': 'application/jose+json' },\n\t\t\tmethod: 'POST'\n\t\t});\n\t\tconst newNonce = response.headers.get('replay-nonce');\n\t\tif (newNonce !== null) nonce = newNonce;\n\t\tconst text = await response.text();\n\t\tconst body =\n\t\t\ttext.length > 0 && response.headers.get('content-type')?.includes('json')\n\t\t\t\t? JSON.parse(text)\n\t\t\t\t: text;\n\t\tif (!response.ok) {\n\t\t\tthrow new AcmeError(\n\t\t\t\t`ACME ${url} → ${response.status}`,\n\t\t\t\tresponse.status,\n\t\t\t\tbody\n\t\t\t);\n\t\t}\n\t\treturn { body, headers: response.headers, status: response.status };\n\t};\n\n\t// 3. Register the account (or reuse if kid is set).\n\tconst publicJwk = await exportPublicJwk(account.key.publicKey);\n\tif (account.kid === undefined) {\n\t\tlog(`[tls] registering ACME account for ${options.email}`);\n\t\tconst result = await post(\n\t\t\tdirectory.newAccount,\n\t\t\t{\n\t\t\t\tcontact: [`mailto:${options.email}`],\n\t\t\t\ttermsOfServiceAgreed: true\n\t\t\t},\n\t\t\t{ jwk: publicJwk }\n\t\t);\n\t\taccount.kid = result.headers.get('location') ?? undefined;\n\t\tif (account.kid === undefined) {\n\t\t\tthrow new Error(\n\t\t\t\t'[deploy/tls] ACME newAccount response missing Location header'\n\t\t\t);\n\t\t}\n\t\tlog(`[tls] account registered: ${account.kid}`);\n\t} else {\n\t\tlog(`[tls] reusing account ${account.kid}`);\n\t}\n\n\tconst accountKid = account.kid;\n\n\t// 4. Submit the order.\n\tlog(`[tls] submitting order for ${options.domains.join(', ')}`);\n\tconst orderResult = await post(\n\t\tdirectory.newOrder,\n\t\t{\n\t\t\tidentifiers: options.domains.map((domain) => ({\n\t\t\t\ttype: 'dns',\n\t\t\t\tvalue: domain\n\t\t\t}))\n\t\t},\n\t\t{ kid: accountKid }\n\t);\n\tlet order = orderResult.body as AcmeOrder;\n\tconst orderUrl = orderResult.headers.get('location');\n\tif (orderUrl === null) {\n\t\tthrow new Error(\n\t\t\t'[deploy/tls] ACME newOrder response missing Location header'\n\t\t);\n\t}\n\n\t// 5. For each authorization, complete the DNS-01 challenge.\n\tconst cleanups: Array<() => Promise<void>> = [];\n\tconst dnsCreated: Array<{\n\t\trecordId: string;\n\t\trecordName: string;\n\t\trecordValue: string;\n\t}> = [];\n\n\ttry {\n\t\tconst thumbprint = await jwkThumbprint(publicJwk);\n\n\t\tfor (const authUrl of order.authorizations) {\n\t\t\tconst authResult = await post(authUrl, '', { kid: accountKid });\n\t\t\tconst auth = authResult.body as AcmeAuthorization;\n\t\t\tconst challenge = auth.challenges.find((c) => c.type === 'dns-01');\n\t\t\tif (challenge === undefined) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`[deploy/tls] no dns-01 challenge for ${auth.identifier.value}`\n\t\t\t\t);\n\t\t\t}\n\t\t\tconst keyAuthorization = `${challenge.token}.${thumbprint}`;\n\t\t\tconst txtBytes = new Uint8Array(\n\t\t\t\tawait crypto.subtle.digest(\n\t\t\t\t\t'SHA-256',\n\t\t\t\t\tnew TextEncoder().encode(keyAuthorization)\n\t\t\t\t)\n\t\t\t);\n\t\t\tconst txtValue = base64UrlEncode(txtBytes);\n\t\t\tconst recordName = `_acme-challenge.${auth.identifier.value}`;\n\t\t\tlog(`[tls] DNS-01: setting ${recordName} = \"${txtValue}\"`);\n\t\t\tconst record = await options.dnsProvider.upsert({\n\t\t\t\tcontent: txtValue,\n\t\t\t\tname: recordName,\n\t\t\t\tttl: 60,\n\t\t\t\ttype: 'TXT'\n\t\t\t});\n\t\t\tdnsCreated.push({\n\t\t\t\trecordId: record.id,\n\t\t\t\trecordName,\n\t\t\t\trecordValue: txtValue\n\t\t\t});\n\t\t\tcleanups.push(() => options.dnsProvider.delete(record.id));\n\n\t\t\tif (options.checkDnsPropagated !== undefined) {\n\t\t\t\tlog('[tls] waiting for DNS propagation (custom checker)…');\n\t\t\t\tconst deadline = Date.now() + dnsDelay;\n\t\t\t\twhile (Date.now() < deadline) {\n\t\t\t\t\tif (\n\t\t\t\t\t\tawait options.checkDnsPropagated(recordName, txtValue)\n\t\t\t\t\t) break;\n\t\t\t\t\tawait sleep(pollMs);\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tlog(`[tls] sleeping ${dnsDelay}ms for DNS propagation`);\n\t\t\t\tawait sleep(dnsDelay);\n\t\t\t}\n\n\t\t\tlog(`[tls] notifying ACME of dns-01 readiness for ${auth.identifier.value}`);\n\t\t\tawait post(challenge.url, {}, { kid: accountKid });\n\n\t\t\t// Poll the authorization until valid/invalid.\n\t\t\tconst authDeadline = Date.now() + orderTimeout;\n\t\t\twhile (Date.now() < authDeadline) {\n\t\t\t\tconst polledResult = await post(authUrl, '', { kid: accountKid });\n\t\t\t\tconst polled = polledResult.body as AcmeAuthorization;\n\t\t\t\tif (polled.status === 'valid') break;\n\t\t\t\tif (polled.status === 'invalid') {\n\t\t\t\t\tthrow new Error(\n\t\t\t\t\t\t`[deploy/tls] authorization failed for ${auth.identifier.value}: ${JSON.stringify(polled.challenges)}`\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\tawait sleep(pollMs);\n\t\t\t}\n\t\t}\n\n\t\t// 6. Finalize — generate cert keypair + CSR.\n\t\tlog('[tls] generating cert keypair + CSR');\n\t\tconst certKey = await generateCertKeyPair();\n\t\tconst csrDer = await buildCsr(options.domains, certKey);\n\t\tconst csr = base64UrlEncode(csrDer);\n\n\t\tawait post(order.finalize, { csr }, { kid: accountKid });\n\n\t\t// 7. Poll the order until valid + certificate URL appears.\n\t\tconst orderDeadline = Date.now() + orderTimeout;\n\t\twhile (Date.now() < orderDeadline) {\n\t\t\tconst refreshed = await post(orderUrl, '', { kid: accountKid });\n\t\t\torder = refreshed.body as AcmeOrder;\n\t\t\tif (order.status === 'valid' && order.certificate !== undefined) break;\n\t\t\tif (order.status === 'invalid') {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`[deploy/tls] order failed: ${JSON.stringify(order)}`\n\t\t\t\t);\n\t\t\t}\n\t\t\tawait sleep(pollMs);\n\t\t}\n\n\t\tif (order.certificate === undefined) {\n\t\t\tthrow new Error('[deploy/tls] order timed out before issuing certificate');\n\t\t}\n\n\t\t// 8. Download the cert (PEM bundle).\n\t\tlog(`[tls] downloading certificate from ${order.certificate}`);\n\t\tconst certResult = await post(order.certificate, '', { kid: accountKid });\n\t\tconst certificatePem =\n\t\t\ttypeof certResult.body === 'string'\n\t\t\t\t? certResult.body\n\t\t\t\t: String(certResult.body);\n\n\t\tconst privateKeyPem = await exportEcPrivateKeyPem(certKey);\n\n\t\treturn {\n\t\t\taccount,\n\t\t\tcertificatePem,\n\t\t\tdomains: options.domains,\n\t\t\tprivateKeyPem\n\t\t};\n\t} finally {\n\t\tfor (const cleanup of cleanups) {\n\t\t\ttry {\n\t\t\t\tawait cleanup();\n\t\t\t} catch (error) {\n\t\t\t\tlog(`[tls] cleanup failed (continuing): ${String(error)}`);\n\t\t\t}\n\t\t}\n\t\t// Suppress unused-variable lint without changing behavior.\n\t\tvoid dnsCreated;\n\t}\n};\n\nconst fetchNonce = async (\n\tfetcher: typeof fetch,\n\turl: string\n): Promise<string> => {\n\tconst response = await fetcher(url, { method: 'HEAD' });\n\tconst nonce = response.headers.get('replay-nonce');\n\tif (nonce === null) {\n\t\tthrow new Error('[deploy/tls] newNonce response missing Replay-Nonce header');\n\t}\n\treturn nonce;\n};\n\n// =============================================================================\n// installCertificateOnTarget\n// =============================================================================\n\nexport type InstallCertificateOptions = {\n\t/** Remote path for the cert chain. Default `/etc/ssl/<firstDomain>/fullchain.pem`. */\n\tcertPath?: string;\n\t/** Remote path for the private key. Default `/etc/ssl/<firstDomain>/privkey.pem`. */\n\tkeyPath?: string;\n\t/** Mode (chmod) for the cert + key. Default `600`. */\n\tmode?: string;\n\t/** Owner (chown) for the cert + key. Default unchanged. */\n\towner?: string;\n\t/** Optional reload command run after install (e.g. `'systemctl reload nginx'`). */\n\treload?: string;\n\t/** Override the writeTo helper (tests). */\n\twriteFile?: (target: Target, path: string, contents: string) => Promise<void>;\n};\n\nconst defaultWriteFile = async (\n\ttarget: Target,\n\tremotePath: string,\n\tcontents: string\n): Promise<void> => {\n\tconst escaped = contents.replaceAll(\"'\", \"'\\\\''\");\n\tawait target.exec(`mkdir -p \"$(dirname '${remotePath}')\"`);\n\tawait target.exec(`cat > '${remotePath}' <<'__ABS_TLS_EOF__'\\n${contents}__ABS_TLS_EOF__\\n`);\n\tvoid escaped;\n};\n\n/**\n * Upload cert + private key to the target. Composable with the deploy\n * pipeline as a verify-step or post-deploy hook.\n */\nexport const installCertificateOnTarget = async (\n\ttarget: Target,\n\tcert: IssuedCertificate,\n\toptions: InstallCertificateOptions = {}\n): Promise<{ certPath: string; keyPath: string }> => {\n\tconst domain = cert.domains[0];\n\tif (domain === undefined) {\n\t\tthrow new Error('[deploy/tls] certificate has no domains');\n\t}\n\tconst certPath = options.certPath ?? `/etc/ssl/${domain}/fullchain.pem`;\n\tconst keyPath = options.keyPath ?? `/etc/ssl/${domain}/privkey.pem`;\n\tconst mode = options.mode ?? '600';\n\tconst writer = options.writeFile ?? defaultWriteFile;\n\n\tawait writer(target, certPath, cert.certificatePem);\n\tawait writer(target, keyPath, cert.privateKeyPem);\n\tawait target.exec(`chmod ${mode} '${certPath}' '${keyPath}'`);\n\tif (options.owner !== undefined) {\n\t\tawait target.exec(`chown ${options.owner} '${certPath}' '${keyPath}'`);\n\t}\n\tif (options.reload !== undefined) {\n\t\tawait target.exec(options.reload);\n\t}\n\n\treturn { certPath, keyPath };\n};\n"
|
|
6
|
+
],
|
|
7
|
+
"mappings": "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",
|
|
8
|
+
"debugId": "54D17E1F39C4D88964756E2164756E21",
|
|
9
|
+
"names": []
|
|
10
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@absolutejs/deploy",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.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 src/dns.ts src/cloudflare.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 src/tls.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}\"",
|
|
@@ -66,6 +66,11 @@
|
|
|
66
66
|
"types": "./dist/cloudflare.d.ts",
|
|
67
67
|
"import": "./dist/cloudflare.js",
|
|
68
68
|
"default": "./dist/cloudflare.js"
|
|
69
|
+
},
|
|
70
|
+
"./tls": {
|
|
71
|
+
"types": "./dist/tls.d.ts",
|
|
72
|
+
"import": "./dist/tls.js",
|
|
73
|
+
"default": "./dist/tls.js"
|
|
69
74
|
}
|
|
70
75
|
},
|
|
71
76
|
"files": ["dist", "README.md"]
|