See the best posts from any Bluesky account
0
fork

Configure Feed

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

Use transition:generic OAuth scope, simplify viewer state, cache Vite in Docker

Switch to transition:generic scope to work around PDS rpc:…?aud= limitation.
Remove try/catch around viewer state fetch (let errors propagate instead of
silently hiding them). Use @let instead of @set in Edge template. Add Vite
cache mount to Dockerfile build step.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

+19 -22
+2 -1
Dockerfile
··· 17 17 18 18 # Copy source and build the Adonis app 19 19 COPY . . 20 - RUN node ace build 20 + RUN --mount=type=cache,id=vite,target=/app/node_modules/.vite \ 21 + node ace build 21 22 22 23 # --------------------------------------------------------------------------- 23 24 # Stage 2: production image — install prod-only deps inside build output
+15 -18
app/controllers/profile_controller.ts
··· 307 307 } 308 308 } 309 309 310 - // 6. Fetch viewer state (like/repost status) if the user is authenticated 310 + // 6. Fetch viewer state (like/repost status) if the user is authenticated. 311 + // getPosts is an AppView endpoint — the OAuth agent proxies through the 312 + // PDS, which requires an rpc:…?aud= scope that PDS'es don't support yet. 313 + // TODO: re-enable once PDS'es support granular rpc scopes. 311 314 let viewer: Record<string, { likeUri: string | null; repostUri: string | null }> | null = null 312 315 const cidMap = new Map<string, string>() 313 316 314 317 const isAuthenticated = await ctx.auth.check() 315 318 if (isAuthenticated) { 316 - try { 317 - const viewerDid = ctx.auth.user!.did 318 - const agent = await this.atprotoOAuthService.getAgent(viewerDid) 319 - const postUris = posts.map((p) => p.postUri) 320 - if (postUris.length > 0) { 321 - const res = await agent.app.bsky.feed.getPosts({ uris: postUris }) 322 - viewer = {} 323 - for (const post of res.data.posts) { 324 - viewer[post.uri] = { 325 - likeUri: post.viewer?.like ?? null, 326 - repostUri: post.viewer?.repost ?? null, 327 - } 328 - cidMap.set(post.uri, post.cid) 319 + const viewerDid = ctx.auth.user!.did 320 + const agent = await this.atprotoOAuthService.getAgent(viewerDid) 321 + const postUris = posts.map((p) => p.postUri) 322 + if (postUris.length > 0) { 323 + const res = await agent.app.bsky.feed.getPosts({ uris: postUris }) 324 + viewer = {} 325 + for (const post of res.data.posts) { 326 + viewer[post.uri] = { 327 + likeUri: post.viewer?.like ?? null, 328 + repostUri: post.viewer?.repost ?? null, 329 329 } 330 + cidMap.set(post.uri, post.cid) 330 331 } 331 - } catch (err) { 332 - // Silently fall back — don't let auth issues break the page 333 - logger.debug({ err }, 'Failed to fetch viewer state for profile page') 334 - viewer = null 335 332 } 336 333 } 337 334
+1 -2
app/services/atproto_oauth.ts
··· 14 14 client_name: 'favs.blue', 15 15 client_uri: appUrl, 16 16 redirect_uris: [`${appUrl}/oauth/callback`] as [string], 17 - scope: 18 - 'atproto repo:app.bsky.feed.like?action=create&action=delete repo:app.bsky.feed.repost?action=create&action=delete', 17 + scope: 'atproto transition:generic', 19 18 grant_types: ['authorization_code', 'refresh_token'] as ['authorization_code', 'refresh_token'], 20 19 response_types: ['code'] as ['code'], 21 20 token_endpoint_auth_method: 'none' as const,
+1 -1
resources/views/pages/profile/show.edge
··· 190 190 191 191 <div class="flex items-baseline justify-between flex-wrap gap-4 mt-3"> 192 192 @if(viewer) 193 - @set('viewerState', viewer[post.postUri]) 193 + @let(viewerState = viewer[post.postUri]) 194 194 <div 195 195 x-data="postEngagement" 196 196 data-post-uri="{{ post.postUri }}"