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

# @sp-uvb/hono

Production-grade Hono middleware for Universal Verification Broker (UVB) authentication. Built for modern JavaScript runtimes including Node.js, Bun, Deno, Cloudflare Workers, and edge environments.

## Features

- 🔐 **Automatic Session Validation** - Validates sessions on every request
- 🌐 **Multi-Runtime Support** - Works on Node.js, Bun, Deno, Cloudflare Workers, and edge runtimes
- 🎯 **Context-Based API** - Access session via `c.get('uvbSession')`
- 🛡️ **MFA Factor Guards** - Require specific authentication factors per route
- 👤 **Resource Ownership** - Verify users own resources they're accessing
- ⚡ **Edge-Ready** - Optimized for Cloudflare Workers and edge compute
- 🔧 **Fully Customizable** - Override error handlers, customize behavior
- 💪 **TypeScript First** - Complete type safety with Hono's type system

## Installation

```bash
npm install @sp-uvb/hono hono
# or
bun add @sp-uvb/hono hono
# or
deno add @sp-uvb/hono hono
```

## Quick Start

### Basic Setup

```typescript
import { Hono } from 'hono';
import { uvbAuth } from '@sp-uvb/hono';

const app = new Hono();

app.use(
  '*',
  uvbAuth({
    uvbUrl: process.env.UVB_URL!,
    tenantId: process.env.UVB_TENANT_ID!,
  })
);

app.get('/', (c) => 'Hello UVB!');

app.get('/profile', (c) => {
  const session = c.get('uvbSession');
  if (!session) {
    return c.json({ error: 'Not authenticated' }, 401);
  }
  return c.json({
    userId: session.userId,
    factors: session.factorsVerified,
  });
});

export default app;
```

### Required Authentication

```typescript
import { Hono } from 'hono';
import { uvbAuth, uvbRequire } from '@sp-uvb/hono';

const app = new Hono();

app.use(
  '*',
  uvbAuth({
    uvbUrl: process.env.UVB_URL!,
    tenantId: process.env.UVB_TENANT_ID!,
  })
);

app.get('/dashboard', uvbRequire(), (c) => {
  const session = c.get('uvbSession')!;
  return c.json({
    welcome: `Hello, user ${session.userId}!`,
    sessionId: session.sessionId,
  });
});

export default app;
```

## Configuration Options

### UvbMiddlewareOptions

```typescript
interface UvbMiddlewareOptions {
  // 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: Environment variable accessor for edge runtimes
  getEnv?: (c: Context) => { UVB_URL?: string; UVB_TENANT_ID?: string };

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

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

## Session Object

The `uvbSession` is attached to context via `c.get('uvbSession')`:

```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 { Hono } from 'hono';
import { uvbAuth, uvbRequire } from '@sp-uvb/hono';

const app = new Hono();

app.use(
  '*',
  uvbAuth({
    uvbUrl: process.env.UVB_URL!,
    tenantId: process.env.UVB_TENANT_ID!,
  })
);

app.get('/public', (c) => c.text('Anyone can access'));

app.get('/protected', uvbRequire(), (c) => {
  const session = c.get('uvbSession')!;
  return c.json({ userId: session.userId });
});

export default app;
```

### MFA Factor Requirements

#### Require All Factors

```typescript
import { Hono } from 'hono';
import { uvbAuth, uvbRequireAllFactors } from '@sp-uvb/hono';

const app = new Hono();

app.use(
  '*',
  uvbAuth({
    uvbUrl: process.env.UVB_URL!,
    tenantId: process.env.UVB_TENANT_ID!,
  })
);

app.post('/transfer', uvbRequireAllFactors(['password', 'totp', 'webauthn']), async (c) => {
  const session = c.get('uvbSession')!;
  const body = await c.req.json();
  return c.json({
    transfer: 'authorized',
    amount: body.amount,
    userId: session.userId,
  });
});

export default app;
```

#### Require Any Factor

```typescript
import { uvbRequireAnyFactor } from '@sp-uvb/hono';

app.get('/settings', uvbRequireAnyFactor(['totp', 'webauthn', 'sms']), (c) => {
  const session = c.get('uvbSession')!;
  return c.json({
    userId: session.userId,
    mfaEnabled: true,
  });
});
```

### Resource Ownership Verification

Ensure users can only access their own resources:

```typescript
import { Hono } from 'hono';
import { uvbAuth, uvbRequire, uvbRequireOwnership } from '@sp-uvb/hono';

// 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 Hono();

app.use(
  '*',
  uvbAuth({
    uvbUrl: process.env.UVB_URL!,
    tenantId: process.env.UVB_TENANT_ID!,
    required: true,
  })
);

app.delete(
  '/posts/:id',
  uvbRequireOwnership({
    getUserId: async (c) => {
      const postId = c.req.param('id');
      const post = posts.get(postId);
      if (!post) throw new Error('Post not found');
      return post.authorId;
    },
  }),
  (c) => {
    const postId = c.req.param('id');
    posts.delete(postId);
    return c.json({ deleted: postId });
  }
);

export default app;
```

## Session Management Routes

Add built-in session management endpoints:

```typescript
import { Hono } from 'hono';
import { uvbAuth, uvbSessionRoutes } from '@sp-uvb/hono';

const app = new Hono();

app.use(
  '*',
  uvbAuth({
    uvbUrl: process.env.UVB_URL!,
    tenantId: process.env.UVB_TENANT_ID!,
  })
);

app.route(
  '/uvb',
  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

export default app;
```

Now you can:

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

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

## Runtime-Specific Examples

### Cloudflare Workers

```typescript
import { Hono } from 'hono';
import { uvbAuth } from '@sp-uvb/hono';

type Bindings = {
  UVB_URL: string;
  UVB_TENANT_ID: string;
};

const app = new Hono<{ Bindings: Bindings }>();

app.use(
  '*',
  uvbAuth({
    uvbUrl: '', // Will be overridden by env
    tenantId: '', // Will be overridden by env
    getEnv: (c) => ({
      UVB_URL: c.env.UVB_URL,
      UVB_TENANT_ID: c.env.UVB_TENANT_ID,
    }),
  })
);

app.get('/api/user', (c) => {
  const session = c.get('uvbSession');
  return c.json({
    authenticated: !!session,
    userId: session?.userId,
  });
});

export default app;
```

### Bun

```typescript
import { Hono } from 'hono';
import { uvbAuth, uvbRequire } from '@sp-uvb/hono';

const app = new Hono();

app.use(
  '*',
  uvbAuth({
    uvbUrl: process.env.UVB_URL!,
    tenantId: process.env.UVB_TENANT_ID!,
  })
);

app.get('/dashboard', uvbRequire(), (c) => {
  const session = c.get('uvbSession')!;
  return c.json({ userId: session.userId });
});

export default {
  port: 3000,
  fetch: app.fetch,
};
```

### Deno

```typescript
import { Hono } from 'hono';
import { uvbAuth } from '@sp-uvb/hono';

const app = new Hono();

app.use(
  '*',
  uvbAuth({
    uvbUrl: Deno.env.get('UVB_URL')!,
    tenantId: Deno.env.get('UVB_TENANT_ID')!,
  })
);

app.get('/', (c) => c.text('Hello from Deno!'));

Deno.serve(app.fetch);
```

### Node.js

```typescript
import { serve } from '@hono/node-server';
import { Hono } from 'hono';
import { uvbAuth } from '@sp-uvb/hono';

const app = new Hono();

app.use(
  '*',
  uvbAuth({
    uvbUrl: process.env.UVB_URL!,
    tenantId: process.env.UVB_TENANT_ID!,
  })
);

app.get('/', (c) => c.text('Hello from Node!'));

serve(app);
```

## Advanced Examples

### Conditional MFA Requirements

```typescript
import { Hono } from 'hono';
import { uvbAuth, uvbRequire, hasAllFactors } from '@sp-uvb/hono';

const app = new Hono();

app.use(
  '*',
  uvbAuth({
    uvbUrl: process.env.UVB_URL!,
    tenantId: process.env.UVB_TENANT_ID!,
    required: true,
  })
);

app.post('/api/transactions', uvbRequire(), async (c) => {
  const session = c.get('uvbSession')!;
  const body = await c.req.json();
  const amount = parseFloat(body.amount);

  // Require MFA for large transactions
  if (amount > 10000 && !hasAllFactors(session, ['totp', 'webauthn'])) {
    return c.json(
      {
        error: 'MFA required for large transactions',
        required: ['totp', 'webauthn'],
        verified: session.factorsVerified,
      },
      403
    );
  }

  return c.json({
    transaction: 'processed',
    amount,
    userId: session.userId,
  });
});

export default app;
```

### Custom Error Handlers

```typescript
import { Hono } from 'hono';
import { uvbAuth } from '@sp-uvb/hono';

const app = new Hono();

app.use(
  '*',
  uvbAuth({
    uvbUrl: process.env.UVB_URL!,
    tenantId: process.env.UVB_TENANT_ID!,
    required: true,
    onUnauthorized: (c) => {
      return c.json(
        {
          error: 'Authentication required',
          message: 'Please log in to continue',
          loginUrl: '/auth/login',
        },
        401
      );
    },
    onError: (c, error) => {
      console.error('UVB error:', error);
      return c.json(
        {
          error: 'Internal authentication error',
          requestId: crypto.randomUUID(),
        },
        500
      );
    },
  })
);

export default app;
```

### Path Exclusions

```typescript
import { Hono } from 'hono';
import { uvbAuth } from '@sp-uvb/hono';

const app = new Hono();

app.use(
  '*',
  uvbAuth({
    uvbUrl: process.env.UVB_URL!,
    tenantId: process.env.UVB_TENANT_ID!,
    required: true,
    excludePaths: ['/health', '/metrics', '/public', '/auth'],
  })
);

app.get('/health', (c) => c.json({ status: 'ok' }));
app.get('/public/terms', (c) => c.text('Terms of Service...'));
app.get('/dashboard', (c) => {
  const session = c.get('uvbSession')!;
  return c.json({ userId: session.userId });
});

export default app;
```

### Multiple Authentication Schemes

```typescript
import { Hono } from 'hono';
import { uvbAuth } from '@sp-uvb/hono';

const app = new Hono();

app.use(
  '*',
  uvbAuth({
    uvbUrl: process.env.UVB_URL!,
    tenantId: process.env.UVB_TENANT_ID!,
  })
);

app.get('/api/data', (c) => {
  // Try UVB session first
  const session = c.get('uvbSession');
  if (session) {
    return c.json({
      data: 'authenticated via UVB',
      userId: session.userId,
    });
  }

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

  return c.json({ error: 'Authentication required' }, 401);
});

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

export default app;
```

## Real-World Patterns

### E-commerce API

```typescript
import { Hono } from 'hono';
import { uvbAuth, uvbRequire, uvbRequireAllFactors, uvbRequireOwnership } from '@sp-uvb/hono';

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

const app = new Hono();

app.use(
  '*',
  uvbAuth({
    uvbUrl: process.env.UVB_URL!,
    tenantId: process.env.UVB_TENANT_ID!,
  })
);

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

// Cart requires authentication
app.post('/cart', uvbRequire(), async (c) => {
  const session = c.get('uvbSession')!;
  const body = await c.req.json();
  return c.json({
    cart: body,
    userId: session.userId,
  });
});

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

// View order (must be owner)
app.get(
  '/orders/:id',
  uvbRequire(),
  uvbRequireOwnership({
    getUserId: async (c) => {
      const orderId = c.req.param('id');
      const order = orders.get(orderId);
      if (!order) throw new Error('Order not found');
      return order.userId;
    },
  }),
  (c) => {
    const orderId = c.req.param('id');
    const order = orders.get(orderId);
    return c.json(order);
  }
);

export default app;
```

### Multi-Tenant SaaS

```typescript
import { Hono } from 'hono';
import { uvbAuth, uvbRequire } from '@sp-uvb/hono';

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 Hono();

app.use(
  '*',
  uvbAuth({
    uvbUrl: process.env.UVB_URL!,
    tenantId: process.env.UVB_TENANT_ID!,
    required: true,
  })
);

// Tenant-scoped data access
app.get('/api/workspace/:workspaceId', uvbRequire(), (c) => {
  const session = c.get('uvbSession')!;
  const user = users.get(session.userId);

  if (!user) {
    return c.json({ error: 'User not found' }, 404);
  }

  // Verify tenant access
  if (user.tenantId !== session.tenantId) {
    return c.json({ error: 'Forbidden' }, 403);
  }

  return c.json({
    workspaceId: c.req.param('workspaceId'),
    tenantId: user.tenantId,
    role: user.role,
  });
});

// Admin-only endpoint
app.post('/api/workspace/:workspaceId/settings', uvbRequire(), async (c) => {
  const session = c.get('uvbSession')!;
  const user = users.get(session.userId);

  if (!user || user.role !== 'admin') {
    return c.json({ error: 'Admin access required' }, 403);
  }

  return c.json({
    updated: true,
    workspaceId: c.req.param('workspaceId'),
  });
});

export default app;
```

### Social Media API

```typescript
import { Hono } from 'hono';
import { uvbAuth, uvbRequire, uvbRequireOwnership, uvbRequireAllFactors } from '@sp-uvb/hono';

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

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

const app = new Hono();

app.use(
  '*',
  uvbAuth({
    uvbUrl: process.env.UVB_URL!,
    tenantId: process.env.UVB_TENANT_ID!,
  })
);

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

// Create post (auth required)
app.post('/posts', uvbRequire(), async (c) => {
  const session = c.get('uvbSession')!;
  const body = await c.req.json();
  const postId = crypto.randomUUID();
  const post: Post = {
    id: postId,
    authorId: session.userId,
    content: body.content,
    visibility: body.visibility || 'public',
  };
  posts.set(postId, post);
  return c.json(post);
});

// Edit post (must be author)
app.patch(
  '/posts/:id',
  uvbRequireOwnership({
    getUserId: async (c) => {
      const postId = c.req.param('id');
      const post = posts.get(postId);
      if (!post) throw new Error('Post not found');
      return post.authorId;
    },
  }),
  async (c) => {
    const postId = c.req.param('id');
    const body = await c.req.json();
    const post = posts.get(postId)!;
    post.content = body.content;
    return c.json(post);
  }
);

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

export default app;
```

## Helper Functions

### Check Individual Factor

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

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

### Check All Factors

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

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

### Check Any Factor

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

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

## API Reference

### Middleware

#### `uvbAuth(options: UvbMiddlewareOptions)`

Main authentication middleware. Validates sessions and attaches to context.

#### `uvbRequire()`

Require authentication for a route. Returns 401 if not authenticated.

#### `uvbRequireFactors(options: UvbFactorOptions)`

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

#### `uvbRequireAllFactors(factors: string[])`

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

#### `uvbRequireAnyFactor(factors: string[])`

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)`

Create session management routes (GET /session, POST /logout).

#### `hasFactor(session, factor): boolean`

Check if session has specific factor.

#### `hasAllFactors(session, factors): boolean`

Check if session has all specified factors.

#### `hasAnyFactor(session, factors): boolean`

Check if session has any of the specified factors.

## Best Practices

### 1. Environment Variables

Always use environment variables for sensitive configuration:

```typescript
app.use(
  '*',
  uvbAuth({
    uvbUrl: process.env.UVB_URL!,
    tenantId: process.env.UVB_TENANT_ID!,
  })
);
```

### 2. Path Exclusions

Exclude health checks and public endpoints:

```typescript
app.use(
  '*',
  uvbAuth({
    // ...
    excludePaths: ['/health', '/metrics', '/public'],
  })
);
```

### 3. Cloudflare Workers

Use `getEnv` for environment bindings:

```typescript
app.use(
  '*',
  uvbAuth({
    uvbUrl: '',
    tenantId: '',
    getEnv: (c) => ({
      UVB_URL: c.env.UVB_URL,
      UVB_TENANT_ID: c.env.UVB_TENANT_ID,
    }),
  })
);
```

### 4. Factor Requirements

Use appropriate MFA levels for sensitive operations:

```typescript
// Low risk: basic auth
app.get('/profile', uvbRequire(), handler);

// Medium risk: any MFA
app.post('/settings', uvbRequireAnyFactor(['totp', 'webauthn']), handler);

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

## 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

### Cloudflare Workers

**Problem**: Environment variables not accessible

**Solutions**:

- Use `getEnv` option to access `c.env`
- Define bindings in `wrangler.toml`
- Use type-safe environment bindings

## 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)