@ai-matrx/vault 0.1.34 → 0.1.35
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.
- package/CHANGELOG.md +23 -1
- package/dist/identity.cjs.map +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/io/bitwarden-json.worker.js +7 -23
- package/dist/io/bitwarden-json.worker.js.map +1 -1
- package/dist/io/csv-import.worker.js.map +1 -1
- package/dist/io/onepux.worker.js +7 -23
- package/dist/io/onepux.worker.js.map +1 -1
- package/dist/io/proton-pass.worker.js +7 -23
- package/dist/io/proton-pass.worker.js.map +1 -1
- package/package.json +5 -5
|
@@ -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 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":[]}
|
|
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 /** Keep an existing quoted field quoted, even when its new value needs no escaping. */\n forceQuote?: 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: options.forceQuote ? '\"\"' : \"\", 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 (options.forceQuote || 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":[]}
|
package/dist/io/onepux.worker.js
CHANGED
|
@@ -4436,7 +4436,7 @@ var init_zip_reader = __esm({
|
|
|
4436
4436
|
}
|
|
4437
4437
|
});
|
|
4438
4438
|
|
|
4439
|
-
// ../../node_modules/.pnpm/lossless-json@4.3.
|
|
4439
|
+
// ../../node_modules/.pnpm/lossless-json@4.3.3_patch_hash=3d667bc403737067026a4025cf7583b9ec434c0318d7d958811f88333c596dfb/node_modules/lossless-json/lib/esm/utils.js
|
|
4440
4440
|
function isInteger(value) {
|
|
4441
4441
|
return INTEGER_REGEX.test(value);
|
|
4442
4442
|
}
|
|
@@ -4531,7 +4531,7 @@ function getSignificantDigitRange(value) {
|
|
|
4531
4531
|
};
|
|
4532
4532
|
}
|
|
4533
4533
|
|
|
4534
|
-
// ../../node_modules/.pnpm/lossless-json@4.3.
|
|
4534
|
+
// ../../node_modules/.pnpm/lossless-json@4.3.3_patch_hash=3d667bc403737067026a4025cf7583b9ec434c0318d7d958811f88333c596dfb/node_modules/lossless-json/lib/esm/LosslessNumber.js
|
|
4535
4535
|
var losslessNumberMarker = /* @__PURE__ */ Symbol.for("lossless-json.LosslessNumber");
|
|
4536
4536
|
var LosslessNumber = class {
|
|
4537
4537
|
// numeric value as string
|
|
@@ -4587,12 +4587,12 @@ function isLosslessNumber(value) {
|
|
|
4587
4587
|
return value !== null && typeof value === "object" && value[losslessNumberMarker] === true;
|
|
4588
4588
|
}
|
|
4589
4589
|
|
|
4590
|
-
// ../../node_modules/.pnpm/lossless-json@4.3.
|
|
4590
|
+
// ../../node_modules/.pnpm/lossless-json@4.3.3_patch_hash=3d667bc403737067026a4025cf7583b9ec434c0318d7d958811f88333c596dfb/node_modules/lossless-json/lib/esm/numberParsers.js
|
|
4591
4591
|
function parseLosslessNumber(value) {
|
|
4592
4592
|
return new LosslessNumber(value);
|
|
4593
4593
|
}
|
|
4594
4594
|
|
|
4595
|
-
// ../../node_modules/.pnpm/lossless-json@4.3.
|
|
4595
|
+
// ../../node_modules/.pnpm/lossless-json@4.3.3_patch_hash=3d667bc403737067026a4025cf7583b9ec434c0318d7d958811f88333c596dfb/node_modules/lossless-json/lib/esm/revive.js
|
|
4596
4596
|
function revive(json, reviver) {
|
|
4597
4597
|
return reviveValue({
|
|
4598
4598
|
"": json
|
|
@@ -4630,7 +4630,7 @@ function reviveArray(array, reviver) {
|
|
|
4630
4630
|
return array;
|
|
4631
4631
|
}
|
|
4632
4632
|
|
|
4633
|
-
// ../../node_modules/.pnpm/lossless-json@4.3.
|
|
4633
|
+
// ../../node_modules/.pnpm/lossless-json@4.3.3_patch_hash=3d667bc403737067026a4025cf7583b9ec434c0318d7d958811f88333c596dfb/node_modules/lossless-json/lib/esm/parse.js
|
|
4634
4634
|
function parse(text, reviver, options) {
|
|
4635
4635
|
const optionsObj = typeof options === "function" ? {
|
|
4636
4636
|
parseNumber: options
|
|
@@ -4668,7 +4668,7 @@ function parse(text, reviver, options) {
|
|
|
4668
4668
|
throwObjectValueExpected();
|
|
4669
4669
|
return;
|
|
4670
4670
|
}
|
|
4671
|
-
if (Object.prototype.hasOwnProperty.call(object3, key)
|
|
4671
|
+
if (Object.prototype.hasOwnProperty.call(object3, key)) {
|
|
4672
4672
|
const returnedValue = onDuplicateKey({
|
|
4673
4673
|
key,
|
|
4674
4674
|
position: start + 1,
|
|
@@ -4905,22 +4905,6 @@ function isNonZeroDigit(code) {
|
|
|
4905
4905
|
function isValidStringCharacter(code) {
|
|
4906
4906
|
return code >= 32 && code <= 1114111;
|
|
4907
4907
|
}
|
|
4908
|
-
function isDeepEqual(a, b2) {
|
|
4909
|
-
if (a === b2) {
|
|
4910
|
-
return true;
|
|
4911
|
-
}
|
|
4912
|
-
if (Array.isArray(a) && Array.isArray(b2)) {
|
|
4913
|
-
return a.length === b2.length && a.every((item, index) => isDeepEqual(item, b2[index]));
|
|
4914
|
-
}
|
|
4915
|
-
if (isObject(a) && isObject(b2)) {
|
|
4916
|
-
const keys = [.../* @__PURE__ */ new Set([...Object.keys(a), ...Object.keys(b2)])];
|
|
4917
|
-
return keys.every((key) => isDeepEqual(a[key], b2[key]));
|
|
4918
|
-
}
|
|
4919
|
-
return false;
|
|
4920
|
-
}
|
|
4921
|
-
function isObject(value) {
|
|
4922
|
-
return typeof value === "object" && value !== null;
|
|
4923
|
-
}
|
|
4924
4908
|
var escapeCharacters = {
|
|
4925
4909
|
'"': '"',
|
|
4926
4910
|
"\\": "\\",
|
|
@@ -4957,7 +4941,7 @@ var codeLowercaseE = 101;
|
|
|
4957
4941
|
var codeUppercaseF = 70;
|
|
4958
4942
|
var codeLowercaseF = 102;
|
|
4959
4943
|
|
|
4960
|
-
// ../../node_modules/.pnpm/lossless-json@4.3.
|
|
4944
|
+
// ../../node_modules/.pnpm/lossless-json@4.3.3_patch_hash=3d667bc403737067026a4025cf7583b9ec434c0318d7d958811f88333c596dfb/node_modules/lossless-json/lib/esm/stringify.js
|
|
4961
4945
|
function stringify(value, replacer, space, numberStringifiers) {
|
|
4962
4946
|
const resolvedSpace = resolveSpace(space);
|
|
4963
4947
|
const replacedValue = typeof replacer === "function" ? replacer.call({
|