Kynetra FX

Documentation

API Keys & Sessions

Beyond JWT, Kynetra FX provides apiKeyAuth for opaque API keys resolved via your own lookup logic, and sessionAuth paired with signSession for HMAC-signed cookie sessions. Both integrate with the same auth() middleware.

API key authentication#

apiKeyAuth({ lookup }) reads an opaque key from a request header and delegates resolution to your lookup function. The function receives the raw key string and the request context, and must return a Principal or null (meaning the key is unknown or revoked).

src/auth/api-key.ts
import { requireAuth, apiKeyAuth } from '@kynetra/fx-auth'
import { kernel } from './kernel'
 
const strategy = apiKeyAuth({
header: 'x-api-key', // default: 'x-api-key'
lookup: async (key, ctx) => {
const [record] = await kernel.apiKeys.list({ key })
if (!record) return null // unknown key
 
return {
id: record.id,
type: 'apikey' as const,
roles: record.roles ?? [],
scopes: record.scopes ?? [],
tenantId: record.tenantId,
}
},
})
 
app.use('/api/*', requireAuth([strategy]))

apiKeyAuth options

NameTypeDescription
headerstringRequest header that carries the API key. Defaults to "x-api-key".
lookup(key: string, ctx: FXContext) => Promise<Principal | null>Required. Called with the raw key value. Return a Principal to authenticate, or null to reject.

Tip

Store API keys hashed (e.g. SHA-256) in your database. In the lookup function, hash the incoming key before querying so the plaintext is never persisted.

Issuing API keys

Kynetra FX does not generate API key strings — that is your responsibility. A common pattern: generate a cryptographically random token, store its hash in the SaaS kernel apiKeys repository, and return the plaintext once to the user.

src/routes/api-keys.ts
import { kernel } from './kernel'
 
app.post('/api-keys', async (ctx) => {
const principal = getPrincipal(ctx)
 
// Generate a random key
const raw = Array.from(crypto.getRandomValues(new Uint8Array(32)))
.map((b) => b.toString(16).padStart(2, '0'))
.join('')
 
// Hash before storing
const buf = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(raw))
const hash = Array.from(new Uint8Array(buf))
.map((b) => b.toString(16).padStart(2, '0'))
.join('')
 
await kernel.apiKeys.create({
id: crypto.randomUUID(),
key: hash,
tenantId: principal.tenantId ?? '',
roles: ['member'],
scopes: ['api:read'],
createdAt: new Date().toISOString(),
})
 
// Return plaintext ONCE — never stored
return ctx.json({ key: raw }, { status: 201 })
})

Session authentication#

sessionAuth({ secret, cookie?, now? }) authenticates requests using a signed session cookie. The cookie value is a compact HMAC-signed string produced by signSession. This is suitable for browser-based apps where you control the session lifecycle server-side.

Signing sessions

Call signSession(claims, secret, opts?) to produce a signed session string, then set it as a cookie. It accepts the same optional timing parameters as signJwt.

src/routes/login.ts
import { signSession } from '@kynetra/fx-auth'
 
app.post('/login', async (ctx) => {
const { email, password } = await ctx.jsonBody<{ email: string; password: string }>()
const user = await authenticateUser(email, password)
 
const session = await signSession(
{ sub: user.id, tenantId: user.tenantId, roles: user.roles },
env.SESSION_SECRET,
{ expiresInSec: 60 * 60 * 8 } // 8 hours
)
 
ctx.setCookie('session', session, {
httpOnly: true,
secure: true,
sameSite: 'Lax',
path: '/',
maxAge: 60 * 60 * 8,
})
 
return ctx.json({ ok: true })
})

Verifying sessions

Use sessionAuth as a strategy in the normal auth() flow. It reads the named cookie, verifies the HMAC signature, checks expiry, and resolves a Principal.

src/app.ts
import { requireAuth, sessionAuth } from '@kynetra/fx-auth'
 
app.use(
requireAuth([
sessionAuth({
secret: env.SESSION_SECRET,
cookie: 'session', // default: 'session'
}),
])
)

sessionAuth options

NameTypeDescription
secretstringHMAC secret for signing and verifying the session cookie value.
cookiestringCookie name to read. Defaults to "session".
now() => numberOverride the current time for expiry validation. Useful in tests.

Combining strategies#

Pass multiple strategies to auth() or requireAuth() to support several authentication methods on the same route. Strategies are tried in order — the first one that successfully resolves a Principal wins.

src/app.ts
import { requireAuth, jwtAuth, apiKeyAuth, sessionAuth } from '@kynetra/fx-auth'
import { kernel } from './kernel'
 
// Try JWT (fast, no DB hit), then API key (DB lookup), then session cookie
const strategies = [
jwtAuth({ secret: env.JWT_SECRET }),
 
apiKeyAuth({
lookup: async (key, _ctx) => {
const [record] = await kernel.apiKeys.list({ key })
if (!record) return null
return { id: record.id, type: 'apikey' as const, roles: [], scopes: [] }
},
}),
 
sessionAuth({ secret: env.SESSION_SECRET }),
]
 
// All authenticated routes
app.use('/app/*', requireAuth(strategies))
 
// Some routes allow anonymous access (e.g. public content)
app.use('/public/*', auth(strategies)) // required: false

Note

The auth() function from @kynetra/fx-auth accepts a second argument { required?: boolean }. Omitting it or passing required: false allows anonymous callers through. Use requireAuth() as a shorthand when auth is always mandatory.

Session logout#

To log out a session user, clear the cookie by setting it with an expired max-age. There is no server-side session store to invalidate — the HMAC signature and expiry in the cookie are the only source of truth.

src/routes/logout.ts
app.post('/logout', (ctx) => {
ctx.setCookie('session', '', {
httpOnly: true,
secure: true,
sameSite: 'Lax',
path: '/',
maxAge: 0, // immediately expires
})
return ctx.json({ ok: true })
})

Tip

If you need server-side session revocation (e.g. "log out all devices"), store session IDs in the sessions repository and check validity in the apiKeyAuth lookup. Cookie sessions alone do not support revocation without a database check.