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 CHANGED
@@ -69,32 +69,24 @@ const lruCache = new LRUCache({
69
69
 
70
70
  #### Validation library of your choice (optional)
71
71
 
72
- To reduce the size of the token, ECWT does not include object keys in the payload. By specifying the schema, you also can validate the payloads.
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
- number,
79
- string,
80
- maxValue,
81
- maxLength,
82
- safeParse } from 'valibot';
83
-
84
- const schema = {
85
- user_id: (value) => safeParse(
86
- number([
87
- maxValue(10),
88
- ]),
89
- value,
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
- [key: string]: (value: any) => boolean,
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 parse tokens.
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
- #schema;
208
- #schema_keys_sorted;
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 {{ [key: string]: (value: any) => boolean }} param0.options.schema Schema for token data. Each property is a validator function that returns true if value is valid.
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
- schema = {}
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.#schema = schema;
257
- this.#schema_keys_sorted = Object.keys(schema).sort();
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
- const payload = [];
271
- for (const key of this.#schema_keys_sorted) {
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 token_raw = (0, import_cbor_x.encode)([
293
+ const payload = [
284
294
  snowflake.toBuffer(),
285
295
  ttl,
286
- payload
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
- const token_raw = await (0, import_evilcrypt.decrypt)(
339
- token_encrypted,
340
- this.#encryption_key
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
- _ttl_initial,
345
- payload
346
- ] = (0, import_cbor_x.decode)(token_raw);
360
+ snowflake_buffer
361
+ ] = payload;
362
+ [
363
+ ,
364
+ ttl_initial,
365
+ data
366
+ ] = payload;
347
367
  snowflake = this.#snowflakeFactory.parse(snowflake_buffer);
348
- ttl_initial = _ttl_initial;
349
- data = {};
350
- for (const [index, key] of this.#schema_keys_sorted.entries()) {
351
- data[key] = payload[index];
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.1.1-beta.2",
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": "^7 || ^8 || ^9 || ^10",
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": "^46.9.1",
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
- "jest": "^29.7.0",
36
- "valibot": "^0.24.1"
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 && bun test",
40
- "test-node": "NODE_OPTIONS=--experimental-vm-modules jest --forceExit",
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 --tag beta; bun run redis-down",
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 } from './utils/errors.js';
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
- #schema;
43
- #schema_keys_sorted;
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 {{ [key: string]: (value: any) => boolean }} param0.options.schema Schema for token data. Each property is a validator function that returns true if value is valid.
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
- schema = {},
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.#schema = schema;
107
- this.#schema_keys_sorted = Object.keys(schema).sort();
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
- const payload = [];
125
- for (const key of this.#schema_keys_sorted) {
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 token_raw = cborEncode([
147
+ const payload = [
150
148
  snowflake.toBuffer(),
151
149
  ttl,
152
- payload,
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
- const token_raw = await evilcryptDecrypt(
215
- token_encrypted,
216
- this.#encryption_key,
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
- _ttl_initial,
222
- payload,
223
- ] = cborDecode(token_raw);
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
- data = {};
229
- for (const [ index, key ] of this.#schema_keys_sorted.entries()) {
230
- data[key] = payload[index];
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
@@ -2,6 +2,7 @@
2
2
  export { EcwtFactory } from './factory.js';
3
3
  export { Ecwt } from './token.js';
4
4
  export {
5
+ EcwtParseError,
5
6
  EcwtInvalidError,
6
7
  EcwtExpiredError,
7
8
  EcwtRevokedError,
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,
@@ -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.');