chore: cleaned up server
a75bc683
9 file(s) · +156 −242
| 1 | 1 | import { useEffect, useState } from "react"; |
|
| 2 | 2 | import MarkdownIt from "markdown-it"; |
|
| 3 | + | import { OWNER_DID, PDS_URL } from "@/data/constants"; |
|
| 3 | 4 | ||
| 4 | 5 | const md = new MarkdownIt({ |
|
| 5 | 6 | html: false, |
|
| 6 | 7 | linkify: true, |
|
| 7 | 8 | typographer: true, |
|
| 8 | 9 | }); |
|
| 9 | - | ||
| 10 | - | const DID = "did:plc:ia2zdnhjaokf5lazhxrmj6eu"; |
|
| 11 | - | const PDS_URL = "https://polybius.social"; |
|
| 12 | 10 | ||
| 13 | 11 | interface Document { |
|
| 14 | 12 | uri: string; |
|
| 34 | 32 | const documentsData = await fetch( |
|
| 35 | 33 | `${PDS_URL}/xrpc/com.atproto.repo.listRecords?` + |
|
| 36 | 34 | new URLSearchParams({ |
|
| 37 | - | repo: DID, |
|
| 35 | + | repo: OWNER_DID, |
|
| 38 | 36 | collection: "site.standard.document", |
|
| 39 | 37 | limit: "20", |
|
| 40 | 38 | }), |
|
| 1 | + | // ATProto configuration |
|
| 2 | + | export const OWNER_DID = "did:plc:ia2zdnhjaokf5lazhxrmj6eu"; |
|
| 3 | + | export const PDS_URL = "https://polybius.social"; |
|
| 4 | + | ||
| 1 | 5 | export const MENU_LINKS = [ |
|
| 2 | 6 | { |
|
| 3 | 7 | title: "Home", |
|
| 37 | 41 | export const SOCIAL_LINKS = { |
|
| 38 | 42 | github: "https://github.com/stevedylandev", |
|
| 39 | 43 | telegram: "https://telegram.me/stevedylandev", |
|
| 40 | - | atproto: "https://pdsls.dev/at://did:plc:ia2zdnhjaokf5lazhxrmj6eu", |
|
| 44 | + | atproto: `https://pdsls.dev/at://${OWNER_DID}`, |
|
| 41 | 45 | linkedin: "https://www.linkedin.com/in/steve-simkins/", |
|
| 42 | 46 | photos: "https://steve.photo", |
|
| 43 | 47 | website: "/", |
|
| 1 | + | import { OWNER_DID } from "@/data/constants"; |
|
| 2 | + | ||
| 1 | 3 | export const prerender = true; |
|
| 4 | + | ||
| 5 | + | const PUBLICATION_RKEY = "self"; |
|
| 2 | 6 | ||
| 3 | 7 | export async function GET() { |
|
| 4 | - | // Your DID from PDSPost.tsx |
|
| 5 | - | const DID = "did:plc:ia2zdnhjaokf5lazhxrmj6eu"; |
|
| 6 | - | ||
| 7 | - | // This should be the rkey of your actual site.standard.publication record |
|
| 8 | - | // You'll need to create this record in your PDS first |
|
| 9 | - | const PUBLICATION_RKEY = "self"; |
|
| 10 | - | ||
| 11 | - | const atUri = `at://${DID}/site.standard.publication/${PUBLICATION_RKEY}`; |
|
| 8 | + | const atUri = `at://${OWNER_DID}/site.standard.publication/${PUBLICATION_RKEY}`; |
|
| 12 | 9 | ||
| 13 | 10 | return new Response(atUri, { |
|
| 14 | 11 | status: 200, |
| 2 | 2 | import PageLayout from "@/layouts/Base"; |
|
| 3 | 3 | import MarkdownIt from "markdown-it"; |
|
| 4 | 4 | import { ReplyContainer } from "@/components/now/ReplyContainer"; |
|
| 5 | + | import { OWNER_DID, PDS_URL } from "@/data/constants"; |
|
| 5 | 6 | ||
| 6 | 7 | export const prerender = false; |
|
| 7 | 8 | ||
| 10 | 11 | linkify: true, |
|
| 11 | 12 | typographer: true, |
|
| 12 | 13 | }); |
|
| 13 | - | ||
| 14 | - | const DID = "did:plc:ia2zdnhjaokf5lazhxrmj6eu"; |
|
| 15 | - | const PDS_URL = "https://polybius.social"; |
|
| 16 | 14 | ||
| 17 | 15 | const { slug } = Astro.params; |
|
| 18 | 16 | ||
| 35 | 33 | const listResponse = await fetch( |
|
| 36 | 34 | `${PDS_URL}/xrpc/com.atproto.repo.listRecords?` + |
|
| 37 | 35 | new URLSearchParams({ |
|
| 38 | - | repo: DID, |
|
| 36 | + | repo: OWNER_DID, |
|
| 39 | 37 | collection: "site.standard.document", |
|
| 40 | 38 | limit: "100", |
|
| 41 | 39 | }), |
|
| 77 | 75 | const documentResponse = await fetch( |
|
| 78 | 76 | `${PDS_URL}/xrpc/com.atproto.repo.getRecord?` + |
|
| 79 | 77 | new URLSearchParams({ |
|
| 80 | - | repo: DID, |
|
| 78 | + | repo: OWNER_DID, |
|
| 81 | 79 | collection: "site.standard.document", |
|
| 82 | 80 | rkey: slug, |
|
| 83 | 81 | }), |
|
| 89 | 87 | title = doc.title || "Post"; |
|
| 90 | 88 | description = doc.content?.markdown?.slice(0, 160) || description; |
|
| 91 | 89 | publishedAt = new Date(doc.publishedAt).toLocaleDateString(); |
|
| 92 | - | atUri = `at://${DID}/site.standard.document/${slug}`; |
|
| 90 | + | atUri = `at://${OWNER_DID}/site.standard.document/${slug}`; |
|
| 93 | 91 | ||
| 94 | 92 | if (doc.content && doc.content.markdown) { |
|
| 95 | 93 | contentHTML = md.render(doc.content.markdown); |
|
| 1 | 1 | import rss from "@astrojs/rss"; |
|
| 2 | 2 | import sanitizeHtml from "sanitize-html"; |
|
| 3 | 3 | import MarkdownIt from "markdown-it"; |
|
| 4 | + | import { OWNER_DID, PDS_URL } from "@/data/constants"; |
|
| 4 | 5 | ||
| 5 | 6 | export const prerender = false; |
|
| 6 | - | ||
| 7 | - | const DID = "did:plc:ia2zdnhjaokf5lazhxrmj6eu"; |
|
| 8 | - | const PDS_URL = "https://polybius.social"; |
|
| 9 | 7 | ||
| 10 | 8 | const md = new MarkdownIt({ |
|
| 11 | 9 | html: true, |
|
| 40 | 38 | const response = await fetch( |
|
| 41 | 39 | `${PDS_URL}/xrpc/com.atproto.repo.listRecords?` + |
|
| 42 | 40 | new URLSearchParams({ |
|
| 43 | - | repo: DID, |
|
| 41 | + | repo: OWNER_DID, |
|
| 44 | 42 | collection: "site.standard.document", |
|
| 45 | 43 | limit: "50", |
|
| 46 | 44 | }), |
|
| 1 | 1 | import * as jose from "jose"; |
|
| 2 | - | import { type DPoPKeyPair, createDPoPProof, extractDPoPNonce } from "./dpop"; |
|
| 2 | + | import { |
|
| 3 | + | type DPoPKeyPair, |
|
| 4 | + | createDPoPProof, |
|
| 5 | + | extractDPoPNonce, |
|
| 6 | + | generateDPoPKeyPair, |
|
| 7 | + | } from "./dpop"; |
|
| 8 | + | import { storeAuthState, getAndDeleteAuthState } from "./session"; |
|
| 3 | 9 | ||
| 4 | 10 | export interface OAuthServerMetadata { |
|
| 5 | 11 | issuer: string; |
|
| 264 | 270 | return { |
|
| 265 | 271 | tokenResponse, |
|
| 266 | 272 | dpopNonce: newNonce || dpopNonce || "", |
|
| 273 | + | }; |
|
| 274 | + | } |
|
| 275 | + | ||
| 276 | + | export interface OAuthFlowConfig { |
|
| 277 | + | pdsUrl: string; |
|
| 278 | + | clientId: string; |
|
| 279 | + | redirectUri: string; |
|
| 280 | + | scope: string; |
|
| 281 | + | } |
|
| 282 | + | ||
| 283 | + | export interface InitiateOAuthResult { |
|
| 284 | + | authUrl: string; |
|
| 285 | + | state: string; |
|
| 286 | + | } |
|
| 287 | + | ||
| 288 | + | /** |
|
| 289 | + | * Initiates an OAuth login flow - generates PKCE, DPoP keypair, sends PAR, and stores auth state |
|
| 290 | + | */ |
|
| 291 | + | export async function initiateOAuthFlow( |
|
| 292 | + | kv: KVNamespace, |
|
| 293 | + | config: OAuthFlowConfig, |
|
| 294 | + | ): Promise<InitiateOAuthResult> { |
|
| 295 | + | const metadata = await fetchOAuthMetadata(config.pdsUrl); |
|
| 296 | + | const pkce = await generatePKCE(); |
|
| 297 | + | const state = generateState(); |
|
| 298 | + | const dpopKeyPair = await generateDPoPKeyPair(); |
|
| 299 | + | ||
| 300 | + | const { parResponse, dpopNonce } = await sendPAR( |
|
| 301 | + | metadata, |
|
| 302 | + | config.clientId, |
|
| 303 | + | config.redirectUri, |
|
| 304 | + | state, |
|
| 305 | + | pkce, |
|
| 306 | + | dpopKeyPair, |
|
| 307 | + | config.scope, |
|
| 308 | + | ); |
|
| 309 | + | ||
| 310 | + | await storeAuthState(kv, state, pkce.codeVerifier, dpopKeyPair, dpopNonce); |
|
| 311 | + | ||
| 312 | + | const authUrl = buildAuthorizationUrl( |
|
| 313 | + | metadata, |
|
| 314 | + | parResponse.request_uri, |
|
| 315 | + | config.clientId, |
|
| 316 | + | ); |
|
| 317 | + | ||
| 318 | + | return { authUrl, state }; |
|
| 319 | + | } |
|
| 320 | + | ||
| 321 | + | export interface CompleteOAuthResult { |
|
| 322 | + | tokenResponse: TokenResponse; |
|
| 323 | + | dpopKeyPair: DPoPKeyPair; |
|
| 324 | + | dpopNonce: string; |
|
| 325 | + | } |
|
| 326 | + | ||
| 327 | + | /** |
|
| 328 | + | * Completes an OAuth callback - validates state, exchanges code for tokens |
|
| 329 | + | */ |
|
| 330 | + | export async function completeOAuthFlow( |
|
| 331 | + | kv: KVNamespace, |
|
| 332 | + | pdsUrl: string, |
|
| 333 | + | code: string, |
|
| 334 | + | state: string, |
|
| 335 | + | clientId: string, |
|
| 336 | + | redirectUri: string, |
|
| 337 | + | ): Promise<CompleteOAuthResult | null> { |
|
| 338 | + | const authState = await getAndDeleteAuthState(kv, state); |
|
| 339 | + | if (!authState) { |
|
| 340 | + | return null; |
|
| 341 | + | } |
|
| 342 | + | ||
| 343 | + | const metadata = await fetchOAuthMetadata(pdsUrl); |
|
| 344 | + | ||
| 345 | + | const { tokenResponse, dpopNonce } = await exchangeCodeForTokens( |
|
| 346 | + | metadata, |
|
| 347 | + | code, |
|
| 348 | + | authState.codeVerifier, |
|
| 349 | + | clientId, |
|
| 350 | + | redirectUri, |
|
| 351 | + | authState.dpopKeyPair, |
|
| 352 | + | authState.dpopNonce, |
|
| 353 | + | ); |
|
| 354 | + | ||
| 355 | + | return { |
|
| 356 | + | tokenResponse, |
|
| 357 | + | dpopKeyPair: authState.dpopKeyPair, |
|
| 358 | + | dpopNonce, |
|
| 267 | 359 | }; |
|
| 268 | 360 | } |
|
| 269 | 361 | ||
| 1 | 1 | import { Hono } from "hono"; |
|
| 2 | - | import { generateDPoPKeyPair } from "../lib/dpop"; |
|
| 3 | 2 | import { |
|
| 4 | 3 | fetchOAuthMetadata, |
|
| 5 | - | generatePKCE, |
|
| 6 | - | generateState, |
|
| 7 | - | sendPAR, |
|
| 8 | - | buildAuthorizationUrl, |
|
| 9 | - | exchangeCodeForTokens, |
|
| 4 | + | refreshAccessToken, |
|
| 5 | + | initiateOAuthFlow, |
|
| 6 | + | completeOAuthFlow, |
|
| 10 | 7 | } from "../lib/oauth"; |
|
| 11 | 8 | import { |
|
| 12 | - | storeAuthState, |
|
| 13 | - | getAndDeleteAuthState, |
|
| 14 | 9 | createSession, |
|
| 15 | 10 | getSession, |
|
| 16 | 11 | deleteSession, |
|
| 20 | 15 | isTokenExpired, |
|
| 21 | 16 | updateSession, |
|
| 22 | 17 | } from "../lib/session"; |
|
| 23 | - | import { refreshAccessToken } from "../lib/oauth"; |
|
| 24 | 18 | ||
| 25 | 19 | interface Env { |
|
| 26 | 20 | SESSIONS: KVNamespace; |
|
| 57 | 51 | const clientId = `${c.env.API_URL}/auth/client-metadata.json`; |
|
| 58 | 52 | const redirectUri = `${c.env.API_URL}/auth/callback`; |
|
| 59 | 53 | ||
| 60 | - | // Fetch OAuth metadata from PDS |
|
| 61 | - | const metadata = await fetchOAuthMetadata(c.env.PDS_URL); |
|
| 62 | - | ||
| 63 | - | // Generate PKCE and state |
|
| 64 | - | const pkce = await generatePKCE(); |
|
| 65 | - | const state = generateState(); |
|
| 66 | - | ||
| 67 | - | // Generate DPoP keypair |
|
| 68 | - | const dpopKeyPair = await generateDPoPKeyPair(); |
|
| 69 | - | ||
| 70 | - | // Send PAR request |
|
| 71 | - | const { parResponse, dpopNonce } = await sendPAR( |
|
| 72 | - | metadata, |
|
| 54 | + | const { authUrl } = await initiateOAuthFlow(c.env.SESSIONS, { |
|
| 55 | + | pdsUrl: c.env.PDS_URL, |
|
| 73 | 56 | clientId, |
|
| 74 | 57 | redirectUri, |
|
| 75 | - | state, |
|
| 76 | - | pkce, |
|
| 77 | - | dpopKeyPair, |
|
| 78 | - | "atproto transition:generic", |
|
| 79 | - | ); |
|
| 80 | - | ||
| 81 | - | // Store auth state in KV |
|
| 82 | - | await storeAuthState( |
|
| 83 | - | c.env.SESSIONS, |
|
| 84 | - | state, |
|
| 85 | - | pkce.codeVerifier, |
|
| 86 | - | dpopKeyPair, |
|
| 87 | - | dpopNonce, |
|
| 88 | - | ); |
|
| 58 | + | scope: "atproto transition:generic", |
|
| 59 | + | }); |
|
| 89 | 60 | ||
| 90 | - | // Build authorization URL and redirect |
|
| 91 | - | const authUrl = buildAuthorizationUrl( |
|
| 92 | - | metadata, |
|
| 93 | - | parResponse.request_uri, |
|
| 94 | - | clientId, |
|
| 95 | - | ); |
|
| 96 | 61 | return c.redirect(authUrl); |
|
| 97 | 62 | } catch (error) { |
|
| 98 | 63 | console.error("Login error:", error); |
|
| 120 | 85 | return c.redirect(`${c.env.CLIENT_URL}/now?error=missing_params`); |
|
| 121 | 86 | } |
|
| 122 | 87 | ||
| 123 | - | // Retrieve and validate auth state |
|
| 124 | - | const authState = await getAndDeleteAuthState(c.env.SESSIONS, state); |
|
| 125 | - | if (!authState) { |
|
| 126 | - | return c.redirect(`${c.env.CLIENT_URL}/now?error=invalid_state`); |
|
| 127 | - | } |
|
| 128 | - | ||
| 129 | 88 | const clientId = `${c.env.API_URL}/auth/client-metadata.json`; |
|
| 130 | 89 | const redirectUri = `${c.env.API_URL}/auth/callback`; |
|
| 131 | 90 | ||
| 132 | - | // Fetch OAuth metadata |
|
| 133 | - | const metadata = await fetchOAuthMetadata(c.env.PDS_URL); |
|
| 134 | - | ||
| 135 | - | // Exchange code for tokens |
|
| 136 | - | const { tokenResponse, dpopNonce } = await exchangeCodeForTokens( |
|
| 137 | - | metadata, |
|
| 91 | + | const oauthResult = await completeOAuthFlow( |
|
| 92 | + | c.env.SESSIONS, |
|
| 93 | + | c.env.PDS_URL, |
|
| 138 | 94 | code, |
|
| 139 | - | authState.codeVerifier, |
|
| 95 | + | state, |
|
| 140 | 96 | clientId, |
|
| 141 | 97 | redirectUri, |
|
| 142 | - | authState.dpopKeyPair, |
|
| 143 | - | authState.dpopNonce, |
|
| 144 | 98 | ); |
|
| 99 | + | ||
| 100 | + | if (!oauthResult) { |
|
| 101 | + | return c.redirect(`${c.env.CLIENT_URL}/now?error=invalid_state`); |
|
| 102 | + | } |
|
| 103 | + | ||
| 104 | + | const { tokenResponse, dpopKeyPair, dpopNonce } = oauthResult; |
|
| 145 | 105 | ||
| 146 | 106 | // CRITICAL: Only allow the site owner |
|
| 147 | 107 | if (tokenResponse.sub !== c.env.ALLOWED_DID) { |
|
| 154 | 114 | c.env.SESSIONS, |
|
| 155 | 115 | tokenResponse.access_token, |
|
| 156 | 116 | tokenResponse.refresh_token || "", |
|
| 157 | - | authState.dpopKeyPair, |
|
| 117 | + | dpopKeyPair, |
|
| 158 | 118 | dpopNonce, |
|
| 159 | 119 | tokenResponse.sub, |
|
| 160 | 120 | tokenResponse.expires_in, |
|
| 1 | 1 | import { Hono } from "hono"; |
|
| 2 | - | import { generateDPoPKeyPair } from "../lib/dpop"; |
|
| 3 | 2 | import { |
|
| 4 | 3 | fetchOAuthMetadata, |
|
| 5 | - | generatePKCE, |
|
| 6 | - | generateState, |
|
| 7 | - | sendPAR, |
|
| 8 | - | buildAuthorizationUrl, |
|
| 9 | - | exchangeCodeForTokens, |
|
| 10 | 4 | refreshAccessToken, |
|
| 5 | + | initiateOAuthFlow, |
|
| 6 | + | completeOAuthFlow, |
|
| 11 | 7 | } from "../lib/oauth"; |
|
| 12 | 8 | import { |
|
| 13 | - | storeAuthState, |
|
| 14 | - | getAndDeleteAuthState, |
|
| 15 | 9 | createSession, |
|
| 16 | 10 | getSession, |
|
| 17 | 11 | deleteSession, |
|
| 136 | 130 | return c.redirect(`${c.env.CLIENT_URL}/now?error=invalid_handle`); |
|
| 137 | 131 | } |
|
| 138 | 132 | ||
| 139 | - | // Fetch OAuth metadata from user's PDS |
|
| 140 | - | const metadata = await fetchOAuthMetadata(pdsUrl); |
|
| 141 | - | ||
| 142 | - | // Generate PKCE and state |
|
| 143 | - | const pkce = await generatePKCE(); |
|
| 144 | - | const state = generateState(); |
|
| 145 | - | ||
| 146 | - | // Generate DPoP keypair |
|
| 147 | - | const dpopKeyPair = await generateDPoPKeyPair(); |
|
| 148 | - | ||
| 149 | - | // Send PAR request |
|
| 150 | - | const { parResponse, dpopNonce } = await sendPAR( |
|
| 151 | - | metadata, |
|
| 133 | + | const { authUrl, state } = await initiateOAuthFlow(c.env.SESSIONS, { |
|
| 134 | + | pdsUrl, |
|
| 152 | 135 | clientId, |
|
| 153 | 136 | redirectUri, |
|
| 154 | - | state, |
|
| 155 | - | pkce, |
|
| 156 | - | dpopKeyPair, |
|
| 157 | - | "atproto repo:site.standard.document.comment?action=create", |
|
| 158 | - | ); |
|
| 159 | - | ||
| 160 | - | // Store auth state in KV with returnTo URL and PDS URL |
|
| 161 | - | await storeAuthState( |
|
| 162 | - | c.env.SESSIONS, |
|
| 163 | - | state, |
|
| 164 | - | pkce.codeVerifier, |
|
| 165 | - | dpopKeyPair, |
|
| 166 | - | dpopNonce, |
|
| 167 | - | ); |
|
| 137 | + | scope: "atproto repo:site.standard.document.comment?action=create", |
|
| 138 | + | }); |
|
| 168 | 139 | ||
| 169 | 140 | // Store returnTo and pdsUrl separately to retrieve after callback |
|
| 170 | 141 | await c.env.SESSIONS.put( |
|
| 178 | 149 | { expirationTtl: 600 }, // 10 minutes |
|
| 179 | 150 | ); |
|
| 180 | 151 | ||
| 181 | - | // Build authorization URL and redirect |
|
| 182 | - | const authUrl = buildAuthorizationUrl( |
|
| 183 | - | metadata, |
|
| 184 | - | parResponse.request_uri, |
|
| 185 | - | clientId, |
|
| 186 | - | ); |
|
| 187 | 152 | return c.redirect(authUrl); |
|
| 188 | 153 | } catch (error) { |
|
| 189 | 154 | console.error("Guest login error:", error); |
|
| 211 | 176 | return c.redirect(`${c.env.CLIENT_URL}/now?error=missing_params`); |
|
| 212 | 177 | } |
|
| 213 | 178 | ||
| 214 | - | // Retrieve and validate auth state |
|
| 215 | - | const authState = await getAndDeleteAuthState(c.env.SESSIONS, state); |
|
| 216 | - | if (!authState) { |
|
| 217 | - | return c.redirect(`${c.env.CLIENT_URL}/now?error=invalid_state`); |
|
| 218 | - | } |
|
| 219 | - | ||
| 220 | 179 | // Get return URL and PDS URL |
|
| 221 | 180 | const returnTo = |
|
| 222 | 181 | (await c.env.SESSIONS.get(`guest_return:${state}`)) || "/now"; |
|
| 231 | 190 | const clientId = `${c.env.API_URL}/guest-auth/client-metadata.json`; |
|
| 232 | 191 | const redirectUri = `${c.env.API_URL}/guest-auth/callback`; |
|
| 233 | 192 | ||
| 234 | - | // Fetch OAuth metadata from user's PDS |
|
| 235 | - | const metadata = await fetchOAuthMetadata(pdsUrl); |
|
| 236 | - | ||
| 237 | - | // Exchange code for tokens |
|
| 238 | - | const { tokenResponse, dpopNonce } = await exchangeCodeForTokens( |
|
| 239 | - | metadata, |
|
| 193 | + | const oauthResult = await completeOAuthFlow( |
|
| 194 | + | c.env.SESSIONS, |
|
| 195 | + | pdsUrl, |
|
| 240 | 196 | code, |
|
| 241 | - | authState.codeVerifier, |
|
| 197 | + | state, |
|
| 242 | 198 | clientId, |
|
| 243 | 199 | redirectUri, |
|
| 244 | - | authState.dpopKeyPair, |
|
| 245 | - | authState.dpopNonce, |
|
| 246 | 200 | ); |
|
| 247 | 201 | ||
| 202 | + | if (!oauthResult) { |
|
| 203 | + | return c.redirect(`${c.env.CLIENT_URL}/now?error=invalid_state`); |
|
| 204 | + | } |
|
| 205 | + | ||
| 206 | + | const { tokenResponse, dpopKeyPair, dpopNonce } = oauthResult; |
|
| 207 | + | ||
| 248 | 208 | // For guests, allow any ATProto account (no DID check) |
|
| 249 | 209 | // Create session with a "guest_" prefix to differentiate from admin sessions |
|
| 250 | 210 | const sessionId = await createSession( |
|
| 251 | 211 | c.env.SESSIONS, |
|
| 252 | 212 | tokenResponse.access_token, |
|
| 253 | 213 | tokenResponse.refresh_token || "", |
|
| 254 | - | authState.dpopKeyPair, |
|
| 214 | + | dpopKeyPair, |
|
| 255 | 215 | dpopNonce, |
|
| 256 | 216 | tokenResponse.sub, |
|
| 257 | 217 | tokenResponse.expires_in, |
|
| 20 | 20 | ||
| 21 | 21 | const now = new Hono<{ Bindings: Env }>(); |
|
| 22 | 22 | ||
| 23 | - | const DID = "did:plc:ia2zdnhjaokf5lazhxrmj6eu"; |
|
| 24 | 23 | const PDS_URL = "https://polybius.social"; |
|
| 25 | 24 | ||
| 26 | 25 | // Helper function to get session for both admin and guest users |
|
| 220 | 219 | const response = await fetch( |
|
| 221 | 220 | `${PDS_URL}/xrpc/com.atproto.repo.listRecords?` + |
|
| 222 | 221 | new URLSearchParams({ |
|
| 223 | - | repo: DID, |
|
| 222 | + | repo: c.env.ALLOWED_DID, |
|
| 224 | 223 | collection: "site.standard.document", |
|
| 225 | 224 | limit: "50", |
|
| 226 | 225 | }), |
|
| 548 | 547 | const rkey = parts[4]; |
|
| 549 | 548 | ||
| 550 | 549 | // Resolve the DID to find the PDS endpoint |
|
| 551 | - | const didDoc = await fetch(`https://plc.directory/${did}`).then((r) => |
|
| 550 | + | const didDoc = (await fetch(`https://plc.directory/${did}`).then((r) => |
|
| 552 | 551 | r.json(), |
|
| 553 | - | ); |
|
| 552 | + | )) as { |
|
| 553 | + | service?: Array<{ type: string; serviceEndpoint: string }>; |
|
| 554 | + | }; |
|
| 554 | 555 | ||
| 555 | 556 | // Find the PDS service endpoint |
|
| 556 | 557 | const pdsService = didDoc.service?.find( |
|
| 557 | - | (s: any) => s.type === "AtprotoPersonalDataServer", |
|
| 558 | + | (s) => s.type === "AtprotoPersonalDataServer", |
|
| 558 | 559 | ); |
|
| 559 | 560 | ||
| 560 | 561 | if (!pdsService?.serviceEndpoint) { |
|
| 581 | 582 | return null; |
|
| 582 | 583 | } |
|
| 583 | 584 | ||
| 584 | - | const data = await recordResponse.json(); |
|
| 585 | + | const data = (await recordResponse.json()) as { |
|
| 586 | + | value: Record<string, unknown>; |
|
| 587 | + | cid: string; |
|
| 588 | + | }; |
|
| 585 | 589 | return { |
|
| 586 | 590 | ...data.value, |
|
| 587 | 591 | uri: ref.uri, |
|
| 597 | 601 | const validComments = comments.filter((comment) => comment !== null); |
|
| 598 | 602 | ||
| 599 | 603 | return c.json({ replies: validComments }); |
|
| 600 | - | } catch (error) { |
|
| 601 | - | console.error("Error fetching comments:", error); |
|
| 602 | - | return c.json({ replies: [] }); |
|
| 603 | - | } |
|
| 604 | - | }); |
|
| 605 | - | ||
| 606 | - | // Get comments for a post (legacy endpoint) |
|
| 607 | - | now.get("/replies/:uri", async (c) => { |
|
| 608 | - | try { |
|
| 609 | - | const encodedUri = c.req.param("uri"); |
|
| 610 | - | const uri = decodeURIComponent(encodedUri); |
|
| 611 | - | ||
| 612 | - | // Get the parent post to find its CID for matching |
|
| 613 | - | const getRecordUrl = |
|
| 614 | - | `${PDS_URL}/xrpc/com.atproto.repo.getRecord?` + |
|
| 615 | - | new URLSearchParams({ |
|
| 616 | - | repo: uri.split("/")[2], // Extract DID from URI |
|
| 617 | - | collection: uri.split("/")[3], // Extract collection |
|
| 618 | - | rkey: uri.split("/")[4], // Extract rkey |
|
| 619 | - | }); |
|
| 620 | - | ||
| 621 | - | const parentResponse = await fetch(getRecordUrl); |
|
| 622 | - | if (!parentResponse.ok) { |
|
| 623 | - | console.error("Failed to fetch parent post:", parentResponse.status); |
|
| 624 | - | return c.json({ replies: [] }); |
|
| 625 | - | } |
|
| 626 | - | ||
| 627 | - | const parentData = (await parentResponse.json()) as { cid: string }; |
|
| 628 | - | const parentCid = parentData.cid; |
|
| 629 | - | ||
| 630 | - | // Fetch all site.standard.document.comment records |
|
| 631 | - | // Note: This is a simple implementation that fetches all comments |
|
| 632 | - | // In production, you'd want to filter by parent URI server-side if possible |
|
| 633 | - | const listUrl = |
|
| 634 | - | `${PDS_URL}/xrpc/com.atproto.repo.listRecords?` + |
|
| 635 | - | new URLSearchParams({ |
|
| 636 | - | repo: DID, |
|
| 637 | - | collection: "site.standard.document.comment", |
|
| 638 | - | limit: "100", |
|
| 639 | - | }); |
|
| 640 | - | ||
| 641 | - | const response = await fetch(listUrl); |
|
| 642 | - | ||
| 643 | - | if (!response.ok) { |
|
| 644 | - | console.error("Failed to fetch comments:", response.status); |
|
| 645 | - | return c.json({ replies: [] }); |
|
| 646 | - | } |
|
| 647 | - | ||
| 648 | - | interface CommentRecord { |
|
| 649 | - | uri: string; |
|
| 650 | - | cid: string; |
|
| 651 | - | value: { |
|
| 652 | - | parent?: { uri?: string; cid?: string }; |
|
| 653 | - | content: string; |
|
| 654 | - | createdAt: string; |
|
| 655 | - | author?: { handle?: string; displayName?: string; avatar?: string }; |
|
| 656 | - | }; |
|
| 657 | - | indexedAt?: string; |
|
| 658 | - | } |
|
| 659 | - | ||
| 660 | - | const data = (await response.json()) as { records: CommentRecord[] }; |
|
| 661 | - | ||
| 662 | - | // Filter comments that match the parent URI |
|
| 663 | - | const replies: any[] = []; |
|
| 664 | - | ||
| 665 | - | for (const record of data.records) { |
|
| 666 | - | const comment = record.value; |
|
| 667 | - | // Check if this comment's parent matches our URI |
|
| 668 | - | if (comment.parent?.uri === uri || comment.parent?.cid === parentCid) { |
|
| 669 | - | replies.push({ |
|
| 670 | - | uri: record.uri, |
|
| 671 | - | cid: record.cid, |
|
| 672 | - | author: { |
|
| 673 | - | did: record.uri.split("/")[2], |
|
| 674 | - | handle: comment.author?.handle || record.uri.split("/")[2], |
|
| 675 | - | displayName: comment.author?.displayName, |
|
| 676 | - | avatar: comment.author?.avatar, |
|
| 677 | - | }, |
|
| 678 | - | record: { |
|
| 679 | - | text: comment.content, |
|
| 680 | - | createdAt: comment.createdAt, |
|
| 681 | - | }, |
|
| 682 | - | indexedAt: record.indexedAt || comment.createdAt, |
|
| 683 | - | replyCount: 0, |
|
| 684 | - | likeCount: 0, |
|
| 685 | - | }); |
|
| 686 | - | } |
|
| 687 | - | } |
|
| 688 | - | ||
| 689 | - | // Sort by creation date |
|
| 690 | - | replies.sort( |
|
| 691 | - | (a, b) => |
|
| 692 | - | new Date(a.record.createdAt).getTime() - |
|
| 693 | - | new Date(b.record.createdAt).getTime(), |
|
| 694 | - | ); |
|
| 695 | - | ||
| 696 | - | return c.json({ replies }); |
|
| 697 | 604 | } catch (error) { |
|
| 698 | 605 | console.error("Error fetching comments:", error); |
|
| 699 | 606 | return c.json({ replies: [] }); |
|