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 a9a8e15d5a5472097d408dcfa48d2359609ea9d2 151 lines 3.9 kB view raw
1import { Hono } from "hono"; 2import { createOAuthClient, OAUTH_SCOPE } from "../lib/oauth-client"; 3import { 4 getSessionDid, 5 setSessionCookie, 6 clearSessionCookie, 7 getReturnToCookie, 8 clearReturnToCookie, 9} from "../lib/session"; 10 11interface Env { 12 SEQUOIA_SESSIONS: KVNamespace; 13 CLIENT_URL: string; 14} 15 16const auth = new Hono<{ Bindings: Env }>(); 17 18// OAuth client metadata endpoint 19auth.get("/client-metadata.json", (c) => { 20 const clientId = `${c.env.CLIENT_URL}/oauth/client-metadata.json`; 21 const redirectUri = `${c.env.CLIENT_URL}/oauth/callback`; 22 23 return c.json({ 24 client_id: clientId, 25 client_name: "Sequoia", 26 client_uri: c.env.CLIENT_URL, 27 redirect_uris: [redirectUri], 28 grant_types: ["authorization_code", "refresh_token"], 29 response_types: ["code"], 30 scope: OAUTH_SCOPE, 31 token_endpoint_auth_method: "none", 32 application_type: "web", 33 dpop_bound_access_tokens: true, 34 }); 35}); 36 37// Start OAuth login flow 38auth.get("/login", async (c) => { 39 try { 40 const handle = c.req.query("handle"); 41 if (!handle) { 42 return c.redirect(`${c.env.CLIENT_URL}/?error=missing_handle`); 43 } 44 45 const client = createOAuthClient(c.env.SEQUOIA_SESSIONS, c.env.CLIENT_URL); 46 const authUrl = await client.authorize(handle, { 47 scope: OAUTH_SCOPE, 48 }); 49 50 return c.redirect(authUrl.toString()); 51 } catch (error) { 52 console.error("Login error:", error); 53 return c.redirect(`${c.env.CLIENT_URL}/?error=login_failed`); 54 } 55}); 56 57// OAuth callback handler 58auth.get("/callback", async (c) => { 59 try { 60 const params = new URLSearchParams(c.req.url.split("?")[1] || ""); 61 62 if (params.get("error")) { 63 const error = params.get("error"); 64 console.error("OAuth error:", error, params.get("error_description")); 65 return c.redirect( 66 `${c.env.CLIENT_URL}/?error=${encodeURIComponent(error!)}`, 67 ); 68 } 69 70 const client = createOAuthClient(c.env.SEQUOIA_SESSIONS, c.env.CLIENT_URL); 71 const { session } = await client.callback(params); 72 73 // Resolve handle from DID 74 let handle: string | undefined; 75 try { 76 const identity = await client.identityResolver.resolve(session.did); 77 handle = identity.handle; 78 } catch { 79 // Handle resolution is best-effort 80 } 81 82 // Store handle in KV alongside the session for quick lookup 83 if (handle) { 84 await c.env.SEQUOIA_SESSIONS.put(`oauth_handle:${session.did}`, handle, { 85 expirationTtl: 60 * 60 * 24 * 14, 86 }); 87 } 88 89 setSessionCookie(c, session.did, c.env.CLIENT_URL); 90 91 // If a subscribe flow set a return URL before initiating OAuth, honor it 92 const returnTo = getReturnToCookie(c); 93 clearReturnToCookie(c, c.env.CLIENT_URL); 94 95 return c.redirect(returnTo ?? `${c.env.CLIENT_URL}/`); 96 } catch (error) { 97 console.error("Callback error:", error); 98 return c.redirect(`${c.env.CLIENT_URL}/?error=callback_failed`); 99 } 100}); 101 102// Logout endpoint 103auth.post("/logout", async (c) => { 104 const did = getSessionDid(c); 105 106 if (did) { 107 try { 108 const client = createOAuthClient( 109 c.env.SEQUOIA_SESSIONS, 110 c.env.CLIENT_URL, 111 ); 112 await client.revoke(did); 113 } catch (error) { 114 console.error("Revoke error:", error); 115 } 116 await c.env.SEQUOIA_SESSIONS.delete(`oauth_handle:${did}`); 117 } 118 119 clearSessionCookie(c, c.env.CLIENT_URL); 120 return c.json({ success: true }); 121}); 122 123// Check auth status 124auth.get("/status", async (c) => { 125 const did = getSessionDid(c); 126 127 if (!did) { 128 return c.json({ authenticated: false }); 129 } 130 131 try { 132 const client = createOAuthClient(c.env.SEQUOIA_SESSIONS, c.env.CLIENT_URL); 133 const session = await client.restore(did); 134 135 const handle = await c.env.SEQUOIA_SESSIONS.get( 136 `oauth_handle:${session.did}`, 137 ); 138 139 return c.json({ 140 authenticated: true, 141 did: session.did, 142 handle: handle || undefined, 143 }); 144 } catch (error) { 145 console.error("Session restore failed:", error); 146 clearSessionCookie(c, c.env.CLIENT_URL); 147 return c.json({ authenticated: false }); 148 } 149}); 150 151export default auth;