@oxyhq/core 5.1.1 → 5.2.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 (114) hide show
  1. package/dist/cjs/.tsbuildinfo +1 -1
  2. package/dist/cjs/AuthManager.js +27 -5
  3. package/dist/cjs/OxyServices.base.js +71 -0
  4. package/dist/cjs/i18n/locales/ar-SA.json +1 -0
  5. package/dist/cjs/i18n/locales/ca-ES.json +1 -0
  6. package/dist/cjs/i18n/locales/de-DE.json +1 -0
  7. package/dist/cjs/i18n/locales/en-US.json +1 -0
  8. package/dist/cjs/i18n/locales/es-ES.json +1 -0
  9. package/dist/cjs/i18n/locales/fr-FR.json +1 -0
  10. package/dist/cjs/i18n/locales/it-IT.json +1 -0
  11. package/dist/cjs/i18n/locales/ja-JP.json +1 -0
  12. package/dist/cjs/i18n/locales/ko-KR.json +1 -0
  13. package/dist/cjs/i18n/locales/locales/ar-SA.json +1 -0
  14. package/dist/cjs/i18n/locales/locales/ca-ES.json +1 -0
  15. package/dist/cjs/i18n/locales/locales/de-DE.json +1 -0
  16. package/dist/cjs/i18n/locales/locales/en-US.json +1 -0
  17. package/dist/cjs/i18n/locales/locales/es-ES.json +1 -0
  18. package/dist/cjs/i18n/locales/locales/fr-FR.json +1 -0
  19. package/dist/cjs/i18n/locales/locales/it-IT.json +1 -0
  20. package/dist/cjs/i18n/locales/locales/ja-JP.json +1 -0
  21. package/dist/cjs/i18n/locales/locales/ko-KR.json +1 -0
  22. package/dist/cjs/i18n/locales/locales/pt-PT.json +1 -0
  23. package/dist/cjs/i18n/locales/locales/zh-CN.json +1 -0
  24. package/dist/cjs/i18n/locales/pt-PT.json +1 -0
  25. package/dist/cjs/i18n/locales/zh-CN.json +1 -0
  26. package/dist/cjs/index.js +3 -1
  27. package/dist/cjs/mixins/OxyServices.accounts.js +10 -25
  28. package/dist/cjs/mixins/OxyServices.assets.js +76 -0
  29. package/dist/cjs/utils/cache.js +2 -0
  30. package/dist/cjs/utils/ssoBounce.js +59 -0
  31. package/dist/esm/.tsbuildinfo +1 -1
  32. package/dist/esm/AuthManager.js +27 -5
  33. package/dist/esm/OxyServices.base.js +71 -0
  34. package/dist/esm/i18n/locales/ar-SA.json +1 -0
  35. package/dist/esm/i18n/locales/ca-ES.json +1 -0
  36. package/dist/esm/i18n/locales/de-DE.json +1 -0
  37. package/dist/esm/i18n/locales/en-US.json +1 -0
  38. package/dist/esm/i18n/locales/es-ES.json +1 -0
  39. package/dist/esm/i18n/locales/fr-FR.json +1 -0
  40. package/dist/esm/i18n/locales/it-IT.json +1 -0
  41. package/dist/esm/i18n/locales/ja-JP.json +1 -0
  42. package/dist/esm/i18n/locales/ko-KR.json +1 -0
  43. package/dist/esm/i18n/locales/locales/ar-SA.json +1 -0
  44. package/dist/esm/i18n/locales/locales/ca-ES.json +1 -0
  45. package/dist/esm/i18n/locales/locales/de-DE.json +1 -0
  46. package/dist/esm/i18n/locales/locales/en-US.json +1 -0
  47. package/dist/esm/i18n/locales/locales/es-ES.json +1 -0
  48. package/dist/esm/i18n/locales/locales/fr-FR.json +1 -0
  49. package/dist/esm/i18n/locales/locales/it-IT.json +1 -0
  50. package/dist/esm/i18n/locales/locales/ja-JP.json +1 -0
  51. package/dist/esm/i18n/locales/locales/ko-KR.json +1 -0
  52. package/dist/esm/i18n/locales/locales/pt-PT.json +1 -0
  53. package/dist/esm/i18n/locales/locales/zh-CN.json +1 -0
  54. package/dist/esm/i18n/locales/pt-PT.json +1 -0
  55. package/dist/esm/i18n/locales/zh-CN.json +1 -0
  56. package/dist/esm/index.js +1 -1
  57. package/dist/esm/mixins/OxyServices.accounts.js +10 -25
  58. package/dist/esm/mixins/OxyServices.assets.js +76 -0
  59. package/dist/esm/utils/cache.js +2 -0
  60. package/dist/esm/utils/ssoBounce.js +57 -0
  61. package/dist/types/.tsbuildinfo +1 -1
  62. package/dist/types/OxyServices.base.d.ts +38 -0
  63. package/dist/types/index.d.ts +2 -2
  64. package/dist/types/mixins/OxyServices.accounts.d.ts +1 -0
  65. package/dist/types/mixins/OxyServices.analytics.d.ts +1 -0
  66. package/dist/types/mixins/OxyServices.appData.d.ts +1 -0
  67. package/dist/types/mixins/OxyServices.assets.d.ts +36 -1
  68. package/dist/types/mixins/OxyServices.auth.d.ts +1 -0
  69. package/dist/types/mixins/OxyServices.civic.d.ts +1 -0
  70. package/dist/types/mixins/OxyServices.connectedApps.d.ts +1 -0
  71. package/dist/types/mixins/OxyServices.contacts.d.ts +1 -0
  72. package/dist/types/mixins/OxyServices.devices.d.ts +1 -0
  73. package/dist/types/mixins/OxyServices.features.d.ts +1 -0
  74. package/dist/types/mixins/OxyServices.fedcm.d.ts +1 -0
  75. package/dist/types/mixins/OxyServices.identity.d.ts +1 -0
  76. package/dist/types/mixins/OxyServices.language.d.ts +1 -0
  77. package/dist/types/mixins/OxyServices.links.d.ts +1 -0
  78. package/dist/types/mixins/OxyServices.location.d.ts +1 -0
  79. package/dist/types/mixins/OxyServices.nodes.d.ts +1 -0
  80. package/dist/types/mixins/OxyServices.payment.d.ts +1 -0
  81. package/dist/types/mixins/OxyServices.privacy.d.ts +1 -0
  82. package/dist/types/mixins/OxyServices.redirect.d.ts +1 -0
  83. package/dist/types/mixins/OxyServices.reputation.d.ts +1 -0
  84. package/dist/types/mixins/OxyServices.security.d.ts +1 -0
  85. package/dist/types/mixins/OxyServices.silent.d.ts +1 -0
  86. package/dist/types/mixins/OxyServices.sso.d.ts +1 -0
  87. package/dist/types/mixins/OxyServices.topics.d.ts +1 -0
  88. package/dist/types/mixins/OxyServices.user.d.ts +1 -0
  89. package/dist/types/mixins/OxyServices.utility.d.ts +1 -0
  90. package/dist/types/models/interfaces.d.ts +23 -0
  91. package/dist/types/utils/ssoBounce.d.ts +47 -0
  92. package/package.json +1 -1
  93. package/src/AuthManager.ts +29 -5
  94. package/src/OxyServices.base.ts +79 -0
  95. package/src/__tests__/establishDeviceRefreshSlot.test.ts +221 -0
  96. package/src/i18n/locales/ar-SA.json +1 -0
  97. package/src/i18n/locales/ca-ES.json +1 -0
  98. package/src/i18n/locales/de-DE.json +1 -0
  99. package/src/i18n/locales/en-US.json +1 -0
  100. package/src/i18n/locales/es-ES.json +1 -0
  101. package/src/i18n/locales/fr-FR.json +1 -0
  102. package/src/i18n/locales/it-IT.json +1 -0
  103. package/src/i18n/locales/ja-JP.json +1 -0
  104. package/src/i18n/locales/ko-KR.json +1 -0
  105. package/src/i18n/locales/pt-PT.json +1 -0
  106. package/src/i18n/locales/zh-CN.json +1 -0
  107. package/src/index.ts +3 -0
  108. package/src/mixins/OxyServices.accounts.ts +10 -33
  109. package/src/mixins/OxyServices.assets.ts +92 -1
  110. package/src/mixins/__tests__/OxyServices.serviceAssetMetadataBySha.test.ts +135 -0
  111. package/src/models/interfaces.ts +24 -0
  112. package/src/utils/__tests__/ssoBounce.test.ts +41 -0
  113. package/src/utils/cache.ts +2 -0
  114. package/src/utils/ssoBounce.ts +61 -0
@@ -397,11 +397,33 @@ class AuthManager {
397
397
  this.currentUser = session.user;
398
398
  }
399
399
  this.currentAuthMethod = method;
400
- const decodedAuthuser = session.accessToken
401
- ? AuthManager.decodeAuthuserFromAccessToken(session.accessToken)
400
+ // Register this primary session in the device's first-party multi-account
401
+ // refresh-cookie set (web only). Every web primary commit funnels through
402
+ // here (FedCM, central `/sso` return, credentials), but only a same-apex
403
+ // `/fedcm/exchange` plants an `oxy_rt_<authuser>` slot as a side effect — the
404
+ // cross-origin/credential-less restores (`/sso/exchange`, the IdP
405
+ // `/auth/silent` postMessage) cannot set an `api.oxy.so` cookie. WITHOUT this
406
+ // call those primaries never join the device set, so `refresh-all` returns no
407
+ // accounts and account-switch persistence has no foundation. The shared
408
+ // `POST /auth/session` primitive plants/reuses this user's slot where the
409
+ // cookies ARE visible, re-plants the rotated access token, and returns the
410
+ // AUTHORITATIVE `authuser` slot (the one actually written) — preferred over the
411
+ // JWT-decoded guess, which may not correspond to a real cookie. No-op on native
412
+ // (returns `null`); best-effort on transient failure.
413
+ const establishedAuthuser = await this.oxyServices.establishDeviceRefreshSlot();
414
+ // The slot mint rotated the access token; pick it up so the registry and the
415
+ // refresh authority track the cookie just written. Falls back to the original
416
+ // session token when the slot was a no-op (native) or failed.
417
+ const rotatedToken = (establishedAuthuser !== null ? this.oxyServices.getAccessToken() : null) ??
418
+ session.accessToken;
419
+ if (rotatedToken) {
420
+ this._lastKnownAccessToken = rotatedToken;
421
+ }
422
+ const decodedAuthuser = rotatedToken
423
+ ? AuthManager.decodeAuthuserFromAccessToken(rotatedToken)
402
424
  : null;
403
- const authuser = decodedAuthuser ?? 0;
404
- if (session.accessToken && session.sessionId) {
425
+ const authuser = establishedAuthuser ?? decodedAuthuser ?? 0;
426
+ if (rotatedToken && session.sessionId) {
405
427
  this.accounts.set(authuser, {
406
428
  authuser,
407
429
  sessionId: session.sessionId,
@@ -411,7 +433,7 @@ class AuthManager {
411
433
  name: session.user.name,
412
434
  avatar: session.user.avatar ?? null,
413
435
  },
414
- accessToken: session.accessToken,
436
+ accessToken: rotatedToken,
415
437
  expiresAt: session.expiresAt,
416
438
  });
417
439
  this.activeAuthuser = authuser;
@@ -11,6 +11,9 @@ const errorUtils_1 = require("./utils/errorUtils");
11
11
  const HttpService_1 = require("./HttpService");
12
12
  const OxyServices_errors_1 = require("./OxyServices.errors");
13
13
  const authWebUrl_1 = require("./utils/authWebUrl");
14
+ const platform_1 = require("./utils/platform");
15
+ const fapiAutoDetect_1 = require("./utils/fapiAutoDetect");
16
+ const loggerUtils_1 = require("./utils/loggerUtils");
14
17
  /**
15
18
  * Base class for OxyServices with core infrastructure
16
19
  */
@@ -253,6 +256,74 @@ class OxyServicesBase {
253
256
  getAccessToken() {
254
257
  return this.httpService.getAccessToken();
255
258
  }
259
+ /**
260
+ * Register the CURRENTLY-ACTIVE session in the device's first-party
261
+ * multi-account refresh-cookie set by calling `POST /auth/session`.
262
+ *
263
+ * This is the single, shared primitive every web primary-session commit and the
264
+ * account switch use to plant their `oxy_rt_<authuser>` slot. It MUST be a
265
+ * dedicated call to `/auth/session` rather than relying on whichever endpoint
266
+ * established the session: that endpoint is frequently OUTSIDE the cookie's
267
+ * `Path=/auth` scope (`/accounts/:id/switch`) or is a cross-origin/credential-
268
+ * less restore (`/sso/exchange`, the IdP `/auth/silent` postMessage) that cannot
269
+ * set an `api.oxy.so` cookie at all. `/auth/session` runs where the device's
270
+ * existing slots ARE visible, so the server resolves this user's slot (reusing an
271
+ * existing one or allocating a new one) without clobbering a sibling account,
272
+ * mints a fresh access token bound to the same session, and returns the resolved
273
+ * `authuser`.
274
+ *
275
+ * Behaviour:
276
+ * - Requires a planted bearer (the caller must have already installed the
277
+ * session's access token); `/auth/session` derives the session from it.
278
+ * - On success re-plants the rotated access token (so the active token matches
279
+ * the freshly-rotated cookie) and returns the device `authuser` slot.
280
+ * - WEB-ONLY: on native there are no first-party refresh cookies → returns
281
+ * `null`.
282
+ * - FIRST-PARTY-ONLY: the cookie is host-only on the API host with
283
+ * `SameSite=Lax`, so it only sticks when the page is SAME-SITE (same
284
+ * registrable apex) as the API. On a cross-apex RP (`mention.earth` calling
285
+ * `api.oxy.so`) the browser rejects the `Set-Cookie` as a third-party cookie,
286
+ * so a returned slot would be a phantom never enumerated by `refresh-all`.
287
+ * Those RPs durably restore via the per-apex `/auth/silent` iframe + `/sso`
288
+ * bounce, NOT this device set → returns `null` without calling the API.
289
+ * - BEST-EFFORT: a failure (e.g. transient network) never throws — the session
290
+ * stays active in-memory; only its reload durability via the device set is at
291
+ * risk. The caller treats `null` as "not registered in the device set".
292
+ *
293
+ * @returns The resolved device `authuser` slot, or `null` on native / cross-apex
294
+ * / failure.
295
+ */
296
+ async establishDeviceRefreshSlot() {
297
+ if (!(0, platform_1.isWeb)()) {
298
+ return null;
299
+ }
300
+ if (typeof window !== 'undefined' && window.location?.hostname) {
301
+ const pageApex = (0, fapiAutoDetect_1.registrableApex)(window.location.hostname);
302
+ let apiApex = null;
303
+ try {
304
+ apiApex = (0, fapiAutoDetect_1.registrableApex)(new URL(this.getBaseURL()).hostname);
305
+ }
306
+ catch {
307
+ apiApex = null;
308
+ }
309
+ if (!pageApex || !apiApex || pageApex !== apiApex) {
310
+ return null;
311
+ }
312
+ }
313
+ try {
314
+ const established = await this.makeRequest('POST', '/auth/session', undefined, { cache: false });
315
+ // `/auth/session` mints a fresh access token off the same session; re-plant
316
+ // it so the active token matches the rotated cookie.
317
+ if (established?.accessToken) {
318
+ this.setTokens(established.accessToken);
319
+ }
320
+ return typeof established?.authuser === 'number' ? established.authuser : null;
321
+ }
322
+ catch (error) {
323
+ loggerUtils_1.logger.warn('[OxyServices] Failed to establish device refresh cookie via POST /auth/session; the session is active in-session but may not survive a reload as part of the device account set', { component: 'OxyServices', method: 'establishDeviceRefreshSlot' }, error);
324
+ return null;
325
+ }
326
+ }
256
327
  /**
257
328
  * Decode the current access token and return its `exp` claim in SECONDS since
258
329
  * the Unix epoch (the raw JWT `exp` unit), or `null` when there is no token,
@@ -207,6 +207,7 @@
207
207
  "accountMenu": {
208
208
  "label": "قائمة الحساب",
209
209
  "manage": "إدارة حساب Oxy الخاص بك",
210
+ "viewProfile": "عرض الملف الشخصي",
210
211
  "addAnother": "إضافة حساب آخر",
211
212
  "signOutAll": "تسجيل الخروج من جميع الحسابات",
212
213
  "open": "قائمة الحساب",
@@ -207,6 +207,7 @@
207
207
  "accountMenu": {
208
208
  "label": "Menú del compte",
209
209
  "manage": "Gestiona el teu compte d'Oxy",
210
+ "viewProfile": "Mostra el perfil",
210
211
  "addAnother": "Afegeix un altre compte",
211
212
  "signOutAll": "Tanca la sessió de tots els comptes",
212
213
  "open": "Menú del compte",
@@ -207,6 +207,7 @@
207
207
  "accountMenu": {
208
208
  "label": "Kontomenü",
209
209
  "manage": "Ihr Oxy-Konto verwalten",
210
+ "viewProfile": "Profil ansehen",
210
211
  "addAnother": "Weiteres Konto hinzufügen",
211
212
  "signOutAll": "Von allen Konten abmelden",
212
213
  "open": "Kontomenü",
@@ -1555,6 +1555,7 @@
1555
1555
  "accountMenu": {
1556
1556
  "label": "Account menu",
1557
1557
  "manage": "Manage your Oxy Account",
1558
+ "viewProfile": "View profile",
1558
1559
  "addAnother": "Add another account",
1559
1560
  "signOutAll": "Sign out of all accounts",
1560
1561
  "open": "Account menu",
@@ -1555,6 +1555,7 @@
1555
1555
  "accountMenu": {
1556
1556
  "label": "Menú de cuenta",
1557
1557
  "manage": "Gestiona tu cuenta de Oxy",
1558
+ "viewProfile": "Ver perfil",
1558
1559
  "addAnother": "Añadir otra cuenta",
1559
1560
  "signOutAll": "Cerrar sesión en todas las cuentas",
1560
1561
  "open": "Menú de cuenta",
@@ -207,6 +207,7 @@
207
207
  "accountMenu": {
208
208
  "label": "Menu du compte",
209
209
  "manage": "Gérer votre compte Oxy",
210
+ "viewProfile": "Voir le profil",
210
211
  "addAnother": "Ajouter un autre compte",
211
212
  "signOutAll": "Se déconnecter de tous les comptes",
212
213
  "open": "Menu du compte",
@@ -207,6 +207,7 @@
207
207
  "accountMenu": {
208
208
  "label": "Menu account",
209
209
  "manage": "Gestisci il tuo account Oxy",
210
+ "viewProfile": "Visualizza profilo",
210
211
  "addAnother": "Aggiungi un altro account",
211
212
  "signOutAll": "Esci da tutti gli account",
212
213
  "open": "Menu account",
@@ -207,6 +207,7 @@
207
207
  "accountMenu": {
208
208
  "label": "アカウントメニュー",
209
209
  "manage": "Oxy アカウントを管理",
210
+ "viewProfile": "プロフィールを表示",
210
211
  "addAnother": "別のアカウントを追加",
211
212
  "signOutAll": "すべてのアカウントからログアウト",
212
213
  "open": "アカウントメニュー",
@@ -207,6 +207,7 @@
207
207
  "accountMenu": {
208
208
  "label": "계정 메뉴",
209
209
  "manage": "Oxy 계정 관리",
210
+ "viewProfile": "프로필 보기",
210
211
  "addAnother": "다른 계정 추가",
211
212
  "signOutAll": "모든 계정에서 로그아웃",
212
213
  "open": "계정 메뉴",
@@ -207,6 +207,7 @@
207
207
  "accountMenu": {
208
208
  "label": "قائمة الحساب",
209
209
  "manage": "إدارة حساب Oxy الخاص بك",
210
+ "viewProfile": "عرض الملف الشخصي",
210
211
  "addAnother": "إضافة حساب آخر",
211
212
  "signOutAll": "تسجيل الخروج من جميع الحسابات",
212
213
  "open": "قائمة الحساب",
@@ -207,6 +207,7 @@
207
207
  "accountMenu": {
208
208
  "label": "Menú del compte",
209
209
  "manage": "Gestiona el teu compte d'Oxy",
210
+ "viewProfile": "Mostra el perfil",
210
211
  "addAnother": "Afegeix un altre compte",
211
212
  "signOutAll": "Tanca la sessió de tots els comptes",
212
213
  "open": "Menú del compte",
@@ -207,6 +207,7 @@
207
207
  "accountMenu": {
208
208
  "label": "Kontomenü",
209
209
  "manage": "Ihr Oxy-Konto verwalten",
210
+ "viewProfile": "Profil ansehen",
210
211
  "addAnother": "Weiteres Konto hinzufügen",
211
212
  "signOutAll": "Von allen Konten abmelden",
212
213
  "open": "Kontomenü",
@@ -1555,6 +1555,7 @@
1555
1555
  "accountMenu": {
1556
1556
  "label": "Account menu",
1557
1557
  "manage": "Manage your Oxy Account",
1558
+ "viewProfile": "View profile",
1558
1559
  "addAnother": "Add another account",
1559
1560
  "signOutAll": "Sign out of all accounts",
1560
1561
  "open": "Account menu",
@@ -1555,6 +1555,7 @@
1555
1555
  "accountMenu": {
1556
1556
  "label": "Menú de cuenta",
1557
1557
  "manage": "Gestiona tu cuenta de Oxy",
1558
+ "viewProfile": "Ver perfil",
1558
1559
  "addAnother": "Añadir otra cuenta",
1559
1560
  "signOutAll": "Cerrar sesión en todas las cuentas",
1560
1561
  "open": "Menú de cuenta",
@@ -207,6 +207,7 @@
207
207
  "accountMenu": {
208
208
  "label": "Menu du compte",
209
209
  "manage": "Gérer votre compte Oxy",
210
+ "viewProfile": "Voir le profil",
210
211
  "addAnother": "Ajouter un autre compte",
211
212
  "signOutAll": "Se déconnecter de tous les comptes",
212
213
  "open": "Menu du compte",
@@ -207,6 +207,7 @@
207
207
  "accountMenu": {
208
208
  "label": "Menu account",
209
209
  "manage": "Gestisci il tuo account Oxy",
210
+ "viewProfile": "Visualizza profilo",
210
211
  "addAnother": "Aggiungi un altro account",
211
212
  "signOutAll": "Esci da tutti gli account",
212
213
  "open": "Menu account",
@@ -207,6 +207,7 @@
207
207
  "accountMenu": {
208
208
  "label": "アカウントメニュー",
209
209
  "manage": "Oxy アカウントを管理",
210
+ "viewProfile": "プロフィールを表示",
210
211
  "addAnother": "別のアカウントを追加",
211
212
  "signOutAll": "すべてのアカウントからログアウト",
212
213
  "open": "アカウントメニュー",
@@ -207,6 +207,7 @@
207
207
  "accountMenu": {
208
208
  "label": "계정 메뉴",
209
209
  "manage": "Oxy 계정 관리",
210
+ "viewProfile": "프로필 보기",
210
211
  "addAnother": "다른 계정 추가",
211
212
  "signOutAll": "모든 계정에서 로그아웃",
212
213
  "open": "계정 메뉴",
@@ -207,6 +207,7 @@
207
207
  "accountMenu": {
208
208
  "label": "Menu da conta",
209
209
  "manage": "Gerir a sua conta Oxy",
210
+ "viewProfile": "Ver perfil",
210
211
  "addAnother": "Adicionar outra conta",
211
212
  "signOutAll": "Terminar sessão em todas as contas",
212
213
  "open": "Menu da conta",
@@ -207,6 +207,7 @@
207
207
  "accountMenu": {
208
208
  "label": "账户菜单",
209
209
  "manage": "管理您的 Oxy 账户",
210
+ "viewProfile": "查看个人资料",
210
211
  "addAnother": "添加其他账户",
211
212
  "signOutAll": "退出所有账户",
212
213
  "open": "账户菜单",
@@ -207,6 +207,7 @@
207
207
  "accountMenu": {
208
208
  "label": "Menu da conta",
209
209
  "manage": "Gerir a sua conta Oxy",
210
+ "viewProfile": "Ver perfil",
210
211
  "addAnother": "Adicionar outra conta",
211
212
  "signOutAll": "Terminar sessão em todas as contas",
212
213
  "open": "Menu da conta",
@@ -207,6 +207,7 @@
207
207
  "accountMenu": {
208
208
  "label": "账户菜单",
209
209
  "manage": "管理您的 Oxy 账户",
210
+ "viewProfile": "查看个人资料",
210
211
  "addAnother": "添加其他账户",
211
212
  "signOutAll": "退出所有账户",
212
213
  "open": "账户菜单",
package/dist/cjs/index.js CHANGED
@@ -21,7 +21,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
21
21
  exports.isIOS = exports.isNative = exports.isWeb = exports.setPlatformOS = exports.getPlatformOS = exports.isRTLLocale = exports.normalizeLanguageCode = exports.getNativeLanguageName = exports.getLanguageName = exports.getLanguageMetadata = exports.SUPPORTED_LANGUAGES = exports.TopicSource = exports.TopicType = exports.SECURITY_EVENT_SEVERITY_MAP = exports.DeviceManager = exports.RecoveryPhraseService = exports.SignatureService = exports.IdentityPersistError = exports.IdentityAlreadyExistsError = exports.KeyManager = exports.sessionsArraysEqual = exports.normalizeAndSortSessions = exports.mergeSessions = exports.authenticatedApiCall = exports.withAuthErrorHandling = exports.isAuthenticationError = exports.ensureValidToken = exports.AuthenticationFailedError = exports.SessionSyncRequiredError = exports.verifyPublicCardAttestation = exports.parseAttestPayload = exports.parseIdPayload = exports.buildUserDid = exports.normalizeProfileLinks = exports.getNormalizedUserHandle = exports.getCanonicalUserHandle = exports.normalizeUserIdentityOrNull = exports.normalizeUserIdentity = exports.getNormalizedUserId = exports.OxyAppDataIdentifierError = exports.ServiceCredentialMismatchError = exports.createCrossDomainAuth = exports.CrossDomainAuth = exports.createAuthManager = exports.AuthManager = exports.oxyClient = exports.OXY_CLOUD_URL = exports.OxyAuthenticationTimeoutError = exports.OxyAuthenticationError = exports.OxyServices = void 0;
22
22
  exports.PASSWORD_REGEX = exports.USERNAME_REGEX = exports.EMAIL_REGEX = exports.retryAsync = exports.validateRequiredFields = exports.handleHttpError = exports.createApiError = exports.ErrorCodes = exports.safeJsonParse = exports.buildPaginationParams = exports.buildUrl = exports.buildSearchParams = exports.translate = exports.createDebugLogger = exports.debugError = exports.debugWarn = exports.debugLog = exports.isDev = exports.withRetry = exports.delay = exports.shouldAllowRequest = exports.recordSuccess = exports.recordFailure = exports.calculateBackoffInterval = exports.createCircuitBreakerState = exports.DEFAULT_CIRCUIT_BREAKER_CONFIG = exports.isRetryableError = exports.isNetworkError = exports.isServerError = exports.isRateLimitError = exports.isNotFoundError = exports.isForbiddenError = exports.isUnauthorizedError = exports.isAlreadyRegisteredError = exports.getErrorMessage = exports.getErrorStatus = exports.HttpStatus = exports.getSystemColorScheme = exports.systemPrefersDarkMode = exports.getOppositeTheme = exports.normalizeColorScheme = exports.normalizeTheme = exports.getContrastTextColor = exports.isLightColor = exports.withOpacity = exports.rgbToHex = exports.hexToRgb = exports.lightenColor = exports.darkenColor = exports.isAndroid = void 0;
23
23
  exports.ssoPriorSessionKey = exports.ssoAttemptedKey = exports.ssoNoSessionKey = exports.ssoDestKey = exports.ssoGuardKey = exports.ssoStateKey = exports.SSO_GUARD_TTL_MS = exports.SSO_CALLBACK_PATH = exports.generateSsoState = exports.consumeSsoReturn = exports.parseSsoReturnFragment = exports.resolveCentralAuthUrl = exports.CENTRAL_IDP_APEX = exports.CENTRAL_AUTH_URL = exports.registrableApex = exports.autoDetectAuthWebUrl = exports.getAccountColor = exports.mergeAccountsFromRefreshAll = exports.formatPublicKeyHandle = exports.getAccountFallbackHandle = exports.getAccountDisplayName = exports.createQuickAccount = exports.buildAccountsArray = exports.updateAvatarVisibility = exports.logPerformance = exports.logPayment = exports.logDevice = exports.logUser = exports.logSession = exports.logApi = exports.logAuth = exports.LogLevel = exports.logger = exports.validateAndSanitizeUserInput = exports.isValidObjectId = exports.sanitizeHTML = exports.sanitizeString = exports.isValidFileType = exports.isValidFileSize = exports.isValidDate = exports.isValidURL = exports.isValidUUID = exports.isValidObject = exports.isValidArray = exports.isRequiredBoolean = exports.isRequiredNumber = exports.isRequiredString = exports.isValidPassword = exports.isValidUsername = exports.isValidEmail = void 0;
24
- exports.packageInfo = exports.runColdBoot = exports.allowSsoBounce = exports.guardActive = exports.isCentralIdPOrigin = exports.buildSsoBounceUrl = exports.getSsoCallbackBootstrapScript = exports.ssoNavigate = exports.ssoCallbackBootstrapKey = void 0;
24
+ exports.packageInfo = exports.runColdBoot = exports.allowSsoBounce = exports.silentRestoreSuppressed = exports.guardActive = exports.isCentralIdPOrigin = exports.buildSsoBounceUrl = exports.getSsoCallbackBootstrapScript = exports.ssoNavigate = exports.ssoCallbackBootstrapKey = exports.ssoSignedOutKey = void 0;
25
25
  // Ensure crypto polyfills are loaded before anything else
26
26
  require("./crypto/polyfill");
27
27
  // ---------------------------------------------------------------------------
@@ -284,12 +284,14 @@ Object.defineProperty(exports, "ssoDestKey", { enumerable: true, get: function (
284
284
  Object.defineProperty(exports, "ssoNoSessionKey", { enumerable: true, get: function () { return ssoBounce_1.ssoNoSessionKey; } });
285
285
  Object.defineProperty(exports, "ssoAttemptedKey", { enumerable: true, get: function () { return ssoBounce_1.ssoAttemptedKey; } });
286
286
  Object.defineProperty(exports, "ssoPriorSessionKey", { enumerable: true, get: function () { return ssoBounce_1.ssoPriorSessionKey; } });
287
+ Object.defineProperty(exports, "ssoSignedOutKey", { enumerable: true, get: function () { return ssoBounce_1.ssoSignedOutKey; } });
287
288
  Object.defineProperty(exports, "ssoCallbackBootstrapKey", { enumerable: true, get: function () { return ssoBounce_1.ssoCallbackBootstrapKey; } });
288
289
  Object.defineProperty(exports, "ssoNavigate", { enumerable: true, get: function () { return ssoBounce_1.ssoNavigate; } });
289
290
  Object.defineProperty(exports, "getSsoCallbackBootstrapScript", { enumerable: true, get: function () { return ssoBounce_1.getSsoCallbackBootstrapScript; } });
290
291
  Object.defineProperty(exports, "buildSsoBounceUrl", { enumerable: true, get: function () { return ssoBounce_1.buildSsoBounceUrl; } });
291
292
  Object.defineProperty(exports, "isCentralIdPOrigin", { enumerable: true, get: function () { return ssoBounce_1.isCentralIdPOrigin; } });
292
293
  Object.defineProperty(exports, "guardActive", { enumerable: true, get: function () { return ssoBounce_1.guardActive; } });
294
+ Object.defineProperty(exports, "silentRestoreSuppressed", { enumerable: true, get: function () { return ssoBounce_1.silentRestoreSuppressed; } });
293
295
  Object.defineProperty(exports, "allowSsoBounce", { enumerable: true, get: function () { return ssoBounce_1.allowSsoBounce; } });
294
296
  var coldBoot_1 = require("./utils/coldBoot");
295
297
  Object.defineProperty(exports, "runColdBoot", { enumerable: true, get: function () { return coldBoot_1.runColdBoot; } });
@@ -2,8 +2,6 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.OxyServicesAccountsMixin = OxyServicesAccountsMixin;
4
4
  const userIdentity_1 = require("../utils/userIdentity");
5
- const platform_1 = require("../utils/platform");
6
- const loggerUtils_1 = require("../utils/loggerUtils");
7
5
  const mixinHelpers_1 = require("./mixinHelpers");
8
6
  function OxyServicesAccountsMixin(Base) {
9
7
  return class extends Base {
@@ -94,29 +92,16 @@ function OxyServicesAccountsMixin(Base) {
94
92
  }
95
93
  // Register the switched session in the device's multi-account set by
96
94
  // establishing its first-party refresh cookie. This MUST be a separate
97
- // call to `POST /auth/session`: the switch route is at `/accounts/*`,
98
- // outside the `oxy_rt_<authuser>` cookie's `Path=/auth` scope, so it can
99
- // never read the device's existing slots and would overwrite slot 0
100
- // (destroying the operator's own session). `/auth/session` runs where the
101
- // cookies ARE visible, so the server allocates a NEW slot that coexists
102
- // with the operator's and returns its `authuser`. Web-only; best-effort.
103
- let authuser = res.authuser;
104
- if ((0, platform_1.isWeb)()) {
105
- try {
106
- const established = await this.makeRequest('POST', '/auth/session', undefined, { cache: false });
107
- if (typeof established?.authuser === 'number') {
108
- authuser = established.authuser;
109
- }
110
- // /auth/session mints a fresh access token off the same session;
111
- // re-plant it so the active token matches the rotated cookie.
112
- if (established?.accessToken) {
113
- this.setTokens(established.accessToken);
114
- }
115
- }
116
- catch (error) {
117
- loggerUtils_1.logger.warn('[OxyServices] Failed to establish device refresh cookie after account switch; the switch is active in-session but may not survive a reload', { component: 'OxyServices', method: 'switchToAccount' }, error);
118
- }
119
- }
95
+ // call to `POST /auth/session` (the shared `establishDeviceRefreshSlot`
96
+ // primitive): the switch route is at `/accounts/*`, outside the
97
+ // `oxy_rt_<authuser>` cookie's `Path=/auth` scope, so it can never read
98
+ // the device's existing slots and would overwrite slot 0 (destroying the
99
+ // operator's own session). `/auth/session` runs where the cookies ARE
100
+ // visible, so the server allocates a NEW slot that coexists with the
101
+ // operator's and returns its `authuser`. Web-only; best-effort (the helper
102
+ // re-plants the rotated access token and returns `null` on native/failure).
103
+ const establishedAuthuser = await this.establishDeviceRefreshSlot();
104
+ const authuser = establishedAuthuser ?? res.authuser;
120
105
  // Identity changed → drop the entire GET response cache so no entry
121
106
  // personalised for the previous identity is reused. Cache keys are
122
107
  // identity-scoped, so a different identity could not READ the old
@@ -11,6 +11,19 @@ const errorUtils_1 = require("../utils/errorUtils");
11
11
  * `getUsersByIds`'s `USERS_BY_IDS_CHUNK_SIZE`.
12
12
  */
13
13
  const SERVICE_ASSET_METADATA_CHUNK_SIZE = 100;
14
+ /**
15
+ * Maximum number of content hashes sent per `POST /assets/service/by-sha256`
16
+ * request. Matches the server-side cap (the route rejects empty or > 100 hash
17
+ * arrays with a 400); larger inputs are chunked and merged. Same ceiling as
18
+ * {@link SERVICE_ASSET_METADATA_CHUNK_SIZE} for the forward id lookup.
19
+ */
20
+ const SERVICE_ASSET_METADATA_BY_SHA_CHUNK_SIZE = 100;
21
+ /**
22
+ * Lowercase hex SHA-256 digest matcher (exactly 64 hex chars). The reverse
23
+ * lookup drops non-conforming hashes client-side so a single malformed value
24
+ * never 400s an otherwise-valid chunk on the server.
25
+ */
26
+ const SHA256_HEX_PATTERN = /^[a-f0-9]{64}$/;
14
27
  function OxyServicesAssetsMixin(Base) {
15
28
  return class extends Base {
16
29
  constructor(...args) {
@@ -213,6 +226,69 @@ function OxyServicesAssetsMixin(Base) {
213
226
  }));
214
227
  return settled.flat();
215
228
  }
229
+ /**
230
+ * Reverse content-address lookup: resolve many content `sha256` digests to
231
+ * the servable Oxy asset holding each, in one round-trip per chunk via
232
+ * `POST /assets/service/by-sha256` (body `{ sha256s }`).
233
+ *
234
+ * This is the INVERSE of {@link getServiceAssetMetadataByIds}: given a
235
+ * record's `blob.sha256`, it returns the asset's `id`, `mime`, byte `size`,
236
+ * `status`, and — for active, public, CDN-reachable assets only — a public
237
+ * `url` (`cloud.oxy.so`). Built for server-to-server callers (e.g. Mention's
238
+ * MTN materializer / node-blob sync) that hold a content hash and need to
239
+ * map it back to a servable asset. Hashes are lowercased, validated against
240
+ * a 64-char hex pattern (malformed entries dropped client-side), and
241
+ * deduplicated before being split into chunks of
242
+ * {@link SERVICE_ASSET_METADATA_BY_SHA_CHUNK_SIZE} (the server-side cap). The
243
+ * server omits unknown/deleted hashes from each chunk's `data`, so the
244
+ * merged result may be shorter than the requested list and the caller is
245
+ * expected to map by `sha256`.
246
+ *
247
+ * **Service-token auth (required).** `/assets/service/by-sha256` is guarded
248
+ * by `serviceAuthMiddleware` + the `files:read` scope and is called via
249
+ * `makeServiceRequest` (`Authorization: Bearer <serviceToken>`, `cache:false`).
250
+ * The calling client MUST be service-configured (`configureServiceAuth`)
251
+ * before invoking; a plain user-session request is rejected by the route's
252
+ * service-auth guard.
253
+ *
254
+ * Resilience: chunks are independent. A failed chunk is logged and skipped —
255
+ * every entry that resolved is still returned. An empty input (or one whose
256
+ * every value is malformed) resolves immediately with `[]` and performs no
257
+ * network call.
258
+ *
259
+ * Not cached at the SDK layer: it's a POST keyed on a multi-hash body (low
260
+ * hit rate), mirroring the sibling service/POST methods which never cache.
261
+ */
262
+ async getServiceAssetMetadataBySha256(sha256s) {
263
+ const uniqueShas = Array.from(new Set(sha256s
264
+ .filter((sha) => typeof sha === 'string')
265
+ .map((sha) => sha.trim().toLowerCase())
266
+ .filter((sha) => SHA256_HEX_PATTERN.test(sha))));
267
+ if (uniqueShas.length === 0) {
268
+ return [];
269
+ }
270
+ const chunks = [];
271
+ for (let i = 0; i < uniqueShas.length; i += SERVICE_ASSET_METADATA_BY_SHA_CHUNK_SIZE) {
272
+ chunks.push(uniqueShas.slice(i, i + SERVICE_ASSET_METADATA_BY_SHA_CHUNK_SIZE));
273
+ }
274
+ // Run chunks concurrently; a single chunk failure must not sink the rest.
275
+ const settled = await Promise.all(chunks.map(async (chunk) => {
276
+ try {
277
+ const entries = await this.makeServiceRequest('POST', '/assets/service/by-sha256', { sha256s: chunk });
278
+ return Array.isArray(entries) ? entries : [];
279
+ }
280
+ catch (error) {
281
+ loggerUtils_1.logger.warn('getServiceAssetMetadataBySha256: chunk failed, continuing with remaining chunks', {
282
+ method: 'getServiceAssetMetadataBySha256',
283
+ chunkSize: chunk.length,
284
+ status: (0, errorUtils_1.extractErrorStatus)(error),
285
+ error: error instanceof Error ? error.message : String(error),
286
+ });
287
+ return [];
288
+ }
289
+ }));
290
+ return settled.flat();
291
+ }
216
292
  /**
217
293
  * Upload raw file data
218
294
  */
@@ -206,6 +206,8 @@ function registerCacheForCleanup(cache) {
206
206
  cache.cleanup();
207
207
  }
208
208
  }, 60000); // Every minute
209
+ // Never block process/worker exit on a housekeeping timer.
210
+ cleanupInterval.unref?.();
209
211
  }
210
212
  }
211
213
  /**
@@ -50,12 +50,14 @@ exports.ssoDestKey = ssoDestKey;
50
50
  exports.ssoNoSessionKey = ssoNoSessionKey;
51
51
  exports.ssoAttemptedKey = ssoAttemptedKey;
52
52
  exports.ssoPriorSessionKey = ssoPriorSessionKey;
53
+ exports.ssoSignedOutKey = ssoSignedOutKey;
53
54
  exports.ssoCallbackBootstrapKey = ssoCallbackBootstrapKey;
54
55
  exports.getSsoCallbackBootstrapScript = getSsoCallbackBootstrapScript;
55
56
  exports.ssoNavigate = ssoNavigate;
56
57
  exports.buildSsoBounceUrl = buildSsoBounceUrl;
57
58
  exports.isCentralIdPOrigin = isCentralIdPOrigin;
58
59
  exports.guardActive = guardActive;
60
+ exports.silentRestoreSuppressed = silentRestoreSuppressed;
59
61
  exports.allowSsoBounce = allowSsoBounce;
60
62
  const authWebUrl_1 = require("./authWebUrl");
61
63
  /**
@@ -81,6 +83,7 @@ const NO_SESSION_KEY_PREFIX = 'oxy_sso_no_session:';
81
83
  const ATTEMPTED_KEY_PREFIX = 'oxy_sso_attempted:';
82
84
  const CALLBACK_BOOTSTRAP_KEY_PREFIX = 'oxy_sso_callback_bootstrap:';
83
85
  const PRIOR_SESSION_KEY_PREFIX = 'oxy_sso_prior_session:';
86
+ const SIGNED_OUT_KEY_PREFIX = 'oxy_signed_out:';
84
87
  /** Per-origin CSRF state key (matched on return to defeat fragment forgery). */
85
88
  function ssoStateKey(origin) {
86
89
  return `${STATE_KEY_PREFIX}${origin}`;
@@ -130,6 +133,36 @@ function ssoAttemptedKey(origin) {
130
133
  function ssoPriorSessionKey(origin) {
131
134
  return `${PRIOR_SESSION_KEY_PREFIX}${origin}`;
132
135
  }
136
+ /**
137
+ * Per-origin DURABLE "the user DELIBERATELY signed out on this device/origin"
138
+ * flag.
139
+ *
140
+ * Like {@link ssoPriorSessionKey} this lives in DURABLE storage (web
141
+ * `localStorage`; services uses its own `storageKeyPrefix`-scoped key), NOT the
142
+ * per-tab `sessionStorage` the loop-breaker keys use — it must survive a reload.
143
+ *
144
+ * It exists purely to suppress AUTOMATIC silent restore after a deliberate
145
+ * sign-out: a still-live IdP session (the central `fedcm_session` / the FedCM
146
+ * credential association) would otherwise let `fedcm-silent` / the per-apex
147
+ * `/auth/silent` iframe re-mint a session on the very next cold boot, so a user
148
+ * who pressed "Sign out" gets silently signed back in on reload. With this flag
149
+ * set, those silent cold-boot steps are skipped while the Gmail-style
150
+ * returning-account fast-path is otherwise preserved.
151
+ *
152
+ * Lifecycle (mirrors the existing gate machinery — set on a definitive event,
153
+ * cleared on its inverse):
154
+ * - SET on EXPLICIT full sign-out (alongside clearing the prior-session hint
155
+ * and the SSO bounce state).
156
+ * - CLEARED on ANY deliberate sign-in (password, FedCM, account switch, device
157
+ * claim) so a real sign-in fully re-enables silent restore — there is no
158
+ * "stuck signed out" state.
159
+ *
160
+ * NOTE: this gates only AUTOMATIC/silent restore. An INTERACTIVE sign-in always
161
+ * clears it first, so the user can always sign back in.
162
+ */
163
+ function ssoSignedOutKey(origin) {
164
+ return `${SIGNED_OUT_KEY_PREFIX}${origin}`;
165
+ }
133
166
  /**
134
167
  * Per-origin marker written by the pre-hydration callback bootstrap.
135
168
  *
@@ -260,6 +293,32 @@ function guardActive(storage, origin, now = Date.now()) {
260
293
  }
261
294
  return now - ts < exports.SSO_GUARD_TTL_MS;
262
295
  }
296
+ /**
297
+ * Whether AUTOMATIC silent restore is SUPPRESSED for this origin because the
298
+ * user deliberately signed out (the durable {@link ssoSignedOutKey} flag).
299
+ *
300
+ * When `true`, the silent cold-boot steps that can re-mint a session from a
301
+ * still-live IdP session WITHOUT user intent — `fedcm-silent` and the per-apex
302
+ * `/auth/silent` iframe — MUST be skipped, so a user who pressed "Sign out" is
303
+ * not silently signed back in on the next reload. Interactive sign-in clears the
304
+ * flag, so this never blocks a deliberate re-sign-in.
305
+ *
306
+ * Defensive: a `getItem` that throws (locked/disabled storage) is treated as NOT
307
+ * suppressed, so the gate fails toward the normal restore behaviour rather than
308
+ * wedging the user out.
309
+ *
310
+ * @param storage - The DURABLE storage to read (web `localStorage`; injected for
311
+ * testability).
312
+ * @param origin - The page origin whose signed-out flag to evaluate.
313
+ */
314
+ function silentRestoreSuppressed(storage, origin) {
315
+ try {
316
+ return storage.getItem(ssoSignedOutKey(origin)) === '1';
317
+ }
318
+ catch {
319
+ return false;
320
+ }
321
+ }
263
322
  /**
264
323
  * Decide whether the terminal `/sso` establish-bounce is ALLOWED for this
265
324
  * visitor (the smart `enabled` gate for the `sso-bounce` cold-boot step).