@oxyhq/core 5.1.1 → 5.2.1
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/AuthManager.js +27 -5
- package/dist/cjs/OxyServices.base.js +71 -0
- 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 +3 -1
- package/dist/cjs/mixins/OxyServices.accounts.js +10 -25
- package/dist/cjs/mixins/OxyServices.assets.js +76 -0
- package/dist/cjs/utils/cache.js +2 -0
- package/dist/cjs/utils/ssoBounce.js +59 -0
- package/dist/esm/.tsbuildinfo +1 -1
- package/dist/esm/AuthManager.js +27 -5
- package/dist/esm/OxyServices.base.js +71 -0
- 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 +1 -1
- package/dist/esm/mixins/OxyServices.accounts.js +10 -25
- package/dist/esm/mixins/OxyServices.assets.js +76 -0
- package/dist/esm/utils/cache.js +2 -0
- package/dist/esm/utils/ssoBounce.js +57 -0
- package/dist/types/.tsbuildinfo +1 -1
- package/dist/types/OxyServices.base.d.ts +38 -0
- package/dist/types/index.d.ts +2 -2
- package/dist/types/mixins/OxyServices.accounts.d.ts +1 -0
- package/dist/types/mixins/OxyServices.analytics.d.ts +1 -0
- package/dist/types/mixins/OxyServices.appData.d.ts +1 -0
- package/dist/types/mixins/OxyServices.assets.d.ts +36 -1
- package/dist/types/mixins/OxyServices.auth.d.ts +1 -0
- package/dist/types/mixins/OxyServices.civic.d.ts +1 -0
- package/dist/types/mixins/OxyServices.connectedApps.d.ts +1 -0
- package/dist/types/mixins/OxyServices.contacts.d.ts +1 -0
- package/dist/types/mixins/OxyServices.devices.d.ts +1 -0
- package/dist/types/mixins/OxyServices.features.d.ts +1 -0
- package/dist/types/mixins/OxyServices.fedcm.d.ts +1 -0
- package/dist/types/mixins/OxyServices.identity.d.ts +1 -0
- package/dist/types/mixins/OxyServices.language.d.ts +1 -0
- package/dist/types/mixins/OxyServices.links.d.ts +1 -0
- package/dist/types/mixins/OxyServices.location.d.ts +1 -0
- package/dist/types/mixins/OxyServices.nodes.d.ts +1 -0
- package/dist/types/mixins/OxyServices.payment.d.ts +1 -0
- package/dist/types/mixins/OxyServices.privacy.d.ts +1 -0
- package/dist/types/mixins/OxyServices.redirect.d.ts +1 -0
- package/dist/types/mixins/OxyServices.reputation.d.ts +1 -0
- package/dist/types/mixins/OxyServices.security.d.ts +1 -0
- package/dist/types/mixins/OxyServices.silent.d.ts +1 -0
- package/dist/types/mixins/OxyServices.sso.d.ts +1 -0
- package/dist/types/mixins/OxyServices.topics.d.ts +1 -0
- package/dist/types/mixins/OxyServices.user.d.ts +1 -0
- package/dist/types/mixins/OxyServices.utility.d.ts +1 -0
- package/dist/types/models/interfaces.d.ts +23 -0
- package/dist/types/utils/ssoBounce.d.ts +47 -0
- package/package.json +1 -1
- package/src/AuthManager.ts +29 -5
- package/src/OxyServices.base.ts +79 -0
- package/src/__tests__/establishDeviceRefreshSlot.test.ts +221 -0
- 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 +3 -0
- package/src/mixins/OxyServices.accounts.ts +10 -33
- package/src/mixins/OxyServices.assets.ts +92 -1
- package/src/mixins/__tests__/OxyServices.serviceAssetMetadataBySha.test.ts +135 -0
- package/src/models/interfaces.ts +24 -0
- package/src/utils/__tests__/ssoBounce.test.ts +41 -0
- package/src/utils/cache.ts +2 -0
- package/src/utils/ssoBounce.ts +61 -0
package/dist/esm/AuthManager.js
CHANGED
|
@@ -393,11 +393,33 @@ export class AuthManager {
|
|
|
393
393
|
this.currentUser = session.user;
|
|
394
394
|
}
|
|
395
395
|
this.currentAuthMethod = method;
|
|
396
|
-
|
|
397
|
-
|
|
396
|
+
// Register this primary session in the device's first-party multi-account
|
|
397
|
+
// refresh-cookie set (web only). Every web primary commit funnels through
|
|
398
|
+
// here (FedCM, central `/sso` return, credentials), but only a same-apex
|
|
399
|
+
// `/fedcm/exchange` plants an `oxy_rt_<authuser>` slot as a side effect — the
|
|
400
|
+
// cross-origin/credential-less restores (`/sso/exchange`, the IdP
|
|
401
|
+
// `/auth/silent` postMessage) cannot set an `api.oxy.so` cookie. WITHOUT this
|
|
402
|
+
// call those primaries never join the device set, so `refresh-all` returns no
|
|
403
|
+
// accounts and account-switch persistence has no foundation. The shared
|
|
404
|
+
// `POST /auth/session` primitive plants/reuses this user's slot where the
|
|
405
|
+
// cookies ARE visible, re-plants the rotated access token, and returns the
|
|
406
|
+
// AUTHORITATIVE `authuser` slot (the one actually written) — preferred over the
|
|
407
|
+
// JWT-decoded guess, which may not correspond to a real cookie. No-op on native
|
|
408
|
+
// (returns `null`); best-effort on transient failure.
|
|
409
|
+
const establishedAuthuser = await this.oxyServices.establishDeviceRefreshSlot();
|
|
410
|
+
// The slot mint rotated the access token; pick it up so the registry and the
|
|
411
|
+
// refresh authority track the cookie just written. Falls back to the original
|
|
412
|
+
// session token when the slot was a no-op (native) or failed.
|
|
413
|
+
const rotatedToken = (establishedAuthuser !== null ? this.oxyServices.getAccessToken() : null) ??
|
|
414
|
+
session.accessToken;
|
|
415
|
+
if (rotatedToken) {
|
|
416
|
+
this._lastKnownAccessToken = rotatedToken;
|
|
417
|
+
}
|
|
418
|
+
const decodedAuthuser = rotatedToken
|
|
419
|
+
? AuthManager.decodeAuthuserFromAccessToken(rotatedToken)
|
|
398
420
|
: null;
|
|
399
|
-
const authuser = decodedAuthuser ?? 0;
|
|
400
|
-
if (
|
|
421
|
+
const authuser = establishedAuthuser ?? decodedAuthuser ?? 0;
|
|
422
|
+
if (rotatedToken && session.sessionId) {
|
|
401
423
|
this.accounts.set(authuser, {
|
|
402
424
|
authuser,
|
|
403
425
|
sessionId: session.sessionId,
|
|
@@ -407,7 +429,7 @@ export class AuthManager {
|
|
|
407
429
|
name: session.user.name,
|
|
408
430
|
avatar: session.user.avatar ?? null,
|
|
409
431
|
},
|
|
410
|
-
accessToken:
|
|
432
|
+
accessToken: rotatedToken,
|
|
411
433
|
expiresAt: session.expiresAt,
|
|
412
434
|
});
|
|
413
435
|
this.activeAuthuser = authuser;
|
|
@@ -8,6 +8,9 @@ import { handleHttpError } from './utils/errorUtils.js';
|
|
|
8
8
|
import { HttpService } from './HttpService.js';
|
|
9
9
|
import { OxyAuthenticationError, OxyAuthenticationTimeoutError } from './OxyServices.errors.js';
|
|
10
10
|
import { resolveCentralAuthUrl } from './utils/authWebUrl.js';
|
|
11
|
+
import { isWeb } from './utils/platform.js';
|
|
12
|
+
import { registrableApex } from './utils/fapiAutoDetect.js';
|
|
13
|
+
import { logger } from './utils/loggerUtils.js';
|
|
11
14
|
/**
|
|
12
15
|
* Base class for OxyServices with core infrastructure
|
|
13
16
|
*/
|
|
@@ -250,6 +253,74 @@ export class OxyServicesBase {
|
|
|
250
253
|
getAccessToken() {
|
|
251
254
|
return this.httpService.getAccessToken();
|
|
252
255
|
}
|
|
256
|
+
/**
|
|
257
|
+
* Register the CURRENTLY-ACTIVE session in the device's first-party
|
|
258
|
+
* multi-account refresh-cookie set by calling `POST /auth/session`.
|
|
259
|
+
*
|
|
260
|
+
* This is the single, shared primitive every web primary-session commit and the
|
|
261
|
+
* account switch use to plant their `oxy_rt_<authuser>` slot. It MUST be a
|
|
262
|
+
* dedicated call to `/auth/session` rather than relying on whichever endpoint
|
|
263
|
+
* established the session: that endpoint is frequently OUTSIDE the cookie's
|
|
264
|
+
* `Path=/auth` scope (`/accounts/:id/switch`) or is a cross-origin/credential-
|
|
265
|
+
* less restore (`/sso/exchange`, the IdP `/auth/silent` postMessage) that cannot
|
|
266
|
+
* set an `api.oxy.so` cookie at all. `/auth/session` runs where the device's
|
|
267
|
+
* existing slots ARE visible, so the server resolves this user's slot (reusing an
|
|
268
|
+
* existing one or allocating a new one) without clobbering a sibling account,
|
|
269
|
+
* mints a fresh access token bound to the same session, and returns the resolved
|
|
270
|
+
* `authuser`.
|
|
271
|
+
*
|
|
272
|
+
* Behaviour:
|
|
273
|
+
* - Requires a planted bearer (the caller must have already installed the
|
|
274
|
+
* session's access token); `/auth/session` derives the session from it.
|
|
275
|
+
* - On success re-plants the rotated access token (so the active token matches
|
|
276
|
+
* the freshly-rotated cookie) and returns the device `authuser` slot.
|
|
277
|
+
* - WEB-ONLY: on native there are no first-party refresh cookies → returns
|
|
278
|
+
* `null`.
|
|
279
|
+
* - FIRST-PARTY-ONLY: the cookie is host-only on the API host with
|
|
280
|
+
* `SameSite=Lax`, so it only sticks when the page is SAME-SITE (same
|
|
281
|
+
* registrable apex) as the API. On a cross-apex RP (`mention.earth` calling
|
|
282
|
+
* `api.oxy.so`) the browser rejects the `Set-Cookie` as a third-party cookie,
|
|
283
|
+
* so a returned slot would be a phantom never enumerated by `refresh-all`.
|
|
284
|
+
* Those RPs durably restore via the per-apex `/auth/silent` iframe + `/sso`
|
|
285
|
+
* bounce, NOT this device set → returns `null` without calling the API.
|
|
286
|
+
* - BEST-EFFORT: a failure (e.g. transient network) never throws — the session
|
|
287
|
+
* stays active in-memory; only its reload durability via the device set is at
|
|
288
|
+
* risk. The caller treats `null` as "not registered in the device set".
|
|
289
|
+
*
|
|
290
|
+
* @returns The resolved device `authuser` slot, or `null` on native / cross-apex
|
|
291
|
+
* / failure.
|
|
292
|
+
*/
|
|
293
|
+
async establishDeviceRefreshSlot() {
|
|
294
|
+
if (!isWeb()) {
|
|
295
|
+
return null;
|
|
296
|
+
}
|
|
297
|
+
if (typeof window !== 'undefined' && window.location?.hostname) {
|
|
298
|
+
const pageApex = registrableApex(window.location.hostname);
|
|
299
|
+
let apiApex = null;
|
|
300
|
+
try {
|
|
301
|
+
apiApex = registrableApex(new URL(this.getBaseURL()).hostname);
|
|
302
|
+
}
|
|
303
|
+
catch {
|
|
304
|
+
apiApex = null;
|
|
305
|
+
}
|
|
306
|
+
if (!pageApex || !apiApex || pageApex !== apiApex) {
|
|
307
|
+
return null;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
try {
|
|
311
|
+
const established = await this.makeRequest('POST', '/auth/session', undefined, { cache: false });
|
|
312
|
+
// `/auth/session` mints a fresh access token off the same session; re-plant
|
|
313
|
+
// it so the active token matches the rotated cookie.
|
|
314
|
+
if (established?.accessToken) {
|
|
315
|
+
this.setTokens(established.accessToken);
|
|
316
|
+
}
|
|
317
|
+
return typeof established?.authuser === 'number' ? established.authuser : null;
|
|
318
|
+
}
|
|
319
|
+
catch (error) {
|
|
320
|
+
logger.warn('[OxyServices] Failed to establish device refresh cookie via POST /auth/session; the session is active in-session but may not survive a reload as part of the device account set', { component: 'OxyServices', method: 'establishDeviceRefreshSlot' }, error);
|
|
321
|
+
return null;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
253
324
|
/**
|
|
254
325
|
* Decode the current access token and return its `exp` claim in SECONDS since
|
|
255
326
|
* the Unix epoch (the raw JWT `exp` unit), or `null` when there is no token,
|
|
@@ -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/esm/index.js
CHANGED
|
@@ -127,7 +127,7 @@ export { CENTRAL_AUTH_URL, CENTRAL_IDP_APEX, resolveCentralAuthUrl } from './uti
|
|
|
127
127
|
export { parseSsoReturnFragment, consumeSsoReturn } from './utils/ssoReturn.js';
|
|
128
128
|
export { generateSsoState } from './mixins/OxyServices.sso.js';
|
|
129
129
|
// SSO bounce — per-origin sessionStorage keys, bounce URL builder, predicates
|
|
130
|
-
export { SSO_CALLBACK_PATH, SSO_GUARD_TTL_MS, ssoStateKey, ssoGuardKey, ssoDestKey, ssoNoSessionKey, ssoAttemptedKey, ssoPriorSessionKey, ssoCallbackBootstrapKey, ssoNavigate, getSsoCallbackBootstrapScript, buildSsoBounceUrl, isCentralIdPOrigin, guardActive, allowSsoBounce, } from './utils/ssoBounce.js';
|
|
130
|
+
export { SSO_CALLBACK_PATH, SSO_GUARD_TTL_MS, ssoStateKey, ssoGuardKey, ssoDestKey, ssoNoSessionKey, ssoAttemptedKey, ssoPriorSessionKey, ssoSignedOutKey, ssoCallbackBootstrapKey, ssoNavigate, getSsoCallbackBootstrapScript, buildSsoBounceUrl, isCentralIdPOrigin, guardActive, silentRestoreSuppressed, allowSsoBounce, } from './utils/ssoBounce.js';
|
|
131
131
|
export { runColdBoot } from './utils/coldBoot.js';
|
|
132
132
|
// API response contracts (request/response Zod schemas + inferred types) live in
|
|
133
133
|
// `@oxyhq/contracts` — the single source of truth shared by the backend and every
|
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
import { normalizeUserIdentity } from '../utils/userIdentity.js';
|
|
2
|
-
import { isWeb } from '../utils/platform.js';
|
|
3
|
-
import { logger } from '../utils/loggerUtils.js';
|
|
4
2
|
import { CACHE_TIMES } from './mixinHelpers.js';
|
|
5
3
|
export function OxyServicesAccountsMixin(Base) {
|
|
6
4
|
return class extends Base {
|
|
@@ -91,29 +89,16 @@ export function OxyServicesAccountsMixin(Base) {
|
|
|
91
89
|
}
|
|
92
90
|
// Register the switched session in the device's multi-account set by
|
|
93
91
|
// establishing its first-party refresh cookie. This MUST be a separate
|
|
94
|
-
// call to `POST /auth/session
|
|
95
|
-
//
|
|
96
|
-
//
|
|
97
|
-
//
|
|
98
|
-
//
|
|
99
|
-
//
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
if (typeof established?.authuser === 'number') {
|
|
105
|
-
authuser = established.authuser;
|
|
106
|
-
}
|
|
107
|
-
// /auth/session mints a fresh access token off the same session;
|
|
108
|
-
// re-plant it so the active token matches the rotated cookie.
|
|
109
|
-
if (established?.accessToken) {
|
|
110
|
-
this.setTokens(established.accessToken);
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
catch (error) {
|
|
114
|
-
logger.warn('[OxyServices] Failed to establish device refresh cookie after account switch; the switch is active in-session but may not survive a reload', { component: 'OxyServices', method: 'switchToAccount' }, error);
|
|
115
|
-
}
|
|
116
|
-
}
|
|
92
|
+
// call to `POST /auth/session` (the shared `establishDeviceRefreshSlot`
|
|
93
|
+
// primitive): the switch route is at `/accounts/*`, outside the
|
|
94
|
+
// `oxy_rt_<authuser>` cookie's `Path=/auth` scope, so it can never read
|
|
95
|
+
// the device's existing slots and would overwrite slot 0 (destroying the
|
|
96
|
+
// operator's own session). `/auth/session` runs where the cookies ARE
|
|
97
|
+
// visible, so the server allocates a NEW slot that coexists with the
|
|
98
|
+
// operator's and returns its `authuser`. Web-only; best-effort (the helper
|
|
99
|
+
// re-plants the rotated access token and returns `null` on native/failure).
|
|
100
|
+
const establishedAuthuser = await this.establishDeviceRefreshSlot();
|
|
101
|
+
const authuser = establishedAuthuser ?? res.authuser;
|
|
117
102
|
// Identity changed → drop the entire GET response cache so no entry
|
|
118
103
|
// personalised for the previous identity is reused. Cache keys are
|
|
119
104
|
// identity-scoped, so a different identity could not READ the old
|
|
@@ -8,6 +8,19 @@ import { extractErrorStatus } from '../utils/errorUtils.js';
|
|
|
8
8
|
* `getUsersByIds`'s `USERS_BY_IDS_CHUNK_SIZE`.
|
|
9
9
|
*/
|
|
10
10
|
const SERVICE_ASSET_METADATA_CHUNK_SIZE = 100;
|
|
11
|
+
/**
|
|
12
|
+
* Maximum number of content hashes sent per `POST /assets/service/by-sha256`
|
|
13
|
+
* request. Matches the server-side cap (the route rejects empty or > 100 hash
|
|
14
|
+
* arrays with a 400); larger inputs are chunked and merged. Same ceiling as
|
|
15
|
+
* {@link SERVICE_ASSET_METADATA_CHUNK_SIZE} for the forward id lookup.
|
|
16
|
+
*/
|
|
17
|
+
const SERVICE_ASSET_METADATA_BY_SHA_CHUNK_SIZE = 100;
|
|
18
|
+
/**
|
|
19
|
+
* Lowercase hex SHA-256 digest matcher (exactly 64 hex chars). The reverse
|
|
20
|
+
* lookup drops non-conforming hashes client-side so a single malformed value
|
|
21
|
+
* never 400s an otherwise-valid chunk on the server.
|
|
22
|
+
*/
|
|
23
|
+
const SHA256_HEX_PATTERN = /^[a-f0-9]{64}$/;
|
|
11
24
|
export function OxyServicesAssetsMixin(Base) {
|
|
12
25
|
return class extends Base {
|
|
13
26
|
constructor(...args) {
|
|
@@ -210,6 +223,69 @@ export function OxyServicesAssetsMixin(Base) {
|
|
|
210
223
|
}));
|
|
211
224
|
return settled.flat();
|
|
212
225
|
}
|
|
226
|
+
/**
|
|
227
|
+
* Reverse content-address lookup: resolve many content `sha256` digests to
|
|
228
|
+
* the servable Oxy asset holding each, in one round-trip per chunk via
|
|
229
|
+
* `POST /assets/service/by-sha256` (body `{ sha256s }`).
|
|
230
|
+
*
|
|
231
|
+
* This is the INVERSE of {@link getServiceAssetMetadataByIds}: given a
|
|
232
|
+
* record's `blob.sha256`, it returns the asset's `id`, `mime`, byte `size`,
|
|
233
|
+
* `status`, and — for active, public, CDN-reachable assets only — a public
|
|
234
|
+
* `url` (`cloud.oxy.so`). Built for server-to-server callers (e.g. Mention's
|
|
235
|
+
* MTN materializer / node-blob sync) that hold a content hash and need to
|
|
236
|
+
* map it back to a servable asset. Hashes are lowercased, validated against
|
|
237
|
+
* a 64-char hex pattern (malformed entries dropped client-side), and
|
|
238
|
+
* deduplicated before being split into chunks of
|
|
239
|
+
* {@link SERVICE_ASSET_METADATA_BY_SHA_CHUNK_SIZE} (the server-side cap). The
|
|
240
|
+
* server omits unknown/deleted hashes from each chunk's `data`, so the
|
|
241
|
+
* merged result may be shorter than the requested list and the caller is
|
|
242
|
+
* expected to map by `sha256`.
|
|
243
|
+
*
|
|
244
|
+
* **Service-token auth (required).** `/assets/service/by-sha256` is guarded
|
|
245
|
+
* by `serviceAuthMiddleware` + the `files:read` scope and is called via
|
|
246
|
+
* `makeServiceRequest` (`Authorization: Bearer <serviceToken>`, `cache:false`).
|
|
247
|
+
* The calling client MUST be service-configured (`configureServiceAuth`)
|
|
248
|
+
* before invoking; a plain user-session request is rejected by the route's
|
|
249
|
+
* service-auth guard.
|
|
250
|
+
*
|
|
251
|
+
* Resilience: chunks are independent. A failed chunk is logged and skipped —
|
|
252
|
+
* every entry that resolved is still returned. An empty input (or one whose
|
|
253
|
+
* every value is malformed) resolves immediately with `[]` and performs no
|
|
254
|
+
* network call.
|
|
255
|
+
*
|
|
256
|
+
* Not cached at the SDK layer: it's a POST keyed on a multi-hash body (low
|
|
257
|
+
* hit rate), mirroring the sibling service/POST methods which never cache.
|
|
258
|
+
*/
|
|
259
|
+
async getServiceAssetMetadataBySha256(sha256s) {
|
|
260
|
+
const uniqueShas = Array.from(new Set(sha256s
|
|
261
|
+
.filter((sha) => typeof sha === 'string')
|
|
262
|
+
.map((sha) => sha.trim().toLowerCase())
|
|
263
|
+
.filter((sha) => SHA256_HEX_PATTERN.test(sha))));
|
|
264
|
+
if (uniqueShas.length === 0) {
|
|
265
|
+
return [];
|
|
266
|
+
}
|
|
267
|
+
const chunks = [];
|
|
268
|
+
for (let i = 0; i < uniqueShas.length; i += SERVICE_ASSET_METADATA_BY_SHA_CHUNK_SIZE) {
|
|
269
|
+
chunks.push(uniqueShas.slice(i, i + SERVICE_ASSET_METADATA_BY_SHA_CHUNK_SIZE));
|
|
270
|
+
}
|
|
271
|
+
// Run chunks concurrently; a single chunk failure must not sink the rest.
|
|
272
|
+
const settled = await Promise.all(chunks.map(async (chunk) => {
|
|
273
|
+
try {
|
|
274
|
+
const entries = await this.makeServiceRequest('POST', '/assets/service/by-sha256', { sha256s: chunk });
|
|
275
|
+
return Array.isArray(entries) ? entries : [];
|
|
276
|
+
}
|
|
277
|
+
catch (error) {
|
|
278
|
+
logger.warn('getServiceAssetMetadataBySha256: chunk failed, continuing with remaining chunks', {
|
|
279
|
+
method: 'getServiceAssetMetadataBySha256',
|
|
280
|
+
chunkSize: chunk.length,
|
|
281
|
+
status: extractErrorStatus(error),
|
|
282
|
+
error: error instanceof Error ? error.message : String(error),
|
|
283
|
+
});
|
|
284
|
+
return [];
|
|
285
|
+
}
|
|
286
|
+
}));
|
|
287
|
+
return settled.flat();
|
|
288
|
+
}
|
|
213
289
|
/**
|
|
214
290
|
* Upload raw file data
|
|
215
291
|
*/
|
package/dist/esm/utils/cache.js
CHANGED
|
@@ -65,6 +65,7 @@ const NO_SESSION_KEY_PREFIX = 'oxy_sso_no_session:';
|
|
|
65
65
|
const ATTEMPTED_KEY_PREFIX = 'oxy_sso_attempted:';
|
|
66
66
|
const CALLBACK_BOOTSTRAP_KEY_PREFIX = 'oxy_sso_callback_bootstrap:';
|
|
67
67
|
const PRIOR_SESSION_KEY_PREFIX = 'oxy_sso_prior_session:';
|
|
68
|
+
const SIGNED_OUT_KEY_PREFIX = 'oxy_signed_out:';
|
|
68
69
|
/** Per-origin CSRF state key (matched on return to defeat fragment forgery). */
|
|
69
70
|
export function ssoStateKey(origin) {
|
|
70
71
|
return `${STATE_KEY_PREFIX}${origin}`;
|
|
@@ -114,6 +115,36 @@ export function ssoAttemptedKey(origin) {
|
|
|
114
115
|
export function ssoPriorSessionKey(origin) {
|
|
115
116
|
return `${PRIOR_SESSION_KEY_PREFIX}${origin}`;
|
|
116
117
|
}
|
|
118
|
+
/**
|
|
119
|
+
* Per-origin DURABLE "the user DELIBERATELY signed out on this device/origin"
|
|
120
|
+
* flag.
|
|
121
|
+
*
|
|
122
|
+
* Like {@link ssoPriorSessionKey} this lives in DURABLE storage (web
|
|
123
|
+
* `localStorage`; services uses its own `storageKeyPrefix`-scoped key), NOT the
|
|
124
|
+
* per-tab `sessionStorage` the loop-breaker keys use — it must survive a reload.
|
|
125
|
+
*
|
|
126
|
+
* It exists purely to suppress AUTOMATIC silent restore after a deliberate
|
|
127
|
+
* sign-out: a still-live IdP session (the central `fedcm_session` / the FedCM
|
|
128
|
+
* credential association) would otherwise let `fedcm-silent` / the per-apex
|
|
129
|
+
* `/auth/silent` iframe re-mint a session on the very next cold boot, so a user
|
|
130
|
+
* who pressed "Sign out" gets silently signed back in on reload. With this flag
|
|
131
|
+
* set, those silent cold-boot steps are skipped while the Gmail-style
|
|
132
|
+
* returning-account fast-path is otherwise preserved.
|
|
133
|
+
*
|
|
134
|
+
* Lifecycle (mirrors the existing gate machinery — set on a definitive event,
|
|
135
|
+
* cleared on its inverse):
|
|
136
|
+
* - SET on EXPLICIT full sign-out (alongside clearing the prior-session hint
|
|
137
|
+
* and the SSO bounce state).
|
|
138
|
+
* - CLEARED on ANY deliberate sign-in (password, FedCM, account switch, device
|
|
139
|
+
* claim) so a real sign-in fully re-enables silent restore — there is no
|
|
140
|
+
* "stuck signed out" state.
|
|
141
|
+
*
|
|
142
|
+
* NOTE: this gates only AUTOMATIC/silent restore. An INTERACTIVE sign-in always
|
|
143
|
+
* clears it first, so the user can always sign back in.
|
|
144
|
+
*/
|
|
145
|
+
export function ssoSignedOutKey(origin) {
|
|
146
|
+
return `${SIGNED_OUT_KEY_PREFIX}${origin}`;
|
|
147
|
+
}
|
|
117
148
|
/**
|
|
118
149
|
* Per-origin marker written by the pre-hydration callback bootstrap.
|
|
119
150
|
*
|
|
@@ -244,6 +275,32 @@ export function guardActive(storage, origin, now = Date.now()) {
|
|
|
244
275
|
}
|
|
245
276
|
return now - ts < SSO_GUARD_TTL_MS;
|
|
246
277
|
}
|
|
278
|
+
/**
|
|
279
|
+
* Whether AUTOMATIC silent restore is SUPPRESSED for this origin because the
|
|
280
|
+
* user deliberately signed out (the durable {@link ssoSignedOutKey} flag).
|
|
281
|
+
*
|
|
282
|
+
* When `true`, the silent cold-boot steps that can re-mint a session from a
|
|
283
|
+
* still-live IdP session WITHOUT user intent — `fedcm-silent` and the per-apex
|
|
284
|
+
* `/auth/silent` iframe — MUST be skipped, so a user who pressed "Sign out" is
|
|
285
|
+
* not silently signed back in on the next reload. Interactive sign-in clears the
|
|
286
|
+
* flag, so this never blocks a deliberate re-sign-in.
|
|
287
|
+
*
|
|
288
|
+
* Defensive: a `getItem` that throws (locked/disabled storage) is treated as NOT
|
|
289
|
+
* suppressed, so the gate fails toward the normal restore behaviour rather than
|
|
290
|
+
* wedging the user out.
|
|
291
|
+
*
|
|
292
|
+
* @param storage - The DURABLE storage to read (web `localStorage`; injected for
|
|
293
|
+
* testability).
|
|
294
|
+
* @param origin - The page origin whose signed-out flag to evaluate.
|
|
295
|
+
*/
|
|
296
|
+
export function silentRestoreSuppressed(storage, origin) {
|
|
297
|
+
try {
|
|
298
|
+
return storage.getItem(ssoSignedOutKey(origin)) === '1';
|
|
299
|
+
}
|
|
300
|
+
catch {
|
|
301
|
+
return false;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
247
304
|
/**
|
|
248
305
|
* Decide whether the terminal `/sso` establish-bounce is ALLOWED for this
|
|
249
306
|
* visitor (the smart `enabled` gate for the `sso-bounce` cold-boot step).
|