| 181 | * a missing secret fails loud regardless of which role is selected. |
| 182 | */ |
| 183 | export function loadWebhookSecrets(logger_: typeof logger = logger): string[] { |
| 184 | const sharedSecret = process.env.SHARED_SECRET; |
| 185 | const sharedSecretPrev = process.env.SHARED_SECRET_PREV; |
| 186 | const webhookSecrets = [sharedSecret, sharedSecretPrev].filter( |
| 187 | (s): s is string => typeof s === "string" && s.length > 0, |
| 188 | ); |
| 189 | |
| 190 | if (webhookSecrets.length > 0) return webhookSecrets; |
| 191 | |
| 192 | const isTestMode = process.env.NODE_ENV === "test"; |
| 193 | const escapeHatch = process.env.HARNESS_ALLOW_NO_SECRET === "1"; |
| 194 | if (isTestMode || escapeHatch) { |
| 195 | // CB-2 (Slot 2 #28): when the escape hatch fires with a real-looking |
| 196 | // NODE_ENV (anything except "test"), log at `warn` so a production |
| 197 | // typo (e.g. NODE_ENV=staging + HARNESS_ALLOW_NO_SECRET=1) is visible |
| 198 | // in dashboards / log alerting. Pure local-dev (NODE_ENV=test) stays |
| 199 | // at info level so a normal unit-test boot doesn't spam warnings. |
| 200 | const logLevel = escapeHatch && !isTestMode ? "warn" : "info"; |
| 201 | logger_[logLevel]("orchestrator.webhook-auth-bypass", { |
| 202 | msg: "webhook auth disabled — neither SHARED_SECRET nor SHARED_SECRET_PREV is set to a non-empty value", |
| 203 | nodeEnv: process.env.NODE_ENV ?? "(unset)", |
| 204 | escapeHatch, |
| 205 | }); |
| 206 | return webhookSecrets; |
| 207 | } |
| 208 | |
| 209 | logger_.error("orchestrator.FATAL-CONFIG", { |
| 210 | msg: "SHARED_SECRET required — refusing to boot", |
| 211 | nodeEnv: process.env.NODE_ENV ?? "(unset)", |
| 212 | }); |
| 213 | throw new Error( |
| 214 | "FATAL-CONFIG: SHARED_SECRET (or SHARED_SECRET_PREV) is required — refusing to boot " + |
| 215 | "in any deployable mode (gate at src/http/server.ts:119 only registers " + |
| 216 | "POST /webhooks/deploy when webhookSecrets.length > 0, so booting without " + |
| 217 | "a secret would silently 404 every notify-harness POST from the " + |
| 218 | "Showcase: Verify Deploy workflow). " + |
| 219 | "Set SHARED_SECRET (or SHARED_SECRET_PREV) in the env, or set " + |
| 220 | "NODE_ENV=test / HARNESS_ALLOW_NO_SECRET=1 for local dev. " + |
| 221 | `Current NODE_ENV=${process.env.NODE_ENV ?? "(unset)"}.`, |
| 222 | ); |
| 223 | } |
| 224 | |
| 225 | /** |
| 226 | * Resolve the PocketBase URL with the SAME symmetric fail-loud predicate |