@stacksjs/ts-cloud 0.7.17 → 0.7.19

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 (60) hide show
  1. package/dist/bin/cli.js +747 -621
  2. package/dist/{chunk-pr3g0b0t.js → chunk-nt2hrnva.js} +744 -52
  3. package/dist/{chunk-qergre5a.js → chunk-xgnxz5dz.js} +828 -758
  4. package/dist/deploy/dashboard-auth.d.ts +82 -0
  5. package/dist/deploy/dashboard-auth.test.d.ts +1 -0
  6. package/dist/deploy/dashboard-guard.d.ts +40 -0
  7. package/dist/deploy/dashboard-login-page.d.ts +16 -0
  8. package/dist/deploy/dashboard-pages.test.d.ts +1 -0
  9. package/dist/deploy/dashboard-policy.d.ts +37 -0
  10. package/dist/deploy/dashboard-policy.test.d.ts +1 -0
  11. package/dist/deploy/dashboard-scope.d.ts +24 -0
  12. package/dist/deploy/dashboard-scope.test.d.ts +1 -0
  13. package/dist/deploy/dashboard-session.d.ts +52 -0
  14. package/dist/deploy/dashboard-users.d.ts +60 -0
  15. package/dist/deploy/index.js +2 -2
  16. package/dist/deploy/local-dashboard-server.d.ts +1 -0
  17. package/dist/drivers/hetzner/client.d.ts +9 -1
  18. package/dist/drivers/hetzner/config.d.ts +83 -0
  19. package/dist/drivers/hetzner/config.test.d.ts +1 -0
  20. package/dist/drivers/hetzner/factory-wiring.test.d.ts +1 -0
  21. package/dist/drivers/index.js +1 -1
  22. package/dist/drivers/shared/rpx-gateway.d.ts +29 -2
  23. package/dist/index.js +2 -2
  24. package/dist/ui/index.html +4 -4
  25. package/dist/ui/server/actions.html +4 -4
  26. package/dist/ui/server/activity.html +1 -1
  27. package/dist/ui/server/backups.html +4 -4
  28. package/dist/ui/server/database.html +4 -4
  29. package/dist/ui/server/deployments.html +4 -4
  30. package/dist/ui/server/diagnostics.html +1 -1
  31. package/dist/ui/server/firewall.html +4 -4
  32. package/dist/ui/server/logs.html +4 -4
  33. package/dist/ui/server/metrics.html +1 -1
  34. package/dist/ui/server/security.html +1 -1
  35. package/dist/ui/server/services.html +4 -4
  36. package/dist/ui/server/sites.html +4 -4
  37. package/dist/ui/server/ssh-keys.html +4 -4
  38. package/dist/ui/server/team.html +1214 -0
  39. package/dist/ui/server/terminal.html +4 -4
  40. package/dist/ui/server/workers.html +4 -4
  41. package/dist/ui/serverless/alarms.html +4 -4
  42. package/dist/ui/serverless/assets.html +4 -4
  43. package/dist/ui/serverless/cost.html +1 -1
  44. package/dist/ui/serverless/data.html +4 -4
  45. package/dist/ui/serverless/deployments.html +4 -4
  46. package/dist/ui/serverless/firewall.html +1 -1
  47. package/dist/ui/serverless/functions.html +4 -4
  48. package/dist/ui/serverless/logs.html +4 -4
  49. package/dist/ui/serverless/metrics.html +1 -1
  50. package/dist/ui/serverless/queues.html +4 -4
  51. package/dist/ui/serverless/scheduler.html +4 -4
  52. package/dist/ui/serverless/secrets.html +4 -4
  53. package/dist/ui/serverless/traces.html +4 -4
  54. package/dist/ui/serverless.html +4 -4
  55. package/dist/ui-src/pages/partials/head.stx +7 -0
  56. package/dist/ui-src/pages/partials/nav.stx +43 -6
  57. package/dist/ui-src/pages/server/firewall.stx +1 -1
  58. package/dist/ui-src/pages/server/sites.stx +42 -20
  59. package/dist/ui-src/pages/server/team.stx +171 -0
  60. package/package.json +3 -3
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Identity and authorization for the management dashboard.
3
+ *
4
+ * The dashboard hosts sites for more than one party: Stacks owns the box, and
5
+ * collaborators are invited to individual sites. That makes authorization a
6
+ * security boundary rather than a convenience — a site collaborator must never
7
+ * reach another tenant's data, and must never reach the box itself.
8
+ *
9
+ * The model has two levels:
10
+ * - Box role: `admin` (owns the box — everything) or `member` (only what they
11
+ * have been granted).
12
+ * - Site grants: a member holds a {@link SiteRole} per site. `owner` may change
13
+ * the site's settings; `collaborator` may view and deploy it.
14
+ *
15
+ * Box-level capabilities (shell, terminal, SSH keys, firewall, databases,
16
+ * cloud-config edits) are **admin-only and never grantable per site**. Each of
17
+ * them yields root on the box, which would hand a single site's collaborator
18
+ * control of every other tenant's site. There is deliberately no grant that
19
+ * unlocks them for a member.
20
+ *
21
+ * Authorization is deny-by-default: {@link authorize} answers `false` for any
22
+ * capability it does not explicitly recognize.
23
+ */
24
+ /** Box-wide role. `admin` owns the server and everything hosted on it. */
25
+ export type BoxRole = 'admin' | 'member';
26
+ /**
27
+ * A member's role on one site. `owner` can additionally change that site's
28
+ * settings; `collaborator` is limited to viewing and deploying it.
29
+ */
30
+ export type SiteRole = 'owner' | 'collaborator';
31
+ export interface DashboardUser {
32
+ username: string;
33
+ /** Encoded scrypt hash — see {@link hashPassword}. Never a plaintext password. */
34
+ passwordHash: string;
35
+ role: BoxRole;
36
+ /** Site name → role. Ignored for admins, who reach every site. */
37
+ sites: Record<string, SiteRole>;
38
+ /** Display name shown in the UI. Defaults to the username. */
39
+ name?: string;
40
+ createdAt?: string;
41
+ }
42
+ /**
43
+ * Everything the dashboard can be asked to do.
44
+ *
45
+ * `box:*` capabilities are admin-only. `site:*` capabilities are evaluated
46
+ * against a specific site and may be granted to a member.
47
+ */
48
+ export type Capability = 'box:read' | 'box:shell' | 'box:ssh' | 'box:firewall' | 'box:database' | 'box:config' | 'box:sites:create' | 'box:sites:delete' | 'box:serverless' | 'box:users' | 'site:read' | 'site:deploy' | 'site:settings';
49
+ /**
50
+ * Capabilities that are never grantable to a member, for any site. Listed
51
+ * explicitly so that adding a `site:*` capability can't silently widen a
52
+ * member's reach into box-level control.
53
+ */
54
+ export declare function isBoxCapability(capability: Capability): boolean;
55
+ export interface AuthorizeInput {
56
+ user: Pick<DashboardUser, 'role' | 'sites'>;
57
+ capability: Capability;
58
+ /** Required for every `site:*` capability; ignored for `box:*`. */
59
+ site?: string;
60
+ }
61
+ /**
62
+ * The single authorization decision point. Deny-by-default: an unrecognized
63
+ * capability, a missing site, or a site the member holds no grant on all
64
+ * return `false`.
65
+ */
66
+ export declare function authorize({ user, capability, site }: AuthorizeInput): boolean;
67
+ /** The sites a user may see. Admins see everything, so pass the full list. */
68
+ export declare function visibleSites(user: Pick<DashboardUser, 'role' | 'sites'>, allSites: string[]): string[];
69
+ /**
70
+ * Hash a password for storage: `scrypt$N$r$p$salt$hash` (both salt and hash
71
+ * base64url). The parameters travel with the hash so they can be raised later
72
+ * without invalidating existing credentials.
73
+ */
74
+ export declare function hashPassword(password: string): string;
75
+ /**
76
+ * Verify a password against an encoded hash, in constant time. Returns false
77
+ * for malformed hashes rather than throwing — a corrupt user record must fail
78
+ * the login, not crash the server.
79
+ */
80
+ export declare function verifyPassword(password: string, stored: string): boolean;
81
+ /** A URL-safe generated password, for invites and the bootstrap admin. */
82
+ export declare function generatePassword(): string;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,40 @@
1
+ /**
2
+ * The request-time gate: resolve who is calling, then decide whether the
3
+ * {@link routePolicy} for their request permits it.
4
+ *
5
+ * Every dashboard request passes through here before reaching a handler, so
6
+ * this file and {@link import('./dashboard-policy')} together are the complete
7
+ * authorization story for the HTTP API.
8
+ */
9
+ import type { DashboardUser } from './dashboard-auth';
10
+ /**
11
+ * A synthetic admin used only when auth is explicitly disabled for local
12
+ * development. It is never persisted and never has a password.
13
+ */
14
+ export declare const LOCAL_ADMIN: DashboardUser;
15
+ export interface GuardOptions {
16
+ cwd: string;
17
+ /** When false, every request is treated as {@link LOCAL_ADMIN}. */
18
+ enabled: boolean;
19
+ secret: string;
20
+ }
21
+ export interface GuardDecision {
22
+ ok: boolean;
23
+ status?: number;
24
+ error?: string;
25
+ /** True when the caller has no valid session at all (vs. lacking a grant). */
26
+ unauthenticated?: boolean;
27
+ }
28
+ export interface DashboardGuard {
29
+ enabled: boolean;
30
+ /** The user for this request, or null when unauthenticated. */
31
+ resolveUser: (req: Request) => DashboardUser | null;
32
+ /** Whether `user` may perform `req`. `site` is required for site-scoped routes. */
33
+ check: (req: Request, pathname: string, user: DashboardUser | null, site?: string) => GuardDecision;
34
+ }
35
+ export declare function createDashboardGuard(options: GuardOptions): DashboardGuard;
36
+ /**
37
+ * Read the site name a site-scoped route acts on, without consuming the body
38
+ * the handler will read (the request is cloned).
39
+ */
40
+ export declare function siteFromRequest(req: Request, pathname: string): Promise<string | undefined>;
@@ -0,0 +1,16 @@
1
+ /**
2
+ * The login page.
3
+ *
4
+ * Served directly by the dashboard server rather than built with the rest of
5
+ * the stx UI: it has to render before there is a session, and the built pages
6
+ * are scope-specific (see the per-scope UI cache in `local-dashboard-server`).
7
+ * Keeping it self-contained also means a broken UI build still leaves a way in.
8
+ *
9
+ * It mirrors the cockpit's existing design tokens so the two don't read as
10
+ * different products.
11
+ */
12
+ /**
13
+ * The page. `serverless` only picks the post-login landing route, matching the
14
+ * redirect the server already does for a serverless deployment.
15
+ */
16
+ export declare function renderLoginPage(serverless?: boolean): string;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,37 @@
1
+ /**
2
+ * The dashboard's route → capability table.
3
+ *
4
+ * This is the whole authorization surface of the HTTP API in one readable
5
+ * place, so it can be reviewed without tracing handlers. {@link routePolicy}
6
+ * maps a method and path to the {@link Capability} it needs.
7
+ *
8
+ * **Fails closed.** A route with no entry resolves to `box:shell` — the most
9
+ * privileged capability, admin-only. Adding a route and forgetting to give it a
10
+ * policy therefore locks members out rather than quietly exposing the route.
11
+ *
12
+ * Site-scoped routes take their site from the request body, so their entry sets
13
+ * `siteFrom: 'body'` and the caller supplies the parsed name.
14
+ */
15
+ import type { Capability } from './dashboard-auth';
16
+ export interface RoutePolicy {
17
+ capability: Capability;
18
+ /** Where the site name comes from, for `site:*` capabilities. */
19
+ siteFrom?: 'body';
20
+ /** Any authenticated user may call it, regardless of role. */
21
+ anyUser?: boolean;
22
+ }
23
+ /**
24
+ * Routes reachable with no session at all. These necessarily run before there
25
+ * is a user to authorize, so they are listed here explicitly — the set is
26
+ * deliberately tiny and should stay that way.
27
+ *
28
+ * `/login` (the page) is served by the same public path.
29
+ */
30
+ export declare const PUBLIC_ROUTES: ReadonlySet<string>;
31
+ export declare function isPublicRoute(method: string, pathname: string): boolean;
32
+ /**
33
+ * The policy for a request. Unlisted API routes fail closed to admin-only.
34
+ */
35
+ export declare function routePolicy(method: string, pathname: string): RoutePolicy;
36
+ /** Every policy entry, for tests and documentation. */
37
+ export declare function allRoutePolicies(): Record<string, RoutePolicy>;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Narrow resolved dashboard data to what a user is allowed to see.
3
+ *
4
+ * This runs server-side, before serialization: a member is *sent* less, not
5
+ * merely shown less. Hiding a panel in the UI would leave every other tenant's
6
+ * deploy history and log lines one devtools tab away.
7
+ *
8
+ * Admins get the payload untouched. For a member we keep only their sites and
9
+ * the observability derived from them, and drop the box-level surface
10
+ * (host metrics, services, SSH keys, firewall, backups) outright — none of it
11
+ * is theirs, and some of it (open ports, auth events) is a map of the box.
12
+ */
13
+ import type { DashboardUser } from './dashboard-auth';
14
+ export interface ScopeOptions {
15
+ user: Pick<DashboardUser, 'role' | 'sites'>;
16
+ /** Project slug, used to match a site to its systemd units. */
17
+ slug: string;
18
+ }
19
+ /**
20
+ * Return a copy of `data` containing only what `user` may see.
21
+ */
22
+ export declare function scopeDashboardData(data: Record<string, any>, options: ScopeOptions): Record<string, any>;
23
+ /** Narrow the sanitized cloud config the same way. */
24
+ export declare function scopeCloudConfig(config: Record<string, any>, user: Pick<DashboardUser, 'role' | 'sites'>): Record<string, any>;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Cookie-backed sessions for the management dashboard.
3
+ *
4
+ * Sessions are stateless signed tokens (`payload.signature`, HMAC-SHA256) so
5
+ * they survive a dashboard restart without a session store. The payload carries
6
+ * only the username and an expiry; every request re-loads the user from the
7
+ * store, so a revoked or downgraded grant takes effect immediately rather than
8
+ * lingering until the token expires.
9
+ *
10
+ * The signing secret is resolved from `TS_CLOUD_DASHBOARD_SECRET`, else
11
+ * generated once and persisted to `.ts-cloud/dashboard-secret`. Rotating it
12
+ * invalidates every outstanding session.
13
+ */
14
+ export declare const SESSION_COOKIE = "ts_cloud_session";
15
+ export declare const SECRET_FILE: string;
16
+ /** Eight hours: long enough to work through a deploy, short enough to expire. */
17
+ export declare const SESSION_TTL_MS: number;
18
+ export interface SessionPayload {
19
+ /** Username the session belongs to. */
20
+ u: string;
21
+ /** Expiry, epoch milliseconds. */
22
+ exp: number;
23
+ }
24
+ /**
25
+ * Resolve the HMAC signing secret, generating and persisting one on first use.
26
+ * The file is written 0600 — it is equivalent to every dashboard credential.
27
+ */
28
+ export declare function resolveSessionSecret(cwd: string): string;
29
+ /** Issue a signed session token for `username`. */
30
+ export declare function createSessionToken(username: string, secret: string, ttlMs?: number): string;
31
+ /**
32
+ * Verify a token and return its payload, or null when the signature is invalid,
33
+ * the token is malformed, or it has expired. Signature is compared before the
34
+ * payload is trusted for anything.
35
+ */
36
+ export declare function verifySessionToken(token: string | undefined, secret: string): SessionPayload | null;
37
+ /** Read one cookie from a request's `Cookie` header. */
38
+ export declare function readCookie(header: string | null, name: string): string | undefined;
39
+ /**
40
+ * Serialize the session cookie. `HttpOnly` keeps it away from page scripts,
41
+ * `SameSite=Lax` blocks cross-site POSTs from carrying it (the dashboard's CSRF
42
+ * defense), and `Secure` is set whenever the dashboard is not on loopback —
43
+ * in production it is always behind TLS.
44
+ */
45
+ export declare function serializeSessionCookie(token: string, options?: {
46
+ secure: boolean;
47
+ maxAgeMs?: number;
48
+ }): string;
49
+ /** Cookie that clears the session (logout). */
50
+ export declare function clearSessionCookie(options?: {
51
+ secure: boolean;
52
+ }): string;
@@ -0,0 +1,60 @@
1
+ /**
2
+ * The dashboard's user store: a JSON file of {@link DashboardUser} records at
3
+ * `.ts-cloud/dashboard-users.json` (0600), holding scrypt hashes and site
4
+ * grants. Small on purpose — a box hosts a handful of collaborators, not a
5
+ * directory service.
6
+ *
7
+ * On first use the store bootstraps a single admin so a freshly provisioned
8
+ * dashboard is never reachable without credentials. The generated password is
9
+ * returned to the caller to print once; only its hash is ever written.
10
+ */
11
+ import type { DashboardUser, SiteRole } from './dashboard-auth';
12
+ export declare const USERS_FILE: string;
13
+ /** Usernames are used in URLs and shell-free contexts; keep them boring. */
14
+ export declare function isValidUsername(value: string): boolean;
15
+ export declare function parseUsersFile(text: string): DashboardUser[];
16
+ export declare function usersFilePath(cwd: string): string;
17
+ export declare function loadUsers(cwd: string): DashboardUser[];
18
+ export declare function saveUsers(cwd: string, users: DashboardUser[]): void;
19
+ export declare function findUser(users: DashboardUser[], username: string): DashboardUser | undefined;
20
+ export interface BootstrapResult {
21
+ users: DashboardUser[];
22
+ /** Set only when an admin was just created — print it once, then it's gone. */
23
+ generated?: {
24
+ username: string;
25
+ password: string;
26
+ };
27
+ }
28
+ /**
29
+ * Ensure at least one admin exists. When the store is empty, create an admin
30
+ * using `TS_CLOUD_UI_PASSWORD` if set, else a generated password that is
31
+ * returned for one-time display.
32
+ */
33
+ export declare function ensureAdminUser(cwd: string, username?: string): BootstrapResult;
34
+ export interface UpsertMemberInput {
35
+ username: string;
36
+ password?: string;
37
+ name?: string;
38
+ sites: Record<string, SiteRole>;
39
+ }
40
+ /**
41
+ * Create or update a member and their site grants. Returns the generated
42
+ * password when one was minted (a new user without an explicit password).
43
+ *
44
+ * Members are created through this path only — it cannot produce an admin, so
45
+ * an invite flow can never escalate someone to box-wide control.
46
+ */
47
+ export declare function upsertMember(cwd: string, input: UpsertMemberInput): {
48
+ user: DashboardUser;
49
+ password?: string;
50
+ };
51
+ /**
52
+ * Remove a user. The last admin cannot be removed — that would lock everyone
53
+ * out of the box with no way back in short of editing the file by hand.
54
+ */
55
+ export declare function removeUser(cwd: string, username: string): {
56
+ ok: boolean;
57
+ error?: string;
58
+ };
59
+ /** Public shape for the UI — never leaks the password hash. */
60
+ export declare function describeUser(user: DashboardUser): Record<string, any>;
@@ -12,7 +12,7 @@ import {
12
12
  sanitizeCloudConfig,
13
13
  setMaintenance,
14
14
  startLocalDashboardServer
15
- } from "../chunk-pr3g0b0t.js";
15
+ } from "../chunk-nt2hrnva.js";
16
16
  import {
17
17
  buildAndPushServerlessImage
18
18
  } from "../chunk-qgnyzfmx.js";
@@ -30,7 +30,7 @@ import {
30
30
  resolveUiSource,
31
31
  siteInstallBase,
32
32
  validateDeploymentConfig
33
- } from "../chunk-qergre5a.js";
33
+ } from "../chunk-xgnxz5dz.js";
34
34
  import"../chunk-49nmy775.js";
35
35
  import"../chunk-93hjhs78.js";
36
36
  import {
@@ -28,4 +28,5 @@ export interface DashboardAction {
28
28
  export declare function sanitizeCloudConfig(config: CloudConfig): Record<string, any>;
29
29
  export declare function dashboardActions(environment: EnvironmentType): DashboardAction[];
30
30
  export declare function resolveDashboardAction(id: string, environment: EnvironmentType): DashboardAction | undefined;
31
+ export declare function isBoxOnlyPage(pathname: string): boolean;
31
32
  export declare function startLocalDashboardServer(options?: LocalDashboardServerOptions): Promise<LocalDashboardServer>;
@@ -2,6 +2,7 @@
2
2
  * Hetzner Cloud API client
3
3
  * @see https://docs.hetzner.cloud/
4
4
  */
5
+ import type { CloudConfig } from '@ts-cloud/core';
5
6
  export interface HetznerApiErrorBody {
6
7
  error?: {
7
8
  code?: string;
@@ -183,7 +184,14 @@ export declare class HetznerClient {
183
184
  maxWaitMs?: number;
184
185
  }): Promise<HetznerServer>;
185
186
  }
186
- export declare function resolveHetznerApiToken(configToken?: string): string;
187
+ /**
188
+ * The Hetzner API token, or a clear error when none is configured.
189
+ *
190
+ * Resolution itself lives in {@link import('./config').resolveHetznerApiToken}
191
+ * so the driver, the client and the dashboard cannot drift apart on where a
192
+ * setting comes from.
193
+ */
194
+ export declare function resolveHetznerApiToken(configToken?: string, config?: CloudConfig): string;
187
195
  /**
188
196
  * Normalize an OpenSSH public key to its `<type> <base64>` body, dropping the
189
197
  * trailing comment. Lets us match a local key against keys already registered
@@ -0,0 +1,83 @@
1
+ /**
2
+ * The single place Hetzner settings are resolved.
3
+ *
4
+ * Every Hetzner value can come from several sources, and they used to be
5
+ * resolved ad-hoc wherever they were needed: the driver, the API client and the
6
+ * dashboard each had their own chain, with defaults duplicated at call sites.
7
+ * That drifted — the dashboard honored `HETZNER_LOCATION` while the driver did
8
+ * not, so a box provisioned in `fsn1` was reported as being somewhere else.
9
+ *
10
+ * One precedence, applied everywhere:
11
+ *
12
+ * 1. an explicit argument (a driver option — the caller means it)
13
+ * 2. `cloud.config.ts` → `hetzner.*` (checked into the repo, reviewable)
14
+ * 3. environment (`HCLOUD_*`, with the `HETZNER_*` alias) — for secrets and
15
+ * per-machine overrides
16
+ * 4. the documented default in {@link HETZNER_DEFAULTS}
17
+ *
18
+ * Config beats environment deliberately, for every field without exception: a
19
+ * value written in `cloud.config.ts` is the reviewed intent for the project,
20
+ * and a stray shell export should not silently redirect a deploy to another
21
+ * datacenter or another account. One rule, no special cases to remember.
22
+ *
23
+ * In practice the token is simply left out of `cloud.config.ts` (it is a
24
+ * secret), so it comes from `HCLOUD_TOKEN` — but that is a convention, not a
25
+ * different precedence.
26
+ */
27
+ import type { CloudConfig } from '@ts-cloud/core';
28
+ /** The documented defaults. These are the only place a Hetzner default lives. */
29
+ export declare const HETZNER_DEFAULTS: {
30
+ /** Falkenstein, Germany. */
31
+ readonly location: 'fsn1';
32
+ readonly image: 'ubuntu-24.04';
33
+ readonly sshUser: 'root';
34
+ readonly sshPrivateKeyPath: '~/.ssh/id_ed25519';
35
+ };
36
+ /** `~/…` → an absolute path. Hetzner key paths are user-supplied and often use `~`. */
37
+ export declare function expandHome(path: string): string;
38
+ /**
39
+ * The Hetzner API token, or undefined when none is set.
40
+ *
41
+ * Never defaulted: a missing token must fail loudly at the call site rather
42
+ * than silently targeting the wrong account. Callers that need one should use
43
+ * `requireHetznerApiToken`.
44
+ */
45
+ export declare function resolveHetznerApiToken(explicit?: string, config?: CloudConfig): string | undefined;
46
+ /** Datacenter location slug, e.g. `fsn1`, `nbg1`, `hel1`. */
47
+ export declare function resolveHetznerLocation(config?: CloudConfig, explicit?: string): string;
48
+ /**
49
+ * Server image slug.
50
+ *
51
+ * `infrastructure.compute.image` wins over `hetzner.image`: it is the
52
+ * provider-agnostic way to pin an image (and is what a golden-image bake sets),
53
+ * so it is the more specific statement of intent.
54
+ */
55
+ export declare function resolveHetznerImage(config?: CloudConfig, explicit?: string): string;
56
+ /** SSH user for deploy commands. */
57
+ export declare function resolveHetznerSshUser(config?: CloudConfig, explicit?: string): string;
58
+ /** Absolute path to the SSH private key used for deploy commands. */
59
+ export declare function resolveHetznerSshPrivateKeyPath(config?: CloudConfig, explicit?: string): string;
60
+ /**
61
+ * Absolute path to the SSH public key uploaded to Hetzner. Defaults to the
62
+ * private key's path with `.pub`, which is where `ssh-keygen` puts it.
63
+ */
64
+ export declare function resolveHetznerSshPublicKeyPath(config?: CloudConfig, explicit?: string, privateKeyPath?: string): string;
65
+ /** Every resolved Hetzner setting, for a driver or a diagnostic to read at once. */
66
+ export interface ResolvedHetznerSettings {
67
+ apiToken?: string;
68
+ location: string;
69
+ image: string;
70
+ sshUser: string;
71
+ sshPrivateKeyPath: string;
72
+ sshPublicKeyPath: string;
73
+ }
74
+ export interface HetznerOverrides {
75
+ apiToken?: string;
76
+ location?: string;
77
+ image?: string;
78
+ sshUser?: string;
79
+ sshPrivateKeyPath?: string;
80
+ sshPublicKeyPath?: string;
81
+ }
82
+ /** Resolve the full Hetzner settings for `config`, applying `overrides` first. */
83
+ export declare function resolveHetznerSettings(config?: CloudConfig, overrides?: HetznerOverrides): ResolvedHetznerSettings;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -48,7 +48,7 @@ import {
48
48
  waitForCloudInit,
49
49
  waitForSsh,
50
50
  wrapCloudInitUserData
51
- } from "../chunk-qergre5a.js";
51
+ } from "../chunk-xgnxz5dz.js";
52
52
  import"../chunk-49nmy775.js";
53
53
  import"../chunk-93hjhs78.js";
54
54
  import"../chunk-qpj3edwz.js";
@@ -48,8 +48,20 @@ export interface RpxRoute {
48
48
  * with automatic health-check failover (see rpx's `ProxyFrom`/`UpstreamTarget`).
49
49
  */
50
50
  from?: string | string[];
51
- /** Absolute directory served for a `server-static` route (`/var/www/<name>`). */
52
- static?: string;
51
+ /**
52
+ * Static serving for a `server-static` route. rpx reads `spa` and
53
+ * `pathRewriteStyle` ONLY from the object form (`{ dir, spa, pathRewriteStyle }`);
54
+ * a bare string disables both (rpx forces `spa: false`, `pathRewriteStyle:
55
+ * 'directory'`). So SPA fallback + flat-URL sites MUST use the object form —
56
+ * see `buildRpxConfig`. The string shorthand remains valid for a plain
57
+ * directory served with the route-level `cleanUrls`.
58
+ */
59
+ static?: string | {
60
+ dir: string;
61
+ spa?: boolean;
62
+ pathRewriteStyle?: 'directory' | 'flat';
63
+ maxAge?: number;
64
+ };
53
65
  /**
54
66
  * Redirect target for a `redirect` site — the gateway answers `to` (the host)
55
67
  * with an HTTP redirect here instead of proxying/serving. The request path +
@@ -197,6 +209,21 @@ export declare function buildRpxLbConfig(sites: Record<string, SiteConfig | unde
197
209
  * resolves its own config from its install dir, not an arbitrary path.
198
210
  */
199
211
  export declare function renderRpxLauncher(config: RpxGatewayConfig): string;
212
+ /**
213
+ * True when this compute is fronted by the rpx gateway — either explicitly
214
+ * (`webServer: 'rpx'`) or implicitly by opting into the rpx proxy engine
215
+ * (`proxy.engine: 'rpx'`). Setting `proxy.engine: 'rpx'` alone provisions and
216
+ * runs the gateway on :80/:443, so the deploy MUST NOT also stand up nginx +
217
+ * certbot for a site — that races the gateway for :80 and the certbot HTTP-01
218
+ * challenge fails with "Address already in use". Treating either signal as
219
+ * "rpx mode" keeps the nginx/SSL path and the gateway path from contradicting.
220
+ */
221
+ export declare function usesRpxProxy(compute?: {
222
+ webServer?: string;
223
+ proxy?: {
224
+ engine?: string;
225
+ };
226
+ }): boolean;
200
227
  /** Default install location for the gateway launcher + config on the box. */
201
228
  export declare const RPX_DIR = "/etc/rpx";
202
229
  export declare const RPX_INSTALL_DIR = "/opt/rpx-gateway";
package/dist/index.js CHANGED
@@ -55,7 +55,7 @@ import {
55
55
  sanitizeCloudConfig,
56
56
  setMaintenance,
57
57
  startLocalDashboardServer
58
- } from "./chunk-pr3g0b0t.js";
58
+ } from "./chunk-nt2hrnva.js";
59
59
  import {
60
60
  buildAndPushServerlessImage
61
61
  } from "./chunk-qgnyzfmx.js";
@@ -105,7 +105,7 @@ import {
105
105
  waitForCloudInit,
106
106
  waitForSsh,
107
107
  wrapCloudInitUserData
108
- } from "./chunk-qergre5a.js";
108
+ } from "./chunk-xgnxz5dz.js";
109
109
  import {
110
110
  ABTestManager,
111
111
  AI,