@proteles/svelte
v0.1.1
Published
Svelte components and stores for Proteles authentication, plus SvelteKit server handlers. Works with Svelte 4 and 5; tokens never reach the browser.
Maintainers
Readme
@proteles/svelte
Svelte components and stores for Proteles authentication, plus the SvelteKit server handlers. Works with Svelte 4 and 5.
Tokens live only in an encrypted, httpOnly cookie set by your own server — the
browser never receives one, only the sanitized user from /api/auth/me.
Install
npm install @proteles/svelteThe five-minute setup (SvelteKit)
1. The auth routes — the whole OAuth flow, in one file:
// src/routes/api/auth/[...proteles]/+server.ts
import { createAuthHandlers } from "@proteles/svelte/server";
export const { GET, POST } = createAuthHandlers();2. The hook — puts the user on locals and guards routes:
// src/hooks.server.ts
import { createAuthHandle } from "@proteles/svelte/server";
export const handle = createAuthHandle(undefined, { publicPaths: ["/"] });3. Hand the user to the client so the first paint is correct:
// src/routes/+layout.server.ts
import { getUser } from "@proteles/svelte/server";
export const load = async (event) => ({ user: await getUser(event) });<!-- src/routes/+layout.svelte -->
<script>
import { AuthProvider } from "@proteles/svelte";
export let data;
</script>
<AuthProvider initialUser={data.user}><slot /></AuthProvider>Then use the components anywhere:
<!-- src/routes/+page.svelte -->
<script>
import { SignedIn, SignedOut, SignInButton, UserButton, getAuth } from "@proteles/svelte";
const auth = getAuth();
</script>
<SignedOut>
<SignInButton />
</SignedOut>
<SignedIn>
Hi {$auth.user?.email} <UserButton />
</SignedIn>Client API
<AuthProvider>
Wrap your app once. Creates the auth store, puts it in context, and revalidates
against /api/auth/me on mount (so a stale initialUser — e.g. after a logout
in another tab — self-corrects).
| Prop | Notes |
| --- | --- |
| initialUser | From getUser(event) in a server load. undefined = fetch on mount, null = known signed-out, an object = known signed-in. |
| basePath | BFF mount path. Defaults to PUBLIC_PROTELES_BASE_PATH or /api/auth. |
getAuth()
The auth store, from context. Call during component initialization, then use
$auth in markup:
<script>
import { getAuth } from "@proteles/svelte";
const auth = getAuth();
</script>
{#if $auth.isLoading}Loading…{:else if $auth.isAuthenticated}{$auth.user.sub}{/if}
<button on:click={() => auth.signIn({ connection: "google" })}>Google</button>The store value is { user, isLoading, isAuthenticated }; the store object also
carries signIn(opts?), signOut(opts?), reload(), and basePath. It throws
a clear error if used outside an <AuthProvider>.
Components
| Component | Renders |
| --- | --- |
| <SignedIn> | its slot when a user is signed in |
| <SignedOut> | its slot when no user is signed in |
| <Protect> | its default slot when signed in, else the fallback slot (nothing while loading) |
| <SignInButton returnTo? connection?> | a button that starts login (connection picks a social provider, e.g. "google") |
| <SignOutButton returnTo?> | a button that logs out |
| <UserButton> | the user's name/email plus a sign-out control |
<SignInButton>/<SignOutButton>/<UserButton> are unstyled by design — pass
class. The wrappers (<AuthProvider>, <SignedIn>, <SignedOut>,
<Protect>) render only their slot and declare no class prop; passing one is
accepted and silently dropped.
<SignedIn>/<SignedOut> render nothing while the first /api/auth/me is in
flight, so there's no signed-in/out flash.
Server API (@proteles/svelte/server)
| Export | Purpose |
| --- | --- |
| createAuthHandlers(config?) | { GET, POST } for src/routes/api/auth/[...proteles]/+server.ts |
| createAuthHandle(config?, { publicPaths, protect }) | A SvelteKit handle hook: sets locals.protelesUser, and guards routes when publicPaths is given |
| getUser(event, config?) | The sanitized user (no tokens) — prefers locals, falls back to the cookie |
| getSession(event, config?) | The full session including tokens — server-side only, for calling APIs as the user |
publicPaths entries match exactly or as a path-segment prefix: "/blog" covers
/blog and /blog/post but not /blogging. "/" is exact-only, so listing
the home page doesn't accidentally make the whole app public.
For typed locals, add to src/app.d.ts:
import type { PublicUser } from "@proteles/svelte/server";
declare global {
namespace App {
interface Locals {
protelesUser: PublicUser | null;
}
}
}
export {};Configuration (environment)
Server-side, read by the handlers — same names as the other Proteles BFF packages:
| Variable | Required | Default |
| --- | --- | --- |
| PROTELES_ISSUER | ✅ | |
| PROTELES_CLIENT_ID | ✅ | |
| PROTELES_CLIENT_SECRET | | (omit for a public PKCE client) |
| PROTELES_SESSION_SECRET | ✅ | base64 of 32 random bytes |
| PROTELES_REDIRECT_URI | ✅* | * or derived from PROTELES_APP_URL |
| PROTELES_APP_URL | | |
| PROTELES_SCOPES | | openid profile email offline_access |
| PROTELES_COOKIE_SECURE | | true |
| PROTELES_BASE_PATH | | /api/auth |
Client-side, only PUBLIC_PROTELES_BASE_PATH is read (and only if you moved the
routes). Generate a session secret:
node -e "console.log(require('crypto').randomBytes(32).toString('base64'))"Svelte 4 and 5
The components use export let props and slots, which both major versions
support, so one build works for either. The store implements the plain Svelte
store contract (subscribe returning an unsubscriber) — no runes required.
Develop
npm install # from the sdk/ workspace root
npm run build # svelte-package -> dist
npm test # tsx + node:testTests cover the store contract and the SvelteKit handlers, and render the real
.svelte components through the Svelte compiler + svelte/server — no bundler
and no jsdom. The full loop also runs against a live server via
sdk/scripts/verify-sveltekit.mjs (Test 17
of scripts/e2e-smoke-test.sh).
