first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,72 @@
# format-detector
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Provides a single `detect()` function that inspects the raw content of a password manager export file (and an optional filename hint) to identify which supported format it belongs to. This enables the `parse()` function to automatically dispatch to the correct parser without requiring callers to specify the format manually. The detector uses content-based heuristics rather than relying solely on file extensions, making it robust against mislabelled or extension-less files.
## Features
- Content-first detection — inspects actual bytes/characters rather than relying purely on file extensions
- KeePass XML detection via `<KeePassFile` start tag
- Bitwarden JSON detection via `"items"` / `"encrypted"` keys; throws `ENCRYPTED_NOT_SUPPORTED` for encrypted vaults
- Apple Passwords CSV detection via `OTPAuth` + `Title` header columns
- Chrome CSV detection via `name`, `url`, `username`, `password` header columns
- Returns `"unknown"` for unrecognised formats rather than throwing
- Accepts both `string` and `Uint8Array` inputs
## Usage: Basic Example
```ts
import { detect } from "@suaveplan/password-formats/detect";
const format = detect(csvContent, "passwords.csv");
console.log(format); // "apple" | "chrome" | "bitwarden" | "keepass" | "unknown"
```
## Usage: Advanced Example
```ts
import { detect } from "@suaveplan/password-formats/detect";
import { PasswordFormatsError, PASSWORD_FORMATS_CODES } from "@suaveplan/password-formats/errors";
async function importFile(name: string, bytes: Uint8Array): Promise<void> {
let format: string;
try {
format = detect(bytes, name);
} catch (err) {
if (err instanceof PasswordFormatsError && err.code === PASSWORD_FORMATS_CODES.ENCRYPTED_NOT_SUPPORTED) {
console.error("Please export an unencrypted Bitwarden vault before importing.");
return;
}
throw err;
}
if (format === "unknown") {
console.warn("Unrecognised format — defaulting to manual selection");
}
}
```
## API Reference
### `detect`
```ts
function detect(input: string | Uint8Array, filename?: string): PasswordFormat
```
**Parameters:**
- `input` — File contents as a UTF-8 string or raw byte array.
- `filename` — Optional filename (e.g. `"export.json"`). Used as a secondary hint alongside content inspection.
**Returns:** One of `"apple"`, `"bitwarden"`, `"chrome"`, `"keepass"`, or `"unknown"`.
**Throws:** `PasswordFormatsError` with code `PASSWORD_FORMATS.ENCRYPTED_NOT_SUPPORTED` when the input is a Bitwarden encrypted export.
## Implementation Notes
Detection order matters: KeePass XML is checked first because `<KeePassFile` is a highly specific marker. JSON detection comes next and is content-driven — a valid JSON object with an `"items"` key is classified as Bitwarden regardless of file extension. CSV header inspection is last because CSV is the most ambiguous format. The `"unknown"` fallback is intentional: callers can handle it gracefully rather than receiving an unexpected exception for unrecognised files.

View File

@@ -0,0 +1,75 @@
# parse
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Provides the primary entry point for parsing password manager export files in the `@suaveplan/password-formats` package. The `parse()` function accepts raw file content as a string or `Uint8Array`, optionally accepts a format hint, and dispatches to the appropriate format-specific parser after auto-detecting the format when none is specified. It instruments every parse operation with OpenTelemetry counters and histograms so operators can monitor import volume and latency in production.
## Features
- Auto-detects format using `detect()` when `format` is not provided
- Dispatches to `parseApplePasswordsCsv`, `parseBitwardenJson`, `parseChromeCsv`, or `parseKeePassXml`
- Emits OTel counter `password_formats.parse.total{format}` on each call
- Records OTel histogram `password_formats.parse.duration_ms{format}` for latency tracking
- Wraps execution in an OTel span `password_formats.parse` with `format` and `entry_count` attributes
- Returns a `Promise<PasswordEntry[]>` for consistent async interface across all callers
- Accepts both `string` and `Uint8Array` inputs (decoded as UTF-8)
## Usage: Basic Example
```ts
import { parse } from "@suaveplan/password-formats/parse";
const entries = await parse(csvContent);
console.log(entries.length); // Number of parsed entries
```
## Usage: Advanced Example
```ts
import { parse } from "@suaveplan/password-formats/parse";
// Explicit format override + parse options
const entries = await parse(jsonContent, "bitwarden", {
skipInvalid: true,
maxEntries: 500,
});
// Pass a Uint8Array from a file upload
const bytes = new Uint8Array(await file.arrayBuffer());
const entries2 = await parse(bytes, undefined, { skipInvalid: false });
```
## API Reference
### `parse`
```ts
async function parse(
input: string | Uint8Array,
format?: PasswordFormat,
options?: ParseOptions,
): Promise<PasswordEntry[]>
```
**Parameters:**
- `input` — File content as a UTF-8 string or byte array.
- `format` — Optional format override. If omitted, `detect()` is called automatically.
- `options.skipInvalid` — Skip malformed entries instead of throwing. Default: `false`.
- `options.maxEntries` — Cap on the number of returned entries.
**Returns:** Promise resolving to an array of `PasswordEntry` objects.
**Throws:**
- `PasswordFormatsError` (`INVALID_INPUT`) — `input` is empty.
- `PasswordFormatsError` (`UNSUPPORTED_FORMAT`) — Format is `"unknown"` or unrecognised.
- `PasswordFormatsError` (`ENCRYPTED_NOT_SUPPORTED`) — Bitwarden encrypted vault detected.
- `PasswordFormatsError` (`PARSE_ERROR`) — Malformed content and `skipInvalid` is `false`.
## Implementation Notes
The function is `async` even though the underlying parsers are synchronous. This design choice future-proofs the API for streaming or worker-thread implementations without breaking callers. The OTel SDK no-ops gracefully when no global tracer/meter provider is registered, so the instrumentation has zero overhead in environments that do not configure OTel. Duration is measured with `Date.now()` and recorded in milliseconds to keep the instrumentation dependency surface minimal.

View File

@@ -0,0 +1,79 @@
# validate
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Validates an array of `PasswordEntry` objects against the minimum required field constraints for the `@suaveplan/password-formats` package. After parsing a password manager export, callers can run `validate()` to get a structured report of which entries are well-formed and which are not — useful for building import UIs that show per-entry error messages before committing the import. The validator is intentionally permissive: it only enforces the two fields that are universally required (`title` and `password`), leaving all other fields optional.
## Features
- Validates that `title` is a non-empty string (after trimming)
- Validates that `password` is a non-empty string
- Returns a `ValidationResult` with per-entry `{ index, message }` error tuples
- Reports `validCount` and `invalidCount` for summary display
- Sets `valid: true` only when there are zero errors
- Does not mutate the input array
## Usage: Basic Example
```ts
import { validate } from "@suaveplan/password-formats/parse";
import type { PasswordEntry } from "@suaveplan/password-formats";
const entries: PasswordEntry[] = [
{ title: "Gmail", password: "secret" },
{ title: "", password: "pass" },
];
const result = validate(entries);
console.log(result.valid); // false
console.log(result.validCount); // 1
console.log(result.invalidCount); // 1
console.log(result.errors[0]); // { index: 1, message: "Entry at index 1: title is required..." }
```
## Usage: Advanced Example
```ts
import { parse, validate } from "@suaveplan/password-formats";
async function importWithValidation(content: string): Promise<void> {
const entries = await parse(content);
const result = validate(entries);
if (!result.valid) {
console.error(`Import has ${result.invalidCount} invalid entries:`);
for (const err of result.errors) {
console.error(` Row ${err.index + 1}: ${err.message}`);
}
return;
}
console.log(`All ${result.validCount} entries are valid.`);
}
```
## API Reference
### `validate`
```ts
function validate(entries: PasswordEntry[]): ValidationResult
```
**Parameters:**
- `entries` — Array of `PasswordEntry` objects to validate.
**Returns:** `ValidationResult`
- `valid` — `true` if no errors were found.
- `errors` — Array of `{ index: number; message: string }` tuples, one per violation.
- `validCount` — Number of entries with no errors.
- `invalidCount` — Number of entries with at least one error.
## Implementation Notes
An entry can generate multiple error tuples (e.g. missing both `title` and `password`), but it counts as one invalid entry in `invalidCount`. The `invalidCount` is computed by counting distinct indices in the errors array rather than simply using `errors.length`, ensuring accurate summary counts when multiple rules fire on the same entry. The validator does not attempt deep structural validation of fields like `url` format or ISO 8601 compliance — that is the responsibility of the consuming application.

View File

@@ -0,0 +1,86 @@
# password-formats-error
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Defines the stable error code registry and custom `PasswordFormatsError` class used throughout the `@suaveplan/password-formats` package. By centralizing all error codes and the error class in one module, callers can discriminate errors with a stable `code` string rather than relying on fragile message-string matching. Every public API in this package throws `PasswordFormatsError` on failure, ensuring a consistent and predictable error surface.
## Features
- `PASSWORD_FORMATS_CODES` — a `const` object of all stable error codes, namespaced under `PASSWORD_FORMATS.*`
- `PasswordFormatsError` — extends `Error`, carries `name = "PasswordFormatsError"` and a `code: string` property
- `PasswordFormatsCode` — union type for all valid code strings, useful for exhaustive `switch` statements
## Usage: Basic Example
```ts
import { PasswordFormatsError, PASSWORD_FORMATS_CODES } from "@suaveplan/password-formats/errors";
try {
throw new PasswordFormatsError("Encrypted vaults are not supported", PASSWORD_FORMATS_CODES.ENCRYPTED_NOT_SUPPORTED);
} catch (err) {
if (err instanceof PasswordFormatsError) {
console.error(err.code); // "PASSWORD_FORMATS.ENCRYPTED_NOT_SUPPORTED"
}
}
```
## Usage: Advanced Example
```ts
import { PasswordFormatsError, PASSWORD_FORMATS_CODES, type PasswordFormatsCode } from "@suaveplan/password-formats/errors";
function handleError(err: unknown): void {
if (!(err instanceof PasswordFormatsError)) throw err;
const actions: Record<PasswordFormatsCode, () => void> = {
[PASSWORD_FORMATS_CODES.UNSUPPORTED_FORMAT]: () => console.warn("Unknown format — try a different file"),
[PASSWORD_FORMATS_CODES.PARSE_ERROR]: () => console.error("Malformed input"),
[PASSWORD_FORMATS_CODES.INVALID_INPUT]: () => console.error("Input is null or empty"),
[PASSWORD_FORMATS_CODES.ENCRYPTED_NOT_SUPPORTED]: () => console.error("Decrypt before importing"),
};
(actions[err.code as PasswordFormatsCode] ?? (() => { throw err; }))();
}
```
## API Reference
### `PASSWORD_FORMATS_CODES`
```ts
const PASSWORD_FORMATS_CODES: {
UNSUPPORTED_FORMAT: "PASSWORD_FORMATS.UNSUPPORTED_FORMAT";
PARSE_ERROR: "PASSWORD_FORMATS.PARSE_ERROR";
INVALID_INPUT: "PASSWORD_FORMATS.INVALID_INPUT";
ENCRYPTED_NOT_SUPPORTED: "PASSWORD_FORMATS.ENCRYPTED_NOT_SUPPORTED";
}
```
Stable string constants for all error conditions in the package.
### `PasswordFormatsError`
```ts
class PasswordFormatsError extends Error {
readonly code: string;
constructor(message: string, code: string): PasswordFormatsError;
}
```
- `message` — human-readable description of what went wrong
- `code` — one of the `PASSWORD_FORMATS_CODES` values
### `PasswordFormatsCode`
```ts
type PasswordFormatsCode = "PASSWORD_FORMATS.UNSUPPORTED_FORMAT" | "PASSWORD_FORMATS.PARSE_ERROR" | ...;
```
Union type over all valid `PASSWORD_FORMATS_CODES` values.
## Implementation Notes
The error class deliberately does not extend a `@suaveplan/error` base class because `@suaveplan/password-formats` is a leaf package with no `@suaveplan/*` runtime dependencies. This keeps the dependency graph minimal and avoids circular dependencies. The `code` property mirrors the pattern established by `SuaveplanError` so that error-handling middleware in consuming applications can treat both interchangeably when checking `.code`.

View File

@@ -0,0 +1,75 @@
# csv-export
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Exports an array of normalized `PasswordEntry` objects to a universal CSV format that can be imported into most password managers or spreadsheet tools. This module is the reverse operation of the format-specific parsers — it produces a consistent, predictable CSV regardless of the original source format. The output CSV uses a standardized seven-column schema that balances completeness with broad import compatibility.
## Features
- Uses `papaparse.unparse()` for standards-compliant CSV generation including proper quoting
- Exports seven columns: `title`, `url`, `username`, `password`, `notes`, `folder`, `tags`
- Tags array is serialized as a comma-separated string within the column
- Missing optional fields are exported as empty strings (not omitted)
- Always includes a header row
## Usage: Basic Example
```ts
import { exportToCsv } from "@suaveplan/password-formats/export";
import type { PasswordEntry } from "@suaveplan/password-formats";
const entries: PasswordEntry[] = [
{ title: "Gmail", password: "secret", url: "https://gmail.com", username: "me@gmail.com" },
];
const csv = exportToCsv(entries);
console.log(csv);
// title,url,username,password,notes,folder,tags
// Gmail,https://gmail.com,me@gmail.com,secret,,,
```
## Usage: Advanced Example
```ts
import { parse, exportToCsv } from "@suaveplan/password-formats";
// Round-trip: parse Bitwarden export → normalize → re-export as universal CSV
const entries = await parse(bitwardenJson, "bitwarden");
const csv = exportToCsv(entries);
// Save or transmit the CSV
const blob = new Blob([csv], { type: "text/csv" });
```
## API Reference
### `exportToCsv`
```ts
function exportToCsv(entries: PasswordEntry[]): string
```
**Parameters:**
- `entries` — Array of `PasswordEntry` objects to export.
**Returns:** A CSV string with a header row and one data row per entry.
**Column mapping:**
| Column | Source field | Notes |
|--------|-------------|-------|
| `title` | `entry.title` | Required field |
| `url` | `entry.url` | Empty string if absent |
| `username` | `entry.username` | Empty string if absent |
| `password` | `entry.password` | Required field |
| `notes` | `entry.notes` | Empty string if absent |
| `folder` | `entry.folder` | Empty string if absent |
| `tags` | `entry.tags` | Comma-joined, empty string if absent |
## Implementation Notes
The `tags` field uses a comma-separated representation within a single CSV column. When a tag itself contains a comma, `papaparse` will correctly quote the entire cell value. Fields not included in the export schema — such as `id`, `favorite`, `type`, `createdAt`, `updatedAt`, and `customFields` — are intentionally omitted to keep the output universally importable. Consumers requiring those fields should use the full `PasswordEntry` object array in memory rather than CSV serialization.

View File

@@ -0,0 +1,65 @@
# apple-passwords
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Parses Apple Passwords CSV exports into the normalized `PasswordEntry[]` format used throughout the `@suaveplan/password-formats` package. Apple's built-in password manager exports credentials as a CSV file with a fixed six-column header. This module handles that format specifically, mapping each column to the appropriate field in `PasswordEntry` and preserving the OTPAuth token as a custom field for TOTP-aware consumers.
## Features
- Uses `papaparse` with `header: true` for robust CSV parsing
- Maps `Title`, `URL`, `Username`, `Password`, `Notes` columns to their `PasswordEntry` counterparts
- Preserves `OTPAuth` value in `customFields.OTPAuth`
- Respects `ParseOptions.skipInvalid` to skip entries missing required fields
- Respects `ParseOptions.maxEntries` to cap the number of returned entries
- Throws `PasswordFormatsError` with code `PARSE_ERROR` for missing required fields when `skipInvalid` is false
## Usage: Basic Example
```ts
import { parseApplePasswordsCsv } from "@suaveplan/password-formats/parsers/apple";
const csv = `Title,URL,Username,Password,Notes,OTPAuth
My Bank,https://bank.com,user@example.com,s3cr3t,Main account,
GitHub,https://github.com,dev@example.com,gh_token,,`;
const entries = parseApplePasswordsCsv(csv);
console.log(entries[0].title); // "My Bank"
console.log(entries[1].url); // "https://github.com"
```
## Usage: Advanced Example
```ts
import { parseApplePasswordsCsv } from "@suaveplan/password-formats/parsers/apple";
const csv = `Title,URL,Username,Password,Notes,OTPAuth
My Account,https://example.com,me@example.com,secret,Backup,otpauth://totp/me@example.com?secret=BASE32`;
const entries = parseApplePasswordsCsv(csv, { skipInvalid: true, maxEntries: 100 });
console.log(entries[0].customFields?.OTPAuth); // "otpauth://totp/..."
```
## API Reference
### `parseApplePasswordsCsv`
```ts
function parseApplePasswordsCsv(input: string, options?: ParseOptions): PasswordEntry[]
```
**Parameters:**
- `input` — Raw Apple Passwords CSV string.
- `options.skipInvalid` — If `true`, rows missing `Title` or `Password` are silently skipped. Default: `false`.
- `options.maxEntries` — Maximum number of entries to return.
**Returns:** Array of `PasswordEntry` objects.
**Throws:** `PasswordFormatsError` (`PARSE_ERROR`) if a row is missing required fields and `skipInvalid` is `false`.
## Implementation Notes
Apple's CSV format always includes a header row with exactly six columns. The `OTPAuth` column contains an `otpauth://` URI when TOTP is configured for an entry, or an empty string otherwise. Empty `OTPAuth` values are omitted from `customFields` to keep entries clean. The parser does not validate the `otpauth://` URI format — it passes the raw string through verbatim so that TOTP consumers can parse it themselves.

View File

@@ -0,0 +1,81 @@
# bitwarden
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Parses Bitwarden JSON exports into the normalized `PasswordEntry[]` format used throughout the `@suaveplan/password-formats` package. Bitwarden is a widely used open-source password manager that exports a JSON file containing an array of credential items with rich metadata including type discrimination, folder references, and revision timestamps. This module handles the full schema including login, secure note, card, and identity item types.
## Features
- Parses Bitwarden unencrypted JSON exports
- Throws `ENCRYPTED_NOT_SUPPORTED` immediately for encrypted vaults — no partial processing
- Maps Bitwarden item `type` numbers (1–4) to `PasswordEntry.type` strings (`login`, `note`, `card`, `identity`)
- Extracts the first URI from `login.uris` as the `url` field
- Preserves `creationDate` and `revisionDate` as ISO 8601 `createdAt`/`updatedAt` fields
- Maps `folderId` to `folder` for folder-aware consumers
- Respects `ParseOptions.skipInvalid` and `ParseOptions.maxEntries`
## Usage: Basic Example
```ts
import { parseBitwardenJson } from "@suaveplan/password-formats/parsers/bitwarden";
const json = JSON.stringify({
encrypted: false,
items: [{
id: "abc123",
name: "Gmail",
login: { username: "user@gmail.com", password: "pass123", uris: [{ uri: "https://gmail.com" }] },
notes: "work email",
favorite: false,
type: 1,
}],
});
const entries = parseBitwardenJson(json);
console.log(entries[0].title); // "Gmail"
console.log(entries[0].username); // "user@gmail.com"
console.log(entries[0].type); // "login"
```
## Usage: Advanced Example
```ts
import { parseBitwardenJson } from "@suaveplan/password-formats/parsers/bitwarden";
import { PasswordFormatsError, PASSWORD_FORMATS_CODES } from "@suaveplan/password-formats/errors";
try {
const entries = parseBitwardenJson(encryptedJson);
} catch (err) {
if (err instanceof PasswordFormatsError && err.code === PASSWORD_FORMATS_CODES.ENCRYPTED_NOT_SUPPORTED) {
console.error("Export without encryption first.");
}
}
```
## API Reference
### `parseBitwardenJson`
```ts
function parseBitwardenJson(input: string, options?: ParseOptions): PasswordEntry[]
```
**Parameters:**
- `input` — Bitwarden JSON export string.
- `options.skipInvalid` — Skip items missing a `name` field. Default: `false`.
- `options.maxEntries` — Cap on returned entries.
**Returns:** Array of `PasswordEntry` objects.
**Throws:**
- `PasswordFormatsError` (`ENCRYPTED_NOT_SUPPORTED`) — Export has `"encrypted": true`.
- `PasswordFormatsError` (`PARSE_ERROR`) — Input is not valid JSON or a required field is missing and `skipInvalid` is `false`.
## Implementation Notes
Bitwarden's `folderId` is a UUID reference to a folder object in the export's `folders` array. This module maps `folderId` directly to `entry.folder` as a raw string rather than resolving the folder name, since the full folders array is not always present. Consumers requiring resolved folder names should parse the `folders` array separately. The `type` field mapping follows Bitwarden's documented numeric scheme: 1=Login, 2=SecureNote, 3=Card, 4=Identity.

View File

@@ -0,0 +1,66 @@
# chrome
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Parses Google Chrome CSV password exports into the normalized `PasswordEntry[]` format used throughout the `@suaveplan/password-formats` package. Chrome's built-in password manager (and Chromium-based browsers including Edge, Brave, and Opera) exports a minimal CSV with four columns. This module maps that simple schema to `PasswordEntry` objects, providing a clean integration path for users migrating away from browser-stored passwords.
## Features
- Uses `papaparse` with `header: true` for robust CSV parsing including quoted values
- Maps Chrome columns: `name` → `title`, `url` → `url`, `username` → `username`, `password` → `password`
- Respects `ParseOptions.skipInvalid` to skip rows missing required fields
- Respects `ParseOptions.maxEntries` to cap the number of returned entries
- Throws `PasswordFormatsError` (`PARSE_ERROR`) on missing required fields when `skipInvalid` is false
## Usage: Basic Example
```ts
import { parseChromeCsv } from "@suaveplan/password-formats/parsers/chrome";
const csv = `name,url,username,password
Gmail,https://gmail.com,test@gmail.com,pass123`;
const entries = parseChromeCsv(csv);
console.log(entries[0].title); // "Gmail"
console.log(entries[0].url); // "https://gmail.com"
console.log(entries[0].username); // "test@gmail.com"
```
## Usage: Advanced Example
```ts
import { parseChromeCsv } from "@suaveplan/password-formats/parsers/chrome";
const csv = `name,url,username,password
Gmail,https://gmail.com,me@gmail.com,secret
,https://missing-name.com,user,pass`;
// Skip the second row (missing name)
const entries = parseChromeCsv(csv, { skipInvalid: true, maxEntries: 50 });
console.log(entries.length); // 1 — the invalid row was skipped
```
## API Reference
### `parseChromeCsv`
```ts
function parseChromeCsv(input: string, options?: ParseOptions): PasswordEntry[]
```
**Parameters:**
- `input` — Chrome CSV export string.
- `options.skipInvalid` — Skip rows missing `name` or `password`. Default: `false`.
- `options.maxEntries` — Maximum number of entries to return.
**Returns:** Array of `PasswordEntry` objects.
**Throws:** `PasswordFormatsError` (`PARSE_ERROR`) when a row is missing required fields and `skipInvalid` is `false`.
## Implementation Notes
Chrome's CSV header uses lowercase column names (`name`, not `Name`). The parser passes the raw CSV through `papaparse` which handles header normalisation. Empty `url` and `username` values are omitted from the resulting entry object rather than set to empty strings, keeping the output clean. Chrome does not export notes, folders, or custom fields — those fields are simply absent from exported entries.

View File

@@ -0,0 +1,90 @@
# keepass
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Parses KeePass XML exports into the normalized `PasswordEntry[]` format used throughout the `@suaveplan/password-formats` package. KeePass is a widely used open-source password manager that can export credentials as an unencrypted XML file. This module handles the hierarchical group structure of KeePass XML, recursively walking all nested `Group` elements and building a dot-separated folder path from the group name chain, enabling consumers to reconstruct the organizational hierarchy.
## Features
- Uses `fast-xml-parser` for robust XML parsing without DOM dependencies
- Recursively walks nested `Group` elements to collect all `Entry` elements
- Builds `folder` path from parent group name chain (e.g. `"Personal/Work/Finance"`)
- Maps KeePass `String` key-value pairs: `Title`, `Password`, `URL`, `UserName`, `Notes`
- Preserves non-standard `String` fields in `customFields`
- Respects `ParseOptions.skipInvalid` and `ParseOptions.maxEntries`
- Throws `PasswordFormatsError` (`PARSE_ERROR`) on malformed XML or missing required fields
## Usage: Basic Example
```ts
import { parseKeePassXml } from "@suaveplan/password-formats/parsers/keepass";
const xml = `<?xml version="1.0" encoding="utf-8"?>
<KeePassFile>
<Root>
<Group>
<Name>Database</Name>
<Entry>
<String><Key>Title</Key><Value>My Site</Value></String>
<String><Key>Password</Key><Value>secret</Value></String>
<String><Key>URL</Key><Value>https://example.com</Value></String>
<String><Key>UserName</Key><Value>user</Value></String>
<String><Key>Notes</Key><Value>notes here</Value></String>
</Entry>
</Group>
</Root>
</KeePassFile>`;
const entries = parseKeePassXml(xml);
console.log(entries[0].title); // "My Site"
console.log(entries[0].username); // "user"
```
## Usage: Advanced Example
```ts
import { parseKeePassXml } from "@suaveplan/password-formats/parsers/keepass";
// Nested groups produce folder paths
const xml = `<KeePassFile><Root><Group>
<Name>Root</Name>
<Group>
<Name>Personal</Name>
<Group>
<Name>Banking</Name>
<Entry>
<String><Key>Title</Key><Value>My Bank</Value></String>
<String><Key>Password</Key><Value>bankpass</Value></String>
</Entry>
</Group>
</Group>
</Group></Root></KeePassFile>`;
const entries = parseKeePassXml(xml);
console.log(entries[0].folder); // "Personal/Banking"
```
## API Reference
### `parseKeePassXml`
```ts
function parseKeePassXml(input: string, options?: ParseOptions): PasswordEntry[]
```
**Parameters:**
- `input` — KeePass XML export string (unencrypted `.xml`, not `.kdbx` binary).
- `options.skipInvalid` — Skip entries missing `Title` or `Password`. Default: `false`.
- `options.maxEntries` — Maximum number of entries to return.
**Returns:** Array of `PasswordEntry` objects.
**Throws:** `PasswordFormatsError` (`PARSE_ERROR`) on malformed XML, missing root structure, or missing required fields when `skipInvalid` is `false`.
## Implementation Notes
KeePass XML uses a `<String>` element with `<Key>` and `<Value>` child elements to represent each field. The root group name is included in the folder path only for nested groups — top-level entries in the root group have an empty `folder`. The `fast-xml-parser` `isArray` callback is configured to always return `true` for non-attribute nodes, ensuring `Entry` and `Group` are always arrays even when only one element is present. This eliminates the need for dual single/array handling throughout the recursive walk.

View File

@@ -0,0 +1,83 @@
# password-formats.types
## Purpose
Defines the shared TypeScript types used across the `@suaveplan/password-formats` package. These types form the canonical data contract between the format-specific parsers (Apple Passwords, Bitwarden, Chrome, KeePass), the auto-detector, the `validate` helper, and any downstream consumers that process or re-export credential data.
## Features
- `PasswordFormat` discriminated union for all supported export formats plus `"unknown"` for unrecognised inputs.
- `PasswordEntry` normalised interface covering all common fields (title, username, password, URL, notes, folder, tags, custom fields, timestamps, entry type).
- `ParseOptions` for controlling parser behaviour (skip invalid entries, cap result size).
- `ValidationResult` summary type returned by the `validate` helper, listing per-entry errors with their index positions.
## Usage: Basic Example
```ts
import type { PasswordEntry, PasswordFormat } from '@suaveplan/password-formats';
const entry: PasswordEntry = {
title: 'GitHub',
username: 'alice@example.com',
password: 's3cr3t!',
};
```
## Usage: Advanced Example
```ts
import type { ParseOptions, ValidationResult } from '@suaveplan/password-formats';
const opts: ParseOptions = { skipInvalid: true, maxEntries: 500 };
function reportErrors(result: ValidationResult): void {
if (!result.valid) {
for (const err of result.errors) {
console.error(`Entry ${err.index}: ${err.message}`);
}
}
}
```
## API Reference
### `PasswordFormat`
String literal union: `"apple" | "bitwarden" | "chrome" | "keepass" | "unknown"`. Returned by the auto-detector and accepted by the format-specific parser selection logic.
### `PasswordEntry`
| Field | Type | Required | Description |
|---|---|---|---|
| `title` | `string` | yes | Display name or site title |
| `password` | `string` | yes | Plaintext credential |
| `username` | `string?` | no | Login username or email |
| `url` | `string?` | no | Associated URL |
| `notes` | `string?` | no | Free-text notes |
| `folder` | `string?` | no | Organisational group |
| `favorite` | `boolean?` | no | Starred / pinned flag |
| `createdAt` | `string?` | no | ISO 8601 creation timestamp |
| `updatedAt` | `string?` | no | ISO 8601 last-modified timestamp |
| `tags` | `string[]?` | no | Arbitrary labels |
| `customFields` | `Record<string,string>?` | no | Manager-specific key/value pairs |
| `type` | `"login"\|"card"\|"note"\|"identity"?` | no | Entry category |
### `ParseOptions`
| Field | Type | Default | Description |
|---|---|---|---|
| `skipInvalid` | `boolean` | `false` | Skip malformed entries instead of throwing |
| `maxEntries` | `number` | unlimited | Stop parsing after this many entries |
### `ValidationResult`
| Field | Type | Description |
|---|---|---|
| `valid` | `boolean` | `true` when `invalidCount === 0` |
| `errors` | `Array<{index,message}>` | Per-entry validation failures |
| `validCount` | `number` | Count of entries that passed |
| `invalidCount` | `number` | Count of entries that failed |
## Implementation Notes
All fields in `PasswordEntry` that are optional in the source format are typed as optional here rather than defaulting to empty strings. This prevents parsers from silently papering over missing data and lets consumers distinguish between a genuinely empty notes field and a format that does not export notes at all. The `type` field maps Bitwarden's item types and KeePass group conventions into a shared four-value enum; entries without a recognisable type are left as `undefined`.