Skip to content
fossyl

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.

Authentication functions must return a Promise. This allows for async operations — JWT verification, database lookups, OAuth token exchange, and more.

// Authentication is just an async function that returns authWrapper()
import { authWrapper<T>(auth: T) => T & Authentication, createRouter<BasePath extends string>(_: BasePath) => Router<BasePath> } from "@fossyl/core";
const routerRouter<"/api"> = createRouter<BasePath extends string>(_: BasePath) => Router<BasePath><"/api">("/api");
// JWT-based auth
const jwtAuth( headers: Record<string, string>, ) => Promise<{ userId: any; role: "admin" | "user" } & Authentication> = async (headersRecord<string, string>: Record<string, string>) => {
const tokenstring = headersRecord<string, string>.authorization?.replace{ (searchValue: string | RegExp, replaceValue: string): string ( searchValue: string | RegExp, replacer: (substring: string, ...args: any[]) => string, ): string ( searchValue: { [Symbol.replace](string: string, replaceValue: string): string }, replaceValue: string, ): string ( searchValue: { [Symbol.replace]( string: string, replacer: (substring: string, ...args: any[]) => string, ): string }, replacer: (substring: string, ...args: any[]) => string, ): string }("Bearer ", "");
if (!tokenstring) throw new AuthenticationError("Missing authorization token");
const payload = verifyJwt(tokenstring);
return authWrapper<T>(auth: T) => T & Authentication({
userId: payload.sub,
role: payload.role"admin" | "user" as "admin" | "user",
});
};
// Use in routes — auth type is inferred
const _protectedRouteRoute = { 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">
.createEndpoint<Path extends `/api${string}`>(path: Path) => Endpoint<Path, true>("/api/profile")
.authenticator<Auth extends Authentication>( authenticationFunction: AuthenticationFunction<Auth>, ) => { validator: <RequestBody extends unknown>( validatorFunction: ValidatorFunction<RequestBody>, ) => { post: <Response extends ResponseData>( handler: undefined extends Auth ? undefined extends RequestBody ? () => Promise<Response> : ( body: RequestBody & { readonly __kind: "body" }, ) => () => Promise<Response> : ( auth: Auth & { readonly __kind: "auth" }, ) => undefined extends RequestBody ? () => Promise<Response> : ( body: RequestBody & { readonly __kind: "body" }, ) => () => Promise<Response>, ) => Route put: <Response extends ResponseData>( handler: undefined extends Auth ? undefined extends RequestBody ? () => Promise<Response> : ( body: RequestBody & { readonly __kind: "body" }, ) => () => Promise<Response> : ( auth: Auth & { readonly __kind: "auth" }, ) => undefined extends RequestBody ? () => Promise<Response> : ( body: RequestBody & { readonly __kind: "body" }, ) => () => Promise<Response>, ) => Route } } & { get: <Response extends ResponseData>( handler: undefined extends Auth ? () => Promise<Response> : (auth: Auth & { readonly __kind: "auth" }) => () => Promise<Response>, ) => Route post: <Response extends ResponseData>( handler: undefined extends Auth ? () => Promise<Response> : (auth: Auth & { readonly __kind: "auth" }) => () => Promise<Response>, ) => Route put: <Response extends ResponseData>( handler: undefined extends Auth ? () => Promise<Response> : (auth: Auth & { readonly __kind: "auth" }) => () => Promise<Response>, ) => Route delete: <Response extends ResponseData>( handler: undefined extends Auth ? () => Promise<Response> : (auth: Auth & { readonly __kind: "auth" }) => () => Promise<Response>, ) => Route }(jwtAuth( headers: Record<string, string>, ) => Promise<{ userId: any; role: "admin" | "user" } & Authentication>)
.get<Response extends ResponseData>( handler: ( auth: { userId: any; role: "admin" | "user" } & Authentication & { readonly __kind: "auth" }, ) => () => Promise<Response>, ) => Route((auth{ userId: any; role: "admin" | "user" } & Authentication & { readonly __kind: "auth" }) => async () => {
// auth is typed as: { userId: string; role: 'admin' | 'user' }
return { typeName: "Profile" as const, id: auth{ userId: any; role: "admin" | "user" } & Authentication & { readonly __kind: "auth" }.userId, role: auth{ userId: any; role: "admin" | "user" } & Authentication & { readonly __kind: "auth" }.role"admin" | "user" };
});

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.

Look up users in your database using getDb() from the Express adapter context:

// SQL-based authentication — look up user in database
import { authWrapper<T>(auth: T) => T & Authentication } from "@fossyl/core";
import { AuthenticationError, getDb<T = unknown>() => DatabaseContext<T> } from "@fossyl/express";
import { createRouter<BasePath extends string>(_: BasePath) => Router<BasePath> } from "@fossyl/core";
const routerRouter<"/api"> = createRouter<BasePath extends string>(_: BasePath) => Router<BasePath><"/api">("/api");
const sqlAuth( headers: Record<string, string>, ) => Promise<{ userId: any; role: any; name: any } & Authentication> = async (headersRecord<string, string>: Record<string, string>) => {
const apiKeystring = headersRecord<string, string>["x-api-key"];
if (!apiKeystring) throw new AuthenticationError("API key required");
const db: any = getDb<T = unknown>() => DatabaseContext<T>().client;
const user = await db
.selectFrom("users")
.where("api_key", "=", apiKeystring)
.select(["id", "role", "name"])
.executeTakeFirst();
if (!user) throw new AuthenticationError("Invalid API key");
return authWrapper<T>(auth: T) => T & Authentication({
userId: user.id,
role: user.role,
name: user.name,
});
};
const _protectedRouteRoute = { 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">
.createEndpoint<Path extends `/api${string}`>(path: Path) => Endpoint<Path, true>("/api/dashboard")
.authenticator<Auth extends Authentication>( authenticationFunction: AuthenticationFunction<Auth>, ) => { validator: <RequestBody extends unknown>( validatorFunction: ValidatorFunction<RequestBody>, ) => { post: <Response extends ResponseData>( handler: undefined extends Auth ? undefined extends RequestBody ? () => Promise<Response> : ( body: RequestBody & { readonly __kind: "body" }, ) => () => Promise<Response> : ( auth: Auth & { readonly __kind: "auth" }, ) => undefined extends RequestBody ? () => Promise<Response> : ( body: RequestBody & { readonly __kind: "body" }, ) => () => Promise<Response>, ) => Route put: <Response extends ResponseData>( handler: undefined extends Auth ? undefined extends RequestBody ? () => Promise<Response> : ( body: RequestBody & { readonly __kind: "body" }, ) => () => Promise<Response> : ( auth: Auth & { readonly __kind: "auth" }, ) => undefined extends RequestBody ? () => Promise<Response> : ( body: RequestBody & { readonly __kind: "body" }, ) => () => Promise<Response>, ) => Route } } & { get: <Response extends ResponseData>( handler: undefined extends Auth ? () => Promise<Response> : (auth: Auth & { readonly __kind: "auth" }) => () => Promise<Response>, ) => Route post: <Response extends ResponseData>( handler: undefined extends Auth ? () => Promise<Response> : (auth: Auth & { readonly __kind: "auth" }) => () => Promise<Response>, ) => Route put: <Response extends ResponseData>( handler: undefined extends Auth ? () => Promise<Response> : (auth: Auth & { readonly __kind: "auth" }) => () => Promise<Response>, ) => Route delete: <Response extends ResponseData>( handler: undefined extends Auth ? () => Promise<Response> : (auth: Auth & { readonly __kind: "auth" }) => () => Promise<Response>, ) => Route }(sqlAuth( headers: Record<string, string>, ) => Promise<{ userId: any; role: any; name: any } & Authentication>)
.get<Response extends ResponseData>( handler: ( auth: { userId: any; role: any; name: any } & Authentication & { readonly __kind: "auth" }, ) => () => Promise<Response>, ) => Route((auth{ userId: any; role: any; name: any } & Authentication & { readonly __kind: "auth" }) => async () => {
// auth is typed as: { userId: string; role: string; name: string }
return {
typeName: "Dashboard" as const,
user: auth{ userId: any; role: any; name: any } & Authentication & { readonly __kind: "auth" }.name,
role: auth{ userId: any; role: any; name: any } & Authentication & { readonly __kind: "auth" }.role,
};
});

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.

Compose authenticators for role-based access. Since authenticators are just functions, you can create higher-order authenticators that check roles:

// Compose authenticators for role-based access
import { authWrapper<T>(auth: T) => T & Authentication } from "@fossyl/core";
import { createRouter<BasePath extends string>(_: BasePath) => Router<BasePath> } from "@fossyl/core";
const routerRouter<"/api"> = createRouter<BasePath extends string>(_: BasePath) => Router<BasePath><"/api">("/api");
const requireRole( ...roles: string[] ) => ( headers: Record<string, string>, ) => Promise<{ userId: any; role: any } & Authentication> =
(...rolesstring[]: string[]) =>
async (headersRecord<string, string>: Record<string, string>) => {
const tokenstring = headersRecord<string, string>.authorization?.replace{ (searchValue: string | RegExp, replaceValue: string): string ( searchValue: string | RegExp, replacer: (substring: string, ...args: any[]) => string, ): string ( searchValue: { [Symbol.replace](string: string, replaceValue: string): string }, replaceValue: string, ): string ( searchValue: { [Symbol.replace]( string: string, replacer: (substring: string, ...args: any[]) => string, ): string }, replacer: (substring: string, ...args: any[]) => string, ): string }("Bearer ", "");
if (!tokenstring) throw new AuthenticationError("Missing token");
const payload = verifyJwt(tokenstring);
if (!rolesstring[].includes(searchElement: string, fromIndex?: number) => boolean(payload.role)) {
throw new AuthenticationError("Insufficient permissions");
}
return authWrapper<T>(auth: T) => T & Authentication({ userId: payload.sub, role: payload.role });
};
// Admin-only route
const _adminRouteRoute = { 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">
.createEndpoint<Path extends `/api${string}`>(path: Path) => Endpoint<Path, true>("/api/admin/users")
.authenticator<Auth extends Authentication>( authenticationFunction: AuthenticationFunction<Auth>, ) => { validator: <RequestBody extends unknown>( validatorFunction: ValidatorFunction<RequestBody>, ) => { post: <Response extends ResponseData>( handler: undefined extends Auth ? undefined extends RequestBody ? () => Promise<Response> : ( body: RequestBody & { readonly __kind: "body" }, ) => () => Promise<Response> : ( auth: Auth & { readonly __kind: "auth" }, ) => undefined extends RequestBody ? () => Promise<Response> : ( body: RequestBody & { readonly __kind: "body" }, ) => () => Promise<Response>, ) => Route put: <Response extends ResponseData>( handler: undefined extends Auth ? undefined extends RequestBody ? () => Promise<Response> : ( body: RequestBody & { readonly __kind: "body" }, ) => () => Promise<Response> : ( auth: Auth & { readonly __kind: "auth" }, ) => undefined extends RequestBody ? () => Promise<Response> : ( body: RequestBody & { readonly __kind: "body" }, ) => () => Promise<Response>, ) => Route } } & { get: <Response extends ResponseData>( handler: undefined extends Auth ? () => Promise<Response> : (auth: Auth & { readonly __kind: "auth" }) => () => Promise<Response>, ) => Route post: <Response extends ResponseData>( handler: undefined extends Auth ? () => Promise<Response> : (auth: Auth & { readonly __kind: "auth" }) => () => Promise<Response>, ) => Route put: <Response extends ResponseData>( handler: undefined extends Auth ? () => Promise<Response> : (auth: Auth & { readonly __kind: "auth" }) => () => Promise<Response>, ) => Route delete: <Response extends ResponseData>( handler: undefined extends Auth ? () => Promise<Response> : (auth: Auth & { readonly __kind: "auth" }) => () => Promise<Response>, ) => Route }(requireRole( ...roles: string[] ) => ( headers: Record<string, string>, ) => Promise<{ userId: any; role: any } & Authentication>("admin"))
.get<Response extends ResponseData>( handler: ( auth: { userId: any; role: any } & Authentication & { readonly __kind: "auth" }, ) => () => Promise<Response>, ) => Route((auth{ userId: any; role: any } & Authentication & { readonly __kind: "auth" }) => async () => {
return { typeName: "AdminPanel" as const, userId: auth{ userId: any; role: any } & Authentication & { readonly __kind: "auth" }.userId };
});
// User-or-admin route
const _profileRouteRoute = { 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">
.createEndpoint<Path extends `/api${string}`>(path: Path) => Endpoint<Path, true>("/api/profile")
.authenticator<Auth extends Authentication>( authenticationFunction: AuthenticationFunction<Auth>, ) => { validator: <RequestBody extends unknown>( validatorFunction: ValidatorFunction<RequestBody>, ) => { post: <Response extends ResponseData>( handler: undefined extends Auth ? undefined extends RequestBody ? () => Promise<Response> : ( body: RequestBody & { readonly __kind: "body" }, ) => () => Promise<Response> : ( auth: Auth & { readonly __kind: "auth" }, ) => undefined extends RequestBody ? () => Promise<Response> : ( body: RequestBody & { readonly __kind: "body" }, ) => () => Promise<Response>, ) => Route put: <Response extends ResponseData>( handler: undefined extends Auth ? undefined extends RequestBody ? () => Promise<Response> : ( body: RequestBody & { readonly __kind: "body" }, ) => () => Promise<Response> : ( auth: Auth & { readonly __kind: "auth" }, ) => undefined extends RequestBody ? () => Promise<Response> : ( body: RequestBody & { readonly __kind: "body" }, ) => () => Promise<Response>, ) => Route } } & { get: <Response extends ResponseData>( handler: undefined extends Auth ? () => Promise<Response> : (auth: Auth & { readonly __kind: "auth" }) => () => Promise<Response>, ) => Route post: <Response extends ResponseData>( handler: undefined extends Auth ? () => Promise<Response> : (auth: Auth & { readonly __kind: "auth" }) => () => Promise<Response>, ) => Route put: <Response extends ResponseData>( handler: undefined extends Auth ? () => Promise<Response> : (auth: Auth & { readonly __kind: "auth" }) => () => Promise<Response>, ) => Route delete: <Response extends ResponseData>( handler: undefined extends Auth ? () => Promise<Response> : (auth: Auth & { readonly __kind: "auth" }) => () => Promise<Response>, ) => Route }(requireRole( ...roles: string[] ) => ( headers: Record<string, string>, ) => Promise<{ userId: any; role: any } & Authentication>("user", "admin"))
.get<Response extends ResponseData>( handler: ( auth: { userId: any; role: any } & Authentication & { readonly __kind: "auth" }, ) => () => Promise<Response>, ) => Route((auth{ userId: any; role: any } & Authentication & { readonly __kind: "auth" }) => async () => {
return { typeName: "Profile" as const, userId: auth{ userId: any; role: any } & Authentication & { readonly __kind: "auth" }.userId };
});

Authentication is available on these route types:

Route TypeAuthUse Case
AuthenticatedRouteRequiredProtected GET/DELETE
FullRouteRequiredProtected POST/PUT
AuthenticatedListRouteRequiredProtected paginated collections

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.