[READ-ONLY] Mirror of https://github.com/flo-bit/svelte-cloudflare-statusphere. statusphere.atmo.tools
atproto cloudflare-workers oauth svelte
0

Configure Feed

Select the types of activity you want to include in your feed.

svelte-cloudflare-statusphere / AGENT_SETUP_REFERENCE.md
12 kB 454 lines
1# Add AT Protocol OAuth to SvelteKit + Cloudflare Workers 2 3You are adding AT Protocol OAuth authentication to an existing SvelteKit project deployed on Cloudflare Workers. This uses server-side OAuth with `@atcute/oauth-node-client`, Cloudflare KV for session storage, and SvelteKit remote functions. 4 5## Prerequisites 6 7The project must already use: 8- SvelteKit with `@sveltejs/adapter-cloudflare` 9- A `wrangler.jsonc` (or `wrangler.toml`) config 10 11## Step 0: Ask the user 12 13Before making any changes, ask the user these questions: 14 151. **UI**: Should I add a login UI? 16 - **`foxui`** — Use `@foxui/social` login modal (polished, recommended) 17 - **`basic`** — Simple login/logout page at `/user` route (uses Tailwind if available) 18 - **`none`** — Backend only, no UI (you'll build your own) 19 202. **Collections**: What AT Protocol collections should your app write to? (e.g. `xyz.statusphere.status`, `app.bsky.feed.like`). Leave empty for read-only. 21 223. **Blobs**: Does the app need to upload blobs (images, video)? If yes, what types? (e.g. `image/*`, `video/*`) 23 24Use the answers to customize `settings.ts` (marked with `CUSTOMIZE` below) and choose which UI dependencies/files to create. 25 26## Step 1: Install dependencies 27 28Always install: 29 30```sh 31pnpm add valibot 32pnpm add -D @atcute/oauth-node-client @atcute/identity-resolver @atcute/lexicons @atcute/client @atcute/tid @cloudflare/workers-types tsx @atcute/atproto @atcute/bluesky 33``` 34 35If UI choice is `foxui`: 36 37```sh 38pnpm add @foxui/social @foxui/core 39``` 40 41## Step 2: Create files 42 43Create all of the following files. These go into `src/lib/atproto/` and `src/routes/(oauth)/`. 44 45### `src/lib/atproto/settings.ts` 46 47Fill in `collections` and `blobs` from the user's answers. If no collections were specified, use an empty array. 48 49```ts 50import { dev } from '$app/environment'; 51 52type Permissions = { 53 collections: readonly string[]; 54 rpc: Record<string, string | string[]>; 55 blobs: readonly string[]; 56}; 57 58export const permissions = { 59 // CUSTOMIZE: add the user's collections 60 collections: [], 61 62 // CUSTOMIZE: add any authenticated RPC requests needed 63 rpc: {}, 64 65 // CUSTOMIZE: add blob types if the user needs uploads (e.g. ['image/*']) 66 blobs: [] 67} as const satisfies Permissions; 68 69type ExtractCollectionBase<T extends string> = T extends `${infer Base}?${string}` ? Base : T; 70 71export type AllowedCollection = ExtractCollectionBase<(typeof permissions.collections)[number]>; 72 73// PDS to use for signup (change to preferred PDS) 74const devPDS = 'https://bsky.social/'; 75const prodPDS = 'https://bsky.social/'; 76export const signUpPDS = dev ? devPDS : prodPDS; 77 78export const REDIRECT_PATH = '/oauth/callback'; 79 80export const DOH_RESOLVER = 'https://mozilla.cloudflare-dns.com/dns-query'; 81``` 82 83### `src/lib/atproto/metadata.ts` 84 85<!-- FILE: src/lib/atproto/metadata.ts --> 86 87### `src/lib/atproto/auth.svelte.ts` 88 89<!-- FILE: src/lib/atproto/auth.svelte.ts --> 90 91### `src/lib/atproto/methods.ts` 92 93<!-- FILE: src/lib/atproto/methods.ts --> 94 95### `src/lib/atproto/index.ts` 96 97<!-- FILE: src/lib/atproto/index.ts --> 98 99### `src/lib/atproto/server/signed-cookie.ts` 100 101<!-- FILE: src/lib/atproto/server/signed-cookie.ts --> 102 103### `src/lib/atproto/server/kv-store.ts` 104 105<!-- FILE: src/lib/atproto/server/kv-store.ts --> 106 107### `src/lib/atproto/server/oauth.ts` 108 109<!-- FILE: src/lib/atproto/server/oauth.ts --> 110 111### `src/lib/atproto/server/oauth.remote.ts` 112 113<!-- FILE: src/lib/atproto/server/oauth.remote.ts --> 114 115### `src/lib/atproto/server/repo.remote.ts` 116 117<!-- FILE: src/lib/atproto/server/repo.remote.ts --> 118 119### `src/lib/atproto/server/session.ts` 120 121<!-- FILE: src/lib/atproto/server/session.ts --> 122 123### `src/lib/atproto/server/profile.ts` 124 125<!-- FILE: src/lib/atproto/server/profile.ts --> 126 127### `src/lib/atproto/scripts/generate-key.ts` 128 129<!-- FILE: src/lib/atproto/scripts/generate-key.ts --> 130 131### `src/lib/atproto/scripts/generate-secret.ts` 132 133<!-- FILE: src/lib/atproto/scripts/generate-secret.ts --> 134 135### `src/lib/atproto/scripts/setup-dev.ts` 136 137<!-- FILE: src/lib/atproto/scripts/setup-dev.ts --> 138 139### `src/routes/(oauth)/oauth/callback/+server.ts` 140 141<!-- FILE: src/routes/(oauth)/oauth/callback/+server.ts --> 142 143### `src/routes/(oauth)/oauth/jwks.json/+server.ts` 144 145<!-- FILE: src/routes/(oauth)/oauth/jwks.json/+server.ts --> 146 147### `src/routes/(oauth)/oauth-client-metadata.json/+server.ts` 148 149<!-- FILE: src/routes/(oauth)/oauth-client-metadata.json/+server.ts --> 150 151### `.env.example` 152 153<!-- FILE: .env.example --> 154 155## Step 3: Modify existing files 156 157### `src/app.d.ts` 158 159Add these to the existing `App` namespace. Merge with any existing `Locals` or `Platform` fields — do not remove existing fields. 160 161```ts 162import type { OAuthSession } from '@atcute/oauth-node-client'; 163import type { Client } from '@atcute/client'; 164import type { Did } from '@atcute/lexicons'; 165``` 166 167Add to `App.Locals`: 168 169```ts 170session: OAuthSession | null; 171client: Client | null; 172did: Did | null; 173``` 174 175Add to `App.Platform`: 176 177```ts 178env: { 179 OAUTH_SESSIONS: KVNamespace; 180 OAUTH_STATES: KVNamespace; 181 CLIENT_ASSERTION_KEY: string; 182 COOKIE_SECRET: string; 183 OAUTH_PUBLIC_URL: string; 184 PROFILE_CACHE?: KVNamespace; 185}; 186``` 187 188Add at the bottom of the file (for lexicon type augmentation): 189 190```ts 191import type {} from '@atcute/atproto'; 192import type {} from '@atcute/bluesky'; 193``` 194 195### `src/hooks.server.ts` 196 197Add session restoration. If the file already has a `handle` export, wrap both in `sequence()` from `@sveltejs/kit`. 198 199```ts 200import type { Handle } from '@sveltejs/kit'; 201import { restoreSession } from '$lib/atproto/server/session'; 202 203const atprotoHandle: Handle = async ({ event, resolve }) => { 204 const { session, client, did } = await restoreSession( 205 event.cookies, event.platform?.env 206 ); 207 event.locals.session = session; 208 event.locals.client = client; 209 event.locals.did = did; 210 return resolve(event); 211}; 212``` 213 214If no existing hooks: `export const handle = atprotoHandle;` 215 216If existing hooks: `export const handle = sequence(existingHandle, atprotoHandle);` (import `sequence` from `@sveltejs/kit`) 217 218### `src/routes/+layout.server.ts` 219 220Add profile loading. Merge with any existing load function. 221 222```ts 223import type { LayoutServerLoad } from './$types'; 224import { loadProfile } from '$lib/atproto/server/profile'; 225 226export const load: LayoutServerLoad = async ({ locals, platform }) => { 227 if (!locals.did) return { did: null, profile: null }; 228 const profile = await loadProfile(locals.did, platform?.env?.PROFILE_CACHE); 229 return { did: locals.did, profile }; 230}; 231``` 232 233If a load function already exists, merge the profile data into its return value. 234 235### `src/routes/+layout.svelte` (foxui only) 236 237Only if the user chose `foxui`. Add the login modal to the existing layout: 238 239```svelte 240<script lang="ts"> 241 import { AtprotoLoginModal } from '@foxui/social'; 242 import { login, signup } from '$lib/atproto'; 243</script> 244 245<!-- keep existing layout content, add this at the bottom: --> 246<AtprotoLoginModal 247 login={async (handle) => { 248 await login(handle); 249 return true; 250 }} 251 signup={async () => { 252 signup(); 253 return true; 254 }} 255/> 256``` 257 258To show the modal from anywhere, use `@foxui/social` state and `@foxui/core` components: 259 260```svelte 261<script lang="ts"> 262 import { Button } from '@foxui/core'; 263 import { atProtoLoginModalState } from '@foxui/social'; 264 import { user, logout } from '$lib/atproto'; 265</script> 266 267{#if user.isLoggedIn} 268 <p>Signed in as {user.profile?.handle ?? user.did}</p> 269 <Button onclick={() => logout()}>Sign Out</Button> 270{:else} 271 <Button onclick={() => atProtoLoginModalState.show()}>Sign In</Button> 272{/if} 273``` 274 275`@foxui/core` also exports `Avatar`, `Input`, and other UI primitives you can use. 276 277### `src/routes/user/+page.svelte` (basic only) 278 279Only if the user chose `basic`. Create this file: 280 281```svelte 282<script lang="ts"> 283 import { user, login, logout } from '$lib/atproto'; 284 285 let handle = $state(''); 286 let error = $state(''); 287 let loading = $state(false); 288 289 async function handleLogin() { 290 if (!handle.trim()) return; 291 loading = true; 292 error = ''; 293 try { 294 await login(handle); 295 } catch (e) { 296 error = e instanceof Error ? e.message : 'Login failed'; 297 loading = false; 298 } 299 } 300</script> 301 302<div class="mx-auto max-w-sm p-8"> 303 {#if user.isLoggedIn} 304 <p class="mb-4">Signed in as <strong>{user.profile?.handle ?? user.did}</strong></p> 305 <button 306 class="rounded bg-red-600 px-4 py-2 text-white hover:bg-red-700" 307 onclick={() => logout()} 308 > 309 Sign Out 310 </button> 311 {:else} 312 <h1 class="mb-4 text-xl font-bold">Sign in</h1> 313 <form onsubmit={handleLogin} class="flex flex-col gap-3"> 314 <input 315 type="text" 316 bind:value={handle} 317 placeholder="handle.bsky.social" 318 class="rounded border px-3 py-2" 319 disabled={loading} 320 /> 321 {#if error} 322 <p class="text-sm text-red-600">{error}</p> 323 {/if} 324 <button 325 type="submit" 326 class="rounded bg-blue-600 px-4 py-2 text-white hover:bg-blue-700 disabled:opacity-50" 327 disabled={loading || !handle.trim()} 328 > 329 {loading ? 'Signing in...' : 'Sign In'} 330 </button> 331 </form> 332 {/if} 333</div> 334``` 335 336If the project does not use Tailwind, replace the Tailwind classes with plain inline styles. 337 338### `svelte.config.js` 339 340Add `remoteFunctions: true` inside `kit.experimental`: 341 342```js 343kit: { 344 adapter: adapter(), 345 experimental: { 346 remoteFunctions: true 347 } 348} 349``` 350 351If `experimental` already exists, merge into it. Do not remove other experimental flags. 352 353### `vite.config.ts` 354 355Add dev server config for loopback OAuth: 356 357```ts 358server: { 359 host: '127.0.0.1', 360 port: 5183 361} 362``` 363 364Add this inside `defineConfig()`. Do not remove existing plugins or config. 365 366### `wrangler.jsonc` 367 368Add or merge these fields: 369 370- Add `"nodejs_compat_v2"` to `compatibility_flags` (create the array if it doesn't exist) 371- Add `"OAUTH_PUBLIC_URL": "https://your-domain.com"` to `vars` (create `vars` if needed) 372- Add KV namespace placeholders to `kv_namespaces`: 373 374```jsonc 375{ "binding": "OAUTH_SESSIONS", "id": "TODO" }, 376{ "binding": "OAUTH_STATES", "id": "TODO" } 377``` 378 379Do not remove existing bindings or vars. 380 381### `tsconfig.json` 382 383Add `"@cloudflare/workers-types"` to `compilerOptions.types`. Create the `types` array if it doesn't exist. 384 385### `package.json` 386 387Add these to the `scripts` section: 388 389```json 390"env:generate-key": "npx tsx src/lib/atproto/scripts/generate-key.ts", 391"env:generate-secret": "npx tsx src/lib/atproto/scripts/generate-secret.ts", 392"env:setup-dev": "npx tsx src/lib/atproto/scripts/setup-dev.ts" 393``` 394 395### `.gitignore` 396 397Ensure these lines are present: 398 399``` 400.env 401.env.* 402!.env.example 403``` 404 405## Step 4: Run setup and verify 406 4071. Run `pnpm env:setup-dev` to generate secrets in `.env` 4082. Run `pnpm dev` to start the dev server 4093. Verify it starts on `http://127.0.0.1:5183` 4104. Tell the user: 411 - Dev mode uses a loopback client (no keys needed) 412 - For production: create KV namespaces with `npx wrangler kv namespace create OAUTH_SESSIONS` and `OAUTH_STATES`, update the IDs in `wrangler.jsonc`, set `OAUTH_PUBLIC_URL` to their domain, and run `npx wrangler secret put CLIENT_ASSERTION_KEY` / `COOKIE_SECRET` with values from `pnpm env:generate-key` / `pnpm env:generate-secret` 413 414## Usage examples 415 416### Login / Logout 417 418```svelte 419<script lang="ts"> 420 import { user, login, logout } from '$lib/atproto'; 421</script> 422 423{#if user.isLoggedIn} 424 <p>Signed in as {user.did}</p> 425 <button onclick={() => logout()}>Sign Out</button> 426{:else} 427 <button onclick={() => login('user.bsky.social')}>Sign In</button> 428{/if} 429``` 430 431### Write operations 432 433```ts 434import { putRecord, deleteRecord, uploadBlob, createTID } from '$lib/atproto'; 435 436await putRecord({ 437 collection: 'your.collection.name', 438 rkey: createTID(), 439 record: { text: 'hello', createdAt: new Date().toISOString() } 440}); 441 442await deleteRecord({ collection: 'your.collection.name', rkey: 'some-key' }); 443 444const blob = await uploadBlob({ blob: file }); 445``` 446 447### Read operations (no auth needed) 448 449```ts 450import { listRecords, getRecord, getDetailedProfile } from '$lib/atproto'; 451 452const records = await listRecords({ did: 'did:plc:...', collection: 'your.collection.name' }); 453const profile = await getDetailedProfile({ did: 'did:plc:...' }); 454```