collapse session bundle to a single bsky client
lex-client 0.3.0 record helpers auto-target the account host per call, so the separate header-less account client is unnecessary: one client with the appview proxy service covers queries and writes. Raw com.atproto server/identity/repo calls that must hit the PDS pass service: null explicitly. Hook names unchanged; usePdsClient now marks "must target the account host" call sites and returns a throwing client when logged out. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+10
-13
@@ -679,23 +679,20 @@ export function useDeviceSignalsQuery() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to prefetch all age assurance data from the server. Reads that hit
|
||||
* the appview (`getState`, device signals) take the appview client; the
|
||||
* preferences/actor-declaration read hits the PDS via the account client.
|
||||
* Helper to prefetch all age assurance data from the server. Takes the single
|
||||
* merged Bluesky client, which serves both roles: the appview reads (`getState`,
|
||||
* device signals) inherit the appview proxy, while `getPreferences` (an SDK
|
||||
* action that passes `service: null` internally) and `fetchActorDeclarationRecord`
|
||||
* (a typed record `get`, `service: null` by default) auto-target the account
|
||||
* host.
|
||||
*/
|
||||
export function prefetchAgeAssuranceServerData({
|
||||
appviewClient,
|
||||
accountClient,
|
||||
}: {
|
||||
appviewClient: Client
|
||||
accountClient: Client
|
||||
}) {
|
||||
export function prefetchAgeAssuranceServerData({client}: {client: Client}) {
|
||||
return Promise.allSettled([
|
||||
// config fetch initiated at the top of the App.platform.tsx files, awaited here
|
||||
configPrefetchPromise,
|
||||
prefetchServerState({appviewClient}),
|
||||
prefetchOtherRequiredData({accountClient}),
|
||||
prefetchDeviceSignals({appviewClient}),
|
||||
prefetchServerState({appviewClient: client}),
|
||||
prefetchOtherRequiredData({accountClient: client}),
|
||||
prefetchDeviceSignals({appviewClient: client}),
|
||||
])
|
||||
}
|
||||
|
||||
|
||||
@@ -39,10 +39,15 @@ export function useBeginAgeAssurance() {
|
||||
throw new Error(`Geolocation not available, cannot init age assurance.`)
|
||||
}
|
||||
|
||||
const {token} = await pdsClient.call(com.atproto.server.getServiceAuth, {
|
||||
aud: BLUESKY_PROXY_DID,
|
||||
lxm: `app.bsky.ageassurance.begin`,
|
||||
})
|
||||
const {token} = await pdsClient.call(
|
||||
com.atproto.server.getServiceAuth,
|
||||
{
|
||||
aud: BLUESKY_PROXY_DID,
|
||||
lxm: `app.bsky.ageassurance.begin`,
|
||||
},
|
||||
// service: null strips the appview proxy header - this must hit the account host (PDS)
|
||||
{service: null},
|
||||
)
|
||||
|
||||
/*
|
||||
* A non-refreshing throwaway client scoped to the service-auth token: it
|
||||
|
||||
@@ -17,10 +17,15 @@ export function useConfirmEmail({
|
||||
throw new Error('No email found for the current account')
|
||||
}
|
||||
|
||||
await pdsClient.call(com.atproto.server.confirmEmail, {
|
||||
email: currentAccount.email.trim(),
|
||||
token: token.trim(),
|
||||
})
|
||||
await pdsClient.call(
|
||||
com.atproto.server.confirmEmail,
|
||||
{
|
||||
email: currentAccount.email.trim(),
|
||||
token: token.trim(),
|
||||
},
|
||||
// service: null strips the appview proxy header - this must hit the account host (PDS)
|
||||
{service: null},
|
||||
)
|
||||
// will update session state at root of app
|
||||
await refreshSession()
|
||||
},
|
||||
|
||||
@@ -19,11 +19,16 @@ export function useManageEmail2FA() {
|
||||
throw new Error('No email found for the current account')
|
||||
}
|
||||
|
||||
await pdsClient.call(com.atproto.server.updateEmail, {
|
||||
email: currentAccount.email,
|
||||
emailAuthFactor: enabled,
|
||||
token,
|
||||
})
|
||||
await pdsClient.call(
|
||||
com.atproto.server.updateEmail,
|
||||
{
|
||||
email: currentAccount.email,
|
||||
emailAuthFactor: enabled,
|
||||
token,
|
||||
},
|
||||
// service: null strips the appview proxy header - this must hit the account host (PDS)
|
||||
{service: null},
|
||||
)
|
||||
// will update session state at root of app
|
||||
await refreshSession()
|
||||
},
|
||||
|
||||
@@ -8,7 +8,12 @@ export function useRequestEmailUpdate() {
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async () => {
|
||||
return await pdsClient.call(com.atproto.server.requestEmailUpdate)
|
||||
return await pdsClient.call(
|
||||
com.atproto.server.requestEmailUpdate,
|
||||
undefined,
|
||||
// service: null strips the appview proxy header - this must hit the account host (PDS)
|
||||
{service: null},
|
||||
)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -8,7 +8,12 @@ export function useRequestEmailVerification() {
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async () => {
|
||||
await pdsClient.call(com.atproto.server.requestEmailConfirmation)
|
||||
await pdsClient.call(
|
||||
com.atproto.server.requestEmailConfirmation,
|
||||
undefined,
|
||||
// service: null strips the appview proxy header - this must hit the account host (PDS)
|
||||
{service: null},
|
||||
)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -11,10 +11,15 @@ async function updateEmailAndRefreshSession(
|
||||
email: string,
|
||||
token?: string,
|
||||
) {
|
||||
await pdsClient.call(com.atproto.server.updateEmail, {
|
||||
email: email.trim(),
|
||||
token,
|
||||
})
|
||||
await pdsClient.call(
|
||||
com.atproto.server.updateEmail,
|
||||
{
|
||||
email: email.trim(),
|
||||
token,
|
||||
},
|
||||
// service: null strips the appview proxy header - this must hit the account host (PDS)
|
||||
{service: null},
|
||||
)
|
||||
await refreshSession()
|
||||
}
|
||||
|
||||
|
||||
@@ -98,10 +98,15 @@ export function CreateListFromStarterPackDialog({
|
||||
|
||||
const chunks = chunk(listitemWrites, 50)
|
||||
for (const c of chunks) {
|
||||
await pdsClient.call(com.atproto.repo.applyWrites, {
|
||||
repo: currentAccount.did as AtIdentifierString,
|
||||
writes: c,
|
||||
})
|
||||
await pdsClient.call(
|
||||
com.atproto.repo.applyWrites,
|
||||
{
|
||||
repo: currentAccount.did as AtIdentifierString,
|
||||
writes: c,
|
||||
},
|
||||
// service: null strips the appview proxy header - this must hit the account host (PDS)
|
||||
{service: null},
|
||||
)
|
||||
}
|
||||
|
||||
await until(
|
||||
|
||||
@@ -53,7 +53,12 @@ function Inner({}: {control: DialogControlProps}) {
|
||||
|
||||
const onPressResendEmail = async () => {
|
||||
setSending(true)
|
||||
await pdsClient.call(com.atproto.server.requestEmailConfirmation)
|
||||
await pdsClient.call(
|
||||
com.atproto.server.requestEmailConfirmation,
|
||||
undefined,
|
||||
// service: null strips the appview proxy header - this must hit the account host (PDS)
|
||||
{service: null},
|
||||
)
|
||||
setSending(false)
|
||||
setStatus('resent')
|
||||
}
|
||||
|
||||
@@ -277,16 +277,26 @@ export function useUpsertLiveStatusMutation(
|
||||
const collection = 'app.bsky.actor.status'
|
||||
|
||||
const existing = await pdsClient
|
||||
.call(com.atproto.repo.getRecord, {repo, collection, rkey: 'self'})
|
||||
// service: null strips the appview proxy header - this must hit the account host (PDS)
|
||||
.call(
|
||||
com.atproto.repo.getRecord,
|
||||
{repo, collection, rkey: 'self'},
|
||||
{service: null},
|
||||
)
|
||||
.catch(_e => undefined)
|
||||
|
||||
await pdsClient.call(com.atproto.repo.putRecord, {
|
||||
repo,
|
||||
collection,
|
||||
rkey: 'self',
|
||||
record,
|
||||
swapRecord: existing?.cid || null,
|
||||
})
|
||||
await pdsClient.call(
|
||||
com.atproto.repo.putRecord,
|
||||
{
|
||||
repo,
|
||||
collection,
|
||||
rkey: 'self',
|
||||
record,
|
||||
swapRecord: existing?.cid || null,
|
||||
},
|
||||
// service: null strips the appview proxy header - this must hit the account host (PDS)
|
||||
{service: null},
|
||||
)
|
||||
}
|
||||
|
||||
await retry(upsert, {
|
||||
|
||||
+10
-5
@@ -177,11 +177,16 @@ export async function post(
|
||||
}
|
||||
|
||||
try {
|
||||
await pdsClient.call(com.atproto.repo.applyWrites, {
|
||||
repo: did,
|
||||
writes: writes,
|
||||
validate: true,
|
||||
})
|
||||
await pdsClient.call(
|
||||
com.atproto.repo.applyWrites,
|
||||
{
|
||||
repo: did,
|
||||
writes: writes,
|
||||
validate: true,
|
||||
},
|
||||
// service: null strips the appview proxy header - this must hit the account host (PDS)
|
||||
{service: null},
|
||||
)
|
||||
} catch (err) {
|
||||
const e = err as Error
|
||||
logger.error(`Failed to create post`, {
|
||||
|
||||
@@ -36,10 +36,17 @@ export const createStarterPackList = async ({
|
||||
purpose: 'app.bsky.graph.defs#referencelist',
|
||||
})
|
||||
if (!list) throw new Error('List creation failed')
|
||||
await client.call(com.atproto.repo.applyWrites, {
|
||||
repo: client.assertDid,
|
||||
writes: profiles.map(p => createListItem({did: p.did, listUri: list.uri})),
|
||||
})
|
||||
await client.call(
|
||||
com.atproto.repo.applyWrites,
|
||||
{
|
||||
repo: client.assertDid,
|
||||
writes: profiles.map(p =>
|
||||
createListItem({did: p.did, listUri: list.uri}),
|
||||
),
|
||||
},
|
||||
// service: null strips the appview proxy header - this must hit the account host (PDS)
|
||||
{service: null},
|
||||
)
|
||||
|
||||
return list
|
||||
}
|
||||
|
||||
@@ -40,11 +40,16 @@ export async function getServiceAuthToken({
|
||||
}
|
||||
resolvedAud = pdsAud
|
||||
}
|
||||
const {token} = await client.call(com.atproto.server.getServiceAuth, {
|
||||
aud: resolvedAud,
|
||||
lxm,
|
||||
exp,
|
||||
})
|
||||
const {token} = await client.call(
|
||||
com.atproto.server.getServiceAuth,
|
||||
{
|
||||
aud: resolvedAud,
|
||||
lxm,
|
||||
exp,
|
||||
},
|
||||
// service: null strips the appview proxy header - this must hit the account host (PDS)
|
||||
{service: null},
|
||||
)
|
||||
return token
|
||||
}
|
||||
|
||||
|
||||
@@ -72,7 +72,12 @@ export function Deactivated() {
|
||||
const handleActivate = useCallback(async () => {
|
||||
try {
|
||||
setPending(true)
|
||||
await pdsClient.call(com.atproto.server.activateAccount)
|
||||
await pdsClient.call(
|
||||
com.atproto.server.activateAccount,
|
||||
undefined,
|
||||
// service: null strips the appview proxy header - this must hit the account host (PDS)
|
||||
{service: null},
|
||||
)
|
||||
await queryClient.resetQueries()
|
||||
await refreshSession()
|
||||
} catch (e: any) {
|
||||
|
||||
@@ -37,10 +37,15 @@ export async function bulkWriteFollows(
|
||||
|
||||
const chunks = chunk(followWrites, 50)
|
||||
for (const chunk of chunks) {
|
||||
await pdsClient.call(com.atproto.repo.applyWrites, {
|
||||
repo: did,
|
||||
writes: chunk,
|
||||
})
|
||||
await pdsClient.call(
|
||||
com.atproto.repo.applyWrites,
|
||||
{
|
||||
repo: did,
|
||||
writes: chunk,
|
||||
},
|
||||
// service: null strips the appview proxy header - this must hit the account host (PDS)
|
||||
{service: null},
|
||||
)
|
||||
}
|
||||
await whenFollowsIndexed(appviewClient, did, res => !!res.follows.length)
|
||||
|
||||
|
||||
@@ -86,9 +86,14 @@ function Inner() {
|
||||
setError('')
|
||||
setIsProcessing(true)
|
||||
try {
|
||||
await pdsClient.call(com.atproto.server.requestPasswordReset, {
|
||||
email: currentAccount.email,
|
||||
})
|
||||
await pdsClient.call(
|
||||
com.atproto.server.requestPasswordReset,
|
||||
{
|
||||
email: currentAccount.email,
|
||||
},
|
||||
// service: null strips the appview proxy header - this must hit the account host (PDS)
|
||||
{service: null},
|
||||
)
|
||||
setStage(Stages.ChangePassword)
|
||||
} catch (e: any) {
|
||||
if (isNetworkError(e)) {
|
||||
@@ -130,10 +135,15 @@ function Inner() {
|
||||
setError('')
|
||||
setIsProcessing(true)
|
||||
try {
|
||||
await pdsClient.call(com.atproto.server.resetPassword, {
|
||||
token: formattedCode,
|
||||
password: newPassword,
|
||||
})
|
||||
await pdsClient.call(
|
||||
com.atproto.server.resetPassword,
|
||||
{
|
||||
token: formattedCode,
|
||||
password: newPassword,
|
||||
},
|
||||
// service: null strips the appview proxy header - this must hit the account host (PDS)
|
||||
{service: null},
|
||||
)
|
||||
setStage(Stages.Done)
|
||||
} catch (e: any) {
|
||||
if (isNetworkError(e)) {
|
||||
|
||||
@@ -44,7 +44,12 @@ function DeactivateAccountDialogInner({
|
||||
const handleDeactivate = useCallback(async () => {
|
||||
try {
|
||||
setPending(true)
|
||||
await pdsClient.call(com.atproto.server.deactivateAccount, {})
|
||||
await pdsClient.call(
|
||||
com.atproto.server.deactivateAccount,
|
||||
{},
|
||||
// service: null strips the appview proxy header - this must hit the account host (PDS)
|
||||
{service: null},
|
||||
)
|
||||
control.close(() => {
|
||||
logoutCurrentAccount('Deactivated')
|
||||
})
|
||||
|
||||
@@ -96,7 +96,12 @@ function DeleteAccountDialogInner({
|
||||
}
|
||||
try {
|
||||
setEmailState(EmailState.PENDING)
|
||||
await pdsClient.call(com.atproto.server.requestAccountDelete)
|
||||
await pdsClient.call(
|
||||
com.atproto.server.requestAccountDelete,
|
||||
undefined,
|
||||
// service: null strips the appview proxy header - this must hit the account host (PDS)
|
||||
{service: null},
|
||||
)
|
||||
setError('')
|
||||
setEmailSentCount(prevCount => prevCount + 1)
|
||||
setStep(Step.VERIFY_CODE)
|
||||
@@ -122,11 +127,16 @@ function DeleteAccountDialogInner({
|
||||
// Inform chat service of intent to delete account. The chat client is
|
||||
// proxied to the chat service; a failure throws.
|
||||
await chatClient.call(chat.bsky.actor.deleteAccount)
|
||||
await pdsClient.call(com.atproto.server.deleteAccount, {
|
||||
did: currentAccount.did as DidString,
|
||||
password,
|
||||
token,
|
||||
})
|
||||
await pdsClient.call(
|
||||
com.atproto.server.deleteAccount,
|
||||
{
|
||||
did: currentAccount.did as DidString,
|
||||
password,
|
||||
token,
|
||||
},
|
||||
// service: null strips the appview proxy header - this must hit the account host (PDS)
|
||||
{service: null},
|
||||
)
|
||||
control.close(() => {
|
||||
toast.show(_(msg`Your account has been deleted, see ya! ✌️`))
|
||||
resetToTab('HomeTab')
|
||||
|
||||
@@ -44,7 +44,12 @@ export function DisableEmail2FADialog({
|
||||
setError('')
|
||||
setIsProcessing(true)
|
||||
try {
|
||||
await pdsClient.call(com.atproto.server.requestEmailUpdate)
|
||||
await pdsClient.call(
|
||||
com.atproto.server.requestEmailUpdate,
|
||||
undefined,
|
||||
// service: null strips the appview proxy header - this must hit the account host (PDS)
|
||||
{service: null},
|
||||
)
|
||||
setStage(Stages.ConfirmCode)
|
||||
} catch (e) {
|
||||
setError(cleanError(String(e)))
|
||||
@@ -58,11 +63,16 @@ export function DisableEmail2FADialog({
|
||||
setIsProcessing(true)
|
||||
try {
|
||||
if (currentAccount?.email) {
|
||||
await pdsClient.call(com.atproto.server.updateEmail, {
|
||||
email: currentAccount.email,
|
||||
token: confirmationCode.trim(),
|
||||
emailAuthFactor: false,
|
||||
})
|
||||
await pdsClient.call(
|
||||
com.atproto.server.updateEmail,
|
||||
{
|
||||
email: currentAccount.email,
|
||||
token: confirmationCode.trim(),
|
||||
emailAuthFactor: false,
|
||||
},
|
||||
// service: null strips the appview proxy header - this must hit the account host (PDS)
|
||||
{service: null},
|
||||
)
|
||||
await refreshSession()
|
||||
Toast.show(_(msg({message: 'Email 2FA disabled', context: 'toast'})))
|
||||
}
|
||||
|
||||
@@ -39,7 +39,12 @@ export function SignupQueued() {
|
||||
const checkStatus = useCallback(async () => {
|
||||
setProcessing(true)
|
||||
try {
|
||||
const res = await pdsClient.call(com.atproto.temp.checkSignupQueue)
|
||||
const res = await pdsClient.call(
|
||||
com.atproto.temp.checkSignupQueue,
|
||||
{},
|
||||
// service: null strips the appview proxy header - this must hit the account host (PDS)
|
||||
{service: null},
|
||||
)
|
||||
if (res.activated) {
|
||||
// ready to go, exchange the access token for a usable one and kick off onboarding
|
||||
const refreshed = await refreshSession()
|
||||
|
||||
@@ -13,7 +13,12 @@ export function useAppPasswordsQuery() {
|
||||
staleTime: STALE.MINUTES.FIVE,
|
||||
queryKey: RQKEY(),
|
||||
queryFn: async () => {
|
||||
const data = await pdsClient.call(com.atproto.server.listAppPasswords, {})
|
||||
const data = await pdsClient.call(
|
||||
com.atproto.server.listAppPasswords,
|
||||
{},
|
||||
// service: null strips the appview proxy header - this must hit the account host (PDS)
|
||||
{service: null},
|
||||
)
|
||||
return data.passwords
|
||||
},
|
||||
})
|
||||
@@ -28,10 +33,15 @@ export function useAppPasswordCreateMutation() {
|
||||
{name: string; privileged: boolean}
|
||||
>({
|
||||
mutationFn: async ({name, privileged}) => {
|
||||
return await pdsClient.call(com.atproto.server.createAppPassword, {
|
||||
name,
|
||||
privileged,
|
||||
})
|
||||
return await pdsClient.call(
|
||||
com.atproto.server.createAppPassword,
|
||||
{
|
||||
name,
|
||||
privileged,
|
||||
},
|
||||
// service: null strips the appview proxy header - this must hit the account host (PDS)
|
||||
{service: null},
|
||||
)
|
||||
},
|
||||
onSuccess() {
|
||||
queryClient.invalidateQueries({
|
||||
@@ -46,9 +56,14 @@ export function useAppPasswordDeleteMutation() {
|
||||
const pdsClient = usePdsClient()
|
||||
return useMutation<void, Error, {name: string}>({
|
||||
mutationFn: async ({name}) => {
|
||||
await pdsClient.call(com.atproto.server.revokeAppPassword, {
|
||||
name,
|
||||
})
|
||||
await pdsClient.call(
|
||||
com.atproto.server.revokeAppPassword,
|
||||
{
|
||||
name,
|
||||
},
|
||||
// service: null strips the appview proxy header - this must hit the account host (PDS)
|
||||
{service: null},
|
||||
)
|
||||
},
|
||||
onSuccess() {
|
||||
queryClient.invalidateQueries({
|
||||
|
||||
@@ -45,9 +45,14 @@ export function useUpdateHandleMutation(opts?: {
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async ({handle}: {handle: string}) => {
|
||||
await pdsClient.call(com.atproto.identity.updateHandle, {
|
||||
handle: handle as HandleString,
|
||||
})
|
||||
await pdsClient.call(
|
||||
com.atproto.identity.updateHandle,
|
||||
{
|
||||
handle: handle as HandleString,
|
||||
},
|
||||
// service: null strips the appview proxy header - this must hit the account host (PDS)
|
||||
{service: null},
|
||||
)
|
||||
},
|
||||
onSuccess(_data, variables) {
|
||||
opts?.onSuccess?.(variables.handle)
|
||||
|
||||
+20
-10
@@ -149,12 +149,17 @@ export function useListMetadataMutation() {
|
||||
} else if (avatar === null) {
|
||||
record.avatar = undefined
|
||||
}
|
||||
const res = await pdsClient.call(com.atproto.repo.putRecord, {
|
||||
repo: currentAccount.did,
|
||||
collection: 'app.bsky.graph.list',
|
||||
rkey,
|
||||
record,
|
||||
})
|
||||
const res = await pdsClient.call(
|
||||
com.atproto.repo.putRecord,
|
||||
{
|
||||
repo: currentAccount.did,
|
||||
collection: 'app.bsky.graph.list',
|
||||
rkey,
|
||||
record,
|
||||
},
|
||||
// service: null strips the appview proxy header - this must hit the account host (PDS)
|
||||
{service: null},
|
||||
)
|
||||
|
||||
// wait for the appview to update
|
||||
await whenAppViewReady(appviewClient, res.uri, v => {
|
||||
@@ -227,10 +232,15 @@ export function useListDeleteMutation() {
|
||||
|
||||
// apply in chunks
|
||||
for (const writesChunk of chunk(writes, 10)) {
|
||||
await pdsClient.call(com.atproto.repo.applyWrites, {
|
||||
repo: currentAccount.did as AtIdentifierString,
|
||||
writes: writesChunk,
|
||||
})
|
||||
await pdsClient.call(
|
||||
com.atproto.repo.applyWrites,
|
||||
{
|
||||
repo: currentAccount.did as AtIdentifierString,
|
||||
writes: writesChunk,
|
||||
},
|
||||
// service: null strips the appview proxy header - this must hit the account host (PDS)
|
||||
{service: null},
|
||||
)
|
||||
}
|
||||
|
||||
// wait for the appview to update. once the list is deleted, getList
|
||||
|
||||
@@ -39,16 +39,21 @@ export function useUpdateActorDeclaration({
|
||||
update.allowGroupInvites ??
|
||||
current?.associated?.chat?.allowGroupInvites,
|
||||
})
|
||||
const result = await pdsClient.call(com.atproto.repo.putRecord, {
|
||||
repo: currentAccount.did as DidString,
|
||||
collection: 'chat.bsky.actor.declaration',
|
||||
rkey: 'self',
|
||||
record: {
|
||||
$type: 'chat.bsky.actor.declaration',
|
||||
allowIncoming,
|
||||
allowGroupInvites,
|
||||
const result = await pdsClient.call(
|
||||
com.atproto.repo.putRecord,
|
||||
{
|
||||
repo: currentAccount.did as DidString,
|
||||
collection: 'chat.bsky.actor.declaration',
|
||||
rkey: 'self',
|
||||
record: {
|
||||
$type: 'chat.bsky.actor.declaration',
|
||||
allowIncoming,
|
||||
allowGroupInvites,
|
||||
},
|
||||
},
|
||||
})
|
||||
// service: null strips the appview proxy header - this must hit the account host (PDS)
|
||||
{service: null},
|
||||
)
|
||||
return result
|
||||
},
|
||||
onMutate: update => {
|
||||
@@ -104,11 +109,16 @@ export function useDeleteActorDeclaration() {
|
||||
return useMutation({
|
||||
mutationFn: async () => {
|
||||
if (!currentAccount) throw new Error('Not signed in')
|
||||
const result = await pdsClient.call(com.atproto.repo.deleteRecord, {
|
||||
repo: currentAccount.did as DidString,
|
||||
collection: 'chat.bsky.actor.declaration',
|
||||
rkey: 'self',
|
||||
})
|
||||
const result = await pdsClient.call(
|
||||
com.atproto.repo.deleteRecord,
|
||||
{
|
||||
repo: currentAccount.did as DidString,
|
||||
collection: 'chat.bsky.actor.declaration',
|
||||
rkey: 'self',
|
||||
},
|
||||
// service: null strips the appview proxy header - this must hit the account host (PDS)
|
||||
{service: null},
|
||||
)
|
||||
return result
|
||||
},
|
||||
})
|
||||
|
||||
@@ -49,11 +49,16 @@ export async function getPostgateRecord({
|
||||
return true
|
||||
},
|
||||
() =>
|
||||
pdsClient.call(com.atproto.repo.getRecord, {
|
||||
repo: urip.host,
|
||||
collection: POSTGATE_COLLECTION,
|
||||
rkey: urip.rkey,
|
||||
}),
|
||||
pdsClient.call(
|
||||
com.atproto.repo.getRecord,
|
||||
{
|
||||
repo: urip.host,
|
||||
collection: POSTGATE_COLLECTION,
|
||||
rkey: urip.rkey,
|
||||
},
|
||||
// service: null strips the appview proxy header - this must hit the account host (PDS)
|
||||
{service: null},
|
||||
),
|
||||
)
|
||||
|
||||
if (data.value && bsky.matches(app.bsky.feed.postgate, data.value)) {
|
||||
@@ -87,12 +92,17 @@ export async function writePostgateRecord({
|
||||
const postUrip = new AtUri(postUri)
|
||||
|
||||
await networkRetry(2, () =>
|
||||
pdsClient.call(com.atproto.repo.putRecord, {
|
||||
repo: pdsClient.assertDid,
|
||||
collection: POSTGATE_COLLECTION,
|
||||
rkey: postUrip.rkey,
|
||||
record: postgate,
|
||||
}),
|
||||
pdsClient.call(
|
||||
com.atproto.repo.putRecord,
|
||||
{
|
||||
repo: pdsClient.assertDid,
|
||||
collection: POSTGATE_COLLECTION,
|
||||
rkey: postUrip.rkey,
|
||||
record: postgate,
|
||||
},
|
||||
// service: null strips the appview proxy header - this must hit the account host (PDS)
|
||||
{service: null},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -145,7 +145,12 @@ export function useClearPreferencesMutation() {
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async () => {
|
||||
await client.call(app.bsky.actor.putPreferences, {preferences: []})
|
||||
// service: null strips the appview proxy header - putPreferences is served by the account host (PDS)
|
||||
await client.call(
|
||||
app.bsky.actor.putPreferences,
|
||||
{preferences: []},
|
||||
{service: null},
|
||||
)
|
||||
// triggers a refetch
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: preferencesQueryKey,
|
||||
|
||||
@@ -194,18 +194,23 @@ export function useEditStarterPackMutation({
|
||||
if (removedItems.length !== 0) {
|
||||
const chunks = chunk(removedItems, 50)
|
||||
for (const chunk of chunks) {
|
||||
await pdsClient.call(com.atproto.repo.applyWrites, {
|
||||
repo: pdsClient.assertDid,
|
||||
writes: chunk.map(
|
||||
(
|
||||
i,
|
||||
): com.atproto.repo.applyWrites.$InputBody['writes'][number] => ({
|
||||
$type: 'com.atproto.repo.applyWrites#delete',
|
||||
collection: 'app.bsky.graph.listitem',
|
||||
rkey: new AtUri(i.uri).rkey,
|
||||
}),
|
||||
),
|
||||
})
|
||||
await pdsClient.call(
|
||||
com.atproto.repo.applyWrites,
|
||||
{
|
||||
repo: pdsClient.assertDid,
|
||||
writes: chunk.map(
|
||||
(
|
||||
i,
|
||||
): com.atproto.repo.applyWrites.$InputBody['writes'][number] => ({
|
||||
$type: 'com.atproto.repo.applyWrites#delete',
|
||||
collection: 'app.bsky.graph.listitem',
|
||||
rkey: new AtUri(i.uri).rkey,
|
||||
}),
|
||||
),
|
||||
},
|
||||
// service: null strips the appview proxy header - this must hit the account host (PDS)
|
||||
{service: null},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -215,42 +220,52 @@ export function useEditStarterPackMutation({
|
||||
if (addedProfiles.length > 0) {
|
||||
const chunks = chunk(addedProfiles, 50)
|
||||
for (const chunk of chunks) {
|
||||
await pdsClient.call(com.atproto.repo.applyWrites, {
|
||||
repo: pdsClient.assertDid,
|
||||
writes: chunk.map(
|
||||
(
|
||||
p,
|
||||
): com.atproto.repo.applyWrites.$InputBody['writes'][number] => ({
|
||||
$type: 'com.atproto.repo.applyWrites#create',
|
||||
collection: 'app.bsky.graph.listitem',
|
||||
value: {
|
||||
$type: 'app.bsky.graph.listitem',
|
||||
subject: p.did,
|
||||
list: currentStarterPack.list?.uri,
|
||||
createdAt: new Date().toISOString(),
|
||||
},
|
||||
}),
|
||||
),
|
||||
})
|
||||
await pdsClient.call(
|
||||
com.atproto.repo.applyWrites,
|
||||
{
|
||||
repo: pdsClient.assertDid,
|
||||
writes: chunk.map(
|
||||
(
|
||||
p,
|
||||
): com.atproto.repo.applyWrites.$InputBody['writes'][number] => ({
|
||||
$type: 'com.atproto.repo.applyWrites#create',
|
||||
collection: 'app.bsky.graph.listitem',
|
||||
value: {
|
||||
$type: 'app.bsky.graph.listitem',
|
||||
subject: p.did,
|
||||
list: currentStarterPack.list?.uri,
|
||||
createdAt: new Date().toISOString(),
|
||||
},
|
||||
}),
|
||||
),
|
||||
},
|
||||
// service: null strips the appview proxy header - this must hit the account host (PDS)
|
||||
{service: null},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const rkey = parseStarterPackUri(currentStarterPack.uri)!.rkey
|
||||
await pdsClient.call(com.atproto.repo.putRecord, {
|
||||
repo: pdsClient.assertDid,
|
||||
collection: 'app.bsky.graph.starterpack',
|
||||
rkey,
|
||||
record: {
|
||||
$type: 'app.bsky.graph.starterpack',
|
||||
name,
|
||||
description,
|
||||
descriptionFacets,
|
||||
list: currentStarterPack.list?.uri,
|
||||
feeds,
|
||||
createdAt: currentStarterPack.record.createdAt,
|
||||
updatedAt: new Date().toISOString(),
|
||||
await pdsClient.call(
|
||||
com.atproto.repo.putRecord,
|
||||
{
|
||||
repo: pdsClient.assertDid,
|
||||
collection: 'app.bsky.graph.starterpack',
|
||||
rkey,
|
||||
record: {
|
||||
$type: 'app.bsky.graph.starterpack',
|
||||
name,
|
||||
description,
|
||||
descriptionFacets,
|
||||
list: currentStarterPack.list?.uri,
|
||||
feeds,
|
||||
createdAt: currentStarterPack.record.createdAt,
|
||||
updatedAt: new Date().toISOString(),
|
||||
},
|
||||
},
|
||||
})
|
||||
// service: null strips the appview proxy header - this must hit the account host (PDS)
|
||||
{service: null},
|
||||
)
|
||||
},
|
||||
onSuccess: async (_, {currentStarterPack}) => {
|
||||
const parsed = parseStarterPackUri(currentStarterPack.uri)
|
||||
|
||||
@@ -112,11 +112,16 @@ export async function getThreadgateRecord({
|
||||
return true
|
||||
},
|
||||
() =>
|
||||
pdsClient.call(com.atproto.repo.getRecord, {
|
||||
repo: urip.host,
|
||||
collection: 'app.bsky.feed.threadgate',
|
||||
rkey: urip.rkey,
|
||||
}),
|
||||
pdsClient.call(
|
||||
com.atproto.repo.getRecord,
|
||||
{
|
||||
repo: urip.host,
|
||||
collection: 'app.bsky.feed.threadgate',
|
||||
rkey: urip.rkey,
|
||||
},
|
||||
// service: null strips the appview proxy header - this must hit the account host (PDS)
|
||||
{service: null},
|
||||
),
|
||||
)
|
||||
|
||||
if (data.value && bsky.matches(app.bsky.feed.threadgate, data.value)) {
|
||||
@@ -155,12 +160,17 @@ export async function writeThreadgateRecord({
|
||||
})
|
||||
|
||||
await networkRetry(2, () =>
|
||||
pdsClient.call(com.atproto.repo.putRecord, {
|
||||
repo: pdsClient.assertDid,
|
||||
collection: 'app.bsky.feed.threadgate',
|
||||
rkey: postUrip.rkey,
|
||||
record,
|
||||
}),
|
||||
pdsClient.call(
|
||||
com.atproto.repo.putRecord,
|
||||
{
|
||||
repo: pdsClient.assertDid,
|
||||
collection: 'app.bsky.feed.threadgate',
|
||||
rkey: postUrip.rkey,
|
||||
record,
|
||||
},
|
||||
// service: null strips the appview proxy header - this must hit the account host (PDS)
|
||||
{service: null},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -27,10 +27,9 @@ jest.mock('jwt-decode', () => ({
|
||||
}))
|
||||
|
||||
import {PUBLIC_BSKY_SERVICE} from '#/lib/constants'
|
||||
import {app, chat} from '#/lexicons'
|
||||
import {app, chat, com} from '#/lexicons'
|
||||
import {
|
||||
buildAccountClient,
|
||||
buildAppviewClient,
|
||||
buildBskyClient,
|
||||
buildChatClient,
|
||||
getPublicLexClient,
|
||||
getUnauthenticatedClient,
|
||||
@@ -123,11 +122,11 @@ function makeSession(
|
||||
})
|
||||
}
|
||||
|
||||
describe('buildAppviewClient', () => {
|
||||
describe('buildBskyClient', () => {
|
||||
it('sets the appview atproto-proxy header and includes only the per-instance labelers', async () => {
|
||||
const {seen, fetchMock} = makeCapturingFetch()
|
||||
const session = makeSession(fetchMock)
|
||||
const client = buildAppviewClient(session, [CUSTOM_LABELER])
|
||||
const client = buildBskyClient(session, [CUSTOM_LABELER])
|
||||
|
||||
await client.call(app.bsky.actor.getProfile.main, {actor: HANDLE})
|
||||
|
||||
@@ -147,36 +146,66 @@ describe('buildAppviewClient', () => {
|
||||
it('routes through the session fetchHandler with the bearer token', async () => {
|
||||
const {seen, fetchMock} = makeCapturingFetch()
|
||||
const session = makeSession(fetchMock)
|
||||
const client = buildAppviewClient(session, [])
|
||||
const client = buildBskyClient(session, [])
|
||||
|
||||
await client.call(app.bsky.actor.getProfile.main, {actor: HANDLE})
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledTimes(1)
|
||||
expect(seen[0].headers.get('authorization')).toBe('Bearer access-jwt')
|
||||
})
|
||||
})
|
||||
|
||||
describe('buildAccountClient', () => {
|
||||
it('has no atproto-proxy header (requests hit the PDS directly)', async () => {
|
||||
it('strips the appview proxy AND accept-labelers headers on a record helper (auto-targets the account host)', async () => {
|
||||
/*
|
||||
* Even though the instance is configured with the appview service and a
|
||||
* per-instance labeler, lex-client 0.3.0's record helpers default per-call
|
||||
* `service = null` / `labelers = null`, deleting both headers so the call
|
||||
* hits the user's PDS. We exercise this via `getRecord`: it shares the
|
||||
* identical `service = null` / `labelers = null` default with the write
|
||||
* helpers (createRecord/putRecord/...), but does not lex-encode a record
|
||||
* body, so it dodges a lex-data CID-encoding incompatibility that makes
|
||||
* createRecord throw before any fetch in this jest environment. The stub
|
||||
* response fails getRecord output validation; `.catch` swallows that
|
||||
* (headers are recorded pre-parse). The request targets bsky.social (the
|
||||
* PDS host), NOT the appview.
|
||||
*/
|
||||
const {seen, fetchMock} = makeCapturingFetch()
|
||||
const session = makeSession(fetchMock)
|
||||
const client = buildAccountClient(session)
|
||||
const client = buildBskyClient(session, [CUSTOM_LABELER])
|
||||
|
||||
await client.call(app.bsky.actor.getProfile.main, {actor: HANDLE})
|
||||
await client.getRecord('app.bsky.feed.post', 'self').catch(() => {})
|
||||
|
||||
expect(seen.length).toBe(1)
|
||||
expect(seen[0].headers.get('atproto-proxy')).toBeNull()
|
||||
expect(seen[0].headers.get('atproto-accept-labelers')).toBeNull()
|
||||
})
|
||||
|
||||
it('routes through the session fetchHandler with the bearer token', async () => {
|
||||
it('inherits the appview proxy on a raw appview query call', async () => {
|
||||
const {seen, fetchMock} = makeCapturingFetch()
|
||||
const session = makeSession(fetchMock)
|
||||
const client = buildAccountClient(session)
|
||||
const client = buildBskyClient(session, [])
|
||||
|
||||
await client.call(app.bsky.actor.getProfile.main, {actor: HANDLE})
|
||||
await client.call(app.bsky.feed.getTimeline.main, {}).catch(() => {})
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledTimes(1)
|
||||
expect(seen[0].headers.get('authorization')).toBe('Bearer access-jwt')
|
||||
expect(seen.length).toBe(1)
|
||||
expect(seen[0].headers.get('atproto-proxy')).toBe(APPVIEW_PROXY)
|
||||
})
|
||||
|
||||
it('strips the appview proxy on a raw call passed {service: null}', async () => {
|
||||
/*
|
||||
* A raw call inherits the instance appview proxy unless the call site opts
|
||||
* out with `{service: null}` - the escape hatch record helpers apply
|
||||
* automatically. getSession must hit the PDS, so its call passes it.
|
||||
*/
|
||||
const {seen, fetchMock} = makeCapturingFetch()
|
||||
const session = makeSession(fetchMock)
|
||||
const client = buildBskyClient(session, [])
|
||||
|
||||
await client
|
||||
.call(com.atproto.server.getSession.main, {}, {service: null})
|
||||
.catch(() => {})
|
||||
|
||||
expect(seen.length).toBe(1)
|
||||
expect(seen[0].headers.get('atproto-proxy')).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -288,11 +317,11 @@ describe('getPublicLexClient', () => {
|
||||
* section 6).
|
||||
*/
|
||||
describe('labeler-header regression guard', () => {
|
||||
it('appview client emits the global Bluesky labeler redacted and the per-instance labeler plain', async () => {
|
||||
it('bsky client emits the global Bluesky labeler redacted and the per-instance labeler plain', async () => {
|
||||
/*
|
||||
* The moderation DID flows only through the global Client.appLabelers,
|
||||
* which lex-client merges into the header per request with the `;redact`
|
||||
* suffix; buildAppviewClient no longer lists it as a per-instance labeler.
|
||||
* suffix; buildBskyClient no longer lists it as a per-instance labeler.
|
||||
* Configure the global appLabelers to the Bluesky moderation DID (matching
|
||||
* switchToBskyAppLabeler in moderation.ts) so the composition matches
|
||||
* production.
|
||||
@@ -301,7 +330,7 @@ describe('labeler-header regression guard', () => {
|
||||
|
||||
const {seen, fetchMock} = makeCapturingFetch()
|
||||
const session = makeSession(fetchMock)
|
||||
const client = buildAppviewClient(session, [CUSTOM_LABELER])
|
||||
const client = buildBskyClient(session, [CUSTOM_LABELER])
|
||||
await client
|
||||
.call(app.bsky.actor.getProfile.main, {actor: HANDLE})
|
||||
.catch(() => {})
|
||||
|
||||
@@ -27,19 +27,6 @@ export function getPublicLexClient(): Client {
|
||||
return publicClient
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the account (PDS) client over a {@link PasswordSession}. Writes and
|
||||
* record mutations go here - no `atproto-proxy` header, so requests hit the
|
||||
* user's PDS directly (the session's `fetchHandler` resolves the PDS origin per
|
||||
* request from the didDoc, falling back to `service`).
|
||||
*
|
||||
* We intentionally do NOT pass `fetch` here: a client built over a session uses
|
||||
* that session's own `fetch` (networkAwareFetch, set at construction).
|
||||
*/
|
||||
export function buildAccountClient(session: PasswordSession): Client {
|
||||
return createLexClient(session)
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the chat client over a {@link PasswordSession}.
|
||||
*
|
||||
@@ -85,20 +72,39 @@ export function getUnauthenticatedClient(): Client {
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the authed appview client over a {@link PasswordSession}. Requests are
|
||||
* proxied to the Bluesky appview and carry the per-instance labelers.
|
||||
* Build the single authed Bluesky client over a {@link PasswordSession}. This
|
||||
* is the merged account-plus-appview client: one instance serves both reads
|
||||
* (proxied to the Bluesky appview) and writes (routed to the user's PDS),
|
||||
* because lex-client 0.3.0's record helpers pick the target per call.
|
||||
*
|
||||
* The instance is configured with `service = BLUESKY_PROXY_HEADER.get()`, so by
|
||||
* default every request carries the `atproto-proxy` header and is proxied to
|
||||
* the Bluesky appview, along with the per-instance labelers. The getter exists
|
||||
* so the e2e `TestCtrls` hack can retarget the appview via
|
||||
* `BLUESKY_PROXY_HEADER.set()` before sign-in (the client is built at sign-in,
|
||||
* so it picks up the override).
|
||||
*
|
||||
* Two request shapes take DIFFERENT targets off this one instance:
|
||||
* - Record helpers (`createRecord`/`putRecord`/...) and the typed record sugar
|
||||
* (`create`/`put`/`get`/`delete`/`list`) default per-call `service = null`
|
||||
* in lex-client 0.3.0, which DELETES the `atproto-proxy` header regardless of
|
||||
* the instance default. So writes auto-target the account host and hit the
|
||||
* user's PDS through the session's `fetchHandler` (which resolves the PDS
|
||||
* origin per request from the didDoc, falling back to `service`).
|
||||
* - Raw `client.call(lexicon, ...)` inherits the appview proxy from the
|
||||
* instance `service`, UNLESS the call site passes `{service: null}` to strip
|
||||
* it (needed for `com.atproto.server`/`identity`/`sync`/`temp` calls that
|
||||
* must hit the PDS directly).
|
||||
*
|
||||
* The Bluesky moderation labeler (`api.moderation.did`) is deliberately NOT
|
||||
* listed here - it must flow only through the global `Client.appLabelers` (see
|
||||
* moderation.ts) so it carries the `;redact` suffix; adding it here would
|
||||
* produce a duplicate, non-redact header entry.
|
||||
* listed in `labelerDids` - it must flow only through the global
|
||||
* `Client.appLabelers` (see moderation.ts) so it carries the `;redact` suffix;
|
||||
* adding it here would produce a duplicate, non-redact header entry.
|
||||
*
|
||||
* The proxy `service` is read from `BLUESKY_PROXY_HEADER.get()`, whose default
|
||||
* equals `api.app.service`. The getter exists so the e2e `TestCtrls` hack can
|
||||
* retarget the appview via `BLUESKY_PROXY_HEADER.set()` before sign-in (the
|
||||
* client is built at sign-in, so it picks up the override).
|
||||
* We intentionally do NOT pass `fetch` here: a client built over a session uses
|
||||
* that session's own `fetch` (networkAwareFetch, set at construction).
|
||||
*/
|
||||
export function buildAppviewClient(
|
||||
export function buildBskyClient(
|
||||
session: PasswordSession,
|
||||
labelerDids: string[],
|
||||
): Client {
|
||||
|
||||
+48
-26
@@ -60,8 +60,9 @@ StateContext.displayName = 'SessionStateContext'
|
||||
|
||||
/**
|
||||
* Holds the full {@link SessionBundle} (or the logged-out
|
||||
* {@link PublicSessionBundle}) for the active account. The three-client hooks
|
||||
* (`useLexClient`/`useAppviewClient`/`usePdsClient`) read from here.
|
||||
* {@link PublicSessionBundle}) for the active account. The authed client hooks
|
||||
* (`useLexClient`/`useAppviewClient`/`usePdsClient`), which all return the one
|
||||
* merged `bskyClient` when signed in, read from here.
|
||||
*/
|
||||
const BundleContext = createContext<SessionBundle | PublicSessionBundle | null>(
|
||||
null,
|
||||
@@ -526,12 +527,18 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
||||
const bundle = state.currentAgentState.agent as unknown as SessionBundle
|
||||
const signal = cancelPendingTask()
|
||||
/*
|
||||
* Fetch through the account (PDS) client and dispatch the patch. We do NOT
|
||||
* mutate the session object (PasswordSession's data is immutable to us); the
|
||||
* reducer patches only the `accounts` entry, and the email-state hook reads
|
||||
* from the account rather than the session.
|
||||
* Fetch through the merged Bluesky client and dispatch the patch. getSession
|
||||
* must hit the user's PDS, not the appview proxy, so this raw call passes
|
||||
* `{service: null}` to strip the instance's appview `atproto-proxy` header.
|
||||
* We do NOT mutate the session object (PasswordSession's data is immutable to
|
||||
* us); the reducer patches only the `accounts` entry, and the email-state
|
||||
* hook reads from the account rather than the session.
|
||||
*/
|
||||
const data = await bundle.accountClient.call(com.atproto.server.getSession)
|
||||
const data = await bundle.bskyClient.call(
|
||||
com.atproto.server.getSession,
|
||||
{},
|
||||
{service: null},
|
||||
)
|
||||
if (signal.aborted) return
|
||||
store.dispatch({
|
||||
type: 'partial-refresh-session',
|
||||
@@ -669,7 +676,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
||||
registerBundleKillSwitch(newBundle, hooks.kill)
|
||||
/*
|
||||
* Reapply this account's subscribed labelers to the freshly built
|
||||
* appview client: buildBundle starts with an empty per-instance
|
||||
* merged Bluesky client: buildBundle starts with an empty per-instance
|
||||
* labeler set, and this rebuild path never runs
|
||||
* configureModerationForAccount on its own. It is fully synchronous
|
||||
* (the labeler cache is a local MMKV read), so the whole prep + arm +
|
||||
@@ -857,39 +864,53 @@ export function useRequireAuth() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticated lex {@link Client} for appview reads. Backed by the active
|
||||
* bundle's appview client (proxied to the Bluesky appview, with labelers); its
|
||||
* identity is stable per-bundle. Falls back to the public client when there is
|
||||
* no bundle (logged out, or used outside the provider) so callers can treat it
|
||||
* as always-present.
|
||||
* Authenticated lex {@link Client} for the active account. Backed by the active
|
||||
* bundle's single merged Bluesky client (proxied to the Bluesky appview, with
|
||||
* labelers); its identity is stable per-bundle. Falls back to the public client
|
||||
* when there is no bundle (logged out, or used outside the provider) so callers
|
||||
* can treat it as always-present.
|
||||
*
|
||||
* All three authed client hooks (this, {@link useAppviewClient},
|
||||
* {@link usePdsClient}) now return the SAME merged client for a signed-in
|
||||
* account. They differ ONLY in their logged-out fallback: this hook and
|
||||
* `useAppviewClient` fall back to the public read client, while `usePdsClient`
|
||||
* falls back to the throwing unauthenticated client.
|
||||
*/
|
||||
export function useLexClient(): Client {
|
||||
const bundle = useContext(BundleContext)
|
||||
return bundle?.appviewClient ?? getPublicLexClient()
|
||||
return bundle?.bskyClient ?? getPublicLexClient()
|
||||
}
|
||||
|
||||
/**
|
||||
* Alias of {@link useLexClient}: the authenticated appview client for the
|
||||
* active account.
|
||||
* Alias of {@link useLexClient}: the authenticated merged Bluesky client for the
|
||||
* active account, falling back to the public read client when logged out.
|
||||
*/
|
||||
export function useAppviewClient(): Client {
|
||||
const bundle = useContext(BundleContext)
|
||||
return bundle?.appviewClient ?? getPublicLexClient()
|
||||
return bundle?.bskyClient ?? getPublicLexClient()
|
||||
}
|
||||
|
||||
/**
|
||||
* The account (PDS) lex {@link Client} for the active account. Writes and record
|
||||
* mutations go here - requests hit the user's PDS directly (no appview proxy).
|
||||
* The authenticated merged Bluesky client for the active account, the SAME
|
||||
* instance returned by {@link useLexClient}/{@link useAppviewClient}. The name
|
||||
* is historical (there is no longer a separate PDS client): use this hook at
|
||||
* call sites whose requests must target the ACCOUNT HOST. Record helpers on the
|
||||
* client auto-target it (lex-client 0.3.0 defaults `service = null` per call);
|
||||
* raw `com.atproto.server`/`identity`/`sync`/`temp` calls must pass
|
||||
* `{service: null}` to strip the appview proxy.
|
||||
*
|
||||
* Logged out, returns a stable client ({@link getUnauthenticatedClient}) that
|
||||
* throws `NotAuthenticatedError` before any network I/O, so an unauthenticated
|
||||
* write fails loudly rather than silently hitting `public.api.bsky.app`.
|
||||
* Components may safely hold this client while logged out; only calling it
|
||||
* throws. To branch on auth state, use {@link useMaybePdsClient} instead.
|
||||
* write fails loudly rather than silently hitting `public.api.bsky.app`. This is
|
||||
* the ONLY logged-out write protection - the public bundle now carries the
|
||||
* public read client in `bskyClient`, so this hook gates on `bundle.session`
|
||||
* (not on a distinct bundle field) to decide whether to throw. Components may
|
||||
* safely hold this client while logged out; only calling it throws. To branch
|
||||
* on auth state, use {@link useMaybePdsClient} instead.
|
||||
*/
|
||||
export function usePdsClient(): Client {
|
||||
const bundle = useContext(BundleContext)
|
||||
return bundle?.accountClient ?? getUnauthenticatedClient()
|
||||
return bundle?.session ? bundle.bskyClient : getUnauthenticatedClient()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -907,8 +928,9 @@ export function useChatClient(): Client {
|
||||
}
|
||||
|
||||
/**
|
||||
* The account (PDS) lex {@link Client} for the active account, or `null` when
|
||||
* there is no active session (logged out, or used outside the provider).
|
||||
* The authenticated merged Bluesky client for the active account (the same
|
||||
* instance {@link usePdsClient} returns when signed in), or `null` when there is
|
||||
* no active session (logged out, or used outside the provider).
|
||||
*
|
||||
* The escape hatch for the rare component that genuinely renders a logged-out
|
||||
* branch and must decide whether a write path is available. Prefer
|
||||
@@ -917,7 +939,7 @@ export function useChatClient(): Client {
|
||||
*/
|
||||
export function useMaybePdsClient(): Client | null {
|
||||
const bundle = useContext(BundleContext)
|
||||
return bundle?.session ? bundle.accountClient : null
|
||||
return bundle?.session ? bundle.bskyClient : null
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -46,9 +46,14 @@ export function readLabelers(did: string): string[] | undefined {
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply an account's subscribed labeler DIDs to a live appview client. The lex
|
||||
* `Client` rebuilds the header per request, so this takes effect on the next
|
||||
* request without a client rebuild.
|
||||
* Apply an account's subscribed labeler DIDs to a live client. The lex `Client`
|
||||
* rebuilds the header per request, so this takes effect on the next request
|
||||
* without a client rebuild.
|
||||
*
|
||||
* These labelers ride the `atproto-accept-labelers` header, which lex-client
|
||||
* 0.3.0 emits only on raw/query calls: record helpers default `labelers = null`
|
||||
* per call, stripping the header. That is fine here - labelers only matter for
|
||||
* the read/query calls moderation cares about.
|
||||
*
|
||||
* We filter out the Bluesky moderation labeler: it is already asserted globally
|
||||
* via `Client.appLabelers` (with `;redact`), and a user "subscribing" to it
|
||||
@@ -76,7 +81,8 @@ export function configureModerationForGuest() {
|
||||
* per-account labelers already applied, in the same tick.
|
||||
*
|
||||
* Takes the whole {@link SessionBundle} so it can apply per-account labelers to
|
||||
* the authed appview client (`bundle.appviewClient`, backing `useLexClient()`).
|
||||
* the single merged Bluesky client (`bundle.bskyClient`, backing
|
||||
* `useLexClient()`).
|
||||
*/
|
||||
export function configureModerationForAccount(
|
||||
bundle: SessionBundle,
|
||||
@@ -97,7 +103,7 @@ export function configureModerationForAccount(
|
||||
// The code below is actually relevant to production (and isn't global).
|
||||
const labelerDids = readLabelers(account.did)
|
||||
if (labelerDids) {
|
||||
applyLabelersToClient(bundle.appviewClient, labelerDids)
|
||||
applyLabelersToClient(bundle.bskyClient, labelerDids)
|
||||
} else {
|
||||
/*
|
||||
* No cached labelers yet (first session on this device), so the initial
|
||||
@@ -115,12 +121,13 @@ function switchToBskyAppLabeler() {
|
||||
|
||||
/**
|
||||
* In the test environment, swap the global app labeler for the test-env
|
||||
* moderation authority, resolving its handle via the bundle's authed appview
|
||||
* client.
|
||||
* moderation authority, resolving its handle via the bundle's merged Bluesky
|
||||
* client. This is a raw call, so it inherits the appview proxy - which is
|
||||
* correct, as `resolveHandle` is served by the appview.
|
||||
*/
|
||||
async function trySwitchToTestAppLabeler(bundle: SessionBundle) {
|
||||
const did = (
|
||||
await bundle.appviewClient
|
||||
await bundle.bskyClient
|
||||
.call(com.atproto.identity.resolveHandle, {handle: 'mod-authority.test'})
|
||||
.catch(_ => undefined)
|
||||
)?.did
|
||||
|
||||
@@ -36,8 +36,7 @@ import {unsafeGetAndComputeAgeAssurance} from '#/ageAssurance/state'
|
||||
import {features} from '#/analytics'
|
||||
import {type app} from '#/lexicons'
|
||||
import {
|
||||
buildAccountClient,
|
||||
buildAppviewClient,
|
||||
buildBskyClient,
|
||||
buildChatClient,
|
||||
getPublicLexClient,
|
||||
getUnauthenticatedClient,
|
||||
@@ -266,10 +265,12 @@ function deriveServiceUrl(session: PasswordSession | null): URL {
|
||||
export type SessionBundle = {
|
||||
/** The single auth core. Never exposed to the reducer. */
|
||||
session: PasswordSession
|
||||
/** Account (writes/records) client - talks to the user's PDS. */
|
||||
accountClient: Client
|
||||
/** Authed appview client (proxied, with labelers). */
|
||||
appviewClient: Client
|
||||
/**
|
||||
* The single authed Bluesky client (merged account + appview). Proxied to
|
||||
* the Bluesky appview and carrying this account's labelers; record helpers
|
||||
* on it auto-target the user's PDS. See {@link buildBskyClient}.
|
||||
*/
|
||||
bskyClient: Client
|
||||
/** Chat client (proxied to `did:web:api.bsky.chat#bsky_chat`). */
|
||||
chatClient: Client
|
||||
/**
|
||||
@@ -306,18 +307,17 @@ export function registerBundleKillSwitch(
|
||||
}
|
||||
|
||||
/**
|
||||
* Assemble a {@link SessionBundle} from a live session: the account, appview,
|
||||
* and chat clients, all read-through views over the one session.
|
||||
* Assemble a {@link SessionBundle} from a live session: the merged Bluesky
|
||||
* client and the chat client, both read-through views over the one session.
|
||||
*/
|
||||
export function buildBundle(session: PasswordSession): SessionBundle {
|
||||
return {
|
||||
session,
|
||||
accountClient: buildAccountClient(session),
|
||||
/*
|
||||
* Starts with an empty per-account labeler set; configureModerationForAccount
|
||||
* applies this account's labelers afterwards.
|
||||
*/
|
||||
appviewClient: buildAppviewClient(session, []),
|
||||
bskyClient: buildBskyClient(session, []),
|
||||
chatClient: buildChatClient(session),
|
||||
/* A getter keeps `.service` live with the session's state (destroyed -> public). */
|
||||
get service() {
|
||||
@@ -415,13 +415,19 @@ export function makeSessionHooks(
|
||||
}
|
||||
|
||||
/**
|
||||
* The public (logged-out) bundle. Its appview client points at the public
|
||||
* appview; the write/chat clients are the throwing unauthenticated client.
|
||||
* The public (logged-out) bundle. Its `bskyClient` is the public appview client
|
||||
* (reads work logged out); the chat client is the throwing unauthenticated
|
||||
* client.
|
||||
*/
|
||||
export type PublicSessionBundle = {
|
||||
session: null
|
||||
accountClient: Client
|
||||
appviewClient: Client
|
||||
/**
|
||||
* The public appview client (reads work logged out). Logged-out WRITE
|
||||
* protection is NOT a property of this client - it lives in the
|
||||
* `usePdsClient` hook's fallback (which returns the throwing unauthenticated
|
||||
* client when there is no session; see index.tsx).
|
||||
*/
|
||||
bskyClient: Client
|
||||
/**
|
||||
* The throwing unauthenticated client (NOT the public client): chat is
|
||||
* meaningless logged out, and `useChatClient()` must fail loudly rather than
|
||||
@@ -442,13 +448,16 @@ export function createPublicSessionBundle(): PublicSessionBundle {
|
||||
return {
|
||||
session: null,
|
||||
/*
|
||||
* The account (PDS) and chat clients throw on use when logged out, so an
|
||||
* unauthenticated write or chat call fails loudly instead of silently
|
||||
* targeting the public appview. Reads keep the public client (appviewClient),
|
||||
* which reads public data without auth.
|
||||
* The public client reads public data without auth. Logged-out write
|
||||
* protection is enforced by the usePdsClient hook (which falls back to the
|
||||
* throwing unauthenticated client when there is no session), NOT here - see
|
||||
* index.tsx.
|
||||
*/
|
||||
bskyClient: publicClient,
|
||||
/*
|
||||
* The chat client throws on use when logged out, so an unauthenticated chat
|
||||
* call fails loudly instead of silently targeting the public appview.
|
||||
*/
|
||||
accountClient: getUnauthenticatedClient(),
|
||||
appviewClient: publicClient,
|
||||
chatClient: getUnauthenticatedClient(),
|
||||
service: new URL(PUBLIC_BSKY_SERVICE),
|
||||
}
|
||||
@@ -498,10 +507,7 @@ export async function createSessionBundleAndResume(
|
||||
storedAccount
|
||||
|
||||
configureModerationForAccount(bundle, earlyAccount)
|
||||
const aa = prefetchAgeAssuranceServerData({
|
||||
appviewClient: bundle.appviewClient,
|
||||
accountClient: bundle.accountClient,
|
||||
})
|
||||
const aa = prefetchAgeAssuranceServerData({client: bundle.bskyClient})
|
||||
await Promise.all([gates, aa])
|
||||
/*
|
||||
* Re-snapshot AFTER prep, right before arm(). A 401 during a prep request
|
||||
@@ -559,10 +565,7 @@ export async function createSessionBundleAndLogin(
|
||||
|
||||
const gates = features.refresh({strategy: 'prefer-fresh-gates'})
|
||||
configureModerationForAccount(bundle, earlyAccount)
|
||||
const aa = prefetchAgeAssuranceServerData({
|
||||
appviewClient: bundle.appviewClient,
|
||||
accountClient: bundle.accountClient,
|
||||
})
|
||||
const aa = prefetchAgeAssuranceServerData({client: bundle.bskyClient})
|
||||
await Promise.all([gates, aa])
|
||||
/*
|
||||
* Re-snapshot AFTER prep, right before arm(): a 401 during a prep request
|
||||
@@ -650,17 +653,14 @@ export async function createSessionBundleAndCreateAccount(
|
||||
setBirthdateForDid({did: earlyAccount.did, birthdate})
|
||||
snoozeBirthdateUpdateAllowedForDid(earlyAccount.did)
|
||||
// do this last
|
||||
const aa = prefetchAgeAssuranceServerData({
|
||||
appviewClient: bundle.appviewClient,
|
||||
accountClient: bundle.accountClient,
|
||||
})
|
||||
const aa = prefetchAgeAssuranceServerData({client: bundle.bskyClient})
|
||||
|
||||
// Not awaited so that we can still get into onboarding.
|
||||
// This is OK because we won't let you toggle adult stuff until you set the date.
|
||||
if (IS_PROD_SERVICE(service)) {
|
||||
void Promise.allSettled([
|
||||
networkRetry(3, () => {
|
||||
return bundle.accountClient.call(setPersonalDetails, {
|
||||
return bundle.bskyClient.call(setPersonalDetails, {
|
||||
birthDate,
|
||||
})
|
||||
}).catch(e => {
|
||||
@@ -670,7 +670,7 @@ export async function createSessionBundleAndCreateAccount(
|
||||
throw e
|
||||
}),
|
||||
networkRetry(3, () => {
|
||||
return bundle.accountClient.call(upsertProfile, prev => {
|
||||
return bundle.bskyClient.call(upsertProfile, prev => {
|
||||
const next: Partial<app.bsky.actor.profile.Main> = prev || {}
|
||||
next.displayName = handle
|
||||
next.createdAt = createdAt
|
||||
@@ -683,7 +683,7 @@ export async function createSessionBundleAndCreateAccount(
|
||||
throw e
|
||||
}),
|
||||
networkRetry(1, () => {
|
||||
return bundle.accountClient.call(overwriteSavedFeeds, [
|
||||
return bundle.bskyClient.call(overwriteSavedFeeds, [
|
||||
{
|
||||
...DISCOVER_SAVED_FEED,
|
||||
id: TID.nextStr(),
|
||||
@@ -704,7 +704,7 @@ export async function createSessionBundleAndCreateAccount(
|
||||
const {flags} = unsafeGetAndComputeAgeAssurance({did: earlyAccount.did})
|
||||
if (flags?.chatDisabled || flags?.groupChatDisabled) {
|
||||
void restrictChatSettings({
|
||||
client: bundle.accountClient,
|
||||
client: bundle.bskyClient,
|
||||
restrictIncoming: flags.chatDisabled,
|
||||
restrictGroupInvites: flags.groupChatDisabled,
|
||||
})
|
||||
@@ -721,7 +721,7 @@ export async function createSessionBundleAndCreateAccount(
|
||||
} else {
|
||||
void Promise.allSettled([
|
||||
networkRetry(3, () => {
|
||||
return bundle.accountClient.call(setPersonalDetails, {
|
||||
return bundle.bskyClient.call(setPersonalDetails, {
|
||||
birthDate,
|
||||
})
|
||||
}).catch(e => {
|
||||
@@ -731,7 +731,7 @@ export async function createSessionBundleAndCreateAccount(
|
||||
throw e
|
||||
}),
|
||||
networkRetry(3, () => {
|
||||
return bundle.accountClient.call(upsertProfile, prev => {
|
||||
return bundle.bskyClient.call(upsertProfile, prev => {
|
||||
const next: Partial<app.bsky.actor.profile.Main> = prev || {}
|
||||
next.createdAt = prev?.createdAt || toDatetimeString(new Date())
|
||||
return next
|
||||
|
||||
Reference in New Issue
Block a user