From 131d4815e6546dad0c8cb5b7fbb3e71cf20838d4 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Mon, 3 Aug 2026 16:45:47 +0300 Subject: [PATCH] keep the stored pds url when a refresh delivers no did doc Co-Authored-By: Claude Fable 5 --- .../provider-session-events-test.tsx | 83 +++++++++++++++++++ src/state/session/index.tsx | 11 ++- 2 files changed, 93 insertions(+), 1 deletion(-) diff --git a/src/state/session/__tests__/provider-session-events-test.tsx b/src/state/session/__tests__/provider-session-events-test.tsx index 778f83d508..e6a51a5414 100644 --- a/src/state/session/__tests__/provider-session-events-test.tsx +++ b/src/state/session/__tests__/provider-session-events-test.tsx @@ -242,6 +242,36 @@ function dyingData(refreshJwt: string): SessionData { } } +/** The rotated payload PasswordSession threads through its `onUpdated` hook. */ +function refreshedData( + refreshJwt: string, + didDoc?: SessionData['didDoc'], +): SessionData { + return { + accessJwt: 'fresh-access-jwt', + refreshJwt, + handle: 'alice.test', + did: DID, + active: true, + service: SERVICE, + ...(didDoc ? {didDoc} : {}), + } +} + +/** A minimal valid DID document whose only service entry is a PDS. */ +function makeDidDoc(pdsUrl: string): SessionData['didDoc'] { + return { + id: DID, + service: [ + { + id: '#atproto_pds', + type: 'AtprotoPersonalDataServer', + serviceEndpoint: pdsUrl, + }, + ], + } +} + beforeEach(() => { mockPersisted.session = {accounts: [], currentAccount: undefined} mockPersisted.latest = {accounts: [], currentAccount: undefined} @@ -366,6 +396,59 @@ describe('expiry rescue', () => { }) }) +/* + * A refresh payload only carries a didDoc when the server sends one, but + * `pdsUrl` is never derived from the login service. If the provider does not + * thread the stored value through, an ordinary refresh persists + * `pdsUrl: undefined` and the next cold start routes pre-refresh requests to + * the entryway instead of the account's PDS. + */ +describe('refresh persistence', () => { + const PDS_HOST = 'https://shimeji.us-east.host.bsky.network' + const DIDDOC_PDS_HOST = 'https://morel.us-west.host.bsky.network' + + it('keeps the stored pdsUrl when the refresh carries no didDoc', async () => { + const account = makeAccount({pdsUrl: `${PDS_HOST}/`}) + const bundle = makeBundle(account) + const {onSessionChange, currentAccount} = await renderLoggedIn( + account, + bundle, + ) + + act(() => { + onSessionChange( + bundle as unknown as SessionBundle, + DID, + 'update', + refreshedData('refresh-jwt-2'), + ) + }) + + expect(currentAccount()?.refreshJwt).toBe('refresh-jwt-2') + expect(currentAccount()?.pdsUrl).toBe(`${PDS_HOST}/`) + }) + + it('prefers the didDoc endpoint over the stored pdsUrl', async () => { + const account = makeAccount({pdsUrl: `${PDS_HOST}/`}) + const bundle = makeBundle(account) + const {onSessionChange, currentAccount} = await renderLoggedIn( + account, + bundle, + ) + + act(() => { + onSessionChange( + bundle as unknown as SessionBundle, + DID, + 'update', + refreshedData('refresh-jwt-2', makeDidDoc(DIDDOC_PDS_HOST)), + ) + }) + + expect(currentAccount()?.pdsUrl).toBe(`${DIDDOC_PDS_HOST}/`) + }) +}) + /** Deliver a cross-tab `persisted` update to the provider's newest listener. */ function emitSynced(session: Schema['session']) { mockPersistedListeners[mockPersistedListeners.length - 1](session) diff --git a/src/state/session/index.tsx b/src/state/session/index.tsx index d58d6a1827..1a22a0e215 100644 --- a/src/state/session/index.tsx +++ b/src/state/session/index.tsx @@ -161,10 +161,19 @@ export function Provider({children}: React.PropsWithChildren<{}>) { /* * PasswordSession invokes its hooks before updating its live getter. Use * the delivered payload so a refresh persists the newly rotated tokens. + * + * A refresh payload carries no didDoc unless the server sends one, so the + * stored account's `pdsUrl` is threaded in as the fallback. Without it the + * refresh would persist `pdsUrl: undefined` and the next cold start would + * route pre-refresh requests to the entryway instead of the PDS. */ const refreshedAccount = sessionEvent === 'update' && sessionData - ? sessionDataToSessionAccount(sessionData, sessionData.service) + ? sessionDataToSessionAccount( + sessionData, + sessionData.service, + store.getState().accounts.find(a => a.did === accountDid)?.pdsUrl, + ) : undefined /*