@stacksjs/ts-cloud 0.2.20 → 0.2.22

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.
@@ -2,6 +2,7 @@
2
2
  * Deployment Modules
3
3
  * High-level deployment functions for common AWS architectures
4
4
  */
5
+ export * from './site-target';
5
6
  export * from './static-site';
6
7
  export * from './static-site-external-dns';
7
8
  export * from './static-site-helper';
@@ -0,0 +1,41 @@
1
+ import type { CloudConfig, SiteConfig, SiteDeployTarget } from '@ts-cloud/core';
2
+ /**
3
+ * The three resolved deployment kinds for a site:
4
+ * - `'bucket'` — upload built `root` to object storage + CDN.
5
+ * - `'server-app'` — `server` + `start`: dynamic app as a systemd service
6
+ * behind the Caddy reverse proxy.
7
+ * - `'server-static'` — `server` + no `start` (has static `root`): a static
8
+ * site built and served on the box via Caddy `file_server`.
9
+ */
10
+ export type SiteDeployKind = 'bucket' | 'server-app' | 'server-static';
11
+ /**
12
+ * Resolve the explicit-or-inferred {@link SiteDeployTarget} for a site.
13
+ *
14
+ * Inference (backward compatible):
15
+ * 1. an explicit `site.deploy` always wins;
16
+ * 2. else if `start` is present → `'server'`;
17
+ * 3. else → `'bucket'`.
18
+ */
19
+ export declare function resolveSiteDeployTarget(site: SiteConfig): SiteDeployTarget;
20
+ /**
21
+ * Resolve the fine-grained {@link SiteDeployKind} for a site, combining the
22
+ * {@link resolveSiteDeployTarget} target with the presence of `start`.
23
+ *
24
+ * - `bucket` → `'bucket'`
25
+ * - `server` + `start` → `'server-app'`
26
+ * - `server` + no `start` → `'server-static'`
27
+ */
28
+ export declare function resolveSiteKind(site: SiteConfig): SiteDeployKind;
29
+ export interface DeploymentValidationResult {
30
+ errors: string[];
31
+ warnings: string[];
32
+ }
33
+ /**
34
+ * Validate the per-site deployment configuration up front, turning what used to
35
+ * be silent runtime failures (e.g. a `start` site with no compute server) into
36
+ * an explicit, actionable contract.
37
+ *
38
+ * Never throws — returns structured `{ errors, warnings }`. Callers should abort
39
+ * on any error and print warnings while continuing.
40
+ */
41
+ export declare function validateDeploymentConfig(config: CloudConfig): DeploymentValidationResult;
@@ -91,10 +91,12 @@ export interface CreateSshKeyOptions {
91
91
  publicKey: string;
92
92
  labels?: Record<string, string>;
93
93
  }
94
+ /** Minimal fetch signature the client relies on (always called with a string URL). */
95
+ export type HetznerFetch = (url: string, init?: RequestInit) => Promise<Response>;
94
96
  export interface HetznerClientOptions {
95
97
  apiToken: string;
96
98
  baseUrl?: string;
97
- fetchImpl?: typeof fetch;
99
+ fetchImpl?: HetznerFetch;
98
100
  }
99
101
  export declare class HetznerClient {
100
102
  readonly name = "hetzner";
@@ -115,6 +117,11 @@ export declare class HetznerClient {
115
117
  firewall: HetznerFirewall;
116
118
  actions: HetznerAction[];
117
119
  }>;
120
+ /**
121
+ * Replace a firewall's rule set in place. Used to keep an existing (reused)
122
+ * firewall's rules in sync with the desired config without recreating it.
123
+ */
124
+ setFirewallRules(firewallId: number, rules: HetznerFirewallRule[]): Promise<HetznerAction[]>;
118
125
  applyFirewallToResources(firewallId: number, applyTo: Array<{
119
126
  type: 'server';
120
127
  server: number;
@@ -13,5 +13,12 @@ export interface UbuntuBootstrapOptions {
13
13
  export declare function generateUbuntuAppCloudInit(options?: UbuntuBootstrapOptions): string;
14
14
  /**
15
15
  * Wrap a bash bootstrap script as Hetzner cloud-init user_data (#cloud-config).
16
+ *
17
+ * The script is written to disk via `write_files` and then executed through an
18
+ * explicit `bash` invocation in `runcmd`. cloud-init runs bare `runcmd` entries
19
+ * with `/bin/sh` (dash on Ubuntu), which chokes on bash-only syntax like
20
+ * `set -o pipefail` and aborts the whole bootstrap — so embedding the script
21
+ * inline under `runcmd:` silently breaks bun/caddy installation. Writing the
22
+ * file (shebang preserved) and running `bash <file>` guarantees a bash shell.
16
23
  */
17
24
  export declare function wrapCloudInitUserData(bootstrapScript: string): string;
@@ -7,6 +7,27 @@ export interface HetznerDriverOptions {
7
7
  sshUser?: string;
8
8
  location?: string;
9
9
  client?: HetznerClient;
10
+ /**
11
+ * After the server reports `running`, block until SSH is reachable and
12
+ * cloud-init has finished before returning from provisioning. Disable in
13
+ * tests (or fast-path provisioning) to avoid real network waits.
14
+ * @default true
15
+ */
16
+ waitForBoot?: boolean;
17
+ /**
18
+ * Tunables for the SSH-readiness / cloud-init wait loops. Overridable so
19
+ * tests can use tiny intervals.
20
+ */
21
+ bootWait?: {
22
+ /** Delay between SSH probe attempts (ms). @default 5000 */
23
+ sshIntervalMs?: number;
24
+ /** Max time to wait for SSH to accept connections (ms). @default 300000 */
25
+ sshTimeoutMs?: number;
26
+ /** Delay between `cloud-init status` polls (ms). @default 5000 */
27
+ cloudInitIntervalMs?: number;
28
+ /** Max time to wait for cloud-init to finish (ms). @default 600000 */
29
+ cloudInitTimeoutMs?: number;
30
+ };
10
31
  }
11
32
  export declare class HetznerDriver implements CloudDriver {
12
33
  readonly name: "hetzner";
@@ -16,6 +37,8 @@ export declare class HetznerDriver implements CloudDriver {
16
37
  private sshPublicKeyPath;
17
38
  private sshUser;
18
39
  private location;
40
+ private waitForBoot;
41
+ private bootWait;
19
42
  constructor(options?: HetznerDriverOptions);
20
43
  provisionComputeInfrastructure(options: ProvisionComputeOptions): Promise<ComputeStackOutputs>;
21
44
  getComputeOutputs(options: ProvisionComputeOptions): Promise<ComputeStackOutputs>;
@@ -29,6 +52,39 @@ export declare class HetznerDriver implements CloudDriver {
29
52
  * denied (publickey)" because the server has no authorized keys.
30
53
  */
31
54
  private ensureSshKey;
55
+ /** getServer that returns null instead of throwing when the server is gone. */
56
+ private tryGetServer;
57
+ /**
58
+ * Look up an existing ts-cloud server for this project/environment by labels
59
+ * (falling back to name match). Used for idempotency when local state is
60
+ * missing, so re-running deploy doesn't spin up a duplicate server.
61
+ */
62
+ private findExistingServer;
63
+ /**
64
+ * Idempotent firewall: reuse an existing firewall with the same name rather
65
+ * than creating a duplicate on every deploy. When found, its rules are
66
+ * updated to the desired set so config changes (new ports) still apply.
67
+ */
68
+ private ensureFirewall;
69
+ /**
70
+ * Collect the upstream app ports that must be reachable when no reverse proxy
71
+ * is fronting traffic (raw-port deploys). Drops 80/443 (handled separately).
72
+ */
73
+ private collectUpstreamPorts;
74
+ private sleep;
75
+ /**
76
+ * Probe SSH (a trivial `true` over the connection) with backoff until the box
77
+ * accepts connections. A freshly booted server refuses SSH for a few seconds
78
+ * while sshd starts; without this, the very next deploy command races it and
79
+ * fails with "Connection refused".
80
+ */
81
+ private waitForSshReady;
82
+ /**
83
+ * Block until cloud-init finishes (`cloud-init status --wait`). cloud-init is
84
+ * what installs the runtime + Caddy; deploying before it completes leaves the
85
+ * release pointing at a half-provisioned box (missing `bun`, no Caddy).
86
+ */
87
+ private waitForCloudInit;
32
88
  private outputsFromState;
33
89
  private sshBaseArgs;
34
90
  private scpToHost;
@@ -3,6 +3,6 @@ export { AwsDriver } from './aws/driver';
3
3
  export { HetznerDriver } from './hetzner/driver';
4
4
  export { HetznerClient, resolveHetznerApiToken } from './hetzner/client';
5
5
  export { generateUbuntuAppCloudInit, wrapCloudInitUserData } from './hetzner/cloud-init';
6
- export { buildCaddyfile } from './shared/caddyfile';
7
- export { buildAwsArtifactFetch, buildLocalArtifactFetch, buildSiteDeployScript, resolveExecStart, } from './shared/deploy-script';
6
+ export { buildCaddyfile, buildCaddyfileFromProxy, isOnDemandDomain, proxyConfigFromSites, resolveCaddyfile, staticSiteServerRoot, } from './shared/caddyfile';
7
+ export { buildAwsArtifactFetch, buildLocalArtifactFetch, buildSiteDeployScript, buildStaticSiteDeployScript, resolveExecStart, } from './shared/deploy-script';
8
8
  export { deployAllComputeSites, deploySiteRelease } from './shared/compute-deploy';
@@ -1,6 +1,46 @@
1
- import type { SiteConfig } from '@ts-cloud/core';
1
+ import type { CaddyAppConfig, CaddyProxyConfig, SiteConfig } from '@ts-cloud/core';
2
+ /** A domain is "on-demand" (needs lazy TLS) if it's a wildcard or bare catch-all. */
3
+ export declare function isOnDemandDomain(domain: string): boolean;
4
+ /**
5
+ * The on-server install path a static site's `root` is shipped to. Mirrors the
6
+ * release layout used by the systemd app deploy (`/var/www/<name>`), so a box
7
+ * can host both proxied apps and file-served static sites side by side.
8
+ */
9
+ export declare function staticSiteServerRoot(name: string): string;
10
+ /**
11
+ * Build a complete Caddyfile from a typed {@link CaddyProxyConfig}.
12
+ *
13
+ * Produces:
14
+ * - a global options block (ACME email, on-demand TLS `ask`, staging CA, extras);
15
+ * - one site block per unique domain set, each performing host-based routing to
16
+ * its upstream app(s);
17
+ * - `tls { on_demand }` inside any block whose domains are wildcards/catch-all.
18
+ *
19
+ * Returns `undefined` when there's nothing to route (no apps, no raw).
20
+ */
21
+ export declare function buildCaddyfileFromProxy(proxy: CaddyProxyConfig): string | undefined;
22
+ /**
23
+ * Derive a {@link CaddyProxyConfig} from the legacy `sites` map: every site
24
+ * that declares a `domain` + `port` becomes a Caddy app. Keeps single-app /
25
+ * sites-driven deploys working without an explicit `compute.proxy` block.
26
+ */
27
+ export declare function proxyConfigFromSites(sites: Record<string, SiteConfig>): CaddyProxyConfig & {
28
+ apps: CaddyAppConfig[];
29
+ };
30
+ /**
31
+ * Resolve the final Caddyfile for a deploy. Prefers the typed `compute.proxy`
32
+ * config (merging in `sites`-derived apps when `proxy.apps` is omitted), and
33
+ * falls back to deriving everything from `sites`.
34
+ *
35
+ * Returns `undefined` when there's nothing to route.
36
+ */
37
+ export declare function resolveCaddyfile(sites: Record<string, SiteConfig>, proxy?: CaddyProxyConfig): string | undefined;
2
38
  /**
3
39
  * Build a Caddyfile from site configs. Sites sharing a domain are grouped;
4
40
  * explicit paths are ordered before catch-all routes.
41
+ *
42
+ * @deprecated Prefer {@link resolveCaddyfile} / {@link buildCaddyfileFromProxy},
43
+ * which support multi-app host routing and on-demand TLS. Retained for
44
+ * backward compatibility.
5
45
  */
6
46
  export declare function buildCaddyfile(sites: Record<string, SiteConfig>): string | undefined;
@@ -20,6 +20,8 @@ export interface DeployAllSitesOptions {
20
20
  logger?: ComputeDeployLogger;
21
21
  }
22
22
  /**
23
- * Deploy every site that declares a `start` command.
23
+ * Deploy every site that targets the compute server — both dynamic apps
24
+ * (`server` + `start`, run as systemd services) and static sites (`server`
25
+ * without `start`, served by Caddy `file_server`). Bucket sites are skipped.
24
26
  */
25
27
  export declare function deployAllComputeSites(options: DeployAllSitesOptions): Promise<boolean>;
@@ -15,10 +15,37 @@ export interface BuildSiteDeployScriptOptions {
15
15
  execStart: string;
16
16
  envEntries: Record<string, string>;
17
17
  port?: number;
18
+ /**
19
+ * Commands run inside `appDir` after extraction + `.env` write, before the
20
+ * systemd unit is (re)written and started. Typically dependency install
21
+ * and/or build steps (e.g. `bun install --frozen-lockfile`, `bun run build`)
22
+ * so the release tarball can omit `node_modules`.
23
+ */
24
+ preStartCommands?: string[];
18
25
  }
19
26
  /**
20
27
  * Build the remote shell commands that install/refresh a site on a compute target.
21
28
  */
22
29
  export declare function buildSiteDeployScript(options: BuildSiteDeployScriptOptions): string[];
30
+ export interface BuildStaticSiteDeployScriptOptions {
31
+ siteName: string;
32
+ /** How the remote host obtains the release tarball */
33
+ artifactFetch: string[];
34
+ /** Target directory served by Caddy `file_server`. Default `/var/www/<site>`. */
35
+ appDir?: string;
36
+ /**
37
+ * Commands run inside `appDir` after extraction — e.g. build the docs/blog on
38
+ * the box itself (`bun install`, `bun run docs:build`) when the tarball ships
39
+ * source rather than a pre-built site.
40
+ */
41
+ preStartCommands?: string[];
42
+ }
43
+ /**
44
+ * Build the remote shell commands that install/refresh a STATIC site on a
45
+ * compute target. Unlike {@link buildSiteDeployScript}, there is no systemd
46
+ * service — the extracted files are served directly by Caddy `file_server`
47
+ * (Caddy is reloaded separately when the Caddyfile changes).
48
+ */
49
+ export declare function buildStaticSiteDeployScript(options: BuildStaticSiteDeployScriptOptions): string[];
23
50
  export declare function buildAwsArtifactFetch(bucket: string, key: string, region: string, siteName: string): string[];
24
51
  export declare function buildLocalArtifactFetch(localPath: string, siteName: string): string[];
package/dist/index.d.ts CHANGED
@@ -7,9 +7,9 @@ export type { AWSRequestOptions, AWSClientConfig, AWSError, AWSCredentials as AW
7
7
  export { createObjectStorageClient, providerEndpoint, resolveObjectStorage, } from './object-storage';
8
8
  export type { ObjectStorageConfig, ObjectStorageCredentials, ObjectStorageProvider, ResolvedObjectStorage, } from './object-storage';
9
9
  export * from './ssl';
10
- export { deployStaticSite, deployStaticSiteFull, uploadStaticFiles, invalidateCache, deleteStaticSite, generateStaticSiteTemplate, deployStaticSiteWithExternalDns, deployStaticSiteWithExternalDnsFull, generateExternalDnsStaticSiteTemplate, deploySite, } from './deploy';
11
- export type { StaticSiteConfig, DeployResult, UploadOptions, ExternalDnsStaticSiteConfig, ExternalDnsDeployResult, DeploySiteConfig, DeploySiteResult, StaticSiteDnsProvider, } from './deploy';
12
- export { createCloudDriver, CloudDriverFactory, cloudDrivers, AwsDriver, HetznerDriver, HetznerClient, resolveHetznerApiToken, generateUbuntuAppCloudInit, wrapCloudInitUserData, buildCaddyfile, buildSiteDeployScript, resolveExecStart, deployAllComputeSites, deploySiteRelease, } from './drivers';
10
+ export { deployStaticSite, deployStaticSiteFull, uploadStaticFiles, invalidateCache, deleteStaticSite, generateStaticSiteTemplate, deployStaticSiteWithExternalDns, deployStaticSiteWithExternalDnsFull, generateExternalDnsStaticSiteTemplate, deploySite, resolveSiteDeployTarget, resolveSiteKind, validateDeploymentConfig, } from './deploy';
11
+ export type { StaticSiteConfig, DeployResult, UploadOptions, ExternalDnsStaticSiteConfig, ExternalDnsDeployResult, DeploySiteConfig, DeploySiteResult, StaticSiteDnsProvider, SiteDeployKind, DeploymentValidationResult, } from './deploy';
12
+ export { createCloudDriver, CloudDriverFactory, cloudDrivers, AwsDriver, HetznerDriver, HetznerClient, resolveHetznerApiToken, generateUbuntuAppCloudInit, wrapCloudInitUserData, buildCaddyfile, buildCaddyfileFromProxy, isOnDemandDomain, proxyConfigFromSites, resolveCaddyfile, staticSiteServerRoot, buildSiteDeployScript, buildStaticSiteDeployScript, resolveExecStart, deployAllComputeSites, deploySiteRelease, } from './drivers';
13
13
  export type { CreateCloudDriverOptions } from './drivers/factory';
14
14
  export { createDnsProvider, detectDnsProvider, DnsProviderFactory, dnsProviders, PorkbunProvider, GoDaddyProvider, Route53Provider, UnifiedDnsValidator, createPorkbunValidator, createGoDaddyValidator, createRoute53Validator, } from './dns';
15
15
  export type { DnsProvider, DnsProviderConfig, DnsRecord, DnsRecordType, DnsRecordResult, CreateRecordResult, DeleteRecordResult, ListRecordsResult, } from './dns';