JSPM

@sp-uvb/hono

0.1.0
    • ESM via JSPM
    • ES Module Entrypoint
    • Export Map
    • Keywords
    • License
    • Repository URL
    • TypeScript Types
    • README
    • Created
    • Published
    • Downloads 2
    • Score
      100M100P100Q23945F
    • License MIT

    Production-grade Hono middleware for Universal Verification Broker (UVB) authentication

    Package Exports

    • @sp-uvb/hono

    Readme

    @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

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

    Quick Start

    Basic Setup

    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

    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

    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'):

    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:

    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

    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

    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:

    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:

    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:

    # 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

    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

    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

    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

    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

    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

    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

    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

    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

    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

    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

    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

    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

    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

    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:

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

    2. Path Exclusions

    Exclude health checks and public endpoints:

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

    3. Cloudflare Workers

    Use getEnv for environment bindings:

    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:

    // 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: