@naturalcycles/nodejs-lib 15.116.3 → 15.118.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -39,7 +39,7 @@ export interface JWTService2Cfg<T extends AnyObject = AnyObject> {
39
39
  * (e.g a per-kid key set mixing EC and RSA keys). Keep the list as narrow as possible.
40
40
  *
41
41
  * The token's `alg` header chooses among these, but only within what the
42
- * verification key can serve: a token/key algorithm mismatch fails with JWT_INVALID.
42
+ * verification key can serve: a token/key algorithm mismatch fails with JWTInvalidError.
43
43
  * (All JWTAlgorithms are asymmetric, so the classic algorithm-confusion attacks
44
44
  * don't apply regardless.)
45
45
  */
@@ -149,7 +149,8 @@ export interface JWTDecoded<T extends AnyObject> {
149
149
  /**
150
150
  * Wraps the `jose` library, exposing an implementation-agnostic API:
151
151
  * no jose types, options or errors leak out of this service.
152
- * All errors are normalized into JWTError with a stable `data.code`.
152
+ * All errors are normalized into JWTError subclasses
153
+ * (JWTExpiredError / JWTNotYetValidError / JWTInvalidError), matchable with `instanceof`.
153
154
  *
154
155
  * Successor of JWTService (jsonwebtoken-based). Tokens are wire-compatible
155
156
  * in both directions, so the two services can be swapped freely for the same key pair.
@@ -174,6 +175,22 @@ export declare class JWTService2<T extends AnyObject = AnyObject> {
174
175
  constructor(cfg: JWTService2Cfg<T>);
175
176
  sign<TT extends T = T>(payload: TT, opt: JWTSignOptions<TT>): Promise<JWTString>;
176
177
  verify<TT extends T = T>(token: JWTString, opt?: JWTVerifyOptions<TT>): Promise<TT>;
178
+ /**
179
+ * Tries to Verify the token, falling back to unverified Decode on failure,
180
+ * so the caller can "peek" into the token's content even when it doesn't verify.
181
+ *
182
+ * Returns an [error, payload] tuple - ALWAYS check the error first:
183
+ * - [null, payload] - the token verified, payload can be trusted
184
+ * - [error, payload] - verification failed, but the token could still be decoded.
185
+ * When the error is JWTExpiredError/JWTNotYetValidError, the signature was already
186
+ * verified and only the time-claim check failed, so the payload is trustworthy
187
+ * (just expired/not yet valid). For any other error the payload is completely
188
+ * UNVERIFIED - never trust it, only peek (e.g to log/report the claimed identity).
189
+ * - [error, null] - the token could not even be decoded
190
+ *
191
+ * The decode fallback is raw: opt.schema/cfg.schema are only applied on the Verify path.
192
+ */
193
+ tryToVerifyOrDecode<TT extends T = T>(token: JWTString, opt?: JWTVerifyOptions<TT>): Promise<[err: null, payload: TT] | [err: Error, payload: TT | null]>;
177
194
  decode<TT extends T = T>(token: JWTString, opt?: JWTDecodeOptions<TT>): JWTDecoded<TT>;
178
195
  /**
179
196
  * Schema-validation errors on Verify/Decode are extended with cfg.errorData:
@@ -183,7 +200,7 @@ export declare class JWTService2<T extends AnyObject = AnyObject> {
183
200
  */
184
201
  private validate;
185
202
  /**
186
- * jose errors are normalized into JWTError (extended with cfg.errorData).
203
+ * jose errors are normalized into JWTError subclasses (extended with cfg.errorData).
187
204
  * Non-jose errors (e.g TypeError from passing a key that doesn't fit the algorithm)
188
205
  * indicate a programming error and are passed through as-is.
189
206
  */
@@ -194,7 +211,7 @@ export declare class JWTService2<T extends AnyObject = AnyObject> {
194
211
  * Standalone version of JWTService2.decode, for when there's no service instance at hand
195
212
  * (no schema validation, no errorData extension).
196
213
  *
197
- * Throws JWTError with code JWT_INVALID if the token cannot be decoded.
214
+ * Throws JWTInvalidError if the token cannot be decoded.
198
215
  */
199
216
  export declare function jwtDecode<T extends AnyObject>(token: JWTString): JWTDecoded<T>;
200
217
  export type JWTErrorCode = 'JWT_EXPIRED' | 'JWT_NOT_YET_VALID' | 'JWT_INVALID';
@@ -202,17 +219,55 @@ export interface JWTErrorData extends ErrorData {
202
219
  code: JWTErrorCode;
203
220
  }
204
221
  /**
205
- * Thrown by JWTService2 on any Verify/Decode failure.
222
+ * Thrown by JWTService2 on any Verify/Decode failure, always as one of its subclasses:
223
+ * - JWTExpiredError - `exp` claim check failed
224
+ * - JWTNotYetValidError - `nbf` claim check failed
225
+ * - JWTInvalidError - anything else (malformed token, wrong signature, other claim mismatches)
206
226
  *
207
- * `data.code` is stable and implementation-agnostic:
208
- * - JWT_EXPIRED - `exp` claim check failed
209
- * - JWT_NOT_YET_VALID - `nbf` claim check failed
210
- * - JWT_INVALID - anything else (malformed token, wrong signature, other claim mismatches)
227
+ * Match errors with `instanceof`, e.g `err instanceof JWTExpiredError`.
228
+ * `data.code` carries the same distinction ('JWT_EXPIRED' | 'JWT_NOT_YET_VALID' | 'JWT_INVALID'),
229
+ * for when the error crosses a serialization boundary (e.g ErrorObject over HTTP)
230
+ * where `instanceof` no longer works.
211
231
  *
212
- * The original underlying error is preserved in `cause`.
232
+ * The underlying jose error is deliberately NOT preserved in `cause`: everything useful
233
+ * from it is already carried (message is copied, the failed claim is the subclass,
234
+ * `payload` is exposed where trustworthy), and jose nests the raw JWT payload into its own
235
+ * `cause`, which would otherwise survive ErrorObject serialization and leak into logs.
213
236
  */
214
237
  export declare class JWTError extends AppError<JWTErrorData> {
215
- constructor(message: string, data: JWTErrorData, opt?: {
216
- cause?: any;
238
+ constructor(message: string, data: JWTErrorData);
239
+ }
240
+ /**
241
+ * The token is well-formed and its signature is valid, but the `exp` claim check failed.
242
+ *
243
+ * `payload` carries the decoded token payload: signature is verified before claims are
244
+ * checked, so the payload is trustworthy - it's just expired.
245
+ * (Note: it's the raw JWT payload, incl. standard claims; cfg.schema is not applied to it.)
246
+ */
247
+ export declare class JWTExpiredError extends JWTError {
248
+ readonly payload: AnyObject;
249
+ constructor(message: string, data?: ErrorData, opt?: {
250
+ payload?: AnyObject;
217
251
  });
218
252
  }
253
+ /**
254
+ * The token is well-formed and its signature is valid, but the `nbf` claim check failed.
255
+ *
256
+ * `payload` carries the decoded token payload: signature is verified before claims are
257
+ * checked, so the payload is trustworthy - it's just not valid yet.
258
+ * (Note: it's the raw JWT payload, incl. standard claims; cfg.schema is not applied to it.)
259
+ */
260
+ export declare class JWTNotYetValidError extends JWTError {
261
+ readonly payload: AnyObject;
262
+ constructor(message: string, data?: ErrorData, opt?: {
263
+ payload?: AnyObject;
264
+ });
265
+ }
266
+ /**
267
+ * The token could not be trusted: malformed token, wrong signature,
268
+ * or a claim mismatch other than exp/nbf (e.g issuer/audience).
269
+ * No payload is exposed - nothing from such a token should be used.
270
+ */
271
+ export declare class JWTInvalidError extends JWTError {
272
+ constructor(message: string, data?: ErrorData);
273
+ }
@@ -5,7 +5,8 @@ import { decodeJwt, decodeProtectedHeader, errors, jwtVerify, SignJWT } from 'jo
5
5
  /**
6
6
  * Wraps the `jose` library, exposing an implementation-agnostic API:
7
7
  * no jose types, options or errors leak out of this service.
8
- * All errors are normalized into JWTError with a stable `data.code`.
8
+ * All errors are normalized into JWTError subclasses
9
+ * (JWTExpiredError / JWTNotYetValidError / JWTInvalidError), matchable with `instanceof`.
9
10
  *
10
11
  * Successor of JWTService (jsonwebtoken-based). Tokens are wire-compatible
11
12
  * in both directions, so the two services can be swapped freely for the same key pair.
@@ -87,6 +88,40 @@ export class JWTService2 {
87
88
  this.validate(data, schema);
88
89
  return data;
89
90
  }
91
+ /**
92
+ * Tries to Verify the token, falling back to unverified Decode on failure,
93
+ * so the caller can "peek" into the token's content even when it doesn't verify.
94
+ *
95
+ * Returns an [error, payload] tuple - ALWAYS check the error first:
96
+ * - [null, payload] - the token verified, payload can be trusted
97
+ * - [error, payload] - verification failed, but the token could still be decoded.
98
+ * When the error is JWTExpiredError/JWTNotYetValidError, the signature was already
99
+ * verified and only the time-claim check failed, so the payload is trustworthy
100
+ * (just expired/not yet valid). For any other error the payload is completely
101
+ * UNVERIFIED - never trust it, only peek (e.g to log/report the claimed identity).
102
+ * - [error, null] - the token could not even be decoded
103
+ *
104
+ * The decode fallback is raw: opt.schema/cfg.schema are only applied on the Verify path.
105
+ */
106
+ async tryToVerifyOrDecode(token, opt = {}) {
107
+ let verifyError;
108
+ try {
109
+ return [null, await this.verify(token, opt)];
110
+ }
111
+ catch (err) {
112
+ verifyError = err;
113
+ }
114
+ // Expired/not-yet-valid errors already carry the (signature-verified) payload
115
+ if (verifyError instanceof JWTExpiredError || verifyError instanceof JWTNotYetValidError) {
116
+ return [verifyError, verifyError.payload];
117
+ }
118
+ try {
119
+ return [verifyError, jwtDecode(token).payload];
120
+ }
121
+ catch {
122
+ return [verifyError, null];
123
+ }
124
+ }
90
125
  decode(token, opt = {}) {
91
126
  let decoded;
92
127
  try {
@@ -120,33 +155,27 @@ export class JWTService2 {
120
155
  }
121
156
  }
122
157
  /**
123
- * jose errors are normalized into JWTError (extended with cfg.errorData).
158
+ * jose errors are normalized into JWTError subclasses (extended with cfg.errorData).
124
159
  * Non-jose errors (e.g TypeError from passing a key that doesn't fit the algorithm)
125
160
  * indicate a programming error and are passed through as-is.
126
161
  */
127
162
  normalizeError(err) {
128
- let code;
163
+ const { errorData } = this.cfg;
129
164
  if (err instanceof errors.JWTExpired) {
130
- code = 'JWT_EXPIRED';
165
+ return new JWTExpiredError(err.message, errorData, { payload: err.payload });
131
166
  }
132
- else if (err instanceof errors.JWTClaimValidationFailed && err.claim === 'nbf') {
133
- code = 'JWT_NOT_YET_VALID';
167
+ if (err instanceof errors.JWTClaimValidationFailed && err.claim === 'nbf') {
168
+ return new JWTNotYetValidError(err.message, errorData, { payload: err.payload });
134
169
  }
135
- else if (err instanceof errors.JOSEError) {
136
- code = 'JWT_INVALID';
170
+ if (err instanceof errors.JOSEError) {
171
+ return new JWTInvalidError(err.message, errorData);
137
172
  }
138
- else if (this.cfg.verifyAlgorithms && err instanceof TypeError) {
173
+ if (this.cfg.verifyAlgorithms && err instanceof TypeError) {
139
174
  // With multiple verifyAlgorithms, a token/key algorithm mismatch is reachable
140
175
  // by untrusted input, and jose reports it as TypeError - treat it as an invalid token
141
- code = 'JWT_INVALID';
142
- }
143
- else {
144
- return err;
176
+ return new JWTInvalidError(err.message, errorData);
145
177
  }
146
- return new JWTError(err.message, {
147
- ...this.cfg.errorData,
148
- code,
149
- }, { cause: err });
178
+ return err;
150
179
  }
151
180
  }
152
181
  /**
@@ -154,7 +183,7 @@ export class JWTService2 {
154
183
  * Standalone version of JWTService2.decode, for when there's no service instance at hand
155
184
  * (no schema validation, no errorData extension).
156
185
  *
157
- * Throws JWTError with code JWT_INVALID if the token cannot be decoded.
186
+ * Throws JWTInvalidError if the token cannot be decoded.
158
187
  */
159
188
  export function jwtDecode(token) {
160
189
  let header;
@@ -164,7 +193,8 @@ export function jwtDecode(token) {
164
193
  payload = decodeJwt(token);
165
194
  }
166
195
  catch (err) {
167
- throw new JWTError('invalid token, unable to decode', { code: 'JWT_INVALID' }, { cause: err });
196
+ // The underlying message is folded in, as it carries the only specific detail
197
+ throw new JWTInvalidError(`invalid token, unable to decode: ${err.message}`);
168
198
  }
169
199
  return {
170
200
  header,
@@ -173,17 +203,62 @@ export function jwtDecode(token) {
173
203
  };
174
204
  }
175
205
  /**
176
- * Thrown by JWTService2 on any Verify/Decode failure.
206
+ * Thrown by JWTService2 on any Verify/Decode failure, always as one of its subclasses:
207
+ * - JWTExpiredError - `exp` claim check failed
208
+ * - JWTNotYetValidError - `nbf` claim check failed
209
+ * - JWTInvalidError - anything else (malformed token, wrong signature, other claim mismatches)
177
210
  *
178
- * `data.code` is stable and implementation-agnostic:
179
- * - JWT_EXPIRED - `exp` claim check failed
180
- * - JWT_NOT_YET_VALID - `nbf` claim check failed
181
- * - JWT_INVALID - anything else (malformed token, wrong signature, other claim mismatches)
211
+ * Match errors with `instanceof`, e.g `err instanceof JWTExpiredError`.
212
+ * `data.code` carries the same distinction ('JWT_EXPIRED' | 'JWT_NOT_YET_VALID' | 'JWT_INVALID'),
213
+ * for when the error crosses a serialization boundary (e.g ErrorObject over HTTP)
214
+ * where `instanceof` no longer works.
182
215
  *
183
- * The original underlying error is preserved in `cause`.
216
+ * The underlying jose error is deliberately NOT preserved in `cause`: everything useful
217
+ * from it is already carried (message is copied, the failed claim is the subclass,
218
+ * `payload` is exposed where trustworthy), and jose nests the raw JWT payload into its own
219
+ * `cause`, which would otherwise survive ErrorObject serialization and leak into logs.
184
220
  */
185
221
  export class JWTError extends AppError {
186
- constructor(message, data, opt) {
187
- super(message, data, { ...opt, name: 'JWTError' });
222
+ constructor(message, data) {
223
+ // `new.target.name` makes subclasses report their own name
224
+ super(message, data, { name: new.target.name });
225
+ }
226
+ }
227
+ /**
228
+ * The token is well-formed and its signature is valid, but the `exp` claim check failed.
229
+ *
230
+ * `payload` carries the decoded token payload: signature is verified before claims are
231
+ * checked, so the payload is trustworthy - it's just expired.
232
+ * (Note: it's the raw JWT payload, incl. standard claims; cfg.schema is not applied to it.)
233
+ */
234
+ export class JWTExpiredError extends JWTError {
235
+ payload;
236
+ constructor(message, data = {}, opt = {}) {
237
+ super(message, { ...data, code: 'JWT_EXPIRED' });
238
+ this.payload = opt.payload || {};
239
+ }
240
+ }
241
+ /**
242
+ * The token is well-formed and its signature is valid, but the `nbf` claim check failed.
243
+ *
244
+ * `payload` carries the decoded token payload: signature is verified before claims are
245
+ * checked, so the payload is trustworthy - it's just not valid yet.
246
+ * (Note: it's the raw JWT payload, incl. standard claims; cfg.schema is not applied to it.)
247
+ */
248
+ export class JWTNotYetValidError extends JWTError {
249
+ payload;
250
+ constructor(message, data = {}, opt = {}) {
251
+ super(message, { ...data, code: 'JWT_NOT_YET_VALID' });
252
+ this.payload = opt.payload || {};
253
+ }
254
+ }
255
+ /**
256
+ * The token could not be trusted: malformed token, wrong signature,
257
+ * or a claim mismatch other than exp/nbf (e.g issuer/audience).
258
+ * No payload is exposed - nothing from such a token should be used.
259
+ */
260
+ export class JWTInvalidError extends JWTError {
261
+ constructor(message, data = {}) {
262
+ super(message, { ...data, code: 'JWT_INVALID' });
188
263
  }
189
264
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@naturalcycles/nodejs-lib",
3
3
  "type": "module",
4
- "version": "15.116.3",
4
+ "version": "15.118.0",
5
5
  "dependencies": {
6
6
  "@naturalcycles/js-lib": "^15",
7
7
  "@standard-schema/spec": "^1",
@@ -62,7 +62,7 @@ export interface JWTService2Cfg<T extends AnyObject = AnyObject> {
62
62
  * (e.g a per-kid key set mixing EC and RSA keys). Keep the list as narrow as possible.
63
63
  *
64
64
  * The token's `alg` header chooses among these, but only within what the
65
- * verification key can serve: a token/key algorithm mismatch fails with JWT_INVALID.
65
+ * verification key can serve: a token/key algorithm mismatch fails with JWTInvalidError.
66
66
  * (All JWTAlgorithms are asymmetric, so the classic algorithm-confusion attacks
67
67
  * don't apply regardless.)
68
68
  */
@@ -182,7 +182,8 @@ export interface JWTDecoded<T extends AnyObject> {
182
182
  /**
183
183
  * Wraps the `jose` library, exposing an implementation-agnostic API:
184
184
  * no jose types, options or errors leak out of this service.
185
- * All errors are normalized into JWTError with a stable `data.code`.
185
+ * All errors are normalized into JWTError subclasses
186
+ * (JWTExpiredError / JWTNotYetValidError / JWTInvalidError), matchable with `instanceof`.
186
187
  *
187
188
  * Successor of JWTService (jsonwebtoken-based). Tokens are wire-compatible
188
189
  * in both directions, so the two services can be swapped freely for the same key pair.
@@ -273,6 +274,45 @@ export class JWTService2<T extends AnyObject = AnyObject> {
273
274
  return data
274
275
  }
275
276
 
277
+ /**
278
+ * Tries to Verify the token, falling back to unverified Decode on failure,
279
+ * so the caller can "peek" into the token's content even when it doesn't verify.
280
+ *
281
+ * Returns an [error, payload] tuple - ALWAYS check the error first:
282
+ * - [null, payload] - the token verified, payload can be trusted
283
+ * - [error, payload] - verification failed, but the token could still be decoded.
284
+ * When the error is JWTExpiredError/JWTNotYetValidError, the signature was already
285
+ * verified and only the time-claim check failed, so the payload is trustworthy
286
+ * (just expired/not yet valid). For any other error the payload is completely
287
+ * UNVERIFIED - never trust it, only peek (e.g to log/report the claimed identity).
288
+ * - [error, null] - the token could not even be decoded
289
+ *
290
+ * The decode fallback is raw: opt.schema/cfg.schema are only applied on the Verify path.
291
+ */
292
+ async tryToVerifyOrDecode<TT extends T = T>(
293
+ token: JWTString,
294
+ opt: JWTVerifyOptions<TT> = {},
295
+ ): Promise<[err: null, payload: TT] | [err: Error, payload: TT | null]> {
296
+ let verifyError: Error
297
+
298
+ try {
299
+ return [null, await this.verify<TT>(token, opt)]
300
+ } catch (err) {
301
+ verifyError = err as Error
302
+ }
303
+
304
+ // Expired/not-yet-valid errors already carry the (signature-verified) payload
305
+ if (verifyError instanceof JWTExpiredError || verifyError instanceof JWTNotYetValidError) {
306
+ return [verifyError, verifyError.payload as TT]
307
+ }
308
+
309
+ try {
310
+ return [verifyError, jwtDecode<TT>(token).payload]
311
+ } catch {
312
+ return [verifyError, null]
313
+ }
314
+ }
315
+
276
316
  decode<TT extends T = T>(token: JWTString, opt: JWTDecodeOptions<TT> = {}): JWTDecoded<TT> {
277
317
  let decoded: JWTDecoded<TT>
278
318
 
@@ -308,35 +348,28 @@ export class JWTService2<T extends AnyObject = AnyObject> {
308
348
  }
309
349
 
310
350
  /**
311
- * jose errors are normalized into JWTError (extended with cfg.errorData).
351
+ * jose errors are normalized into JWTError subclasses (extended with cfg.errorData).
312
352
  * Non-jose errors (e.g TypeError from passing a key that doesn't fit the algorithm)
313
353
  * indicate a programming error and are passed through as-is.
314
354
  */
315
355
  private normalizeError(err: unknown): Error {
316
- let code: JWTErrorCode
356
+ const { errorData } = this.cfg
317
357
 
318
358
  if (err instanceof errors.JWTExpired) {
319
- code = 'JWT_EXPIRED'
320
- } else if (err instanceof errors.JWTClaimValidationFailed && err.claim === 'nbf') {
321
- code = 'JWT_NOT_YET_VALID'
322
- } else if (err instanceof errors.JOSEError) {
323
- code = 'JWT_INVALID'
324
- } else if (this.cfg.verifyAlgorithms && err instanceof TypeError) {
359
+ return new JWTExpiredError(err.message, errorData, { payload: err.payload })
360
+ }
361
+ if (err instanceof errors.JWTClaimValidationFailed && err.claim === 'nbf') {
362
+ return new JWTNotYetValidError(err.message, errorData, { payload: err.payload })
363
+ }
364
+ if (err instanceof errors.JOSEError) {
365
+ return new JWTInvalidError(err.message, errorData)
366
+ }
367
+ if (this.cfg.verifyAlgorithms && err instanceof TypeError) {
325
368
  // With multiple verifyAlgorithms, a token/key algorithm mismatch is reachable
326
369
  // by untrusted input, and jose reports it as TypeError - treat it as an invalid token
327
- code = 'JWT_INVALID'
328
- } else {
329
- return err as Error
370
+ return new JWTInvalidError(err.message, errorData)
330
371
  }
331
-
332
- return new JWTError(
333
- (err as Error).message,
334
- {
335
- ...this.cfg.errorData,
336
- code,
337
- },
338
- { cause: err },
339
- )
372
+ return err as Error
340
373
  }
341
374
  }
342
375
 
@@ -345,7 +378,7 @@ export class JWTService2<T extends AnyObject = AnyObject> {
345
378
  * Standalone version of JWTService2.decode, for when there's no service instance at hand
346
379
  * (no schema validation, no errorData extension).
347
380
  *
348
- * Throws JWTError with code JWT_INVALID if the token cannot be decoded.
381
+ * Throws JWTInvalidError if the token cannot be decoded.
349
382
  */
350
383
  export function jwtDecode<T extends AnyObject>(token: JWTString): JWTDecoded<T> {
351
384
  let header: JWTHeader
@@ -353,9 +386,10 @@ export function jwtDecode<T extends AnyObject>(token: JWTString): JWTDecoded<T>
353
386
 
354
387
  try {
355
388
  header = decodeProtectedHeader(token) as JWTHeader
356
- payload = decodeJwt(token) as T
389
+ payload = decodeJwt(token)
357
390
  } catch (err) {
358
- throw new JWTError('invalid token, unable to decode', { code: 'JWT_INVALID' }, { cause: err })
391
+ // The underlying message is folded in, as it carries the only specific detail
392
+ throw new JWTInvalidError(`invalid token, unable to decode: ${(err as Error).message}`)
359
393
  }
360
394
 
361
395
  return {
@@ -372,17 +406,67 @@ export interface JWTErrorData extends ErrorData {
372
406
  }
373
407
 
374
408
  /**
375
- * Thrown by JWTService2 on any Verify/Decode failure.
409
+ * Thrown by JWTService2 on any Verify/Decode failure, always as one of its subclasses:
410
+ * - JWTExpiredError - `exp` claim check failed
411
+ * - JWTNotYetValidError - `nbf` claim check failed
412
+ * - JWTInvalidError - anything else (malformed token, wrong signature, other claim mismatches)
376
413
  *
377
- * `data.code` is stable and implementation-agnostic:
378
- * - JWT_EXPIRED - `exp` claim check failed
379
- * - JWT_NOT_YET_VALID - `nbf` claim check failed
380
- * - JWT_INVALID - anything else (malformed token, wrong signature, other claim mismatches)
414
+ * Match errors with `instanceof`, e.g `err instanceof JWTExpiredError`.
415
+ * `data.code` carries the same distinction ('JWT_EXPIRED' | 'JWT_NOT_YET_VALID' | 'JWT_INVALID'),
416
+ * for when the error crosses a serialization boundary (e.g ErrorObject over HTTP)
417
+ * where `instanceof` no longer works.
381
418
  *
382
- * The original underlying error is preserved in `cause`.
419
+ * The underlying jose error is deliberately NOT preserved in `cause`: everything useful
420
+ * from it is already carried (message is copied, the failed claim is the subclass,
421
+ * `payload` is exposed where trustworthy), and jose nests the raw JWT payload into its own
422
+ * `cause`, which would otherwise survive ErrorObject serialization and leak into logs.
383
423
  */
384
424
  export class JWTError extends AppError<JWTErrorData> {
385
- constructor(message: string, data: JWTErrorData, opt?: { cause?: any }) {
386
- super(message, data, { ...opt, name: 'JWTError' })
425
+ constructor(message: string, data: JWTErrorData) {
426
+ // `new.target.name` makes subclasses report their own name
427
+ super(message, data, { name: new.target.name })
428
+ }
429
+ }
430
+
431
+ /**
432
+ * The token is well-formed and its signature is valid, but the `exp` claim check failed.
433
+ *
434
+ * `payload` carries the decoded token payload: signature is verified before claims are
435
+ * checked, so the payload is trustworthy - it's just expired.
436
+ * (Note: it's the raw JWT payload, incl. standard claims; cfg.schema is not applied to it.)
437
+ */
438
+ export class JWTExpiredError extends JWTError {
439
+ readonly payload: AnyObject
440
+
441
+ constructor(message: string, data: ErrorData = {}, opt: { payload?: AnyObject } = {}) {
442
+ super(message, { ...data, code: 'JWT_EXPIRED' })
443
+ this.payload = opt.payload || {}
444
+ }
445
+ }
446
+
447
+ /**
448
+ * The token is well-formed and its signature is valid, but the `nbf` claim check failed.
449
+ *
450
+ * `payload` carries the decoded token payload: signature is verified before claims are
451
+ * checked, so the payload is trustworthy - it's just not valid yet.
452
+ * (Note: it's the raw JWT payload, incl. standard claims; cfg.schema is not applied to it.)
453
+ */
454
+ export class JWTNotYetValidError extends JWTError {
455
+ readonly payload: AnyObject
456
+
457
+ constructor(message: string, data: ErrorData = {}, opt: { payload?: AnyObject } = {}) {
458
+ super(message, { ...data, code: 'JWT_NOT_YET_VALID' })
459
+ this.payload = opt.payload || {}
460
+ }
461
+ }
462
+
463
+ /**
464
+ * The token could not be trusted: malformed token, wrong signature,
465
+ * or a claim mismatch other than exp/nbf (e.g issuer/audience).
466
+ * No payload is exposed - nothing from such a token should be used.
467
+ */
468
+ export class JWTInvalidError extends JWTError {
469
+ constructor(message: string, data: ErrorData = {}) {
470
+ super(message, { ...data, code: 'JWT_INVALID' })
387
471
  }
388
472
  }