ecwt 0.1.0-beta.1 → 0.1.0-beta.3
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 +232 -1
- package/dist/ecwt.cjs +354 -7
- package/package.json +16 -10
- package/src/factory.js +322 -2
- package/src/main.js +1 -1
- package/src/token.js +82 -1
- package/src/utils/base62.js +4 -0
- package/src/utils/errors.js +32 -0
- package/src/utils/time.js +17 -0
package/README.md
CHANGED
|
@@ -1,3 +1,234 @@
|
|
|
1
1
|
|
|
2
|
-
#
|
|
2
|
+
# ECWT
|
|
3
3
|
Encrypted CBOR-encoded Web Token
|
|
4
|
+
|
|
5
|
+
## What is it?
|
|
6
|
+
|
|
7
|
+
ECWT is module for creating and verifying encrypted CBOR-encoded Web Tokens. It is designed to be used in situations where JWT is used, but there are major differences:
|
|
8
|
+
|
|
9
|
+
| | JWT | ECWT |
|
|
10
|
+
| --- | --- | --- |
|
|
11
|
+
| Encoding | 🧐 JSON with base64 | ✅ CBOR <br> 2x smaller output |
|
|
12
|
+
| Binary data | 🧐 Double base64 encoding | ✅ Supported out of the box |
|
|
13
|
+
| Security | 📝 Signed <br> Payload is readable by everyone | 🔒 Encrypted <br> Payload is readable only by the private key possessor |
|
|
14
|
+
| Metadata | ➕ Type and algorithm, increases size | ✅ No unnecessary metadata |
|
|
15
|
+
| Revocation | 🧑💻 Requires additional implementation | ✅ Included with Redis |
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
ECWT depends on other modules, so you need to install them too.
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
npm install ecwt @kirick/snowflake
|
|
23
|
+
pnpm install ecwt @kirick/snowflake
|
|
24
|
+
bun install ecwt @kirick/snowflake
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### Some dependencies
|
|
28
|
+
|
|
29
|
+
`EcwtFactory` depends on other modules, so you might be need to install them too.
|
|
30
|
+
|
|
31
|
+
#### `@kirick/snowflake` to create unique IDs (required)
|
|
32
|
+
|
|
33
|
+
For documentation, see [snowflake-js repository](https://github.com/kirick13/snowflake-js).
|
|
34
|
+
|
|
35
|
+
```javascript
|
|
36
|
+
import { SnowflakeFactory } from '@kirick/snowflake';
|
|
37
|
+
|
|
38
|
+
const snowflakeFactory = new SnowflakeFactory({
|
|
39
|
+
server_id: 0,
|
|
40
|
+
worker_id: 0,
|
|
41
|
+
});
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
#### `redis` to store revoked tokens (optional)
|
|
45
|
+
|
|
46
|
+
```javascript
|
|
47
|
+
import { createClient } from 'redis';
|
|
48
|
+
|
|
49
|
+
const redisClient = createClient({
|
|
50
|
+
socket: {
|
|
51
|
+
host: 'localhost',
|
|
52
|
+
port: 6379,
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
await redisClient.connect();
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
#### `lru` to avoid decrypt the same token multiple times (optional)
|
|
60
|
+
|
|
61
|
+
```javascript
|
|
62
|
+
import { LRUCache } from 'lru-cache';
|
|
63
|
+
|
|
64
|
+
const lruCache = new LRUCache({
|
|
65
|
+
max: 1000, // maximum of 1000 items
|
|
66
|
+
ttl: 60 * 60 * 1000, // 1 hour
|
|
67
|
+
});
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
#### Validation library of your choice (optional)
|
|
71
|
+
|
|
72
|
+
To reduce the size of the token, ECWT does not include object keys in the payload. By specifying the schema, you also can validate the payloads.
|
|
73
|
+
|
|
74
|
+
In our example, we use [valibot](https://valibot.dev) library.
|
|
75
|
+
|
|
76
|
+
```javascript
|
|
77
|
+
import {
|
|
78
|
+
number,
|
|
79
|
+
string,
|
|
80
|
+
maxValue,
|
|
81
|
+
maxLength,
|
|
82
|
+
safeParse } from 'valibot';
|
|
83
|
+
|
|
84
|
+
const schema = {
|
|
85
|
+
user_id: (value) => safeParse(
|
|
86
|
+
number([
|
|
87
|
+
maxValue(10),
|
|
88
|
+
]),
|
|
89
|
+
value,
|
|
90
|
+
).success,
|
|
91
|
+
nick: (value) => safeParse(
|
|
92
|
+
string([
|
|
93
|
+
maxLength(10),
|
|
94
|
+
]),
|
|
95
|
+
value,
|
|
96
|
+
).success,
|
|
97
|
+
};
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
That schema will prevent creating tokens for users with ID greater than 10 and nicknames longer than 10 characters.
|
|
101
|
+
|
|
102
|
+
## API
|
|
103
|
+
|
|
104
|
+
### `EcwtFactory`
|
|
105
|
+
|
|
106
|
+
```typescript
|
|
107
|
+
constructor({
|
|
108
|
+
redisClient: RedisClientType?,
|
|
109
|
+
lruCache: LRU?,
|
|
110
|
+
snowflakeFactory: SnowflakeFactory,
|
|
111
|
+
options: {
|
|
112
|
+
namespace: string?,
|
|
113
|
+
key: Buffer,
|
|
114
|
+
schema: {
|
|
115
|
+
[key: string]: (value: any) => boolean,
|
|
116
|
+
} = {},
|
|
117
|
+
},
|
|
118
|
+
})
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Create your `EcwtFactory` instance to create, and parse tokens.
|
|
122
|
+
|
|
123
|
+
```javascript
|
|
124
|
+
import { EcwtFactory } from 'ecwt';
|
|
125
|
+
|
|
126
|
+
const ecwtFactory = new EcwtFactory({
|
|
127
|
+
redisClient,
|
|
128
|
+
lruCache,
|
|
129
|
+
snowflakeFactory,
|
|
130
|
+
options: {
|
|
131
|
+
// "options.namespace" is required to identify the storage of revoked tokens in Redis
|
|
132
|
+
namespace: 'test',
|
|
133
|
+
key: Buffer.from(
|
|
134
|
+
'54RoavO+7orGGCKqLXcMwNGFGbcnSEq22f9bJX3lT9lgEPSaRAMBaEnHgMQPTPXcifFvGZmDGzOFqUMfqXsAhQ==',
|
|
135
|
+
'base64',
|
|
136
|
+
),
|
|
137
|
+
schema,
|
|
138
|
+
},
|
|
139
|
+
});
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
#### Class method `create`
|
|
143
|
+
|
|
144
|
+
```typescript
|
|
145
|
+
create(
|
|
146
|
+
payload: {
|
|
147
|
+
[key: string]: any,
|
|
148
|
+
},
|
|
149
|
+
options: {
|
|
150
|
+
ttl: number?,
|
|
151
|
+
}
|
|
152
|
+
): Promise<Ecwt>
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Creates a token.
|
|
156
|
+
|
|
157
|
+
`options.ttl` specifies the time to live of the token in seconds. If not specified, the token will not expire.
|
|
158
|
+
|
|
159
|
+
Returns `Ecwt` instance.
|
|
160
|
+
|
|
161
|
+
```javascript
|
|
162
|
+
const ecwt_token = await ecwtFactory.create(
|
|
163
|
+
{
|
|
164
|
+
user_id: 1,
|
|
165
|
+
nick: 'kirick',
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
ttl: 30 * 60,
|
|
169
|
+
}
|
|
170
|
+
);
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
#### Class method `verify`
|
|
174
|
+
|
|
175
|
+
```typescript
|
|
176
|
+
verify(
|
|
177
|
+
token: string,
|
|
178
|
+
): Promise<Ecwt>
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
Parses string representation of the token and verifies it:
|
|
182
|
+
|
|
183
|
+
- to be decrypted properly,
|
|
184
|
+
- for expiration,
|
|
185
|
+
- for revocation (if Redis client is provided),
|
|
186
|
+
- for schema.
|
|
187
|
+
|
|
188
|
+
Returns `Ecwt` instance.
|
|
189
|
+
|
|
190
|
+
If the token is invalid, throws `EcwtInvalidError` which contains `Ecwt` instance in the `ecwt` property.
|
|
191
|
+
|
|
192
|
+
```javascript
|
|
193
|
+
const ecwt_token = await ecwtFactory.verify(token);
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
### `Ecwt`
|
|
197
|
+
|
|
198
|
+
Represents the token. It cannot be created by the user.
|
|
199
|
+
|
|
200
|
+
```javascript
|
|
201
|
+
import { Ecwt } from 'ecwt';
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
#### Class property `token: string`
|
|
205
|
+
|
|
206
|
+
The string representation of the token.
|
|
207
|
+
|
|
208
|
+
#### Class property `id: string`
|
|
209
|
+
|
|
210
|
+
The unique ID of the token.
|
|
211
|
+
|
|
212
|
+
#### Class property `snowflake: Snowflake`
|
|
213
|
+
|
|
214
|
+
The `Snowflake` instance of the token. For documentation, see [snowflake-js repository](https://github.com/kirick13/snowflake-js).
|
|
215
|
+
|
|
216
|
+
#### Class property `ts_expired: number`
|
|
217
|
+
|
|
218
|
+
The timestamp of the token expiration in seconds. If the token does not expire, it is `Number.POSITIVE_INFINITY`.
|
|
219
|
+
|
|
220
|
+
#### Class property `ttl: number`
|
|
221
|
+
|
|
222
|
+
Current the time to live of the token in seconds. If the token does not expire, it is `Number.POSITIVE_INFINITY`.
|
|
223
|
+
|
|
224
|
+
#### Class property `data: { [key: string]: any }`
|
|
225
|
+
|
|
226
|
+
The payload of the token.
|
|
227
|
+
|
|
228
|
+
#### Class method `revoke`
|
|
229
|
+
|
|
230
|
+
```typescript
|
|
231
|
+
revoke(): Promise<void>
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
Revokes the token. It will be impossible to verify it after that.
|
package/dist/ecwt.cjs
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
var __create = Object.create;
|
|
1
2
|
var __defProp = Object.defineProperty;
|
|
2
3
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
3
4
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
4
6
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
5
7
|
var __export = (target, all) => {
|
|
6
8
|
for (var name in all)
|
|
@@ -14,29 +16,374 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
14
16
|
}
|
|
15
17
|
return to;
|
|
16
18
|
};
|
|
19
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
20
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
21
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
22
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
23
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
24
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
25
|
+
mod
|
|
26
|
+
));
|
|
17
27
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
18
28
|
|
|
19
29
|
// src/main.js
|
|
20
30
|
var main_exports = {};
|
|
21
31
|
__export(main_exports, {
|
|
22
32
|
Ecwt: () => Ecwt,
|
|
23
|
-
|
|
33
|
+
EcwtFactory: () => EcwtFactory
|
|
24
34
|
});
|
|
25
35
|
module.exports = __toCommonJS(main_exports);
|
|
26
36
|
|
|
27
37
|
// src/factory.js
|
|
28
|
-
var
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
38
|
+
var import_snowflake = require("@kirick/snowflake");
|
|
39
|
+
var import_cbor_x = require("cbor-x");
|
|
40
|
+
var import_evilcrypt = require("evilcrypt");
|
|
41
|
+
var import_lru_cache = require("lru-cache");
|
|
42
|
+
var import_redis = require("redis");
|
|
43
|
+
|
|
44
|
+
// src/utils/time.js
|
|
45
|
+
function toSeconds(value) {
|
|
46
|
+
return Math.floor(value / 1e3);
|
|
47
|
+
}
|
|
32
48
|
|
|
33
49
|
// src/token.js
|
|
34
50
|
var Ecwt = class {
|
|
35
|
-
|
|
51
|
+
#ecwtFactory;
|
|
52
|
+
#token;
|
|
53
|
+
#snowflake;
|
|
54
|
+
#ttl_initial;
|
|
55
|
+
#data;
|
|
56
|
+
/**
|
|
57
|
+
* @param {EcwtFactory} ecwtFactory -
|
|
58
|
+
* @param {object} options -
|
|
59
|
+
* @param {string} options.token String representation of token.
|
|
60
|
+
* @param {Snowflake} options.snowflake -
|
|
61
|
+
* @param {number | null} options.ttl_initial Time to live in seconds at the moment of token creation.
|
|
62
|
+
* @param {object} options.data Data stored in token.
|
|
63
|
+
*/
|
|
64
|
+
constructor(ecwtFactory, {
|
|
65
|
+
token,
|
|
66
|
+
snowflake,
|
|
67
|
+
ttl_initial,
|
|
68
|
+
data
|
|
69
|
+
}) {
|
|
70
|
+
this.#ecwtFactory = ecwtFactory;
|
|
71
|
+
this.#token = token;
|
|
72
|
+
this.#snowflake = snowflake;
|
|
73
|
+
this.#ttl_initial = ttl_initial;
|
|
74
|
+
this.#data = Object.freeze(data);
|
|
75
|
+
}
|
|
76
|
+
get token() {
|
|
77
|
+
return this.#token;
|
|
78
|
+
}
|
|
79
|
+
get id() {
|
|
80
|
+
return this.#snowflake.base62;
|
|
81
|
+
}
|
|
82
|
+
get snowflake() {
|
|
83
|
+
return this.#snowflake;
|
|
84
|
+
}
|
|
85
|
+
get ts_expired() {
|
|
86
|
+
if (this.#ttl_initial === null) {
|
|
87
|
+
return Number.POSITIVE_INFINITY;
|
|
88
|
+
}
|
|
89
|
+
return toSeconds(this.#snowflake.timestamp) + this.#ttl_initial;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Time to live in seconds.
|
|
93
|
+
* @type {number}
|
|
94
|
+
* @readonly
|
|
95
|
+
*/
|
|
96
|
+
get ttl() {
|
|
97
|
+
if (this.#ttl_initial === null) {
|
|
98
|
+
return Number.POSITIVE_INFINITY;
|
|
99
|
+
}
|
|
100
|
+
return this.#ttl_initial - toSeconds(Date.now() - this.#snowflake.timestamp);
|
|
101
|
+
}
|
|
102
|
+
get data() {
|
|
103
|
+
return this.#data;
|
|
104
|
+
}
|
|
105
|
+
/* async */
|
|
106
|
+
revoke() {
|
|
107
|
+
return this.#ecwtFactory._revoke({
|
|
108
|
+
token_id: this.id,
|
|
109
|
+
ts_ms_created: this.#snowflake.timestamp,
|
|
110
|
+
ttl_initial: this.#ttl_initial
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
// src/utils/base62.js
|
|
116
|
+
var import_base_x = __toESM(require("base-x"), 1);
|
|
117
|
+
var base62 = (0, import_base_x.default)("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz");
|
|
118
|
+
|
|
119
|
+
// src/utils/errors.js
|
|
120
|
+
var InvalidPackageInstanceError = class extends TypeError {
|
|
121
|
+
constructor(property, class_name, package_name) {
|
|
122
|
+
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
|
+
}
|
|
124
|
+
};
|
|
125
|
+
var EcwtInvalidError = class extends Error {
|
|
126
|
+
constructor(ecwt) {
|
|
127
|
+
super("Ecwt token is invalid.");
|
|
128
|
+
this.ecwt = ecwt;
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
var EcwtExpiredError = class extends EcwtInvalidError {
|
|
132
|
+
constructor(ecwt) {
|
|
133
|
+
super();
|
|
134
|
+
this.ecwt = ecwt;
|
|
135
|
+
this.message = "Ecwt is expired.";
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
var EcwtRevokedError = class extends EcwtInvalidError {
|
|
139
|
+
constructor(ecwt) {
|
|
140
|
+
super();
|
|
141
|
+
this.ecwt = ecwt;
|
|
142
|
+
this.message = "Ecwt is revoked.";
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
// src/factory.js
|
|
147
|
+
var REDIS_PREFIX = "@ecwt:";
|
|
148
|
+
function getAllKeysList(value) {
|
|
149
|
+
const keys = [];
|
|
150
|
+
for (const key in value) {
|
|
151
|
+
keys.push(key);
|
|
152
|
+
}
|
|
153
|
+
return keys.sort().join(",");
|
|
154
|
+
}
|
|
155
|
+
var redisClient = (0, import_redis.createClient)();
|
|
156
|
+
var redis_client_constructor_name = redisClient.constructor.name;
|
|
157
|
+
var redis_client_keys = getAllKeysList(redisClient);
|
|
158
|
+
var EcwtFactory = class {
|
|
159
|
+
#redisClient;
|
|
160
|
+
#lruCache;
|
|
161
|
+
#snowflakeFactory;
|
|
162
|
+
#redis_keys = {};
|
|
163
|
+
#encryption_key;
|
|
164
|
+
#schema;
|
|
165
|
+
#schema_keys_sorted;
|
|
166
|
+
/**
|
|
167
|
+
*
|
|
168
|
+
* @param {object} param0 -
|
|
169
|
+
* @param {import('redis').RedisClientType} [param0.redisClient] RedisClient instance. If not provided, tokens will not be revoked and cannot be checked for revocation.
|
|
170
|
+
* @param {LRUCache} [param0.lruCache] LRUCache instance. If not provided, tokens will be decrypted every time they are verified.
|
|
171
|
+
* @param {SnowflakeFactory} param0.snowflakeFactory SnowflakeFactory instance.
|
|
172
|
+
* @param {object} param0.options -
|
|
173
|
+
* @param {string} [param0.options.namespace] Namespace for Redis keys.
|
|
174
|
+
* @param {Buffer} param0.options.key Encryption key, 64 bytes
|
|
175
|
+
* @param {{ [key: string]: (value: any) => boolean }} param0.options.schema Schema for token data. Each property is a validator function that returns true if value is valid.
|
|
176
|
+
*/
|
|
177
|
+
constructor({
|
|
178
|
+
redisClient: redisClient2 = null,
|
|
179
|
+
lruCache = null,
|
|
180
|
+
snowflakeFactory,
|
|
181
|
+
options: {
|
|
182
|
+
namespace = null,
|
|
183
|
+
key,
|
|
184
|
+
schema = {}
|
|
185
|
+
}
|
|
186
|
+
}) {
|
|
187
|
+
if (redisClient2 !== null && (redisClient2.constructor.name !== redis_client_constructor_name || getAllKeysList(redisClient2) !== redis_client_keys)) {
|
|
188
|
+
throw new InvalidPackageInstanceError(
|
|
189
|
+
"redisClient",
|
|
190
|
+
"Commander extends RedisClient",
|
|
191
|
+
"redis"
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
this.#redisClient = redisClient2;
|
|
195
|
+
if (lruCache !== null && lruCache instanceof import_lru_cache.LRUCache !== true) {
|
|
196
|
+
throw new InvalidPackageInstanceError(
|
|
197
|
+
"lruCache",
|
|
198
|
+
"LRUCache",
|
|
199
|
+
"lru-cache"
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
this.#lruCache = lruCache;
|
|
203
|
+
if (snowflakeFactory instanceof import_snowflake.SnowflakeFactory !== true) {
|
|
204
|
+
throw new InvalidPackageInstanceError(
|
|
205
|
+
"snowflakeFactory",
|
|
206
|
+
"SnowflakeFactory",
|
|
207
|
+
"@kirick/snowflake"
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
this.#snowflakeFactory = snowflakeFactory;
|
|
211
|
+
this.#redis_keys.revoked = `${REDIS_PREFIX}${namespace}:revoked`;
|
|
212
|
+
this.#encryption_key = key;
|
|
213
|
+
this.#schema = schema;
|
|
214
|
+
this.#schema_keys_sorted = Object.keys(schema).sort();
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Creates new token.
|
|
218
|
+
* @async
|
|
219
|
+
* @param {object} data Data to be stored in token.
|
|
220
|
+
* @param {object} [options] -
|
|
221
|
+
* @param {number} [options.ttl] Time to live in seconds. By default, token will never expire.
|
|
222
|
+
* @returns {Promise<Ecwt>} -
|
|
223
|
+
*/
|
|
224
|
+
async create(data, {
|
|
225
|
+
ttl
|
|
226
|
+
} = {}) {
|
|
227
|
+
const payload = [];
|
|
228
|
+
for (const key of this.#schema_keys_sorted) {
|
|
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);
|
|
235
|
+
}
|
|
236
|
+
if (typeof ttl !== "number" && Number.isNaN(ttl) !== true || ttl === Number.POSITIVE_INFINITY) {
|
|
237
|
+
ttl = null;
|
|
238
|
+
}
|
|
239
|
+
const snowflake = await this.#snowflakeFactory.createSafe();
|
|
240
|
+
const token_raw = (0, import_cbor_x.encode)([
|
|
241
|
+
snowflake.buffer,
|
|
242
|
+
ttl,
|
|
243
|
+
payload
|
|
244
|
+
]);
|
|
245
|
+
const token_encrypted = await import_evilcrypt.v2.encrypt(
|
|
246
|
+
token_raw,
|
|
247
|
+
this.#encryption_key
|
|
248
|
+
);
|
|
249
|
+
const token = base62.encode(token_encrypted);
|
|
250
|
+
this.#setCache(
|
|
251
|
+
token,
|
|
252
|
+
{
|
|
253
|
+
snowflake,
|
|
254
|
+
ttl_initial: ttl,
|
|
255
|
+
data
|
|
256
|
+
}
|
|
257
|
+
);
|
|
258
|
+
return new Ecwt(
|
|
259
|
+
this,
|
|
260
|
+
{
|
|
261
|
+
token,
|
|
262
|
+
snowflake,
|
|
263
|
+
ttl_initial: ttl,
|
|
264
|
+
data
|
|
265
|
+
}
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
#setCache(token, data) {
|
|
269
|
+
this.#lruCache?.set(
|
|
270
|
+
token,
|
|
271
|
+
data,
|
|
272
|
+
{
|
|
273
|
+
ttl: data.ttl * 1e3
|
|
274
|
+
}
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Parses token.
|
|
279
|
+
* @async
|
|
280
|
+
* @param {string} token String representation of token.
|
|
281
|
+
* @returns {Promise<Ecwt>} -
|
|
282
|
+
*/
|
|
283
|
+
async verify(token) {
|
|
284
|
+
if (typeof token !== "string") {
|
|
285
|
+
throw new TypeError("Token must be a string.");
|
|
286
|
+
}
|
|
287
|
+
let snowflake;
|
|
288
|
+
let ttl_initial;
|
|
289
|
+
let data;
|
|
290
|
+
const cached_entry = this.#lruCache?.info(token);
|
|
291
|
+
if (cached_entry === void 0) {
|
|
292
|
+
const token_encrypted = Buffer.from(
|
|
293
|
+
base62.decode(token)
|
|
294
|
+
);
|
|
295
|
+
const token_raw = await (0, import_evilcrypt.decrypt)(
|
|
296
|
+
token_encrypted,
|
|
297
|
+
this.#encryption_key
|
|
298
|
+
);
|
|
299
|
+
const [
|
|
300
|
+
snowflake_buffer,
|
|
301
|
+
_ttl_initial,
|
|
302
|
+
payload
|
|
303
|
+
] = (0, import_cbor_x.decode)(token_raw);
|
|
304
|
+
snowflake = this.#snowflakeFactory.parse(snowflake_buffer);
|
|
305
|
+
ttl_initial = _ttl_initial;
|
|
306
|
+
data = {};
|
|
307
|
+
for (const [index, key] of this.#schema_keys_sorted.entries()) {
|
|
308
|
+
data[key] = payload[index];
|
|
309
|
+
}
|
|
310
|
+
this.#setCache(
|
|
311
|
+
token,
|
|
312
|
+
{
|
|
313
|
+
snowflake,
|
|
314
|
+
ttl_initial,
|
|
315
|
+
data
|
|
316
|
+
}
|
|
317
|
+
);
|
|
318
|
+
} else {
|
|
319
|
+
({
|
|
320
|
+
snowflake,
|
|
321
|
+
ttl_initial,
|
|
322
|
+
data
|
|
323
|
+
} = cached_entry.value);
|
|
324
|
+
}
|
|
325
|
+
const ecwt = new Ecwt(
|
|
326
|
+
this,
|
|
327
|
+
{
|
|
328
|
+
token,
|
|
329
|
+
snowflake,
|
|
330
|
+
ttl_initial,
|
|
331
|
+
data
|
|
332
|
+
}
|
|
333
|
+
);
|
|
334
|
+
if (typeof ttl_initial === "number" && Number.isNaN(ttl_initial) !== true && snowflake.timestamp + ttl_initial * 1e3 < Date.now()) {
|
|
335
|
+
throw new EcwtExpiredError(ecwt);
|
|
336
|
+
}
|
|
337
|
+
if (this.#redisClient) {
|
|
338
|
+
const score = await this.#redisClient.ZSCORE(
|
|
339
|
+
this.#redis_keys.revoked,
|
|
340
|
+
ecwt.id
|
|
341
|
+
);
|
|
342
|
+
if (score !== null) {
|
|
343
|
+
throw new EcwtRevokedError(ecwt);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
return ecwt;
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* Revokes token.
|
|
350
|
+
* @async
|
|
351
|
+
* @param {object} options -
|
|
352
|
+
* @param {string} options.token_id -
|
|
353
|
+
* @param {number} options.ts_ms_created -
|
|
354
|
+
* @param {number} options.ttl_initial -
|
|
355
|
+
* @returns {Promise<void>} -
|
|
356
|
+
*/
|
|
357
|
+
async _revoke({
|
|
358
|
+
token_id,
|
|
359
|
+
ts_ms_created,
|
|
360
|
+
ttl_initial
|
|
361
|
+
}) {
|
|
362
|
+
if (this.#redisClient) {
|
|
363
|
+
const ts_ms_expired = ts_ms_created + ttl_initial * 1e3;
|
|
364
|
+
if (ts_ms_expired > Date.now()) {
|
|
365
|
+
await this.#redisClient.sendCommand([
|
|
366
|
+
"ZADD",
|
|
367
|
+
this.#redis_keys.revoked,
|
|
368
|
+
String(ts_ms_expired),
|
|
369
|
+
token_id
|
|
370
|
+
]);
|
|
371
|
+
}
|
|
372
|
+
} else {
|
|
373
|
+
console.warn("[ecwt] Redis client is not provided. Tokens cannot be revoked.");
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* Purges cache.
|
|
378
|
+
* @private
|
|
379
|
+
* @returns {void} -
|
|
380
|
+
*/
|
|
381
|
+
_purgeCache() {
|
|
382
|
+
this.#lruCache?.clear();
|
|
36
383
|
}
|
|
37
384
|
};
|
|
38
385
|
// Annotate the CommonJS export names for ESM import in node:
|
|
39
386
|
0 && (module.exports = {
|
|
40
387
|
Ecwt,
|
|
41
|
-
|
|
388
|
+
EcwtFactory
|
|
42
389
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ecwt",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
3
|
+
"version": "0.1.0-beta.3",
|
|
4
4
|
"description": "Encrypted CBOR-encoded Web Token",
|
|
5
5
|
"main": "src/main.js",
|
|
6
6
|
"type": "module",
|
|
@@ -14,30 +14,36 @@
|
|
|
14
14
|
"node": ">=14.0.0"
|
|
15
15
|
},
|
|
16
16
|
"dependencies": {
|
|
17
|
-
"cbor-x": "1.5.6",
|
|
18
17
|
"base-x": "4.0.0",
|
|
19
|
-
"
|
|
18
|
+
"cbor-x": "1.5.6",
|
|
19
|
+
"evilcrypt": "0.2.0-beta.4"
|
|
20
20
|
},
|
|
21
21
|
"peerDependencies": {
|
|
22
|
-
"@kirick/
|
|
23
|
-
"
|
|
24
|
-
"
|
|
22
|
+
"@kirick/snowflake": "^0.2.0-beta.7",
|
|
23
|
+
"lru-cache": "^7 || ^8 || ^9 || ^10",
|
|
24
|
+
"redis": "^4"
|
|
25
25
|
},
|
|
26
26
|
"devDependencies": {
|
|
27
27
|
"@babel/eslint-parser": "7.21.8",
|
|
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": "^46.9.1",
|
|
31
32
|
"eslint-plugin-node": "11.1.0",
|
|
32
33
|
"eslint-plugin-promise": "6.1.1",
|
|
33
|
-
"eslint-plugin-unicorn": "47.0.0"
|
|
34
|
+
"eslint-plugin-unicorn": "47.0.0",
|
|
35
|
+
"jest": "^29.7.0",
|
|
36
|
+
"valibot": "^0.24.1"
|
|
34
37
|
},
|
|
35
38
|
"scripts": {
|
|
36
|
-
"test": "npm run test-node && bun test",
|
|
37
|
-
"test-node": "NODE_OPTIONS=--experimental-vm-modules jest",
|
|
39
|
+
"test": "bun run redis-up && npm run test-node && bun test",
|
|
40
|
+
"test-node": "NODE_OPTIONS=--experimental-vm-modules jest --forceExit",
|
|
41
|
+
"coverage": "bun run redis-up && bun test --coverage",
|
|
38
42
|
"build": "bun run build-cjs",
|
|
39
43
|
"build-cjs": "bunx esbuild --bundle --platform=node --format=cjs --packages=external --outfile=dist/ecwt.cjs src/main.js",
|
|
40
|
-
"npm-publish": "bun run build && npm publish --tag beta"
|
|
44
|
+
"npm-publish": "bun run build && bun run eslint . && bun run test && npm publish --tag beta; bun run redis-down",
|
|
45
|
+
"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
|
+
"redis-down": "docker stop test-redis-ecwt-test"
|
|
41
47
|
},
|
|
42
48
|
"repository": {
|
|
43
49
|
"type": "git",
|
package/src/factory.js
CHANGED
|
@@ -1,5 +1,325 @@
|
|
|
1
1
|
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
import { SnowflakeFactory } from '@kirick/snowflake';
|
|
3
|
+
import {
|
|
4
|
+
encode as cborEncode,
|
|
5
|
+
decode as cborDecode } from 'cbor-x';
|
|
6
|
+
import {
|
|
7
|
+
decrypt as evilcryptDecrypt,
|
|
8
|
+
v2 as evilcryptV2 } from 'evilcrypt';
|
|
9
|
+
import { LRUCache } from 'lru-cache';
|
|
10
|
+
import { createClient } from 'redis';
|
|
11
|
+
import { Ecwt } from './token.js';
|
|
12
|
+
import { base62 } from './utils/base62.js';
|
|
13
|
+
import {
|
|
14
|
+
InvalidPackageInstanceError,
|
|
15
|
+
EcwtExpiredError,
|
|
16
|
+
EcwtRevokedError } from './utils/errors.js';
|
|
17
|
+
|
|
18
|
+
const REDIS_PREFIX = '@ecwt:';
|
|
19
|
+
|
|
20
|
+
// eslint-disable-next-line jsdoc/require-jsdoc
|
|
21
|
+
function getAllKeysList(value) {
|
|
22
|
+
const keys = [];
|
|
23
|
+
// eslint-disable-next-line guard-for-in
|
|
24
|
+
for (const key in value) {
|
|
25
|
+
keys.push(key);
|
|
26
|
+
}
|
|
27
|
+
return keys.sort().join(',');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const redisClient = createClient();
|
|
31
|
+
const redis_client_constructor_name = redisClient.constructor.name;
|
|
32
|
+
const redis_client_keys = getAllKeysList(redisClient);
|
|
33
|
+
|
|
34
|
+
export class EcwtFactory {
|
|
35
|
+
#redisClient;
|
|
36
|
+
#lruCache;
|
|
37
|
+
#snowflakeFactory;
|
|
38
|
+
|
|
39
|
+
#redis_keys = {};
|
|
40
|
+
#encryption_key;
|
|
41
|
+
|
|
42
|
+
#schema;
|
|
43
|
+
#schema_keys_sorted;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
*
|
|
47
|
+
* @param {object} param0 -
|
|
48
|
+
* @param {import('redis').RedisClientType} [param0.redisClient] RedisClient instance. If not provided, tokens will not be revoked and cannot be checked for revocation.
|
|
49
|
+
* @param {LRUCache} [param0.lruCache] LRUCache instance. If not provided, tokens will be decrypted every time they are verified.
|
|
50
|
+
* @param {SnowflakeFactory} param0.snowflakeFactory SnowflakeFactory instance.
|
|
51
|
+
* @param {object} param0.options -
|
|
52
|
+
* @param {string} [param0.options.namespace] Namespace for Redis keys.
|
|
53
|
+
* @param {Buffer} param0.options.key Encryption key, 64 bytes
|
|
54
|
+
* @param {{ [key: string]: (value: any) => boolean }} param0.options.schema Schema for token data. Each property is a validator function that returns true if value is valid.
|
|
55
|
+
*/
|
|
56
|
+
constructor({
|
|
57
|
+
redisClient = null,
|
|
58
|
+
lruCache = null,
|
|
59
|
+
snowflakeFactory,
|
|
60
|
+
options: {
|
|
61
|
+
namespace = null,
|
|
62
|
+
key,
|
|
63
|
+
schema = {},
|
|
64
|
+
},
|
|
65
|
+
}) {
|
|
66
|
+
if (
|
|
67
|
+
redisClient !== null
|
|
68
|
+
&& (
|
|
69
|
+
redisClient.constructor.name !== redis_client_constructor_name
|
|
70
|
+
|| getAllKeysList(redisClient) !== redis_client_keys
|
|
71
|
+
)
|
|
72
|
+
) {
|
|
73
|
+
throw new InvalidPackageInstanceError(
|
|
74
|
+
'redisClient',
|
|
75
|
+
'Commander extends RedisClient',
|
|
76
|
+
'redis',
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
this.#redisClient = redisClient;
|
|
80
|
+
|
|
81
|
+
if (
|
|
82
|
+
lruCache !== null
|
|
83
|
+
&& lruCache instanceof LRUCache !== true
|
|
84
|
+
) {
|
|
85
|
+
throw new InvalidPackageInstanceError(
|
|
86
|
+
'lruCache',
|
|
87
|
+
'LRUCache',
|
|
88
|
+
'lru-cache',
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
this.#lruCache = lruCache;
|
|
92
|
+
|
|
93
|
+
if (snowflakeFactory instanceof SnowflakeFactory !== true) {
|
|
94
|
+
throw new InvalidPackageInstanceError(
|
|
95
|
+
'snowflakeFactory',
|
|
96
|
+
'SnowflakeFactory',
|
|
97
|
+
'@kirick/snowflake',
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
this.#snowflakeFactory = snowflakeFactory;
|
|
101
|
+
|
|
102
|
+
this.#redis_keys.revoked = `${REDIS_PREFIX}${namespace}:revoked`;
|
|
103
|
+
|
|
104
|
+
this.#encryption_key = key;
|
|
105
|
+
|
|
106
|
+
this.#schema = schema;
|
|
107
|
+
this.#schema_keys_sorted = Object.keys(schema).sort();
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Creates new token.
|
|
112
|
+
* @async
|
|
113
|
+
* @param {object} data Data to be stored in token.
|
|
114
|
+
* @param {object} [options] -
|
|
115
|
+
* @param {number} [options.ttl] Time to live in seconds. By default, token will never expire.
|
|
116
|
+
* @returns {Promise<Ecwt>} -
|
|
117
|
+
*/
|
|
118
|
+
async create(
|
|
119
|
+
data,
|
|
120
|
+
{
|
|
121
|
+
ttl,
|
|
122
|
+
} = {},
|
|
123
|
+
) {
|
|
124
|
+
const payload = [];
|
|
125
|
+
for (const key of this.#schema_keys_sorted) {
|
|
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);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (
|
|
140
|
+
(
|
|
141
|
+
typeof ttl !== 'number'
|
|
142
|
+
&& Number.isNaN(ttl) !== true
|
|
143
|
+
)
|
|
144
|
+
|| ttl === Number.POSITIVE_INFINITY
|
|
145
|
+
) {
|
|
146
|
+
ttl = null;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const snowflake = await this.#snowflakeFactory.createSafe();
|
|
150
|
+
|
|
151
|
+
const token_raw = cborEncode([
|
|
152
|
+
snowflake.buffer,
|
|
153
|
+
ttl,
|
|
154
|
+
payload,
|
|
155
|
+
]);
|
|
156
|
+
|
|
157
|
+
const token_encrypted = await evilcryptV2.encrypt(
|
|
158
|
+
token_raw,
|
|
159
|
+
this.#encryption_key,
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
const token = base62.encode(token_encrypted);
|
|
163
|
+
|
|
164
|
+
this.#setCache(
|
|
165
|
+
token,
|
|
166
|
+
{
|
|
167
|
+
snowflake,
|
|
168
|
+
ttl_initial: ttl,
|
|
169
|
+
data,
|
|
170
|
+
},
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
return new Ecwt(
|
|
174
|
+
this,
|
|
175
|
+
{
|
|
176
|
+
token,
|
|
177
|
+
snowflake,
|
|
178
|
+
ttl_initial: ttl,
|
|
179
|
+
data,
|
|
180
|
+
},
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
#setCache(token, data) {
|
|
185
|
+
this.#lruCache?.set(
|
|
186
|
+
token,
|
|
187
|
+
data,
|
|
188
|
+
{
|
|
189
|
+
ttl: data.ttl * 1000,
|
|
190
|
+
},
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Parses token.
|
|
196
|
+
* @async
|
|
197
|
+
* @param {string} token String representation of token.
|
|
198
|
+
* @returns {Promise<Ecwt>} -
|
|
199
|
+
*/
|
|
200
|
+
async verify(token) {
|
|
201
|
+
if (typeof token !== 'string') {
|
|
202
|
+
throw new TypeError('Token must be a string.');
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
let snowflake;
|
|
206
|
+
let ttl_initial;
|
|
207
|
+
let data;
|
|
208
|
+
|
|
209
|
+
const cached_entry = this.#lruCache?.info(token);
|
|
210
|
+
// token is cached
|
|
211
|
+
if (cached_entry === undefined) {
|
|
212
|
+
const token_encrypted = Buffer.from(
|
|
213
|
+
base62.decode(token),
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
const token_raw = await evilcryptDecrypt(
|
|
217
|
+
token_encrypted,
|
|
218
|
+
this.#encryption_key,
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
const [
|
|
222
|
+
snowflake_buffer,
|
|
223
|
+
_ttl_initial,
|
|
224
|
+
payload,
|
|
225
|
+
] = cborDecode(token_raw);
|
|
226
|
+
|
|
227
|
+
snowflake = this.#snowflakeFactory.parse(snowflake_buffer);
|
|
228
|
+
ttl_initial = _ttl_initial;
|
|
229
|
+
|
|
230
|
+
data = {};
|
|
231
|
+
for (const [ index, key ] of this.#schema_keys_sorted.entries()) {
|
|
232
|
+
data[key] = payload[index];
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
this.#setCache(
|
|
236
|
+
token,
|
|
237
|
+
{
|
|
238
|
+
snowflake,
|
|
239
|
+
ttl_initial,
|
|
240
|
+
data,
|
|
241
|
+
},
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
else {
|
|
245
|
+
({
|
|
246
|
+
snowflake,
|
|
247
|
+
ttl_initial,
|
|
248
|
+
data,
|
|
249
|
+
} = cached_entry.value);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// console.log('snowflake', snowflake);
|
|
253
|
+
// console.log('ttl', ttl);
|
|
254
|
+
// console.log('data', data);
|
|
255
|
+
|
|
256
|
+
const ecwt = new Ecwt(
|
|
257
|
+
this,
|
|
258
|
+
{
|
|
259
|
+
token,
|
|
260
|
+
snowflake,
|
|
261
|
+
ttl_initial,
|
|
262
|
+
data,
|
|
263
|
+
},
|
|
264
|
+
);
|
|
265
|
+
|
|
266
|
+
if (
|
|
267
|
+
typeof ttl_initial === 'number'
|
|
268
|
+
&& Number.isNaN(ttl_initial) !== true
|
|
269
|
+
&& snowflake.timestamp + (ttl_initial * 1000) < Date.now()
|
|
270
|
+
) {
|
|
271
|
+
throw new EcwtExpiredError(ecwt);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (this.#redisClient) {
|
|
275
|
+
const score = await this.#redisClient.ZSCORE(
|
|
276
|
+
this.#redis_keys.revoked,
|
|
277
|
+
ecwt.id,
|
|
278
|
+
);
|
|
279
|
+
if (score !== null) {
|
|
280
|
+
throw new EcwtRevokedError(ecwt);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return ecwt;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Revokes token.
|
|
289
|
+
* @async
|
|
290
|
+
* @param {object} options -
|
|
291
|
+
* @param {string} options.token_id -
|
|
292
|
+
* @param {number} options.ts_ms_created -
|
|
293
|
+
* @param {number} options.ttl_initial -
|
|
294
|
+
* @returns {Promise<void>} -
|
|
295
|
+
*/
|
|
296
|
+
async _revoke({
|
|
297
|
+
token_id,
|
|
298
|
+
ts_ms_created,
|
|
299
|
+
ttl_initial,
|
|
300
|
+
}) {
|
|
301
|
+
if (this.#redisClient) {
|
|
302
|
+
const ts_ms_expired = ts_ms_created + (ttl_initial * 1000);
|
|
303
|
+
if (ts_ms_expired > Date.now()) {
|
|
304
|
+
await this.#redisClient.sendCommand([
|
|
305
|
+
'ZADD',
|
|
306
|
+
this.#redis_keys.revoked,
|
|
307
|
+
String(ts_ms_expired),
|
|
308
|
+
token_id,
|
|
309
|
+
]);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
else {
|
|
313
|
+
console.warn('[ecwt] Redis client is not provided. Tokens cannot be revoked.');
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Purges cache.
|
|
319
|
+
* @private
|
|
320
|
+
* @returns {void} -
|
|
321
|
+
*/
|
|
322
|
+
_purgeCache() {
|
|
323
|
+
this.#lruCache?.clear();
|
|
4
324
|
}
|
|
5
325
|
}
|
package/src/main.js
CHANGED
package/src/token.js
CHANGED
|
@@ -1,5 +1,86 @@
|
|
|
1
1
|
|
|
2
|
+
import { toSeconds } from './utils/time.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @typedef {import('@kirick/snowflake/src/snowflake.js').Snowflake} Snowflake
|
|
6
|
+
* @typedef {import('./factory.js').EcwtFactory} EcwtFactory
|
|
7
|
+
*/
|
|
8
|
+
|
|
2
9
|
export class Ecwt {
|
|
3
|
-
|
|
10
|
+
#ecwtFactory;
|
|
11
|
+
|
|
12
|
+
#token;
|
|
13
|
+
#snowflake;
|
|
14
|
+
#ttl_initial;
|
|
15
|
+
#data;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @param {EcwtFactory} ecwtFactory -
|
|
19
|
+
* @param {object} options -
|
|
20
|
+
* @param {string} options.token String representation of token.
|
|
21
|
+
* @param {Snowflake} options.snowflake -
|
|
22
|
+
* @param {number | null} options.ttl_initial Time to live in seconds at the moment of token creation.
|
|
23
|
+
* @param {object} options.data Data stored in token.
|
|
24
|
+
*/
|
|
25
|
+
constructor(
|
|
26
|
+
ecwtFactory,
|
|
27
|
+
{
|
|
28
|
+
token,
|
|
29
|
+
snowflake,
|
|
30
|
+
ttl_initial,
|
|
31
|
+
data,
|
|
32
|
+
},
|
|
33
|
+
) {
|
|
34
|
+
this.#ecwtFactory = ecwtFactory;
|
|
35
|
+
|
|
36
|
+
this.#token = token;
|
|
37
|
+
this.#snowflake = snowflake;
|
|
38
|
+
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
|
+
|
|
50
|
+
get snowflake() {
|
|
51
|
+
return this.#snowflake;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
get ts_expired() {
|
|
55
|
+
if (this.#ttl_initial === null) {
|
|
56
|
+
return Number.POSITIVE_INFINITY;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return toSeconds(this.#snowflake.timestamp) + this.#ttl_initial;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Time to live in seconds.
|
|
64
|
+
* @type {number}
|
|
65
|
+
* @readonly
|
|
66
|
+
*/
|
|
67
|
+
get ttl() {
|
|
68
|
+
if (this.#ttl_initial === null) {
|
|
69
|
+
return Number.POSITIVE_INFINITY;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return this.#ttl_initial - toSeconds(Date.now() - this.#snowflake.timestamp);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
get data() {
|
|
76
|
+
return this.#data;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/* async */ revoke() {
|
|
80
|
+
return this.#ecwtFactory._revoke({
|
|
81
|
+
token_id: this.id,
|
|
82
|
+
ts_ms_created: this.#snowflake.timestamp,
|
|
83
|
+
ttl_initial: this.#ttl_initial,
|
|
84
|
+
});
|
|
4
85
|
}
|
|
5
86
|
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
|
|
2
|
+
export class InvalidPackageInstanceError extends TypeError {
|
|
3
|
+
constructor(property, class_name, package_name) {
|
|
4
|
+
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.`);
|
|
5
|
+
}
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export class EcwtInvalidError extends Error {
|
|
9
|
+
constructor(ecwt) {
|
|
10
|
+
super('Ecwt token is invalid.');
|
|
11
|
+
|
|
12
|
+
this.ecwt = ecwt;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export class EcwtExpiredError extends EcwtInvalidError {
|
|
17
|
+
constructor(ecwt) {
|
|
18
|
+
super();
|
|
19
|
+
|
|
20
|
+
this.ecwt = ecwt;
|
|
21
|
+
this.message = 'Ecwt is expired.';
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export class EcwtRevokedError extends EcwtInvalidError {
|
|
26
|
+
constructor(ecwt) {
|
|
27
|
+
super();
|
|
28
|
+
|
|
29
|
+
this.ecwt = ecwt;
|
|
30
|
+
this.message = 'Ecwt is revoked.';
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
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
|
+
// }
|