diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..420024a --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,35 @@ +name: Build, Test and Publish +on: + workflow_dispatch: + push: + branches: + - 'main' +jobs: + build-test-publish: + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - uses: pnpm/action-setup@v4 + - name: Use Node.js + uses: actions/setup-node@v4 + with: + node-version: '20.x' + cache: 'pnpm' + registry-url: 'https://registry.npmjs.org' + scope: '@4sure-tech' + - run: pnpm install + - run: pnpm build + - run: pnpm test:ci + + - name: "Setup git coordinates" + run: | + git remote set-url origin https://github.com/4sure-tech/vc-bitstring-status-lists.git + git config user.name ${{secrets.GH_USER}} + git config user.email ${{secrets.GH_EMAIL}} + + - name: Publish to npm + run: npm publish + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.gitignore b/.gitignore index 9a5aced..1a4c0cb 100644 --- a/.gitignore +++ b/.gitignore @@ -137,3 +137,4 @@ dist # Vite logs files vite.config.js.timestamp-* vite.config.ts.timestamp-* +/.idea/ diff --git a/README.md b/README.md index 81cc2b0..c44fb8a 100644 --- a/README.md +++ b/README.md @@ -1 +1,438 @@ -# vc-bitstring-status-lists \ No newline at end of file +# vc-bitstring-status-lists + +A TypeScript library implementing the [W3C Bitstring Status List v1.0 specification](https://www.w3.org/TR/vc-bitstring-status-list/) for privacy‑preserving credential status +management in Verifiable Credentials. + +## What is Bitstring Status List? + +Think of the Bitstring Status List as a privacy‑preserving way to check whether a credential has been revoked or suspended. Instead of maintaining a public database of revoked +credentials (which would reveal sensitive information), the specification uses a compressed bitstring where each bit represents the status of a credential. + +Here's how it works conceptually: imagine you have 100 000 credentials. Rather than listing "credential #1234 is revoked," you create a bitstring where position 1234 contains a bit +indicating the status. The entire bitstring is compressed and published, allowing anyone to check a credential's status without revealing which credentials they're checking. + +## Key Features + +This library provides a complete implementation of the W3C specification with the following capabilities: + +* **Direct credential access** through the `BitManager` class, which handles low‑level bit operations without requiring explicit entry creation +* **Compressed storage** using gzip compression and base64url encoding, meeting the W3C requirement for minimum 16 KB bitstrings +* **Uniform status width** where all credentials in a status list use the same number of bits for their status values +* **Full W3C compliance** including proper validation, minimum bitstring sizes, and status purpose matching +* **TypeScript support** with comprehensive type definitions for all specification interfaces + +## Installation + +```bash +pnpm install @4sure-tech/vc-bitstring-status-lists # or npm / yarn +``` + +--- + +# Quick Start + +Let's walk through creating and using a status list step by step. + +## 1 · Creating a Status List *in memory* + +```ts +import {BitstreamStatusList} from "vc-bitstring-status-lists"; + +// Create a new status list with 1‑bit status values (0 = valid, 1 = revoked) +const statusList = new BitstreamStatusList({statusSize: 1}); + +// Set credential statuses directly using their indices +statusList.setStatus(0, 0); // Credential at index 0 is valid +statusList.setStatus(1, 1); // Credential at index 1 is revoked +statusList.setStatus(42, 1); // Credential at index 42 is revoked +``` + +The key insight here is that you **don't need to "add" entries first**. The system automatically handles any credential index you reference, creating the necessary bit positions as +needed. + +## 2 · Publishing the Status List in a Credential + +```ts +import {createStatusListCredential} from "vc-bitstring-status-lists"; +import type {BitstringStatusListCredentialUnsigned} from "vc-bitstring-status-lists"; + +const statusListCredential: BitstringStatusListCredentialUnsigned = + await createStatusListCredential({ + id: "https://example.com/status-lists/1", + issuer: "https://example.com/issuer", + statusPurpose: "revocation", + statusSize: 1, // optional, 1 is default + statusList: statusList, // Optional: pass a preset list — if omitted the library creates an empty (all‑zero) list + validFrom: new Date("2025-07-01"), + validUntil: new Date("2026-07-01"), + }); + +console.log( + statusListCredential.credentialSubject.encodedList /* → "u…" */ +); +``` + +## 3 · Checking a Credential's Status + +```ts +import {checkStatus} from "vc-bitstring-status-lists"; + +// A credential that references the status list +const credential = { + "@context": ["https://www.w3.org/ns/credentials/v2"], + id: "https://example.com/credential/456", + type: ["VerifiableCredential"], + issuer: "https://example.com/issuer", + credentialSubject: { + id: "did:example:123", + type: "Person", + name: "Alice", + }, + credentialStatus: { + type: "BitstringStatusListEntry", + statusPurpose: "revocation", + statusListIndex: "1", // This credential is at index 1 in the status list + statusSize: 1, + statusListCredential: "https://example.com/status-lists/1", + }, +}; + +const result = await checkStatus({ + credential, + getStatusListCredential: async (url) => statusListCredential, +}); + +console.log(result); // { verified: false, status: 1 } +``` + +--- + +## Typical Issuer Workflow: *Create Empty → Update Later*  ⭐️ **(Most‑common use case)** + +Most issuers first publish an **empty** status‑list credential when onboarding a new batch of credentials, and only update individual bits as real‑world events (revocations, +suspensions, etc.) occur. +The library makes this flow trivial. + +### 1 · Create a *blank* Status List Credential + +```ts +import { + createStatusListCredential, + BitstreamStatusList, +} from "vc-bitstring-status-lists"; + +// Omit the `statusList` option → the library creates a zero‑filled list for you +const blankStatusListCredential = await createStatusListCredential({ + id: "https://example.com/status-lists/2025-issuer-1", + issuer: "https://example.com/issuer", + statusPurpose: "revocation", + // statusSize defaults to 1 +}); + +// Publish `blankStatusListCredential` at the URL above (IPFS, HTTPS, etc.) +``` + +### 2 · Issue Verifiable Credentials that reference the list + +Because all bits are `0`(valid) by default, you can safely reference the list immediately: + +```ts +function issueCredential(holderDid: string, listIndex: number) { + return { + "@context": ["https://www.w3.org/ns/credentials/v2"], + type: ["VerifiableCredential"], + issuer: "https://example.com/issuer", + credentialSubject: {id: holderDid}, + credentialStatus: { + type: "BitstringStatusListEntry", + statusPurpose: "revocation", + statusListIndex: String(listIndex), + statusSize: 1, + statusListCredential: "https://example.com/status-lists/2025-issuer-1", + }, + } satisfies Credential; +} +``` + +### 3 · Later: Update the Status of a Credential + +When you need to revoke (or otherwise change) a credential, fetch the current status list, mutate the relevant bit, re‑encode, and re‑publish. + +```ts +import {BitstreamStatusList} from "vc-bitstring-status-lists"; + +// 1. Fetch the existing credential (e.g. with `fetch`) and grab the encoded list string +const encoded = blankStatusListCredential.credentialSubject.encodedList; + +// 2. Decode into a mutable BitstreamStatusList +const list = await BitstreamStatusList.decode({encodedList: encoded, statusSize: 1}); + +// 3. Change status → index 123 now revoked +list.setStatus(123, 1); + +// 4. Re‑encode and splice back into the credential +blankStatusListCredential.credentialSubject.encodedList = await list.encode(); + +// 5. Re‑publish the **updated** credential at the same URL +``` + +*That’s it!* The credential you changed will now fail verification, while all others continue to pass. + +--- + +# Advanced Usage + +### Multi‑bit Status Values + +The specification supports more than just binary states. You can use multiple bits per credential to represent complex status information: + +```ts +// Create a status list with 4 bits per credential (supports values 0‑15) +const statusList = new BitstreamStatusList({statusSize: 4}); + +// Set complex status values +statusList.setStatus(0, 12); // Binary: 1100 (could represent multiple flags) +statusList.setStatus(1, 3); // Binary: 0011 (different status combination) + +console.log(statusList.getStatus(0)); // → 12 +``` + +### Status Messages + +Attach human‑readable messages to status codes: + +```ts +const credential = { + // … other VC properties … + credentialStatus: { + type: "BitstringStatusListEntry", + statusPurpose: "revocation", + statusListIndex: "0", + statusSize: 2, // Required for status values up to 0x2 + statusListCredential: "https://example.com/status-lists/1", + statusMessage: [ // "statusMessage MAY be present if statusSize is 1, and MUST be present if statusSize is greater than 1 + {id: "0x0", message: "Credential is valid"}, + {id: "0x1", message: "Credential has been revoked"}, + {id: "0x2", message: "Credential is under review"}, + ], + }, +}; +``` + +### Multiple `statusPurpose` Values (same list serves many purposes) + +A single status list can be re‑used for several independent purposes—e.g. *revocation* **and** *suspension*—by declaring an **array** in the credential: + +```ts +const multiPurposeListCredential = await createStatusListCredential({ + id: "https://example.com/status-lists/combo", + issuer: "https://example.com/issuer", + statusPurpose: [ + "revocation", // bit = 1 ⇒ revoked + "suspension", // bit = 1 ⇒ suspended + "kyc-level", // bit‑pair ⇒ 0=low,1=mid,2=high,3=banned + ], + statusSize: 2, // enough bits to hold the largest purpose (kyc-level) +}); +``` + +A credential can then choose which purpose applies to it: + +```ts +const credentialSuspended = { + credentialStatus: { + type: "BitstringStatusListEntry", + statusPurpose: "suspension", + statusListIndex: "42", + statusSize: 1, + statusListCredential: multiPurposeListCredential.id, + }, +}; +``` + +> **Tip:** When you pass an array to `statusPurpose`, each purpose gets its own contiguous *block* of bits in the list. Keep the `statusSize` large enough for the purpose that +> needs the most bits. + +### Working with Existing Status Lists + +```ts +// Decode a status list from an encoded string +const existing = await BitstreamStatusList.decode({ + encodedList: "u…", // from a credential + statusSize: 1, +}); + +console.log(existing.getStatus(42)); // status of credential 42 +existing.setStatus(100, 1); // revoke credential 100 + +// Re‑encode for publishing +const updatedEncoded = await existing.encode(); +``` + +--- + +# Understanding the Architecture + +The library is built around several key components that work together to provide a complete W3C‑compliant implementation: + +## BitManager Class + +The `BitManager` is the foundation that handles all low‑level bit operations. It manages a growing buffer of bytes and provides methods to set and get multi‑bit values at specific +positions. Think of it as a specialized array where you can efficiently pack multiple small integers. + +The key insight is that it calculates bit positions mathematically: credential index 42 with a 2‑bit status size occupies bits 84‑85 in the bitstring. This eliminates the need for +explicit entry management. + +## BitstreamStatusList Class + +The `BitstreamStatusList` wraps the `BitManager` and adds the W3C‑specific requirements like compression, encoding, and minimum size constraints. It ensures that the resulting +bitstring meets the specification's 16 KB minimum size requirement. + +## Verification Functions + +The `checkStatus` function implements the complete verification algorithm, including fetching the status list credential, validating time bounds, checking status purposes, and +extracting the actual status value. + +--- + +# W3C Compliance Checklist + +* **Minimum bitstring size**— Encoded status lists are padded to ≥ 16 KB (131 072 bits) +* **Compression**— gzip compression as required by the spec +* **Base64url encoding**— Proper encoding with the required "u" prefix +* **Status purpose validation**— Credential entries must match one of the list's declared purposes +* **Temporal validation**—`validFrom` / `validUntil` respected +* **Uniform status size**— Every credential in a list uses the same number of bits + +--- + +# API Reference + +## Core Classes + +### `BitstreamStatusList` + +```ts +class BitstreamStatusList { + constructor(options?: { buffer?: Uint8Array; statusSize?: number; initialSize?: number }); + + getStatus(credentialIndex: number): number; + + setStatus(credentialIndex: number, status: number): void; + + getStatusSize(): number; + + getLength(): number; + + encode(): Promise; + + static decode(options: { encodedList: string; statusSize?: number }): Promise; + + static getStatusListLength(encodedList: string, statusSize: number): number; +} +``` + +### `BitManager` + +Low‑level bit manipulation class (typically used internally). + +```ts +class BitManager { + constructor(options: { statusSize?: number; buffer?: Uint8Array; initialSize?: number }); + + getStatus(credentialIndex: number): number; + + setStatus(credentialIndex: number, status: number): void; + + getStatusSize(): number; + + getBufferLength(): number; + + toBuffer(): Uint8Array; +} +``` + +## High‑Level Functions + +### `createStatusListCredential` + +```ts +function createStatusListCredential(options: { + id: string; + issuer: string | IIssuer; + statusSize?: number; + statusList?: BitstreamStatusList; + statusPurpose: string | string[]; + validFrom?: Date; + validUntil?: Date; + ttl?: number; +}): Promise; +``` + +### `checkStatus` + +```ts +function checkStatus(options: { + credential: CredentialWithStatus; + getStatusListCredential: (url: string) => Promise; +}): Promise; +``` + +--- + +# Error Handling + +```ts +try { + const result = await checkStatus({credential, getStatusListCredential}); + if (!result.verified) { + console.log("Verification failed:", result.error?.message); + console.log("Status code:", result.status); + } +} catch (err) { + console.error("Status check failed:", err); +} +``` + +--- + +# Building & Testing + +```bash +# Build the library +pnpm run build + +# Run tests +pnpm test + +# The build outputs both ESM and CommonJS formats +# - dist/index.js (ESM) +# - dist/index.cjs (CommonJS) +# - dist/index.d.ts (TypeScript definitions) +``` + +--- + +# Contributing + +This library implements a W3C specification, so contributions should maintain strict compliance with +the [Bitstring Status List v1.0 specification](https://www.w3.org/TR/vc-bitstring-status-list/). When making changes, ensure that: + +1. All existing tests continue to pass +2. New features include comprehensive test coverage +3. The implementation remains compatible with the W3C specification +4. Type definitions are updated for any API changes + +--- + +# License + +Licensed under the Apache License, Version 2.0. + +--- + +# Related Resources + +* [W3C Bitstring Status List v1.0 Specification](https://www.w3.org/TR/vc-bitstring-status-list/) +* [W3C Verifiable Credentials Data Model](https://www.w3.org/TR/vc-data-model/) +* [Verifiable Credentials Implementation Guide](https://www.w3.org/TR/vc-imp-guide/) diff --git a/__tests__/BitManager.test.ts b/__tests__/BitManager.test.ts new file mode 100644 index 0000000..d95a934 --- /dev/null +++ b/__tests__/BitManager.test.ts @@ -0,0 +1,109 @@ +import {beforeEach, describe, expect, it} from 'vitest' +import {BitManager} from '../src' + +describe('BitManager', () => { + let bitManager: BitManager + + beforeEach(() => { + bitManager = new BitManager() + }) + + describe('constructor', () => { + it('should create with default size', () => { + const manager = new BitManager({}) + expect(manager.toBuffer()).toBeInstanceOf(Uint8Array) + }) + + it('should create with custom initial size', () => { + const manager = new BitManager({initialSize: 2048}) + expect(manager.toBuffer()).toBeInstanceOf(Uint8Array) + expect(manager.getStatus((2048 * 8) -1 )).toBe(0) + expect(() => manager.getStatus((2048 * 8))).toThrow('credentialIndex 16384 exceeds buffer bounds') + }) + + it('should create with existing buffer', () => { + const buffer = new Uint8Array([0xFF, 0x00, 0xFF]) + const manager = new BitManager({buffer}) + expect(manager.toBuffer()).toEqual(new Uint8Array([0xFF, 0x00, 0xFF])) + }) + + it('should create with custom status size', () => { + const manager = new BitManager({statusSize: 4}) + expect(manager.getStatusSize()).toBe(4) + }) + }) + + describe('getStatus', () => { + it('should return 0 for unset credential', () => { + expect(bitManager.getStatus(0)).toBe(0) + }) + + it('should return correct status for multi-bit entry', () => { + const manager = new BitManager({statusSize: 4}) + manager.setStatus(0, 5) + expect(manager.getStatus(0)).toBe(5) + }) + + it('should throw for negative credential index', () => { + expect(() => bitManager.getStatus(-1)).toThrow('credentialIndex must be a non-negative integer') + }) + + it('should throw for index exceeding buffer bounds', () => { + expect(() => bitManager.getStatus(132000)).toThrow('exceeds buffer bounds') + }) + }) + + describe('setStatus', () => { + it('should set status for single bit', () => { + bitManager.setStatus(0, 1) + expect(bitManager.getStatus(0)).toBe(1) + }) + + it('should set status for multi-bit entry', () => { + const manager = new BitManager({statusSize: 4}) + manager.setStatus(0, 15) + expect(manager.getStatus(0)).toBe(15) + }) + + it('should throw for negative credential index', () => { + expect(() => bitManager.setStatus(-1, 1)).toThrow('credentialIndex must be a non-negative integer') + }) + + it('should throw for negative status', () => { + expect(() => bitManager.setStatus(0, -1)).toThrow('status must be a non-negative integer') + }) + + it('should throw for status exceeding bit size', () => { + const manager = new BitManager({statusSize: 2}) + expect(() => manager.setStatus(0, 4)).toThrow('Status 4 exceeds maximum value 3 for 2 bits') + }) + + it('should handle multiple entries independently', () => { + const manager = new BitManager({statusSize: 2}) + manager.setStatus(0, 2) + manager.setStatus(1, 1) + expect(manager.getStatus(0)).toBe(2) + expect(manager.getStatus(1)).toBe(1) + }) + }) + + describe('toBuffer', () => { + it('should return empty buffer for no entries', () => { + expect(bitManager.toBuffer()).toEqual(new Uint8Array([])) + }) + + it('should return correct buffer size for entries', () => { + const manager = new BitManager({statusSize: 9}) + manager.setStatus(0, 1) + const buffer = manager.toBuffer() + expect(buffer.length).toBe(2) + }) + + it('should preserve bit patterns', () => { + const manager = new BitManager({statusSize: 8}) + manager.setStatus(0, 0b10101010) + const buffer = manager.toBuffer() + expect(buffer[0]).toBe(0b10101010) + }) + }) +}) \ No newline at end of file diff --git a/__tests__/StatusList.test.ts b/__tests__/StatusList.test.ts new file mode 100644 index 0000000..567c953 --- /dev/null +++ b/__tests__/StatusList.test.ts @@ -0,0 +1,201 @@ +import {beforeEach, describe, expect, it} from 'vitest' +import {BitstreamStatusList} from '../src' + +describe('StatusList', () => { + let statusList: BitstreamStatusList + + beforeEach(() => { + statusList = new BitstreamStatusList({statusSize: 1}) + }) + + describe('constructor', () => { + it('should create with default options', () => { + const list = new BitstreamStatusList() + expect(list).toBeInstanceOf(BitstreamStatusList) + expect(list.getStatusSize()).toBe(1) // default + }) + + it('should create with initial size', () => { + const list = new BitstreamStatusList({initialSize: 2048}) + expect(list).toBeInstanceOf(BitstreamStatusList) + expect(list.getStatus((2048 * 8) -1 )).toBe(0) + expect(() => list.getStatus((2048 * 8))).toThrow('credentialIndex 16384 exceeds buffer bounds') + }) + + it('should create with buffer', () => { + const buffer = new Uint8Array([0xFF, 0x00]) + const list = new BitstreamStatusList({buffer}) + expect(list).toBeInstanceOf(BitstreamStatusList) + }) + + it('should create with custom statusSize', () => { + const list = new BitstreamStatusList({statusSize: 4}) + expect(list.getStatusSize()).toBe(4) + }) + }) + + + describe('getStatus', () => { + it('should return 0 for new entry', () => { + expect(statusList.getStatus(0)).toBe(0) + }) + + it('should return set status', () => { + statusList.setStatus(0, 1) + expect(statusList.getStatus(0)).toBe(1) + }) + }) + + describe('setStatus', () => { + it('should set status correctly', () => { + statusList.setStatus(0, 1) + expect(statusList.getStatus(0)).toBe(1) + }) + + it('should set multi-bit status', () => { + const list = new BitstreamStatusList({statusSize: 4}) + list.setStatus(0, 12) + expect(list.getStatus(0)).toBe(12) + }) + }) + + describe('getStatusSize', () => { + it('should return the uniform statusSize', () => { + expect(statusList.getStatusSize()).toBe(1) + + const list4 = new BitstreamStatusList({statusSize: 4}) + expect(list4.getStatusSize()).toBe(4) + }) + }) + + describe('encode', () => { + it('should encode empty list', async () => { + const encoded = await statusList.encode() + expect(encoded).toMatch(/^u/) + }) + + it('should encode list with entries', async () => { + statusList.setStatus(0, 1) + statusList.setStatus(1, 1) + const encoded = await statusList.encode() + expect(encoded).toMatch(/^u/) + expect(encoded.length).toBeGreaterThan(1) + }) + }) + + describe('decode', () => { + it('should decode encoded list into a StatusList instance', async () => { + statusList.setStatus(0, 1) + statusList.setStatus(1, 0) + + const encoded = await statusList.encode() + const decoded = await BitstreamStatusList.decode({encodedList: encoded, statusSize: 1}) + + expect(decoded).toBeInstanceOf(BitstreamStatusList) + expect(decoded.getStatusSize()).toBe(1) + + expect(decoded.getStatus(0)).toBe(1) + expect(decoded.getStatus(1)).toBe(0) + }) + + it('should return correct length for decoded instance', async () => { + const list = new BitstreamStatusList({statusSize: 2}) + list.setStatus(0, 1) + list.setStatus(100, 2) + + const encoded = await list.encode() + const decoded = await BitstreamStatusList.decode({encodedList: encoded, statusSize: 2}) + + const length = decoded.getLength() + expect(length).toBe(65536) // (16384 * 8) / 2 + }) + + it('should handle round-trip encoding/decoding (uniform statusSize)', async () => { + const list4 = new BitstreamStatusList({statusSize: 4}) + list4.setStatus(0, 10) + list4.setStatus(1, 3) + + const encoded = await list4.encode() + const decoded = await BitstreamStatusList.decode({encodedList: encoded, statusSize: 4}) + + expect(decoded.getStatus(0)).toBe(10) + expect(decoded.getStatus(1)).toBe(3) + expect(decoded.getStatusSize()).toBe(4) + }) + + it('should preserve statusSize when decoding', async () => { + const list = new BitstreamStatusList({statusSize: 8}) + list.setStatus(0, 255) + + const encoded = await list.encode() + const decoded = await BitstreamStatusList.decode({encodedList: encoded, statusSize: 8}) + + expect(decoded.getStatusSize()).toBe(8) + expect(decoded.getStatus(0)).toBe(255) + }) + }) + + describe('getStatusListLength', () => { + it('should compute correct number of entries', async () => { + const list = new BitstreamStatusList({statusSize: 2}) + list.setStatus(0, 0) + list.setStatus(1, 0) + list.setStatus(2, 0) + + const encoded = await list.encode() + const length = BitstreamStatusList.getStatusListLength(encoded, 2) + + // With 16KB minimum and 2-bit statusSize: (16384 * 8) / 2 = 65536 entries + expect(length).toBe(65536) + }) + + it('should handle different statusSizes', async () => { + const list = new BitstreamStatusList({statusSize: 4}) + const encoded = await list.encode() + + const length = BitstreamStatusList.getStatusListLength(encoded, 4) + // With 16KB minimum and 4-bit statusSize: (16384 * 8) / 4 = 32768 entries + expect(length).toBe(32768) + }) + }) + + describe('StatusList W3C Compliance', () => { + describe('minimum 16KB requirement', () => { + it('should pad to minimum 16KB on encode', async () => { + statusList.setStatus(0, 1) + + const encoded = await statusList.encode() + const length = BitstreamStatusList.getStatusListLength(encoded, 1) + + // Should have at least 131072 bits / 1 bit per entry = 131072 entries + expect(length).toBeGreaterThanOrEqual(131072) + }) + + it('should reject lists shorter than 16KB on decode', async () => { + const shortBuffer = new Uint8Array(1024) + const compressed = await import('pako').then(pako => pako.gzip(shortBuffer)) + const {bytesToBase64url} = await import('../src/utils/base64') + const encoded = `u${bytesToBase64url(compressed)}` + + await expect( + BitstreamStatusList.decode({encodedList: encoded, statusSize: 1}) + ).rejects.toThrow('Status list must be at least') + }) + + it('should handle large buffers correctly', async () => { + const list = new BitstreamStatusList({statusSize: 1}) + // Add many entries to exceed 16KB naturally + for (let i = 0; i < 150000; i++) { + list.setStatus(i, 0) + } + + const encoded = await list.encode() + const length = BitstreamStatusList.getStatusListLength(encoded, 1) + expect(length).toBeGreaterThanOrEqual(131072) + + const decoded = await BitstreamStatusList.decode({encodedList: encoded, statusSize: 1}) + expect(() => decoded.getStatus(200000)).toThrow('exceeds buffer bounds') + }) + }) + }) +}) diff --git a/__tests__/create.test.ts b/__tests__/create.test.ts new file mode 100644 index 0000000..7a4b295 --- /dev/null +++ b/__tests__/create.test.ts @@ -0,0 +1,134 @@ +import {describe, expect, it} from 'vitest' +import {BitstreamStatusList, createStatusListCredential} from '../src' + +describe('createStatusListCredential', () => { + it('should create basic credential', async () => { + const credential = await createStatusListCredential({ + id: 'https://example.com/status/1', + issuer: 'https://example.com/issuer', + statusPurpose: 'revocation' + }) + + expect(credential).toMatchObject({ + '@context': [ + 'https://www.w3.org/ns/credentials/v2', + 'https://www.w3.org/ns/credentials/status/v1' + ], + id: 'https://example.com/status/1', + type: ['VerifiableCredential', 'BitstringStatusListCredential'], + issuer: 'https://example.com/issuer', + credentialSubject: { + id: 'https://example.com/status/1#list', + type: 'BitstringStatusList', + statusPurpose: 'revocation', + encodedList: expect.stringMatching(/^u/) + } + }) + }) + + it('should create credential with issuer object', async () => { + const issuer = {id: 'https://example.com/issuer', name: 'Test Issuer'} + + const credential = await createStatusListCredential({ + id: 'https://example.com/status/1', + issuer, + statusPurpose: 'revocation', + statusSize: 2 + }) + + expect(credential.issuer).toEqual(issuer) + }) + + it('should create credential with validity period', async () => { + const validFrom = new Date('2024-01-01T00:00:00Z') + const validUntil = new Date('2024-12-31T23:59:59Z') + + const credential = await createStatusListCredential({ + id: 'https://example.com/status/1', + issuer: 'https://example.com/issuer', + statusPurpose: 'revocation', + statusSize: 3, + validFrom, + validUntil + }) + + expect(credential.validFrom).toBe('2024-01-01T00:00:00.000Z') + expect(credential.validUntil).toBe('2024-12-31T23:59:59.000Z') + }) + + it('should create credential with multiple status purposes', async () => { + const statusPurpose = ['revocation', 'suspension'] + + const credential = await createStatusListCredential({ + id: 'https://example.com/status/1', + issuer: 'https://example.com/issuer', + statusPurpose + }) + + expect(credential.credentialSubject.statusPurpose).toEqual(statusPurpose) + }) + + it('should create credential with TTL', async () => { + const ttl = 86400000 // 24 hours + + const credential = await createStatusListCredential({ + id: 'https://example.com/status/1', + issuer: 'https://example.com/issuer', + statusPurpose: 'revocation', + ttl + }) + + expect(credential.credentialSubject.ttl).toBe(ttl) + }) + + it('should create credential with custom statusSize', async () => { + const credential = await createStatusListCredential({ + id: 'https://example.com/status/1', + issuer: 'https://example.com/issuer', + statusPurpose: 'message', + statusSize: 4 + }) + + expect(credential.credentialSubject.encodedList).toMatch(/^u/) + }) + + it('should create empty status list by default', async () => { + const credential = await createStatusListCredential({ + id: 'https://example.com/status/1', + issuer: 'https://example.com/issuer', + statusPurpose: 'revocation' + }) + + // Verify the encoded list can be decoded + const statusList = await BitstreamStatusList.decode({ + encodedList: credential.credentialSubject.encodedList, + statusSize: 1 + }) + + expect(statusList.getStatusSize()).toBe(1) + }) + + it('should create credential with existing status list', async () => { + // Create a status list with some data + const existingList = new BitstreamStatusList({statusSize: 2}) + existingList.setStatus(0, 1) + existingList.setStatus(5, 3) + + const credential = await createStatusListCredential({ + id: 'https://example.com/status/1', + issuer: 'https://example.com/issuer', + statusPurpose: 'revocation', + statusSize: 2, + statusList: existingList + }) + + // Decode and verify the status list contains the existing data + const decoded = await BitstreamStatusList.decode({ + encodedList: credential.credentialSubject.encodedList, + statusSize: 2 + }) + + expect(decoded.getStatus(0)).toBe(1) + expect(decoded.getStatus(5)).toBe(3) + }) +}) diff --git a/__tests__/index.test.ts b/__tests__/index.test.ts new file mode 100644 index 0000000..a92d2ae --- /dev/null +++ b/__tests__/index.test.ts @@ -0,0 +1,37 @@ +import { describe, it, expect } from 'vitest' +import * as index from '../src/index' + +describe('index exports', () => { + it('should export BitManager', () => { + expect(index.BitManager).toBeDefined() + expect(typeof index.BitManager).toBe('function') + }) + + it('should export StatusList', () => { + expect(index.BitstreamStatusList).toBeDefined() + expect(typeof index.BitstreamStatusList).toBe('function') + }) + + it('should export createStatusListCredential', () => { + expect(index.createStatusListCredential).toBeDefined() + expect(typeof index.createStatusListCredential).toBe('function') + }) + + it('should export checkStatus', () => { + expect(index.checkStatus).toBeDefined() + expect(typeof index.checkStatus).toBe('function') + }) + + it('should have all expected exports', () => { + const expectedExports = [ + 'BitManager', + 'BitstreamStatusList', + 'createStatusListCredential', + 'checkStatus' + ] + + expectedExports.forEach(exportName => { + expect(index).toHaveProperty(exportName) + }) + }) +}) diff --git a/__tests__/verify.test.ts b/__tests__/verify.test.ts new file mode 100644 index 0000000..a0f018d --- /dev/null +++ b/__tests__/verify.test.ts @@ -0,0 +1,320 @@ +import {describe, expect, it} from 'vitest' +import {checkStatus, BitstreamStatusList} from '../src' +import {BitstringStatusListCredentialUnsigned, BitstringStatusListEntry, CredentialWithStatus} from '../src/types' + +describe('checkStatus', () => { + const createMockCredential = ( + statusPurpose = 'revocation', + statusIndex = '0', + statusSize?: number + ): CredentialWithStatus => ({ + '@context': ['https://www.w3.org/ns/credentials/v2'], + id: 'https://example.com/credential/1', + type: ['VerifiableCredential'], + issuer: 'https://example.com/issuer', + credentialSubject: { + id: 'did:example:123', + type: 'Person' + }, + credentialStatus: { + type: 'BitstringStatusListEntry', + statusPurpose, + statusListIndex: statusIndex, + statusListCredential: 'https://example.com/status/1', + ...(statusSize && {statusSize}) + } as BitstringStatusListEntry + }) + + const createStatusListCredential = async ( + statuses: number[] = [0], + statusSize = 1, + statusPurpose: string | string[] = 'revocation' + ): Promise => { + const list = new BitstreamStatusList({statusSize}) + statuses.forEach((stat, idx) => { + list.setStatus(idx, stat) + }) + + return { + '@context': ['https://www.w3.org/ns/credentials/v2'], + id: 'https://example.com/status/1', + type: ['VerifiableCredential', 'BitstringStatusListCredential'], + issuer: 'https://example.com/issuer', + credentialSubject: { + id: 'https://example.com/status/1#list', + type: 'BitstringStatusList', + statusPurpose, + statusSize, + encodedList: await list.encode() + } + } + } + + it('should verify valid credential (status 0)', async () => { + const credential = createMockCredential() + const statusListCred = await createStatusListCredential([0]) + + const result = await checkStatus({ + credential, + getStatusListCredential: async (url) => { + return statusListCred + } + }) + expect(result.verified).toBe(true) + expect(result.status).toBe(0) + expect(result.error).toBeUndefined() + }) + + + it('should verify revoked credential (status 1)', async () => { + const credential = createMockCredential('revocation', '0') + const statusListCred = await createStatusListCredential([1]) + + const result = await checkStatus({ + credential, + getStatusListCredential: async () => statusListCred + }) + expect(result.verified).toBe(false) + expect(result.status).toBe(1) + }) + + it('should handle status messages in credential status entry', async () => { + const credential = createMockCredential() + ;(credential.credentialStatus as BitstringStatusListEntry).statusMessage = [ + {id: '0x0', message: 'Valid'}, + {id: '0x1', message: 'Revoked'} + ] + const statusListCred = await createStatusListCredential([1]) + + const result = await checkStatus({ + credential, + getStatusListCredential: async () => statusListCred + }) + expect(result.verified).toBe(false) + expect(result.status).toBe(1) + expect(result.statusMessage).toEqual({id: '0x1', message: 'Revoked'}) + }) + + it('should handle array of credential statuses', async () => { + const credential = { + ...createMockCredential(), + credentialStatus: [ + { + type: 'SomeOtherStatusType' as any, + statusPurpose: 'other', + statusListIndex: '0', + statusListCredential: 'https://example.com/other' + }, + createMockCredential().credentialStatus + ] + } as CredentialWithStatus + + const statusListCred = await createStatusListCredential([0]) + + const result = await checkStatus({ + credential, + getStatusListCredential: async () => statusListCred + }) + expect(result.verified).toBe(true) + expect(result.status).toBe(0) + }) + + it('should reject credential without BitstringStatusListEntry', async () => { + const credential = { + ...createMockCredential(), + credentialStatus: { + statusPurpose: 'revocation', + statusListIndex: '0', + statusListCredential: 'https://example.com/status/1' + } as any + } as CredentialWithStatus + + const result = await checkStatus({ + credential, + getStatusListCredential: async () => { + throw new Error('Should not be called') + } + }) + expect(result.verified).toBe(false) + expect(result.status).toBe(-1) + expect(result.error?.message).toBe('No BitstringStatusListEntry found in credentialStatus') + }) + + it('should reject expired status list credential', async () => { + const credential = createMockCredential() + const base = await createStatusListCredential([0]) + const expired = {...base, validUntil: new Date(Date.now() - 86400000).toISOString()} + + const result = await checkStatus({ + credential, + getStatusListCredential: async () => expired + }) + expect(result.verified).toBe(false) + expect(result.status).toBe(-1) + expect(result.error?.message).toBe('Status list credential has expired') + }) + + it('should reject not-yet-valid status list credential', async () => { + const credential = createMockCredential() + const base = await createStatusListCredential([0]) + const future = {...base, validFrom: new Date(Date.now() + 86400000).toISOString()} + + const result = await checkStatus({ + credential, + getStatusListCredential: async () => future + }) + expect(result.verified).toBe(false) + expect(result.status).toBe(-1) + expect(result.error?.message).toBe('Status list credential is not yet valid') + }) + + it('should handle index out of bounds', async () => { + const credential = createMockCredential('revocation', '132000') + const statusListCred = await createStatusListCredential([0]) + + const result = await checkStatus({ + credential, + getStatusListCredential: async () => statusListCred + }) + expect(result.verified).toBe(false) + expect(result.status).toBe(-1) + expect(result.error?.message).toContain('credentialIndex 132000 exceeds buffer bounds') + }) + + it('should handle getStatusListCredential rejection', async () => { + const credential = createMockCredential() + + const result = await checkStatus({ + credential, + getStatusListCredential: async () => { + throw new Error('Network error') + } + }) + expect(result.verified).toBe(false) + expect(result.status).toBe(-1) + expect(result.error?.message).toBe('Network error') + }) + + it('should handle invalid credential parameter', async () => { + const result = await checkStatus({ + credential: null as any, + getStatusListCredential: async () => { + throw new Error('Should not be called') + } + }) + expect(result.verified).toBe(false) + expect(result.status).toBe(-1) + expect(result.error).toBeDefined() + }) + + it('should handle multi-bit status entries', async () => { + const credential = createMockCredential('message', '0', 4) + const statusListCred = await createStatusListCredential([12], 4, 'message') + + const result = await checkStatus({ + credential, + getStatusListCredential: async () => statusListCred + }) + expect(result.verified).toBe(true) + expect(result.status).toBe(12) + }) + + describe('checkStatus W3C Compliance', () => { + describe('statusPurpose validation', () => { + it('should reject mismatched statusPurpose', async () => { + const credential = createMockCredential('suspension') + const statusListCred = await createStatusListCredential([0], 1, 'revocation') + + const result = await checkStatus({ + credential, + getStatusListCredential: async () => statusListCred + }) + expect(result.verified).toBe(false) + expect(result.error?.message).toContain('Status purpose') + }) + + it('should accept matching statusPurpose in array', async () => { + const credential = createMockCredential('revocation') + const statusListCred = await createStatusListCredential([0], 1, ['revocation', 'suspension']) + + const result = await checkStatus({ + credential, + getStatusListCredential: async () => statusListCred + }) + expect(result.verified).toBe(true) + }) + }) + + describe('minimum bitstring length validation', () => { + it('should accept lists that are automatically padded', async () => { + const credential = createMockCredential('revocation', '0') + const smallList = new BitstreamStatusList({statusSize: 1, initialSize: 10}) + smallList.setStatus(0, 0) + + const statusListCred = { + '@context': ['https://www.w3.org/ns/credentials/v2'], + id: 'https://example.com/status/1', + type: ['VerifiableCredential', 'BitstringStatusListCredential'], + issuer: 'https://example.com/issuer', + credentialSubject: { + id: 'https://example.com/status/1#list', + type: 'BitstringStatusList', + statusPurpose: 'revocation', + statusSize: 1, + encodedList: await smallList.encode() + } + } + + const result = await checkStatus({ + credential, + getStatusListCredential: async () => statusListCred as BitstringStatusListCredentialUnsigned + }) + + expect(result.verified).toBe(true) + expect(result.status).toBe(0) + }) + + it('should accept lists meeting minimum length requirement', async () => { + const credential = createMockCredential('revocation', '0') + const statusListCred = await createStatusListCredential([0]) + + + const result = await checkStatus({ + credential, + getStatusListCredential: async () => statusListCred + }) + + expect(result.verified).toBe(true) + expect(result.status).toBe(0) + }) + }) + + describe('statusSize handling', () => { + it('should handle statusSize mismatch gracefully', async () => { + const credential = createMockCredential('revocation', '0', 4) + const statusListCred = await createStatusListCredential([1], 1, 'revocation') + + const result = await checkStatus({ + credential, + getStatusListCredential: async () => statusListCred + }) + + expect(result.verified).toBe(false) + expect(result.status).toBe(-1) + }) + + it('should handle default statusSize', async () => { + const credential = createMockCredential('revocation', '0') + const statusListCred = await createStatusListCredential([1]) + + const result = await checkStatus({ + credential, + getStatusListCredential: async () => statusListCred + }) + + expect(result.verified).toBe(false) + expect(result.status).toBe(1) + }) + }) + }) +}) \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..26f8dfe --- /dev/null +++ b/package.json @@ -0,0 +1,78 @@ +{ + "name": "@4sure-tech/vc-bitstring-status-lists", + "version": "0.1.0", + "description": "TypeScript library for W3C Bitstring Status List v1.0 specification - privacy-preserving credential status management", + "source": "src/index.ts", + "type": "module", + "main": "./dist/index.cjs", + "module": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + "import": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + }, + "require": { + "types": "./dist/index.d.cts", + "require": "./dist/index.cjs" + } + }, + "scripts": { + "build": "tsup", + "test:ci": "vitest" + }, + "keywords": [ + "verifiable-credentials", + "bitstring", + "status-list", + "w3c", + "revocation", + "suspension", + "typescript" + ], + "author": "4Sure Technology Solutions", + "contributors": [ + { + "name": "Sander Postma", + "email": "info@4sure.tech", + "url": "https://github.com/sanderPostma" + } + ], + "repository": { + "type": "git", + "url": "https://github.com/4sure-tech/vc-bitstring-status-lists.git" + }, + "bugs": { + "url": "https://github.com/4sure-tech/vc-bitstring-status-lists/issues" + }, + "homepage": "https://4sure.tech", + "license": "Apache-2.0", + "packageManager": "pnpm@10.13.1", + "engines": { + "node": ">=18" + }, + "files": [ + "dist", + "README.md", + "LICENSE" + ], + "sideEffects": false, + "dependencies": { + "base64url-universal": "^2.0.0", + "pako": "^2.1.0", + "uint8arrays": "^5.1.0" + }, + "devDependencies": { + "@types/node": "^24.0.12", + "@types/pako": "^2.0.3", + "tsup": "^8.5.0", + "turbo": "^2.5.4", + "typescript": "^5.8.3", + "vite": "^7.0.3", + "vite-tsconfig-paths": "^5.1.4", + "vitest": "^3.2.4" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..722ffd4 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,1697 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + base64url-universal: + specifier: ^2.0.0 + version: 2.0.0 + pako: + specifier: ^2.1.0 + version: 2.1.0 + uint8arrays: + specifier: ^5.1.0 + version: 5.1.0 + devDependencies: + '@types/node': + specifier: ^24.0.12 + version: 24.0.12 + '@types/pako': + specifier: ^2.0.3 + version: 2.0.3 + tsup: + specifier: ^8.5.0 + version: 8.5.0(postcss@8.5.6)(typescript@5.8.3) + turbo: + specifier: ^2.5.4 + version: 2.5.4 + typescript: + specifier: ^5.8.3 + version: 5.8.3 + vite: + specifier: ^7.0.3 + version: 7.0.3(@types/node@24.0.12) + vite-tsconfig-paths: + specifier: ^5.1.4 + version: 5.1.4(typescript@5.8.3)(vite@7.0.3(@types/node@24.0.12)) + vitest: + specifier: ^3.2.4 + version: 3.2.4(@types/node@24.0.12) + +packages: + + '@esbuild/aix-ppc64@0.25.6': + resolution: {integrity: sha512-ShbM/3XxwuxjFiuVBHA+d3j5dyac0aEVVq1oluIDf71hUw0aRF59dV/efUsIwFnR6m8JNM2FjZOzmaZ8yG61kw==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.25.6': + resolution: {integrity: sha512-hd5zdUarsK6strW+3Wxi5qWws+rJhCCbMiC9QZyzoxfk5uHRIE8T287giQxzVpEvCwuJ9Qjg6bEjcRJcgfLqoA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.25.6': + resolution: {integrity: sha512-S8ToEOVfg++AU/bHwdksHNnyLyVM+eMVAOf6yRKFitnwnbwwPNqKr3srzFRe7nzV69RQKb5DgchIX5pt3L53xg==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.25.6': + resolution: {integrity: sha512-0Z7KpHSr3VBIO9A/1wcT3NTy7EB4oNC4upJ5ye3R7taCc2GUdeynSLArnon5G8scPwaU866d3H4BCrE5xLW25A==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.25.6': + resolution: {integrity: sha512-FFCssz3XBavjxcFxKsGy2DYK5VSvJqa6y5HXljKzhRZ87LvEi13brPrf/wdyl/BbpbMKJNOr1Sd0jtW4Ge1pAA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.25.6': + resolution: {integrity: sha512-GfXs5kry/TkGM2vKqK2oyiLFygJRqKVhawu3+DOCk7OxLy/6jYkWXhlHwOoTb0WqGnWGAS7sooxbZowy+pK9Yg==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.25.6': + resolution: {integrity: sha512-aoLF2c3OvDn2XDTRvn8hN6DRzVVpDlj2B/F66clWd/FHLiHaG3aVZjxQX2DYphA5y/evbdGvC6Us13tvyt4pWg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.25.6': + resolution: {integrity: sha512-2SkqTjTSo2dYi/jzFbU9Plt1vk0+nNg8YC8rOXXea+iA3hfNJWebKYPs3xnOUf9+ZWhKAaxnQNUf2X9LOpeiMQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.25.6': + resolution: {integrity: sha512-b967hU0gqKd9Drsh/UuAm21Khpoh6mPBSgz8mKRq4P5mVK8bpA+hQzmm/ZwGVULSNBzKdZPQBRT3+WuVavcWsQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.25.6': + resolution: {integrity: sha512-SZHQlzvqv4Du5PrKE2faN0qlbsaW/3QQfUUc6yO2EjFcA83xnwm91UbEEVx4ApZ9Z5oG8Bxz4qPE+HFwtVcfyw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.25.6': + resolution: {integrity: sha512-aHWdQ2AAltRkLPOsKdi3xv0mZ8fUGPdlKEjIEhxCPm5yKEThcUjHpWB1idN74lfXGnZ5SULQSgtr5Qos5B0bPw==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.25.6': + resolution: {integrity: sha512-VgKCsHdXRSQ7E1+QXGdRPlQ/e08bN6WMQb27/TMfV+vPjjTImuT9PmLXupRlC90S1JeNNW5lzkAEO/McKeJ2yg==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.25.6': + resolution: {integrity: sha512-WViNlpivRKT9/py3kCmkHnn44GkGXVdXfdc4drNmRl15zVQ2+D2uFwdlGh6IuK5AAnGTo2qPB1Djppj+t78rzw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.25.6': + resolution: {integrity: sha512-wyYKZ9NTdmAMb5730I38lBqVu6cKl4ZfYXIs31Baf8aoOtB4xSGi3THmDYt4BTFHk7/EcVixkOV2uZfwU3Q2Jw==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.25.6': + resolution: {integrity: sha512-KZh7bAGGcrinEj4qzilJ4hqTY3Dg2U82c8bv+e1xqNqZCrCyc+TL9AUEn5WGKDzm3CfC5RODE/qc96OcbIe33w==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.25.6': + resolution: {integrity: sha512-9N1LsTwAuE9oj6lHMyyAM+ucxGiVnEqUdp4v7IaMmrwb06ZTEVCIs3oPPplVsnjPfyjmxwHxHMF8b6vzUVAUGw==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.25.6': + resolution: {integrity: sha512-A6bJB41b4lKFWRKNrWoP2LHsjVzNiaurf7wyj/XtFNTsnPuxwEBWHLty+ZE0dWBKuSK1fvKgrKaNjBS7qbFKig==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.25.6': + resolution: {integrity: sha512-IjA+DcwoVpjEvyxZddDqBY+uJ2Snc6duLpjmkXm/v4xuS3H+3FkLZlDm9ZsAbF9rsfP3zeA0/ArNDORZgrxR/Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.25.6': + resolution: {integrity: sha512-dUXuZr5WenIDlMHdMkvDc1FAu4xdWixTCRgP7RQLBOkkGgwuuzaGSYcOpW4jFxzpzL1ejb8yF620UxAqnBrR9g==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.25.6': + resolution: {integrity: sha512-l8ZCvXP0tbTJ3iaqdNf3pjaOSd5ex/e6/omLIQCVBLmHTlfXW3zAxQ4fnDmPLOB1x9xrcSi/xtCWFwCZRIaEwg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.25.6': + resolution: {integrity: sha512-hKrmDa0aOFOr71KQ/19JC7az1P0GWtCN1t2ahYAf4O007DHZt/dW8ym5+CUdJhQ/qkZmI1HAF8KkJbEFtCL7gw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.25.6': + resolution: {integrity: sha512-+SqBcAWoB1fYKmpWoQP4pGtx+pUUC//RNYhFdbcSA16617cchuryuhOCRpPsjCblKukAckWsV+aQ3UKT/RMPcA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.25.6': + resolution: {integrity: sha512-dyCGxv1/Br7MiSC42qinGL8KkG4kX0pEsdb0+TKhmJZgCUDBGmyo1/ArCjNGiOLiIAgdbWgmWgib4HoCi5t7kA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.25.6': + resolution: {integrity: sha512-42QOgcZeZOvXfsCBJF5Afw73t4veOId//XD3i+/9gSkhSV6Gk3VPlWncctI+JcOyERv85FUo7RxuxGy+z8A43Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.25.6': + resolution: {integrity: sha512-4AWhgXmDuYN7rJI6ORB+uU9DHLq/erBbuMoAuB4VWJTu5KtCgcKYPynF0YI1VkBNuEfjNlLrFr9KZPJzrtLkrQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.25.6': + resolution: {integrity: sha512-NgJPHHbEpLQgDH2MjQu90pzW/5vvXIZ7KOnPyNBm92A6WgZ/7b6fJyUBjoumLqeOQQGqY2QjQxRo97ah4Sj0cA==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@isaacs/cliui@8.0.2': + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} + + '@jridgewell/gen-mapping@0.3.12': + resolution: {integrity: sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.4': + resolution: {integrity: sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==} + + '@jridgewell/trace-mapping@0.3.29': + resolution: {integrity: sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==} + + '@pkgjs/parseargs@0.11.0': + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} + engines: {node: '>=14'} + + '@rollup/rollup-android-arm-eabi@4.44.2': + resolution: {integrity: sha512-g0dF8P1e2QYPOj1gu7s/3LVP6kze9A7m6x0BZ9iTdXK8N5c2V7cpBKHV3/9A4Zd8xxavdhK0t4PnqjkqVmUc9Q==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.44.2': + resolution: {integrity: sha512-Yt5MKrOosSbSaAK5Y4J+vSiID57sOvpBNBR6K7xAaQvk3MkcNVV0f9fE20T+41WYN8hDn6SGFlFrKudtx4EoxA==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.44.2': + resolution: {integrity: sha512-EsnFot9ZieM35YNA26nhbLTJBHD0jTwWpPwmRVDzjylQT6gkar+zenfb8mHxWpRrbn+WytRRjE0WKsfaxBkVUA==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.44.2': + resolution: {integrity: sha512-dv/t1t1RkCvJdWWxQ2lWOO+b7cMsVw5YFaS04oHpZRWehI1h0fV1gF4wgGCTyQHHjJDfbNpwOi6PXEafRBBezw==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.44.2': + resolution: {integrity: sha512-W4tt4BLorKND4qeHElxDoim0+BsprFTwb+vriVQnFFtT/P6v/xO5I99xvYnVzKWrK6j7Hb0yp3x7V5LUbaeOMg==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.44.2': + resolution: {integrity: sha512-tdT1PHopokkuBVyHjvYehnIe20fxibxFCEhQP/96MDSOcyjM/shlTkZZLOufV3qO6/FQOSiJTBebhVc12JyPTA==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.44.2': + resolution: {integrity: sha512-+xmiDGGaSfIIOXMzkhJ++Oa0Gwvl9oXUeIiwarsdRXSe27HUIvjbSIpPxvnNsRebsNdUo7uAiQVgBD1hVriwSQ==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.44.2': + resolution: {integrity: sha512-bDHvhzOfORk3wt8yxIra8N4k/N0MnKInCW5OGZaeDYa/hMrdPaJzo7CSkjKZqX4JFUWjUGm88lI6QJLCM7lDrA==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.44.2': + resolution: {integrity: sha512-NMsDEsDiYghTbeZWEGnNi4F0hSbGnsuOG+VnNvxkKg0IGDvFh7UVpM/14mnMwxRxUf9AdAVJgHPvKXf6FpMB7A==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.44.2': + resolution: {integrity: sha512-lb5bxXnxXglVq+7imxykIp5xMq+idehfl+wOgiiix0191av84OqbjUED+PRC5OA8eFJYj5xAGcpAZ0pF2MnW+A==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-loongarch64-gnu@4.44.2': + resolution: {integrity: sha512-Yl5Rdpf9pIc4GW1PmkUGHdMtbx0fBLE1//SxDmuf3X0dUC57+zMepow2LK0V21661cjXdTn8hO2tXDdAWAqE5g==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-powerpc64le-gnu@4.44.2': + resolution: {integrity: sha512-03vUDH+w55s680YYryyr78jsO1RWU9ocRMaeV2vMniJJW/6HhoTBwyyiiTPVHNWLnhsnwcQ0oH3S9JSBEKuyqw==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.44.2': + resolution: {integrity: sha512-iYtAqBg5eEMG4dEfVlkqo05xMOk6y/JXIToRca2bAWuqjrJYJlx/I7+Z+4hSrsWU8GdJDFPL4ktV3dy4yBSrzg==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-riscv64-musl@4.44.2': + resolution: {integrity: sha512-e6vEbgaaqz2yEHqtkPXa28fFuBGmUJ0N2dOJK8YUfijejInt9gfCSA7YDdJ4nYlv67JfP3+PSWFX4IVw/xRIPg==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.44.2': + resolution: {integrity: sha512-evFOtkmVdY3udE+0QKrV5wBx7bKI0iHz5yEVx5WqDJkxp9YQefy4Mpx3RajIVcM6o7jxTvVd/qpC1IXUhGc1Mw==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.44.2': + resolution: {integrity: sha512-/bXb0bEsWMyEkIsUL2Yt5nFB5naLAwyOWMEviQfQY1x3l5WsLKgvZf66TM7UTfED6erckUVUJQ/jJ1FSpm3pRQ==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.44.2': + resolution: {integrity: sha512-3D3OB1vSSBXmkGEZR27uiMRNiwN08/RVAcBKwhUYPaiZ8bcvdeEwWPvbnXvvXHY+A/7xluzcN+kaiOFNiOZwWg==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-win32-arm64-msvc@4.44.2': + resolution: {integrity: sha512-VfU0fsMK+rwdK8mwODqYeM2hDrF2WiHaSmCBrS7gColkQft95/8tphyzv2EupVxn3iE0FI78wzffoULH1G+dkw==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.44.2': + resolution: {integrity: sha512-+qMUrkbUurpE6DVRjiJCNGZBGo9xM4Y0FXU5cjgudWqIBWbcLkjE3XprJUsOFgC6xjBClwVa9k6O3A7K3vxb5Q==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.44.2': + resolution: {integrity: sha512-3+QZROYfJ25PDcxFF66UEk8jGWigHJeecZILvkPkyQN7oc5BvFo4YEXFkOs154j3FTMp9mn9Ky8RCOwastduEA==} + cpu: [x64] + os: [win32] + + '@types/chai@5.2.2': + resolution: {integrity: sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg==} + + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/node@24.0.12': + resolution: {integrity: sha512-LtOrbvDf5ndC9Xi+4QZjVL0woFymF/xSTKZKPgrrl7H7XoeDvnD+E2IclKVDyaK9UM756W/3BXqSU+JEHopA9g==} + + '@types/pako@2.0.3': + resolution: {integrity: sha512-bq0hMV9opAcrmE0Byyo0fY3Ew4tgOevJmQ9grUhpXQhYfyLJ1Kqg3P33JT5fdbT2AjeAjR51zqqVjAL/HMkx7Q==} + + '@vitest/expect@3.2.4': + resolution: {integrity: sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==} + + '@vitest/mocker@3.2.4': + resolution: {integrity: sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==} + peerDependencies: + msw: ^2.4.9 + vite: ^5.0.0 || ^6.0.0 || ^7.0.0-0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@3.2.4': + resolution: {integrity: sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==} + + '@vitest/runner@3.2.4': + resolution: {integrity: sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==} + + '@vitest/snapshot@3.2.4': + resolution: {integrity: sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==} + + '@vitest/spy@3.2.4': + resolution: {integrity: sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==} + + '@vitest/utils@3.2.4': + resolution: {integrity: sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==} + + acorn@8.15.0: + resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} + engines: {node: '>=0.4.0'} + hasBin: true + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-regex@6.1.0: + resolution: {integrity: sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==} + engines: {node: '>=12'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + ansi-styles@6.2.1: + resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} + engines: {node: '>=12'} + + any-promise@1.3.0: + resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} + + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + base64url-universal@2.0.0: + resolution: {integrity: sha512-6Hpg7EBf3t148C3+fMzjf+CHnADVDafWzlJUXAqqqbm4MKNXbsoPdOkWeRTjNlkYG7TpyjIpRO1Gk0SnsFD1rw==} + engines: {node: '>=14'} + + base64url@3.0.1: + resolution: {integrity: sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==} + engines: {node: '>=6.0.0'} + + brace-expansion@2.0.2: + resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} + + bundle-require@5.1.0: + resolution: {integrity: sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + peerDependencies: + esbuild: '>=0.18' + + cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + + chai@5.2.1: + resolution: {integrity: sha512-5nFxhUrX0PqtyogoYOA8IPswy5sZFTOsBFl/9bNsmDLgsxYTzSZQJDPppDnZPTQbzSEm0hqGjWPzRemQCYbD6A==} + engines: {node: '>=18'} + + check-error@2.1.1: + resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==} + engines: {node: '>= 16'} + + chokidar@4.0.3: + resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} + engines: {node: '>= 14.16.0'} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + commander@4.1.1: + resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} + engines: {node: '>= 6'} + + confbox@0.1.8: + resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} + + consola@3.4.2: + resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} + engines: {node: ^14.18.0 || >=16.10.0} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + debug@4.4.1: + resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + deep-eql@5.0.2: + resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} + engines: {node: '>=6'} + + eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + + es-module-lexer@1.7.0: + resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + + esbuild@0.25.6: + resolution: {integrity: sha512-GVuzuUwtdsghE3ocJ9Bs8PNoF13HNQ5TXbEi2AhvVb8xU1Iwt9Fos9FEamfoee+u/TOsn7GUWc04lz46n2bbTg==} + engines: {node: '>=18'} + hasBin: true + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + expect-type@1.2.2: + resolution: {integrity: sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==} + engines: {node: '>=12.0.0'} + + fdir@6.4.6: + resolution: {integrity: sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + fix-dts-default-cjs-exports@1.0.1: + resolution: {integrity: sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==} + + foreground-child@3.3.1: + resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} + engines: {node: '>=14'} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + glob@10.4.5: + resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} + hasBin: true + + globrex@0.1.2: + resolution: {integrity: sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + jackspeak@3.4.3: + resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} + + joycon@3.1.1: + resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} + engines: {node: '>=10'} + + js-tokens@9.0.1: + resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} + + lilconfig@3.1.3: + resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} + engines: {node: '>=14'} + + lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + + load-tsconfig@0.2.5: + resolution: {integrity: sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + lodash.sortby@4.7.0: + resolution: {integrity: sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==} + + loupe@3.1.4: + resolution: {integrity: sha512-wJzkKwJrheKtknCOKNEtDK4iqg/MxmZheEMtSTYvnzRdEYaZzmgH976nenp8WdJRdx5Vc1X/9MO0Oszl6ezeXg==} + + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + + magic-string@0.30.17: + resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} + + minimatch@9.0.5: + resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} + engines: {node: '>=16 || 14 >=14.17'} + + minipass@7.1.2: + resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} + engines: {node: '>=16 || 14 >=14.17'} + + mlly@1.7.4: + resolution: {integrity: sha512-qmdSIPC4bDJXgZTCR7XosJiNKySV7O215tsPtDN9iEO/7q/76b/ijtgRu/+epFXSJhijtTCCGp3DWS549P3xKw==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + multiformats@13.3.7: + resolution: {integrity: sha512-meL9DERHj+fFVWoOX9fXqfcYcSpUfSYJPcFvDPKrxitICbwAoWR+Ut4j5NO9zAT917HUHLQmqzQbAsGNHlDcxQ==} + + mz@2.7.0: + resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + package-json-from-dist@1.0.1: + resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + + pako@2.1.0: + resolution: {integrity: sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-scurry@1.11.1: + resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} + engines: {node: '>=16 || 14 >=14.18'} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + pathval@2.0.1: + resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==} + engines: {node: '>= 14.16'} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@4.0.2: + resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==} + engines: {node: '>=12'} + + pirates@4.0.7: + resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} + engines: {node: '>= 6'} + + pkg-types@1.3.1: + resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} + + postcss-load-config@6.0.1: + resolution: {integrity: sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==} + engines: {node: '>= 18'} + peerDependencies: + jiti: '>=1.21.0' + postcss: '>=8.0.9' + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + jiti: + optional: true + postcss: + optional: true + tsx: + optional: true + yaml: + optional: true + + postcss@8.5.6: + resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} + engines: {node: ^10 || ^12 || >=14} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + readdirp@4.1.2: + resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} + engines: {node: '>= 14.18.0'} + + resolve-from@5.0.0: + resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} + engines: {node: '>=8'} + + rollup@4.44.2: + resolution: {integrity: sha512-PVoapzTwSEcelaWGth3uR66u7ZRo6qhPHc0f2uRO9fX6XDVNrIiGYS0Pj9+R8yIIYSD/mCx2b16Ws9itljKSPg==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + source-map@0.8.0-beta.0: + resolution: {integrity: sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==} + engines: {node: '>= 8'} + + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + std-env@3.9.0: + resolution: {integrity: sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-ansi@7.1.0: + resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} + engines: {node: '>=12'} + + strip-literal@3.0.0: + resolution: {integrity: sha512-TcccoMhJOM3OebGhSBEmp3UZ2SfDMZUEBdRA/9ynfLi8yYajyWX3JiXArcJt4Umh4vISpspkQIY8ZZoCqjbviA==} + + sucrase@3.35.0: + resolution: {integrity: sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==} + engines: {node: '>=16 || 14 >=14.17'} + hasBin: true + + thenify-all@1.6.0: + resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} + engines: {node: '>=0.8'} + + thenify@3.3.1: + resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@0.3.2: + resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + + tinyglobby@0.2.14: + resolution: {integrity: sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==} + engines: {node: '>=12.0.0'} + + tinypool@1.1.1: + resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} + engines: {node: ^18.0.0 || >=20.0.0} + + tinyrainbow@2.0.0: + resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==} + engines: {node: '>=14.0.0'} + + tinyspy@4.0.3: + resolution: {integrity: sha512-t2T/WLB2WRgZ9EpE4jgPJ9w+i66UZfDc8wHh0xrwiRNN+UwH98GIJkTeZqX9rg0i0ptwzqW+uYeIF0T4F8LR7A==} + engines: {node: '>=14.0.0'} + + tr46@1.0.1: + resolution: {integrity: sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==} + + tree-kill@1.2.2: + resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} + hasBin: true + + ts-interface-checker@0.1.13: + resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} + + tsconfck@3.1.6: + resolution: {integrity: sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w==} + engines: {node: ^18 || >=20} + hasBin: true + peerDependencies: + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true + + tsup@8.5.0: + resolution: {integrity: sha512-VmBp77lWNQq6PfuMqCHD3xWl22vEoWsKajkF8t+yMBawlUS8JzEI+vOVMeuNZIuMML8qXRizFKi9oD5glKQVcQ==} + engines: {node: '>=18'} + hasBin: true + peerDependencies: + '@microsoft/api-extractor': ^7.36.0 + '@swc/core': ^1 + postcss: ^8.4.12 + typescript: '>=4.5.0' + peerDependenciesMeta: + '@microsoft/api-extractor': + optional: true + '@swc/core': + optional: true + postcss: + optional: true + typescript: + optional: true + + turbo-darwin-64@2.5.4: + resolution: {integrity: sha512-ah6YnH2dErojhFooxEzmvsoZQTMImaruZhFPfMKPBq8sb+hALRdvBNLqfc8NWlZq576FkfRZ/MSi4SHvVFT9PQ==} + cpu: [x64] + os: [darwin] + + turbo-darwin-arm64@2.5.4: + resolution: {integrity: sha512-2+Nx6LAyuXw2MdXb7pxqle3MYignLvS7OwtsP9SgtSBaMlnNlxl9BovzqdYAgkUW3AsYiQMJ/wBRb7d+xemM5A==} + cpu: [arm64] + os: [darwin] + + turbo-linux-64@2.5.4: + resolution: {integrity: sha512-5May2kjWbc8w4XxswGAl74GZ5eM4Gr6IiroqdLhXeXyfvWEdm2mFYCSWOzz0/z5cAgqyGidF1jt1qzUR8hTmOA==} + cpu: [x64] + os: [linux] + + turbo-linux-arm64@2.5.4: + resolution: {integrity: sha512-/2yqFaS3TbfxV3P5yG2JUI79P7OUQKOUvAnx4MV9Bdz6jqHsHwc9WZPpO4QseQm+NvmgY6ICORnoVPODxGUiJg==} + cpu: [arm64] + os: [linux] + + turbo-windows-64@2.5.4: + resolution: {integrity: sha512-EQUO4SmaCDhO6zYohxIjJpOKRN3wlfU7jMAj3CgcyTPvQR/UFLEKAYHqJOnJtymbQmiiM/ihX6c6W6Uq0yC7mA==} + cpu: [x64] + os: [win32] + + turbo-windows-arm64@2.5.4: + resolution: {integrity: sha512-oQ8RrK1VS8lrxkLriotFq+PiF7iiGgkZtfLKF4DDKsmdbPo0O9R2mQxm7jHLuXraRCuIQDWMIw6dpcr7Iykf4A==} + cpu: [arm64] + os: [win32] + + turbo@2.5.4: + resolution: {integrity: sha512-kc8ZibdRcuWUG1pbYSBFWqmIjynlD8Lp7IB6U3vIzvOv9VG+6Sp8bzyeBWE3Oi8XV5KsQrznyRTBPvrf99E4mA==} + hasBin: true + + typescript@5.8.3: + resolution: {integrity: sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==} + engines: {node: '>=14.17'} + hasBin: true + + ufo@1.6.1: + resolution: {integrity: sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==} + + uint8arrays@5.1.0: + resolution: {integrity: sha512-vA6nFepEmlSKkMBnLBaUMVvAC4G3CTmO58C12y4sq6WPDOR7mOFYOi7GlrQ4djeSbP6JG9Pv9tJDM97PedRSww==} + + undici-types@7.8.0: + resolution: {integrity: sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==} + + vite-node@3.2.4: + resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + + vite-tsconfig-paths@5.1.4: + resolution: {integrity: sha512-cYj0LRuLV2c2sMqhqhGpaO3LretdtMn/BVX4cPLanIZuwwrkVl+lK84E/miEXkCHWXuq65rhNN4rXsBcOB3S4w==} + peerDependencies: + vite: '*' + peerDependenciesMeta: + vite: + optional: true + + vite@7.0.3: + resolution: {integrity: sha512-y2L5oJZF7bj4c0jgGYgBNSdIu+5HF+m68rn2cQXFbGoShdhV1phX9rbnxy9YXj82aS8MMsCLAAFkRxZeWdldrQ==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + jiti: '>=1.21.0' + less: ^4.0.0 + lightningcss: ^1.21.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vitest@3.2.4: + resolution: {integrity: sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/debug': ^4.1.12 + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + '@vitest/browser': 3.2.4 + '@vitest/ui': 3.2.4 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/debug': + optional: true + '@types/node': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + webidl-conversions@4.0.2: + resolution: {integrity: sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==} + + whatwg-url@7.1.0: + resolution: {integrity: sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} + +snapshots: + + '@esbuild/aix-ppc64@0.25.6': + optional: true + + '@esbuild/android-arm64@0.25.6': + optional: true + + '@esbuild/android-arm@0.25.6': + optional: true + + '@esbuild/android-x64@0.25.6': + optional: true + + '@esbuild/darwin-arm64@0.25.6': + optional: true + + '@esbuild/darwin-x64@0.25.6': + optional: true + + '@esbuild/freebsd-arm64@0.25.6': + optional: true + + '@esbuild/freebsd-x64@0.25.6': + optional: true + + '@esbuild/linux-arm64@0.25.6': + optional: true + + '@esbuild/linux-arm@0.25.6': + optional: true + + '@esbuild/linux-ia32@0.25.6': + optional: true + + '@esbuild/linux-loong64@0.25.6': + optional: true + + '@esbuild/linux-mips64el@0.25.6': + optional: true + + '@esbuild/linux-ppc64@0.25.6': + optional: true + + '@esbuild/linux-riscv64@0.25.6': + optional: true + + '@esbuild/linux-s390x@0.25.6': + optional: true + + '@esbuild/linux-x64@0.25.6': + optional: true + + '@esbuild/netbsd-arm64@0.25.6': + optional: true + + '@esbuild/netbsd-x64@0.25.6': + optional: true + + '@esbuild/openbsd-arm64@0.25.6': + optional: true + + '@esbuild/openbsd-x64@0.25.6': + optional: true + + '@esbuild/openharmony-arm64@0.25.6': + optional: true + + '@esbuild/sunos-x64@0.25.6': + optional: true + + '@esbuild/win32-arm64@0.25.6': + optional: true + + '@esbuild/win32-ia32@0.25.6': + optional: true + + '@esbuild/win32-x64@0.25.6': + optional: true + + '@isaacs/cliui@8.0.2': + dependencies: + string-width: 5.1.2 + string-width-cjs: string-width@4.2.3 + strip-ansi: 7.1.0 + strip-ansi-cjs: strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: wrap-ansi@7.0.0 + + '@jridgewell/gen-mapping@0.3.12': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.4 + '@jridgewell/trace-mapping': 0.3.29 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.4': {} + + '@jridgewell/trace-mapping@0.3.29': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.4 + + '@pkgjs/parseargs@0.11.0': + optional: true + + '@rollup/rollup-android-arm-eabi@4.44.2': + optional: true + + '@rollup/rollup-android-arm64@4.44.2': + optional: true + + '@rollup/rollup-darwin-arm64@4.44.2': + optional: true + + '@rollup/rollup-darwin-x64@4.44.2': + optional: true + + '@rollup/rollup-freebsd-arm64@4.44.2': + optional: true + + '@rollup/rollup-freebsd-x64@4.44.2': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.44.2': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.44.2': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.44.2': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.44.2': + optional: true + + '@rollup/rollup-linux-loongarch64-gnu@4.44.2': + optional: true + + '@rollup/rollup-linux-powerpc64le-gnu@4.44.2': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.44.2': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.44.2': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.44.2': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.44.2': + optional: true + + '@rollup/rollup-linux-x64-musl@4.44.2': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.44.2': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.44.2': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.44.2': + optional: true + + '@types/chai@5.2.2': + dependencies: + '@types/deep-eql': 4.0.2 + + '@types/deep-eql@4.0.2': {} + + '@types/estree@1.0.8': {} + + '@types/node@24.0.12': + dependencies: + undici-types: 7.8.0 + + '@types/pako@2.0.3': {} + + '@vitest/expect@3.2.4': + dependencies: + '@types/chai': 5.2.2 + '@vitest/spy': 3.2.4 + '@vitest/utils': 3.2.4 + chai: 5.2.1 + tinyrainbow: 2.0.0 + + '@vitest/mocker@3.2.4(vite@7.0.3(@types/node@24.0.12))': + dependencies: + '@vitest/spy': 3.2.4 + estree-walker: 3.0.3 + magic-string: 0.30.17 + optionalDependencies: + vite: 7.0.3(@types/node@24.0.12) + + '@vitest/pretty-format@3.2.4': + dependencies: + tinyrainbow: 2.0.0 + + '@vitest/runner@3.2.4': + dependencies: + '@vitest/utils': 3.2.4 + pathe: 2.0.3 + strip-literal: 3.0.0 + + '@vitest/snapshot@3.2.4': + dependencies: + '@vitest/pretty-format': 3.2.4 + magic-string: 0.30.17 + pathe: 2.0.3 + + '@vitest/spy@3.2.4': + dependencies: + tinyspy: 4.0.3 + + '@vitest/utils@3.2.4': + dependencies: + '@vitest/pretty-format': 3.2.4 + loupe: 3.1.4 + tinyrainbow: 2.0.0 + + acorn@8.15.0: {} + + ansi-regex@5.0.1: {} + + ansi-regex@6.1.0: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansi-styles@6.2.1: {} + + any-promise@1.3.0: {} + + assertion-error@2.0.1: {} + + balanced-match@1.0.2: {} + + base64url-universal@2.0.0: + dependencies: + base64url: 3.0.1 + + base64url@3.0.1: {} + + brace-expansion@2.0.2: + dependencies: + balanced-match: 1.0.2 + + bundle-require@5.1.0(esbuild@0.25.6): + dependencies: + esbuild: 0.25.6 + load-tsconfig: 0.2.5 + + cac@6.7.14: {} + + chai@5.2.1: + dependencies: + assertion-error: 2.0.1 + check-error: 2.1.1 + deep-eql: 5.0.2 + loupe: 3.1.4 + pathval: 2.0.1 + + check-error@2.1.1: {} + + chokidar@4.0.3: + dependencies: + readdirp: 4.1.2 + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + commander@4.1.1: {} + + confbox@0.1.8: {} + + consola@3.4.2: {} + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + debug@4.4.1: + dependencies: + ms: 2.1.3 + + deep-eql@5.0.2: {} + + eastasianwidth@0.2.0: {} + + emoji-regex@8.0.0: {} + + emoji-regex@9.2.2: {} + + es-module-lexer@1.7.0: {} + + esbuild@0.25.6: + optionalDependencies: + '@esbuild/aix-ppc64': 0.25.6 + '@esbuild/android-arm': 0.25.6 + '@esbuild/android-arm64': 0.25.6 + '@esbuild/android-x64': 0.25.6 + '@esbuild/darwin-arm64': 0.25.6 + '@esbuild/darwin-x64': 0.25.6 + '@esbuild/freebsd-arm64': 0.25.6 + '@esbuild/freebsd-x64': 0.25.6 + '@esbuild/linux-arm': 0.25.6 + '@esbuild/linux-arm64': 0.25.6 + '@esbuild/linux-ia32': 0.25.6 + '@esbuild/linux-loong64': 0.25.6 + '@esbuild/linux-mips64el': 0.25.6 + '@esbuild/linux-ppc64': 0.25.6 + '@esbuild/linux-riscv64': 0.25.6 + '@esbuild/linux-s390x': 0.25.6 + '@esbuild/linux-x64': 0.25.6 + '@esbuild/netbsd-arm64': 0.25.6 + '@esbuild/netbsd-x64': 0.25.6 + '@esbuild/openbsd-arm64': 0.25.6 + '@esbuild/openbsd-x64': 0.25.6 + '@esbuild/openharmony-arm64': 0.25.6 + '@esbuild/sunos-x64': 0.25.6 + '@esbuild/win32-arm64': 0.25.6 + '@esbuild/win32-ia32': 0.25.6 + '@esbuild/win32-x64': 0.25.6 + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.8 + + expect-type@1.2.2: {} + + fdir@6.4.6(picomatch@4.0.2): + optionalDependencies: + picomatch: 4.0.2 + + fix-dts-default-cjs-exports@1.0.1: + dependencies: + magic-string: 0.30.17 + mlly: 1.7.4 + rollup: 4.44.2 + + foreground-child@3.3.1: + dependencies: + cross-spawn: 7.0.6 + signal-exit: 4.1.0 + + fsevents@2.3.3: + optional: true + + glob@10.4.5: + dependencies: + foreground-child: 3.3.1 + jackspeak: 3.4.3 + minimatch: 9.0.5 + minipass: 7.1.2 + package-json-from-dist: 1.0.1 + path-scurry: 1.11.1 + + globrex@0.1.2: {} + + is-fullwidth-code-point@3.0.0: {} + + isexe@2.0.0: {} + + jackspeak@3.4.3: + dependencies: + '@isaacs/cliui': 8.0.2 + optionalDependencies: + '@pkgjs/parseargs': 0.11.0 + + joycon@3.1.1: {} + + js-tokens@9.0.1: {} + + lilconfig@3.1.3: {} + + lines-and-columns@1.2.4: {} + + load-tsconfig@0.2.5: {} + + lodash.sortby@4.7.0: {} + + loupe@3.1.4: {} + + lru-cache@10.4.3: {} + + magic-string@0.30.17: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.4 + + minimatch@9.0.5: + dependencies: + brace-expansion: 2.0.2 + + minipass@7.1.2: {} + + mlly@1.7.4: + dependencies: + acorn: 8.15.0 + pathe: 2.0.3 + pkg-types: 1.3.1 + ufo: 1.6.1 + + ms@2.1.3: {} + + multiformats@13.3.7: {} + + mz@2.7.0: + dependencies: + any-promise: 1.3.0 + object-assign: 4.1.1 + thenify-all: 1.6.0 + + nanoid@3.3.11: {} + + object-assign@4.1.1: {} + + package-json-from-dist@1.0.1: {} + + pako@2.1.0: {} + + path-key@3.1.1: {} + + path-scurry@1.11.1: + dependencies: + lru-cache: 10.4.3 + minipass: 7.1.2 + + pathe@2.0.3: {} + + pathval@2.0.1: {} + + picocolors@1.1.1: {} + + picomatch@4.0.2: {} + + pirates@4.0.7: {} + + pkg-types@1.3.1: + dependencies: + confbox: 0.1.8 + mlly: 1.7.4 + pathe: 2.0.3 + + postcss-load-config@6.0.1(postcss@8.5.6): + dependencies: + lilconfig: 3.1.3 + optionalDependencies: + postcss: 8.5.6 + + postcss@8.5.6: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + punycode@2.3.1: {} + + readdirp@4.1.2: {} + + resolve-from@5.0.0: {} + + rollup@4.44.2: + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.44.2 + '@rollup/rollup-android-arm64': 4.44.2 + '@rollup/rollup-darwin-arm64': 4.44.2 + '@rollup/rollup-darwin-x64': 4.44.2 + '@rollup/rollup-freebsd-arm64': 4.44.2 + '@rollup/rollup-freebsd-x64': 4.44.2 + '@rollup/rollup-linux-arm-gnueabihf': 4.44.2 + '@rollup/rollup-linux-arm-musleabihf': 4.44.2 + '@rollup/rollup-linux-arm64-gnu': 4.44.2 + '@rollup/rollup-linux-arm64-musl': 4.44.2 + '@rollup/rollup-linux-loongarch64-gnu': 4.44.2 + '@rollup/rollup-linux-powerpc64le-gnu': 4.44.2 + '@rollup/rollup-linux-riscv64-gnu': 4.44.2 + '@rollup/rollup-linux-riscv64-musl': 4.44.2 + '@rollup/rollup-linux-s390x-gnu': 4.44.2 + '@rollup/rollup-linux-x64-gnu': 4.44.2 + '@rollup/rollup-linux-x64-musl': 4.44.2 + '@rollup/rollup-win32-arm64-msvc': 4.44.2 + '@rollup/rollup-win32-ia32-msvc': 4.44.2 + '@rollup/rollup-win32-x64-msvc': 4.44.2 + fsevents: 2.3.3 + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + siginfo@2.0.0: {} + + signal-exit@4.1.0: {} + + source-map-js@1.2.1: {} + + source-map@0.8.0-beta.0: + dependencies: + whatwg-url: 7.1.0 + + stackback@0.0.2: {} + + std-env@3.9.0: {} + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string-width@5.1.2: + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.1.0 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-ansi@7.1.0: + dependencies: + ansi-regex: 6.1.0 + + strip-literal@3.0.0: + dependencies: + js-tokens: 9.0.1 + + sucrase@3.35.0: + dependencies: + '@jridgewell/gen-mapping': 0.3.12 + commander: 4.1.1 + glob: 10.4.5 + lines-and-columns: 1.2.4 + mz: 2.7.0 + pirates: 4.0.7 + ts-interface-checker: 0.1.13 + + thenify-all@1.6.0: + dependencies: + thenify: 3.3.1 + + thenify@3.3.1: + dependencies: + any-promise: 1.3.0 + + tinybench@2.9.0: {} + + tinyexec@0.3.2: {} + + tinyglobby@0.2.14: + dependencies: + fdir: 6.4.6(picomatch@4.0.2) + picomatch: 4.0.2 + + tinypool@1.1.1: {} + + tinyrainbow@2.0.0: {} + + tinyspy@4.0.3: {} + + tr46@1.0.1: + dependencies: + punycode: 2.3.1 + + tree-kill@1.2.2: {} + + ts-interface-checker@0.1.13: {} + + tsconfck@3.1.6(typescript@5.8.3): + optionalDependencies: + typescript: 5.8.3 + + tsup@8.5.0(postcss@8.5.6)(typescript@5.8.3): + dependencies: + bundle-require: 5.1.0(esbuild@0.25.6) + cac: 6.7.14 + chokidar: 4.0.3 + consola: 3.4.2 + debug: 4.4.1 + esbuild: 0.25.6 + fix-dts-default-cjs-exports: 1.0.1 + joycon: 3.1.1 + picocolors: 1.1.1 + postcss-load-config: 6.0.1(postcss@8.5.6) + resolve-from: 5.0.0 + rollup: 4.44.2 + source-map: 0.8.0-beta.0 + sucrase: 3.35.0 + tinyexec: 0.3.2 + tinyglobby: 0.2.14 + tree-kill: 1.2.2 + optionalDependencies: + postcss: 8.5.6 + typescript: 5.8.3 + transitivePeerDependencies: + - jiti + - supports-color + - tsx + - yaml + + turbo-darwin-64@2.5.4: + optional: true + + turbo-darwin-arm64@2.5.4: + optional: true + + turbo-linux-64@2.5.4: + optional: true + + turbo-linux-arm64@2.5.4: + optional: true + + turbo-windows-64@2.5.4: + optional: true + + turbo-windows-arm64@2.5.4: + optional: true + + turbo@2.5.4: + optionalDependencies: + turbo-darwin-64: 2.5.4 + turbo-darwin-arm64: 2.5.4 + turbo-linux-64: 2.5.4 + turbo-linux-arm64: 2.5.4 + turbo-windows-64: 2.5.4 + turbo-windows-arm64: 2.5.4 + + typescript@5.8.3: {} + + ufo@1.6.1: {} + + uint8arrays@5.1.0: + dependencies: + multiformats: 13.3.7 + + undici-types@7.8.0: {} + + vite-node@3.2.4(@types/node@24.0.12): + dependencies: + cac: 6.7.14 + debug: 4.4.1 + es-module-lexer: 1.7.0 + pathe: 2.0.3 + vite: 7.0.3(@types/node@24.0.12) + transitivePeerDependencies: + - '@types/node' + - jiti + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + + vite-tsconfig-paths@5.1.4(typescript@5.8.3)(vite@7.0.3(@types/node@24.0.12)): + dependencies: + debug: 4.4.1 + globrex: 0.1.2 + tsconfck: 3.1.6(typescript@5.8.3) + optionalDependencies: + vite: 7.0.3(@types/node@24.0.12) + transitivePeerDependencies: + - supports-color + - typescript + + vite@7.0.3(@types/node@24.0.12): + dependencies: + esbuild: 0.25.6 + fdir: 6.4.6(picomatch@4.0.2) + picomatch: 4.0.2 + postcss: 8.5.6 + rollup: 4.44.2 + tinyglobby: 0.2.14 + optionalDependencies: + '@types/node': 24.0.12 + fsevents: 2.3.3 + + vitest@3.2.4(@types/node@24.0.12): + dependencies: + '@types/chai': 5.2.2 + '@vitest/expect': 3.2.4 + '@vitest/mocker': 3.2.4(vite@7.0.3(@types/node@24.0.12)) + '@vitest/pretty-format': 3.2.4 + '@vitest/runner': 3.2.4 + '@vitest/snapshot': 3.2.4 + '@vitest/spy': 3.2.4 + '@vitest/utils': 3.2.4 + chai: 5.2.1 + debug: 4.4.1 + expect-type: 1.2.2 + magic-string: 0.30.17 + pathe: 2.0.3 + picomatch: 4.0.2 + std-env: 3.9.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinyglobby: 0.2.14 + tinypool: 1.1.1 + tinyrainbow: 2.0.0 + vite: 7.0.3(@types/node@24.0.12) + vite-node: 3.2.4(@types/node@24.0.12) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 24.0.12 + transitivePeerDependencies: + - jiti + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + + webidl-conversions@4.0.2: {} + + whatwg-url@7.1.0: + dependencies: + lodash.sortby: 4.7.0 + tr46: 1.0.1 + webidl-conversions: 4.0.2 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@8.1.0: + dependencies: + ansi-styles: 6.2.1 + string-width: 5.1.2 + strip-ansi: 7.1.0 diff --git a/src/bit-manager/BitManager.ts b/src/bit-manager/BitManager.ts new file mode 100644 index 0000000..3f92c84 --- /dev/null +++ b/src/bit-manager/BitManager.ts @@ -0,0 +1,211 @@ +import {assertIsNonNegativeInteger, assertIsPositiveInteger} from "../utils/assertions" +import {StatusRangeError} from "../status-list/errors"; + +const LIST_BLOCK_SIZE = 16384 // 16KB + +/** + * BitManager - Low-level bitstring manipulation for W3C Bitstring Status Lists + * + * Manages a packed bitstring where credentials are mapped to bit positions. + * Each credential gets a fixed-width status entry at position: credentialIndex * statusSize. + * + * Features: + * - Direct bit access without entry tracking + * - Automatic buffer expansion in 16KB blocks + * - MSB-first bit ordering within bytes + * - Zero-initialized status values + * + * @example + * ```typescript + * const manager = new BitManager({ statusSize: 2 }) + * manager.setStatus(0, 3) // Sets 2 bits at position 0 + * console.log(manager.getStatus(0)) // Returns 3 + * console.log(manager.getStatus(1)) // Returns 0 (unset) + * ``` + */ +export class BitManager { + private bits: Uint8Array + private readonly statusSize: number + + /** + * Creates a new BitManager instance + * + * @param options.statusSize - Bits per credential status (default: 1, max: 31) + * @param options.buffer - Existing buffer for decoding + * @param options.initialSize - Initial buffer size in bytes (default: 16KB) + */ + constructor(options?: { + statusSize?: number + buffer?: Uint8Array + initialSize?: number + }) { + const {statusSize = 1, buffer, initialSize = LIST_BLOCK_SIZE} = options ?? {}; + + assertIsPositiveInteger(statusSize, 'statusSize') + + // Guard against JavaScript bitwise operation overflow (which is insane for this use case, but it's the real limit.) + if (statusSize > 30) { + throw new StatusRangeError(`statusSize ${statusSize} exceeds maximum supported value of 30 bits`) + } + + this.statusSize = statusSize + this.bits = buffer ? new Uint8Array(buffer) : new Uint8Array(initialSize) + } + + /** + * Gets the status value for a credential + * + * @param credentialIndex - Non-negative credential identifier + * @returns Status value (0 to 2^statusSize - 1) + */ + getStatus(credentialIndex: number): number { + assertIsNonNegativeInteger(credentialIndex, 'credentialIndex') + + // Check if index exceeds reasonable bounds + const maxIndex = Math.floor(this.bits.length * 8 / this.statusSize) + if (credentialIndex >= maxIndex) { + throw new StatusRangeError(`credentialIndex ${credentialIndex} exceeds buffer bounds`) + } + + const startBit = credentialIndex * this.statusSize + return this.readStatusBits(startBit) + } + + /** + * Sets the status value for a credential + * + * @param credentialIndex - Non-negative credential identifier + * @param status - Status value (0 to 2^statusSize - 1) + * @throws {StatusRangeError} If status exceeds maximum value for statusSize + */ + setStatus(credentialIndex: number, status: number): void { + assertIsNonNegativeInteger(credentialIndex, 'credentialIndex') + assertIsNonNegativeInteger(status, 'status') + + const maxValue = (1 << this.statusSize) - 1 + if (status > maxValue) { + throw new StatusRangeError(`Status ${status} exceeds maximum value ${maxValue} for ${this.statusSize} bits`) + } + + const startBit = credentialIndex * this.statusSize + this.writeStatusBits(startBit, status) + } + + /** + * Returns current buffer trimmed to actual data size + * + * @returns Copy of buffer containing only written data + */ + toBuffer(): Uint8Array { + // search backwards from the end for non-zero bytes + let lastNonZeroByte = -1 + for (let i = this.bits.length - 1; i >= 0; i--) { + if (this.bits[i] !== 0) { + lastNonZeroByte = i + break + } + } + + if (lastNonZeroByte === -1) { + return new Uint8Array([]) + } + + return this.bits.slice(0, lastNonZeroByte + 1) + } + + + /** + * Gets the uniform status size for all entries + * + * @returns Number of bits per status entry + */ + getStatusSize(): number { + return this.statusSize + } + + + /** + * Gets the total buffer size in bytes + * + * @returns Buffer size in bytes + */ + getBufferLength(): number { + return this.bits.length + } + + /** + * Reads status bits from buffer starting at bit position + * + * @param startBit - Starting bit position + * @returns Decoded status value + */ + private readStatusBits(startBit: number): number { + let status = 0 + + for (let i = 0; i < this.statusSize; i++) { + const bitIndex = startBit + i + const byteIndex = Math.floor(bitIndex / 8) + const bitOffset = bitIndex % 8 + + if (byteIndex >= this.bits.length) { + continue + } + + const bit = (this.bits[byteIndex] >> (7 - bitOffset)) & 1 + // MSB of status value should be leftmost bit + status |= bit << (this.statusSize - i - 1) + } + + return status + } + + /** + * Writes status bits to buffer starting at bit position + * + * @param startBit - Starting bit position + * @param status - Status value to write + */ + private writeStatusBits(startBit: number, status: number): void { + for (let i = 0; i < this.statusSize; i++) { + const bitIndex = startBit + i + const byteIndex = Math.floor(bitIndex / 8) + const bitOffset = bitIndex % 8 + + this.ensureBufferCanHold(byteIndex + 1) + + // MSB of status value should be leftmost bit + const bit = (status >> (this.statusSize - i - 1)) & 1 + + if (bit === 1) { + this.bits[byteIndex] |= (1 << (7 - bitOffset)) + } else { + this.bits[byteIndex] &= ~(1 << (7 - bitOffset)) + } + } + } + + /** + * Ensures buffer can hold the specified number of bytes + * Grows in bitstring-sized chunks for better memory efficiency + * + * @param requiredBytes - Minimum bytes needed + */ + private ensureBufferCanHold(requiredBytes: number): void { + if (requiredBytes > this.bits.length) { + // Calculate growth in bitstring-sized chunks rather than fixed 16KB blocks + const bitsNeeded = requiredBytes * 8 + const entriesNeeded = Math.ceil(bitsNeeded / this.statusSize) + const optimalBits = entriesNeeded * this.statusSize + const optimalBytes = Math.ceil(optimalBits / 8) + + // Still ensure minimum block size for reasonable growth + const minGrowthBytes = Math.max(optimalBytes, LIST_BLOCK_SIZE) + const blocksNeeded = Math.ceil(minGrowthBytes / LIST_BLOCK_SIZE) + const newSize = blocksNeeded * LIST_BLOCK_SIZE + + const newBuffer = new Uint8Array(newSize) + newBuffer.set(this.bits) + this.bits = newBuffer + } + } +} diff --git a/src/credential/create.ts b/src/credential/create.ts new file mode 100644 index 0000000..7066ae8 --- /dev/null +++ b/src/credential/create.ts @@ -0,0 +1,95 @@ +/** + * High-level credential creation functions for W3C Bitstring Status Lists + * + * Provides convenient functions to create compliant BitstringStatusListCredentials + * according to the W3C Bitstring Status List v1.0 specification. + */ + +import {BitstreamStatusList} from '../status-list/BitstreamStatusList' +import {BitstringStatusListCredentialSubject, BitstringStatusListCredentialUnsigned, IIssuer} from '../types' + +/** + * Creates a W3C compliant BitstringStatusListCredential with an empty status list + * + * The created credential contains an empty status list that can be referenced by + * other credentials through their credentialStatus.statusListCredential property. + * Individual credential statuses are managed separately by updating the status list + * and republishing the credential. + * + * @example + * ```typescript + * // Create a revocation list for single-bit status (revoked/not revoked) + * const credential = await createStatusListCredential({ + * id: 'https://issuer.example/status/1', + * issuer: 'https://issuer.example', + * statusPurpose: 'revocation' + * }) + * + * // Create a message list with 4-bit status codes + * const messageList = await createStatusListCredential({ + * id: 'https://issuer.example/status/messages', + * issuer: { id: 'https://issuer.example', name: 'Example Issuer' }, + * statusPurpose: 'message', + * statusSize: 4, + * validUntil: new Date('2025-12-31') + * }) + * ``` + * + * @param options.id - Unique identifier for the status list credential + * @param options.issuer - Issuer identifier (string) or issuer object with id + * @param options.statusSize - Uniform bit width for status entries (default: 1) + * @param options.statusPurpose - Purpose(s) of the status list (e.g., 'revocation', 'suspension') + * @param options.validFrom - Optional start of validity period + * @param options.validUntil - Optional end of validity period + * @param options.ttl - Optional time-to-live in milliseconds + * @returns Promise resolving to unsigned BitstringStatusListCredential + */ +export async function createStatusListCredential(options: { + id: string + issuer: string | IIssuer + statusSize?: number + statusList?: BitstreamStatusList + statusPurpose: string | string[] + validFrom?: Date + validUntil?: Date + ttl?: number +}): Promise { + const { + id, + issuer, + statusSize = 1, + statusList, + statusPurpose, + validFrom, + validUntil, + ttl + } = options + + // Create empty status list with specified statusSize + const encodedList = await (statusList ?? new BitstreamStatusList({statusSize})).encode() + + // Build credential subject according to W3C spec + const credentialSubject: BitstringStatusListCredentialSubject = { + id: `${id}#list`, + type: 'BitstringStatusList', + statusPurpose, + encodedList, + ...(ttl && {ttl}) + } + + // Build the complete credential with W3C contexts + const credential: BitstringStatusListCredentialUnsigned = { + '@context': [ + 'https://www.w3.org/ns/credentials/v2', // Core VC context + 'https://www.w3.org/ns/credentials/status/v1' // Status list context + ], + id, + type: ['VerifiableCredential', 'BitstringStatusListCredential'], + issuer, + credentialSubject, + ...(validFrom && {validFrom: validFrom.toISOString()}), + ...(validUntil && {validUntil: validUntil.toISOString()}) + } + + return credential +} \ No newline at end of file diff --git a/src/credential/verify.ts b/src/credential/verify.ts new file mode 100644 index 0000000..5240259 --- /dev/null +++ b/src/credential/verify.ts @@ -0,0 +1,210 @@ +/** + * W3C Bitstring Status List credential verification logic + * + * Implements the complete W3C Bitstring Status List v1.0 verification algorithm + * including status purpose validation, temporal validity checks, and minimum + * bitstring length requirements. + */ + +import {BitstreamStatusList} from '../status-list/BitstreamStatusList' +import {BitstringStatusListEntry, CheckStatusOptions, StatusMessage, VerificationResult} from '../types' +import {assertIsObject} from '../utils/assertions' + +/** W3C minimum bitstring size in bits */ +const MIN_BITSTRING_SIZE_BITS = 131072 // 16KB * 8 + +/** + * Checks the status of a credential against its referenced status list + * + * Implements the W3C Bitstring Status List verification algorithm: + * 1. Validates credential structure and extracts BitstringStatusListEntry + * 2. Retrieves and validates the status list credential + * 3. Checks temporal validity (validFrom/validUntil) + * 4. Validates status purpose matching + * 5. Verifies minimum bitstring length requirements + * 6. Decodes status list and retrieves credential status + * 7. Determines verification result based on status purpose + * + * @example + * ```typescript + * const result = await checkStatus({ + * credential: someCredentialWithStatus, + * getStatusListCredential: async (url) => { + * const response = await fetch(url) + * return response.json() + * } + * }) + * + * if (result.verified) { + * console.log('Credential is valid') + * } else { + * console.log('Credential failed:', result.error?.message) + * } + * ``` + * + * @param options.credential - The credential to check status for + * @param options.getStatusListCredential - Function to retrieve status list credential by URL + * @returns Promise resolving to verification result with status details + */ +export async function checkStatus(options: CheckStatusOptions): Promise { + try { + const {credential, getStatusListCredential} = options + + // Validate input credential structure + assertIsObject(credential, 'credential') + + if (!credential.credentialStatus) { + return Promise.reject(new Error('No credentialStatus found in credential')) + } + + // Extract BitstringStatusListEntry from credential status + const entry = extractBitstringStatusEntry(credential.credentialStatus) + if (!entry) { + throw new Error('No BitstringStatusListEntry found in credentialStatus') + } + + // Retrieve the status list credential + const listCredential = await getStatusListCredential(entry.statusListCredential) + + // Validate temporal constraints + validateTemporalValidity(listCredential) + + // Validate status purpose matching (W3C requirement) + validateStatusPurposeAndSize(entry, listCredential) + + // Decode status list and validate minimum size + const statusSize = entry.statusSize || 1 + const statusList = await BitstreamStatusList.decode({ + encodedList: listCredential.credentialSubject.encodedList, + statusSize + }) + + + // Verify W3C minimum bitstring length requirement + validateMinimumBitstringLength(listCredential.credentialSubject.encodedList, statusSize) + + // Retrieve the actual status value + const statusIndex = parseInt(entry.statusListIndex, 10) + const status = statusList.getStatus(statusIndex) + + // Find corresponding status message if available + const statusMessage = findStatusMessage(entry, status) + + // Determine verification result based on status purpose + const verified = determineVerificationResult(entry.statusPurpose, status) + + return { + verified, + status, + statusMessage + } + + } catch (error) { + return { + verified: false, + status: -1, + error: error instanceof Error ? error : new Error(String(error)) + } + } +} + +/** + * Extracts BitstringStatusListEntry from credential status + * Handles both single entry and array of entries + */ +function extractBitstringStatusEntry( + credentialStatus: any +): BitstringStatusListEntry | null { + const statusEntries = Array.isArray(credentialStatus) + ? credentialStatus + : [credentialStatus] + + return statusEntries.find(entry => entry.type === 'BitstringStatusListEntry') || null +} + +/** + * Validates temporal validity of the status list credential + * Checks validFrom and validUntil against current time + */ +function validateTemporalValidity(listCredential: any): void { + const now = new Date() + + if (listCredential.validFrom && new Date(listCredential.validFrom) > now) { + throw new Error('Status list credential is not yet valid') + } + + if (listCredential.validUntil && new Date(listCredential.validUntil) < now) { + throw new Error('Status list credential has expired') + } +} + +/** + * Validates that the entry's statusPurpose matches the list credential's purpose(s) + * Implements W3C specification requirement for purpose matching + */ +function validateStatusPurposeAndSize( + entry: BitstringStatusListEntry, + listCredential: any +): void { + const listStatusPurpose = listCredential.credentialSubject.statusPurpose + const purposes = Array.isArray(listStatusPurpose) ? listStatusPurpose : [listStatusPurpose] + + if (!purposes.includes(entry.statusPurpose)) { + throw new Error( + `Status purpose '${entry.statusPurpose}' does not match any purpose in status list credential: ${purposes.join(', ')}` + ) + } + + const statusSize = entry.statusSize || 1 + if ( listCredential.credentialSubject.statusSize !== statusSize) { + throw new Error(`StatusSize mismatch: expected ${statusSize}, got from credentialSubject ${listCredential.credentialSubject.statusSize}`) + } + +} + +/** + * Validates that the bitstring meets W3C minimum length requirements + * Uses efficient ISIZE-based calculation to avoid full decompression + */ +function validateMinimumBitstringLength(encodedList: string, statusSize: number): void { + const totalEntries = BitstreamStatusList.getStatusListLength(encodedList, statusSize) + const minEntries = MIN_BITSTRING_SIZE_BITS / statusSize + + if (totalEntries < minEntries) { + throw new Error( + `Status list length error: bitstring must support at least ${minEntries} entries for statusSize ${statusSize}` + ) + } +} + +/** + * Finds the corresponding status message for a given status value + * Returns undefined if no matching message found + */ +function findStatusMessage( + entry: BitstringStatusListEntry, + status: number +): StatusMessage | undefined { + if (!entry.statusMessage) { + return undefined + } + + const statusHex = `0x${status.toString(16)}` + return entry.statusMessage.find(msg => msg.id === statusHex) +} + +/** + * Determines the verification result based on status purpose and value + * + * For 'revocation' and 'suspension': 0 = valid, non-zero = invalid + * For other purposes: defaults to valid (assumes status is informational) + */ +function determineVerificationResult(statusPurpose: string, status: number): boolean { + if (statusPurpose === 'revocation' || statusPurpose === 'suspension') { + return status === 0 // 0 means not revoked/suspended + } + + // For other purposes (e.g., 'message'), default to valid + // The status value provides information but doesn't affect validity + return true +} \ No newline at end of file diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..6f0e93d --- /dev/null +++ b/src/index.ts @@ -0,0 +1,22 @@ +// Core Classes +export {BitManager} from './bit-manager/BitManager' +export {BitstreamStatusList} from './status-list/BitstreamStatusList' + +// High-Level Functions +export {createStatusListCredential} from './credential/create' +export {checkStatus} from './credential/verify' + +// Types +export type { + StatusMessage, + BitstringStatusPurpose, + BitstringStatusListEntry, + BitstringStatusListCredentialSubject, + BitstringStatusListCredentialUnsigned, + IIssuer, + CredentialStatus, + CredentialWithStatus, + CheckStatusOptions, + VerificationResult, + AdditionalClaims +} from './types' diff --git a/src/status-list/BitstreamStatusList.ts b/src/status-list/BitstreamStatusList.ts new file mode 100644 index 0000000..2956774 --- /dev/null +++ b/src/status-list/BitstreamStatusList.ts @@ -0,0 +1,235 @@ +/** + * StatusList - High-level W3C Bitstring Status List implementation + * + * Provides a complete implementation of the W3C Bitstring Status List v1.0 specification. + * Manages a compressed bitstring where each credential has a uniform-width status entry. + * + * Features: + * - W3C compliant 16KB minimum bitstring size + * - GZIP compression with multibase encoding (u-prefix) + * - Uniform status size for all entries in a list + * - Direct credential access without explicit entry creation + * - Efficient ISIZE-based length calculation + * + * @example + * ```typescript + * // Create a status list for revocation (1-bit per credential) + * const list = new StatusList({ statusSize: 1 }) + * list.setStatus(42, 1) // Mark credential 42 as revoked + * + * const encoded = await list.encode() // Get compressed, encoded string + * const decoded = await StatusList.decode({ encodedList: encoded, statusSize: 1 }) + * console.log(decoded.getStatus(42)) // Returns 1 + * ``` + */ + +import {BitManager} from '../bit-manager/BitManager' +import {base64urlToBytes, bytesToBase64url} from '../utils/base64' +import {assertIsPositiveInteger, assertIsString} from '../utils/assertions' +import pako from 'pako' +import {MalformedValueError, StatusListLengthError} from "./errors"; + +/** W3C specification minimum bitstring size */ +const MIN_BITSTRING_SIZE_BYTES = 16384 // 16KB (131,072 bits) +const MIN_BITSTRING_SIZE_BITS = 131072 + +export class BitstreamStatusList { + private readonly bitManager: BitManager + private readonly statusSize: number + + /** + * Creates a new StatusList instance + * + * @param options.buffer - Existing bitstring buffer for decoding (uncompressed) + * @param options.initialSize - Initial buffer size in bytes (default: 16KB) + * @param options.statusSize - Uniform bit width for all entries (default: 1) + */ + constructor(options: { + buffer?: Uint8Array + initialSize?: number + statusSize?: number + } = {}) { + const {buffer, initialSize, statusSize = 1} = options + assertIsPositiveInteger(statusSize, 'statusSize') + + this.statusSize = statusSize + this.bitManager = new BitManager({statusSize, buffer, initialSize}) + } + + /** + * Gets the status value for a credential + * + * @param credentialIndex - Credential identifier + * @returns Status value (0 to 2^statusSize - 1) + */ + getStatus(credentialIndex: number): number { + return this.bitManager.getStatus(credentialIndex) + } + + /** + * Sets the status value for a credential + * + * @param credentialIndex - Credential identifier + * @param status - Status value (0 to 2^statusSize - 1) + */ + setStatus(credentialIndex: number, status: number): void { + this.bitManager.setStatus(credentialIndex, status) + } + + /** + * Returns the uniform status size (bit width) for all entries + */ + getStatusSize(): number { + return this.statusSize + } + + /** + * Encodes the status list into a W3C compliant compressed string + * + * Process: + * 1. Get current bitstring buffer + * 2. Pad to minimum 16KB (W3C requirement) + * 3. GZIP compress the padded buffer + * 4. Base64url encode with 'u' prefix (multibase) + * + * @returns Promise resolving to u-prefixed compressed string + */ + async encode(): Promise { + const buffer = this.bitManager.toBuffer() + const paddedBuffer = this.padToMinimumSize(buffer) + const compressed = pako.gzip(paddedBuffer) + + return `u${bytesToBase64url(compressed)}` + } + + /** + * Decodes a W3C compliant status list string into a StatusList instance + * + * @param options.encodedList - u-prefixed, gzip-compressed base64url string + * @param options.statusSize - Uniform bit width used during encoding (default: 1) + * @returns Promise resolving to new StatusList instance + * @throws {MalformedValueError} If format is invalid + * @throws {StatusListLengthError} If size requirements not met + */ + static async decode(options: { + encodedList: string + statusSize?: number + }): Promise { + const {encodedList, statusSize = 1} = options + + assertIsString(encodedList, 'encodedList') + assertIsPositiveInteger(statusSize, 'statusSize') + + // Validate multibase prefix + if (!encodedList.startsWith('u')) { + throw new MalformedValueError('encodedList must start with lowercase "u" prefix') + } + + // Validate base64url alphabet and no padding + const base64urlPart = encodedList.slice(1) + if (!/^[A-Za-z0-9_-]+$/.test(base64urlPart)) { + throw new MalformedValueError('encodedList contains invalid base64url characters or padding') + } + + let compressed: Uint8Array + let buffer: Uint8Array + + try { + // Decode multibase + compressed = base64urlToBytes(base64urlPart) + // Decompress + buffer = pako.ungzip(compressed) + } catch (error) { + throw new MalformedValueError(`Failed to decode or decompress encodedList: ${error.message}`) + } + + // Enforce W3C minimum size requirement + if (buffer.length < MIN_BITSTRING_SIZE_BYTES) { + throw new StatusListLengthError( + `Status list must be at least ${MIN_BITSTRING_SIZE_BYTES} bytes (16KB), got ${buffer.length}` + ) + } + + // W3C spec step 9: validate minimum entries based on statusSize + const totalBits = buffer.length * 8 + const availableEntries = Math.floor(totalBits / statusSize) + const minimumEntries = Math.floor(MIN_BITSTRING_SIZE_BITS / statusSize) + + if (availableEntries < minimumEntries) { + throw new StatusListLengthError( + `Status list must support at least ${minimumEntries} entries for statusSize ${statusSize}, got ${availableEntries}` + ) + } + + return new BitstreamStatusList({buffer, statusSize}) + } + + /** + * Gets the maximum number of entries this decoded status list can hold + * + * @returns Maximum number of entries based on current buffer size + */ + getLength(): number { + const totalBits = this.bitManager.getBufferLength() * 8 + return Math.floor(totalBits / this.statusSize) + } + + /** + * Efficiently calculates the maximum number of entries in an encoded status list + * Uses GZIP ISIZE field to determine uncompressed size without full decompression + * + * @param encodedList - u-prefixed, gzip-compressed base64url string + * @param statusSize - Uniform bit width used during encoding + * @returns Maximum number of entries (uncompressed_bits / statusSize) + * @throws {MalformedValueError} If format is invalid + */ + static getStatusListLength(encodedList: string, statusSize: number): number { + assertIsString(encodedList, 'encodedList') + assertIsPositiveInteger(statusSize, 'statusSize') + + // Validate multibase prefix + if (!encodedList.startsWith('u')) { + throw new MalformedValueError('encodedList must start with lowercase "u" prefix') + } + + // Validate base64url alphabet and no padding + const base64urlPart = encodedList.slice(1) + if (!/^[A-Za-z0-9_-]+$/.test(base64urlPart)) { + throw new MalformedValueError('encodedList contains invalid base64url characters or padding') + } + + let compressed: Uint8Array + try { + compressed = base64urlToBytes(base64urlPart) + } catch (error) { + throw new MalformedValueError(`Failed to decode base64url: ${error.message}`) + } + + if (compressed.length < 4) { + throw new MalformedValueError('Invalid gzip data: too short to contain ISIZE field') + } + + // Read ISIZE (last 4 bytes of gzip data, little-endian) + const isizeOffset = compressed.byteOffset + compressed.length - 4 + const view = new DataView(compressed.buffer, isizeOffset, 4) + const uncompressedBytes = view.getUint32(0, true) + + // Convert bytes to total available entries + const totalBits = uncompressedBytes * 8 + return Math.floor(totalBits / statusSize) + } + + /** + * Pads buffer to W3C minimum size requirement (16KB) + * + * @param buffer - Source buffer to pad + * @returns Padded buffer (original if already >= 16KB) + */ + private padToMinimumSize(buffer: Uint8Array): Uint8Array { + if (buffer.length >= MIN_BITSTRING_SIZE_BYTES) { + return buffer + } + + return Uint8Array.from({length: MIN_BITSTRING_SIZE_BYTES}, (_, i) => buffer[i] || 0) + } +} diff --git a/src/status-list/errors.ts b/src/status-list/errors.ts new file mode 100644 index 0000000..9df89d0 --- /dev/null +++ b/src/status-list/errors.ts @@ -0,0 +1,41 @@ +export class BitstringStatusListError extends Error { + public readonly type: string + public readonly code: string + + constructor(type: string, code: string, message: string) { + super(message) + this.name = 'BitstringStatusListError' + this.type = `https://www.w3.org/ns/credentials/status-list#${type}` + this.code = code + } +} + +export class StatusListLengthError extends BitstringStatusListError { + constructor(message: string) { + super('STATUS_LIST_LENGTH_ERROR', 'STATUS_LIST_LENGTH_ERROR', message) + } +} + +export class StatusRangeError extends BitstringStatusListError { + constructor(message: string) { + super('RANGE_ERROR', 'RANGE_ERROR', message) + } +} + +export class MalformedValueError extends BitstringStatusListError { + constructor(message: string) { + super('MALFORMED_VALUE_ERROR', 'MALFORMED_VALUE_ERROR', message) + } +} + +export class StatusRetrievalError extends BitstringStatusListError { + constructor(message: string) { + super('STATUS_RETRIEVAL_ERROR', 'STATUS_RETRIEVAL_ERROR', message) + } +} + +export class StatusVerificationError extends BitstringStatusListError { + constructor(message: string) { + super('STATUS_VERIFICATION_ERROR', 'STATUS_VERIFICATION_ERROR', message) + } +} diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..b1f137d --- /dev/null +++ b/src/types.ts @@ -0,0 +1,78 @@ +/** + * Central type definitions for the Bitstring Status List library + * Based on W3C Bitstring Status List v1.0 specification + */ +export type BitstringStatusPurpose = 'revocation' | 'suspension' | 'refresh' | 'message' | string + +export interface BitstringStatusListEntry { + id?: string // Optional identifier for the status list entry + type: 'BitstringStatusListEntry' + statusPurpose: 'revocation' | 'suspension' | 'message' | string + statusListIndex: string // Index as a string integer >= 0 + statusListCredential: string // URL pointing to the BitstringStatusListCredential + statusSize?: number // Optional size of status entry in bits, defaults to 1 + statusMessage?: StatusMessage[] // Optional array of status messages + statusReference?: string | string[] // Optional reference URLs +} + +export interface BitstringStatusListCredentialSubject { + id: string // The ID of the credential subject + type: 'BitstringStatusList' + statusPurpose: 'revocation' | 'suspension' | 'message' | string | string[] // Can be array for multiple purposes + encodedList: string // The u-prefixed, compressed, base64url-encoded string + ttl?: number // Optional time to live in milliseconds +} + +export type AdditionalClaims = Record + +export type BitstringStatusListCredentialUnsigned = AdditionalClaims & { + '@context': string[] + id: string + issuer: string | IIssuer + type: string[] + credentialSubject: BitstringStatusListCredentialSubject + validFrom?: string + validUntil?: string +} + +export interface IIssuer { + id: string + + [x: string]: any +} + +export type CredentialStatus = BitstringStatusListEntry | BitstringStatusListEntry[] + +type CredentialSubject = { + id: string + type: string + [key: string]: any +}; + +export interface CredentialWithStatus { + '@context': string[] + id: string + type: string[] + issuer: string | IIssuer + validFrom?: string + validUntil?: string + credentialStatus?: CredentialStatus + credentialSubject: CredentialSubject +} + +export interface CheckStatusOptions { + credential: CredentialWithStatus + getStatusListCredential: (url: string) => Promise +} + +export interface VerificationResult { + verified: boolean + status: number // The numeric status code found + statusMessage?: StatusMessage // The corresponding message object if found + error?: Error +} + +export interface StatusMessage { + id: string // The hex status code (e.g., "0x0", "0x1") + message: string // The human-readable description of the status +} diff --git a/src/utils/assertions.ts b/src/utils/assertions.ts new file mode 100644 index 0000000..d278aec --- /dev/null +++ b/src/utils/assertions.ts @@ -0,0 +1,48 @@ +/** + * Runtime type guards and assertion utilities + * Throws descriptive TypeError exceptions on validation failure + */ + +export function assert(condition: boolean, message: string): void { + if (!condition) { + throw new TypeError(message) + } +} + +export function assertIsNumber(value: any, name: string): void { + if (typeof value !== 'number' || isNaN(value)) { + throw new TypeError(`${name} must be a number, got ${typeof value}`) + } +} + +export function assertIsPositiveInteger(value: any, name: string): void { + assertIsNumber(value, name) + if (!Number.isInteger(value) || value <= 0) { + throw new TypeError(`${name} must be a positive integer, got ${value}`) + } +} + +export function assertIsNonNegativeInteger(value: any, name: string): void { + assertIsNumber(value, name) + if (!Number.isInteger(value) || value < 0) { + throw new TypeError(`${name} must be a non-negative integer, got ${value}`) + } +} + +export function assertIsString(value: any, name: string): void { + if (typeof value !== 'string') { + throw new TypeError(`${name} must be a string, got ${typeof value}`) + } +} + +export function assertIsObject(value: any, name: string): void { + if (typeof value !== 'object' || value === null || Array.isArray(value)) { + throw new TypeError(`${name} must be an object, got ${typeof value}`) + } +} + +export function assertIsUint8Array(value: any, name: string): void { + if (!(value instanceof Uint8Array)) { + throw new TypeError(`${name} must be a Uint8Array, got ${typeof value}`) + } +} diff --git a/src/utils/base64.ts b/src/utils/base64.ts new file mode 100644 index 0000000..baddfbb --- /dev/null +++ b/src/utils/base64.ts @@ -0,0 +1,9 @@ +import * as u8a from 'uint8arrays' + +export function bytesToBase64url(b: Uint8Array): string { + return u8a.toString(b, 'base64url') +} + +export function base64urlToBytes(s: string): Uint8Array { + return u8a.fromString(s, 'base64url') +} \ No newline at end of file diff --git a/tsconfig.base.json b/tsconfig.base.json new file mode 100644 index 0000000..53c0fa4 --- /dev/null +++ b/tsconfig.base.json @@ -0,0 +1,81 @@ +{ + "$schema": "http://json.schemastore.org/tsconfig", + "compilerOptions": { + /* Basic Options */ + "target": "esnext" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */, + "lib": ["esnext"], + "moduleDetection": "force", +// "verbatimModuleSyntax": true, + "incremental": true, + "module": "preserve" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */, +// "moduleResolution": "NodeNext" /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */, + /* "lib": [ + "ES6", + "dom" + ]*/ /* Specify library files to be included in the compilation. */ + "allowJs": true /* Allow javascript files to be compiled. */, + // "checkJs": true, /* Report errors in .js files. */ + "jsx": "react" /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */, + "declaration": true /* Generates corresponding '.d.ts' file. */, + "sourceMap": true /* Generates corresponding '.map' file. */, + // "outFile": "./", /* Concatenate and emit output to single file. */ + // "outDir": "./", /* Redirect output structure to the directory. */ + // "rootDir": "./" /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */, + // "removeComments": true, /* Do not emit comments to output. */ + // "noEmit": true /* Do not emit outputs. */, + // "incremental": true, /* Enable incremental compilation */ + // "importHelpers": true, /* Import emit helpers from 'tslib'. */ + "downlevelIteration": true /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */, + // "isolatedModules": true /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */, + + /* Strict Type-Checking Options */ + "strict": true /* Enable all strict type-checking options. */, + // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* Enable strict null checks. */ + // "strictFunctionTypes": true, /* Enable strict checking of function types. */ + // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ + // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ + // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ + + /* Additional Checks */ + "noUnusedLocals": true /* Report errors on unused locals. */, + // "noUnusedParameters": true /* Report errors on unused parameters. */, + "noImplicitReturns": true /* Report error when not all code paths in function return a value. */, + "noFallthroughCasesInSwitch": true /* Report errors for fallthrough cases in switch statement. */, + + /* Module Resolution Options */ +// "moduleResolution": "node" /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */, +// "baseUrl": "./" /* Base directory to resolve non-absolute module names. */, + // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ + // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ + // "typeRoots": [], /* List of folders to include type definitions from. */ + // "types": [], /* Type declaration files to be included in compilation. */ + // "allowSyntheticDefaultImports": true /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */, + "allowSyntheticDefaultImports": true, + "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */, + + // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ + "skipLibCheck": true /* Skip type checking of declaration files. */, + + /* Source Map Options */ + // "sourceRoot": "./", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ + // "mapRoot": "./", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ + // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ + + "resolveJsonModule": true, + /* Experimental Options */ + "useUnknownInCatchVariables": false, + "experimentalDecorators": true /* Enables experimental support for ES7 decorators. */, + "emitDecoratorMetadata": true /* Enables experimental support for emitting type metadata for decorators. */, + "preserveConstEnums": true, + "declarationMap": true, + "composite": true, + "tsBuildInfoFile": "${configDir}/tsconfig.tsbuildinfo", + "outDir": "${configDir}/dist", + "baseUrl": "${configDir}", + "rootDir": "${configDir}/src" + }, +// "include": ["${configDir}/lib","${configDir}/lib/**/*.json"], + "exclude": ["${configDir}/node_modules", "${configDir}/dist", "**/__tests__/**/*", "**/dist/**/*", "**/.yalc/**/*", "**/node_modules", "**/vc-status-list-tests/**/*"] +} diff --git a/tsconfig.build.json b/tsconfig.build.json new file mode 100644 index 0000000..d4adfeb --- /dev/null +++ b/tsconfig.build.json @@ -0,0 +1,11 @@ +{ + "extends": "./tsconfig.base.json", + "compilerOptions": { + "moduleResolution": "bundler", + "declaration": true, + "emitDeclarationOnly": true, + "noEmit": false, + "composite": false, + "outDir": "dist" + } +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..19046f7 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.build.json", + "compilerOptions": { + "composite": true, + "outDir": "dist", + "declaration": true + }, + "include": ["src"] +} diff --git a/tsconfig.test.json b/tsconfig.test.json new file mode 100644 index 0000000..cae045d --- /dev/null +++ b/tsconfig.test.json @@ -0,0 +1,12 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "module": "CommonJS", + "moduleResolution": "Node", + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "isolatedModules": false, + "outDir": "dist-jest", // Not used, but required + "noEmit": true + } +} diff --git a/tsconfig.tsup.json b/tsconfig.tsup.json new file mode 100644 index 0000000..61bc264 --- /dev/null +++ b/tsconfig.tsup.json @@ -0,0 +1,7 @@ +{ + "extends": "./tsconfig.base.json", + "compilerOptions": { + "rootDir": "./src", + "composite": false + } +} \ No newline at end of file diff --git a/tsup.config.ts b/tsup.config.ts new file mode 100644 index 0000000..6f129f5 --- /dev/null +++ b/tsup.config.ts @@ -0,0 +1,18 @@ +import { defineConfig } from 'tsup' + +export default defineConfig({ + entry: ['src/index.ts'], + format: ['esm', 'cjs'], + tsconfig: './tsconfig.tsup.json', + dts: true, + target: ['es2022'], + platform: 'neutral', + cjsInterop: false, + experimentalDts: false, + shims: true, + sourcemap: true, + splitting: false, + outDir: 'dist', + clean: true, + skipNodeModulesBundle: true +}) \ No newline at end of file diff --git a/vitest.config.mjs b/vitest.config.mjs new file mode 100644 index 0000000..a9c9117 --- /dev/null +++ b/vitest.config.mjs @@ -0,0 +1,18 @@ +import { defineConfig } from 'vitest/config'; +// const tsconfigPaths = require('vite-tsconfig-paths'); + +export default defineConfig({ + // plugins: [tsconfigPaths()], + test: { + testTimeout: 0, + server: { + deps: { + fallbackCJS: true, + inline: true + } + }, + /* for example, use global to avoid globals imports (describe, test, expect): */ + globals: false, + // testTimeout: 0 + } +}); \ No newline at end of file