forked from
jollywhoppers.com/witchsky.app
Bluesky app fork with some witchin' additions 馃挮
1import {Agent, type AtpSessionData} from '@atproto/api'
2import {type OutputSchema} from '@atproto/api/dist/client/types/com/atproto/server/getSession'
3import {type OAuthSession} from '@atproto/oauth-client-browser'
4
5import {BLUESKY_PROXY_HEADER, BSKY_SERVICE} from '#/lib/constants'
6import {logger} from '#/logger'
7import {sessionAccountToSession} from './agent'
8import {configureModerationForAccount} from './moderation'
9import {getWebOAuthClient} from './oauth-web-client'
10import {type SessionAccount} from './types'
11
12export async function oauthCreateAgent(session: OAuthSession) {
13 const agent = new OauthBskyAppAgent(session)
14 const account = await oauthAgentAndSessionToSessionAccountOrThrow(
15 agent,
16 session,
17 )
18 const gates = Promise.resolve()
19 const moderation = configureModerationForAccount(agent, account)
20 return agent.prepare(account, gates, moderation)
21}
22
23const OAUTH_RESTORE_TIMEOUT_MS = 10_000
24
25export async function oauthResumeSession(account: SessionAccount) {
26 const client = getWebOAuthClient()
27 let session: OAuthSession
28 try {
29 session = await Promise.race([
30 client.restore(account.did),
31 new Promise<never>((_, reject) =>
32 setTimeout(
33 () => reject(new Error('OAuth session restore timed out')),
34 OAUTH_RESTORE_TIMEOUT_MS,
35 ),
36 ),
37 ])
38 } catch (e) {
39 logger.error('oauthResumeSession: restore failed', {
40 did: account.did,
41 error: e instanceof Error ? e.message : String(e),
42 })
43 throw e
44 }
45 return await oauthCreateAgent(session)
46}
47
48export async function oauthAgentAndSessionToSessionAccountOrThrow(
49 agent: Agent,
50 session: OAuthSession,
51): Promise<SessionAccount> {
52 const account = await oauthAgentAndSessionToSessionAccount(agent, session)
53 if (!account) {
54 throw Error('Expected an active session')
55 }
56 return account
57}
58
59export async function oauthAgentAndSessionToSessionAccount(
60 agent: Agent,
61 session: OAuthSession,
62): Promise<SessionAccount | undefined> {
63 let data: OutputSchema
64 try {
65 const res = await Promise.race([
66 agent.com.atproto.server.getSession(),
67 new Promise<never>((_, reject) =>
68 setTimeout(
69 () => reject(new Error('getSession timed out')),
70 OAUTH_RESTORE_TIMEOUT_MS,
71 ),
72 ),
73 ])
74 data = res.data
75 } catch (e: any) {
76 logger.error('oauthAgentAndSessionToSessionAccount: getSession failed', e)
77 return undefined
78 }
79 let aud: string
80 try {
81 const tokenInfo = await Promise.race([
82 session.getTokenInfo(false),
83 new Promise<never>((_, reject) =>
84 setTimeout(
85 () => reject(new Error('getTokenInfo timed out')),
86 OAUTH_RESTORE_TIMEOUT_MS,
87 ),
88 ),
89 ])
90 aud = tokenInfo.aud
91 } catch (e: any) {
92 logger.error('oauthAgentAndSessionToSessionAccount: getTokenInfo failed', e)
93 return undefined
94 }
95 return {
96 service: session.serverMetadata.issuer,
97 did: session.did,
98 handle: data.handle,
99 email: data.email,
100 emailConfirmed: data.emailConfirmed,
101 emailAuthFactor: data.emailAuthFactor,
102 active: data.active,
103 status: data.status,
104 pdsUrl: aud,
105 isSelfHosted: !session.server.issuer.startsWith(BSKY_SERVICE),
106 isOauthSession: true,
107 }
108}
109
110export class OauthBskyAppAgent extends Agent {
111 session?: AtpSessionData
112 dispatchUrl?: string
113
114 constructor(session: OAuthSession) {
115 super(session)
116 }
117
118 async prepare(
119 account: SessionAccount,
120 gates: Promise<void>,
121 moderation: Promise<void>,
122 ) {
123 this.session = sessionAccountToSession(account)
124 this.dispatchUrl = account.pdsUrl
125 this.configureProxy(BLUESKY_PROXY_HEADER.get())
126
127 await Promise.all([gates, moderation])
128
129 return {account, agent: this}
130 }
131
132 dispose() {}
133
134 cloneWithoutProxy(): OauthBskyAppAgent {
135 const cloned = new OauthBskyAppAgent(this.sessionManager as OAuthSession)
136 cloned.session = this.session
137 cloned.configureProxy(null)
138 return cloned
139 }
140}