@sp-uvb/sveltekit
v0.1.0
Published
SvelteKit hooks and utilities for Universal Verification Broker (UVB) authentication
Maintainers
Readme
@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
npm install @sp-uvb/sveltekit
# or
yarn add @sp-uvb/sveltekit
# or
pnpm add @sp-uvb/sveltekitQuick Start
1. Configure Server Hook
Create or update src/hooks.server.ts:
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
// 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
// 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
<!-- 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:
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:
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.
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).
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.
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.
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).
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.
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.
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.
<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.
<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.
<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.
<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.
<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
// 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
};
};<!-- 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
// 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
// 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
// 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
// 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
<!-- 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
<!-- 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 />// 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
# .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-keyAccess in code:
// 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:
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:
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
