@naturalcycles/nodejs-lib 15.119.0 → 15.121.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.
@@ -1,2 +1 @@
1
1
  export * from './jwt.service.js';
2
- export * from './jwt.service2.js';
package/dist/jwt/index.js CHANGED
@@ -1,2 +1 @@
1
1
  export * from './jwt.service.js';
2
- export * from './jwt.service2.js';
@@ -1,49 +1,166 @@
1
1
  import type { ErrorData } from '@naturalcycles/js-lib/error';
2
- import type { AnyObject, JWTString } from '@naturalcycles/js-lib/types';
3
- import type { Algorithm, JwtHeader, SignOptions, VerifyOptions } from 'jsonwebtoken';
4
- import jsonwebtoken from 'jsonwebtoken';
2
+ import { AppError } from '@naturalcycles/js-lib/error/error.util.js';
3
+ import type { AnyObject, JWTString, NumberOfSeconds, UnixTimestamp } from '@naturalcycles/js-lib/types';
5
4
  import type { AjvSchema, JSchema } from '../validation/ajv/jSchema.js';
6
- export { jsonwebtoken };
7
- export type { Algorithm, JwtHeader, SignOptions, VerifyOptions };
8
- export interface JWTServiceCfg {
5
+ /**
6
+ * Asymmetric JWS algorithms supported by JWTService.
7
+ */
8
+ export type JWTAlgorithm = 'ES256' | 'ES384' | 'ES512' | 'RS256' | 'RS384' | 'RS512' | 'PS256' | 'PS384' | 'PS512' | 'EdDSA';
9
+ export interface JWTServiceCfg<T extends AnyObject = AnyObject> {
9
10
  /**
10
11
  * Public key is required to Verify incoming tokens.
11
12
  * Optional if you only want to Decode or Sign.
13
+ *
14
+ * PEM string/Buffer. Both SPKI ("PUBLIC KEY") and a private key PEM
15
+ * (public key is derived from it) are accepted.
12
16
  */
13
17
  publicKey?: string | Buffer;
14
18
  /**
15
19
  * Private key is required to Sign (create) outgoing tokens.
16
20
  * Optional if you only want to Decode or Verify.
21
+ *
22
+ * PEM string/Buffer. Both PKCS8 ("PRIVATE KEY") and SEC1 ("EC PRIVATE KEY",
23
+ * as generated by `openssl ecparam`) are accepted.
17
24
  */
18
25
  privateKey?: string | Buffer;
19
26
  /**
20
27
  * Recommended: ES256
21
28
  * Keys (private/public) should be generated using proper settings
22
29
  * that fit the used Algorithm.
30
+ *
31
+ * Used for Sign, and (unless `verifyAlgorithms` is set) it's also
32
+ * the only algorithm accepted on Verify.
23
33
  */
24
- algorithm: Algorithm;
34
+ algorithm: JWTAlgorithm;
25
35
  /**
26
- * If provided - will be applied to every Sign operation.
36
+ * JWS algorithms accepted on Verify. Defaults to `[algorithm]`.
37
+ *
38
+ * Only needed when one verifier must accept keys of different types
39
+ * (e.g a per-kid key set mixing EC and RSA keys). Keep the list as narrow as possible.
40
+ *
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 JWTInvalidError.
43
+ * (All JWTAlgorithms are asymmetric, so the classic algorithm-confusion attacks
44
+ * don't apply regardless.)
45
+ */
46
+ verifyAlgorithms?: JWTAlgorithm[];
47
+ /**
48
+ * If provided - payloads are validated against it on every Sign/Verify/Decode.
49
+ * Can be overridden per-call via `opt.schema`.
50
+ *
51
+ * Note: on Verify/Decode the schema is applied to the raw JWT payload, which includes
52
+ * the standard claims (exp/nbf/iss/aud/sub/iat/jti) if they were set on Sign.
53
+ * Declare (or allow) them in the schema, otherwise a strict schema will
54
+ * strip them from the returned payload (default AjvSchema behavior: removeAdditional).
27
55
  */
28
- signOptions?: SignOptions;
56
+ schema?: JSchema<T, any> | AjvSchema<T>;
29
57
  /**
30
58
  * If provided - will be applied to every Sign operation.
59
+ * Absolute-timestamp options (expiresAt/notBefore/issuedAt) are excluded,
60
+ * as they would be fixed to the same moment for all tokens signed by this service.
61
+ */
62
+ signOptions?: Omit<JWTSignOptions, 'expiresAt' | 'notBefore' | 'issuedAt' | 'schema'>;
63
+ /**
64
+ * If provided - will be applied to every Verify operation.
31
65
  */
32
- verifyOptions?: VerifyOptions;
66
+ verifyOptions?: Omit<JWTVerifyOptions, 'schema' | 'publicKey'>;
33
67
  /**
34
- * If set - errors thrown from this service will be extended
68
+ * If set - JWTErrors thrown from this service will be extended
35
69
  * with this errorData (in err.data)
36
70
  */
37
71
  errorData?: ErrorData;
38
72
  }
73
+ export interface JWTSignOptions<T extends AnyObject = AnyObject> {
74
+ /**
75
+ * Sets the `exp` claim, as an absolute UnixTimestamp.
76
+ * Convenient to build with LocalTime, e.g:
77
+ * `localTime.now().plus(30, 'minute').unix`
78
+ *
79
+ * Required, to protect from accidentally issuing never-expiring tokens.
80
+ * Pass `null` to explicitly sign a token without expiration.
81
+ */
82
+ expiresAt: UnixTimestamp | null;
83
+ /**
84
+ * Sets the `nbf` (not before) claim, as an absolute UnixTimestamp.
85
+ */
86
+ notBefore?: UnixTimestamp;
87
+ issuer?: string;
88
+ audience?: string | string[];
89
+ subject?: string;
90
+ jwtid?: string;
91
+ /**
92
+ * Sets the `iat` claim, as an absolute UnixTimestamp (e.g `localTime.nowUnix()`).
93
+ * By default `iat` is NOT set (same as the legacy jsonwebtoken-based JWTService
94
+ * with its `noTimestamp: true` default).
95
+ */
96
+ issuedAt?: UnixTimestamp;
97
+ /**
98
+ * Sets the `kid` (key id) header, required by some APIs (e.g Apple App Store Connect).
99
+ */
100
+ kid?: string;
101
+ /**
102
+ * Overrides cfg.schema for this call.
103
+ */
104
+ schema?: JSchema<T, any> | AjvSchema<T>;
105
+ }
106
+ export interface JWTVerifyOptions<T extends AnyObject = AnyObject> {
107
+ audience?: string | string[];
108
+ issuer?: string | string[];
109
+ subject?: string;
110
+ /**
111
+ * Clock skew tolerance, in seconds.
112
+ */
113
+ clockTolerance?: NumberOfSeconds;
114
+ /**
115
+ * Maximum allowed age of the token (based on its `iat` claim), in seconds.
116
+ */
117
+ maxTokenAge?: NumberOfSeconds;
118
+ /**
119
+ * "Now" override, useful in tests.
120
+ */
121
+ now?: UnixTimestamp;
122
+ requiredClaims?: string[];
123
+ /**
124
+ * Overrides cfg.schema for this call.
125
+ */
126
+ schema?: JSchema<T, any> | AjvSchema<T>;
127
+ /**
128
+ * Overrides cfg.publicKey for this call,
129
+ * e.g when verifying tokens signed with different keys (kid-based).
130
+ */
131
+ publicKey?: string | Buffer;
132
+ }
133
+ export interface JWTDecodeOptions<T extends AnyObject = AnyObject> {
134
+ /**
135
+ * Overrides cfg.schema for this call.
136
+ */
137
+ schema?: JSchema<T, any> | AjvSchema<T>;
138
+ }
139
+ export interface JWTHeader {
140
+ alg: string;
141
+ typ?: string;
142
+ kid?: string;
143
+ [k: string]: unknown;
144
+ }
145
+ export interface JWTDecoded<T extends AnyObject> {
146
+ header: JWTHeader;
147
+ payload: T;
148
+ signature: string;
149
+ }
39
150
  /**
40
- * @deprecated use JWTService2 (jose-based) instead.
41
- * Tokens are wire-compatible in both directions, only the API differs
42
- * (async sign/verify, normalized JWTError).
43
- * JWTService2 will be renamed to JWTService when this class is dropped.
151
+ * Wraps the `jose` library, exposing an implementation-agnostic API:
152
+ * no jose types, options or errors leak out of this service.
153
+ * All errors are normalized into JWTError subclasses
154
+ * (JWTExpiredError / JWTNotYetValidError / JWTInvalidError), matchable with `instanceof`.
155
+ *
156
+ * Successor of the legacy jsonwebtoken-based JWTService. Tokens are wire-compatible
157
+ * in both directions, so the two services can be swapped freely for the same key pair.
158
+ * Difference is that Sign and Verify are async (jose is WebCrypto-based, which is Promise-only),
159
+ * while Decode remains sync (pure base64url/JSON parsing, no crypto).
44
160
  *
45
- * Wraps popular `jsonwebtoken` library.
46
161
  * You should create one instance of JWTService for each pair of private/public key.
162
+ * Providing cfg.schema types the service to its payload and validates it
163
+ * on every Sign/Verify/Decode; it can be overridden per-call via opt.schema.
47
164
  *
48
165
  * Generate key pair like this.
49
166
  * Please note that parameters should be different for different algorithms.
@@ -52,14 +169,115 @@ export interface JWTServiceCfg {
52
169
  * openssl ecparam -name prime256v1 -genkey -noout -out key.pem
53
170
  * openssl ec -in key.pem -pubout > key.pub.pem
54
171
  */
55
- export declare class JWTService {
56
- cfg: JWTServiceCfg;
57
- constructor(cfg: JWTServiceCfg);
58
- sign<T extends AnyObject>(payload: T, schema?: JSchema<T, any> | AjvSchema<T>, opt?: SignOptions): JWTString;
59
- verify<T extends AnyObject>(token: JWTString, schema?: JSchema<T, any> | AjvSchema<T>, opt?: VerifyOptions, publicKey?: string): T;
60
- decode<T extends AnyObject>(token: JWTString, schema?: JSchema<T, any> | AjvSchema<T>): {
61
- header: JwtHeader;
62
- payload: T;
63
- signature: string;
64
- };
172
+ export declare class JWTService<T extends AnyObject = AnyObject> {
173
+ cfg: JWTServiceCfg<T>;
174
+ private privateKey?;
175
+ private publicKey?;
176
+ constructor(cfg: JWTServiceCfg<T>);
177
+ sign<TT extends T = T>(payload: TT, opt: JWTSignOptions<TT>): Promise<JWTString>;
178
+ verify<TT extends T = T>(token: JWTString, opt?: JWTVerifyOptions<TT>): Promise<TT>;
179
+ /**
180
+ * Tries to Verify the token, returning an [error, payload] tuple instead of throwing:
181
+ * - [null, payload] - the token verified, payload can be trusted
182
+ * - [error, null] - verification failed
183
+ *
184
+ * Same contract as tryToVerifyOrDecode, but without the unverified-decode fallback:
185
+ * the payload is only returned when the token verified.
186
+ */
187
+ tryToVerify<TT extends T = T>(token: JWTString, opt?: JWTVerifyOptions<TT>): Promise<[err: null, payload: TT] | [err: Error, payload: null]>;
188
+ /**
189
+ * Tries to Verify the token, falling back to unverified Decode on failure,
190
+ * so the caller can "peek" into the token's content even when it doesn't verify.
191
+ *
192
+ * Returns an [error, payload] tuple - ALWAYS check the error first:
193
+ * - [null, payload] - the token verified, payload can be trusted
194
+ * - [error, payload] - verification failed, but the token could still be decoded.
195
+ * When the error is JWTExpiredError/JWTNotYetValidError, the signature was already
196
+ * verified and only the time-claim check failed, so the payload is trustworthy
197
+ * (just expired/not yet valid). For any other error the payload is completely
198
+ * UNVERIFIED - never trust it, only peek (e.g to log/report the claimed identity).
199
+ * - [error, null] - the token could not even be decoded
200
+ *
201
+ * The decode fallback is raw: opt.schema/cfg.schema are only applied on the Verify path.
202
+ */
203
+ tryToVerifyOrDecode<TT extends T = T>(token: JWTString, opt?: JWTVerifyOptions<TT>): Promise<[err: null, payload: TT] | [err: Error, payload: TT | null]>;
204
+ decode<TT extends T = T>(token: JWTString, opt?: JWTDecodeOptions<TT>): JWTDecoded<TT>;
205
+ /**
206
+ * Schema-validation errors on Verify/Decode are extended with cfg.errorData:
207
+ * a token with a non-conforming payload is as unauthorized as an invalid one.
208
+ * (On Sign the payload comes from own code, not from user input, so a validation
209
+ * error there indicates a programming error and is thrown as-is.)
210
+ */
211
+ private validate;
212
+ /**
213
+ * jose errors are normalized into JWTError subclasses (extended with cfg.errorData).
214
+ * Non-jose errors (e.g TypeError from passing a key that doesn't fit the algorithm)
215
+ * indicate a programming error and are passed through as-is.
216
+ */
217
+ private normalizeError;
218
+ }
219
+ /**
220
+ * Decodes a JWT without verifying its signature (no key needed) - never trust the result.
221
+ * Standalone version of JWTService.decode, for when there's no service instance at hand
222
+ * (no schema validation, no errorData extension).
223
+ *
224
+ * Throws JWTInvalidError if the token cannot be decoded.
225
+ */
226
+ export declare function jwtDecode<T extends AnyObject>(token: JWTString): JWTDecoded<T>;
227
+ export type JWTErrorCode = 'JWT_EXPIRED' | 'JWT_NOT_YET_VALID' | 'JWT_INVALID';
228
+ export interface JWTErrorData extends ErrorData {
229
+ code: JWTErrorCode;
230
+ }
231
+ /**
232
+ * Thrown by JWTService on any Verify/Decode failure, always as one of its subclasses:
233
+ * - JWTExpiredError - `exp` claim check failed
234
+ * - JWTNotYetValidError - `nbf` claim check failed
235
+ * - JWTInvalidError - anything else (malformed token, wrong signature, other claim mismatches)
236
+ *
237
+ * Match errors with `instanceof`, e.g `err instanceof JWTExpiredError`.
238
+ * `data.code` carries the same distinction ('JWT_EXPIRED' | 'JWT_NOT_YET_VALID' | 'JWT_INVALID'),
239
+ * for when the error crosses a serialization boundary (e.g ErrorObject over HTTP)
240
+ * where `instanceof` no longer works.
241
+ *
242
+ * The underlying jose error is deliberately NOT preserved in `cause`: everything useful
243
+ * from it is already carried (message is copied, the failed claim is the subclass,
244
+ * `payload` is exposed where trustworthy), and jose nests the raw JWT payload into its own
245
+ * `cause`, which would otherwise survive ErrorObject serialization and leak into logs.
246
+ */
247
+ export declare class JWTError extends AppError<JWTErrorData> {
248
+ constructor(message: string, data: JWTErrorData);
249
+ }
250
+ /**
251
+ * The token is well-formed and its signature is valid, but the `exp` claim check failed.
252
+ *
253
+ * `payload` carries the decoded token payload: signature is verified before claims are
254
+ * checked, so the payload is trustworthy - it's just expired.
255
+ * (Note: it's the raw JWT payload, incl. standard claims; cfg.schema is not applied to it.)
256
+ */
257
+ export declare class JWTExpiredError extends JWTError {
258
+ readonly payload: AnyObject;
259
+ constructor(message: string, data?: ErrorData, opt?: {
260
+ payload?: AnyObject;
261
+ });
262
+ }
263
+ /**
264
+ * The token is well-formed and its signature is valid, but the `nbf` claim check failed.
265
+ *
266
+ * `payload` carries the decoded token payload: signature is verified before claims are
267
+ * checked, so the payload is trustworthy - it's just not valid yet.
268
+ * (Note: it's the raw JWT payload, incl. standard claims; cfg.schema is not applied to it.)
269
+ */
270
+ export declare class JWTNotYetValidError extends JWTError {
271
+ readonly payload: AnyObject;
272
+ constructor(message: string, data?: ErrorData, opt?: {
273
+ payload?: AnyObject;
274
+ });
275
+ }
276
+ /**
277
+ * The token could not be trusted: malformed token, wrong signature,
278
+ * or a claim mismatch other than exp/nbf (e.g issuer/audience).
279
+ * No payload is exposed - nothing from such a token should be used.
280
+ */
281
+ export declare class JWTInvalidError extends JWTError {
282
+ constructor(message: string, data?: ErrorData);
65
283
  }
@@ -1,19 +1,21 @@
1
+ import { createPrivateKey, createPublicKey } from 'node:crypto';
1
2
  import { _assert } from '@naturalcycles/js-lib/error/assert.js';
2
- import { _errorDataAppend } from '@naturalcycles/js-lib/error/error.util.js';
3
- import jsonwebtoken from 'jsonwebtoken';
4
- export { jsonwebtoken };
5
- // todo: define JWTError and list possible options
6
- // jwt expired (TokenExpiredError)
7
- // jwt invalid
8
- // jwt token is empty
3
+ import { _errorDataAppend, AppError } from '@naturalcycles/js-lib/error/error.util.js';
4
+ import { decodeJwt, decodeProtectedHeader, errors, jwtVerify, SignJWT } from 'jose';
9
5
  /**
10
- * @deprecated use JWTService2 (jose-based) instead.
11
- * Tokens are wire-compatible in both directions, only the API differs
12
- * (async sign/verify, normalized JWTError).
13
- * JWTService2 will be renamed to JWTService when this class is dropped.
6
+ * Wraps the `jose` library, exposing an implementation-agnostic API:
7
+ * no jose types, options or errors leak out of this service.
8
+ * All errors are normalized into JWTError subclasses
9
+ * (JWTExpiredError / JWTNotYetValidError / JWTInvalidError), matchable with `instanceof`.
10
+ *
11
+ * Successor of the legacy jsonwebtoken-based JWTService. Tokens are wire-compatible
12
+ * in both directions, so the two services can be swapped freely for the same key pair.
13
+ * Difference is that Sign and Verify are async (jose is WebCrypto-based, which is Promise-only),
14
+ * while Decode remains sync (pure base64url/JSON parsing, no crypto).
14
15
  *
15
- * Wraps popular `jsonwebtoken` library.
16
16
  * You should create one instance of JWTService for each pair of private/public key.
17
+ * Providing cfg.schema types the service to its payload and validates it
18
+ * on every Sign/Verify/Decode; it can be overridden per-call via opt.schema.
17
19
  *
18
20
  * Generate key pair like this.
19
21
  * Please note that parameters should be different for different algorithms.
@@ -24,47 +26,251 @@ export { jsonwebtoken };
24
26
  */
25
27
  export class JWTService {
26
28
  cfg;
29
+ privateKey;
30
+ publicKey;
27
31
  constructor(cfg) {
28
32
  this.cfg = cfg;
33
+ // KeyObjects are parsed synchronously via node:crypto (also accepts SEC1 PEMs,
34
+ // which jose's own async importers reject), keeping the constructor sync.
35
+ if (cfg.privateKey)
36
+ this.privateKey = createPrivateKey(cfg.privateKey);
37
+ if (cfg.publicKey)
38
+ this.publicKey = createPublicKey(cfg.publicKey);
29
39
  }
30
- sign(payload, schema, opt = {}) {
31
- _assert(this.cfg.privateKey, 'JWTService: privateKey is required to be able to verify, but not provided');
32
- schema?.validate(payload);
33
- return jsonwebtoken.sign(payload, this.cfg.privateKey, {
34
- algorithm: this.cfg.algorithm,
35
- noTimestamp: true,
40
+ async sign(payload, opt) {
41
+ _assert(this.privateKey, 'JWTService: privateKey is required to be able to sign, but not provided', this.cfg.errorData);
42
+ const { expiresAt, notBefore, issuer, audience, subject, jwtid, issuedAt, kid, schema } = {
36
43
  ...this.cfg.signOptions,
37
44
  ...opt,
45
+ };
46
+ (schema || this.cfg.schema)?.validate(payload);
47
+ // `kid: undefined` is dropped by JSON serialization, keeping the header unchanged when not set
48
+ const jwt = new SignJWT(payload).setProtectedHeader({
49
+ alg: this.cfg.algorithm,
50
+ typ: 'JWT',
51
+ kid,
38
52
  });
53
+ if (expiresAt !== null)
54
+ jwt.setExpirationTime(expiresAt);
55
+ if (notBefore !== undefined)
56
+ jwt.setNotBefore(notBefore);
57
+ if (issuer)
58
+ jwt.setIssuer(issuer);
59
+ if (audience)
60
+ jwt.setAudience(audience);
61
+ if (subject)
62
+ jwt.setSubject(subject);
63
+ if (jwtid)
64
+ jwt.setJti(jwtid);
65
+ if (issuedAt !== undefined)
66
+ jwt.setIssuedAt(issuedAt);
67
+ return await jwt.sign(this.privateKey);
39
68
  }
40
- verify(token, schema, opt = {}, publicKey) {
41
- _assert(this.cfg.publicKey, 'JWTService: publicKey is required to be able to verify, but not provided');
69
+ async verify(token, opt = {}) {
70
+ const { now, schema, publicKey, ...joseOpt } = {
71
+ ...this.cfg.verifyOptions,
72
+ ...opt,
73
+ };
74
+ const key = publicKey ? createPublicKey(publicKey) : this.publicKey;
75
+ _assert(key, 'JWTService: publicKey is required to be able to verify, but not provided', this.cfg.errorData);
76
+ let data;
42
77
  try {
43
- const data = jsonwebtoken.verify(token, publicKey || this.cfg.publicKey, {
44
- algorithms: [this.cfg.algorithm],
45
- ...this.cfg.verifyOptions,
46
- ...opt,
78
+ const { payload } = await jwtVerify(token, key, {
79
+ algorithms: this.cfg.verifyAlgorithms || [this.cfg.algorithm],
80
+ ...joseOpt,
81
+ currentDate: now === undefined ? undefined : new Date(now * 1000),
47
82
  });
48
- schema?.validate(data);
49
- return data;
83
+ data = payload;
84
+ }
85
+ catch (err) {
86
+ throw this.normalizeError(err);
87
+ }
88
+ this.validate(data, schema);
89
+ return data;
90
+ }
91
+ /**
92
+ * Tries to Verify the token, returning an [error, payload] tuple instead of throwing:
93
+ * - [null, payload] - the token verified, payload can be trusted
94
+ * - [error, null] - verification failed
95
+ *
96
+ * Same contract as tryToVerifyOrDecode, but without the unverified-decode fallback:
97
+ * the payload is only returned when the token verified.
98
+ */
99
+ async tryToVerify(token, opt = {}) {
100
+ try {
101
+ return [null, await this.verify(token, opt)];
102
+ }
103
+ catch (err) {
104
+ return [err, null];
105
+ }
106
+ }
107
+ /**
108
+ * Tries to Verify the token, falling back to unverified Decode on failure,
109
+ * so the caller can "peek" into the token's content even when it doesn't verify.
110
+ *
111
+ * Returns an [error, payload] tuple - ALWAYS check the error first:
112
+ * - [null, payload] - the token verified, payload can be trusted
113
+ * - [error, payload] - verification failed, but the token could still be decoded.
114
+ * When the error is JWTExpiredError/JWTNotYetValidError, the signature was already
115
+ * verified and only the time-claim check failed, so the payload is trustworthy
116
+ * (just expired/not yet valid). For any other error the payload is completely
117
+ * UNVERIFIED - never trust it, only peek (e.g to log/report the claimed identity).
118
+ * - [error, null] - the token could not even be decoded
119
+ *
120
+ * The decode fallback is raw: opt.schema/cfg.schema are only applied on the Verify path.
121
+ */
122
+ async tryToVerifyOrDecode(token, opt = {}) {
123
+ const [verifyError, payload] = await this.tryToVerify(token, opt);
124
+ if (!verifyError)
125
+ return [null, payload];
126
+ // Expired/not-yet-valid errors already carry the (signature-verified) payload
127
+ if (verifyError instanceof JWTExpiredError || verifyError instanceof JWTNotYetValidError) {
128
+ return [verifyError, verifyError.payload];
129
+ }
130
+ try {
131
+ return [verifyError, jwtDecode(token).payload];
132
+ }
133
+ catch {
134
+ return [verifyError, null];
135
+ }
136
+ }
137
+ decode(token, opt = {}) {
138
+ let decoded;
139
+ try {
140
+ decoded = jwtDecode(token);
50
141
  }
51
142
  catch (err) {
52
143
  if (this.cfg.errorData) {
53
- _errorDataAppend(err, {
54
- ...this.cfg.errorData,
55
- });
144
+ _errorDataAppend(err, this.cfg.errorData);
56
145
  }
57
146
  throw err;
58
147
  }
148
+ this.validate(decoded.payload, opt.schema);
149
+ return decoded;
59
150
  }
60
- decode(token, schema) {
61
- const data = jsonwebtoken.decode(token, {
62
- complete: true,
63
- });
64
- _assert(data?.payload, 'invalid token, decoded value is empty', {
65
- ...this.cfg.errorData,
66
- });
67
- schema?.validate(data.payload);
68
- return data;
151
+ /**
152
+ * Schema-validation errors on Verify/Decode are extended with cfg.errorData:
153
+ * a token with a non-conforming payload is as unauthorized as an invalid one.
154
+ * (On Sign the payload comes from own code, not from user input, so a validation
155
+ * error there indicates a programming error and is thrown as-is.)
156
+ */
157
+ validate(payload, schema) {
158
+ try {
159
+ ;
160
+ (schema || this.cfg.schema)?.validate(payload);
161
+ }
162
+ catch (err) {
163
+ if (this.cfg.errorData) {
164
+ _errorDataAppend(err, this.cfg.errorData);
165
+ }
166
+ throw err;
167
+ }
168
+ }
169
+ /**
170
+ * jose errors are normalized into JWTError subclasses (extended with cfg.errorData).
171
+ * Non-jose errors (e.g TypeError from passing a key that doesn't fit the algorithm)
172
+ * indicate a programming error and are passed through as-is.
173
+ */
174
+ normalizeError(err) {
175
+ const { errorData } = this.cfg;
176
+ if (err instanceof errors.JWTExpired) {
177
+ return new JWTExpiredError(err.message, errorData, { payload: err.payload });
178
+ }
179
+ if (err instanceof errors.JWTClaimValidationFailed && err.claim === 'nbf') {
180
+ return new JWTNotYetValidError(err.message, errorData, { payload: err.payload });
181
+ }
182
+ if (err instanceof errors.JOSEError) {
183
+ return new JWTInvalidError(err.message, errorData);
184
+ }
185
+ if (this.cfg.verifyAlgorithms && err instanceof TypeError) {
186
+ // With multiple verifyAlgorithms, a token/key algorithm mismatch is reachable
187
+ // by untrusted input, and jose reports it as TypeError - treat it as an invalid token
188
+ return new JWTInvalidError(err.message, errorData);
189
+ }
190
+ return err;
191
+ }
192
+ }
193
+ /**
194
+ * Decodes a JWT without verifying its signature (no key needed) - never trust the result.
195
+ * Standalone version of JWTService.decode, for when there's no service instance at hand
196
+ * (no schema validation, no errorData extension).
197
+ *
198
+ * Throws JWTInvalidError if the token cannot be decoded.
199
+ */
200
+ export function jwtDecode(token) {
201
+ let header;
202
+ let payload;
203
+ try {
204
+ header = decodeProtectedHeader(token);
205
+ payload = decodeJwt(token);
206
+ }
207
+ catch (err) {
208
+ // The underlying message is folded in, as it carries the only specific detail
209
+ throw new JWTInvalidError(`invalid token, unable to decode: ${err.message}`);
210
+ }
211
+ return {
212
+ header,
213
+ payload,
214
+ signature: token.split('.')[2],
215
+ };
216
+ }
217
+ /**
218
+ * Thrown by JWTService on any Verify/Decode failure, always as one of its subclasses:
219
+ * - JWTExpiredError - `exp` claim check failed
220
+ * - JWTNotYetValidError - `nbf` claim check failed
221
+ * - JWTInvalidError - anything else (malformed token, wrong signature, other claim mismatches)
222
+ *
223
+ * Match errors with `instanceof`, e.g `err instanceof JWTExpiredError`.
224
+ * `data.code` carries the same distinction ('JWT_EXPIRED' | 'JWT_NOT_YET_VALID' | 'JWT_INVALID'),
225
+ * for when the error crosses a serialization boundary (e.g ErrorObject over HTTP)
226
+ * where `instanceof` no longer works.
227
+ *
228
+ * The underlying jose error is deliberately NOT preserved in `cause`: everything useful
229
+ * from it is already carried (message is copied, the failed claim is the subclass,
230
+ * `payload` is exposed where trustworthy), and jose nests the raw JWT payload into its own
231
+ * `cause`, which would otherwise survive ErrorObject serialization and leak into logs.
232
+ */
233
+ export class JWTError extends AppError {
234
+ constructor(message, data) {
235
+ // `new.target.name` makes subclasses report their own name
236
+ super(message, data, { name: new.target.name });
237
+ }
238
+ }
239
+ /**
240
+ * The token is well-formed and its signature is valid, but the `exp` claim check failed.
241
+ *
242
+ * `payload` carries the decoded token payload: signature is verified before claims are
243
+ * checked, so the payload is trustworthy - it's just expired.
244
+ * (Note: it's the raw JWT payload, incl. standard claims; cfg.schema is not applied to it.)
245
+ */
246
+ export class JWTExpiredError extends JWTError {
247
+ payload;
248
+ constructor(message, data = {}, opt = {}) {
249
+ super(message, { ...data, code: 'JWT_EXPIRED' });
250
+ this.payload = opt.payload || {};
251
+ }
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 class JWTNotYetValidError extends JWTError {
261
+ payload;
262
+ constructor(message, data = {}, opt = {}) {
263
+ super(message, { ...data, code: 'JWT_NOT_YET_VALID' });
264
+ this.payload = opt.payload || {};
265
+ }
266
+ }
267
+ /**
268
+ * The token could not be trusted: malformed token, wrong signature,
269
+ * or a claim mismatch other than exp/nbf (e.g issuer/audience).
270
+ * No payload is exposed - nothing from such a token should be used.
271
+ */
272
+ export class JWTInvalidError extends JWTError {
273
+ constructor(message, data = {}) {
274
+ super(message, { ...data, code: 'JWT_INVALID' });
69
275
  }
70
276
  }
@@ -271,7 +271,16 @@ export declare class JString<OUT extends string | undefined = string, Opt extend
271
271
  ipv4(): this;
272
272
  ipv6(): this;
273
273
  slug(): this;
274
+ /**
275
+ * Validates the 3-part semver format: `major.minor.patch`, e.g `1.2.3`.
276
+ */
274
277
  semVer(): this;
278
+ /**
279
+ * Validates the 4-part version format: `major.minor.patch.build`, e.g `1.2.3.4`.
280
+ *
281
+ * Not a semver by the spec, but is used by e.g .NET and some mobile app versions.
282
+ */
283
+ semVer4(): this;
275
284
  languageTag(): this;
276
285
  countryCode(): this;
277
286
  currency(): this;