ecwt 0.2.1-beta.5 → 0.2.4

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.
@@ -1,38 +0,0 @@
1
- /**
2
- * @typedef {import('../token.js').Ecwt} Ecwt
3
- */
4
- export class InvalidPackageInstanceError extends TypeError {
5
- /**
6
- * @param {string} property -
7
- * @param {string} class_name -
8
- * @param {string} package_name -
9
- */
10
- constructor(property: string, class_name: string, package_name: string);
11
- }
12
- /**
13
- * Error thrown when string token cannot be parsed to Ecwt.
14
- */
15
- export class EcwtParseError extends Error {
16
- constructor();
17
- }
18
- /**
19
- * Error thrown when parsed Ecwt is invalid.
20
- */
21
- export class EcwtInvalidError extends Error {
22
- /**
23
- * @param {Ecwt} ecwt -
24
- */
25
- constructor(ecwt: Ecwt);
26
- ecwt: import("../token.js").Ecwt<Record<string, any>>;
27
- }
28
- /**
29
- * Error thrown when parsed Ecwt is expired.
30
- */
31
- export class EcwtExpiredError extends EcwtInvalidError {
32
- }
33
- /**
34
- * Error thrown when parsed Ecwt is revoked.
35
- */
36
- export class EcwtRevokedError extends EcwtInvalidError {
37
- }
38
- export type Ecwt = import("../token.js").Ecwt;
@@ -1,6 +0,0 @@
1
- /**
2
- * Convert timestamp in seconds to milliseconds.
3
- * @param {*} value Timestamp in milliseconds.
4
- * @returns {number} Timestamp in seconds.
5
- */
6
- export function toSeconds(value: any): number;
package/src/factory.js DELETED
@@ -1,385 +0,0 @@
1
-
2
- /**
3
- * @typedef {import('@kirick/snowflake').Snowflake} Snowflake
4
- */
5
- /**
6
- * @typedef {object} CacheValue
7
- * @property {Snowflake} snowflake -
8
- * @property {number | null} ttl_initial -
9
- * @property {Record<string, any>} data -
10
- */
11
-
12
- import { SnowflakeFactory } from '@kirick/snowflake';
13
- import {
14
- Encoder as CborEncoder,
15
- encode as cborEncode,
16
- decode as cborDecode,
17
- } from 'cbor-x';
18
- import {
19
- decrypt as evilcryptDecrypt,
20
- v2 as evilcryptV2,
21
- } from 'evilcrypt';
22
- import { LRUCache } from 'lru-cache';
23
- import { Ecwt } from './token.js';
24
- import { base62 } from './utils/base62.js';
25
- import {
26
- InvalidPackageInstanceError,
27
- EcwtInvalidError,
28
- EcwtExpiredError,
29
- EcwtRevokedError,
30
- EcwtParseError,
31
- } from './utils/errors.js';
32
-
33
- const REDIS_PREFIX = '@ecwt:';
34
-
35
- /**
36
- * @template {Record<string, any>} [D=Record<string, any>]
37
- */
38
- export class EcwtFactory {
39
- #redisClient;
40
- #lruCache;
41
- #snowflakeFactory;
42
- #redis_key_revoked;
43
- #encryption_key;
44
- #validator;
45
- /** @type {CborEncoder | null} */
46
- #cborEncoder = null;
47
-
48
- /**
49
- * @param {object} param0 -
50
- * @param {import('redis').RedisClientType<import('redis').RedisModules, import('redis').RedisFunctions, import('redis').RedisScripts>} [param0.redisClient] RedisClient instance. If not provided, tokens will not be revoked and cannot be checked for revocation.
51
- * @param {LRUCache<string, CacheValue>} [param0.lruCache] LRUCache instance. If not provided, tokens will be decrypted every time they are verified.
52
- * @param {SnowflakeFactory} param0.snowflakeFactory SnowflakeFactory instance.
53
- * @param {object} param0.options -
54
- * @param {string} [param0.options.namespace] Namespace for Redis keys.
55
- * @param {Buffer} param0.options.key Encryption key, 64 bytes
56
- * @param {(value: unknown) => D} [param0.options.validator] Validator for token data. Should return validated value or throw an error.
57
- * @param {Record<string, number>} [param0.options.senml_key_map] Payload object keys mapped for their SenML keys.
58
- */
59
- constructor({
60
- redisClient,
61
- lruCache,
62
- snowflakeFactory,
63
- options: {
64
- namespace,
65
- key,
66
- validator,
67
- senml_key_map,
68
- },
69
- }) {
70
- this.#redisClient = redisClient;
71
-
72
- if (
73
- lruCache !== undefined
74
- && lruCache instanceof LRUCache !== true
75
- ) {
76
- throw new InvalidPackageInstanceError(
77
- 'lruCache',
78
- 'LRUCache',
79
- 'lru-cache',
80
- );
81
- }
82
-
83
- this.#lruCache = lruCache;
84
-
85
- if (snowflakeFactory instanceof SnowflakeFactory !== true) {
86
- throw new InvalidPackageInstanceError(
87
- 'snowflakeFactory',
88
- 'SnowflakeFactory',
89
- '@kirick/snowflake',
90
- );
91
- }
92
-
93
- this.#snowflakeFactory = snowflakeFactory;
94
-
95
- this.#redis_key_revoked = `${REDIS_PREFIX}${namespace}:revoked`;
96
-
97
- this.#encryption_key = key;
98
-
99
- this.#validator = validator;
100
-
101
- if (senml_key_map) {
102
- this.#cborEncoder = new CborEncoder({
103
- keyMap: senml_key_map,
104
- });
105
- }
106
- }
107
-
108
- /**
109
- * Creates new token.
110
- * @async
111
- * @param {D} data Data to be stored in token.
112
- * @param {object} [options] -
113
- * @param {number | null} [options.ttl] Time to live in seconds. By default, token will never expire.
114
- * @returns {Promise<Ecwt<D>>} -
115
- */
116
- async create(
117
- data,
118
- {
119
- ttl = null,
120
- } = {},
121
- ) {
122
- if (typeof this.#validator === 'function') {
123
- data = this.#validator(data);
124
- }
125
-
126
- if (
127
- typeof ttl !== 'number'
128
- && Number.isNaN(ttl) !== true
129
- && ttl !== null
130
- ) {
131
- throw new TypeError('TTL must be a number or null.');
132
- }
133
-
134
- const snowflake = await this.#snowflakeFactory.createSafe();
135
-
136
- const payload = [
137
- snowflake.toBuffer(),
138
- ttl,
139
- data,
140
- ];
141
- const token_raw = this.#cborEncoder
142
- ? this.#cborEncoder.encode(payload)
143
- : cborEncode(payload);
144
-
145
- const token_encrypted = await evilcryptV2.encrypt(
146
- token_raw,
147
- this.#encryption_key,
148
- );
149
-
150
- const token = base62.encode(token_encrypted);
151
-
152
- this.#setCache(
153
- token,
154
- {
155
- snowflake,
156
- ttl_initial: ttl,
157
- data,
158
- },
159
- );
160
-
161
- return new Ecwt(
162
- this,
163
- {
164
- token,
165
- snowflake,
166
- ttl_initial: ttl,
167
- data,
168
- },
169
- );
170
- }
171
-
172
- /**
173
- * Sets data to cache.
174
- * @param {string} token String representation of token.
175
- * @param {CacheValue} cache_value Data to be stored in cache.
176
- */
177
- #setCache(token, cache_value) {
178
- this.#lruCache?.set(
179
- token,
180
- cache_value,
181
- cache_value.ttl_initial === null
182
- ? undefined
183
- : {
184
- ttl: cache_value.ttl_initial * 1000,
185
- },
186
- );
187
- }
188
-
189
- /**
190
- * Parses token.
191
- * @async
192
- * @param {string} token String representation of token.
193
- * @returns {Promise<Ecwt<D>>} -
194
- */
195
- async verify(token) {
196
- if (typeof token !== 'string') {
197
- throw new TypeError('Token must be a string.');
198
- }
199
-
200
- let snowflake;
201
- let ttl_initial;
202
- let data;
203
-
204
- const cached_entry = this.#lruCache?.info(token);
205
- // token is not cached
206
- if (cached_entry === undefined) {
207
- const token_encrypted = Buffer.from(
208
- base62.decode(token),
209
- );
210
-
211
- let token_raw;
212
- try {
213
- token_raw = await evilcryptDecrypt(
214
- token_encrypted,
215
- this.#encryption_key,
216
- );
217
- }
218
- catch {
219
- throw new EcwtParseError();
220
- }
221
-
222
- const payload = this.#cborEncoder
223
- ? this.#cborEncoder.decode(token_raw)
224
- : cborDecode(token_raw);
225
-
226
- const [ snowflake_buffer ] = payload;
227
- [
228
- ,
229
- ttl_initial,
230
- data,
231
- ] = payload;
232
-
233
- snowflake = this.#snowflakeFactory.parse(snowflake_buffer);
234
-
235
- if (typeof this.#validator === 'function') {
236
- try {
237
- data = this.#validator(data);
238
- }
239
- catch {
240
- throw new EcwtParseError();
241
- }
242
- }
243
-
244
- this.#setCache(
245
- token,
246
- {
247
- snowflake,
248
- ttl_initial,
249
- data,
250
- },
251
- );
252
- }
253
- else {
254
- ({
255
- snowflake,
256
- ttl_initial,
257
- data,
258
- } = cached_entry.value);
259
- }
260
-
261
- // console.log('snowflake', snowflake);
262
- // console.log('ttl', ttl);
263
- // console.log('data', data);
264
-
265
- const ecwt = new Ecwt(
266
- this,
267
- {
268
- token,
269
- snowflake,
270
- ttl_initial,
271
- data,
272
- },
273
- );
274
-
275
- if (
276
- typeof ttl_initial === 'number'
277
- && Number.isNaN(ttl_initial) !== true
278
- && snowflake.timestamp + (ttl_initial * 1000) < Date.now()
279
- ) {
280
- throw new EcwtExpiredError(ecwt);
281
- }
282
-
283
- if (this.#redisClient) {
284
- const score = await this.#redisClient.ZSCORE(
285
- this.#redis_key_revoked,
286
- ecwt.id,
287
- );
288
- if (score !== null) {
289
- throw new EcwtRevokedError(ecwt);
290
- }
291
- }
292
-
293
- return ecwt;
294
- }
295
-
296
- /**
297
- * Parses token without throwing errors.
298
- * @async
299
- * @param {string} token String representation of token.
300
- * @returns {Promise<{ success: true, ecwt: Ecwt<D> } | { success: false, ecwt: Ecwt<D> | null }>} Returns whether token was parsed and verified successfully and Ecwt if parsed.
301
- */
302
- async safeVerify(token) {
303
- let ecwt = null;
304
- try {
305
- ecwt = await this.verify(token);
306
-
307
- return {
308
- success: true,
309
- ecwt,
310
- };
311
- }
312
- catch (error) {
313
- if (error instanceof EcwtParseError) {
314
- return {
315
- success: false,
316
- ecwt: null,
317
- };
318
- }
319
-
320
- if (error instanceof EcwtInvalidError) {
321
- return {
322
- success: false,
323
- ecwt,
324
- };
325
- }
326
-
327
- throw error;
328
- }
329
- }
330
-
331
- /**
332
- * Revokes token.
333
- * @async
334
- * @param {object} options -
335
- * @param {string} options.token_id -
336
- * @param {number} options.ts_ms_created -
337
- * @param {number | null} options.ttl_initial -
338
- * @returns {Promise<void>} -
339
- */
340
- async _revoke({
341
- token_id,
342
- ts_ms_created,
343
- ttl_initial,
344
- }) {
345
- if (this.#redisClient) {
346
- ttl_initial ??= Number.MAX_SAFE_INTEGER;
347
-
348
- const ts_ms_expired = ts_ms_created + (ttl_initial * 1000);
349
- if (ts_ms_expired > Date.now()) {
350
- // await this.#redisClient.sendCommand([
351
- // 'ZADD',
352
- // this.#redis_keys.revoked,
353
- // String(ts_ms_expired),
354
- // token_id,
355
- // ]);
356
- await this.#redisClient.MULTI()
357
- .addCommand([
358
- 'ZADD',
359
- this.#redis_key_revoked,
360
- String(ts_ms_expired),
361
- token_id,
362
- ])
363
- .addCommand([
364
- 'ZREMRANGEBYSCORE',
365
- this.#redis_key_revoked,
366
- '-inf',
367
- String(Date.now()),
368
- ])
369
- .EXEC();
370
- }
371
- }
372
- else {
373
- console.warn('[ecwt] Redis client is not provided. Tokens cannot be revoked.');
374
- }
375
- }
376
-
377
- /**
378
- * Purges cache.
379
- * @private
380
- * @returns {void} -
381
- */
382
- _purgeCache() {
383
- this.#lruCache?.clear();
384
- }
385
- }
package/src/main.js DELETED
@@ -1,9 +0,0 @@
1
-
2
- export { EcwtFactory } from './factory.js';
3
- export { Ecwt } from './token.js';
4
- export {
5
- EcwtParseError,
6
- EcwtInvalidError,
7
- EcwtExpiredError,
8
- EcwtRevokedError,
9
- } from './utils/errors.js';
package/src/token.js DELETED
@@ -1,106 +0,0 @@
1
-
2
- import { toSeconds } from './utils/time.js';
3
-
4
- /**
5
- * @typedef {import('@kirick/snowflake').Snowflake} Snowflake
6
- * @typedef {import('./factory.js').EcwtFactory} EcwtFactory
7
- */
8
-
9
- /**
10
- * @template {Record<string, any>} [D=Record<string, any>]
11
- */
12
- export class Ecwt {
13
- /**
14
- * Token string representation.
15
- * @type {string}
16
- * @readonly
17
- */
18
- token;
19
- /**
20
- * Token ID.
21
- * @type {string}
22
- * @readonly
23
- */
24
- id;
25
- /**
26
- * Snowflake associated with token.
27
- * @type {Snowflake}
28
- * @readonly
29
- */
30
- snowflake;
31
- /**
32
- * Timestamp when token expires in seconds.
33
- * @type {number?}
34
- * @readonly
35
- */
36
- ts_expired;
37
- /**
38
- * Data stored in token.
39
- * @type {D}
40
- * @readonly
41
- */
42
- data;
43
- /** @type {EcwtFactory} */
44
- #ecwtFactory;
45
- /** @type {number | null} */
46
- #ttl_initial;
47
-
48
- /**
49
- * @param {EcwtFactory} ecwtFactory -
50
- * @param {object} options -
51
- * @param {string} options.token String representation of token.
52
- * @param {Snowflake} options.snowflake -
53
- * @param {number | null} options.ttl_initial Time to live in seconds at the moment of token creation.
54
- * @param {D} options.data Data stored in token.
55
- */
56
- constructor(
57
- ecwtFactory,
58
- {
59
- token,
60
- snowflake,
61
- ttl_initial,
62
- data,
63
- },
64
- ) {
65
- this.#ecwtFactory = ecwtFactory;
66
- this.#ttl_initial = ttl_initial;
67
-
68
- this.token = token;
69
- this.id = snowflake.toBase62();
70
- this.snowflake = snowflake;
71
- this.ts_expired = this.#getTimestampExpired();
72
- this.data = Object.freeze(data);
73
- }
74
-
75
- #getTimestampExpired() {
76
- if (this.#ttl_initial === null) {
77
- return null;
78
- }
79
-
80
- return toSeconds(this.snowflake.timestamp) + this.#ttl_initial;
81
- }
82
-
83
- /**
84
- * Actual time to live in seconds.
85
- * @returns {number | null} -
86
- */
87
- getTTL() {
88
- if (this.#ttl_initial === null) {
89
- return null;
90
- }
91
-
92
- return this.#ttl_initial - toSeconds(Date.now() - this.snowflake.timestamp);
93
- }
94
-
95
- /**
96
- * Revokes token.
97
- * @returns {Promise<void>} -
98
- */
99
- /* async */ revoke() {
100
- return this.#ecwtFactory._revoke({
101
- token_id: this.id,
102
- ts_ms_created: this.snowflake.timestamp,
103
- ttl_initial: this.#ttl_initial,
104
- });
105
- }
106
- }
@@ -1,4 +0,0 @@
1
-
2
- import basex from 'base-x';
3
-
4
- export const base62 = basex('0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz');
@@ -1,58 +0,0 @@
1
-
2
- /**
3
- * @typedef {import('../token.js').Ecwt} Ecwt
4
- */
5
-
6
- export class InvalidPackageInstanceError extends TypeError {
7
- /**
8
- * @param {string} property -
9
- * @param {string} class_name -
10
- * @param {string} package_name -
11
- */
12
- constructor(property, class_name, package_name) {
13
- super(
14
- `Value ${property} must be an instance of ${class_name} from package "${package_name}".`
15
- + ` That error is probably caused by two separate installations of "${package_name}".`
16
- + ` Please, make sure that "${package_name}" in your project is matches "peerDependencies" of "ecwt" package.`,
17
- );
18
- }
19
- }
20
-
21
- /**
22
- * Error thrown when string token cannot be parsed to Ecwt.
23
- */
24
- export class EcwtParseError extends Error {
25
- constructor() {
26
- super('Cannot parse data to Ecwt token.');
27
- }
28
- }
29
-
30
- /**
31
- * Error thrown when parsed Ecwt is invalid.
32
- */
33
- export class EcwtInvalidError extends Error {
34
- message = 'Ecwt token is invalid.';
35
-
36
- /**
37
- * @param {Ecwt} ecwt -
38
- */
39
- constructor(ecwt) {
40
- super();
41
-
42
- this.ecwt = ecwt;
43
- }
44
- }
45
-
46
- /**
47
- * Error thrown when parsed Ecwt is expired.
48
- */
49
- export class EcwtExpiredError extends EcwtInvalidError {
50
- message = 'Ecwt is expired.';
51
- }
52
-
53
- /**
54
- * Error thrown when parsed Ecwt is revoked.
55
- */
56
- export class EcwtRevokedError extends EcwtInvalidError {
57
- message = 'Ecwt is revoked.';
58
- }
package/src/utils/time.js DELETED
@@ -1,17 +0,0 @@
1
-
2
- /**
3
- * Convert timestamp in seconds to milliseconds.
4
- * @param {*} value Timestamp in milliseconds.
5
- * @returns {number} Timestamp in seconds.
6
- */
7
- export function toSeconds(value) {
8
- return Math.floor(value / 1000);
9
- }
10
-
11
- // /**
12
- // * Returns current timestamp in seconds.
13
- // * @returns {number} Timestamp in seconds.
14
- // */
15
- // export function unixtime() {
16
- // return toSeconds(Date.now());
17
- // }