@oxyhq/core 12.9.0 → 12.10.1
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/HttpService.js +22 -12
- package/dist/cjs/crypto/keyManager.js +89 -0
- package/dist/cjs/i18n/locales/ar-SA.json +4 -0
- package/dist/cjs/i18n/locales/ca-ES.json +4 -0
- package/dist/cjs/i18n/locales/de-DE.json +4 -0
- package/dist/cjs/i18n/locales/en-US.json +69 -3
- package/dist/cjs/i18n/locales/es-ES.json +68 -2
- package/dist/cjs/i18n/locales/fr-FR.json +4 -0
- package/dist/cjs/i18n/locales/it-IT.json +4 -0
- package/dist/cjs/i18n/locales/ja-JP.json +4 -0
- package/dist/cjs/i18n/locales/ko-KR.json +4 -0
- package/dist/cjs/i18n/locales/locales/ar-SA.json +4 -0
- package/dist/cjs/i18n/locales/locales/ca-ES.json +4 -0
- package/dist/cjs/i18n/locales/locales/de-DE.json +4 -0
- package/dist/cjs/i18n/locales/locales/en-US.json +69 -3
- package/dist/cjs/i18n/locales/locales/es-ES.json +68 -2
- package/dist/cjs/i18n/locales/locales/fr-FR.json +4 -0
- package/dist/cjs/i18n/locales/locales/it-IT.json +4 -0
- package/dist/cjs/i18n/locales/locales/ja-JP.json +4 -0
- package/dist/cjs/i18n/locales/locales/ko-KR.json +4 -0
- package/dist/cjs/i18n/locales/locales/pt-PT.json +4 -0
- package/dist/cjs/i18n/locales/locales/zh-CN.json +4 -0
- package/dist/cjs/i18n/locales/pt-PT.json +4 -0
- package/dist/cjs/i18n/locales/zh-CN.json +4 -0
- package/dist/cjs/index.js +7 -4
- package/dist/cjs/mixins/OxyServices.auth.js +51 -20
- package/dist/cjs/mixins/OxyServices.connectedApps.js +6 -1
- package/dist/cjs/mixins/OxyServices.deviceBoot.js +9 -2
- package/dist/cjs/mixins/OxyServices.identityBackup.js +11 -0
- package/dist/cjs/mixins/OxyServices.user.js +6 -0
- package/dist/cjs/session/accountDialogController.js +7 -5
- package/dist/cjs/utils/commonsApproval.js +31 -0
- package/dist/esm/.tsbuildinfo +1 -1
- package/dist/esm/HttpService.js +22 -12
- package/dist/esm/crypto/keyManager.js +89 -0
- package/dist/esm/i18n/locales/ar-SA.json +4 -0
- package/dist/esm/i18n/locales/ca-ES.json +4 -0
- package/dist/esm/i18n/locales/de-DE.json +4 -0
- package/dist/esm/i18n/locales/en-US.json +69 -3
- package/dist/esm/i18n/locales/es-ES.json +68 -2
- package/dist/esm/i18n/locales/fr-FR.json +4 -0
- package/dist/esm/i18n/locales/it-IT.json +4 -0
- package/dist/esm/i18n/locales/ja-JP.json +4 -0
- package/dist/esm/i18n/locales/ko-KR.json +4 -0
- package/dist/esm/i18n/locales/locales/ar-SA.json +4 -0
- package/dist/esm/i18n/locales/locales/ca-ES.json +4 -0
- package/dist/esm/i18n/locales/locales/de-DE.json +4 -0
- package/dist/esm/i18n/locales/locales/en-US.json +69 -3
- package/dist/esm/i18n/locales/locales/es-ES.json +68 -2
- package/dist/esm/i18n/locales/locales/fr-FR.json +4 -0
- package/dist/esm/i18n/locales/locales/it-IT.json +4 -0
- package/dist/esm/i18n/locales/locales/ja-JP.json +4 -0
- package/dist/esm/i18n/locales/locales/ko-KR.json +4 -0
- package/dist/esm/i18n/locales/locales/pt-PT.json +4 -0
- package/dist/esm/i18n/locales/locales/zh-CN.json +4 -0
- package/dist/esm/i18n/locales/pt-PT.json +4 -0
- package/dist/esm/i18n/locales/zh-CN.json +4 -0
- package/dist/esm/index.js +2 -1
- package/dist/esm/mixins/OxyServices.auth.js +48 -19
- package/dist/esm/mixins/OxyServices.connectedApps.js +6 -1
- package/dist/esm/mixins/OxyServices.deviceBoot.js +9 -2
- package/dist/esm/mixins/OxyServices.identityBackup.js +11 -0
- package/dist/esm/mixins/OxyServices.user.js +6 -0
- package/dist/esm/session/accountDialogController.js +7 -5
- package/dist/esm/utils/commonsApproval.js +27 -0
- package/dist/types/.tsbuildinfo +1 -1
- package/dist/types/HttpService.d.ts +14 -0
- package/dist/types/crypto/keyManager.d.ts +33 -0
- package/dist/types/index.d.ts +2 -1
- package/dist/types/mixins/OxyServices.auth.d.ts +4 -4
- package/dist/types/session/accountDialogController.d.ts +7 -2
- package/dist/types/utils/commonsApproval.d.ts +13 -0
- package/package.json +3 -3
- package/src/HttpService.ts +37 -14
- package/src/__tests__/httpServiceAuthSelfAwait.test.ts +108 -0
- package/src/__tests__/httpServiceQueueDeadlock.test.ts +128 -0
- package/src/crypto/__tests__/keyManager.recoveryMnemonic.test.ts +138 -0
- package/src/crypto/keyManager.ts +99 -0
- package/src/i18n/locales/ar-SA.json +4 -0
- package/src/i18n/locales/ca-ES.json +4 -0
- package/src/i18n/locales/de-DE.json +4 -0
- package/src/i18n/locales/en-US.json +69 -3
- package/src/i18n/locales/es-ES.json +68 -2
- package/src/i18n/locales/fr-FR.json +4 -0
- package/src/i18n/locales/it-IT.json +4 -0
- package/src/i18n/locales/ja-JP.json +4 -0
- package/src/i18n/locales/ko-KR.json +4 -0
- package/src/i18n/locales/pt-PT.json +4 -0
- package/src/i18n/locales/zh-CN.json +4 -0
- package/src/index.ts +7 -1
- package/src/mixins/OxyServices.auth.ts +48 -24
- package/src/mixins/OxyServices.connectedApps.ts +6 -1
- package/src/mixins/OxyServices.deviceBoot.ts +7 -2
- package/src/mixins/OxyServices.identityBackup.ts +13 -0
- package/src/mixins/OxyServices.user.ts +6 -0
- package/src/mixins/__tests__/OxyServices.deviceBoot.test.ts +4 -2
- package/src/mixins/__tests__/commonsSignIn.test.ts +4 -4
- package/src/mixins/__tests__/identityBackup.test.ts +57 -0
- package/src/mixins/__tests__/preSessionSkipAuth.test.ts +152 -0
- package/src/mixins/__tests__/privacyCacheInvalidation.test.ts +6 -0
- package/src/mixins/__tests__/webauthnAuth.test.ts +6 -6
- package/src/session/__tests__/accountDialogController.test.ts +3 -3
- package/src/session/accountDialogController.ts +7 -6
- package/src/utils/__tests__/commonsApproval.test.ts +60 -0
- package/src/utils/commonsApproval.ts +39 -0
|
@@ -9,6 +9,10 @@ import { loginResultSchema, safeParseContract } from '@oxyhq/contracts';
|
|
|
9
9
|
import type { SessionLoginResponse } from '../models/session';
|
|
10
10
|
import type { OxyServicesBase } from '../OxyServices.base';
|
|
11
11
|
import type { PublicApplication } from './OxyServices.connectedApps';
|
|
12
|
+
export {
|
|
13
|
+
getCommonsApprovalBlockingReason,
|
|
14
|
+
parseCommonsApprovalExpiresAt,
|
|
15
|
+
} from '../utils/commonsApproval';
|
|
12
16
|
import { OxyAuthenticationError } from '../OxyServices.errors';
|
|
13
17
|
import { KeyManager } from '../crypto/keyManager';
|
|
14
18
|
import { SignatureService } from '../crypto/signatureService';
|
|
@@ -96,7 +100,7 @@ export interface CommonsSignInStatus {
|
|
|
96
100
|
*/
|
|
97
101
|
export interface CommonsApprovalInfo {
|
|
98
102
|
/** Sanitized, display-safe identity of the requesting application. */
|
|
99
|
-
application: PublicApplication;
|
|
103
|
+
application: PublicApplication | null;
|
|
100
104
|
/** OAuth scopes the application is requesting. */
|
|
101
105
|
scopes: string[];
|
|
102
106
|
/** The origin the session is bound to (the RP web origin), when applicable. */
|
|
@@ -109,8 +113,8 @@ export interface CommonsApprovalInfo {
|
|
|
109
113
|
* "not verified") by {@link OxyServicesAuthMixin.getCommonsApprovalInfo}.
|
|
110
114
|
*/
|
|
111
115
|
originVerified: boolean;
|
|
112
|
-
/** Server-authoritative expiry (epoch
|
|
113
|
-
expiresAt: number;
|
|
116
|
+
/** Server-authoritative expiry (epoch ms or ISO-8601 string from the API). */
|
|
117
|
+
expiresAt: number | string;
|
|
114
118
|
/** Session lifecycle status. */
|
|
115
119
|
status: string;
|
|
116
120
|
}
|
|
@@ -122,11 +126,11 @@ export interface CommonsApprovalInfo {
|
|
|
122
126
|
* into {@link CommonsApprovalInfo}.
|
|
123
127
|
*/
|
|
124
128
|
interface CommonsApprovalInfoResponse {
|
|
125
|
-
application: PublicApplication;
|
|
129
|
+
application: PublicApplication | null;
|
|
126
130
|
scopes: string[];
|
|
127
131
|
boundOrigin?: string;
|
|
128
132
|
originVerified?: unknown;
|
|
129
|
-
expiresAt: number;
|
|
133
|
+
expiresAt: number | string;
|
|
130
134
|
status: string;
|
|
131
135
|
}
|
|
132
136
|
|
|
@@ -367,7 +371,7 @@ export function OxyServicesAuthMixin<T extends typeof OxyServicesBase>(Base: T)
|
|
|
367
371
|
'POST',
|
|
368
372
|
'/auth/service-token',
|
|
369
373
|
{ apiKey: key, apiSecret: secret },
|
|
370
|
-
{ cache: false, retry: false }
|
|
374
|
+
{ cache: false, retry: false, skipAuth: true }
|
|
371
375
|
);
|
|
372
376
|
|
|
373
377
|
const expiresAt = Date.now() + response.expiresIn * 1000;
|
|
@@ -482,7 +486,7 @@ export function OxyServicesAuthMixin<T extends typeof OxyServicesBase>(Base: T)
|
|
|
482
486
|
publicKey,
|
|
483
487
|
signature,
|
|
484
488
|
timestamp,
|
|
485
|
-
}, { cache: false });
|
|
489
|
+
}, { cache: false, skipAuth: true });
|
|
486
490
|
|
|
487
491
|
if (!res || (typeof res === 'object' && Object.keys(res).length === 0)) {
|
|
488
492
|
throw new OxyAuthenticationError('Registration failed', 'REGISTER_FAILED', 400);
|
|
@@ -511,7 +515,7 @@ export function OxyServicesAuthMixin<T extends typeof OxyServicesBase>(Base: T)
|
|
|
511
515
|
try {
|
|
512
516
|
return await this.makeRequest<ChallengeResponse>('POST', '/auth/challenge', {
|
|
513
517
|
publicKey,
|
|
514
|
-
}, { cache: false, ...requestOptions });
|
|
518
|
+
}, { cache: false, skipAuth: true, ...requestOptions });
|
|
515
519
|
} catch (error) {
|
|
516
520
|
throw this.handleError(error);
|
|
517
521
|
}
|
|
@@ -548,7 +552,7 @@ export function OxyServicesAuthMixin<T extends typeof OxyServicesBase>(Base: T)
|
|
|
548
552
|
timestamp,
|
|
549
553
|
deviceName,
|
|
550
554
|
deviceFingerprint,
|
|
551
|
-
}, { cache: false, ...requestOptions });
|
|
555
|
+
}, { cache: false, skipAuth: true, ...requestOptions });
|
|
552
556
|
|
|
553
557
|
// Plant the freshly-minted tokens, mirroring `claimSessionByToken`.
|
|
554
558
|
// `/auth/verify` returns the first access token (and refresh token) in
|
|
@@ -577,7 +581,7 @@ export function OxyServicesAuthMixin<T extends typeof OxyServicesBase>(Base: T)
|
|
|
577
581
|
'GET',
|
|
578
582
|
`/auth/check-publickey/${encodeURIComponent(publicKey)}`,
|
|
579
583
|
undefined,
|
|
580
|
-
{ cache: false }
|
|
584
|
+
{ cache: false, skipAuth: true }
|
|
581
585
|
);
|
|
582
586
|
} catch (error) {
|
|
583
587
|
throw this.handleError(error);
|
|
@@ -593,7 +597,12 @@ export function OxyServicesAuthMixin<T extends typeof OxyServicesBase>(Base: T)
|
|
|
593
597
|
'GET',
|
|
594
598
|
`/auth/user/${encodeURIComponent(publicKey)}`,
|
|
595
599
|
undefined,
|
|
596
|
-
{
|
|
600
|
+
{
|
|
601
|
+
cache: true,
|
|
602
|
+
cacheTTL: 2 * 60 * 1000,
|
|
603
|
+
// Public lookup by public key (pre-session) — skip the bearer preflight.
|
|
604
|
+
skipAuth: true,
|
|
605
|
+
},
|
|
597
606
|
);
|
|
598
607
|
return normalizeUserIdentity(user);
|
|
599
608
|
} catch (error) {
|
|
@@ -693,7 +702,8 @@ export function OxyServicesAuthMixin<T extends typeof OxyServicesBase>(Base: T)
|
|
|
693
702
|
sessionToken,
|
|
694
703
|
...(options.deviceFingerprint ? { deviceFingerprint: options.deviceFingerprint } : {}),
|
|
695
704
|
},
|
|
696
|
-
|
|
705
|
+
// Body-authenticated device-flow claim (no bearer) — skip the preflight.
|
|
706
|
+
{ cache: false, retry: false, skipAuth: true }
|
|
697
707
|
);
|
|
698
708
|
|
|
699
709
|
this.setTokens(res.accessToken);
|
|
@@ -802,7 +812,10 @@ export function OxyServicesAuthMixin<T extends typeof OxyServicesBase>(Base: T)
|
|
|
802
812
|
'POST',
|
|
803
813
|
'/auth/session/create',
|
|
804
814
|
{ sessionToken, expiresAt, clientId: params.clientId },
|
|
805
|
-
|
|
815
|
+
// Public/pre-session (no bearer): skip the preflight so a stale
|
|
816
|
+
// near-expiry token cannot re-enter refreshAccessToken while the
|
|
817
|
+
// refresh handler is already in flight (self-await hang).
|
|
818
|
+
{ cache: false, skipAuth: true },
|
|
806
819
|
);
|
|
807
820
|
|
|
808
821
|
return {
|
|
@@ -832,7 +845,10 @@ export function OxyServicesAuthMixin<T extends typeof OxyServicesBase>(Base: T)
|
|
|
832
845
|
'GET',
|
|
833
846
|
`/auth/session/status/${encodeURIComponent(sessionToken)}`,
|
|
834
847
|
undefined,
|
|
835
|
-
|
|
848
|
+
// Public/pre-session (no bearer): a preflight here is wrong per se and,
|
|
849
|
+
// if ever reached while a refresh is pending, would re-enter
|
|
850
|
+
// refreshAccessToken and await the very promise it runs inside.
|
|
851
|
+
{ cache: false, retry: false, skipAuth: true }
|
|
836
852
|
);
|
|
837
853
|
} catch (error) {
|
|
838
854
|
throw this.handleError(error);
|
|
@@ -855,7 +871,9 @@ export function OxyServicesAuthMixin<T extends typeof OxyServicesBase>(Base: T)
|
|
|
855
871
|
'GET',
|
|
856
872
|
`/auth/session/approve-info/${encodeURIComponent(authorizeCode)}`,
|
|
857
873
|
undefined,
|
|
858
|
-
|
|
874
|
+
// Public (no auth required) — skip the bearer preflight (avoids the
|
|
875
|
+
// pre-session self-await class).
|
|
876
|
+
{ cache: false, skipAuth: true }
|
|
859
877
|
);
|
|
860
878
|
return {
|
|
861
879
|
application: raw.application,
|
|
@@ -913,7 +931,8 @@ export function OxyServicesAuthMixin<T extends typeof OxyServicesBase>(Base: T)
|
|
|
913
931
|
...(params.deviceName ? { deviceName: params.deviceName } : {}),
|
|
914
932
|
...(params.deviceFingerprint ? { deviceFingerprint: params.deviceFingerprint } : {}),
|
|
915
933
|
},
|
|
916
|
-
|
|
934
|
+
// Key-signed, cookieless (no bearer) — skip the preflight.
|
|
935
|
+
{ cache: false, skipAuth: true }
|
|
917
936
|
);
|
|
918
937
|
} catch (error) {
|
|
919
938
|
throw this.handleError(error);
|
|
@@ -932,7 +951,8 @@ export function OxyServicesAuthMixin<T extends typeof OxyServicesBase>(Base: T)
|
|
|
932
951
|
'POST',
|
|
933
952
|
`/auth/session/deny/${encodeURIComponent(authorizeCode)}`,
|
|
934
953
|
undefined,
|
|
935
|
-
|
|
954
|
+
// Public (no auth required) — skip the bearer preflight.
|
|
955
|
+
{ cache: false, skipAuth: true }
|
|
936
956
|
);
|
|
937
957
|
} catch (error) {
|
|
938
958
|
throw this.handleError(error);
|
|
@@ -1065,7 +1085,8 @@ export function OxyServicesAuthMixin<T extends typeof OxyServicesBase>(Base: T)
|
|
|
1065
1085
|
*/
|
|
1066
1086
|
async checkUsernameAvailability(username: string): Promise<{ available: boolean; message: string }> {
|
|
1067
1087
|
try {
|
|
1068
|
-
|
|
1088
|
+
// Public availability lookup (pre-session) — skip the bearer preflight.
|
|
1089
|
+
return await this.makeRequest('GET', `/auth/check-username/${username}`, undefined, { cache: false, skipAuth: true });
|
|
1069
1090
|
} catch (error) {
|
|
1070
1091
|
throw this.handleError(error);
|
|
1071
1092
|
}
|
|
@@ -1076,7 +1097,8 @@ export function OxyServicesAuthMixin<T extends typeof OxyServicesBase>(Base: T)
|
|
|
1076
1097
|
*/
|
|
1077
1098
|
async checkEmailAvailability(email: string): Promise<{ available: boolean; message: string }> {
|
|
1078
1099
|
try {
|
|
1079
|
-
|
|
1100
|
+
// Public availability lookup (pre-session) — skip the bearer preflight.
|
|
1101
|
+
return await this.makeRequest('GET', `/auth/check-email/${email}`, undefined, { cache: false, skipAuth: true });
|
|
1080
1102
|
} catch (error) {
|
|
1081
1103
|
throw this.handleError(error);
|
|
1082
1104
|
}
|
|
@@ -1099,7 +1121,7 @@ export function OxyServicesAuthMixin<T extends typeof OxyServicesBase>(Base: T)
|
|
|
1099
1121
|
'POST',
|
|
1100
1122
|
'/auth/webauthn/register/options',
|
|
1101
1123
|
{ ...(username !== undefined ? { username } : {}) },
|
|
1102
|
-
{ cache: false },
|
|
1124
|
+
{ cache: false, ...(username !== undefined ? { skipAuth: true } : {}) },
|
|
1103
1125
|
);
|
|
1104
1126
|
} catch (error) {
|
|
1105
1127
|
throw this.handleError(error);
|
|
@@ -1133,7 +1155,7 @@ export function OxyServicesAuthMixin<T extends typeof OxyServicesBase>(Base: T)
|
|
|
1133
1155
|
'POST',
|
|
1134
1156
|
'/auth/webauthn/register/verify',
|
|
1135
1157
|
{ response, ...envelope },
|
|
1136
|
-
{ cache: false },
|
|
1158
|
+
{ cache: false, ...(envelope.username !== undefined ? { skipAuth: true } : {}) },
|
|
1137
1159
|
);
|
|
1138
1160
|
if (res && typeof res === 'object') {
|
|
1139
1161
|
const record = res as Record<string, unknown>;
|
|
@@ -1176,7 +1198,8 @@ export function OxyServicesAuthMixin<T extends typeof OxyServicesBase>(Base: T)
|
|
|
1176
1198
|
'POST',
|
|
1177
1199
|
'/auth/webauthn/login/options',
|
|
1178
1200
|
{ ...(username !== undefined ? { username } : {}) },
|
|
1179
|
-
|
|
1201
|
+
// Pre-session login ceremony — skip the bearer preflight.
|
|
1202
|
+
{ cache: false, skipAuth: true },
|
|
1180
1203
|
);
|
|
1181
1204
|
} catch (error) {
|
|
1182
1205
|
throw this.handleError(error);
|
|
@@ -1200,7 +1223,8 @@ export function OxyServicesAuthMixin<T extends typeof OxyServicesBase>(Base: T)
|
|
|
1200
1223
|
'POST',
|
|
1201
1224
|
'/auth/webauthn/login/verify',
|
|
1202
1225
|
{ response, ...envelope },
|
|
1203
|
-
|
|
1226
|
+
// Pre-session login ceremony — skip the bearer preflight.
|
|
1227
|
+
{ cache: false, skipAuth: true },
|
|
1204
1228
|
);
|
|
1205
1229
|
const parsed = safeParseContract(loginResultSchema, res);
|
|
1206
1230
|
if (!parsed) {
|
|
@@ -1233,7 +1257,7 @@ export function OxyServicesAuthMixin<T extends typeof OxyServicesBase>(Base: T)
|
|
|
1233
1257
|
clientId: params.clientId,
|
|
1234
1258
|
redirectUri: params.redirectUri,
|
|
1235
1259
|
codeVerifier: params.codeVerifier,
|
|
1236
|
-
}, { cache: false });
|
|
1260
|
+
}, { cache: false, skipAuth: true });
|
|
1237
1261
|
const payload =
|
|
1238
1262
|
(res as { data?: Record<string, unknown> }).data ??
|
|
1239
1263
|
(res as Record<string, unknown>);
|
|
@@ -105,7 +105,12 @@ export function OxyServicesConnectedAppsMixin<T extends typeof OxyServicesBase>(
|
|
|
105
105
|
'GET',
|
|
106
106
|
`/auth/oauth/client/${encodeURIComponent(clientId)}`,
|
|
107
107
|
undefined,
|
|
108
|
-
{
|
|
108
|
+
{
|
|
109
|
+
cache: true,
|
|
110
|
+
cacheTTL: CACHE_TIMES.MEDIUM,
|
|
111
|
+
// Public client metadata (pre-session consent UI) — skip the bearer preflight.
|
|
112
|
+
skipAuth: true,
|
|
113
|
+
},
|
|
109
114
|
);
|
|
110
115
|
return res.application;
|
|
111
116
|
} catch (error) {
|
|
@@ -58,7 +58,10 @@ export function OxyServicesDeviceBootMixin<T extends typeof OxyServicesBase>(Bas
|
|
|
58
58
|
'POST',
|
|
59
59
|
'/session/device/token',
|
|
60
60
|
{ deviceId, deviceSecret },
|
|
61
|
-
|
|
61
|
+
// `bypassQueue`: this mint is the control-plane call the auth lane
|
|
62
|
+
// depends on — it must run even when every RequestQueue slot is parked
|
|
63
|
+
// awaiting it, or the whole client deadlocks. See RequestOptions.bypassQueue.
|
|
64
|
+
{ cache: false, skipAuth: true, retry: false, bypassQueue: true },
|
|
62
65
|
);
|
|
63
66
|
const parsed = safeParseContract(deviceTokenMintResponseSchema, res);
|
|
64
67
|
if (!parsed) {
|
|
@@ -99,7 +102,9 @@ export function OxyServicesDeviceBootMixin<T extends typeof OxyServicesBase>(Bas
|
|
|
99
102
|
'POST',
|
|
100
103
|
'/session/device/redeem-ticket',
|
|
101
104
|
{ ticket, returnOrigin },
|
|
102
|
-
|
|
105
|
+
// Public device-hub sync mint (bearer-less). Same control-plane class as
|
|
106
|
+
// the device-secret mint — bypassQueue so it never waits for a slot.
|
|
107
|
+
{ cache: false, skipAuth: true, bypassQueue: true },
|
|
103
108
|
);
|
|
104
109
|
const parsed = safeParseContract(deviceHubTicketRedeemResponseSchema, res);
|
|
105
110
|
if (!parsed) {
|
|
@@ -218,6 +218,19 @@ export function OxyServicesIdentityBackupMixin<T extends typeof OxyServicesBase>
|
|
|
218
218
|
);
|
|
219
219
|
const payload = JSON.parse(new TextDecoder().decode(plaintext)) as BackupPayload;
|
|
220
220
|
|
|
221
|
+
if (!payload.privateKey || !payload.publicKey) {
|
|
222
|
+
throw new Error('Backup payload is missing key material');
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const derivedFromPhrase = await RecoveryPhraseService.derivePublicKeyFromPhrase(phrase);
|
|
226
|
+
const derivedFromPrivate = KeyManager.derivePublicKey(payload.privateKey);
|
|
227
|
+
const phrasePk = derivedFromPhrase.toLowerCase();
|
|
228
|
+
const payloadPk = payload.publicKey.toLowerCase();
|
|
229
|
+
const privatePk = derivedFromPrivate.toLowerCase();
|
|
230
|
+
if (phrasePk !== payloadPk || privatePk !== payloadPk) {
|
|
231
|
+
throw new Error('Backup payload does not match the recovery phrase');
|
|
232
|
+
}
|
|
233
|
+
|
|
221
234
|
// Persist the recovered key. Native-only; refuses to clobber a different
|
|
222
235
|
// identity unless overwrite — the IdentityAlreadyExistsError propagates.
|
|
223
236
|
return await KeyManager.importKeyPair(payload.privateKey, {
|
|
@@ -186,6 +186,8 @@ export function OxyServicesUserMixin<T extends typeof OxyServicesBase>(Base: T)
|
|
|
186
186
|
return await this.makeRequest('GET', `/auth/lookup/${encodeURIComponent(username)}`, undefined, {
|
|
187
187
|
cache: true,
|
|
188
188
|
cacheTTL: 60 * 1000, // 1 minute cache
|
|
189
|
+
// Public login-flow lookup (pre-session) — skip the bearer preflight.
|
|
190
|
+
skipAuth: true,
|
|
189
191
|
});
|
|
190
192
|
}
|
|
191
193
|
|
|
@@ -606,6 +608,10 @@ export function OxyServicesUserMixin<T extends typeof OxyServicesBase>(Base: T)
|
|
|
606
608
|
const result = await this.makeRequest<PrivacySettings>('PATCH', `/privacy/${id}/privacy`, settings, {
|
|
607
609
|
cache: false,
|
|
608
610
|
});
|
|
611
|
+
this.clearCacheByPrefix('GET:/session/user/');
|
|
612
|
+
this.clearCacheByPrefix('GET:/users/me');
|
|
613
|
+
this.clearCacheByPrefix('GET:/profiles/username/');
|
|
614
|
+
this.clearCacheEntry(`GET:/users/${id}`);
|
|
609
615
|
this.clearCacheEntry(`GET:/privacy/${id}/privacy`);
|
|
610
616
|
return result;
|
|
611
617
|
} catch (error) {
|
|
@@ -31,17 +31,19 @@ describe('OxyServices.deviceBoot', () => {
|
|
|
31
31
|
},
|
|
32
32
|
};
|
|
33
33
|
|
|
34
|
-
it('POSTs deviceId + deviceSecret with skipAuth + retry:false (no bearer, no cache, single attempt) and returns the validated mint', async () => {
|
|
34
|
+
it('POSTs deviceId + deviceSecret with skipAuth + retry:false + bypassQueue (no bearer, no cache, single attempt, never queued) and returns the validated mint', async () => {
|
|
35
35
|
makeRequest.mockResolvedValueOnce(MINT);
|
|
36
36
|
const result = await oxy.mintFromDeviceSecret('dev-1', 'ds-current-secret');
|
|
37
37
|
expect(result).toEqual(MINT);
|
|
38
38
|
// `retry: false` — the scheduler/401 lane own backoff; HttpService's inner
|
|
39
39
|
// retry here would only multiply cold-boot latency on a slow network.
|
|
40
|
+
// `bypassQueue: true` — the mint is the control-plane call the auth lane
|
|
41
|
+
// depends on; it must never wait for a RequestQueue slot (deadlock).
|
|
40
42
|
expect(makeRequest).toHaveBeenCalledWith(
|
|
41
43
|
'POST',
|
|
42
44
|
'/session/device/token',
|
|
43
45
|
{ deviceId: 'dev-1', deviceSecret: 'ds-current-secret' },
|
|
44
|
-
{ cache: false, skipAuth: true, retry: false },
|
|
46
|
+
{ cache: false, skipAuth: true, retry: false, bypassQueue: true },
|
|
45
47
|
);
|
|
46
48
|
});
|
|
47
49
|
|
|
@@ -57,7 +57,7 @@ describe('OxyServices — "Sign in with Oxy" handoff', () => {
|
|
|
57
57
|
'POST',
|
|
58
58
|
'/auth/session/create',
|
|
59
59
|
{ sessionToken: 'secret-session-token', expiresAt: 1700000300000, clientId: 'oxy_dk_test' },
|
|
60
|
-
expect.objectContaining({ cache: false }),
|
|
60
|
+
expect.objectContaining({ cache: false, skipAuth: true }),
|
|
61
61
|
);
|
|
62
62
|
// ...and the handle carries the SECRET token + the server's public code/payload.
|
|
63
63
|
expect(handle).toEqual({
|
|
@@ -251,7 +251,7 @@ describe('OxyServices — "Sign in with Oxy" handoff', () => {
|
|
|
251
251
|
'POST',
|
|
252
252
|
'/auth/challenge',
|
|
253
253
|
{ publicKey: 'pub-x' },
|
|
254
|
-
{ cache: false },
|
|
254
|
+
{ cache: false, skipAuth: true },
|
|
255
255
|
);
|
|
256
256
|
|
|
257
257
|
await oxy.requestChallenge('pub-x', { retry: false });
|
|
@@ -259,7 +259,7 @@ describe('OxyServices — "Sign in with Oxy" handoff', () => {
|
|
|
259
259
|
'POST',
|
|
260
260
|
'/auth/challenge',
|
|
261
261
|
{ publicKey: 'pub-x' },
|
|
262
|
-
{ cache: false, retry: false },
|
|
262
|
+
{ cache: false, skipAuth: true, retry: false },
|
|
263
263
|
);
|
|
264
264
|
});
|
|
265
265
|
|
|
@@ -282,7 +282,7 @@ describe('OxyServices — "Sign in with Oxy" handoff', () => {
|
|
|
282
282
|
deviceName: 'dev',
|
|
283
283
|
deviceFingerprint: 'fp',
|
|
284
284
|
},
|
|
285
|
-
{ cache: false, retry: false, timeout: 9000 },
|
|
285
|
+
{ cache: false, skipAuth: true, retry: false, timeout: 9000 },
|
|
286
286
|
);
|
|
287
287
|
});
|
|
288
288
|
});
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
import { OxyServices } from '../../OxyServices';
|
|
16
16
|
import { KeyManager, IdentityAlreadyExistsError } from '../../crypto/keyManager';
|
|
17
17
|
import { RecoveryPhraseService } from '../../crypto/recoveryPhrase';
|
|
18
|
+
import { encryptAead, decryptAead } from '../../crypto/aead';
|
|
18
19
|
import type { EncryptedBackupEnvelope, BackupUploadRequest } from '@oxyhq/contracts';
|
|
19
20
|
|
|
20
21
|
const FIXED_PHRASE =
|
|
@@ -192,6 +193,62 @@ describe('encrypted identity backup mixin', () => {
|
|
|
192
193
|
await expect(oxy.restoreFromEncryptedBackup(FIXED_PHRASE)).rejects.toThrow();
|
|
193
194
|
});
|
|
194
195
|
|
|
196
|
+
it('rejects when decrypted payload publicKey does not match the recovery phrase', async () => {
|
|
197
|
+
fetchMock.mockResolvedValueOnce(plainResponse({ exists: true }));
|
|
198
|
+
await oxy.createEncryptedBackup(FIXED_PHRASE);
|
|
199
|
+
const uploaded = uploadBodyFromCall(0);
|
|
200
|
+
|
|
201
|
+
const { backupKey } = await RecoveryPhraseService.deriveBackupMaterial(FIXED_PHRASE);
|
|
202
|
+
const aad = new TextEncoder().encode(
|
|
203
|
+
JSON.stringify({ version: uploaded.version, publicKeyHint: uploaded.publicKeyHint }),
|
|
204
|
+
);
|
|
205
|
+
const fromHex = (hex: string): Uint8Array => {
|
|
206
|
+
const out = new Uint8Array(hex.length / 2);
|
|
207
|
+
for (let i = 0; i < out.length; i += 1) {
|
|
208
|
+
out[i] = Number.parseInt(hex.slice(i * 2, i * 2 + 2), 16);
|
|
209
|
+
}
|
|
210
|
+
return out;
|
|
211
|
+
};
|
|
212
|
+
const toHex = (bytes: Uint8Array): string =>
|
|
213
|
+
Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join('');
|
|
214
|
+
|
|
215
|
+
const plaintext = decryptAead(
|
|
216
|
+
backupKey,
|
|
217
|
+
fromHex(uploaded.nonce),
|
|
218
|
+
fromHex(uploaded.ciphertext),
|
|
219
|
+
aad,
|
|
220
|
+
);
|
|
221
|
+
const payload = JSON.parse(new TextDecoder().decode(plaintext)) as {
|
|
222
|
+
privateKey: string;
|
|
223
|
+
publicKey: string;
|
|
224
|
+
createdAt: string;
|
|
225
|
+
};
|
|
226
|
+
payload.publicKey = `${payload.publicKey.slice(0, -1)}0`;
|
|
227
|
+
|
|
228
|
+
const { nonce, ciphertext } = encryptAead(
|
|
229
|
+
backupKey,
|
|
230
|
+
new TextEncoder().encode(JSON.stringify(payload)),
|
|
231
|
+
aad,
|
|
232
|
+
);
|
|
233
|
+
const tampered: EncryptedBackupEnvelope = {
|
|
234
|
+
version: uploaded.version,
|
|
235
|
+
algorithm: uploaded.algorithm,
|
|
236
|
+
kdfInfo: uploaded.kdfInfo,
|
|
237
|
+
nonce: toHex(nonce),
|
|
238
|
+
ciphertext: toHex(ciphertext),
|
|
239
|
+
publicKeyHint: uploaded.publicKeyHint,
|
|
240
|
+
createdAt: uploaded.createdAt,
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
const importSpy = jest.spyOn(KeyManager, 'importKeyPair').mockResolvedValue('x');
|
|
244
|
+
fetchMock.mockResolvedValueOnce(plainResponse(tampered));
|
|
245
|
+
|
|
246
|
+
await expect(oxy.restoreFromEncryptedBackup(FIXED_PHRASE)).rejects.toThrow(
|
|
247
|
+
/does not match the recovery phrase/,
|
|
248
|
+
);
|
|
249
|
+
expect(importSpy).not.toHaveBeenCalled();
|
|
250
|
+
});
|
|
251
|
+
|
|
195
252
|
it('propagates IdentityAlreadyExistsError UNCHANGED (so the caller can offer overwrite)', async () => {
|
|
196
253
|
fetchMock.mockResolvedValueOnce(plainResponse({ exists: true }));
|
|
197
254
|
await oxy.createEncryptedBackup(FIXED_PHRASE);
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Regression: every purely pre-session public SDK endpoint must pass `skipAuth`
|
|
3
|
+
* so a pending refresh handler cannot self-await (see httpServiceAuthSelfAwait).
|
|
4
|
+
*/
|
|
5
|
+
import { OxyServices } from '../../OxyServices';
|
|
6
|
+
import { SignatureService } from '../../crypto/signatureService';
|
|
7
|
+
|
|
8
|
+
describe('pre-session public endpoints use skipAuth', () => {
|
|
9
|
+
let oxy: OxyServices;
|
|
10
|
+
let makeRequest: jest.SpyInstance;
|
|
11
|
+
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
oxy = new OxyServices({ baseURL: 'http://test.invalid' });
|
|
14
|
+
makeRequest = jest.spyOn(oxy, 'makeRequest').mockResolvedValue({} as never);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
afterEach(() => jest.restoreAllMocks());
|
|
18
|
+
|
|
19
|
+
it('lookupUsername skips auth preflight', async () => {
|
|
20
|
+
await oxy.lookupUsername('alice');
|
|
21
|
+
expect(makeRequest).toHaveBeenCalledWith(
|
|
22
|
+
'GET',
|
|
23
|
+
'/auth/lookup/alice',
|
|
24
|
+
undefined,
|
|
25
|
+
expect.objectContaining({ skipAuth: true }),
|
|
26
|
+
);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('getPublicApplication skips auth preflight', async () => {
|
|
30
|
+
makeRequest.mockResolvedValueOnce({ application: { id: 'app-1', name: 'App' } });
|
|
31
|
+
await oxy.getPublicApplication('oxy_dk_test');
|
|
32
|
+
expect(makeRequest).toHaveBeenCalledWith(
|
|
33
|
+
'GET',
|
|
34
|
+
'/auth/oauth/client/oxy_dk_test',
|
|
35
|
+
undefined,
|
|
36
|
+
expect.objectContaining({ skipAuth: true }),
|
|
37
|
+
);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('claimSessionByToken skips auth preflight', async () => {
|
|
41
|
+
makeRequest.mockResolvedValueOnce({
|
|
42
|
+
accessToken: 'tok',
|
|
43
|
+
sessionId: 's1',
|
|
44
|
+
deviceId: 'd1',
|
|
45
|
+
expiresAt: '2030-01-01T00:00:00.000Z',
|
|
46
|
+
user: { id: 'u1', username: 'u' },
|
|
47
|
+
});
|
|
48
|
+
await oxy.claimSessionByToken('secret-session-token');
|
|
49
|
+
expect(makeRequest).toHaveBeenCalledWith(
|
|
50
|
+
'POST',
|
|
51
|
+
'/auth/session/claim',
|
|
52
|
+
{ sessionToken: 'secret-session-token' },
|
|
53
|
+
expect.objectContaining({ skipAuth: true, retry: false }),
|
|
54
|
+
);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('startCommonsSignIn skips auth preflight', async () => {
|
|
58
|
+
jest.spyOn(Date, 'now').mockReturnValue(1700000000000);
|
|
59
|
+
const generateChallenge = jest
|
|
60
|
+
.spyOn(SignatureService, 'generateChallenge')
|
|
61
|
+
.mockResolvedValue('secret-session-token');
|
|
62
|
+
makeRequest.mockResolvedValueOnce({
|
|
63
|
+
authorizeCode: 'code-1',
|
|
64
|
+
qrPayload: 'oxycommons://approve?v=1&code=code-1',
|
|
65
|
+
status: 'pending',
|
|
66
|
+
});
|
|
67
|
+
await oxy.startCommonsSignIn({ clientId: 'oxy_dk_test' });
|
|
68
|
+
expect(makeRequest).toHaveBeenCalledWith(
|
|
69
|
+
'POST',
|
|
70
|
+
'/auth/session/create',
|
|
71
|
+
expect.objectContaining({ sessionToken: 'secret-session-token', clientId: 'oxy_dk_test' }),
|
|
72
|
+
expect.objectContaining({ cache: false, skipAuth: true }),
|
|
73
|
+
);
|
|
74
|
+
generateChallenge.mockRestore();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('getUserByPublicKey skips auth preflight', async () => {
|
|
78
|
+
makeRequest.mockResolvedValueOnce({ id: 'u1', username: 'alice' });
|
|
79
|
+
await oxy.getUserByPublicKey('abc123');
|
|
80
|
+
expect(makeRequest).toHaveBeenCalledWith(
|
|
81
|
+
'GET',
|
|
82
|
+
'/auth/user/abc123',
|
|
83
|
+
undefined,
|
|
84
|
+
expect.objectContaining({ skipAuth: true }),
|
|
85
|
+
);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('webauthnRegisterOptions skips auth preflight on signup (username provided)', async () => {
|
|
89
|
+
await oxy.webauthnRegisterOptions('alice');
|
|
90
|
+
expect(makeRequest).toHaveBeenCalledWith(
|
|
91
|
+
'POST',
|
|
92
|
+
'/auth/webauthn/register/options',
|
|
93
|
+
{ username: 'alice' },
|
|
94
|
+
expect.objectContaining({ skipAuth: true }),
|
|
95
|
+
);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('webauthnRegisterVerify skips auth preflight on signup (username in envelope)', async () => {
|
|
99
|
+
makeRequest.mockResolvedValueOnce({
|
|
100
|
+
sessionId: 's1',
|
|
101
|
+
deviceId: 'd1',
|
|
102
|
+
accessToken: 'tok',
|
|
103
|
+
expiresAt: '2030-01-01T00:00:00.000Z',
|
|
104
|
+
deviceSecret: 'ds',
|
|
105
|
+
user: { id: 'u1', username: 'alice' },
|
|
106
|
+
});
|
|
107
|
+
await oxy.webauthnRegisterVerify({ id: 'cred' }, { username: 'alice' });
|
|
108
|
+
expect(makeRequest).toHaveBeenCalledWith(
|
|
109
|
+
'POST',
|
|
110
|
+
'/auth/webauthn/register/verify',
|
|
111
|
+
expect.objectContaining({ username: 'alice' }),
|
|
112
|
+
expect.objectContaining({ skipAuth: true }),
|
|
113
|
+
);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('getServiceToken skips auth preflight', async () => {
|
|
117
|
+
makeRequest.mockResolvedValueOnce({ token: 'svc-tok', expiresIn: 3600 });
|
|
118
|
+
await oxy.getServiceToken('oxy_dk_svc', 'secret-value');
|
|
119
|
+
expect(makeRequest).toHaveBeenCalledWith(
|
|
120
|
+
'POST',
|
|
121
|
+
'/auth/service-token',
|
|
122
|
+
{ apiKey: 'oxy_dk_svc', apiSecret: 'secret-value' },
|
|
123
|
+
expect.objectContaining({ skipAuth: true, cache: false, retry: false }),
|
|
124
|
+
);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('exchangeOAuthCode skips auth preflight', async () => {
|
|
128
|
+
makeRequest.mockResolvedValueOnce({
|
|
129
|
+
sessionId: 's1',
|
|
130
|
+
deviceId: 'd1',
|
|
131
|
+
accessToken: 'tok',
|
|
132
|
+
user: { id: 'u1' },
|
|
133
|
+
});
|
|
134
|
+
await oxy.exchangeOAuthCode({
|
|
135
|
+
code: 'code-1',
|
|
136
|
+
clientId: 'oxy_dk_test',
|
|
137
|
+
redirectUri: 'https://app.example/callback',
|
|
138
|
+
codeVerifier: 'verifier',
|
|
139
|
+
});
|
|
140
|
+
expect(makeRequest).toHaveBeenCalledWith(
|
|
141
|
+
'POST',
|
|
142
|
+
'/auth/oauth/token',
|
|
143
|
+
{
|
|
144
|
+
code: 'code-1',
|
|
145
|
+
clientId: 'oxy_dk_test',
|
|
146
|
+
redirectUri: 'https://app.example/callback',
|
|
147
|
+
codeVerifier: 'verifier',
|
|
148
|
+
},
|
|
149
|
+
expect.objectContaining({ skipAuth: true }),
|
|
150
|
+
);
|
|
151
|
+
});
|
|
152
|
+
});
|
|
@@ -129,6 +129,7 @@ describe('privacy cache invalidation', () => {
|
|
|
129
129
|
|
|
130
130
|
it('invalidates the exact logical keys on block/restrict/settings writes', async () => {
|
|
131
131
|
const clearSpy = jest.spyOn(oxy, 'clearCacheEntry');
|
|
132
|
+
const clearPrefixSpy = jest.spyOn(oxy, 'clearCacheByPrefix');
|
|
132
133
|
|
|
133
134
|
fetchMock.mockResolvedValueOnce(jsonResponse({ message: 'ok' }));
|
|
134
135
|
await oxy.blockUser('u1');
|
|
@@ -141,7 +142,12 @@ describe('privacy cache invalidation', () => {
|
|
|
141
142
|
fetchMock.mockResolvedValueOnce(jsonResponse({ isPrivateAccount: true }));
|
|
142
143
|
await oxy.updatePrivacySettings({ isPrivateAccount: true }, 'me');
|
|
143
144
|
expect(clearSpy).toHaveBeenCalledWith('GET:/privacy/me/privacy');
|
|
145
|
+
expect(clearSpy).toHaveBeenCalledWith('GET:/users/me');
|
|
146
|
+
expect(clearPrefixSpy).toHaveBeenCalledWith('GET:/session/user/');
|
|
147
|
+
expect(clearPrefixSpy).toHaveBeenCalledWith('GET:/users/me');
|
|
148
|
+
expect(clearPrefixSpy).toHaveBeenCalledWith('GET:/profiles/username/');
|
|
144
149
|
|
|
145
150
|
clearSpy.mockRestore();
|
|
151
|
+
clearPrefixSpy.mockRestore();
|
|
146
152
|
});
|
|
147
153
|
});
|
|
@@ -48,7 +48,7 @@ describe('webauthnRegisterOptions', () => {
|
|
|
48
48
|
'POST',
|
|
49
49
|
'/auth/webauthn/register/options',
|
|
50
50
|
{ username: 'alice' },
|
|
51
|
-
{ cache: false },
|
|
51
|
+
{ cache: false, skipAuth: true },
|
|
52
52
|
);
|
|
53
53
|
});
|
|
54
54
|
|
|
@@ -83,7 +83,7 @@ describe('webauthnLoginOptions', () => {
|
|
|
83
83
|
'POST',
|
|
84
84
|
'/auth/webauthn/login/options',
|
|
85
85
|
{ username: 'alice' },
|
|
86
|
-
{ cache: false },
|
|
86
|
+
{ cache: false, skipAuth: true },
|
|
87
87
|
);
|
|
88
88
|
});
|
|
89
89
|
|
|
@@ -94,7 +94,7 @@ describe('webauthnLoginOptions', () => {
|
|
|
94
94
|
'POST',
|
|
95
95
|
'/auth/webauthn/login/options',
|
|
96
96
|
{},
|
|
97
|
-
{ cache: false },
|
|
97
|
+
{ cache: false, skipAuth: true },
|
|
98
98
|
);
|
|
99
99
|
});
|
|
100
100
|
});
|
|
@@ -129,7 +129,7 @@ describe('webauthnRegisterVerify', () => {
|
|
|
129
129
|
'POST',
|
|
130
130
|
'/auth/webauthn/register/verify',
|
|
131
131
|
{ response: CREATE_RESPONSE, username: 'alice', deviceName: 'Phone', deviceFingerprint: 'fp-1' },
|
|
132
|
-
{ cache: false },
|
|
132
|
+
{ cache: false, skipAuth: true },
|
|
133
133
|
);
|
|
134
134
|
});
|
|
135
135
|
|
|
@@ -183,7 +183,7 @@ describe('webauthnLoginVerify', () => {
|
|
|
183
183
|
'POST',
|
|
184
184
|
'/auth/webauthn/login/verify',
|
|
185
185
|
{ response: GET_RESPONSE, deviceName: 'Phone', deviceFingerprint: 'fp-1', deviceId: 'dev-persisted' },
|
|
186
|
-
{ cache: false },
|
|
186
|
+
{ cache: false, skipAuth: true },
|
|
187
187
|
);
|
|
188
188
|
});
|
|
189
189
|
|
|
@@ -194,7 +194,7 @@ describe('webauthnLoginVerify', () => {
|
|
|
194
194
|
'POST',
|
|
195
195
|
'/auth/webauthn/login/verify',
|
|
196
196
|
{ response: GET_RESPONSE },
|
|
197
|
-
{ cache: false },
|
|
197
|
+
{ cache: false, skipAuth: true },
|
|
198
198
|
);
|
|
199
199
|
});
|
|
200
200
|
|