Your sign-in code is ${code}. It expires in 5 minutes.
`, }); }, // [!code highlight] }), // [!code highlight] ], }); ``` The plugin throws at startup unless `auth.session` is configured, because it issues a session on successful verification. `auth.session.secret` must be at least 32 characters. You can also pass the same options as a top-level `emailOtp` key to `createTheAuth` instead of using the plugin. That exposes `theauth.emailOtp` (`sendCode`, `verifyCode`, `handleRequest`), which is `null` when the key is absent or `auth.session` is not set.Loading...
; if (!session) returnNo active session.
; return (Session expires: {session.expiresAt ? new Date(session.expiresAt).toLocaleString() : 'unknown'}
); } ```{user.name}
{user.email}
Loading agents...
} {agents.map((agent) => (Loading...
{:else if $session}Signed in as {$user?.email}
{:else} Sign in {/if} ``` ## Sign in, sign up, sign out `signIn` and `signUp` take positional arguments and resolve to `{ success: true, data }` or `{ success: false, error }`, where `error` is a string. ```svelte title="src/routes/sign-in/+page.svelte" ``` ```typescript title="Sign up and sign out" import { signUp, signOut } from '$lib/theauth'; await signUp(email, password, name); await signOut(); ``` ## createAgentStore Manage agent identities in reactive Svelte stores. Pass the `user` store from your client and the agent list loads when a user is present, or call `load(userId)` yourself. ```svelte {#each $agents as agent}Loading...
; if (!isAuthenticated) { return ( ); } return (Signed in as {user?.email}
{error}
}Hey ${name}, your account is ready. Get started.
`, }; }, }, }); ```{user?.emailAddresses[0]?.emailAddress}
{user?.email}
&redirect_uri=
&client_id=
&code_verifier=
```
For `refresh_token` grant:
```
grant_type=refresh_token
&refresh_token=
&client_id=
```
**Response** `200 OK` (with `Cache-Control: no-store`)
```json
{
"access_token": "eyJ...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "...",
"scope": "mcp:read mcp:execute"
}
```
`refresh_token` is present only when the granted scope includes `offline_access`. `expires_in` is the module's `accessTokenTtl`. Failures return `401` for `invalid_client` and `400` for everything else.
---
## Dashboard endpoints
### Stats overview
```
GET /dashboard/stats
```
Returns aggregate statistics for the admin dashboard. The audit counts cover the last 24 hours and are computed from at most the 1,000 most recent entries.
**Response** `200 OK`
```json
{
"agents": { "total": 42, "active": 38, "revoked": 3, "expired": 1 },
"users": { "total": 12 },
"audit": { "last24h": 480, "allowed": 470, "denied": 10, "rateLimited": 0 }
}
```
### Dashboard agents and audit
```
GET /dashboard/agents
GET /dashboard/audit
```
Aliases for `GET /agents` and `GET /audit`, with the same query parameters and responses.
## Password reset and email verification
```
POST /auth/forgot-password
POST /auth/reset-password
POST /auth/verify-email/send
POST /auth/verify-email/confirm
```
These forward the request to `theauth.passwordReset` and `theauth.emailVerification`, which exist only when you pass `passwordReset` or `emailVerification` to `createTheAuth` (and, for password reset, `username` and `auth.session`). When the module is not configured the route returns `404`. Request and response bodies are defined by those modules, not by the agent endpoints above.
## Related
Framework adapters that mount these endpoints on your server.
Core concepts behind the agent endpoints and token lifecycle.
Querying and exporting audit logs via the REST API.
Error codes and HTTP status reference for all endpoints.
---
# OpenAPI spec
Source: https://docs.theauth.dev/openapi
`generateOpenAPISpec` returns an OpenAPI 3.1 document describing the agent, authorization, audit and delegation REST endpoints. Feed it to a code generator to produce a client for Python, Java, Rust or any other language.
```ts title="openapi.ts"
import { generateOpenAPISpec } from '@glinr/theauth';
import { writeFileSync } from 'node:fs';
const spec = generateOpenAPISpec({
baseUrl: 'https://auth.example.com/api/theauth', // default http://localhost:3000
version: '1.0.0', // default 0.0.1
});
writeFileSync('theauth-openapi.json', JSON.stringify(spec, null, 2));
```
Then generate a client, for example:
```bash
npx @openapitools/openapi-generator-cli generate -i theauth-openapi.json -g python -o ./theauth-client
```
## Serving it
The function is pure, so you can expose it from any route:
```ts
app.get('/openapi.json', (c) => c.json(generateOpenAPISpec({ baseUrl: new URL(c.req.url).origin })));
```
## What it covers
| Path | Purpose |
|---|---|
| `/agents`, `/agents/{id}`, `/agents/{id}/rotate` | Create, read, update, revoke and rotate agents |
| `/authorize`, `/authorize/token` | Authorization checks by agent ID or bearer token |
| `/audit` | Query the audit trail |
| `/delegations` | Delegation chains |
Requests authenticate with a bearer token (`BearerAuth` security scheme). The spec describes the core agent API only. Endpoints that plugins register (email, OAuth, passkeys and so on) are not included; see [Writing plugins](/plugins) for how those are mounted.
---
# Writing plugins
Source: https://docs.theauth.dev/plugins
A plugin is a plain object with an `id`, an optional `init` function and optional `hooks`. `@glinr/theauth-email`, the discovery plugin and the telemetry plugin are built this way. You pass plugins to `createTheAuth({ plugins: [...] })`.
## A complete plugin
```ts title="ping-plugin.ts"
import { createTheAuth, type TheAuthPlugin } from '@glinr/theauth';
const json = (body: unknown, status = 200) =>
new Response(JSON.stringify(body), { status, headers: { 'Content-Type': 'application/json' } });
export function ping(): TheAuthPlugin {
return {
id: 'example-ping',
async init(ctx) {
ctx.addMigration(
'CREATE TABLE IF NOT EXISTS example_pings (id TEXT PRIMARY KEY, at INTEGER NOT NULL)',
);
ctx.addEndpoint({
method: 'GET',
path: '/ping/:name',
metadata: { description: 'Say hello', rateLimit: { window: 60, max: 30 }, requireAuth: false },
async handler(request) {
const name = new URL(request.url).searchParams.get('_param_name');
return json({ hello: name });
},
});
return { context: { pingVersion: 1 } };
},
};
}
const theauth = await createTheAuth({
database: { provider: 'sqlite', url: ':memory:' },
plugins: [ping()],
});
const res = await theauth.plugins.handleRequest(new Request('http://localhost/ping/ada'));
console.log(await res?.json()); // { hello: 'ada' }
```
## The context passed to `init`
| Member | Purpose |
|---|---|
| `db` | The Drizzle database handle. |
| `config` | The resolved `createTheAuth` config. |
| `addEndpoint(endpoint)` | Register an HTTP endpoint (below). |
| `addMigration(sql)` | Queue a `CREATE TABLE IF NOT EXISTS` statement. Migrations run after all plugins initialize, so keep each statement idempotent. |
| `sessionManager` | The shared session manager, or `null` when `auth.session` is not configured. |
`init` may return `{ context }`; those values are merged into `theauth.plugins.getContext()`.
## Endpoints
An endpoint has a `method`, a `path` relative to the mount point, a `handler(request, ctx)` that takes a Web `Request` and returns a `Response`, and optional `metadata`:
- `rateLimit: { window, max }`: `window` is in seconds. Counted per client IP, in memory, per process. Over the limit the router returns 429.
- `requireAuth`: when true, the router calls `getUser` first and returns 401 if there is no session.
- `description`: used for documentation.
Paths support `:param` segments. The router copies captured values into the request URL as `_param_` query parameters, as shown above. The handler `ctx` also gives you `db`, `getUser(request)` and `getSession(token)`.
The JSON, body-parsing and cookie helpers that built-in plugins use are internal, so write small equivalents in your own plugin (the `json` function above is one).
## Lifecycle hooks
```ts
hooks: {
onRequest: async (request) => undefined, // return a Request to replace it, or a Response to short-circuit
onAuthenticate: async (user, session) => {}, // after a successful sign-in
onSessionCreate: async (userId) => ({ source: 'my-plugin' }), // return metadata to attach
onSessionRevoke: async (sessionId) => {},
}
```
## Serving plugin endpoints
`theauth.plugins.handleRequest(request, basePath?)` returns a `Response`, or `null` when no endpoint matches, so you can fall through to the rest of your app. `theauth.plugins.getEndpoints()` lists every registered endpoint, which is what the [framework adapters](/adapters) use to mount routes. Strip your mount prefix by passing it as `basePath`.
```ts
const response = await theauth.plugins.handleRequest(request, '/api/theauth');
return response ?? new Response('Not found', { status: 404 });
```
## Notes
- `schema` on a plugin is for Drizzle type safety only. Tables are created by `addMigration`.
- `AuthPlugin` is a deprecated alias of `TheAuthPlugin`.
- Plugin rate limits are per process. Behind several instances, enforce limits at your gateway as well.
---
# Redirect chains
Source: https://docs.theauth.dev/redirect
`createRedirectChain` remembers the page a user wanted before they were sent to sign in, lets you queue extra steps (verify email, onboarding), and tells you where to send them next. State lives in one short-lived cookie, so it works on any runtime that has Web `Request` and `Response`.
```ts title="redirects.ts"
import { createRedirectChain } from '@glinr/theauth/redirect';
const redirects = createRedirectChain({ defaultPath: '/dashboard' });
// 1. Auth middleware: not signed in. Remember where they were headed.
function requireSignIn(request: Request): Response {
return new Response(null, {
status: 302,
headers: { Location: '/sign-in', 'Set-Cookie': redirects.capture(request) },
});
}
// 2. After sign-up: add steps that must happen first.
const c1 = redirects.push('/verify-email', { label: 'verify' });
// 3. As each step finishes, pop the next destination.
function next(request: Request): Response {
const { url, done, clearCookie } = redirects.pop(request);
const headers = new Headers({ Location: url });
if (done && clearCookie) headers.append('Set-Cookie', clearCookie);
return new Response(null, { status: 302, headers });
}
```
`push` returns a `Set-Cookie` value, so send it on the response that moves the user to the next step. The same helpers are also exported from `@glinr/theauth`.
## API
| Method | Returns |
|---|---|
| `capture(request)` | `Set-Cookie` that stores the request URL as the origin. Keeps an existing origin if the user reloads the sign-in page. |
| `push(path, { label?, query? })` | `Set-Cookie` with a step added. Steps pop last-in, first-out, then the origin. |
| `pop(request)` | `{ url, done, clearCookie }`. `done` is true once the origin has been returned. |
| `peek(request)` | `{ url, remaining }` without consuming, or `null`. |
| `getOrigin(request)`, `parse(request)` | The origin entry or full chain state, or `null` if absent or expired. |
| `clear()` | `Set-Cookie` that deletes the chain. |
| `buildUrl(entry)`, `createEntry(url, label?)` | Helpers for entries. |
## Options
| Option | Default |
|---|---|
| `cookieName` | `theauth_redirect` |
| `maxAge` (seconds) | `600` |
| `defaultPath` | `/` |
| `excludePaths` | `/sign-in`, `/sign-up`, `/forgot-password`, `/reset-password`, `/verify-email`, `/api/` |
| `preserveQuery`, `preserveHash` | `true` |
| `maxDepth` | `10` |
| `cookie` | `httpOnly`, `secure`, `sameSite: "lax"`, `path: "/"` |
If the captured page is in `excludePaths`, the chain falls back to `defaultPath`, so users never bounce back to the sign-in page.
## Security notes
Entries created from a URL keep only its path, query and hash, never the origin. The cookie itself is base64url JSON and is **not signed**, so a user can edit it. Before you redirect to the `url` that `pop` or `peek` returns, check that it starts with a single `/` (and not `//` or `/\`), and validate any `redirectTo` value you accept from users before passing it to `createEntry` or `push`.
```ts
const safe = (u: string) => u.startsWith('/') && !u.startsWith('//') && !u.startsWith('/\\');
```
---
# Error codes
Source: https://docs.theauth.dev/errors
theAuth returns structured errors in the following shape:
```typescript
interface TheAuthError {
code: string; // machine-readable error code
message: string; // human-readable description
details?: Record; // additional context
}
```
All SDK functions that can fail return a `Result` discriminated union rather than throwing:
```typescript
const result = await theauth.authorize(agentId, { action: 'write', resource: 'mcp:github:*' });
if (!result.allowed) {
console.log(result.reason); // e.g. 'Rate limit exceeded: 20/20 calls per hour for resource "mcp:deploy:staging"'
}
```
For REST API calls, errors are returned as JSON with the corresponding HTTP status code:
```json
{
"code": "AGENT_NOT_FOUND",
"message": "Agent agt_01abc does not exist or has been revoked."
}
```
## Error code reference
`theauth.authorize()` and `theauth.delegate()` do not return the permission and delegation codes in the tables below. `authorize()` returns `{ allowed, reason, auditId }` where `reason` is free-form text (for example `No permission grants agent "x" access to "write" on "mcp:github:repos"`), and `delegate()` throws a plain `Error` with a descriptive message (a subset violation or a depth overrun). Treat the Agent, Permission, Token, and Delegation tables as a reference vocabulary for your own HTTP layer, not as values the TypeScript core emits. The MCP module returns `Result` errors with codes such as `INVALID_CLIENT`, `INVALID_GRANT`, `LOGIN_REQUIRED`, `INVALID_TOKEN`, `TOKEN_EXPIRED`, `INVALID_AUDIENCE`, `INVALID_ISSUER`, `INSUFFICIENT_SCOPE`, `UNAUTHORIZED`, and `SERVER_ERROR`.
### Agent errors
| Code | HTTP status | Description |
|------|-------------|-------------|
| `AGENT_NOT_FOUND` | 404 | No agent with the given ID exists. |
| `AGENT_LIMIT_EXCEEDED` | 422 | The user has reached their `maxPerUser` agent limit. |
| `AGENT_REVOKED` | 403 | The agent has been explicitly revoked and cannot be used. |
| `AGENT_EXPIRED` | 403 | The agent's `expiresAt` is in the past. |
### Permission errors
| Code | HTTP status | Description |
|------|-------------|-------------|
| `PERMISSION_DENIED` | 403 | The agent does not have a permission matching the requested resource and action. |
| `RATE_LIMITED` | 429 | The agent has exceeded its `maxCallsPerHour` constraint for this resource. |
| `OUTSIDE_TIME_WINDOW` | 403 | The current time falls outside the permission's `timeWindow` constraint. |
| `IP_NOT_ALLOWED` | 403 | The request IP is not in the permission's `ipAllowlist`. |
| `REQUIRES_APPROVAL` | 202 | The action requires human approval (`requireApproval: true`). An approval request has been created. |
### Token errors
| Code | HTTP status | Description |
|------|-------------|-------------|
| `INVALID_TOKEN` | 401 | The token cannot be verified (bad signature, malformed, or unknown). |
| `TOKEN_EXPIRED` | 401 | The token's `exp` claim is in the past. |
### Delegation errors
| Code | HTTP status | Description |
|------|-------------|-------------|
| `DELEGATION_DEPTH_EXCEEDED` | 422 | The delegation would exceed the `maxDepth` limit. |
| `INSUFFICIENT_PERMISSIONS` | 403 | The delegating agent is attempting to grant permissions it does not hold. |
| `DELEGATION_NOT_FOUND` | 404 | No delegation with the given ID exists. |
| `DELEGATION_EXPIRED` | 403 | The delegation chain's `expiresAt` is in the past. |
### MCP / OAuth errors
| Code | HTTP status | Description |
|------|-------------|-------------|
| `MCP_CLIENT_NOT_FOUND` | 401 | The OAuth `client_id` is not registered. |
| `MCP_INVALID_GRANT` | 400 | The authorization code or refresh token is invalid, expired, or already consumed. |
| `MCP_INVALID_REDIRECT_URI` | 400 | The `redirect_uri` does not match the registered client. |
| `MCP_PKCE_FAILED` | 400 | The `code_verifier` does not match the stored `code_challenge`. |
| `MCP_SCOPE_INSUFFICIENT` | 403 | The token does not carry the required scopes for this endpoint. |
| `MCP_CLIENT_DISABLED` | 403 | The OAuth client has been disabled by an administrator. |
### General errors
| Code | HTTP status | Description |
|------|-------------|-------------|
| `BAD_REQUEST` | 400 | The request body or parameters failed validation. |
| `UNAUTHORIZED` | 401 | No valid credential was provided. |
| `FORBIDDEN` | 403 | The credential is valid but does not have access to this resource. |
| `NOT_FOUND` | 404 | The requested resource does not exist. |
| `INTERNAL_ERROR` | 500 | An unexpected error occurred. Check server logs for details. |
## Related
Full REST endpoint reference with request and response shapes.
Agent creation, rotation, and revocation that produce these error codes.
Permission and delegation errors explained in context.
Assert on error codes in unit tests without a real database.
---
# Test utilities
Source: https://docs.theauth.dev/test-utils
`@glinr/theauth-test-utils` provides factories, mock servers, and assertion helpers so you can test auth-dependent code without a real database or network.
## Install
```bash
pnpm add -D @glinr/theauth-test-utils
```
## Factories
Factory functions create realistic mock entities with sensible defaults. Pass overrides for any fields relevant to the test.
```ts
import {
createMockUser,
createMockSession,
createMockAgent,
createMockPermission,
} from '@glinr/theauth-test-utils';
const user = createMockUser({ email: 'alice@example.com' });
const session = createMockSession({ user });
const agent = createMockAgent({ type: 'service', permissions: [] });
const perm = createMockPermission({ resource: 'files', actions: ['read', 'write'] });
```
Each call generates unique IDs, so you can create multiple entities in the same test without collisions.
## Mock auth server
`createMockAuthServer` returns an in-memory `AuthAdapter` implementation with zero network or database calls. Use it in server-side unit tests that exercise code paths calling `resolveUser`, `getUser`, or `syncUser`.
```ts
import { createMockAuthServer, createMockUser } from '@glinr/theauth-test-utils';
const server = createMockAuthServer();
const user = createMockUser();
server.addUser(user);
server.setActiveUser(user.id);
const resolved = await server.resolveUser(new Request('https://example.com'));
// resolved.id === user.id
```
### Per-request user override
Set the `x-mock-theauth-user-id` header on a `Request` to override the active user for that specific request only, without calling `setActiveUser`:
```ts
import { MOCK_USER_ID_HEADER } from '@glinr/theauth-test-utils';
const req = new Request('https://example.com', {
headers: { [MOCK_USER_ID_HEADER]: user.id },
});
const resolved = await server.resolveUser(req);
```
### Cleanup
```ts
afterEach(() => server.reset()); // clears the store and active session
```
## Assertions
Three typed assertion helpers narrow `ActionResult` (the result type used by `@glinr/theauth-react`, where a failure carries an `error` string) and throw descriptive errors on failure.
```ts
import {
expectAuthenticated,
expectUnauthenticated,
expectPermissionDenied,
} from '@glinr/theauth-test-utils';
// Passes only when result.success === true
expectAuthenticated(result);
console.log(result.data); // typed
// Passes only when result.success === false
expectUnauthenticated(result);
// Passes only when result.success === false and error contains "permission" (case-insensitive)
expectPermissionDenied(result);
// Custom substring match
expectPermissionDenied(result, 'not allowed');
```
## Mock React provider
For component tests, `MockTheAuthProvider` replaces `` with fixed values. It accepts `user`, `session`, `isAuthenticated`, `isLoading`, and optional `signIn`, `signUp`, `signOut`, and `refresh` overrides. The actions default to `vi.fn()` spies, so you can assert on calls.
```tsx
import { MockTheAuthProvider, createMockUser, createMockSession } from '@glinr/theauth-test-utils';
const user = createMockUser();
const session = createMockSession({ user });
render(
);
```
The mock server itself has no dependencies. It matches the `AuthAdapter` interface structurally, so TypeScript will accept it anywhere an `AuthAdapter` is expected. The package entry point also exports `MockTheAuthProvider`, which imports `vitest`, `react` and `@glinr/theauth-react` (`react` and `@glinr/theauth-react` are optional peer dependencies), so use the package from a Vitest setup.
## Related
How theAuth reports errors.
In-memory SQLite for integration tests that need a real database.
Agent creation and permission checking to exercise in integration tests.
Lifecycle hooks you can attach to your instance.
---
# A2A protocol
Source: https://docs.theauth.dev/a2a
## What A2A is
The Agent-to-Agent (A2A) protocol is an open standard from Google that lets AI agents discover and communicate with each other. Where MCP handles the connection between agents and tools, A2A handles the connection between agents themselves.
Think of it this way: MCP is how an agent uses a tool. A2A is how an agent talks to another agent.
theAuth implements A2A with built-in authentication, so every agent interaction is identity-verified and audited.
### A2A and MCP compared
| | MCP | A2A |
|---|---|---|
| Purpose | Agent-to-tool communication | Agent-to-agent communication |
| Discovery | OAuth metadata endpoints | `/.well-known/agent.json` |
| Protocol | Tool-specific RPC | JSON-RPC 2.0 |
| Auth | OAuth 2.1 | OAuth 2.1, API keys, OIDC, mTLS |
| Lifecycle | Stateless tool calls | Stateful task lifecycle |
| Streaming | Not defined | SSE via `message/stream` |
## Setup
### Install
```bash
pnpm add @glinr/theauth
```
### Create an A2A server
The A2A server exposes your agent as a JSON-RPC endpoint that other agents can call.
```typescript
import { createTheAuth } from '@glinr/theauth';
import { createAgentCard, createA2AServer } from '@glinr/theauth/a2a';
const theauth = await createTheAuth({
database: { provider: 'sqlite', url: 'theauth.db' },
});
// Register an agent identity
const agent = await theauth.agent.create({
ownerId: 'user-1',
name: 'Code Reviewer',
type: 'service',
permissions: [{ resource: 'a2a:*', actions: ['execute'] }],
});
// Build the A2A Agent Card
const card = createAgentCard({
agent: { id: agent.id, name: agent.name, type: agent.type },
url: 'https://your-app.com/a2a',
description: 'Reviews pull requests and suggests improvements',
version: '1.0.0',
skills: [
{
id: 'review-code',
name: 'Code review',
description: 'Analyzes code changes and provides feedback',
tags: ['code', 'review', 'quality'],
},
],
capabilities: { streaming: true },
securitySchemes: {
bearer: { type: 'http', scheme: 'bearer', bearerFormat: 'JWT' },
},
security: [{ bearer: [] }],
});
// Create the server
const server = createA2AServer({
agentCard: card,
handler: {
onMessage: async (message) => {
// Your agent logic here
const text = message.parts
.filter((p) => p.type === 'text')
.map((p) => p.text)
.join('\n');
return {
id: crypto.randomUUID(),
contextId: crypto.randomUUID(),
status: { code: 'completed' },
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
artifacts: [
{
id: crypto.randomUUID(),
parts: [{ type: 'text', text: `Review complete for: ${text}` }],
createdAt: new Date().toISOString(),
},
],
};
},
},
authenticate: async (request) => {
const token = request.headers.get('Authorization')?.replace('Bearer ', '');
if (!token) return null;
// Agent bearer tokens (kv_...) are validated against the stored hash
const caller = await theauth.agent.validateToken(token);
return caller?.id ?? null;
},
onAudit: async (event) => {
console.log('[A2A audit]', event.method, event.agentId, event.success);
},
});
// Mount in your framework
// app.all('/a2a', (req) => server.handleRequest(req));
// app.get('/.well-known/agent.json', (req) => server.handleRequest(req));
```
### A2A server config
The Agent Card that describes this agent. Served at /.well-known/agent.json.
Callbacks for handling incoming messages, cancellations, and streaming.
Promise`}>Validates the caller. Return an agent/user ID or null to reject. Optional, omit to allow unauthenticated calls.
Promise`}>Called after each A2A interaction for audit logging.
Custom task persistence. Defaults to an in-memory Map.
## Agent Cards
An Agent Card is a JSON document that describes what an agent can do, how to authenticate with it, and where to reach it. Other agents fetch this card to decide whether and how to communicate.
### Creating a card
```typescript
import { createAgentCard } from '@glinr/theauth/a2a';
const card = createAgentCard({
agent: { id: 'agent-1', name: 'Translator', type: 'service' },
url: 'https://translator.example.com/a2a',
description: 'Translates text between languages',
version: '2.1.0',
skills: [
{
id: 'translate',
name: 'Translate text',
description: 'Translates text from one language to another',
supportedMediaTypes: ['text/plain'],
tags: ['translation', 'i18n'],
},
],
provider: { name: 'Acme Corp', url: 'https://acme.com' },
defaultInputModes: ['text/plain'],
defaultOutputModes: ['text/plain'],
});
```
### Validating a card
```typescript
import { validateAgentCard } from '@glinr/theauth/a2a';
const result = validateAgentCard(incomingJson);
if (!result.success) {
console.error('Bad card:', result.error.message);
}
```
### Signing and verifying
Agent Cards can be signed with a private key so that consumers can verify the card hasn't been tampered with.
```typescript
import { generateKeyPair } from 'jose';
import { signAgentCard, verifyAgentCard } from '@glinr/theauth/a2a';
const { publicKey, privateKey } = await generateKeyPair('ES256');
// Sign
const signed = await signAgentCard({ card, privateKey });
if (!signed.success) throw new Error(signed.error.message);
// Verify
const verified = await verifyAgentCard({
card: signed.data,
publicKey,
});
if (verified.success) console.log('Valid:', verified.data.valid); // true
```
## Calling other agents
The A2A client discovers remote agents and sends them tasks.
```typescript
import { createA2AClient } from '@glinr/theauth/a2a';
const client = createA2AClient({
agent: 'https://remote-agent.example.com',
// The bearer token for the calling agent: the `token` returned by
// theauth.agent.create() or theauth.agent.rotate(). It is shown only once,
// so keep it in your secret store.
getAuthToken: async () => process.env.AGENT_TOKEN!,
});
// Discover the remote agent's capabilities
const discovery = await client.discover();
if (discovery.success) {
console.log('Skills:', discovery.data.skills.map((s) => s.name));
}
// Send a message
const result = await client.sendMessage({
message: {
id: crypto.randomUUID(),
role: 'user',
parts: [{ type: 'text', text: 'Review this PR: https://github.com/...' }],
createdAt: new Date().toISOString(),
},
});
if (result.success) {
console.log('Task status:', result.data.status.code);
console.log('Artifacts:', result.data.artifacts);
}
```
### Retrieving and canceling tasks
```typescript
// Get a task by ID
const task = await client.getTask({ id: 'task-123' });
// Cancel a running task
const canceled = await client.cancelTask({ id: 'task-123' });
```
## Authentication between agents
theAuth supports all A2A security schemes. You declare them in the Agent Card and enforce them in the server's `authenticate` callback.
```typescript
const card = createAgentCard({
// ...
securitySchemes: {
bearer: { type: 'http', scheme: 'bearer', bearerFormat: 'JWT' },
},
security: [{ bearer: [] }],
});
const server = createA2AServer({
agentCard: card,
handler: { onMessage: handleMessage },
authenticate: async (req) => {
const token = req.headers.get('Authorization')?.slice(7);
if (!token) return null;
const caller = await theauth.agent.validateToken(token);
return caller?.id ?? null;
},
});
```
```typescript
const card = createAgentCard({
// ...
securitySchemes: {
apiKey: { type: 'apiKey', name: 'X-API-Key', in: 'header' },
},
security: [{ apiKey: [] }],
});
const server = createA2AServer({
agentCard: card,
handler: { onMessage: handleMessage },
authenticate: async (req) => {
const key = req.headers.get('X-API-Key');
if (!key) return null;
// An agent bearer token sent in a custom header
const agent = await theauth.agent.validateToken(key);
return agent?.id ?? null;
},
});
```
```typescript
const card = createAgentCard({
// ...
securitySchemes: {
oauth2: {
type: 'oauth2',
flows: {
clientCredentials: {
tokenUrl: 'https://auth.example.com/oauth/token',
scopes: { 'a2a:call': 'Call this agent' },
},
},
},
},
security: [{ oauth2: ['a2a:call'] }],
});
```
## Task lifecycle
A2A tasks go through a defined state machine:
```
submitted -> working -> completed
\-> failed
\-> canceled
\-> input-required -> working -> ...
\-> auth-required -> working -> ...
\-> rejected
```
Each state transition is tracked with timestamps. When the server's `onAudit` callback is set, every interaction is logged with the method name, caller identity, task ID, and outcome.
A2A tasks are independent from MCP tool calls. An agent might use MCP to call tools internally while processing an A2A task, but the two protocols operate at different layers.
## JSON-RPC methods
| Method | Purpose |
|---|---|
| `message/send` | Send a message and get a task result |
| `message/stream` | Send a message and receive SSE events |
| `tasks/get` | Retrieve a task by ID |
| `tasks/cancel` | Cancel a running task |
## Related
OAuth 2.1 authorization server for MCP tool connections.
Create and manage agent identities with scoped permissions.
Chain agent permissions across trust boundaries.
Query and export the full agent activity trail.
---
# Compare theAuth
Source: https://docs.theauth.dev/compare/index
Auth is a crowded space. These pages are meant to help you pick the right tool, not sell you on any particular one. If a competitor does something better, we say so.
Both are MIT TypeScript libraries with a broad OAuth provider list. The split is agent primitives: better-auth treats agents as OAuth clients; theAuth makes them first-class entities with delegation, ephemeral sessions, trust scoring, and compliance exports.
Hanko is a passkey-first library with an AGPL backend. If passkeys are your entire auth story and you have no agent workloads, it's worth a look. If you need any agent identity or RBAC, you'll be building on top of it yourself.
Casdoor is a deployed Go IAM service with LDAP, CAS, and RADIUS support. It targets employee SSO in Go shops. theAuth is a library, not a service, and it's built for agent-native TypeScript apps.
Clerk and Auth0 are polished, fast to start, and expensive at scale. They're also closed-source. This page covers the open-source vs. managed tradeoff without reproducing any pricing tables.
---
# theAuth vs better-auth
Source: https://docs.theauth.dev/compare/vs-ba
better-auth is a solid, well-maintained TypeScript auth library. It has more OAuth providers today, a mature Prisma integration, and a large ecosystem of community plugins. If you're building a standard web app where human auth is the entire story, it gets you there fast.
theAuth starts from a different premise: agents are first-class entities, not OAuth clients. The comparison below reflects that split honestly. Where better-auth ships something, we say so. Where it doesn't, we say that too.
A migration guide from better-auth is coming soon.
## Feature matrix
| Capability | theAuth | better-auth |
|---|---|---|
| Language | TypeScript, MIT | TypeScript, MIT |
| Named OAuth providers | 24 | 37 |
| MCP OAuth 2.1 server | Built in with agent identity, delegation, and ephemeral sessions | Thin OIDC wrapper plugin |
| Agent identity | First-class `AgentIdentity` entity next to `User` | Treated as an OAuth client |
| A2A protocol | Server + client + Agent Cards with JWS signing | Not shipped |
| Ephemeral agent sessions | Built in with auto-expiry, action limits, and audit grouping | Not shipped |
| Cost attribution per agent/tool/chain | Built in with alerts and budget integration | Not shipped |
| Trust scoring | 5-level built in | Not shipped |
| Compliance reports (EU AI Act, NIST AI RMF, SOC 2, ISO 42001) | Exports built in | Not shipped |
| Unified RBAC + ABAC + ReBAC policy engine | One engine | RBAC only |
| Approval flows (CIBA) | Built in | Not shipped |
| Verifiable Credentials audit export | On roadmap | Not shipped |
| Edge runtime (Workers, Deno, Bun) | Zero `node:crypto` imports, Web Crypto throughout | Partial |
| DB adapters | Drizzle (core) plus Prisma (`@glinr/theauth-prisma`) | Prisma, Drizzle, Kysely, Mongo, Redis |
| Client libraries | React, Vue, Svelte, Electron, Expo, plain fetch | React, Vue, Svelte, Solid, Electron, Expo |
## Pick theAuth if
- Your app runs AI agents with their own identity, permissions, or audit requirements.
- You need MCP OAuth 2.1 with proper agent delegation, not just an OIDC wrapper.
- You're targeting Cloudflare Workers, Deno, or Bun and need full edge compatibility from day one.
## Pick better-auth if
- You're building a human-facing web app with no agent workloads.
- You need one of the 13 additional OAuth providers it ships that theAuth doesn't yet cover.
- You want a Mongo or Redis adapter and Prisma first-class support right now.
Both are MIT, both are TypeScript. The question is whether agents are part of your architecture.
## Related
Step-by-step migration with before and after code diffs.
How theAuth compares to the passkey-first auth library.
theAuth versus Clerk and Auth0 across cost and agent support.
The mental model behind agents, permissions, and audit.
---
# theAuth vs Hanko
Source: https://docs.theauth.dev/compare/vs-hanko
Hanko is built around one idea: passkeys should be the default, not the fallback. It's a focused library with a Go backend, official TypeScript bindings, and an AGPL license. If passkeys are your entire auth surface and you have zero agent workloads, it's worth a serious look.
theAuth includes passkey support as one method among many. The bigger difference is what happens after the human authenticates: theAuth was designed for the agent layer that comes next.
A migration guide from Hanko is coming soon.
## Feature matrix
| Capability | theAuth | Hanko |
|---|---|---|
| License | MIT | AGPL (backend), MIT (JS SDK) |
| Primary focus | Agent-first auth SDK | Passkey-first auth |
| TypeScript SDK | Yes, first-party | Yes, first-party |
| Named OAuth providers | 24 | ~3 (Google, Apple, GitHub) |
| Passkey support | Yes | Yes, core focus |
| MCP OAuth 2.1 server | Built in | Not shipped |
| Agent identity | First-class `AgentIdentity` entity | Not shipped |
| RBAC / permissions | Unified RBAC + ABAC + ReBAC | Not shipped |
| Ephemeral sessions | Built in with auto-expiry and audit grouping | Not shipped |
| Edge runtime | Web Crypto throughout | Go backend required |
| Self-hostable | Yes | Yes |
## Pick theAuth if
- You need more than passkeys: OAuth providers, agent identity, or a policy engine.
- You're building AI-powered products where agents need their own auth layer.
- You want a permissive MIT license for the full stack, not just the client SDK.
## Pick Hanko if
- You want the smallest possible passkey-only library with a tight scope.
- You have no agent story and passkey-first is exactly the feature you need.
- You're comfortable with AGPL for your backend auth service.
Hanko does one thing well. theAuth does more, which is a tradeoff in either direction.
## Related
Feature comparison with the TypeScript-native auth library.
theAuth as a library versus Casdoor as a standalone Go service.
WebAuthn / FIDO2 support in theAuth.
What theAuth adds beyond passkey auth for the agent layer.
---
# theAuth vs Casdoor
Source: https://docs.theauth.dev/compare/vs-casdoor
Casdoor is a deployed Go IAM service. You run it as a separate process alongside your app, and it handles SSO, LDAP, CAS, RADIUS, and a native MCP OAuth server. It's designed for organizations that need a standalone identity provider, especially in Go environments.
theAuth is a library, not a service. You import it into your TypeScript app and it runs in-process. Same MCP OAuth 2.1 spec, very different deployment model.
A migration guide from Casdoor is coming soon.
## Feature matrix
| Capability | theAuth | Casdoor |
|---|---|---|
| Language | TypeScript library | Go service |
| First-party TypeScript SDK | Yes | No (third-party only) |
| Deployment model | In-process library | Standalone IAM server |
| MCP OAuth 2.1 server | Built in with agent delegation | Built in |
| Agent identity | First-class `AgentIdentity` entity with delegation and audit | Not shipped |
| LDAP / CAS / RADIUS | Not shipped | Yes |
| RBAC | Unified RBAC + ABAC + ReBAC | RBAC via Casbin |
| Ephemeral agent sessions | Built in | Not shipped |
| Cost attribution | Built in | Not shipped |
| Trust scoring | 5-level built in | Not shipped |
| Edge runtime | Web Crypto throughout | Go, not applicable |
| Self-hostable | Yes | Yes |
| License | MIT | Apache 2.0 |
## Pick theAuth if
- You're building a TypeScript or edge-native app and want auth in-process, not as a sidecar.
- You need first-class agent primitives: delegation, ephemeral sessions, trust scoring, and cost attribution.
- Your MCP OAuth story needs to know which agent made which call, not just which client.
## Pick Casdoor if
- You want a deployed IAM service that your whole organization can log into, including non-TypeScript services.
- You need LDAP, CAS, or RADIUS compatibility for employee SSO.
- You're in a Go shop and want to own the full server.
The clearest signal: if you're writing `import { createTheAuth }`, theAuth. If you're writing `docker run casdoor`, Casdoor.
## Related
How theAuth compares to the TypeScript-native auth library.
theAuth versus the passkey-first Go auth library.
How theAuth implements the MCP authorization server spec.
First-class agent identities with delegation and audit.
---
# theAuth vs paid auth platforms
Source: https://docs.theauth.dev/compare/vs-paid
Clerk and Auth0 are polished products. The onboarding is fast, the UI components are good, and you can be in production in an afternoon. That's a real advantage worth naming before anything else.
The tradeoff is real too. You don't own the code, self-hosting is not an option, and the bill scales with your user count. Both have free tiers that feel generous until you hit the ceiling. Check their pricing pages directly, [Clerk pricing](https://clerk.com/pricing) and [Auth0 pricing](https://auth0.com/pricing), before making a spreadsheet.
## The structural differences
**Open source vs. closed.** theAuth is MIT. You can read every line, fork it, run it anywhere. Paid platforms are proprietary. You're trusting their security posture, their uptime SLA, and their product roadmap.
**Self-hosted vs. vendor.** Running theAuth means you control the database, the logs, and the data residency. That matters if you're in healthcare, finance, or working under GDPR data locality requirements. Vendor platforms handle the infrastructure but own the logs too.
**Agent-native vs. bolted on.** Neither Clerk nor Auth0 was designed with AI agents in mind. Both can issue tokens that an agent can use, but there's no concept of `AgentIdentity`, delegation chains, ephemeral sessions, or cost attribution. You build that layer yourself on top of their APIs. theAuth ships it.
## When managed platforms make sense
Managed auth is the right call when speed matters more than cost or control. If you're validating a product idea, a startup in week two, or a solo developer who doesn't want to run a database, the vendor handles the operational complexity.
It's also worth considering if your team has no one who wants to own auth infrastructure. theAuth is simple to run, but it still runs on your stack.
## When open source makes sense
Once you have a stable user base, the cost curve of managed platforms typically exceeds what it costs to run a database and a self-hosted SDK. Beyond cost, open source gives you the ability to audit the code for compliance, patch issues without waiting on a vendor, and keep sensitive auth data inside your own infrastructure.
For any product with AI agents, theAuth is the only open-source option that treats them as first-class entities rather than an afterthought.
## Related
How to move a Clerk app to theAuth with a concrete migration plan.
Comparing theAuth to the open-source better-auth library.
Built-in data export and deletion for self-hosted data residency.
The agent primitives that paid platforms don't ship.
---