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