import { mkdir, readFile, writeFile } from 'node:fs/promises'; import { randomBytes } from 'node:crypto'; import path from 'node:path'; const dataDirectory = path.join(process.cwd(), 'data'); const configPath = path.join(dataDirectory, 'config.json'); const defaultConfig = () => ({ adminToken: randomBytes(24).toString('base64url'), settings: { corsOrigins: [], trustedProxyHops: 0 }, credentials: [], documents: [], publications: [] }); function migrateConfig(config) { if (config.credentials) { return { ...config, settings: { ...config.settings, corsOrigins: config.settings?.corsOrigins || [], trustedProxyHops: Number(config.settings?.trustedProxyHops) || 0 } }; } const legacyCredential = config.google?.apiKey || config.google?.serviceAccountJson ? { id: crypto.randomUUID(), name: 'Imported Google connection', mode: config.google.mode, apiKey: config.google.apiKey, serviceAccountJson: config.google.serviceAccountJson, requestsPerMinute: config.google.requestsPerMinute || 45 } : null; const documents = (config.publications || []).map((publication) => ({ id: crypto.randomUUID(), name: publication.name, spreadsheetId: publication.spreadsheetId, credentialId: legacyCredential?.id || '', sheets: publication.sheets || [] })); return { adminToken: config.adminToken, settings: { corsOrigins: [], trustedProxyHops: 0 }, credentials: legacyCredential ? [legacyCredential] : [], documents, publications: (config.publications || []).map((publication, index) => ({ id: publication.id, name: publication.name, documentId: documents[index].id, sheets: publication.sheets || [], apiToken: publication.apiToken, cacheSeconds: config.google?.cacheSeconds || 30 })) }; } export async function loadConfig() { try { const config = migrateConfig(JSON.parse(await readFile(configPath, 'utf8'))); await saveConfig(config); return config; } catch (error) { if (error.code !== 'ENOENT') throw error; const config = defaultConfig(); await saveConfig(config); console.log(`Created admin token: ${config.adminToken}`); return config; } } export async function saveConfig(config) { await mkdir(dataDirectory, { recursive: true }); await writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`, { mode: 0o600 }); } export function publicConfig(config) { return { settings: config.settings, credentials: config.credentials.map(({ apiKey, serviceAccountJson, ...credential }) => ({ ...credential, configured: credential.mode === 'apiKey' ? Boolean(apiKey) : Boolean(serviceAccountJson) })), documents: config.documents, publications: config.publications.map(({ apiToken, ...publication }) => ({ ...publication, dataFormat: publication.dataFormat || 'rows', range: publication.range || '', omitBlankRows: Boolean(publication.omitBlankRows), maxRows: Math.max(1, Math.min(100_000, Number(publication.maxRows) || 10_000)), consumerRequestsPerMinute: Math.max(1, Number(publication.consumerRequestsPerMinute) || 120), scheduledRefresh: Boolean(publication.scheduledRefresh), enabled: publication.enabled !== false, tokenConfigured: Boolean(apiToken) })) }; }