ecwt 0.1.1-beta.2 → 0.2.0-beta.2
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/README.md +25 -26
- package/dist/ecwt.cjs +52 -29
- package/package.json +9 -8
- package/src/factory.js +52 -35
- package/src/main.js +1 -0
- package/src/token.js +4 -0
- package/src/utils/errors.js +6 -0
package/README.md
CHANGED
|
@@ -69,32 +69,24 @@ const lruCache = new LRUCache({
|
|
|
69
69
|
|
|
70
70
|
#### Validation library of your choice (optional)
|
|
71
71
|
|
|
72
|
-
|
|
72
|
+
By specifying the schema, you also validate the payloads. Schema is a function that takes a value and returns it back or throws.
|
|
73
73
|
|
|
74
74
|
In our example, we use [valibot](https://valibot.dev) library.
|
|
75
75
|
|
|
76
76
|
```javascript
|
|
77
|
-
import
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
).success,
|
|
91
|
-
nick: (value) => safeParse(
|
|
92
|
-
string([
|
|
93
|
-
maxLength(10),
|
|
94
|
-
]),
|
|
95
|
-
value,
|
|
96
|
-
).success,
|
|
97
|
-
};
|
|
77
|
+
import * as v from 'valibot';
|
|
78
|
+
|
|
79
|
+
const schema = (value) => v.parse(
|
|
80
|
+
v.object({
|
|
81
|
+
user_id: v.number([
|
|
82
|
+
v.maxValue(10),
|
|
83
|
+
]),
|
|
84
|
+
nick: v.string([
|
|
85
|
+
v.maxLength(10),
|
|
86
|
+
]),
|
|
87
|
+
}),
|
|
88
|
+
value,
|
|
89
|
+
);
|
|
98
90
|
```
|
|
99
91
|
|
|
100
92
|
That schema will prevent creating tokens for users with ID greater than 10 and nicknames longer than 10 characters.
|
|
@@ -111,14 +103,15 @@ constructor({
|
|
|
111
103
|
options: {
|
|
112
104
|
namespace: string?,
|
|
113
105
|
key: Buffer,
|
|
114
|
-
schema:
|
|
115
|
-
|
|
116
|
-
|
|
106
|
+
schema: (value: any) => any,
|
|
107
|
+
senmlKeyMap: {
|
|
108
|
+
[key: string]: number,
|
|
109
|
+
}?,
|
|
117
110
|
},
|
|
118
111
|
})
|
|
119
112
|
```
|
|
120
113
|
|
|
121
|
-
Create your `EcwtFactory` instance to create, and
|
|
114
|
+
Create your `EcwtFactory` instance to create, validate and revoke tokens.
|
|
122
115
|
|
|
123
116
|
```javascript
|
|
124
117
|
import { EcwtFactory } from 'ecwt';
|
|
@@ -135,10 +128,16 @@ const ecwtFactory = new EcwtFactory({
|
|
|
135
128
|
'base64',
|
|
136
129
|
),
|
|
137
130
|
schema,
|
|
131
|
+
senml_key_map: {
|
|
132
|
+
user_id: 1,
|
|
133
|
+
nick: 2,
|
|
134
|
+
},
|
|
138
135
|
},
|
|
139
136
|
});
|
|
140
137
|
```
|
|
141
138
|
|
|
139
|
+
To reduce token size, which is especially important to reduce amount of data sent over the network, you can use `options.senml_key_map` to map keys to numbers. This way, CBOR encoder will use numbers instead of strings in object keys. You **should never change** number assigned to a key or **reassign number** to another key to avoid breaking the schema. For more information, see [RFC 8428](https://datatracker.ietf.org/doc/html/rfc8428#section-6).
|
|
140
|
+
|
|
142
141
|
#### Class method `create`
|
|
143
142
|
|
|
144
143
|
```typescript
|
package/dist/ecwt.cjs
CHANGED
|
@@ -33,6 +33,7 @@ __export(main_exports, {
|
|
|
33
33
|
EcwtExpiredError: () => EcwtExpiredError,
|
|
34
34
|
EcwtFactory: () => EcwtFactory,
|
|
35
35
|
EcwtInvalidError: () => EcwtInvalidError,
|
|
36
|
+
EcwtParseError: () => EcwtParseError,
|
|
36
37
|
EcwtRevokedError: () => EcwtRevokedError
|
|
37
38
|
});
|
|
38
39
|
module.exports = __toCommonJS(main_exports);
|
|
@@ -145,6 +146,10 @@ var Ecwt = class {
|
|
|
145
146
|
}
|
|
146
147
|
return this.#ttl_initial - toSeconds(Date.now() - this.snowflake.timestamp);
|
|
147
148
|
}
|
|
149
|
+
/**
|
|
150
|
+
* Revokes token.
|
|
151
|
+
* @returns {Promise<void>} -
|
|
152
|
+
*/
|
|
148
153
|
/* async */
|
|
149
154
|
revoke() {
|
|
150
155
|
return this.#ecwtFactory._revoke({
|
|
@@ -165,6 +170,11 @@ var InvalidPackageInstanceError = class extends TypeError {
|
|
|
165
170
|
super(`Value ${property} must be an instance of ${class_name} from package "${package_name}". That error is probably caused by two separate installations of "${package_name}". Please, make sure that "${package_name}" in your project is matches "peerDependencies" of "ecwt" package.`);
|
|
166
171
|
}
|
|
167
172
|
};
|
|
173
|
+
var EcwtParseError = class extends Error {
|
|
174
|
+
constructor() {
|
|
175
|
+
super("Cannot parse data to Ecwt token.");
|
|
176
|
+
}
|
|
177
|
+
};
|
|
168
178
|
var EcwtInvalidError = class extends Error {
|
|
169
179
|
constructor(ecwt) {
|
|
170
180
|
super("Ecwt token is invalid.");
|
|
@@ -204,8 +214,8 @@ var EcwtFactory = class {
|
|
|
204
214
|
#snowflakeFactory;
|
|
205
215
|
#redis_keys = {};
|
|
206
216
|
#encryption_key;
|
|
207
|
-
#
|
|
208
|
-
#
|
|
217
|
+
#validator;
|
|
218
|
+
#cborEncoder;
|
|
209
219
|
/**
|
|
210
220
|
*
|
|
211
221
|
* @param {object} param0 -
|
|
@@ -215,7 +225,8 @@ var EcwtFactory = class {
|
|
|
215
225
|
* @param {object} param0.options -
|
|
216
226
|
* @param {string} [param0.options.namespace] Namespace for Redis keys.
|
|
217
227
|
* @param {Buffer} param0.options.key Encryption key, 64 bytes
|
|
218
|
-
* @param {
|
|
228
|
+
* @param {(value: any) => any} [param0.options.validator] Validator for token data. Should return validated value or throw an error.
|
|
229
|
+
* @param {{ [key: string]: number }} [param0.options.senml_key_map] Payload object keys mapped for their SenML keys.
|
|
219
230
|
*/
|
|
220
231
|
constructor({
|
|
221
232
|
redisClient: redisClient2 = null,
|
|
@@ -224,7 +235,8 @@ var EcwtFactory = class {
|
|
|
224
235
|
options: {
|
|
225
236
|
namespace = null,
|
|
226
237
|
key,
|
|
227
|
-
|
|
238
|
+
validator,
|
|
239
|
+
senml_key_map
|
|
228
240
|
}
|
|
229
241
|
}) {
|
|
230
242
|
if (redisClient2 !== null && (redisClient2.constructor.name !== redis_client_constructor_name || getAllKeysList(redisClient2) !== redis_client_keys)) {
|
|
@@ -253,8 +265,12 @@ var EcwtFactory = class {
|
|
|
253
265
|
this.#snowflakeFactory = snowflakeFactory;
|
|
254
266
|
this.#redis_keys.revoked = `${REDIS_PREFIX}${namespace}:revoked`;
|
|
255
267
|
this.#encryption_key = key;
|
|
256
|
-
this.#
|
|
257
|
-
|
|
268
|
+
this.#validator = validator;
|
|
269
|
+
if (senml_key_map) {
|
|
270
|
+
this.#cborEncoder = new import_cbor_x.Encoder({
|
|
271
|
+
keyMap: senml_key_map
|
|
272
|
+
});
|
|
273
|
+
}
|
|
258
274
|
}
|
|
259
275
|
/**
|
|
260
276
|
* Creates new token.
|
|
@@ -267,24 +283,19 @@ var EcwtFactory = class {
|
|
|
267
283
|
async create(data, {
|
|
268
284
|
ttl
|
|
269
285
|
} = {}) {
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
const value = data[key];
|
|
273
|
-
const validator = this.#schema[key];
|
|
274
|
-
if (typeof validator === "function" && validator(value) !== true) {
|
|
275
|
-
throw new TypeError(`Value "${value}" of property "${key}" is invalid.`);
|
|
276
|
-
}
|
|
277
|
-
payload.push(value);
|
|
286
|
+
if (typeof this.#validator === "function") {
|
|
287
|
+
data = this.#validator(data);
|
|
278
288
|
}
|
|
279
289
|
if (typeof ttl !== "number" && Number.isNaN(ttl) !== true && ttl !== null) {
|
|
280
290
|
throw new TypeError("TTL must be a number or null.");
|
|
281
291
|
}
|
|
282
292
|
const snowflake = await this.#snowflakeFactory.createSafe();
|
|
283
|
-
const
|
|
293
|
+
const payload = [
|
|
284
294
|
snowflake.toBuffer(),
|
|
285
295
|
ttl,
|
|
286
|
-
|
|
287
|
-
]
|
|
296
|
+
data
|
|
297
|
+
];
|
|
298
|
+
const token_raw = this.#cborEncoder ? this.#cborEncoder.encode(payload) : (0, import_cbor_x.encode)(payload);
|
|
288
299
|
const token_encrypted = await import_evilcrypt.v2.encrypt(
|
|
289
300
|
token_raw,
|
|
290
301
|
this.#encryption_key
|
|
@@ -335,20 +346,31 @@ var EcwtFactory = class {
|
|
|
335
346
|
const token_encrypted = Buffer.from(
|
|
336
347
|
base62.decode(token)
|
|
337
348
|
);
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
349
|
+
let token_raw;
|
|
350
|
+
try {
|
|
351
|
+
token_raw = await (0, import_evilcrypt.decrypt)(
|
|
352
|
+
token_encrypted,
|
|
353
|
+
this.#encryption_key
|
|
354
|
+
);
|
|
355
|
+
} catch {
|
|
356
|
+
throw new EcwtParseError();
|
|
357
|
+
}
|
|
358
|
+
const payload = this.#cborEncoder ? this.#cborEncoder.decode(token_raw) : (0, import_cbor_x.decode)(token_raw);
|
|
342
359
|
const [
|
|
343
|
-
snowflake_buffer
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
360
|
+
snowflake_buffer
|
|
361
|
+
] = payload;
|
|
362
|
+
[
|
|
363
|
+
,
|
|
364
|
+
ttl_initial,
|
|
365
|
+
data
|
|
366
|
+
] = payload;
|
|
347
367
|
snowflake = this.#snowflakeFactory.parse(snowflake_buffer);
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
368
|
+
if (typeof this.#validator === "function") {
|
|
369
|
+
try {
|
|
370
|
+
data = this.#validator(data);
|
|
371
|
+
} catch {
|
|
372
|
+
throw new EcwtParseError();
|
|
373
|
+
}
|
|
352
374
|
}
|
|
353
375
|
this.#setCache(
|
|
354
376
|
token,
|
|
@@ -432,5 +454,6 @@ var EcwtFactory = class {
|
|
|
432
454
|
EcwtExpiredError,
|
|
433
455
|
EcwtFactory,
|
|
434
456
|
EcwtInvalidError,
|
|
457
|
+
EcwtParseError,
|
|
435
458
|
EcwtRevokedError
|
|
436
459
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ecwt",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0-beta.2",
|
|
4
4
|
"description": "Encrypted CBOR-encoded Web Token",
|
|
5
5
|
"main": "src/main.js",
|
|
6
6
|
"type": "module",
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
},
|
|
21
21
|
"peerDependencies": {
|
|
22
22
|
"@kirick/snowflake": "^0.2.1-beta.1",
|
|
23
|
-
"lru-cache": "^
|
|
23
|
+
"lru-cache": "^9 || ^10",
|
|
24
24
|
"redis": "^4"
|
|
25
25
|
},
|
|
26
26
|
"devDependencies": {
|
|
@@ -28,20 +28,21 @@
|
|
|
28
28
|
"eslint": "8.41.0",
|
|
29
29
|
"eslint-config-xo": "0.43.1",
|
|
30
30
|
"eslint-plugin-import": "2.27.5",
|
|
31
|
-
"eslint-plugin-jsdoc": "
|
|
31
|
+
"eslint-plugin-jsdoc": "46.9.1",
|
|
32
32
|
"eslint-plugin-node": "11.1.0",
|
|
33
33
|
"eslint-plugin-promise": "6.1.1",
|
|
34
34
|
"eslint-plugin-unicorn": "47.0.0",
|
|
35
|
-
"
|
|
36
|
-
"
|
|
35
|
+
"valibot": "^0.24.1",
|
|
36
|
+
"vitest": "1.3.1"
|
|
37
37
|
},
|
|
38
38
|
"scripts": {
|
|
39
|
-
"test": "bun run redis-up && npm run test-node
|
|
40
|
-
"test-node": "
|
|
39
|
+
"test": "bun run redis-up && bun test && npm run test-node",
|
|
40
|
+
"test-node": "vitest run --no-file-parallelism",
|
|
41
41
|
"coverage": "bun run redis-up && bun test --coverage",
|
|
42
42
|
"build": "bun run build-cjs",
|
|
43
43
|
"build-cjs": "bunx esbuild --bundle --platform=node --format=cjs --packages=external --outfile=dist/ecwt.cjs src/main.js",
|
|
44
|
-
"npm-publish": "bun run build && bun run eslint . && bun run test && npm publish
|
|
44
|
+
"npm-publish": "bun run build && bun run eslint . && bun run test && npm publish; bun run redis-down",
|
|
45
|
+
"npm-publish-beta": "bun run build && bun run eslint . && bun run test && npm publish --tag beta; bun run redis-down",
|
|
45
46
|
"redis-up": "docker ps | grep test-redis-ecwt-test >/dev/null || docker run --rm -d -p 16274:6379 --name test-redis-ecwt-test redis:7-alpine",
|
|
46
47
|
"redis-down": "docker stop test-redis-ecwt-test"
|
|
47
48
|
},
|
package/src/factory.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
|
|
2
2
|
import { SnowflakeFactory } from '@kirick/snowflake';
|
|
3
3
|
import {
|
|
4
|
+
Encoder as CborEncoder,
|
|
4
5
|
encode as cborEncode,
|
|
5
6
|
decode as cborDecode } from 'cbor-x';
|
|
6
7
|
import {
|
|
@@ -13,7 +14,8 @@ import { base62 } from './utils/base62.js';
|
|
|
13
14
|
import {
|
|
14
15
|
InvalidPackageInstanceError,
|
|
15
16
|
EcwtExpiredError,
|
|
16
|
-
EcwtRevokedError
|
|
17
|
+
EcwtRevokedError,
|
|
18
|
+
EcwtParseError } from './utils/errors.js';
|
|
17
19
|
|
|
18
20
|
const REDIS_PREFIX = '@ecwt:';
|
|
19
21
|
|
|
@@ -39,8 +41,8 @@ export class EcwtFactory {
|
|
|
39
41
|
#redis_keys = {};
|
|
40
42
|
#encryption_key;
|
|
41
43
|
|
|
42
|
-
#
|
|
43
|
-
#
|
|
44
|
+
#validator;
|
|
45
|
+
#cborEncoder;
|
|
44
46
|
|
|
45
47
|
/**
|
|
46
48
|
*
|
|
@@ -51,7 +53,8 @@ export class EcwtFactory {
|
|
|
51
53
|
* @param {object} param0.options -
|
|
52
54
|
* @param {string} [param0.options.namespace] Namespace for Redis keys.
|
|
53
55
|
* @param {Buffer} param0.options.key Encryption key, 64 bytes
|
|
54
|
-
* @param {
|
|
56
|
+
* @param {(value: any) => any} [param0.options.validator] Validator for token data. Should return validated value or throw an error.
|
|
57
|
+
* @param {{ [key: string]: number }} [param0.options.senml_key_map] Payload object keys mapped for their SenML keys.
|
|
55
58
|
*/
|
|
56
59
|
constructor({
|
|
57
60
|
redisClient = null,
|
|
@@ -60,7 +63,8 @@ export class EcwtFactory {
|
|
|
60
63
|
options: {
|
|
61
64
|
namespace = null,
|
|
62
65
|
key,
|
|
63
|
-
|
|
66
|
+
validator,
|
|
67
|
+
senml_key_map,
|
|
64
68
|
},
|
|
65
69
|
}) {
|
|
66
70
|
if (
|
|
@@ -103,8 +107,13 @@ export class EcwtFactory {
|
|
|
103
107
|
|
|
104
108
|
this.#encryption_key = key;
|
|
105
109
|
|
|
106
|
-
this.#
|
|
107
|
-
|
|
110
|
+
this.#validator = validator;
|
|
111
|
+
|
|
112
|
+
if (senml_key_map) {
|
|
113
|
+
this.#cborEncoder = new CborEncoder({
|
|
114
|
+
keyMap: senml_key_map,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
108
117
|
}
|
|
109
118
|
|
|
110
119
|
/**
|
|
@@ -121,19 +130,8 @@ export class EcwtFactory {
|
|
|
121
130
|
ttl,
|
|
122
131
|
} = {},
|
|
123
132
|
) {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
const value = data[key];
|
|
127
|
-
const validator = this.#schema[key];
|
|
128
|
-
|
|
129
|
-
if (
|
|
130
|
-
typeof validator === 'function'
|
|
131
|
-
&& validator(value) !== true
|
|
132
|
-
) {
|
|
133
|
-
throw new TypeError(`Value "${value}" of property "${key}" is invalid.`);
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
payload.push(value);
|
|
133
|
+
if (typeof this.#validator === 'function') {
|
|
134
|
+
data = this.#validator(data);
|
|
137
135
|
}
|
|
138
136
|
|
|
139
137
|
if (
|
|
@@ -146,11 +144,14 @@ export class EcwtFactory {
|
|
|
146
144
|
|
|
147
145
|
const snowflake = await this.#snowflakeFactory.createSafe();
|
|
148
146
|
|
|
149
|
-
const
|
|
147
|
+
const payload = [
|
|
150
148
|
snowflake.toBuffer(),
|
|
151
149
|
ttl,
|
|
152
|
-
|
|
153
|
-
]
|
|
150
|
+
data,
|
|
151
|
+
];
|
|
152
|
+
const token_raw = this.#cborEncoder
|
|
153
|
+
? this.#cborEncoder.encode(payload)
|
|
154
|
+
: cborEncode(payload);
|
|
154
155
|
|
|
155
156
|
const token_encrypted = await evilcryptV2.encrypt(
|
|
156
157
|
token_raw,
|
|
@@ -205,29 +206,45 @@ export class EcwtFactory {
|
|
|
205
206
|
let data;
|
|
206
207
|
|
|
207
208
|
const cached_entry = this.#lruCache?.info(token);
|
|
208
|
-
// token is cached
|
|
209
|
+
// token is not cached
|
|
209
210
|
if (cached_entry === undefined) {
|
|
210
211
|
const token_encrypted = Buffer.from(
|
|
211
212
|
base62.decode(token),
|
|
212
213
|
);
|
|
213
214
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
215
|
+
let token_raw;
|
|
216
|
+
try {
|
|
217
|
+
token_raw = await evilcryptDecrypt(
|
|
218
|
+
token_encrypted,
|
|
219
|
+
this.#encryption_key,
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
catch {
|
|
223
|
+
throw new EcwtParseError();
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const payload = this.#cborEncoder
|
|
227
|
+
? this.#cborEncoder.decode(token_raw)
|
|
228
|
+
: cborDecode(token_raw);
|
|
218
229
|
|
|
219
230
|
const [
|
|
220
231
|
snowflake_buffer,
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
232
|
+
] = payload;
|
|
233
|
+
[
|
|
234
|
+
,
|
|
235
|
+
ttl_initial,
|
|
236
|
+
data,
|
|
237
|
+
] = payload;
|
|
224
238
|
|
|
225
239
|
snowflake = this.#snowflakeFactory.parse(snowflake_buffer);
|
|
226
|
-
ttl_initial = _ttl_initial;
|
|
227
240
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
241
|
+
if (typeof this.#validator === 'function') {
|
|
242
|
+
try {
|
|
243
|
+
data = this.#validator(data);
|
|
244
|
+
}
|
|
245
|
+
catch {
|
|
246
|
+
throw new EcwtParseError();
|
|
247
|
+
}
|
|
231
248
|
}
|
|
232
249
|
|
|
233
250
|
this.#setCache(
|
package/src/main.js
CHANGED
package/src/token.js
CHANGED
|
@@ -120,6 +120,10 @@ export class Ecwt {
|
|
|
120
120
|
return this.#ttl_initial - toSeconds(Date.now() - this.snowflake.timestamp);
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
+
/**
|
|
124
|
+
* Revokes token.
|
|
125
|
+
* @returns {Promise<void>} -
|
|
126
|
+
*/
|
|
123
127
|
/* async */ revoke() {
|
|
124
128
|
return this.#ecwtFactory._revoke({
|
|
125
129
|
token_id: this.id,
|
package/src/utils/errors.js
CHANGED
|
@@ -5,6 +5,12 @@ export class InvalidPackageInstanceError extends TypeError {
|
|
|
5
5
|
}
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
+
export class EcwtParseError extends Error {
|
|
9
|
+
constructor() {
|
|
10
|
+
super('Cannot parse data to Ecwt token.');
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
8
14
|
export class EcwtInvalidError extends Error {
|
|
9
15
|
constructor(ecwt) {
|
|
10
16
|
super('Ecwt token is invalid.');
|