@stacksjs/ts-cloud 0.4.1 → 0.5.0

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.
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Auto-deploy of the ts-cloud management dashboard (the `@ts-cloud/ui` stx app)
3
+ * on every server provision/deploy.
4
+ *
5
+ * Resolves the UI directory (the repo's local `ui/`, else the prebuilt UI that
6
+ * ships inside the installed package at `dist/ui`), derives a `dashboard.<apex>`
7
+ * host, and injects it into `config.sites` as a server-static site. It is served
8
+ * behind htpasswd ONLY when `TS_CLOUD_UI_PASSWORD` is set; when it is not, the
9
+ * dashboard is served without auth (no password is invented).
10
+ *
11
+ * Env:
12
+ * - `TS_CLOUD_UI_PASSWORD` htpasswd password (unset ⇒ no auth)
13
+ * - `TS_CLOUD_UI_USERNAME` htpasswd user (default `admin`)
14
+ * - `TS_CLOUD_UI_DOMAIN` explicit dashboard host (else `dashboard.<apex>`)
15
+ * - `TS_CLOUD_UI_REALM` browser auth realm
16
+ * - `TS_CLOUD_UI_DISABLE` set truthy to skip auto-deploy
17
+ */
18
+ import type { CloudConfig } from '@stacksjs/ts-cloud';
19
+ export interface EnsureDashboardLogger {
20
+ info: (msg: string) => void;
21
+ warn: (msg: string) => void;
22
+ }
23
+ /**
24
+ * Resolve the UI source to ship. Prefers the repo's local `ui/` (built on the
25
+ * deploy machine), then the prebuilt UI bundled in the installed package.
26
+ * Returns `{ uiRoot, build }` or null when no UI is available.
27
+ */
28
+ export declare function resolveUiSource(cwd: string): {
29
+ uiRoot: string;
30
+ build: string | false;
31
+ } | null;
32
+ /**
33
+ * Inject the management dashboard into `config.sites` for a server deploy.
34
+ * Mutates and returns `config`. Idempotent and safe to call on every deploy.
35
+ */
36
+ export declare function ensureManagementDashboard(config: CloudConfig, options?: {
37
+ cwd?: string;
38
+ logger?: EnsureDashboardLogger;
39
+ }): CloudConfig;
package/dist/index.js CHANGED
@@ -48,7 +48,7 @@ import { readFileSync as readFileSync5 } from "node:fs";
48
48
  import { dirname as dirname42, join as join8 } from "node:path";
49
49
  import { fileURLToPath as fileURLToPath3 } from "node:url";
50
50
  import { execFileSync as execFileSync2 } from "node:child_process";
51
- import { existsSync as existsSync52, mkdtempSync as mkdtempSync3, readdirSync as readdirSync52, readFileSync as readFileSync6, rmSync as rmSync3, statSync as statSync32, writeFileSync as writeFileSync52 } from "node:fs";
51
+ import { existsSync as existsSync52, mkdirSync as mkdirSync22, mkdtempSync as mkdtempSync3, readdirSync as readdirSync52, readFileSync as readFileSync6, rmSync as rmSync3, statSync as statSync32, writeFileSync as writeFileSync52 } from "node:fs";
52
52
  import { tmpdir as tmpdir3 } from "node:os";
53
53
  import { join as join9, relative as relative22 } from "node:path";
54
54
  import { execSync as execSync2 } from "node:child_process";
@@ -7654,6 +7654,56 @@ function createDashboardSite(options) {
7654
7654
  }
7655
7655
  };
7656
7656
  }
7657
+ function apexOf(domain) {
7658
+ const parts = domain.split(".").filter(Boolean);
7659
+ return parts.length <= 2 ? domain : parts.slice(-2).join(".");
7660
+ }
7661
+ function resolveDashboardDomain(config6, environment, explicit) {
7662
+ if (explicit)
7663
+ return explicit;
7664
+ const candidates = [];
7665
+ for (const site of Object.values(config6.sites ?? {})) {
7666
+ const d = site?.domain;
7667
+ if (typeof d === "string")
7668
+ candidates.push(d);
7669
+ else if (Array.isArray(d))
7670
+ candidates.push(...d.filter((x) => typeof x === "string"));
7671
+ }
7672
+ if (environment && config6.environments?.[environment]?.domain)
7673
+ candidates.push(config6.environments[environment].domain);
7674
+ const dnsDomain = config6.infrastructure?.dns?.domain;
7675
+ if (dnsDomain)
7676
+ candidates.push(dnsDomain);
7677
+ const base = candidates.find((d) => d && !d.startsWith("dashboard."));
7678
+ if (!base)
7679
+ return null;
7680
+ return `dashboard.${apexOf(base)}`;
7681
+ }
7682
+ function hasManagementDashboardSite(config6) {
7683
+ return Object.entries(config6.sites ?? {}).some(([name, site]) => {
7684
+ if (!site)
7685
+ return false;
7686
+ const root = site.root ?? "";
7687
+ return name === "dashboard" || root === "ui/dist" || root.endsWith("/ui/dist") || root.endsWith("/dist/ui");
7688
+ });
7689
+ }
7690
+ function resolveManagementDashboardSite(config6, environment, opts) {
7691
+ if (hasManagementDashboardSite(config6))
7692
+ return null;
7693
+ const domain = resolveDashboardDomain(config6, environment, opts.domain);
7694
+ if (!domain)
7695
+ return null;
7696
+ const site = {
7697
+ root: opts.uiRoot,
7698
+ deploy: "server",
7699
+ type: "static",
7700
+ domain,
7701
+ ssl: { provider: "letsencrypt" },
7702
+ ...opts.build === false || opts.build === undefined ? {} : { build: opts.build },
7703
+ ...opts.password ? { auth: { username: opts.username || "admin", password: opts.password, realm: opts.realm } } : {}
7704
+ };
7705
+ return { name: "dashboard", site };
7706
+ }
7657
7707
  function deepMerge5(target, source) {
7658
7708
  const result = { ...target };
7659
7709
  for (const key in source) {
@@ -26343,13 +26393,18 @@ function phpLayerBuildStage(phpVersion, asName) {
26343
26393
  const scl = `php${phpVersion.replace(".", "")}`;
26344
26394
  const packages = phpLayerPackages(phpVersion).join(" \\\n ");
26345
26395
  const sclRoot = `/opt/remi/${scl}/root`;
26346
- const from = asName ? `FROM amazonlinux:2023 AS ${asName}` : "FROM amazonlinux:2023";
26396
+ const sclEtc = `/etc/opt/remi/${scl}`;
26397
+ const from = asName ? `FROM almalinux:9 AS ${asName}` : "FROM almalinux:9";
26347
26398
  return `${from}
26348
26399
 
26349
- # Remi provides version-isolated PHP SCL packages for EL9 (AL2023 compatible).
26350
- RUN dnf -y install dnf-plugins-core 'dnf-command(config-manager)' && \\
26400
+ # Build PHP on AlmaLinux 9 (where Remi's version-isolated SCL packages install
26401
+ # cleanly Remi's release RPM rejects Amazon Linux 2023). AlmaLinux 9 shares
26402
+ # glibc 2.34 with the provided.al2023 Lambda runtime, and the relocation step
26403
+ # below bundles every non-glibc shared lib under /opt/php/lib, so the resulting
26404
+ # /opt tree runs on Lambda's provided.al2023.
26405
+ RUN dnf -y install epel-release dnf-plugins-core 'dnf-command(config-manager)' && \\
26406
+ dnf config-manager --set-enabled crb && \\
26351
26407
  dnf -y install https://rpms.remirepo.net/enterprise/remi-release-9.rpm && \\
26352
- dnf -y update && \\
26353
26408
  dnf -y install \\
26354
26409
  ${packages} \\
26355
26410
  findutils tar gzip && \\
@@ -26362,7 +26417,10 @@ RUN set -eux; \\
26362
26417
  cp ${sclRoot}/usr/sbin/php-fpm /opt/php/sbin/php-fpm; \\
26363
26418
  EXT_DIR="$(${sclRoot}/usr/bin/php -r 'echo ini_get("extension_dir");')"; \\
26364
26419
  cp -a "$EXT_DIR"/*.so /opt/php/lib/php/modules/ || true; \\
26365
- cp -a ${sclRoot}/etc/php.d/*.ini /opt/php/etc/php.d/ 2>/dev/null || true; \\
26420
+ cp -a ${sclEtc}/php.d/*.ini /opt/php/etc/php.d/ 2>/dev/null || true; \\
26421
+ # Rewrite any absolute extension/zend_extension paths to bare names so they
26422
+ # resolve against the relocated extension_dir.
26423
+ sed -i -E "s#(zend_)?extension *= *.*/([^/]+\\.so)#\\1extension=\\2#" /opt/php/etc/php.d/*.ini 2>/dev/null || true; \\
26366
26424
  # Copy shared-library dependencies of php, php-fpm, and the extension modules.
26367
26425
  for bin in /opt/php/bin/php /opt/php/sbin/php-fpm /opt/php/lib/php/modules/*.so; do \\
26368
26426
  ldd "$bin" 2>/dev/null | awk '/=>/{print $3}/ld-linux/{print $1}' | sort -u | while read -r lib; do \\
@@ -26370,10 +26428,9 @@ RUN set -eux; \\
26370
26428
  done; \\
26371
26429
  done
26372
26430
 
26373
- # Point PHP at the relocated config + extension dir.
26431
+ # Build the relocated main php.ini: extension_dir + every per-extension directive.
26374
26432
  RUN printf 'extension_dir=/opt/php/lib/php/modules\\n' > /opt/php/etc/php.ini && \\
26375
26433
  cat /opt/php/etc/php.d/*.ini >> /opt/php/etc/php.ini 2>/dev/null || true
26376
- ENV PHP_INI_SCAN_DIR=/opt/php/etc/php.d
26377
26434
 
26378
26435
  # Runtime assets (bootstrap, runtime loops, fpm config) are added by the build
26379
26436
  # orchestrator after the image is produced. Export /opt as the layer payload.
@@ -26685,16 +26742,17 @@ function buildPhpRuntimeLayerZip(options = {}) {
26685
26742
  writeFileSync52(join9(stage, "Dockerfile"), generatePhpLayerDockerfile(options));
26686
26743
  step("Building PHP runtime image (docker)");
26687
26744
  execFileSync2("docker", ["build", "--platform", platform, "-t", imageTag, stage], { stdio: "inherit" });
26688
- step("Extracting /opt from image");
26745
+ step("Extracting /opt/php from image");
26689
26746
  const cid = execFileSync2("docker", ["create", "--platform", platform, imageTag], { encoding: "utf-8" }).trim();
26690
26747
  const optDir = join9(stage, "opt");
26748
+ mkdirSync22(optDir, { recursive: true });
26691
26749
  try {
26692
- execFileSync2("docker", ["cp", `${cid}:/opt/.`, optDir], { stdio: "inherit" });
26750
+ execFileSync2("docker", ["cp", `${cid}:/opt/php`, join9(optDir, "php")], { stdio: "inherit" });
26693
26751
  } finally {
26694
26752
  execFileSync2("docker", ["rm", cid], { stdio: "ignore" });
26695
26753
  }
26696
- if (!existsSync52(optDir))
26697
- throw new Error("layer build produced no /opt directory");
26754
+ if (!existsSync52(join9(optDir, "php", "bin", "php")))
26755
+ throw new Error("layer build produced no /opt/php/bin/php");
26698
26756
  const entries = [];
26699
26757
  for (const file of walk(optDir)) {
26700
26758
  const rel = relative22(optDir, file).replace(/\\/g, "/");
@@ -45734,6 +45792,7 @@ async function sendFallbackSms(to, message) {
45734
45792
  PHP_LAYER_EXTENSIONS = [
45735
45793
  "cli",
45736
45794
  "fpm",
45795
+ "curl",
45737
45796
  "mbstring",
45738
45797
  "xml",
45739
45798
  "pdo",
@@ -83441,15 +83500,18 @@ function buildDatabaseSetupScript(database, services = {}) {
83441
83500
  "TS_CLOUD_PG_EOF"
83442
83501
  ];
83443
83502
  }
83503
+ const sock = useMariadb ? "/var/lib/pantry/mariadb/mariadbd.sock" : "/var/lib/pantry/mysql/mysqld.sock";
83444
83504
  const ident = (v) => v.replace(/`/g, "``");
83445
83505
  const lit = (v) => v.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
83446
83506
  return [
83447
83507
  pantryEnvActivation(),
83448
- "for i in $(seq 1 30); do mysqladmin -h 127.0.0.1 -P 3306 -u root ping 2>/dev/null | grep -q alive && break; sleep 2; done",
83449
- "mysql -h 127.0.0.1 -P 3306 -u root <<'TS_CLOUD_SQL_EOF'",
83508
+ `for i in $(seq 1 30); do mysqladmin --socket=${sock} -u root ping 2>/dev/null | grep -q alive && break; sleep 2; done`,
83509
+ `mysql --socket=${sock} -u root <<'TS_CLOUD_SQL_EOF'`,
83450
83510
  `CREATE DATABASE IF NOT EXISTS \`${ident(name)}\` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;`,
83451
83511
  `CREATE USER IF NOT EXISTS '${lit(user)}'@'%' IDENTIFIED BY '${lit(pass)}';`,
83512
+ `CREATE USER IF NOT EXISTS '${lit(user)}'@'localhost' IDENTIFIED BY '${lit(pass)}';`,
83452
83513
  `GRANT ALL PRIVILEGES ON \`${ident(name)}\`.* TO '${lit(user)}'@'%';`,
83514
+ `GRANT ALL PRIVILEGES ON \`${ident(name)}\`.* TO '${lit(user)}'@'localhost';`,
83453
83515
  "FLUSH PRIVILEGES;",
83454
83516
  "TS_CLOUD_SQL_EOF"
83455
83517
  ];
@@ -86266,9 +86328,11 @@ export {
86266
86328
  resolveQueueNames,
86267
86329
  resolveProjectStackName,
86268
86330
  resolveObjectStorage,
86331
+ resolveManagementDashboardSite,
86269
86332
  resolveHetznerApiToken,
86270
86333
  resolveExecStart,
86271
86334
  resolveDeployBucketName,
86335
+ resolveDashboardDomain,
86272
86336
  resolveCredentials,
86273
86337
  resolveCloudProvider,
86274
86338
  resolveApp,
@@ -86331,6 +86395,7 @@ export {
86331
86395
  hashFile,
86332
86396
  hashDirectory,
86333
86397
  hashBuffer,
86398
+ hasManagementDashboardSite,
86334
86399
  guardDutyManager,
86335
86400
  globalResourceManager,
86336
86401
  getTimestamp,
@@ -0,0 +1,22 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Page Not Found</title>
7
+ <style>
8
+ body { font-family: system-ui, sans-serif; display: flex; align-items: center; justify-content: center; min-height: 100vh; margin: 0; }
9
+ .container { text-align: center; }
10
+ h1 { font-size: 6rem; margin: 0; color: #333; }
11
+ p { color: #666; }
12
+ a { color: #0066cc; }
13
+ </style>
14
+ </head>
15
+ <body>
16
+ <div class="container">
17
+ <h1>404</h1>
18
+ <p>Page not found</p>
19
+ <a href="/">Go home</a>
20
+ </div>
21
+ </body>
22
+ </html>