diff --git a/packages/api/package.json b/packages/api/package.json index c301818320e..5ed46f4f8e7 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -24,6 +24,7 @@ "dependencies": { "@atproto/common-web": "workspace:^", "@atproto/lexicon": "workspace:^", + "@atproto/oauth-client": "workspace:^", "@atproto/syntax": "workspace:^", "@atproto/xrpc": "workspace:^", "multiformats": "^9.9.0", diff --git a/packages/api/src/agent.ts b/packages/api/src/agent.ts index 091bcdd2cca..4a285220b0a 100644 --- a/packages/api/src/agent.ts +++ b/packages/api/src/agent.ts @@ -1,3 +1,4 @@ +import { OAuthClient } from '@atproto/oauth-client' import { AtpClient } from './client' import { BSKY_LABELER_DID } from './const' import { AtpDispatcher } from './dispatcher/atp-dispatcher' @@ -6,6 +7,7 @@ import { StatelessDispatcherOptions, } from './dispatcher/stateless-dispatcher' import { AtpAgentGlobalOpts, AtprotoServiceType } from './types' +import { OAuthDispatcher } from './dispatcher/oauth-dispatcher' const MAX_LABELERS = 10 @@ -34,11 +36,15 @@ export class AtpAgent { return this.api.com } - constructor(options: AtpDispatcher | StatelessDispatcherOptions) { + constructor( + options: AtpDispatcher | OAuthClient | StatelessDispatcherOptions, + ) { this.dispatcher = options instanceof AtpDispatcher ? options - : new StatelessDispatcher(options) + : options instanceof OAuthClient + ? new OAuthDispatcher(options) + : new StatelessDispatcher(options) this.api = new AtpClient(this.dispatcher) this.api.setHeader('atproto-accept-labelers', () => diff --git a/packages/api/src/dispatcher/oauth-dispatcher.ts b/packages/api/src/dispatcher/oauth-dispatcher.ts new file mode 100644 index 00000000000..55c8dbdfa5f --- /dev/null +++ b/packages/api/src/dispatcher/oauth-dispatcher.ts @@ -0,0 +1,33 @@ +import { FetchError, OAuthClient } from '@atproto/oauth-client' +import { XRPCError } from '@atproto/xrpc' +import { AtpDispatcher } from './atp-dispatcher' + +export class OAuthDispatcher extends AtpDispatcher { + constructor(readonly client: OAuthClient) { + super(async (url, init) => { + try { + return await client.request(url, init) + } catch (cause) { + if (cause instanceof FetchError) { + throw new XRPCError( + cause.statusCode, + undefined, + cause.message, + undefined, + { cause }, + ) + } + throw cause + } + }) + } + + async getServiceUrl(): Promise { + return new URL(this.client.serverMetadata.issuer) + } + + async getDid(): Promise { + const { sub } = await this.client.getTokenSet() + return sub + } +} diff --git a/packages/oauth-client-browser-example/src/app.tsx b/packages/oauth-client-browser-example/src/app.tsx index 407e493977c..96dd76f096e 100644 --- a/packages/oauth-client-browser-example/src/app.tsx +++ b/packages/oauth-client-browser-example/src/app.tsx @@ -34,7 +34,7 @@ export type AppState = { function App() { const { initialized, - atpClient, + agent, client, signedIn, signOut, @@ -52,17 +52,17 @@ function App() { const info = await client.getUserinfo() console.log('info', info) - if (!atpClient) return + if (!agent) return // A call that requires to be authenticated console.log( - await atpClient.com.atproto.server.getServiceAuth({ + await agent.com.atproto.server.getServiceAuth({ aud: info.sub, }), ) // This call does not require authentication - const profile = await atpClient.com.atproto.repo.getRecord({ + const profile = await agent.com.atproto.repo.getRecord({ repo: info.sub, collection: 'app.bsky.actor.profile', rkey: 'self', @@ -71,7 +71,7 @@ function App() { console.log(profile) setProfile(profile.data) - }, [client, atpClient]) + }, [client, agent]) if (!initialized) { return

{error || 'Loading...'}

diff --git a/packages/oauth-client-browser-example/src/oauth.ts b/packages/oauth-client-browser-example/src/oauth.ts index 27afe6914b5..70b2bd86ea6 100644 --- a/packages/oauth-client-browser-example/src/oauth.ts +++ b/packages/oauth-client-browser-example/src/oauth.ts @@ -1,12 +1,11 @@ -import { AtpClient, schemas } from '@atproto/api' +import { BskyAgent } from '@atproto/api' import { OAuthAuthorizeOptions, OAuthClient } from '@atproto/oauth-client' import { BrowserOAuthClientFactory, LoginContinuedInParentWindowError, } from '@atproto/oauth-client-browser' -import { XrpcAgent, XrpcClient } from '@atproto/xrpc' -import { useCallback, useEffect, useRef, useState } from 'react' +import { useCallback, useEffect, useMemo, useRef, useState } from 'react' const CURRENT_SESSION_ID_KEY = 'CURRENT_SESSION_ID_KEY' @@ -14,11 +13,12 @@ export function useOAuth(factory: BrowserOAuthClientFactory) { const [initialized, setInitialized] = useState(false) const [client, setClient] = useState(void 0) const [clients, setClients] = useState<{ [_: string]: OAuthClient }>({}) - const [atpClient, setAtpClient] = useState(null) const [error, setError] = useState(null) const [loading, setLoading] = useState(true) const [state, setState] = useState(undefined) + const agent = useMemo(() => (client ? new BskyAgent(client) : null), [client]) + const semaphore = useRef(0) useEffect(() => { @@ -29,19 +29,6 @@ export function useOAuth(factory: BrowserOAuthClientFactory) { } }, [client]) - useEffect(() => { - const atpClient = client - ? new AtpClient( - new XrpcClient( - new XrpcAgent((url, init) => client.request(url, init)), - schemas, - ), - ) - : null - - setAtpClient(atpClient) - }, [client]) - useEffect(() => { semaphore.current++ @@ -126,7 +113,7 @@ export function useOAuth(factory: BrowserOAuthClientFactory) { initialized, clients, client: client ?? null, - atpClient, + agent, state, loading, error, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b8e4658a2da..75bd58e56d6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -74,6 +74,9 @@ importers: '@atproto/lexicon': specifier: workspace:^ version: link:../lexicon + '@atproto/oauth-client': + specifier: workspace:^ + version: link:../oauth-client '@atproto/syntax': specifier: workspace:^ version: link:../syntax