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.
// 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 authconst 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 inferredconst _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.
SQL-Based Authentication
Section titled “SQL-Based Authentication”Look up users in your database using getDb() from the Express adapter context:
// SQL-based authentication — look up user in databaseimport { 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.
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:
// Compose authenticators for role-based accessimport { 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 routeconst _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 routeconst _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 }; });Route Types with Authentication
Section titled “Route Types with Authentication”Authentication is available on these route types:
| Route Type | Auth | Use Case |
|---|---|---|
AuthenticatedRoute | Required | Protected GET/DELETE |
FullRoute | Required | Protected POST/PUT |
AuthenticatedListRoute | 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.