openmates 0.26.0-alpha.0 → 0.26.0-alpha.1

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.
@@ -32489,6 +32489,41 @@ function formatTs(ts) {
32489
32489
  return d.toISOString().slice(0, 16).replace("T", " ");
32490
32490
  }
32491
32491
 
32492
+ // src/serverNotifications.ts
32493
+ function evaluateNotificationDestinationConfiguration(config) {
32494
+ const configuredCount = [config.email, config.discordWebhookUrl, config.genericWebhook].filter(Boolean).length;
32495
+ return {
32496
+ id: "notifications.destination_configured",
32497
+ status: configuredCount >= 2 ? "passed" : "failed",
32498
+ required: true,
32499
+ duration_ms: 0,
32500
+ ...configuredCount < 2 ? {
32501
+ failureClass: "configuration",
32502
+ sanitized_reason: "notification_destination_or_fallback_missing"
32503
+ } : {}
32504
+ };
32505
+ }
32506
+ function resolveHostNotificationDestinations(value, deploymentMode, serverEnvironment) {
32507
+ const configured = (key) => {
32508
+ const candidate = value(key)?.trim();
32509
+ return candidate && candidate !== "IMPORTED_TO_VAULT" ? candidate : void 0;
32510
+ };
32511
+ const to = configured("OPENMATES_RUNTIME_HEALTH_EMAIL_TO") || configured("ADMIN_NOTIFY_EMAIL");
32512
+ const from = configured("OPENMATES_RUNTIME_HEALTH_EMAIL_FROM") || configured("EMAIL_SENDER_EMAIL") || "noreply@openmates.org";
32513
+ const apiKey = configured("OPENMATES_RUNTIME_HEALTH_BREVO_API_KEY") || configured("BREVO_API_KEY");
32514
+ const email = to && apiKey ? { to, from, apiKey } : void 0;
32515
+ const scopedDiscord = deploymentMode === "self_host" ? configured("DISCORD_WEBHOOK_OPERATIONAL_MONITORING_SELF_HOST") || configured("OPENMATES_RUNTIME_HEALTH_DISCORD_WEBHOOK_URL_SELF_HOST") || configured("OPENMATES_RUNTIME_HEALTH_DISCORD_WEBHOOK_URL") : serverEnvironment === "production" ? configured("OPENMATES_RUNTIME_HEALTH_DISCORD_WEBHOOK_URL_PRODUCTION") || configured("DISCORD_WEBHOOK_OPERATIONAL_MONITORING_PRODUCTION") : configured("OPENMATES_RUNTIME_HEALTH_DISCORD_WEBHOOK_URL_DEVELOPMENT") || configured("DISCORD_WEBHOOK_OPERATIONAL_MONITORING_DEVELOPMENT");
32516
+ const legacyDiscord = deploymentMode === "official_cloud" && configured("OPENMATES_RUNTIME_HEALTH_LEGACY_DISCORD_ENVIRONMENT") === serverEnvironment ? configured("OPENMATES_RUNTIME_HEALTH_DISCORD_WEBHOOK_URL") : void 0;
32517
+ const fallbackDiscord = deploymentMode === "self_host" ? void 0 : serverEnvironment === "production" ? configured("DISCORD_WEBHOOK_PROD_SMOKE") : configured("DISCORD_WEBHOOK_DEV_NIGHTLY") || configured("DISCORD_WEBHOOK_DEV_SMOKE");
32518
+ const discordWebhookUrl = scopedDiscord || legacyDiscord || fallbackDiscord;
32519
+ return {
32520
+ email,
32521
+ discordWebhookUrl,
32522
+ discordDestinationSource: scopedDiscord ? "canonical" : legacyDiscord ? "legacy_environment_mapping" : fallbackDiscord ? serverEnvironment === "production" ? "prod_smoke_fallback" : "dev_fallback" : "missing",
32523
+ discordFallbackUsed: !scopedDiscord && !legacyDiscord && Boolean(fallbackDiscord)
32524
+ };
32525
+ }
32526
+
32492
32527
  // src/server.ts
32493
32528
  import { execFileSync as execFileSync5, execSync, spawn as nodeSpawn, spawnSync } from "child_process";
32494
32529
  import { createHash as createHash18, randomBytes as randomBytes9, randomUUID as randomUUID15 } from "crypto";
@@ -33270,6 +33305,109 @@ import { accessSync, constants as constants2, existsSync as existsSync11, mkdirS
33270
33305
  import { tmpdir } from "os";
33271
33306
  import { dirname as dirname6, join as join12 } from "path";
33272
33307
  var hash = (text4) => createHash17("sha256").update(text4).digest("hex");
33308
+ function resolveCaddyProfile(role, explicit, previous) {
33309
+ if (explicit === void 0) return previous ?? "self-host";
33310
+ if (role !== "upload" || explicit !== "official-upload") throw new Error("caddy_profile_unsupported");
33311
+ if (previous && previous !== explicit) throw new Error("caddy_profile_conflicts_with_baseline");
33312
+ return explicit;
33313
+ }
33314
+ function scopedHandleBlock(content, name) {
33315
+ const lines = content.split("\n");
33316
+ const start = lines.findIndex((line) => new RegExp(`^\\s*handle\\s+${name}\\s*\\{$`).test(line));
33317
+ if (start < 0) return void 0;
33318
+ let depth = 0;
33319
+ for (let i = start; i < lines.length; i++) {
33320
+ const code = lines[i].replace(/#.*$/, "");
33321
+ depth += (code.match(/\{/g)?.length ?? 0) - (code.match(/\}/g)?.length ?? 0);
33322
+ if (depth === 0) return lines.slice(start, i + 1).join("\n");
33323
+ }
33324
+ return void 0;
33325
+ }
33326
+ function reverseProxyBlocks(content) {
33327
+ const lines = content.split("\n");
33328
+ const blocks = [];
33329
+ for (let i = 0; i < lines.length; i++) {
33330
+ const first = lines[i].replace(/#.*$/, "");
33331
+ if (!/^\s*reverse_proxy\b/.test(first)) continue;
33332
+ if (!/\{\s*$/.test(first)) throw new Error("caddy_official_upload_structure_missing");
33333
+ const start = i;
33334
+ let depth = 0;
33335
+ let closed = false;
33336
+ for (; i < lines.length; i++) {
33337
+ const code = lines[i].replace(/#.*$/, "");
33338
+ depth += (code.match(/\{/g)?.length ?? 0) - (code.match(/\}/g)?.length ?? 0);
33339
+ if (depth === 0) {
33340
+ blocks.push(lines.slice(start, i + 1).join("\n"));
33341
+ closed = true;
33342
+ break;
33343
+ }
33344
+ }
33345
+ if (!closed) throw new Error("caddy_official_upload_structure_missing");
33346
+ }
33347
+ return blocks;
33348
+ }
33349
+ function headerUpAffectsTargetEnv(line) {
33350
+ const field = /^header_up\s+(\S+)/i.exec(line)?.[1]?.replace(/^['"]|['"]$/g, "").replace(/^[+-]/, "").toLowerCase();
33351
+ if (!field) return false;
33352
+ const escaped = field.split("*").map((part) => part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join(".*");
33353
+ return new RegExp(`^${escaped}$`).test("x-target-env");
33354
+ }
33355
+ function officialUploadOrigins(content) {
33356
+ const origins = {};
33357
+ for (const environment of ["prod", "dev"]) {
33358
+ const matches2 = [...content.matchAll(new RegExp(`^\\s*@${environment}_origin\\s+expression\\s+\\{header\\.Origin\\}\\s*==\\s*"([^"\\n]+)"\\s*$`, "gm"))];
33359
+ if (matches2.length !== 1) {
33360
+ throw new Error("caddy_official_upload_structure_missing");
33361
+ }
33362
+ const origin = matches2[0][1];
33363
+ let parsed;
33364
+ try {
33365
+ parsed = new URL(origin);
33366
+ } catch {
33367
+ throw new Error("caddy_official_upload_origin_invalid");
33368
+ }
33369
+ if (parsed.protocol !== "https:" || parsed.origin !== origin || parsed.username || parsed.password || parsed.pathname !== "/" || parsed.search || parsed.hash) {
33370
+ throw new Error("caddy_official_upload_origin_invalid");
33371
+ }
33372
+ origins[environment] = origin;
33373
+ }
33374
+ if (origins.prod === origins.dev) throw new Error("caddy_official_upload_origin_conflict");
33375
+ return origins;
33376
+ }
33377
+ function validateOfficialUploadCaddy(content, expectedOrigins) {
33378
+ const origins = officialUploadOrigins(content);
33379
+ if (expectedOrigins && (origins.prod !== expectedOrigins.prod || origins.dev !== expectedOrigins.dev)) {
33380
+ throw new Error("caddy_official_upload_origin_mismatch");
33381
+ }
33382
+ const scoped = matchers(content, null, true);
33383
+ if (!scoped.has("@prod_origin/@upload_api") || !scoped.has("@dev_origin/@upload_api") || !scoped.has("@upload_health") || !scoped.has("@admin_paths")) {
33384
+ throw new Error("caddy_official_upload_structure_missing");
33385
+ }
33386
+ for (const environment of ["prod", "dev"]) {
33387
+ const block = scopedHandleBlock(content, `@${environment}_origin`);
33388
+ const active = block?.split("\n").filter((line) => !line.trim().startsWith("#")).join("\n") ?? "";
33389
+ if (!block || !active.includes("@upload_options method OPTIONS") || !/handle\s*\{\s*abort\s*\}/.test(active)) {
33390
+ throw new Error("caddy_official_upload_structure_missing");
33391
+ }
33392
+ const proxies = reverseProxyBlocks(active);
33393
+ if (proxies.length < 2) throw new Error("caddy_official_upload_structure_missing");
33394
+ for (const proxy of proxies) {
33395
+ const targetHeaders = proxy.split("\n").map((line) => line.replace(/#.*$/, "").trim()).filter(headerUpAffectsTargetEnv);
33396
+ if (targetHeaders.length !== 1 || !new RegExp(`^header_up\\s+X-Target-Env\\s+(?:"${environment}"|${environment})$`, "i").test(targetHeaders[0])) {
33397
+ throw new Error("caddy_official_upload_structure_missing");
33398
+ }
33399
+ }
33400
+ }
33401
+ }
33402
+ function renderOfficialUploadCaddyTemplate(template, domain, email) {
33403
+ if (!/^[a-z0-9.-]+$/i.test(domain) || !domain.includes(".") || !/^[^\s{}$@]+@[^\s{}$@]+\.[^\s{}$@]+$/.test(email)) {
33404
+ throw new Error("caddy_official_upload_render_values_invalid");
33405
+ }
33406
+ const rendered = template.replaceAll("${DEPLOY_UPLOAD_DOMAIN}", domain).replaceAll("${DEPLOY_ACME_EMAIL}", email);
33407
+ if (/\$\{[A-Z_][A-Z0-9_]*\}/.test(rendered)) throw new Error("caddy_official_upload_template_unresolved");
33408
+ validateOfficialUploadCaddy(rendered);
33409
+ return rendered;
33410
+ }
33273
33411
  function siteRange(lines, site) {
33274
33412
  if (!site) return [0, lines.length];
33275
33413
  const start = lines.findIndex((line) => line.trim() === `${site} {`);
@@ -33282,36 +33420,48 @@ function siteRange(lines, site) {
33282
33420
  }
33283
33421
  throw new Error("caddy_managed_site_unclosed");
33284
33422
  }
33285
- function matchers(content, site) {
33423
+ function matchers(content, site, scopeAware = false) {
33286
33424
  const lines = content.split("\n");
33287
33425
  const [start, end] = siteRange(lines, site);
33288
33426
  const result = /* @__PURE__ */ new Map();
33427
+ let depth = 0;
33428
+ const handles = [];
33289
33429
  for (let i = start; i < end; i++) {
33290
33430
  const named = /^\s*(@[\w-]+)\s+(.*)$/.exec(lines[i]);
33291
- if (!named) continue;
33292
- let pathLine = i;
33293
- if (named[2].trim() === "{") {
33294
- pathLine = -1;
33295
- for (let j = i + 1; j < end && lines[j].trim() !== "}"; j++) {
33296
- if (/^\s*path\s/.test(lines[j])) {
33297
- if (pathLine !== -1) throw new Error("caddy_ambiguous_path_matcher");
33298
- pathLine = j;
33431
+ if (named) {
33432
+ let pathLine = i;
33433
+ if (named[2].trim() === "{") {
33434
+ pathLine = -1;
33435
+ for (let j = i + 1; j < end && lines[j].trim() !== "}"; j++) {
33436
+ if (/^\s*path\s/.test(lines[j])) {
33437
+ if (pathLine !== -1) throw new Error("caddy_ambiguous_path_matcher");
33438
+ pathLine = j;
33439
+ }
33299
33440
  }
33300
33441
  }
33301
- if (pathLine === -1) continue;
33442
+ const path2 = pathLine >= 0 ? /^(.*?\bpath\s+)([^#]*?)(\s*(?:#.*)?)$/.exec(lines[pathLine]) : null;
33443
+ if (path2) {
33444
+ const paths = path2[2].trim().split(/\s+/);
33445
+ if (paths.some((value) => !value.startsWith("/") || /[{}"'\\]/.test(value))) throw new Error("caddy_unsupported_path_matcher");
33446
+ const scope2 = scopeAware ? handles.at(-1)?.name : void 0;
33447
+ const key = scope2 ? `${scope2}/${named[1]}` : named[1];
33448
+ if (result.has(key)) throw new Error("caddy_ambiguous_path_matcher");
33449
+ result.set(key, { line: pathLine, paths, prefix: path2[1], suffix: path2[3] });
33450
+ }
33451
+ }
33452
+ if (scopeAware) {
33453
+ const handle = /^\s*handle\s+(@[\w-]+)\s*\{/.exec(lines[i]);
33454
+ if (handle) handles.push({ name: handle[1], depth: depth + 1 });
33455
+ const code = lines[i].replace(/#.*$/, "");
33456
+ depth += (code.match(/\{/g)?.length ?? 0) - (code.match(/\}/g)?.length ?? 0);
33457
+ while (handles.length && depth < handles[handles.length - 1].depth) handles.pop();
33302
33458
  }
33303
- const path2 = /^(.*?\bpath\s+)([^#]*?)(\s*(?:#.*)?)$/.exec(lines[pathLine]);
33304
- if (!path2) continue;
33305
- const paths = path2[2].trim().split(/\s+/);
33306
- if (paths.some((value) => !value.startsWith("/") || /[{}"'\\]/.test(value))) throw new Error("caddy_unsupported_path_matcher");
33307
- if (result.has(named[1])) throw new Error("caddy_ambiguous_path_matcher");
33308
- result.set(named[1], { line: pathLine, paths, prefix: path2[1], suffix: path2[3] });
33309
33459
  }
33310
33460
  return result;
33311
33461
  }
33312
33462
  function mergeCaddyPaths(input) {
33313
- const current = matchers(input.current, input.site);
33314
- const target = matchers(input.target, input.site);
33463
+ const current = matchers(input.current, input.site, input.scopeAware);
33464
+ const target = matchers(input.target, input.site, input.scopeAware);
33315
33465
  if (!target.size) throw new Error("caddy_release_matchers_missing");
33316
33466
  const lines = input.current.split("\n");
33317
33467
  const paths = {};
@@ -33337,7 +33487,7 @@ function readCaddyUpdateState(path2, configPath, site) {
33337
33487
  if (!existsSync11(path2)) return void 0;
33338
33488
  try {
33339
33489
  const state = JSON.parse(readFileSync12(path2, "utf8"));
33340
- if (state.version !== 1 || state.configPath !== configPath || state.site !== site || !/^[a-f0-9]{40}$/i.test(state.revision) || typeof state.template !== "string" || !state.template || !state.paths || Array.isArray(state.paths) || Object.entries(state.paths).some(([name, paths]) => !/^@[\w-]+$/.test(name) || !Array.isArray(paths) || paths.some((path3) => typeof path3 !== "string" || !path3.startsWith("/")))) {
33490
+ if (state.version !== 1 || state.configPath !== configPath || state.site !== site || !/^[a-f0-9]{40}$/i.test(state.revision) || typeof state.template !== "string" || !state.template || !state.paths || Array.isArray(state.paths) || state.profile !== void 0 && state.profile !== "self-host" && state.profile !== "official-upload" || Object.entries(state.paths).some(([name, paths]) => !/^(?:@[\w-]+\/)?@[\w-]+$/.test(name) || !Array.isArray(paths) || paths.some((path3) => typeof path3 !== "string" || !path3.startsWith("/")))) {
33341
33491
  throw new Error();
33342
33492
  }
33343
33493
  return state;
@@ -33446,15 +33596,17 @@ async function applyCaddyPathUpdate(input) {
33446
33596
  const current = caddyHostOperation({ action: "read", configPath: input.configPath }).content;
33447
33597
  const statePath = join12(input.installPath, ".openmates", "caddy", `${input.role}.json`);
33448
33598
  const previous = readCaddyUpdateState(statePath, input.configPath, input.site);
33449
- const merged = previous ? { content: mergeCaddyRelease(current, previous.template, input.target), paths: Object.fromEntries([...matchers(input.target, input.site)].map(([name, matcher]) => [name, matcher.paths])) } : mergeCaddyPaths({ current, target: input.target, site: input.site });
33599
+ const merged = previous ? { content: mergeCaddyRelease(current, previous.template, input.target), paths: Object.fromEntries([...matchers(input.target, input.site, input.profile === "official-upload")].map(([name, matcher]) => [name, matcher.paths])) } : mergeCaddyPaths({ current, target: input.target, site: input.site, scopeAware: input.profile === "official-upload" });
33600
+ if (input.profile === "official-upload") validateOfficialUploadCaddy(merged.content, officialUploadOrigins(input.target));
33450
33601
  const result = caddyHostOperation({ action: "apply", configPath: input.configPath, expectedHash: hash(current), content: merged.content });
33451
33602
  try {
33452
33603
  await input.verify();
33453
- } catch {
33604
+ } catch (error) {
33454
33605
  if (result.backupPath) caddyHostOperation({ action: "restore", configPath: input.configPath, expectedHash: hash(merged.content), backupPath: result.backupPath });
33606
+ if (error instanceof Error && /^caddy_upload_(?:health_route_failed|prod_preflight_failed|dev_preflight_failed|unknown_origin_allowed)$/.test(error.message)) throw error;
33455
33607
  throw new Error("caddy_public_route_verification_failed");
33456
33608
  }
33457
- const state = { version: 1, configPath: input.configPath, site: input.site, revision: input.revision, paths: merged.paths, template: input.target };
33609
+ const state = { version: 1, configPath: input.configPath, site: input.site, revision: input.revision, paths: merged.paths, template: input.target, profile: input.profile ?? "self-host" };
33458
33610
  mkdirSync10(dirname6(statePath), { recursive: true });
33459
33611
  const temp = `${statePath}.${process.pid}.tmp`;
33460
33612
  writeFileSync6(temp, JSON.stringify(state) + "\n", { mode: 384 });
@@ -33483,6 +33635,52 @@ async function verifyCaddyCoreRoutes(apiUrl, origin) {
33483
33635
  if (response.status !== 401 || response.headers.get("access-control-allow-origin") !== origin) throw new Error("caddy_workflow_route_failed");
33484
33636
  }
33485
33637
  }
33638
+ async function verifyCaddyUploadRoutes(baseUrl, origins, fetcher = fetch) {
33639
+ const endpoint = baseUrl.replace(/\/$/, "");
33640
+ const request = (path2, init = {}) => fetcher(`${endpoint}${path2}`, {
33641
+ ...init,
33642
+ redirect: "error",
33643
+ signal: AbortSignal.timeout(5e3)
33644
+ });
33645
+ let health;
33646
+ try {
33647
+ health = await request("/health");
33648
+ } catch {
33649
+ throw new Error("caddy_upload_health_route_failed");
33650
+ }
33651
+ if (!health.ok) throw new Error("caddy_upload_health_route_failed");
33652
+ for (const environment of ["prod", "dev"]) {
33653
+ const origin = origins[environment];
33654
+ let preflight;
33655
+ try {
33656
+ preflight = await request("/v1/upload/file", { method: "OPTIONS", headers: {
33657
+ Origin: origin,
33658
+ "Access-Control-Request-Method": "POST",
33659
+ "Access-Control-Request-Headers": "content-type"
33660
+ } });
33661
+ } catch {
33662
+ throw new Error(`caddy_upload_${environment}_preflight_failed`);
33663
+ }
33664
+ const methods = preflight.headers.get("access-control-allow-methods")?.split(",").map((value) => value.trim().toUpperCase()) ?? [];
33665
+ if (!preflight.ok || preflight.headers.get("access-control-allow-origin") !== origin || preflight.headers.get("access-control-allow-credentials") !== "true" || !methods.includes("POST")) {
33666
+ throw new Error(`caddy_upload_${environment}_preflight_failed`);
33667
+ }
33668
+ }
33669
+ const unknownOrigin = "https://openmates-route-probe.invalid";
33670
+ if (Object.values(origins).includes(unknownOrigin)) throw new Error("caddy_upload_unknown_origin_allowed");
33671
+ try {
33672
+ const denied = await request("/v1/upload/file", { method: "OPTIONS", headers: {
33673
+ Origin: unknownOrigin,
33674
+ "Access-Control-Request-Method": "POST",
33675
+ "Access-Control-Request-Headers": "content-type"
33676
+ } });
33677
+ if (![403, 404].includes(denied.status) || denied.headers.get("access-control-allow-origin") === unknownOrigin) {
33678
+ throw new Error("caddy_upload_unknown_origin_allowed");
33679
+ }
33680
+ } catch (error) {
33681
+ if (error instanceof Error && error.message === "caddy_upload_unknown_origin_allowed") throw error;
33682
+ }
33683
+ }
33486
33684
 
33487
33685
  // src/serverHealth.ts
33488
33686
  import dnsModule, { promises as dns } from "dns";
@@ -36435,19 +36633,9 @@ function runRuntimeVerification(installPath, role, config) {
36435
36633
  if (cmsCacheCheck.status !== "passed") output.status = "failed";
36436
36634
  }
36437
36635
  if (mode.effectiveMode === "official_cloud") {
36438
- const destinations = runtimeNotificationConfig(installPath);
36439
- const configuredCount = [destinations.email, destinations.discordWebhookUrl, destinations.genericWebhook].filter(Boolean).length;
36440
- if (configuredCount < 2) {
36441
- output.status = "failed";
36442
- output.checks.push({
36443
- id: "notifications.destination_configured",
36444
- status: "failed",
36445
- required: true,
36446
- duration_ms: 0,
36447
- failureClass: "configuration",
36448
- sanitized_reason: "notification_destination_or_fallback_missing"
36449
- });
36450
- }
36636
+ const notificationCheck = evaluateNotificationDestinationConfiguration(runtimeNotificationConfig(installPath));
36637
+ output.checks.push(notificationCheck);
36638
+ if (notificationCheck.status === "failed") output.status = "failed";
36451
36639
  }
36452
36640
  for (const check of output.checks) {
36453
36641
  const marker = check.status === "passed" ? "x" : "!";
@@ -36462,26 +36650,17 @@ function runtimeNotificationConfig(installPath) {
36462
36650
  const deploymentMode = getInstallDeploymentMode(installPath, serverConfig);
36463
36651
  const serverEnvironment = value("SERVER_ENVIRONMENT") === "production" ? "production" : "development";
36464
36652
  const environment = deploymentMode === "self_host" ? "self_host" : serverEnvironment;
36465
- const emailTo = value("OPENMATES_RUNTIME_HEALTH_EMAIL_TO") || value("ADMIN_NOTIFY_EMAIL");
36466
- const emailFrom = value("OPENMATES_RUNTIME_HEALTH_EMAIL_FROM") || value("EMAIL_SENDER_EMAIL") || "noreply@openmates.org";
36467
- const emailApiKey = value("OPENMATES_RUNTIME_HEALTH_BREVO_API_KEY") || value("BREVO_API_KEY");
36468
- const email = emailTo && emailFrom && emailApiKey ? { to: emailTo, from: emailFrom, apiKey: emailApiKey } : void 0;
36653
+ const destinations = resolveHostNotificationDestinations(value, deploymentMode, serverEnvironment);
36469
36654
  const webhookUrl = value("OPENMATES_RUNTIME_HEALTH_WEBHOOK_URL");
36470
36655
  const webhookSecret = value("OPENMATES_RUNTIME_HEALTH_WEBHOOK_SECRET");
36471
36656
  const allowLocalDevelopmentFixture = value("SERVER_ENVIRONMENT") === "development" && value("OPENMATES_RUNTIME_HEALTH_ALLOW_LOCAL_WEBHOOK_FIXTURE") === "true";
36472
36657
  const genericWebhook = webhookUrl && webhookSecret ? { url: webhookUrl, secret: webhookSecret, allowLocalDevelopmentFixture } : void 0;
36473
- const canonicalDiscordWebhookUrl = deploymentMode === "self_host" ? value("DISCORD_WEBHOOK_OPERATIONAL_MONITORING_SELF_HOST") || value("OPENMATES_RUNTIME_HEALTH_DISCORD_WEBHOOK_URL_SELF_HOST") || value("OPENMATES_RUNTIME_HEALTH_DISCORD_WEBHOOK_URL") : serverEnvironment === "production" ? value("OPENMATES_RUNTIME_HEALTH_DISCORD_WEBHOOK_URL_PRODUCTION") || value("DISCORD_WEBHOOK_OPERATIONAL_MONITORING_PRODUCTION") : value("OPENMATES_RUNTIME_HEALTH_DISCORD_WEBHOOK_URL_DEVELOPMENT") || value("DISCORD_WEBHOOK_OPERATIONAL_MONITORING_DEVELOPMENT");
36474
- const fallbackDiscordWebhookUrl = deploymentMode === "self_host" ? void 0 : serverEnvironment === "production" ? value("DISCORD_WEBHOOK_PROD_SMOKE") : value("DISCORD_WEBHOOK_DEV_NIGHTLY") || value("DISCORD_WEBHOOK_DEV_SMOKE");
36475
- const discordWebhookUrl = canonicalDiscordWebhookUrl || fallbackDiscordWebhookUrl;
36476
36658
  return {
36477
36659
  environment,
36478
36660
  deploymentMode,
36479
36661
  serverName: value("OPENMATES_RUNTIME_SERVER_NAME"),
36480
36662
  version: value("OPENMATES_IMAGE_TAG") || serverConfig?.imageTag || "source",
36481
- email,
36482
- discordWebhookUrl,
36483
- discordDestinationSource: canonicalDiscordWebhookUrl ? "canonical" : fallbackDiscordWebhookUrl ? serverEnvironment === "production" ? "prod_smoke_fallback" : "dev_fallback" : "missing",
36484
- discordFallbackUsed: !canonicalDiscordWebhookUrl && Boolean(fallbackDiscordWebhookUrl),
36663
+ ...destinations,
36485
36664
  genericWebhook
36486
36665
  };
36487
36666
  }
@@ -36681,7 +36860,15 @@ function caddyUpdatePlan(installPath, role, config, flags) {
36681
36860
  const configPath = resolve7(typeof flags["caddy-config"] === "string" ? flags["caddy-config"] : "/etc/caddy/Caddyfile");
36682
36861
  const required = role === "core" && getInstallDeploymentMode(installPath, config) === "official_cloud";
36683
36862
  if (!existsSync13(configPath) && required) throw new Error("Managed cloud Caddyfile is missing; update cannot proceed.");
36684
- return { configPath, status: existsSync13(configPath) ? "planned" : "not_installed" };
36863
+ const prior = role === "upload" ? readCaddyUpdateState(join15(installPath, ".openmates", "caddy", `${role}.json`), configPath, null) : void 0;
36864
+ const profile = resolveCaddyProfile(role, flags["caddy-profile"], prior ? prior.profile ?? "self-host" : void 0);
36865
+ if (profile === "official-upload" && !existsSync13(configPath)) throw new Error("caddy_official_upload_host_missing");
36866
+ if (role === "upload" && existsSync13(configPath)) {
36867
+ const live = caddyHostOperation({ action: "read", configPath }).content;
36868
+ if (profile === "official-upload") validateOfficialUploadCaddy(live);
36869
+ else if (live.includes("@prod_origin") || live.includes("@dev_origin")) throw new Error("caddy_profile_required_official_upload");
36870
+ }
36871
+ return { configPath, status: existsSync13(configPath) ? "planned" : "not_installed", profile };
36685
36872
  }
36686
36873
  async function updateServerCaddy(input) {
36687
36874
  const plan = caddyUpdatePlan(input.installPath, input.role, input.config, input.flags);
@@ -36697,6 +36884,8 @@ async function updateServerCaddy(input) {
36697
36884
  templatePath = site === "api.openmates.org" ? "deployment/prod_server/Caddyfile" : "deployment/dev_server/Caddyfile";
36698
36885
  } else if (cloud) {
36699
36886
  templatePath = `deployment/${input.role}_server/Caddyfile`;
36887
+ } else if (plan.profile === "official-upload") {
36888
+ templatePath = "deployment/upload_server/Caddyfile";
36700
36889
  }
36701
36890
  const revision = input.mode === "image" ? installedImageLabels(input)["org.opencontainers.image.revision"]?.trim() : exec("git rev-parse HEAD", input.installPath).trim();
36702
36891
  if (!revision || !/^[a-f0-9]{40}$/i.test(revision)) throw new Error("caddy_release_revision_invalid");
@@ -36711,8 +36900,15 @@ async function updateServerCaddy(input) {
36711
36900
  const env = readEnvMap(input.installPath);
36712
36901
  const urls = deriveSelfHostCliUrls(readEnvContent(input.installPath));
36713
36902
  const domain = env[`DEPLOY_${input.role.toUpperCase()}_DOMAIN`];
36714
- const hostname = site ?? domain ?? live.split("\n").map((line) => /^([^\s{$]+)\s+\{$/.exec(line)?.[1]).find(Boolean);
36903
+ const liveHostname = live.split("\n").map((line) => /^([^\s{$]+)\s+\{$/.exec(line)?.[1]).find(Boolean);
36904
+ const hostname = site ?? (plan.profile === "official-upload" ? liveHostname : domain ?? liveHostname);
36715
36905
  if (!hostname) throw new Error("caddy_public_endpoint_unavailable");
36906
+ if (plan.profile === "official-upload") {
36907
+ const email = /^\s*email\s+(\S+)\s*$/m.exec(live)?.[1];
36908
+ if (!email) throw new Error("caddy_official_upload_email_missing");
36909
+ target = renderOfficialUploadCaddyTemplate(target, hostname, email);
36910
+ validateOfficialUploadCaddy(live, officialUploadOrigins(target));
36911
+ }
36716
36912
  const baseUrl = hostname.includes("://") ? hostname : `https://${hostname}`;
36717
36913
  const endpoint = new URL(baseUrl);
36718
36914
  if (!["http:", "https:"].includes(endpoint.protocol) || endpoint.username || endpoint.password) throw new Error("caddy_public_endpoint_invalid");
@@ -36723,8 +36919,10 @@ async function updateServerCaddy(input) {
36723
36919
  site,
36724
36920
  target,
36725
36921
  revision,
36922
+ profile: plan.profile,
36726
36923
  verify: async () => {
36727
36924
  if (input.role === "core") await verifyCaddyCoreRoutes(baseUrl, new URL(urls.appUrl).origin);
36925
+ else if (plan.profile === "official-upload") await verifyCaddyUploadRoutes(baseUrl, officialUploadOrigins(target));
36728
36926
  else {
36729
36927
  const response = await fetch(`${baseUrl}/health`, { redirect: "error", signal: AbortSignal.timeout(5e3) });
36730
36928
  if (!response.ok) throw new Error("caddy_health_route_failed");
@@ -38196,6 +38394,7 @@ Command Options:
38196
38394
  update:
38197
38395
  --dry-run Show update plan without changing files or containers
38198
38396
  --caddy-config <file> Host Caddyfile (default: /etc/caddy/Caddyfile); automatically update managed routes
38397
+ --caddy-profile official-upload Preserve the upload host's prod/dev Origin routing; saved after verification
38199
38398
  --services <csv> Update only selected role services
38200
38399
  --exclude <csv> Update all role services except selected services
38201
38400
  --image-tag <tag> Image mode: update to a specific prebuilt image tag
package/dist/cli.js CHANGED
@@ -19,7 +19,7 @@ import {
19
19
  serializeToYaml,
20
20
  shouldRequireTrustedAccountGuard,
21
21
  waitForWorkflowRun
22
- } from "./chunk-YW2BJUWQ.js";
22
+ } from "./chunk-OURQ2HZW.js";
23
23
  import "./chunk-IWKP55ZB.js";
24
24
  import "./chunk-BBKJZ23O.js";
25
25
  export {
package/dist/index.js CHANGED
@@ -43,7 +43,7 @@ import {
43
43
  selectAssistantMessagesForSpeech,
44
44
  serializeToYaml,
45
45
  summarizeAssistantSpeech
46
- } from "./chunk-YW2BJUWQ.js";
46
+ } from "./chunk-OURQ2HZW.js";
47
47
  import "./chunk-IWKP55ZB.js";
48
48
  import "./chunk-BBKJZ23O.js";
49
49
  export {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openmates",
3
- "version": "0.26.0-alpha.0",
3
+ "version": "0.26.0-alpha.1",
4
4
  "description": "OpenMates CLI and SDK",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",