@stacksjs/ts-cloud 0.7.32 → 0.7.34

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 (36) hide show
  1. package/dist/bin/cli.js +709 -693
  2. package/dist/{chunk-dah449r1.js → chunk-e6xhm3qe.js} +47 -11
  3. package/dist/{chunk-jgenfdz6.js → chunk-jvp3s3na.js} +4 -2
  4. package/dist/deploy/index.js +2 -2
  5. package/dist/drivers/index.js +1 -1
  6. package/dist/drivers/shared/ubuntu-bootstrap.d.ts +10 -0
  7. package/dist/index.js +2 -2
  8. package/dist/ui/index.html +3 -3
  9. package/dist/ui/server/actions.html +23 -6
  10. package/dist/ui/server/backups.html +3 -3
  11. package/dist/ui/server/database.html +3 -3
  12. package/dist/ui/server/deployments.html +3 -3
  13. package/dist/ui/server/firewall.html +3 -3
  14. package/dist/ui/server/logs.html +3 -3
  15. package/dist/ui/server/services.html +3 -3
  16. package/dist/ui/server/sites.html +3 -3
  17. package/dist/ui/server/ssh-keys.html +26 -4
  18. package/dist/ui/server/team.html +26 -4
  19. package/dist/ui/server/terminal.html +3 -3
  20. package/dist/ui/server/workers.html +3 -3
  21. package/dist/ui/serverless/alarms.html +3 -3
  22. package/dist/ui/serverless/assets.html +3 -3
  23. package/dist/ui/serverless/data.html +3 -3
  24. package/dist/ui/serverless/deployments.html +3 -3
  25. package/dist/ui/serverless/functions.html +3 -3
  26. package/dist/ui/serverless/logs.html +3 -3
  27. package/dist/ui/serverless/queues.html +3 -3
  28. package/dist/ui/serverless/scheduler.html +3 -3
  29. package/dist/ui/serverless/secrets.html +3 -3
  30. package/dist/ui/serverless/traces.html +3 -3
  31. package/dist/ui/serverless.html +23 -6
  32. package/dist/ui-src/pages/server/actions.stx +27 -4
  33. package/dist/ui-src/pages/server/ssh-keys.stx +22 -1
  34. package/dist/ui-src/pages/server/team.stx +23 -1
  35. package/dist/ui-src/pages/serverless.stx +27 -4
  36. package/package.json +3 -3
@@ -401,7 +401,14 @@ function vhostBody(options) {
401
401
  return lines;
402
402
  }
403
403
  function buildNginxVhost(options) {
404
- const serverNames = [options.domain, ...options.aliases || []].filter(Boolean).join(" ");
404
+ const hosts = [options.domain, ...options.aliases || []].filter(Boolean);
405
+ for (const host of hosts) {
406
+ if (!/^(?=.{1,253}$)(?:\*\.)?[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)*$/i.test(host.trim()))
407
+ throw new Error(`Refusing to build a vhost: '${host}' is not a valid hostname.`);
408
+ }
409
+ if (!hosts.length)
410
+ throw new Error("Refusing to build a vhost: no server_name (domain) was given.");
411
+ const serverNames = hosts.join(" ");
405
412
  const body = vhostBody(options);
406
413
  if (options.ssl) {
407
414
  const redirect = [
@@ -1004,6 +1011,8 @@ function buildRpxProvisionScript(options) {
1004
1011
  const poolEnv = [`Environment=RPX_UPSTREAM_TIMEOUT=${upstreamTimeout}`];
1005
1012
  if (typeof proxy.maxUpstreamConns === "number")
1006
1013
  poolEnv.push(`Environment=RPX_MAX_UPSTREAM_CONNS=${proxy.maxUpstreamConns}`);
1014
+ const memoryHigh = proxy.memoryHigh ?? "512M";
1015
+ const memoryMax = proxy.memoryMax ?? "768M";
1007
1016
  return [
1008
1017
  "set -euo pipefail",
1009
1018
  `mkdir -p ${RPX_DIR} ${RPX_SITES_DIR} ${certsDir} ${RPX_INSTALL_DIR}`,
@@ -1028,6 +1037,10 @@ function buildRpxProvisionScript(options) {
1028
1037
  `WorkingDirectory=${RPX_INSTALL_DIR}`,
1029
1038
  `Environment=BUN_INSTALL=/root/.bun`,
1030
1039
  ...poolEnv,
1040
+ "MemoryAccounting=true",
1041
+ `MemoryHigh=${memoryHigh}`,
1042
+ `MemoryMax=${memoryMax}`,
1043
+ "OOMPolicy=stop",
1031
1044
  "Restart=always",
1032
1045
  "RestartSec=5",
1033
1046
  "LimitNOFILE=1048576",
@@ -1469,7 +1482,8 @@ function buildUbuntuBootstrapScript(options = {}) {
1469
1482
  servicesProvision,
1470
1483
  caddyfile,
1471
1484
  rpxProvision,
1472
- baked = false
1485
+ baked = false,
1486
+ swapGb = 2
1473
1487
  } = options;
1474
1488
  const packages = new Set(systemPackages);
1475
1489
  if (database === "sqlite")
@@ -1481,6 +1495,25 @@ function buildUbuntuBootstrapScript(options = {}) {
1481
1495
  let script = `#!/bin/bash
1482
1496
  set -euo pipefail
1483
1497
  `;
1498
+ const swapSizeGb = Math.floor(swapGb);
1499
+ if (swapSizeGb > 0) {
1500
+ script += `
1501
+ # Swap: a swapless box under memory pressure lets the kernel OOM killer choose
1502
+ # victims box-wide (the gateway, a tenant app, postgres). A modest swapfile
1503
+ # with low swappiness gives the kernel headroom without hurting responsiveness.
1504
+ if ! swapon --show=NAME --noheadings 2>/dev/null | grep -qx '/swapfile'; then
1505
+ if [ ! -f /swapfile ]; then
1506
+ fallocate -l ${swapSizeGb}G /swapfile || dd if=/dev/zero of=/swapfile bs=1M count=${swapSizeGb * 1024} status=none
1507
+ chmod 600 /swapfile
1508
+ mkswap /swapfile >/dev/null
1509
+ fi
1510
+ swapon /swapfile
1511
+ fi
1512
+ grep -qE '^/swapfile\\s' /etc/fstab || echo '/swapfile none swap sw 0 0' >> /etc/fstab
1513
+ echo 'vm.swappiness=10' > /etc/sysctl.d/99-ts-cloud-swap.conf
1514
+ sysctl -w vm.swappiness=10 >/dev/null
1515
+ `;
1516
+ }
1484
1517
  if (!baked) {
1485
1518
  script += `
1486
1519
  export DEBIAN_FRONTEND=noninteractive
@@ -2459,7 +2492,8 @@ class HetznerDriver {
2459
2492
  phpProvision: provision.phpProvision,
2460
2493
  servicesProvision: provision.servicesProvision,
2461
2494
  rpxProvision,
2462
- baked
2495
+ baked,
2496
+ swapGb: compute.swapGb
2463
2497
  });
2464
2498
  const userData = wrapCloudInitUserData(bootstrap);
2465
2499
  const serverType = resolveHetznerServerType(compute.size);
@@ -2530,7 +2564,7 @@ class HetznerDriver {
2530
2564
  { direction: "in", protocol: "tcp", port: "7700", source_ips: [network.ip_range] }
2531
2565
  ]);
2532
2566
  const servicesProvision = buildFleetServicesBoxProvision(config);
2533
- const servicesUserData = wrapCloudInitUserData(buildUbuntuBootstrapScript({ runtime: "php", servicesProvision, baked }));
2567
+ const servicesUserData = wrapCloudInitUserData(buildUbuntuBootstrapScript({ runtime: "php", servicesProvision, baked, swapGb: compute.swapGb }));
2534
2568
  const all = await this.client.listServers().catch(() => []);
2535
2569
  const newServerIds = [];
2536
2570
  let svcServer = all.find((s) => matchesTsCloudLabels(s.labels, slug, environment, "services"));
@@ -2566,7 +2600,7 @@ class HetznerDriver {
2566
2600
  optimizeForProduction: compute.php?.optimizeForProduction,
2567
2601
  ini: compute.php?.ini
2568
2602
  });
2569
- const appUserData = wrapCloudInitUserData(buildUbuntuBootstrapScript({ runtime: "php", phpProvision: appPhp, servicesProvision: appProvision, baked }));
2603
+ const appUserData = wrapCloudInitUserData(buildUbuntuBootstrapScript({ runtime: "php", phpProvision: appPhp, servicesProvision: appProvision, baked, swapGb: compute.swapGb }));
2570
2604
  const existingApp = all.filter((s) => matchesTsCloudLabels(s.labels, slug, environment, "app"));
2571
2605
  const appServerIds = existingApp.map((s) => s.id);
2572
2606
  if (existingApp.length > topology.appServers) {
@@ -2700,7 +2734,7 @@ class HetznerDriver {
2700
2734
  { direction: "in", protocol: "tcp", port: "7700", source_ips: [network.ip_range] }
2701
2735
  ]);
2702
2736
  const servicesProvision = buildFleetServicesBoxProvision(config);
2703
- const servicesUserData = wrapCloudInitUserData(buildUbuntuBootstrapScript({ runtime: "bun", servicesProvision, baked }));
2737
+ const servicesUserData = wrapCloudInitUserData(buildUbuntuBootstrapScript({ runtime: "bun", servicesProvision, baked, swapGb: compute.swapGb }));
2704
2738
  let svcServer = all.find((s) => matchesTsCloudLabels(s.labels, slug, environment, "services"));
2705
2739
  if (!svcServer) {
2706
2740
  const { server, action } = await this.client.createServer({
@@ -2732,7 +2766,8 @@ class HetznerDriver {
2732
2766
  systemPackages: compute.systemPackages,
2733
2767
  database: config.infrastructure?.database,
2734
2768
  servicesProvision: appProvisionScripts.servicesProvision,
2735
- baked
2769
+ baked,
2770
+ swapGb: compute.swapGb
2736
2771
  }));
2737
2772
  const existingApp = all.filter((s) => matchesTsCloudLabels(s.labels, slug, environment, "app"));
2738
2773
  const appServerIds = existingApp.map((s) => s.id);
@@ -3712,7 +3747,7 @@ function buildRollbackScript(paths, options = {}) {
3712
3747
  return [
3713
3748
  ...flip,
3714
3749
  `TS_CLOUD_RB_ID=$(basename "$(readlink -f ${paths.current})")`,
3715
- `if [ -f /etc/systemd/system/${unitBase}@.service ]; then ` + `systemctl start "${unitBase}@\${TS_CLOUD_RB_ID}.service"; sleep 2; ` + `systemctl is-active --quiet "${unitBase}@\${TS_CLOUD_RB_ID}.service" || { echo "rolled-back release failed to start" >&2; exit 1; }; ` + `systemctl enable "${unitBase}@\${TS_CLOUD_RB_ID}.service" 2>/dev/null || true; ` + `systemctl list-units --plain --no-legend --type=service "${unitBase}@*.service" 2>/dev/null | awk '{print $1}' | grep -v "^${unitBase}@\${TS_CLOUD_RB_ID}.service$" | while read -r TS_CLOUD_U; do systemctl stop "$TS_CLOUD_U" 2>/dev/null || true; systemctl disable "$TS_CLOUD_U" 2>/dev/null || true; done; ` + `elif [ -f /etc/systemd/system/${unitBase}.service ]; then systemctl restart ${unitBase}.service; fi`
3750
+ `if [ -f /etc/systemd/system/${unitBase}@.service ]; then ` + `systemctl start "${unitBase}@\${TS_CLOUD_RB_ID}.service"; sleep 2; ` + `systemctl is-active --quiet "${unitBase}@\${TS_CLOUD_RB_ID}.service" || { echo "rolled-back release failed to start" >&2; exit 1; }; ` + `systemctl enable "${unitBase}@\${TS_CLOUD_RB_ID}.service" 2>/dev/null || true; ` + `systemctl list-units --plain --no-legend --type=service "${unitBase}@*.service" 2>/dev/null | awk '{print $1}' | { grep -v "^${unitBase}@\${TS_CLOUD_RB_ID}.service$" || true; } | while read -r TS_CLOUD_U; do systemctl stop "$TS_CLOUD_U" 2>/dev/null || true; systemctl disable "$TS_CLOUD_U" 2>/dev/null || true; done; ` + `elif [ -f /etc/systemd/system/${unitBase}.service ]; then systemctl restart ${unitBase}.service; fi`
3716
3751
  ];
3717
3752
  }
3718
3753
  function buildPruneReleases(paths, keep = DEFAULT_KEEP_RELEASES) {
@@ -3842,7 +3877,7 @@ function buildSiteDeployScript(options) {
3842
3877
  ...buildActivateRelease(paths),
3843
3878
  `systemctl enable ${instance} 2>/dev/null || true`,
3844
3879
  `for TS_CLOUD_U in \${TS_CLOUD_OLD_UNITS}; do systemctl stop "$TS_CLOUD_U" 2>/dev/null || true; systemctl disable "$TS_CLOUD_U" 2>/dev/null || true; done`,
3845
- `systemctl list-unit-files --plain --no-legend "${unitBase}@*.service" 2>/dev/null | awk '{print $1}' | grep -v -e "^${instance}$" -e "^${unitBase}@\\.service$" | while read -r TS_CLOUD_U; do systemctl disable "$TS_CLOUD_U" 2>/dev/null || true; done`,
3880
+ `systemctl list-unit-files --plain --no-legend "${unitBase}@*.service" 2>/dev/null | awk '{print $1}' | { grep -v -e "^${instance}$" -e "^${unitBase}@\\.service$" || true; } | while read -r TS_CLOUD_U; do systemctl disable "$TS_CLOUD_U" 2>/dev/null || true; done`,
3846
3881
  `if [ -f /etc/systemd/system/${serviceName} ]; then systemctl disable ${serviceName} 2>/dev/null || true; rm -f /etc/systemd/system/${serviceName}; systemctl daemon-reload; fi`,
3847
3882
  ...buildPruneReleases(paths, keepReleases)
3848
3883
  ];
@@ -3987,9 +4022,10 @@ function resolveDashboardAuth(cwd, username, logger) {
3987
4022
  writeFileSync(file, `${JSON.stringify({ username, password, generatedAt: new Date().toISOString() }, null, 2)}
3988
4023
  `);
3989
4024
  chmodSync(file, 384);
3990
- logger.info(`Management dashboard: generated a password and saved it to ${DASHBOARD_CREDENTIALS_FILE} (user: ${username}, pass: ${password}). Set TS_CLOUD_UI_PASSWORD to pin your own, or TS_CLOUD_UI_PUBLIC=1 to serve without auth.`);
4025
+ logger.info(`Management dashboard: generated a password for '${username}' and saved it to ${DASHBOARD_CREDENTIALS_FILE} (read it there — it is not printed). Set TS_CLOUD_UI_PASSWORD to pin your own, or TS_CLOUD_UI_PUBLIC=1 to serve without auth.`);
3991
4026
  } catch (error) {
3992
- logger.warn(`Management dashboard: could not persist the generated password (${error?.message ?? error}). Using it for this deploy only — pass: ${password}`);
4027
+ logger.warn(`Management dashboard: could not persist the generated password (${error?.message ?? error}). Using it for this deploy only — pass: ${password}
4028
+ This password is now in your deploy log. Set TS_CLOUD_UI_PASSWORD to a value of your own and redeploy once ${DASHBOARD_CREDENTIALS_FILE} is writable.`);
3993
4029
  }
3994
4030
  return { password, source: "generated" };
3995
4031
  }
@@ -29,7 +29,7 @@ import {
29
29
  resolveSiteKind,
30
30
  resolveUiSource,
31
31
  siteInstallBase
32
- } from "./chunk-dah449r1.js";
32
+ } from "./chunk-e6xhm3qe.js";
33
33
  import {
34
34
  artifactKey,
35
35
  composeServerlessAppTemplate,
@@ -12638,7 +12638,7 @@ function renderSiteSnippet(input) {
12638
12638
  `);
12639
12639
  }
12640
12640
  function escapeSingle(value) {
12641
- return value.replace(/\\/g, "\\\\").replaceAll(String.fromCharCode(39), "\\'");
12641
+ return value.replace(/\\/g, "\\\\").replaceAll(String.fromCharCode(39), "\\'").replace(/\r/g, "\\r").replace(/\n/g, "\\n");
12642
12642
  }
12643
12643
  function normalizeSiteName(name) {
12644
12644
  const normalized = name.trim();
@@ -13405,6 +13405,8 @@ async function startLocalDashboardServer(options = {}) {
13405
13405
  }
13406
13406
  if (body.port !== undefined && body.port !== null && body.port !== "" && (!Number.isInteger(Number(body.port)) || Number(body.port) < 1 || Number(body.port) > 65535))
13407
13407
  return json({ ok: false, error: "Port must be a number between 1 and 65535." }, 422);
13408
+ if (typeof body.domain === "string" && body.domain.trim() && !isValidHostname(body.domain.trim()))
13409
+ return json({ ok: false, error: `Domain '${body.domain.trim()}' is not a valid hostname.` }, 422);
13408
13410
  let text2 = await readFile(configPath, "utf8");
13409
13411
  const set = (key, valueText) => {
13410
13412
  text2 = setSitePropertyInCloudConfig({ configText: text2, siteName: name, key, valueText });
@@ -12,7 +12,7 @@ import {
12
12
  sanitizeCloudConfig,
13
13
  setMaintenance,
14
14
  startLocalDashboardServer
15
- } from "../chunk-jgenfdz6.js";
15
+ } from "../chunk-jvp3s3na.js";
16
16
  import {
17
17
  buildAndPushServerlessImage
18
18
  } from "../chunk-tskj9fay.js";
@@ -30,7 +30,7 @@ import {
30
30
  resolveUiSource,
31
31
  siteInstallBase,
32
32
  validateDeploymentConfig
33
- } from "../chunk-dah449r1.js";
33
+ } from "../chunk-e6xhm3qe.js";
34
34
  import"../chunk-stt1z5cx.js";
35
35
  import"../chunk-93hjhs78.js";
36
36
  import {
@@ -50,7 +50,7 @@ import {
50
50
  waitForCloudInit,
51
51
  waitForSsh,
52
52
  wrapCloudInitUserData
53
- } from "../chunk-dah449r1.js";
53
+ } from "../chunk-e6xhm3qe.js";
54
54
  import"../chunk-stt1z5cx.js";
55
55
  import"../chunk-93hjhs78.js";
56
56
  import"../chunk-qpj3edwz.js";
@@ -47,6 +47,16 @@ export interface UbuntuBootstrapOptions {
47
47
  * boot near-instant. @default false
48
48
  */
49
49
  baked?: boolean;
50
+ /**
51
+ * Swapfile size in GB, from `compute.swapGb`. Small shared boxes run several
52
+ * tenants + services with no swap at all, so a memory spike (deploy, bun
53
+ * install, on-box builds) leaves the kernel OOM killer picking victims
54
+ * box-wide. Provisioned at the very top of the bootstrap — BEFORE the
55
+ * memory-hungry installs — on cold and baked boots alike (a golden image
56
+ * deliberately bakes no swap; see the image recipe). Idempotent: skipped
57
+ * when `/swapfile` is already active. `0` disables. @default 2
58
+ */
59
+ swapGb?: number;
50
60
  }
51
61
  /**
52
62
  * Build the Ubuntu provisioning bash script (with `#!/bin/bash` shebang).
package/dist/index.js CHANGED
@@ -55,7 +55,7 @@ import {
55
55
  sanitizeCloudConfig,
56
56
  setMaintenance,
57
57
  startLocalDashboardServer
58
- } from "./chunk-jgenfdz6.js";
58
+ } from "./chunk-jvp3s3na.js";
59
59
  import {
60
60
  buildAndPushServerlessImage
61
61
  } from "./chunk-tskj9fay.js";
@@ -105,7 +105,7 @@ import {
105
105
  waitForCloudInit,
106
106
  waitForSsh,
107
107
  wrapCloudInitUserData
108
- } from "./chunk-dah449r1.js";
108
+ } from "./chunk-e6xhm3qe.js";
109
109
  import {
110
110
  ABTestManager,
111
111
  AI,