@oxyhq/core 12.10.0 → 12.10.2
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/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/mixins/OxyServices.auth.js +47 -19
- package/dist/cjs/mixins/OxyServices.connectedApps.js +6 -1
- package/dist/cjs/mixins/OxyServices.deviceBoot.js +9 -2
- package/dist/cjs/mixins/OxyServices.user.js +2 -0
- package/dist/cjs/session/accountDialogController.js +7 -5
- package/dist/esm/.tsbuildinfo +1 -1
- package/dist/esm/HttpService.js +22 -12
- 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/mixins/OxyServices.auth.js +47 -19
- package/dist/esm/mixins/OxyServices.connectedApps.js +6 -1
- package/dist/esm/mixins/OxyServices.deviceBoot.js +9 -2
- package/dist/esm/mixins/OxyServices.user.js +2 -0
- package/dist/esm/session/accountDialogController.js +7 -5
- package/dist/types/.tsbuildinfo +1 -1
- package/dist/types/HttpService.d.ts +14 -0
- package/dist/types/session/accountDialogController.d.ts +7 -2
- package/package.json +1 -1
- package/src/HttpService.ts +37 -14
- package/src/__tests__/httpServiceAuthSelfAwait.test.ts +108 -0
- package/src/__tests__/httpServiceQueueDeadlock.test.ts +128 -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/mixins/OxyServices.auth.ts +39 -19
- package/src/mixins/OxyServices.connectedApps.ts +6 -1
- package/src/mixins/OxyServices.deviceBoot.ts +7 -2
- package/src/mixins/OxyServices.user.ts +2 -0
- package/src/mixins/__tests__/OxyServices.deviceBoot.test.ts +4 -2
- package/src/mixins/__tests__/commonsSignIn.test.ts +4 -4
- package/src/mixins/__tests__/preSessionSkipAuth.test.ts +152 -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
|
@@ -112,6 +112,10 @@
|
|
|
112
112
|
}
|
|
113
113
|
},
|
|
114
114
|
"accountSwitcher": {
|
|
115
|
+
"sections": {
|
|
116
|
+
"yourAccounts": "내 계정"
|
|
117
|
+
},
|
|
118
|
+
"loading": "계정을 불러오는 중...",
|
|
115
119
|
"qrHeadline": "Oxy ID로 로그인",
|
|
116
120
|
"signInWithOxy": "Oxy로 로그인",
|
|
117
121
|
"useIdentityOnDevice": "Commons가 없으신가요? 이 기기의 패스키를 사용하세요",
|
|
@@ -112,6 +112,10 @@
|
|
|
112
112
|
}
|
|
113
113
|
},
|
|
114
114
|
"accountSwitcher": {
|
|
115
|
+
"sections": {
|
|
116
|
+
"yourAccounts": "As suas contas"
|
|
117
|
+
},
|
|
118
|
+
"loading": "A carregar contas...",
|
|
115
119
|
"qrHeadline": "Inicie sessão com a sua identidade Oxy",
|
|
116
120
|
"signInWithOxy": "Iniciar sessão com Oxy",
|
|
117
121
|
"useIdentityOnDevice": "Não tem o Commons? Use uma chave de acesso neste dispositivo",
|
|
@@ -371,7 +371,7 @@ export function OxyServicesAuthMixin<T extends typeof OxyServicesBase>(Base: T)
|
|
|
371
371
|
'POST',
|
|
372
372
|
'/auth/service-token',
|
|
373
373
|
{ apiKey: key, apiSecret: secret },
|
|
374
|
-
{ cache: false, retry: false }
|
|
374
|
+
{ cache: false, retry: false, skipAuth: true }
|
|
375
375
|
);
|
|
376
376
|
|
|
377
377
|
const expiresAt = Date.now() + response.expiresIn * 1000;
|
|
@@ -486,7 +486,7 @@ export function OxyServicesAuthMixin<T extends typeof OxyServicesBase>(Base: T)
|
|
|
486
486
|
publicKey,
|
|
487
487
|
signature,
|
|
488
488
|
timestamp,
|
|
489
|
-
}, { cache: false });
|
|
489
|
+
}, { cache: false, skipAuth: true });
|
|
490
490
|
|
|
491
491
|
if (!res || (typeof res === 'object' && Object.keys(res).length === 0)) {
|
|
492
492
|
throw new OxyAuthenticationError('Registration failed', 'REGISTER_FAILED', 400);
|
|
@@ -515,7 +515,7 @@ export function OxyServicesAuthMixin<T extends typeof OxyServicesBase>(Base: T)
|
|
|
515
515
|
try {
|
|
516
516
|
return await this.makeRequest<ChallengeResponse>('POST', '/auth/challenge', {
|
|
517
517
|
publicKey,
|
|
518
|
-
}, { cache: false, ...requestOptions });
|
|
518
|
+
}, { cache: false, skipAuth: true, ...requestOptions });
|
|
519
519
|
} catch (error) {
|
|
520
520
|
throw this.handleError(error);
|
|
521
521
|
}
|
|
@@ -552,7 +552,7 @@ export function OxyServicesAuthMixin<T extends typeof OxyServicesBase>(Base: T)
|
|
|
552
552
|
timestamp,
|
|
553
553
|
deviceName,
|
|
554
554
|
deviceFingerprint,
|
|
555
|
-
}, { cache: false, ...requestOptions });
|
|
555
|
+
}, { cache: false, skipAuth: true, ...requestOptions });
|
|
556
556
|
|
|
557
557
|
// Plant the freshly-minted tokens, mirroring `claimSessionByToken`.
|
|
558
558
|
// `/auth/verify` returns the first access token (and refresh token) in
|
|
@@ -581,7 +581,7 @@ export function OxyServicesAuthMixin<T extends typeof OxyServicesBase>(Base: T)
|
|
|
581
581
|
'GET',
|
|
582
582
|
`/auth/check-publickey/${encodeURIComponent(publicKey)}`,
|
|
583
583
|
undefined,
|
|
584
|
-
{ cache: false }
|
|
584
|
+
{ cache: false, skipAuth: true }
|
|
585
585
|
);
|
|
586
586
|
} catch (error) {
|
|
587
587
|
throw this.handleError(error);
|
|
@@ -597,7 +597,12 @@ export function OxyServicesAuthMixin<T extends typeof OxyServicesBase>(Base: T)
|
|
|
597
597
|
'GET',
|
|
598
598
|
`/auth/user/${encodeURIComponent(publicKey)}`,
|
|
599
599
|
undefined,
|
|
600
|
-
{
|
|
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
|
+
},
|
|
601
606
|
);
|
|
602
607
|
return normalizeUserIdentity(user);
|
|
603
608
|
} catch (error) {
|
|
@@ -697,7 +702,8 @@ export function OxyServicesAuthMixin<T extends typeof OxyServicesBase>(Base: T)
|
|
|
697
702
|
sessionToken,
|
|
698
703
|
...(options.deviceFingerprint ? { deviceFingerprint: options.deviceFingerprint } : {}),
|
|
699
704
|
},
|
|
700
|
-
|
|
705
|
+
// Body-authenticated device-flow claim (no bearer) — skip the preflight.
|
|
706
|
+
{ cache: false, retry: false, skipAuth: true }
|
|
701
707
|
);
|
|
702
708
|
|
|
703
709
|
this.setTokens(res.accessToken);
|
|
@@ -806,7 +812,10 @@ export function OxyServicesAuthMixin<T extends typeof OxyServicesBase>(Base: T)
|
|
|
806
812
|
'POST',
|
|
807
813
|
'/auth/session/create',
|
|
808
814
|
{ sessionToken, expiresAt, clientId: params.clientId },
|
|
809
|
-
|
|
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 },
|
|
810
819
|
);
|
|
811
820
|
|
|
812
821
|
return {
|
|
@@ -836,7 +845,10 @@ export function OxyServicesAuthMixin<T extends typeof OxyServicesBase>(Base: T)
|
|
|
836
845
|
'GET',
|
|
837
846
|
`/auth/session/status/${encodeURIComponent(sessionToken)}`,
|
|
838
847
|
undefined,
|
|
839
|
-
|
|
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 }
|
|
840
852
|
);
|
|
841
853
|
} catch (error) {
|
|
842
854
|
throw this.handleError(error);
|
|
@@ -859,7 +871,9 @@ export function OxyServicesAuthMixin<T extends typeof OxyServicesBase>(Base: T)
|
|
|
859
871
|
'GET',
|
|
860
872
|
`/auth/session/approve-info/${encodeURIComponent(authorizeCode)}`,
|
|
861
873
|
undefined,
|
|
862
|
-
|
|
874
|
+
// Public (no auth required) — skip the bearer preflight (avoids the
|
|
875
|
+
// pre-session self-await class).
|
|
876
|
+
{ cache: false, skipAuth: true }
|
|
863
877
|
);
|
|
864
878
|
return {
|
|
865
879
|
application: raw.application,
|
|
@@ -917,7 +931,8 @@ export function OxyServicesAuthMixin<T extends typeof OxyServicesBase>(Base: T)
|
|
|
917
931
|
...(params.deviceName ? { deviceName: params.deviceName } : {}),
|
|
918
932
|
...(params.deviceFingerprint ? { deviceFingerprint: params.deviceFingerprint } : {}),
|
|
919
933
|
},
|
|
920
|
-
|
|
934
|
+
// Key-signed, cookieless (no bearer) — skip the preflight.
|
|
935
|
+
{ cache: false, skipAuth: true }
|
|
921
936
|
);
|
|
922
937
|
} catch (error) {
|
|
923
938
|
throw this.handleError(error);
|
|
@@ -936,7 +951,8 @@ export function OxyServicesAuthMixin<T extends typeof OxyServicesBase>(Base: T)
|
|
|
936
951
|
'POST',
|
|
937
952
|
`/auth/session/deny/${encodeURIComponent(authorizeCode)}`,
|
|
938
953
|
undefined,
|
|
939
|
-
|
|
954
|
+
// Public (no auth required) — skip the bearer preflight.
|
|
955
|
+
{ cache: false, skipAuth: true }
|
|
940
956
|
);
|
|
941
957
|
} catch (error) {
|
|
942
958
|
throw this.handleError(error);
|
|
@@ -1069,7 +1085,8 @@ export function OxyServicesAuthMixin<T extends typeof OxyServicesBase>(Base: T)
|
|
|
1069
1085
|
*/
|
|
1070
1086
|
async checkUsernameAvailability(username: string): Promise<{ available: boolean; message: string }> {
|
|
1071
1087
|
try {
|
|
1072
|
-
|
|
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 });
|
|
1073
1090
|
} catch (error) {
|
|
1074
1091
|
throw this.handleError(error);
|
|
1075
1092
|
}
|
|
@@ -1080,7 +1097,8 @@ export function OxyServicesAuthMixin<T extends typeof OxyServicesBase>(Base: T)
|
|
|
1080
1097
|
*/
|
|
1081
1098
|
async checkEmailAvailability(email: string): Promise<{ available: boolean; message: string }> {
|
|
1082
1099
|
try {
|
|
1083
|
-
|
|
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 });
|
|
1084
1102
|
} catch (error) {
|
|
1085
1103
|
throw this.handleError(error);
|
|
1086
1104
|
}
|
|
@@ -1103,7 +1121,7 @@ export function OxyServicesAuthMixin<T extends typeof OxyServicesBase>(Base: T)
|
|
|
1103
1121
|
'POST',
|
|
1104
1122
|
'/auth/webauthn/register/options',
|
|
1105
1123
|
{ ...(username !== undefined ? { username } : {}) },
|
|
1106
|
-
{ cache: false },
|
|
1124
|
+
{ cache: false, ...(username !== undefined ? { skipAuth: true } : {}) },
|
|
1107
1125
|
);
|
|
1108
1126
|
} catch (error) {
|
|
1109
1127
|
throw this.handleError(error);
|
|
@@ -1137,7 +1155,7 @@ export function OxyServicesAuthMixin<T extends typeof OxyServicesBase>(Base: T)
|
|
|
1137
1155
|
'POST',
|
|
1138
1156
|
'/auth/webauthn/register/verify',
|
|
1139
1157
|
{ response, ...envelope },
|
|
1140
|
-
{ cache: false },
|
|
1158
|
+
{ cache: false, ...(envelope.username !== undefined ? { skipAuth: true } : {}) },
|
|
1141
1159
|
);
|
|
1142
1160
|
if (res && typeof res === 'object') {
|
|
1143
1161
|
const record = res as Record<string, unknown>;
|
|
@@ -1180,7 +1198,8 @@ export function OxyServicesAuthMixin<T extends typeof OxyServicesBase>(Base: T)
|
|
|
1180
1198
|
'POST',
|
|
1181
1199
|
'/auth/webauthn/login/options',
|
|
1182
1200
|
{ ...(username !== undefined ? { username } : {}) },
|
|
1183
|
-
|
|
1201
|
+
// Pre-session login ceremony — skip the bearer preflight.
|
|
1202
|
+
{ cache: false, skipAuth: true },
|
|
1184
1203
|
);
|
|
1185
1204
|
} catch (error) {
|
|
1186
1205
|
throw this.handleError(error);
|
|
@@ -1204,7 +1223,8 @@ export function OxyServicesAuthMixin<T extends typeof OxyServicesBase>(Base: T)
|
|
|
1204
1223
|
'POST',
|
|
1205
1224
|
'/auth/webauthn/login/verify',
|
|
1206
1225
|
{ response, ...envelope },
|
|
1207
|
-
|
|
1226
|
+
// Pre-session login ceremony — skip the bearer preflight.
|
|
1227
|
+
{ cache: false, skipAuth: true },
|
|
1208
1228
|
);
|
|
1209
1229
|
const parsed = safeParseContract(loginResultSchema, res);
|
|
1210
1230
|
if (!parsed) {
|
|
@@ -1237,7 +1257,7 @@ export function OxyServicesAuthMixin<T extends typeof OxyServicesBase>(Base: T)
|
|
|
1237
1257
|
clientId: params.clientId,
|
|
1238
1258
|
redirectUri: params.redirectUri,
|
|
1239
1259
|
codeVerifier: params.codeVerifier,
|
|
1240
|
-
}, { cache: false });
|
|
1260
|
+
}, { cache: false, skipAuth: true });
|
|
1241
1261
|
const payload =
|
|
1242
1262
|
(res as { data?: Record<string, unknown> }).data ??
|
|
1243
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) {
|
|
@@ -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
|
|
|
@@ -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
|
});
|
|
@@ -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
|
+
});
|
|
@@ -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
|
|
|
@@ -163,7 +163,7 @@ describe('AccountDialogController — initial + views', () => {
|
|
|
163
163
|
expect(snap.commonsAvailability).toBe('unknown');
|
|
164
164
|
});
|
|
165
165
|
|
|
166
|
-
it('setView / add
|
|
166
|
+
it('setView / add move between views and notify subscribers', () => {
|
|
167
167
|
const { controller } = makeHarness();
|
|
168
168
|
const seen: string[] = [];
|
|
169
169
|
controller.subscribe((s) => seen.push(s.view));
|
|
@@ -172,7 +172,7 @@ describe('AccountDialogController — initial + views', () => {
|
|
|
172
172
|
expect(controller.getSnapshot().view).toBe('add');
|
|
173
173
|
controller.setView('signin');
|
|
174
174
|
expect(controller.getSnapshot().view).toBe('signin');
|
|
175
|
-
controller.
|
|
175
|
+
controller.setView('accounts');
|
|
176
176
|
expect(controller.getSnapshot().view).toBe('accounts');
|
|
177
177
|
|
|
178
178
|
expect(seen).toEqual(['add', 'signin', 'accounts']);
|
|
@@ -185,7 +185,7 @@ describe('AccountDialogController — initial + views', () => {
|
|
|
185
185
|
|
|
186
186
|
controller.startSignup();
|
|
187
187
|
expect(controller.getSnapshot().view).toBe('signup');
|
|
188
|
-
controller.
|
|
188
|
+
controller.setView('accounts');
|
|
189
189
|
expect(controller.getSnapshot().view).toBe('accounts');
|
|
190
190
|
|
|
191
191
|
expect(seen).toEqual(['signup', 'accounts']);
|
|
@@ -35,6 +35,13 @@
|
|
|
35
35
|
* password, social login, and 2FA were removed ecosystem-wide. Account
|
|
36
36
|
* creation (`signup` view) is the same two identity backends: a passkey
|
|
37
37
|
* ceremony on web, or a Commons-created identity.
|
|
38
|
+
*
|
|
39
|
+
* The controller owns NO surface PRESENTATION: whether the account dialog is
|
|
40
|
+
* mounted, visible, or dismissed is the job of the shared surface stack
|
|
41
|
+
* (`@oxyhq/services` presents the `AccountDialog` surface into `@oxyhq/bloom`'s
|
|
42
|
+
* stack). This is a headless state machine only — it exposes `setView` /
|
|
43
|
+
* `add` / `startSignup` (the view axis) and `cancelSignIn` (device-flow
|
|
44
|
+
* teardown), never an `open` / `close` / `visible`.
|
|
38
45
|
*/
|
|
39
46
|
|
|
40
47
|
import type { OxyServices } from '../OxyServices';
|
|
@@ -462,12 +469,6 @@ export class AccountDialogController {
|
|
|
462
469
|
this.emit();
|
|
463
470
|
}
|
|
464
471
|
|
|
465
|
-
/** Return to the account list and cancel any in-flight sign-in flow. */
|
|
466
|
-
close(): void {
|
|
467
|
-
this.cancelSignIn();
|
|
468
|
-
this.setView('accounts');
|
|
469
|
-
}
|
|
470
|
-
|
|
471
472
|
/** Switch to the "add account" view (the sign-in entry chooser). */
|
|
472
473
|
add(): void {
|
|
473
474
|
this.setView('add');
|