@oxyhq/core 7.1.1 → 8.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/README.md +48 -24
- package/dist/cjs/.tsbuildinfo +1 -1
- package/dist/cjs/HttpService.js +6 -6
- package/dist/cjs/i18n/locales/en-US.json +44 -1
- package/dist/cjs/i18n/locales/es-ES.json +44 -1
- package/dist/cjs/i18n/locales/locales/en-US.json +45 -2
- package/dist/cjs/i18n/locales/locales/es-ES.json +45 -2
- package/dist/cjs/index.js +19 -16
- package/dist/cjs/server/index.js +1 -7
- package/dist/cjs/session/accountDialogController.js +1 -1
- package/dist/cjs/session/accountProjection.js +1 -1
- package/dist/cjs/session/projectSessionState.js +1 -1
- package/dist/cjs/session/sessionClientHost.js +1 -2
- package/dist/cjs/utils/accountUtils.js +1 -1
- package/dist/cjs/utils/oauthPkce.js +142 -0
- package/dist/cjs/utils/platform.js +1 -1
- package/dist/esm/.tsbuildinfo +1 -1
- package/dist/esm/HttpService.js +6 -6
- package/dist/esm/i18n/locales/en-US.json +44 -1
- package/dist/esm/i18n/locales/es-ES.json +44 -1
- package/dist/esm/i18n/locales/locales/en-US.json +45 -2
- package/dist/esm/i18n/locales/locales/es-ES.json +45 -2
- package/dist/esm/index.js +11 -13
- package/dist/esm/server/index.js +0 -5
- package/dist/esm/session/accountDialogController.js +1 -1
- package/dist/esm/session/accountProjection.js +1 -1
- package/dist/esm/session/projectSessionState.js +1 -1
- package/dist/esm/session/sessionClientHost.js +1 -2
- package/dist/esm/utils/accountUtils.js +1 -1
- package/dist/esm/utils/oauthPkce.js +135 -0
- package/dist/esm/utils/platform.js +1 -1
- package/dist/types/.tsbuildinfo +1 -1
- package/dist/types/HttpService.d.ts +1 -1
- package/dist/types/index.d.ts +3 -2
- package/dist/types/mixins/OxyServices.accounts.d.ts +13 -3
- package/dist/types/mixins/OxyServices.connectedApps.d.ts +4 -0
- package/dist/types/mixins/OxyServices.devices.d.ts +3 -2
- package/dist/types/models/interfaces.d.ts +4 -4
- package/dist/types/server/index.d.ts +0 -1
- package/dist/types/session/accountDialogController.d.ts +1 -1
- package/dist/types/session/accountProjection.d.ts +1 -1
- package/dist/types/session/projectSessionState.d.ts +1 -1
- package/dist/types/session/refresh.d.ts +4 -8
- package/dist/types/session/sessionClientHost.d.ts +1 -2
- package/dist/types/utils/accountUtils.d.ts +1 -1
- package/dist/types/utils/oauthPkce.d.ts +74 -0
- package/dist/types/utils/platform.d.ts +1 -1
- package/package.json +3 -3
- package/src/HttpService.ts +6 -6
- package/src/i18n/locales/en-US.json +45 -2
- package/src/i18n/locales/es-ES.json +45 -2
- package/src/index.ts +23 -16
- package/src/mixins/OxyServices.accounts.ts +12 -0
- package/src/mixins/OxyServices.connectedApps.ts +4 -0
- package/src/mixins/OxyServices.devices.ts +6 -5
- package/src/mixins/__tests__/accounts.test.ts +1 -1
- package/src/models/interfaces.ts +7 -5
- package/src/server/index.ts +0 -6
- package/src/session/accountDialogController.ts +1 -1
- package/src/session/accountProjection.ts +1 -1
- package/src/session/projectSessionState.ts +1 -1
- package/src/session/refresh.ts +4 -8
- package/src/session/sessionClientHost.ts +1 -2
- package/src/utils/__tests__/coldBoot.test.ts +55 -65
- package/src/utils/__tests__/oauthPkce.test.ts +154 -0
- package/src/utils/accountUtils.ts +1 -1
- package/src/utils/oauthPkce.ts +189 -0
- package/src/utils/platform.ts +1 -1
- package/dist/cjs/utils/ssoBounce.js +0 -24
- package/dist/esm/utils/ssoBounce.js +0 -21
- package/dist/types/utils/ssoBounce.d.ts +0 -21
- package/src/utils/ssoBounce.ts +0 -22
package/dist/esm/HttpService.js
CHANGED
|
@@ -53,7 +53,7 @@ const CSRF_FETCH_RETRY_DELAY_MS = 500;
|
|
|
53
53
|
/**
|
|
54
54
|
* Cooldown (ms) applied after a failed access-token refresh before another
|
|
55
55
|
* refresh is attempted. Prevents a refresh storm (and server hammering) when
|
|
56
|
-
* the
|
|
56
|
+
* the auth refresh handler is failing — every in-flight request that
|
|
57
57
|
* hits a 401 would otherwise trigger its own refresh.
|
|
58
58
|
*/
|
|
59
59
|
const TOKEN_REFRESH_COOLDOWN_MS = 15000;
|
|
@@ -140,7 +140,7 @@ export class HttpService {
|
|
|
140
140
|
this.cacheSizeWarningSilentUntil = 0;
|
|
141
141
|
/**
|
|
142
142
|
* Fan-out listeners notified on EVERY access-token change on this instance:
|
|
143
|
-
* explicit `setTokens`, `clearTokens`,
|
|
143
|
+
* explicit `setTokens`, `clearTokens`, a refresh-handler rotation, and the
|
|
144
144
|
* internal 401-driven clear. This is a Set so multiple independent observers
|
|
145
145
|
* can mirror token state without clobbering each other.
|
|
146
146
|
*
|
|
@@ -358,9 +358,9 @@ export class HttpService {
|
|
|
358
358
|
clearTimeout(timeoutId);
|
|
359
359
|
// Handle response
|
|
360
360
|
if (!response.ok) {
|
|
361
|
-
// On 401, delegate refresh
|
|
362
|
-
// giving up. HttpService deliberately does not know any
|
|
363
|
-
// routes; the
|
|
361
|
+
// On 401, delegate to the installed auth refresh handler and retry
|
|
362
|
+
// once before giving up. HttpService deliberately does not know any
|
|
363
|
+
// session routes; the refresh handler owns session rotation.
|
|
364
364
|
if (response.status === 401 && !config._isAuthRetry && !config.skipAuth) {
|
|
365
365
|
const refreshed = await this.refreshAccessToken('response-401');
|
|
366
366
|
if (refreshed) {
|
|
@@ -820,7 +820,7 @@ export class HttpService {
|
|
|
820
820
|
this.tokenStore.setTokens(newToken);
|
|
821
821
|
this.notifyTokenChange();
|
|
822
822
|
}
|
|
823
|
-
this.logger.debug('Token refreshed via
|
|
823
|
+
this.logger.debug('Token refreshed via the auth refresh handler');
|
|
824
824
|
return newToken;
|
|
825
825
|
})
|
|
826
826
|
.catch((error) => {
|
|
@@ -384,7 +384,11 @@
|
|
|
384
384
|
"subtitle": "Add another account to switch between them quickly"
|
|
385
385
|
},
|
|
386
386
|
"label": "Account switcher",
|
|
387
|
-
"searchPlaceholder": "Search accounts"
|
|
387
|
+
"searchPlaceholder": "Search accounts",
|
|
388
|
+
"scanTitle": "Scan to sign in",
|
|
389
|
+
"scanSubtitle": "Open the Oxy app on your phone and scan this code",
|
|
390
|
+
"scanWithOxy": "Scan with the Oxy app to continue",
|
|
391
|
+
"scanQr": "Scan QR code"
|
|
388
392
|
},
|
|
389
393
|
"reputation": {
|
|
390
394
|
"faq": {
|
|
@@ -1905,5 +1909,44 @@
|
|
|
1905
1909
|
"earlyAdopter": "Early Adopter",
|
|
1906
1910
|
"earlyAdopterDesc": "Been part of the community from the start"
|
|
1907
1911
|
}
|
|
1912
|
+
},
|
|
1913
|
+
"consent": {
|
|
1914
|
+
"title": "Continue to {{app}}",
|
|
1915
|
+
"subtitle": "Use your Oxy account to sign in to {{app}}. Review what this connection means before you continue.",
|
|
1916
|
+
"provenance": {
|
|
1917
|
+
"title": "Who is requesting access",
|
|
1918
|
+
"official": "Official Oxy application",
|
|
1919
|
+
"developer": "Published by {{developer}}",
|
|
1920
|
+
"thirdParty": "Third-party application"
|
|
1921
|
+
},
|
|
1922
|
+
"permissions": {
|
|
1923
|
+
"title": "Permissions requested",
|
|
1924
|
+
"basic": "Sign you in and read your basic profile"
|
|
1925
|
+
},
|
|
1926
|
+
"scopes": {
|
|
1927
|
+
"openid": "Confirm your identity",
|
|
1928
|
+
"profile": "Read your basic profile",
|
|
1929
|
+
"email": "Read your email address",
|
|
1930
|
+
"offlineAccess": "Keep you signed in when you're away",
|
|
1931
|
+
"userRead": "Read your basic profile",
|
|
1932
|
+
"filesRead": "Read your files",
|
|
1933
|
+
"filesWrite": "Upload and modify your files",
|
|
1934
|
+
"filesDelete": "Delete your files",
|
|
1935
|
+
"webhooksReceive": "Receive webhooks",
|
|
1936
|
+
"chatCompletions": "Use AI chat on your behalf",
|
|
1937
|
+
"modelsRead": "List available AI models",
|
|
1938
|
+
"federationWrite": "Act across federated services"
|
|
1939
|
+
},
|
|
1940
|
+
"account": {
|
|
1941
|
+
"title": "Signing in as"
|
|
1942
|
+
},
|
|
1943
|
+
"links": {
|
|
1944
|
+
"website": "Website",
|
|
1945
|
+
"privacy": "Privacy Policy",
|
|
1946
|
+
"terms": "Terms of Service"
|
|
1947
|
+
},
|
|
1948
|
+
"allow": "Continue to {{app}}",
|
|
1949
|
+
"deny": "Cancel",
|
|
1950
|
+
"disclaimer": "By continuing, {{app}} will be able to sign in with your Oxy account. You can manage connected apps anytime in your Oxy account settings."
|
|
1908
1951
|
}
|
|
1909
1952
|
}
|
|
@@ -581,7 +581,11 @@
|
|
|
581
581
|
"subtitle": "Añade otra cuenta para cambiar rápidamente entre ellas"
|
|
582
582
|
},
|
|
583
583
|
"label": "Selector de cuentas",
|
|
584
|
-
"searchPlaceholder": "Buscar cuentas"
|
|
584
|
+
"searchPlaceholder": "Buscar cuentas",
|
|
585
|
+
"scanTitle": "Escanea para iniciar sesión",
|
|
586
|
+
"scanSubtitle": "Abre la app de Oxy en tu móvil y escanea este código",
|
|
587
|
+
"scanWithOxy": "Escanea con la app de Oxy para continuar",
|
|
588
|
+
"scanQr": "Escanear código QR"
|
|
585
589
|
},
|
|
586
590
|
"feedback": {
|
|
587
591
|
"type": {
|
|
@@ -1905,5 +1909,44 @@
|
|
|
1905
1909
|
"earlyAdopter": "Pionero",
|
|
1906
1910
|
"earlyAdopterDesc": "Has formado parte de la comunidad desde el principio"
|
|
1907
1911
|
}
|
|
1912
|
+
},
|
|
1913
|
+
"consent": {
|
|
1914
|
+
"title": "Continuar a {{app}}",
|
|
1915
|
+
"subtitle": "Usa tu cuenta de Oxy para iniciar sesión en {{app}}. Revisa qué implica esta conexión antes de continuar.",
|
|
1916
|
+
"provenance": {
|
|
1917
|
+
"title": "Quién solicita acceso",
|
|
1918
|
+
"official": "Aplicación oficial de Oxy",
|
|
1919
|
+
"developer": "Publicada por {{developer}}",
|
|
1920
|
+
"thirdParty": "Aplicación de terceros"
|
|
1921
|
+
},
|
|
1922
|
+
"permissions": {
|
|
1923
|
+
"title": "Permisos solicitados",
|
|
1924
|
+
"basic": "Iniciar sesión y leer tu perfil básico"
|
|
1925
|
+
},
|
|
1926
|
+
"scopes": {
|
|
1927
|
+
"openid": "Confirmar tu identidad",
|
|
1928
|
+
"profile": "Leer tu perfil básico",
|
|
1929
|
+
"email": "Leer tu dirección de correo",
|
|
1930
|
+
"offlineAccess": "Mantener tu sesión iniciada cuando no estés",
|
|
1931
|
+
"userRead": "Leer tu perfil básico",
|
|
1932
|
+
"filesRead": "Leer tus archivos",
|
|
1933
|
+
"filesWrite": "Subir y modificar tus archivos",
|
|
1934
|
+
"filesDelete": "Eliminar tus archivos",
|
|
1935
|
+
"webhooksReceive": "Recibir webhooks",
|
|
1936
|
+
"chatCompletions": "Usar el chat con IA en tu nombre",
|
|
1937
|
+
"modelsRead": "Ver los modelos de IA disponibles",
|
|
1938
|
+
"federationWrite": "Actuar en servicios federados"
|
|
1939
|
+
},
|
|
1940
|
+
"account": {
|
|
1941
|
+
"title": "Iniciando sesión como"
|
|
1942
|
+
},
|
|
1943
|
+
"links": {
|
|
1944
|
+
"website": "Sitio web",
|
|
1945
|
+
"privacy": "Política de privacidad",
|
|
1946
|
+
"terms": "Términos del servicio"
|
|
1947
|
+
},
|
|
1948
|
+
"allow": "Continuar a {{app}}",
|
|
1949
|
+
"deny": "Cancelar",
|
|
1950
|
+
"disclaimer": "Al continuar, {{app}} podrá iniciar sesión con tu cuenta de Oxy. Puedes gestionar las apps conectadas cuando quieras en los ajustes de tu cuenta de Oxy."
|
|
1908
1951
|
}
|
|
1909
1952
|
}
|
|
@@ -384,7 +384,11 @@
|
|
|
384
384
|
"subtitle": "Add another account to switch between them quickly"
|
|
385
385
|
},
|
|
386
386
|
"label": "Account switcher",
|
|
387
|
-
"searchPlaceholder": "Search accounts"
|
|
387
|
+
"searchPlaceholder": "Search accounts",
|
|
388
|
+
"scanTitle": "Scan to sign in",
|
|
389
|
+
"scanSubtitle": "Open the Oxy app on your phone and scan this code",
|
|
390
|
+
"scanWithOxy": "Scan with the Oxy app to continue",
|
|
391
|
+
"scanQr": "Scan QR code"
|
|
388
392
|
},
|
|
389
393
|
"reputation": {
|
|
390
394
|
"faq": {
|
|
@@ -1905,5 +1909,44 @@
|
|
|
1905
1909
|
"earlyAdopter": "Early Adopter",
|
|
1906
1910
|
"earlyAdopterDesc": "Been part of the community from the start"
|
|
1907
1911
|
}
|
|
1912
|
+
},
|
|
1913
|
+
"consent": {
|
|
1914
|
+
"title": "Continue to {{app}}",
|
|
1915
|
+
"subtitle": "Use your Oxy account to sign in to {{app}}. Review what this connection means before you continue.",
|
|
1916
|
+
"provenance": {
|
|
1917
|
+
"title": "Who is requesting access",
|
|
1918
|
+
"official": "Official Oxy application",
|
|
1919
|
+
"developer": "Published by {{developer}}",
|
|
1920
|
+
"thirdParty": "Third-party application"
|
|
1921
|
+
},
|
|
1922
|
+
"permissions": {
|
|
1923
|
+
"title": "Permissions requested",
|
|
1924
|
+
"basic": "Sign you in and read your basic profile"
|
|
1925
|
+
},
|
|
1926
|
+
"scopes": {
|
|
1927
|
+
"openid": "Confirm your identity",
|
|
1928
|
+
"profile": "Read your basic profile",
|
|
1929
|
+
"email": "Read your email address",
|
|
1930
|
+
"offlineAccess": "Keep you signed in when you're away",
|
|
1931
|
+
"userRead": "Read your basic profile",
|
|
1932
|
+
"filesRead": "Read your files",
|
|
1933
|
+
"filesWrite": "Upload and modify your files",
|
|
1934
|
+
"filesDelete": "Delete your files",
|
|
1935
|
+
"webhooksReceive": "Receive webhooks",
|
|
1936
|
+
"chatCompletions": "Use AI chat on your behalf",
|
|
1937
|
+
"modelsRead": "List available AI models",
|
|
1938
|
+
"federationWrite": "Act across federated services"
|
|
1939
|
+
},
|
|
1940
|
+
"account": {
|
|
1941
|
+
"title": "Signing in as"
|
|
1942
|
+
},
|
|
1943
|
+
"links": {
|
|
1944
|
+
"website": "Website",
|
|
1945
|
+
"privacy": "Privacy Policy",
|
|
1946
|
+
"terms": "Terms of Service"
|
|
1947
|
+
},
|
|
1948
|
+
"allow": "Continue to {{app}}",
|
|
1949
|
+
"deny": "Cancel",
|
|
1950
|
+
"disclaimer": "By continuing, {{app}} will be able to sign in with your Oxy account. You can manage connected apps anytime in your Oxy account settings."
|
|
1908
1951
|
}
|
|
1909
|
-
}
|
|
1952
|
+
}
|
|
@@ -581,7 +581,11 @@
|
|
|
581
581
|
"subtitle": "Añade otra cuenta para cambiar rápidamente entre ellas"
|
|
582
582
|
},
|
|
583
583
|
"label": "Selector de cuentas",
|
|
584
|
-
"searchPlaceholder": "Buscar cuentas"
|
|
584
|
+
"searchPlaceholder": "Buscar cuentas",
|
|
585
|
+
"scanTitle": "Escanea para iniciar sesión",
|
|
586
|
+
"scanSubtitle": "Abre la app de Oxy en tu móvil y escanea este código",
|
|
587
|
+
"scanWithOxy": "Escanea con la app de Oxy para continuar",
|
|
588
|
+
"scanQr": "Escanear código QR"
|
|
585
589
|
},
|
|
586
590
|
"feedback": {
|
|
587
591
|
"type": {
|
|
@@ -1905,5 +1909,44 @@
|
|
|
1905
1909
|
"earlyAdopter": "Pionero",
|
|
1906
1910
|
"earlyAdopterDesc": "Has formado parte de la comunidad desde el principio"
|
|
1907
1911
|
}
|
|
1912
|
+
},
|
|
1913
|
+
"consent": {
|
|
1914
|
+
"title": "Continuar a {{app}}",
|
|
1915
|
+
"subtitle": "Usa tu cuenta de Oxy para iniciar sesión en {{app}}. Revisa qué implica esta conexión antes de continuar.",
|
|
1916
|
+
"provenance": {
|
|
1917
|
+
"title": "Quién solicita acceso",
|
|
1918
|
+
"official": "Aplicación oficial de Oxy",
|
|
1919
|
+
"developer": "Publicada por {{developer}}",
|
|
1920
|
+
"thirdParty": "Aplicación de terceros"
|
|
1921
|
+
},
|
|
1922
|
+
"permissions": {
|
|
1923
|
+
"title": "Permisos solicitados",
|
|
1924
|
+
"basic": "Iniciar sesión y leer tu perfil básico"
|
|
1925
|
+
},
|
|
1926
|
+
"scopes": {
|
|
1927
|
+
"openid": "Confirmar tu identidad",
|
|
1928
|
+
"profile": "Leer tu perfil básico",
|
|
1929
|
+
"email": "Leer tu dirección de correo",
|
|
1930
|
+
"offlineAccess": "Mantener tu sesión iniciada cuando no estés",
|
|
1931
|
+
"userRead": "Leer tu perfil básico",
|
|
1932
|
+
"filesRead": "Leer tus archivos",
|
|
1933
|
+
"filesWrite": "Subir y modificar tus archivos",
|
|
1934
|
+
"filesDelete": "Eliminar tus archivos",
|
|
1935
|
+
"webhooksReceive": "Recibir webhooks",
|
|
1936
|
+
"chatCompletions": "Usar el chat con IA en tu nombre",
|
|
1937
|
+
"modelsRead": "Ver los modelos de IA disponibles",
|
|
1938
|
+
"federationWrite": "Actuar en servicios federados"
|
|
1939
|
+
},
|
|
1940
|
+
"account": {
|
|
1941
|
+
"title": "Iniciando sesión como"
|
|
1942
|
+
},
|
|
1943
|
+
"links": {
|
|
1944
|
+
"website": "Sitio web",
|
|
1945
|
+
"privacy": "Política de privacidad",
|
|
1946
|
+
"terms": "Términos del servicio"
|
|
1947
|
+
},
|
|
1948
|
+
"allow": "Continuar a {{app}}",
|
|
1949
|
+
"deny": "Cancelar",
|
|
1950
|
+
"disclaimer": "Al continuar, {{app}} podrá iniciar sesión con tu cuenta de Oxy. Puedes gestionar las apps conectadas cuando quieras en los ajustes de tu cuenta de Oxy."
|
|
1908
1951
|
}
|
|
1909
|
-
}
|
|
1952
|
+
}
|
package/dist/esm/index.js
CHANGED
|
@@ -119,28 +119,26 @@ export { buildAccountsArray, createQuickAccount, getAccountDisplayName, getAccou
|
|
|
119
119
|
// ---------------------------------------------------------------------------
|
|
120
120
|
// Registrable-domain + central-IdP-apex helpers.
|
|
121
121
|
//
|
|
122
|
-
//
|
|
123
|
-
//
|
|
124
|
-
//
|
|
125
|
-
//
|
|
126
|
-
// `packages/api/src/utils/sameSite.ts` for same-site origin checks, and
|
|
127
|
-
// `CENTRAL_IDP_APEX` by `server/cors.ts`'s `createOxyCors` (auto-allows
|
|
128
|
-
// `*.oxy.so`). `SSO_CALLBACK_PATH` has no remaining importer outside this
|
|
129
|
-
// module as of wave 2 — kept exported for now rather than removed here (an
|
|
130
|
-
// export deletion is a logic change, out of scope for a comment sweep); flag
|
|
131
|
-
// for a follow-up dead-export cleanup pass.
|
|
122
|
+
// `registrableApex` (eTLD+1) is consumed via the `@oxyhq/core/server`
|
|
123
|
+
// re-export by `packages/api/src/utils/sameSite.ts` for same-site origin
|
|
124
|
+
// checks; `CENTRAL_IDP_APEX` by `server/cors.ts`'s `createOxyCors` (auto-allows
|
|
125
|
+
// `*.oxy.so`).
|
|
132
126
|
// ---------------------------------------------------------------------------
|
|
133
127
|
export { registrableApex } from './utils/registrableApex.js';
|
|
134
128
|
export { CENTRAL_IDP_APEX } from './utils/authWebUrl.js';
|
|
135
|
-
export { SSO_CALLBACK_PATH } from './utils/ssoBounce.js';
|
|
136
129
|
export { runColdBoot } from './utils/coldBoot.js';
|
|
137
130
|
// ---------------------------------------------------------------------------
|
|
131
|
+
// OAuth 2.0 Authorization Code + PKCE helpers ("Sign in with Oxy" third party).
|
|
132
|
+
// Standard OAuth against auth.oxy.so/authorize — no FedCM/cookies/SSO bounce.
|
|
133
|
+
// ---------------------------------------------------------------------------
|
|
134
|
+
export { buildOAuthAuthorizeUrl, computeCodeChallenge, generateOAuthState, generatePkcePair, DEFAULT_OAUTH_SCOPE, OXY_AUTHORIZE_URL, } from './utils/oauthPkce.js';
|
|
135
|
+
// ---------------------------------------------------------------------------
|
|
138
136
|
// Session sync (device-scoped multi-account session client)
|
|
139
137
|
// ---------------------------------------------------------------------------
|
|
140
138
|
export { SessionClient } from './session/SessionClient.js';
|
|
141
139
|
// Shared SessionClient integration layer: the host adapter, the pure
|
|
142
140
|
// DeviceSessionState projection helpers, and the client factory are defined
|
|
143
|
-
// ONCE here so `@oxyhq/services`
|
|
141
|
+
// ONCE here so every `@oxyhq/services` platform variant reuses them instead of
|
|
144
142
|
// duplicating a local copy. Each consumer supplies its own `TokenTransport`
|
|
145
143
|
// (native vs. web mint strategies differ) to `createSessionClient`.
|
|
146
144
|
export { createSessionClientHost } from './session/sessionClientHost.js';
|
|
@@ -149,7 +147,7 @@ export { deviceStateToClientSessions, activeSessionIdOf, activeUserOf, accountId
|
|
|
149
147
|
// Unified account-list projection (THE single source of truth for the account
|
|
150
148
|
// chooser: device sign-ins ∪ account graph, deduped by accountId). Pure +
|
|
151
149
|
// I/O-free — the caller hydrates profiles via `getUsersByIds`. Shared by
|
|
152
|
-
// `@oxyhq/services
|
|
150
|
+
// `@oxyhq/services` and auth.oxy.so so the list can't diverge.
|
|
153
151
|
export { projectSwitchableAccounts, switchableAccountIds, } from './session/accountProjection.js';
|
|
154
152
|
// Headless controller for the unified account dialog. Framework-agnostic
|
|
155
153
|
// state machine + subscribe/getSnapshot store (bind via `useSyncExternalStore`)
|
package/dist/esm/server/index.js
CHANGED
|
@@ -27,8 +27,3 @@ export { verifySecret } from './verifySecret.js';
|
|
|
27
27
|
// Pure host handling (no browser deps), so it is safe on the server subpath and
|
|
28
28
|
// lets `@oxyhq/api` derive `auth.<apex>` without duplicating PSL logic.
|
|
29
29
|
export { registrableApex } from '../utils/registrableApex.js';
|
|
30
|
-
// The single RP callback path the IdP redirects back to. A pure wire-contract
|
|
31
|
-
// constant (no browser deps at module top level), re-used server-side so the
|
|
32
|
-
// `/sso/establish-token` `return_to` cannot drift from what `/sso/establish`
|
|
33
|
-
// validates.
|
|
34
|
-
export { SSO_CALLBACK_PATH } from '../utils/ssoBounce.js';
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* A framework-agnostic state machine + subscribe/getSnapshot store (the same
|
|
5
5
|
* pattern {@link SessionClient} uses — no React, no RN) that both
|
|
6
|
-
*
|
|
6
|
+
* every `OxyProvider` platform variant (Expo/RN and RN-Web)
|
|
7
7
|
* bind to via `useSyncExternalStore`, so the account chooser is ONE
|
|
8
8
|
* implementation across the ecosystem instead of the five drifting copies it
|
|
9
9
|
* replaces.
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* merging the device's server-authoritative session set (`DeviceSessionState`
|
|
6
6
|
* from {@link SessionClient}) with the caller's account graph (`AccountNode[]`
|
|
7
7
|
* from `oxyServices.listAccounts()`), deduped by `accountId`. This lives in
|
|
8
|
-
* `@oxyhq/core` so `@oxyhq/services`
|
|
8
|
+
* `@oxyhq/core` so every `@oxyhq/services` platform variant — and
|
|
9
9
|
* `auth.oxy.so` — all render the SAME list from the SAME logic and cannot
|
|
10
10
|
* diverge.
|
|
11
11
|
*
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Pure projection helpers: `DeviceSessionState` (the device-scoped
|
|
3
3
|
* multi-account session-sync state produced by `SessionClient`) -> the
|
|
4
|
-
* shapes
|
|
4
|
+
* shapes `@oxyhq/services` consumers render today
|
|
5
5
|
* (`ClientSession[]`, an active session id, an active `User`).
|
|
6
6
|
*
|
|
7
7
|
* No I/O. The caller fetches profiles via
|
|
@@ -4,8 +4,7 @@
|
|
|
4
4
|
* `SessionClient` is host-agnostic: it only needs a REST + token surface.
|
|
5
5
|
* `OxyServices` already exposes all of that except `getCurrentAccountId`,
|
|
6
6
|
* which has no direct equivalent — the adapter holds a mutable ref set by
|
|
7
|
-
* the caller (`OxyContext` in `@oxyhq/services
|
|
8
|
-
* `@oxyhq/auth`) via `setCurrentAccountId`.
|
|
7
|
+
* the caller (`OxyContext` in `@oxyhq/services`) via `setCurrentAccountId`.
|
|
9
8
|
*
|
|
10
9
|
* Shared here (rather than duplicated per consumer) because it is entirely
|
|
11
10
|
* platform-agnostic: every method it calls exists identically on
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Shared account types and pure helper functions.
|
|
3
|
-
* Used by
|
|
3
|
+
* Used by the @oxyhq/services account stores (Expo/RN and RN-Web).
|
|
4
4
|
*/
|
|
5
5
|
import { translate } from '../i18n/index.js';
|
|
6
6
|
/**
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OAuth 2.0 Authorization Code + PKCE helpers for "Sign in with Oxy" third-party
|
|
3
|
+
* sign-in.
|
|
4
|
+
*
|
|
5
|
+
* Third-party Relying Parties (SPAs, static sites, and native apps that are NOT
|
|
6
|
+
* Oxy first-party) authenticate through the standard OAuth flow against
|
|
7
|
+
* `auth.oxy.so/authorize` — never FedCM, SSO bounces, or Oxy session cookies.
|
|
8
|
+
* Public clients (no secret) prove possession of the authorization code with
|
|
9
|
+
* PKCE (RFC 7636, S256): the RP generates a random `code_verifier`, sends its
|
|
10
|
+
* `code_challenge = BASE64URL(SHA-256(code_verifier))` on the authorize
|
|
11
|
+
* redirect, and later replays the raw verifier on the token exchange.
|
|
12
|
+
*
|
|
13
|
+
* All cross-platform crypto (random bytes, SHA-256) is delegated to the shared
|
|
14
|
+
* `@oxyhq/protocol` platform loaders — the exact primitives the rest of core's
|
|
15
|
+
* crypto already uses — so these helpers run identically on web, Node, and
|
|
16
|
+
* React Native. No `require()`, so the ESM build stays bundler-clean.
|
|
17
|
+
*/
|
|
18
|
+
import { isNodeJS, isReactNative, loadExpoCrypto, loadNodeCrypto, sha256 } from '@oxyhq/protocol';
|
|
19
|
+
import { logger } from './loggerUtils.js';
|
|
20
|
+
/** The central Oxy IdP authorization endpoint used by default. */
|
|
21
|
+
export const OXY_AUTHORIZE_URL = 'https://auth.oxy.so/authorize';
|
|
22
|
+
/** Default OAuth scope requested for a "Sign in with Oxy" third-party flow. */
|
|
23
|
+
export const DEFAULT_OAUTH_SCOPE = 'openid profile';
|
|
24
|
+
/**
|
|
25
|
+
* Number of random bytes behind a PKCE `code_verifier`. 64 bytes → 86 base64url
|
|
26
|
+
* characters, comfortably inside RFC 7636 §4.1's required 43–128 range.
|
|
27
|
+
*/
|
|
28
|
+
const PKCE_VERIFIER_BYTES = 64;
|
|
29
|
+
/** Number of random bytes behind an OAuth `state` (CSRF) token. */
|
|
30
|
+
const OAUTH_STATE_BYTES = 32;
|
|
31
|
+
/** RFC 4648 §5 base64url alphabet (URL- and filename-safe, no padding). */
|
|
32
|
+
const BASE64URL_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_';
|
|
33
|
+
/**
|
|
34
|
+
* Cryptographically-secure random bytes, cross-platform.
|
|
35
|
+
*
|
|
36
|
+
* Mirrors the platform gating the rest of core's crypto already uses:
|
|
37
|
+
* `expo-crypto` on React Native, Node's built-in `crypto` on the server, and
|
|
38
|
+
* the Web Crypto API in the browser (also the fallback if Node's `crypto`
|
|
39
|
+
* fails to load in an unusual bundled-Node environment).
|
|
40
|
+
*/
|
|
41
|
+
async function getSecureRandomBytes(byteLength) {
|
|
42
|
+
if (isReactNative()) {
|
|
43
|
+
const crypto = await loadExpoCrypto();
|
|
44
|
+
return Uint8Array.from(await crypto.getRandomBytesAsync(byteLength));
|
|
45
|
+
}
|
|
46
|
+
if (isNodeJS()) {
|
|
47
|
+
try {
|
|
48
|
+
const nodeCrypto = await loadNodeCrypto();
|
|
49
|
+
return Uint8Array.from(nodeCrypto.randomBytes(byteLength));
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
logger.warn('[oxy.oauth] Node crypto unavailable for PKCE random bytes, falling back to Web Crypto', { component: 'oauthPkce' }, error);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
const bytes = new Uint8Array(byteLength);
|
|
56
|
+
globalThis.crypto.getRandomValues(bytes);
|
|
57
|
+
return bytes;
|
|
58
|
+
}
|
|
59
|
+
/** Encode raw bytes as unpadded base64url (RFC 4648 §5). */
|
|
60
|
+
function bytesToBase64Url(bytes) {
|
|
61
|
+
let output = '';
|
|
62
|
+
for (let i = 0; i < bytes.length; i += 3) {
|
|
63
|
+
const byte0 = bytes[i];
|
|
64
|
+
const hasByte1 = i + 1 < bytes.length;
|
|
65
|
+
const hasByte2 = i + 2 < bytes.length;
|
|
66
|
+
const byte1 = hasByte1 ? bytes[i + 1] : 0;
|
|
67
|
+
const byte2 = hasByte2 ? bytes[i + 2] : 0;
|
|
68
|
+
output += BASE64URL_ALPHABET[byte0 >> 2];
|
|
69
|
+
output += BASE64URL_ALPHABET[((byte0 & 0x03) << 4) | (byte1 >> 4)];
|
|
70
|
+
if (hasByte1) {
|
|
71
|
+
output += BASE64URL_ALPHABET[((byte1 & 0x0f) << 2) | (byte2 >> 6)];
|
|
72
|
+
}
|
|
73
|
+
if (hasByte2) {
|
|
74
|
+
output += BASE64URL_ALPHABET[byte2 & 0x3f];
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return output;
|
|
78
|
+
}
|
|
79
|
+
/** Decode a lowercase-hex string into its raw bytes. */
|
|
80
|
+
function hexToBytes(hex) {
|
|
81
|
+
const bytes = new Uint8Array(hex.length / 2);
|
|
82
|
+
for (let i = 0; i < bytes.length; i += 1) {
|
|
83
|
+
bytes[i] = Number.parseInt(hex.slice(i * 2, i * 2 + 2), 16);
|
|
84
|
+
}
|
|
85
|
+
return bytes;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Compute the PKCE S256 `code_challenge` for a given verifier:
|
|
89
|
+
* `BASE64URL(SHA-256(ASCII(codeVerifier)))` (RFC 7636 §4.2). The verifier is
|
|
90
|
+
* base64url (ASCII), so its UTF-8 and ASCII byte encodings are identical.
|
|
91
|
+
*
|
|
92
|
+
* Reuses `@oxyhq/protocol`'s cross-platform {@link sha256} (which returns
|
|
93
|
+
* lowercase hex); the digest bytes are recovered and re-encoded as base64url.
|
|
94
|
+
*/
|
|
95
|
+
export async function computeCodeChallenge(codeVerifier) {
|
|
96
|
+
const digestHex = await sha256(codeVerifier);
|
|
97
|
+
return bytesToBase64Url(hexToBytes(digestHex));
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Generate a fresh PKCE verifier/challenge pair for an OAuth authorization-code
|
|
101
|
+
* flow. The verifier is 64 random bytes as base64url (86 chars, within RFC 7636
|
|
102
|
+
* §4.1's 43–128 range and drawn only from the unreserved set); the challenge is
|
|
103
|
+
* its S256 transform.
|
|
104
|
+
*/
|
|
105
|
+
export async function generatePkcePair() {
|
|
106
|
+
const codeVerifier = bytesToBase64Url(await getSecureRandomBytes(PKCE_VERIFIER_BYTES));
|
|
107
|
+
const codeChallenge = await computeCodeChallenge(codeVerifier);
|
|
108
|
+
return { codeVerifier, codeChallenge, method: 'S256' };
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Generate an opaque, single-use OAuth `state` token (32 random bytes as
|
|
112
|
+
* base64url) for CSRF protection across the authorize redirect.
|
|
113
|
+
*/
|
|
114
|
+
export async function generateOAuthState() {
|
|
115
|
+
return bytesToBase64Url(await getSecureRandomBytes(OAUTH_STATE_BYTES));
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Build the `auth.oxy.so/authorize` redirect URL for an OAuth authorization-code
|
|
119
|
+
* + PKCE (S256) flow. Built via the WHATWG `URL` API so a custom
|
|
120
|
+
* `authorizeBaseUrl` that already carries a query string keeps its existing
|
|
121
|
+
* params (the OAuth params are merged in, not clobbered by a naive `?` concat).
|
|
122
|
+
* All values are percent-encoded by `URL.searchParams`.
|
|
123
|
+
*/
|
|
124
|
+
export function buildOAuthAuthorizeUrl(params) {
|
|
125
|
+
const { authorizeBaseUrl = OXY_AUTHORIZE_URL, clientId, redirectUri, scope = DEFAULT_OAUTH_SCOPE, state, codeChallenge, } = params;
|
|
126
|
+
const url = new URL(authorizeBaseUrl);
|
|
127
|
+
url.searchParams.set('client_id', clientId);
|
|
128
|
+
url.searchParams.set('redirect_uri', redirectUri);
|
|
129
|
+
url.searchParams.set('response_type', 'code');
|
|
130
|
+
url.searchParams.set('state', state);
|
|
131
|
+
url.searchParams.set('scope', scope);
|
|
132
|
+
url.searchParams.set('code_challenge', codeChallenge);
|
|
133
|
+
url.searchParams.set('code_challenge_method', 'S256');
|
|
134
|
+
return url.toString();
|
|
135
|
+
}
|
|
@@ -89,7 +89,7 @@ export function setPlatformOS(os) {
|
|
|
89
89
|
*
|
|
90
90
|
* Native defines a global `window` but no `document`, so the DOM probe — not a
|
|
91
91
|
* bare `window` check — is the reliable discriminator. This is the single
|
|
92
|
-
* source of truth consumed by `@oxyhq/services`
|
|
92
|
+
* source of truth consumed by `@oxyhq/services` (which dropped
|
|
93
93
|
* their local copies), so every consumer shares the exact same predicate.
|
|
94
94
|
*
|
|
95
95
|
* NOTE: this is a live runtime probe (not the cached `getPlatformOS()` verdict)
|