@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.
Files changed (106) hide show
  1. package/dist/cjs/.tsbuildinfo +1 -1
  2. package/dist/cjs/HttpService.js +22 -12
  3. package/dist/cjs/crypto/keyManager.js +89 -0
  4. package/dist/cjs/i18n/locales/ar-SA.json +4 -0
  5. package/dist/cjs/i18n/locales/ca-ES.json +4 -0
  6. package/dist/cjs/i18n/locales/de-DE.json +4 -0
  7. package/dist/cjs/i18n/locales/en-US.json +69 -3
  8. package/dist/cjs/i18n/locales/es-ES.json +68 -2
  9. package/dist/cjs/i18n/locales/fr-FR.json +4 -0
  10. package/dist/cjs/i18n/locales/it-IT.json +4 -0
  11. package/dist/cjs/i18n/locales/ja-JP.json +4 -0
  12. package/dist/cjs/i18n/locales/ko-KR.json +4 -0
  13. package/dist/cjs/i18n/locales/locales/ar-SA.json +4 -0
  14. package/dist/cjs/i18n/locales/locales/ca-ES.json +4 -0
  15. package/dist/cjs/i18n/locales/locales/de-DE.json +4 -0
  16. package/dist/cjs/i18n/locales/locales/en-US.json +69 -3
  17. package/dist/cjs/i18n/locales/locales/es-ES.json +68 -2
  18. package/dist/cjs/i18n/locales/locales/fr-FR.json +4 -0
  19. package/dist/cjs/i18n/locales/locales/it-IT.json +4 -0
  20. package/dist/cjs/i18n/locales/locales/ja-JP.json +4 -0
  21. package/dist/cjs/i18n/locales/locales/ko-KR.json +4 -0
  22. package/dist/cjs/i18n/locales/locales/pt-PT.json +4 -0
  23. package/dist/cjs/i18n/locales/locales/zh-CN.json +4 -0
  24. package/dist/cjs/i18n/locales/pt-PT.json +4 -0
  25. package/dist/cjs/i18n/locales/zh-CN.json +4 -0
  26. package/dist/cjs/index.js +7 -4
  27. package/dist/cjs/mixins/OxyServices.auth.js +51 -20
  28. package/dist/cjs/mixins/OxyServices.connectedApps.js +6 -1
  29. package/dist/cjs/mixins/OxyServices.deviceBoot.js +9 -2
  30. package/dist/cjs/mixins/OxyServices.identityBackup.js +11 -0
  31. package/dist/cjs/mixins/OxyServices.user.js +6 -0
  32. package/dist/cjs/session/accountDialogController.js +7 -5
  33. package/dist/cjs/utils/commonsApproval.js +31 -0
  34. package/dist/esm/.tsbuildinfo +1 -1
  35. package/dist/esm/HttpService.js +22 -12
  36. package/dist/esm/crypto/keyManager.js +89 -0
  37. package/dist/esm/i18n/locales/ar-SA.json +4 -0
  38. package/dist/esm/i18n/locales/ca-ES.json +4 -0
  39. package/dist/esm/i18n/locales/de-DE.json +4 -0
  40. package/dist/esm/i18n/locales/en-US.json +69 -3
  41. package/dist/esm/i18n/locales/es-ES.json +68 -2
  42. package/dist/esm/i18n/locales/fr-FR.json +4 -0
  43. package/dist/esm/i18n/locales/it-IT.json +4 -0
  44. package/dist/esm/i18n/locales/ja-JP.json +4 -0
  45. package/dist/esm/i18n/locales/ko-KR.json +4 -0
  46. package/dist/esm/i18n/locales/locales/ar-SA.json +4 -0
  47. package/dist/esm/i18n/locales/locales/ca-ES.json +4 -0
  48. package/dist/esm/i18n/locales/locales/de-DE.json +4 -0
  49. package/dist/esm/i18n/locales/locales/en-US.json +69 -3
  50. package/dist/esm/i18n/locales/locales/es-ES.json +68 -2
  51. package/dist/esm/i18n/locales/locales/fr-FR.json +4 -0
  52. package/dist/esm/i18n/locales/locales/it-IT.json +4 -0
  53. package/dist/esm/i18n/locales/locales/ja-JP.json +4 -0
  54. package/dist/esm/i18n/locales/locales/ko-KR.json +4 -0
  55. package/dist/esm/i18n/locales/locales/pt-PT.json +4 -0
  56. package/dist/esm/i18n/locales/locales/zh-CN.json +4 -0
  57. package/dist/esm/i18n/locales/pt-PT.json +4 -0
  58. package/dist/esm/i18n/locales/zh-CN.json +4 -0
  59. package/dist/esm/index.js +2 -1
  60. package/dist/esm/mixins/OxyServices.auth.js +48 -19
  61. package/dist/esm/mixins/OxyServices.connectedApps.js +6 -1
  62. package/dist/esm/mixins/OxyServices.deviceBoot.js +9 -2
  63. package/dist/esm/mixins/OxyServices.identityBackup.js +11 -0
  64. package/dist/esm/mixins/OxyServices.user.js +6 -0
  65. package/dist/esm/session/accountDialogController.js +7 -5
  66. package/dist/esm/utils/commonsApproval.js +27 -0
  67. package/dist/types/.tsbuildinfo +1 -1
  68. package/dist/types/HttpService.d.ts +14 -0
  69. package/dist/types/crypto/keyManager.d.ts +33 -0
  70. package/dist/types/index.d.ts +2 -1
  71. package/dist/types/mixins/OxyServices.auth.d.ts +4 -4
  72. package/dist/types/session/accountDialogController.d.ts +7 -2
  73. package/dist/types/utils/commonsApproval.d.ts +13 -0
  74. package/package.json +3 -3
  75. package/src/HttpService.ts +37 -14
  76. package/src/__tests__/httpServiceAuthSelfAwait.test.ts +108 -0
  77. package/src/__tests__/httpServiceQueueDeadlock.test.ts +128 -0
  78. package/src/crypto/__tests__/keyManager.recoveryMnemonic.test.ts +138 -0
  79. package/src/crypto/keyManager.ts +99 -0
  80. package/src/i18n/locales/ar-SA.json +4 -0
  81. package/src/i18n/locales/ca-ES.json +4 -0
  82. package/src/i18n/locales/de-DE.json +4 -0
  83. package/src/i18n/locales/en-US.json +69 -3
  84. package/src/i18n/locales/es-ES.json +68 -2
  85. package/src/i18n/locales/fr-FR.json +4 -0
  86. package/src/i18n/locales/it-IT.json +4 -0
  87. package/src/i18n/locales/ja-JP.json +4 -0
  88. package/src/i18n/locales/ko-KR.json +4 -0
  89. package/src/i18n/locales/pt-PT.json +4 -0
  90. package/src/i18n/locales/zh-CN.json +4 -0
  91. package/src/index.ts +7 -1
  92. package/src/mixins/OxyServices.auth.ts +48 -24
  93. package/src/mixins/OxyServices.connectedApps.ts +6 -1
  94. package/src/mixins/OxyServices.deviceBoot.ts +7 -2
  95. package/src/mixins/OxyServices.identityBackup.ts +13 -0
  96. package/src/mixins/OxyServices.user.ts +6 -0
  97. package/src/mixins/__tests__/OxyServices.deviceBoot.test.ts +4 -2
  98. package/src/mixins/__tests__/commonsSignIn.test.ts +4 -4
  99. package/src/mixins/__tests__/identityBackup.test.ts +57 -0
  100. package/src/mixins/__tests__/preSessionSkipAuth.test.ts +152 -0
  101. package/src/mixins/__tests__/privacyCacheInvalidation.test.ts +6 -0
  102. package/src/mixins/__tests__/webauthnAuth.test.ts +6 -6
  103. package/src/session/__tests__/accountDialogController.test.ts +3 -3
  104. package/src/session/accountDialogController.ts +7 -6
  105. package/src/utils/__tests__/commonsApproval.test.ts +60 -0
  106. package/src/utils/commonsApproval.ts +39 -0
@@ -290,6 +290,8 @@
290
290
  "saving": "Guardando…",
291
291
  "helper": "El círculo recortado es lo que aparecerá en tu perfil. Pellizca para acercar, arrastra para colocar.",
292
292
  "zoom": "{{value}}×",
293
+ "zoomIn": "Acercar",
294
+ "zoomOut": "Alejar",
293
295
  "a11yImage": "Vista previa del recorte. Pellizca para acercar y arrastra para reposicionar la imagen.",
294
296
  "a11yReset": "Restablecer el recorte a la posición predeterminada",
295
297
  "a11yResetAnnouncement": "Recorte restablecido"
@@ -310,6 +312,47 @@
310
312
  },
311
313
  "changeAvatar": "Cambiar avatar"
312
314
  },
315
+ "changeAvatar": {
316
+ "title": "Cambiar foto de perfil",
317
+ "sources": {
318
+ "upload": {
319
+ "title": "Subir desde el dispositivo",
320
+ "description": "Elige una foto de tu galería"
321
+ },
322
+ "camera": {
323
+ "title": "Hacer una foto",
324
+ "description": "Usa tu cámara"
325
+ },
326
+ "files": {
327
+ "title": "Mis archivos de Oxy",
328
+ "description": "Elige una imagen que ya has subido"
329
+ },
330
+ "remove": {
331
+ "title": "Eliminar la foto actual",
332
+ "description": "Vuelve a tus iniciales"
333
+ }
334
+ },
335
+ "remove": {
336
+ "confirmTitle": "¿Eliminar la foto de perfil?",
337
+ "confirmMessage": "Tu perfil mostrará tus iniciales. Puedes añadir una foto nueva cuando quieras.",
338
+ "confirmLabel": "Eliminar"
339
+ },
340
+ "permission": {
341
+ "libraryDenied": "Oxy necesita acceso a tus fotos para subir una imagen.",
342
+ "cameraDenied": "Oxy necesita acceso a tu cámara para hacer una foto.",
343
+ "openSettings": "Abrir ajustes"
344
+ },
345
+ "errors": {
346
+ "pickerUnavailable": "La selección de fotos no está disponible en este dispositivo.",
347
+ "cameraUnavailable": "La cámara no está disponible en este dispositivo.",
348
+ "pickFailed": "No se pudo abrir el selector de fotos.",
349
+ "cameraFailed": "No se pudo abrir la cámara.",
350
+ "loadImageFailed": "No se pudo cargar la imagen seleccionada."
351
+ },
352
+ "a11y": {
353
+ "currentPhoto": "Tu foto de perfil actual"
354
+ }
355
+ },
313
356
  "common": {
314
357
  "actions": {
315
358
  "back": "Atrás",
@@ -619,7 +662,8 @@
619
662
  "getCommons": "Descargar Commons",
620
663
  "commonsNotInstalled": "¿No tienes Commons? Descarga la app para iniciar sesión con tu Oxy ID.",
621
664
  "showQrAnyway": "Tengo Commons en otro dispositivo",
622
- "switchWhileSignedInAs": "Cambiar de cuenta, con la sesión iniciada como {{name}}"
665
+ "switchWhileSignedInAs": "Cambiar de cuenta, con la sesión iniciada como {{name}}",
666
+ "manageOnDevice": "Gestionar cuentas en este dispositivo"
623
667
  },
624
668
  "feedback": {
625
669
  "type": {
@@ -1442,6 +1486,11 @@
1442
1486
  "uploadPhotos": "Subir fotos",
1443
1487
  "uploadFiles": "Subir archivos",
1444
1488
  "clearSearch": "Limpiar búsqueda",
1489
+ "retry": "Reintentar",
1490
+ "loadError": {
1491
+ "title": "No se pudieron cargar tus archivos",
1492
+ "description": "Algo salió mal. Comprueba tu conexión e inténtalo de nuevo."
1493
+ },
1445
1494
  "choosePhoto": "Elegir foto",
1446
1495
  "done": "Listo",
1447
1496
  "doneWithCount": "Listo ({{count}})",
@@ -1615,6 +1664,8 @@
1615
1664
  "accountMenu": {
1616
1665
  "label": "Menú de cuenta",
1617
1666
  "manage": "Gestiona tu cuenta de Oxy",
1667
+ "greeting": "¡Hola, {{name}}!",
1668
+ "switchAccount": "Cambiar de cuenta",
1618
1669
  "viewProfile": "Ver perfil",
1619
1670
  "addAnother": "Añadir otra cuenta",
1620
1671
  "signOutAll": "Cerrar sesión en todas las cuentas",
@@ -1622,7 +1673,22 @@
1622
1673
  "openHint": "Abre el menú de cuenta",
1623
1674
  "openWithUser": "Menú de cuenta de {{name}}",
1624
1675
  "switching": "Cambiando de cuenta…",
1625
- "signOutAccount": "Cerrar sesión de {{name}}"
1676
+ "signOutAccount": "Cerrar sesión de {{name}}",
1677
+ "storage": {
1678
+ "title": "Almacenamiento de Oxy",
1679
+ "usage": "{{used}} de {{total}} en uso",
1680
+ "used": "Usado",
1681
+ "free": "Libre",
1682
+ "unavailable": "Detalles de uso no disponibles",
1683
+ "upgrade": "Mejorar plan",
1684
+ "manage": "Gestionar almacenamiento"
1685
+ },
1686
+ "data": "Tus datos en Oxy",
1687
+ "settings": "Ajustes de Oxy",
1688
+ "help": "Ayuda y comentarios",
1689
+ "signOut": "Cerrar sesión",
1690
+ "privacy": "Política de privacidad",
1691
+ "terms": "Términos del servicio"
1626
1692
  },
1627
1693
  "accounts": {
1628
1694
  "activeAccount": {
@@ -112,6 +112,10 @@
112
112
  }
113
113
  },
114
114
  "accountSwitcher": {
115
+ "sections": {
116
+ "yourAccounts": "Vos comptes"
117
+ },
118
+ "loading": "Chargement des comptes...",
115
119
  "qrHeadline": "Connectez-vous avec votre identité Oxy",
116
120
  "signInWithOxy": "Se connecter avec Oxy",
117
121
  "useIdentityOnDevice": "Pas de Commons ? Utilisez une clé d'accès sur cet appareil",
@@ -112,6 +112,10 @@
112
112
  }
113
113
  },
114
114
  "accountSwitcher": {
115
+ "sections": {
116
+ "yourAccounts": "I tuoi account"
117
+ },
118
+ "loading": "Caricamento account...",
115
119
  "qrHeadline": "Accedi con la tua identità Oxy",
116
120
  "signInWithOxy": "Accedi con Oxy",
117
121
  "useIdentityOnDevice": "Non hai Commons? Usa una passkey su questo dispositivo",
@@ -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": "내 계정"
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?改用此设备上的通行密钥",
@@ -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?改用此设备上的通行密钥",
package/dist/esm/index.js CHANGED
@@ -26,7 +26,8 @@ export { OXY_CLOUD_URL, oxyClient } from './OxyServices.js';
26
26
  // ---------------------------------------------------------------------------
27
27
  // Authentication
28
28
  // ---------------------------------------------------------------------------
29
- export { ServiceCredentialMismatchError } from './mixins/OxyServices.auth.js';
29
+ export { ServiceCredentialMismatchError, } from './mixins/OxyServices.auth.js';
30
+ export { getCommonsApprovalBlockingReason, parseCommonsApprovalExpiresAt, } from './utils/commonsApproval.js';
30
31
  export { OxyAppDataIdentifierError } from './mixins/OxyServices.appData.js';
31
32
  // ---------------------------------------------------------------------------
32
33
  // User identity and handles
@@ -1,4 +1,5 @@
1
1
  import { loginResultSchema, safeParseContract } from '@oxyhq/contracts';
2
+ export { getCommonsApprovalBlockingReason, parseCommonsApprovalExpiresAt, } from '../utils/commonsApproval.js';
2
3
  import { OxyAuthenticationError } from '../OxyServices.errors.js';
3
4
  import { KeyManager } from '../crypto/keyManager.js';
4
5
  import { SignatureService } from '../crypto/signatureService.js';
@@ -181,7 +182,7 @@ export function OxyServicesAuthMixin(Base) {
181
182
  * @internal
182
183
  */
183
184
  async _doFetchServiceToken(key, secret, cacheKey, secretBuf) {
184
- const response = await this.makeRequest('POST', '/auth/service-token', { apiKey: key, apiSecret: secret }, { cache: false, retry: false });
185
+ const response = await this.makeRequest('POST', '/auth/service-token', { apiKey: key, apiSecret: secret }, { cache: false, retry: false, skipAuth: true });
185
186
  const expiresAt = Date.now() + response.expiresIn * 1000;
186
187
  // Update the entry in-place so any caller that already grabbed a reference
187
188
  // (via `_serviceTokenCache.get(...)`) sees the fresh state.
@@ -278,7 +279,7 @@ export function OxyServicesAuthMixin(Base) {
278
279
  publicKey,
279
280
  signature,
280
281
  timestamp,
281
- }, { cache: false });
282
+ }, { cache: false, skipAuth: true });
282
283
  if (!res || (typeof res === 'object' && Object.keys(res).length === 0)) {
283
284
  throw new OxyAuthenticationError('Registration failed', 'REGISTER_FAILED', 400);
284
285
  }
@@ -302,7 +303,7 @@ export function OxyServicesAuthMixin(Base) {
302
303
  try {
303
304
  return await this.makeRequest('POST', '/auth/challenge', {
304
305
  publicKey,
305
- }, { cache: false, ...requestOptions });
306
+ }, { cache: false, skipAuth: true, ...requestOptions });
306
307
  }
307
308
  catch (error) {
308
309
  throw this.handleError(error);
@@ -331,7 +332,7 @@ export function OxyServicesAuthMixin(Base) {
331
332
  timestamp,
332
333
  deviceName,
333
334
  deviceFingerprint,
334
- }, { cache: false, ...requestOptions });
335
+ }, { cache: false, skipAuth: true, ...requestOptions });
335
336
  // Plant the freshly-minted tokens, mirroring `claimSessionByToken`.
336
337
  // `/auth/verify` returns the first access token (and refresh token) in
337
338
  // its body, so installing it here means callers get an authenticated
@@ -354,7 +355,7 @@ export function OxyServicesAuthMixin(Base) {
354
355
  */
355
356
  async checkPublicKeyRegistered(publicKey) {
356
357
  try {
357
- return await this.makeRequest('GET', `/auth/check-publickey/${encodeURIComponent(publicKey)}`, undefined, { cache: false });
358
+ return await this.makeRequest('GET', `/auth/check-publickey/${encodeURIComponent(publicKey)}`, undefined, { cache: false, skipAuth: true });
358
359
  }
359
360
  catch (error) {
360
361
  throw this.handleError(error);
@@ -365,7 +366,12 @@ export function OxyServicesAuthMixin(Base) {
365
366
  */
366
367
  async getUserByPublicKey(publicKey) {
367
368
  try {
368
- const user = await this.makeRequest('GET', `/auth/user/${encodeURIComponent(publicKey)}`, undefined, { cache: true, cacheTTL: 2 * 60 * 1000 });
369
+ const user = await this.makeRequest('GET', `/auth/user/${encodeURIComponent(publicKey)}`, undefined, {
370
+ cache: true,
371
+ cacheTTL: 2 * 60 * 1000,
372
+ // Public lookup by public key (pre-session) — skip the bearer preflight.
373
+ skipAuth: true,
374
+ });
369
375
  return normalizeUserIdentity(user);
370
376
  }
371
377
  catch (error) {
@@ -436,7 +442,9 @@ export function OxyServicesAuthMixin(Base) {
436
442
  const res = await this.makeRequest('POST', '/auth/session/claim', {
437
443
  sessionToken,
438
444
  ...(options.deviceFingerprint ? { deviceFingerprint: options.deviceFingerprint } : {}),
439
- }, { cache: false, retry: false });
445
+ },
446
+ // Body-authenticated device-flow claim (no bearer) — skip the preflight.
447
+ { cache: false, retry: false, skipAuth: true });
440
448
  this.setTokens(res.accessToken);
441
449
  return res;
442
450
  }
@@ -520,7 +528,11 @@ export function OxyServicesAuthMixin(Base) {
520
528
  // QR. Reuses the platform-safe random generator.
521
529
  const sessionToken = await SignatureService.generateChallenge();
522
530
  const expiresAt = Date.now() + COMMONS_SIGN_IN_EXPIRY_MS;
523
- const res = await this.makeRequest('POST', '/auth/session/create', { sessionToken, expiresAt, clientId: params.clientId }, { cache: false });
531
+ const res = await this.makeRequest('POST', '/auth/session/create', { sessionToken, expiresAt, clientId: params.clientId },
532
+ // Public/pre-session (no bearer): skip the preflight so a stale
533
+ // near-expiry token cannot re-enter refreshAccessToken while the
534
+ // refresh handler is already in flight (self-await hang).
535
+ { cache: false, skipAuth: true });
524
536
  return {
525
537
  sessionToken,
526
538
  authorizeCode: res.authorizeCode,
@@ -544,7 +556,11 @@ export function OxyServicesAuthMixin(Base) {
544
556
  */
545
557
  async pollCommonsSignIn(sessionToken) {
546
558
  try {
547
- return await this.makeRequest('GET', `/auth/session/status/${encodeURIComponent(sessionToken)}`, undefined, { cache: false, retry: false });
559
+ return await this.makeRequest('GET', `/auth/session/status/${encodeURIComponent(sessionToken)}`, undefined,
560
+ // Public/pre-session (no bearer): a preflight here is wrong per se and,
561
+ // if ever reached while a refresh is pending, would re-enter
562
+ // refreshAccessToken and await the very promise it runs inside.
563
+ { cache: false, retry: false, skipAuth: true });
548
564
  }
549
565
  catch (error) {
550
566
  throw this.handleError(error);
@@ -562,7 +578,10 @@ export function OxyServicesAuthMixin(Base) {
562
578
  */
563
579
  async getCommonsApprovalInfo(authorizeCode) {
564
580
  try {
565
- const raw = await this.makeRequest('GET', `/auth/session/approve-info/${encodeURIComponent(authorizeCode)}`, undefined, { cache: false });
581
+ const raw = await this.makeRequest('GET', `/auth/session/approve-info/${encodeURIComponent(authorizeCode)}`, undefined,
582
+ // Public (no auth required) — skip the bearer preflight (avoids the
583
+ // pre-session self-await class).
584
+ { cache: false, skipAuth: true });
566
585
  return {
567
586
  application: raw.application,
568
587
  scopes: raw.scopes,
@@ -609,7 +628,9 @@ export function OxyServicesAuthMixin(Base) {
609
628
  timestamp: signed.timestamp,
610
629
  ...(params.deviceName ? { deviceName: params.deviceName } : {}),
611
630
  ...(params.deviceFingerprint ? { deviceFingerprint: params.deviceFingerprint } : {}),
612
- }, { cache: false });
631
+ },
632
+ // Key-signed, cookieless (no bearer) — skip the preflight.
633
+ { cache: false, skipAuth: true });
613
634
  }
614
635
  catch (error) {
615
636
  throw this.handleError(error);
@@ -623,7 +644,9 @@ export function OxyServicesAuthMixin(Base) {
623
644
  */
624
645
  async denyCommonsSignIn(authorizeCode) {
625
646
  try {
626
- return await this.makeRequest('POST', `/auth/session/deny/${encodeURIComponent(authorizeCode)}`, undefined, { cache: false });
647
+ return await this.makeRequest('POST', `/auth/session/deny/${encodeURIComponent(authorizeCode)}`, undefined,
648
+ // Public (no auth required) — skip the bearer preflight.
649
+ { cache: false, skipAuth: true });
627
650
  }
628
651
  catch (error) {
629
652
  throw this.handleError(error);
@@ -734,7 +757,8 @@ export function OxyServicesAuthMixin(Base) {
734
757
  */
735
758
  async checkUsernameAvailability(username) {
736
759
  try {
737
- return await this.makeRequest('GET', `/auth/check-username/${username}`, undefined, { cache: false });
760
+ // Public availability lookup (pre-session) skip the bearer preflight.
761
+ return await this.makeRequest('GET', `/auth/check-username/${username}`, undefined, { cache: false, skipAuth: true });
738
762
  }
739
763
  catch (error) {
740
764
  throw this.handleError(error);
@@ -745,7 +769,8 @@ export function OxyServicesAuthMixin(Base) {
745
769
  */
746
770
  async checkEmailAvailability(email) {
747
771
  try {
748
- return await this.makeRequest('GET', `/auth/check-email/${email}`, undefined, { cache: false });
772
+ // Public availability lookup (pre-session) skip the bearer preflight.
773
+ return await this.makeRequest('GET', `/auth/check-email/${email}`, undefined, { cache: false, skipAuth: true });
749
774
  }
750
775
  catch (error) {
751
776
  throw this.handleError(error);
@@ -764,7 +789,7 @@ export function OxyServicesAuthMixin(Base) {
764
789
  */
765
790
  async webauthnRegisterOptions(username) {
766
791
  try {
767
- return await this.makeRequest('POST', '/auth/webauthn/register/options', { ...(username !== undefined ? { username } : {}) }, { cache: false });
792
+ return await this.makeRequest('POST', '/auth/webauthn/register/options', { ...(username !== undefined ? { username } : {}) }, { cache: false, ...(username !== undefined ? { skipAuth: true } : {}) });
768
793
  }
769
794
  catch (error) {
770
795
  throw this.handleError(error);
@@ -785,7 +810,7 @@ export function OxyServicesAuthMixin(Base) {
785
810
  */
786
811
  async webauthnRegisterVerify(response, envelope = {}) {
787
812
  try {
788
- const res = await this.makeRequest('POST', '/auth/webauthn/register/verify', { response, ...envelope }, { cache: false });
813
+ const res = await this.makeRequest('POST', '/auth/webauthn/register/verify', { response, ...envelope }, { cache: false, ...(envelope.username !== undefined ? { skipAuth: true } : {}) });
789
814
  if (res && typeof res === 'object') {
790
815
  const record = res;
791
816
  // Signup branch: mints a session (LoginSessionResult, carries
@@ -823,7 +848,9 @@ export function OxyServicesAuthMixin(Base) {
823
848
  */
824
849
  async webauthnLoginOptions(username) {
825
850
  try {
826
- return await this.makeRequest('POST', '/auth/webauthn/login/options', { ...(username !== undefined ? { username } : {}) }, { cache: false });
851
+ return await this.makeRequest('POST', '/auth/webauthn/login/options', { ...(username !== undefined ? { username } : {}) },
852
+ // Pre-session login ceremony — skip the bearer preflight.
853
+ { cache: false, skipAuth: true });
827
854
  }
828
855
  catch (error) {
829
856
  throw this.handleError(error);
@@ -839,7 +866,9 @@ export function OxyServicesAuthMixin(Base) {
839
866
  */
840
867
  async webauthnLoginVerify(response, envelope = {}) {
841
868
  try {
842
- const res = await this.makeRequest('POST', '/auth/webauthn/login/verify', { response, ...envelope }, { cache: false });
869
+ const res = await this.makeRequest('POST', '/auth/webauthn/login/verify', { response, ...envelope },
870
+ // Pre-session login ceremony — skip the bearer preflight.
871
+ { cache: false, skipAuth: true });
843
872
  const parsed = safeParseContract(loginResultSchema, res);
844
873
  if (!parsed) {
845
874
  throw new Error('auth/webauthn/login/verify returned an unexpected response shape');
@@ -866,7 +895,7 @@ export function OxyServicesAuthMixin(Base) {
866
895
  clientId: params.clientId,
867
896
  redirectUri: params.redirectUri,
868
897
  codeVerifier: params.codeVerifier,
869
- }, { cache: false });
898
+ }, { cache: false, skipAuth: true });
870
899
  const payload = res.data ??
871
900
  res;
872
901
  if (!payload || typeof payload !== 'object') {
@@ -16,7 +16,12 @@ export function OxyServicesConnectedAppsMixin(Base) {
16
16
  */
17
17
  async getPublicApplication(clientId) {
18
18
  try {
19
- const res = await this.makeRequest('GET', `/auth/oauth/client/${encodeURIComponent(clientId)}`, undefined, { cache: true, cacheTTL: CACHE_TIMES.MEDIUM });
19
+ const res = await this.makeRequest('GET', `/auth/oauth/client/${encodeURIComponent(clientId)}`, undefined, {
20
+ cache: true,
21
+ cacheTTL: CACHE_TIMES.MEDIUM,
22
+ // Public client metadata (pre-session consent UI) — skip the bearer preflight.
23
+ skipAuth: true,
24
+ });
20
25
  return res.application;
21
26
  }
22
27
  catch (error) {
@@ -41,7 +41,11 @@ export function OxyServicesDeviceBootMixin(Base) {
41
41
  */
42
42
  async mintFromDeviceSecret(deviceId, deviceSecret) {
43
43
  try {
44
- const res = await this.makeRequest('POST', '/session/device/token', { deviceId, deviceSecret }, { cache: false, skipAuth: true, retry: false });
44
+ const res = await this.makeRequest('POST', '/session/device/token', { deviceId, deviceSecret },
45
+ // `bypassQueue`: this mint is the control-plane call the auth lane
46
+ // depends on — it must run even when every RequestQueue slot is parked
47
+ // awaiting it, or the whole client deadlocks. See RequestOptions.bypassQueue.
48
+ { cache: false, skipAuth: true, retry: false, bypassQueue: true });
45
49
  const parsed = safeParseContract(deviceTokenMintResponseSchema, res);
46
50
  if (!parsed) {
47
51
  throw new Error('session/device/token returned an unexpected response shape');
@@ -69,7 +73,10 @@ export function OxyServicesDeviceBootMixin(Base) {
69
73
  /** Redeem a hub sync ticket for a fresh device secret (public). */
70
74
  async redeemHubTicket(ticket, returnOrigin) {
71
75
  try {
72
- const res = await this.makeRequest('POST', '/session/device/redeem-ticket', { ticket, returnOrigin }, { cache: false, skipAuth: true });
76
+ const res = await this.makeRequest('POST', '/session/device/redeem-ticket', { ticket, returnOrigin },
77
+ // Public device-hub sync mint (bearer-less). Same control-plane class as
78
+ // the device-secret mint — bypassQueue so it never waits for a slot.
79
+ { cache: false, skipAuth: true, bypassQueue: true });
73
80
  const parsed = safeParseContract(deviceHubTicketRedeemResponseSchema, res);
74
81
  if (!parsed) {
75
82
  throw new Error('session/device/redeem-ticket returned an unexpected response shape');
@@ -138,6 +138,17 @@ export function OxyServicesIdentityBackupMixin(Base) {
138
138
  const aad = buildBackupAad(envelope.version, envelope.publicKeyHint);
139
139
  const plaintext = decryptAead(backupKey, fromHex(envelope.nonce), fromHex(envelope.ciphertext), aad);
140
140
  const payload = JSON.parse(new TextDecoder().decode(plaintext));
141
+ if (!payload.privateKey || !payload.publicKey) {
142
+ throw new Error('Backup payload is missing key material');
143
+ }
144
+ const derivedFromPhrase = await RecoveryPhraseService.derivePublicKeyFromPhrase(phrase);
145
+ const derivedFromPrivate = KeyManager.derivePublicKey(payload.privateKey);
146
+ const phrasePk = derivedFromPhrase.toLowerCase();
147
+ const payloadPk = payload.publicKey.toLowerCase();
148
+ const privatePk = derivedFromPrivate.toLowerCase();
149
+ if (phrasePk !== payloadPk || privatePk !== payloadPk) {
150
+ throw new Error('Backup payload does not match the recovery phrase');
151
+ }
141
152
  // Persist the recovered key. Native-only; refuses to clobber a different
142
153
  // identity unless overwrite — the IdentityAlreadyExistsError propagates.
143
154
  return await KeyManager.importKeyPair(payload.privateKey, {
@@ -56,6 +56,8 @@ export function OxyServicesUserMixin(Base) {
56
56
  return await this.makeRequest('GET', `/auth/lookup/${encodeURIComponent(username)}`, undefined, {
57
57
  cache: true,
58
58
  cacheTTL: 60 * 1000, // 1 minute cache
59
+ // Public login-flow lookup (pre-session) — skip the bearer preflight.
60
+ skipAuth: true,
59
61
  });
60
62
  }
61
63
  /**
@@ -420,6 +422,10 @@ export function OxyServicesUserMixin(Base) {
420
422
  const result = await this.makeRequest('PATCH', `/privacy/${id}/privacy`, settings, {
421
423
  cache: false,
422
424
  });
425
+ this.clearCacheByPrefix('GET:/session/user/');
426
+ this.clearCacheByPrefix('GET:/users/me');
427
+ this.clearCacheByPrefix('GET:/profiles/username/');
428
+ this.clearCacheEntry(`GET:/users/${id}`);
423
429
  this.clearCacheEntry(`GET:/privacy/${id}/privacy`);
424
430
  return result;
425
431
  }
@@ -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
  import { logger } from '../logger/index.js';
40
47
  import { extractErrorStatus } from '../utils/errorUtils.js';
@@ -248,11 +255,6 @@ export class AccountDialogController {
248
255
  this.view = view;
249
256
  this.emit();
250
257
  }
251
- /** Return to the account list and cancel any in-flight sign-in flow. */
252
- close() {
253
- this.cancelSignIn();
254
- this.setView('accounts');
255
- }
256
258
  /** Switch to the "add account" view (the sign-in entry chooser). */
257
259
  add() {
258
260
  this.setView('add');
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Returns a user-facing blocking reason when an approval payload must not be
3
+ * shown as actionable, or `null` when the request is still pending and valid.
4
+ */
5
+ export function getCommonsApprovalBlockingReason(info) {
6
+ if (!info.application?.id) {
7
+ return 'The requesting application could not be resolved.';
8
+ }
9
+ if (info.status !== 'pending') {
10
+ return 'This sign-in request is invalid, already used, or expired.';
11
+ }
12
+ const expiresAtMs = parseCommonsApprovalExpiresAt(info.expiresAt);
13
+ if (expiresAtMs !== null && expiresAtMs < Date.now()) {
14
+ return 'This sign-in request has expired. Ask for a new QR code.';
15
+ }
16
+ return null;
17
+ }
18
+ /** Normalize API `expiresAt` (number or ISO string) to epoch ms. */
19
+ export function parseCommonsApprovalExpiresAt(expiresAt) {
20
+ if (typeof expiresAt === 'number' && Number.isFinite(expiresAt))
21
+ return expiresAt;
22
+ if (typeof expiresAt === 'string') {
23
+ const ms = Date.parse(expiresAt);
24
+ return Number.isFinite(ms) ? ms : null;
25
+ }
26
+ return null;
27
+ }