@ai-matrx/vault 0.1.13 → 0.1.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../kit/src/delimited.ts","../../src/io/csv-import.ts","../../src/io/csv-import.worker.ts"],"sourcesContent":["/**\n * @ai-matrx/kit/delimited — THE one CSV/TSV writer for every Matrx package and app.\n *\n * WHY HERE. Every package that writes delimited text depends on kit; most sit BELOW\n * `@ai-matrx/alchemy` (design-system, records, meet) and cannot import it. So the one cell\n * encoder lives at the bottom of the graph, and Alchemy's `serialize` / `toDelimitedText` /\n * `buildFile` call it (decision AP-2, recorded in alchemy FEATURE.md). A second hand-rolled\n * `.replace(/\"/g, '\"\"')` writer anywhere is a defect (`check-shared-doors.mjs` `handcsv`).\n *\n * THE ONE spreadsheetSafe DEFAULT. Output meant for a spreadsheet — the csv and tsv\n * formats — is formula-guarded unless the caller passes `spreadsheetSafe: false` with a reason\n * (a lossless clipboard round-trip, text an AI reads). Every entry point resolves its default\n * through `spreadsheetSafeByDefault(format)`; none restates the rule.\n *\n * THE GUARD (OWASP \"CSV Injection\"): a TEXT cell is prefixed with `'` when it starts with\n * `=` `+` `-` `@`, a tab or a carriage return — and also when leading spaces/line breaks precede\n * `=` `+` `-` `@` (spreadsheets trim them before deciding a cell is a formula). A real number\n * (`-5` passed as a number) is never altered, and neither is text that is exactly a signed number\n * (`\"-5\"`, `\"+3.2\"`, `\"-1e3\"`): a spreadsheet reads it as that number, never as a formula.\n * An .xlsx workbook is NOT in the set: its text cells are typed strings a spreadsheet never\n * evaluates, so guarding them would only corrupt the value (decision AP-2, alchemy FEATURE.md).\n *\n * THE ONE READER sits beside the one writer: `parseDelimited` (Papa Parse–compatible in the options\n * the platform uses) is here, below every package that reads CSV/TSV (records, records-ui,\n * rich-content, the apps); Alchemy's `/operate/read` re-exports it (decision AP-2). A second\n * char-loop quote reader anywhere is a defect (`check-shared-doors.mjs` `handcsv`).\n * THE GUARD IS REVERSIBLE AND OUR READER REVERSES IT (coordinator ruling, AP-2 A1/A7):\n * `parseDelimited` strips the guard's `'` by default (`unguardFormulas: false` keeps raw text), so\n * our own grid paste and importers get the original value back while every export stays guarded.\n * The writer also prefixes text that already LOOKS guarded (`'=x` → `''=x`), so write → read is\n * exact for every string.\n * Pure; no DOM, no dependencies.\n */\n\nexport type DelimitedFormat = \"csv\" | \"tsv\";\n\n/** One cell. Strings are text (guarded); numbers, booleans and bigints are values (never guarded). */\nexport type DelimitedCell = string | number | boolean | bigint | null | undefined;\n\n/** The delimited formats a spreadsheet opens as text it may evaluate — formula-guarded by default. */\nexport const SPREADSHEET_SAFE_FORMATS: readonly string[] = [\"csv\", \"tsv\"];\n\n/** THE default for `spreadsheetSafe`: on for csv and tsv; off for every other format. */\nexport function spreadsheetSafeByDefault(format: string): boolean {\n return SPREADSHEET_SAFE_FORMATS.includes(format);\n}\n\nconst FORMULA_LEAD = /^(?:[\\t\\r]|[\\t\\r\\n ]*[=+\\-@])/;\n\n/** Text that is exactly one signed decimal number — a spreadsheet reads it as that number. */\nconst SIGNED_NUMBER = /^[+-](?:\\d+(?:\\.\\d*)?|\\.\\d+)(?:[eE][+-]?\\d+)?$/;\n\n/**\n * True when a spreadsheet could read this text as a formula (the OWASP CSV-injection set). A\n * signed number written as text (`\"-5\"`, `\"+3.2\"`) is not: it can only ever be that number.\n */\nexport function isFormulaLike(text: string): boolean {\n return FORMULA_LEAD.test(text) && !SIGNED_NUMBER.test(text);\n}\n\n/**\n * True when this text is a GUARDED cell: a leading `'` whose remainder the guard would have\n * prefixed (formula-like, or itself a guarded cell). This is exactly the set `parseDelimited`\n * unguards, and the writer prefixes it too, so write → read is exact for every string:\n * `\"+1 949 807 2145\"` → `\"'+1 949 807 2145\"` → `\"+1 949 807 2145\"`; `\"'tis\"` is never touched.\n */\nexport function isGuardedCell(text: string): boolean {\n let i = 0;\n while (text[i] === \"'\") {\n if (isFormulaLike(text.slice(i + 1))) return true;\n i++;\n }\n return false;\n}\n\n/** Reverse the formula guard: `'=SUM(A1)` → `=SUM(A1)`; anything not guarded comes back as-is. */\nexport function unguardDelimitedCell(text: string): string {\n return isGuardedCell(text) ? text.slice(1) : text;\n}\n\nexport interface DelimitedCellOptions {\n /** Which format the cell is for; picks the default delimiter and guard. Defaults to csv. */\n format?: DelimitedFormat;\n /** Defaults to the format's (`,` for csv, a tab for tsv). */\n delimiter?: string;\n /** Defaults to `spreadsheetSafeByDefault(format)`. */\n spreadsheetSafe?: boolean;\n}\n\n/** One cell as delimited text; `escaped` says the formula guard fired. */\nexport function encodeDelimitedCell(\n cell: DelimitedCell,\n options: DelimitedCellOptions = {},\n): { text: string; escaped: boolean } {\n const format = options.format ?? \"csv\";\n const delimiter = options.delimiter ?? (format === \"tsv\" ? \"\\t\" : \",\");\n const spreadsheetSafe = options.spreadsheetSafe ?? spreadsheetSafeByDefault(format);\n if (cell === null || cell === undefined) return { text: \"\", escaped: false };\n let text = typeof cell === \"string\" ? cell : String(cell);\n let escaped = false;\n if (typeof cell === \"string\" && spreadsheetSafe && (isFormulaLike(text) || isGuardedCell(text))) {\n text = `'${text}`;\n escaped = true;\n }\n if (text.includes(delimiter) || /[\"\\n\\r]/.test(text)) text = `\"${text.replace(/\"/g, '\"\"')}\"`;\n return { text, escaped };\n}\n\nexport interface DelimitedOptions {\n /** `csv` (comma) or `tsv` (tab). Defaults to csv. */\n format?: DelimitedFormat;\n /** Overrides the format's delimiter (e.g. `;` or `|` for a block that declares one). */\n delimiter?: string;\n /** Defaults to `spreadsheetSafeByDefault(format)` — on. */\n spreadsheetSafe?: boolean;\n /** Between records. Defaults to `\\n` (Excel, Sheets and every parser here accept it). */\n lineEnding?: \"\\n\" | \"\\r\\n\";\n /** End the text with one line ending. Defaults to false. */\n trailingNewline?: boolean;\n /** Called once per guarded cell (row 0 is the first row passed, usually the header). */\n onEscaped?: (row: number, column: number) => void;\n}\n\n/** Rows of cells as CSV/TSV text — the one writer. */\nexport function toDelimited(\n rows: readonly (readonly DelimitedCell[])[],\n options: DelimitedOptions = {},\n): string {\n const format = options.format ?? \"csv\";\n const delimiter = options.delimiter ?? (format === \"tsv\" ? \"\\t\" : \",\");\n const spreadsheetSafe = options.spreadsheetSafe ?? spreadsheetSafeByDefault(format);\n const lineEnding = options.lineEnding ?? \"\\n\";\n const text = rows\n .map((row, r) =>\n row\n .map((cell, c) => {\n const out = encodeDelimitedCell(cell, { format, delimiter, spreadsheetSafe });\n if (out.escaped) options.onEscaped?.(r, c);\n return out.text;\n })\n .join(delimiter),\n )\n .join(lineEnding);\n // One row holding one empty cell would write as nothing, and nothing reads back as NO rows: quote\n // that lone empty field (`\"\"`) so `[[\"\"]]` reads back as one row with one empty cell.\n const lone = rows.length === 1 && rows[0]!.length === 1 && text === \"\" ? '\"\"' : text;\n return options.trailingNewline && rows.length > 0 ? `${lone}${lineEnding}` : lone;\n}\n\n// ─── Read: THE one CSV/TSV parser (moved from @ai-matrx/alchemy/operate/read, AP-2) ──────────────\n\nexport type DelimitedParseError = {\n type: \"Quotes\" | \"Delimiter\" | \"FieldMismatch\";\n code: \"MissingQuotes\" | \"InvalidQuotes\" | \"UndetectableDelimiter\" | \"TooFewFields\" | \"TooManyFields\";\n message: string;\n /** 0-based index of the data row the error belongs to (absent for whole-file errors). */\n row?: number;\n};\n\nexport type DelimitedParseOptions = {\n /** Field delimiter. Absent → guessed from `,` `\\t` `|` `;` and the ASCII record/unit separators; `,` when none fits. */\n delimiter?: string;\n /** true: an exactly empty line is dropped; \"greedy\": a line of only whitespace is dropped too. */\n skipEmptyLines?: boolean | \"greedy\";\n /** Numbers, `true`/`false` and ISO date-times become typed values; an empty field becomes null. */\n dynamicTyping?: boolean;\n /**\n * Reverse the writer's formula guard (default true): a field that is `'` + text the guard would\n * have prefixed (`'=SUM(A1)`, `'+1 949 807 2145`, `'-`, `'@home`, `' =x`) loses that one `'`.\n * Every reader of ours reads text our writer may have guarded, so this is on unless a caller\n * needs the raw bytes (`false`). A genuine leading apostrophe (`'tis`) is never touched.\n */\n unguardFormulas?: boolean;\n /**\n * A field that opens with `\"` but is not a well-formed quoted field: `\"quoted\"` (default, Papa\n * Parse's behaviour) keeps reading it as quoted; `\"literal\"` reads the quote as text and ends\n * the field at the next delimiter (`5\" screen`) — a spreadsheet's paste reading. Both report it.\n */\n strayQuotes?: \"quoted\" | \"literal\";\n /** Header mode only: rename a header (e.g. trim + lowercase). */\n transformHeader?(header: string, index: number): string;\n};\n\n/** A parsed field under `dynamicTyping` (the writer's `ParsedDelimitedCell` is the input side). */\nexport type ParsedDelimitedCell = string | number | boolean | Date | null;\nexport type DelimitedMeta = { delimiter: string; linebreak: string; fields?: string[] };\nexport type DelimitedResult<T> = { data: T[]; errors: DelimitedParseError[]; meta: DelimitedMeta };\n\nconst RECORD_SEP = String.fromCharCode(30);\nconst UNIT_SEP = String.fromCharCode(31);\nconst GUESS_DELIMITERS = [\",\", \"\\t\", \"|\", \";\", RECORD_SEP, UNIT_SEP];\nconst FLOAT = /^\\s*-?(\\d+\\.?|\\.\\d+|\\d+\\.\\d+)([eE][-+]?\\d+)?\\s*$/;\nconst ISO_DATE =\n /^((\\d{4}-[01]\\d-[0-3]\\dT[0-2]\\d:[0-5]\\d:[0-5]\\d\\.\\d+([+-][0-2]\\d:[0-5]\\d|Z))|(\\d{4}-[01]\\d-[0-3]\\dT[0-2]\\d:[0-5]\\d:[0-5]\\d([+-][0-2]\\d:[0-5]\\d|Z))|(\\d{4}-[01]\\d-[0-3]\\dT[0-2]\\d:[0-5]\\d([+-][0-2]\\d:[0-5]\\d|Z)))$/;\n\nfunction guessLinebreak(rawText: string): string {\n // Quoted cells may hold any line break; only the ones between records say what the file uses.\n const text = rawText.includes('\"') ? rawText.replace(/\"[^\"]*\"/g, '\"\"') : rawText;\n const crlf = text.indexOf(\"\\r\\n\");\n const lf = text.indexOf(\"\\n\");\n const cr = text.indexOf(\"\\r\");\n if (crlf !== -1 && crlf === cr) return \"\\r\\n\";\n if (cr !== -1 && (lf === -1 || cr < lf)) return \"\\r\";\n return \"\\n\";\n}\n\n/** Tokenize into rows of raw string fields; quote errors are reported against their data row. */\nfunction tokenize(\n text: string,\n delimiter: string,\n linebreak: string,\n preview = Infinity,\n strayQuotes: \"quoted\" | \"literal\" = \"quoted\",\n): { rows: string[][]; errors: DelimitedParseError[] } {\n const rows: string[][] = [];\n const errors: DelimitedParseError[] = [];\n if (text === \"\") return { rows, errors };\n let row: string[] = [];\n let i = 0;\n const n = text.length;\n const dl = delimiter.length;\n const ll = linebreak.length;\n while (i <= n && rows.length < preview) {\n if (text.startsWith('\"', i)) {\n // Quoted field: \"\" is a literal quote; the closing quote must precede a delimiter, a line break or the end.\n let value = \"\";\n let j = i + 1;\n let closed = false;\n let malformed = false;\n while (j < n) {\n const q = text.indexOf('\"', j);\n if (q === -1) break;\n value += text.slice(j, q);\n if (text[q + 1] === '\"') {\n value += '\"';\n j = q + 2;\n continue;\n }\n const after = q + 1;\n if (after >= n || text.startsWith(delimiter, after) || text.startsWith(linebreak, after)) {\n closed = true;\n j = after;\n break;\n }\n errors.push({ type: \"Quotes\", code: \"InvalidQuotes\", message: \"Trailing quote on quoted field is malformed\", row: rows.length });\n malformed = true;\n if (strayQuotes === \"literal\") break;\n value += '\"';\n j = after;\n }\n if (!closed && !malformed)\n errors.push({ type: \"Quotes\", code: \"MissingQuotes\", message: \"Quoted field unterminated\", row: rows.length });\n if (strayQuotes === \"quoted\" || closed) {\n if (!closed) {\n value += text.slice(j);\n row.push(value);\n rows.push(row);\n return { rows, errors };\n }\n row.push(value);\n i = j;\n if (i >= n) {\n rows.push(row);\n break;\n }\n if (text.startsWith(delimiter, i)) {\n i += dl;\n if (i >= n) {\n row.push(\"\");\n rows.push(row);\n break;\n }\n continue;\n }\n rows.push(row);\n row = [];\n i += ll;\n if (i >= n) {\n rows.push([\"\"]);\n break;\n }\n continue;\n }\n // strayQuotes \"literal\": a field that merely STARTS with a quote (`5\" screen`) is text,\n // ending at the next delimiter or line break — how a spreadsheet reads a pasted block.\n }\n const nextDelim = text.indexOf(delimiter, i);\n const nextBreak = text.indexOf(linebreak, i);\n if (nextDelim !== -1 && (nextBreak === -1 || nextDelim < nextBreak)) {\n row.push(text.slice(i, nextDelim));\n i = nextDelim + dl;\n if (i >= n) {\n row.push(\"\");\n rows.push(row);\n break;\n }\n continue;\n }\n if (nextBreak !== -1) {\n row.push(text.slice(i, nextBreak));\n rows.push(row);\n row = [];\n i = nextBreak + ll;\n if (i >= n) {\n rows.push([\"\"]);\n break;\n }\n continue;\n }\n row.push(text.slice(i));\n rows.push(row);\n break;\n }\n return { rows, errors };\n}\n\nconst isEmptyRow = (row: readonly string[], mode: boolean | \"greedy\") =>\n mode === \"greedy\" ? row.join(\"\").trim() === \"\" : row.length === 1 && row[0]!.length === 0;\n\nfunction guessDelimiter(text: string, linebreak: string, skip: boolean | \"greedy\" | undefined): { delimiter: string; successful: boolean } {\n let best: string | undefined;\n let bestDelta: number | undefined;\n let bestAvg: number | undefined;\n for (const delim of GUESS_DELIMITERS) {\n const { rows } = tokenize(text, delim, linebreak, 10);\n let delta = 0;\n let avg = 0;\n let empty = 0;\n let prev: number | undefined;\n for (const row of rows) {\n if (skip && isEmptyRow(row, skip)) {\n empty++;\n continue;\n }\n const count = row.length;\n avg += count;\n if (prev === undefined) {\n prev = count;\n continue;\n }\n if (count > 0) {\n delta += Math.abs(count - prev);\n prev = count;\n }\n }\n if (rows.length > 0) avg /= rows.length - empty;\n if ((bestDelta === undefined || delta <= bestDelta) && (bestAvg === undefined || avg > bestAvg) && avg > 1.99) {\n bestDelta = delta;\n bestAvg = avg;\n best = delim;\n }\n }\n return { delimiter: best ?? \",\", successful: best !== undefined };\n}\n\nfunction typed(value: string): ParsedDelimitedCell {\n if (value === \"true\" || value === \"TRUE\") return true;\n if (value === \"false\" || value === \"FALSE\") return false;\n if (FLOAT.test(value)) {\n const num = parseFloat(value);\n if (num > -(2 ** 53) && num < 2 ** 53) return num;\n return value;\n }\n if (ISO_DATE.test(value)) return new Date(value);\n return value === \"\" ? null : value;\n}\n\n/** Parse delimited text into rows of fields (header row first). */\nexport function parseDelimited(text: string, options?: DelimitedParseOptions & { header?: false; dynamicTyping?: false }): DelimitedResult<string[]>;\nexport function parseDelimited(text: string, options: DelimitedParseOptions & { header?: false; dynamicTyping: true }): DelimitedResult<ParsedDelimitedCell[]>;\n/** Parse delimited text into records keyed by the (transformed) header row. Extra fields land in `__parsed_extra`. */\nexport function parseDelimited(text: string, options: DelimitedParseOptions & { header: true; dynamicTyping?: false }): DelimitedResult<Record<string, string>>;\nexport function parseDelimited(text: string, options: DelimitedParseOptions & { header: true; dynamicTyping: true }): DelimitedResult<Record<string, ParsedDelimitedCell>>;\nexport function parseDelimited(text: string, options: DelimitedParseOptions & { header?: boolean } = {}): DelimitedResult<unknown> {\n const input = text.charCodeAt(0) === 0xfeff ? text.slice(1) : text;\n const linebreak = guessLinebreak(input);\n const errors: DelimitedParseError[] = [];\n let delimiter = options.delimiter;\n if (!delimiter) {\n const guess = guessDelimiter(input, linebreak, options.skipEmptyLines);\n delimiter = guess.delimiter;\n if (!guess.successful)\n errors.push({ type: \"Delimiter\", code: \"UndetectableDelimiter\", message: \"Unable to auto-detect delimiting character; defaulted to ','\" });\n }\n const tokens = tokenize(input, delimiter, linebreak, Infinity, options.strayQuotes);\n errors.unshift(...tokens.errors);\n let rows = options.unguardFormulas === false ? tokens.rows : tokens.rows.map((row) => row.map(unguardDelimitedCell));\n if (options.skipEmptyLines) rows = rows.filter((row) => !isEmptyRow(row, options.skipEmptyLines!));\n const meta: DelimitedMeta = { delimiter, linebreak };\n if (!options.header) {\n return { data: options.dynamicTyping ? rows.map((row) => row.map(typed)) : rows, errors, meta };\n }\n const [head = [], ...body] = rows;\n const seen = new Map<string, number>();\n const fields = head.map((raw, index) => {\n const name = options.transformHeader ? options.transformHeader(raw, index) : raw;\n const count = seen.get(name) ?? 0;\n seen.set(name, count + 1);\n return count === 0 ? name : `${name}_${count}`;\n });\n meta.fields = fields;\n const data = body.map((row, index) => {\n const record: Record<string, unknown> = {};\n row.forEach((value, j) => {\n const cell = options.dynamicTyping ? typed(value) : value;\n if (j >= fields.length) {\n ((record.__parsed_extra as unknown[] | undefined) ??= []).push(cell);\n } else record[fields[j]!] = cell;\n });\n if (row.length > fields.length)\n errors.push({ type: \"FieldMismatch\", code: \"TooManyFields\", message: `Too many fields: expected ${fields.length} fields but parsed ${row.length}`, row: index });\n else if (row.length < fields.length)\n errors.push({ type: \"FieldMismatch\", code: \"TooFewFields\", message: `Too few fields: expected ${fields.length} fields but parsed ${row.length}`, row: index });\n return record;\n });\n return { data, errors, meta };\n}\n","/** Local-only CSV normalization for Vault's first password-manager import. */\nimport type { VaultItemCreateRequest } from \"@ai-matrx/api/account\";\nimport { parseDelimited } from \"@ai-matrx/kit/delimited\";\n\nimport { isLoopbackUrl } from \"./loopback\";\n\nexport type CsvImportLimits = {\n maxFileBytes: number;\n maxRecords: number;\n maxColumns: number;\n maxCellBytes: number;\n maxFields: number;\n maxPlaintextFieldBytes: number;\n maxRequestBodyBytes: number;\n maxJsonDepth?: number;\n jsonWorkerTimeoutMs?: number;\n};\nexport type CsvColumnRole =\n \"title\" | \"username\" | \"password\" | \"url\" | \"notes\" | \"otp\" | \"keep\";\nexport type CsvImportRow = {\n rowNumber: number;\n cells: string[];\n issue?: \"invalid\" | \"unsupported\" | undefined;\n};\nexport type CsvImportPreview = {\n headers: string[];\n rows: CsvImportRow[];\n issues: number;\n};\nexport type CsvImportCommand = {\n rowId: string;\n body: VaultItemCreateRequest;\n hasOtp: boolean;\n};\nexport type CsvImportPreparation =\n | { status: \"ready\"; command: CsvImportCommand }\n | {\n status: \"skipped\";\n reason:\n | \"invalid\"\n | \"unsupported\"\n | \"deleted\"\n | \"archived\"\n | \"possible_duplicate\";\n }\n | { status: \"invalid\"; diagnostic: string };\nexport type CsvImportOutcome = {\n imported: number;\n skipped: number;\n failed: number;\n cancelled: boolean;\n definitive: boolean;\n /** The first command that was not confirmed. Reuse it on a same-session retry. */\n progressCursor: number;\n};\n\nexport type CsvImportDispatch = \"committed\" | \"retryable\" | \"definitive\";\n\n/** The request's owner: an organization's vault, or the person's own. */\nexport function principalOf(organizationId: string | null): NonNullable<VaultItemCreateRequest[\"principal\"]> {\n return organizationId ? { type: \"organization\", organization_id: organizationId } : { type: \"user\" };\n}\n\nfunction utf8ByteLength(value: string): number {\n if (typeof TextEncoder !== \"undefined\")\n return new TextEncoder().encode(value).byteLength;\n return unescape(encodeURIComponent(value)).length;\n}\n\nfunction csvParserInput(text: string): string {\n // The parser emits an extra empty record for the final record terminator. Remove\n // exactly that terminator; interior/explicit blank rows retain their numbers.\n return text.replace(/^\\uFEFF/, \"\").replace(/(?:\\r\\n|\\r|\\n)$/, \"\");\n}\n\nexport function parseCsvText(\n text: string,\n limits: CsvImportLimits,\n): CsvImportPreview {\n if (utf8ByteLength(text) > limits.maxFileBytes)\n throw new Error(\"The file exceeds this organization’s import size limit.\");\n const parsed = parseDelimited(csvParserInput(text), {\n delimiter: \",\",\n skipEmptyLines: false,\n });\n if (\n parsed.errors.some(\n (error) =>\n error.code !== \"TooFewFields\" && error.code !== \"TooManyFields\",\n )\n )\n throw new Error(\"The CSV could not be read. Fix the file and try again.\");\n return validateParsedCsv(parsed.data, limits);\n}\n\nfunction validateParsedCsv(\n parsed: string[][],\n limits: CsvImportLimits,\n): CsvImportPreview {\n const [headers, ...data] = parsed;\n if (!headers?.length || headers.length > limits.maxColumns)\n throw new Error(\"The CSV has an unsupported number of columns.\");\n if (data.length > limits.maxRecords)\n throw new Error(\"The CSV has more records than this organization allows.\");\n const nonLoginTypeColumn = headers.findIndex((header) =>\n /^(item )?(type|kind|category)$/i.test(header.trim()),\n );\n const unsupportedValueColumns = headers\n .map((header, index) =>\n /(?:^|[\\s_-])(passkey|webauthn|fido|attachment|file)(?:$|[\\s_-])/i.test(\n header.trim(),\n )\n ? index\n : -1,\n )\n .filter((index) => index >= 0);\n let issues = 0;\n const rows = data.map((cells, index) => {\n const invalid =\n cells.length !== headers.length ||\n cells.some((cell) => utf8ByteLength(cell) > limits.maxCellBytes) ||\n cells.every((cell) => cell.trim().length === 0);\n const type =\n nonLoginTypeColumn >= 0\n ? (cells[nonLoginTypeColumn] ?? \"\").trim().toLowerCase()\n : \"\";\n const unsupported =\n !invalid &&\n (unsupportedValueColumns.some((column) =>\n Boolean(cells[column]?.trim()),\n ) ||\n (Boolean(type) &&\n !/^(login|website|web|password|credential)$/.test(type)));\n if (invalid || unsupported) issues += 1;\n return {\n rowNumber: index + 2,\n cells,\n issue: invalid\n ? (\"invalid\" as const)\n : unsupported\n ? (\"unsupported\" as const)\n : undefined,\n };\n });\n return { headers, rows, issues };\n}\n\nexport function safeDestination(raw: string): {\n metadata: string | null;\n host: string | null;\n} {\n try {\n const parsed = new URL(raw);\n if (!/^https?:$/.test(parsed.protocol))\n return { metadata: null, host: null };\n if (parsed.username || parsed.password)\n return { metadata: null, host: null };\n return { metadata: parsed.origin, host: parsed.host };\n } catch {\n return { metadata: null, host: null };\n }\n}\n\nexport function suggestedCsvMapping(\n headers: string[],\n source?: string,\n): CsvColumnRole[] {\n const aliases: Record<CsvColumnRole, ReadonlySet<string>> = {\n title: new Set([\"name\", \"title\", \"label\"]),\n username: new Set([\n \"username\",\n \"user\",\n \"login\",\n \"email\",\n \"loginusername\",\n \"loginemail\",\n ]),\n password: new Set([\"password\", \"pass\", \"pwd\", \"loginpassword\"]),\n url: new Set([\n \"url\",\n \"uri\",\n \"website\",\n \"websiteaddress\",\n \"loginurl\",\n \"loginuri\",\n ]),\n notes: new Set([\"note\", \"notes\", \"extra\"]),\n otp: new Set([\"otp\", \"totp\", \"otpauth\", \"onetimepassword\", \"onetimecode\"]),\n keep: new Set(),\n };\n let usernameAssigned = false;\n return headers.map((header) => {\n const key = header.trim().toLowerCase().replace(/[ _-]/g, \"\");\n if (source === \"nordpass\" && key === \"email\" && usernameAssigned)\n return \"keep\";\n const role =\n (Object.entries(aliases) as [CsvColumnRole, ReadonlySet<string>][]).find(\n ([, values]) => values.has(key),\n )?.[0] ?? \"keep\";\n if (role === \"username\") usernameAssigned = true;\n return role;\n });\n}\n\nfunction fieldKey(index: number, used: Set<string>): string {\n const base = `import_column_${index + 1}`;\n let key = base;\n let suffix = 2;\n while (used.has(key)) key = `${base}_${suffix++}`;\n used.add(key);\n return key;\n}\n\nfunction roleValue(\n row: CsvImportRow,\n mapping: CsvColumnRole[],\n role: CsvColumnRole,\n): string | null {\n const index = mapping.findIndex((entry) => entry === role);\n return index < 0 ? null : (row.cells[index] ?? \"\");\n}\n\nexport function hasAmbiguousCsvMapping(mapping: CsvColumnRole[]): boolean {\n return ([\"title\", \"username\", \"password\", \"notes\", \"otp\"] as const).some(\n (role) => mapping.filter((entry) => entry === role).length > 1,\n );\n}\n\nexport function isPossibleDuplicateRow(\n row: CsvImportRow,\n _preview: CsvImportPreview,\n mapping: CsvColumnRole[],\n existingItems: { displayName: string; loginUrls: string[] }[],\n): boolean {\n if (row.issue) return false;\n const title =\n roleValue(row, mapping, \"title\") || `Imported credential ${row.rowNumber}`;\n const urls = row.cells\n .filter((_, index) => mapping[index] === \"url\")\n .map(safeDestination)\n .flatMap((destination) =>\n destination.metadata ? [destination.metadata] : [],\n );\n return existingItems.some((item) => {\n if (item.displayName !== title) return false;\n const existingOrigins = item.loginUrls\n .map(safeDestination)\n .flatMap((destination) =>\n destination.metadata ? [destination.metadata] : [],\n );\n return urls.length > 0\n ? existingOrigins.some((origin) => urls.includes(origin))\n : existingOrigins.length === 0;\n });\n}\n\nexport function prepareCsvImportRow(input: {\n source: string;\n preview: CsvImportPreview;\n row: CsvImportRow;\n mapping: CsvColumnRole[];\n /** null = the person's own vault. */\n organizationId: string | null;\n rowId: string;\n limits: CsvImportLimits;\n browserFillEnabled?: boolean;\n existingItems?: { displayName: string; loginUrls: string[] }[];\n skipPossibleDuplicate?: boolean;\n}): CsvImportPreparation {\n if (input.row.issue === \"invalid\")\n return { status: \"skipped\", reason: \"invalid\" };\n if (input.row.issue === \"unsupported\")\n return { status: \"skipped\", reason: \"unsupported\" };\n if (\n input.skipPossibleDuplicate &&\n isPossibleDuplicateRow(\n input.row,\n input.preview,\n input.mapping,\n input.existingItems ?? [],\n )\n )\n return { status: \"skipped\", reason: \"possible_duplicate\" };\n const { headers } = input.preview;\n const { row, mapping } = input;\n const title =\n roleValue(row, mapping, \"title\") || `Imported credential ${row.rowNumber}`;\n const rawUrls = row.cells\n .filter((_, index) => mapping[index] === \"url\")\n .filter(Boolean);\n const urls = rawUrls\n .map(safeDestination)\n .flatMap((url) => (url.metadata ? [url.metadata] : []));\n const fields: NonNullable<VaultItemCreateRequest[\"fields\"]> = [];\n const used = new Set<string>();\n const add = (field_key: string, value: string, editable = true) => {\n if (!value) return;\n used.add(field_key);\n fields.push({\n field_key,\n value,\n handling: \"revealable\",\n editable,\n inject_into_sandbox: false,\n });\n };\n add(\"username\", roleValue(row, mapping, \"username\") ?? \"\");\n add(\"password\", roleValue(row, mapping, \"password\") ?? \"\");\n add(\"import_notes\", roleValue(row, mapping, \"notes\") ?? \"\");\n for (let index = 0; index < row.cells.length; index += 1) {\n if (mapping[index] === \"keep\")\n add(fieldKey(index, used), row.cells[index] ?? \"\");\n }\n // OTP stays only in the encrypted, revealable source representation until explicit enrollment.\n add(\n \"import_source_record\",\n JSON.stringify({\n source_vendor: input.source,\n format: \"csv\",\n cells: row.cells.map((value: string, column_index: number) => ({\n column_index,\n header: headers[column_index] ?? \"\",\n value,\n })),\n }),\n false,\n );\n if (\n fields.length > input.limits.maxFields ||\n fields.some(\n (field) =>\n utf8ByteLength(field.value) > input.limits.maxPlaintextFieldBytes,\n )\n )\n return {\n status: \"invalid\",\n diagnostic: `Row ${row.rowNumber} exceeds this organization’s encrypted field limit.`,\n };\n const command: CsvImportCommand = {\n rowId: input.rowId,\n hasOtp: Boolean(roleValue(row, mapping, \"otp\")),\n body: {\n principal: principalOf(input.organizationId),\n display_name: title,\n definition_key: \"website_login\",\n source: \"system_import\",\n login_urls: urls,\n uri_match_mode: \"never\",\n browser_fill_enabled:\n Boolean(input.browserFillEnabled) &&\n Boolean(roleValue(row, mapping, \"username\")) &&\n Boolean(roleValue(row, mapping, \"password\")) &&\n urls.length > 0 &&\n urls.every(\n (url) => new URL(url).protocol === \"https:\" || isLoopbackUrl(url),\n ),\n fields,\n },\n };\n // The browser matcher treats `never` as an absolute refusal. Keep this pair\n // coherent with the explicit, eligible bulk opt-in rather than advertising a\n // fill capability the canonical matcher will always reject.\n if (command.body.browser_fill_enabled) command.body.uri_match_mode = \"host\";\n if (\n utf8ByteLength(JSON.stringify(command.body)) >\n input.limits.maxRequestBodyBytes\n )\n return {\n status: \"invalid\",\n diagnostic: `Row ${row.rowNumber} exceeds this organization’s request size limit.`,\n };\n return { status: \"ready\", command };\n}\n\n/** Compatibility helper for callers that only need a command after preparation. */\nexport function toCsvImportCommand(\n input: Parameters<typeof prepareCsvImportRow>[0],\n): CsvImportCommand | null {\n const prepared = prepareCsvImportRow(input);\n return prepared.status === \"ready\" ? prepared.command : null;\n}\n\n/** Dispatch only one immutable command at a time; cancellation never rolls back a committed row. */\nexport async function runCsvImportCommands(\n commands: Array<CsvImportCommand | null>,\n dispatch: (command: CsvImportCommand) => Promise<CsvImportDispatch>,\n cancelled: () => boolean,\n progressCursor = 0,\n): Promise<CsvImportOutcome> {\n let imported = 0;\n let skipped = 0;\n let failed = 0;\n for (let index = progressCursor; index < commands.length; index += 1) {\n const command = commands[index];\n if (cancelled())\n return {\n imported,\n skipped,\n failed,\n cancelled: true,\n definitive: false,\n progressCursor: index,\n };\n if (!command) {\n skipped += 1;\n continue;\n }\n const result = await dispatch(command);\n if (result === \"committed\") {\n imported += 1;\n continue;\n }\n failed += 1;\n // A retryable or definitive outcome retains this exact command/UUID for review.\n return {\n imported,\n skipped,\n failed,\n cancelled: result === \"definitive\" || cancelled(),\n definitive: result === \"definitive\",\n progressCursor: index,\n };\n }\n return {\n imported,\n skipped,\n failed,\n cancelled: cancelled(),\n definitive: false,\n progressCursor: commands.length,\n };\n}\n","/**\n * Parses a CSV import off the main thread: a file near the size limit would otherwise freeze the dialog.\n * Imports only the pure parser module; reaching `csv-import-file.ts` (which starts this worker) hangs\n * the Turbopack build (`pnpm check:worker-cycles`).\n */\nimport { parseCsvText, type CsvImportLimits, type CsvImportPreview } from \"./csv-import\";\n\nexport type CsvWorkerRequest = { text: string; limits: CsvImportLimits };\nexport type CsvWorkerResponse =\n | { ok: true; preview: CsvImportPreview }\n | { ok: false; message: string };\n\nself.onmessage = (event: MessageEvent<CsvWorkerRequest>) => {\n let response: CsvWorkerResponse;\n try {\n response = { ok: true, preview: parseCsvText(event.data.text, event.data.limits) };\n } catch (error) {\n response = {\n ok: false,\n message: error instanceof Error ? error.message : \"The CSV could not be read. Fix the file and try again.\",\n };\n }\n (self as unknown as { postMessage(message: CsvWorkerResponse): void }).postMessage(response);\n};\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../../kit/src/delimited.ts","../../src/io/csv-import.ts","../../src/io/csv-import.worker.ts"],"sourcesContent":["/**\n * @ai-matrx/kit/delimited — THE one CSV/TSV writer for every Matrx package and app.\n *\n * WHY HERE. Every package that writes delimited text depends on kit; most sit BELOW\n * `@ai-matrx/alchemy` (design-system, records, meet) and cannot import it. So the one cell\n * encoder lives at the bottom of the graph, and Alchemy's `serialize` / `toDelimitedText` /\n * `buildFile` call it (decision AP-2, recorded in alchemy FEATURE.md). A second hand-rolled\n * `.replace(/\"/g, '\"\"')` writer anywhere is a defect (`check-shared-doors.mjs` `handcsv`).\n *\n * THE ONE spreadsheetSafe DEFAULT. Output meant for a spreadsheet — the csv and tsv\n * formats — is formula-guarded unless the caller passes `spreadsheetSafe: false` with a reason\n * (a lossless clipboard round-trip, text an AI reads). Every entry point resolves its default\n * through `spreadsheetSafeByDefault(format)`; none restates the rule.\n *\n * THE GUARD (OWASP \"CSV Injection\"): a TEXT cell is prefixed with `'` when it starts with\n * `=` `+` `-` `@`, a tab or a carriage return — and also when leading spaces/line breaks precede\n * `=` `+` `-` `@` (spreadsheets trim them before deciding a cell is a formula). A real number\n * (`-5` passed as a number) is never altered, and neither is text that is exactly a signed number\n * (`\"-5\"`, `\"+3.2\"`, `\"-1e3\"`): a spreadsheet reads it as that number, never as a formula.\n * An .xlsx workbook is NOT in the set: its text cells are typed strings a spreadsheet never\n * evaluates, so guarding them would only corrupt the value (decision AP-2, alchemy FEATURE.md).\n *\n * THE ONE READER sits beside the one writer: `parseDelimited` (Papa Parse–compatible in the options\n * the platform uses) is here, below every package that reads CSV/TSV (records, records-ui,\n * rich-content, the apps); Alchemy's `/operate/read` re-exports it (decision AP-2). A second\n * char-loop quote reader anywhere is a defect (`check-shared-doors.mjs` `handcsv`).\n * THE GUARD IS REVERSIBLE AND OUR READER REVERSES IT (coordinator ruling, AP-2 A1/A7):\n * `parseDelimited` strips the guard's `'` by default (`unguardFormulas: false` keeps raw text), so\n * our own grid paste and importers get the original value back while every export stays guarded.\n * The writer also prefixes text that already LOOKS guarded (`'=x` → `''=x`), so write → read is\n * exact for every string.\n * Pure; no DOM, no dependencies.\n */\n\nexport type DelimitedFormat = \"csv\" | \"tsv\";\n\n/** One cell. Strings are text (guarded); numbers, booleans and bigints are values (never guarded). */\nexport type DelimitedCell = string | number | boolean | bigint | null | undefined;\n\n/** The delimited formats a spreadsheet opens as text it may evaluate — formula-guarded by default. */\nexport const SPREADSHEET_SAFE_FORMATS: readonly string[] = [\"csv\", \"tsv\"];\n\n/** THE default for `spreadsheetSafe`: on for csv and tsv; off for every other format. */\nexport function spreadsheetSafeByDefault(format: string): boolean {\n return SPREADSHEET_SAFE_FORMATS.includes(format);\n}\n\nconst FORMULA_LEAD = /^(?:[\\t\\r]|[\\t\\r\\n ]*[=+\\-@])/;\n\n/** Text that is exactly one signed decimal number — a spreadsheet reads it as that number. */\nconst SIGNED_NUMBER = /^[+-](?:\\d+(?:\\.\\d*)?|\\.\\d+)(?:[eE][+-]?\\d+)?$/;\n\n/**\n * True when a spreadsheet could read this text as a formula (the OWASP CSV-injection set). A\n * signed number written as text (`\"-5\"`, `\"+3.2\"`) is not: it can only ever be that number.\n */\nexport function isFormulaLike(text: string): boolean {\n return FORMULA_LEAD.test(text) && !SIGNED_NUMBER.test(text);\n}\n\n/**\n * True when this text is a GUARDED cell: a leading `'` whose remainder the guard would have\n * prefixed (formula-like, or itself a guarded cell). This is exactly the set `parseDelimited`\n * unguards, and the writer prefixes it too, so write → read is exact for every string:\n * `\"+1 949 807 2145\"` → `\"'+1 949 807 2145\"` → `\"+1 949 807 2145\"`; `\"'tis\"` is never touched.\n */\nexport function isGuardedCell(text: string): boolean {\n let i = 0;\n while (text[i] === \"'\") {\n if (isFormulaLike(text.slice(i + 1))) return true;\n i++;\n }\n return false;\n}\n\n/** Reverse the formula guard: `'=SUM(A1)` → `=SUM(A1)`; anything not guarded comes back as-is. */\nexport function unguardDelimitedCell(text: string): string {\n return isGuardedCell(text) ? text.slice(1) : text;\n}\n\nexport interface DelimitedCellOptions {\n /** Which format the cell is for; picks the default delimiter and guard. Defaults to csv. */\n format?: DelimitedFormat;\n /** Defaults to the format's (`,` for csv, a tab for tsv). */\n delimiter?: string;\n /** Defaults to `spreadsheetSafeByDefault(format)`. */\n spreadsheetSafe?: boolean;\n}\n\n/** One cell as delimited text; `escaped` says the formula guard fired. */\nexport function encodeDelimitedCell(\n cell: DelimitedCell,\n options: DelimitedCellOptions = {},\n): { text: string; escaped: boolean } {\n const format = options.format ?? \"csv\";\n const delimiter = options.delimiter ?? (format === \"tsv\" ? \"\\t\" : \",\");\n const spreadsheetSafe = options.spreadsheetSafe ?? spreadsheetSafeByDefault(format);\n if (cell === null || cell === undefined) return { text: \"\", escaped: false };\n let text = typeof cell === \"string\" ? cell : String(cell);\n let escaped = false;\n if (typeof cell === \"string\" && spreadsheetSafe && (isFormulaLike(text) || isGuardedCell(text))) {\n text = `'${text}`;\n escaped = true;\n }\n if (text.includes(delimiter) || /[\"\\n\\r]/.test(text)) text = `\"${text.replace(/\"/g, '\"\"')}\"`;\n return { text, escaped };\n}\n\nexport interface DelimitedOptions {\n /** `csv` (comma) or `tsv` (tab). Defaults to csv. */\n format?: DelimitedFormat;\n /** Overrides the format's delimiter (e.g. `;` or `|` for a block that declares one). */\n delimiter?: string;\n /** Defaults to `spreadsheetSafeByDefault(format)` — on. */\n spreadsheetSafe?: boolean;\n /** Between records. Defaults to `\\n` (Excel, Sheets and every parser here accept it). */\n lineEnding?: \"\\n\" | \"\\r\\n\";\n /** End the text with one line ending. Defaults to false. */\n trailingNewline?: boolean;\n /** Called once per guarded cell (row 0 is the first row passed, usually the header). */\n onEscaped?: (row: number, column: number) => void;\n}\n\n/** Rows of cells as CSV/TSV text — the one writer. */\nexport function toDelimited(\n rows: readonly (readonly DelimitedCell[])[],\n options: DelimitedOptions = {},\n): string {\n const format = options.format ?? \"csv\";\n const delimiter = options.delimiter ?? (format === \"tsv\" ? \"\\t\" : \",\");\n const spreadsheetSafe = options.spreadsheetSafe ?? spreadsheetSafeByDefault(format);\n const lineEnding = options.lineEnding ?? \"\\n\";\n const text = rows\n .map((row, r) =>\n row\n .map((cell, c) => {\n const out = encodeDelimitedCell(cell, { format, delimiter, spreadsheetSafe });\n if (out.escaped) options.onEscaped?.(r, c);\n return out.text;\n })\n .join(delimiter),\n )\n .join(lineEnding);\n // One row holding one empty cell would write as nothing, and nothing reads back as NO rows: quote\n // that lone empty field (`\"\"`) so `[[\"\"]]` reads back as one row with one empty cell.\n const lone = rows.length === 1 && rows[0]!.length === 1 && text === \"\" ? '\"\"' : text;\n return options.trailingNewline && rows.length > 0 ? `${lone}${lineEnding}` : lone;\n}\n\n// ─── Read: THE one CSV/TSV parser (moved from @ai-matrx/alchemy/operate/read, AP-2) ──────────────\n\nexport type DelimitedParseError = {\n type: \"Quotes\" | \"Delimiter\" | \"FieldMismatch\";\n code: \"MissingQuotes\" | \"InvalidQuotes\" | \"UndetectableDelimiter\" | \"TooFewFields\" | \"TooManyFields\";\n message: string;\n /** 0-based index of the data row the error belongs to (absent for whole-file errors). */\n row?: number;\n};\n\nexport type DelimitedParseOptions = {\n /** Field delimiter. Absent → guessed from `,` `\\t` `|` `;` and the ASCII record/unit separators; `,` when none fits. */\n delimiter?: string;\n /** true: an exactly empty line is dropped; \"greedy\": a line of only whitespace is dropped too. */\n skipEmptyLines?: boolean | \"greedy\";\n /** Numbers, `true`/`false` and ISO date-times become typed values; an empty field becomes null. */\n dynamicTyping?: boolean;\n /**\n * Reverse the writer's formula guard (default true): a field that is `'` + text the guard would\n * have prefixed (`'=SUM(A1)`, `'+1 949 807 2145`, `'-`, `'@home`, `' =x`) loses that one `'`.\n * Every reader of ours reads text our writer may have guarded, so this is on unless a caller\n * needs the raw bytes (`false`). A genuine leading apostrophe (`'tis`) is never touched.\n */\n unguardFormulas?: boolean;\n /**\n * A field that opens with `\"` but is not a well-formed quoted field: `\"quoted\"` (default, Papa\n * Parse's behavior) keeps reading it as quoted; `\"literal\"` reads the quote as text and ends\n * the field at the next delimiter (`5\" screen`) — a spreadsheet's paste reading. Both report it.\n */\n strayQuotes?: \"quoted\" | \"literal\";\n /** Header mode only: rename a header (e.g. trim + lowercase). */\n transformHeader?(header: string, index: number): string;\n};\n\n/** A parsed field under `dynamicTyping` (the writer's `ParsedDelimitedCell` is the input side). */\nexport type ParsedDelimitedCell = string | number | boolean | Date | null;\nexport type DelimitedMeta = { delimiter: string; linebreak: string; fields?: string[] };\nexport type DelimitedResult<T> = { data: T[]; errors: DelimitedParseError[]; meta: DelimitedMeta };\n\nconst RECORD_SEP = String.fromCharCode(30);\nconst UNIT_SEP = String.fromCharCode(31);\nconst GUESS_DELIMITERS = [\",\", \"\\t\", \"|\", \";\", RECORD_SEP, UNIT_SEP];\nconst FLOAT = /^\\s*-?(\\d+\\.?|\\.\\d+|\\d+\\.\\d+)([eE][-+]?\\d+)?\\s*$/;\nconst ISO_DATE =\n /^((\\d{4}-[01]\\d-[0-3]\\dT[0-2]\\d:[0-5]\\d:[0-5]\\d\\.\\d+([+-][0-2]\\d:[0-5]\\d|Z))|(\\d{4}-[01]\\d-[0-3]\\dT[0-2]\\d:[0-5]\\d:[0-5]\\d([+-][0-2]\\d:[0-5]\\d|Z))|(\\d{4}-[01]\\d-[0-3]\\dT[0-2]\\d:[0-5]\\d([+-][0-2]\\d:[0-5]\\d|Z)))$/;\n\nfunction guessLinebreak(rawText: string): string {\n // Quoted cells may hold any line break; only the ones between records say what the file uses.\n const text = rawText.includes('\"') ? rawText.replace(/\"[^\"]*\"/g, '\"\"') : rawText;\n const crlf = text.indexOf(\"\\r\\n\");\n const lf = text.indexOf(\"\\n\");\n const cr = text.indexOf(\"\\r\");\n if (crlf !== -1 && crlf === cr) return \"\\r\\n\";\n if (cr !== -1 && (lf === -1 || cr < lf)) return \"\\r\";\n return \"\\n\";\n}\n\n/** Tokenize into rows of raw string fields; quote errors are reported against their data row. */\nfunction tokenize(\n text: string,\n delimiter: string,\n linebreak: string,\n preview = Infinity,\n strayQuotes: \"quoted\" | \"literal\" = \"quoted\",\n): { rows: string[][]; errors: DelimitedParseError[] } {\n const rows: string[][] = [];\n const errors: DelimitedParseError[] = [];\n if (text === \"\") return { rows, errors };\n let row: string[] = [];\n let i = 0;\n const n = text.length;\n const dl = delimiter.length;\n const ll = linebreak.length;\n while (i <= n && rows.length < preview) {\n if (text.startsWith('\"', i)) {\n // Quoted field: \"\" is a literal quote; the closing quote must precede a delimiter, a line break or the end.\n let value = \"\";\n let j = i + 1;\n let closed = false;\n let malformed = false;\n while (j < n) {\n const q = text.indexOf('\"', j);\n if (q === -1) break;\n value += text.slice(j, q);\n if (text[q + 1] === '\"') {\n value += '\"';\n j = q + 2;\n continue;\n }\n const after = q + 1;\n if (after >= n || text.startsWith(delimiter, after) || text.startsWith(linebreak, after)) {\n closed = true;\n j = after;\n break;\n }\n errors.push({ type: \"Quotes\", code: \"InvalidQuotes\", message: \"Trailing quote on quoted field is malformed\", row: rows.length });\n malformed = true;\n if (strayQuotes === \"literal\") break;\n value += '\"';\n j = after;\n }\n if (!closed && !malformed)\n errors.push({ type: \"Quotes\", code: \"MissingQuotes\", message: \"Quoted field unterminated\", row: rows.length });\n if (strayQuotes === \"quoted\" || closed) {\n if (!closed) {\n value += text.slice(j);\n row.push(value);\n rows.push(row);\n return { rows, errors };\n }\n row.push(value);\n i = j;\n if (i >= n) {\n rows.push(row);\n break;\n }\n if (text.startsWith(delimiter, i)) {\n i += dl;\n if (i >= n) {\n row.push(\"\");\n rows.push(row);\n break;\n }\n continue;\n }\n rows.push(row);\n row = [];\n i += ll;\n if (i >= n) {\n rows.push([\"\"]);\n break;\n }\n continue;\n }\n // strayQuotes \"literal\": a field that merely STARTS with a quote (`5\" screen`) is text,\n // ending at the next delimiter or line break — how a spreadsheet reads a pasted block.\n }\n const nextDelim = text.indexOf(delimiter, i);\n const nextBreak = text.indexOf(linebreak, i);\n if (nextDelim !== -1 && (nextBreak === -1 || nextDelim < nextBreak)) {\n row.push(text.slice(i, nextDelim));\n i = nextDelim + dl;\n if (i >= n) {\n row.push(\"\");\n rows.push(row);\n break;\n }\n continue;\n }\n if (nextBreak !== -1) {\n row.push(text.slice(i, nextBreak));\n rows.push(row);\n row = [];\n i = nextBreak + ll;\n if (i >= n) {\n rows.push([\"\"]);\n break;\n }\n continue;\n }\n row.push(text.slice(i));\n rows.push(row);\n break;\n }\n return { rows, errors };\n}\n\nconst isEmptyRow = (row: readonly string[], mode: boolean | \"greedy\") =>\n mode === \"greedy\" ? row.join(\"\").trim() === \"\" : row.length === 1 && row[0]!.length === 0;\n\nfunction guessDelimiter(text: string, linebreak: string, skip: boolean | \"greedy\" | undefined): { delimiter: string; successful: boolean } {\n let best: string | undefined;\n let bestDelta: number | undefined;\n let bestAvg: number | undefined;\n for (const delim of GUESS_DELIMITERS) {\n const { rows } = tokenize(text, delim, linebreak, 10);\n let delta = 0;\n let avg = 0;\n let empty = 0;\n let prev: number | undefined;\n for (const row of rows) {\n if (skip && isEmptyRow(row, skip)) {\n empty++;\n continue;\n }\n const count = row.length;\n avg += count;\n if (prev === undefined) {\n prev = count;\n continue;\n }\n if (count > 0) {\n delta += Math.abs(count - prev);\n prev = count;\n }\n }\n if (rows.length > 0) avg /= rows.length - empty;\n if ((bestDelta === undefined || delta <= bestDelta) && (bestAvg === undefined || avg > bestAvg) && avg > 1.99) {\n bestDelta = delta;\n bestAvg = avg;\n best = delim;\n }\n }\n return { delimiter: best ?? \",\", successful: best !== undefined };\n}\n\nfunction typed(value: string): ParsedDelimitedCell {\n if (value === \"true\" || value === \"TRUE\") return true;\n if (value === \"false\" || value === \"FALSE\") return false;\n if (FLOAT.test(value)) {\n const num = parseFloat(value);\n if (num > -(2 ** 53) && num < 2 ** 53) return num;\n return value;\n }\n if (ISO_DATE.test(value)) return new Date(value);\n return value === \"\" ? null : value;\n}\n\n/** Parse delimited text into rows of fields (header row first). */\nexport function parseDelimited(text: string, options?: DelimitedParseOptions & { header?: false; dynamicTyping?: false }): DelimitedResult<string[]>;\nexport function parseDelimited(text: string, options: DelimitedParseOptions & { header?: false; dynamicTyping: true }): DelimitedResult<ParsedDelimitedCell[]>;\n/** Parse delimited text into records keyed by the (transformed) header row. Extra fields land in `__parsed_extra`. */\nexport function parseDelimited(text: string, options: DelimitedParseOptions & { header: true; dynamicTyping?: false }): DelimitedResult<Record<string, string>>;\nexport function parseDelimited(text: string, options: DelimitedParseOptions & { header: true; dynamicTyping: true }): DelimitedResult<Record<string, ParsedDelimitedCell>>;\nexport function parseDelimited(text: string, options: DelimitedParseOptions & { header?: boolean } = {}): DelimitedResult<unknown> {\n const input = text.charCodeAt(0) === 0xfeff ? text.slice(1) : text;\n const linebreak = guessLinebreak(input);\n const errors: DelimitedParseError[] = [];\n let delimiter = options.delimiter;\n if (!delimiter) {\n const guess = guessDelimiter(input, linebreak, options.skipEmptyLines);\n delimiter = guess.delimiter;\n if (!guess.successful)\n errors.push({ type: \"Delimiter\", code: \"UndetectableDelimiter\", message: \"Unable to auto-detect delimiting character; defaulted to ','\" });\n }\n const tokens = tokenize(input, delimiter, linebreak, Infinity, options.strayQuotes);\n errors.unshift(...tokens.errors);\n let rows = options.unguardFormulas === false ? tokens.rows : tokens.rows.map((row) => row.map(unguardDelimitedCell));\n if (options.skipEmptyLines) rows = rows.filter((row) => !isEmptyRow(row, options.skipEmptyLines!));\n const meta: DelimitedMeta = { delimiter, linebreak };\n if (!options.header) {\n return { data: options.dynamicTyping ? rows.map((row) => row.map(typed)) : rows, errors, meta };\n }\n const [head = [], ...body] = rows;\n const seen = new Map<string, number>();\n const fields = head.map((raw, index) => {\n const name = options.transformHeader ? options.transformHeader(raw, index) : raw;\n const count = seen.get(name) ?? 0;\n seen.set(name, count + 1);\n return count === 0 ? name : `${name}_${count}`;\n });\n meta.fields = fields;\n const data = body.map((row, index) => {\n const record: Record<string, unknown> = {};\n row.forEach((value, j) => {\n const cell = options.dynamicTyping ? typed(value) : value;\n if (j >= fields.length) {\n ((record.__parsed_extra as unknown[] | undefined) ??= []).push(cell);\n } else record[fields[j]!] = cell;\n });\n if (row.length > fields.length)\n errors.push({ type: \"FieldMismatch\", code: \"TooManyFields\", message: `Too many fields: expected ${fields.length} fields but parsed ${row.length}`, row: index });\n else if (row.length < fields.length)\n errors.push({ type: \"FieldMismatch\", code: \"TooFewFields\", message: `Too few fields: expected ${fields.length} fields but parsed ${row.length}`, row: index });\n return record;\n });\n return { data, errors, meta };\n}\n","/** Local-only CSV normalization for Vault's first password-manager import. */\nimport type { VaultItemCreateRequest } from \"@ai-matrx/api/account\";\nimport { parseDelimited } from \"@ai-matrx/kit/delimited\";\n\nimport { isLoopbackUrl } from \"./loopback\";\n\nexport type CsvImportLimits = {\n maxFileBytes: number;\n maxRecords: number;\n maxColumns: number;\n maxCellBytes: number;\n maxFields: number;\n maxPlaintextFieldBytes: number;\n maxRequestBodyBytes: number;\n maxJsonDepth?: number;\n jsonWorkerTimeoutMs?: number;\n};\nexport type CsvColumnRole =\n \"title\" | \"username\" | \"password\" | \"url\" | \"notes\" | \"otp\" | \"keep\";\nexport type CsvImportRow = {\n rowNumber: number;\n cells: string[];\n issue?: \"invalid\" | \"unsupported\" | undefined;\n};\nexport type CsvImportPreview = {\n headers: string[];\n rows: CsvImportRow[];\n issues: number;\n};\nexport type CsvImportCommand = {\n rowId: string;\n body: VaultItemCreateRequest;\n hasOtp: boolean;\n};\nexport type CsvImportPreparation =\n | { status: \"ready\"; command: CsvImportCommand }\n | {\n status: \"skipped\";\n reason:\n | \"invalid\"\n | \"unsupported\"\n | \"deleted\"\n | \"archived\"\n | \"possible_duplicate\";\n }\n | { status: \"invalid\"; diagnostic: string };\nexport type CsvImportOutcome = {\n imported: number;\n skipped: number;\n failed: number;\n cancelled: boolean;\n definitive: boolean;\n /** The first command that was not confirmed. Reuse it on a same-session retry. */\n progressCursor: number;\n};\n\nexport type CsvImportDispatch = \"committed\" | \"retryable\" | \"definitive\";\n\n/** The request's owner: an organization's vault, or the person's own. */\nexport function principalOf(organizationId: string | null): NonNullable<VaultItemCreateRequest[\"principal\"]> {\n return organizationId ? { type: \"organization\", organization_id: organizationId } : { type: \"user\" };\n}\n\nfunction utf8ByteLength(value: string): number {\n if (typeof TextEncoder !== \"undefined\")\n return new TextEncoder().encode(value).byteLength;\n return unescape(encodeURIComponent(value)).length;\n}\n\nfunction csvParserInput(text: string): string {\n // The parser emits an extra empty record for the final record terminator. Remove\n // exactly that terminator; interior/explicit blank rows retain their numbers.\n return text.replace(/^\\uFEFF/, \"\").replace(/(?:\\r\\n|\\r|\\n)$/, \"\");\n}\n\nexport function parseCsvText(\n text: string,\n limits: CsvImportLimits,\n): CsvImportPreview {\n if (utf8ByteLength(text) > limits.maxFileBytes)\n throw new Error(\"The file exceeds this organization’s import size limit.\");\n const parsed = parseDelimited(csvParserInput(text), {\n delimiter: \",\",\n skipEmptyLines: false,\n });\n if (\n parsed.errors.some(\n (error) =>\n error.code !== \"TooFewFields\" && error.code !== \"TooManyFields\",\n )\n )\n throw new Error(\"The CSV could not be read. Fix the file and try again.\");\n return validateParsedCsv(parsed.data, limits);\n}\n\nfunction validateParsedCsv(\n parsed: string[][],\n limits: CsvImportLimits,\n): CsvImportPreview {\n const [headers, ...data] = parsed;\n if (!headers?.length || headers.length > limits.maxColumns)\n throw new Error(\"The CSV has an unsupported number of columns.\");\n if (data.length > limits.maxRecords)\n throw new Error(\"The CSV has more records than this organization allows.\");\n const nonLoginTypeColumn = headers.findIndex((header) =>\n /^(item )?(type|kind|category)$/i.test(header.trim()),\n );\n const unsupportedValueColumns = headers\n .map((header, index) =>\n /(?:^|[\\s_-])(passkey|webauthn|fido|attachment|file)(?:$|[\\s_-])/i.test(\n header.trim(),\n )\n ? index\n : -1,\n )\n .filter((index) => index >= 0);\n let issues = 0;\n const rows = data.map((cells, index) => {\n const invalid =\n cells.length !== headers.length ||\n cells.some((cell) => utf8ByteLength(cell) > limits.maxCellBytes) ||\n cells.every((cell) => cell.trim().length === 0);\n const type =\n nonLoginTypeColumn >= 0\n ? (cells[nonLoginTypeColumn] ?? \"\").trim().toLowerCase()\n : \"\";\n const unsupported =\n !invalid &&\n (unsupportedValueColumns.some((column) =>\n Boolean(cells[column]?.trim()),\n ) ||\n (Boolean(type) &&\n !/^(login|website|web|password|credential)$/.test(type)));\n if (invalid || unsupported) issues += 1;\n return {\n rowNumber: index + 2,\n cells,\n issue: invalid\n ? (\"invalid\" as const)\n : unsupported\n ? (\"unsupported\" as const)\n : undefined,\n };\n });\n return { headers, rows, issues };\n}\n\nexport function safeDestination(raw: string): {\n metadata: string | null;\n host: string | null;\n} {\n try {\n const parsed = new URL(raw);\n if (!/^https?:$/.test(parsed.protocol))\n return { metadata: null, host: null };\n if (parsed.username || parsed.password)\n return { metadata: null, host: null };\n return { metadata: parsed.origin, host: parsed.host };\n } catch {\n return { metadata: null, host: null };\n }\n}\n\nexport function suggestedCsvMapping(\n headers: string[],\n source?: string,\n): CsvColumnRole[] {\n const aliases: Record<CsvColumnRole, ReadonlySet<string>> = {\n title: new Set([\"name\", \"title\", \"label\"]),\n username: new Set([\n \"username\",\n \"user\",\n \"login\",\n \"email\",\n \"loginusername\",\n \"loginemail\",\n ]),\n password: new Set([\"password\", \"pass\", \"pwd\", \"loginpassword\"]),\n url: new Set([\n \"url\",\n \"uri\",\n \"website\",\n \"websiteaddress\",\n \"loginurl\",\n \"loginuri\",\n ]),\n notes: new Set([\"note\", \"notes\", \"extra\"]),\n otp: new Set([\"otp\", \"totp\", \"otpauth\", \"onetimepassword\", \"onetimecode\"]),\n keep: new Set(),\n };\n let usernameAssigned = false;\n return headers.map((header) => {\n const key = header.trim().toLowerCase().replace(/[ _-]/g, \"\");\n if (source === \"nordpass\" && key === \"email\" && usernameAssigned)\n return \"keep\";\n const role =\n (Object.entries(aliases) as [CsvColumnRole, ReadonlySet<string>][]).find(\n ([, values]) => values.has(key),\n )?.[0] ?? \"keep\";\n if (role === \"username\") usernameAssigned = true;\n return role;\n });\n}\n\nfunction fieldKey(index: number, used: Set<string>): string {\n const base = `import_column_${index + 1}`;\n let key = base;\n let suffix = 2;\n while (used.has(key)) key = `${base}_${suffix++}`;\n used.add(key);\n return key;\n}\n\nfunction roleValue(\n row: CsvImportRow,\n mapping: CsvColumnRole[],\n role: CsvColumnRole,\n): string | null {\n const index = mapping.findIndex((entry) => entry === role);\n return index < 0 ? null : (row.cells[index] ?? \"\");\n}\n\nexport function hasAmbiguousCsvMapping(mapping: CsvColumnRole[]): boolean {\n return ([\"title\", \"username\", \"password\", \"notes\", \"otp\"] as const).some(\n (role) => mapping.filter((entry) => entry === role).length > 1,\n );\n}\n\nexport function isPossibleDuplicateRow(\n row: CsvImportRow,\n _preview: CsvImportPreview,\n mapping: CsvColumnRole[],\n existingItems: { displayName: string; loginUrls: string[] }[],\n): boolean {\n if (row.issue) return false;\n const title =\n roleValue(row, mapping, \"title\") || `Imported credential ${row.rowNumber}`;\n const urls = row.cells\n .filter((_, index) => mapping[index] === \"url\")\n .map(safeDestination)\n .flatMap((destination) =>\n destination.metadata ? [destination.metadata] : [],\n );\n return existingItems.some((item) => {\n if (item.displayName !== title) return false;\n const existingOrigins = item.loginUrls\n .map(safeDestination)\n .flatMap((destination) =>\n destination.metadata ? [destination.metadata] : [],\n );\n return urls.length > 0\n ? existingOrigins.some((origin) => urls.includes(origin))\n : existingOrigins.length === 0;\n });\n}\n\nexport function prepareCsvImportRow(input: {\n source: string;\n preview: CsvImportPreview;\n row: CsvImportRow;\n mapping: CsvColumnRole[];\n /** null = the person's own vault. */\n organizationId: string | null;\n rowId: string;\n limits: CsvImportLimits;\n browserFillEnabled?: boolean;\n existingItems?: { displayName: string; loginUrls: string[] }[];\n skipPossibleDuplicate?: boolean;\n}): CsvImportPreparation {\n if (input.row.issue === \"invalid\")\n return { status: \"skipped\", reason: \"invalid\" };\n if (input.row.issue === \"unsupported\")\n return { status: \"skipped\", reason: \"unsupported\" };\n if (\n input.skipPossibleDuplicate &&\n isPossibleDuplicateRow(\n input.row,\n input.preview,\n input.mapping,\n input.existingItems ?? [],\n )\n )\n return { status: \"skipped\", reason: \"possible_duplicate\" };\n const { headers } = input.preview;\n const { row, mapping } = input;\n const title =\n roleValue(row, mapping, \"title\") || `Imported credential ${row.rowNumber}`;\n const rawUrls = row.cells\n .filter((_, index) => mapping[index] === \"url\")\n .filter(Boolean);\n const urls = rawUrls\n .map(safeDestination)\n .flatMap((url) => (url.metadata ? [url.metadata] : []));\n const fields: NonNullable<VaultItemCreateRequest[\"fields\"]> = [];\n const used = new Set<string>();\n const add = (field_key: string, value: string, editable = true) => {\n if (!value) return;\n used.add(field_key);\n fields.push({\n field_key,\n value,\n handling: \"revealable\",\n editable,\n inject_into_sandbox: false,\n });\n };\n add(\"username\", roleValue(row, mapping, \"username\") ?? \"\");\n add(\"password\", roleValue(row, mapping, \"password\") ?? \"\");\n add(\"import_notes\", roleValue(row, mapping, \"notes\") ?? \"\");\n for (let index = 0; index < row.cells.length; index += 1) {\n if (mapping[index] === \"keep\")\n add(fieldKey(index, used), row.cells[index] ?? \"\");\n }\n // OTP stays only in the encrypted, revealable source representation until explicit enrollment.\n add(\n \"import_source_record\",\n JSON.stringify({\n source_vendor: input.source,\n format: \"csv\",\n cells: row.cells.map((value: string, column_index: number) => ({\n column_index,\n header: headers[column_index] ?? \"\",\n value,\n })),\n }),\n false,\n );\n if (\n fields.length > input.limits.maxFields ||\n fields.some(\n (field) =>\n utf8ByteLength(field.value) > input.limits.maxPlaintextFieldBytes,\n )\n )\n return {\n status: \"invalid\",\n diagnostic: `Row ${row.rowNumber} exceeds this organization’s encrypted field limit.`,\n };\n const command: CsvImportCommand = {\n rowId: input.rowId,\n hasOtp: Boolean(roleValue(row, mapping, \"otp\")),\n body: {\n principal: principalOf(input.organizationId),\n display_name: title,\n definition_key: \"website_login\",\n source: \"system_import\",\n login_urls: urls,\n uri_match_mode: \"never\",\n browser_fill_enabled:\n Boolean(input.browserFillEnabled) &&\n Boolean(roleValue(row, mapping, \"username\")) &&\n Boolean(roleValue(row, mapping, \"password\")) &&\n urls.length > 0 &&\n urls.every(\n (url) => new URL(url).protocol === \"https:\" || isLoopbackUrl(url),\n ),\n fields,\n },\n };\n // The browser matcher treats `never` as an absolute refusal. Keep this pair\n // coherent with the explicit, eligible bulk opt-in rather than advertising a\n // fill capability the canonical matcher will always reject.\n if (command.body.browser_fill_enabled) command.body.uri_match_mode = \"host\";\n if (\n utf8ByteLength(JSON.stringify(command.body)) >\n input.limits.maxRequestBodyBytes\n )\n return {\n status: \"invalid\",\n diagnostic: `Row ${row.rowNumber} exceeds this organization’s request size limit.`,\n };\n return { status: \"ready\", command };\n}\n\n/** Compatibility helper for callers that only need a command after preparation. */\nexport function toCsvImportCommand(\n input: Parameters<typeof prepareCsvImportRow>[0],\n): CsvImportCommand | null {\n const prepared = prepareCsvImportRow(input);\n return prepared.status === \"ready\" ? prepared.command : null;\n}\n\n/** Dispatch only one immutable command at a time; cancellation never rolls back a committed row. */\nexport async function runCsvImportCommands(\n commands: Array<CsvImportCommand | null>,\n dispatch: (command: CsvImportCommand) => Promise<CsvImportDispatch>,\n cancelled: () => boolean,\n progressCursor = 0,\n): Promise<CsvImportOutcome> {\n let imported = 0;\n let skipped = 0;\n let failed = 0;\n for (let index = progressCursor; index < commands.length; index += 1) {\n const command = commands[index];\n if (cancelled())\n return {\n imported,\n skipped,\n failed,\n cancelled: true,\n definitive: false,\n progressCursor: index,\n };\n if (!command) {\n skipped += 1;\n continue;\n }\n const result = await dispatch(command);\n if (result === \"committed\") {\n imported += 1;\n continue;\n }\n failed += 1;\n // A retryable or definitive outcome retains this exact command/UUID for review.\n return {\n imported,\n skipped,\n failed,\n cancelled: result === \"definitive\" || cancelled(),\n definitive: result === \"definitive\",\n progressCursor: index,\n };\n }\n return {\n imported,\n skipped,\n failed,\n cancelled: cancelled(),\n definitive: false,\n progressCursor: commands.length,\n };\n}\n","/**\n * Parses a CSV import off the main thread: a file near the size limit would otherwise freeze the dialog.\n * Imports only the pure parser module; reaching `csv-import-file.ts` (which starts this worker) hangs\n * the Turbopack build (`pnpm check:worker-cycles`).\n */\nimport { parseCsvText, type CsvImportLimits, type CsvImportPreview } from \"./csv-import\";\n\nexport type CsvWorkerRequest = { text: string; limits: CsvImportLimits };\nexport type CsvWorkerResponse =\n | { ok: true; preview: CsvImportPreview }\n | { ok: false; message: string };\n\nself.onmessage = (event: MessageEvent<CsvWorkerRequest>) => {\n let response: CsvWorkerResponse;\n try {\n response = { ok: true, preview: parseCsvText(event.data.text, event.data.limits) };\n } catch (error) {\n response = {\n ok: false,\n message: error instanceof Error ? error.message : \"The CSV could not be read. Fix the file and try again.\",\n };\n }\n (self as unknown as { postMessage(message: CsvWorkerResponse): void }).postMessage(response);\n};\n"],"mappings":"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","names":[]}
@@ -8499,7 +8499,7 @@ e(setDefaultConfiguration);
8499
8499
  var safe = (value) => Number.isSafeInteger(value) && value >= 0;
8500
8500
  var unsafeName = (name) => /[\\\0-\x1F\x7F-\x9F]/.test(name);
8501
8501
  function stopIfCancelled(signal) {
8502
- if (signal?.aborted) throw new Error("The archive import was cancelled.");
8502
+ if (signal?.aborted) throw new Error("The archive import was canceled.");
8503
8503
  }
8504
8504
  function hasWarnings(reader, entry) {
8505
8505
  return Boolean(reader.warnings?.length || entry.warnings?.length);
@@ -8600,7 +8600,7 @@ async function openBoundedZip(file, limits, signal) {
8600
8600
  };
8601
8601
  } catch (error) {
8602
8602
  await reader.close();
8603
- if (signal?.aborted) throw new Error("The archive import was cancelled.");
8603
+ if (signal?.aborted) throw new Error("The archive import was canceled.");
8604
8604
  throw error;
8605
8605
  }
8606
8606
  }
@@ -8649,7 +8649,7 @@ async function readOnePuxArchive(file, limits, signal) {
8649
8649
  if (!attributesText || !dataText) invalid();
8650
8650
  return { attributesText, dataText, attachmentMemberCount };
8651
8651
  } catch (error) {
8652
- if (signal?.aborted) throw new Error("The 1Password import was cancelled.");
8652
+ if (signal?.aborted) throw new Error("The 1Password import was canceled.");
8653
8653
  if (error instanceof Error && /size limit/.test(error.message))
8654
8654
  throw new Error(
8655
8655
  "The 1Password export exceeds this organization\u2019s import size limit."