@sd-jwt/jwt-status-list 0.6.2-next.27
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 +201 -0
- package/README.md +94 -0
- package/dist/index.d.mts +121 -0
- package/dist/index.d.ts +121 -0
- package/dist/index.js +216 -0
- package/dist/index.mjs +176 -0
- package/package.json +66 -0
- package/src/index.ts +3 -0
- package/src/status-list-jwt.ts +73 -0
- package/src/status-list.ts +167 -0
- package/src/test/status-list-jwt.spec.ts +126 -0
- package/src/test/status-list.spec.ts +222 -0
- package/src/types.ts +43 -0
- package/tsconfig.json +7 -0
- package/vitest.config.mts +4 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var src_exports = {};
|
|
32
|
+
__export(src_exports, {
|
|
33
|
+
StatusList: () => StatusList,
|
|
34
|
+
createHeaderAndPayload: () => createHeaderAndPayload,
|
|
35
|
+
getListFromStatusListJWT: () => getListFromStatusListJWT,
|
|
36
|
+
getStatusListFromJWT: () => getStatusListFromJWT
|
|
37
|
+
});
|
|
38
|
+
module.exports = __toCommonJS(src_exports);
|
|
39
|
+
|
|
40
|
+
// src/status-list.ts
|
|
41
|
+
var import_pako = require("pako");
|
|
42
|
+
var import_base64url = __toESM(require("base64url"));
|
|
43
|
+
var StatusList = class _StatusList {
|
|
44
|
+
/**
|
|
45
|
+
* Create a new StatusListManager instance.
|
|
46
|
+
* @param statusList
|
|
47
|
+
* @param bitsPerStatus
|
|
48
|
+
*/
|
|
49
|
+
constructor(statusList, bitsPerStatus) {
|
|
50
|
+
if (![1, 2, 4, 8].includes(bitsPerStatus)) {
|
|
51
|
+
throw new Error("bitsPerStatus must be 1, 2, 4, or 8");
|
|
52
|
+
}
|
|
53
|
+
for (let i = 0; i < statusList.length; i++) {
|
|
54
|
+
if (statusList[i] > 2 ** bitsPerStatus) {
|
|
55
|
+
throw Error(
|
|
56
|
+
`Status value out of range at index ${i} with value ${statusList[i]}`
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
this.statusList = statusList;
|
|
61
|
+
this.bitsPerStatus = bitsPerStatus;
|
|
62
|
+
this.totalStatuses = statusList.length;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Get the number of statuses.
|
|
66
|
+
* @returns
|
|
67
|
+
*/
|
|
68
|
+
getBitsPerStatus() {
|
|
69
|
+
return this.bitsPerStatus;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Get the status at a specific index.
|
|
73
|
+
* @param index
|
|
74
|
+
* @returns
|
|
75
|
+
*/
|
|
76
|
+
getStatus(index) {
|
|
77
|
+
if (index < 0 || index >= this.totalStatuses) {
|
|
78
|
+
throw new Error("Index out of bounds");
|
|
79
|
+
}
|
|
80
|
+
return this.statusList[index];
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Set the status at a specific index.
|
|
84
|
+
* @param index
|
|
85
|
+
* @param value
|
|
86
|
+
*/
|
|
87
|
+
setStatus(index, value) {
|
|
88
|
+
if (index < 0 || index >= this.totalStatuses) {
|
|
89
|
+
throw new Error("Index out of bounds");
|
|
90
|
+
}
|
|
91
|
+
this.statusList[index] = value;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Compress the status list.
|
|
95
|
+
* @returns
|
|
96
|
+
*/
|
|
97
|
+
compressStatusList() {
|
|
98
|
+
const byteArray = this.encodeStatusList();
|
|
99
|
+
const compressed = (0, import_pako.deflate)(byteArray, { level: 9 });
|
|
100
|
+
return import_base64url.default.encode(compressed);
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Decompress the compressed status list and return a new StatusList instance.
|
|
104
|
+
* @param compressed
|
|
105
|
+
* @param bitsPerStatus
|
|
106
|
+
* @returns
|
|
107
|
+
*/
|
|
108
|
+
static decompressStatusList(compressed, bitsPerStatus) {
|
|
109
|
+
const decoded = new Uint8Array(
|
|
110
|
+
import_base64url.default.decode(compressed, "binary").split("").map((c) => c.charCodeAt(0))
|
|
111
|
+
);
|
|
112
|
+
try {
|
|
113
|
+
const decompressed = (0, import_pako.inflate)(decoded);
|
|
114
|
+
const statusList = _StatusList.decodeStatusList(
|
|
115
|
+
decompressed,
|
|
116
|
+
bitsPerStatus
|
|
117
|
+
);
|
|
118
|
+
return new _StatusList(statusList, bitsPerStatus);
|
|
119
|
+
} catch (err) {
|
|
120
|
+
throw new Error(`Decompression failed: ${err}`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Encode the status list into a byte array.
|
|
125
|
+
* @returns
|
|
126
|
+
**/
|
|
127
|
+
encodeStatusList() {
|
|
128
|
+
const numBits = this.bitsPerStatus;
|
|
129
|
+
const numBytes = Math.ceil(this.totalStatuses * numBits / 8);
|
|
130
|
+
const byteArray = new Uint8Array(numBytes);
|
|
131
|
+
let byteIndex = 0;
|
|
132
|
+
let bitIndex = 0;
|
|
133
|
+
let currentByte = "";
|
|
134
|
+
for (let i = 0; i < this.totalStatuses; i++) {
|
|
135
|
+
const status = this.statusList[i];
|
|
136
|
+
currentByte = status.toString(2).padStart(numBits, "0") + currentByte;
|
|
137
|
+
bitIndex += numBits;
|
|
138
|
+
if (bitIndex >= 8 || i === this.totalStatuses - 1) {
|
|
139
|
+
if (i === this.totalStatuses - 1 && bitIndex % 8 !== 0) {
|
|
140
|
+
currentByte = currentByte.padStart(8, "0");
|
|
141
|
+
}
|
|
142
|
+
byteArray[byteIndex] = Number.parseInt(currentByte, 2);
|
|
143
|
+
currentByte = "";
|
|
144
|
+
bitIndex = 0;
|
|
145
|
+
byteIndex++;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return byteArray;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Decode the byte array into a status list.
|
|
152
|
+
* @param byteArray
|
|
153
|
+
* @param bitsPerStatus
|
|
154
|
+
* @returns
|
|
155
|
+
*/
|
|
156
|
+
static decodeStatusList(byteArray, bitsPerStatus) {
|
|
157
|
+
const numBits = bitsPerStatus;
|
|
158
|
+
const totalStatuses = byteArray.length * 8 / numBits;
|
|
159
|
+
const statusList = new Array(totalStatuses);
|
|
160
|
+
let bitIndex = 0;
|
|
161
|
+
for (let i = 0; i < totalStatuses; i++) {
|
|
162
|
+
const byte = byteArray[Math.floor(i * numBits / 8)];
|
|
163
|
+
let byteString = byte.toString(2);
|
|
164
|
+
if (byteString.length < 8) {
|
|
165
|
+
byteString = "0".repeat(8 - byteString.length) + byteString;
|
|
166
|
+
}
|
|
167
|
+
const status = byteString.slice(bitIndex, bitIndex + numBits);
|
|
168
|
+
const group = Math.floor(i / (8 / numBits));
|
|
169
|
+
const indexInGroup = i % (8 / numBits);
|
|
170
|
+
const position = group * (8 / numBits) + (8 / numBits + -1 - indexInGroup);
|
|
171
|
+
statusList[position] = Number.parseInt(status, 2);
|
|
172
|
+
bitIndex = (bitIndex + numBits) % 8;
|
|
173
|
+
}
|
|
174
|
+
return statusList;
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
// src/status-list-jwt.ts
|
|
179
|
+
var import_base64url2 = __toESM(require("base64url"));
|
|
180
|
+
function decodeJwt(jwt) {
|
|
181
|
+
const parts = jwt.split(".");
|
|
182
|
+
return JSON.parse(import_base64url2.default.decode(parts[1]));
|
|
183
|
+
}
|
|
184
|
+
function createHeaderAndPayload(list, payload, header) {
|
|
185
|
+
if (!payload.iss) {
|
|
186
|
+
throw new Error("iss field is required");
|
|
187
|
+
}
|
|
188
|
+
if (!payload.sub) {
|
|
189
|
+
throw new Error("sub field is required");
|
|
190
|
+
}
|
|
191
|
+
if (!payload.iat) {
|
|
192
|
+
throw new Error("iat field is required");
|
|
193
|
+
}
|
|
194
|
+
header.typ = "statuslist+jwt";
|
|
195
|
+
payload.status_list = {
|
|
196
|
+
bits: list.getBitsPerStatus(),
|
|
197
|
+
lst: list.compressStatusList()
|
|
198
|
+
};
|
|
199
|
+
return { header, payload };
|
|
200
|
+
}
|
|
201
|
+
function getListFromStatusListJWT(jwt) {
|
|
202
|
+
const payload = decodeJwt(jwt);
|
|
203
|
+
const statusList = payload.status_list;
|
|
204
|
+
return StatusList.decompressStatusList(statusList.lst, statusList.bits);
|
|
205
|
+
}
|
|
206
|
+
function getStatusListFromJWT(jwt) {
|
|
207
|
+
const payload = decodeJwt(jwt);
|
|
208
|
+
return payload.status.status_list;
|
|
209
|
+
}
|
|
210
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
211
|
+
0 && (module.exports = {
|
|
212
|
+
StatusList,
|
|
213
|
+
createHeaderAndPayload,
|
|
214
|
+
getListFromStatusListJWT,
|
|
215
|
+
getStatusListFromJWT
|
|
216
|
+
});
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
// src/status-list.ts
|
|
2
|
+
import { deflate, inflate } from "pako";
|
|
3
|
+
import base64Url from "base64url";
|
|
4
|
+
var StatusList = class _StatusList {
|
|
5
|
+
/**
|
|
6
|
+
* Create a new StatusListManager instance.
|
|
7
|
+
* @param statusList
|
|
8
|
+
* @param bitsPerStatus
|
|
9
|
+
*/
|
|
10
|
+
constructor(statusList, bitsPerStatus) {
|
|
11
|
+
if (![1, 2, 4, 8].includes(bitsPerStatus)) {
|
|
12
|
+
throw new Error("bitsPerStatus must be 1, 2, 4, or 8");
|
|
13
|
+
}
|
|
14
|
+
for (let i = 0; i < statusList.length; i++) {
|
|
15
|
+
if (statusList[i] > 2 ** bitsPerStatus) {
|
|
16
|
+
throw Error(
|
|
17
|
+
`Status value out of range at index ${i} with value ${statusList[i]}`
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
this.statusList = statusList;
|
|
22
|
+
this.bitsPerStatus = bitsPerStatus;
|
|
23
|
+
this.totalStatuses = statusList.length;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Get the number of statuses.
|
|
27
|
+
* @returns
|
|
28
|
+
*/
|
|
29
|
+
getBitsPerStatus() {
|
|
30
|
+
return this.bitsPerStatus;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Get the status at a specific index.
|
|
34
|
+
* @param index
|
|
35
|
+
* @returns
|
|
36
|
+
*/
|
|
37
|
+
getStatus(index) {
|
|
38
|
+
if (index < 0 || index >= this.totalStatuses) {
|
|
39
|
+
throw new Error("Index out of bounds");
|
|
40
|
+
}
|
|
41
|
+
return this.statusList[index];
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Set the status at a specific index.
|
|
45
|
+
* @param index
|
|
46
|
+
* @param value
|
|
47
|
+
*/
|
|
48
|
+
setStatus(index, value) {
|
|
49
|
+
if (index < 0 || index >= this.totalStatuses) {
|
|
50
|
+
throw new Error("Index out of bounds");
|
|
51
|
+
}
|
|
52
|
+
this.statusList[index] = value;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Compress the status list.
|
|
56
|
+
* @returns
|
|
57
|
+
*/
|
|
58
|
+
compressStatusList() {
|
|
59
|
+
const byteArray = this.encodeStatusList();
|
|
60
|
+
const compressed = deflate(byteArray, { level: 9 });
|
|
61
|
+
return base64Url.encode(compressed);
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Decompress the compressed status list and return a new StatusList instance.
|
|
65
|
+
* @param compressed
|
|
66
|
+
* @param bitsPerStatus
|
|
67
|
+
* @returns
|
|
68
|
+
*/
|
|
69
|
+
static decompressStatusList(compressed, bitsPerStatus) {
|
|
70
|
+
const decoded = new Uint8Array(
|
|
71
|
+
base64Url.decode(compressed, "binary").split("").map((c) => c.charCodeAt(0))
|
|
72
|
+
);
|
|
73
|
+
try {
|
|
74
|
+
const decompressed = inflate(decoded);
|
|
75
|
+
const statusList = _StatusList.decodeStatusList(
|
|
76
|
+
decompressed,
|
|
77
|
+
bitsPerStatus
|
|
78
|
+
);
|
|
79
|
+
return new _StatusList(statusList, bitsPerStatus);
|
|
80
|
+
} catch (err) {
|
|
81
|
+
throw new Error(`Decompression failed: ${err}`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Encode the status list into a byte array.
|
|
86
|
+
* @returns
|
|
87
|
+
**/
|
|
88
|
+
encodeStatusList() {
|
|
89
|
+
const numBits = this.bitsPerStatus;
|
|
90
|
+
const numBytes = Math.ceil(this.totalStatuses * numBits / 8);
|
|
91
|
+
const byteArray = new Uint8Array(numBytes);
|
|
92
|
+
let byteIndex = 0;
|
|
93
|
+
let bitIndex = 0;
|
|
94
|
+
let currentByte = "";
|
|
95
|
+
for (let i = 0; i < this.totalStatuses; i++) {
|
|
96
|
+
const status = this.statusList[i];
|
|
97
|
+
currentByte = status.toString(2).padStart(numBits, "0") + currentByte;
|
|
98
|
+
bitIndex += numBits;
|
|
99
|
+
if (bitIndex >= 8 || i === this.totalStatuses - 1) {
|
|
100
|
+
if (i === this.totalStatuses - 1 && bitIndex % 8 !== 0) {
|
|
101
|
+
currentByte = currentByte.padStart(8, "0");
|
|
102
|
+
}
|
|
103
|
+
byteArray[byteIndex] = Number.parseInt(currentByte, 2);
|
|
104
|
+
currentByte = "";
|
|
105
|
+
bitIndex = 0;
|
|
106
|
+
byteIndex++;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return byteArray;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Decode the byte array into a status list.
|
|
113
|
+
* @param byteArray
|
|
114
|
+
* @param bitsPerStatus
|
|
115
|
+
* @returns
|
|
116
|
+
*/
|
|
117
|
+
static decodeStatusList(byteArray, bitsPerStatus) {
|
|
118
|
+
const numBits = bitsPerStatus;
|
|
119
|
+
const totalStatuses = byteArray.length * 8 / numBits;
|
|
120
|
+
const statusList = new Array(totalStatuses);
|
|
121
|
+
let bitIndex = 0;
|
|
122
|
+
for (let i = 0; i < totalStatuses; i++) {
|
|
123
|
+
const byte = byteArray[Math.floor(i * numBits / 8)];
|
|
124
|
+
let byteString = byte.toString(2);
|
|
125
|
+
if (byteString.length < 8) {
|
|
126
|
+
byteString = "0".repeat(8 - byteString.length) + byteString;
|
|
127
|
+
}
|
|
128
|
+
const status = byteString.slice(bitIndex, bitIndex + numBits);
|
|
129
|
+
const group = Math.floor(i / (8 / numBits));
|
|
130
|
+
const indexInGroup = i % (8 / numBits);
|
|
131
|
+
const position = group * (8 / numBits) + (8 / numBits + -1 - indexInGroup);
|
|
132
|
+
statusList[position] = Number.parseInt(status, 2);
|
|
133
|
+
bitIndex = (bitIndex + numBits) % 8;
|
|
134
|
+
}
|
|
135
|
+
return statusList;
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
// src/status-list-jwt.ts
|
|
140
|
+
import base64Url2 from "base64url";
|
|
141
|
+
function decodeJwt(jwt) {
|
|
142
|
+
const parts = jwt.split(".");
|
|
143
|
+
return JSON.parse(base64Url2.decode(parts[1]));
|
|
144
|
+
}
|
|
145
|
+
function createHeaderAndPayload(list, payload, header) {
|
|
146
|
+
if (!payload.iss) {
|
|
147
|
+
throw new Error("iss field is required");
|
|
148
|
+
}
|
|
149
|
+
if (!payload.sub) {
|
|
150
|
+
throw new Error("sub field is required");
|
|
151
|
+
}
|
|
152
|
+
if (!payload.iat) {
|
|
153
|
+
throw new Error("iat field is required");
|
|
154
|
+
}
|
|
155
|
+
header.typ = "statuslist+jwt";
|
|
156
|
+
payload.status_list = {
|
|
157
|
+
bits: list.getBitsPerStatus(),
|
|
158
|
+
lst: list.compressStatusList()
|
|
159
|
+
};
|
|
160
|
+
return { header, payload };
|
|
161
|
+
}
|
|
162
|
+
function getListFromStatusListJWT(jwt) {
|
|
163
|
+
const payload = decodeJwt(jwt);
|
|
164
|
+
const statusList = payload.status_list;
|
|
165
|
+
return StatusList.decompressStatusList(statusList.lst, statusList.bits);
|
|
166
|
+
}
|
|
167
|
+
function getStatusListFromJWT(jwt) {
|
|
168
|
+
const payload = decodeJwt(jwt);
|
|
169
|
+
return payload.status.status_list;
|
|
170
|
+
}
|
|
171
|
+
export {
|
|
172
|
+
StatusList,
|
|
173
|
+
createHeaderAndPayload,
|
|
174
|
+
getListFromStatusListJWT,
|
|
175
|
+
getStatusListFromJWT
|
|
176
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@sd-jwt/jwt-status-list",
|
|
3
|
+
"version": "0.6.2-next.27+5711484",
|
|
4
|
+
"description": "Implementation based on https://datatracker.ietf.org/doc/draft-ietf-oauth-status-list/",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"module": "dist/index.mjs",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": "./dist/index.mjs",
|
|
11
|
+
"require": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"scripts": {
|
|
15
|
+
"build": "rm -rf **/dist && tsup",
|
|
16
|
+
"lint": "biome lint ./src",
|
|
17
|
+
"test": "pnpm run test:node && pnpm run test:browser && pnpm run test:cov",
|
|
18
|
+
"test:node": "vitest run ./src/test/*.spec.ts",
|
|
19
|
+
"test:browser": "vitest run ./src/test/*.spec.ts --environment jsdom",
|
|
20
|
+
"test:cov": "vitest run --coverage"
|
|
21
|
+
},
|
|
22
|
+
"keywords": [
|
|
23
|
+
"sd-jwt-vc",
|
|
24
|
+
"status-list",
|
|
25
|
+
"sd-jwt"
|
|
26
|
+
],
|
|
27
|
+
"engines": {
|
|
28
|
+
"node": ">=18"
|
|
29
|
+
},
|
|
30
|
+
"repository": {
|
|
31
|
+
"type": "git",
|
|
32
|
+
"url": "https://github.com/openwallet-foundation-labs/sd-jwt-js"
|
|
33
|
+
},
|
|
34
|
+
"author": "Mirko Mollik <mirkomollik@gmail.com>",
|
|
35
|
+
"homepage": "https://github.com/openwallet-foundation-labs/sd-jwt-js/wiki",
|
|
36
|
+
"bugs": {
|
|
37
|
+
"url": "https://github.com/openwallet-foundation-labs/sd-jwt-js/issues"
|
|
38
|
+
},
|
|
39
|
+
"license": "Apache-2.0",
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@types/pako": "^2.0.3",
|
|
42
|
+
"jose": "^5.2.2"
|
|
43
|
+
},
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"@sd-jwt/types": "0.6.2-next.27+5711484",
|
|
46
|
+
"base64url": "^3.0.1",
|
|
47
|
+
"pako": "^2.1.0"
|
|
48
|
+
},
|
|
49
|
+
"publishConfig": {
|
|
50
|
+
"access": "public"
|
|
51
|
+
},
|
|
52
|
+
"tsup": {
|
|
53
|
+
"entry": [
|
|
54
|
+
"./src/index.ts"
|
|
55
|
+
],
|
|
56
|
+
"sourceMap": true,
|
|
57
|
+
"splitting": false,
|
|
58
|
+
"clean": true,
|
|
59
|
+
"dts": true,
|
|
60
|
+
"format": [
|
|
61
|
+
"cjs",
|
|
62
|
+
"esm"
|
|
63
|
+
]
|
|
64
|
+
},
|
|
65
|
+
"gitHead": "5711484f7aedc207763835f1f7f656a75558798d"
|
|
66
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { JwtPayload } from '@sd-jwt/types';
|
|
2
|
+
import { StatusList } from './status-list.js';
|
|
3
|
+
import type {
|
|
4
|
+
JWTwithStatusListPayload,
|
|
5
|
+
StatusListJWTHeaderParameters,
|
|
6
|
+
StatusListEntry,
|
|
7
|
+
StatusListJWTPayload,
|
|
8
|
+
} from './types.js';
|
|
9
|
+
import base64Url from 'base64url';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Decode a JWT and return the payload.
|
|
13
|
+
* @param jwt JWT token in compact JWS serialization.
|
|
14
|
+
* @returns Payload of the JWT.
|
|
15
|
+
*/
|
|
16
|
+
function decodeJwt<T>(jwt: string): T {
|
|
17
|
+
const parts = jwt.split('.');
|
|
18
|
+
return JSON.parse(base64Url.decode(parts[1]));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Adds the status list to the payload and header of a JWT.
|
|
23
|
+
* @param list
|
|
24
|
+
* @param payload
|
|
25
|
+
* @param header
|
|
26
|
+
* @returns The header and payload with the status list added.
|
|
27
|
+
*/
|
|
28
|
+
export function createHeaderAndPayload(
|
|
29
|
+
list: StatusList,
|
|
30
|
+
payload: JwtPayload,
|
|
31
|
+
header: StatusListJWTHeaderParameters,
|
|
32
|
+
) {
|
|
33
|
+
// validate if the required fieds are present based on https://www.ietf.org/archive/id/draft-ietf-oauth-status-list-02.html#section-5.1
|
|
34
|
+
|
|
35
|
+
if (!payload.iss) {
|
|
36
|
+
throw new Error('iss field is required');
|
|
37
|
+
}
|
|
38
|
+
if (!payload.sub) {
|
|
39
|
+
throw new Error('sub field is required');
|
|
40
|
+
}
|
|
41
|
+
if (!payload.iat) {
|
|
42
|
+
throw new Error('iat field is required');
|
|
43
|
+
}
|
|
44
|
+
//exp and tll are optional. We will not validate the business logic of the values like exp > iat etc.
|
|
45
|
+
|
|
46
|
+
header.typ = 'statuslist+jwt';
|
|
47
|
+
payload.status_list = {
|
|
48
|
+
bits: list.getBitsPerStatus(),
|
|
49
|
+
lst: list.compressStatusList(),
|
|
50
|
+
};
|
|
51
|
+
return { header, payload };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Get the status list from a JWT, but do not verify the signature.
|
|
56
|
+
* @param jwt
|
|
57
|
+
* @returns
|
|
58
|
+
*/
|
|
59
|
+
export function getListFromStatusListJWT(jwt: string): StatusList {
|
|
60
|
+
const payload = decodeJwt<StatusListJWTPayload>(jwt);
|
|
61
|
+
const statusList = payload.status_list;
|
|
62
|
+
return StatusList.decompressStatusList(statusList.lst, statusList.bits);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Get the status list entry from a JWT, but do not verify the signature.
|
|
67
|
+
* @param jwt
|
|
68
|
+
* @returns
|
|
69
|
+
*/
|
|
70
|
+
export function getStatusListFromJWT(jwt: string): StatusListEntry {
|
|
71
|
+
const payload = decodeJwt<JWTwithStatusListPayload>(jwt);
|
|
72
|
+
return payload.status.status_list;
|
|
73
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { deflate, inflate } from 'pako';
|
|
2
|
+
import base64Url from 'base64url';
|
|
3
|
+
import type { BitsPerStatus } from './types.js';
|
|
4
|
+
/**
|
|
5
|
+
* StatusListManager is a class that manages a list of statuses with variable bit size.
|
|
6
|
+
*/
|
|
7
|
+
export class StatusList {
|
|
8
|
+
private statusList: number[];
|
|
9
|
+
private bitsPerStatus: BitsPerStatus;
|
|
10
|
+
private totalStatuses: number;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Create a new StatusListManager instance.
|
|
14
|
+
* @param statusList
|
|
15
|
+
* @param bitsPerStatus
|
|
16
|
+
*/
|
|
17
|
+
constructor(statusList: number[], bitsPerStatus: BitsPerStatus) {
|
|
18
|
+
if (![1, 2, 4, 8].includes(bitsPerStatus)) {
|
|
19
|
+
throw new Error('bitsPerStatus must be 1, 2, 4, or 8');
|
|
20
|
+
}
|
|
21
|
+
//check that the entries in the statusList are within the range of the bitsPerStatus
|
|
22
|
+
for (let i = 0; i < statusList.length; i++) {
|
|
23
|
+
if (statusList[i] > 2 ** bitsPerStatus) {
|
|
24
|
+
throw Error(
|
|
25
|
+
`Status value out of range at index ${i} with value ${statusList[i]}`,
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
this.statusList = statusList;
|
|
30
|
+
this.bitsPerStatus = bitsPerStatus;
|
|
31
|
+
this.totalStatuses = statusList.length;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Get the number of statuses.
|
|
36
|
+
* @returns
|
|
37
|
+
*/
|
|
38
|
+
getBitsPerStatus(): BitsPerStatus {
|
|
39
|
+
return this.bitsPerStatus;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Get the status at a specific index.
|
|
44
|
+
* @param index
|
|
45
|
+
* @returns
|
|
46
|
+
*/
|
|
47
|
+
getStatus(index: number): number {
|
|
48
|
+
if (index < 0 || index >= this.totalStatuses) {
|
|
49
|
+
throw new Error('Index out of bounds');
|
|
50
|
+
}
|
|
51
|
+
return this.statusList[index];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Set the status at a specific index.
|
|
56
|
+
* @param index
|
|
57
|
+
* @param value
|
|
58
|
+
*/
|
|
59
|
+
setStatus(index: number, value: number): void {
|
|
60
|
+
if (index < 0 || index >= this.totalStatuses) {
|
|
61
|
+
throw new Error('Index out of bounds');
|
|
62
|
+
}
|
|
63
|
+
this.statusList[index] = value;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Compress the status list.
|
|
68
|
+
* @returns
|
|
69
|
+
*/
|
|
70
|
+
compressStatusList(): string {
|
|
71
|
+
const byteArray = this.encodeStatusList();
|
|
72
|
+
const compressed = deflate(byteArray, { level: 9 });
|
|
73
|
+
return base64Url.encode(compressed as Buffer);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Decompress the compressed status list and return a new StatusList instance.
|
|
78
|
+
* @param compressed
|
|
79
|
+
* @param bitsPerStatus
|
|
80
|
+
* @returns
|
|
81
|
+
*/
|
|
82
|
+
static decompressStatusList(
|
|
83
|
+
compressed: string,
|
|
84
|
+
bitsPerStatus: BitsPerStatus,
|
|
85
|
+
): StatusList {
|
|
86
|
+
const decoded = new Uint8Array(
|
|
87
|
+
base64Url
|
|
88
|
+
.decode(compressed, 'binary')
|
|
89
|
+
.split('')
|
|
90
|
+
.map((c) => c.charCodeAt(0)),
|
|
91
|
+
);
|
|
92
|
+
try {
|
|
93
|
+
const decompressed = inflate(decoded);
|
|
94
|
+
const statusList = StatusList.decodeStatusList(
|
|
95
|
+
decompressed,
|
|
96
|
+
bitsPerStatus,
|
|
97
|
+
);
|
|
98
|
+
return new StatusList(statusList, bitsPerStatus);
|
|
99
|
+
} catch (err: unknown) {
|
|
100
|
+
throw new Error(`Decompression failed: ${err}`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Encode the status list into a byte array.
|
|
106
|
+
* @returns
|
|
107
|
+
**/
|
|
108
|
+
public encodeStatusList(): Uint8Array {
|
|
109
|
+
const numBits = this.bitsPerStatus;
|
|
110
|
+
const numBytes = Math.ceil((this.totalStatuses * numBits) / 8);
|
|
111
|
+
const byteArray = new Uint8Array(numBytes);
|
|
112
|
+
let byteIndex = 0;
|
|
113
|
+
let bitIndex = 0;
|
|
114
|
+
let currentByte = '';
|
|
115
|
+
for (let i = 0; i < this.totalStatuses; i++) {
|
|
116
|
+
const status = this.statusList[i];
|
|
117
|
+
// Place bits from status into currentByte, starting from the most significant bit.
|
|
118
|
+
currentByte = status.toString(2).padStart(numBits, '0') + currentByte;
|
|
119
|
+
bitIndex += numBits;
|
|
120
|
+
|
|
121
|
+
// If currentByte is full or this is the last status, add it to byteArray and reset currentByte and bitIndex.
|
|
122
|
+
if (bitIndex >= 8 || i === this.totalStatuses - 1) {
|
|
123
|
+
// If this is the last status and bitIndex is not a multiple of 8, shift currentByte to the left.
|
|
124
|
+
if (i === this.totalStatuses - 1 && bitIndex % 8 !== 0) {
|
|
125
|
+
currentByte = currentByte.padStart(8, '0');
|
|
126
|
+
}
|
|
127
|
+
byteArray[byteIndex] = Number.parseInt(currentByte, 2);
|
|
128
|
+
currentByte = '';
|
|
129
|
+
bitIndex = 0;
|
|
130
|
+
byteIndex++;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return byteArray;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Decode the byte array into a status list.
|
|
139
|
+
* @param byteArray
|
|
140
|
+
* @param bitsPerStatus
|
|
141
|
+
* @returns
|
|
142
|
+
*/
|
|
143
|
+
private static decodeStatusList(
|
|
144
|
+
byteArray: Uint8Array,
|
|
145
|
+
bitsPerStatus: BitsPerStatus,
|
|
146
|
+
): number[] {
|
|
147
|
+
const numBits = bitsPerStatus;
|
|
148
|
+
const totalStatuses = (byteArray.length * 8) / numBits;
|
|
149
|
+
const statusList = new Array<number>(totalStatuses);
|
|
150
|
+
let bitIndex = 0; // Current position in byte
|
|
151
|
+
for (let i = 0; i < totalStatuses; i++) {
|
|
152
|
+
const byte = byteArray[Math.floor((i * numBits) / 8)];
|
|
153
|
+
let byteString = byte.toString(2);
|
|
154
|
+
if (byteString.length < 8) {
|
|
155
|
+
byteString = '0'.repeat(8 - byteString.length) + byteString;
|
|
156
|
+
}
|
|
157
|
+
const status = byteString.slice(bitIndex, bitIndex + numBits);
|
|
158
|
+
const group = Math.floor(i / (8 / numBits));
|
|
159
|
+
const indexInGroup = i % (8 / numBits);
|
|
160
|
+
const position =
|
|
161
|
+
group * (8 / numBits) + (8 / numBits + -1 - indexInGroup);
|
|
162
|
+
statusList[position] = Number.parseInt(status, 2);
|
|
163
|
+
bitIndex = (bitIndex + numBits) % 8;
|
|
164
|
+
}
|
|
165
|
+
return statusList;
|
|
166
|
+
}
|
|
167
|
+
}
|