chore: refactored to use atproto oauth lib
c04e4383
8 file(s) · +340 −5
| 13 | 13 | "name": "docs", |
|
| 14 | 14 | "version": "0.0.0", |
|
| 15 | 15 | "dependencies": { |
|
| 16 | + | "@atproto-labs/handle-resolver": "latest", |
|
| 17 | + | "@atproto/jwk-jose": "latest", |
|
| 18 | + | "@atproto/oauth-client": "latest", |
|
| 16 | 19 | "hono": "latest", |
|
| 17 | 20 | "react": "latest", |
|
| 18 | 21 | "react-dom": "latest", |
|
| 92 | 95 | ||
| 93 | 96 | "@atproto/lexicon": ["@atproto/lexicon@0.6.1", "", { "dependencies": { "@atproto/common-web": "^0.4.13", "@atproto/syntax": "^0.4.3", "iso-datestring-validator": "^2.2.2", "multiformats": "^9.9.0", "zod": "^3.23.8" } }, "sha512-/vI1kVlY50Si+5MXpvOucelnYwb0UJ6Qto5mCp+7Q5C+Jtp+SoSykAPVvjVtTnQUH2vrKOFOwpb3C375vSKzXw=="], |
|
| 94 | 97 | ||
| 95 | - | "@atproto/oauth-client": ["@atproto/oauth-client@0.5.14", "", { "dependencies": { "@atproto-labs/did-resolver": "0.2.6", "@atproto-labs/fetch": "0.2.3", "@atproto-labs/handle-resolver": "0.3.6", "@atproto-labs/identity-resolver": "0.3.6", "@atproto-labs/simple-store": "0.3.0", "@atproto-labs/simple-store-memory": "0.1.4", "@atproto/did": "0.3.0", "@atproto/jwk": "0.6.0", "@atproto/oauth-types": "0.6.2", "@atproto/xrpc": "0.7.7", "core-js": "^3", "multiformats": "^9.9.0", "zod": "^3.23.8" } }, "sha512-sPH+vcdq9maTEAhJI0HzmFcFAMrkCS19np+RUssNkX6kS8Xr3OYr57tvYRCbkcnIyYTfYcxKQgpwHKx3RVEaYw=="], |
|
| 98 | + | "@atproto/oauth-client": ["@atproto/oauth-client@0.6.0", "", { "dependencies": { "@atproto-labs/did-resolver": "^0.2.6", "@atproto-labs/fetch": "^0.2.3", "@atproto-labs/handle-resolver": "^0.3.6", "@atproto-labs/identity-resolver": "^0.3.6", "@atproto-labs/simple-store": "^0.3.0", "@atproto-labs/simple-store-memory": "^0.1.4", "@atproto/did": "^0.3.0", "@atproto/jwk": "^0.6.0", "@atproto/oauth-types": "^0.6.3", "@atproto/xrpc": "^0.7.7", "core-js": "^3", "multiformats": "^9.9.0", "zod": "^3.23.8" } }, "sha512-F7ZTKzFptXgyihMkd7QTdRSkrh4XqrS+qTw+V81k5Q6Bh3MB1L3ypvfSJ6v7SSUJa6XxoZYJTCahHC1e+ndE6Q=="], |
|
| 96 | 99 | ||
| 97 | 100 | "@atproto/oauth-client-node": ["@atproto/oauth-client-node@0.3.16", "", { "dependencies": { "@atproto-labs/did-resolver": "0.2.6", "@atproto-labs/handle-resolver-node": "0.1.25", "@atproto-labs/simple-store": "0.3.0", "@atproto/did": "0.3.0", "@atproto/jwk": "0.6.0", "@atproto/jwk-jose": "0.1.11", "@atproto/jwk-webcrypto": "0.2.0", "@atproto/oauth-client": "0.5.14", "@atproto/oauth-types": "0.6.2" } }, "sha512-2dooMzxAkiQ4MkOAZlEQ3iwbB9SEovrbIKMNuBbVCLQYORVNxe20tMdjs3lvhrzdpzvaHLlQnJJhw5dA9VELFw=="], |
|
| 98 | 101 | ||
| 99 | - | "@atproto/oauth-types": ["@atproto/oauth-types@0.6.2", "", { "dependencies": { "@atproto/did": "0.3.0", "@atproto/jwk": "0.6.0", "zod": "^3.23.8" } }, "sha512-2cuboM4RQBCYR8NQC5uGRkW6KgCgKyq/B5/+tnMmWZYtZGVUQvsUWQHK/ZiMCnVXbcDNtc/RIEJQJDZ8FXMoxg=="], |
|
| 102 | + | "@atproto/oauth-types": ["@atproto/oauth-types@0.6.3", "", { "dependencies": { "@atproto/did": "^0.3.0", "@atproto/jwk": "^0.6.0", "zod": "^3.23.8" } }, "sha512-jdKuoPknJuh/WjI+mYk7agSbx9mNVMbS6Dr3k1z2YMY2oRiCQjxYBuo4MLKATbxj05nMQaZRWlHRUazoAu5Cng=="], |
|
| 100 | 103 | ||
| 101 | 104 | "@atproto/syntax": ["@atproto/syntax@0.4.3", "", { "dependencies": { "tslib": "^2.8.1" } }, "sha512-YoZUz40YAJr5nPwvCDWgodEOlt5IftZqPJvA0JDWjuZKD8yXddTwSzXSaKQAzGOpuM+/A3uXRtPzJJqlScc+iA=="], |
|
| 102 | 105 | ||
| 1535 | 1538 | "zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], |
|
| 1536 | 1539 | ||
| 1537 | 1540 | "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="], |
|
| 1541 | + | ||
| 1542 | + | "@atproto/oauth-client-node/@atproto/oauth-client": ["@atproto/oauth-client@0.5.14", "", { "dependencies": { "@atproto-labs/did-resolver": "0.2.6", "@atproto-labs/fetch": "0.2.3", "@atproto-labs/handle-resolver": "0.3.6", "@atproto-labs/identity-resolver": "0.3.6", "@atproto-labs/simple-store": "0.3.0", "@atproto-labs/simple-store-memory": "0.1.4", "@atproto/did": "0.3.0", "@atproto/jwk": "0.6.0", "@atproto/oauth-types": "0.6.2", "@atproto/xrpc": "0.7.7", "core-js": "^3", "multiformats": "^9.9.0", "zod": "^3.23.8" } }, "sha512-sPH+vcdq9maTEAhJI0HzmFcFAMrkCS19np+RUssNkX6kS8Xr3OYr57tvYRCbkcnIyYTfYcxKQgpwHKx3RVEaYw=="], |
|
| 1543 | + | ||
| 1544 | + | "@atproto/oauth-client-node/@atproto/oauth-types": ["@atproto/oauth-types@0.6.2", "", { "dependencies": { "@atproto/did": "0.3.0", "@atproto/jwk": "0.6.0", "zod": "^3.23.8" } }, "sha512-2cuboM4RQBCYR8NQC5uGRkW6KgCgKyq/B5/+tnMmWZYtZGVUQvsUWQHK/ZiMCnVXbcDNtc/RIEJQJDZ8FXMoxg=="], |
|
| 1538 | 1545 | ||
| 1539 | 1546 | "@babel/helper-compilation-targets/lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], |
|
| 1540 | 1547 | ||
| 11 | 11 | "preview": "vocs preview" |
|
| 12 | 12 | }, |
|
| 13 | 13 | "dependencies": { |
|
| 14 | + | "@atproto/oauth-client": "latest", |
|
| 15 | + | "@atproto/jwk-jose": "latest", |
|
| 16 | + | "@atproto-labs/handle-resolver": "latest", |
|
| 14 | 17 | "hono": "latest", |
|
| 15 | 18 | "react": "latest", |
|
| 16 | 19 | "react-dom": "latest", |
| 1 | 1 | import { Hono } from "hono"; |
|
| 2 | + | import auth from "./routes/auth"; |
|
| 2 | 3 | ||
| 3 | 4 | type Bindings = { |
|
| 4 | 5 | ASSETS: Fetcher; |
|
| 6 | + | SEQUOIA_SESSIONS: KVNamespace; |
|
| 7 | + | CLIENT_URL: string; |
|
| 5 | 8 | }; |
|
| 6 | 9 | ||
| 7 | 10 | const app = new Hono<{ Bindings: Bindings }>(); |
|
| 8 | 11 | ||
| 9 | - | app.get("/oauth/callback", (c) => { |
|
| 10 | - | return c.text("Not Implemented", 501); |
|
| 11 | - | }); |
|
| 12 | + | app.route("/oauth", auth); |
|
| 12 | 13 | ||
| 13 | 14 | app.get("/api/health", (c) => { |
|
| 14 | 15 | return c.json({ status: "ok" }); |
| 1 | + | import { JoseKey } from "@atproto/jwk-jose"; |
|
| 2 | + | import type { |
|
| 3 | + | Key, |
|
| 4 | + | InternalStateData, |
|
| 5 | + | SessionStore, |
|
| 6 | + | StateStore, |
|
| 7 | + | } from "@atproto/oauth-client"; |
|
| 8 | + | ||
| 9 | + | type SerializedStateData = Omit<InternalStateData, "dpopKey"> & { |
|
| 10 | + | dpopJwk: Record<string, unknown>; |
|
| 11 | + | }; |
|
| 12 | + | ||
| 13 | + | type SerializedSession = Omit< |
|
| 14 | + | Parameters<SessionStore["set"]>[1], |
|
| 15 | + | "dpopKey" |
|
| 16 | + | > & { |
|
| 17 | + | dpopJwk: Record<string, unknown>; |
|
| 18 | + | }; |
|
| 19 | + | ||
| 20 | + | function serializeKey(key: Key): Record<string, unknown> { |
|
| 21 | + | const jwk = key.privateJwk; |
|
| 22 | + | if (!jwk) throw new Error("Private DPoP JWK is missing"); |
|
| 23 | + | return jwk as Record<string, unknown>; |
|
| 24 | + | } |
|
| 25 | + | ||
| 26 | + | async function deserializeKey(jwk: Record<string, unknown>): Promise<Key> { |
|
| 27 | + | return JoseKey.fromJWK(jwk); |
|
| 28 | + | } |
|
| 29 | + | ||
| 30 | + | export function createStateStore( |
|
| 31 | + | kv: KVNamespace, |
|
| 32 | + | ttl = 600, |
|
| 33 | + | ): StateStore { |
|
| 34 | + | return { |
|
| 35 | + | async set(key, { dpopKey, ...rest }) { |
|
| 36 | + | const data: SerializedStateData = { |
|
| 37 | + | ...rest, |
|
| 38 | + | dpopJwk: serializeKey(dpopKey), |
|
| 39 | + | }; |
|
| 40 | + | await kv.put(`oauth_state:${key}`, JSON.stringify(data), { |
|
| 41 | + | expirationTtl: ttl, |
|
| 42 | + | }); |
|
| 43 | + | }, |
|
| 44 | + | async get(key) { |
|
| 45 | + | const raw = await kv.get(`oauth_state:${key}`); |
|
| 46 | + | if (!raw) return undefined; |
|
| 47 | + | const { dpopJwk, ...rest }: SerializedStateData = JSON.parse(raw); |
|
| 48 | + | const dpopKey = await deserializeKey(dpopJwk); |
|
| 49 | + | return { ...rest, dpopKey }; |
|
| 50 | + | }, |
|
| 51 | + | async del(key) { |
|
| 52 | + | await kv.delete(`oauth_state:${key}`); |
|
| 53 | + | }, |
|
| 54 | + | }; |
|
| 55 | + | } |
|
| 56 | + | ||
| 57 | + | export function createSessionStore( |
|
| 58 | + | kv: KVNamespace, |
|
| 59 | + | ttl = 60 * 60 * 24 * 14, |
|
| 60 | + | ): SessionStore { |
|
| 61 | + | return { |
|
| 62 | + | async set(sub, { dpopKey, ...rest }) { |
|
| 63 | + | const data: SerializedSession = { |
|
| 64 | + | ...rest, |
|
| 65 | + | dpopJwk: serializeKey(dpopKey), |
|
| 66 | + | }; |
|
| 67 | + | await kv.put(`oauth_session:${sub}`, JSON.stringify(data), { |
|
| 68 | + | expirationTtl: ttl, |
|
| 69 | + | }); |
|
| 70 | + | }, |
|
| 71 | + | async get(sub) { |
|
| 72 | + | const raw = await kv.get(`oauth_session:${sub}`); |
|
| 73 | + | if (!raw) return undefined; |
|
| 74 | + | const { dpopJwk, ...rest }: SerializedSession = JSON.parse(raw); |
|
| 75 | + | const dpopKey = await deserializeKey(dpopJwk); |
|
| 76 | + | return { ...rest, dpopKey }; |
|
| 77 | + | }, |
|
| 78 | + | async del(sub) { |
|
| 79 | + | await kv.delete(`oauth_session:${sub}`); |
|
| 80 | + | }, |
|
| 81 | + | }; |
|
| 82 | + | } |
| 1 | + | import { JoseKey } from "@atproto/jwk-jose"; |
|
| 2 | + | import { OAuthClient } from "@atproto/oauth-client"; |
|
| 3 | + | import { AtprotoDohHandleResolver } from "@atproto-labs/handle-resolver"; |
|
| 4 | + | import { createStateStore, createSessionStore } from "./kv-stores"; |
|
| 5 | + | ||
| 6 | + | export function createOAuthClient(kv: KVNamespace, clientUrl: string) { |
|
| 7 | + | const clientId = `${clientUrl}/oauth/client-metadata.json`; |
|
| 8 | + | const redirectUri = `${clientUrl}/oauth/callback`; |
|
| 9 | + | ||
| 10 | + | return new OAuthClient({ |
|
| 11 | + | responseMode: "query", |
|
| 12 | + | handleResolver: new AtprotoDohHandleResolver({ |
|
| 13 | + | dohEndpoint: "https://cloudflare-dns.com/dns-query", |
|
| 14 | + | }), |
|
| 15 | + | clientMetadata: { |
|
| 16 | + | client_id: clientId, |
|
| 17 | + | client_name: "Sequoia", |
|
| 18 | + | client_uri: clientUrl, |
|
| 19 | + | redirect_uris: [redirectUri], |
|
| 20 | + | grant_types: ["authorization_code", "refresh_token"], |
|
| 21 | + | response_types: ["code"], |
|
| 22 | + | scope: "atproto transition:generic", |
|
| 23 | + | token_endpoint_auth_method: "none", |
|
| 24 | + | application_type: "web", |
|
| 25 | + | dpop_bound_access_tokens: true, |
|
| 26 | + | }, |
|
| 27 | + | runtimeImplementation: { |
|
| 28 | + | createKey: (algs: string[]) => JoseKey.generate(algs), |
|
| 29 | + | getRandomValues: (length: number) => |
|
| 30 | + | crypto.getRandomValues(new Uint8Array(length)), |
|
| 31 | + | digest: async (data: Uint8Array, { name }: { name: string }) => { |
|
| 32 | + | const buf = await crypto.subtle.digest( |
|
| 33 | + | name.replace("sha", "SHA-"), |
|
| 34 | + | new Uint8Array(data), |
|
| 35 | + | ); |
|
| 36 | + | return new Uint8Array(buf); |
|
| 37 | + | }, |
|
| 38 | + | requestLock: <T>(_name: string, fn: () => T | PromiseLike<T>) => fn(), |
|
| 39 | + | }, |
|
| 40 | + | stateStore: createStateStore(kv), |
|
| 41 | + | sessionStore: createSessionStore(kv), |
|
| 42 | + | }); |
|
| 43 | + | } |
| 1 | + | import type { Context } from "hono"; |
|
| 2 | + | ||
| 3 | + | const SESSION_COOKIE_NAME = "session_id"; |
|
| 4 | + | const SESSION_TTL = 60 * 60 * 24 * 14; // 14 days in seconds |
|
| 5 | + | ||
| 6 | + | /** |
|
| 7 | + | * Get DID from session cookie |
|
| 8 | + | */ |
|
| 9 | + | export function getSessionDid(c: Context): string | null { |
|
| 10 | + | const cookie = c.req.header("Cookie"); |
|
| 11 | + | if (!cookie) return null; |
|
| 12 | + | ||
| 13 | + | const match = cookie.match(new RegExp(`${SESSION_COOKIE_NAME}=([^;]+)`)); |
|
| 14 | + | return match ? decodeURIComponent(match[1]) : null; |
|
| 15 | + | } |
|
| 16 | + | ||
| 17 | + | /** |
|
| 18 | + | * Set session cookie with the user's DID |
|
| 19 | + | */ |
|
| 20 | + | export function setSessionCookie( |
|
| 21 | + | c: Context, |
|
| 22 | + | did: string, |
|
| 23 | + | clientUrl: string, |
|
| 24 | + | ): void { |
|
| 25 | + | const isLocalhost = clientUrl.includes("localhost"); |
|
| 26 | + | const domain = isLocalhost ? "" : "; Domain=.sequoia.pub"; |
|
| 27 | + | const secure = isLocalhost ? "" : "; Secure"; |
|
| 28 | + | ||
| 29 | + | c.header( |
|
| 30 | + | "Set-Cookie", |
|
| 31 | + | `${SESSION_COOKIE_NAME}=${encodeURIComponent(did)}; HttpOnly; SameSite=Lax; Path=/${domain}${secure}; Max-Age=${SESSION_TTL}`, |
|
| 32 | + | ); |
|
| 33 | + | } |
|
| 34 | + | ||
| 35 | + | /** |
|
| 36 | + | * Clear session cookie |
|
| 37 | + | */ |
|
| 38 | + | export function clearSessionCookie(c: Context, clientUrl: string): void { |
|
| 39 | + | const isLocalhost = clientUrl.includes("localhost"); |
|
| 40 | + | const domain = isLocalhost ? "" : "; Domain=.sequoia.pub"; |
|
| 41 | + | const secure = isLocalhost ? "" : "; Secure"; |
|
| 42 | + | ||
| 43 | + | c.header( |
|
| 44 | + | "Set-Cookie", |
|
| 45 | + | `${SESSION_COOKIE_NAME}=; HttpOnly; SameSite=Lax; Path=/${domain}${secure}; Max-Age=0`, |
|
| 46 | + | ); |
|
| 47 | + | } |
| 1 | + | import { Hono } from "hono"; |
|
| 2 | + | import { createOAuthClient } from "../lib/oauth-client"; |
|
| 3 | + | import { |
|
| 4 | + | getSessionDid, |
|
| 5 | + | setSessionCookie, |
|
| 6 | + | clearSessionCookie, |
|
| 7 | + | } from "../lib/session"; |
|
| 8 | + | ||
| 9 | + | interface Env { |
|
| 10 | + | SEQUOIA_SESSIONS: KVNamespace; |
|
| 11 | + | CLIENT_URL: string; |
|
| 12 | + | } |
|
| 13 | + | ||
| 14 | + | const auth = new Hono<{ Bindings: Env }>(); |
|
| 15 | + | ||
| 16 | + | // OAuth client metadata endpoint |
|
| 17 | + | auth.get("/client-metadata.json", (c) => { |
|
| 18 | + | const clientId = `${c.env.CLIENT_URL}/oauth/client-metadata.json`; |
|
| 19 | + | const redirectUri = `${c.env.CLIENT_URL}/oauth/callback`; |
|
| 20 | + | ||
| 21 | + | return c.json({ |
|
| 22 | + | client_id: clientId, |
|
| 23 | + | client_name: "Sequoia", |
|
| 24 | + | client_uri: c.env.CLIENT_URL, |
|
| 25 | + | redirect_uris: [redirectUri], |
|
| 26 | + | grant_types: ["authorization_code", "refresh_token"], |
|
| 27 | + | response_types: ["code"], |
|
| 28 | + | scope: "atproto transition:generic", |
|
| 29 | + | token_endpoint_auth_method: "none", |
|
| 30 | + | application_type: "web", |
|
| 31 | + | dpop_bound_access_tokens: true, |
|
| 32 | + | }); |
|
| 33 | + | }); |
|
| 34 | + | ||
| 35 | + | // Start OAuth login flow |
|
| 36 | + | auth.get("/login", async (c) => { |
|
| 37 | + | try { |
|
| 38 | + | const handle = c.req.query("handle"); |
|
| 39 | + | if (!handle) { |
|
| 40 | + | return c.redirect(`${c.env.CLIENT_URL}/?error=missing_handle`); |
|
| 41 | + | } |
|
| 42 | + | ||
| 43 | + | const client = createOAuthClient(c.env.SEQUOIA_SESSIONS, c.env.CLIENT_URL); |
|
| 44 | + | const authUrl = await client.authorize(handle, { |
|
| 45 | + | scope: "atproto transition:generic", |
|
| 46 | + | }); |
|
| 47 | + | ||
| 48 | + | return c.redirect(authUrl.toString()); |
|
| 49 | + | } catch (error) { |
|
| 50 | + | console.error("Login error:", error); |
|
| 51 | + | return c.redirect(`${c.env.CLIENT_URL}/?error=login_failed`); |
|
| 52 | + | } |
|
| 53 | + | }); |
|
| 54 | + | ||
| 55 | + | // OAuth callback handler |
|
| 56 | + | auth.get("/callback", async (c) => { |
|
| 57 | + | try { |
|
| 58 | + | const params = new URLSearchParams(c.req.url.split("?")[1] || ""); |
|
| 59 | + | ||
| 60 | + | if (params.get("error")) { |
|
| 61 | + | const error = params.get("error"); |
|
| 62 | + | console.error("OAuth error:", error, params.get("error_description")); |
|
| 63 | + | return c.redirect( |
|
| 64 | + | `${c.env.CLIENT_URL}/?error=${encodeURIComponent(error!)}`, |
|
| 65 | + | ); |
|
| 66 | + | } |
|
| 67 | + | ||
| 68 | + | const client = createOAuthClient(c.env.SEQUOIA_SESSIONS, c.env.CLIENT_URL); |
|
| 69 | + | const { session } = await client.callback(params); |
|
| 70 | + | ||
| 71 | + | // Resolve handle from DID |
|
| 72 | + | let handle: string | undefined; |
|
| 73 | + | try { |
|
| 74 | + | const identity = await client.identityResolver.resolve(session.did); |
|
| 75 | + | handle = identity.handle; |
|
| 76 | + | } catch { |
|
| 77 | + | // Handle resolution is best-effort |
|
| 78 | + | } |
|
| 79 | + | ||
| 80 | + | // Store handle in KV alongside the session for quick lookup |
|
| 81 | + | if (handle) { |
|
| 82 | + | await c.env.SEQUOIA_SESSIONS.put(`oauth_handle:${session.did}`, handle, { |
|
| 83 | + | expirationTtl: 60 * 60 * 24 * 14, |
|
| 84 | + | }); |
|
| 85 | + | } |
|
| 86 | + | ||
| 87 | + | setSessionCookie(c, session.did, c.env.CLIENT_URL); |
|
| 88 | + | return c.redirect(`${c.env.CLIENT_URL}/`); |
|
| 89 | + | } catch (error) { |
|
| 90 | + | console.error("Callback error:", error); |
|
| 91 | + | return c.redirect(`${c.env.CLIENT_URL}/?error=callback_failed`); |
|
| 92 | + | } |
|
| 93 | + | }); |
|
| 94 | + | ||
| 95 | + | // Logout endpoint |
|
| 96 | + | auth.post("/logout", async (c) => { |
|
| 97 | + | const did = getSessionDid(c); |
|
| 98 | + | ||
| 99 | + | if (did) { |
|
| 100 | + | try { |
|
| 101 | + | const client = createOAuthClient( |
|
| 102 | + | c.env.SEQUOIA_SESSIONS, |
|
| 103 | + | c.env.CLIENT_URL, |
|
| 104 | + | ); |
|
| 105 | + | await client.revoke(did); |
|
| 106 | + | } catch (error) { |
|
| 107 | + | console.error("Revoke error:", error); |
|
| 108 | + | } |
|
| 109 | + | await c.env.SEQUOIA_SESSIONS.delete(`oauth_handle:${did}`); |
|
| 110 | + | } |
|
| 111 | + | ||
| 112 | + | clearSessionCookie(c, c.env.CLIENT_URL); |
|
| 113 | + | return c.json({ success: true }); |
|
| 114 | + | }); |
|
| 115 | + | ||
| 116 | + | // Check auth status |
|
| 117 | + | auth.get("/status", async (c) => { |
|
| 118 | + | const did = getSessionDid(c); |
|
| 119 | + | ||
| 120 | + | if (!did) { |
|
| 121 | + | return c.json({ authenticated: false }); |
|
| 122 | + | } |
|
| 123 | + | ||
| 124 | + | try { |
|
| 125 | + | const client = createOAuthClient(c.env.SEQUOIA_SESSIONS, c.env.CLIENT_URL); |
|
| 126 | + | const session = await client.restore(did); |
|
| 127 | + | ||
| 128 | + | const handle = await c.env.SEQUOIA_SESSIONS.get( |
|
| 129 | + | `oauth_handle:${session.did}`, |
|
| 130 | + | ); |
|
| 131 | + | ||
| 132 | + | return c.json({ |
|
| 133 | + | authenticated: true, |
|
| 134 | + | did: session.did, |
|
| 135 | + | handle: handle || undefined, |
|
| 136 | + | }); |
|
| 137 | + | } catch (error) { |
|
| 138 | + | console.error("Session restore failed:", error); |
|
| 139 | + | clearSessionCookie(c, c.env.CLIENT_URL); |
|
| 140 | + | return c.json({ authenticated: false }); |
|
| 141 | + | } |
|
| 142 | + | }); |
|
| 143 | + | ||
| 144 | + | export default auth; |
| 1 | 1 | name = "sequoia-docs" |
|
| 2 | 2 | main = "src/index.ts" |
|
| 3 | 3 | compatibility_date = "2025-04-01" |
|
| 4 | + | compatibility_flags = ["nodejs_compat"] |
|
| 4 | 5 | ||
| 5 | 6 | [assets] |
|
| 6 | 7 | directory = "./docs/dist" |
|
| 8 | 9 | not_found_handling = "single-page-application" |
|
| 9 | 10 | html_handling = "auto-trailing-slash" |
|
| 10 | 11 | run_worker_first = ["/api/*", "/oauth/*"] |
|
| 12 | + | ||
| 13 | + | [[kv_namespaces]] |
|
| 14 | + | binding = "SEQUOIA_SESSIONS" |
|
| 15 | + | id = "b9fedf2798a249669b3aeeaca70a0bf8" |
|
| 16 | + | ||
| 17 | + | [vars] |
|
| 18 | + | CLIENT_URL = "https://sequoia.pub" |
|