[SDK] Validate and brand the persisted account did (#11387)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Samuel Newman
2026-08-13 22:26:22 +03:00
committed by GitHub
parent 9fe0084874
commit 3677c98873
46 changed files with 154 additions and 205 deletions
+78 -53
View File
@@ -1,5 +1,11 @@
import {Client, lexParse, type XrpcRequestParams} from '@atproto/lex'
import {
type Client,
type XrpcRequestParams,
XrpcResponseError,
} from '@atproto/lex'
import {PUBLIC_APPVIEW} from '#/lib/constants'
import {createLexClient} from '#/lib/lexClient'
import {
getAppLanguageAsContentLanguage,
getContentLanguages,
@@ -96,68 +102,87 @@ export class CustomFeedAPI implements FeedAPI {
}
}
// HACK
// we want feeds to give language-specific results immediately when a
// logged-out user changes their language. this comes with two problems:
// 1. not all languages have content, and
// 2. our public caching layer isnt correctly busting against the accept-language header
// for now we handle both of these with a manual workaround
// -prf
async function loggedOutFetch({
feed,
limit,
cursor,
}: {
feed: string
limit: number
cursor?: string
}): Promise<app.bsky.feed.getFeed.$OutputBody | null> {
let contentLangs = getAppLanguageAsContentLanguage()
let loggedOutAppviewClient: Client | undefined
/*
* This request is hand-rolled rather than issued through a client, so it has
* to reproduce the header lex would have emitted from the global static.
*/
const labelersHeader = {
'atproto-accept-labelers': Client.appLabelers
.map(l => `${l};redact`)
.join(', '),
}
/**
* The unauthenticated {@link Client} for logged-out feed reads, pointed at the
* direct appview ({@link PUBLIC_APPVIEW}, `api.bsky.app`).
*
* Deliberately NOT the public appview client (`public.api.bsky.app`): that host
* fronts a cache which does not vary on `Accept-Language`, so it would answer a
* language-filtered read from another language's cached body. The direct
* appview respects the header (verified 2026-08-04), at the cost of not being
* cached. See {@link loggedOutFetch}.
*
* A single module-level instance, because there is no session to scope it to.
* Like the public chat client, it uses plain `fetch` rather than
* `networkAwareFetch`, matching the ad-hoc fetch it replaces: this read has its
* own failure handling and should not move the app-wide network signal.
*/
function getLoggedOutAppviewClient(): Client {
return (loggedOutAppviewClient ??= createLexClient({
service: PUBLIC_APPVIEW,
}))
}
// manually construct fetch call so we can add the `lang` cache-busting param
let res = await fetch(
`https://api.bsky.app/xrpc/app.bsky.feed.getFeed?feed=${feed}${
cursor ? `&cursor=${cursor}` : ''
}&limit=${limit}&lang=${contentLangs}`,
{
method: 'GET',
headers: {'Accept-Language': contentLangs, ...labelersHeader},
},
)
/*
* The response is hand-decoded rather than validated, so the lex output shape
* is asserted here just as the old-world one was.
*/
let data = res.ok
? (lexParse(await res.text()) as app.bsky.feed.getFeed.$OutputBody)
: null
/*
* HACK
* We want feeds to give language-specific results immediately when a logged-out
* user changes their language. That comes with two problems:
* 1. not all languages have content, and
* 2. our public caching layer does not bust against the `Accept-Language`
* header.
* -prf
*
* Problem 2 is why this uses its own client rather than the app's public
* appview one: it talks to the direct appview, which honors the header, instead
* of the cached `public.api.bsky.app`, which does not vary on it. That trades
* CDN caching for language correctness on logged-out feed traffic.
*
* Problem 1 is host-independent, so it is still handled here: an empty
* language-filtered feed is retried once with the language constraint removed.
*/
async function loggedOutFetch(
params: GetCustomFeedParams,
): Promise<app.bsky.feed.getFeed.$OutputBody | null> {
const contentLangs = getAppLanguageAsContentLanguage()
let data = await getFeedOrNull(params, contentLangs)
if (data?.feed?.length) {
return data
}
// no data, try again with language headers removed
res = await fetch(
`https://api.bsky.app/xrpc/app.bsky.feed.getFeed?feed=${feed}${
cursor ? `&cursor=${cursor}` : ''
}&limit=${limit}`,
{method: 'GET', headers: {'Accept-Language': '', ...labelersHeader}},
)
data = res.ok
? (lexParse(await res.text()) as app.bsky.feed.getFeed.$OutputBody)
: null
data = await getFeedOrNull(params, '')
if (data?.feed?.length) {
return data
}
return null
}
/**
* A logged-out `getFeed` read that resolves to null on a response error.
*
* The pre-client code only guarded `res.ok`, so a failed RESPONSE fell through
* to the next attempt while a failed REQUEST rejected. Catching
* `XrpcResponseError` preserves that split: every other lex error - the fetch
* and validation ones - still propagates.
*/
async function getFeedOrNull(
params: GetCustomFeedParams,
contentLangs: string,
): Promise<app.bsky.feed.getFeed.$OutputBody | null> {
try {
return await getLoggedOutAppviewClient().call(
app.bsky.feed.getFeed,
params,
{headers: {'Accept-Language': contentLangs}},
)
} catch (e) {
if (e instanceof XrpcResponseError) {
return null
}
throw e
}
}
-10
View File
@@ -307,11 +307,6 @@ async function resolveEmbed(
if (resolvedLink.type === 'record') {
return {
$type: 'app.bsky.embed.record',
/*
* `resolve.ts` is still legacy-typed - its strong refs and URIs carry
* unbranded strings. Assert at the boundary until it moves to the
* clients.
*/
record: resolvedLink.record,
}
}
@@ -495,11 +490,6 @@ async function resolveMedia(
return undefined
}
/*
* `resolve.ts` still returns legacy-typed views, so its strong refs carry plain
* strings where the record write wants the branded syntax types; assert at the
* boundary until those views come from the generated lexicons.
*/
async function resolveRecord(
clients: LinkResolvers,
queryClient: QueryClient,