import { type AuthState, AuthStateType, type CurrentUser, getAuthState, type LoggedInAuthState, logout, type SanityInstance, } from "@sanity/sdk"; import { and, assign, fromCallback, fromObservable, fromPromise, setup, stateIn, } from "xstate"; import { os } from "../runtime/bus"; import type { OSBaseInput } from "./root.machine"; /** * The session context the auth machine establishes. * @internal */ export interface AuthInput extends OSBaseInput {} const authStateLogic = fromObservable( ({ input }) => getAuthState(input.instance).observable, ); // Forwards `auth.token.refresh` requests into the machine, so `publishToken` // stays the sole `auth.token` emitter: the machine re-publishes the token and // replies to the caller, modelling a genuine token request. Per-app re-issuance // slots into that handler later — today it resends the session's own token. const tokenRefreshResponder = fromCallback(({ sendBack }) => { const stop = new AbortController(); os.subscribe( "auth.token.refresh", (message) => { sendBack({ type: "token.refresh", reply: message.reply }); }, { signal: stop.signal }, ); return () => stop.abort(); }); /** * @internal */ export interface LogoutInput extends OSBaseInput {} const logoutActorLogic = fromPromise(async ({ input }) => { await logout(input.instance); }); type AuthContext = { instance: SanityInstance; token: string | null; currentUser: CurrentUser | null; error: unknown; }; type AuthEvent = | { type: "auth.logout" } | { type: "token.refresh"; reply: (token: string) => void }; const isLoggedInComplete = (state: AuthState | undefined): boolean => state?.type === AuthStateType.LOGGED_IN && Boolean((state as LoggedInAuthState).token) && (state as LoggedInAuthState).currentUser !== null; const sessionChanged = ( context: AuthContext, state: LoggedInAuthState, ): boolean => state.token !== context.token || state.currentUser?.id !== context.currentUser?.id || state.currentUser?.name !== context.currentUser?.name || state.currentUser?.email !== context.currentUser?.email || state.currentUser?.profileImage !== context.currentUser?.profileImage; export const authLogic = setup({ types: { input: {} as AuthInput, context: {} as AuthContext, events: {} as AuthEvent, tags: {} as "authenticating" | "authenticated" | "error", }, actors: { authState: authStateLogic, logoutActor: logoutActorLogic, tokenRefreshResponder, }, delays: { authTimeout: 30_000, }, guards: { isLoggedInComplete: (_, params: { state: AuthState | undefined }) => isLoggedInComplete(params.state), isAuthState: ( _, params: { state: AuthState | undefined; type: AuthStateType }, ) => params.state?.type === params.type, }, actions: { publishToken: ({ context }) => { os.emit("auth.token", context.token); }, replyToken: ({ context }, params: { reply: (token: string) => void }) => { if (context.token) params.reply(context.token); }, publishCurrentUser: ({ context }) => { const user = context.currentUser; os.emit( "users.current", user && { id: user.id, name: user.name, email: user.email, profileImage: user.profileImage, }, ); }, setLoggedIn: assign({ token: (_, params: { token: string; currentUser: CurrentUser }) => params.token, currentUser: (_, params: { token: string; currentUser: CurrentUser }) => params.currentUser, error: () => null, }), clearAuth: assign({ token: () => null, currentUser: () => null, error: () => null, }), setError: assign({ token: () => null, currentUser: () => null, error: (_, params: { error: unknown }) => params.error, }), }, }).createMachine({ id: "auth", initial: "init", context: ({ input }) => ({ instance: input.instance, token: null, currentUser: null, error: null, }), invoke: { src: "authState", input: ({ context }) => ({ instance: context.instance }), onSnapshot: [ // Already logged in and the session actually changed (token refresh, user // update): update in place — no re-entry, no republish of an unchanged // user. { guard: and([ ({ event }) => isLoggedInComplete(event.snapshot.context), stateIn(`#auth.${AuthStateType.LOGGED_IN}`), ({ context, event }) => sessionChanged( context, event.snapshot.context as LoggedInAuthState, ), ]), actions: [ { type: "setLoggedIn", params: ({ event }) => { const state = event.snapshot.context as LoggedInAuthState; return { token: state.token, currentUser: state.currentUser!, }; }, }, { type: "publishCurrentUser" }, { type: "publishToken" }, ], }, // Already logged in, nothing changed: swallow the snapshot. { guard: and([ ({ event }) => isLoggedInComplete(event.snapshot.context), stateIn(`#auth.${AuthStateType.LOGGED_IN}`), ]), }, { guard: { type: "isLoggedInComplete", params: ({ event }) => ({ state: event.snapshot.context, }), }, actions: [ { type: "setLoggedIn", params: ({ event }) => { const state = event.snapshot.context as LoggedInAuthState; return { token: state.token, currentUser: state.currentUser!, }; }, }, ], target: `.${AuthStateType.LOGGED_IN}`, }, { guard: { type: "isAuthState", params: ({ event }) => ({ state: event.snapshot.context, type: AuthStateType.LOGGING_IN, }), }, actions: [{ type: "clearAuth" }], target: `.${AuthStateType.LOGGING_IN}`, }, { guard: { type: "isAuthState", params: ({ event }) => ({ state: event.snapshot.context, type: AuthStateType.ERROR, }), }, actions: [ { type: "setError", params: ({ event }) => ({ error: event.snapshot.context?.type === AuthStateType.ERROR ? event.snapshot.context.error : null, }), }, ], target: `.${AuthStateType.ERROR}`, }, { guard: { type: "isAuthState", params: ({ event }) => ({ state: event.snapshot.context, type: AuthStateType.LOGGED_OUT, }), }, actions: [{ type: "clearAuth" }], target: `.${AuthStateType.LOGGED_OUT}`, }, ], }, states: { init: { tags: ["authenticating"], after: { authTimeout: { actions: [ { type: "setError", params: () => ({ error: new Error("Authentication timed out"), }), }, ], target: AuthStateType.ERROR, }, }, }, [AuthStateType.LOGGING_IN]: { tags: ["authenticating"], after: { authTimeout: { actions: [ { type: "setError", params: () => ({ error: new Error("Authentication timed out"), }), }, ], target: AuthStateType.ERROR, }, }, }, [AuthStateType.LOGGED_IN]: { tags: ["authenticated"], entry: [{ type: "publishCurrentUser" }, { type: "publishToken" }], // Leaving LOGGED_IN unsubscribes the responder, so signed-out callers get // NO_RESPONDER instead of a token. invoke: { src: "tokenRefreshResponder" }, on: { "auth.logout": { target: "logging-out" }, // A refresh runs the publish path (deduped today, since the token is // unchanged) and replies to the caller with the live token. "token.refresh": { actions: [ { type: "publishToken" }, { type: "replyToken", params: ({ event }) => ({ reply: event.reply }), }, ], }, }, }, ["logging-out"]: { invoke: { src: "logoutActor", input: ({ context }) => ({ instance: context.instance }), onDone: { target: AuthStateType.LOGGED_OUT, }, onError: { actions: [ { type: "setError", params: ({ event }) => ({ error: event.error }), }, ], target: AuthStateType.ERROR, }, }, }, [AuthStateType.LOGGED_OUT]: { entry: [{ type: "publishCurrentUser" }, { type: "publishToken" }], }, [AuthStateType.ERROR]: { tags: ["error"], entry: [{ type: "publishCurrentUser" }, { type: "publishToken" }], }, }, });