@naturalcycles/nodejs-lib 15.116.0 → 15.116.2

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.
@@ -78,6 +78,10 @@ export interface JWTSignOptions<T extends AnyObject = AnyObject> {
78
78
  * By default `iat` is NOT set (same as legacy JWTService with its `noTimestamp: true` default).
79
79
  */
80
80
  issuedAt?: UnixTimestamp;
81
+ /**
82
+ * Sets the `kid` (key id) header, required by some APIs (e.g Apple App Store Connect).
83
+ */
84
+ kid?: string;
81
85
  /**
82
86
  * Overrides cfg.schema for this call.
83
87
  */
@@ -156,6 +160,13 @@ export declare class JWTService2<T extends AnyObject = AnyObject> {
156
160
  sign<TT extends T = T>(payload: TT, opt: JWTSignOptions<TT>): Promise<JWTString>;
157
161
  verify<TT extends T = T>(token: JWTString, opt?: JWTVerifyOptions<TT>): Promise<TT>;
158
162
  decode<TT extends T = T>(token: JWTString, opt?: JWTDecodeOptions<TT>): JWTDecoded<TT>;
163
+ /**
164
+ * Schema-validation errors on Verify/Decode are extended with cfg.errorData:
165
+ * a token with a non-conforming payload is as unauthorized as an invalid one.
166
+ * (On Sign the payload comes from own code, not from user input, so a validation
167
+ * error there indicates a programming error and is thrown as-is.)
168
+ */
169
+ private validate;
159
170
  /**
160
171
  * jose errors are normalized into JWTError (extended with cfg.errorData).
161
172
  * Non-jose errors (e.g TypeError from passing a key that doesn't fit the algorithm)
@@ -163,6 +174,14 @@ export declare class JWTService2<T extends AnyObject = AnyObject> {
163
174
  */
164
175
  private normalizeError;
165
176
  }
177
+ /**
178
+ * Decodes a JWT without verifying its signature (no key needed) - never trust the result.
179
+ * Standalone version of JWTService2.decode, for when there's no service instance at hand
180
+ * (no schema validation, no errorData extension).
181
+ *
182
+ * Throws JWTError with code JWT_INVALID if the token cannot be decoded.
183
+ */
184
+ export declare function jwtDecode<T extends AnyObject>(token: JWTString): JWTDecoded<T>;
166
185
  export type JWTErrorCode = 'JWT_EXPIRED' | 'JWT_NOT_YET_VALID' | 'JWT_INVALID';
167
186
  export interface JWTErrorData extends ErrorData {
168
187
  code: JWTErrorCode;
@@ -1,6 +1,6 @@
1
1
  import { createPrivateKey, createPublicKey } from 'node:crypto';
2
2
  import { _assert } from '@naturalcycles/js-lib/error/assert.js';
3
- import { AppError } from '@naturalcycles/js-lib/error/error.util.js';
3
+ import { _errorDataAppend, AppError } from '@naturalcycles/js-lib/error/error.util.js';
4
4
  import { decodeJwt, decodeProtectedHeader, errors, jwtVerify, SignJWT } from 'jose';
5
5
  /**
6
6
  * Wraps the `jose` library, exposing an implementation-agnostic API:
@@ -38,12 +38,17 @@ export class JWTService2 {
38
38
  }
39
39
  async sign(payload, opt) {
40
40
  _assert(this.privateKey, 'JWTService2: privateKey is required to be able to sign, but not provided', this.cfg.errorData);
41
- const { expiresAt, notBefore, issuer, audience, subject, jwtid, issuedAt, schema } = {
41
+ const { expiresAt, notBefore, issuer, audience, subject, jwtid, issuedAt, kid, schema } = {
42
42
  ...this.cfg.signOptions,
43
43
  ...opt,
44
44
  };
45
45
  (schema || this.cfg.schema)?.validate(payload);
46
- const jwt = new SignJWT(payload).setProtectedHeader({ alg: this.cfg.algorithm, typ: 'JWT' });
46
+ // `kid: undefined` is dropped by JSON serialization, keeping the header unchanged when not set
47
+ const jwt = new SignJWT(payload).setProtectedHeader({
48
+ alg: this.cfg.algorithm,
49
+ typ: 'JWT',
50
+ kid,
51
+ });
47
52
  if (expiresAt !== null)
48
53
  jwt.setExpirationTime(expiresAt);
49
54
  if (notBefore !== undefined)
@@ -79,30 +84,40 @@ export class JWTService2 {
79
84
  catch (err) {
80
85
  throw this.normalizeError(err);
81
86
  }
82
- ;
83
- (schema || this.cfg.schema)?.validate(data);
87
+ this.validate(data, schema);
84
88
  return data;
85
89
  }
86
90
  decode(token, opt = {}) {
87
- let header;
88
- let payload;
91
+ let decoded;
89
92
  try {
90
- header = decodeProtectedHeader(token);
91
- payload = decodeJwt(token);
93
+ decoded = jwtDecode(token);
92
94
  }
93
95
  catch (err) {
94
- throw new JWTError('invalid token, unable to decode', {
95
- ...this.cfg.errorData,
96
- code: 'JWT_INVALID',
97
- }, { cause: err });
96
+ if (this.cfg.errorData) {
97
+ _errorDataAppend(err, this.cfg.errorData);
98
+ }
99
+ throw err;
100
+ }
101
+ this.validate(decoded.payload, opt.schema);
102
+ return decoded;
103
+ }
104
+ /**
105
+ * Schema-validation errors on Verify/Decode are extended with cfg.errorData:
106
+ * a token with a non-conforming payload is as unauthorized as an invalid one.
107
+ * (On Sign the payload comes from own code, not from user input, so a validation
108
+ * error there indicates a programming error and is thrown as-is.)
109
+ */
110
+ validate(payload, schema) {
111
+ try {
112
+ ;
113
+ (schema || this.cfg.schema)?.validate(payload);
114
+ }
115
+ catch (err) {
116
+ if (this.cfg.errorData) {
117
+ _errorDataAppend(err, this.cfg.errorData);
118
+ }
119
+ throw err;
98
120
  }
99
- ;
100
- (opt.schema || this.cfg.schema)?.validate(payload);
101
- return {
102
- header,
103
- payload,
104
- signature: token.split('.')[2],
105
- };
106
121
  }
107
122
  /**
108
123
  * jose errors are normalized into JWTError (extended with cfg.errorData).
@@ -129,6 +144,29 @@ export class JWTService2 {
129
144
  }, { cause: err });
130
145
  }
131
146
  }
147
+ /**
148
+ * Decodes a JWT without verifying its signature (no key needed) - never trust the result.
149
+ * Standalone version of JWTService2.decode, for when there's no service instance at hand
150
+ * (no schema validation, no errorData extension).
151
+ *
152
+ * Throws JWTError with code JWT_INVALID if the token cannot be decoded.
153
+ */
154
+ export function jwtDecode(token) {
155
+ let header;
156
+ let payload;
157
+ try {
158
+ header = decodeProtectedHeader(token);
159
+ payload = decodeJwt(token);
160
+ }
161
+ catch (err) {
162
+ throw new JWTError('invalid token, unable to decode', { code: 'JWT_INVALID' }, { cause: err });
163
+ }
164
+ return {
165
+ header,
166
+ payload,
167
+ signature: token.split('.')[2],
168
+ };
169
+ }
132
170
  /**
133
171
  * Thrown by JWTService2 on any Verify/Decode failure.
134
172
  *
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@naturalcycles/nodejs-lib",
3
3
  "type": "module",
4
- "version": "15.116.0",
4
+ "version": "15.116.2",
5
5
  "dependencies": {
6
6
  "@naturalcycles/js-lib": "^15",
7
7
  "@standard-schema/spec": "^1",
@@ -2,7 +2,7 @@ import { createPrivateKey, createPublicKey } from 'node:crypto'
2
2
  import type { KeyObject } from 'node:crypto'
3
3
  import type { ErrorData } from '@naturalcycles/js-lib/error'
4
4
  import { _assert } from '@naturalcycles/js-lib/error/assert.js'
5
- import { AppError } from '@naturalcycles/js-lib/error/error.util.js'
5
+ import { _errorDataAppend, AppError } from '@naturalcycles/js-lib/error/error.util.js'
6
6
  import type {
7
7
  AnyObject,
8
8
  JWTString,
@@ -105,6 +105,10 @@ export interface JWTSignOptions<T extends AnyObject = AnyObject> {
105
105
  * By default `iat` is NOT set (same as legacy JWTService with its `noTimestamp: true` default).
106
106
  */
107
107
  issuedAt?: UnixTimestamp
108
+ /**
109
+ * Sets the `kid` (key id) header, required by some APIs (e.g Apple App Store Connect).
110
+ */
111
+ kid?: string
108
112
  /**
109
113
  * Overrides cfg.schema for this call.
110
114
  */
@@ -198,14 +202,19 @@ export class JWTService2<T extends AnyObject = AnyObject> {
198
202
  this.cfg.errorData,
199
203
  )
200
204
 
201
- const { expiresAt, notBefore, issuer, audience, subject, jwtid, issuedAt, schema } = {
205
+ const { expiresAt, notBefore, issuer, audience, subject, jwtid, issuedAt, kid, schema } = {
202
206
  ...this.cfg.signOptions,
203
207
  ...opt,
204
208
  }
205
209
 
206
210
  ;(schema || this.cfg.schema)?.validate(payload)
207
211
 
208
- const jwt = new SignJWT(payload).setProtectedHeader({ alg: this.cfg.algorithm, typ: 'JWT' })
212
+ // `kid: undefined` is dropped by JSON serialization, keeping the header unchanged when not set
213
+ const jwt = new SignJWT(payload).setProtectedHeader({
214
+ alg: this.cfg.algorithm,
215
+ typ: 'JWT',
216
+ kid,
217
+ })
209
218
  if (expiresAt !== null) jwt.setExpirationTime(expiresAt)
210
219
  if (notBefore !== undefined) jwt.setNotBefore(notBefore)
211
220
  if (issuer) jwt.setIssuer(issuer)
@@ -243,35 +252,42 @@ export class JWTService2<T extends AnyObject = AnyObject> {
243
252
  throw this.normalizeError(err)
244
253
  }
245
254
 
246
- ;(schema || this.cfg.schema)?.validate(data)
255
+ this.validate(data, schema)
247
256
 
248
257
  return data
249
258
  }
250
259
 
251
260
  decode<TT extends T = T>(token: JWTString, opt: JWTDecodeOptions<TT> = {}): JWTDecoded<TT> {
252
- let header: JWTHeader
253
- let payload: TT
261
+ let decoded: JWTDecoded<TT>
254
262
 
255
263
  try {
256
- header = decodeProtectedHeader(token) as JWTHeader
257
- payload = decodeJwt(token) as TT
264
+ decoded = jwtDecode<TT>(token)
258
265
  } catch (err) {
259
- throw new JWTError(
260
- 'invalid token, unable to decode',
261
- {
262
- ...this.cfg.errorData,
263
- code: 'JWT_INVALID',
264
- },
265
- { cause: err },
266
- )
266
+ if (this.cfg.errorData) {
267
+ _errorDataAppend(err, this.cfg.errorData)
268
+ }
269
+ throw err
267
270
  }
268
271
 
269
- ;(opt.schema || this.cfg.schema)?.validate(payload)
272
+ this.validate(decoded.payload, opt.schema)
273
+
274
+ return decoded
275
+ }
270
276
 
271
- return {
272
- header,
273
- payload,
274
- signature: token.split('.')[2]!,
277
+ /**
278
+ * Schema-validation errors on Verify/Decode are extended with cfg.errorData:
279
+ * a token with a non-conforming payload is as unauthorized as an invalid one.
280
+ * (On Sign the payload comes from own code, not from user input, so a validation
281
+ * error there indicates a programming error and is thrown as-is.)
282
+ */
283
+ private validate<TT extends T>(payload: TT, schema?: JSchema<TT, any> | AjvSchema<TT>): void {
284
+ try {
285
+ ;(schema || this.cfg.schema)?.validate(payload)
286
+ } catch (err) {
287
+ if (this.cfg.errorData) {
288
+ _errorDataAppend(err, this.cfg.errorData)
289
+ }
290
+ throw err
275
291
  }
276
292
  }
277
293
 
@@ -304,6 +320,31 @@ export class JWTService2<T extends AnyObject = AnyObject> {
304
320
  }
305
321
  }
306
322
 
323
+ /**
324
+ * Decodes a JWT without verifying its signature (no key needed) - never trust the result.
325
+ * Standalone version of JWTService2.decode, for when there's no service instance at hand
326
+ * (no schema validation, no errorData extension).
327
+ *
328
+ * Throws JWTError with code JWT_INVALID if the token cannot be decoded.
329
+ */
330
+ export function jwtDecode<T extends AnyObject>(token: JWTString): JWTDecoded<T> {
331
+ let header: JWTHeader
332
+ let payload: T
333
+
334
+ try {
335
+ header = decodeProtectedHeader(token) as JWTHeader
336
+ payload = decodeJwt(token) as T
337
+ } catch (err) {
338
+ throw new JWTError('invalid token, unable to decode', { code: 'JWT_INVALID' }, { cause: err })
339
+ }
340
+
341
+ return {
342
+ header,
343
+ payload,
344
+ signature: token.split('.')[2]!,
345
+ }
346
+ }
347
+
307
348
  export type JWTErrorCode = 'JWT_EXPIRED' | 'JWT_NOT_YET_VALID' | 'JWT_INVALID'
308
349
 
309
350
  export interface JWTErrorData extends ErrorData {