npm package: @sp-uvb/elysia
Description: Production-grade Elysia plugin for Universal Verification Broker (UVB) authentication
Author: UVB Team
Latest version: 0.1.0

# @sp-uvb/elysia

Production-grade Elysia plugin for Universal Verification Broker (UVB) authentication. Built using Elysia's Service Locator pattern for type-safe session management across your Bun application.

## Features

- 🔐 **Automatic Session Validation** - Validates sessions on every request
- 🎯 **Service Locator Pattern** - Type-safe `uvbSession` access in all routes
- 🛡️ **MFA Factor Guards** - Require specific authentication factors per route
- 👤 **Resource Ownership** - Verify users own resources they're accessing
- 🚀 **Zero Configuration** - Works out of the box with sensible defaults
- 📊 **Built-in Session Routes** - Optional `/uvb/session` and `/uvb/logout` endpoints
- 🔧 **Fully Customizable** - Override error handlers, customize behavior
- 💪 **TypeScript First** - Complete type safety with Elysia's plugin system

## Installation

```bash
bun add @sp-uvb/elysia elysia
```

## Quick Start

### Basic Setup

```typescript
import { Elysia } from 'elysia';
import { uvb } from '@sp-uvb/elysia';

const app = new Elysia()
  .use(
    uvb({
      uvbUrl: process.env.UVB_URL || 'http://localhost:8080',
      tenantId: process.env.UVB_TENANT_ID || 'tenant_123',
    })
  )
  .get('/', () => 'Hello UVB!')
  .get('/profile', ({ uvbSession }) => {
    if (!uvbSession) {
      return { error: 'Not authenticated' };
    }
    return {
      userId: uvbSession.userId,
      factors: uvbSession.factorsVerified,
    };
  })
  .listen(3000);

console.log(`🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}`);
```

### Required Authentication

```typescript
import { Elysia } from 'elysia';
import { uvb, uvbGuard } from '@sp-uvb/elysia';

const app = new Elysia()
  .use(
    uvb({
      uvbUrl: process.env.UVB_URL!,
      tenantId: process.env.UVB_TENANT_ID!,
      required: true, // All routes require authentication
    })
  )
  .get('/dashboard', ({ uvbSession }) => {
    // uvbSession is guaranteed to exist when required: true
    return {
      welcome: `Hello, user ${uvbSession.userId}!`,
      sessionId: uvbSession.sessionId,
    };
  })
  .listen(3000);
```

## Configuration Options

### UvbPluginOptions

```typescript
interface UvbPluginOptions {
  // Required: UVB server URL
  uvbUrl: string;

  // Required: Your tenant ID
  tenantId: string;

  // Optional: Cookie name for session token (default: 'uvb_session')
  cookieName?: string;

  // Optional: Header name for session token (default: 'x-uvb-session')
  headerName?: string;

  // Optional: Paths to exclude from authentication (default: [])
  excludePaths?: string[];

  // Optional: Require authentication on all routes (default: false)
  required?: boolean;

  // Optional: Custom error handler
  onError?: (context: Context, error: Error) => Response | Promise<Response>;

  // Optional: Custom unauthorized handler
  onUnauthorized?: (context: Context) => Response | Promise<Response>;

  // Optional: Custom insufficient factors handler
  onInsufficientFactors?: (
    context: Context,
    required: string[],
    verified: string[]
  ) => Response | Promise<Response>;
}
```

## Session Object

The `uvbSession` object is automatically attached to your route context:

```typescript
interface UvbSession {
  userId: string; // Unique user identifier
  tenantId: string; // Tenant identifier
  sessionId: string; // Session identifier
  factorsVerified: string[]; // List of verified authentication factors
  expiresAt: Date; // Session expiration timestamp
  metadata?: Record<string, any>; // Optional session metadata
}
```

## Authentication Guards

### Basic Guard

Require authentication for specific routes:

```typescript
import { Elysia } from 'elysia';
import { uvb, uvbGuard } from '@sp-uvb/elysia';

const app = new Elysia()
  .use(
    uvb({
      uvbUrl: process.env.UVB_URL!,
      tenantId: process.env.UVB_TENANT_ID!,
    })
  )
  .get('/public', () => 'Anyone can access')
  .get(
    '/protected',
    ({ uvbSession }) => {
      return { userId: uvbSession!.userId };
    },
    {
      beforeHandle: uvbGuard(),
    }
  )
  .listen(3000);
```

### MFA Factor Requirements

#### Require All Factors

```typescript
import { Elysia } from 'elysia';
import { uvb, uvbRequireAllFactors } from '@sp-uvb/elysia';

const app = new Elysia()
  .use(
    uvb({
      uvbUrl: process.env.UVB_URL!,
      tenantId: process.env.UVB_TENANT_ID!,
    })
  )
  .post(
    '/transfer',
    async ({ body, uvbSession }) => {
      // User has verified password + TOTP + WebAuthn
      return {
        transfer: 'authorized',
        amount: body.amount,
        userId: uvbSession!.userId,
      };
    },
    {
      beforeHandle: uvbRequireAllFactors(['password', 'totp', 'webauthn']),
    }
  )
  .listen(3000);
```

#### Require Any Factor

```typescript
import { Elysia } from 'elysia';
import { uvb, uvbRequireAnyFactor } from '@sp-uvb/elysia';

const app = new Elysia()
  .use(
    uvb({
      uvbUrl: process.env.UVB_URL!,
      tenantId: process.env.UVB_TENANT_ID!,
    })
  )
  .get(
    '/settings',
    ({ uvbSession }) => {
      return {
        userId: uvbSession!.userId,
        mfaEnabled: true,
      };
    },
    {
      beforeHandle: uvbRequireAnyFactor(['totp', 'webauthn', 'sms']),
    }
  )
  .listen(3000);
```

### Resource Ownership Verification

Ensure users can only access their own resources:

```typescript
import { Elysia } from 'elysia';
import { uvb, uvbRequireOwnership } from '@sp-uvb/elysia';

// Mock database
const posts = new Map([
  ['post_1', { id: 'post_1', title: 'Hello', authorId: 'user_123' }],
  ['post_2', { id: 'post_2', title: 'World', authorId: 'user_456' }],
]);

const app = new Elysia()
  .use(
    uvb({
      uvbUrl: process.env.UVB_URL!,
      tenantId: process.env.UVB_TENANT_ID!,
      required: true,
    })
  )
  .delete(
    '/posts/:id',
    ({ params }) => {
      posts.delete(params.id);
      return { deleted: params.id };
    },
    {
      beforeHandle: uvbRequireOwnership({
        getUserId: async ({ params }) => {
          const post = posts.get(params.id);
          if (!post) throw new Error('Post not found');
          return post.authorId;
        },
      }),
    }
  )
  .listen(3000);
```

## Session Management Routes

Add built-in session management endpoints:

```typescript
import { Elysia } from 'elysia';
import { uvb, uvbSessionRoutes } from '@sp-uvb/elysia';

const app = new Elysia()
  .use(
    uvb({
      uvbUrl: process.env.UVB_URL!,
      tenantId: process.env.UVB_TENANT_ID!,
    })
  )
  .use(
    uvbSessionRoutes({
      uvbUrl: process.env.UVB_URL!,
      tenantId: process.env.UVB_TENANT_ID!,
    })
  )
  // Adds:
  // GET  /uvb/session - Get current session info
  // POST /uvb/logout  - Revoke session and clear cookie
  .listen(3000);
```

Now you can:

```bash
# Get session info
curl http://localhost:3000/uvb/session \
  -H "Authorization: Bearer <session_token>"

# Logout
curl -X POST http://localhost:3000/uvb/logout \
  -H "Authorization: Bearer <session_token>"
```

## Advanced Examples

### Conditional MFA Requirements

```typescript
import { Elysia } from 'elysia';
import { uvb, uvbGuard, hasAllFactors } from '@sp-uvb/elysia';

const app = new Elysia()
  .use(
    uvb({
      uvbUrl: process.env.UVB_URL!,
      tenantId: process.env.UVB_TENANT_ID!,
      required: true,
    })
  )
  .post('/api/transactions', async ({ body, uvbSession }) => {
    const amount = parseFloat(body.amount);

    // Require MFA for large transactions
    if (amount > 10000 && !hasAllFactors(uvbSession, ['totp', 'webauthn'])) {
      return new Response(
        JSON.stringify({
          error: 'MFA required for large transactions',
          required: ['totp', 'webauthn'],
          verified: uvbSession!.factorsVerified,
        }),
        {
          status: 403,
          headers: { 'Content-Type': 'application/json' },
        }
      );
    }

    return {
      transaction: 'processed',
      amount,
      userId: uvbSession!.userId,
    };
  })
  .listen(3000);
```

### Custom Error Handlers

```typescript
import { Elysia } from 'elysia';
import { uvb } from '@sp-uvb/elysia';

const app = new Elysia()
  .use(
    uvb({
      uvbUrl: process.env.UVB_URL!,
      tenantId: process.env.UVB_TENANT_ID!,
      required: true,
      onUnauthorized: (context) => {
        return new Response(
          JSON.stringify({
            error: 'Authentication required',
            message: 'Please log in to continue',
            loginUrl: '/auth/login',
          }),
          {
            status: 401,
            headers: {
              'Content-Type': 'application/json',
              'WWW-Authenticate': 'Bearer realm="UVB"',
            },
          }
        );
      },
      onError: (context, error) => {
        console.error('UVB error:', error);
        return new Response(
          JSON.stringify({
            error: 'Internal authentication error',
            requestId: crypto.randomUUID(),
          }),
          {
            status: 500,
            headers: { 'Content-Type': 'application/json' },
          }
        );
      },
    })
  )
  .listen(3000);
```

### Path Exclusions

```typescript
import { Elysia } from 'elysia';
import { uvb } from '@sp-uvb/elysia';

const app = new Elysia()
  .use(
    uvb({
      uvbUrl: process.env.UVB_URL!,
      tenantId: process.env.UVB_TENANT_ID!,
      required: true,
      excludePaths: ['/health', '/metrics', '/public', '/auth'],
    })
  )
  .get('/health', () => ({ status: 'ok' }))
  .get('/public/terms', () => 'Terms of Service...')
  .get('/dashboard', ({ uvbSession }) => {
    return { userId: uvbSession!.userId };
  })
  .listen(3000);
```

### Multiple Authentication Schemes

```typescript
import { Elysia } from 'elysia';
import { uvb } from '@sp-uvb/elysia';

const app = new Elysia()
  .use(
    uvb({
      uvbUrl: process.env.UVB_URL!,
      tenantId: process.env.UVB_TENANT_ID!,
    })
  )
  .get('/api/data', ({ uvbSession, headers }) => {
    // Try UVB session first
    if (uvbSession) {
      return {
        data: 'authenticated via UVB',
        userId: uvbSession.userId,
      };
    }

    // Fallback to API key
    const apiKey = headers['x-api-key'];
    if (apiKey && isValidApiKey(apiKey)) {
      return {
        data: 'authenticated via API key',
        apiKey: apiKey.substring(0, 8) + '...',
      };
    }

    return new Response(JSON.stringify({ error: 'Authentication required' }), {
      status: 401,
      headers: { 'Content-Type': 'application/json' },
    });
  })
  .listen(3000);

function isValidApiKey(key: string): boolean {
  // Your API key validation logic
  return key.startsWith('sk_');
}
```

### WebSocket with Authentication

```typescript
import { Elysia } from 'elysia';
import { uvb } from '@sp-uvb/elysia';

const app = new Elysia()
  .use(
    uvb({
      uvbUrl: process.env.UVB_URL!,
      tenantId: process.env.UVB_TENANT_ID!,
    })
  )
  .ws('/ws', {
    open(ws) {
      const session = ws.data.uvbSession;
      if (!session) {
        ws.close(1008, 'Authentication required');
        return;
      }
      console.log(`User ${session.userId} connected`);
    },
    message(ws, message) {
      const session = ws.data.uvbSession;
      ws.send({
        echo: message,
        userId: session?.userId,
        timestamp: new Date().toISOString(),
      });
    },
    close(ws) {
      console.log('Client disconnected');
    },
  })
  .listen(3000);
```

## Helper Functions

### Check Individual Factor

```typescript
import { hasFactor } from '@sp-uvb/elysia';

app.get('/settings/mfa', ({ uvbSession }) => {
  return {
    totpEnabled: hasFactor(uvbSession, 'totp'),
    webauthnEnabled: hasFactor(uvbSession, 'webauthn'),
    smsEnabled: hasFactor(uvbSession, 'sms'),
  };
});
```

### Check All Factors

```typescript
import { hasAllFactors } from '@sp-uvb/elysia';

app.get('/admin/panel', ({ uvbSession }) => {
  if (!hasAllFactors(uvbSession, ['password', 'totp', 'webauthn'])) {
    return new Response('Admin requires full MFA', { status: 403 });
  }
  return { admin: 'panel' };
});
```

### Check Any Factor

```typescript
import { hasAnyFactor } from '@sp-uvb/elysia';

app.get('/settings', ({ uvbSession }) => {
  const hasMFA = hasAnyFactor(uvbSession, ['totp', 'webauthn', 'sms']);
  return {
    mfaEnabled: hasMFA,
    message: hasMFA ? 'MFA is active' : 'Enable MFA for better security',
  };
});
```

## Real-World Patterns

### E-commerce API

```typescript
import { Elysia } from 'elysia';
import { uvb, uvbGuard, uvbRequireAllFactors, uvbRequireOwnership } from '@sp-uvb/elysia';

// Mock database
const orders = new Map<string, { id: string; userId: string; total: number }>();

const app = new Elysia()
  .use(
    uvb({
      uvbUrl: process.env.UVB_URL!,
      tenantId: process.env.UVB_TENANT_ID!,
    })
  )

  // Public product listing
  .get('/products', () => {
    return [
      { id: 'prod_1', name: 'Widget', price: 29.99 },
      { id: 'prod_2', name: 'Gadget', price: 49.99 },
    ];
  })

  // Cart requires authentication
  .post(
    '/cart',
    ({ body, uvbSession }) => {
      return {
        cart: body,
        userId: uvbSession!.userId,
      };
    },
    {
      beforeHandle: uvbGuard(),
    }
  )

  // Checkout requires MFA
  .post(
    '/checkout',
    async ({ body, uvbSession }) => {
      const orderId = crypto.randomUUID();
      orders.set(orderId, {
        id: orderId,
        userId: uvbSession!.userId,
        total: body.total,
      });
      return { orderId, status: 'confirmed' };
    },
    {
      beforeHandle: uvbRequireAllFactors(['password', 'totp']),
    }
  )

  // View order (must be owner)
  .get(
    '/orders/:id',
    ({ params }) => {
      const order = orders.get(params.id);
      if (!order) {
        return new Response('Order not found', { status: 404 });
      }
      return order;
    },
    {
      beforeHandle: [
        uvbGuard(),
        uvbRequireOwnership({
          getUserId: async ({ params }) => {
            const order = orders.get(params.id);
            if (!order) throw new Error('Order not found');
            return order.userId;
          },
        }),
      ],
    }
  )

  .listen(3000);
```

### Multi-Tenant SaaS

```typescript
import { Elysia } from 'elysia';
import { uvb, uvbGuard } from '@sp-uvb/elysia';

interface User {
  id: string;
  tenantId: string;
  role: 'admin' | 'member';
}

const users = new Map<string, User>([
  ['user_1', { id: 'user_1', tenantId: 'tenant_123', role: 'admin' }],
  ['user_2', { id: 'user_2', tenantId: 'tenant_123', role: 'member' }],
  ['user_3', { id: 'user_3', tenantId: 'tenant_456', role: 'admin' }],
]);

const app = new Elysia()
  .use(
    uvb({
      uvbUrl: process.env.UVB_URL!,
      tenantId: process.env.UVB_TENANT_ID!,
      required: true,
    })
  )
  .derive(({ uvbSession }) => {
    const user = users.get(uvbSession!.userId);
    return { user };
  })

  // Tenant-scoped data access
  .get('/api/workspace/:workspaceId', ({ params, user, uvbSession }) => {
    if (!user) {
      return new Response('User not found', { status: 404 });
    }

    // Verify tenant access
    if (user.tenantId !== uvbSession!.tenantId) {
      return new Response('Forbidden', { status: 403 });
    }

    return {
      workspaceId: params.workspaceId,
      tenantId: user.tenantId,
      role: user.role,
    };
  })

  // Admin-only endpoint
  .post('/api/workspace/:workspaceId/settings', ({ params, user }) => {
    if (!user || user.role !== 'admin') {
      return new Response('Admin access required', { status: 403 });
    }

    return {
      updated: true,
      workspaceId: params.workspaceId,
    };
  })

  .listen(3000);
```

### Social Media API

```typescript
import { Elysia } from 'elysia';
import { uvb, uvbGuard, uvbRequireOwnership, uvbRequireAllFactors } from '@sp-uvb/elysia';

interface Post {
  id: string;
  authorId: string;
  content: string;
  visibility: 'public' | 'private';
}

const posts = new Map<string, Post>();

const app = new Elysia()
  .use(
    uvb({
      uvbUrl: process.env.UVB_URL!,
      tenantId: process.env.UVB_TENANT_ID!,
    })
  )

  // Public feed (no auth required)
  .get('/feed', () => {
    return Array.from(posts.values())
      .filter((p) => p.visibility === 'public')
      .slice(0, 20);
  })

  // Create post (auth required)
  .post(
    '/posts',
    ({ body, uvbSession }) => {
      const postId = crypto.randomUUID();
      const post: Post = {
        id: postId,
        authorId: uvbSession!.userId,
        content: body.content,
        visibility: body.visibility || 'public',
      };
      posts.set(postId, post);
      return post;
    },
    {
      beforeHandle: uvbGuard(),
    }
  )

  // Edit post (must be author)
  .patch(
    '/posts/:id',
    ({ params, body }) => {
      const post = posts.get(params.id);
      if (!post) {
        return new Response('Post not found', { status: 404 });
      }
      post.content = body.content;
      return post;
    },
    {
      beforeHandle: uvbRequireOwnership({
        getUserId: async ({ params }) => {
          const post = posts.get(params.id);
          if (!post) throw new Error('Post not found');
          return post.authorId;
        },
      }),
    }
  )

  // Delete post (must be author + MFA)
  .delete(
    '/posts/:id',
    ({ params }) => {
      posts.delete(params.id);
      return { deleted: params.id };
    },
    {
      beforeHandle: [
        uvbRequireAllFactors(['password', 'totp']),
        uvbRequireOwnership({
          getUserId: async ({ params }) => {
            const post = posts.get(params.id);
            if (!post) throw new Error('Post not found');
            return post.authorId;
          },
        }),
      ],
    }
  )

  .listen(3000);
```

## Testing

### Unit Testing

```typescript
import { describe, expect, it } from 'bun:test';
import { Elysia } from 'elysia';
import { uvb, uvbGuard } from '@sp-uvb/elysia';

describe('UVB Authentication', () => {
  it('should allow public access without auth', async () => {
    const app = new Elysia()
      .use(
        uvb({
          uvbUrl: 'http://localhost:8080',
          tenantId: 'test_tenant',
        })
      )
      .get('/public', () => 'public');

    const response = await app.handle(new Request('http://localhost/public'));
    expect(response.status).toBe(200);
    expect(await response.text()).toBe('public');
  });

  it('should block protected routes without auth', async () => {
    const app = new Elysia()
      .use(
        uvb({
          uvbUrl: 'http://localhost:8080',
          tenantId: 'test_tenant',
        })
      )
      .get(
        '/protected',
        ({ uvbSession }) => {
          return { userId: uvbSession!.userId };
        },
        {
          beforeHandle: uvbGuard(),
        }
      );

    const response = await app.handle(new Request('http://localhost/protected'));
    expect(response.status).toBe(401);
  });

  it('should allow access with valid session', async () => {
    const app = new Elysia()
      .use(
        uvb({
          uvbUrl: 'http://localhost:8080',
          tenantId: 'test_tenant',
        })
      )
      .get(
        '/profile',
        ({ uvbSession }) => {
          return { userId: uvbSession!.userId };
        },
        {
          beforeHandle: uvbGuard(),
        }
      );

    const response = await app.handle(
      new Request('http://localhost/profile', {
        headers: {
          Authorization: 'Bearer valid_session_token',
        },
      })
    );

    expect(response.status).toBe(200);
    const data = await response.json();
    expect(data.userId).toBeDefined();
  });
});
```

### Integration Testing

```typescript
import { describe, expect, it, beforeAll, afterAll } from 'bun:test';
import { Elysia } from 'elysia';
import { uvb, uvbRequireAllFactors } from '@sp-uvb/elysia';

let app: Elysia;
let sessionToken: string;

beforeAll(async () => {
  // Start app
  app = new Elysia()
    .use(
      uvb({
        uvbUrl: process.env.UVB_URL!,
        tenantId: process.env.UVB_TENANT_ID!,
      })
    )
    .get('/data', ({ uvbSession }) => ({ data: 'test' }), {
      beforeHandle: uvbRequireAllFactors(['password', 'totp']),
    })
    .listen(3001);

  // Create test session
  const authResponse = await fetch(`${process.env.UVB_URL}/api/v1/auth/login`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      tenant_id: process.env.UVB_TENANT_ID,
      username: 'test@example.com',
      password: 'password',
    }),
  });
  const authData = await authResponse.json();
  sessionToken = authData.session_token;
});

afterAll(() => {
  app.stop();
});

describe('MFA Requirements', () => {
  it('should block access without MFA', async () => {
    const response = await fetch('http://localhost:3001/data', {
      headers: { Authorization: `Bearer ${sessionToken}` },
    });
    expect(response.status).toBe(403);
  });

  it('should allow access with MFA', async () => {
    // Complete MFA challenge
    await fetch(`${process.env.UVB_URL}/api/v1/mfa/verify`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        session_token: sessionToken,
        factor: 'totp',
        code: '123456',
      }),
    });

    const response = await fetch('http://localhost:3001/data', {
      headers: { Authorization: `Bearer ${sessionToken}` },
    });
    expect(response.status).toBe(200);
  });
});
```

## API Reference

### Main Plugin

#### `uvb(options: UvbPluginOptions)`

Main authentication plugin. Adds session validation and attaches `uvbSession` to context.

### Guards

#### `uvbGuard(options?: UvbGuardOptions)`

Basic authentication guard. Returns 401 if not authenticated.

#### `uvbRequireFactors(factors: string[], options?: UvbGuardOptions)`

Require specific authentication factors. Returns 403 if factors not verified.

#### `uvbRequireAllFactors(factors: string[], options?: UvbGuardOptions)`

Require all specified factors. Returns 403 if any factor missing.

#### `uvbRequireAnyFactor(factors: string[], options?: UvbGuardOptions)`

Require at least one of the specified factors. Returns 403 if no factors match.

#### `uvbRequireOwnership(options: UvbOwnershipOptions)`

Verify user owns a resource. Returns 403 if not owner.

### Utilities

#### `uvbSessionRoutes(options: { uvbUrl: string; tenantId: string })`

Add session management routes:

- `GET /uvb/session` - Get current session
- `POST /uvb/logout` - Revoke session

#### `hasFactor(session: UvbSession | null, factor: string): boolean`

Check if session has specific factor.

#### `hasAllFactors(session: UvbSession | null, factors: string[]): boolean`

Check if session has all specified factors.

#### `hasAnyFactor(session: UvbSession | null, factors: string[]): boolean`

Check if session has any of the specified factors.

## Best Practices

### 1. Environment Variables

Always use environment variables for sensitive configuration:

```typescript
const app = new Elysia().use(
  uvb({
    uvbUrl: process.env.UVB_URL!,
    tenantId: process.env.UVB_TENANT_ID!,
  })
);
```

### 2. Path Exclusions

Exclude health checks and public endpoints:

```typescript
.use(uvb({
  // ...
  excludePaths: ['/health', '/metrics', '/public'],
}))
```

### 3. Custom Error Handlers

Provide user-friendly error messages:

```typescript
.use(uvb({
  // ...
  onUnauthorized: () => new Response(
    JSON.stringify({ error: 'Please log in' }),
    { status: 401, headers: { 'Content-Type': 'application/json' } }
  ),
}))
```

### 4. Factor Requirements

Use appropriate MFA levels for sensitive operations:

```typescript
// Low risk: basic auth
.get('/profile', handler, { beforeHandle: uvbGuard() })

// Medium risk: any MFA
.post('/settings', handler, {
  beforeHandle: uvbRequireAnyFactor(['totp', 'webauthn'])
})

// High risk: all factors
.post('/delete-account', handler, {
  beforeHandle: uvbRequireAllFactors(['password', 'totp', 'webauthn'])
})
```

### 5. Session Expiration

Check session expiration in your application:

```typescript
.get('/data', ({ uvbSession }) => {
  if (uvbSession && uvbSession.expiresAt < new Date()) {
    return new Response('Session expired', { status: 401 })
  }
  return { data: 'value' }
})
```

## Troubleshooting

### Session Not Found

**Problem**: `uvbSession` is always `null`

**Solutions**:

- Check UVB server is running and accessible
- Verify `uvbUrl` and `tenantId` are correct
- Ensure session token is being sent (cookie or header)
- Check session hasn't expired

### 401 Errors

**Problem**: All routes return 401

**Solutions**:

- Set `required: false` for optional auth
- Add public paths to `excludePaths`
- Verify session token is valid

### 403 Errors

**Problem**: Factor requirements failing

**Solutions**:

- Check user has completed required MFA factors
- Use `hasAllFactors()` to debug which factors are verified
- Consider using `uvbRequireAnyFactor` instead of `uvbRequireAllFactors`

### TypeScript Errors

**Problem**: `uvbSession` type errors

**Solutions**:

- Ensure `@sp-uvb/elysia` is properly installed
- Use guards to guarantee session exists
- Check for null: `if (!uvbSession) return ...`

## Migration Guide

### From Custom Elysia Middleware

Before:

```typescript
.derive(async ({ cookie }) => {
  const token = cookie.session
  const session = await validateWithUvb(token)
  return { session }
})
```

After:

```typescript
.use(uvb({
  uvbUrl: process.env.UVB_URL!,
  tenantId: process.env.UVB_TENANT_ID!,
}))
// Access via context.uvbSession
```

### From Manual Factor Checks

Before:

```typescript
.post('/transfer', async ({ session }) => {
  if (!session.factors.includes('totp')) {
    throw new Error('TOTP required')
  }
  // ...
})
```

After:

```typescript
.post('/transfer', handler, {
  beforeHandle: uvbRequireAllFactors(['totp'])
})
```

## License

MIT

## Support

For issues and questions:

- GitHub: [https://github.com/uvb/uvb](https://github.com/uvb/uvb)
- Documentation: [https://docs.uvb.dev](https://docs.uvb.dev)