diff --git a/src/lib/api/feed/custom.ts b/src/lib/api/feed/custom.ts index 75c4a62..6f3f690 100644 --- a/src/lib/api/feed/custom.ts +++ b/src/lib/api/feed/custom.ts @@ -52,16 +52,30 @@ export class CustomFeedAPI implements FeedAPI { cursor: string | undefined limit: number }): Promise { - // Only use the microcosm batch path when there's no session. Personalized - // feeds (e.g. For You) require a service-auth JWT forwarded to the feed - // generator, which the batch path doesn't yet mint — without it the - // generator rejects the request. Logged-in always uses the AppView path - // below, which forwards auth correctly. - if (MICROCOSM_ENABLED && !this.agent.did) { + // Microcosm batch path: Slingshot proxies the feed generator's skeleton and + // fetches all post records in one request. For personalized feeds we forward + // a service-auth token (aud = the feed generator's DID) so the generator can + // identify the viewer — same as the AppView would. + if (MICROCOSM_ENABLED) { try { const page = await buildCustomFeed(this.params.feed, { + viewerDid: this.agent.did, limit, cursor, + getAuthorization: this.agent.did + ? async (feedGenDid: string) => { + try { + const {data} = + await this.agent.com.atproto.server.getServiceAuth({ + aud: feedGenDid, + lxm: 'app.bsky.feed.getFeedSkeleton', + }) + return data.token + } catch { + return undefined + } + } + : undefined, }) if (page.feed.length) { return {cursor: page.cursor, feed: page.feed} diff --git a/src/lib/microcosm/feed.ts b/src/lib/microcosm/feed.ts index 7a30b8c..562f37b 100644 --- a/src/lib/microcosm/feed.ts +++ b/src/lib/microcosm/feed.ts @@ -153,14 +153,34 @@ async function resolveFeedGenDid( */ export async function buildCustomFeed( feedUri: string, - opts: {viewerDid?: string; limit?: number; cursor?: string} = {}, + opts: { + viewerDid?: string + limit?: number + cursor?: string + /** + * Mint a service-auth token scoped to the feed generator (aud = its DID, + * lxm = app.bsky.feed.getFeedSkeleton). Called after the gen DID is known. + * Required for personalized feeds; omit when logged out. + */ + getAuthorization?: (feedGenDid: string) => Promise + } = {}, signal?: AbortSignal, ): Promise { const feedGenDid = await resolveFeedGenDid(feedUri, signal) if (!feedGenDid) return {feed: []} + const authorization = opts.getAuthorization + ? await opts.getAuthorization(feedGenDid).catch(() => undefined) + : undefined + const batch = await hydrateFeedSkeleton( - {feedGenDid, feed: feedUri, limit: opts.limit ?? 30, cursor: opts.cursor}, + { + feedGenDid, + feed: feedUri, + limit: opts.limit ?? 30, + cursor: opts.cursor, + authorization, + }, signal, ).catch(() => undefined) if (!batch) return {feed: []} diff --git a/src/lib/microcosm/slingshot.ts b/src/lib/microcosm/slingshot.ts index 816873f..8ff15da 100644 --- a/src/lib/microcosm/slingshot.ts +++ b/src/lib/microcosm/slingshot.ts @@ -117,6 +117,11 @@ export async function hydrateFeedSkeleton( feed: string limit?: number cursor?: string + /** + * Service-auth token whose `aud` matches the feed generator DID. Required + * for personalized feeds; forwarded to the generator by Slingshot. + */ + authorization?: string }, signal?: AbortSignal, ): Promise<{ @@ -127,6 +132,7 @@ export async function hydrateFeedSkeleton( const payload = { xrpc: 'app.bsky.feed.getFeedSkeleton', atproto_proxy: `${args.feedGenDid}#bsky_fg`, + ...(args.authorization ? {authorization: args.authorization} : {}), params: { feed: args.feed, limit: args.limit ?? 30,