@stacksjs/ts-cloud 0.7.92 → 0.7.94

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 (69) hide show
  1. package/dist/bin/cli.js +1235 -1183
  2. package/dist/bin/dashboard-server.js +451 -399
  3. package/dist/{chunk-29fwqkb7.js → chunk-0htae1c2.js} +456 -21
  4. package/dist/{chunk-fbwcv2vf.js → chunk-bs3jfnzp.js} +451 -41
  5. package/dist/control-plane/store.d.ts +1 -0
  6. package/dist/deploy/dashboard-database.d.ts +91 -3
  7. package/dist/deploy/dashboard-vitess.d.ts +154 -0
  8. package/dist/deploy/dashboard-vitess.test.d.ts +1 -0
  9. package/dist/deploy/index.js +2 -2
  10. package/dist/drivers/hetzner/client.d.ts +96 -0
  11. package/dist/drivers/hetzner/migrate-server.d.ts +124 -0
  12. package/dist/drivers/hetzner/migrate-server.test.d.ts +1 -0
  13. package/dist/drivers/hetzner/role-swap.d.ts +139 -0
  14. package/dist/drivers/hetzner/role-swap.test.d.ts +1 -0
  15. package/dist/drivers/index.js +1 -1
  16. package/dist/drivers/shared/db-provision.d.ts +4 -0
  17. package/dist/drivers/shared/deploy-script.d.ts +17 -1
  18. package/dist/drivers/shared/package-manager.d.ts +2 -0
  19. package/dist/drivers/shared/releases.d.ts +41 -2
  20. package/dist/drivers/shared/vitess-provision.d.ts +133 -0
  21. package/dist/drivers/shared/vitess-provision.test.d.ts +1 -0
  22. package/dist/index.d.ts +1 -1
  23. package/dist/index.js +6 -2
  24. package/dist/telemetry/store.d.ts +1 -0
  25. package/dist/ui/account/automation.html +4 -4
  26. package/dist/ui/account/security.html +2 -2
  27. package/dist/ui/applications/compose.html +4 -4
  28. package/dist/ui/data/backups.html +4 -4
  29. package/dist/ui/data/services.html +4 -4
  30. package/dist/ui/data/volumes.html +3 -3
  31. package/dist/ui/index.html +4 -4
  32. package/dist/ui/integrations.html +2 -2
  33. package/dist/ui/operations/alerts.html +4 -4
  34. package/dist/ui/operations/configuration.html +4 -4
  35. package/dist/ui/operations/jobs.html +4 -4
  36. package/dist/ui/operations/maintenance.html +3 -3
  37. package/dist/ui/operations/observability.html +4 -4
  38. package/dist/ui/operations/previews.html +4 -4
  39. package/dist/ui/operations/queue.html +4 -4
  40. package/dist/ui/operations/regions.html +4 -4
  41. package/dist/ui/operations/releases.html +4 -4
  42. package/dist/ui/operations/workloads.html +4 -4
  43. package/dist/ui/security.html +2 -2
  44. package/dist/ui/server/actions.html +4 -4
  45. package/dist/ui/server/activity.html +2 -2
  46. package/dist/ui/server/capacity.html +4 -4
  47. package/dist/ui/server/database.html +124 -8
  48. package/dist/ui/server/deployments.html +4 -4
  49. package/dist/ui/server/diagnostics.html +2 -2
  50. package/dist/ui/server/firewall.html +4 -4
  51. package/dist/ui/server/fleet.html +4 -4
  52. package/dist/ui/server/logs.html +4 -4
  53. package/dist/ui/server/metrics.html +4 -4
  54. package/dist/ui/server/services.html +2 -2
  55. package/dist/ui/server/sites.html +4 -4
  56. package/dist/ui/server/ssh-keys.html +4 -4
  57. package/dist/ui/server/team.html +4 -4
  58. package/dist/ui/server/terminal.html +2 -2
  59. package/dist/ui/serverless/alarms.html +4 -4
  60. package/dist/ui/serverless/data.html +3 -3
  61. package/dist/ui/serverless/firewall.html +2 -2
  62. package/dist/ui/serverless/functions.html +4 -4
  63. package/dist/ui/serverless/logs.html +4 -4
  64. package/dist/ui/serverless/queues.html +4 -4
  65. package/dist/ui/serverless/secrets.html +4 -4
  66. package/dist/ui/serverless/traces.html +4 -4
  67. package/dist/ui/serverless.html +3 -3
  68. package/dist/ui-src/pages/server/database.stx +257 -9
  69. package/package.json +3 -3
@@ -45,7 +45,7 @@ import {
45
45
  resolveSiteKind,
46
46
  resolveUiSource,
47
47
  siteInstallBase
48
- } from "./chunk-fbwcv2vf.js";
48
+ } from "./chunk-bs3jfnzp.js";
49
49
  import {
50
50
  artifactKey,
51
51
  buildCloudFormationTemplate,
@@ -92,7 +92,7 @@ import {
92
92
  import { createHash as createHash5 } from "node:crypto";
93
93
 
94
94
  // src/control-plane/migrations.ts
95
- var CONTROL_PLANE_SCHEMA_VERSION = 35;
95
+ var CONTROL_PLANE_SCHEMA_VERSION = 36;
96
96
  var controlPlaneMigrations = [
97
97
  {
98
98
  version: 1,
@@ -1506,11 +1506,56 @@ var controlPlaneMigrations = [
1506
1506
  CREATE INDEX cleanup_plan_status ON cleanup_plans(project_id,status,expires_at);
1507
1507
  CREATE INDEX dr_drill_status ON disaster_recovery_drills(project_id,status,created_at DESC);
1508
1508
  `
1509
+ },
1510
+ {
1511
+ version: 36,
1512
+ name: "telemetry_source_rollups",
1513
+ sql: `
1514
+ CREATE TABLE telemetry_source_rollups (
1515
+ project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
1516
+ environment_id TEXT NOT NULL DEFAULT '',
1517
+ resource_id TEXT NOT NULL DEFAULT '',
1518
+ source TEXT NOT NULL,
1519
+ first_observed_at TEXT NOT NULL,
1520
+ latest_observed_at TEXT NOT NULL,
1521
+ ingested_bytes INTEGER NOT NULL DEFAULT 0 CHECK (ingested_bytes >= 0),
1522
+ record_count INTEGER NOT NULL DEFAULT 0 CHECK (record_count >= 0),
1523
+ PRIMARY KEY(project_id, environment_id, resource_id, source)
1524
+ ) STRICT;
1525
+
1526
+ INSERT INTO telemetry_source_rollups (
1527
+ project_id, environment_id, resource_id, source,
1528
+ first_observed_at, latest_observed_at, ingested_bytes, record_count
1529
+ )
1530
+ SELECT
1531
+ project_id, COALESCE(environment_id, ''), COALESCE(resource_id, ''), source,
1532
+ MIN(observed_at), MAX(observed_at), SUM(ingested_bytes), COUNT(*)
1533
+ FROM telemetry_records
1534
+ GROUP BY project_id, COALESCE(environment_id, ''), COALESCE(resource_id, ''), source;
1535
+
1536
+ CREATE TRIGGER telemetry_source_rollups_insert
1537
+ AFTER INSERT ON telemetry_records
1538
+ BEGIN
1539
+ INSERT INTO telemetry_source_rollups (
1540
+ project_id, environment_id, resource_id, source,
1541
+ first_observed_at, latest_observed_at, ingested_bytes, record_count
1542
+ )
1543
+ VALUES (
1544
+ NEW.project_id, COALESCE(NEW.environment_id, ''), COALESCE(NEW.resource_id, ''), NEW.source,
1545
+ NEW.observed_at, NEW.observed_at, NEW.ingested_bytes, 1
1546
+ )
1547
+ ON CONFLICT(project_id, environment_id, resource_id, source) DO UPDATE SET
1548
+ first_observed_at = MIN(first_observed_at, excluded.first_observed_at),
1549
+ latest_observed_at = MAX(latest_observed_at, excluded.latest_observed_at),
1550
+ ingested_bytes = ingested_bytes + excluded.ingested_bytes,
1551
+ record_count = record_count + 1;
1552
+ END;
1553
+ `
1509
1554
  }
1510
1555
  ];
1511
1556
  // src/control-plane/store.ts
1512
1557
  import { createHash, randomBytes } from "node:crypto";
1513
- import { chmodSync, existsSync, mkdirSync, statSync, writeFileSync } from "node:fs";
1558
+ import { chmodSync, existsSync, mkdirSync, statSync } from "node:fs";
1514
1559
  import { dirname } from "node:path";
1515
1560
  import { Database } from "bun:sqlite";
1516
1561
 
@@ -1978,6 +2023,12 @@ class ControlPlaneStore {
1978
2023
  } catch {}
1979
2024
  }
1980
2025
  }
2026
+ backupTo(path) {
2027
+ this.database.run("VACUUM INTO ?", [path]);
2028
+ try {
2029
+ chmodSync(path, 384);
2030
+ } catch {}
2031
+ }
1981
2032
  migrate() {
1982
2033
  const row = this.database.query("PRAGMA user_version").get();
1983
2034
  const current = Number(row?.user_version ?? 0);
@@ -1988,7 +2039,7 @@ class ControlPlaneStore {
1988
2039
  let backupPath;
1989
2040
  if (current > 0 && this.path !== ":memory:") {
1990
2041
  backupPath = `${this.path}.v${current}.${Date.now()}.bak`;
1991
- writeFileSync(backupPath, this.database.serialize(), { mode: 384 });
2042
+ this.backupTo(backupPath);
1992
2043
  }
1993
2044
  try {
1994
2045
  const apply = this.database.transaction(() => {
@@ -2263,7 +2314,7 @@ class ControlPlaneStore {
2263
2314
  return row ? mapMembership(row) : undefined;
2264
2315
  }
2265
2316
  listMemberships(organizationId, options = {}) {
2266
- const revoked = options.includeRevoked ? "" : "AND status = 'active'";
2317
+ const revoked = options.includeRevoked ? "" : `AND status = 'active'`;
2267
2318
  return this.database.query(`SELECT * FROM organization_memberships WHERE organization_id = ? ${revoked} ORDER BY created_at, id`).all(organizationId).map(mapMembership);
2268
2319
  }
2269
2320
  updateMembership(input) {
@@ -2314,7 +2365,7 @@ class ControlPlaneStore {
2314
2365
  return membership;
2315
2366
  }
2316
2367
  assertAnotherOwner(current) {
2317
- const owners = Number(this.database.query("SELECT COUNT(*) AS count FROM organization_memberships WHERE organization_id = ? AND status = ? AND role_template = 'owner'").get(current.organizationId, "active")?.count ?? 0);
2368
+ const owners = Number(this.database.query(`SELECT COUNT(*) AS count FROM organization_memberships WHERE organization_id = ? AND status = ? AND role_template = 'owner'`).get(current.organizationId, "active")?.count ?? 0);
2318
2369
  if (owners <= 1)
2319
2370
  throw new Error("Cannot remove or demote the last organization owner");
2320
2371
  }
@@ -2873,7 +2924,7 @@ class ControlPlaneStore {
2873
2924
  if (this.path === ":memory:")
2874
2925
  throw new Error("Cannot create a filesystem backup for an in-memory control plane");
2875
2926
  const backupPath = `${this.path}.${Date.now()}.bak`;
2876
- writeFileSync(backupPath, this.database.serialize(), { mode: 384 });
2927
+ this.backupTo(backupPath);
2877
2928
  this.setSetting("storage.last_backup", { path: backupPath, createdAt: this.now(), reason });
2878
2929
  return backupPath;
2879
2930
  }
@@ -7990,7 +8041,7 @@ function estimateExistingStaticFullStackMonthlyCost(options = {}) {
7990
8041
  // src/deploy/container-image.ts
7991
8042
  import { execFileSync } from "node:child_process";
7992
8043
  import { createHash as createHash8 } from "node:crypto";
7993
- import { existsSync as existsSync3, mkdirSync as mkdirSync2, mkdtempSync, readdirSync, readFileSync, rmSync, statSync as statSync2, writeFileSync as writeFileSync2 } from "node:fs";
8044
+ import { existsSync as existsSync3, mkdirSync as mkdirSync2, mkdtempSync, readdirSync, readFileSync, rmSync, statSync as statSync2, writeFileSync } from "node:fs";
7994
8045
  import { tmpdir } from "node:os";
7995
8046
  import { basename as basename2, join as join3, relative, resolve as resolve2 } from "node:path";
7996
8047
  var IGNORED_DIRECTORIES = new Set([".git", "node_modules", "dist", "coverage"]);
@@ -8093,7 +8144,7 @@ async function buildAndPushContainerImage(options, injected) {
8093
8144
  const registry = auth.proxyEndpoint.replace(/^https?:\/\//, "");
8094
8145
  const dockerConfig = join3(temporaryDirectory, "docker");
8095
8146
  mkdirSync2(dockerConfig, { recursive: true });
8096
- writeFileSync2(join3(dockerConfig, "config.json"), JSON.stringify({ auths: { [registry]: { auth: auth.authorizationToken } } }), { mode: 384 });
8147
+ writeFileSync(join3(dockerConfig, "config.json"), JSON.stringify({ auths: { [registry]: { auth: auth.authorizationToken } } }), { mode: 384 });
8097
8148
  dependencies2.run("docker", ["push", imageUri], { env: { ...process.env, DOCKER_CONFIG: dockerConfig } });
8098
8149
  } finally {
8099
8150
  rmSync(temporaryDirectory, { recursive: true, force: true });
@@ -10895,9 +10946,11 @@ class TelemetryStore {
10895
10946
  if (environmentId)
10896
10947
  bindings.push(environmentId);
10897
10948
  const boundedResources = [...new Set(resourceIds ?? [])].slice(0, 100);
10949
+ if (resourceIds && boundedResources.length === 0)
10950
+ return [];
10898
10951
  const resources = resourceIds ? ` AND resource_id IN (${boundedResources.map(() => "?").join(",") || "''"})` : "";
10899
10952
  bindings.push(...boundedResources);
10900
- const rows = this.controlPlane.database.query(`SELECT source, MAX(observed_at) latest, SUM(ingested_bytes) bytes, COUNT(*) count, MIN(observed_at) first FROM telemetry_records WHERE project_id=?${environment2}${resources} GROUP BY source`).all(...bindings);
10953
+ const rows = this.controlPlane.database.query(`SELECT source, MAX(latest_observed_at) latest, SUM(ingested_bytes) bytes, SUM(record_count) count, MIN(first_observed_at) first FROM telemetry_source_rollups WHERE project_id=?${environment2}${resources} GROUP BY source`).all(...bindings);
10901
10954
  const now = this.now().getTime();
10902
10955
  return rows.map((row) => {
10903
10956
  const latest = String(row.latest);
@@ -11016,8 +11069,26 @@ class TelemetryStore {
11016
11069
  const excess = Number(this.controlPlane.database.query(`SELECT MAX(0, COUNT(*)-?) excess FROM telemetry_records${projectId ? " WHERE project_id=?" : ""}`).get(policy.maxRecords, ...projectId ? [projectId] : [])?.excess ?? 0);
11017
11070
  if (excess > 0)
11018
11071
  this.controlPlane.database.run(`DELETE FROM telemetry_records WHERE id IN (SELECT id FROM telemetry_records${projectId ? " WHERE project_id=?" : ""} ORDER BY timestamp ASC LIMIT ?)`, projectId ? [projectId, excess] : [excess]);
11072
+ this.rebuildStatusRollups(projectId);
11019
11073
  return { deleted: removed + excess, ...downsampled };
11020
11074
  }
11075
+ rebuildStatusRollups(projectId) {
11076
+ const scope = projectId ? " WHERE project_id=?" : "";
11077
+ const bindings = projectId ? [projectId] : [];
11078
+ const rebuild = this.controlPlane.database.transaction(() => {
11079
+ this.controlPlane.database.run(`DELETE FROM telemetry_source_rollups${scope}`, bindings);
11080
+ this.controlPlane.database.run(`INSERT INTO telemetry_source_rollups (
11081
+ project_id, environment_id, resource_id, source,
11082
+ first_observed_at, latest_observed_at, ingested_bytes, record_count
11083
+ )
11084
+ SELECT
11085
+ project_id, COALESCE(environment_id, ''), COALESCE(resource_id, ''), source,
11086
+ MIN(observed_at), MAX(observed_at), SUM(ingested_bytes), COUNT(*)
11087
+ FROM telemetry_records${scope}
11088
+ GROUP BY project_id, COALESCE(environment_id, ''), COALESCE(resource_id, ''), source`, bindings);
11089
+ });
11090
+ rebuild();
11091
+ }
11021
11092
  }
11022
11093
  // src/alerts/telemetry.ts
11023
11094
  function evaluateTelemetryAlertRules(store4, projectId, environmentId, now = new Date) {
@@ -12075,7 +12146,7 @@ async function manifestRequest(fetchFn, root, image, token, signal, authorizatio
12075
12146
  }
12076
12147
  // src/onboarding/artifact-store.ts
12077
12148
  import { createHash as createHash12 } from "node:crypto";
12078
- import { chmodSync as chmodSync2, existsSync as existsSync5, mkdirSync as mkdirSync3, renameSync, unlinkSync, writeFileSync as writeFileSync3 } from "node:fs";
12149
+ import { chmodSync as chmodSync2, existsSync as existsSync5, mkdirSync as mkdirSync3, renameSync, unlinkSync, writeFileSync as writeFileSync2 } from "node:fs";
12079
12150
  import { basename as basename3, join as join5 } from "node:path";
12080
12151
  function map2(row) {
12081
12152
  return {
@@ -12122,7 +12193,7 @@ class ApplicationArtifactStore {
12122
12193
  const path = join5(this.root, `${id}.archive`);
12123
12194
  const temporary = `${path}.partial`;
12124
12195
  try {
12125
- writeFileSync3(temporary, input.bytes, { mode: 384, flag: "wx" });
12196
+ writeFileSync2(temporary, input.bytes, { mode: 384, flag: "wx" });
12126
12197
  renameSync(temporary, path);
12127
12198
  chmodSync2(path, 384);
12128
12199
  this.controlPlane.database.run("INSERT INTO application_artifacts (id, organization_id, project_id, filename, storage_path, sha256, size, format, entry_count, expanded_bytes, created_by_actor_id, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", [
@@ -14755,7 +14826,7 @@ function webhookEndpoint(baseUrl, webhook) {
14755
14826
  return new URL(`/api/source/webhooks/${encodeURIComponent(webhook.endpointToken)}`, base).href;
14756
14827
  }
14757
14828
  // src/source/git-workspace.ts
14758
- import { chmodSync as chmodSync3, mkdtempSync as mkdtempSync2, rmSync as rmSync2, writeFileSync as writeFileSync4 } from "node:fs";
14829
+ import { chmodSync as chmodSync3, mkdtempSync as mkdtempSync2, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "node:fs";
14759
14830
  import { tmpdir as tmpdir2 } from "node:os";
14760
14831
  import { join as join6, resolve as resolve5 } from "node:path";
14761
14832
  function validateRemote(value, deployKey) {
@@ -14802,8 +14873,8 @@ function transportEnvironment(options) {
14802
14873
  directory = mkdtempSync2(join6(tmpdir2(), "ts-cloud-git-transport-"));
14803
14874
  const keyPath = join6(directory, "key");
14804
14875
  const knownHostsPath = join6(directory, "known_hosts");
14805
- writeFileSync4(keyPath, options.deployKey.privateKey, { mode: 384 });
14806
- writeFileSync4(knownHostsPath, `${options.deployKey.host} ${options.deployKey.hostKey}
14876
+ writeFileSync3(keyPath, options.deployKey.privateKey, { mode: 384 });
14877
+ writeFileSync3(knownHostsPath, `${options.deployKey.host} ${options.deployKey.hostKey}
14807
14878
  `, { mode: 384 });
14808
14879
  chmodSync3(keyPath, 384);
14809
14880
  chmodSync3(knownHostsPath, 384);
@@ -18280,7 +18351,7 @@ async function sendAuthenticationEmail(config, message) {
18280
18351
  }
18281
18352
  // src/auth/encryption.ts
18282
18353
  import { randomBytes as randomBytes6 } from "node:crypto";
18283
- import { chmodSync as chmodSync4, existsSync as existsSync6, mkdirSync as mkdirSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync5 } from "node:fs";
18354
+ import { chmodSync as chmodSync4, existsSync as existsSync6, mkdirSync as mkdirSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "node:fs";
18284
18355
  import { dirname as dirname2 } from "node:path";
18285
18356
  function authEncryptionKeyFile() {
18286
18357
  return statePath("auth-encryption-key");
@@ -18300,7 +18371,7 @@ function resolveAuthEncryptionKey(cwd) {
18300
18371
  const key = randomBytes6(32).toString("base64url");
18301
18372
  try {
18302
18373
  mkdirSync4(dirname2(file), { recursive: true, mode: 448 });
18303
- writeFileSync5(file, `${key}
18374
+ writeFileSync4(file, `${key}
18304
18375
  `, { mode: 384 });
18305
18376
  chmodSync4(file, 384);
18306
18377
  } catch {}
@@ -23027,7 +23098,7 @@ import { homedir as homedir3 } from "os";
23027
23098
  import { dirname as dirname4, resolve as resolve12 } from "path";
23028
23099
  import process20 from "process";
23029
23100
  import { existsSync as existsSync22, statSync as statSync22 } from "fs";
23030
- import { existsSync as existsSync82, mkdirSync as mkdirSync32, readdirSync as readdirSync32, writeFileSync as writeFileSync52 } from "fs";
23101
+ import { existsSync as existsSync82, mkdirSync as mkdirSync32, readdirSync as readdirSync32, writeFileSync as writeFileSync5 } from "fs";
23031
23102
  import { homedir as homedir2 } from "os";
23032
23103
  import { dirname as dirname3, resolve as resolve72 } from "path";
23033
23104
  import process12 from "process";
@@ -28162,7 +28233,7 @@ function generateConfigTypes(options) {
28162
28233
  const content = `// Generated by bunfig v${version}
28163
28234
  export type ConfigNames = ${files.length ? `'${files.join("' | '")}'` : "string"}
28164
28235
  `;
28165
- writeFileSync52(outputFile, content, { mode: 438 });
28236
+ writeFileSync5(outputFile, content, { mode: 438 });
28166
28237
  }
28167
28238
  function createLibraryConfig(options) {
28168
28239
  let configCache = null;
@@ -39416,6 +39487,10 @@ async function resolveServerDashboardData(config6, environment2, options = {}) {
39416
39487
  }
39417
39488
 
39418
39489
  // src/deploy/dashboard-database.ts
39490
+ var EXTERNAL_ENGINES = new Set(["singlestore", "vitess"]);
39491
+ function isExternalEngine(engine) {
39492
+ return EXTERNAL_ENGINES.has(engine);
39493
+ }
39419
39494
  var SOCKETS = {
39420
39495
  mysql: "/var/lib/pantry/mysql/mysqld.sock",
39421
39496
  mariadb: "/var/lib/pantry/mariadb/mariadbd.sock"
@@ -39428,6 +39503,10 @@ function normalizeEngine(engine) {
39428
39503
  return "postgres";
39429
39504
  if (engine === "mariadb")
39430
39505
  return "mariadb";
39506
+ if (engine === "singlestore")
39507
+ return "singlestore";
39508
+ if (engine === "vitess")
39509
+ return "vitess";
39431
39510
  return "mysql";
39432
39511
  }
39433
39512
  function resolveDbEngine(config6) {
@@ -39454,6 +39533,8 @@ function pgExec(sql, database) {
39454
39533
  return [`${pgAdminCommand(database)} -tA <<'TS_CLOUD_PG_EOF'`, ...sql, "TS_CLOUD_PG_EOF"];
39455
39534
  }
39456
39535
  function buildListScript(engine, database) {
39536
+ if (engine === "vitess")
39537
+ return buildVitessListScript(database);
39457
39538
  if (engine === "postgres") {
39458
39539
  return pgExec([
39459
39540
  "SELECT 'DB=' || datname FROM pg_database WHERE datistemplate = false;",
@@ -39465,6 +39546,72 @@ function buildListScript(engine, database) {
39465
39546
  "SELECT DISTINCT CONCAT('USER=', User) FROM mysql.user WHERE User NOT IN ('root', 'mysql.sys', 'mysql.session', 'mysql.infoschema', 'debian-sys-maint');"
39466
39547
  ]);
39467
39548
  }
39549
+ function externalMysqlExec(sql, database, port) {
39550
+ const host2 = database?.host ?? "127.0.0.1";
39551
+ const user = database?.username ?? "root";
39552
+ const pass = database?.password ?? "";
39553
+ const tls = database?.ssl === false ? "" : " --ssl-mode=PREFERRED";
39554
+ const shq = (v) => `'${String(v).replace(/'/g, `'\\''`)}'`;
39555
+ const env = pass ? `MYSQL_PWD=${shq(pass)} ` : "";
39556
+ return [
39557
+ `${env}mysql -h ${shq(host2)} -P ${database?.port ?? port} -u ${shq(user)}${tls} --batch --skip-column-names <<'TS_CLOUD_SQL_EOF'`,
39558
+ ...sql,
39559
+ "TS_CLOUD_SQL_EOF"
39560
+ ];
39561
+ }
39562
+ var VTGATE_DEFAULT_PORT = 15306;
39563
+ function buildVitessListScript(database) {
39564
+ const script = externalMysqlExec(["SHOW KEYSPACES;"], database, VTGATE_DEFAULT_PORT);
39565
+ script[0] = `${script[0]} | sed 's/^/DB=/'`;
39566
+ return script;
39567
+ }
39568
+ function buildVitessTopologyScript(database) {
39569
+ return externalMysqlExec([
39570
+ `SHOW KEYSPACES;`,
39571
+ `SHOW VITESS_SHARDS;`,
39572
+ `SHOW VITESS_TABLETS;`
39573
+ ], database, VTGATE_DEFAULT_PORT);
39574
+ }
39575
+ function parseVitessTopology(output) {
39576
+ const keyspaces = [];
39577
+ const shards = [];
39578
+ const tablets = [];
39579
+ for (const rawLine of output.split(`
39580
+ `)) {
39581
+ const line = rawLine.trim();
39582
+ if (!line)
39583
+ continue;
39584
+ const cols = line.split("\t").map((c) => c.trim()).filter(Boolean);
39585
+ if (cols.length >= 6) {
39586
+ const [cell, keyspace, shard, type, state, alias, hostname] = cols;
39587
+ tablets.push({
39588
+ cell: cell ?? "",
39589
+ keyspace: keyspace ?? "",
39590
+ shard: shard ?? "",
39591
+ type: type ?? "",
39592
+ state: state ?? "",
39593
+ alias: alias ?? "",
39594
+ hostname: hostname ?? ""
39595
+ });
39596
+ continue;
39597
+ }
39598
+ if (cols.length === 1) {
39599
+ const only = cols[0];
39600
+ if (only.includes("/")) {
39601
+ const [keyspace, shard] = only.split("/");
39602
+ if (keyspace && shard)
39603
+ shards.push({ keyspace, shard });
39604
+ } else {
39605
+ keyspaces.push(only);
39606
+ }
39607
+ }
39608
+ }
39609
+ return {
39610
+ keyspaces: [...new Set(keyspaces)].filter(Boolean).sort(),
39611
+ shards,
39612
+ tablets
39613
+ };
39614
+ }
39468
39615
  function buildCreateDatabaseScript(engine, name, database) {
39469
39616
  if (engine === "postgres") {
39470
39617
  return pgExec([
@@ -39541,18 +39688,31 @@ async function runDb(config6, environment2, commands, comment) {
39541
39688
  stderr: result2.perInstance?.[0]?.error ?? result2.error ?? ""
39542
39689
  };
39543
39690
  }
39691
+ function externalEngineRefusal(engine, action) {
39692
+ const where = engine === "vitess" ? "Create a keyspace with `vtctldclient CreateKeyspace`, and manage vtgate credentials on the cluster." : "Use the provider console or the cluster's own admin connection.";
39693
+ return {
39694
+ ok: false,
39695
+ error: `${action} is not available for ${engine}: it is an external cluster, so this box has no engine to administer. ${where}`
39696
+ };
39697
+ }
39544
39698
  async function listDatabases(config6, environment2) {
39545
39699
  const engine = resolveDbEngine(config6);
39700
+ if (isExternalEngine(engine) && engine !== "vitess")
39701
+ return { ...externalEngineRefusal(engine, "Listing databases"), engine, databases: [], users: [] };
39546
39702
  const r = await runDb(config6, environment2, buildListScript(engine, resolveAppDatabase(config6)), "ts-cloud db:list");
39547
39703
  const parsed = r.ok && r.stdout ? parseDbList(r.stdout) : { databases: [], users: [] };
39548
39704
  return { ...r, engine, ...parsed };
39549
39705
  }
39550
39706
  async function createDatabase(config6, environment2, name) {
39551
39707
  const engine = resolveDbEngine(config6);
39708
+ if (isExternalEngine(engine))
39709
+ return externalEngineRefusal(engine, "Creating a database");
39552
39710
  return runDb(config6, environment2, buildCreateDatabaseScript(engine, name, resolveAppDatabase(config6)), `ts-cloud db:create ${name}`);
39553
39711
  }
39554
39712
  async function createDatabaseUser(config6, environment2, input) {
39555
39713
  const engine = resolveDbEngine(config6);
39714
+ if (isExternalEngine(engine))
39715
+ return externalEngineRefusal(engine, "Creating a database user");
39556
39716
  return runDb(config6, environment2, buildCreateUserScript(engine, input, resolveAppDatabase(config6)), `ts-cloud db:user ${input.username}`);
39557
39717
  }
39558
39718
  var DB_BACKUP_DIR = "/var/backups/ts-cloud/databases";
@@ -39596,6 +39756,8 @@ async function backupDatabase(config6, environment2, name) {
39596
39756
  if (!isValidDbIdentifier(name))
39597
39757
  return { ok: false, error: "Database name must be a valid identifier.", database: name };
39598
39758
  const engine = resolveDbEngine(config6);
39759
+ if (isExternalEngine(engine))
39760
+ return { ...externalEngineRefusal(engine, "Backing up"), database: name };
39599
39761
  const r = await runDb(config6, environment2, buildBackupScript(engine, name, DB_BACKUP_DIR, resolveAppDatabase(config6)), `ts-cloud db:backup ${name}`);
39600
39762
  return { ...r, database: name };
39601
39763
  }
@@ -39604,6 +39766,233 @@ async function listDatabaseBackups(config6, environment2) {
39604
39766
  const backups = r.ok && r.stdout ? parseBackups(r.stdout) : [];
39605
39767
  return { ...r, backups };
39606
39768
  }
39769
+ async function describeVitess(config6, environment2) {
39770
+ const engine = resolveDbEngine(config6);
39771
+ const empty = { keyspaces: [], shards: [], tablets: [] };
39772
+ if (engine !== "vitess")
39773
+ return { ok: false, error: "The configured database engine is not Vitess.", engine, ...empty };
39774
+ const r = await runDb(config6, environment2, buildVitessTopologyScript(resolveAppDatabase(config6)), "ts-cloud vitess:topology");
39775
+ const parsed = r.ok && r.stdout ? parseVitessTopology(r.stdout) : empty;
39776
+ return { ...r, engine, ...parsed };
39777
+ }
39778
+ function unhealthyTablets(tablets) {
39779
+ return tablets.filter((t) => t.state.toUpperCase() !== "SERVING");
39780
+ }
39781
+ function shardsMissingPrimary(topology2) {
39782
+ const withPrimary = new Set(topology2.tablets.filter((t) => t.type.toUpperCase() === "PRIMARY" && t.state.toUpperCase() === "SERVING").map((t) => `${t.keyspace}/${t.shard}`));
39783
+ return topology2.shards.filter((s) => !withPrimary.has(`${s.keyspace}/${s.shard}`));
39784
+ }
39785
+
39786
+ // src/deploy/dashboard-vitess.ts
39787
+ var VTCTLDCLIENT_BIN = "/usr/local/bin/vtctldclient";
39788
+ var DEFAULT_VTCTLDCLIENT_VERSION = "21.0.0";
39789
+ function sh(value2) {
39790
+ return `'${String(value2).split("'").join("'\\''")}'`;
39791
+ }
39792
+ function resolveVitessConfig(config6) {
39793
+ return resolveAppDatabase(config6)?.vitess ?? {};
39794
+ }
39795
+ function isValidKeyspaceName(value2) {
39796
+ return /^[A-Za-z_][A-Za-z0-9_]{0,63}$/.test(value2);
39797
+ }
39798
+ function isValidMigrationUuid(value2) {
39799
+ return /^[0-9a-f]{8}(?:_[0-9a-f]{4}){3}_[0-9a-f]{12}$/i.test(value2);
39800
+ }
39801
+ function buildVtctldClientInstallScript(version3 = DEFAULT_VTCTLDCLIENT_VERSION) {
39802
+ const v = sh(version3);
39803
+ return [
39804
+ `if ${VTCTLDCLIENT_BIN} --version 2>/dev/null | grep -q ${v}; then echo "vtctldclient ${version3} already installed"; else`,
39805
+ " command -v curl >/dev/null 2>&1 || (apt-get update -y && apt-get install -y curl ca-certificates)",
39806
+ ' if [ "$(uname -m)" != "x86_64" ]; then',
39807
+ ' echo "vtctldclient: Vitess publishes only an x86_64 release tarball; this box is $(uname -m). Build from source or install via pantry." >&2',
39808
+ " exit 1",
39809
+ " fi",
39810
+ ' tmp="$(mktemp -d)"',
39811
+ ` api="https://api.github.com/repos/vitessio/vitess/releases/tags/v${version3}"`,
39812
+ ` url="$(curl -fsSL "$api" | grep -oE '"browser_download_url"[[:space:]]*:[[:space:]]*"[^"]+vitess-[0-9][^"]*\\.tar\\.gz"' | head -1 | sed -E 's/.*"(https[^"]+)"/\\1/')"`,
39813
+ ' if [ -z "$url" ]; then echo "vtctldclient: no release tarball found for v' + version3 + '" >&2; rm -rf "$tmp"; exit 1; fi',
39814
+ ` curl -fsSL "$url" | tar -xz -C "$tmp" --strip-components=2 --wildcards --occurrence=1 '*/bin/vtctldclient' || true`,
39815
+ ' if [ ! -s "$tmp/vtctldclient" ]; then',
39816
+ ' echo "vtctldclient: could not extract the binary from $url" >&2',
39817
+ ' rm -rf "$tmp"; exit 1',
39818
+ " fi",
39819
+ ` install -m 0755 "$tmp/vtctldclient" ${VTCTLDCLIENT_BIN}`,
39820
+ ' rm -rf "$tmp"',
39821
+ ` echo "installed vtctldclient ${version3}"`,
39822
+ "fi"
39823
+ ];
39824
+ }
39825
+ function vtctldCommand(vitess) {
39826
+ if (!vitess.vtctldAddr)
39827
+ return null;
39828
+ return `${VTCTLDCLIENT_BIN} --server ${sh(vitess.vtctldAddr)}`;
39829
+ }
39830
+ function buildCreateKeyspaceScript(vitess, name, options = {}) {
39831
+ const base = vtctldCommand(vitess);
39832
+ if (!base)
39833
+ return null;
39834
+ const sharded = options.sharded ? " --sharded" : "";
39835
+ return [`${base} CreateKeyspace${sharded} ${sh(name)}`];
39836
+ }
39837
+ function buildApplyVSchemaScript(vitess, keyspace, vschemaJson) {
39838
+ const base = vtctldCommand(vitess);
39839
+ if (!base)
39840
+ return null;
39841
+ return [
39842
+ 'vschema_tmp="$(mktemp)"',
39843
+ `cat > "$vschema_tmp" <<'TS_CLOUD_VSCHEMA_EOF'`,
39844
+ vschemaJson,
39845
+ "TS_CLOUD_VSCHEMA_EOF",
39846
+ `${base} ApplyVSchema --vschema-file "$vschema_tmp" ${sh(keyspace)}`,
39847
+ 'rm -f "$vschema_tmp"'
39848
+ ];
39849
+ }
39850
+ function buildApplySchemaScript(database, sql, strategy2 = "vitess") {
39851
+ return externalMysqlExec([`SET @@ddl_strategy='${strategy2}';`, sql.endsWith(";") ? sql : `${sql};`], database, VTGATE_DEFAULT_PORT);
39852
+ }
39853
+ function buildMigrationsScript(database, keyspace) {
39854
+ const statements = keyspace ? [`USE ${keyspace.replace(/[^A-Z0-9_]/gi, "")};`, "SHOW VITESS_MIGRATIONS;"] : ["SHOW VITESS_MIGRATIONS;"];
39855
+ const script = externalMysqlExec(statements, database, VTGATE_DEFAULT_PORT);
39856
+ script[0] = script[0].replace(" --skip-column-names", "");
39857
+ return script;
39858
+ }
39859
+ function parseMigrations(output) {
39860
+ const lines = output.split(`
39861
+ `).map((l) => l.trimEnd()).filter((l) => l.trim().length > 0);
39862
+ if (lines.length === 0)
39863
+ return [];
39864
+ const header3 = lines[0].split("\t").map((c) => c.trim());
39865
+ const col = (name) => header3.indexOf(name);
39866
+ const columns = {
39867
+ uuid: col("migration_uuid"),
39868
+ keyspace: col("keyspace"),
39869
+ shard: col("shard"),
39870
+ table: col("mysql_table"),
39871
+ status: col("migration_status"),
39872
+ strategy: col("strategy"),
39873
+ added: col("added_timestamp"),
39874
+ completed: col("completed_timestamp"),
39875
+ progress: col("progress")
39876
+ };
39877
+ if (columns.uuid === -1)
39878
+ return [];
39879
+ const rows = [];
39880
+ for (const line of lines.slice(1)) {
39881
+ const cells = line.split("\t");
39882
+ const at = (i) => i >= 0 ? (cells[i] ?? "").trim() : "";
39883
+ const uuid = at(columns.uuid);
39884
+ if (!uuid)
39885
+ continue;
39886
+ rows.push({
39887
+ uuid,
39888
+ keyspace: at(columns.keyspace),
39889
+ shard: at(columns.shard),
39890
+ table: at(columns.table),
39891
+ status: at(columns.status),
39892
+ strategy: at(columns.strategy),
39893
+ added: at(columns.added),
39894
+ completed: at(columns.completed),
39895
+ progress: at(columns.progress)
39896
+ });
39897
+ }
39898
+ return rows;
39899
+ }
39900
+ function failedMigrations(migrations2) {
39901
+ return migrations2.filter((m) => m.status.toLowerCase() === "failed");
39902
+ }
39903
+ function runningMigrations(migrations2) {
39904
+ return migrations2.filter((m) => ["running", "queued", "ready"].includes(m.status.toLowerCase()));
39905
+ }
39906
+ function buildMigrationActionScript(database, uuid, action) {
39907
+ const verb = action.toUpperCase();
39908
+ return externalMysqlExec([`ALTER VITESS_MIGRATION '${uuid}' ${verb};`], database, VTGATE_DEFAULT_PORT);
39909
+ }
39910
+ function requireVitess(config6) {
39911
+ if (resolveDbEngine(config6) !== "vitess")
39912
+ return { ok: false, error: "The configured database engine is not Vitess." };
39913
+ return null;
39914
+ }
39915
+ function requireControlPlane(vitess) {
39916
+ if (!vitess.vtctldAddr) {
39917
+ return {
39918
+ ok: false,
39919
+ error: "No vtctld address is configured. Creating keyspaces and applying a VSchema are topology operations that vtgate cannot perform. " + "Set `infrastructure.appDatabase.vitess.vtctldAddr` (for example `vtctld.internal:15999`) and redeploy."
39920
+ };
39921
+ }
39922
+ return null;
39923
+ }
39924
+ async function installVtctldClient(config6, environment2) {
39925
+ const wrong = requireVitess(config6);
39926
+ if (wrong)
39927
+ return wrong;
39928
+ const vitess = resolveVitessConfig(config6);
39929
+ return runDb(config6, environment2, buildVtctldClientInstallScript(vitess.clientVersion), "ts-cloud vitess:install-client");
39930
+ }
39931
+ async function createKeyspace(config6, environment2, name, options = {}) {
39932
+ const wrong = requireVitess(config6);
39933
+ if (wrong)
39934
+ return { ...wrong, keyspace: name };
39935
+ if (!isValidKeyspaceName(name))
39936
+ return { ok: false, error: "Keyspace name must be a valid identifier.", keyspace: name };
39937
+ const vitess = resolveVitessConfig(config6);
39938
+ const missing = requireControlPlane(vitess);
39939
+ if (missing)
39940
+ return { ...missing, keyspace: name };
39941
+ const script = buildCreateKeyspaceScript(vitess, name, options);
39942
+ if (!script)
39943
+ return { ...requireControlPlane({}), keyspace: name };
39944
+ const r = await runDb(config6, environment2, script, `ts-cloud vitess:create-keyspace ${name}`);
39945
+ return { ...r, keyspace: name };
39946
+ }
39947
+ async function applyVSchema(config6, environment2, keyspace, vschemaJson) {
39948
+ const wrong = requireVitess(config6);
39949
+ if (wrong)
39950
+ return { ...wrong, keyspace };
39951
+ if (!isValidKeyspaceName(keyspace))
39952
+ return { ok: false, error: "Keyspace name must be a valid identifier.", keyspace };
39953
+ try {
39954
+ const parsed = JSON.parse(vschemaJson);
39955
+ if (!parsed || typeof parsed !== "object")
39956
+ return { ok: false, error: "The VSchema must be a JSON object.", keyspace };
39957
+ } catch (error2) {
39958
+ return { ok: false, error: `The VSchema is not valid JSON: ${error2?.message ?? error2}`, keyspace };
39959
+ }
39960
+ const vitess = resolveVitessConfig(config6);
39961
+ const missing = requireControlPlane(vitess);
39962
+ if (missing)
39963
+ return { ...missing, keyspace };
39964
+ const script = buildApplyVSchemaScript(vitess, keyspace, vschemaJson);
39965
+ if (!script)
39966
+ return { ...requireControlPlane({}), keyspace };
39967
+ const r = await runDb(config6, environment2, script, `ts-cloud vitess:apply-vschema ${keyspace}`);
39968
+ return { ...r, keyspace };
39969
+ }
39970
+ async function applySchemaChange(config6, environment2, sql) {
39971
+ const wrong = requireVitess(config6);
39972
+ if (wrong)
39973
+ return wrong;
39974
+ if (!sql.trim())
39975
+ return { ok: false, error: "No SQL was provided." };
39976
+ const vitess = resolveVitessConfig(config6);
39977
+ return runDb(config6, environment2, buildApplySchemaScript(resolveAppDatabase(config6), sql, vitess.ddlStrategy ?? "vitess"), "ts-cloud vitess:apply-schema");
39978
+ }
39979
+ async function listMigrations(config6, environment2, keyspace) {
39980
+ const wrong = requireVitess(config6);
39981
+ if (wrong)
39982
+ return { ...wrong, migrations: [], failed: [], running: [] };
39983
+ const r = await runDb(config6, environment2, buildMigrationsScript(resolveAppDatabase(config6), keyspace), "ts-cloud vitess:migrations");
39984
+ const migrations2 = r.ok && r.stdout ? parseMigrations(r.stdout) : [];
39985
+ return { ...r, migrations: migrations2, failed: failedMigrations(migrations2), running: runningMigrations(migrations2) };
39986
+ }
39987
+ async function actOnMigration(config6, environment2, uuid, action) {
39988
+ const wrong = requireVitess(config6);
39989
+ if (wrong)
39990
+ return { ...wrong, uuid };
39991
+ if (!isValidMigrationUuid(uuid))
39992
+ return { ok: false, error: "Migration UUID is not in the expected format.", uuid };
39993
+ const r = await runDb(config6, environment2, buildMigrationActionScript(resolveAppDatabase(config6), uuid, action), `ts-cloud vitess:migration ${action} ${uuid}`);
39994
+ return { ...r, uuid };
39995
+ }
39607
39996
 
39608
39997
  // src/deploy/dashboard-policy.ts
39609
39998
  var FAIL_CLOSED = {
@@ -39843,6 +40232,13 @@ var POLICIES = {
39843
40232
  "DELETE /api/firewall": { capability: "fleet:manage" },
39844
40233
  "GET /api/databases": { capability: "data:read" },
39845
40234
  "POST /api/databases": { capability: "data:admin" },
40235
+ "GET /api/databases/vitess": { capability: "data:read" },
40236
+ "GET /api/databases/vitess/migrations": { capability: "data:read" },
40237
+ "POST /api/databases/vitess/migrations/action": { capability: "data:admin" },
40238
+ "POST /api/databases/vitess/keyspaces": { capability: "data:admin" },
40239
+ "POST /api/databases/vitess/vschema": { capability: "data:admin" },
40240
+ "POST /api/databases/vitess/schema": { capability: "data:admin" },
40241
+ "POST /api/databases/vitess/install-client": { capability: "data:admin" },
39846
40242
  "GET /api/databases/backups": { capability: "backups:read" },
39847
40243
  "POST /api/databases/backup": { capability: "backups:create" },
39848
40244
  "POST /api/databases/users": { capability: "data:admin" },
@@ -40133,7 +40529,7 @@ function clearSessionCookie(options = { secure: true }) {
40133
40529
  }
40134
40530
 
40135
40531
  // src/deploy/dashboard-users.ts
40136
- import { chmodSync as chmodSync6, existsSync as existsSync19, mkdirSync as mkdirSync7, readFileSync as readFileSync10, writeFileSync as writeFileSync10 } from "node:fs";
40532
+ import { chmodSync as chmodSync6, existsSync as existsSync19, mkdirSync as mkdirSync7, readFileSync as readFileSync10, writeFileSync as writeFileSync9 } from "node:fs";
40137
40533
  import { dirname as dirname8 } from "node:path";
40138
40534
  function usersFile() {
40139
40535
  return statePath("dashboard-users.json");
@@ -40188,7 +40584,7 @@ function loadUsers(cwd) {
40188
40584
  function saveUsers(cwd, users) {
40189
40585
  const file = usersFilePath(cwd);
40190
40586
  mkdirSync7(dirname8(file), { recursive: true });
40191
- writeFileSync10(file, `${JSON.stringify({ users }, null, 2)}
40587
+ writeFileSync9(file, `${JSON.stringify({ users }, null, 2)}
40192
40588
  `);
40193
40589
  chmodSync6(file, 384);
40194
40590
  }
@@ -48816,6 +49212,45 @@ data: ${JSON.stringify({ cursor: position, at: new Date().toISOString() })}
48816
49212
  }, 422);
48817
49213
  return json15({ ...await createDatabase(config6, environment2, name), name });
48818
49214
  }
49215
+ if (url.pathname === "/api/databases/vitess" && req.method === "GET") {
49216
+ const topology2 = await describeVitess(config6, environment2);
49217
+ return json15({
49218
+ ...topology2,
49219
+ unhealthy: unhealthyTablets(topology2.tablets),
49220
+ missingPrimary: shardsMissingPrimary(topology2)
49221
+ });
49222
+ }
49223
+ if (url.pathname === "/api/databases/vitess/migrations" && req.method === "GET") {
49224
+ const keyspace = url.searchParams.get("keyspace") ?? undefined;
49225
+ return json15(await listMigrations(config6, environment2, keyspace));
49226
+ }
49227
+ if (url.pathname === "/api/databases/vitess/migrations/action" && req.method === "POST") {
49228
+ const body = await readJsonBody(req);
49229
+ const uuid = String(body.uuid ?? "").trim();
49230
+ const action = String(body.action ?? "");
49231
+ if (!["retry", "cancel", "cleanup", "complete"].includes(action))
49232
+ return json15({ ok: false, error: "Unknown migration action." }, 422);
49233
+ return json15(await actOnMigration(config6, environment2, uuid, action));
49234
+ }
49235
+ if (url.pathname === "/api/databases/vitess/keyspaces" && req.method === "POST") {
49236
+ const body = await readJsonBody(req);
49237
+ const name = String(body.name ?? "").trim();
49238
+ const sharded = body.sharded === true;
49239
+ return json15(await createKeyspace(config6, environment2, name, { sharded }));
49240
+ }
49241
+ if (url.pathname === "/api/databases/vitess/vschema" && req.method === "POST") {
49242
+ const body = await readJsonBody(req);
49243
+ const keyspace = String(body.keyspace ?? "").trim();
49244
+ const vschema = typeof body.vschema === "string" ? body.vschema : JSON.stringify(body.vschema ?? null);
49245
+ return json15(await applyVSchema(config6, environment2, keyspace, vschema));
49246
+ }
49247
+ if (url.pathname === "/api/databases/vitess/schema" && req.method === "POST") {
49248
+ const body = await readJsonBody(req);
49249
+ const sql = String(body.sql ?? "");
49250
+ return json15(await applySchemaChange(config6, environment2, sql));
49251
+ }
49252
+ if (url.pathname === "/api/databases/vitess/install-client" && req.method === "POST")
49253
+ return json15(await installVtctldClient(config6, environment2));
48819
49254
  if (url.pathname === "/api/databases/backups" && req.method === "GET")
48820
49255
  return json15(await listDatabaseBackups(config6, environment2));
48821
49256
  if (url.pathname === "/api/databases/backup" && req.method === "POST") {