A CLI for publishing standard.site documents to ATProto sequoia.pub
standard site lexicon cli publishing
57
fork

Configure Feed

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

at 501c7bd6dba25dfd3631c8bbec75fdadf9957851 144 lines 3.7 kB view raw
1import { Hono } from "hono"; 2import { createOAuthClient } from "../lib/oauth-client"; 3import { 4 getSessionDid, 5 setSessionCookie, 6 clearSessionCookie, 7} from "../lib/session"; 8 9interface Env { 10 SEQUOIA_SESSIONS: KVNamespace; 11 CLIENT_URL: string; 12} 13 14const auth = new Hono<{ Bindings: Env }>(); 15 16// OAuth client metadata endpoint 17auth.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 36auth.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 56auth.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 96auth.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 117auth.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 144export default auth;