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>
The pnpm override pinned @atproto/lex-client to 0.2.4, predating both
lex-client 0.3.0 and sdk preview.2 - which assumes 0.3.0 semantics
(per-call service: null is a no-op on 0.2.4). 0.3.0 also returns
non-JSON encodings raw, so the ExportCarDialog fetchHandler
workaround for mis-parsed application/jsonl is unnecessary.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Three production logout root causes (Logout issues Linear project):
- a stale tab's ExpiredToken no longer clears newer tokens; the expiry
path compares against the latest persisted generation and rescues
from it (loop-guarded) instead of logging every tab out
- follower-tab resumeSession disposes its bundle and bails when a
logout or account switch raced it; aborted resumes now dispose too
- the reducer drops ALL events from non-current bundles, so a refresh
landing mid-logout can't resurrect cleared tokens; session-dropped
toast gated on the bundle still being current
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
With moderation config sync, the bundle swap no longer defers behind
an async labeler read. The race guard from the review stays as a
defensive invariant against reintroducing an await.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The per-account labeler cache was the only async dependency in
configureModerationForAccount. MMKV-backed sync reads let session
bundle prep apply labelers in the same tick. No AsyncStorage
backfill: the cache is rewritten on every preferences fetch.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The per-client labelers list also carried api.moderation.did, which
the header builder emits plain alongside the global did;redact entry -
a duplicate that could weaken redaction. Per-client lists now carry
only account subscriptions.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
lex-client's strict mode rejects the legacy blob reference format
({cid, mimeType}) still present in older records; the old stack
tolerated it. Route all client construction through the factory.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The phase-2 session rework dropped `metadata` from the child logger's
useMemo deps to satisfy exhaustive-deps (which misclassifies the
closure-captured value as outer-scope because createLogger is defined
outside a component - lint-staged's --fix strips the dep on commit).
But AnalyticsContext explicitly re-renders (not remounts) on account
change with a new did, producing a fresh createLogger closure with new
metadata; a mounted consumer's memoized child logger kept the previous
account's metadata, misattributing telemetry across session switches.
Suppress the rule with the why and keep the dep.
Found by a full-diff roast pass.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The 'different error code' test asserted a MATCHING code returns true
(with an unrelated method as witness), contradicting its own name and
leaving the code-mismatch path untested. Split into two tests: a real
mismatch (approveJoinRequest declares both names, so no as-any cast
needed) asserting false, and the non-XRPC case.
Found by a roast pass over the error-matching changes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The migration replaced typed error classes (e instanceof
SomeNsid.SomeError) with getErrorName(e) === 'X' string comparisons,
where a typo'd or undeclared name compiles silently. isXrpcErrorOf
(method, e, name) constrains name to InferMethodError<M> - the errors
tuple the generated lexicon schema declares - restoring the old
compile-time guarantee (verified: a typo'd name fails typecheck).
47 comparison sites across 16 files migrated.
Deliberately left on getErrorName, with comments:
- LoginForm AuthFactorTokenRequired: a LexAuthFactorError from
PasswordSession.login, not an XrpcError.
- AppealForm AlreadyAppealed: createReport declares NO errors in its
lexicon; the server sends it anyway (undeclared behavior worth
raising upstream).
- PostFeedErrorMessage Block detection: generic over feed descriptors,
source method genuinely ambiguous.
- isErrorMaybeAppPasswordPermissions: generic InvalidToken + message.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
isErrorMaybeAppPasswordPermissions compared getErrorName against
'TokenInvalid', a code the PDS never sends - the string fallback was
doing all the work. The real wire shape is the GENERIC 'InvalidToken'
code (also used for malformed/expired tokens) with the app-password
specifics only in the message, so the typed path now matches code AND
message ('Bad token scope' / 'Bad token method').
Deactivated and DeactivateAccountDialog used raw switch(e.message)
equality; both now go through the helper. Tests updated to pin the
actual PDS wire shape (and reject plain malformed-token errors).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adversarial review (roast) of the branch, 9 path-scoped passes:
- cross-tab rebuild armed the new session's hooks before the deferred
bundle dispatch, so an 'expired' event fired in that async window
would be dropped by the reducer's bundle-identity check; arm now
happens in the same callback as the dispatch, and the dispatched
account is re-read from the live session (mirrors snapshot-after-
prep).
- Composer onPressPublish's dep array omitted appviewClient and
resolveClients (stale-client hazard after cross-tab rebuild).
- follow-all catch blocks (onboarding starter-pack card, starter pack
screen) showed the error toast but fell through to dereference the
undefined followUris map, throwing and showing a bogus success toast.
- contacts createProfileRecord unconditionally overwrote the profile's
createdAt on every run; now preserved when already set, matching
StepFinished.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- PostFeedErrorMessage read error?.status directly; only
XrpcResponseError carries .status now, so route through
getErrorStatus() like the rest of detectKnownError (also drops an
oxlint no-unsafe-member-access suppression).
- reportSubject.ts docblocks contradicted each other about cid
branding; l.CidString is a plain (unbranded) string alias, so the
file docstring now says so and the strongRef comments agree.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- ExternalPlayer gated WebView navigation with
event.url.includes('www.youtube.com'), which also matches hostile
URLs like https://www.youtube.com.evil.com; parse and compare the
hostname exactly.
- isValidUrlAndDomain used a nested-quantifier regex that backtracks
exponentially on inputs like '//0.' + '00.'.repeat(n), and it runs on
every composer keystroke; rewritten as linear-time parsing with the
same semantics (http/https/ftp/protocol-relative, userinfo, public
IPv4 or domain host, 2-5 digit port, whitespace-free path). New test
file pins accept/reject behavior plus ReDoS canaries.
Both pre-existing (2024) issues surfaced by CodeQL on the migration PR.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Review fixes (PR #11182 round 3):
- The session factories snapshotted the account BEFORE the prep awaits
(moderation config, age-assurance prefetch) with hooks still disarmed.
A 401 during prep triggers PasswordSession's internal auto-refresh,
rotating both tokens; the dropped onUpdated meant the stale refreshJwt
got persisted (dead on next cold start). The returned account is now
re-snapshotted after prep, right before arm() - matching the old
BskyAppAgent.prepare ordering. Tests pin the mid-prep rotation case.
- Cross-tab sync rebuilt and killed the active bundle on ANY saved-
account change; now bails out when the current account's tokens are
unchanged.
- buildChatClient hard-coded the SDK's prod chat DID, making the env-
configurable CHAT_PROXY_DID dead code and routing staging DMs to prod
chat; new CHAT_PROXY_SERVICE constant restores the override.
- The cross-tab labeler reapply was fire-and-forget; the bundle swap now
defers until the labeler config resolves, matching the factories.
Note: the reviewer's URL.canParse/ReadableStream claim was verified
INCORRECT - Expo 54's WinterCG runtime defines URL.canParse and Metro
injects a ReadableStream polyfill (expo/virtual/streams.js) into dev and
release bundles.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Review fixes (PR #11182 round 2). Client identity is no longer stable
across token refresh - on web, a cross-tab sync rebuilds the bundle and
disposes the old clients (whose fetch now throws) - so anything that
captured a client at construction time must re-point at the live one:
- MessagesEventBus and Convo get updateClient(); providers sync it via
effect. No remount, so poll cursors and optimistic pendingMessages
survive.
- FeedAPI implementations get setClient(); the post-feed queryFn and
pollLatest re-point page-held apis before fetching (Merge/Home apis
are stateful across pages, so they cannot be rebuilt per fetch).
- usePreferencesQuery applies fetched labeler dids to the live appview
client (applyLabelersToClient, factored from session/moderation),
restoring the old BskyAgent.getPreferences header side effect.
- ageAssurance redirect overlay/dialog polling is mount-only with
render-synced refs; a client swap mid-poll no longer latches the
unmounted flag and strands the overlay.
- Convo message-failure classification defers to lex's shouldRetry()
instead of treating all status-less errors as recoverable.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Review fixes (PR #11182 round 1):
- PasswordSession fires onUpdated/onDeleted BEFORE committing its
session data; thread the hook payload through to the provider instead
of reading the live getter. Fixes rotated tokens never persisting
(eventual forced logout) and expiry not logging out.
- getErrorName now gates on LexError, so LexAuthFactorError (a sibling
of XrpcError) surfaces AuthFactorTokenRequired and email-2fa users
get the code input.
- disposeBundle was a no-op; add a kill-switch closure around the
session's injected fetch (covers the internal auto-refresh path) so a
replaced session can't consume rotated refresh tokens. kill() also
disarms the hooks so stale bundles can't dispatch into the reducer.
- cross-tab same-did rebuild now reapplies subscribed labelers to the
fresh appview client (was built with an empty per-instance set).
- isAppLabeler reads Client.appLabelers instead of the hard-coded prod
did, restoring test-env and regional-authority classification.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The CI bundle-size-diff action require()s stats.json as one string; the
migration's fine-grained lex/sdk module graph pushed the default stats
output to ~611MB, past V8's 512MB string cap. The action only reads
assets[], so drop the module graph from the generated file.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Audit of usePdsClient call sites: the PDS client is for com.atproto.*
account/repo operations. Appview-private state (actor/list/thread mutes,
notification seen-state) and mention/facet resolution now use the
appview-routed client. useRichText also regains logged-out mention
resolution via useLexClient's public fallback (usePdsClient throws when
logged out, silently leaving mentions unresolved on StarterPackLanding
and web ProfileHoverCard).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
oxlint-suppressions.json gains a baseline entry for webpack.config.js: the
file was never previously staged/linted (lint script covers src+modules
only), and editing it exposed its pre-existing violations to lint-staged.
All suppressed counts are pre-existing; this change only removes a line.
Task 8: codemod repointed 150 namespace-type files to #/lexicons
(AppBskyFeedDefs.PostView -> app.bsky.feed.defs.PostView, .Record -> .Main)
plus ~105 hand-fixed consumers (composer/video state, DebugMod, Profile,
onboarding, dialogs). DM_SERVICE_HEADERS deleted; last chat call sites on
the chat client. RichText.tsx flipped to SDK-only. Remaining @atproto/api
importers: the three session bridge files, the deliberate dual-world
matcher in lib/xrpc-error.ts, and test fixtures. toLex boundary casts are
TODO(phase4)-tagged for bridge removal.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Phase 3 tasks 3-7 (parallel wave): composer/post pipeline on
pdsClient/appviewClient with structural+instance blob guards and a golden
CID fixture test; chat Convo/EventBus/queries on the dedicated chat client;
preferences sugar to SDK actions on the PDS client; remaining state/queries
producers (usePostThread unspecced flip, video scoped-token clients,
notifications, starter packs, lists) to client.call; UI runtime sweep
(AtUri from @atproto/syntax, moderation fns from @bsky.app/sdk/moderation,
SDK RichText, ozone reason tokens, guard rewrites via #/types/bsky).
Intermediate checkpoint (hooks skipped): ~114 typecheck errors remain in
cross-boundary consumer files, resolved by the type-only codemod next.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Phase 3 task 2: types/bsky sibling modules (post/profile/starterPack) now
source from generated #/lexicons with interim dual-world widening aliases
(TODO(phase4) markers). SessionBundle gains a chatClient proxied to
did:web:api.bsky.chat#bsky_chat via useChatClient(). usePdsClient() and
useChatClient() no longer fall back to the public appview when logged out -
they return a client that throws NotAuthenticatedError before any network
I/O; useMaybePdsClient()/useMaybeChatClient() cover logged-out-aware
callers. RichText pilot: useRichText.ts on @bsky.app/sdk/richtext with
detectFacets(pdsClient); RichText.tsx display sink accepts both worlds.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Phase 3 foundations (task 1): src/lib/xrpc-error.ts matches both the old
@atproto/api XRPCError and lex-client XrpcError/XrpcResponseError during
the migration; errors.ts token-invalid matching goes through it. toLex<T>()
added to #/types/bsky as a marked interim cast for mixed-world boundaries.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- 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>