From 1718e8d41bcb6991965a9e7e8ccb81cb81fdf7f2 Mon Sep 17 00:00:00 2001 From: Eric Gullickson <16152721+ericgullickson@users.noreply.github.com> Date: Sun, 18 Jan 2026 18:02:10 -0600 Subject: [PATCH] fix: use file-based secrets for Stripe API keys (refs #55) --- backend/src/core/config/config-loader.ts | 16 ++++++++++++++++ backend/src/features/subscriptions/CLAUDE.md | 9 ++++++--- backend/src/features/subscriptions/README.md | 13 ++++++++++--- .../external/stripe/stripe.client.ts | 18 +++++++----------- 4 files changed, 39 insertions(+), 17 deletions(-) diff --git a/backend/src/core/config/config-loader.ts b/backend/src/core/config/config-loader.ts index 6035e52..f9aa904 100644 --- a/backend/src/core/config/config-loader.ts +++ b/backend/src/core/config/config-loader.ts @@ -126,6 +126,9 @@ const secretsSchema = z.object({ auth0_management_client_secret: z.string(), google_maps_api_key: z.string(), resend_api_key: z.string(), + // Stripe secrets (API keys only - price IDs are config, not secrets) + stripe_secret_key: z.string(), + stripe_webhook_secret: z.string(), }); type Config = z.infer; @@ -140,6 +143,10 @@ export interface AppConfiguration { getRedisUrl(): string; getAuth0Config(): { domain: string; audience: string; clientSecret: string }; getAuth0ManagementConfig(): { domain: string; clientId: string; clientSecret: string }; + getStripeConfig(): { + secretKey: string; + webhookSecret: string; + }; } class ConfigurationLoader { @@ -178,6 +185,8 @@ class ConfigurationLoader { 'auth0-management-client-secret', 'google-maps-api-key', 'resend-api-key', + 'stripe-secret-key', + 'stripe-webhook-secret', ]; for (const secretFile of secretFiles) { @@ -240,6 +249,13 @@ class ConfigurationLoader { clientSecret: secrets.auth0_management_client_secret, }; }, + + getStripeConfig() { + return { + secretKey: secrets.stripe_secret_key, + webhookSecret: secrets.stripe_webhook_secret, + }; + }, }; // Set RESEND_API_KEY in environment for EmailService diff --git a/backend/src/features/subscriptions/CLAUDE.md b/backend/src/features/subscriptions/CLAUDE.md index 6a77922..b413fbd 100644 --- a/backend/src/features/subscriptions/CLAUDE.md +++ b/backend/src/features/subscriptions/CLAUDE.md @@ -45,10 +45,13 @@ Stripe payment integration for subscription tiers and donations. ### Webhooks (Public) - POST /api/webhooks/stripe - Stripe webhook (signature verified) -## Environment Variables +## Configuration -- STRIPE_SECRET_KEY -- STRIPE_WEBHOOK_SECRET +### Secrets (files via config-loader) +- `/run/secrets/stripe-secret-key` - Stripe API secret key +- `/run/secrets/stripe-webhook-secret` - Stripe webhook signing secret + +### Environment Variables (docker-compose) - STRIPE_PRO_MONTHLY_PRICE_ID - STRIPE_PRO_YEARLY_PRICE_ID - STRIPE_ENTERPRISE_MONTHLY_PRICE_ID diff --git a/backend/src/features/subscriptions/README.md b/backend/src/features/subscriptions/README.md index 2e4e000..932de61 100644 --- a/backend/src/features/subscriptions/README.md +++ b/backend/src/features/subscriptions/README.md @@ -151,11 +151,18 @@ When user downgrades to a tier with fewer vehicle allowance: 5. Selections saved to tier_vehicle_selections table 6. On upgrade, all vehicles become accessible again -## Environment Variables +## Configuration +### Secrets (loaded from files via config-loader) +Secrets are loaded from `/run/secrets/` (or `SECRETS_DIR` env var): +``` +/run/secrets/stripe-secret-key # Stripe API secret key +/run/secrets/stripe-webhook-secret # Stripe webhook signing secret +``` + +### Environment Variables (docker-compose) +Price IDs are not secrets and are configured via environment variables: ``` -STRIPE_SECRET_KEY=sk_live_... -STRIPE_WEBHOOK_SECRET=whsec_... STRIPE_PRO_MONTHLY_PRICE_ID=price_... STRIPE_PRO_YEARLY_PRICE_ID=price_... STRIPE_ENTERPRISE_MONTHLY_PRICE_ID=price_... diff --git a/backend/src/features/subscriptions/external/stripe/stripe.client.ts b/backend/src/features/subscriptions/external/stripe/stripe.client.ts index d59019c..597b8bb 100644 --- a/backend/src/features/subscriptions/external/stripe/stripe.client.ts +++ b/backend/src/features/subscriptions/external/stripe/stripe.client.ts @@ -5,6 +5,7 @@ import Stripe from 'stripe'; import { logger } from '../../../../core/logging/logger'; +import { appConfig } from '../../../../core/config/config-loader'; import { StripeCustomer, StripeSubscription, @@ -14,18 +15,18 @@ import { export class StripeClient { private stripe: Stripe; + private webhookSecret: string; constructor() { - const apiKey = process.env.STRIPE_SECRET_KEY; - if (!apiKey) { - throw new Error('STRIPE_SECRET_KEY environment variable is required'); - } + const stripeConfig = appConfig.getStripeConfig(); - this.stripe = new Stripe(apiKey, { + this.stripe = new Stripe(stripeConfig.secretKey, { apiVersion: '2025-12-15.clover', typescript: true, }); + this.webhookSecret = stripeConfig.webhookSecret; + logger.info('Stripe client initialized'); } @@ -237,15 +238,10 @@ export class StripeClient { */ constructWebhookEvent(payload: Buffer, signature: string): StripeWebhookEvent { try { - const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET; - if (!webhookSecret) { - throw new Error('STRIPE_WEBHOOK_SECRET environment variable is required'); - } - const event = this.stripe.webhooks.constructEvent( payload, signature, - webhookSecret + this.webhookSecret ); logger.info('Stripe webhook event verified', { eventId: event.id, type: event.type });