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 {restoreOAuthSession} from './oauth-client-adapter'
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 let session: OAuthSession
27 try {
28 session = await Promise.race([
29 restoreOAuthSession(account.did),
30 new Promise<never>((_, reject) =>
31 setTimeout(
32 () => reject(new Error('OAuth session restore timed out')),
33 OAUTH_RESTORE_TIMEOUT_MS,
34 ),
35 ),
36 ])
37 } catch (e) {
38 logger.error('oauthResumeSession: restore failed', {
39 did: account.did,
40 error: e instanceof Error ? e.message : String(e),
41 })
42 throw e
43 }
44 return await oauthCreateAgent(session)
45}
46
47export async function oauthAgentAndSessionToSessionAccountOrThrow(
48 agent: Agent,
49 session: OAuthSession,
50): Promise<SessionAccount> {
51 const account = await oauthAgentAndSessionToSessionAccount(agent, session)
52 if (!account) {
53 throw Error('Expected an active session')
54 }
55 return account
56}
57
58export async function oauthAgentAndSessionToSessionAccount(
59 agent: Agent,
60 session: OAuthSession,
61): Promise<SessionAccount | undefined> {
62 let data: OutputSchema
63 try {
64 const res = await Promise.race([
65 agent.com.atproto.server.getSession(),
66 new Promise<never>((_, reject) =>
67 setTimeout(
68 () => reject(new Error('getSession timed out')),
69 OAUTH_RESTORE_TIMEOUT_MS,
70 ),
71 ),
72 ])
73 data = res.data
74 } catch (e: any) {
75 logger.error('oauthAgentAndSessionToSessionAccount: getSession failed', e)
76 return undefined
77 }
78 let aud: string
79 try {
80 const tokenInfo = await Promise.race([
81 session.getTokenInfo(false),
82 new Promise<never>((_, reject) =>
83 setTimeout(
84 () => reject(new Error('getTokenInfo timed out')),
85 OAUTH_RESTORE_TIMEOUT_MS,
86 ),
87 ),
88 ])
89 aud = tokenInfo.aud
90 } catch (e: any) {
91 logger.error('oauthAgentAndSessionToSessionAccount: getTokenInfo failed', e)
92 return undefined
93 }
94 return {
95 service: session.serverMetadata.issuer,
96 did: session.did,
97 handle: data.handle,
98 email: data.email,
99 emailConfirmed: data.emailConfirmed,
100 emailAuthFactor: data.emailAuthFactor,
101 active: data.active,
102 status: data.status,
103 pdsUrl: aud,
104 isSelfHosted: !session.server.issuer.startsWith(BSKY_SERVICE),
105 isOauthSession: true,
106 }
107}
108
109export class OauthBskyAppAgent extends Agent {
110 readonly sessionManager: OAuthSession
111 session?: AtpSessionData
112 private _serviceUrl: URL
113 private _pdsUrl?: URL
114
115 constructor(session: OAuthSession) {
116 super(session)
117 this.sessionManager = session
118 this._serviceUrl = new URL(session.serverMetadata.issuer)
119 }
120
121 clone(): this {
122 const cloned = this.copyInto(new OauthBskyAppAgent(this.sessionManager))
123 cloned.session = this.session
124 cloned._serviceUrl = this._serviceUrl
125 cloned._pdsUrl = this._pdsUrl
126 return cloned as this
127 }
128
129 get serviceUrl() {
130 return this._serviceUrl
131 }
132
133 get pdsUrl() {
134 return this._pdsUrl
135 }
136
137 get dispatchUrl() {
138 return this.pdsUrl || this.serviceUrl
139 }
140
141 /** @deprecated use {@link serviceUrl} instead */
142 get service() {
143 return this.serviceUrl
144 }
145
146 async prepare(
147 account: SessionAccount,
148 gates: Promise<void>,
149 moderation: Promise<void>,
150 ) {
151 this.session = sessionAccountToSession(account)
152 this._serviceUrl = new URL(account.service)
153 this._pdsUrl = account.pdsUrl ? new URL(account.pdsUrl) : undefined
154 this.configureProxy(BLUESKY_PROXY_HEADER.get())
155
156 await Promise.all([gates, moderation])
157
158 return {account, agent: this}
159 }
160
161 dispose() {}
162
163 cloneWithoutProxy(): OauthBskyAppAgent {
164 const cloned = this.clone()
165 cloned.configureProxy(null)
166 return cloned
167 }
168}