From 6a29e3c2b854e6915bbc7cf85be3e85b00917a5f Mon Sep 17 00:00:00 2001 From: sanderPostma Date: Wed, 9 Jul 2025 17:58:28 +0200 Subject: [PATCH 01/31] chore: initial commit with BitsteamStatusList implementation --- __tests__/BitManager.test.ts | 154 +++ __tests__/StatusList.test.ts | 167 ++++ __tests__/create.test.ts | 107 +++ __tests__/index.test.ts | 37 + __tests__/verify.test.ts | 310 ++++++ package.json | 56 ++ pnpm-lock.yaml | 1697 +++++++++++++++++++++++++++++++++ src/bit-manager/BitManager.ts | 112 +++ src/credential/create.ts | 43 + src/credential/verify.ts | 109 +++ src/index.ts | 21 + src/status-list/StatusList.ts | 73 ++ src/types.ts | 75 ++ src/utils/assertions.ts | 48 + src/utils/base64.ts | 9 + tsconfig.base.json | 81 ++ tsconfig.build.json | 11 + tsconfig.json | 9 + tsconfig.test.json | 12 + tsconfig.tsup.json | 7 + tsup.config.ts | 18 + vitest.config.mjs | 18 + 22 files changed, 3174 insertions(+) create mode 100644 __tests__/BitManager.test.ts create mode 100644 __tests__/StatusList.test.ts create mode 100644 __tests__/create.test.ts create mode 100644 __tests__/index.test.ts create mode 100644 __tests__/verify.test.ts create mode 100644 package.json create mode 100644 pnpm-lock.yaml create mode 100644 src/bit-manager/BitManager.ts create mode 100644 src/credential/create.ts create mode 100644 src/credential/verify.ts create mode 100644 src/index.ts create mode 100644 src/status-list/StatusList.ts create mode 100644 src/types.ts create mode 100644 src/utils/assertions.ts create mode 100644 src/utils/base64.ts create mode 100644 tsconfig.base.json create mode 100644 tsconfig.build.json create mode 100644 tsconfig.json create mode 100644 tsconfig.test.json create mode 100644 tsconfig.tsup.json create mode 100644 tsup.config.ts create mode 100644 vitest.config.mjs diff --git a/__tests__/BitManager.test.ts b/__tests__/BitManager.test.ts new file mode 100644 index 0000000..8e56481 --- /dev/null +++ b/__tests__/BitManager.test.ts @@ -0,0 +1,154 @@ +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) + }) + + it('should create with existing buffer', () => { + const buffer = new Uint8Array([0xFF, 0x00, 0xFF]) + const manager = new BitManager({buffer}) + expect(manager.toBuffer()).toEqual(new Uint8Array([])) + }) + }) + + describe('addEntry', () => { + it('should add entry with default status size', () => { + bitManager.addEntry(0) + const entries = bitManager.getEntries() + expect(entries.has(0)).toBe(true) + expect(entries.get(0)?.statusSize).toBe(1) + }) + + it('should add entry with custom status size', () => { + bitManager.addEntry(0, 4) + const entries = bitManager.getEntries() + expect(entries.get(0)?.statusSize).toBe(4) + }) + + it('should assign consecutive bit positions', () => { + bitManager.addEntry(0, 2) + bitManager.addEntry(1, 3) + const entries = bitManager.getEntries() + expect(entries.get(0)?.startBit).toBe(0) + expect(entries.get(1)?.startBit).toBe(2) + }) + + it('should throw for duplicate credential index', () => { + bitManager.addEntry(0) + expect(() => bitManager.addEntry(0)).toThrow('Entry for credentialIndex 0 already exists') + }) + + it('should throw for negative credential index', () => { + expect(() => bitManager.addEntry(-1)).toThrow() + }) + + it('should throw for zero status size', () => { + expect(() => bitManager.addEntry(0, 0)).toThrow() + }) + + it('should throw for negative status size', () => { + expect(() => bitManager.addEntry(0, -1)).toThrow() + }) + }) + + describe('getStatus', () => { + it('should return 0 for new entry', () => { + bitManager.addEntry(0) + expect(bitManager.getStatus(0)).toBe(0) + }) + + it('should throw for non-existent entry', () => { + expect(() => bitManager.getStatus(0)).toThrow('No entry found for credentialIndex 0') + }) + + it('should return correct status for multi-bit entry', () => { + bitManager.addEntry(0, 4) + bitManager.setStatus(0, 5) + expect(bitManager.getStatus(0)).toBe(5) + }) + }) + + describe('setStatus', () => { + it('should set status for single bit', () => { + bitManager.addEntry(0) + bitManager.setStatus(0, 1) + expect(bitManager.getStatus(0)).toBe(1) + }) + + it('should set status for multi-bit entry', () => { + bitManager.addEntry(0, 4) + bitManager.setStatus(0, 15) + expect(bitManager.getStatus(0)).toBe(15) + }) + + it('should throw for non-existent entry', () => { + expect(() => bitManager.setStatus(0, 1)).toThrow('No entry found for credentialIndex 0') + }) + + it('should throw for negative status', () => { + bitManager.addEntry(0) + expect(() => bitManager.setStatus(0, -1)).toThrow() + }) + + it('should throw for status exceeding bit size', () => { + bitManager.addEntry(0, 2) + expect(() => bitManager.setStatus(0, 4)).toThrow('Status 4 exceeds maximum value 3 for 2 bits') + }) + + it('should handle multiple entries independently', () => { + bitManager.addEntry(0, 2) + bitManager.addEntry(1, 3) + bitManager.setStatus(0, 2) + bitManager.setStatus(1, 5) + expect(bitManager.getStatus(0)).toBe(2) + expect(bitManager.getStatus(1)).toBe(5) + }) + }) + + describe('toBuffer', () => { + it('should return empty buffer for no entries', () => { + expect(bitManager.toBuffer()).toEqual(new Uint8Array([])) + }) + + it('should return correct buffer size for entries', () => { + bitManager.addEntry(0, 9) + const buffer = bitManager.toBuffer() + expect(buffer.length).toBe(2) + }) + + it('should preserve bit patterns', () => { + bitManager.addEntry(0, 8) + bitManager.setStatus(0, 0b10101010) + const buffer = bitManager.toBuffer() + expect(buffer[0]).toBe(0b01010101) // Corrected: LSB first in BitManager + }) + }) + + describe('getEntries', () => { + it('should return empty map for no entries', () => { + expect(bitManager.getEntries().size).toBe(0) + }) + + it('should return copy of entries map', () => { + bitManager.addEntry(0) + const entries = bitManager.getEntries() + entries.clear() + expect(bitManager.getEntries().size).toBe(1) + }) + }) +}) diff --git a/__tests__/StatusList.test.ts b/__tests__/StatusList.test.ts new file mode 100644 index 0000000..e308f12 --- /dev/null +++ b/__tests__/StatusList.test.ts @@ -0,0 +1,167 @@ +import {beforeEach, describe, expect, it} from 'vitest' +import {StatusList} from '../src' + +describe('StatusList', () => { + let statusList: StatusList + + beforeEach(() => { + statusList = new StatusList() + }) + + describe('constructor', () => { + it('should create with default options', () => { + const list = new StatusList() + expect(list).toBeInstanceOf(StatusList) + }) + + it('should create with initial size', () => { + const list = new StatusList({ initialSize: 2048 }) + expect(list).toBeInstanceOf(StatusList) + }) + + it('should create with buffer', () => { + const buffer = new Uint8Array([0xFF, 0x00]) + const list = new StatusList({ buffer }) + expect(list).toBeInstanceOf(StatusList) + }) + }) + + describe('addEntry', () => { + it('should add entry with default status size', () => { + statusList.addEntry(0) + expect(statusList.getStatus(0)).toBe(0) + }) + + it('should add entry with custom status size', () => { + statusList.addEntry(0, 4) + expect(statusList.getStatus(0)).toBe(0) + }) + }) + + describe('getStatus', () => { + it('should return 0 for new entry', () => { + statusList.addEntry(0) + expect(statusList.getStatus(0)).toBe(0) + }) + + it('should return set status', () => { + statusList.addEntry(0) + statusList.setStatus(0, 1) + expect(statusList.getStatus(0)).toBe(1) + }) + }) + + describe('setStatus', () => { + it('should set status correctly', () => { + statusList.addEntry(0) + statusList.setStatus(0, 1) + expect(statusList.getStatus(0)).toBe(1) + }) + + it('should set multi-bit status', () => { + statusList.addEntry(0, 4) + statusList.setStatus(0, 12) + expect(statusList.getStatus(0)).toBe(12) + }) + }) + + 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.addEntry(0) + statusList.addEntry(1) + statusList.setStatus(0, 1) + const encoded = await statusList.encode() + expect(encoded).toMatch(/^u/) + expect(encoded.length).toBeGreaterThan(1) + }) + }) + + describe('decode', () => { + it('should decode encoded list', async () => { + statusList.addEntry(0) + statusList.addEntry(1) + statusList.setStatus(0, 1) + + const encoded = await statusList.encode() + const { buffer } = await StatusList.decode({ encodedList: encoded }) + + expect(buffer).toBeInstanceOf(Uint8Array) + expect(buffer.length).toBeGreaterThan(0) + }) + + it('should reject invalid prefix', async () => { + await expect(StatusList.decode({ encodedList: 'invalid' })) + .rejects.toThrow('encodedList must start with "u" prefix') + }) + + it('should reject non-string input', async () => { + await expect(StatusList.decode({ encodedList: null as any })) + .rejects.toThrow() + }) + + it('should handle round-trip encoding/decoding', async () => { + statusList.addEntry(0, 4) + statusList.addEntry(1, 2) + statusList.setStatus(0, 10) + statusList.setStatus(1, 3) + + const encoded = await statusList.encode() + const { buffer } = await StatusList.decode({ encodedList: encoded }) + + const decodedList = new StatusList({ buffer }) + decodedList.addEntry(0, 4) + decodedList.addEntry(1, 2) + + expect(decodedList.getStatus(0)).toBe(10) + expect(decodedList.getStatus(1)).toBe(3) + }) + }) +}) + +describe('StatusList W3C Compliance', () => { + let statusList: StatusList + + beforeEach(() => { + statusList = new StatusList() + }) + + describe('minimum 16KB requirement', () => { + it('should pad to minimum 16KB on encode', async () => { + statusList.addEntry(0) + statusList.setStatus(0, 1) + + const encoded = await statusList.encode() + const { buffer } = await StatusList.decode({ encodedList: encoded }) + + expect(buffer.length).toBe(16384) // 16KB + }) + + it('should reject lists shorter than 16KB on decode', async () => { + // Create a short buffer and manually compress/encode it + const shortBuffer = new Uint8Array(1024) // 1KB + const compressed = await import('pako').then(pako => pako.gzip(shortBuffer)) + const encoded = `u${await import('../src/utils/base64').then(b64 => b64.bytesToBase64url(compressed))}` + + await expect(StatusList.decode({ encodedList: encoded })) + .rejects.toThrow('Status list must be at least 16384 bytes (16KB)') + }) + + it('should handle large buffers correctly', async () => { + const list = new StatusList() + // Add enough entries to exceed 16KB naturally + for (let i = 0; i < 20000; i++) { + list.addEntry(i) + } + + const encoded = await list.encode() + const { buffer } = await StatusList.decode({ encodedList: encoded }) + + expect(buffer.length).toBeGreaterThanOrEqual(16384) + }) + }) +}) diff --git a/__tests__/create.test.ts b/__tests__/create.test.ts new file mode 100644 index 0000000..7b8a466 --- /dev/null +++ b/__tests__/create.test.ts @@ -0,0 +1,107 @@ +import {describe, expect, it} from 'vitest' +import {createStatusListCredential, StatusList} from '../src' + +describe('createStatusListCredential', () => { + it('should create basic credential', async () => { + const list = new StatusList() + const credential = await createStatusListCredential({ + list, + 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 list = new StatusList() + const issuer = {id: 'https://example.com/issuer', name: 'Test Issuer'} + + const credential = await createStatusListCredential({ + list, + id: 'https://example.com/status/1', + issuer, + statusPurpose: 'revocation' + }) + + expect(credential.issuer).toEqual(issuer) + }) + + it('should create credential with validity period', async () => { + const list = new StatusList() + const validFrom = '2024-01-01T00:00:00Z' + const validUntil = '2024-12-31T23:59:59Z' + + const credential = await createStatusListCredential({ + list, + id: 'https://example.com/status/1', + issuer: 'https://example.com/issuer', + statusPurpose: 'revocation', + validFrom, + validUntil + }) + + expect(credential.validFrom).toBe(validFrom) + expect(credential.validUntil).toBe(validUntil) + }) + + it('should create credential with multiple status purposes', async () => { + const list = new StatusList() + const statusPurpose = ['revocation', 'suspension'] + + const credential = await createStatusListCredential({ + list, + 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 list = new StatusList() + const ttl = 86400000 // 24 hours + + const credential = await createStatusListCredential({ + list, + 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 populated status list', async () => { + const list = new StatusList() + list.addEntry(0) + list.addEntry(1) + list.setStatus(0, 1) + + const credential = await createStatusListCredential({ + list, + id: 'https://example.com/status/1', + issuer: 'https://example.com/issuer', + statusPurpose: 'revocation' + }) + + expect(credential.credentialSubject.encodedList).toMatch(/^u/) + }) +}) diff --git a/__tests__/index.test.ts b/__tests__/index.test.ts new file mode 100644 index 0000000..0fee2af --- /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.StatusList).toBeDefined() + expect(typeof index.StatusList).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', + 'StatusList', + '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..1c63254 --- /dev/null +++ b/__tests__/verify.test.ts @@ -0,0 +1,310 @@ +import {describe, expect, it, vi} from 'vitest' +import {BitstringStatusListCredentialUnsigned, checkStatus, CredentialWithStatus, StatusList} from '../src' + + +describe('checkStatus', () => { + const createMockCredential = (statusPurpose: string = 'revocation', statusIndex: string = '0'): 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' + } + }) + + const createMockStatusListCredential = async (statuses: number[] = [0]): Promise => { + const list = new StatusList() + statuses.forEach((status, index) => { + list.addEntry(index) + list.setStatus(index, status) + }) + + 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: 'revocation', + encodedList: await list.encode() + } + } + } + + it('should verify valid credential (status 0)', async () => { + const credential = createMockCredential() + const statusListCredential = await createMockStatusListCredential([0]) + + const result = await checkStatus({ + credential, + getStatusListCredential: vi.fn().mockResolvedValue(statusListCredential) + }) + + 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 statusListCredential = await createMockStatusListCredential([1]) + + const result = await checkStatus({ + credential, + getStatusListCredential: vi.fn().mockResolvedValue(statusListCredential) + }) + + expect(result.verified).toBe(false) + expect(result.status).toBe(1) + }) + + it('should handle status messages in credential status entry', async () => { + const credential: CredentialWithStatus = { + ...createMockCredential(), + credentialStatus: { + type: 'BitstringStatusListEntry', + statusPurpose: 'revocation', + statusListIndex: '0', + statusListCredential: 'https://example.com/status/1', + statusMessage: [ + {id: '0x0', message: 'Valid'}, + {id: '0x1', message: 'Revoked'} + ] + } + } + + const statusListCredential = await createMockStatusListCredential([1]) + + const result = await checkStatus({ + credential, + getStatusListCredential: vi.fn().mockResolvedValue(statusListCredential) + }) + + 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: CredentialWithStatus = { + ...createMockCredential(), + credentialStatus: [ + { + type: 'SomeOtherStatusType' as any, + statusPurpose: 'other', + statusListIndex: '0', + statusListCredential: 'https://example.com/other' + }, + { + type: 'BitstringStatusListEntry', + statusPurpose: 'revocation', + statusListIndex: '0', + statusListCredential: 'https://example.com/status/1' + } + ] + } + + const statusListCredential = await createMockStatusListCredential([0]) + + const result = await checkStatus({ + credential, + getStatusListCredential: vi.fn().mockResolvedValue(statusListCredential) + }) + + expect(result.verified).toBe(true) + expect(result.status).toBe(0) + }) + + it('should reject credential without BitstringStatusListEntry', async () => { + const credential: CredentialWithStatus = { + ...createMockCredential(), + credentialStatus: { + statusPurpose: 'revocation', + statusListIndex: '0', + statusListCredential: 'https://example.com/status/1' + } as any + } + + const result = await checkStatus({ + credential, + getStatusListCredential: vi.fn() + }) + + 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 statusListCredential = { + ...await createMockStatusListCredential([0]), + validUntil: new Date(Date.now() - 86400000).toISOString() // 1 day ago + } + + const result = await checkStatus({ + credential, + getStatusListCredential: vi.fn().mockResolvedValue(statusListCredential) + }) + + 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 statusListCredential = { + ...await createMockStatusListCredential([0]), + validFrom: new Date(Date.now() + 86400000).toISOString() // 1 day in future + } + + const result = await checkStatus({ + credential, + getStatusListCredential: vi.fn().mockResolvedValue(statusListCredential) + }) + + 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 statusListCredential = await createMockStatusListCredential([0]) + + const result = await checkStatus({ + credential, + getStatusListCredential: vi.fn().mockResolvedValue(statusListCredential) + }) + + expect(result.verified).toBe(false) + expect(result.status).toBe(-1) + expect(result.error?.message).toContain('exceeds buffer bounds') + }) + + it('should handle getStatusListCredential rejection', async () => { + const credential = createMockCredential() + const getStatusListCredential = vi.fn().mockRejectedValue(new Error('Network error')) + + const result = await checkStatus({ + credential, + getStatusListCredential + }) + + 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: vi.fn() + }) + + expect(result.verified).toBe(false) + expect(result.status).toBe(-1) + expect(result.error).toBeDefined() + }) + + + describe('checkStatus W3C Compliance', () => { + describe('statusPurpose validation', () => { + it('should reject mismatched statusPurpose', async () => { + const credential = createMockCredential('suspension', '0') // Changed to 'suspension' + + // Status list credential has 'revocation' purpose + const list = new StatusList() + list.addEntry(0) + list.setStatus(0, 0) + + const statusListCredential: BitstringStatusListCredentialUnsigned = { + '@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', // Different from credential's 'suspension' + encodedList: await list.encode() + } + } + + const result = await checkStatus({ + credential, + getStatusListCredential: vi.fn().mockResolvedValue(statusListCredential) + }) + + 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 list = new StatusList() + list.addEntry(0) + list.setStatus(0, 0) + + const statusListCredential: BitstringStatusListCredentialUnsigned = { + '@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', 'suspension'], // Array containing matching purpose + encodedList: await list.encode() + } + } + + const result = await checkStatus({ + credential, + getStatusListCredential: vi.fn().mockResolvedValue(statusListCredential) + }) + + expect(result.verified).toBe(true) + }) + }) + + describe('minimum bitstring length validation', () => { + it('should reject status lists that are too short', async () => { + const credential = createMockCredential('revocation', '0') + + // Mock StatusList.decode to return short buffer directly + const mockDecode = vi.spyOn(StatusList, 'decode').mockImplementation(async () => { + return { buffer: new Uint8Array(1024) } // 1KB - less than required 16KB + }) + + const result = await checkStatus({ + credential, + getStatusListCredential: vi.fn().mockResolvedValue({ + credentialSubject: { + statusPurpose: 'revocation', + encodedList: 'mock-encoded-list' + } + } as any) + }) + + expect(result.verified).toBe(false) + expect(result.error?.message).toContain('Status list length error') + + mockDecode.mockRestore() + }) + }) + }) +}) + diff --git a/package.json b/package.json new file mode 100644 index 0000000..131f084 --- /dev/null +++ b/package.json @@ -0,0 +1,56 @@ +{ + "name": "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" + }, + "keywords": [ + "verifiable-credentials", + "bitstring", + "status-list", + "w3c", + "revocation", + "suspension", + "typescript" + ], + "author": "", + "license": "Apache-2.0", + "packageManager": "pnpm@10.13.1", + "files": [ + "dist", + "src", + "README.md", + "LICENSE" + ], + "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" + } +} 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..c9c2fb9 --- /dev/null +++ b/src/bit-manager/BitManager.ts @@ -0,0 +1,112 @@ +import {assertIsNonNegativeInteger, assertIsPositiveInteger} from "../utils/assertions"; + +interface BitEntry { + credentialIndex: number + statusSize: number + startBit: number +} + +export class BitManager { + private entries: Map = new Map() + private bits: Uint8Array + private nextBitPosition: number = 0 + + constructor(options: { buffer?: Uint8Array; initialSize?: number }) { + if (options.buffer) { + this.bits = new Uint8Array(options.buffer) + } else { + // Start with reasonable size, expand as needed + this.bits = new Uint8Array(options.initialSize || 1024) + } + } + + addEntry(credentialIndex: number, statusSize: number = 1): void { + assertIsNonNegativeInteger(credentialIndex, 'credentialIndex') + assertIsPositiveInteger(statusSize, 'statusSize') + + if (this.entries.has(credentialIndex)) { + throw new TypeError(`Entry for credentialIndex ${credentialIndex} already exists`) + } + + const entry: BitEntry = { + credentialIndex, + statusSize, + startBit: this.nextBitPosition + } + + this.entries.set(credentialIndex, entry) + this.nextBitPosition += statusSize + + // Expand buffer if needed + this.ensureBufferSize() + } + + getStatus(credentialIndex: number): number { + const entry = this.entries.get(credentialIndex) + if (!entry) { + throw new TypeError(`No entry found for credentialIndex ${credentialIndex}`) + } + + const {startBit, statusSize} = entry + let status = 0 + + for (let i = 0; i < statusSize; i++) { + const bitIndex = startBit + i + const byteIndex = Math.floor(bitIndex / 8) + const bitOffset = bitIndex % 8 + + const bit = (this.bits[byteIndex] >> (7 - bitOffset)) & 1 + status |= bit << i + } + + return status + } + + setStatus(credentialIndex: number, status: number): void { + const entry = this.entries.get(credentialIndex) + if (!entry) { + throw new TypeError(`No entry found for credentialIndex ${credentialIndex}`) + } + + const {startBit, statusSize} = entry + const maxValue = (1 << statusSize) - 1 + + assertIsNonNegativeInteger(status, 'status') + if (status > maxValue) { + throw new TypeError(`Status ${status} exceeds maximum value ${maxValue} for ${statusSize} bits`) + } + + for (let i = 0; i < statusSize; i++) { + const bitIndex = startBit + i + const byteIndex = Math.floor(bitIndex / 8) + const bitOffset = bitIndex % 8 + + const bit = (status >> i) & 1 + + if (bit === 1) { + this.bits[byteIndex] |= (1 << (7 - bitOffset)) + } else { + this.bits[byteIndex] &= ~(1 << (7 - bitOffset)) + } + } + } + + private ensureBufferSize(): void { + const requiredBytes = Math.ceil(this.nextBitPosition / 8) + if (requiredBytes > this.bits.length) { + const newSize = Math.max(requiredBytes, this.bits.length * 2) + const newBuffer = new Uint8Array(newSize) + newBuffer.set(this.bits) + this.bits = newBuffer + } + } + + toBuffer(): Uint8Array { + const requiredBytes = Math.ceil(this.nextBitPosition / 8) + return this.bits.slice(0, requiredBytes) + } + + getEntries(): Map { + return new Map(this.entries) + } +} \ No newline at end of file diff --git a/src/credential/create.ts b/src/credential/create.ts new file mode 100644 index 0000000..6b66de9 --- /dev/null +++ b/src/credential/create.ts @@ -0,0 +1,43 @@ +/** + * High-level functions for creating status list credentials + */ + +import {StatusList} from '../status-list/StatusList' +import {BitstringStatusListCredentialSubject, BitstringStatusListCredentialUnsigned, IIssuer, StatusMessage} from '../types' + +export async function createStatusListCredential(options: { + list: StatusList + id: string + issuer: string | IIssuer + validFrom?: string + validUntil?: string + statusPurpose: string | string[] + statusMessage?: StatusMessage[] + ttl?: number +}): Promise { + const {list, id, issuer, validFrom, validUntil, statusPurpose, statusMessage, ttl} = options + + const encodedList = await list.encode() + + const credentialSubject = { + id: `${id}#list`, + type: 'BitstringStatusList', + statusPurpose, + encodedList, + ...(statusMessage && {statusMessage}), + ...(ttl && {ttl}) + } satisfies BitstringStatusListCredentialSubject + + return { + '@context': [ + 'https://www.w3.org/ns/credentials/v2', + 'https://www.w3.org/ns/credentials/status/v1' + ], + id, + type: ['VerifiableCredential', 'BitstringStatusListCredential'], + issuer, + credentialSubject, + ...(validFrom && {validFrom}), + ...(validUntil && {validUntil}) + } +} diff --git a/src/credential/verify.ts b/src/credential/verify.ts new file mode 100644 index 0000000..7d58897 --- /dev/null +++ b/src/credential/verify.ts @@ -0,0 +1,109 @@ +/** + * Verifying credential status logic + */ + +import {StatusList} from '../status-list/StatusList' +import {BitstringStatusListEntry, CheckStatusOptions, StatusMessage, VerificationResult} from '../types' +import {assertIsObject} from '../utils/assertions' + +export async function checkStatus(options: CheckStatusOptions): Promise { + try { + const {credential, getStatusListCredential} = options + + assertIsObject(credential, 'credential') + + if (!credential.credentialStatus) { + return Promise.reject(new Error('No credentialStatus found in credential')) + } + + // Extract BitstringStatusListEntry (handle both single and array) + const statusEntries = Array.isArray(credential.credentialStatus) + ? credential.credentialStatus + : [credential.credentialStatus] + + const entry = statusEntries.find(e => e.type === 'BitstringStatusListEntry') as BitstringStatusListEntry + if (!entry) { + throw Error('No BitstringStatusListEntry found in credentialStatus') + } + + // Get the status list credential (assumed to be verified) + const listCredential = await getStatusListCredential(entry.statusListCredential) + + // Check validity period + 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') + } + + // Validate statusPurpose matches (W3C spec requirement) + const listStatusPurpose = listCredential.credentialSubject.statusPurpose + const purposes = Array.isArray(listStatusPurpose) ? listStatusPurpose : [listStatusPurpose] + + console.log('Entry statusPurpose:', entry.statusPurpose) + console.log('List statusPurpose:', purposes) + + if (!purposes.includes(entry.statusPurpose)) { + throw new Error(`Status purpose '${entry.statusPurpose}' does not match any purpose in status list credential: ${purposes.join(', ')}`) + } + + // Decode the status list and get status + const {buffer} = await StatusList.decode({ + encodedList: listCredential.credentialSubject.encodedList + }) + + // Verify minimum bitstring length (W3C spec requirement) + const statusSize = entry.statusSize || 1 + const totalBits = buffer.length * 8 + const minEntries = 131072 / statusSize // 16KB in bits divided by statusSize + + if (totalBits / statusSize < minEntries) { + throw new Error(`Status list length error: bitstring must support at least ${minEntries} entries for statusSize ${statusSize}`) + } + + const statusIndex = parseInt(entry.statusListIndex, 10) + const startBit = statusIndex * statusSize + + let status = 0 + for (let i = 0; i < statusSize; i++) { + const bitIndex = startBit + i + const byteIndex = Math.floor(bitIndex / 8) + const bitOffset = bitIndex % 8 + + if (byteIndex >= buffer.length) { + throw Error(`Index ${statusIndex} with statusSize ${statusSize} exceeds buffer bounds`) + } + + const bit = (buffer[byteIndex] >> (7 - bitOffset)) & 1 + status |= bit << i + } + + // Find corresponding status message + const statusHex = `0x${status.toString(16)}` + let statusMessage: StatusMessage | undefined + if (entry.statusMessage) { + statusMessage = entry.statusMessage.find(msg => msg.id === statusHex) + } + + // Determine verification result based on status purpose + let verified = true + if (entry.statusPurpose === 'revocation' || entry.statusPurpose === 'suspension') { + verified = status === 0 // 0 means not revoked/suspended + } + + return { + verified, + status, + statusMessage + } + + } catch (error) { + return { + verified: false, + status: -1, + error: error instanceof Error ? error : new Error(String(error)) + } + } +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..ccd3ad5 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,21 @@ +// Core Classes +export {BitManager} from './bit-manager/BitManager' +export {StatusList} from './status-list/StatusList' + +// High-Level Functions +export {createStatusListCredential} from './credential/create' +export {checkStatus} from './credential/verify' + +// Types +export type { + StatusMessage, + BitstringStatusListEntry, + BitstringStatusListCredentialSubject, + BitstringStatusListCredentialUnsigned, + IIssuer, + CredentialStatus, + CredentialWithStatus, + CheckStatusOptions, + VerificationResult, + AdditionalClaims +} from './types' diff --git a/src/status-list/StatusList.ts b/src/status-list/StatusList.ts new file mode 100644 index 0000000..c08cfda --- /dev/null +++ b/src/status-list/StatusList.ts @@ -0,0 +1,73 @@ +/** + * Status list encoding/decoding wrapper using BitManager + * Handles gzip compression and base64url encoding as per W3C spec + */ + +import {BitManager} from '../bit-manager/BitManager' +import {base64urlToBytes, bytesToBase64url} from '../utils/base64' +import {assertIsString} from '../utils/assertions' +import pako from 'pako' + +// W3C spec requires minimum 16KB (131,072 bits) +const MIN_BITSTRING_SIZE_BYTES = 16384 // 16KB + +export class StatusList { + private bitManager: BitManager + + constructor(options: { buffer?: Uint8Array; initialSize?: number } = {}) { + this.bitManager = new BitManager(options) + } + + addEntry(credentialIndex: number, statusSize: number = 1): void { + this.bitManager.addEntry(credentialIndex, statusSize) + } + + getStatus(credentialIndex: number): number { + return this.bitManager.getStatus(credentialIndex) + } + + setStatus(credentialIndex: number, status: number): void { + this.bitManager.setStatus(credentialIndex, status) + } + + async encode(): Promise { + const buffer = this.bitManager.toBuffer() + + // Pad to minimum 16KB as required by W3C spec + const paddedBuffer = this.padToMinimumSize(buffer) + + const compressed = pako.gzip(paddedBuffer) + const encoded = bytesToBase64url(compressed) + return `u${encoded}` + } + + private padToMinimumSize(buffer: Uint8Array): Uint8Array { + if (buffer.length >= MIN_BITSTRING_SIZE_BYTES) { + return buffer + } + + const paddedBuffer = new Uint8Array(MIN_BITSTRING_SIZE_BYTES) + paddedBuffer.set(buffer) + return paddedBuffer + } + + static async decode(options: { encodedList: string }): Promise<{ buffer: Uint8Array }> { + const {encodedList} = options + assertIsString(encodedList, 'encodedList') + + if (!encodedList.startsWith('u')) { + return Promise.reject(new TypeError('encodedList must start with "u" prefix')) + } + + const base64urlString = encodedList.slice(1) + const compressed = base64urlToBytes(base64urlString) + const buffer = pako.ungzip(compressed) + + // Verify minimum size requirement + if (buffer.length < MIN_BITSTRING_SIZE_BYTES) { + return Promise.reject(new TypeError(`Status list must be at least ${MIN_BITSTRING_SIZE_BYTES} bytes (16KB), got ${buffer.length} bytes`)) + } + + return {buffer} + } +} \ No newline at end of file diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..1e421c8 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,75 @@ +/** + * Central type definitions for the Bitstring Status List library + * Based on W3C Bitstring Status List v1.0 specification + */ + +export interface StatusMessage { + id: string // The hex status code (e.g., "0x0", "0x1") + message: string // The human-readable description of the status +} + +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[] + +export interface CredentialWithStatus { + '@context': string[] + id: string + type: string[] + issuer: string | IIssuer + validFrom?: string + validUntil?: string + credentialStatus?: CredentialStatus + credentialSubject: { + id: string + type: string + [key: string]: any + } +} + +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 +} 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 From 0478d25a1a6182708d302076c083233b8988e267 Mon Sep 17 00:00:00 2001 From: sanderPostma Date: Wed, 9 Jul 2025 18:17:53 +0200 Subject: [PATCH 02/31] chore: initial README.md --- README.md | 310 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 309 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 81cc2b0..8616f78 100644 --- a/README.md +++ b/README.md @@ -1 +1,309 @@ -# vc-bitstring-status-lists \ No newline at end of file +# vc-bitstring-status-lists# 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: + +- **Efficient bit manipulation** through the `BitManager` class, which handles the low-level operations of setting and getting status bits +- **Compressed storage** using gzip compression and base64url encoding, meeting the W3C requirement for minimum 16KB bitstrings +- **Multiple status support** beyond simple revocation/suspension, including custom status messages and multi-bit 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 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 + +```typescript +import { StatusList, createStatusListCredential } from 'vc-bitstring-status-lists' + +// Create a new status list +const statusList = new StatusList() + +// Add credentials to track (each gets assigned a sequential index) +statusList.addEntry(0) // First credential at index 0 +statusList.addEntry(1) // Second credential at index 1 +statusList.addEntry(2) // Third credential at index 2 + +// Set some statuses (0 = valid, 1 = revoked for 'revocation' purpose) +statusList.setStatus(0, 0) // Valid +statusList.setStatus(1, 1) // Revoked +statusList.setStatus(2, 0) // Valid +``` +
+
+ + + +
+
+ All new entries have status 0 - Valid by default +
+
+ + +### 2. Publishing the Status List + +```typescript +// Create a verifiable credential containing the status list +const statusListCredential = await createStatusListCredential({ + list: statusList, + id: 'https://example.com/status-lists/1', + issuer: 'https://example.com/issuer', + statusPurpose: 'revocation', + validFrom: '2024-01-01T00:00:00Z', + validUntil: '2024-12-31T23:59:59Z' +}) + +// The credential now contains a compressed, encoded bitstring +console.log(statusListCredential.credentialSubject.encodedList) +// Output: "u..." (compressed and base64url-encoded bitstring) +``` + +### 3. Checking Credential Status + +```typescript +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 + statusListCredential: 'https://example.com/status-lists/1' + } +} + +// Check the credential's status +const result = await checkStatus({ + credential, + getStatusListCredential: async (url) => { + // In practice, you'd fetch this from the URL + return statusListCredential + } +}) + +console.log(result) +// Output: { verified: false, status: 1 } (credential is revoked) +``` + +## 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: + +```typescript +const statusList = new StatusList() + +// Add credential with 4 bits of status information (supports values 0-15) +statusList.addEntry(0, 4) + +// Set a complex status +statusList.setStatus(0, 12) // Binary: 1100, could represent multiple flags + +// Get the status +const status = statusList.getStatus(0) // Returns: 12 +``` + +### Status Messages + +You can provide human-readable messages for different status values: + +```typescript +const credential = { + // ... other properties + credentialStatus: { + type: 'BitstringStatusListEntry', + statusPurpose: 'revocation', + statusListIndex: '0', + statusListCredential: 'https://example.com/status-lists/1', + statusMessage: [ + { id: '0x0', message: 'Credential is valid' }, + { id: '0x1', message: 'Credential has been revoked' }, + { id: '0x2', message: 'Credential is under review' } + ] + } +} +``` + +### Multiple Status Purposes + +A single status list can serve multiple purposes: + +```typescript +const statusListCredential = await createStatusListCredential({ + list: statusList, + id: 'https://example.com/status-lists/1', + issuer: 'https://example.com/issuer', + statusPurpose: ['revocation', 'suspension'] // Multiple purposes +}) +``` + +## Understanding the Architecture + +The library is built around several key components that work together: + +### 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. + +### StatusList Class + +The `StatusList` 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 16KB 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 + +This library implements all requirements from the W3C Bitstring Status List v1.0 specification: + +- **Minimum bitstring size**: All encoded status lists are padded to at least 16KB (131,072 bits) +- **Compression**: Uses gzip compression as required by the specification +- **Base64url encoding**: Proper encoding with the required "u" prefix +- **Status purpose validation**: Ensures that credential entries match the status list's declared purposes +- **Temporal validation**: Checks `validFrom` and `validUntil` dates on status list credentials + +## API Reference + +### Core Classes + +#### `StatusList` + +The main class for creating and managing status lists. + +```typescript +class StatusList { + constructor(options?: { buffer?: Uint8Array; initialSize?: number }) + addEntry(credentialIndex: number, statusSize?: number): void + getStatus(credentialIndex: number): number + setStatus(credentialIndex: number, status: number): void + encode(): Promise + static decode(options: { encodedList: string }): Promise<{ buffer: Uint8Array }> +} +``` + +#### `BitManager` + +Low-level bit manipulation class (typically used internally). + +```typescript +class BitManager { + constructor(options: { buffer?: Uint8Array; initialSize?: number }) + addEntry(credentialIndex: number, statusSize?: number): void + getStatus(credentialIndex: number): number + setStatus(credentialIndex: number, status: number): void + toBuffer(): Uint8Array + getEntries(): Map +} +``` + +### High-Level Functions + +#### `createStatusListCredential` + +Creates a verifiable credential containing a status list. + +```typescript +function createStatusListCredential(options: { + list: StatusList + id: string + issuer: string | IIssuer + validFrom?: string + validUntil?: string + statusPurpose: string | string[] + statusMessage?: StatusMessage[] + ttl?: number +}): Promise +``` + +#### `checkStatus` + +Verifies a credential's status against its referenced status list. + +```typescript +function checkStatus(options: { + credential: CredentialWithStatus + getStatusListCredential: (url: string) => Promise +}): Promise +``` + +## Error Handling + +The library provides comprehensive error handling with descriptive messages: + +```typescript +try { + const result = await checkStatus({ credential, getStatusListCredential }) + if (!result.verified) { + console.log('Verification failed:', result.error?.message) + console.log('Status code:', result.status) + } +} catch (error) { + console.error('Status check failed:', error) +} +``` + +## Building and Testing + +The project uses modern TypeScript tooling: + +```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/) From 7a52314c20997d5f7973dd5546d82721eed9c65d Mon Sep 17 00:00:00 2001 From: sanderPostma Date: Wed, 9 Jul 2025 18:21:48 +0200 Subject: [PATCH 03/31] chore: README.md infobox update --- README.md | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/README.md b/README.md index 8616f78..d7d01cd 100644 --- a/README.md +++ b/README.md @@ -46,16 +46,7 @@ statusList.setStatus(0, 0) // Valid statusList.setStatus(1, 1) // Revoked statusList.setStatus(2, 0) // Valid ``` -
-
- - - -
-
- All new entries have status 0 - Valid by default -
-
+> ℹ️ - All new entries have status `0` – Valid by default ### 2. Publishing the Status List From c0423067437ee903b1b225ee4d4f4cf2af36bd55 Mon Sep 17 00:00:00 2001 From: sanderPostma Date: Wed, 9 Jul 2025 18:25:51 +0200 Subject: [PATCH 04/31] chore: updated package.json --- package.json | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 131f084..7d8e13a 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "vc-bitstring-status-lists", + "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", @@ -29,7 +29,12 @@ "suspension", "typescript" ], - "author": "", + "author": "4Sure Technology Solutions", + "repository": { + "type": "git", + "url": "https://github.com/4sure-tech/vc-bitstring-status-lists.git" + }, + "homepage": "https://4sure.tech", "license": "Apache-2.0", "packageManager": "pnpm@10.13.1", "files": [ From bcd5038806d569b4642efb12ee4bf29c72baaf1f Mon Sep 17 00:00:00 2001 From: sanderPostma Date: Wed, 9 Jul 2025 18:28:10 +0200 Subject: [PATCH 05/31] chore: ignore .idea --- .gitignore | 1 + 1 file changed, 1 insertion(+) 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/ From 4d0bb180bf756fce25d4b777730646b805071391 Mon Sep 17 00:00:00 2001 From: sanderPostma Date: Wed, 9 Jul 2025 18:43:24 +0200 Subject: [PATCH 06/31] chore: removed rogue field --- README.md | 1 - src/credential/create.ts | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/README.md b/README.md index d7d01cd..9752b90 100644 --- a/README.md +++ b/README.md @@ -231,7 +231,6 @@ function createStatusListCredential(options: { validFrom?: string validUntil?: string statusPurpose: string | string[] - statusMessage?: StatusMessage[] ttl?: number }): Promise ``` diff --git a/src/credential/create.ts b/src/credential/create.ts index 6b66de9..73ae421 100644 --- a/src/credential/create.ts +++ b/src/credential/create.ts @@ -15,7 +15,7 @@ export async function createStatusListCredential(options: { statusMessage?: StatusMessage[] ttl?: number }): Promise { - const {list, id, issuer, validFrom, validUntil, statusPurpose, statusMessage, ttl} = options + const {list, id, issuer, validFrom, validUntil, statusPurpose, ttl} = options const encodedList = await list.encode() @@ -24,7 +24,6 @@ export async function createStatusListCredential(options: { type: 'BitstringStatusList', statusPurpose, encodedList, - ...(statusMessage && {statusMessage}), ...(ttl && {ttl}) } satisfies BitstringStatusListCredentialSubject From bf41ed92c6b9b0a4f23e8438034d45d1c2a43c36 Mon Sep 17 00:00:00 2001 From: sanderPostma Date: Wed, 9 Jul 2025 20:14:16 +0200 Subject: [PATCH 07/31] chore: tweaking implementation --- README.md | 2 +- __tests__/create.test.ts | 12 ++--- src/bit-manager/BitManager.ts | 15 +++--- src/credential/create.ts | 18 +++---- src/credential/verify.ts | 3 -- src/index.ts | 1 + src/status-list/StatusList.ts | 88 +++++++++++++++++++++++------------ src/types.ts | 12 +++-- 8 files changed, 92 insertions(+), 59 deletions(-) diff --git a/README.md b/README.md index 9752b90..4405625 100644 --- a/README.md +++ b/README.md @@ -150,7 +150,7 @@ A single status list can serve multiple purposes: ```typescript const statusListCredential = await createStatusListCredential({ - list: statusList, + statusList: statusList, id: 'https://example.com/status-lists/1', issuer: 'https://example.com/issuer', statusPurpose: ['revocation', 'suspension'] // Multiple purposes diff --git a/__tests__/create.test.ts b/__tests__/create.test.ts index 7b8a466..9ad1984 100644 --- a/__tests__/create.test.ts +++ b/__tests__/create.test.ts @@ -5,7 +5,7 @@ describe('createStatusListCredential', () => { it('should create basic credential', async () => { const list = new StatusList() const credential = await createStatusListCredential({ - list, + statusList: list, id: 'https://example.com/status/1', issuer: 'https://example.com/issuer', statusPurpose: 'revocation' @@ -33,7 +33,7 @@ describe('createStatusListCredential', () => { const issuer = {id: 'https://example.com/issuer', name: 'Test Issuer'} const credential = await createStatusListCredential({ - list, + statusList: list, id: 'https://example.com/status/1', issuer, statusPurpose: 'revocation' @@ -48,7 +48,7 @@ describe('createStatusListCredential', () => { const validUntil = '2024-12-31T23:59:59Z' const credential = await createStatusListCredential({ - list, + statusList: list, id: 'https://example.com/status/1', issuer: 'https://example.com/issuer', statusPurpose: 'revocation', @@ -65,7 +65,7 @@ describe('createStatusListCredential', () => { const statusPurpose = ['revocation', 'suspension'] const credential = await createStatusListCredential({ - list, + statusList: list, id: 'https://example.com/status/1', issuer: 'https://example.com/issuer', statusPurpose @@ -79,7 +79,7 @@ describe('createStatusListCredential', () => { const ttl = 86400000 // 24 hours const credential = await createStatusListCredential({ - list, + statusList: list, id: 'https://example.com/status/1', issuer: 'https://example.com/issuer', statusPurpose: 'revocation', @@ -96,7 +96,7 @@ describe('createStatusListCredential', () => { list.setStatus(0, 1) const credential = await createStatusListCredential({ - list, + statusList: list, id: 'https://example.com/status/1', issuer: 'https://example.com/issuer', statusPurpose: 'revocation' diff --git a/src/bit-manager/BitManager.ts b/src/bit-manager/BitManager.ts index c9c2fb9..8483a8e 100644 --- a/src/bit-manager/BitManager.ts +++ b/src/bit-manager/BitManager.ts @@ -1,5 +1,7 @@ import {assertIsNonNegativeInteger, assertIsPositiveInteger} from "../utils/assertions"; +const EXPAND_BLOCK_SIZE = 16384 // 16KB + interface BitEntry { credentialIndex: number statusSize: number @@ -11,15 +13,14 @@ export class BitManager { private bits: Uint8Array private nextBitPosition: number = 0 - constructor(options: { buffer?: Uint8Array; initialSize?: number }) { + constructor(options: { buffer?: Uint8Array; initialSize?: number } = {}) { if (options.buffer) { this.bits = new Uint8Array(options.buffer) } else { - // Start with reasonable size, expand as needed - this.bits = new Uint8Array(options.initialSize || 1024) + // Default to W3C minimum size (16KB) + this.bits = new Uint8Array(options.initialSize || 16384) } } - addEntry(credentialIndex: number, statusSize: number = 1): void { assertIsNonNegativeInteger(credentialIndex, 'credentialIndex') assertIsPositiveInteger(statusSize, 'statusSize') @@ -94,13 +95,15 @@ export class BitManager { private ensureBufferSize(): void { const requiredBytes = Math.ceil(this.nextBitPosition / 8) if (requiredBytes > this.bits.length) { - const newSize = Math.max(requiredBytes, this.bits.length * 2) + // Expand in 16KB blocks to maintain W3C compliance and efficiency + const blocksNeeded = Math.ceil(requiredBytes / EXPAND_BLOCK_SIZE) + const newSize = blocksNeeded * EXPAND_BLOCK_SIZE + const newBuffer = new Uint8Array(newSize) newBuffer.set(this.bits) this.bits = newBuffer } } - toBuffer(): Uint8Array { const requiredBytes = Math.ceil(this.nextBitPosition / 8) return this.bits.slice(0, requiredBytes) diff --git a/src/credential/create.ts b/src/credential/create.ts index 73ae421..789c40d 100644 --- a/src/credential/create.ts +++ b/src/credential/create.ts @@ -3,26 +3,26 @@ */ import {StatusList} from '../status-list/StatusList' -import {BitstringStatusListCredentialSubject, BitstringStatusListCredentialUnsigned, IIssuer, StatusMessage} from '../types' +import {BitstringStatusListCredentialSubject, BitstringStatusListCredentialUnsigned, IIssuer} from '../types' export async function createStatusListCredential(options: { - list: StatusList id: string issuer: string | IIssuer - validFrom?: string - validUntil?: string + statusSize?: number statusPurpose: string | string[] - statusMessage?: StatusMessage[] + validFrom?: Date + validUntil?: Date ttl?: number }): Promise { - const {list, id, issuer, validFrom, validUntil, statusPurpose, ttl} = options + const { id, issuer, statusSize ,validFrom, validUntil, statusPurpose, ttl} = options - const encodedList = await list.encode() + const encodedList = await new StatusList({statusSize}).encode() const credentialSubject = { id: `${id}#list`, type: 'BitstringStatusList', statusPurpose, + statusSize: statusSize ?? 1, encodedList, ...(ttl && {ttl}) } satisfies BitstringStatusListCredentialSubject @@ -36,7 +36,7 @@ export async function createStatusListCredential(options: { type: ['VerifiableCredential', 'BitstringStatusListCredential'], issuer, credentialSubject, - ...(validFrom && {validFrom}), - ...(validUntil && {validUntil}) + ...(validFrom && {validFrom: validFrom.toISOString()}), + ...(validUntil && {validUntil: validUntil.toISOString()}) } } diff --git a/src/credential/verify.ts b/src/credential/verify.ts index 7d58897..b1adde5 100644 --- a/src/credential/verify.ts +++ b/src/credential/verify.ts @@ -42,9 +42,6 @@ export async function checkStatus(options: CheckStatusOptions): Promise { - const buffer = this.bitManager.toBuffer() - - // Pad to minimum 16KB as required by W3C spec - const paddedBuffer = this.padToMinimumSize(buffer) - - const compressed = pako.gzip(paddedBuffer) - const encoded = bytesToBase64url(compressed) - return `u${encoded}` + /** @returns the list-level bit-width */ + getStatusSize(): number { + return this.statusSize } - private padToMinimumSize(buffer: Uint8Array): Uint8Array { - if (buffer.length >= MIN_BITSTRING_SIZE_BYTES) { - return buffer - } + async encode(): Promise { + const buffer = this.bitManager.toBuffer() + const padded = buffer.length >= MIN_BITSTRING_SIZE_BYTES + ? buffer + : Uint8Array.from({length: MIN_BITSTRING_SIZE_BYTES}, (_, i) => buffer[i] ?? 0) - const paddedBuffer = new Uint8Array(MIN_BITSTRING_SIZE_BYTES) - paddedBuffer.set(buffer) - return paddedBuffer + const compressed = pako.gzip(padded) + return `u${bytesToBase64url(compressed)}` } static async decode(options: { encodedList: string }): Promise<{ buffer: Uint8Array }> { const {encodedList} = options assertIsString(encodedList, 'encodedList') - if (!encodedList.startsWith('u')) { - return Promise.reject(new TypeError('encodedList must start with "u" prefix')) + throw new TypeError('encodedList must start with "u" prefix') } - const base64urlString = encodedList.slice(1) - const compressed = base64urlToBytes(base64urlString) + const compressed = base64urlToBytes(encodedList.slice(1)) const buffer = pako.ungzip(compressed) - // Verify minimum size requirement if (buffer.length < MIN_BITSTRING_SIZE_BYTES) { - return Promise.reject(new TypeError(`Status list must be at least ${MIN_BITSTRING_SIZE_BYTES} bytes (16KB), got ${buffer.length} bytes`)) + throw new TypeError( + `Status list must be at least ${MIN_BITSTRING_SIZE_BYTES} bytes (16KB), got ${buffer.length}` + ) } return {buffer} } -} \ No newline at end of file + + /** + * Compute how many list entries are present by reading the gzip ISIZE + * (uncompressed length) and dividing by the uniform statusSize. + * @param encodedList u-prefixed, gzip-compressed base64url string + * @param statusSize uniform bit-width used when encoding + */ + static getStatusListLength(encodedList: string, statusSize: number): number { + assertIsString(encodedList, 'encodedList') + assertIsPositiveInteger(statusSize, 'statusSize') + if (!encodedList.startsWith('u')) { + throw new TypeError('encodedList must start with "u" prefix') + } + + const data = base64urlToBytes(encodedList.slice(1)) + if (data.length < 4) { + throw new TypeError('Invalid gzip data: too short to contain ISIZE') + } + const offset = data.byteOffset + data.length - 4 + const view = new DataView(data.buffer, offset, 4) + const uncompressedBytes = view.getUint32(0, true) + + // total bits = uncompressedBytes * 8 + // number of entries = totalBits / statusSize + return Math.floor((uncompressedBytes * 8) / statusSize) + } +} diff --git a/src/types.ts b/src/types.ts index 1e421c8..5ac9b20 100644 --- a/src/types.ts +++ b/src/types.ts @@ -2,11 +2,7 @@ * Central type definitions for the Bitstring Status List library * Based on W3C Bitstring Status List v1.0 specification */ - -export interface StatusMessage { - id: string // The hex status code (e.g., "0x0", "0x1") - message: string // The human-readable description of the status -} +export type BitstringStatusPurpose = 'revocation' | 'suspension' | 'refresh' | 'message' | string export interface BitstringStatusListEntry { id?: string // Optional identifier for the status list entry @@ -23,6 +19,7 @@ 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 + statusSize: number // The statusSize indicates the size of the status entry in bits encodedList: string // The u-prefixed, compressed, base64url-encoded string ttl?: number // Optional time to live in milliseconds } @@ -73,3 +70,8 @@ export interface VerificationResult { 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 +} From 40022626965c681888202274d63a68309d481aeb Mon Sep 17 00:00:00 2001 From: sanderPostma Date: Thu, 10 Jul 2025 01:16:44 +0200 Subject: [PATCH 08/31] chore: simplified and all tests passing --- __tests__/BitManager.test.ts | 117 ++++-------- __tests__/StatusList.test.ts | 200 ++++++++++++--------- __tests__/create.test.ts | 73 +++++--- __tests__/index.test.ts | 6 +- __tests__/verify.test.ts | 240 +++++++++++++------------ src/bit-manager/BitManager.ts | 204 ++++++++++++++------- src/credential/create.ts | 78 ++++++-- src/credential/verify.ts | 224 ++++++++++++++++------- src/index.ts | 2 +- src/status-list/BitstreamStatusList.ts | 196 ++++++++++++++++++++ src/status-list/StatusList.ts | 103 ----------- src/types.ts | 1 - 12 files changed, 898 insertions(+), 546 deletions(-) create mode 100644 src/status-list/BitstreamStatusList.ts delete mode 100644 src/status-list/StatusList.ts diff --git a/__tests__/BitManager.test.ts b/__tests__/BitManager.test.ts index 8e56481..1c698b0 100644 --- a/__tests__/BitManager.test.ts +++ b/__tests__/BitManager.test.ts @@ -22,101 +22,66 @@ describe('BitManager', () => { it('should create with existing buffer', () => { const buffer = new Uint8Array([0xFF, 0x00, 0xFF]) const manager = new BitManager({buffer}) - expect(manager.toBuffer()).toEqual(new Uint8Array([])) - }) - }) - - describe('addEntry', () => { - it('should add entry with default status size', () => { - bitManager.addEntry(0) - const entries = bitManager.getEntries() - expect(entries.has(0)).toBe(true) - expect(entries.get(0)?.statusSize).toBe(1) - }) - - it('should add entry with custom status size', () => { - bitManager.addEntry(0, 4) - const entries = bitManager.getEntries() - expect(entries.get(0)?.statusSize).toBe(4) - }) - - it('should assign consecutive bit positions', () => { - bitManager.addEntry(0, 2) - bitManager.addEntry(1, 3) - const entries = bitManager.getEntries() - expect(entries.get(0)?.startBit).toBe(0) - expect(entries.get(1)?.startBit).toBe(2) + expect(manager.toBuffer()).toEqual(new Uint8Array([0xFF, 0x00, 0xFF])) }) - it('should throw for duplicate credential index', () => { - bitManager.addEntry(0) - expect(() => bitManager.addEntry(0)).toThrow('Entry for credentialIndex 0 already exists') - }) - - it('should throw for negative credential index', () => { - expect(() => bitManager.addEntry(-1)).toThrow() - }) - - it('should throw for zero status size', () => { - expect(() => bitManager.addEntry(0, 0)).toThrow() - }) - - it('should throw for negative status size', () => { - expect(() => bitManager.addEntry(0, -1)).toThrow() + 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 new entry', () => { - bitManager.addEntry(0) + it('should return 0 for unset credential', () => { expect(bitManager.getStatus(0)).toBe(0) }) - it('should throw for non-existent entry', () => { - expect(() => bitManager.getStatus(0)).toThrow('No entry found for credentialIndex 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 return correct status for multi-bit entry', () => { - bitManager.addEntry(0, 4) - bitManager.setStatus(0, 5) - expect(bitManager.getStatus(0)).toBe(5) + it('should throw for negative credential index', () => { + expect(() => bitManager.getStatus(-1)).toThrow() + }) + + 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.addEntry(0) bitManager.setStatus(0, 1) expect(bitManager.getStatus(0)).toBe(1) }) it('should set status for multi-bit entry', () => { - bitManager.addEntry(0, 4) - bitManager.setStatus(0, 15) - expect(bitManager.getStatus(0)).toBe(15) + const manager = new BitManager({statusSize: 4}) + manager.setStatus(0, 15) + expect(manager.getStatus(0)).toBe(15) }) - it('should throw for non-existent entry', () => { - expect(() => bitManager.setStatus(0, 1)).toThrow('No entry found for credentialIndex 0') + it('should throw for negative credential index', () => { + expect(() => bitManager.setStatus(-1, 1)).toThrow() }) it('should throw for negative status', () => { - bitManager.addEntry(0) expect(() => bitManager.setStatus(0, -1)).toThrow() }) it('should throw for status exceeding bit size', () => { - bitManager.addEntry(0, 2) - expect(() => bitManager.setStatus(0, 4)).toThrow('Status 4 exceeds maximum value 3 for 2 bits') + 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', () => { - bitManager.addEntry(0, 2) - bitManager.addEntry(1, 3) - bitManager.setStatus(0, 2) - bitManager.setStatus(1, 5) - expect(bitManager.getStatus(0)).toBe(2) - expect(bitManager.getStatus(1)).toBe(5) + 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) }) }) @@ -126,29 +91,17 @@ describe('BitManager', () => { }) it('should return correct buffer size for entries', () => { - bitManager.addEntry(0, 9) - const buffer = bitManager.toBuffer() + const manager = new BitManager({statusSize: 9}) + manager.setStatus(0, 1) + const buffer = manager.toBuffer() expect(buffer.length).toBe(2) }) it('should preserve bit patterns', () => { - bitManager.addEntry(0, 8) - bitManager.setStatus(0, 0b10101010) - const buffer = bitManager.toBuffer() - expect(buffer[0]).toBe(0b01010101) // Corrected: LSB first in BitManager - }) - }) - - describe('getEntries', () => { - it('should return empty map for no entries', () => { - expect(bitManager.getEntries().size).toBe(0) - }) - - it('should return copy of entries map', () => { - bitManager.addEntry(0) - const entries = bitManager.getEntries() - entries.clear() - expect(bitManager.getEntries().size).toBe(1) + const manager = new BitManager({statusSize: 8}) + manager.setStatus(0, 0b10101010) + const buffer = manager.toBuffer() + expect(buffer[0]).toBe(0b01010101) // LSB-first mapping }) }) -}) +}) \ No newline at end of file diff --git a/__tests__/StatusList.test.ts b/__tests__/StatusList.test.ts index e308f12..337d10d 100644 --- a/__tests__/StatusList.test.ts +++ b/__tests__/StatusList.test.ts @@ -1,51 +1,44 @@ import {beforeEach, describe, expect, it} from 'vitest' -import {StatusList} from '../src' +import {BitstreamStatusList} from '../src' describe('StatusList', () => { - let statusList: StatusList + let statusList: BitstreamStatusList beforeEach(() => { - statusList = new StatusList() + statusList = new BitstreamStatusList({statusSize: 1}) }) describe('constructor', () => { it('should create with default options', () => { - const list = new StatusList() - expect(list).toBeInstanceOf(StatusList) + const list = new BitstreamStatusList() + expect(list).toBeInstanceOf(BitstreamStatusList) + expect(list.getStatusSize()).toBe(1) // default }) it('should create with initial size', () => { - const list = new StatusList({ initialSize: 2048 }) - expect(list).toBeInstanceOf(StatusList) + const list = new BitstreamStatusList({initialSize: 2048}) + expect(list).toBeInstanceOf(BitstreamStatusList) }) it('should create with buffer', () => { const buffer = new Uint8Array([0xFF, 0x00]) - const list = new StatusList({ buffer }) - expect(list).toBeInstanceOf(StatusList) - }) - }) - - describe('addEntry', () => { - it('should add entry with default status size', () => { - statusList.addEntry(0) - expect(statusList.getStatus(0)).toBe(0) + const list = new BitstreamStatusList({buffer}) + expect(list).toBeInstanceOf(BitstreamStatusList) }) - it('should add entry with custom status size', () => { - statusList.addEntry(0, 4) - expect(statusList.getStatus(0)).toBe(0) + 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', () => { - statusList.addEntry(0) expect(statusList.getStatus(0)).toBe(0) }) it('should return set status', () => { - statusList.addEntry(0) statusList.setStatus(0, 1) expect(statusList.getStatus(0)).toBe(1) }) @@ -53,15 +46,23 @@ describe('StatusList', () => { describe('setStatus', () => { it('should set status correctly', () => { - statusList.addEntry(0) statusList.setStatus(0, 1) expect(statusList.getStatus(0)).toBe(1) }) it('should set multi-bit status', () => { - statusList.addEntry(0, 4) - statusList.setStatus(0, 12) - expect(statusList.getStatus(0)).toBe(12) + 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) }) }) @@ -72,9 +73,8 @@ describe('StatusList', () => { }) it('should encode list with entries', async () => { - statusList.addEntry(0) - statusList.addEntry(1) statusList.setStatus(0, 1) + statusList.setStatus(1, 1) const encoded = await statusList.encode() expect(encoded).toMatch(/^u/) expect(encoded.length).toBeGreaterThan(1) @@ -82,86 +82,118 @@ describe('StatusList', () => { }) describe('decode', () => { - it('should decode encoded list', async () => { - statusList.addEntry(0) - statusList.addEntry(1) + it('should decode encoded list into a StatusList instance', async () => { statusList.setStatus(0, 1) + statusList.setStatus(1, 0) const encoded = await statusList.encode() - const { buffer } = await StatusList.decode({ encodedList: encoded }) + const decoded = await BitstreamStatusList.decode({encodedList: encoded, statusSize: 1}) - expect(buffer).toBeInstanceOf(Uint8Array) - expect(buffer.length).toBeGreaterThan(0) - }) + expect(decoded).toBeInstanceOf(BitstreamStatusList) + expect(decoded.getStatusSize()).toBe(1) - it('should reject invalid prefix', async () => { - await expect(StatusList.decode({ encodedList: 'invalid' })) - .rejects.toThrow('encodedList must start with "u" prefix') + expect(decoded.getStatus(0)).toBe(1) + expect(decoded.getStatus(1)).toBe(0) }) - it('should reject non-string input', async () => { - await expect(StatusList.decode({ encodedList: null as any })) - .rejects.toThrow() - }) + it('should return correct length for decoded instance', async () => { + const list = new BitstreamStatusList({statusSize: 2}) + list.setStatus(0, 1) + list.setStatus(100, 2) - it('should handle round-trip encoding/decoding', async () => { - statusList.addEntry(0, 4) - statusList.addEntry(1, 2) - statusList.setStatus(0, 10) - statusList.setStatus(1, 3) + const encoded = await list.encode() + const decoded = await BitstreamStatusList.decode({encodedList: encoded, statusSize: 2}) - const encoded = await statusList.encode() - const { buffer } = await StatusList.decode({ encodedList: encoded }) + const length = decoded.getLength() + expect(length).toBe(65536) // (16384 * 8) / 2 + }) - const decodedList = new StatusList({ buffer }) - decodedList.addEntry(0, 4) - decodedList.addEntry(1, 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) - expect(decodedList.getStatus(0)).toBe(10) - expect(decodedList.getStatus(1)).toBe(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) }) - }) -}) -describe('StatusList W3C Compliance', () => { - let statusList: StatusList + it('should preserve statusSize when decoding', async () => { + const list = new BitstreamStatusList({statusSize: 8}) + list.setStatus(0, 255) - beforeEach(() => { - statusList = new StatusList() + 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('minimum 16KB requirement', () => { - it('should pad to minimum 16KB on encode', async () => { - statusList.addEntry(0) - statusList.setStatus(0, 1) + 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 statusList.encode() - const { buffer } = await StatusList.decode({ encodedList: encoded }) + const encoded = await list.encode() + const length = BitstreamStatusList.getStatusListLength(encoded, 2) - expect(buffer.length).toBe(16384) // 16KB + // With 16KB minimum and 2-bit statusSize: (16384 * 8) / 2 = 65536 entries + expect(length).toBe(65536) }) - it('should reject lists shorter than 16KB on decode', async () => { - // Create a short buffer and manually compress/encode it - const shortBuffer = new Uint8Array(1024) // 1KB - const compressed = await import('pako').then(pako => pako.gzip(shortBuffer)) - const encoded = `u${await import('../src/utils/base64').then(b64 => b64.bytesToBase64url(compressed))}` + it('should handle different statusSizes', async () => { + const list = new BitstreamStatusList({statusSize: 4}) + const encoded = await list.encode() - await expect(StatusList.decode({ encodedList: encoded })) - .rejects.toThrow('Status list must be at least 16384 bytes (16KB)') + const length = BitstreamStatusList.getStatusListLength(encoded, 4) + // With 16KB minimum and 4-bit statusSize: (16384 * 8) / 4 = 32768 entries + expect(length).toBe(32768) }) + }) - it('should handle large buffers correctly', async () => { - const list = new StatusList() - // Add enough entries to exceed 16KB naturally - for (let i = 0; i < 20000; i++) { - list.addEntry(i) - } - - const encoded = await list.encode() - const { buffer } = await StatusList.decode({ encodedList: encoded }) - - expect(buffer.length).toBeGreaterThanOrEqual(16384) + 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 index 9ad1984..7a4b295 100644 --- a/__tests__/create.test.ts +++ b/__tests__/create.test.ts @@ -1,11 +1,9 @@ import {describe, expect, it} from 'vitest' -import {createStatusListCredential, StatusList} from '../src' +import {BitstreamStatusList, createStatusListCredential} from '../src' describe('createStatusListCredential', () => { it('should create basic credential', async () => { - const list = new StatusList() const credential = await createStatusListCredential({ - statusList: list, id: 'https://example.com/status/1', issuer: 'https://example.com/issuer', statusPurpose: 'revocation' @@ -29,43 +27,39 @@ describe('createStatusListCredential', () => { }) it('should create credential with issuer object', async () => { - const list = new StatusList() const issuer = {id: 'https://example.com/issuer', name: 'Test Issuer'} const credential = await createStatusListCredential({ - statusList: list, id: 'https://example.com/status/1', issuer, - statusPurpose: 'revocation' + statusPurpose: 'revocation', + statusSize: 2 }) expect(credential.issuer).toEqual(issuer) }) it('should create credential with validity period', async () => { - const list = new StatusList() - const validFrom = '2024-01-01T00:00:00Z' - const validUntil = '2024-12-31T23:59:59Z' + const validFrom = new Date('2024-01-01T00:00:00Z') + const validUntil = new Date('2024-12-31T23:59:59Z') const credential = await createStatusListCredential({ - statusList: list, id: 'https://example.com/status/1', issuer: 'https://example.com/issuer', statusPurpose: 'revocation', + statusSize: 3, validFrom, validUntil }) - expect(credential.validFrom).toBe(validFrom) - expect(credential.validUntil).toBe(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 list = new StatusList() const statusPurpose = ['revocation', 'suspension'] const credential = await createStatusListCredential({ - statusList: list, id: 'https://example.com/status/1', issuer: 'https://example.com/issuer', statusPurpose @@ -75,11 +69,9 @@ describe('createStatusListCredential', () => { }) it('should create credential with TTL', async () => { - const list = new StatusList() const ttl = 86400000 // 24 hours const credential = await createStatusListCredential({ - statusList: list, id: 'https://example.com/status/1', issuer: 'https://example.com/issuer', statusPurpose: 'revocation', @@ -89,19 +81,54 @@ describe('createStatusListCredential', () => { expect(credential.credentialSubject.ttl).toBe(ttl) }) - it('should create credential with populated status list', async () => { - const list = new StatusList() - list.addEntry(0) - list.addEntry(1) - list.setStatus(0, 1) + 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({ - statusList: list, id: 'https://example.com/status/1', issuer: 'https://example.com/issuer', statusPurpose: 'revocation' }) - expect(credential.credentialSubject.encodedList).toMatch(/^u/) + // 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 index 0fee2af..a92d2ae 100644 --- a/__tests__/index.test.ts +++ b/__tests__/index.test.ts @@ -8,8 +8,8 @@ describe('index exports', () => { }) it('should export StatusList', () => { - expect(index.StatusList).toBeDefined() - expect(typeof index.StatusList).toBe('function') + expect(index.BitstreamStatusList).toBeDefined() + expect(typeof index.BitstreamStatusList).toBe('function') }) it('should export createStatusListCredential', () => { @@ -25,7 +25,7 @@ describe('index exports', () => { it('should have all expected exports', () => { const expectedExports = [ 'BitManager', - 'StatusList', + 'BitstreamStatusList', 'createStatusListCredential', 'checkStatus' ] diff --git a/__tests__/verify.test.ts b/__tests__/verify.test.ts index 1c63254..a0f018d 100644 --- a/__tests__/verify.test.ts +++ b/__tests__/verify.test.ts @@ -1,9 +1,13 @@ -import {describe, expect, it, vi} from 'vitest' -import {BitstringStatusListCredentialUnsigned, checkStatus, CredentialWithStatus, StatusList} from '../src' - +import {describe, expect, it} from 'vitest' +import {checkStatus, BitstreamStatusList} from '../src' +import {BitstringStatusListCredentialUnsigned, BitstringStatusListEntry, CredentialWithStatus} from '../src/types' describe('checkStatus', () => { - const createMockCredential = (statusPurpose: string = 'revocation', statusIndex: string = '0'): CredentialWithStatus => ({ + 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'], @@ -16,15 +20,19 @@ describe('checkStatus', () => { type: 'BitstringStatusListEntry', statusPurpose, statusListIndex: statusIndex, - statusListCredential: 'https://example.com/status/1' - } + statusListCredential: 'https://example.com/status/1', + ...(statusSize && {statusSize}) + } as BitstringStatusListEntry }) - const createMockStatusListCredential = async (statuses: number[] = [0]): Promise => { - const list = new StatusList() - statuses.forEach((status, index) => { - list.addEntry(index) - list.setStatus(index, status) + 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 { @@ -35,7 +43,8 @@ describe('checkStatus', () => { credentialSubject: { id: 'https://example.com/status/1#list', type: 'BitstringStatusList', - statusPurpose: 'revocation', + statusPurpose, + statusSize, encodedList: await list.encode() } } @@ -43,60 +52,51 @@ describe('checkStatus', () => { it('should verify valid credential (status 0)', async () => { const credential = createMockCredential() - const statusListCredential = await createMockStatusListCredential([0]) + const statusListCred = await createStatusListCredential([0]) const result = await checkStatus({ credential, - getStatusListCredential: vi.fn().mockResolvedValue(statusListCredential) + 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 statusListCredential = await createMockStatusListCredential([1]) + const statusListCred = await createStatusListCredential([1]) const result = await checkStatus({ credential, - getStatusListCredential: vi.fn().mockResolvedValue(statusListCredential) + 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: CredentialWithStatus = { - ...createMockCredential(), - credentialStatus: { - type: 'BitstringStatusListEntry', - statusPurpose: 'revocation', - statusListIndex: '0', - statusListCredential: 'https://example.com/status/1', - statusMessage: [ - {id: '0x0', message: 'Valid'}, - {id: '0x1', message: 'Revoked'} - ] - } - } - - const statusListCredential = await createMockStatusListCredential([1]) + 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: vi.fn().mockResolvedValue(statusListCredential) + 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: CredentialWithStatus = { + const credential = { ...createMockCredential(), credentialStatus: [ { @@ -105,41 +105,36 @@ describe('checkStatus', () => { statusListIndex: '0', statusListCredential: 'https://example.com/other' }, - { - type: 'BitstringStatusListEntry', - statusPurpose: 'revocation', - statusListIndex: '0', - statusListCredential: 'https://example.com/status/1' - } + createMockCredential().credentialStatus ] - } + } as CredentialWithStatus - const statusListCredential = await createMockStatusListCredential([0]) + const statusListCred = await createStatusListCredential([0]) const result = await checkStatus({ credential, - getStatusListCredential: vi.fn().mockResolvedValue(statusListCredential) + getStatusListCredential: async () => statusListCred }) - expect(result.verified).toBe(true) expect(result.status).toBe(0) }) it('should reject credential without BitstringStatusListEntry', async () => { - const credential: CredentialWithStatus = { + const credential = { ...createMockCredential(), credentialStatus: { statusPurpose: 'revocation', statusListIndex: '0', statusListCredential: 'https://example.com/status/1' } as any - } + } as CredentialWithStatus const result = await checkStatus({ credential, - getStatusListCredential: vi.fn() + 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') @@ -147,16 +142,13 @@ describe('checkStatus', () => { it('should reject expired status list credential', async () => { const credential = createMockCredential() - const statusListCredential = { - ...await createMockStatusListCredential([0]), - validUntil: new Date(Date.now() - 86400000).toISOString() // 1 day ago - } + const base = await createStatusListCredential([0]) + const expired = {...base, validUntil: new Date(Date.now() - 86400000).toISOString()} const result = await checkStatus({ credential, - getStatusListCredential: vi.fn().mockResolvedValue(statusListCredential) + getStatusListCredential: async () => expired }) - expect(result.verified).toBe(false) expect(result.status).toBe(-1) expect(result.error?.message).toBe('Status list credential has expired') @@ -164,16 +156,13 @@ describe('checkStatus', () => { it('should reject not-yet-valid status list credential', async () => { const credential = createMockCredential() - const statusListCredential = { - ...await createMockStatusListCredential([0]), - validFrom: new Date(Date.now() + 86400000).toISOString() // 1 day in future - } + const base = await createStatusListCredential([0]) + const future = {...base, validFrom: new Date(Date.now() + 86400000).toISOString()} const result = await checkStatus({ credential, - getStatusListCredential: vi.fn().mockResolvedValue(statusListCredential) + 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') @@ -181,27 +170,26 @@ describe('checkStatus', () => { it('should handle index out of bounds', async () => { const credential = createMockCredential('revocation', '132000') - const statusListCredential = await createMockStatusListCredential([0]) + const statusListCred = await createStatusListCredential([0]) const result = await checkStatus({ credential, - getStatusListCredential: vi.fn().mockResolvedValue(statusListCredential) + getStatusListCredential: async () => statusListCred }) - expect(result.verified).toBe(false) expect(result.status).toBe(-1) - expect(result.error?.message).toContain('exceeds buffer bounds') + expect(result.error?.message).toContain('credentialIndex 132000 exceeds buffer bounds') }) it('should handle getStatusListCredential rejection', async () => { const credential = createMockCredential() - const getStatusListCredential = vi.fn().mockRejectedValue(new Error('Network error')) const result = await checkStatus({ credential, - getStatusListCredential + getStatusListCredential: async () => { + throw new Error('Network error') + } }) - expect(result.verified).toBe(false) expect(result.status).toBe(-1) expect(result.error?.message).toBe('Network error') @@ -210,55 +198,60 @@ describe('checkStatus', () => { it('should handle invalid credential parameter', async () => { const result = await checkStatus({ credential: null as any, - getStatusListCredential: vi.fn() + 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', '0') // Changed to 'suspension' - - // Status list credential has 'revocation' purpose - const list = new StatusList() - list.addEntry(0) - list.setStatus(0, 0) - - const statusListCredential: BitstringStatusListCredentialUnsigned = { - '@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', // Different from credential's 'suspension' - encodedList: await list.encode() - } - } + const credential = createMockCredential('suspension') + const statusListCred = await createStatusListCredential([0], 1, 'revocation') const result = await checkStatus({ credential, - getStatusListCredential: vi.fn().mockResolvedValue(statusListCredential) + 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) + }) + }) - const list = new StatusList() - list.addEntry(0) - list.setStatus(0, 0) + 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 statusListCredential: BitstringStatusListCredentialUnsigned = { + const statusListCred = { '@context': ['https://www.w3.org/ns/credentials/v2'], id: 'https://example.com/status/1', type: ['VerifiableCredential', 'BitstringStatusListCredential'], @@ -266,45 +259,62 @@ describe('checkStatus', () => { credentialSubject: { id: 'https://example.com/status/1#list', type: 'BitstringStatusList', - statusPurpose: ['revocation', 'suspension'], // Array containing matching purpose - encodedList: await list.encode() + statusPurpose: 'revocation', + statusSize: 1, + encodedList: await smallList.encode() } } const result = await checkStatus({ credential, - getStatusListCredential: vi.fn().mockResolvedValue(statusListCredential) + getStatusListCredential: async () => statusListCred as BitstringStatusListCredentialUnsigned }) expect(result.verified).toBe(true) + expect(result.status).toBe(0) }) - }) - describe('minimum bitstring length validation', () => { - it('should reject status lists that are too short', async () => { + it('should accept lists meeting minimum length requirement', async () => { const credential = createMockCredential('revocation', '0') + const statusListCred = await createStatusListCredential([0]) + - // Mock StatusList.decode to return short buffer directly - const mockDecode = vi.spyOn(StatusList, 'decode').mockImplementation(async () => { - return { buffer: new Uint8Array(1024) } // 1KB - less than required 16KB + 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: vi.fn().mockResolvedValue({ - credentialSubject: { - statusPurpose: 'revocation', - encodedList: 'mock-encoded-list' - } - } as any) + getStatusListCredential: async () => statusListCred }) expect(result.verified).toBe(false) - expect(result.error?.message).toContain('Status list length error') + expect(result.status).toBe(-1) + }) + + it('should handle default statusSize', async () => { + const credential = createMockCredential('revocation', '0') + const statusListCred = await createStatusListCredential([1]) - mockDecode.mockRestore() + 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/src/bit-manager/BitManager.ts b/src/bit-manager/BitManager.ts index 8483a8e..10e2d6b 100644 --- a/src/bit-manager/BitManager.ts +++ b/src/bit-manager/BitManager.ts @@ -1,61 +1,151 @@ -import {assertIsNonNegativeInteger, assertIsPositiveInteger} from "../utils/assertions"; +import {assertIsNonNegativeInteger, assertIsPositiveInteger} from "../utils/assertions" const EXPAND_BLOCK_SIZE = 16384 // 16KB -interface BitEntry { - credentialIndex: number - statusSize: number - startBit: number -} - +/** + * 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 entries: Map = new Map() private bits: Uint8Array - private nextBitPosition: number = 0 - - constructor(options: { buffer?: Uint8Array; initialSize?: number } = {}) { - if (options.buffer) { - this.bits = new Uint8Array(options.buffer) - } else { - // Default to W3C minimum size (16KB) - this.bits = new Uint8Array(options.initialSize || 16384) + private readonly statusSize: number + + /** + * Creates a new BitManager instance + * + * @param options.statusSize - Bits per credential status (default: 1) + * @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 = 16384} = options + + assertIsPositiveInteger(statusSize, 'statusSize') + 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 TypeError(`credentialIndex ${credentialIndex} exceeds buffer bounds`) } + + const startBit = credentialIndex * this.statusSize + return this.readStatusBits(startBit) } - addEntry(credentialIndex: number, statusSize: number = 1): void { + + /** + * Sets the status value for a credential + * + * @param credentialIndex - Non-negative credential identifier + * @param status - Status value (0 to 2^statusSize - 1) + * @throws {TypeError} If status exceeds maximum value for statusSize + */ + setStatus(credentialIndex: number, status: number): void { assertIsNonNegativeInteger(credentialIndex, 'credentialIndex') - assertIsPositiveInteger(statusSize, 'statusSize') + assertIsNonNegativeInteger(status, 'status') - if (this.entries.has(credentialIndex)) { - throw new TypeError(`Entry for credentialIndex ${credentialIndex} already exists`) + const maxValue = (1 << this.statusSize) - 1 + if (status > maxValue) { + throw new TypeError(`Status ${status} exceeds maximum value ${maxValue} for ${this.statusSize} bits`) } - const entry: BitEntry = { - credentialIndex, - statusSize, - startBit: this.nextBitPosition + 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 { + // Find highest credential index that's been set + let maxCredentialIndex = -1 + for (let i = 0; i < this.bits.length * 8; i += this.statusSize) { + const credentialIndex = i / this.statusSize + if (this.readStatusBits(i) !== 0) { + maxCredentialIndex = credentialIndex + } } - this.entries.set(credentialIndex, entry) - this.nextBitPosition += statusSize + if (maxCredentialIndex === -1) { + return new Uint8Array([]) + } - // Expand buffer if needed - this.ensureBufferSize() + const requiredBits = (maxCredentialIndex + 1) * this.statusSize + const requiredBytes = Math.ceil(requiredBits / 8) + return this.bits.slice(0, requiredBytes) } - getStatus(credentialIndex: number): number { - const entry = this.entries.get(credentialIndex) - if (!entry) { - throw new TypeError(`No entry found for credentialIndex ${credentialIndex}`) - } - const {startBit, statusSize} = entry + /** + * 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 < statusSize; i++) { + 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 status |= bit << i } @@ -63,25 +153,20 @@ export class BitManager { return status } - setStatus(credentialIndex: number, status: number): void { - const entry = this.entries.get(credentialIndex) - if (!entry) { - throw new TypeError(`No entry found for credentialIndex ${credentialIndex}`) - } - - const {startBit, statusSize} = entry - const maxValue = (1 << statusSize) - 1 - - assertIsNonNegativeInteger(status, 'status') - if (status > maxValue) { - throw new TypeError(`Status ${status} exceeds maximum value ${maxValue} for ${statusSize} bits`) - } - - for (let i = 0; i < statusSize; i++) { + /** + * 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) + const bit = (status >> i) & 1 if (bit === 1) { @@ -92,10 +177,13 @@ export class BitManager { } } - private ensureBufferSize(): void { - const requiredBytes = Math.ceil(this.nextBitPosition / 8) + /** + * Ensures buffer can hold the specified number of bytes + * + * @param requiredBytes - Minimum bytes needed + */ + private ensureBufferCanHold(requiredBytes: number): void { if (requiredBytes > this.bits.length) { - // Expand in 16KB blocks to maintain W3C compliance and efficiency const blocksNeeded = Math.ceil(requiredBytes / EXPAND_BLOCK_SIZE) const newSize = blocksNeeded * EXPAND_BLOCK_SIZE @@ -104,12 +192,4 @@ export class BitManager { this.bits = newBuffer } } - toBuffer(): Uint8Array { - const requiredBytes = Math.ceil(this.nextBitPosition / 8) - return this.bits.slice(0, requiredBytes) - } - - getEntries(): Map { - return new Map(this.entries) - } -} \ No newline at end of file +} diff --git a/src/credential/create.ts b/src/credential/create.ts index 789c40d..ea5a848 100644 --- a/src/credential/create.ts +++ b/src/credential/create.ts @@ -1,36 +1,88 @@ /** - * High-level functions for creating status list credentials + * 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 {StatusList} from '../status-list/StatusList' +import {BitstreamStatusList} from '../status-list/BitstreamStatusList' import {BitstringStatusListCredentialSubject, BitstringStatusListCredentialUnsigned, IIssuer} from '../types' +import * as console from "node:console"; +/** + * 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 ,validFrom, validUntil, statusPurpose, ttl} = options + const { + id, + issuer, + statusSize = 1, + statusList, + statusPurpose, + validFrom, + validUntil, + ttl + } = options - const encodedList = await new StatusList({statusSize}).encode() + // Create empty status list with specified statusSize + const encodedList = await (statusList ?? new BitstreamStatusList({statusSize})).encode() - const credentialSubject = { + // Build credential subject according to W3C spec + const credentialSubject: BitstringStatusListCredentialSubject = { id: `${id}#list`, type: 'BitstringStatusList', statusPurpose, - statusSize: statusSize ?? 1, encodedList, ...(ttl && {ttl}) - } satisfies BitstringStatusListCredentialSubject - - return { + } +console.log('credentialSubject', JSON.stringify(credentialSubject)) + // Build the complete credential with W3C contexts + const credential: BitstringStatusListCredentialUnsigned = { '@context': [ - 'https://www.w3.org/ns/credentials/v2', - 'https://www.w3.org/ns/credentials/status/v1' + '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'], @@ -39,4 +91,6 @@ export async function createStatusListCredential(options: { ...(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 index b1adde5..5240259 100644 --- a/src/credential/verify.ts +++ b/src/credential/verify.ts @@ -1,94 +1,97 @@ /** - * Verifying credential status logic + * 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 {StatusList} from '../status-list/StatusList' +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 (handle both single and array) - const statusEntries = Array.isArray(credential.credentialStatus) - ? credential.credentialStatus - : [credential.credentialStatus] - - const entry = statusEntries.find(e => e.type === 'BitstringStatusListEntry') as BitstringStatusListEntry + // Extract BitstringStatusListEntry from credential status + const entry = extractBitstringStatusEntry(credential.credentialStatus) if (!entry) { - throw Error('No BitstringStatusListEntry found in credentialStatus') + throw new Error('No BitstringStatusListEntry found in credentialStatus') } - // Get the status list credential (assumed to be verified) + // Retrieve the status list credential const listCredential = await getStatusListCredential(entry.statusListCredential) - // Check validity period - 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') - } - - // Validate statusPurpose matches (W3C spec requirement) - const listStatusPurpose = listCredential.credentialSubject.statusPurpose - const purposes = Array.isArray(listStatusPurpose) ? listStatusPurpose : [listStatusPurpose] + // Validate temporal constraints + validateTemporalValidity(listCredential) - if (!purposes.includes(entry.statusPurpose)) { - throw new Error(`Status purpose '${entry.statusPurpose}' does not match any purpose in status list credential: ${purposes.join(', ')}`) - } + // Validate status purpose matching (W3C requirement) + validateStatusPurposeAndSize(entry, listCredential) - // Decode the status list and get status - const {buffer} = await StatusList.decode({ - encodedList: listCredential.credentialSubject.encodedList + // Decode status list and validate minimum size + const statusSize = entry.statusSize || 1 + const statusList = await BitstreamStatusList.decode({ + encodedList: listCredential.credentialSubject.encodedList, + statusSize }) - // Verify minimum bitstring length (W3C spec requirement) - const statusSize = entry.statusSize || 1 - const totalBits = buffer.length * 8 - const minEntries = 131072 / statusSize // 16KB in bits divided by statusSize - if (totalBits / statusSize < minEntries) { - throw new Error(`Status list length error: bitstring must support at least ${minEntries} entries for statusSize ${statusSize}`) - } + // Verify W3C minimum bitstring length requirement + validateMinimumBitstringLength(listCredential.credentialSubject.encodedList, statusSize) + // Retrieve the actual status value const statusIndex = parseInt(entry.statusListIndex, 10) - const startBit = statusIndex * statusSize + const status = statusList.getStatus(statusIndex) - let status = 0 - for (let i = 0; i < statusSize; i++) { - const bitIndex = startBit + i - const byteIndex = Math.floor(bitIndex / 8) - const bitOffset = bitIndex % 8 - - if (byteIndex >= buffer.length) { - throw Error(`Index ${statusIndex} with statusSize ${statusSize} exceeds buffer bounds`) - } - - const bit = (buffer[byteIndex] >> (7 - bitOffset)) & 1 - status |= bit << i - } - - // Find corresponding status message - const statusHex = `0x${status.toString(16)}` - let statusMessage: StatusMessage | undefined - if (entry.statusMessage) { - statusMessage = entry.statusMessage.find(msg => msg.id === statusHex) - } + // Find corresponding status message if available + const statusMessage = findStatusMessage(entry, status) // Determine verification result based on status purpose - let verified = true - if (entry.statusPurpose === 'revocation' || entry.statusPurpose === 'suspension') { - verified = status === 0 // 0 means not revoked/suspended - } + const verified = determineVerificationResult(entry.statusPurpose, status) return { verified, @@ -104,3 +107,104 @@ export async function checkStatus(options: CheckStatusOptions): Promise 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 index 6a0242c..6f0e93d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,6 @@ // Core Classes export {BitManager} from './bit-manager/BitManager' -export {StatusList} from './status-list/StatusList' +export {BitstreamStatusList} from './status-list/BitstreamStatusList' // High-Level Functions export {createStatusListCredential} from './credential/create' diff --git a/src/status-list/BitstreamStatusList.ts b/src/status-list/BitstreamStatusList.ts new file mode 100644 index 0000000..78062cc --- /dev/null +++ b/src/status-list/BitstreamStatusList.ts @@ -0,0 +1,196 @@ +/** + * 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' + +/** W3C specification minimum bitstring size */ +const MIN_BITSTRING_SIZE_BYTES = 16384 // 16KB (131,072 bits) + +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 {TypeError} If format is invalid or size requirements not met + */ + static async decode(options: { + encodedList: string + statusSize?: number + }): Promise { + const {encodedList, statusSize = 1} = options + + assertIsString(encodedList, 'encodedList') + assertIsPositiveInteger(statusSize, 'statusSize') + + if (!encodedList.startsWith('u')) { + throw new TypeError('encodedList must start with "u" prefix') + } + + // Decode multibase and decompress + const compressed = base64urlToBytes(encodedList.slice(1)) + const buffer = pako.ungzip(compressed) + + // Enforce W3C minimum size requirement + if (buffer.length < MIN_BITSTRING_SIZE_BYTES) { + throw new TypeError( + `Status list must be at least ${MIN_BITSTRING_SIZE_BYTES} bytes (16KB), got ${buffer.length}` + ) + } + + 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 {TypeError} If format is invalid + */ + static getStatusListLength(encodedList: string, statusSize: number): number { + assertIsString(encodedList, 'encodedList') + assertIsPositiveInteger(statusSize, 'statusSize') + + if (!encodedList.startsWith('u')) { + throw new TypeError('encodedList must start with "u" prefix') + } + + const compressed = base64urlToBytes(encodedList.slice(1)) + + if (compressed.length < 4) { + throw new TypeError('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 + } + + const paddedBuffer = new Uint8Array(MIN_BITSTRING_SIZE_BYTES) + paddedBuffer.set(buffer) + return paddedBuffer + } +} \ No newline at end of file diff --git a/src/status-list/StatusList.ts b/src/status-list/StatusList.ts deleted file mode 100644 index 4d200b8..0000000 --- a/src/status-list/StatusList.ts +++ /dev/null @@ -1,103 +0,0 @@ -/** - * Status list encoding/decoding wrapper using BitManager - * Handles gzip compression and base64url encoding as per W3C spec - */ - -import {BitManager} from '../bit-manager/BitManager' -import {base64urlToBytes, bytesToBase64url} from '../utils/base64' -import {assertIsPositiveInteger, assertIsString} from '../utils/assertions' -import pako from 'pako' - -// W3C spec requires minimum 16KB (131,072 bits) -const MIN_BITSTRING_SIZE_BYTES = 16384 // 16KB - -export class StatusList { - private bitManager: BitManager - private readonly statusSize: number - - /** - * @param options.buffer existing bitstring buffer (uncompressed form) - * @param options.initialSize initial byte-length to allocate - * @param options.statusSize uniform bit-width for every entry (default: 1) - */ - constructor(options: { buffer?: Uint8Array; statusSize?: number } = {}) { - const {buffer, statusSize = 1} = options - assertIsPositiveInteger(statusSize, 'statusSize') - - this.statusSize = statusSize - this.bitManager = new BitManager({buffer}) - } - - /** Always uses the list’s uniform statusSize */ - addEntry(credentialIndex: number): void { - this.bitManager.addEntry(credentialIndex, this.statusSize) - } - - getStatus(credentialIndex: number): number { - return this.bitManager.getStatus(credentialIndex) - } - - setStatus(credentialIndex: number, status: number): void { - this.bitManager.setStatus(credentialIndex, status) - } - - /** @returns the list-level bit-width */ - getStatusSize(): number { - return this.statusSize - } - - async encode(): Promise { - const buffer = this.bitManager.toBuffer() - const padded = buffer.length >= MIN_BITSTRING_SIZE_BYTES - ? buffer - : Uint8Array.from({length: MIN_BITSTRING_SIZE_BYTES}, (_, i) => buffer[i] ?? 0) - - const compressed = pako.gzip(padded) - return `u${bytesToBase64url(compressed)}` - } - - static async decode(options: { encodedList: string }): Promise<{ buffer: Uint8Array }> { - const {encodedList} = options - assertIsString(encodedList, 'encodedList') - if (!encodedList.startsWith('u')) { - throw new TypeError('encodedList must start with "u" prefix') - } - - const compressed = base64urlToBytes(encodedList.slice(1)) - const buffer = pako.ungzip(compressed) - - if (buffer.length < MIN_BITSTRING_SIZE_BYTES) { - throw new TypeError( - `Status list must be at least ${MIN_BITSTRING_SIZE_BYTES} bytes (16KB), got ${buffer.length}` - ) - } - - return {buffer} - } - - /** - * Compute how many list entries are present by reading the gzip ISIZE - * (uncompressed length) and dividing by the uniform statusSize. - * @param encodedList u-prefixed, gzip-compressed base64url string - * @param statusSize uniform bit-width used when encoding - */ - static getStatusListLength(encodedList: string, statusSize: number): number { - assertIsString(encodedList, 'encodedList') - assertIsPositiveInteger(statusSize, 'statusSize') - if (!encodedList.startsWith('u')) { - throw new TypeError('encodedList must start with "u" prefix') - } - - const data = base64urlToBytes(encodedList.slice(1)) - if (data.length < 4) { - throw new TypeError('Invalid gzip data: too short to contain ISIZE') - } - const offset = data.byteOffset + data.length - 4 - const view = new DataView(data.buffer, offset, 4) - const uncompressedBytes = view.getUint32(0, true) - - // total bits = uncompressedBytes * 8 - // number of entries = totalBits / statusSize - return Math.floor((uncompressedBytes * 8) / statusSize) - } -} diff --git a/src/types.ts b/src/types.ts index 5ac9b20..8cb7362 100644 --- a/src/types.ts +++ b/src/types.ts @@ -19,7 +19,6 @@ 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 - statusSize: number // The statusSize indicates the size of the status entry in bits encodedList: string // The u-prefixed, compressed, base64url-encoded string ttl?: number // Optional time to live in milliseconds } From 52c14d2ddfdb187dc25ed2ec9c1ae25dfb276e5c Mon Sep 17 00:00:00 2001 From: sanderPostma Date: Thu, 10 Jul 2025 13:00:06 +0200 Subject: [PATCH 09/31] chore: minimal Github CI --- .github/workflows/release.yml | 37 +++++++++++++++++++++++++++++++++++ package.json | 3 ++- 2 files changed, 39 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..95ac320 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,37 @@ +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 + with: + version: 10.8.1 + - 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/package.json b/package.json index 7d8e13a..167db3a 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,8 @@ } }, "scripts": { - "build": "tsup" + "build": "tsup", + "test:ci": "vitest" }, "keywords": [ "verifiable-credentials", From e03ce654ae728b9d26cd3c92ee0fb9c3d4c4be89 Mon Sep 17 00:00:00 2001 From: sanderPostma Date: Thu, 10 Jul 2025 13:02:19 +0200 Subject: [PATCH 10/31] chore: Github CI use pnpm version from package.json --- .github/workflows/release.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 95ac320..420024a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -12,8 +12,6 @@ jobs: with: fetch-depth: 0 - uses: pnpm/action-setup@v4 - with: - version: 10.8.1 - name: Use Node.js uses: actions/setup-node@v4 with: From ec04d35b70b3f7c23495bbced1763df241bbc46a Mon Sep 17 00:00:00 2001 From: sanderPostma Date: Thu, 10 Jul 2025 13:11:24 +0200 Subject: [PATCH 11/31] chore: set access public --- package.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/package.json b/package.json index 167db3a..ee36fc4 100644 --- a/package.json +++ b/package.json @@ -58,5 +58,8 @@ "vite": "^7.0.3", "vite-tsconfig-paths": "^5.1.4", "vitest": "^3.2.4" + }, + "publishConfig": { + "access": "public" } } From 8790b8de7fc411554c1d79106d8123201b59df6d Mon Sep 17 00:00:00 2001 From: sanderPostma Date: Fri, 11 Jul 2025 09:53:18 +0200 Subject: [PATCH 12/31] chore: README update --- README.md | 107 +++++++++++++++++++++++++++++++++--------------------- 1 file changed, 65 insertions(+), 42 deletions(-) diff --git a/README.md b/README.md index 4405625..1835a9e 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# vc-bitstring-status-lists# vc-bitstring-status-lists +# 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. @@ -12,16 +12,16 @@ Here's how it works conceptually: imagine you have 100,000 credentials. Rather t This library provides a complete implementation of the W3C specification with the following capabilities: -- **Efficient bit manipulation** through the `BitManager` class, which handles the low-level operations of setting and getting status bits +- **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 16KB bitstrings -- **Multiple status support** beyond simple revocation/suspension, including custom status messages and multi-bit status values +- **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 vc-bitstring-status-lists (or npm / yarn) +pnpm install vc-bitstring-status-lists # or npm / yarn ``` ## Quick Start @@ -31,35 +31,30 @@ Let's walk through creating and using a status list step by step: ### 1. Creating a Status List ```typescript -import { StatusList, createStatusListCredential } from 'vc-bitstring-status-lists' +import { BitstreamStatusList, createStatusListCredential } from 'vc-bitstring-status-lists' -// Create a new status list -const statusList = new StatusList() +// Create a new status list with 1-bit status values (0 = valid, 1 = revoked) +const statusList = new BitstreamStatusList({ statusSize: 1 }) -// Add credentials to track (each gets assigned a sequential index) -statusList.addEntry(0) // First credential at index 0 -statusList.addEntry(1) // Second credential at index 1 -statusList.addEntry(2) // Third credential at index 2 - -// Set some statuses (0 = valid, 1 = revoked for 'revocation' purpose) -statusList.setStatus(0, 0) // Valid -statusList.setStatus(1, 1) // Revoked -statusList.setStatus(2, 0) // Valid +// 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 ``` -> ℹ️ - All new entries have status `0` – Valid by default +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 ```typescript // Create a verifiable credential containing the status list const statusListCredential = await createStatusListCredential({ - list: statusList, id: 'https://example.com/status-lists/1', issuer: 'https://example.com/issuer', statusPurpose: 'revocation', - validFrom: '2024-01-01T00:00:00Z', - validUntil: '2024-12-31T23:59:59Z' + statusList: statusList, // Pass your configured status list + validFrom: new Date('2024-01-01'), + validUntil: new Date('2024-12-31') }) // The credential now contains a compressed, encoded bitstring @@ -86,7 +81,7 @@ const credential = { credentialStatus: { type: 'BitstringStatusListEntry', statusPurpose: 'revocation', - statusListIndex: '1', // This credential is at index 1 + statusListIndex: '1', // This credential is at index 1 in the status list statusListCredential: 'https://example.com/status-lists/1' } } @@ -111,18 +106,19 @@ console.log(result) The specification supports more than just binary states. You can use multiple bits per credential to represent complex status information: ```typescript -const statusList = new StatusList() - -// Add credential with 4 bits of status information (supports values 0-15) -statusList.addEntry(0, 4) +// Create a status list with 4 bits per credential (supports values 0-15) +const statusList = new BitstreamStatusList({ statusSize: 4 }) -// Set a complex status +// Set complex status values statusList.setStatus(0, 12) // Binary: 1100, could represent multiple flags +statusList.setStatus(1, 3) // Binary: 0011, different status combination -// Get the status +// Retrieve the status const status = statusList.getStatus(0) // Returns: 12 ``` +This approach is particularly useful when you need to track multiple aspects of a credential's status simultaneously, such as revocation status, verification level, and processing state. + ### Status Messages You can provide human-readable messages for different status values: @@ -157,17 +153,40 @@ const statusListCredential = await createStatusListCredential({ }) ``` +### Working with Existing Status Lists + +You can decode and work with existing status lists: + +```typescript +// Decode a status list from an encoded string +const existingStatusList = await BitstreamStatusList.decode({ + encodedList: 'u...', // The encoded bitstring from a credential + statusSize: 1 +}) + +// Check or modify statuses +console.log(existingStatusList.getStatus(42)) // Get status of credential 42 +existingStatusList.setStatus(100, 1) // Revoke credential 100 + +// Re-encode for publishing +const updatedEncoded = await existingStatusList.encode() +``` + ## Understanding the Architecture -The library is built around several key components that work together: +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. -### StatusList Class +The key insight is that it calculates bit positions mathematically: credential index 42 with a 2-bit status size would occupy 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 16KB minimum size requirement. -The `StatusList` 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 16KB minimum size requirement. +This class handles the complex process of GZIP compression and multibase encoding that the W3C specification requires, while providing a simple interface for credential status management. ### Verification Functions @@ -175,30 +194,33 @@ The `checkStatus` function implements the complete verification algorithm, inclu ## W3C Compliance -This library implements all requirements from the W3C Bitstring Status List v1.0 specification: +This library implements all requirements from [the W3C Bitstring Status List v1.0 specification.](https://www.w3.org/TR/vc-bitstring-status-list): - **Minimum bitstring size**: All encoded status lists are padded to at least 16KB (131,072 bits) - **Compression**: Uses gzip compression as required by the specification - **Base64url encoding**: Proper encoding with the required "u" prefix - **Status purpose validation**: Ensures that credential entries match the status list's declared purposes - **Temporal validation**: Checks `validFrom` and `validUntil` dates on status list credentials +- **Uniform status size**: All credentials in a status list use the same number of bits for their status ## API Reference ### Core Classes -#### `StatusList` +#### `BitstreamStatusList` The main class for creating and managing status lists. ```typescript -class StatusList { - constructor(options?: { buffer?: Uint8Array; initialSize?: number }) - addEntry(credentialIndex: number, statusSize?: number): void +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 }): Promise<{ buffer: Uint8Array }> + static decode(options: { encodedList: string; statusSize?: number }): Promise + static getStatusListLength(encodedList: string, statusSize: number): number } ``` @@ -208,12 +230,12 @@ Low-level bit manipulation class (typically used internally). ```typescript class BitManager { - constructor(options: { buffer?: Uint8Array; initialSize?: number }) - addEntry(credentialIndex: number, statusSize?: number): void + constructor(options: { statusSize?: number; buffer?: Uint8Array; initialSize?: number }) getStatus(credentialIndex: number): number setStatus(credentialIndex: number, status: number): void + getStatusSize(): number + getBufferLength(): number toBuffer(): Uint8Array - getEntries(): Map } ``` @@ -225,12 +247,13 @@ Creates a verifiable credential containing a status list. ```typescript function createStatusListCredential(options: { - list: StatusList id: string issuer: string | IIssuer - validFrom?: string - validUntil?: string + statusSize?: number + statusList?: BitstreamStatusList statusPurpose: string | string[] + validFrom?: Date + validUntil?: Date ttl?: number }): Promise ``` From a56adf533194ca5020959635bd35eeb0ca92933e Mon Sep 17 00:00:00 2001 From: sanderPostma Date: Fri, 11 Jul 2025 10:35:59 +0200 Subject: [PATCH 13/31] chore: scrutinized details --- __tests__/BitManager.test.ts | 2 +- src/bit-manager/BitManager.ts | 56 +++++++++++++-------- src/status-list/BitstreamStatusList.ts | 67 ++++++++++++++++++++------ src/status-list/errors.ts | 42 ++++++++++++++++ 4 files changed, 132 insertions(+), 35 deletions(-) create mode 100644 src/status-list/errors.ts diff --git a/__tests__/BitManager.test.ts b/__tests__/BitManager.test.ts index 1c698b0..c8a808a 100644 --- a/__tests__/BitManager.test.ts +++ b/__tests__/BitManager.test.ts @@ -101,7 +101,7 @@ describe('BitManager', () => { const manager = new BitManager({statusSize: 8}) manager.setStatus(0, 0b10101010) const buffer = manager.toBuffer() - expect(buffer[0]).toBe(0b01010101) // LSB-first mapping + expect(buffer[0]).toBe(0b10101010) }) }) }) \ No newline at end of file diff --git a/src/bit-manager/BitManager.ts b/src/bit-manager/BitManager.ts index 10e2d6b..ec4788b 100644 --- a/src/bit-manager/BitManager.ts +++ b/src/bit-manager/BitManager.ts @@ -1,6 +1,7 @@ import {assertIsNonNegativeInteger, assertIsPositiveInteger} from "../utils/assertions" +import {StatusRangeError} from "../status-list/errors"; -const EXPAND_BLOCK_SIZE = 16384 // 16KB +const LIST_BLOCK_SIZE = 16384 // 16KB /** * BitManager - Low-level bitstring manipulation for W3C Bitstring Status Lists @@ -29,7 +30,7 @@ export class BitManager { /** * Creates a new BitManager instance * - * @param options.statusSize - Bits per credential status (default: 1) + * @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) */ @@ -38,9 +39,15 @@ export class BitManager { buffer?: Uint8Array initialSize?: number } = {}) { - const {statusSize = 1, buffer, initialSize = 16384} = options + const {statusSize = 1, buffer, initialSize = LIST_BLOCK_SIZE} = options assertIsPositiveInteger(statusSize, 'statusSize') + + // Guard against JavaScript bitwise operation overflow + 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) } @@ -57,7 +64,7 @@ export class BitManager { // Check if index exceeds reasonable bounds const maxIndex = Math.floor(this.bits.length * 8 / this.statusSize) if (credentialIndex >= maxIndex) { - throw new TypeError(`credentialIndex ${credentialIndex} exceeds buffer bounds`) + throw new StatusRangeError(`credentialIndex ${credentialIndex} exceeds buffer bounds`) } const startBit = credentialIndex * this.statusSize @@ -69,7 +76,7 @@ export class BitManager { * * @param credentialIndex - Non-negative credential identifier * @param status - Status value (0 to 2^statusSize - 1) - * @throws {TypeError} If status exceeds maximum value for statusSize + * @throws {StatusRangeError} If status exceeds maximum value for statusSize */ setStatus(credentialIndex: number, status: number): void { assertIsNonNegativeInteger(credentialIndex, 'credentialIndex') @@ -77,7 +84,7 @@ export class BitManager { const maxValue = (1 << this.statusSize) - 1 if (status > maxValue) { - throw new TypeError(`Status ${status} exceeds maximum value ${maxValue} for ${this.statusSize} bits`) + throw new StatusRangeError(`Status ${status} exceeds maximum value ${maxValue} for ${this.statusSize} bits`) } const startBit = credentialIndex * this.statusSize @@ -90,22 +97,20 @@ export class BitManager { * @returns Copy of buffer containing only written data */ toBuffer(): Uint8Array { - // Find highest credential index that's been set - let maxCredentialIndex = -1 - for (let i = 0; i < this.bits.length * 8; i += this.statusSize) { - const credentialIndex = i / this.statusSize - if (this.readStatusBits(i) !== 0) { - maxCredentialIndex = credentialIndex + // 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 (maxCredentialIndex === -1) { + if (lastNonZeroByte === -1) { return new Uint8Array([]) } - const requiredBits = (maxCredentialIndex + 1) * this.statusSize - const requiredBytes = Math.ceil(requiredBits / 8) - return this.bits.slice(0, requiredBytes) + return this.bits.slice(0, lastNonZeroByte + 1) } @@ -147,7 +152,8 @@ export class BitManager { } const bit = (this.bits[byteIndex] >> (7 - bitOffset)) & 1 - status |= bit << i + // MSB of status value should be leftmost bit + status |= bit << (this.statusSize - i - 1) } return status @@ -167,7 +173,8 @@ export class BitManager { this.ensureBufferCanHold(byteIndex + 1) - const bit = (status >> i) & 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)) @@ -179,13 +186,22 @@ export class BitManager { /** * 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) { - const blocksNeeded = Math.ceil(requiredBytes / EXPAND_BLOCK_SIZE) - const newSize = blocksNeeded * EXPAND_BLOCK_SIZE + // 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) diff --git a/src/status-list/BitstreamStatusList.ts b/src/status-list/BitstreamStatusList.ts index 78062cc..2956774 100644 --- a/src/status-list/BitstreamStatusList.ts +++ b/src/status-list/BitstreamStatusList.ts @@ -27,9 +27,11 @@ 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 @@ -106,7 +108,8 @@ export class BitstreamStatusList { * @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 {TypeError} If format is invalid or size requirements not met + * @throws {MalformedValueError} If format is invalid + * @throws {StatusListLengthError} If size requirements not met */ static async decode(options: { encodedList: string @@ -117,21 +120,47 @@ export class BitstreamStatusList { assertIsString(encodedList, 'encodedList') assertIsPositiveInteger(statusSize, 'statusSize') + // Validate multibase prefix if (!encodedList.startsWith('u')) { - throw new TypeError('encodedList must start with "u" prefix') + throw new MalformedValueError('encodedList must start with lowercase "u" prefix') } - // Decode multibase and decompress - const compressed = base64urlToBytes(encodedList.slice(1)) - const buffer = pako.ungzip(compressed) + // 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 TypeError( + 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}) } @@ -152,20 +181,32 @@ export class BitstreamStatusList { * @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 {TypeError} If format is invalid + * @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 TypeError('encodedList must start with "u" prefix') + throw new MalformedValueError('encodedList must start with lowercase "u" prefix') } - const compressed = base64urlToBytes(encodedList.slice(1)) + // 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 TypeError('Invalid gzip data: too short to contain ISIZE field') + throw new MalformedValueError('Invalid gzip data: too short to contain ISIZE field') } // Read ISIZE (last 4 bytes of gzip data, little-endian) @@ -189,8 +230,6 @@ export class BitstreamStatusList { return buffer } - const paddedBuffer = new Uint8Array(MIN_BITSTRING_SIZE_BYTES) - paddedBuffer.set(buffer) - return paddedBuffer + return Uint8Array.from({length: MIN_BITSTRING_SIZE_BYTES}, (_, i) => buffer[i] || 0) } -} \ No newline at end of file +} diff --git a/src/status-list/errors.ts b/src/status-list/errors.ts new file mode 100644 index 0000000..db420f4 --- /dev/null +++ b/src/status-list/errors.ts @@ -0,0 +1,42 @@ +// errors.ts +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) + } +} From 91b8f55b1a762a63ab19c25a18fdad080a16e7c1 Mon Sep 17 00:00:00 2001 From: sanderPostma Date: Fri, 11 Jul 2025 10:40:28 +0200 Subject: [PATCH 14/31] chore: readme fix --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1835a9e..dbc8b63 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ This library provides a complete implementation of the W3C specification with th ## Installation ```bash -pnpm install vc-bitstring-status-lists # or npm / yarn +pnpm install @4sure-tech/vc-bitstring-status-lists # or npm / yarn ``` ## Quick Start From a53e31694af2551021d769c4bd05da659ac4ff77 Mon Sep 17 00:00:00 2001 From: sanderPostma Date: Fri, 11 Jul 2025 10:52:01 +0200 Subject: [PATCH 15/31] chore: readme fix --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index dbc8b63..fcacc51 100644 --- a/README.md +++ b/README.md @@ -53,8 +53,8 @@ const statusListCredential = await createStatusListCredential({ issuer: 'https://example.com/issuer', statusPurpose: 'revocation', statusList: statusList, // Pass your configured status list - validFrom: new Date('2024-01-01'), - validUntil: new Date('2024-12-31') + validFrom: new Date('2025-07-01'), + validUntil: new Date('2026-07-01') }) // The credential now contains a compressed, encoded bitstring From b5dcc4972a47977881c5682afb9881e56a1dd62e Mon Sep 17 00:00:00 2001 From: sanderPostma Date: Fri, 11 Jul 2025 10:53:39 +0200 Subject: [PATCH 16/31] chore: removed debug line --- src/credential/create.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/credential/create.ts b/src/credential/create.ts index ea5a848..1a9c843 100644 --- a/src/credential/create.ts +++ b/src/credential/create.ts @@ -77,7 +77,7 @@ export async function createStatusListCredential(options: { encodedList, ...(ttl && {ttl}) } -console.log('credentialSubject', JSON.stringify(credentialSubject)) + // Build the complete credential with W3C contexts const credential: BitstringStatusListCredentialUnsigned = { '@context': [ From 9358d2f36b21060add4240e5cee8e41dd625a82c Mon Sep 17 00:00:00 2001 From: sanderPostma Date: Fri, 11 Jul 2025 10:56:47 +0200 Subject: [PATCH 17/31] chore: readme update --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index fcacc51..5dce299 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@ The key insight here is that you don't need to "add" entries first. The system a ```typescript // Create a verifiable credential containing the status list -const statusListCredential = await createStatusListCredential({ +const statusListCredential:BitstringStatusListCredentialUnsigned = await createStatusListCredential({ id: 'https://example.com/status-lists/1', issuer: 'https://example.com/issuer', statusPurpose: 'revocation', @@ -145,7 +145,7 @@ const credential = { A single status list can serve multiple purposes: ```typescript -const statusListCredential = await createStatusListCredential({ +const statusListCredential:BitstringStatusListCredentialUnsigned = await createStatusListCredential({ statusList: statusList, id: 'https://example.com/status-lists/1', issuer: 'https://example.com/issuer', From 71f33dc8f9a38e11ad6a95d09a30bf5eb0dd29f1 Mon Sep 17 00:00:00 2001 From: sanderPostma Date: Fri, 11 Jul 2025 11:33:48 +0200 Subject: [PATCH 18/31] chore: import optimize --- src/credential/create.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/credential/create.ts b/src/credential/create.ts index 1a9c843..7066ae8 100644 --- a/src/credential/create.ts +++ b/src/credential/create.ts @@ -7,7 +7,6 @@ import {BitstreamStatusList} from '../status-list/BitstreamStatusList' import {BitstringStatusListCredentialSubject, BitstringStatusListCredentialUnsigned, IIssuer} from '../types' -import * as console from "node:console"; /** * Creates a W3C compliant BitstringStatusListCredential with an empty status list From 68c619a4aa6d4280b7e2ac60a709c0396ecdfb76 Mon Sep 17 00:00:00 2001 From: sanderPostma Date: Fri, 11 Jul 2025 11:37:03 +0200 Subject: [PATCH 19/31] chore: comment update --- src/bit-manager/BitManager.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bit-manager/BitManager.ts b/src/bit-manager/BitManager.ts index ec4788b..b1b28eb 100644 --- a/src/bit-manager/BitManager.ts +++ b/src/bit-manager/BitManager.ts @@ -43,7 +43,7 @@ export class BitManager { assertIsPositiveInteger(statusSize, 'statusSize') - // Guard against JavaScript bitwise operation overflow + // 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`) } From 5ea23b450f768033c27684ff474c60f7c50097bd Mon Sep 17 00:00:00 2001 From: sanderPostma Date: Fri, 11 Jul 2025 11:59:23 +0200 Subject: [PATCH 20/31] chore: release unstable --- .github/workflows/release.yml | 1 + package.json | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 420024a..e3b52eb 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -4,6 +4,7 @@ on: push: branches: - 'main' + - 'develop' jobs: build-test-publish: runs-on: ubuntu-24.04 diff --git a/package.json b/package.json index ee36fc4..81aa3b7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@4sure-tech/vc-bitstring-status-lists", - "version": "0.1.0", + "version": "0.1.0-unstable.1", "description": "TypeScript library for W3C Bitstring Status List v1.0 specification - privacy-preserving credential status management", "source": "src/index.ts", "type": "module", From e6beb7881c570d1346a54f55f0a247786f4192be Mon Sep 17 00:00:00 2001 From: sanderPostma Date: Fri, 11 Jul 2025 12:24:07 +0200 Subject: [PATCH 21/31] chore: do not publish src --- package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/package.json b/package.json index 81aa3b7..6764057 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,6 @@ "packageManager": "pnpm@10.13.1", "files": [ "dist", - "src", "README.md", "LICENSE" ], From 81ce0aa6391e9db8760e60034d902d31c797fa61 Mon Sep 17 00:00:00 2001 From: sanderPostma Date: Fri, 11 Jul 2025 12:25:58 +0200 Subject: [PATCH 22/31] chore: README fix --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 5dce299..c679fad 100644 --- a/README.md +++ b/README.md @@ -82,6 +82,7 @@ const credential = { 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' } } @@ -129,7 +130,8 @@ const credential = { credentialStatus: { type: 'BitstringStatusListEntry', statusPurpose: 'revocation', - statusListIndex: '0', + statusListIndex: '0', + statusSize: 2, // Required to support status values up to 0x2 statusListCredential: 'https://example.com/status-lists/1', statusMessage: [ { id: '0x0', message: 'Credential is valid' }, From 431c5846a6326b4700c9d3fdc2e92a7b377f0e1e Mon Sep 17 00:00:00 2001 From: sanderPostma Date: Fri, 11 Jul 2025 12:31:38 +0200 Subject: [PATCH 23/31] chore: README fix --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index c679fad..08e9e5e 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,7 @@ const statusListCredential:BitstringStatusListCredentialUnsigned = await createS id: 'https://example.com/status-lists/1', issuer: 'https://example.com/issuer', statusPurpose: 'revocation', + statusSize: 1, // optional, 1 is default statusList: statusList, // Pass your configured status list validFrom: new Date('2025-07-01'), validUntil: new Date('2026-07-01') From 420b20628845a2b6e591e4f0eb19413ae0af0971 Mon Sep 17 00:00:00 2001 From: sanderPostma Date: Fri, 11 Jul 2025 12:38:11 +0200 Subject: [PATCH 24/31] chore: package.json stuff --- package.json | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/package.json b/package.json index 6764057..bd7aeb0 100644 --- a/package.json +++ b/package.json @@ -31,18 +31,32 @@ "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", From d6be09c45bce6d9e49aeab64bc8137596e1386e2 Mon Sep 17 00:00:00 2001 From: sanderPostma Date: Tue, 15 Jul 2025 23:06:19 +0200 Subject: [PATCH 25/31] chore: Only release main branch for now --- .github/workflows/release.yml | 1 - README.md | 513 +++++++++++++++++++++------------- 2 files changed, 313 insertions(+), 201 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e3b52eb..420024a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -4,7 +4,6 @@ on: push: branches: - 'main' - - 'develop' jobs: build-test-publish: runs-on: ubuntu-24.04 diff --git a/README.md b/README.md index 08e9e5e..979bf85 100644 --- a/README.md +++ b/README.md @@ -1,296 +1,402 @@ # 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. +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. +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. +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 16KB 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 +* **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 +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: +# Quick Start -### 1. Creating a Status List +Let's walk through creating and using a status list step by step. -```typescript -import { BitstreamStatusList, createStatusListCredential } from 'vc-bitstring-status-lists' +## 1 · Creating a Status List *in memory* -// Create a new status list with 1-bit status values (0 = valid, 1 = revoked) -const statusList = new BitstreamStatusList({ statusSize: 1 }) +```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 +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 - -```typescript -// Create a verifiable credential containing the status list -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, // Pass your configured status list - validFrom: new Date('2025-07-01'), - validUntil: new Date('2026-07-01') -}) - -// The credential now contains a compressed, encoded bitstring -console.log(statusListCredential.credentialSubject.encodedList) -// Output: "u..." (compressed and base64url-encoded bitstring) +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 Credential Status +## 3 · Checking a Credential's Status -```typescript -import { checkStatus } from 'vc-bitstring-status-lists' +```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' - } -} + "@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", + }, +}; -// Check the credential's status const result = await checkStatus({ - credential, - getStatusListCredential: async (url) => { - // In practice, you'd fetch this from the URL - return statusListCredential - } -}) - -console.log(result) -// Output: { verified: false, status: 1 } (credential is revoked) + credential, + getStatusListCredential: async (url) => statusListCredential, +}); + +console.log(result); // { verified: false, status: 1 } ``` -## Advanced Usage +--- + +## 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. + +--- -### Multi-bit Status Values +# 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: -```typescript -// Create a status list with 4 bits per credential (supports values 0-15) -const statusList = new BitstreamStatusList({ statusSize: 4 }) +```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 +statusList.setStatus(0, 12); // Binary: 1100 (could represent multiple flags) +statusList.setStatus(1, 3); // Binary: 0011 (different status combination) -// Retrieve the status -const status = statusList.getStatus(0) // Returns: 12 +console.log(statusList.getStatus(0)); // → 12 ``` -This approach is particularly useful when you need to track multiple aspects of a credential's status simultaneously, such as revocation status, verification level, and processing state. - ### Status Messages -You can provide human-readable messages for different status values: +Attach human‑readable messages to status codes: -```typescript +```ts const credential = { - // ... other properties - credentialStatus: { - type: 'BitstringStatusListEntry', - statusPurpose: 'revocation', - statusListIndex: '0', - statusSize: 2, // Required to support status values up to 0x2 - statusListCredential: 'https://example.com/status-lists/1', - statusMessage: [ - { id: '0x0', message: 'Credential is valid' }, - { id: '0x1', message: 'Credential has been revoked' }, - { id: '0x2', message: 'Credential is under review' } - ] - } -} + // … 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: [ + {id: "0x0", message: "Credential is valid"}, + {id: "0x1", message: "Credential has been revoked"}, + {id: "0x2", message: "Credential is under review"}, + ], + }, +}; ``` -### Multiple Status Purposes - -A single status list can serve multiple purposes: +### 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) +}); +``` -```typescript -const statusListCredential:BitstringStatusListCredentialUnsigned = await createStatusListCredential({ - statusList: statusList, - id: 'https://example.com/status-lists/1', - issuer: 'https://example.com/issuer', - statusPurpose: ['revocation', 'suspension'] // Multiple purposes -}) +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, + }, +}; ``` -### Working with Existing Status Lists +> **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. -You can decode and work with existing status lists: +### Working with Existing Status Lists -```typescript +```ts // Decode a status list from an encoded string -const existingStatusList = await BitstreamStatusList.decode({ - encodedList: 'u...', // The encoded bitstring from a credential - statusSize: 1 -}) +const existing = await BitstreamStatusList.decode({ + encodedList: "u…", // from a credential + statusSize: 1, +}); -// Check or modify statuses -console.log(existingStatusList.getStatus(42)) // Get status of credential 42 -existingStatusList.setStatus(100, 1) // Revoke credential 100 +console.log(existing.getStatus(42)); // status of credential 42 +existing.setStatus(100, 1); // revoke credential 100 -// Re-encode for publishing -const updatedEncoded = await existingStatusList.encode() +// 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: +# Understanding the Architecture -### BitManager Class +The library is built around several key components that work together to provide a complete W3C‑compliant implementation: -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. +## BitManager Class -The key insight is that it calculates bit positions mathematically: credential index 42 with a 2-bit status size would occupy bits 84-85 in the bitstring. This eliminates the need for explicit entry management. +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. -### BitstreamStatusList Class +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. -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 16KB minimum size requirement. +## BitstreamStatusList Class -This class handles the complex process of GZIP compression and multibase encoding that the W3C specification requires, while providing a simple interface for credential status management. +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 +## 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. +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 +--- -This library implements all requirements from [the W3C Bitstring Status List v1.0 specification.](https://www.w3.org/TR/vc-bitstring-status-list): +# W3C Compliance Checklist -- **Minimum bitstring size**: All encoded status lists are padded to at least 16KB (131,072 bits) -- **Compression**: Uses gzip compression as required by the specification -- **Base64url encoding**: Proper encoding with the required "u" prefix -- **Status purpose validation**: Ensures that credential entries match the status list's declared purposes -- **Temporal validation**: Checks `validFrom` and `validUntil` dates on status list credentials -- **Uniform status size**: All credentials in a status list use the same number of bits for their status +* **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 +# API Reference -#### `BitstreamStatusList` +## Core Classes -The main class for creating and managing status lists. +### `BitstreamStatusList` -```typescript +```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 + 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` +### `BitManager` -Low-level bit manipulation class (typically used internally). +Low‑level bit manipulation class (typically used internally). -```typescript +```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 + 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` +## High‑Level Functions -Creates a verifiable credential containing a status list. +### `createStatusListCredential` -```typescript +```ts function createStatusListCredential(options: { - id: string - issuer: string | IIssuer - statusSize?: number - statusList?: BitstreamStatusList - statusPurpose: string | string[] - validFrom?: Date - validUntil?: Date - ttl?: number -}): Promise + id: string; + issuer: string | IIssuer; + statusSize?: number; + statusList?: BitstreamStatusList; + statusPurpose: string | string[]; + validFrom?: Date; + validUntil?: Date; + ttl?: number; +}): Promise; ``` -#### `checkStatus` - -Verifies a credential's status against its referenced status list. +### `checkStatus` -```typescript +```ts function checkStatus(options: { - credential: CredentialWithStatus - getStatusListCredential: (url: string) => Promise -}): Promise + credential: CredentialWithStatus; + getStatusListCredential: (url: string) => Promise; +}): Promise; ``` -## Error Handling +--- -The library provides comprehensive error handling with descriptive messages: +# Error Handling -```typescript +```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 (error) { - console.error('Status check failed:', error) + 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 and Testing +--- -The project uses modern TypeScript tooling: +# Building & Testing ```bash # Build the library @@ -300,26 +406,33 @@ pnpm run build pnpm test # The build outputs both ESM and CommonJS formats -# - dist/index.js (ESM) -# - dist/index.cjs (CommonJS) -# - dist/index.d.ts (TypeScript definitions) +# - 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: +# 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 +--- + +# License Licensed under the Apache License, Version 2.0. -## Related Resources +--- + +# 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/) +* [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/) From 9b2654926ef968758ef5461a5453ad065b39e6e6 Mon Sep 17 00:00:00 2001 From: sanderPostma Date: Tue, 15 Jul 2025 23:07:48 +0200 Subject: [PATCH 26/31] chore: publishConfig private until approval --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index bd7aeb0..2a7e8fd 100644 --- a/package.json +++ b/package.json @@ -73,6 +73,6 @@ "vitest": "^3.2.4" }, "publishConfig": { - "access": "public" + "access": "private" } } From af702521056c4bbb026002c56c442e0aeee6d98b Mon Sep 17 00:00:00 2001 From: sanderPostma Date: Tue, 15 Jul 2025 23:31:51 +0200 Subject: [PATCH 27/31] chore: Addressed PR feedback --- README.md | 2 +- __tests__/BitManager.test.ts | 8 ++++---- src/bit-manager/BitManager.ts | 6 +++--- src/types.ts | 12 +++++++----- 4 files changed, 15 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 979bf85..c44fb8a 100644 --- a/README.md +++ b/README.md @@ -210,7 +210,7 @@ const credential = { statusListIndex: "0", statusSize: 2, // Required for status values up to 0x2 statusListCredential: "https://example.com/status-lists/1", - statusMessage: [ + 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"}, diff --git a/__tests__/BitManager.test.ts b/__tests__/BitManager.test.ts index c8a808a..ad16382 100644 --- a/__tests__/BitManager.test.ts +++ b/__tests__/BitManager.test.ts @@ -5,7 +5,7 @@ describe('BitManager', () => { let bitManager: BitManager beforeEach(() => { - bitManager = new BitManager({}) + bitManager = new BitManager() }) describe('constructor', () => { @@ -43,7 +43,7 @@ describe('BitManager', () => { }) it('should throw for negative credential index', () => { - expect(() => bitManager.getStatus(-1)).toThrow() + expect(() => bitManager.getStatus(-1)).toThrow('credentialIndex must be a non-negative integer') }) it('should throw for index exceeding buffer bounds', () => { @@ -64,11 +64,11 @@ describe('BitManager', () => { }) it('should throw for negative credential index', () => { - expect(() => bitManager.setStatus(-1, 1)).toThrow() + 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() + expect(() => bitManager.setStatus(0, -1)).toThrow('status must be a non-negative integer') }) it('should throw for status exceeding bit size', () => { diff --git a/src/bit-manager/BitManager.ts b/src/bit-manager/BitManager.ts index b1b28eb..3f92c84 100644 --- a/src/bit-manager/BitManager.ts +++ b/src/bit-manager/BitManager.ts @@ -34,12 +34,12 @@ export class BitManager { * @param options.buffer - Existing buffer for decoding * @param options.initialSize - Initial buffer size in bytes (default: 16KB) */ - constructor(options: { + constructor(options?: { statusSize?: number buffer?: Uint8Array initialSize?: number - } = {}) { - const {statusSize = 1, buffer, initialSize = LIST_BLOCK_SIZE} = options + }) { + const {statusSize = 1, buffer, initialSize = LIST_BLOCK_SIZE} = options ?? {}; assertIsPositiveInteger(statusSize, 'statusSize') diff --git a/src/types.ts b/src/types.ts index 8cb7362..b1f137d 100644 --- a/src/types.ts +++ b/src/types.ts @@ -43,6 +43,12 @@ export interface IIssuer { export type CredentialStatus = BitstringStatusListEntry | BitstringStatusListEntry[] +type CredentialSubject = { + id: string + type: string + [key: string]: any +}; + export interface CredentialWithStatus { '@context': string[] id: string @@ -51,11 +57,7 @@ export interface CredentialWithStatus { validFrom?: string validUntil?: string credentialStatus?: CredentialStatus - credentialSubject: { - id: string - type: string - [key: string]: any - } + credentialSubject: CredentialSubject } export interface CheckStatusOptions { From 911ca57cf6602381f7535aebc93555031a3f6095 Mon Sep 17 00:00:00 2001 From: sanderPostma Date: Wed, 16 Jul 2025 10:02:10 +0200 Subject: [PATCH 28/31] chore: Addressed PR feedback --- __tests__/BitManager.test.ts | 2 ++ __tests__/StatusList.test.ts | 2 ++ 2 files changed, 4 insertions(+) diff --git a/__tests__/BitManager.test.ts b/__tests__/BitManager.test.ts index ad16382..d95a934 100644 --- a/__tests__/BitManager.test.ts +++ b/__tests__/BitManager.test.ts @@ -17,6 +17,8 @@ describe('BitManager', () => { 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', () => { diff --git a/__tests__/StatusList.test.ts b/__tests__/StatusList.test.ts index 337d10d..567c953 100644 --- a/__tests__/StatusList.test.ts +++ b/__tests__/StatusList.test.ts @@ -18,6 +18,8 @@ describe('StatusList', () => { 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', () => { From a7198b401cb0abe405a09afc1d2555a2a1c9a5ee Mon Sep 17 00:00:00 2001 From: sanderPostma Date: Mon, 21 Jul 2025 10:47:45 +0200 Subject: [PATCH 29/31] chore: Set npm to public --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2a7e8fd..bd7aeb0 100644 --- a/package.json +++ b/package.json @@ -73,6 +73,6 @@ "vitest": "^3.2.4" }, "publishConfig": { - "access": "private" + "access": "public" } } From dca7a77b73bbc7a984e2efc03b03161dc85aefe4 Mon Sep 17 00:00:00 2001 From: sanderPostma Date: Mon, 21 Jul 2025 11:01:09 +0200 Subject: [PATCH 30/31] chore: set version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index bd7aeb0..26f8dfe 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@4sure-tech/vc-bitstring-status-lists", - "version": "0.1.0-unstable.1", + "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", From d3f29cd2798d0c5aa135c74b6896977f241ce4c1 Mon Sep 17 00:00:00 2001 From: sanderPostma Date: Mon, 21 Jul 2025 15:10:08 +0200 Subject: [PATCH 31/31] chore: cleanup --- src/status-list/errors.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/status-list/errors.ts b/src/status-list/errors.ts index db420f4..9df89d0 100644 --- a/src/status-list/errors.ts +++ b/src/status-list/errors.ts @@ -1,4 +1,3 @@ -// errors.ts export class BitstringStatusListError extends Error { public readonly type: string public readonly code: string