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 +2 -6
- package/bun.lock +2 -2
- package/dist/main.cjs +31 -24
- package/dist/main.d.cts +16 -16
- package/dist/main.d.mts +16 -16
- package/dist/main.mjs +30 -24
- package/package.json +3 -3
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
|
|
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.
|
|
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.
|
|
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
|
|
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,
|
|
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
|
|
187
|
-
|
|
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(
|
|
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
|
|
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 (
|
|
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
|
-
|
|
250
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
87
|
+
constructor(ecwtFactory: EcwtFactory<D>, options: {
|
|
88
88
|
token: string;
|
|
89
89
|
snowflake: Snowflake;
|
|
90
|
-
ttl_initial: number
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
87
|
+
constructor(ecwtFactory: EcwtFactory<D>, options: {
|
|
88
88
|
token: string;
|
|
89
89
|
snowflake: Snowflake;
|
|
90
|
-
ttl_initial: number
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
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
|
|
163
|
-
|
|
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(
|
|
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
|
|
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 (
|
|
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
|
-
|
|
226
|
-
|
|
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
|
+
"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": {
|