Kizuna.identity
Define who can call your API with an OpenAPI security scheme, the context a passing guard provides, and the access fields routes may constrain.
An identity describes one kind of authenticated caller: an OpenAPI security scheme (how the credential travels), an optional context schema (what a passing guard provides to handlers), and an optional access schema (the fields the auth map may constrain per route). Omit context for an authentication-only identity, a pure gate with no data to hand the handler. For the full walkthrough, see the Auth guide.
pnpm add @ts-kizuna/corebun add @ts-kizuna/corenpm install @ts-kizuna/coreimport { Kizuna } from '@ts-kizuna/core';Builders
One builder per authentication method, matching the OpenAPI security scheme types:
| Builder | Credential source | OpenAPI scheme |
|---|---|---|
Kizuna.identity.bearer | Authorization: Bearer <token> | { type: 'http', scheme: 'bearer' } |
Kizuna.identity.apiKey | A named header, query parameter, or cookie | { type: 'apiKey', name, in } |
Kizuna.identity.basic | Authorization: Basic <base64>, decoded | { type: 'http', scheme: 'basic' } |
Kizuna.identity.oauth2 | Authorization: Bearer <token>, with scopes | { type: 'oauth2', flows } |
Kizuna.identity.openIdConnect | Authorization: Bearer <token> | { type: 'openIdConnect', ... } |
Kizuna.identity.custom | Read by the guard (e.g. a path segment) | none, emits x-kizuna-guarded |
Every builder takes context (optional), access (optional), description (optional), and scheme (optional, where identities sharing one credential set the same scheme name and emit a single OpenAPI scheme). Method-specific fields:
| Builder | Extra fields |
|---|---|
Kizuna.identity.bearer | bearerFormat? (e.g. 'JWT') |
Kizuna.identity.apiKey | name, in: 'header' | 'query' | 'cookie' |
Kizuna.identity.oauth2 | flows (OpenAPI OAuth Flows object) |
Kizuna.identity.openIdConnect | openIdConnectUrl |
Example
import { z } from 'zod';
import { Kizuna } from '@ts-kizuna/core';
export const user = Kizuna.identity.bearer({
context: z.object({
userId: z.string(),
}),
});
export const member = Kizuna.identity.apiKey({
name: 'x-workspace-token',
in: 'header',
context: z.object({
workspaceUserId: z.string(),
}),
access: z.object({
role: z.enum(['owner', 'admin']),
}),
});context vs access
Both describe what the guard returns and the handler reads:
contextis who the caller is. A guard must return it, and handlers of secured routes receive it underauth, keyed by the identity's name (auth.user,auth.member).accessis the fields theauthmap may gate per route. Gating on{ member: { role: 'owner' } }enforces a 403 at runtime and narrows the handler's type, soauth.member.roleis'owner', not'owner' | 'admin'.
Skip access for identities that never gate on fields; the auth map can still require them ('user'), it just can't constrain them.
Authentication-only identities
Some identities are pure gates, such as an API key that only proves the caller is a known client, or a bearer token where "authenticated" is all the handler needs. These have no data to hand the handler, so omit context (and access) entirely:
export const apiConsumer = Kizuna.identity.apiKey({
name: 'x-api-key',
in: 'header',
});Its guard returns nothing on success (or deny(...) to reject), with no return {} boilerplate:
export const requireApiKey = server.guard('apiConsumer', ({ apiKey, deny }) => {
if (!apiKey || !(await isKnownKey(apiKey.value))) return deny(401, 'Unauthorized');
});A route secured only by a context-less identity gets no arg for it, because the identity contributes nothing to the handler args.
Custom identities
Some credentials don't fit any OpenAPI security scheme. A capability URL is the common case: you email or SMS a link like /invites/:token, and the path token is the credential. OpenAPI's apiKey can't describe it, since its in allows only header, query, and cookie with no path option, so declaring one would emit a scheme a generated client would act on incorrectly.
Kizuna.identity.custom is the escape hatch. It has no OpenAPI scheme; its guard reads the credential itself from wherever it lives:
export const inviteToken = Kizuna.identity.custom({
context: z.object({
inviteId: z.string(),
}),
});export const requireInviteToken = server.guard('inviteToken', ({ params, deny }) => {
const inviteId = await resolveInvite(params.token);
if (!inviteId) return deny(404, 'Not found');
return {
inviteId,
};
});The guard receives the usual { params, deny, scopes } (plus your framework's handler context) with no credential key, since there is nothing for the runtime to pre-extract. Everything else is ordinary typed auth: the identity registers under identities, the auth map references it, handlers read auth.inviteToken.inviteId, and access gates work as usual.
Because it emits no security, a custom-guarded route would otherwise look public in the spec. To keep the distinction honest, generateOpenApi marks the operation with an x-kizuna-guarded extension listing the custom identities, so a genuinely public route (no security, no extension) stays distinguishable from one protected out-of-band.
Reach for custom only when the credential is truly inexpressible. A token belongs in bearer, a signature header in apiKey; mutual TLS has its own OpenAPI type.
Registering identities
Pass identities to new Kizuna() under identities. Their keys become the names everything else uses: auth map values, guard registrations, and handler context keys:
import { Kizuna } from '@ts-kizuna/core';
import { tags } from './tags';
import { user, member } from './identities';
export const k = new Kizuna({
identities: {
user,
member,
},
tags,
});Securing routes
Routes never mention identities directly. The contract's auth map assigns them per group (or per route via a '*' cascade), keyed by the names above:
export const contract = k.contract({
routes,
auth: {
users: false,
health: false,
members: 'user',
workspace: {
'*': 'member',
deleteWorkspace: {
member: {
role: 'owner',
},
},
},
},
});See the auth map reference for every value form. From there, each identity needs exactly one guard, registered on server.api.
OpenAPI emission
generateOpenApi emits identities under components.securitySchemes, and each secured route references them in its security, with any OAuth scopes the auth map declares.