@carecard/jwt-read 3.18.0 → 3.19.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/.agents/skills/carecard-workspace-standards/SKILL.md +49 -20
- package/.agents/skills/github-pr-create-update/SKILL.md +22 -1
- package/.agents/skills/github-pr-merge-cleanup/SKILL.md +22 -1
- package/.agents/skills/pkg-jwt-read-coding-standards-and-best-practices/SKILL.md +42 -7
- package/.agents/skills/pkg-jwt-read-jwt-middleware-library/SKILL.md +39 -12
- package/.agents/skills/pkg-publish/SKILL.md +22 -1
- package/.agents/skills/software-design-patterns-and-clean-code/SKILL.md +23 -3
- package/.prettierrc.js +4 -2
- package/AGENTS.md +19 -0
- package/eslint.config.mjs +52 -0
- package/index.d.ts +5 -1
- package/index.js +2 -1
- package/lib/jwtLib.js +220 -60
- package/lib/jwtRoles.js +6 -2
- package/lint-staged.config.mjs +36 -0
- package/package.json +17 -11
- package/readme.md +22 -1
- package/scripts/canonicalTestCommand.test.mjs +33 -0
- package/scripts/packageTaskRunner.audit.mjs +37 -0
- package/scripts/packageTaskRunner.test.mjs +4 -26
- package/scripts/runPackageTask.mjs +65 -16
- package/scripts/testOrder/randomizeTestOrder.cjs +15 -5
- package/scripts/testOrder/{testOrderPolicy.test.mjs → testOrderPolicy.audit.mjs} +8 -2
- package/scripts/testParallel/{parallelTestPolicy.test.mjs → parallelTestPolicy.audit.mjs} +28 -9
- package/scripts/testParallel/runIndexedMochaTests.cjs +3 -1
- package/scripts/testParallel/runIndexedMochaTests.test.mjs +8 -2
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 (
|
|
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)
|
|
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
|
}
|
|
@@ -118,13 +125,26 @@ function validateAndExtractWebToken(req, publicKey, headerName, customErrorFunct
|
|
|
118
125
|
|
|
119
126
|
// Pattern: Decorator - extends no-throw JWT extraction without changing req.jwt failure semantics.
|
|
120
127
|
function validateAndExtractJwtObjectNoThrow(req, publicKey, options) {
|
|
121
|
-
_validateAndExtractGenericNoThrow(
|
|
128
|
+
_validateAndExtractGenericNoThrow(
|
|
129
|
+
req,
|
|
130
|
+
publicKey,
|
|
131
|
+
_validateJwtNoThrow,
|
|
132
|
+
_extractJwtObjectNoThrow,
|
|
133
|
+
'jwt',
|
|
134
|
+
);
|
|
122
135
|
validateAndExtractOptionalUserAuthorizationObjectNoThrow(req, options);
|
|
123
136
|
return req;
|
|
124
137
|
}
|
|
125
138
|
|
|
126
139
|
// Pattern: Decorator - keeps service-JWT checks distinct from optional user authorization context.
|
|
127
|
-
function validateAndExtractServiceJwtObject(
|
|
140
|
+
function validateAndExtractServiceJwtObject(
|
|
141
|
+
req,
|
|
142
|
+
publicKey,
|
|
143
|
+
expectedIssuer,
|
|
144
|
+
expectedAudience,
|
|
145
|
+
customErrorFunction,
|
|
146
|
+
options,
|
|
147
|
+
) {
|
|
128
148
|
const jwtString = _validateJwt(req, customErrorFunction);
|
|
129
149
|
const isJwtSignatureValid = jwtVerifySignedToken(jwtString, publicKey);
|
|
130
150
|
|
|
@@ -144,10 +164,21 @@ function validateAndExtractServiceJwtObject(req, publicKey, expectedIssuer, expe
|
|
|
144
164
|
}
|
|
145
165
|
|
|
146
166
|
// Pattern: Decorator - normalizes primary auth first, then applies optional scoped authorization.
|
|
147
|
-
async function validateAndExtractJwtOrServerAuthObject(
|
|
167
|
+
async function validateAndExtractJwtOrServerAuthObject(
|
|
168
|
+
req,
|
|
169
|
+
publicKey,
|
|
170
|
+
serverAuthIntrospector,
|
|
171
|
+
customErrorFunction,
|
|
172
|
+
options,
|
|
173
|
+
) {
|
|
148
174
|
if (!tryValidateAndExtractJwtObject(req, publicKey)) {
|
|
149
175
|
const serverAuthToken = _validateServerAuthToken(req, customErrorFunction);
|
|
150
|
-
const claims = await introspectServerAuthToken(
|
|
176
|
+
const claims = await introspectServerAuthToken(
|
|
177
|
+
serverAuthIntrospector,
|
|
178
|
+
serverAuthToken,
|
|
179
|
+
req,
|
|
180
|
+
customErrorFunction,
|
|
181
|
+
);
|
|
151
182
|
attachServerAuthClaims(req, claims, customErrorFunction);
|
|
152
183
|
}
|
|
153
184
|
|
|
@@ -157,14 +188,26 @@ async function validateAndExtractJwtOrServerAuthObject(req, publicKey, serverAut
|
|
|
157
188
|
|
|
158
189
|
// Pattern: Decorator - extends custom-header no-throw extraction with optional user authorization context.
|
|
159
190
|
function validateAndExtractWebTokenObjectNoThrow(req, publicKey, headerName, options) {
|
|
160
|
-
_validateAndExtractGenericNoThrow(
|
|
191
|
+
_validateAndExtractGenericNoThrow(
|
|
192
|
+
req,
|
|
193
|
+
publicKey,
|
|
194
|
+
r => _validateWebTokenNoThrow(r, headerName),
|
|
195
|
+
_extractJwtObjectNoThrow,
|
|
196
|
+
'jwt',
|
|
197
|
+
);
|
|
161
198
|
validateAndExtractOptionalUserAuthorizationObjectNoThrow(req, options);
|
|
162
199
|
return req;
|
|
163
200
|
}
|
|
164
201
|
|
|
165
202
|
// Pattern: Decorator - keeps visitor token extraction independent from optional user authorization context.
|
|
166
203
|
function validateAndExtractVisitorObjectNoThrow(req, publicKey, options) {
|
|
167
|
-
_validateAndExtractGenericNoThrow(
|
|
204
|
+
_validateAndExtractGenericNoThrow(
|
|
205
|
+
req,
|
|
206
|
+
publicKey,
|
|
207
|
+
_validateVisitorNoThrow,
|
|
208
|
+
_extractVisitorObjectNoThrow,
|
|
209
|
+
'visitor',
|
|
210
|
+
);
|
|
168
211
|
validateAndExtractOptionalUserAuthorizationObjectNoThrow(req, options);
|
|
169
212
|
return req;
|
|
170
213
|
}
|
|
@@ -196,10 +239,23 @@ function verifyJwt(publicKey, customErrorFunction, options) {
|
|
|
196
239
|
}
|
|
197
240
|
|
|
198
241
|
// Pattern: Middleware - verifies service identity and optionally carries user authorization context.
|
|
199
|
-
function verifyServiceJwt(
|
|
242
|
+
function verifyServiceJwt(
|
|
243
|
+
publicKey,
|
|
244
|
+
expectedIssuer,
|
|
245
|
+
expectedAudience,
|
|
246
|
+
customErrorFunction,
|
|
247
|
+
options,
|
|
248
|
+
) {
|
|
200
249
|
return function (req, res, next) {
|
|
201
250
|
try {
|
|
202
|
-
validateAndExtractServiceJwtObject(
|
|
251
|
+
validateAndExtractServiceJwtObject(
|
|
252
|
+
req,
|
|
253
|
+
publicKey,
|
|
254
|
+
expectedIssuer,
|
|
255
|
+
expectedAudience,
|
|
256
|
+
customErrorFunction,
|
|
257
|
+
options,
|
|
258
|
+
);
|
|
203
259
|
next();
|
|
204
260
|
} catch (err) {
|
|
205
261
|
next(err);
|
|
@@ -211,7 +267,13 @@ function verifyServiceJwt(publicKey, expectedIssuer, expectedAudience, customErr
|
|
|
211
267
|
function verifyJwtOrServerAuth(publicKey, serverAuthIntrospector, customErrorFunction, options) {
|
|
212
268
|
return async function (req, res, next) {
|
|
213
269
|
try {
|
|
214
|
-
await validateAndExtractJwtOrServerAuthObject(
|
|
270
|
+
await validateAndExtractJwtOrServerAuthObject(
|
|
271
|
+
req,
|
|
272
|
+
publicKey,
|
|
273
|
+
serverAuthIntrospector,
|
|
274
|
+
customErrorFunction,
|
|
275
|
+
options,
|
|
276
|
+
);
|
|
215
277
|
next();
|
|
216
278
|
} catch (err) {
|
|
217
279
|
next(err);
|
|
@@ -220,10 +282,22 @@ function verifyJwtOrServerAuth(publicKey, serverAuthIntrospector, customErrorFun
|
|
|
220
282
|
}
|
|
221
283
|
|
|
222
284
|
// Pattern: Middleware - composes flexible auth, optional user authorization, and role checks.
|
|
223
|
-
function verifyJwtOrServerAuthAndHasRole(
|
|
285
|
+
function verifyJwtOrServerAuthAndHasRole(
|
|
286
|
+
role,
|
|
287
|
+
publicKey,
|
|
288
|
+
serverAuthIntrospector,
|
|
289
|
+
customErrorFunction,
|
|
290
|
+
options,
|
|
291
|
+
) {
|
|
224
292
|
return async function (req, res, next) {
|
|
225
293
|
try {
|
|
226
|
-
await validateAndExtractJwtOrServerAuthObject(
|
|
294
|
+
await validateAndExtractJwtOrServerAuthObject(
|
|
295
|
+
req,
|
|
296
|
+
publicKey,
|
|
297
|
+
serverAuthIntrospector,
|
|
298
|
+
customErrorFunction,
|
|
299
|
+
options,
|
|
300
|
+
);
|
|
227
301
|
const isRoleExist = doesJwtUserHasRole(req, role);
|
|
228
302
|
_isLoginRequired(isRoleExist, customErrorFunction);
|
|
229
303
|
next();
|
|
@@ -313,7 +387,9 @@ function validateAndExtractUserAuthorizationObject(req, publicKey, customErrorFu
|
|
|
313
387
|
if (header.present && isUserAuthorizationTokenAllowed(header.token, config)) {
|
|
314
388
|
_extractUserAuthorizationObjectNoThrow(req, header.token);
|
|
315
389
|
} else {
|
|
316
|
-
if (req)
|
|
390
|
+
if (req) {
|
|
391
|
+
req.userAuthorization = null;
|
|
392
|
+
}
|
|
317
393
|
throwError(customErrorFunction);
|
|
318
394
|
}
|
|
319
395
|
|
|
@@ -337,7 +413,9 @@ function validateAndExtractUserAuthorizationObjectNoThrow(req, publicKey, option
|
|
|
337
413
|
// Pattern: Guard Clause - skips optional context handling unless a caller explicitly configured it.
|
|
338
414
|
function validateAndExtractOptionalUserAuthorizationObject(req, options, customErrorFunction) {
|
|
339
415
|
const config = createOptionalUserAuthorizationConfig(options);
|
|
340
|
-
if (!config)
|
|
416
|
+
if (!config) {
|
|
417
|
+
return req;
|
|
418
|
+
}
|
|
341
419
|
|
|
342
420
|
const header = readUserAuthorizationHeader(req, config);
|
|
343
421
|
if (!header.present) {
|
|
@@ -358,7 +436,9 @@ function validateAndExtractOptionalUserAuthorizationObject(req, options, customE
|
|
|
358
436
|
// Pattern: Guard Clause - optional no-throw context extraction never changes primary auth results.
|
|
359
437
|
function validateAndExtractOptionalUserAuthorizationObjectNoThrow(req, options) {
|
|
360
438
|
const config = createOptionalUserAuthorizationConfig(options);
|
|
361
|
-
if (!config)
|
|
439
|
+
if (!config) {
|
|
440
|
+
return req;
|
|
441
|
+
}
|
|
362
442
|
|
|
363
443
|
const header = readUserAuthorizationHeader(req, config);
|
|
364
444
|
if (header.present && isUserAuthorizationTokenAllowed(header.token, config)) {
|
|
@@ -385,7 +465,9 @@ function _isLoginRequired(hasRequiredRole, customErrorFunction) {
|
|
|
385
465
|
|
|
386
466
|
function tryValidateAndExtractJwtObject(req, publicKey) {
|
|
387
467
|
const jwtString = _validateJwtNoThrow(req);
|
|
388
|
-
if (!jwtString || !isJwtString(jwtString))
|
|
468
|
+
if (!jwtString || !isJwtString(jwtString)) {
|
|
469
|
+
return false;
|
|
470
|
+
}
|
|
389
471
|
|
|
390
472
|
const isJwtSignatureValid = jwtVerifySignedToken(jwtString, publicKey);
|
|
391
473
|
if (!isJwtSignatureValid) {
|
|
@@ -400,7 +482,9 @@ function tryValidateAndExtractJwtObject(req, publicKey) {
|
|
|
400
482
|
function _validateServerAuthToken(req, customErrorFunction) {
|
|
401
483
|
const authorizationHeader = req?.get?.('Authorization') || req?.get?.('authorization');
|
|
402
484
|
const token = _extractJwtNoThrow(authorizationHeader);
|
|
403
|
-
if (token)
|
|
485
|
+
if (token) {
|
|
486
|
+
return token;
|
|
487
|
+
}
|
|
404
488
|
|
|
405
489
|
throwError(customErrorFunction);
|
|
406
490
|
return null;
|
|
@@ -422,7 +506,9 @@ async function introspectServerAuthToken(serverAuthIntrospector, token, req, cus
|
|
|
422
506
|
function attachServerAuthClaims(req, claims, customErrorFunction) {
|
|
423
507
|
const payload = createServerAuthPayload(claims);
|
|
424
508
|
if (!payload.sub) {
|
|
425
|
-
if (req)
|
|
509
|
+
if (req) {
|
|
510
|
+
req.jwt = null;
|
|
511
|
+
}
|
|
426
512
|
throwError(customErrorFunction);
|
|
427
513
|
}
|
|
428
514
|
|
|
@@ -439,10 +525,9 @@ function attachServerAuthClaims(req, claims, customErrorFunction) {
|
|
|
439
525
|
// Pattern: Projection - copies only authoritative verification names and preserves their exact values.
|
|
440
526
|
function pickEmailVerificationClaims(claims) {
|
|
441
527
|
return Object.fromEntries(
|
|
442
|
-
EMAIL_VERIFICATION_CLAIM_NAMES.filter(claimName =>
|
|
443
|
-
claimName,
|
|
444
|
-
|
|
445
|
-
]),
|
|
528
|
+
EMAIL_VERIFICATION_CLAIM_NAMES.filter(claimName =>
|
|
529
|
+
Object.prototype.hasOwnProperty.call(claims, claimName),
|
|
530
|
+
).map(claimName => [claimName, claims[claimName]]),
|
|
446
531
|
);
|
|
447
532
|
}
|
|
448
533
|
|
|
@@ -462,51 +547,85 @@ function createServerAuthPayload(claims) {
|
|
|
462
547
|
}
|
|
463
548
|
|
|
464
549
|
function readEpochSeconds(value) {
|
|
465
|
-
if (value === undefined || value === null || value === '')
|
|
466
|
-
|
|
550
|
+
if (value === undefined || value === null || value === '') {
|
|
551
|
+
return undefined;
|
|
552
|
+
}
|
|
553
|
+
if (typeof value === 'number') {
|
|
554
|
+
return normalizeSeconds(value);
|
|
555
|
+
}
|
|
467
556
|
const millis = Date.parse(value);
|
|
468
557
|
return Number.isFinite(millis) ? Math.floor(millis / 1000) : undefined;
|
|
469
558
|
}
|
|
470
559
|
|
|
471
560
|
function normalizeSeconds(value) {
|
|
472
|
-
if (!Number.isFinite(value))
|
|
561
|
+
if (!Number.isFinite(value)) {
|
|
562
|
+
return null;
|
|
563
|
+
}
|
|
473
564
|
return value > 1000000000000 ? Math.floor(value / 1000) : Math.floor(value);
|
|
474
565
|
}
|
|
475
566
|
|
|
476
567
|
function _isServiceJwtFor(payload, expectedIssuer, expectedAudience) {
|
|
477
|
-
if (!payload)
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
if (
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
if (
|
|
568
|
+
if (!payload) {
|
|
569
|
+
return false;
|
|
570
|
+
}
|
|
571
|
+
if (payload.iss !== expectedIssuer) {
|
|
572
|
+
return false;
|
|
573
|
+
}
|
|
574
|
+
if (payload.sub !== expectedIssuer) {
|
|
575
|
+
return false;
|
|
576
|
+
}
|
|
577
|
+
if (!payloadAudienceMatches(payload.aud, expectedAudience)) {
|
|
578
|
+
return false;
|
|
579
|
+
}
|
|
580
|
+
if (isJwtPayloadIssuedInFuture(payload)) {
|
|
581
|
+
return false;
|
|
582
|
+
}
|
|
583
|
+
if (isJwtPayloadExpired(payload)) {
|
|
584
|
+
return false;
|
|
585
|
+
}
|
|
586
|
+
if (isJwtPayloadNotYetValid(payload)) {
|
|
587
|
+
return false;
|
|
588
|
+
}
|
|
484
589
|
return true;
|
|
485
590
|
}
|
|
486
591
|
|
|
487
592
|
function payloadAudienceMatches(actualAudience, expectedAudience) {
|
|
488
|
-
if (Array.isArray(actualAudience))
|
|
593
|
+
if (Array.isArray(actualAudience)) {
|
|
594
|
+
return actualAudience.includes(expectedAudience);
|
|
595
|
+
}
|
|
489
596
|
return actualAudience === expectedAudience;
|
|
490
597
|
}
|
|
491
598
|
|
|
492
599
|
function isJwtPayloadExpired(payload) {
|
|
493
|
-
if (!payload.exp)
|
|
600
|
+
if (!payload.exp) {
|
|
601
|
+
return true;
|
|
602
|
+
}
|
|
494
603
|
const exp = normalizeSeconds(payload.exp);
|
|
495
|
-
if (!Number.isInteger(exp))
|
|
604
|
+
if (!Number.isInteger(exp)) {
|
|
605
|
+
return true;
|
|
606
|
+
}
|
|
496
607
|
return Math.floor(Date.now() / 1000) >= exp;
|
|
497
608
|
}
|
|
498
609
|
|
|
499
610
|
function isJwtPayloadIssuedInFuture(payload) {
|
|
500
|
-
if (!payload.iat)
|
|
611
|
+
if (!payload.iat) {
|
|
612
|
+
return true;
|
|
613
|
+
}
|
|
501
614
|
const iat = normalizeSeconds(payload.iat);
|
|
502
|
-
if (!Number.isInteger(iat))
|
|
615
|
+
if (!Number.isInteger(iat)) {
|
|
616
|
+
return true;
|
|
617
|
+
}
|
|
503
618
|
return Math.floor(Date.now() / 1000) < iat;
|
|
504
619
|
}
|
|
505
620
|
|
|
506
621
|
function isJwtPayloadNotYetValid(payload) {
|
|
507
|
-
if (!payload.nbf)
|
|
622
|
+
if (!payload.nbf) {
|
|
623
|
+
return false;
|
|
624
|
+
}
|
|
508
625
|
const nbf = normalizeSeconds(payload.nbf);
|
|
509
|
-
if (!Number.isInteger(nbf))
|
|
626
|
+
if (!Number.isInteger(nbf)) {
|
|
627
|
+
return true;
|
|
628
|
+
}
|
|
510
629
|
return Math.floor(Date.now() / 1000) < nbf;
|
|
511
630
|
}
|
|
512
631
|
|
|
@@ -520,7 +639,9 @@ function createUserAuthorizationConfig(publicKey, options) {
|
|
|
520
639
|
|
|
521
640
|
// Pattern: Factory - detects optional user authorization configuration without affecting legacy callers.
|
|
522
641
|
function createOptionalUserAuthorizationConfig(options) {
|
|
523
|
-
if (!options || !Object.prototype.hasOwnProperty.call(options, 'userAuthorization'))
|
|
642
|
+
if (!options || !Object.prototype.hasOwnProperty.call(options, 'userAuthorization')) {
|
|
643
|
+
return null;
|
|
644
|
+
}
|
|
524
645
|
return normalizeUserAuthorizationConfig(options.userAuthorization || {});
|
|
525
646
|
}
|
|
526
647
|
|
|
@@ -529,7 +650,10 @@ function normalizeUserAuthorizationConfig(options) {
|
|
|
529
650
|
return {
|
|
530
651
|
publicKey: options?.publicKey,
|
|
531
652
|
headerName: options?.headerName || DEFAULT_USER_AUTHORIZATION_HEADER_NAME,
|
|
532
|
-
maxTokenLength: normalizePositiveInteger(
|
|
653
|
+
maxTokenLength: normalizePositiveInteger(
|
|
654
|
+
options?.maxTokenLength,
|
|
655
|
+
DEFAULT_USER_AUTHORIZATION_MAX_TOKEN_LENGTH,
|
|
656
|
+
),
|
|
533
657
|
expectedType: options?.expectedType,
|
|
534
658
|
expectedIssuer: options?.expectedIssuer,
|
|
535
659
|
expectedAudience: options?.expectedAudience,
|
|
@@ -546,35 +670,59 @@ function normalizePositiveInteger(value, defaultValue) {
|
|
|
546
670
|
function readUserAuthorizationHeader(req, config) {
|
|
547
671
|
const rawToken = getRequestHeader(req, config.headerName);
|
|
548
672
|
const present = rawToken !== undefined && rawToken !== null;
|
|
549
|
-
if (!present || typeof rawToken !== 'string')
|
|
673
|
+
if (!present || typeof rawToken !== 'string') {
|
|
674
|
+
return { present, token: null };
|
|
675
|
+
}
|
|
550
676
|
|
|
551
677
|
const token = rawToken.trim();
|
|
552
|
-
if (!token || token.length > config.maxTokenLength || !isJwtString(token))
|
|
678
|
+
if (!token || token.length > config.maxTokenLength || !isJwtString(token)) {
|
|
679
|
+
return { present: true, token: null };
|
|
680
|
+
}
|
|
553
681
|
return { present: true, token };
|
|
554
682
|
}
|
|
555
683
|
|
|
556
684
|
// Pattern: Adapter - supports Express case-insensitive headers and simple test doubles.
|
|
557
685
|
function getRequestHeader(req, headerName) {
|
|
558
686
|
const value = req?.get?.(headerName);
|
|
559
|
-
if (value !== undefined && value !== null)
|
|
687
|
+
if (value !== undefined && value !== null) {
|
|
688
|
+
return value;
|
|
689
|
+
}
|
|
560
690
|
|
|
561
691
|
const lowerCaseHeaderName = headerName.toLowerCase();
|
|
562
|
-
if (lowerCaseHeaderName === headerName)
|
|
692
|
+
if (lowerCaseHeaderName === headerName) {
|
|
693
|
+
return value;
|
|
694
|
+
}
|
|
563
695
|
return req?.get?.(lowerCaseHeaderName);
|
|
564
696
|
}
|
|
565
697
|
|
|
566
698
|
// Pattern: Single Responsibility - verifies signature and registered JWT time claims for user authorization.
|
|
567
699
|
function isUserAuthorizationTokenAllowed(token, config) {
|
|
568
|
-
if (!token || !config.publicKey || !jwtVerifySignedToken(token, config.publicKey))
|
|
700
|
+
if (!token || !config.publicKey || !jwtVerifySignedToken(token, config.publicKey)) {
|
|
701
|
+
return false;
|
|
702
|
+
}
|
|
569
703
|
|
|
570
704
|
const payload = readVerifiedJwtPayload(token);
|
|
571
|
-
if (!payload)
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
if (
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
if (
|
|
705
|
+
if (!payload) {
|
|
706
|
+
return false;
|
|
707
|
+
}
|
|
708
|
+
if (isJwtPayloadIssuedInFuture(payload)) {
|
|
709
|
+
return false;
|
|
710
|
+
}
|
|
711
|
+
if (isJwtPayloadExpired(payload)) {
|
|
712
|
+
return false;
|
|
713
|
+
}
|
|
714
|
+
if (isJwtPayloadNotYetValid(payload)) {
|
|
715
|
+
return false;
|
|
716
|
+
}
|
|
717
|
+
if (config.expectedType && payload.typ !== config.expectedType) {
|
|
718
|
+
return false;
|
|
719
|
+
}
|
|
720
|
+
if (config.expectedIssuer && payload.iss !== config.expectedIssuer) {
|
|
721
|
+
return false;
|
|
722
|
+
}
|
|
723
|
+
if (!expectedAudienceMatches(payload.aud, config.expectedAudience)) {
|
|
724
|
+
return false;
|
|
725
|
+
}
|
|
578
726
|
|
|
579
727
|
return true;
|
|
580
728
|
}
|
|
@@ -586,14 +734,20 @@ function readVerifiedJwtPayload(token) {
|
|
|
586
734
|
|
|
587
735
|
// Pattern: Pure Function - supports either one expected audience or a small allowed set.
|
|
588
736
|
function expectedAudienceMatches(actualAudience, expectedAudience) {
|
|
589
|
-
if (expectedAudience === undefined || expectedAudience === null || expectedAudience === '')
|
|
590
|
-
|
|
737
|
+
if (expectedAudience === undefined || expectedAudience === null || expectedAudience === '') {
|
|
738
|
+
return true;
|
|
739
|
+
}
|
|
740
|
+
if (Array.isArray(expectedAudience)) {
|
|
741
|
+
return expectedAudience.some(audience => payloadAudienceMatches(actualAudience, audience));
|
|
742
|
+
}
|
|
591
743
|
return payloadAudienceMatches(actualAudience, expectedAudience);
|
|
592
744
|
}
|
|
593
745
|
|
|
594
746
|
function _validateGenericNoThrow(req, headerName, extractor) {
|
|
595
747
|
let jwtRaw = req.get(headerName);
|
|
596
|
-
if (!jwtRaw)
|
|
748
|
+
if (!jwtRaw) {
|
|
749
|
+
return null;
|
|
750
|
+
}
|
|
597
751
|
let jwt = extractor(jwtRaw);
|
|
598
752
|
return isJwtString(jwt) ? jwt : null;
|
|
599
753
|
}
|
|
@@ -601,7 +755,9 @@ function _validateGenericNoThrow(req, headerName, extractor) {
|
|
|
601
755
|
function _validateGeneric(req, headerName, extractor, customErrorFunction) {
|
|
602
756
|
let jwtRaw = req.get(headerName);
|
|
603
757
|
let jwt = extractor(jwtRaw, customErrorFunction);
|
|
604
|
-
if (isJwtString(jwt))
|
|
758
|
+
if (isJwtString(jwt)) {
|
|
759
|
+
return jwt;
|
|
760
|
+
}
|
|
605
761
|
throwError(customErrorFunction);
|
|
606
762
|
return null;
|
|
607
763
|
}
|
|
@@ -617,7 +773,9 @@ function _validateAndExtractGenericNoThrow(req, publicKey, validator, extractor,
|
|
|
617
773
|
req[propertyName] = null;
|
|
618
774
|
}
|
|
619
775
|
} catch (err) {
|
|
620
|
-
if (req)
|
|
776
|
+
if (req) {
|
|
777
|
+
req[propertyName] = null;
|
|
778
|
+
}
|
|
621
779
|
throw err;
|
|
622
780
|
}
|
|
623
781
|
return req;
|
|
@@ -629,7 +787,9 @@ function _extractGenericObjectNoThrow(req, jwt, attacher, propertyName) {
|
|
|
629
787
|
attacher(obj);
|
|
630
788
|
req[propertyName] = obj;
|
|
631
789
|
} else {
|
|
632
|
-
if (req)
|
|
790
|
+
if (req) {
|
|
791
|
+
req[propertyName] = null;
|
|
792
|
+
}
|
|
633
793
|
}
|
|
634
794
|
}
|
|
635
795
|
|
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')
|
|
48
|
-
|
|
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.
|
|
3
|
+
"version": "3.19.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/
|
|
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
|
-
"
|
|
18
|
-
"format
|
|
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,15 +31,18 @@
|
|
|
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": "
|
|
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
|
|
42
|
+
"lint-staged": "17.2.0",
|
|
38
43
|
"mocha": "11.7.6",
|
|
39
44
|
"nyc": "18.0.0",
|
|
40
|
-
"prettier": "3.
|
|
45
|
+
"prettier": "3.9.6",
|
|
41
46
|
"ts-node": "10.9.2",
|
|
42
47
|
"typescript": "6.0.3"
|
|
43
48
|
},
|
|
@@ -48,8 +53,9 @@
|
|
|
48
53
|
},
|
|
49
54
|
"overrides": {
|
|
50
55
|
"diff": "8.0.4",
|
|
51
|
-
"
|
|
56
|
+
"brace-expansion": "5.0.9",
|
|
57
|
+
"minimatch": "10.2.6",
|
|
52
58
|
"serialize-javascript": "7.0.5",
|
|
53
|
-
"js-yaml": "4.3.
|
|
59
|
+
"js-yaml": "4.3.1"
|
|
54
60
|
}
|
|
55
61
|
}
|
package/readme.md
CHANGED
|
@@ -14,7 +14,9 @@ introspected by `ms-auth`.
|
|
|
14
14
|
|
|
15
15
|
## Development Rule
|
|
16
16
|
|
|
17
|
-
Non-negotiable TDD rule: Always write the failing test first, run it to confirm it fails for the intended reason, then implement the code and rerun the test until it passes. Test Driven Development is required for all coding work and must not be skipped. For documentation- or skill-only edits,
|
|
17
|
+
Non-negotiable TDD rule: Always write the failing test first, run it to confirm it fails for the intended reason, then implement the code and rerun the test until it passes. Test Driven Development is required for all coding work and must not be skipped. For documentation- or skill-only edits, run the relevant focused non-test
|
|
18
|
+
validation before changing the prose; do not add automated tests that inspect
|
|
19
|
+
prose, files, or repository structure.
|
|
18
20
|
|
|
19
21
|
Non-negotiable repository isolation rule: Every repository must run its Husky hooks and tests using only files, code, fixtures, dependencies, and services contained within that repository. Tests and Husky scripts must not import, require, read, execute, or otherwise depend on sibling repositories or paths outside the repository root. app-e2e-tests is the only exception because cross-repository end-to-end testing is its explicit responsibility.
|
|
20
22
|
|
|
@@ -249,3 +251,22 @@ immediately when no helper remains, allow only a bounded 250 ms settlement
|
|
|
249
251
|
window for already-stopping helpers, fail persistent descendants, preserve
|
|
250
252
|
failures and output, use exit code `124` only for a real outer deadline, and
|
|
251
253
|
remain a final guard rather than a substitute for explicit cleanup.
|
|
254
|
+
|
|
255
|
+
## TDD And Validation
|
|
256
|
+
|
|
257
|
+
Test Driven Development is a non-negotiable requirement.
|
|
258
|
+
|
|
259
|
+
The sole purpose of automated tests is to verify observable functionality and externally visible behavior.
|
|
260
|
+
Tests must validate what the system does through its public interfaces and expected outcomes.
|
|
261
|
+
|
|
262
|
+
Tests must not assert, inspect, or depend on implementation details, including but not limited to:
|
|
263
|
+
|
|
264
|
+
- The existence of specific lines of code, statements, functions, classes, files, or modules.
|
|
265
|
+
- Specific algorithms, control flow, variable names, method calls, code snippets, or internal implementation choices.
|
|
266
|
+
- Any internal structure that can change without changing externally observable behavior.
|
|
267
|
+
|
|
268
|
+
A correct implementation may be completely rewritten or refactored without requiring changes to functional tests, provided its externally observable behavior remains unchanged.
|
|
269
|
+
|
|
270
|
+
Any test that fails solely because the implementation changed while the externally observable behavior remained correct is incorrectly designed and must be rewritten or removed.
|
|
271
|
+
|
|
272
|
+
This requirement is mandatory for all new tests and must be applied whenever existing tests are modified.
|