@stacksjs/ts-cloud 0.5.26 → 0.5.28

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/dist/bin/cli.js +1223 -7366
  2. package/dist/deploy/dashboard-data-server.d.ts +27 -5
  3. package/dist/deploy/dashboard-database.d.ts +38 -0
  4. package/dist/deploy/dashboard-database.test.d.ts +1 -0
  5. package/dist/deploy/dashboard-operations.d.ts +48 -0
  6. package/dist/deploy/dashboard-operations.test.d.ts +1 -0
  7. package/dist/deploy/index.d.ts +1 -0
  8. package/dist/deploy/local-dashboard-server.d.ts +31 -0
  9. package/dist/deploy/management-dashboard.d.ts +21 -1
  10. package/dist/deploy/site-config-editor.d.ts +37 -0
  11. package/dist/deploy/site-target.d.ts +3 -1
  12. package/dist/deploy/ssh-config-editor.d.ts +22 -0
  13. package/dist/deploy/ssh-config-editor.test.d.ts +1 -0
  14. package/dist/drivers/factory.d.ts +4 -0
  15. package/dist/drivers/index.d.ts +1 -0
  16. package/dist/drivers/local-box/driver.d.ts +20 -0
  17. package/dist/drivers/shared/compute-deploy.d.ts +2 -0
  18. package/dist/drivers/shared/deploy-script.d.ts +28 -12
  19. package/dist/drivers/shared/rpx-gateway.d.ts +99 -2
  20. package/dist/index.d.ts +2 -0
  21. package/dist/index.js +3381 -210
  22. package/dist/ui/index.html +83 -1
  23. package/dist/ui/server/actions.html +1193 -0
  24. package/dist/ui/server/activity.html +1132 -0
  25. package/dist/ui/server/backups.html +57 -1
  26. package/dist/ui/server/database.html +1208 -0
  27. package/dist/ui/server/deployments.html +79 -1
  28. package/dist/ui/server/diagnostics.html +1132 -0
  29. package/dist/ui/server/logs.html +1165 -0
  30. package/dist/ui/server/metrics.html +1 -1
  31. package/dist/ui/server/security.html +1132 -0
  32. package/dist/ui/server/services.html +57 -1
  33. package/dist/ui/server/sites.html +141 -1
  34. package/dist/ui/server/ssh-keys.html +58 -1
  35. package/dist/ui/server/workers.html +57 -1
  36. package/dist/ui/serverless/assets.html +1 -1
  37. package/dist/ui/serverless/cost.html +1 -1
  38. package/dist/ui/serverless/data.html +1 -1
  39. package/dist/ui/serverless/deployments.html +1 -1
  40. package/dist/ui/serverless/firewall.html +1 -1
  41. package/dist/ui/serverless/functions.html +1 -1
  42. package/dist/ui/serverless/logs.html +1165 -0
  43. package/dist/ui/serverless/metrics.html +1 -1
  44. package/dist/ui/serverless/queues.html +1 -1
  45. package/dist/ui/serverless/scheduler.html +1 -1
  46. package/dist/ui/serverless/secrets.html +1 -1
  47. package/dist/ui/serverless.html +1 -1
  48. package/dist/ui-src/package.json +5 -0
  49. package/dist/ui-src/pages/index.stx +308 -0
  50. package/dist/ui-src/pages/partials/head.stx +294 -0
  51. package/dist/ui-src/pages/partials/nav.stx +54 -0
  52. package/dist/ui-src/pages/partials/op-confirm.stx +7 -0
  53. package/dist/ui-src/pages/server/actions.stx +60 -0
  54. package/dist/ui-src/pages/server/activity.stx +55 -0
  55. package/dist/ui-src/pages/server/backups.stx +91 -0
  56. package/dist/ui-src/pages/server/database.stx +119 -0
  57. package/dist/ui-src/pages/server/deployments.stx +106 -0
  58. package/dist/ui-src/pages/server/diagnostics.stx +88 -0
  59. package/dist/ui-src/pages/server/logs.stx +84 -0
  60. package/dist/ui-src/pages/server/metrics.stx +183 -0
  61. package/dist/ui-src/pages/server/security.stx +127 -0
  62. package/dist/ui-src/pages/server/services.stx +82 -0
  63. package/dist/ui-src/pages/server/sites.stx +143 -0
  64. package/dist/ui-src/pages/server/ssh-keys.stx +93 -0
  65. package/dist/ui-src/pages/server/workers.stx +99 -0
  66. package/dist/ui-src/pages/serverless/assets.stx +66 -0
  67. package/dist/ui-src/pages/serverless/cost.stx +149 -0
  68. package/dist/ui-src/pages/serverless/data.stx +76 -0
  69. package/dist/ui-src/pages/serverless/deployments.stx +56 -0
  70. package/dist/ui-src/pages/serverless/firewall.stx +58 -0
  71. package/dist/ui-src/pages/serverless/functions.stx +118 -0
  72. package/dist/ui-src/pages/serverless/logs.stx +84 -0
  73. package/dist/ui-src/pages/serverless/metrics.stx +76 -0
  74. package/dist/ui-src/pages/serverless/queues.stx +81 -0
  75. package/dist/ui-src/pages/serverless/scheduler.stx +60 -0
  76. package/dist/ui-src/pages/serverless/secrets.stx +55 -0
  77. package/dist/ui-src/pages/serverless.stx +222 -0
  78. package/dist/ui-src/src/charts.ts +17333 -0
  79. package/package.json +3 -3
@@ -2,13 +2,35 @@
2
2
  * Resolve REAL server-dashboard data for a provisioned compute box. Runs a small
3
3
  * metrics script over the active driver (SSM/SSH), parses the KEY=VALUE output,
4
4
  * and derives sites/SSH/workers from the cloud config. Everything is best-effort:
5
- * if no box is reachable it returns null and the dashboard renders sample data.
5
+ * if no box is reachable it returns config-derived data marked unavailable.
6
6
  */
7
7
  import type { CloudConfig, EnvironmentType } from '@ts-cloud/core';
8
+ interface ProbedService {
9
+ name: string;
10
+ status: string;
11
+ memBytes?: number;
12
+ enabled?: string;
13
+ since?: string;
14
+ }
8
15
  export declare function parseBlock(output: string): Record<string, string> & {
9
- services: Array<{
10
- name: string;
11
- status: string;
12
- }>;
16
+ services: ProbedService[];
13
17
  };
18
+ interface ServerLogSource {
19
+ /** journalctl `-u` pattern (a unit name or a glob like `acme-web-queue-*`). */
20
+ pattern: string;
21
+ /** Clean source label shown + filtered on in the UI. */
22
+ label: string;
23
+ }
24
+ /**
25
+ * Every log source on the box: the web server, each managed service, and per
26
+ * server-app site the app service PLUS its queue workers and daemons (collected
27
+ * via journalctl unit globs so all worker instances are covered, not just the
28
+ * main service). Deduped by label.
29
+ */
30
+ export declare function serverLogSources(config: CloudConfig): ServerLogSource[];
31
+ export declare function parseServerLogs(output: string): Array<Record<string, any>>;
32
+ export declare function parseServerSecurity(output: string): Record<string, any>;
33
+ export declare function parseDeployHistory(output: string, sites?: Record<string, any>): Array<Record<string, any>>;
34
+ export declare function resolveConfigOnlyServerDashboardData(config: CloudConfig, environment: EnvironmentType): Record<string, any>;
14
35
  export declare function resolveServerDashboardData(config: CloudConfig, environment: EnvironmentType): Promise<Record<string, any> | null>;
36
+ export {};
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Database & user management for the cockpit: list databases/users and create a
3
+ * database or an app user over the active driver. MySQL/MariaDB run as root via
4
+ * the pantry UNIX socket; Postgres over local TCP — mirroring the provisioning
5
+ * path in {@link import('../drivers/shared/db-provision')}.
6
+ */
7
+ import type { CloudConfig, EnvironmentType } from '@ts-cloud/core';
8
+ export type DbEngine = 'mysql' | 'mariadb' | 'postgres';
9
+ /** Valid SQL identifier for a database/user name (kept strict for safety). */
10
+ export declare function isValidDbIdentifier(value: string): boolean;
11
+ export declare function resolveDbEngine(config: CloudConfig): DbEngine;
12
+ export declare function buildListScript(engine: DbEngine): string[];
13
+ export declare function buildCreateDatabaseScript(engine: DbEngine, name: string): string[];
14
+ export interface CreateUserInput {
15
+ username: string;
16
+ password: string;
17
+ database?: string;
18
+ access?: 'all' | 'readonly';
19
+ }
20
+ export declare function buildCreateUserScript(engine: DbEngine, input: CreateUserInput): string[];
21
+ export declare function parseDbList(output: string): {
22
+ databases: string[];
23
+ users: string[];
24
+ };
25
+ interface DbRunResult {
26
+ ok: boolean;
27
+ stdout?: string;
28
+ stderr?: string;
29
+ error?: string;
30
+ }
31
+ export declare function listDatabases(config: CloudConfig, environment: EnvironmentType): Promise<DbRunResult & {
32
+ engine: DbEngine;
33
+ databases: string[];
34
+ users: string[];
35
+ }>;
36
+ export declare function createDatabase(config: CloudConfig, environment: EnvironmentType, name: string): Promise<DbRunResult>;
37
+ export declare function createDatabaseUser(config: CloudConfig, environment: EnvironmentType, input: CreateUserInput): Promise<DbRunResult>;
38
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Generalized server operations for the management cockpit: service lifecycle
3
+ * (restart/reload/start/stop/enable/disable), deployment rollback, scheduled
4
+ * backup run + database restore, worker restart, and scheduler run-now.
5
+ *
6
+ * The catalog is derived purely from the cloud config + live server data (so it
7
+ * is trivially unit-testable); the runner dispatches each operation over the
8
+ * active driver (SSH/SSM), reusing the shared compute-ops builders.
9
+ */
10
+ import type { CloudConfig, EnvironmentType } from '@ts-cloud/core';
11
+ export type OperationGroup = 'service' | 'deploy' | 'backup' | 'worker' | 'scheduler';
12
+ export interface DashboardOperation {
13
+ id: string;
14
+ label: string;
15
+ group: OperationGroup;
16
+ target: string;
17
+ mutates: boolean;
18
+ /** Token the operator must type to run a mutating operation. */
19
+ confirm: string;
20
+ /** Destructive operations rendered with a danger affordance. */
21
+ danger?: boolean;
22
+ }
23
+ export interface DashboardOperationResult {
24
+ operation: string;
25
+ command?: string;
26
+ ok: boolean;
27
+ stdout?: string;
28
+ stderr?: string;
29
+ error?: string;
30
+ }
31
+ /** Guard systemd unit names so an operation id can never inject shell. */
32
+ export declare function isSafeSystemdUnit(value: string): boolean;
33
+ /**
34
+ * Build the full operation catalog from config + live server data. Pure: every
35
+ * operation that appears here is runnable by {@link runDashboardOperation}.
36
+ */
37
+ export declare function buildDashboardOperations(config: CloudConfig, data: Record<string, any>): DashboardOperation[];
38
+ export declare function resolveDashboardOperation(id: string, config: CloudConfig, data: Record<string, any>): DashboardOperation | undefined;
39
+ /**
40
+ * Run an operation over the active driver. Rollback + DB restore delegate to the
41
+ * shared compute-ops; the rest run a single bounded remote script on the app
42
+ * target. Returns a structured result (never throws on a remote failure).
43
+ */
44
+ export interface RunOperationOptions {
45
+ /** For rollback: the release id to roll back to (omit for the previous release). */
46
+ to?: string;
47
+ }
48
+ export declare function runDashboardOperation(config: CloudConfig, environment: EnvironmentType, operation: DashboardOperation, options?: RunOperationOptions): Promise<DashboardOperationResult>;
@@ -0,0 +1 @@
1
+ export {};
@@ -9,3 +9,4 @@ export * from './static-site-helper';
9
9
  export { buildFunctionEnv, type CodeSource, deployServerlessApp, type DeployServerlessOptions, infraEnvFromOutputs, redeployServerlessApp, type ResolvedContext, rollbackServerlessApp, runRemoteCommand, setMaintenance, } from './serverless-app';
10
10
  export { buildAndPushServerlessImage, type BuildImageOptions, type BuiltImage, } from './serverless-image';
11
11
  export { type DashboardData, resolveDashboardData, } from './dashboard-data';
12
+ export { dashboardActions, resolveDashboardAction, sanitizeCloudConfig, startLocalDashboardServer, type DashboardAction, type LocalDashboardServer, type LocalDashboardServerOptions, } from './local-dashboard-server';
@@ -0,0 +1,31 @@
1
+ import type { CloudConfig, EnvironmentType } from '@ts-cloud/core';
2
+ export interface LocalDashboardServerOptions {
3
+ host?: string;
4
+ port?: number;
5
+ cwd?: string;
6
+ environment?: EnvironmentType;
7
+ cliEntry?: string;
8
+ verbose?: boolean;
9
+ /**
10
+ * Box mode: the dashboard runs ON the provisioned server. Data resolution and
11
+ * operations execute against localhost (the {@link LocalBoxDriver}) instead of
12
+ * reaching out over SSH/SSM.
13
+ */
14
+ box?: boolean;
15
+ }
16
+ export interface LocalDashboardServer {
17
+ url: string;
18
+ server: ReturnType<typeof Bun.serve>;
19
+ }
20
+ export interface DashboardAction {
21
+ id: string;
22
+ label: string;
23
+ description: string;
24
+ command: string[];
25
+ mutates: boolean;
26
+ confirm?: string;
27
+ }
28
+ export declare function sanitizeCloudConfig(config: CloudConfig): Record<string, any>;
29
+ export declare function dashboardActions(environment: EnvironmentType): DashboardAction[];
30
+ export declare function resolveDashboardAction(id: string, environment: EnvironmentType): DashboardAction | undefined;
31
+ export declare function startLocalDashboardServer(options?: LocalDashboardServerOptions): Promise<LocalDashboardServer>;
@@ -15,7 +15,9 @@
15
15
  * - `TS_CLOUD_UI_REALM` browser auth realm
16
16
  * - `TS_CLOUD_UI_DISABLE` set truthy to skip auto-deploy
17
17
  */
18
- import type { CloudConfig } from '@stacksjs/ts-cloud';
18
+ import type { CloudConfig } from '@ts-cloud/core';
19
+ /** Site key under which the management dashboard is auto-injected. */
20
+ export declare const MANAGEMENT_DASHBOARD_SITE = "dashboard";
19
21
  export interface EnsureDashboardLogger {
20
22
  info: (msg: string) => void;
21
23
  warn: (msg: string) => void;
@@ -37,3 +39,21 @@ export declare function ensureManagementDashboard(config: CloudConfig, options?:
37
39
  cwd?: string;
38
40
  logger?: EnsureDashboardLogger;
39
41
  }): CloudConfig;
42
+ export interface BuildDashboardArtifactOptions {
43
+ cwd?: string;
44
+ slug: string;
45
+ sha: string;
46
+ logger?: EnsureDashboardLogger;
47
+ }
48
+ /**
49
+ * Build the management-dashboard release tarball from its injected site config:
50
+ * run the UI `build` (when not already built), then package `site.root`. Returns
51
+ * the tarball path, or null when the UI cannot be built/found.
52
+ *
53
+ * Best-effort: the dashboard is auxiliary, so any failure logs and returns null
54
+ * rather than throwing — the surrounding app deploy must never be blocked by it.
55
+ */
56
+ export declare function buildManagementDashboardArtifact(site: {
57
+ root?: string;
58
+ build?: string | false;
59
+ } | undefined, options: BuildDashboardArtifactOptions): string | null;
@@ -10,6 +10,43 @@ export interface AddSiteConfigInput {
10
10
  port?: number;
11
11
  type?: string;
12
12
  pathRewriteStyle?: 'directory' | 'flat';
13
+ /** Per-site environment variables. */
14
+ env?: Record<string, string>;
15
+ /** TLS: `false` to disable, or `{ provider }` (e.g. letsencrypt). */
16
+ ssl?: boolean | {
17
+ provider?: string;
18
+ };
13
19
  }
14
20
  export declare function addSiteToCloudConfig(input: AddSiteConfigInput): string;
21
+ export interface RemoveSiteInput {
22
+ configText: string;
23
+ name: string;
24
+ }
25
+ /** Remove a site (its whole `name: { ... }` entry) from the sites block. */
26
+ export declare function removeSiteFromCloudConfig(input: RemoveSiteInput): string;
27
+ export interface UpdateSiteInput extends Omit<AddSiteConfigInput, 'configText'> {
28
+ configText: string;
29
+ }
30
+ /**
31
+ * Replace a site's definition with a regenerated one carrying the merged fields.
32
+ * Implemented as remove + add so it reuses the validated insert path (the site
33
+ * moves to the end of the sites block, which keeps the config valid).
34
+ */
35
+ export declare function updateSiteInCloudConfig(input: UpdateSiteInput): string;
36
+ /**
37
+ * Set (or insert) a single property on a site, preserving every other field —
38
+ * the safe path for editing `ssl`/`env` on a site that may also carry queues,
39
+ * auth, etc. `valueText` is raw TS (use {@link renderSslValue}/{@link renderEnvValue}).
40
+ */
41
+ export declare function setSitePropertyInCloudConfig(input: {
42
+ configText: string;
43
+ siteName: string;
44
+ key: string;
45
+ valueText: string;
46
+ }): string;
47
+ export declare function renderSslValue(ssl: boolean | {
48
+ provider?: string;
49
+ }): string;
50
+ export declare function renderStringValue(value: string): string;
51
+ export declare function renderEnvValue(env: Record<string, string>): string;
15
52
  export declare function renderSiteSnippet(input: Omit<AddSiteConfigInput, 'configText'>): string;
@@ -6,8 +6,10 @@ import type { CloudConfig, SiteConfig, SiteDeployTarget } from '@ts-cloud/core';
6
6
  * - `'server-static'` — `server` + no `start` (has static `root`): a static
7
7
  * site built and shipped to `/var/www/<site>` on the box
8
8
  * (served by the operator's own proxy, e.g. rpx + tlsx).
9
+ * - `'redirect'` — gateway-only: `redirect` is set. Nothing is shipped;
10
+ * the gateway answers `domain` with an HTTP redirect.
9
11
  */
10
- export type SiteDeployKind = 'bucket' | 'server-app' | 'server-static' | 'server-php';
12
+ export type SiteDeployKind = 'bucket' | 'server-app' | 'server-static' | 'server-php' | 'redirect';
11
13
  /**
12
14
  * A PHP/Laravel site: deployed to the compute box via git clone into atomic
13
15
  * release directories and served by nginx + php-fpm. Identified by a PHP
@@ -0,0 +1,22 @@
1
+ import type { SshKeyConfig } from '@ts-cloud/core';
2
+ export interface AddSshKeyInput {
3
+ configText: string;
4
+ name: string;
5
+ publicKey: string;
6
+ existingKeys?: SshKeyConfig[];
7
+ }
8
+ export interface RemoveSshKeyInput {
9
+ configText: string;
10
+ name: string;
11
+ existingKeys?: SshKeyConfig[];
12
+ }
13
+ export interface ManagedSshKey {
14
+ name: string;
15
+ publicKey: string;
16
+ type: string;
17
+ fingerprint: string;
18
+ added: string;
19
+ }
20
+ export declare function addSshKeyToCloudConfig(input: AddSshKeyInput): string;
21
+ export declare function removeSshKeyFromCloudConfig(input: RemoveSshKeyInput): string;
22
+ export declare function describeSshKeys(keys?: SshKeyConfig[]): ManagedSshKey[];
@@ -0,0 +1 @@
1
+ export {};
@@ -5,6 +5,10 @@ export interface CreateCloudDriverOptions {
5
5
  }
6
6
  /**
7
7
  * Create a cloud infrastructure driver from configuration.
8
+ *
9
+ * In box mode (`TS_CLOUD_DASHBOARD_BOX`), every driver resolves to the
10
+ * {@link LocalBoxDriver} so the on-box management dashboard runs its metrics
11
+ * scripts and operations against localhost instead of reaching out over SSH/SSM.
8
12
  */
9
13
  export declare function createCloudDriver(options: CreateCloudDriverOptions): CloudDriver;
10
14
  /**
@@ -1,6 +1,7 @@
1
1
  export * from './factory';
2
2
  export { AwsDriver } from './aws/driver';
3
3
  export { HetznerDriver } from './hetzner/driver';
4
+ export { isBoxMode, LocalBoxDriver } from './local-box/driver';
4
5
  export { HetznerClient, resolveHetznerApiToken } from './hetzner/client';
5
6
  export { generateUbuntuAppCloudInit, wrapCloudInitUserData } from './hetzner/cloud-init';
6
7
  export { buildAwsArtifactFetch, buildLocalArtifactFetch, buildSiteDeployScript, buildStaticSiteDeployScript, resolveExecStart, } from './shared/deploy-script';
@@ -0,0 +1,20 @@
1
+ import type { CloudDriver, ComputeStackOutputs, ComputeTarget, FindComputeTargetsOptions, RemoteDeployResult, RunRemoteDeployOptions, UploadReleaseOptions, UploadReleaseResult } from '@ts-cloud/core';
2
+ /**
3
+ * A {@link CloudDriver} that runs every "remote" command on the LOCAL machine.
4
+ *
5
+ * Used when the management dashboard runs ON a provisioned server (box mode):
6
+ * data resolution and operations execute against localhost via `bash` instead of
7
+ * SSH/SSM, so the existing dashboard code path — metrics scripts, service ops,
8
+ * rollbacks — works unchanged on the box itself. The single synthetic target is
9
+ * the box; provisioning/upload methods are inert (the box is already up).
10
+ */
11
+ export declare class LocalBoxDriver implements CloudDriver {
12
+ readonly name: "hetzner";
13
+ readonly usesCloudFormation = false;
14
+ getComputeOutputs(): Promise<ComputeStackOutputs>;
15
+ uploadRelease(options: UploadReleaseOptions): Promise<UploadReleaseResult>;
16
+ findComputeTargets(_options: FindComputeTargetsOptions): Promise<ComputeTarget[]>;
17
+ runRemoteDeploy(options: RunRemoteDeployOptions): Promise<RemoteDeployResult>;
18
+ }
19
+ /** Truthy-env check shared by the box-mode gate. */
20
+ export declare function isBoxMode(): boolean;
@@ -18,6 +18,8 @@ export interface DeployAllSitesOptions {
18
18
  runtime: 'bun' | 'node' | 'deno' | 'php';
19
19
  tarballForSite: (siteName: string) => string;
20
20
  logger?: ComputeDeployLogger;
21
+ /** Project root used to resolve/build the management dashboard UI. Defaults to `process.cwd()`. */
22
+ cwd?: string;
21
23
  }
22
24
  /**
23
25
  * Deploy every site that targets the compute server — both dynamic apps
@@ -8,40 +8,56 @@ export interface BuildSiteDeployScriptOptions {
8
8
  slug: string;
9
9
  /** How the remote host obtains the release tarball */
10
10
  artifactFetch: string[];
11
+ /** Site base dir holding `releases/`, `shared/`, `current`. Default `/var/www/<site>`. */
11
12
  appDir?: string;
13
+ /** Unique id for this release dir (typically the commit sha). */
14
+ releaseId: string;
12
15
  execStart: string;
13
16
  envEntries: Record<string, string>;
14
17
  port?: number;
18
+ /** Past releases to keep for rollback. @default {@link DEFAULT_KEEP_RELEASES} */
19
+ keepReleases?: number;
15
20
  /**
16
- * Commands run inside `appDir` after extraction + `.env` write, before the
17
- * systemd unit is (re)written and started. Typically dependency install
18
- * and/or build steps (e.g. `bun install --frozen-lockfile`, `bun run build`)
19
- * so the release tarball can omit `node_modules`.
21
+ * Commands run inside the new release dir after extraction + `.env` link,
22
+ * before the `current` symlink is repointed and the service restarted.
23
+ * Typically dependency install and/or build steps (e.g.
24
+ * `bun install --frozen-lockfile`, `bun run build`) so the tarball can omit
25
+ * `node_modules`.
20
26
  */
21
27
  preStartCommands?: string[];
22
28
  }
23
29
  /**
24
- * Build the remote shell commands that install/refresh a site on a compute target.
30
+ * Build the remote shell commands that install/refresh a server-app site on a
31
+ * compute target with a **zero-downtime atomic release** (Envoyer-style): unpack
32
+ * into `releases/<id>`, link the shared `.env`, build, then atomically repoint
33
+ * `current` and restart the systemd service (which runs from `current`). Old
34
+ * releases are pruned for rollback.
25
35
  */
26
36
  export declare function buildSiteDeployScript(options: BuildSiteDeployScriptOptions): string[];
27
37
  export interface BuildStaticSiteDeployScriptOptions {
28
38
  siteName: string;
29
39
  /** How the remote host obtains the release tarball */
30
40
  artifactFetch: string[];
31
- /** Target directory the static site is shipped to. Default `/var/www/<site>`. */
41
+ /** Site base dir holding `releases/`, `current`. Default `/var/www/<site>`. */
32
42
  appDir?: string;
43
+ /** Unique id for this release dir (typically the commit sha). */
44
+ releaseId: string;
45
+ /** Past releases to keep for rollback. @default {@link DEFAULT_KEEP_RELEASES} */
46
+ keepReleases?: number;
33
47
  /**
34
- * Commands run inside `appDir` after extraction — e.g. build the docs/blog on
35
- * the box itself (`bun install`, `bun run docs:build`) when the tarball ships
36
- * source rather than a pre-built site.
48
+ * Commands run inside the new release dir after extraction — e.g. build the
49
+ * docs/blog on the box itself (`bun install`, `bun run docs:build`) when the
50
+ * tarball ships source rather than a pre-built site.
37
51
  */
38
52
  preStartCommands?: string[];
39
53
  }
40
54
  /**
41
55
  * Build the remote shell commands that install/refresh a STATIC site on a
42
- * compute target. Unlike {@link buildSiteDeployScript}, there is no systemd
43
- * service the extracted files are shipped to `/var/www/<site>` and served by
44
- * the operator's own proxy (e.g. rpx + tlsx), which ts-cloud does not manage.
56
+ * compute target with a **zero-downtime atomic release** (Envoyer-style). Unlike
57
+ * {@link buildSiteDeployScript}, there is no systemd service: the artifact is
58
+ * unpacked into `releases/<id>` and `current` is repointed atomically, so the
59
+ * docroot is never empty mid-deploy. The gateway serves `<base>/current` (rpx +
60
+ * tlsx), which ts-cloud points at the symlink. Old releases are pruned.
45
61
  */
46
62
  export declare function buildStaticSiteDeployScript(options: BuildStaticSiteDeployScriptOptions): string[];
47
63
  export declare function buildAwsArtifactFetch(bucket: string, key: string, region: string, siteName: string): string[];
@@ -17,9 +17,23 @@
17
17
  * It replaces the old Caddyfile generation — pantry/stacks use rpx (their own
18
18
  * tooling), so the gateway is rpx, not Caddy.
19
19
  */
20
- import type { ComputeProxyConfig, SiteConfig } from '@ts-cloud/core';
20
+ import type { ComputeProxyConfig, SiteConfig, SiteRedirectConfig } from '@ts-cloud/core';
21
21
  /** Default directory on the box that holds real per-domain TLS certs. */
22
22
  export declare const DEFAULT_RPX_CERTS_DIR = "/etc/rpx/certs";
23
+ /** Default webroot the gateway serves ACME http-01 challenges from on `:80`. */
24
+ export declare const DEFAULT_ACME_WEBROOT = "/var/www/acme-challenge";
25
+ /** A normalized redirect target on an {@link RpxRoute} (see rpx's `redirect`). */
26
+ export interface RpxRedirect {
27
+ to: string;
28
+ status?: 301 | 302 | 307 | 308;
29
+ preservePath?: boolean;
30
+ }
31
+ /**
32
+ * Normalize a site's `redirect` (string shorthand or object) into the minimal
33
+ * {@link RpxRedirect} the gateway config carries. Optional fields are omitted
34
+ * when unset so rpx applies its own defaults (status `301`, path-preserving).
35
+ */
36
+ export declare function normalizeSiteRedirect(input: string | SiteRedirectConfig): RpxRedirect;
23
37
  /** A single rpx proxy route, mapped from one site. */
24
38
  export interface RpxRoute {
25
39
  /** Public host this route is served under (the site's `domain`). */
@@ -30,13 +44,36 @@ export interface RpxRoute {
30
44
  from?: string;
31
45
  /** Absolute directory served for a `server-static` route (`/var/www/<name>`). */
32
46
  static?: string;
47
+ /**
48
+ * Redirect target for a `redirect` site — the gateway answers `to` (the host)
49
+ * with an HTTP redirect here instead of proxying/serving. The request path +
50
+ * query are appended unless `preservePath` is `false`.
51
+ */
52
+ redirect?: RpxRedirect;
33
53
  /** Strip `.html` and resolve clean URLs (set for static sites). */
34
54
  cleanUrls?: boolean;
35
55
  /** SPA fallback for static sites. */
36
56
  spa?: boolean;
57
+ /**
58
+ * HTTP Basic auth gate for this route (from the site's `auth`). rpx challenges
59
+ * every request to the route until valid credentials are supplied — this is
60
+ * how the management dashboard (and other protected sites) stay private behind
61
+ * rpx, the same way the nginx driver applies htpasswd.
62
+ */
63
+ auth?: {
64
+ username: string;
65
+ password: string;
66
+ realm?: string;
67
+ };
37
68
  /** Stable id used when rpx registers the route. Derived from `to`+`path`. */
38
69
  id: string;
39
70
  }
71
+ /**
72
+ * Resolve a site's `auth` into the rpx route auth shape, or `undefined` when the
73
+ * site is public. Mirrors the management-dashboard preset: auth applies only when
74
+ * enabled (default) AND a password is present — no password is ever invented.
75
+ */
76
+ export declare function resolveRouteAuth(site: SiteConfig): RpxRoute['auth'];
40
77
  /** The rpx daemon/proxy config produced from a sites model. */
41
78
  export interface RpxGatewayConfig {
42
79
  /** Multi-proxy route list (host + path keyed). */
@@ -58,6 +95,13 @@ export interface RpxGatewayConfig {
58
95
  email?: string;
59
96
  certsDir: string;
60
97
  };
98
+ /**
99
+ * Directory the gateway serves ACME http-01 challenge tokens from on `:80`
100
+ * before redirecting to HTTPS. Set when ts-cloud manages certs so the renewal
101
+ * cron (`tlsx acme:renew --webroot`) can issue/renew without taking the gateway
102
+ * down to free `:80`. Omitted ⇒ the `:80` server only redirects.
103
+ */
104
+ acmeChallengeWebroot?: string;
61
105
  /** Always `true` — the gateway terminates TLS on the box. */
62
106
  https: true;
63
107
  /** Never touch `/etc/hosts` on a real server with real DNS. */
@@ -104,21 +148,74 @@ export declare function buildRpxConfig(sites: Record<string, SiteConfig | undefi
104
148
  /**
105
149
  * Render the rpx gateway config as a self-contained launcher TS module. The
106
150
  * systemd unit runs `bun <file>`, which imports `startProxies` from the
107
- * globally-installed `@stacksjs/rpx` and starts the gateway with the generated
151
+ * managed `/opt/rpx-gateway` install and starts the gateway with the generated
108
152
  * options. We ship a runnable launcher (not a bare config) because rpx's CLI
109
153
  * resolves its own config from its install dir, not an arbitrary path.
110
154
  */
111
155
  export declare function renderRpxLauncher(config: RpxGatewayConfig): string;
112
156
  /** Default install location for the gateway launcher + config on the box. */
113
157
  export declare const RPX_DIR = "/etc/rpx";
158
+ export declare const RPX_INSTALL_DIR = "/opt/rpx-gateway";
114
159
  export declare const RPX_LAUNCHER_PATH = "/etc/rpx/gateway.ts";
115
160
  export declare const RPX_SERVICE_NAME = "rpx-gateway.service";
161
+ /**
162
+ * Per-app gateway registry. Each project's deploy writes ONLY its own fragment
163
+ * (`<slug>.json`) here; the launcher ({@link renderRpxAssembler}) merges every
164
+ * fragment at startup. So several independent apps share one box's gateway
165
+ * Forge-style — one app's deploy never clobbers another's routes.
166
+ */
167
+ export declare const RPX_SITES_DIR = "/etc/rpx/sites.d";
168
+ /** A registry fragment: one project's gateway config, tagged with its slug. */
169
+ export type RpxFragment = RpxGatewayConfig & {
170
+ slug: string;
171
+ };
172
+ /**
173
+ * Merge per-app fragments into one gateway config (the runtime equivalent of
174
+ * what {@link renderRpxAssembler} does on the box, exported for testing).
175
+ *
176
+ * Routes are concatenated (deduped by `id`, first writer wins); on-demand
177
+ * suffixes and origin-guard hosts are unioned; the first non-empty email /
178
+ * certsDir / acmeChallengeWebroot / origin-guard header+secret wins. Fragments
179
+ * are applied in the given order (the box sorts them by filename).
180
+ */
181
+ export declare function mergeRpxFragments(fragments: RpxGatewayConfig[]): RpxGatewayConfig;
182
+ /**
183
+ * Render the stable assembler launcher. Its content is identical for every app
184
+ * and every deploy — it reads all `<slug>.json` fragments from {@link RPX_SITES_DIR}
185
+ * at startup, merges them (same algorithm as {@link mergeRpxFragments}), and
186
+ * starts the gateway. A malformed fragment is skipped, not fatal.
187
+ */
188
+ export declare function renderRpxAssembler(sitesDir?: string, defaultCertsDir?: string): string;
116
189
  export interface BuildRpxProvisionOptions {
117
190
  config: RpxGatewayConfig;
118
191
  proxy: ComputeProxyConfig;
192
+ /**
193
+ * This project's slug — the registry fragment is written to
194
+ * `<sites.d>/<slug>.json`. So a box can host several independent apps and each
195
+ * deploy only rewrites its own fragment. Defaults to `'app'` for single-app
196
+ * boxes / backward compatibility.
197
+ */
198
+ slug?: string;
119
199
  /** Absolute path to the `bun` binary on the box. @default '/usr/local/bin/bun' */
120
200
  bunBin?: string;
121
201
  }
202
+ export declare const RPX_CERT_RENEW_SCRIPT = "/etc/rpx/renew-certs.sh";
203
+ export declare const RPX_CERT_RENEW_SERVICE = "rpx-cert-renew.service";
204
+ export declare const RPX_CERT_RENEW_TIMER = "rpx-cert-renew.timer";
205
+ /** The routable FQDNs in a gateway config — each terminates TLS so each needs a cert. */
206
+ export declare function certDomainsForConfig(config: RpxGatewayConfig): string[];
207
+ /**
208
+ * Commands that make ts-cloud manage the gateway's TLS certs end-to-end: install
209
+ * tlsx, issue a Let's Encrypt cert for every routed domain via http-01 (the
210
+ * running gateway serves the challenge from {@link RpxGatewayConfig.acmeChallengeWebroot}
211
+ * on `:80`, so no downtime), and a daily systemd timer that renews anything
212
+ * expiring within 30 days and reloads the gateway only when a cert changed.
213
+ *
214
+ * Returns `[]` (no-op) unless on-demand/managed TLS is enabled and there's at
215
+ * least one routable domain. Must run AFTER the gateway is started so the
216
+ * challenge listener is live.
217
+ */
218
+ export declare function buildCertManagementCommands(options: BuildRpxProvisionOptions): string[];
122
219
  /**
123
220
  * Build the idempotent, re-runnable shell commands that install rpx as the
124
221
  * gateway, write the generated launcher + ensure the certs dir, install the
package/dist/index.d.ts CHANGED
@@ -13,6 +13,8 @@ export { deployStaticSite, deployStaticSiteFull, uploadStaticFiles, invalidateCa
13
13
  export type { StaticSiteConfig, DeployResult, UploadOptions, ExternalDnsStaticSiteConfig, ExternalDnsDeployResult, DeploySiteConfig, DeploySiteResult, StaticSiteDnsProvider, SiteDeployKind, DeploymentValidationResult, BuildImageOptions, BuiltImage, CodeSource, DeployServerlessOptions, ResolvedContext, } from './deploy';
14
14
  export { createCloudDriver, CloudDriverFactory, cloudDrivers, AwsDriver, HetznerDriver, HetznerClient, resolveHetznerApiToken, generateUbuntuAppCloudInit, wrapCloudInitUserData, buildSiteDeployScript, buildStaticSiteDeployScript, resolveExecStart, deployAllComputeSites, deploySiteRelease, } from './drivers';
15
15
  export type { CreateCloudDriverOptions } from './drivers/factory';
16
+ export { dashboardActions, resolveDashboardAction, sanitizeCloudConfig, startLocalDashboardServer, } from './deploy/local-dashboard-server';
17
+ export type { DashboardAction, LocalDashboardServer, LocalDashboardServerOptions, } from './deploy/local-dashboard-server';
16
18
  export { createDnsProvider, detectDnsProvider, DnsProviderFactory, dnsProviders, PorkbunProvider, GoDaddyProvider, Route53Provider, UnifiedDnsValidator, createPorkbunValidator, createGoDaddyValidator, createRoute53Validator, } from './dns';
17
19
  export type { DnsProvider, DnsProviderConfig, DnsRecord, DnsRecordType, DnsRecordResult, CreateRecordResult, DeleteRecordResult, ListRecordsResult, } from './dns';
18
20
  export * from '@ts-cloud/core';