From fcbf285d1fc54fb1c4737deb0fa5cf232cb073d6 Mon Sep 17 00:00:00 2001 From: David Bushell Date: Thu, 05 Mar 2026 18:03:09 +0000 Subject: [PATCH] cloudflare deployment --- .env.example | 2 ++ .gitignore | 6 ++++++ lex.config.ts | 6 ++++++ package.json | 6 ++++-- svelte.config.js | 3 ++- tsconfig.json | 2 +- wrangler.jsonc | 27 +++++++++++++++++++++++++++ src/app.d.ts | 13 ++++++++++--- src/lexicons/index.ts | 2 +- src/routes/+page.server.ts | 6 ++++-- src/lib/server/oauth.ts | 67 ++++++++++++++++++++++++++++++++++++++++++++++++++++++------------- src/lib/server/session.ts | 11 ++++++++--- src/routes/favicon.ico/+server.ts | 16 +++++++++++++--- src/routes/oauth-client-metadata.json/+server.ts | 12 ++++++++++++ src/routes/.well-known/jwks.json/+server.ts | 12 ++++++++++++ src/routes/avatar/[did]/+server.ts | 3 +-- src/routes/oauth/callback/+server.ts | 18 ++++++++++++------ 17 file(s) changed, 175 insertion(s)(+), 37 deletion(s)(-) diff --git a/.env.example b/.env.example new file mode 100644 --- /dev/null +++ b/.env.example @@ -0,0 +1,2 @@ +PRIVATE_COOKIE_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +ORIGIN=http://127.0.0.1:5173 diff --git a/.gitignore b/.gitignore --- a/.gitignore +++ b/.gitignore @@ -7,15 +7,21 @@ !src/**/*.ts !src/**/*.svelte !src/routes/favicon.ico +!src/routes/.well-known +!src/routes/.well-known/jwks.json +!src/routes/oauth-client-metadata.json !static !static/**/*.css !static/**/*.ico !static/**/*.png !static/**/*.svg !static/**/*.txt +!.env.example !.gitignore +!lex.config.ts !package.json !README.md !svelte.config.js !tsconfig.json !vite.config.ts +!wrangler.jsonc diff --git a/lex.config.ts b/lex.config.ts new file mode 100644 --- /dev/null +++ b/lex.config.ts @@ -0,0 +1,6 @@ +import { defineLexiconConfig } from "@atcute/lex-cli"; + +export default defineLexiconConfig({ + files: ["lexicons/**/*.json"], + outdir: "src/lexicons/", +}); diff --git a/package.json b/package.json --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "sveltekit-atproto", + "name": "attic", "private": true, "version": "0.0.1", "type": "module", @@ -14,9 +14,12 @@ }, "devDependencies": { "@atcute/lex-cli": "^2.5.3", + "@cloudflare/workers-types": "^4.20260305.1", "@sveltejs/adapter-auto": "^7.0.0", + "@sveltejs/adapter-cloudflare": "^7.2.8", "@sveltejs/kit": "^2.50.2", "@sveltejs/vite-plugin-svelte": "^6.2.4", + "@types/node": "^25.3.3", "svelte": "^5.51.0", "svelte-check": "^4.4.2", "typescript": "^5.9.3", @@ -30,7 +33,6 @@ "@atcute/identity-resolver-node": "^1.0.3", "@atcute/lexicons": "^1.2.9", "@atcute/oauth-node-client": "^1.1.0", - "@types/node": "^25.3.3", "valibot": "^1.2.0" } } diff --git a/svelte.config.js b/svelte.config.js --- a/svelte.config.js +++ b/svelte.config.js @@ -1,4 +1,5 @@ -import adapter from "@sveltejs/adapter-auto"; +// import adapter from "@sveltejs/adapter-auto"; +import adapter from "@sveltejs/adapter-cloudflare"; /** @type {import('@sveltejs/kit').Config} */ const config = { diff --git a/tsconfig.json b/tsconfig.json --- a/tsconfig.json +++ b/tsconfig.json @@ -11,6 +11,6 @@ "sourceMap": true, "strict": true, "moduleResolution": "bundler", - "types": ["@atcute/bluesky"], + "types": [], }, } diff --git a/wrangler.jsonc b/wrangler.jsonc new file mode 100644 --- /dev/null +++ b/wrangler.jsonc @@ -0,0 +1,27 @@ +{ + "name": "attic", + "main": ".svelte-kit/cloudflare/_worker.js", + "compatibility_flags": ["nodejs_compat"], + "compatibility_date": "2026-03-05", + "workers_dev": false, + "preview_urls": false, + "vars": { + "ORIGIN": "https://attic.social", + }, + "assets": { + "binding": "ASSETS", + "directory": ".svelte-kit/cloudflare", + }, + "routes": [ + { + "pattern": "attic.social", + "custom_domain": true, + }, + ], + "observability": { + "logs": { + "enabled": true, + "invocation_logs": false, + }, + }, +} diff --git a/src/app.d.ts b/src/app.d.ts --- a/src/app.d.ts +++ b/src/app.d.ts @@ -1,8 +1,6 @@ import type { PrivateUserData } from "$lib/valibot.ts"; import type { OAuthClient } from "@atcute/oauth-node-client"; -// See https://svelte.dev/docs/kit/types#app.d.ts -// for information about these interfaces declare global { namespace App { // interface Error {} @@ -12,7 +10,16 @@ } // interface PageData {} // interface PageState {} - // interface Platform {} + interface Platform { + env: { + ASSETS: { + fetch: typeof fetch; + }; + ORIGIN: string; + PRIVATE_COOKIE_KEY: string; + PRIVATE_KEY_JWK?: string; + }; + } } } diff --git a/src/lexicons/index.ts b/src/lexicons/index.ts --- a/src/lexicons/index.ts +++ b/src/lexicons/index.ts @@ -1,1 +1,1 @@ -export * as SocialAtticActorProfile from "./types/social/attic/actor/profile.js"; +export * as SocialAtticActorProfile from "./types/social/attic/actor/profile.ts"; diff --git a/src/routes/+page.server.ts b/src/routes/+page.server.ts --- a/src/routes/+page.server.ts +++ b/src/routes/+page.server.ts @@ -1,6 +1,6 @@ +import { HANDLE_COOKIE } from "$lib/server/constants.ts"; import { destroySession, startSession } from "$lib/server/session.ts"; import { type Actions, fail, redirect } from "@sveltejs/kit"; - export const actions = { logout: async (event) => { await destroySession(event); @@ -11,8 +11,10 @@ const handle = formData.get("handle"); let url: URL; try { + event.cookies.delete(HANDLE_COOKIE, { path: "/" }); url = await startSession(event, String(handle ?? "")); - } catch { + } catch (err) { + console.log(err); return fail(400, { handle, invalid: true }); } redirect(303, url); diff --git a/src/lib/server/oauth.ts b/src/lib/server/oauth.ts --- a/src/lib/server/oauth.ts +++ b/src/lib/server/oauth.ts @@ -1,5 +1,4 @@ import { dev } from "$app/environment"; -import { env } from "$env/dynamic/private"; import { OAUTH_COOKIE_PREFIX, OAUTH_MAX_AGE, @@ -9,23 +8,35 @@ import { CompositeDidDocumentResolver, CompositeHandleResolver, + DohJsonHandleResolver, LocalActorResolver, PlcDidDocumentResolver, WebDidDocumentResolver, WellKnownHandleResolver, } from "@atcute/identity-resolver"; import { NodeDnsHandleResolver } from "@atcute/identity-resolver-node"; -import { OAuthClient, scope, type Store } from "@atcute/oauth-node-client"; +import { + type ClientAssertionPrivateJwk, + OAuthClient, + type OAuthClientOptions, + scope, + type Store, +} from "@atcute/oauth-node-client"; import type { Cookies } from "@sveltejs/kit"; import { Buffer } from "node:buffer"; class CookieStore implements Store { #cookies: Cookies; + #secret: string; #prefix = OAUTH_COOKIE_PREFIX; #maxAge = SESSION_MAX_AGE; - constructor(event: { cookies: Cookies }, options?: { maxAge?: number }) { + constructor( + event: { cookies: Cookies; platform: App.Platform }, + options?: { maxAge?: number }, + ) { this.#cookies = event.cookies; + this.#secret = event.platform.env.PRIVATE_COOKIE_KEY; if (options?.maxAge) { this.#maxAge = options.maxAge; } @@ -45,7 +56,7 @@ try { const value = await decryptText( cookieValue, - env.PRIVATE_COOKIE_KEY, + this.#secret, ); return JSON.parse(value); } catch { @@ -57,7 +68,7 @@ const cookieName = this.cookieName(key); const cookieValue = await encryptText( JSON.stringify(value), - env.PRIVATE_COOKIE_KEY, + this.#secret, ); if (cookieValue.length > 4000) { throw new Error("too large"); @@ -90,24 +101,54 @@ } export function createOAuthClient( - event: { cookies: Cookies; locals: App.Locals }, + event: { cookies: Cookies; locals: App.Locals; platform?: App.Platform }, ): OAuthClient { + if (event.platform === undefined) { + throw new Error(); + } if (event.locals.oAuthClient) { return event.locals.oAuthClient; } - // [TODO] dynamic hostname/port - const redirectUri = `http://127.0.0.1:5173/oauth/callback`; + const dns = dev ? new NodeDnsHandleResolver() : new DohJsonHandleResolver({ + dohUrl: "https://mozilla.cloudflare-dns.com/dns-query", + }); + + const redirect = new URL("/oauth/callback", event.platform.env.ORIGIN); + const scopes = [scope.rpc({ lxm: ["app.bsky.actor.getProfile"], aud: "*" })]; + let metadata: OAuthClientOptions["metadata"]; + let keyset: ClientAssertionPrivateJwk[] | undefined; + + if (dev) { + metadata = { + redirect_uris: [redirect.href], + scope: scopes, + }; + } else { + metadata = { + client_id: + new URL("/oauth-client-metadata.json", event.platform.env.ORIGIN).href, + redirect_uris: [redirect.href], + jwks_uri: + new URL("/.well-known/jwks.json", event.platform.env.ORIGIN).href, + scope: scopes, + }; + if (event.platform.env.PRIVATE_KEY_JWK) { + keyset = [ + JSON.parse( + event.platform.env.PRIVATE_KEY_JWK, + ) as ClientAssertionPrivateJwk, + ]; + } else throw new Error(); + } const client = new OAuthClient({ - metadata: { - redirect_uris: [redirectUri], - scope: [scope.rpc({ lxm: ["app.bsky.actor.getProfile"], aud: "*" })], - }, + metadata, + keyset, actorResolver: new LocalActorResolver({ handleResolver: new CompositeHandleResolver({ methods: { - dns: new NodeDnsHandleResolver(), + dns, http: new WellKnownHandleResolver(), }, }), diff --git a/src/lib/server/session.ts b/src/lib/server/session.ts --- a/src/lib/server/session.ts +++ b/src/lib/server/session.ts @@ -1,5 +1,4 @@ import { dev } from "$app/environment"; -import { env } from "$env/dynamic/private"; import { HANDLE_COOKIE, OAUTH_MAX_AGE, @@ -65,7 +64,10 @@ * Setup OAuth client from cookies */ export const restoreSession = async (event: RequestEvent): Promise => { - const { cookies } = event; + const { cookies, platform } = event; + if (platform?.env === undefined) { + throw new Error(); + } const encrypted = cookies.get(SESSION_COOKIE); if (encrypted === undefined) { return; @@ -73,7 +75,10 @@ // Parse and validate or delete cookie let data: PublicUserData; try { - const decrypted = await decryptText(encrypted, env.PRIVATE_COOKIE_KEY); + const decrypted = await decryptText( + encrypted, + platform?.env.PRIVATE_COOKIE_KEY, + ); data = parsePublicUser(JSON.parse(decrypted)); } catch { cookies.delete(SESSION_COOKIE, { path: "/" }); diff --git a/src/routes/favicon.ico/+server.ts b/src/routes/favicon.ico/+server.ts --- a/src/routes/favicon.ico/+server.ts +++ b/src/routes/favicon.ico/+server.ts @@ -1,7 +1,17 @@ +import { error } from "@sveltejs/kit"; import type { RequestHandler } from "./$types.d.ts"; export const GET: RequestHandler = async (event) => { - const response = await event.fetch("/images/favicon.ico"); - response.headers.set("Content-Type", "image/x-icon"); - return response; + if (event.platform === undefined) { + error(404); + } + const response = await event.platform.env.ASSETS.fetch( + new URL("/images/favicon.ico", event.platform.env.ORIGIN), + ); + // if (dev) { + const headers = new Headers(response.headers); + headers.set("Content-Type", "image/x-icon"); + return new Response(response.body, { headers }); + // } + // return response; }; diff --git a/src/routes/oauth-client-metadata.json/+server.ts b/src/routes/oauth-client-metadata.json/+server.ts new file mode 100644 --- /dev/null +++ b/src/routes/oauth-client-metadata.json/+server.ts @@ -0,0 +1,12 @@ +import { createOAuthClient } from "$lib/server/oauth.ts"; +import { error, json } from "@sveltejs/kit"; +import type { RequestHandler } from "./$types.d.ts"; + +export const GET: RequestHandler = (event) => { + try { + const oAuthClient = createOAuthClient(event); + return json(oAuthClient.metadata); + } catch { + error(404); + } +}; diff --git a/src/routes/.well-known/jwks.json/+server.ts b/src/routes/.well-known/jwks.json/+server.ts new file mode 100644 --- /dev/null +++ b/src/routes/.well-known/jwks.json/+server.ts @@ -0,0 +1,12 @@ +import { createOAuthClient } from "$lib/server/oauth.ts"; +import { error, json } from "@sveltejs/kit"; +import type { RequestHandler } from "./$types.d.ts"; + +export const GET: RequestHandler = (event) => { + try { + const oAuthClient = createOAuthClient(event); + return json(oAuthClient.jwks ?? { keys: [] }); + } catch { + error(404); + } +}; diff --git a/src/routes/avatar/[did]/+server.ts b/src/routes/avatar/[did]/+server.ts --- a/src/routes/avatar/[did]/+server.ts +++ b/src/routes/avatar/[did]/+server.ts @@ -15,8 +15,7 @@ })); const avatar = new URL(profile.avatar!); return event.fetch(avatar); - } catch (err) { - console.log(err); + } catch { error(404); } }; diff --git a/src/routes/oauth/callback/+server.ts b/src/routes/oauth/callback/+server.ts --- a/src/routes/oauth/callback/+server.ts +++ b/src/routes/oauth/callback/+server.ts @@ -1,6 +1,9 @@ import { dev } from "$app/environment"; -import { env } from "$env/dynamic/private"; -import { HANDLE_COOKIE, SESSION_COOKIE } from "$lib/server/constants.ts"; +import { + HANDLE_COOKIE, + SESSION_COOKIE, + SESSION_MAX_AGE, +} from "$lib/server/constants.ts"; import { encryptText } from "$lib/server/crypto.ts"; import { createOAuthClient } from "$lib/server/oauth.ts"; import type { PublicUserData } from "$lib/valibot.ts"; @@ -12,7 +15,10 @@ import type { RequestHandler } from "./$types.d.ts"; export const GET: RequestHandler = async (event) => { - const { url, cookies } = event; + const { cookies, platform, url } = event; + if (platform?.env === undefined) { + throw new Error(); + } const handle = cookies.get(HANDLE_COOKIE); if (isHandle(handle) === false) { @@ -51,7 +57,7 @@ const encrypted = await encryptText( JSON.stringify(data), - env.PRIVATE_COOKIE_KEY, + platform.env.PRIVATE_COOKIE_KEY, ); cookies.set( @@ -59,12 +65,12 @@ encrypted, { httpOnly: true, - maxAge: 60 * 60 * 24, + maxAge: SESSION_MAX_AGE, path: "/", sameSite: "lax", secure: !dev, }, ); - redirect(303, "/?success"); + redirect(303, "/"); }; -- tangled.sh