forked from Adamantine-guild/guildpass-core
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathconfig.ts
More file actions
128 lines (115 loc) · 3.46 KB
/
Copy pathconfig.ts
File metadata and controls
128 lines (115 loc) · 3.46 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
import { z } from 'zod';
// Define your configuration schema
const ConfigSchema = z.object({
// Server
port: z.coerce
.number()
.int()
.positive('Port must be a positive integer')
.default(3000),
nodeEnv: z
.enum(['development', 'production', 'test'])
.default('development'),
// Database (REQUIRED)
databaseUrl: z
.string()
.url('DATABASE_URL must be a valid URL')
.min(1, 'DATABASE_URL is required'),
// Logging
logLevel: z
.enum(['error', 'warn', 'info', 'debug'])
.default('info'),
// Access decision caching (disabled by default)
accessDecisionCacheEnabled: z
.coerce
.boolean()
.default(false),
accessDecisionCacheTtlSeconds: z
.coerce
.number()
.int()
.positive('accessDecisionCacheTtlSeconds must be > 0')
.default(30),
// TTL for version counters; prevents unbounded key growth if never updated
accessDecisionCacheVersionTtlSeconds: z
.coerce
.number()
.int()
.positive('accessDecisionCacheVersionTtlSeconds must be > 0')
.default(86400),
// Redis connection (required only when accessDecisionCacheEnabled=true)
redisUrl: z.string().optional(),
// Reconciliation worker
reconciliationIntervalMs: z.coerce
.number()
.int()
.positive()
.default(60_000),
// Rate limiting
rateLimitEnabled: z
.string()
.transform((v: string) => v !== 'false' && v !== '0')
.default('true'),
rateLimitWindowMs: z.coerce
.number()
.int()
.positive()
.default(60_000),
rateLimitDefaultMax: z.coerce
.number()
.int()
.positive()
.default(100),
rateLimitExpensiveMax: z.coerce
.number()
.int()
.positive()
.default(20),
redisUrl: z.string().optional(),
});
export type Config = z.infer<typeof ConfigSchema>;
/**
* Validates environment configuration at startup.
* Fails fast with clear error messages if any required vars are missing or malformed.
*/
function validateConfig(): Config {
const envVars = {
port: process.env.PORT,
nodeEnv: process.env.NODE_ENV,
databaseUrl: process.env.DATABASE_URL,
logLevel: process.env.LOG_LEVEL,
reconciliationIntervalMs: process.env.RECONCILIATION_INTERVAL_MS,
rateLimitEnabled: process.env.RATE_LIMIT_ENABLED,
rateLimitWindowMs: process.env.RATE_LIMIT_WINDOW_MS,
rateLimitDefaultMax: process.env.RATE_LIMIT_DEFAULT_MAX,
rateLimitExpensiveMax: process.env.RATE_LIMIT_EXPENSIVE_MAX,
redisUrl: process.env.REDIS_URL,
};
const result = ConfigSchema.safeParse(envVars);
if (!result.success) {
console.error(
'\n❌ Environment configuration validation failed:\n'
);
result.error.errors.forEach((err) => {
const path = err.path.join('.');
console.error(` ${path}: ${err.message}`);
});
console.error(
'\n📖 See .env.example for all required and optional settings.\n'
);
process.exit(1);
}
// Log configuration on startup (non-sensitive values only)
if (result.data.nodeEnv !== 'test') {
console.log(`\n✅ Configuration loaded successfully`);
console.log(` NODE_ENV: ${result.data.nodeEnv}`);
console.log(` PORT: ${result.data.port}`);
console.log(` LOG_LEVEL: ${result.data.logLevel}\n`);
}
// If caching is enabled, ensure redisUrl is present.
if (result.data.accessDecisionCacheEnabled && !result.data.redisUrl) {
throw new Error('accessDecisionCacheEnabled=true requires redisUrl');
}
return result.data;
}
export const config = validateConfig();