|
1 | | -import { building } from '$app/environment'; |
2 | | -import { z } from 'zod'; |
3 | | - |
4 | | -/** |
5 | | - * Read an environment variable from worker runtime (globalThis.env) or Bun/Node (process.env). |
6 | | - */ |
7 | | -function getEnv(key: string): string | undefined { |
8 | | - if (typeof globalThis !== 'undefined' && (globalThis as any).env) { |
9 | | - const val = (globalThis as any).env[key]; |
10 | | - |
11 | | - if (val !== undefined) { |
12 | | - return String(val); |
13 | | - } |
14 | | - } |
15 | | - |
16 | | - if (typeof process !== 'undefined' && process.env) { |
17 | | - return process.env[key]; |
18 | | - } |
19 | | - |
20 | | - return undefined; |
21 | | -} |
22 | | - |
23 | | -// UUID-like pattern (less strict than RFC 4122) |
24 | | -const uuidLike = z |
25 | | - .string() |
26 | | - .regex(/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/, 'Invalid UUID format'); |
27 | | - |
28 | | -// Environment schema |
29 | | -const EnvironmentSchema = z.enum(['development', 'staging', 'production', 'test']); |
30 | | - |
31 | | -// Check if DATABASE binding is available (worker runtime) |
32 | | -const hasDatabaseBinding = !!(globalThis as any).env?.DATABASE?.query; |
33 | | - |
34 | | -// Configuration schema |
35 | | -const ConfigSchema = z.object({ |
36 | | - // Environment |
37 | | - nodeEnv: EnvironmentSchema.default('development'), |
38 | | - |
39 | | - // Server |
40 | | - port: z.coerce.number().int().positive().default(7000), |
41 | | - |
42 | | - // JWT |
43 | | - jwt: z.object({ |
44 | | - access: z.object({ |
45 | | - secret: z.string().min(32, 'JWT_ACCESS_SECRET must be at least 32 characters'), |
46 | | - expiresIn: z.string().default('15m') |
47 | | - }), |
48 | | - refresh: z.object({ |
49 | | - secret: z.string().min(32, 'JWT_REFRESH_SECRET must be at least 32 characters'), |
50 | | - expiresIn: z.string().default('18h') |
51 | | - }) |
52 | | - }), |
53 | | - |
54 | | - // GitHub OAuth |
55 | | - github: z.object({ |
56 | | - clientId: z.string().optional(), |
57 | | - clientSecret: z.string().optional() |
58 | | - }), |
59 | | - |
60 | | - // Postgate (SQL proxy) |
61 | | - postgate: z.object({ |
62 | | - url: z.url().default('http://localhost:6080'), |
63 | | - // Token (pg_xxx format) - for accessing OpenWorkers database |
64 | | - token: (() => { |
65 | | - const pgToken = z.string().regex(/^pg_[a-f0-9]{64}$/, 'POSTGATE_TOKEN must be a valid pg_xxx token'); |
66 | | - return hasDatabaseBinding ? pgToken.optional() : pgToken; |
67 | | - })(), |
68 | | - // Secret for generating deterministic system tokens |
69 | | - systemTokenSecret: z.string().min(32, 'POSTGATE_SYSTEM_TOKEN_SECRET must be at least 32 characters') |
70 | | - }), |
71 | | - |
72 | | - // Shared S3/R2 for assets and storage bindings |
73 | | - sharedStorage: z.object({ |
74 | | - bucket: z.string().optional(), |
75 | | - endpoint: z.string().optional(), |
76 | | - accessKeyId: z.string().optional(), |
77 | | - secretAccessKey: z.string().optional(), |
78 | | - publicUrl: z.string().optional() |
79 | | - }), |
80 | | - |
81 | | - // AI Services |
82 | | - mistral: z.object({ |
83 | | - apiKey: z.string().optional() |
84 | | - }), |
85 | | - |
86 | | - // Email provider (verification, password reset) |
87 | | - email: z.object({ |
88 | | - provider: z.enum(['scaleway']).optional(), |
89 | | - from: z.string().default('noreply@openworkers.dev'), |
90 | | - // Scaleway-specific |
91 | | - secretKey: z.string().optional(), |
92 | | - projectId: z.string().optional(), |
93 | | - region: z.string().default('fr-par') |
94 | | - }), |
95 | | - |
96 | | - // App URLs for email links |
97 | | - appUrl: z.string().url().default('http://localhost:4200') |
98 | | -}); |
99 | | - |
100 | | -// Type inference |
101 | | -export type Config = z.infer<typeof ConfigSchema>; |
102 | | -export type Environment = z.infer<typeof EnvironmentSchema>; |
103 | | - |
104 | | -// Parse and validate environment variables |
105 | | -function loadConfig(): Config { |
106 | | - const rawConfig = { |
107 | | - nodeEnv: getEnv('NODE_ENV'), |
108 | | - port: getEnv('PORT'), |
109 | | - jwt: { |
110 | | - access: { |
111 | | - secret: getEnv('JWT_ACCESS_SECRET'), |
112 | | - expiresIn: getEnv('JWT_ACCESS_EXP') |
113 | | - }, |
114 | | - refresh: { |
115 | | - secret: getEnv('JWT_REFRESH_SECRET'), |
116 | | - expiresIn: getEnv('JWT_REFRESH_EXP') |
117 | | - } |
118 | | - }, |
119 | | - github: { |
120 | | - clientId: getEnv('GITHUB_CLIENT_ID'), |
121 | | - clientSecret: getEnv('GITHUB_CLIENT_SECRET') |
122 | | - }, |
123 | | - postgate: { |
124 | | - url: getEnv('POSTGATE_URL'), |
125 | | - token: getEnv('POSTGATE_TOKEN'), |
126 | | - systemTokenSecret: getEnv('POSTGATE_SYSTEM_TOKEN_SECRET') |
127 | | - }, |
128 | | - sharedStorage: { |
129 | | - bucket: getEnv('SHARED_STORAGE_BUCKET'), |
130 | | - endpoint: getEnv('SHARED_STORAGE_ENDPOINT'), |
131 | | - accessKeyId: getEnv('SHARED_STORAGE_ACCESS_KEY_ID'), |
132 | | - secretAccessKey: getEnv('SHARED_STORAGE_SECRET_ACCESS_KEY'), |
133 | | - publicUrl: getEnv('SHARED_STORAGE_PUBLIC_URL') |
134 | | - }, |
135 | | - mistral: { |
136 | | - apiKey: getEnv('MISTRAL_API_KEY') |
137 | | - }, |
138 | | - email: { |
139 | | - provider: getEnv('EMAIL_PROVIDER'), |
140 | | - from: getEnv('EMAIL_FROM'), |
141 | | - secretKey: getEnv('SCW_SECRET_KEY'), |
142 | | - projectId: getEnv('SCW_PROJECT_ID'), |
143 | | - region: getEnv('SCW_REGION') |
144 | | - }, |
145 | | - appUrl: getEnv('APP_URL') |
146 | | - }; |
147 | | - |
148 | | - try { |
149 | | - const config = ConfigSchema.parse(rawConfig); |
150 | | - |
151 | | - // Log configuration status |
152 | | - if (config.nodeEnv === 'development') { |
153 | | - console.log('Running in DEVELOPMENT mode'); |
154 | | - } else if (config.nodeEnv === 'production') { |
155 | | - console.log('Running in PRODUCTION mode'); |
156 | | - } |
157 | | - |
158 | | - // Warn about missing GitHub OAuth |
159 | | - if (!config.github.clientId || !config.github.clientSecret) { |
160 | | - console.warn('GitHub OAuth not configured (GITHUB_CLIENT_ID/GITHUB_CLIENT_SECRET missing)'); |
161 | | - } |
162 | | - |
163 | | - // Warn about missing email provider |
164 | | - if (!config.email.provider) { |
165 | | - console.warn('Email not configured (EMAIL_PROVIDER missing) - email features disabled'); |
166 | | - } |
167 | | - |
168 | | - return config; |
169 | | - } catch (error) { |
170 | | - if (error instanceof z.ZodError) { |
171 | | - console.error('Configuration validation failed:', error); |
172 | | - throw new Error('Invalid configuration'); |
173 | | - } |
174 | | - throw error; |
175 | | - } |
176 | | -} |
177 | | - |
178 | | -// At build time, env vars are unavailable — use safe defaults |
179 | | -function buildConfig(): Config { |
180 | | - return { |
181 | | - nodeEnv: 'development', |
182 | | - port: 7000, |
183 | | - jwt: { access: { secret: '-' }, refresh: { secret: '-' } }, |
184 | | - github: {}, |
185 | | - postgate: { url: '-', token: '-', systemTokenSecret: '-' }, |
186 | | - sharedStorage: {}, |
187 | | - email: {}, |
188 | | - mistral: {}, |
189 | | - appUrl: 'http://localhost:4200' |
190 | | - } as Config; |
191 | | -} |
192 | | - |
193 | | -// Export singleton config instance |
194 | | -export const config: Config = building ? buildConfig() : loadConfig(); |
195 | | - |
196 | | -// Export individual sections for convenience |
197 | | -export const { nodeEnv, port, jwt, github, postgate, sharedStorage, mistral, email, appUrl } = config; |
| 1 | +export { getEnv } from './env'; |
| 2 | +export { getNodeEnv, getPort } from './server'; |
| 3 | +export type { Environment } from './server'; |
| 4 | +export { getJwtConfig } from './jwt'; |
| 5 | +export type { JwtConfig } from './jwt'; |
| 6 | +export { getPostgateConfig } from './postgate'; |
| 7 | +export type { PostgateConfig } from './postgate'; |
| 8 | +export { getGithubConfig } from './github'; |
| 9 | +export type { GithubConfig } from './github'; |
| 10 | +export { getStorageConfig } from './storage'; |
| 11 | +export type { StorageConfig } from './storage'; |
| 12 | +export { getMistralConfig } from './mistral'; |
| 13 | +export type { MistralConfig } from './mistral'; |
| 14 | +export { getEmailConfig } from './email'; |
| 15 | +export type { EmailConfig } from './email'; |
0 commit comments