your personal website on atproto - mirror blento.app
25
fork

Configure Feed

Select the types of activity you want to include in your feed.

some fixes

Florian 30e874bb 1bb79750

+180 -96
+2 -5
src/lib/atproto/auth.svelte.ts
··· 62 62 export async function login(handle: ActorIdentifier) { 63 63 const { oauthLogin } = await import('./server/oauth.remote'); 64 64 65 - const returnTo = location.pathname + location.search; 66 - 67 65 if (handle.startsWith('did:')) { 68 66 if (handle.length < 6) throw new Error('DID must be at least 6 characters'); 69 67 } else if (handle.includes('.') && handle.length > 3) { ··· 75 73 throw new Error('Please provide a valid handle or DID.'); 76 74 } 77 75 78 - const { url } = await oauthLogin({ handle, returnTo }); 76 + const { url } = await oauthLogin({ handle }); 79 77 window.location.assign(url); 80 78 } 81 79 82 80 export async function signup() { 83 81 const { oauthLogin } = await import('./server/oauth.remote'); 84 82 85 - const returnTo = location.pathname + location.search; 86 - const { url } = await oauthLogin({ signup: true, returnTo }); 83 + const { url } = await oauthLogin({ signup: true }); 87 84 window.location.assign(url); 88 85 } 89 86
+2 -12
src/lib/atproto/server/oauth.remote.ts
··· 15 15 export const oauthLogin = command( 16 16 v.object({ 17 17 handle: v.optional(v.pipe(v.string(), v.minLength(3))), 18 - signup: v.optional(v.boolean()), 19 - returnTo: v.optional(v.string()) 18 + signup: v.optional(v.boolean()) 20 19 }), 21 20 async (input) => { 22 - const { platform, cookies } = getRequestEvent(); 21 + const { platform } = getRequestEvent(); 23 22 24 23 try { 25 24 const oauth = createOAuthClient(platform?.env, getDomain()); ··· 33 32 scope: scopes.join(' '), 34 33 prompt: input.signup ? 'create' : undefined 35 34 }); 36 - 37 - // Store return path in a cookie so the callback can redirect back 38 - if (input.returnTo) { 39 - cookies.set('oauth_return_to', encodeURIComponent(input.returnTo), { 40 - path: '/', 41 - httpOnly: true, 42 - maxAge: 600 // 10 minutes 43 - }); 44 - } 45 35 46 36 return { url: url.toString() }; 47 37 } catch (e) {
+11 -2
src/lib/atproto/server/repo.remote.ts
··· 56 56 rkey: rkeySchema 57 57 }), 58 58 async (input) => { 59 - const { locals } = getRequestEvent(); 59 + const { locals, platform } = getRequestEvent(); 60 60 if (!locals.client || !locals.did) error(401, 'Not authenticated'); 61 61 62 + const rkey = input.rkey || 'self'; 62 63 const response = await locals.client.post('com.atproto.repo.deleteRecord', { 63 64 input: { 64 65 collection: input.collection as `${string}.${string}.${string}`, 65 66 repo: locals.did, 66 - rkey: input.rkey || 'self' 67 + rkey 67 68 } 68 69 }); 70 + 71 + // Tell contrail the record is gone — notify() re-fetches and removes on 404 72 + const db = platform?.env?.DB; 73 + if (db && response.ok) { 74 + await ensureInit(db); 75 + const uri = `at://${locals.did}/${input.collection}/${rkey}`; 76 + await contrail.notify(uri, db).catch(() => {}); 77 + } 69 78 70 79 return { ok: response.ok }; 71 80 }
+2 -1
src/lib/contrail/config.ts
··· 15 15 }, 16 16 profiles: [ 17 17 'app.bsky.actor.profile', 18 - { collection: 'site.standard.publication', rkey: 'blento.self' } 18 + { collection: 'site.standard.publication', rkey: 'blento.self' }, 19 + { collection: 'app.nearhorizon.actor.pronouns', rkey: 'self' } 19 20 ] 20 21 };
+151 -62
src/lib/website/load.ts
··· 43 43 }; 44 44 45 45 /** 46 - * Extract a bsky-style profile and publication from contrail profile entries. 46 + * Extract a bsky-style profile, publication, and pronouns from contrail profile entries. 47 47 */ 48 48 function extractProfileData( 49 49 did: string, ··· 51 51 ): { 52 52 profile: AppBskyActorDefs.ProfileViewDetailed; 53 53 publication: WebsiteData['publication'] | undefined; 54 + pronounsRecord: PronounsRecord | undefined; 54 55 } { 55 56 let bskyRecord: Record<string, unknown> | undefined; 56 57 let pubRecord: Record<string, unknown> | undefined; 58 + let pronounsValue: Record<string, unknown> | undefined; 57 59 let handle = did; 58 60 59 61 for (const p of profiles) { ··· 66 68 } 67 69 if (p.collection === 'site.standard.publication' && record) { 68 70 pubRecord = record; 71 + } 72 + if (p.collection === 'app.nearhorizon.actor.pronouns' && record) { 73 + pronounsValue = record; 69 74 } 70 75 } 71 76 ··· 84 89 avatar 85 90 } as AppBskyActorDefs.ProfileViewDetailed; 86 91 87 - const publication = pubRecord 88 - ? (pubRecord as WebsiteData['publication']) 89 - : undefined; 92 + const publication = pubRecord ? (pubRecord as WebsiteData['publication']) : undefined; 93 + 94 + const pronounsRecord = pronounsValue ? ({ value: pronounsValue } as PronounsRecord) : undefined; 95 + 96 + return { profile, publication, pronounsRecord }; 97 + } 90 98 91 - return { profile, publication }; 99 + /** 100 + * Fetch only the profile bundle (bsky profile + publication + pronouns) from contrail. 101 + * Used by single-card routes that don't need the full card list. 102 + */ 103 + async function loadProfilesFromContrail( 104 + actor: ActorIdentifier, 105 + db: D1Database 106 + ): Promise<ContrailProfile[] | null> { 107 + try { 108 + const client = getServerClient(db); 109 + const res = await client.get('app.blento.getProfile', { params: { actor } }); 110 + if (!res.ok) return null; 111 + return (res.data.profiles ?? []) as ContrailProfile[]; 112 + } catch (e) { 113 + console.error('Contrail getProfile failed', e); 114 + return null; 115 + } 116 + } 117 + 118 + /** 119 + * Fetch a single card from contrail by DID + rkey. 120 + */ 121 + async function loadCardFromContrail( 122 + did: Did, 123 + rkey: string, 124 + db: D1Database 125 + ): Promise<Item | null> { 126 + try { 127 + const client = getServerClient(db); 128 + const uri = `at://${did}/app.blento.card/${rkey}` as const; 129 + const res = await client.get('app.blento.card.getRecord', { 130 + params: { uri } 131 + }); 132 + if (!res.ok) return null; 133 + return { ...(res.data.record as object) } as Item; 134 + } catch (e) { 135 + console.error('Contrail card.getRecord failed', e); 136 + return null; 137 + } 92 138 } 93 139 94 140 /** ··· 168 214 const extracted = extractProfileData(did, contrailData.profiles); 169 215 profile = extracted.profile; 170 216 publication = extracted.publication; 171 - 172 - // Pronouns still from PDS (not in contrail) 173 - pronounsRecord = await getRecord({ 174 - did, 175 - collection: 'app.nearhorizon.actor.pronouns', 176 - rkey: 'self' 177 - }).catch(() => undefined) as PronounsRecord | undefined; 217 + pronounsRecord = extracted.pronounsRecord; 178 218 } else { 179 219 // Fallback: no D1 available (e.g. vite dev) — use PDS directly 180 220 const [cardRecords, pageRecs, mainPub, prof, pronouns] = await Promise.all([ ··· 207 247 208 248 // If no publication found from contrail profiles, check page records 209 249 if (!publication) { 210 - const pubFromPages = pageRecords.find( 211 - (v) => parseUri(v.uri)?.rkey === 'blento.' + page 212 - ); 250 + const pubFromPages = pageRecords.find((v) => parseUri(v.uri)?.rkey === 'blento.' + page); 213 251 publication = pubFromPages?.value as WebsiteData['publication'] | undefined; 214 252 } 215 253 ··· 218 256 description: profile?.description 219 257 } as WebsiteData['publication']; 220 258 221 - const additionalData = await loadAdditionalData( 222 - cards, 223 - { did, handle, cache, platform }, 224 - env 225 - ); 259 + const additionalData = await loadAdditionalData(cards, { did, handle, cache, platform }, env); 226 260 227 261 return checkData({ 228 262 page: 'blento.' + page, ··· 243 277 handle: ActorIdentifier, 244 278 rkey: string, 245 279 cache: CacheService | undefined, 246 - env?: Record<string, string | undefined> 280 + env?: Record<string, string | undefined>, 281 + platform?: App.Platform 247 282 ): Promise<WebsiteData> { 248 283 if (!handle) throw error(404); 249 284 if (handle === 'favicon.ico') throw error(404); ··· 251 286 const did = await resolveDid(handle); 252 287 if (!did) throw error(404); 253 288 254 - const [cardRecord, profile, pronounsRecord] = await Promise.all([ 255 - getRecord({ 256 - did, 257 - collection: 'app.blento.card', 258 - rkey 259 - }).catch(() => undefined), 260 - getDetailedProfile({ did }), 261 - getRecord({ 262 - did, 263 - collection: 'app.nearhorizon.actor.pronouns', 264 - rkey: 'self' 265 - }).catch(() => undefined) 266 - ]); 289 + const db = platform?.env?.DB; 290 + 291 + let cardValue: Item | undefined; 292 + let profile: WebsiteData['profile'] | undefined; 293 + let publication: WebsiteData['publication'] | undefined; 294 + let pronounsRecord: PronounsRecord | undefined; 295 + 296 + if (db) { 297 + const [card, profiles] = await Promise.all([ 298 + loadCardFromContrail(did, rkey, db), 299 + loadProfilesFromContrail(handle, db) 300 + ]); 301 + 302 + if (!card) throw error(404, 'Card not found'); 303 + cardValue = card; 304 + 305 + if (profiles) { 306 + const extracted = extractProfileData(did, profiles); 307 + profile = extracted.profile; 308 + publication = extracted.publication; 309 + pronounsRecord = extracted.pronounsRecord; 310 + } 311 + } 312 + 313 + if (!cardValue) { 314 + // Fallback: no D1 (e.g. vite dev) — fetch from PDS 315 + const [cardRecord, prof, pronouns] = await Promise.all([ 316 + getRecord({ did, collection: 'app.blento.card', rkey }).catch(() => undefined), 317 + getDetailedProfile({ did }), 318 + getRecord({ 319 + did, 320 + collection: 'app.nearhorizon.actor.pronouns', 321 + rkey: 'self' 322 + }).catch(() => undefined) 323 + ]); 267 324 268 - if (!cardRecord?.value) { 269 - throw error(404, 'Card not found'); 325 + if (!cardRecord?.value) throw error(404, 'Card not found'); 326 + cardValue = cardRecord.value as Item; 327 + profile = prof; 328 + pronounsRecord = pronouns as PronounsRecord | undefined; 270 329 } 271 330 272 - const card = migrateCard(structuredClone(cardRecord.value) as Item); 331 + if (!profile) throw error(404); 332 + 333 + const card = migrateCard(structuredClone(cardValue)); 273 334 const page = card.page ?? 'blento.self'; 274 335 275 - const publication = await getRecord({ 276 - did, 277 - collection: page === 'blento.self' ? 'site.standard.publication' : 'app.blento.page', 278 - rkey: page 279 - }).catch(() => undefined); 336 + // For non-self pages, publication comes from app.blento.page (not in contrail profiles). 337 + if (!publication || page !== 'blento.self') { 338 + const pubRecord = await getRecord({ 339 + did, 340 + collection: page === 'blento.self' ? 'site.standard.publication' : 'app.blento.page', 341 + rkey: page 342 + }).catch(() => undefined); 343 + if (pubRecord?.value) publication = pubRecord.value as WebsiteData['publication']; 344 + } 280 345 281 346 const cards = [card]; 282 347 const resolvedHandle = profile?.handle || (isHandle(handle) ? handle : did); 283 348 284 349 const additionalData = await loadAdditionalData( 285 350 cards, 286 - { did, handle: resolvedHandle, cache }, 351 + { did, handle: resolvedHandle, cache, platform }, 287 352 env 288 353 ); 289 354 ··· 293 358 did, 294 359 cards, 295 360 publication: 296 - publication?.value ?? 361 + publication ?? 297 362 ({ 298 363 name: profile?.displayName || profile?.handle, 299 364 description: profile?.description ··· 301 366 additionalData, 302 367 profile, 303 368 pronouns: formatPronouns(pronounsRecord, profile), 304 - pronounsRecord: pronounsRecord as PronounsRecord | undefined, 369 + pronounsRecord, 305 370 updatedAt: Date.now(), 306 371 version: 1 307 372 }; ··· 312 377 type: string, 313 378 cardData: Record<string, unknown>, 314 379 cache: CacheService | undefined, 315 - env?: Record<string, string | undefined> 380 + env?: Record<string, string | undefined>, 381 + platform?: App.Platform 316 382 ): Promise<WebsiteData> { 317 383 if (!handle) throw error(404); 318 384 if (handle === 'favicon.ico') throw error(404); ··· 325 391 const did = await resolveDid(handle); 326 392 if (!did) throw error(404); 327 393 328 - const [publication, profile, pronounsRecord] = await Promise.all([ 329 - getRecord({ 330 - did, 331 - collection: 'site.standard.publication', 332 - rkey: 'blento.self' 333 - }).catch(() => undefined), 334 - getDetailedProfile({ did }), 335 - getRecord({ 336 - did, 337 - collection: 'app.nearhorizon.actor.pronouns', 338 - rkey: 'self' 339 - }).catch(() => undefined) 340 - ]); 394 + const db = platform?.env?.DB; 395 + 396 + let profile: WebsiteData['profile'] | undefined; 397 + let publication: WebsiteData['publication'] | undefined; 398 + let pronounsRecord: PronounsRecord | undefined; 399 + 400 + if (db) { 401 + const profiles = await loadProfilesFromContrail(handle, db); 402 + if (profiles) { 403 + const extracted = extractProfileData(did, profiles); 404 + profile = extracted.profile; 405 + publication = extracted.publication; 406 + pronounsRecord = extracted.pronounsRecord; 407 + } 408 + } 409 + 410 + if (!profile) { 411 + const [pubRecord, prof, pronouns] = await Promise.all([ 412 + getRecord({ 413 + did, 414 + collection: 'site.standard.publication', 415 + rkey: 'blento.self' 416 + }).catch(() => undefined), 417 + getDetailedProfile({ did }), 418 + getRecord({ 419 + did, 420 + collection: 'app.nearhorizon.actor.pronouns', 421 + rkey: 'self' 422 + }).catch(() => undefined) 423 + ]); 424 + profile = prof; 425 + publication = pubRecord?.value as WebsiteData['publication'] | undefined; 426 + pronounsRecord = pronouns as PronounsRecord | undefined; 427 + } 428 + 429 + if (!profile) throw error(404); 341 430 342 431 const card = createEmptyCard('blento.self'); 343 432 card.cardType = type; ··· 353 442 354 443 const additionalData = await loadAdditionalData( 355 444 cards, 356 - { did, handle: resolvedHandle, cache }, 445 + { did, handle: resolvedHandle, cache, platform }, 357 446 env 358 447 ); 359 448 ··· 363 452 did, 364 453 cards, 365 454 publication: 366 - publication?.value ?? 455 + publication ?? 367 456 ({ 368 457 name: profile?.displayName || profile?.handle, 369 458 description: profile?.description ··· 371 460 additionalData, 372 461 profile, 373 462 pronouns: formatPronouns(pronounsRecord, profile), 374 - pronounsRecord: pronounsRecord as PronounsRecord | undefined, 463 + pronounsRecord, 375 464 updatedAt: Date.now(), 376 465 version: 1 377 466 });
+8 -11
src/routes/(auth)/oauth/callback/+server.ts
··· 2 2 import { createOAuthClient } from '$lib/atproto/server/oauth'; 3 3 import { setSignedCookie } from '$lib/atproto/server/signed-cookie'; 4 4 import { scopes } from '$lib/atproto/server/scopes'; 5 + import { getDetailedProfile } from '$lib/atproto/methods'; 5 6 import { dev } from '$app/environment'; 7 + import type { Did } from '@atcute/lexicons'; 6 8 import type { RequestHandler } from './$types'; 7 9 8 10 export const GET: RequestHandler = async ({ url, platform, cookies, request }) => { 9 11 const customDomain = request.headers.get('X-Custom-Domain')?.toLowerCase() || undefined; 10 12 const oauth = createOAuthClient(platform?.env, customDomain); 11 13 14 + let did: Did; 12 15 try { 13 16 const { session } = await oauth.callback(url.searchParams); 17 + did = session.did; 14 18 15 19 const cookieOpts = { 16 20 path: '/', ··· 20 24 maxAge: 60 * 60 * 24 * 180 // 180 days 21 25 }; 22 26 23 - setSignedCookie(cookies, 'did', session.did, cookieOpts); 27 + setSignedCookie(cookies, 'did', did, cookieOpts); 24 28 setSignedCookie(cookies, 'scope', scopes.join(' '), cookieOpts); 25 29 } catch (e) { 26 30 console.error('OAuth callback failed:', e); 27 31 redirect(303, '/?error=auth_failed'); 28 32 } 29 33 30 - const returnTo = cookies.get('oauth_return_to'); 31 - if (returnTo) { 32 - cookies.delete('oauth_return_to', { path: '/' }); 33 - const decoded = decodeURIComponent(returnTo); 34 - if (decoded.startsWith('/') && !decoded.startsWith('//')) { 35 - redirect(303, decoded); 36 - } 37 - } 38 - 39 - redirect(303, '/'); 34 + const profile = await getDetailedProfile({ did }).catch(() => undefined); 35 + const actor = profile?.handle && profile.handle !== 'handle.invalid' ? profile.handle : did; 36 + redirect(303, `/${actor}`); 40 37 };
+1 -1
src/routes/[[actor=actor]]/card/[rkey]/+page.server.ts
··· 12 12 throw error(404, 'Page not found'); 13 13 } 14 14 15 - return await loadCardData(actor, params.rkey, cache, env); 15 + return await loadCardData(actor, params.rkey, cache, env, platform); 16 16 }
+2 -1
src/routes/[[actor=actor]]/card/[rkey]/type/[type]/+page.server.ts
··· 54 54 params.type, 55 55 getCardDataFromSearchParams(url.searchParams), 56 56 cache, 57 - env 57 + env, 58 + platform 58 59 ); 59 60 }
+1 -1
src/routes/[[actor=actor]]/og-new.png/+server.ts
··· 64 64 height: 630, 65 65 deviceScaleFactor: 2 66 66 }, 67 - waitForTimeout: 1000 67 + waitForTimeout: 3000 68 68 }) 69 69 } 70 70 );