TILLAUTH · SDK · NODE

@tilldev/auth-node

JWT verifier (EdDSA, JWKS-cached), Express middleware, Fastify hook, generic fetch-handler for Workers / Bun / Deno, and a cookie-mode proxy for HttpOnly sessions. No native dependencies.

pnpm add @tilldev/auth-node

The verifier

ts
import { TillAuthVerifier } from '@tilldev/auth-node'

const auth = new TillAuthVerifier({
  appId: 'tau_pub_…',
  // apiBase defaults to https://auth.tilldev.dev; override for self-hosted.
})

// Anywhere on the hot path:
const claims = await auth.verify(accessToken)
// claims: { sub, app, email, mfa, iat, exp }

First call fetches the JWKS from<apiBase>/.well-known/jwks.json; thereafter it's cached and revalidated on kid miss. Verification is offline once the cache is warm — no per-request round trip to TillAuth.

Constructor options

  • appId — your TillAuth public ID. Required. Tokens for a different app are rejected with code='wrong_app'.
  • apiBase — override the API base. Defaults to https://auth.tilldev.dev.
  • publicJwks — inline an array of JWK keys. Use this for air-gapped verification where the verifier can't reach TillAuth at startup.
  • requireVerifiedEmail — reject tokens whose user hasn't completed email verification. Defaults to false.
  • requireMfa — reject tokens whose primary signin wasn't MFA-challenged. Defaults to false.

Errors

TillAuthVerifyError with a typedcode: 'no_token', 'invalid_token', 'wrong_app', 'mfa_required', 'email_not_verified'.

Express middleware

ts
import express from 'express'
import { TillAuthVerifier, expressMiddleware } from '@tilldev/auth-node'

const auth = new TillAuthVerifier({ appId: 'tau_pub_…' })
const app = express()

// Protect a route group:
app.use('/api', expressMiddleware(auth, {
  onError: (err) => ({ error: err.message, code: err.code }),  // optional
}))

// In handlers:
app.get('/api/me', (req, res) => {
  // req.user is the typed AuthClaims object
  res.json({ user_id: req.user.sub, email: req.user.email })
})

Fastify hook

ts
import fastify from 'fastify'
import { TillAuthVerifier, fastifyHook } from '@tilldev/auth-node'

const app = fastify()
const auth = new TillAuthVerifier({ appId: 'tau_pub_…' })

app.addHook('preHandler', fastifyHook(auth))

app.get('/api/me', async (req) => ({
  user_id: req.user.sub,
  email: req.user.email,
}))

Workers · Bun · Deno · plain fetch

createFetchHandler returns a(req: Request) => Promise<Response | null>you can drop into any fetch-shaped runtime. It enforces verification and returns null on success (so you fall through to your handler):

ts
import { createVerifier, createFetchHandler } from '@tilldev/auth-node'

const auth = createVerifier({ appId: 'tau_pub_…' })
const requireAuth = createFetchHandler(auth)

export default {
  async fetch(req) {
    const failed = await requireAuth(req)
    if (failed) return failed  // 401 with typed code

    const claims = (req as any).user  // attached by the handler
    return new Response(JSON.stringify({ user: claims.sub }))
  },
}

Cookie-mode proxy

Pair this with @tilldev/auth-react'ssessionMode: 'cookie'. The proxy receives the same-site requests from the SDK, forwards them to TillAuth, and translates the responses into Set-Cookie for HttpOnly access and refresh cookies. Tokens never live in JavaScript.

ts
// Express
import express from 'express'
import { expressCookieProxy } from '@tilldev/auth-node'

const app = express()
app.use('/api/auth', expressCookieProxy({ appId: 'tau_pub_…' }))

// Workers / Bun / Deno — same options, fetch handler shape:
import { createFetchHandler } from '@tilldev/auth-node'
const handler = createFetchHandler({ ... })  // see CookieProxyOptions

Cookies are set HttpOnly, Secure (in prod), SameSite=Lax, with the access cookie's TTL aligned to the JWT exp and the refresh cookie's to 30 days.

Inline keys (air-gapped)

If your verifier can't reach TillAuth at startup, fetch the JWKS once and pass it in:

ts
const keys = JSON.parse(process.env.TILLAUTH_PUBLIC_JWKS!)  // [{ kid, kty, crv, x }]
const auth = new TillAuthVerifier({ appId: 'tau_pub_…', publicJwks: keys })

Rotation is your responsibility in this mode — re-deploy with the new JWKS during the 24-hour overlap window.

Performance notes

  • Verification is ~150μs after the JWKS cache is warm. Cold-path adds the first JWKS fetch (one round trip, then cached).
  • The verifier is safe to share across requests — no per-request state.
  • On a kid miss (key rotation), JWKS is re-fetched lazily. The 24-hour overlap window means you should never see this happen on a hot token.

Browser side: @tilldev/auth-react.