Add session v2

This commit is contained in:
Eric Bailey
2024-04-25 14:30:00 -05:00
parent 5137b70610
commit 0688c65df8
4 changed files with 619 additions and 562 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
import * as V1 from '#/state/session/v1'
import * as V2 from '#/state/session/v2'
export type {SessionAccount} from '#/state/session/v1'
export type {CurrentAccount, SessionAccount} from '#/state/session/types'
const isV2 = false
+88
View File
@@ -0,0 +1,88 @@
import {BskyAgent} from '@atproto/api'
import {LogEvents} from '#/lib/statsig/statsig'
import {PersistedAccount} from '#/state/persisted'
/**
* Alias for `PersistedAccount` from persisted storage.
*/
export type SessionAccount = PersistedAccount
/**
* Subset of `SessionAccount` that excludes tokens.
*/
export type CurrentAccount = Omit<SessionAccount, 'accessJwt' | 'refreshJwt'>
/**
* Context shape returned from `useSession()`
*/
export type SessionStateContext = {
currentAgent: BskyAgent
isInitialLoad: boolean
isSwitchingAccounts: boolean
hasSession: boolean
accounts: SessionAccount[]
/**
* Contains the full account object persisted to storage, minus access
* tokens.
*/
currentAccount: CurrentAccount | undefined
}
/**
* Context shape returned from `useSessionApi()`
*/
export type SessionApiContext = {
createAccount: (props: {
service: string
email: string
password: string
handle: string
inviteCode?: string
verificationPhone?: string
verificationCode?: string
}) => Promise<void>
login: (
props: {
service: string
identifier: string
password: string
},
logContext: LogEvents['account:loggedIn']['logContext'],
) => Promise<void>
/**
* A full logout. Clears the `currentAccount` from session, AND removes
* access tokens from all accounts, so that returning as any user will
* require a full login.
*/
logout: (
logContext: LogEvents['account:loggedOut']['logContext'],
) => Promise<void>
/**
* A partial logout. Clears the `currentAccount` from session, but DOES NOT
* clear access tokens from accounts, allowing the user to return to their
* other accounts without logging in.
*
* Used when adding a new account, deleting an account.
*/
clearCurrentAccount: () => void
initSession: (account: SessionAccount) => Promise<void>
resumeSession: (account?: SessionAccount) => Promise<void>
removeAccount: (account: SessionAccount) => void
selectAccount: (
account: SessionAccount,
logContext: LogEvents['account:loggedIn']['logContext'],
) => Promise<void>
/**
* Refreshes the BskyAgent's session and derive a fresh `currentAccount`
*/
refreshSession: () => void
/**
* @deprecated Use `refreshSession` instead.
*/
updateCurrentAccount: (
account: Partial<
Pick<SessionAccount, 'handle' | 'email' | 'emailConfirmed'>
>,
) => void
}
+179
View File
@@ -0,0 +1,179 @@
import {BSKY_LABELER_DID, BskyAgent} from '@atproto/api'
import {jwtDecode} from 'jwt-decode'
import {IS_TEST_USER} from '#/lib/constants'
import {hasProp} from '#/lib/type-guards'
import {logger} from '#/logger'
import * as persisted from '#/state/persisted'
import {readLabelers} from '#/state/session/agent-config'
import {SessionAccount, SessionApiContext} from '#/state/session/types'
export function isSessionDeactivated(accessJwt: string | undefined) {
if (accessJwt) {
const sessData = jwtDecode(accessJwt)
return (
hasProp(sessData, 'scope') && sessData.scope === 'com.atproto.deactivated'
)
}
return false
}
export function readLastActiveAccount() {
const {currentAccount, accounts} = persisted.get('session')
return accounts.find(a => a.did === currentAccount?.did)
}
export function agentToSessionAccount(
agent: BskyAgent,
): SessionAccount | undefined {
if (!agent.session) return undefined
return {
service: agent.service.toString(),
did: agent.session.did,
handle: agent.session.handle,
email: agent.session.email,
emailConfirmed: agent.session.emailConfirmed,
deactivated: isSessionDeactivated(agent.session.accessJwt),
refreshJwt: agent.session.refreshJwt,
accessJwt: agent.session.accessJwt,
}
}
export function sessionAccountToAgentSession(
account: SessionAccount,
): BskyAgent['session'] {
return {
did: account.did,
handle: account.handle,
email: account.email,
emailConfirmed: account.emailConfirmed,
accessJwt: account.accessJwt || '',
refreshJwt: account.refreshJwt || '',
}
}
export async function configureModeration(
agent: BskyAgent,
account?: SessionAccount,
) {
if (account) {
if (IS_TEST_USER(account.handle)) {
const did = (
await agent
.resolveHandle({handle: 'mod-authority.test'})
.catch(_ => undefined)
)?.data.did
if (did) {
console.warn('USING TEST ENV MODERATION')
BskyAgent.configure({appLabelers: [did]})
}
} else {
BskyAgent.configure({appLabelers: [BSKY_LABELER_DID]})
if (account) {
const labelerDids = await readLabelers(account.did).catch(_ => {})
if (labelerDids) {
agent.configureLabelersHeader(
labelerDids.filter(did => did !== BSKY_LABELER_DID),
)
}
}
}
} else {
BskyAgent.configure({appLabelers: [BSKY_LABELER_DID]})
}
}
export function isSessionExpired(account: SessionAccount) {
let canReusePrevSession = false
try {
if (account.accessJwt) {
const decoded = jwtDecode(account.accessJwt)
if (decoded.exp) {
const didExpire = Date.now() >= decoded.exp * 1000
if (!didExpire) {
canReusePrevSession = true
}
}
}
} catch (e) {
logger.error(`session: could not decode jwt`)
}
return !canReusePrevSession
}
export async function createAgentAndLogin({
service,
identifier,
password,
}: {
service: string
identifier: string
password: string
}) {
const agent = new BskyAgent({service})
await agent.login({identifier, password})
if (!agent.session) {
throw new Error(`session: login failed to establish a session`)
}
const account = agentToSessionAccount(agent)!
await configureModeration(agent, account)
return {
agent,
account,
}
}
export async function createAgentAndCreateAccount({
service,
email,
password,
handle,
inviteCode,
verificationPhone,
verificationCode,
}: Parameters<SessionApiContext['createAccount']>[0]) {
const agent = new BskyAgent({service})
await agent.createAccount({
handle,
password,
email,
inviteCode,
verificationPhone,
verificationCode,
})
if (!agent.session) {
throw new Error(`session: createAccount failed to establish a session`)
}
const deactivated = isSessionDeactivated(agent.session.accessJwt)
if (!deactivated) {
/*dont await*/ agent.upsertProfile(_existing => {
return {
displayName: '',
// HACKFIX
// creating a bunch of identical profile objects is breaking the relay
// tossing this unspecced field onto it to reduce the size of the problem
// -prf
createdAt: new Date().toISOString(),
}
})
}
const account = agentToSessionAccount(agent)!
await configureModeration(agent, account)
return {
agent,
account,
}
}
+351 -561
View File
File diff suppressed because it is too large Load Diff