diff --git a/apps/api/src/config.ts b/apps/api/src/config.ts index 998aee1..72ed9b0 100644 --- a/apps/api/src/config.ts +++ b/apps/api/src/config.ts @@ -1,5 +1,8 @@ import { z } from 'zod'; +const optionalString = z.preprocess((value) => value === '' ? undefined : value, z.string().min(1).optional()); +const optionalUuid = z.preprocess((value) => value === '' ? undefined : value, z.string().uuid().optional()); + const baseSchema = z.object({ NODE_ENV: z.enum(['development', 'test', 'production']).default('development'), PORT: z.coerce.number().int().min(1).max(65535).default(3003), @@ -12,13 +15,13 @@ const baseSchema = z.object({ MEDIA_ROOT: z.string().min(1).default('/data/media'), MEDIA_SIGNING_SECRET: z.string().min(32).optional(), MEDIA_PUBLIC_BASE_URL: z.string().url().optional(), - SUPABASE_AUTH_SERVICE_ROLE_KEY: z.string().min(1).optional(), - REVENUECAT_API_KEY: z.string().min(1).optional(), + SUPABASE_AUTH_SERVICE_ROLE_KEY: optionalString, + REVENUECAT_API_KEY: optionalString, REVENUECAT_ENTITLEMENT_ID: z.string().min(1).default('Sauci Pro'), - REVENUECAT_WEBHOOK_SECRET: z.string().min(1).optional(), - ADMIN_API_SERVICE_TOKEN: z.string().min(32).optional(), - ADMIN_API_SERVICE_USER_ID: z.string().uuid().optional(), - ADMIN_PRIVATE_KEY_JWK: z.string().optional(), + REVENUECAT_WEBHOOK_SECRET: optionalString, + ADMIN_API_SERVICE_TOKEN: z.preprocess((value) => value === '' ? undefined : value, z.string().min(32).optional()), + ADMIN_API_SERVICE_USER_ID: optionalUuid, + ADMIN_PRIVATE_KEY_JWK: optionalString, }); export interface AppConfig { diff --git a/apps/api/test/auth.test.ts b/apps/api/test/auth.test.ts index c4b6bea..51416bc 100644 --- a/apps/api/test/auth.test.ts +++ b/apps/api/test/auth.test.ts @@ -61,4 +61,28 @@ describe('Supabase Auth JWT verification', () => { AUTH_TEST_JWKS: jwks, })).toThrow('AUTH_TEST_JWKS is forbidden'); }); + + it('treats empty optional deployment integrations as disabled', async () => { + const { jwks } = await signedToken(); + const config = loadConfig({ + NODE_ENV: 'test', + DATABASE_URL: 'postgresql://localhost/sauci_test', + SUPABASE_AUTH_ISSUER: issuer, + AUTH_TEST_JWKS: jwks, + SUPABASE_AUTH_SERVICE_ROLE_KEY: '', + REVENUECAT_API_KEY: '', + REVENUECAT_WEBHOOK_SECRET: '', + ADMIN_API_SERVICE_TOKEN: '', + ADMIN_API_SERVICE_USER_ID: '', + ADMIN_PRIVATE_KEY_JWK: '', + }); + expect(config).toMatchObject({ + supabaseAuthServiceRoleKey: undefined, + revenueCatApiKey: undefined, + revenueCatWebhookSecret: undefined, + adminApiServiceToken: undefined, + adminApiServiceUserId: undefined, + adminPrivateKeyJwk: undefined, + }); + }); }); diff --git a/infra/backend/README.md b/infra/backend/README.md index d96230f..9cd6833 100644 --- a/infra/backend/README.md +++ b/infra/backend/README.md @@ -45,6 +45,12 @@ different name `SAUCI_ADMIN_API_TOKEN` plus its independent caller-facing `SAUCI_MCP_API_KEY`. The token does not confer Supabase authority: the API maps it to the configured administrator and applies normal permissions and auditing. Do not give either admin token to mobile, admin browser, or worker containers. +An environment without an imported active administrator may leave both values +empty; customer API routes still run, while MCP service authentication remains +disabled until the pair is configured. RevenueCat credentials are likewise +optional at infrastructure bootstrap, but purchase verification, webhooks, and +account-deletion provider cleanup remain unavailable until their dedicated +server-side credentials are installed. Secret ownership is intentionally split: diff --git a/infra/backend/compose.production.yaml b/infra/backend/compose.production.yaml index 8fb0ac9..c3c0287 100644 --- a/infra/backend/compose.production.yaml +++ b/infra/backend/compose.production.yaml @@ -10,12 +10,12 @@ x-api-environment: &api-environment PORT: 3003 SUPABASE_AUTH_URL: ${SUPABASE_AUTH_URL:?set SUPABASE_AUTH_URL} SUPABASE_AUTH_AUDIENCE: ${SUPABASE_AUTH_AUDIENCE:-authenticated} - SUPABASE_AUTH_SERVICE_ROLE_KEY: ${SUPABASE_AUTH_SERVICE_ROLE_KEY:?set SUPABASE_AUTH_SERVICE_ROLE_KEY} - REVENUECAT_API_KEY: ${REVENUECAT_API_KEY:?set REVENUECAT_API_KEY} + SUPABASE_AUTH_SERVICE_ROLE_KEY: ${SUPABASE_AUTH_SERVICE_ROLE_KEY:-} + REVENUECAT_API_KEY: ${REVENUECAT_API_KEY:-} REVENUECAT_ENTITLEMENT_ID: ${REVENUECAT_ENTITLEMENT_ID:-Sauci Pro} - REVENUECAT_WEBHOOK_SECRET: ${REVENUECAT_WEBHOOK_SECRET:?set REVENUECAT_WEBHOOK_SECRET} - ADMIN_API_SERVICE_TOKEN: ${ADMIN_API_SERVICE_TOKEN:?set ADMIN_API_SERVICE_TOKEN} - ADMIN_API_SERVICE_USER_ID: ${ADMIN_API_SERVICE_USER_ID:?set ADMIN_API_SERVICE_USER_ID} + REVENUECAT_WEBHOOK_SECRET: ${REVENUECAT_WEBHOOK_SECRET:-} + ADMIN_API_SERVICE_TOKEN: ${ADMIN_API_SERVICE_TOKEN:-} + ADMIN_API_SERVICE_USER_ID: ${ADMIN_API_SERVICE_USER_ID:-} ADMIN_PRIVATE_KEY_JWK: ${ADMIN_PRIVATE_KEY_JWK:-} x-worker-environment: &worker-environment