ecwt 0.3.3 → 0.4.0

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
@@ -9,7 +9,7 @@ ECWT is module for creating and verifying encrypted CBOR Web Tokens. It is desig
9
9
  | --- | --- | --- |
10
10
  | Encoding | 🧐 JSON with base64 | ✅ CBOR <br> 2x smaller output |
11
11
  | Binary data | 🧐 Double base64 encoding | ✅ Supported out of the box |
12
- | Security | 📝 Signed <br> Payload is readable by everyone | 🔒 Encrypted <br> Payload is readable only by the private key possessor |
12
+ | Security | 📝 Signed <br> Payload is readable by everyone | 🔒 Encrypted <br> Payload is readable only with private key |
13
13
  | Metadata | ➕ Type and algorithm, increases size | ✅ No unnecessary metadata |
14
14
  | Revocation | 🧑‍💻 Requires additional implementation | ✅ Included with Redis |
15
15
 
@@ -168,10 +168,6 @@ console.log(`Expiration timestamp: ${ecwt.ts_expired}`);
168
168
  console.log(`Remaining validity: ${ecwt.getTTL()} seconds`);
169
169
  ```
170
170
 
171
- > **Warning regarding non-expiring tokens:**
172
- >
173
- > When using `ttl: null`, revoked tokens remain in Redis storage indefinitely. This can lead to uncontrolled database growth over time as these tokens are never automatically purged. Consider implementing a periodic cleanup strategy if non-expiring tokens are required.
174
-
175
171
  ### Token Verification
176
172
 
177
173
  Implement verification with appropriate error handling:
@@ -252,7 +248,7 @@ try {
252
248
 
253
249
  ### Advanced: Token Size Optimization
254
250
 
255
- To reduce token size, use SenML key mapping that replaces string object keys with numeric identifiers throughout your entire payload structure. This compression works at any nesting depth. When implementing, catalog all potential keys across your schema and assign consistent numeric values to each, as these mappings cannot be changed once tokens are in circulation.
251
+ To reduce token size, use SenML key mapping that replaces string object keys with numeric identifiers throughout your entire payload structure. This compression works at any nesting depth.
256
252
 
257
253
  > **Important:** The SenML key mapping configuration establishes a permanent relationship between field names and their numeric identifiers. Once deployed, these mappings must remain consistent to maintain compatibility with existing tokens. Adding new fields is acceptable, but changing existing mappings can break previously issued tokens.
258
254
 
package/bun.lock CHANGED
@@ -8,6 +8,7 @@
8
8
  "base-x": "5.0.1",
9
9
  "cbor-x": "1.5.6",
10
10
  "evilcrypt": "0.2.3",
11
+ "valibot": "1.4.1",
11
12
  },
12
13
  "devDependencies": {
13
14
  "@biomejs/biome": "2.4.10",
@@ -19,7 +20,6 @@
19
20
  "tsdown": "0.21.10",
20
21
  "typescript": "6.0.2",
21
22
  "unplugin-unused": "0.5.7",
22
- "valibot": "1.4.0",
23
23
  "vitest": "4.1.7",
24
24
  },
25
25
  "peerDependencies": {
@@ -700,7 +700,7 @@
700
700
 
701
701
  "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
702
702
 
703
- "valibot": ["valibot@1.4.0", "", { "peerDependencies": { "typescript": ">=5" }, "optionalPeers": ["typescript"] }, "sha512-iC/x7fVcSyOwlm/VSt7RlHnzNGLGvR9GnxdifUeWoCJo0q4ZZvrVkIHC6faTlkxG47I2Y4UrFquPuVHCrOnrLg=="],
703
+ "valibot": ["valibot@1.4.1", "", { "peerDependencies": { "typescript": ">=5" }, "optionalPeers": ["typescript"] }, "sha512-klCmFTz2jeDluy9RwX+F884TCiogtdBJ/YaxSx1EOBYXa3NXNWj8kR1jjN8rzluwojJVWWaHJ4r1U5LfICnM3g=="],
704
704
 
705
705
  "vite": ["vite@7.3.3", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-/4XH147Ui7OGTjg3HbdWe5arnZQSbfuRzdr9Ec7TQi5I7R+ir0Rlc9GIvD4v0XZurELqA035KVXJXpR61xhiTA=="],
706
706
 
package/dist/main.cjs CHANGED
@@ -23,6 +23,8 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
23
23
  //#endregion
24
24
  let cbor_x = require("cbor-x");
25
25
  let evilcrypt = require("evilcrypt");
26
+ let valibot = require("valibot");
27
+ valibot = __toESM(valibot, 1);
26
28
  let base_x = require("base-x");
27
29
  base_x = __toESM(base_x, 1);
28
30
  //#region src/errors.ts
@@ -66,7 +68,7 @@ var Ecwt = class {
66
68
  * @param options -
67
69
  * @param options.token String representation of token.
68
70
  * @param options.snowflake -
69
- * @param options.ttl_initial Time to live in seconds at the moment of token creation.
71
+ * @param options.ttl_initial Time to live in **seconds** at the moment of token creation.
70
72
  * @param options.data Data stored in token.
71
73
  */
72
74
  constructor(ecwtFactory, options) {
@@ -78,19 +80,17 @@ var Ecwt = class {
78
80
  this.#ttl_initial = options.ttl_initial;
79
81
  }
80
82
  /**
81
- * Unix timestamp of token expiration in seconds.
83
+ * Unix timestamp of token expiration in **seconds**.
82
84
  * @returns -
83
85
  */
84
86
  get ts_expired() {
85
- if (this.#ttl_initial === null) return null;
86
87
  return Math.floor(this.snowflake.timestamp / 1e3) + this.#ttl_initial;
87
88
  }
88
89
  /**
89
- * Actual time to live in seconds.
90
+ * Actual time to live in **seconds**.
90
91
  * @returns -
91
92
  */
92
93
  getTTL() {
93
- if (this.#ttl_initial === null) return null;
94
94
  return this.#ttl_initial - Math.floor((Date.now() - this.snowflake.timestamp) / 1e3);
95
95
  }
96
96
  /** Revokes token. */
@@ -104,6 +104,11 @@ const base62 = (0, base_x.default)("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefgh
104
104
  //#endregion
105
105
  //#region src/factory.ts
106
106
  const REDIS_PREFIX = "@ecwt:";
107
+ const tokenSchema = valibot.tuple([
108
+ valibot.pipe(valibot.unknown(), valibot.check((value) => Buffer.isBuffer(value)), valibot.transform((value) => value)),
109
+ valibot.number(),
110
+ valibot.record(valibot.string(), valibot.unknown())
111
+ ]);
107
112
  var EcwtFactory = class {
108
113
  #redisClient;
109
114
  #lruCache;
@@ -126,16 +131,15 @@ var EcwtFactory = class {
126
131
  * @async
127
132
  * @param data - Data to be stored in token.
128
133
  * @param options -
129
- * @param options.ttl - Time to live in seconds. If not defined, token will never expire.
134
+ * @param options.ttl - Time to live in **seconds**.
130
135
  * @returns -
131
136
  */
132
- async create(data, options = {}) {
137
+ async create(data, options) {
133
138
  if (typeof this.#validator === "function") data = this.#validator(data);
134
- const ttl = options.ttl ?? null;
135
139
  const snowflake = await this.#snowflakeFactory.createSafe();
136
140
  const payload = [
137
141
  snowflake.toBuffer(),
138
- ttl,
142
+ options.ttl,
139
143
  data
140
144
  ];
141
145
  const token_raw = this.#cborEncoder ? this.#cborEncoder.encode(payload) : (0, cbor_x.encode)(payload);
@@ -143,13 +147,13 @@ var EcwtFactory = class {
143
147
  const token = base62.encode(token_encrypted);
144
148
  this.setCache(token, {
145
149
  snowflake,
146
- ttl_initial: ttl,
150
+ ttl_initial: options.ttl,
147
151
  data
148
152
  });
149
153
  return new Ecwt(this, {
150
154
  token,
151
155
  snowflake,
152
- ttl_initial: ttl,
156
+ ttl_initial: options.ttl,
153
157
  data
154
158
  });
155
159
  }
@@ -160,7 +164,7 @@ var EcwtFactory = class {
160
164
  */
161
165
  setCache(token, cache_value) {
162
166
  var _this$lruCache;
163
- (_this$lruCache = this.#lruCache) === null || _this$lruCache === void 0 || _this$lruCache.set(token, cache_value, cache_value.ttl_initial === null ? void 0 : { ttl: cache_value.ttl_initial * 1e3 });
167
+ (_this$lruCache = this.#lruCache) === null || _this$lruCache === void 0 || _this$lruCache.set(token, cache_value, { ttl: cache_value.ttl_initial * 1e3 });
164
168
  }
165
169
  /**
166
170
  * Parses token.
@@ -182,28 +186,34 @@ var EcwtFactory = class {
182
186
  } catch {
183
187
  throw new EcwtParseError();
184
188
  }
185
- const payload = this.#cborEncoder ? this.#cborEncoder.decode(token_raw) : (0, cbor_x.decode)(token_raw);
186
- const [snowflake_buffer] = payload;
187
- [, ttl_initial, data] = payload;
189
+ const payload = valibot.parse(tokenSchema, this.#cborEncoder ? this.#cborEncoder.decode(token_raw) : (0, cbor_x.decode)(token_raw));
190
+ const snowflake_buffer = payload[0];
191
+ ttl_initial = payload[1];
192
+ const data_raw = payload[2];
188
193
  snowflake = this.#snowflakeFactory.parse(snowflake_buffer);
189
194
  if (typeof this.#validator === "function") try {
190
- data = this.#validator(data);
195
+ data = this.#validator(data_raw);
191
196
  } catch {
192
197
  throw new EcwtParseError();
193
198
  }
199
+ else data = data_raw;
194
200
  this.setCache(token, {
195
201
  snowflake,
196
202
  ttl_initial,
197
203
  data
198
204
  });
199
- } else ({snowflake, ttl_initial, data} = cached_entry.value);
205
+ } else {
206
+ snowflake = cached_entry.value.snowflake;
207
+ ttl_initial = cached_entry.value.ttl_initial;
208
+ data = cached_entry.value.data;
209
+ }
200
210
  const ecwt = new Ecwt(this, {
201
211
  token,
202
212
  snowflake,
203
213
  ttl_initial,
204
214
  data
205
215
  });
206
- if (typeof ttl_initial === "number" && Number.isNaN(ttl_initial) !== true && snowflake.timestamp + ttl_initial * 1e3 < Date.now()) throw new EcwtExpiredError(ecwt);
216
+ if (snowflake.timestamp + ttl_initial * 1e3 < Date.now()) throw new EcwtExpiredError(ecwt);
207
217
  if (this.#redisClient) {
208
218
  await this.#migrateExpired();
209
219
  if (await this.#redisClient.HEXISTS(this.#redis_key_revoked, ecwt.id)) throw new EcwtRevokedError(ecwt);
@@ -246,17 +256,14 @@ var EcwtFactory = class {
246
256
  async _revoke(token_id, created_at_ms, ttl_initial) {
247
257
  if (this.#redisClient) {
248
258
  await this.#migrateExpired();
249
- if (ttl_initial === null) await this.#redisClient.HSET(this.#redis_key_revoked, token_id, "");
250
- else {
251
- const expires_in_ms = created_at_ms + ttl_initial * 1e3 - Date.now();
252
- if (expires_in_ms > 0) await this.#redisClient.MULTI().HSET(this.#redis_key_revoked, token_id, "").HPEXPIRE(this.#redis_key_revoked, token_id, expires_in_ms).EXEC();
253
- }
259
+ const expires_in_ms = created_at_ms + ttl_initial * 1e3 - Date.now();
260
+ if (expires_in_ms > 0) await this.#redisClient.MULTI().HSET(this.#redis_key_revoked, token_id, "").HPEXPIRE(this.#redis_key_revoked, token_id, expires_in_ms).EXEC();
254
261
  } else console.warn("[ecwt] Redis client is not provided. Tokens cannot be revoked.");
255
262
  }
256
263
  #migrated = false;
257
264
  async #migrateExpired() {
258
265
  if (this.#redisClient && !this.#migrated) {
259
- await this.#redisClient.EVAL("local key = KEYS[1] if redis.call(\"TYPE\", key)[\"ok\"] ~= \"zset\" then return end local key_hash = key .. \":hash\" local ts_now = tonumber(ARGV[1]) local cursor = \"0\" repeat local scan = redis.call(\"ZSCAN\", key, cursor, \"COUNT\", 1000) cursor = scan[1] local items = scan[2] for i = 1, #items, 2 do local field = items[i] local expire_at = tonumber(items[i + 1]) local expire_in = expire_at and expire_at - ts_now if expire_in and expire_in > 0 then redis.call(\"HSET\", key_hash, field, \"\") redis.call(\"HPEXPIRE\", key_hash, expire_in, \"FIELDS\", 1, field) end end until cursor == \"0\" redis.call(\"DEL\", key) redis.call(\"RENAME\", key_hash, key)", {
266
+ await this.#redisClient.EVAL("local key = KEYS[1] if redis.call(\"TYPE\", key)[\"ok\"] ~= \"zset\" then return end local key_hash = key .. \":hash\" local ts_now = tonumber(ARGV[1]) local cursor = \"0\" repeat local scan = redis.call(\"ZSCAN\", key, cursor, \"COUNT\", 1000) cursor = scan[1] local items = scan[2] for i = 1, #items, 2 do local field = items[i] local expire_at = tonumber(items[i + 1]) local expire_in = expire_at and expire_at - ts_now if expire_in and expire_in > 0 then redis.call(\"HSET\", key_hash, field, \"\") redis.call(\"HPEXPIRE\", key_hash, expire_in, \"FIELDS\", 1, field) end end until cursor == \"0\" redis.call(\"DEL\", key) if redis.call(\"EXISTS\", key_hash) == 1 then redis.call(\"RENAME\", key_hash, key) end", {
260
267
  keys: [this.#redis_key_revoked],
261
268
  arguments: [String(Date.now())]
262
269
  });
package/dist/main.d.cts CHANGED
@@ -5,7 +5,7 @@ import { RedisClientType, RedisFunctions, RedisModules, RedisScripts } from "red
5
5
  //#region src/factory.d.ts
6
6
  type LRUCacheValue = {
7
7
  snowflake: Snowflake;
8
- ttl_initial: number | null;
8
+ ttl_initial: number;
9
9
  data: Record<string, unknown>;
10
10
  };
11
11
  type RedisClient = RedisClientType<RedisModules, RedisFunctions, RedisScripts>;
@@ -33,11 +33,11 @@ declare class EcwtFactory<const D extends Record<string, unknown> = Record<strin
33
33
  * @async
34
34
  * @param data - Data to be stored in token.
35
35
  * @param options -
36
- * @param options.ttl - Time to live in seconds. If not defined, token will never expire.
36
+ * @param options.ttl - Time to live in **seconds**.
37
37
  * @returns -
38
38
  */
39
- create(data: D, options?: {
40
- /** Time to live in seconds. If not defined, token will never expire. */ttl?: number;
39
+ create(data: D, options: {
40
+ /** Time to live in **seconds**. */ttl: number;
41
41
  }): Promise<Ecwt<D>>;
42
42
  /**
43
43
  * Sets data to cache.
@@ -81,25 +81,25 @@ declare class Ecwt<const D extends Record<string, unknown> = Record<string, unkn
81
81
  * @param options -
82
82
  * @param options.token String representation of token.
83
83
  * @param options.snowflake -
84
- * @param options.ttl_initial Time to live in seconds at the moment of token creation.
84
+ * @param options.ttl_initial Time to live in **seconds** at the moment of token creation.
85
85
  * @param options.data Data stored in token.
86
86
  */
87
- constructor(ecwtFactory: EcwtFactory, options: {
87
+ constructor(ecwtFactory: EcwtFactory<D>, options: {
88
88
  token: string;
89
89
  snowflake: Snowflake;
90
- ttl_initial: number | null;
90
+ ttl_initial: number;
91
91
  data: D;
92
92
  });
93
93
  /**
94
- * Unix timestamp of token expiration in seconds.
94
+ * Unix timestamp of token expiration in **seconds**.
95
95
  * @returns -
96
96
  */
97
- get ts_expired(): number | null;
97
+ get ts_expired(): number;
98
98
  /**
99
- * Actual time to live in seconds.
99
+ * Actual time to live in **seconds**.
100
100
  * @returns -
101
101
  */
102
- getTTL(): number | null;
102
+ getTTL(): number;
103
103
  /** Revokes token. */
104
104
  revoke(): Promise<void>;
105
105
  }
@@ -110,17 +110,17 @@ declare class EcwtParseError extends Error {
110
110
  constructor();
111
111
  }
112
112
  /** Error thrown when parsed Ecwt is invalid. */
113
- declare class EcwtInvalidError extends Error {
114
- readonly ecwt: Ecwt;
113
+ declare class EcwtInvalidError<D extends Record<string, unknown>> extends Error {
114
+ readonly ecwt: Ecwt<D>;
115
115
  override message: string;
116
- constructor(ecwt: Ecwt);
116
+ constructor(ecwt: Ecwt<D>);
117
117
  }
118
118
  /** Error thrown when parsed Ecwt is expired. */
119
- declare class EcwtExpiredError extends EcwtInvalidError {
119
+ declare class EcwtExpiredError<D extends Record<string, unknown>> extends EcwtInvalidError<D> {
120
120
  override message: string;
121
121
  }
122
122
  /** Error thrown when parsed Ecwt is revoked. */
123
- declare class EcwtRevokedError extends EcwtInvalidError {
123
+ declare class EcwtRevokedError<D extends Record<string, unknown>> extends EcwtInvalidError<D> {
124
124
  override message: string;
125
125
  }
126
126
  //#endregion
package/dist/main.d.mts CHANGED
@@ -5,7 +5,7 @@ import { RedisClientType, RedisFunctions, RedisModules, RedisScripts } from "red
5
5
  //#region src/factory.d.ts
6
6
  type LRUCacheValue = {
7
7
  snowflake: Snowflake;
8
- ttl_initial: number | null;
8
+ ttl_initial: number;
9
9
  data: Record<string, unknown>;
10
10
  };
11
11
  type RedisClient = RedisClientType<RedisModules, RedisFunctions, RedisScripts>;
@@ -33,11 +33,11 @@ declare class EcwtFactory<const D extends Record<string, unknown> = Record<strin
33
33
  * @async
34
34
  * @param data - Data to be stored in token.
35
35
  * @param options -
36
- * @param options.ttl - Time to live in seconds. If not defined, token will never expire.
36
+ * @param options.ttl - Time to live in **seconds**.
37
37
  * @returns -
38
38
  */
39
- create(data: D, options?: {
40
- /** Time to live in seconds. If not defined, token will never expire. */ttl?: number;
39
+ create(data: D, options: {
40
+ /** Time to live in **seconds**. */ttl: number;
41
41
  }): Promise<Ecwt<D>>;
42
42
  /**
43
43
  * Sets data to cache.
@@ -81,25 +81,25 @@ declare class Ecwt<const D extends Record<string, unknown> = Record<string, unkn
81
81
  * @param options -
82
82
  * @param options.token String representation of token.
83
83
  * @param options.snowflake -
84
- * @param options.ttl_initial Time to live in seconds at the moment of token creation.
84
+ * @param options.ttl_initial Time to live in **seconds** at the moment of token creation.
85
85
  * @param options.data Data stored in token.
86
86
  */
87
- constructor(ecwtFactory: EcwtFactory, options: {
87
+ constructor(ecwtFactory: EcwtFactory<D>, options: {
88
88
  token: string;
89
89
  snowflake: Snowflake;
90
- ttl_initial: number | null;
90
+ ttl_initial: number;
91
91
  data: D;
92
92
  });
93
93
  /**
94
- * Unix timestamp of token expiration in seconds.
94
+ * Unix timestamp of token expiration in **seconds**.
95
95
  * @returns -
96
96
  */
97
- get ts_expired(): number | null;
97
+ get ts_expired(): number;
98
98
  /**
99
- * Actual time to live in seconds.
99
+ * Actual time to live in **seconds**.
100
100
  * @returns -
101
101
  */
102
- getTTL(): number | null;
102
+ getTTL(): number;
103
103
  /** Revokes token. */
104
104
  revoke(): Promise<void>;
105
105
  }
@@ -110,17 +110,17 @@ declare class EcwtParseError extends Error {
110
110
  constructor();
111
111
  }
112
112
  /** Error thrown when parsed Ecwt is invalid. */
113
- declare class EcwtInvalidError extends Error {
114
- readonly ecwt: Ecwt;
113
+ declare class EcwtInvalidError<D extends Record<string, unknown>> extends Error {
114
+ readonly ecwt: Ecwt<D>;
115
115
  override message: string;
116
- constructor(ecwt: Ecwt);
116
+ constructor(ecwt: Ecwt<D>);
117
117
  }
118
118
  /** Error thrown when parsed Ecwt is expired. */
119
- declare class EcwtExpiredError extends EcwtInvalidError {
119
+ declare class EcwtExpiredError<D extends Record<string, unknown>> extends EcwtInvalidError<D> {
120
120
  override message: string;
121
121
  }
122
122
  /** Error thrown when parsed Ecwt is revoked. */
123
- declare class EcwtRevokedError extends EcwtInvalidError {
123
+ declare class EcwtRevokedError<D extends Record<string, unknown>> extends EcwtInvalidError<D> {
124
124
  override message: string;
125
125
  }
126
126
  //#endregion
package/dist/main.mjs CHANGED
@@ -1,5 +1,6 @@
1
1
  import { Encoder, decode, encode } from "cbor-x";
2
2
  import { decrypt, v2 } from "evilcrypt";
3
+ import * as v from "valibot";
3
4
  import basex from "base-x";
4
5
  //#region src/errors.ts
5
6
  /** Error thrown when string token cannot be parsed to Ecwt. */
@@ -42,7 +43,7 @@ var Ecwt = class {
42
43
  * @param options -
43
44
  * @param options.token String representation of token.
44
45
  * @param options.snowflake -
45
- * @param options.ttl_initial Time to live in seconds at the moment of token creation.
46
+ * @param options.ttl_initial Time to live in **seconds** at the moment of token creation.
46
47
  * @param options.data Data stored in token.
47
48
  */
48
49
  constructor(ecwtFactory, options) {
@@ -54,19 +55,17 @@ var Ecwt = class {
54
55
  this.#ttl_initial = options.ttl_initial;
55
56
  }
56
57
  /**
57
- * Unix timestamp of token expiration in seconds.
58
+ * Unix timestamp of token expiration in **seconds**.
58
59
  * @returns -
59
60
  */
60
61
  get ts_expired() {
61
- if (this.#ttl_initial === null) return null;
62
62
  return Math.floor(this.snowflake.timestamp / 1e3) + this.#ttl_initial;
63
63
  }
64
64
  /**
65
- * Actual time to live in seconds.
65
+ * Actual time to live in **seconds**.
66
66
  * @returns -
67
67
  */
68
68
  getTTL() {
69
- if (this.#ttl_initial === null) return null;
70
69
  return this.#ttl_initial - Math.floor((Date.now() - this.snowflake.timestamp) / 1e3);
71
70
  }
72
71
  /** Revokes token. */
@@ -80,6 +79,11 @@ const base62 = basex("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuv
80
79
  //#endregion
81
80
  //#region src/factory.ts
82
81
  const REDIS_PREFIX = "@ecwt:";
82
+ const tokenSchema = v.tuple([
83
+ v.pipe(v.unknown(), v.check((value) => Buffer.isBuffer(value)), v.transform((value) => value)),
84
+ v.number(),
85
+ v.record(v.string(), v.unknown())
86
+ ]);
83
87
  var EcwtFactory = class {
84
88
  #redisClient;
85
89
  #lruCache;
@@ -102,16 +106,15 @@ var EcwtFactory = class {
102
106
  * @async
103
107
  * @param data - Data to be stored in token.
104
108
  * @param options -
105
- * @param options.ttl - Time to live in seconds. If not defined, token will never expire.
109
+ * @param options.ttl - Time to live in **seconds**.
106
110
  * @returns -
107
111
  */
108
- async create(data, options = {}) {
112
+ async create(data, options) {
109
113
  if (typeof this.#validator === "function") data = this.#validator(data);
110
- const ttl = options.ttl ?? null;
111
114
  const snowflake = await this.#snowflakeFactory.createSafe();
112
115
  const payload = [
113
116
  snowflake.toBuffer(),
114
- ttl,
117
+ options.ttl,
115
118
  data
116
119
  ];
117
120
  const token_raw = this.#cborEncoder ? this.#cborEncoder.encode(payload) : encode(payload);
@@ -119,13 +122,13 @@ var EcwtFactory = class {
119
122
  const token = base62.encode(token_encrypted);
120
123
  this.setCache(token, {
121
124
  snowflake,
122
- ttl_initial: ttl,
125
+ ttl_initial: options.ttl,
123
126
  data
124
127
  });
125
128
  return new Ecwt(this, {
126
129
  token,
127
130
  snowflake,
128
- ttl_initial: ttl,
131
+ ttl_initial: options.ttl,
129
132
  data
130
133
  });
131
134
  }
@@ -136,7 +139,7 @@ var EcwtFactory = class {
136
139
  */
137
140
  setCache(token, cache_value) {
138
141
  var _this$lruCache;
139
- (_this$lruCache = this.#lruCache) === null || _this$lruCache === void 0 || _this$lruCache.set(token, cache_value, cache_value.ttl_initial === null ? void 0 : { ttl: cache_value.ttl_initial * 1e3 });
142
+ (_this$lruCache = this.#lruCache) === null || _this$lruCache === void 0 || _this$lruCache.set(token, cache_value, { ttl: cache_value.ttl_initial * 1e3 });
140
143
  }
141
144
  /**
142
145
  * Parses token.
@@ -158,28 +161,34 @@ var EcwtFactory = class {
158
161
  } catch {
159
162
  throw new EcwtParseError();
160
163
  }
161
- const payload = this.#cborEncoder ? this.#cborEncoder.decode(token_raw) : decode(token_raw);
162
- const [snowflake_buffer] = payload;
163
- [, ttl_initial, data] = payload;
164
+ const payload = v.parse(tokenSchema, this.#cborEncoder ? this.#cborEncoder.decode(token_raw) : decode(token_raw));
165
+ const snowflake_buffer = payload[0];
166
+ ttl_initial = payload[1];
167
+ const data_raw = payload[2];
164
168
  snowflake = this.#snowflakeFactory.parse(snowflake_buffer);
165
169
  if (typeof this.#validator === "function") try {
166
- data = this.#validator(data);
170
+ data = this.#validator(data_raw);
167
171
  } catch {
168
172
  throw new EcwtParseError();
169
173
  }
174
+ else data = data_raw;
170
175
  this.setCache(token, {
171
176
  snowflake,
172
177
  ttl_initial,
173
178
  data
174
179
  });
175
- } else ({snowflake, ttl_initial, data} = cached_entry.value);
180
+ } else {
181
+ snowflake = cached_entry.value.snowflake;
182
+ ttl_initial = cached_entry.value.ttl_initial;
183
+ data = cached_entry.value.data;
184
+ }
176
185
  const ecwt = new Ecwt(this, {
177
186
  token,
178
187
  snowflake,
179
188
  ttl_initial,
180
189
  data
181
190
  });
182
- if (typeof ttl_initial === "number" && Number.isNaN(ttl_initial) !== true && snowflake.timestamp + ttl_initial * 1e3 < Date.now()) throw new EcwtExpiredError(ecwt);
191
+ if (snowflake.timestamp + ttl_initial * 1e3 < Date.now()) throw new EcwtExpiredError(ecwt);
183
192
  if (this.#redisClient) {
184
193
  await this.#migrateExpired();
185
194
  if (await this.#redisClient.HEXISTS(this.#redis_key_revoked, ecwt.id)) throw new EcwtRevokedError(ecwt);
@@ -222,17 +231,14 @@ var EcwtFactory = class {
222
231
  async _revoke(token_id, created_at_ms, ttl_initial) {
223
232
  if (this.#redisClient) {
224
233
  await this.#migrateExpired();
225
- if (ttl_initial === null) await this.#redisClient.HSET(this.#redis_key_revoked, token_id, "");
226
- else {
227
- const expires_in_ms = created_at_ms + ttl_initial * 1e3 - Date.now();
228
- if (expires_in_ms > 0) await this.#redisClient.MULTI().HSET(this.#redis_key_revoked, token_id, "").HPEXPIRE(this.#redis_key_revoked, token_id, expires_in_ms).EXEC();
229
- }
234
+ const expires_in_ms = created_at_ms + ttl_initial * 1e3 - Date.now();
235
+ if (expires_in_ms > 0) await this.#redisClient.MULTI().HSET(this.#redis_key_revoked, token_id, "").HPEXPIRE(this.#redis_key_revoked, token_id, expires_in_ms).EXEC();
230
236
  } else console.warn("[ecwt] Redis client is not provided. Tokens cannot be revoked.");
231
237
  }
232
238
  #migrated = false;
233
239
  async #migrateExpired() {
234
240
  if (this.#redisClient && !this.#migrated) {
235
- await this.#redisClient.EVAL("local key = KEYS[1] if redis.call(\"TYPE\", key)[\"ok\"] ~= \"zset\" then return end local key_hash = key .. \":hash\" local ts_now = tonumber(ARGV[1]) local cursor = \"0\" repeat local scan = redis.call(\"ZSCAN\", key, cursor, \"COUNT\", 1000) cursor = scan[1] local items = scan[2] for i = 1, #items, 2 do local field = items[i] local expire_at = tonumber(items[i + 1]) local expire_in = expire_at and expire_at - ts_now if expire_in and expire_in > 0 then redis.call(\"HSET\", key_hash, field, \"\") redis.call(\"HPEXPIRE\", key_hash, expire_in, \"FIELDS\", 1, field) end end until cursor == \"0\" redis.call(\"DEL\", key) redis.call(\"RENAME\", key_hash, key)", {
241
+ await this.#redisClient.EVAL("local key = KEYS[1] if redis.call(\"TYPE\", key)[\"ok\"] ~= \"zset\" then return end local key_hash = key .. \":hash\" local ts_now = tonumber(ARGV[1]) local cursor = \"0\" repeat local scan = redis.call(\"ZSCAN\", key, cursor, \"COUNT\", 1000) cursor = scan[1] local items = scan[2] for i = 1, #items, 2 do local field = items[i] local expire_at = tonumber(items[i + 1]) local expire_in = expire_at and expire_at - ts_now if expire_in and expire_in > 0 then redis.call(\"HSET\", key_hash, field, \"\") redis.call(\"HPEXPIRE\", key_hash, expire_in, \"FIELDS\", 1, field) end end until cursor == \"0\" redis.call(\"DEL\", key) if redis.call(\"EXISTS\", key_hash) == 1 then redis.call(\"RENAME\", key_hash, key) end", {
236
242
  keys: [this.#redis_key_revoked],
237
243
  arguments: [String(Date.now())]
238
244
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ecwt",
3
- "version": "0.3.3",
3
+ "version": "0.4.0",
4
4
  "description": "Encrypted CBOR Web Token",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -24,7 +24,8 @@
24
24
  "dependencies": {
25
25
  "base-x": "5.0.1",
26
26
  "cbor-x": "1.5.6",
27
- "evilcrypt": "0.2.3"
27
+ "evilcrypt": "0.2.3",
28
+ "valibot": "1.4.1"
28
29
  },
29
30
  "peerDependencies": {
30
31
  "@kirick/snowflake": "^0.3",
@@ -41,7 +42,6 @@
41
42
  "tsdown": "0.21.10",
42
43
  "typescript": "6.0.2",
43
44
  "unplugin-unused": "0.5.7",
44
- "valibot": "1.4.0",
45
45
  "vitest": "4.1.7"
46
46
  },
47
47
  "scripts": {