import { access, readFile } from "node:fs/promises"; import { constants } from "node:fs"; import { spawn } from "node:child_process"; import process from "node:process"; async function readable(path) { try { await access(path, constants.R_OK); return true; } catch { return false; } } async function settingFromFile(environmentName, fileEnvironmentName, defaultFile, required = false) { const direct = process.env[environmentName]; if (direct !== undefined) return direct.trim(); const configuredFile = process.env[fileEnvironmentName] || defaultFile; if (await readable(configuredFile)) return (await readFile(configuredFile, "utf8")).trim(); if (required) throw new Error(`${environmentName} is required; create ${configuredFile}`); return ""; } function validateCalendarConfiguration(raw) { let values; try { values = JSON.parse(raw); } catch { throw new Error("Calendar secret must be a JSON array"); } if (!Array.isArray(values) || values.length < 1 || values.length > 10) { throw new Error("Calendar secret must contain between 1 and 10 URLs"); } const normalized = values.map((value) => { const url = new URL(String(value)); if (url.protocol !== "https:" || url.hostname !== "calendar.google.com" || !url.pathname.endsWith("/basic.ics")) { throw new Error("Every calendar URL must be an HTTPS calendar.google.com basic.ics URL"); } return url.toString(); }); return JSON.stringify([...new Set(normalized)]); } async function main() { const calendarSecret = await settingFromFile( "CALENDAR_ICAL_URLS", "CALENDAR_ICAL_URLS_FILE", "/run/secrets/calendar_ical_urls", true, ); const accessToken = await settingFromFile( "ORGANIZER_ACCESS_TOKEN", "ORGANIZER_ACCESS_TOKEN_FILE", "/run/secrets/organizer_access_token", ); if (accessToken && !/^[A-Za-z0-9._~-]{32,512}$/.test(accessToken)) { throw new Error("ORGANIZER_ACCESS_TOKEN must be empty or 32 to 512 URL-safe characters"); } const organizerTimeZone = process.env.ORGANIZER_TIMEZONE || "America/Chicago"; try { new Intl.DateTimeFormat("en-US", { timeZone: organizerTimeZone }).format(new Date()); } catch { throw new Error(`ORGANIZER_TIMEZONE is not a valid IANA timezone: ${organizerTimeZone}`); } const environment = { ...process.env, CALENDAR_ICAL_URLS: validateCalendarConfiguration(calendarSecret), ORGANIZER_ACCESS_TOKEN: accessToken, ORGANIZER_TIMEZONE: organizerTimeZone, HOSTNAME: process.env.HOSTNAME || "0.0.0.0", PORT: process.env.PORT || "3000", }; const childOptions = { cwd: process.cwd(), env: environment, stdio: "inherit", }; if (process.platform !== "win32" && typeof process.getuid === "function" && process.getuid() === 0) { childOptions.uid = 10001; childOptions.gid = 10001; } const child = spawn( process.execPath, ["node_modules/next/dist/bin/next", "start", "-H", environment.HOSTNAME, "-p", environment.PORT], childOptions, ); const forwardSignal = (signal) => { if (!child.killed) child.kill(signal); }; process.on("SIGINT", () => forwardSignal("SIGINT")); process.on("SIGTERM", () => forwardSignal("SIGTERM")); child.on("exit", (code, signal) => { if (signal) process.kill(process.pid, signal); else process.exit(code ?? 1); }); } main().catch((error) => { console.error(`[organizer] startup failed: ${error instanceof Error ? error.message : String(error)}`); process.exit(1); });