From bf072ae04baf669e6395e663c46bb61d26bde844 Mon Sep 17 00:00:00 2001 From: Business Goose Date: Wed, 23 Sep 2026 10:05:42 -0700 Subject: [PATCH] auth: support the atproto loopback client for local development Logging in locally required a tunnel, because the deployed client publishes its metadata for the authorization server to fetch, and that server cannot reach localhost. The spec's localhost exception removes the need: with a client_id of http://localhost?...the server synthesises the metadata from the client_id's own query parameters, so nothing is published and no signing key is used. getOAuthClient now picks between the public loopback client and the confidential deployed client from PUBLIC_BASE_URL alone. The redirect URI must be on 127.0.0.1/[::1] while the client_id stays http://localhost, so local docs now say to browse 127.0.0.1 rather than localhost. Verified against bsky.social: the login endpoint returns a loopback client_id and its sign-in page renders for it. --- .dev.vars.example | 23 ++++ API_USAGE.md | 4 +- AUTH_SETUP.md | 4 +- CLOUDFLARE_DEPLOYMENT.md | 23 ++-- IMPLEMENTATION_NOTES.md | 4 +- LOCAL_DEVELOPMENT.md | 223 ++++++++----------------------- PROJECT_SUMMARY.md | 2 +- README.md | 12 +- STATUS.md | 20 ++- src/lib/server/auth.ts | 145 +++++++++++++------- src/routes/auth/login/+server.ts | 3 + 11 files changed, 214 insertions(+), 249 deletions(-) create mode 100644 .dev.vars.example diff --git a/.dev.vars.example b/.dev.vars.example new file mode 100644 index 0000000..d7fc1dd --- /dev/null +++ b/.dev.vars.example @@ -0,0 +1,23 @@ +# Local development variables for `wrangler pages dev` / `wrangler dev`. +# Copy this file to `.dev.vars` (gitignored) and fill in your own values. +# +# PUBLIC_BASE_URL is the origin the app is served from. On a loopback origin the +# app uses the atproto localhost exception and logging in works without any +# tunnel: the authorization server synthesises the client metadata from the +# client_id itself, so nothing has to be published and no signing key is needed. +# +# Point it at a loopback origin, and open the app on that same origin: +# +# http://127.0.0.1:8788 full local login. Use 127.0.0.1, not localhost: the +# spec only allows 127.0.0.1/[::1] redirect URIs. +# https://.ngrok.app exercises the deployed (confidential) client config +# locally, which does publish metadata over HTTPS. +# See LOCAL_DEVELOPMENT.md. +# +PUBLIC_BASE_URL=http://127.0.0.1:8788 + +SESSION_SECRET=change-this-to-a-random-secret + +# Only needed away from loopback, where the client signs its own requests +# (`npm run setup:key`). +# PRIVATE_KEY_JWK={"kty":"...generated by npm run setup:key..."} diff --git a/API_USAGE.md b/API_USAGE.md index 6d2b9fe..45a4d47 100644 --- a/API_USAGE.md +++ b/API_USAGE.md @@ -152,12 +152,12 @@ curl -X POST http://localhost:5173/api/games/3l4k5j6h7g8f9/pass \ ## OAuth Metadata Endpoints ### Client Metadata -**GET** `/oauth/client-metadata.json` +**GET** `/oauth/v2/client-metadata.json` Returns OAuth client metadata for discovery. ```bash -curl http://localhost:5173/oauth/client-metadata.json +curl http://localhost:5173/oauth/v2/client-metadata.json ``` --- diff --git a/AUTH_SETUP.md b/AUTH_SETUP.md index f35ef5f..3e1a347 100644 --- a/AUTH_SETUP.md +++ b/AUTH_SETUP.md @@ -117,7 +117,7 @@ Properly signs out the user: The following endpoints are required for OAuth discovery: -### `/oauth/client-metadata.json` +### `/oauth/v2/client-metadata.json` Serves the OAuth client metadata including: - `client_id` - Identifies this application @@ -259,7 +259,7 @@ curl -X POST http://localhost:5173/auth/login \ -i # Check metadata -curl http://localhost:5173/oauth/client-metadata.json | jq +curl http://localhost:5173/oauth/v2/client-metadata.json | jq # Check JWKS curl http://localhost:5173/jwks.json | jq diff --git a/CLOUDFLARE_DEPLOYMENT.md b/CLOUDFLARE_DEPLOYMENT.md index c3e674a..ae76864 100644 --- a/CLOUDFLARE_DEPLOYMENT.md +++ b/CLOUDFLARE_DEPLOYMENT.md @@ -96,10 +96,14 @@ PUBLIC_BASE_URL = "https://your-app-name.pages.dev" # Update with your actual U Create `.dev.vars` file for local development (already in .gitignore): ``` SESSION_SECRET=your-local-secret -PRIVATE_KEY_JWK={"kty":"EC",...} -PUBLIC_BASE_URL=http://localhost:8788 +PUBLIC_BASE_URL=http://127.0.0.1:8788 ``` +Logging in on a loopback origin uses the atproto localhost exception, so +`PRIVATE_KEY_JWK` is not needed for local development — only for the deployed +client. Use `127.0.0.1` rather than `localhost`: the spec allows redirect URIs +only on `127.0.0.1` or `[::1]`. See `LOCAL_DEVELOPMENT.md`. + ## Step 8: Build and Deploy ### Option A: Direct Deployment with Wrangler @@ -135,12 +139,9 @@ npx wrangler pages deploy .svelte-kit/cloudflare --project-name=atprotogo To test locally with Cloudflare's runtime: ```bash npm run build -npx wrangler pages dev .svelte-kit/cloudflare \ - --compatibility-date=2024-01-01 \ - --d1=DB \ - --kv=SESSIONS_KV \ - --kv=STATES_KV +npx wrangler pages dev --port 8788 ``` +The D1 and KV bindings come from `wrangler.toml`, so they need no flags. ## Verification Checklist @@ -166,9 +167,11 @@ After deployment, verify: - Check KV namespaces exist: `npx wrangler kv:namespace list` ### OAuth errors -- Verify `PUBLIC_BASE_URL` is set correctly -- Check `SESSION_SECRET` and `PRIVATE_KEY_JWK` are set -- Ensure OAuth callback URL matches the deployed URL +- Verify `PUBLIC_BASE_URL` is set correctly: it is both the origin users are + sent to and the deployed OAuth `client_id` +- Check `SESSION_SECRET` and `PRIVATE_KEY_JWK` are set (deployed only) +- Locally, browse `127.0.0.1` rather than `localhost`, so the callback lands on + the same origin that holds the session cookie ### Image generation fails - Check that `@cf-wasm/resvg` is working correctly diff --git a/IMPLEMENTATION_NOTES.md b/IMPLEMENTATION_NOTES.md index 6d04ae2..0a73fa7 100644 --- a/IMPLEMENTATION_NOTES.md +++ b/IMPLEMENTATION_NOTES.md @@ -62,13 +62,13 @@ The Wgo.js library is integrated for: import { NodeOAuthClient } from '@atproto/oauth-client-node'; const client = await NodeOAuthClient.fromClientId({ - clientId: `${PUBLIC_BASE_URL}/oauth/client-metadata.json`, + clientId: `${PUBLIC_BASE_URL}/oauth/v2/client-metadata.json`, stateStore: new StateStore(), // Implement state persistence sessionStore: new SessionStore(), // Implement session persistence }); ``` -2. Update `oauth/client-metadata.json` with production URLs +2. Update `oauth/v2/client-metadata.json` with production URLs 3. Implement proper session encryption (currently using base64 encoding) 4. Handle OAuth flows: - Login initiation with `client.authorize(handle)` diff --git a/LOCAL_DEVELOPMENT.md b/LOCAL_DEVELOPMENT.md index d9ae6ff..33ebd92 100644 --- a/LOCAL_DEVELOPMENT.md +++ b/LOCAL_DEVELOPMENT.md @@ -1,190 +1,73 @@ # Local Development with OAuth -AT Protocol OAuth requires HTTPS URLs for security. For local development, you have two options: +Logging in locally works on a loopback origin — no tunnel, no signing key, no +published metadata. That is the atproto spec's localhost exception: when the +`client_id` is `http://localhost?...`, the authorization server synthesises the +client metadata from the client_id's own query parameters instead of fetching a +metadata document. -## Option 1: Use ngrok (Recommended) +Two things to get right: -### 1. Install ngrok +**1. Open the app on `127.0.0.1`, not `localhost`.** The spec allows the +`client_id` to be `http://localhost` but requires redirect URIs to be on +`127.0.0.1` or `[::1]`, so the app builds its redirect as +`http://127.0.0.1:/auth/callback`. Browse the same origin, otherwise the +session cookie is set on a host you are not on. -```bash -# Using Homebrew (macOS) -brew install ngrok - -# Or download from https://ngrok.com/download -``` - -### 2. Start your dev server - -```bash -npm run dev -``` - -### 3. In a new terminal, start ngrok - -```bash -ngrok http 5173 -``` - -You'll see output like: -``` -Forwarding https://abc123.ngrok-free.app -> http://localhost:5173 -``` - -### 4. Update your .env file - -Copy the HTTPS URL from ngrok and update `.env`: - -```bash -PUBLIC_BASE_URL=https://abc123.ngrok-free.app -``` - -### 5. Restart your dev server +**2. Point each dev server's own env file at that origin.** Both are gitignored, +so a stale value is easy to miss; in particular, if either still points at the +deployed URL, clicking Login hands you to *production* and the session lands on +the wrong origin. -Stop and restart the dev server to pick up the new URL: +| Dev server | Reads | Start | `PUBLIC_BASE_URL` | +| --- | --- | --- | --- | +| `wrangler pages dev` (port 8788) | `.dev.vars` | `npx wrangler pages dev --port 8788` | `http://127.0.0.1:8788` | +| `npm run dev` (Vite, port 5173) | `.env` | `npm run dev` | `http://127.0.0.1:5173` | -```bash -# Ctrl+C to stop, then: -npm run dev -``` +`wrangler pages dev` serves the built worker, so run `npm run build` first (or +`npm run watch`); it also provides the D1 and KV bindings that the Vite dev +server does not have. -### 6. Access your app via the ngrok URL +## Loopback client vs. deployed client -Open `https://abc123.ngrok-free.app` in your browser and try logging in! +| | Loopback | Deployed | +| --- | --- | --- | +| Client type | public (`token_endpoint_auth_method: none`) | confidential (`private_key_jwt`) | +| `client_id` | `http://localhost?scope=...&redirect_uri=...`, built by the library at startup | `/oauth/v2/client-metadata.json` | +| Metadata | synthesised by the authorization server | published by this app, fetched by the authorization server | +| `PRIVATE_KEY_JWK` | unused | required | -**Note:** The ngrok URL changes each time you restart it (unless you have a paid account). You'll need to update `.env` each time. +`getOAuthClient` picks between the two from `PUBLIC_BASE_URL` alone. ---- +## Testing the deployed client configuration locally -## Option 2: Local HTTPS with mkcert - -This creates a permanent local HTTPS setup. - -### 1. Install mkcert +The deployed client publishes its metadata and signs its requests, so the +authorization server has to be able to reach it over HTTPS. To exercise that +path locally, tunnel the dev server and use the tunnel origin in the env file +(this is the only reason a tunnel is needed): ```bash -# macOS -brew install mkcert - -# Then install the local CA -mkcert -install +npx wrangler pages dev --port 8788 +npx ngrok http 8788 # https://abc123.ngrok-free.app +# .dev.vars: PUBLIC_BASE_URL=https://abc123.ngrok-free.app ``` -### 2. Generate certificates +The free ngrok URL changes on every restart, so update the env file and restart +the dev server each time. `PRIVATE_KEY_JWK` must be set (`npm run setup:key`). -```bash -cd /Users/dan/Documents/Coding/atprotogo -mkdir -p .cert -mkcert -key-file .cert/key.pem -cert-file .cert/cert.pem localhost -``` - -### 3. Update vite.config.ts - -```typescript -import { sveltekit } from '@sveltejs/kit/vite'; -import { defineConfig } from 'vite'; -import fs from 'fs'; - -export default defineConfig({ - plugins: [sveltekit()], - server: { - port: 5173, - https: { - key: fs.readFileSync('.cert/key.pem'), - cert: fs.readFileSync('.cert/cert.pem'), - }, - }, -}); -``` - -### 4. Update .env +## Troubleshooting -```bash -PUBLIC_BASE_URL=https://localhost:5173 -``` +- **You end up on the production site when clicking Login.** `PUBLIC_BASE_URL` + in the env file the running dev server reads still points at production. +- **`Missing sec-fetch-site header` from `bsky.social`.** You opened the + `authorizationUrl` outside a browser (curl, a script). The authorization + endpoint requires normal browser navigation; the login endpoint returns the + URL for the page to navigate to rather than redirecting. +- **Logged out immediately after logging in.** The callback landed on a + different origin than the one you are browsing (`localhost` vs `127.0.0.1`). -### 5. Restart dev server +## Production -```bash -npm run dev -``` - -Access at `https://localhost:5173` - -**Note:** AT Protocol servers may still reject `localhost` URLs. In that case, use ngrok. - ---- - -## Option 3: Use a Custom Domain (Advanced) - -If you have a domain, you can: -1. Set up a subdomain (e.g., `dev.yourdomain.com`) -2. Point it to your local machine via /etc/hosts -3. Use Caddy or nginx for HTTPS reverse proxy -4. Update PUBLIC_BASE_URL to your subdomain - ---- - -## Quick Start Script (ngrok) - -Create a helper script for easy development: - -**scripts/dev-with-ngrok.sh:** -```bash -#!/bin/bash - -# Start dev server in background -npm run dev & -DEV_PID=$! - -# Wait for server to start -sleep 3 - -# Start ngrok -echo "Starting ngrok tunnel..." -ngrok http 5173 - -# Cleanup on exit -trap "kill $DEV_PID" EXIT -``` - -Make it executable: -```bash -chmod +x scripts/dev-with-ngrok.sh -``` - -Then run: -```bash -./scripts/dev-with-ngrok.sh -``` - -**Remember to update PUBLIC_BASE_URL in .env with the ngrok URL before testing!** - ---- - -## Testing Without OAuth (Alternative) - -If you want to skip OAuth for now and test the game functionality: - -1. Comment out the OAuth check in protected endpoints -2. Manually set a test session in your browser console -3. Use the API directly with a test DID - -**Not recommended for production**, but useful for testing game logic. - ---- - -## Production Deployment - -For production: -- Deploy to a hosting service (Vercel, Netlify, Railway, etc.) -- Use your production domain in PUBLIC_BASE_URL -- Ensure HTTPS is enabled (automatic on most platforms) -- Regenerate PRIVATE_KEY_JWK for production - -Example production .env: -```bash -PUBLIC_BASE_URL=https://atprotogo.yourdomain.com -PRIVATE_KEY_JWK={"kty":"EC"...} -DATABASE_PATH=/var/data/app.db -SESSION_SECRET=long-random-production-secret -``` +`PUBLIC_BASE_URL` is the deployed origin (`https://go.sky.boo`), set in +`wrangler.toml`. `PRIVATE_KEY_JWK`, `SESSION_SECRET` and the KV bindings are +Cloudflare secrets/bindings — see `CLOUDFLARE_DEPLOYMENT.md`. diff --git a/PROJECT_SUMMARY.md b/PROJECT_SUMMARY.md index 3f02eda..719b691 100644 --- a/PROJECT_SUMMARY.md +++ b/PROJECT_SUMMARY.md @@ -272,7 +272,7 @@ SQLite tables mirror the lexicon structure: 1. Properly configure NodeOAuthClient: ```typescript const client = await NodeOAuthClient.fromClientId({ - clientId: `${PUBLIC_BASE_URL}/oauth/client-metadata.json`, + clientId: `${PUBLIC_BASE_URL}/oauth/v2/client-metadata.json`, stateStore: new StateStore(), sessionStore: new SessionStore(), }); diff --git a/README.md b/README.md index ca30706..04f26f0 100644 --- a/README.md +++ b/README.md @@ -76,20 +76,24 @@ This application has been migrated from Node.js to Cloudflare Workers. See [MIGR cp .env.example .env ``` -4. Edit `.env` and set a random `SESSION_SECRET` (and generate OAuth keys if you have not already): +4. Edit `.env` and set a random `SESSION_SECRET`: ``` SESSION_SECRET=your-random-secret-here - PUBLIC_BASE_URL=http://localhost:5173 - PRIVATE_KEY_JWK={"kty":"...generated by npm run setup:key..."} + PUBLIC_BASE_URL=http://127.0.0.1:5173 ``` + On a loopback origin the app uses the atproto localhost exception, so logging + in needs no tunnel and no signing key. Open the app on `127.0.0.1` rather than + `localhost` — the spec only allows `127.0.0.1`/`[::1]` redirect URIs. See + [LOCAL_DEVELOPMENT.md](./LOCAL_DEVELOPMENT.md). + ### Running the Development Server ```bash npm run dev ``` -The application will be available at http://localhost:5173 +The application will be available at http://127.0.0.1:5173 ### Building for Production diff --git a/STATUS.md b/STATUS.md index 7aba35b..5896d10 100644 --- a/STATUS.md +++ b/STATUS.md @@ -12,7 +12,7 @@ The application builds (`npm run build`) and runs on Cloudflare Pages with a D1 - SvelteKit app built for Cloudflare Pages (`@sveltejs/adapter-cloudflare`), deployed with `npm run deploy` / `wrangler pages deploy` - D1 database (`atprotogo-db`, bound as `DB`) accessed through Kysely (`kysely-d1`) - OAuth sessions and state in KV when the `SESSIONS_KV` / `STATES_KV` bindings are configured, otherwise an in-memory store -- Bluesky OAuth via `@atcute/oauth-node-client` +- Bluesky OAuth via `@atcute/oauth-node-client`, in two configurations: a public *loopback* client for local development (the atproto localhost exception — the authorization server synthesises the metadata from the `client_id`, so nothing is published and no signing key is used) and a *confidential* client that publishes `/oauth/v2/client-metadata.json` and signs its requests with `PRIVATE_KEY_JWK` - Image generation (board, OG, reaction, nudge) with `@cf-wasm/resvg` ### Features @@ -81,19 +81,17 @@ firehose-worker/ # Durable Object Jetstream consumer # Install dependencies npm install -# Generate OAuth keys (first time only) -npm run setup:key - -# Start dev server -npm run dev -``` - -Database-backed routes need a D1 binding, so the full stack runs through Wrangler: -```bash +# Build, then serve the worker with its D1 and KV bindings npm run build -npx wrangler pages dev .svelte-kit/cloudflare --d1=DB +npx wrangler pages dev --port 8788 ``` +Logging in needs neither a tunnel nor a signing key on a loopback origin, so +`npm run setup:key` is only required to exercise the deployed (confidential) +client configuration. Open the app at `http://127.0.0.1:8788`, not +`localhost` — the spec only allows `127.0.0.1`/`[::1]` redirect URIs. See +`LOCAL_DEVELOPMENT.md`. + ### Production (Cloudflare Pages) ```bash # Build for production diff --git a/src/lib/server/auth.ts b/src/lib/server/auth.ts index 598fa37..8d6a6e9 100644 --- a/src/lib/server/auth.ts +++ b/src/lib/server/auth.ts @@ -37,6 +37,27 @@ export const OAUTH_SCOPE = */ export const OAUTH_CLIENT_METADATA_PATH = "/oauth/v2/client-metadata.json"; +/** + * Is this origin a loopback address (`localhost`, `127.0.0.0/8`, `[::1]`)? + * + * Loopback origins get a different OAuth client — the atproto spec's localhost + * exception lets such a client skip publishing metadata altogether, see + * `getOAuthClient`. + */ +export function isLoopbackOrigin(origin: string): boolean { + try { + const { hostname } = new URL(origin); + return ( + hostname === "localhost" || + hostname === "::1" || + hostname.endsWith(".localhost") || + /^127\./.test(hostname) + ); + } catch { + return false; + } +} + // Fetch-based DNS resolver for Cloudflare Workers class FetchDnsHandleResolver { async resolve(handle: string): Promise { @@ -169,12 +190,6 @@ export async function getOAuthClient(platform: App.Platform | undefined): Promis const PRIVATE_KEY_JWK = env?.PRIVATE_KEY_JWK ?? (typeof process !== 'undefined' ? process.env?.PRIVATE_KEY_JWK : undefined); const PUBLIC_BASE_URL = env?.PUBLIC_BASE_URL ?? (typeof process !== 'undefined' ? process.env?.PUBLIC_BASE_URL : undefined); - if (!PRIVATE_KEY_JWK) { - throw new Error( - "PRIVATE_KEY_JWK environment variable is required for OAuth", - ); - } - if (!PUBLIC_BASE_URL) { throw new Error( "PUBLIC_BASE_URL environment variable is required for OAuth. Check your configuration.", @@ -190,52 +205,88 @@ export async function getOAuthClient(platform: App.Platform | undefined): Promis const publicUrl = new URL(PUBLIC_BASE_URL); - // Parse the JWK directly - the new API accepts JWK objects - const privateJwk = JSON.parse(PRIVATE_KEY_JWK) as ClientAssertionPrivateJwk; - - const client = new OAuthClient({ - metadata: { - client_id: new URL(OAUTH_CLIENT_METADATA_PATH, publicUrl).href, - client_name: "Cloud Go", - redirect_uris: [new URL("/auth/callback", publicUrl).href], - scope: OAUTH_SCOPE, - jwks_uri: new URL("/jwks.json", publicUrl).href, - }, - - keyset: [privateJwk], - - actorResolver: new LocalActorResolver({ - handleResolver: new CompositeHandleResolver({ - methods: { - dns: new FetchDnsHandleResolver() as any, // Type cast for compatibility - http: new WellKnownHandleResolver(), - }, - }), - didDocumentResolver: new CompositeDidDocumentResolver({ - methods: { - plc: new CloudflarePlcDidDocumentResolver() as any, - web: new CloudflareWebDidDocumentResolver() as any, - }, - }), + const actorResolver = new LocalActorResolver({ + handleResolver: new CompositeHandleResolver({ + methods: { + dns: new FetchDnsHandleResolver() as any, // Type cast for compatibility + http: new WellKnownHandleResolver(), + }, + }), + didDocumentResolver: new CompositeDidDocumentResolver({ + methods: { + plc: new CloudflarePlcDidDocumentResolver() as any, + web: new CloudflareWebDidDocumentResolver() as any, + }, }), - - stores: platform?.env?.SESSIONS_KV && platform?.env?.STATES_KV ? { - // Production: Use KV storage - sessions: new KVStateStore(platform.env.SESSIONS_KV, 'session') as any, - states: new KVStateStore(platform.env.STATES_KV, 'state', 600) as any, // 10 min TTL - } : { - // Local development: Use MemoryStore - sessions: new MemoryStore({ maxSize: 10_000 }) as any, - states: new MemoryStore({ - maxSize: 10_000, - ttl: TEN_MINUTES_MS, - ttlAutopurge: true, - }) as any, - }, }); + const stores = + platform?.env?.SESSIONS_KV && platform?.env?.STATES_KV + ? { + // Production: Use KV storage + sessions: new KVStateStore(platform.env.SESSIONS_KV, "session") as any, + states: new KVStateStore( + platform.env.STATES_KV, + "state", + 600, + ) as any, // 10 min TTL + } + : { + // Local development: Use MemoryStore + sessions: new MemoryStore({ maxSize: 10_000 }) as any, + states: new MemoryStore({ + maxSize: 10_000, + ttl: TEN_MINUTES_MS, + ttlAutopurge: true, + }) as any, + }; + + let client: OAuthClient; + + // The atproto spec allows a `http://localhost` client_id for local + // development: the authorization server synthesises the client metadata from + // the client_id's own query parameters, so nothing has to be published and no + // signing key is involved - it is a public client, not a confidential one. + // Note the asymmetry the spec requires: the client_id is `http://localhost`, + // while the redirect URI has to be on 127.0.0.1 or [::1]. + if (isLoopbackOrigin(PUBLIC_BASE_URL)) { + client = new OAuthClient({ + metadata: { + redirect_uris: [ + `http://127.0.0.1:${publicUrl.port || "80"}/auth/callback`, + ], + scope: OAUTH_SCOPE, + }, + actorResolver, + stores, + }); + } else { + if (!PRIVATE_KEY_JWK) { + throw new Error( + "PRIVATE_KEY_JWK environment variable is required for OAuth", + ); + } + + client = new OAuthClient({ + metadata: { + client_id: new URL(OAUTH_CLIENT_METADATA_PATH, publicUrl).href, + client_name: "Cloud Go", + redirect_uris: [new URL("/auth/callback", publicUrl).href], + scope: OAUTH_SCOPE, + jwks_uri: new URL("/jwks.json", publicUrl).href, + }, + + // Parse the JWK directly - the new API accepts JWK objects + keyset: [JSON.parse(PRIVATE_KEY_JWK) as ClientAssertionPrivateJwk], + + actorResolver, + stores, + }); + } + oauthClients.set(cacheKey, client); return client; + return client; } export interface Session { diff --git a/src/routes/auth/login/+server.ts b/src/routes/auth/login/+server.ts index ae4bb0d..b6ea34c 100644 --- a/src/routes/auth/login/+server.ts +++ b/src/routes/auth/login/+server.ts @@ -55,6 +55,9 @@ export const POST: RequestHandler = async ({ request, platform }) => { return json({ authorizationUrl: url.href }); } catch (err) { console.error("Login error:", err); + if (err && typeof err === "object" && "status" in err) { + throw err; + } throw error( 500, err instanceof Error ? err.message : "Failed to initiate OAuth flow", -- knotmirror 0.0.1