Auth / Auth and Guard
The seam: one principal type
The guard set declares the principal's Schema, and auth's principal
function returns a value of it. That shared type is the whole contract
between the two packages (from examples/auth/src/domain.ts):
ts
import { Schema } from 'effect'
import { Entity, Expr } from 'foldkit-entity'
import { Guards } from 'foldkit-guard'
export const User = Schema.Struct({
id: Schema.String,
role: Schema.Literals(['admin', 'member']),
})
export type User = typeof User.Type
const NoteFields = Entity.define(
'Note',
Schema.Struct({ id: Schema.String, authorId: Schema.String, title: Schema.String }),
)
export const G = Guards.declare(
'Notes',
User,
guard => ({
isAdmin: guard.principal(),
isAuthor: guard.row(NoteFields, p => Expr.eq(NoteFields.fields.authorId, p.id)),
}),
{ key: user => user.id, unguarded: 'deny' },
)
export const Note = NoteFields.pipe(Guards.attach(G, { row: { Read: [G.isAuthor, G.isAdmin] } }))This file is shared: the browser imports it too. isAdmin is a name here;
its body is given only on the server. isAuthor is data, an Expr, which
the server compiles to SQL.
On the server, the two packages are wired into one endpoint (from
examples/auth/src/server.ts, reduced):
ts
import { Option } from 'effect'
import { AuthServer } from 'foldkit-auth/server'
import { RemoteServer } from 'foldkit-remote-server'
import { serveFetch } from 'foldkit-remote-server/fetch'
// Guard: the principal guards' bodies, server only.
const Policy = G.implement({
isAdmin: principal => Option.exists(principal, user => user.role === 'admin'),
})
// Auth: who a session is, looked up per request, never taken from its claims.
const principal = (session: { readonly subjectId: string }) => Users.find(session.subjectId)
const resolving = { auth: AppAuth, http, principal, key: Policy.key }
export const remote = serveFetch({
server, // built with GuardServer.memory / GuardDrizzle, checked by GuardServer.validate
resolvePrincipal: AuthServer.resolver(resolving), // auth: request -> Option<User>
headers: AuthServer.answered(resolving),
layer: () => AppAuthLive,
})serveFetch asks auth once per request, then answers the request's reads and
writes through the server's Sources, each of which reads that principal
through the guard set's rules. The two never call each other.