@oxy.so/core 4.5.0-oxy-one.0 → 4.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/README.md +1 -1
- package/dist/cjs/HttpService.js +63 -19
- package/dist/cjs/api/accounts.js +10 -3
- package/dist/cjs/api/agency.js +3 -1
- package/dist/cjs/api/apps.js +15 -4
- package/dist/cjs/api/assets.js +49 -12
- package/dist/cjs/api/auth.js +37 -11
- package/dist/cjs/api/billing.js +24 -7
- package/dist/cjs/api/civic.js +14 -4
- package/dist/cjs/api/follows.js +9 -2
- package/dist/cjs/api/identity.js +31 -9
- package/dist/cjs/api/nodes.js +3 -1
- package/dist/cjs/api/privacy.js +8 -2
- package/dist/cjs/api/session.js +15 -4
- package/dist/cjs/api/store.js +15 -5
- package/dist/cjs/api/topics.js +10 -3
- package/dist/cjs/api/users.js +16 -4
- package/dist/cjs/boot/sessionColdBoot.js +40 -13
- package/dist/cjs/constants/version.js +6 -6
- package/dist/cjs/crypto/identityMarker.js +3 -1
- package/dist/cjs/crypto/identityProof.js +6 -1
- package/dist/cjs/crypto/keyManager.js +87 -32
- package/dist/cjs/crypto/recoveryPhrase.js +2 -2
- package/dist/cjs/crypto/signatureService.js +3 -4
- package/dist/cjs/i18n/index.js +16 -3
- package/dist/cjs/i18n/locales/ar-SA.js +366 -366
- package/dist/cjs/i18n/locales/ca-ES.js +366 -366
- package/dist/cjs/i18n/locales/de-DE.js +366 -366
- package/dist/cjs/i18n/locales/en-US.js +1994 -1994
- package/dist/cjs/i18n/locales/es-ES.js +1994 -1994
- package/dist/cjs/i18n/locales/fr-FR.js +366 -366
- package/dist/cjs/i18n/locales/it-IT.js +366 -366
- package/dist/cjs/i18n/locales/ja-JP.js +366 -366
- package/dist/cjs/i18n/locales/ko-KR.js +366 -366
- package/dist/cjs/i18n/locales/pt-PT.js +366 -366
- package/dist/cjs/i18n/locales/zh-CN.js +366 -366
- package/dist/cjs/inference/OxyInferenceClient.js +12 -18
- package/dist/cjs/models/interfaces.js +11 -11
- package/dist/cjs/server/OxyServer.js +86 -28
- package/dist/cjs/server/agentAccount.js +6 -2
- package/dist/cjs/server/aliaMachineCredential.js +39 -14
- package/dist/cjs/server/auth.js +5 -4
- package/dist/cjs/server/capabilityTicket.js +5 -5
- package/dist/cjs/server/index.js +2 -1
- package/dist/cjs/server/liveCapabilityTicket.js +4 -1
- package/dist/cjs/server/middleware.js +61 -42
- package/dist/cjs/server/namespaces.js +28 -7
- package/dist/cjs/server/requesterAssertion.js +36 -30
- package/dist/cjs/server/serviceTokenLifetime.js +7 -4
- package/dist/cjs/server/traffic.js +53 -14
- package/dist/cjs/server/trafficNodeHttp.js +33 -8
- package/dist/cjs/server/workloadIdentity.client.js +4 -0
- package/dist/cjs/server/workloadIdentity.js +28 -1
- package/dist/cjs/session/SessionClient.js +54 -17
- package/dist/cjs/session/accountDelegationAccess.js +4 -6
- package/dist/cjs/session/accountDialogController.js +1 -1
- package/dist/cjs/session/authStateStore.js +18 -9
- package/dist/cjs/session/identityPin.js +6 -2
- package/dist/cjs/session/refresh.js +81 -23
- package/dist/cjs/session/sharedDeviceCredential.js +28 -15
- package/dist/cjs/shared/utils/errorUtils.js +1 -1
- package/dist/cjs/shared/utils/networkUtils.js +1 -1
- package/dist/cjs/shared/utils/themeUtils.js +1 -1
- package/dist/cjs/utils/accountCacheSweep.js +3 -1
- package/dist/cjs/utils/accountUtils.js +1 -3
- package/dist/cjs/utils/asyncUtils.js +1 -1
- package/dist/cjs/utils/authHelpers.js +2 -1
- package/dist/cjs/utils/avatarUtils.js +4 -2
- package/dist/cjs/utils/cache.js +1 -1
- package/dist/cjs/utils/cacheKey.js +3 -1
- package/dist/cjs/utils/deviceManager.js +4 -4
- package/dist/cjs/utils/errorUtils.js +6 -4
- package/dist/cjs/utils/languageUtils.js +404 -57
- package/dist/cjs/utils/profileLinks.js +1 -3
- package/dist/cjs/utils/requestUtils.js +7 -1
- package/dist/cjs/utils/userIdentity.js +1 -1
- package/dist/cjs/utils/validationUtils.js +5 -6
- package/dist/esm/HttpService.js +64 -20
- package/dist/esm/api/accounts.js +11 -4
- package/dist/esm/api/agency.js +3 -1
- package/dist/esm/api/apps.js +15 -4
- package/dist/esm/api/assets.js +49 -12
- package/dist/esm/api/auth.js +37 -11
- package/dist/esm/api/billing.js +25 -8
- package/dist/esm/api/civic.js +14 -4
- package/dist/esm/api/follows.js +9 -2
- package/dist/esm/api/identity.js +31 -9
- package/dist/esm/api/nodes.js +3 -1
- package/dist/esm/api/privacy.js +8 -2
- package/dist/esm/api/session.js +15 -4
- package/dist/esm/api/store.js +15 -5
- package/dist/esm/api/topics.js +10 -3
- package/dist/esm/api/users.js +16 -4
- package/dist/esm/boot/sessionColdBoot.js +41 -14
- package/dist/esm/constants/version.js +6 -6
- package/dist/esm/crypto/identityMarker.js +3 -1
- package/dist/esm/crypto/identityProof.js +6 -1
- package/dist/esm/crypto/internal.js +1 -1
- package/dist/esm/crypto/keyManager.js +87 -32
- package/dist/esm/crypto/recoveryPhrase.js +2 -2
- package/dist/esm/crypto/signatureService.js +3 -4
- package/dist/esm/i18n/index.js +16 -3
- package/dist/esm/i18n/locales/ar-SA.js +366 -366
- package/dist/esm/i18n/locales/ca-ES.js +366 -366
- package/dist/esm/i18n/locales/de-DE.js +366 -366
- package/dist/esm/i18n/locales/en-US.js +1994 -1994
- package/dist/esm/i18n/locales/es-ES.js +1994 -1994
- package/dist/esm/i18n/locales/fr-FR.js +366 -366
- package/dist/esm/i18n/locales/it-IT.js +366 -366
- package/dist/esm/i18n/locales/ja-JP.js +366 -366
- package/dist/esm/i18n/locales/ko-KR.js +366 -366
- package/dist/esm/i18n/locales/pt-PT.js +366 -366
- package/dist/esm/i18n/locales/zh-CN.js +366 -366
- package/dist/esm/index.js +1 -1
- package/dist/esm/inference/OxyInferenceClient.js +13 -19
- package/dist/esm/models/interfaces.js +11 -11
- package/dist/esm/server/OxyServer.js +87 -29
- package/dist/esm/server/agentAccount.js +6 -2
- package/dist/esm/server/aliaMachineCredential.js +39 -14
- package/dist/esm/server/auth.js +6 -5
- package/dist/esm/server/capabilityTicket.js +6 -6
- package/dist/esm/server/index.js +5 -5
- package/dist/esm/server/liveCapabilityTicket.js +6 -3
- package/dist/esm/server/middleware.js +62 -43
- package/dist/esm/server/namespaces.js +31 -10
- package/dist/esm/server/requesterAssertion.js +36 -30
- package/dist/esm/server/serviceTokenLifetime.js +7 -4
- package/dist/esm/server/traffic.js +55 -16
- package/dist/esm/server/trafficNodeHttp.js +34 -9
- package/dist/esm/server/userInvalidation.js +1 -1
- package/dist/esm/server/workloadIdentity.client.js +3 -0
- package/dist/esm/server/workloadIdentity.js +27 -1
- package/dist/esm/session/SessionClient.js +54 -17
- package/dist/esm/session/accountDelegationAccess.js +5 -7
- package/dist/esm/session/accountDialogController.js +1 -1
- package/dist/esm/session/authStateStore.js +18 -9
- package/dist/esm/session/identityPin.js +6 -2
- package/dist/esm/session/refresh.js +82 -24
- package/dist/esm/session/sharedDeviceCredential.js +28 -15
- package/dist/esm/shared/utils/errorUtils.js +1 -1
- package/dist/esm/shared/utils/networkUtils.js +1 -1
- package/dist/esm/shared/utils/themeUtils.js +1 -1
- package/dist/esm/utils/accountCacheSweep.js +3 -1
- package/dist/esm/utils/accountUtils.js +1 -3
- package/dist/esm/utils/asyncUtils.js +1 -1
- package/dist/esm/utils/authHelpers.js +2 -1
- package/dist/esm/utils/avatarUtils.js +4 -2
- package/dist/esm/utils/cache.js +1 -1
- package/dist/esm/utils/cacheKey.js +3 -1
- package/dist/esm/utils/deviceManager.js +4 -4
- package/dist/esm/utils/errorUtils.js +6 -4
- package/dist/esm/utils/languageUtils.js +404 -57
- package/dist/esm/utils/profileLinks.js +1 -3
- package/dist/esm/utils/requestUtils.js +7 -1
- package/dist/esm/utils/userIdentity.js +1 -1
- package/dist/esm/utils/validationUtils.js +5 -6
- package/dist/types/HttpService.d.ts +1 -1
- package/dist/types/api/accounts.d.ts +1 -1
- package/dist/types/api/apps.d.ts +4 -4
- package/dist/types/api/assets.d.ts +2 -0
- package/dist/types/api/auth.d.ts +8 -3
- package/dist/types/crypto/internal.d.ts +1 -1
- package/dist/types/index.d.ts +4 -4
- package/dist/types/server/index.d.ts +6 -6
- package/dist/types/server/requesterAssertion.d.ts +2 -2
- package/dist/types/server/workloadIdentity.client.d.ts +5 -1
- package/dist/types/server/workloadIdentity.d.ts +26 -0
- package/dist/types/session/accountDelegationAccess.d.ts +1 -1
- package/dist/types/session/index.d.ts +1 -1
- package/dist/types/utils/userIdentity.d.ts +1 -1
- package/dist/types/utils/validationUtils.d.ts +4 -5
- package/package.json +2 -3
- package/src/HttpService.ts +183 -53
- package/src/OxyServices.errors.ts +7 -3
- package/src/OxyServices.ts +6 -2
- package/src/api/accounts.ts +76 -26
- package/src/api/agency.ts +24 -9
- package/src/api/appData.ts +12 -2
- package/src/api/apps.ts +81 -31
- package/src/api/assets.ts +122 -40
- package/src/api/auth.ts +154 -79
- package/src/api/billing.ts +112 -65
- package/src/api/civic.ts +61 -19
- package/src/api/contacts.ts +4 -1
- package/src/api/devices.ts +54 -21
- package/src/api/follows.ts +88 -31
- package/src/api/identity.ts +101 -33
- package/src/api/linkedAccounts.ts +26 -8
- package/src/api/nodes.ts +18 -6
- package/src/api/notifications.ts +26 -6
- package/src/api/privacy.ts +42 -8
- package/src/api/reputation.ts +18 -7
- package/src/api/session.ts +23 -5
- package/src/api/store.ts +81 -31
- package/src/api/topics.ts +18 -6
- package/src/api/users.ts +98 -35
- package/src/boot/sessionColdBoot.ts +60 -19
- package/src/civic/payloads.ts +1 -4
- package/src/client/context.ts +6 -1
- package/src/constants/version.ts +7 -6
- package/src/crypto/aead.ts +1 -5
- package/src/crypto/ecdh.ts +1 -4
- package/src/crypto/identityDerivations.ts +5 -1
- package/src/crypto/identityMarker.ts +3 -1
- package/src/crypto/identityProof.ts +6 -1
- package/src/crypto/index.ts +11 -11
- package/src/crypto/internal.ts +6 -1
- package/src/crypto/keyManager.ts +348 -98
- package/src/crypto/recoveryPhrase.ts +14 -9
- package/src/crypto/registrationPow.ts +1 -1
- package/src/crypto/signatureService.ts +14 -6
- package/src/i18n/accountCategoryLabels.ts +1 -4
- package/src/i18n/accountRoleLabels.ts +1 -4
- package/src/i18n/index.ts +25 -7
- package/src/i18n/locales/ar-SA.ts +375 -366
- package/src/i18n/locales/ca-ES.ts +382 -366
- package/src/i18n/locales/de-DE.ts +387 -366
- package/src/i18n/locales/en-US.ts +2034 -1994
- package/src/i18n/locales/es-ES.ts +2043 -1994
- package/src/i18n/locales/fr-FR.ts +387 -366
- package/src/i18n/locales/it-IT.ts +380 -366
- package/src/i18n/locales/ja-JP.ts +380 -366
- package/src/i18n/locales/ko-KR.ts +377 -366
- package/src/i18n/locales/pt-PT.ts +383 -366
- package/src/i18n/locales/zh-CN.ts +373 -366
- package/src/i18n/trustTierLabels.ts +1 -4
- package/src/index.ts +371 -349
- package/src/inference/OxyInferenceClient.ts +784 -780
- package/src/models/interfaces.ts +30 -27
- package/src/server/OxyServer.ts +227 -68
- package/src/server/agentAccount.ts +8 -2
- package/src/server/aliaMachineCredential.ts +57 -16
- package/src/server/auth.ts +9 -5
- package/src/server/capabilityTicket.ts +30 -21
- package/src/server/index.ts +29 -6
- package/src/server/liveCapabilityTicket.ts +31 -9
- package/src/server/middleware.ts +212 -121
- package/src/server/namespaces.ts +247 -94
- package/src/server/rateLimit.ts +2 -4
- package/src/server/requesterAssertion.ts +93 -54
- package/src/server/serviceTokenLifetime.ts +9 -4
- package/src/server/traffic.ts +117 -23
- package/src/server/trafficNodeHttp.ts +61 -14
- package/src/server/userInvalidation.ts +1 -4
- package/src/server/workloadIdentity.client.ts +12 -1
- package/src/server/workloadIdentity.ts +52 -5
- package/src/session/SessionClient.ts +170 -49
- package/src/session/accountDelegationAccess.ts +24 -31
- package/src/session/accountDialogController.ts +59 -14
- package/src/session/accountSwitchTargets.ts +10 -15
- package/src/session/authStateStore.ts +121 -81
- package/src/session/deviceDirectory.ts +1 -3
- package/src/session/identityPin.ts +10 -11
- package/src/session/identitySession.ts +3 -1
- package/src/session/index.ts +87 -82
- package/src/session/refresh.ts +138 -44
- package/src/session/sessionClientHost.ts +1 -3
- package/src/session/sharedDeviceCredential.ts +35 -14
- package/src/session/socketLoader.ts +9 -2
- package/src/shared/utils/errorUtils.ts +5 -2
- package/src/shared/utils/networkUtils.ts +5 -8
- package/src/shared/utils/themeUtils.ts +2 -6
- package/src/types/buffer.d.ts +66 -21
- package/src/types/expo-secure-store.d.ts +5 -1
- package/src/utils/accountCacheSweep.ts +4 -5
- package/src/utils/accountUtils.ts +107 -108
- package/src/utils/apiUtils.ts +3 -3
- package/src/utils/asyncUtils.ts +2 -2
- package/src/utils/authHelpers.ts +10 -5
- package/src/utils/avatarUtils.ts +11 -6
- package/src/utils/cache.ts +9 -10
- package/src/utils/cacheKey.ts +5 -1
- package/src/utils/coldBoot.ts +1 -3
- package/src/utils/commonsDelivery.ts +1 -4
- package/src/utils/deviceManager.ts +15 -14
- package/src/utils/errorUtils.ts +68 -86
- package/src/utils/languageUtils.ts +407 -58
- package/src/utils/oauthPkce.ts +1 -4
- package/src/utils/platform.ts +0 -1
- package/src/utils/profileLinks.ts +2 -5
- package/src/utils/requestUtils.ts +18 -14
- package/src/utils/sessionUtils.ts +34 -33
- package/src/utils/userHandle.ts +3 -1
- package/src/utils/userIdentity.ts +1 -1
- package/src/utils/validationUtils.ts +11 -9
package/src/server/middleware.ts
CHANGED
|
@@ -14,7 +14,10 @@ import type { ApiError, User } from '../models/interfaces';
|
|
|
14
14
|
import { loadNodeCrypto } from '@oxy.so/protocol';
|
|
15
15
|
import { logger } from '../logger';
|
|
16
16
|
import { toOxyApiError } from '../OxyServices.errors';
|
|
17
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
OXY_SERVICE_ENVIRONMENTS,
|
|
19
|
+
type OxyServiceEnvironment,
|
|
20
|
+
} from '../utils/oxyServiceEnvironment';
|
|
18
21
|
|
|
19
22
|
interface JwtPayload {
|
|
20
23
|
exp?: number;
|
|
@@ -245,8 +248,7 @@ class ServiceTokenClaimError extends Error {
|
|
|
245
248
|
|
|
246
249
|
function isOxyServiceEnvironment(value: unknown): value is OxyServiceEnvironment {
|
|
247
250
|
return (
|
|
248
|
-
typeof value === 'string' &&
|
|
249
|
-
(OXY_SERVICE_ENVIRONMENTS as readonly string[]).includes(value)
|
|
251
|
+
typeof value === 'string' && (OXY_SERVICE_ENVIRONMENTS as readonly string[]).includes(value)
|
|
250
252
|
);
|
|
251
253
|
}
|
|
252
254
|
|
|
@@ -290,7 +292,6 @@ export interface AuthMiddlewareOptions {
|
|
|
290
292
|
expectedAudience?: string;
|
|
291
293
|
}
|
|
292
294
|
|
|
293
|
-
|
|
294
295
|
/**
|
|
295
296
|
* What the middleware needs from the server it is mounted on. `OxyServer`
|
|
296
297
|
* builds it; every member is resolved at call time, so a test can spy on the
|
|
@@ -303,9 +304,16 @@ export interface OxyMiddlewareHost {
|
|
|
303
304
|
sessionId: string,
|
|
304
305
|
options?: { deviceFingerprint?: string; useHeaderValidation?: boolean },
|
|
305
306
|
): Promise<{ valid: boolean; user?: User; actor?: unknown } | null>;
|
|
306
|
-
verifyActingAs(
|
|
307
|
-
|
|
308
|
-
|
|
307
|
+
verifyActingAs(
|
|
308
|
+
appId: string,
|
|
309
|
+
userId: string,
|
|
310
|
+
options?: {
|
|
311
|
+
cache?: boolean;
|
|
312
|
+
credentialId?: string;
|
|
313
|
+
ownerAccountId?: string;
|
|
314
|
+
environment?: string;
|
|
315
|
+
},
|
|
316
|
+
): Promise<ServiceActingAsVerification | null>;
|
|
309
317
|
}
|
|
310
318
|
|
|
311
319
|
/** Build the middleware set for one server. See `OxyServer.middleware`. */
|
|
@@ -434,10 +442,14 @@ export function createOxyMiddleware(host: OxyMiddlewareHost) {
|
|
|
434
442
|
try {
|
|
435
443
|
onRefusal(recorded);
|
|
436
444
|
} catch (observerError) {
|
|
437
|
-
logger.warn(
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
445
|
+
logger.warn(
|
|
446
|
+
'[oxy.auth] onRefusal observer threw',
|
|
447
|
+
{
|
|
448
|
+
component: 'auth',
|
|
449
|
+
method: 'auth',
|
|
450
|
+
},
|
|
451
|
+
observerError,
|
|
452
|
+
);
|
|
441
453
|
}
|
|
442
454
|
}
|
|
443
455
|
};
|
|
@@ -470,7 +482,7 @@ export function createOxyMiddleware(host: OxyMiddlewareHost) {
|
|
|
470
482
|
error: 'MISSING_TOKEN',
|
|
471
483
|
message: 'Access token required',
|
|
472
484
|
code: 'MISSING_TOKEN',
|
|
473
|
-
status: 401
|
|
485
|
+
status: 401,
|
|
474
486
|
};
|
|
475
487
|
if (onError) return onError(error);
|
|
476
488
|
return res.status(401).json(error);
|
|
@@ -488,10 +500,14 @@ export function createOxyMiddleware(host: OxyMiddlewareHost) {
|
|
|
488
500
|
status: 401,
|
|
489
501
|
});
|
|
490
502
|
if (debug) {
|
|
491
|
-
logger.debug(
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
503
|
+
logger.debug(
|
|
504
|
+
'[oxy.auth] Token decode failed',
|
|
505
|
+
{
|
|
506
|
+
component: 'auth',
|
|
507
|
+
method: 'auth',
|
|
508
|
+
},
|
|
509
|
+
decodeError,
|
|
510
|
+
);
|
|
495
511
|
}
|
|
496
512
|
if (optional) {
|
|
497
513
|
req.userId = null;
|
|
@@ -503,7 +519,7 @@ export function createOxyMiddleware(host: OxyMiddlewareHost) {
|
|
|
503
519
|
error: 'INVALID_TOKEN_FORMAT',
|
|
504
520
|
message: 'Invalid token format',
|
|
505
521
|
code: 'INVALID_TOKEN_FORMAT',
|
|
506
|
-
status: 401
|
|
522
|
+
status: 401,
|
|
507
523
|
};
|
|
508
524
|
if (onError) return onError(error);
|
|
509
525
|
return res.status(401).json(error);
|
|
@@ -536,9 +552,10 @@ export function createOxyMiddleware(host: OxyMiddlewareHost) {
|
|
|
536
552
|
verifyError instanceof ServiceTokenSignatureError ||
|
|
537
553
|
verifyError instanceof ServiceTokenClaimError
|
|
538
554
|
) {
|
|
539
|
-
const code =
|
|
540
|
-
|
|
541
|
-
|
|
555
|
+
const code =
|
|
556
|
+
verifyError instanceof ServiceTokenClaimError
|
|
557
|
+
? 'INVALID_SERVICE_TOKEN_CLAIMS'
|
|
558
|
+
: 'INVALID_SERVICE_TOKEN';
|
|
542
559
|
// The SDK's own message is the whole diagnostic value here —
|
|
543
560
|
// "signing key is unknown" (rotated/unpublished kid), "key set
|
|
544
561
|
// is unavailable" (JWKS empty, unreachable or malformed) and
|
|
@@ -573,10 +590,14 @@ export function createOxyMiddleware(host: OxyMiddlewareHost) {
|
|
|
573
590
|
return res.status(401).json(error);
|
|
574
591
|
}
|
|
575
592
|
|
|
576
|
-
logger.error(
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
593
|
+
logger.error(
|
|
594
|
+
'[oxy.auth] Unexpected error during service token verification',
|
|
595
|
+
verifyError,
|
|
596
|
+
{
|
|
597
|
+
component: 'auth',
|
|
598
|
+
method: 'auth.serviceToken',
|
|
599
|
+
},
|
|
600
|
+
);
|
|
580
601
|
const error = {
|
|
581
602
|
error: 'AUTH_INTERNAL_ERROR',
|
|
582
603
|
message: 'Internal authentication error',
|
|
@@ -603,7 +624,12 @@ export function createOxyMiddleware(host: OxyMiddlewareHost) {
|
|
|
603
624
|
req.user = null;
|
|
604
625
|
return next();
|
|
605
626
|
}
|
|
606
|
-
const error = {
|
|
627
|
+
const error = {
|
|
628
|
+
error: 'TOKEN_EXPIRED',
|
|
629
|
+
message: 'Service token expired',
|
|
630
|
+
code: 'TOKEN_EXPIRED',
|
|
631
|
+
status: 401,
|
|
632
|
+
};
|
|
607
633
|
if (onError) return onError(error);
|
|
608
634
|
return res.status(401).json(error);
|
|
609
635
|
}
|
|
@@ -614,7 +640,12 @@ export function createOxyMiddleware(host: OxyMiddlewareHost) {
|
|
|
614
640
|
reason: 'Service token is not yet valid (nbf)',
|
|
615
641
|
status: 401,
|
|
616
642
|
});
|
|
617
|
-
const error = {
|
|
643
|
+
const error = {
|
|
644
|
+
error: 'INVALID_SERVICE_TOKEN_CLAIMS',
|
|
645
|
+
message: 'Service token is not yet valid',
|
|
646
|
+
code: 'INVALID_SERVICE_TOKEN_CLAIMS',
|
|
647
|
+
status: 401,
|
|
648
|
+
};
|
|
618
649
|
if (onError) return onError(error);
|
|
619
650
|
return res.status(401).json(error);
|
|
620
651
|
}
|
|
@@ -632,10 +663,12 @@ export function createOxyMiddleware(host: OxyMiddlewareHost) {
|
|
|
632
663
|
!isExactNonEmptyServiceClaim(decoded.appName) ||
|
|
633
664
|
!isExactNonEmptyServiceClaim(credentialId) ||
|
|
634
665
|
!isExactNonEmptyServiceClaim(ownerAccountId) ||
|
|
635
|
-
!isOxyServiceEnvironment(environment)
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
666
|
+
!isOxyServiceEnvironment(environment) ||
|
|
667
|
+
!Array.isArray(decoded.scopes) ||
|
|
668
|
+
!decoded.scopes.every(
|
|
669
|
+
(scope) => typeof scope === 'string' && scope.length > 0 && scope === scope.trim(),
|
|
670
|
+
) ||
|
|
671
|
+
new Set(decoded.scopes).size !== decoded.scopes.length
|
|
639
672
|
) {
|
|
640
673
|
recordRefusal(req, {
|
|
641
674
|
code: 'INVALID_SERVICE_TOKEN',
|
|
@@ -643,14 +676,18 @@ export function createOxyMiddleware(host: OxyMiddlewareHost) {
|
|
|
643
676
|
// Names the field rather than its value: an exact-match claim
|
|
644
677
|
// failing on invisible whitespace reads as "present" in a log
|
|
645
678
|
// that only says which claims were missing.
|
|
646
|
-
reason: `Service token claims unusable (${
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
679
|
+
reason: `Service token claims unusable (${
|
|
680
|
+
[
|
|
681
|
+
!isExactNonEmptyServiceClaim(appId) ? 'appId' : null,
|
|
682
|
+
!isExactNonEmptyServiceClaim(decoded.appName) ? 'appName' : null,
|
|
683
|
+
!isExactNonEmptyServiceClaim(credentialId) ? 'credentialId' : null,
|
|
684
|
+
!isExactNonEmptyServiceClaim(ownerAccountId) ? 'ownerAccountId' : null,
|
|
685
|
+
!isOxyServiceEnvironment(environment) ? 'environment' : null,
|
|
686
|
+
!Array.isArray(decoded.scopes) ? 'scopes' : null,
|
|
687
|
+
]
|
|
688
|
+
.filter((field) => field !== null)
|
|
689
|
+
.join(', ') || 'scopes'
|
|
690
|
+
})`,
|
|
654
691
|
status: 401,
|
|
655
692
|
});
|
|
656
693
|
if (optional) {
|
|
@@ -658,7 +695,12 @@ export function createOxyMiddleware(host: OxyMiddlewareHost) {
|
|
|
658
695
|
req.user = null;
|
|
659
696
|
return next();
|
|
660
697
|
}
|
|
661
|
-
const error = {
|
|
698
|
+
const error = {
|
|
699
|
+
error: 'INVALID_SERVICE_TOKEN',
|
|
700
|
+
message: 'Invalid service token: missing required claims',
|
|
701
|
+
code: 'INVALID_SERVICE_TOKEN',
|
|
702
|
+
status: 401,
|
|
703
|
+
};
|
|
662
704
|
if (onError) return onError(error);
|
|
663
705
|
return res.status(401).json(error);
|
|
664
706
|
}
|
|
@@ -694,8 +736,10 @@ export function createOxyMiddleware(host: OxyMiddlewareHost) {
|
|
|
694
736
|
// `req.userId = oxyUserId` would let any service impersonate
|
|
695
737
|
// any user simply by setting the header.
|
|
696
738
|
const grant = await host.verifyActingAs(appId, oxyUserId, {
|
|
697
|
-
cache: false,
|
|
698
|
-
|
|
739
|
+
cache: false,
|
|
740
|
+
credentialId: decoded.credentialId as string,
|
|
741
|
+
ownerAccountId: decoded.ownerAccountId as string,
|
|
742
|
+
environment: decoded.environment as string,
|
|
699
743
|
});
|
|
700
744
|
if (!grant || !grant.authorized) {
|
|
701
745
|
logger.warn('[oxy.auth] Service token rejected — no delegation grant', {
|
|
@@ -750,10 +794,13 @@ export function createOxyMiddleware(host: OxyMiddlewareHost) {
|
|
|
750
794
|
};
|
|
751
795
|
|
|
752
796
|
if (debug) {
|
|
753
|
-
logger.debug(
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
797
|
+
logger.debug(
|
|
798
|
+
`[oxy.auth] Service token OK app=${decoded.appName} delegateUser=${oxyUserId || '(none)'}`,
|
|
799
|
+
{
|
|
800
|
+
component: 'auth',
|
|
801
|
+
method: 'auth.serviceToken',
|
|
802
|
+
},
|
|
803
|
+
);
|
|
757
804
|
}
|
|
758
805
|
|
|
759
806
|
return next();
|
|
@@ -779,7 +826,7 @@ export function createOxyMiddleware(host: OxyMiddlewareHost) {
|
|
|
779
826
|
error: 'INVALID_TOKEN_PAYLOAD',
|
|
780
827
|
message: 'Token missing user ID',
|
|
781
828
|
code: 'INVALID_TOKEN_PAYLOAD',
|
|
782
|
-
status: 401
|
|
829
|
+
status: 401,
|
|
783
830
|
};
|
|
784
831
|
if (onError) return onError(error);
|
|
785
832
|
return res.status(401).json(error);
|
|
@@ -804,7 +851,7 @@ export function createOxyMiddleware(host: OxyMiddlewareHost) {
|
|
|
804
851
|
error: 'TOKEN_EXPIRED',
|
|
805
852
|
message: 'Token expired',
|
|
806
853
|
code: 'TOKEN_EXPIRED',
|
|
807
|
-
status: 401
|
|
854
|
+
status: 401,
|
|
808
855
|
};
|
|
809
856
|
if (onError) return onError(error);
|
|
810
857
|
return res.status(401).json(error);
|
|
@@ -833,7 +880,7 @@ export function createOxyMiddleware(host: OxyMiddlewareHost) {
|
|
|
833
880
|
error: 'SESSION_REQUIRED',
|
|
834
881
|
message: 'Access token is not bound to a session',
|
|
835
882
|
code: 'SESSION_REQUIRED',
|
|
836
|
-
status: 401
|
|
883
|
+
status: 401,
|
|
837
884
|
};
|
|
838
885
|
if (onError) return onError(error);
|
|
839
886
|
return res.status(401).json(error);
|
|
@@ -863,7 +910,7 @@ export function createOxyMiddleware(host: OxyMiddlewareHost) {
|
|
|
863
910
|
error: 'INVALID_SESSION',
|
|
864
911
|
message: 'Session invalid or expired',
|
|
865
912
|
code: 'INVALID_SESSION',
|
|
866
|
-
status: 401
|
|
913
|
+
status: 401,
|
|
867
914
|
};
|
|
868
915
|
if (onError) return onError(error);
|
|
869
916
|
return res.status(401).json(error);
|
|
@@ -888,7 +935,7 @@ export function createOxyMiddleware(host: OxyMiddlewareHost) {
|
|
|
888
935
|
error: 'INVALID_SESSION',
|
|
889
936
|
message: 'Session did not resolve to a usable identity',
|
|
890
937
|
code: 'INVALID_SESSION',
|
|
891
|
-
status: 401
|
|
938
|
+
status: 401,
|
|
892
939
|
};
|
|
893
940
|
if (onError) return onError(error);
|
|
894
941
|
return res.status(401).json(error);
|
|
@@ -921,7 +968,7 @@ export function createOxyMiddleware(host: OxyMiddlewareHost) {
|
|
|
921
968
|
error: 'SESSION_USER_MISMATCH',
|
|
922
969
|
message: 'Token user does not match the session',
|
|
923
970
|
code: 'SESSION_USER_MISMATCH',
|
|
924
|
-
status: 401
|
|
971
|
+
status: 401,
|
|
925
972
|
};
|
|
926
973
|
if (onError) return onError(error);
|
|
927
974
|
return res.status(401).json(error);
|
|
@@ -957,7 +1004,7 @@ export function createOxyMiddleware(host: OxyMiddlewareHost) {
|
|
|
957
1004
|
error: 'SESSION_ACTOR_MISMATCH',
|
|
958
1005
|
message: 'Session actor does not match the session',
|
|
959
1006
|
code: 'SESSION_ACTOR_MISMATCH',
|
|
960
|
-
status: 401
|
|
1007
|
+
status: 401,
|
|
961
1008
|
};
|
|
962
1009
|
if (onError) return onError(error);
|
|
963
1010
|
return res.status(401).json(error);
|
|
@@ -970,7 +1017,9 @@ export function createOxyMiddleware(host: OxyMiddlewareHost) {
|
|
|
970
1017
|
// Session validation already returned the full user, so `loadUser`
|
|
971
1018
|
// costs no extra round-trip.
|
|
972
1019
|
// One id source: the one `getUserIdentityId` validated (`id`, else `_id`).
|
|
973
|
-
req.user = loadUser
|
|
1020
|
+
req.user = loadUser
|
|
1021
|
+
? ({ ...validationResult.user, id: validatedUserId } as OxyRequestUser)
|
|
1022
|
+
: { id: validatedUserId };
|
|
974
1023
|
|
|
975
1024
|
if (debug) {
|
|
976
1025
|
logger.debug(`[oxy.auth] OK user=${validatedUserId} session=${sessionId}`, {
|
|
@@ -991,10 +1040,14 @@ export function createOxyMiddleware(host: OxyMiddlewareHost) {
|
|
|
991
1040
|
status: 401,
|
|
992
1041
|
});
|
|
993
1042
|
if (debug) {
|
|
994
|
-
logger.debug(
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
1043
|
+
logger.debug(
|
|
1044
|
+
'[oxy.auth] Session validation failed',
|
|
1045
|
+
{
|
|
1046
|
+
component: 'auth',
|
|
1047
|
+
method: 'auth',
|
|
1048
|
+
},
|
|
1049
|
+
validationError,
|
|
1050
|
+
);
|
|
998
1051
|
}
|
|
999
1052
|
|
|
1000
1053
|
if (optional) {
|
|
@@ -1007,7 +1060,7 @@ export function createOxyMiddleware(host: OxyMiddlewareHost) {
|
|
|
1007
1060
|
error: 'SESSION_VALIDATION_ERROR',
|
|
1008
1061
|
message: 'Session validation failed',
|
|
1009
1062
|
code: 'SESSION_VALIDATION_ERROR',
|
|
1010
|
-
status: 401
|
|
1063
|
+
status: 401,
|
|
1011
1064
|
};
|
|
1012
1065
|
if (onError) return onError(error);
|
|
1013
1066
|
return res.status(401).json(error);
|
|
@@ -1026,10 +1079,14 @@ export function createOxyMiddleware(host: OxyMiddlewareHost) {
|
|
|
1026
1079
|
};
|
|
1027
1080
|
|
|
1028
1081
|
if (debug) {
|
|
1029
|
-
logger.debug(
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1082
|
+
logger.debug(
|
|
1083
|
+
'[oxy.auth] Error',
|
|
1084
|
+
{
|
|
1085
|
+
component: 'auth',
|
|
1086
|
+
method: 'auth',
|
|
1087
|
+
},
|
|
1088
|
+
apiError,
|
|
1089
|
+
);
|
|
1033
1090
|
}
|
|
1034
1091
|
|
|
1035
1092
|
if (onError) return onError(apiError);
|
|
@@ -1078,10 +1135,14 @@ export function createOxyMiddleware(host: OxyMiddlewareHost) {
|
|
|
1078
1135
|
decoded = jwtDecode<JwtPayload>(token);
|
|
1079
1136
|
} catch (decodeError) {
|
|
1080
1137
|
if (debug) {
|
|
1081
|
-
logger.debug(
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1138
|
+
logger.debug(
|
|
1139
|
+
'[oxy.authSocket] Token decode failed',
|
|
1140
|
+
{
|
|
1141
|
+
component: 'auth',
|
|
1142
|
+
method: 'authSocket',
|
|
1143
|
+
},
|
|
1144
|
+
decodeError,
|
|
1145
|
+
);
|
|
1085
1146
|
}
|
|
1086
1147
|
return next(new Error('Invalid token'));
|
|
1087
1148
|
}
|
|
@@ -1124,10 +1185,14 @@ export function createOxyMiddleware(host: OxyMiddlewareHost) {
|
|
|
1124
1185
|
userId = validatedUserId;
|
|
1125
1186
|
} catch (validateErr) {
|
|
1126
1187
|
if (debug) {
|
|
1127
|
-
logger.debug(
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1188
|
+
logger.debug(
|
|
1189
|
+
'[oxy.authSocket] Session validation failed',
|
|
1190
|
+
{
|
|
1191
|
+
component: 'auth',
|
|
1192
|
+
method: 'authSocket',
|
|
1193
|
+
},
|
|
1194
|
+
validateErr,
|
|
1195
|
+
);
|
|
1131
1196
|
}
|
|
1132
1197
|
return next(new Error('Session validation failed'));
|
|
1133
1198
|
}
|
|
@@ -1153,10 +1218,14 @@ export function createOxyMiddleware(host: OxyMiddlewareHost) {
|
|
|
1153
1218
|
next();
|
|
1154
1219
|
} catch (err) {
|
|
1155
1220
|
if (debug) {
|
|
1156
|
-
logger.debug(
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1221
|
+
logger.debug(
|
|
1222
|
+
'[oxy.authSocket] Error',
|
|
1223
|
+
{
|
|
1224
|
+
component: 'auth',
|
|
1225
|
+
method: 'authSocket',
|
|
1226
|
+
},
|
|
1227
|
+
err,
|
|
1228
|
+
);
|
|
1160
1229
|
}
|
|
1161
1230
|
next(new Error('Authentication error'));
|
|
1162
1231
|
}
|
|
@@ -1178,14 +1247,16 @@ export function createOxyMiddleware(host: OxyMiddlewareHost) {
|
|
|
1178
1247
|
* });
|
|
1179
1248
|
* ```
|
|
1180
1249
|
*/
|
|
1181
|
-
function service(
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1250
|
+
function service(
|
|
1251
|
+
options: {
|
|
1252
|
+
debug?: boolean;
|
|
1253
|
+
/** Observe refusals on the service lane; forwarded straight to `auth()`. */
|
|
1254
|
+
onRefusal?: (refusal: OxyAuthRefusal) => void;
|
|
1255
|
+
serviceTokenJwksUrl?: string;
|
|
1256
|
+
expectedIssuer?: string;
|
|
1257
|
+
expectedAudience?: string;
|
|
1258
|
+
} = {},
|
|
1259
|
+
) {
|
|
1189
1260
|
const innerAuth = auth({ ...options });
|
|
1190
1261
|
|
|
1191
1262
|
return async (req: AuthReq, res: AuthRes, next: AuthNext) => {
|
|
@@ -1287,7 +1358,6 @@ export function createOxyMiddleware(host: OxyMiddlewareHost) {
|
|
|
1287
1358
|
/** The middleware set `OxyServer.middleware` exposes. */
|
|
1288
1359
|
export type OxyMiddleware = ReturnType<typeof createOxyMiddleware>;
|
|
1289
1360
|
|
|
1290
|
-
|
|
1291
1361
|
// ---------------------------------------------------------------------------
|
|
1292
1362
|
// Service token verification helpers
|
|
1293
1363
|
// ---------------------------------------------------------------------------
|
|
@@ -1319,12 +1389,15 @@ const JWKS_MAX_BYTES = 64 * 1024;
|
|
|
1319
1389
|
const JWKS_MAX_KEYS = 20;
|
|
1320
1390
|
|
|
1321
1391
|
export function parseJsonSegment(segment: string): Record<string, unknown> {
|
|
1322
|
-
if (!/^[A-Za-z0-9_-]+$/.test(segment))
|
|
1392
|
+
if (!/^[A-Za-z0-9_-]+$/.test(segment))
|
|
1393
|
+
throw new ServiceTokenStructureError('Service token segment is not base64url');
|
|
1323
1394
|
const bytes = Buffer.from(segment, 'base64url');
|
|
1324
|
-
if (bytes.toString('base64url') !== segment)
|
|
1395
|
+
if (bytes.toString('base64url') !== segment)
|
|
1396
|
+
throw new ServiceTokenStructureError('Service token segment is not canonical base64url');
|
|
1325
1397
|
try {
|
|
1326
1398
|
const value = JSON.parse(bytes.toString('utf8')) as unknown;
|
|
1327
|
-
if (typeof value !== 'object' || value === null || Array.isArray(value))
|
|
1399
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value))
|
|
1400
|
+
throw new Error('not an object');
|
|
1328
1401
|
return value as Record<string, unknown>;
|
|
1329
1402
|
} catch {
|
|
1330
1403
|
throw new ServiceTokenStructureError('Service token segment is not a JSON object');
|
|
@@ -1332,25 +1405,29 @@ export function parseJsonSegment(segment: string): Record<string, unknown> {
|
|
|
1332
1405
|
}
|
|
1333
1406
|
|
|
1334
1407
|
function parseServiceTokenJwks(value: unknown): Map<string, ServiceTokenPublicJwk> {
|
|
1335
|
-
if (typeof value !== 'object' || value === null || Array.isArray(value))
|
|
1408
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value))
|
|
1409
|
+
throw new ServiceTokenSignatureError('Oxy JWKS is malformed');
|
|
1336
1410
|
const keys = (value as { keys?: unknown }).keys;
|
|
1337
|
-
if (!Array.isArray(keys) || keys.length === 0 || keys.length > JWKS_MAX_KEYS)
|
|
1411
|
+
if (!Array.isArray(keys) || keys.length === 0 || keys.length > JWKS_MAX_KEYS)
|
|
1412
|
+
throw new ServiceTokenSignatureError('Oxy JWKS has an invalid key set');
|
|
1338
1413
|
const result = new Map<string, ServiceTokenPublicJwk>();
|
|
1339
1414
|
for (const value of keys) {
|
|
1340
|
-
if (typeof value !== 'object' || value === null || Array.isArray(value))
|
|
1415
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value))
|
|
1416
|
+
throw new ServiceTokenSignatureError('Oxy JWKS contains a malformed key');
|
|
1341
1417
|
const key = value as Record<string, unknown>;
|
|
1342
1418
|
if (
|
|
1343
|
-
key.kty !== 'OKP'
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
)
|
|
1419
|
+
key.kty !== 'OKP' ||
|
|
1420
|
+
key.crv !== 'Ed25519' ||
|
|
1421
|
+
typeof key.x !== 'string' ||
|
|
1422
|
+
key.x.length === 0 ||
|
|
1423
|
+
key.use !== 'sig' ||
|
|
1424
|
+
key.alg !== 'EdDSA' ||
|
|
1425
|
+
typeof key.kid !== 'string' ||
|
|
1426
|
+
!/^[A-Za-z0-9._-]{1,128}$/.test(key.kid) ||
|
|
1427
|
+
Object.prototype.hasOwnProperty.call(key, 'd') ||
|
|
1428
|
+
result.has(key.kid)
|
|
1429
|
+
)
|
|
1430
|
+
throw new ServiceTokenSignatureError('Oxy JWKS contains an unsupported or duplicate key');
|
|
1354
1431
|
const x = Buffer.from(key.x, 'base64url');
|
|
1355
1432
|
if (x.length !== 32 || x.toString('base64url') !== key.x) {
|
|
1356
1433
|
throw new ServiceTokenSignatureError('Oxy JWKS contains an invalid Ed25519 key');
|
|
@@ -1373,9 +1450,11 @@ async function refreshServiceTokenJwks(url: string, cache: ServiceTokenJwksCache
|
|
|
1373
1450
|
signal: controller.signal,
|
|
1374
1451
|
redirect: 'error',
|
|
1375
1452
|
});
|
|
1376
|
-
if (!response.ok)
|
|
1453
|
+
if (!response.ok)
|
|
1454
|
+
throw new ServiceTokenSignatureError(`Oxy JWKS returned HTTP ${response.status}`);
|
|
1377
1455
|
const source = await response.text();
|
|
1378
|
-
if (Buffer.byteLength(source, 'utf8') > JWKS_MAX_BYTES)
|
|
1456
|
+
if (Buffer.byteLength(source, 'utf8') > JWKS_MAX_BYTES)
|
|
1457
|
+
throw new ServiceTokenSignatureError('Oxy JWKS exceeds the size limit');
|
|
1379
1458
|
cache.keys = parseServiceTokenJwks(JSON.parse(source) as unknown);
|
|
1380
1459
|
cache.expiresAt = Date.now() + JWKS_CACHE_MS;
|
|
1381
1460
|
} finally {
|
|
@@ -1397,7 +1476,8 @@ export async function resolveServiceTokenPublicKey(
|
|
|
1397
1476
|
const now = Date.now();
|
|
1398
1477
|
const cached = cache.keys.get(kid);
|
|
1399
1478
|
if (cached && cache.expiresAt > now) return cached;
|
|
1400
|
-
const mayRefresh =
|
|
1479
|
+
const mayRefresh =
|
|
1480
|
+
cache.expiresAt <= now || now - cache.lastAttemptAt >= JWKS_UNKNOWN_KID_REFRESH_MS;
|
|
1401
1481
|
if (mayRefresh) {
|
|
1402
1482
|
try {
|
|
1403
1483
|
await refreshServiceTokenJwks(url, cache);
|
|
@@ -1408,7 +1488,8 @@ export async function resolveServiceTokenPublicKey(
|
|
|
1408
1488
|
}
|
|
1409
1489
|
}
|
|
1410
1490
|
const resolved = cache.keys.get(kid);
|
|
1411
|
-
if (!resolved || cache.expiresAt <= Date.now())
|
|
1491
|
+
if (!resolved || cache.expiresAt <= Date.now())
|
|
1492
|
+
throw new ServiceTokenSignatureError('Service token signing key is unknown');
|
|
1412
1493
|
return resolved;
|
|
1413
1494
|
}
|
|
1414
1495
|
|
|
@@ -1432,12 +1513,13 @@ async function verifyServiceTokenSignature(
|
|
|
1432
1513
|
}
|
|
1433
1514
|
const header = parseJsonSegment(headerB64);
|
|
1434
1515
|
if (
|
|
1435
|
-
Object.keys(header).length !== 3
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
)
|
|
1516
|
+
Object.keys(header).length !== 3 ||
|
|
1517
|
+
header.alg !== 'EdDSA' ||
|
|
1518
|
+
header.typ !== 'JWT' ||
|
|
1519
|
+
typeof header.kid !== 'string' ||
|
|
1520
|
+
!/^[A-Za-z0-9._-]{1,128}$/.test(header.kid)
|
|
1521
|
+
)
|
|
1522
|
+
throw new ServiceTokenStructureError('Service token JOSE header is not supported');
|
|
1441
1523
|
const jwk = await resolveServiceTokenPublicKey(header.kid, options.jwksUrl, options.cache);
|
|
1442
1524
|
let publicKey: Awaited<ReturnType<typeof nodeCrypto.createPublicKey>>;
|
|
1443
1525
|
try {
|
|
@@ -1446,8 +1528,10 @@ async function verifyServiceTokenSignature(
|
|
|
1446
1528
|
throw new ServiceTokenSignatureError('Service token public key is invalid');
|
|
1447
1529
|
}
|
|
1448
1530
|
const signature = Buffer.from(signatureB64, 'base64url');
|
|
1449
|
-
if (signature.toString('base64url') !== signatureB64 || signature.length !== 64)
|
|
1450
|
-
|
|
1531
|
+
if (signature.toString('base64url') !== signatureB64 || signature.length !== 64)
|
|
1532
|
+
throw new ServiceTokenStructureError('Service token signature is malformed');
|
|
1533
|
+
if (!nodeCrypto.verify(null, Buffer.from(`${headerB64}.${payloadB64}`), publicKey, signature))
|
|
1534
|
+
throw new ServiceTokenSignatureError();
|
|
1451
1535
|
}
|
|
1452
1536
|
|
|
1453
1537
|
/**
|
|
@@ -1479,13 +1563,13 @@ function readStringClaim(value: unknown): string | null {
|
|
|
1479
1563
|
/**
|
|
1480
1564
|
* Resolve the canonical user id from a validated session's user object.
|
|
1481
1565
|
*
|
|
1482
|
-
* The API serializer emits `id`, but some upstream shapes carry
|
|
1483
|
-
*
|
|
1566
|
+
* The API serializer emits `id`, but some upstream shapes carry a raw `_id`
|
|
1567
|
+
* instead. We accept either, but only a non-empty string — anything else
|
|
1484
1568
|
* means the validated identity is unusable and the caller must reject.
|
|
1485
1569
|
*/
|
|
1486
1570
|
function getUserIdentityId(user: User): string | null {
|
|
1487
|
-
const candidate =
|
|
1488
|
-
?? (user as { id?: unknown; _id?: unknown })._id;
|
|
1571
|
+
const candidate =
|
|
1572
|
+
(user as { id?: unknown; _id?: unknown }).id ?? (user as { id?: unknown; _id?: unknown })._id;
|
|
1489
1573
|
return typeof candidate === 'string' && candidate.length > 0 ? candidate : null;
|
|
1490
1574
|
}
|
|
1491
1575
|
|
|
@@ -1501,21 +1585,29 @@ function verifyServiceTokenClaims(
|
|
|
1501
1585
|
expected: { audience: string; issuer: string },
|
|
1502
1586
|
): void {
|
|
1503
1587
|
if (!hasBoundedServiceTokenLifetime(decoded)) {
|
|
1504
|
-
throw new ServiceTokenClaimError(
|
|
1588
|
+
throw new ServiceTokenClaimError(
|
|
1589
|
+
'Service token lifetime must be positive and at most 300 seconds',
|
|
1590
|
+
);
|
|
1505
1591
|
}
|
|
1506
1592
|
if (decoded.type !== 'service') {
|
|
1507
1593
|
throw new ServiceTokenClaimError(`Service token has unexpected type '${String(decoded.type)}'`);
|
|
1508
1594
|
}
|
|
1509
1595
|
if (decoded.iss !== expected.issuer) {
|
|
1510
|
-
throw new ServiceTokenClaimError(
|
|
1596
|
+
throw new ServiceTokenClaimError(
|
|
1597
|
+
`Service token issuer mismatch: expected '${expected.issuer}', got '${String(decoded.iss)}'`,
|
|
1598
|
+
);
|
|
1511
1599
|
}
|
|
1512
1600
|
const aud = decoded.aud;
|
|
1513
1601
|
if (Array.isArray(aud)) {
|
|
1514
1602
|
if (!aud.includes(expected.audience)) {
|
|
1515
|
-
throw new ServiceTokenClaimError(
|
|
1603
|
+
throw new ServiceTokenClaimError(
|
|
1604
|
+
`Service token audience does not include '${expected.audience}'`,
|
|
1605
|
+
);
|
|
1516
1606
|
}
|
|
1517
1607
|
} else if (aud !== expected.audience) {
|
|
1518
|
-
throw new ServiceTokenClaimError(
|
|
1608
|
+
throw new ServiceTokenClaimError(
|
|
1609
|
+
`Service token audience mismatch: expected '${expected.audience}', got '${String(aud)}'`,
|
|
1610
|
+
);
|
|
1519
1611
|
}
|
|
1520
1612
|
}
|
|
1521
1613
|
|
|
@@ -1568,4 +1660,3 @@ interface SocketLike {
|
|
|
1568
1660
|
data?: Record<string, unknown>;
|
|
1569
1661
|
user?: { id: string; userId: string; sessionId?: string | null };
|
|
1570
1662
|
}
|
|
1571
|
-
|