import { z } from 'zod';
import * as fs from 'fs';
import * as path from 'path';
// Authentication mode
export const AuthModeSchema = z.enum(['oauth', 'service_account']);
export type AuthMode = z.infer<typeof AuthModeSchema>;
// Feature flags for safety
export const FeatureFlagsSchema = z.object({
// Gmail
gmail_readonly: z.boolean().default(true),
gmail_send_enabled: z.boolean().default(false),
gmail_delete_enabled: z.boolean().default(false),
gmail_modify_labels_enabled: z.boolean().default(false),
// Drive
drive_write_enabled: z.boolean().default(true),
drive_delete_enabled: z.boolean().default(false),
// Sheets
sheets_write_enabled: z.boolean().default(true),
// Docs
docs_write_enabled: z.boolean().default(true),
// Calendar
calendar_read_enabled: z.boolean().default(true),
calendar_write_enabled: z.boolean().default(false),
calendar_dry_run_default: z.boolean().default(true),
});
export type FeatureFlags = z.infer<typeof FeatureFlagsSchema>;
// Main configuration schema
export const ConfigSchema = z.object({
// Auth
auth_mode: AuthModeSchema.default('oauth'),
// OAuth settings
oauth_client_id: z.string().optional(),
oauth_client_secret: z.string().optional(),
oauth_redirect_uri: z.string().default('http://localhost:3000/oauth/callback'),
oauth_token_path: z.string().default('.secrets/token.json'),
// Service Account settings
service_account_path: z.string().default('.secrets/service-account.json'),
service_account_email: z.string().optional(),
// Scopes
google_scopes: z.array(z.string()).default([
'https://www.googleapis.com/auth/gmail.readonly',
'https://www.googleapis.com/auth/gmail.send',
'https://www.googleapis.com/auth/drive',
'https://www.googleapis.com/auth/spreadsheets',
'https://www.googleapis.com/auth/documents',
'https://www.googleapis.com/auth/calendar',
]),
// Allowlist for Drive operations (folder IDs)
drive_allowlist_folders: z.array(z.string()).default([]),
// Feature flags
features: FeatureFlagsSchema.default({}),
// Logging
log_dir: z.string().default('./logs'),
log_to_console: z.boolean().default(true),
log_to_file: z.boolean().default(true),
log_level: z.enum(['debug', 'info', 'warn', 'error']).default('info'),
// Server
server_mode: z.enum(['stdio', 'http']).default('stdio'),
server_port: z.number().default(3000),
});
export type Config = z.infer<typeof ConfigSchema>;
// Load config from environment variables
export function loadConfigFromEnv(): Config {
const envConfig: Record<string, unknown> = {
auth_mode: process.env.AUTH_MODE,
oauth_client_id: process.env.OAUTH_CLIENT_ID,
oauth_client_secret: process.env.OAUTH_CLIENT_SECRET,
oauth_redirect_uri: process.env.OAUTH_REDIRECT_URI,
oauth_token_path: process.env.OAUTH_TOKEN_PATH,
service_account_path: process.env.SERVICE_ACCOUNT_PATH,
service_account_email: process.env.SERVICE_ACCOUNT_EMAIL,
google_scopes: process.env.GOOGLE_SCOPES?.split(',').map((s) => s.trim()),
drive_allowlist_folders: process.env.DRIVE_ALLOWLIST_FOLDERS?.split(',').map((s) => s.trim()),
log_dir: process.env.LOG_DIR,
log_to_console: process.env.LOG_TO_CONSOLE === 'true',
log_to_file: process.env.LOG_TO_FILE !== 'false',
log_level: process.env.LOG_LEVEL,
server_mode: process.env.SERVER_MODE,
server_port: process.env.SERVER_PORT ? parseInt(process.env.SERVER_PORT, 10) : undefined,
features: {
gmail_readonly: process.env.GMAIL_READONLY !== 'false',
gmail_send_enabled: process.env.GMAIL_SEND_ENABLED === 'true',
gmail_delete_enabled: process.env.GMAIL_DELETE_ENABLED === 'true',
gmail_modify_labels_enabled: process.env.GMAIL_MODIFY_LABELS_ENABLED === 'true',
drive_write_enabled: process.env.DRIVE_WRITE_ENABLED !== 'false',
drive_delete_enabled: process.env.DRIVE_DELETE_ENABLED === 'true',
calendar_read_enabled: process.env.CALENDAR_READ_ENABLED !== 'false',
calendar_write_enabled: process.env.CALENDAR_WRITE_ENABLED === 'true',
calendar_dry_run_default: process.env.CALENDAR_DRY_RUN_DEFAULT !== 'false',
},
};
// Remove undefined values
const cleanConfig = Object.fromEntries(
Object.entries(envConfig).filter(([, v]) => v !== undefined)
);
return ConfigSchema.parse(cleanConfig);
}
// Validate config and return errors if any
export function validateConfig(config: unknown): { valid: boolean; errors: string[] } {
const result = ConfigSchema.safeParse(config);
if (result.success) {
return { valid: true, errors: [] };
}
const errors = result.error.issues.map(
(issue) => `${issue.path.join('.')}: ${issue.message}`
);
return { valid: false, errors };
}
// Check if required files exist
export function checkConfigFiles(config: Config): { valid: boolean; missing: string[] } {
const missing: string[] = [];
if (config.auth_mode === 'oauth') {
// Check OAuth credentials file if not using env vars
if (!config.oauth_client_id || !config.oauth_client_secret) {
const credentialsPath = path.resolve('.secrets/credentials.json');
if (!fs.existsSync(credentialsPath)) {
missing.push(credentialsPath);
}
}
} else if (config.auth_mode === 'service_account') {
const saPath = path.resolve(config.service_account_path);
if (!fs.existsSync(saPath)) {
missing.push(saPath);
}
}
return {
valid: missing.length === 0,
missing,
};
}
// Singleton config instance
let configInstance: Config | null = null;
export function getConfig(): Config {
if (!configInstance) {
configInstance = loadConfigFromEnv();
}
return configInstance;
}
export function resetConfig(): void {
configInstance = null;
}