Skip to content

Commit

Permalink
Instantiate XrpcClient from an OAuthAgent (#2714)
Browse files Browse the repository at this point in the history
* Improve transformation of fetchHandler errors into XrpcError

* Add ability to instantiate XrpcClient from FetchHandlerObject type

* Remove un-necessary dev dependency

* Allow oauthAgent to be used in order to instantiate XrpcClient

* fix lock file

* Move OAuthAtpAgent  to api package

* correct doc

* docs(oauth-client): improve example

* fix example code

* Rename OAuthAgent into OAuthSession

* Allow instantiating Agent and XrpcClient with OAuthSession

* Fix changesets

* codegen

* tidy

* tidy

* tidy

* Update .changeset/chilled-jokes-relax.md

Co-authored-by: surfdude29 <[email protected]>

* Update packages/oauth/oauth-client/README.md

Co-authored-by: surfdude29 <[email protected]>

* Update packages/api/OAUTH.md

Co-authored-by: surfdude29 <[email protected]>

* Update .changeset/old-mice-give.md

Co-authored-by: surfdude29 <[email protected]>

* Update packages/api/OAUTH.md

* Update packages/api/README.md

* Update packages/api/README.md

* Update .changeset/polite-toys-happen.md

---------

Co-authored-by: surfdude29 <[email protected]>
Co-authored-by: devin ivy <[email protected]>
  • Loading branch information
3 people authored Aug 22, 2024
1 parent f70bd6a commit d9ffa3c
Show file tree
Hide file tree
Showing 36 changed files with 625 additions and 365 deletions.
7 changes: 7 additions & 0 deletions .changeset/chilled-jokes-relax.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@atproto/oauth-client-browser": minor
"@atproto/oauth-client-node": minor
"@atproto/oauth-client": minor
---

The `OAuthClient` (and runtime specific sub-classes) no longer return @atproto/api `Agent` instances. Instead, they return `OAuthSession` instances that can be used to instantiate the `Agent` class.
5 changes: 5 additions & 0 deletions .changeset/early-rivers-occur.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@atproto/oauth-client-node": patch
---

Remove un-necessary dev dependency
5 changes: 5 additions & 0 deletions .changeset/hungry-parrots-study.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@atproto/xrpc": patch
---

Improve handling of fetchHandler errors when turning them into `XrpcError`.
5 changes: 5 additions & 0 deletions .changeset/nine-deers-count.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@atproto/api": patch
---

Drop use of `AtpBaseClient` class
6 changes: 6 additions & 0 deletions .changeset/old-mice-give.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@atproto/api": patch
---

Expose the `CredentialSession` class that can be used to instantiate both `Agent` and `XrpcClient`, while internally managing credential based (username/password) sessions.

5 changes: 5 additions & 0 deletions .changeset/polite-toys-happen.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@atproto/api": patch
---

`Agent` is no longer an abstract class. Instead it can be instantiated using object implementing a new `SessionManager` interface. If your project extends `Agent` and overrides the constructor or any method implementations, consider that you may want to call them from `super`.
5 changes: 5 additions & 0 deletions .changeset/short-llamas-rescue.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@atproto/oauth-client": patch
---

Add `getTokenInfo()` method to `OAuthSession`.
5 changes: 5 additions & 0 deletions .changeset/smooth-houses-hope.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@atproto/oauth-client": minor
---

Rename OAuthAgent into OAuthSession
5 changes: 5 additions & 0 deletions .changeset/tame-elephants-unite.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@atproto/oauth-client": minor
---

Rename `OAuthSession`'s `request` method to `fetchHandler`. The goal of this change is to allow `OAuthSession` to be used in order to instantiate `XrpcClient` by implementing the `FetchHandlerObject` interface.
5 changes: 5 additions & 0 deletions .changeset/tasty-dingos-design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@atproto/xrpc": patch
---

Add ability to instantiate XrpcClient from FetchHandlerObject type
5 changes: 5 additions & 0 deletions .changeset/twelve-years-speak.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@atproto/xrpc": patch
---

Add global headers to `XrpcClient` instances
5 changes: 5 additions & 0 deletions .changeset/wet-radios-fry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@atproto/oauth-client": patch
---

Make `getTokenSet()` method public in `OAuthSession`.
32 changes: 22 additions & 10 deletions packages/api/OAUTH.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@ ngrok as the `client_id`:
Replace the content of the `src/app.ts` file, with the following content:

```typescript
import { Agent } from '@atproto/api'
import { BrowserOAuthClient } from '@atproto/oauth-client-browser'

async function main() {
Expand Down Expand Up @@ -200,19 +201,28 @@ following code:

```typescript
const result = await oauthClient.init()
const agent = result?.agent

if (result) {
if ('state' in result) {
console.log('The user was just redirected back from the authorization page')
}

console.log(`The user is currently signed in as ${result.session.did}`)
}

const session = result?.session

// TO BE CONTINUED
```

At this point you can detect if the user is already authenticated or not (by
checking if `agent` is `undefined`).
checking if `session` is `undefined`).

Let's initiate an authentication flow if the user is not authenticated. Replace
the `// TO BE CONTINUED` comment with the following code:

```typescript
if (!agent) {
if (!session) {
const handle = prompt('Enter your atproto handle to authenticate')
if (!handle) throw new Error('Authentication process canceled by the user')

Expand All @@ -234,14 +244,16 @@ if (!agent) {
// TO BE CONTINUED
```

At this point in the script, the user **will** be authenticated. API calls can
be made using the `agent`. The `agent` is an instance of a sub-class of the
`Agent` from `@atproto/api`. Let's make a simple call to the API to retrieve the
user's profile. Replace the `// TO BE CONTINUED` comment with the following
code:
At this point in the script, the user **will** be authenticated. Authenticated
API calls can be made using the `session`. The `session` can be used to instantiate the
`Agent` class from `@atproto/api`. Let's make a simple call to the API to
retrieve the user's profile. Replace the `// TO BE CONTINUED` comment with the
following code:

```typescript
if (agent) {
if (session) {
const agent = new Agent(session)

const fetchProfile = async () => {
const profile = await agent.getProfile({ actor: agent.did })
return profile.data
Expand All @@ -263,7 +275,7 @@ if (agent) {
document.body.appendChild(logoutBtn)
logoutBtn.textContent = 'Logout'
logoutBtn.onclick = async () => {
await oauthAgent.signOut()
await session.signOut()
window.location.reload()
}

Expand Down
24 changes: 17 additions & 7 deletions packages/api/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,24 +88,32 @@ are available:
Lower lever; compatible with most JS engines.

Every `@atproto/oauth-client-*` implementation has a different way to obtain an
OAuth based API agent instance. Here is an example restoring a previously
saved session:
`OAuthSession` instance that can be used to instantiate an `Agent` (from
`@atproto/api`). Here is an example restoring a previously saved session:

```typescript
import { Agent } from '@atproto/api'
import { OAuthClient } from '@atproto/oauth-client'

const oauthClient = new OAuthClient({
// ...
})

const agent = await oauthClient.restore('did:plc:123')
const oauthSession = await oauthClient.restore('did:plc:123')

// Instantiate the api Agent using an OAuthSession
const agent = new Agent(oauthSession)
```

### API calls

The agent includes methods for many common operations, including:

```typescript
// The DID of the user currently authenticated (or undefined)
agent.did
agent.accountDid // Throws if the user is not authenticated

// Feeds and content
await agent.getTimeline(params, opts)
await agent.getAuthorFeed(params, opts)
Expand Down Expand Up @@ -151,11 +159,13 @@ await agent.updateSeenNotifications()
await agent.resolveHandle(params, opts)
await agent.updateHandle(params, opts)

// Session management (OAuth based agent instances have a different set of methods)
// Legacy: Session management should be performed through the SessionManager
// rather than the Agent instance.
if (agent instanceof AtpAgent) {
await agent.createAccount(params)
await agent.login(params)
await agent.resumeSession(session)
// AtpAgent instances support using different sessions during their lifetime
await agent.createAccount({ ... }) // session a
await agent.login({ ... }) // session b
await agent.resumeSession(savedSession) // session c
}
```

Expand Down
42 changes: 29 additions & 13 deletions packages/api/src/agent.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
import { TID } from '@atproto/common-web'
import { AtUri, ensureValidDid } from '@atproto/syntax'
import {
buildFetchHandler,
FetchHandler,
FetchHandlerOptions,
} from '@atproto/xrpc'
import { buildFetchHandler, FetchHandler, XrpcClient } from '@atproto/xrpc'
import AwaitLock from 'await-lock'
import {
AppBskyActorDefs,
AppBskyActorProfile,
AppBskyFeedPost,
AppBskyLabelerDefs,
AtpBaseClient,
AppNS,
ChatNS,
ComAtprotoRepoPutRecord,
ComNS,
ToolsNS,
} from './client/index'
import { schemas } from './client/lexicons'
import { MutedWord } from './client/types/app/bsky/actor/defs'
import { BSKY_LABELER_DID } from './const'
import { interpretLabelValueDefinitions } from './moderation'
Expand All @@ -23,6 +23,7 @@ import {
LabelPreference,
ModerationPrefs,
} from './moderation/types'
import { SessionManager } from './session-manager'
import {
AtpAgentGlobalOpts,
AtprotoServiceType,
Expand Down Expand Up @@ -68,14 +69,13 @@ export type { FetchHandler }
/**
* An {@link Agent} is an {@link AtpBaseClient} with the following
* additional features:
* - Abstract session management utilities
* - AT Protocol labelers configuration utilities
* - AT Protocol proxy configuration utilities
* - Cloning utilities
* - `app.bsky` syntactic sugar
* - `com.atproto` syntactic sugar
*/
export abstract class Agent extends AtpBaseClient {
export class Agent extends XrpcClient {
//#region Static configuration

/**
Expand All @@ -94,8 +94,18 @@ export abstract class Agent extends AtpBaseClient {

//#endregion

constructor(fetchHandlerOpts: FetchHandler | FetchHandlerOptions) {
const fetchHandler = buildFetchHandler(fetchHandlerOpts)
com = new ComNS(this)
app = new AppNS(this)
chat = new ChatNS(this)
tools = new ToolsNS(this)

/** @deprecated use `this` instead */
get xrpc(): XrpcClient {
return this
}

constructor(readonly sessionManager: SessionManager) {
const fetchHandler = buildFetchHandler(sessionManager)

super((url, init) => {
const headers = new Headers(init?.headers)
Expand All @@ -118,16 +128,20 @@ export abstract class Agent extends AtpBaseClient {
)

return fetchHandler(url, { ...init, headers })
})
}, schemas)
}

//#region Cloning utilities

abstract clone(): Agent
clone(): Agent {
return this.copyInto(new Agent(this.sessionManager))
}

copyInto<T extends Agent>(inst: T): T {
inst.configureLabelers(this.labelers)
inst.configureProxy(this.proxy ?? null)
inst.clearHeaders()
for (const [key, value] of this.headers) inst.setHeader(key, value)
return inst
}

Expand Down Expand Up @@ -185,7 +199,9 @@ export abstract class Agent extends AtpBaseClient {
/**
* Get the authenticated user's DID, if any.
*/
abstract readonly did?: string
get did() {
return this.sessionManager.did
}

/**
* Get the authenticated user's DID, or throw an error if not authenticated.
Expand Down
Loading

0 comments on commit d9ffa3c

Please sign in to comment.