@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
@@ -0,0 +1,284 @@
1
+ "use strict";
2
+ /**
3
+ * Persisted auth state — ONE shape, web + native.
4
+ *
5
+ * The device-first session model (auth-centralization wave 1) persists the
6
+ * rotating refresh-token family head per ORIGIN so a reload restores the
7
+ * session locally without a redirect. This module is the storage seam: a tiny
8
+ * `load / save / clear` interface plus platform factories, so the cold boot
9
+ * (`coldBootV2`) and the unified refresh handler (`refresh.ts`) never touch a
10
+ * platform storage API directly.
11
+ *
12
+ * Platform-agnostic — the native factory takes an INJECTED key/value store
13
+ * (`@oxyhq/services` passes a SecureStore-backed adapter) so `@oxyhq/core`
14
+ * never imports `expo-secure-store`. The web factory is self-contained
15
+ * (`localStorage`) and degrades to in-memory when storage is unavailable
16
+ * (sandboxed iframe `SecurityError`, private-mode quota, SSR).
17
+ *
18
+ * ESM-safe (no `require()`).
19
+ */
20
+ Object.defineProperty(exports, "__esModule", { value: true });
21
+ exports.DEVICE_TOKEN_STORAGE_KEY = exports.AUTH_STATE_STORAGE_KEY = void 0;
22
+ exports.createMemoryAuthStateStore = createMemoryAuthStateStore;
23
+ exports.createWebAuthStateStore = createWebAuthStateStore;
24
+ exports.createNativeAuthStateStore = createNativeAuthStateStore;
25
+ /**
26
+ * Versioned storage key. The `.v1` suffix lets a future shape change ship a
27
+ * `.v2` key without reading a stale/incompatible `.v1` blob. Distinct from the
28
+ * `oxy_shared_*` keychain keys in `KeyManager`, so it never collides.
29
+ */
30
+ exports.AUTH_STATE_STORAGE_KEY = 'oxy.auth.v1';
31
+ /**
32
+ * Storage key for the long-lived device-attribution token. Separate from
33
+ * {@link AUTH_STATE_STORAGE_KEY} because it must OUTLIVE a session `clear()`
34
+ * (sign-out) — the device is unchanged across sign-ins.
35
+ */
36
+ exports.DEVICE_TOKEN_STORAGE_KEY = 'oxy.device.v1';
37
+ /**
38
+ * Parse + shape-validate a stored blob. Returns `null` for anything that is
39
+ * not a well-formed {@link PersistedAuthState} (absent, malformed JSON, wrong
40
+ * types) so a corrupt entry degrades to "signed out" rather than throwing.
41
+ */
42
+ function deserialize(raw) {
43
+ if (!raw) {
44
+ return null;
45
+ }
46
+ let parsed;
47
+ try {
48
+ parsed = JSON.parse(raw);
49
+ }
50
+ catch {
51
+ return null;
52
+ }
53
+ if (!parsed || typeof parsed !== 'object') {
54
+ return null;
55
+ }
56
+ const candidate = parsed;
57
+ if (typeof candidate.sessionId !== 'string' ||
58
+ typeof candidate.refreshToken !== 'string' ||
59
+ typeof candidate.userId !== 'string' ||
60
+ candidate.sessionId.length === 0 ||
61
+ candidate.refreshToken.length === 0 ||
62
+ candidate.userId.length === 0) {
63
+ return null;
64
+ }
65
+ const state = {
66
+ sessionId: candidate.sessionId,
67
+ refreshToken: candidate.refreshToken,
68
+ userId: candidate.userId,
69
+ };
70
+ if (typeof candidate.deviceToken === 'string' && candidate.deviceToken.length > 0) {
71
+ state.deviceToken = candidate.deviceToken;
72
+ }
73
+ if (typeof candidate.accessToken === 'string' && candidate.accessToken.length > 0) {
74
+ state.accessToken = candidate.accessToken;
75
+ }
76
+ if (typeof candidate.expiresAt === 'string' && candidate.expiresAt.length > 0) {
77
+ state.expiresAt = candidate.expiresAt;
78
+ }
79
+ return state;
80
+ }
81
+ /**
82
+ * A process-lifetime, in-memory {@link AuthStateStore}. Used directly for
83
+ * tests/SSR and as the degraded fallback of the web store when `localStorage`
84
+ * is unreachable. Not durable across reloads — that is acceptable for the
85
+ * fallback because the alternative (throwing) would break cold boot entirely.
86
+ */
87
+ function createMemoryAuthStateStore() {
88
+ let current = null;
89
+ let deviceToken = null;
90
+ return {
91
+ load: async () => current,
92
+ save: async (state) => {
93
+ current = state;
94
+ },
95
+ clear: async () => {
96
+ current = null;
97
+ },
98
+ loadDeviceToken: async () => deviceToken,
99
+ saveDeviceToken: async (token) => {
100
+ deviceToken = token;
101
+ },
102
+ clearDeviceToken: async () => {
103
+ deviceToken = null;
104
+ },
105
+ };
106
+ }
107
+ /**
108
+ * Read the ambient `localStorage`, tolerating the case where merely ACCESSING
109
+ * `window.localStorage` throws. In a sandboxed / cross-origin iframe the getter
110
+ * itself raises `SecurityError` (not the method calls), so the access must be
111
+ * inside the try. Returns `null` when storage is unavailable.
112
+ */
113
+ function safeGetLocalStorage() {
114
+ try {
115
+ if (typeof globalThis === 'undefined') {
116
+ return null;
117
+ }
118
+ const store = globalThis.localStorage;
119
+ return store ?? null;
120
+ }
121
+ catch {
122
+ return null;
123
+ }
124
+ }
125
+ /**
126
+ * A `localStorage`-backed {@link AuthStateStore} under the versioned
127
+ * {@link AUTH_STATE_STORAGE_KEY}.
128
+ *
129
+ * Resilience:
130
+ * - If `localStorage` is unreachable (sandboxed-iframe `SecurityError`, SSR),
131
+ * the whole store degrades to an in-memory {@link createMemoryAuthStateStore}
132
+ * for this page's lifetime — never throws on construction.
133
+ * - Individual `getItem`/`setItem`/`removeItem` are each wrapped: a read that
134
+ * throws yields `null`; a write that throws (quota, private mode) is
135
+ * swallowed. The persisted-refresh lane treats a failed persist as "no
136
+ * durable state" (falls back to the bootstrap hop) rather than crashing.
137
+ */
138
+ function createWebAuthStateStore() {
139
+ const storage = safeGetLocalStorage();
140
+ if (!storage) {
141
+ return createMemoryAuthStateStore();
142
+ }
143
+ // In-memory mirror so a FAILED persist (quota / private mode / locked store)
144
+ // does not silently lose the session for this page's lifetime. `undefined`
145
+ // means "never written this session" → fall back to storage; any set value
146
+ // (including `null` after clear) is authoritative and preferred over storage.
147
+ let sessionMirror;
148
+ let deviceTokenMirror;
149
+ return {
150
+ load: async () => {
151
+ if (sessionMirror !== undefined) {
152
+ return sessionMirror;
153
+ }
154
+ try {
155
+ return deserialize(storage.getItem(exports.AUTH_STATE_STORAGE_KEY));
156
+ }
157
+ catch {
158
+ return null;
159
+ }
160
+ },
161
+ save: async (state) => {
162
+ sessionMirror = state; // mirror FIRST — authoritative even if persist fails
163
+ try {
164
+ storage.setItem(exports.AUTH_STATE_STORAGE_KEY, JSON.stringify(state));
165
+ }
166
+ catch {
167
+ // Quota / private-mode / disabled storage — non-fatal. The session
168
+ // stays live via the in-memory mirror; only reload durability is lost.
169
+ }
170
+ },
171
+ clear: async () => {
172
+ sessionMirror = null;
173
+ try {
174
+ storage.removeItem(exports.AUTH_STATE_STORAGE_KEY);
175
+ }
176
+ catch {
177
+ // Non-fatal — see save().
178
+ }
179
+ },
180
+ loadDeviceToken: async () => {
181
+ if (deviceTokenMirror !== undefined) {
182
+ return deviceTokenMirror;
183
+ }
184
+ try {
185
+ return storage.getItem(exports.DEVICE_TOKEN_STORAGE_KEY);
186
+ }
187
+ catch {
188
+ return null;
189
+ }
190
+ },
191
+ saveDeviceToken: async (token) => {
192
+ deviceTokenMirror = token;
193
+ try {
194
+ storage.setItem(exports.DEVICE_TOKEN_STORAGE_KEY, token);
195
+ }
196
+ catch {
197
+ // Non-fatal — see save().
198
+ }
199
+ },
200
+ clearDeviceToken: async () => {
201
+ deviceTokenMirror = null;
202
+ try {
203
+ storage.removeItem(exports.DEVICE_TOKEN_STORAGE_KEY);
204
+ }
205
+ catch {
206
+ // Non-fatal — see save().
207
+ }
208
+ },
209
+ };
210
+ }
211
+ /**
212
+ * A native {@link AuthStateStore} over an injected async key/value store.
213
+ *
214
+ * `@oxyhq/core` never imports `expo-secure-store`; `@oxyhq/services` constructs
215
+ * the SecureStore-backed adapter and passes it here. Every operation is wrapped
216
+ * so a storage exception degrades gracefully (read → `null`, write → swallowed)
217
+ * exactly like the web store.
218
+ */
219
+ function createNativeAuthStateStore(storage) {
220
+ // Same in-memory mirror as the web store — a locked/failed SecureStore write
221
+ // must not silently lose the session for the app's lifetime.
222
+ let sessionMirror;
223
+ let deviceTokenMirror;
224
+ return {
225
+ load: async () => {
226
+ if (sessionMirror !== undefined) {
227
+ return sessionMirror;
228
+ }
229
+ try {
230
+ return deserialize(await storage.getItem(exports.AUTH_STATE_STORAGE_KEY));
231
+ }
232
+ catch {
233
+ return null;
234
+ }
235
+ },
236
+ save: async (state) => {
237
+ sessionMirror = state;
238
+ try {
239
+ await storage.setItem(exports.AUTH_STATE_STORAGE_KEY, JSON.stringify(state));
240
+ }
241
+ catch {
242
+ // Non-fatal — session stays live via the in-memory mirror.
243
+ }
244
+ },
245
+ clear: async () => {
246
+ sessionMirror = null;
247
+ try {
248
+ await storage.removeItem(exports.AUTH_STATE_STORAGE_KEY);
249
+ }
250
+ catch {
251
+ // Non-fatal.
252
+ }
253
+ },
254
+ loadDeviceToken: async () => {
255
+ if (deviceTokenMirror !== undefined) {
256
+ return deviceTokenMirror;
257
+ }
258
+ try {
259
+ return await storage.getItem(exports.DEVICE_TOKEN_STORAGE_KEY);
260
+ }
261
+ catch {
262
+ return null;
263
+ }
264
+ },
265
+ saveDeviceToken: async (token) => {
266
+ deviceTokenMirror = token;
267
+ try {
268
+ await storage.setItem(exports.DEVICE_TOKEN_STORAGE_KEY, token);
269
+ }
270
+ catch {
271
+ // Non-fatal.
272
+ }
273
+ },
274
+ clearDeviceToken: async () => {
275
+ deviceTokenMirror = null;
276
+ try {
277
+ await storage.removeItem(exports.DEVICE_TOKEN_STORAGE_KEY);
278
+ }
279
+ catch {
280
+ // Non-fatal.
281
+ }
282
+ },
283
+ };
284
+ }
@@ -0,0 +1,264 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.TOKEN_REFRESH_LEAD_MS = void 0;
4
+ exports.refreshPersistedSession = refreshPersistedSession;
5
+ exports.createAuthRefreshHandler = createAuthRefreshHandler;
6
+ exports.installAuthRefreshHandler = installAuthRefreshHandler;
7
+ exports.startTokenRefreshScheduler = startTokenRefreshScheduler;
8
+ const platform_1 = require("../utils/platform");
9
+ const loggerUtils_1 = require("../utils/loggerUtils");
10
+ /**
11
+ * Lead time (ms) before access-token expiry at which the proactive scheduler
12
+ * refreshes. Mirrors `HttpService`'s per-request `TOKEN_REFRESH_LEAD_SECONDS`
13
+ * (60s) so the scheduled refresh and the request-time preflight refresh use
14
+ * the same window — the scheduler just fires it during idle/background.
15
+ */
16
+ exports.TOKEN_REFRESH_LEAD_MS = 60000;
17
+ /**
18
+ * Max `setTimeout` delay (2^31 − 1 ms, ~24.8 days). A larger delay overflows
19
+ * the int32 timer field and fires IMMEDIATELY — with a long-TTL token that
20
+ * turns the reschedule-on-finish loop into a tight busy refresh. Clamp to it.
21
+ */
22
+ const MAX_TIMEOUT_DELAY_MS = 2147483647;
23
+ /**
24
+ * Floor (ms) on ANY scheduled delay. An already-expired / in-lead-window token
25
+ * computes a non-positive `exp − now − lead`; without this floor that becomes
26
+ * `setTimeout(…, 0)`, and a FAILING refresh (offline / server error) would
27
+ * re-arm at 0 in the finally block → a tight 100%-CPU busy loop. The floor
28
+ * guarantees every re-arm yields the event loop.
29
+ */
30
+ const MIN_SCHEDULE_DELAY_MS = 1000;
31
+ /**
32
+ * Backoff schedule (ms) applied when a scheduled refresh FAILS: first retry
33
+ * after {@link MIN_FAILURE_BACKOFF_MS}, doubling up to {@link MAX_FAILURE_BACKOFF_MS}.
34
+ * Reset to 0 on any success or token change. This is what converts the former
35
+ * zero-delay failure loop into a bounded, backing-off retry.
36
+ */
37
+ const MIN_FAILURE_BACKOFF_MS = 5000;
38
+ const MAX_FAILURE_BACKOFF_MS = 5 * 60000;
39
+ /**
40
+ * Error codes (in addition to HTTP 401/403) that mean the stored refresh token
41
+ * is permanently unusable — the family was revoked or a reuse was detected. On
42
+ * any of these the persisted store is CLEARED (the session is truly over);
43
+ * transient failures (network, 5xx) leave the store intact so a later attempt
44
+ * can still succeed.
45
+ */
46
+ const REVOKED_REFRESH_CODES = new Set([
47
+ 'invalid_grant',
48
+ 'refresh_token_revoked',
49
+ 'refresh_token_reuse',
50
+ 'token_reuse',
51
+ 'invalid_token',
52
+ ]);
53
+ /** Does this error mean the refresh token is permanently dead (vs. transient)? */
54
+ function isRevokedRefreshError(error) {
55
+ if (!error || typeof error !== 'object') {
56
+ return false;
57
+ }
58
+ const e = error;
59
+ if (e.status === 401 || e.status === 403) {
60
+ return true;
61
+ }
62
+ return typeof e.code === 'string' && REVOKED_REFRESH_CODES.has(e.code);
63
+ }
64
+ /**
65
+ * Rotate the persisted session and return the fresh access token, or `null`
66
+ * when no arm could produce one.
67
+ *
68
+ * Arm 1 (`POST /auth/refresh-token`): if the store holds a refresh token, rotate
69
+ * it — on success plant + persist the rotated pair; on a REVOKED error clear the
70
+ * store; on a transient error leave the store and return `null`.
71
+ *
72
+ * Arm 2 (native shared-keychain): when there is no refresh token or arm 1 was
73
+ * revoked, re-mint via `signInWithSharedIdentity` (which plants tokens). The
74
+ * shared keychain — not the per-origin store — is the durable native credential,
75
+ * so this arm does not write the store.
76
+ */
77
+ async function refreshPersistedSession(deps) {
78
+ const { oxy, store } = deps;
79
+ const allowSharedKeyFallback = deps.allowSharedKeyFallback ?? (0, platform_1.isNative)();
80
+ const persisted = await store.load();
81
+ if (persisted?.refreshToken) {
82
+ try {
83
+ const rotated = await oxy.refreshWithToken(persisted.refreshToken);
84
+ oxy.setTokens(rotated.accessToken);
85
+ const next = {
86
+ sessionId: rotated.sessionId,
87
+ refreshToken: rotated.refreshToken,
88
+ userId: persisted.userId,
89
+ accessToken: rotated.accessToken,
90
+ expiresAt: rotated.expiresAt,
91
+ };
92
+ if (persisted.deviceToken) {
93
+ next.deviceToken = persisted.deviceToken;
94
+ }
95
+ await store.save(next);
96
+ return rotated.accessToken;
97
+ }
98
+ catch (error) {
99
+ if (isRevokedRefreshError(error)) {
100
+ await store.clear();
101
+ // Fall through to the native shared-key arm below — on a shared-key
102
+ // device the refresh family being revoked does not end the session.
103
+ }
104
+ else {
105
+ loggerUtils_1.logger.debug('Persisted refresh failed (transient) — keeping store', { component: 'refresh', method: 'refreshPersistedSession' }, error);
106
+ return null;
107
+ }
108
+ }
109
+ }
110
+ if (allowSharedKeyFallback) {
111
+ try {
112
+ const session = await oxy.signInWithSharedIdentity();
113
+ if (session?.accessToken) {
114
+ return session.accessToken;
115
+ }
116
+ }
117
+ catch (error) {
118
+ loggerUtils_1.logger.debug('Shared-key refresh fallback failed', { component: 'refresh', method: 'refreshPersistedSession' }, error);
119
+ }
120
+ }
121
+ return null;
122
+ }
123
+ /**
124
+ * Build the reactive `AuthRefreshHandler` (arm 1 + arm 2). Install it via
125
+ * {@link installAuthRefreshHandler} or directly on
126
+ * `oxy.httpService.setAuthRefreshHandler`. `HttpService` owns single-flight
127
+ * dedup + cooldown, so the timer, the request-time preflight, and a 401 all
128
+ * collapse to one network attempt.
129
+ */
130
+ function createAuthRefreshHandler(deps) {
131
+ return async (_reason) => {
132
+ return refreshPersistedSession(deps);
133
+ };
134
+ }
135
+ /**
136
+ * Install the unified refresh handler on the owner client's `HttpService`.
137
+ * Returns a disposer that removes it.
138
+ */
139
+ function installAuthRefreshHandler(deps) {
140
+ deps.oxy.httpService.setAuthRefreshHandler(createAuthRefreshHandler(deps));
141
+ return () => {
142
+ deps.oxy.httpService.setAuthRefreshHandler(null);
143
+ };
144
+ }
145
+ /**
146
+ * Start the proactive refresh scheduler against `oxy`.
147
+ *
148
+ * Schedules a single timer to fire {@link TOKEN_REFRESH_LEAD_MS} before the
149
+ * current access token's `exp`, calling
150
+ * `oxy.httpService.refreshAccessToken('preflight')` (which runs the installed
151
+ * handler; deduped + cooldown-guarded). After every attempt it reschedules
152
+ * from the possibly-rotated token. It also reschedules whenever the token
153
+ * changes (a sign-out that clears the token cancels the timer) and, on web
154
+ * tab-focus, refreshes immediately if already inside the lead window (a
155
+ * long-hidden tab throttles timers, so the token can be expired on return).
156
+ *
157
+ * No-ops cleanly when there is no token or an opaque/no-`exp` token — the
158
+ * reactive 401 path stays the only refresh trigger in that case. The timer is
159
+ * `.unref?.()`-ed so it never keeps a Node/Jest event loop alive.
160
+ */
161
+ function startTokenRefreshScheduler(oxy) {
162
+ let disposed = false;
163
+ let timer = null;
164
+ // 0 = no active backoff; grows on consecutive failures, resets on success /
165
+ // token change. Keeps a failing refresh from re-arming at zero delay.
166
+ let failureBackoffMs = 0;
167
+ const clearTimer = () => {
168
+ if (timer !== null) {
169
+ clearTimeout(timer);
170
+ timer = null;
171
+ }
172
+ };
173
+ /** Arm the timer for `delayMs`, flooring at {@link MIN_SCHEDULE_DELAY_MS} and capping at the int32 max. */
174
+ const armTimer = (delayMs) => {
175
+ clearTimer();
176
+ const clamped = Math.min(Math.max(delayMs, MIN_SCHEDULE_DELAY_MS), MAX_TIMEOUT_DELAY_MS);
177
+ timer = setTimeout(runRefresh, clamped);
178
+ // Never keep a Node/Jest event loop alive for a background refresh timer.
179
+ timer.unref?.();
180
+ };
181
+ /** Schedule the next refresh from the current token's expiry (the healthy path). */
182
+ const scheduleFromExpiry = () => {
183
+ clearTimer();
184
+ if (disposed || !oxy.getAccessToken()) {
185
+ return;
186
+ }
187
+ const expSeconds = oxy.getAccessTokenExpiry();
188
+ if (expSeconds === null) {
189
+ return;
190
+ }
191
+ armTimer(expSeconds * 1000 - Date.now() - exports.TOKEN_REFRESH_LEAD_MS);
192
+ };
193
+ const runRefresh = () => {
194
+ // Clear any pending timer up front so an out-of-band trigger (focus) plus
195
+ // a fired timer can never double-run.
196
+ clearTimer();
197
+ void oxy.httpService.refreshAccessToken('preflight')
198
+ .then((token) => Boolean(token))
199
+ .catch(() => false)
200
+ .then((ok) => {
201
+ if (disposed) {
202
+ return;
203
+ }
204
+ if (ok) {
205
+ // Success — drop any backoff and re-arm from the rotated token's exp.
206
+ failureBackoffMs = 0;
207
+ scheduleFromExpiry();
208
+ return;
209
+ }
210
+ // Failure — back off (never re-arm at zero) and retry.
211
+ failureBackoffMs =
212
+ failureBackoffMs === 0
213
+ ? MIN_FAILURE_BACKOFF_MS
214
+ : Math.min(failureBackoffMs * 2, MAX_FAILURE_BACKOFF_MS);
215
+ armTimer(failureBackoffMs);
216
+ });
217
+ };
218
+ /** Public (re)schedule entry: a fresh token / focus signal — drop backoff and arm from expiry. */
219
+ const schedule = () => {
220
+ failureBackoffMs = 0;
221
+ scheduleFromExpiry();
222
+ };
223
+ const onFocus = () => {
224
+ if (disposed || !oxy.getAccessToken()) {
225
+ return;
226
+ }
227
+ const expSeconds = oxy.getAccessTokenExpiry();
228
+ if (expSeconds === null) {
229
+ return;
230
+ }
231
+ const remainingMs = expSeconds * 1000 - Date.now();
232
+ if (remainingMs <= exports.TOKEN_REFRESH_LEAD_MS) {
233
+ runRefresh();
234
+ }
235
+ else {
236
+ schedule();
237
+ }
238
+ };
239
+ const unsubscribeTokens = oxy.onTokensChanged(() => {
240
+ if (!disposed) {
241
+ schedule();
242
+ }
243
+ });
244
+ let removeFocusListener = null;
245
+ if (typeof document !== 'undefined') {
246
+ const handler = () => {
247
+ if (document.visibilityState === 'visible') {
248
+ onFocus();
249
+ }
250
+ };
251
+ document.addEventListener('visibilitychange', handler);
252
+ removeFocusListener = () => document.removeEventListener('visibilitychange', handler);
253
+ }
254
+ schedule();
255
+ return {
256
+ dispose() {
257
+ disposed = true;
258
+ clearTimer();
259
+ unsubscribeTokens();
260
+ removeFocusListener?.();
261
+ removeFocusListener = null;
262
+ },
263
+ };
264
+ }
@@ -25,7 +25,7 @@ const isDev = () => {
25
25
  exports.isDev = isDev;
26
26
  /**
27
27
  * Log a debug message (only in development)
28
- * @param prefix - Log prefix (e.g., '[FedCM]')
28
+ * @param prefix - Log prefix (e.g., '[ColdBoot]')
29
29
  * @param args - Arguments to log
30
30
  */
31
31
  const debugLog = (prefix, ...args) => {
@@ -58,12 +58,12 @@ const debugError = (prefix, ...args) => {
58
58
  exports.debugError = debugError;
59
59
  /**
60
60
  * Create a namespaced debug logger
61
- * @param namespace - Logger namespace (e.g., 'FedCM', 'SilentAuth')
61
+ * @param namespace - Logger namespace (e.g., 'ColdBoot', 'DeviceAuth')
62
62
  * @returns Object with log, warn, error methods
63
63
  *
64
64
  * @example
65
65
  * ```ts
66
- * const debug = createDebugLogger('FedCM');
66
+ * const debug = createDebugLogger('ColdBoot');
67
67
  * debug.log('Starting authentication');
68
68
  * debug.warn('Token expires soon');
69
69
  * debug.error('Authentication failed', error);
@@ -4,7 +4,7 @@
4
4
  * Used by both @oxyhq/services (React Native) and @oxyhq/auth (Web) account stores.
5
5
  */
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
- exports.getAccountColor = exports.mergeAccountsFromRefreshAll = exports.createQuickAccount = exports.buildAccountsArray = exports.getAccountFallbackHandle = exports.getAccountDisplayName = exports.formatPublicKeyHandle = void 0;
7
+ exports.getAccountColor = exports.createQuickAccount = exports.buildAccountsArray = exports.getAccountFallbackHandle = exports.getAccountDisplayName = exports.formatPublicKeyHandle = void 0;
8
8
  const i18n_1 = require("../i18n");
9
9
  /**
10
10
  * Truncate a long public key for display, e.g. `0x12345678…`.
@@ -124,60 +124,6 @@ const createQuickAccount = (sessionId, userData, existingAccount, getFileDownloa
124
124
  };
125
125
  };
126
126
  exports.createQuickAccount = createQuickAccount;
127
- /**
128
- * Merge a fresh `/auth/refresh-all` snapshot into an existing QuickAccount
129
- * list, preserving any cached fields (`avatarUrl`) for slots that didn't
130
- * change. The fresh response is canonical: the resulting list contains EXACTLY
131
- * the slots present in `fresh`, sorted by `authuser` ascending. Stale stored
132
- * accounts that no longer appear in `fresh` are dropped (the server already
133
- * authoritatively cleared the corresponding cookie).
134
- *
135
- * @param stored Previously persisted QuickAccount list (any order).
136
- * @param fresh Server's authoritative refresh-all response.
137
- * @returns Canonical merged list, sorted by `authuser` asc.
138
- */
139
- const mergeAccountsFromRefreshAll = (stored, fresh) => {
140
- const storedByAuthuser = new Map();
141
- if (stored) {
142
- for (const account of stored) {
143
- if (typeof account.authuser === 'number') {
144
- storedByAuthuser.set(account.authuser, account);
145
- }
146
- }
147
- }
148
- const merged = fresh.map((entry) => {
149
- const previous = storedByAuthuser.get(entry.authuser);
150
- // Preserve any previously cached identity for a slot that arrives
151
- // without a user shape rather than overwriting it with blanks, and let
152
- // AuthManager's getCurrentUser() hydration refresh it on the next
153
- // snapshot.
154
- const wireUser = entry.user;
155
- const username = wireUser?.username ?? previous?.username ?? '';
156
- const displayName = (0, exports.getAccountDisplayName)({
157
- name: wireUser?.name,
158
- username,
159
- });
160
- const avatar = wireUser?.avatar ?? previous?.avatar ?? undefined;
161
- const avatarUrl = previous && previous.avatar === avatar ? previous.avatarUrl : undefined;
162
- return {
163
- sessionId: entry.sessionId,
164
- userId: wireUser?.id ?? previous?.userId,
165
- username,
166
- displayName,
167
- avatar,
168
- avatarUrl,
169
- authuser: entry.authuser,
170
- color: wireUser?.color ?? previous?.color ?? null,
171
- };
172
- });
173
- merged.sort((a, b) => {
174
- const aIdx = a.authuser ?? Number.POSITIVE_INFINITY;
175
- const bIdx = b.authuser ?? Number.POSITIVE_INFINITY;
176
- return aIdx - bIdx;
177
- });
178
- return merged;
179
- };
180
- exports.mergeAccountsFromRefreshAll = mergeAccountsFromRefreshAll;
181
127
  /**
182
128
  * Return the account's preferred Bloom color preset, or `null` if it has no
183
129
  * preference. Centralises the `color ?? null` normalisation so consumers can