@oxyhq/core 1.11.24 → 2.1.0

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 (150) hide show
  1. package/README.md +5 -6
  2. package/dist/cjs/.tsbuildinfo +1 -1
  3. package/dist/cjs/AuthManager.js +678 -4
  4. package/dist/cjs/AuthManagerTypes.js +13 -0
  5. package/dist/cjs/CrossDomainAuth.js +45 -3
  6. package/dist/cjs/OxyServices.base.js +16 -0
  7. package/dist/cjs/i18n/locales/ar-SA.json +83 -0
  8. package/dist/cjs/i18n/locales/ca-ES.json +83 -0
  9. package/dist/cjs/i18n/locales/de-DE.json +83 -0
  10. package/dist/cjs/i18n/locales/en-US.json +83 -0
  11. package/dist/cjs/i18n/locales/es-ES.json +99 -4
  12. package/dist/cjs/i18n/locales/fr-FR.json +83 -0
  13. package/dist/cjs/i18n/locales/it-IT.json +83 -0
  14. package/dist/cjs/i18n/locales/ja-JP.json +83 -0
  15. package/dist/cjs/i18n/locales/ko-KR.json +83 -0
  16. package/dist/cjs/i18n/locales/locales/ar-SA.json +83 -1
  17. package/dist/cjs/i18n/locales/locales/ca-ES.json +83 -1
  18. package/dist/cjs/i18n/locales/locales/de-DE.json +83 -1
  19. package/dist/cjs/i18n/locales/locales/en-US.json +83 -0
  20. package/dist/cjs/i18n/locales/locales/es-ES.json +99 -4
  21. package/dist/cjs/i18n/locales/locales/fr-FR.json +83 -1
  22. package/dist/cjs/i18n/locales/locales/it-IT.json +83 -1
  23. package/dist/cjs/i18n/locales/locales/ja-JP.json +200 -117
  24. package/dist/cjs/i18n/locales/locales/ko-KR.json +83 -1
  25. package/dist/cjs/i18n/locales/locales/pt-PT.json +83 -1
  26. package/dist/cjs/i18n/locales/locales/zh-CN.json +83 -1
  27. package/dist/cjs/i18n/locales/pt-PT.json +83 -0
  28. package/dist/cjs/i18n/locales/zh-CN.json +83 -0
  29. package/dist/cjs/index.js +121 -57
  30. package/dist/cjs/mixins/OxyServices.auth.js +235 -0
  31. package/dist/cjs/mixins/OxyServices.fedcm.js +36 -0
  32. package/dist/cjs/mixins/OxyServices.popup.js +61 -1
  33. package/dist/cjs/mixins/OxyServices.user.js +18 -0
  34. package/dist/cjs/utils/accountUtils.js +64 -1
  35. package/dist/cjs/utils/coldBoot.js +71 -0
  36. package/dist/cjs/utils/fapiAutoDetect.js +88 -0
  37. package/dist/esm/.tsbuildinfo +1 -1
  38. package/dist/esm/AuthManager.js +678 -4
  39. package/dist/esm/AuthManagerTypes.js +12 -0
  40. package/dist/esm/CrossDomainAuth.js +45 -3
  41. package/dist/esm/OxyServices.base.js +16 -0
  42. package/dist/esm/i18n/locales/ar-SA.json +83 -0
  43. package/dist/esm/i18n/locales/ca-ES.json +83 -0
  44. package/dist/esm/i18n/locales/de-DE.json +83 -0
  45. package/dist/esm/i18n/locales/en-US.json +83 -0
  46. package/dist/esm/i18n/locales/es-ES.json +99 -4
  47. package/dist/esm/i18n/locales/fr-FR.json +83 -0
  48. package/dist/esm/i18n/locales/it-IT.json +83 -0
  49. package/dist/esm/i18n/locales/ja-JP.json +83 -0
  50. package/dist/esm/i18n/locales/ko-KR.json +83 -0
  51. package/dist/esm/i18n/locales/locales/ar-SA.json +83 -1
  52. package/dist/esm/i18n/locales/locales/ca-ES.json +83 -1
  53. package/dist/esm/i18n/locales/locales/de-DE.json +83 -1
  54. package/dist/esm/i18n/locales/locales/en-US.json +83 -0
  55. package/dist/esm/i18n/locales/locales/es-ES.json +99 -4
  56. package/dist/esm/i18n/locales/locales/fr-FR.json +83 -1
  57. package/dist/esm/i18n/locales/locales/it-IT.json +83 -1
  58. package/dist/esm/i18n/locales/locales/ja-JP.json +200 -117
  59. package/dist/esm/i18n/locales/locales/ko-KR.json +83 -1
  60. package/dist/esm/i18n/locales/locales/pt-PT.json +83 -1
  61. package/dist/esm/i18n/locales/locales/zh-CN.json +83 -1
  62. package/dist/esm/i18n/locales/pt-PT.json +83 -0
  63. package/dist/esm/i18n/locales/zh-CN.json +83 -0
  64. package/dist/esm/index.js +74 -26
  65. package/dist/esm/mixins/OxyServices.auth.js +235 -0
  66. package/dist/esm/mixins/OxyServices.fedcm.js +36 -0
  67. package/dist/esm/mixins/OxyServices.popup.js +61 -1
  68. package/dist/esm/mixins/OxyServices.user.js +18 -0
  69. package/dist/esm/utils/accountUtils.js +61 -0
  70. package/dist/esm/utils/coldBoot.js +68 -0
  71. package/dist/esm/utils/fapiAutoDetect.js +85 -0
  72. package/dist/types/.tsbuildinfo +1 -1
  73. package/dist/types/AuthManager.d.ts +243 -3
  74. package/dist/types/AuthManagerTypes.d.ts +68 -0
  75. package/dist/types/CrossDomainAuth.d.ts +23 -0
  76. package/dist/types/OxyServices.base.d.ts +14 -0
  77. package/dist/types/OxyServices.d.ts +7 -0
  78. package/dist/types/index.d.ts +31 -17
  79. package/dist/types/mixins/OxyServices.analytics.d.ts +1 -0
  80. package/dist/types/mixins/OxyServices.appData.d.ts +1 -0
  81. package/dist/types/mixins/OxyServices.assets.d.ts +4 -1
  82. package/dist/types/mixins/OxyServices.auth.d.ts +73 -1
  83. package/dist/types/mixins/OxyServices.contacts.d.ts +1 -0
  84. package/dist/types/mixins/OxyServices.developer.d.ts +1 -0
  85. package/dist/types/mixins/OxyServices.devices.d.ts +1 -0
  86. package/dist/types/mixins/OxyServices.features.d.ts +2 -5
  87. package/dist/types/mixins/OxyServices.fedcm.d.ts +34 -0
  88. package/dist/types/mixins/OxyServices.karma.d.ts +1 -0
  89. package/dist/types/mixins/OxyServices.language.d.ts +1 -0
  90. package/dist/types/mixins/OxyServices.location.d.ts +1 -0
  91. package/dist/types/mixins/OxyServices.managedAccounts.d.ts +1 -0
  92. package/dist/types/mixins/OxyServices.payment.d.ts +1 -0
  93. package/dist/types/mixins/OxyServices.popup.d.ts +40 -0
  94. package/dist/types/mixins/OxyServices.privacy.d.ts +1 -0
  95. package/dist/types/mixins/OxyServices.redirect.d.ts +1 -0
  96. package/dist/types/mixins/OxyServices.security.d.ts +1 -0
  97. package/dist/types/mixins/OxyServices.topics.d.ts +1 -0
  98. package/dist/types/mixins/OxyServices.user.d.ts +16 -1
  99. package/dist/types/mixins/OxyServices.utility.d.ts +1 -0
  100. package/dist/types/models/interfaces.d.ts +98 -0
  101. package/dist/types/models/session.d.ts +8 -0
  102. package/dist/types/utils/accountUtils.d.ts +33 -0
  103. package/dist/types/utils/coldBoot.d.ts +102 -0
  104. package/dist/types/utils/fapiAutoDetect.d.ts +37 -0
  105. package/package.json +9 -18
  106. package/src/AuthManager.ts +776 -7
  107. package/src/AuthManagerTypes.ts +72 -0
  108. package/src/CrossDomainAuth.ts +54 -3
  109. package/src/OxyServices.base.ts +17 -0
  110. package/src/OxyServices.ts +7 -0
  111. package/src/__tests__/authManager.cookiePath.test.ts +339 -0
  112. package/src/__tests__/authManager.security.test.ts +342 -0
  113. package/src/__tests__/crossDomainAuth.test.ts +191 -0
  114. package/src/i18n/locales/ar-SA.json +83 -1
  115. package/src/i18n/locales/ca-ES.json +83 -1
  116. package/src/i18n/locales/de-DE.json +83 -1
  117. package/src/i18n/locales/en-US.json +83 -0
  118. package/src/i18n/locales/es-ES.json +99 -4
  119. package/src/i18n/locales/fr-FR.json +83 -1
  120. package/src/i18n/locales/it-IT.json +83 -1
  121. package/src/i18n/locales/ja-JP.json +200 -117
  122. package/src/i18n/locales/ko-KR.json +83 -1
  123. package/src/i18n/locales/pt-PT.json +83 -1
  124. package/src/i18n/locales/zh-CN.json +83 -1
  125. package/src/index.ts +309 -112
  126. package/src/mixins/OxyServices.auth.ts +268 -1
  127. package/src/mixins/OxyServices.fedcm.ts +63 -0
  128. package/src/mixins/OxyServices.popup.ts +79 -1
  129. package/src/mixins/OxyServices.user.ts +33 -1
  130. package/src/mixins/__tests__/popup.test.ts +307 -0
  131. package/src/mixins/__tests__/sessionBaseUrl.test.ts +61 -0
  132. package/src/models/interfaces.ts +116 -0
  133. package/src/models/session.ts +8 -0
  134. package/src/utils/__tests__/coldBoot.test.ts +226 -0
  135. package/src/utils/__tests__/fapiAutoDetect.test.ts +93 -0
  136. package/src/utils/accountUtils.ts +84 -0
  137. package/src/utils/coldBoot.ts +136 -0
  138. package/src/utils/fapiAutoDetect.ts +82 -0
  139. package/dist/cjs/crypto/index.js +0 -22
  140. package/dist/cjs/shared/index.js +0 -70
  141. package/dist/cjs/utils/index.js +0 -26
  142. package/dist/esm/crypto/index.js +0 -13
  143. package/dist/esm/shared/index.js +0 -31
  144. package/dist/esm/utils/index.js +0 -7
  145. package/dist/types/crypto/index.d.ts +0 -11
  146. package/dist/types/shared/index.d.ts +0 -28
  147. package/dist/types/utils/index.d.ts +0 -6
  148. package/src/crypto/index.ts +0 -30
  149. package/src/shared/index.ts +0 -82
  150. package/src/utils/index.ts +0 -21
@@ -8,6 +8,7 @@
8
8
  */
9
9
  import type { OxyServices } from './OxyServices';
10
10
  import type { SessionLoginResponse, MinimalUserData } from './models/session';
11
+ import type { AuthManagerAccount, RestoreFromCookiesResult, SwitchAuthuserResult } from './AuthManagerTypes';
11
12
  /**
12
13
  * Storage adapter interface for platform-agnostic storage.
13
14
  */
@@ -36,6 +37,20 @@ export interface AuthManagerConfig {
36
37
  refreshBuffer?: number;
37
38
  /** Enable cross-tab coordination via BroadcastChannel (default: true in browsers) */
38
39
  crossTabSync?: boolean;
40
+ /**
41
+ * "Cookie-only" mode for web apps that rely exclusively on the
42
+ * `oxy_rt_${authuser}` httpOnly refresh cookies and refuse to fall back
43
+ * to the legacy localStorage token/refresh-token path.
44
+ *
45
+ * - `false` (default): `initialize()` tries `restoreFromCookies()` first;
46
+ * if no accounts are restored it falls back to the legacy localStorage
47
+ * path (`oxy_access_token` / `oxy_session`).
48
+ * - `true`: `initialize()` ONLY uses `restoreFromCookies()`. No token /
49
+ * refresh-token / session JSON is read from or written to localStorage.
50
+ * This is the secure default for apps that ship the cookie path end-to-
51
+ * end and want to guarantee no tokens leak to JS-accessible storage.
52
+ */
53
+ cookieOnly?: boolean;
39
54
  }
40
55
  /**
41
56
  * AuthManager - Centralized authentication management.
@@ -76,6 +91,66 @@ export declare class AuthManager {
76
91
  private _broadcastChannel;
77
92
  /** Set to true when another tab broadcasts a successful refresh, so this tab can skip its own. */
78
93
  private _otherTabRefreshed;
94
+ /**
95
+ * Identifier for this AuthManager instance (≈ "this tab"). Random hex
96
+ * generated at construction; advertised in every outgoing broadcast and
97
+ * used as the lookup key in `_knownPeerNonces`.
98
+ */
99
+ private readonly _tabId;
100
+ /**
101
+ * Per-tab nonce, advertised in every outgoing broadcast. Receivers record
102
+ * the first (tabId, nonce) pair they see from a given peer; subsequent
103
+ * messages from the same tabId MUST carry the same nonce or they're
104
+ * ignored.
105
+ *
106
+ * Threat model: a same-origin XSS payload can post to the channel but can
107
+ * NOT read this instance's private `_broadcastNonce` field (it lives in
108
+ * closure, not on `window`). Forged broadcasts from XSS therefore can't
109
+ * impersonate this tab. A new attacker-controlled tabId trips the
110
+ * "first message from a new peer" branch, which is by definition trusted
111
+ * — so the gate raises the bar but is not a complete defence (a perfect
112
+ * mitigation would require message signing with a server-issued key).
113
+ */
114
+ private readonly _broadcastNonce;
115
+ /**
116
+ * Bounded LRU of `(tabId → nonce)` pairs seen on inbound broadcasts. First
117
+ * sighting of a new tabId records its nonce; later messages from that
118
+ * tabId are rejected if the nonce doesn't match.
119
+ */
120
+ private readonly _knownPeerNonces;
121
+ private static readonly _MAX_KNOWN_PEERS;
122
+ /**
123
+ * In-flight `switchAuthuser` promise. Deduplicates concurrent calls so two
124
+ * near-simultaneous switches don't both fire refresh requests and rotate
125
+ * the slot twice. Mirrors the `refreshPromise` pattern used by
126
+ * `refreshToken`.
127
+ */
128
+ private _switchPromise;
129
+ /**
130
+ * Last `restoreFromCookies()` completion timestamp, keyed by the
131
+ * AuthManager's active authuser at the time of completion. Used to gate
132
+ * cross-tab cascade: a flurry of BroadcastChannel events from sibling
133
+ * tabs can otherwise trigger N back-to-back snapshots and rotate every
134
+ * slot's access token N times.
135
+ */
136
+ private readonly _lastRestoreAt;
137
+ private static readonly _RESTORE_DEBOUNCE_MS;
138
+ /**
139
+ * In-memory registry of every device-local account the AuthManager knows
140
+ * about, keyed by `authuser` slot index. Populated by:
141
+ * - `restoreFromCookies()` (cold boot)
142
+ * - `switchAuthuser()` (per-account rotation)
143
+ * - `handleAuthSuccess()` (fresh login when the server response carries
144
+ * an `authuser` field)
145
+ * Access tokens live ONLY here in the cookie path — they are never
146
+ * persisted to localStorage.
147
+ */
148
+ private accounts;
149
+ /**
150
+ * Currently-active `authuser` slot in the cookie path. `null` means either
151
+ * the cookie path hasn't been initialised yet, or no slots are signed in.
152
+ */
153
+ private activeAuthuser;
79
154
  constructor(oxyServices: OxyServices, config?: AuthManagerConfig);
80
155
  /**
81
156
  * Initialize BroadcastChannel for cross-tab token refresh coordination.
@@ -87,9 +162,38 @@ export declare class AuthManager {
87
162
  */
88
163
  private _handleCrossTabMessage;
89
164
  /**
90
- * Broadcast a message to other tabs.
165
+ * Broadcast a message to other tabs. Always stamps this tab's `tabId` and
166
+ * `nonce` onto the message so receivers can run the cross-tab nonce gate.
91
167
  */
92
168
  private _broadcast;
169
+ /**
170
+ * Generate `bytes` bytes of cryptographic randomness encoded as lowercase
171
+ * hex. Prefers Web Crypto's `getRandomValues` when available (browser /
172
+ * modern Node); falls back to `Math.random` ONLY in environments without
173
+ * Web Crypto (the resulting nonce is still unguessable to a same-origin
174
+ * XSS payload — the goal is unforgeability across tabs, not cryptographic
175
+ * secrecy across the network).
176
+ */
177
+ private static _randomHex;
178
+ /**
179
+ * Validate an inbound broadcast against the cross-tab nonce gate.
180
+ *
181
+ * Returns `true` when the message should be honoured, `false` when it
182
+ * MUST be ignored:
183
+ * - Message is missing `tabId` or `nonce` → ignore (forged or
184
+ * mismatched-version sibling tab).
185
+ * - First sighting of `tabId` → record the nonce and honour the message
186
+ * (trust-on-first-use, the best we can do without a shared secret).
187
+ * - Subsequent message from the same `tabId` with the SAME nonce →
188
+ * honour.
189
+ * - Subsequent message from the same `tabId` with a DIFFERENT nonce →
190
+ * ignore (the canonical "forged broadcast" case — a same-origin XSS
191
+ * payload can't read the real tab's `_broadcastNonce`).
192
+ *
193
+ * Echoes of this tab's own broadcasts (same `tabId`) are also dropped so
194
+ * we don't react to our own messages.
195
+ */
196
+ private _acceptBroadcast;
93
197
  /**
94
198
  * Get default storage based on environment.
95
199
  */
@@ -147,11 +251,147 @@ export declare class AuthManager {
147
251
  */
148
252
  getAuthMethod(): Promise<AuthMethod | null>;
149
253
  /**
150
- * Initialize auth state from storage.
254
+ * Initialize auth state on app startup.
151
255
  *
152
- * Call this on app startup to restore previous session.
256
+ * Order of operations:
257
+ * 1. Try the cookie path via `restoreFromCookies()`. This is the
258
+ * preferred path because the httpOnly refresh cookies are
259
+ * cross-tab, persist across hard reloads, and don't expose any
260
+ * refresh-token material to JS.
261
+ * 2. If the cookie path yielded zero accounts AND `cookieOnly` is
262
+ * `false`, fall back to the legacy localStorage path
263
+ * (`oxy_access_token` / `oxy_session`) for backwards compatibility
264
+ * with apps that haven't migrated to the cookie endpoint yet.
265
+ * 3. If `cookieOnly` is `true`, skip the legacy fallback entirely.
266
+ * This guarantees no tokens or refresh tokens are ever read from
267
+ * or written to JS-accessible storage.
268
+ *
269
+ * Returns the active user on success, or `null` when neither path
270
+ * restored a session.
153
271
  */
154
272
  initialize(): Promise<MinimalUserData | null>;
273
+ /**
274
+ * Read the persisted active `authuser` slot index. Returns `null` when
275
+ * none is persisted, the value is corrupt, or the storage adapter has no
276
+ * record. Storage failures are non-fatal: the cookie path falls back to
277
+ * "lowest authuser" deterministic selection.
278
+ */
279
+ private readActiveAuthuser;
280
+ /**
281
+ * Persist the active `authuser` slot index. No-ops on storage failure
282
+ * (e.g. Safari private mode, native SecureStore unavailable) — this is
283
+ * best-effort UX persistence, not authoritative state.
284
+ */
285
+ private writeActiveAuthuser;
286
+ /**
287
+ * Clear the persisted active `authuser` so the next cold boot starts from
288
+ * a clean slate (used on full sign-out).
289
+ */
290
+ private clearActiveAuthuser;
291
+ /**
292
+ * Build a `MinimalUserData` from a `RefreshAllAccount`. Returns `null` when
293
+ * the wire entry has no user shape (legacy `/auth/refresh` fallback) — the
294
+ * AuthManager's caller is expected to hydrate via `/users/me` in that
295
+ * case.
296
+ */
297
+ private static toMinimalUser;
298
+ /**
299
+ * Hydrate the user shape for a slot whose AuthManagerAccount currently has
300
+ * `user: null` (legacy refresh fallback, or a switch onto a previously
301
+ * unknown slot). Calls `/users/me` with the slot's freshly-planted access
302
+ * token already on the HTTP client; merges the result back into the
303
+ * registry entry. Network failures are non-fatal — the slot remains with
304
+ * `user: null` and the UI is expected to render the public-key fallback
305
+ * handle until a later restore picks the real user shape up.
306
+ */
307
+ private _hydrateUnknownUser;
308
+ /**
309
+ * Snapshot of the registered cookie-path accounts, sorted by `authuser`
310
+ * ascending (canonical order). Mutating the returned array does not
311
+ * affect AuthManager state.
312
+ */
313
+ getAccounts(): AuthManagerAccount[];
314
+ /**
315
+ * The slot index that is currently active in the cookie path, or `null`
316
+ * if the cookie path hasn't been initialised or no slots are signed in.
317
+ */
318
+ getActiveAuthuser(): number | null;
319
+ /**
320
+ * Convenience: the AuthManagerAccount currently flagged active.
321
+ */
322
+ getActiveAccount(): AuthManagerAccount | null;
323
+ /**
324
+ * Restore every device-local account from the httpOnly refresh cookies.
325
+ *
326
+ * Calls `oxyServices.refreshAllSessions()` (`POST /auth/refresh-all` with
327
+ * `credentials: 'include'`). The server rotates every presented
328
+ * `oxy_rt_${authuser}` cookie in parallel and returns one entry per
329
+ * VALID slot. The SDK transparently falls back to the legacy single-slot
330
+ * `/auth/refresh` against older servers (handled inside
331
+ * `refreshAllSessions`).
332
+ *
333
+ * Plants the active account's access token on the shared HTTP client;
334
+ * sibling slots' tokens stay in the in-memory registry so a later
335
+ * `switchAuthuser()` can hot-swap them without a network round-trip.
336
+ *
337
+ * The persisted `oxy_active_authuser` slot wins when it matches a
338
+ * returned account; otherwise the lowest returned `authuser` is chosen
339
+ * deterministically.
340
+ *
341
+ * Returns `{ accounts: [], activeAuthuser: null }` on any failure or
342
+ * empty snapshot — callers treat that as "no signed-in accounts" and
343
+ * proceed unauthenticated. State is NOT cleared on failure; existing
344
+ * accounts (if any) remain intact.
345
+ */
346
+ restoreFromCookies(): Promise<RestoreFromCookiesResult>;
347
+ /**
348
+ * Switch the active account to a different device-local slot.
349
+ *
350
+ * Calls `oxyServices.refreshTokenViaCookie({ authuser })` to mint a fresh
351
+ * access token from the slot's httpOnly cookie, updates the in-memory
352
+ * registry entry, plants the token on the HTTP client, persists the new
353
+ * active slot, and broadcasts cross-tab.
354
+ *
355
+ * Throws when the slot's refresh cookie is missing / expired / reused
356
+ * (the SDK returns `null` from `refreshTokenViaCookie` in that case, and
357
+ * we surface it as an `Error` so callers can clean up the slot from
358
+ * their UI).
359
+ */
360
+ switchAuthuser(authuser: number): Promise<SwitchAuthuserResult>;
361
+ private _doSwitchAuthuser;
362
+ /**
363
+ * Sign out a single device-local slot.
364
+ *
365
+ * Calls `oxyServices.logoutSessionByAuthuser(authuser)`: server-side
366
+ * revokes the slot's refresh-token family and clears the
367
+ * `oxy_rt_${authuser}` cookie via `Set-Cookie`. The slot is removed from
368
+ * the in-memory registry. If the slot was active, the next lowest
369
+ * remaining authuser becomes active (or `null` when none remain).
370
+ */
371
+ signOutAuthuser(authuser: number): Promise<void>;
372
+ /**
373
+ * Sign out EVERY device-local account on this device.
374
+ *
375
+ * Calls `oxyServices.logoutAllSessionsViaCookie()`: server-side revokes
376
+ * every presented family and `Set-Cookie`s an immediate expiry for every
377
+ * recognised `oxy_rt_${n}` slot AND the legacy `oxy_rt` cookie. The
378
+ * in-memory registry is wiped, the active slot is cleared, and the
379
+ * persisted `oxy_active_authuser` is removed so the next cold boot
380
+ * starts fresh.
381
+ */
382
+ signOutAllViaCookies(): Promise<void>;
383
+ /**
384
+ * Schedule an auto-refresh for the cookie path on the active slot. Reuses
385
+ * the same single `refreshTimer` as the legacy path (the AuthManager has
386
+ * exactly ONE active slot at a time, so one timer suffices).
387
+ */
388
+ private setupCookieRefresh;
389
+ /**
390
+ * Decode the session id from an unverified JWT access token. Decode-only
391
+ * (no signature verification) — the server already verified the
392
+ * signature when minting the token. Returns `null` on malformed input.
393
+ */
394
+ private static decodeSessionIdFromAccessToken;
155
395
  /**
156
396
  * Destroy the auth manager and clean up resources.
157
397
  */
@@ -0,0 +1,68 @@
1
+ /**
2
+ * AuthManager — public types for the multi-account cookie path.
3
+ *
4
+ * Lives in its own module (rather than the 670-line `models/interfaces.ts`)
5
+ * so consumers can `import type` exactly the multi-account surface without
6
+ * pulling in the full interfaces graph, and so `AuthManager.ts` stays
7
+ * decoupled from the wire shapes — these types re-state the wire as the
8
+ * AuthManager's in-memory representation.
9
+ *
10
+ * @module core/AuthManagerTypes
11
+ */
12
+ import type { RefreshAllAccountUser } from './models/interfaces';
13
+ /**
14
+ * One device-local account known to `AuthManager` in the cookie path.
15
+ *
16
+ * Built from a `POST /auth/refresh-all` entry, OR from a single
17
+ * `POST /auth/refresh?authuser=N` rotation after a switch, OR from a
18
+ * `handleAuthSuccess` call after a fresh login. The `accessToken` is held in
19
+ * memory only — the refresh token never enters JS (it lives in the httpOnly
20
+ * `oxy_rt_${authuser}` cookie).
21
+ */
22
+ export interface AuthManagerAccount {
23
+ /** Device-local cookie slot index (0..N-1). */
24
+ authuser: number;
25
+ /** Server-side session id this slot is bound to. */
26
+ sessionId: string;
27
+ /**
28
+ * Projected user shape from the wire (username/avatar/color/email).
29
+ *
30
+ * `null` when a refresh-via-cookie planted a fresh access token for a slot
31
+ * that the AuthManager has no prior in-memory user metadata for — e.g. the
32
+ * legacy `/auth/refresh` 404 fallback path inside `refreshAllSessions`, or
33
+ * a `switchAuthuser` against a slot that wasn't present in the previous
34
+ * `restoreFromCookies` snapshot. Callers (or the AuthManager itself) are
35
+ * expected to hydrate the user shape via `getCurrentUser()` after the token
36
+ * is planted; the chooser UI must render the public-key fallback handle
37
+ * until the hydration completes.
38
+ */
39
+ user: RefreshAllAccountUser | null;
40
+ /** Currently-valid access token for this slot (in-memory only). */
41
+ accessToken: string;
42
+ /** ISO-8601 expiry of the access token. */
43
+ expiresAt: string;
44
+ }
45
+ /**
46
+ * Outcome of `AuthManager.restoreFromCookies()`.
47
+ *
48
+ * `accounts` is sorted by `authuser` ascending (matching the server's
49
+ * canonical ordering). `activeAuthuser` is whichever slot the AuthManager
50
+ * picked as active — usually the persisted `oxy_active_authuser` if it
51
+ * matched a returned slot, otherwise the lowest returned `authuser`, or
52
+ * `null` if no accounts were restored.
53
+ */
54
+ export interface RestoreFromCookiesResult {
55
+ accounts: AuthManagerAccount[];
56
+ activeAuthuser: number | null;
57
+ }
58
+ /**
59
+ * Outcome of `AuthManager.switchAuthuser()`.
60
+ *
61
+ * Mirrors the wire `RefreshCookieResponse` but with `authuser` narrowed to
62
+ * `number` (the SDK boundary normalises the legacy `null` slot to `0`).
63
+ */
64
+ export interface SwitchAuthuserResult {
65
+ accessToken: string;
66
+ expiresAt: string;
67
+ authuser: number;
68
+ }
@@ -51,6 +51,13 @@ export interface CrossDomainAuthOptions {
51
51
  * Callback when auth method is selected
52
52
  */
53
53
  onMethodSelected?: (method: 'fedcm' | 'popup' | 'redirect') => void;
54
+ /**
55
+ * A popup window the caller already opened SYNCHRONOUSLY in the user-gesture
56
+ * handler. Forwarded to `OxyServices.signInWithPopup` so the popup is not
57
+ * blocked by Chrome after any prior `await` (FedCM / silent SSO) has
58
+ * consumed the transient user activation. See `OxyServices.openBlankPopup`.
59
+ */
60
+ popup?: Window | null;
54
61
  }
55
62
  export declare class CrossDomainAuth {
56
63
  private oxyServices;
@@ -67,6 +74,14 @@ export declare class CrossDomainAuth {
67
74
  * @returns Session with user data and access token
68
75
  */
69
76
  signIn(options?: CrossDomainAuthOptions): Promise<SessionLoginResponse | null>;
77
+ /**
78
+ * Close a caller-supplied popup window that is no longer needed (e.g. the
79
+ * resolved auth method didn't end up using it). Safe against null / already
80
+ * closed handles.
81
+ *
82
+ * @private
83
+ */
84
+ private closeOrphanPopup;
70
85
  /**
71
86
  * Automatic sign-in with progressive enhancement
72
87
  *
@@ -112,6 +127,14 @@ export declare class CrossDomainAuth {
112
127
  * For redirect method - restores previously authenticated session from localStorage
113
128
  */
114
129
  restoreSession(): boolean;
130
+ /**
131
+ * Open a blank popup SYNCHRONOUSLY (call from a raw user-gesture handler
132
+ * BEFORE any `await`). Returns `null` if the popup was blocked. Pass the
133
+ * handle into `signIn({ popup })` / `signInWithPopup({ popup })` so the
134
+ * popup is not blocked by Chrome after any prior `await` consumed the
135
+ * transient user activation. Delegates to `OxyServices.openBlankPopup`.
136
+ */
137
+ openBlankPopup(width?: number, height?: number): Window | null;
115
138
  /**
116
139
  * Check if FedCM is supported in current browser
117
140
  */
@@ -21,6 +21,20 @@ export declare class OxyServicesBase {
21
21
  * Get the configured Oxy API base URL
22
22
  */
23
23
  getBaseURL(): string;
24
+ /**
25
+ * Get the base URL the SDK's first-party session/refresh calls should target.
26
+ *
27
+ * Returns the configured `sessionBaseUrl` when provided, otherwise falls back
28
+ * to the API `baseURL` (`getBaseURL()`). Per the 2026 session architecture
29
+ * (docs/SESSION-ARCHITECTURE.md), non-`oxy.so` apps point this at their own
30
+ * same-site backend (e.g. `https://api.mention.earth`) whose session bridge
31
+ * forwards the user's refresh credential to `api.oxy.so`; `*.oxy.so` apps
32
+ * leave it unset so it resolves to `https://api.oxy.so` and nothing changes.
33
+ *
34
+ * This is additive: it only exposes configuration for `@oxyhq/services` to
35
+ * consume in a later phase. No refresh/auth logic in core reads it yet.
36
+ */
37
+ getSessionBaseUrl(): string;
24
38
  /**
25
39
  * Get the HTTP service instance
26
40
  * Useful for advanced use cases where direct access to the HTTP service is needed
@@ -117,6 +117,13 @@ export interface OxyServices extends InstanceType<ReturnType<typeof composeOxySe
117
117
  getFedCMConfig(): FedCMConfig;
118
118
  signInWithPopup(options?: PopupAuthOptions): Promise<SessionLoginResponse>;
119
119
  signUpWithPopup(options?: PopupAuthOptions): Promise<SessionLoginResponse>;
120
+ /**
121
+ * Open a blank popup SYNCHRONOUSLY (call from a raw user-gesture handler
122
+ * BEFORE any `await`). Returns `null` if the popup was blocked. Pass the
123
+ * handle into `signInWithPopup({ popup })` to navigate it to auth.oxy.so
124
+ * after the async portion of the sign-in flow runs.
125
+ */
126
+ openBlankPopup(width?: number, height?: number): Window | null;
120
127
  signInWithRedirect(options?: RedirectAuthOptions): void;
121
128
  signUpWithRedirect(options?: RedirectAuthOptions): void;
122
129
  auth(options?: {
@@ -12,31 +12,44 @@
12
12
  *
13
13
  * const user = await oxyClient.signIn(publicKey);
14
14
  * ```
15
+ *
16
+ * Every export below is NOMINAL — no `export *`, no barrels, no compat shims.
17
+ * If a symbol does not appear here, it is NOT part of the public API.
15
18
  */
16
19
  import './crypto/polyfill';
17
20
  export { OxyServices, OxyAuthenticationError, OxyAuthenticationTimeoutError } from './OxyServices';
18
21
  export { OXY_CLOUD_URL, oxyClient } from './OxyServices';
19
22
  export { AuthManager, createAuthManager } from './AuthManager';
20
- export type { StorageAdapter, AuthStateChangeCallback, AuthMethod, AuthManagerConfig } from './AuthManager';
23
+ export type { StorageAdapter, AuthStateChangeCallback, AuthMethod, AuthManagerConfig, } from './AuthManager';
24
+ export type { AuthManagerAccount, RestoreFromCookiesResult, SwitchAuthuserResult, } from './AuthManagerTypes';
21
25
  export { CrossDomainAuth, createCrossDomainAuth } from './CrossDomainAuth';
22
26
  export type { CrossDomainAuthOptions } from './CrossDomainAuth';
23
- export type { FedCMAuthOptions, FedCMConfig } from './mixins/OxyServices.fedcm';
27
+ export type { FedCMAuthOptions, FedCMConfig, AuthorizedApp } from './mixins/OxyServices.fedcm';
24
28
  export type { PopupAuthOptions } from './mixins/OxyServices.popup';
25
29
  export type { RedirectAuthOptions } from './mixins/OxyServices.redirect';
26
30
  export { ServiceCredentialMismatchError } from './mixins/OxyServices.auth';
27
31
  export type { ServiceTokenResponse } from './mixins/OxyServices.auth';
28
32
  export type { ServiceApp, ServiceActingAsVerification } from './mixins/OxyServices.utility';
29
- export type { CreateManagedAccountInput, ManagedAccountManager, ManagedAccount } from './mixins/OxyServices.managedAccounts';
30
- export type { ContactDiscoveryMatch, ContactDiscoveryResponse } from './mixins/OxyServices.contacts';
33
+ export type { CreateManagedAccountInput, ManagedAccountManager, ManagedAccount, } from './mixins/OxyServices.managedAccounts';
34
+ export type { ContactDiscoveryMatch, ContactDiscoveryResponse, } from './mixins/OxyServices.contacts';
31
35
  export { OxyAppDataIdentifierError } from './mixins/OxyServices.appData';
32
- export { KeyManager, SignatureService, RecoveryPhraseService, IdentityAlreadyExistsError, IdentityPersistError, } from './crypto';
33
- export type { KeyPair, SignedMessage, AuthChallenge, RecoveryPhraseResult } from './crypto';
34
- export * from './models/interfaces';
35
- export * from './models/session';
36
- export type { TopicData, TopicTranslation } from './models/Topic';
37
- export { TopicType, TopicSource } from './models/Topic';
36
+ export { SessionSyncRequiredError, AuthenticationFailedError, ensureValidToken, isAuthenticationError, withAuthErrorHandling, authenticatedApiCall, } from './utils/authHelpers';
37
+ export type { HandleApiErrorOptions } from './utils/authHelpers';
38
+ export { mergeSessions, normalizeAndSortSessions, sessionsArraysEqual, } from './utils/sessionUtils';
39
+ export type { ClientSession, StorageKeys, MinimalUserData, SessionLoginResponse, } from './models/session';
40
+ export type { RefreshAllResponse, RefreshAllAccount, RefreshAllAccountUser, RefreshCookieResponse, } from './models/interfaces';
41
+ export { KeyManager, IdentityAlreadyExistsError, IdentityPersistError, } from './crypto/keyManager';
42
+ export type { KeyPair } from './crypto/keyManager';
43
+ export { SignatureService } from './crypto/signatureService';
44
+ export type { SignedMessage, AuthChallenge } from './crypto/signatureService';
45
+ export { RecoveryPhraseService } from './crypto/recoveryPhrase';
46
+ export type { RecoveryPhraseResult } from './crypto/recoveryPhrase';
38
47
  export { DeviceManager } from './utils/deviceManager';
39
48
  export type { DeviceFingerprint, StoredDeviceInfo } from './utils/deviceManager';
49
+ export type { OxyConfig, PrivacySettings, NotificationPreferences, UserPreferences, User, LoginResponse, Notification, Wallet, Transaction, BlockedUser, RestrictedUser, TransferFundsRequest, PurchaseRequest, WithdrawalRequest, TransactionResponse, PaginationInfo, SearchProfilesResponse, KarmaRule, KarmaHistory, KarmaLeaderboardEntry, KarmaAwardRequest, ApiError, PaymentMethod, PaymentRequest, PaymentResponse, AnalyticsData, FollowerDetails, ContentViewer, FileMetadata, FileUploadResponse, FileListResponse, FileUpdateRequest, FileDeleteResponse, RNFileDescriptor, AssetUploadInput, FileVisibility, AssetLink, AssetMetadata, AssetVariant, Asset, AssetInitRequest, AssetInitResponse, AssetCompleteRequest, AssetLinkRequest, AssetUnlinkRequest, AssetUrlResponse, AssetDeleteSummary, AssetUpdateVisibilityRequest, AssetUpdateVisibilityResponse, AccountStorageCategoryUsage, AccountStorageUsageResponse, SecurityEventType, SecurityEventSeverity, SecurityActivity, SecurityActivityResponse, AssetUploadProgress, DeviceSession, DeviceSessionsResponse, DeviceSessionLogoutResponse, UpdateDeviceNameResponse, } from './models/interfaces';
50
+ export { SECURITY_EVENT_SEVERITY_MAP } from './models/interfaces';
51
+ export { TopicType, TopicSource } from './models/Topic';
52
+ export type { TopicData, TopicTranslation } from './models/Topic';
40
53
  export { SUPPORTED_LANGUAGES, getLanguageMetadata, getLanguageName, getNativeLanguageName, normalizeLanguageCode, isRTLLocale, } from './utils/languageUtils';
41
54
  export type { LanguageMetadata } from './utils/languageUtils';
42
55
  export { getPlatformOS, setPlatformOS, isWeb, isNative, isIOS, isAndroid, } from './utils/platform';
@@ -49,18 +62,19 @@ export { DEFAULT_CIRCUIT_BREAKER_CONFIG, createCircuitBreakerState, calculateBac
49
62
  export type { CircuitBreakerState, CircuitBreakerConfig } from './shared/utils/networkUtils';
50
63
  export { isDev, debugLog, debugWarn, debugError, createDebugLogger, } from './shared/utils/debugUtils';
51
64
  export { translate } from './i18n';
52
- export { SessionSyncRequiredError, AuthenticationFailedError, ensureValidToken, isAuthenticationError, withAuthErrorHandling, authenticatedApiCall, } from './utils/authHelpers';
53
- export type { HandleApiErrorOptions } from './utils/authHelpers';
54
- export { mergeSessions, normalizeAndSortSessions, sessionsArraysEqual } from './utils/sessionUtils';
55
- export { packageInfo } from './constants/version';
56
- export * from './utils/apiUtils';
65
+ export { buildSearchParams, buildUrl, buildPaginationParams, safeJsonParse, } from './utils/apiUtils';
66
+ export type { PaginationParams, ApiResponse, ErrorResponse, } from './utils/apiUtils';
57
67
  export { ErrorCodes, createApiError, handleHttpError, validateRequiredFields, } from './utils/errorUtils';
58
68
  export { retryAsync } from './utils/asyncUtils';
59
- export * from './utils/validationUtils';
69
+ export { EMAIL_REGEX, USERNAME_REGEX, PASSWORD_REGEX, isValidEmail, isValidUsername, isValidPassword, isRequiredString, isRequiredNumber, isRequiredBoolean, isValidArray, isValidObject, isValidUUID, isValidURL, isValidDate, isValidFileSize, isValidFileType, sanitizeString, sanitizeHTML, isValidObjectId, validateAndSanitizeUserInput, } from './utils/validationUtils';
60
70
  export { logger, LogLevel, logAuth, logApi, logSession, logUser, logDevice, logPayment, logPerformance, } from './utils/loggerUtils';
61
71
  export type { LogContext } from './utils/loggerUtils';
62
72
  export { updateAvatarVisibility } from './utils/avatarUtils';
63
- export { buildAccountsArray, createQuickAccount, getAccountDisplayName, getAccountFallbackHandle, formatPublicKeyHandle, } from './utils/accountUtils';
73
+ export { buildAccountsArray, createQuickAccount, getAccountDisplayName, getAccountFallbackHandle, formatPublicKeyHandle, mergeAccountsFromRefreshAll, getAccountColor, } from './utils/accountUtils';
64
74
  export type { QuickAccount, DisplayNameUserShape } from './utils/accountUtils';
75
+ export { autoDetectAuthWebUrl } from './utils/fapiAutoDetect';
76
+ export { runColdBoot } from './utils/coldBoot';
77
+ export type { ColdBootStep, ColdBootStepResult, ColdBootSession, ColdBootSkip, ColdBootOutcome, RunColdBootOptions, } from './utils/coldBoot';
78
+ export { packageInfo } from './constants/version';
65
79
  import { OxyServices } from './OxyServices';
66
80
  export default OxyServices;
@@ -25,6 +25,7 @@ export declare function OxyServicesAnalyticsMixin<T extends typeof OxyServicesBa
25
25
  __resetTokensForTests(): void;
26
26
  makeRequest<T_1>(method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE", url: string, data?: any, options?: import("../HttpService").RequestOptions): Promise<T_1>;
27
27
  getBaseURL(): string;
28
+ getSessionBaseUrl(): string;
28
29
  getClient(): import("../HttpService").HttpService;
29
30
  getMetrics(): {
30
31
  totalRequests: number;
@@ -61,6 +61,7 @@ export declare function OxyServicesAppDataMixin<T extends typeof OxyServicesBase
61
61
  __resetTokensForTests(): void;
62
62
  makeRequest<T_1>(method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE", url: string, data?: any, options?: import("../HttpService").RequestOptions): Promise<T_1>;
63
63
  getBaseURL(): string;
64
+ getSessionBaseUrl(): string;
64
65
  getClient(): import("../HttpService").HttpService;
65
66
  getMetrics(): {
66
67
  totalRequests: number;
@@ -98,6 +98,7 @@ export declare function OxyServicesAssetsMixin<T extends typeof OxyServicesBase>
98
98
  __resetTokensForTests(): void;
99
99
  makeRequest<T_1>(method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE", url: string, data?: any, options?: import("../HttpService").RequestOptions): Promise<T_1>;
100
100
  getBaseURL(): string;
101
+ getSessionBaseUrl(): string;
101
102
  getClient(): import("../HttpService").HttpService;
102
103
  getMetrics(): {
103
104
  totalRequests: number;
@@ -131,7 +132,9 @@ export declare function OxyServicesAssetsMixin<T extends typeof OxyServicesBase>
131
132
  withAuthRetry<T_1>(operation: () => Promise<T_1>, operationName: string, options?: {
132
133
  maxRetries?: number;
133
134
  retryDelay?: number;
134
- authTimeoutMs?: number;
135
+ authTimeoutMs? /**
136
+ * Get asset metadata
137
+ */: number;
135
138
  }): Promise<T_1>;
136
139
  validate(): Promise<boolean>;
137
140
  handleError(error: unknown): Error;
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * Supports password-based login (email/username) and public key challenge-response.
5
5
  */
6
- import type { User } from '../models/interfaces';
6
+ import type { User, RefreshAllResponse, RefreshCookieResponse } from '../models/interfaces';
7
7
  import type { SessionLoginResponse } from '../models/session';
8
8
  import type { OxyServicesBase } from '../OxyServices.base';
9
9
  export interface ChallengeResponse {
@@ -234,6 +234,77 @@ export declare function OxyServicesAuthMixin<T extends typeof OxyServicesBase>(B
234
234
  expiresAt: string;
235
235
  user: User;
236
236
  }>;
237
+ /**
238
+ * Refresh every device-local refresh-cookie slot in a single round trip
239
+ * (Google-style multi-account rebuild).
240
+ *
241
+ * Calls `POST {sessionBaseUrl}/auth/refresh-all` with `credentials: 'include'`
242
+ * and NO bearer. The browser attaches every `oxy_rt*` cookie it has; the
243
+ * server rotates each in parallel and returns one entry per VALID account.
244
+ *
245
+ * Failure handling:
246
+ * - 401 → no signed-in accounts on this device → returns `{ accounts: [] }`
247
+ * (NOT an error; this is the cold-boot "not signed in" path).
248
+ * - 404 → server is older than the multi-account endpoint. We fall back to
249
+ * `POST /auth/refresh` (single-slot) and wrap its response in the
250
+ * refresh-all shape so callers can treat the two paths uniformly. The
251
+ * fallback entry has `authuser: 0` (the legacy slot maps to slot 0 by
252
+ * convention) and a minimal `user` shape — consumers needing the full
253
+ * user must fetch it separately. Always exactly one account in this
254
+ * shape.
255
+ * - Any other non-2xx → throws via `handleError`.
256
+ *
257
+ * The refresh cookie itself never enters JS — only the rotated access
258
+ * tokens do. Each access token still needs to be planted via
259
+ * `setTokens(...)` (or per-account in-memory storage) at the consumer.
260
+ */
261
+ refreshAllSessions(): Promise<RefreshAllResponse>;
262
+ /**
263
+ * Rotate a single refresh-cookie slot and return the fresh access token.
264
+ *
265
+ * When `authuser` is provided, the server rotates ONLY that slot
266
+ * (`oxy_rt_${authuser}`) — sibling accounts on the same device stay
267
+ * untouched. When omitted, the server picks the lowest indexed slot
268
+ * present (legacy fallback applies). The refresh cookie itself never
269
+ * enters JS.
270
+ *
271
+ * Returns `null` on 401 (no cookie / expired / reused) so the caller can
272
+ * fall through cleanly to the unauthenticated path.
273
+ */
274
+ refreshTokenViaCookie(opts?: {
275
+ authuser?: number;
276
+ }): Promise<RefreshCookieResponse | null>;
277
+ /**
278
+ * Sign out a single device-local account by its authuser slot index.
279
+ *
280
+ * Revokes that slot's refresh-token family and deactivates its session;
281
+ * sibling indexed slots stay signed in. The browser-side `oxy_rt_${n}`
282
+ * cookie is cleared by the server's `Set-Cookie` response header.
283
+ */
284
+ logoutSessionByAuthuser(authuser: number): Promise<void>;
285
+ /**
286
+ * Sign out EVERY device-local account on this device by clearing every
287
+ * presented refresh-cookie slot at once. Revokes every family + clears
288
+ * every slot. Always succeeds (idempotent on unknown/garbage tokens).
289
+ */
290
+ logoutAllSessionsViaCookie(): Promise<void>;
291
+ /**
292
+ * Internal: raw `POST /auth/refresh[?authuser=N]` call returning the
293
+ * minted access token. Returns `null` on 401 / non-2xx. Used as both the
294
+ * implementation of `refreshTokenViaCookie` and the legacy fallback for
295
+ * `refreshAllSessions` against older servers.
296
+ *
297
+ * @internal
298
+ */
299
+ _refreshCookieRaw(authuser?: number): Promise<RefreshCookieResponse | null>;
300
+ /**
301
+ * Internal: decode (without verifying) the `sessionId` claim from a
302
+ * server-signed access token. The server already verified the signature;
303
+ * the client only reads the claim to drive multi-session state.
304
+ *
305
+ * @internal
306
+ */
307
+ _decodeSessionIdFromAccessToken(token: string): string | null;
237
308
  /**
238
309
  * Get sessions by session ID
239
310
  */
@@ -292,6 +363,7 @@ export declare function OxyServicesAuthMixin<T extends typeof OxyServicesBase>(B
292
363
  __resetTokensForTests(): void;
293
364
  makeRequest<T_1>(method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE", url: string, data?: any, options?: import("../HttpService").RequestOptions): Promise<T_1>;
294
365
  getBaseURL(): string;
366
+ getSessionBaseUrl(): string;
295
367
  getClient(): import("../HttpService").HttpService;
296
368
  getMetrics(): {
297
369
  totalRequests: number;