rework session layer on PasswordSession + three-client pattern

- PasswordSession (@atproto/lex-password-session) is now the single auth
  core; SessionAgent bridges it to the base Agent from @atproto/api so
  the ~148 useAgent() consumer files keep working unchanged
- SessionBundle carries {session, agent, accountClient, appviewClient};
  useLexClient() now returns the authed appview client, new
  useAppviewClient()/usePdsClient() hooks
- reducer: OpaqueSessionBundle, new replaced-current-bundle action for
  network-free cross-tab same-did sync (PasswordSession is immutable, so
  the bundle is rebuilt instead of mutated in place)
- moderation: AtpAgent.configure -> Client.configure + Agent.configure,
  per-account labelers on both the bridge and the appview client
- push-token unregistration uses temporary PasswordSessions
- hard-tail migrations: SignupQueued refresh via resumeSession shim,
  useBeginAgeAssurance scoped-token raw Client, useAccountEmailState
  reads currentAccount
- PDS routing preserved on the no-network resume fast path by
  synthesizing a minimal didDoc from the persisted pdsUrl

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Samuel Newman
2026-07-16 14:40:01 +03:00
parent 105e68c12c
commit bf8b6bc0c7
58 changed files with 2826 additions and 996 deletions
+49 -2
View File
@@ -1,7 +1,10 @@
import {type AtpAgent} from '@atproto/api'
import {Client} from '@atproto/lex-client'
import {type PasswordSession} from '@atproto/lex-password-session'
import {api} from '@bsky.app/sdk'
import {PUBLIC_BSKY_SERVICE} from '#/lib/constants'
import {networkAwareFetch} from './session-core'
/**
* Stable per-agent cache of lex `Client` instances. We never reuse an
@@ -58,11 +61,55 @@ export function agentToLexClient(agent: AtpAgent): Client {
*/
let publicClient: Client | undefined
function getPublicLexClient(): Client {
publicClient ??= new Client(PUBLIC_BSKY_SERVICE)
export function getPublicLexClient(): Client {
/*
* Pass networkAwareFetch so the unauthenticated public path feeds the same
* reachability signal as the session-backed clients (see session-core).
*/
publicClient ??= new Client({
service: PUBLIC_BSKY_SERVICE,
fetch: networkAwareFetch,
})
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`).
*
* The session already owns its own `fetch` (networkAwareFetch, set at
* construction), so we intentionally do NOT pass `fetch` here: a `Client`
* built over an existing `Agent`/session uses that agent's fetch.
*/
export function buildAccountClient(session: PasswordSession): Client {
return new Client(session)
}
/**
* Build the authed appview client over a {@link PasswordSession}.
*
* Requests are proxied to the Bluesky appview (`atproto-proxy:
* did:web:api.bsky.app#bsky_appview`) and carry the per-instance labelers.
* The Bluesky moderation labeler (`api.moderation.did`) is always included as
* a base labeler because sending ANY `atproto-accept-labelers` header replaces
* the server-side default - so we must re-assert it to keep it active.
*/
export function buildAppviewClient(
session: PasswordSession,
labelerDids: string[],
): Client {
return new Client(session, {
service: api.app.service,
/* labelerDids are validated DID strings; cast to the DidString template type */
labelers: [
api.moderation.did,
...labelerDids,
] as `did:${string}:${string}`[],
})
}
/**
* Unauthenticated lex {@link Client} for public appview reads. A process-wide
* singleton, so its identity is stable across renders.