@vess-id/status-list
v0.3.0
Published
IETF OAuth Status List implementation for TypeScript/Node.js (JWT & CWT)
Downloads
389
Maintainers
Readme
@vess-id/status-list
IETF OAuth Status List implementation for TypeScript/Node.js, supporting both JWT and CWT formats.
Overview
This library implements the IETF OAuth Status List specification for managing credential status information in a privacy-preserving, space-efficient manner.
Key Features:
- ✅ Dual Format Support: JWT and CWT (CBOR Web Token) formats
- ✅ Space Efficient: ZLIB compression reduces storage by 90%+
- ✅ Privacy Preserving: Herd privacy through large status lists
- ✅ Flexible Status Values: Support for 1, 2, 4, or 8-bit status values
- ✅ Full Lifecycle: Create, sign, verify, and query status lists
- ✅ Standards Compliant: Follows IETF draft-ietf-oauth-status-list-14
- ✅ TypeScript Native: Full type safety with comprehensive types
- ✅ Production Ready: Extensive test coverage (86+ tests)
Use Cases
- SD-JWT Verifiable Credentials: Revocation for Selective Disclosure JWTs
- mdoc/mDL: Status management for mobile Driver's Licenses and ISO 18013-5 documents
- W3C Verifiable Credentials: Credential status tracking
- OAuth Token Revocation: Scalable token status lists
Installation
npm install @vess-id/status-listRequirements:
- Node.js >= 20
- TypeScript >= 5.0 (if using TypeScript)
Quick Start
Creating a JWT Status List
import {
StatusList,
createJWTStatusListPayload,
signStatusListJWT,
StandardStatusValues,
} from '@vess-id/status-list';
import { importPKCS8 } from 'jose';
// 1. Create a status list (1 bit per entry: 0=valid, 1=revoked)
const list = StatusList.create(10000, 1);
// 2. Revoke some credentials
list.setStatus(42, StandardStatusValues.INVALID);
list.setStatus(100, StandardStatusValues.INVALID);
// 3. Create JWT payload
const { payload } = createJWTStatusListPayload({
statusList: list,
iss: 'https://issuer.example.com',
sub: 'https://issuer.example.com/status/1',
iat: Math.floor(Date.now() / 1000),
exp: Math.floor(Date.now() / 1000) + 86400, // 24 hours
ttl: 3600, // Cache for 1 hour
});
// 4. Sign the JWT
const privateKey = await importPKCS8(pemPrivateKey, 'ES256');
const jwt = await signStatusListJWT(payload, privateKey, { alg: 'ES256' });
console.log('Status List JWT:', jwt);Verifying Credential Status
import {
StatusListTokenHelper,
StandardStatusValues,
} from '@vess-id/status-list';
// Extract status reference from credential
const credentialStatus = {
idx: 42,
uri: 'https://issuer.example.com/status/1',
};
// Fetch and parse status list
const helper = await StatusListTokenHelper.fromStatusReference(credentialStatus);
// Check if expired
if (helper.isExpired()) {
throw new Error('Status list expired');
}
// Check credential status
const status = helper.getStatus(credentialStatus.idx);
if (status === StandardStatusValues.INVALID) {
console.log('❌ Credential is REVOKED');
} else if (status === StandardStatusValues.VALID) {
console.log('✅ Credential is VALID');
} else if (status === StandardStatusValues.SUSPENDED) {
console.log('⏸️ Credential is SUSPENDED');
}Creating a CWT Status List (for mdoc)
import {
StatusList,
createCWTStatusListPayload,
encodeCWTPayload,
} from '@vess-id/status-list';
// 1. Create status list
const list = StatusList.create(10000, 2); // 2 bits: supports 4 status values
// 2. Set statuses
list.setStatus(0, 0); // Valid
list.setStatus(1, 1); // Invalid
list.setStatus(2, 2); // Suspended
// 3. Create CWT payload
const payload = createCWTStatusListPayload({
sub: 'https://issuer.example.com/status/1',
iss: 'https://issuer.example.com',
lst: list.compressToBytes(),
bits: 2,
iat: Math.floor(Date.now() / 1000),
exp: Math.floor(Date.now() / 1000) + 86400,
});
// 4. Encode to CBOR
const cwtBytes = encodeCWTPayload(payload);
console.log('CWT Status List:', cwtBytes);API Reference
Core Classes
StatusList
Main class for managing bit-packed status arrays.
// Create new list
const list = StatusList.create(size: number, bits: BitsPerStatus);
// From existing statuses
const list = StatusList.fromArray(statuses: StatusValue[], bits: BitsPerStatus);
// Decompress from JWT
const list = StatusList.decompressFromBase64URL(compressed: string, bits: BitsPerStatus);
// Decompress from CWT
const list = StatusList.decompressFromBytes(compressed: Uint8Array, bits: BitsPerStatus);
// Get/Set status
const status = list.getStatus(index: number): StatusValue;
list.setStatus(index: number, value: StatusValue): void;
// Compress for JWT
const compressed = list.compressToBase64URL(): string;
// Compress for CWT
const compressed = list.compressToBytes(): Uint8Array;
// Metadata
const size = list.getSize(): number;
const bits = list.getBitsPerStatus(): BitsPerStatus;StatusListTokenHelper
High-level helper for working with Status List Tokens.
// From token (auto-detects JWT/CWT)
const helper = StatusListTokenHelper.fromToken(token: string | Uint8Array);
// From URI (fetches via HTTP)
const helper = await StatusListTokenHelper.fromStatusReference(
reference: { idx: number; uri: string },
options?: FetchOptions
);
// Query status
const status = helper.getStatus(index: number): StatusValue;
// Check expiration
const expired = helper.isExpired(currentTime?: number): boolean;
// Access metadata
const iss = helper.iss;
const sub = helper.sub;
const iat = helper.iat;
const exp = helper.exp;
const ttl = helper.ttl;
const bits = helper.bits;
const size = helper.size;
const format = helper.tokenFormat; // 'jwt' | 'cwt'JWT Format
Creating JWT Status Lists
import {
createJWTStatusListPayload,
signStatusListJWT,
type CreateJWTStatusListOptions,
} from '@vess-id/status-list';
// Create payload
const { header, payload } = createJWTStatusListPayload({
statusList: list,
iss: string,
sub: string,
iat?: number, // defaults to now
exp?: number,
ttl?: number,
aggregationUri?: string,
});
// Sign
const jwt = await signStatusListJWT(
payload,
privateKey: KeyLike,
options?: {
alg?: string;
kid?: string;
additionalHeaders?: Record<string, unknown>;
}
): Promise<string>;Parsing JWT Status Lists
import {
parseJWTStatusList,
verifyStatusListJWT,
extractStatusListReference,
} from '@vess-id/status-list';
// Parse (without verification)
const { header, payload, statusList } = parseJWTStatusList(jwt: string);
// Verify signature
const { header, payload } = await verifyStatusListJWT(
jwt: string,
publicKey: KeyLike,
options?: {
issuer?: string;
validateExp?: boolean;
}
);
// Extract reference from credential
const reference = extractStatusListReference(credentialJWT: string);
// Returns: { idx: number, uri: string }CWT Format
Creating CWT Status Lists
import {
createCWTStatusListPayload,
encodeCWTPayload,
signCWTStatusList,
type COSEKey,
} from '@vess-id/status-list';
// Create payload
const payload = createCWTStatusListPayload({
sub?: string,
iss?: string,
lst: Uint8Array,
bits: BitsPerStatus,
iat?: number,
exp?: number,
ttl?: number,
aggregation_uri?: string,
additionalClaims?: Record<number, unknown>,
});
// Encode to CBOR (unsigned)
const cwtBytes = encodeCWTPayload(payload);
// Sign with COSE Sign1 (advanced)
const cwtSigned = signCWTStatusList(
payload,
privateKey: COSEKey,
options?: {
alg?: number;
kid?: Uint8Array;
additionalHeaders?: Map<number, unknown>;
}
);Parsing CWT Status Lists
import {
parseCWTStatusList,
parseCWTStatusListSigned,
extractStatusListReferenceCBOR,
} from '@vess-id/status-list';
// Parse unsigned CWT
const { payload, statusList } = parseCWTStatusList(cwtBytes: Uint8Array);
// Parse and verify signed CWT
const { payload, statusList } = parseCWTStatusListSigned(
cwtBytes: Uint8Array,
publicKey: COSEKey
);
// Extract reference from mdoc credential
const reference = extractStatusListReferenceCBOR(credentialCBOR: Uint8Array);
// Returns: { idx: number, uri: string }Validation
import {
validateJWTPayload,
validateCWTPayload,
validateExpiry,
validateTTLBounds,
isExpired,
} from '@vess-id/status-list';
// Validate payload structure
const result = validateJWTPayload(payload);
if (!result.valid) {
console.error('Errors:', result.errors);
}
// Check expiration
const expiryResult = validateExpiry(exp, iat, ttl);
if (!expiryResult.valid) {
console.error('Token expired');
}
// Validate TTL bounds (DoS prevention)
const ttlResult = validateTTLBounds(ttl);
if (!ttlResult.valid) {
console.warn('TTL outside recommended bounds:', ttlResult.errors);
}
// Simple expiration check
if (isExpired(exp, iat, ttl)) {
throw new Error('Token expired');
}Fetching Status Lists
import {
fetchStatusListToken,
isValidStatusListUri,
type FetchOptions,
} from '@vess-id/status-list';
// Fetch status list (auto-detects JWT/CWT)
const token = await fetchStatusListToken(
uri: string,
options?: {
timeout?: number; // default: 10000ms
headers?: Record<string, string>;
maxRedirects?: number; // default: 3
fetchImpl?: typeof fetch;
}
);
// Validate URI
if (!isValidStatusListUri(uri)) {
throw new Error('Invalid status list URI');
}Types
Status Values
type BitsPerStatus = 1 | 2 | 4 | 8;
type StatusValue = number; // 0-255 depending on bits
enum StandardStatusValues {
VALID = 0x00,
INVALID = 0x01,
SUSPENDED = 0x02,
APPLICATION_SPECIFIC = 0x03,
}Status Format
type StatusFormat = 'jwt' | 'cwt';
interface StatusListReference {
idx: number;
uri: string;
}Advanced Examples
Large-Scale Status List (100K credentials)
import { StatusList, StandardStatusValues } from '@vess-id/status-list';
// Create list for 100,000 credentials
const list = StatusList.create(100000, 1);
// Revoke every 1000th credential
for (let i = 0; i < 100000; i += 1000) {
list.setStatus(i, StandardStatusValues.INVALID);
}
// Compress
const compressed = list.compressToBase64URL();
console.log('Compressed size:', compressed.length, 'characters');
// Expected: ~4-5KB (90%+ compression)
// Original size: 100,000 bits = 12,500 bytes
// Compressed: ~5,000 bytes (~60% compression for sparse data)Multi-Status System (2-bit)
import { StatusList } from '@vess-id/status-list';
const STATUS = {
ACTIVE: 0,
REVOKED: 1,
SUSPENDED: 2,
EXPIRED: 3,
};
// 2 bits per status = 4 possible values
const list = StatusList.create(1000, 2);
list.setStatus(0, STATUS.ACTIVE);
list.setStatus(1, STATUS.REVOKED);
list.setStatus(2, STATUS.SUSPENDED);
list.setStatus(3, STATUS.EXPIRED);
const status = list.getStatus(1);
console.log(status === STATUS.REVOKED); // trueCustom Validation
import {
StatusListTokenHelper,
validateExpiry,
validateTTLBounds,
} from '@vess-id/status-list';
async function validateCredentialStatus(reference: { idx: number; uri: string }) {
// Fetch status list
const helper = await StatusListTokenHelper.fromStatusReference(reference);
// Validate expiry
const expiryResult = validateExpiry(helper.exp, helper.iat, helper.ttl);
if (!expiryResult.valid) {
throw new Error(`Status list expired: ${expiryResult.errors.join(', ')}`);
}
// Validate TTL bounds
const ttlResult = validateTTLBounds(helper.ttl);
if (!ttlResult.valid) {
console.warn(`TTL warning: ${ttlResult.errors.join(', ')}`);
}
// Check status
const status = helper.getStatus(reference.idx);
return {
valid: status === 0,
status,
issuer: helper.iss,
expiresAt: helper.exp,
};
}Credential with Status (SD-JWT Example)
import { createJWTStatusListPayload, signStatusListJWT } from '@vess-id/status-list';
// 1. Issue credential with status claim
const credential = {
iss: 'https://issuer.example.com',
sub: '[email protected]',
iat: Math.floor(Date.now() / 1000),
exp: Math.floor(Date.now() / 1000) + 31536000, // 1 year
vc: {
type: ['VerifiableCredential', 'UniversityDegree'],
credentialSubject: {
name: 'Alice Smith',
degree: 'Bachelor of Science',
},
},
status: {
idx: 42,
uri: 'https://issuer.example.com/status/1',
},
};
// 2. Create status list
const list = StatusList.create(10000, 1);
const { payload } = createJWTStatusListPayload({
statusList: list,
iss: 'https://issuer.example.com',
sub: 'https://issuer.example.com/status/1',
iat: Math.floor(Date.now() / 1000),
exp: Math.floor(Date.now() / 1000) + 86400,
ttl: 3600,
});
// 3. Sign status list
const statusListJWT = await signStatusListJWT(payload, privateKey);
// 4. Verifier checks status
const helper = await StatusListTokenHelper.fromStatusReference(credential.status);
const status = helper.getStatus(credential.status.idx);
if (status === 1) {
throw new Error('Credential has been revoked');
}Security Considerations
This library follows the security recommendations from the IETF specification:
1. TTL Bounds (Section 11.5)
// Default limits prevent DoS attacks
const MAX_TTL = 31536000; // 1 year
const MIN_TTL = 60; // 1 minute
// Validate TTL
const result = validateTTLBounds(ttl);
if (!result.valid) {
console.warn('TTL outside recommended bounds');
}2. Redirect Limits (Section 11.4)
// Default maximum: 3 redirects
const token = await fetchStatusListToken(uri, {
maxRedirects: 3,
});3. Timeout Protection
// Default timeout: 10 seconds
const token = await fetchStatusListToken(uri, {
timeout: 10000,
});4. Signature Verification
// Always verify signatures in production
const verified = await verifyStatusListJWT(jwt, publicKey);5. Herd Privacy
// Use large lists for privacy (recommended: 10,000+ entries)
const list = StatusList.create(100000, 1);
// Even with few revoked credentials, observers cannot correlatePerformance
Compression Efficiency
| Entries | Bits | Uncompressed | Compressed | Ratio | |---------|------|--------------|------------|-------| | 10,000 | 1 | 1.22 KB | ~500 B | 60% | | 100,000 | 1 | 12.2 KB | ~5 KB | 60% | | 10,000 | 2 | 2.44 KB | ~1 KB | 60% | | 100,000 | 8 | 97.7 KB | ~20 KB | 80% |
Compression ratio depends on data entropy. Sparse lists compress better.
Memory Usage
// Minimal memory footprint
const list = StatusList.create(100000, 1);
// Memory: ~12.5 KB (uncompressed in memory)
// Network: ~5 KB (compressed over wire)Testing
# Run all tests
npm test
# Run tests in watch mode
npm run test:watch
# Run tests with coverage
npm run test:coverage
# Build
npm run build
# Lint
npm run lint
# Format
npm run formatTest Coverage
Test Files: 5 passed (5)
Tests: 86 passed (86)
- Core (bitpack, compression, StatusList): 58 tests
- JWT integration: 10 tests
- CWT integration: 18 testsCompatibility
Node.js Versions
- ✅ Node.js 20+
- ✅ Node.js 22+
Module Systems
- ✅ ESM (import)
- ✅ CommonJS (require)
TypeScript
- ✅ TypeScript 5.0+
- ✅ Full type definitions included
- ✅ Strict mode compatible
IETF Specification Compliance
This library implements draft-ietf-oauth-status-list-14:
- ✅ Section 3: Status List Structure
- ✅ Section 4: Bit Packing (LSB-first)
- ✅ Section 5: JWT Status List
- ✅ Section 5.2: CWT Status List
- ✅ Section 6: Status Reference
- ✅ Section 11: Security Considerations
Related Projects
- EUDI Wallet IT (Python) - Reference implementation
- @sd-jwt/jwt-status-list - SD-JWT TypeScript implementation
Contributing
Contributions are welcome! Please follow these guidelines:
- Fork the repository
- Create a feature branch
- Write tests for new features
- Ensure all tests pass
- Submit a pull request
License
Apache License 2.0
Copyright (c) 2024 VESS Project
See LICENSE file for details.
Support
- Issues: GitHub Issues
- Documentation: API Reference
- Specification: IETF Draft
Made with ❤️ for the Verifiable Credentials ecosystem
