@envsync-cloud/deploy 0.20.12 → 0.21.2

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 (3) hide show
  1. package/dist/cli.js +118 -94
  2. package/dist/index.js +118 -94
  3. package/package.json +2 -2
package/dist/cli.js CHANGED
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli.ts
4
- import { createHash as createHash2, generateKeyPairSync, randomBytes as randomBytes2, randomInt } from "crypto";
4
+ import { createHash as createHash3, generateKeyPairSync, randomBytes as randomBytes2, randomInt } from "crypto";
5
5
  import { spawnSync } from "child_process";
6
- import fs4 from "fs";
6
+ import fs5 from "fs";
7
7
  import path4 from "path";
8
8
  import readline2 from "readline";
9
9
  import chalk from "chalk";
@@ -37,13 +37,11 @@ var deployConfigSchema = z.object({
37
37
  services: z.object({
38
38
  stack_name: z.string().default("envsync"),
39
39
  api_port: z.number().int().positive().default(4e3),
40
- management_api_port: z.number().int().positive().default(4001),
41
40
  public_http_port: z.number().int().positive().default(80),
42
41
  public_https_port: z.number().int().positive().default(443)
43
42
  }).default({}),
44
43
  images: z.object({
45
44
  api: z.string().default("ghcr.io/envsync-cloud/envsync-api:stable"),
46
- management_api: z.string().default("ghcr.io/envsync-cloud/envsync-management-api:stable"),
47
45
  web: z.string().default("ghcr.io/envsync-cloud/envsync-web-oss-static:stable"),
48
46
  enterprise_web: z.string().default("ghcr.io/envsync-cloud/envsync-web-static:stable"),
49
47
  landing: z.string().default("ghcr.io/envsync-cloud/envsync-landing-static:stable"),
@@ -286,6 +284,16 @@ function formatDeploymentPlan(plan, format = "yaml") {
286
284
  }
287
285
 
288
286
  // src/render.ts
287
+ import { createHash } from "crypto";
288
+ import fs2 from "fs";
289
+ function swarmConfigObjectName(stackName, logical, filePath, fallbackVersion) {
290
+ let token = fallbackVersion.replace(/[^a-zA-Z0-9]+/g, "_").replace(/^_|_$/g, "") || "v";
291
+ try {
292
+ token = createHash("sha256").update(fs2.readFileSync(filePath)).digest("hex").slice(0, 12);
293
+ } catch {
294
+ }
295
+ return `${stackName}_${logical}_${token}`;
296
+ }
289
297
  function domainMap(rootDomain) {
290
298
  return {
291
299
  landing: rootDomain,
@@ -383,7 +391,6 @@ function buildRuntimeEnv2(config, generated, options) {
383
391
  ENVSYNC_STACK_NAME: config.services.stack_name,
384
392
  DB_AUTO_MIGRATE: "false",
385
393
  PORT: `${config.services.api_port}`,
386
- MANAGEMENT_API_PORT: `${config.services.management_api_port}`,
387
394
  DATABASE_HOST: "postgres",
388
395
  DATABASE_PORT: "5432",
389
396
  DATABASE_USER: "postgres",
@@ -427,7 +434,13 @@ function buildRuntimeEnv2(config, generated, options) {
427
434
  OPENFGA_MODEL_ID: generated.openfga.model_id,
428
435
  OPENFGA_DB_PASSWORD: generated.secrets.openfga_db_password,
429
436
  API_URL: publicHttpsUrl(config, hosts.api),
437
+ SAML_SESSION_SECRET: generated.secrets.saml_session_secret,
430
438
  MANAGEMENT_API_URL: oss ? "" : publicHttpsUrl(config, hosts.api, "/api/v1/manage"),
439
+ KEYCLOAK_ACCESS_TOKEN_LIFESPAN_SECONDS: "3600",
440
+ KEYCLOAK_SSO_SESSION_IDLE_TIMEOUT_SECONDS: "604800",
441
+ KEYCLOAK_SSO_SESSION_MAX_LIFESPAN_SECONDS: "604800",
442
+ KEYCLOAK_CLIENT_SESSION_IDLE_TIMEOUT_SECONDS: "604800",
443
+ KEYCLOAK_CLIENT_SESSION_MAX_LIFESPAN_SECONDS: "604800",
431
444
  CLICKSTACK_OPERATOR_EMAIL: generated.clickstack.operator_email,
432
445
  CLICKSTACK_OPERATOR_PASSWORD: generated.clickstack.operator_password,
433
446
  CLICKSTACK_ACCESS_KEY: generated.clickstack.access_key,
@@ -466,12 +479,22 @@ function renderKeycloakRealm(config, runtimeEnv) {
466
479
  const webOrigins = publicHttpsOriginVariants(config, hosts.app);
467
480
  const apiRedirectUris = publicHttpsUrlVariants(config, hosts.api, "/api/access/api/callback");
468
481
  const apiOrigins = publicHttpsOriginVariants(config, hosts.api);
482
+ const accessTokenLifespan = Number(runtimeEnv.KEYCLOAK_ACCESS_TOKEN_LIFESPAN_SECONDS || "3600");
483
+ const ssoSessionIdleTimeout = Number(runtimeEnv.KEYCLOAK_SSO_SESSION_IDLE_TIMEOUT_SECONDS || "604800");
484
+ const ssoSessionMaxLifespan = Number(runtimeEnv.KEYCLOAK_SSO_SESSION_MAX_LIFESPAN_SECONDS || "604800");
485
+ const clientSessionIdleTimeout = Number(runtimeEnv.KEYCLOAK_CLIENT_SESSION_IDLE_TIMEOUT_SECONDS || "604800");
486
+ const clientSessionMaxLifespan = Number(runtimeEnv.KEYCLOAK_CLIENT_SESSION_MAX_LIFESPAN_SECONDS || "604800");
469
487
  return JSON.stringify(
470
488
  {
471
489
  realm: config.auth.keycloak_realm,
472
490
  enabled: true,
473
491
  loginTheme: "envsync",
474
492
  emailTheme: "envsync",
493
+ accessTokenLifespan,
494
+ ssoSessionIdleTimeout,
495
+ ssoSessionMaxLifespan,
496
+ clientSessionIdleTimeout,
497
+ clientSessionMaxLifespan,
475
498
  clients: [
476
499
  {
477
500
  clientId: config.auth.web_client_id,
@@ -524,7 +547,7 @@ function renderTraefikDynamicConfig(config, generated) {
524
547
  const landingEnabled = false;
525
548
  const managementEnabled = !isOssConfig(config);
526
549
  const otelAllowedOrigins = [
527
- ...landingEnabled ? publicHttpsOriginVariants(config, hosts.landing) : [],
550
+ ...publicHttpsOriginVariants(config, hosts.landing),
528
551
  ...publicHttpsOriginVariants(config, hosts.app)
529
552
  ];
530
553
  return [
@@ -912,6 +935,7 @@ function renderStack(config, runtimeEnv, generated, mode, paths) {
912
935
  const apiLicenseVolume = managementEnabled ? "\n volumes:\n - /etc/envsync/license:/etc/envsync/license:ro" : "";
913
936
  const deployment = createSteadyApiDeploymentState(config, generated);
914
937
  const stackName = config.services.stack_name;
938
+ const configName = (logical, filePath) => swarmConfigObjectName(stackName, logical, filePath, config.release.version);
915
939
  const s3RouterName = `${stackName}-s3-router`;
916
940
  const s3ServiceName = `${stackName}-s3-service`;
917
941
  const s3ConsoleRouterName = `${stackName}-s3-console-router`;
@@ -1021,7 +1045,7 @@ ${includeRuntimeInfra ? `
1021
1045
  image: ${config.images.keycloak}
1022
1046
  entrypoint: ["/bin/sh", "-lc"]
1023
1047
  command:
1024
- - /opt/keycloak/bin/kc.sh import --dir /opt/keycloak/data/import --override true && exec /opt/keycloak/bin/kc.sh start --optimized
1048
+ - /opt/keycloak/bin/kc.sh import --dir /opt/keycloak/data/import --override false && exec /opt/keycloak/bin/kc.sh start --optimized
1025
1049
  environment:
1026
1050
  ${renderEnvList({
1027
1051
  KC_DB: "postgres",
@@ -1203,56 +1227,62 @@ volumes:
1203
1227
 
1204
1228
  configs:
1205
1229
  keycloak_realm:
1230
+ name: ${configName("keycloak_realm", paths.keycloakRealmFile)}
1206
1231
  file: ${paths.keycloakRealmFile}
1207
1232
  clickstack_clickhouse_conf:
1233
+ name: ${configName("clickstack_clickhouse_conf", paths.clickstackClickhouseConf)}
1208
1234
  file: ${paths.clickstackClickhouseConf}
1209
1235
  otel_agent_conf:
1236
+ name: ${configName("otel_agent_conf", paths.otelAgentConf)}
1210
1237
  file: ${paths.otelAgentConf}
1211
1238
  ${landingEnabled ? ` nginx_landing_conf:
1239
+ name: ${configName("nginx_landing_conf", paths.nginxLandingConf)}
1212
1240
  file: ${paths.nginxLandingConf}
1213
1241
  ` : ""}
1214
1242
  nginx_web_conf:
1243
+ name: ${configName("nginx_web_conf", paths.nginxWebConf)}
1215
1244
  file: ${paths.nginxWebConf}
1216
1245
  nginx_api_maintenance_conf:
1246
+ name: ${configName("nginx_api_maintenance_conf", paths.nginxApiMaintenanceConf)}
1217
1247
  file: ${paths.nginxApiMaintenanceConf}
1218
1248
  `.trimStart();
1219
1249
  }
1220
1250
 
1221
1251
  // src/static-bundle.ts
1222
- import fs2 from "fs";
1252
+ import fs3 from "fs";
1223
1253
  import path2 from "path";
1224
1254
  function exists(target) {
1225
- return fs2.existsSync(target);
1255
+ return fs3.existsSync(target);
1226
1256
  }
1227
1257
  function normalizeExtractedStaticBundle(kind, targetDir) {
1228
1258
  const directIndex = path2.join(targetDir, "index.html");
1229
1259
  if (exists(directIndex)) {
1230
1260
  return;
1231
1261
  }
1232
- const childDirs = fs2.readdirSync(targetDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => path2.join(targetDir, entry.name));
1262
+ const childDirs = fs3.readdirSync(targetDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => path2.join(targetDir, entry.name));
1233
1263
  const candidateRoots = childDirs.filter((dir) => exists(path2.join(dir, "index.html")));
1234
1264
  if (candidateRoots.length !== 1) {
1235
1265
  return;
1236
1266
  }
1237
1267
  const nestedRoot = candidateRoots[0];
1238
- for (const entry of fs2.readdirSync(nestedRoot)) {
1239
- fs2.cpSync(path2.join(nestedRoot, entry), path2.join(targetDir, entry), { recursive: true });
1268
+ for (const entry of fs3.readdirSync(nestedRoot)) {
1269
+ fs3.cpSync(path2.join(nestedRoot, entry), path2.join(targetDir, entry), { recursive: true });
1240
1270
  }
1241
- fs2.rmSync(nestedRoot, { recursive: true, force: true });
1271
+ fs3.rmSync(nestedRoot, { recursive: true, force: true });
1242
1272
  }
1243
1273
  function validateStaticBundle(kind, targetDir) {
1244
1274
  if (exists(path2.join(targetDir, "index.html"))) {
1245
1275
  return;
1246
1276
  }
1247
- const entries = exists(targetDir) ? fs2.readdirSync(targetDir).slice(0, 20).join(", ") : "";
1277
+ const entries = exists(targetDir) ? fs3.readdirSync(targetDir).slice(0, 20).join(", ") : "";
1248
1278
  throw new Error(
1249
1279
  `Invalid ${kind} static bundle at ${targetDir}: missing index.html${entries ? `. Found: ${entries}` : ""}`
1250
1280
  );
1251
1281
  }
1252
1282
 
1253
1283
  // src/org-setup.ts
1254
- import { createHash, randomBytes } from "crypto";
1255
- import fs3 from "fs";
1284
+ import { createHash as createHash2, randomBytes } from "crypto";
1285
+ import fs4 from "fs";
1256
1286
  import path3 from "path";
1257
1287
  import readline from "readline";
1258
1288
  var SETUP_TOKEN_HEADER = "X-EnvSync-Setup-Token";
@@ -1262,33 +1292,33 @@ function generateSetupToken() {
1262
1292
  function ensureSetupTokenFile(tokenFile, existingToken) {
1263
1293
  if (existingToken?.trim()) {
1264
1294
  const dir2 = path3.dirname(tokenFile);
1265
- fs3.mkdirSync(dir2, { recursive: true, mode: 448 });
1266
- fs3.writeFileSync(tokenFile, `${existingToken.trim()}
1295
+ fs4.mkdirSync(dir2, { recursive: true, mode: 448 });
1296
+ fs4.writeFileSync(tokenFile, `${existingToken.trim()}
1267
1297
  `, { mode: 384 });
1268
1298
  return existingToken.trim();
1269
1299
  }
1270
- if (fs3.existsSync(tokenFile)) {
1271
- const fromFile = fs3.readFileSync(tokenFile, "utf8").trim();
1300
+ if (fs4.existsSync(tokenFile)) {
1301
+ const fromFile = fs4.readFileSync(tokenFile, "utf8").trim();
1272
1302
  if (fromFile.length >= 16) {
1273
1303
  return fromFile;
1274
1304
  }
1275
1305
  }
1276
1306
  const token = generateSetupToken();
1277
1307
  const dir = path3.dirname(tokenFile);
1278
- fs3.mkdirSync(dir, { recursive: true, mode: 448 });
1279
- fs3.writeFileSync(tokenFile, `${token}
1308
+ fs4.mkdirSync(dir, { recursive: true, mode: 448 });
1309
+ fs4.writeFileSync(tokenFile, `${token}
1280
1310
  `, { mode: 384 });
1281
1311
  return token;
1282
1312
  }
1283
1313
  function readSetupTokenFile(tokenFile) {
1284
- if (!fs3.existsSync(tokenFile)) {
1314
+ if (!fs4.existsSync(tokenFile)) {
1285
1315
  return null;
1286
1316
  }
1287
- const token = fs3.readFileSync(tokenFile, "utf8").trim();
1317
+ const token = fs4.readFileSync(tokenFile, "utf8").trim();
1288
1318
  return token.length >= 16 ? token : null;
1289
1319
  }
1290
1320
  function setupTokenFingerprint(token) {
1291
- return createHash("sha256").update(token).digest("hex").slice(0, 12);
1321
+ return createHash2("sha256").update(token).digest("hex").slice(0, 12);
1292
1322
  }
1293
1323
  async function fetchSetupStatus(apiBaseUrl, setupToken) {
1294
1324
  const base = apiBaseUrl.replace(/\/$/, "");
@@ -1581,8 +1611,6 @@ var DEFAULT_ENTERPRISE_LICENSE_SERVER_URL = "https://license.envsync.cloud";
1581
1611
  var MANAGED_VERSIONED_IMAGE_PREFIXES = {
1582
1612
  api: "ghcr.io/envsync-cloud/envsync-api:",
1583
1613
  api_enterprise: "ghcr.io/envsync-cloud/envsync-api-enterprise:",
1584
- // Retired second process — kept so old deploy.yaml image strings still parse as "managed".
1585
- management_api: "ghcr.io/envsync-cloud/envsync-management-api:",
1586
1614
  keycloak: "envsync-keycloak:",
1587
1615
  web: "ghcr.io/envsync-cloud/envsync-web-static:",
1588
1616
  web_oss: "ghcr.io/envsync-cloud/envsync-web-oss-static:",
@@ -1777,12 +1805,12 @@ ${typeof result.stderr === "string" ? result.stderr : ""}`.toLowerCase();
1777
1805
  ${stderr}` : ""}`);
1778
1806
  }
1779
1807
  function ensureDir(dir) {
1780
- fs4.mkdirSync(dir, { recursive: true });
1808
+ fs5.mkdirSync(dir, { recursive: true });
1781
1809
  }
1782
1810
  function writeFile(target, content, mode) {
1783
1811
  ensureDir(path4.dirname(target));
1784
- fs4.writeFileSync(target, content, "utf8");
1785
- if (mode != null) fs4.chmodSync(target, mode);
1812
+ fs5.writeFileSync(target, content, "utf8");
1813
+ if (mode != null) fs5.chmodSync(target, mode);
1786
1814
  }
1787
1815
  function writeFileMaybe(target, content, mode) {
1788
1816
  if (currentOptions.dryRun) {
@@ -1794,7 +1822,7 @@ function writeFileMaybe(target, content, mode) {
1794
1822
  function ensureEnterpriseLicenseFilesReadable() {
1795
1823
  if (exists2(LICENSE_ROOT)) {
1796
1824
  try {
1797
- fs4.chmodSync(LICENSE_ROOT, LICENSE_DIR_MODE);
1825
+ fs5.chmodSync(LICENSE_ROOT, LICENSE_DIR_MODE);
1798
1826
  } catch {
1799
1827
  throw new Error(
1800
1828
  `Cannot update directory mode for ${LICENSE_ROOT}. Fix by running: chmod ${LICENSE_DIR_MODE.toString(8)} ${LICENSE_ROOT}`
@@ -1804,7 +1832,7 @@ function ensureEnterpriseLicenseFilesReadable() {
1804
1832
  for (const filePath of [LICENSE_BUNDLE_FILE, LICENSE_CERT_FILE, LICENSE_KEY_FILE, LICENSE_ROOT_CA_FILE]) {
1805
1833
  if (!exists2(filePath)) continue;
1806
1834
  try {
1807
- fs4.chmodSync(filePath, LICENSE_FILE_MODE);
1835
+ fs5.chmodSync(filePath, LICENSE_FILE_MODE);
1808
1836
  } catch {
1809
1837
  throw new Error(
1810
1838
  `Cannot update file mode for ${filePath}. Fix by running: chmod ${LICENSE_FILE_MODE.toString(8)} ${filePath}`
@@ -1813,7 +1841,7 @@ function ensureEnterpriseLicenseFilesReadable() {
1813
1841
  }
1814
1842
  }
1815
1843
  function exists2(target) {
1816
- return fs4.existsSync(target);
1844
+ return fs5.existsSync(target);
1817
1845
  }
1818
1846
  function randomSecret(bytes = 24) {
1819
1847
  return randomBytes2(bytes).toString("hex");
@@ -1823,7 +1851,7 @@ function generateMinikmsSessionSigningKey() {
1823
1851
  return privateKey.export({ type: "pkcs8", format: "pem" }).toString();
1824
1852
  }
1825
1853
  function deterministicInstallFingerprint(rootDomain, stackName) {
1826
- return `envsync-${createHash2("sha256").update(`${rootDomain}:${stackName}`).digest("hex").slice(0, 32)}`;
1854
+ return `envsync-${createHash3("sha256").update(`${rootDomain}:${stackName}`).digest("hex").slice(0, 32)}`;
1827
1855
  }
1828
1856
  function randomStrongPassword() {
1829
1857
  return `EnvSync!${randomBytes2(8).toString("hex")}Aa1`;
@@ -1927,7 +1955,7 @@ function normalizeNetutilsForwards(raw, stackName, enabled) {
1927
1955
  }
1928
1956
  function loadWireguardState() {
1929
1957
  if (!exists2(WIREGUARD_STATE_FILE)) return null;
1930
- const raw = JSON.parse(fs4.readFileSync(WIREGUARD_STATE_FILE, "utf8"));
1958
+ const raw = JSON.parse(fs5.readFileSync(WIREGUARD_STATE_FILE, "utf8"));
1931
1959
  return {
1932
1960
  private_key: raw.private_key,
1933
1961
  public_key: raw.public_key,
@@ -2111,7 +2139,7 @@ function publicHttpsUrl2(config, host, path5 = "") {
2111
2139
  function getDeployCliVersion() {
2112
2140
  try {
2113
2141
  const packageJsonPath = new URL("../package.json", import.meta.url);
2114
- const raw = fs4.readFileSync(packageJsonPath, "utf8");
2142
+ const raw = fs5.readFileSync(packageJsonPath, "utf8");
2115
2143
  return JSON.parse(raw).version ?? "0.0.0";
2116
2144
  } catch {
2117
2145
  return process.env.npm_package_version ?? "0.0.0";
@@ -2203,7 +2231,6 @@ function buildOperatorOverview() {
2203
2231
  const bootstrapComplete = hasCompleteBootstrapState(generated) && generated.bootstrap.completed_at.length > 0;
2204
2232
  const api = apiHealth(services, config.services.stack_name);
2205
2233
  const web = serviceHealth(services, `${config.services.stack_name}_web_nginx`);
2206
- const landing = serviceHealth(services, `${config.services.stack_name}_landing_nginx`);
2207
2234
  statusLines.push(`Configured: ${chalk.green("yes")}`);
2208
2235
  statusLines.push(`Pinned release: ${chalk.cyan(config.release.version)}`);
2209
2236
  statusLines.push(`Stack: ${stackRunning ? chalk.green(config.services.stack_name) : chalk.yellow("not running")}`);
@@ -2211,7 +2238,7 @@ function buildOperatorOverview() {
2211
2238
  statusLines.push(`Active API slot: ${chalk.cyan(generated.deployment.active_slot)}`);
2212
2239
  statusLines.push(`API: ${api === "healthy" ? chalk.green(api) : api === "missing" ? chalk.red(api) : chalk.yellow(api)}`);
2213
2240
  statusLines.push(`Web: ${web === "healthy" ? chalk.green(web) : web === "missing" ? chalk.red(web) : chalk.yellow(web)}`);
2214
- statusLines.push(`Landing: ${landing === "healthy" ? chalk.green(landing) : landing === "missing" ? chalk.red(landing) : chalk.yellow(landing)}`);
2241
+ statusLines.push(`Landing: ${chalk.dim("omitted")}`);
2215
2242
  if (!bootstrapComplete) {
2216
2243
  return {
2217
2244
  statusLines,
@@ -2221,7 +2248,7 @@ function buildOperatorOverview() {
2221
2248
  ]
2222
2249
  };
2223
2250
  }
2224
- if (api !== "healthy" || web !== "healthy" || landing !== "healthy") {
2251
+ if (api !== "healthy" || web !== "healthy") {
2225
2252
  return {
2226
2253
  statusLines,
2227
2254
  nextSteps: [
@@ -2290,8 +2317,6 @@ function versionedImages(version, edition = "enterprise") {
2290
2317
  const api = edition === "oss" ? `ghcr.io/envsync-cloud/envsync-api:${version}` : `ghcr.io/envsync-cloud/envsync-api-enterprise:${version}`;
2291
2318
  return {
2292
2319
  api,
2293
- // Deprecated field: kept for older deploy.yaml keys; unused by stack render.
2294
- management_api: api,
2295
2320
  keycloak: `envsync-keycloak:${version}`,
2296
2321
  web,
2297
2322
  landing: `ghcr.io/envsync-cloud/envsync-landing-static:${version}`
@@ -2377,7 +2402,6 @@ function normalizeConfig(raw) {
2377
2402
  },
2378
2403
  images: {
2379
2404
  api: !raw.images?.api || isManagedVersionedImage(raw.images.api, "api") ? derivedImages.api : raw.images.api,
2380
- management_api: !raw.images?.management_api || isManagedVersionedImage(raw.images.management_api, "management_api") ? derivedImages.management_api : raw.images.management_api,
2381
2405
  keycloak: !raw.images?.keycloak || isManagedVersionedImage(raw.images.keycloak, "keycloak") ? derivedImages.keycloak : raw.images.keycloak,
2382
2406
  web: !raw.images?.web || isManagedVersionedImage(raw.images.web, "web") ? derivedImages.web : raw.images.web,
2383
2407
  landing: !raw.images?.landing || isManagedVersionedImage(raw.images.landing, "landing") ? derivedImages.landing : raw.images.landing,
@@ -2388,7 +2412,6 @@ function normalizeConfig(raw) {
2388
2412
  services: {
2389
2413
  stack_name: stackName,
2390
2414
  api_port: requireDefined(raw.services?.api_port, "services.api_port"),
2391
- management_api_port: raw.services?.management_api_port ?? 4001,
2392
2415
  public_http_port: raw.services?.public_http_port ?? 80,
2393
2416
  public_https_port: raw.services?.public_https_port ?? 443,
2394
2417
  clickstack_ui_port: requireDefined(raw.services?.clickstack_ui_port, "services.clickstack_ui_port"),
@@ -2489,7 +2512,8 @@ function emptyGeneratedState() {
2489
2512
  openfga_db_password: "",
2490
2513
  minikms_root_key: "",
2491
2514
  minikms_session_signing_key: "",
2492
- minikms_db_password: ""
2515
+ minikms_db_password: "",
2516
+ saml_session_secret: ""
2493
2517
  },
2494
2518
  bootstrap: {
2495
2519
  completed_at: ""
@@ -2526,7 +2550,8 @@ function normalizeGeneratedState(raw) {
2526
2550
  openfga_db_password: raw?.secrets?.openfga_db_password ?? defaults.secrets.openfga_db_password,
2527
2551
  minikms_root_key: raw?.secrets?.minikms_root_key ?? defaults.secrets.minikms_root_key,
2528
2552
  minikms_session_signing_key: raw?.secrets?.minikms_session_signing_key ?? defaults.secrets.minikms_session_signing_key,
2529
- minikms_db_password: raw?.secrets?.minikms_db_password ?? defaults.secrets.minikms_db_password
2553
+ minikms_db_password: raw?.secrets?.minikms_db_password ?? defaults.secrets.minikms_db_password,
2554
+ saml_session_secret: raw?.secrets?.saml_session_secret ?? defaults.secrets.saml_session_secret
2530
2555
  },
2531
2556
  bootstrap: {
2532
2557
  completed_at: raw?.bootstrap?.completed_at ?? defaults.bootstrap.completed_at
@@ -2535,7 +2560,7 @@ function normalizeGeneratedState(raw) {
2535
2560
  }
2536
2561
  function readInternalState() {
2537
2562
  if (!exists2(INTERNAL_CONFIG_JSON)) return null;
2538
- const raw = JSON.parse(fs4.readFileSync(INTERNAL_CONFIG_JSON, "utf8"));
2563
+ const raw = JSON.parse(fs5.readFileSync(INTERNAL_CONFIG_JSON, "utf8"));
2539
2564
  return {
2540
2565
  config: raw.config ? normalizeConfig(raw.config) : void 0,
2541
2566
  generated: normalizeGeneratedState(raw.generated)
@@ -2545,7 +2570,7 @@ function loadConfig() {
2545
2570
  if (!exists2(DEPLOY_YAML)) {
2546
2571
  throw new Error(`Missing deploy config at ${DEPLOY_YAML}. Run setup first.`);
2547
2572
  }
2548
- const raw = fs4.readFileSync(DEPLOY_YAML, "utf8");
2573
+ const raw = fs5.readFileSync(DEPLOY_YAML, "utf8");
2549
2574
  if (raw.trimStart().startsWith("{")) {
2550
2575
  return normalizeConfig(JSON.parse(raw));
2551
2576
  }
@@ -2553,7 +2578,7 @@ function loadConfig() {
2553
2578
  }
2554
2579
  function loadGeneratedEnv() {
2555
2580
  if (!exists2(DEPLOY_ENV)) return {};
2556
- return parseEnvFile(fs4.readFileSync(DEPLOY_ENV, "utf8"));
2581
+ return parseEnvFile(fs5.readFileSync(DEPLOY_ENV, "utf8"));
2557
2582
  }
2558
2583
  function mergeGeneratedState(env, generated) {
2559
2584
  const normalized = normalizeGeneratedState(generated);
@@ -2577,7 +2602,8 @@ function mergeGeneratedState(env, generated) {
2577
2602
  openfga_db_password: env.OPENFGA_DB_PASSWORD ?? normalized.secrets.openfga_db_password,
2578
2603
  minikms_root_key: env.MINIKMS_ROOT_KEY ?? normalized.secrets.minikms_root_key,
2579
2604
  minikms_session_signing_key: env.MINIKMS_SESSION_SIGNING_KEY ?? normalized.secrets.minikms_session_signing_key,
2580
- minikms_db_password: env.MINIKMS_DB_PASSWORD ?? normalized.secrets.minikms_db_password
2605
+ minikms_db_password: env.MINIKMS_DB_PASSWORD ?? normalized.secrets.minikms_db_password,
2606
+ saml_session_secret: env.SAML_SESSION_SECRET ?? normalized.secrets.saml_session_secret
2581
2607
  },
2582
2608
  bootstrap: normalized.bootstrap
2583
2609
  });
@@ -2606,7 +2632,8 @@ function ensureGeneratedRuntimeState(config, generated) {
2606
2632
  openfga_db_password: generated.secrets.openfga_db_password || randomSecret(),
2607
2633
  minikms_root_key: generated.secrets.minikms_root_key || randomBytes2(32).toString("hex"),
2608
2634
  minikms_session_signing_key: generated.secrets.minikms_session_signing_key || generateMinikmsSessionSigningKey(),
2609
- minikms_db_password: generated.secrets.minikms_db_password || randomSecret()
2635
+ minikms_db_password: generated.secrets.minikms_db_password || randomSecret(),
2636
+ saml_session_secret: generated.secrets.saml_session_secret || randomBytes2(32).toString("hex")
2610
2637
  },
2611
2638
  bootstrap: generated.bootstrap
2612
2639
  });
@@ -2839,7 +2866,7 @@ function extractStaticBundle(kind, image, targetDir) {
2839
2866
  logDryRun(`Would extract ${kind} bundle from ${image} into ${targetDir}`);
2840
2867
  return;
2841
2868
  }
2842
- fs4.rmSync(targetDir, { recursive: true, force: true });
2869
+ fs5.rmSync(targetDir, { recursive: true, force: true });
2843
2870
  ensureDir(targetDir);
2844
2871
  const containerId = run("docker", ["create", image], { quiet: true }).trim();
2845
2872
  try {
@@ -2867,23 +2894,23 @@ function writeFrontendRuntimeConfig(targetDir, runtimeConfig) {
2867
2894
  function syncFrontendReleaseContents(sourceDir, targetDir) {
2868
2895
  ensureDir(targetDir);
2869
2896
  const deferredEntries = /* @__PURE__ */ new Set(["index.html", "runtime-config.js"]);
2870
- const entries = fs4.readdirSync(sourceDir, { withFileTypes: true });
2897
+ const entries = fs5.readdirSync(sourceDir, { withFileTypes: true });
2871
2898
  for (const entry of entries) {
2872
2899
  if (deferredEntries.has(entry.name)) {
2873
2900
  continue;
2874
2901
  }
2875
- fs4.cpSync(path4.join(sourceDir, entry.name), path4.join(targetDir, entry.name), {
2902
+ fs5.cpSync(path4.join(sourceDir, entry.name), path4.join(targetDir, entry.name), {
2876
2903
  recursive: true,
2877
2904
  force: true
2878
2905
  });
2879
2906
  }
2880
2907
  const stagedRuntimeConfig = path4.join(sourceDir, "runtime-config.js");
2881
2908
  if (exists2(stagedRuntimeConfig)) {
2882
- fs4.cpSync(stagedRuntimeConfig, path4.join(targetDir, "runtime-config.js"), { force: true });
2909
+ fs5.cpSync(stagedRuntimeConfig, path4.join(targetDir, "runtime-config.js"), { force: true });
2883
2910
  }
2884
2911
  const stagedIndex = path4.join(sourceDir, "index.html");
2885
2912
  if (exists2(stagedIndex)) {
2886
- fs4.cpSync(stagedIndex, path4.join(targetDir, "index.html"), { force: true });
2913
+ fs5.cpSync(stagedIndex, path4.join(targetDir, "index.html"), { force: true });
2887
2914
  }
2888
2915
  }
2889
2916
  function activateFrontendRelease(kind, version, runtimeConfig) {
@@ -3281,7 +3308,7 @@ ${trimmed}`
3281
3308
  }
3282
3309
  function readRenderedRuntimeConfig(filePath) {
3283
3310
  if (!exists2(filePath)) return null;
3284
- const raw = fs4.readFileSync(filePath, "utf8").trim();
3311
+ const raw = fs5.readFileSync(filePath, "utf8").trim();
3285
3312
  const prefix = "window.__ENVSYNC_RUNTIME_CONFIG__ = ";
3286
3313
  if (!raw.startsWith(prefix) || !raw.endsWith(";")) return null;
3287
3314
  try {
@@ -3578,7 +3605,7 @@ function removeTarget(target) {
3578
3605
  logDryRun(`Would remove ${target}`);
3579
3606
  return;
3580
3607
  }
3581
- fs4.rmSync(target, { recursive: true, force: true });
3608
+ fs5.rmSync(target, { recursive: true, force: true });
3582
3609
  logInfo(`Removed ${target}`);
3583
3610
  }
3584
3611
  async function cmdRemove() {
@@ -3977,7 +4004,6 @@ async function cmdSetup() {
3977
4004
  domain: { root_domain: rootDomain, acme_email: acmeEmail },
3978
4005
  images: {
3979
4006
  api: releaseImages.api,
3980
- management_api: releaseImages.management_api,
3981
4007
  keycloak: releaseImages.keycloak,
3982
4008
  web: releaseImages.web,
3983
4009
  landing: releaseImages.landing,
@@ -3988,7 +4014,6 @@ async function cmdSetup() {
3988
4014
  services: {
3989
4015
  stack_name: "envsync",
3990
4016
  api_port: 4e3,
3991
- management_api_port: 4001,
3992
4017
  public_http_port: 80,
3993
4018
  public_https_port: 443,
3994
4019
  clickstack_ui_port: 8080,
@@ -4327,7 +4352,7 @@ async function cmdDeploy() {
4327
4352
  sleepSeconds(3);
4328
4353
  promoted = true;
4329
4354
  if (snapshotPath && !currentOptions.dryRun) {
4330
- fs4.rmSync(snapshotPath, { force: true });
4355
+ fs5.rmSync(snapshotPath, { force: true });
4331
4356
  }
4332
4357
  } catch (error) {
4333
4358
  const rollbackTarget = preUpgradeHead ?? "zero";
@@ -4361,7 +4386,7 @@ ${rollbackError instanceof Error ? rollbackError.message : String(rollbackError)
4361
4386
  deployRenderedStack(config, "rollback recovery");
4362
4387
  waitForApiSlotHealthy(config, recoveredState.deployment.active_slot);
4363
4388
  if (snapshotPath && !config.upgrade.keep_failed_upgrade_db_snapshot && !currentOptions.dryRun) {
4364
- fs4.rmSync(snapshotPath, { force: true });
4389
+ fs5.rmSync(snapshotPath, { force: true });
4365
4390
  }
4366
4391
  throw new Error(
4367
4392
  usedSnapshotRestore ? `Deploy failed after DB migration. Previous release was restored using DB snapshot recovery. ${error instanceof Error ? error.message : String(error)}` : `Deploy failed after DB migration. Previous release was restored. ${error instanceof Error ? error.message : String(error)}`
@@ -4387,7 +4412,6 @@ ${rollbackError instanceof Error ? rollbackError.message : String(rollbackError)
4387
4412
  { label: "openfga", getHealth: (services) => serviceHealth(services, `${config.services.stack_name}_openfga`) },
4388
4413
  { label: "minikms", getHealth: (services) => serviceHealth(services, `${config.services.stack_name}_minikms`) },
4389
4414
  { label: "clickstack", getHealth: (services) => serviceHealth(services, `${config.services.stack_name}_clickstack`) },
4390
- ...isOssConfig2(config) ? [] : [{ label: "landing", getHealth: (services) => serviceHealth(services, `${config.services.stack_name}_landing_nginx`) }],
4391
4415
  { label: "web", getHealth: (services) => serviceHealth(services, `${config.services.stack_name}_web_nginx`) },
4392
4416
  { label: "api", getHealth: (services) => apiHealth(services, config.services.stack_name) }
4393
4417
  ]);
@@ -4430,7 +4454,7 @@ async function cmdPromote(target) {
4430
4454
  }
4431
4455
  });
4432
4456
  writeDeployArtifacts(config, promotedState);
4433
- if (exists2(releaseAssetDir("web", promotedState.deployment.slots[targetSlot].release_version)) && exists2(releaseAssetDir("landing", promotedState.deployment.slots[targetSlot].release_version))) {
4457
+ if (exists2(releaseAssetDir("web", promotedState.deployment.slots[targetSlot].release_version))) {
4434
4458
  activateFrontendReleaseForState(config, promotedState);
4435
4459
  } else {
4436
4460
  logWarn(`Missing staged frontend assets for release ${promotedState.deployment.slots[targetSlot].release_version}; leaving current frontend assets unchanged.`);
@@ -4461,7 +4485,7 @@ async function cmdRollback() {
4461
4485
  }
4462
4486
  });
4463
4487
  writeDeployArtifacts(config, rollbackState);
4464
- if (exists2(releaseAssetDir("web", rollbackState.deployment.slots[rollbackState.deployment.active_slot].release_version)) && exists2(releaseAssetDir("landing", rollbackState.deployment.slots[rollbackState.deployment.active_slot].release_version))) {
4488
+ if (exists2(releaseAssetDir("web", rollbackState.deployment.slots[rollbackState.deployment.active_slot].release_version))) {
4465
4489
  activateFrontendReleaseForState(config, rollbackState);
4466
4490
  } else {
4467
4491
  logWarn(`Missing staged frontend assets for release ${rollbackState.deployment.slots[rollbackState.deployment.active_slot].release_version}; leaving current frontend assets unchanged.`);
@@ -4483,7 +4507,7 @@ async function cmdHealth(asJson) {
4483
4507
  const hosts = domainMap2(config.domain.root_domain);
4484
4508
  const services = listStackServices(config);
4485
4509
  const stackName = config.services.stack_name;
4486
- const traefikDynamic = exists2(TRAEFIK_DYNAMIC_FILE) ? fs4.readFileSync(TRAEFIK_DYNAMIC_FILE, "utf8") : "";
4510
+ const traefikDynamic = exists2(TRAEFIK_DYNAMIC_FILE) ? fs5.readFileSync(TRAEFIK_DYNAMIC_FILE, "utf8") : "";
4487
4511
  const webRuntimeConfig = readRenderedRuntimeConfig(path4.join(RELEASES_ROOT, "web", "current", "runtime-config.js"));
4488
4512
  const landingRuntimeConfig = readRenderedRuntimeConfig(path4.join(RELEASES_ROOT, "landing", "current", "runtime-config.js"));
4489
4513
  const clickstackSearchState = getClickstackSearchState(config);
@@ -4557,7 +4581,7 @@ async function cmdHealth(asJson) {
4557
4581
  }
4558
4582
  },
4559
4583
  web: serviceHealth(services, `${stackName}_web_nginx`),
4560
- landing: serviceHealth(services, `${stackName}_landing_nginx`)
4584
+ landing: "omitted"
4561
4585
  },
4562
4586
  database: {
4563
4587
  api: databaseHealth
@@ -4675,8 +4699,8 @@ async function cmdUpgradeDeps() {
4675
4699
  await cmdDeploy();
4676
4700
  }
4677
4701
  function sha256File(filePath) {
4678
- const hash = createHash2("sha256");
4679
- hash.update(fs4.readFileSync(filePath));
4702
+ const hash = createHash3("sha256");
4703
+ hash.update(fs5.readFileSync(filePath));
4680
4704
  return hash.digest("hex");
4681
4705
  }
4682
4706
  function parseEnterpriseLicenseBundle(raw) {
@@ -4705,7 +4729,7 @@ function parseEnterpriseLicenseBundle(raw) {
4705
4729
  return candidate;
4706
4730
  }
4707
4731
  function readEnterpriseLicenseBundle(bundlePath) {
4708
- return parseEnterpriseLicenseBundle(JSON.parse(fs4.readFileSync(bundlePath, "utf8")));
4732
+ return parseEnterpriseLicenseBundle(JSON.parse(fs5.readFileSync(bundlePath, "utf8")));
4709
4733
  }
4710
4734
  function normalizeLicenseServerUrl(serverUrl, route) {
4711
4735
  const trimmed = serverUrl.replace(/\/+$/, "");
@@ -4967,19 +4991,19 @@ async function cmdBackup() {
4967
4991
  logWarn("Backup will temporarily stop the EnvSync stack to capture consistent volume data.");
4968
4992
  stopManagedRuntime(config, "Stopping existing EnvSync services for consistent backup");
4969
4993
  }
4970
- writeFile(path4.join(staged, "deploy.env"), fs4.readFileSync(DEPLOY_ENV, "utf8"));
4971
- writeFile(path4.join(staged, "deploy.yaml"), fs4.readFileSync(DEPLOY_YAML, "utf8"));
4972
- writeFile(path4.join(staged, "config.json"), fs4.readFileSync(INTERNAL_CONFIG_JSON, "utf8"));
4973
- writeFile(path4.join(staged, "versions.lock.json"), fs4.readFileSync(VERSIONS_LOCK, "utf8"));
4974
- writeFile(path4.join(staged, "docker-stack.bootstrap.base.yaml"), fs4.readFileSync(BOOTSTRAP_BASE_STACK_FILE, "utf8"));
4975
- writeFile(path4.join(staged, "docker-stack.bootstrap.yaml"), fs4.readFileSync(BOOTSTRAP_STACK_FILE, "utf8"));
4976
- writeFile(path4.join(staged, "docker-stack.yaml"), fs4.readFileSync(STACK_FILE, "utf8"));
4977
- writeFile(path4.join(staged, "traefik-dynamic.yaml"), fs4.readFileSync(TRAEFIK_DYNAMIC_FILE, "utf8"));
4978
- writeFile(path4.join(staged, "keycloak-realm.envsync.json"), fs4.readFileSync(KEYCLOAK_REALM_FILE, "utf8"));
4979
- writeFile(path4.join(staged, "otel-agent.yaml"), fs4.readFileSync(OTEL_AGENT_CONF, "utf8"));
4980
- writeFile(path4.join(staged, "clickhouse-listen.xml"), fs4.readFileSync(CLICKSTACK_CLICKHOUSE_CONF, "utf8"));
4994
+ writeFile(path4.join(staged, "deploy.env"), fs5.readFileSync(DEPLOY_ENV, "utf8"));
4995
+ writeFile(path4.join(staged, "deploy.yaml"), fs5.readFileSync(DEPLOY_YAML, "utf8"));
4996
+ writeFile(path4.join(staged, "config.json"), fs5.readFileSync(INTERNAL_CONFIG_JSON, "utf8"));
4997
+ writeFile(path4.join(staged, "versions.lock.json"), fs5.readFileSync(VERSIONS_LOCK, "utf8"));
4998
+ writeFile(path4.join(staged, "docker-stack.bootstrap.base.yaml"), fs5.readFileSync(BOOTSTRAP_BASE_STACK_FILE, "utf8"));
4999
+ writeFile(path4.join(staged, "docker-stack.bootstrap.yaml"), fs5.readFileSync(BOOTSTRAP_STACK_FILE, "utf8"));
5000
+ writeFile(path4.join(staged, "docker-stack.yaml"), fs5.readFileSync(STACK_FILE, "utf8"));
5001
+ writeFile(path4.join(staged, "traefik-dynamic.yaml"), fs5.readFileSync(TRAEFIK_DYNAMIC_FILE, "utf8"));
5002
+ writeFile(path4.join(staged, "keycloak-realm.envsync.json"), fs5.readFileSync(KEYCLOAK_REALM_FILE, "utf8"));
5003
+ writeFile(path4.join(staged, "otel-agent.yaml"), fs5.readFileSync(OTEL_AGENT_CONF, "utf8"));
5004
+ writeFile(path4.join(staged, "clickhouse-listen.xml"), fs5.readFileSync(CLICKSTACK_CLICKHOUSE_CONF, "utf8"));
4981
5005
  if (!isOssConfig2(config) && exists2(LICENSE_ROOT)) {
4982
- fs4.cpSync(LICENSE_ROOT, path4.join(staged, "license"), { recursive: true });
5006
+ fs5.cpSync(LICENSE_ROOT, path4.join(staged, "license"), { recursive: true });
4983
5007
  }
4984
5008
  const volumesDir = path4.join(staged, "volumes");
4985
5009
  for (const volume of STACK_VOLUMES) {
@@ -5046,21 +5070,21 @@ async function cmdRestore(archivePath, autoDeploy = false) {
5046
5070
  }
5047
5071
  ensureDir(restoreRoot);
5048
5072
  run("bash", ["-lc", `tar -xzf ${JSON.stringify(archivePath)} -C ${JSON.stringify(restoreRoot)}`]);
5049
- writeFile(DEPLOY_ENV, fs4.readFileSync(path4.join(restoreRoot, "deploy.env"), "utf8"), 384);
5050
- writeFile(DEPLOY_YAML, fs4.readFileSync(path4.join(restoreRoot, "deploy.yaml"), "utf8"));
5051
- writeFile(INTERNAL_CONFIG_JSON, fs4.readFileSync(path4.join(restoreRoot, "config.json"), "utf8"));
5052
- writeFile(VERSIONS_LOCK, fs4.readFileSync(path4.join(restoreRoot, "versions.lock.json"), "utf8"));
5053
- writeFile(BOOTSTRAP_BASE_STACK_FILE, fs4.readFileSync(path4.join(restoreRoot, "docker-stack.bootstrap.base.yaml"), "utf8"));
5054
- writeFile(BOOTSTRAP_STACK_FILE, fs4.readFileSync(path4.join(restoreRoot, "docker-stack.bootstrap.yaml"), "utf8"));
5055
- writeFile(STACK_FILE, fs4.readFileSync(path4.join(restoreRoot, "docker-stack.yaml"), "utf8"));
5056
- writeFile(TRAEFIK_DYNAMIC_FILE, fs4.readFileSync(path4.join(restoreRoot, "traefik-dynamic.yaml"), "utf8"));
5057
- writeFile(KEYCLOAK_REALM_FILE, fs4.readFileSync(path4.join(restoreRoot, "keycloak-realm.envsync.json"), "utf8"));
5058
- writeFile(OTEL_AGENT_CONF, fs4.readFileSync(path4.join(restoreRoot, "otel-agent.yaml"), "utf8"));
5059
- writeFile(CLICKSTACK_CLICKHOUSE_CONF, fs4.readFileSync(path4.join(restoreRoot, "clickhouse-listen.xml"), "utf8"));
5073
+ writeFile(DEPLOY_ENV, fs5.readFileSync(path4.join(restoreRoot, "deploy.env"), "utf8"), 384);
5074
+ writeFile(DEPLOY_YAML, fs5.readFileSync(path4.join(restoreRoot, "deploy.yaml"), "utf8"));
5075
+ writeFile(INTERNAL_CONFIG_JSON, fs5.readFileSync(path4.join(restoreRoot, "config.json"), "utf8"));
5076
+ writeFile(VERSIONS_LOCK, fs5.readFileSync(path4.join(restoreRoot, "versions.lock.json"), "utf8"));
5077
+ writeFile(BOOTSTRAP_BASE_STACK_FILE, fs5.readFileSync(path4.join(restoreRoot, "docker-stack.bootstrap.base.yaml"), "utf8"));
5078
+ writeFile(BOOTSTRAP_STACK_FILE, fs5.readFileSync(path4.join(restoreRoot, "docker-stack.bootstrap.yaml"), "utf8"));
5079
+ writeFile(STACK_FILE, fs5.readFileSync(path4.join(restoreRoot, "docker-stack.yaml"), "utf8"));
5080
+ writeFile(TRAEFIK_DYNAMIC_FILE, fs5.readFileSync(path4.join(restoreRoot, "traefik-dynamic.yaml"), "utf8"));
5081
+ writeFile(KEYCLOAK_REALM_FILE, fs5.readFileSync(path4.join(restoreRoot, "keycloak-realm.envsync.json"), "utf8"));
5082
+ writeFile(OTEL_AGENT_CONF, fs5.readFileSync(path4.join(restoreRoot, "otel-agent.yaml"), "utf8"));
5083
+ writeFile(CLICKSTACK_CLICKHOUSE_CONF, fs5.readFileSync(path4.join(restoreRoot, "clickhouse-listen.xml"), "utf8"));
5060
5084
  const restoredLicenseRoot = path4.join(restoreRoot, "license");
5061
5085
  if (exists2(restoredLicenseRoot)) {
5062
- fs4.rmSync(LICENSE_ROOT, { recursive: true, force: true });
5063
- fs4.cpSync(restoredLicenseRoot, LICENSE_ROOT, { recursive: true });
5086
+ fs5.rmSync(LICENSE_ROOT, { recursive: true, force: true });
5087
+ fs5.cpSync(restoredLicenseRoot, LICENSE_ROOT, { recursive: true });
5064
5088
  ensureEnterpriseLicenseFilesReadable();
5065
5089
  }
5066
5090
  const config = loadConfig();
package/dist/index.js CHANGED
@@ -243,13 +243,11 @@ var init_src = __esm({
243
243
  services: z.object({
244
244
  stack_name: z.string().default("envsync"),
245
245
  api_port: z.number().int().positive().default(4e3),
246
- management_api_port: z.number().int().positive().default(4001),
247
246
  public_http_port: z.number().int().positive().default(80),
248
247
  public_https_port: z.number().int().positive().default(443)
249
248
  }).default({}),
250
249
  images: z.object({
251
250
  api: z.string().default("ghcr.io/envsync-cloud/envsync-api:stable"),
252
- management_api: z.string().default("ghcr.io/envsync-cloud/envsync-management-api:stable"),
253
251
  web: z.string().default("ghcr.io/envsync-cloud/envsync-web-oss-static:stable"),
254
252
  enterprise_web: z.string().default("ghcr.io/envsync-cloud/envsync-web-static:stable"),
255
253
  landing: z.string().default("ghcr.io/envsync-cloud/envsync-landing-static:stable"),
@@ -287,6 +285,16 @@ var init_src = __esm({
287
285
  });
288
286
 
289
287
  // src/render.ts
288
+ import { createHash } from "crypto";
289
+ import fs2 from "fs";
290
+ function swarmConfigObjectName(stackName, logical, filePath, fallbackVersion) {
291
+ let token = fallbackVersion.replace(/[^a-zA-Z0-9]+/g, "_").replace(/^_|_$/g, "") || "v";
292
+ try {
293
+ token = createHash("sha256").update(fs2.readFileSync(filePath)).digest("hex").slice(0, 12);
294
+ } catch {
295
+ }
296
+ return `${stackName}_${logical}_${token}`;
297
+ }
290
298
  function domainMap(rootDomain) {
291
299
  return {
292
300
  landing: rootDomain,
@@ -384,7 +392,6 @@ function buildRuntimeEnv2(config, generated, options) {
384
392
  ENVSYNC_STACK_NAME: config.services.stack_name,
385
393
  DB_AUTO_MIGRATE: "false",
386
394
  PORT: `${config.services.api_port}`,
387
- MANAGEMENT_API_PORT: `${config.services.management_api_port}`,
388
395
  DATABASE_HOST: "postgres",
389
396
  DATABASE_PORT: "5432",
390
397
  DATABASE_USER: "postgres",
@@ -428,7 +435,13 @@ function buildRuntimeEnv2(config, generated, options) {
428
435
  OPENFGA_MODEL_ID: generated.openfga.model_id,
429
436
  OPENFGA_DB_PASSWORD: generated.secrets.openfga_db_password,
430
437
  API_URL: publicHttpsUrl(config, hosts.api),
438
+ SAML_SESSION_SECRET: generated.secrets.saml_session_secret,
431
439
  MANAGEMENT_API_URL: oss ? "" : publicHttpsUrl(config, hosts.api, "/api/v1/manage"),
440
+ KEYCLOAK_ACCESS_TOKEN_LIFESPAN_SECONDS: "3600",
441
+ KEYCLOAK_SSO_SESSION_IDLE_TIMEOUT_SECONDS: "604800",
442
+ KEYCLOAK_SSO_SESSION_MAX_LIFESPAN_SECONDS: "604800",
443
+ KEYCLOAK_CLIENT_SESSION_IDLE_TIMEOUT_SECONDS: "604800",
444
+ KEYCLOAK_CLIENT_SESSION_MAX_LIFESPAN_SECONDS: "604800",
432
445
  CLICKSTACK_OPERATOR_EMAIL: generated.clickstack.operator_email,
433
446
  CLICKSTACK_OPERATOR_PASSWORD: generated.clickstack.operator_password,
434
447
  CLICKSTACK_ACCESS_KEY: generated.clickstack.access_key,
@@ -467,12 +480,22 @@ function renderKeycloakRealm(config, runtimeEnv) {
467
480
  const webOrigins = publicHttpsOriginVariants(config, hosts.app);
468
481
  const apiRedirectUris = publicHttpsUrlVariants(config, hosts.api, "/api/access/api/callback");
469
482
  const apiOrigins = publicHttpsOriginVariants(config, hosts.api);
483
+ const accessTokenLifespan = Number(runtimeEnv.KEYCLOAK_ACCESS_TOKEN_LIFESPAN_SECONDS || "3600");
484
+ const ssoSessionIdleTimeout = Number(runtimeEnv.KEYCLOAK_SSO_SESSION_IDLE_TIMEOUT_SECONDS || "604800");
485
+ const ssoSessionMaxLifespan = Number(runtimeEnv.KEYCLOAK_SSO_SESSION_MAX_LIFESPAN_SECONDS || "604800");
486
+ const clientSessionIdleTimeout = Number(runtimeEnv.KEYCLOAK_CLIENT_SESSION_IDLE_TIMEOUT_SECONDS || "604800");
487
+ const clientSessionMaxLifespan = Number(runtimeEnv.KEYCLOAK_CLIENT_SESSION_MAX_LIFESPAN_SECONDS || "604800");
470
488
  return JSON.stringify(
471
489
  {
472
490
  realm: config.auth.keycloak_realm,
473
491
  enabled: true,
474
492
  loginTheme: "envsync",
475
493
  emailTheme: "envsync",
494
+ accessTokenLifespan,
495
+ ssoSessionIdleTimeout,
496
+ ssoSessionMaxLifespan,
497
+ clientSessionIdleTimeout,
498
+ clientSessionMaxLifespan,
476
499
  clients: [
477
500
  {
478
501
  clientId: config.auth.web_client_id,
@@ -525,7 +548,7 @@ function renderTraefikDynamicConfig(config, generated) {
525
548
  const landingEnabled = false;
526
549
  const managementEnabled = !isOssConfig(config);
527
550
  const otelAllowedOrigins = [
528
- ...landingEnabled ? publicHttpsOriginVariants(config, hosts.landing) : [],
551
+ ...publicHttpsOriginVariants(config, hosts.landing),
529
552
  ...publicHttpsOriginVariants(config, hosts.app)
530
553
  ];
531
554
  return [
@@ -909,6 +932,7 @@ function renderStack(config, runtimeEnv, generated, mode, paths) {
909
932
  const apiLicenseVolume = managementEnabled ? "\n volumes:\n - /etc/envsync/license:/etc/envsync/license:ro" : "";
910
933
  const deployment = createSteadyApiDeploymentState(config, generated);
911
934
  const stackName = config.services.stack_name;
935
+ const configName = (logical, filePath) => swarmConfigObjectName(stackName, logical, filePath, config.release.version);
912
936
  const s3RouterName = `${stackName}-s3-router`;
913
937
  const s3ServiceName = `${stackName}-s3-service`;
914
938
  const s3ConsoleRouterName = `${stackName}-s3-console-router`;
@@ -1018,7 +1042,7 @@ ${includeRuntimeInfra ? `
1018
1042
  image: ${config.images.keycloak}
1019
1043
  entrypoint: ["/bin/sh", "-lc"]
1020
1044
  command:
1021
- - /opt/keycloak/bin/kc.sh import --dir /opt/keycloak/data/import --override true && exec /opt/keycloak/bin/kc.sh start --optimized
1045
+ - /opt/keycloak/bin/kc.sh import --dir /opt/keycloak/data/import --override false && exec /opt/keycloak/bin/kc.sh start --optimized
1022
1046
  environment:
1023
1047
  ${renderEnvList({
1024
1048
  KC_DB: "postgres",
@@ -1200,17 +1224,23 @@ volumes:
1200
1224
 
1201
1225
  configs:
1202
1226
  keycloak_realm:
1227
+ name: ${configName("keycloak_realm", paths.keycloakRealmFile)}
1203
1228
  file: ${paths.keycloakRealmFile}
1204
1229
  clickstack_clickhouse_conf:
1230
+ name: ${configName("clickstack_clickhouse_conf", paths.clickstackClickhouseConf)}
1205
1231
  file: ${paths.clickstackClickhouseConf}
1206
1232
  otel_agent_conf:
1233
+ name: ${configName("otel_agent_conf", paths.otelAgentConf)}
1207
1234
  file: ${paths.otelAgentConf}
1208
1235
  ${landingEnabled ? ` nginx_landing_conf:
1236
+ name: ${configName("nginx_landing_conf", paths.nginxLandingConf)}
1209
1237
  file: ${paths.nginxLandingConf}
1210
1238
  ` : ""}
1211
1239
  nginx_web_conf:
1240
+ name: ${configName("nginx_web_conf", paths.nginxWebConf)}
1212
1241
  file: ${paths.nginxWebConf}
1213
1242
  nginx_api_maintenance_conf:
1243
+ name: ${configName("nginx_api_maintenance_conf", paths.nginxApiMaintenanceConf)}
1214
1244
  file: ${paths.nginxApiMaintenanceConf}
1215
1245
  `.trimStart();
1216
1246
  }
@@ -1226,32 +1256,32 @@ var init_render = __esm({
1226
1256
  });
1227
1257
 
1228
1258
  // src/static-bundle.ts
1229
- import fs2 from "fs";
1259
+ import fs3 from "fs";
1230
1260
  import path2 from "path";
1231
1261
  function exists(target) {
1232
- return fs2.existsSync(target);
1262
+ return fs3.existsSync(target);
1233
1263
  }
1234
1264
  function normalizeExtractedStaticBundle(kind, targetDir) {
1235
1265
  const directIndex = path2.join(targetDir, "index.html");
1236
1266
  if (exists(directIndex)) {
1237
1267
  return;
1238
1268
  }
1239
- const childDirs = fs2.readdirSync(targetDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => path2.join(targetDir, entry.name));
1269
+ const childDirs = fs3.readdirSync(targetDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => path2.join(targetDir, entry.name));
1240
1270
  const candidateRoots = childDirs.filter((dir) => exists(path2.join(dir, "index.html")));
1241
1271
  if (candidateRoots.length !== 1) {
1242
1272
  return;
1243
1273
  }
1244
1274
  const nestedRoot = candidateRoots[0];
1245
- for (const entry of fs2.readdirSync(nestedRoot)) {
1246
- fs2.cpSync(path2.join(nestedRoot, entry), path2.join(targetDir, entry), { recursive: true });
1275
+ for (const entry of fs3.readdirSync(nestedRoot)) {
1276
+ fs3.cpSync(path2.join(nestedRoot, entry), path2.join(targetDir, entry), { recursive: true });
1247
1277
  }
1248
- fs2.rmSync(nestedRoot, { recursive: true, force: true });
1278
+ fs3.rmSync(nestedRoot, { recursive: true, force: true });
1249
1279
  }
1250
1280
  function validateStaticBundle(kind, targetDir) {
1251
1281
  if (exists(path2.join(targetDir, "index.html"))) {
1252
1282
  return;
1253
1283
  }
1254
- const entries = exists(targetDir) ? fs2.readdirSync(targetDir).slice(0, 20).join(", ") : "";
1284
+ const entries = exists(targetDir) ? fs3.readdirSync(targetDir).slice(0, 20).join(", ") : "";
1255
1285
  throw new Error(
1256
1286
  `Invalid ${kind} static bundle at ${targetDir}: missing index.html${entries ? `. Found: ${entries}` : ""}`
1257
1287
  );
@@ -1263,8 +1293,8 @@ var init_static_bundle = __esm({
1263
1293
  });
1264
1294
 
1265
1295
  // src/org-setup.ts
1266
- import { createHash, randomBytes } from "crypto";
1267
- import fs3 from "fs";
1296
+ import { createHash as createHash2, randomBytes } from "crypto";
1297
+ import fs4 from "fs";
1268
1298
  import path3 from "path";
1269
1299
  import readline from "readline";
1270
1300
  function generateSetupToken() {
@@ -1273,33 +1303,33 @@ function generateSetupToken() {
1273
1303
  function ensureSetupTokenFile(tokenFile, existingToken) {
1274
1304
  if (existingToken?.trim()) {
1275
1305
  const dir2 = path3.dirname(tokenFile);
1276
- fs3.mkdirSync(dir2, { recursive: true, mode: 448 });
1277
- fs3.writeFileSync(tokenFile, `${existingToken.trim()}
1306
+ fs4.mkdirSync(dir2, { recursive: true, mode: 448 });
1307
+ fs4.writeFileSync(tokenFile, `${existingToken.trim()}
1278
1308
  `, { mode: 384 });
1279
1309
  return existingToken.trim();
1280
1310
  }
1281
- if (fs3.existsSync(tokenFile)) {
1282
- const fromFile = fs3.readFileSync(tokenFile, "utf8").trim();
1311
+ if (fs4.existsSync(tokenFile)) {
1312
+ const fromFile = fs4.readFileSync(tokenFile, "utf8").trim();
1283
1313
  if (fromFile.length >= 16) {
1284
1314
  return fromFile;
1285
1315
  }
1286
1316
  }
1287
1317
  const token = generateSetupToken();
1288
1318
  const dir = path3.dirname(tokenFile);
1289
- fs3.mkdirSync(dir, { recursive: true, mode: 448 });
1290
- fs3.writeFileSync(tokenFile, `${token}
1319
+ fs4.mkdirSync(dir, { recursive: true, mode: 448 });
1320
+ fs4.writeFileSync(tokenFile, `${token}
1291
1321
  `, { mode: 384 });
1292
1322
  return token;
1293
1323
  }
1294
1324
  function readSetupTokenFile(tokenFile) {
1295
- if (!fs3.existsSync(tokenFile)) {
1325
+ if (!fs4.existsSync(tokenFile)) {
1296
1326
  return null;
1297
1327
  }
1298
- const token = fs3.readFileSync(tokenFile, "utf8").trim();
1328
+ const token = fs4.readFileSync(tokenFile, "utf8").trim();
1299
1329
  return token.length >= 16 ? token : null;
1300
1330
  }
1301
1331
  function setupTokenFingerprint(token) {
1302
- return createHash("sha256").update(token).digest("hex").slice(0, 12);
1332
+ return createHash2("sha256").update(token).digest("hex").slice(0, 12);
1303
1333
  }
1304
1334
  async function fetchSetupStatus(apiBaseUrl, setupToken) {
1305
1335
  const base = apiBaseUrl.replace(/\/$/, "");
@@ -1442,9 +1472,9 @@ var init_org_setup = __esm({
1442
1472
 
1443
1473
  // src/cli.ts
1444
1474
  var cli_exports = {};
1445
- import { createHash as createHash2, generateKeyPairSync, randomBytes as randomBytes2, randomInt } from "crypto";
1475
+ import { createHash as createHash3, generateKeyPairSync, randomBytes as randomBytes2, randomInt } from "crypto";
1446
1476
  import { spawnSync } from "child_process";
1447
- import fs4 from "fs";
1477
+ import fs5 from "fs";
1448
1478
  import path4 from "path";
1449
1479
  import readline2 from "readline";
1450
1480
  import chalk from "chalk";
@@ -1638,12 +1668,12 @@ ${typeof result.stderr === "string" ? result.stderr : ""}`.toLowerCase();
1638
1668
  ${stderr}` : ""}`);
1639
1669
  }
1640
1670
  function ensureDir(dir) {
1641
- fs4.mkdirSync(dir, { recursive: true });
1671
+ fs5.mkdirSync(dir, { recursive: true });
1642
1672
  }
1643
1673
  function writeFile(target, content, mode) {
1644
1674
  ensureDir(path4.dirname(target));
1645
- fs4.writeFileSync(target, content, "utf8");
1646
- if (mode != null) fs4.chmodSync(target, mode);
1675
+ fs5.writeFileSync(target, content, "utf8");
1676
+ if (mode != null) fs5.chmodSync(target, mode);
1647
1677
  }
1648
1678
  function writeFileMaybe(target, content, mode) {
1649
1679
  if (currentOptions.dryRun) {
@@ -1655,7 +1685,7 @@ function writeFileMaybe(target, content, mode) {
1655
1685
  function ensureEnterpriseLicenseFilesReadable() {
1656
1686
  if (exists2(LICENSE_ROOT)) {
1657
1687
  try {
1658
- fs4.chmodSync(LICENSE_ROOT, LICENSE_DIR_MODE);
1688
+ fs5.chmodSync(LICENSE_ROOT, LICENSE_DIR_MODE);
1659
1689
  } catch {
1660
1690
  throw new Error(
1661
1691
  `Cannot update directory mode for ${LICENSE_ROOT}. Fix by running: chmod ${LICENSE_DIR_MODE.toString(8)} ${LICENSE_ROOT}`
@@ -1665,7 +1695,7 @@ function ensureEnterpriseLicenseFilesReadable() {
1665
1695
  for (const filePath of [LICENSE_BUNDLE_FILE, LICENSE_CERT_FILE, LICENSE_KEY_FILE, LICENSE_ROOT_CA_FILE]) {
1666
1696
  if (!exists2(filePath)) continue;
1667
1697
  try {
1668
- fs4.chmodSync(filePath, LICENSE_FILE_MODE);
1698
+ fs5.chmodSync(filePath, LICENSE_FILE_MODE);
1669
1699
  } catch {
1670
1700
  throw new Error(
1671
1701
  `Cannot update file mode for ${filePath}. Fix by running: chmod ${LICENSE_FILE_MODE.toString(8)} ${filePath}`
@@ -1674,7 +1704,7 @@ function ensureEnterpriseLicenseFilesReadable() {
1674
1704
  }
1675
1705
  }
1676
1706
  function exists2(target) {
1677
- return fs4.existsSync(target);
1707
+ return fs5.existsSync(target);
1678
1708
  }
1679
1709
  function randomSecret(bytes = 24) {
1680
1710
  return randomBytes2(bytes).toString("hex");
@@ -1684,7 +1714,7 @@ function generateMinikmsSessionSigningKey() {
1684
1714
  return privateKey.export({ type: "pkcs8", format: "pem" }).toString();
1685
1715
  }
1686
1716
  function deterministicInstallFingerprint(rootDomain, stackName) {
1687
- return `envsync-${createHash2("sha256").update(`${rootDomain}:${stackName}`).digest("hex").slice(0, 32)}`;
1717
+ return `envsync-${createHash3("sha256").update(`${rootDomain}:${stackName}`).digest("hex").slice(0, 32)}`;
1688
1718
  }
1689
1719
  function randomStrongPassword() {
1690
1720
  return `EnvSync!${randomBytes2(8).toString("hex")}Aa1`;
@@ -1788,7 +1818,7 @@ function normalizeNetutilsForwards(raw, stackName, enabled) {
1788
1818
  }
1789
1819
  function loadWireguardState() {
1790
1820
  if (!exists2(WIREGUARD_STATE_FILE)) return null;
1791
- const raw = JSON.parse(fs4.readFileSync(WIREGUARD_STATE_FILE, "utf8"));
1821
+ const raw = JSON.parse(fs5.readFileSync(WIREGUARD_STATE_FILE, "utf8"));
1792
1822
  return {
1793
1823
  private_key: raw.private_key,
1794
1824
  public_key: raw.public_key,
@@ -1972,7 +2002,7 @@ function publicHttpsUrl2(config, host, path5 = "") {
1972
2002
  function getDeployCliVersion() {
1973
2003
  try {
1974
2004
  const packageJsonPath = new URL("../package.json", import.meta.url);
1975
- const raw = fs4.readFileSync(packageJsonPath, "utf8");
2005
+ const raw = fs5.readFileSync(packageJsonPath, "utf8");
1976
2006
  return JSON.parse(raw).version ?? "0.0.0";
1977
2007
  } catch {
1978
2008
  return process.env.npm_package_version ?? "0.0.0";
@@ -2064,7 +2094,6 @@ function buildOperatorOverview() {
2064
2094
  const bootstrapComplete = hasCompleteBootstrapState(generated) && generated.bootstrap.completed_at.length > 0;
2065
2095
  const api = apiHealth(services, config.services.stack_name);
2066
2096
  const web = serviceHealth(services, `${config.services.stack_name}_web_nginx`);
2067
- const landing = serviceHealth(services, `${config.services.stack_name}_landing_nginx`);
2068
2097
  statusLines.push(`Configured: ${chalk.green("yes")}`);
2069
2098
  statusLines.push(`Pinned release: ${chalk.cyan(config.release.version)}`);
2070
2099
  statusLines.push(`Stack: ${stackRunning ? chalk.green(config.services.stack_name) : chalk.yellow("not running")}`);
@@ -2072,7 +2101,7 @@ function buildOperatorOverview() {
2072
2101
  statusLines.push(`Active API slot: ${chalk.cyan(generated.deployment.active_slot)}`);
2073
2102
  statusLines.push(`API: ${api === "healthy" ? chalk.green(api) : api === "missing" ? chalk.red(api) : chalk.yellow(api)}`);
2074
2103
  statusLines.push(`Web: ${web === "healthy" ? chalk.green(web) : web === "missing" ? chalk.red(web) : chalk.yellow(web)}`);
2075
- statusLines.push(`Landing: ${landing === "healthy" ? chalk.green(landing) : landing === "missing" ? chalk.red(landing) : chalk.yellow(landing)}`);
2104
+ statusLines.push(`Landing: ${chalk.dim("omitted")}`);
2076
2105
  if (!bootstrapComplete) {
2077
2106
  return {
2078
2107
  statusLines,
@@ -2082,7 +2111,7 @@ function buildOperatorOverview() {
2082
2111
  ]
2083
2112
  };
2084
2113
  }
2085
- if (api !== "healthy" || web !== "healthy" || landing !== "healthy") {
2114
+ if (api !== "healthy" || web !== "healthy") {
2086
2115
  return {
2087
2116
  statusLines,
2088
2117
  nextSteps: [
@@ -2151,8 +2180,6 @@ function versionedImages(version, edition = "enterprise") {
2151
2180
  const api = edition === "oss" ? `ghcr.io/envsync-cloud/envsync-api:${version}` : `ghcr.io/envsync-cloud/envsync-api-enterprise:${version}`;
2152
2181
  return {
2153
2182
  api,
2154
- // Deprecated field: kept for older deploy.yaml keys; unused by stack render.
2155
- management_api: api,
2156
2183
  keycloak: `envsync-keycloak:${version}`,
2157
2184
  web,
2158
2185
  landing: `ghcr.io/envsync-cloud/envsync-landing-static:${version}`
@@ -2238,7 +2265,6 @@ function normalizeConfig(raw) {
2238
2265
  },
2239
2266
  images: {
2240
2267
  api: !raw.images?.api || isManagedVersionedImage(raw.images.api, "api") ? derivedImages.api : raw.images.api,
2241
- management_api: !raw.images?.management_api || isManagedVersionedImage(raw.images.management_api, "management_api") ? derivedImages.management_api : raw.images.management_api,
2242
2268
  keycloak: !raw.images?.keycloak || isManagedVersionedImage(raw.images.keycloak, "keycloak") ? derivedImages.keycloak : raw.images.keycloak,
2243
2269
  web: !raw.images?.web || isManagedVersionedImage(raw.images.web, "web") ? derivedImages.web : raw.images.web,
2244
2270
  landing: !raw.images?.landing || isManagedVersionedImage(raw.images.landing, "landing") ? derivedImages.landing : raw.images.landing,
@@ -2249,7 +2275,6 @@ function normalizeConfig(raw) {
2249
2275
  services: {
2250
2276
  stack_name: stackName,
2251
2277
  api_port: requireDefined(raw.services?.api_port, "services.api_port"),
2252
- management_api_port: raw.services?.management_api_port ?? 4001,
2253
2278
  public_http_port: raw.services?.public_http_port ?? 80,
2254
2279
  public_https_port: raw.services?.public_https_port ?? 443,
2255
2280
  clickstack_ui_port: requireDefined(raw.services?.clickstack_ui_port, "services.clickstack_ui_port"),
@@ -2350,7 +2375,8 @@ function emptyGeneratedState() {
2350
2375
  openfga_db_password: "",
2351
2376
  minikms_root_key: "",
2352
2377
  minikms_session_signing_key: "",
2353
- minikms_db_password: ""
2378
+ minikms_db_password: "",
2379
+ saml_session_secret: ""
2354
2380
  },
2355
2381
  bootstrap: {
2356
2382
  completed_at: ""
@@ -2387,7 +2413,8 @@ function normalizeGeneratedState(raw) {
2387
2413
  openfga_db_password: raw?.secrets?.openfga_db_password ?? defaults.secrets.openfga_db_password,
2388
2414
  minikms_root_key: raw?.secrets?.minikms_root_key ?? defaults.secrets.minikms_root_key,
2389
2415
  minikms_session_signing_key: raw?.secrets?.minikms_session_signing_key ?? defaults.secrets.minikms_session_signing_key,
2390
- minikms_db_password: raw?.secrets?.minikms_db_password ?? defaults.secrets.minikms_db_password
2416
+ minikms_db_password: raw?.secrets?.minikms_db_password ?? defaults.secrets.minikms_db_password,
2417
+ saml_session_secret: raw?.secrets?.saml_session_secret ?? defaults.secrets.saml_session_secret
2391
2418
  },
2392
2419
  bootstrap: {
2393
2420
  completed_at: raw?.bootstrap?.completed_at ?? defaults.bootstrap.completed_at
@@ -2396,7 +2423,7 @@ function normalizeGeneratedState(raw) {
2396
2423
  }
2397
2424
  function readInternalState() {
2398
2425
  if (!exists2(INTERNAL_CONFIG_JSON)) return null;
2399
- const raw = JSON.parse(fs4.readFileSync(INTERNAL_CONFIG_JSON, "utf8"));
2426
+ const raw = JSON.parse(fs5.readFileSync(INTERNAL_CONFIG_JSON, "utf8"));
2400
2427
  return {
2401
2428
  config: raw.config ? normalizeConfig(raw.config) : void 0,
2402
2429
  generated: normalizeGeneratedState(raw.generated)
@@ -2406,7 +2433,7 @@ function loadConfig() {
2406
2433
  if (!exists2(DEPLOY_YAML)) {
2407
2434
  throw new Error(`Missing deploy config at ${DEPLOY_YAML}. Run setup first.`);
2408
2435
  }
2409
- const raw = fs4.readFileSync(DEPLOY_YAML, "utf8");
2436
+ const raw = fs5.readFileSync(DEPLOY_YAML, "utf8");
2410
2437
  if (raw.trimStart().startsWith("{")) {
2411
2438
  return normalizeConfig(JSON.parse(raw));
2412
2439
  }
@@ -2414,7 +2441,7 @@ function loadConfig() {
2414
2441
  }
2415
2442
  function loadGeneratedEnv() {
2416
2443
  if (!exists2(DEPLOY_ENV)) return {};
2417
- return parseEnvFile(fs4.readFileSync(DEPLOY_ENV, "utf8"));
2444
+ return parseEnvFile(fs5.readFileSync(DEPLOY_ENV, "utf8"));
2418
2445
  }
2419
2446
  function mergeGeneratedState(env, generated) {
2420
2447
  const normalized = normalizeGeneratedState(generated);
@@ -2438,7 +2465,8 @@ function mergeGeneratedState(env, generated) {
2438
2465
  openfga_db_password: env.OPENFGA_DB_PASSWORD ?? normalized.secrets.openfga_db_password,
2439
2466
  minikms_root_key: env.MINIKMS_ROOT_KEY ?? normalized.secrets.minikms_root_key,
2440
2467
  minikms_session_signing_key: env.MINIKMS_SESSION_SIGNING_KEY ?? normalized.secrets.minikms_session_signing_key,
2441
- minikms_db_password: env.MINIKMS_DB_PASSWORD ?? normalized.secrets.minikms_db_password
2468
+ minikms_db_password: env.MINIKMS_DB_PASSWORD ?? normalized.secrets.minikms_db_password,
2469
+ saml_session_secret: env.SAML_SESSION_SECRET ?? normalized.secrets.saml_session_secret
2442
2470
  },
2443
2471
  bootstrap: normalized.bootstrap
2444
2472
  });
@@ -2467,7 +2495,8 @@ function ensureGeneratedRuntimeState(config, generated) {
2467
2495
  openfga_db_password: generated.secrets.openfga_db_password || randomSecret(),
2468
2496
  minikms_root_key: generated.secrets.minikms_root_key || randomBytes2(32).toString("hex"),
2469
2497
  minikms_session_signing_key: generated.secrets.minikms_session_signing_key || generateMinikmsSessionSigningKey(),
2470
- minikms_db_password: generated.secrets.minikms_db_password || randomSecret()
2498
+ minikms_db_password: generated.secrets.minikms_db_password || randomSecret(),
2499
+ saml_session_secret: generated.secrets.saml_session_secret || randomBytes2(32).toString("hex")
2471
2500
  },
2472
2501
  bootstrap: generated.bootstrap
2473
2502
  });
@@ -2700,7 +2729,7 @@ function extractStaticBundle(kind, image, targetDir) {
2700
2729
  logDryRun(`Would extract ${kind} bundle from ${image} into ${targetDir}`);
2701
2730
  return;
2702
2731
  }
2703
- fs4.rmSync(targetDir, { recursive: true, force: true });
2732
+ fs5.rmSync(targetDir, { recursive: true, force: true });
2704
2733
  ensureDir(targetDir);
2705
2734
  const containerId = run("docker", ["create", image], { quiet: true }).trim();
2706
2735
  try {
@@ -2728,23 +2757,23 @@ function writeFrontendRuntimeConfig(targetDir, runtimeConfig) {
2728
2757
  function syncFrontendReleaseContents(sourceDir, targetDir) {
2729
2758
  ensureDir(targetDir);
2730
2759
  const deferredEntries = /* @__PURE__ */ new Set(["index.html", "runtime-config.js"]);
2731
- const entries = fs4.readdirSync(sourceDir, { withFileTypes: true });
2760
+ const entries = fs5.readdirSync(sourceDir, { withFileTypes: true });
2732
2761
  for (const entry of entries) {
2733
2762
  if (deferredEntries.has(entry.name)) {
2734
2763
  continue;
2735
2764
  }
2736
- fs4.cpSync(path4.join(sourceDir, entry.name), path4.join(targetDir, entry.name), {
2765
+ fs5.cpSync(path4.join(sourceDir, entry.name), path4.join(targetDir, entry.name), {
2737
2766
  recursive: true,
2738
2767
  force: true
2739
2768
  });
2740
2769
  }
2741
2770
  const stagedRuntimeConfig = path4.join(sourceDir, "runtime-config.js");
2742
2771
  if (exists2(stagedRuntimeConfig)) {
2743
- fs4.cpSync(stagedRuntimeConfig, path4.join(targetDir, "runtime-config.js"), { force: true });
2772
+ fs5.cpSync(stagedRuntimeConfig, path4.join(targetDir, "runtime-config.js"), { force: true });
2744
2773
  }
2745
2774
  const stagedIndex = path4.join(sourceDir, "index.html");
2746
2775
  if (exists2(stagedIndex)) {
2747
- fs4.cpSync(stagedIndex, path4.join(targetDir, "index.html"), { force: true });
2776
+ fs5.cpSync(stagedIndex, path4.join(targetDir, "index.html"), { force: true });
2748
2777
  }
2749
2778
  }
2750
2779
  function activateFrontendRelease(kind, version, runtimeConfig) {
@@ -3137,7 +3166,7 @@ ${trimmed}`
3137
3166
  }
3138
3167
  function readRenderedRuntimeConfig(filePath) {
3139
3168
  if (!exists2(filePath)) return null;
3140
- const raw = fs4.readFileSync(filePath, "utf8").trim();
3169
+ const raw = fs5.readFileSync(filePath, "utf8").trim();
3141
3170
  const prefix = "window.__ENVSYNC_RUNTIME_CONFIG__ = ";
3142
3171
  if (!raw.startsWith(prefix) || !raw.endsWith(";")) return null;
3143
3172
  try {
@@ -3434,7 +3463,7 @@ function removeTarget(target) {
3434
3463
  logDryRun(`Would remove ${target}`);
3435
3464
  return;
3436
3465
  }
3437
- fs4.rmSync(target, { recursive: true, force: true });
3466
+ fs5.rmSync(target, { recursive: true, force: true });
3438
3467
  logInfo(`Removed ${target}`);
3439
3468
  }
3440
3469
  async function cmdRemove() {
@@ -3833,7 +3862,6 @@ async function cmdSetup() {
3833
3862
  domain: { root_domain: rootDomain, acme_email: acmeEmail },
3834
3863
  images: {
3835
3864
  api: releaseImages.api,
3836
- management_api: releaseImages.management_api,
3837
3865
  keycloak: releaseImages.keycloak,
3838
3866
  web: releaseImages.web,
3839
3867
  landing: releaseImages.landing,
@@ -3844,7 +3872,6 @@ async function cmdSetup() {
3844
3872
  services: {
3845
3873
  stack_name: "envsync",
3846
3874
  api_port: 4e3,
3847
- management_api_port: 4001,
3848
3875
  public_http_port: 80,
3849
3876
  public_https_port: 443,
3850
3877
  clickstack_ui_port: 8080,
@@ -4183,7 +4210,7 @@ async function cmdDeploy() {
4183
4210
  sleepSeconds(3);
4184
4211
  promoted = true;
4185
4212
  if (snapshotPath && !currentOptions.dryRun) {
4186
- fs4.rmSync(snapshotPath, { force: true });
4213
+ fs5.rmSync(snapshotPath, { force: true });
4187
4214
  }
4188
4215
  } catch (error) {
4189
4216
  const rollbackTarget = preUpgradeHead ?? "zero";
@@ -4217,7 +4244,7 @@ ${rollbackError instanceof Error ? rollbackError.message : String(rollbackError)
4217
4244
  deployRenderedStack(config, "rollback recovery");
4218
4245
  waitForApiSlotHealthy(config, recoveredState.deployment.active_slot);
4219
4246
  if (snapshotPath && !config.upgrade.keep_failed_upgrade_db_snapshot && !currentOptions.dryRun) {
4220
- fs4.rmSync(snapshotPath, { force: true });
4247
+ fs5.rmSync(snapshotPath, { force: true });
4221
4248
  }
4222
4249
  throw new Error(
4223
4250
  usedSnapshotRestore ? `Deploy failed after DB migration. Previous release was restored using DB snapshot recovery. ${error instanceof Error ? error.message : String(error)}` : `Deploy failed after DB migration. Previous release was restored. ${error instanceof Error ? error.message : String(error)}`
@@ -4243,7 +4270,6 @@ ${rollbackError instanceof Error ? rollbackError.message : String(rollbackError)
4243
4270
  { label: "openfga", getHealth: (services) => serviceHealth(services, `${config.services.stack_name}_openfga`) },
4244
4271
  { label: "minikms", getHealth: (services) => serviceHealth(services, `${config.services.stack_name}_minikms`) },
4245
4272
  { label: "clickstack", getHealth: (services) => serviceHealth(services, `${config.services.stack_name}_clickstack`) },
4246
- ...isOssConfig2(config) ? [] : [{ label: "landing", getHealth: (services) => serviceHealth(services, `${config.services.stack_name}_landing_nginx`) }],
4247
4273
  { label: "web", getHealth: (services) => serviceHealth(services, `${config.services.stack_name}_web_nginx`) },
4248
4274
  { label: "api", getHealth: (services) => apiHealth(services, config.services.stack_name) }
4249
4275
  ]);
@@ -4286,7 +4312,7 @@ async function cmdPromote(target) {
4286
4312
  }
4287
4313
  });
4288
4314
  writeDeployArtifacts(config, promotedState);
4289
- if (exists2(releaseAssetDir("web", promotedState.deployment.slots[targetSlot].release_version)) && exists2(releaseAssetDir("landing", promotedState.deployment.slots[targetSlot].release_version))) {
4315
+ if (exists2(releaseAssetDir("web", promotedState.deployment.slots[targetSlot].release_version))) {
4290
4316
  activateFrontendReleaseForState(config, promotedState);
4291
4317
  } else {
4292
4318
  logWarn(`Missing staged frontend assets for release ${promotedState.deployment.slots[targetSlot].release_version}; leaving current frontend assets unchanged.`);
@@ -4317,7 +4343,7 @@ async function cmdRollback() {
4317
4343
  }
4318
4344
  });
4319
4345
  writeDeployArtifacts(config, rollbackState);
4320
- if (exists2(releaseAssetDir("web", rollbackState.deployment.slots[rollbackState.deployment.active_slot].release_version)) && exists2(releaseAssetDir("landing", rollbackState.deployment.slots[rollbackState.deployment.active_slot].release_version))) {
4346
+ if (exists2(releaseAssetDir("web", rollbackState.deployment.slots[rollbackState.deployment.active_slot].release_version))) {
4321
4347
  activateFrontendReleaseForState(config, rollbackState);
4322
4348
  } else {
4323
4349
  logWarn(`Missing staged frontend assets for release ${rollbackState.deployment.slots[rollbackState.deployment.active_slot].release_version}; leaving current frontend assets unchanged.`);
@@ -4339,7 +4365,7 @@ async function cmdHealth(asJson) {
4339
4365
  const hosts = domainMap2(config.domain.root_domain);
4340
4366
  const services = listStackServices(config);
4341
4367
  const stackName = config.services.stack_name;
4342
- const traefikDynamic = exists2(TRAEFIK_DYNAMIC_FILE) ? fs4.readFileSync(TRAEFIK_DYNAMIC_FILE, "utf8") : "";
4368
+ const traefikDynamic = exists2(TRAEFIK_DYNAMIC_FILE) ? fs5.readFileSync(TRAEFIK_DYNAMIC_FILE, "utf8") : "";
4343
4369
  const webRuntimeConfig = readRenderedRuntimeConfig(path4.join(RELEASES_ROOT, "web", "current", "runtime-config.js"));
4344
4370
  const landingRuntimeConfig = readRenderedRuntimeConfig(path4.join(RELEASES_ROOT, "landing", "current", "runtime-config.js"));
4345
4371
  const clickstackSearchState = getClickstackSearchState(config);
@@ -4413,7 +4439,7 @@ async function cmdHealth(asJson) {
4413
4439
  }
4414
4440
  },
4415
4441
  web: serviceHealth(services, `${stackName}_web_nginx`),
4416
- landing: serviceHealth(services, `${stackName}_landing_nginx`)
4442
+ landing: "omitted"
4417
4443
  },
4418
4444
  database: {
4419
4445
  api: databaseHealth
@@ -4531,8 +4557,8 @@ async function cmdUpgradeDeps() {
4531
4557
  await cmdDeploy();
4532
4558
  }
4533
4559
  function sha256File(filePath) {
4534
- const hash = createHash2("sha256");
4535
- hash.update(fs4.readFileSync(filePath));
4560
+ const hash = createHash3("sha256");
4561
+ hash.update(fs5.readFileSync(filePath));
4536
4562
  return hash.digest("hex");
4537
4563
  }
4538
4564
  function parseEnterpriseLicenseBundle(raw) {
@@ -4561,7 +4587,7 @@ function parseEnterpriseLicenseBundle(raw) {
4561
4587
  return candidate;
4562
4588
  }
4563
4589
  function readEnterpriseLicenseBundle(bundlePath) {
4564
- return parseEnterpriseLicenseBundle(JSON.parse(fs4.readFileSync(bundlePath, "utf8")));
4590
+ return parseEnterpriseLicenseBundle(JSON.parse(fs5.readFileSync(bundlePath, "utf8")));
4565
4591
  }
4566
4592
  function normalizeLicenseServerUrl(serverUrl, route) {
4567
4593
  const trimmed = serverUrl.replace(/\/+$/, "");
@@ -4823,19 +4849,19 @@ async function cmdBackup() {
4823
4849
  logWarn("Backup will temporarily stop the EnvSync stack to capture consistent volume data.");
4824
4850
  stopManagedRuntime(config, "Stopping existing EnvSync services for consistent backup");
4825
4851
  }
4826
- writeFile(path4.join(staged, "deploy.env"), fs4.readFileSync(DEPLOY_ENV, "utf8"));
4827
- writeFile(path4.join(staged, "deploy.yaml"), fs4.readFileSync(DEPLOY_YAML, "utf8"));
4828
- writeFile(path4.join(staged, "config.json"), fs4.readFileSync(INTERNAL_CONFIG_JSON, "utf8"));
4829
- writeFile(path4.join(staged, "versions.lock.json"), fs4.readFileSync(VERSIONS_LOCK, "utf8"));
4830
- writeFile(path4.join(staged, "docker-stack.bootstrap.base.yaml"), fs4.readFileSync(BOOTSTRAP_BASE_STACK_FILE, "utf8"));
4831
- writeFile(path4.join(staged, "docker-stack.bootstrap.yaml"), fs4.readFileSync(BOOTSTRAP_STACK_FILE, "utf8"));
4832
- writeFile(path4.join(staged, "docker-stack.yaml"), fs4.readFileSync(STACK_FILE, "utf8"));
4833
- writeFile(path4.join(staged, "traefik-dynamic.yaml"), fs4.readFileSync(TRAEFIK_DYNAMIC_FILE, "utf8"));
4834
- writeFile(path4.join(staged, "keycloak-realm.envsync.json"), fs4.readFileSync(KEYCLOAK_REALM_FILE, "utf8"));
4835
- writeFile(path4.join(staged, "otel-agent.yaml"), fs4.readFileSync(OTEL_AGENT_CONF, "utf8"));
4836
- writeFile(path4.join(staged, "clickhouse-listen.xml"), fs4.readFileSync(CLICKSTACK_CLICKHOUSE_CONF, "utf8"));
4852
+ writeFile(path4.join(staged, "deploy.env"), fs5.readFileSync(DEPLOY_ENV, "utf8"));
4853
+ writeFile(path4.join(staged, "deploy.yaml"), fs5.readFileSync(DEPLOY_YAML, "utf8"));
4854
+ writeFile(path4.join(staged, "config.json"), fs5.readFileSync(INTERNAL_CONFIG_JSON, "utf8"));
4855
+ writeFile(path4.join(staged, "versions.lock.json"), fs5.readFileSync(VERSIONS_LOCK, "utf8"));
4856
+ writeFile(path4.join(staged, "docker-stack.bootstrap.base.yaml"), fs5.readFileSync(BOOTSTRAP_BASE_STACK_FILE, "utf8"));
4857
+ writeFile(path4.join(staged, "docker-stack.bootstrap.yaml"), fs5.readFileSync(BOOTSTRAP_STACK_FILE, "utf8"));
4858
+ writeFile(path4.join(staged, "docker-stack.yaml"), fs5.readFileSync(STACK_FILE, "utf8"));
4859
+ writeFile(path4.join(staged, "traefik-dynamic.yaml"), fs5.readFileSync(TRAEFIK_DYNAMIC_FILE, "utf8"));
4860
+ writeFile(path4.join(staged, "keycloak-realm.envsync.json"), fs5.readFileSync(KEYCLOAK_REALM_FILE, "utf8"));
4861
+ writeFile(path4.join(staged, "otel-agent.yaml"), fs5.readFileSync(OTEL_AGENT_CONF, "utf8"));
4862
+ writeFile(path4.join(staged, "clickhouse-listen.xml"), fs5.readFileSync(CLICKSTACK_CLICKHOUSE_CONF, "utf8"));
4837
4863
  if (!isOssConfig2(config) && exists2(LICENSE_ROOT)) {
4838
- fs4.cpSync(LICENSE_ROOT, path4.join(staged, "license"), { recursive: true });
4864
+ fs5.cpSync(LICENSE_ROOT, path4.join(staged, "license"), { recursive: true });
4839
4865
  }
4840
4866
  const volumesDir = path4.join(staged, "volumes");
4841
4867
  for (const volume of STACK_VOLUMES) {
@@ -4902,21 +4928,21 @@ async function cmdRestore(archivePath, autoDeploy = false) {
4902
4928
  }
4903
4929
  ensureDir(restoreRoot);
4904
4930
  run("bash", ["-lc", `tar -xzf ${JSON.stringify(archivePath)} -C ${JSON.stringify(restoreRoot)}`]);
4905
- writeFile(DEPLOY_ENV, fs4.readFileSync(path4.join(restoreRoot, "deploy.env"), "utf8"), 384);
4906
- writeFile(DEPLOY_YAML, fs4.readFileSync(path4.join(restoreRoot, "deploy.yaml"), "utf8"));
4907
- writeFile(INTERNAL_CONFIG_JSON, fs4.readFileSync(path4.join(restoreRoot, "config.json"), "utf8"));
4908
- writeFile(VERSIONS_LOCK, fs4.readFileSync(path4.join(restoreRoot, "versions.lock.json"), "utf8"));
4909
- writeFile(BOOTSTRAP_BASE_STACK_FILE, fs4.readFileSync(path4.join(restoreRoot, "docker-stack.bootstrap.base.yaml"), "utf8"));
4910
- writeFile(BOOTSTRAP_STACK_FILE, fs4.readFileSync(path4.join(restoreRoot, "docker-stack.bootstrap.yaml"), "utf8"));
4911
- writeFile(STACK_FILE, fs4.readFileSync(path4.join(restoreRoot, "docker-stack.yaml"), "utf8"));
4912
- writeFile(TRAEFIK_DYNAMIC_FILE, fs4.readFileSync(path4.join(restoreRoot, "traefik-dynamic.yaml"), "utf8"));
4913
- writeFile(KEYCLOAK_REALM_FILE, fs4.readFileSync(path4.join(restoreRoot, "keycloak-realm.envsync.json"), "utf8"));
4914
- writeFile(OTEL_AGENT_CONF, fs4.readFileSync(path4.join(restoreRoot, "otel-agent.yaml"), "utf8"));
4915
- writeFile(CLICKSTACK_CLICKHOUSE_CONF, fs4.readFileSync(path4.join(restoreRoot, "clickhouse-listen.xml"), "utf8"));
4931
+ writeFile(DEPLOY_ENV, fs5.readFileSync(path4.join(restoreRoot, "deploy.env"), "utf8"), 384);
4932
+ writeFile(DEPLOY_YAML, fs5.readFileSync(path4.join(restoreRoot, "deploy.yaml"), "utf8"));
4933
+ writeFile(INTERNAL_CONFIG_JSON, fs5.readFileSync(path4.join(restoreRoot, "config.json"), "utf8"));
4934
+ writeFile(VERSIONS_LOCK, fs5.readFileSync(path4.join(restoreRoot, "versions.lock.json"), "utf8"));
4935
+ writeFile(BOOTSTRAP_BASE_STACK_FILE, fs5.readFileSync(path4.join(restoreRoot, "docker-stack.bootstrap.base.yaml"), "utf8"));
4936
+ writeFile(BOOTSTRAP_STACK_FILE, fs5.readFileSync(path4.join(restoreRoot, "docker-stack.bootstrap.yaml"), "utf8"));
4937
+ writeFile(STACK_FILE, fs5.readFileSync(path4.join(restoreRoot, "docker-stack.yaml"), "utf8"));
4938
+ writeFile(TRAEFIK_DYNAMIC_FILE, fs5.readFileSync(path4.join(restoreRoot, "traefik-dynamic.yaml"), "utf8"));
4939
+ writeFile(KEYCLOAK_REALM_FILE, fs5.readFileSync(path4.join(restoreRoot, "keycloak-realm.envsync.json"), "utf8"));
4940
+ writeFile(OTEL_AGENT_CONF, fs5.readFileSync(path4.join(restoreRoot, "otel-agent.yaml"), "utf8"));
4941
+ writeFile(CLICKSTACK_CLICKHOUSE_CONF, fs5.readFileSync(path4.join(restoreRoot, "clickhouse-listen.xml"), "utf8"));
4916
4942
  const restoredLicenseRoot = path4.join(restoreRoot, "license");
4917
4943
  if (exists2(restoredLicenseRoot)) {
4918
- fs4.rmSync(LICENSE_ROOT, { recursive: true, force: true });
4919
- fs4.cpSync(restoredLicenseRoot, LICENSE_ROOT, { recursive: true });
4944
+ fs5.rmSync(LICENSE_ROOT, { recursive: true, force: true });
4945
+ fs5.cpSync(restoredLicenseRoot, LICENSE_ROOT, { recursive: true });
4920
4946
  ensureEnterpriseLicenseFilesReadable();
4921
4947
  }
4922
4948
  const config = loadConfig();
@@ -5338,8 +5364,6 @@ var init_cli = __esm({
5338
5364
  MANAGED_VERSIONED_IMAGE_PREFIXES = {
5339
5365
  api: "ghcr.io/envsync-cloud/envsync-api:",
5340
5366
  api_enterprise: "ghcr.io/envsync-cloud/envsync-api-enterprise:",
5341
- // Retired second process — kept so old deploy.yaml image strings still parse as "managed".
5342
- management_api: "ghcr.io/envsync-cloud/envsync-management-api:",
5343
5367
  keycloak: "envsync-keycloak:",
5344
5368
  web: "ghcr.io/envsync-cloud/envsync-web-static:",
5345
5369
  web_oss: "ghcr.io/envsync-cloud/envsync-web-oss-static:",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@envsync-cloud/deploy",
3
- "version": "0.20.12",
3
+ "version": "0.21.2",
4
4
  "description": "OSS CLI for self-hosted EnvSync deployment on Docker Swarm",
5
5
  "type": "module",
6
6
  "bin": {
@@ -46,7 +46,7 @@
46
46
  "zod": "^3.25.76"
47
47
  },
48
48
  "devDependencies": {
49
- "@envsync-cloud/deploy-core": "0.20.12",
49
+ "@envsync-cloud/deploy-core": "0.21.2",
50
50
  "tsup": "^8.5.0",
51
51
  "typescript": "^5.9.2"
52
52
  },