import { Agent } from "@atproto/api"; import { Hono } from "hono"; import { createOAuthClient } from "../lib/oauth-client"; import { getSessionDid, setReturnToCookie } from "../lib/session"; interface Env { ASSETS: Fetcher; SEQUOIA_SESSIONS: KVNamespace; CLIENT_URL: string; } // Cache the vocs-generated stylesheet href across requests (changes on rebuild). let _vocsStyleHref: string | null = null; async function getVocsStyleHref( assets: Fetcher, baseUrl: string, ): Promise { if (_vocsStyleHref) return _vocsStyleHref; try { const indexUrl = new URL("/", baseUrl).toString(); const res = await assets.fetch(indexUrl); const html = await res.text(); const match = html.match(/]+href="(\/assets\/style[^"]+\.css)"/); if (match?.[1]) { _vocsStyleHref = match[1]; return match[1]; } } catch { // Fall back to the custom stylesheet which at least provides --sequoia-* vars } return "/styles.css"; } const subscribe = new Hono<{ Bindings: Env }>(); const COLLECTION = "site.standard.graph.subscription"; const REDIRECT_DELAY_SECONDS = 5; // ============================================================================ // Helpers // ============================================================================ /** * Scan the user's repo for an existing site.standard.graph.subscription * matching the given publication URI. Returns the record AT-URI if found. */ async function findExistingSubscription( agent: Agent, did: string, publicationUri: string, ): Promise { let cursor: string | undefined; do { const result = await agent.com.atproto.repo.listRecords({ repo: did, collection: COLLECTION, limit: 100, cursor, }); for (const record of result.data.records) { const value = record.value as { publication?: string }; if (value.publication === publicationUri) { return record.uri; } } cursor = result.data.cursor; } while (cursor); return null; } // ============================================================================ // POST /subscribe // // Called via fetch() from the sequoia-subscribe web component. // Body JSON: { publicationUri: string } // // Responses: // 200 { subscribed: true, existing: boolean, recordUri: string } // 400 { error: string } // 401 { authenticated: false, subscribeUrl: string } // ============================================================================ subscribe.post("/", async (c) => { let publicationUri: string; try { const body = await c.req.json<{ publicationUri?: string }>(); publicationUri = body.publicationUri ?? ""; } catch { return c.json({ error: "Invalid JSON body" }, 400); } if (!publicationUri || !publicationUri.startsWith("at://")) { return c.json({ error: "Missing or invalid publicationUri" }, 400); } const did = getSessionDid(c); if (!did) { const subscribeUrl = `${c.env.CLIENT_URL}/subscribe?publicationUri=${encodeURIComponent(publicationUri)}`; return c.json({ authenticated: false, subscribeUrl }, 401); } try { const client = createOAuthClient(c.env.SEQUOIA_SESSIONS, c.env.CLIENT_URL); const session = await client.restore(did); const agent = new Agent(session); const existingUri = await findExistingSubscription( agent, did, publicationUri, ); if (existingUri) { return c.json({ subscribed: true, existing: true, recordUri: existingUri, }); } const result = await agent.com.atproto.repo.createRecord({ repo: did, collection: COLLECTION, record: { $type: COLLECTION, publication: publicationUri, }, }); return c.json({ subscribed: true, existing: false, recordUri: result.data.uri, }); } catch (error) { console.error("Subscribe POST error:", error); // Treat expired/missing session as unauthenticated const subscribeUrl = `${c.env.CLIENT_URL}/subscribe?publicationUri=${encodeURIComponent(publicationUri)}`; return c.json({ authenticated: false, subscribeUrl }, 401); } }); // ============================================================================ // GET /subscribe?publicationUri=at://... // // Full-page OAuth + subscription flow. Unauthenticated users land here after // the component redirects them, and authenticated users land here after the // OAuth callback (via the login_return_to cookie set in POST /subscribe/login). // ============================================================================ subscribe.get("/", async (c) => { const publicationUri = c.req.query("publicationUri"); const styleHref = await getVocsStyleHref(c.env.ASSETS, c.req.url); if (!publicationUri || !publicationUri.startsWith("at://")) { return c.html( renderError("Missing or invalid publication URI.", styleHref), 400, ); } // Prefer an explicit returnTo query param (survives the OAuth round-trip); // fall back to the Referer header on the first visit, ignoring self-referrals. const referer = c.req.header("referer"); const returnTo = c.req.query("returnTo") ?? (referer && !referer.includes("/subscribe") ? referer : undefined); const did = getSessionDid(c); if (!did) { return c.html(renderHandleForm(publicationUri, styleHref, returnTo)); } try { const client = createOAuthClient(c.env.SEQUOIA_SESSIONS, c.env.CLIENT_URL); const session = await client.restore(did); const agent = new Agent(session); const existingUri = await findExistingSubscription( agent, did, publicationUri, ); if (existingUri) { return c.html( renderSuccess(publicationUri, existingUri, true, styleHref, returnTo), ); } const result = await agent.com.atproto.repo.createRecord({ repo: did, collection: COLLECTION, record: { $type: COLLECTION, publication: publicationUri, }, }); return c.html( renderSuccess( publicationUri, result.data.uri, false, styleHref, returnTo, ), ); } catch (error) { console.error("Subscribe GET error:", error); // Session expired - ask the user to sign in again return c.html( renderHandleForm( publicationUri, styleHref, returnTo, "Session expired. Please sign in again.", ), ); } }); // ============================================================================ // POST /subscribe/login // // Handles the handle-entry form submission. Stores the return URL in a cookie // so the OAuth callback in auth.ts can redirect back to /subscribe after auth. // ============================================================================ subscribe.post("/login", async (c) => { const body = await c.req.parseBody(); const handle = (body["handle"] as string | undefined)?.trim(); const publicationUri = body["publicationUri"] as string | undefined; const formReturnTo = (body["returnTo"] as string | undefined) || undefined; if (!handle || !publicationUri) { const styleHref = await getVocsStyleHref(c.env.ASSETS, c.req.url); return c.html( renderError("Missing handle or publication URI.", styleHref), 400, ); } const returnTo = `${c.env.CLIENT_URL}/subscribe?publicationUri=${encodeURIComponent(publicationUri)}` + (formReturnTo ? `&returnTo=${encodeURIComponent(formReturnTo)}` : ""); setReturnToCookie(c, returnTo, c.env.CLIENT_URL); return c.redirect( `${c.env.CLIENT_URL}/oauth/login?handle=${encodeURIComponent(handle)}`, ); }); // ============================================================================ // HTML rendering // ============================================================================ function renderHandleForm( publicationUri: string, styleHref: string, returnTo?: string, error?: string, ): string { const errorHtml = error ? `

${escapeHtml(error)}

` : ""; const returnToInput = returnTo ? `` : ""; return page( `

Subscribe on Bluesky

Enter your Bluesky handle to subscribe to this publication.

${errorHtml}
${returnToInput}
`, styleHref, ); } function renderSuccess( publicationUri: string, recordUri: string, existing: boolean, styleHref: string, returnTo?: string, ): string { const msg = existing ? "You're already subscribed to this publication." : "You've successfully subscribed!"; const escapedPublicationUri = escapeHtml(publicationUri); const escapedRecordUri = escapeHtml(recordUri); const redirectHtml = returnTo ? `

Redirecting to ${escapeHtml(returnTo)} in ${REDIRECT_DELAY_SECONDS}\u00a0seconds\u2026

` : ""; const headExtra = returnTo ? `` : ""; return page( `

Subscribed ✓

${msg}

${redirectHtml}
Publication
Record
`, styleHref, headExtra, ); } function renderError(message: string, styleHref: string): string { return page( `

Error

${escapeHtml(message)}

`, styleHref, ); } function page(body: string, styleHref: string, headExtra = ""): string { return ` Sequoia · Subscribe ${headExtra}
${body}
`; } function escapeHtml(text: string): string { return text .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """); } export default subscribe;