import {type AtpSessionData} from '@atproto/api' import {getPdsEndpoint, isValidDidDoc} from '@atproto/common-web' import {type SessionData} from '@atproto/lex-password-session' import {jwtDecode} from 'jwt-decode' import {BSKY_SERVICE} from '#/lib/constants' import {isJwtExpired} from '#/lib/jwt' import {hasProp} from '#/lib/type-guards' import {type SessionAccount} from './types' /** Whether an access token was issued for a queued (waitlisted) signup. */ export function isSignupQueued(accessJwt: string | undefined) { if (accessJwt) { const sessData = jwtDecode(accessJwt) return ( hasProp(sessData, 'scope') && sessData.scope === 'com.atproto.signupQueued' ) } return false } /** * Convert live `PasswordSession` session data into the persisted * `SessionAccount` snapshot. * * The object literal's field order is load-bearing: the reducer's * `JSON.stringify` fast path and the session test snapshots depend on * byte-stable serialization. `service` and `pdsUrl` are normalized through * `new URL().toString()` for a stable trailing slash. * * `pdsUrl` comes from the DID document or a pre-refresh stored value. It does * not fall back to the login service. */ export function sessionDataToSessionAccount( session: SessionData | null | undefined, service: string, storedPdsUrl?: string, ): SessionAccount | undefined { if (!session) { return undefined } const normalizedService = new URL(service).toString() const didDocPdsUrl = session.didDoc && isValidDidDoc(session.didDoc) ? getPdsEndpoint(session.didDoc) : undefined const pdsUrl = didDocPdsUrl ?? storedPdsUrl return { service: normalizedService, did: session.did, handle: session.handle, email: session.email, emailConfirmed: session.emailConfirmed || false, emailAuthFactor: session.emailAuthFactor || false, refreshJwt: session.refreshJwt, accessJwt: session.accessJwt, signupQueued: isSignupQueued(session.accessJwt), active: session.active, status: session.status, pdsUrl: pdsUrl ? new URL(pdsUrl).toString() : undefined, isSelfHosted: !normalizedService.startsWith(BSKY_SERVICE), } } /** Convert a persisted account into data suitable for `PasswordSession`. */ export function sessionAccountToSessionData( account: SessionAccount, ): SessionData { return { accessJwt: account.accessJwt ?? '', active: account.active ?? true, did: account.did as SessionData['did'], email: account.email, emailAuthFactor: account.emailAuthFactor, emailConfirmed: account.emailConfirmed, handle: account.handle as SessionData['handle'], refreshJwt: account.refreshJwt ?? '', status: account.status, service: account.service, } } /** Convert a persisted account into data suitable for `AtpAgent`. */ export function sessionAccountToSession( account: SessionAccount, ): AtpSessionData { return { // Sorted in the same property order as when returned by BskyAgent (alphabetical). accessJwt: account.accessJwt ?? '', did: account.did, email: account.email, emailAuthFactor: account.emailAuthFactor, emailConfirmed: account.emailConfirmed, handle: account.handle, refreshJwt: account.refreshJwt ?? '', /** * @see https://github.com/bluesky-social/atproto/blob/c5d36d5ba2a2c2a5c4f366a5621c06a5608e361e/packages/api/src/agent.ts#L188 */ active: account.active ?? true, status: account.status, } } export function isSessionExpired(account: SessionAccount) { return account.accessJwt ? isJwtExpired(account.accessJwt) : true }