@carecard/jwt-read 3.18.0 → 3.20.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.
package/lib/jwtLib.js CHANGED
@@ -26,7 +26,12 @@ function doesJwtUserHasRole(req, userRole) {
26
26
 
27
27
  const jwtObj = req?.jwt || this;
28
28
 
29
- if (!userRole || typeof userRole !== 'string' || !jwtObj?.payload?.roles || !Array.isArray(jwtObj.payload.roles)) {
29
+ if (
30
+ !userRole ||
31
+ typeof userRole !== 'string' ||
32
+ !jwtObj?.payload?.roles ||
33
+ !Array.isArray(jwtObj.payload.roles)
34
+ ) {
30
35
  throwNotAuthorizedError();
31
36
  }
32
37
 
@@ -40,7 +45,9 @@ function throwError(customErrorFunction) {
40
45
 
41
46
  const customError = customErrorFunction();
42
47
 
43
- if (customError instanceof Error) throw customError;
48
+ if (customError instanceof Error) {
49
+ throw customError;
50
+ }
44
51
 
45
52
  throw new Error('Custom authentication error function returned without throwing');
46
53
  }
@@ -90,6 +97,7 @@ function validateAndExtractJwtObject(req, publicKey, customErrorFunction, option
90
97
 
91
98
  if (jwtString && isJwtSignatureValid) {
92
99
  _extractJwtObject(req, jwtString, customErrorFunction);
100
+ requireActiveExtractedToken(req, 'jwt', customErrorFunction);
93
101
  validateAndExtractOptionalUserAuthorizationObject(req, options, customErrorFunction);
94
102
  } else {
95
103
  req['jwt'] = null;
@@ -107,6 +115,7 @@ function validateAndExtractWebToken(req, publicKey, headerName, customErrorFunct
107
115
 
108
116
  if (webTokenString && isJwtSignatureValid) {
109
117
  _extractJwtObject(req, webTokenString, customErrorFunction);
118
+ requireActiveExtractedToken(req, 'jwt', customErrorFunction);
110
119
  validateAndExtractOptionalUserAuthorizationObject(req, options, customErrorFunction);
111
120
  } else {
112
121
  req['jwt'] = null;
@@ -118,13 +127,26 @@ function validateAndExtractWebToken(req, publicKey, headerName, customErrorFunct
118
127
 
119
128
  // Pattern: Decorator - extends no-throw JWT extraction without changing req.jwt failure semantics.
120
129
  function validateAndExtractJwtObjectNoThrow(req, publicKey, options) {
121
- _validateAndExtractGenericNoThrow(req, publicKey, _validateJwtNoThrow, _extractJwtObjectNoThrow, 'jwt');
130
+ _validateAndExtractGenericNoThrow(
131
+ req,
132
+ publicKey,
133
+ _validateJwtNoThrow,
134
+ _extractJwtObjectNoThrow,
135
+ 'jwt',
136
+ );
122
137
  validateAndExtractOptionalUserAuthorizationObjectNoThrow(req, options);
123
138
  return req;
124
139
  }
125
140
 
126
141
  // Pattern: Decorator - keeps service-JWT checks distinct from optional user authorization context.
127
- function validateAndExtractServiceJwtObject(req, publicKey, expectedIssuer, expectedAudience, customErrorFunction, options) {
142
+ function validateAndExtractServiceJwtObject(
143
+ req,
144
+ publicKey,
145
+ expectedIssuer,
146
+ expectedAudience,
147
+ customErrorFunction,
148
+ options,
149
+ ) {
128
150
  const jwtString = _validateJwt(req, customErrorFunction);
129
151
  const isJwtSignatureValid = jwtVerifySignedToken(jwtString, publicKey);
130
152
 
@@ -144,10 +166,21 @@ function validateAndExtractServiceJwtObject(req, publicKey, expectedIssuer, expe
144
166
  }
145
167
 
146
168
  // Pattern: Decorator - normalizes primary auth first, then applies optional scoped authorization.
147
- async function validateAndExtractJwtOrServerAuthObject(req, publicKey, serverAuthIntrospector, customErrorFunction, options) {
169
+ async function validateAndExtractJwtOrServerAuthObject(
170
+ req,
171
+ publicKey,
172
+ serverAuthIntrospector,
173
+ customErrorFunction,
174
+ options,
175
+ ) {
148
176
  if (!tryValidateAndExtractJwtObject(req, publicKey)) {
149
177
  const serverAuthToken = _validateServerAuthToken(req, customErrorFunction);
150
- const claims = await introspectServerAuthToken(serverAuthIntrospector, serverAuthToken, req, customErrorFunction);
178
+ const claims = await introspectServerAuthToken(
179
+ serverAuthIntrospector,
180
+ serverAuthToken,
181
+ req,
182
+ customErrorFunction,
183
+ );
151
184
  attachServerAuthClaims(req, claims, customErrorFunction);
152
185
  }
153
186
 
@@ -157,14 +190,26 @@ async function validateAndExtractJwtOrServerAuthObject(req, publicKey, serverAut
157
190
 
158
191
  // Pattern: Decorator - extends custom-header no-throw extraction with optional user authorization context.
159
192
  function validateAndExtractWebTokenObjectNoThrow(req, publicKey, headerName, options) {
160
- _validateAndExtractGenericNoThrow(req, publicKey, r => _validateWebTokenNoThrow(r, headerName), _extractJwtObjectNoThrow, 'jwt');
193
+ _validateAndExtractGenericNoThrow(
194
+ req,
195
+ publicKey,
196
+ r => _validateWebTokenNoThrow(r, headerName),
197
+ _extractJwtObjectNoThrow,
198
+ 'jwt',
199
+ );
161
200
  validateAndExtractOptionalUserAuthorizationObjectNoThrow(req, options);
162
201
  return req;
163
202
  }
164
203
 
165
204
  // Pattern: Decorator - keeps visitor token extraction independent from optional user authorization context.
166
205
  function validateAndExtractVisitorObjectNoThrow(req, publicKey, options) {
167
- _validateAndExtractGenericNoThrow(req, publicKey, _validateVisitorNoThrow, _extractVisitorObjectNoThrow, 'visitor');
206
+ _validateAndExtractGenericNoThrow(
207
+ req,
208
+ publicKey,
209
+ _validateVisitorNoThrow,
210
+ _extractVisitorObjectNoThrow,
211
+ 'visitor',
212
+ );
168
213
  validateAndExtractOptionalUserAuthorizationObjectNoThrow(req, options);
169
214
  return req;
170
215
  }
@@ -196,10 +241,23 @@ function verifyJwt(publicKey, customErrorFunction, options) {
196
241
  }
197
242
 
198
243
  // Pattern: Middleware - verifies service identity and optionally carries user authorization context.
199
- function verifyServiceJwt(publicKey, expectedIssuer, expectedAudience, customErrorFunction, options) {
244
+ function verifyServiceJwt(
245
+ publicKey,
246
+ expectedIssuer,
247
+ expectedAudience,
248
+ customErrorFunction,
249
+ options,
250
+ ) {
200
251
  return function (req, res, next) {
201
252
  try {
202
- validateAndExtractServiceJwtObject(req, publicKey, expectedIssuer, expectedAudience, customErrorFunction, options);
253
+ validateAndExtractServiceJwtObject(
254
+ req,
255
+ publicKey,
256
+ expectedIssuer,
257
+ expectedAudience,
258
+ customErrorFunction,
259
+ options,
260
+ );
203
261
  next();
204
262
  } catch (err) {
205
263
  next(err);
@@ -211,7 +269,13 @@ function verifyServiceJwt(publicKey, expectedIssuer, expectedAudience, customErr
211
269
  function verifyJwtOrServerAuth(publicKey, serverAuthIntrospector, customErrorFunction, options) {
212
270
  return async function (req, res, next) {
213
271
  try {
214
- await validateAndExtractJwtOrServerAuthObject(req, publicKey, serverAuthIntrospector, customErrorFunction, options);
272
+ await validateAndExtractJwtOrServerAuthObject(
273
+ req,
274
+ publicKey,
275
+ serverAuthIntrospector,
276
+ customErrorFunction,
277
+ options,
278
+ );
215
279
  next();
216
280
  } catch (err) {
217
281
  next(err);
@@ -220,10 +284,22 @@ function verifyJwtOrServerAuth(publicKey, serverAuthIntrospector, customErrorFun
220
284
  }
221
285
 
222
286
  // Pattern: Middleware - composes flexible auth, optional user authorization, and role checks.
223
- function verifyJwtOrServerAuthAndHasRole(role, publicKey, serverAuthIntrospector, customErrorFunction, options) {
287
+ function verifyJwtOrServerAuthAndHasRole(
288
+ role,
289
+ publicKey,
290
+ serverAuthIntrospector,
291
+ customErrorFunction,
292
+ options,
293
+ ) {
224
294
  return async function (req, res, next) {
225
295
  try {
226
- await validateAndExtractJwtOrServerAuthObject(req, publicKey, serverAuthIntrospector, customErrorFunction, options);
296
+ await validateAndExtractJwtOrServerAuthObject(
297
+ req,
298
+ publicKey,
299
+ serverAuthIntrospector,
300
+ customErrorFunction,
301
+ options,
302
+ );
227
303
  const isRoleExist = doesJwtUserHasRole(req, role);
228
304
  _isLoginRequired(isRoleExist, customErrorFunction);
229
305
  next();
@@ -313,7 +389,9 @@ function validateAndExtractUserAuthorizationObject(req, publicKey, customErrorFu
313
389
  if (header.present && isUserAuthorizationTokenAllowed(header.token, config)) {
314
390
  _extractUserAuthorizationObjectNoThrow(req, header.token);
315
391
  } else {
316
- if (req) req.userAuthorization = null;
392
+ if (req) {
393
+ req.userAuthorization = null;
394
+ }
317
395
  throwError(customErrorFunction);
318
396
  }
319
397
 
@@ -337,7 +415,9 @@ function validateAndExtractUserAuthorizationObjectNoThrow(req, publicKey, option
337
415
  // Pattern: Guard Clause - skips optional context handling unless a caller explicitly configured it.
338
416
  function validateAndExtractOptionalUserAuthorizationObject(req, options, customErrorFunction) {
339
417
  const config = createOptionalUserAuthorizationConfig(options);
340
- if (!config) return req;
418
+ if (!config) {
419
+ return req;
420
+ }
341
421
 
342
422
  const header = readUserAuthorizationHeader(req, config);
343
423
  if (!header.present) {
@@ -358,7 +438,9 @@ function validateAndExtractOptionalUserAuthorizationObject(req, options, customE
358
438
  // Pattern: Guard Clause - optional no-throw context extraction never changes primary auth results.
359
439
  function validateAndExtractOptionalUserAuthorizationObjectNoThrow(req, options) {
360
440
  const config = createOptionalUserAuthorizationConfig(options);
361
- if (!config) return req;
441
+ if (!config) {
442
+ return req;
443
+ }
362
444
 
363
445
  const header = readUserAuthorizationHeader(req, config);
364
446
  if (header.present && isUserAuthorizationTokenAllowed(header.token, config)) {
@@ -383,9 +465,12 @@ function _isLoginRequired(hasRequiredRole, customErrorFunction) {
383
465
  }
384
466
  }
385
467
 
468
+ // Pattern: Fail-Closed Probe - accepts a bearer JWT only when signature and lifetime are valid.
386
469
  function tryValidateAndExtractJwtObject(req, publicKey) {
387
470
  const jwtString = _validateJwtNoThrow(req);
388
- if (!jwtString || !isJwtString(jwtString)) return false;
471
+ if (!jwtString || !isJwtString(jwtString)) {
472
+ return false;
473
+ }
389
474
 
390
475
  const isJwtSignatureValid = jwtVerifySignedToken(jwtString, publicKey);
391
476
  if (!isJwtSignatureValid) {
@@ -394,13 +479,19 @@ function tryValidateAndExtractJwtObject(req, publicKey) {
394
479
  }
395
480
 
396
481
  _extractJwtObjectNoThrow(req, jwtString);
397
- return Boolean(req?.jwt);
482
+ if (!isJwtPayloadWithinLifetime(req?.jwt?.payload)) {
483
+ req.jwt = null;
484
+ return false;
485
+ }
486
+ return true;
398
487
  }
399
488
 
400
489
  function _validateServerAuthToken(req, customErrorFunction) {
401
490
  const authorizationHeader = req?.get?.('Authorization') || req?.get?.('authorization');
402
491
  const token = _extractJwtNoThrow(authorizationHeader);
403
- if (token) return token;
492
+ if (token) {
493
+ return token;
494
+ }
404
495
 
405
496
  throwError(customErrorFunction);
406
497
  return null;
@@ -422,7 +513,9 @@ async function introspectServerAuthToken(serverAuthIntrospector, token, req, cus
422
513
  function attachServerAuthClaims(req, claims, customErrorFunction) {
423
514
  const payload = createServerAuthPayload(claims);
424
515
  if (!payload.sub) {
425
- if (req) req.jwt = null;
516
+ if (req) {
517
+ req.jwt = null;
518
+ }
426
519
  throwError(customErrorFunction);
427
520
  }
428
521
 
@@ -439,10 +532,9 @@ function attachServerAuthClaims(req, claims, customErrorFunction) {
439
532
  // Pattern: Projection - copies only authoritative verification names and preserves their exact values.
440
533
  function pickEmailVerificationClaims(claims) {
441
534
  return Object.fromEntries(
442
- EMAIL_VERIFICATION_CLAIM_NAMES.filter(claimName => Object.prototype.hasOwnProperty.call(claims, claimName)).map(claimName => [
443
- claimName,
444
- claims[claimName],
445
- ]),
535
+ EMAIL_VERIFICATION_CLAIM_NAMES.filter(claimName =>
536
+ Object.prototype.hasOwnProperty.call(claims, claimName),
537
+ ).map(claimName => [claimName, claims[claimName]]),
446
538
  );
447
539
  }
448
540
 
@@ -462,54 +554,101 @@ function createServerAuthPayload(claims) {
462
554
  }
463
555
 
464
556
  function readEpochSeconds(value) {
465
- if (value === undefined || value === null || value === '') return undefined;
466
- if (typeof value === 'number') return normalizeSeconds(value);
557
+ if (value === undefined || value === null || value === '') {
558
+ return undefined;
559
+ }
560
+ if (typeof value === 'number') {
561
+ return normalizeSeconds(value);
562
+ }
467
563
  const millis = Date.parse(value);
468
564
  return Number.isFinite(millis) ? Math.floor(millis / 1000) : undefined;
469
565
  }
470
566
 
471
567
  function normalizeSeconds(value) {
472
- if (!Number.isFinite(value)) return null;
568
+ if (!Number.isFinite(value)) {
569
+ return null;
570
+ }
473
571
  return value > 1000000000000 ? Math.floor(value / 1000) : Math.floor(value);
474
572
  }
475
573
 
574
+ // Pattern: Policy - validates service identity, audience, and registered token lifetime.
476
575
  function _isServiceJwtFor(payload, expectedIssuer, expectedAudience) {
477
- if (!payload) return false;
478
- if (payload.iss !== expectedIssuer) return false;
479
- if (payload.sub !== expectedIssuer) return false;
480
- if (!payloadAudienceMatches(payload.aud, expectedAudience)) return false;
481
- if (isJwtPayloadIssuedInFuture(payload)) return false;
482
- if (isJwtPayloadExpired(payload)) return false;
483
- if (isJwtPayloadNotYetValid(payload)) return false;
484
- return true;
576
+ if (!payload) {
577
+ return false;
578
+ }
579
+ if (payload.iss !== expectedIssuer) {
580
+ return false;
581
+ }
582
+ if (payload.sub !== expectedIssuer) {
583
+ return false;
584
+ }
585
+ if (!payloadAudienceMatches(payload.aud, expectedAudience)) {
586
+ return false;
587
+ }
588
+ return isJwtPayloadWithinLifetime(payload);
485
589
  }
486
590
 
487
591
  function payloadAudienceMatches(actualAudience, expectedAudience) {
488
- if (Array.isArray(actualAudience)) return actualAudience.includes(expectedAudience);
592
+ if (Array.isArray(actualAudience)) {
593
+ return actualAudience.includes(expectedAudience);
594
+ }
489
595
  return actualAudience === expectedAudience;
490
596
  }
491
597
 
492
598
  function isJwtPayloadExpired(payload) {
493
- if (!payload.exp) return true;
599
+ if (!payload.exp) {
600
+ return true;
601
+ }
494
602
  const exp = normalizeSeconds(payload.exp);
495
- if (!Number.isInteger(exp)) return true;
603
+ if (!Number.isInteger(exp)) {
604
+ return true;
605
+ }
496
606
  return Math.floor(Date.now() / 1000) >= exp;
497
607
  }
498
608
 
499
609
  function isJwtPayloadIssuedInFuture(payload) {
500
- if (!payload.iat) return true;
610
+ if (!payload.iat) {
611
+ return true;
612
+ }
501
613
  const iat = normalizeSeconds(payload.iat);
502
- if (!Number.isInteger(iat)) return true;
614
+ if (!Number.isInteger(iat)) {
615
+ return true;
616
+ }
503
617
  return Math.floor(Date.now() / 1000) < iat;
504
618
  }
505
619
 
506
620
  function isJwtPayloadNotYetValid(payload) {
507
- if (!payload.nbf) return false;
621
+ if (!payload.nbf) {
622
+ return false;
623
+ }
508
624
  const nbf = normalizeSeconds(payload.nbf);
509
- if (!Number.isInteger(nbf)) return true;
625
+ if (!Number.isInteger(nbf)) {
626
+ return true;
627
+ }
510
628
  return Math.floor(Date.now() / 1000) < nbf;
511
629
  }
512
630
 
631
+ // Pattern: Specification - centralizes the registered time-claim policy for signed tokens.
632
+ function isJwtPayloadWithinLifetime(payload) {
633
+ return (
634
+ Boolean(payload) &&
635
+ !isJwtPayloadIssuedInFuture(payload) &&
636
+ !isJwtPayloadExpired(payload) &&
637
+ !isJwtPayloadNotYetValid(payload)
638
+ );
639
+ }
640
+
641
+ // Pattern: Guard Clause - clears an extracted token before surfacing an authentication failure.
642
+ function requireActiveExtractedToken(req, propertyName, customErrorFunction) {
643
+ if (isJwtPayloadWithinLifetime(req?.[propertyName]?.payload)) {
644
+ return;
645
+ }
646
+ if (req) {
647
+ req[propertyName] = null;
648
+ }
649
+ throwError(customErrorFunction);
650
+ }
651
+
513
652
  // Pattern: Factory - normalizes direct user authorization options into one config shape.
514
653
  function createUserAuthorizationConfig(publicKey, options) {
515
654
  return normalizeUserAuthorizationConfig({
@@ -520,7 +659,9 @@ function createUserAuthorizationConfig(publicKey, options) {
520
659
 
521
660
  // Pattern: Factory - detects optional user authorization configuration without affecting legacy callers.
522
661
  function createOptionalUserAuthorizationConfig(options) {
523
- if (!options || !Object.prototype.hasOwnProperty.call(options, 'userAuthorization')) return null;
662
+ if (!options || !Object.prototype.hasOwnProperty.call(options, 'userAuthorization')) {
663
+ return null;
664
+ }
524
665
  return normalizeUserAuthorizationConfig(options.userAuthorization || {});
525
666
  }
526
667
 
@@ -529,7 +670,10 @@ function normalizeUserAuthorizationConfig(options) {
529
670
  return {
530
671
  publicKey: options?.publicKey,
531
672
  headerName: options?.headerName || DEFAULT_USER_AUTHORIZATION_HEADER_NAME,
532
- maxTokenLength: normalizePositiveInteger(options?.maxTokenLength, DEFAULT_USER_AUTHORIZATION_MAX_TOKEN_LENGTH),
673
+ maxTokenLength: normalizePositiveInteger(
674
+ options?.maxTokenLength,
675
+ DEFAULT_USER_AUTHORIZATION_MAX_TOKEN_LENGTH,
676
+ ),
533
677
  expectedType: options?.expectedType,
534
678
  expectedIssuer: options?.expectedIssuer,
535
679
  expectedAudience: options?.expectedAudience,
@@ -546,35 +690,53 @@ function normalizePositiveInteger(value, defaultValue) {
546
690
  function readUserAuthorizationHeader(req, config) {
547
691
  const rawToken = getRequestHeader(req, config.headerName);
548
692
  const present = rawToken !== undefined && rawToken !== null;
549
- if (!present || typeof rawToken !== 'string') return { present, token: null };
693
+ if (!present || typeof rawToken !== 'string') {
694
+ return { present, token: null };
695
+ }
550
696
 
551
697
  const token = rawToken.trim();
552
- if (!token || token.length > config.maxTokenLength || !isJwtString(token)) return { present: true, token: null };
698
+ if (!token || token.length > config.maxTokenLength || !isJwtString(token)) {
699
+ return { present: true, token: null };
700
+ }
553
701
  return { present: true, token };
554
702
  }
555
703
 
556
704
  // Pattern: Adapter - supports Express case-insensitive headers and simple test doubles.
557
705
  function getRequestHeader(req, headerName) {
558
706
  const value = req?.get?.(headerName);
559
- if (value !== undefined && value !== null) return value;
707
+ if (value !== undefined && value !== null) {
708
+ return value;
709
+ }
560
710
 
561
711
  const lowerCaseHeaderName = headerName.toLowerCase();
562
- if (lowerCaseHeaderName === headerName) return value;
712
+ if (lowerCaseHeaderName === headerName) {
713
+ return value;
714
+ }
563
715
  return req?.get?.(lowerCaseHeaderName);
564
716
  }
565
717
 
566
718
  // Pattern: Single Responsibility - verifies signature and registered JWT time claims for user authorization.
567
719
  function isUserAuthorizationTokenAllowed(token, config) {
568
- if (!token || !config.publicKey || !jwtVerifySignedToken(token, config.publicKey)) return false;
720
+ if (!token || !config.publicKey || !jwtVerifySignedToken(token, config.publicKey)) {
721
+ return false;
722
+ }
569
723
 
570
724
  const payload = readVerifiedJwtPayload(token);
571
- if (!payload) return false;
572
- if (isJwtPayloadIssuedInFuture(payload)) return false;
573
- if (isJwtPayloadExpired(payload)) return false;
574
- if (isJwtPayloadNotYetValid(payload)) return false;
575
- if (config.expectedType && payload.typ !== config.expectedType) return false;
576
- if (config.expectedIssuer && payload.iss !== config.expectedIssuer) return false;
577
- if (!expectedAudienceMatches(payload.aud, config.expectedAudience)) return false;
725
+ if (!payload) {
726
+ return false;
727
+ }
728
+ if (!isJwtPayloadWithinLifetime(payload)) {
729
+ return false;
730
+ }
731
+ if (config.expectedType && payload.typ !== config.expectedType) {
732
+ return false;
733
+ }
734
+ if (config.expectedIssuer && payload.iss !== config.expectedIssuer) {
735
+ return false;
736
+ }
737
+ if (!expectedAudienceMatches(payload.aud, config.expectedAudience)) {
738
+ return false;
739
+ }
578
740
 
579
741
  return true;
580
742
  }
@@ -586,14 +748,20 @@ function readVerifiedJwtPayload(token) {
586
748
 
587
749
  // Pattern: Pure Function - supports either one expected audience or a small allowed set.
588
750
  function expectedAudienceMatches(actualAudience, expectedAudience) {
589
- if (expectedAudience === undefined || expectedAudience === null || expectedAudience === '') return true;
590
- if (Array.isArray(expectedAudience)) return expectedAudience.some(audience => payloadAudienceMatches(actualAudience, audience));
751
+ if (expectedAudience === undefined || expectedAudience === null || expectedAudience === '') {
752
+ return true;
753
+ }
754
+ if (Array.isArray(expectedAudience)) {
755
+ return expectedAudience.some(audience => payloadAudienceMatches(actualAudience, audience));
756
+ }
591
757
  return payloadAudienceMatches(actualAudience, expectedAudience);
592
758
  }
593
759
 
594
760
  function _validateGenericNoThrow(req, headerName, extractor) {
595
761
  let jwtRaw = req.get(headerName);
596
- if (!jwtRaw) return null;
762
+ if (!jwtRaw) {
763
+ return null;
764
+ }
597
765
  let jwt = extractor(jwtRaw);
598
766
  return isJwtString(jwt) ? jwt : null;
599
767
  }
@@ -601,11 +769,14 @@ function _validateGenericNoThrow(req, headerName, extractor) {
601
769
  function _validateGeneric(req, headerName, extractor, customErrorFunction) {
602
770
  let jwtRaw = req.get(headerName);
603
771
  let jwt = extractor(jwtRaw, customErrorFunction);
604
- if (isJwtString(jwt)) return jwt;
772
+ if (isJwtString(jwt)) {
773
+ return jwt;
774
+ }
605
775
  throwError(customErrorFunction);
606
776
  return null;
607
777
  }
608
778
 
779
+ // Pattern: Fail-Closed Extraction - optional tokens survive only valid signatures and lifetimes.
609
780
  function _validateAndExtractGenericNoThrow(req, publicKey, validator, extractor, propertyName) {
610
781
  try {
611
782
  const jwtString = validator(req);
@@ -613,11 +784,16 @@ function _validateAndExtractGenericNoThrow(req, publicKey, validator, extractor,
613
784
 
614
785
  if (jwtString && isJwtSignatureValid) {
615
786
  extractor(req, jwtString);
787
+ if (!isJwtPayloadWithinLifetime(req?.[propertyName]?.payload)) {
788
+ req[propertyName] = null;
789
+ }
616
790
  } else {
617
791
  req[propertyName] = null;
618
792
  }
619
793
  } catch (err) {
620
- if (req) req[propertyName] = null;
794
+ if (req) {
795
+ req[propertyName] = null;
796
+ }
621
797
  throw err;
622
798
  }
623
799
  return req;
@@ -629,7 +805,9 @@ function _extractGenericObjectNoThrow(req, jwt, attacher, propertyName) {
629
805
  attacher(obj);
630
806
  req[propertyName] = obj;
631
807
  } else {
632
- if (req) req[propertyName] = null;
808
+ if (req) {
809
+ req[propertyName] = null;
810
+ }
633
811
  }
634
812
  }
635
813
 
package/lib/jwtRoles.js CHANGED
@@ -44,8 +44,12 @@ function getContext(req) {
44
44
 
45
45
  function getUserAuthorization(req) {
46
46
  const userAuthorization = req?.userAuthorization;
47
- if (!userAuthorization || typeof userAuthorization !== 'object') return null;
48
- if (!userAuthorization.payload || typeof userAuthorization.payload !== 'object') return null;
47
+ if (!userAuthorization || typeof userAuthorization !== 'object') {
48
+ return null;
49
+ }
50
+ if (!userAuthorization.payload || typeof userAuthorization.payload !== 'object') {
51
+ return null;
52
+ }
49
53
 
50
54
  return userAuthorization;
51
55
  }
@@ -0,0 +1,36 @@
1
+ import { ESLint } from 'eslint';
2
+
3
+ const eslint = new ESLint();
4
+
5
+ // Pattern: Pure Function - builds one deterministic command without shell interpolation.
6
+ const createCommand = (command, filePaths) =>
7
+ `${command} ${filePaths.map(filePath => JSON.stringify(filePath)).join(' ')}`;
8
+
9
+ // Pattern: Adapter - derives lint-staged input from ESLint's authoritative ignore rules.
10
+ const removeEslintIgnoredFiles = async filePaths => {
11
+ const ignoredFileStates = await Promise.all(
12
+ filePaths.map(filePath => eslint.isPathIgnored(filePath)),
13
+ );
14
+
15
+ return filePaths.flatMap((filePath, index) => (ignoredFileStates[index] ? [] : [filePath]));
16
+ };
17
+
18
+ // Pattern: Pipeline - preserves ESLint-before-Prettier ordering for staged code.
19
+ const createJavaScriptTasks = async filePaths => {
20
+ const lintableFilePaths = await removeEslintIgnoredFiles(filePaths);
21
+ const tasks = [];
22
+
23
+ if (lintableFilePaths.length > 0) {
24
+ tasks.push(createCommand('eslint --fix --max-warnings 0', lintableFilePaths));
25
+ }
26
+
27
+ tasks.push(createCommand('prettier --write', filePaths));
28
+ return tasks;
29
+ };
30
+
31
+ const lintStagedConfig = {
32
+ '*.{js,jsx,mjs,cjs,ts,tsx,mts,cts}': createJavaScriptTasks,
33
+ '*.{json,jsonc,md,mdx,css,scss,yaml,yml}': ['prettier --write'],
34
+ };
35
+
36
+ export default lintStagedConfig;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@carecard/jwt-read",
3
- "version": "3.18.0",
3
+ "version": "3.20.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/CareCard-ca/pkg-jwt-read.git"
@@ -10,15 +10,17 @@
10
10
  "types": "index.d.ts",
11
11
  "scripts": {
12
12
  "test": "node scripts/runPackageTask.mjs test",
13
- "test:order": "node --test scripts/testOrder/randomizeTestOrder.test.mjs scripts/testOrder/testOrderPolicy.test.mjs scripts/testParallel/runIndexedMochaTests.test.mjs scripts/testParallel/parallelTestPolicy.test.mjs scripts/packageTaskRunner.test.mjs",
13
+ "test:order": "node --test scripts/testOrder/randomizeTestOrder.test.mjs scripts/testParallel/runIndexedMochaTests.test.mjs scripts/packageTaskRunner.test.mjs scripts/canonicalTestCommand.test.mjs",
14
14
  "test:types": "node scripts/runPackageTask.mjs test:types",
15
15
  "test:coverage": "node scripts/runPackageTask.mjs test:coverage",
16
16
  "test:All": "node scripts/runPackageTask.mjs test:All",
17
- "format": "prettier --write .",
18
- "format:check": "prettier --check .",
17
+ "validate:audits": "node scripts/runPackageTask.mjs validate:audits",
18
+ "format": "prettier --write \"**/*.{js,jsx,mjs,cjs,ts,tsx,mts,cts}\"",
19
+ "format:check": "prettier --check \"**/*.{js,jsx,mjs,cjs,ts,tsx,mts,cts}\"",
20
+ "lint-staged": "lint-staged",
19
21
  "prepare": "husky",
20
- "lint:fix": "eslint --fix",
21
- "lint": "eslint"
22
+ "lint:fix": "eslint . --fix --max-warnings 0",
23
+ "lint": "eslint . --max-warnings 0"
22
24
  },
23
25
  "keywords": [
24
26
  "auth",
@@ -29,27 +31,31 @@
29
31
  "author": "CareCard team",
30
32
  "license": "ISC",
31
33
  "devDependencies": {
34
+ "@eslint/js": "9.39.5",
32
35
  "@types/express": "5.0.6",
33
36
  "@types/mocha": "10.0.10",
34
37
  "@types/node": "25.9.3",
35
- "eslint": "9.39.4",
38
+ "@typescript-eslint/parser": "8.67.0",
39
+ "eslint": "9.39.5",
40
+ "globals": "17.7.0",
36
41
  "husky": "9.1.7",
37
- "lint-staged": "17.0.7",
42
+ "lint-staged": "17.2.0",
38
43
  "mocha": "11.7.6",
39
44
  "nyc": "18.0.0",
40
- "prettier": "3.8.4",
45
+ "prettier": "3.9.6",
41
46
  "ts-node": "10.9.2",
42
47
  "typescript": "6.0.3"
43
48
  },
44
49
  "dependencies": {
45
- "@carecard/auth-util": "3.18.0",
46
- "@carecard/common-util": "3.18.0",
47
- "@carecard/validate": "3.18.0"
50
+ "@carecard/auth-util": "3.20.0",
51
+ "@carecard/common-util": "3.20.0",
52
+ "@carecard/validate": "3.20.0"
48
53
  },
49
54
  "overrides": {
50
55
  "diff": "8.0.4",
51
- "minimatch": "10.2.5",
56
+ "brace-expansion": "5.0.9",
57
+ "minimatch": "10.2.6",
52
58
  "serialize-javascript": "7.0.5",
53
- "js-yaml": "4.3.0"
59
+ "js-yaml": "4.3.1"
54
60
  }
55
61
  }