@ai-matrx/vault 0.1.33 → 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.
@@ -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":";AA+CA,IAAM,eAAe;AAGrB,IAAM,gBAAgB;AAMf,SAAS,cAAc,MAAuB;AACnD,SAAO,aAAa,KAAK,IAAI,KAAK,CAAC,cAAc,KAAK,IAAI;AAC5D;AAQO,SAAS,cAAc,MAAuB;AACnD,MAAI,IAAI;AACR,SAAO,KAAK,CAAC,MAAM,KAAK;AACtB,QAAI,cAAc,KAAK,MAAM,IAAI,CAAC,CAAC,EAAG,QAAO;AAC7C;AAAA,EACF;AACA,SAAO;AACT;AAGO,SAAS,qBAAqB,MAAsB;AACzD,SAAO,cAAc,IAAI,IAAI,KAAK,MAAM,CAAC,IAAI;AAC/C;AAgHA,IAAM,aAAa,OAAO,aAAa,EAAE;AACzC,IAAM,WAAW,OAAO,aAAa,EAAE;AACvC,IAAM,mBAAmB,CAAC,KAAK,KAAM,KAAK,KAAK,YAAY,QAAQ;AACnE,IAAM,QAAQ;AACd,IAAM,WACJ;AAEF,SAAS,eAAe,SAAyB;AAE/C,QAAM,OAAO,QAAQ,SAAS,GAAG,IAAI,QAAQ,QAAQ,YAAY,IAAI,IAAI;AACzE,QAAM,OAAO,KAAK,QAAQ,MAAM;AAChC,QAAM,KAAK,KAAK,QAAQ,IAAI;AAC5B,QAAM,KAAK,KAAK,QAAQ,IAAI;AAC5B,MAAI,SAAS,MAAM,SAAS,GAAI,QAAO;AACvC,MAAI,OAAO,OAAO,OAAO,MAAM,KAAK,IAAK,QAAO;AAChD,SAAO;AACT;AAGA,SAAS,SACP,MACA,WACA,WACA,UAAU,UACV,cAAoC,UACiB;AACrD,QAAM,OAAmB,CAAC;AAC1B,QAAM,SAAgC,CAAC;AACvC,MAAI,SAAS,GAAI,QAAO,EAAE,MAAM,OAAO;AACvC,MAAI,MAAgB,CAAC;AACrB,MAAI,IAAI;AACR,QAAM,IAAI,KAAK;AACf,QAAM,KAAK,UAAU;AACrB,QAAM,KAAK,UAAU;AACrB,SAAO,KAAK,KAAK,KAAK,SAAS,SAAS;AACtC,QAAI,KAAK,WAAW,KAAK,CAAC,GAAG;AAE3B,UAAI,QAAQ;AACZ,UAAI,IAAI,IAAI;AACZ,UAAI,SAAS;AACb,UAAI,YAAY;AAChB,aAAO,IAAI,GAAG;AACZ,cAAM,IAAI,KAAK,QAAQ,KAAK,CAAC;AAC7B,YAAI,MAAM,GAAI;AACd,iBAAS,KAAK,MAAM,GAAG,CAAC;AACxB,YAAI,KAAK,IAAI,CAAC,MAAM,KAAK;AACvB,mBAAS;AACT,cAAI,IAAI;AACR;AAAA,QACF;AACA,cAAM,QAAQ,IAAI;AAClB,YAAI,SAAS,KAAK,KAAK,WAAW,WAAW,KAAK,KAAK,KAAK,WAAW,WAAW,KAAK,GAAG;AACxF,mBAAS;AACT,cAAI;AACJ;AAAA,QACF;AACA,eAAO,KAAK,EAAE,MAAM,UAAU,MAAM,iBAAiB,SAAS,+CAA+C,KAAK,KAAK,OAAO,CAAC;AAC/H,oBAAY;AACZ,YAAI,gBAAgB,UAAW;AAC/B,iBAAS;AACT,YAAI;AAAA,MACN;AACA,UAAI,CAAC,UAAU,CAAC;AACd,eAAO,KAAK,EAAE,MAAM,UAAU,MAAM,iBAAiB,SAAS,6BAA6B,KAAK,KAAK,OAAO,CAAC;AAC/G,UAAI,gBAAgB,YAAY,QAAQ;AACtC,YAAI,CAAC,QAAQ;AACX,mBAAS,KAAK,MAAM,CAAC;AACrB,cAAI,KAAK,KAAK;AACd,eAAK,KAAK,GAAG;AACb,iBAAO,EAAE,MAAM,OAAO;AAAA,QACxB;AACA,YAAI,KAAK,KAAK;AACd,YAAI;AACJ,YAAI,KAAK,GAAG;AACV,eAAK,KAAK,GAAG;AACb;AAAA,QACF;AACA,YAAI,KAAK,WAAW,WAAW,CAAC,GAAG;AACjC,eAAK;AACL,cAAI,KAAK,GAAG;AACV,gBAAI,KAAK,EAAE;AACX,iBAAK,KAAK,GAAG;AACb;AAAA,UACF;AACA;AAAA,QACF;AACA,aAAK,KAAK,GAAG;AACb,cAAM,CAAC;AACP,aAAK;AACL,YAAI,KAAK,GAAG;AACV,eAAK,KAAK,CAAC,EAAE,CAAC;AACd;AAAA,QACF;AACA;AAAA,MACF;AAAA,IAGF;AACA,UAAM,YAAY,KAAK,QAAQ,WAAW,CAAC;AAC3C,UAAM,YAAY,KAAK,QAAQ,WAAW,CAAC;AAC3C,QAAI,cAAc,OAAO,cAAc,MAAM,YAAY,YAAY;AACnE,UAAI,KAAK,KAAK,MAAM,GAAG,SAAS,CAAC;AACjC,UAAI,YAAY;AAChB,UAAI,KAAK,GAAG;AACV,YAAI,KAAK,EAAE;AACX,aAAK,KAAK,GAAG;AACb;AAAA,MACF;AACA;AAAA,IACF;AACA,QAAI,cAAc,IAAI;AACpB,UAAI,KAAK,KAAK,MAAM,GAAG,SAAS,CAAC;AACjC,WAAK,KAAK,GAAG;AACb,YAAM,CAAC;AACP,UAAI,YAAY;AAChB,UAAI,KAAK,GAAG;AACV,aAAK,KAAK,CAAC,EAAE,CAAC;AACd;AAAA,MACF;AACA;AAAA,IACF;AACA,QAAI,KAAK,KAAK,MAAM,CAAC,CAAC;AACtB,SAAK,KAAK,GAAG;AACb;AAAA,EACF;AACA,SAAO,EAAE,MAAM,OAAO;AACxB;AAEA,IAAM,aAAa,CAAC,KAAwB,SAC1C,SAAS,WAAW,IAAI,KAAK,EAAE,EAAE,KAAK,MAAM,KAAK,IAAI,WAAW,KAAK,IAAI,CAAC,EAAG,WAAW;AAE1F,SAAS,eAAe,MAAc,WAAmB,MAAkF;AACzI,MAAI;AACJ,MAAI;AACJ,MAAI;AACJ,aAAW,SAAS,kBAAkB;AACpC,UAAM,EAAE,KAAK,IAAI,SAAS,MAAM,OAAO,WAAW,EAAE;AACpD,QAAI,QAAQ;AACZ,QAAI,MAAM;AACV,QAAI,QAAQ;AACZ,QAAI;AACJ,eAAW,OAAO,MAAM;AACtB,UAAI,QAAQ,WAAW,KAAK,IAAI,GAAG;AACjC;AACA;AAAA,MACF;AACA,YAAM,QAAQ,IAAI;AAClB,aAAO;AACP,UAAI,SAAS,QAAW;AACtB,eAAO;AACP;AAAA,MACF;AACA,UAAI,QAAQ,GAAG;AACb,iBAAS,KAAK,IAAI,QAAQ,IAAI;AAC9B,eAAO;AAAA,MACT;AAAA,IACF;AACA,QAAI,KAAK,SAAS,EAAG,QAAO,KAAK,SAAS;AAC1C,SAAK,cAAc,UAAa,SAAS,eAAe,YAAY,UAAa,MAAM,YAAY,MAAM,MAAM;AAC7G,kBAAY;AACZ,gBAAU;AACV,aAAO;AAAA,IACT;AAAA,EACF;AACA,SAAO,EAAE,WAAW,QAAQ,KAAK,YAAY,SAAS,OAAU;AAClE;AAEA,SAAS,MAAM,OAAoC;AACjD,MAAI,UAAU,UAAU,UAAU,OAAQ,QAAO;AACjD,MAAI,UAAU,WAAW,UAAU,QAAS,QAAO;AACnD,MAAI,MAAM,KAAK,KAAK,GAAG;AACrB,UAAM,MAAM,WAAW,KAAK;AAC5B,QAAI,MAAM,EAAE,KAAK,OAAO,MAAM,KAAK,GAAI,QAAO;AAC9C,WAAO;AAAA,EACT;AACA,MAAI,SAAS,KAAK,KAAK,EAAG,QAAO,IAAI,KAAK,KAAK;AAC/C,SAAO,UAAU,KAAK,OAAO;AAC/B;AAQO,SAAS,eAAe,MAAc,UAAwD,CAAC,GAA6B;AACjI,QAAM,QAAQ,KAAK,WAAW,CAAC,MAAM,QAAS,KAAK,MAAM,CAAC,IAAI;AAC9D,QAAM,YAAY,eAAe,KAAK;AACtC,QAAM,SAAgC,CAAC;AACvC,MAAI,YAAY,QAAQ;AACxB,MAAI,CAAC,WAAW;AACd,UAAM,QAAQ,eAAe,OAAO,WAAW,QAAQ,cAAc;AACrE,gBAAY,MAAM;AAClB,QAAI,CAAC,MAAM;AACT,aAAO,KAAK,EAAE,MAAM,aAAa,MAAM,yBAAyB,SAAS,+DAA+D,CAAC;AAAA,EAC7I;AACA,QAAM,SAAS,SAAS,OAAO,WAAW,WAAW,UAAU,QAAQ,WAAW;AAClF,SAAO,QAAQ,GAAG,OAAO,MAAM;AAC/B,MAAI,OAAO,QAAQ,oBAAoB,QAAQ,OAAO,OAAO,OAAO,KAAK,IAAI,CAAC,QAAQ,IAAI,IAAI,oBAAoB,CAAC;AACnH,MAAI,QAAQ,eAAgB,QAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,WAAW,KAAK,QAAQ,cAAe,CAAC;AACjG,QAAM,OAAsB,EAAE,WAAW,UAAU;AACnD,MAAI,CAAC,QAAQ,QAAQ;AACnB,WAAO,EAAE,MAAM,QAAQ,gBAAgB,KAAK,IAAI,CAAC,QAAQ,IAAI,IAAI,KAAK,CAAC,IAAI,MAAM,QAAQ,KAAK;AAAA,EAChG;AACA,QAAM,CAAC,OAAO,CAAC,GAAG,GAAG,IAAI,IAAI;AAC7B,QAAM,OAAO,oBAAI,IAAoB;AACrC,QAAM,SAAS,KAAK,IAAI,CAAC,KAAK,UAAU;AACtC,UAAM,OAAO,QAAQ,kBAAkB,QAAQ,gBAAgB,KAAK,KAAK,IAAI;AAC7E,UAAM,QAAQ,KAAK,IAAI,IAAI,KAAK;AAChC,SAAK,IAAI,MAAM,QAAQ,CAAC;AACxB,WAAO,UAAU,IAAI,OAAO,GAAG,IAAI,IAAI,KAAK;AAAA,EAC9C,CAAC;AACD,OAAK,SAAS;AACd,QAAM,OAAO,KAAK,IAAI,CAAC,KAAK,UAAU;AACpC,UAAM,SAAkC,CAAC;AACzC,QAAI,QAAQ,CAAC,OAAO,MAAM;AACxB,YAAM,OAAO,QAAQ,gBAAgB,MAAM,KAAK,IAAI;AACpD,UAAI,KAAK,OAAO,QAAQ;AACtB,SAAE,OAAO,mBAA6C,CAAC,GAAG,KAAK,IAAI;AAAA,MACrE,MAAO,QAAO,OAAO,CAAC,CAAE,IAAI;AAAA,IAC9B,CAAC;AACD,QAAI,IAAI,SAAS,OAAO;AACtB,aAAO,KAAK,EAAE,MAAM,iBAAiB,MAAM,iBAAiB,SAAS,6BAA6B,OAAO,MAAM,sBAAsB,IAAI,MAAM,IAAI,KAAK,MAAM,CAAC;AAAA,aACxJ,IAAI,SAAS,OAAO;AAC3B,aAAO,KAAK,EAAE,MAAM,iBAAiB,MAAM,gBAAgB,SAAS,4BAA4B,OAAO,MAAM,sBAAsB,IAAI,MAAM,IAAI,KAAK,MAAM,CAAC;AAC/J,WAAO;AAAA,EACT,CAAC;AACD,SAAO,EAAE,MAAM,QAAQ,KAAK;AAC9B;;;ACnWA,SAAS,eAAe,OAAuB;AAC7C,MAAI,OAAO,gBAAgB;AACzB,WAAO,IAAI,YAAY,EAAE,OAAO,KAAK,EAAE;AACzC,SAAO,SAAS,mBAAmB,KAAK,CAAC,EAAE;AAC7C;AAEA,SAAS,eAAe,MAAsB;AAG5C,SAAO,KAAK,QAAQ,WAAW,EAAE,EAAE,QAAQ,mBAAmB,EAAE;AAClE;AAEO,SAAS,aACd,MACA,QACkB;AAClB,MAAI,eAAe,IAAI,IAAI,OAAO;AAChC,UAAM,IAAI,MAAM,8DAAyD;AAC3E,QAAM,SAAS,eAAe,eAAe,IAAI,GAAG;AAAA,IAClD,WAAW;AAAA,IACX,gBAAgB;AAAA,EAClB,CAAC;AACD,MACE,OAAO,OAAO;AAAA,IACZ,CAAC,UACC,MAAM,SAAS,kBAAkB,MAAM,SAAS;AAAA,EACpD;AAEA,UAAM,IAAI,MAAM,wDAAwD;AAC1E,SAAO,kBAAkB,OAAO,MAAM,MAAM;AAC9C;AAEA,SAAS,kBACP,QACA,QACkB;AAClB,QAAM,CAAC,SAAS,GAAG,IAAI,IAAI;AAC3B,MAAI,CAAC,SAAS,UAAU,QAAQ,SAAS,OAAO;AAC9C,UAAM,IAAI,MAAM,+CAA+C;AACjE,MAAI,KAAK,SAAS,OAAO;AACvB,UAAM,IAAI,MAAM,yDAAyD;AAC3E,QAAM,qBAAqB,QAAQ;AAAA,IAAU,CAAC,WAC5C,kCAAkC,KAAK,OAAO,KAAK,CAAC;AAAA,EACtD;AACA,QAAM,0BAA0B,QAC7B;AAAA,IAAI,CAAC,QAAQ,UACZ,mEAAmE;AAAA,MACjE,OAAO,KAAK;AAAA,IACd,IACI,QACA;AAAA,EACN,EACC,OAAO,CAAC,UAAU,SAAS,CAAC;AAC/B,MAAI,SAAS;AACb,QAAM,OAAO,KAAK,IAAI,CAAC,OAAO,UAAU;AACtC,UAAM,UACJ,MAAM,WAAW,QAAQ,UACzB,MAAM,KAAK,CAAC,SAAS,eAAe,IAAI,IAAI,OAAO,YAAY,KAC/D,MAAM,MAAM,CAAC,SAAS,KAAK,KAAK,EAAE,WAAW,CAAC;AAChD,UAAM,OACJ,sBAAsB,KACjB,MAAM,kBAAkB,KAAK,IAAI,KAAK,EAAE,YAAY,IACrD;AACN,UAAM,cACJ,CAAC,YACA,wBAAwB;AAAA,MAAK,CAAC,WAC7B,QAAQ,MAAM,MAAM,GAAG,KAAK,CAAC;AAAA,IAC/B,KACG,QAAQ,IAAI,KACX,CAAC,4CAA4C,KAAK,IAAI;AAC5D,QAAI,WAAW,YAAa,WAAU;AACtC,WAAO;AAAA,MACL,WAAW,QAAQ;AAAA,MACnB;AAAA,MACA,OAAO,UACF,YACD,cACG,gBACD;AAAA,IACR;AAAA,EACF,CAAC;AACD,SAAO,EAAE,SAAS,MAAM,OAAO;AACjC;;;ACrIA,KAAK,YAAY,CAAC,UAA0C;AAC1D,MAAI;AACJ,MAAI;AACF,eAAW,EAAE,IAAI,MAAM,SAAS,aAAa,MAAM,KAAK,MAAM,MAAM,KAAK,MAAM,EAAE;AAAA,EACnF,SAAS,OAAO;AACd,eAAW;AAAA,MACT,IAAI;AAAA,MACJ,SAAS,iBAAiB,QAAQ,MAAM,UAAU;AAAA,IACpD;AAAA,EACF;AACA,EAAC,KAAsE,YAAY,QAAQ;AAC7F;","names":[]}
@@ -4436,7 +4436,7 @@ var init_zip_reader = __esm({
4436
4436
  }
4437
4437
  });
4438
4438
 
4439
- // ../../node_modules/.pnpm/lossless-json@4.3.1/node_modules/lossless-json/lib/esm/utils.js
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
  }
@@ -4450,6 +4450,9 @@ function isSafeNumber(value, config3) {
4450
4450
  return Number.isSafeInteger(Number.parseInt(value, 10));
4451
4451
  }
4452
4452
  const num = Number.parseFloat(value);
4453
+ if (Number.isInteger(num) && !Number.isSafeInteger(num)) {
4454
+ return false;
4455
+ }
4453
4456
  const parsed = String(num);
4454
4457
  if (value === parsed) {
4455
4458
  return true;
@@ -4461,7 +4464,7 @@ function isSafeNumber(value, config3) {
4461
4464
  }
4462
4465
  if (config3?.approx === true) {
4463
4466
  const requiredDigits = 14;
4464
- if (!isInteger(value) && parsedDigits.length >= requiredDigits && valueDigits.startsWith(parsedDigits.substring(0, requiredDigits))) {
4467
+ if (parsedDigits.length >= requiredDigits && valueDigits.startsWith(parsedDigits.substring(0, requiredDigits))) {
4465
4468
  return true;
4466
4469
  }
4467
4470
  }
@@ -4528,11 +4531,21 @@ function getSignificantDigitRange(value) {
4528
4531
  };
4529
4532
  }
4530
4533
 
4531
- // ../../node_modules/.pnpm/lossless-json@4.3.1/node_modules/lossless-json/lib/esm/LosslessNumber.js
4534
+ // ../../node_modules/.pnpm/lossless-json@4.3.3_patch_hash=3d667bc403737067026a4025cf7583b9ec434c0318d7d958811f88333c596dfb/node_modules/lossless-json/lib/esm/LosslessNumber.js
4535
+ var losslessNumberMarker = /* @__PURE__ */ Symbol.for("lossless-json.LosslessNumber");
4532
4536
  var LosslessNumber = class {
4533
4537
  // numeric value as string
4534
- // type information
4538
+ value;
4539
+ /**
4540
+ * @deprecated Use the isLosslessNumber(value) function instead.
4541
+ *
4542
+ * Ordinary objects may also have an isLosslessNumber property, so this
4543
+ * field cannot reliably identify lossless numbers.
4544
+ */
4535
4545
  isLosslessNumber = true;
4546
+ get [losslessNumberMarker]() {
4547
+ return true;
4548
+ }
4536
4549
  constructor(value) {
4537
4550
  if (!isNumber(value)) {
4538
4551
  throw new Error(`Invalid number (value: "${value}")`);
@@ -4571,15 +4584,15 @@ var LosslessNumber = class {
4571
4584
  // stringified JSON since it has to be parsed into a number first.
4572
4585
  };
4573
4586
  function isLosslessNumber(value) {
4574
- return value && typeof value === "object" && value.isLosslessNumber || false;
4587
+ return value !== null && typeof value === "object" && value[losslessNumberMarker] === true;
4575
4588
  }
4576
4589
 
4577
- // ../../node_modules/.pnpm/lossless-json@4.3.1/node_modules/lossless-json/lib/esm/numberParsers.js
4590
+ // ../../node_modules/.pnpm/lossless-json@4.3.3_patch_hash=3d667bc403737067026a4025cf7583b9ec434c0318d7d958811f88333c596dfb/node_modules/lossless-json/lib/esm/numberParsers.js
4578
4591
  function parseLosslessNumber(value) {
4579
4592
  return new LosslessNumber(value);
4580
4593
  }
4581
4594
 
4582
- // ../../node_modules/.pnpm/lossless-json@4.3.1/node_modules/lossless-json/lib/esm/revive.js
4595
+ // ../../node_modules/.pnpm/lossless-json@4.3.3_patch_hash=3d667bc403737067026a4025cf7583b9ec434c0318d7d958811f88333c596dfb/node_modules/lossless-json/lib/esm/revive.js
4583
4596
  function revive(json, reviver) {
4584
4597
  return reviveValue({
4585
4598
  "": json
@@ -4607,12 +4620,17 @@ function reviveObject(object3, reviver) {
4607
4620
  }
4608
4621
  function reviveArray(array, reviver) {
4609
4622
  for (let i = 0; i < array.length; i++) {
4610
- array[i] = reviveValue(array, String(i), array[i], reviver);
4623
+ const value = reviveValue(array, String(i), array[i], reviver);
4624
+ if (value === void 0) {
4625
+ delete array[i];
4626
+ } else {
4627
+ array[i] = value;
4628
+ }
4611
4629
  }
4612
4630
  return array;
4613
4631
  }
4614
4632
 
4615
- // ../../node_modules/.pnpm/lossless-json@4.3.1/node_modules/lossless-json/lib/esm/parse.js
4633
+ // ../../node_modules/.pnpm/lossless-json@4.3.3_patch_hash=3d667bc403737067026a4025cf7583b9ec434c0318d7d958811f88333c596dfb/node_modules/lossless-json/lib/esm/parse.js
4616
4634
  function parse(text, reviver, options) {
4617
4635
  const optionsObj = typeof options === "function" ? {
4618
4636
  parseNumber: options
@@ -4650,7 +4668,7 @@ function parse(text, reviver, options) {
4650
4668
  throwObjectValueExpected();
4651
4669
  return;
4652
4670
  }
4653
- if (Object.prototype.hasOwnProperty.call(object3, key) && !isDeepEqual(value2, object3[key])) {
4671
+ if (Object.prototype.hasOwnProperty.call(object3, key)) {
4654
4672
  const returnedValue = onDuplicateKey({
4655
4673
  key,
4656
4674
  position: start + 1,
@@ -4658,10 +4676,10 @@ function parse(text, reviver, options) {
4658
4676
  newValue: value2
4659
4677
  });
4660
4678
  if (returnedValue !== void 0) {
4661
- object3[key] = returnedValue;
4679
+ setObjectKey(object3, key, returnedValue);
4662
4680
  }
4663
4681
  } else {
4664
- object3[key] = value2;
4682
+ setObjectKey(object3, key, value2);
4665
4683
  }
4666
4684
  }
4667
4685
  if (text.charCodeAt(i) !== codeClosingBrace) {
@@ -4759,6 +4777,8 @@ function parse(text, reviver, options) {
4759
4777
  while (isDigit(text.charCodeAt(i))) {
4760
4778
  i++;
4761
4779
  }
4780
+ } else {
4781
+ return void 0;
4762
4782
  }
4763
4783
  if (text.charCodeAt(i) === codeDot) {
4764
4784
  i++;
@@ -4822,11 +4842,10 @@ function parse(text, reviver, options) {
4822
4842
  function throwObjectKeyExpected() {
4823
4843
  throw new SyntaxError(`Quoted object key expected ${gotAt()}`);
4824
4844
  }
4825
- function throwDuplicateKey(_ref) {
4826
- let {
4827
- key,
4828
- position
4829
- } = _ref;
4845
+ function throwDuplicateKey({
4846
+ key,
4847
+ position
4848
+ }) {
4830
4849
  throw new SyntaxError(`Duplicate key '${key}' encountered at position ${position}`);
4831
4850
  }
4832
4851
  function throwObjectKeyOrEndExpected() {
@@ -4859,6 +4878,18 @@ function parse(text, reviver, options) {
4859
4878
  return `${got()} ${pos()}`;
4860
4879
  }
4861
4880
  }
4881
+ function setObjectKey(object3, key, value) {
4882
+ if (key === "__proto__") {
4883
+ Object.defineProperty(object3, key, {
4884
+ value,
4885
+ writable: true,
4886
+ enumerable: true,
4887
+ configurable: true
4888
+ });
4889
+ } else {
4890
+ object3[key] = value;
4891
+ }
4892
+ }
4862
4893
  function isWhitespace(code) {
4863
4894
  return code === codeSpace || code === codeNewline || code === codeTab || code === codeReturn;
4864
4895
  }
@@ -4874,22 +4905,6 @@ function isNonZeroDigit(code) {
4874
4905
  function isValidStringCharacter(code) {
4875
4906
  return code >= 32 && code <= 1114111;
4876
4907
  }
4877
- function isDeepEqual(a, b2) {
4878
- if (a === b2) {
4879
- return true;
4880
- }
4881
- if (Array.isArray(a) && Array.isArray(b2)) {
4882
- return a.length === b2.length && a.every((item, index) => isDeepEqual(item, b2[index]));
4883
- }
4884
- if (isObject(a) && isObject(b2)) {
4885
- const keys = [.../* @__PURE__ */ new Set([...Object.keys(a), ...Object.keys(b2)])];
4886
- return keys.every((key) => isDeepEqual(a[key], b2[key]));
4887
- }
4888
- return false;
4889
- }
4890
- function isObject(value) {
4891
- return typeof value === "object" && value !== null;
4892
- }
4893
4908
  var escapeCharacters = {
4894
4909
  '"': '"',
4895
4910
  "\\": "\\",
@@ -4926,7 +4941,7 @@ var codeLowercaseE = 101;
4926
4941
  var codeUppercaseF = 70;
4927
4942
  var codeLowercaseF = 102;
4928
4943
 
4929
- // ../../node_modules/.pnpm/lossless-json@4.3.1/node_modules/lossless-json/lib/esm/stringify.js
4944
+ // ../../node_modules/.pnpm/lossless-json@4.3.3_patch_hash=3d667bc403737067026a4025cf7583b9ec434c0318d7d958811f88333c596dfb/node_modules/lossless-json/lib/esm/stringify.js
4930
4945
  function stringify(value, replacer, space, numberStringifiers) {
4931
4946
  const resolvedSpace = resolveSpace(space);
4932
4947
  const replacedValue = typeof replacer === "function" ? replacer.call({
@@ -4947,7 +4962,7 @@ function stringify(value, replacer, space, numberStringifiers) {
4947
4962
  if (typeof value2 === "boolean" || typeof value2 === "number" || typeof value2 === "string" || value2 === null || value2 instanceof Date || value2 instanceof Boolean || value2 instanceof Number || value2 instanceof String) {
4948
4963
  return JSON.stringify(value2);
4949
4964
  }
4950
- if (value2?.isLosslessNumber) {
4965
+ if (isLosslessNumber(value2)) {
4951
4966
  return value2.toString();
4952
4967
  }
4953
4968
  if (typeof value2 === "bigint") {
@@ -4973,7 +4988,8 @@ function stringify(value, replacer, space, numberStringifiers) {
4973
4988
  str += childIndent;
4974
4989
  }
4975
4990
  if (typeof item !== "undefined" && typeof item !== "function") {
4976
- str += stringifyValue(item, childIndent);
4991
+ const serialized = stringifyValue(item, childIndent);
4992
+ str += serialized === void 0 ? "null" : serialized;
4977
4993
  } else {
4978
4994
  str += "null";
4979
4995
  }
@@ -4987,9 +5003,9 @@ ${indent}]` : "]";
4987
5003
  }
4988
5004
  function stringifyObject(object3, indent) {
4989
5005
  if (typeof object3.toJSON === "function") {
4990
- return stringify(object3.toJSON(), replacer, space, void 0);
5006
+ return stringifyValue(object3.toJSON(), indent);
4991
5007
  }
4992
- const keys = Array.isArray(replacer) ? replacer.map(String) : Object.keys(object3);
5008
+ const keys = Array.isArray(replacer) ? [...new Set(replacer.map(String))] : Object.keys(object3);
4993
5009
  if (keys.length === 0) {
4994
5010
  return "{}";
4995
5011
  }
@@ -4999,6 +5015,10 @@ ${indent}]` : "]";
4999
5015
  for (const key of keys) {
5000
5016
  const value2 = typeof replacer === "function" ? replacer.call(object3, key, object3[key]) : object3[key];
5001
5017
  if (includeProperty(key, value2)) {
5018
+ const serialized = stringifyValue(value2, childIndent);
5019
+ if (serialized === void 0) {
5020
+ continue;
5021
+ }
5002
5022
  if (first) {
5003
5023
  first = false;
5004
5024
  } else {
@@ -5006,7 +5026,7 @@ ${indent}]` : "]";
5006
5026
  }
5007
5027
  const keyStr = JSON.stringify(key);
5008
5028
  str += resolvedSpace ? `${childIndent + keyStr}: ` : `${keyStr}:`;
5009
- str += stringifyValue(value2, childIndent);
5029
+ str += serialized;
5010
5030
  }
5011
5031
  }
5012
5032
  str += resolvedSpace ? `
@@ -5017,12 +5037,14 @@ ${indent}}` : "}";
5017
5037
  return typeof value2 !== "undefined" && typeof value2 !== "function" && typeof value2 !== "symbol";
5018
5038
  }
5019
5039
  }
5040
+ var MAX_SPACE_LENGTH = 10;
5020
5041
  function resolveSpace(space) {
5021
5042
  if (typeof space === "number") {
5022
- return " ".repeat(space);
5043
+ const count = Math.min(Math.trunc(space), MAX_SPACE_LENGTH);
5044
+ return count >= 1 ? " ".repeat(count) : void 0;
5023
5045
  }
5024
5046
  if (typeof space === "string" && space !== "") {
5025
- return space;
5047
+ return space.slice(0, MAX_SPACE_LENGTH);
5026
5048
  }
5027
5049
  return void 0;
5028
5050
  }