import type { CiphersImportPayload } from '@/lib/api/vault'; import { addFolder, cardBrand, type CsvRow, convertToNoteIfNeeded, makeLoginCipher, normalizeUri, parseCsv, parseCsvRows, processKvp, txt, val, } from '@/lib/import-format-shared'; function splitPipedField(raw: string): string { const s = txt(raw); if (!s) return ''; const p = s.split('|'); if (p.length <= 2) return s; return [...p.slice(0, 2), p.slice(2).join('|')].pop() || ''; } export function parseMSecureCsv(textRaw: string): CiphersImportPayload { const rows = parseCsvRows(textRaw); const result: CiphersImportPayload = { ciphers: [], folders: [], folderRelationships: [] }; for (const row of rows) { if (row.length < 3) continue; const folderName = txt(row[2]) && txt(row[2]) !== 'Unassigned' ? row[2] : ''; const type = txt(row[1]); const cipher = makeLoginCipher(); cipher.name = val(txt(row[0]).split('|')[0], '--'); if (type === 'Web Logins' || type === 'Login') { const login = cipher.login as Record; login.username = val(splitPipedField(row[5] || '')); login.password = val(splitPipedField(row[6] || '')); const uri = normalizeUri(splitPipedField(row[4] || '') || ''); login.uris = uri ? [{ uri, match: null }] : null; cipher.notes = val((row[3] || '').split('\\n').join('\n')); } else if (type === 'Credit Card') { cipher.type = 3; cipher.login = null; const cardNumber = val(splitPipedField(row[4] || '')); let expMonth: string | null = null; let expYear: string | null = null; const exp = splitPipedField(row[5] || ''); const m = exp.match(/^(\d{1,2})\s*\/\s*(\d{2,4})$/); if (m) { expMonth = m[1]; expYear = m[2].length === 2 ? `20${m[2]}` : m[2]; } let code: string | null = null; let holder: string | null = null; for (const entry of row) { if (/^Security Code\|\d*\|/.test(entry)) code = val(splitPipedField(entry)); if (/^Name on Card\|\d*\|/.test(entry)) holder = val(splitPipedField(entry)); } const noteRegex = /\|\d*\|/; const rawNotes = row.slice(2).filter((entry) => txt(entry) && !noteRegex.test(entry)); const indexedNotes = [8, 10, 11] .filter((idx) => row[idx] && noteRegex.test(row[idx])) .map((idx) => `${txt(row[idx]).split('|')[0]}: ${splitPipedField(row[idx])}`); cipher.notes = [...rawNotes, ...indexedNotes].join('\n') || null; cipher.card = { number: cardNumber, cardholderName: holder, code, expMonth, expYear, brand: cardBrand(cardNumber), }; } else if (row.length > 3) { cipher.type = 2; cipher.login = null; cipher.secureNote = { type: 0 }; const noteLines: string[] = []; for (let i = 3; i < row.length; i++) { if (txt(row[i])) noteLines.push(row[i]); } cipher.notes = noteLines.join('\n') || null; } if (txt(type) && Number(cipher.type) !== 1 && Number(cipher.type) !== 3) { cipher.name = `${type}: ${txt(cipher.name)}`; } const idx = result.ciphers.push(cipher) - 1; addFolder(result, folderName, idx); } return result; } export function parseMykiCsv(textRaw: string): CiphersImportPayload { const rows = parseCsv(textRaw); const result: CiphersImportPayload = { ciphers: [], folders: [], folderRelationships: [] }; const mappedBase = new Set(['nickname', 'additionalInfo']); function unmapped(cipher: Record, row: CsvRow, mapped: Set): void { for (const key of Object.keys(row)) { if (mapped.has(key)) continue; processKvp(cipher, key, row[key], false); } } for (const row of rows) { const cipher = makeLoginCipher(); cipher.name = val(row.nickname, '--'); cipher.notes = val(txt(row.additionalInfo).replace(/\s+$/g, '')); if (row.url !== undefined) { const mapped = new Set([...mappedBase, 'url', 'username', 'password', 'twofaSecret']); const login = cipher.login as Record; const uri = normalizeUri(row.url || ''); login.uris = uri ? [{ uri, match: null }] : null; login.username = val(row.username); login.password = val(row.password); login.totp = val(row.twofaSecret); unmapped(cipher, row, mapped); } else if (row.authToken !== undefined) { const mapped = new Set([...mappedBase, 'authToken']); (cipher.login as Record).totp = val(row.authToken); unmapped(cipher, row, mapped); } else if (row.cardNumber !== undefined) { const mapped = new Set([...mappedBase, 'cardNumber', 'cardName', 'exp_month', 'exp_year', 'cvv']); cipher.type = 3; cipher.login = null; cipher.card = { cardholderName: val(row.cardName), number: val(row.cardNumber), brand: cardBrand(val(row.cardNumber)), expMonth: val(row.exp_month), expYear: val(row.exp_year), code: val(row.cvv), }; unmapped(cipher, row, mapped); } else if (row.firstName !== undefined) { const mapped = new Set([ ...mappedBase, 'title', 'firstName', 'middleName', 'lastName', 'email', 'firstAddressLine', 'secondAddressLine', 'city', 'country', 'zipCode', ]); cipher.type = 4; cipher.login = null; cipher.identity = { title: val(row.title), firstName: val(row.firstName), middleName: val(row.middleName), lastName: val(row.lastName), phone: val((row as Record).number), email: val(row.email), address1: val(row.firstAddressLine), address2: val(row.secondAddressLine), city: val(row.city), country: val(row.country), postalCode: val(row.zipCode), }; unmapped(cipher, row, mapped); } else if (row.idType !== undefined) { const mapped = new Set([...mappedBase, 'idName', 'idNumber', 'idCountry']); const fullName = txt((row as Record).idName); const parts = fullName.split(/\s+/).filter(Boolean); const idType = txt((row as Record).idType); const idNumber = val((row as Record).idNumber); cipher.type = 4; cipher.login = null; cipher.identity = { firstName: parts[0] || null, middleName: parts.length >= 3 ? parts[1] : null, lastName: parts.length >= 2 ? parts.slice(parts.length >= 3 ? 2 : 1).join(' ') : null, country: val((row as Record).idCountry), passportNumber: idType === 'Passport' ? idNumber : null, ssn: idType === 'Social Security' ? idNumber : null, licenseNumber: idType !== 'Passport' && idType !== 'Social Security' ? idNumber : null, }; unmapped(cipher, row, mapped); } else if (row.content !== undefined) { const mapped = new Set([...mappedBase, 'content']); cipher.type = 2; cipher.login = null; cipher.secureNote = { type: 0 }; cipher.notes = val(txt(row.content).replace(/\s+$/g, '')); unmapped(cipher, row, mapped); } else { continue; } result.ciphers.push(cipher); } return result; } export function parseNetwrixCsv(textRaw: string): CiphersImportPayload { const rows = parseCsv(textRaw); const result: CiphersImportPayload = { ciphers: [], folders: [], folderRelationships: [] }; const mapped = new Set(['Organisationseinheit', 'Informationen', 'Beschreibung', 'Benutzername', 'Passwort', 'Internetseite', 'One-Time Passwort']); for (const row of rows) { const cipher = makeLoginCipher(); cipher.notes = val(txt(row.Informationen).replace(/\s+$/g, '')); cipher.name = val(row.Beschreibung, '--'); const login = cipher.login as Record; login.username = val(row.Benutzername); login.password = val(row.Passwort); login.totp = val((row as Record)['One-Time Passwort']); const uri = normalizeUri(row.Internetseite || ''); login.uris = uri ? [{ uri, match: null }] : null; for (const key of Object.keys(row)) { if (mapped.has(key)) continue; processKvp(cipher, key, row[key], false); } const idx = result.ciphers.push(cipher) - 1; addFolder(result, row.Organisationseinheit, idx); } return result; } export function parseRoboFormCsv(textRaw: string): CiphersImportPayload { const rows = parseCsv(textRaw); const result: CiphersImportPayload = { ciphers: [], folders: [], folderRelationships: [] }; for (const row of rows) { const cipher = makeLoginCipher(); const folder = txt(row.Folder).startsWith('/') ? txt(row.Folder).slice(1) : txt(row.Folder); cipher.notes = val(row.Note); cipher.name = val(row.Name, '--'); const login = cipher.login as Record; login.username = val(row.Login); login.password = val(row.Pwd, val(row.Password)); const uri = normalizeUri(row.Url || row.URL || ''); login.uris = uri ? [{ uri, match: null }] : null; if (txt(row.Rf_fields)) processKvp(cipher, 'Rf_fields', txt(row.Rf_fields), true); if (txt(row.RfFieldsV2)) processKvp(cipher, 'RfFieldsV2', txt(row.RfFieldsV2), true); convertToNoteIfNeeded(cipher); const idx = result.ciphers.push(cipher) - 1; addFolder(result, folder, idx); } return result; } export function parseZohoVaultCsv(textRaw: string): CiphersImportPayload { const rows = parseCsv(textRaw); const result: CiphersImportPayload = { ciphers: [], folders: [], folderRelationships: [] }; for (const row of rows) { if (!txt(row['Password Name']) && !txt(row['Secret Name'])) continue; const cipher = makeLoginCipher(); cipher.favorite = txt(row.Favorite) === '1'; cipher.notes = val(row.Notes); cipher.name = val(row['Password Name'], val(row['Secret Name'], '--')); const login = cipher.login as Record; const uri = normalizeUri(txt(row['Password URL']) || txt(row['Secret URL'])); login.uris = uri ? [{ uri, match: null }] : null; login.totp = val(row.login_totp); const parseData = (data: string) => { if (!txt(data)) return; for (const line of data.split(/\r?\n/)) { const pos = line.indexOf(':'); if (pos < 0) continue; const key = txt(line.slice(0, pos)); const value = txt(line.slice(pos + 1)); if (!key || !value || key === 'SecretType') continue; const low = key.toLowerCase(); if (!txt(login.username) && ['username', 'user', 'email', 'login', 'id'].includes(low)) login.username = value; else if (!txt(login.password) && ['password', 'pass', 'passwd'].includes(low)) login.password = value; else processKvp(cipher, key, value, false); } }; parseData(txt(row.SecretData)); parseData(txt(row.CustomData)); convertToNoteIfNeeded(cipher); const idx = result.ciphers.push(cipher) - 1; addFolder(result, row['Folder Name'], idx); } return result; } export function parseNordpassCsv(textRaw: string): CiphersImportPayload { const rows = parseCsv(textRaw); const result: CiphersImportPayload = { ciphers: [], folders: [], folderRelationships: [] }; for (const r of rows) { const t = txt(r.type); if (!t) continue; if (t === 'password') { const cipher = makeLoginCipher(); cipher.name = val(r.name, '--'); cipher.notes = val(r.note); const login = cipher.login as Record; login.username = val(r.username); login.password = val(r.password); const uris: string[] = []; const main = normalizeUri(r.url || ''); if (main) uris.push(main); if (txt(r.additional_urls)) { try { const extra = JSON.parse(r.additional_urls) as string[]; for (const u of extra || []) { const n = normalizeUri(u || ''); if (n) uris.push(n); } } catch {} } login.uris = uris.length ? uris.map((u) => ({ uri: u, match: null })) : null; if (txt(r.custom_fields)) { try { const cfs = JSON.parse(r.custom_fields) as any[]; for (const cf of cfs || []) processKvp(cipher, cf.label || '', cf.value || '', cf.type === 'hidden'); } catch {} } const idx = result.ciphers.push(cipher) - 1; addFolder(result, r.folder, idx); continue; } if (t === 'note') { const idx = result.ciphers.push({ type: 2, name: val(r.name, '--'), notes: val(r.note), favorite: false, reprompt: 0, key: null, login: null, card: null, identity: null, secureNote: { type: 0 }, fields: null, passwordHistory: null, sshKey: null, }) - 1; addFolder(result, r.folder, idx); continue; } if (t === 'credit_card') { const idx = result.ciphers.push({ type: 3, name: val(r.name, '--'), notes: val(r.note), favorite: false, reprompt: 0, key: null, login: null, card: { cardholderName: val(r.cardholdername), number: val(r.cardnumber), brand: cardBrand(val(r.cardnumber)), code: val(r.cvc), expMonth: val(r.expiry_month), expYear: val(r.expiry_year), }, identity: null, secureNote: null, fields: null, passwordHistory: null, sshKey: null, }) - 1; addFolder(result, r.folder, idx); continue; } if (t === 'personal_info') { const identity = { title: val(r.title), firstName: val(r.first_name), middleName: val(r.middle_name), lastName: val(r.last_name), phone: val(r.phone_number), email: val(r.email), address1: val(r.address1), address2: val(r.address2), city: val(r.city), state: val(r.state), postalCode: val(r.postal_code), country: val(r.country), username: val(r.username), company: val(r.company), }; const idx = result.ciphers.push({ type: 4, name: val(r.name, '--'), notes: val(r.note), favorite: false, reprompt: 0, key: null, login: null, card: null, identity, secureNote: null, fields: null, passwordHistory: null, sshKey: null, }) - 1; addFolder(result, r.folder, idx); } } return result; } export function parsePassmanJson(textRaw: string): CiphersImportPayload { const rows = JSON.parse(textRaw) as any[]; const result: CiphersImportPayload = { ciphers: [], folders: [], folderRelationships: [] }; for (const c of rows || []) { const cipher = makeLoginCipher(); cipher.name = val(c.label, '--'); const login = cipher.login as Record; login.username = val(c.username, val(c.email)); login.password = val(c.password); const uri = normalizeUri(c.url || ''); login.uris = uri ? [{ uri, match: null }] : null; login.totp = val(c?.otp?.secret); const email = txt(c.email); const desc = txt(c.description); cipher.notes = `${login.username && email && txt(login.username) !== email ? `Email: ${email}\n` : ''}${desc}` || null; for (const cf of c.custom_fields || []) { const t = txt(cf.field_type); if (t === 'text' || t === 'password') processKvp(cipher, cf.label || '', cf.value || '', false); } const idx = result.ciphers.push(cipher) - 1; const folder = c?.tags?.[0]?.text; if (folder) addFolder(result, String(folder), idx); } return result; } export function parsePasskyJson(textRaw: string): CiphersImportPayload { const parsed = JSON.parse(textRaw) as { encrypted?: boolean; passwords?: any[] }; if (parsed.encrypted === true) throw new Error('Unable to import an encrypted passky backup.'); const list = Array.isArray(parsed.passwords) ? parsed.passwords : []; const result: CiphersImportPayload = { ciphers: [], folders: [], folderRelationships: [] }; for (const p of list) { const cipher = makeLoginCipher(); cipher.name = val(p.website, '--'); const login = cipher.login as Record; login.username = val(p.username); login.password = val(p.password); const uri = normalizeUri(String(p.website || '')); login.uris = uri ? [{ uri, match: null }] : null; cipher.notes = val(p.message); result.ciphers.push(cipher); } return result; } export function parsePsonoJson(textRaw: string): CiphersImportPayload { const parsed = JSON.parse(textRaw) as any; const result: CiphersImportPayload = { ciphers: [], folders: [], folderRelationships: [] }; function parseItem(item: any, folderName: string | null) { if (!item || typeof item !== 'object') return; const type = txt(item.type); const cipher = makeLoginCipher(); if (type === 'website_password') { cipher.name = val(item.website_password_title, '--'); cipher.notes = val(item.website_password_notes); const login = cipher.login as Record; login.username = val(item.website_password_username); login.password = val(item.website_password_password); const uri = normalizeUri(item.website_password_url || ''); login.uris = uri ? [{ uri, match: null }] : null; const idx = result.ciphers.push(cipher) - 1; if (folderName) addFolder(result, folderName, idx); return; } if (type === 'application_password') { cipher.name = val(item.application_password_title, '--'); cipher.notes = val(item.application_password_notes); const login = cipher.login as Record; login.username = val(item.application_password_username); login.password = val(item.application_password_password); const idx = result.ciphers.push(cipher) - 1; if (folderName) addFolder(result, folderName, idx); return; } if (type === 'totp') { cipher.name = val(item.totp_title, '--'); cipher.notes = val(item.totp_notes); (cipher.login as Record).totp = val(item.totp_code); const idx = result.ciphers.push(cipher) - 1; if (folderName) addFolder(result, folderName, idx); return; } if (type === 'bookmark') { cipher.name = val(item.bookmark_title, '--'); cipher.notes = val(item.bookmark_notes); const uri = normalizeUri(item.bookmark_url || ''); (cipher.login as Record).uris = uri ? [{ uri, match: null }] : null; const idx = result.ciphers.push(cipher) - 1; if (folderName) addFolder(result, folderName, idx); return; } if (type === 'note' || type === 'environment_variables') { const secure = { type: 2, name: val(type === 'note' ? item.note_title : item.environment_variables_title, '--'), notes: val(type === 'note' ? item.note_notes : item.environment_variables_notes), favorite: false, reprompt: 0, key: null, login: null, card: null, identity: null, secureNote: { type: 0 }, fields: null, passwordHistory: null, sshKey: null, } as Record; const idx = result.ciphers.push(secure) - 1; if (folderName) addFolder(result, folderName, idx); } } function walkFolders(folders: any[], parent: string | null) { for (const f of folders || []) { const name = parent ? `${parent}/${txt(f.name)}` : txt(f.name); for (const item of f.items || []) parseItem(item, name); if (Array.isArray(f.folders)) walkFolders(f.folders, name); } } for (const item of parsed.items || []) parseItem(item, null); walkFolders(parsed.folders || [], null); return result; } export function parsePasswordBossJson(textRaw: string): CiphersImportPayload { const parsed = JSON.parse(textRaw) as { folders?: any[]; items?: any[] }; const result: CiphersImportPayload = { ciphers: [], folders: [], folderRelationships: [] }; const folderNameById = new Map(); for (const f of parsed.folders || []) { if (f?.id && f?.name) folderNameById.set(String(f.id), String(f.name)); } for (const item of parsed.items || []) { const ids = item?.identifiers || {}; const isCard = txt(item?.type) === 'CreditCard'; const base = isCard ? { type: 3, name: val(item?.name, '--'), notes: val(ids.notes), favorite: false, reprompt: 0, key: null, login: null, card: { number: val(ids.cardNumber), cardholderName: val(ids.nameOnCard), code: val(ids.security_code), brand: cardBrand(val(ids.cardNumber)), expMonth: null, expYear: null, }, identity: null, secureNote: null, fields: [], passwordHistory: null, sshKey: null, } : makeLoginCipher(); if (!isCard) { base.name = val(item?.name, '--'); base.notes = val(ids.notes); const login = base.login as Record; login.username = val(ids.username, val(ids.email)); login.password = val(ids.password); login.totp = val(ids.totp); const uri = normalizeUri(item?.login_url || ids.url || ''); login.uris = uri ? [{ uri, match: null }] : null; } if (Array.isArray(ids.custom_fields)) { for (const cf of ids.custom_fields) processKvp(base as Record, cf?.name || '', cf?.value || '', false); } const idx = result.ciphers.push(base as Record) - 1; const folderId = item?.folder; if (folderId && folderNameById.has(String(folderId))) addFolder(result, folderNameById.get(String(folderId)) || '', idx); } return result; }