@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.
Files changed (79) hide show
  1. package/README.md +204 -123
  2. package/dist/appStoreConnect.d.ts +182 -0
  3. package/dist/appStoreConnect.js +587 -0
  4. package/dist/appStoreConnect.js.map +10 -0
  5. package/dist/cloudTarget.d.ts +4 -4
  6. package/dist/cloudflare.d.ts +2 -2
  7. package/dist/cloudflare.js +11 -9
  8. package/dist/cloudflare.js.map +3 -3
  9. package/dist/deployer.d.ts +9 -9
  10. package/dist/digitalocean.d.ts +5 -5
  11. package/dist/digitalocean.js +21 -13
  12. package/dist/digitalocean.js.map +5 -5
  13. package/dist/digitaloceanDns.d.ts +2 -2
  14. package/dist/digitaloceanDns.js +16 -8
  15. package/dist/digitaloceanDns.js.map +6 -6
  16. package/dist/digitaloceanEphemeralInfrastructure.js +16 -8
  17. package/dist/digitaloceanEphemeralInfrastructure.js.map +5 -5
  18. package/dist/digitaloceanInfrastructure.js +16 -8
  19. package/dist/digitaloceanInfrastructure.js.map +6 -6
  20. package/dist/digitaloceanIngress.js +16 -8
  21. package/dist/digitaloceanIngress.js.map +5 -5
  22. package/dist/dns.d.ts +1 -1
  23. package/dist/dns.js +9 -7
  24. package/dist/dns.js.map +3 -3
  25. package/dist/edgeIngress.js +11 -9
  26. package/dist/edgeIngress.js.map +2 -2
  27. package/dist/env.d.ts +1 -1
  28. package/dist/env.js +13 -11
  29. package/dist/env.js.map +3 -3
  30. package/dist/gcp.js +99 -93
  31. package/dist/gcp.js.map +3 -3
  32. package/dist/gcpIngress.js +93 -91
  33. package/dist/gcpIngress.js.map +2 -2
  34. package/dist/googlePlay.d.ts +10 -2
  35. package/dist/googlePlay.js +96 -91
  36. package/dist/googlePlay.js.map +3 -3
  37. package/dist/hetzner.d.ts +3 -3
  38. package/dist/hetzner.js +21 -13
  39. package/dist/hetzner.js.map +5 -5
  40. package/dist/hetznerDns.d.ts +2 -2
  41. package/dist/hetznerDns.js +11 -9
  42. package/dist/hetznerDns.js.map +3 -3
  43. package/dist/hetznerInfrastructure.js +16 -8
  44. package/dist/hetznerInfrastructure.js.map +7 -7
  45. package/dist/index.d.ts +5 -1
  46. package/dist/index.js +1347 -284
  47. package/dist/index.js.map +10 -8
  48. package/dist/linode.d.ts +3 -3
  49. package/dist/linode.js +21 -13
  50. package/dist/linode.js.map +5 -5
  51. package/dist/linodeInfrastructure.js +21 -9
  52. package/dist/linodeInfrastructure.js.map +7 -7
  53. package/dist/managedPreview.js +9 -7
  54. package/dist/managedPreview.js.map +2 -2
  55. package/dist/mobileUpdate.d.ts +109 -0
  56. package/dist/mobileUpdate.js +464 -0
  57. package/dist/mobileUpdate.js.map +10 -0
  58. package/dist/nativeRelease.d.ts +17 -1
  59. package/dist/nativeRelease.js +63 -22
  60. package/dist/nativeRelease.js.map +3 -3
  61. package/dist/preview.d.ts +3 -3
  62. package/dist/preview.js +15 -10
  63. package/dist/preview.js.map +3 -3
  64. package/dist/processManagers.d.ts +4 -4
  65. package/dist/releaseArtifact.js +12 -10
  66. package/dist/releaseArtifact.js.map +2 -2
  67. package/dist/route53.d.ts +2 -2
  68. package/dist/route53.js +9 -7
  69. package/dist/route53.js.map +3 -3
  70. package/dist/targets.d.ts +1 -1
  71. package/dist/tls.d.ts +5 -5
  72. package/dist/tls.js +23 -18
  73. package/dist/tls.js.map +3 -3
  74. package/dist/vultr.d.ts +4 -4
  75. package/dist/vultr.js +21 -13
  76. package/dist/vultr.js.map +5 -5
  77. package/dist/vultrInfrastructure.js +16 -8
  78. package/dist/vultrInfrastructure.js.map +7 -7
  79. 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` through any `@absolutejs/blob`
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 '@absolutejs/deploy/native-release';
25
- import { s3BlobStore } from '@absolutejs/blob/s3';
24
+ import { createNativeReleaseRegistry } from "@absolutejs/deploy/native-release";
25
+ import { s3BlobStore } from "@absolutejs/blob/s3";
26
26
 
27
- const store = s3BlobStore({ client, bucket: 'absolute-releases' });
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
- '.absolutejs/mobile/releases/android/amobile_android_<sha256>',
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: 'android',
37
+ platform: "android",
39
38
  releaseId: published.record.metadata.releaseId,
40
- channel: 'production'
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 '@absolutejs/deploy/google-play';
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 '@absolutejs/deploy/digitalocean-infrastructure';
157
+ import { createDigitalOceanInfrastructureProvider } from "@absolutejs/deploy/digitalocean-infrastructure";
91
158
 
92
159
  const provider = createDigitalOceanInfrastructureProvider({
93
160
  token: process.env.DIGITALOCEAN_TOKEN!,
94
- tag: 'absolutejs-paas-node',
95
- regions: [{
96
- region: 'nyc3',
97
- size: 's-2vcpu-4gb',
98
- image: 'ubuntu-24-04-x64',
99
- sshKeys: [process.env.DIGITALOCEAN_SSH_KEY!],
100
- userData: process.env.ABSOLUTEJS_NODE_CLOUD_INIT,
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: 'absolutejs-node-01',
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 '@absolutejs/deploy/digitalocean-ingress';
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: 'absolutejs-edge',
210
+ name: "absolutejs-edge",
142
211
  idempotencyKey: crypto.randomUUID(),
143
212
  backends: [
144
- { region: 'nyc3', resourceId: 'regional-lb-east', priority: 1 },
145
- { region: 'sfo3', resourceId: 'regional-lb-west', priority: 2 },
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: 'https',
218
+ protocol: "https",
150
219
  targetPort: 443,
151
220
  tlsPassthrough: true,
152
221
  },
153
- healthCheck: { protocol: 'tcp', port: 443 },
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: 'my-app',
238
+ appName: "my-app",
174
239
  target: sshTarget({
175
- host: 'droplet-1.example.com',
176
- user: 'deploy',
177
- identity: '~/.ssh/id_ed25519',
240
+ host: "droplet-1.example.com",
241
+ user: "deploy",
242
+ identity: "~/.ssh/id_ed25519",
178
243
  }),
179
- source: { kind: 'directory', root: './' },
180
- env: { PORT: '3000', DATABASE_URL: process.env.DATABASE_URL! },
181
- processManager: systemdManager({ user: 'deploy' }),
182
- verify: { kind: 'http', url: 'http://localhost:3000/health' },
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) => process[stream === 'stderr' ? 'stderr' : 'stdout'].write(`[${step}] ${line}\n`),
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 | Use |
217
- |---|---|
218
- | `localTarget({ root, env? })` | Tests, local-dev, and "deploy to the same box" workflows. |
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(cmd: string, opts?: { cwd?; env?; timeoutMs?; onLog?; stdin? }): Promise<{ stdout; stderr; exitCode }>;
227
- upload(local: string, remote: string, opts?: { exclude?; deleteOrphans? }): Promise<void>;
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 | What it does |
237
- |---|---|
238
- | `bareManager({ command? })` | Default. `nohup bun run start &`, pid file under `/var/lib/<appName>/`, logs to `/var/log/<appName>/`. Zero remote dependency. |
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 '@absolutejs/deploy';
284
- import { digitalOceanTarget } from '@absolutejs/deploy/digitalocean';
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: 'absolutejs-prod-1', // idempotency key
289
- region: 'nyc3',
290
- size: 's-1vcpu-1gb',
291
- image: 'ubuntu-22-04-x64',
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: ['absolutejs'],
294
- userData: '#!/bin/bash\ncurl -fsSL https://bun.sh/install | bash',
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: 'my-app', target });
301
- await deployer.deploy({ source: { kind: 'directory', path: './build' } });
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 '@absolutejs/deploy';
329
- import { hetznerTarget } from '@absolutejs/deploy/hetzner';
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: 'absolutejs-prod-1',
334
- location: 'nbg1',
335
- serverType: 'cx22',
336
- image: 'ubuntu-22.04',
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: 'prod', team: 'platform' },
339
- userData: '#!/bin/bash\ncurl -fsSL https://bun.sh/install | bash',
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: 'my-app', target });
343
- await deployer.deploy({ source: { kind: 'directory', path: './build' } });
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 '@absolutejs/deploy/hetzner';
362
- import { cloudflareProvider } from '@absolutejs/deploy/cloudflare';
363
- import { ensureDnsForTarget } from '@absolutejs/deploy/dns';
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: 'api.example.com',
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 '@absolutejs/deploy/hetzner';
409
- import { cloudflareProvider } from '@absolutejs/deploy/cloudflare';
410
- import { ensureDnsForTarget } from '@absolutejs/deploy/dns';
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 '@absolutejs/deploy/tls';
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: 'api.example.com', target, ttl: 60 });
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: ['api.example.com'],
507
+ domains: ["api.example.com"],
429
508
  dnsProvider: dns,
430
- email: 'ops@example.com',
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: 'systemctl reload nginx',
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: ['app.customer.com'],
527
+ domains: ["app.customer.com"],
449
528
  dnsProvider: platformValidationDns,
450
- email: 'ops@platform.example',
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 '@absolutejs/secrets';
477
- import { hetznerTarget } from '@absolutejs/deploy/hetzner';
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 '@absolutejs/deploy/env';
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: 'postgres://prod-db',
490
- STRIPE_KEY: 'sk_live_old',
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: 'api-1', /* … */ });
497
- const worker = await hetznerTarget({ name: 'worker-1', /* … */ });
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: '/etc/api.env',
503
- secretNames: ['STRIPE_KEY', 'DATABASE_URL'],
504
- extras: { NODE_ENV: 'production', PORT: '3000' },
505
- reload: 'systemctl reload api',
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: '/etc/worker.env',
510
- secretNames: ['DATABASE_URL'],
511
- extras: { NODE_ENV: 'production' },
512
- reload: 'systemctl restart worker',
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('STRIPE_KEY');
599
+ await broker.rotate("STRIPE_KEY");
521
600
  await syncSecretsToDeployments(
522
601
  broker,
523
- deploymentsUsing('STRIPE_KEY', deployments)
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 '@absolutejs/deploy/tls';
557
- import { readFile, writeFile } from 'node:fs/promises';
635
+ } from "@absolutejs/deploy/tls";
636
+ import { readFile, writeFile } from "node:fs/promises";
558
637
 
559
- const currentCertificatePem = await readFile('./cert.pem', 'utf8').catch(() => undefined);
638
+ const currentCertificatePem = await readFile("./cert.pem", "utf8").catch(
639
+ () => undefined,
640
+ );
560
641
  const account = await importAccount(
561
- JSON.parse(await readFile('./account.json', 'utf8'))
642
+ JSON.parse(await readFile("./account.json", "utf8")),
562
643
  );
563
644
 
564
645
  const result = await renewCertificate({
565
646
  currentCertificatePem,
566
- domains: ['api.example.com'],
647
+ domains: ["api.example.com"],
567
648
  dnsProvider: dns,
568
- email: 'ops@example.com',
649
+ email: "ops@example.com",
569
650
  account,
570
- renewWhenDaysRemaining: 30, // default
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: 'systemctl reload nginx',
657
+ reload: "systemctl reload nginx",
577
658
  });
578
- await writeFile('./cert.pem', result.certificate.certificatePem);
579
- await writeFile('./key.pem', result.certificate.privateKeyPem);
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
- - `@absolutejs/deploy/digitalocean` — droplets
641
- - `@absolutejs/deploy/hetzner` — Hetzner Cloud servers
642
- - `@absolutejs/deploy/linode` — Linode instances
643
- - `@absolutejs/deploy/vultr` — Vultr instances
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
- - `@absolutejs/deploy/cloudflare`
648
- - `@absolutejs/deploy/digitalocean-dns`
649
- - `@absolutejs/deploy/hetzner-dns`
650
- - `@absolutejs/deploy/route53` — narrow client interface; BYO `@aws-sdk/client-route-53` via a 4-line shim, or hand-roll a SigV4 fetch client.
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)