@absolutejs/deploy 0.23.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
 
@@ -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,40 @@ 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
+
82
115
  ## Infrastructure providers (0.14.0)
83
116
 
84
117
  Control planes use the normalized `InfrastructureProvider` contract from
@@ -87,25 +120,27 @@ live in this package beside their deploy targets so providers never become
87
120
  scattered across host applications.
88
121
 
89
122
  ```ts
90
- import { createDigitalOceanInfrastructureProvider } from '@absolutejs/deploy/digitalocean-infrastructure';
123
+ import { createDigitalOceanInfrastructureProvider } from "@absolutejs/deploy/digitalocean-infrastructure";
91
124
 
92
125
  const provider = createDigitalOceanInfrastructureProvider({
93
126
  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
- }],
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
+ ],
102
137
  agent: { preferPrivateNetwork: true, port: 8081 },
103
138
  });
104
139
 
105
140
  await provider.listNodes();
106
141
  await provider.provisionNode({
107
142
  idempotencyKey: crypto.randomUUID(),
108
- name: 'absolutejs-node-01',
143
+ name: "absolutejs-node-01",
109
144
  });
110
145
  ```
111
146
 
@@ -131,26 +166,26 @@ and idempotent removal. Provider resource construction stays here instead of
131
166
  leaking DigitalOcean or GCP APIs into a control plane.
132
167
 
133
168
  ```ts
134
- import { createDigitalOceanIngressProvider } from '@absolutejs/deploy/digitalocean-ingress';
169
+ import { createDigitalOceanIngressProvider } from "@absolutejs/deploy/digitalocean-ingress";
135
170
 
136
171
  const ingress = createDigitalOceanIngressProvider({
137
172
  token: process.env.DIGITALOCEAN_TOKEN!,
138
173
  });
139
174
 
140
175
  await ingress.reconcileIngress({
141
- name: 'absolutejs-edge',
176
+ name: "absolutejs-edge",
142
177
  idempotencyKey: crypto.randomUUID(),
143
178
  backends: [
144
- { region: 'nyc3', resourceId: 'regional-lb-east', priority: 1 },
145
- { 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 },
146
181
  ],
147
182
  listener: {
148
183
  port: 443,
149
- protocol: 'https',
184
+ protocol: "https",
150
185
  targetPort: 443,
151
186
  tlsPassthrough: true,
152
187
  },
153
- healthCheck: { protocol: 'tcp', port: 443 },
188
+ healthCheck: { protocol: "tcp", port: 443 },
154
189
  });
155
190
  ```
156
191
 
@@ -163,26 +198,25 @@ operations before advancing dependent resources. Creating an adapter does not
163
198
  provision anything; only `reconcileIngress()` mutates provider state.
164
199
 
165
200
  ```ts
166
- import {
167
- createDeployer,
168
- sshTarget,
169
- systemdManager,
170
- } from '@absolutejs/deploy';
201
+ import { createDeployer, sshTarget, systemdManager } from "@absolutejs/deploy";
171
202
 
172
203
  const deployer = createDeployer({
173
- appName: 'my-app',
204
+ appName: "my-app",
174
205
  target: sshTarget({
175
- host: 'droplet-1.example.com',
176
- user: 'deploy',
177
- identity: '~/.ssh/id_ed25519',
206
+ host: "droplet-1.example.com",
207
+ user: "deploy",
208
+ identity: "~/.ssh/id_ed25519",
178
209
  }),
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' },
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" },
183
214
  hooks: {
184
215
  onStepStart: ({ name, releaseId }) => console.log(`▸ ${releaseId} ${name}`),
185
- 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
+ ),
186
220
  },
187
221
  });
188
222
 
@@ -213,9 +247,9 @@ await deployer.stop();
213
247
 
214
248
  ### Targets
215
249
 
216
- | Adapter | Use |
217
- |---|---|
218
- | `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. |
219
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. |
220
254
 
221
255
  A `Target` is just:
@@ -223,8 +257,15 @@ A `Target` is just:
223
257
  ```ts
224
258
  type Target = {
225
259
  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>;
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>;
228
269
  close?(): Promise<void>;
229
270
  };
230
271
  ```
@@ -233,10 +274,10 @@ If you can implement those two methods, you can deploy through `@absolutejs/depl
233
274
 
234
275
  ### Process managers
235
276
 
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. |
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. |
240
281
 
241
282
  A `ProcessManager` is just `{ reload, stop?, status? }`. Wrap PM2, supervisord, runit, or even `@absolutejs/runtime` — whatever your remote uses.
242
283
 
@@ -280,25 +321,25 @@ up a droplet by name; creates it via the v2 API if absent; waits for
280
321
  hand to `createDeployer`.
281
322
 
282
323
  ```ts
283
- import { createDeployer } from '@absolutejs/deploy';
284
- import { digitalOceanTarget } from '@absolutejs/deploy/digitalocean';
324
+ import { createDeployer } from "@absolutejs/deploy";
325
+ import { digitalOceanTarget } from "@absolutejs/deploy/digitalocean";
285
326
 
286
327
  const target = await digitalOceanTarget({
287
328
  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',
329
+ name: "absolutejs-prod-1", // idempotency key
330
+ region: "nyc3",
331
+ size: "s-1vcpu-1gb",
332
+ image: "ubuntu-22-04-x64",
292
333
  sshKeys: [process.env.DO_KEY_FINGERPRINT!],
293
- tags: ['absolutejs'],
294
- userData: '#!/bin/bash\ncurl -fsSL https://bun.sh/install | bash',
334
+ tags: ["absolutejs"],
335
+ userData: "#!/bin/bash\ncurl -fsSL https://bun.sh/install | bash",
295
336
  onLog: (line) => console.log(line),
296
337
  });
297
338
 
298
339
  console.log(`droplet ${target.dropletId} at ${target.ipv4}`);
299
340
 
300
- const deployer = createDeployer({ appName: 'my-app', target });
301
- 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" } });
302
343
 
303
344
  // Tear it down when you're done:
304
345
  await target.destroy();
@@ -325,22 +366,22 @@ mappings underneath. Hetzner-specific differences: locations
325
366
  and public-net IPv4/IPv6 are independently toggleable.
326
367
 
327
368
  ```ts
328
- import { createDeployer } from '@absolutejs/deploy';
329
- import { hetznerTarget } from '@absolutejs/deploy/hetzner';
369
+ import { createDeployer } from "@absolutejs/deploy";
370
+ import { hetznerTarget } from "@absolutejs/deploy/hetzner";
330
371
 
331
372
  const target = await hetznerTarget({
332
373
  token: process.env.HETZNER_TOKEN!,
333
- name: 'absolutejs-prod-1',
334
- location: 'nbg1',
335
- serverType: 'cx22',
336
- image: 'ubuntu-22.04',
374
+ name: "absolutejs-prod-1",
375
+ location: "nbg1",
376
+ serverType: "cx22",
377
+ image: "ubuntu-22.04",
337
378
  sshKeys: [process.env.HETZNER_KEY_FINGERPRINT!],
338
- labels: { env: 'prod', team: 'platform' },
339
- 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",
340
381
  });
341
382
 
342
- const deployer = createDeployer({ appName: 'my-app', target });
343
- 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" } });
344
385
  await target.destroy();
345
386
  ```
346
387
 
@@ -358,11 +399,13 @@ implements the shared `DnsProvider` contract from
358
399
  `@absolutejs/deploy/dns`.
359
400
 
360
401
  ```ts
361
- import { hetznerTarget } from '@absolutejs/deploy/hetzner';
362
- import { cloudflareProvider } from '@absolutejs/deploy/cloudflare';
363
- 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";
364
405
 
365
- const target = await hetznerTarget({ /* … */ });
406
+ const target = await hetznerTarget({
407
+ /* … */
408
+ });
366
409
  const dns = cloudflareProvider({
367
410
  token: process.env.CLOUDFLARE_TOKEN!,
368
411
  zoneId: process.env.CLOUDFLARE_ZONE_ID!,
@@ -370,7 +413,7 @@ const dns = cloudflareProvider({
370
413
 
371
414
  // Idempotent — create or update so the A record points at target.ipv4.
372
415
  await ensureDnsForTarget(dns, {
373
- name: 'api.example.com',
416
+ name: "api.example.com",
374
417
  target,
375
418
  ttl: 60,
376
419
  proxied: false,
@@ -405,36 +448,38 @@ against Bun's `crypto.subtle`. The audit surface stays in this
405
448
  repo.
406
449
 
407
450
  ```ts
408
- import { hetznerTarget } from '@absolutejs/deploy/hetzner';
409
- import { cloudflareProvider } from '@absolutejs/deploy/cloudflare';
410
- 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";
411
454
  import {
412
455
  issueCertificate,
413
456
  installCertificateOnTarget,
414
457
  LETSENCRYPT_PRODUCTION,
415
- } from '@absolutejs/deploy/tls';
458
+ } from "@absolutejs/deploy/tls";
416
459
 
417
- const target = await hetznerTarget({ /* … */ });
460
+ const target = await hetznerTarget({
461
+ /* … */
462
+ });
418
463
  const dns = cloudflareProvider({
419
464
  token: process.env.CLOUDFLARE_TOKEN!,
420
465
  zoneId: process.env.CLOUDFLARE_ZONE_ID!,
421
466
  });
422
467
 
423
468
  // 1. Point DNS at the box.
424
- await ensureDnsForTarget(dns, { name: 'api.example.com', target, ttl: 60 });
469
+ await ensureDnsForTarget(dns, { name: "api.example.com", target, ttl: 60 });
425
470
 
426
471
  // 2. Issue a cert via DNS-01.
427
472
  const cert = await issueCertificate({
428
- domains: ['api.example.com'],
473
+ domains: ["api.example.com"],
429
474
  dnsProvider: dns,
430
- email: 'ops@example.com',
475
+ email: "ops@example.com",
431
476
  directoryUrl: LETSENCRYPT_PRODUCTION,
432
477
  onLog: (line) => console.log(line),
433
478
  });
434
479
 
435
480
  // 3. Install on the box.
436
481
  await installCertificateOnTarget(target, cert, {
437
- reload: 'systemctl reload nginx',
482
+ reload: "systemctl reload nginx",
438
483
  });
439
484
  ```
440
485
 
@@ -445,9 +490,9 @@ map the provider write while leaving propagation checks on the public name:
445
490
 
446
491
  ```ts
447
492
  const cert = await issueCertificate({
448
- domains: ['app.customer.com'],
493
+ domains: ["app.customer.com"],
449
494
  dnsProvider: platformValidationDns,
450
- email: 'ops@platform.example',
495
+ email: "ops@platform.example",
451
496
  mapDnsChallengeRecord: ({ domain }) =>
452
497
  `${stableDomainToken(domain)}.acme.platform.example`,
453
498
  });
@@ -473,43 +518,43 @@ this module handles the deploy-side (push values to remote env
473
518
  files, atomic swap, conditional service reload).
474
519
 
475
520
  ```ts
476
- import { createSecretBroker, inMemoryAdapter } from '@absolutejs/secrets';
477
- import { hetznerTarget } from '@absolutejs/deploy/hetzner';
521
+ import { createSecretBroker, inMemoryAdapter } from "@absolutejs/secrets";
522
+ import { hetznerTarget } from "@absolutejs/deploy/hetzner";
478
523
  import {
479
524
  syncSecretsToDeployments,
480
525
  deploymentsUsing,
481
526
  type EnvDeployment,
482
- } from '@absolutejs/deploy/env';
527
+ } from "@absolutejs/deploy/env";
483
528
 
484
529
  // One source of truth — the SecretBroker. Swap in whatever adapter
485
530
  // (env, file, vault, etc.) makes sense for your team.
486
531
  const broker = createSecretBroker({
487
532
  adapter: inMemoryAdapter({
488
533
  initial: {
489
- DATABASE_URL: 'postgres://prod-db',
490
- STRIPE_KEY: 'sk_live_old',
534
+ DATABASE_URL: "postgres://prod-db",
535
+ STRIPE_KEY: "sk_live_old",
491
536
  },
492
537
  }),
493
538
  });
494
539
 
495
540
  // Each deployed service is one EnvDeployment.
496
- const api = await hetznerTarget({ name: 'api-1', /* … */ });
497
- const worker = await hetznerTarget({ name: 'worker-1', /* … */ });
541
+ const api = await hetznerTarget({ name: "api-1" /* … */ });
542
+ const worker = await hetznerTarget({ name: "worker-1" /* … */ });
498
543
 
499
544
  const deployments: EnvDeployment[] = [
500
545
  {
501
546
  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',
547
+ remotePath: "/etc/api.env",
548
+ secretNames: ["STRIPE_KEY", "DATABASE_URL"],
549
+ extras: { NODE_ENV: "production", PORT: "3000" },
550
+ reload: "systemctl reload api",
506
551
  },
507
552
  {
508
553
  target: worker,
509
- remotePath: '/etc/worker.env',
510
- secretNames: ['DATABASE_URL'],
511
- extras: { NODE_ENV: 'production' },
512
- reload: 'systemctl restart worker',
554
+ remotePath: "/etc/worker.env",
555
+ secretNames: ["DATABASE_URL"],
556
+ extras: { NODE_ENV: "production" },
557
+ reload: "systemctl restart worker",
513
558
  },
514
559
  ];
515
560
 
@@ -517,10 +562,10 @@ const deployments: EnvDeployment[] = [
517
562
  await syncSecretsToDeployments(broker, deployments);
518
563
 
519
564
  // Rotate STRIPE_KEY everywhere it's used:
520
- await broker.rotate('STRIPE_KEY');
565
+ await broker.rotate("STRIPE_KEY");
521
566
  await syncSecretsToDeployments(
522
567
  broker,
523
- deploymentsUsing('STRIPE_KEY', deployments)
568
+ deploymentsUsing("STRIPE_KEY", deployments),
524
569
  );
525
570
  ```
526
571
 
@@ -553,33 +598,35 @@ import {
553
598
  renewCertificate,
554
599
  installCertificateOnTarget,
555
600
  importAccount,
556
- } from '@absolutejs/deploy/tls';
557
- import { readFile, writeFile } from 'node:fs/promises';
601
+ } from "@absolutejs/deploy/tls";
602
+ import { readFile, writeFile } from "node:fs/promises";
558
603
 
559
- const currentCertificatePem = await readFile('./cert.pem', 'utf8').catch(() => undefined);
604
+ const currentCertificatePem = await readFile("./cert.pem", "utf8").catch(
605
+ () => undefined,
606
+ );
560
607
  const account = await importAccount(
561
- JSON.parse(await readFile('./account.json', 'utf8'))
608
+ JSON.parse(await readFile("./account.json", "utf8")),
562
609
  );
563
610
 
564
611
  const result = await renewCertificate({
565
612
  currentCertificatePem,
566
- domains: ['api.example.com'],
613
+ domains: ["api.example.com"],
567
614
  dnsProvider: dns,
568
- email: 'ops@example.com',
615
+ email: "ops@example.com",
569
616
  account,
570
- renewWhenDaysRemaining: 30, // default
617
+ renewWhenDaysRemaining: 30, // default
571
618
  });
572
619
 
573
620
  if (result.renewed) {
574
621
  console.log(`renewed (${result.reason})`);
575
622
  await installCertificateOnTarget(target, result.certificate, {
576
- reload: 'systemctl reload nginx',
623
+ reload: "systemctl reload nginx",
577
624
  });
578
- await writeFile('./cert.pem', result.certificate.certificatePem);
579
- await writeFile('./key.pem', result.certificate.privateKeyPem);
625
+ await writeFile("./cert.pem", result.certificate.certificatePem);
626
+ await writeFile("./key.pem", result.certificate.privateKeyPem);
580
627
  } else {
581
628
  console.log(
582
- `still fresh — ${result.inspection.daysRemaining} days remaining`
629
+ `still fresh — ${result.inspection.daysRemaining} days remaining`,
583
630
  );
584
631
  }
585
632
  ```
@@ -637,17 +684,17 @@ and traffic cutover remain control-plane responsibilities.
637
684
 
638
685
  Compute (`Target` via `createCloudTarget`):
639
686
 
640
- - `@absolutejs/deploy/digitalocean` — droplets
641
- - `@absolutejs/deploy/hetzner` — Hetzner Cloud servers
642
- - `@absolutejs/deploy/linode` — Linode instances
643
- - `@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
644
691
 
645
692
  DNS (`DnsProvider`):
646
693
 
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.
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.
651
698
 
652
699
  All compute adapters share the same `createCloudTarget` machinery
653
700
  (find-or-create + wait-for-ready + wait-for-SSH + sshTarget wrap)