@absolutejs/deploy 0.22.0 → 0.24.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 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
 
@@ -48,6 +47,71 @@ both publication and promotion for intentionally non-publishable local-testing
48
47
  artifacts. The registry uses the structural BlobStore shape, so Deploy does not
49
48
  take a runtime dependency on `@absolutejs/blob` or a cloud SDK.
50
49
 
50
+ ### Google Play distribution (0.23.0)
51
+
52
+ `@absolutejs/deploy/google-play` composes the native registry with Google Play.
53
+ The same BlobStore persists stage receipts before external effects, allowing a
54
+ retry to reuse an unexpired edit or resumable upload and to reconcile a commit
55
+ whose successful response was lost.
56
+
57
+ ```ts
58
+ import { createGooglePlayReleasePublisher } from "@absolutejs/deploy/google-play";
59
+
60
+ export default createGooglePlayReleasePublisher({
61
+ receiptStore: store,
62
+ registry: releases,
63
+ });
64
+ ```
65
+
66
+ AbsoluteJS supplies the requested track and rollout intent when the developer
67
+ runs `absolute mobile publish android --play-track internal`. Google
68
+ Application Default Credentials must have Android Publisher access to the app.
69
+ Before Gradle runs, the publisher inspects Play's existing bundles and returns
70
+ the next version code. It persists that allocation by application and complete
71
+ build identity, so retries and promotion to another track reuse the same code.
72
+ AbsoluteJS injects it into the AAB and the publisher verifies the upload response
73
+ returned that exact code. Serialize publication jobs for one application because
74
+ Google Play provides monotonic version codes but no reservation operation.
75
+ The default commit behavior is `ERROR_IF_IN_REVIEW`; opting into cancellation
76
+ of an existing review must therefore be explicit. Production staged rollouts
77
+ use `status: 'inProgress'` with `0 < userFraction < 1`; subsequent calls for the
78
+ same release can increase the fraction, halt or resume it, or mark it completed
79
+ without uploading the AAB again.
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
+
51
115
  ## Infrastructure providers (0.14.0)
52
116
 
53
117
  Control planes use the normalized `InfrastructureProvider` contract from
@@ -56,25 +120,27 @@ live in this package beside their deploy targets so providers never become
56
120
  scattered across host applications.
57
121
 
58
122
  ```ts
59
- import { createDigitalOceanInfrastructureProvider } from '@absolutejs/deploy/digitalocean-infrastructure';
123
+ import { createDigitalOceanInfrastructureProvider } from "@absolutejs/deploy/digitalocean-infrastructure";
60
124
 
61
125
  const provider = createDigitalOceanInfrastructureProvider({
62
126
  token: process.env.DIGITALOCEAN_TOKEN!,
63
- tag: 'absolutejs-paas-node',
64
- regions: [{
65
- region: 'nyc3',
66
- size: 's-2vcpu-4gb',
67
- image: 'ubuntu-24-04-x64',
68
- sshKeys: [process.env.DIGITALOCEAN_SSH_KEY!],
69
- userData: process.env.ABSOLUTEJS_NODE_CLOUD_INIT,
70
- }],
127
+ tag: "absolutejs-paas-node",
128
+ regions: [
129
+ {
130
+ region: "nyc3",
131
+ size: "s-2vcpu-4gb",
132
+ image: "ubuntu-24-04-x64",
133
+ sshKeys: [process.env.DIGITALOCEAN_SSH_KEY!],
134
+ userData: process.env.ABSOLUTEJS_NODE_CLOUD_INIT,
135
+ },
136
+ ],
71
137
  agent: { preferPrivateNetwork: true, port: 8081 },
72
138
  });
73
139
 
74
140
  await provider.listNodes();
75
141
  await provider.provisionNode({
76
142
  idempotencyKey: crypto.randomUUID(),
77
- name: 'absolutejs-node-01',
143
+ name: "absolutejs-node-01",
78
144
  });
79
145
  ```
80
146
 
@@ -100,26 +166,26 @@ and idempotent removal. Provider resource construction stays here instead of
100
166
  leaking DigitalOcean or GCP APIs into a control plane.
101
167
 
102
168
  ```ts
103
- import { createDigitalOceanIngressProvider } from '@absolutejs/deploy/digitalocean-ingress';
169
+ import { createDigitalOceanIngressProvider } from "@absolutejs/deploy/digitalocean-ingress";
104
170
 
105
171
  const ingress = createDigitalOceanIngressProvider({
106
172
  token: process.env.DIGITALOCEAN_TOKEN!,
107
173
  });
108
174
 
109
175
  await ingress.reconcileIngress({
110
- name: 'absolutejs-edge',
176
+ name: "absolutejs-edge",
111
177
  idempotencyKey: crypto.randomUUID(),
112
178
  backends: [
113
- { region: 'nyc3', resourceId: 'regional-lb-east', priority: 1 },
114
- { region: 'sfo3', resourceId: 'regional-lb-west', priority: 2 },
179
+ { region: "nyc3", resourceId: "regional-lb-east", priority: 1 },
180
+ { region: "sfo3", resourceId: "regional-lb-west", priority: 2 },
115
181
  ],
116
182
  listener: {
117
183
  port: 443,
118
- protocol: 'https',
184
+ protocol: "https",
119
185
  targetPort: 443,
120
186
  tlsPassthrough: true,
121
187
  },
122
- healthCheck: { protocol: 'tcp', port: 443 },
188
+ healthCheck: { protocol: "tcp", port: 443 },
123
189
  });
124
190
  ```
125
191
 
@@ -132,26 +198,25 @@ operations before advancing dependent resources. Creating an adapter does not
132
198
  provision anything; only `reconcileIngress()` mutates provider state.
133
199
 
134
200
  ```ts
135
- import {
136
- createDeployer,
137
- sshTarget,
138
- systemdManager,
139
- } from '@absolutejs/deploy';
201
+ import { createDeployer, sshTarget, systemdManager } from "@absolutejs/deploy";
140
202
 
141
203
  const deployer = createDeployer({
142
- appName: 'my-app',
204
+ appName: "my-app",
143
205
  target: sshTarget({
144
- host: 'droplet-1.example.com',
145
- user: 'deploy',
146
- identity: '~/.ssh/id_ed25519',
206
+ host: "droplet-1.example.com",
207
+ user: "deploy",
208
+ identity: "~/.ssh/id_ed25519",
147
209
  }),
148
- source: { kind: 'directory', root: './' },
149
- env: { PORT: '3000', DATABASE_URL: process.env.DATABASE_URL! },
150
- processManager: systemdManager({ user: 'deploy' }),
151
- verify: { kind: 'http', url: 'http://localhost:3000/health' },
210
+ source: { kind: "directory", root: "./" },
211
+ env: { PORT: "3000", DATABASE_URL: process.env.DATABASE_URL! },
212
+ processManager: systemdManager({ user: "deploy" }),
213
+ verify: { kind: "http", url: "http://localhost:3000/health" },
152
214
  hooks: {
153
215
  onStepStart: ({ name, releaseId }) => console.log(`▸ ${releaseId} ${name}`),
154
- onLog: (line, stream, step) => process[stream === 'stderr' ? 'stderr' : 'stdout'].write(`[${step}] ${line}\n`),
216
+ onLog: (line, stream, step) =>
217
+ process[stream === "stderr" ? "stderr" : "stdout"].write(
218
+ `[${step}] ${line}\n`,
219
+ ),
155
220
  },
156
221
  });
157
222
 
@@ -182,9 +247,9 @@ await deployer.stop();
182
247
 
183
248
  ### Targets
184
249
 
185
- | Adapter | Use |
186
- |---|---|
187
- | `localTarget({ root, env? })` | Tests, local-dev, and "deploy to the same box" workflows. |
250
+ | Adapter | Use |
251
+ | ----------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
252
+ | `localTarget({ root, env? })` | Tests, local-dev, and "deploy to the same box" workflows. |
188
253
  | `sshTarget({ host, user?, port?, identity?, sshFlags?, rsync? })` | Any VPS — DigitalOcean Droplets, Linode, Hetzner, Vultr, Lightsail, Scaleway. Uses the system `ssh` / `rsync` — no npm dep. |
189
254
 
190
255
  A `Target` is just:
@@ -192,8 +257,15 @@ A `Target` is just:
192
257
  ```ts
193
258
  type Target = {
194
259
  description: string;
195
- exec(cmd: string, opts?: { cwd?; env?; timeoutMs?; onLog?; stdin? }): Promise<{ stdout; stderr; exitCode }>;
196
- upload(local: string, remote: string, opts?: { exclude?; deleteOrphans? }): Promise<void>;
260
+ exec(
261
+ cmd: string,
262
+ opts?: { cwd?; env?; timeoutMs?; onLog?; stdin? },
263
+ ): Promise<{ stdout; stderr; exitCode }>;
264
+ upload(
265
+ local: string,
266
+ remote: string,
267
+ opts?: { exclude?; deleteOrphans? },
268
+ ): Promise<void>;
197
269
  close?(): Promise<void>;
198
270
  };
199
271
  ```
@@ -202,10 +274,10 @@ If you can implement those two methods, you can deploy through `@absolutejs/depl
202
274
 
203
275
  ### Process managers
204
276
 
205
- | Manager | What it does |
206
- |---|---|
207
- | `bareManager({ command? })` | Default. `nohup bun run start &`, pid file under `/var/lib/<appName>/`, logs to `/var/log/<appName>/`. Zero remote dependency. |
208
- | `systemdManager({ user?, group?, execStart?, restart?, ... })` | Templated systemd unit pointing at `current/`, `daemon-reload` + `restart`. The production answer for VMs. |
277
+ | Manager | What it does |
278
+ | -------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
279
+ | `bareManager({ command? })` | Default. `nohup bun run start &`, pid file under `/var/lib/<appName>/`, logs to `/var/log/<appName>/`. Zero remote dependency. |
280
+ | `systemdManager({ user?, group?, execStart?, restart?, ... })` | Templated systemd unit pointing at `current/`, `daemon-reload` + `restart`. The production answer for VMs. |
209
281
 
210
282
  A `ProcessManager` is just `{ reload, stop?, status? }`. Wrap PM2, supervisord, runit, or even `@absolutejs/runtime` — whatever your remote uses.
211
283
 
@@ -249,25 +321,25 @@ up a droplet by name; creates it via the v2 API if absent; waits for
249
321
  hand to `createDeployer`.
250
322
 
251
323
  ```ts
252
- import { createDeployer } from '@absolutejs/deploy';
253
- import { digitalOceanTarget } from '@absolutejs/deploy/digitalocean';
324
+ import { createDeployer } from "@absolutejs/deploy";
325
+ import { digitalOceanTarget } from "@absolutejs/deploy/digitalocean";
254
326
 
255
327
  const target = await digitalOceanTarget({
256
328
  token: process.env.DO_TOKEN!,
257
- name: 'absolutejs-prod-1', // idempotency key
258
- region: 'nyc3',
259
- size: 's-1vcpu-1gb',
260
- image: 'ubuntu-22-04-x64',
329
+ name: "absolutejs-prod-1", // idempotency key
330
+ region: "nyc3",
331
+ size: "s-1vcpu-1gb",
332
+ image: "ubuntu-22-04-x64",
261
333
  sshKeys: [process.env.DO_KEY_FINGERPRINT!],
262
- tags: ['absolutejs'],
263
- userData: '#!/bin/bash\ncurl -fsSL https://bun.sh/install | bash',
334
+ tags: ["absolutejs"],
335
+ userData: "#!/bin/bash\ncurl -fsSL https://bun.sh/install | bash",
264
336
  onLog: (line) => console.log(line),
265
337
  });
266
338
 
267
339
  console.log(`droplet ${target.dropletId} at ${target.ipv4}`);
268
340
 
269
- const deployer = createDeployer({ appName: 'my-app', target });
270
- await deployer.deploy({ source: { kind: 'directory', path: './build' } });
341
+ const deployer = createDeployer({ appName: "my-app", target });
342
+ await deployer.deploy({ source: { kind: "directory", path: "./build" } });
271
343
 
272
344
  // Tear it down when you're done:
273
345
  await target.destroy();
@@ -294,22 +366,22 @@ mappings underneath. Hetzner-specific differences: locations
294
366
  and public-net IPv4/IPv6 are independently toggleable.
295
367
 
296
368
  ```ts
297
- import { createDeployer } from '@absolutejs/deploy';
298
- import { hetznerTarget } from '@absolutejs/deploy/hetzner';
369
+ import { createDeployer } from "@absolutejs/deploy";
370
+ import { hetznerTarget } from "@absolutejs/deploy/hetzner";
299
371
 
300
372
  const target = await hetznerTarget({
301
373
  token: process.env.HETZNER_TOKEN!,
302
- name: 'absolutejs-prod-1',
303
- location: 'nbg1',
304
- serverType: 'cx22',
305
- image: 'ubuntu-22.04',
374
+ name: "absolutejs-prod-1",
375
+ location: "nbg1",
376
+ serverType: "cx22",
377
+ image: "ubuntu-22.04",
306
378
  sshKeys: [process.env.HETZNER_KEY_FINGERPRINT!],
307
- labels: { env: 'prod', team: 'platform' },
308
- userData: '#!/bin/bash\ncurl -fsSL https://bun.sh/install | bash',
379
+ labels: { env: "prod", team: "platform" },
380
+ userData: "#!/bin/bash\ncurl -fsSL https://bun.sh/install | bash",
309
381
  });
310
382
 
311
- const deployer = createDeployer({ appName: 'my-app', target });
312
- await deployer.deploy({ source: { kind: 'directory', path: './build' } });
383
+ const deployer = createDeployer({ appName: "my-app", target });
384
+ await deployer.deploy({ source: { kind: "directory", path: "./build" } });
313
385
  await target.destroy();
314
386
  ```
315
387
 
@@ -327,11 +399,13 @@ implements the shared `DnsProvider` contract from
327
399
  `@absolutejs/deploy/dns`.
328
400
 
329
401
  ```ts
330
- import { hetznerTarget } from '@absolutejs/deploy/hetzner';
331
- import { cloudflareProvider } from '@absolutejs/deploy/cloudflare';
332
- import { ensureDnsForTarget } from '@absolutejs/deploy/dns';
402
+ import { hetznerTarget } from "@absolutejs/deploy/hetzner";
403
+ import { cloudflareProvider } from "@absolutejs/deploy/cloudflare";
404
+ import { ensureDnsForTarget } from "@absolutejs/deploy/dns";
333
405
 
334
- const target = await hetznerTarget({ /* … */ });
406
+ const target = await hetznerTarget({
407
+ /* … */
408
+ });
335
409
  const dns = cloudflareProvider({
336
410
  token: process.env.CLOUDFLARE_TOKEN!,
337
411
  zoneId: process.env.CLOUDFLARE_ZONE_ID!,
@@ -339,7 +413,7 @@ const dns = cloudflareProvider({
339
413
 
340
414
  // Idempotent — create or update so the A record points at target.ipv4.
341
415
  await ensureDnsForTarget(dns, {
342
- name: 'api.example.com',
416
+ name: "api.example.com",
343
417
  target,
344
418
  ttl: 60,
345
419
  proxied: false,
@@ -374,36 +448,38 @@ against Bun's `crypto.subtle`. The audit surface stays in this
374
448
  repo.
375
449
 
376
450
  ```ts
377
- import { hetznerTarget } from '@absolutejs/deploy/hetzner';
378
- import { cloudflareProvider } from '@absolutejs/deploy/cloudflare';
379
- import { ensureDnsForTarget } from '@absolutejs/deploy/dns';
451
+ import { hetznerTarget } from "@absolutejs/deploy/hetzner";
452
+ import { cloudflareProvider } from "@absolutejs/deploy/cloudflare";
453
+ import { ensureDnsForTarget } from "@absolutejs/deploy/dns";
380
454
  import {
381
455
  issueCertificate,
382
456
  installCertificateOnTarget,
383
457
  LETSENCRYPT_PRODUCTION,
384
- } from '@absolutejs/deploy/tls';
458
+ } from "@absolutejs/deploy/tls";
385
459
 
386
- const target = await hetznerTarget({ /* … */ });
460
+ const target = await hetznerTarget({
461
+ /* … */
462
+ });
387
463
  const dns = cloudflareProvider({
388
464
  token: process.env.CLOUDFLARE_TOKEN!,
389
465
  zoneId: process.env.CLOUDFLARE_ZONE_ID!,
390
466
  });
391
467
 
392
468
  // 1. Point DNS at the box.
393
- await ensureDnsForTarget(dns, { name: 'api.example.com', target, ttl: 60 });
469
+ await ensureDnsForTarget(dns, { name: "api.example.com", target, ttl: 60 });
394
470
 
395
471
  // 2. Issue a cert via DNS-01.
396
472
  const cert = await issueCertificate({
397
- domains: ['api.example.com'],
473
+ domains: ["api.example.com"],
398
474
  dnsProvider: dns,
399
- email: 'ops@example.com',
475
+ email: "ops@example.com",
400
476
  directoryUrl: LETSENCRYPT_PRODUCTION,
401
477
  onLog: (line) => console.log(line),
402
478
  });
403
479
 
404
480
  // 3. Install on the box.
405
481
  await installCertificateOnTarget(target, cert, {
406
- reload: 'systemctl reload nginx',
482
+ reload: "systemctl reload nginx",
407
483
  });
408
484
  ```
409
485
 
@@ -414,9 +490,9 @@ map the provider write while leaving propagation checks on the public name:
414
490
 
415
491
  ```ts
416
492
  const cert = await issueCertificate({
417
- domains: ['app.customer.com'],
493
+ domains: ["app.customer.com"],
418
494
  dnsProvider: platformValidationDns,
419
- email: 'ops@platform.example',
495
+ email: "ops@platform.example",
420
496
  mapDnsChallengeRecord: ({ domain }) =>
421
497
  `${stableDomainToken(domain)}.acme.platform.example`,
422
498
  });
@@ -442,43 +518,43 @@ this module handles the deploy-side (push values to remote env
442
518
  files, atomic swap, conditional service reload).
443
519
 
444
520
  ```ts
445
- import { createSecretBroker, inMemoryAdapter } from '@absolutejs/secrets';
446
- import { hetznerTarget } from '@absolutejs/deploy/hetzner';
521
+ import { createSecretBroker, inMemoryAdapter } from "@absolutejs/secrets";
522
+ import { hetznerTarget } from "@absolutejs/deploy/hetzner";
447
523
  import {
448
524
  syncSecretsToDeployments,
449
525
  deploymentsUsing,
450
526
  type EnvDeployment,
451
- } from '@absolutejs/deploy/env';
527
+ } from "@absolutejs/deploy/env";
452
528
 
453
529
  // One source of truth — the SecretBroker. Swap in whatever adapter
454
530
  // (env, file, vault, etc.) makes sense for your team.
455
531
  const broker = createSecretBroker({
456
532
  adapter: inMemoryAdapter({
457
533
  initial: {
458
- DATABASE_URL: 'postgres://prod-db',
459
- STRIPE_KEY: 'sk_live_old',
534
+ DATABASE_URL: "postgres://prod-db",
535
+ STRIPE_KEY: "sk_live_old",
460
536
  },
461
537
  }),
462
538
  });
463
539
 
464
540
  // Each deployed service is one EnvDeployment.
465
- const api = await hetznerTarget({ name: 'api-1', /* … */ });
466
- const worker = await hetznerTarget({ name: 'worker-1', /* … */ });
541
+ const api = await hetznerTarget({ name: "api-1" /* … */ });
542
+ const worker = await hetznerTarget({ name: "worker-1" /* … */ });
467
543
 
468
544
  const deployments: EnvDeployment[] = [
469
545
  {
470
546
  target: api,
471
- remotePath: '/etc/api.env',
472
- secretNames: ['STRIPE_KEY', 'DATABASE_URL'],
473
- extras: { NODE_ENV: 'production', PORT: '3000' },
474
- reload: 'systemctl reload api',
547
+ remotePath: "/etc/api.env",
548
+ secretNames: ["STRIPE_KEY", "DATABASE_URL"],
549
+ extras: { NODE_ENV: "production", PORT: "3000" },
550
+ reload: "systemctl reload api",
475
551
  },
476
552
  {
477
553
  target: worker,
478
- remotePath: '/etc/worker.env',
479
- secretNames: ['DATABASE_URL'],
480
- extras: { NODE_ENV: 'production' },
481
- reload: 'systemctl restart worker',
554
+ remotePath: "/etc/worker.env",
555
+ secretNames: ["DATABASE_URL"],
556
+ extras: { NODE_ENV: "production" },
557
+ reload: "systemctl restart worker",
482
558
  },
483
559
  ];
484
560
 
@@ -486,10 +562,10 @@ const deployments: EnvDeployment[] = [
486
562
  await syncSecretsToDeployments(broker, deployments);
487
563
 
488
564
  // Rotate STRIPE_KEY everywhere it's used:
489
- await broker.rotate('STRIPE_KEY');
565
+ await broker.rotate("STRIPE_KEY");
490
566
  await syncSecretsToDeployments(
491
567
  broker,
492
- deploymentsUsing('STRIPE_KEY', deployments)
568
+ deploymentsUsing("STRIPE_KEY", deployments),
493
569
  );
494
570
  ```
495
571
 
@@ -522,33 +598,35 @@ import {
522
598
  renewCertificate,
523
599
  installCertificateOnTarget,
524
600
  importAccount,
525
- } from '@absolutejs/deploy/tls';
526
- import { readFile, writeFile } from 'node:fs/promises';
601
+ } from "@absolutejs/deploy/tls";
602
+ import { readFile, writeFile } from "node:fs/promises";
527
603
 
528
- const currentCertificatePem = await readFile('./cert.pem', 'utf8').catch(() => undefined);
604
+ const currentCertificatePem = await readFile("./cert.pem", "utf8").catch(
605
+ () => undefined,
606
+ );
529
607
  const account = await importAccount(
530
- JSON.parse(await readFile('./account.json', 'utf8'))
608
+ JSON.parse(await readFile("./account.json", "utf8")),
531
609
  );
532
610
 
533
611
  const result = await renewCertificate({
534
612
  currentCertificatePem,
535
- domains: ['api.example.com'],
613
+ domains: ["api.example.com"],
536
614
  dnsProvider: dns,
537
- email: 'ops@example.com',
615
+ email: "ops@example.com",
538
616
  account,
539
- renewWhenDaysRemaining: 30, // default
617
+ renewWhenDaysRemaining: 30, // default
540
618
  });
541
619
 
542
620
  if (result.renewed) {
543
621
  console.log(`renewed (${result.reason})`);
544
622
  await installCertificateOnTarget(target, result.certificate, {
545
- reload: 'systemctl reload nginx',
623
+ reload: "systemctl reload nginx",
546
624
  });
547
- await writeFile('./cert.pem', result.certificate.certificatePem);
548
- await writeFile('./key.pem', result.certificate.privateKeyPem);
625
+ await writeFile("./cert.pem", result.certificate.certificatePem);
626
+ await writeFile("./key.pem", result.certificate.privateKeyPem);
549
627
  } else {
550
628
  console.log(
551
- `still fresh — ${result.inspection.daysRemaining} days remaining`
629
+ `still fresh — ${result.inspection.daysRemaining} days remaining`,
552
630
  );
553
631
  }
554
632
  ```
@@ -606,17 +684,17 @@ and traffic cutover remain control-plane responsibilities.
606
684
 
607
685
  Compute (`Target` via `createCloudTarget`):
608
686
 
609
- - `@absolutejs/deploy/digitalocean` — droplets
610
- - `@absolutejs/deploy/hetzner` — Hetzner Cloud servers
611
- - `@absolutejs/deploy/linode` — Linode instances
612
- - `@absolutejs/deploy/vultr` — Vultr instances
687
+ - `@absolutejs/deploy/digitalocean` — droplets
688
+ - `@absolutejs/deploy/hetzner` — Hetzner Cloud servers
689
+ - `@absolutejs/deploy/linode` — Linode instances
690
+ - `@absolutejs/deploy/vultr` — Vultr instances
613
691
 
614
692
  DNS (`DnsProvider`):
615
693
 
616
- - `@absolutejs/deploy/cloudflare`
617
- - `@absolutejs/deploy/digitalocean-dns`
618
- - `@absolutejs/deploy/hetzner-dns`
619
- - `@absolutejs/deploy/route53` — narrow client interface; BYO `@aws-sdk/client-route-53` via a 4-line shim, or hand-roll a SigV4 fetch client.
694
+ - `@absolutejs/deploy/cloudflare`
695
+ - `@absolutejs/deploy/digitalocean-dns`
696
+ - `@absolutejs/deploy/hetzner-dns`
697
+ - `@absolutejs/deploy/route53` — narrow client interface; BYO `@aws-sdk/client-route-53` via a 4-line shim, or hand-roll a SigV4 fetch client.
620
698
 
621
699
  All compute adapters share the same `createCloudTarget` machinery
622
700
  (find-or-create + wait-for-ready + wait-for-SSH + sshTarget wrap)