@vess-id/status-list 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +51 -0
- package/README.md +710 -0
- package/dist/index.cjs +1576 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +1634 -0
- package/dist/index.d.ts +1634 -0
- package/dist/index.js +1508 -0
- package/dist/index.js.map +1 -0
- package/package.json +79 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1508 @@
|
|
|
1
|
+
import * as pako from 'pako';
|
|
2
|
+
import { SignJWT, jwtVerify } from 'jose';
|
|
3
|
+
import * as cbor from 'cbor';
|
|
4
|
+
import { createSign, createVerify } from 'crypto';
|
|
5
|
+
|
|
6
|
+
// lib/types/errors.ts
|
|
7
|
+
var StatusListError = class extends Error {
|
|
8
|
+
constructor(message) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.name = "StatusListError";
|
|
11
|
+
if (Error.captureStackTrace) {
|
|
12
|
+
Error.captureStackTrace(this, this.constructor);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
var IndexOutOfBoundsError = class extends StatusListError {
|
|
17
|
+
constructor(index, maxIndex) {
|
|
18
|
+
super(`Index ${index} is out of bounds. Valid range: 0-${maxIndex}`);
|
|
19
|
+
this.name = "IndexOutOfBoundsError";
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
var InvalidBitSizeError = class extends StatusListError {
|
|
23
|
+
constructor(bits) {
|
|
24
|
+
super(`Invalid bit size: ${bits}. Must be one of: 1, 2, 4, or 8`);
|
|
25
|
+
this.name = "InvalidBitSizeError";
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
var InvalidStatusValueError = class extends StatusListError {
|
|
29
|
+
constructor(value, maxValue, bitsPerStatus) {
|
|
30
|
+
super(`Invalid status value: ${value}. For ${bitsPerStatus}-bit status, valid range is 0-${maxValue}`);
|
|
31
|
+
this.name = "InvalidStatusValueError";
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
var InvalidTokenFormatError = class extends StatusListError {
|
|
35
|
+
constructor(message) {
|
|
36
|
+
super(`Invalid token format: ${message}`);
|
|
37
|
+
this.name = "InvalidTokenFormatError";
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
var StatusListExpiredError = class extends StatusListError {
|
|
41
|
+
constructor(exp, currentTime) {
|
|
42
|
+
super(`Status list expired at ${exp} (current time: ${currentTime})`);
|
|
43
|
+
this.name = "StatusListExpiredError";
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
var FetchError = class extends StatusListError {
|
|
47
|
+
constructor(message) {
|
|
48
|
+
super(message);
|
|
49
|
+
this.name = "FetchError";
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
var MissingStatusListUriError = class extends StatusListError {
|
|
53
|
+
constructor(message = "Status list URI is missing from token") {
|
|
54
|
+
super(message);
|
|
55
|
+
this.name = "MissingStatusListUriError";
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
var CompressionError = class extends StatusListError {
|
|
59
|
+
constructor(message, cause) {
|
|
60
|
+
const fullMessage = cause ? `${message}: ${cause.message}` : message;
|
|
61
|
+
super(fullMessage);
|
|
62
|
+
this.name = "CompressionError";
|
|
63
|
+
this.cause = cause;
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
var ValidationError = class extends StatusListError {
|
|
67
|
+
errors;
|
|
68
|
+
constructor(errors) {
|
|
69
|
+
super(`Validation failed: ${errors.join(", ")}`);
|
|
70
|
+
this.name = "ValidationError";
|
|
71
|
+
this.errors = errors;
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
// lib/core/bitpack.ts
|
|
76
|
+
function validateBitSize(bits) {
|
|
77
|
+
if (bits !== 1 && bits !== 2 && bits !== 4 && bits !== 8) {
|
|
78
|
+
throw new InvalidBitSizeError(bits);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
function validateStatusValue(value, bitsPerStatus) {
|
|
82
|
+
const maxValue = (1 << bitsPerStatus) - 1;
|
|
83
|
+
if (value < 0 || value > maxValue) {
|
|
84
|
+
throw new InvalidStatusValueError(value, maxValue, bitsPerStatus);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
function packBits(statuses, bitsPerStatus) {
|
|
88
|
+
validateBitSize(bitsPerStatus);
|
|
89
|
+
const totalBits = statuses.length * bitsPerStatus;
|
|
90
|
+
const byteLength = Math.ceil(totalBits / 8);
|
|
91
|
+
const bytes = new Uint8Array(byteLength);
|
|
92
|
+
for (let i = 0; i < statuses.length; i++) {
|
|
93
|
+
const value = statuses[i];
|
|
94
|
+
validateStatusValue(value, bitsPerStatus);
|
|
95
|
+
const bitOffset = i * bitsPerStatus;
|
|
96
|
+
const byteIndex = Math.floor(bitOffset / 8);
|
|
97
|
+
const bitPosition = bitOffset % 8;
|
|
98
|
+
bytes[byteIndex] |= value << bitPosition;
|
|
99
|
+
if (bitPosition + bitsPerStatus > 8) {
|
|
100
|
+
const bitsInNextByte = bitPosition + bitsPerStatus - 8;
|
|
101
|
+
bytes[byteIndex + 1] |= value >> bitsPerStatus - bitsInNextByte;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return bytes;
|
|
105
|
+
}
|
|
106
|
+
function unpackBits(bytes, bitsPerStatus) {
|
|
107
|
+
validateBitSize(bitsPerStatus);
|
|
108
|
+
const totalBits = bytes.length * 8;
|
|
109
|
+
const statusCount = Math.floor(totalBits / bitsPerStatus);
|
|
110
|
+
const statuses = new Array(statusCount);
|
|
111
|
+
const mask = (1 << bitsPerStatus) - 1;
|
|
112
|
+
for (let i = 0; i < statusCount; i++) {
|
|
113
|
+
statuses[i] = getBitValue(bytes, i, bitsPerStatus, mask);
|
|
114
|
+
}
|
|
115
|
+
return statuses;
|
|
116
|
+
}
|
|
117
|
+
function getBitValue(bytes, index, bitsPerStatus, mask) {
|
|
118
|
+
validateBitSize(bitsPerStatus);
|
|
119
|
+
const totalBits = bytes.length * 8;
|
|
120
|
+
const maxIndex = Math.floor(totalBits / bitsPerStatus) - 1;
|
|
121
|
+
if (index < 0 || index > maxIndex) {
|
|
122
|
+
throw new IndexOutOfBoundsError(index, maxIndex);
|
|
123
|
+
}
|
|
124
|
+
const jump = bitsPerStatus * index;
|
|
125
|
+
const byteIndex = Math.floor(jump / 8);
|
|
126
|
+
const bitOffset = jump % 8;
|
|
127
|
+
const calculatedMask = mask ?? (1 << bitsPerStatus) - 1;
|
|
128
|
+
let value = bytes[byteIndex] >> bitOffset & calculatedMask;
|
|
129
|
+
if (bitOffset + bitsPerStatus > 8) {
|
|
130
|
+
const bitsInNextByte = bitOffset + bitsPerStatus - 8;
|
|
131
|
+
const nextByteBits = bytes[byteIndex + 1] & (1 << bitsInNextByte) - 1;
|
|
132
|
+
value |= nextByteBits << bitsPerStatus - bitsInNextByte;
|
|
133
|
+
}
|
|
134
|
+
return value;
|
|
135
|
+
}
|
|
136
|
+
function setBitValue(bytes, index, value, bitsPerStatus) {
|
|
137
|
+
validateBitSize(bitsPerStatus);
|
|
138
|
+
validateStatusValue(value, bitsPerStatus);
|
|
139
|
+
const totalBits = bytes.length * 8;
|
|
140
|
+
const maxIndex = Math.floor(totalBits / bitsPerStatus) - 1;
|
|
141
|
+
if (index < 0 || index > maxIndex) {
|
|
142
|
+
throw new IndexOutOfBoundsError(index, maxIndex);
|
|
143
|
+
}
|
|
144
|
+
const jump = bitsPerStatus * index;
|
|
145
|
+
const byteIndex = Math.floor(jump / 8);
|
|
146
|
+
const bitOffset = jump % 8;
|
|
147
|
+
const mask = (1 << bitsPerStatus) - 1;
|
|
148
|
+
bytes[byteIndex] &= ~(mask << bitOffset);
|
|
149
|
+
bytes[byteIndex] |= (value & mask) << bitOffset;
|
|
150
|
+
if (bitOffset + bitsPerStatus > 8) {
|
|
151
|
+
const bitsInNextByte = bitOffset + bitsPerStatus - 8;
|
|
152
|
+
const nextByteMask = (1 << bitsInNextByte) - 1;
|
|
153
|
+
bytes[byteIndex + 1] &= ~nextByteMask;
|
|
154
|
+
bytes[byteIndex + 1] |= value >> bitsPerStatus - bitsInNextByte & nextByteMask;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
function calculateCapacity(byteLength, bitsPerStatus) {
|
|
158
|
+
validateBitSize(bitsPerStatus);
|
|
159
|
+
return Math.floor(byteLength * 8 / bitsPerStatus);
|
|
160
|
+
}
|
|
161
|
+
function compress(data) {
|
|
162
|
+
try {
|
|
163
|
+
return pako.deflate(data, { level: 9 });
|
|
164
|
+
} catch (error) {
|
|
165
|
+
throw new CompressionError("Failed to compress data", error);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
function decompress(compressed) {
|
|
169
|
+
try {
|
|
170
|
+
return pako.inflate(compressed);
|
|
171
|
+
} catch (error) {
|
|
172
|
+
throw new CompressionError("Failed to decompress data", error);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
function compressToBase64URL(data) {
|
|
176
|
+
const compressed = compress(data);
|
|
177
|
+
return base64UrlEncode(compressed);
|
|
178
|
+
}
|
|
179
|
+
function decompressFromBase64URL(base64url) {
|
|
180
|
+
try {
|
|
181
|
+
const compressed = base64UrlDecode(base64url);
|
|
182
|
+
return decompress(compressed);
|
|
183
|
+
} catch (error) {
|
|
184
|
+
if (error instanceof CompressionError) {
|
|
185
|
+
throw error;
|
|
186
|
+
}
|
|
187
|
+
throw new CompressionError("Failed to decode or decompress base64url data", error);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
function base64UrlEncode(bytes) {
|
|
191
|
+
let base64 = "";
|
|
192
|
+
if (typeof Buffer !== "undefined") {
|
|
193
|
+
base64 = Buffer.from(bytes).toString("base64");
|
|
194
|
+
} else {
|
|
195
|
+
const binary = String.fromCharCode(...bytes);
|
|
196
|
+
base64 = btoa(binary);
|
|
197
|
+
}
|
|
198
|
+
return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
|
199
|
+
}
|
|
200
|
+
function base64UrlDecode(base64url) {
|
|
201
|
+
let base64 = base64url.replace(/-/g, "+").replace(/_/g, "/");
|
|
202
|
+
const padding = base64.length % 4;
|
|
203
|
+
if (padding > 0) {
|
|
204
|
+
base64 += "=".repeat(4 - padding);
|
|
205
|
+
}
|
|
206
|
+
if (typeof Buffer !== "undefined") {
|
|
207
|
+
return new Uint8Array(Buffer.from(base64, "base64"));
|
|
208
|
+
} else {
|
|
209
|
+
const binary = atob(base64);
|
|
210
|
+
const bytes = new Uint8Array(binary.length);
|
|
211
|
+
for (let i = 0; i < binary.length; i++) {
|
|
212
|
+
bytes[i] = binary.charCodeAt(i);
|
|
213
|
+
}
|
|
214
|
+
return bytes;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// lib/core/StatusList.ts
|
|
219
|
+
var StatusList = class _StatusList {
|
|
220
|
+
statusArray;
|
|
221
|
+
bitsPerStatus;
|
|
222
|
+
/**
|
|
223
|
+
* Creates a StatusList from a byte array.
|
|
224
|
+
*
|
|
225
|
+
* @param statusArray - Byte array containing packed status values
|
|
226
|
+
* @param bitsPerStatus - Number of bits per status entry
|
|
227
|
+
*
|
|
228
|
+
* @private Use static factory methods instead: fromArray(), decompressFromBase64URL(), decompressFromBytes()
|
|
229
|
+
*/
|
|
230
|
+
constructor(statusArray, bitsPerStatus) {
|
|
231
|
+
this.statusArray = statusArray;
|
|
232
|
+
this.bitsPerStatus = bitsPerStatus;
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Gets the status value at a specific index.
|
|
236
|
+
*
|
|
237
|
+
* @param index - Index of the status entry (0-based)
|
|
238
|
+
* @returns Status value at the given index
|
|
239
|
+
* @throws {IndexOutOfBoundsError} If index is out of bounds
|
|
240
|
+
*
|
|
241
|
+
* @example
|
|
242
|
+
* ```typescript
|
|
243
|
+
* const status = list.getStatus(42);
|
|
244
|
+
* if (status === StandardStatusValues.INVALID) {
|
|
245
|
+
* console.log('Credential is revoked');
|
|
246
|
+
* }
|
|
247
|
+
* ```
|
|
248
|
+
*/
|
|
249
|
+
getStatus(index) {
|
|
250
|
+
return getBitValue(this.statusArray, index, this.bitsPerStatus);
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Sets the status value at a specific index.
|
|
254
|
+
*
|
|
255
|
+
* Note: This modifies the status list in place. After modifying statuses,
|
|
256
|
+
* you'll need to recompress and re-sign the status list token.
|
|
257
|
+
*
|
|
258
|
+
* @param index - Index of the status entry (0-based)
|
|
259
|
+
* @param value - New status value
|
|
260
|
+
* @throws {IndexOutOfBoundsError} If index is out of bounds
|
|
261
|
+
* @throws {InvalidStatusValueError} If value exceeds the maximum for the bit size
|
|
262
|
+
*
|
|
263
|
+
* @example
|
|
264
|
+
* ```typescript
|
|
265
|
+
* // Revoke credential at index 42
|
|
266
|
+
* list.setStatus(42, StandardStatusValues.INVALID);
|
|
267
|
+
*
|
|
268
|
+
* // Suspend credential at index 100
|
|
269
|
+
* list.setStatus(100, StandardStatusValues.SUSPENDED);
|
|
270
|
+
* ```
|
|
271
|
+
*/
|
|
272
|
+
setStatus(index, value) {
|
|
273
|
+
setBitValue(this.statusArray, index, value, this.bitsPerStatus);
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Gets the number of bits used per status entry.
|
|
277
|
+
*
|
|
278
|
+
* @returns Number of bits per status (1, 2, 4, or 8)
|
|
279
|
+
*/
|
|
280
|
+
getBitsPerStatus() {
|
|
281
|
+
return this.bitsPerStatus;
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Gets the total number of status entries in this list.
|
|
285
|
+
*
|
|
286
|
+
* @returns Total capacity of the status list
|
|
287
|
+
*/
|
|
288
|
+
getSize() {
|
|
289
|
+
return calculateCapacity(this.statusArray.length, this.bitsPerStatus);
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Gets the raw byte array (for internal use).
|
|
293
|
+
*
|
|
294
|
+
* @returns The underlying byte array
|
|
295
|
+
*/
|
|
296
|
+
getBytes() {
|
|
297
|
+
return this.statusArray;
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Compresses the status list to base64url format (for JWT).
|
|
301
|
+
*
|
|
302
|
+
* @returns Base64URL-encoded compressed status list
|
|
303
|
+
*
|
|
304
|
+
* @example
|
|
305
|
+
* ```typescript
|
|
306
|
+
* const lst = list.compressToBase64URL();
|
|
307
|
+
* // Use in JWT payload: { status_list: { bits: 1, lst } }
|
|
308
|
+
* ```
|
|
309
|
+
*/
|
|
310
|
+
compressToBase64URL() {
|
|
311
|
+
return compressToBase64URL(this.statusArray);
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* Compresses the status list to raw bytes (for CWT).
|
|
315
|
+
*
|
|
316
|
+
* @returns Compressed status list as bytes
|
|
317
|
+
*
|
|
318
|
+
* @example
|
|
319
|
+
* ```typescript
|
|
320
|
+
* const lst = list.compressToBytes();
|
|
321
|
+
* // Use in CWT payload: { 65533: { bits: 1, lst } }
|
|
322
|
+
* ```
|
|
323
|
+
*/
|
|
324
|
+
compressToBytes() {
|
|
325
|
+
return compress(this.statusArray);
|
|
326
|
+
}
|
|
327
|
+
// ===== Static Factory Methods =====
|
|
328
|
+
/**
|
|
329
|
+
* Creates a StatusList from an array of status values.
|
|
330
|
+
*
|
|
331
|
+
* @param statuses - Array of status values
|
|
332
|
+
* @param bitsPerStatus - Number of bits per status entry (1, 2, 4, or 8)
|
|
333
|
+
* @returns New StatusList instance
|
|
334
|
+
* @throws {InvalidBitSizeError} If bitsPerStatus is not 1, 2, 4, or 8
|
|
335
|
+
* @throws {InvalidStatusValueError} If any status value exceeds the maximum for the bit size
|
|
336
|
+
*
|
|
337
|
+
* @example
|
|
338
|
+
* ```typescript
|
|
339
|
+
* // Create list with 1-bit statuses (valid/invalid)
|
|
340
|
+
* const statuses = new Array(10000).fill(0); // All valid
|
|
341
|
+
* statuses[42] = 1; // Revoke one credential
|
|
342
|
+
* const list = StatusList.fromArray(statuses, 1);
|
|
343
|
+
*
|
|
344
|
+
* // Create list with 2-bit statuses (valid/invalid/suspended/custom)
|
|
345
|
+
* const statuses2 = [0, 1, 2, 0, 1, 3];
|
|
346
|
+
* const list2 = StatusList.fromArray(statuses2, 2);
|
|
347
|
+
* ```
|
|
348
|
+
*/
|
|
349
|
+
static fromArray(statuses, bitsPerStatus) {
|
|
350
|
+
const bytes = packBits(statuses, bitsPerStatus);
|
|
351
|
+
return new _StatusList(bytes, bitsPerStatus);
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* Creates a StatusList by decompressing a base64url-encoded string (from JWT).
|
|
355
|
+
*
|
|
356
|
+
* @param compressed - Base64URL-encoded compressed status list
|
|
357
|
+
* @param bitsPerStatus - Number of bits per status entry
|
|
358
|
+
* @returns New StatusList instance
|
|
359
|
+
* @throws {CompressionError} If decompression fails
|
|
360
|
+
*
|
|
361
|
+
* @example
|
|
362
|
+
* ```typescript
|
|
363
|
+
* // From JWT payload
|
|
364
|
+
* const payload = parseJWT(token);
|
|
365
|
+
* const list = StatusList.decompressFromBase64URL(
|
|
366
|
+
* payload.status_list.lst,
|
|
367
|
+
* payload.status_list.bits
|
|
368
|
+
* );
|
|
369
|
+
* ```
|
|
370
|
+
*/
|
|
371
|
+
static decompressFromBase64URL(compressed, bitsPerStatus) {
|
|
372
|
+
const bytes = decompressFromBase64URL(compressed);
|
|
373
|
+
return new _StatusList(bytes, bitsPerStatus);
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* Creates a StatusList by decompressing raw bytes (from CWT).
|
|
377
|
+
*
|
|
378
|
+
* @param compressed - Compressed status list as bytes
|
|
379
|
+
* @param bitsPerStatus - Number of bits per status entry
|
|
380
|
+
* @returns New StatusList instance
|
|
381
|
+
* @throws {CompressionError} If decompression fails
|
|
382
|
+
*
|
|
383
|
+
* @example
|
|
384
|
+
* ```typescript
|
|
385
|
+
* // From CWT payload
|
|
386
|
+
* const payload = parseCWT(token);
|
|
387
|
+
* const statusListData = payload[65533]; // status_list claim
|
|
388
|
+
* const list = StatusList.decompressFromBytes(
|
|
389
|
+
* statusListData.lst,
|
|
390
|
+
* statusListData.bits
|
|
391
|
+
* );
|
|
392
|
+
* ```
|
|
393
|
+
*/
|
|
394
|
+
static decompressFromBytes(compressed, bitsPerStatus) {
|
|
395
|
+
const bytes = decompress(compressed);
|
|
396
|
+
return new _StatusList(bytes, bitsPerStatus);
|
|
397
|
+
}
|
|
398
|
+
/**
|
|
399
|
+
* Creates an empty StatusList with a specified capacity.
|
|
400
|
+
*
|
|
401
|
+
* All status entries are initialized to 0 (VALID).
|
|
402
|
+
*
|
|
403
|
+
* @param capacity - Number of status entries
|
|
404
|
+
* @param bitsPerStatus - Number of bits per status entry (1, 2, 4, or 8)
|
|
405
|
+
* @returns New StatusList instance
|
|
406
|
+
*
|
|
407
|
+
* @example
|
|
408
|
+
* ```typescript
|
|
409
|
+
* // Create empty list for 100,000 credentials
|
|
410
|
+
* const list = StatusList.create(100000, 1);
|
|
411
|
+
* ```
|
|
412
|
+
*/
|
|
413
|
+
static create(capacity, bitsPerStatus) {
|
|
414
|
+
const totalBits = capacity * bitsPerStatus;
|
|
415
|
+
const byteLength = Math.ceil(totalBits / 8);
|
|
416
|
+
const bytes = new Uint8Array(byteLength);
|
|
417
|
+
return new _StatusList(bytes, bitsPerStatus);
|
|
418
|
+
}
|
|
419
|
+
/**
|
|
420
|
+
* Converts the status list to an array of status values (for debugging/testing).
|
|
421
|
+
*
|
|
422
|
+
* Warning: For large status lists, this can be memory-intensive.
|
|
423
|
+
*
|
|
424
|
+
* @returns Array of all status values
|
|
425
|
+
*/
|
|
426
|
+
toArray() {
|
|
427
|
+
return unpackBits(this.statusArray, this.bitsPerStatus);
|
|
428
|
+
}
|
|
429
|
+
};
|
|
430
|
+
|
|
431
|
+
// lib/types/common.ts
|
|
432
|
+
var StandardStatusValues = /* @__PURE__ */ ((StandardStatusValues2) => {
|
|
433
|
+
StandardStatusValues2[StandardStatusValues2["VALID"] = 0] = "VALID";
|
|
434
|
+
StandardStatusValues2[StandardStatusValues2["INVALID"] = 1] = "INVALID";
|
|
435
|
+
StandardStatusValues2[StandardStatusValues2["SUSPENDED"] = 2] = "SUSPENDED";
|
|
436
|
+
StandardStatusValues2[StandardStatusValues2["APPLICATION_SPECIFIC"] = 3] = "APPLICATION_SPECIFIC";
|
|
437
|
+
return StandardStatusValues2;
|
|
438
|
+
})(StandardStatusValues || {});
|
|
439
|
+
|
|
440
|
+
// lib/formats/jwt/StatusListJWT.ts
|
|
441
|
+
function createJWTStatusListPayload(options) {
|
|
442
|
+
const {
|
|
443
|
+
iss,
|
|
444
|
+
sub,
|
|
445
|
+
lst,
|
|
446
|
+
bits,
|
|
447
|
+
iat = Math.floor(Date.now() / 1e3),
|
|
448
|
+
exp,
|
|
449
|
+
ttl,
|
|
450
|
+
aggregation_uri,
|
|
451
|
+
additionalClaims = {}
|
|
452
|
+
} = options;
|
|
453
|
+
const header = {
|
|
454
|
+
typ: "statuslist+jwt",
|
|
455
|
+
alg: "ES256"
|
|
456
|
+
// Default algorithm, can be overridden when signing
|
|
457
|
+
};
|
|
458
|
+
const payload = {
|
|
459
|
+
iss,
|
|
460
|
+
sub,
|
|
461
|
+
iat,
|
|
462
|
+
status_list: {
|
|
463
|
+
bits,
|
|
464
|
+
lst,
|
|
465
|
+
...aggregation_uri && { aggregation_uri }
|
|
466
|
+
},
|
|
467
|
+
...additionalClaims
|
|
468
|
+
};
|
|
469
|
+
if (exp !== void 0) {
|
|
470
|
+
payload.exp = exp;
|
|
471
|
+
}
|
|
472
|
+
if (ttl !== void 0) {
|
|
473
|
+
payload.ttl = ttl;
|
|
474
|
+
}
|
|
475
|
+
return { header, payload };
|
|
476
|
+
}
|
|
477
|
+
function parseJWTStatusList(jwt) {
|
|
478
|
+
const parts = jwt.split(".");
|
|
479
|
+
if (parts.length !== 3) {
|
|
480
|
+
throw new InvalidTokenFormatError("JWT must have 3 parts separated by dots");
|
|
481
|
+
}
|
|
482
|
+
try {
|
|
483
|
+
const headerJson = base64UrlDecode2(parts[0]);
|
|
484
|
+
const header = JSON.parse(headerJson);
|
|
485
|
+
if (header.typ !== "statuslist+jwt") {
|
|
486
|
+
throw new InvalidTokenFormatError(`Invalid typ header: expected "statuslist+jwt", got "${header.typ}"`);
|
|
487
|
+
}
|
|
488
|
+
const payloadJson = base64UrlDecode2(parts[1]);
|
|
489
|
+
const payload = JSON.parse(payloadJson);
|
|
490
|
+
validateJWTPayload(payload);
|
|
491
|
+
const statusList = StatusList.decompressFromBase64URL(payload.status_list.lst, payload.status_list.bits);
|
|
492
|
+
return {
|
|
493
|
+
header,
|
|
494
|
+
payload,
|
|
495
|
+
jwt,
|
|
496
|
+
statusList
|
|
497
|
+
};
|
|
498
|
+
} catch (error) {
|
|
499
|
+
if (error instanceof InvalidTokenFormatError) {
|
|
500
|
+
throw error;
|
|
501
|
+
}
|
|
502
|
+
throw new InvalidTokenFormatError(`Failed to parse JWT: ${error.message}`);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
function extractStatusListReference(credentialJWT) {
|
|
506
|
+
const parts = credentialJWT.split(".");
|
|
507
|
+
if (parts.length !== 3) {
|
|
508
|
+
throw new InvalidTokenFormatError("JWT must have 3 parts");
|
|
509
|
+
}
|
|
510
|
+
try {
|
|
511
|
+
const payloadJson = base64UrlDecode2(parts[1]);
|
|
512
|
+
const payload = JSON.parse(payloadJson);
|
|
513
|
+
if (!payload.status || typeof payload.status !== "object") {
|
|
514
|
+
throw new InvalidTokenFormatError("Missing status claim in credential");
|
|
515
|
+
}
|
|
516
|
+
const status = payload.status;
|
|
517
|
+
if (!status.status_list || typeof status.status_list !== "object") {
|
|
518
|
+
throw new InvalidTokenFormatError("Missing status_list in status claim");
|
|
519
|
+
}
|
|
520
|
+
const statusList = status.status_list;
|
|
521
|
+
if (typeof statusList.idx !== "number" || statusList.idx < 0) {
|
|
522
|
+
throw new InvalidTokenFormatError("Invalid or missing idx in status_list");
|
|
523
|
+
}
|
|
524
|
+
if (typeof statusList.uri !== "string" || !statusList.uri) {
|
|
525
|
+
throw new InvalidTokenFormatError("Invalid or missing uri in status_list");
|
|
526
|
+
}
|
|
527
|
+
return {
|
|
528
|
+
idx: statusList.idx,
|
|
529
|
+
uri: statusList.uri
|
|
530
|
+
};
|
|
531
|
+
} catch (error) {
|
|
532
|
+
if (error instanceof InvalidTokenFormatError) {
|
|
533
|
+
throw error;
|
|
534
|
+
}
|
|
535
|
+
throw new InvalidTokenFormatError(`Failed to extract status reference: ${error.message}`);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
function validateJWTPayload(payload) {
|
|
539
|
+
if (!payload.iss || typeof payload.iss !== "string") {
|
|
540
|
+
throw new InvalidTokenFormatError('Missing or invalid "iss" claim');
|
|
541
|
+
}
|
|
542
|
+
if (!payload.sub || typeof payload.sub !== "string") {
|
|
543
|
+
throw new InvalidTokenFormatError('Missing or invalid "sub" claim');
|
|
544
|
+
}
|
|
545
|
+
if (typeof payload.iat !== "number") {
|
|
546
|
+
throw new InvalidTokenFormatError('Missing or invalid "iat" claim');
|
|
547
|
+
}
|
|
548
|
+
if (!payload.status_list || typeof payload.status_list !== "object") {
|
|
549
|
+
throw new InvalidTokenFormatError('Missing or invalid "status_list" claim');
|
|
550
|
+
}
|
|
551
|
+
const { bits, lst } = payload.status_list;
|
|
552
|
+
if (![1, 2, 4, 8].includes(bits)) {
|
|
553
|
+
throw new InvalidTokenFormatError(`Invalid "bits" value: must be 1, 2, 4, or 8, got ${bits}`);
|
|
554
|
+
}
|
|
555
|
+
if (!lst || typeof lst !== "string") {
|
|
556
|
+
throw new InvalidTokenFormatError('Missing or invalid "lst" field in status_list');
|
|
557
|
+
}
|
|
558
|
+
if (payload.exp !== void 0 && typeof payload.exp !== "number") {
|
|
559
|
+
throw new InvalidTokenFormatError('Invalid "exp" claim: must be a number');
|
|
560
|
+
}
|
|
561
|
+
if (payload.ttl !== void 0 && typeof payload.ttl !== "number") {
|
|
562
|
+
throw new InvalidTokenFormatError('Invalid "ttl" claim: must be a number');
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
function base64UrlDecode2(base64url) {
|
|
566
|
+
let base64 = base64url.replace(/-/g, "+").replace(/_/g, "/");
|
|
567
|
+
const padding = base64.length % 4;
|
|
568
|
+
if (padding > 0) {
|
|
569
|
+
base64 += "=".repeat(4 - padding);
|
|
570
|
+
}
|
|
571
|
+
if (typeof Buffer !== "undefined") {
|
|
572
|
+
return Buffer.from(base64, "base64").toString("utf-8");
|
|
573
|
+
} else {
|
|
574
|
+
return decodeURIComponent(
|
|
575
|
+
atob(base64).split("").map((c) => "%" + ("00" + c.charCodeAt(0).toString(16)).slice(-2)).join("")
|
|
576
|
+
);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
async function signStatusListJWT(payload, privateKey, options) {
|
|
580
|
+
const { alg = "ES256", kid, additionalHeaders = {} } = options || {};
|
|
581
|
+
const jwt = new SignJWT(payload).setProtectedHeader({
|
|
582
|
+
typ: "statuslist+jwt",
|
|
583
|
+
alg,
|
|
584
|
+
...kid && { kid },
|
|
585
|
+
...additionalHeaders
|
|
586
|
+
}).setIssuer(payload.iss).setSubject(payload.sub).setIssuedAt(payload.iat);
|
|
587
|
+
if (payload.exp !== void 0) {
|
|
588
|
+
jwt.setExpirationTime(payload.exp);
|
|
589
|
+
}
|
|
590
|
+
return await jwt.sign(privateKey);
|
|
591
|
+
}
|
|
592
|
+
async function verifyStatusListJWT(jwt, publicKey, options) {
|
|
593
|
+
const { issuer, subject, currentTime, clockTolerance = 0 } = options || {};
|
|
594
|
+
try {
|
|
595
|
+
const result = await jwtVerify(jwt, publicKey, {
|
|
596
|
+
typ: "statuslist+jwt",
|
|
597
|
+
...issuer && { issuer },
|
|
598
|
+
...subject && { subject },
|
|
599
|
+
...currentTime && { currentDate: new Date(currentTime * 1e3) },
|
|
600
|
+
clockTolerance
|
|
601
|
+
});
|
|
602
|
+
const header = result.protectedHeader;
|
|
603
|
+
const payload = result.payload;
|
|
604
|
+
if (header.typ !== "statuslist+jwt") {
|
|
605
|
+
throw new InvalidTokenFormatError(`Invalid typ header: expected "statuslist+jwt", got "${header.typ}"`);
|
|
606
|
+
}
|
|
607
|
+
if (!payload.iss || typeof payload.iss !== "string") {
|
|
608
|
+
throw new InvalidTokenFormatError('Missing or invalid "iss" claim');
|
|
609
|
+
}
|
|
610
|
+
if (!payload.sub || typeof payload.sub !== "string") {
|
|
611
|
+
throw new InvalidTokenFormatError('Missing or invalid "sub" claim');
|
|
612
|
+
}
|
|
613
|
+
if (typeof payload.iat !== "number") {
|
|
614
|
+
throw new InvalidTokenFormatError('Missing or invalid "iat" claim');
|
|
615
|
+
}
|
|
616
|
+
if (!payload.status_list || typeof payload.status_list !== "object") {
|
|
617
|
+
throw new InvalidTokenFormatError('Missing or invalid "status_list" claim');
|
|
618
|
+
}
|
|
619
|
+
const { bits, lst } = payload.status_list;
|
|
620
|
+
if (![1, 2, 4, 8].includes(bits)) {
|
|
621
|
+
throw new InvalidTokenFormatError(`Invalid "bits" value: must be 1, 2, 4, or 8, got ${bits}`);
|
|
622
|
+
}
|
|
623
|
+
if (!lst || typeof lst !== "string") {
|
|
624
|
+
throw new InvalidTokenFormatError('Missing or invalid "lst" field in status_list');
|
|
625
|
+
}
|
|
626
|
+
return {
|
|
627
|
+
header,
|
|
628
|
+
payload
|
|
629
|
+
};
|
|
630
|
+
} catch (error) {
|
|
631
|
+
if (error instanceof InvalidTokenFormatError) {
|
|
632
|
+
throw error;
|
|
633
|
+
}
|
|
634
|
+
const message = error.message;
|
|
635
|
+
throw new InvalidTokenFormatError(`JWT verification failed: ${message}`);
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
async function verifySignatureOnly(jwt, publicKey) {
|
|
639
|
+
try {
|
|
640
|
+
const result = await jwtVerify(jwt, publicKey, {
|
|
641
|
+
typ: "statuslist+jwt"
|
|
642
|
+
});
|
|
643
|
+
return {
|
|
644
|
+
header: result.protectedHeader,
|
|
645
|
+
payload: result.payload
|
|
646
|
+
};
|
|
647
|
+
} catch (error) {
|
|
648
|
+
throw new InvalidTokenFormatError(`Signature verification failed: ${error.message}`);
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
// lib/formats/cwt/types.ts
|
|
653
|
+
var CWT_CLAIMS = {
|
|
654
|
+
/** Issuer (iss) - Claim key 1 */
|
|
655
|
+
ISS: 1,
|
|
656
|
+
/** Subject (sub) - Claim key 2 */
|
|
657
|
+
SUB: 2,
|
|
658
|
+
/** Expiration Time (exp) - Claim key 4 */
|
|
659
|
+
EXP: 4,
|
|
660
|
+
/** Issued At (iat) - Claim key 6 */
|
|
661
|
+
IAT: 6,
|
|
662
|
+
/** Time to Live (ttl) - Claim key 65534 (custom) */
|
|
663
|
+
TTL: 65534,
|
|
664
|
+
/** Status List - Claim key 65533 (custom) */
|
|
665
|
+
STATUS_LIST: 65533
|
|
666
|
+
};
|
|
667
|
+
var COSE_HEADERS = {
|
|
668
|
+
/** Algorithm - Header parameter 1 */
|
|
669
|
+
ALG: 1,
|
|
670
|
+
/** Content Type - Header parameter 16 */
|
|
671
|
+
CONTENT_TYPE: 16,
|
|
672
|
+
/** Key ID - Header parameter 4 */
|
|
673
|
+
KID: 4
|
|
674
|
+
};
|
|
675
|
+
var COSE_ALGORITHMS = {
|
|
676
|
+
/** ES256 - ECDSA with SHA-256 */
|
|
677
|
+
ES256: -7,
|
|
678
|
+
/** ES384 - ECDSA with SHA-384 */
|
|
679
|
+
ES384: -35,
|
|
680
|
+
/** ES512 - ECDSA with SHA-512 */
|
|
681
|
+
ES512: -36,
|
|
682
|
+
/** EdDSA */
|
|
683
|
+
EdDSA: -8
|
|
684
|
+
};
|
|
685
|
+
var COSE_SIGN1_TAG = 18;
|
|
686
|
+
var ALGORITHM_MAP = {
|
|
687
|
+
"-7": { hash: "sha256", curve: "prime256v1" },
|
|
688
|
+
// ES256
|
|
689
|
+
"-35": { hash: "sha384", curve: "secp384r1" },
|
|
690
|
+
// ES384
|
|
691
|
+
"-36": { hash: "sha512", curve: "secp521r1" },
|
|
692
|
+
// ES512
|
|
693
|
+
"-8": { hash: "sha512" }
|
|
694
|
+
// EdDSA (Ed25519)
|
|
695
|
+
};
|
|
696
|
+
function signCOSE(payload, protectedHeader, privateKey) {
|
|
697
|
+
const protectedHeaderEncoded = cbor.encode(protectedHeader);
|
|
698
|
+
const sigStructure = cbor.encode([
|
|
699
|
+
"Signature1",
|
|
700
|
+
// Context
|
|
701
|
+
protectedHeaderEncoded,
|
|
702
|
+
new Uint8Array(0),
|
|
703
|
+
// External AAD (empty)
|
|
704
|
+
payload
|
|
705
|
+
]);
|
|
706
|
+
const alg = protectedHeader.get(1);
|
|
707
|
+
if (!alg) {
|
|
708
|
+
throw new InvalidTokenFormatError("Algorithm (alg) must be specified in protected header");
|
|
709
|
+
}
|
|
710
|
+
const signature = signData(sigStructure, alg, privateKey);
|
|
711
|
+
const coseSign1 = [
|
|
712
|
+
protectedHeaderEncoded,
|
|
713
|
+
// Protected header (encoded)
|
|
714
|
+
{},
|
|
715
|
+
// Unprotected header (empty map)
|
|
716
|
+
payload,
|
|
717
|
+
// Payload
|
|
718
|
+
signature
|
|
719
|
+
// Signature
|
|
720
|
+
];
|
|
721
|
+
const tagged = new cbor.Tagged(COSE_SIGN1_TAG, coseSign1);
|
|
722
|
+
return cbor.encode(tagged);
|
|
723
|
+
}
|
|
724
|
+
function verifyCOSE(coseSign1, publicKey) {
|
|
725
|
+
try {
|
|
726
|
+
const decoded = cbor.decode(coseSign1);
|
|
727
|
+
if (!(decoded instanceof cbor.Tagged) || decoded.tag !== COSE_SIGN1_TAG) {
|
|
728
|
+
throw new InvalidTokenFormatError("Invalid COSE Sign1 structure: missing tag 18");
|
|
729
|
+
}
|
|
730
|
+
const [protectedHeaderEncoded, , payload, signature] = decoded.value;
|
|
731
|
+
const protectedHeader = cbor.decode(protectedHeaderEncoded);
|
|
732
|
+
const alg = protectedHeader.get(1);
|
|
733
|
+
if (!alg) {
|
|
734
|
+
throw new InvalidTokenFormatError("Missing algorithm in protected header");
|
|
735
|
+
}
|
|
736
|
+
const sigStructure = cbor.encode([
|
|
737
|
+
"Signature1",
|
|
738
|
+
protectedHeaderEncoded,
|
|
739
|
+
new Uint8Array(0),
|
|
740
|
+
// External AAD
|
|
741
|
+
payload
|
|
742
|
+
]);
|
|
743
|
+
const valid = verifySignature(sigStructure, signature, alg, publicKey);
|
|
744
|
+
if (!valid) {
|
|
745
|
+
throw new InvalidTokenFormatError("Invalid COSE Sign1 signature");
|
|
746
|
+
}
|
|
747
|
+
return payload;
|
|
748
|
+
} catch (error) {
|
|
749
|
+
if (error instanceof InvalidTokenFormatError) {
|
|
750
|
+
throw error;
|
|
751
|
+
}
|
|
752
|
+
throw new InvalidTokenFormatError(`Failed to verify COSE Sign1: ${error.message}`);
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
function signData(data, alg, privateKey) {
|
|
756
|
+
const algInfo = ALGORITHM_MAP[alg.toString()];
|
|
757
|
+
if (!algInfo) {
|
|
758
|
+
throw new InvalidTokenFormatError(`Unsupported algorithm: ${alg}`);
|
|
759
|
+
}
|
|
760
|
+
if (privateKey.kty === "EC2") {
|
|
761
|
+
if (!privateKey.d) {
|
|
762
|
+
throw new InvalidTokenFormatError("Private key (d) is required for signing");
|
|
763
|
+
}
|
|
764
|
+
const pemKey = ec2KeyToPEM();
|
|
765
|
+
const sign = createSign(`RSA-SHA${algInfo.hash.replace("sha", "")}`);
|
|
766
|
+
sign.update(data);
|
|
767
|
+
const signature = sign.sign(pemKey);
|
|
768
|
+
return derToRaw(signature, getSignatureLength(alg));
|
|
769
|
+
} else if (privateKey.kty === "OKP") {
|
|
770
|
+
if (!privateKey.d) {
|
|
771
|
+
throw new InvalidTokenFormatError("Private key (d) is required for signing");
|
|
772
|
+
}
|
|
773
|
+
const pemKey = okpKeyToPEM();
|
|
774
|
+
const sign = createSign("SHA512");
|
|
775
|
+
sign.update(data);
|
|
776
|
+
return sign.sign(pemKey);
|
|
777
|
+
}
|
|
778
|
+
const _exhaustiveCheck = privateKey;
|
|
779
|
+
throw new InvalidTokenFormatError(`Unsupported key type: ${_exhaustiveCheck.kty}`);
|
|
780
|
+
}
|
|
781
|
+
function verifySignature(data, signature, alg, publicKey) {
|
|
782
|
+
const algInfo = ALGORITHM_MAP[alg.toString()];
|
|
783
|
+
if (!algInfo) {
|
|
784
|
+
throw new InvalidTokenFormatError(`Unsupported algorithm: ${alg}`);
|
|
785
|
+
}
|
|
786
|
+
if (publicKey.kty === "EC2") {
|
|
787
|
+
const pemKey = ec2KeyToPEM();
|
|
788
|
+
const derSignature = rawToDer(signature);
|
|
789
|
+
const verify = createVerify(`RSA-SHA${algInfo.hash.replace("sha", "")}`);
|
|
790
|
+
verify.update(data);
|
|
791
|
+
return verify.verify(pemKey, derSignature);
|
|
792
|
+
} else if (publicKey.kty === "OKP") {
|
|
793
|
+
const pemKey = okpKeyToPEM();
|
|
794
|
+
const verify = createVerify("SHA512");
|
|
795
|
+
verify.update(data);
|
|
796
|
+
return verify.verify(pemKey, signature);
|
|
797
|
+
}
|
|
798
|
+
const _exhaustiveCheck = publicKey;
|
|
799
|
+
throw new InvalidTokenFormatError(`Unsupported key type: ${_exhaustiveCheck.kty}`);
|
|
800
|
+
}
|
|
801
|
+
function ec2KeyToPEM(_key, _isPrivate) {
|
|
802
|
+
throw new Error("EC2 to PEM conversion not fully implemented. Use a COSE library like @transmute/cose instead.");
|
|
803
|
+
}
|
|
804
|
+
function okpKeyToPEM(_key, _isPrivate) {
|
|
805
|
+
throw new Error("OKP to PEM conversion not fully implemented. Use a COSE library like @transmute/cose instead.");
|
|
806
|
+
}
|
|
807
|
+
function derToRaw(derSignature, length) {
|
|
808
|
+
let offset = 2;
|
|
809
|
+
const rLength = derSignature[offset + 1];
|
|
810
|
+
offset += 2;
|
|
811
|
+
const r = derSignature.slice(offset, offset + rLength);
|
|
812
|
+
offset += rLength;
|
|
813
|
+
const sLength = derSignature[offset + 1];
|
|
814
|
+
offset += 2;
|
|
815
|
+
const s = derSignature.slice(offset, offset + sLength);
|
|
816
|
+
const halfLength = length / 2;
|
|
817
|
+
const raw = new Uint8Array(length);
|
|
818
|
+
raw.set(r.slice(-halfLength), 0);
|
|
819
|
+
raw.set(s.slice(-halfLength), halfLength);
|
|
820
|
+
return raw;
|
|
821
|
+
}
|
|
822
|
+
function rawToDer(rawSignature) {
|
|
823
|
+
const halfLength = rawSignature.length / 2;
|
|
824
|
+
const r = rawSignature.slice(0, halfLength);
|
|
825
|
+
const s = rawSignature.slice(halfLength);
|
|
826
|
+
const derR = encodeDerInteger(r);
|
|
827
|
+
const derS = encodeDerInteger(s);
|
|
828
|
+
const sequence = new Uint8Array(2 + derR.length + derS.length);
|
|
829
|
+
sequence[0] = 48;
|
|
830
|
+
sequence[1] = derR.length + derS.length;
|
|
831
|
+
sequence.set(derR, 2);
|
|
832
|
+
sequence.set(derS, 2 + derR.length);
|
|
833
|
+
return sequence;
|
|
834
|
+
}
|
|
835
|
+
function encodeDerInteger(value) {
|
|
836
|
+
let i = 0;
|
|
837
|
+
while (i < value.length && value[i] === 0) {
|
|
838
|
+
i++;
|
|
839
|
+
}
|
|
840
|
+
value = value.slice(i);
|
|
841
|
+
const needsLeadingZero = value[0] >= 128;
|
|
842
|
+
const length = value.length + (needsLeadingZero ? 1 : 0);
|
|
843
|
+
const der = new Uint8Array(2 + length);
|
|
844
|
+
der[0] = 2;
|
|
845
|
+
der[1] = length;
|
|
846
|
+
if (needsLeadingZero) {
|
|
847
|
+
der[2] = 0;
|
|
848
|
+
der.set(value, 3);
|
|
849
|
+
} else {
|
|
850
|
+
der.set(value, 2);
|
|
851
|
+
}
|
|
852
|
+
return der;
|
|
853
|
+
}
|
|
854
|
+
function getSignatureLength(alg) {
|
|
855
|
+
switch (alg) {
|
|
856
|
+
case -7:
|
|
857
|
+
return 64;
|
|
858
|
+
case -35:
|
|
859
|
+
return 96;
|
|
860
|
+
case -36:
|
|
861
|
+
return 132;
|
|
862
|
+
case -8:
|
|
863
|
+
return 64;
|
|
864
|
+
default:
|
|
865
|
+
throw new InvalidTokenFormatError(`Unknown signature length for algorithm: ${alg}`);
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
// lib/formats/cwt/StatusListCWT.ts
|
|
870
|
+
function createCWTStatusListPayload(options) {
|
|
871
|
+
const {
|
|
872
|
+
iss,
|
|
873
|
+
sub,
|
|
874
|
+
lst,
|
|
875
|
+
bits,
|
|
876
|
+
iat = Math.floor(Date.now() / 1e3),
|
|
877
|
+
exp,
|
|
878
|
+
ttl,
|
|
879
|
+
aggregation_uri,
|
|
880
|
+
additionalClaims = {}
|
|
881
|
+
} = options;
|
|
882
|
+
const payload = {
|
|
883
|
+
...additionalClaims,
|
|
884
|
+
[CWT_CLAIMS.STATUS_LIST]: {
|
|
885
|
+
bits,
|
|
886
|
+
lst,
|
|
887
|
+
...aggregation_uri && { aggregation_uri }
|
|
888
|
+
}
|
|
889
|
+
};
|
|
890
|
+
if (iss !== void 0) {
|
|
891
|
+
payload[CWT_CLAIMS.ISS] = iss;
|
|
892
|
+
}
|
|
893
|
+
if (sub !== void 0) {
|
|
894
|
+
payload[CWT_CLAIMS.SUB] = sub;
|
|
895
|
+
}
|
|
896
|
+
if (iat !== void 0) {
|
|
897
|
+
payload[CWT_CLAIMS.IAT] = iat;
|
|
898
|
+
}
|
|
899
|
+
if (exp !== void 0) {
|
|
900
|
+
payload[CWT_CLAIMS.EXP] = exp;
|
|
901
|
+
}
|
|
902
|
+
if (ttl !== void 0) {
|
|
903
|
+
payload[CWT_CLAIMS.TTL] = ttl;
|
|
904
|
+
}
|
|
905
|
+
return payload;
|
|
906
|
+
}
|
|
907
|
+
function encodeCWTPayload(payload) {
|
|
908
|
+
return cbor.encode(payload);
|
|
909
|
+
}
|
|
910
|
+
function parseCWTStatusList(cwtBytes) {
|
|
911
|
+
try {
|
|
912
|
+
const payload = cbor.decode(cwtBytes);
|
|
913
|
+
validateCWTPayload(payload);
|
|
914
|
+
const typedPayload = payload;
|
|
915
|
+
const statusListClaim = typedPayload[CWT_CLAIMS.STATUS_LIST];
|
|
916
|
+
const statusList = StatusList.decompressFromBytes(statusListClaim.lst, statusListClaim.bits);
|
|
917
|
+
return {
|
|
918
|
+
protectedHeader: /* @__PURE__ */ new Map(),
|
|
919
|
+
unprotectedHeader: /* @__PURE__ */ new Map(),
|
|
920
|
+
payload: typedPayload,
|
|
921
|
+
cwt: cwtBytes,
|
|
922
|
+
statusList
|
|
923
|
+
};
|
|
924
|
+
} catch (error) {
|
|
925
|
+
if (error instanceof InvalidTokenFormatError) {
|
|
926
|
+
throw error;
|
|
927
|
+
}
|
|
928
|
+
throw new InvalidTokenFormatError(`Failed to parse CWT: ${error.message}`);
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
function parseCWTStatusListSigned(cwtBytes, publicKey) {
|
|
932
|
+
try {
|
|
933
|
+
const payloadBytes = verifyCOSE(cwtBytes, publicKey);
|
|
934
|
+
const payload = cbor.decode(payloadBytes);
|
|
935
|
+
validateCWTPayload(payload);
|
|
936
|
+
const typedPayload = payload;
|
|
937
|
+
const statusListClaim = typedPayload[CWT_CLAIMS.STATUS_LIST];
|
|
938
|
+
const statusList = StatusList.decompressFromBytes(statusListClaim.lst, statusListClaim.bits);
|
|
939
|
+
const decoded = cbor.decode(cwtBytes);
|
|
940
|
+
const [protectedHeaderEncoded] = decoded.value;
|
|
941
|
+
const protectedHeader = cbor.decode(protectedHeaderEncoded);
|
|
942
|
+
return {
|
|
943
|
+
protectedHeader,
|
|
944
|
+
unprotectedHeader: /* @__PURE__ */ new Map(),
|
|
945
|
+
payload: typedPayload,
|
|
946
|
+
cwt: cwtBytes,
|
|
947
|
+
statusList
|
|
948
|
+
};
|
|
949
|
+
} catch (error) {
|
|
950
|
+
if (error instanceof InvalidTokenFormatError) {
|
|
951
|
+
throw error;
|
|
952
|
+
}
|
|
953
|
+
throw new InvalidTokenFormatError(`Failed to parse signed CWT: ${error.message}`);
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
function signCWTStatusList(payload, privateKey, options) {
|
|
957
|
+
const { alg = COSE_ALGORITHMS.ES256, kid, additionalHeaders = /* @__PURE__ */ new Map() } = options || {};
|
|
958
|
+
const protectedHeader = new Map([
|
|
959
|
+
[COSE_HEADERS.CONTENT_TYPE, "application/statuslist+cwt"],
|
|
960
|
+
[COSE_HEADERS.ALG, alg],
|
|
961
|
+
...additionalHeaders
|
|
962
|
+
]);
|
|
963
|
+
if (kid) {
|
|
964
|
+
protectedHeader.set(COSE_HEADERS.KID, kid);
|
|
965
|
+
}
|
|
966
|
+
const payloadBytes = cbor.encode(payload);
|
|
967
|
+
return signCOSE(payloadBytes, protectedHeader, privateKey);
|
|
968
|
+
}
|
|
969
|
+
function extractStatusListReferenceCBOR(credentialCBOR) {
|
|
970
|
+
try {
|
|
971
|
+
const credential = cbor.decode(credentialCBOR);
|
|
972
|
+
const status = credential.status || credential["status"];
|
|
973
|
+
if (!status || typeof status !== "object") {
|
|
974
|
+
throw new InvalidTokenFormatError("Missing status claim in credential");
|
|
975
|
+
}
|
|
976
|
+
const statusObj = status;
|
|
977
|
+
const statusList = statusObj.status_list || statusObj["status_list"];
|
|
978
|
+
if (!statusList || typeof statusList !== "object") {
|
|
979
|
+
throw new InvalidTokenFormatError("Missing status_list in status claim");
|
|
980
|
+
}
|
|
981
|
+
const statusListObj = statusList;
|
|
982
|
+
if (typeof statusListObj.idx !== "number" || statusListObj.idx < 0) {
|
|
983
|
+
throw new InvalidTokenFormatError("Invalid or missing idx in status_list");
|
|
984
|
+
}
|
|
985
|
+
if (typeof statusListObj.uri !== "string" || !statusListObj.uri) {
|
|
986
|
+
throw new InvalidTokenFormatError("Invalid or missing uri in status_list");
|
|
987
|
+
}
|
|
988
|
+
return {
|
|
989
|
+
idx: statusListObj.idx,
|
|
990
|
+
uri: statusListObj.uri
|
|
991
|
+
};
|
|
992
|
+
} catch (error) {
|
|
993
|
+
if (error instanceof InvalidTokenFormatError) {
|
|
994
|
+
throw error;
|
|
995
|
+
}
|
|
996
|
+
throw new InvalidTokenFormatError(`Failed to extract status reference: ${error.message}`);
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
function validateCWTPayload(payload) {
|
|
1000
|
+
const statusList = payload[CWT_CLAIMS.STATUS_LIST];
|
|
1001
|
+
if (!statusList || typeof statusList !== "object") {
|
|
1002
|
+
throw new InvalidTokenFormatError(`Missing or invalid status_list claim (key ${CWT_CLAIMS.STATUS_LIST})`);
|
|
1003
|
+
}
|
|
1004
|
+
const statusListObj = statusList;
|
|
1005
|
+
if (!statusListObj.bits || ![1, 2, 4, 8].includes(statusListObj.bits)) {
|
|
1006
|
+
throw new InvalidTokenFormatError(`Invalid "bits" value: must be 1, 2, 4, or 8`);
|
|
1007
|
+
}
|
|
1008
|
+
if (!statusListObj.lst || !(statusListObj.lst instanceof Uint8Array)) {
|
|
1009
|
+
throw new InvalidTokenFormatError('Missing or invalid "lst" field in status_list');
|
|
1010
|
+
}
|
|
1011
|
+
if (payload[CWT_CLAIMS.ISS] !== void 0 && typeof payload[CWT_CLAIMS.ISS] !== "string") {
|
|
1012
|
+
throw new InvalidTokenFormatError(`Invalid "iss" claim (key ${CWT_CLAIMS.ISS}): must be a string`);
|
|
1013
|
+
}
|
|
1014
|
+
if (payload[CWT_CLAIMS.SUB] !== void 0 && typeof payload[CWT_CLAIMS.SUB] !== "string") {
|
|
1015
|
+
throw new InvalidTokenFormatError(`Invalid "sub" claim (key ${CWT_CLAIMS.SUB}): must be a string`);
|
|
1016
|
+
}
|
|
1017
|
+
if (payload[CWT_CLAIMS.IAT] !== void 0 && typeof payload[CWT_CLAIMS.IAT] !== "number") {
|
|
1018
|
+
throw new InvalidTokenFormatError(`Invalid "iat" claim (key ${CWT_CLAIMS.IAT}): must be a number`);
|
|
1019
|
+
}
|
|
1020
|
+
if (payload[CWT_CLAIMS.EXP] !== void 0 && typeof payload[CWT_CLAIMS.EXP] !== "number") {
|
|
1021
|
+
throw new InvalidTokenFormatError(`Invalid "exp" claim (key ${CWT_CLAIMS.EXP}): must be a number`);
|
|
1022
|
+
}
|
|
1023
|
+
if (payload[CWT_CLAIMS.TTL] !== void 0 && typeof payload[CWT_CLAIMS.TTL] !== "number") {
|
|
1024
|
+
throw new InvalidTokenFormatError(`Invalid "ttl" claim (key ${CWT_CLAIMS.TTL}): must be a number`);
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
// lib/helper/fetcher.ts
|
|
1029
|
+
var DEFAULT_TIMEOUT = 1e4;
|
|
1030
|
+
var DEFAULT_MAX_REDIRECTS = 3;
|
|
1031
|
+
var CONTENT_TYPES = {
|
|
1032
|
+
JWT: "application/statuslist+jwt",
|
|
1033
|
+
CWT: "application/statuslist+cwt"
|
|
1034
|
+
};
|
|
1035
|
+
async function fetchStatusListToken(uri, options = {}) {
|
|
1036
|
+
if (!uri || typeof uri !== "string") {
|
|
1037
|
+
throw new MissingStatusListUriError("Status list URI is required");
|
|
1038
|
+
}
|
|
1039
|
+
let parsedUri;
|
|
1040
|
+
try {
|
|
1041
|
+
parsedUri = new URL(uri);
|
|
1042
|
+
} catch (error) {
|
|
1043
|
+
throw new MissingStatusListUriError(`Invalid status list URI: ${uri}`);
|
|
1044
|
+
}
|
|
1045
|
+
if (!["http:", "https:"].includes(parsedUri.protocol)) {
|
|
1046
|
+
throw new FetchError(`Unsupported protocol: ${parsedUri.protocol}. Only HTTP(S) is allowed.`);
|
|
1047
|
+
}
|
|
1048
|
+
const {
|
|
1049
|
+
timeout = DEFAULT_TIMEOUT,
|
|
1050
|
+
headers = {},
|
|
1051
|
+
maxRedirects = DEFAULT_MAX_REDIRECTS,
|
|
1052
|
+
fetchImpl = fetch
|
|
1053
|
+
} = options;
|
|
1054
|
+
const controller = new AbortController();
|
|
1055
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
1056
|
+
try {
|
|
1057
|
+
const response = await fetchImpl(uri, {
|
|
1058
|
+
method: "GET",
|
|
1059
|
+
headers: {
|
|
1060
|
+
Accept: `${CONTENT_TYPES.JWT}, ${CONTENT_TYPES.CWT}`,
|
|
1061
|
+
...headers
|
|
1062
|
+
},
|
|
1063
|
+
signal: controller.signal,
|
|
1064
|
+
redirect: "follow",
|
|
1065
|
+
// @ts-expect-error - maxRedirects is not in standard fetch but supported by undici
|
|
1066
|
+
maxRedirects
|
|
1067
|
+
});
|
|
1068
|
+
clearTimeout(timeoutId);
|
|
1069
|
+
if (!response.ok) {
|
|
1070
|
+
throw new FetchError(
|
|
1071
|
+
`HTTP ${response.status} ${response.statusText} while fetching status list from ${uri}`
|
|
1072
|
+
);
|
|
1073
|
+
}
|
|
1074
|
+
const contentType = response.headers.get("content-type");
|
|
1075
|
+
if (contentType?.includes(CONTENT_TYPES.JWT)) {
|
|
1076
|
+
return await response.text();
|
|
1077
|
+
}
|
|
1078
|
+
if (contentType?.includes(CONTENT_TYPES.CWT)) {
|
|
1079
|
+
const arrayBuffer2 = await response.arrayBuffer();
|
|
1080
|
+
return new Uint8Array(arrayBuffer2);
|
|
1081
|
+
}
|
|
1082
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
1083
|
+
const bytes = new Uint8Array(arrayBuffer);
|
|
1084
|
+
const text = new TextDecoder().decode(bytes.slice(0, 3));
|
|
1085
|
+
if (text === "eyJ") {
|
|
1086
|
+
return new TextDecoder().decode(bytes);
|
|
1087
|
+
}
|
|
1088
|
+
return bytes;
|
|
1089
|
+
} catch (error) {
|
|
1090
|
+
clearTimeout(timeoutId);
|
|
1091
|
+
if (error instanceof FetchError || error instanceof MissingStatusListUriError) {
|
|
1092
|
+
throw error;
|
|
1093
|
+
}
|
|
1094
|
+
if (error.name === "AbortError") {
|
|
1095
|
+
throw new FetchError(`Request timeout after ${timeout}ms while fetching ${uri}`);
|
|
1096
|
+
}
|
|
1097
|
+
throw new FetchError(
|
|
1098
|
+
`Failed to fetch status list from ${uri}: ${error.message}`
|
|
1099
|
+
);
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
function isValidStatusListUri(uri) {
|
|
1103
|
+
if (!uri || typeof uri !== "string") {
|
|
1104
|
+
return false;
|
|
1105
|
+
}
|
|
1106
|
+
try {
|
|
1107
|
+
const parsed = new URL(uri);
|
|
1108
|
+
return ["http:", "https:"].includes(parsed.protocol);
|
|
1109
|
+
} catch {
|
|
1110
|
+
return false;
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
// lib/helper/validator.ts
|
|
1115
|
+
var MAX_TTL_SECONDS = 31536e3;
|
|
1116
|
+
var MIN_TTL_SECONDS = 60;
|
|
1117
|
+
function validateJWTPayload2(payload) {
|
|
1118
|
+
const errors = [];
|
|
1119
|
+
if (!payload.iss || typeof payload.iss !== "string") {
|
|
1120
|
+
errors.push('Missing or invalid "iss" claim (must be a non-empty string)');
|
|
1121
|
+
}
|
|
1122
|
+
if (!payload.sub || typeof payload.sub !== "string") {
|
|
1123
|
+
errors.push('Missing or invalid "sub" claim (must be a non-empty string)');
|
|
1124
|
+
}
|
|
1125
|
+
if (typeof payload.iat !== "number") {
|
|
1126
|
+
errors.push('Missing or invalid "iat" claim (must be a number)');
|
|
1127
|
+
} else if (payload.iat < 0) {
|
|
1128
|
+
errors.push('"iat" claim must be a positive number');
|
|
1129
|
+
}
|
|
1130
|
+
if (!payload.status_list || typeof payload.status_list !== "object") {
|
|
1131
|
+
errors.push('Missing or invalid "status_list" claim (must be an object)');
|
|
1132
|
+
} else {
|
|
1133
|
+
const { bits, lst } = payload.status_list;
|
|
1134
|
+
if (![1, 2, 4, 8].includes(bits)) {
|
|
1135
|
+
errors.push('"status_list.bits" must be 1, 2, 4, or 8');
|
|
1136
|
+
}
|
|
1137
|
+
if (!lst || typeof lst !== "string") {
|
|
1138
|
+
errors.push('"status_list.lst" must be a non-empty base64url-encoded string');
|
|
1139
|
+
}
|
|
1140
|
+
if (payload.status_list.aggregation_uri !== void 0) {
|
|
1141
|
+
if (typeof payload.status_list.aggregation_uri !== "string") {
|
|
1142
|
+
errors.push('"status_list.aggregation_uri" must be a string');
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
if (payload.exp !== void 0) {
|
|
1147
|
+
if (typeof payload.exp !== "number") {
|
|
1148
|
+
errors.push('"exp" claim must be a number');
|
|
1149
|
+
} else if (payload.exp < 0) {
|
|
1150
|
+
errors.push('"exp" claim must be a positive number');
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
if (payload.ttl !== void 0) {
|
|
1154
|
+
if (typeof payload.ttl !== "number") {
|
|
1155
|
+
errors.push('"ttl" claim must be a number');
|
|
1156
|
+
} else if (payload.ttl < 0) {
|
|
1157
|
+
errors.push('"ttl" claim must be a positive number');
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
return {
|
|
1161
|
+
valid: errors.length === 0,
|
|
1162
|
+
errors
|
|
1163
|
+
};
|
|
1164
|
+
}
|
|
1165
|
+
function validateCWTPayload2(payload) {
|
|
1166
|
+
const errors = [];
|
|
1167
|
+
const statusList = payload[CWT_CLAIMS.STATUS_LIST];
|
|
1168
|
+
if (!statusList || typeof statusList !== "object") {
|
|
1169
|
+
errors.push(`Missing or invalid status_list claim (key ${CWT_CLAIMS.STATUS_LIST})`);
|
|
1170
|
+
} else {
|
|
1171
|
+
const { bits, lst } = statusList;
|
|
1172
|
+
if (![1, 2, 4, 8].includes(bits)) {
|
|
1173
|
+
errors.push('"status_list.bits" must be 1, 2, 4, or 8');
|
|
1174
|
+
}
|
|
1175
|
+
if (!(lst instanceof Uint8Array)) {
|
|
1176
|
+
errors.push('"status_list.lst" must be a Uint8Array');
|
|
1177
|
+
} else if (lst.length === 0) {
|
|
1178
|
+
errors.push('"status_list.lst" must not be empty');
|
|
1179
|
+
}
|
|
1180
|
+
if (statusList.aggregation_uri !== void 0) {
|
|
1181
|
+
if (typeof statusList.aggregation_uri !== "string") {
|
|
1182
|
+
errors.push('"status_list.aggregation_uri" must be a string');
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
if (payload[CWT_CLAIMS.ISS] !== void 0) {
|
|
1187
|
+
if (typeof payload[CWT_CLAIMS.ISS] !== "string") {
|
|
1188
|
+
errors.push(`"iss" claim (key ${CWT_CLAIMS.ISS}) must be a string`);
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
if (payload[CWT_CLAIMS.SUB] !== void 0) {
|
|
1192
|
+
if (typeof payload[CWT_CLAIMS.SUB] !== "string") {
|
|
1193
|
+
errors.push(`"sub" claim (key ${CWT_CLAIMS.SUB}) must be a string`);
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
if (payload[CWT_CLAIMS.EXP] !== void 0) {
|
|
1197
|
+
const exp = payload[CWT_CLAIMS.EXP];
|
|
1198
|
+
if (typeof exp !== "number") {
|
|
1199
|
+
errors.push(`"exp" claim (key ${CWT_CLAIMS.EXP}) must be a number`);
|
|
1200
|
+
} else if (exp < 0) {
|
|
1201
|
+
errors.push(`"exp" claim must be a positive number`);
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
if (payload[CWT_CLAIMS.IAT] !== void 0) {
|
|
1205
|
+
const iat = payload[CWT_CLAIMS.IAT];
|
|
1206
|
+
if (typeof iat !== "number") {
|
|
1207
|
+
errors.push(`"iat" claim (key ${CWT_CLAIMS.IAT}) must be a number`);
|
|
1208
|
+
} else if (iat < 0) {
|
|
1209
|
+
errors.push(`"iat" claim must be a positive number`);
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
if (payload[CWT_CLAIMS.TTL] !== void 0) {
|
|
1213
|
+
const ttl = payload[CWT_CLAIMS.TTL];
|
|
1214
|
+
if (typeof ttl !== "number") {
|
|
1215
|
+
errors.push(`"ttl" claim (key ${CWT_CLAIMS.TTL}) must be a number`);
|
|
1216
|
+
} else if (ttl < 0) {
|
|
1217
|
+
errors.push(`"ttl" claim must be a positive number`);
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
return {
|
|
1221
|
+
valid: errors.length === 0,
|
|
1222
|
+
errors
|
|
1223
|
+
};
|
|
1224
|
+
}
|
|
1225
|
+
function validateExpiry(exp, iat, ttl, currentTime = Math.floor(Date.now() / 1e3)) {
|
|
1226
|
+
const errors = [];
|
|
1227
|
+
if (iat !== void 0 && iat > currentTime) {
|
|
1228
|
+
errors.push(`Token issued in the future (iat: ${iat}, current: ${currentTime})`);
|
|
1229
|
+
}
|
|
1230
|
+
if (exp !== void 0) {
|
|
1231
|
+
if (exp < currentTime) {
|
|
1232
|
+
errors.push(`Token expired (exp: ${exp}, current: ${currentTime})`);
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
if (ttl !== void 0 && iat !== void 0) {
|
|
1236
|
+
const expirationTime = iat + ttl;
|
|
1237
|
+
if (expirationTime < currentTime) {
|
|
1238
|
+
errors.push(`Token expired by ttl (iat: ${iat}, ttl: ${ttl}, current: ${currentTime})`);
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
if (exp !== void 0 && ttl !== void 0 && iat !== void 0) {
|
|
1242
|
+
const calculatedExp = iat + ttl;
|
|
1243
|
+
if (Math.abs(exp - calculatedExp) > 1) {
|
|
1244
|
+
errors.push(
|
|
1245
|
+
`Inconsistent exp and ttl values (exp: ${exp}, iat + ttl: ${calculatedExp})`
|
|
1246
|
+
);
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
return {
|
|
1250
|
+
valid: errors.length === 0,
|
|
1251
|
+
errors
|
|
1252
|
+
};
|
|
1253
|
+
}
|
|
1254
|
+
function validateTTLBounds(ttl) {
|
|
1255
|
+
const errors = [];
|
|
1256
|
+
if (ttl === void 0) {
|
|
1257
|
+
return { valid: true, errors: [] };
|
|
1258
|
+
}
|
|
1259
|
+
if (ttl < MIN_TTL_SECONDS) {
|
|
1260
|
+
errors.push(
|
|
1261
|
+
`TTL too short (${ttl} seconds). Recommended minimum: ${MIN_TTL_SECONDS} seconds (1 minute)`
|
|
1262
|
+
);
|
|
1263
|
+
}
|
|
1264
|
+
if (ttl > MAX_TTL_SECONDS) {
|
|
1265
|
+
errors.push(
|
|
1266
|
+
`TTL too large (${ttl} seconds). Recommended maximum: ${MAX_TTL_SECONDS} seconds (1 year). Excessively large TTL values may be used for DoS attacks.`
|
|
1267
|
+
);
|
|
1268
|
+
}
|
|
1269
|
+
return {
|
|
1270
|
+
valid: errors.length === 0,
|
|
1271
|
+
errors
|
|
1272
|
+
};
|
|
1273
|
+
}
|
|
1274
|
+
function isExpired(exp, iat, ttl, currentTime = Math.floor(Date.now() / 1e3)) {
|
|
1275
|
+
if (exp !== void 0 && exp < currentTime) {
|
|
1276
|
+
return true;
|
|
1277
|
+
}
|
|
1278
|
+
if (ttl !== void 0 && iat !== void 0) {
|
|
1279
|
+
const expirationTime = iat + ttl;
|
|
1280
|
+
if (expirationTime < currentTime) {
|
|
1281
|
+
return true;
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
return false;
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
// lib/helper/StatusListTokenHelper.ts
|
|
1288
|
+
var StatusListTokenHelper = class _StatusListTokenHelper {
|
|
1289
|
+
constructor(header, payload, statusList, format) {
|
|
1290
|
+
this.header = header;
|
|
1291
|
+
this.payload = payload;
|
|
1292
|
+
this.statusList = statusList;
|
|
1293
|
+
this.format = format;
|
|
1294
|
+
}
|
|
1295
|
+
/**
|
|
1296
|
+
* Creates a helper from a Status List Token (JWT or CWT).
|
|
1297
|
+
*
|
|
1298
|
+
* Automatically detects the format:
|
|
1299
|
+
* - JWT: starts with "eyJ" (base64url-encoded header)
|
|
1300
|
+
* - CWT: CBOR binary format
|
|
1301
|
+
*
|
|
1302
|
+
* @param token - JWT string or CWT Uint8Array
|
|
1303
|
+
* @returns StatusListTokenHelper instance
|
|
1304
|
+
* @throws {InvalidTokenFormatError} If token format is invalid
|
|
1305
|
+
*
|
|
1306
|
+
* @example
|
|
1307
|
+
* ```typescript
|
|
1308
|
+
* // From JWT
|
|
1309
|
+
* const helper = StatusListTokenHelper.fromToken(jwtString);
|
|
1310
|
+
*
|
|
1311
|
+
* // From CWT
|
|
1312
|
+
* const helper = StatusListTokenHelper.fromToken(cwtBytes);
|
|
1313
|
+
* ```
|
|
1314
|
+
*/
|
|
1315
|
+
static fromToken(token) {
|
|
1316
|
+
if (typeof token === "string") {
|
|
1317
|
+
const { header, payload: payload2, statusList: statusList2 } = parseJWTStatusList(token);
|
|
1318
|
+
return new _StatusListTokenHelper(header, payload2, statusList2, "jwt");
|
|
1319
|
+
}
|
|
1320
|
+
const { protectedHeader, payload, statusList } = parseCWTStatusList(token);
|
|
1321
|
+
return new _StatusListTokenHelper(protectedHeader, payload, statusList, "cwt");
|
|
1322
|
+
}
|
|
1323
|
+
/**
|
|
1324
|
+
* Creates a helper by fetching a Status List Token from a URI.
|
|
1325
|
+
*
|
|
1326
|
+
* Fetches the token via HTTP GET and automatically detects the format.
|
|
1327
|
+
*
|
|
1328
|
+
* @param reference - Status reference containing idx and uri
|
|
1329
|
+
* @param options - Fetch options
|
|
1330
|
+
* @returns StatusListTokenHelper instance
|
|
1331
|
+
* @throws {FetchError} If HTTP request fails
|
|
1332
|
+
* @throws {InvalidTokenFormatError} If token format is invalid
|
|
1333
|
+
*
|
|
1334
|
+
* @example
|
|
1335
|
+
* ```typescript
|
|
1336
|
+
* const reference = {
|
|
1337
|
+
* idx: 42,
|
|
1338
|
+
* uri: 'https://issuer.example.com/status/1'
|
|
1339
|
+
* };
|
|
1340
|
+
*
|
|
1341
|
+
* const helper = await StatusListTokenHelper.fromStatusReference(reference);
|
|
1342
|
+
* const status = helper.getStatus(reference.idx);
|
|
1343
|
+
* ```
|
|
1344
|
+
*/
|
|
1345
|
+
static async fromStatusReference(reference, options) {
|
|
1346
|
+
const token = await fetchStatusListToken(reference.uri, options);
|
|
1347
|
+
return _StatusListTokenHelper.fromToken(token);
|
|
1348
|
+
}
|
|
1349
|
+
/**
|
|
1350
|
+
* Gets the status value at a specific index.
|
|
1351
|
+
*
|
|
1352
|
+
* @param index - Index in the status list
|
|
1353
|
+
* @returns Status value (0-255 depending on bits per status)
|
|
1354
|
+
* @throws {IndexOutOfBoundsError} If index is out of bounds
|
|
1355
|
+
*
|
|
1356
|
+
* @example
|
|
1357
|
+
* ```typescript
|
|
1358
|
+
* const status = helper.getStatus(42);
|
|
1359
|
+
* if (status === StandardStatusValues.INVALID) {
|
|
1360
|
+
* console.log('Credential 42 is revoked');
|
|
1361
|
+
* }
|
|
1362
|
+
* ```
|
|
1363
|
+
*/
|
|
1364
|
+
getStatus(index) {
|
|
1365
|
+
return this.statusList.getStatus(index);
|
|
1366
|
+
}
|
|
1367
|
+
/**
|
|
1368
|
+
* Checks if the status list token is expired.
|
|
1369
|
+
*
|
|
1370
|
+
* A token is expired if:
|
|
1371
|
+
* - exp is set and is in the past, OR
|
|
1372
|
+
* - ttl is set and iat + ttl is in the past
|
|
1373
|
+
*
|
|
1374
|
+
* @param currentTime - Current time (seconds since epoch). Defaults to now.
|
|
1375
|
+
* @returns True if expired
|
|
1376
|
+
*
|
|
1377
|
+
* @example
|
|
1378
|
+
* ```typescript
|
|
1379
|
+
* if (helper.isExpired()) {
|
|
1380
|
+
* throw new Error('Status list expired - fetch a new one');
|
|
1381
|
+
* }
|
|
1382
|
+
* ```
|
|
1383
|
+
*/
|
|
1384
|
+
isExpired(currentTime) {
|
|
1385
|
+
return isExpired(this.exp, this.iat, this.ttl, currentTime);
|
|
1386
|
+
}
|
|
1387
|
+
/**
|
|
1388
|
+
* Gets the issuer identifier (iss claim).
|
|
1389
|
+
*
|
|
1390
|
+
* @returns Issuer string or undefined if not set
|
|
1391
|
+
*/
|
|
1392
|
+
get iss() {
|
|
1393
|
+
if (this.format === "jwt") {
|
|
1394
|
+
return this.payload.iss;
|
|
1395
|
+
}
|
|
1396
|
+
return this.payload[CWT_CLAIMS.ISS];
|
|
1397
|
+
}
|
|
1398
|
+
/**
|
|
1399
|
+
* Gets the subject identifier (sub claim).
|
|
1400
|
+
*
|
|
1401
|
+
* @returns Subject string or undefined if not set
|
|
1402
|
+
*/
|
|
1403
|
+
get sub() {
|
|
1404
|
+
if (this.format === "jwt") {
|
|
1405
|
+
return this.payload.sub;
|
|
1406
|
+
}
|
|
1407
|
+
return this.payload[CWT_CLAIMS.SUB];
|
|
1408
|
+
}
|
|
1409
|
+
/**
|
|
1410
|
+
* Gets the issued at time (iat claim).
|
|
1411
|
+
*
|
|
1412
|
+
* @returns Timestamp in seconds since epoch, or undefined if not set
|
|
1413
|
+
*/
|
|
1414
|
+
get iat() {
|
|
1415
|
+
if (this.format === "jwt") {
|
|
1416
|
+
return this.payload.iat;
|
|
1417
|
+
}
|
|
1418
|
+
return this.payload[CWT_CLAIMS.IAT];
|
|
1419
|
+
}
|
|
1420
|
+
/**
|
|
1421
|
+
* Gets the expiration time (exp claim).
|
|
1422
|
+
*
|
|
1423
|
+
* @returns Timestamp in seconds since epoch, or undefined if not set
|
|
1424
|
+
*/
|
|
1425
|
+
get exp() {
|
|
1426
|
+
if (this.format === "jwt") {
|
|
1427
|
+
return this.payload.exp;
|
|
1428
|
+
}
|
|
1429
|
+
return this.payload[CWT_CLAIMS.EXP];
|
|
1430
|
+
}
|
|
1431
|
+
/**
|
|
1432
|
+
* Gets the time to live (ttl claim).
|
|
1433
|
+
*
|
|
1434
|
+
* @returns TTL in seconds, or undefined if not set
|
|
1435
|
+
*/
|
|
1436
|
+
get ttl() {
|
|
1437
|
+
if (this.format === "jwt") {
|
|
1438
|
+
return this.payload.ttl;
|
|
1439
|
+
}
|
|
1440
|
+
return this.payload[CWT_CLAIMS.TTL];
|
|
1441
|
+
}
|
|
1442
|
+
/**
|
|
1443
|
+
* Gets the number of bits per status entry.
|
|
1444
|
+
*
|
|
1445
|
+
* @returns Bits per status (1, 2, 4, or 8)
|
|
1446
|
+
*/
|
|
1447
|
+
get bits() {
|
|
1448
|
+
if (this.format === "jwt") {
|
|
1449
|
+
return this.payload.status_list.bits;
|
|
1450
|
+
}
|
|
1451
|
+
return this.payload[CWT_CLAIMS.STATUS_LIST].bits;
|
|
1452
|
+
}
|
|
1453
|
+
/**
|
|
1454
|
+
* Gets the aggregation URI if set.
|
|
1455
|
+
*
|
|
1456
|
+
* @returns Aggregation URI or undefined if not set
|
|
1457
|
+
*/
|
|
1458
|
+
get aggregationUri() {
|
|
1459
|
+
if (this.format === "jwt") {
|
|
1460
|
+
return this.payload.status_list.aggregation_uri;
|
|
1461
|
+
}
|
|
1462
|
+
return this.payload[CWT_CLAIMS.STATUS_LIST].aggregation_uri;
|
|
1463
|
+
}
|
|
1464
|
+
/**
|
|
1465
|
+
* Gets the format of the token.
|
|
1466
|
+
*
|
|
1467
|
+
* @returns 'jwt' or 'cwt'
|
|
1468
|
+
*/
|
|
1469
|
+
get tokenFormat() {
|
|
1470
|
+
return this.format;
|
|
1471
|
+
}
|
|
1472
|
+
/**
|
|
1473
|
+
* Gets the size of the status list.
|
|
1474
|
+
*
|
|
1475
|
+
* @returns Number of status entries
|
|
1476
|
+
*/
|
|
1477
|
+
get size() {
|
|
1478
|
+
return this.statusList.getSize();
|
|
1479
|
+
}
|
|
1480
|
+
/**
|
|
1481
|
+
* Gets the raw header.
|
|
1482
|
+
*
|
|
1483
|
+
* @returns Header as object (JWT) or Map (CWT)
|
|
1484
|
+
*/
|
|
1485
|
+
getHeader() {
|
|
1486
|
+
return this.header;
|
|
1487
|
+
}
|
|
1488
|
+
/**
|
|
1489
|
+
* Gets the raw payload.
|
|
1490
|
+
*
|
|
1491
|
+
* @returns Payload object
|
|
1492
|
+
*/
|
|
1493
|
+
getPayload() {
|
|
1494
|
+
return this.payload;
|
|
1495
|
+
}
|
|
1496
|
+
/**
|
|
1497
|
+
* Gets the underlying StatusList instance.
|
|
1498
|
+
*
|
|
1499
|
+
* @returns StatusList instance
|
|
1500
|
+
*/
|
|
1501
|
+
getStatusList() {
|
|
1502
|
+
return this.statusList;
|
|
1503
|
+
}
|
|
1504
|
+
};
|
|
1505
|
+
|
|
1506
|
+
export { COSE_ALGORITHMS, COSE_HEADERS, CWT_CLAIMS, CompressionError, FetchError, IndexOutOfBoundsError, InvalidBitSizeError, InvalidStatusValueError, InvalidTokenFormatError, MissingStatusListUriError, StandardStatusValues, StatusList, StatusListError, StatusListExpiredError, StatusListTokenHelper, ValidationError, calculateCapacity, compress, compressToBase64URL, createCWTStatusListPayload, createJWTStatusListPayload, decompress, decompressFromBase64URL, encodeCWTPayload, extractStatusListReference, extractStatusListReferenceCBOR, fetchStatusListToken, getBitValue, isExpired, isValidStatusListUri, packBits, parseCWTStatusList, parseCWTStatusListSigned, parseJWTStatusList, setBitValue, signCOSE, signCWTStatusList, signStatusListJWT, unpackBits, validateCWTPayload2 as validateCWTPayload, validateExpiry, validateJWTPayload2 as validateJWTPayload, validateTTLBounds, verifyCOSE, verifySignatureOnly, verifyStatusListJWT };
|
|
1507
|
+
//# sourceMappingURL=index.js.map
|
|
1508
|
+
//# sourceMappingURL=index.js.map
|