Bluesky app fork with some witchin' additions 💫

[Session] Refactor to prepare for V2 (#3781)

* Move types to another file

Co-authored-by: dan <dan.abramov@gmail.com>

* Move utilities out

Co-authored-by: dan <dan.abramov@gmail.com>

* Move PUBLIC_BSKY_AGENT

Co-authored-by: dan <dan.abramov@gmail.com>

* Move createPersistSessionHandler inline

Co-authored-by: dan <dan.abramov@gmail.com>

* Call configureModeration when clearing account too

This ensures that the app labelers get reset in a test environment.

Co-authored-by: dan <dan.abramov@gmail.com>

* Make guest configureModeration sync, non-guest async

* Extract isSessionExpired

Co-authored-by: dan <dan.abramov@gmail.com>

* Flip isSessionExpired condition

Co-authored-by: dan <dan.abramov@gmail.com>

* Extract agentToSessionAccount

Co-authored-by: dan <dan.abramov@gmail.com>

* Extract createAgent*

Co-authored-by: dan <dan.abramov@gmail.com>

* Simplify isSessionExpired

---------

Co-authored-by: Eric Bailey <git@esb.lol>

authored by danabra.mov danabra.mov

Eric Bailey and committed by
GitHub
39807a86 66ad5543

+399 -332
+1 -1
src/App.native.tsx
··· 18 18 import {Provider as StatsigProvider} from '#/lib/statsig/statsig' 19 19 import {init as initPersistedState} from '#/state/persisted' 20 20 import {Provider as LabelDefsProvider} from '#/state/preferences/label-defs' 21 - import {readLastActiveAccount} from '#/state/session/util/readLastActiveAccount' 21 + import {readLastActiveAccount} from '#/state/session/util' 22 22 import {useIntentHandler} from 'lib/hooks/useIntentHandler' 23 23 import {useNotificationsListener} from 'lib/notifications/notifications' 24 24 import {QueryProvider} from 'lib/react-query'
+1 -1
src/App.web.tsx
··· 8 8 import {Provider as StatsigProvider} from '#/lib/statsig/statsig' 9 9 import {init as initPersistedState} from '#/state/persisted' 10 10 import {Provider as LabelDefsProvider} from '#/state/preferences/label-defs' 11 - import {readLastActiveAccount} from '#/state/session/util/readLastActiveAccount' 11 + import {readLastActiveAccount} from '#/state/session/util' 12 12 import {useIntentHandler} from 'lib/hooks/useIntentHandler' 13 13 import {QueryProvider} from 'lib/react-query' 14 14 import {ThemeProvider} from 'lib/ThemeContext'
-8
src/state/queries/index.ts
··· 1 - import {BskyAgent} from '@atproto/api' 2 - 3 - import {PUBLIC_BSKY_SERVICE} from '#/lib/constants' 4 - 5 - export const PUBLIC_BSKY_AGENT = new BskyAgent({ 6 - service: PUBLIC_BSKY_SERVICE, 7 - }) 8 - 9 1 export const STALE = { 10 2 SECONDS: { 11 3 FIFTEEN: 1e3 * 15,
+155 -316
src/state/session/index.tsx
··· 1 1 import React from 'react' 2 - import { 3 - AtpPersistSessionHandler, 4 - BSKY_LABELER_DID, 5 - BskyAgent, 6 - } from '@atproto/api' 7 - import {jwtDecode} from 'jwt-decode' 2 + import {AtpPersistSessionHandler, BskyAgent} from '@atproto/api' 8 3 9 4 import {track} from '#/lib/analytics/analytics' 10 5 import {networkRetry} from '#/lib/async/retry' 11 - import {IS_TEST_USER} from '#/lib/constants' 12 - import {logEvent, LogEvents, tryFetchGates} from '#/lib/statsig/statsig' 13 - import {hasProp} from '#/lib/type-guards' 6 + import {PUBLIC_BSKY_SERVICE} from '#/lib/constants' 7 + import {logEvent, tryFetchGates} from '#/lib/statsig/statsig' 14 8 import {logger} from '#/logger' 15 9 import {isWeb} from '#/platform/detection' 16 10 import * as persisted from '#/state/persisted' 17 - import {PUBLIC_BSKY_AGENT} from '#/state/queries' 18 11 import {useCloseAllActiveElements} from '#/state/util' 19 12 import {useGlobalDialogsControlContext} from '#/components/dialogs/Context' 20 13 import {IS_DEV} from '#/env' 21 14 import {emitSessionDropped} from '../events' 22 - import {readLabelers} from './agent-config' 15 + import { 16 + agentToSessionAccount, 17 + configureModerationForAccount, 18 + configureModerationForGuest, 19 + createAgentAndCreateAccount, 20 + createAgentAndLogin, 21 + isSessionDeactivated, 22 + isSessionExpired, 23 + } from './util' 23 24 24 - let __globalAgent: BskyAgent = PUBLIC_BSKY_AGENT 25 + export type {SessionAccount} from '#/state/session/types' 26 + import { 27 + SessionAccount, 28 + SessionApiContext, 29 + SessionState, 30 + SessionStateContext, 31 + } from '#/state/session/types' 25 32 26 - function __getAgent() { 27 - return __globalAgent 28 - } 33 + export {isSessionDeactivated} 29 34 30 - export function useAgent() { 31 - return React.useMemo(() => ({getAgent: __getAgent}), []) 32 - } 33 - 34 - export type SessionAccount = persisted.PersistedAccount 35 - 36 - export type SessionState = { 37 - isInitialLoad: boolean 38 - isSwitchingAccounts: boolean 39 - accounts: SessionAccount[] 40 - currentAccount: SessionAccount | undefined 41 - } 42 - export type StateContext = SessionState & { 43 - hasSession: boolean 44 - } 45 - export type ApiContext = { 46 - createAccount: (props: { 47 - service: string 48 - email: string 49 - password: string 50 - handle: string 51 - inviteCode?: string 52 - verificationPhone?: string 53 - verificationCode?: string 54 - }) => Promise<void> 55 - login: ( 56 - props: { 57 - service: string 58 - identifier: string 59 - password: string 60 - authFactorToken?: string | undefined 61 - }, 62 - logContext: LogEvents['account:loggedIn']['logContext'], 63 - ) => Promise<void> 64 - /** 65 - * A full logout. Clears the `currentAccount` from session, AND removes 66 - * access tokens from all accounts, so that returning as any user will 67 - * require a full login. 68 - */ 69 - logout: ( 70 - logContext: LogEvents['account:loggedOut']['logContext'], 71 - ) => Promise<void> 72 - /** 73 - * A partial logout. Clears the `currentAccount` from session, but DOES NOT 74 - * clear access tokens from accounts, allowing the user to return to their 75 - * other accounts without logging in. 76 - * 77 - * Used when adding a new account, deleting an account. 78 - */ 79 - clearCurrentAccount: () => void 80 - initSession: (account: SessionAccount) => Promise<void> 81 - resumeSession: (account?: SessionAccount) => Promise<void> 82 - removeAccount: (account: SessionAccount) => void 83 - selectAccount: ( 84 - account: SessionAccount, 85 - logContext: LogEvents['account:loggedIn']['logContext'], 86 - ) => Promise<void> 87 - updateCurrentAccount: ( 88 - account: Partial< 89 - Pick< 90 - SessionAccount, 91 - 'handle' | 'email' | 'emailConfirmed' | 'emailAuthFactor' 92 - > 93 - >, 94 - ) => void 95 - } 35 + const PUBLIC_BSKY_AGENT = new BskyAgent({service: PUBLIC_BSKY_SERVICE}) 36 + configureModerationForGuest() 96 37 97 - const StateContext = React.createContext<StateContext>({ 38 + const StateContext = React.createContext<SessionStateContext>({ 98 39 isInitialLoad: true, 99 40 isSwitchingAccounts: false, 100 41 accounts: [], ··· 102 43 hasSession: false, 103 44 }) 104 45 105 - const ApiContext = React.createContext<ApiContext>({ 46 + const ApiContext = React.createContext<SessionApiContext>({ 106 47 createAccount: async () => {}, 107 48 login: async () => {}, 108 49 logout: async () => {}, ··· 114 55 clearCurrentAccount: () => {}, 115 56 }) 116 57 117 - function createPersistSessionHandler( 118 - agent: BskyAgent, 119 - account: SessionAccount, 120 - persistSessionCallback: (props: { 121 - expired: boolean 122 - refreshedAccount: SessionAccount 123 - }) => void, 124 - { 125 - networkErrorCallback, 126 - }: { 127 - networkErrorCallback?: () => void 128 - } = {}, 129 - ): AtpPersistSessionHandler { 130 - return function persistSession(event, session) { 131 - const expired = event === 'expired' || event === 'create-failed' 132 - 133 - if (event === 'network-error') { 134 - logger.warn(`session: persistSessionHandler received network-error event`) 135 - networkErrorCallback?.() 136 - return 137 - } 138 - 139 - const refreshedAccount: SessionAccount = { 140 - service: account.service, 141 - did: session?.did || account.did, 142 - handle: session?.handle || account.handle, 143 - email: session?.email || account.email, 144 - emailConfirmed: session?.emailConfirmed || account.emailConfirmed, 145 - emailAuthFactor: session?.emailAuthFactor || account.emailAuthFactor, 146 - deactivated: isSessionDeactivated(session?.accessJwt), 147 - pdsUrl: agent.pdsUrl?.toString(), 58 + let __globalAgent: BskyAgent = PUBLIC_BSKY_AGENT 148 59 149 - /* 150 - * Tokens are undefined if the session expires, or if creation fails for 151 - * any reason e.g. tokens are invalid, network error, etc. 152 - */ 153 - refreshJwt: session?.refreshJwt, 154 - accessJwt: session?.accessJwt, 155 - } 156 - 157 - logger.debug(`session: persistSession`, { 158 - event, 159 - deactivated: refreshedAccount.deactivated, 160 - }) 161 - 162 - if (expired) { 163 - logger.warn(`session: expired`) 164 - emitSessionDropped() 165 - } 166 - 167 - /* 168 - * If the session expired, or it was successfully created/updated, we want 169 - * to update/persist the data. 170 - * 171 - * If the session creation failed, it could be a network error, or it could 172 - * be more serious like an invalid token(s). We can't differentiate, so in 173 - * order to allow the user to get a fresh token (if they need it), we need 174 - * to persist this data and wipe their tokens, effectively logging them 175 - * out. 176 - */ 177 - persistSessionCallback({ 178 - expired, 179 - refreshedAccount, 180 - }) 181 - } 60 + function __getAgent() { 61 + return __globalAgent 182 62 } 183 63 184 64 export function Provider({children}: React.PropsWithChildren<{}>) { ··· 214 94 const clearCurrentAccount = React.useCallback(() => { 215 95 logger.warn(`session: clear current account`) 216 96 __globalAgent = PUBLIC_BSKY_AGENT 97 + configureModerationForGuest() 217 98 setStateAndPersist(s => ({ 218 99 ...s, 219 100 currentAccount: undefined, 220 101 })) 221 102 }, [setStateAndPersist]) 222 103 223 - const createAccount = React.useCallback<ApiContext['createAccount']>( 104 + const createPersistSessionHandler = React.useCallback( 105 + ( 106 + agent: BskyAgent, 107 + account: SessionAccount, 108 + persistSessionCallback: (props: { 109 + expired: boolean 110 + refreshedAccount: SessionAccount 111 + }) => void, 112 + { 113 + networkErrorCallback, 114 + }: { 115 + networkErrorCallback?: () => void 116 + } = {}, 117 + ): AtpPersistSessionHandler => { 118 + return function persistSession(event, session) { 119 + const expired = event === 'expired' || event === 'create-failed' 120 + 121 + if (event === 'network-error') { 122 + logger.warn( 123 + `session: persistSessionHandler received network-error event`, 124 + ) 125 + networkErrorCallback?.() 126 + return 127 + } 128 + 129 + // TODO: use agentToSessionAccount for this too. 130 + const refreshedAccount: SessionAccount = { 131 + service: account.service, 132 + did: session?.did || account.did, 133 + handle: session?.handle || account.handle, 134 + email: session?.email || account.email, 135 + emailConfirmed: session?.emailConfirmed || account.emailConfirmed, 136 + emailAuthFactor: session?.emailAuthFactor || account.emailAuthFactor, 137 + deactivated: isSessionDeactivated(session?.accessJwt), 138 + pdsUrl: agent.pdsUrl?.toString(), 139 + 140 + /* 141 + * Tokens are undefined if the session expires, or if creation fails for 142 + * any reason e.g. tokens are invalid, network error, etc. 143 + */ 144 + refreshJwt: session?.refreshJwt, 145 + accessJwt: session?.accessJwt, 146 + } 147 + 148 + logger.debug(`session: persistSession`, { 149 + event, 150 + deactivated: refreshedAccount.deactivated, 151 + }) 152 + 153 + if (expired) { 154 + logger.warn(`session: expired`) 155 + emitSessionDropped() 156 + } 157 + 158 + /* 159 + * If the session expired, or it was successfully created/updated, we want 160 + * to update/persist the data. 161 + * 162 + * If the session creation failed, it could be a network error, or it could 163 + * be more serious like an invalid token(s). We can't differentiate, so in 164 + * order to allow the user to get a fresh token (if they need it), we need 165 + * to persist this data and wipe their tokens, effectively logging them 166 + * out. 167 + */ 168 + persistSessionCallback({ 169 + expired, 170 + refreshedAccount, 171 + }) 172 + } 173 + }, 174 + [], 175 + ) 176 + 177 + const createAccount = React.useCallback<SessionApiContext['createAccount']>( 224 178 async ({ 225 179 service, 226 180 email, ··· 229 183 inviteCode, 230 184 verificationPhone, 231 185 verificationCode, 232 - }: any) => { 186 + }) => { 233 187 logger.info(`session: creating account`) 234 188 track('Try Create Account') 235 189 logEvent('account:create:begin', {}) 236 - 237 - const agent = new BskyAgent({service}) 238 - 239 - await agent.createAccount({ 240 - handle, 241 - password, 242 - email, 243 - inviteCode, 244 - verificationPhone, 245 - verificationCode, 246 - }) 247 - 248 - if (!agent.session) { 249 - throw new Error(`session: createAccount failed to establish a session`) 250 - } 251 - const fetchingGates = tryFetchGates( 252 - agent.session.did, 253 - 'prefer-fresh-gates', 190 + const {agent, account, fetchingGates} = await createAgentAndCreateAccount( 191 + { 192 + service, 193 + email, 194 + password, 195 + handle, 196 + inviteCode, 197 + verificationPhone, 198 + verificationCode, 199 + }, 254 200 ) 255 201 256 - const deactivated = isSessionDeactivated(agent.session.accessJwt) 257 - if (!deactivated) { 258 - /*dont await*/ agent.upsertProfile(_existing => { 259 - return { 260 - displayName: '', 261 - 262 - // HACKFIX 263 - // creating a bunch of identical profile objects is breaking the relay 264 - // tossing this unspecced field onto it to reduce the size of the problem 265 - // -prf 266 - createdAt: new Date().toISOString(), 267 - } 268 - }) 269 - } 270 - 271 - const account: SessionAccount = { 272 - service: agent.service.toString(), 273 - did: agent.session.did, 274 - handle: agent.session.handle, 275 - email: agent.session.email, 276 - emailConfirmed: agent.session.emailConfirmed, 277 - emailAuthFactor: agent.session.emailAuthFactor, 278 - refreshJwt: agent.session.refreshJwt, 279 - accessJwt: agent.session.accessJwt, 280 - deactivated, 281 - pdsUrl: agent.pdsUrl?.toString(), 282 - } 283 - 284 - await configureModeration(agent, account) 285 - 286 202 agent.setPersistSessionHandler( 287 203 createPersistSessionHandler( 288 204 agent, ··· 302 218 track('Create Account') 303 219 logEvent('account:create:success', {}) 304 220 }, 305 - [upsertAccount, clearCurrentAccount], 221 + [upsertAccount, clearCurrentAccount, createPersistSessionHandler], 306 222 ) 307 223 308 - const login = React.useCallback<ApiContext['login']>( 224 + const login = React.useCallback<SessionApiContext['login']>( 309 225 async ({service, identifier, password, authFactorToken}, logContext) => { 310 226 logger.debug(`session: login`, {}, logger.DebugContext.session) 311 - 312 - const agent = new BskyAgent({service}) 313 - 314 - await agent.login({identifier, password, authFactorToken}) 315 - 316 - if (!agent.session) { 317 - throw new Error(`session: login failed to establish a session`) 318 - } 319 - const fetchingGates = tryFetchGates( 320 - agent.session.did, 321 - 'prefer-fresh-gates', 322 - ) 323 - 324 - const account: SessionAccount = { 325 - service: agent.service.toString(), 326 - did: agent.session.did, 327 - handle: agent.session.handle, 328 - email: agent.session.email, 329 - emailConfirmed: agent.session.emailConfirmed, 330 - emailAuthFactor: agent.session.emailAuthFactor, 331 - refreshJwt: agent.session.refreshJwt, 332 - accessJwt: agent.session.accessJwt, 333 - deactivated: isSessionDeactivated(agent.session.accessJwt), 334 - pdsUrl: agent.pdsUrl?.toString(), 335 - } 336 - 337 - await configureModeration(agent, account) 227 + const {agent, account, fetchingGates} = await createAgentAndLogin({ 228 + service, 229 + identifier, 230 + password, 231 + authFactorToken, 232 + }) 338 233 339 234 agent.setPersistSessionHandler( 340 235 createPersistSessionHandler( ··· 358 253 track('Sign In', {resumedSession: false}) 359 254 logEvent('account:loggedIn', {logContext, withPassword: true}) 360 255 }, 361 - [upsertAccount, clearCurrentAccount], 256 + [upsertAccount, clearCurrentAccount, createPersistSessionHandler], 362 257 ) 363 258 364 - const logout = React.useCallback<ApiContext['logout']>( 259 + const logout = React.useCallback<SessionApiContext['logout']>( 365 260 async logContext => { 366 261 logger.debug(`session: logout`) 367 262 clearCurrentAccount() ··· 380 275 [clearCurrentAccount, setStateAndPersist], 381 276 ) 382 277 383 - const initSession = React.useCallback<ApiContext['initSession']>( 278 + const initSession = React.useCallback<SessionApiContext['initSession']>( 384 279 async account => { 385 280 logger.debug(`session: initSession`, {}, logger.DebugContext.session) 386 281 const fetchingGates = tryFetchGates(account.did, 'prefer-low-latency') ··· 405 300 406 301 // @ts-ignore 407 302 if (IS_DEV && isWeb) window.agent = agent 408 - await configureModeration(agent, account) 409 - 410 - let canReusePrevSession = false 411 - try { 412 - if (account.accessJwt) { 413 - const decoded = jwtDecode(account.accessJwt) 414 - if (decoded.exp) { 415 - const didExpire = Date.now() >= decoded.exp * 1000 416 - if (!didExpire) { 417 - canReusePrevSession = true 418 - } 419 - } 420 - } 421 - } catch (e) { 422 - logger.error(`session: could not decode jwt`) 423 - } 303 + await configureModerationForAccount(agent, account) 424 304 425 305 const accountOrSessionDeactivated = 426 306 isSessionDeactivated(account.accessJwt) || account.deactivated ··· 432 312 handle: account.handle, 433 313 } 434 314 435 - if (canReusePrevSession) { 315 + if (isSessionExpired(account)) { 316 + logger.debug(`session: attempting to resume using previous session`) 317 + 318 + try { 319 + const freshAccount = await resumeSessionWithFreshAccount() 320 + __globalAgent = agent 321 + await fetchingGates 322 + upsertAccount(freshAccount) 323 + } catch (e) { 324 + /* 325 + * Note: `agent.persistSession` is also called when this fails, and 326 + * we handle that failure via `createPersistSessionHandler` 327 + */ 328 + logger.info(`session: resumeSessionWithFreshAccount failed`, { 329 + message: e, 330 + }) 331 + 332 + __globalAgent = PUBLIC_BSKY_AGENT 333 + } 334 + } else { 436 335 logger.debug(`session: attempting to reuse previous session`) 437 336 438 337 agent.session = prevSession ··· 469 368 470 369 __globalAgent = PUBLIC_BSKY_AGENT 471 370 }) 472 - } else { 473 - logger.debug(`session: attempting to resume using previous session`) 474 - 475 - try { 476 - const freshAccount = await resumeSessionWithFreshAccount() 477 - __globalAgent = agent 478 - await fetchingGates 479 - upsertAccount(freshAccount) 480 - } catch (e) { 481 - /* 482 - * Note: `agent.persistSession` is also called when this fails, and 483 - * we handle that failure via `createPersistSessionHandler` 484 - */ 485 - logger.info(`session: resumeSessionWithFreshAccount failed`, { 486 - message: e, 487 - }) 488 - 489 - __globalAgent = PUBLIC_BSKY_AGENT 490 - } 491 371 } 492 372 493 373 async function resumeSessionWithFreshAccount(): Promise<SessionAccount> { 494 374 logger.debug(`session: resumeSessionWithFreshAccount`) 495 375 496 376 await networkRetry(1, () => agent.resumeSession(prevSession)) 497 - 377 + const sessionAccount = agentToSessionAccount(agent) 498 378 /* 499 379 * If `agent.resumeSession` fails above, it'll throw. This is just to 500 380 * make TypeScript happy. 501 381 */ 502 - if (!agent.session) { 382 + if (!sessionAccount) { 503 383 throw new Error(`session: initSession failed to establish a session`) 504 384 } 505 - 506 - // ensure changes in handle/email etc are captured on reload 507 - return { 508 - service: agent.service.toString(), 509 - did: agent.session.did, 510 - handle: agent.session.handle, 511 - email: agent.session.email, 512 - emailConfirmed: agent.session.emailConfirmed, 513 - emailAuthFactor: agent.session.emailAuthFactor, 514 - refreshJwt: agent.session.refreshJwt, 515 - accessJwt: agent.session.accessJwt, 516 - deactivated: isSessionDeactivated(agent.session.accessJwt), 517 - pdsUrl: agent.pdsUrl?.toString(), 518 - } 385 + return sessionAccount 519 386 } 520 387 }, 521 - [upsertAccount, clearCurrentAccount], 388 + [upsertAccount, clearCurrentAccount, createPersistSessionHandler], 522 389 ) 523 390 524 - const resumeSession = React.useCallback<ApiContext['resumeSession']>( 391 + const resumeSession = React.useCallback<SessionApiContext['resumeSession']>( 525 392 async account => { 526 393 try { 527 394 if (account) { ··· 539 406 [initSession], 540 407 ) 541 408 542 - const removeAccount = React.useCallback<ApiContext['removeAccount']>( 409 + const removeAccount = React.useCallback<SessionApiContext['removeAccount']>( 543 410 account => { 544 411 setStateAndPersist(s => { 545 412 return { ··· 552 419 ) 553 420 554 421 const updateCurrentAccount = React.useCallback< 555 - ApiContext['updateCurrentAccount'] 422 + SessionApiContext['updateCurrentAccount'] 556 423 >( 557 424 account => { 558 425 setStateAndPersist(s => { ··· 588 455 [setStateAndPersist], 589 456 ) 590 457 591 - const selectAccount = React.useCallback<ApiContext['selectAccount']>( 458 + const selectAccount = React.useCallback<SessionApiContext['selectAccount']>( 592 459 async (account, logContext) => { 593 460 setState(s => ({...s, isSwitchingAccounts: true})) 594 461 try { ··· 714 581 ) 715 582 } 716 583 717 - async function configureModeration(agent: BskyAgent, account: SessionAccount) { 718 - if (IS_TEST_USER(account.handle)) { 719 - const did = ( 720 - await agent 721 - .resolveHandle({handle: 'mod-authority.test'}) 722 - .catch(_ => undefined) 723 - )?.data.did 724 - if (did) { 725 - console.warn('USING TEST ENV MODERATION') 726 - BskyAgent.configure({appLabelers: [did]}) 727 - } 728 - } else { 729 - BskyAgent.configure({appLabelers: [BSKY_LABELER_DID]}) 730 - const labelerDids = await readLabelers(account.did).catch(_ => {}) 731 - if (labelerDids) { 732 - agent.configureLabelersHeader( 733 - labelerDids.filter(did => did !== BSKY_LABELER_DID), 734 - ) 735 - } 736 - } 737 - } 738 - 739 584 export function useSession() { 740 585 return React.useContext(StateContext) 741 586 } ··· 762 607 ) 763 608 } 764 609 765 - export function isSessionDeactivated(accessJwt: string | undefined) { 766 - if (accessJwt) { 767 - const sessData = jwtDecode(accessJwt) 768 - return ( 769 - hasProp(sessData, 'scope') && sessData.scope === 'com.atproto.deactivated' 770 - ) 771 - } 772 - return false 610 + export function useAgent() { 611 + return React.useMemo(() => ({getAgent: __getAgent}), []) 773 612 }
+65
src/state/session/types.ts
··· 1 + import {LogEvents} from '#/lib/statsig/statsig' 2 + import {PersistedAccount} from '#/state/persisted' 3 + 4 + export type SessionAccount = PersistedAccount 5 + 6 + export type SessionState = { 7 + isInitialLoad: boolean 8 + isSwitchingAccounts: boolean 9 + accounts: SessionAccount[] 10 + currentAccount: SessionAccount | undefined 11 + } 12 + export type SessionStateContext = SessionState & { 13 + hasSession: boolean 14 + } 15 + export type SessionApiContext = { 16 + createAccount: (props: { 17 + service: string 18 + email: string 19 + password: string 20 + handle: string 21 + inviteCode?: string 22 + verificationPhone?: string 23 + verificationCode?: string 24 + }) => Promise<void> 25 + login: ( 26 + props: { 27 + service: string 28 + identifier: string 29 + password: string 30 + authFactorToken?: string | undefined 31 + }, 32 + logContext: LogEvents['account:loggedIn']['logContext'], 33 + ) => Promise<void> 34 + /** 35 + * A full logout. Clears the `currentAccount` from session, AND removes 36 + * access tokens from all accounts, so that returning as any user will 37 + * require a full login. 38 + */ 39 + logout: ( 40 + logContext: LogEvents['account:loggedOut']['logContext'], 41 + ) => Promise<void> 42 + /** 43 + * A partial logout. Clears the `currentAccount` from session, but DOES NOT 44 + * clear access tokens from accounts, allowing the user to return to their 45 + * other accounts without logging in. 46 + * 47 + * Used when adding a new account, deleting an account. 48 + */ 49 + clearCurrentAccount: () => void 50 + initSession: (account: SessionAccount) => Promise<void> 51 + resumeSession: (account?: SessionAccount) => Promise<void> 52 + removeAccount: (account: SessionAccount) => void 53 + selectAccount: ( 54 + account: SessionAccount, 55 + logContext: LogEvents['account:loggedIn']['logContext'], 56 + ) => Promise<void> 57 + updateCurrentAccount: ( 58 + account: Partial< 59 + Pick< 60 + SessionAccount, 61 + 'handle' | 'email' | 'emailConfirmed' | 'emailAuthFactor' 62 + > 63 + >, 64 + ) => void 65 + }
+177
src/state/session/util/index.ts
··· 1 + import {BSKY_LABELER_DID, BskyAgent} from '@atproto/api' 2 + import {jwtDecode} from 'jwt-decode' 3 + 4 + import {IS_TEST_USER} from '#/lib/constants' 5 + import {tryFetchGates} from '#/lib/statsig/statsig' 6 + import {hasProp} from '#/lib/type-guards' 7 + import {logger} from '#/logger' 8 + import * as persisted from '#/state/persisted' 9 + import {readLabelers} from '../agent-config' 10 + import {SessionAccount, SessionApiContext} from '../types' 11 + 12 + export function isSessionDeactivated(accessJwt: string | undefined) { 13 + if (accessJwt) { 14 + const sessData = jwtDecode(accessJwt) 15 + return ( 16 + hasProp(sessData, 'scope') && sessData.scope === 'com.atproto.deactivated' 17 + ) 18 + } 19 + return false 20 + } 21 + 22 + export function readLastActiveAccount() { 23 + const {currentAccount, accounts} = persisted.get('session') 24 + return accounts.find(a => a.did === currentAccount?.did) 25 + } 26 + 27 + export function agentToSessionAccount( 28 + agent: BskyAgent, 29 + ): SessionAccount | undefined { 30 + if (!agent.session) return undefined 31 + 32 + return { 33 + service: agent.service.toString(), 34 + did: agent.session.did, 35 + handle: agent.session.handle, 36 + email: agent.session.email, 37 + emailConfirmed: agent.session.emailConfirmed || false, 38 + emailAuthFactor: agent.session.emailAuthFactor || false, 39 + refreshJwt: agent.session.refreshJwt, 40 + accessJwt: agent.session.accessJwt, 41 + deactivated: isSessionDeactivated(agent.session.accessJwt), 42 + pdsUrl: agent.pdsUrl?.toString(), 43 + } 44 + } 45 + 46 + export function configureModerationForGuest() { 47 + switchToBskyAppLabeler() 48 + } 49 + 50 + export async function configureModerationForAccount( 51 + agent: BskyAgent, 52 + account: SessionAccount, 53 + ) { 54 + switchToBskyAppLabeler() 55 + if (IS_TEST_USER(account.handle)) { 56 + await trySwitchToTestAppLabeler(agent) 57 + } 58 + 59 + const labelerDids = await readLabelers(account.did).catch(_ => {}) 60 + if (labelerDids) { 61 + agent.configureLabelersHeader( 62 + labelerDids.filter(did => did !== BSKY_LABELER_DID), 63 + ) 64 + } else { 65 + // If there are no headers in the storage, we'll not send them on the initial requests. 66 + // If we wanted to fix this, we could block on the preferences query here. 67 + } 68 + } 69 + 70 + function switchToBskyAppLabeler() { 71 + BskyAgent.configure({appLabelers: [BSKY_LABELER_DID]}) 72 + } 73 + 74 + async function trySwitchToTestAppLabeler(agent: BskyAgent) { 75 + const did = ( 76 + await agent 77 + .resolveHandle({handle: 'mod-authority.test'}) 78 + .catch(_ => undefined) 79 + )?.data.did 80 + if (did) { 81 + console.warn('USING TEST ENV MODERATION') 82 + BskyAgent.configure({appLabelers: [did]}) 83 + } 84 + } 85 + 86 + export function isSessionExpired(account: SessionAccount) { 87 + try { 88 + if (account.accessJwt) { 89 + const decoded = jwtDecode(account.accessJwt) 90 + if (decoded.exp) { 91 + const didExpire = Date.now() >= decoded.exp * 1000 92 + return didExpire 93 + } 94 + } 95 + } catch (e) { 96 + logger.error(`session: could not decode jwt`) 97 + } 98 + return true 99 + } 100 + 101 + export async function createAgentAndLogin({ 102 + service, 103 + identifier, 104 + password, 105 + authFactorToken, 106 + }: { 107 + service: string 108 + identifier: string 109 + password: string 110 + authFactorToken?: string 111 + }) { 112 + const agent = new BskyAgent({service}) 113 + await agent.login({identifier, password, authFactorToken}) 114 + 115 + const account = agentToSessionAccount(agent) 116 + if (!agent.session || !account) { 117 + throw new Error(`session: login failed to establish a session`) 118 + } 119 + 120 + const fetchingGates = tryFetchGates(account.did, 'prefer-fresh-gates') 121 + await configureModerationForAccount(agent, account) 122 + 123 + return { 124 + agent, 125 + account, 126 + fetchingGates, 127 + } 128 + } 129 + 130 + export async function createAgentAndCreateAccount({ 131 + service, 132 + email, 133 + password, 134 + handle, 135 + inviteCode, 136 + verificationPhone, 137 + verificationCode, 138 + }: Parameters<SessionApiContext['createAccount']>[0]) { 139 + const agent = new BskyAgent({service}) 140 + await agent.createAccount({ 141 + email, 142 + password, 143 + handle, 144 + inviteCode, 145 + verificationPhone, 146 + verificationCode, 147 + }) 148 + 149 + const account = agentToSessionAccount(agent)! 150 + if (!agent.session || !account) { 151 + throw new Error(`session: createAccount failed to establish a session`) 152 + } 153 + 154 + const fetchingGates = tryFetchGates(account.did, 'prefer-fresh-gates') 155 + 156 + if (!account.deactivated) { 157 + /*dont await*/ agent.upsertProfile(_existing => { 158 + return { 159 + displayName: '', 160 + 161 + // HACKFIX 162 + // creating a bunch of identical profile objects is breaking the relay 163 + // tossing this unspecced field onto it to reduce the size of the problem 164 + // -prf 165 + createdAt: new Date().toISOString(), 166 + } 167 + }) 168 + } 169 + 170 + await configureModerationForAccount(agent, account) 171 + 172 + return { 173 + agent, 174 + account, 175 + fetchingGates, 176 + } 177 + }
-6
src/state/session/util/readLastActiveAccount.ts
··· 1 - import * as persisted from '#/state/persisted' 2 - 3 - export function readLastActiveAccount() { 4 - const {currentAccount, accounts} = persisted.get('session') 5 - return accounts.find(a => a.did === currentAccount?.did) 6 - }