npm package: @sp-uvb/sveltekit
Description: SvelteKit hooks and utilities for Universal Verification Broker (UVB) authentication
Author: UVB Team
Latest version: 0.1.0

# @sp-uvb/sveltekit

Production-grade SvelteKit integration for Universal Verification Broker (UVB) authentication. Provides server hooks, load function utilities, API route helpers, and Svelte stores.

## Installation

```bash
npm install @sp-uvb/sveltekit
# or
yarn add @sp-uvb/sveltekit
# or
pnpm add @sp-uvb/sveltekit
```

## Quick Start

### 1. Configure Server Hook

Create or update `src/hooks.server.ts`:

```typescript
import { createUvbHandle } from '@sp-uvb/sveltekit/hooks';
import { sequence } from '@sveltejs/kit/hooks';

export const handle = createUvbHandle({
  tenantId: 'your-tenant-id',
  uvbUrl: 'http://localhost:8080',
  cookieName: 'uvb_session',
  excludePaths: ['/login', '/register', '/api/public'],
});

// If you have other hooks, sequence them:
export const handle = sequence(createUvbHandle({ tenantId: 'your-tenant-id' }), yourOtherHook);
```

### 2. Use in Server Load Functions

```typescript
// src/routes/profile/+page.server.ts
import { requireUvbSession } from '@sp-uvb/sveltekit/server';
import type { PageServerLoad } from './$types';

export const load: PageServerLoad = async (event) => {
  const session = requireUvbSession(event);

  return {
    user: {
      id: session.userId,
      factors: session.factorsVerified,
    },
  };
};
```

### 3. Use in API Routes

```typescript
// src/routes/api/profile/+server.ts
import { requireUvbSession, requireFactors } from '@sp-uvb/sveltekit/server';
import { json } from '@sveltejs/kit';
import type { RequestHandler } from './$types';

export const GET: RequestHandler = async (event) => {
  const session = requireUvbSession(event);

  return json({
    userId: session.userId,
    tenantId: session.tenantId,
  });
};

export const DELETE: RequestHandler = async (event) => {
  // Require strong MFA for destructive operations
  const session = requireFactors(event, ['totp', 'webauthn']);

  // Perform deletion
  return json({ success: true });
};
```

### 4. Use in Svelte Components

```svelte
<!-- src/routes/+layout.svelte -->
<script lang="ts">
  import { onMount } from 'svelte'
  import { uvbSession, isAuthenticated } from '@sp-uvb/sveltekit/client'
  import type { PageData } from './$types'

  export let data: PageData

  // Initialize session from server
  onMount(() => {
    if (data.uvbSession) {
      $uvbSession = data.uvbSession
    }
  })
</script>

{#if $isAuthenticated}
  <nav>
    <a href="/profile">Profile</a>
    <span>User: {$uvbSession?.userId}</span>
  </nav>
  <slot />
{:else}
  <a href="/login">Login</a>
{/if}
```

## API Reference

### Hooks (`@sp-uvb/sveltekit/hooks`)

#### `createUvbHandle(options)`

Creates a SvelteKit handle hook for authentication.

**Options:**

```typescript
interface UvbHandleOptions {
  uvbUrl?: string; // Default: 'http://localhost:8080'
  tenantId: string; // Required: Your UVB tenant ID
  apiKey?: string; // Optional: API key for server-to-server auth
  cookieName?: string; // Default: 'uvb_session'
  excludePaths?: string[]; // Default: []
}
```

**Example:**

```typescript
import { createUvbHandle } from '@sp-uvb/sveltekit/hooks';

export const handle = createUvbHandle({
  tenantId: import.meta.env.VITE_UVB_TENANT_ID,
  uvbUrl: import.meta.env.VITE_UVB_URL,
  apiKey: import.meta.env.UVB_API_KEY,
  excludePaths: ['/login', '/register', '/api/public'],
});
```

#### `sequence(...handlers)`

Utility to combine multiple handle hooks.

```typescript
import { createUvbHandle, sequence } from '@sp-uvb/sveltekit/hooks';

const uvbHandle = createUvbHandle({ tenantId: 'my-tenant' });
const loggingHandle = async ({ event, resolve }) => {
  console.log(`Request: ${event.request.method} ${event.url.pathname}`);
  return resolve(event);
};

export const handle = sequence(uvbHandle, loggingHandle);
```

### Server Utilities (`@sp-uvb/sveltekit/server`)

#### `getUvbSession(event)`

Get session from request event (returns `undefined` if no session).

```typescript
import { getUvbSession } from '@sp-uvb/sveltekit/server';
import type { RequestHandler } from './$types';

export const GET: RequestHandler = async (event) => {
  const session = getUvbSession(event);

  if (!session) {
    return new Response('Not authenticated', { status: 401 });
  }

  return json({ userId: session.userId });
};
```

#### `requireUvbSession(event)`

Require session or throw 401 error.

```typescript
import { requireUvbSession } from '@sp-uvb/sveltekit/server';
import type { PageServerLoad } from './$types';

export const load: PageServerLoad = async (event) => {
  const session = requireUvbSession(event);
  // If we get here, user is authenticated

  return { userId: session.userId };
};
```

#### `requireFactors(event, factors)`

Require specific MFA factors or throw 403 error.

```typescript
import { requireFactors } from '@sp-uvb/sveltekit/server';
import type { RequestHandler } from './$types';

export const POST: RequestHandler = async (event) => {
  // Require both TOTP and WebAuthn
  const session = requireFactors(event, ['totp', 'webauthn']);

  // Perform admin action
  return json({ success: true });
};
```

#### `hasFactor(event, factor)`

Check if user has specific factor (returns boolean).

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

export const load: PageServerLoad = async (event) => {
  const hasTotp = hasFactor(event, 'totp');

  return { totpEnabled: hasTotp };
};
```

#### `isOwner(event, resourceUserId)`

Check if current user owns a resource.

```typescript
import { isOwner } from '@sp-uvb/sveltekit/server';

export const GET: RequestHandler = async ({ params, ...event }) => {
  const post = await getPost(params.id);

  if (!isOwner(event, post.userId)) {
    return new Response('Forbidden', { status: 403 });
  }

  return json(post);
};
```

#### `requireOwnership(event, resourceUserId)`

Require ownership or throw 403 error.

```typescript
import { requireOwnership } from '@sp-uvb/sveltekit/server';

export const DELETE: RequestHandler = async ({ params, ...event }) => {
  const post = await getPost(params.id);
  requireOwnership(event, post.userId);

  await deletePost(params.id);
  return json({ success: true });
};
```

### Client Stores (`@sp-uvb/sveltekit/client`)

#### `uvbSession`

Writable store containing the current session.

```svelte
<script lang="ts">
  import { uvbSession } from '@sp-uvb/sveltekit/client'
</script>

{#if $uvbSession}
  <p>User ID: {$uvbSession.userId}</p>
  <p>Factors: {$uvbSession.factorsVerified.join(', ')}</p>
{/if}
```

#### `isAuthenticated`

Derived store indicating if user is authenticated.

```svelte
<script lang="ts">
  import { isAuthenticated } from '@sp-uvb/sveltekit/client'
</script>

{#if $isAuthenticated}
  <a href="/dashboard">Dashboard</a>
{:else}
  <a href="/login">Login</a>
{/if}
```

#### `hasFactor(factor)`

Create a derived store checking for a specific factor.

```svelte
<script lang="ts">
  import { hasFactor } from '@sp-uvb/sveltekit/client'

  const totpEnabled = hasFactor('totp')
  const webauthnEnabled = hasFactor('webauthn')
</script>

{#if $totpEnabled}
  <span>✓ TOTP Enabled</span>
{/if}

{#if $webauthnEnabled}
  <span>✓ WebAuthn Enabled</span>
{/if}
```

#### `hasAllFactors(factors)`

Create a derived store checking if user has all specified factors.

```svelte
<script lang="ts">
  import { hasAllFactors } from '@sp-uvb/sveltekit/client'

  const hasStrongAuth = hasAllFactors(['totp', 'webauthn'])
</script>

{#if $hasStrongAuth}
  <button on:click={performSensitiveAction}>Delete Account</button>
{:else}
  <p>Enable TOTP and WebAuthn to access this feature</p>
{/if}
```

#### `hasAnyFactor(factors)`

Create a derived store checking if user has any of the specified factors.

```svelte
<script lang="ts">
  import { hasAnyFactor } from '@sp-uvb/sveltekit/client'

  const hasMfa = hasAnyFactor(['totp', 'webauthn', 'sms'])
</script>

{#if $hasMfa}
  <span class="badge">MFA Enabled</span>
{/if}
```

## Complete Examples

### Protected Page with Load Function

```typescript
// src/routes/dashboard/+page.server.ts
import { requireUvbSession } from '@sp-uvb/sveltekit/server';
import type { PageServerLoad } from './$types';

export const load: PageServerLoad = async (event) => {
  const session = requireUvbSession(event);

  // Fetch user data
  const user = await db.users.findUnique({
    where: { id: session.userId },
  });

  return {
    user,
    uvbSession: session, // Pass to client
  };
};
```

```svelte
<!-- src/routes/dashboard/+page.svelte -->
<script lang="ts">
  import { onMount } from 'svelte'
  import { uvbSession } from '@sp-uvb/sveltekit/client'
  import type { PageData } from './$types'

  export let data: PageData

  onMount(() => {
    $uvbSession = data.uvbSession
  })
</script>

<h1>Dashboard</h1>
<p>Welcome, {data.user.name}</p>
<p>User ID: {$uvbSession?.userId}</p>
```

### API Route with Factor Requirements

```typescript
// src/routes/api/admin/users/+server.ts
import { requireFactors } from '@sp-uvb/sveltekit/server';
import { json, error as svelteError } from '@sveltejs/kit';
import type { RequestHandler } from './$types';

export const GET: RequestHandler = async (event) => {
  const session = requireFactors(event, ['totp', 'webauthn']);

  const users = await db.users.findMany();

  return json({ users });
};

export const POST: RequestHandler = async (event) => {
  const session = requireFactors(event, ['totp', 'webauthn']);
  const body = await event.request.json();

  // Validate input
  if (!body.email || !body.name) {
    throw svelteError(400, 'Email and name are required');
  }

  const user = await db.users.create({
    data: {
      email: body.email,
      name: body.name,
      createdBy: session.userId,
    },
  });

  return json({ user }, { status: 201 });
};
```

### Resource Ownership Protection

```typescript
// src/routes/api/posts/[id]/+server.ts
import { requireOwnership, requireUvbSession } from '@sp-uvb/sveltekit/server';
import { json, error as svelteError } from '@sveltejs/kit';
import type { RequestHandler } from './$types';

export const GET: RequestHandler = async ({ params }) => {
  const post = await db.posts.findUnique({
    where: { id: params.id },
  });

  if (!post) {
    throw svelteError(404, 'Post not found');
  }

  return json({ post });
};

export const PATCH: RequestHandler = async ({ params, request, ...event }) => {
  const post = await db.posts.findUnique({
    where: { id: params.id },
  });

  if (!post) {
    throw svelteError(404, 'Post not found');
  }

  // Ensure user owns the post
  requireOwnership(event, post.userId);

  const body = await request.json();
  const updated = await db.posts.update({
    where: { id: params.id },
    data: body,
  });

  return json({ post: updated });
};

export const DELETE: RequestHandler = async ({ params, ...event }) => {
  const post = await db.posts.findUnique({
    where: { id: params.id },
  });

  if (!post) {
    throw svelteError(404, 'Post not found');
  }

  requireOwnership(event, post.userId);

  await db.posts.delete({
    where: { id: params.id },
  });

  return json({ success: true });
};
```

### Conditional MFA Requirements

```typescript
// src/routes/api/transfer/+server.ts
import { requireUvbSession, requireFactors } from '@sp-uvb/sveltekit/server';
import { json, error as svelteError } from '@sveltejs/kit';
import type { RequestHandler } from './$types';

export const POST: RequestHandler = async (event) => {
  const session = requireUvbSession(event);
  const body = await event.request.json();

  if (!body.amount || !body.recipient) {
    throw svelteError(400, 'Amount and recipient are required');
  }

  // Require stronger auth for larger amounts
  if (body.amount > 10000) {
    requireFactors(event, ['totp', 'webauthn']);
  } else if (body.amount > 1000) {
    requireFactors(event, ['totp']);
  }

  // Process transfer
  const transfer = await processTransfer({
    from: session.userId,
    to: body.recipient,
    amount: body.amount,
  });

  return json({ transfer });
};
```

### Form Actions with Authentication

```typescript
// src/routes/settings/+page.server.ts
import { requireUvbSession, requireFactors } from '@sp-uvb/sveltekit/server';
import { fail } from '@sveltejs/kit';
import type { Actions, PageServerLoad } from './$types';

export const load: PageServerLoad = async (event) => {
  const session = requireUvbSession(event);

  const user = await db.users.findUnique({
    where: { id: session.userId },
  });

  return { user, uvbSession: session };
};

export const actions = {
  updateProfile: async (event) => {
    const session = requireUvbSession(event);
    const formData = await event.request.formData();

    const name = formData.get('name');
    const email = formData.get('email');

    if (!name || !email) {
      return fail(400, { error: 'Name and email are required' });
    }

    await db.users.update({
      where: { id: session.userId },
      data: { name: name.toString(), email: email.toString() },
    });

    return { success: true };
  },

  deleteAccount: async (event) => {
    // Require strong MFA for account deletion
    const session = requireFactors(event, ['totp', 'webauthn']);

    await db.users.delete({
      where: { id: session.userId },
    });

    return { success: true };
  },
} satisfies Actions;
```

### Client-Side Factor Checking

```svelte
<!-- src/routes/settings/security/+page.svelte -->
<script lang="ts">
  import { hasFactor, hasAllFactors } from '@sp-uvb/sveltekit/client'
  import type { PageData } from './$types'

  export let data: PageData

  const totpEnabled = hasFactor('totp')
  const webauthnEnabled = hasFactor('webauthn')
  const strongAuth = hasAllFactors(['totp', 'webauthn'])
</script>

<h1>Security Settings</h1>

<section>
  <h2>Two-Factor Authentication</h2>

  <div class="factor">
    <h3>TOTP Authenticator</h3>
    {#if $totpEnabled}
      <span class="badge success">✓ Enabled</span>
      <form method="POST" action="?/disableTotp">
        <button type="submit">Disable</button>
      </form>
    {:else}
      <span class="badge">Not Enabled</span>
      <a href="/settings/security/totp/setup">Enable TOTP</a>
    {/if}
  </div>

  <div class="factor">
    <h3>WebAuthn (Security Key)</h3>
    {#if $webauthnEnabled}
      <span class="badge success">✓ Enabled</span>
      <form method="POST" action="?/disableWebauthn">
        <button type="submit">Manage Keys</button>
      </form>
    {:else}
      <span class="badge">Not Enabled</span>
      <a href="/settings/security/webauthn/setup">Add Security Key</a>
    {/if}
  </div>
</section>

{#if $strongAuth}
  <section class="admin-features">
    <h2>Advanced Features</h2>
    <p>You have strong authentication enabled. You can now:</p>
    <ul>
      <li><a href="/settings/api-keys">Manage API Keys</a></li>
      <li><a href="/settings/account">Delete Account</a></li>
    </ul>
  </section>
{/if}
```

### Layout with Session Initialization

```svelte
<!-- src/routes/+layout.svelte -->
<script lang="ts">
  import { onMount } from 'svelte'
  import { uvbSession } from '@sp-uvb/sveltekit/client'
  import type { LayoutData } from './$types'

  export let data: LayoutData

  onMount(() => {
    if (data.uvbSession) {
      $uvbSession = data.uvbSession
    }
  })
</script>

<slot />
```

```typescript
// src/routes/+layout.server.ts
import { getUvbSession } from '@sp-uvb/sveltekit/server';
import type { LayoutServerLoad } from './$types';

export const load: LayoutServerLoad = async (event) => {
  const session = getUvbSession(event);

  return {
    uvbSession: session, // Make available to all pages
  };
};
```

## Environment Variables

```bash
# .env
VITE_UVB_TENANT_ID=your-tenant-id
VITE_UVB_URL=http://localhost:8080

# Server-only (not prefixed with VITE_)
UVB_API_KEY=your-api-key
```

Access in code:

```typescript
// Client-side (vite env)
import { createUvbHandle } from '@sp-uvb/sveltekit/hooks';

export const handle = createUvbHandle({
  tenantId: import.meta.env.VITE_UVB_TENANT_ID,
  uvbUrl: import.meta.env.VITE_UVB_URL,
});

// Server-side (node env)
import { env } from '$env/dynamic/private';

export const handle = createUvbHandle({
  tenantId: env.UVB_TENANT_ID!,
  apiKey: env.UVB_API_KEY,
});
```

## TypeScript

Full TypeScript support with type exports:

```typescript
import type { UvbSession, UvbHandleOptions } from '@sp-uvb/sveltekit';

// Session is also available in App.Locals
declare global {
  namespace App {
    interface Locals {
      uvbSession?: UvbSession;
    }
  }
}
```

## Error Handling

All server utilities throw SvelteKit errors:

```typescript
import { requireUvbSession } from '@sp-uvb/sveltekit/server';
import { error } from '@sveltejs/kit';

export const load: PageServerLoad = async (event) => {
  try {
    const session = requireUvbSession(event);
    return { userId: session.userId };
  } catch (err) {
    // requireUvbSession throws error(401, ...)
    // This will be caught by SvelteKit's error handling
    throw err;
  }
};
```

## License

MIT