Authentication
Authentication in fossyl is a function that receives request headers and returns a branded Authentication object via authWrapper(). The return type flows into your handler — no explicit generics needed.
How It Works
Section titled “How It Works”Authentication functions must return a Promise. This allows for async operations — JWT verification, database lookups, OAuth token exchange, and more.
export const authenticator(
headers: Record<string, string>,
) => Promise<{ userId: string } & Authentication> = async (headersRecord<string, string>: Record<string, string>) => { const userIdstring = headersRecord<string, string>["x-user-id"]; if (!userIdstring) { throw new AuthenticationError("Unauthorized"); } return authWrapper<T>(auth: T) => T & Authentication({ userIdstring });};The authWrapper() function brands the return value so the type system can distinguish authentication data from body data. This is how fossyl knows to pass auth data to the correct handler parameter position.
SQL-Based Authentication
Section titled “SQL-Based Authentication”Look up users in your database using getDb() from the Express adapter context:
export const getTodoRoute = {
path: string
method: RestMethod
steps: Steps[]
handler: Function
authenticator?: AuthenticationFunction<any>
validator?: ValidatorFunction<any>
queryValidator?: ValidatorFunction<any>
urlParamValidator?: ValidatorFunction<any>
paginationConfig?: PaginationConfig
hasTransaction: boolean
} = routerRouter<"/api/todos"> .createEndpoint<Path extends `/api/todos${string}`>(
path: Path,
) => Endpoint<Path, true>("/api/todos/:id") .authenticator(
headers: Record<string, string>,
) => Promise<{ userId: string } & Authentication>(authenticator(
headers: Record<string, string>,
) => Promise<{ userId: string } & Authentication>) .get<Response extends ResponseData>(
handler: (
parameters: {
query: { q: string; limit?: number; offset?: number } & {
readonly __kind: "query"
}
} & { readonly __kind: "parameters" },
) => () => Promise<Response>,
) => Route((paramsRecord<string, string | undefined>) => (_auth{ userId: string } & Authentication & { readonly __kind: "auth" }) => async () => { const todoTodoRow = {
id: number
title: string
completed: number
created_at: string
} = await todoService.getTodoRoute = {
path: string
method: RestMethod
steps: Steps[]
handler: Function
authenticator?: AuthenticationFunction<any>
validator?: ValidatorFunction<any>
queryValidator?: ValidatorFunction<any>
urlParamValidator?: ValidatorFunction<any>
paginationConfig?: PaginationConfig
hasTransaction: boolean
}(Number(paramsRecord<string, string | undefined>.url{ id: string } & { readonly __kind: "url" }.idnumber)); return { typeName: "Todo" as const, ...todoTodoRow = {
id: number
title: string
completed: number
created_at: string
} }; });The database client is available because the database adapter provides it via AsyncLocalStorage. This works automatically when you configure a database adapter on the framework adapter.
Role-Based Access Control
Section titled “Role-Based Access Control”Compose authenticators for role-based access. Since authenticators are just functions, you can create higher-order authenticators that check roles:
export const createTodoRoute = {
path: string
method: RestMethod
steps: Steps[]
handler: Function
authenticator?: AuthenticationFunction<any>
validator?: ValidatorFunction<any>
queryValidator?: ValidatorFunction<any>
urlParamValidator?: ValidatorFunction<any>
paginationConfig?: PaginationConfig
hasTransaction: boolean
} = routerRouter<"/api/todos"> .createEndpoint<Path extends `/api/todos${string}`>(
path: Path,
) => Endpoint<Path, true>("/api/todos") .authenticator(
headers: Record<string, string>,
) => Promise<{ userId: string } & Authentication>(authenticator(
headers: Record<string, string>,
) => Promise<{ userId: string } & Authentication>) .validator<RequestBody extends unknown>(
validatorFunction: ValidatorFunction<RequestBody>,
) => {
post: <Response extends ResponseData>(
handler: (
auth: { userId: string } & Authentication & { readonly __kind: "auth" },
) => undefined extends RequestBody
? () => Promise<Response>
: (
body: RequestBody & { readonly __kind: "body" },
) => () => Promise<Response>,
) => Route
put: <Response extends ResponseData>(
handler: (
auth: { userId: string } & Authentication & { readonly __kind: "auth" },
) => undefined extends RequestBody
? () => Promise<Response>
: (
body: RequestBody & { readonly __kind: "body" },
) => () => Promise<Response>,
) => Route
}(createTodoValidator(
data: unknown,
params?: util.InexactPartial<ParseParams>,
) => { title: string }) .post<Response extends ResponseData>(
handler: (
auth: { userId: string } & Authentication & { readonly __kind: "auth" },
) => (
body: { title: string } & { readonly __kind: "body" },
) => () => Promise<Response>,
) => Route((_auth{ userId: string } & Authentication & { readonly __kind: "auth" }) => (body{ title: string } & { readonly __kind: "body" }) => async () => { const todoTodoRow = {
id: number
title: string
completed: number
created_at: string
} = await todoService.createTodoRoute = {
path: string
method: RestMethod
steps: Steps[]
handler: Function
authenticator?: AuthenticationFunction<any>
validator?: ValidatorFunction<any>
queryValidator?: ValidatorFunction<any>
urlParamValidator?: ValidatorFunction<any>
paginationConfig?: PaginationConfig
hasTransaction: boolean
}(body{ title: string } & { readonly __kind: "body" }.titlestring); return { typeName: "Todo" as const, ...todoTodoRow = {
id: number
title: string
completed: number
created_at: string
} }; });Builder Configurations with Authentication
Section titled “Builder Configurations with Authentication”Authentication is available on these builder configurations:
| Configuration | Auth | Use Case |
|---|---|---|
Authenticated (.authenticator().get()) | Required | Protected GET/DELETE |
Full (.authenticator().validator().post()) | Required | Protected POST/PUT |
Authenticated + Paginated (.paginate().authenticator().get()) | Required | Protected paginated collections |
Error Handling
Section titled “Error Handling”Throw AuthenticationError from your authenticator to return a 401 response:
import { AuthenticationError } from '@fossyl/express';
// The adapter catches this and returns:// {// success: "false",// error: { code: 1002, message: "Invalid token" }// }The Express adapter’s error middleware handles AuthenticationError automatically, returning a standardized error response with error code 1002.