Attic is a cozy space with lofty ambitions. attic.social

cloudflare deployment

dbushell.com fcbf285d 971b837e

verified
+175 -37
+2
.env.example
··· 1 + PRIVATE_COOKIE_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 2 + ORIGIN=http://127.0.0.1:5173
+6
.gitignore
··· 7 7 !src/**/*.ts 8 8 !src/**/*.svelte 9 9 !src/routes/favicon.ico 10 + !src/routes/.well-known 11 + !src/routes/.well-known/jwks.json 12 + !src/routes/oauth-client-metadata.json 10 13 !static 11 14 !static/**/*.css 12 15 !static/**/*.ico 13 16 !static/**/*.png 14 17 !static/**/*.svg 15 18 !static/**/*.txt 19 + !.env.example 16 20 !.gitignore 21 + !lex.config.ts 17 22 !package.json 18 23 !README.md 19 24 !svelte.config.js 20 25 !tsconfig.json 21 26 !vite.config.ts 27 + !wrangler.jsonc
+6
lex.config.ts
··· 1 + import { defineLexiconConfig } from "@atcute/lex-cli"; 2 + 3 + export default defineLexiconConfig({ 4 + files: ["lexicons/**/*.json"], 5 + outdir: "src/lexicons/", 6 + });
+4 -2
package.json
··· 1 1 { 2 - "name": "sveltekit-atproto", 2 + "name": "attic", 3 3 "private": true, 4 4 "version": "0.0.1", 5 5 "type": "module", ··· 14 14 }, 15 15 "devDependencies": { 16 16 "@atcute/lex-cli": "^2.5.3", 17 + "@cloudflare/workers-types": "^4.20260305.1", 17 18 "@sveltejs/adapter-auto": "^7.0.0", 19 + "@sveltejs/adapter-cloudflare": "^7.2.8", 18 20 "@sveltejs/kit": "^2.50.2", 19 21 "@sveltejs/vite-plugin-svelte": "^6.2.4", 22 + "@types/node": "^25.3.3", 20 23 "svelte": "^5.51.0", 21 24 "svelte-check": "^4.4.2", 22 25 "typescript": "^5.9.3", ··· 30 33 "@atcute/identity-resolver-node": "^1.0.3", 31 34 "@atcute/lexicons": "^1.2.9", 32 35 "@atcute/oauth-node-client": "^1.1.0", 33 - "@types/node": "^25.3.3", 34 36 "valibot": "^1.2.0" 35 37 } 36 38 }
+10 -3
src/app.d.ts
··· 1 1 import type { PrivateUserData } from "$lib/valibot.ts"; 2 2 import type { OAuthClient } from "@atcute/oauth-node-client"; 3 3 4 - // See https://svelte.dev/docs/kit/types#app.d.ts 5 - // for information about these interfaces 6 4 declare global { 7 5 namespace App { 8 6 // interface Error {} ··· 12 10 } 13 11 // interface PageData {} 14 12 // interface PageState {} 15 - // interface Platform {} 13 + interface Platform { 14 + env: { 15 + ASSETS: { 16 + fetch: typeof fetch; 17 + }; 18 + ORIGIN: string; 19 + PRIVATE_COOKIE_KEY: string; 20 + PRIVATE_KEY_JWK?: string; 21 + }; 22 + } 16 23 } 17 24 } 18 25
+1 -1
src/lexicons/index.ts
··· 1 - export * as SocialAtticActorProfile from "./types/social/attic/actor/profile.js"; 1 + export * as SocialAtticActorProfile from "./types/social/attic/actor/profile.ts";
+54 -13
src/lib/server/oauth.ts
··· 1 1 import { dev } from "$app/environment"; 2 - import { env } from "$env/dynamic/private"; 3 2 import { 4 3 OAUTH_COOKIE_PREFIX, 5 4 OAUTH_MAX_AGE, ··· 9 8 import { 10 9 CompositeDidDocumentResolver, 11 10 CompositeHandleResolver, 11 + DohJsonHandleResolver, 12 12 LocalActorResolver, 13 13 PlcDidDocumentResolver, 14 14 WebDidDocumentResolver, 15 15 WellKnownHandleResolver, 16 16 } from "@atcute/identity-resolver"; 17 17 import { NodeDnsHandleResolver } from "@atcute/identity-resolver-node"; 18 - import { OAuthClient, scope, type Store } from "@atcute/oauth-node-client"; 18 + import { 19 + type ClientAssertionPrivateJwk, 20 + OAuthClient, 21 + type OAuthClientOptions, 22 + scope, 23 + type Store, 24 + } from "@atcute/oauth-node-client"; 19 25 import type { Cookies } from "@sveltejs/kit"; 20 26 import { Buffer } from "node:buffer"; 21 27 22 28 class CookieStore<K extends string, V> implements Store<K, V> { 23 29 #cookies: Cookies; 30 + #secret: string; 24 31 #prefix = OAUTH_COOKIE_PREFIX; 25 32 #maxAge = SESSION_MAX_AGE; 26 33 27 - constructor(event: { cookies: Cookies }, options?: { maxAge?: number }) { 34 + constructor( 35 + event: { cookies: Cookies; platform: App.Platform }, 36 + options?: { maxAge?: number }, 37 + ) { 28 38 this.#cookies = event.cookies; 39 + this.#secret = event.platform.env.PRIVATE_COOKIE_KEY; 29 40 if (options?.maxAge) { 30 41 this.#maxAge = options.maxAge; 31 42 } ··· 45 56 try { 46 57 const value = await decryptText( 47 58 cookieValue, 48 - env.PRIVATE_COOKIE_KEY, 59 + this.#secret, 49 60 ); 50 61 return JSON.parse(value); 51 62 } catch { ··· 57 68 const cookieName = this.cookieName(key); 58 69 const cookieValue = await encryptText( 59 70 JSON.stringify(value), 60 - env.PRIVATE_COOKIE_KEY, 71 + this.#secret, 61 72 ); 62 73 if (cookieValue.length > 4000) { 63 74 throw new Error("too large"); ··· 90 101 } 91 102 92 103 export function createOAuthClient( 93 - event: { cookies: Cookies; locals: App.Locals }, 104 + event: { cookies: Cookies; locals: App.Locals; platform?: App.Platform }, 94 105 ): OAuthClient { 106 + if (event.platform === undefined) { 107 + throw new Error(); 108 + } 95 109 if (event.locals.oAuthClient) { 96 110 return event.locals.oAuthClient; 97 111 } 98 112 99 - // [TODO] dynamic hostname/port 100 - const redirectUri = `http://127.0.0.1:5173/oauth/callback`; 113 + const dns = dev ? new NodeDnsHandleResolver() : new DohJsonHandleResolver({ 114 + dohUrl: "https://mozilla.cloudflare-dns.com/dns-query", 115 + }); 116 + 117 + const redirect = new URL("/oauth/callback", event.platform.env.ORIGIN); 118 + const scopes = [scope.rpc({ lxm: ["app.bsky.actor.getProfile"], aud: "*" })]; 119 + let metadata: OAuthClientOptions["metadata"]; 120 + let keyset: ClientAssertionPrivateJwk[] | undefined; 121 + 122 + if (dev) { 123 + metadata = { 124 + redirect_uris: [redirect.href], 125 + scope: scopes, 126 + }; 127 + } else { 128 + metadata = { 129 + client_id: 130 + new URL("/oauth-client-metadata.json", event.platform.env.ORIGIN).href, 131 + redirect_uris: [redirect.href], 132 + jwks_uri: 133 + new URL("/.well-known/jwks.json", event.platform.env.ORIGIN).href, 134 + scope: scopes, 135 + }; 136 + if (event.platform.env.PRIVATE_KEY_JWK) { 137 + keyset = [ 138 + JSON.parse( 139 + event.platform.env.PRIVATE_KEY_JWK, 140 + ) as ClientAssertionPrivateJwk, 141 + ]; 142 + } else throw new Error(); 143 + } 101 144 102 145 const client = new OAuthClient({ 103 - metadata: { 104 - redirect_uris: [redirectUri], 105 - scope: [scope.rpc({ lxm: ["app.bsky.actor.getProfile"], aud: "*" })], 106 - }, 146 + metadata, 147 + keyset, 107 148 actorResolver: new LocalActorResolver({ 108 149 handleResolver: new CompositeHandleResolver({ 109 150 methods: { 110 - dns: new NodeDnsHandleResolver(), 151 + dns, 111 152 http: new WellKnownHandleResolver(), 112 153 }, 113 154 }),
+8 -3
src/lib/server/session.ts
··· 1 1 import { dev } from "$app/environment"; 2 - import { env } from "$env/dynamic/private"; 3 2 import { 4 3 HANDLE_COOKIE, 5 4 OAUTH_MAX_AGE, ··· 65 64 * Setup OAuth client from cookies 66 65 */ 67 66 export const restoreSession = async (event: RequestEvent): Promise<void> => { 68 - const { cookies } = event; 67 + const { cookies, platform } = event; 68 + if (platform?.env === undefined) { 69 + throw new Error(); 70 + } 69 71 const encrypted = cookies.get(SESSION_COOKIE); 70 72 if (encrypted === undefined) { 71 73 return; ··· 73 75 // Parse and validate or delete cookie 74 76 let data: PublicUserData; 75 77 try { 76 - const decrypted = await decryptText(encrypted, env.PRIVATE_COOKIE_KEY); 78 + const decrypted = await decryptText( 79 + encrypted, 80 + platform?.env.PRIVATE_COOKIE_KEY, 81 + ); 77 82 data = parsePublicUser(JSON.parse(decrypted)); 78 83 } catch { 79 84 cookies.delete(SESSION_COOKIE, { path: "/" });
+4 -2
src/routes/+page.server.ts
··· 1 + import { HANDLE_COOKIE } from "$lib/server/constants.ts"; 1 2 import { destroySession, startSession } from "$lib/server/session.ts"; 2 3 import { type Actions, fail, redirect } from "@sveltejs/kit"; 3 - 4 4 export const actions = { 5 5 logout: async (event) => { 6 6 await destroySession(event); ··· 11 11 const handle = formData.get("handle"); 12 12 let url: URL; 13 13 try { 14 + event.cookies.delete(HANDLE_COOKIE, { path: "/" }); 14 15 url = await startSession(event, String(handle ?? "")); 15 - } catch { 16 + } catch (err) { 17 + console.log(err); 16 18 return fail(400, { handle, invalid: true }); 17 19 } 18 20 redirect(303, url);
+12
src/routes/.well-known/jwks.json/+server.ts
··· 1 + import { createOAuthClient } from "$lib/server/oauth.ts"; 2 + import { error, json } from "@sveltejs/kit"; 3 + import type { RequestHandler } from "./$types.d.ts"; 4 + 5 + export const GET: RequestHandler = (event) => { 6 + try { 7 + const oAuthClient = createOAuthClient(event); 8 + return json(oAuthClient.jwks ?? { keys: [] }); 9 + } catch { 10 + error(404); 11 + } 12 + };
+1 -2
src/routes/avatar/[did]/+server.ts
··· 15 15 })); 16 16 const avatar = new URL(profile.avatar!); 17 17 return event.fetch(avatar); 18 - } catch (err) { 19 - console.log(err); 18 + } catch { 20 19 error(404); 21 20 } 22 21 };
+13 -3
src/routes/favicon.ico/+server.ts
··· 1 + import { error } from "@sveltejs/kit"; 1 2 import type { RequestHandler } from "./$types.d.ts"; 2 3 3 4 export const GET: RequestHandler = async (event) => { 4 - const response = await event.fetch("/images/favicon.ico"); 5 - response.headers.set("Content-Type", "image/x-icon"); 6 - return response; 5 + if (event.platform === undefined) { 6 + error(404); 7 + } 8 + const response = await event.platform.env.ASSETS.fetch( 9 + new URL("/images/favicon.ico", event.platform.env.ORIGIN), 10 + ); 11 + // if (dev) { 12 + const headers = new Headers(response.headers); 13 + headers.set("Content-Type", "image/x-icon"); 14 + return new Response(response.body, { headers }); 15 + // } 16 + // return response; 7 17 };
+12
src/routes/oauth-client-metadata.json/+server.ts
··· 1 + import { createOAuthClient } from "$lib/server/oauth.ts"; 2 + import { error, json } from "@sveltejs/kit"; 3 + import type { RequestHandler } from "./$types.d.ts"; 4 + 5 + export const GET: RequestHandler = (event) => { 6 + try { 7 + const oAuthClient = createOAuthClient(event); 8 + return json(oAuthClient.metadata); 9 + } catch { 10 + error(404); 11 + } 12 + };
+12 -6
src/routes/oauth/callback/+server.ts
··· 1 1 import { dev } from "$app/environment"; 2 - import { env } from "$env/dynamic/private"; 3 - import { HANDLE_COOKIE, SESSION_COOKIE } from "$lib/server/constants.ts"; 2 + import { 3 + HANDLE_COOKIE, 4 + SESSION_COOKIE, 5 + SESSION_MAX_AGE, 6 + } from "$lib/server/constants.ts"; 4 7 import { encryptText } from "$lib/server/crypto.ts"; 5 8 import { createOAuthClient } from "$lib/server/oauth.ts"; 6 9 import type { PublicUserData } from "$lib/valibot.ts"; ··· 12 15 import type { RequestHandler } from "./$types.d.ts"; 13 16 14 17 export const GET: RequestHandler = async (event) => { 15 - const { url, cookies } = event; 18 + const { cookies, platform, url } = event; 19 + if (platform?.env === undefined) { 20 + throw new Error(); 21 + } 16 22 17 23 const handle = cookies.get(HANDLE_COOKIE); 18 24 if (isHandle(handle) === false) { ··· 51 57 52 58 const encrypted = await encryptText( 53 59 JSON.stringify(data), 54 - env.PRIVATE_COOKIE_KEY, 60 + platform.env.PRIVATE_COOKIE_KEY, 55 61 ); 56 62 57 63 cookies.set( ··· 59 65 encrypted, 60 66 { 61 67 httpOnly: true, 62 - maxAge: 60 * 60 * 24, 68 + maxAge: SESSION_MAX_AGE, 63 69 path: "/", 64 70 sameSite: "lax", 65 71 secure: !dev, 66 72 }, 67 73 ); 68 74 69 - redirect(303, "/?success"); 75 + redirect(303, "/"); 70 76 };
+2 -1
svelte.config.js
··· 1 - import adapter from "@sveltejs/adapter-auto"; 1 + // import adapter from "@sveltejs/adapter-auto"; 2 + import adapter from "@sveltejs/adapter-cloudflare"; 2 3 3 4 /** @type {import('@sveltejs/kit').Config} */ 4 5 const config = {
+1 -1
tsconfig.json
··· 11 11 "sourceMap": true, 12 12 "strict": true, 13 13 "moduleResolution": "bundler", 14 - "types": ["@atcute/bluesky"], 14 + "types": [], 15 15 }, 16 16 }
+27
wrangler.jsonc
··· 1 + { 2 + "name": "attic", 3 + "main": ".svelte-kit/cloudflare/_worker.js", 4 + "compatibility_flags": ["nodejs_compat"], 5 + "compatibility_date": "2026-03-05", 6 + "workers_dev": false, 7 + "preview_urls": false, 8 + "vars": { 9 + "ORIGIN": "https://attic.social", 10 + }, 11 + "assets": { 12 + "binding": "ASSETS", 13 + "directory": ".svelte-kit/cloudflare", 14 + }, 15 + "routes": [ 16 + { 17 + "pattern": "attic.social", 18 + "custom_domain": true, 19 + }, 20 + ], 21 + "observability": { 22 + "logs": { 23 + "enabled": true, 24 + "invocation_logs": false, 25 + }, 26 + }, 27 + }