Atproto: @atproto/[email protected] Release
8 Commits in this Release
Ordered by the degree to which they evolved the repo in this version.
Browse Other Releases
Top Contributors in @atproto/[email protected]
Directory Browser for @atproto/[email protected]
All files are compared to previous version, @atproto/[email protected]. Click here to browse diffs between other versions.
Release Notes Published
Minor Changes
#2483
b934b396b
Thanks @matthieusieben!Motivation
The motivation for these changes is the need to make the
@atproto/api
package compatible with OAuth session management. We don't have OAuth client support "launched" and documented quite yet, so you can keep using the current app password authentication system. When we do "launch" OAuth support and begin encouraging its usage in the near future (see the OAuth Roadmap), these changes will make it easier to migrate.In addition, the redesigned session management system fixes a bug that could cause the session data to become invalid when Agent clones are created (e.g. using
agent.withProxy()
).New Features
We've restructured the
XrpcClient
HTTP fetch handler to be specified during the instantiation of the XRPC client, through the constructor, instead of using a default implementation (which was statically defined).With this refactor, the XRPC client is now more modular and reusable. Session management, retries, cryptographic signing, and other request-specific logic can be implemented in the fetch handler itself rather than by the calling code.
A new abstract class named
Agent
, has been added to@atproto/api
. This class will be the base class for all Bluesky agents classes in the@atproto
ecosystem. It is meant to be extended by implementations that provide session management and fetch handling.As you adapt your code to these changes, make sure to use the
Agent
type wherever you expect to receive an agent, and use theAtpAgent
type (class) only to instantiate your client. The reason for this is to be forward compatible with the OAuth agent implementation that will also extendAgent
, and notAtpAgent
.import { Agent, AtpAgent } from '@atproto/api' async function setupAgent( service: string, username: string, password: string, ): Promise<Agent> { const agent = new AtpAgent({ service, persistSession: (evt, session) => { // handle session update }, }) await agent.login(username, password) return agent }
import { Agent } from '@atproto/api' async function doStuffWithAgent(agent: Agent, arg: string) { return agent.resolveHandle(arg) }
import { Agent, AtpAgent } from '@atproto/api' class MyClass { agent: Agent constructor() { this.agent = new AtpAgent() } }
Breaking changes
Most of the changes introduced in this version are backward-compatible. However, there are a couple of breaking changes you should be aware of:
- Customizing
fetch
: The ability to customize thefetch: FetchHandler
property of@atproto/xrpc
'sClient
and@atproto/api
'sAtpAgent
classes has been removed. Previously, thefetch
property could be set to a function that would be used as the fetch handler for that instance, and was initialized to a default fetch handler. That property is still accessible in a read-only fashion through thefetchHandler
property and can only be set during the instance creation. Attempting to set/get thefetch
property will now result in an error. - The
fetch()
method, as well as WhatWG compliantRequest
andHeaders
constructors, must be globally available in your environment. Use a polyfill if necessary. - The
AtpBaseClient
has been removed. TheAtpServiceClient
has been renamedAtpBaseClient
. Any code using either of these classes will need to be updated. - Instead of wrapping an
XrpcClient
in itsxrpc
property, theAtpBaseClient
(formerlyAtpServiceClient
) class - created throughlex-cli
- now extends theXrpcClient
class. This means that a client instance now passes theinstanceof XrpcClient
check. Thexrpc
property now returns the instance itself and has been deprecated. -
setSessionPersistHandler
is no longer available on theAtpAgent
orBskyAgent
classes. The session handler can only be set though thepersistSession
options of theAtpAgent
constructor. - The new class hierarchy is as follows:
-
BskyAgent
extendsAtpAgent
: but add no functionality (hence its deprecation). -
AtpAgent
extendsAgent
: adds password based session management. -
Agent
extendsAtpBaseClient
: this abstract class that adds syntactic sugar methodsapp.bsky
lexicons. It also adds abstract session management methods and adds atproto specific utilities (labelers
&proxy
headers, cloning capability) -
AtpBaseClient
extendsXrpcClient
: automatically code that adds fully typed lexicon defined namespaces (instance.app.bsky.feed.getPosts()
) to theXrpcClient
. -
XrpcClient
is the base class.
-
Non-breaking changes
- The
com.*
andapp.*
namespaces have been made directly available to everyAgent
instances.
Deprecations
- The default export of the
@atproto/xrpc
package has been deprecated. Use named exports instead. - The
Client
andServiceClient
classes are now deprecated. They are replaced by a singleXrpcClient
class. - The default export of the
@atproto/api
package has been deprecated. Use named exports instead. - The
BskyAgent
has been deprecated. Use theAtpAgent
class instead. - The
xrpc
property of theAtpClient
instances has been deprecated. The instance itself should be used as the XRPC client. - The
api
property of theAtpAgent
andBskyAgent
instances has been deprecated. Use the instance itself instead.
Migration
The
@atproto/api
packageIf you were relying on the
AtpBaseClient
solely to perform validation, use this:<table> <tr> <td><center>Before</center></td> <td><center>After</center></td> </tr> <tr> <td>
import { AtpBaseClient, ComAtprotoSyncSubscribeRepos } from '@atproto/api' const baseClient = new AtpBaseClient() baseClient.xrpc.lex.assertValidXrpcMessage('io.example.doStuff', { // ... })
</td> <td>
import { lexicons } from '@atproto/api' lexicons.assertValidXrpcMessage('io.example.doStuff', { // ... })
</td> </tr> </table>
If you are extending the
BskyAgent
to perform customsession
manipulation, define your ownAgent
subclass instead:<table> <tr> <td><center>Before</center></td> <td><center>After</center></td> </tr> <tr> <td>
import { BskyAgent } from '@atproto/api' class MyAgent extends BskyAgent { private accessToken?: string async createOrRefreshSession(identifier: string, password: string) { // custom logic here this.accessToken = 'my-access-jwt' } async doStuff() { return this.call('io.example.doStuff', { headers: { Authorization: this.accessToken && `Bearer ${this.accessToken}`, }, }) } }
</td> <td>
import { Agent } from '@atproto/api' class MyAgent extends Agent { private accessToken?: string public did?: string constructor(private readonly service: string | URL) { super({ service, headers: { Authorization: () => this.accessToken ? `Bearer ${this.accessToken}` : null, }, }) } clone(): MyAgent { const agent = new MyAgent(this.service) agent.accessToken = this.accessToken agent.did = this.did return this.copyInto(agent) } async createOrRefreshSession(identifier: string, password: string) { // custom logic here this.did = 'did:example:123' this.accessToken = 'my-access-jwt' } }
</td> </tr> </table>
If you are monkey patching the
xrpc
service client to perform client-side rate limiting, you can now do this in theFetchHandler
function:<table> <tr> <td><center>Before</center></td> <td><center>After</center></td> </tr> <tr> <td>
import { BskyAgent } from '@atproto/api' import { RateLimitThreshold } from 'rate-limit-threshold' const agent = new BskyAgent() const limiter = new RateLimitThreshold(3000, 300_000) const origCall = agent.api.xrpc.call agent.api.xrpc.call = async function (...args) { await limiter.wait() return origCall.call(this, ...args) }
</td> <td>
import { AtpAgent } from '@atproto/api' import { RateLimitThreshold } from 'rate-limit-threshold' class LimitedAtpAgent extends AtpAgent { constructor(options: AtpAgentOptions) { const fetch: typeof globalThis.fetch = options.fetch ?? globalThis.fetch const limiter = new RateLimitThreshold(3000, 300_000) super({ ...options, fetch: async (...args) => { await limiter.wait() return fetch(...args) }, }) } }
</td> </tr> </table>
If you configure a static
fetch
handler on theBskyAgent
class - for example to modify the headers of every request - you can now do this by providing your ownfetch
function:<table> <tr> <td><center>Before</center></td> <td><center>After</center></td> </tr> <tr> <td>
import { BskyAgent, defaultFetchHandler } from '@atproto/api' BskyAgent.configure({ fetch: async (httpUri, httpMethod, httpHeaders, httpReqBody) => { const ua = httpHeaders['User-Agent'] httpHeaders['User-Agent'] = ua ? `${ua} ${userAgent}` : userAgent return defaultFetchHandler(httpUri, httpMethod, httpHeaders, httpReqBody) }, })
</td> <td>
import { AtpAgent } from '@atproto/api' class MyAtpAgent extends AtpAgent { constructor(options: AtpAgentOptions) { const fetch = options.fetch ?? globalThis.fetch super({ ...options, fetch: async (url, init) => { const headers = new Headers(init.headers) const ua = headersList.get('User-Agent') headersList.set('User-Agent', ua ? `${ua} ${userAgent}` : userAgent) return fetch(url, { ...init, headers }) }, }) } }
</td> </tr> </table>
<!-- <table> <tr> <td><center>Before</center></td> <td><center>After</center></td> </tr> <tr> <td>
// before
</td> <td>
// after
</td> </tr> </table> -->
The
@atproto/xrpc
packageThe
Client
andServiceClient
classes are now deprecated. If you need a lexicon based client, you should update the code to use theXrpcClient
class instead.The deprecated
ServiceClient
class now extends the newXrpcClient
class. Because of this, thefetch
FetchHandler
can no longer be configured on theClient
instances (including the default export of the package). If you are not relying on thefetch
FetchHandler
, the new changes should have no impact on your code. Beware that the deprecated classes will eventually be removed in a future version.Since its use has completely changed, the
FetchHandler
type has also completely changed. The newFetchHandler
type is now a function that receives aurl
pathname and aRequestInit
object and returns aPromise<Response>
. This function is responsible for making the actual request to the server.export type FetchHandler = ( this: void, /** * The URL (pathname + query parameters) to make the request to, without the * origin. The origin (protocol, hostname, and port) must be added by this * {@link FetchHandler}, typically based on authentication or other factors. */ url: string, init: RequestInit, ) => Promise<Response>
A noticeable change that has been introduced is that the
uri
field of theServiceClient
class has not been ported to the newXrpcClient
class. It is now the responsibility of theFetchHandler
to determine the full URL to make the request to. The same goes for theheaders
, which should now be set through theFetchHandler
function.If you do rely on the legacy
Client.fetch
property to perform custom logic upon request, you will need to migrate your code to use the newXrpcClient
class. TheXrpcClient
class has a similar API to the oldServiceClient
class, but with a few differences: - TheClient
+ServiceClient
duality was removed in favor of a singleXrpcClient
class. This means that:- There no longer exists a centralized lexicon registry. If you need a global lexicon registry, you can maintain one yourself using a `new Lexicons` (from `@atproto/lexicon`). - The `FetchHandler` is no longer a statically defined property of the `Client` class. Instead, it is passed as an argument to the `XrpcClient` constructor.
- The
XrpcClient
constructor now requires aFetchHandler
function as the first argument, and an optionalLexicon
instance as the second argument. - The
setHeader
andunsetHeader
methods were not ported to the newXrpcClient
class. If you need to set or unset headers, you should do so in theFetchHandler
function provided in the constructor arg.
<table> <tr> <td><center>Before</center></td> <td><center>After</center></td> </tr> <tr> <td>
import client, { defaultFetchHandler } from '@atproto/xrpc' client.fetch = function ( httpUri: string, httpMethod: string, httpHeaders: Headers, httpReqBody: unknown, ) { // Custom logic here return defaultFetchHandler(httpUri, httpMethod, httpHeaders, httpReqBody) } client.addLexicon({ lexicon: 1, id: 'io.example.doStuff', defs: {}, }) const instance = client.service('http://my-service.com') instance.setHeader('my-header', 'my-value') await instance.call('io.example.doStuff')
</td> <td>
import { XrpcClient } from '@atproto/xrpc' const instance = new XrpcClient( async (url, init) => { const headers = new Headers(init.headers) headers.set('my-header', 'my-value') // Custom logic here const fullUrl = new URL(url, 'http://my-service.com') return fetch(fullUrl, { ...init, headers }) }, [ { lexicon: 1, id: 'io.example.doStuff', defs: {}, }, ], ) await instance.call('io.example.doStuff')
</td> </tr> </table>
If your fetch handler does not require any "custom logic", and all you need is an
XrpcClient
that makes its HTTP requests towards a static service URL, the previous example can be simplified to:import { XrpcClient } from '@atproto/xrpc' const instance = new XrpcClient('http://my-service.com', [ { lexicon: 1, id: 'io.example.doStuff', defs: {}, }, ])
If you need to add static headers to all requests, you can instead instantiate the
XrpcClient
as follows:import { XrpcClient } from '@atproto/xrpc' const instance = new XrpcClient( { service: 'http://my-service.com', headers: { 'my-header': 'my-value', }, }, [ { lexicon: 1, id: 'io.example.doStuff', defs: {}, }, ], )
If you need the headers or service url to be dynamic, you can define them using functions:
import { XrpcClient } from '@atproto/xrpc' const instance = new XrpcClient( { service: () => 'http://my-service.com', headers: { 'my-header': () => 'my-value', 'my-ignored-header': () => null, // ignored }, }, [ { lexicon: 1, id: 'io.example.doStuff', defs: {}, }, ], )
- Customizing
#2483
b934b396b
Thanks @matthieusieben! - Add the ability to usefetch()
compatibleBodyInit
body when making XRPC calls.
Patch Changes
- Updated dependencies [
b934b396b
,2bdf75d7a
,b934b396b
,b934b396b
]:- @atproto/[email protected]
- @atproto/[email protected]