@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.
- package/dist/aws/cloudwatch.d.ts +2 -0
- package/dist/aws/xray.d.ts +45 -0
- package/dist/bin/cli.js +718 -701
- package/dist/deploy/dashboard-database.d.ts +25 -0
- package/dist/deploy/dashboard-operations.d.ts +7 -0
- package/dist/deploy/firewall-config-editor.d.ts +15 -0
- package/dist/deploy/firewall-config-editor.test.d.ts +1 -0
- package/dist/deploy/index.d.ts +1 -0
- package/dist/deploy/management-dashboard.d.ts +26 -3
- package/dist/deploy/serverless-operations.d.ts +150 -0
- package/dist/deploy/serverless-operations.test.d.ts +1 -0
- package/dist/deploy/site-config-editor.d.ts +6 -0
- package/dist/deploy/ssh-config-editor.d.ts +1 -0
- package/dist/deploy/terminal-session.d.ts +23 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1003 -33
- package/dist/ui/index.html +4 -4
- package/dist/ui/server/actions.html +34 -5
- package/dist/ui/server/activity.html +1 -1
- package/dist/ui/server/backups.html +4 -4
- package/dist/ui/server/database.html +27 -5
- package/dist/ui/server/deployments.html +4 -4
- package/dist/ui/server/diagnostics.html +1 -1
- package/dist/ui/server/firewall.html +1233 -0
- package/dist/ui/server/logs.html +4 -4
- package/dist/ui/server/metrics.html +1 -1
- package/dist/ui/server/security.html +1 -1
- package/dist/ui/server/services.html +4 -4
- package/dist/ui/server/sites.html +24 -5
- package/dist/ui/server/ssh-keys.html +4 -4
- package/dist/ui/server/terminal.html +1191 -0
- package/dist/ui/server/workers.html +4 -4
- package/dist/ui/serverless/alarms.html +1236 -0
- package/dist/ui/serverless/assets.html +53 -1
- package/dist/ui/serverless/cost.html +1 -1
- package/dist/ui/serverless/data.html +58 -1
- package/dist/ui/serverless/deployments.html +53 -1
- package/dist/ui/serverless/firewall.html +1 -1
- package/dist/ui/serverless/functions.html +63 -1
- package/dist/ui/serverless/logs.html +4 -4
- package/dist/ui/serverless/metrics.html +1 -1
- package/dist/ui/serverless/queues.html +94 -1
- package/dist/ui/serverless/scheduler.html +55 -1
- package/dist/ui/serverless/secrets.html +99 -1
- package/dist/ui/serverless/traces.html +1177 -0
- package/dist/ui/serverless.html +87 -1
- package/dist/ui-src/pages/index.stx +1 -1
- package/dist/ui-src/pages/partials/head.stx +6 -6
- package/dist/ui-src/pages/partials/nav.stx +4 -3
- package/dist/ui-src/pages/server/actions.stx +34 -1
- package/dist/ui-src/pages/server/backups.stx +1 -1
- package/dist/ui-src/pages/server/database.stx +32 -9
- package/dist/ui-src/pages/server/deployments.stx +2 -2
- package/dist/ui-src/pages/server/firewall.stx +134 -0
- package/dist/ui-src/pages/server/logs.stx +2 -2
- package/dist/ui-src/pages/server/metrics.stx +5 -5
- package/dist/ui-src/pages/server/security.stx +1 -1
- package/dist/ui-src/pages/server/services.stx +1 -1
- package/dist/ui-src/pages/server/sites.stx +24 -3
- package/dist/ui-src/pages/server/ssh-keys.stx +2 -2
- package/dist/ui-src/pages/server/terminal.stx +65 -0
- package/dist/ui-src/pages/server/workers.stx +1 -1
- package/dist/ui-src/pages/serverless/alarms.stx +153 -0
- package/dist/ui-src/pages/serverless/assets.stx +29 -2
- package/dist/ui-src/pages/serverless/cost.stx +4 -4
- package/dist/ui-src/pages/serverless/data.stx +56 -3
- package/dist/ui-src/pages/serverless/deployments.stx +30 -6
- package/dist/ui-src/pages/serverless/firewall.stx +1 -1
- package/dist/ui-src/pages/serverless/functions.stx +62 -4
- package/dist/ui-src/pages/serverless/logs.stx +2 -2
- package/dist/ui-src/pages/serverless/metrics.stx +5 -1
- package/dist/ui-src/pages/serverless/queues.stx +90 -26
- package/dist/ui-src/pages/serverless/scheduler.stx +31 -2
- package/dist/ui-src/pages/serverless/secrets.stx +94 -14
- package/dist/ui-src/pages/serverless/traces.stx +88 -0
- package/dist/ui-src/pages/serverless.stx +87 -24
- 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: "
|
|
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"
|
|
9733
|
-
errors.push("Invalid mode: must be one of server
|
|
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"
|
|
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
|
|
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 {
|
|
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
|
|
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
|
|
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
|
-
|
|
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-generated — see ${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(
|
|
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
|
-
|
|
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
|
-
|
|
89507
|
-
|
|
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
|
-
|
|
89510
|
-
|
|
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
|
|
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 =
|
|
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
|
|
90025
|
-
if (output.length <=
|
|
90789
|
+
function clampOutput3(output) {
|
|
90790
|
+
if (output.length <= MAX_OUTPUT_BYTES3)
|
|
90026
90791
|
return output;
|
|
90027
|
-
return `${output.slice(0,
|
|
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:
|
|
90049
|
-
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
|
-
|
|
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
|
-
|
|
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,
|