ecwt 0.1.0-beta.2 → 0.1.1-beta.1

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
@@ -1,3 +1,243 @@
1
1
 
2
- # ecwt
2
+ # ECWT
3
3
  Encrypted CBOR-encoded Web Token
4
+
5
+ ## What is it?
6
+
7
+ ECWT is module for creating and verifying encrypted CBOR-encoded Web Tokens. It is designed to be used in situations where JWT is used, but there are major differences:
8
+
9
+ | | JWT | ECWT |
10
+ | --- | --- | --- |
11
+ | Encoding | 🧐 JSON with base64 | ✅ CBOR <br> 2x smaller output |
12
+ | Binary data | 🧐 Double base64 encoding | ✅ Supported out of the box |
13
+ | Security | 📝 Signed <br> Payload is readable by everyone | 🔒 Encrypted <br> Payload is readable only by the private key possessor |
14
+ | Metadata | ➕ Type and algorithm, increases size | ✅ No unnecessary metadata |
15
+ | Revocation | 🧑‍💻 Requires additional implementation | ✅ Included with Redis |
16
+
17
+ ## Installation
18
+
19
+ ECWT depends on other modules, so you need to install them too.
20
+
21
+ ```
22
+ npm install ecwt @kirick/snowflake
23
+ pnpm install ecwt @kirick/snowflake
24
+ bun install ecwt @kirick/snowflake
25
+ ```
26
+
27
+ ### Some dependencies
28
+
29
+ `EcwtFactory` depends on other modules, so you might be need to install them too.
30
+
31
+ #### `@kirick/snowflake` to create unique IDs (required)
32
+
33
+ For documentation, see [snowflake-js repository](https://github.com/kirick13/snowflake-js).
34
+
35
+ ```javascript
36
+ import { SnowflakeFactory } from '@kirick/snowflake';
37
+
38
+ const snowflakeFactory = new SnowflakeFactory({
39
+ server_id: 0,
40
+ worker_id: 0,
41
+ });
42
+ ```
43
+
44
+ #### `redis` to store revoked tokens (optional)
45
+
46
+ ```javascript
47
+ import { createClient } from 'redis';
48
+
49
+ const redisClient = createClient({
50
+ socket: {
51
+ host: 'localhost',
52
+ port: 6379,
53
+ },
54
+ });
55
+
56
+ await redisClient.connect();
57
+ ```
58
+
59
+ #### `lru` to avoid decrypt the same token multiple times (optional)
60
+
61
+ ```javascript
62
+ import { LRUCache } from 'lru-cache';
63
+
64
+ const lruCache = new LRUCache({
65
+ max: 1000, // maximum of 1000 items
66
+ ttl: 60 * 60 * 1000, // 1 hour
67
+ });
68
+ ```
69
+
70
+ #### Validation library of your choice (optional)
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.
73
+
74
+ In our example, we use [valibot](https://valibot.dev) library.
75
+
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
+ };
98
+ ```
99
+
100
+ That schema will prevent creating tokens for users with ID greater than 10 and nicknames longer than 10 characters.
101
+
102
+ ## API
103
+
104
+ ### `EcwtFactory`
105
+
106
+ ```typescript
107
+ constructor({
108
+ redisClient: RedisClientType?,
109
+ lruCache: LRU?,
110
+ snowflakeFactory: SnowflakeFactory,
111
+ options: {
112
+ namespace: string?,
113
+ key: Buffer,
114
+ schema: {
115
+ [key: string]: (value: any) => boolean,
116
+ } = {},
117
+ },
118
+ })
119
+ ```
120
+
121
+ Create your `EcwtFactory` instance to create, and parse tokens.
122
+
123
+ ```javascript
124
+ import { EcwtFactory } from 'ecwt';
125
+
126
+ const ecwtFactory = new EcwtFactory({
127
+ redisClient,
128
+ lruCache,
129
+ snowflakeFactory,
130
+ options: {
131
+ // "options.namespace" is required to identify the storage of revoked tokens in Redis
132
+ namespace: 'test',
133
+ key: Buffer.from(
134
+ '54RoavO+7orGGCKqLXcMwNGFGbcnSEq22f9bJX3lT9lgEPSaRAMBaEnHgMQPTPXcifFvGZmDGzOFqUMfqXsAhQ==',
135
+ 'base64',
136
+ ),
137
+ schema,
138
+ },
139
+ });
140
+ ```
141
+
142
+ #### Class method `create`
143
+
144
+ ```typescript
145
+ create(
146
+ payload: {
147
+ [key: string]: any,
148
+ },
149
+ options: {
150
+ ttl: number | null,
151
+ }
152
+ ): Promise<Ecwt>
153
+ ```
154
+
155
+ Creates a token.
156
+
157
+ `options.ttl` specifies the time to live of the token in seconds. If set to null, token will never expire.
158
+
159
+ > **Be careful with `ttl=null`!**
160
+ >
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.
162
+
163
+ Returns `Ecwt` instance.
164
+
165
+ ```javascript
166
+ // Example
167
+ const ecwt_token = await ecwtFactory.create(
168
+ {
169
+ user_id: 1,
170
+ nick: 'kirick',
171
+ },
172
+ {
173
+ ttl: 30 * 60,
174
+ }
175
+ );
176
+ ```
177
+
178
+ #### Class method `verify`
179
+
180
+ ```typescript
181
+ verify(
182
+ token: string,
183
+ ): Promise<Ecwt>
184
+ ```
185
+
186
+ Parses string representation of the token and verifies it:
187
+
188
+ - to be decrypted properly,
189
+ - for expiration,
190
+ - for revocation (if Redis client is provided),
191
+ - for schema.
192
+
193
+ Returns `Ecwt` instance.
194
+
195
+ If the token is invalid, throws `EcwtInvalidError` which contains `Ecwt` instance in the `ecwt` property.
196
+
197
+ ```javascript
198
+ const ecwt_token = await ecwtFactory.verify(token);
199
+ ```
200
+
201
+ ### `Ecwt`
202
+
203
+ Represents the token. It cannot be created by the user.
204
+
205
+ ```javascript
206
+ import { Ecwt } from 'ecwt';
207
+ ```
208
+
209
+ #### Class property `readonly token: string`
210
+
211
+ The string representation of the token.
212
+
213
+ #### Class property `readonly id: string`
214
+
215
+ The unique ID of the token.
216
+
217
+ #### Class property `readonly snowflake: Snowflake`
218
+
219
+ The `Snowflake` instance of the token. For documentation, see [snowflake-js repository](https://github.com/kirick13/snowflake-js).
220
+
221
+ #### Class property `readonly ts_expired: number | null`
222
+
223
+ The timestamp of the token expiration in seconds. Equals to `null` if the token does not expire.
224
+
225
+ #### Class property `readonly data: { [key: string]: any }`
226
+
227
+ The payload of the token.
228
+
229
+ #### Class method `getTTL`
230
+
231
+ ```typescript
232
+ getTTL(): number | null
233
+ ```
234
+
235
+ Returns current the time to live of the token in seconds. If the token does not expire, returns `null`.
236
+
237
+ #### Class method `revoke`
238
+
239
+ ```typescript
240
+ revoke(): Promise<void>
241
+ ```
242
+
243
+ Revokes the token. Attempts to verify the revoked token will throw `EcwtRevokedError`.
package/dist/ecwt.cjs CHANGED
@@ -47,18 +47,57 @@ function toSeconds(value) {
47
47
  }
48
48
 
49
49
  // src/token.js
50
+ function assign(target, key, value) {
51
+ Object.defineProperty(
52
+ target,
53
+ key,
54
+ {
55
+ value,
56
+ enumerable: true,
57
+ writable: false,
58
+ configurable: false
59
+ }
60
+ );
61
+ }
50
62
  var Ecwt = class {
51
63
  #ecwtFactory;
52
- #token;
53
- #snowflake;
54
64
  #ttl_initial;
55
- #data;
65
+ /**
66
+ * Token string representation.
67
+ * @type {string}
68
+ * @readonly
69
+ */
70
+ token;
71
+ /**
72
+ * Token ID.
73
+ * @type {string}
74
+ * @readonly
75
+ */
76
+ id;
77
+ /**
78
+ * Snowflake associated with token.
79
+ * @type {Snowflake}
80
+ * @readonly
81
+ */
82
+ snowflake;
83
+ /**
84
+ * Timestamp when token expires in seconds.
85
+ * @type {number?}
86
+ * @readonly
87
+ */
88
+ ts_expired;
89
+ /**
90
+ * Data stored in token.
91
+ * @type {{ [key: string]: any }}
92
+ * @readonly
93
+ */
94
+ data;
56
95
  /**
57
96
  * @param {EcwtFactory} ecwtFactory -
58
97
  * @param {object} options -
59
98
  * @param {string} options.token String representation of token.
60
99
  * @param {Snowflake} options.snowflake -
61
- * @param {number | null} options.ttl_initial Time to live in seconds at the moment of token creation.
100
+ * @param {number?} options.ttl_initial Time to live in seconds at the moment of token creation.
62
101
  * @param {object} options.data Data stored in token.
63
102
  */
64
103
  constructor(ecwtFactory, {
@@ -68,45 +107,46 @@ var Ecwt = class {
68
107
  data
69
108
  }) {
70
109
  this.#ecwtFactory = ecwtFactory;
71
- this.#token = token;
72
- this.#snowflake = snowflake;
73
110
  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;
111
+ assign(this, "token", token);
112
+ assign(
113
+ this,
114
+ "id",
115
+ snowflake.toBase62()
116
+ );
117
+ assign(this, "snowflake", snowflake);
118
+ assign(
119
+ this,
120
+ "ts_expired",
121
+ this.#getTimestampExpired()
122
+ );
123
+ assign(
124
+ this,
125
+ "data",
126
+ Object.freeze(data)
127
+ );
84
128
  }
85
- get ts_expired() {
129
+ #getTimestampExpired() {
86
130
  if (this.#ttl_initial === null) {
87
- return Number.POSITIVE_INFINITY;
131
+ return null;
88
132
  }
89
- return toSeconds(this.#snowflake.timestamp) + this.#ttl_initial;
133
+ return toSeconds(this.snowflake.timestamp) + this.#ttl_initial;
90
134
  }
91
135
  /**
92
- * Time to live in seconds.
93
- * @type {number}
94
- * @readonly
136
+ * Actual time to live in seconds.
137
+ * @returns {number | null} -
95
138
  */
96
- get ttl() {
139
+ getTTL() {
97
140
  if (this.#ttl_initial === null) {
98
- return Number.POSITIVE_INFINITY;
141
+ return null;
99
142
  }
100
- return this.#ttl_initial - toSeconds(Date.now() - this.#snowflake.timestamp);
101
- }
102
- get data() {
103
- return this.#data;
143
+ return this.#ttl_initial - toSeconds(Date.now() - this.snowflake.timestamp);
104
144
  }
105
145
  /* async */
106
146
  revoke() {
107
147
  return this.#ecwtFactory._revoke({
108
148
  token_id: this.id,
109
- ts_ms_created: this.#snowflake.timestamp,
149
+ ts_ms_created: this.snowflake.timestamp,
110
150
  ttl_initial: this.#ttl_initial
111
151
  });
112
152
  }
@@ -233,16 +273,16 @@ var EcwtFactory = class {
233
273
  }
234
274
  payload.push(value);
235
275
  }
236
- if (typeof ttl !== "number" && Number.isNaN(ttl) !== true || ttl === Number.POSITIVE_INFINITY) {
237
- ttl = null;
276
+ if (typeof ttl !== "number" && Number.isNaN(ttl) !== true && ttl !== null) {
277
+ throw new TypeError("TTL must be a number or null.");
238
278
  }
239
279
  const snowflake = await this.#snowflakeFactory.createSafe();
240
280
  const token_raw = (0, import_cbor_x.encode)([
241
- snowflake.buffer,
281
+ snowflake.toBuffer(),
242
282
  ttl,
243
283
  payload
244
284
  ]);
245
- const token_encrypted = await (0, import_evilcrypt.encrypt)(
285
+ const token_encrypted = await import_evilcrypt.v2.encrypt(
246
286
  token_raw,
247
287
  this.#encryption_key
248
288
  );
@@ -360,6 +400,7 @@ var EcwtFactory = class {
360
400
  ttl_initial
361
401
  }) {
362
402
  if (this.#redisClient) {
403
+ ttl_initial = ttl_initial ?? Number.MAX_SAFE_INTEGER;
363
404
  const ts_ms_expired = ts_ms_created + ttl_initial * 1e3;
364
405
  if (ts_ms_expired > Date.now()) {
365
406
  await this.#redisClient.sendCommand([
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ecwt",
3
- "version": "0.1.0-beta.2",
3
+ "version": "0.1.1-beta.1",
4
4
  "description": "Encrypted CBOR-encoded Web Token",
5
5
  "main": "src/main.js",
6
6
  "type": "module",
@@ -16,10 +16,10 @@
16
16
  "dependencies": {
17
17
  "base-x": "4.0.0",
18
18
  "cbor-x": "1.5.6",
19
- "evilcrypt": "0.2.0-beta.2"
19
+ "evilcrypt": "0.2.0-beta.4"
20
20
  },
21
21
  "peerDependencies": {
22
- "@kirick/snowflake": "^0.2.0-beta.7",
22
+ "@kirick/snowflake": "^0.2.1-beta.1",
23
23
  "lru-cache": "^7 || ^8 || ^9 || ^10",
24
24
  "redis": "^4"
25
25
  },
package/src/factory.js CHANGED
@@ -4,8 +4,8 @@ import {
4
4
  encode as cborEncode,
5
5
  decode as cborDecode } from 'cbor-x';
6
6
  import {
7
- encrypt as evilcryptEncrypt,
8
- decrypt as evilcryptDecrypt } from 'evilcrypt';
7
+ decrypt as evilcryptDecrypt,
8
+ v2 as evilcryptV2 } from 'evilcrypt';
9
9
  import { LRUCache } from 'lru-cache';
10
10
  import { createClient } from 'redis';
11
11
  import { Ecwt } from './token.js';
@@ -137,24 +137,22 @@ export class EcwtFactory {
137
137
  }
138
138
 
139
139
  if (
140
- (
141
- typeof ttl !== 'number'
142
- && Number.isNaN(ttl) !== true
143
- )
144
- || ttl === Number.POSITIVE_INFINITY
140
+ typeof ttl !== 'number'
141
+ && Number.isNaN(ttl) !== true
142
+ && ttl !== null
145
143
  ) {
146
- ttl = null;
144
+ throw new TypeError('TTL must be a number or null.');
147
145
  }
148
146
 
149
147
  const snowflake = await this.#snowflakeFactory.createSafe();
150
148
 
151
149
  const token_raw = cborEncode([
152
- snowflake.buffer,
150
+ snowflake.toBuffer(),
153
151
  ttl,
154
152
  payload,
155
153
  ]);
156
154
 
157
- const token_encrypted = await evilcryptEncrypt(
155
+ const token_encrypted = await evilcryptV2.encrypt(
158
156
  token_raw,
159
157
  this.#encryption_key,
160
158
  );
@@ -299,6 +297,8 @@ export class EcwtFactory {
299
297
  ttl_initial,
300
298
  }) {
301
299
  if (this.#redisClient) {
300
+ ttl_initial = ttl_initial ?? Number.MAX_SAFE_INTEGER;
301
+
302
302
  const ts_ms_expired = ts_ms_created + (ttl_initial * 1000);
303
303
  if (ts_ms_expired > Date.now()) {
304
304
  await this.#redisClient.sendCommand([
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,51 @@ 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
83
 
42
- get token() {
43
- return this.#token;
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
+ );
44
101
  }
45
102
 
46
- get id() {
47
- return this.#snowflake.base62;
48
- }
49
-
50
- get snowflake() {
51
- return this.#snowflake;
52
- }
53
-
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
 
79
123
  /* async */ revoke() {
80
124
  return this.#ecwtFactory._revoke({
81
125
  token_id: this.id,
82
- ts_ms_created: this.#snowflake.timestamp,
126
+ ts_ms_created: this.snowflake.timestamp,
83
127
  ttl_initial: this.#ttl_initial,
84
128
  });
85
129
  }