synapse-react-client
Version:
[](https://badge.fury.io/js/synapse-react-client) [](https://github.com/prettier/prettie
201 lines (200 loc) • 6.57 kB
JavaScript
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