Files
bsky-social-app/src/state/session/clients.ts
T
2026-07-31 16:07:52 +03:00

115 lines
3.9 KiB
TypeScript

import {type Agent, type Client} from '@atproto/lex'
import {type PasswordSession} from '@atproto/lex-password-session'
import {
BLUESKY_PROXY_HEADER,
CHAT_PROXY_SERVICE,
PUBLIC_BSKY_SERVICE,
} from '#/lib/constants'
import {createLexClient} from '#/lib/lexClient'
import {networkAwareFetch} from './network'
/**
* Lazily-constructed unauthenticated client pointed at the public appview. It
* hits {@link PUBLIC_BSKY_SERVICE} directly, so no proxying is required.
*/
let publicClient: Client | undefined
export function getPublicAppviewClient(): Client {
/*
* Pass networkAwareFetch so the unauthenticated public path feeds the same
* reachability signal as the session-backed clients.
*/
publicClient ??= createLexClient({
service: PUBLIC_BSKY_SERVICE,
fetch: networkAwareFetch,
})
return publicClient
}
/**
* Build the chat client over a {@link PasswordSession}.
*
* {@link CHAT_PROXY_SERVICE} (`${CHAT_PROXY_DID}#bsky_chat`, default
* `did:web:api.bsky.chat#bsky_chat`) is passed as the client's `service`, so
* lex-client sets `atproto-proxy: <that value>` on every request and
* `chat.bsky.*` calls are proxied to the chat service. The DID is read from the
* env-configurable `CHAT_PROXY_DID` (via `EXPO_PUBLIC_CHAT_PROXY_DID`) rather
* than the hard-coded SDK constant, so it can be retargeted per environment.
*/
export function buildChatClient(agent: Agent): Client {
return createLexClient(agent, {service: CHAT_PROXY_SERVICE})
}
/** Thrown when a write/auth-only client is used with no active session. */
export class NotAuthenticatedError extends Error {
constructor(op = 'this operation') {
super(`Not authenticated: ${op} requires an active session`)
this.name = 'NotAuthenticatedError'
}
}
/**
* A stable {@link Client} that throws {@link NotAuthenticatedError} on any
* request, before any network I/O. Used as the logged-out value of the
* write/auth-only hooks (`usePdsClient`/`useChatClient`) so an unauthenticated
* call fails immediately and legibly instead of silently hitting the public
* appview (which would 404/405 with an opaque error).
*
* A lazily-constructed process-wide singleton, so its identity is stable across
* renders - safe to use in React Query keys and as a hook return value.
*/
let unauthedClient: Client | undefined
export function getUnauthenticatedThrowingClient(): Client {
unauthedClient ??= createLexClient({
did: undefined,
fetchHandler: () => {
throw new NotAuthenticatedError()
},
})
return unauthedClient
}
/**
* Build the signed-in appview client. Raw calls inherit the configured appview
* proxy; record helpers still target the account host by default.
*
* The Bluesky moderation labeler (`api.moderation.did`) is deliberately NOT
* 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.
*
* 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(
agent: Agent,
labelerDids: string[],
): Client {
return createLexClient(agent, {
service: BLUESKY_PROXY_HEADER.get(),
labelers: labelerDids as `did:${string}:${string}`[],
})
}
/** Build the signed-in account-host client with no service proxy. */
export function buildPdsClient(agent: Agent): Client {
return createLexClient(agent)
}
/** Route client requests to a PDS while retaining the session's auth lifecycle. */
export function routeSessionToPds(
session: PasswordSession,
pdsUrl: string,
): Agent {
return {
did: session.did,
fetchHandler(path, init) {
const url = new URL(path, pdsUrl).href
// PasswordSession preserves absolute inputs while applying auth/refresh.
return session.fetchHandler(url, init)
},
}
}