diff --git a/packages/bsky/src/api/app/bsky/actor/getSuggestions.ts b/packages/bsky/src/api/app/bsky/actor/getSuggestions.ts index df580521af9..3bfeccfa28e 100644 --- a/packages/bsky/src/api/app/bsky/actor/getSuggestions.ts +++ b/packages/bsky/src/api/app/bsky/actor/getSuggestions.ts @@ -43,7 +43,7 @@ const skeleton = async ( ): Promise => { const { db } = ctx const { viewer } = params - const alreadyIncluded = parseCursor(params.cursor) + const alreadyIncluded = parseCursor(params.cursor) // @NOTE handles bad cursor e.g. on appview swap const { ref } = db.db.dynamic const suggestions = await db.db .selectFrom('suggested_follow') diff --git a/packages/bsky/src/api/app/bsky/actor/searchActors.ts b/packages/bsky/src/api/app/bsky/actor/searchActors.ts index bcc30a6bd66..82f0327ef89 100644 --- a/packages/bsky/src/api/app/bsky/actor/searchActors.ts +++ b/packages/bsky/src/api/app/bsky/actor/searchActors.ts @@ -15,6 +15,7 @@ export default function (server: Server, ctx: AppContext) { let results: string[] let resCursor: string | undefined if (ctx.searchAgent) { + // @NOTE cursors wont change on appview swap const res = await ctx.searchAgent.api.app.bsky.unspecced.searchActorsSkeleton({ q: query, diff --git a/packages/bsky/src/api/app/bsky/feed/getActorFeeds.ts b/packages/bsky/src/api/app/bsky/feed/getActorFeeds.ts index bc4ecd7caac..266839c7711 100644 --- a/packages/bsky/src/api/app/bsky/feed/getActorFeeds.ts +++ b/packages/bsky/src/api/app/bsky/feed/getActorFeeds.ts @@ -10,6 +10,12 @@ export default function (server: Server, ctx: AppContext) { handler: async ({ auth, params }) => { const { actor, limit, cursor } = params const viewer = auth.credentials.iss + if (TimeCidKeyset.clearlyBad(cursor)) { + return { + encoding: 'application/json', + body: { feeds: [] }, + } + } const db = ctx.db.getReplica() const actorService = ctx.services.actor(db) diff --git a/packages/bsky/src/api/app/bsky/feed/getActorLikes.ts b/packages/bsky/src/api/app/bsky/feed/getActorLikes.ts index 151e9086ca9..48d6437e494 100644 --- a/packages/bsky/src/api/app/bsky/feed/getActorLikes.ts +++ b/packages/bsky/src/api/app/bsky/feed/getActorLikes.ts @@ -67,6 +67,10 @@ const skeleton = async ( throw new InvalidRequestError('Profile not found') } + if (FeedKeyset.clearlyBad(cursor)) { + return { params, feedItems: [] } + } + let feedItemsQb = feedService .selectFeedItemQb() .innerJoin('like', 'like.subject', 'feed_item.uri') diff --git a/packages/bsky/src/api/app/bsky/feed/getAuthorFeed.ts b/packages/bsky/src/api/app/bsky/feed/getAuthorFeed.ts index f2163cd251b..6c783efdd0c 100644 --- a/packages/bsky/src/api/app/bsky/feed/getAuthorFeed.ts +++ b/packages/bsky/src/api/app/bsky/feed/getAuthorFeed.ts @@ -81,6 +81,10 @@ export const skeleton = async ( } } + if (FeedKeyset.clearlyBad(cursor)) { + return { params, feedItems: [] } + } + // defaults to posts, reposts, and replies let feedItemsQb = feedService .selectFeedItemQb() diff --git a/packages/bsky/src/api/app/bsky/feed/getFeed.ts b/packages/bsky/src/api/app/bsky/feed/getFeed.ts index 7620153121f..9d99f699781 100644 --- a/packages/bsky/src/api/app/bsky/feed/getFeed.ts +++ b/packages/bsky/src/api/app/bsky/feed/getFeed.ts @@ -43,6 +43,7 @@ export default function (server: Server, ctx: AppContext) { authorization: req.headers['authorization'], 'accept-language': req.headers['accept-language'], }) + // @NOTE feed cursors should not be affected by appview swap const { timerSkele, timerHydr, resHeaders, ...result } = await getFeed( { ...params, viewer }, { diff --git a/packages/bsky/src/api/app/bsky/feed/getLikes.ts b/packages/bsky/src/api/app/bsky/feed/getLikes.ts index 8df916f29c9..2d59656a517 100644 --- a/packages/bsky/src/api/app/bsky/feed/getLikes.ts +++ b/packages/bsky/src/api/app/bsky/feed/getLikes.ts @@ -41,6 +41,10 @@ const skeleton = async ( const { uri, cid, limit, cursor } = params const { ref } = db.db.dynamic + if (TimeCidKeyset.clearlyBad(cursor)) { + return { params, likes: [] } + } + let builder = db.db .selectFrom('like') .where('like.subject', '=', uri) diff --git a/packages/bsky/src/api/app/bsky/feed/getListFeed.ts b/packages/bsky/src/api/app/bsky/feed/getListFeed.ts index 8af7764a6b7..478d9b08efa 100644 --- a/packages/bsky/src/api/app/bsky/feed/getListFeed.ts +++ b/packages/bsky/src/api/app/bsky/feed/getListFeed.ts @@ -56,6 +56,10 @@ export const skeleton = async ( const { db } = ctx const { ref } = db.db.dynamic + if (FeedKeyset.clearlyBad(cursor)) { + return { params, feedItems: [] } + } + const keyset = new FeedKeyset(ref('post.sortAt'), ref('post.cid')) const sortFrom = keyset.unpack(cursor)?.primary diff --git a/packages/bsky/src/api/app/bsky/feed/getRepostedBy.ts b/packages/bsky/src/api/app/bsky/feed/getRepostedBy.ts index e84bb745b42..7d28014a7b7 100644 --- a/packages/bsky/src/api/app/bsky/feed/getRepostedBy.ts +++ b/packages/bsky/src/api/app/bsky/feed/getRepostedBy.ts @@ -46,6 +46,10 @@ const skeleton = async ( const { limit, cursor, uri, cid } = params const { ref } = db.db.dynamic + if (TimeCidKeyset.clearlyBad(cursor)) { + return { params, repostedBy: [] } + } + let builder = db.db .selectFrom('repost') .where('repost.subject', '=', uri) diff --git a/packages/bsky/src/api/app/bsky/feed/getSuggestedFeeds.ts b/packages/bsky/src/api/app/bsky/feed/getSuggestedFeeds.ts index b72a191c9aa..b96ae2722fa 100644 --- a/packages/bsky/src/api/app/bsky/feed/getSuggestedFeeds.ts +++ b/packages/bsky/src/api/app/bsky/feed/getSuggestedFeeds.ts @@ -6,8 +6,8 @@ export default function (server: Server, ctx: AppContext) { server.app.bsky.feed.getSuggestedFeeds({ auth: ctx.authVerifier.standardOptional, handler: async ({ auth }) => { + // @NOTE ignores cursor, doesn't matter for appview swap const viewer = auth.credentials.iss - const db = ctx.db.getReplica() const feedService = ctx.services.feed(db) const actorService = ctx.services.actor(db) diff --git a/packages/bsky/src/api/app/bsky/feed/getTimeline.ts b/packages/bsky/src/api/app/bsky/feed/getTimeline.ts index 3b6fbe70a33..05ef505ea04 100644 --- a/packages/bsky/src/api/app/bsky/feed/getTimeline.ts +++ b/packages/bsky/src/api/app/bsky/feed/getTimeline.ts @@ -61,6 +61,10 @@ export const skeleton = async ( return skeletonLimit1(params, ctx) } + if (FeedKeyset.clearlyBad(cursor)) { + return { params, feedItems: [] } + } + const keyset = new FeedKeyset(ref('feed_item.sortAt'), ref('feed_item.cid')) const sortFrom = keyset.unpack(cursor)?.primary diff --git a/packages/bsky/src/api/app/bsky/feed/searchPosts.ts b/packages/bsky/src/api/app/bsky/feed/searchPosts.ts index 9598c6ff88c..3ac0b3f9477 100644 --- a/packages/bsky/src/api/app/bsky/feed/searchPosts.ts +++ b/packages/bsky/src/api/app/bsky/feed/searchPosts.ts @@ -49,6 +49,7 @@ const skeleton = async ( params: Params, ctx: Context, ): Promise => { + // @NOTE cursors wont change on appview swap const res = await ctx.searchAgent.api.app.bsky.unspecced.searchPostsSkeleton({ q: params.q, cursor: params.cursor, diff --git a/packages/bsky/src/api/app/bsky/graph/getBlocks.ts b/packages/bsky/src/api/app/bsky/graph/getBlocks.ts index 518fd2d62ec..adc28752a25 100644 --- a/packages/bsky/src/api/app/bsky/graph/getBlocks.ts +++ b/packages/bsky/src/api/app/bsky/graph/getBlocks.ts @@ -9,6 +9,13 @@ export default function (server: Server, ctx: AppContext) { handler: async ({ params, auth }) => { const { limit, cursor } = params const requester = auth.credentials.iss + if (TimeCidKeyset.clearlyBad(cursor)) { + return { + encoding: 'application/json', + body: { blocks: [] }, + } + } + const db = ctx.db.getReplica() const { ref } = db.db.dynamic diff --git a/packages/bsky/src/api/app/bsky/graph/getFollowers.ts b/packages/bsky/src/api/app/bsky/graph/getFollowers.ts index 9fb199c7563..bf22b2be6cb 100644 --- a/packages/bsky/src/api/app/bsky/graph/getFollowers.ts +++ b/packages/bsky/src/api/app/bsky/graph/getFollowers.ts @@ -52,6 +52,10 @@ const skeleton = async ( throw new InvalidRequestError(`Actor not found: ${actor}`) } + if (TimeCidKeyset.clearlyBad(cursor)) { + return { params, followers: [], subject } + } + let followersReq = db.db .selectFrom('follow') .where('follow.subjectDid', '=', subject.did) diff --git a/packages/bsky/src/api/app/bsky/graph/getFollows.ts b/packages/bsky/src/api/app/bsky/graph/getFollows.ts index 2195824b696..8d294b27354 100644 --- a/packages/bsky/src/api/app/bsky/graph/getFollows.ts +++ b/packages/bsky/src/api/app/bsky/graph/getFollows.ts @@ -52,6 +52,10 @@ const skeleton = async ( throw new InvalidRequestError(`Actor not found: ${actor}`) } + if (TimeCidKeyset.clearlyBad(cursor)) { + return { params, follows: [], creator } + } + let followsReq = db.db .selectFrom('follow') .where('follow.creator', '=', creator.did) diff --git a/packages/bsky/src/api/app/bsky/graph/getList.ts b/packages/bsky/src/api/app/bsky/graph/getList.ts index 08d3f725663..82007b45388 100644 --- a/packages/bsky/src/api/app/bsky/graph/getList.ts +++ b/packages/bsky/src/api/app/bsky/graph/getList.ts @@ -49,6 +49,10 @@ const skeleton = async ( throw new InvalidRequestError(`List not found: ${list}`) } + if (TimeCidKeyset.clearlyBad(cursor)) { + return { params, list: listRes, listItems: [] } + } + let itemsReq = graphService .getListItemsQb() .where('list_item.listUri', '=', list) diff --git a/packages/bsky/src/api/app/bsky/graph/getListBlocks.ts b/packages/bsky/src/api/app/bsky/graph/getListBlocks.ts index b5a6e97986d..6fb6df55e82 100644 --- a/packages/bsky/src/api/app/bsky/graph/getListBlocks.ts +++ b/packages/bsky/src/api/app/bsky/graph/getListBlocks.ts @@ -45,6 +45,10 @@ const skeleton = async ( const { limit, cursor, viewer } = params const { ref } = db.db.dynamic + if (TimeCidKeyset.clearlyBad(cursor)) { + return { params, listInfos: [] } + } + let listsReq = graphService .getListsQb(viewer) .whereExists( diff --git a/packages/bsky/src/api/app/bsky/graph/getListMutes.ts b/packages/bsky/src/api/app/bsky/graph/getListMutes.ts index f5f14844e32..8baa509ae47 100644 --- a/packages/bsky/src/api/app/bsky/graph/getListMutes.ts +++ b/packages/bsky/src/api/app/bsky/graph/getListMutes.ts @@ -9,6 +9,13 @@ export default function (server: Server, ctx: AppContext) { handler: async ({ params, auth }) => { const { limit, cursor } = params const requester = auth.credentials.iss + if (TimeCidKeyset.clearlyBad(cursor)) { + return { + encoding: 'application/json', + body: { lists: [] }, + } + } + const db = ctx.db.getReplica() const { ref } = db.db.dynamic diff --git a/packages/bsky/src/api/app/bsky/graph/getLists.ts b/packages/bsky/src/api/app/bsky/graph/getLists.ts index 888963b3fa3..40bb903f5b4 100644 --- a/packages/bsky/src/api/app/bsky/graph/getLists.ts +++ b/packages/bsky/src/api/app/bsky/graph/getLists.ts @@ -10,6 +10,13 @@ export default function (server: Server, ctx: AppContext) { handler: async ({ params, auth }) => { const { actor, limit, cursor } = params const requester = auth.credentials.iss + if (TimeCidKeyset.clearlyBad(cursor)) { + return { + encoding: 'application/json', + body: { lists: [] }, + } + } + const db = ctx.db.getReplica() const { ref } = db.db.dynamic diff --git a/packages/bsky/src/api/app/bsky/graph/getMutes.ts b/packages/bsky/src/api/app/bsky/graph/getMutes.ts index 2481e8de240..827573258bd 100644 --- a/packages/bsky/src/api/app/bsky/graph/getMutes.ts +++ b/packages/bsky/src/api/app/bsky/graph/getMutes.ts @@ -9,6 +9,13 @@ export default function (server: Server, ctx: AppContext) { handler: async ({ params, auth }) => { const { limit, cursor } = params const requester = auth.credentials.iss + if (TimeCidKeyset.clearlyBad(cursor)) { + return { + encoding: 'application/json', + body: { mutes: [] }, + } + } + const db = ctx.db.getReplica() const { ref } = db.db.dynamic diff --git a/packages/bsky/src/api/app/bsky/notification/listNotifications.ts b/packages/bsky/src/api/app/bsky/notification/listNotifications.ts index c0de1925120..ce5274a9da7 100644 --- a/packages/bsky/src/api/app/bsky/notification/listNotifications.ts +++ b/packages/bsky/src/api/app/bsky/notification/listNotifications.ts @@ -51,6 +51,9 @@ const skeleton = async ( if (params.seenAt) { throw new InvalidRequestError('The seenAt parameter is unsupported') } + if (NotifsKeyset.clearlyBad(cursor)) { + return { params, notifs: [] } + } let notifBuilder = db.db .selectFrom('notification as notif') .where('notif.did', '=', viewer) diff --git a/packages/bsky/src/api/app/bsky/unspecced/getPopularFeedGenerators.ts b/packages/bsky/src/api/app/bsky/unspecced/getPopularFeedGenerators.ts index b8456d111a4..c15a5242b0f 100644 --- a/packages/bsky/src/api/app/bsky/unspecced/getPopularFeedGenerators.ts +++ b/packages/bsky/src/api/app/bsky/unspecced/getPopularFeedGenerators.ts @@ -12,11 +12,17 @@ export default function (server: Server, ctx: AppContext) { handler: async ({ auth, params }) => { const { limit, cursor, query } = params const requester = auth.credentials.iss + if (LikeCountKeyset.clearlyBad(cursor)) { + return { + encoding: 'application/json', + body: { feeds: [] }, + } + } + const db = ctx.db.getReplica() const { ref } = db.db.dynamic const feedService = ctx.services.feed(db) const actorService = ctx.services.actor(db) - let inner = db.db .selectFrom('feed_generator') .select([ diff --git a/packages/bsky/src/api/app/bsky/unspecced/getTimelineSkeleton.ts b/packages/bsky/src/api/app/bsky/unspecced/getTimelineSkeleton.ts index f45b657af1e..74f02a3df5a 100644 --- a/packages/bsky/src/api/app/bsky/unspecced/getTimelineSkeleton.ts +++ b/packages/bsky/src/api/app/bsky/unspecced/getTimelineSkeleton.ts @@ -12,6 +12,7 @@ export default function (server: Server, ctx: AppContext) { const feedService = ctx.services.feed(db) const viewer = auth.credentials.iss + // @NOTE bad cursor during appview swap handled within skeleton() const result = await skeleton({ ...params, viewer }, { db, feedService }) return { diff --git a/packages/bsky/src/db/pagination.ts b/packages/bsky/src/db/pagination.ts index b38c69e5ada..f08702cb003 100644 --- a/packages/bsky/src/db/pagination.ts +++ b/packages/bsky/src/db/pagination.ts @@ -27,6 +27,9 @@ export abstract class GenericKeyset { abstract labelResult(result: R): LR abstract labeledResultToCursor(labeled: LR): Cursor abstract cursorToLabeledResult(cursor: Cursor): LR + static clearlyBad(cursor?: string) { + return cursor !== undefined && !cursor.includes('::') + } packFromResult(results: R | R[]): string | undefined { const result = Array.isArray(results) ? results.at(-1) : results if (!result) return diff --git a/packages/bsky/tests/views/notifications.test.ts b/packages/bsky/tests/views/notifications.test.ts index fad620288af..376ead163fc 100644 --- a/packages/bsky/tests/views/notifications.test.ts +++ b/packages/bsky/tests/views/notifications.test.ts @@ -296,4 +296,13 @@ describe('notification views', () => { ), ) }) + + it('fails open on clearly bad cursor.', async () => { + const { data: notifs } = + await agent.api.app.bsky.notification.listNotifications( + { cursor: 'bad' }, + { headers: await network.serviceHeaders(alice) }, + ) + expect(notifs).toEqual({ notifications: [] }) + }) }) diff --git a/packages/bsky/tests/views/timeline.test.ts b/packages/bsky/tests/views/timeline.test.ts index 014bb5339ce..dd9b89535c8 100644 --- a/packages/bsky/tests/views/timeline.test.ts +++ b/packages/bsky/tests/views/timeline.test.ts @@ -298,4 +298,12 @@ describe('timeline views', () => { ), ) }) + + it('fails open on clearly bad cursor.', async () => { + const { data: timeline } = await agent.api.app.bsky.feed.getTimeline( + { cursor: 'bad' }, + { headers: await network.serviceHeaders(alice) }, + ) + expect(timeline).toEqual({ feed: [] }) + }) })