@oxyhq/core 5.5.0 → 7.0.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 (196) hide show
  1. package/dist/cjs/.tsbuildinfo +1 -1
  2. package/dist/cjs/HttpService.js +7 -4
  3. package/dist/cjs/OxyServices.base.js +7 -102
  4. package/dist/cjs/boot/coldBootV2.js +350 -0
  5. package/dist/cjs/boot/deviceBootReturn.js +152 -0
  6. package/dist/cjs/crypto/keyManager.js +95 -0
  7. package/dist/cjs/i18n/locales/en-US.json +13 -1
  8. package/dist/cjs/i18n/locales/es-ES.json +13 -1
  9. package/dist/cjs/i18n/locales/locales/en-US.json +13 -1
  10. package/dist/cjs/i18n/locales/locales/es-ES.json +13 -1
  11. package/dist/cjs/index.js +54 -45
  12. package/dist/cjs/mixins/OxyServices.accounts.js +13 -26
  13. package/dist/cjs/mixins/OxyServices.auth.js +66 -201
  14. package/dist/cjs/mixins/OxyServices.deviceBoot.js +119 -0
  15. package/dist/cjs/mixins/index.js +8 -17
  16. package/dist/cjs/server/index.js +2 -2
  17. package/dist/cjs/session/SessionClient.js +40 -1
  18. package/dist/cjs/session/authStateStore.js +284 -0
  19. package/dist/cjs/session/refresh.js +264 -0
  20. package/dist/cjs/shared/utils/debugUtils.js +3 -3
  21. package/dist/cjs/utils/accountUtils.js +1 -55
  22. package/dist/cjs/utils/authWebUrl.js +10 -39
  23. package/dist/cjs/utils/coldBoot.js +10 -8
  24. package/dist/cjs/utils/platform.js +19 -0
  25. package/dist/cjs/utils/registrableApex.js +49 -0
  26. package/dist/cjs/utils/ssoBounce.js +15 -362
  27. package/dist/cjs/utils/validationUtils.js +57 -0
  28. package/dist/esm/.tsbuildinfo +1 -1
  29. package/dist/esm/HttpService.js +7 -4
  30. package/dist/esm/OxyServices.base.js +7 -102
  31. package/dist/esm/boot/coldBootV2.js +344 -0
  32. package/dist/esm/boot/deviceBootReturn.js +146 -0
  33. package/dist/esm/crypto/keyManager.js +95 -0
  34. package/dist/esm/i18n/locales/en-US.json +13 -1
  35. package/dist/esm/i18n/locales/es-ES.json +13 -1
  36. package/dist/esm/i18n/locales/locales/en-US.json +13 -1
  37. package/dist/esm/i18n/locales/locales/es-ES.json +13 -1
  38. package/dist/esm/index.js +34 -17
  39. package/dist/esm/mixins/OxyServices.accounts.js +13 -26
  40. package/dist/esm/mixins/OxyServices.auth.js +66 -201
  41. package/dist/esm/mixins/OxyServices.deviceBoot.js +116 -0
  42. package/dist/esm/mixins/index.js +8 -17
  43. package/dist/esm/server/index.js +1 -1
  44. package/dist/esm/session/SessionClient.js +40 -1
  45. package/dist/esm/session/authStateStore.js +278 -0
  46. package/dist/esm/session/refresh.js +257 -0
  47. package/dist/esm/shared/utils/debugUtils.js +3 -3
  48. package/dist/esm/utils/accountUtils.js +0 -53
  49. package/dist/esm/utils/authWebUrl.js +9 -37
  50. package/dist/esm/utils/coldBoot.js +10 -8
  51. package/dist/esm/utils/platform.js +18 -0
  52. package/dist/esm/utils/registrableApex.js +46 -0
  53. package/dist/esm/utils/ssoBounce.js +14 -345
  54. package/dist/esm/utils/validationUtils.js +56 -0
  55. package/dist/types/.tsbuildinfo +1 -1
  56. package/dist/types/HttpService.d.ts +14 -1
  57. package/dist/types/OxyServices.base.d.ts +0 -52
  58. package/dist/types/OxyServices.d.ts +0 -25
  59. package/dist/types/boot/coldBootV2.d.ts +76 -0
  60. package/dist/types/boot/deviceBootReturn.d.ts +83 -0
  61. package/dist/types/crypto/keyManager.d.ts +21 -0
  62. package/dist/types/index.d.ts +14 -21
  63. package/dist/types/mixins/OxyServices.accounts.d.ts +24 -29
  64. package/dist/types/mixins/OxyServices.analytics.d.ts +0 -2
  65. package/dist/types/mixins/OxyServices.appData.d.ts +0 -2
  66. package/dist/types/mixins/OxyServices.assets.d.ts +0 -2
  67. package/dist/types/mixins/OxyServices.auth.d.ts +35 -77
  68. package/dist/types/mixins/OxyServices.civic.d.ts +0 -2
  69. package/dist/types/mixins/OxyServices.connectedApps.d.ts +0 -2
  70. package/dist/types/mixins/OxyServices.contacts.d.ts +0 -2
  71. package/dist/types/mixins/OxyServices.deviceBoot.d.ts +110 -0
  72. package/dist/types/mixins/OxyServices.devices.d.ts +0 -2
  73. package/dist/types/mixins/OxyServices.features.d.ts +0 -2
  74. package/dist/types/mixins/OxyServices.identity.d.ts +0 -2
  75. package/dist/types/mixins/OxyServices.language.d.ts +0 -2
  76. package/dist/types/mixins/OxyServices.links.d.ts +0 -2
  77. package/dist/types/mixins/OxyServices.location.d.ts +0 -2
  78. package/dist/types/mixins/OxyServices.nodes.d.ts +0 -2
  79. package/dist/types/mixins/OxyServices.payment.d.ts +0 -2
  80. package/dist/types/mixins/OxyServices.privacy.d.ts +0 -2
  81. package/dist/types/mixins/OxyServices.reputation.d.ts +0 -2
  82. package/dist/types/mixins/OxyServices.security.d.ts +0 -2
  83. package/dist/types/mixins/OxyServices.topics.d.ts +0 -2
  84. package/dist/types/mixins/OxyServices.user.d.ts +0 -2
  85. package/dist/types/mixins/OxyServices.utility.d.ts +0 -2
  86. package/dist/types/mixins/index.d.ts +5 -9
  87. package/dist/types/models/interfaces.d.ts +0 -67
  88. package/dist/types/models/session.d.ts +4 -5
  89. package/dist/types/server/index.d.ts +1 -1
  90. package/dist/types/session/SessionClient.d.ts +26 -1
  91. package/dist/types/session/authStateStore.d.ts +119 -0
  92. package/dist/types/session/refresh.d.ts +93 -0
  93. package/dist/types/shared/utils/debugUtils.d.ts +3 -3
  94. package/dist/types/utils/accountUtils.d.ts +2 -17
  95. package/dist/types/utils/authWebUrl.d.ts +9 -35
  96. package/dist/types/utils/coldBoot.d.ts +17 -14
  97. package/dist/types/utils/platform.d.ts +14 -0
  98. package/dist/types/utils/registrableApex.d.ts +31 -0
  99. package/dist/types/utils/ssoBounce.d.ts +14 -280
  100. package/dist/types/utils/validationUtils.d.ts +15 -0
  101. package/package.json +2 -2
  102. package/src/HttpService.ts +20 -4
  103. package/src/OxyServices.base.ts +7 -112
  104. package/src/OxyServices.ts +0 -38
  105. package/src/boot/__tests__/coldBootV2.test.ts +317 -0
  106. package/src/boot/__tests__/deviceBootReturn.test.ts +158 -0
  107. package/src/boot/coldBootV2.ts +426 -0
  108. package/src/boot/deviceBootReturn.ts +195 -0
  109. package/src/crypto/__tests__/sharedDeviceToken.test.ts +24 -0
  110. package/src/crypto/keyManager.ts +101 -0
  111. package/src/i18n/locales/en-US.json +13 -1
  112. package/src/i18n/locales/es-ES.json +13 -1
  113. package/src/index.ts +75 -65
  114. package/src/mixins/OxyServices.accounts.ts +27 -39
  115. package/src/mixins/OxyServices.auth.ts +78 -253
  116. package/src/mixins/OxyServices.deviceBoot.ts +146 -0
  117. package/src/mixins/__tests__/OxyServices.deviceBoot.test.ts +107 -0
  118. package/src/mixins/__tests__/accounts.test.ts +17 -44
  119. package/src/mixins/__tests__/passwordSignIn.test.ts +91 -0
  120. package/src/mixins/index.ts +10 -23
  121. package/src/models/interfaces.ts +0 -79
  122. package/src/models/session.ts +4 -5
  123. package/src/server/index.ts +1 -1
  124. package/src/session/SessionClient.ts +53 -2
  125. package/src/session/__tests__/SessionClient.additive.test.ts +92 -0
  126. package/src/session/__tests__/SessionClient.rest.test.ts +25 -0
  127. package/src/session/__tests__/SessionClient.state.test.ts +18 -5
  128. package/src/session/__tests__/authStateStore.test.ts +209 -0
  129. package/src/session/__tests__/refresh.test.ts +256 -0
  130. package/src/session/authStateStore.ts +335 -0
  131. package/src/session/refresh.ts +334 -0
  132. package/src/shared/utils/debugUtils.ts +3 -3
  133. package/src/utils/__tests__/authWebUrl.test.ts +5 -40
  134. package/src/utils/__tests__/registrableApex.test.ts +62 -0
  135. package/src/utils/__tests__/validationUtils.test.ts +30 -0
  136. package/src/utils/accountUtils.ts +2 -65
  137. package/src/utils/authWebUrl.ts +9 -39
  138. package/src/utils/coldBoot.ts +17 -14
  139. package/src/utils/platform.ts +21 -0
  140. package/src/utils/registrableApex.ts +45 -0
  141. package/src/utils/ssoBounce.ts +14 -393
  142. package/src/utils/validationUtils.ts +62 -0
  143. package/dist/cjs/AuthManager.js +0 -1110
  144. package/dist/cjs/AuthManagerTypes.js +0 -13
  145. package/dist/cjs/CrossDomainAuth.js +0 -206
  146. package/dist/cjs/mixins/OxyServices.fedcm.js +0 -823
  147. package/dist/cjs/mixins/OxyServices.redirect.js +0 -95
  148. package/dist/cjs/mixins/OxyServices.silent.js +0 -204
  149. package/dist/cjs/mixins/OxyServices.sso.js +0 -208
  150. package/dist/cjs/utils/fapiAutoDetect.js +0 -99
  151. package/dist/cjs/utils/ssoEstablish.js +0 -110
  152. package/dist/cjs/utils/ssoReturn.js +0 -275
  153. package/dist/esm/AuthManager.js +0 -1105
  154. package/dist/esm/AuthManagerTypes.js +0 -12
  155. package/dist/esm/CrossDomainAuth.js +0 -201
  156. package/dist/esm/mixins/OxyServices.fedcm.js +0 -821
  157. package/dist/esm/mixins/OxyServices.redirect.js +0 -92
  158. package/dist/esm/mixins/OxyServices.silent.js +0 -202
  159. package/dist/esm/mixins/OxyServices.sso.js +0 -204
  160. package/dist/esm/utils/fapiAutoDetect.js +0 -95
  161. package/dist/esm/utils/ssoEstablish.js +0 -107
  162. package/dist/esm/utils/ssoReturn.js +0 -271
  163. package/dist/types/AuthManager.d.ts +0 -380
  164. package/dist/types/AuthManagerTypes.d.ts +0 -81
  165. package/dist/types/CrossDomainAuth.d.ts +0 -164
  166. package/dist/types/mixins/OxyServices.fedcm.d.ts +0 -331
  167. package/dist/types/mixins/OxyServices.redirect.d.ts +0 -92
  168. package/dist/types/mixins/OxyServices.silent.d.ts +0 -132
  169. package/dist/types/mixins/OxyServices.sso.d.ts +0 -138
  170. package/dist/types/utils/fapiAutoDetect.d.ts +0 -56
  171. package/dist/types/utils/ssoEstablish.d.ts +0 -85
  172. package/dist/types/utils/ssoReturn.d.ts +0 -156
  173. package/src/AuthManager.ts +0 -1269
  174. package/src/AuthManagerTypes.ts +0 -86
  175. package/src/CrossDomainAuth.ts +0 -243
  176. package/src/__tests__/authManager.cookiePath.test.ts +0 -390
  177. package/src/__tests__/authManager.security.test.ts +0 -377
  178. package/src/__tests__/crossDomainAuth.test.ts +0 -116
  179. package/src/__tests__/establishDeviceRefreshSlot.test.ts +0 -221
  180. package/src/mixins/OxyServices.fedcm.ts +0 -1026
  181. package/src/mixins/OxyServices.redirect.ts +0 -122
  182. package/src/mixins/OxyServices.silent.ts +0 -272
  183. package/src/mixins/OxyServices.sso.ts +0 -261
  184. package/src/mixins/__tests__/constructorAuthWebUrl.test.ts +0 -85
  185. package/src/mixins/__tests__/fedcm.test.ts +0 -667
  186. package/src/mixins/__tests__/sessionBaseUrl.test.ts +0 -61
  187. package/src/mixins/__tests__/silent.test.ts +0 -102
  188. package/src/mixins/__tests__/sso.test.ts +0 -228
  189. package/src/utils/__tests__/consumeSsoReturn.test.ts +0 -816
  190. package/src/utils/__tests__/fapiAutoDetect.test.ts +0 -183
  191. package/src/utils/__tests__/ssoBounce.test.ts +0 -219
  192. package/src/utils/__tests__/ssoEstablish.test.ts +0 -204
  193. package/src/utils/__tests__/ssoReturn.test.ts +0 -276
  194. package/src/utils/fapiAutoDetect.ts +0 -91
  195. package/src/utils/ssoEstablish.ts +0 -174
  196. package/src/utils/ssoReturn.ts +0 -389
@@ -2,20 +2,6 @@ import type { UserNameResponse } from '@oxyhq/contracts';
2
2
  export interface OxyConfig {
3
3
  baseURL: string;
4
4
  cloudURL?: string;
5
- /**
6
- * Base URL the SDK's first-party session/refresh calls target.
7
- *
8
- * Per the 2026 session architecture (docs/SESSION-ARCHITECTURE.md), every app
9
- * keeps its OWN first-party session on its OWN domain. For non-`oxy.so` apps
10
- * this is the app's own same-site backend (e.g. `https://api.mention.earth`),
11
- * whose session bridge forwards the user's refresh credential to
12
- * `api.oxy.so`. For `*.oxy.so` apps this is omitted and falls back to
13
- * `baseURL` (`https://api.oxy.so`), so their behavior is unchanged.
14
- *
15
- * Resolve via {@link OxyServices.getSessionBaseUrl}; when unset it returns
16
- * `baseURL`. This is purely additive — no refresh/auth logic reads it yet.
17
- */
18
- sessionBaseUrl?: string;
19
5
  authWebUrl?: string;
20
6
  authRedirectUri?: string;
21
7
  /**
@@ -110,7 +96,6 @@ export interface User {
110
96
  phone?: string;
111
97
  address?: string;
112
98
  birthday?: string;
113
- location?: string;
114
99
  website?: string;
115
100
  createdAt?: string;
116
101
  updatedAt?: string;
@@ -584,55 +569,3 @@ export interface UpdateDeviceNameResponse {
584
569
  message: string;
585
570
  deviceName: string;
586
571
  }
587
- /**
588
- * Minimal user shape included in a `RefreshAllAccount` entry. The server
589
- * projects a small whitelist (`username name avatar email color`) so the
590
- * client can render the account chooser without an extra `/users/me` round
591
- * trip per account.
592
- *
593
- * `avatar` and `color` are `string | null` because they are stored as nullable
594
- * fields in the user document.
595
- */
596
- export interface RefreshAllAccountUser {
597
- id: string;
598
- username: string;
599
- /**
600
- * Structured human name as emitted by `formatUserResponse` (the canonical
601
- * {@link UserNameResponse} `{ first?, last?, full? }` subdocument), NOT a bare
602
- * string. The server projects `name` verbatim from the user document. The
603
- * single source of truth is `@oxyhq/contracts`.
604
- */
605
- name: UserNameResponse;
606
- avatar?: string | null;
607
- email?: string;
608
- color?: string | null;
609
- }
610
- /**
611
- * One rotated account entry returned by `POST /auth/refresh-all`. `authuser` is
612
- * the device-local slot index (0..N-1) the cookie was bound to.
613
- */
614
- export interface RefreshAllAccount {
615
- authuser: number;
616
- accessToken: string;
617
- expiresAt: string;
618
- sessionId: string;
619
- user: RefreshAllAccountUser | null;
620
- }
621
- /**
622
- * Wire shape of `POST /auth/refresh-all`. Always 200 with a (possibly empty)
623
- * accounts array — 401 means "no accounts signed in on this device" and is
624
- * normalised to `{ accounts: [] }` at the SDK layer.
625
- */
626
- export interface RefreshAllResponse {
627
- accounts: RefreshAllAccount[];
628
- }
629
- /**
630
- * Wire shape of `POST /auth/refresh` (single-slot refresh, optionally targeting
631
- * a specific `?authuser=N` slot). The server always includes the numeric slot in
632
- * the response.
633
- */
634
- export interface RefreshCookieResponse {
635
- accessToken: string;
636
- expiresAt: string;
637
- authuser: number;
638
- }
@@ -7,11 +7,10 @@ export interface ClientSession {
7
7
  userId?: string;
8
8
  isCurrent?: boolean;
9
9
  /**
10
- * Web-only: the device-local refresh-cookie slot index (0..N) that backs
11
- * this session. Populated from `POST /auth/refresh-all` and from login /
12
- * signup / fedcm-exchange responses. Required for per-session web token
13
- * refresh via `refreshTokenViaCookie({ authuser })` without a bearer token.
14
- * Absent on native (RN uses the bearer-protected session id directly).
10
+ * The account's ordinal slot (0..N) within the device's account set
11
+ * (`SessionAccount.authuser` in `@oxyhq/contracts`), projected from the
12
+ * device-first `DeviceSessionState` used purely for stable Google-style
13
+ * account-chooser ordering, not for any token-refresh mechanism.
15
14
  */
16
15
  authuser?: number;
17
16
  }
@@ -23,5 +23,5 @@ export type { SafeFetchOptions, SafeFetchResult, SsrfCheckFail, SsrfCheckOk, Ssr
23
23
  export { createOxyCors } from './cors';
24
24
  export type { OxyCorsOptions } from './cors';
25
25
  export { verifySecret } from './verifySecret';
26
- export { registrableApex } from '../utils/fapiAutoDetect';
26
+ export { registrableApex } from '../utils/registrableApex';
27
27
  export { SSO_CALLBACK_PATH } from '../utils/ssoBounce';
@@ -1,7 +1,7 @@
1
1
  import { type DeviceSessionState } from '@oxyhq/contracts';
2
2
  import type { MinimalSocket, SocketIOFactory } from './socketLoader';
3
3
  export interface TokenTransport {
4
- /** Ensure this app holds a per-domain access token for state.activeAccountId (mint via FedCM/silent/sso/keychain). Best-effort. */
4
+ /** Ensure this app holds a per-domain access token for state.activeAccountId (mint via the persisted refresh family / shared keychain). Best-effort. */
5
5
  ensureActiveToken(state: DeviceSessionState): Promise<void>;
6
6
  }
7
7
  export interface SessionClientHost {
@@ -16,6 +16,16 @@ export interface SessionClientHost {
16
16
  }
17
17
  export interface SessionClientOptions {
18
18
  transport?: TokenTransport;
19
+ /**
20
+ * Invoked when an APPLIED state has zero accounts — i.e. a device
21
+ * signout-all removed the last account from this device set. Providers use
22
+ * this to clear the persisted {@link AuthStateStore} so a reload does not
23
+ * try to restore a session that no longer exists on the device.
24
+ *
25
+ * Only fires when a state is actually applied (revision advanced), never on
26
+ * a stale/rejected push. Exceptions thrown by the callback are isolated.
27
+ */
28
+ onUnauthenticated?: () => void;
19
29
  /**
20
30
  * Statically-injected `socket.io-client` factory (its `io` export).
21
31
  * `@oxyhq/services` and `@oxyhq/auth` list `socket.io-client` as a real
@@ -60,6 +70,21 @@ export declare class SessionClient {
60
70
  all: true;
61
71
  }): Promise<void>;
62
72
  addCurrentAccount(): Promise<void>;
73
+ /**
74
+ * Register the just-signed-in account into the device set AND make it the
75
+ * ACTIVE account — the explicit user-intent activation a sign-in UI performs.
76
+ *
77
+ * `addCurrentAccount` alone honors the server's `activate:'if-empty'` policy
78
+ * (a new account does NOT steal focus from an already-active one), which is
79
+ * correct for a background/silent add but wrong right after a deliberate
80
+ * sign-in. This adds, then switches to the target so the UI lands on the
81
+ * account the user just authenticated.
82
+ *
83
+ * @param accountId - The signed-in account id (e.g. `session.user.id`). When
84
+ * omitted, falls back to the host's current-account ref. If neither
85
+ * resolves, the add still applies and no switch is performed.
86
+ */
87
+ registerAndActivate(accountId?: string): Promise<void>;
63
88
  start(): Promise<void>;
64
89
  stop(): void;
65
90
  private connectSocket;
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Persisted auth state — ONE shape, web + native.
3
+ *
4
+ * The device-first session model (auth-centralization wave 1) persists the
5
+ * rotating refresh-token family head per ORIGIN so a reload restores the
6
+ * session locally without a redirect. This module is the storage seam: a tiny
7
+ * `load / save / clear` interface plus platform factories, so the cold boot
8
+ * (`coldBootV2`) and the unified refresh handler (`refresh.ts`) never touch a
9
+ * platform storage API directly.
10
+ *
11
+ * Platform-agnostic — the native factory takes an INJECTED key/value store
12
+ * (`@oxyhq/services` passes a SecureStore-backed adapter) so `@oxyhq/core`
13
+ * never imports `expo-secure-store`. The web factory is self-contained
14
+ * (`localStorage`) and degrades to in-memory when storage is unavailable
15
+ * (sandboxed iframe `SecurityError`, private-mode quota, SSR).
16
+ *
17
+ * ESM-safe (no `require()`).
18
+ */
19
+ /**
20
+ * The persisted session credential set for a single origin.
21
+ *
22
+ * `refreshToken` is the rotating single-use family head; `sessionId` + `userId`
23
+ * identify the owning device session and account. `deviceToken` is the opaque,
24
+ * add-only device attribution token (mirrored to the shared keychain on native
25
+ * so every Oxy app on one phone shares one DeviceSession).
26
+ *
27
+ * `accessToken` + `expiresAt` are OPTIONAL warm-boot fields. Persisting them
28
+ * lets the cold boot plant a still-valid access token on the very first paint
29
+ * WITHOUT a blocking `/auth/refresh-token` round-trip — the proactive scheduler
30
+ * then rotates it in the background. They are a strict optimization: the store
31
+ * is fully functional (via `refreshToken`) when they are absent or stale, and
32
+ * the access token is short-lived, so persisting it adds no exposure the
33
+ * already-persisted refresh token does not (see the plan's XSS risk note — the
34
+ * refresh token is the dominant secret either way).
35
+ */
36
+ export interface PersistedAuthState {
37
+ sessionId: string;
38
+ refreshToken: string;
39
+ userId: string;
40
+ deviceToken?: string;
41
+ /** Optional warm-boot access token (short-lived; see interface docs). */
42
+ accessToken?: string;
43
+ /** Optional warm-boot access-token expiry, ISO-8601. */
44
+ expiresAt?: string;
45
+ }
46
+ /**
47
+ * The storage seam consumed by the cold boot and the refresh handler. Async
48
+ * throughout so one interface fits both synchronous web `localStorage` and
49
+ * asynchronous native SecureStore/AsyncStorage.
50
+ *
51
+ * Two lifetimes:
52
+ * - The SESSION credential blob (`load`/`save`/`clear`) is per-sign-in and is
53
+ * wiped on `clear()` (sign-out).
54
+ * - The DEVICE token (`loadDeviceToken`/`saveDeviceToken`/`clearDeviceToken`)
55
+ * is long-lived device attribution that SURVIVES `clear()`: a signed-out
56
+ * browser is still the same device, and a later in-app (cross-apex,
57
+ * cookie-less) login sends this token so the new session joins the SAME
58
+ * server-side DeviceSession. Only an explicit device signout-all
59
+ * (`clearDeviceToken`) removes it.
60
+ */
61
+ export interface AuthStateStore {
62
+ load(): Promise<PersistedAuthState | null>;
63
+ save(state: PersistedAuthState): Promise<void>;
64
+ clear(): Promise<void>;
65
+ loadDeviceToken(): Promise<string | null>;
66
+ saveDeviceToken(token: string): Promise<void>;
67
+ clearDeviceToken(): Promise<void>;
68
+ }
69
+ /**
70
+ * The minimal async key/value surface a native store must provide. Matches
71
+ * both `expo-secure-store` (wrapped) and `@react-native-async-storage`.
72
+ */
73
+ export interface NativeKeyValueStorage {
74
+ getItem(key: string): Promise<string | null>;
75
+ setItem(key: string, value: string): Promise<void>;
76
+ removeItem(key: string): Promise<void>;
77
+ }
78
+ /**
79
+ * Versioned storage key. The `.v1` suffix lets a future shape change ship a
80
+ * `.v2` key without reading a stale/incompatible `.v1` blob. Distinct from the
81
+ * `oxy_shared_*` keychain keys in `KeyManager`, so it never collides.
82
+ */
83
+ export declare const AUTH_STATE_STORAGE_KEY = "oxy.auth.v1";
84
+ /**
85
+ * Storage key for the long-lived device-attribution token. Separate from
86
+ * {@link AUTH_STATE_STORAGE_KEY} because it must OUTLIVE a session `clear()`
87
+ * (sign-out) — the device is unchanged across sign-ins.
88
+ */
89
+ export declare const DEVICE_TOKEN_STORAGE_KEY = "oxy.device.v1";
90
+ /**
91
+ * A process-lifetime, in-memory {@link AuthStateStore}. Used directly for
92
+ * tests/SSR and as the degraded fallback of the web store when `localStorage`
93
+ * is unreachable. Not durable across reloads — that is acceptable for the
94
+ * fallback because the alternative (throwing) would break cold boot entirely.
95
+ */
96
+ export declare function createMemoryAuthStateStore(): AuthStateStore;
97
+ /**
98
+ * A `localStorage`-backed {@link AuthStateStore} under the versioned
99
+ * {@link AUTH_STATE_STORAGE_KEY}.
100
+ *
101
+ * Resilience:
102
+ * - If `localStorage` is unreachable (sandboxed-iframe `SecurityError`, SSR),
103
+ * the whole store degrades to an in-memory {@link createMemoryAuthStateStore}
104
+ * for this page's lifetime — never throws on construction.
105
+ * - Individual `getItem`/`setItem`/`removeItem` are each wrapped: a read that
106
+ * throws yields `null`; a write that throws (quota, private mode) is
107
+ * swallowed. The persisted-refresh lane treats a failed persist as "no
108
+ * durable state" (falls back to the bootstrap hop) rather than crashing.
109
+ */
110
+ export declare function createWebAuthStateStore(): AuthStateStore;
111
+ /**
112
+ * A native {@link AuthStateStore} over an injected async key/value store.
113
+ *
114
+ * `@oxyhq/core` never imports `expo-secure-store`; `@oxyhq/services` constructs
115
+ * the SecureStore-backed adapter and passes it here. Every operation is wrapped
116
+ * so a storage exception degrades gracefully (read → `null`, write → swallowed)
117
+ * exactly like the web store.
118
+ */
119
+ export declare function createNativeAuthStateStore(storage: NativeKeyValueStorage): AuthStateStore;
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Unified token refresh — THE single refresh implementation for web + native.
3
+ *
4
+ * Before device-first, refresh was duplicated: `@oxyhq/auth`'s
5
+ * `session/tokenRefresh.ts` (per-apex `/auth/silent` iframe) and
6
+ * `@oxyhq/services`'s `inSessionTokenRefresh.ts` (native shared-key). This
7
+ * module replaces both with ONE persisted-refresh-token rotation shared by
8
+ * every consumer:
9
+ *
10
+ * - `refreshPersistedSession` — arm 1 rotates the stored refresh-token family
11
+ * (`POST /auth/refresh-token`), planting + persisting the rotated pair; arm 2
12
+ * (native only) re-mints via the shared-keychain identity when there is no
13
+ * live refresh token. It is used BOTH reactively (wrapped as the
14
+ * `AuthRefreshHandler` installed on `HttpService`) AND proactively (the
15
+ * cold-boot `stored-tokens` step calls it directly).
16
+ * - `createAuthRefreshHandler` / `installAuthRefreshHandler` wire arm 1+2 into
17
+ * `HttpService.setAuthRefreshHandler`, keeping that layer's single-flight
18
+ * dedup + cooldown (this module does NOT reimplement them).
19
+ * - `startTokenRefreshScheduler` — a proactive scheduler (lifted from the
20
+ * better of the two prior duplicates, `@oxyhq/auth`'s `tokenRefresh.ts`),
21
+ * decoupled from any React / auth-sdk type: refreshes ~60s before `exp`,
22
+ * re-arms on token change + web tab-focus, `.unref?.()`s its timer in Node.
23
+ *
24
+ * Framework-free; no module-level mutable state.
25
+ */
26
+ import type { OxyServices } from '../OxyServices';
27
+ import type { AuthRefreshHandler } from '../HttpService';
28
+ import type { AuthStateStore } from './authStateStore';
29
+ /**
30
+ * Lead time (ms) before access-token expiry at which the proactive scheduler
31
+ * refreshes. Mirrors `HttpService`'s per-request `TOKEN_REFRESH_LEAD_SECONDS`
32
+ * (60s) so the scheduled refresh and the request-time preflight refresh use
33
+ * the same window — the scheduler just fires it during idle/background.
34
+ */
35
+ export declare const TOKEN_REFRESH_LEAD_MS = 60000;
36
+ export interface RefreshDeps {
37
+ oxy: OxyServices;
38
+ store: AuthStateStore;
39
+ /**
40
+ * Whether to fall back to the native shared-keychain re-mint (arm 2) when
41
+ * there is no live refresh token / arm 1 is revoked. Defaults to `isNative()`
42
+ * — web has no shared keychain. Exposed for tests.
43
+ */
44
+ allowSharedKeyFallback?: boolean;
45
+ }
46
+ /**
47
+ * Rotate the persisted session and return the fresh access token, or `null`
48
+ * when no arm could produce one.
49
+ *
50
+ * Arm 1 (`POST /auth/refresh-token`): if the store holds a refresh token, rotate
51
+ * it — on success plant + persist the rotated pair; on a REVOKED error clear the
52
+ * store; on a transient error leave the store and return `null`.
53
+ *
54
+ * Arm 2 (native shared-keychain): when there is no refresh token or arm 1 was
55
+ * revoked, re-mint via `signInWithSharedIdentity` (which plants tokens). The
56
+ * shared keychain — not the per-origin store — is the durable native credential,
57
+ * so this arm does not write the store.
58
+ */
59
+ export declare function refreshPersistedSession(deps: RefreshDeps): Promise<string | null>;
60
+ /**
61
+ * Build the reactive `AuthRefreshHandler` (arm 1 + arm 2). Install it via
62
+ * {@link installAuthRefreshHandler} or directly on
63
+ * `oxy.httpService.setAuthRefreshHandler`. `HttpService` owns single-flight
64
+ * dedup + cooldown, so the timer, the request-time preflight, and a 401 all
65
+ * collapse to one network attempt.
66
+ */
67
+ export declare function createAuthRefreshHandler(deps: RefreshDeps): AuthRefreshHandler;
68
+ /**
69
+ * Install the unified refresh handler on the owner client's `HttpService`.
70
+ * Returns a disposer that removes it.
71
+ */
72
+ export declare function installAuthRefreshHandler(deps: RefreshDeps): () => void;
73
+ /** Handle returned by {@link startTokenRefreshScheduler}; `dispose()` tears it down. */
74
+ export interface TokenRefreshSchedulerHandle {
75
+ dispose(): void;
76
+ }
77
+ /**
78
+ * Start the proactive refresh scheduler against `oxy`.
79
+ *
80
+ * Schedules a single timer to fire {@link TOKEN_REFRESH_LEAD_MS} before the
81
+ * current access token's `exp`, calling
82
+ * `oxy.httpService.refreshAccessToken('preflight')` (which runs the installed
83
+ * handler; deduped + cooldown-guarded). After every attempt it reschedules
84
+ * from the possibly-rotated token. It also reschedules whenever the token
85
+ * changes (a sign-out that clears the token cancels the timer) and, on web
86
+ * tab-focus, refreshes immediately if already inside the lead window (a
87
+ * long-hidden tab throttles timers, so the token can be expired on return).
88
+ *
89
+ * No-ops cleanly when there is no token or an opaque/no-`exp` token — the
90
+ * reactive 401 path stays the only refresh trigger in that case. The timer is
91
+ * `.unref?.()`-ed so it never keeps a Node/Jest event loop alive.
92
+ */
93
+ export declare function startTokenRefreshScheduler(oxy: OxyServices): TokenRefreshSchedulerHandle;
@@ -12,7 +12,7 @@
12
12
  export declare const isDev: () => boolean;
13
13
  /**
14
14
  * Log a debug message (only in development)
15
- * @param prefix - Log prefix (e.g., '[FedCM]')
15
+ * @param prefix - Log prefix (e.g., '[ColdBoot]')
16
16
  * @param args - Arguments to log
17
17
  */
18
18
  export declare const debugLog: (prefix: string, ...args: unknown[]) => void;
@@ -30,12 +30,12 @@ export declare const debugWarn: (prefix: string, ...args: unknown[]) => void;
30
30
  export declare const debugError: (prefix: string, ...args: unknown[]) => void;
31
31
  /**
32
32
  * Create a namespaced debug logger
33
- * @param namespace - Logger namespace (e.g., 'FedCM', 'SilentAuth')
33
+ * @param namespace - Logger namespace (e.g., 'ColdBoot', 'DeviceAuth')
34
34
  * @returns Object with log, warn, error methods
35
35
  *
36
36
  * @example
37
37
  * ```ts
38
- * const debug = createDebugLogger('FedCM');
38
+ * const debug = createDebugLogger('ColdBoot');
39
39
  * debug.log('Starting authentication');
40
40
  * debug.warn('Token expires soon');
41
41
  * debug.error('Authentication failed', error);
@@ -2,7 +2,6 @@
2
2
  * Shared account types and pure helper functions.
3
3
  * Used by both @oxyhq/services (React Native) and @oxyhq/auth (Web) account stores.
4
4
  */
5
- import type { RefreshAllAccount } from '../models/interfaces';
6
5
  export interface QuickAccount {
7
6
  sessionId: string;
8
7
  userId?: string;
@@ -12,9 +11,8 @@ export interface QuickAccount {
12
11
  avatarUrl?: string;
13
12
  /**
14
13
  * Device-local account slot index, 0..N-1 (Google-style multi-account).
15
- * Mirrors the server's `oxy_rt_${authuser}` cookie slot. Optional so that
16
- * pre-multi-account QuickAccounts (sessionId-only, non-cookie auth on RN)
17
- * remain valid; web flows always populate it after `refreshAllSessions`.
14
+ * Optional so that pre-multi-account QuickAccounts (sessionId-only) remain
15
+ * valid; the device session set populates it where available.
18
16
  */
19
17
  authuser?: number;
20
18
  /**
@@ -94,19 +92,6 @@ export declare const createQuickAccount: (sessionId: string, userData: {
94
92
  } | string;
95
93
  avatar?: string | null;
96
94
  }, existingAccount?: QuickAccount, getFileDownloadUrl?: (fileId: string, variant: string) => string) => QuickAccount;
97
- /**
98
- * Merge a fresh `/auth/refresh-all` snapshot into an existing QuickAccount
99
- * list, preserving any cached fields (`avatarUrl`) for slots that didn't
100
- * change. The fresh response is canonical: the resulting list contains EXACTLY
101
- * the slots present in `fresh`, sorted by `authuser` ascending. Stale stored
102
- * accounts that no longer appear in `fresh` are dropped (the server already
103
- * authoritatively cleared the corresponding cookie).
104
- *
105
- * @param stored Previously persisted QuickAccount list (any order).
106
- * @param fresh Server's authoritative refresh-all response.
107
- * @returns Canonical merged list, sorted by `authuser` asc.
108
- */
109
- export declare const mergeAccountsFromRefreshAll: (stored: QuickAccount[] | undefined, fresh: RefreshAllAccount[]) => QuickAccount[];
110
95
  /**
111
96
  * Return the account's preferred Bloom color preset, or `null` if it has no
112
97
  * preference. Centralises the `color ?? null` normalisation so consumers can
@@ -1,42 +1,16 @@
1
1
  /**
2
- * Central IdP (auth web) URL resolution for cross-domain SSO.
2
+ * Central IdP apex constant.
3
3
  *
4
- * The Oxy ecosystem runs a single, central Identity Provider at
5
- * `auth.oxy.so`. For TRUE central cross-domain SSO (Google/Meta/Clerk style),
6
- * FedCM and the opaque-code SSO bounce always target this one origin it owns
7
- * the host-only `fedcm_session` cookie and the central session store reachable
8
- * via `api.oxy.so`. Relying Parties (mention.earth, homiio.com, alia.onl, …)
9
- * delegate to it rather than standing up a per-apex IdP.
10
- *
11
- * This module is intentionally pure: it performs no DOM access, reads no
12
- * `window`/`location`, and has no side effects. It is the single source of
13
- * truth for the central IdP origin so call sites never hardcode the literal.
14
- *
15
- * Note: this is distinct from `autoDetectAuthWebUrl` (per-apex `auth.<rp-apex>`
16
- * derivation). The central-SSO path deliberately does NOT auto-detect per-apex
17
- * IdPs — it is central only. An explicitly-configured `authWebUrl` still wins.
4
+ * The client SSO/FedCM resolvers (`resolveCentralAuthUrl`, `CENTRAL_AUTH_URL`)
5
+ * were removed in the device-first / legacy-final cutovers. The lone survivor is
6
+ * `CENTRAL_IDP_APEX`, kept because it has a LIVE consumer
7
+ * `@oxyhq/core/server`'s CORS helper (`server/cors.ts`'s `createOxyCors`)
8
+ * auto-allows `*.oxy.so` from it. That CORS use is permanent, so this stays
9
+ * past the SSO/FedCM teardown.
18
10
  */
19
11
  /**
20
12
  * The registrable apex (eTLD+1) of the Oxy ecosystem's central Identity
21
- * Provider. The central IdP is reachable at `auth.${CENTRAL_IDP_APEX}` and the
22
- * ID-token assertion issuer is always `https://auth.${CENTRAL_IDP_APEX}`
23
- * regardless of which per-apex `auth.<rp>` host served a given request.
24
- *
25
- * Kept as a standalone constant so the IdP worker and the SDK derive the same
26
- * literal from one source of truth (the worker imports it to brand assertions).
13
+ * Provider, reachable at `auth.${CENTRAL_IDP_APEX}`. Single source of truth so
14
+ * the CORS helper (and anything else that needs the central apex) never drifts.
27
15
  */
28
16
  export declare const CENTRAL_IDP_APEX = "oxy.so";
29
- /**
30
- * The canonical central Identity Provider origin for the Oxy ecosystem.
31
- * No trailing slash. Derived from {@link CENTRAL_IDP_APEX} so the apex and the
32
- * full origin never drift apart.
33
- */
34
- export declare const CENTRAL_AUTH_URL = "https://auth.oxy.so";
35
- /**
36
- * Resolve the central IdP origin, honouring an explicit override.
37
- *
38
- * @param explicit - A caller-supplied auth web URL, or `undefined`/empty to use
39
- * the central default. An explicit non-empty value always wins.
40
- * @returns The explicit value when provided, otherwise {@link CENTRAL_AUTH_URL}.
41
- */
42
- export declare function resolveCentralAuthUrl(explicit?: string): string;
@@ -3,19 +3,21 @@
3
3
  * authentication resolution.
4
4
  *
5
5
  * On a fresh page load / app launch the SDK may have several ways to recover an
6
- * existing session (silent FedCM, a persisted refresh token, a cross-domain
7
- * claim, a redirect SSO return, ...). They must be attempted in a deterministic
8
- * order*, and the FIRST one that yields a session wins — every later step is
9
- * skipped. This module encodes exactly that contract and nothing else.
6
+ * existing session (a persisted refresh-token family, a shared-keychain
7
+ * identity, a cross-domain boot-fragment return, ...). They must be attempted
8
+ * in a deterministic order, and the FIRST one that yields a session wins —
9
+ * every later step is skipped. This module encodes exactly that contract and
10
+ * nothing else.
10
11
  *
11
12
  * Design constraints (all enforced):
12
13
  * - PURE: no DOM, no `navigator`, no `window`, no React, no platform globals.
13
14
  * - NO module-level mutable state. Every call to {@link runColdBoot} is fully
14
15
  * self-contained, so it is safe under bundler re-evaluation (e.g. the Metro
15
- * web bundle, which is precisely why the FedCM silent-SSO guard had to live
16
- * in consumers rather than a core singleton).
17
- * - Architecture-agnostic: both candidate cross-domain SSO designs consume
18
- * this same primitive; it knows nothing about HOW a step resolves a session.
16
+ * web bundle the reason any run-once guard for a step must live in the
17
+ * calling consumer, never in a core module-level singleton).
18
+ * - Architecture-agnostic: it knows nothing about HOW a step resolves a
19
+ * session; `runSessionColdBoot` (`boot/coldBootV2.ts`) is the current
20
+ * device-first consumer.
19
21
  *
20
22
  * A step is skipped (without running) when its `enabled` predicate returns
21
23
  * false. Any thrown error — from either `enabled` or `run` — is reported via
@@ -92,15 +94,16 @@ export interface RunColdBootOptions<S> {
92
94
  * fails to settle before the deadline, the runner abandons the await for that
93
95
  * step (reporting it via `onStepDeadline`) and CONTINUES to the next step,
94
96
  * each now racing against an already-expired deadline. This is deliberate:
95
- * the runner keeps iterating so the TERMINAL step (e.g. the `/sso` bounce,
96
- * whose `run()` performs its side effect synchronously before its first
97
- * `await`) still gets to fire. A step that has nothing to contribute after
98
- * the deadline simply doesn't settle and is skipped in turn.
97
+ * the runner keeps iterating so the TERMINAL step (e.g. `coldBootV2`'s
98
+ * `bootstrap-hop`, whose `run()` performs its navigation side effect
99
+ * synchronously before its first `await`) still gets to fire. A step that
100
+ * has nothing to contribute after the deadline simply doesn't settle and is
101
+ * skipped in turn.
99
102
  *
100
103
  * Per-step timeouts inside `run()` remain the first line of defense and
101
104
  * should keep every step well under this budget on a healthy load; this only
102
- * trips when one of them regresses (the production FedCM-silent hang). When
103
- * omitted there is no overall deadline.
105
+ * trips when one of them regresses (a step hanging past its own timeout).
106
+ * When omitted there is no overall deadline.
104
107
  */
105
108
  readonly overallDeadlineMs?: number;
106
109
  /**
@@ -33,3 +33,17 @@ export declare function isAndroid(): boolean;
33
33
  * This allows lazy detection in environments where react-native is available
34
34
  */
35
35
  export declare function setPlatformOS(os: PlatformOS): void;
36
+ /**
37
+ * True only in a real web browser (a DOM is present), false on React Native
38
+ * and Node/SSR.
39
+ *
40
+ * Native defines a global `window` but no `document`, so the DOM probe — not a
41
+ * bare `window` check — is the reliable discriminator. This is the single
42
+ * source of truth consumed by `@oxyhq/services` and `@oxyhq/auth` (both dropped
43
+ * their local copies), so every consumer shares the exact same predicate.
44
+ *
45
+ * NOTE: this is a live runtime probe (not the cached `getPlatformOS()` verdict)
46
+ * because it must reflect the actual DOM availability at call time in the
47
+ * consumer's bundle.
48
+ */
49
+ export declare function isWebBrowser(): boolean;
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Registrable-apex (eTLD+1) host kernel.
3
+ *
4
+ * The client FAPI auto-detection helper was removed in the device-first cutover
5
+ * (which is why this file is now named for what it actually is, not the old
6
+ * `fapiAutoDetect`). What survives is the pure registrable-domain kernel, still
7
+ * used server-side by the api's device-first same-apex trust checks
8
+ * (`deviceAuth.ts`'s `POST /auth/device/web-session`, via `sameSite.ts`'s
9
+ * `isSameSiteTrustedRequest`) and the `@oxyhq/core/server` CORS/re-export layer.
10
+ *
11
+ * `registrableApex` is NOT legacy — the device-first same-apex check is a live
12
+ * consumer, so this kernel stays regardless of the SSO/FedCM removal.
13
+ */
14
+ /**
15
+ * Compute the bare registrable apex (eTLD+1) of a hostname using the Public
16
+ * Suffix List, including private hosted suffixes.
17
+ *
18
+ * Performs NO protocol handling and builds NO URL — it only answers "what is
19
+ * the registrable domain of this host, or is that undefinable?".
20
+ *
21
+ * Returns `null` (apex undefinable) for:
22
+ * - empty input;
23
+ * - IPv4 literals (`192.168.1.10`);
24
+ * - IPv6 literals or any host carrying a port (`[::1]`, anything with `:`);
25
+ * - single-label hosts (`intranet`, `localhost`);
26
+ * - public suffixes without a registrable label (e.g. `co.uk`, `github.io`).
27
+ *
28
+ * @param hostname - A bare hostname (no scheme), e.g. `www.mention.earth`.
29
+ * @returns The eTLD+1 (`mention.earth`), or `null` when undefinable.
30
+ */
31
+ export declare function registrableApex(hostname: string): string | null;