Skip to main content

Generic Web-Fetch adapter

WIP

Mount duck-auth on any runtime that speaks the Web-Fetch Request / Response standard. Bun, Deno, Cloudflare Workers, Service Workers - all work.

When to use

The generic adapter is the lowest-level mount point. It exposes:

  • authExecuteIntents(intents, baseStatus?) -> Response - turn Intent[] into a Response.
  • parseSignInBody(raw) - validate a sign-in JSON payload.
  • parseProviderBeginBody(raw) - validate a provider-begin payload.

Use it when:

  • Your framework isn't on the shipped-adapters list and you don't want to PR a new adapter.
  • You need to customise the response shape (e.g. wrap everything in { ok: true, data: ... }).
  • You're writing a Service Worker that intercepts auth calls client-side.

Bun (native)

import { authExecuteIntents, parseSignInBody } from '@gentleduck/auth/server/generic'
import { auth } from './lib/auth'

Bun.serve({
  port: 3000,
  async fetch(req) {
    const url = new URL(req.url)

    if (url.pathname === '/auth/signin' && req.method === 'POST') {
      const body = parseSignInBody(await req.json())
      if (!body) return new Response('Bad Request', { status: 400 })

      const intents = await auth.flows.signIn(body)
      return authExecuteIntents(intents)
    }

    if (url.pathname === '/auth/session' && req.method === 'GET') {
      const ctx = await auth.resolveSession(req)
      if (!ctx) return new Response('Unauthorized', { status: 401 })
      return Response.json(ctx)
    }

    return new Response('Not Found', { status: 404 })
  },
})

Deno

import { authExecuteIntents } from '@gentleduck/auth/server/generic'
import { auth } from './lib/auth.ts'

Deno.serve({ port: 3000 }, async (req) => {
  const url = new URL(req.url)
  if (url.pathname === '/auth/signin' && req.method === 'POST') {
    const body = await req.json()
    const intents = await auth.flows.signIn(body)
    return authExecuteIntents(intents)
  }
  // ...
  return new Response('Not Found', { status: 404 })
})

Cloudflare Workers

import { authExecuteIntents } from '@gentleduck/auth/server/generic'

export default {
  async fetch(req: Request, env: Env): Promise<Response> {
    const auth = createAuthForRequest(env)
    const url = new URL(req.url)

    if (url.pathname === '/auth/signin' && req.method === 'POST') {
      const intents = await auth.flows.signIn(await req.json())
      return authExecuteIntents(intents)
    }
    return new Response('Not Found', { status: 404 })
  },
}

Custom intent processing

If you need to wrap the response or add headers:

import { authExecuteIntents } from '@gentleduck/auth/server/generic'

const baseResponse = authExecuteIntents(intents)

// Read the body, wrap it, return a new Response with the same headers.
const wrapped = new Response(
  JSON.stringify({ ok: true, data: await baseResponse.json() }),
  {
    status: baseResponse.status,
    headers: baseResponse.headers,
  },
)

return wrapped

For more sophisticated needs (different status codes, redirects to your SPA after sign-in), construct the response by walking the intents:

for (const intent of intents) {
  switch (intent.kind) {
    case 'startSession':
      // mint cookie/token, attach to response
      break
    case 'redirect':
      return Response.redirect(intent.url, intent.status ?? 302)
    case 'json':
      return Response.json(intent.body, { status: intent.status })
  }
}

This is rarely necessary - the shipped adapters cover 99% of cases - but the door is open.