| 218 | // would be open registration for anyone with an account at the IdP, bypassing |
| 219 | // the invite gate entirely. |
| 220 | const resolveSso = (): SsoConfig | undefined => { |
| 221 | const clientId = process.env.EXECUTOR_SSO_CLIENT_ID?.trim(); |
| 222 | const clientSecret = process.env.EXECUTOR_SSO_CLIENT_SECRET?.trim(); |
| 223 | if (!clientId && !clientSecret) return undefined; |
| 224 | if (!clientId || !clientSecret) { |
| 225 | // oxlint-disable-next-line executor/no-try-catch-or-throw, executor/no-error-constructor -- boundary: refuse to boot on half-configured SSO credentials |
| 226 | throw new Error("EXECUTOR_SSO_CLIENT_ID and EXECUTOR_SSO_CLIENT_SECRET must be set together"); |
| 227 | } |
| 228 | const providerId = process.env.EXECUTOR_SSO_PROVIDER_ID?.trim().toLowerCase() ?? ""; |
| 229 | if (!PROVIDER_ID_PATTERN.test(providerId)) { |
| 230 | // oxlint-disable-next-line executor/no-try-catch-or-throw, executor/no-error-constructor -- boundary: refuse to boot on a missing/malformed provider id |
| 231 | throw new Error( |
| 232 | 'EXECUTOR_SSO_PROVIDER_ID is required when SSO is configured (1-48 chars of [a-z0-9-], e.g. "google" or "okta") — it names the provider and its OAuth callback path', |
| 233 | ); |
| 234 | } |
| 235 | const discoveryUrl = |
| 236 | process.env.EXECUTOR_SSO_DISCOVERY_URL?.trim() || DISCOVERY_PRESETS[providerId]; |
| 237 | if (!discoveryUrl) { |
| 238 | // oxlint-disable-next-line executor/no-try-catch-or-throw, executor/no-error-constructor -- boundary: refuse to boot without a way to reach the IdP |
| 239 | throw new Error( |
| 240 | `EXECUTOR_SSO_DISCOVERY_URL is required for provider ${JSON.stringify(providerId)} (the IdP's …/.well-known/openid-configuration URL)`, |
| 241 | ); |
| 242 | } |
| 243 | const allowedDomains = (process.env.EXECUTOR_SSO_ALLOWED_DOMAINS ?? "") |
| 244 | .split(",") |
| 245 | .map((domain) => domain.trim().replace(/^@/, "").toLowerCase()) |
| 246 | .filter((domain) => domain.length > 0); |
| 247 | if (allowedDomains.length === 0) { |
| 248 | // oxlint-disable-next-line executor/no-try-catch-or-throw, executor/no-error-constructor -- boundary: SSO sign-in without a domain allowlist is open registration; refuse to boot |
| 249 | throw new Error( |
| 250 | 'EXECUTOR_SSO_ALLOWED_DOMAINS is required when SSO is configured (comma-separated email domains, e.g. "example.com") — it is what gates sign-ups in place of an invite code', |
| 251 | ); |
| 252 | } |
| 253 | const providerName = |
| 254 | process.env.EXECUTOR_SSO_PROVIDER_NAME?.trim() || |
| 255 | providerId.charAt(0).toUpperCase() + providerId.slice(1); |
| 256 | return { providerId, providerName, discoveryUrl, clientId, clientSecret, allowedDomains }; |
| 257 | }; |
| 258 | |
| 259 | // A malformed value is refused rather than silently ignored: an operator who |
| 260 | // sets the knob and typos it should find out at boot, not by watching a |