ecwt 0.1.1-beta.1 → 0.1.1-beta.101

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
@@ -156,15 +155,15 @@ Creates a token.
156
155
 
157
156
  `options.ttl` specifies the time to live of the token in seconds. If set to null, token will never expire.
158
157
 
159
- > **Be careful with `ttl=null`!**
158
+ > **Be careful with `ttl = null`!**
160
159
  >
161
- > Revoked tokens are stored in Redis until they expire. But such tokens will be stored in Redis forever, which will lead to uncontrolled Redis database growth.
160
+ > Revoked tokens are stored in Redis until they expire. Never-expiring tokens will be stored in Redis **forever**, which will lead to uncontrolled Redis database growth.
162
161
 
163
162
  Returns `Ecwt` instance.
164
163
 
165
164
  ```javascript
166
165
  // Example
167
- const ecwt_token = await ecwtFactory.create(
166
+ const ecwt = await ecwtFactory.create(
168
167
  {
169
168
  user_id: 1,
170
169
  nick: 'kirick',
@@ -195,12 +194,12 @@ Returns `Ecwt` instance.
195
194
  If the token is invalid, throws `EcwtInvalidError` which contains `Ecwt` instance in the `ecwt` property.
196
195
 
197
196
  ```javascript
198
- const ecwt_token = await ecwtFactory.verify(token);
197
+ const ecwt = await ecwtFactory.verify(token);
199
198
  ```
200
199
 
201
200
  ### `Ecwt`
202
201
 
203
- Represents the token. It cannot be created by the user.
202
+ Represents the token. Its counstructor cannot be called by the user.
204
203
 
205
204
  ```javascript
206
205
  import { Ecwt } from 'ecwt';
package/dist/ecwt.cjs CHANGED
@@ -30,7 +30,10 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  var main_exports = {};
31
31
  __export(main_exports, {
32
32
  Ecwt: () => Ecwt,
33
- EcwtFactory: () => EcwtFactory
33
+ EcwtExpiredError: () => EcwtExpiredError,
34
+ EcwtFactory: () => EcwtFactory,
35
+ EcwtInvalidError: () => EcwtInvalidError,
36
+ EcwtRevokedError: () => EcwtRevokedError
34
37
  });
35
38
  module.exports = __toCommonJS(main_exports);
36
39
 
@@ -142,6 +145,10 @@ var Ecwt = class {
142
145
  }
143
146
  return this.#ttl_initial - toSeconds(Date.now() - this.snowflake.timestamp);
144
147
  }
148
+ /**
149
+ * Revokes token.
150
+ * @returns {Promise<void>} -
151
+ */
145
152
  /* async */
146
153
  revoke() {
147
154
  return this.#ecwtFactory._revoke({
@@ -162,6 +169,11 @@ var InvalidPackageInstanceError = class extends TypeError {
162
169
  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.`);
163
170
  }
164
171
  };
172
+ var EcwtParseError = class extends Error {
173
+ constructor() {
174
+ super("Cannot parse data to Ecwt token.");
175
+ }
176
+ };
165
177
  var EcwtInvalidError = class extends Error {
166
178
  constructor(ecwt) {
167
179
  super("Ecwt token is invalid.");
@@ -201,8 +213,8 @@ var EcwtFactory = class {
201
213
  #snowflakeFactory;
202
214
  #redis_keys = {};
203
215
  #encryption_key;
204
- #schema;
205
- #schema_keys_sorted;
216
+ #validator;
217
+ #cborEncoder;
206
218
  /**
207
219
  *
208
220
  * @param {object} param0 -
@@ -212,7 +224,8 @@ var EcwtFactory = class {
212
224
  * @param {object} param0.options -
213
225
  * @param {string} [param0.options.namespace] Namespace for Redis keys.
214
226
  * @param {Buffer} param0.options.key Encryption key, 64 bytes
215
- * @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.
227
+ * @param {(value: any) => any} [param0.options.validator] Validator for token data. Should return validated value or throw an error.
228
+ * @param {{ [key: string]: number }} [param0.options.senml_key_map] Payload object keys mapped for their SenML keys.
216
229
  */
217
230
  constructor({
218
231
  redisClient: redisClient2 = null,
@@ -221,7 +234,8 @@ var EcwtFactory = class {
221
234
  options: {
222
235
  namespace = null,
223
236
  key,
224
- schema = {}
237
+ validator,
238
+ senml_key_map
225
239
  }
226
240
  }) {
227
241
  if (redisClient2 !== null && (redisClient2.constructor.name !== redis_client_constructor_name || getAllKeysList(redisClient2) !== redis_client_keys)) {
@@ -250,8 +264,12 @@ var EcwtFactory = class {
250
264
  this.#snowflakeFactory = snowflakeFactory;
251
265
  this.#redis_keys.revoked = `${REDIS_PREFIX}${namespace}:revoked`;
252
266
  this.#encryption_key = key;
253
- this.#schema = schema;
254
- this.#schema_keys_sorted = Object.keys(schema).sort();
267
+ this.#validator = validator;
268
+ if (senml_key_map) {
269
+ this.#cborEncoder = new import_cbor_x.Encoder({
270
+ keyMap: senml_key_map
271
+ });
272
+ }
255
273
  }
256
274
  /**
257
275
  * Creates new token.
@@ -264,24 +282,19 @@ var EcwtFactory = class {
264
282
  async create(data, {
265
283
  ttl
266
284
  } = {}) {
267
- const payload = [];
268
- for (const key of this.#schema_keys_sorted) {
269
- const value = data[key];
270
- const validator = this.#schema[key];
271
- if (typeof validator === "function" && validator(value) !== true) {
272
- throw new TypeError(`Value "${value}" of property "${key}" is invalid.`);
273
- }
274
- payload.push(value);
285
+ if (typeof this.#validator === "function") {
286
+ data = this.#validator(data);
275
287
  }
276
288
  if (typeof ttl !== "number" && Number.isNaN(ttl) !== true && ttl !== null) {
277
289
  throw new TypeError("TTL must be a number or null.");
278
290
  }
279
291
  const snowflake = await this.#snowflakeFactory.createSafe();
280
- const token_raw = (0, import_cbor_x.encode)([
292
+ const payload = [
281
293
  snowflake.toBuffer(),
282
294
  ttl,
283
- payload
284
- ]);
295
+ data
296
+ ];
297
+ const token_raw = this.#cborEncoder ? this.#cborEncoder.encode(payload) : (0, import_cbor_x.encode)(payload);
285
298
  const token_encrypted = await import_evilcrypt.v2.encrypt(
286
299
  token_raw,
287
300
  this.#encryption_key
@@ -332,20 +345,31 @@ var EcwtFactory = class {
332
345
  const token_encrypted = Buffer.from(
333
346
  base62.decode(token)
334
347
  );
335
- const token_raw = await (0, import_evilcrypt.decrypt)(
336
- token_encrypted,
337
- this.#encryption_key
338
- );
348
+ let token_raw;
349
+ try {
350
+ token_raw = await (0, import_evilcrypt.decrypt)(
351
+ token_encrypted,
352
+ this.#encryption_key
353
+ );
354
+ } catch {
355
+ throw new EcwtParseError();
356
+ }
357
+ const payload = this.#cborEncoder ? this.#cborEncoder.decode(token_raw) : (0, import_cbor_x.decode)(token_raw);
339
358
  const [
340
- snowflake_buffer,
341
- _ttl_initial,
342
- payload
343
- ] = (0, import_cbor_x.decode)(token_raw);
359
+ snowflake_buffer
360
+ ] = payload;
361
+ [
362
+ ,
363
+ ttl_initial,
364
+ data
365
+ ] = payload;
344
366
  snowflake = this.#snowflakeFactory.parse(snowflake_buffer);
345
- ttl_initial = _ttl_initial;
346
- data = {};
347
- for (const [index, key] of this.#schema_keys_sorted.entries()) {
348
- data[key] = payload[index];
367
+ if (typeof this.#validator === "function") {
368
+ try {
369
+ data = this.#validator(data);
370
+ } catch {
371
+ throw new EcwtParseError();
372
+ }
349
373
  }
350
374
  this.#setCache(
351
375
  token,
@@ -426,5 +450,8 @@ var EcwtFactory = class {
426
450
  // Annotate the CommonJS export names for ESM import in node:
427
451
  0 && (module.exports = {
428
452
  Ecwt,
429
- EcwtFactory
453
+ EcwtExpiredError,
454
+ EcwtFactory,
455
+ EcwtInvalidError,
456
+ EcwtRevokedError
430
457
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ecwt",
3
- "version": "0.1.1-beta.1",
3
+ "version": "0.1.1-beta.101",
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
@@ -1,3 +1,8 @@
1
1
 
2
2
  export { EcwtFactory } from './factory.js';
3
3
  export { Ecwt } from './token.js';
4
+ export {
5
+ EcwtInvalidError,
6
+ EcwtExpiredError,
7
+ EcwtRevokedError,
8
+ } from './utils/errors.js';
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.');