// Bulk-add API-key planner. // // Background: the backend upserts apikey connections BY NAME // (src/lib/db/repos/connectionsRepo.js ~L144: existing = all.find(c => // c.authType === "apikey" && c.name === data.name)). A colliding name // overwrites an existing key instead of inserting a new one. Bulk-add used to // derive " " from the paste position, blind to existing // names, so re-adding keys often silently replaced earlier ones. // // This planner gap-fills the smallest free " " against both existing // connection names and names already assigned earlier in the same batch, so a // generated name is never reused and the backend always inserts. // // ponytail: only numeric-suffix collision is handled. A user who manually // types an exact existing non-numbered custom name (no index) will still hit // the backend upsert — but bulk auto-naming always appends " ", so this // path is unreachable from the bulk modal. Upgrade path: a backend // "skip-if-exists" flag on POST /api/providers if single-add ever needs it. /** * Parse one pipe-separated bulk line into { baseName, apiKey, providerSpecificData? }. * @param {string} line * @param {{isCloudflareAi?: boolean}} [opts] * @returns {{baseName: string, apiKey: string, providerSpecificData?: object}|null} */ function parseLine(line, opts = {}) { const { isCloudflareAi = false } = opts; const parts = line.split("|"); if (isCloudflareAi && parts.length >= 3) { // name|apiKey|accountId (apiKey may itself contain pipes) const baseName = parts[0].trim(); const apiKey = parts.slice(1, -1).join("|").trim(); const accountId = parts[parts.length - 1].trim(); return { baseName: baseName || "Key", apiKey, providerSpecificData: { accountId }, }; } if (parts.length >= 2) { // name|apiKey (apiKey may itself contain pipes) const baseName = parts[0].trim(); const apiKey = parts.slice(1).join("|").trim(); return { baseName: baseName || "Key", apiKey }; } // apiKey only — auto-named "Key N" const apiKey = parts[0].trim(); return { baseName: "Key", apiKey }; } /** * Plan a bulk add: parse lines, assign collision-free " " names. * * @param {string[]} lines raw paste lines * @param {string[]|null|undefined} existingNames connection names already saved * @param {{isCloudflareAi?: boolean}} [opts] * @returns {{name: string, apiKey: string, skipped: boolean, providerSpecificData?: object}[]} */ export function planBulkAdd(lines, existingNames, opts = {}) { const { isCloudflareAi = false } = opts; const safeExisting = Array.isArray(existingNames) ? existingNames : []; const used = new Set(safeExisting.map((n) => (typeof n === "string" ? n.toLowerCase() : ""))); const out = []; for (const raw of lines) { const line = typeof raw === "string" ? raw.trim() : ""; if (!line) continue; const parsed = parseLine(line, { isCloudflareAi }); if (!parsed || !parsed.apiKey) continue; const base = parsed.baseName; // Gap-fill from 1: smallest free " " not in `used`. // O(batch * existing) — fine for bulk add (tens to low hundreds of keys). let idx = 1; let name; for (;;) { name = `${base} ${idx}`; if (!used.has(name.toLowerCase())) break; idx += 1; } used.add(name.toLowerCase()); const entry = { name, apiKey: parsed.apiKey, skipped: false }; if (parsed.providerSpecificData) entry.providerSpecificData = parsed.providerSpecificData; out.push(entry); } return out; }