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
+3 -3
View File
@@ -1,20 +1,20 @@
import {
AppBskyFeedDefs,
type AppBskyFeedGetAuthorFeed as GetAuthorFeed,
type AtpAgent,
} from '@atproto/api'
import {type SessionAgent} from '#/state/session'
import {type FeedAPI, type FeedAPIResponse} from './types'
export class AuthorFeedAPI implements FeedAPI {
agent: AtpAgent
agent: SessionAgent
_params: GetAuthorFeed.QueryParams
constructor({
agent,
feedParams,
}: {
agent: AtpAgent
agent: SessionAgent
feedParams: GetAuthorFeed.QueryParams
}) {
this.agent = agent
+3 -2
View File
@@ -9,11 +9,12 @@ import {
getAppLanguageAsContentLanguage,
getContentLanguages,
} from '#/state/preferences/languages'
import {type SessionAgent} from '#/state/session'
import {type FeedAPI, type FeedAPIResponse} from './types'
import {createBskyTopicsHeader, isBlueskyOwnedFeed} from './utils'
export class CustomFeedAPI implements FeedAPI {
agent: AtpAgent
agent: SessionAgent
params: GetCustomFeed.QueryParams
userInterests?: string
@@ -22,7 +23,7 @@ export class CustomFeedAPI implements FeedAPI {
feedParams,
userInterests,
}: {
agent: AtpAgent
agent: SessionAgent
feedParams: GetCustomFeed.QueryParams
userInterests?: string
}) {
+4 -3
View File
@@ -1,12 +1,13 @@
import {type AppBskyFeedDefs, type AtpAgent} from '@atproto/api'
import {type AppBskyFeedDefs} from '@atproto/api'
import {DEMO_FEED} from '#/lib/demo'
import {type SessionAgent} from '#/state/session'
import {type FeedAPI, type FeedAPIResponse} from './types'
export class DemoFeedAPI implements FeedAPI {
agent: AtpAgent
agent: SessionAgent
constructor({agent}: {agent: AtpAgent}) {
constructor({agent}: {agent: SessionAgent}) {
this.agent = agent
}
+4 -3
View File
@@ -1,11 +1,12 @@
import {type AppBskyFeedDefs, type AtpAgent} from '@atproto/api'
import {type AppBskyFeedDefs} from '@atproto/api'
import {type SessionAgent} from '#/state/session'
import {type FeedAPI, type FeedAPIResponse} from './types'
export class FollowingFeedAPI implements FeedAPI {
agent: AtpAgent
agent: SessionAgent
constructor({agent}: {agent: AtpAgent}) {
constructor({agent}: {agent: SessionAgent}) {
this.agent = agent
}
+4 -3
View File
@@ -1,6 +1,7 @@
import {type AppBskyFeedDefs, type AtpAgent} from '@atproto/api'
import {type AppBskyFeedDefs} from '@atproto/api'
import {PROD_DEFAULT_FEED} from '#/lib/constants'
import {type SessionAgent} from '#/state/session'
import {CustomFeedAPI} from './custom'
import {FollowingFeedAPI} from './following'
import {type FeedAPI, type FeedAPIResponse} from './types'
@@ -27,7 +28,7 @@ export const FALLBACK_MARKER_POST: AppBskyFeedDefs.FeedViewPost = {
}
export class HomeFeedAPI implements FeedAPI {
agent: AtpAgent
agent: SessionAgent
following: FollowingFeedAPI
discover: CustomFeedAPI
usingDiscover = false
@@ -39,7 +40,7 @@ export class HomeFeedAPI implements FeedAPI {
agent,
}: {
userInterests?: string
agent: AtpAgent
agent: SessionAgent
}) {
this.agent = agent
this.following = new FollowingFeedAPI({agent})
+3 -3
View File
@@ -1,20 +1,20 @@
import {
type AppBskyFeedDefs,
type AppBskyFeedGetActorLikes as GetActorLikes,
type AtpAgent,
} from '@atproto/api'
import {type SessionAgent} from '#/state/session'
import {type FeedAPI, type FeedAPIResponse} from './types'
export class LikesFeedAPI implements FeedAPI {
agent: AtpAgent
agent: SessionAgent
params: GetActorLikes.QueryParams
constructor({
agent,
feedParams,
}: {
agent: AtpAgent
agent: SessionAgent
feedParams: GetActorLikes.QueryParams
}) {
this.agent = agent
+8 -11
View File
@@ -1,8 +1,4 @@
import {
type AppBskyFeedDefs,
type AppBskyFeedGetTimeline,
type AtpAgent,
} from '@atproto/api'
import {type AppBskyFeedDefs, type AppBskyFeedGetTimeline} from '@atproto/api'
import shuffle from 'lodash.shuffle'
import {bundleAsync} from '#/lib/async/bundle'
@@ -10,6 +6,7 @@ import {timeout} from '#/lib/async/timeout'
import {feedUriToHref} from '#/lib/strings/url-helpers'
import {getContentLanguages} from '#/state/preferences/languages'
import {type FeedParams} from '#/state/queries/post-feed'
import {type SessionAgent} from '#/state/session'
import {FeedTuner} from '../feed-manip'
import {type FeedTunerFn} from '../feed-manip'
import {
@@ -24,7 +21,7 @@ const POST_AGE_CUTOFF = 60e3 * 60 * 24 // 24hours
export class MergeFeedAPI implements FeedAPI {
userInterests?: string
agent: AtpAgent
agent: SessionAgent
params: FeedParams
feedTuners: FeedTunerFn[]
following: MergeFeedSource_Following
@@ -39,7 +36,7 @@ export class MergeFeedAPI implements FeedAPI {
feedTuners,
userInterests,
}: {
agent: AtpAgent
agent: SessionAgent
feedParams: FeedParams
feedTuners: FeedTunerFn[]
userInterests?: string
@@ -175,7 +172,7 @@ export class MergeFeedAPI implements FeedAPI {
}
class MergeFeedSource {
agent: AtpAgent
agent: SessionAgent
feedTuners: FeedTunerFn[]
sourceInfo: ReasonFeedSource | undefined
cursor: string | undefined = undefined
@@ -186,7 +183,7 @@ class MergeFeedSource {
agent,
feedTuners,
}: {
agent: AtpAgent
agent: SessionAgent
feedTuners: FeedTunerFn[]
}) {
this.agent = agent
@@ -253,7 +250,7 @@ class MergeFeedSource_Following extends MergeFeedSource {
}
class MergeFeedSource_Custom extends MergeFeedSource {
agent: AtpAgent
agent: SessionAgent
minDate: Date
feedUri: string
userInterests?: string
@@ -264,7 +261,7 @@ class MergeFeedSource_Custom extends MergeFeedSource {
feedTuners,
userInterests,
}: {
agent: AtpAgent
agent: SessionAgent
feedUri: string
feedTuners: FeedTunerFn[]
userInterests?: string
+7 -7
View File
@@ -7,7 +7,6 @@ import {
type AppBskyEmbedRecordWithMedia,
type AppBskyEmbedVideo,
AppBskyFeedPost,
type AtpAgent,
BlobRef,
ChatBskyGroupDefs,
type ComAtprotoLabelDefs,
@@ -35,6 +34,7 @@ import {
createThreadgateRecord,
threadgateAllowUISettingToAllowRecordValue,
} from '#/state/queries/threadgate'
import {type SessionAgent} from '#/state/session'
import {
type EmbedDraft,
type PostDraft,
@@ -54,7 +54,7 @@ interface PostOpts {
}
export async function post(
agent: AtpAgent,
agent: SessionAgent,
queryClient: QueryClient,
opts: PostOpts,
) {
@@ -196,7 +196,7 @@ export async function post(
return {uris}
}
async function resolveRT(agent: AtpAgent, richtext: RichText) {
async function resolveRT(agent: SessionAgent, richtext: RichText) {
const trimmedText = richtext.text
// Trim leading whitespace-only lines (but don't break ASCII art).
.replace(/^(\s*\n)+/, '')
@@ -216,7 +216,7 @@ export class ReplyDeletedError extends Error {
}
}
async function resolveReply(agent: AtpAgent, replyTo: string) {
async function resolveReply(agent: SessionAgent, replyTo: string) {
const {data} = await agent.app.bsky.feed.getPosts({
uris: [replyTo],
})
@@ -249,7 +249,7 @@ async function resolveReply(agent: AtpAgent, replyTo: string) {
}
async function resolveEmbed(
agent: AtpAgent,
agent: SessionAgent,
queryClient: QueryClient,
draft: PostDraft,
onStateChange: ((state: string) => void) | undefined,
@@ -308,7 +308,7 @@ async function resolveEmbed(
}
async function resolveMedia(
agent: AtpAgent,
agent: SessionAgent,
queryClient: QueryClient,
embedDraft: EmbedDraft,
onStateChange: ((state: string) => void) | undefined,
@@ -481,7 +481,7 @@ async function resolveMedia(
}
async function resolveRecord(
agent: AtpAgent,
agent: SessionAgent,
queryClient: QueryClient,
uri: string,
): Promise<ComAtprotoRepoStrongRef.Main> {
+4 -4
View File
@@ -1,7 +1,6 @@
import {
type AppBskyFeedDefs,
type AppBskyGraphDefs,
type AtpAgent,
type ComAtprotoRepoStrongRef,
} from '@atproto/api'
import {AtUri} from '@atproto/api'
@@ -28,6 +27,7 @@ import {
import {type ComposerImage} from '#/state/gallery'
import {createComposerImage} from '#/state/gallery'
import {type ChatInvitePreview} from '#/state/queries/join-links'
import {type SessionAgent} from '#/state/session'
import {type Gif} from '#/features/gifPicker/types'
import {createGIFDescription} from '../gif-alt-text'
@@ -95,7 +95,7 @@ export class EmbeddingDisabledError extends Error {
}
export async function resolveLink(
agent: AtpAgent,
agent: SessionAgent,
uri: string,
): Promise<ResolvedLink> {
if (isShortLink(uri)) {
@@ -217,7 +217,7 @@ export async function resolveLink(
}
export async function resolveGif(
agent: AtpAgent,
agent: SessionAgent,
gif: Gif,
): Promise<ResolvedExternalLink> {
const gifUrl = gif.media_formats.gif.url
@@ -259,7 +259,7 @@ function getFileSlug(url: string | undefined): string | undefined {
}
async function resolveExternal(
agent: AtpAgent,
agent: SessionAgent,
uri: string,
): Promise<ResolvedExternalLink> {
const result = await getLinkMeta(agent, uri)
+2 -2
View File
@@ -1,5 +1,5 @@
import {copyAsync} from 'expo-file-system/legacy'
import {type AtpAgent, type ComAtprotoRepoUploadBlob} from '@atproto/api'
import {type Agent, type ComAtprotoRepoUploadBlob} from '@atproto/api'
import {safeDeleteAsync} from '#/lib/media/manip'
@@ -7,7 +7,7 @@ import {safeDeleteAsync} from '#/lib/media/manip'
* @param encoding Allows overriding the blob's type
*/
export async function uploadBlob(
agent: AtpAgent,
agent: Agent,
input: string | Blob,
encoding?: string,
): Promise<ComAtprotoRepoUploadBlob.Response> {
+2 -2
View File
@@ -1,4 +1,4 @@
import {type AtpAgent, type ComAtprotoRepoUploadBlob} from '@atproto/api'
import {type Agent, type ComAtprotoRepoUploadBlob} from '@atproto/api'
/**
* @note It is recommended, on web, to use the `file` instance of the file
@@ -7,7 +7,7 @@ import {type AtpAgent, type ComAtprotoRepoUploadBlob} from '@atproto/api'
* be passed directly to this function.
*/
export async function uploadBlob(
agent: AtpAgent,
agent: Agent,
input: string | Blob,
encoding?: string,
): Promise<ComAtprotoRepoUploadBlob.Response> {