@ts-cloud/core 0.4.2 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -29,6 +29,7 @@ export * from './presets/traditional-web-app';
29
29
  export * from './presets/laravel';
30
30
  export * from './presets/serverless-laravel';
31
31
  export * from './presets/dashboard';
32
+ export * from './presets/management-dashboard';
32
33
  export * from './presets/extend';
33
34
  export * from './aws/signature';
34
35
  export * from './aws/credentials';
package/dist/index.js CHANGED
@@ -22307,6 +22307,57 @@ function createDashboardSite(options) {
22307
22307
  }
22308
22308
  };
22309
22309
  }
22310
+ // src/presets/management-dashboard.ts
22311
+ function apexOf(domain) {
22312
+ const parts = domain.split(".").filter(Boolean);
22313
+ return parts.length <= 2 ? domain : parts.slice(-2).join(".");
22314
+ }
22315
+ function resolveDashboardDomain(config, environment, explicit) {
22316
+ if (explicit)
22317
+ return explicit;
22318
+ const candidates = [];
22319
+ for (const site of Object.values(config.sites ?? {})) {
22320
+ const d = site?.domain;
22321
+ if (typeof d === "string")
22322
+ candidates.push(d);
22323
+ else if (Array.isArray(d))
22324
+ candidates.push(...d.filter((x) => typeof x === "string"));
22325
+ }
22326
+ if (environment && config.environments?.[environment]?.domain)
22327
+ candidates.push(config.environments[environment].domain);
22328
+ const dnsDomain = config.infrastructure?.dns?.domain;
22329
+ if (dnsDomain)
22330
+ candidates.push(dnsDomain);
22331
+ const base = candidates.find((d) => d && !d.startsWith("dashboard."));
22332
+ if (!base)
22333
+ return null;
22334
+ return `dashboard.${apexOf(base)}`;
22335
+ }
22336
+ function hasManagementDashboardSite(config) {
22337
+ return Object.entries(config.sites ?? {}).some(([name, site]) => {
22338
+ if (!site)
22339
+ return false;
22340
+ const root = site.root ?? "";
22341
+ return name === "dashboard" || root === "ui/dist" || root.endsWith("/ui/dist") || root.endsWith("/dist/ui");
22342
+ });
22343
+ }
22344
+ function resolveManagementDashboardSite(config, environment, opts) {
22345
+ if (hasManagementDashboardSite(config))
22346
+ return null;
22347
+ const domain = resolveDashboardDomain(config, environment, opts.domain);
22348
+ if (!domain)
22349
+ return null;
22350
+ const site = {
22351
+ root: opts.uiRoot,
22352
+ deploy: "server",
22353
+ type: "static",
22354
+ domain,
22355
+ ssl: { provider: "letsencrypt" },
22356
+ ...opts.build === false || opts.build === undefined ? {} : { build: opts.build },
22357
+ ...opts.password ? { auth: { username: opts.username || "admin", password: opts.password, realm: opts.realm } } : {}
22358
+ };
22359
+ return { name: "dashboard", site };
22360
+ }
22310
22361
  // src/presets/extend.ts
22311
22362
  function deepMerge(target, source) {
22312
22363
  const result = { ...target };
@@ -45466,6 +45517,7 @@ function composeServerlessAppTemplate(opts) {
45466
45517
  TSCLOUD_LAMBDA_MODE: mode,
45467
45518
  TSCLOUD_ENV: environment,
45468
45519
  ...app.octane ? { TSCLOUD_OCTANE: "1" } : {},
45520
+ ...app.scheduler === "sub-minute" ? { TSCLOUD_SCHEDULER: "sub-minute" } : {},
45469
45521
  ...cacheEnabled ? { TSCLOUD_CACHE_TABLE: `${slug}-${environment}-cache` } : {},
45470
45522
  ...hasQueue ? { TSCLOUD_QUEUE: queueNames[0] } : {},
45471
45523
  ...app.env ?? {}
@@ -45485,7 +45537,7 @@ function composeServerlessAppTemplate(opts) {
45485
45537
  ]
45486
45538
  }
45487
45539
  } : {};
45488
- function addFunction(logicalId, name, handler8, mode, memory, timeout, reservedConcurrency) {
45540
+ function addFunction(logicalId, name, handler8, mode, memory, timeout, reservedConcurrency, tmp = tmpStorage) {
45489
45541
  resources[`${logicalId}LogGroup`] = {
45490
45542
  Type: "AWS::Logs::LogGroup",
45491
45543
  Properties: { LogGroupName: `/aws/lambda/${name}`, RetentionInDays: 14 }
@@ -45510,17 +45562,17 @@ function composeServerlessAppTemplate(opts) {
45510
45562
  Timeout: timeout,
45511
45563
  Role: Fn2.getAtt("AppRole", "Arn"),
45512
45564
  Environment: { Variables: baseEnv(mode) },
45513
- EphemeralStorage: { Size: tmpStorage },
45565
+ EphemeralStorage: { Size: tmp },
45514
45566
  ...codeProps,
45515
45567
  ...reservedConcurrency !== undefined ? { ReservedConcurrentExecutions: reservedConcurrency } : {},
45516
45568
  ...vpcConfig
45517
45569
  }
45518
45570
  };
45519
45571
  }
45520
- addFunction("HttpFunction", functionNames.http, handlers.http, "http", app.memory ?? 1024, app.timeout ?? 28, app.concurrency);
45521
- addFunction("CliFunction", functionNames.cli, handlers.cli, "cli", app.cliMemory ?? 1024, app.cliTimeout ?? 900);
45572
+ addFunction("HttpFunction", functionNames.http, handlers.http, "http", app.memory ?? 1024, app.timeout ?? 28, app.concurrency, tmpStorage);
45573
+ addFunction("CliFunction", functionNames.cli, handlers.cli, "cli", app.cliMemory ?? 1024, app.cliTimeout ?? 900, undefined, app.cliTmpStorage ?? tmpStorage);
45522
45574
  if (hasQueue)
45523
- addFunction("QueueFunction", functionNames.queue, handlers.queue, "queue", app.queueMemory ?? 1024, app.queueTimeout ?? 120);
45575
+ addFunction("QueueFunction", functionNames.queue, handlers.queue, "queue", app.queueMemory ?? 1024, app.queueTimeout ?? 120, undefined, app.queueTmpStorage ?? tmpStorage);
45524
45576
  resources.HttpApi = {
45525
45577
  Type: "AWS::ApiGatewayV2::Api",
45526
45578
  Properties: {
@@ -45567,6 +45619,59 @@ function composeServerlessAppTemplate(opts) {
45567
45619
  Value: Fn2.getAtt("HttpApi", "ApiEndpoint")
45568
45620
  };
45569
45621
  outputs.HttpApiId = { Description: "HTTP API id", Value: Fn2.ref("HttpApi") };
45622
+ const domains = (Array.isArray(app.domain) ? app.domain : app.domain ? [app.domain] : []).filter(Boolean);
45623
+ if (domains.length) {
45624
+ if (!app.certificateArn && !app.hostedZoneId) {
45625
+ throw new Error("serverless app: a custom `domain` needs either `certificateArn` (pre-issued, regional) or `hostedZoneId` (to auto-issue + validate an ACM cert).");
45626
+ }
45627
+ let certRef = app.certificateArn;
45628
+ if (!certRef) {
45629
+ resources.HttpCertificate = {
45630
+ Type: "AWS::CertificateManager::Certificate",
45631
+ Properties: {
45632
+ DomainName: domains[0],
45633
+ ...domains.length > 1 ? { SubjectAlternativeNames: domains.slice(1) } : {},
45634
+ ValidationMethod: "DNS",
45635
+ DomainValidationOptions: domains.map((d) => ({ DomainName: d, HostedZoneId: app.hostedZoneId }))
45636
+ }
45637
+ };
45638
+ certRef = Fn2.ref("HttpCertificate");
45639
+ }
45640
+ domains.forEach((d, i) => {
45641
+ const dn = `HttpDomain${i}`;
45642
+ resources[dn] = {
45643
+ Type: "AWS::ApiGatewayV2::DomainName",
45644
+ Properties: {
45645
+ DomainName: d,
45646
+ DomainNameConfigurations: [{ CertificateArn: certRef, EndpointType: "REGIONAL" }]
45647
+ }
45648
+ };
45649
+ resources[`HttpApiMapping${i}`] = {
45650
+ Type: "AWS::ApiGatewayV2::ApiMapping",
45651
+ DependsOn: ["HttpStage"],
45652
+ Properties: { ApiId: Fn2.ref("HttpApi"), DomainName: Fn2.ref(dn), Stage: "$default" }
45653
+ };
45654
+ if (app.hostedZoneId) {
45655
+ resources[`HttpDomainRecord${i}`] = {
45656
+ Type: "AWS::Route53::RecordSet",
45657
+ Properties: {
45658
+ HostedZoneId: app.hostedZoneId,
45659
+ Name: d,
45660
+ Type: "A",
45661
+ AliasTarget: {
45662
+ DNSName: Fn2.getAtt(dn, "RegionalDomainName"),
45663
+ HostedZoneId: Fn2.getAtt(dn, "RegionalHostedZoneId")
45664
+ }
45665
+ }
45666
+ };
45667
+ }
45668
+ outputs[`CustomDomain${i}`] = { Description: `Custom domain ${d}`, Value: d };
45669
+ outputs[`CustomDomainTarget${i}`] = {
45670
+ Description: `Point ${d} (CNAME/alias) at this APIGW regional domain`,
45671
+ Value: Fn2.getAtt(dn, "RegionalDomainName")
45672
+ };
45673
+ });
45674
+ }
45570
45675
  if (hasQueue) {
45571
45676
  resources.AppQueueDlq = {
45572
45677
  Type: "AWS::SQS::Queue",
@@ -46910,7 +47015,9 @@ export {
46910
47015
  resolveRegion,
46911
47016
  resolveQueueNames,
46912
47017
  resolveProjectStackName,
47018
+ resolveManagementDashboardSite,
46913
47019
  resolveDeployBucketName,
47020
+ resolveDashboardDomain,
46914
47021
  resolveCredentials,
46915
47022
  resolveCloudProvider,
46916
47023
  resolveApp,
@@ -46964,6 +47071,7 @@ export {
46964
47071
  hashFile,
46965
47072
  hashDirectory,
46966
47073
  hashBuffer,
47074
+ hasManagementDashboardSite,
46967
47075
  guardDutyManager,
46968
47076
  globalResourceManager,
46969
47077
  getTimestamp,
@@ -0,0 +1,44 @@
1
+ import type { CloudConfig, EnvironmentType, SiteConfig } from '../types';
2
+ /**
3
+ * Auto-deployed management dashboard (the `@ts-cloud/ui` stx app — the Server +
4
+ * Serverless views). When a server is provisioned, ts-cloud injects this as a
5
+ * server-static site so the dashboard ships automatically with every box.
6
+ *
7
+ * It is served behind HTTP Basic auth (htpasswd) whose password comes from an
8
+ * environment value (`TS_CLOUD_UI_PASSWORD`). If that value is NOT set, the
9
+ * dashboard is served WITHOUT auth — no password is invented.
10
+ */
11
+ export interface ManagementDashboardOptions {
12
+ /** Directory shipped as the static site root (built UI, or source dir + build). */
13
+ uiRoot: string;
14
+ /** Build command producing {@link uiRoot}, or false when it is already built. */
15
+ build?: string | false;
16
+ /** Explicit domain (e.g. from `TS_CLOUD_UI_DOMAIN`); else derived. */
17
+ domain?: string;
18
+ /** Basic-auth username. @default 'admin' */
19
+ username?: string;
20
+ /**
21
+ * Basic-auth password. When empty/undefined the dashboard is served WITHOUT
22
+ * htpasswd (no default password is invented).
23
+ */
24
+ password?: string;
25
+ /** Browser auth realm. */
26
+ realm?: string;
27
+ }
28
+ /**
29
+ * Derive a dashboard hostname (`dashboard.<apex>`) from the project's configured
30
+ * domains. Prefers `explicit`, then any site domain, then the environment domain,
31
+ * then `infrastructure.dns.domain`. Returns null when nothing is available.
32
+ */
33
+ export declare function resolveDashboardDomain(config: Pick<CloudConfig, 'sites' | 'environments' | 'infrastructure'>, environment?: EnvironmentType, explicit?: string): string | null;
34
+ /** Does the config already define the management dashboard as a site? */
35
+ export declare function hasManagementDashboardSite(config: Pick<CloudConfig, 'sites'>): boolean;
36
+ /**
37
+ * Build the management-dashboard site to auto-inject on server deploys, or null
38
+ * when no domain can be resolved (the static-site model is domain-routed) or it
39
+ * is already configured.
40
+ */
41
+ export declare function resolveManagementDashboardSite(config: Pick<CloudConfig, 'sites' | 'environments' | 'infrastructure'>, environment: EnvironmentType, opts: ManagementDashboardOptions): {
42
+ name: string;
43
+ site: SiteConfig;
44
+ } | null;
@@ -0,0 +1 @@
1
+ export {};
package/dist/types.d.ts CHANGED
@@ -949,6 +949,23 @@ export interface SiteConfig {
949
949
  * process.env.UI_PASSWORD }`. The htpasswd file is generated on the box.
950
950
  */
951
951
  auth?: SiteAuthConfig;
952
+ /**
953
+ * Package-registry credentials written into the release before
954
+ * `composer install` / `npm install` so private packages resolve (Forge's
955
+ * Composer/npm credentials feature).
956
+ */
957
+ credentials?: SiteCredentialsConfig;
958
+ }
959
+ /** Private package-registry credentials for a site's build. */
960
+ export interface SiteCredentialsConfig {
961
+ /**
962
+ * Composer `auth.json` contents — an object (serialized to JSON) or a
963
+ * ready-made JSON string. Written to the release root before `composer
964
+ * install` (e.g. `{ 'github-oauth': { 'github.com': '<token>' } }`).
965
+ */
966
+ composerAuth?: Record<string, unknown> | string;
967
+ /** `.npmrc` contents written to the release root before `npm install`. */
968
+ npmrc?: string;
952
969
  }
953
970
  /** HTTP Basic auth for a site's nginx vhost. See {@link SiteConfig.auth}. */
954
971
  export interface SiteAuthConfig {
@@ -1006,6 +1023,32 @@ export interface SiteSslConfig {
1006
1023
  certPath?: string;
1007
1024
  /** Path to the private key (PEM) when `provider: 'custom'`. */
1008
1025
  keyPath?: string;
1026
+ /**
1027
+ * Issue a **wildcard** certificate (`*.<domain>` + `<domain>`). Requires
1028
+ * DNS-01 validation, so {@link dns} must be set. The nginx plugin can't do
1029
+ * wildcards (that needs HTTP-01 per host).
1030
+ */
1031
+ wildcard?: boolean;
1032
+ /**
1033
+ * Use DNS-01 validation via a certbot DNS plugin instead of the nginx
1034
+ * (HTTP-01) challenge. Needed for wildcard certs and for issuing before the
1035
+ * domain resolves to the box. The plugin + credentials are wired on the box.
1036
+ */
1037
+ dns?: SslDnsConfig;
1038
+ }
1039
+ /** certbot DNS-01 plugin configuration for DNS-validated / wildcard certs. */
1040
+ export interface SslDnsConfig {
1041
+ /** DNS provider whose certbot plugin handles the `_acme-challenge` records. */
1042
+ provider: 'cloudflare' | 'route53' | 'digitalocean' | 'google';
1043
+ /**
1044
+ * Provider credentials written to a root-only INI certbot reads
1045
+ * (`--dns-<provider>-credentials`). For route53, AWS env/instance-role creds
1046
+ * are used instead, so this may be omitted. Keys are provider-specific, e.g.
1047
+ * `{ dns_cloudflare_api_token: '…' }`.
1048
+ */
1049
+ credentials?: Record<string, string>;
1050
+ /** Seconds to wait for DNS propagation before certbot asks the CA to verify. */
1051
+ propagationSeconds?: number;
1009
1052
  }
1010
1053
  /**
1011
1054
  * A Laravel queue worker (or Horizon supervisor) run as a systemd service.
@@ -1123,6 +1166,34 @@ export interface DatabaseConfig {
1123
1166
  host?: string;
1124
1167
  /** Port (defaults: mysql/mariadb 3306, postgres 5432). */
1125
1168
  port?: number;
1169
+ /**
1170
+ * Additional database users to create beyond the app {@link username}
1171
+ * (the Forge Database Users feature). Each can be granted full or read-only
1172
+ * access to one or more databases. Created at provision time on the on-box
1173
+ * engine.
1174
+ */
1175
+ users?: DatabaseUserConfig[];
1176
+ }
1177
+ /**
1178
+ * An extra database user provisioned on the on-box engine (per-user grants).
1179
+ * Beyond the application user, you can create reporting/read-only accounts or
1180
+ * service-specific logins with their own access scope.
1181
+ */
1182
+ export interface DatabaseUserConfig {
1183
+ /** User name to create. */
1184
+ username: string;
1185
+ /** Password for the user. */
1186
+ password: string;
1187
+ /**
1188
+ * Databases this user may access. Defaults to the app database
1189
+ * ({@link DatabaseConfig.name}) when omitted.
1190
+ */
1191
+ databases?: string[];
1192
+ /**
1193
+ * Access level granted on {@link databases}. `all` (default) is full
1194
+ * read/write; `readonly` grants SELECT only (plus connect on Postgres).
1195
+ */
1196
+ access?: 'all' | 'readonly';
1126
1197
  }
1127
1198
  export interface CacheConfig {
1128
1199
  type?: 'redis' | 'memcached';
@@ -1530,8 +1601,12 @@ export interface ServerlessAppConfig {
1530
1601
  rdsProxy?: boolean | {
1531
1602
  name?: string;
1532
1603
  };
1533
- /** Ephemeral `/tmp` size in MB (51210240). @default 512 */
1604
+ /** Ephemeral storage in MB (512 to 10240) for the HTTP function. @default 512 */
1534
1605
  tmpStorage?: number;
1606
+ /** Ephemeral storage in MB for the CLI function. @default tmpStorage */
1607
+ cliTmpStorage?: number;
1608
+ /** Ephemeral storage in MB for the queue function. @default tmpStorage */
1609
+ queueTmpStorage?: number;
1535
1610
  /** Database attachment. */
1536
1611
  database?: {
1537
1612
  connection?: 'rds-proxy' | 'aurora-serverless' | 'rds';
@@ -1548,10 +1623,17 @@ export interface ServerlessAppConfig {
1548
1623
  };
1549
1624
  /** Managed WAF in front of the HTTP API / CloudFront. */
1550
1625
  firewall?: WafConfig;
1551
- /** Custom domain for the app (overrides {@link EnvironmentConfig.domain}). */
1626
+ /** Custom domain(s) for the app's HTTP API (overrides {@link EnvironmentConfig.domain}). */
1552
1627
  domain?: string | string[];
1553
- /** Pre-issued ACM certificate ARN for the custom domain. */
1628
+ /** Pre-issued ACM certificate ARN for the custom domain (regional, same region). */
1554
1629
  certificateArn?: string;
1630
+ /**
1631
+ * Route53 hosted zone ID for the custom domain. When set (and no
1632
+ * `certificateArn`), ts-cloud issues + DNS-validates an ACM cert and creates
1633
+ * the alias record automatically. Without it, supply `certificateArn` and
1634
+ * point your DNS at the API's regional domain (emitted as a stack output).
1635
+ */
1636
+ hostedZoneId?: string;
1555
1637
  /** Local directory whose contents are uploaded to S3/CloudFront as versioned assets. */
1556
1638
  assets?: string;
1557
1639
  /** PHP version for the runtime layer. @default '8.3' */
@@ -3191,7 +3273,7 @@ export interface RealtimeHooksConfig {
3191
3273
  * })
3192
3274
  *
3193
3275
  * // Private channel
3194
- * Echo.private(`user.${userId}`).listen('notification', (e) => {
3276
+ * Echo.private('user.' + userId).listen('notification', (e) => {
3195
3277
  * console.log('Private notification:', e)
3196
3278
  * })
3197
3279
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ts-cloud/core",
3
- "version": "0.4.2",
3
+ "version": "0.5.1",
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.4.2"
34
+ "@ts-cloud/aws-types": "0.5.1"
35
35
  },
36
36
  "devDependencies": {
37
37
  "typescript": "^5.9.3"