@stacksjs/ts-cloud 0.5.34 → 0.6.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.
Files changed (77) hide show
  1. package/dist/aws/cloudwatch.d.ts +2 -0
  2. package/dist/aws/xray.d.ts +45 -0
  3. package/dist/bin/cli.js +723 -701
  4. package/dist/deploy/dashboard-database.d.ts +25 -0
  5. package/dist/deploy/dashboard-operations.d.ts +7 -0
  6. package/dist/deploy/firewall-config-editor.d.ts +15 -0
  7. package/dist/deploy/firewall-config-editor.test.d.ts +1 -0
  8. package/dist/deploy/index.d.ts +1 -0
  9. package/dist/deploy/management-dashboard.d.ts +26 -3
  10. package/dist/deploy/serverless-operations.d.ts +150 -0
  11. package/dist/deploy/serverless-operations.test.d.ts +1 -0
  12. package/dist/deploy/site-config-editor.d.ts +6 -0
  13. package/dist/deploy/ssh-config-editor.d.ts +1 -0
  14. package/dist/deploy/terminal-session.d.ts +23 -0
  15. package/dist/index.d.ts +2 -2
  16. package/dist/index.js +1008 -33
  17. package/dist/ui/index.html +4 -4
  18. package/dist/ui/server/actions.html +34 -5
  19. package/dist/ui/server/activity.html +1 -1
  20. package/dist/ui/server/backups.html +4 -4
  21. package/dist/ui/server/database.html +27 -5
  22. package/dist/ui/server/deployments.html +4 -4
  23. package/dist/ui/server/diagnostics.html +1 -1
  24. package/dist/ui/server/firewall.html +1233 -0
  25. package/dist/ui/server/logs.html +4 -4
  26. package/dist/ui/server/metrics.html +1 -1
  27. package/dist/ui/server/security.html +1 -1
  28. package/dist/ui/server/services.html +4 -4
  29. package/dist/ui/server/sites.html +24 -5
  30. package/dist/ui/server/ssh-keys.html +4 -4
  31. package/dist/ui/server/terminal.html +1191 -0
  32. package/dist/ui/server/workers.html +4 -4
  33. package/dist/ui/serverless/alarms.html +1236 -0
  34. package/dist/ui/serverless/assets.html +53 -1
  35. package/dist/ui/serverless/cost.html +1 -1
  36. package/dist/ui/serverless/data.html +58 -1
  37. package/dist/ui/serverless/deployments.html +53 -1
  38. package/dist/ui/serverless/firewall.html +1 -1
  39. package/dist/ui/serverless/functions.html +63 -1
  40. package/dist/ui/serverless/logs.html +4 -4
  41. package/dist/ui/serverless/metrics.html +1 -1
  42. package/dist/ui/serverless/queues.html +94 -1
  43. package/dist/ui/serverless/scheduler.html +55 -1
  44. package/dist/ui/serverless/secrets.html +99 -1
  45. package/dist/ui/serverless/traces.html +1177 -0
  46. package/dist/ui/serverless.html +87 -1
  47. package/dist/ui-src/pages/index.stx +1 -1
  48. package/dist/ui-src/pages/partials/head.stx +6 -6
  49. package/dist/ui-src/pages/partials/nav.stx +4 -3
  50. package/dist/ui-src/pages/server/actions.stx +34 -1
  51. package/dist/ui-src/pages/server/backups.stx +1 -1
  52. package/dist/ui-src/pages/server/database.stx +32 -9
  53. package/dist/ui-src/pages/server/deployments.stx +2 -2
  54. package/dist/ui-src/pages/server/firewall.stx +134 -0
  55. package/dist/ui-src/pages/server/logs.stx +2 -2
  56. package/dist/ui-src/pages/server/metrics.stx +5 -5
  57. package/dist/ui-src/pages/server/security.stx +1 -1
  58. package/dist/ui-src/pages/server/services.stx +1 -1
  59. package/dist/ui-src/pages/server/sites.stx +24 -3
  60. package/dist/ui-src/pages/server/ssh-keys.stx +2 -2
  61. package/dist/ui-src/pages/server/terminal.stx +65 -0
  62. package/dist/ui-src/pages/server/workers.stx +1 -1
  63. package/dist/ui-src/pages/serverless/alarms.stx +153 -0
  64. package/dist/ui-src/pages/serverless/assets.stx +29 -2
  65. package/dist/ui-src/pages/serverless/cost.stx +4 -4
  66. package/dist/ui-src/pages/serverless/data.stx +56 -3
  67. package/dist/ui-src/pages/serverless/deployments.stx +30 -6
  68. package/dist/ui-src/pages/serverless/firewall.stx +1 -1
  69. package/dist/ui-src/pages/serverless/functions.stx +62 -4
  70. package/dist/ui-src/pages/serverless/logs.stx +2 -2
  71. package/dist/ui-src/pages/serverless/metrics.stx +5 -1
  72. package/dist/ui-src/pages/serverless/queues.stx +90 -26
  73. package/dist/ui-src/pages/serverless/scheduler.stx +31 -2
  74. package/dist/ui-src/pages/serverless/secrets.stx +94 -14
  75. package/dist/ui-src/pages/serverless/traces.stx +88 -0
  76. package/dist/ui-src/pages/serverless.stx +87 -24
  77. package/package.json +3 -3
package/dist/index.js CHANGED
@@ -697,6 +697,30 @@ function isNodeCryptoAvailable() {
697
697
  function isWebCryptoAvailable() {
698
698
  return typeof crypto !== "undefined" && typeof crypto.subtle !== "undefined";
699
699
  }
700
+ function detectDeploymentTargets(config6) {
701
+ const server = config6.infrastructure?.compute != null;
702
+ const serverless = Object.values(config6.environments ?? {}).some((env) => env?.app != null);
703
+ return { server, serverless };
704
+ }
705
+ function resolveDeploymentMode(config6) {
706
+ if (config6.mode === "server" || config6.mode === "serverless")
707
+ return config6.mode;
708
+ const { serverless } = detectDeploymentTargets(config6);
709
+ return serverless ? "serverless" : "server";
710
+ }
711
+ function deploymentCoexistenceError(config6) {
712
+ const { server, serverless } = detectDeploymentTargets(config6);
713
+ if (server && serverless) {
714
+ return "A project cannot be both a server and a serverless deployment. " + "Found `infrastructure.compute` (server) AND `environments.<env>.app` (serverless Lambda). " + "Keep one: remove `infrastructure.compute` for a serverless app, or remove `environments.<env>.app` for a server.";
715
+ }
716
+ if (config6.mode === "server" && serverless && !server) {
717
+ return "Config sets `mode: 'server'` but declares a serverless app (`environments.<env>.app`). " + "Remove the app or change the mode.";
718
+ }
719
+ if (config6.mode === "serverless" && server && !serverless) {
720
+ return "Config sets `mode: 'serverless'` but declares a server (`infrastructure.compute`). " + "Remove the compute block or change the mode.";
721
+ }
722
+ return null;
723
+ }
700
724
  function resolveCloudProvider(config6) {
701
725
  if (config6.cloud?.provider)
702
726
  return config6.cloud.provider;
@@ -6052,7 +6076,7 @@ function createFullStackAppPreset(options) {
6052
6076
  slug,
6053
6077
  region: "us-east-1"
6054
6078
  },
6055
- mode: "hybrid",
6079
+ mode: "server",
6056
6080
  environments: {
6057
6081
  production: {
6058
6082
  type: "production",
@@ -9729,8 +9753,8 @@ function validateAgainstSchema(config6) {
9729
9753
  if (config6.project?.region && !validRegions.includes(config6.project.region)) {
9730
9754
  errors.push(`Invalid region: ${config6.project.region}. Must be one of: ${validRegions.join(", ")}`);
9731
9755
  }
9732
- if (config6.mode && !["server", "serverless", "hybrid"].includes(config6.mode)) {
9733
- errors.push("Invalid mode: must be one of server, serverless, or hybrid");
9756
+ if (config6.mode && !["server", "serverless"].includes(config6.mode)) {
9757
+ errors.push("Invalid mode: must be one of server or serverless");
9734
9758
  }
9735
9759
  return {
9736
9760
  valid: errors.length === 0,
@@ -42108,9 +42132,8 @@ Details:`;
42108
42132
  },
42109
42133
  mode: {
42110
42134
  type: "string",
42111
- description: "Deployment mode",
42112
- enum: ["server", "serverless", "hybrid"],
42113
- default: "serverless"
42135
+ description: "Deployment mode (server and serverless are mutually exclusive; auto-detected when omitted)",
42136
+ enum: ["server", "serverless"]
42114
42137
  },
42115
42138
  environments: {
42116
42139
  type: "object",
@@ -55774,6 +55797,11 @@ var init_email = __esm(() => {
55774
55797
  });
55775
55798
 
55776
55799
  // src/aws/rds.ts
55800
+ var exports_rds = {};
55801
+ __export(exports_rds, {
55802
+ RDSClient: () => RDSClient
55803
+ });
55804
+
55777
55805
  class RDSClient {
55778
55806
  client;
55779
55807
  region;
@@ -83323,6 +83351,7 @@ class AcmeClient {
83323
83351
  }
83324
83352
  }
83325
83353
  // src/deploy/site-target.ts
83354
+ await init_dist2();
83326
83355
  var PHP_SITE_TYPES = new Set([
83327
83356
  "laravel",
83328
83357
  "php",
@@ -83359,6 +83388,9 @@ function validateDeploymentConfig(config6) {
83359
83388
  const warnings = [];
83360
83389
  const sites = config6.sites || {};
83361
83390
  const computeConfigured = hasComputeConfigured(config6);
83391
+ const coexistence = deploymentCoexistenceError(config6);
83392
+ if (coexistence)
83393
+ errors.push(coexistence);
83362
83394
  const portOwners = new Map;
83363
83395
  for (const [name, site] of Object.entries(sites)) {
83364
83396
  if (!site) {
@@ -84034,6 +84066,24 @@ async function serverlessInfo(config6, environment) {
84034
84066
  lastRelease: release ? { sha: release.sha, timestamp: release.timestamp } : undefined
84035
84067
  };
84036
84068
  }
84069
+ function serverlessClusterId(slug, environment) {
84070
+ return `${slug}-${environment}-db`;
84071
+ }
84072
+ async function scaleServerlessDatabase(config6, environment, minCapacity, maxCapacity) {
84073
+ const ctx = resolveContext(config6, environment);
84074
+ if (ctx.app.database?.connection !== "aurora-serverless")
84075
+ throw new Error("No Aurora Serverless v2 database is configured for this environment.");
84076
+ const { RDSClient: RDSClient2 } = await Promise.resolve().then(() => (init_rds(), exports_rds));
84077
+ const rds2 = new RDSClient2(ctx.region);
84078
+ const id = serverlessClusterId(ctx.slug, environment);
84079
+ step(`Scaling ${id} → ${minCapacity}–${maxCapacity} ACUs`);
84080
+ await rds2.modifyDBCluster({
84081
+ DBClusterIdentifier: id,
84082
+ ServerlessV2ScalingConfiguration: { MinCapacity: minCapacity, MaxCapacity: maxCapacity },
84083
+ ApplyImmediately: true
84084
+ });
84085
+ success(`Scaling applied to ${id} (takes effect shortly).`);
84086
+ }
84037
84087
 
84038
84088
  // src/deploy/index.ts
84039
84089
  await init_serverless_image();
@@ -84165,6 +84215,15 @@ class CloudWatchClient {
84165
84215
  });
84166
84216
  await this.query(params);
84167
84217
  }
84218
+ async deleteAlarms(alarmNames) {
84219
+ if (!alarmNames.length)
84220
+ return;
84221
+ const params = { Action: "DeleteAlarms" };
84222
+ alarmNames.forEach((name, i) => {
84223
+ params[`AlarmNames.member.${i + 1}`] = name;
84224
+ });
84225
+ await this.query(params);
84226
+ }
84168
84227
  }
84169
84228
 
84170
84229
  // src/deploy/dashboard-data.ts
@@ -84500,6 +84559,17 @@ async function resolveDashboardData(config6, environment) {
84500
84559
  } catch {}
84501
84560
  } catch {}
84502
84561
  }
84562
+ const dataServices = [];
84563
+ if (out.aurora)
84564
+ dataServices.push({ name: "Aurora Serverless v2", detail: `${out.aurora.id} · ${out.aurora.minAcu}-${out.aurora.maxAcu} ACU`, status: out.aurora.status });
84565
+ if (out.proxy)
84566
+ dataServices.push({ name: "RDS Proxy", detail: out.proxy.name, status: out.proxy.status });
84567
+ if (out.redis)
84568
+ dataServices.push({ name: `ElastiCache (${out.redis.engine})`, detail: `${out.redis.node} · ${out.redis.hitRate}% hit rate`, status: out.redis.status });
84569
+ if (out.efs)
84570
+ dataServices.push({ name: "EFS", detail: `${out.efs.mount} · ${out.efs.sizeMb} MB`, status: out.efs.status });
84571
+ if (dataServices.length)
84572
+ out.dataServices = dataServices;
84503
84573
  if (info2.scheduler !== "off" && app?.kind === "php") {
84504
84574
  try {
84505
84575
  const out2 = await runRemoteCommand(config6, environment, "schedule:list");
@@ -84517,6 +84587,7 @@ async function resolveDashboardData(config6, environment) {
84517
84587
  return out;
84518
84588
  }
84519
84589
  // src/deploy/local-dashboard-server.ts
84590
+ await init_dist2();
84520
84591
  import { existsSync as existsSync26, statSync as statSync6 } from "node:fs";
84521
84592
  import { readFile as readFile2, writeFile as writeFile6 } from "node:fs/promises";
84522
84593
  import { mkdtempSync as mkdtempSync6 } from "node:fs";
@@ -84525,7 +84596,7 @@ import { dirname as dirname9, extname as extname3, join as join23, normalize } f
84525
84596
  import { fileURLToPath as fileURLToPath6 } from "node:url";
84526
84597
 
84527
84598
  // src/deploy/dashboard-data-server.ts
84528
- import { existsSync as existsSync24, readFileSync as readFileSync17 } from "node:fs";
84599
+ import { existsSync as existsSync24, readFileSync as readFileSync18 } from "node:fs";
84529
84600
  import { join as join21 } from "node:path";
84530
84601
 
84531
84602
  // src/drivers/factory.ts
@@ -85507,6 +85578,11 @@ ${servicesProvision.join(`
85507
85578
  }
85508
85579
  if (runtime === "bun") {
85509
85580
  script += `
85581
+ # cloud-init runs this with \`set -u\` and NO $HOME set. bun's install.sh
85582
+ # dereferences $HOME even when BUN_INSTALL is set, so without this the install
85583
+ # aborts with "HOME: unbound variable" and /usr/local/bin/bun never appears —
85584
+ # breaking a from-scratch provision (adopting an existing box masked it).
85585
+ export HOME="\${HOME:-/root}"
85510
85586
  export BUN_INSTALL="/root/.bun"
85511
85587
  curl -fsSL https://bun.sh/install | bash${runtimeVersion === "latest" ? "" : ` -s "bun-v${runtimeVersion}"`}
85512
85588
  ln -sf /root/.bun/bin/bun /usr/local/bin/bun
@@ -87396,11 +87472,42 @@ await init_dist2();
87396
87472
  // src/deploy/management-dashboard.ts
87397
87473
  await init_dist2();
87398
87474
  import { execSync as execSync5 } from "node:child_process";
87399
- import { existsSync as existsSync21 } from "node:fs";
87475
+ import { randomBytes as randomBytes6 } from "node:crypto";
87476
+ import { chmodSync, existsSync as existsSync21, mkdirSync as mkdirSync9, readFileSync as readFileSync17, writeFileSync as writeFileSync12 } from "node:fs";
87400
87477
  import { tmpdir as tmpdir6 } from "node:os";
87401
87478
  import { dirname as dirname8, isAbsolute as isAbsolute5, join as join20 } from "node:path";
87402
87479
  import { fileURLToPath as fileURLToPath4 } from "node:url";
87403
87480
  var MANAGEMENT_DASHBOARD_SITE = "dashboard";
87481
+ var DASHBOARD_CREDENTIALS_FILE = join20(".ts-cloud", "dashboard-credentials.json");
87482
+ function generatePassword() {
87483
+ return randomBytes6(24).toString("base64url");
87484
+ }
87485
+ function resolveDashboardAuth(cwd, username, logger4) {
87486
+ const explicit = process.env.TS_CLOUD_UI_PASSWORD?.trim();
87487
+ if (explicit)
87488
+ return { password: explicit, source: "env" };
87489
+ if (truthy(process.env.TS_CLOUD_UI_PUBLIC))
87490
+ return { password: undefined, source: "public" };
87491
+ const file = join20(cwd, DASHBOARD_CREDENTIALS_FILE);
87492
+ try {
87493
+ if (existsSync21(file)) {
87494
+ const saved = JSON.parse(readFileSync17(file, "utf8"));
87495
+ if (saved?.password)
87496
+ return { password: saved.password, source: "generated" };
87497
+ }
87498
+ } catch {}
87499
+ const password = generatePassword();
87500
+ try {
87501
+ mkdirSync9(dirname8(file), { recursive: true });
87502
+ writeFileSync12(file, `${JSON.stringify({ username, password, generatedAt: new Date().toISOString() }, null, 2)}
87503
+ `);
87504
+ chmodSync(file, 384);
87505
+ logger4.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.`);
87506
+ } catch (error2) {
87507
+ logger4.warn(`Management dashboard: could not persist the generated password (${error2?.message ?? error2}). Using it for this deploy only — pass: ${password}`);
87508
+ }
87509
+ return { password, source: "generated" };
87510
+ }
87404
87511
  var noopLogger = { info: () => {}, warn: () => {} };
87405
87512
  function truthy(v) {
87406
87513
  return v != null && v !== "" && v !== "0" && v.toLowerCase() !== "false";
@@ -87436,7 +87543,8 @@ function ensureManagementDashboard(config6, options = {}) {
87436
87543
  logger4.info("Management dashboard: UI not found (no local ui/ or packaged dist/ui) — skipping auto-deploy.");
87437
87544
  return config6;
87438
87545
  }
87439
- const password = process.env.TS_CLOUD_UI_PASSWORD?.trim() || undefined;
87546
+ const username = process.env.TS_CLOUD_UI_USERNAME?.trim() || "admin";
87547
+ const auth = resolveDashboardAuth(cwd, username, logger4);
87440
87548
  const environment = config6.environments && Object.keys(config6.environments)[0];
87441
87549
  const live = truthy(process.env.TS_CLOUD_UI_LIVE);
87442
87550
  const port = Number(process.env.TS_CLOUD_UI_PORT) || undefined;
@@ -87444,8 +87552,8 @@ function ensureManagementDashboard(config6, options = {}) {
87444
87552
  uiRoot: ui.uiRoot,
87445
87553
  build: ui.build,
87446
87554
  domain: process.env.TS_CLOUD_UI_DOMAIN?.trim() || undefined,
87447
- username: process.env.TS_CLOUD_UI_USERNAME?.trim() || undefined,
87448
- password,
87555
+ username,
87556
+ password: auth.password,
87449
87557
  realm: process.env.TS_CLOUD_UI_REALM?.trim() || undefined,
87450
87558
  live,
87451
87559
  port
@@ -87455,7 +87563,8 @@ function ensureManagementDashboard(config6, options = {}) {
87455
87563
  return config6;
87456
87564
  }
87457
87565
  config6.sites = { ...config6.sites ?? {}, [resolved.name]: resolved.site };
87458
- logger4.info(`Management dashboard https://${resolved.site.domain} (${password ? "htpasswd-protected" : "NO AUTHset TS_CLOUD_UI_PASSWORD"})`);
87566
+ const authNote = auth.source === "public" ? "NO AUTH — TS_CLOUD_UI_PUBLIC is set (dashboard is publicly reachable)" : auth.source === "env" ? "htpasswd-protected (TS_CLOUD_UI_PASSWORD)" : `htpasswd-protected (auto-generatedsee ${DASHBOARD_CREDENTIALS_FILE})`;
87567
+ logger4.info(`Management dashboard → https://${resolved.site.domain} (${authNote})`);
87459
87568
  return config6;
87460
87569
  }
87461
87570
  function buildManagementDashboardArtifact(site, options) {
@@ -88525,7 +88634,7 @@ function loadLocalState(config6, environment) {
88525
88634
  if (!existsSync24(statePath))
88526
88635
  return null;
88527
88636
  try {
88528
- return JSON.parse(readFileSync17(statePath, "utf8"));
88637
+ return JSON.parse(readFileSync18(statePath, "utf8"));
88529
88638
  } catch {
88530
88639
  return null;
88531
88640
  }
@@ -89034,6 +89143,9 @@ function resolveConfigOnlyServerDashboardData(config6, environment) {
89034
89143
  root: kind === "server-static" ? `/var/www/${s.name}` : `/var/www/${s.name}/current`,
89035
89144
  branch: kind === "server-static" ? "build artifact" : "main",
89036
89145
  build: site.build,
89146
+ php: site.php ?? site.phpVersion,
89147
+ aliases: Array.isArray(site.aliases) ? site.aliases : [],
89148
+ redirects: site.redirects && typeof site.redirects === "object" ? site.redirects : {},
89037
89149
  envKeys: Object.keys(site.env ?? site.environment ?? {})
89038
89150
  };
89039
89151
  }),
@@ -89307,6 +89419,45 @@ async function createDatabaseUser(config6, environment, input) {
89307
89419
  const engine = resolveDbEngine(config6);
89308
89420
  return runDb(config6, environment, buildCreateUserScript(engine, input), `ts-cloud db:user ${input.username}`);
89309
89421
  }
89422
+ var DB_BACKUP_DIR = "/var/backups/ts-cloud/databases";
89423
+ function buildBackupScript(engine, name, destDir = DB_BACKUP_DIR) {
89424
+ const file = `${destDir}/${name}-$(date +%Y%m%d-%H%M%S).sql.gz`;
89425
+ const mkdir5 = `mkdir -p ${destDir}`;
89426
+ if (engine === "postgres")
89427
+ return [mkdir5, `pg_dump -h 127.0.0.1 -p 5432 -U postgres ${name} | gzip > "${file}"`, `echo "BACKUP=${file}"`, `ls -l "${file}"`];
89428
+ const sock = engine === "mariadb" ? SOCKETS.mariadb : SOCKETS.mysql;
89429
+ return [mkdir5, `mysqldump --socket=${sock} -u root ${name} | gzip > "${file}"`, `echo "BACKUP=${file}"`, `ls -l "${file}"`];
89430
+ }
89431
+ function buildListBackupsScript(destDir = DB_BACKUP_DIR) {
89432
+ return [`ls -1t ${destDir}/*.sql.gz 2>/dev/null | head -50 | sed 's|^|BACKUP=|' || true`];
89433
+ }
89434
+ function parseBackups(output) {
89435
+ const out = [];
89436
+ for (const rawLine of output.split(`
89437
+ `)) {
89438
+ const line = rawLine.trim();
89439
+ if (!line.startsWith("BACKUP="))
89440
+ continue;
89441
+ const file = line.slice(7);
89442
+ const base = file.split("/").pop() ?? file;
89443
+ const database = base.replace(/-\d{8}-\d{6}\.sql\.gz$/, "");
89444
+ if (file)
89445
+ out.push({ file, database });
89446
+ }
89447
+ return out;
89448
+ }
89449
+ async function backupDatabase(config6, environment, name) {
89450
+ if (!isValidDbIdentifier(name))
89451
+ return { ok: false, error: "Database name must be a valid identifier.", database: name };
89452
+ const engine = resolveDbEngine(config6);
89453
+ const r = await runDb(config6, environment, buildBackupScript(engine, name), `ts-cloud db:backup ${name}`);
89454
+ return { ...r, database: name };
89455
+ }
89456
+ async function listDatabaseBackups(config6, environment) {
89457
+ const r = await runDb(config6, environment, buildListBackupsScript(), "ts-cloud db:backups");
89458
+ const backups = r.ok && r.stdout ? parseBackups(r.stdout) : [];
89459
+ return { ...r, backups };
89460
+ }
89310
89461
 
89311
89462
  // src/drivers/shared/compute-ops.ts
89312
89463
  var noopLogger3 = {
@@ -89425,8 +89576,17 @@ function buildDashboardOperations(config6, data) {
89425
89576
  for (const [name, site] of Object.entries(config6.sites ?? {})) {
89426
89577
  if (!site || !isSafeSiteName(name))
89427
89578
  continue;
89428
- if (site.scheduler || site.schedule)
89429
- ops.push({ id: `scheduler:run:${name}`, label: `Run scheduler (${name})`, group: "scheduler", target: name, mutates: true, confirm: name });
89579
+ if (site.scheduler || site.schedule) {
89580
+ const laravel = resolveSiteFramework(site) === "laravel";
89581
+ ops.push({
89582
+ id: `scheduler:run:${name}`,
89583
+ label: `${laravel ? "Run" : "Restart"} scheduler (${name})`,
89584
+ group: "scheduler",
89585
+ target: name,
89586
+ mutates: true,
89587
+ confirm: name
89588
+ });
89589
+ }
89430
89590
  }
89431
89591
  const backups = config6.infrastructure?.compute?.backups;
89432
89592
  if (backups?.enabled) {
@@ -89454,6 +89614,28 @@ function siteArtisanCommand(site, artisan) {
89454
89614
  `cd ${current} && ${pantryEnvEval()} && php artisan ${artisan}`
89455
89615
  ];
89456
89616
  }
89617
+ function workerRestartCommand(framework, slug, site) {
89618
+ if (framework === "laravel")
89619
+ return siteArtisanCommand(site, "queue:restart");
89620
+ const pattern = `${slug}-${site}-queue-*.service`;
89621
+ return [
89622
+ "set -uo pipefail",
89623
+ `units=$(systemctl list-units --type=service --all --no-legend '${pattern}' 2>/dev/null | awk '{print $1}')`,
89624
+ `[ -n "$units" ] || { echo "no queue-worker units found for ${site} (${pattern})" >&2; exit 1; }`,
89625
+ "systemctl restart $units",
89626
+ `systemctl --no-legend --type=service list-units '${pattern}' 2>/dev/null || true`
89627
+ ];
89628
+ }
89629
+ function schedulerRunCommand(framework, slug, site) {
89630
+ if (framework === "laravel")
89631
+ return siteArtisanCommand(site, "schedule:run");
89632
+ const unit = `${slug}-${site}-scheduler.service`;
89633
+ return [
89634
+ "set -uo pipefail",
89635
+ `systemctl restart ${unit}`,
89636
+ `systemctl is-active ${unit} 2>/dev/null || true`
89637
+ ];
89638
+ }
89457
89639
  function backupRunCommand() {
89458
89640
  return [
89459
89641
  "set -uo pipefail",
@@ -89498,11 +89680,13 @@ async function runDashboardOperation(config6, environment, operation, options =
89498
89680
  commands = serviceCommand(verb, rest.join(":"));
89499
89681
  command = `systemctl ${verb} ${operation.target}`;
89500
89682
  } else if (operation.group === "worker") {
89501
- commands = siteArtisanCommand(operation.target, "queue:restart");
89502
- command = `queue:restart (${operation.target})`;
89683
+ const framework = resolveSiteFramework(config6.sites?.[operation.target] ?? {});
89684
+ commands = workerRestartCommand(framework, slug, operation.target);
89685
+ command = framework === "laravel" ? `queue:restart (${operation.target})` : `restart queue units (${operation.target})`;
89503
89686
  } else if (operation.group === "scheduler") {
89504
- commands = siteArtisanCommand(operation.target, "schedule:run");
89505
- command = `schedule:run (${operation.target})`;
89687
+ const framework = resolveSiteFramework(config6.sites?.[operation.target] ?? {});
89688
+ commands = schedulerRunCommand(framework, slug, operation.target);
89689
+ command = framework === "laravel" ? `schedule:run (${operation.target})` : `restart scheduler (${operation.target})`;
89506
89690
  } else if (operation.id === "backup:run") {
89507
89691
  commands = backupRunCommand();
89508
89692
  command = "ts-cloud backup";
@@ -89523,6 +89707,519 @@ async function runDashboardOperation(config6, environment, operation, options =
89523
89707
  stderr: clampOutput(result.perInstance?.[0]?.error ?? result.error ?? "")
89524
89708
  };
89525
89709
  }
89710
+ async function runServerShellCommand(config6, environment, command) {
89711
+ const trimmed = command.trim();
89712
+ if (!trimmed)
89713
+ return { operation: "command", ok: false, error: "A command is required." };
89714
+ let driver;
89715
+ try {
89716
+ driver = createCloudDriver({ config: config6 });
89717
+ } catch (error2) {
89718
+ return { operation: "command", ok: false, error: `Could not initialize the cloud driver: ${error2?.message ?? error2}` };
89719
+ }
89720
+ const slug = config6.project.slug;
89721
+ const targets = await driver.findComputeTargets({ slug, environment, role: "app" });
89722
+ if (!targets.length)
89723
+ return { operation: "command", ok: false, error: "No app server target was found for this environment." };
89724
+ const result = await driver.runRemoteDeploy({
89725
+ targets: [targets[0]],
89726
+ commands: ["set -o pipefail", trimmed],
89727
+ comment: "ts-cloud dashboard:command",
89728
+ tags: { Project: slug, Environment: environment, Role: "app" }
89729
+ });
89730
+ return {
89731
+ operation: "command",
89732
+ command: trimmed,
89733
+ ok: result.success,
89734
+ stdout: clampOutput(result.perInstance?.[0]?.output ?? ""),
89735
+ stderr: clampOutput(result.perInstance?.[0]?.error ?? result.error ?? "")
89736
+ };
89737
+ }
89738
+
89739
+ // src/deploy/firewall-config-editor.ts
89740
+ var ALWAYS_OPEN = new Set([22, 80, 443]);
89741
+ function isValidPort(port) {
89742
+ return Number.isInteger(port) && port >= 1 && port <= 65535;
89743
+ }
89744
+ function normalizePorts(ports) {
89745
+ return [...new Set(ports.filter((p) => isValidPort(p) && !ALWAYS_OPEN.has(p)))].sort((a, b) => a - b);
89746
+ }
89747
+ function addFirewallPort(configText, port, existing = []) {
89748
+ if (!isValidPort(port))
89749
+ throw new Error("Port must be an integer between 1 and 65535.");
89750
+ if (ALWAYS_OPEN.has(port))
89751
+ throw new Error(`Port ${port} (SSH/HTTP/HTTPS) is always open and is not managed here.`);
89752
+ return setFirewallPorts({ configText, ports: normalizePorts([...existing, port]) });
89753
+ }
89754
+ function removeFirewallPort(configText, port, existing = []) {
89755
+ return setFirewallPorts({ configText, ports: normalizePorts(existing.filter((p) => p !== port)) });
89756
+ }
89757
+ function findBlock(text, start, end, keyword, open) {
89758
+ const body = text.slice(start, end);
89759
+ const match = new RegExp(`\\b${keyword}\\s*:\\s*\\${open}`).exec(body);
89760
+ if (!match)
89761
+ return null;
89762
+ const propStart = start + match.index;
89763
+ const openIdx = text.indexOf(open, propStart);
89764
+ return { propStart, open: openIdx, close: findMatching(text, openIdx, open, open === "{" ? "}" : "]") };
89765
+ }
89766
+ function renderPortsArray(ports) {
89767
+ return `allowedPorts: [${ports.join(", ")}]`;
89768
+ }
89769
+ function setFirewallPorts(input) {
89770
+ const { configText } = input;
89771
+ const ports = normalizePorts(input.ports);
89772
+ const computeMatch = /\bcompute\s*:\s*{/.exec(configText);
89773
+ if (!computeMatch)
89774
+ throw new Error("Could not find infrastructure.compute in the cloud config.");
89775
+ const computeOpen = configText.indexOf("{", computeMatch.index);
89776
+ const computeClose = findMatching(configText, computeOpen, "{", "}");
89777
+ const firewall = findBlock(configText, computeOpen + 1, computeClose, "firewall", "{");
89778
+ if (firewall) {
89779
+ const portsArr = findBlock(configText, firewall.open + 1, firewall.close, "allowedPorts", "[");
89780
+ if (portsArr) {
89781
+ let lineStart = portsArr.propStart;
89782
+ while (lineStart > 0 && configText[lineStart - 1] !== `
89783
+ ` && /\s/.test(configText[lineStart - 1]))
89784
+ lineStart--;
89785
+ const indent = configText.slice(lineStart, portsArr.propStart);
89786
+ return `${configText.slice(0, lineStart)}${indent}${renderPortsArray(ports)}${configText.slice(portsArr.close + 1)}`;
89787
+ }
89788
+ const before2 = configText.slice(0, firewall.close).trimEnd();
89789
+ const after2 = configText.slice(firewall.close);
89790
+ const sep2 = before2.endsWith(",") || before2.endsWith("{") ? "" : ",";
89791
+ return `${before2}${sep2}
89792
+ ${renderPortsArray(ports)},
89793
+ ${after2}`;
89794
+ }
89795
+ const before = configText.slice(0, computeClose).trimEnd();
89796
+ const after = configText.slice(computeClose);
89797
+ const sep = before.endsWith(",") || before.endsWith("{") ? "" : ",";
89798
+ return `${before}${sep}
89799
+ firewall: {
89800
+ enabled: true,
89801
+ ${renderPortsArray(ports)},
89802
+ },
89803
+ ${after}`;
89804
+ }
89805
+
89806
+ // src/deploy/serverless-operations.ts
89807
+ init_cloudformation();
89808
+ init_cloudfront();
89809
+ await init_dist2();
89810
+
89811
+ // src/aws/xray.ts
89812
+ init_client();
89813
+
89814
+ class XRayClient {
89815
+ client;
89816
+ region;
89817
+ constructor(region = "us-east-1") {
89818
+ this.region = region;
89819
+ this.client = new AWSClient;
89820
+ }
89821
+ async getTraceSummaries(options) {
89822
+ const body = {
89823
+ StartTime: Math.floor(options.startTime.getTime() / 1000),
89824
+ EndTime: Math.floor(options.endTime.getTime() / 1000),
89825
+ TimeRangeType: "Event",
89826
+ Sampling: false
89827
+ };
89828
+ if (options.filterExpression)
89829
+ body.FilterExpression = options.filterExpression;
89830
+ if (options.nextToken)
89831
+ body.NextToken = options.nextToken;
89832
+ const res = await this.client.request({
89833
+ service: "xray",
89834
+ region: this.region,
89835
+ method: "POST",
89836
+ path: "/TraceSummaries",
89837
+ headers: { "Content-Type": "application/json" },
89838
+ body: JSON.stringify(body)
89839
+ });
89840
+ return { summaries: res?.TraceSummaries ?? [], nextToken: res?.NextToken };
89841
+ }
89842
+ async batchGetTraces(traceIds) {
89843
+ if (!traceIds.length)
89844
+ return [];
89845
+ const res = await this.client.request({
89846
+ service: "xray",
89847
+ region: this.region,
89848
+ method: "POST",
89849
+ path: "/Traces",
89850
+ headers: { "Content-Type": "application/json" },
89851
+ body: JSON.stringify({ TraceIds: traceIds.slice(0, 5) })
89852
+ });
89853
+ return res?.Traces ?? [];
89854
+ }
89855
+ }
89856
+
89857
+ // src/deploy/serverless-operations.ts
89858
+ var MAX_OUTPUT_BYTES2 = 64 * 1024;
89859
+ function clampOutput2(output) {
89860
+ return output.length <= MAX_OUTPUT_BYTES2 ? output : `${output.slice(0, MAX_OUTPUT_BYTES2)}
89861
+
89862
+ [output truncated]`;
89863
+ }
89864
+ function serverlessApp(config6, environment) {
89865
+ return config6.environments?.[environment]?.app;
89866
+ }
89867
+ function buildServerlessOperations(config6, environment, data) {
89868
+ const ops = [];
89869
+ const app = serverlessApp(config6, environment);
89870
+ ops.push({ id: "redeploy", label: "Redeploy current build", group: "deploy", target: "app", mutates: true, confirm: "redeploy" });
89871
+ ops.push({ id: "rollback", label: "Roll back to previous build", group: "deploy", target: "app", mutates: true, confirm: "rollback", danger: true });
89872
+ const maint = data.maintenance?.enabled;
89873
+ if (maint !== true)
89874
+ ops.push({ id: "maintenance:on", label: "Enable maintenance mode", group: "maintenance", target: "app", mutates: true, confirm: "maintenance", danger: true });
89875
+ if (maint !== false)
89876
+ ops.push({ id: "maintenance:off", label: "Disable maintenance mode", group: "maintenance", target: "app", mutates: true, confirm: "live" });
89877
+ if (app?.assets || data.assetsInfo)
89878
+ ops.push({ id: "assets:invalidate", label: "Invalidate CDN cache", group: "assets", target: "assets", mutates: true, confirm: "invalidate" });
89879
+ if (app?.database?.connection === "aurora-serverless") {
89880
+ ops.push({
89881
+ id: "db:scale",
89882
+ label: "Scale Aurora capacity",
89883
+ group: "database",
89884
+ target: "database",
89885
+ mutates: true,
89886
+ confirm: "scale",
89887
+ inputs: [
89888
+ { name: "min", label: "Min ACUs", placeholder: String(app.database.minCapacity ?? 0.5) },
89889
+ { name: "max", label: "Max ACUs", placeholder: String(app.database.maxCapacity ?? 4) }
89890
+ ]
89891
+ });
89892
+ }
89893
+ for (const q of data.queues ?? []) {
89894
+ const short = String(q?.name ?? "").trim();
89895
+ if (short && /^[A-Za-z0-9_-]+$/.test(short))
89896
+ ops.push({ id: `queue:purge:${short}`, label: `Purge queue (${short})`, group: "queue", target: short, mutates: true, confirm: short, danger: true });
89897
+ }
89898
+ return ops;
89899
+ }
89900
+ function resolveServerlessOperation(id, config6, environment, data) {
89901
+ return buildServerlessOperations(config6, environment, data).find((op) => op.id === id);
89902
+ }
89903
+ async function assetsDistributionId(config6, environment, region) {
89904
+ try {
89905
+ const cf = new CloudFormationClient(region);
89906
+ const { Stacks } = await cf.describeStacks({ stackName: resolveServerlessAppStackName(config6, environment) });
89907
+ const outputs = {};
89908
+ for (const o of Stacks?.[0]?.Outputs ?? [])
89909
+ if (o.OutputKey)
89910
+ outputs[o.OutputKey] = o.OutputValue ?? "";
89911
+ const domain = outputs.AssetsCdnDomain;
89912
+ if (!domain)
89913
+ return null;
89914
+ const dist = await new CloudFrontClient(region).findDistributionByDomain(domain);
89915
+ return dist?.Id ?? null;
89916
+ } catch {
89917
+ return null;
89918
+ }
89919
+ }
89920
+ async function runServerlessOperation(config6, environment, operation, options = {}) {
89921
+ try {
89922
+ if (operation.id === "redeploy") {
89923
+ await redeployServerlessApp(config6, environment);
89924
+ return { operation: operation.id, command: "serverless redeploy", ok: true, stdout: "Redeploy complete." };
89925
+ }
89926
+ if (operation.id === "rollback") {
89927
+ await rollbackServerlessApp(config6, environment);
89928
+ return { operation: operation.id, command: "serverless rollback", ok: true, stdout: "Rollback complete." };
89929
+ }
89930
+ if (operation.id === "maintenance:on") {
89931
+ await setMaintenance(config6, environment, true);
89932
+ return { operation: operation.id, command: "maintenance on", ok: true, stdout: "Application is now in maintenance mode." };
89933
+ }
89934
+ if (operation.id === "maintenance:off") {
89935
+ await setMaintenance(config6, environment, false);
89936
+ return { operation: operation.id, command: "maintenance off", ok: true, stdout: "Application is live." };
89937
+ }
89938
+ if (operation.id === "db:scale") {
89939
+ const min = Number(options.min);
89940
+ const max = Number(options.max);
89941
+ if (!Number.isFinite(min) || !Number.isFinite(max) || min <= 0 || max < min)
89942
+ return { operation: operation.id, ok: false, error: "Provide valid Aurora capacities (0 < min ≤ max)." };
89943
+ await scaleServerlessDatabase(config6, environment, min, max);
89944
+ return { operation: operation.id, command: `db:scale ${min}-${max} ACU`, ok: true, stdout: `Scaling applied (${min}-${max} ACUs); takes effect shortly.` };
89945
+ }
89946
+ if (operation.id === "assets:invalidate") {
89947
+ const info2 = await serverlessInfo(config6, environment);
89948
+ const distId = await assetsDistributionId(config6, environment, info2.region);
89949
+ if (!distId)
89950
+ return { operation: operation.id, ok: false, error: "Could not resolve the asset CloudFront distribution for this environment." };
89951
+ const cf = new CloudFrontClient(info2.region);
89952
+ const res = await cf.invalidateAll(distId);
89953
+ return { operation: operation.id, command: `cloudfront invalidate ${distId} /*`, ok: true, stdout: `Invalidation ${res?.Id ?? "created"} for ${distId} (/*).` };
89954
+ }
89955
+ if (operation.group === "queue" && operation.id.startsWith("queue:purge:")) {
89956
+ const short = operation.target;
89957
+ const info2 = await serverlessInfo(config6, environment);
89958
+ const sqs2 = new SQSClient(info2.region);
89959
+ const queueName = `${info2.slug}-${environment}-${short}`;
89960
+ const { QueueUrl } = await sqs2.getQueueUrl(queueName);
89961
+ await sqs2.purgeQueue(QueueUrl);
89962
+ return { operation: operation.id, command: `sqs purge ${queueName}`, ok: true, stdout: `Purged queue ${queueName}.` };
89963
+ }
89964
+ return { operation: operation.id, ok: false, error: "Unknown or unavailable serverless operation." };
89965
+ } catch (error2) {
89966
+ return { operation: operation.id, ok: false, error: clampOutput2(error2?.message ?? String(error2)) };
89967
+ }
89968
+ }
89969
+ async function runServerlessCommand(config6, environment, command) {
89970
+ const trimmed = command.trim();
89971
+ if (!trimmed)
89972
+ return { operation: "command", ok: false, error: "A command is required." };
89973
+ try {
89974
+ const output = await runRemoteCommand(config6, environment, trimmed);
89975
+ return { operation: "command", command: trimmed, ok: true, stdout: clampOutput2(output || "(no output)") };
89976
+ } catch (error2) {
89977
+ return { operation: "command", command: trimmed, ok: false, error: clampOutput2(error2?.message ?? String(error2)) };
89978
+ }
89979
+ }
89980
+ var DLQ_UNAVAILABLE = "No dead-letter queue is provisioned for this environment.";
89981
+ async function dlqUrl(sqs2, slug, environment) {
89982
+ try {
89983
+ const { QueueUrl } = await sqs2.getQueueUrl(`${slug}-${environment}-dlq`);
89984
+ return QueueUrl;
89985
+ } catch {
89986
+ return null;
89987
+ }
89988
+ }
89989
+ async function listDlqMessages(config6, environment, max = 10) {
89990
+ try {
89991
+ const info2 = await serverlessInfo(config6, environment);
89992
+ const sqs2 = new SQSClient(info2.region);
89993
+ const url = await dlqUrl(sqs2, info2.slug, environment);
89994
+ if (!url)
89995
+ return { ok: false, messages: [], error: DLQ_UNAVAILABLE };
89996
+ const received = await sqs2.receiveMessages({
89997
+ queueUrl: url,
89998
+ maxMessages: Math.min(10, Math.max(1, max)),
89999
+ waitTimeSeconds: 1,
90000
+ visibilityTimeout: 2
90001
+ });
90002
+ const messages = (received.Messages ?? []).map((m) => ({
90003
+ id: m.MessageId,
90004
+ receiptHandle: m.ReceiptHandle,
90005
+ body: clampOutput2(String(m.Body ?? ""))
90006
+ }));
90007
+ return { ok: true, messages };
90008
+ } catch (error2) {
90009
+ return { ok: false, messages: [], error: clampOutput2(error2?.message ?? String(error2)) };
90010
+ }
90011
+ }
90012
+ async function redriveDlq(config6, environment, opts = {}) {
90013
+ try {
90014
+ const info2 = await serverlessInfo(config6, environment);
90015
+ const app = serverlessApp(config6, environment);
90016
+ const queues = app ? resolveQueues(app, info2.slug, environment) : [];
90017
+ const shortNames = queues.map((q) => q.name.replace(`${info2.slug}-${environment}-`, ""));
90018
+ const target = opts.targetQueue && shortNames.includes(opts.targetQueue) ? opts.targetQueue : shortNames[0];
90019
+ if (!target)
90020
+ return { operation: "dlq:redrive", ok: false, error: "No source queue is configured to redrive into." };
90021
+ const sqs2 = new SQSClient(info2.region);
90022
+ const dlq = await dlqUrl(sqs2, info2.slug, environment);
90023
+ if (!dlq)
90024
+ return { operation: "dlq:redrive", ok: false, error: DLQ_UNAVAILABLE };
90025
+ const { QueueUrl: targetUrl } = await sqs2.getQueueUrl(`${info2.slug}-${environment}-${target}`);
90026
+ const cap = Math.min(50, Math.max(1, opts.max ?? 10));
90027
+ let moved = 0;
90028
+ while (moved < cap) {
90029
+ const batch2 = await sqs2.receiveMessages({ queueUrl: dlq, maxMessages: Math.min(10, cap - moved), waitTimeSeconds: 1, visibilityTimeout: 30 });
90030
+ const msgs = batch2.Messages ?? [];
90031
+ if (!msgs.length)
90032
+ break;
90033
+ for (const m of msgs) {
90034
+ await sqs2.sendMessage({ queueUrl: targetUrl, messageBody: String(m.Body ?? "") });
90035
+ await sqs2.deleteMessage(dlq, m.ReceiptHandle);
90036
+ moved++;
90037
+ }
90038
+ }
90039
+ return { operation: "dlq:redrive", command: `redrive → ${target}`, ok: true, stdout: `Moved ${moved} message(s) from the DLQ to ${target}.` };
90040
+ } catch (error2) {
90041
+ return { operation: "dlq:redrive", ok: false, error: clampOutput2(error2?.message ?? String(error2)) };
90042
+ }
90043
+ }
90044
+ async function purgeDlq(config6, environment) {
90045
+ try {
90046
+ const info2 = await serverlessInfo(config6, environment);
90047
+ const sqs2 = new SQSClient(info2.region);
90048
+ const url = await dlqUrl(sqs2, info2.slug, environment);
90049
+ if (!url)
90050
+ return { operation: "dlq:purge", ok: false, error: DLQ_UNAVAILABLE };
90051
+ await sqs2.purgeQueue(url);
90052
+ return { operation: "dlq:purge", command: "sqs purge dlq", ok: true, stdout: "Dead-letter queue purged." };
90053
+ } catch (error2) {
90054
+ return { operation: "dlq:purge", ok: false, error: clampOutput2(error2?.message ?? String(error2)) };
90055
+ }
90056
+ }
90057
+ function configuredSecretIds(config6, environment) {
90058
+ const app = serverlessApp(config6, environment);
90059
+ if (!app?.secrets)
90060
+ return [];
90061
+ return Array.isArray(app.secrets) ? app.secrets.map((name) => ({ key: name.split("/").pop().toUpperCase().replace(/[^A-Z0-9_]/g, "_"), secretId: name })) : Object.entries(app.secrets).map(([key, secretId]) => ({ key, secretId: String(secretId) }));
90062
+ }
90063
+ async function setServerlessSecret(config6, environment, secretId, value) {
90064
+ if (!secretId.trim())
90065
+ return { operation: "secret:set", ok: false, error: "A secret id is required." };
90066
+ try {
90067
+ const info2 = await serverlessInfo(config6, environment);
90068
+ const sm = new SecretsManagerClient(info2.region);
90069
+ try {
90070
+ await sm.putSecretValue({ SecretId: secretId, SecretString: value });
90071
+ } catch {
90072
+ await sm.createSecret({ Name: secretId, SecretString: value });
90073
+ }
90074
+ return { operation: "secret:set", command: `secret set ${secretId}`, ok: true, stdout: `Secret ${secretId} updated. Redeploy to apply it to the functions.` };
90075
+ } catch (error2) {
90076
+ return { operation: "secret:set", ok: false, error: clampOutput2(error2?.message ?? String(error2)) };
90077
+ }
90078
+ }
90079
+ async function deleteServerlessSecret(config6, environment, secretId) {
90080
+ if (!secretId.trim())
90081
+ return { operation: "secret:delete", ok: false, error: "A secret id is required." };
90082
+ try {
90083
+ const info2 = await serverlessInfo(config6, environment);
90084
+ const sm = new SecretsManagerClient(info2.region);
90085
+ await sm.deleteSecret({ SecretId: secretId, RecoveryWindowInDays: 7 });
90086
+ return { operation: "secret:delete", command: `secret delete ${secretId}`, ok: true, stdout: `Secret ${secretId} scheduled for deletion (recoverable for 7 days).` };
90087
+ } catch (error2) {
90088
+ return { operation: "secret:delete", ok: false, error: clampOutput2(error2?.message ?? String(error2)) };
90089
+ }
90090
+ }
90091
+ var FUNCTION_MODES = new Set(["http", "queue", "cli"]);
90092
+ async function updateFunctionConfig(config6, environment, mode, opts) {
90093
+ if (!FUNCTION_MODES.has(mode))
90094
+ return { operation: "function:config", ok: false, error: `Unknown function mode '${mode}'.` };
90095
+ const memory = opts.memory == null ? undefined : Number(opts.memory);
90096
+ const timeout = opts.timeout == null ? undefined : Number(opts.timeout);
90097
+ if (memory != null && (!Number.isInteger(memory) || memory < 128 || memory > 10240))
90098
+ return { operation: "function:config", ok: false, error: "Memory must be an integer between 128 and 10240 MB." };
90099
+ if (timeout != null && (!Number.isInteger(timeout) || timeout < 1 || timeout > 900))
90100
+ return { operation: "function:config", ok: false, error: "Timeout must be an integer between 1 and 900 seconds." };
90101
+ if (memory == null && timeout == null)
90102
+ return { operation: "function:config", ok: false, error: "Provide a memory and/or timeout value." };
90103
+ try {
90104
+ const info2 = await serverlessInfo(config6, environment);
90105
+ const lambda2 = new LambdaClient(info2.region);
90106
+ const name = `${info2.slug}-${environment}-${mode}`;
90107
+ await lambda2.updateFunctionConfiguration({
90108
+ FunctionName: name,
90109
+ ...memory != null ? { MemorySize: memory } : {},
90110
+ ...timeout != null ? { Timeout: timeout } : {}
90111
+ });
90112
+ const parts = [memory != null ? `${memory} MB` : "", timeout != null ? `${timeout}s` : ""].filter(Boolean).join(", ");
90113
+ return { operation: "function:config", command: `update ${name} (${parts})`, ok: true, stdout: `Updated ${name}: ${parts}.` };
90114
+ } catch (error2) {
90115
+ return { operation: "function:config", ok: false, error: clampOutput2(error2?.message ?? String(error2)) };
90116
+ }
90117
+ }
90118
+ var ALARM_PRESETS = [
90119
+ { key: "http-errors", label: "HTTP function errors", namespace: "AWS/Lambda", metricName: "Errors", statistic: "Sum", comparison: "GreaterThanThreshold", fnMode: "http", unit: "errors / 5 min" },
90120
+ { key: "http-throttles", label: "HTTP function throttles", namespace: "AWS/Lambda", metricName: "Throttles", statistic: "Sum", comparison: "GreaterThanThreshold", fnMode: "http", unit: "throttles / 5 min" },
90121
+ { key: "http-duration", label: "HTTP function duration (avg)", namespace: "AWS/Lambda", metricName: "Duration", statistic: "Average", comparison: "GreaterThanThreshold", fnMode: "http", unit: "ms" },
90122
+ { key: "http-concurrency", label: "HTTP concurrent executions", namespace: "AWS/Lambda", metricName: "ConcurrentExecutions", statistic: "Maximum", comparison: "GreaterThanThreshold", fnMode: "http", unit: "concurrent" },
90123
+ { key: "queue-errors", label: "Queue function errors", namespace: "AWS/Lambda", metricName: "Errors", statistic: "Sum", comparison: "GreaterThanThreshold", fnMode: "queue", unit: "errors / 5 min" }
90124
+ ];
90125
+ function resolveAlarmPreset(key) {
90126
+ return ALARM_PRESETS.find((p) => p.key === key);
90127
+ }
90128
+ async function listAlarms(config6, environment) {
90129
+ try {
90130
+ const info2 = await serverlessInfo(config6, environment);
90131
+ const cw = new CloudWatchClient(info2.region);
90132
+ const all = await cw.describeAlarms({ AlarmNamePrefix: `${info2.slug}-${environment}-`, MaxRecords: 100 });
90133
+ return { ok: true, alarms: all, presets: ALARM_PRESETS };
90134
+ } catch (error2) {
90135
+ return { ok: false, alarms: [], presets: ALARM_PRESETS, error: clampOutput2(error2?.message ?? String(error2)) };
90136
+ }
90137
+ }
90138
+ async function createAlarm(config6, environment, presetKey, threshold) {
90139
+ const preset = resolveAlarmPreset(presetKey);
90140
+ if (!preset)
90141
+ return { operation: "alarm:create", ok: false, error: `Unknown alarm metric '${presetKey}'.` };
90142
+ if (!Number.isFinite(threshold) || threshold < 0)
90143
+ return { operation: "alarm:create", ok: false, error: "Provide a non-negative threshold." };
90144
+ try {
90145
+ const info2 = await serverlessInfo(config6, environment);
90146
+ const cw = new CloudWatchClient(info2.region);
90147
+ const alarmName = `${info2.slug}-${environment}-${preset.key}`;
90148
+ const dims = preset.fnMode ? [{ Name: "FunctionName", Value: `${info2.slug}-${environment}-${preset.fnMode}` }] : [];
90149
+ await cw.putMetricAlarm({
90150
+ AlarmName: alarmName,
90151
+ Namespace: preset.namespace,
90152
+ MetricName: preset.metricName,
90153
+ ComparisonOperator: preset.comparison,
90154
+ Threshold: threshold,
90155
+ EvaluationPeriods: 1,
90156
+ Period: 300,
90157
+ Statistic: preset.statistic,
90158
+ Dimensions: dims,
90159
+ AlarmDescription: `ts-cloud: ${preset.label} > ${threshold} ${preset.unit}`
90160
+ });
90161
+ return { operation: "alarm:create", command: `alarm ${alarmName}`, ok: true, stdout: `Alarm ${alarmName} armed at ${threshold} ${preset.unit}.` };
90162
+ } catch (error2) {
90163
+ return { operation: "alarm:create", ok: false, error: clampOutput2(error2?.message ?? String(error2)) };
90164
+ }
90165
+ }
90166
+ async function deleteAlarm(config6, environment, alarmName) {
90167
+ try {
90168
+ const info2 = await serverlessInfo(config6, environment);
90169
+ if (!alarmName.startsWith(`${info2.slug}-${environment}-`))
90170
+ return { operation: "alarm:delete", ok: false, error: "Refusing to delete an alarm outside this environment." };
90171
+ const cw = new CloudWatchClient(info2.region);
90172
+ await cw.deleteAlarms([alarmName]);
90173
+ return { operation: "alarm:delete", command: `delete alarm ${alarmName}`, ok: true, stdout: `Alarm ${alarmName} deleted.` };
90174
+ } catch (error2) {
90175
+ return { operation: "alarm:delete", ok: false, error: clampOutput2(error2?.message ?? String(error2)) };
90176
+ }
90177
+ }
90178
+ function shapeTrace(s) {
90179
+ const status = s.HasFault ? "fault" : s.HasError ? "error" : s.HasThrottle ? "throttle" : "ok";
90180
+ return {
90181
+ id: String(s.Id ?? ""),
90182
+ durationMs: Math.round((s.Duration ?? 0) * 1000),
90183
+ responseMs: Math.round((s.ResponseTime ?? 0) * 1000),
90184
+ status,
90185
+ method: s.Http?.HttpMethod ?? "-",
90186
+ url: s.Http?.HttpURL ?? "-",
90187
+ httpStatus: s.Http?.HttpStatus ?? 0
90188
+ };
90189
+ }
90190
+ async function listTraces(config6, environment, minutes = 30) {
90191
+ try {
90192
+ const info2 = await serverlessInfo(config6, environment);
90193
+ const xray = new XRayClient(info2.region);
90194
+ const end = new Date;
90195
+ const start = new Date(end.getTime() - Math.min(360, Math.max(1, minutes)) * 60000);
90196
+ const httpFn = `${info2.slug}-${environment}-http`;
90197
+ let res = await xray.getTraceSummaries({ startTime: start, endTime: end, filterExpression: `service("${httpFn}")` });
90198
+ if (!res.summaries.length)
90199
+ res = await xray.getTraceSummaries({ startTime: start, endTime: end });
90200
+ return { ok: true, traces: res.summaries.slice(0, 100).map(shapeTrace) };
90201
+ } catch (error2) {
90202
+ return { ok: false, traces: [], error: clampOutput2(error2?.message ?? String(error2)) };
90203
+ }
90204
+ }
90205
+ async function controlScheduler(config6, environment, action) {
90206
+ try {
90207
+ const info2 = await serverlessInfo(config6, environment);
90208
+ const ruleName = `${info2.slug}-${environment}-scheduler`;
90209
+ if (action === "run") {
90210
+ const output = await runRemoteCommand(config6, environment, "schedule:run");
90211
+ return { operation: "scheduler:run", command: "schedule:run", ok: true, stdout: clampOutput2(output || "Scheduler run triggered.") };
90212
+ }
90213
+ const eb = new EventBridgeClient(info2.region);
90214
+ if (action === "enable")
90215
+ await eb.enableRule({ Name: ruleName });
90216
+ else
90217
+ await eb.disableRule({ Name: ruleName });
90218
+ return { operation: `scheduler:${action}`, command: `${action} ${ruleName}`, ok: true, stdout: `Scheduler rule ${ruleName} ${action}d.` };
90219
+ } catch (error2) {
90220
+ return { operation: `scheduler:${action}`, ok: false, error: clampOutput2(error2?.message ?? String(error2)) };
90221
+ }
90222
+ }
89526
90223
 
89527
90224
  // src/deploy/site-config-editor.ts
89528
90225
  function addSiteToCloudConfig(input) {
@@ -89612,6 +90309,28 @@ ${entries.map(([key, value]) => ` ${key}: '${escapeSingle(String(value))}
89612
90309
  `)}
89613
90310
  }`;
89614
90311
  }
90312
+ function isValidHostname(value) {
90313
+ return /^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,63}$/i.test(value.trim());
90314
+ }
90315
+ function renderAliasesValue(aliases) {
90316
+ const list = [...new Set(aliases.map((a) => a.trim().toLowerCase()).filter(Boolean))];
90317
+ for (const host of list) {
90318
+ if (!isValidHostname(host))
90319
+ throw new Error(`Alias '${host}' is not a valid hostname.`);
90320
+ }
90321
+ if (list.length === 0)
90322
+ return "[]";
90323
+ return `[${list.map((a) => `'${escapeSingle(a)}'`).join(", ")}]`;
90324
+ }
90325
+ function renderRedirectsValue(redirects) {
90326
+ const entries = Object.entries(redirects).map(([from, to]) => [from.trim(), String(to).trim()]).filter(([from, to]) => from && to);
90327
+ if (entries.length === 0)
90328
+ return "{}";
90329
+ return `{
90330
+ ${entries.map(([from, to]) => ` '${escapeSingle(from)}': '${escapeSingle(to)}',`).join(`
90331
+ `)}
90332
+ }`;
90333
+ }
89615
90334
  function scanValueEnd(text, start, limit) {
89616
90335
  let i = start;
89617
90336
  while (i < limit && /\s/.test(text[i]))
@@ -89788,10 +90507,57 @@ function findMatchingBrace(text, start) {
89788
90507
  throw new Error("Could not find the closing brace for sites: { ... } in cloud.config.ts");
89789
90508
  }
89790
90509
 
90510
+ // src/deploy/terminal-session.ts
90511
+ function createTerminalSession(onData, options = {}) {
90512
+ const shell = options.shell || (process.platform === "win32" ? "powershell.exe" : "bash");
90513
+ const proc = Bun.spawn([shell, "-i"], {
90514
+ stdin: "pipe",
90515
+ stdout: "pipe",
90516
+ stderr: "pipe",
90517
+ cwd: options.cwd,
90518
+ env: { ...process.env, TERM: "xterm-256color", PS1: "\\w $ " }
90519
+ });
90520
+ const decoder = new TextDecoder;
90521
+ const pump = async (stream) => {
90522
+ if (!stream)
90523
+ return;
90524
+ const reader = stream.getReader();
90525
+ try {
90526
+ for (;; ) {
90527
+ const { done, value } = await reader.read();
90528
+ if (done)
90529
+ break;
90530
+ if (value)
90531
+ onData(decoder.decode(value));
90532
+ }
90533
+ } catch {}
90534
+ };
90535
+ pump(proc.stdout);
90536
+ pump(proc.stderr);
90537
+ if (options.onExit)
90538
+ proc.exited.then((code) => options.onExit(code)).catch(() => {});
90539
+ return {
90540
+ write(data) {
90541
+ try {
90542
+ proc.stdin.write(data);
90543
+ proc.stdin.flush();
90544
+ } catch {}
90545
+ },
90546
+ close() {
90547
+ try {
90548
+ proc.stdin.end();
90549
+ } catch {}
90550
+ try {
90551
+ proc.kill();
90552
+ } catch {}
90553
+ }
90554
+ };
90555
+ }
90556
+
89791
90557
  // src/deploy/local-dashboard-server.ts
89792
90558
  var DEFAULT_HOST = "127.0.0.1";
89793
90559
  var DEFAULT_PORT = 7676;
89794
- var MAX_OUTPUT_BYTES2 = 64 * 1024;
90560
+ var MAX_OUTPUT_BYTES3 = 64 * 1024;
89795
90561
  var here = dirname9(fileURLToPath6(import.meta.url));
89796
90562
  var contentTypes = {
89797
90563
  ".html": "text/html; charset=utf-8",
@@ -89871,16 +90637,20 @@ function replaceSiteConfig(config6, name, site) {
89871
90637
  const sites = config6.sites ?? (config6.sites = {});
89872
90638
  sites[name] = site;
89873
90639
  }
90640
+ function computeFirewallPorts(config6) {
90641
+ return normalizePorts(config6.infrastructure?.compute?.firewall?.allowedPorts ?? []);
90642
+ }
90643
+ function replaceFirewallPorts(config6, ports) {
90644
+ const infra = config6.infrastructure ?? (config6.infrastructure = {});
90645
+ const compute = infra.compute ?? (infra.compute = {});
90646
+ const firewall = compute.firewall ?? (compute.firewall = {});
90647
+ firewall.allowedPorts = ports;
90648
+ }
89874
90649
  async function readJsonBody(req) {
89875
90650
  return await req.json().catch(() => ({}));
89876
90651
  }
89877
- function resolveDashboardMode(config6) {
89878
- if (config6.mode === "hybrid")
89879
- return "hybrid";
89880
- return config6.infrastructure?.compute ? "server" : "serverless";
89881
- }
89882
90652
  async function resolveLiveDashboardData(config6, environment) {
89883
- const mode = resolveDashboardMode(config6);
90653
+ const mode = resolveDeploymentMode(config6);
89884
90654
  const meta = { mode, environment, environments: Object.keys(config6.environments ?? {}) };
89885
90655
  try {
89886
90656
  const data = mode === "serverless" ? await resolveDashboardData(config6, environment) : await resolveServerDashboardData(config6, environment);
@@ -90016,10 +90786,10 @@ function dashboardActions(environment) {
90016
90786
  function resolveDashboardAction(id, environment) {
90017
90787
  return dashboardActions(environment).find((action) => action.id === id);
90018
90788
  }
90019
- function clampOutput2(output) {
90020
- if (output.length <= MAX_OUTPUT_BYTES2)
90789
+ function clampOutput3(output) {
90790
+ if (output.length <= MAX_OUTPUT_BYTES3)
90021
90791
  return output;
90022
- return `${output.slice(0, MAX_OUTPUT_BYTES2)}
90792
+ return `${output.slice(0, MAX_OUTPUT_BYTES3)}
90023
90793
 
90024
90794
  [output truncated]`;
90025
90795
  }
@@ -90040,8 +90810,8 @@ async function runAction(action, options) {
90040
90810
  command: ["cloud", ...action.command].join(" "),
90041
90811
  exitCode,
90042
90812
  ok: exitCode === 0,
90043
- stdout: clampOutput2(stdout),
90044
- stderr: clampOutput2(stderr)
90813
+ stdout: clampOutput3(stdout),
90814
+ stderr: clampOutput3(stderr)
90045
90815
  };
90046
90816
  }
90047
90817
  function staticPath(uiRoot, pathname) {
@@ -90089,12 +90859,49 @@ async function startLocalDashboardServer(options = {}) {
90089
90859
  let activeUiRoot = liveUiRoot ?? packagedUi?.uiRoot;
90090
90860
  if (!activeUiRoot)
90091
90861
  throw new Error("ts-cloud dashboard UI not found. Run `bun run build` in ts-cloud or reinstall the package.");
90862
+ const terminalSessions = new WeakMap;
90863
+ const terminalEnabled = process.env.TS_CLOUD_DASHBOARD_TERMINAL !== "0";
90092
90864
  const server = Bun.serve({
90093
90865
  hostname: host,
90094
90866
  port,
90095
- async fetch(req) {
90867
+ websocket: {
90868
+ open(ws) {
90869
+ if (!terminalEnabled) {
90870
+ ws.send(`The web terminal is disabled (TS_CLOUD_DASHBOARD_TERMINAL=0).\r
90871
+ `);
90872
+ ws.close();
90873
+ return;
90874
+ }
90875
+ const session = createTerminalSession((chunk2) => ws.send(chunk2), {
90876
+ cwd,
90877
+ onExit: () => {
90878
+ try {
90879
+ ws.close();
90880
+ } catch {}
90881
+ }
90882
+ });
90883
+ terminalSessions.set(ws, session);
90884
+ ws.send(`Connected to ${host === "127.0.0.1" ? "localhost" : host} shell. This is a line-oriented shell (no full-screen apps).\r
90885
+ `);
90886
+ },
90887
+ message(ws, message) {
90888
+ terminalSessions.get(ws)?.write(typeof message === "string" ? message : new TextDecoder().decode(message));
90889
+ },
90890
+ close(ws) {
90891
+ terminalSessions.get(ws)?.close();
90892
+ terminalSessions.delete(ws);
90893
+ }
90894
+ },
90895
+ async fetch(req, server2) {
90096
90896
  const url = new URL(req.url);
90097
90897
  try {
90898
+ if (url.pathname === "/api/terminal") {
90899
+ if (!terminalEnabled)
90900
+ return json({ ok: false, error: "The web terminal is disabled." }, 403);
90901
+ if (server2.upgrade(req))
90902
+ return;
90903
+ return text("WebSocket upgrade failed", 400);
90904
+ }
90098
90905
  if (url.pathname === "/api/health") {
90099
90906
  return json({
90100
90907
  ok: true,
@@ -90103,6 +90910,7 @@ async function startLocalDashboardServer(options = {}) {
90103
90910
  environments: availableEnvironments,
90104
90911
  uiRoot: activeUiRoot,
90105
90912
  liveData: !!liveUiRoot,
90913
+ terminal: terminalEnabled,
90106
90914
  localPackage: import.meta.url.includes("/Code/Libraries/ts-cloud/")
90107
90915
  });
90108
90916
  }
@@ -90163,6 +90971,42 @@ async function startLocalDashboardServer(options = {}) {
90163
90971
  replaceComputeSshKeys(config6, currentKeys.filter((key) => key.name !== name.trim()));
90164
90972
  return json({ ok: true, configPath, keys: describeSshKeys(computeSshKeys(config6)) });
90165
90973
  }
90974
+ if (url.pathname === "/api/firewall" && req.method === "GET")
90975
+ return json({ configPath, alwaysOpen: [22, 80, 443], ports: computeFirewallPorts(config6) });
90976
+ if (url.pathname === "/api/firewall" && req.method === "POST") {
90977
+ if (!configPath)
90978
+ return json({ ok: false, error: "No cloud config file was found in this checkout." }, 404);
90979
+ const body = await readJsonBody(req);
90980
+ const port2 = Number(body.port);
90981
+ if (!isValidPort(port2))
90982
+ return json({ ok: false, error: "Port must be an integer between 1 and 65535." }, 422);
90983
+ const current = computeFirewallPorts(config6);
90984
+ const before = await readFile2(configPath, "utf8");
90985
+ try {
90986
+ await writeFile6(configPath, addFirewallPort(before, port2, current));
90987
+ } catch (error2) {
90988
+ return json({ ok: false, error: error2?.message ?? String(error2) }, 422);
90989
+ }
90990
+ const ports = normalizePorts([...current, port2]);
90991
+ replaceFirewallPorts(config6, ports);
90992
+ const apply = body.apply === false ? null : await runServerShellCommand(config6, environment, `ufw allow ${port2}/tcp`).catch((e) => ({ ok: false, error: String(e?.message ?? e) }));
90993
+ return json({ ok: true, configPath, ports, apply });
90994
+ }
90995
+ if (url.pathname === "/api/firewall" && req.method === "DELETE") {
90996
+ if (!configPath)
90997
+ return json({ ok: false, error: "No cloud config file was found in this checkout." }, 404);
90998
+ const body = await readJsonBody(req);
90999
+ const port2 = Number(body.port ?? url.searchParams.get("port"));
91000
+ if (!isValidPort(port2))
91001
+ return json({ ok: false, error: "Port must be an integer between 1 and 65535." }, 422);
91002
+ const current = computeFirewallPorts(config6);
91003
+ const before = await readFile2(configPath, "utf8");
91004
+ await writeFile6(configPath, removeFirewallPort(before, port2, current));
91005
+ const ports = normalizePorts(current.filter((p) => p !== port2));
91006
+ replaceFirewallPorts(config6, ports);
91007
+ const apply = body.apply === false ? null : await runServerShellCommand(config6, environment, `ufw delete allow ${port2}/tcp`).catch((e) => ({ ok: false, error: String(e?.message ?? e) }));
91008
+ return json({ ok: true, configPath, ports, apply });
91009
+ }
90166
91010
  if (url.pathname === "/api/sites" && req.method === "POST") {
90167
91011
  if (!configPath)
90168
91012
  return json({ ok: false, error: "No cloud config file was found in this checkout." }, 404);
@@ -90236,7 +91080,16 @@ async function startLocalDashboardServer(options = {}) {
90236
91080
  set("env", renderEnvValue(body.env));
90237
91081
  existing.env = body.env;
90238
91082
  }
90239
- for (const key of ["domain", "path", "build", "start", "type", "root"]) {
91083
+ if (body.aliases !== undefined && Array.isArray(body.aliases)) {
91084
+ const aliases = body.aliases.map((a) => String(a));
91085
+ set("aliases", renderAliasesValue(aliases));
91086
+ existing.aliases = aliases.map((a) => a.trim().toLowerCase()).filter(Boolean);
91087
+ }
91088
+ if (body.redirects !== undefined && body.redirects && typeof body.redirects === "object") {
91089
+ set("redirects", renderRedirectsValue(body.redirects));
91090
+ existing.redirects = body.redirects;
91091
+ }
91092
+ for (const key of ["domain", "path", "build", "start", "type", "root", "php"]) {
90240
91093
  if (typeof body[key] === "string" && body[key].trim()) {
90241
91094
  set(key, renderStringValue(String(body[key]).trim()));
90242
91095
  existing[key] = String(body[key]).trim();
@@ -90279,6 +91132,17 @@ async function startLocalDashboardServer(options = {}) {
90279
91132
  return json({ ok: false, error: "Database name must be a valid identifier (letters, numbers, underscore; not starting with a digit)." }, 422);
90280
91133
  return json({ ...await createDatabase(config6, environment, name), name });
90281
91134
  }
91135
+ if (url.pathname === "/api/databases/backups" && req.method === "GET")
91136
+ return json(await listDatabaseBackups(config6, environment));
91137
+ if (url.pathname === "/api/databases/backup" && req.method === "POST") {
91138
+ const body = await readJsonBody(req);
91139
+ const name = String(body.database ?? "").trim();
91140
+ if (!isValidDbIdentifier(name))
91141
+ return json({ ok: false, error: "Database name must be a valid identifier." }, 422);
91142
+ if (body.confirm !== name)
91143
+ return json({ ok: false, error: `Type "${name}" to back up this database.` }, 409);
91144
+ return json(await backupDatabase(config6, environment, name));
91145
+ }
90282
91146
  if (url.pathname === "/api/databases/users" && req.method === "POST") {
90283
91147
  const body = await readJsonBody(req);
90284
91148
  const username = String(body.username ?? "").trim();
@@ -90315,6 +91179,108 @@ async function startLocalDashboardServer(options = {}) {
90315
91179
  return json({ ok: false, error: `Type "${operation.confirm}" to run this operation.` }, 409);
90316
91180
  return json(await runDashboardOperation(config6, environment, operation, { to: body.to }));
90317
91181
  }
91182
+ if (url.pathname === "/api/server/command" && req.method === "POST") {
91183
+ const body = await req.json().catch(() => ({}));
91184
+ if (body.confirm !== "run")
91185
+ return json({ ok: false, error: 'Type "run" to execute this command on the server.' }, 409);
91186
+ return json(await runServerShellCommand(config6, environment, String(body.command ?? "")));
91187
+ }
91188
+ if (url.pathname === "/api/serverless/operations")
91189
+ return json(buildServerlessOperations(config6, environment, latestData));
91190
+ if (url.pathname === "/api/serverless/operations/run" && req.method === "POST") {
91191
+ const body = await req.json().catch(() => ({}));
91192
+ const operation = body.operation ? resolveServerlessOperation(body.operation, config6, environment, latestData) : undefined;
91193
+ if (!operation)
91194
+ return json({ ok: false, error: "Unknown or unavailable serverless operation." }, 404);
91195
+ if (operation.mutates && body.confirm !== operation.confirm)
91196
+ return json({ ok: false, error: `Type "${operation.confirm}" to run this operation.` }, 409);
91197
+ const result = await runServerlessOperation(config6, environment, operation, { min: body.min, max: body.max });
91198
+ latestData = await resolveLiveDashboardData(config6, environment);
91199
+ return json(result);
91200
+ }
91201
+ if (url.pathname === "/api/serverless/command" && req.method === "POST") {
91202
+ const body = await req.json().catch(() => ({}));
91203
+ if (body.confirm !== "run")
91204
+ return json({ ok: false, error: 'Type "run" to execute this command.' }, 409);
91205
+ return json(await runServerlessCommand(config6, environment, String(body.command ?? "")));
91206
+ }
91207
+ if (url.pathname === "/api/serverless/dlq" && req.method === "GET") {
91208
+ const max = Number(url.searchParams.get("max")) || 10;
91209
+ return json(await listDlqMessages(config6, environment, max));
91210
+ }
91211
+ if (url.pathname === "/api/serverless/dlq/redrive" && req.method === "POST") {
91212
+ const body = await req.json().catch(() => ({}));
91213
+ if (body.confirm !== "redrive")
91214
+ return json({ ok: false, error: 'Type "redrive" to move messages back onto a source queue.' }, 409);
91215
+ return json(await redriveDlq(config6, environment, { max: body.max, targetQueue: body.targetQueue }));
91216
+ }
91217
+ if (url.pathname === "/api/serverless/dlq/purge" && req.method === "POST") {
91218
+ const body = await req.json().catch(() => ({}));
91219
+ if (body.confirm !== "purge")
91220
+ return json({ ok: false, error: 'Type "purge" to permanently discard every DLQ message.' }, 409);
91221
+ return json(await purgeDlq(config6, environment));
91222
+ }
91223
+ if (url.pathname === "/api/serverless/secrets" && req.method === "GET")
91224
+ return json({ secrets: configuredSecretIds(config6, environment) });
91225
+ if (url.pathname === "/api/serverless/secrets" && req.method === "POST") {
91226
+ const body = await req.json().catch(() => ({}));
91227
+ const secretId = String(body.secretId ?? "").trim();
91228
+ if (!secretId)
91229
+ return json({ ok: false, error: "A secret id is required." }, 422);
91230
+ if (typeof body.value !== "string" || body.value === "")
91231
+ return json({ ok: false, error: "A non-empty value is required." }, 422);
91232
+ return json(await setServerlessSecret(config6, environment, secretId, body.value));
91233
+ }
91234
+ if (url.pathname === "/api/serverless/secrets" && req.method === "DELETE") {
91235
+ const body = await req.json().catch(() => ({}));
91236
+ const secretId = String(body.secretId ?? "").trim();
91237
+ if (!secretId)
91238
+ return json({ ok: false, error: "A secret id is required." }, 422);
91239
+ if (body.confirm !== secretId)
91240
+ return json({ ok: false, error: `Type "${secretId}" to delete this secret.` }, 409);
91241
+ return json(await deleteServerlessSecret(config6, environment, secretId));
91242
+ }
91243
+ if (url.pathname === "/api/serverless/functions/config" && req.method === "POST") {
91244
+ const body = await req.json().catch(() => ({}));
91245
+ const mode = String(body.mode ?? "").trim();
91246
+ if (body.confirm !== mode)
91247
+ return json({ ok: false, error: `Type "${mode}" to update this function.` }, 409);
91248
+ return json(await updateFunctionConfig(config6, environment, mode, { memory: body.memory, timeout: body.timeout }));
91249
+ }
91250
+ if (url.pathname === "/api/serverless/alarms" && req.method === "GET")
91251
+ return json(await listAlarms(config6, environment));
91252
+ if (url.pathname === "/api/serverless/alarms" && req.method === "POST") {
91253
+ const body = await req.json().catch(() => ({}));
91254
+ const preset = String(body.preset ?? "").trim();
91255
+ const threshold = Number(body.threshold);
91256
+ if (!preset)
91257
+ return json({ ok: false, error: "An alarm metric is required." }, 422);
91258
+ if (!Number.isFinite(threshold))
91259
+ return json({ ok: false, error: "A numeric threshold is required." }, 422);
91260
+ return json(await createAlarm(config6, environment, preset, threshold));
91261
+ }
91262
+ if (url.pathname === "/api/serverless/alarms" && req.method === "DELETE") {
91263
+ const body = await req.json().catch(() => ({}));
91264
+ const name = String(body.name ?? "").trim();
91265
+ if (!name)
91266
+ return json({ ok: false, error: "An alarm name is required." }, 422);
91267
+ if (body.confirm !== name)
91268
+ return json({ ok: false, error: `Type "${name}" to delete this alarm.` }, 409);
91269
+ return json(await deleteAlarm(config6, environment, name));
91270
+ }
91271
+ if (url.pathname === "/api/serverless/traces" && req.method === "GET") {
91272
+ const minutes = Number(url.searchParams.get("minutes")) || 30;
91273
+ return json(await listTraces(config6, environment, minutes));
91274
+ }
91275
+ if (url.pathname === "/api/serverless/scheduler" && req.method === "POST") {
91276
+ const body = await req.json().catch(() => ({}));
91277
+ const action = String(body.action ?? "").trim();
91278
+ if (action !== "enable" && action !== "disable" && action !== "run")
91279
+ return json({ ok: false, error: "Unknown scheduler action." }, 422);
91280
+ if (body.confirm !== action)
91281
+ return json({ ok: false, error: `Type "${action}" to ${action} the scheduler.` }, 409);
91282
+ return json(await controlScheduler(config6, environment, action));
91283
+ }
90318
91284
  const requestedEnv = url.searchParams.get("env");
90319
91285
  if (requestedEnv && availableEnvironments.includes(requestedEnv) && requestedEnv !== environment) {
90320
91286
  environment = requestedEnv;
@@ -90393,6 +91359,7 @@ export {
90393
91359
  rollbackServerlessApp,
90394
91360
  responseToResult,
90395
91361
  resourceManagementManager,
91362
+ resolveUiSource,
90396
91363
  resolveStorageBucketName,
90397
91364
  resolveSiteStackName,
90398
91365
  resolveSiteResourceName,
@@ -90411,8 +91378,10 @@ export {
90411
91378
  resolveManagementDashboardSite,
90412
91379
  resolveHetznerApiToken,
90413
91380
  resolveExecStart,
91381
+ resolveDeploymentMode,
90414
91382
  resolveDeployBucketName,
90415
91383
  resolveDashboardDomain,
91384
+ resolveDashboardAuth,
90416
91385
  resolveDashboardAction,
90417
91386
  resolveCredentials,
90418
91387
  resolveCloudProvider,
@@ -90558,6 +91527,7 @@ export {
90558
91527
  fifoQueueManager,
90559
91528
  extendPreset,
90560
91529
  eventToRequest,
91530
+ ensureManagementDashboard,
90561
91531
  emailTemplateManager,
90562
91532
  emailAnalyticsManager,
90563
91533
  drManager,
@@ -90568,6 +91538,8 @@ export {
90568
91538
  detectServiceRegion,
90569
91539
  detectMisconfigurations,
90570
91540
  detectDnsProvider,
91541
+ detectDeploymentTargets,
91542
+ deploymentCoexistenceError,
90571
91543
  deployStaticSiteWithExternalDnsFull,
90572
91544
  deployStaticSiteWithExternalDns,
90573
91545
  deployStaticSiteFull,
@@ -90637,6 +91609,7 @@ export {
90637
91609
  buildPhpRuntimeLayerZip,
90638
91610
  buildOptimizationManager,
90639
91611
  buildNodeRuntimeLayerZip,
91612
+ buildManagementDashboardArtifact,
90640
91613
  buildFunctionEnv,
90641
91614
  buildCloudFormationTemplate,
90642
91615
  buildBunRuntimeLayerZip,
@@ -90744,6 +91717,7 @@ export {
90744
91717
  MigrationManager,
90745
91718
  MetricsManager,
90746
91719
  Messaging,
91720
+ MANAGEMENT_DASHBOARD_SITE,
90747
91721
  MANAGED_NODE_VERSIONS,
90748
91722
  LogsManager,
90749
91723
  LambdaVersionsManager,
@@ -90797,6 +91771,7 @@ export {
90797
91771
  DNS,
90798
91772
  DLQMonitoringManager,
90799
91773
  DEFAULT_SERVICE_LIMITS,
91774
+ DASHBOARD_CREDENTIALS_FILE,
90800
91775
  CrossRegionReferenceManager,
90801
91776
  CredentialError,
90802
91777
  ContainerRegistryManager,