Skip to content

Commit

Permalink
feat(api): add support for oauth client
Browse files Browse the repository at this point in the history
  • Loading branch information
matthieusieben committed May 3, 2024
1 parent 0086798 commit a5de0ef
Show file tree
Hide file tree
Showing 6 changed files with 55 additions and 25 deletions.
1 change: 1 addition & 0 deletions packages/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
10 changes: 8 additions & 2 deletions packages/api/src/agent.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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

Expand Down Expand Up @@ -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', () =>
Expand Down
33 changes: 33 additions & 0 deletions packages/api/src/dispatcher/oauth-dispatcher.ts
Original file line number Diff line number Diff line change
@@ -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<URL> {
return new URL(this.client.serverMetadata.issuer)
}

async getDid(): Promise<string> {
const { sub } = await this.client.getTokenSet()
return sub
}
}
10 changes: 5 additions & 5 deletions packages/oauth-client-browser-example/src/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export type AppState = {
function App() {
const {
initialized,
atpClient,
agent,
client,
signedIn,
signOut,
Expand All @@ -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',
Expand All @@ -71,7 +71,7 @@ function App() {
console.log(profile)

setProfile(profile.data)
}, [client, atpClient])
}, [client, agent])

if (!initialized) {
return <p>{error || 'Loading...'}</p>
Expand Down
23 changes: 5 additions & 18 deletions packages/oauth-client-browser-example/src/oauth.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,24 @@
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'

export function useOAuth(factory: BrowserOAuthClientFactory) {
const [initialized, setInitialized] = useState(false)
const [client, setClient] = useState<undefined | null | OAuthClient>(void 0)
const [clients, setClients] = useState<{ [_: string]: OAuthClient }>({})
const [atpClient, setAtpClient] = useState<AtpClient | null>(null)
const [error, setError] = useState<null | string>(null)
const [loading, setLoading] = useState(true)
const [state, setState] = useState<undefined | string>(undefined)

const agent = useMemo(() => (client ? new BskyAgent(client) : null), [client])

const semaphore = useRef(0)

useEffect(() => {
Expand All @@ -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++

Expand Down Expand Up @@ -126,7 +113,7 @@ export function useOAuth(factory: BrowserOAuthClientFactory) {
initialized,
clients,
client: client ?? null,
atpClient,
agent,
state,
loading,
error,
Expand Down
3 changes: 3 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit a5de0ef

Please sign in to comment.