diff --git a/.changeset/frontend-url-env-parsing.md b/.changeset/frontend-url-env-parsing.md new file mode 100644 index 0000000..1bc1dc4 --- /dev/null +++ b/.changeset/frontend-url-env-parsing.md @@ -0,0 +1,9 @@ +--- +'seamless-auth-api': patch +--- + +Parse the `FRONTEND_URL` env var as a single string in `parseSystemConfigEnvValue`. +The `frontend_url` system config was added to the env map and schema but never +handled by the env parser, so bootstrap threw `Unhandled system config key: +frontend_url` whenever `FRONTEND_URL` was set. Document the variable in +`.env.example`. diff --git a/.env.example b/.env.example index ef82ca3..fed1c94 100644 --- a/.env.example +++ b/.env.example @@ -61,6 +61,9 @@ TOTP_SECRET_ENCRYPTION_KEY= RPID=localhost ORIGINS=http://localhost:5173,http://localhost:5174 +# Base URL for links emailed to users (magic links). Falls back to the first ORIGINS entry when unset. +FRONTEND_URL=http://localhost:5173 + # OAUTH # JSON array of provider config. Provider client secrets stay in env vars referenced by # clientSecretEnv, never in system_config or this JSON value. diff --git a/src/schemas/systemConfig.schema.ts b/src/schemas/systemConfig.schema.ts index 2fcfe33..04b3d3f 100644 --- a/src/schemas/systemConfig.schema.ts +++ b/src/schemas/systemConfig.schema.ts @@ -78,8 +78,6 @@ export const SystemConfigSchema = z.object({ rpid: z.string().min(1), origins: z.array(z.url()).min(1), - // Base URL of the user-facing frontend, used to build links emailed to users - // (e.g. magic links). Falls back to origins[0] when unset. frontend_url: z.url().optional(), }); diff --git a/src/utils/parseEnvConfigs.ts b/src/utils/parseEnvConfigs.ts index 13f4c3c..c28b598 100644 --- a/src/utils/parseEnvConfigs.ts +++ b/src/utils/parseEnvConfigs.ts @@ -21,9 +21,6 @@ export function parseSystemConfigEnvValue(key: keyof typeof SYSTEM_CONFIG_ENV_MA .filter(Boolean); case 'oauth_providers': - // Validate through the schema so per-provider defaults (subjectJsonPath, - // emailJsonPath, ...) are applied; raw JSON.parse leaves them undefined and - // OAuth profile extraction then silently fails. return z.array(OAuthProviderConfigSchema).parse(JSON.parse(raw)); case 'lockout_policy': @@ -40,6 +37,7 @@ export function parseSystemConfigEnvValue(key: keyof typeof SYSTEM_CONFIG_ENV_MA case 'refresh_token_ttl': case 'rpid': case 'app_name': + case 'frontend_url': return raw; default: diff --git a/tests/unit/utils/parseSystemConfigEnvValue.spec.ts b/tests/unit/utils/parseSystemConfigEnvValue.spec.ts index dc8c392..c3ea35a 100644 --- a/tests/unit/utils/parseSystemConfigEnvValue.spec.ts +++ b/tests/unit/utils/parseSystemConfigEnvValue.spec.ts @@ -54,6 +54,12 @@ describe('parseSystemConfigEnvValue', () => { expect(result).toBe('SeamlessAuth'); }); + + it('returns frontend_url as a single string, not an array', () => { + const result = parseSystemConfigEnvValue('frontend_url', 'http://localhost:5173'); + + expect(result).toBe('http://localhost:5173'); + }); }); describe('boolean parsing', () => {