Typescript API Package Auth Refactor
Today we are merging some changes to how the TypeScript @atproto/api
package works with authentication sessions. The changes are mostly backwards compatible, but some parts are now deprecated, and there are some breaking changes for advanced uses.
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. Here is the class hierarchy:
As you adapt your code to these changes, make sure to use the Agent
type wherever you expect to receive an agent, and use the AtpAgent
type (class) only to instantiate your client. The reason for this is to be forward compatible with the OAuth agent implementation that will also extend Agent
, and not AtpAgent
.
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
package
If you were relying on the AtpBaseClient
solely to perform validation, use this:
|
|
If you are extending the BskyAgent
to perform custom session
manipulation, define your own Agent
subclass instead:
|
|
If you are monkey patching the xrpc
service client to perform client-side rate limiting, you can now do this in the FetchHandler
function:
|
|
If you configure a static fetch
handler on the BskyAgent
class - for example to modify the headers of every request - you can now do this by providing your own fetch
function:
|
|
The @atproto/xrpc
package
The Client
and ServiceClient
classes are now deprecated. If you need a lexicon based client, you should update the code to use the XrpcClient
class instead.
The deprecated ServiceClient
class now extends the new XrpcClient
class. Because of this, the fetch
FetchHandler
can no longer be configured on the Client
instances (including the default export of the package). If you are not relying on the fetch
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 new FetchHandler
type is now a function that receives a url
pathname and a RequestInit
object and returns a Promise<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 the ServiceClient
class has not been ported to the new XrpcClient
class. It is now the responsibility of the FetchHandler
to determine the full URL to make the request to. The same goes for the headers
, which should now be set through the FetchHandler
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 new XrpcClient
class. The XrpcClient
class has a similar API to the old ServiceClient
class, but with a few differences:
- The
Client
+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 theClient
class. Instead, it is passed as an argument to theXrpcClient
constructor.
- There no longer exists a centralized lexicon registry. If you need a global lexicon registry, you can maintain one yourself using a
- 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.
|
|
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: {},
},
],
)