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 +49 -41
- package/dist/ecwt.cjs +131 -63
- package/package.json +10 -9
- package/src/factory.js +59 -42
- package/src/main.js +5 -0
- package/src/token.js +82 -34
- package/src/utils/errors.js +6 -0
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
|
-
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
197
|
+
const ecwt = await ecwtFactory.verify(token);
|
|
194
198
|
```
|
|
195
199
|
|
|
196
200
|
### `Ecwt`
|
|
197
201
|
|
|
198
|
-
Represents the token.
|
|
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
|
-
|
|
224
|
+
#### Class property `readonly data: { [key: string]: any }`
|
|
219
225
|
|
|
220
|
-
|
|
226
|
+
The payload of the token.
|
|
221
227
|
|
|
222
|
-
|
|
228
|
+
#### Class method `getTTL`
|
|
223
229
|
|
|
224
|
-
|
|
230
|
+
```typescript
|
|
231
|
+
getTTL(): number | null
|
|
232
|
+
```
|
|
225
233
|
|
|
226
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
|
|
132
|
+
#getTimestampExpired() {
|
|
86
133
|
if (this.#ttl_initial === null) {
|
|
87
|
-
return
|
|
134
|
+
return null;
|
|
88
135
|
}
|
|
89
|
-
return toSeconds(this
|
|
136
|
+
return toSeconds(this.snowflake.timestamp) + this.#ttl_initial;
|
|
90
137
|
}
|
|
91
138
|
/**
|
|
92
|
-
*
|
|
93
|
-
* @
|
|
94
|
-
* @readonly
|
|
139
|
+
* Actual time to live in seconds.
|
|
140
|
+
* @returns {number | null} -
|
|
95
141
|
*/
|
|
96
|
-
|
|
142
|
+
getTTL() {
|
|
97
143
|
if (this.#ttl_initial === null) {
|
|
98
|
-
return
|
|
144
|
+
return null;
|
|
99
145
|
}
|
|
100
|
-
return this.#ttl_initial - toSeconds(Date.now() - this
|
|
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
|
|
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
|
-
#
|
|
165
|
-
#
|
|
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 {
|
|
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
|
-
|
|
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.#
|
|
214
|
-
|
|
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
|
-
|
|
228
|
-
|
|
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
|
|
237
|
-
|
|
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
|
|
241
|
-
snowflake.
|
|
292
|
+
const payload = [
|
|
293
|
+
snowflake.toBuffer(),
|
|
242
294
|
ttl,
|
|
243
|
-
|
|
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
|
-
|
|
296
|
-
|
|
297
|
-
|
|
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
|
-
|
|
302
|
-
|
|
303
|
-
|
|
359
|
+
snowflake_buffer
|
|
360
|
+
] = payload;
|
|
361
|
+
[
|
|
362
|
+
,
|
|
363
|
+
ttl_initial,
|
|
364
|
+
data
|
|
365
|
+
] = payload;
|
|
304
366
|
snowflake = this.#snowflakeFactory.parse(snowflake_buffer);
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
23
|
-
"lru-cache": "^
|
|
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": "
|
|
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
|
-
"
|
|
36
|
-
"
|
|
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
|
|
40
|
-
"test-node": "
|
|
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
|
|
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
|
|
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
|
-
#
|
|
43
|
-
#
|
|
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 {
|
|
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
|
-
|
|
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.#
|
|
107
|
-
|
|
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
|
-
|
|
125
|
-
|
|
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
|
-
|
|
142
|
-
|
|
143
|
-
)
|
|
144
|
-
|| ttl === Number.POSITIVE_INFINITY
|
|
138
|
+
typeof ttl !== 'number'
|
|
139
|
+
&& Number.isNaN(ttl) !== true
|
|
140
|
+
&& ttl !== null
|
|
145
141
|
) {
|
|
146
|
-
|
|
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
|
|
152
|
-
snowflake.
|
|
147
|
+
const payload = [
|
|
148
|
+
snowflake.toBuffer(),
|
|
153
149
|
ttl,
|
|
154
|
-
|
|
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
|
-
|
|
217
|
-
|
|
218
|
-
|
|
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
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
51
|
-
|
|
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
|
-
|
|
103
|
+
#getTimestampExpired() {
|
|
55
104
|
if (this.#ttl_initial === null) {
|
|
56
|
-
return
|
|
105
|
+
return null;
|
|
57
106
|
}
|
|
58
107
|
|
|
59
|
-
return toSeconds(this
|
|
108
|
+
return toSeconds(this.snowflake.timestamp) + this.#ttl_initial;
|
|
60
109
|
}
|
|
61
110
|
|
|
62
111
|
/**
|
|
63
|
-
*
|
|
64
|
-
* @
|
|
65
|
-
* @readonly
|
|
112
|
+
* Actual time to live in seconds.
|
|
113
|
+
* @returns {number | null} -
|
|
66
114
|
*/
|
|
67
|
-
|
|
115
|
+
getTTL() {
|
|
68
116
|
if (this.#ttl_initial === null) {
|
|
69
|
-
return
|
|
117
|
+
return null;
|
|
70
118
|
}
|
|
71
119
|
|
|
72
|
-
return this.#ttl_initial - toSeconds(Date.now() - this
|
|
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
|
|
130
|
+
ts_ms_created: this.snowflake.timestamp,
|
|
83
131
|
ttl_initial: this.#ttl_initial,
|
|
84
132
|
});
|
|
85
133
|
}
|
package/src/utils/errors.js
CHANGED
|
@@ -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.');
|