@absolutejs/deploy 0.23.0 → 0.25.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 +204 -123
- package/dist/appStoreConnect.d.ts +182 -0
- package/dist/appStoreConnect.js +587 -0
- package/dist/appStoreConnect.js.map +10 -0
- package/dist/cloudTarget.d.ts +4 -4
- package/dist/cloudflare.d.ts +2 -2
- package/dist/cloudflare.js +11 -9
- package/dist/cloudflare.js.map +3 -3
- package/dist/deployer.d.ts +9 -9
- package/dist/digitalocean.d.ts +5 -5
- package/dist/digitalocean.js +21 -13
- package/dist/digitalocean.js.map +5 -5
- package/dist/digitaloceanDns.d.ts +2 -2
- package/dist/digitaloceanDns.js +16 -8
- package/dist/digitaloceanDns.js.map +6 -6
- package/dist/digitaloceanEphemeralInfrastructure.js +16 -8
- package/dist/digitaloceanEphemeralInfrastructure.js.map +5 -5
- package/dist/digitaloceanInfrastructure.js +16 -8
- package/dist/digitaloceanInfrastructure.js.map +6 -6
- package/dist/digitaloceanIngress.js +16 -8
- package/dist/digitaloceanIngress.js.map +5 -5
- package/dist/dns.d.ts +1 -1
- package/dist/dns.js +9 -7
- package/dist/dns.js.map +3 -3
- package/dist/edgeIngress.js +11 -9
- package/dist/edgeIngress.js.map +2 -2
- package/dist/env.d.ts +1 -1
- package/dist/env.js +13 -11
- package/dist/env.js.map +3 -3
- package/dist/gcp.js +99 -93
- package/dist/gcp.js.map +3 -3
- package/dist/gcpIngress.js +93 -91
- package/dist/gcpIngress.js.map +2 -2
- package/dist/googlePlay.d.ts +10 -2
- package/dist/googlePlay.js +96 -91
- package/dist/googlePlay.js.map +3 -3
- package/dist/hetzner.d.ts +3 -3
- package/dist/hetzner.js +21 -13
- package/dist/hetzner.js.map +5 -5
- package/dist/hetznerDns.d.ts +2 -2
- package/dist/hetznerDns.js +11 -9
- package/dist/hetznerDns.js.map +3 -3
- package/dist/hetznerInfrastructure.js +16 -8
- package/dist/hetznerInfrastructure.js.map +7 -7
- package/dist/index.d.ts +5 -1
- package/dist/index.js +1347 -284
- package/dist/index.js.map +10 -8
- package/dist/linode.d.ts +3 -3
- package/dist/linode.js +21 -13
- package/dist/linode.js.map +5 -5
- package/dist/linodeInfrastructure.js +21 -9
- package/dist/linodeInfrastructure.js.map +7 -7
- package/dist/managedPreview.js +9 -7
- package/dist/managedPreview.js.map +2 -2
- package/dist/mobileUpdate.d.ts +109 -0
- package/dist/mobileUpdate.js +464 -0
- package/dist/mobileUpdate.js.map +10 -0
- package/dist/nativeRelease.d.ts +17 -1
- package/dist/nativeRelease.js +63 -22
- package/dist/nativeRelease.js.map +3 -3
- package/dist/preview.d.ts +3 -3
- package/dist/preview.js +15 -10
- package/dist/preview.js.map +3 -3
- package/dist/processManagers.d.ts +4 -4
- package/dist/releaseArtifact.js +12 -10
- package/dist/releaseArtifact.js.map +2 -2
- package/dist/route53.d.ts +2 -2
- package/dist/route53.js +9 -7
- package/dist/route53.js.map +3 -3
- package/dist/targets.d.ts +1 -1
- package/dist/tls.d.ts +5 -5
- package/dist/tls.js +23 -18
- package/dist/tls.js.map +3 -3
- package/dist/vultr.d.ts +4 -4
- package/dist/vultr.js +21 -13
- package/dist/vultr.js.map +5 -5
- package/dist/vultrInfrastructure.js +16 -8
- package/dist/vultrInfrastructure.js.map +7 -7
- package/package.json +12 -2
package/README.md
CHANGED
|
@@ -15,29 +15,28 @@ Zero `ssh2` / `node-ssh` dependency — `sshTarget` shells out to the system
|
|
|
15
15
|
## Native application releases (0.22.0)
|
|
16
16
|
|
|
17
17
|
`@absolutejs/deploy/native-release` publishes the immutable release directory
|
|
18
|
-
created by `absolute mobile build android`
|
|
19
|
-
adapter. It verifies the local AAB's declared size and SHA-256 digest again,
|
|
18
|
+
created by `absolute mobile build android` or `absolute mobile build ios` through
|
|
19
|
+
any `@absolutejs/blob` adapter. It verifies the local AAB/IPA's declared size and SHA-256 digest again,
|
|
20
20
|
requires a signed build by default, and writes the binary only once under its
|
|
21
21
|
content-derived release identity.
|
|
22
22
|
|
|
23
23
|
```ts
|
|
24
|
-
import { createNativeReleaseRegistry } from
|
|
25
|
-
import { s3BlobStore } from
|
|
24
|
+
import { createNativeReleaseRegistry } from "@absolutejs/deploy/native-release";
|
|
25
|
+
import { s3BlobStore } from "@absolutejs/blob/s3";
|
|
26
26
|
|
|
27
|
-
const store = s3BlobStore({ client, bucket:
|
|
27
|
+
const store = s3BlobStore({ client, bucket: "absolute-releases" });
|
|
28
28
|
const releases = createNativeReleaseRegistry({ store });
|
|
29
29
|
|
|
30
30
|
const published = await releases.publish({
|
|
31
|
-
releaseRoot:
|
|
32
|
-
|
|
33
|
-
channel: 'internal'
|
|
31
|
+
releaseRoot: ".absolutejs/mobile/releases/android/amobile_android_<sha256>",
|
|
32
|
+
channel: "internal",
|
|
34
33
|
});
|
|
35
34
|
|
|
36
35
|
await releases.promote({
|
|
37
36
|
appId: published.record.metadata.appId,
|
|
38
|
-
platform:
|
|
37
|
+
platform: "android",
|
|
39
38
|
releaseId: published.record.metadata.releaseId,
|
|
40
|
-
channel:
|
|
39
|
+
channel: "production",
|
|
41
40
|
});
|
|
42
41
|
```
|
|
43
42
|
|
|
@@ -56,11 +55,11 @@ retry to reuse an unexpired edit or resumable upload and to reconcile a commit
|
|
|
56
55
|
whose successful response was lost.
|
|
57
56
|
|
|
58
57
|
```ts
|
|
59
|
-
import { createGooglePlayReleasePublisher } from
|
|
58
|
+
import { createGooglePlayReleasePublisher } from "@absolutejs/deploy/google-play";
|
|
60
59
|
|
|
61
60
|
export default createGooglePlayReleasePublisher({
|
|
62
61
|
receiptStore: store,
|
|
63
|
-
registry: releases
|
|
62
|
+
registry: releases,
|
|
64
63
|
});
|
|
65
64
|
```
|
|
66
65
|
|
|
@@ -79,6 +78,74 @@ use `status: 'inProgress'` with `0 < userFraction < 1`; subsequent calls for the
|
|
|
79
78
|
same release can increase the fraction, halt or resume it, or mark it completed
|
|
80
79
|
without uploading the AAB again.
|
|
81
80
|
|
|
81
|
+
### App Store Connect and TestFlight distribution (0.24.0)
|
|
82
|
+
|
|
83
|
+
`@absolutejs/deploy/app-store-connect` uses Apple's current Build Upload API,
|
|
84
|
+
then waits for the processed build and assigns it to named or ID-addressed
|
|
85
|
+
TestFlight groups. It stores only durable Apple resource IDs in retry receipts;
|
|
86
|
+
time-limited upload URLs and API credentials are never persisted.
|
|
87
|
+
|
|
88
|
+
```ts
|
|
89
|
+
import { createAppStoreConnectReleasePublisher } from "@absolutejs/deploy/app-store-connect";
|
|
90
|
+
import { createGooglePlayReleasePublisher } from "@absolutejs/deploy/google-play";
|
|
91
|
+
|
|
92
|
+
const google = createGooglePlayReleasePublisher({
|
|
93
|
+
receiptStore: store,
|
|
94
|
+
registry: releases,
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
export default createAppStoreConnectReleasePublisher({
|
|
98
|
+
auth: {
|
|
99
|
+
issuerId: process.env.APP_STORE_CONNECT_ISSUER_ID!,
|
|
100
|
+
keyId: process.env.APP_STORE_CONNECT_KEY_ID!,
|
|
101
|
+
privateKey: process.env.APP_STORE_CONNECT_PRIVATE_KEY!,
|
|
102
|
+
},
|
|
103
|
+
receiptStore: store,
|
|
104
|
+
registry: google,
|
|
105
|
+
});
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Provider wrappers compose, so this one module serves Android and iOS. AbsoluteJS
|
|
109
|
+
asks the adapter for a stable next integer build number before Xcode archives;
|
|
110
|
+
the allocation is tied to the web build, native-source fingerprint, and marketing
|
|
111
|
+
version. Serialize release jobs for one Apple app because App Store Connect has
|
|
112
|
+
no build-number reservation operation. Internal groups need no review. External
|
|
113
|
+
review is submitted only when explicitly requested.
|
|
114
|
+
|
|
115
|
+
### Signed mobile web-bundle updates (0.25.0)
|
|
116
|
+
|
|
117
|
+
`@absolutejs/deploy/mobile-update` publishes the signed immutable update
|
|
118
|
+
directory created by `absolute mobile update build`. The trusted server verifies
|
|
119
|
+
the ECDSA P-256 signature and every file digest before storing anything, then
|
|
120
|
+
selects staged-rollout cohorts from an anonymous installation ID. Native runtime
|
|
121
|
+
fingerprints prevent a web bundle from crossing a plugin, permission, Auth, or
|
|
122
|
+
local-data ABI boundary.
|
|
123
|
+
|
|
124
|
+
```ts
|
|
125
|
+
import {
|
|
126
|
+
createMobileUpdateHandler,
|
|
127
|
+
createMobileUpdateRegistry,
|
|
128
|
+
} from "@absolutejs/deploy/mobile-update";
|
|
129
|
+
|
|
130
|
+
const updates = createMobileUpdateRegistry({
|
|
131
|
+
publicKeys: { "production-2026": process.env.MOBILE_UPDATE_PUBLIC_KEY! },
|
|
132
|
+
store,
|
|
133
|
+
});
|
|
134
|
+
const handleUpdate = createMobileUpdateHandler({
|
|
135
|
+
appId: "com.example.product",
|
|
136
|
+
channel: "production",
|
|
137
|
+
registry: updates,
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
export default updates;
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Mount `handleUpdate` at
|
|
144
|
+
`/__absolute/mobile/updates/production/*`. Manifests are served without caching;
|
|
145
|
+
release assets are content-verified and immutable. Promotion only changes a
|
|
146
|
+
small channel pointer, and rollback can select a prior release or the embedded
|
|
147
|
+
store build without copying bundle bytes.
|
|
148
|
+
|
|
82
149
|
## Infrastructure providers (0.14.0)
|
|
83
150
|
|
|
84
151
|
Control planes use the normalized `InfrastructureProvider` contract from
|
|
@@ -87,25 +154,27 @@ live in this package beside their deploy targets so providers never become
|
|
|
87
154
|
scattered across host applications.
|
|
88
155
|
|
|
89
156
|
```ts
|
|
90
|
-
import { createDigitalOceanInfrastructureProvider } from
|
|
157
|
+
import { createDigitalOceanInfrastructureProvider } from "@absolutejs/deploy/digitalocean-infrastructure";
|
|
91
158
|
|
|
92
159
|
const provider = createDigitalOceanInfrastructureProvider({
|
|
93
160
|
token: process.env.DIGITALOCEAN_TOKEN!,
|
|
94
|
-
tag:
|
|
95
|
-
regions: [
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
161
|
+
tag: "absolutejs-paas-node",
|
|
162
|
+
regions: [
|
|
163
|
+
{
|
|
164
|
+
region: "nyc3",
|
|
165
|
+
size: "s-2vcpu-4gb",
|
|
166
|
+
image: "ubuntu-24-04-x64",
|
|
167
|
+
sshKeys: [process.env.DIGITALOCEAN_SSH_KEY!],
|
|
168
|
+
userData: process.env.ABSOLUTEJS_NODE_CLOUD_INIT,
|
|
169
|
+
},
|
|
170
|
+
],
|
|
102
171
|
agent: { preferPrivateNetwork: true, port: 8081 },
|
|
103
172
|
});
|
|
104
173
|
|
|
105
174
|
await provider.listNodes();
|
|
106
175
|
await provider.provisionNode({
|
|
107
176
|
idempotencyKey: crypto.randomUUID(),
|
|
108
|
-
name:
|
|
177
|
+
name: "absolutejs-node-01",
|
|
109
178
|
});
|
|
110
179
|
```
|
|
111
180
|
|
|
@@ -131,26 +200,26 @@ and idempotent removal. Provider resource construction stays here instead of
|
|
|
131
200
|
leaking DigitalOcean or GCP APIs into a control plane.
|
|
132
201
|
|
|
133
202
|
```ts
|
|
134
|
-
import { createDigitalOceanIngressProvider } from
|
|
203
|
+
import { createDigitalOceanIngressProvider } from "@absolutejs/deploy/digitalocean-ingress";
|
|
135
204
|
|
|
136
205
|
const ingress = createDigitalOceanIngressProvider({
|
|
137
206
|
token: process.env.DIGITALOCEAN_TOKEN!,
|
|
138
207
|
});
|
|
139
208
|
|
|
140
209
|
await ingress.reconcileIngress({
|
|
141
|
-
name:
|
|
210
|
+
name: "absolutejs-edge",
|
|
142
211
|
idempotencyKey: crypto.randomUUID(),
|
|
143
212
|
backends: [
|
|
144
|
-
{ region:
|
|
145
|
-
{ region:
|
|
213
|
+
{ region: "nyc3", resourceId: "regional-lb-east", priority: 1 },
|
|
214
|
+
{ region: "sfo3", resourceId: "regional-lb-west", priority: 2 },
|
|
146
215
|
],
|
|
147
216
|
listener: {
|
|
148
217
|
port: 443,
|
|
149
|
-
protocol:
|
|
218
|
+
protocol: "https",
|
|
150
219
|
targetPort: 443,
|
|
151
220
|
tlsPassthrough: true,
|
|
152
221
|
},
|
|
153
|
-
healthCheck: { protocol:
|
|
222
|
+
healthCheck: { protocol: "tcp", port: 443 },
|
|
154
223
|
});
|
|
155
224
|
```
|
|
156
225
|
|
|
@@ -163,26 +232,25 @@ operations before advancing dependent resources. Creating an adapter does not
|
|
|
163
232
|
provision anything; only `reconcileIngress()` mutates provider state.
|
|
164
233
|
|
|
165
234
|
```ts
|
|
166
|
-
import {
|
|
167
|
-
createDeployer,
|
|
168
|
-
sshTarget,
|
|
169
|
-
systemdManager,
|
|
170
|
-
} from '@absolutejs/deploy';
|
|
235
|
+
import { createDeployer, sshTarget, systemdManager } from "@absolutejs/deploy";
|
|
171
236
|
|
|
172
237
|
const deployer = createDeployer({
|
|
173
|
-
appName:
|
|
238
|
+
appName: "my-app",
|
|
174
239
|
target: sshTarget({
|
|
175
|
-
host:
|
|
176
|
-
user:
|
|
177
|
-
identity:
|
|
240
|
+
host: "droplet-1.example.com",
|
|
241
|
+
user: "deploy",
|
|
242
|
+
identity: "~/.ssh/id_ed25519",
|
|
178
243
|
}),
|
|
179
|
-
source: { kind:
|
|
180
|
-
env: { PORT:
|
|
181
|
-
processManager: systemdManager({ user:
|
|
182
|
-
verify: { kind:
|
|
244
|
+
source: { kind: "directory", root: "./" },
|
|
245
|
+
env: { PORT: "3000", DATABASE_URL: process.env.DATABASE_URL! },
|
|
246
|
+
processManager: systemdManager({ user: "deploy" }),
|
|
247
|
+
verify: { kind: "http", url: "http://localhost:3000/health" },
|
|
183
248
|
hooks: {
|
|
184
249
|
onStepStart: ({ name, releaseId }) => console.log(`▸ ${releaseId} ${name}`),
|
|
185
|
-
onLog: (line, stream, step) =>
|
|
250
|
+
onLog: (line, stream, step) =>
|
|
251
|
+
process[stream === "stderr" ? "stderr" : "stdout"].write(
|
|
252
|
+
`[${step}] ${line}\n`,
|
|
253
|
+
),
|
|
186
254
|
},
|
|
187
255
|
});
|
|
188
256
|
|
|
@@ -213,9 +281,9 @@ await deployer.stop();
|
|
|
213
281
|
|
|
214
282
|
### Targets
|
|
215
283
|
|
|
216
|
-
| Adapter
|
|
217
|
-
|
|
218
|
-
| `localTarget({ root, env? })`
|
|
284
|
+
| Adapter | Use |
|
|
285
|
+
| ----------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
|
|
286
|
+
| `localTarget({ root, env? })` | Tests, local-dev, and "deploy to the same box" workflows. |
|
|
219
287
|
| `sshTarget({ host, user?, port?, identity?, sshFlags?, rsync? })` | Any VPS — DigitalOcean Droplets, Linode, Hetzner, Vultr, Lightsail, Scaleway. Uses the system `ssh` / `rsync` — no npm dep. |
|
|
220
288
|
|
|
221
289
|
A `Target` is just:
|
|
@@ -223,8 +291,15 @@ A `Target` is just:
|
|
|
223
291
|
```ts
|
|
224
292
|
type Target = {
|
|
225
293
|
description: string;
|
|
226
|
-
exec(
|
|
227
|
-
|
|
294
|
+
exec(
|
|
295
|
+
cmd: string,
|
|
296
|
+
opts?: { cwd?; env?; timeoutMs?; onLog?; stdin? },
|
|
297
|
+
): Promise<{ stdout; stderr; exitCode }>;
|
|
298
|
+
upload(
|
|
299
|
+
local: string,
|
|
300
|
+
remote: string,
|
|
301
|
+
opts?: { exclude?; deleteOrphans? },
|
|
302
|
+
): Promise<void>;
|
|
228
303
|
close?(): Promise<void>;
|
|
229
304
|
};
|
|
230
305
|
```
|
|
@@ -233,10 +308,10 @@ If you can implement those two methods, you can deploy through `@absolutejs/depl
|
|
|
233
308
|
|
|
234
309
|
### Process managers
|
|
235
310
|
|
|
236
|
-
| Manager
|
|
237
|
-
|
|
238
|
-
| `bareManager({ command? })`
|
|
239
|
-
| `systemdManager({ user?, group?, execStart?, restart?, ... })` | Templated systemd unit pointing at `current/`, `daemon-reload` + `restart`. The production answer for VMs.
|
|
311
|
+
| Manager | What it does |
|
|
312
|
+
| -------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
|
|
313
|
+
| `bareManager({ command? })` | Default. `nohup bun run start &`, pid file under `/var/lib/<appName>/`, logs to `/var/log/<appName>/`. Zero remote dependency. |
|
|
314
|
+
| `systemdManager({ user?, group?, execStart?, restart?, ... })` | Templated systemd unit pointing at `current/`, `daemon-reload` + `restart`. The production answer for VMs. |
|
|
240
315
|
|
|
241
316
|
A `ProcessManager` is just `{ reload, stop?, status? }`. Wrap PM2, supervisord, runit, or even `@absolutejs/runtime` — whatever your remote uses.
|
|
242
317
|
|
|
@@ -280,25 +355,25 @@ up a droplet by name; creates it via the v2 API if absent; waits for
|
|
|
280
355
|
hand to `createDeployer`.
|
|
281
356
|
|
|
282
357
|
```ts
|
|
283
|
-
import { createDeployer } from
|
|
284
|
-
import { digitalOceanTarget } from
|
|
358
|
+
import { createDeployer } from "@absolutejs/deploy";
|
|
359
|
+
import { digitalOceanTarget } from "@absolutejs/deploy/digitalocean";
|
|
285
360
|
|
|
286
361
|
const target = await digitalOceanTarget({
|
|
287
362
|
token: process.env.DO_TOKEN!,
|
|
288
|
-
name:
|
|
289
|
-
region:
|
|
290
|
-
size:
|
|
291
|
-
image:
|
|
363
|
+
name: "absolutejs-prod-1", // idempotency key
|
|
364
|
+
region: "nyc3",
|
|
365
|
+
size: "s-1vcpu-1gb",
|
|
366
|
+
image: "ubuntu-22-04-x64",
|
|
292
367
|
sshKeys: [process.env.DO_KEY_FINGERPRINT!],
|
|
293
|
-
tags: [
|
|
294
|
-
userData:
|
|
368
|
+
tags: ["absolutejs"],
|
|
369
|
+
userData: "#!/bin/bash\ncurl -fsSL https://bun.sh/install | bash",
|
|
295
370
|
onLog: (line) => console.log(line),
|
|
296
371
|
});
|
|
297
372
|
|
|
298
373
|
console.log(`droplet ${target.dropletId} at ${target.ipv4}`);
|
|
299
374
|
|
|
300
|
-
const deployer = createDeployer({ appName:
|
|
301
|
-
await deployer.deploy({ source: { kind:
|
|
375
|
+
const deployer = createDeployer({ appName: "my-app", target });
|
|
376
|
+
await deployer.deploy({ source: { kind: "directory", path: "./build" } });
|
|
302
377
|
|
|
303
378
|
// Tear it down when you're done:
|
|
304
379
|
await target.destroy();
|
|
@@ -325,22 +400,22 @@ mappings underneath. Hetzner-specific differences: locations
|
|
|
325
400
|
and public-net IPv4/IPv6 are independently toggleable.
|
|
326
401
|
|
|
327
402
|
```ts
|
|
328
|
-
import { createDeployer } from
|
|
329
|
-
import { hetznerTarget } from
|
|
403
|
+
import { createDeployer } from "@absolutejs/deploy";
|
|
404
|
+
import { hetznerTarget } from "@absolutejs/deploy/hetzner";
|
|
330
405
|
|
|
331
406
|
const target = await hetznerTarget({
|
|
332
407
|
token: process.env.HETZNER_TOKEN!,
|
|
333
|
-
name:
|
|
334
|
-
location:
|
|
335
|
-
serverType:
|
|
336
|
-
image:
|
|
408
|
+
name: "absolutejs-prod-1",
|
|
409
|
+
location: "nbg1",
|
|
410
|
+
serverType: "cx22",
|
|
411
|
+
image: "ubuntu-22.04",
|
|
337
412
|
sshKeys: [process.env.HETZNER_KEY_FINGERPRINT!],
|
|
338
|
-
labels: { env:
|
|
339
|
-
userData:
|
|
413
|
+
labels: { env: "prod", team: "platform" },
|
|
414
|
+
userData: "#!/bin/bash\ncurl -fsSL https://bun.sh/install | bash",
|
|
340
415
|
});
|
|
341
416
|
|
|
342
|
-
const deployer = createDeployer({ appName:
|
|
343
|
-
await deployer.deploy({ source: { kind:
|
|
417
|
+
const deployer = createDeployer({ appName: "my-app", target });
|
|
418
|
+
await deployer.deploy({ source: { kind: "directory", path: "./build" } });
|
|
344
419
|
await target.destroy();
|
|
345
420
|
```
|
|
346
421
|
|
|
@@ -358,11 +433,13 @@ implements the shared `DnsProvider` contract from
|
|
|
358
433
|
`@absolutejs/deploy/dns`.
|
|
359
434
|
|
|
360
435
|
```ts
|
|
361
|
-
import { hetznerTarget } from
|
|
362
|
-
import { cloudflareProvider } from
|
|
363
|
-
import { ensureDnsForTarget } from
|
|
436
|
+
import { hetznerTarget } from "@absolutejs/deploy/hetzner";
|
|
437
|
+
import { cloudflareProvider } from "@absolutejs/deploy/cloudflare";
|
|
438
|
+
import { ensureDnsForTarget } from "@absolutejs/deploy/dns";
|
|
364
439
|
|
|
365
|
-
const target = await hetznerTarget({
|
|
440
|
+
const target = await hetznerTarget({
|
|
441
|
+
/* … */
|
|
442
|
+
});
|
|
366
443
|
const dns = cloudflareProvider({
|
|
367
444
|
token: process.env.CLOUDFLARE_TOKEN!,
|
|
368
445
|
zoneId: process.env.CLOUDFLARE_ZONE_ID!,
|
|
@@ -370,7 +447,7 @@ const dns = cloudflareProvider({
|
|
|
370
447
|
|
|
371
448
|
// Idempotent — create or update so the A record points at target.ipv4.
|
|
372
449
|
await ensureDnsForTarget(dns, {
|
|
373
|
-
name:
|
|
450
|
+
name: "api.example.com",
|
|
374
451
|
target,
|
|
375
452
|
ttl: 60,
|
|
376
453
|
proxied: false,
|
|
@@ -405,36 +482,38 @@ against Bun's `crypto.subtle`. The audit surface stays in this
|
|
|
405
482
|
repo.
|
|
406
483
|
|
|
407
484
|
```ts
|
|
408
|
-
import { hetznerTarget } from
|
|
409
|
-
import { cloudflareProvider } from
|
|
410
|
-
import { ensureDnsForTarget } from
|
|
485
|
+
import { hetznerTarget } from "@absolutejs/deploy/hetzner";
|
|
486
|
+
import { cloudflareProvider } from "@absolutejs/deploy/cloudflare";
|
|
487
|
+
import { ensureDnsForTarget } from "@absolutejs/deploy/dns";
|
|
411
488
|
import {
|
|
412
489
|
issueCertificate,
|
|
413
490
|
installCertificateOnTarget,
|
|
414
491
|
LETSENCRYPT_PRODUCTION,
|
|
415
|
-
} from
|
|
492
|
+
} from "@absolutejs/deploy/tls";
|
|
416
493
|
|
|
417
|
-
const target = await hetznerTarget({
|
|
494
|
+
const target = await hetznerTarget({
|
|
495
|
+
/* … */
|
|
496
|
+
});
|
|
418
497
|
const dns = cloudflareProvider({
|
|
419
498
|
token: process.env.CLOUDFLARE_TOKEN!,
|
|
420
499
|
zoneId: process.env.CLOUDFLARE_ZONE_ID!,
|
|
421
500
|
});
|
|
422
501
|
|
|
423
502
|
// 1. Point DNS at the box.
|
|
424
|
-
await ensureDnsForTarget(dns, { name:
|
|
503
|
+
await ensureDnsForTarget(dns, { name: "api.example.com", target, ttl: 60 });
|
|
425
504
|
|
|
426
505
|
// 2. Issue a cert via DNS-01.
|
|
427
506
|
const cert = await issueCertificate({
|
|
428
|
-
domains: [
|
|
507
|
+
domains: ["api.example.com"],
|
|
429
508
|
dnsProvider: dns,
|
|
430
|
-
email:
|
|
509
|
+
email: "ops@example.com",
|
|
431
510
|
directoryUrl: LETSENCRYPT_PRODUCTION,
|
|
432
511
|
onLog: (line) => console.log(line),
|
|
433
512
|
});
|
|
434
513
|
|
|
435
514
|
// 3. Install on the box.
|
|
436
515
|
await installCertificateOnTarget(target, cert, {
|
|
437
|
-
reload:
|
|
516
|
+
reload: "systemctl reload nginx",
|
|
438
517
|
});
|
|
439
518
|
```
|
|
440
519
|
|
|
@@ -445,9 +524,9 @@ map the provider write while leaving propagation checks on the public name:
|
|
|
445
524
|
|
|
446
525
|
```ts
|
|
447
526
|
const cert = await issueCertificate({
|
|
448
|
-
domains: [
|
|
527
|
+
domains: ["app.customer.com"],
|
|
449
528
|
dnsProvider: platformValidationDns,
|
|
450
|
-
email:
|
|
529
|
+
email: "ops@platform.example",
|
|
451
530
|
mapDnsChallengeRecord: ({ domain }) =>
|
|
452
531
|
`${stableDomainToken(domain)}.acme.platform.example`,
|
|
453
532
|
});
|
|
@@ -473,43 +552,43 @@ this module handles the deploy-side (push values to remote env
|
|
|
473
552
|
files, atomic swap, conditional service reload).
|
|
474
553
|
|
|
475
554
|
```ts
|
|
476
|
-
import { createSecretBroker, inMemoryAdapter } from
|
|
477
|
-
import { hetznerTarget } from
|
|
555
|
+
import { createSecretBroker, inMemoryAdapter } from "@absolutejs/secrets";
|
|
556
|
+
import { hetznerTarget } from "@absolutejs/deploy/hetzner";
|
|
478
557
|
import {
|
|
479
558
|
syncSecretsToDeployments,
|
|
480
559
|
deploymentsUsing,
|
|
481
560
|
type EnvDeployment,
|
|
482
|
-
} from
|
|
561
|
+
} from "@absolutejs/deploy/env";
|
|
483
562
|
|
|
484
563
|
// One source of truth — the SecretBroker. Swap in whatever adapter
|
|
485
564
|
// (env, file, vault, etc.) makes sense for your team.
|
|
486
565
|
const broker = createSecretBroker({
|
|
487
566
|
adapter: inMemoryAdapter({
|
|
488
567
|
initial: {
|
|
489
|
-
DATABASE_URL:
|
|
490
|
-
STRIPE_KEY:
|
|
568
|
+
DATABASE_URL: "postgres://prod-db",
|
|
569
|
+
STRIPE_KEY: "sk_live_old",
|
|
491
570
|
},
|
|
492
571
|
}),
|
|
493
572
|
});
|
|
494
573
|
|
|
495
574
|
// Each deployed service is one EnvDeployment.
|
|
496
|
-
const api = await hetznerTarget({ name:
|
|
497
|
-
const worker = await hetznerTarget({ name:
|
|
575
|
+
const api = await hetznerTarget({ name: "api-1" /* … */ });
|
|
576
|
+
const worker = await hetznerTarget({ name: "worker-1" /* … */ });
|
|
498
577
|
|
|
499
578
|
const deployments: EnvDeployment[] = [
|
|
500
579
|
{
|
|
501
580
|
target: api,
|
|
502
|
-
remotePath:
|
|
503
|
-
secretNames: [
|
|
504
|
-
extras: { NODE_ENV:
|
|
505
|
-
reload:
|
|
581
|
+
remotePath: "/etc/api.env",
|
|
582
|
+
secretNames: ["STRIPE_KEY", "DATABASE_URL"],
|
|
583
|
+
extras: { NODE_ENV: "production", PORT: "3000" },
|
|
584
|
+
reload: "systemctl reload api",
|
|
506
585
|
},
|
|
507
586
|
{
|
|
508
587
|
target: worker,
|
|
509
|
-
remotePath:
|
|
510
|
-
secretNames: [
|
|
511
|
-
extras: { NODE_ENV:
|
|
512
|
-
reload:
|
|
588
|
+
remotePath: "/etc/worker.env",
|
|
589
|
+
secretNames: ["DATABASE_URL"],
|
|
590
|
+
extras: { NODE_ENV: "production" },
|
|
591
|
+
reload: "systemctl restart worker",
|
|
513
592
|
},
|
|
514
593
|
];
|
|
515
594
|
|
|
@@ -517,10 +596,10 @@ const deployments: EnvDeployment[] = [
|
|
|
517
596
|
await syncSecretsToDeployments(broker, deployments);
|
|
518
597
|
|
|
519
598
|
// Rotate STRIPE_KEY everywhere it's used:
|
|
520
|
-
await broker.rotate(
|
|
599
|
+
await broker.rotate("STRIPE_KEY");
|
|
521
600
|
await syncSecretsToDeployments(
|
|
522
601
|
broker,
|
|
523
|
-
deploymentsUsing(
|
|
602
|
+
deploymentsUsing("STRIPE_KEY", deployments),
|
|
524
603
|
);
|
|
525
604
|
```
|
|
526
605
|
|
|
@@ -553,33 +632,35 @@ import {
|
|
|
553
632
|
renewCertificate,
|
|
554
633
|
installCertificateOnTarget,
|
|
555
634
|
importAccount,
|
|
556
|
-
} from
|
|
557
|
-
import { readFile, writeFile } from
|
|
635
|
+
} from "@absolutejs/deploy/tls";
|
|
636
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
558
637
|
|
|
559
|
-
const currentCertificatePem = await readFile(
|
|
638
|
+
const currentCertificatePem = await readFile("./cert.pem", "utf8").catch(
|
|
639
|
+
() => undefined,
|
|
640
|
+
);
|
|
560
641
|
const account = await importAccount(
|
|
561
|
-
JSON.parse(await readFile(
|
|
642
|
+
JSON.parse(await readFile("./account.json", "utf8")),
|
|
562
643
|
);
|
|
563
644
|
|
|
564
645
|
const result = await renewCertificate({
|
|
565
646
|
currentCertificatePem,
|
|
566
|
-
domains: [
|
|
647
|
+
domains: ["api.example.com"],
|
|
567
648
|
dnsProvider: dns,
|
|
568
|
-
email:
|
|
649
|
+
email: "ops@example.com",
|
|
569
650
|
account,
|
|
570
|
-
renewWhenDaysRemaining: 30,
|
|
651
|
+
renewWhenDaysRemaining: 30, // default
|
|
571
652
|
});
|
|
572
653
|
|
|
573
654
|
if (result.renewed) {
|
|
574
655
|
console.log(`renewed (${result.reason})`);
|
|
575
656
|
await installCertificateOnTarget(target, result.certificate, {
|
|
576
|
-
reload:
|
|
657
|
+
reload: "systemctl reload nginx",
|
|
577
658
|
});
|
|
578
|
-
await writeFile(
|
|
579
|
-
await writeFile(
|
|
659
|
+
await writeFile("./cert.pem", result.certificate.certificatePem);
|
|
660
|
+
await writeFile("./key.pem", result.certificate.privateKeyPem);
|
|
580
661
|
} else {
|
|
581
662
|
console.log(
|
|
582
|
-
`still fresh — ${result.inspection.daysRemaining} days remaining
|
|
663
|
+
`still fresh — ${result.inspection.daysRemaining} days remaining`,
|
|
583
664
|
);
|
|
584
665
|
}
|
|
585
666
|
```
|
|
@@ -637,17 +718,17 @@ and traffic cutover remain control-plane responsibilities.
|
|
|
637
718
|
|
|
638
719
|
Compute (`Target` via `createCloudTarget`):
|
|
639
720
|
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
721
|
+
- `@absolutejs/deploy/digitalocean` — droplets
|
|
722
|
+
- `@absolutejs/deploy/hetzner` — Hetzner Cloud servers
|
|
723
|
+
- `@absolutejs/deploy/linode` — Linode instances
|
|
724
|
+
- `@absolutejs/deploy/vultr` — Vultr instances
|
|
644
725
|
|
|
645
726
|
DNS (`DnsProvider`):
|
|
646
727
|
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
728
|
+
- `@absolutejs/deploy/cloudflare`
|
|
729
|
+
- `@absolutejs/deploy/digitalocean-dns`
|
|
730
|
+
- `@absolutejs/deploy/hetzner-dns`
|
|
731
|
+
- `@absolutejs/deploy/route53` — narrow client interface; BYO `@aws-sdk/client-route-53` via a 4-line shim, or hand-roll a SigV4 fetch client.
|
|
651
732
|
|
|
652
733
|
All compute adapters share the same `createCloudTarget` machinery
|
|
653
734
|
(find-or-create + wait-for-ready + wait-for-SSH + sshTarget wrap)
|