@oxy.so/core 1.3.1 → 1.5.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/dist/cjs/.tsbuildinfo +1 -1
- package/dist/cjs/mixins/OxyServices.agency.js +41 -0
- package/dist/cjs/mixins/OxyServices.utility.js +173 -6
- package/dist/cjs/server/auth.js +29 -0
- package/dist/cjs/server/index.js +17 -1
- package/dist/cjs/server/requesterAssertion.js +362 -0
- package/dist/esm/.tsbuildinfo +1 -1
- package/dist/esm/mixins/OxyServices.agency.js +41 -0
- package/dist/esm/mixins/OxyServices.utility.js +173 -6
- package/dist/esm/server/auth.js +28 -0
- package/dist/esm/server/index.js +4 -1
- package/dist/esm/server/requesterAssertion.js +353 -0
- package/dist/types/.tsbuildinfo +1 -1
- package/dist/types/index.d.ts +2 -2
- package/dist/types/mixins/OxyServices.agency.d.ts +41 -0
- package/dist/types/mixins/OxyServices.utility.d.ts +49 -0
- package/dist/types/server/auth.d.ts +19 -1
- package/dist/types/server/index.d.ts +4 -2
- package/dist/types/server/requesterAssertion.d.ts +151 -0
- package/package.json +1 -1
- package/src/index.ts +3 -1
- package/src/mixins/OxyServices.agency.ts +76 -0
- package/src/mixins/OxyServices.utility.ts +226 -5
- package/src/mixins/__tests__/authRefusalObservability.test.ts +396 -0
- package/src/server/__tests__/requesterAssertion.test.ts +359 -0
- package/src/server/auth.ts +37 -1
- package/src/server/index.ts +29 -0
- package/src/server/requesterAssertion.ts +465 -0
|
@@ -90,6 +90,44 @@ export interface ServiceApp {
|
|
|
90
90
|
environment: OxyServiceEnvironment;
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
+
/**
|
|
94
|
+
* Why a request's credential was refused, recorded on the request itself.
|
|
95
|
+
*
|
|
96
|
+
* The middleware answers a fixed, deliberately uninformative body to the
|
|
97
|
+
* client; this is the other half of that trade. A refusal is ALWAYS observable
|
|
98
|
+
* to the HOST: it is logged at `warn` with a stable `code`, handed to
|
|
99
|
+
* `onRefusal`, and left on `req.oxyAuthRefusal` for the host's own logs.
|
|
100
|
+
*
|
|
101
|
+
* It exists because the opposite cost was measured: Oxy served
|
|
102
|
+
* `{"keys":[]}` from `/.well-known/jwks.json` with no Ed25519 signing key
|
|
103
|
+
* bound, every service token failed `Oxy service-token key set is unavailable`,
|
|
104
|
+
* and on the OPTIONAL path that failure fell straight through to `next()`.
|
|
105
|
+
* The host then answered its own generic 401 and logged nothing at all, so the
|
|
106
|
+
* one fact that named the fault — an empty key set — existed nowhere. Hours.
|
|
107
|
+
*
|
|
108
|
+
* **Nothing here is ever sent to a client, and `reason` never contains the
|
|
109
|
+
* token, a signature, a secret or a session id.** It carries the SDK's own
|
|
110
|
+
* failure message (`kid` unknown, audience mismatch, key set unavailable) plus
|
|
111
|
+
* the ids already considered safe to log elsewhere in this middleware.
|
|
112
|
+
*/
|
|
113
|
+
export interface OxyAuthRefusal {
|
|
114
|
+
/** Stable, greppable code. Matches the `code` a non-optional refusal answers. */
|
|
115
|
+
code: string;
|
|
116
|
+
/** Which credential lane refused. */
|
|
117
|
+
stage: 'token' | 'service-token' | 'session';
|
|
118
|
+
/** Human-readable cause. Never a token, signature, secret or session id. */
|
|
119
|
+
reason: string;
|
|
120
|
+
/** The status a NON-optional mount would have answered with. */
|
|
121
|
+
status: number;
|
|
122
|
+
/**
|
|
123
|
+
* `true` when the middleware was mounted with `optional: true`, so this
|
|
124
|
+
* refusal did not produce a response — the request continued unauthenticated
|
|
125
|
+
* and whatever the host does next (typically its own generic 401) is the
|
|
126
|
+
* only thing the client sees.
|
|
127
|
+
*/
|
|
128
|
+
optional: boolean;
|
|
129
|
+
}
|
|
130
|
+
|
|
93
131
|
/**
|
|
94
132
|
* Expected JWT audience for tokens issued by the Oxy auth service.
|
|
95
133
|
*/
|
|
@@ -151,6 +189,17 @@ interface AuthMiddlewareOptions {
|
|
|
151
189
|
loadUser?: boolean;
|
|
152
190
|
/** Optional auth - attach user if token present but don't block (default: false) */
|
|
153
191
|
optional?: boolean;
|
|
192
|
+
/**
|
|
193
|
+
* Called whenever a PRESENTED credential is refused, on the optional path as
|
|
194
|
+
* well as the blocking one. Runs before the response (if any) and after
|
|
195
|
+
* `req.oxyAuthRefusal` is set, so a host can raise its own alert or carry the
|
|
196
|
+
* code into its request log. Throwing from it is swallowed — an observer must
|
|
197
|
+
* not be able to turn a refusal into a 500.
|
|
198
|
+
*
|
|
199
|
+
* Refusal is about a credential that was offered and rejected: a request with
|
|
200
|
+
* NO `Authorization` header is not refused and does not reach this.
|
|
201
|
+
*/
|
|
202
|
+
onRefusal?: (refusal: OxyAuthRefusal) => void;
|
|
154
203
|
/**
|
|
155
204
|
* Public JWKS endpoint used for Ed25519 service-token verification. Defaults
|
|
156
205
|
* to `/.well-known/jwks.json` on this Oxy client's configured API origin.
|
|
@@ -355,6 +404,7 @@ export function OxyServicesUtilityMixin<T extends typeof OxyServicesBase>(Base:
|
|
|
355
404
|
const {
|
|
356
405
|
debug = false,
|
|
357
406
|
onError,
|
|
407
|
+
onRefusal,
|
|
358
408
|
loadUser = false,
|
|
359
409
|
optional = false,
|
|
360
410
|
jwtSecret,
|
|
@@ -366,6 +416,50 @@ export function OxyServicesUtilityMixin<T extends typeof OxyServicesBase>(Base:
|
|
|
366
416
|
// composed OxyServices we know we have at runtime.
|
|
367
417
|
const oxyInstance = this as unknown as OxyAuthInstance;
|
|
368
418
|
|
|
419
|
+
/**
|
|
420
|
+
* Make a refusal observable to the HOST, without telling the client
|
|
421
|
+
* anything it is not already told.
|
|
422
|
+
*
|
|
423
|
+
* Called at the top of every branch that rejects a PRESENTED credential,
|
|
424
|
+
* including the ones `optional` then swallows — those are the expensive
|
|
425
|
+
* ones, because they leave no trace anywhere else. The response bodies
|
|
426
|
+
* below are untouched by design: what a client sees does not change.
|
|
427
|
+
*
|
|
428
|
+
* `warn`, not `debug`: a debug-gated reason is only there for whoever
|
|
429
|
+
* already suspects this middleware, and the failure mode this exists for
|
|
430
|
+
* is the one where nobody does. The volume is bounded by refusals, and a
|
|
431
|
+
* host drowning in them has a fault worth the lines.
|
|
432
|
+
*/
|
|
433
|
+
const recordRefusal = (
|
|
434
|
+
req: AuthReq,
|
|
435
|
+
refusal: { code: string; stage: OxyAuthRefusal['stage']; reason: string; status: number },
|
|
436
|
+
): void => {
|
|
437
|
+
const recorded: OxyAuthRefusal = { ...refusal, optional };
|
|
438
|
+
req.oxyAuthRefusal = recorded;
|
|
439
|
+
logger.warn(`[oxy.auth] refused ${recorded.code}: ${recorded.reason}`, {
|
|
440
|
+
component: 'auth',
|
|
441
|
+
method: 'auth',
|
|
442
|
+
code: recorded.code,
|
|
443
|
+
stage: recorded.stage,
|
|
444
|
+
reason: recorded.reason,
|
|
445
|
+
status: recorded.status,
|
|
446
|
+
// Says whether the client will see this status or the host's own
|
|
447
|
+
// generic answer — the difference that made the JWKS outage invisible.
|
|
448
|
+
optional: recorded.optional,
|
|
449
|
+
path: req.path,
|
|
450
|
+
});
|
|
451
|
+
if (onRefusal) {
|
|
452
|
+
try {
|
|
453
|
+
onRefusal(recorded);
|
|
454
|
+
} catch (observerError) {
|
|
455
|
+
logger.warn('[oxy.auth] onRefusal observer threw', {
|
|
456
|
+
component: 'auth',
|
|
457
|
+
method: 'auth',
|
|
458
|
+
}, observerError);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
};
|
|
462
|
+
|
|
369
463
|
// Return an async middleware function
|
|
370
464
|
return async (req: AuthReq, res: AuthRes, next: AuthNext) => {
|
|
371
465
|
try {
|
|
@@ -405,6 +499,12 @@ export function OxyServicesUtilityMixin<T extends typeof OxyServicesBase>(Base:
|
|
|
405
499
|
try {
|
|
406
500
|
decoded = jwtDecode<JwtPayload>(token);
|
|
407
501
|
} catch (decodeError) {
|
|
502
|
+
recordRefusal(req, {
|
|
503
|
+
code: 'INVALID_TOKEN_FORMAT',
|
|
504
|
+
stage: 'token',
|
|
505
|
+
reason: 'Bearer credential is not a decodable JWT',
|
|
506
|
+
status: 401,
|
|
507
|
+
});
|
|
408
508
|
if (debug) {
|
|
409
509
|
logger.debug('[oxy.auth] Token decode failed', {
|
|
410
510
|
component: 'auth',
|
|
@@ -453,6 +553,12 @@ export function OxyServicesUtilityMixin<T extends typeof OxyServicesBase>(Base:
|
|
|
453
553
|
if (
|
|
454
554
|
verifyError instanceof ServiceTokenConfigurationError
|
|
455
555
|
) {
|
|
556
|
+
recordRefusal(req, {
|
|
557
|
+
code: 'SERVICE_TOKEN_NOT_CONFIGURED',
|
|
558
|
+
stage: 'service-token',
|
|
559
|
+
reason: verifyError.message,
|
|
560
|
+
status: 403,
|
|
561
|
+
});
|
|
456
562
|
if (optional) {
|
|
457
563
|
req.userId = null;
|
|
458
564
|
req.user = null;
|
|
@@ -472,6 +578,20 @@ export function OxyServicesUtilityMixin<T extends typeof OxyServicesBase>(Base:
|
|
|
472
578
|
verifyError instanceof ServiceTokenSignatureError ||
|
|
473
579
|
verifyError instanceof ServiceTokenClaimError
|
|
474
580
|
) {
|
|
581
|
+
const code = verifyError instanceof ServiceTokenClaimError
|
|
582
|
+
? 'INVALID_SERVICE_TOKEN_CLAIMS'
|
|
583
|
+
: 'INVALID_SERVICE_TOKEN';
|
|
584
|
+
// The SDK's own message is the whole diagnostic value here —
|
|
585
|
+
// "signing key is unknown" (rotated/unpublished kid), "key set
|
|
586
|
+
// is unavailable" (JWKS empty, unreachable or malformed) and
|
|
587
|
+
// "audience mismatch" are three different outages that answer
|
|
588
|
+
// the client identically.
|
|
589
|
+
recordRefusal(req, {
|
|
590
|
+
code,
|
|
591
|
+
stage: 'service-token',
|
|
592
|
+
reason: `${verifyError.name}: ${verifyError.message}`,
|
|
593
|
+
status: 401,
|
|
594
|
+
});
|
|
475
595
|
if (debug) {
|
|
476
596
|
logger.debug('[oxy.auth] Service token rejected', {
|
|
477
597
|
component: 'auth',
|
|
@@ -485,11 +605,6 @@ export function OxyServicesUtilityMixin<T extends typeof OxyServicesBase>(Base:
|
|
|
485
605
|
req.user = null;
|
|
486
606
|
return next();
|
|
487
607
|
}
|
|
488
|
-
const code = verifyError instanceof ServiceTokenSignatureError
|
|
489
|
-
? 'INVALID_SERVICE_TOKEN'
|
|
490
|
-
: verifyError instanceof ServiceTokenStructureError
|
|
491
|
-
? 'INVALID_SERVICE_TOKEN'
|
|
492
|
-
: 'INVALID_SERVICE_TOKEN_CLAIMS';
|
|
493
608
|
const error = {
|
|
494
609
|
error: code,
|
|
495
610
|
message: verifyError.message,
|
|
@@ -517,6 +632,14 @@ export function OxyServicesUtilityMixin<T extends typeof OxyServicesBase>(Base:
|
|
|
517
632
|
// Check expiration — reject tokens at exact expiry second (use <=)
|
|
518
633
|
const now = Math.floor(Date.now() / 1000);
|
|
519
634
|
if (!Number.isInteger(decoded.exp) || (decoded.exp as number) <= now) {
|
|
635
|
+
recordRefusal(req, {
|
|
636
|
+
code: 'TOKEN_EXPIRED',
|
|
637
|
+
stage: 'service-token',
|
|
638
|
+
reason: Number.isInteger(decoded.exp)
|
|
639
|
+
? 'Service token expired'
|
|
640
|
+
: 'Service token has no integer exp claim',
|
|
641
|
+
status: 401,
|
|
642
|
+
});
|
|
520
643
|
if (optional) {
|
|
521
644
|
req.userId = null;
|
|
522
645
|
req.user = null;
|
|
@@ -527,6 +650,12 @@ export function OxyServicesUtilityMixin<T extends typeof OxyServicesBase>(Base:
|
|
|
527
650
|
return res.status(401).json(error);
|
|
528
651
|
}
|
|
529
652
|
if (decoded.nbf !== undefined && (!Number.isInteger(decoded.nbf) || decoded.nbf > now)) {
|
|
653
|
+
recordRefusal(req, {
|
|
654
|
+
code: 'INVALID_SERVICE_TOKEN_CLAIMS',
|
|
655
|
+
stage: 'service-token',
|
|
656
|
+
reason: 'Service token is not yet valid (nbf)',
|
|
657
|
+
status: 401,
|
|
658
|
+
});
|
|
530
659
|
const error = { error: 'INVALID_SERVICE_TOKEN_CLAIMS', message: 'Service token is not yet valid', code: 'INVALID_SERVICE_TOKEN_CLAIMS', status: 401 };
|
|
531
660
|
if (onError) return onError(error);
|
|
532
661
|
return res.status(401).json(error);
|
|
@@ -550,6 +679,22 @@ export function OxyServicesUtilityMixin<T extends typeof OxyServicesBase>(Base:
|
|
|
550
679
|
|| !decoded.scopes.every((scope) => typeof scope === 'string' && scope.length > 0 && scope === scope.trim())
|
|
551
680
|
|| new Set(decoded.scopes).size !== decoded.scopes.length
|
|
552
681
|
) {
|
|
682
|
+
recordRefusal(req, {
|
|
683
|
+
code: 'INVALID_SERVICE_TOKEN',
|
|
684
|
+
stage: 'service-token',
|
|
685
|
+
// Names the field rather than its value: an exact-match claim
|
|
686
|
+
// failing on invisible whitespace reads as "present" in a log
|
|
687
|
+
// that only says which claims were missing.
|
|
688
|
+
reason: `Service token claims unusable (${[
|
|
689
|
+
!isExactNonEmptyServiceClaim(appId) ? 'appId' : null,
|
|
690
|
+
!isExactNonEmptyServiceClaim(decoded.appName) ? 'appName' : null,
|
|
691
|
+
!isExactNonEmptyServiceClaim(credentialId) ? 'credentialId' : null,
|
|
692
|
+
!isExactNonEmptyServiceClaim(ownerAccountId) ? 'ownerAccountId' : null,
|
|
693
|
+
!isOxyServiceEnvironment(environment) ? 'environment' : null,
|
|
694
|
+
!Array.isArray(decoded.scopes) ? 'scopes' : null,
|
|
695
|
+
].filter((field) => field !== null).join(', ') || 'scopes'})`,
|
|
696
|
+
status: 401,
|
|
697
|
+
});
|
|
553
698
|
if (optional) {
|
|
554
699
|
req.userId = null;
|
|
555
700
|
req.user = null;
|
|
@@ -564,6 +709,12 @@ export function OxyServicesUtilityMixin<T extends typeof OxyServicesBase>(Base:
|
|
|
564
709
|
const oxyUserIdRaw = req.headers['x-oxy-user-id'];
|
|
565
710
|
const oxyUserId = isExactNonEmptyServiceClaim(oxyUserIdRaw) ? oxyUserIdRaw : null;
|
|
566
711
|
if (oxyUserIdRaw !== undefined && oxyUserId === null) {
|
|
712
|
+
recordRefusal(req, {
|
|
713
|
+
code: 'INVALID_SERVICE_TOKEN_CLAIMS',
|
|
714
|
+
stage: 'service-token',
|
|
715
|
+
reason: 'X-Oxy-User-Id is not an exact non-empty id',
|
|
716
|
+
status: 401,
|
|
717
|
+
});
|
|
567
718
|
const error = {
|
|
568
719
|
error: 'INVALID_SERVICE_TOKEN_CLAIMS',
|
|
569
720
|
message: 'Delegated user id must be an exact non-empty id',
|
|
@@ -588,6 +739,16 @@ export function OxyServicesUtilityMixin<T extends typeof OxyServicesBase>(Base:
|
|
|
588
739
|
appId,
|
|
589
740
|
attemptedUserId: oxyUserId,
|
|
590
741
|
});
|
|
742
|
+
// A verifier with no service credentials of its own cannot
|
|
743
|
+
// reach the verify endpoint at all, so `null` here can mean
|
|
744
|
+
// "no grant" OR "this host cannot ask". Both refuse, and both
|
|
745
|
+
// are worth naming on the request.
|
|
746
|
+
recordRefusal(req, {
|
|
747
|
+
code: 'SERVICE_ACTING_AS_UNAUTHORIZED',
|
|
748
|
+
stage: 'service-token',
|
|
749
|
+
reason: `No delegation grant for app ${appId} acting as ${oxyUserId} (or this verifier could not reach the grant check)`,
|
|
750
|
+
status: 403,
|
|
751
|
+
});
|
|
591
752
|
const error = {
|
|
592
753
|
error: 'SERVICE_ACTING_AS_UNAUTHORIZED',
|
|
593
754
|
message: 'Service not authorized to act as this user',
|
|
@@ -636,6 +797,12 @@ export function OxyServicesUtilityMixin<T extends typeof OxyServicesBase>(Base:
|
|
|
636
797
|
// compared against the id the validated session resolves to.
|
|
637
798
|
const claimedUserId = readStringClaim(decoded.userId) ?? readStringClaim(decoded.id);
|
|
638
799
|
if (!claimedUserId) {
|
|
800
|
+
recordRefusal(req, {
|
|
801
|
+
code: 'INVALID_TOKEN_PAYLOAD',
|
|
802
|
+
stage: 'token',
|
|
803
|
+
reason: 'Token carries no usable user id claim',
|
|
804
|
+
status: 401,
|
|
805
|
+
});
|
|
639
806
|
if (optional) {
|
|
640
807
|
req.userId = null;
|
|
641
808
|
req.user = null;
|
|
@@ -655,6 +822,12 @@ export function OxyServicesUtilityMixin<T extends typeof OxyServicesBase>(Base:
|
|
|
655
822
|
// Check token expiration locally first (fast path)
|
|
656
823
|
// Reject tokens at exact expiry second (use <=)
|
|
657
824
|
if (decoded.exp && decoded.exp <= Math.floor(Date.now() / 1000)) {
|
|
825
|
+
recordRefusal(req, {
|
|
826
|
+
code: 'TOKEN_EXPIRED',
|
|
827
|
+
stage: 'token',
|
|
828
|
+
reason: 'User access token expired',
|
|
829
|
+
status: 401,
|
|
830
|
+
});
|
|
658
831
|
if (optional) {
|
|
659
832
|
req.userId = null;
|
|
660
833
|
req.user = null;
|
|
@@ -678,6 +851,12 @@ export function OxyServicesUtilityMixin<T extends typeof OxyServicesBase>(Base:
|
|
|
678
851
|
// required this.
|
|
679
852
|
const sessionId = readStringClaim(decoded.sessionId);
|
|
680
853
|
if (!sessionId) {
|
|
854
|
+
recordRefusal(req, {
|
|
855
|
+
code: 'SESSION_REQUIRED',
|
|
856
|
+
stage: 'token',
|
|
857
|
+
reason: 'User access token is not bound to a session',
|
|
858
|
+
status: 401,
|
|
859
|
+
});
|
|
681
860
|
if (optional) {
|
|
682
861
|
req.userId = null;
|
|
683
862
|
req.user = null;
|
|
@@ -702,6 +881,12 @@ export function OxyServicesUtilityMixin<T extends typeof OxyServicesBase>(Base:
|
|
|
702
881
|
});
|
|
703
882
|
|
|
704
883
|
if (!validationResult || !validationResult.valid || !validationResult.user) {
|
|
884
|
+
recordRefusal(req, {
|
|
885
|
+
code: 'INVALID_SESSION',
|
|
886
|
+
stage: 'session',
|
|
887
|
+
reason: 'Session is invalid, revoked or expired',
|
|
888
|
+
status: 401,
|
|
889
|
+
});
|
|
705
890
|
if (optional) {
|
|
706
891
|
req.userId = null;
|
|
707
892
|
req.user = null;
|
|
@@ -721,6 +906,12 @@ export function OxyServicesUtilityMixin<T extends typeof OxyServicesBase>(Base:
|
|
|
721
906
|
// The session — not the token — is the source of truth for identity.
|
|
722
907
|
const validatedUserId = getUserIdentityId(validationResult.user);
|
|
723
908
|
if (!validatedUserId) {
|
|
909
|
+
recordRefusal(req, {
|
|
910
|
+
code: 'INVALID_SESSION',
|
|
911
|
+
stage: 'session',
|
|
912
|
+
reason: 'Session did not resolve to a usable identity',
|
|
913
|
+
status: 401,
|
|
914
|
+
});
|
|
724
915
|
if (optional) {
|
|
725
916
|
req.userId = null;
|
|
726
917
|
req.user = null;
|
|
@@ -747,6 +938,12 @@ export function OxyServicesUtilityMixin<T extends typeof OxyServicesBase>(Base:
|
|
|
747
938
|
claimedUserId,
|
|
748
939
|
validatedUserId,
|
|
749
940
|
});
|
|
941
|
+
recordRefusal(req, {
|
|
942
|
+
code: 'SESSION_USER_MISMATCH',
|
|
943
|
+
stage: 'session',
|
|
944
|
+
reason: 'Token user claim does not own the presented session',
|
|
945
|
+
status: 401,
|
|
946
|
+
});
|
|
750
947
|
|
|
751
948
|
if (optional) {
|
|
752
949
|
req.userId = null;
|
|
@@ -780,6 +977,15 @@ export function OxyServicesUtilityMixin<T extends typeof OxyServicesBase>(Base:
|
|
|
780
977
|
|
|
781
978
|
return next();
|
|
782
979
|
} catch (validationError) {
|
|
980
|
+
recordRefusal(req, {
|
|
981
|
+
code: 'SESSION_VALIDATION_ERROR',
|
|
982
|
+
stage: 'session',
|
|
983
|
+
// Name and transport code only. The message of a failed
|
|
984
|
+
// `validateSession` can carry the request URL, and that URL
|
|
985
|
+
// contains the session id — which is a credential.
|
|
986
|
+
reason: `Session validation call failed (${describeErrorSafely(validationError)})`,
|
|
987
|
+
status: 401,
|
|
988
|
+
});
|
|
783
989
|
if (debug) {
|
|
784
990
|
logger.debug('[oxy.auth] Session validation failed', {
|
|
785
991
|
component: 'auth',
|
|
@@ -1251,6 +1457,20 @@ async function verifyServiceTokenSignature(
|
|
|
1251
1457
|
* those values out of URL construction and identity comparison, so an
|
|
1252
1458
|
* unexpected shape becomes a 401 rather than a stringified surprise.
|
|
1253
1459
|
*/
|
|
1460
|
+
/**
|
|
1461
|
+
* A short, log-safe description of a thrown value: its class name and, when the
|
|
1462
|
+
* transport attached one, its `code` (`ECONNREFUSED`, `ETIMEDOUT`, `ENOTFOUND`
|
|
1463
|
+
* …). Never the message — a failed request's message can quote the URL it was
|
|
1464
|
+
* made against, and a session-validation URL contains the session id.
|
|
1465
|
+
*/
|
|
1466
|
+
function describeErrorSafely(error: unknown): string {
|
|
1467
|
+
if (!(error instanceof Error)) return typeof error;
|
|
1468
|
+
const code = (error as { code?: unknown }).code;
|
|
1469
|
+
return typeof code === 'string' && /^[A-Za-z0-9_]{1,40}$/.test(code)
|
|
1470
|
+
? `${error.name}: ${code}`
|
|
1471
|
+
: error.name;
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1254
1474
|
function readStringClaim(value: unknown): string | null {
|
|
1255
1475
|
return typeof value === 'string' && value.length > 0 ? value : null;
|
|
1256
1476
|
}
|
|
@@ -1315,6 +1535,7 @@ interface AuthReq {
|
|
|
1315
1535
|
sessionId?: string | null;
|
|
1316
1536
|
serviceApp?: ServiceApp;
|
|
1317
1537
|
serviceActingAs?: { userId: string; scopes: string[] };
|
|
1538
|
+
oxyAuthRefusal?: OxyAuthRefusal;
|
|
1318
1539
|
}
|
|
1319
1540
|
|
|
1320
1541
|
interface AuthRes {
|