Expose Session in library

This commit is contained in:
Stan Wohlwend 2024-06-08 14:55:05 +02:00
parent 5f5b6d65b5
commit 543eb0cefb
6 changed files with 215 additions and 72 deletions

View File

@ -16,7 +16,7 @@ const createAdminApp = cacheFunction((baseUrl: string, projectId: string, userId
baseUrl,
projectId,
tokenStore: null,
projectOwnerSession: usersMap.get(userId)!.session,
projectOwnerSession: usersMap.get(userId)!._internalSession,
});
});

View File

@ -1,6 +1,6 @@
import { ServerAuthApplicationOptions, StackServerInterface } from "./serverInterface";
import { EmailConfigJson, ProjectJson, SharedProvider, StandardProvider } from "./clientInterface";
import { Session } from "../sessions";
import { InternalSession } from "../sessions";
export type AdminAuthApplicationOptions = Readonly<
ServerAuthApplicationOptions &
@ -9,7 +9,7 @@ export type AdminAuthApplicationOptions = Readonly<
superSecretAdminKey: string,
}
| {
projectOwnerSession: Session,
projectOwnerSession: InternalSession,
}
)
>
@ -86,7 +86,7 @@ export class StackAdminInterface extends StackServerInterface {
super(options);
}
protected async sendAdminRequest(path: string, options: RequestInit, session: Session | null, requestType: "admin" = "admin") {
protected async sendAdminRequest(path: string, options: RequestInit, session: InternalSession | null, requestType: "admin" = "admin") {
return await this.sendServerRequest(
path,
{
@ -169,7 +169,7 @@ export class StackAdminInterface extends StackServerInterface {
);
}
async getApiKeySet(id: string, session: Session): Promise<ApiKeySetJson> {
async getApiKeySet(id: string, session: InternalSession): Promise<ApiKeySetJson> {
const response = await this.sendAdminRequest(`/api-keys/${id}`, {}, session);
return await response.json();
}

View File

@ -8,7 +8,7 @@ import { StackAssertionError, captureError, throwErr } from '../utils/errors';
import { ProjectUpdateOptions } from './adminInterface';
import { cookies } from '@stackframe/stack-sc';
import { generateSecureRandomString } from '../utils/crypto';
import { AccessToken, RefreshToken, Session } from '../sessions';
import { AccessToken, RefreshToken, InternalSession } from '../sessions';
import { globalVar } from '../utils/globals';
import { logged } from '../utils/proxies';
@ -56,7 +56,7 @@ export type ClientInterfaceOptions = {
} & ({
publishableClientKey: string,
} | {
projectOwnerSession: Session,
projectOwnerSession: InternalSession,
});
export type SharedProvider = "shared-github" | "shared-google" | "shared-facebook" | "shared-microsoft" | "shared-spotify";
@ -242,7 +242,7 @@ export class StackClientInterface {
protected async sendClientRequest(
path: string,
requestOptions: RequestInit,
session: Session | null,
session: InternalSession | null,
requestType: "client" | "server" | "admin" = "client",
) {
session ??= this.createSession({
@ -259,8 +259,8 @@ export class StackClientInterface {
);
}
public createSession(options: Omit<ConstructorParameters<typeof Session>[0], "refreshAccessTokenCallback">): Session {
const session = new Session({
public createSession(options: Omit<ConstructorParameters<typeof InternalSession>[0], "refreshAccessTokenCallback">): InternalSession {
const session = new InternalSession({
refreshAccessTokenCallback: async (refreshToken) => await this.fetchNewAccessToken(refreshToken),
...options,
});
@ -270,7 +270,7 @@ export class StackClientInterface {
protected async sendClientRequestAndCatchKnownError<E extends typeof KnownErrors[keyof KnownErrors]>(
path: string,
requestOptions: RequestInit,
tokenStoreOrNull: Session | null,
tokenStoreOrNull: InternalSession | null,
errorsToCatch: readonly E[],
): Promise<Result<
Response & {
@ -296,7 +296,7 @@ export class StackClientInterface {
private async sendClientRequestInner(
path: string,
options: RequestInit,
session: Session,
session: InternalSession,
requestType: "client" | "server" | "admin",
): Promise<Result<Response & {
usedTokens: {
@ -471,7 +471,7 @@ export class StackClientInterface {
async sendVerificationEmail(
emailVerificationRedirectUrl: string,
session: Session
session: InternalSession
): Promise<KnownErrors["EmailAlreadyVerified"] | undefined> {
const res = await this.sendClientRequestAndCatchKnownError(
"/auth/send-verification-email",
@ -541,7 +541,7 @@ export class StackClientInterface {
async updatePassword(
options: { oldPassword: string, newPassword: string },
session: Session
session: InternalSession
): Promise<KnownErrors["PasswordMismatch"] | KnownErrors["PasswordRequirementsNotMet"] | undefined> {
const res = await this.sendClientRequestAndCatchKnownError(
"/auth/update-password",
@ -593,7 +593,7 @@ export class StackClientInterface {
async signInWithCredential(
email: string,
password: string,
session: Session
session: InternalSession
): Promise<KnownErrors["EmailPasswordMismatch"] | { accessToken: string, refreshToken: string }> {
const res = await this.sendClientRequestAndCatchKnownError(
"/auth/signin",
@ -626,7 +626,7 @@ export class StackClientInterface {
email: string,
password: string,
emailVerificationRedirectUrl: string,
session: Session,
session: InternalSession,
): Promise<KnownErrors["UserEmailAlreadyExists"] | KnownErrors["PasswordRequirementsNotMet"] | { accessToken: string, refreshToken: string }> {
const res = await this.sendClientRequestAndCatchKnownError(
"/auth/signup",
@ -656,7 +656,7 @@ export class StackClientInterface {
};
}
async signInWithMagicLink(code: string, session: Session): Promise<KnownErrors["MagicLinkError"] | { newUser: boolean, accessToken: string, refreshToken: string }> {
async signInWithMagicLink(code: string, session: InternalSession): Promise<KnownErrors["MagicLinkError"] | { newUser: boolean, accessToken: string, refreshToken: string }> {
const res = await this.sendClientRequestAndCatchKnownError(
"/auth/magic-link-verification",
{
@ -766,7 +766,7 @@ export class StackClientInterface {
};
}
async signOut(session: Session): Promise<void> {
async signOut(session: InternalSession): Promise<void> {
const tokenObj = await session.getPotentiallyExpiredTokens();
if (tokenObj) {
if (!tokenObj.refreshToken) {
@ -789,10 +789,10 @@ export class StackClientInterface {
await res.json();
}
}
session.invalidate();
session.markInvalid();
}
async getClientUserByToken(tokenStore: Session): Promise<Result<UserJson>> {
async getClientUserByToken(tokenStore: InternalSession): Promise<Result<UserJson>> {
const response = await this.sendClientRequest(
"/current-user",
{},
@ -809,7 +809,7 @@ export class StackClientInterface {
type: 'global' | 'team',
direct: boolean,
},
session: Session
session: InternalSession
): Promise<PermissionDefinitionJson[]> {
const response = await this.sendClientRequest(
`/current-user/teams/${options.teamId}/permissions?type=${options.type}&direct=${options.direct}`,
@ -820,7 +820,7 @@ export class StackClientInterface {
return permissions;
}
async listClientUserTeams(session: Session): Promise<TeamJson[]> {
async listClientUserTeams(session: InternalSession): Promise<TeamJson[]> {
const response = await this.sendClientRequest(
"/current-user/teams",
{},
@ -837,7 +837,7 @@ export class StackClientInterface {
return Result.ok(project);
}
async setClientUserCustomizableData(update: UserUpdateJson, session: Session) {
async setClientUserCustomizableData(update: UserUpdateJson, session: InternalSession) {
await this.sendClientRequest(
"/current-user",
{
@ -851,7 +851,7 @@ export class StackClientInterface {
);
}
async listProjects(session: Session): Promise<ProjectJson[]> {
async listProjects(session: InternalSession): Promise<ProjectJson[]> {
const response = await this.sendClientRequest("/projects", {}, session);
if (!response.ok) {
throw new Error("Failed to list projects: " + response.status + " " + (await response.text()));
@ -863,7 +863,7 @@ export class StackClientInterface {
async createProject(
project: ProjectUpdateOptions & { displayName: string },
session: Session,
session: InternalSession,
): Promise<ProjectJson> {
const fetchResponse = await this.sendClientRequest(
"/projects",

View File

@ -11,7 +11,7 @@ import {
import { Result } from "../utils/results";
import { ReadonlyJson } from "../utils/json";
import { EmailTemplateCrud, ListEmailTemplatesCrud } from "./crud/email-templates";
import { Session } from "../sessions";
import { InternalSession } from "../sessions";
export type ServerUserJson = UserJson & {
serverMetadata: ReadonlyJson,
@ -50,7 +50,7 @@ export type ServerAuthApplicationOptions = (
readonly secretServerKey: string,
}
| {
readonly projectOwnerSession: Session,
readonly projectOwnerSession: InternalSession,
}
)
);
@ -63,7 +63,7 @@ export class StackServerInterface extends StackClientInterface {
super(options);
}
protected async sendServerRequest(path: string, options: RequestInit, session: Session | null, requestType: "server" | "admin" = "server") {
protected async sendServerRequest(path: string, options: RequestInit, session: InternalSession | null, requestType: "server" | "admin" = "server") {
return await this.sendClientRequest(
path,
{
@ -78,7 +78,7 @@ export class StackServerInterface extends StackClientInterface {
);
}
async getServerUserByToken(session: Session): Promise<Result<ServerUserJson>> {
async getServerUserByToken(session: InternalSession): Promise<Result<ServerUserJson>> {
const response = await this.sendServerRequest(
"/current-user?server=true",
{},
@ -106,7 +106,7 @@ export class StackServerInterface extends StackClientInterface {
type: 'global' | 'team',
direct: boolean,
},
session: Session
session: InternalSession
): Promise<ServerPermissionDefinitionJson[]> {
const response = await this.sendServerRequest(
`/current-user/teams/${options.teamId}/permissions?type=${options.type}&direct=${options.direct}&server=true`,
@ -117,7 +117,7 @@ export class StackServerInterface extends StackClientInterface {
return permissions;
}
async listServerUserTeams(session: Session): Promise<ServerTeamJson[]> {
async listServerUserTeams(session: InternalSession): Promise<ServerTeamJson[]> {
const response = await this.sendServerRequest(
"/current-user/teams?server=true",
{},

View File

@ -16,17 +16,17 @@ export class RefreshToken {
}
/**
* A session represents a user's session, which may or may not be valid. It may contain an access token, a refresh token, or both.
* An InternalSession represents a user's session, which may or may not be valid. It may contain an access token, a refresh token, or both.
*
* A session never changes which user or session it belongs to, but the tokens may change over time.
* A session never changes which user or session it belongs to, but the tokens in it may change over time.
*/
export class Session {
export class InternalSession {
/**
* Each session has a session key that depends on the tokens inside. If the session has a refresh token, the session key depends only on the refresh token. If the session does not have a refresh token, the session key depends only on the access token.
*
* Multiple Session objects may have the same session key, which implies that they represent the same session by the same user. Furthermore, a session's key never changes over the lifetime of a session object.
*
* This makes session keys useful for caching and indexing sessions.
* This is useful for caching and indexing sessions.
*/
public readonly sessionKey: string;
@ -52,7 +52,7 @@ export class Session {
}) {
this._accessToken = new Store(_options.accessToken ? new AccessToken(_options.accessToken) : null);
this._refreshToken = _options.refreshToken ? new RefreshToken(_options.refreshToken) : null;
this.sessionKey = Session.calculateSessionKey({ accessToken: _options.accessToken ?? null, refreshToken: _options.refreshToken });
this.sessionKey = InternalSession.calculateSessionKey({ accessToken: _options.accessToken ?? null, refreshToken: _options.refreshToken });
}
static calculateSessionKey(ofTokens: { refreshToken: string | null, accessToken?: string | null }): string {
@ -65,7 +65,10 @@ export class Session {
}
}
invalidate() {
/**
* Marks the session object as invalid, meaning that the refresh and access tokens can no longer be used.
*/
markInvalid() {
this._accessToken.set(null);
this._knownToBeInvalid.set(true);
}
@ -74,12 +77,28 @@ export class Session {
return this._knownToBeInvalid.onChange(() => callback());
}
/**
* Returns the access token if it is found in the cache, fetching it otherwise.
*
* This is usually the function you want to call to get an access token. When using the access token, you should catch errors that occur if it expires, and call `markAccessTokenExpired` to mark the token as expired if so (after which a call to this function will always refetch the token).
*
* @returns null if the session is known to be invalid, cached tokens if they exist in the cache (which may or may not be valid still), or new tokens otherwise.
*/
async getPotentiallyExpiredTokens(): Promise<{ accessToken: AccessToken, refreshToken: RefreshToken | null } | null> {
const accessToken = await this._getPotentiallyExpiredAccessToken();
return accessToken ? { accessToken, refreshToken: this._refreshToken } : null;
}
async getNewlyFetchedTokens(): Promise<{ accessToken: AccessToken, refreshToken: RefreshToken | null } | null> {
/**
* Fetches new tokens that are, at the time of fetching, guaranteed to be valid.
*
* The newly generated tokens are shortlived, so it's good practice not to rely on their validity (if possible). However, this function is useful in some cases where you only want to pass access tokens to a service, and you want to make sure said access token has the longest possible lifetime.
*
* In most cases, you should prefer `getPotentiallyExpiredTokens` with a fallback to `markAccessTokenExpired` and a retry mechanism if the endpoint rejects the token.
*
* @returns null if the session is known to be invalid, or new tokens otherwise (which, at the time of fetching, are guaranteed to be valid).
*/
async fetchNewTokens(): Promise<{ accessToken: AccessToken, refreshToken: RefreshToken | null } | null> {
const accessToken = await this._getNewlyFetchedAccessToken();
return accessToken ? { accessToken, refreshToken: this._refreshToken } : null;
}
@ -132,7 +151,7 @@ export class Session {
this._refreshPromise = null;
this._accessToken.set(accessToken);
if (!accessToken) {
this.invalidate();
this.markInvalid();
}
}
return accessToken;

View File

@ -22,8 +22,9 @@ import { EmailTemplateCrud, ListEmailTemplatesCrud } from "@stackframe/stack-sha
import { scrambleDuringCompileTime } from "@stackframe/stack-shared/dist/utils/compile-time";
import { isReactServer } from "@stackframe/stack-sc";
import * as cookie from "cookie";
import { Session } from "@stackframe/stack-shared/dist/sessions";
import { InternalSession } from "@stackframe/stack-shared/dist/sessions";
import { useTrigger } from "@stackframe/stack-shared/dist/hooks/use-trigger";
import { pick } from "@stackframe/stack-shared/src/utils/objects";
// NextNavigation.useRouter does not exist in react-server environments and some bundlers try to be helpful and throw a warning. Ignore the warning.
@ -137,7 +138,7 @@ export type StackAdminAppConstructorOptions<HasTokenStore extends boolean, Proje
| (
& Omit<StackServerAppConstructorOptions<HasTokenStore, ProjectId>, "publishableClientKey" | "secretServerKey">
& {
projectOwnerSession: Session,
projectOwnerSession: InternalSession,
}
)
);
@ -249,8 +250,8 @@ const createCache = <D extends any[], T>(fetcher: (dependencies: D) => Promise<T
);
};
const createCacheBySession = <D extends any[], T>(fetcher: (session: Session, extraDependencies: D) => Promise<T> ) => {
return new AsyncCache<[Session, ...D], T>(
const createCacheBySession = <D extends any[], T>(fetcher: (session: InternalSession, extraDependencies: D) => Promise<T> ) => {
return new AsyncCache<[InternalSession, ...D], T>(
async ([session, ...extraDependencies]) => await fetcher(session, extraDependencies),
{
onSubscribe: ([session], refresh) => {
@ -384,12 +385,30 @@ class _StackClientAppImpl<HasTokenStore extends boolean, ProjectId extends strin
case "memory": {
return this._memoryTokenStore;
}
case null: {
return createEmptyTokenStore();
}
default: {
if (tokenStoreInit !== null && typeof tokenStoreInit === "object" && "headers" in tokenStoreInit) {
if (tokenStoreInit === null) {
return createEmptyTokenStore();
} else if (typeof tokenStoreInit === "object" && "headers" in tokenStoreInit) {
if (this._requestTokenStores.has(tokenStoreInit)) return this._requestTokenStores.get(tokenStoreInit)!;
// x-stack-auth header
const stackAuthHeader = tokenStoreInit.headers.get("x-stack-auth");
if (stackAuthHeader) {
let parsed;
try {
parsed = JSON.parse(stackAuthHeader);
if (typeof parsed !== "object") throw new Error("x-stack-auth header must be a JSON object");
if (parsed === null) throw new Error("x-stack-auth header must not be null");
} catch (e) {
throw new Error(`Invalid x-stack-auth header: ${stackAuthHeader}`, { cause: e });
}
return this._getOrCreateTokenStore({
accessToken: parsed.accessToken ?? null,
refreshToken: parsed.refreshToken ?? null,
});
}
// read from cookies
const cookieHeader = tokenStoreInit.headers.get("cookie");
const parsed = cookie.parse(cookieHeader || "");
const res = new Store<TokenObject>({
@ -398,6 +417,11 @@ class _StackClientAppImpl<HasTokenStore extends boolean, ProjectId extends strin
});
this._requestTokenStores.set(tokenStoreInit, res);
return res;
} else if ("accessToken" in tokenStoreInit || "refreshToken" in tokenStoreInit) {
return new Store<TokenObject>({
refreshToken: tokenStoreInit.refreshToken,
accessToken: tokenStoreInit.accessToken,
});
}
throw new Error(`Invalid token store ${tokenStoreInit}`);
@ -413,10 +437,10 @@ class _StackClientAppImpl<HasTokenStore extends boolean, ProjectId extends strin
* - So we can garbage-collect Session objects when the token store is garbage-collected
* - So different token stores are separated and don't leak information between each other, eg. if the same user sends two requests to the same server they should get a different session object
*/
private _sessionsByTokenStoreAndSessionKey = new WeakMap<Store<TokenObject>, Map<string, Session>>();
protected _getSessionFromTokenStore(tokenStore: Store<TokenObject>): Session {
private _sessionsByTokenStoreAndSessionKey = new WeakMap<Store<TokenObject>, Map<string, InternalSession>>();
protected _getSessionFromTokenStore(tokenStore: Store<TokenObject>): InternalSession {
const tokenObj = tokenStore.get();
const sessionKey = Session.calculateSessionKey(tokenObj);
const sessionKey = InternalSession.calculateSessionKey(tokenObj);
const existing = sessionKey ? this._sessionsByTokenStoreAndSessionKey.get(tokenStore)?.get(sessionKey) : null;
if (existing) return existing;
@ -443,11 +467,11 @@ class _StackClientAppImpl<HasTokenStore extends boolean, ProjectId extends strin
sessionsBySessionKey.set(sessionKey, session);
return session;
}
protected _getSession(overrideTokenStoreInit?: TokenStoreInit): Session {
protected _getSession(overrideTokenStoreInit?: TokenStoreInit): InternalSession {
const tokenStore = this._getOrCreateTokenStore(overrideTokenStoreInit);
return this._getSessionFromTokenStore(tokenStore);
}
protected _useSession(overrideTokenStoreInit?: TokenStoreInit): Session {
protected _useSession(overrideTokenStoreInit?: TokenStoreInit): InternalSession {
const tokenStore = this._getOrCreateTokenStore(overrideTokenStoreInit);
const subscribe = useCallback((cb: () => void) => {
const { unsubscribe } = tokenStore.onChange(() => cb());
@ -456,7 +480,6 @@ class _StackClientAppImpl<HasTokenStore extends boolean, ProjectId extends strin
const getSnapshot = useCallback(() => this._getSessionFromTokenStore(tokenStore), [tokenStore]);
return React.useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
}
protected async _signInToAccountWithTokens(tokens: { accessToken: string | null, refreshToken: string }) {
const tokenStore = this._getOrCreateTokenStore();
@ -599,14 +622,23 @@ class _StackClientAppImpl<HasTokenStore extends boolean, ProjectId extends strin
};
}
protected _currentUserFromJson(json: UserJson, session: Session): ProjectCurrentUser<ProjectId>;
protected _currentUserFromJson(json: UserJson | null, session: Session): ProjectCurrentUser<ProjectId> | null;
protected _currentUserFromJson(json: UserJson | null, session: Session): ProjectCurrentUser<ProjectId> | null {
protected _currentUserFromJson(json: UserJson, session: InternalSession): ProjectCurrentUser<ProjectId>;
protected _currentUserFromJson(json: UserJson | null, session: InternalSession): ProjectCurrentUser<ProjectId> | null;
protected _currentUserFromJson(json: UserJson | null, session: InternalSession): ProjectCurrentUser<ProjectId> | null {
if (json === null) return null;
const app = this;
const currentUser: CurrentUser = {
...this._userFromJson(json),
session,
_internalSession: session,
currentSession: {
async getTokens() {
const tokens = await session.getPotentiallyExpiredTokens();
return {
accessToken: tokens?.accessToken.token ?? null,
refreshToken: tokens?.refreshToken?.token ?? null,
};
},
},
async updateSelectedTeam(team: Team | null) {
await app._updateUser({ selectedTeamId: team?.id ?? null }, session);
},
@ -685,7 +717,7 @@ class _StackClientAppImpl<HasTokenStore extends boolean, ProjectId extends strin
};
}
protected _createAdminInterface(forProjectId: string, session: Session): StackAdminInterface {
protected _createAdminInterface(forProjectId: string, session: InternalSession): StackAdminInterface {
return new StackAdminInterface({
baseUrl: this._interface.options.baseUrl,
projectId: forProjectId,
@ -702,6 +734,19 @@ class _StackClientAppImpl<HasTokenStore extends boolean, ProjectId extends strin
return getUrls(this._urlOptions);
}
async getCrossOriginHeaders(): Promise<{ "x-stack-auth": string }> {
return {
"x-stack-auth": JSON.stringify(await this.getCrossOriginTokenObject()),
};
}
async getCrossOriginTokenObject(): Promise<{ accessToken: string | null, refreshToken: string | null }> {
const user = await this.getUser();
if (!user) return { accessToken: null, refreshToken: null };
const tokens = await user.currentSession.getTokens();
return tokens;
}
protected async _redirectTo(handlerName: keyof HandlerUrls, options?: RedirectToOptions) {
const url = this.urls[handlerName];
if (!url) {
@ -828,7 +873,7 @@ class _StackClientAppImpl<HasTokenStore extends boolean, ProjectId extends strin
});
}
protected async _updateUser(update: UserUpdateJson, session: Session) {
protected async _updateUser(update: UserUpdateJson, session: InternalSession) {
const res = await this._interface.setClientUserCustomizableData(update, session);
await this._refreshUser(session);
return res;
@ -904,19 +949,19 @@ class _StackClientAppImpl<HasTokenStore extends boolean, ProjectId extends strin
return false;
}
protected async _signOut(session: Session): Promise<void> {
protected async _signOut(session: InternalSession): Promise<void> {
await this._interface.signOut(session);
await this.redirectToAfterSignOut();
}
protected async _sendVerificationEmail(session: Session): Promise<KnownErrors["EmailAlreadyVerified"] | void> {
protected async _sendVerificationEmail(session: InternalSession): Promise<KnownErrors["EmailAlreadyVerified"] | void> {
const emailVerificationRedirectUrl = constructRedirectUrl(this.urls.emailVerification);
return await this._interface.sendVerificationEmail(emailVerificationRedirectUrl, session);
}
protected async _updatePassword(
options: { oldPassword: string, newPassword: string },
session: Session
session: InternalSession
): Promise<KnownErrors["PasswordMismatch"] | KnownErrors["PasswordRequirementsNotMet"] | void> {
return await this._interface.updatePassword(options, session);
}
@ -987,7 +1032,7 @@ class _StackClientAppImpl<HasTokenStore extends boolean, ProjectId extends strin
return res;
}
protected async _refreshUser(session: Session) {
protected async _refreshUser(session: InternalSession) {
await this._currentUserCache.refresh([session]);
}
@ -999,7 +1044,7 @@ class _StackClientAppImpl<HasTokenStore extends boolean, ProjectId extends strin
await this._currentProjectCache.refresh([]);
}
protected async _refreshOwnedProjects(session: Session) {
protected async _refreshOwnedProjects(session: InternalSession) {
await this._ownedProjectsCache.refresh([session]);
}
@ -1200,15 +1245,24 @@ class _StackServerAppImpl<HasTokenStore extends boolean, ProjectId extends strin
};
}
protected _currentServerUserFromJson(json: ServerUserJson, session: Session): ProjectCurrentSeverUser<ProjectId>;
protected _currentServerUserFromJson(json: ServerUserJson | null, session: Session): ProjectCurrentSeverUser<ProjectId> | null;
protected _currentServerUserFromJson(json: ServerUserJson | null, session: Session): ProjectCurrentSeverUser<ProjectId> | null {
protected _currentServerUserFromJson(json: ServerUserJson, session: InternalSession): ProjectCurrentSeverUser<ProjectId>;
protected _currentServerUserFromJson(json: ServerUserJson | null, session: InternalSession): ProjectCurrentSeverUser<ProjectId> | null;
protected _currentServerUserFromJson(json: ServerUserJson | null, session: InternalSession): ProjectCurrentSeverUser<ProjectId> | null {
if (json === null) return null;
const app = this;
const nonCurrentServerUser = this._serverUserFromJson(json);
const currentUser: CurrentServerUser = {
...nonCurrentServerUser,
session,
_internalSession: session,
currentSession: {
async getTokens() {
const tokens = await session.getPotentiallyExpiredTokens();
return {
accessToken: tokens?.accessToken.token ?? null,
refreshToken: tokens?.refreshToken?.token ?? null,
};
},
},
async delete() {
const res = await nonCurrentServerUser.delete();
await app._refreshUser(session);
@ -1433,7 +1487,7 @@ class _StackServerAppImpl<HasTokenStore extends boolean, ProjectId extends strin
}, [teams, teamId]);
}
protected override async _refreshUser(session: Session) {
protected override async _refreshUser(session: InternalSession) {
await Promise.all([
super._refreshUser(session),
this._currentServerUserCache.refresh([session]),
@ -1606,11 +1660,21 @@ type RedirectToOptions = {
replace?: boolean,
};
type Session = {
getTokens(): Promise<{ accessToken: string | null, refreshToken: string | null }>,
};
/**
* Contains everything related to the current user session.
*/
type Auth<T, C> = {
readonly session: Session,
updateSelectedTeam(this: T, team: Team | null): Promise<void>,
update(this: T, user: C): Promise<void>,
readonly _internalSession: InternalSession,
readonly currentSession: Session,
signOut(this: T): Promise<void>,
// TODO these should not actually be here
update(this: T, user: C): Promise<void>,
updateSelectedTeam(this: T, team: Team | null): Promise<void>,
sendVerificationEmail(this: T): Promise<KnownErrors["EmailAlreadyVerified"] | void>,
updatePassword(this: T, options: { oldPassword: string, newPassword: string}): Promise<KnownErrors["PasswordMismatch"] | KnownErrors["PasswordRequirementsNotMet"] | void>,
};
@ -1840,6 +1904,66 @@ export type StackClientApp<HasTokenStore extends boolean = boolean, ProjectId ex
verifyEmail(code: string): Promise<KnownErrors["EmailVerificationError"] | void>,
signInWithMagicLink(code: string): Promise<KnownErrors["MagicLinkError"] | void>,
/**
* With most browsers now disabling third-party cookies by default, the best way to send authenticated requests
* across different origins is to pass the tokens in a header.
*
* This function returns a header object that can be used with `fetch` or other HTTP request libraries to send
* authenticated requests.
*
* On the server, you can then pass in the `Request` object to the `tokenStore` option
* on your Stack app to fetch user details. Please note that CORS by default does not allow custom headers, so you
* must set the [`Access-Control-Allow-Headers` header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Headers)
* to include `x-stack-auth` in the CORS preflight response.
*
* Example:
*
* ```ts
* // client
* const res = await fetch("https://api.example.com", {
* headers: {
* ...await stackApp.getCrossOriginHeaders()
* // you can also add your own headers here
* },
* });
*
* // server
* function handleRequest(req: Request) {
* const user = await stackServerApp.getUser({ tokenStore: req });
* return new Response("Welcome, " + user.displayName);
* }
* ```
*/
getCrossOriginHeaders(): Promise<{ "x-stack-auth": string }>,
/**
* With most browsers now disabling third-party cookies by default, there need to be new ways to send authenticated
* requests across different origins. While `getCrossOriginHeaders` is the recommended way to do this, there
* are some cases where you might want to send the tokens differently, for example when you are using WebSockets
* or non-HTTP protocols.
*
* This function returns a token object that can be JSON-serialized and sent to the server in any way you like.
* There, you can use the `tokenStore` option on your Stack app to fetch user details.
*
* Example:
*
* ```ts
* // client
* const res = await rpcCall(rpcEndpoint, {
* data: {
* auth: await stackApp.getCrossOriginTokenObject(),
* },
* });
*
* // server
* function handleRequest(data) {
* const user = await stackServerApp.getUser({ tokenStore: data.auth });
* return new Response("Welcome, " + user.displayName);
* }
* ```
*/
getCrossOriginTokenObject(): Promise<{ accessToken: string | null, refreshToken: string | null }>,
[stackAppInternalsSymbol]: {
toClientJson(): StackClientAppJson<HasTokenStore, ProjectId>,
setCurrentUser(userJsonPromise: Promise<UserJson | null>): void,