UNPKG

synapse-react-client

Version:

[![npm version](https://badge.fury.io/js/synapse-react-client.svg)](https://badge.fury.io/js/synapse-react-client) [![code style: prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg?style=flat-square)](https://github.com/prettier/prettie

201 lines (200 loc) 6.57 kB
import * as o from "../../../synapse-client/SynapseClient.js"; import "@sage-bionetworks/synapse-client/generated/models/ErrorResponseCode"; import "@sage-bionetworks/synapse-client/generated/models/TwoFactorAuthErrorResponse"; import "@sage-bionetworks/synapse-client/util/SynapseClientError"; import "@sage-bionetworks/synapse-types"; import "../../functions/EntityTypeUtils.js"; import { getEndpoint as l, BackendDestinationEnum as c } from "../../functions/getEndpoint.js"; import "lodash-es"; import "@sage-bionetworks/synapse-client/util/synapseClientFetch"; import { SynapseClientError as h, SynapseClient as d } from "@sage-bionetworks/synapse-client"; const S = 6e4, u = { token: void 0, realmId: void 0, userId: void 0, isAuthenticated: !1, hasInitializedSession: !1 }; class r { state = u; listeners = /* @__PURE__ */ new Set(); intervalId = null; options; constructor(e = {}) { this.options = { ...e, defaultRealm: e.defaultRealm ?? "0" }, this.subscribe = this.subscribe.bind(this), this.getSnapshot = this.getSnapshot.bind(this), this.getServerSnapshot = this.getServerSnapshot.bind(this); } /** * Subscribe to state changes. The listener is called whenever session state is updated. * @returns an unsubscribe function. * * Compatible with React's `useSyncExternalStore(manager.subscribe, manager.getSnapshot)`. */ subscribe(e) { return this.listeners.add(e), () => { this.listeners.delete(e); }; } /** * Get the current session state snapshot. Returns a stable reference that only * changes when the state is updated. * * Compatible with React's `useSyncExternalStore(manager.subscribe, manager.getSnapshot)`. */ getSnapshot() { return this.state; } /** * Get the server-side (SSR) snapshot: always returns the unauthenticated initial state. * Required by React's `useSyncExternalStore` when rendering on the server. */ getServerSnapshot() { return u; } /** * Update mutable options. Values are read at `refreshSession`/`clearSession` call time, * so changes take effect on the next refresh cycle. */ setOptions(e) { e.defaultRealm !== void 0 && (this.options.defaultRealm = e.defaultRealm), e.maxAge !== void 0 && (this.options.maxAge = e.maxAge); } /** * Start the session manager: performs an initial refresh and sets up the periodic refresh interval. */ start() { this.refreshSession(), this.intervalId = setInterval(() => { this.refreshSession(); }, S); } /** * Stop the periodic refresh and clean up resources. */ dispose() { this.intervalId !== null && (clearInterval(this.intervalId), this.intervalId = null); } /** * Refresh the session by reading the stored token, validating it, and updating state. * If no token is stored, initializes an anonymous session. * If the token is invalid, signs out and triggers onSessionInvalid (or reloads the page). */ async refreshSession() { let e = await r.getStoredToken(); e || (e = await this.initAnonymousUserState()); const i = await r.validateToken( e, this.options.maxAge ); if (i === null) { await o.signOut(this.options.defaultRealm), this.options.onSessionInvalid ? this.options.onSessionInvalid() : window.location.reload(); return; } const n = i.sub; let t; try { t = await r.getCurrentRealmPrincipals( e ); } catch (s) { const a = s instanceof h && s.reason?.toLowerCase().includes("terms of service"), m = s instanceof h && s.errorResponse && "errorCode" in s.errorResponse && s.errorResponse.errorCode === "TWO_FA_ENABLED_REQUIRED"; if (a || m) { console.warn( `${a ? "Terms of Service not accepted" : "Two factor authentication required"}. Session will be initialized without realm principals.` ), this.updateState({ token: e, hasInitializedSession: !0, realmId: this.options.defaultRealm, userId: n, isAuthenticated: !0 // User has a valid token, even if ToS/2FA not complete }); return; } throw console.error("Error fetching realm principals: ", s), s; } const p = n !== t.anonymousUser; this.updateState({ token: e, hasInitializedSession: !0, realmId: t.realmId, userId: n, isAuthenticated: p }); } /** * Clear the current session by signing out and initializing an anonymous session. * Does NOT handle navigation/reload — the caller is responsible for that. */ async clearSession() { await this.initAnonymousUserState(), await this.refreshSession(); } /** * Attempt to get the stored access token from the browser cookie. * Returns the token string, or undefined if not found or on error. */ static async getStoredToken() { try { return await o.getAccessTokenFromCookie(); } catch (e) { console.error("Unable to get the access token: ", e); return; } } /** * Validate the token by calling the token introspection service. * @return the introspection response if valid, or null if the token is invalid */ static async validateToken(e, i) { const n = new d({ accessToken: e, basePath: l(c.REPO_ENDPOINT) }); try { const t = await n.openIDConnectServicesClient.postAuthV1Oauth2Introspect( { oAuthTokenIntrospectionRequest: { token: e, max_age: i } } ); if (!t.active) throw new Error("Token is not active."); return t; } catch (t) { console.error( "Error validating the access token. Clearing session. Error: ", t.message ); } return null; } /** * Get the realm principals for the current token. */ static async getCurrentRealmPrincipals(e) { return new d({ accessToken: e, basePath: l(c.REPO_ENDPOINT) }).realmServicesClient.getRepoV1RealmPrincipals(); } /** * Initialize the session for an anonymous user. * @return the new anonymous token. */ async initAnonymousUserState() { return this.options.onMissingExpectedAuthentication && this.options.onMissingExpectedAuthentication(), o.signOut(this.options.defaultRealm); } updateState(e) { this.state = e, this.emitChange(); } emitChange() { for (const e of this.listeners) e(); } } export { r as SynapseSessionManager }; //# sourceMappingURL=SynapseSessionManager.js.map