import { unwrapPastedSecret } from './account-text-parser.js'; const EMAIL_RE = /[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/; // A whole credential can be one opaque string: a show-auth-token / // OneTimeAuthToken value is 47 chars of [A-Za-z0-9_-] (measured live), and the // desktop callback is a `windsurf://…?access_token=…` deep link. Without this // a token-only line was ERR_FORMAT_INVALID, so a pasted token could not be // batch-imported at all (F3/F9). const BARE_TOKEN_RE = /^[A-Za-z0-9_\-\.\$\/+=]{40,}$/; const ABSOLUTE_URL_RE = /^[a-z][a-z0-9+.\-]*:\/\//i; function tokenFromLine(line) { if (BARE_TOKEN_RE.test(line)) return line; if (!ABSOLUTE_URL_RE.test(line)) return ''; try { return unwrapPastedSecret(line); } catch { // A URL with no token (or a proxy URL) is not a credential; the caller // reports the line as ERR_FORMAT_INVALID exactly as before. return ''; } } export class BatchImportParseError extends Error { constructor(code, message = code) { super(message); this.code = code; } } function toTrimmedString(value) { if (value == null) return ''; if (typeof value === 'string') return value.trim(); if (typeof value === 'number' || typeof value === 'boolean') return String(value).trim(); return ''; } function readCaseInsensitive(obj, key) { if (!obj || typeof obj !== 'object' || Array.isArray(obj)) return undefined; const match = Object.keys(obj).find(k => k.toLowerCase() === key.toLowerCase()); return match ? obj[match] : undefined; } function getFirstString(obj, keys) { for (const key of keys) { const direct = toTrimmedString(readCaseInsensitive(obj, key)); if (direct) return direct; } return ''; } function normalizeProxy(rawProxy, parseProxyUrl) { if (!rawProxy) return { proxyRaw: null, proxy: null }; if (typeof rawProxy === 'object' && !Array.isArray(rawProxy)) { const host = toTrimmedString(rawProxy.host); const port = Number.parseInt(rawProxy.port, 10); if (!host || !Number.isInteger(port) || port <= 0) { return { error: 'ERR_PROXY_FORMAT_INVALID' }; } return { proxyRaw: `${toTrimmedString(rawProxy.type || 'http')}://${host}:${port}`, proxy: { type: toTrimmedString(rawProxy.type || 'http'), host, port, username: toTrimmedString(rawProxy.username), password: toTrimmedString(rawProxy.password), }, }; } const proxyRaw = toTrimmedString(rawProxy); if (!proxyRaw) return { proxyRaw: null, proxy: null }; const proxy = parseProxyUrl(proxyRaw); if (!proxy) return { error: 'ERR_PROXY_FORMAT_INVALID' }; return { proxyRaw, proxy }; } function buildIssue({ lineNumber, raw, code, label }) { return { kind: 'issue', success: false, skipped: true, lineNumber, raw, label: label || (lineNumber ? `Line ${lineNumber}` : 'Skipped'), error: code, }; } function buildEntry(base) { return { success: false, skipped: false, lineNumber: base.lineNumber || 0, raw: base.raw || '', proxyRaw: base.proxyRaw || null, proxy: base.proxy || null, label: base.label || '', ...base, }; } function parsePasswordRemainder(remainder) { if (!remainder) return ''; const trimmed = remainder.trimStart(); const label = trimmed.match(/^(?:(?:密码|密碼|口令)\s*[::=]?\s*|(?:password|pass|pwd)\b\s*(?:[::=]\s*|\s+))/i); if (label) return trimmed.slice(label[0].length).trim(); if (/^\s+/.test(remainder)) return remainder.trim(); for (const separator of ['----', '|', ',', ':', ':', '=']) { if (trimmed.startsWith(separator)) return trimmed.slice(separator.length).trim(); } return ''; } function normalizeEmailPrefix(prefix) { const raw = String(prefix || '').trim(); if (!raw) return ''; const withoutLabel = raw .replace(/(?:\s*(?:邮箱|账号|账户|邮件|email|e-mail|mail|username|user)\s*[::=]?)+\s*$/i, '') .trim(); return withoutLabel || ''; } export function parseBatchImportTextLine(rawLine, lineNumber, parseProxyUrl) { const line = String(rawLine || '').trim(); if (!line) return null; const emailMatch = line.match(EMAIL_RE); if (!emailMatch) { const token = tokenFromLine(line); if (token) { return buildEntry({ kind: 'token', lineNumber, raw: rawLine, token, label: '' }); } return buildIssue({ lineNumber, raw: rawLine, code: 'ERR_FORMAT_INVALID' }); } const email = emailMatch[0]; const start = emailMatch.index || 0; const end = start + email.length; const prefix = normalizeEmailPrefix(line.slice(0, start)); const remainder = line.slice(end); let proxyRaw = null; let proxy = null; if (prefix) { const normalizedProxy = normalizeProxy(prefix, parseProxyUrl); if (normalizedProxy.error) { return buildIssue({ lineNumber, raw: rawLine, code: normalizedProxy.error }); } proxyRaw = normalizedProxy.proxyRaw; proxy = normalizedProxy.proxy; } const password = parsePasswordRemainder(remainder); if (!password) { return buildIssue({ lineNumber, raw: rawLine, code: 'ERR_FORMAT_INVALID' }); } return buildEntry({ kind: 'email_password', lineNumber, raw: rawLine, email, password, proxyRaw, proxy, label: email, }); } export function parseBatchImportJsonItem(item, lineNumber, parseProxyUrl) { if (typeof item === 'string') return parseBatchImportTextLine(item, lineNumber, parseProxyUrl); if (!item || typeof item !== 'object' || Array.isArray(item)) { return buildIssue({ lineNumber, raw: JSON.stringify(item), code: 'ERR_UNSUPPORTED_IMPORT_ITEM' }); } const nestedAccount = readCaseInsensitive(item, 'account'); const nestedAccountValue = ( nestedAccount && typeof nestedAccount === 'object' && !Array.isArray(nestedAccount) ) ? nestedAccount : null; const email = getFirstString(item, ['email', 'username', 'account']) || getFirstString(nestedAccountValue, ['email', 'username', 'account']); const password = getFirstString(item, ['password', 'pass']); const token = getFirstString(item, ['token', 'access_token', 'accessToken']); const apiKey = getFirstString(item, ['api_key', 'apiKey', 'key']); const windsurfApiKey = getFirstString(item, ['windsurf_api_key']) || getFirstString(readCaseInsensitive(item, 'windsurf_auth_status_raw'), ['apiKey']); const windsurfAuthToken = getFirstString(item, ['windsurf_auth_token']) || getFirstString(readCaseInsensitive(item, 'windsurf_auth_status_raw'), ['authToken']); const windsurfApiServerUrl = getFirstString(item, ['windsurf_api_server_url']) || getFirstString(readCaseInsensitive(item, 'windsurf_auth_status_raw'), ['apiServerUrl', 'api_server_url']); const exportEmail = getFirstString(item, ['github_email']) || getFirstString(readCaseInsensitive(item, 'windsurf_user_status'), ['email']) || getFirstString(readCaseInsensitive(item, 'windsurf_auth_status_raw'), ['email']); const label = getFirstString(item, ['label', 'name', 'note']) || email; const normalizedProxy = normalizeProxy(readCaseInsensitive(item, 'proxy'), parseProxyUrl); if (normalizedProxy.error) { return buildIssue({ lineNumber, raw: JSON.stringify(item), code: normalizedProxy.error, label }); } if (token) { return buildEntry({ kind: 'token', lineNumber, raw: JSON.stringify(item), token, label, email: email || label || '', proxyRaw: normalizedProxy.proxyRaw, proxy: normalizedProxy.proxy, }); } if (apiKey) { return buildEntry({ kind: 'api_key', lineNumber, raw: JSON.stringify(item), apiKey, label, email: email || label || '', proxyRaw: normalizedProxy.proxyRaw, proxy: normalizedProxy.proxy, }); } if (windsurfApiKey) { return buildEntry({ kind: 'api_key', lineNumber, raw: JSON.stringify(item), apiKey: windsurfApiKey, apiServerUrl: windsurfApiServerUrl || '', label: label || exportEmail, email: exportEmail || label || '', proxyRaw: normalizedProxy.proxyRaw, proxy: normalizedProxy.proxy, }); } if (windsurfAuthToken) { return buildEntry({ kind: 'token', lineNumber, raw: JSON.stringify(item), token: windsurfAuthToken, apiServerUrl: windsurfApiServerUrl || '', label: label || exportEmail, email: exportEmail || label || '', proxyRaw: normalizedProxy.proxyRaw, proxy: normalizedProxy.proxy, }); } if (email && password) { return buildEntry({ kind: 'email_password', lineNumber, raw: JSON.stringify(item), email, password, label, proxyRaw: normalizedProxy.proxyRaw, proxy: normalizedProxy.proxy, }); } return buildIssue({ lineNumber, raw: JSON.stringify(item), code: 'ERR_UNSUPPORTED_IMPORT_ITEM', label, }); } function normalizeJsonContainer(parsed) { if (Array.isArray(parsed)) return parsed; if (!parsed || typeof parsed !== 'object') { return [parsed]; } const accounts = readCaseInsensitive(parsed, 'accounts'); if (Array.isArray(accounts)) return accounts; const items = readCaseInsensitive(parsed, 'items'); if (Array.isArray(items)) return items; return [parsed]; } export function parseBatchImportInput(text, parseProxyUrl) { const source = String(text || ''); if (!source.trim()) { return { mode: 'empty', items: [] }; } const trimmed = source.trim(); if (trimmed.startsWith('{') || trimmed.startsWith('[')) { let parsed; try { parsed = JSON.parse(trimmed); } catch { throw new BatchImportParseError('ERR_JSON_INVALID'); } const items = normalizeJsonContainer(parsed) .map((item, index) => parseBatchImportJsonItem(item, index + 1, parseProxyUrl)) .filter(Boolean); return { mode: 'json', items }; } const items = source .replace(/\r\n?/g, '\n') .split('\n') .map((line, index) => parseBatchImportTextLine(line, index + 1, parseProxyUrl)) .filter(Boolean); return { mode: 'text', items }; }