diff --git a/.eslintrc.json b/.eslintrc.json
new file mode 100644
index 0000000..4ae4378
--- /dev/null
+++ b/.eslintrc.json
@@ -0,0 +1,22 @@
+{
+ "root": true,
+ "parser": "@typescript-eslint/parser",
+ "parserOptions": {
+ "ecmaVersion": 2022,
+ "sourceType": "module",
+ "project": "./tsconfig.json"
+ },
+ "plugins": ["@typescript-eslint"],
+ "extends": [
+ "eslint:recommended",
+ "plugin:@typescript-eslint/recommended",
+ "plugin:@typescript-eslint/recommended-requiring-type-checking"
+ ],
+ "rules": {
+ "@typescript-eslint/explicit-function-return-type": "warn",
+ "@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_" }],
+ "@typescript-eslint/no-explicit-any": "error",
+ "no-console": "warn"
+ },
+ "ignorePatterns": ["dist", "node_modules", "*.js", "vitest.config.ts"]
+}
diff --git a/.gitignore b/.gitignore
index 91e24b0..2460477 100644
--- a/.gitignore
+++ b/.gitignore
@@ -35,3 +35,4 @@ coverage/
tmp/
temp/
*.tmp
+dummy-credentials.ini
diff --git a/CLAUDE.md b/CLAUDE.md
index f5cfe75..2363277 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -2,38 +2,57 @@
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
+## Build Commands
+
+```bash
+npm install # Install dependencies
+npm run build # Build TypeScript to dist/
+npm test # Run tests with vitest
+npm run typecheck # Type check without emitting
+```
+
## Project Overview
Binect-JS is a JavaScript/TypeScript wrapper for the Binect REST API (https://app.binect.de/index.jsp?id=api) that enables sending PDF documents as physical mail. The project consists of two artifacts:
-1. **Binect-JS SDK** (`@binect/js`) - A thin API wrapper
-2. **Binect Explorer** (`@binect/explorer`) - A browser-based interactive tool for learning and experimentation
+1. **Binect-JS SDK** (`@binect/js`) - A thin API wrapper in `src/`
+2. **Binect Explorer** - A browser-based interactive tool in `explorer/`
-## Architecture Principles
+## Architecture
The SDK is organized around domain-aligned sub-clients that mirror the API vocabulary:
-- `documents` - PDF upload, status inspection, parameter modification
-- `attachments` - Attachment handling
-- `sendings` - Mail dispatch triggers, cancellation
-- `accounts` - Account management
-- `invoices` - Invoice access
+- `client.documents` - PDF upload, status inspection, parameter modification
+- `client.attachments` - Attachment handling
+- `client.sendings` - Mail dispatch triggers, cancellation
+- `client.accounts` - Account management
+- `client.invoices` - Invoice access
### SDK Layer Separation
-**Core API Layer** (authoritative): 1:1 semantic mapping to REST endpoints. Methods like `documents.uploadPdf`, `documents.getStatus`, `sendings.announce`, `sendings.cancel`.
+**Core API Layer** (authoritative): 1:1 semantic mapping to REST endpoints. Methods like `documents.upload`, `documents.get`, `sendings.send`, `sendings.cancel`.
-**Convenience Layer** (optional, non-authoritative): Additive helpers like status predicates (`isShippable`), error extraction, polling helpers. These must never be the only way to perform an action.
+**Convenience Layer** (optional, non-authoritative): Additive helpers in `src/helpers.ts` like status predicates (`isShippable`), error extraction, polling helpers. These must never be the only way to perform an action.
## Design Constraints
-- **No backend dependency**: Must function entirely in browser/JS runtime
+- **No runtime dependencies**: Uses native `fetch` API only
- **No semantic reinterpretation**: Wrapper must not alter business meaning or outcomes
- **Transparency over abstraction**: Developers must reason about actual API calls
- **No default retries**: Network behavior must be explicit and opt-in
- **Authentication**: HTTP Basic Auth, credentials are ephemeral (not stored/cached)
+## Key Files
+
+- `src/client.ts` - Main BinectClient class
+- `src/clients/*.ts` - Domain sub-clients
+- `src/types.ts` - TypeScript type definitions
+- `src/errors.ts` - BinectApiError and BinectAuthError
+- `src/http.ts` - Low-level HTTP client
+- `src/helpers.ts` - Convenience helpers (predicates, polling)
+- `explorer/index.html` - Standalone Explorer UI
+
## API Integration Notes
-- Uploads use base64-encoded PDFs
-- All non-success responses surface as structured errors preserving HTTP status, endpoint, and parsed response
-- The wrapper does not reinterpret business errors from the API
+- Uploads use base64-encoded PDFs (max 12 MB)
+- All non-success responses surface as structured `BinectApiError`
+- Document status codes: 1=Preparing, 2=Shippable, 3=Queue, 4=Printing, 5=Sent, 6=Canceled, 7=Error
diff --git a/README.md b/README.md
index df62a34..3418fe4 100644
--- a/README.md
+++ b/README.md
@@ -1,11 +1,206 @@
-BinectJs
+# Binect-JS
-*Javascript Binect API wrapper*
+A JavaScript/TypeScript wrapper for the [Binect API](https://app.binect.de/index.jsp?id=api) to send PDF documents as physical mail via Deutsche Post.
-# Binect JS - Send Papermail from your Browser
+## Features
-An easy to use javascript library to send pdf-documents as papermail using the app.binect.de API.
+- **SDK (`@binect/js`)**: Type-safe API wrapper with domain-aligned sub-clients
+- **Explorer**: Browser-based interactive tool for learning and testing the API
+- **Zero Dependencies**: Uses native `fetch` API, works in browsers and Node.js >= 18
-For documentation of the API see: https://app.binect.de/index.jsp?id=api
+## Installation
-xxx
+```bash
+npm install @binect/js
+```
+
+## Quick Start
+
+```typescript
+import { BinectClient, DocumentStatus, isShippable } from '@binect/js';
+import { readFileSync } from 'fs';
+
+// Create client with your Binect credentials
+const client = new BinectClient({
+ username: 'your@email.com',
+ password: 'your-password',
+});
+
+// Upload a PDF document
+const pdfContent = readFileSync('letter.pdf').toString('base64');
+const document = await client.documents.upload({
+ content: pdfContent,
+ color: false,
+ duplex: true,
+ envelope: 'DINLANG',
+});
+
+console.log('Document ID:', document.documentId);
+console.log('Status:', document.status);
+
+// Check if ready to send
+if (isShippable(document)) {
+ // Send the document as physical mail
+ const sending = await client.sendings.send(document.documentId);
+ console.log('Mail dispatched!');
+}
+```
+
+## SDK API Reference
+
+The SDK provides domain-aligned sub-clients:
+
+### Documents (`client.documents`)
+
+- `upload(options)` - Upload a PDF document
+- `list(pagination?)` - List shippable documents
+- `listErrors(pagination?)` - List documents with errors
+- `get(documentId)` - Get document details
+- `delete(documentId)` - Delete a document
+- `getPdf(documentId)` - Get PDF preview
+- `getPng(documentId)` - Get PNG preview
+- `getAttributes(documentId)` - Get document attributes
+- `setAttributes(documentId, attributes)` - Set attributes
+- `applyTransformation(documentId, transformation)` - Apply scaling/offset
+- `addCoverPage(documentId, options)` - Add cover page
+
+### Sendings (`client.sendings`)
+
+- `announce(documentIds)` - Announce documents for delivery
+- `list(pagination?)` - List all sendings
+- `send(documentId)` - Trigger single document send
+- `cancel(documentId)` - Cancel a sending
+- `getStatus(documentIds)` - Batch status check
+
+### Attachments (`client.attachments`)
+
+- `upload(options)` - Upload an attachment
+- `list(pagination?)` - List all attachments
+- `get(attachmentId)` - Get attachment details
+- `delete(attachmentId)` - Delete an attachment
+- `attachToDocuments(attachmentId, documentIds)` - Attach to documents
+
+### Accounts (`client.accounts`)
+
+- `get()` - Get account balance/credit info
+- `getPersonalData()` - Get personal data
+- `updatePersonalData(data)` - Update personal data
+- `getOptions()` - Get default print options
+- `updateOptions(options)` - Update print options
+- `getJournal(month)` - Get transaction journal
+
+### Invoices (`client.invoices`)
+
+- `list(pagination?)` - List all invoices
+- `get(invoiceNumber)` - Get invoice details
+- `getPdf(invoiceNumber)` - Download invoice PDF
+
+## Convenience Helpers
+
+```typescript
+import {
+ isShippable,
+ isErroneous,
+ isSent,
+ isTerminal,
+ isCancelable,
+ hasErrors,
+ hasWarnings,
+ getErrors,
+ getStatusDescription,
+ waitForShippable,
+ fileToBase64,
+} from '@binect/js';
+
+// Status predicates
+if (isShippable(document)) { /* ... */ }
+if (isErroneous(document)) { /* ... */ }
+
+// Validation helpers
+const errors = getErrors(document);
+if (hasWarnings(document)) {
+ console.log('Document has warnings');
+}
+
+// Polling (opt-in)
+const readyDoc = await waitForShippable(
+ () => client.documents.get(docId),
+ { intervalMs: 2000, maxAttempts: 30 }
+);
+
+// Base64 encoding (browser)
+const file = document.querySelector('input[type="file"]').files[0];
+const base64 = await fileToBase64(file);
+```
+
+## Explorer
+
+The Binect Explorer is a browser-based interactive tool for:
+- Learning the Binect API
+- Testing document uploads and mail dispatch
+- Managing use case profiles
+
+Open `explorer/index.html` in a browser to use it.
+
+## Document Status Codes
+
+| Code | Status | Description |
+|------|--------|-------------|
+| 1 | IN_PREPARATION | Document is being validated |
+| 2 | SHIPPABLE | Ready to send |
+| 3 | PRODUCTION_QUEUE | In production queue |
+| 4 | PRINTING | Currently printing |
+| 5 | SENT | Mail has been sent |
+| 6 | CANCELED | Sending was canceled |
+| 7 | ERRONEOUS | Document has validation errors |
+
+## Development
+
+```bash
+# Install dependencies
+npm install
+
+# Build
+npm run build
+
+# Run tests
+npm test
+
+# Run e2e tests (requires Binect credentials)
+BINECT_USERNAME=your@email.com BINECT_PASSWORD=yourpass npm run test:e2e
+
+# Type check
+npm run typecheck
+```
+
+## Project Structure
+
+```
+binect-js/
+├── src/ # SDK source code
+│ ├── client.ts # Main BinectClient
+│ ├── clients/ # Domain sub-clients
+│ ├── types.ts # Type definitions
+│ ├── errors.ts # Error classes
+│ ├── http.ts # HTTP layer
+│ └── helpers.ts # Convenience helpers
+├── explorer/ # Browser-based Explorer UI
+├── tests/ # Test suite
+├── architecture/ # Architecture Decision Records
+└── research/ # API documentation research
+```
+
+## Architecture
+
+See [architecture/](./architecture/) for Architecture Decision Records (ADRs):
+- ADR-001: SDK Architecture
+- ADR-002: No External Dependencies
+- ADR-003: Explorer Architecture
+
+## API Documentation
+
+Official Binect API documentation: https://app.binect.de/index.jsp?id=api
+
+## License
+
+MIT
diff --git a/architecture/ADR-001-sdk-architecture.md b/architecture/ADR-001-sdk-architecture.md
new file mode 100644
index 0000000..a82a977
--- /dev/null
+++ b/architecture/ADR-001-sdk-architecture.md
@@ -0,0 +1,78 @@
+# ADR-001: SDK Architecture
+
+## Status
+Accepted
+
+## Context
+The Binect-JS SDK needs to provide a JavaScript/TypeScript wrapper for the Binect REST API. Per the PRD and TSD, the SDK must:
+- Be transparent and thin (no semantic reinterpretation)
+- Work in both browser and Node.js environments
+- Use domain-aligned sub-clients mirroring the API vocabulary
+- Separate core API layer from optional convenience helpers
+- Handle HTTP Basic Authentication without storing credentials
+
+## Decision
+
+### 1. Client Structure
+We adopt a **main client with domain-aligned sub-clients** pattern:
+
+```typescript
+const client = new BinectClient({ username, password });
+client.documents.upload(...)
+client.sendings.announce(...)
+client.attachments.list(...)
+client.accounts.get(...)
+client.invoices.list(...)
+```
+
+Each sub-client maps to an API domain and provides 1:1 method mapping to REST endpoints.
+
+### 2. HTTP Layer
+We use the native `fetch` API for HTTP requests:
+- Works in both browser and Node.js (>=18)
+- No external dependencies
+- Predictable behavior without hidden retries or timeouts
+
+### 3. Authentication
+- Credentials passed at client construction
+- Converted to Base64 Basic Auth header per request
+- Never stored beyond client instance lifetime
+- No automatic credential refresh
+
+### 4. Error Handling
+- Non-2xx responses throw `BinectApiError` with:
+ - HTTP status code
+ - Endpoint path
+ - Parsed response body (when available)
+- Network errors surface as-is (no wrapping)
+
+### 5. Type Safety
+- Full TypeScript types for all API requests/responses
+- Enums for document status, envelope types, franking types
+- Generic response types preserving API structure
+
+### 6. Convenience Layer (Optional)
+Additive helpers in separate modules:
+- Status predicates (`isShippable`, `isErroneous`)
+- Polling utilities (opt-in, no default behavior)
+- Response extractors
+
+These never replace core methods.
+
+## Consequences
+
+### Positive
+- Clear mental model mapping to API documentation
+- No hidden behavior or magic
+- Works in all JavaScript environments with fetch
+- Type-safe development experience
+
+### Negative
+- Developers must understand API structure
+- No automatic retry on transient failures (by design)
+- More verbose than heavily abstracted SDKs
+
+## References
+- PRD: Section 3.1 (Product Intent)
+- TSD: Section 3 (SDK Technical Orientation)
+- Binect API: https://app.binect.de/index.jsp?id=api
diff --git a/architecture/ADR-002-no-external-dependencies.md b/architecture/ADR-002-no-external-dependencies.md
new file mode 100644
index 0000000..78cd92b
--- /dev/null
+++ b/architecture/ADR-002-no-external-dependencies.md
@@ -0,0 +1,49 @@
+# ADR-002: No External Runtime Dependencies
+
+## Status
+Accepted
+
+## Context
+The SDK needs to make HTTP requests and handle authentication. Common approaches include using libraries like axios, node-fetch, or got for HTTP, and various utilities for base64 encoding.
+
+Per the TSD (Section 2, Design Guardrail #1): "No backend dependency - The product must function entirely in browser and JavaScript runtime environments."
+
+## Decision
+
+The SDK will have **zero runtime dependencies**:
+
+1. **HTTP Requests**: Use native `fetch` API
+ - Available in all modern browsers
+ - Built into Node.js >= 18
+ - No polyfills required for target environments
+
+2. **Base64 Encoding**: Use native APIs
+ - Browser: `btoa()` / `atob()`
+ - Node.js: `Buffer.from().toString('base64')`
+ - Provide isomorphic wrapper
+
+3. **Type Checking**: TypeScript (dev dependency only)
+
+## Consequences
+
+### Positive
+- No dependency vulnerabilities to manage
+- Smaller bundle size
+- Predictable behavior (no library-specific quirks)
+- Works identically in browser and Node.js
+- No version conflicts with consumer projects
+
+### Negative
+- Must implement utility functions ourselves
+- Cannot leverage library conveniences (interceptors, etc.)
+- Requires Node.js >= 18 (has native fetch)
+
+## Alternatives Considered
+
+1. **axios**: Popular but adds ~13KB and has had security vulnerabilities
+2. **node-fetch**: Would require different code paths for browser/Node
+3. **ky**: Modern but still an external dependency
+
+## References
+- TSD: Section 2 (Design Guardrails)
+- Node.js fetch: https://nodejs.org/docs/latest-v18.x/api/globals.html#fetch
diff --git a/architecture/ADR-003-explorer-architecture.md b/architecture/ADR-003-explorer-architecture.md
new file mode 100644
index 0000000..35e7905
--- /dev/null
+++ b/architecture/ADR-003-explorer-architecture.md
@@ -0,0 +1,72 @@
+# ADR-003: Explorer Architecture
+
+## Status
+Accepted
+
+## Context
+The Binect Explorer needs to be a browser-based interactive tool for:
+- Learning the Binect API
+- Experimentation and evaluation
+- Safe testing before production integration
+
+Per the TSD (Section 4), the Explorer:
+- Must operate without server-side components
+- Must clearly distinguish between preview and send operations
+- Must require explicit confirmation for destructive actions
+- Is a learning tool, not an operations dashboard
+
+## Decision
+
+### 1. Technology Stack
+We use a **vanilla JavaScript/HTML/CSS** approach:
+- No framework dependencies (React, Vue, etc.)
+- Single HTML file with embedded CSS and JS
+- Can use the SDK directly via module import
+- Easy to host as a static file
+
+Rationale: Per TSD Section 7, the product must remain independent of specific UI frameworks. A vanilla approach ensures maximum portability and simplicity.
+
+### 2. Architecture Pattern
+**Component-based with vanilla JS**:
+- Modular JavaScript functions for each feature
+- Event-driven UI updates
+- State management via simple objects
+
+### 3. Feature Organization
+The Explorer UI is organized around the API domains:
+- **Credentials Panel**: Input and manage API credentials
+- **Documents Panel**: Upload, view, manage documents
+- **Sendings Panel**: Announce and track mail dispatch
+- **Attachments Panel**: Manage attachments
+- **Account Panel**: View account info and options
+
+### 4. Safety Features
+Per TSD requirements:
+- Credentials are ephemeral by default (cleared on page refresh)
+- Optional local storage for convenience (opt-in)
+- Send operations require explicit confirmation dialog
+- Preview available before sending
+- Clear visual distinction between safe (read) and destructive (send/delete) actions
+
+### 5. Use Case Profiles
+- Stored in browser localStorage
+- Export/import as JSON files
+- Contain only parameter configurations, not workflows
+
+## Consequences
+
+### Positive
+- Zero external dependencies
+- Works as single HTML file
+- Easy to understand and modify
+- Can be hosted anywhere (CDN, local file, etc.)
+- Aligns with TSD requirement for framework independence
+
+### Negative
+- Less sophisticated UI compared to framework-based apps
+- Manual DOM manipulation
+- No virtual DOM or reactive updates
+
+## References
+- TSD: Section 4 (Explorer Technical Orientation)
+- PRD: Section 4.1 (Functional Expectations)
diff --git a/examples/din5008/260114-brief-testbriefBinectJs.odt b/examples/din5008/260114-brief-testbriefBinectJs.odt
new file mode 100755
index 0000000..4b51f72
Binary files /dev/null and b/examples/din5008/260114-brief-testbriefBinectJs.odt differ
diff --git a/examples/din5008/260114-brief-testbriefBinectJs.pdf b/examples/din5008/260114-brief-testbriefBinectJs.pdf
new file mode 100755
index 0000000..7ed0378
Binary files /dev/null and b/examples/din5008/260114-brief-testbriefBinectJs.pdf differ
diff --git a/examples/din5008/260114-brief-testbriefKeinePlz.odt b/examples/din5008/260114-brief-testbriefKeinePlz.odt
new file mode 100755
index 0000000..f76d397
Binary files /dev/null and b/examples/din5008/260114-brief-testbriefKeinePlz.odt differ
diff --git a/examples/din5008/260114-brief-testbriefKeinePlz.pdf b/examples/din5008/260114-brief-testbriefKeinePlz.pdf
new file mode 100755
index 0000000..7d999af
Binary files /dev/null and b/examples/din5008/260114-brief-testbriefKeinePlz.pdf differ
diff --git a/explorer/index.html b/explorer/index.html
new file mode 100644
index 0000000..eb2c631
--- /dev/null
+++ b/explorer/index.html
@@ -0,0 +1,1335 @@
+
+
+
+
+
+ Binect Explorer
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Documents
+ Upload
+ Sendings
+ Attachments
+ Raw Response
+
+
+
+
+
+ Load Shippable
+ Load Errors
+ Refresh
+
+
+
+
Connect and load documents to see them here.
+
+
+
+
+
+
+
+
+
+ Load Sendings
+
+
+
+
Connect and load sendings to see them here.
+
+
+
+
+
+
+ Load Attachments
+
+
+
+
Connect and load attachments to see them here.
+
+
+
+
+
+
Last API response will be shown here.
+
+ // No response yet
+
+
+
+
+
+
+
Document Details
+
+
+ Send Document
+ Preview PDF
+ Delete
+
+
+
+
+
+
+
+
+
+
Confirm Action
+
Are you sure you want to proceed?
+
+ This action will send physical mail and incur charges.
+
+
+ Cancel
+ Confirm
+
+
+
+
+
+
+
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 0000000..405985c
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,1849 @@
+{
+ "name": "@binect/js",
+ "version": "0.1.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "@binect/js",
+ "version": "0.1.0",
+ "license": "MIT",
+ "devDependencies": {
+ "@types/node": "^20.10.0",
+ "typescript": "^5.3.0",
+ "vitest": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
+ "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz",
+ "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz",
+ "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz",
+ "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz",
+ "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz",
+ "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz",
+ "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz",
+ "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz",
+ "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz",
+ "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz",
+ "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz",
+ "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz",
+ "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz",
+ "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz",
+ "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz",
+ "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz",
+ "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
+ "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
+ "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz",
+ "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz",
+ "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz",
+ "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz",
+ "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@jest/schemas": {
+ "version": "29.6.3",
+ "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz",
+ "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@sinclair/typebox": "^0.27.8"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.5.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@rollup/rollup-android-arm-eabi": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.55.1.tgz",
+ "integrity": "sha512-9R0DM/ykwfGIlNu6+2U09ga0WXeZ9MRC2Ter8jnz8415VbuIykVuc6bhdrbORFZANDmTDvq26mJrEVTl8TdnDg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-android-arm64": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.55.1.tgz",
+ "integrity": "sha512-eFZCb1YUqhTysgW3sj/55du5cG57S7UTNtdMjCW7LwVcj3dTTcowCsC8p7uBdzKsZYa8J7IDE8lhMI+HX1vQvg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-arm64": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.55.1.tgz",
+ "integrity": "sha512-p3grE2PHcQm2e8PSGZdzIhCKbMCw/xi9XvMPErPhwO17vxtvCN5FEA2mSLgmKlCjHGMQTP6phuQTYWUnKewwGg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-x64": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.55.1.tgz",
+ "integrity": "sha512-rDUjG25C9qoTm+e02Esi+aqTKSBYwVTaoS1wxcN47/Luqef57Vgp96xNANwt5npq9GDxsH7kXxNkJVEsWEOEaQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-arm64": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.55.1.tgz",
+ "integrity": "sha512-+JiU7Jbp5cdxekIgdte0jfcu5oqw4GCKr6i3PJTlXTCU5H5Fvtkpbs4XJHRmWNXF+hKmn4v7ogI5OQPaupJgOg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-x64": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.55.1.tgz",
+ "integrity": "sha512-V5xC1tOVWtLLmr3YUk2f6EJK4qksksOYiz/TCsFHu/R+woubcLWdC9nZQmwjOAbmExBIVKsm1/wKmEy4z4u4Bw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.55.1.tgz",
+ "integrity": "sha512-Rn3n+FUk2J5VWx+ywrG/HGPTD9jXNbicRtTM11e/uorplArnXZYsVifnPPqNNP5BsO3roI4n8332ukpY/zN7rQ==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.55.1.tgz",
+ "integrity": "sha512-grPNWydeKtc1aEdrJDWk4opD7nFtQbMmV7769hiAaYyUKCT1faPRm2av8CX1YJsZ4TLAZcg9gTR1KvEzoLjXkg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.55.1.tgz",
+ "integrity": "sha512-a59mwd1k6x8tXKcUxSyISiquLwB5pX+fJW9TkWU46lCqD/GRDe9uDN31jrMmVP3feI3mhAdvcCClhV8V5MhJFQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-musl": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.55.1.tgz",
+ "integrity": "sha512-puS1MEgWX5GsHSoiAsF0TYrpomdvkaXm0CofIMG5uVkP6IBV+ZO9xhC5YEN49nsgYo1DuuMquF9+7EDBVYu4uA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-gnu": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.55.1.tgz",
+ "integrity": "sha512-r3Wv40in+lTsULSb6nnoudVbARdOwb2u5fpeoOAZjFLznp6tDU8kd+GTHmJoqZ9lt6/Sys33KdIHUaQihFcu7g==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-musl": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.55.1.tgz",
+ "integrity": "sha512-MR8c0+UxAlB22Fq4R+aQSPBayvYa3+9DrwG/i1TKQXFYEaoW3B5b/rkSRIypcZDdWjWnpcvxbNaAJDcSbJU3Lw==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-gnu": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.55.1.tgz",
+ "integrity": "sha512-3KhoECe1BRlSYpMTeVrD4sh2Pw2xgt4jzNSZIIPLFEsnQn9gAnZagW9+VqDqAHgm1Xc77LzJOo2LdigS5qZ+gw==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-musl": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.55.1.tgz",
+ "integrity": "sha512-ziR1OuZx0vdYZZ30vueNZTg73alF59DicYrPViG0NEgDVN8/Jl87zkAPu4u6VjZST2llgEUjaiNl9JM6HH1Vdw==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.55.1.tgz",
+ "integrity": "sha512-uW0Y12ih2XJRERZ4jAfKamTyIHVMPQnTZcQjme2HMVDAHY4amf5u414OqNYC+x+LzRdRcnIG1YodLrrtA8xsxw==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-musl": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.55.1.tgz",
+ "integrity": "sha512-u9yZ0jUkOED1BFrqu3BwMQoixvGHGZ+JhJNkNKY/hyoEgOwlqKb62qu+7UjbPSHYjiVy8kKJHvXKv5coH4wDeg==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.55.1.tgz",
+ "integrity": "sha512-/0PenBCmqM4ZUd0190j7J0UsQ/1nsi735iPRakO8iPciE7BQ495Y6msPzaOmvx0/pn+eJVVlZrNrSh4WSYLxNg==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.55.1.tgz",
+ "integrity": "sha512-a8G4wiQxQG2BAvo+gU6XrReRRqj+pLS2NGXKm8io19goR+K8lw269eTrPkSdDTALwMmJp4th2Uh0D8J9bEV1vg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-musl": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.55.1.tgz",
+ "integrity": "sha512-bD+zjpFrMpP/hqkfEcnjXWHMw5BIghGisOKPj+2NaNDuVT+8Ds4mPf3XcPHuat1tz89WRL+1wbcxKY3WSbiT7w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-openbsd-x64": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.55.1.tgz",
+ "integrity": "sha512-eLXw0dOiqE4QmvikfQ6yjgkg/xDM+MdU9YJuP4ySTibXU0oAvnEWXt7UDJmD4UkYialMfOGFPJnIHSe/kdzPxg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-openharmony-arm64": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.55.1.tgz",
+ "integrity": "sha512-xzm44KgEP11te3S2HCSyYf5zIzWmx3n8HDCc7EE59+lTcswEWNpvMLfd9uJvVX8LCg9QWG67Xt75AuHn4vgsXw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.55.1.tgz",
+ "integrity": "sha512-yR6Bl3tMC/gBok5cz/Qi0xYnVbIxGx5Fcf/ca0eB6/6JwOY+SRUcJfI0OpeTpPls7f194as62thCt/2BjxYN8g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.55.1.tgz",
+ "integrity": "sha512-3fZBidchE0eY0oFZBnekYCfg+5wAB0mbpCBuofh5mZuzIU/4jIVkbESmd2dOsFNS78b53CYv3OAtwqkZZmU5nA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-gnu": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.55.1.tgz",
+ "integrity": "sha512-xGGY5pXj69IxKb4yv/POoocPy/qmEGhimy/FoTpTSVju3FYXUQQMFCaZZXJVidsmGxRioZAwpThl/4zX41gRKg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-msvc": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.55.1.tgz",
+ "integrity": "sha512-SPEpaL6DX4rmcXtnhdrQYgzQ5W2uW3SCJch88lB2zImhJRhIIK44fkUrgIV/Q8yUNfw5oyZ5vkeQsZLhCb06lw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@sinclair/typebox": {
+ "version": "0.27.8",
+ "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz",
+ "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
+ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/node": {
+ "version": "20.19.29",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.29.tgz",
+ "integrity": "sha512-YrT9ArrGaHForBaCNwFjoqJWmn8G1Pr7+BH/vwyLHciA9qT/wSiuOhxGCT50JA5xLvFBd6PIiGkE3afxcPE1nw==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "undici-types": "~6.21.0"
+ }
+ },
+ "node_modules/@vitest/expect": {
+ "version": "1.6.1",
+ "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.6.1.tgz",
+ "integrity": "sha512-jXL+9+ZNIJKruofqXuuTClf44eSpcHlgj3CiuNihUF3Ioujtmc0zIa3UJOW5RjDK1YLBJZnWBlPuqhYycLioog==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/spy": "1.6.1",
+ "@vitest/utils": "1.6.1",
+ "chai": "^4.3.10"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/runner": {
+ "version": "1.6.1",
+ "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.6.1.tgz",
+ "integrity": "sha512-3nSnYXkVkf3mXFfE7vVyPmi3Sazhb/2cfZGGs0JRzFsPFvAMBEcrweV1V1GsrstdXeKCTXlJbvnQwGWgEIHmOA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/utils": "1.6.1",
+ "p-limit": "^5.0.0",
+ "pathe": "^1.1.1"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/snapshot": {
+ "version": "1.6.1",
+ "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.6.1.tgz",
+ "integrity": "sha512-WvidQuWAzU2p95u8GAKlRMqMyN1yOJkGHnx3M1PL9Raf7AQ1kwLKg04ADlCa3+OXUZE7BceOhVZiuWAbzCKcUQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "magic-string": "^0.30.5",
+ "pathe": "^1.1.1",
+ "pretty-format": "^29.7.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/spy": {
+ "version": "1.6.1",
+ "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.6.1.tgz",
+ "integrity": "sha512-MGcMmpGkZebsMZhbQKkAf9CX5zGvjkBTqf8Zx3ApYWXr3wG+QvEu2eXWfnIIWYSJExIp4V9FCKDEeygzkYrXMw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tinyspy": "^2.2.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/utils": {
+ "version": "1.6.1",
+ "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.6.1.tgz",
+ "integrity": "sha512-jOrrUvXM4Av9ZWiG1EajNto0u96kWAhJ1LmPmJhXXQx/32MecEKd10pOLYgS2BQx1TgkGhloPU1ArDW2vvaY6g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "diff-sequences": "^29.6.3",
+ "estree-walker": "^3.0.3",
+ "loupe": "^2.3.7",
+ "pretty-format": "^29.7.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/acorn": {
+ "version": "8.15.0",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
+ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "acorn": "bin/acorn"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/acorn-walk": {
+ "version": "8.3.4",
+ "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz",
+ "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "acorn": "^8.11.0"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/ansi-styles": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
+ "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/assertion-error": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz",
+ "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/cac": {
+ "version": "6.7.14",
+ "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz",
+ "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/chai": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/chai/-/chai-4.5.0.tgz",
+ "integrity": "sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "assertion-error": "^1.1.0",
+ "check-error": "^1.0.3",
+ "deep-eql": "^4.1.3",
+ "get-func-name": "^2.0.2",
+ "loupe": "^2.3.6",
+ "pathval": "^1.1.1",
+ "type-detect": "^4.1.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/check-error": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz",
+ "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "get-func-name": "^2.0.2"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/confbox": {
+ "version": "0.1.8",
+ "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz",
+ "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/cross-spawn": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
+ "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "path-key": "^3.1.0",
+ "shebang-command": "^2.0.0",
+ "which": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/deep-eql": {
+ "version": "4.1.4",
+ "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.4.tgz",
+ "integrity": "sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "type-detect": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/diff-sequences": {
+ "version": "29.6.3",
+ "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz",
+ "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/esbuild": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
+ "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.21.5",
+ "@esbuild/android-arm": "0.21.5",
+ "@esbuild/android-arm64": "0.21.5",
+ "@esbuild/android-x64": "0.21.5",
+ "@esbuild/darwin-arm64": "0.21.5",
+ "@esbuild/darwin-x64": "0.21.5",
+ "@esbuild/freebsd-arm64": "0.21.5",
+ "@esbuild/freebsd-x64": "0.21.5",
+ "@esbuild/linux-arm": "0.21.5",
+ "@esbuild/linux-arm64": "0.21.5",
+ "@esbuild/linux-ia32": "0.21.5",
+ "@esbuild/linux-loong64": "0.21.5",
+ "@esbuild/linux-mips64el": "0.21.5",
+ "@esbuild/linux-ppc64": "0.21.5",
+ "@esbuild/linux-riscv64": "0.21.5",
+ "@esbuild/linux-s390x": "0.21.5",
+ "@esbuild/linux-x64": "0.21.5",
+ "@esbuild/netbsd-x64": "0.21.5",
+ "@esbuild/openbsd-x64": "0.21.5",
+ "@esbuild/sunos-x64": "0.21.5",
+ "@esbuild/win32-arm64": "0.21.5",
+ "@esbuild/win32-ia32": "0.21.5",
+ "@esbuild/win32-x64": "0.21.5"
+ }
+ },
+ "node_modules/estree-walker": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
+ "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "^1.0.0"
+ }
+ },
+ "node_modules/execa": {
+ "version": "8.0.1",
+ "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz",
+ "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cross-spawn": "^7.0.3",
+ "get-stream": "^8.0.1",
+ "human-signals": "^5.0.0",
+ "is-stream": "^3.0.0",
+ "merge-stream": "^2.0.0",
+ "npm-run-path": "^5.1.0",
+ "onetime": "^6.0.0",
+ "signal-exit": "^4.1.0",
+ "strip-final-newline": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=16.17"
+ },
+ "funding": {
+ "url": "https://github.com/sindresorhus/execa?sponsor=1"
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/get-func-name": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz",
+ "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/get-stream": {
+ "version": "8.0.1",
+ "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz",
+ "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/human-signals": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz",
+ "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=16.17.0"
+ }
+ },
+ "node_modules/is-stream": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz",
+ "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/isexe": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/js-tokens": {
+ "version": "9.0.1",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz",
+ "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/local-pkg": {
+ "version": "0.5.1",
+ "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.5.1.tgz",
+ "integrity": "sha512-9rrA30MRRP3gBD3HTGnC6cDFpaE1kVDWxWgqWJUN0RvDNAo+Nz/9GxB+nHOH0ifbVFy0hSA1V6vFDvnx54lTEQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "mlly": "^1.7.3",
+ "pkg-types": "^1.2.1"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/loupe": {
+ "version": "2.3.7",
+ "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz",
+ "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "get-func-name": "^2.0.1"
+ }
+ },
+ "node_modules/magic-string": {
+ "version": "0.30.21",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
+ "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.5"
+ }
+ },
+ "node_modules/merge-stream": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
+ "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/mimic-fn": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz",
+ "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/mlly": {
+ "version": "1.8.0",
+ "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.0.tgz",
+ "integrity": "sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "acorn": "^8.15.0",
+ "pathe": "^2.0.3",
+ "pkg-types": "^1.3.1",
+ "ufo": "^1.6.1"
+ }
+ },
+ "node_modules/mlly/node_modules/pathe": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
+ "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.11",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
+ "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/npm-run-path": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz",
+ "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "path-key": "^4.0.0"
+ },
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/npm-run-path/node_modules/path-key": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz",
+ "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/onetime": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz",
+ "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "mimic-fn": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-limit": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-5.0.0.tgz",
+ "integrity": "sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "yocto-queue": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/path-key": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/pathe": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz",
+ "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/pathval": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz",
+ "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/pkg-types": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz",
+ "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "confbox": "^0.1.8",
+ "mlly": "^1.7.4",
+ "pathe": "^2.0.1"
+ }
+ },
+ "node_modules/pkg-types/node_modules/pathe": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
+ "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/postcss": {
+ "version": "8.5.6",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
+ "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.11",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/pretty-format": {
+ "version": "29.7.0",
+ "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz",
+ "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/schemas": "^29.6.3",
+ "ansi-styles": "^5.0.0",
+ "react-is": "^18.0.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/react-is": {
+ "version": "18.3.1",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
+ "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/rollup": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.55.1.tgz",
+ "integrity": "sha512-wDv/Ht1BNHB4upNbK74s9usvl7hObDnvVzknxqY/E/O3X6rW1U1rV1aENEfJ54eFZDTNo7zv1f5N4edCluH7+A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "1.0.8"
+ },
+ "bin": {
+ "rollup": "dist/bin/rollup"
+ },
+ "engines": {
+ "node": ">=18.0.0",
+ "npm": ">=8.0.0"
+ },
+ "optionalDependencies": {
+ "@rollup/rollup-android-arm-eabi": "4.55.1",
+ "@rollup/rollup-android-arm64": "4.55.1",
+ "@rollup/rollup-darwin-arm64": "4.55.1",
+ "@rollup/rollup-darwin-x64": "4.55.1",
+ "@rollup/rollup-freebsd-arm64": "4.55.1",
+ "@rollup/rollup-freebsd-x64": "4.55.1",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.55.1",
+ "@rollup/rollup-linux-arm-musleabihf": "4.55.1",
+ "@rollup/rollup-linux-arm64-gnu": "4.55.1",
+ "@rollup/rollup-linux-arm64-musl": "4.55.1",
+ "@rollup/rollup-linux-loong64-gnu": "4.55.1",
+ "@rollup/rollup-linux-loong64-musl": "4.55.1",
+ "@rollup/rollup-linux-ppc64-gnu": "4.55.1",
+ "@rollup/rollup-linux-ppc64-musl": "4.55.1",
+ "@rollup/rollup-linux-riscv64-gnu": "4.55.1",
+ "@rollup/rollup-linux-riscv64-musl": "4.55.1",
+ "@rollup/rollup-linux-s390x-gnu": "4.55.1",
+ "@rollup/rollup-linux-x64-gnu": "4.55.1",
+ "@rollup/rollup-linux-x64-musl": "4.55.1",
+ "@rollup/rollup-openbsd-x64": "4.55.1",
+ "@rollup/rollup-openharmony-arm64": "4.55.1",
+ "@rollup/rollup-win32-arm64-msvc": "4.55.1",
+ "@rollup/rollup-win32-ia32-msvc": "4.55.1",
+ "@rollup/rollup-win32-x64-gnu": "4.55.1",
+ "@rollup/rollup-win32-x64-msvc": "4.55.1",
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/shebang-command": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+ "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "shebang-regex": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/shebang-regex": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/siginfo": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz",
+ "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/signal-exit": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
+ "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/stackback": {
+ "version": "0.0.2",
+ "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",
+ "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/std-env": {
+ "version": "3.10.0",
+ "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz",
+ "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/strip-final-newline": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz",
+ "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/strip-literal": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-2.1.1.tgz",
+ "integrity": "sha512-631UJ6O00eNGfMiWG78ck80dfBab8X6IVFB51jZK5Icd7XAs60Z5y7QdSd/wGIklnWvRbUNloVzhOKKmutxQ6Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "js-tokens": "^9.0.1"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/tinybench": {
+ "version": "2.9.0",
+ "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
+ "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/tinypool": {
+ "version": "0.8.4",
+ "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.8.4.tgz",
+ "integrity": "sha512-i11VH5gS6IFeLY3gMBQ00/MmLncVP7JLXOw1vlgkytLmJK7QnEr7NXf0LBdxfmNPAeyetukOk0bOYrJrFGjYJQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/tinyspy": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-2.2.1.tgz",
+ "integrity": "sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/type-detect": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz",
+ "integrity": "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/typescript": {
+ "version": "5.9.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
+ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/ufo": {
+ "version": "1.6.2",
+ "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.2.tgz",
+ "integrity": "sha512-heMioaxBcG9+Znsda5Q8sQbWnLJSl98AFDXTO80wELWEzX3hordXsTdxrIfMQoO9IY1MEnoGoPjpoKpMj+Yx0Q==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/undici-types": {
+ "version": "6.21.0",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
+ "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/vite": {
+ "version": "5.4.21",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz",
+ "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "^0.21.3",
+ "postcss": "^8.4.43",
+ "rollup": "^4.20.0"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^18.0.0 || >=20.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^18.0.0 || >=20.0.0",
+ "less": "*",
+ "lightningcss": "^1.21.0",
+ "sass": "*",
+ "sass-embedded": "*",
+ "stylus": "*",
+ "sugarss": "*",
+ "terser": "^5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "lightningcss": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vite-node": {
+ "version": "1.6.1",
+ "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.6.1.tgz",
+ "integrity": "sha512-YAXkfvGtuTzwWbDSACdJSg4A4DZiAqckWe90Zapc/sEX3XvHcw1NdurM/6od8J207tSDqNbSsgdCacBgvJKFuA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cac": "^6.7.14",
+ "debug": "^4.3.4",
+ "pathe": "^1.1.1",
+ "picocolors": "^1.0.0",
+ "vite": "^5.0.0"
+ },
+ "bin": {
+ "vite-node": "vite-node.mjs"
+ },
+ "engines": {
+ "node": "^18.0.0 || >=20.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/vitest": {
+ "version": "1.6.1",
+ "resolved": "https://registry.npmjs.org/vitest/-/vitest-1.6.1.tgz",
+ "integrity": "sha512-Ljb1cnSJSivGN0LqXd/zmDbWEM0RNNg2t1QW/XUhYl/qPqyu7CsqeWtqQXHVaJsecLPuDoak2oJcZN2QoRIOag==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/expect": "1.6.1",
+ "@vitest/runner": "1.6.1",
+ "@vitest/snapshot": "1.6.1",
+ "@vitest/spy": "1.6.1",
+ "@vitest/utils": "1.6.1",
+ "acorn-walk": "^8.3.2",
+ "chai": "^4.3.10",
+ "debug": "^4.3.4",
+ "execa": "^8.0.1",
+ "local-pkg": "^0.5.0",
+ "magic-string": "^0.30.5",
+ "pathe": "^1.1.1",
+ "picocolors": "^1.0.0",
+ "std-env": "^3.5.0",
+ "strip-literal": "^2.0.0",
+ "tinybench": "^2.5.1",
+ "tinypool": "^0.8.3",
+ "vite": "^5.0.0",
+ "vite-node": "1.6.1",
+ "why-is-node-running": "^2.2.2"
+ },
+ "bin": {
+ "vitest": "vitest.mjs"
+ },
+ "engines": {
+ "node": "^18.0.0 || >=20.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "@edge-runtime/vm": "*",
+ "@types/node": "^18.0.0 || >=20.0.0",
+ "@vitest/browser": "1.6.1",
+ "@vitest/ui": "1.6.1",
+ "happy-dom": "*",
+ "jsdom": "*"
+ },
+ "peerDependenciesMeta": {
+ "@edge-runtime/vm": {
+ "optional": true
+ },
+ "@types/node": {
+ "optional": true
+ },
+ "@vitest/browser": {
+ "optional": true
+ },
+ "@vitest/ui": {
+ "optional": true
+ },
+ "happy-dom": {
+ "optional": true
+ },
+ "jsdom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/which": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "node-which": "bin/node-which"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/why-is-node-running": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz",
+ "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "siginfo": "^2.0.0",
+ "stackback": "0.0.2"
+ },
+ "bin": {
+ "why-is-node-running": "cli.js"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/yocto-queue": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.2.tgz",
+ "integrity": "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.20"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ }
+ }
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..0ac60dc
--- /dev/null
+++ b/package.json
@@ -0,0 +1,46 @@
+{
+ "name": "@binect/js",
+ "version": "0.1.0",
+ "description": "JavaScript/TypeScript wrapper for the Binect API - Send PDF documents as physical mail",
+ "type": "module",
+ "main": "./dist/index.js",
+ "module": "./dist/index.js",
+ "types": "./dist/index.d.ts",
+ "exports": {
+ ".": {
+ "import": "./dist/index.js",
+ "types": "./dist/index.d.ts"
+ }
+ },
+ "files": [
+ "dist"
+ ],
+ "scripts": {
+ "build": "tsc",
+ "test": "node --experimental-vm-modules node_modules/vitest/vitest.mjs run",
+ "test:watch": "node --experimental-vm-modules node_modules/vitest/vitest.mjs",
+ "test:e2e": "node --experimental-vm-modules node_modules/vitest/vitest.mjs run tests/e2e.test.ts",
+ "typecheck": "tsc --noEmit",
+ "clean": "rm -rf dist",
+ "prepublishOnly": "npm run build"
+ },
+ "keywords": [
+ "binect",
+ "mail",
+ "letter",
+ "post",
+ "pdf",
+ "api",
+ "deutsche-post"
+ ],
+ "author": "",
+ "license": "MIT",
+ "devDependencies": {
+ "@types/node": "^20.10.0",
+ "typescript": "^5.3.0",
+ "vitest": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+}
diff --git a/research/binect-api-specification.md b/research/binect-api-specification.md
new file mode 100644
index 0000000..ef6dcf6
--- /dev/null
+++ b/research/binect-api-specification.md
@@ -0,0 +1,115 @@
+# Binect API REST Specification
+
+**Source:** https://app.binect.de/binectapi/v1_swagger_api_kernel.json
+**Retrieved:** 2026-01-14
+
+## Overview
+The Binect API (v0.9.9) is a letter shipping service that handles document upload, validation, processing, and Deutsche Post delivery. The API uses HTTP Basic Authentication and HTTPS exclusively.
+
+## Base Configuration
+- **Base Path:** `/binectapi/v1`
+- **Host:** `app.binect.de`
+- **Scheme:** HTTPS only
+- **Authentication:** HTTP Basic Auth (required for all endpoints)
+- **Contact:** kontakt@binect.de
+
+## Core Endpoints
+
+### Documents Management
+**Upload & Retrieve:**
+- `POST /documents` – Upload letters/serial letters with base64-encoded content
+- `GET /documents` – List shippable documents (status 2)
+- `GET /documents/errors` – List erroneous documents (status 7)
+- `GET /documents/{documentID}` – Fetch specific document
+- `DELETE /documents/{documentID}` – Remove document
+
+**Document Attributes:**
+- `GET /documents/{documentID}/attributes` – Get all attributes
+- `POST /documents/{documentID}/attributes` – Set attributes
+- `GET /documents/{documentID}/attributes/{key}` – Get specific attribute
+- `PUT /documents/{documentID}/attributes/{key}` – Update specific attribute
+- `DELETE /documents/{documentID}/attributes/{key}` – Delete specific attribute
+
+**Document Modifications:**
+- `PUT /documents/{documentID}/transformations` – Apply scaling/offset adjustments
+- `DELETE /documents/{documentID}/transformations` – Revert to original
+- `PUT /documents/{documentID}/coverpage` – Add cover page
+- `DELETE /documents/{documentID}/coverpage` – Remove cover page
+- `GET /documents/{documentID}/pdf` – PDF preview
+- `GET /documents/{documentID}/png` – PNG preview
+
+**Attachments on Documents:**
+- `GET /documents/{documentID}/attachments` – Get document attachments
+- `POST /documents/{documentID}/attachments` – Add attachment to document
+- `PATCH /documents/{documentID}/attachments` – Update attachments
+- `DELETE /documents/{documentID}/attachments` – Remove attachments
+
+### Sendings (Shipments)
+- `POST /sendings` – Announce documents for delivery
+- `GET /sendings` – List all shipments (statuses 3-7)
+- `PUT /sendings` – Cancel unshipped letters
+- `POST /sendings/{documentID}` – Trigger single shipment
+- `GET /sendings/{documentID}` – Get specific sending
+- `PUT /sendings/{documentID}` – Cancel specific sending
+- `DELETE /sendings/{documentID}` – Delete sending
+- `POST /sendings/document` – Upload and immediately send
+- `GET /sendings/status` – Batch status check
+
+### Attachments (Standalone)
+- `POST /attachments` – Upload attachment
+- `GET /attachments` – List all attachments
+- `GET /attachments/{attachmentID}` – Get specific attachment
+- `DELETE /attachments/{attachmentID}` – Delete attachment
+- `GET /attachments/{attachmentID}/pdf` – Preview attachment PDF
+- `GET /attachments/{attachmentID}/documents` – Find associated documents
+- `PATCH /attachments/{attachmentID}/documents` – Attach to multiple documents
+
+### Account Management
+- `GET /accounts` – Retrieve credit/financial data
+- `GET /accounts/personaldata` – Get user details
+- `PATCH /accounts/personaldata` – Update user details
+- `GET /accounts/options` – Get default print options
+- `PUT /accounts/options` – Update default print options
+- `GET /accounts/coworkers` – List team members
+- `GET /accounts/coworkers/{debitornumber}/journal/{month}` – Coworker transactions
+- `GET /accounts/journal/{month}` – Account transactions
+
+### Invoicing
+- `GET /invoices` – List all invoices
+- `GET /invoices/{invoiceNumber}` – Fetch invoice transactions
+- `GET /invoices/{invoiceNumber}/pdf` – Download invoice PDF
+
+## Key Data Models
+
+### Document Status Codes
+- 1: In preparation
+- 2: Shippable
+- 3: Production queue
+- 4: Printing
+- 5: Sent
+- 6: Canceled
+- 7: Erroneous
+
+### Envelope Options
+- DINLANG
+- C4
+
+### Franking Types
+- UNSPECIFIED
+- STANDARD_FRANKING
+- DV_FRANKING
+
+### Production Countries
+- UNSPECIFIED
+- DE
+- AT
+
+### Response Format Options
+- FULL (default): Complete validation results
+- SHORT: Minimal response; validation runs asynchronously
+
+## Constraints
+- Maximum file size: 12 MB
+- Pagination supported via `limit` and `offset` parameters
+- Serial letters support token-based or page-count splitting
+- Transformations and cover pages can only be applied to original documents
diff --git a/src/client.ts b/src/client.ts
new file mode 100644
index 0000000..aeee646
--- /dev/null
+++ b/src/client.ts
@@ -0,0 +1,75 @@
+import { HttpClient, DEFAULT_BASE_URL } from './http.js';
+import { DocumentsClient } from './clients/documents.js';
+import { AttachmentsClient } from './clients/attachments.js';
+import { SendingsClient } from './clients/sendings.js';
+import { AccountsClient } from './clients/accounts.js';
+import { InvoicesClient } from './clients/invoices.js';
+import type { BinectClientConfig } from './types.js';
+
+/**
+ * Main client for interacting with the Binect API.
+ *
+ * Provides access to all API domains through sub-clients:
+ * - documents: Upload, manage, and preview documents
+ * - attachments: Manage standalone attachments
+ * - sendings: Announce, send, and track mail dispatch
+ * - accounts: Access account info, options, and journals
+ * - invoices: List and download invoices
+ *
+ * @example
+ * ```typescript
+ * const client = new BinectClient({
+ * username: 'user@example.com',
+ * password: 'your-password',
+ * });
+ *
+ * // Upload a document
+ * const doc = await client.documents.upload({
+ * content: base64PdfContent,
+ * color: false,
+ * duplex: true,
+ * });
+ *
+ * // Send when ready
+ * if (doc.status === DocumentStatus.SHIPPABLE) {
+ * await client.sendings.send(doc.documentId);
+ * }
+ * ```
+ */
+export class BinectClient {
+ private readonly http: HttpClient;
+
+ /** Client for document operations */
+ public readonly documents: DocumentsClient;
+
+ /** Client for attachment operations */
+ public readonly attachments: AttachmentsClient;
+
+ /** Client for sending/shipment operations */
+ public readonly sendings: SendingsClient;
+
+ /** Client for account operations */
+ public readonly accounts: AccountsClient;
+
+ /** Client for invoice operations */
+ public readonly invoices: InvoicesClient;
+
+ /**
+ * Creates a new Binect API client.
+ *
+ * @param config - Client configuration including credentials
+ */
+ constructor(config: BinectClientConfig) {
+ this.http = new HttpClient({
+ baseUrl: config.baseUrl ?? DEFAULT_BASE_URL,
+ username: config.username,
+ password: config.password,
+ });
+
+ this.documents = new DocumentsClient(this.http);
+ this.attachments = new AttachmentsClient(this.http);
+ this.sendings = new SendingsClient(this.http);
+ this.accounts = new AccountsClient(this.http);
+ this.invoices = new InvoicesClient(this.http);
+ }
+}
diff --git a/src/clients/accounts.ts b/src/clients/accounts.ts
new file mode 100644
index 0000000..c26c40e
--- /dev/null
+++ b/src/clients/accounts.ts
@@ -0,0 +1,129 @@
+import type { HttpClient } from '../http.js';
+import type {
+ AccountInfo,
+ PersonalData,
+ PersonalDataUpdate,
+ AccountPrintOptions,
+ Coworker,
+ JournalEntry,
+ ListResponse,
+} from '../types.js';
+
+/**
+ * Client for account-related API operations.
+ * Handles account info, personal data, print options, and journal access.
+ */
+export class AccountsClient {
+ constructor(private readonly http: HttpClient) {}
+
+ /**
+ * Get account balance and credit information.
+ * GET /accounts
+ *
+ * @returns Account info including credit balance
+ */
+ async get(): Promise {
+ return this.http.request({
+ method: 'GET',
+ path: '/accounts',
+ });
+ }
+
+ /**
+ * Get personal data for the account.
+ * GET /accounts/personaldata
+ *
+ * @returns Personal data including contact information
+ */
+ async getPersonalData(): Promise {
+ return this.http.request({
+ method: 'GET',
+ path: '/accounts/personaldata',
+ });
+ }
+
+ /**
+ * Update personal data for the account.
+ * PATCH /accounts/personaldata
+ *
+ * @param data - Fields to update
+ * @returns Updated personal data
+ */
+ async updatePersonalData(data: PersonalDataUpdate): Promise {
+ return this.http.request({
+ method: 'PATCH',
+ path: '/accounts/personaldata',
+ body: data,
+ });
+ }
+
+ /**
+ * Get default print options for the account.
+ * GET /accounts/options
+ *
+ * @returns Default print options
+ */
+ async getOptions(): Promise {
+ return this.http.request({
+ method: 'GET',
+ path: '/accounts/options',
+ });
+ }
+
+ /**
+ * Update default print options for the account.
+ * PUT /accounts/options
+ *
+ * @param options - New default print options
+ * @returns Updated print options
+ */
+ async updateOptions(options: AccountPrintOptions): Promise {
+ return this.http.request({
+ method: 'PUT',
+ path: '/accounts/options',
+ body: options,
+ });
+ }
+
+ /**
+ * List coworkers associated with this account.
+ * GET /accounts/coworkers
+ *
+ * @returns List of coworkers
+ */
+ async getCoworkers(): Promise> {
+ return this.http.request>({
+ method: 'GET',
+ path: '/accounts/coworkers',
+ });
+ }
+
+ /**
+ * Get journal/transaction entries for a coworker in a specific month.
+ * GET /accounts/coworkers/{debitornumber}/journal/{month}
+ *
+ * @param debitornumber - The coworker's debitor number
+ * @param month - Month in YYYY-MM format
+ * @returns List of journal entries
+ */
+ async getCoworkerJournal(debitornumber: string, month: string): Promise> {
+ return this.http.request>({
+ method: 'GET',
+ path: `/accounts/coworkers/${encodeURIComponent(debitornumber)}/journal/${encodeURIComponent(month)}`,
+ });
+ }
+
+ /**
+ * Get journal/transaction entries for the account in a specific month.
+ * GET /accounts/journal/{month}
+ *
+ * @param month - Month in YYYY-MM format
+ * @returns List of journal entries
+ */
+ async getJournal(month: string): Promise> {
+ return this.http.request>({
+ method: 'GET',
+ path: `/accounts/journal/${encodeURIComponent(month)}`,
+ });
+ }
+}
diff --git a/src/clients/attachments.ts b/src/clients/attachments.ts
new file mode 100644
index 0000000..14a652a
--- /dev/null
+++ b/src/clients/attachments.ts
@@ -0,0 +1,110 @@
+import type { HttpClient } from '../http.js';
+import type { Attachment, AttachmentUploadOptions, ListResponse, PaginationOptions } from '../types.js';
+
+/**
+ * Client for attachment-related API operations.
+ * Handles standalone attachment upload, retrieval, and management.
+ */
+export class AttachmentsClient {
+ constructor(private readonly http: HttpClient) {}
+
+ /**
+ * Upload a new attachment.
+ * POST /attachments
+ *
+ * @param options - Attachment upload options including base64-encoded PDF content
+ * @returns The created attachment
+ */
+ async upload(options: AttachmentUploadOptions): Promise {
+ return this.http.request({
+ method: 'POST',
+ path: '/attachments',
+ body: options,
+ });
+ }
+
+ /**
+ * List all attachments.
+ * GET /attachments
+ *
+ * @param pagination - Optional pagination parameters
+ * @returns List of attachments
+ */
+ async list(pagination?: PaginationOptions): Promise> {
+ return this.http.request>({
+ method: 'GET',
+ path: '/attachments',
+ query: pagination,
+ });
+ }
+
+ /**
+ * Get a specific attachment by ID.
+ * GET /attachments/{attachmentID}
+ *
+ * @param attachmentId - The attachment ID
+ * @returns The attachment details
+ */
+ async get(attachmentId: string): Promise {
+ return this.http.request({
+ method: 'GET',
+ path: `/attachments/${encodeURIComponent(attachmentId)}`,
+ });
+ }
+
+ /**
+ * Delete an attachment.
+ * DELETE /attachments/{attachmentID}
+ *
+ * @param attachmentId - The attachment ID to delete
+ */
+ async delete(attachmentId: string): Promise {
+ await this.http.request({
+ method: 'DELETE',
+ path: `/attachments/${encodeURIComponent(attachmentId)}`,
+ });
+ }
+
+ /**
+ * Get PDF preview of an attachment.
+ * GET /attachments/{attachmentID}/pdf
+ *
+ * @param attachmentId - The attachment ID
+ * @returns Response containing PDF data
+ */
+ async getPdf(attachmentId: string): Promise {
+ return this.http.requestRaw({
+ method: 'GET',
+ path: `/attachments/${encodeURIComponent(attachmentId)}/pdf`,
+ });
+ }
+
+ /**
+ * Get documents that have this attachment.
+ * GET /attachments/{attachmentID}/documents
+ *
+ * @param attachmentId - The attachment ID
+ * @returns Array of document IDs
+ */
+ async getDocuments(attachmentId: string): Promise {
+ return this.http.request({
+ method: 'GET',
+ path: `/attachments/${encodeURIComponent(attachmentId)}/documents`,
+ });
+ }
+
+ /**
+ * Attach this attachment to multiple documents.
+ * PATCH /attachments/{attachmentID}/documents
+ *
+ * @param attachmentId - The attachment ID
+ * @param documentIds - Array of document IDs to attach to
+ */
+ async attachToDocuments(attachmentId: string, documentIds: string[]): Promise {
+ await this.http.request({
+ method: 'PATCH',
+ path: `/attachments/${encodeURIComponent(attachmentId)}/documents`,
+ body: { documentIds },
+ });
+ }
+}
diff --git a/src/clients/documents.ts b/src/clients/documents.ts
new file mode 100644
index 0000000..b703dff
--- /dev/null
+++ b/src/clients/documents.ts
@@ -0,0 +1,352 @@
+import type { HttpClient } from '../http.js';
+import type {
+ Document,
+ DocumentUploadOptions,
+ DocumentUploadRequestBody,
+ DocumentTransformation,
+ CoverPageOptions,
+ DocumentAttribute,
+ ListResponse,
+ PaginationOptions,
+} from '../types.js';
+
+/**
+ * Client for document-related API operations.
+ * Handles document upload, retrieval, modification, and preview.
+ */
+export class DocumentsClient {
+ constructor(private readonly http: HttpClient) {}
+
+ /**
+ * Upload a new document (letter or serial letter).
+ * POST /documents
+ *
+ * @param options - Document upload options including base64-encoded PDF content
+ * @returns The created document with validation results
+ */
+ async upload(options: DocumentUploadOptions): Promise {
+ // Transform user-friendly options to API request body format
+ const requestBody: DocumentUploadRequestBody = {
+ content: {
+ filename: options.filename,
+ content: options.content,
+ },
+ };
+
+ // Add options if any are specified
+ if (options.simplex !== undefined || options.color !== undefined ||
+ options.envelope !== undefined || options.franking !== undefined ||
+ options.productionCountry !== undefined) {
+ requestBody.options = {
+ simplex: options.simplex,
+ color: options.color,
+ envelope: options.envelope,
+ franking: options.franking,
+ productionCountry: options.productionCountry,
+ };
+ }
+
+ // Add attributes if specified
+ if (options.attributes) {
+ requestBody.attributes = options.attributes;
+ }
+
+ // Add split params if specified
+ if (options.splitToken !== undefined || options.pagesPerLetter !== undefined) {
+ requestBody.splitParams = {
+ splitToken: options.splitToken,
+ splitAfterNumberOfPages: options.pagesPerLetter,
+ };
+ }
+
+ // Add response format if specified
+ if (options.responseFormat !== undefined) {
+ requestBody.responseFormat = options.responseFormat;
+ }
+
+ return this.http.request({
+ method: 'POST',
+ path: '/documents',
+ body: requestBody,
+ });
+ }
+
+ /**
+ * List all shippable documents (status 2).
+ * GET /documents
+ *
+ * @param pagination - Optional pagination parameters
+ * @returns List of shippable documents
+ */
+ async list(pagination?: PaginationOptions): Promise> {
+ return this.http.request>({
+ method: 'GET',
+ path: '/documents',
+ query: pagination,
+ });
+ }
+
+ /**
+ * List all documents with errors (status 7).
+ * GET /documents/errors
+ *
+ * @param pagination - Optional pagination parameters
+ * @returns List of erroneous documents
+ */
+ async listErrors(pagination?: PaginationOptions): Promise> {
+ return this.http.request>({
+ method: 'GET',
+ path: '/documents/errors',
+ query: pagination,
+ });
+ }
+
+ /**
+ * Get a specific document by ID.
+ * GET /documents/{documentID}
+ *
+ * @param documentId - The document ID
+ * @returns The document details
+ */
+ async get(documentId: string): Promise {
+ return this.http.request({
+ method: 'GET',
+ path: `/documents/${encodeURIComponent(documentId)}`,
+ });
+ }
+
+ /**
+ * Delete a document.
+ * DELETE /documents/{documentID}
+ *
+ * @param documentId - The document ID to delete
+ */
+ async delete(documentId: string): Promise {
+ await this.http.request({
+ method: 'DELETE',
+ path: `/documents/${encodeURIComponent(documentId)}`,
+ });
+ }
+
+ /**
+ * Get all attributes for a document.
+ * GET /documents/{documentID}/attributes
+ *
+ * @param documentId - The document ID
+ * @returns Key-value attributes
+ */
+ async getAttributes(documentId: string): Promise> {
+ return this.http.request>({
+ method: 'GET',
+ path: `/documents/${encodeURIComponent(documentId)}/attributes`,
+ });
+ }
+
+ /**
+ * Set attributes for a document.
+ * POST /documents/{documentID}/attributes
+ *
+ * @param documentId - The document ID
+ * @param attributes - Array of key-value attributes to set
+ */
+ async setAttributes(documentId: string, attributes: DocumentAttribute[]): Promise {
+ await this.http.request({
+ method: 'POST',
+ path: `/documents/${encodeURIComponent(documentId)}/attributes`,
+ body: attributes,
+ });
+ }
+
+ /**
+ * Get a specific attribute value.
+ * GET /documents/{documentID}/attributes/{key}
+ *
+ * @param documentId - The document ID
+ * @param key - The attribute key
+ * @returns The attribute value
+ */
+ async getAttribute(documentId: string, key: string): Promise {
+ return this.http.request({
+ method: 'GET',
+ path: `/documents/${encodeURIComponent(documentId)}/attributes/${encodeURIComponent(key)}`,
+ });
+ }
+
+ /**
+ * Update a specific attribute value.
+ * PUT /documents/{documentID}/attributes/{key}
+ *
+ * @param documentId - The document ID
+ * @param key - The attribute key
+ * @param value - The new attribute value
+ */
+ async updateAttribute(documentId: string, key: string, value: string): Promise {
+ await this.http.request({
+ method: 'PUT',
+ path: `/documents/${encodeURIComponent(documentId)}/attributes/${encodeURIComponent(key)}`,
+ body: { value },
+ });
+ }
+
+ /**
+ * Delete a specific attribute.
+ * DELETE /documents/{documentID}/attributes/{key}
+ *
+ * @param documentId - The document ID
+ * @param key - The attribute key to delete
+ */
+ async deleteAttribute(documentId: string, key: string): Promise {
+ await this.http.request({
+ method: 'DELETE',
+ path: `/documents/${encodeURIComponent(documentId)}/attributes/${encodeURIComponent(key)}`,
+ });
+ }
+
+ /**
+ * Apply transformations (scaling/offset) to a document.
+ * PUT /documents/{documentID}/transformations
+ *
+ * @param documentId - The document ID
+ * @param transformation - Transformation parameters
+ * @returns The updated document
+ */
+ async applyTransformation(
+ documentId: string,
+ transformation: DocumentTransformation
+ ): Promise {
+ return this.http.request({
+ method: 'PUT',
+ path: `/documents/${encodeURIComponent(documentId)}/transformations`,
+ body: transformation,
+ });
+ }
+
+ /**
+ * Revert transformations to original document.
+ * DELETE /documents/{documentID}/transformations
+ *
+ * @param documentId - The document ID
+ * @returns The updated document
+ */
+ async revertTransformation(documentId: string): Promise {
+ return this.http.request({
+ method: 'DELETE',
+ path: `/documents/${encodeURIComponent(documentId)}/transformations`,
+ });
+ }
+
+ /**
+ * Add a cover page to a document.
+ * PUT /documents/{documentID}/coverpage
+ *
+ * @param documentId - The document ID
+ * @param options - Cover page options with base64-encoded PDF
+ * @returns The updated document
+ */
+ async addCoverPage(documentId: string, options: CoverPageOptions): Promise {
+ return this.http.request({
+ method: 'PUT',
+ path: `/documents/${encodeURIComponent(documentId)}/coverpage`,
+ body: options,
+ });
+ }
+
+ /**
+ * Remove the cover page from a document.
+ * DELETE /documents/{documentID}/coverpage
+ *
+ * @param documentId - The document ID
+ * @returns The updated document
+ */
+ async removeCoverPage(documentId: string): Promise {
+ return this.http.request({
+ method: 'DELETE',
+ path: `/documents/${encodeURIComponent(documentId)}/coverpage`,
+ });
+ }
+
+ /**
+ * Get PDF preview of a document.
+ * GET /documents/{documentID}/pdf
+ *
+ * @param documentId - The document ID
+ * @returns Response containing PDF data
+ */
+ async getPdf(documentId: string): Promise {
+ return this.http.requestRaw({
+ method: 'GET',
+ path: `/documents/${encodeURIComponent(documentId)}/pdf`,
+ });
+ }
+
+ /**
+ * Get PNG preview of a document (first page).
+ * GET /documents/{documentID}/png
+ *
+ * @param documentId - The document ID
+ * @returns Response containing PNG data
+ */
+ async getPng(documentId: string): Promise {
+ return this.http.requestRaw({
+ method: 'GET',
+ path: `/documents/${encodeURIComponent(documentId)}/png`,
+ });
+ }
+
+ /**
+ * Get attachments for a document.
+ * GET /documents/{documentID}/attachments
+ *
+ * @param documentId - The document ID
+ * @returns Array of attachment IDs
+ */
+ async getAttachments(documentId: string): Promise {
+ return this.http.request({
+ method: 'GET',
+ path: `/documents/${encodeURIComponent(documentId)}/attachments`,
+ });
+ }
+
+ /**
+ * Add an attachment to a document.
+ * POST /documents/{documentID}/attachments
+ *
+ * @param documentId - The document ID
+ * @param attachmentId - The attachment ID to add
+ */
+ async addAttachment(documentId: string, attachmentId: string): Promise {
+ await this.http.request({
+ method: 'POST',
+ path: `/documents/${encodeURIComponent(documentId)}/attachments`,
+ body: { attachmentId },
+ });
+ }
+
+ /**
+ * Update attachments for a document (replace all).
+ * PATCH /documents/{documentID}/attachments
+ *
+ * @param documentId - The document ID
+ * @param attachmentIds - Array of attachment IDs
+ */
+ async updateAttachments(documentId: string, attachmentIds: string[]): Promise {
+ await this.http.request({
+ method: 'PATCH',
+ path: `/documents/${encodeURIComponent(documentId)}/attachments`,
+ body: { attachmentIds },
+ });
+ }
+
+ /**
+ * Remove all attachments from a document.
+ * DELETE /documents/{documentID}/attachments
+ *
+ * @param documentId - The document ID
+ */
+ async removeAttachments(documentId: string): Promise {
+ await this.http.request({
+ method: 'DELETE',
+ path: `/documents/${encodeURIComponent(documentId)}/attachments`,
+ });
+ }
+}
diff --git a/src/clients/index.ts b/src/clients/index.ts
new file mode 100644
index 0000000..7e08320
--- /dev/null
+++ b/src/clients/index.ts
@@ -0,0 +1,5 @@
+export { DocumentsClient } from './documents.js';
+export { AttachmentsClient } from './attachments.js';
+export { SendingsClient } from './sendings.js';
+export { AccountsClient } from './accounts.js';
+export { InvoicesClient } from './invoices.js';
diff --git a/src/clients/invoices.ts b/src/clients/invoices.ts
new file mode 100644
index 0000000..45f15b9
--- /dev/null
+++ b/src/clients/invoices.ts
@@ -0,0 +1,53 @@
+import type { HttpClient } from '../http.js';
+import type { Invoice, InvoiceDetail, ListResponse, PaginationOptions } from '../types.js';
+
+/**
+ * Client for invoice-related API operations.
+ * Handles invoice listing, details, and PDF download.
+ */
+export class InvoicesClient {
+ constructor(private readonly http: HttpClient) {}
+
+ /**
+ * List all invoices.
+ * GET /invoices
+ *
+ * @param pagination - Optional pagination parameters
+ * @returns List of invoices
+ */
+ async list(pagination?: PaginationOptions): Promise> {
+ return this.http.request>({
+ method: 'GET',
+ path: '/invoices',
+ query: pagination,
+ });
+ }
+
+ /**
+ * Get invoice details including transactions.
+ * GET /invoices/{invoiceNumber}
+ *
+ * @param invoiceNumber - The invoice number
+ * @returns Invoice details with transaction entries
+ */
+ async get(invoiceNumber: string): Promise {
+ return this.http.request({
+ method: 'GET',
+ path: `/invoices/${encodeURIComponent(invoiceNumber)}`,
+ });
+ }
+
+ /**
+ * Download invoice as PDF.
+ * GET /invoices/{invoiceNumber}/pdf
+ *
+ * @param invoiceNumber - The invoice number
+ * @returns Response containing PDF data
+ */
+ async getPdf(invoiceNumber: string): Promise {
+ return this.http.requestRaw({
+ method: 'GET',
+ path: `/invoices/${encodeURIComponent(invoiceNumber)}/pdf`,
+ });
+ }
+}
diff --git a/src/clients/sendings.ts b/src/clients/sendings.ts
new file mode 100644
index 0000000..ea12b89
--- /dev/null
+++ b/src/clients/sendings.ts
@@ -0,0 +1,153 @@
+import type { HttpClient } from '../http.js';
+import type {
+ Sending,
+ Document,
+ DocumentUploadAndSendOptions,
+ ListResponse,
+ PaginationOptions,
+ BatchStatusResponse,
+ DocumentStatus,
+} from '../types.js';
+
+/**
+ * Client for sending/shipment-related API operations.
+ * Handles document dispatch, cancellation, and status tracking.
+ */
+export class SendingsClient {
+ constructor(private readonly http: HttpClient) {}
+
+ /**
+ * Announce multiple documents for delivery.
+ * POST /sendings
+ *
+ * @param documentIds - Array of document IDs to announce (as numbers or numeric strings)
+ * @returns Array of sending confirmations
+ */
+ async announce(documentIds: (string | number)[]): Promise {
+ // API expects a raw array of integers
+ const ids = documentIds.map((id) => (typeof id === 'string' ? parseInt(id, 10) : id));
+ return this.http.request({
+ method: 'POST',
+ path: '/sendings',
+ body: ids,
+ });
+ }
+
+ /**
+ * List all sendings/shipments (statuses 3-7).
+ * GET /sendings
+ *
+ * @param pagination - Optional pagination parameters
+ * @returns List of sendings
+ */
+ async list(pagination?: PaginationOptions): Promise> {
+ return this.http.request>({
+ method: 'GET',
+ path: '/sendings',
+ query: pagination,
+ });
+ }
+
+ /**
+ * Cancel multiple announced sendings.
+ * PUT /sendings
+ *
+ * @param documentIds - Array of document IDs to cancel (as numbers or numeric strings)
+ * @returns Array of updated sendings
+ */
+ async cancelMultiple(documentIds: (string | number)[]): Promise {
+ // API expects a raw array of integers
+ const ids = documentIds.map((id) => (typeof id === 'string' ? parseInt(id, 10) : id));
+ return this.http.request({
+ method: 'PUT',
+ path: '/sendings',
+ body: ids,
+ });
+ }
+
+ /**
+ * Trigger sending for a single document.
+ * POST /sendings/{documentID}
+ *
+ * @param documentId - The document ID to send
+ * @returns The sending confirmation
+ */
+ async send(documentId: string): Promise {
+ return this.http.request({
+ method: 'POST',
+ path: `/sendings/${encodeURIComponent(documentId)}`,
+ });
+ }
+
+ /**
+ * Get sending status for a specific document.
+ * GET /sendings/{documentID}
+ *
+ * @param documentId - The document ID
+ * @returns The sending details
+ */
+ async get(documentId: string): Promise {
+ return this.http.request({
+ method: 'GET',
+ path: `/sendings/${encodeURIComponent(documentId)}`,
+ });
+ }
+
+ /**
+ * Cancel a specific sending.
+ * PUT /sendings/{documentID}
+ *
+ * @param documentId - The document ID to cancel
+ * @returns The updated sending
+ */
+ async cancel(documentId: string): Promise {
+ return this.http.request({
+ method: 'PUT',
+ path: `/sendings/${encodeURIComponent(documentId)}`,
+ });
+ }
+
+ /**
+ * Delete a sending record.
+ * DELETE /sendings/{documentID}
+ *
+ * @param documentId - The document ID to delete
+ */
+ async delete(documentId: string): Promise {
+ await this.http.request({
+ method: 'DELETE',
+ path: `/sendings/${encodeURIComponent(documentId)}`,
+ });
+ }
+
+ /**
+ * Upload a document and immediately send it.
+ * POST /sendings/document
+ *
+ * @param options - Document upload options
+ * @returns The created and sent document
+ */
+ async uploadAndSend(options: DocumentUploadAndSendOptions): Promise {
+ return this.http.request({
+ method: 'POST',
+ path: '/sendings/document',
+ body: options,
+ });
+ }
+
+ /**
+ * Get batch status for multiple documents.
+ * GET /sendings/status
+ *
+ * @param documentIds - Array of document IDs to check
+ * @returns Map of document IDs to their statuses
+ */
+ async getStatus(documentIds: string[]): Promise> {
+ const response = await this.http.request({
+ method: 'GET',
+ path: '/sendings/status',
+ query: { documentIds: documentIds.join(',') },
+ });
+ return response.statuses;
+ }
+}
diff --git a/src/errors.ts b/src/errors.ts
new file mode 100644
index 0000000..1dcee4d
--- /dev/null
+++ b/src/errors.ts
@@ -0,0 +1,71 @@
+import type { ApiErrorResponse } from './types.js';
+
+/**
+ * Error thrown when the Binect API returns a non-success response.
+ * Preserves HTTP status, endpoint, and parsed response body.
+ */
+export class BinectApiError extends Error {
+ /** HTTP status code */
+ public readonly status: number;
+ /** API endpoint that was called */
+ public readonly endpoint: string;
+ /** HTTP method used */
+ public readonly method: string;
+ /** Parsed error response from API (when available) */
+ public readonly response: ApiErrorResponse | null;
+
+ constructor(
+ message: string,
+ status: number,
+ endpoint: string,
+ method: string,
+ response: ApiErrorResponse | null = null
+ ) {
+ super(message);
+ this.name = 'BinectApiError';
+ this.status = status;
+ this.endpoint = endpoint;
+ this.method = method;
+ this.response = response;
+
+ // Maintains proper stack trace for where error was thrown (V8 engines)
+ if (Error.captureStackTrace) {
+ Error.captureStackTrace(this, BinectApiError);
+ }
+ }
+
+ /**
+ * Returns a detailed string representation of the error
+ */
+ toDetailedString(): string {
+ const parts = [
+ `BinectApiError: ${this.message}`,
+ ` Status: ${this.status}`,
+ ` Endpoint: ${this.method} ${this.endpoint}`,
+ ];
+
+ if (this.response) {
+ if (this.response.error) {
+ parts.push(` Error: ${this.response.error}`);
+ }
+ if (this.response.message) {
+ parts.push(` Message: ${this.response.message}`);
+ }
+ if (this.response.details && this.response.details.length > 0) {
+ parts.push(` Details: ${this.response.details.join(', ')}`);
+ }
+ }
+
+ return parts.join('\n');
+ }
+}
+
+/**
+ * Error thrown when credentials are invalid or missing
+ */
+export class BinectAuthError extends BinectApiError {
+ constructor(endpoint: string, method: string, response: ApiErrorResponse | null = null) {
+ super('Authentication failed: Invalid credentials', 401, endpoint, method, response);
+ this.name = 'BinectAuthError';
+ }
+}
diff --git a/src/helpers.ts b/src/helpers.ts
new file mode 100644
index 0000000..a8f792c
--- /dev/null
+++ b/src/helpers.ts
@@ -0,0 +1,274 @@
+/**
+ * Convenience Layer - Optional Helpers
+ *
+ * These helpers are purely additive and do not replace core API methods.
+ * They provide convenient predicates and utilities for common operations.
+ */
+
+import { DocumentStatus, type Document, type Sending, type ValidationMessage } from './types.js';
+
+/**
+ * Helper to get status code from document or sending
+ */
+function getStatusCode(doc: Document | Sending): DocumentStatus {
+ // Document has status.code, Sending might have status directly
+ if (typeof doc.status === 'object' && 'code' in doc.status) {
+ return doc.status.code;
+ }
+ return doc.status as unknown as DocumentStatus;
+}
+
+// ============================================================================
+// Status Predicates
+// ============================================================================
+
+/**
+ * Check if a document is shippable (status 2).
+ */
+export function isShippable(doc: Document | Sending): boolean {
+ return getStatusCode(doc) === DocumentStatus.SHIPPABLE;
+}
+
+/**
+ * Check if a document has errors (status 7).
+ */
+export function isErroneous(doc: Document | Sending): boolean {
+ return getStatusCode(doc) === DocumentStatus.ERRONEOUS;
+}
+
+/**
+ * Check if a document is still being prepared (status 1).
+ */
+export function isInPreparation(doc: Document | Sending): boolean {
+ return getStatusCode(doc) === DocumentStatus.IN_PREPARATION;
+}
+
+/**
+ * Check if a document is in the production queue (status 3).
+ */
+export function isInProductionQueue(doc: Document | Sending): boolean {
+ return getStatusCode(doc) === DocumentStatus.PRODUCTION_QUEUE;
+}
+
+/**
+ * Check if a document is currently being printed (status 4).
+ */
+export function isPrinting(doc: Document | Sending): boolean {
+ return getStatusCode(doc) === DocumentStatus.PRINTING;
+}
+
+/**
+ * Check if a document has been sent (status 5).
+ */
+export function isSent(doc: Document | Sending): boolean {
+ return getStatusCode(doc) === DocumentStatus.SENT;
+}
+
+/**
+ * Check if a document was canceled (status 6).
+ */
+export function isCanceled(doc: Document | Sending): boolean {
+ return getStatusCode(doc) === DocumentStatus.CANCELED;
+}
+
+/**
+ * Check if a document is in a terminal state (sent, canceled, or erroneous).
+ */
+export function isTerminal(doc: Document | Sending): boolean {
+ const status = getStatusCode(doc);
+ return (
+ status === DocumentStatus.SENT ||
+ status === DocumentStatus.CANCELED ||
+ status === DocumentStatus.ERRONEOUS
+ );
+}
+
+/**
+ * Check if a document can still be canceled (in queue or printing).
+ */
+export function isCancelable(doc: Document | Sending): boolean {
+ const status = getStatusCode(doc);
+ return (
+ status === DocumentStatus.PRODUCTION_QUEUE ||
+ status === DocumentStatus.PRINTING
+ );
+}
+
+// ============================================================================
+// Validation Helpers
+// ============================================================================
+
+/**
+ * Get all validation messages from a document.
+ */
+function getValidationMessages(doc: Document): ValidationMessage[] {
+ return doc.letter?.errors ?? [];
+}
+
+/**
+ * Extract error messages from validation results.
+ */
+export function getErrors(doc: Document): ValidationMessage[] {
+ return getValidationMessages(doc).filter((m: ValidationMessage) => m.type === 'ERROR');
+}
+
+/**
+ * Extract warning messages from validation results.
+ */
+export function getWarnings(doc: Document): ValidationMessage[] {
+ return getValidationMessages(doc).filter((m: ValidationMessage) => m.type === 'WARNING');
+}
+
+/**
+ * Extract info messages from validation results.
+ */
+export function getInfoMessages(doc: Document): ValidationMessage[] {
+ return getValidationMessages(doc).filter((m: ValidationMessage) => m.type === 'INFO');
+}
+
+/**
+ * Check if document has any validation errors.
+ */
+export function hasErrors(doc: Document): boolean {
+ return getErrors(doc).length > 0;
+}
+
+/**
+ * Check if document has any validation warnings.
+ */
+export function hasWarnings(doc: Document): boolean {
+ return getWarnings(doc).length > 0;
+}
+
+// ============================================================================
+// Status Description
+// ============================================================================
+
+/**
+ * Get human-readable description of document status.
+ */
+export function getStatusDescription(status: DocumentStatus): string {
+ switch (status) {
+ case DocumentStatus.IN_PREPARATION:
+ return 'In preparation';
+ case DocumentStatus.SHIPPABLE:
+ return 'Ready to ship';
+ case DocumentStatus.PRODUCTION_QUEUE:
+ return 'In production queue';
+ case DocumentStatus.PRINTING:
+ return 'Printing';
+ case DocumentStatus.SENT:
+ return 'Sent';
+ case DocumentStatus.CANCELED:
+ return 'Canceled';
+ case DocumentStatus.ERRONEOUS:
+ return 'Has errors';
+ default:
+ return 'Unknown status';
+ }
+}
+
+// ============================================================================
+// Base64 Utilities
+// ============================================================================
+
+/**
+ * Encode a file/blob to base64 string (browser environment).
+ * Returns a promise that resolves to the base64-encoded content.
+ */
+export function fileToBase64(file: Blob): Promise {
+ return new Promise((resolve, reject) => {
+ const reader = new FileReader();
+ reader.onload = (): void => {
+ const result = reader.result as string;
+ // Remove data URL prefix (e.g., "data:application/pdf;base64,")
+ const base64 = result.split(',')[1];
+ if (base64) {
+ resolve(base64);
+ } else {
+ reject(new Error('Failed to extract base64 content'));
+ }
+ };
+ reader.onerror = (): void => reject(reader.error);
+ reader.readAsDataURL(file);
+ });
+}
+
+/**
+ * Encode a Buffer to base64 string (Node.js environment).
+ */
+export function bufferToBase64(buffer: Buffer): string {
+ return buffer.toString('base64');
+}
+
+// ============================================================================
+// Polling Utilities (Opt-in, no default behavior)
+// ============================================================================
+
+/**
+ * Options for polling operations.
+ */
+export interface PollOptions {
+ /** Interval between polls in milliseconds (default: 2000) */
+ intervalMs?: number;
+ /** Maximum number of poll attempts (default: 30) */
+ maxAttempts?: number;
+ /** Abort signal for cancellation */
+ signal?: AbortSignal;
+}
+
+/**
+ * Poll until a condition is met.
+ * This is an opt-in utility - no automatic polling occurs.
+ *
+ * @param fn - Function to poll that returns the current state
+ * @param condition - Condition to check against the result
+ * @param options - Polling options
+ * @returns The final result when condition is met
+ * @throws Error if max attempts exceeded or aborted
+ */
+export async function pollUntil(
+ fn: () => Promise,
+ condition: (result: T) => boolean,
+ options: PollOptions = {}
+): Promise {
+ const intervalMs = options.intervalMs ?? 2000;
+ const maxAttempts = options.maxAttempts ?? 30;
+
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
+ if (options.signal?.aborted) {
+ throw new Error('Polling aborted');
+ }
+
+ const result = await fn();
+
+ if (condition(result)) {
+ return result;
+ }
+
+ if (attempt < maxAttempts - 1) {
+ await new Promise((resolve) => setTimeout(resolve, intervalMs));
+ }
+ }
+
+ throw new Error(`Polling exceeded maximum attempts (${maxAttempts})`);
+}
+
+/**
+ * Wait for a document to reach a shippable state.
+ * Convenience wrapper around pollUntil.
+ *
+ * @param getDocument - Function that fetches the document
+ * @param options - Polling options
+ * @returns The document when it becomes shippable or erroneous
+ */
+export async function waitForShippable(
+ getDocument: () => Promise,
+ options: PollOptions = {}
+): Promise {
+ return pollUntil(
+ getDocument,
+ (doc) => isShippable(doc) || isErroneous(doc),
+ options
+ );
+}
diff --git a/src/http.ts b/src/http.ts
new file mode 100644
index 0000000..85c0938
--- /dev/null
+++ b/src/http.ts
@@ -0,0 +1,183 @@
+import { BinectApiError, BinectAuthError } from './errors.js';
+import type { ApiErrorResponse } from './types.js';
+
+/**
+ * Default base URL for the Binect API
+ */
+export const DEFAULT_BASE_URL = 'https://app.binect.de/binectapi/v1';
+
+/**
+ * Encodes credentials for HTTP Basic Authentication.
+ * Works in both browser and Node.js environments.
+ */
+export function encodeBasicAuth(username: string, password: string): string {
+ const credentials = `${username}:${password}`;
+
+ // Use Buffer in Node.js, btoa in browser
+ if (typeof Buffer !== 'undefined') {
+ return Buffer.from(credentials, 'utf-8').toString('base64');
+ }
+
+ // Browser environment
+ return btoa(credentials);
+}
+
+/**
+ * HTTP client configuration
+ */
+export interface HttpClientConfig {
+ baseUrl: string;
+ username: string;
+ password: string;
+}
+
+/**
+ * HTTP request options
+ */
+export interface RequestOptions {
+ method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
+ path: string;
+ body?: unknown;
+ query?: Record;
+}
+
+/**
+ * Low-level HTTP client for Binect API requests.
+ * Handles authentication, request formatting, and error parsing.
+ */
+export class HttpClient {
+ private readonly baseUrl: string;
+ private readonly authHeader: string;
+
+ constructor(config: HttpClientConfig) {
+ this.baseUrl = config.baseUrl;
+ this.authHeader = `Basic ${encodeBasicAuth(config.username, config.password)}`;
+ }
+
+ /**
+ * Makes an HTTP request to the Binect API
+ */
+ async request(options: RequestOptions): Promise {
+ const url = this.buildUrl(options.path, options.query);
+
+ const headers: Record = {
+ Authorization: this.authHeader,
+ Accept: 'application/json',
+ };
+
+ const init: RequestInit = {
+ method: options.method,
+ headers,
+ };
+
+ if (options.body !== undefined) {
+ headers['Content-Type'] = 'application/json';
+ init.body = JSON.stringify(options.body);
+ }
+
+ const response = await fetch(url, init);
+
+ if (!response.ok) {
+ await this.handleErrorResponse(response, options.path, options.method);
+ }
+
+ // Handle empty responses (204 No Content, etc.)
+ const contentType = response.headers.get('content-type');
+ if (!contentType || !contentType.includes('application/json')) {
+ return undefined as T;
+ }
+
+ const text = await response.text();
+ if (!text) {
+ return undefined as T;
+ }
+
+ // Some endpoints may return non-JSON even with JSON content-type
+ // Check if response looks like JSON before parsing
+ const trimmed = text.trim();
+ if (!trimmed.startsWith('{') && !trimmed.startsWith('[')) {
+ return undefined as T;
+ }
+
+ return JSON.parse(text) as T;
+ }
+
+ /**
+ * Makes a request and returns raw response (for binary data like PDFs)
+ */
+ async requestRaw(options: RequestOptions): Promise {
+ const url = this.buildUrl(options.path, options.query);
+
+ const headers: Record = {
+ Authorization: this.authHeader,
+ };
+
+ const init: RequestInit = {
+ method: options.method,
+ headers,
+ };
+
+ const response = await fetch(url, init);
+
+ if (!response.ok) {
+ await this.handleErrorResponse(response, options.path, options.method);
+ }
+
+ return response;
+ }
+
+ /**
+ * Builds the full URL with query parameters
+ */
+ private buildUrl(path: string, query?: Record): string {
+ // Ensure proper URL construction by concatenating base and path
+ // Remove trailing slash from base and leading slash from path to avoid double slashes
+ const base = this.baseUrl.endsWith('/') ? this.baseUrl.slice(0, -1) : this.baseUrl;
+ const cleanPath = path.startsWith('/') ? path : `/${path}`;
+ const url = new URL(`${base}${cleanPath}`);
+
+ if (query) {
+ for (const [key, value] of Object.entries(query)) {
+ if (value !== undefined) {
+ url.searchParams.set(key, String(value));
+ }
+ }
+ }
+
+ return url.toString();
+ }
+
+ /**
+ * Handles error responses from the API
+ */
+ private async handleErrorResponse(
+ response: Response,
+ endpoint: string,
+ method: string
+ ): Promise {
+ let errorResponse: ApiErrorResponse | null = null;
+ let rawText = '';
+
+ try {
+ rawText = await response.text();
+ if (rawText) {
+ // Try to parse as JSON
+ const trimmed = rawText.trim();
+ if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
+ errorResponse = JSON.parse(rawText) as ApiErrorResponse;
+ }
+ }
+ } catch {
+ // JSON parsing failed - keep the raw text for the error message
+ }
+
+ if (response.status === 401) {
+ throw new BinectAuthError(endpoint, method, errorResponse);
+ }
+
+ // Use structured error message if available, otherwise use raw text or generic message
+ const message = errorResponse?.message ?? errorResponse?.error ?? (rawText || `HTTP ${response.status} error`);
+
+ throw new BinectApiError(message, response.status, endpoint, method, errorResponse);
+ }
+}
diff --git a/src/index.ts b/src/index.ts
new file mode 100644
index 0000000..54d1db3
--- /dev/null
+++ b/src/index.ts
@@ -0,0 +1,95 @@
+/**
+ * Binect-JS SDK
+ *
+ * A JavaScript/TypeScript wrapper for the Binect API
+ * to send PDF documents as physical mail.
+ *
+ * @packageDocumentation
+ */
+
+// Main client
+export { BinectClient } from './client.js';
+
+// Sub-clients (for advanced usage)
+export {
+ DocumentsClient,
+ AttachmentsClient,
+ SendingsClient,
+ AccountsClient,
+ InvoicesClient,
+} from './clients/index.js';
+
+// Types
+export {
+ // Enums
+ DocumentStatus,
+ EnvelopeType,
+ FrankingType,
+ ProductionCountry,
+ ResponseFormat,
+ // Request types
+ type DocumentUploadOptions,
+ type DocumentUploadAndSendOptions,
+ type DocumentTransformation,
+ type CoverPageOptions,
+ type DocumentAttribute,
+ type PaginationOptions,
+ type SendingAnnounceOptions,
+ type SendingCancelOptions,
+ type PersonalDataUpdate,
+ type AccountPrintOptions,
+ type AttachmentUploadOptions,
+ // Response types
+ type ValidationMessage,
+ type ExtractedAddress,
+ type PriceInfo,
+ type DocumentStatusInfo,
+ type LetterData,
+ type Letter,
+ type Document,
+ type ListResponse,
+ type Attachment,
+ type Sending,
+ type AccountInfo,
+ type PersonalData,
+ type Coworker,
+ type JournalEntry,
+ type Invoice,
+ type InvoiceDetail,
+ type BatchStatusResponse,
+ type ApiErrorResponse,
+ // Config
+ type BinectClientConfig,
+} from './types.js';
+
+// Errors
+export { BinectApiError, BinectAuthError } from './errors.js';
+
+// Convenience helpers
+export {
+ // Status predicates
+ isShippable,
+ isErroneous,
+ isInPreparation,
+ isInProductionQueue,
+ isPrinting,
+ isSent,
+ isCanceled,
+ isTerminal,
+ isCancelable,
+ // Validation helpers
+ getErrors,
+ getWarnings,
+ getInfoMessages,
+ hasErrors,
+ hasWarnings,
+ // Status description
+ getStatusDescription,
+ // Base64 utilities
+ fileToBase64,
+ bufferToBase64,
+ // Polling utilities
+ pollUntil,
+ waitForShippable,
+ type PollOptions,
+} from './helpers.js';
diff --git a/src/types.ts b/src/types.ts
new file mode 100644
index 0000000..1c42ed4
--- /dev/null
+++ b/src/types.ts
@@ -0,0 +1,446 @@
+/**
+ * Binect API Type Definitions
+ * Based on API specification v0.9.9
+ */
+
+// ============================================================================
+// Enums
+// ============================================================================
+
+/**
+ * Document status codes as defined by the Binect API
+ */
+export enum DocumentStatus {
+ /** Document is being prepared/validated */
+ IN_PREPARATION = 1,
+ /** Document is ready to be shipped */
+ SHIPPABLE = 2,
+ /** Document is in production queue */
+ PRODUCTION_QUEUE = 3,
+ /** Document is being printed */
+ PRINTING = 4,
+ /** Document has been sent */
+ SENT = 5,
+ /** Document was canceled */
+ CANCELED = 6,
+ /** Document has errors */
+ ERRONEOUS = 7,
+}
+
+/**
+ * Envelope type options
+ */
+export enum EnvelopeType {
+ DINLANG = 'DINLANG',
+ C4 = 'C4',
+}
+
+/**
+ * Franking type options
+ */
+export enum FrankingType {
+ UNSPECIFIED = 'UNSPECIFIED',
+ STANDARD_FRANKING = 'STANDARD_FRANKING',
+ DV_FRANKING = 'DV_FRANKING',
+}
+
+/**
+ * Production country options
+ */
+export enum ProductionCountry {
+ UNSPECIFIED = 'UNSPECIFIED',
+ DE = 'DE',
+ AT = 'AT',
+}
+
+/**
+ * Response format options for document upload
+ */
+export enum ResponseFormat {
+ /** Complete validation results (default) */
+ FULL = 'FULL',
+ /** Minimal response; validation runs asynchronously */
+ SHORT = 'SHORT',
+}
+
+// ============================================================================
+// Request Types
+// ============================================================================
+
+/**
+ * Options for uploading a document
+ */
+export interface DocumentUploadOptions {
+ /** Base64-encoded PDF content */
+ content: string;
+ /** Filename for the document (optional) */
+ filename?: string;
+ /** Whether to print in color (default: false) */
+ color?: boolean;
+ /** Whether to print simplex/single-sided (default: false, meaning duplex) */
+ simplex?: boolean;
+ /** Envelope type */
+ envelope?: EnvelopeType;
+ /** Franking type */
+ franking?: FrankingType;
+ /** Production country */
+ productionCountry?: ProductionCountry;
+ /** Response format */
+ responseFormat?: ResponseFormat;
+ /** Number of pages per letter for serial letter splitting */
+ pagesPerLetter?: number;
+ /** Token for serial letter splitting */
+ splitToken?: string;
+ /** Custom attributes as key-value pairs */
+ attributes?: DocumentAttribute[];
+}
+
+/**
+ * Internal API request body for document upload
+ * @internal
+ */
+export interface DocumentUploadRequestBody {
+ content: {
+ filename?: string;
+ content: string;
+ };
+ options?: {
+ simplex?: boolean;
+ color?: boolean;
+ envelope?: EnvelopeType;
+ franking?: FrankingType;
+ productionCountry?: ProductionCountry;
+ };
+ attributes?: DocumentAttribute[];
+ splitParams?: {
+ splitToken?: string;
+ splitAfterNumberOfPages?: number;
+ };
+ responseFormat?: ResponseFormat;
+}
+
+/**
+ * Options for uploading and immediately sending a document
+ */
+export interface DocumentUploadAndSendOptions extends DocumentUploadOptions {
+ /** Send immediately after upload */
+ send?: boolean;
+}
+
+/**
+ * Transformation parameters for document scaling/offset
+ */
+export interface DocumentTransformation {
+ /** Horizontal offset in mm */
+ offsetX?: number;
+ /** Vertical offset in mm */
+ offsetY?: number;
+ /** Scale factor (1.0 = 100%) */
+ scale?: number;
+}
+
+/**
+ * Cover page parameters
+ */
+export interface CoverPageOptions {
+ /** Base64-encoded PDF content for cover page */
+ content: string;
+}
+
+/**
+ * Key-value attribute
+ */
+export interface DocumentAttribute {
+ key: string;
+ value: string;
+}
+
+/**
+ * Pagination options for list endpoints
+ */
+export interface PaginationOptions {
+ /** Maximum number of results to return */
+ limit?: number;
+ /** Number of results to skip */
+ offset?: number;
+ /** Index signature for compatibility with query parameters */
+ [key: string]: number | undefined;
+}
+
+/**
+ * Options for announcing documents for sending
+ */
+export interface SendingAnnounceOptions {
+ /** Document IDs to announce */
+ documentIds: string[];
+}
+
+/**
+ * Options for canceling sendings
+ */
+export interface SendingCancelOptions {
+ /** Document IDs to cancel */
+ documentIds: string[];
+}
+
+/**
+ * Personal data update options
+ */
+export interface PersonalDataUpdate {
+ company?: string;
+ firstName?: string;
+ lastName?: string;
+ street?: string;
+ houseNumber?: string;
+ zipCode?: string;
+ city?: string;
+ country?: string;
+ phone?: string;
+ email?: string;
+}
+
+/**
+ * Account print options
+ */
+export interface AccountPrintOptions {
+ color?: boolean;
+ duplex?: boolean;
+ envelope?: EnvelopeType;
+ franking?: FrankingType;
+ productionCountry?: ProductionCountry;
+}
+
+/**
+ * Attachment upload options
+ */
+export interface AttachmentUploadOptions {
+ /** Base64-encoded PDF content */
+ content: string;
+ /** Name for the attachment */
+ name?: string;
+}
+
+// ============================================================================
+// Response Types
+// ============================================================================
+
+/**
+ * Validation message from document processing
+ */
+export interface ValidationMessage {
+ type: 'INFO' | 'WARNING' | 'ERROR';
+ code: string;
+ message: string;
+ page?: number;
+}
+
+/**
+ * Address extracted from document
+ */
+export interface ExtractedAddress {
+ name?: string;
+ company?: string;
+ street?: string;
+ houseNumber?: string;
+ zipCode?: string;
+ city?: string;
+ country?: string;
+}
+
+/**
+ * Price information from API
+ */
+export interface PriceInfo {
+ priceBeforeTax: number;
+ priceAfterTax: number;
+ unit: 'EUROCENT' | string;
+ taxInPercent: number;
+}
+
+/**
+ * Document status from API
+ */
+export interface DocumentStatusInfo {
+ code: DocumentStatus;
+ text: string;
+}
+
+/**
+ * Letter data from API response
+ */
+export interface LetterData {
+ recipientAddress?: string;
+ price?: PriceInfo;
+ international?: boolean;
+ options?: {
+ simplex?: boolean;
+ color?: boolean;
+ franking?: FrankingType;
+ productionCountry?: ProductionCountry;
+ envelope?: EnvelopeType;
+ };
+ attributes?: DocumentAttribute[];
+ attachments?: string[];
+}
+
+/**
+ * Letter from API response
+ */
+export interface Letter {
+ letterType?: string;
+ letterData?: LetterData;
+ errors?: ValidationMessage[];
+}
+
+/**
+ * Document response from API
+ */
+export interface Document {
+ /** Document ID (numeric) */
+ id: number;
+ /** Filename of uploaded document */
+ filename?: string;
+ /** Number of pages in document */
+ numberOfPages: number;
+ /** Document status */
+ status: DocumentStatusInfo;
+ /** Type of document */
+ documentType?: 'LETTER' | 'SERIALLETTER' | string;
+ /** Letter details (for single letters) */
+ letter?: Letter;
+ /** Array of letters (for serial letters) */
+ letters?: Letter[];
+}
+
+/**
+ * List response wrapper
+ */
+export interface ListResponse {
+ items: T[];
+ total: number;
+ limit: number;
+ offset: number;
+}
+
+/**
+ * Attachment response from API
+ */
+export interface Attachment {
+ attachmentId: string;
+ name: string;
+ pageCount: number;
+ createdAt: string;
+}
+
+/**
+ * Sending/shipment response from API
+ */
+export interface Sending {
+ documentId: string;
+ status: DocumentStatus;
+ price?: number;
+ trackingId?: string;
+ shippedAt?: string;
+ deliveredAt?: string;
+}
+
+/**
+ * Account balance/credit information
+ */
+export interface AccountInfo {
+ credit: number;
+ currency: string;
+ debitornumber: string;
+}
+
+/**
+ * Personal data response
+ */
+export interface PersonalData {
+ company?: string;
+ firstName?: string;
+ lastName?: string;
+ street?: string;
+ houseNumber?: string;
+ zipCode?: string;
+ city?: string;
+ country?: string;
+ phone?: string;
+ email?: string;
+ debitornumber: string;
+}
+
+/**
+ * Coworker information
+ */
+export interface Coworker {
+ debitornumber: string;
+ firstName?: string;
+ lastName?: string;
+ email?: string;
+}
+
+/**
+ * Journal/transaction entry
+ */
+export interface JournalEntry {
+ date: string;
+ description: string;
+ amount: number;
+ balance: number;
+ documentId?: string;
+}
+
+/**
+ * Invoice summary
+ */
+export interface Invoice {
+ invoiceNumber: string;
+ date: string;
+ amount: number;
+ currency: string;
+}
+
+/**
+ * Invoice detail with transactions
+ */
+export interface InvoiceDetail extends Invoice {
+ entries: JournalEntry[];
+}
+
+/**
+ * Batch status check response
+ */
+export interface BatchStatusResponse {
+ statuses: Record;
+}
+
+// ============================================================================
+// Error Types
+// ============================================================================
+
+/**
+ * API error response structure
+ */
+export interface ApiErrorResponse {
+ error?: string;
+ message?: string;
+ details?: string[];
+ validationErrors?: ValidationMessage[];
+}
+
+// ============================================================================
+// Client Configuration
+// ============================================================================
+
+/**
+ * Configuration options for BinectClient
+ */
+export interface BinectClientConfig {
+ /** Binect username (email) */
+ username: string;
+ /** Binect password */
+ password: string;
+ /** Base URL override (default: https://app.binect.de/binectapi/v1) */
+ baseUrl?: string;
+}
diff --git a/tests/client.test.ts b/tests/client.test.ts
new file mode 100644
index 0000000..cd6cc2d
--- /dev/null
+++ b/tests/client.test.ts
@@ -0,0 +1,59 @@
+import { describe, it, expect } from 'vitest';
+import { BinectClient } from '../src/client.js';
+import { DocumentsClient } from '../src/clients/documents.js';
+import { AttachmentsClient } from '../src/clients/attachments.js';
+import { SendingsClient } from '../src/clients/sendings.js';
+import { AccountsClient } from '../src/clients/accounts.js';
+import { InvoicesClient } from '../src/clients/invoices.js';
+
+describe('BinectClient', () => {
+ it('creates client with required config', () => {
+ const client = new BinectClient({
+ username: 'test@example.com',
+ password: 'password123',
+ });
+
+ expect(client).toBeInstanceOf(BinectClient);
+ });
+
+ it('creates client with custom baseUrl', () => {
+ const client = new BinectClient({
+ username: 'test@example.com',
+ password: 'password123',
+ baseUrl: 'https://custom.api.com/v1',
+ });
+
+ expect(client).toBeInstanceOf(BinectClient);
+ });
+
+ describe('sub-clients', () => {
+ let client: BinectClient;
+
+ beforeAll(() => {
+ client = new BinectClient({
+ username: 'test@example.com',
+ password: 'password123',
+ });
+ });
+
+ it('has documents client', () => {
+ expect(client.documents).toBeInstanceOf(DocumentsClient);
+ });
+
+ it('has attachments client', () => {
+ expect(client.attachments).toBeInstanceOf(AttachmentsClient);
+ });
+
+ it('has sendings client', () => {
+ expect(client.sendings).toBeInstanceOf(SendingsClient);
+ });
+
+ it('has accounts client', () => {
+ expect(client.accounts).toBeInstanceOf(AccountsClient);
+ });
+
+ it('has invoices client', () => {
+ expect(client.invoices).toBeInstanceOf(InvoicesClient);
+ });
+ });
+});
diff --git a/tests/errors.test.ts b/tests/errors.test.ts
new file mode 100644
index 0000000..16c94b6
--- /dev/null
+++ b/tests/errors.test.ts
@@ -0,0 +1,89 @@
+import { describe, it, expect } from 'vitest';
+import { BinectApiError, BinectAuthError } from '../src/errors.js';
+
+describe('BinectApiError', () => {
+ it('creates error with all properties', () => {
+ const response = { error: 'Not found', message: 'Document not found' };
+ const error = new BinectApiError('Not found', 404, '/documents/123', 'GET', response);
+
+ expect(error.name).toBe('BinectApiError');
+ expect(error.message).toBe('Not found');
+ expect(error.status).toBe(404);
+ expect(error.endpoint).toBe('/documents/123');
+ expect(error.method).toBe('GET');
+ expect(error.response).toEqual(response);
+ });
+
+ it('creates error without response', () => {
+ const error = new BinectApiError('Server error', 500, '/documents', 'POST');
+
+ expect(error.status).toBe(500);
+ expect(error.response).toBeNull();
+ });
+
+ it('has correct inheritance', () => {
+ const error = new BinectApiError('Test', 400, '/test', 'GET');
+
+ expect(error instanceof Error).toBe(true);
+ expect(error instanceof BinectApiError).toBe(true);
+ });
+
+ describe('toDetailedString', () => {
+ it('includes all error details', () => {
+ const response = {
+ error: 'Validation error',
+ message: 'Invalid document format',
+ details: ['Page size incorrect', 'Missing address'],
+ };
+ const error = new BinectApiError('Validation failed', 400, '/documents', 'POST', response);
+
+ const detailed = error.toDetailedString();
+
+ expect(detailed).toContain('BinectApiError: Validation failed');
+ expect(detailed).toContain('Status: 400');
+ expect(detailed).toContain('Endpoint: POST /documents');
+ expect(detailed).toContain('Error: Validation error');
+ expect(detailed).toContain('Message: Invalid document format');
+ expect(detailed).toContain('Details: Page size incorrect, Missing address');
+ });
+
+ it('handles minimal error response', () => {
+ const error = new BinectApiError('Server error', 500, '/test', 'GET');
+
+ const detailed = error.toDetailedString();
+
+ expect(detailed).toContain('BinectApiError: Server error');
+ expect(detailed).toContain('Status: 500');
+ // Should not contain "Error:" as a separate field (only in class name)
+ expect(detailed).not.toContain(' Error:');
+ expect(detailed).not.toContain(' Message:');
+ });
+ });
+});
+
+describe('BinectAuthError', () => {
+ it('creates authentication error', () => {
+ const error = new BinectAuthError('/accounts', 'GET');
+
+ expect(error.name).toBe('BinectAuthError');
+ expect(error.message).toBe('Authentication failed: Invalid credentials');
+ expect(error.status).toBe(401);
+ expect(error.endpoint).toBe('/accounts');
+ expect(error.method).toBe('GET');
+ });
+
+ it('includes response when provided', () => {
+ const response = { error: 'Unauthorized', message: 'Invalid credentials' };
+ const error = new BinectAuthError('/accounts', 'GET', response);
+
+ expect(error.response).toEqual(response);
+ });
+
+ it('inherits from BinectApiError', () => {
+ const error = new BinectAuthError('/test', 'GET');
+
+ expect(error instanceof Error).toBe(true);
+ expect(error instanceof BinectApiError).toBe(true);
+ expect(error instanceof BinectAuthError).toBe(true);
+ });
+});
diff --git a/tests/helpers.test.ts b/tests/helpers.test.ts
new file mode 100644
index 0000000..ec2daae
--- /dev/null
+++ b/tests/helpers.test.ts
@@ -0,0 +1,281 @@
+import { describe, it, expect } from 'vitest';
+import {
+ isShippable,
+ isErroneous,
+ isInPreparation,
+ isInProductionQueue,
+ isPrinting,
+ isSent,
+ isCanceled,
+ isTerminal,
+ isCancelable,
+ getErrors,
+ getWarnings,
+ getInfoMessages,
+ hasErrors,
+ hasWarnings,
+ getStatusDescription,
+ pollUntil,
+} from '../src/helpers.js';
+import { DocumentStatus } from '../src/types.js';
+import type { Document, ValidationMessage } from '../src/types.js';
+
+// Helper to create mock documents with specific status
+function createMockDocument(status: DocumentStatus, validationMessages?: ValidationMessage[]): Document {
+ return {
+ id: 12345,
+ filename: 'test-doc.pdf',
+ numberOfPages: 1,
+ status: {
+ code: status,
+ text: 'Test status',
+ },
+ documentType: 'LETTER',
+ letter: {
+ letterType: 'LETTERDATA',
+ letterData: {
+ options: {
+ simplex: false,
+ color: false,
+ franking: 'STANDARD_FRANKING',
+ productionCountry: 'DE',
+ envelope: 'DINLANG',
+ },
+ },
+ errors: validationMessages ?? [],
+ },
+ };
+}
+
+describe('Status Predicates', () => {
+ describe('isShippable', () => {
+ it('returns true for status SHIPPABLE', () => {
+ const doc = createMockDocument(DocumentStatus.SHIPPABLE);
+ expect(isShippable(doc)).toBe(true);
+ });
+
+ it('returns false for other statuses', () => {
+ expect(isShippable(createMockDocument(DocumentStatus.IN_PREPARATION))).toBe(false);
+ expect(isShippable(createMockDocument(DocumentStatus.ERRONEOUS))).toBe(false);
+ expect(isShippable(createMockDocument(DocumentStatus.SENT))).toBe(false);
+ });
+ });
+
+ describe('isErroneous', () => {
+ it('returns true for status ERRONEOUS', () => {
+ const doc = createMockDocument(DocumentStatus.ERRONEOUS);
+ expect(isErroneous(doc)).toBe(true);
+ });
+
+ it('returns false for other statuses', () => {
+ expect(isErroneous(createMockDocument(DocumentStatus.SHIPPABLE))).toBe(false);
+ });
+ });
+
+ describe('isInPreparation', () => {
+ it('returns true for status IN_PREPARATION', () => {
+ const doc = createMockDocument(DocumentStatus.IN_PREPARATION);
+ expect(isInPreparation(doc)).toBe(true);
+ });
+ });
+
+ describe('isInProductionQueue', () => {
+ it('returns true for status PRODUCTION_QUEUE', () => {
+ const doc = createMockDocument(DocumentStatus.PRODUCTION_QUEUE);
+ expect(isInProductionQueue(doc)).toBe(true);
+ });
+ });
+
+ describe('isPrinting', () => {
+ it('returns true for status PRINTING', () => {
+ const doc = createMockDocument(DocumentStatus.PRINTING);
+ expect(isPrinting(doc)).toBe(true);
+ });
+ });
+
+ describe('isSent', () => {
+ it('returns true for status SENT', () => {
+ const doc = createMockDocument(DocumentStatus.SENT);
+ expect(isSent(doc)).toBe(true);
+ });
+ });
+
+ describe('isCanceled', () => {
+ it('returns true for status CANCELED', () => {
+ const doc = createMockDocument(DocumentStatus.CANCELED);
+ expect(isCanceled(doc)).toBe(true);
+ });
+ });
+
+ describe('isTerminal', () => {
+ it('returns true for SENT, CANCELED, and ERRONEOUS', () => {
+ expect(isTerminal(createMockDocument(DocumentStatus.SENT))).toBe(true);
+ expect(isTerminal(createMockDocument(DocumentStatus.CANCELED))).toBe(true);
+ expect(isTerminal(createMockDocument(DocumentStatus.ERRONEOUS))).toBe(true);
+ });
+
+ it('returns false for non-terminal statuses', () => {
+ expect(isTerminal(createMockDocument(DocumentStatus.IN_PREPARATION))).toBe(false);
+ expect(isTerminal(createMockDocument(DocumentStatus.SHIPPABLE))).toBe(false);
+ expect(isTerminal(createMockDocument(DocumentStatus.PRODUCTION_QUEUE))).toBe(false);
+ expect(isTerminal(createMockDocument(DocumentStatus.PRINTING))).toBe(false);
+ });
+ });
+
+ describe('isCancelable', () => {
+ it('returns true for PRODUCTION_QUEUE and PRINTING', () => {
+ expect(isCancelable(createMockDocument(DocumentStatus.PRODUCTION_QUEUE))).toBe(true);
+ expect(isCancelable(createMockDocument(DocumentStatus.PRINTING))).toBe(true);
+ });
+
+ it('returns false for non-cancelable statuses', () => {
+ expect(isCancelable(createMockDocument(DocumentStatus.IN_PREPARATION))).toBe(false);
+ expect(isCancelable(createMockDocument(DocumentStatus.SENT))).toBe(false);
+ expect(isCancelable(createMockDocument(DocumentStatus.CANCELED))).toBe(false);
+ });
+ });
+});
+
+describe('Validation Helpers', () => {
+ const mockMessages: ValidationMessage[] = [
+ { type: 'ERROR', code: 'ERR001', message: 'Error 1' },
+ { type: 'ERROR', code: 'ERR002', message: 'Error 2' },
+ { type: 'WARNING', code: 'WARN001', message: 'Warning 1' },
+ { type: 'INFO', code: 'INFO001', message: 'Info 1' },
+ ];
+
+ describe('getErrors', () => {
+ it('returns only error messages', () => {
+ const doc = createMockDocument(DocumentStatus.ERRONEOUS, mockMessages);
+ const errors = getErrors(doc);
+ expect(errors).toHaveLength(2);
+ expect(errors.every(m => m.type === 'ERROR')).toBe(true);
+ });
+
+ it('returns empty array when no validation messages', () => {
+ const doc = createMockDocument(DocumentStatus.SHIPPABLE);
+ expect(getErrors(doc)).toEqual([]);
+ });
+ });
+
+ describe('getWarnings', () => {
+ it('returns only warning messages', () => {
+ const doc = createMockDocument(DocumentStatus.SHIPPABLE, mockMessages);
+ const warnings = getWarnings(doc);
+ expect(warnings).toHaveLength(1);
+ expect(warnings[0]?.type).toBe('WARNING');
+ });
+ });
+
+ describe('getInfoMessages', () => {
+ it('returns only info messages', () => {
+ const doc = createMockDocument(DocumentStatus.SHIPPABLE, mockMessages);
+ const info = getInfoMessages(doc);
+ expect(info).toHaveLength(1);
+ expect(info[0]?.type).toBe('INFO');
+ });
+ });
+
+ describe('hasErrors', () => {
+ it('returns true when document has errors', () => {
+ const doc = createMockDocument(DocumentStatus.ERRONEOUS, mockMessages);
+ expect(hasErrors(doc)).toBe(true);
+ });
+
+ it('returns false when document has no errors', () => {
+ const doc = createMockDocument(DocumentStatus.SHIPPABLE, [
+ { type: 'WARNING', code: 'WARN001', message: 'Warning' },
+ ]);
+ expect(hasErrors(doc)).toBe(false);
+ });
+ });
+
+ describe('hasWarnings', () => {
+ it('returns true when document has warnings', () => {
+ const doc = createMockDocument(DocumentStatus.SHIPPABLE, mockMessages);
+ expect(hasWarnings(doc)).toBe(true);
+ });
+
+ it('returns false when document has no warnings', () => {
+ const doc = createMockDocument(DocumentStatus.SHIPPABLE, [
+ { type: 'INFO', code: 'INFO001', message: 'Info' },
+ ]);
+ expect(hasWarnings(doc)).toBe(false);
+ });
+ });
+});
+
+describe('getStatusDescription', () => {
+ it('returns correct descriptions for all statuses', () => {
+ expect(getStatusDescription(DocumentStatus.IN_PREPARATION)).toBe('In preparation');
+ expect(getStatusDescription(DocumentStatus.SHIPPABLE)).toBe('Ready to ship');
+ expect(getStatusDescription(DocumentStatus.PRODUCTION_QUEUE)).toBe('In production queue');
+ expect(getStatusDescription(DocumentStatus.PRINTING)).toBe('Printing');
+ expect(getStatusDescription(DocumentStatus.SENT)).toBe('Sent');
+ expect(getStatusDescription(DocumentStatus.CANCELED)).toBe('Canceled');
+ expect(getStatusDescription(DocumentStatus.ERRONEOUS)).toBe('Has errors');
+ });
+
+ it('returns "Unknown status" for invalid status', () => {
+ expect(getStatusDescription(999 as DocumentStatus)).toBe('Unknown status');
+ });
+});
+
+describe('Polling Utilities', () => {
+ describe('pollUntil', () => {
+ it('returns immediately when condition is met', async () => {
+ let callCount = 0;
+ const result = await pollUntil(
+ async () => {
+ callCount++;
+ return { value: 42 };
+ },
+ (r) => r.value === 42,
+ { intervalMs: 10 }
+ );
+
+ expect(result.value).toBe(42);
+ expect(callCount).toBe(1);
+ });
+
+ it('polls until condition is met', async () => {
+ let callCount = 0;
+ const result = await pollUntil(
+ async () => {
+ callCount++;
+ return { value: callCount };
+ },
+ (r) => r.value >= 3,
+ { intervalMs: 10 }
+ );
+
+ expect(result.value).toBe(3);
+ expect(callCount).toBe(3);
+ });
+
+ it('throws when max attempts exceeded', async () => {
+ await expect(
+ pollUntil(
+ async () => ({ value: 1 }),
+ (r) => r.value > 100,
+ { intervalMs: 10, maxAttempts: 3 }
+ )
+ ).rejects.toThrow('Polling exceeded maximum attempts (3)');
+ });
+
+ it('respects abort signal', async () => {
+ const controller = new AbortController();
+
+ // Abort immediately
+ controller.abort();
+
+ await expect(
+ pollUntil(
+ async () => ({ value: 1 }),
+ () => false,
+ { intervalMs: 10, signal: controller.signal }
+ )
+ ).rejects.toThrow('Polling aborted');
+ });
+ });
+});
diff --git a/tests/http.test.ts b/tests/http.test.ts
new file mode 100644
index 0000000..9497b55
--- /dev/null
+++ b/tests/http.test.ts
@@ -0,0 +1,221 @@
+import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
+import { HttpClient, encodeBasicAuth, DEFAULT_BASE_URL } from '../src/http.js';
+import { BinectApiError, BinectAuthError } from '../src/errors.js';
+
+describe('encodeBasicAuth', () => {
+ it('encodes credentials correctly', () => {
+ const encoded = encodeBasicAuth('user@example.com', 'password123');
+ // "user@example.com:password123" in base64
+ expect(encoded).toBe('dXNlckBleGFtcGxlLmNvbTpwYXNzd29yZDEyMw==');
+ });
+
+ it('handles special characters', () => {
+ const encoded = encodeBasicAuth('user@example.com', 'p@ss:word!');
+ const decoded = Buffer.from(encoded, 'base64').toString('utf-8');
+ expect(decoded).toBe('user@example.com:p@ss:word!');
+ });
+});
+
+describe('DEFAULT_BASE_URL', () => {
+ it('points to Binect API', () => {
+ expect(DEFAULT_BASE_URL).toBe('https://app.binect.de/binectapi/v1');
+ });
+});
+
+describe('HttpClient', () => {
+ let client: HttpClient;
+ let mockFetch: ReturnType;
+
+ beforeEach(() => {
+ mockFetch = vi.fn();
+ global.fetch = mockFetch;
+
+ client = new HttpClient({
+ baseUrl: 'https://api.example.com',
+ username: 'testuser',
+ password: 'testpass',
+ });
+ });
+
+ afterEach(() => {
+ vi.restoreAllMocks();
+ });
+
+ describe('request', () => {
+ it('makes GET request with auth header', async () => {
+ mockFetch.mockResolvedValueOnce({
+ ok: true,
+ headers: new Headers({ 'content-type': 'application/json' }),
+ text: async () => JSON.stringify({ id: '123' }),
+ });
+
+ const result = await client.request<{ id: string }>({
+ method: 'GET',
+ path: '/test',
+ });
+
+ expect(mockFetch).toHaveBeenCalledWith(
+ 'https://api.example.com/test',
+ expect.objectContaining({
+ method: 'GET',
+ headers: expect.objectContaining({
+ Authorization: expect.stringMatching(/^Basic /),
+ Accept: 'application/json',
+ }),
+ })
+ );
+ expect(result.id).toBe('123');
+ });
+
+ it('makes POST request with JSON body', async () => {
+ mockFetch.mockResolvedValueOnce({
+ ok: true,
+ headers: new Headers({ 'content-type': 'application/json' }),
+ text: async () => JSON.stringify({ success: true }),
+ });
+
+ await client.request({
+ method: 'POST',
+ path: '/test',
+ body: { data: 'test' },
+ });
+
+ expect(mockFetch).toHaveBeenCalledWith(
+ 'https://api.example.com/test',
+ expect.objectContaining({
+ method: 'POST',
+ body: JSON.stringify({ data: 'test' }),
+ headers: expect.objectContaining({
+ 'Content-Type': 'application/json',
+ }),
+ })
+ );
+ });
+
+ it('adds query parameters to URL', async () => {
+ mockFetch.mockResolvedValueOnce({
+ ok: true,
+ headers: new Headers({ 'content-type': 'application/json' }),
+ text: async () => '{}',
+ });
+
+ await client.request({
+ method: 'GET',
+ path: '/test',
+ query: { limit: 10, offset: 20 },
+ });
+
+ expect(mockFetch).toHaveBeenCalledWith(
+ 'https://api.example.com/test?limit=10&offset=20',
+ expect.any(Object)
+ );
+ });
+
+ it('skips undefined query parameters', async () => {
+ mockFetch.mockResolvedValueOnce({
+ ok: true,
+ headers: new Headers({ 'content-type': 'application/json' }),
+ text: async () => '{}',
+ });
+
+ await client.request({
+ method: 'GET',
+ path: '/test',
+ query: { limit: 10, offset: undefined },
+ });
+
+ expect(mockFetch).toHaveBeenCalledWith(
+ 'https://api.example.com/test?limit=10',
+ expect.any(Object)
+ );
+ });
+
+ it('handles empty response', async () => {
+ mockFetch.mockResolvedValueOnce({
+ ok: true,
+ headers: new Headers({ 'content-type': 'text/plain' }),
+ text: async () => '',
+ });
+
+ const result = await client.request({ method: 'DELETE', path: '/test' });
+
+ expect(result).toBeUndefined();
+ });
+
+ it('throws BinectAuthError on 401', async () => {
+ mockFetch.mockResolvedValueOnce({
+ ok: false,
+ status: 401,
+ text: async () => JSON.stringify({ error: 'Unauthorized' }),
+ });
+
+ await expect(
+ client.request({ method: 'GET', path: '/test' })
+ ).rejects.toThrow(BinectAuthError);
+ });
+
+ it('throws BinectApiError on other errors', async () => {
+ mockFetch.mockResolvedValueOnce({
+ ok: false,
+ status: 404,
+ text: async () => JSON.stringify({ message: 'Not found' }),
+ });
+
+ await expect(
+ client.request({ method: 'GET', path: '/test' })
+ ).rejects.toThrow(BinectApiError);
+
+ try {
+ await client.request({ method: 'GET', path: '/test' });
+ } catch (e) {
+ if (e instanceof BinectApiError) {
+ expect(e.status).toBe(404);
+ expect(e.message).toBe('Not found');
+ }
+ }
+ });
+
+ it('handles non-JSON error response', async () => {
+ mockFetch.mockResolvedValueOnce({
+ ok: false,
+ status: 500,
+ text: async () => 'Internal Server Error',
+ });
+
+ // Now captures the raw text response as the error message
+ await expect(
+ client.request({ method: 'GET', path: '/test' })
+ ).rejects.toThrow('Internal Server Error');
+ });
+ });
+
+ describe('requestRaw', () => {
+ it('returns raw response for binary data', async () => {
+ const mockResponse = {
+ ok: true,
+ headers: new Headers({ 'content-type': 'application/pdf' }),
+ blob: async () => new Blob(['pdf content']),
+ };
+ mockFetch.mockResolvedValueOnce(mockResponse);
+
+ const response = await client.requestRaw({
+ method: 'GET',
+ path: '/documents/123/pdf',
+ });
+
+ expect(response).toBe(mockResponse);
+ });
+
+ it('throws on error response', async () => {
+ mockFetch.mockResolvedValueOnce({
+ ok: false,
+ status: 404,
+ text: async () => JSON.stringify({ message: 'Document not found' }),
+ });
+
+ await expect(
+ client.requestRaw({ method: 'GET', path: '/documents/123/pdf' })
+ ).rejects.toThrow(BinectApiError);
+ });
+ });
+});
diff --git a/tests/types.test.ts b/tests/types.test.ts
new file mode 100644
index 0000000..2331bb6
--- /dev/null
+++ b/tests/types.test.ts
@@ -0,0 +1,52 @@
+import { describe, it, expect } from 'vitest';
+import {
+ DocumentStatus,
+ EnvelopeType,
+ FrankingType,
+ ProductionCountry,
+ ResponseFormat,
+} from '../src/types.js';
+
+describe('Enums', () => {
+ describe('DocumentStatus', () => {
+ it('has correct values', () => {
+ expect(DocumentStatus.IN_PREPARATION).toBe(1);
+ expect(DocumentStatus.SHIPPABLE).toBe(2);
+ expect(DocumentStatus.PRODUCTION_QUEUE).toBe(3);
+ expect(DocumentStatus.PRINTING).toBe(4);
+ expect(DocumentStatus.SENT).toBe(5);
+ expect(DocumentStatus.CANCELED).toBe(6);
+ expect(DocumentStatus.ERRONEOUS).toBe(7);
+ });
+ });
+
+ describe('EnvelopeType', () => {
+ it('has correct values', () => {
+ expect(EnvelopeType.DINLANG).toBe('DINLANG');
+ expect(EnvelopeType.C4).toBe('C4');
+ });
+ });
+
+ describe('FrankingType', () => {
+ it('has correct values', () => {
+ expect(FrankingType.UNSPECIFIED).toBe('UNSPECIFIED');
+ expect(FrankingType.STANDARD_FRANKING).toBe('STANDARD_FRANKING');
+ expect(FrankingType.DV_FRANKING).toBe('DV_FRANKING');
+ });
+ });
+
+ describe('ProductionCountry', () => {
+ it('has correct values', () => {
+ expect(ProductionCountry.UNSPECIFIED).toBe('UNSPECIFIED');
+ expect(ProductionCountry.DE).toBe('DE');
+ expect(ProductionCountry.AT).toBe('AT');
+ });
+ });
+
+ describe('ResponseFormat', () => {
+ it('has correct values', () => {
+ expect(ResponseFormat.FULL).toBe('FULL');
+ expect(ResponseFormat.SHORT).toBe('SHORT');
+ });
+ });
+});
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..207bd1f
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,35 @@
+{
+ "compilerOptions": {
+ "target": "ES2022",
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "lib": ["ES2022", "DOM"],
+ "declaration": true,
+ "declarationMap": true,
+ "sourceMap": true,
+ "outDir": "./dist",
+ "rootDir": "./src",
+ "strict": true,
+ "noImplicitAny": true,
+ "strictNullChecks": true,
+ "strictFunctionTypes": true,
+ "strictBindCallApply": true,
+ "strictPropertyInitialization": true,
+ "noImplicitThis": true,
+ "useUnknownInCatchVariables": true,
+ "alwaysStrict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "exactOptionalPropertyTypes": false,
+ "noImplicitReturns": true,
+ "noFallthroughCasesInSwitch": true,
+ "noUncheckedIndexedAccess": true,
+ "noImplicitOverride": true,
+ "noPropertyAccessFromIndexSignature": true,
+ "esModuleInterop": true,
+ "skipLibCheck": true,
+ "forceConsistentCasingInFileNames": true
+ },
+ "include": ["src/**/*"],
+ "exclude": ["node_modules", "dist", "tests"]
+}
diff --git a/vitest.config.ts b/vitest.config.ts
new file mode 100644
index 0000000..da43663
--- /dev/null
+++ b/vitest.config.ts
@@ -0,0 +1,15 @@
+import { defineConfig } from 'vitest/config';
+
+export default defineConfig({
+ test: {
+ globals: true,
+ environment: 'node',
+ include: ['tests/**/*.test.ts'],
+ coverage: {
+ provider: 'v8',
+ reporter: ['text', 'json', 'html'],
+ include: ['src/**/*.ts'],
+ exclude: ['src/**/*.d.ts'],
+ },
+ },
+});