Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 14 additions & 6 deletions ENV_VARS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@

## Backend

| Variable | Description | Default |
| -------------------- | ----------------------------------- | ------- |
| PORT | Server port | 3001 |
| CORS_ALLOWED_ORIGINS | Allowed origins for CORS | \* |
| JOBS_ENABLED | Enable/disable background jobs | true |
| STELLAR_NETWORK | Stellar network (testnet or public) | testnet |
| Variable | Description | Default | Required |
| -------------------- | ----------------------------------- | ------- | -------- |
| PORT | Server port | 3001 | No |
| CORS_ALLOWED_ORIGINS | Allowed origins for CORS | \* | No |
| JOBS_ENABLED | Enable/disable background jobs | true | No |
| STELLAR_NETWORK | Stellar network (testnet or public) | testnet | No |
| OPENAI_API_KEY | OpenAI API key for AI services | - | **Yes** |

## Frontend

Expand All @@ -18,6 +19,13 @@

## Environment Files

- `.env.example`
PORT=3001
CORS_ALLOWED_ORIGINS=http://localhost:3000
JOBS_ENABLED=true
STELLAR_NETWORK=testnet
OPENAI_API_KEY=sk-your-openai-api-key

- `.env.development` — local development
- `.env.staging` — staging environment
- `.env.production` — production environment
Expand Down
67 changes: 67 additions & 0 deletions backend/src/config/__tests__/env.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { validateEnv, config } from '../env.js';
import { z } from 'zod';

describe('Environment Validation', () => {
const originalEnv = process.env;

beforeEach(() => {
vi.resetModules();
process.env = { ...originalEnv };
// Clear the cached config
vi.stubGlobal('process', {
...process,
exit: vi.fn() as any,
});
});

afterEach(() => {
process.env = originalEnv;
vi.unstubAllGlobals();
});

it('throws and exits when OPENAI_API_KEY is missing', () => {
delete process.env.OPENAI_API_KEY;

// We expect process.exit(1) to be called
validateEnv();

expect(process.exit).toHaveBeenCalledWith(1);
});

it('successfully parses valid environment variables', () => {
process.env.OPENAI_API_KEY = 'test-key';
process.env.PORT = '4000';
process.env.STELLAR_NETWORK = 'public';

const parsed = validateEnv();

expect(parsed.OPENAI_API_KEY).toBe('test-key');
expect(parsed.PORT).toBe(4000);
expect(parsed.STELLAR_NETWORK).toBe('public');
});

it('uses default values for optional variables', () => {
process.env.OPENAI_API_KEY = 'test-key';
delete process.env.PORT;
delete process.env.STELLAR_NETWORK;

const parsed = validateEnv();

expect(parsed.PORT).toBe(3001);
expect(parsed.STELLAR_NETWORK).toBe('testnet');
});

it('transforms JOBS_ENABLED correctly', () => {
process.env.OPENAI_API_KEY = 'test-key';

process.env.JOBS_ENABLED = 'false';
expect(validateEnv().JOBS_ENABLED).toBe(false);

process.env.JOBS_ENABLED = 'true';
expect(validateEnv().JOBS_ENABLED).toBe(true);

process.env.JOBS_ENABLED = 'any-other-string';
expect(validateEnv().JOBS_ENABLED).toBe(true);
});
});
46 changes: 46 additions & 0 deletions backend/src/config/env.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { z } from 'zod';
import dotenv from 'dotenv';

dotenv.config();

const envSchema = z.object({
NODE_ENV: z.enum(['development', 'test', 'production']).default('development'),
PORT: z.coerce.number().default(3001),
CORS_ALLOWED_ORIGINS: z.string().default('*'),
STELLAR_NETWORK: z.enum(['testnet', 'public']).default('testnet'),
OPENAI_API_KEY: z.string({
required_error: 'OPENAI_API_KEY is required for verification and invoicing services',
}).min(1, 'OPENAI_API_KEY cannot be empty'),
JOBS_ENABLED: z.coerce.string().transform((val) => val !== 'false').default('true'),
QUEUE_ENABLED: z.coerce.string().transform((val) => val !== 'false').default('true'),
RATE_LIMIT_FREE: z.coerce.number().default(100),
RATE_LIMIT_PRO: z.coerce.number().default(300),
RATE_LIMIT_ENTERPRISE: z.coerce.number().default(1000),
RATE_LIMIT_WINDOW_MS: z.coerce.number().default(15 * 60 * 1000),
});

export type Env = z.infer<typeof envSchema>;

let _config: Env | undefined;

export const validateEnv = (): Env => {
try {
_config = envSchema.parse(process.env);
return _config;
} catch (error: unknown) {
if (error instanceof z.ZodError) {
const missingVars = error.errors.map((err: z.ZodIssue) => `${err.path.join('.')}: ${err.message}`);
console.error('❌ Invalid environment variables:');
missingVars.forEach((msg: string) => console.error(` - ${msg}`));
process.exit(1);
}
throw error;
}
};

export const config = (): Env => {
if (!_config) {
return validateEnv();
}
return _config;
};
5 changes: 5 additions & 0 deletions backend/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ import { messageQueue } from './services/queue.js';
import { registerDefaultProcessors } from './services/queue-producers.js';
import { slaTrackingMiddleware } from './middleware/slaTracking.js';
import { requestIdMiddleware, REQUEST_ID_HEADER } from './middleware/requestId.js';
import { validateEnv, config } from './config/env.js';

// Validate environment variables at startup
validateEnv();
const env = config();

const traceStorage = new AsyncLocalStorage<string>();

Expand Down
8 changes: 2 additions & 6 deletions backend/src/services/invoice.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,10 @@
import OpenAI from 'openai';
import { config } from '../config/env.js';

let openaiClient: OpenAI | null = null;

const getOpenAIClient = () => {
const apiKey = process.env.OPENAI_API_KEY;
if (!apiKey) {
throw new Error(
'The OPENAI_API_KEY environment variable is missing or empty; provide it to generate invoices.'
);
}
const apiKey = config().OPENAI_API_KEY;

if (!openaiClient) {
openaiClient = new OpenAI({ apiKey });
Expand Down
4 changes: 3 additions & 1 deletion backend/src/services/stellar.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import * as StellarSdk from '@stellar/stellar-sdk';
import { config } from '../config/env.js';

const NETWORK = process.env.STELLAR_NETWORK || 'testnet';
const NETWORK = config().STELLAR_NETWORK;
const HORIZON_URL =
NETWORK === 'public'
? 'https://horizon.stellar.org'
: 'https://horizon-testnet.stellar.org';

export const server = new StellarSdk.Horizon.Server(HORIZON_URL);


export class ValidationError extends Error {
statusCode: number;

Expand Down
8 changes: 2 additions & 6 deletions backend/src/services/verification.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,10 @@
import OpenAI from 'openai';
import { config } from '../config/env.js';

let openaiClient: OpenAI | null = null;

const getOpenAIClient = () => {
const apiKey = process.env.OPENAI_API_KEY;
if (!apiKey) {
throw new Error(
'The OPENAI_API_KEY environment variable is missing or empty; provide it to run verification.'
);
}
const apiKey = config().OPENAI_API_KEY;

if (!openaiClient) {
openaiClient = new OpenAI({ apiKey });
Expand Down
Loading