@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.
Files changed (86) hide show
  1. package/dist/cjs/.tsbuildinfo +1 -1
  2. package/dist/cjs/HttpService.js +22 -12
  3. package/dist/cjs/i18n/locales/ar-SA.json +4 -0
  4. package/dist/cjs/i18n/locales/ca-ES.json +4 -0
  5. package/dist/cjs/i18n/locales/de-DE.json +4 -0
  6. package/dist/cjs/i18n/locales/en-US.json +69 -3
  7. package/dist/cjs/i18n/locales/es-ES.json +68 -2
  8. package/dist/cjs/i18n/locales/fr-FR.json +4 -0
  9. package/dist/cjs/i18n/locales/it-IT.json +4 -0
  10. package/dist/cjs/i18n/locales/ja-JP.json +4 -0
  11. package/dist/cjs/i18n/locales/ko-KR.json +4 -0
  12. package/dist/cjs/i18n/locales/locales/ar-SA.json +4 -0
  13. package/dist/cjs/i18n/locales/locales/ca-ES.json +4 -0
  14. package/dist/cjs/i18n/locales/locales/de-DE.json +4 -0
  15. package/dist/cjs/i18n/locales/locales/en-US.json +69 -3
  16. package/dist/cjs/i18n/locales/locales/es-ES.json +68 -2
  17. package/dist/cjs/i18n/locales/locales/fr-FR.json +4 -0
  18. package/dist/cjs/i18n/locales/locales/it-IT.json +4 -0
  19. package/dist/cjs/i18n/locales/locales/ja-JP.json +4 -0
  20. package/dist/cjs/i18n/locales/locales/ko-KR.json +4 -0
  21. package/dist/cjs/i18n/locales/locales/pt-PT.json +4 -0
  22. package/dist/cjs/i18n/locales/locales/zh-CN.json +4 -0
  23. package/dist/cjs/i18n/locales/pt-PT.json +4 -0
  24. package/dist/cjs/i18n/locales/zh-CN.json +4 -0
  25. package/dist/cjs/mixins/OxyServices.auth.js +47 -19
  26. package/dist/cjs/mixins/OxyServices.connectedApps.js +6 -1
  27. package/dist/cjs/mixins/OxyServices.deviceBoot.js +9 -2
  28. package/dist/cjs/mixins/OxyServices.user.js +2 -0
  29. package/dist/cjs/session/accountDialogController.js +7 -5
  30. package/dist/esm/.tsbuildinfo +1 -1
  31. package/dist/esm/HttpService.js +22 -12
  32. package/dist/esm/i18n/locales/ar-SA.json +4 -0
  33. package/dist/esm/i18n/locales/ca-ES.json +4 -0
  34. package/dist/esm/i18n/locales/de-DE.json +4 -0
  35. package/dist/esm/i18n/locales/en-US.json +69 -3
  36. package/dist/esm/i18n/locales/es-ES.json +68 -2
  37. package/dist/esm/i18n/locales/fr-FR.json +4 -0
  38. package/dist/esm/i18n/locales/it-IT.json +4 -0
  39. package/dist/esm/i18n/locales/ja-JP.json +4 -0
  40. package/dist/esm/i18n/locales/ko-KR.json +4 -0
  41. package/dist/esm/i18n/locales/locales/ar-SA.json +4 -0
  42. package/dist/esm/i18n/locales/locales/ca-ES.json +4 -0
  43. package/dist/esm/i18n/locales/locales/de-DE.json +4 -0
  44. package/dist/esm/i18n/locales/locales/en-US.json +69 -3
  45. package/dist/esm/i18n/locales/locales/es-ES.json +68 -2
  46. package/dist/esm/i18n/locales/locales/fr-FR.json +4 -0
  47. package/dist/esm/i18n/locales/locales/it-IT.json +4 -0
  48. package/dist/esm/i18n/locales/locales/ja-JP.json +4 -0
  49. package/dist/esm/i18n/locales/locales/ko-KR.json +4 -0
  50. package/dist/esm/i18n/locales/locales/pt-PT.json +4 -0
  51. package/dist/esm/i18n/locales/locales/zh-CN.json +4 -0
  52. package/dist/esm/i18n/locales/pt-PT.json +4 -0
  53. package/dist/esm/i18n/locales/zh-CN.json +4 -0
  54. package/dist/esm/mixins/OxyServices.auth.js +47 -19
  55. package/dist/esm/mixins/OxyServices.connectedApps.js +6 -1
  56. package/dist/esm/mixins/OxyServices.deviceBoot.js +9 -2
  57. package/dist/esm/mixins/OxyServices.user.js +2 -0
  58. package/dist/esm/session/accountDialogController.js +7 -5
  59. package/dist/types/.tsbuildinfo +1 -1
  60. package/dist/types/HttpService.d.ts +14 -0
  61. package/dist/types/session/accountDialogController.d.ts +7 -2
  62. package/package.json +1 -1
  63. package/src/HttpService.ts +37 -14
  64. package/src/__tests__/httpServiceAuthSelfAwait.test.ts +108 -0
  65. package/src/__tests__/httpServiceQueueDeadlock.test.ts +128 -0
  66. package/src/i18n/locales/ar-SA.json +4 -0
  67. package/src/i18n/locales/ca-ES.json +4 -0
  68. package/src/i18n/locales/de-DE.json +4 -0
  69. package/src/i18n/locales/en-US.json +69 -3
  70. package/src/i18n/locales/es-ES.json +68 -2
  71. package/src/i18n/locales/fr-FR.json +4 -0
  72. package/src/i18n/locales/it-IT.json +4 -0
  73. package/src/i18n/locales/ja-JP.json +4 -0
  74. package/src/i18n/locales/ko-KR.json +4 -0
  75. package/src/i18n/locales/pt-PT.json +4 -0
  76. package/src/i18n/locales/zh-CN.json +4 -0
  77. package/src/mixins/OxyServices.auth.ts +39 -19
  78. package/src/mixins/OxyServices.connectedApps.ts +6 -1
  79. package/src/mixins/OxyServices.deviceBoot.ts +7 -2
  80. package/src/mixins/OxyServices.user.ts +2 -0
  81. package/src/mixins/__tests__/OxyServices.deviceBoot.test.ts +4 -2
  82. package/src/mixins/__tests__/commonsSignIn.test.ts +4 -4
  83. package/src/mixins/__tests__/preSessionSkipAuth.test.ts +152 -0
  84. package/src/mixins/__tests__/webauthnAuth.test.ts +6 -6
  85. package/src/session/__tests__/accountDialogController.test.ts +3 -3
  86. 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",
@@ -112,6 +112,10 @@
112
112
  }
113
113
  },
114
114
  "accountSwitcher": {
115
+ "sections": {
116
+ "yourAccounts": "你的账户"
117
+ },
118
+ "loading": "正在加载账户...",
115
119
  "qrHeadline": "使用您的 Oxy 身份登录",
116
120
  "signInWithOxy": "使用 Oxy 登录",
117
121
  "useIdentityOnDevice": "没有 Commons?改用此设备上的通行密钥",
@@ -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
- { cache: true, cacheTTL: 2 * 60 * 1000 }
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
- { cache: false, retry: false }
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
- { cache: false }
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
- { cache: false, retry: false }
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
- { cache: false }
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
- { cache: false }
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
- { cache: false }
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
- return await this.makeRequest('GET', `/auth/check-username/${username}`, undefined, { cache: false });
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
- return await this.makeRequest('GET', `/auth/check-email/${email}`, undefined, { cache: false });
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
- { cache: false },
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
- { cache: false },
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
- { cache: true, cacheTTL: CACHE_TIMES.MEDIUM },
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
- { cache: false, skipAuth: true, retry: false },
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
- { cache: false, skipAuth: true },
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 / close move between views and notify subscribers', () => {
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.close();
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.close();
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');