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.