ecwt 0.2.1-beta.5 → 0.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,10 +1,9 @@
1
-
2
1
  # ECWT
3
- Encrypted CBOR-encoded Web Token
4
2
 
5
- ## What is it?
3
+ [![npm version](https://img.shields.io/npm/v/ecwt.svg)](https://www.npmjs.com/package/ecwt)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
6
5
 
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:
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-js repository](https://github.com/kirick13/snowflake-js).
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
- server_id: 0,
40
- worker_id: 0,
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
- socket: {
51
- host: 'localhost',
52
- port: 6379,
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
- max: 1000, // maximum of 1000 items
66
- ttl: 60 * 60 * 1000, // 1 hour
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 schema = v.parse.bind(
80
- null,
81
- v.object({
82
- user_id: v.number([
83
- v.maxValue(10),
84
- ]),
85
- nick: v.string([
86
- v.maxLength(10),
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 schema will prevent creating tokens for users with ID greater than 10 and nicknames longer than 10 characters.
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
- ### `EcwtFactory`
96
+ ## Usage Examples
97
97
 
98
- ```typescript
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
- Create your `EcwtFactory` instance to create, validate and revoke tokens.
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
- const ecwtFactory = new EcwtFactory({
120
- redisClient,
121
- lruCache,
122
- snowflakeFactory,
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
- 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).
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
- #### Class method `create`
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
- ```typescript
144
- create(
145
- payload: {
146
- [key: string]: any,
147
- },
148
- options: {
149
- ttl: number | null,
150
- }
151
- ): Promise<Ecwt>
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
- Creates a token.
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
- > **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.
161
-
162
- Returns `Ecwt` instance.
147
+ Generate tokens with precise payload and expiration controls:
163
148
 
164
149
  ```javascript
165
- // Example
150
+ // Create an access token with a 30-minute expiration
166
151
  const ecwt = await ecwtFactory.create(
167
- {
168
- user_id: 1,
169
- nick: 'kirick',
170
- },
171
- {
172
- ttl: 30 * 60,
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
- #### Class method `verify`
162
+ // Get string representation of the token
163
+ const serializedToken = ecwt.token;
178
164
 
179
- ```typescript
180
- verify(
181
- token: string,
182
- ): Promise<Ecwt>
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
- Parses string representation of the token and verifies it:
186
-
187
- - to be decrypted properly,
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
- Returns `Ecwt` instance.
175
+ ### Token Verification
193
176
 
194
- If the token is invalid, throws `EcwtInvalidError` which contains `Ecwt` instance in the `ecwt` property.
177
+ Implement verification with appropriate error handling:
195
178
 
196
179
  ```javascript
197
- const ecwt = await ecwtFactory.verify(token);
198
- ```
199
-
200
- #### Class method `safeVerify`
201
-
202
- ```typescript
203
- safeVerify(
204
- token: string,
205
- ): Promise<{
206
- success: boolean,
207
- ecwt: Ecwt | null,
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
- The same method as `verify`, but does not throw an error if the token is invalid, expired or revoked.
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
- success,
220
- ecwt,
221
- } = await ecwtFactory.safeVerify(token);
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
- ### `Ecwt`
233
+ ### Token Revocation
225
234
 
226
- Represents the token. Its counstructor cannot be called by the user.
235
+ Implement secure session termination with token revocation:
227
236
 
228
237
  ```javascript
229
- import { Ecwt } from 'ecwt';
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
- #### Class property `readonly token: string`
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
- The `Snowflake` instance of the token. For documentation, see [snowflake-js repository](https://github.com/kirick13/snowflake-js).
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
- #### Class property `readonly ts_expired: number | null`
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
- The timestamp of the token expiration in seconds. Equals to `null` if the token does not expire.
247
-
248
- #### Class property `readonly data: { [key: string]: any }`
249
-
250
- The payload of the token.
251
-
252
- #### Class method `getTTL`
253
-
254
- ```typescript
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
- #### Class method `revoke`
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
- ```typescript
263
- revoke(): Promise<void>
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`.