@stacksjs/ts-cloud 0.5.35 → 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 +718 -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 +1003 -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
@@ -87401,11 +87472,42 @@ await init_dist2();
87401
87472
  // src/deploy/management-dashboard.ts
87402
87473
  await init_dist2();
87403
87474
  import { execSync as execSync5 } from "node:child_process";
87404
- 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";
87405
87477
  import { tmpdir as tmpdir6 } from "node:os";
87406
87478
  import { dirname as dirname8, isAbsolute as isAbsolute5, join as join20 } from "node:path";
87407
87479
  import { fileURLToPath as fileURLToPath4 } from "node:url";
87408
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
+ }
87409
87511
  var noopLogger = { info: () => {}, warn: () => {} };
87410
87512
  function truthy(v) {
87411
87513
  return v != null && v !== "" && v !== "0" && v.toLowerCase() !== "false";
@@ -87441,7 +87543,8 @@ function ensureManagementDashboard(config6, options = {}) {
87441
87543
  logger4.info("Management dashboard: UI not found (no local ui/ or packaged dist/ui) — skipping auto-deploy.");
87442
87544
  return config6;
87443
87545
  }
87444
- 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);
87445
87548
  const environment = config6.environments && Object.keys(config6.environments)[0];
87446
87549
  const live = truthy(process.env.TS_CLOUD_UI_LIVE);
87447
87550
  const port = Number(process.env.TS_CLOUD_UI_PORT) || undefined;
@@ -87449,8 +87552,8 @@ function ensureManagementDashboard(config6, options = {}) {
87449
87552
  uiRoot: ui.uiRoot,
87450
87553
  build: ui.build,
87451
87554
  domain: process.env.TS_CLOUD_UI_DOMAIN?.trim() || undefined,
87452
- username: process.env.TS_CLOUD_UI_USERNAME?.trim() || undefined,
87453
- password,
87555
+ username,
87556
+ password: auth.password,
87454
87557
  realm: process.env.TS_CLOUD_UI_REALM?.trim() || undefined,
87455
87558
  live,
87456
87559
  port
@@ -87460,7 +87563,8 @@ function ensureManagementDashboard(config6, options = {}) {
87460
87563
  return config6;
87461
87564
  }
87462
87565
  config6.sites = { ...config6.sites ?? {}, [resolved.name]: resolved.site };
87463
- 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})`);
87464
87568
  return config6;
87465
87569
  }
87466
87570
  function buildManagementDashboardArtifact(site, options) {
@@ -88530,7 +88634,7 @@ function loadLocalState(config6, environment) {
88530
88634
  if (!existsSync24(statePath))
88531
88635
  return null;
88532
88636
  try {
88533
- return JSON.parse(readFileSync17(statePath, "utf8"));
88637
+ return JSON.parse(readFileSync18(statePath, "utf8"));
88534
88638
  } catch {
88535
88639
  return null;
88536
88640
  }
@@ -89039,6 +89143,9 @@ function resolveConfigOnlyServerDashboardData(config6, environment) {
89039
89143
  root: kind === "server-static" ? `/var/www/${s.name}` : `/var/www/${s.name}/current`,
89040
89144
  branch: kind === "server-static" ? "build artifact" : "main",
89041
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 : {},
89042
89149
  envKeys: Object.keys(site.env ?? site.environment ?? {})
89043
89150
  };
89044
89151
  }),
@@ -89312,6 +89419,45 @@ async function createDatabaseUser(config6, environment, input) {
89312
89419
  const engine = resolveDbEngine(config6);
89313
89420
  return runDb(config6, environment, buildCreateUserScript(engine, input), `ts-cloud db:user ${input.username}`);
89314
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
+ }
89315
89461
 
89316
89462
  // src/drivers/shared/compute-ops.ts
89317
89463
  var noopLogger3 = {
@@ -89430,8 +89576,17 @@ function buildDashboardOperations(config6, data) {
89430
89576
  for (const [name, site] of Object.entries(config6.sites ?? {})) {
89431
89577
  if (!site || !isSafeSiteName(name))
89432
89578
  continue;
89433
- if (site.scheduler || site.schedule)
89434
- 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
+ }
89435
89590
  }
89436
89591
  const backups = config6.infrastructure?.compute?.backups;
89437
89592
  if (backups?.enabled) {
@@ -89459,6 +89614,28 @@ function siteArtisanCommand(site, artisan) {
89459
89614
  `cd ${current} && ${pantryEnvEval()} && php artisan ${artisan}`
89460
89615
  ];
89461
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
+ }
89462
89639
  function backupRunCommand() {
89463
89640
  return [
89464
89641
  "set -uo pipefail",
@@ -89503,11 +89680,13 @@ async function runDashboardOperation(config6, environment, operation, options =
89503
89680
  commands = serviceCommand(verb, rest.join(":"));
89504
89681
  command = `systemctl ${verb} ${operation.target}`;
89505
89682
  } else if (operation.group === "worker") {
89506
- commands = siteArtisanCommand(operation.target, "queue:restart");
89507
- 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})`;
89508
89686
  } else if (operation.group === "scheduler") {
89509
- commands = siteArtisanCommand(operation.target, "schedule:run");
89510
- 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})`;
89511
89690
  } else if (operation.id === "backup:run") {
89512
89691
  commands = backupRunCommand();
89513
89692
  command = "ts-cloud backup";
@@ -89528,6 +89707,519 @@ async function runDashboardOperation(config6, environment, operation, options =
89528
89707
  stderr: clampOutput(result.perInstance?.[0]?.error ?? result.error ?? "")
89529
89708
  };
89530
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
+ }
89531
90223
 
89532
90224
  // src/deploy/site-config-editor.ts
89533
90225
  function addSiteToCloudConfig(input) {
@@ -89617,6 +90309,28 @@ ${entries.map(([key, value]) => ` ${key}: '${escapeSingle(String(value))}
89617
90309
  `)}
89618
90310
  }`;
89619
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
+ }
89620
90334
  function scanValueEnd(text, start, limit) {
89621
90335
  let i = start;
89622
90336
  while (i < limit && /\s/.test(text[i]))
@@ -89793,10 +90507,57 @@ function findMatchingBrace(text, start) {
89793
90507
  throw new Error("Could not find the closing brace for sites: { ... } in cloud.config.ts");
89794
90508
  }
89795
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
+
89796
90557
  // src/deploy/local-dashboard-server.ts
89797
90558
  var DEFAULT_HOST = "127.0.0.1";
89798
90559
  var DEFAULT_PORT = 7676;
89799
- var MAX_OUTPUT_BYTES2 = 64 * 1024;
90560
+ var MAX_OUTPUT_BYTES3 = 64 * 1024;
89800
90561
  var here = dirname9(fileURLToPath6(import.meta.url));
89801
90562
  var contentTypes = {
89802
90563
  ".html": "text/html; charset=utf-8",
@@ -89876,16 +90637,20 @@ function replaceSiteConfig(config6, name, site) {
89876
90637
  const sites = config6.sites ?? (config6.sites = {});
89877
90638
  sites[name] = site;
89878
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
+ }
89879
90649
  async function readJsonBody(req) {
89880
90650
  return await req.json().catch(() => ({}));
89881
90651
  }
89882
- function resolveDashboardMode(config6) {
89883
- if (config6.mode === "hybrid")
89884
- return "hybrid";
89885
- return config6.infrastructure?.compute ? "server" : "serverless";
89886
- }
89887
90652
  async function resolveLiveDashboardData(config6, environment) {
89888
- const mode = resolveDashboardMode(config6);
90653
+ const mode = resolveDeploymentMode(config6);
89889
90654
  const meta = { mode, environment, environments: Object.keys(config6.environments ?? {}) };
89890
90655
  try {
89891
90656
  const data = mode === "serverless" ? await resolveDashboardData(config6, environment) : await resolveServerDashboardData(config6, environment);
@@ -90021,10 +90786,10 @@ function dashboardActions(environment) {
90021
90786
  function resolveDashboardAction(id, environment) {
90022
90787
  return dashboardActions(environment).find((action) => action.id === id);
90023
90788
  }
90024
- function clampOutput2(output) {
90025
- if (output.length <= MAX_OUTPUT_BYTES2)
90789
+ function clampOutput3(output) {
90790
+ if (output.length <= MAX_OUTPUT_BYTES3)
90026
90791
  return output;
90027
- return `${output.slice(0, MAX_OUTPUT_BYTES2)}
90792
+ return `${output.slice(0, MAX_OUTPUT_BYTES3)}
90028
90793
 
90029
90794
  [output truncated]`;
90030
90795
  }
@@ -90045,8 +90810,8 @@ async function runAction(action, options) {
90045
90810
  command: ["cloud", ...action.command].join(" "),
90046
90811
  exitCode,
90047
90812
  ok: exitCode === 0,
90048
- stdout: clampOutput2(stdout),
90049
- stderr: clampOutput2(stderr)
90813
+ stdout: clampOutput3(stdout),
90814
+ stderr: clampOutput3(stderr)
90050
90815
  };
90051
90816
  }
90052
90817
  function staticPath(uiRoot, pathname) {
@@ -90094,12 +90859,49 @@ async function startLocalDashboardServer(options = {}) {
90094
90859
  let activeUiRoot = liveUiRoot ?? packagedUi?.uiRoot;
90095
90860
  if (!activeUiRoot)
90096
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";
90097
90864
  const server = Bun.serve({
90098
90865
  hostname: host,
90099
90866
  port,
90100
- 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) {
90101
90896
  const url = new URL(req.url);
90102
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
+ }
90103
90905
  if (url.pathname === "/api/health") {
90104
90906
  return json({
90105
90907
  ok: true,
@@ -90108,6 +90910,7 @@ async function startLocalDashboardServer(options = {}) {
90108
90910
  environments: availableEnvironments,
90109
90911
  uiRoot: activeUiRoot,
90110
90912
  liveData: !!liveUiRoot,
90913
+ terminal: terminalEnabled,
90111
90914
  localPackage: import.meta.url.includes("/Code/Libraries/ts-cloud/")
90112
90915
  });
90113
90916
  }
@@ -90168,6 +90971,42 @@ async function startLocalDashboardServer(options = {}) {
90168
90971
  replaceComputeSshKeys(config6, currentKeys.filter((key) => key.name !== name.trim()));
90169
90972
  return json({ ok: true, configPath, keys: describeSshKeys(computeSshKeys(config6)) });
90170
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
+ }
90171
91010
  if (url.pathname === "/api/sites" && req.method === "POST") {
90172
91011
  if (!configPath)
90173
91012
  return json({ ok: false, error: "No cloud config file was found in this checkout." }, 404);
@@ -90241,7 +91080,16 @@ async function startLocalDashboardServer(options = {}) {
90241
91080
  set("env", renderEnvValue(body.env));
90242
91081
  existing.env = body.env;
90243
91082
  }
90244
- 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"]) {
90245
91093
  if (typeof body[key] === "string" && body[key].trim()) {
90246
91094
  set(key, renderStringValue(String(body[key]).trim()));
90247
91095
  existing[key] = String(body[key]).trim();
@@ -90284,6 +91132,17 @@ async function startLocalDashboardServer(options = {}) {
90284
91132
  return json({ ok: false, error: "Database name must be a valid identifier (letters, numbers, underscore; not starting with a digit)." }, 422);
90285
91133
  return json({ ...await createDatabase(config6, environment, name), name });
90286
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
+ }
90287
91146
  if (url.pathname === "/api/databases/users" && req.method === "POST") {
90288
91147
  const body = await readJsonBody(req);
90289
91148
  const username = String(body.username ?? "").trim();
@@ -90320,6 +91179,108 @@ async function startLocalDashboardServer(options = {}) {
90320
91179
  return json({ ok: false, error: `Type "${operation.confirm}" to run this operation.` }, 409);
90321
91180
  return json(await runDashboardOperation(config6, environment, operation, { to: body.to }));
90322
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
+ }
90323
91284
  const requestedEnv = url.searchParams.get("env");
90324
91285
  if (requestedEnv && availableEnvironments.includes(requestedEnv) && requestedEnv !== environment) {
90325
91286
  environment = requestedEnv;
@@ -90398,6 +91359,7 @@ export {
90398
91359
  rollbackServerlessApp,
90399
91360
  responseToResult,
90400
91361
  resourceManagementManager,
91362
+ resolveUiSource,
90401
91363
  resolveStorageBucketName,
90402
91364
  resolveSiteStackName,
90403
91365
  resolveSiteResourceName,
@@ -90416,8 +91378,10 @@ export {
90416
91378
  resolveManagementDashboardSite,
90417
91379
  resolveHetznerApiToken,
90418
91380
  resolveExecStart,
91381
+ resolveDeploymentMode,
90419
91382
  resolveDeployBucketName,
90420
91383
  resolveDashboardDomain,
91384
+ resolveDashboardAuth,
90421
91385
  resolveDashboardAction,
90422
91386
  resolveCredentials,
90423
91387
  resolveCloudProvider,
@@ -90563,6 +91527,7 @@ export {
90563
91527
  fifoQueueManager,
90564
91528
  extendPreset,
90565
91529
  eventToRequest,
91530
+ ensureManagementDashboard,
90566
91531
  emailTemplateManager,
90567
91532
  emailAnalyticsManager,
90568
91533
  drManager,
@@ -90573,6 +91538,8 @@ export {
90573
91538
  detectServiceRegion,
90574
91539
  detectMisconfigurations,
90575
91540
  detectDnsProvider,
91541
+ detectDeploymentTargets,
91542
+ deploymentCoexistenceError,
90576
91543
  deployStaticSiteWithExternalDnsFull,
90577
91544
  deployStaticSiteWithExternalDns,
90578
91545
  deployStaticSiteFull,
@@ -90642,6 +91609,7 @@ export {
90642
91609
  buildPhpRuntimeLayerZip,
90643
91610
  buildOptimizationManager,
90644
91611
  buildNodeRuntimeLayerZip,
91612
+ buildManagementDashboardArtifact,
90645
91613
  buildFunctionEnv,
90646
91614
  buildCloudFormationTemplate,
90647
91615
  buildBunRuntimeLayerZip,
@@ -90749,6 +91717,7 @@ export {
90749
91717
  MigrationManager,
90750
91718
  MetricsManager,
90751
91719
  Messaging,
91720
+ MANAGEMENT_DASHBOARD_SITE,
90752
91721
  MANAGED_NODE_VERSIONS,
90753
91722
  LogsManager,
90754
91723
  LambdaVersionsManager,
@@ -90802,6 +91771,7 @@ export {
90802
91771
  DNS,
90803
91772
  DLQMonitoringManager,
90804
91773
  DEFAULT_SERVICE_LIMITS,
91774
+ DASHBOARD_CREDENTIALS_FILE,
90805
91775
  CrossRegionReferenceManager,
90806
91776
  CredentialError,
90807
91777
  ContainerRegistryManager,