From 90e5df4a2d1b4eb107fe119042b888c762575022 Mon Sep 17 00:00:00 2001 From: Mark Rapson Date: Sun, 5 Jul 2026 21:13:18 +0100 Subject: [PATCH] Added check for duplicate config entries --- lib/config.js | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/lib/config.js b/lib/config.js index 7d50af0..edd2f48 100644 --- a/lib/config.js +++ b/lib/config.js @@ -42,6 +42,54 @@ function validateConfig(config) { throw new Error('Configuration must include at least one `apiKeys` or `serviceAccounts`'); } + // Ensure API key values are unique when using apiKeys + if (hasApiKeys) { + const extracted = config.apiKeys.map(item => { + if (typeof item === 'string') return item.trim(); + if (item && typeof item === 'object') return (item.key || item.apiKey || '').toString().trim(); + return ''; + }); + const nonEmpty = extracted.filter(Boolean); + const unique = new Set(nonEmpty); + if (unique.size !== nonEmpty.length) { + throw new Error('`apiKeys` contains duplicate `key` values; ensure all API keys are unique'); + } + } + + // Ensure service account credential paths are unique when using serviceAccounts + if (hasSvcAccounts) { + // Read and canonicalize the contents of each service account credential + const svcContents = config.serviceAccounts.map(item => { + let p = ''; + if (typeof item === 'string') p = item.trim(); + else if (item && typeof item === 'object') p = (item.path || item.file || item.credentialsPath || '').toString().trim(); + if (!p) return ''; + const full = path.isAbsolute(p) ? p : path.resolve(process.cwd(), p); + if (!fs.existsSync(full)) { + throw new Error(`Service account file not found: ${p}`); + } + const raw = fs.readFileSync(full, 'utf8').trim(); + try { + const parsed = JSON.parse(raw); + const canonicalize = (obj) => { + if (obj === null || typeof obj !== 'object') return JSON.stringify(obj); + if (Array.isArray(obj)) return '[' + obj.map(canonicalize).join(',') + ']'; + const keys = Object.keys(obj).sort(); + return '{' + keys.map(k => JSON.stringify(k) + ':' + canonicalize(obj[k])).join(',') + '}'; + }; + return canonicalize(parsed); + } catch (e) { + // Non-JSON files: use raw contents + return raw; + } + }).filter(Boolean); + + const uniqueSvc = new Set(svcContents); + if (uniqueSvc.size !== svcContents.length) { + throw new Error('`serviceAccounts` contains duplicate credential contents; ensure all service account credentials are unique'); + } + } + // Basic validation of documents/sheets structure for (const doc of config.documents) { if (!doc.documentId) throw new Error('Each document must include a `documentId`');