TILLAUTH · SDK · NODE

@tillstack/auth-node

A JWT verifier that validates tokens against your app's public signing keys (JWKS-cached), Express middleware, Fastify hook, a generic fetch-handler for Workers / Bun / Deno, and a cookie-mode proxy for HttpOnly sessions. No native dependencies — the core is Web Crypto, so it runs on edge runtimes unchanged (Workers guide).

01Install

Install

bash
pnpm add @tillstack/auth-node
02Verify

The verifier

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

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

// 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 typed code: 'no_token', 'invalid_token', 'wrong_app', 'mfa_required', 'email_not_verified'.

03Adapters

Express middleware

ts
import express from 'express'
import { TillAuthVerifier, expressMiddleware } from '@tillstack/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 })
})
04Adapters

Fastify hook

ts
import fastify from 'fastify'
import { TillAuthVerifier, fastifyHook } from '@tillstack/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,
}))
05Adapters

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 '@tillstack/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 }))
  },
}
07Advanced

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.

08Performance

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: @tillstack/auth-react.