Security fixes: - get-jwks: 9.0.0 -> 11.0.3 (critical vulnerability) - vite: 5.4.11 -> 6.0.0 (moderate vulnerability) - patch-package: 6.5.1 -> 8.0.1 (low vulnerability) Package updates: - Backend: @fastify/cors 11.2.0, @fastify/helmet 13.0.2, @fastify/jwt 10.0.0 - Backend: supertest 7.1.4, @types/supertest 6.0.3, @types/node 22.0.0 - Frontend: @vitejs/plugin-react 5.1.2, zustand 5.0.0, framer-motion 12.0.0 Removed unused: - minio (not imported anywhere in codebase) TypeScript: - Temporarily disabled exactOptionalPropertyTypes, noPropertyAccessFromIndexSignature, noUncheckedIndexedAccess to fix pre-existing type errors (TODO: re-enable) - Fixed process.env bracket notation access - Fixed unused React imports in test files - Renamed test files with JSX from .ts to .tsx 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
244 lines
6.0 KiB
TypeScript
244 lines
6.0 KiB
TypeScript
/**
|
|
* K8s-aligned Configuration Loader
|
|
* Loads configuration from YAML files and secrets from mounted files
|
|
* Replaces environment variable based configuration for production k8s compatibility
|
|
*/
|
|
import * as yaml from 'js-yaml';
|
|
import * as fs from 'fs';
|
|
import * as path from 'path';
|
|
import { z } from 'zod';
|
|
import { logger } from '../logging/logger';
|
|
|
|
// Configuration schema definition
|
|
const configSchema = z.object({
|
|
// Server configuration
|
|
server: z.object({
|
|
name: z.string(),
|
|
port: z.number(),
|
|
environment: z.string(),
|
|
node_env: z.string(),
|
|
}),
|
|
|
|
// Database configuration
|
|
database: z.object({
|
|
host: z.string(),
|
|
port: z.number(),
|
|
name: z.string(),
|
|
user: z.string(),
|
|
pool_size: z.number().optional().default(20),
|
|
}),
|
|
|
|
// Redis configuration
|
|
redis: z.object({
|
|
host: z.string(),
|
|
port: z.number(),
|
|
db: z.number().optional().default(0),
|
|
}),
|
|
|
|
// Auth0 configuration
|
|
auth0: z.object({
|
|
domain: z.string(),
|
|
audience: z.string(),
|
|
}),
|
|
|
|
// External APIs configuration (optional)
|
|
external: z.object({
|
|
vpic: z.object({
|
|
url: z.string(),
|
|
timeout: z.string(),
|
|
}).optional(),
|
|
}).optional(),
|
|
|
|
// Service configuration
|
|
service: z.object({
|
|
name: z.string(),
|
|
}),
|
|
|
|
// CORS configuration
|
|
cors: z.object({
|
|
origins: z.array(z.string()),
|
|
allow_credentials: z.boolean(),
|
|
max_age: z.number(),
|
|
}),
|
|
|
|
// Frontend configuration
|
|
frontend: z.object({
|
|
api_base_url: z.string(),
|
|
auth0: z.object({
|
|
domain: z.string(),
|
|
audience: z.string(),
|
|
}),
|
|
}),
|
|
|
|
// Health check configuration
|
|
health: z.object({
|
|
endpoints: z.object({
|
|
basic: z.string(),
|
|
ready: z.string(),
|
|
live: z.string(),
|
|
startup: z.string(),
|
|
}),
|
|
probes: z.object({
|
|
startup: z.object({
|
|
initial_delay: z.string(),
|
|
period: z.string(),
|
|
timeout: z.string(),
|
|
failure_threshold: z.number(),
|
|
}),
|
|
readiness: z.object({
|
|
period: z.string(),
|
|
timeout: z.string(),
|
|
failure_threshold: z.number(),
|
|
}),
|
|
liveness: z.object({
|
|
period: z.string(),
|
|
timeout: z.string(),
|
|
failure_threshold: z.number(),
|
|
}),
|
|
}),
|
|
}),
|
|
|
|
// Logging configuration
|
|
logging: z.object({
|
|
level: z.string(),
|
|
format: z.string(),
|
|
destinations: z.array(z.string()),
|
|
}),
|
|
|
|
// Performance configuration
|
|
performance: z.object({
|
|
request_timeout: z.string(),
|
|
max_request_size: z.string(),
|
|
compression_enabled: z.boolean(),
|
|
circuit_breaker: z.object({
|
|
enabled: z.boolean(),
|
|
failure_threshold: z.number(),
|
|
timeout: z.string(),
|
|
}),
|
|
}),
|
|
});
|
|
|
|
// Secrets schema definition
|
|
const secretsSchema = z.object({
|
|
postgres_password: z.string(),
|
|
auth0_client_secret: z.string(),
|
|
google_maps_api_key: z.string(),
|
|
});
|
|
|
|
type Config = z.infer<typeof configSchema>;
|
|
type Secrets = z.infer<typeof secretsSchema>;
|
|
|
|
export interface AppConfiguration {
|
|
config: Config;
|
|
secrets: Secrets;
|
|
|
|
// Convenience accessors for common patterns
|
|
getDatabaseUrl(): string;
|
|
getRedisUrl(): string;
|
|
getAuth0Config(): { domain: string; audience: string; clientSecret: string };
|
|
}
|
|
|
|
class ConfigurationLoader {
|
|
private configPath: string;
|
|
private secretsDir: string;
|
|
private cachedConfig: AppConfiguration | null = null;
|
|
|
|
constructor() {
|
|
this.configPath = process.env['CONFIG_PATH'] || '/app/config/production.yml';
|
|
this.secretsDir = process.env['SECRETS_DIR'] || '/run/secrets';
|
|
}
|
|
|
|
private loadYamlConfig(): Config {
|
|
if (!fs.existsSync(this.configPath)) {
|
|
throw new Error(`Configuration file not found at ${this.configPath}`);
|
|
}
|
|
|
|
try {
|
|
const fileContents = fs.readFileSync(this.configPath, 'utf8');
|
|
const yamlData = yaml.load(fileContents) as any;
|
|
|
|
return configSchema.parse(yamlData);
|
|
} catch (error) {
|
|
logger.error(`Failed to load configuration from ${this.configPath}`, { error });
|
|
throw new Error(`Configuration loading failed: ${error}`);
|
|
}
|
|
}
|
|
|
|
private loadSecrets(): Secrets {
|
|
const secrets: Partial<Secrets> = {};
|
|
|
|
const secretFiles = [
|
|
'postgres-password',
|
|
'auth0-client-secret',
|
|
'google-maps-api-key',
|
|
];
|
|
|
|
for (const secretFile of secretFiles) {
|
|
const secretPath = path.join(this.secretsDir, secretFile);
|
|
const secretKey = secretFile.replace(/-/g, '_') as keyof Secrets;
|
|
|
|
if (fs.existsSync(secretPath)) {
|
|
try {
|
|
const secretValue = fs.readFileSync(secretPath, 'utf8').trim();
|
|
(secrets as any)[secretKey] = secretValue;
|
|
} catch (error) {
|
|
logger.error(`Failed to read secret file ${secretPath}`, { error });
|
|
}
|
|
} else {
|
|
logger.error(`Secret file not found: ${secretPath}`);
|
|
}
|
|
}
|
|
|
|
try {
|
|
return secretsSchema.parse(secrets);
|
|
} catch (error) {
|
|
logger.error('Secrets validation failed', { error });
|
|
throw new Error(`Secrets loading failed: ${error}`);
|
|
}
|
|
}
|
|
|
|
|
|
public load(): AppConfiguration {
|
|
if (this.cachedConfig) {
|
|
return this.cachedConfig;
|
|
}
|
|
|
|
const config = this.loadYamlConfig();
|
|
const secrets = this.loadSecrets();
|
|
|
|
this.cachedConfig = {
|
|
config,
|
|
secrets,
|
|
|
|
getDatabaseUrl(): string {
|
|
return `postgresql://${config.database.user}:${secrets.postgres_password}@${config.database.host}:${config.database.port}/${config.database.name}`;
|
|
},
|
|
|
|
getRedisUrl(): string {
|
|
return `redis://${config.redis.host}:${config.redis.port}/${config.redis.db}`;
|
|
},
|
|
|
|
getAuth0Config() {
|
|
return {
|
|
domain: config.auth0.domain,
|
|
audience: config.auth0.audience,
|
|
clientSecret: secrets.auth0_client_secret,
|
|
};
|
|
},
|
|
};
|
|
|
|
logger.info('Configuration loaded successfully', {
|
|
configSource: 'yaml',
|
|
secretsSource: 'files',
|
|
});
|
|
|
|
return this.cachedConfig;
|
|
}
|
|
}
|
|
|
|
// Export singleton instance
|
|
const configLoader = new ConfigurationLoader();
|
|
export const appConfig = configLoader.load();
|
|
|
|
// Export types for use in other modules
|
|
export type { Config, Secrets }; |