ecwt 0.2.1-beta.6 → 0.2.5
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 +197 -157
- package/bun.lock +948 -0
- package/dist/main.cjs +249 -435
- package/dist/main.d.ts +160 -0
- package/dist/main.js +246 -0
- package/package.json +28 -23
- package/bun.lockb +0 -0
- package/dist/types/factory.d.ts +0 -97
- package/dist/types/main.d.ts +0 -4
- package/dist/types/token.d.ts +0 -66
- package/dist/types/utils/base62.d.ts +0 -2
- package/dist/types/utils/errors.d.ts +0 -38
- package/dist/types/utils/time.d.ts +0 -6
- package/src/factory.js +0 -385
- package/src/main.js +0 -13
- package/src/token.js +0 -106
- package/src/utils/base62.js +0 -4
- package/src/utils/errors.js +0 -58
- package/src/utils/time.js +0 -17
package/README.md
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
|
|
2
1
|
# ECWT
|
|
3
|
-
Encrypted CBOR-encoded Web Token
|
|
4
2
|
|
|
5
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/ecwt)
|
|
4
|
+
[](https://opensource.org/licenses/MIT)
|
|
6
5
|
|
|
7
|
-
ECWT is module for creating and verifying encrypted CBOR
|
|
6
|
+
ECWT is module for creating and verifying encrypted CBOR Web Tokens. It is designed to be used in situations where JWT is used, but there are major differences:
|
|
8
7
|
|
|
9
8
|
| | JWT | ECWT |
|
|
10
9
|
| --- | --- | --- |
|
|
@@ -18,9 +17,11 @@ ECWT is module for creating and verifying encrypted CBOR-encoded Web Tokens. It
|
|
|
18
17
|
|
|
19
18
|
ECWT depends on other modules, so you need to install them too.
|
|
20
19
|
|
|
21
|
-
```
|
|
20
|
+
```sh
|
|
22
21
|
npm install ecwt @kirick/snowflake
|
|
22
|
+
# or
|
|
23
23
|
pnpm install ecwt @kirick/snowflake
|
|
24
|
+
# or
|
|
24
25
|
bun install ecwt @kirick/snowflake
|
|
25
26
|
```
|
|
26
27
|
|
|
@@ -30,14 +31,14 @@ bun install ecwt @kirick/snowflake
|
|
|
30
31
|
|
|
31
32
|
#### `@kirick/snowflake` to create unique IDs (required)
|
|
32
33
|
|
|
33
|
-
For documentation, see [snowflake
|
|
34
|
+
For documentation, see [snowflake repository](https://github.com/kirick-ts/snowflake).
|
|
34
35
|
|
|
35
36
|
```javascript
|
|
36
37
|
import { SnowflakeFactory } from '@kirick/snowflake';
|
|
37
38
|
|
|
38
39
|
const snowflakeFactory = new SnowflakeFactory({
|
|
39
|
-
|
|
40
|
-
|
|
40
|
+
server_id: 0,
|
|
41
|
+
worker_id: 0,
|
|
41
42
|
});
|
|
42
43
|
```
|
|
43
44
|
|
|
@@ -47,23 +48,23 @@ const snowflakeFactory = new SnowflakeFactory({
|
|
|
47
48
|
import { createClient } from 'redis';
|
|
48
49
|
|
|
49
50
|
const redisClient = createClient({
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
51
|
+
socket: {
|
|
52
|
+
host: 'localhost',
|
|
53
|
+
port: 6379,
|
|
54
|
+
},
|
|
54
55
|
});
|
|
55
56
|
|
|
56
57
|
await redisClient.connect();
|
|
57
58
|
```
|
|
58
59
|
|
|
59
|
-
#### `lru` to avoid decrypt the same token multiple times (optional)
|
|
60
|
+
#### `lru-cache` to avoid decrypt the same token multiple times (optional)
|
|
60
61
|
|
|
61
62
|
```javascript
|
|
62
63
|
import { LRUCache } from 'lru-cache';
|
|
63
64
|
|
|
64
65
|
const lruCache = new LRUCache({
|
|
65
|
-
|
|
66
|
-
|
|
66
|
+
max: 1000, // maximum of 1000 items
|
|
67
|
+
ttl: 60 * 60 * 1000, // 1 hour
|
|
67
68
|
});
|
|
68
69
|
```
|
|
69
70
|
|
|
@@ -76,191 +77,230 @@ In our example, we use [valibot](https://valibot.dev) library.
|
|
|
76
77
|
```javascript
|
|
77
78
|
import * as v from 'valibot';
|
|
78
79
|
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
80
|
+
const validator = v.parser(
|
|
81
|
+
v.object({
|
|
82
|
+
user_id: v.pipe(
|
|
83
|
+
v.number(),
|
|
84
|
+
v.maxValue(10),
|
|
85
|
+
),
|
|
86
|
+
nick: v.pipe(
|
|
87
|
+
v.string(),
|
|
88
|
+
v.maxLength(10),
|
|
89
|
+
),
|
|
90
|
+
}),
|
|
89
91
|
);
|
|
90
92
|
```
|
|
91
93
|
|
|
92
|
-
That
|
|
93
|
-
|
|
94
|
-
## API
|
|
94
|
+
That validator will prevent creating tokens for users with ID greater than 10 and nicknames longer than 10 characters.
|
|
95
95
|
|
|
96
|
-
|
|
96
|
+
## Usage Examples
|
|
97
97
|
|
|
98
|
-
|
|
99
|
-
constructor({
|
|
100
|
-
redisClient: RedisClientType?,
|
|
101
|
-
lruCache: LRU?,
|
|
102
|
-
snowflakeFactory: SnowflakeFactory,
|
|
103
|
-
options: {
|
|
104
|
-
namespace: string?,
|
|
105
|
-
key: Buffer,
|
|
106
|
-
schema: (value: any) => any,
|
|
107
|
-
senmlKeyMap: {
|
|
108
|
-
[key: string]: number,
|
|
109
|
-
}?,
|
|
110
|
-
},
|
|
111
|
-
})
|
|
112
|
-
```
|
|
98
|
+
### Initializing the EcwtFactory
|
|
113
99
|
|
|
114
|
-
|
|
100
|
+
First, configure the EcwtFactory with your environment dependencies:
|
|
115
101
|
|
|
116
102
|
```javascript
|
|
117
103
|
import { EcwtFactory } from 'ecwt';
|
|
104
|
+
import { SnowflakeFactory } from '@kirick/snowflake';
|
|
105
|
+
import { LRUCache } from 'lru-cache';
|
|
106
|
+
import { createClient } from 'redis';
|
|
118
107
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
options: {
|
|
124
|
-
// "options.namespace" is required to identify the storage of revoked tokens in Redis
|
|
125
|
-
namespace: 'test',
|
|
126
|
-
key: Buffer.from(
|
|
127
|
-
'54RoavO+7orGGCKqLXcMwNGFGbcnSEq22f9bJX3lT9lgEPSaRAMBaEnHgMQPTPXcifFvGZmDGzOFqUMfqXsAhQ==',
|
|
128
|
-
'base64',
|
|
129
|
-
),
|
|
130
|
-
schema,
|
|
131
|
-
senml_key_map: {
|
|
132
|
-
user_id: 1,
|
|
133
|
-
nick: 2,
|
|
134
|
-
},
|
|
135
|
-
},
|
|
108
|
+
// Required: Initialize SnowflakeFactory for token ID generation
|
|
109
|
+
const snowflakeFactory = new SnowflakeFactory({
|
|
110
|
+
server_id: 0,
|
|
111
|
+
worker_id: 0,
|
|
136
112
|
});
|
|
137
|
-
```
|
|
138
113
|
|
|
139
|
-
|
|
114
|
+
// Optional but recommended: Configure LRU cache for performance optimization
|
|
115
|
+
const lruCache = new LRUCache({
|
|
116
|
+
max: 1000, // Maximum cache size
|
|
117
|
+
ttl: 60 * 60 * 1000, // Cache expiration (1 hour)
|
|
118
|
+
});
|
|
140
119
|
|
|
141
|
-
|
|
120
|
+
// Optional: Set up Redis client for token revocation capabilities
|
|
121
|
+
const redisClient = createClient({
|
|
122
|
+
socket: {
|
|
123
|
+
host: 'localhost',
|
|
124
|
+
port: 6379,
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
await redisClient.connect();
|
|
142
128
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
)
|
|
129
|
+
// Initialize the factory with your configuration
|
|
130
|
+
const ecwtFactory = new EcwtFactory({
|
|
131
|
+
redisClient,
|
|
132
|
+
lruCache,
|
|
133
|
+
snowflakeFactory,
|
|
134
|
+
options: {
|
|
135
|
+
// Unique namespace for Redis keys to prevent collisions
|
|
136
|
+
namespace: 'auth-service',
|
|
137
|
+
// Your 64-byte encryption key (store securely)
|
|
138
|
+
key: Buffer.from('YOUR_BASE64_KEY', 'base64'),
|
|
139
|
+
// Schema validator for payload structure validation
|
|
140
|
+
validator: myValidator,
|
|
141
|
+
},
|
|
142
|
+
});
|
|
152
143
|
```
|
|
153
144
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
`options.ttl` specifies the time to live of the token in seconds. If set to null, token will never expire.
|
|
145
|
+
### Token Generation
|
|
157
146
|
|
|
158
|
-
|
|
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.
|
|
161
|
-
|
|
162
|
-
Returns `Ecwt` instance.
|
|
147
|
+
Generate tokens with precise payload and expiration controls:
|
|
163
148
|
|
|
164
149
|
```javascript
|
|
165
|
-
//
|
|
150
|
+
// Create an access token with a 30-minute expiration
|
|
166
151
|
const ecwt = await ecwtFactory.create(
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
152
|
+
{
|
|
153
|
+
user_id: 123,
|
|
154
|
+
name: "John Doe",
|
|
155
|
+
role: "admin"
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
ttl: 30 * 60 // 30 minutes in seconds
|
|
159
|
+
}
|
|
174
160
|
);
|
|
175
|
-
```
|
|
176
161
|
|
|
177
|
-
|
|
162
|
+
// Get string representation of the token
|
|
163
|
+
const serializedToken = ecwt.token;
|
|
178
164
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
165
|
+
// Access token metadata
|
|
166
|
+
console.log(`Token ID: ${ecwt.id}`);
|
|
167
|
+
console.log(`Expiration timestamp: ${ecwt.ts_expired}`);
|
|
168
|
+
console.log(`Remaining validity: ${ecwt.getTTL()} seconds`);
|
|
183
169
|
```
|
|
184
170
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
- for expiration,
|
|
189
|
-
- for revocation (if Redis client is provided),
|
|
190
|
-
- for schema.
|
|
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.
|
|
191
174
|
|
|
192
|
-
|
|
175
|
+
### Token Verification
|
|
193
176
|
|
|
194
|
-
|
|
177
|
+
Implement verification with appropriate error handling:
|
|
195
178
|
|
|
196
179
|
```javascript
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
180
|
+
import {
|
|
181
|
+
EcwtExpiredError,
|
|
182
|
+
EcwtRevokedError,
|
|
183
|
+
EcwtParseError,
|
|
184
|
+
EcwtInvalidError
|
|
185
|
+
} from 'ecwt';
|
|
186
|
+
|
|
187
|
+
try {
|
|
188
|
+
// Verify and decode the token
|
|
189
|
+
const verifiedToken = await ecwtFactory.verify(serializedToken);
|
|
190
|
+
|
|
191
|
+
// Access verified payload data
|
|
192
|
+
const { user_id, name, role } = verifiedToken.data;
|
|
193
|
+
|
|
194
|
+
// Proceed with authenticated operation
|
|
195
|
+
|
|
196
|
+
} catch (error) {
|
|
197
|
+
// Handle specific verification failures
|
|
198
|
+
if (error instanceof EcwtExpiredError) {
|
|
199
|
+
return respondWithError(401, "Authentication expired");
|
|
200
|
+
} else if (error instanceof EcwtRevokedError) {
|
|
201
|
+
return respondWithError(401, "Authentication revoked");
|
|
202
|
+
} else if (error instanceof EcwtParseError) {
|
|
203
|
+
return respondWithError(400, "Malformed authentication token");
|
|
204
|
+
} else if (error instanceof EcwtInvalidError) {
|
|
205
|
+
return respondWithError(401, "Invalid authentication token");
|
|
206
|
+
} else {
|
|
207
|
+
logger.error("Token verification error", error);
|
|
208
|
+
return respondWithError(500, "Authentication service error");
|
|
209
|
+
}
|
|
210
|
+
}
|
|
209
211
|
```
|
|
210
212
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
Property `success` is `true` if the token is valid.
|
|
214
|
-
|
|
215
|
-
Property `ecwt` is `null` if the token cannot be parsed, otherwise it contains `Ecwt` instance.
|
|
213
|
+
For exception-free verification, use `safeVerify`:
|
|
216
214
|
|
|
217
215
|
```javascript
|
|
218
|
-
const {
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
216
|
+
const { success, ecwt } = await ecwtFactory.safeVerify(serializedToken);
|
|
217
|
+
|
|
218
|
+
if (success) {
|
|
219
|
+
// Proceed with authenticated request
|
|
220
|
+
const userData = ecwt.data;
|
|
221
|
+
return processAuthenticatedRequest(userData);
|
|
222
|
+
} else if (ecwt) {
|
|
223
|
+
// Token structure was valid but failed verification
|
|
224
|
+
logger.info(`Auth failure: token ${ecwt.id} is invalid`);
|
|
225
|
+
return respondWithError(401, "Authentication token invalid");
|
|
226
|
+
} else {
|
|
227
|
+
// Unparsable token structure
|
|
228
|
+
logger.warn(`Auth failure: malformed token received`);
|
|
229
|
+
return respondWithError(400, "Malformed authentication token");
|
|
230
|
+
}
|
|
222
231
|
```
|
|
223
232
|
|
|
224
|
-
###
|
|
233
|
+
### Token Revocation
|
|
225
234
|
|
|
226
|
-
|
|
235
|
+
Implement secure session termination with token revocation:
|
|
227
236
|
|
|
228
237
|
```javascript
|
|
229
|
-
|
|
238
|
+
// Terminate user session by revoking the token
|
|
239
|
+
await accessToken.revoke();
|
|
240
|
+
logger.info(`Session terminated: Token ${accessToken.id} revoked`);
|
|
241
|
+
|
|
242
|
+
// Subsequent verification attempts will fail with EcwtRevokedError
|
|
243
|
+
try {
|
|
244
|
+
await ecwtFactory.verify(accessToken.token);
|
|
245
|
+
} catch (error) {
|
|
246
|
+
if (error instanceof EcwtRevokedError) {
|
|
247
|
+
// Expected behavior for revoked tokens
|
|
248
|
+
logger.debug("Token verification correctly rejected revoked token");
|
|
249
|
+
}
|
|
250
|
+
}
|
|
230
251
|
```
|
|
231
252
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
The string representation of the token.
|
|
235
|
-
|
|
236
|
-
#### Class property `readonly id: string`
|
|
237
|
-
|
|
238
|
-
The unique ID of the token.
|
|
239
|
-
|
|
240
|
-
#### Class property `readonly snowflake: Snowflake`
|
|
253
|
+
### Advanced: Token Size Optimization
|
|
241
254
|
|
|
242
|
-
|
|
255
|
+
To reduce token size, use SenML key mapping that replaces string object keys with numeric identifiers throughout your entire payload structure. This compression works at any nesting depth. When implementing, catalog all potential keys across your schema and assign consistent numeric values to each, as these mappings cannot be changed once tokens are in circulation.
|
|
243
256
|
|
|
244
|
-
|
|
257
|
+
> **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.
|
|
245
258
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
getTTL(): number | null
|
|
256
|
-
```
|
|
257
|
-
|
|
258
|
-
Returns current the time to live of the token in seconds. If the token does not expire, returns `null`.
|
|
259
|
+
```javascript
|
|
260
|
+
// Standard configuration without key mapping
|
|
261
|
+
const standardFactory = new EcwtFactory({
|
|
262
|
+
/* Core dependencies */
|
|
263
|
+
options: {
|
|
264
|
+
namespace: 'auth-service',
|
|
265
|
+
key: encryptionKey,
|
|
266
|
+
},
|
|
267
|
+
});
|
|
259
268
|
|
|
260
|
-
|
|
269
|
+
// Optimized configuration with key mapping
|
|
270
|
+
const optimizedFactory = new EcwtFactory({
|
|
271
|
+
/* Core dependencies */
|
|
272
|
+
options: {
|
|
273
|
+
namespace: 'auth-service',
|
|
274
|
+
key: encryptionKey,
|
|
275
|
+
senml_key_map: {
|
|
276
|
+
user_id: 1,
|
|
277
|
+
name: 2,
|
|
278
|
+
roles: 3,
|
|
279
|
+
permissions: 4,
|
|
280
|
+
metadata: 5,
|
|
281
|
+
last_login: 6,
|
|
282
|
+
},
|
|
283
|
+
},
|
|
284
|
+
});
|
|
261
285
|
|
|
262
|
-
|
|
263
|
-
|
|
286
|
+
// Measure token size difference
|
|
287
|
+
const payload = {
|
|
288
|
+
user_id: 12345,
|
|
289
|
+
name: "John Smith",
|
|
290
|
+
roles: ["admin", "editor"],
|
|
291
|
+
permissions: ["read", "write", "delete"],
|
|
292
|
+
metadata: { last_login: Date.now() },
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
const standardToken = await standardFactory.create(payload, { ttl: 3600 });
|
|
296
|
+
const optimizedToken = await optimizedFactory.create(payload, { ttl: 3600 });
|
|
297
|
+
|
|
298
|
+
console.log(`Standard token size: ${standardToken.token.length} bytes`);
|
|
299
|
+
console.log(`Optimized token size: ${optimizedToken.token.length} bytes`);
|
|
300
|
+
console.log(`Size reduction: ${(1 - optimizedToken.token.length / standardToken.token.length).toFixed(2) * 100}%`);
|
|
301
|
+
|
|
302
|
+
// Outputs:
|
|
303
|
+
// > Standard token size: 210 bytes
|
|
304
|
+
// > Optimized token size: 146 bytes
|
|
305
|
+
// > Size reduction: 30%
|
|
264
306
|
```
|
|
265
|
-
|
|
266
|
-
Revokes the token. Attempts to verify the revoked token will throw `EcwtRevokedError`.
|