ecwt 0.1.0-beta.3 → 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
@@ -147,19 +146,24 @@ create(
147
146
  [key: string]: any,
148
147
  },
149
148
  options: {
150
- ttl: number?,
149
+ ttl: number | null,
151
150
  }
152
151
  ): Promise<Ecwt>
153
152
  ```
154
153
 
155
154
  Creates a token.
156
155
 
157
- `options.ttl` specifies the time to live of the token in seconds. If not specified, the token will not expire.
156
+ `options.ttl` specifies the time to live of the token in seconds. If set to null, token will never expire.
157
+
158
+ > **Be careful with `ttl = null`!**
159
+ >
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.
158
161
 
159
162
  Returns `Ecwt` instance.
160
163
 
161
164
  ```javascript
162
- const ecwt_token = await ecwtFactory.create(
165
+ // Example
166
+ const ecwt = await ecwtFactory.create(
163
167
  {
164
168
  user_id: 1,
165
169
  nick: 'kirick',
@@ -190,40 +194,44 @@ Returns `Ecwt` instance.
190
194
  If the token is invalid, throws `EcwtInvalidError` which contains `Ecwt` instance in the `ecwt` property.
191
195
 
192
196
  ```javascript
193
- const ecwt_token = await ecwtFactory.verify(token);
197
+ const ecwt = await ecwtFactory.verify(token);
194
198
  ```
195
199
 
196
200
  ### `Ecwt`
197
201
 
198
- Represents the token. It cannot be created by the user.
202
+ Represents the token. Its counstructor cannot be called by the user.
199
203
 
200
204
  ```javascript
201
205
  import { Ecwt } from 'ecwt';
202
206
  ```
203
207
 
204
- #### Class property `token: string`
208
+ #### Class property `readonly token: string`
205
209
 
206
210
  The string representation of the token.
207
211
 
208
- #### Class property `id: string`
212
+ #### Class property `readonly id: string`
209
213
 
210
214
  The unique ID of the token.
211
215
 
212
- #### Class property `snowflake: Snowflake`
216
+ #### Class property `readonly snowflake: Snowflake`
213
217
 
214
218
  The `Snowflake` instance of the token. For documentation, see [snowflake-js repository](https://github.com/kirick13/snowflake-js).
215
219
 
216
- #### Class property `ts_expired: number`
220
+ #### Class property `readonly ts_expired: number | null`
221
+
222
+ The timestamp of the token expiration in seconds. Equals to `null` if the token does not expire.
217
223
 
218
- The timestamp of the token expiration in seconds. If the token does not expire, it is `Number.POSITIVE_INFINITY`.
224
+ #### Class property `readonly data: { [key: string]: any }`
219
225
 
220
- #### Class property `ttl: number`
226
+ The payload of the token.
221
227
 
222
- Current the time to live of the token in seconds. If the token does not expire, it is `Number.POSITIVE_INFINITY`.
228
+ #### Class method `getTTL`
223
229
 
224
- #### Class property `data: { [key: string]: any }`
230
+ ```typescript
231
+ getTTL(): number | null
232
+ ```
225
233
 
226
- The payload of the token.
234
+ Returns current the time to live of the token in seconds. If the token does not expire, returns `null`.
227
235
 
228
236
  #### Class method `revoke`
229
237
 
@@ -231,4 +239,4 @@ The payload of the token.
231
239
  revoke(): Promise<void>
232
240
  ```
233
241
 
234
- Revokes the token. It will be impossible to verify it after that.
242
+ Revokes the token. Attempts to verify the revoked token will throw `EcwtRevokedError`.
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
 
@@ -47,18 +50,57 @@ function toSeconds(value) {
47
50
  }
48
51
 
49
52
  // src/token.js
53
+ function assign(target, key, value) {
54
+ Object.defineProperty(
55
+ target,
56
+ key,
57
+ {
58
+ value,
59
+ enumerable: true,
60
+ writable: false,
61
+ configurable: false
62
+ }
63
+ );
64
+ }
50
65
  var Ecwt = class {
51
66
  #ecwtFactory;
52
- #token;
53
- #snowflake;
54
67
  #ttl_initial;
55
- #data;
68
+ /**
69
+ * Token string representation.
70
+ * @type {string}
71
+ * @readonly
72
+ */
73
+ token;
74
+ /**
75
+ * Token ID.
76
+ * @type {string}
77
+ * @readonly
78
+ */
79
+ id;
80
+ /**
81
+ * Snowflake associated with token.
82
+ * @type {Snowflake}
83
+ * @readonly
84
+ */
85
+ snowflake;
86
+ /**
87
+ * Timestamp when token expires in seconds.
88
+ * @type {number?}
89
+ * @readonly
90
+ */
91
+ ts_expired;
92
+ /**
93
+ * Data stored in token.
94
+ * @type {{ [key: string]: any }}
95
+ * @readonly
96
+ */
97
+ data;
56
98
  /**
57
99
  * @param {EcwtFactory} ecwtFactory -
58
100
  * @param {object} options -
59
101
  * @param {string} options.token String representation of token.
60
102
  * @param {Snowflake} options.snowflake -
61
- * @param {number | null} options.ttl_initial Time to live in seconds at the moment of token creation.
103
+ * @param {number?} options.ttl_initial Time to live in seconds at the moment of token creation.
62
104
  * @param {object} options.data Data stored in token.
63
105
  */
64
106
  constructor(ecwtFactory, {
@@ -68,45 +110,50 @@ var Ecwt = class {
68
110
  data
69
111
  }) {
70
112
  this.#ecwtFactory = ecwtFactory;
71
- this.#token = token;
72
- this.#snowflake = snowflake;
73
113
  this.#ttl_initial = ttl_initial;
74
- this.#data = Object.freeze(data);
75
- }
76
- get token() {
77
- return this.#token;
78
- }
79
- get id() {
80
- return this.#snowflake.base62;
81
- }
82
- get snowflake() {
83
- return this.#snowflake;
114
+ assign(this, "token", token);
115
+ assign(
116
+ this,
117
+ "id",
118
+ snowflake.toBase62()
119
+ );
120
+ assign(this, "snowflake", snowflake);
121
+ assign(
122
+ this,
123
+ "ts_expired",
124
+ this.#getTimestampExpired()
125
+ );
126
+ assign(
127
+ this,
128
+ "data",
129
+ Object.freeze(data)
130
+ );
84
131
  }
85
- get ts_expired() {
132
+ #getTimestampExpired() {
86
133
  if (this.#ttl_initial === null) {
87
- return Number.POSITIVE_INFINITY;
134
+ return null;
88
135
  }
89
- return toSeconds(this.#snowflake.timestamp) + this.#ttl_initial;
136
+ return toSeconds(this.snowflake.timestamp) + this.#ttl_initial;
90
137
  }
91
138
  /**
92
- * Time to live in seconds.
93
- * @type {number}
94
- * @readonly
139
+ * Actual time to live in seconds.
140
+ * @returns {number | null} -
95
141
  */
96
- get ttl() {
142
+ getTTL() {
97
143
  if (this.#ttl_initial === null) {
98
- return Number.POSITIVE_INFINITY;
144
+ return null;
99
145
  }
100
- return this.#ttl_initial - toSeconds(Date.now() - this.#snowflake.timestamp);
101
- }
102
- get data() {
103
- return this.#data;
146
+ return this.#ttl_initial - toSeconds(Date.now() - this.snowflake.timestamp);
104
147
  }
148
+ /**
149
+ * Revokes token.
150
+ * @returns {Promise<void>} -
151
+ */
105
152
  /* async */
106
153
  revoke() {
107
154
  return this.#ecwtFactory._revoke({
108
155
  token_id: this.id,
109
- ts_ms_created: this.#snowflake.timestamp,
156
+ ts_ms_created: this.snowflake.timestamp,
110
157
  ttl_initial: this.#ttl_initial
111
158
  });
112
159
  }
@@ -122,6 +169,11 @@ var InvalidPackageInstanceError = class extends TypeError {
122
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.`);
123
170
  }
124
171
  };
172
+ var EcwtParseError = class extends Error {
173
+ constructor() {
174
+ super("Cannot parse data to Ecwt token.");
175
+ }
176
+ };
125
177
  var EcwtInvalidError = class extends Error {
126
178
  constructor(ecwt) {
127
179
  super("Ecwt token is invalid.");
@@ -161,8 +213,8 @@ var EcwtFactory = class {
161
213
  #snowflakeFactory;
162
214
  #redis_keys = {};
163
215
  #encryption_key;
164
- #schema;
165
- #schema_keys_sorted;
216
+ #validator;
217
+ #cborEncoder;
166
218
  /**
167
219
  *
168
220
  * @param {object} param0 -
@@ -172,7 +224,8 @@ var EcwtFactory = class {
172
224
  * @param {object} param0.options -
173
225
  * @param {string} [param0.options.namespace] Namespace for Redis keys.
174
226
  * @param {Buffer} param0.options.key Encryption key, 64 bytes
175
- * @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.
176
229
  */
177
230
  constructor({
178
231
  redisClient: redisClient2 = null,
@@ -181,7 +234,8 @@ var EcwtFactory = class {
181
234
  options: {
182
235
  namespace = null,
183
236
  key,
184
- schema = {}
237
+ validator,
238
+ senml_key_map
185
239
  }
186
240
  }) {
187
241
  if (redisClient2 !== null && (redisClient2.constructor.name !== redis_client_constructor_name || getAllKeysList(redisClient2) !== redis_client_keys)) {
@@ -210,8 +264,12 @@ var EcwtFactory = class {
210
264
  this.#snowflakeFactory = snowflakeFactory;
211
265
  this.#redis_keys.revoked = `${REDIS_PREFIX}${namespace}:revoked`;
212
266
  this.#encryption_key = key;
213
- this.#schema = schema;
214
- 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
+ }
215
273
  }
216
274
  /**
217
275
  * Creates new token.
@@ -224,24 +282,19 @@ var EcwtFactory = class {
224
282
  async create(data, {
225
283
  ttl
226
284
  } = {}) {
227
- const payload = [];
228
- for (const key of this.#schema_keys_sorted) {
229
- const value = data[key];
230
- const validator = this.#schema[key];
231
- if (typeof validator === "function" && validator(value) !== true) {
232
- throw new TypeError(`Value "${value}" of property "${key}" is invalid.`);
233
- }
234
- payload.push(value);
285
+ if (typeof this.#validator === "function") {
286
+ data = this.#validator(data);
235
287
  }
236
- if (typeof ttl !== "number" && Number.isNaN(ttl) !== true || ttl === Number.POSITIVE_INFINITY) {
237
- ttl = null;
288
+ if (typeof ttl !== "number" && Number.isNaN(ttl) !== true && ttl !== null) {
289
+ throw new TypeError("TTL must be a number or null.");
238
290
  }
239
291
  const snowflake = await this.#snowflakeFactory.createSafe();
240
- const token_raw = (0, import_cbor_x.encode)([
241
- snowflake.buffer,
292
+ const payload = [
293
+ snowflake.toBuffer(),
242
294
  ttl,
243
- payload
244
- ]);
295
+ data
296
+ ];
297
+ const token_raw = this.#cborEncoder ? this.#cborEncoder.encode(payload) : (0, import_cbor_x.encode)(payload);
245
298
  const token_encrypted = await import_evilcrypt.v2.encrypt(
246
299
  token_raw,
247
300
  this.#encryption_key
@@ -292,20 +345,31 @@ var EcwtFactory = class {
292
345
  const token_encrypted = Buffer.from(
293
346
  base62.decode(token)
294
347
  );
295
- const token_raw = await (0, import_evilcrypt.decrypt)(
296
- token_encrypted,
297
- this.#encryption_key
298
- );
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);
299
358
  const [
300
- snowflake_buffer,
301
- _ttl_initial,
302
- payload
303
- ] = (0, import_cbor_x.decode)(token_raw);
359
+ snowflake_buffer
360
+ ] = payload;
361
+ [
362
+ ,
363
+ ttl_initial,
364
+ data
365
+ ] = payload;
304
366
  snowflake = this.#snowflakeFactory.parse(snowflake_buffer);
305
- ttl_initial = _ttl_initial;
306
- data = {};
307
- for (const [index, key] of this.#schema_keys_sorted.entries()) {
308
- data[key] = payload[index];
367
+ if (typeof this.#validator === "function") {
368
+ try {
369
+ data = this.#validator(data);
370
+ } catch {
371
+ throw new EcwtParseError();
372
+ }
309
373
  }
310
374
  this.#setCache(
311
375
  token,
@@ -360,6 +424,7 @@ var EcwtFactory = class {
360
424
  ttl_initial
361
425
  }) {
362
426
  if (this.#redisClient) {
427
+ ttl_initial = ttl_initial ?? Number.MAX_SAFE_INTEGER;
363
428
  const ts_ms_expired = ts_ms_created + ttl_initial * 1e3;
364
429
  if (ts_ms_expired > Date.now()) {
365
430
  await this.#redisClient.sendCommand([
@@ -385,5 +450,8 @@ var EcwtFactory = class {
385
450
  // Annotate the CommonJS export names for ESM import in node:
386
451
  0 && (module.exports = {
387
452
  Ecwt,
388
- EcwtFactory
453
+ EcwtExpiredError,
454
+ EcwtFactory,
455
+ EcwtInvalidError,
456
+ EcwtRevokedError
389
457
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ecwt",
3
- "version": "0.1.0-beta.3",
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",
@@ -19,8 +19,8 @@
19
19
  "evilcrypt": "0.2.0-beta.4"
20
20
  },
21
21
  "peerDependencies": {
22
- "@kirick/snowflake": "^0.2.0-beta.7",
23
- "lru-cache": "^7 || ^8 || ^9 || ^10",
22
+ "@kirick/snowflake": "^0.2.1-beta.1",
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,38 +130,28 @@ 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 (
140
- (
141
- typeof ttl !== 'number'
142
- && Number.isNaN(ttl) !== true
143
- )
144
- || ttl === Number.POSITIVE_INFINITY
138
+ typeof ttl !== 'number'
139
+ && Number.isNaN(ttl) !== true
140
+ && ttl !== null
145
141
  ) {
146
- ttl = null;
142
+ throw new TypeError('TTL must be a number or null.');
147
143
  }
148
144
 
149
145
  const snowflake = await this.#snowflakeFactory.createSafe();
150
146
 
151
- const token_raw = cborEncode([
152
- snowflake.buffer,
147
+ const payload = [
148
+ snowflake.toBuffer(),
153
149
  ttl,
154
- payload,
155
- ]);
150
+ data,
151
+ ];
152
+ const token_raw = this.#cborEncoder
153
+ ? this.#cborEncoder.encode(payload)
154
+ : cborEncode(payload);
156
155
 
157
156
  const token_encrypted = await evilcryptV2.encrypt(
158
157
  token_raw,
@@ -207,29 +206,45 @@ export class EcwtFactory {
207
206
  let data;
208
207
 
209
208
  const cached_entry = this.#lruCache?.info(token);
210
- // token is cached
209
+ // token is not cached
211
210
  if (cached_entry === undefined) {
212
211
  const token_encrypted = Buffer.from(
213
212
  base62.decode(token),
214
213
  );
215
214
 
216
- const token_raw = await evilcryptDecrypt(
217
- token_encrypted,
218
- this.#encryption_key,
219
- );
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);
220
229
 
221
230
  const [
222
231
  snowflake_buffer,
223
- _ttl_initial,
224
- payload,
225
- ] = cborDecode(token_raw);
232
+ ] = payload;
233
+ [
234
+ ,
235
+ ttl_initial,
236
+ data,
237
+ ] = payload;
226
238
 
227
239
  snowflake = this.#snowflakeFactory.parse(snowflake_buffer);
228
- ttl_initial = _ttl_initial;
229
240
 
230
- data = {};
231
- for (const [ index, key ] of this.#schema_keys_sorted.entries()) {
232
- 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
+ }
233
248
  }
234
249
 
235
250
  this.#setCache(
@@ -299,6 +314,8 @@ export class EcwtFactory {
299
314
  ttl_initial,
300
315
  }) {
301
316
  if (this.#redisClient) {
317
+ ttl_initial = ttl_initial ?? Number.MAX_SAFE_INTEGER;
318
+
302
319
  const ts_ms_expired = ts_ms_created + (ttl_initial * 1000);
303
320
  if (ts_ms_expired > Date.now()) {
304
321
  await this.#redisClient.sendCommand([
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
@@ -2,24 +2,70 @@
2
2
  import { toSeconds } from './utils/time.js';
3
3
 
4
4
  /**
5
- * @typedef {import('@kirick/snowflake/src/snowflake.js').Snowflake} Snowflake
5
+ * @typedef {import('@kirick/snowflake').Snowflake} Snowflake
6
6
  * @typedef {import('./factory.js').EcwtFactory} EcwtFactory
7
7
  */
8
8
 
9
+ /**
10
+ * Assigns property to object.
11
+ * @param {object} target -
12
+ * @param {string} key -
13
+ * @param {any} value -
14
+ */
15
+ function assign(target, key, value) {
16
+ Object.defineProperty(
17
+ target,
18
+ key,
19
+ {
20
+ value,
21
+ enumerable: true,
22
+ writable: false,
23
+ configurable: false,
24
+ },
25
+ );
26
+ }
27
+
9
28
  export class Ecwt {
10
29
  #ecwtFactory;
11
-
12
- #token;
13
- #snowflake;
14
30
  #ttl_initial;
15
- #data;
31
+
32
+ /**
33
+ * Token string representation.
34
+ * @type {string}
35
+ * @readonly
36
+ */
37
+ token;
38
+ /**
39
+ * Token ID.
40
+ * @type {string}
41
+ * @readonly
42
+ */
43
+ id;
44
+ /**
45
+ * Snowflake associated with token.
46
+ * @type {Snowflake}
47
+ * @readonly
48
+ */
49
+ snowflake;
50
+ /**
51
+ * Timestamp when token expires in seconds.
52
+ * @type {number?}
53
+ * @readonly
54
+ */
55
+ ts_expired;
56
+ /**
57
+ * Data stored in token.
58
+ * @type {{ [key: string]: any }}
59
+ * @readonly
60
+ */
61
+ data;
16
62
 
17
63
  /**
18
64
  * @param {EcwtFactory} ecwtFactory -
19
65
  * @param {object} options -
20
66
  * @param {string} options.token String representation of token.
21
67
  * @param {Snowflake} options.snowflake -
22
- * @param {number | null} options.ttl_initial Time to live in seconds at the moment of token creation.
68
+ * @param {number?} options.ttl_initial Time to live in seconds at the moment of token creation.
23
69
  * @param {object} options.data Data stored in token.
24
70
  */
25
71
  constructor(
@@ -33,53 +79,55 @@ export class Ecwt {
33
79
  ) {
34
80
  this.#ecwtFactory = ecwtFactory;
35
81
 
36
- this.#token = token;
37
- this.#snowflake = snowflake;
38
82
  this.#ttl_initial = ttl_initial;
39
- this.#data = Object.freeze(data);
40
- }
41
-
42
- get token() {
43
- return this.#token;
44
- }
45
-
46
- get id() {
47
- return this.#snowflake.base62;
48
- }
49
83
 
50
- get snowflake() {
51
- return this.#snowflake;
84
+ assign(this, 'token', token);
85
+ assign(
86
+ this,
87
+ 'id',
88
+ snowflake.toBase62(),
89
+ );
90
+ assign(this, 'snowflake', snowflake);
91
+ assign(
92
+ this,
93
+ 'ts_expired',
94
+ this.#getTimestampExpired(),
95
+ );
96
+ assign(
97
+ this,
98
+ 'data',
99
+ Object.freeze(data),
100
+ );
52
101
  }
53
102
 
54
- get ts_expired() {
103
+ #getTimestampExpired() {
55
104
  if (this.#ttl_initial === null) {
56
- return Number.POSITIVE_INFINITY;
105
+ return null;
57
106
  }
58
107
 
59
- return toSeconds(this.#snowflake.timestamp) + this.#ttl_initial;
108
+ return toSeconds(this.snowflake.timestamp) + this.#ttl_initial;
60
109
  }
61
110
 
62
111
  /**
63
- * Time to live in seconds.
64
- * @type {number}
65
- * @readonly
112
+ * Actual time to live in seconds.
113
+ * @returns {number | null} -
66
114
  */
67
- get ttl() {
115
+ getTTL() {
68
116
  if (this.#ttl_initial === null) {
69
- return Number.POSITIVE_INFINITY;
117
+ return null;
70
118
  }
71
119
 
72
- return this.#ttl_initial - toSeconds(Date.now() - this.#snowflake.timestamp);
73
- }
74
-
75
- get data() {
76
- return this.#data;
120
+ return this.#ttl_initial - toSeconds(Date.now() - this.snowflake.timestamp);
77
121
  }
78
122
 
123
+ /**
124
+ * Revokes token.
125
+ * @returns {Promise<void>} -
126
+ */
79
127
  /* async */ revoke() {
80
128
  return this.#ecwtFactory._revoke({
81
129
  token_id: this.id,
82
- ts_ms_created: this.#snowflake.timestamp,
130
+ ts_ms_created: this.snowflake.timestamp,
83
131
  ttl_initial: this.#ttl_initial,
84
132
  });
85
133
  }
@@ -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.');