Spaces:
Sleeping
Sleeping
File size: 3,673 Bytes
1460b37 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 | // 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 "<base> <lineIndex>" from the paste position, blind to existing
// names, so re-adding keys often silently replaced earlier ones.
//
// This planner gap-fills the smallest free "<base> <n>" 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 " <n>", 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 "<base> <n>" 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 "<base> <n>" 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;
}
|