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
+13 -9
View File
@@ -21,7 +21,7 @@ import {
snoozeBirthdateUpdateAllowedForDid,
} from '#/state/birthdate'
import {fetchActorDeclarationRecord} from '#/state/queries/messages/actor-declaration'
import {useAgent, useSession} from '#/state/session'
import {type SessionAgent, useAgent, useSession} from '#/state/session'
import {DEVICE_SIGNALS_SUPPORTED} from '#/ageAssurance/const'
import * as debug from '#/ageAssurance/debug'
import {logger} from '#/ageAssurance/logger'
@@ -63,7 +63,7 @@ const [, cacheHydrationPromise] = persistQueryClient({
persister,
})
export function getDidFromAgentSession(agent: AtpAgent) {
export function getDidFromAgentSession(agent: SessionAgent) {
const sessionManager = agent.sessionManager
if (!sessionManager || !sessionManager.did) return
return sessionManager.did
@@ -187,7 +187,7 @@ export function useConfigQuery() {
export function createServerStateQueryKey({did}: {did: string}) {
return ['serverState', did]
}
export async function getServerState({agent}: {agent: AtpAgent}) {
export async function getServerState({agent}: {agent: SessionAgent}) {
if (debug.enabled && debug.serverState)
return debug.resolve(debug.serverState)
const geolocation = device.get(['mergedGeolocation'])
@@ -218,7 +218,7 @@ export function getServerStateFromCache({
createServerStateQueryKey({did}),
)
}
export async function prefetchServerState({agent}: {agent: AtpAgent}) {
export async function prefetchServerState({agent}: {agent: SessionAgent}) {
const did = getDidFromAgentSession(agent)
if (!did) return
@@ -245,7 +245,7 @@ export async function prefetchServerState({agent}: {agent: AtpAgent}) {
})
}
}
export async function refetchServerState({agent}: {agent: AtpAgent}) {
export async function refetchServerState({agent}: {agent: SessionAgent}) {
const did = getDidFromAgentSession(agent)
if (!did) return
logger.debug(`refetchServerState: fetching...`)
@@ -344,7 +344,7 @@ export function createOtherRequiredDataQueryKey({did}: {did: string}) {
async function getOtherRequiredData({
agent,
}: {
agent: AtpAgent
agent: SessionAgent
}): Promise<OtherRequiredData> {
if (debug.enabled) return debug.resolve(debug.otherRequiredData)
const did = getDidFromAgentSession(agent)
@@ -426,7 +426,11 @@ export function setOtherRequiredDataActorDeclarationCache({
next,
)
}
export async function prefetchOtherRequiredData({agent}: {agent: AtpAgent}) {
export async function prefetchOtherRequiredData({
agent,
}: {
agent: SessionAgent
}) {
const did = getDidFromAgentSession(agent)
if (!did) return
@@ -577,7 +581,7 @@ export function setDeviceSignalsForRegion({
prev => ({...prev, [regionKey]: signals}),
)
}
export async function prefetchDeviceSignals({agent}: {agent: AtpAgent}) {
export async function prefetchDeviceSignals({agent}: {agent: SessionAgent}) {
const did = getDidFromAgentSession(agent)
if (!did) return
@@ -659,7 +663,7 @@ export function useDeviceSignalsQuery() {
/**
* Helper to prefetch all age assurance data from the server.
*/
export function prefetchAgeAssuranceServerData({agent}: {agent: AtpAgent}) {
export function prefetchAgeAssuranceServerData({agent}: {agent: SessionAgent}) {
return Promise.allSettled([
// config fetch initiated at the top of the App.platform.tsx files, awaited here
configPrefetchPromise,
+15 -8
View File
@@ -1,5 +1,5 @@
import {Platform} from 'react-native'
import {type AppBskyAgeassuranceBegin, AtpAgent} from '@atproto/api'
import {Client} from '@atproto/lex-client'
import {useMutation} from '@tanstack/react-query'
import {wait} from '#/lib/async/wait'
@@ -15,6 +15,7 @@ import {logger} from '#/ageAssurance/logger'
import {useAnalytics} from '#/analytics'
import {BLUESKY_PROXY_DID} from '#/env'
import {useGeolocation} from '#/geolocation'
import {app} from '#/lexicons'
const IS_DEV_ENV = BLUESKY_PROXY_DID !== PUBLIC_APPVIEW_DID
const APPVIEW = IS_DEV_ENV ? DEV_ENV_APPVIEW : PUBLIC_APPVIEW
@@ -28,7 +29,7 @@ export function useBeginAgeAssurance() {
return useMutation({
async mutationFn(
props: Omit<
AppBskyAgeassuranceBegin.InputSchema,
app.bsky.ageassurance.begin.$InputBody,
'countryCode' | 'regionCode'
>,
) {
@@ -45,10 +46,16 @@ export function useBeginAgeAssurance() {
lxm: `app.bsky.ageassurance.begin`,
})
const appView = new AtpAgent({service: APPVIEW})
appView.sessionManager.session = {...agent.session!}
appView.sessionManager.session.accessJwt = token
appView.sessionManager.session.refreshJwt = ''
/*
* A non-refreshing throwaway client scoped to the service-auth token: it
* has no session, so nothing can refresh it. Requests go straight to the
* appview with the token as a static Authorization header (a raw client,
* unlike a session, is allowed to preset that header).
*/
const scopedClient = new Client({
service: APPVIEW,
headers: {authorization: `Bearer ${token}`},
})
ax.metric('ageAssurance:api:begin', {
platform: Platform.OS,
@@ -60,9 +67,9 @@ export function useBeginAgeAssurance() {
* 2s wait is good actually. Email sending takes a hot sec and this helps
* ensure the email is ready for the user once they open their inbox.
*/
const {data} = await wait(
const data = await wait(
2e3,
appView.app.bsky.ageassurance.begin({
scopedClient.call(app.bsky.ageassurance.begin, {
...props,
countryCode,
regionCode,