import { KnownErrors } from "../known-errors"; import { AccessToken, InternalSession, RefreshToken } from "../sessions"; import { StackAssertionError } from "../utils/errors"; import { filterUndefined } from "../utils/objects"; import { Result } from "../utils/results"; import { ClientInterfaceOptions, StackClientInterface } from "./clientInterface"; import { ContactChannelsCrud } from "./crud/contact-channels"; import { CurrentUserCrud } from "./crud/current-user"; import { ConnectedAccountAccessTokenCrud } from "./crud/oauth"; import { TeamMemberProfilesCrud } from "./crud/team-member-profiles"; import { TeamMembershipsCrud } from "./crud/team-memberships"; import { TeamPermissionsCrud } from "./crud/team-permissions"; import { TeamsCrud } from "./crud/teams"; import { UsersCrud } from "./crud/users"; export type ServerAuthApplicationOptions = ( & ClientInterfaceOptions & ( | { readonly secretServerKey: string, } | { readonly projectOwnerSession: InternalSession, } ) ); export class StackServerInterface extends StackClientInterface { constructor(public override options: ServerAuthApplicationOptions) { super(options); } protected async sendServerRequest(path: string, options: RequestInit, session: InternalSession | null, requestType: "server" | "admin" = "server") { return await this.sendClientRequest( path, { ...options, headers: { "x-stack-secret-server-key": "secretServerKey" in this.options ? this.options.secretServerKey : "", ...options.headers, }, }, session, requestType, ); } protected async sendServerRequestAndCatchKnownError( path: string, requestOptions: RequestInit, tokenStoreOrNull: InternalSession | null, errorsToCatch: readonly E[], ): Promise >> { try { return Result.ok(await this.sendServerRequest(path, requestOptions, tokenStoreOrNull)); } catch (e) { for (const errorType of errorsToCatch) { if (e instanceof errorType) { return Result.error(e as InstanceType); } } throw e; } } async createServerUser(data: UsersCrud['Server']['Create']): Promise { const response = await this.sendServerRequest( "/users", { method: "POST", headers: { "content-type": "application/json", }, body: JSON.stringify(data), }, null, ); return await response.json(); } async getServerUserByToken(session: InternalSession): Promise { const responseOrError = await this.sendServerRequestAndCatchKnownError( "/users/me", {}, session, [KnownErrors.CannotGetOwnUserWithoutUser], ); if (responseOrError.status === "error") { if (responseOrError.error instanceof KnownErrors.CannotGetOwnUserWithoutUser) { return null; } else { throw new StackAssertionError("Unexpected uncaught error", { cause: responseOrError.error }); } } const response = responseOrError.data; const user: CurrentUserCrud['Server']['Read'] = await response.json(); if (!(user as any)) throw new StackAssertionError("User endpoint returned null; this should never happen"); return user; } async getServerUserById(userId: string): Promise> { const response = await this.sendServerRequest( `/users/${userId}`, {}, null, ); const user: CurrentUserCrud['Server']['Read'] | null = await response.json(); if (!user) return Result.error(new Error("Failed to get user")); return Result.ok(user); } async listServerTeamMemberProfiles( options: { teamId: string, }, ): Promise { const response = await this.sendServerRequest( "/team-member-profiles?team_id=" + options.teamId, {}, null, ); const result = await response.json() as TeamMemberProfilesCrud['Server']['List']; return result.items; } async getServerTeamMemberProfile( options: { teamId: string, userId: string, }, ): Promise { const response = await this.sendServerRequest( `/team-member-profiles/${options.teamId}/${options.userId}`, {}, null, ); return await response.json(); } async listServerTeamPermissions( options: { userId?: string, teamId?: string, recursive: boolean, }, session: InternalSession | null, ): Promise { const response = await this.sendServerRequest( "/team-permissions?" + new URLSearchParams(filterUndefined({ user_id: options.userId, team_id: options.teamId, recursive: options.recursive.toString(), })), {}, session, ); const result = await response.json() as TeamPermissionsCrud['Server']['List']; return result.items; } async listServerUsers(options: { cursor?: string, limit?: number, orderBy?: 'signedUpAt', desc?: boolean, query?: string, }): Promise { const searchParams = new URLSearchParams(filterUndefined({ cursor: options.cursor, limit: options.limit?.toString(), desc: options.desc?.toString(), ...options.orderBy ? { order_by: { signedUpAt: "signed_up_at", }[options.orderBy], } : {}, ...options.query ? { query: options.query, } : {}, })); const response = await this.sendServerRequest("/users?" + searchParams.toString(), {}, null); return await response.json(); } async listServerTeams(options?: { userId?: string, }): Promise { const response = await this.sendServerRequest( "/teams?" + new URLSearchParams(filterUndefined({ user_id: options?.userId, })), {}, null ); const result = await response.json() as TeamsCrud['Server']['List']; return result.items; } async listServerTeamUsers(teamId: string): Promise { const response = await this.sendServerRequest(`/users?team_id=${teamId}`, {}, null); const result = await response.json() as UsersCrud['Server']['List']; return result.items; } /* when passing a session, the user will be added to the team */ async createServerTeam(data: TeamsCrud['Server']['Create']): Promise { const response = await this.sendServerRequest( "/teams", { method: "POST", headers: { "content-type": "application/json", }, body: JSON.stringify(data), }, null ); return await response.json(); } async updateServerTeam(teamId: string, data: TeamsCrud['Server']['Update']): Promise { const response = await this.sendServerRequest( `/teams/${teamId}`, { method: "PATCH", headers: { "content-type": "application/json", }, body: JSON.stringify(data), }, null, ); return await response.json(); } async deleteServerTeam(teamId: string): Promise { await this.sendServerRequest( `/teams/${teamId}`, { method: "DELETE" }, null, ); } async addServerUserToTeam(options: { userId: string, teamId: string, }): Promise { const response = await this.sendServerRequest( `/team-memberships/${options.teamId}/${options.userId}`, { method: "POST", headers: { "content-type": "application/json", }, body: JSON.stringify({}), }, null, ); return await response.json(); } async removeServerUserFromTeam(options: { userId: string, teamId: string, }) { await this.sendServerRequest( `/team-memberships/${options.teamId}/${options.userId}`, { method: "DELETE", headers: { "content-type": "application/json", }, body: JSON.stringify({}), }, null, ); } async updateServerUser(userId: string, update: UsersCrud['Server']['Update']): Promise { const response = await this.sendServerRequest( `/users/${userId}`, { method: "PATCH", headers: { "content-type": "application/json", }, body: JSON.stringify(update), }, null, ); return await response.json(); } async createServerProviderAccessToken( userId: string, provider: string, scope: string, ): Promise { const response = await this.sendServerRequest( `/connected-accounts/${userId}/${provider}/access-token`, { method: "POST", headers: { "content-type": "application/json", }, body: JSON.stringify({ scope }), }, null, ); return await response.json(); } async createServerUserSession(userId: string, expiresInMillis: number): Promise<{ accessToken: string, refreshToken: string }> { const response = await this.sendServerRequest( "/auth/sessions", { method: "POST", headers: { "content-type": "application/json", }, body: JSON.stringify({ user_id: userId, expires_in_millis: expiresInMillis, }), }, null, ); const result = await response.json(); return { accessToken: result.access_token, refreshToken: result.refresh_token, }; } async leaveServerTeam( options: { teamId: string, userId: string, }, ) { await this.sendClientRequest( `/team-memberships/${options.teamId}/${options.userId}`, { method: "DELETE", headers: { "content-type": "application/json", }, body: JSON.stringify({}), }, null, ); } async updateServerTeamMemberProfile(options: { teamId: string, userId: string, profile: TeamMemberProfilesCrud['Server']['Update'], }) { await this.sendServerRequest( `/team-member-profiles/${options.teamId}/${options.userId}`, { method: "PATCH", headers: { "content-type": "application/json", }, body: JSON.stringify(options.profile), }, null, ); } async grantServerTeamUserPermission(teamId: string, userId: string, permissionId: string) { await this.sendServerRequest( `/team-permissions/${teamId}/${userId}/${permissionId}`, { method: "POST", headers: { "content-type": "application/json", }, body: JSON.stringify({}), }, null, ); } async revokeServerTeamUserPermission(teamId: string, userId: string, permissionId: string) { await this.sendServerRequest( `/team-permissions/${teamId}/${userId}/${permissionId}`, { method: "DELETE", headers: { "content-type": "application/json", }, body: JSON.stringify({}), }, null, ); } async deleteServerServerUser(userId: string) { await this.sendServerRequest( `/users/${userId}`, { method: "DELETE", headers: { "content-type": "application/json", }, body: JSON.stringify({}), }, null, ); } async createServerContactChannel( data: ContactChannelsCrud['Server']['Create'], ): Promise { const response = await this.sendServerRequest( "/contact-channels", { method: "POST", headers: { "content-type": "application/json", }, body: JSON.stringify(data), }, null, ); return await response.json(); } async updateServerContactChannel( userId: string, contactChannelId: string, data: ContactChannelsCrud['Server']['Update'], ): Promise { const response = await this.sendServerRequest( `/contact-channels/${userId}/${contactChannelId}`, { method: "PATCH", headers: { "content-type": "application/json", }, body: JSON.stringify(data), }, null, ); return await response.json(); } async deleteServerContactChannel( userId: string, contactChannelId: string, ): Promise { await this.sendServerRequest( `/contact-channels/${userId}/${contactChannelId}`, { method: "DELETE", }, null, ); } async listServerContactChannels( userId: string, ): Promise { const response = await this.sendServerRequest( `/contact-channels?user_id=${userId}`, { method: "GET", }, null, ); const json = await response.json() as ContactChannelsCrud['Server']['List']; return json.items; } async sendServerContactChannelVerificationEmail( userId: string, contactChannelId: string, callbackUrl: string, ): Promise { await this.sendServerRequest( `/contact-channels/${userId}/${contactChannelId}/send-verification-code`, { method: "POST", headers: { "content-type": "application/json", }, body: JSON.stringify({ callback_url: callbackUrl }), }, null, ); } async sendServerTeamInvitation(options: { email: string, teamId: string, callbackUrl: string, }): Promise { await this.sendServerRequest( "/team-invitations/send-code", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ email: options.email, team_id: options.teamId, callback_url: options.callbackUrl, }), }, null, ); } }