Skip to main content

Fetch runtimes

WIP

Writing the auth routes on Bun, Deno or Cloudflare Workers over auth.flows, and the building blocks for a route or adapter of your own

Every route is a plain (req: Request) => Promise<Response>, so it serves any Fetch runtime. On Bun:

import { AuthError, csrfGuard } from '@gentleduck/auth/core'
import { callerContext, errorResponse, executeIntents, readBodyJson } from '@gentleduck/auth/server/generic'

async function signIn(req: Request, ip: string): Promise<Response> {
  const body = await readBodyJson(req)
  if (typeof body !== 'object' || body === null) throw new AuthError('AUTH_INVALID_PARAMETERS')
  const providerId: unknown = Reflect.get(body, 'providerId')
  if (typeof providerId !== 'string') throw new AuthError('AUTH_INVALID_PARAMETERS')
  const { intents } = await auth.flows.signIn({
    input: Reflect.get(body, 'input') ?? {},
    providerId,
    ...callerContext({ ip, userAgent: req.headers.get('user-agent') }),
    previousSid: auth.transport.extract(req) ?? undefined,
  })
  return executeIntents(intents)
}

Bun.serve({
  async fetch(req, server) {
    try {
      await csrfGuard(auth, req)
      const { pathname } = new URL(req.url)
      if (pathname === '/auth/signin') return await signIn(req, server.requestIP(req)?.address ?? '')
      return new Response('Not found', { status: 404 })
    } catch (err) {
      return errorResponse(err)
    }
  },
})

Deno's Deno.serve and a Worker's fetch take the same function. csrfGuard passes a safe method, so it can sit in front of every route but the OAuth callback. previousSid lets the flow revoke the session the browser held before, so signing in again never leaves two.

Building blocks

@gentleduck/auth/server/generic is what every adapter is built from:

ExportDoes
executeIntents(intents)A flow's intents as a Response: cookies, redirect, JSON body, no-store. Refuses an unsafe redirect.
errorResponse(err)A thrown error as a Response, the cause of a 5xx logged and withheld. errorToHttp is the same as { status, body }.
jsonResponse(status, body)A JSON Response with no-store.
readBodyJson(req), readBodyText(req)The body, null past 100 KiB or when it does not parse.
callerContext({ ip, userAgent })The caller fingerprint a flow takes, trimmed to what the session row holds.
requestSecurity(auth, { caller })The hijack and anomaly hooks the actor wrappers run.
nodeHeadersToFetch(headers)A Node header object as Headers.
isSafeRedirectUrl(url)A same-origin path or an http(s) URL; check a redirect before handing it to a script.
serializeCookieThe cookie writer the rest use.