@ts-cloud/core 0.2.25 → 0.2.27

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.
@@ -13,6 +13,10 @@ export interface ComputeStackOutputs {
13
13
  appInstanceId?: string;
14
14
  appPublicIp?: string;
15
15
  sshUser?: string;
16
+ /** Fleet: private IP of the dedicated services box (DB/cache/search). */
17
+ servicesPrivateIp?: string;
18
+ /** Fleet: public IP of the load balancer fronting the app servers. */
19
+ loadBalancerIp?: string;
16
20
  }
17
21
  export interface RemoteDeployInstanceResult {
18
22
  instanceId: string;
@@ -65,6 +69,15 @@ export interface CloudDriver {
65
69
  readonly usesCloudFormation: boolean;
66
70
  /** Provision compute infrastructure (Hetzner). AWS uses InfrastructureGenerator + CFN. */
67
71
  provisionComputeInfrastructure?(options: ProvisionComputeOptions): Promise<ComputeStackOutputs>;
72
+ /**
73
+ * Tear down the lightweight single-server compute (instance + security
74
+ * group/firewall + local state) provisioned by
75
+ * {@link provisionComputeInfrastructure}. Returns a human-readable summary of
76
+ * what was destroyed. Not for CloudFormation-managed stacks.
77
+ */
78
+ destroyCompute?(options: ProvisionComputeOptions): Promise<{
79
+ destroyed: string[];
80
+ }>;
68
81
  /** Read outputs needed for deploy (stack outputs, state file, or live API) */
69
82
  getComputeOutputs(options: ProvisionComputeOptions): Promise<ComputeStackOutputs>;
70
83
  /** Upload a release tarball to provider-specific staging storage */
@@ -81,8 +94,13 @@ export interface DeploySiteReleaseOptions {
81
94
  site: SiteConfig;
82
95
  slug: string;
83
96
  sha: string;
84
- runtime: 'bun' | 'node' | 'deno';
85
- localTarballPath: string;
97
+ runtime: 'bun' | 'node' | 'deno' | 'php';
98
+ /**
99
+ * Local release tarball to ship. Required for tarball deploys (bun/node/deno
100
+ * apps and server-static sites); omitted for PHP/Laravel sites, which clone
101
+ * from git on the box instead.
102
+ */
103
+ localTarballPath?: string;
86
104
  }
87
105
  export interface DeploySiteReleaseResult {
88
106
  success: boolean;
package/dist/index.d.ts CHANGED
@@ -25,6 +25,8 @@ export * from './presets/realtime-app';
25
25
  export * from './presets/data-pipeline';
26
26
  export * from './presets/ml-api';
27
27
  export * from './presets/traditional-web-app';
28
+ export * from './presets/laravel';
29
+ export * from './presets/dashboard';
28
30
  export * from './presets/extend';
29
31
  export * from './aws/signature';
30
32
  export * from './aws/credentials';
package/dist/index.js CHANGED
@@ -2567,7 +2567,7 @@ class Storage {
2567
2567
  for (const docsPath of docsPaths) {
2568
2568
  const fullPath = join(projectRoot, docsPath);
2569
2569
  if (existsSync(fullPath)) {
2570
- const distPaths = ["dist", "build", ".vitepress/dist", "_site", "out", "public"];
2570
+ const distPaths = ["dist", "build", ".bunpress/dist", "_site", "out", "public"];
2571
2571
  for (const distPath of distPaths) {
2572
2572
  const fullDistPath = join(fullPath, distPath);
2573
2573
  if (existsSync(fullDistPath)) {
@@ -3408,7 +3408,7 @@ exports.handler = async (event) => {
3408
3408
  }
3409
3409
 
3410
3410
  // Try to determine if this is a directory or a file
3411
- // First, try appending .html (for VitePress clean URLs)
3411
+ // First, try appending .html (for BunPress clean URLs)
3412
3412
  // If the file doesn't exist, CloudFront will try the directory with index.html
3413
3413
 
3414
3414
  // Check if the URI looks like a file path without extension
@@ -6041,7 +6041,8 @@ echo "Database tools installed!"
6041
6041
  java21: "java21",
6042
6042
  java17: "java17",
6043
6043
  go: "provided.al2023",
6044
- rust: "provided.al2023"
6044
+ rust: "provided.al2023",
6045
+ bun: "provided.al2023"
6045
6046
  },
6046
6047
  presets: {
6047
6048
  api: {
@@ -22145,6 +22146,77 @@ function createTraditionalWebAppPreset(options) {
22145
22146
  }
22146
22147
  };
22147
22148
  }
22149
+ // src/presets/laravel.ts
22150
+ function createLaravelPreset(options) {
22151
+ const {
22152
+ name,
22153
+ slug,
22154
+ domain,
22155
+ repository,
22156
+ phpVersion = "8.3",
22157
+ size = "small",
22158
+ database: database2 = slug.replace(/-/g, "_"),
22159
+ databasePassword,
22160
+ sslEmail,
22161
+ deployStrategy = "push",
22162
+ provider = "hetzner"
22163
+ } = options;
22164
+ return {
22165
+ project: { name, slug, region: "us-east-1" },
22166
+ mode: "server",
22167
+ cloud: { provider },
22168
+ environments: {
22169
+ production: { type: "production", domain }
22170
+ },
22171
+ infrastructure: {
22172
+ compute: {
22173
+ mode: "server",
22174
+ size,
22175
+ runtime: "php",
22176
+ webServer: "nginx",
22177
+ php: { versions: [phpVersion], default: phpVersion },
22178
+ managedServices: { mysql: true, redis: true },
22179
+ firewall: { enabled: true },
22180
+ autoUpdates: true,
22181
+ monitoring: true,
22182
+ backups: { enabled: true, schedule: "0 2 * * *", retentionCount: 7 }
22183
+ },
22184
+ appDatabase: {
22185
+ engine: "mysql",
22186
+ name: database2,
22187
+ username: database2,
22188
+ password: databasePassword
22189
+ }
22190
+ },
22191
+ sites: {
22192
+ main: {
22193
+ root: ".",
22194
+ type: "laravel",
22195
+ domain,
22196
+ phpVersion,
22197
+ repository: { ...repository, strategy: deployStrategy },
22198
+ scheduler: true,
22199
+ queues: [{ connection: "redis", queue: "default", processes: 1 }],
22200
+ ssl: { provider: "letsencrypt", email: sslEmail }
22201
+ }
22202
+ }
22203
+ };
22204
+ }
22205
+ // src/presets/dashboard.ts
22206
+ function createDashboardSite(options) {
22207
+ return {
22208
+ root: options.root ?? "ui/dist",
22209
+ deploy: "server",
22210
+ type: "static",
22211
+ domain: options.domain,
22212
+ build: options.build ?? "cd ui && bun install && bun run build",
22213
+ ssl: { provider: "letsencrypt" },
22214
+ auth: {
22215
+ username: options.username ?? "admin",
22216
+ password: options.password
22217
+ }
22218
+ };
22219
+ }
22148
22220
  // src/presets/extend.ts
22149
22221
  function deepMerge(target, source) {
22150
22222
  const result = { ...target };
@@ -45580,10 +45652,12 @@ export {
45580
45652
  createMockAWS,
45581
45653
  createMicroservicesPreset,
45582
45654
  createMLApiPreset,
45655
+ createLaravelPreset,
45583
45656
  createJamstackPreset,
45584
45657
  createFullStackAppPreset,
45585
45658
  createError,
45586
45659
  createDataPipelinePreset,
45660
+ createDashboardSite,
45587
45661
  createCredentialProvider,
45588
45662
  createApiBackendPreset,
45589
45663
  containerRegistryManager,
@@ -171,7 +171,7 @@ export declare class CDN {
171
171
  */
172
172
  static readonly EdgeFunctionTemplates: {
173
173
  /**
174
- * Origin request handler for docs/VitePress routing
174
+ * Origin request handler for docs/BunPress routing
175
175
  */
176
176
  docsOriginRequest: string;
177
177
  /**
@@ -1073,6 +1073,7 @@ export declare class Compute {
1073
1073
  readonly java17: "java17";
1074
1074
  readonly go: "provided.al2023";
1075
1075
  readonly rust: "provided.al2023";
1076
+ readonly bun: "provided.al2023";
1076
1077
  };
1077
1078
  /**
1078
1079
  * Common function configurations
@@ -1145,7 +1146,7 @@ export declare class Compute {
1145
1146
  * - Multiple sites can share one EC2 instance
1146
1147
  */
1147
1148
  generateBunAppScript: (options: {
1148
- runtime?: "bun" | "node" | "deno";
1149
+ runtime?: "bun" | "node" | "deno" | "php";
1149
1150
  runtimeVersion?: string;
1150
1151
  systemPackages?: string[];
1151
1152
  database?: "sqlite" | "mysql" | "postgres";
@@ -1269,7 +1270,13 @@ export declare class Compute {
1269
1270
  subnetId: string;
1270
1271
  instanceType?: string;
1271
1272
  imageId?: string;
1272
- keyName: string;
1273
+ /**
1274
+ * EC2 KeyName for SSH access. Optional — when omitted, no SSH key is
1275
+ * associated with the instance and shell access goes via SSM Session
1276
+ * Manager. The provided KeyName must already exist in EC2 or the
1277
+ * launch fails with "key pair does not exist".
1278
+ */
1279
+ keyName?: string;
1273
1280
  domain?: string;
1274
1281
  userData?: string;
1275
1282
  allowedPorts?: number[];
@@ -0,0 +1,26 @@
1
+ import type { SiteConfig } from '../types';
2
+ /**
3
+ * The ts-cloud management dashboard (`@ts-cloud/ui`) as a deployable site —
4
+ * a static stx app served on the box by nginx, behind HTTP Basic auth whose
5
+ * password comes from an env value.
6
+ *
7
+ * Add it to your config's `sites` so `cloud deploy` builds + publishes it:
8
+ *
9
+ * ```ts
10
+ * sites: {
11
+ * dashboard: createDashboardSite({ domain: 'dashboard.acme.com', password: process.env.TS_CLOUD_UI_PASSWORD }),
12
+ * }
13
+ * ```
14
+ */
15
+ export declare function createDashboardSite(options: {
16
+ /** Domain the dashboard is served on (required for nginx vhost + SSL). */
17
+ domain: string;
18
+ /** Basic-auth password — typically `process.env.TS_CLOUD_UI_PASSWORD`. */
19
+ password?: string;
20
+ /** Basic-auth username. @default 'admin' */
21
+ username?: string;
22
+ /** Built UI output directory shipped to the box. @default 'ui/dist' */
23
+ root?: string;
24
+ /** Build command producing {@link root}. @default builds @ts-cloud/ui */
25
+ build?: string;
26
+ }): SiteConfig;
@@ -10,4 +10,5 @@ export { createRealtimeAppPreset } from './realtime-app';
10
10
  export { createDataPipelinePreset } from './data-pipeline';
11
11
  export { createMLApiPreset } from './ml-api';
12
12
  export { createTraditionalWebAppPreset } from './traditional-web-app';
13
+ export { createLaravelPreset } from './laravel';
13
14
  export { extendPreset, composePresets, createPreset, mergeInfrastructure, withMonitoring, withSecurity, withDatabase, withCache, withCDN, withQueue, } from './extend';
@@ -0,0 +1,36 @@
1
+ import type { CloudConfig } from '../types';
2
+ /**
3
+ * Laravel Preset — a Forge-style single server.
4
+ *
5
+ * Provisions one box (Hetzner by default) running nginx + php-fpm + Composer,
6
+ * MySQL + Redis on the box, host firewall (UFW), automatic security updates,
7
+ * monitoring, and scheduled backups. The app is deployed from git into atomic
8
+ * zero-downtime releases, served over HTTPS via Let's Encrypt, with the queue
9
+ * worker and scheduler enabled.
10
+ */
11
+ export declare function createLaravelPreset(options: {
12
+ name: string;
13
+ slug: string;
14
+ /** App domain (also used for the Let's Encrypt cert). */
15
+ domain?: string;
16
+ /** Git repository to deploy. */
17
+ repository: {
18
+ url: string;
19
+ branch?: string;
20
+ provider?: 'github' | 'gitlab' | 'bitbucket' | 'custom';
21
+ };
22
+ /** PHP version. @default '8.3' */
23
+ phpVersion?: string;
24
+ /** Server size (provider-specific token). @default 'small' */
25
+ size?: string;
26
+ /** App database name. @default slug */
27
+ database?: string;
28
+ /** Database password (set via env in real configs). */
29
+ databasePassword?: string;
30
+ /** Contact email for Let's Encrypt. */
31
+ sslEmail?: string;
32
+ /** Deploy strategy. @default 'push' */
33
+ deployStrategy?: 'push' | 'tag';
34
+ /** Provider. @default 'hetzner' */
35
+ provider?: 'hetzner' | 'aws';
36
+ }): Partial<CloudConfig>;
package/dist/types.d.ts CHANGED
@@ -77,6 +77,12 @@ export interface CloudConfig {
77
77
  environments: Record<string, EnvironmentConfig>;
78
78
  infrastructure?: InfrastructureConfig;
79
79
  sites?: Record<string, SiteConfig>;
80
+ /**
81
+ * Notification channels for deploy, SSL, health-check, and backup events
82
+ * (Slack, Discord, Telegram, email, generic webhook). Project-wide default;
83
+ * a site may override via {@link SiteConfig.notifications}.
84
+ */
85
+ notifications?: NotificationsConfig;
80
86
  /**
81
87
  * AWS-specific configuration
82
88
  */
@@ -294,6 +300,14 @@ export interface InfrastructureConfig {
294
300
  * - `'postgres'` → RDS Postgres with sane defaults, DATABASE_URL injected into env
295
301
  */
296
302
  database?: 'sqlite' | 'mysql' | 'postgres';
303
+ /**
304
+ * Application database connection (object form) for the Forge-style on-box /
305
+ * managed database path. Provides the name/user/password that
306
+ * `compute.managedServices` creates on the box, and the `DB_*` values
307
+ * auto-wired into PHP sites' `.env`. Distinct from the {@link database}
308
+ * string shorthand.
309
+ */
310
+ appDatabase?: DatabaseConfig;
297
311
  cache?: CacheConfig;
298
312
  cdn?: Record<string, CdnItemConfig & ResourceConditions> | CdnItemConfig;
299
313
  /**
@@ -824,6 +838,252 @@ export interface SiteConfig {
824
838
  * Example: ['bun install --frozen-lockfile', 'bun run build']
825
839
  */
826
840
  preStart?: string[];
841
+ /**
842
+ * SSR only. tar `--exclude` patterns applied when packaging the release
843
+ * tarball. Keep host-specific / heavy paths out of the artifact — most
844
+ * importantly `node_modules` (host-built native binaries won't run on the
845
+ * target OS; install fresh via `preStart` instead), plus `.git`, dev caches,
846
+ * and the built frontend.
847
+ *
848
+ * Example: ['node_modules', '.git', 'dist']
849
+ */
850
+ exclude?: string[];
851
+ /**
852
+ * Application type. Drives the default deploy script and the nginx vhost
853
+ * template:
854
+ * - `'laravel'` — `public/` web root, Laravel deploy script (composer,
855
+ * artisan caches, migrate, storage:link, queue:restart).
856
+ * - `'php'` — generic PHP app behind php-fpm (vanilla PHP, custom framework).
857
+ * - `'statamic'` / `'wordpress'` — PHP apps with framework-specific defaults.
858
+ * - `'static'` — plain static files served by nginx.
859
+ * - `'spa'` — single-page app with a `try_files … /index.html` fallback.
860
+ *
861
+ * When omitted the legacy inference applies (`start` ⇒ a systemd runtime app,
862
+ * otherwise a bucket static site) — so existing bun/node sites are unaffected.
863
+ */
864
+ type?: 'laravel' | 'php' | 'statamic' | 'wordpress' | 'static' | 'spa';
865
+ /**
866
+ * PHP version for this site (e.g. `'8.3'`). Selects the php-fpm pool/socket
867
+ * the nginx vhost points at. Must be one of `compute.php.versions`. Defaults
868
+ * to `compute.php.default`.
869
+ */
870
+ phpVersion?: string;
871
+ /**
872
+ * Web root relative to the release directory. Defaults to `'public'` for
873
+ * `laravel`/`statamic`/`wordpress`, and `''` (the release root) for `php`,
874
+ * `static`, and `spa`.
875
+ */
876
+ webDirectory?: string;
877
+ /**
878
+ * Git repository the server clones/pulls on deploy (Forge-style). When set,
879
+ * the deploy clones `branch` into `releases/<sha>` rather than shipping a
880
+ * tarball over SCP.
881
+ */
882
+ repository?: SiteRepositoryConfig;
883
+ /**
884
+ * Override the deploy script run inside the new release directory. When
885
+ * omitted, a sensible default for `type` is used (e.g. the Laravel script).
886
+ * The special tokens `$CREATE_RELEASE`, `$ACTIVATE_RELEASE`, and
887
+ * `$RESTART_QUEUES` expand to the zero-downtime release macros.
888
+ */
889
+ deployScript?: string[];
890
+ /**
891
+ * Paths symlinked from the site's `shared/` directory into every release so
892
+ * they persist across deploys (e.g. `storage`, uploaded files, a SQLite db).
893
+ * `.env` is always shared and need not be listed.
894
+ * @default ['storage', '.env']
895
+ */
896
+ sharedPaths?: string[];
897
+ /**
898
+ * Number of past releases to retain on the box for rollback.
899
+ * @default 4
900
+ */
901
+ keepReleases?: number;
902
+ /**
903
+ * Use zero-downtime atomic releases (Envoyer-style: clone → build → flip the
904
+ * `current` symlink only after every step succeeds, with the previous release
905
+ * kept for instant rollback). On by default for git-deployed PHP sites — set
906
+ * `false` only to deploy in place.
907
+ * @default true
908
+ */
909
+ zeroDowntime?: boolean;
910
+ /** Laravel queue workers to run for this site (systemd-managed). */
911
+ queues?: QueueWorkerConfig[];
912
+ /**
913
+ * Run the Laravel scheduler for this site
914
+ * (`* * * * * php artisan schedule:run`).
915
+ */
916
+ scheduler?: boolean;
917
+ /** Arbitrary long-running processes to keep alive (systemd-managed). */
918
+ daemons?: DaemonConfig[];
919
+ /** TLS configuration for this site's nginx vhost. */
920
+ ssl?: SiteSslConfig;
921
+ /** Additional hostnames served by the same vhost (nginx `server_name`). */
922
+ aliases?: string[];
923
+ /** `from` path/host → `to` URL redirects emitted into the nginx vhost. */
924
+ redirects?: Record<string, string>;
925
+ /**
926
+ * Give this site a dedicated php-fpm pool (isolated user/process) rather than
927
+ * sharing the default pool.
928
+ */
929
+ isolation?: boolean;
930
+ /** Post-deploy health check (Forge-style) pinged after `current` is flipped. */
931
+ healthCheck?: {
932
+ path?: string;
933
+ };
934
+ /**
935
+ * Per-site notification channels, overriding the project-wide
936
+ * {@link CloudConfig.notifications} for this site's events.
937
+ */
938
+ notifications?: NotificationsConfig;
939
+ /**
940
+ * HTTP Basic auth (htpasswd) protecting the whole site at the nginx layer.
941
+ * Typically driven from an env value, e.g. `{ username: 'admin', password:
942
+ * process.env.UI_PASSWORD }`. The htpasswd file is generated on the box.
943
+ */
944
+ auth?: SiteAuthConfig;
945
+ }
946
+ /** HTTP Basic auth for a site's nginx vhost. See {@link SiteConfig.auth}. */
947
+ export interface SiteAuthConfig {
948
+ /** Enable basic auth. @default true when this object is present */
949
+ enabled?: boolean;
950
+ /** Username. @default 'admin' */
951
+ username?: string;
952
+ /** Plaintext password (hashed on the box). Usually `process.env.X`. */
953
+ password?: string;
954
+ /** Realm shown in the browser auth prompt. @default 'Restricted' */
955
+ realm?: string;
956
+ }
957
+ /**
958
+ * Git source for a Forge-style git-clone deploy. See {@link SiteConfig.repository}.
959
+ */
960
+ export interface SiteRepositoryConfig {
961
+ /** Clone URL (https or git@). */
962
+ url: string;
963
+ /** Branch to deploy. @default 'main' */
964
+ branch?: string;
965
+ /** Hosting provider — drives push-to-deploy hook wiring. @default 'github' */
966
+ provider?: 'github' | 'gitlab' | 'bitbucket' | 'custom';
967
+ /**
968
+ * Deploy strategy:
969
+ * - `'push'` (default) — deploy the tip of `branch` (push-to-deploy).
970
+ * - `'tag'` — deploy a git version tag: a specific {@link tag}, or the latest
971
+ * tag matching {@link tagPattern} (e.g. release `v*` tags). Useful for
972
+ * promoting tagged releases rather than every push.
973
+ */
974
+ strategy?: 'push' | 'tag';
975
+ /** Exact tag to deploy when `strategy: 'tag'`. Overrides {@link tagPattern}. */
976
+ tag?: string;
977
+ /**
978
+ * Glob matching the tags to consider when `strategy: 'tag'` and no explicit
979
+ * {@link tag} is set; the highest version (`-sort=-v:refname`) is deployed.
980
+ * @default 'v*'
981
+ */
982
+ tagPattern?: string;
983
+ }
984
+ /**
985
+ * TLS for a PHP/static site's nginx vhost. See {@link SiteConfig.ssl}.
986
+ */
987
+ export interface SiteSslConfig {
988
+ /**
989
+ * Certificate source:
990
+ * - `'letsencrypt'` — issue + auto-renew via certbot (default for sites with
991
+ * a `domain`).
992
+ * - `'custom'` — install operator-provided `certPath`/`keyPath`.
993
+ * - `'none'` — serve plain HTTP only.
994
+ */
995
+ provider?: 'letsencrypt' | 'custom' | 'none';
996
+ /** Contact email for Let's Encrypt registration/expiry notices. */
997
+ email?: string;
998
+ /** Path to the certificate (PEM) when `provider: 'custom'`. */
999
+ certPath?: string;
1000
+ /** Path to the private key (PEM) when `provider: 'custom'`. */
1001
+ keyPath?: string;
1002
+ }
1003
+ /**
1004
+ * A Laravel queue worker (or Horizon supervisor) run as a systemd service.
1005
+ * Mirrors Forge's queue configuration. See {@link SiteConfig.queues}.
1006
+ */
1007
+ export interface QueueWorkerConfig {
1008
+ /**
1009
+ * Use `php artisan horizon` instead of `queue:work`. When true, connection /
1010
+ * queue / worker tuning is taken from the app's `config/horizon.php`.
1011
+ * @default false
1012
+ */
1013
+ horizon?: boolean;
1014
+ /** Queue connection (`php artisan queue:work <connection>`). @default 'default' */
1015
+ connection?: string;
1016
+ /** Comma-separated queues to consume, highest priority first. @default 'default' */
1017
+ queue?: string;
1018
+ /** Number of worker processes to run in parallel. @default 1 */
1019
+ processes?: number;
1020
+ /** `--timeout`: seconds a child job may run before being killed. @default 60 */
1021
+ timeout?: number;
1022
+ /** `--sleep`: seconds to wait when no job is available. @default 3 */
1023
+ sleep?: number;
1024
+ /** `--tries`: attempts before a job is marked failed. @default 3 */
1025
+ tries?: number;
1026
+ /** `--max-jobs`: restart the worker after N jobs (0 = unlimited). */
1027
+ maxJobs?: number;
1028
+ /** `--max-time`: restart the worker after N seconds (0 = unlimited). */
1029
+ maxTime?: number;
1030
+ /** `--memory`: restart the worker when it exceeds N MB. @default 128 */
1031
+ memory?: number;
1032
+ /** Seconds to wait for in-flight jobs to finish on stop/restart. @default 90 */
1033
+ stopWaitSecs?: number;
1034
+ }
1035
+ /**
1036
+ * A generic long-running process kept alive by systemd. Mirrors Forge daemons.
1037
+ * See {@link SiteConfig.daemons}.
1038
+ */
1039
+ export interface DaemonConfig {
1040
+ /** Command to run (becomes systemd `ExecStart`). */
1041
+ command: string;
1042
+ /** Working directory. Defaults to the site's `current` release directory. */
1043
+ directory?: string;
1044
+ /** User to run as. Defaults to the deploy user. */
1045
+ user?: string;
1046
+ /** Number of identical processes to run. @default 1 */
1047
+ processes?: number;
1048
+ /** Restart policy. @default 'always' */
1049
+ restart?: 'always' | 'on-failure' | 'no';
1050
+ /** Optional explicit unit name; defaults to a slug of the command. */
1051
+ name?: string;
1052
+ }
1053
+ /** A lifecycle event that can trigger a notification. */
1054
+ export type NotifyEvent = 'deploy' | 'deploy-failed' | 'ssl' | 'health' | 'backup';
1055
+ /**
1056
+ * Notification channels (Forge-style). Configure any subset; each configured
1057
+ * channel receives the events listed in {@link events} (all events by default).
1058
+ */
1059
+ export interface NotificationsConfig {
1060
+ /** Slack incoming-webhook URL. */
1061
+ slack?: {
1062
+ webhookUrl: string;
1063
+ };
1064
+ /** Discord webhook URL. */
1065
+ discord?: {
1066
+ webhookUrl: string;
1067
+ };
1068
+ /** Telegram bot token + chat id. */
1069
+ telegram?: {
1070
+ botToken: string;
1071
+ chatId: string;
1072
+ };
1073
+ /** Email recipients (sent via ts-cloud's email/SES client). */
1074
+ email?: {
1075
+ to: string | string[];
1076
+ from?: string;
1077
+ };
1078
+ /** Generic webhook — receives `{ event, message }` as JSON. */
1079
+ webhook?: {
1080
+ url: string;
1081
+ method?: 'POST' | 'GET';
1082
+ };
1083
+ /**
1084
+ * Which events to notify on. @default all events
1085
+ */
1086
+ events?: NotifyEvent[];
827
1087
  }
828
1088
  export interface VpcConfig {
829
1089
  cidr?: string;
@@ -844,8 +1104,18 @@ export interface BucketConfig {
844
1104
  }
845
1105
  export interface DatabaseConfig {
846
1106
  type?: 'rds' | 'dynamodb';
847
- engine?: 'postgres' | 'mysql';
1107
+ engine?: 'postgres' | 'mysql' | 'mariadb';
848
1108
  instanceType?: string;
1109
+ /** Database/schema name to create (e.g. `forge`). */
1110
+ name?: string;
1111
+ /** Application database user to create. */
1112
+ username?: string;
1113
+ /** Password for {@link username}. */
1114
+ password?: string;
1115
+ /** Hostname for a managed/external database (default `127.0.0.1` on-box). */
1116
+ host?: string;
1117
+ /** Port (defaults: mysql/mariadb 3306, postgres 5432). */
1118
+ port?: number;
849
1119
  }
850
1120
  export interface CacheConfig {
851
1121
  type?: 'redis' | 'memcached';
@@ -1316,6 +1586,23 @@ export interface ComputeConfig {
1316
1586
  * @default 'micro'
1317
1587
  */
1318
1588
  size?: InstanceSize;
1589
+ /**
1590
+ * Number of application servers (Forge load-balanced fleet). When > 1, a load
1591
+ * balancer is provisioned in front and a private network connects the fleet;
1592
+ * the app is deployed to every app server. Pair with {@link servicesServer}
1593
+ * so the database/cache/search live on one shared box. @default 1
1594
+ */
1595
+ appServers?: number;
1596
+ /**
1597
+ * Provision a **dedicated services server** (its own box) running the
1598
+ * configured {@link managedServices} (MySQL/Redis/Meilisearch), instead of
1599
+ * co-locating them on the app server(s). App servers then point their `.env`
1600
+ * at this box over the private network. Required for a multi-app fleet so all
1601
+ * app servers share one database/cache. `true` uses the default size.
1602
+ */
1603
+ servicesServer?: boolean | {
1604
+ size?: InstanceSize;
1605
+ };
1319
1606
  /**
1320
1607
  * Mixed instance fleet for cost optimization
1321
1608
  * Allows combining different sizes and spot instances
@@ -1329,10 +1616,20 @@ export interface ComputeConfig {
1329
1616
  */
1330
1617
  fleet?: InstanceConfig[];
1331
1618
  /**
1332
- * Custom machine image (optional)
1333
- * If not specified, uses the provider's default Linux image
1619
+ * Custom machine image (optional). For the Forge path this is a ts-cloud
1620
+ * **golden image** (a Hetzner snapshot / AWS AMI baked with the full stack
1621
+ * nginx, php-fpm, Composer, services). If not specified, the provider's
1622
+ * default Ubuntu image is used and the stack is installed at first boot.
1623
+ * @see bakedImage
1334
1624
  */
1335
1625
  image?: string;
1626
+ /**
1627
+ * The configured {@link image} is a pre-provisioned golden image that already
1628
+ * has the runtime + PHP + services + base packages installed. Boot skips the
1629
+ * install-heavy provisioning for a near-instant start. Build + publish the
1630
+ * image with the bake recipe (see scripts/build-image.ts). @default false
1631
+ */
1632
+ bakedImage?: boolean;
1336
1633
  /**
1337
1634
  * CloudFront custom origin for the registry/app server (when the site stack
1338
1635
  * fronts EC2 instead of S3-only). Use the EC2 public DNS name, not a raw IP.
@@ -1349,6 +1646,12 @@ export interface ComputeConfig {
1349
1646
  instanceType?: string;
1350
1647
  ami?: string;
1351
1648
  keyPair?: string;
1649
+ /**
1650
+ * IAM instance profile name attached at launch. For the lightweight EC2
1651
+ * boot path, this should grant `AmazonSSMManagedInstanceCore` so deploys
1652
+ * (SSM Run Command) reach the box.
1653
+ */
1654
+ iamInstanceProfile?: string;
1352
1655
  autoScaling?: {
1353
1656
  min?: number;
1354
1657
  max?: number;
@@ -1515,9 +1818,10 @@ export interface ComputeConfig {
1515
1818
  };
1516
1819
  /**
1517
1820
  * Application runtime to install on the instance.
1518
- * Shared by every site that gets deployed to this compute.
1821
+ * Shared by every site that gets deployed to this compute. `'php'` provisions
1822
+ * nginx + php-fpm + Composer (see {@link php}) for Laravel/PHP sites.
1519
1823
  */
1520
- runtime?: 'bun' | 'node' | 'deno';
1824
+ runtime?: 'bun' | 'node' | 'deno' | 'php';
1521
1825
  /**
1522
1826
  * Pinned runtime version (e.g. '1.3.13'). Defaults to 'latest'.
1523
1827
  */
@@ -1552,6 +1856,126 @@ export interface ComputeConfig {
1552
1856
  * static dirs — so an app, docs, and a public site can share one domain.
1553
1857
  */
1554
1858
  proxy?: ComputeProxyConfig;
1859
+ /**
1860
+ * PHP-FPM provisioning. When set (or `runtime: 'php'`), the box installs the
1861
+ * requested PHP versions (via `ppa:ondrej/php`), Composer, and the common
1862
+ * Laravel extension set. Each site picks its version with `SiteConfig.phpVersion`.
1863
+ */
1864
+ php?: ComputePhpConfig;
1865
+ /**
1866
+ * Web server that fronts the box.
1867
+ * - `'nginx'` (default) — per-site nginx vhost + php-fpm, Let's Encrypt via certbot.
1868
+ * - `'rpx'` — the existing `@stacksjs/rpx` gateway with on-demand TLS.
1869
+ * Independent of {@link proxy}, which only configures the rpx engine details.
1870
+ * @default 'nginx'
1871
+ */
1872
+ webServer?: 'nginx' | 'rpx';
1873
+ /**
1874
+ * On-box managed services to install (Forge's single-server model): the
1875
+ * database engine, cache, and search. Each may be `true` for defaults or an
1876
+ * object for pinning a version. Omit to install nothing (e.g. when pointing
1877
+ * the app at a managed/RDS database instead).
1878
+ *
1879
+ * Named `managedServices` to avoid colliding with the ECS microservices
1880
+ * `services` array above.
1881
+ */
1882
+ managedServices?: ComputeServicesConfig;
1883
+ /**
1884
+ * Host firewall (UFW). When enabled, only SSH + the listed ports are open.
1885
+ * On Hetzner this complements the cloud firewall; on a bare box it's the
1886
+ * primary firewall. @default { enabled: true } for PHP boxes
1887
+ */
1888
+ firewall?: ComputeFirewallConfig;
1889
+ /**
1890
+ * Automatic unattended security/system updates (Forge's "maintenance"). When
1891
+ * enabled, installs `unattended-upgrades` and enables daily auto-updates.
1892
+ * @default true for PHP boxes
1893
+ */
1894
+ autoUpdates?: boolean;
1895
+ /**
1896
+ * Scheduled database backups (powered by `ts-backups`), synced to object
1897
+ * storage. Off unless configured.
1898
+ */
1899
+ backups?: ComputeBackupConfig;
1900
+ /**
1901
+ * Operator SSH keys authorized on the box, in addition to the deploy key.
1902
+ * Managed declaratively: keys are written to `authorized_keys` inside a
1903
+ * ts-cloud-managed block on every provision/deploy, so adding one is as
1904
+ * simple as adding an entry here and redeploying.
1905
+ */
1906
+ sshKeys?: SshKeyConfig[];
1907
+ }
1908
+ /** An operator SSH key authorized on the box. See {@link ComputeConfig.sshKeys}. */
1909
+ export interface SshKeyConfig {
1910
+ /** Human label for the key (comment). */
1911
+ name: string;
1912
+ /** The public key line (e.g. `ssh-ed25519 AAAA… user@host`). */
1913
+ publicKey: string;
1914
+ }
1915
+ /** Host firewall (UFW) configuration. See {@link ComputeConfig.firewall}. */
1916
+ export interface ComputeFirewallConfig {
1917
+ /** Enable UFW. @default true */
1918
+ enabled?: boolean;
1919
+ /** TCP ports to allow in addition to SSH/80/443 (always allowed). */
1920
+ allowedPorts?: number[];
1921
+ }
1922
+ /** Scheduled database backup configuration. See {@link ComputeConfig.backups}. */
1923
+ export interface ComputeBackupConfig {
1924
+ /** Enable scheduled backups. @default false */
1925
+ enabled?: boolean;
1926
+ /** Cron schedule for the backup run. @default '0 2 * * *' (daily 02:00) */
1927
+ schedule?: string;
1928
+ /** Keep the newest N backups locally. @default 5 */
1929
+ retentionCount?: number;
1930
+ /** Delete local backups older than N days. @default 30 */
1931
+ retentionDays?: number;
1932
+ /** Object-storage bucket (S3 or Hetzner) the backups are synced to. */
1933
+ bucket?: string;
1934
+ /** S3-compatible endpoint (e.g. Hetzner object storage). Omit for AWS S3. */
1935
+ endpoint?: string;
1936
+ }
1937
+ /**
1938
+ * PHP-FPM provisioning for a compute box. See {@link ComputeConfig.php}.
1939
+ */
1940
+ export interface ComputePhpConfig {
1941
+ /**
1942
+ * PHP versions to install (e.g. `['8.3', '8.2']`). Each gets its own php-fpm
1943
+ * pool/socket so sites can pin different versions. @default ['8.3']
1944
+ */
1945
+ versions?: string[];
1946
+ /** Default PHP version for sites that don't set `phpVersion`. @default first of `versions` */
1947
+ default?: string;
1948
+ /**
1949
+ * Extra PHP extensions to install beyond the Laravel baseline (mbstring, xml,
1950
+ * curl, mysql, pgsql, redis, gd, bcmath, zip, intl). apt package suffixes,
1951
+ * e.g. `['imagick', 'swoole']`.
1952
+ */
1953
+ extensions?: string[];
1954
+ }
1955
+ /**
1956
+ * On-box managed services (database / cache / search) for a compute box.
1957
+ * Each entry is `true` (install with defaults) or an object pinning a version.
1958
+ * See {@link ComputeConfig.services}.
1959
+ */
1960
+ export interface ComputeServicesConfig {
1961
+ mysql?: boolean | {
1962
+ version?: string;
1963
+ };
1964
+ mariadb?: boolean | {
1965
+ version?: string;
1966
+ };
1967
+ postgres?: boolean | {
1968
+ version?: string;
1969
+ };
1970
+ redis?: boolean | {
1971
+ version?: string;
1972
+ };
1973
+ memcached?: boolean | {
1974
+ version?: string;
1975
+ };
1976
+ meilisearch?: boolean | {
1977
+ version?: string;
1978
+ };
1555
1979
  }
1556
1980
  /**
1557
1981
  * Reverse-proxy gateway provisioning for a compute box. The gateway is
@@ -1583,6 +2007,34 @@ export interface ComputeProxyConfig {
1583
2007
  onDemandTls?: boolean;
1584
2008
  /** Contact email for the ACME account when {@link onDemandTls} is enabled. */
1585
2009
  onDemandTlsEmail?: string;
2010
+ /**
2011
+ * Put a CDN (CloudFront) in front of this self-hosted gateway. A CDN custom
2012
+ * origin can't be a bare IP and can't be one of the public aliases (it would
2013
+ * resolve back to the CDN — an infinite loop), so it needs a dedicated origin
2014
+ * hostname pointing at this box. Requests then flow
2015
+ * `viewer → CDN → originDomain (this box)`.
2016
+ *
2017
+ * When {@link CdnFrontConfig.secret} is set, the CDN injects it as a header
2018
+ * on the origin hop and the gateway rejects any request to the fronted hosts
2019
+ * that lacks it — so the publicly-resolvable origin can't be used to bypass
2020
+ * the CDN (origin lockdown via rpx `createOriginGuard`). Pair with
2021
+ * `buildCloudFrontOriginConfig` for the matching AWS distribution config.
2022
+ */
2023
+ cdn?: CdnFrontConfig;
2024
+ }
2025
+ /** CDN-in-front-of-gateway configuration (see {@link ComputeProxyConfig.cdn}). */
2026
+ export interface CdnFrontConfig {
2027
+ /**
2028
+ * Hostname the CDN connects to (e.g. `origin.example.com`). MUST resolve to
2029
+ * this box and MUST NOT be one of {@link frontedHosts} (else the CDN loops).
2030
+ */
2031
+ originDomain: string;
2032
+ /** Public hosts served through the CDN (its aliases) — locked down when {@link secret} is set. */
2033
+ frontedHosts: string[];
2034
+ /** Shared secret the CDN injects on the origin hop; the gateway enforces it on {@link frontedHosts}. */
2035
+ secret?: string;
2036
+ /** Header name carrying {@link secret}. @default 'X-Origin-Verify' */
2037
+ secretHeader?: string;
1586
2038
  }
1587
2039
  export interface DatabaseItemConfig {
1588
2040
  engine?: 'dynamodb' | 'postgres' | 'mysql';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ts-cloud/core",
3
- "version": "0.2.25",
3
+ "version": "0.2.27",
4
4
  "type": "module",
5
5
  "description": "Core CloudFormation generation library for ts-cloud",
6
6
  "author": "Chris Breuer <chris@stacksjs.com>",
@@ -31,7 +31,7 @@
31
31
  "typecheck": "tsc --noEmit"
32
32
  },
33
33
  "dependencies": {
34
- "@ts-cloud/aws-types": "0.2.25"
34
+ "@ts-cloud/aws-types": "0.2.27"
35
35
  },
36
36
  "devDependencies": {
37
37
  "typescript": "^5.9.3"