@oxyhq/core 7.1.1 → 8.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/README.md +48 -24
  2. package/dist/cjs/.tsbuildinfo +1 -1
  3. package/dist/cjs/HttpService.js +6 -6
  4. package/dist/cjs/boot/coldBootV2.js +97 -2
  5. package/dist/cjs/boot/deviceBootReturn.js +15 -0
  6. package/dist/cjs/i18n/locales/en-US.json +44 -1
  7. package/dist/cjs/i18n/locales/es-ES.json +44 -1
  8. package/dist/cjs/i18n/locales/locales/en-US.json +45 -2
  9. package/dist/cjs/i18n/locales/locales/es-ES.json +45 -2
  10. package/dist/cjs/index.js +19 -16
  11. package/dist/cjs/mixins/OxyServices.deviceBoot.js +28 -0
  12. package/dist/cjs/server/index.js +1 -7
  13. package/dist/cjs/session/accountDialogController.js +1 -1
  14. package/dist/cjs/session/accountProjection.js +1 -1
  15. package/dist/cjs/session/authStateStore.js +6 -0
  16. package/dist/cjs/session/projectSessionState.js +1 -1
  17. package/dist/cjs/session/refresh.js +9 -0
  18. package/dist/cjs/session/sessionClientHost.js +1 -2
  19. package/dist/cjs/utils/accountUtils.js +1 -1
  20. package/dist/cjs/utils/oauthPkce.js +142 -0
  21. package/dist/cjs/utils/platform.js +1 -1
  22. package/dist/esm/.tsbuildinfo +1 -1
  23. package/dist/esm/HttpService.js +6 -6
  24. package/dist/esm/boot/coldBootV2.js +97 -2
  25. package/dist/esm/boot/deviceBootReturn.js +15 -0
  26. package/dist/esm/i18n/locales/en-US.json +44 -1
  27. package/dist/esm/i18n/locales/es-ES.json +44 -1
  28. package/dist/esm/i18n/locales/locales/en-US.json +45 -2
  29. package/dist/esm/i18n/locales/locales/es-ES.json +45 -2
  30. package/dist/esm/index.js +11 -13
  31. package/dist/esm/mixins/OxyServices.deviceBoot.js +29 -1
  32. package/dist/esm/server/index.js +0 -5
  33. package/dist/esm/session/accountDialogController.js +1 -1
  34. package/dist/esm/session/accountProjection.js +1 -1
  35. package/dist/esm/session/authStateStore.js +6 -0
  36. package/dist/esm/session/projectSessionState.js +1 -1
  37. package/dist/esm/session/refresh.js +9 -0
  38. package/dist/esm/session/sessionClientHost.js +1 -2
  39. package/dist/esm/utils/accountUtils.js +1 -1
  40. package/dist/esm/utils/oauthPkce.js +135 -0
  41. package/dist/esm/utils/platform.js +1 -1
  42. package/dist/types/.tsbuildinfo +1 -1
  43. package/dist/types/HttpService.d.ts +1 -1
  44. package/dist/types/index.d.ts +3 -2
  45. package/dist/types/mixins/OxyServices.accounts.d.ts +13 -3
  46. package/dist/types/mixins/OxyServices.connectedApps.d.ts +4 -0
  47. package/dist/types/mixins/OxyServices.deviceBoot.d.ts +17 -1
  48. package/dist/types/mixins/OxyServices.devices.d.ts +3 -2
  49. package/dist/types/models/interfaces.d.ts +4 -4
  50. package/dist/types/server/index.d.ts +0 -1
  51. package/dist/types/session/accountDialogController.d.ts +1 -1
  52. package/dist/types/session/accountProjection.d.ts +1 -1
  53. package/dist/types/session/authStateStore.d.ts +20 -0
  54. package/dist/types/session/projectSessionState.d.ts +1 -1
  55. package/dist/types/session/refresh.d.ts +4 -8
  56. package/dist/types/session/sessionClientHost.d.ts +1 -2
  57. package/dist/types/utils/accountUtils.d.ts +1 -1
  58. package/dist/types/utils/oauthPkce.d.ts +74 -0
  59. package/dist/types/utils/platform.d.ts +1 -1
  60. package/package.json +3 -3
  61. package/src/HttpService.ts +6 -6
  62. package/src/boot/__tests__/coldBootV2.test.ts +215 -1
  63. package/src/boot/__tests__/deviceBootReturn.test.ts +32 -0
  64. package/src/boot/coldBootV2.ts +117 -2
  65. package/src/boot/deviceBootReturn.ts +15 -0
  66. package/src/i18n/locales/en-US.json +45 -2
  67. package/src/i18n/locales/es-ES.json +45 -2
  68. package/src/index.ts +23 -16
  69. package/src/mixins/OxyServices.accounts.ts +12 -0
  70. package/src/mixins/OxyServices.connectedApps.ts +4 -0
  71. package/src/mixins/OxyServices.deviceBoot.ts +38 -0
  72. package/src/mixins/OxyServices.devices.ts +6 -5
  73. package/src/mixins/__tests__/OxyServices.deviceBoot.test.ts +44 -1
  74. package/src/mixins/__tests__/accounts.test.ts +1 -1
  75. package/src/models/interfaces.ts +7 -5
  76. package/src/server/index.ts +0 -6
  77. package/src/session/__tests__/authStateStore.test.ts +27 -0
  78. package/src/session/__tests__/refresh.test.ts +14 -0
  79. package/src/session/accountDialogController.ts +1 -1
  80. package/src/session/accountProjection.ts +1 -1
  81. package/src/session/authStateStore.ts +26 -0
  82. package/src/session/projectSessionState.ts +1 -1
  83. package/src/session/refresh.ts +13 -8
  84. package/src/session/sessionClientHost.ts +1 -2
  85. package/src/utils/__tests__/coldBoot.test.ts +55 -65
  86. package/src/utils/__tests__/oauthPkce.test.ts +154 -0
  87. package/src/utils/accountUtils.ts +1 -1
  88. package/src/utils/oauthPkce.ts +189 -0
  89. package/src/utils/platform.ts +1 -1
  90. package/dist/cjs/utils/ssoBounce.js +0 -24
  91. package/dist/esm/utils/ssoBounce.js +0 -21
  92. package/dist/types/utils/ssoBounce.d.ts +0 -21
  93. package/src/utils/ssoBounce.ts +0 -22
@@ -56,7 +56,7 @@ const CSRF_FETCH_RETRY_DELAY_MS = 500;
56
56
  /**
57
57
  * Cooldown (ms) applied after a failed access-token refresh before another
58
58
  * refresh is attempted. Prevents a refresh storm (and server hammering) when
59
- * the AuthManager's refresh handler is failing — every in-flight request that
59
+ * the auth refresh handler is failing — every in-flight request that
60
60
  * hits a 401 would otherwise trigger its own refresh.
61
61
  */
62
62
  const TOKEN_REFRESH_COOLDOWN_MS = 15000;
@@ -143,7 +143,7 @@ class HttpService {
143
143
  this.cacheSizeWarningSilentUntil = 0;
144
144
  /**
145
145
  * Fan-out listeners notified on EVERY access-token change on this instance:
146
- * explicit `setTokens`, `clearTokens`, an AuthManager-owned refresh, and the
146
+ * explicit `setTokens`, `clearTokens`, a refresh-handler rotation, and the
147
147
  * internal 401-driven clear. This is a Set so multiple independent observers
148
148
  * can mirror token state without clobbering each other.
149
149
  *
@@ -361,9 +361,9 @@ class HttpService {
361
361
  clearTimeout(timeoutId);
362
362
  // Handle response
363
363
  if (!response.ok) {
364
- // On 401, delegate refresh to AuthManager and retry once before
365
- // giving up. HttpService deliberately does not know any session
366
- // routes; the AuthManager is the single session authority.
364
+ // On 401, delegate to the installed auth refresh handler and retry
365
+ // once before giving up. HttpService deliberately does not know any
366
+ // session routes; the refresh handler owns session rotation.
367
367
  if (response.status === 401 && !config._isAuthRetry && !config.skipAuth) {
368
368
  const refreshed = await this.refreshAccessToken('response-401');
369
369
  if (refreshed) {
@@ -823,7 +823,7 @@ class HttpService {
823
823
  this.tokenStore.setTokens(newToken);
824
824
  this.notifyTokenChange();
825
825
  }
826
- this.logger.debug('Token refreshed via AuthManager');
826
+ this.logger.debug('Token refreshed via the auth refresh handler');
827
827
  return newToken;
828
828
  })
829
829
  .catch((error) => {
@@ -34,6 +34,7 @@ exports.runSessionColdBoot = runSessionColdBoot;
34
34
  const contracts_1 = require("@oxyhq/contracts");
35
35
  const coldBoot_1 = require("../utils/coldBoot");
36
36
  const platform_1 = require("../utils/platform");
37
+ const errorUtils_1 = require("../utils/errorUtils");
37
38
  const keyManager_1 = require("../crypto/keyManager");
38
39
  const loggerUtils_1 = require("../utils/loggerUtils");
39
40
  const refresh_1 = require("../session/refresh");
@@ -165,6 +166,18 @@ function isSameApex(pageHost, apiHost) {
165
166
  function sessionFromPersisted(state, accessToken) {
166
167
  return { sessionId: state.sessionId, userId: state.userId, accessToken };
167
168
  }
169
+ function classifyMintFailure(error) {
170
+ if ((0, errorUtils_1.extractErrorStatus)(error) === 401) {
171
+ // Structural read (not `instanceof Error`): the thrown value can be a plain
172
+ // ApiError-shaped object or come from another realm, where instanceof fails
173
+ // and a `no_active_session` would be misread as a stale secret and dropped.
174
+ const message = error?.message;
175
+ return typeof message === 'string' && message.includes('no_active_session')
176
+ ? 'no_active_session'
177
+ : 'invalid_secret';
178
+ }
179
+ return 'transient';
180
+ }
168
181
  /**
169
182
  * Run the device-first cold boot. Resolves to the `runColdBoot` outcome and, as
170
183
  * a side effect, invokes `onSession` (winning session, token already planted)
@@ -180,7 +193,73 @@ async function runSessionColdBoot(opts) {
180
193
  // they cannot leak across boots or break under bundler re-evaluation.
181
194
  let signedOutReason = 'no_session';
182
195
  let navigating = false;
196
+ // Set when the zero-cookie mint reports `no_active_session` (phase 2c): the
197
+ // device is authoritatively signed out, so the migratory fallback lanes below
198
+ // (stored-tokens / shared-key / bootstrap-hop) must NOT run — we already know
199
+ // there is no session and must not bounce a known-signed-out device.
200
+ let deviceKnownSignedOut = false;
183
201
  const steps = [];
202
+ // 0. device-secret-mint (phase 2c) — the zero-cookie fast path. When the
203
+ // origin persisted a deviceId + deviceSecret, mint a short access token with
204
+ // a single bearer-less POST (no cookie, no navigation). FIRST in the chain
205
+ // so it wins over the migratory cookie lanes below. Gated OFF while a
206
+ // #oxy_boot return fragment is present so `bootstrap-return` still consumes
207
+ // + strips it first (a device holding a secret never triggers that hop, so
208
+ // this only defers a rare stale/forged fragment). The rest of the chain
209
+ // stays as the additive migratory fallback for devices not yet on the secret.
210
+ steps.push({
211
+ id: 'device-secret-mint',
212
+ enabled: () => !(isWeb && (0, deviceBootReturn_1.hashHasBootFragment)(dom.getHash())),
213
+ run: async () => {
214
+ const persisted = await store.load();
215
+ if (!persisted?.deviceId || !persisted?.deviceSecret) {
216
+ return { kind: 'skip' };
217
+ }
218
+ try {
219
+ const mint = await oxy.mintFromDeviceSecret(persisted.deviceId, persisted.deviceSecret);
220
+ // Rotation-in-use anti-loss: persist the NEXT secret (+ refreshed warm
221
+ // fields, + the server's authoritative active account) BEFORE planting
222
+ // the minted access token, so a multi-tab race that rotates again can
223
+ // never strand this tab with a superseded secret.
224
+ const active = mint.state.accounts.find((a) => a.accountId === mint.state.activeAccountId);
225
+ const next = {
226
+ ...persisted,
227
+ deviceId: mint.state.deviceId,
228
+ deviceSecret: mint.nextDeviceSecret,
229
+ accessToken: mint.accessToken,
230
+ expiresAt: mint.expiresAt,
231
+ ...(active ? { sessionId: active.sessionId, userId: active.accountId } : {}),
232
+ };
233
+ await store.save(next);
234
+ oxy.setTokens(mint.accessToken);
235
+ return {
236
+ kind: 'session',
237
+ session: { sessionId: next.sessionId, userId: next.userId, accessToken: mint.accessToken },
238
+ };
239
+ }
240
+ catch (error) {
241
+ const failure = classifyMintFailure(error);
242
+ if (failure === 'invalid_secret') {
243
+ // Stale/diverged secret — drop it so the mint lane stops firing, then
244
+ // fall through to the migratory refresh/cookie lanes. Setting it
245
+ // undefined drops the key on the store's JSON serialization, and the
246
+ // mint guard treats undefined as absent.
247
+ await store.save({ ...persisted, deviceSecret: undefined });
248
+ return { kind: 'skip' };
249
+ }
250
+ if (failure === 'no_active_session') {
251
+ // Device known, no live session — authoritative signed-out. KEEP the
252
+ // secret and stop the chain (do not bounce a known-signed-out device).
253
+ deviceKnownSignedOut = true;
254
+ signedOutReason = 'no_session';
255
+ return { kind: 'skip' };
256
+ }
257
+ // Transient (network / 5xx): keep the secret, let the fallback lanes try.
258
+ loggerUtils_1.logger.debug('device-secret mint failed (transient) — keeping secret, falling back', { component: 'coldBootV2', method: 'device-secret-mint' }, error);
259
+ return { kind: 'skip' };
260
+ }
261
+ },
262
+ });
184
263
  // 1. bootstrap-return (web) — consume a #oxy_boot fragment.
185
264
  steps.push({
186
265
  id: 'bootstrap-return',
@@ -214,6 +293,7 @@ async function runSessionColdBoot(opts) {
214
293
  // 2. stored-tokens — warm-plant or rotate the persisted refresh family.
215
294
  steps.push({
216
295
  id: 'stored-tokens',
296
+ enabled: () => !deviceKnownSignedOut,
217
297
  run: async () => {
218
298
  const persisted = await store.load();
219
299
  if (!persisted) {
@@ -240,7 +320,7 @@ async function runSessionColdBoot(opts) {
240
320
  // 3. shared-key-signin (native) — re-mint from the shared identity.
241
321
  steps.push({
242
322
  id: 'shared-key-signin',
243
- enabled: () => isNative,
323
+ enabled: () => isNative && !deviceKnownSignedOut,
244
324
  run: async () => {
245
325
  const session = await oxy.signInWithSharedIdentity();
246
326
  if (!session?.accessToken) {
@@ -273,7 +353,7 @@ async function runSessionColdBoot(opts) {
273
353
  // 4. bootstrap-hop (web, terminal) — same-apex inline fetch OR cross-apex nav.
274
354
  steps.push({
275
355
  id: 'bootstrap-hop',
276
- enabled: () => isWeb,
356
+ enabled: () => isWeb && !deviceKnownSignedOut,
277
357
  run: async () => {
278
358
  const pageHost = dom.getLocationHostname();
279
359
  let apiHost = null;
@@ -306,6 +386,21 @@ async function runSessionColdBoot(opts) {
306
386
  accessToken: bundle.accessToken,
307
387
  expiresAt: bundle.expiresAt,
308
388
  };
389
+ // Phase 2c: the web-session bundle may carry a rotating `deviceSecret`
390
+ // but NOT a deviceId. Persist the secret and carry any prior deviceId
391
+ // (from a deviceId-bearing login lane) forward so the mint lane stays
392
+ // usable — this overwrite must not orphan it.
393
+ const prior = await store.load();
394
+ if (prior?.deviceId) {
395
+ next.deviceId = prior.deviceId;
396
+ }
397
+ // Prefer the bundle's secret (the server just rotated onto it); keep the
398
+ // prior one when the bundle omits it — this lane also runs as the
399
+ // TRANSIENT-mint fallback, and must not orphan a still-valid secret.
400
+ const carriedSecret = bundle.deviceSecret ?? prior?.deviceSecret;
401
+ if (carriedSecret) {
402
+ next.deviceSecret = carriedSecret;
403
+ }
309
404
  await store.save(next);
310
405
  oxy.setTokens(bundle.accessToken);
311
406
  return { kind: 'session', session: sessionFromPersisted(next, bundle.accessToken) };
@@ -135,6 +135,21 @@ async function consumeDeviceBootReturn(deps) {
135
135
  accessToken: bundle.accessToken,
136
136
  expiresAt: bundle.expiresAt,
137
137
  };
138
+ // Phase 2c: the cookie-bootstrap bundle may carry a rotating `deviceSecret`
139
+ // but NOT a deviceId. Persist the secret, and carry any prior deviceId
140
+ // forward (from a deviceId-bearing login lane) so the pair stays usable by
141
+ // the zero-cookie mint — an overwrite here must not orphan the mint lane.
142
+ const prior = await deps.store.load();
143
+ if (prior?.deviceId) {
144
+ next.deviceId = prior.deviceId;
145
+ }
146
+ // Prefer the bundle's secret (the server just rotated onto it); keep the
147
+ // prior one when the bundle omits it so a cookie-lane boot can never orphan
148
+ // a still-valid secret captured by an earlier login lane.
149
+ const carriedSecret = bundle.deviceSecret ?? prior?.deviceSecret;
150
+ if (carriedSecret) {
151
+ next.deviceSecret = carriedSecret;
152
+ }
138
153
  await deps.store.save(next);
139
154
  deps.plantAccessToken(bundle.accessToken);
140
155
  return {
@@ -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/cjs/index.js CHANGED
@@ -20,8 +20,8 @@
20
20
  Object.defineProperty(exports, "__esModule", { value: true });
21
21
  exports.lightenColor = exports.darkenColor = exports.isWebBrowser = exports.isAndroid = 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.oxyClient = exports.OXY_CLOUD_URL = exports.OxyAuthenticationTimeoutError = exports.OxyAuthenticationError = exports.OxyServices = void 0;
22
22
  exports.isValidPassword = exports.isValidUsername = exports.isValidEmail = 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 = void 0;
23
- exports.AUTH_STATE_STORAGE_KEY = exports.createMemoryAuthStateStore = exports.createNativeAuthStateStore = exports.createWebAuthStateStore = exports.createAccountDialogController = exports.AccountDialogController = exports.switchableAccountIds = exports.projectSwitchableAccounts = exports.accountIdsOf = exports.activeUserOf = exports.activeSessionIdOf = exports.deviceStateToClientSessions = exports.createSessionClient = exports.createSessionClientHost = exports.SessionClient = exports.runColdBoot = exports.SSO_CALLBACK_PATH = exports.CENTRAL_IDP_APEX = exports.registrableApex = exports.getAccountColor = 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.isValidDisplayName = void 0;
24
- exports.packageInfo = exports.BOOT_STATE_SESSION_KEY = exports.BOOT_FRAGMENT_PARAM = exports.hashHasBootFragment = exports.parseDeviceBootFragment = exports.consumeDeviceBootReturn = exports.BOOT_ATTEMPTED_KEY = exports.isSameApex = exports.createBrowserColdBootDom = exports.runSessionColdBoot = exports.TOKEN_REFRESH_LEAD_MS = exports.startTokenRefreshScheduler = exports.installAuthRefreshHandler = exports.createAuthRefreshHandler = exports.refreshPersistedSession = exports.DEVICE_TOKEN_STORAGE_KEY = void 0;
23
+ exports.AccountDialogController = exports.switchableAccountIds = exports.projectSwitchableAccounts = exports.accountIdsOf = exports.activeUserOf = exports.activeSessionIdOf = exports.deviceStateToClientSessions = exports.createSessionClient = exports.createSessionClientHost = exports.SessionClient = exports.OXY_AUTHORIZE_URL = exports.DEFAULT_OAUTH_SCOPE = exports.generatePkcePair = exports.generateOAuthState = exports.computeCodeChallenge = exports.buildOAuthAuthorizeUrl = exports.runColdBoot = exports.CENTRAL_IDP_APEX = exports.registrableApex = exports.getAccountColor = 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.isValidDisplayName = void 0;
24
+ exports.packageInfo = exports.BOOT_STATE_SESSION_KEY = exports.BOOT_FRAGMENT_PARAM = exports.hashHasBootFragment = exports.parseDeviceBootFragment = exports.consumeDeviceBootReturn = exports.BOOT_ATTEMPTED_KEY = exports.isSameApex = exports.createBrowserColdBootDom = exports.runSessionColdBoot = exports.TOKEN_REFRESH_LEAD_MS = exports.startTokenRefreshScheduler = exports.installAuthRefreshHandler = exports.createAuthRefreshHandler = exports.refreshPersistedSession = exports.DEVICE_TOKEN_STORAGE_KEY = exports.AUTH_STATE_STORAGE_KEY = exports.createMemoryAuthStateStore = exports.createNativeAuthStateStore = exports.createWebAuthStateStore = exports.createAccountDialogController = void 0;
25
25
  // Ensure crypto polyfills are loaded before anything else
26
26
  require("./crypto/polyfill");
27
27
  // ---------------------------------------------------------------------------
@@ -256,33 +256,36 @@ Object.defineProperty(exports, "getAccountColor", { enumerable: true, get: funct
256
256
  // ---------------------------------------------------------------------------
257
257
  // Registrable-domain + central-IdP-apex helpers.
258
258
  //
259
- // The client SSO-bounce / silent-iframe / FedCM machinery was removed in the
260
- // device-first cutover (wave 2, ecosystem-wide bump complete). `registrableApex`
261
- // (eTLD+1) and `CENTRAL_IDP_APEX` are still genuinely used: `registrableApex`
262
- // via the `@oxyhq/core/server` re-export consumed by
263
- // `packages/api/src/utils/sameSite.ts` for same-site origin checks, and
264
- // `CENTRAL_IDP_APEX` by `server/cors.ts`'s `createOxyCors` (auto-allows
265
- // `*.oxy.so`). `SSO_CALLBACK_PATH` has no remaining importer outside this
266
- // module as of wave 2 — kept exported for now rather than removed here (an
267
- // export deletion is a logic change, out of scope for a comment sweep); flag
268
- // for a follow-up dead-export cleanup pass.
259
+ // `registrableApex` (eTLD+1) is consumed via the `@oxyhq/core/server`
260
+ // re-export by `packages/api/src/utils/sameSite.ts` for same-site origin
261
+ // checks; `CENTRAL_IDP_APEX` by `server/cors.ts`'s `createOxyCors` (auto-allows
262
+ // `*.oxy.so`).
269
263
  // ---------------------------------------------------------------------------
270
264
  var registrableApex_1 = require("./utils/registrableApex");
271
265
  Object.defineProperty(exports, "registrableApex", { enumerable: true, get: function () { return registrableApex_1.registrableApex; } });
272
266
  var authWebUrl_1 = require("./utils/authWebUrl");
273
267
  Object.defineProperty(exports, "CENTRAL_IDP_APEX", { enumerable: true, get: function () { return authWebUrl_1.CENTRAL_IDP_APEX; } });
274
- var ssoBounce_1 = require("./utils/ssoBounce");
275
- Object.defineProperty(exports, "SSO_CALLBACK_PATH", { enumerable: true, get: function () { return ssoBounce_1.SSO_CALLBACK_PATH; } });
276
268
  var coldBoot_1 = require("./utils/coldBoot");
277
269
  Object.defineProperty(exports, "runColdBoot", { enumerable: true, get: function () { return coldBoot_1.runColdBoot; } });
278
270
  // ---------------------------------------------------------------------------
271
+ // OAuth 2.0 Authorization Code + PKCE helpers ("Sign in with Oxy" third party).
272
+ // Standard OAuth against auth.oxy.so/authorize — no FedCM/cookies/SSO bounce.
273
+ // ---------------------------------------------------------------------------
274
+ var oauthPkce_1 = require("./utils/oauthPkce");
275
+ Object.defineProperty(exports, "buildOAuthAuthorizeUrl", { enumerable: true, get: function () { return oauthPkce_1.buildOAuthAuthorizeUrl; } });
276
+ Object.defineProperty(exports, "computeCodeChallenge", { enumerable: true, get: function () { return oauthPkce_1.computeCodeChallenge; } });
277
+ Object.defineProperty(exports, "generateOAuthState", { enumerable: true, get: function () { return oauthPkce_1.generateOAuthState; } });
278
+ Object.defineProperty(exports, "generatePkcePair", { enumerable: true, get: function () { return oauthPkce_1.generatePkcePair; } });
279
+ Object.defineProperty(exports, "DEFAULT_OAUTH_SCOPE", { enumerable: true, get: function () { return oauthPkce_1.DEFAULT_OAUTH_SCOPE; } });
280
+ Object.defineProperty(exports, "OXY_AUTHORIZE_URL", { enumerable: true, get: function () { return oauthPkce_1.OXY_AUTHORIZE_URL; } });
281
+ // ---------------------------------------------------------------------------
279
282
  // Session sync (device-scoped multi-account session client)
280
283
  // ---------------------------------------------------------------------------
281
284
  var SessionClient_1 = require("./session/SessionClient");
282
285
  Object.defineProperty(exports, "SessionClient", { enumerable: true, get: function () { return SessionClient_1.SessionClient; } });
283
286
  // Shared SessionClient integration layer: the host adapter, the pure
284
287
  // DeviceSessionState projection helpers, and the client factory are defined
285
- // ONCE here so `@oxyhq/services` and `@oxyhq/auth` both reuse them instead of
288
+ // ONCE here so every `@oxyhq/services` platform variant reuses them instead of
286
289
  // duplicating a local copy. Each consumer supplies its own `TokenTransport`
287
290
  // (native vs. web mint strategies differ) to `createSessionClient`.
288
291
  var sessionClientHost_1 = require("./session/sessionClientHost");
@@ -297,7 +300,7 @@ Object.defineProperty(exports, "accountIdsOf", { enumerable: true, get: function
297
300
  // Unified account-list projection (THE single source of truth for the account
298
301
  // chooser: device sign-ins ∪ account graph, deduped by accountId). Pure +
299
302
  // I/O-free — the caller hydrates profiles via `getUsersByIds`. Shared by
300
- // `@oxyhq/services`, `@oxyhq/auth`, and auth.oxy.so so the list can't diverge.
303
+ // `@oxyhq/services` and auth.oxy.so so the list can't diverge.
301
304
  var accountProjection_1 = require("./session/accountProjection");
302
305
  Object.defineProperty(exports, "projectSwitchableAccounts", { enumerable: true, get: function () { return accountProjection_1.projectSwitchableAccounts; } });
303
306
  Object.defineProperty(exports, "switchableAccountIds", { enumerable: true, get: function () { return accountProjection_1.switchableAccountIds; } });
@@ -105,6 +105,34 @@ function OxyServicesDeviceBootMixin(Base) {
105
105
  throw this.handleError(error);
106
106
  }
107
107
  }
108
+ /**
109
+ * Zero-cookie mint (phase 2c). Present the first-party `deviceId` +
110
+ * `deviceSecret` to `POST /session/device/token` — NO bearer, NO cookies:
111
+ * possession of the secret IS the device-ownership proof. Returns a fresh
112
+ * short access token for the device's active account plus `nextDeviceSecret`
113
+ * (rotation-in-use) and the projected device-session `state`.
114
+ *
115
+ * `skipAuth` (like {@link refreshWithToken}): this call carries no bearer, so
116
+ * a 401 must surface DIRECTLY — never trigger `HttpService`'s 401→refresh→
117
+ * retry dance (which would pointlessly rotate the refresh family). The cold
118
+ * boot reads the 401 body (`invalid_device_secret` vs `no_active_session`) to
119
+ * decide whether to drop the secret and fall back or resolve signed-out.
120
+ *
121
+ * @throws if the response does not match {@link deviceTokenMintResponseSchema}.
122
+ */
123
+ async mintFromDeviceSecret(deviceId, deviceSecret) {
124
+ try {
125
+ const res = await this.makeRequest('POST', '/session/device/token', { deviceId, deviceSecret }, { cache: false, skipAuth: true });
126
+ const parsed = (0, contracts_1.safeParseContract)(contracts_1.deviceTokenMintResponseSchema, res);
127
+ if (!parsed) {
128
+ throw new Error('session/device/token returned an unexpected response shape');
129
+ }
130
+ return parsed;
131
+ }
132
+ catch (error) {
133
+ throw this.handleError(error);
134
+ }
135
+ }
108
136
  /**
109
137
  * Build the top-level `GET /auth/device/bootstrap` URL for the cross-apex
110
138
  * hop. The server validates `return_to` against the trusted-origin lane and
@@ -16,7 +16,7 @@
16
16
  * ```
17
17
  */
18
18
  Object.defineProperty(exports, "__esModule", { value: true });
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;
19
+ 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; } });
@@ -52,9 +52,3 @@ Object.defineProperty(exports, "verifySecret", { enumerable: true, get: function
52
52
  // lets `@oxyhq/api` derive `auth.<apex>` without duplicating PSL logic.
53
53
  var registrableApex_1 = require("../utils/registrableApex");
54
54
  Object.defineProperty(exports, "registrableApex", { enumerable: true, get: function () { return registrableApex_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; } });
@@ -4,7 +4,7 @@
4
4
  *
5
5
  * A framework-agnostic state machine + subscribe/getSnapshot store (the same
6
6
  * pattern {@link SessionClient} uses — no React, no RN) that both
7
- * `@oxyhq/services` (RN `OxyProvider`) and `@oxyhq/auth` (web `WebOxyProvider`)
7
+ * every `OxyProvider` platform variant (Expo/RN and RN-Web)
8
8
  * bind to via `useSyncExternalStore`, so the account chooser is ONE
9
9
  * implementation across the ecosystem instead of the five drifting copies it
10
10
  * replaces.
@@ -6,7 +6,7 @@
6
6
  * merging the device's server-authoritative session set (`DeviceSessionState`
7
7
  * from {@link SessionClient}) with the caller's account graph (`AccountNode[]`
8
8
  * from `oxyServices.listAccounts()`), deduped by `accountId`. This lives in
9
- * `@oxyhq/core` so `@oxyhq/services` (RN) and `@oxyhq/auth` (web) — and
9
+ * `@oxyhq/core` so every `@oxyhq/services` platform variant — and
10
10
  * `auth.oxy.so` — all render the SAME list from the SAME logic and cannot
11
11
  * diverge.
12
12
  *
@@ -70,6 +70,12 @@ function deserialize(raw) {
70
70
  if (typeof candidate.deviceToken === 'string' && candidate.deviceToken.length > 0) {
71
71
  state.deviceToken = candidate.deviceToken;
72
72
  }
73
+ if (typeof candidate.deviceId === 'string' && candidate.deviceId.length > 0) {
74
+ state.deviceId = candidate.deviceId;
75
+ }
76
+ if (typeof candidate.deviceSecret === 'string' && candidate.deviceSecret.length > 0) {
77
+ state.deviceSecret = candidate.deviceSecret;
78
+ }
73
79
  if (typeof candidate.accessToken === 'string' && candidate.accessToken.length > 0) {
74
80
  state.accessToken = candidate.accessToken;
75
81
  }
@@ -7,7 +7,7 @@ exports.accountIdsOf = accountIdsOf;
7
7
  /**
8
8
  * Pure projection helpers: `DeviceSessionState` (the device-scoped
9
9
  * multi-account session-sync state produced by `SessionClient`) -> the
10
- * shapes consumers (`@oxyhq/services`, `@oxyhq/auth`) render today
10
+ * shapes `@oxyhq/services` consumers render today
11
11
  * (`ClientSession[]`, an active session id, an active `User`).
12
12
  *
13
13
  * No I/O. The caller fetches profiles via