@auxiora/config 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +191 -0
- package/dist/index.d.ts +1790 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +358 -0
- package/dist/index.js.map +1 -0
- package/dist/validator.d.ts +38 -0
- package/dist/validator.d.ts.map +1 -0
- package/dist/validator.js +180 -0
- package/dist/validator.js.map +1 -0
- package/package.json +26 -0
- package/src/index.ts +403 -0
- package/src/validator.ts +226 -0
- package/tests/config.test.ts +390 -0
- package/tsconfig.json +11 -0
- package/tsconfig.tsbuildinfo +1 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
import * as fs from 'node:fs/promises';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
import { getConfigPath, isWindows } from '@auxiora/core';
|
|
5
|
+
|
|
6
|
+
const GatewayConfigSchema = z.object({
|
|
7
|
+
host: z.string().default('0.0.0.0'),
|
|
8
|
+
port: z.number().int().min(1).max(65535).default(18800),
|
|
9
|
+
corsOrigins: z.array(z.string()).default(['http://localhost:18800']),
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
const AuthConfigSchema = z.object({
|
|
13
|
+
mode: z.enum(['none', 'password', 'jwt']).default('none'),
|
|
14
|
+
/** Argon2id hash of the gateway password */
|
|
15
|
+
passwordHash: z.string().optional(),
|
|
16
|
+
/** Secret for signing JWT tokens (min 32 chars recommended) */
|
|
17
|
+
jwtSecret: z.string().optional(),
|
|
18
|
+
jwtExpiresIn: z.string().default('7d'),
|
|
19
|
+
refreshExpiresIn: z.string().default('30d'),
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const RateLimitConfigSchema = z.object({
|
|
23
|
+
enabled: z.boolean().default(true),
|
|
24
|
+
windowMs: z.number().int().positive().default(60000), // 1 minute
|
|
25
|
+
maxRequests: z.number().int().positive().default(300),
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const PairingConfigSchema = z.object({
|
|
29
|
+
enabled: z.boolean().default(true),
|
|
30
|
+
codeLength: z.number().int().min(4).max(12).default(6),
|
|
31
|
+
expiryMinutes: z.number().int().positive().default(15),
|
|
32
|
+
autoApproveChannels: z.array(z.string()).default(['webchat']),
|
|
33
|
+
persistPath: z.string().optional(),
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const ModelRoutingSchema = z.object({
|
|
37
|
+
enabled: z.boolean().default(true),
|
|
38
|
+
defaultModel: z.string().optional(),
|
|
39
|
+
rules: z.array(z.object({
|
|
40
|
+
task: z.enum(['reasoning', 'code', 'creative', 'vision', 'long-context', 'fast', 'private', 'image-gen']),
|
|
41
|
+
provider: z.string(),
|
|
42
|
+
model: z.string(),
|
|
43
|
+
priority: z.number().default(0),
|
|
44
|
+
})).default([]),
|
|
45
|
+
costLimits: z.object({
|
|
46
|
+
dailyBudget: z.number().positive().optional(),
|
|
47
|
+
monthlyBudget: z.number().positive().optional(),
|
|
48
|
+
perMessageMax: z.number().positive().optional(),
|
|
49
|
+
warnAt: z.number().min(0).max(1).default(0.8),
|
|
50
|
+
}).default({}),
|
|
51
|
+
preferences: z.object({
|
|
52
|
+
preferLocal: z.boolean().default(false),
|
|
53
|
+
preferCheap: z.boolean().default(false),
|
|
54
|
+
sensitiveToLocal: z.boolean().default(false),
|
|
55
|
+
}).default({}),
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const ProviderConfigSchema = z.object({
|
|
59
|
+
primary: z.string().default('anthropic'),
|
|
60
|
+
fallback: z.string().optional(),
|
|
61
|
+
anthropic: z.object({
|
|
62
|
+
model: z.string().default('claude-sonnet-4-20250514'),
|
|
63
|
+
maxTokens: z.number().int().positive().default(4096),
|
|
64
|
+
}).default({}),
|
|
65
|
+
openai: z.object({
|
|
66
|
+
model: z.string().default('gpt-5.2'),
|
|
67
|
+
maxTokens: z.number().int().positive().default(4096),
|
|
68
|
+
}).default({}),
|
|
69
|
+
google: z.object({
|
|
70
|
+
model: z.string().default('gemini-2.5-flash'),
|
|
71
|
+
maxTokens: z.number().int().positive().default(4096),
|
|
72
|
+
}).default({}),
|
|
73
|
+
ollama: z.object({
|
|
74
|
+
model: z.string().default('llama3'),
|
|
75
|
+
maxTokens: z.number().int().positive().default(4096),
|
|
76
|
+
baseUrl: z.string().default('http://localhost:11434'),
|
|
77
|
+
}).default({}),
|
|
78
|
+
openaiCompatible: z.object({
|
|
79
|
+
model: z.string().default(''),
|
|
80
|
+
maxTokens: z.number().int().positive().default(4096),
|
|
81
|
+
baseUrl: z.string().default(''),
|
|
82
|
+
name: z.string().default('custom'),
|
|
83
|
+
}).default({}),
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
const SessionConfigSchema = z.object({
|
|
87
|
+
maxContextTokens: z.number().int().positive().default(100000),
|
|
88
|
+
ttlMinutes: z.number().int().positive().default(1440), // 24 hours
|
|
89
|
+
autoSave: z.boolean().default(true),
|
|
90
|
+
compactionEnabled: z.boolean().default(true),
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
const LoggingConfigSchema = z.object({
|
|
94
|
+
level: z.enum(['debug', 'info', 'warn', 'error']).default('info'),
|
|
95
|
+
auditEnabled: z.boolean().default(true),
|
|
96
|
+
maxFileSizeMb: z.number().positive().default(10),
|
|
97
|
+
maxFiles: z.number().int().positive().default(5),
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
const ChannelConfigSchema = z.object({
|
|
101
|
+
discord: z.object({
|
|
102
|
+
enabled: z.boolean().default(false),
|
|
103
|
+
mentionOnly: z.boolean().default(true),
|
|
104
|
+
}).default({}),
|
|
105
|
+
telegram: z.object({
|
|
106
|
+
enabled: z.boolean().default(false),
|
|
107
|
+
webhookMode: z.boolean().default(false),
|
|
108
|
+
}).default({}),
|
|
109
|
+
slack: z.object({
|
|
110
|
+
enabled: z.boolean().default(false),
|
|
111
|
+
socketMode: z.boolean().default(true),
|
|
112
|
+
}).default({}),
|
|
113
|
+
twilio: z.object({
|
|
114
|
+
enabled: z.boolean().default(false),
|
|
115
|
+
smsEnabled: z.boolean().default(true),
|
|
116
|
+
whatsappEnabled: z.boolean().default(false),
|
|
117
|
+
}).default({}),
|
|
118
|
+
webchat: z.object({
|
|
119
|
+
enabled: z.boolean().default(true),
|
|
120
|
+
}).default({}),
|
|
121
|
+
matrix: z.object({
|
|
122
|
+
enabled: z.boolean().default(false),
|
|
123
|
+
autoJoinRooms: z.boolean().default(true),
|
|
124
|
+
}).default({}),
|
|
125
|
+
signal: z.object({
|
|
126
|
+
enabled: z.boolean().default(false),
|
|
127
|
+
}).default({}),
|
|
128
|
+
email: z.object({
|
|
129
|
+
enabled: z.boolean().default(false),
|
|
130
|
+
pollInterval: z.number().int().positive().default(30000),
|
|
131
|
+
}).default({}),
|
|
132
|
+
teams: z.object({
|
|
133
|
+
enabled: z.boolean().default(false),
|
|
134
|
+
}).default({}),
|
|
135
|
+
whatsapp: z.object({
|
|
136
|
+
enabled: z.boolean().default(false),
|
|
137
|
+
}).default({}),
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
const VoiceConfigSchema = z.object({
|
|
141
|
+
enabled: z.boolean().default(false),
|
|
142
|
+
sttProvider: z.enum(['openai-whisper']).default('openai-whisper'),
|
|
143
|
+
ttsProvider: z.enum(['openai-tts']).default('openai-tts'),
|
|
144
|
+
defaultVoice: z.string().default('alloy'),
|
|
145
|
+
language: z.string().default('en'),
|
|
146
|
+
maxAudioDuration: z.number().int().positive().default(30),
|
|
147
|
+
sampleRate: z.number().int().positive().default(16000),
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
const WebhookConfigSchema = z.object({
|
|
151
|
+
enabled: z.boolean().default(false),
|
|
152
|
+
basePath: z.string().default('/api/v1/webhooks'),
|
|
153
|
+
signatureHeader: z.string().default('x-webhook-signature'),
|
|
154
|
+
maxPayloadSize: z.number().int().positive().default(65536),
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
const DashboardConfigSchema = z.object({
|
|
158
|
+
enabled: z.boolean().default(true),
|
|
159
|
+
sessionTtlMs: z.number().int().positive().default(86_400_000),
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
// [P6] Desktop config
|
|
163
|
+
export const DesktopConfigSchema = z.object({
|
|
164
|
+
autoStart: z.boolean().default(false),
|
|
165
|
+
minimizeToTray: z.boolean().default(true),
|
|
166
|
+
hotkey: z.string().default('CommandOrControl+Shift+A'),
|
|
167
|
+
notificationsEnabled: z.boolean().default(true),
|
|
168
|
+
updateChannel: z.enum(['stable', 'beta', 'nightly']).default('stable'),
|
|
169
|
+
ollamaEnabled: z.boolean().default(false),
|
|
170
|
+
ollamaPort: z.number().int().min(1).max(65535).default(11434),
|
|
171
|
+
windowWidth: z.number().int().positive().default(1024),
|
|
172
|
+
windowHeight: z.number().int().positive().default(768),
|
|
173
|
+
});
|
|
174
|
+
export type DesktopConfig = z.infer<typeof DesktopConfigSchema>;
|
|
175
|
+
|
|
176
|
+
// [P7] Cloud config
|
|
177
|
+
export const CloudConfigSchema = z.object({
|
|
178
|
+
enabled: z.boolean().default(false),
|
|
179
|
+
baseDataDir: z.string().default('/data/tenants'),
|
|
180
|
+
jwtSecret: z.string().default(''),
|
|
181
|
+
stripeSecretKey: z.string().optional(),
|
|
182
|
+
stripeWebhookSecret: z.string().optional(),
|
|
183
|
+
domain: z.string().default('localhost'),
|
|
184
|
+
});
|
|
185
|
+
export type CloudConfig = z.infer<typeof CloudConfigSchema>;
|
|
186
|
+
|
|
187
|
+
// [P12] Trust / Autonomy
|
|
188
|
+
const TrustConfigSchema = z.object({
|
|
189
|
+
enabled: z.boolean().default(true),
|
|
190
|
+
defaultLevel: z.number().int().min(0).max(4).default(0),
|
|
191
|
+
autoPromote: z.boolean().default(true),
|
|
192
|
+
promotionThreshold: z.number().int().positive().default(10),
|
|
193
|
+
demotionThreshold: z.number().int().positive().default(3),
|
|
194
|
+
autoPromoteCeiling: z.number().int().min(0).max(4).default(3),
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
const ResearchConfigSchema = z.object({
|
|
198
|
+
enabled: z.boolean().default(true),
|
|
199
|
+
braveApiKey: z.string().optional(),
|
|
200
|
+
defaultDepth: z.enum(['quick', 'standard', 'deep']).default('standard'),
|
|
201
|
+
maxConcurrentSources: z.number().int().positive().default(5),
|
|
202
|
+
searchTimeout: z.number().int().positive().default(10000),
|
|
203
|
+
fetchTimeout: z.number().int().positive().default(15000),
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
const IntentConfigSchema = z.object({
|
|
207
|
+
enabled: z.boolean().default(true),
|
|
208
|
+
confidenceThreshold: z.number().min(0).max(1).default(0.3),
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
const PluginsConfigSchema = z.object({
|
|
212
|
+
enabled: z.boolean().default(true),
|
|
213
|
+
dir: z.string().optional(),
|
|
214
|
+
marketplace: z.object({
|
|
215
|
+
registryUrl: z.string().default('https://registry.auxiora.dev'),
|
|
216
|
+
autoUpdate: z.boolean().default(false),
|
|
217
|
+
}).default({}),
|
|
218
|
+
pluginConfigs: z.record(z.string(), z.unknown()).default({}),
|
|
219
|
+
approvedPermissions: z.record(z.string(), z.array(z.enum([
|
|
220
|
+
'NETWORK', 'FILESYSTEM', 'SHELL', 'PROVIDER_ACCESS', 'CHANNEL_ACCESS', 'MEMORY_ACCESS',
|
|
221
|
+
]))).default({}),
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
const MemoryConfigSchema = z.object({
|
|
225
|
+
enabled: z.boolean().default(true),
|
|
226
|
+
autoExtract: z.boolean().default(true),
|
|
227
|
+
maxEntries: z.number().int().positive().default(1000),
|
|
228
|
+
encryptAtRest: z.boolean().default(false),
|
|
229
|
+
cleanupIntervalMinutes: z.number().int().positive().default(60),
|
|
230
|
+
adaptivePersonality: z.boolean().default(true),
|
|
231
|
+
patternDetection: z.boolean().default(true),
|
|
232
|
+
relationshipTracking: z.boolean().default(true),
|
|
233
|
+
importanceDecay: z.number().min(0).max(1).default(0.01),
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
const OrchestrationConfigSchema = z.object({
|
|
237
|
+
enabled: z.boolean().default(true),
|
|
238
|
+
maxConcurrentAgents: z.number().int().min(1).max(10).default(5),
|
|
239
|
+
defaultTimeout: z.number().int().positive().default(60000),
|
|
240
|
+
totalTimeout: z.number().int().positive().default(300000),
|
|
241
|
+
allowedPatterns: z.array(z.enum([
|
|
242
|
+
'parallel', 'sequential', 'debate', 'map-reduce', 'supervisor',
|
|
243
|
+
])).default(['parallel', 'sequential', 'debate', 'map-reduce', 'supervisor']),
|
|
244
|
+
costMultiplierWarning: z.number().positive().default(3),
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
const UserPreferencesSchema = z.object({
|
|
248
|
+
verbosity: z.number().min(0).max(1).default(0.5),
|
|
249
|
+
formality: z.number().min(0).max(1).default(0.5),
|
|
250
|
+
proactiveness: z.number().min(0).max(1).default(0.5),
|
|
251
|
+
riskTolerance: z.number().min(0).max(1).default(0.5),
|
|
252
|
+
humor: z.number().min(0).max(1).default(0.3),
|
|
253
|
+
feedbackStyle: z.enum(['direct', 'sandwich', 'gentle']).default('direct'),
|
|
254
|
+
expertiseAssumption: z.enum(['beginner', 'intermediate', 'expert']).default('intermediate'),
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
const ModeIdSchema = z.enum([
|
|
258
|
+
'operator', 'analyst', 'advisor', 'writer',
|
|
259
|
+
'socratic', 'legal', 'roast', 'companion',
|
|
260
|
+
]);
|
|
261
|
+
|
|
262
|
+
const ModesConfigSchema = z.object({
|
|
263
|
+
enabled: z.boolean().default(true),
|
|
264
|
+
defaultMode: z.union([ModeIdSchema, z.literal('auto'), z.literal('off')]).default('auto'),
|
|
265
|
+
autoDetection: z.boolean().default(true),
|
|
266
|
+
confirmationThreshold: z.number().min(0).max(1).default(0.4),
|
|
267
|
+
preferences: UserPreferencesSchema.default({}),
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
const AgentIdentitySchema = z.object({
|
|
271
|
+
name: z.string().default('Auxiora'),
|
|
272
|
+
pronouns: z.string().default('they/them'),
|
|
273
|
+
avatar: z.string().optional(),
|
|
274
|
+
vibe: z.string().max(200).optional(),
|
|
275
|
+
customInstructions: z.string().max(4000).optional(),
|
|
276
|
+
personality: z.string().default('professional'),
|
|
277
|
+
tone: z.object({
|
|
278
|
+
warmth: z.number().min(0).max(1).default(0.6),
|
|
279
|
+
directness: z.number().min(0).max(1).default(0.5),
|
|
280
|
+
humor: z.number().min(0).max(1).default(0.3),
|
|
281
|
+
formality: z.number().min(0).max(1).default(0.5),
|
|
282
|
+
}).default({}),
|
|
283
|
+
expertise: z.array(z.string()).default([]),
|
|
284
|
+
errorStyle: z.enum(['apologetic', 'matter_of_fact', 'self_deprecating', 'professional', 'gentle', 'detailed', 'encouraging', 'terse', 'educational']).default('professional'),
|
|
285
|
+
catchphrases: z.object({
|
|
286
|
+
greeting: z.string().optional(),
|
|
287
|
+
farewell: z.string().optional(),
|
|
288
|
+
thinking: z.string().optional(),
|
|
289
|
+
success: z.string().optional(),
|
|
290
|
+
error: z.string().optional(),
|
|
291
|
+
}).default({}),
|
|
292
|
+
boundaries: z.object({
|
|
293
|
+
neverJokeAbout: z.array(z.string()).default([]),
|
|
294
|
+
neverAdviseOn: z.array(z.string()).default([]),
|
|
295
|
+
}).default({}),
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
export type AgentIdentity = z.infer<typeof AgentIdentitySchema>;
|
|
299
|
+
|
|
300
|
+
export const ConfigSchema = z.object({
|
|
301
|
+
gateway: GatewayConfigSchema.default({}),
|
|
302
|
+
auth: AuthConfigSchema.default({}),
|
|
303
|
+
rateLimit: RateLimitConfigSchema.default({}),
|
|
304
|
+
pairing: PairingConfigSchema.default({}),
|
|
305
|
+
provider: ProviderConfigSchema.default({}),
|
|
306
|
+
routing: ModelRoutingSchema.default({}),
|
|
307
|
+
session: SessionConfigSchema.default({}),
|
|
308
|
+
logging: LoggingConfigSchema.default({}),
|
|
309
|
+
channels: ChannelConfigSchema.default({}),
|
|
310
|
+
voice: VoiceConfigSchema.default({}),
|
|
311
|
+
webhooks: WebhookConfigSchema.default({}),
|
|
312
|
+
dashboard: DashboardConfigSchema.default({}),
|
|
313
|
+
plugins: PluginsConfigSchema.default({}),
|
|
314
|
+
memory: MemoryConfigSchema.default({}),
|
|
315
|
+
orchestration: OrchestrationConfigSchema.default({}),
|
|
316
|
+
agent: AgentIdentitySchema.default({}),
|
|
317
|
+
modes: ModesConfigSchema.default({}),
|
|
318
|
+
research: ResearchConfigSchema.default({}),
|
|
319
|
+
// [P12] Trust / Autonomy
|
|
320
|
+
trust: TrustConfigSchema.default({}),
|
|
321
|
+
intent: IntentConfigSchema.default({}),
|
|
322
|
+
// [P6] Desktop
|
|
323
|
+
desktop: DesktopConfigSchema.default({}),
|
|
324
|
+
// [P7] Cloud
|
|
325
|
+
cloud: CloudConfigSchema.default({}),
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
export type Config = z.infer<typeof ConfigSchema>;
|
|
329
|
+
export type ModelRouting = z.infer<typeof ModelRoutingSchema>;
|
|
330
|
+
export type OrchestrationConfig = z.infer<typeof OrchestrationConfigSchema>;
|
|
331
|
+
export type ResearchConfig = z.infer<typeof ResearchConfigSchema>;
|
|
332
|
+
export type TrustConfig = z.infer<typeof TrustConfigSchema>;
|
|
333
|
+
export type IntentConfig = z.infer<typeof IntentConfigSchema>;
|
|
334
|
+
export type ModesConfig = z.infer<typeof ModesConfigSchema>;
|
|
335
|
+
|
|
336
|
+
const ENV_PREFIX = 'AUXIORA_';
|
|
337
|
+
|
|
338
|
+
function camelToSnake(str: string): string {
|
|
339
|
+
return str.replace(/[A-Z]/g, (letter) => `_${letter}`).toUpperCase();
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function getEnvValue(path: string[]): string | undefined {
|
|
343
|
+
const envKey = ENV_PREFIX + path.map(camelToSnake).join('_');
|
|
344
|
+
return process.env[envKey];
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function applyEnvOverrides(config: Record<string, unknown>, path: string[] = []): void {
|
|
348
|
+
for (const [key, value] of Object.entries(config)) {
|
|
349
|
+
const currentPath = [...path, key];
|
|
350
|
+
|
|
351
|
+
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
|
352
|
+
applyEnvOverrides(value as Record<string, unknown>, currentPath);
|
|
353
|
+
} else {
|
|
354
|
+
const envValue = getEnvValue(currentPath);
|
|
355
|
+
if (envValue !== undefined) {
|
|
356
|
+
if (typeof value === 'boolean') {
|
|
357
|
+
config[key] = envValue.toLowerCase() === 'true';
|
|
358
|
+
} else if (typeof value === 'number') {
|
|
359
|
+
config[key] = Number(envValue);
|
|
360
|
+
} else {
|
|
361
|
+
config[key] = envValue;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
export async function loadConfig(): Promise<Config> {
|
|
369
|
+
const configPath = getConfigPath();
|
|
370
|
+
let rawConfig: Record<string, unknown> = {};
|
|
371
|
+
|
|
372
|
+
try {
|
|
373
|
+
const content = await fs.readFile(configPath, 'utf-8');
|
|
374
|
+
rawConfig = JSON.parse(content);
|
|
375
|
+
} catch (error) {
|
|
376
|
+
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {
|
|
377
|
+
throw error;
|
|
378
|
+
}
|
|
379
|
+
// File doesn't exist, use defaults
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Apply environment variable overrides
|
|
383
|
+
applyEnvOverrides(rawConfig);
|
|
384
|
+
|
|
385
|
+
// Validate and return
|
|
386
|
+
return ConfigSchema.parse(rawConfig);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
export async function saveConfig(config: Config): Promise<void> {
|
|
390
|
+
const configPath = getConfigPath();
|
|
391
|
+
const configDir = path.dirname(configPath);
|
|
392
|
+
|
|
393
|
+
await fs.mkdir(configDir, { recursive: true });
|
|
394
|
+
await fs.writeFile(configPath, JSON.stringify(config, null, 2), 'utf-8');
|
|
395
|
+
|
|
396
|
+
if (!isWindows()) {
|
|
397
|
+
await fs.chmod(configPath, 0o600);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
export async function getDefaultConfig(): Promise<Config> {
|
|
402
|
+
return ConfigSchema.parse({});
|
|
403
|
+
}
|
package/src/validator.ts
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration validation with detailed error messages
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { type Config } from './index.js';
|
|
6
|
+
import type { ZodError } from 'zod';
|
|
7
|
+
|
|
8
|
+
export interface ValidationResult {
|
|
9
|
+
valid: boolean;
|
|
10
|
+
errors: ValidationError[];
|
|
11
|
+
warnings: ValidationWarning[];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface ValidationError {
|
|
15
|
+
path: string;
|
|
16
|
+
message: string;
|
|
17
|
+
value?: any;
|
|
18
|
+
suggestion?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface ValidationWarning {
|
|
22
|
+
path: string;
|
|
23
|
+
message: string;
|
|
24
|
+
suggestion: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Validate configuration and provide helpful error messages
|
|
29
|
+
*/
|
|
30
|
+
export function validateConfig(config: Config): ValidationResult {
|
|
31
|
+
const errors: ValidationError[] = [];
|
|
32
|
+
const warnings: ValidationWarning[] = [];
|
|
33
|
+
|
|
34
|
+
// Validate gateway configuration
|
|
35
|
+
if (config.gateway.host === '0.0.0.0' && config.auth.mode === 'none') {
|
|
36
|
+
warnings.push({
|
|
37
|
+
path: 'gateway.host',
|
|
38
|
+
message: 'Gateway bound to 0.0.0.0 with no authentication',
|
|
39
|
+
suggestion: 'Consider binding to 127.0.0.1 or enabling JWT authentication for security',
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (config.gateway.port < 1024) {
|
|
44
|
+
warnings.push({
|
|
45
|
+
path: 'gateway.port',
|
|
46
|
+
message: `Port ${config.gateway.port} requires elevated privileges`,
|
|
47
|
+
suggestion: 'Use a port >= 1024 to avoid needing root/admin access',
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Validate authentication configuration
|
|
52
|
+
if (config.auth.mode === 'jwt') {
|
|
53
|
+
if (!config.auth.jwtSecret) {
|
|
54
|
+
errors.push({
|
|
55
|
+
path: 'auth.jwtSecret',
|
|
56
|
+
message: 'JWT secret required when auth mode is "jwt"',
|
|
57
|
+
suggestion: 'Generate a secret: openssl rand -hex 32',
|
|
58
|
+
});
|
|
59
|
+
} else if (config.auth.jwtSecret.length < 32) {
|
|
60
|
+
warnings.push({
|
|
61
|
+
path: 'auth.jwtSecret',
|
|
62
|
+
message: 'JWT secret should be at least 32 characters',
|
|
63
|
+
suggestion: 'Use a longer secret for better security: openssl rand -hex 32',
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (config.auth.mode === 'password' && !config.auth.passwordHash) {
|
|
69
|
+
errors.push({
|
|
70
|
+
path: 'auth.passwordHash',
|
|
71
|
+
message: 'Password hash required when auth mode is "password"',
|
|
72
|
+
suggestion: 'Set a password using the CLI: auxiora auth set-password',
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Validate rate limiting
|
|
77
|
+
if (!config.rateLimit.enabled) {
|
|
78
|
+
warnings.push({
|
|
79
|
+
path: 'rateLimit.enabled',
|
|
80
|
+
message: 'Rate limiting is disabled',
|
|
81
|
+
suggestion: 'Enable rate limiting to prevent abuse',
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (config.rateLimit.maxRequests > 1000) {
|
|
86
|
+
warnings.push({
|
|
87
|
+
path: 'rateLimit.maxRequests',
|
|
88
|
+
message: `High rate limit (${config.rateLimit.maxRequests} requests)`,
|
|
89
|
+
suggestion: 'Consider a lower limit to prevent API quota exhaustion',
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Validate provider configuration
|
|
94
|
+
if (config.provider.primary === config.provider.fallback) {
|
|
95
|
+
warnings.push({
|
|
96
|
+
path: 'provider.fallback',
|
|
97
|
+
message: 'Fallback provider same as primary',
|
|
98
|
+
suggestion: 'Use a different provider for fallback or remove fallback',
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Validate session configuration
|
|
103
|
+
if (config.session.maxContextTokens > 200000) {
|
|
104
|
+
warnings.push({
|
|
105
|
+
path: 'session.maxContextTokens',
|
|
106
|
+
message: 'Very large context window may cause performance issues',
|
|
107
|
+
suggestion: 'Consider reducing to 100000 tokens for better performance',
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (config.session.ttlMinutes < 60) {
|
|
112
|
+
warnings.push({
|
|
113
|
+
path: 'session.ttlMinutes',
|
|
114
|
+
message: 'Short session TTL may cause frequent session loss',
|
|
115
|
+
suggestion: 'Consider at least 60 minutes for better user experience',
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Validate channel configuration
|
|
120
|
+
const enabledChannels = Object.entries(config.channels)
|
|
121
|
+
.filter(([_, channelConfig]) => channelConfig.enabled)
|
|
122
|
+
.map(([name]) => name);
|
|
123
|
+
|
|
124
|
+
if (enabledChannels.length === 0) {
|
|
125
|
+
warnings.push({
|
|
126
|
+
path: 'channels',
|
|
127
|
+
message: 'No channels enabled',
|
|
128
|
+
suggestion: 'Enable at least webchat for testing',
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Validate logging configuration
|
|
133
|
+
if (config.logging.maxFileSizeMb > 100) {
|
|
134
|
+
warnings.push({
|
|
135
|
+
path: 'logging.maxFileSizeMb',
|
|
136
|
+
message: 'Large log file size may consume significant disk space',
|
|
137
|
+
suggestion: 'Consider a smaller size with log rotation',
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
valid: errors.length === 0,
|
|
143
|
+
errors,
|
|
144
|
+
warnings,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Format validation errors for display
|
|
150
|
+
*/
|
|
151
|
+
export function formatValidationErrors(result: ValidationResult): string {
|
|
152
|
+
const lines: string[] = [];
|
|
153
|
+
|
|
154
|
+
if (result.errors.length > 0) {
|
|
155
|
+
lines.push('ā Configuration Errors:\n');
|
|
156
|
+
for (const error of result.errors) {
|
|
157
|
+
lines.push(` ${error.path}: ${error.message}`);
|
|
158
|
+
if (error.suggestion) {
|
|
159
|
+
lines.push(` š” ${error.suggestion}`);
|
|
160
|
+
}
|
|
161
|
+
lines.push('');
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (result.warnings.length > 0) {
|
|
166
|
+
lines.push('ā ļø Configuration Warnings:\n');
|
|
167
|
+
for (const warning of result.warnings) {
|
|
168
|
+
lines.push(` ${warning.path}: ${warning.message}`);
|
|
169
|
+
lines.push(` š” ${warning.suggestion}`);
|
|
170
|
+
lines.push('');
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return lines.join('\n');
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Format Zod validation errors
|
|
179
|
+
*/
|
|
180
|
+
export function formatZodError(error: ZodError): string {
|
|
181
|
+
const lines: string[] = ['ā Configuration Validation Failed:\n'];
|
|
182
|
+
|
|
183
|
+
for (const issue of error.issues) {
|
|
184
|
+
const path = issue.path.join('.');
|
|
185
|
+
lines.push(` ${path || 'config'}: ${issue.message}`);
|
|
186
|
+
|
|
187
|
+
// Add helpful suggestions based on error type
|
|
188
|
+
if (issue.code === 'invalid_type') {
|
|
189
|
+
lines.push(` š” Expected ${issue.expected}, got ${issue.received}`);
|
|
190
|
+
} else if (issue.code === 'invalid_enum_value') {
|
|
191
|
+
lines.push(` š” Valid options: ${issue.options.join(', ')}`);
|
|
192
|
+
} else if (issue.code === 'too_small') {
|
|
193
|
+
lines.push(` š” Minimum value: ${issue.minimum}`);
|
|
194
|
+
} else if (issue.code === 'too_big') {
|
|
195
|
+
lines.push(` š” Maximum value: ${issue.maximum}`);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
lines.push('');
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
lines.push('See .env.example for configuration reference');
|
|
202
|
+
|
|
203
|
+
return lines.join('\n');
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Validate and report configuration issues
|
|
208
|
+
*/
|
|
209
|
+
export function validateAndReport(config: Config): boolean {
|
|
210
|
+
const result = validateConfig(config);
|
|
211
|
+
|
|
212
|
+
if (!result.valid || result.warnings.length > 0) {
|
|
213
|
+
console.error(formatValidationErrors(result));
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (!result.valid) {
|
|
217
|
+
console.error('\nā Cannot start with invalid configuration\n');
|
|
218
|
+
return false;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (result.warnings.length > 0) {
|
|
222
|
+
console.warn('ā ļø Starting with warnings (see above)\n');
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return true;
|
|
226
|
+
}
|