A CLI for publishing standard.site documents to ATProto
at sync-update 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;