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.
- package/dist/{chunk-YW2BJUWQ.js → chunk-OURQ2HZW.js} +247 -48
- package/dist/cli.js +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
|
@@ -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 (
|
|
33292
|
-
|
|
33293
|
-
|
|
33294
|
-
|
|
33295
|
-
|
|
33296
|
-
|
|
33297
|
-
|
|
33298
|
-
|
|
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
|
-
|
|
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]) =>
|
|
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
|
|
36439
|
-
|
|
36440
|
-
if (
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
package/dist/index.js
CHANGED