@oxyhq/core 5.2.0 → 5.3.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/CrossDomainAuth.js +32 -42
- package/dist/cjs/i18n/locales/ar-SA.json +1 -0
- package/dist/cjs/i18n/locales/ca-ES.json +1 -0
- package/dist/cjs/i18n/locales/de-DE.json +1 -0
- package/dist/cjs/i18n/locales/en-US.json +1 -0
- package/dist/cjs/i18n/locales/es-ES.json +1 -0
- package/dist/cjs/i18n/locales/fr-FR.json +1 -0
- package/dist/cjs/i18n/locales/it-IT.json +1 -0
- package/dist/cjs/i18n/locales/ja-JP.json +1 -0
- package/dist/cjs/i18n/locales/ko-KR.json +1 -0
- package/dist/cjs/i18n/locales/locales/ar-SA.json +1 -0
- package/dist/cjs/i18n/locales/locales/ca-ES.json +1 -0
- package/dist/cjs/i18n/locales/locales/de-DE.json +1 -0
- package/dist/cjs/i18n/locales/locales/en-US.json +1 -0
- package/dist/cjs/i18n/locales/locales/es-ES.json +1 -0
- package/dist/cjs/i18n/locales/locales/fr-FR.json +1 -0
- package/dist/cjs/i18n/locales/locales/it-IT.json +1 -0
- package/dist/cjs/i18n/locales/locales/ja-JP.json +1 -0
- package/dist/cjs/i18n/locales/locales/ko-KR.json +1 -0
- package/dist/cjs/i18n/locales/locales/pt-PT.json +1 -0
- package/dist/cjs/i18n/locales/locales/zh-CN.json +1 -0
- package/dist/cjs/i18n/locales/pt-PT.json +1 -0
- package/dist/cjs/i18n/locales/zh-CN.json +1 -0
- package/dist/cjs/index.js +24 -2
- package/dist/cjs/mixins/OxyServices.sso.js +36 -0
- package/dist/cjs/mixins/OxyServices.user.js +24 -0
- package/dist/cjs/server/index.js +13 -1
- package/dist/cjs/session/SessionClient.js +142 -0
- package/dist/cjs/session/createSessionClient.js +26 -0
- package/dist/cjs/session/projectSessionState.js +75 -0
- package/dist/cjs/session/sessionClientHost.js +30 -0
- package/dist/cjs/session/socketLoader.js +55 -0
- package/dist/cjs/utils/cache.js +2 -0
- package/dist/cjs/utils/ssoBounce.js +9 -9
- package/dist/cjs/utils/ssoEstablish.js +110 -0
- package/dist/esm/.tsbuildinfo +1 -1
- package/dist/esm/CrossDomainAuth.js +32 -42
- package/dist/esm/i18n/locales/ar-SA.json +1 -0
- package/dist/esm/i18n/locales/ca-ES.json +1 -0
- package/dist/esm/i18n/locales/de-DE.json +1 -0
- package/dist/esm/i18n/locales/en-US.json +1 -0
- package/dist/esm/i18n/locales/es-ES.json +1 -0
- package/dist/esm/i18n/locales/fr-FR.json +1 -0
- package/dist/esm/i18n/locales/it-IT.json +1 -0
- package/dist/esm/i18n/locales/ja-JP.json +1 -0
- package/dist/esm/i18n/locales/ko-KR.json +1 -0
- package/dist/esm/i18n/locales/locales/ar-SA.json +1 -0
- package/dist/esm/i18n/locales/locales/ca-ES.json +1 -0
- package/dist/esm/i18n/locales/locales/de-DE.json +1 -0
- package/dist/esm/i18n/locales/locales/en-US.json +1 -0
- package/dist/esm/i18n/locales/locales/es-ES.json +1 -0
- package/dist/esm/i18n/locales/locales/fr-FR.json +1 -0
- package/dist/esm/i18n/locales/locales/it-IT.json +1 -0
- package/dist/esm/i18n/locales/locales/ja-JP.json +1 -0
- package/dist/esm/i18n/locales/locales/ko-KR.json +1 -0
- package/dist/esm/i18n/locales/locales/pt-PT.json +1 -0
- package/dist/esm/i18n/locales/locales/zh-CN.json +1 -0
- package/dist/esm/i18n/locales/pt-PT.json +1 -0
- package/dist/esm/i18n/locales/zh-CN.json +1 -0
- package/dist/esm/index.js +14 -0
- package/dist/esm/mixins/OxyServices.sso.js +36 -0
- package/dist/esm/mixins/OxyServices.user.js +24 -0
- package/dist/esm/server/index.js +10 -0
- package/dist/esm/session/SessionClient.js +138 -0
- package/dist/esm/session/createSessionClient.js +23 -0
- package/dist/esm/session/projectSessionState.js +69 -0
- package/dist/esm/session/sessionClientHost.js +27 -0
- package/dist/esm/session/socketLoader.js +19 -0
- package/dist/esm/utils/cache.js +2 -0
- package/dist/esm/utils/ssoBounce.js +9 -9
- package/dist/esm/utils/ssoEstablish.js +107 -0
- package/dist/types/.tsbuildinfo +1 -1
- package/dist/types/CrossDomainAuth.d.ts +33 -13
- package/dist/types/index.d.ts +7 -0
- package/dist/types/mixins/OxyServices.sso.d.ts +24 -0
- package/dist/types/mixins/OxyServices.user.d.ts +14 -0
- package/dist/types/server/index.d.ts +2 -0
- package/dist/types/session/SessionClient.d.ts +55 -0
- package/dist/types/session/createSessionClient.d.ts +23 -0
- package/dist/types/session/projectSessionState.d.ts +43 -0
- package/dist/types/session/sessionClientHost.d.ts +18 -0
- package/dist/types/session/socketLoader.d.ts +9 -0
- package/dist/types/utils/ssoBounce.d.ts +9 -9
- package/dist/types/utils/ssoEstablish.d.ts +85 -0
- package/package.json +1 -1
- package/src/CrossDomainAuth.ts +33 -44
- package/src/__tests__/crossDomainAuth.test.ts +33 -16
- package/src/i18n/locales/ar-SA.json +1 -0
- package/src/i18n/locales/ca-ES.json +1 -0
- package/src/i18n/locales/de-DE.json +1 -0
- package/src/i18n/locales/en-US.json +1 -0
- package/src/i18n/locales/es-ES.json +1 -0
- package/src/i18n/locales/fr-FR.json +1 -0
- package/src/i18n/locales/it-IT.json +1 -0
- package/src/i18n/locales/ja-JP.json +1 -0
- package/src/i18n/locales/ko-KR.json +1 -0
- package/src/i18n/locales/pt-PT.json +1 -0
- package/src/i18n/locales/zh-CN.json +1 -0
- package/src/index.ts +24 -0
- package/src/mixins/OxyServices.sso.ts +55 -0
- package/src/mixins/OxyServices.user.ts +26 -0
- package/src/mixins/__tests__/sso.test.ts +41 -0
- package/src/server/index.ts +12 -0
- package/src/session/SessionClient.ts +175 -0
- package/src/session/__tests__/SessionClient.rest.test.ts +79 -0
- package/src/session/__tests__/SessionClient.socket.test.ts +132 -0
- package/src/session/__tests__/SessionClient.state.test.ts +64 -0
- package/src/session/__tests__/sessionIntegration.test.ts +202 -0
- package/src/session/createSessionClient.ts +31 -0
- package/src/session/projectSessionState.ts +83 -0
- package/src/session/sessionClientHost.ts +32 -0
- package/src/session/socketLoader.ts +28 -0
- package/src/utils/__tests__/ssoEstablish.test.ts +204 -0
- package/src/utils/cache.ts +2 -0
- package/src/utils/ssoBounce.ts +9 -9
- package/src/utils/ssoEstablish.ts +174 -0
|
@@ -2,11 +2,19 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Cross-Domain Authentication Helper
|
|
4
4
|
*
|
|
5
|
-
* Provides a simplified API for cross-domain SSO authentication
|
|
6
|
-
*
|
|
5
|
+
* Provides a simplified API for cross-domain SSO authentication. The
|
|
6
|
+
* automatic sign-in path uses a full-page redirect through the central IdP
|
|
7
|
+
* (`auth.oxy.so`) — a tokenless, universal mechanism that works in every
|
|
8
|
+
* browser.
|
|
7
9
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
+
* FedCM (`signInWithFedCM`) is intentionally NOT part of the automatic
|
|
11
|
+
* (`'auto'`) path: it is a Chrome-only browser API, and a misconfigured or
|
|
12
|
+
* unreachable FedCM endpoint fails fast and silently, which — combined with a
|
|
13
|
+
* caller's auth-guard effect re-invoking `signIn()` whenever the user is still
|
|
14
|
+
* unauthenticated — produced a real production incident (an accelerating
|
|
15
|
+
* `autoSignIn` → FedCM-fails → redirect retry loop). `signInWithFedCM` remains
|
|
16
|
+
* available for callers that want to opt into it EXPLICITLY
|
|
17
|
+
* (`signIn({ method: 'fedcm' })`).
|
|
10
18
|
*
|
|
11
19
|
* Usage:
|
|
12
20
|
* ```typescript
|
|
@@ -14,7 +22,7 @@
|
|
|
14
22
|
*
|
|
15
23
|
* const auth = new CrossDomainAuth(oxyServices);
|
|
16
24
|
*
|
|
17
|
-
* // Automatic method selection
|
|
25
|
+
* // Automatic method selection (always redirect)
|
|
18
26
|
* const session = await auth.signIn();
|
|
19
27
|
*
|
|
20
28
|
* // Or use a specific method
|
|
@@ -30,11 +38,11 @@ class CrossDomainAuth {
|
|
|
30
38
|
this.oxyServices = oxyServices;
|
|
31
39
|
}
|
|
32
40
|
/**
|
|
33
|
-
* Sign in with automatic method selection
|
|
41
|
+
* Sign in with automatic method selection.
|
|
34
42
|
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
43
|
+
* Auto mode always uses the full-page redirect (see the class doc comment
|
|
44
|
+
* for why FedCM was removed from this path). Pass `{ method: 'fedcm' }` to
|
|
45
|
+
* opt into FedCM explicitly.
|
|
38
46
|
*
|
|
39
47
|
* @param options - Authentication options
|
|
40
48
|
* @returns Session with user data and access token
|
|
@@ -52,22 +60,18 @@ class CrossDomainAuth {
|
|
|
52
60
|
return this.autoSignIn(options);
|
|
53
61
|
}
|
|
54
62
|
/**
|
|
55
|
-
* Automatic sign-in
|
|
63
|
+
* Automatic sign-in.
|
|
64
|
+
*
|
|
65
|
+
* Goes straight to the full-page redirect — the sole automatic method.
|
|
66
|
+
* FedCM is deliberately NOT attempted here (see the class doc comment):
|
|
67
|
+
* it is Chrome-only, and its fast/silent failure mode combined with a
|
|
68
|
+
* caller's auth-guard effect re-invoking `signIn()` produced a real
|
|
69
|
+
* production sign-in loop. Use `signIn({ method: 'fedcm' })` to opt in
|
|
70
|
+
* explicitly.
|
|
56
71
|
*
|
|
57
72
|
* @private
|
|
58
73
|
*/
|
|
59
74
|
async autoSignIn(options) {
|
|
60
|
-
// 1. Try FedCM first (best UX, most modern)
|
|
61
|
-
if (this.isFedCMSupported()) {
|
|
62
|
-
try {
|
|
63
|
-
options.onMethodSelected?.('fedcm');
|
|
64
|
-
return await this.signInWithFedCM(options);
|
|
65
|
-
}
|
|
66
|
-
catch (error) {
|
|
67
|
-
loggerUtils_1.logger.warn('FedCM failed, falling back to redirect', { component: 'CrossDomainAuth', method: 'autoSignIn' }, error);
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
// 2. Fallback to redirect (always works)
|
|
71
75
|
options.onMethodSelected?.('redirect');
|
|
72
76
|
this.signInWithRedirect(options);
|
|
73
77
|
return null;
|
|
@@ -104,25 +108,13 @@ class CrossDomainAuth {
|
|
|
104
108
|
/**
|
|
105
109
|
* Silent sign-in (check for existing session)
|
|
106
110
|
*
|
|
107
|
-
* Tries to automatically sign in without user interaction
|
|
108
|
-
*
|
|
111
|
+
* Tries to automatically sign in without user interaction, via the
|
|
112
|
+
* iframe-based silent auth against the per-apex `/auth/silent` IdP host.
|
|
113
|
+
* FedCM is deliberately NOT attempted here (see the class doc comment).
|
|
109
114
|
*
|
|
110
115
|
* @returns Session if user is already signed in, null otherwise
|
|
111
116
|
*/
|
|
112
117
|
async silentSignIn() {
|
|
113
|
-
// Try FedCM silent sign-in first (if supported)
|
|
114
|
-
if (this.isFedCMSupported()) {
|
|
115
|
-
try {
|
|
116
|
-
const session = await this.oxyServices.silentSignInWithFedCM();
|
|
117
|
-
if (session) {
|
|
118
|
-
return session;
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
catch (error) {
|
|
122
|
-
loggerUtils_1.logger.debug('FedCM silent sign-in did not resolve', { component: 'CrossDomainAuth', method: 'silentSignIn' }, error);
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
// Fallback to iframe-based silent auth
|
|
126
118
|
try {
|
|
127
119
|
return await this.oxyServices.silentSignIn();
|
|
128
120
|
}
|
|
@@ -151,15 +143,13 @@ class CrossDomainAuth {
|
|
|
151
143
|
/**
|
|
152
144
|
* Get recommended authentication method for current environment
|
|
153
145
|
*
|
|
146
|
+
* Redirect is the sole recommended automatic method — it works in every
|
|
147
|
+
* browser, unlike FedCM (Chrome-only). Callers that want FedCM must opt in
|
|
148
|
+
* explicitly via `signIn({ method: 'fedcm' })`.
|
|
149
|
+
*
|
|
154
150
|
* @returns Recommended method name and reason
|
|
155
151
|
*/
|
|
156
152
|
getRecommendedMethod() {
|
|
157
|
-
if (this.isFedCMSupported()) {
|
|
158
|
-
return {
|
|
159
|
-
method: 'fedcm',
|
|
160
|
-
reason: 'FedCM is supported - provides best UX with browser-native auth',
|
|
161
|
-
};
|
|
162
|
-
}
|
|
163
153
|
if (typeof window !== 'undefined') {
|
|
164
154
|
return {
|
|
165
155
|
method: 'redirect',
|
|
@@ -207,6 +207,7 @@
|
|
|
207
207
|
"accountMenu": {
|
|
208
208
|
"label": "Menú del compte",
|
|
209
209
|
"manage": "Gestiona el teu compte d'Oxy",
|
|
210
|
+
"viewProfile": "Mostra el perfil",
|
|
210
211
|
"addAnother": "Afegeix un altre compte",
|
|
211
212
|
"signOutAll": "Tanca la sessió de tots els comptes",
|
|
212
213
|
"open": "Menú del compte",
|
|
@@ -1555,6 +1555,7 @@
|
|
|
1555
1555
|
"accountMenu": {
|
|
1556
1556
|
"label": "Account menu",
|
|
1557
1557
|
"manage": "Manage your Oxy Account",
|
|
1558
|
+
"viewProfile": "View profile",
|
|
1558
1559
|
"addAnother": "Add another account",
|
|
1559
1560
|
"signOutAll": "Sign out of all accounts",
|
|
1560
1561
|
"open": "Account menu",
|
|
@@ -1555,6 +1555,7 @@
|
|
|
1555
1555
|
"accountMenu": {
|
|
1556
1556
|
"label": "Menú de cuenta",
|
|
1557
1557
|
"manage": "Gestiona tu cuenta de Oxy",
|
|
1558
|
+
"viewProfile": "Ver perfil",
|
|
1558
1559
|
"addAnother": "Añadir otra cuenta",
|
|
1559
1560
|
"signOutAll": "Cerrar sesión en todas las cuentas",
|
|
1560
1561
|
"open": "Menú de cuenta",
|
|
@@ -207,6 +207,7 @@
|
|
|
207
207
|
"accountMenu": {
|
|
208
208
|
"label": "Menu du compte",
|
|
209
209
|
"manage": "Gérer votre compte Oxy",
|
|
210
|
+
"viewProfile": "Voir le profil",
|
|
210
211
|
"addAnother": "Ajouter un autre compte",
|
|
211
212
|
"signOutAll": "Se déconnecter de tous les comptes",
|
|
212
213
|
"open": "Menu du compte",
|
|
@@ -207,6 +207,7 @@
|
|
|
207
207
|
"accountMenu": {
|
|
208
208
|
"label": "Menu account",
|
|
209
209
|
"manage": "Gestisci il tuo account Oxy",
|
|
210
|
+
"viewProfile": "Visualizza profilo",
|
|
210
211
|
"addAnother": "Aggiungi un altro account",
|
|
211
212
|
"signOutAll": "Esci da tutti gli account",
|
|
212
213
|
"open": "Menu account",
|
|
@@ -207,6 +207,7 @@
|
|
|
207
207
|
"accountMenu": {
|
|
208
208
|
"label": "Menú del compte",
|
|
209
209
|
"manage": "Gestiona el teu compte d'Oxy",
|
|
210
|
+
"viewProfile": "Mostra el perfil",
|
|
210
211
|
"addAnother": "Afegeix un altre compte",
|
|
211
212
|
"signOutAll": "Tanca la sessió de tots els comptes",
|
|
212
213
|
"open": "Menú del compte",
|
|
@@ -1555,6 +1555,7 @@
|
|
|
1555
1555
|
"accountMenu": {
|
|
1556
1556
|
"label": "Account menu",
|
|
1557
1557
|
"manage": "Manage your Oxy Account",
|
|
1558
|
+
"viewProfile": "View profile",
|
|
1558
1559
|
"addAnother": "Add another account",
|
|
1559
1560
|
"signOutAll": "Sign out of all accounts",
|
|
1560
1561
|
"open": "Account menu",
|
|
@@ -1555,6 +1555,7 @@
|
|
|
1555
1555
|
"accountMenu": {
|
|
1556
1556
|
"label": "Menú de cuenta",
|
|
1557
1557
|
"manage": "Gestiona tu cuenta de Oxy",
|
|
1558
|
+
"viewProfile": "Ver perfil",
|
|
1558
1559
|
"addAnother": "Añadir otra cuenta",
|
|
1559
1560
|
"signOutAll": "Cerrar sesión en todas las cuentas",
|
|
1560
1561
|
"open": "Menú de cuenta",
|
|
@@ -207,6 +207,7 @@
|
|
|
207
207
|
"accountMenu": {
|
|
208
208
|
"label": "Menu du compte",
|
|
209
209
|
"manage": "Gérer votre compte Oxy",
|
|
210
|
+
"viewProfile": "Voir le profil",
|
|
210
211
|
"addAnother": "Ajouter un autre compte",
|
|
211
212
|
"signOutAll": "Se déconnecter de tous les comptes",
|
|
212
213
|
"open": "Menu du compte",
|
|
@@ -207,6 +207,7 @@
|
|
|
207
207
|
"accountMenu": {
|
|
208
208
|
"label": "Menu account",
|
|
209
209
|
"manage": "Gestisci il tuo account Oxy",
|
|
210
|
+
"viewProfile": "Visualizza profilo",
|
|
210
211
|
"addAnother": "Aggiungi un altro account",
|
|
211
212
|
"signOutAll": "Esci da tutti gli account",
|
|
212
213
|
"open": "Menu account",
|
package/dist/cjs/index.js
CHANGED
|
@@ -20,8 +20,8 @@
|
|
|
20
20
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
21
21
|
exports.isIOS = exports.isNative = exports.isWeb = exports.setPlatformOS = exports.getPlatformOS = exports.isRTLLocale = exports.normalizeLanguageCode = exports.getNativeLanguageName = exports.getLanguageName = exports.getLanguageMetadata = exports.SUPPORTED_LANGUAGES = exports.TopicSource = exports.TopicType = exports.SECURITY_EVENT_SEVERITY_MAP = exports.DeviceManager = exports.RecoveryPhraseService = exports.SignatureService = exports.IdentityPersistError = exports.IdentityAlreadyExistsError = exports.KeyManager = exports.sessionsArraysEqual = exports.normalizeAndSortSessions = exports.mergeSessions = exports.authenticatedApiCall = exports.withAuthErrorHandling = exports.isAuthenticationError = exports.ensureValidToken = exports.AuthenticationFailedError = exports.SessionSyncRequiredError = exports.verifyPublicCardAttestation = exports.parseAttestPayload = exports.parseIdPayload = exports.buildUserDid = exports.normalizeProfileLinks = exports.getNormalizedUserHandle = exports.getCanonicalUserHandle = exports.normalizeUserIdentityOrNull = exports.normalizeUserIdentity = exports.getNormalizedUserId = exports.OxyAppDataIdentifierError = exports.ServiceCredentialMismatchError = exports.createCrossDomainAuth = exports.CrossDomainAuth = exports.createAuthManager = exports.AuthManager = exports.oxyClient = exports.OXY_CLOUD_URL = exports.OxyAuthenticationTimeoutError = exports.OxyAuthenticationError = exports.OxyServices = void 0;
|
|
22
22
|
exports.PASSWORD_REGEX = exports.USERNAME_REGEX = exports.EMAIL_REGEX = exports.retryAsync = exports.validateRequiredFields = exports.handleHttpError = exports.createApiError = exports.ErrorCodes = exports.safeJsonParse = exports.buildPaginationParams = exports.buildUrl = exports.buildSearchParams = exports.translate = exports.createDebugLogger = exports.debugError = exports.debugWarn = exports.debugLog = exports.isDev = exports.withRetry = exports.delay = exports.shouldAllowRequest = exports.recordSuccess = exports.recordFailure = exports.calculateBackoffInterval = exports.createCircuitBreakerState = exports.DEFAULT_CIRCUIT_BREAKER_CONFIG = exports.isRetryableError = exports.isNetworkError = exports.isServerError = exports.isRateLimitError = exports.isNotFoundError = exports.isForbiddenError = exports.isUnauthorizedError = exports.isAlreadyRegisteredError = exports.getErrorMessage = exports.getErrorStatus = exports.HttpStatus = exports.getSystemColorScheme = exports.systemPrefersDarkMode = exports.getOppositeTheme = exports.normalizeColorScheme = exports.normalizeTheme = exports.getContrastTextColor = exports.isLightColor = exports.withOpacity = exports.rgbToHex = exports.hexToRgb = exports.lightenColor = exports.darkenColor = exports.isAndroid = void 0;
|
|
23
|
-
exports.
|
|
24
|
-
exports.packageInfo = exports.runColdBoot = exports.allowSsoBounce = exports.silentRestoreSuppressed = exports.guardActive = exports.isCentralIdPOrigin = exports.buildSsoBounceUrl = exports.getSsoCallbackBootstrapScript = exports.ssoNavigate = exports.ssoCallbackBootstrapKey = exports.ssoSignedOutKey = void 0;
|
|
23
|
+
exports.ssoAttemptedKey = exports.ssoNoSessionKey = exports.ssoDestKey = exports.ssoGuardKey = exports.ssoStateKey = exports.SSO_GUARD_TTL_MS = exports.SSO_CALLBACK_PATH = exports.establishIdpSessionAfterClaim = exports.generateSsoState = exports.consumeSsoReturn = exports.parseSsoReturnFragment = exports.resolveCentralAuthUrl = exports.CENTRAL_IDP_APEX = exports.CENTRAL_AUTH_URL = exports.registrableApex = exports.autoDetectAuthWebUrl = exports.getAccountColor = exports.mergeAccountsFromRefreshAll = exports.formatPublicKeyHandle = exports.getAccountFallbackHandle = exports.getAccountDisplayName = exports.createQuickAccount = exports.buildAccountsArray = exports.updateAvatarVisibility = exports.logPerformance = exports.logPayment = exports.logDevice = exports.logUser = exports.logSession = exports.logApi = exports.logAuth = exports.LogLevel = exports.logger = exports.validateAndSanitizeUserInput = exports.isValidObjectId = exports.sanitizeHTML = exports.sanitizeString = exports.isValidFileType = exports.isValidFileSize = exports.isValidDate = exports.isValidURL = exports.isValidUUID = exports.isValidObject = exports.isValidArray = exports.isRequiredBoolean = exports.isRequiredNumber = exports.isRequiredString = exports.isValidPassword = exports.isValidUsername = exports.isValidEmail = void 0;
|
|
24
|
+
exports.packageInfo = exports.accountIdsOf = exports.activeUserOf = exports.activeSessionIdOf = exports.deviceStateToClientSessions = exports.createSessionClient = exports.createSessionClientHost = exports.SessionClient = exports.runColdBoot = exports.allowSsoBounce = exports.silentRestoreSuppressed = exports.guardActive = exports.isCentralIdPOrigin = exports.buildSsoBounceUrl = exports.getSsoCallbackBootstrapScript = exports.ssoNavigate = exports.ssoCallbackBootstrapKey = exports.ssoSignedOutKey = exports.ssoPriorSessionKey = void 0;
|
|
25
25
|
// Ensure crypto polyfills are loaded before anything else
|
|
26
26
|
require("./crypto/polyfill");
|
|
27
27
|
// ---------------------------------------------------------------------------
|
|
@@ -274,6 +274,9 @@ Object.defineProperty(exports, "parseSsoReturnFragment", { enumerable: true, get
|
|
|
274
274
|
Object.defineProperty(exports, "consumeSsoReturn", { enumerable: true, get: function () { return ssoReturn_1.consumeSsoReturn; } });
|
|
275
275
|
var OxyServices_sso_1 = require("./mixins/OxyServices.sso");
|
|
276
276
|
Object.defineProperty(exports, "generateSsoState", { enumerable: true, get: function () { return OxyServices_sso_1.generateSsoState; } });
|
|
277
|
+
// Post-claim durable-session establish hop (web device-flow / QR sign-in).
|
|
278
|
+
var ssoEstablish_1 = require("./utils/ssoEstablish");
|
|
279
|
+
Object.defineProperty(exports, "establishIdpSessionAfterClaim", { enumerable: true, get: function () { return ssoEstablish_1.establishIdpSessionAfterClaim; } });
|
|
277
280
|
// SSO bounce — per-origin sessionStorage keys, bounce URL builder, predicates
|
|
278
281
|
var ssoBounce_1 = require("./utils/ssoBounce");
|
|
279
282
|
Object.defineProperty(exports, "SSO_CALLBACK_PATH", { enumerable: true, get: function () { return ssoBounce_1.SSO_CALLBACK_PATH; } });
|
|
@@ -295,6 +298,25 @@ Object.defineProperty(exports, "silentRestoreSuppressed", { enumerable: true, ge
|
|
|
295
298
|
Object.defineProperty(exports, "allowSsoBounce", { enumerable: true, get: function () { return ssoBounce_1.allowSsoBounce; } });
|
|
296
299
|
var coldBoot_1 = require("./utils/coldBoot");
|
|
297
300
|
Object.defineProperty(exports, "runColdBoot", { enumerable: true, get: function () { return coldBoot_1.runColdBoot; } });
|
|
301
|
+
// ---------------------------------------------------------------------------
|
|
302
|
+
// Session sync (device-scoped multi-account session client)
|
|
303
|
+
// ---------------------------------------------------------------------------
|
|
304
|
+
var SessionClient_1 = require("./session/SessionClient");
|
|
305
|
+
Object.defineProperty(exports, "SessionClient", { enumerable: true, get: function () { return SessionClient_1.SessionClient; } });
|
|
306
|
+
// Shared SessionClient integration layer: the host adapter, the pure
|
|
307
|
+
// DeviceSessionState projection helpers, and the client factory are defined
|
|
308
|
+
// ONCE here so `@oxyhq/services` and `@oxyhq/auth` both reuse them instead of
|
|
309
|
+
// duplicating a local copy. Each consumer supplies its own `TokenTransport`
|
|
310
|
+
// (native vs. web mint strategies differ) to `createSessionClient`.
|
|
311
|
+
var sessionClientHost_1 = require("./session/sessionClientHost");
|
|
312
|
+
Object.defineProperty(exports, "createSessionClientHost", { enumerable: true, get: function () { return sessionClientHost_1.createSessionClientHost; } });
|
|
313
|
+
var createSessionClient_1 = require("./session/createSessionClient");
|
|
314
|
+
Object.defineProperty(exports, "createSessionClient", { enumerable: true, get: function () { return createSessionClient_1.createSessionClient; } });
|
|
315
|
+
var projectSessionState_1 = require("./session/projectSessionState");
|
|
316
|
+
Object.defineProperty(exports, "deviceStateToClientSessions", { enumerable: true, get: function () { return projectSessionState_1.deviceStateToClientSessions; } });
|
|
317
|
+
Object.defineProperty(exports, "activeSessionIdOf", { enumerable: true, get: function () { return projectSessionState_1.activeSessionIdOf; } });
|
|
318
|
+
Object.defineProperty(exports, "activeUserOf", { enumerable: true, get: function () { return projectSessionState_1.activeUserOf; } });
|
|
319
|
+
Object.defineProperty(exports, "accountIdsOf", { enumerable: true, get: function () { return projectSessionState_1.accountIdsOf; } });
|
|
298
320
|
// API response contracts (request/response Zod schemas + inferred types) live in
|
|
299
321
|
// `@oxyhq/contracts` — the single source of truth shared by the backend and every
|
|
300
322
|
// client SDK. Import them directly from `@oxyhq/contracts`; `@oxyhq/core` does NOT
|
|
@@ -168,5 +168,41 @@ function OxyServicesSsoMixin(Base) {
|
|
|
168
168
|
};
|
|
169
169
|
return session;
|
|
170
170
|
}
|
|
171
|
+
/**
|
|
172
|
+
* Mint a server-formed `/sso/establish` URL for the caller's OWN session,
|
|
173
|
+
* bound to an approved RP `origin`.
|
|
174
|
+
*
|
|
175
|
+
* Bearer-authenticated (the session id is taken from the caller's own
|
|
176
|
+
* bearer, server-side — never from any argument). The server validates that
|
|
177
|
+
* `origin` is an approved client origin (and matches the request `Origin`),
|
|
178
|
+
* derives the per-apex IdP host (`auth.<apex>`), mints a short-lived HS256
|
|
179
|
+
* establish-token, and returns a fully-formed
|
|
180
|
+
* `https://<auth-host>/sso/establish?et=…&return_to=<origin>/__oxy/sso-callback&state=<state>`.
|
|
181
|
+
*
|
|
182
|
+
* Used AFTER a web device-flow claim to plant the durable first-party
|
|
183
|
+
* `fedcm_session` cookie so a reload can re-mint a token (see
|
|
184
|
+
* {@link establishIdpSessionAfterClaim}). Cache-free (a POST is never
|
|
185
|
+
* cached, but `cache: false` is explicit).
|
|
186
|
+
*
|
|
187
|
+
* @param origin - The RP origin (`window.location.origin`) to establish for.
|
|
188
|
+
* @param state - The CSRF state echoed back in the callback fragment; the
|
|
189
|
+
* caller persists the SAME value under `ssoStateKey(origin)` so the
|
|
190
|
+
* post-bounce `sso-return` step validates it.
|
|
191
|
+
*/
|
|
192
|
+
async requestSsoEstablishUrl(origin, state) {
|
|
193
|
+
if (typeof origin !== 'string' || origin.length === 0) {
|
|
194
|
+
throw this.handleError(new Error('requestSsoEstablishUrl requires a non-empty origin'));
|
|
195
|
+
}
|
|
196
|
+
if (typeof state !== 'string' || state.length === 0) {
|
|
197
|
+
throw this.handleError(new Error('requestSsoEstablishUrl requires a non-empty state'));
|
|
198
|
+
}
|
|
199
|
+
const response = await this.makeRequest('POST', '/sso/establish-token', { origin, state }, { cache: false });
|
|
200
|
+
if (!response ||
|
|
201
|
+
typeof response.establishUrl !== 'string' ||
|
|
202
|
+
response.establishUrl.length === 0) {
|
|
203
|
+
throw this.handleError(new Error('SSO establish-token returned no establishUrl'));
|
|
204
|
+
}
|
|
205
|
+
return { establishUrl: response.establishUrl };
|
|
206
|
+
}
|
|
171
207
|
};
|
|
172
208
|
}
|
|
@@ -633,6 +633,30 @@ function OxyServicesUserMixin(Base) {
|
|
|
633
633
|
throw this.handleError(error);
|
|
634
634
|
}
|
|
635
635
|
}
|
|
636
|
+
/**
|
|
637
|
+
* Get the authenticated VIEWER's OWN mutual-follow user ids — the accounts the
|
|
638
|
+
* viewer follows that ALSO follow the viewer back (a bidirectional follow
|
|
639
|
+
* edge). The viewer is derived server-side from the SDK's auth token (never a
|
|
640
|
+
* param), so there is no target id to pass.
|
|
641
|
+
*
|
|
642
|
+
* Returns a bounded, lean list of ids meant to SEED a "Mutuals" feed (the
|
|
643
|
+
* consumer hydrates/ranks the posts itself) — distinct from
|
|
644
|
+
* {@link getUserMutuals}, which returns hydrated "followers you know" DTOs
|
|
645
|
+
* about ANOTHER profile. An anonymous caller resolves to an empty array.
|
|
646
|
+
*/
|
|
647
|
+
async getMutualUserIds(params) {
|
|
648
|
+
try {
|
|
649
|
+
const query = (0, apiUtils_1.buildPaginationParams)(params || {});
|
|
650
|
+
const response = await this.makeRequest('GET', '/users/mutual-ids', query, {
|
|
651
|
+
cache: true,
|
|
652
|
+
cacheTTL: 2 * 60 * 1000, // 2 minutes cache
|
|
653
|
+
});
|
|
654
|
+
return response.data || [];
|
|
655
|
+
}
|
|
656
|
+
catch (error) {
|
|
657
|
+
throw this.handleError(error);
|
|
658
|
+
}
|
|
659
|
+
}
|
|
636
660
|
/**
|
|
637
661
|
* Get notifications
|
|
638
662
|
*/
|
package/dist/cjs/server/index.js
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* ```
|
|
17
17
|
*/
|
|
18
18
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
19
|
-
exports.verifySecret = exports.createOxyCors = exports.UPSTREAM_HEADERS_TIMEOUT_MS = exports.MAX_URL_LENGTH = exports.MAX_REDIRECTS = exports.DEFAULT_USER_AGENT = exports.BLOCKED_HOSTNAMES = exports.ALLOWED_PROTOCOLS = exports.ALLOWED_PORTS = exports.UpstreamError = exports.SsrfRejection = exports.safeFetch = exports.isBlockedIp = exports.assertSafePublicUrl = exports.createOxyRateLimit = exports.requireOxyAuth = exports.isOxyAuthenticated = exports.getRequiredOxyUserId = exports.getOxyUserId = exports.createOxyAuthMiddleware = exports.createOptionalOxyAuth = void 0;
|
|
19
|
+
exports.SSO_CALLBACK_PATH = exports.registrableApex = exports.verifySecret = exports.createOxyCors = exports.UPSTREAM_HEADERS_TIMEOUT_MS = exports.MAX_URL_LENGTH = exports.MAX_REDIRECTS = exports.DEFAULT_USER_AGENT = exports.BLOCKED_HOSTNAMES = exports.ALLOWED_PROTOCOLS = exports.ALLOWED_PORTS = exports.UpstreamError = exports.SsrfRejection = exports.safeFetch = exports.isBlockedIp = exports.assertSafePublicUrl = exports.createOxyRateLimit = exports.requireOxyAuth = exports.isOxyAuthenticated = exports.getRequiredOxyUserId = exports.getOxyUserId = exports.createOxyAuthMiddleware = exports.createOptionalOxyAuth = void 0;
|
|
20
20
|
var auth_1 = require("./auth");
|
|
21
21
|
Object.defineProperty(exports, "createOptionalOxyAuth", { enumerable: true, get: function () { return auth_1.createOptionalOxyAuth; } });
|
|
22
22
|
Object.defineProperty(exports, "createOxyAuthMiddleware", { enumerable: true, get: function () { return auth_1.createOxyAuthMiddleware; } });
|
|
@@ -46,3 +46,15 @@ Object.defineProperty(exports, "createOxyCors", { enumerable: true, get: functio
|
|
|
46
46
|
// Constant-time secret comparison.
|
|
47
47
|
var verifySecret_1 = require("./verifySecret");
|
|
48
48
|
Object.defineProperty(exports, "verifySecret", { enumerable: true, get: function () { return verifySecret_1.verifySecret; } });
|
|
49
|
+
// Registrable-apex (eTLD+1) derivation via the Public Suffix List — the SINGLE
|
|
50
|
+
// SOURCE OF TRUTH shared with the IdP worker and the client FAPI auto-detect.
|
|
51
|
+
// Pure host handling (no browser deps), so it is safe on the server subpath and
|
|
52
|
+
// lets `@oxyhq/api` derive `auth.<apex>` without duplicating PSL logic.
|
|
53
|
+
var fapiAutoDetect_1 = require("../utils/fapiAutoDetect");
|
|
54
|
+
Object.defineProperty(exports, "registrableApex", { enumerable: true, get: function () { return fapiAutoDetect_1.registrableApex; } });
|
|
55
|
+
// The single RP callback path the IdP redirects back to. A pure wire-contract
|
|
56
|
+
// constant (no browser deps at module top level), re-used server-side so the
|
|
57
|
+
// `/sso/establish-token` `return_to` cannot drift from what `/sso/establish`
|
|
58
|
+
// validates.
|
|
59
|
+
var ssoBounce_1 = require("../utils/ssoBounce");
|
|
60
|
+
Object.defineProperty(exports, "SSO_CALLBACK_PATH", { enumerable: true, get: function () { return ssoBounce_1.SSO_CALLBACK_PATH; } });
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SessionClient = void 0;
|
|
4
|
+
const contracts_1 = require("@oxyhq/contracts");
|
|
5
|
+
const loggerUtils_1 = require("../utils/loggerUtils");
|
|
6
|
+
const socketLoader_1 = require("./socketLoader");
|
|
7
|
+
class SessionClient {
|
|
8
|
+
constructor(host, options = {}) {
|
|
9
|
+
this.host = host;
|
|
10
|
+
this.options = options;
|
|
11
|
+
this.state = null;
|
|
12
|
+
this.listeners = new Set();
|
|
13
|
+
this.socket = null;
|
|
14
|
+
this.tokenUnsub = null;
|
|
15
|
+
this.started = false;
|
|
16
|
+
}
|
|
17
|
+
getState() {
|
|
18
|
+
return this.state;
|
|
19
|
+
}
|
|
20
|
+
subscribe(listener) {
|
|
21
|
+
this.listeners.add(listener);
|
|
22
|
+
return () => {
|
|
23
|
+
this.listeners.delete(listener);
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
notify() {
|
|
27
|
+
for (const listener of this.listeners) {
|
|
28
|
+
try {
|
|
29
|
+
listener(this.state);
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
loggerUtils_1.logger.error('[SessionClient] subscriber threw', error);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/** Validate + last-writer-wins by revision. Returns true if applied. */
|
|
37
|
+
applyState(raw) {
|
|
38
|
+
const next = (0, contracts_1.safeParseContract)(contracts_1.deviceSessionStateSchema, raw);
|
|
39
|
+
if (!next) {
|
|
40
|
+
loggerUtils_1.logger.warn('[SessionClient] discarded invalid session state');
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
if (this.state && next.revision <= this.state.revision) {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
this.state = next;
|
|
47
|
+
this.notify();
|
|
48
|
+
if (this.options.transport) {
|
|
49
|
+
void this.options.transport.ensureActiveToken(next).catch((error) => {
|
|
50
|
+
loggerUtils_1.logger.warn('[SessionClient] ensureActiveToken failed', { component: 'SessionClient' }, error);
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Validate `{ state, activeToken }`, apply the state, and plant the active token host-side.
|
|
57
|
+
* Token-planting is decoupled from whether `applyState` advanced the revision: a socket push
|
|
58
|
+
* followed by this same `GET /state` fetch returns the SAME revision (applyState no-ops), but
|
|
59
|
+
* the token still needs to be planted. The account-match guard rejects a stale response for an
|
|
60
|
+
* account that is no longer active.
|
|
61
|
+
*/
|
|
62
|
+
applySync(raw) {
|
|
63
|
+
const sync = (0, contracts_1.safeParseContract)(contracts_1.deviceSessionSyncSchema, raw);
|
|
64
|
+
if (!sync) {
|
|
65
|
+
loggerUtils_1.logger.warn('[SessionClient] discarded invalid session sync');
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
this.applyState(sync.state);
|
|
69
|
+
if (sync.activeToken && this.state && sync.state.activeAccountId === this.state.activeAccountId) {
|
|
70
|
+
this.host.setTokens(sync.activeToken.accessToken);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
async bootstrap() {
|
|
74
|
+
const res = await this.host.makeRequest('GET', '/session/device/state', undefined, { cache: false });
|
|
75
|
+
this.applySync(res?.data);
|
|
76
|
+
}
|
|
77
|
+
async switchAccount(accountId) {
|
|
78
|
+
const res = await this.host.makeRequest('POST', '/session/device/switch', { accountId }, { cache: false });
|
|
79
|
+
this.applySync(res?.data);
|
|
80
|
+
}
|
|
81
|
+
async signOut(target) {
|
|
82
|
+
const res = await this.host.makeRequest('POST', '/session/device/signout', target, { cache: false });
|
|
83
|
+
this.applySync(res?.data);
|
|
84
|
+
}
|
|
85
|
+
async addCurrentAccount() {
|
|
86
|
+
const res = await this.host.makeRequest('POST', '/session/device/add', undefined, { cache: false });
|
|
87
|
+
this.applySync(res?.data);
|
|
88
|
+
}
|
|
89
|
+
async start() {
|
|
90
|
+
if (this.started)
|
|
91
|
+
return;
|
|
92
|
+
this.started = true;
|
|
93
|
+
this.tokenUnsub = this.host.onTokensChanged((token) => {
|
|
94
|
+
if (token && this.socket && !this.socket.connected) {
|
|
95
|
+
this.socket.connect();
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
await this.bootstrap();
|
|
99
|
+
await this.connectSocket();
|
|
100
|
+
}
|
|
101
|
+
stop() {
|
|
102
|
+
this.started = false;
|
|
103
|
+
if (this.tokenUnsub) {
|
|
104
|
+
this.tokenUnsub();
|
|
105
|
+
this.tokenUnsub = null;
|
|
106
|
+
}
|
|
107
|
+
if (this.socket) {
|
|
108
|
+
this.socket.disconnect();
|
|
109
|
+
this.socket = null;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
async connectSocket() {
|
|
113
|
+
const io = await (0, socketLoader_1.getSocketIO)();
|
|
114
|
+
if (!io) {
|
|
115
|
+
loggerUtils_1.logger.warn('[SessionClient] no socket.io-client; running REST-only (no realtime sync)', { component: 'SessionClient' });
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
if (!this.started)
|
|
119
|
+
return; // stopped while the dynamic import was in flight
|
|
120
|
+
const hasToken = Boolean(this.host.getAccessToken());
|
|
121
|
+
const socket = io(this.host.getBaseURL(), {
|
|
122
|
+
transports: ['websocket'],
|
|
123
|
+
autoConnect: hasToken,
|
|
124
|
+
auth: (cb) => {
|
|
125
|
+
cb({ token: this.host.getAccessToken() ?? '' });
|
|
126
|
+
},
|
|
127
|
+
});
|
|
128
|
+
socket.on('session_state', (payload) => {
|
|
129
|
+
const applied = this.applyState(payload);
|
|
130
|
+
if (applied) {
|
|
131
|
+
const active = this.state?.activeAccountId ?? null;
|
|
132
|
+
if (active && active !== this.host.getCurrentAccountId()) {
|
|
133
|
+
void this.bootstrap().catch((error) => {
|
|
134
|
+
loggerUtils_1.logger.warn('[SessionClient] post-push token fetch failed', { component: 'SessionClient' }, error);
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
this.socket = socket;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
exports.SessionClient = SessionClient;
|