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 + + + +
+
+

Binect Explorer

+

Interactive tool for learning and testing the Binect API

+
+
+ +
+
+ + + + +
+
+
+ + + + + +
+ + +
+
+ + + +
+
+
+

Connect and load documents to see them here.

+
+
+ + +
+
+
+ + +

Maximum file size: 12 MB

+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ +
+
+
+ + +
+
+ +
+
+
+

Connect and load sendings to see them here.

+
+
+ + +
+
+ +
+
+
+

Connect and load attachments to see them here.

+
+
+ + +
+

Last API response will be shown here.

+
+ // No response yet +
+
+
+ + + +
+
+
+ + + + + + + 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'], + }, + }, +});