Auth / foldkit-auth

Sixty seconds

ts
import { Effect } from 'effect'
import { AuthServer } from 'foldkit-auth/server'
import { serveFetch } from 'foldkit-remote-server/fetch'

const http = { origin: 'https://app.example' } as const // the options given Http.make(AppAuth, http)

export const remote = serveFetch({
  server, // RemoteServer.make(…)
  resolvePrincipal: AuthServer.resolver({
    auth: AppAuth, // Auth.make(…)
    http,
    principal: session => Effect.succeed({ id: session.subjectId }),
  }),
  layer: () => AppAuthLive, // AppAuth.layer, with its persistence
})
  • AuthServer.resolver(config) is a declaration: it returns the function serveFetch calls per request, and throws at once if http names an origin that is not one (null, a trailing slash). Each call reads headers and asks the session store; it writes nothing.
  • auth is the Auth.make(…) value; the resolver yields it as a service, so the host's layer must provide it. That is the one store, shared with the routes that set the cookie.
  • http is the same object given Http.make: the cookie's name, the trusted origins and the CSRF header are read from it, so the two agree.
  • principal builds the application's principal from the verified session, per request. Read memberships fresh here, from session.subjectId; a session's claims are a snapshot taken at sign-in. It may refuse, with RemoteServer.unauthenticated(reason).

The client's side

Remote's transport posts every call, so a browser sends Origin on its own; the CSRF header is the client's to add:

ts
import { Remote } from 'foldkit-remote'

const transport = Remote.httpWithLive('/remote', {
  headers: () => ({ 'x-effect-auth-csrf': '1' }),
})

A refusal reaches the Model as a failure; RemoteData.denied(error) from foldkit-remote reads it as Unauthenticated with the reason below.