From b8192c609c84ea1c5e295f3445df10f6b338fedb Mon Sep 17 00:00:00 2001 From: Matthieu Sieben Date: Fri, 26 Apr 2024 10:57:55 +0200 Subject: [PATCH] feat(api): add support for oauth client --- packages/api/package.json | 1 + packages/api/src/agent.ts | 11 +++++-- .../api/src/session/oauth-session-manager.ts | 33 +++++++++++++++++++ .../oauth-client-browser-example/src/app.tsx | 10 +++--- .../oauth-client-browser-example/src/oauth.ts | 23 +++---------- pnpm-lock.yaml | 3 ++ 6 files changed, 56 insertions(+), 25 deletions(-) create mode 100644 packages/api/src/session/oauth-session-manager.ts diff --git a/packages/api/package.json b/packages/api/package.json index ab6ff45856b..d121873be41 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 63f33ea33f4..7878db78384 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 { SessionManager, isSessionManager } from './session/session-manager' @@ -6,10 +7,14 @@ import { StatelessSessionManagerOptions, } from './session/stateless-session-handler' import { AtpAgentGlobalOpts, AtprotoServiceType } from './types' +import { OAuthSessionManager } from './dispatcher/oauth-dispatcher' const MAX_LABELERS = 10 -export type AtpAgentOptions = SessionManager | StatelessSessionManagerOptions +export type AtpAgentOptions = + | SessionManager + | OAuthClient + | StatelessSessionManagerOptions export class AtpAgent { /** @@ -39,7 +44,9 @@ export class AtpAgent { constructor(options: AtpAgentOptions) { this.sessionManager = isSessionManager(options) ? options - : new StatelessSessionManager(options) + : options instanceof OAuthClient + ? new OAuthSessionManager(options) + : new StatelessSessionManager(options) this.api = new AtpClient((...args) => // The function needs to be "bound" to the right context diff --git a/packages/api/src/session/oauth-session-manager.ts b/packages/api/src/session/oauth-session-manager.ts new file mode 100644 index 00000000000..f9dee5804b0 --- /dev/null +++ b/packages/api/src/session/oauth-session-manager.ts @@ -0,0 +1,33 @@ +import { FetchError, OAuthClient } from '@atproto/oauth-client' +import { XRPCError } from '@atproto/xrpc' +import { SessionManager } from './session-manager' + +export class OAuthSessionManager implements SessionManager { + constructor(readonly client: OAuthClient) {} + + async fetchHandler(url: string, init: RequestInit): Promise { + try { + return await this.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 3724215a635..ac108680111 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