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