diff --git a/packages/api/docs/rn-fetch-handler.ts b/packages/api/docs/rn-fetch-handler.ts deleted file mode 100644 index 448915fcfa7..00000000000 --- a/packages/api/docs/rn-fetch-handler.ts +++ /dev/null @@ -1,88 +0,0 @@ -/** - * The following is the react-native fetch handler used currently in the bluesky app - * It's not our prettiest work, but it gets the job done - */ - -import { BskyAgent, stringifyLex, jsonToLex } from '@atproto/api' -import RNFS from 'react-native-fs' - -const GET_TIMEOUT = 15e3 // 15s -const POST_TIMEOUT = 60e3 // 60s - -export function doPolyfill() { - BskyAgent.configure({ fetch: fetchHandler }) -} - -interface FetchHandlerResponse { - status: number - headers: Record - body: ArrayBuffer | undefined -} - -async function fetchHandler( - reqUri: string, - reqMethod: string, - reqHeaders: Record, - reqBody: any, -): Promise { - const reqMimeType = reqHeaders['Content-Type'] || reqHeaders['content-type'] - if (reqMimeType && reqMimeType.startsWith('application/json')) { - reqBody = stringifyLex(reqBody) - } else if ( - typeof reqBody === 'string' && - (reqBody.startsWith('/') || reqBody.startsWith('file:')) - ) { - if (reqBody.endsWith('.jpeg') || reqBody.endsWith('.jpg')) { - // HACK - // React native has a bug that inflates the size of jpegs on upload - // we get around that by renaming the file ext to .bin - // see https://github.com/facebook/react-native/issues/27099 - // -prf - const newPath = reqBody.replace(/\.jpe?g$/, '.bin') - await RNFS.moveFile(reqBody, newPath) - reqBody = newPath - } - // NOTE - // React native treats bodies with {uri: string} as file uploads to pull from cache - // -prf - reqBody = { uri: reqBody } - } - - const controller = new AbortController() - const to = setTimeout( - () => controller.abort(), - reqMethod === 'post' ? POST_TIMEOUT : GET_TIMEOUT, - ) - - const res = await fetch(reqUri, { - method: reqMethod, - headers: reqHeaders, - body: reqBody, - signal: controller.signal, - }) - - const resStatus = res.status - const resHeaders: Record = {} - res.headers.forEach((value: string, key: string) => { - resHeaders[key] = value - }) - const resMimeType = resHeaders['Content-Type'] || resHeaders['content-type'] - let resBody - if (resMimeType) { - if (resMimeType.startsWith('application/json')) { - resBody = jsonToLex(await res.json()) - } else if (resMimeType.startsWith('text/')) { - resBody = await res.text() - } else { - resBody = await res.blob() - } - } - - clearTimeout(to) - - return { - status: resStatus, - headers: resHeaders, - body: resBody, - } -} diff --git a/packages/api/src/agent.ts b/packages/api/src/agent.ts index 17ef3b978bd..63f33ea33f4 100644 --- a/packages/api/src/agent.ts +++ b/packages/api/src/agent.ts @@ -1,97 +1,71 @@ -import { ErrorResponseBody, errorResponseBody } from '@atproto/xrpc' -import { defaultFetchHandler, XRPCError, ResponseType } from '@atproto/xrpc' -import { isValidDidDoc, getPdsEndpoint } from '@atproto/common-web' -import { - AtpBaseClient, - AtpServiceClient, - ComAtprotoServerCreateAccount, - ComAtprotoServerCreateSession, - ComAtprotoServerGetSession, - ComAtprotoServerRefreshSession, -} from './client' -import { - AtpSessionData, - AtpAgentLoginOpts, - AtpAgentFetchHandler, - AtpAgentFetchHandlerResponse, - AtpAgentGlobalOpts, - AtpPersistSessionHandler, - AtpAgentOpts, - AtprotoServiceType, -} from './types' +import { AtpClient } from './client' import { BSKY_LABELER_DID } from './const' +import { SessionManager, isSessionManager } from './session/session-manager' +import { + StatelessSessionManager, + StatelessSessionManagerOptions, +} from './session/stateless-session-handler' +import { AtpAgentGlobalOpts, AtprotoServiceType } from './types' -const MAX_MOD_AUTHORITIES = 3 const MAX_LABELERS = 10 -const REFRESH_SESSION = 'com.atproto.server.refreshSession' - -/** - * An ATP "Agent" - * Manages session token lifecycles and provides convenience methods. - */ -export class AtpAgent { - service: URL - api: AtpServiceClient - session?: AtpSessionData - labelersHeader: string[] = [] - proxyHeader: string | undefined - pdsUrl: URL | undefined // The PDS URL, driven by the did doc. May be undefined. - - protected _baseClient: AtpBaseClient - protected _persistSession?: AtpPersistSessionHandler - protected _refreshSessionPromise: Promise | undefined - get com() { - return this.api.com - } - - /** - * The `fetch` implementation; must be implemented for your platform. - */ - static fetch: AtpAgentFetchHandler | undefined = defaultFetchHandler +export type AtpAgentOptions = SessionManager | StatelessSessionManagerOptions +export class AtpAgent { /** * The labelers to be used across all requests with the takedown capability */ - static appLabelers: string[] = [BSKY_LABELER_DID] + static appLabelers: readonly string[] = [BSKY_LABELER_DID] /** - * Configures the API globally. + * Configures the AtpAgent globally. */ static configure(opts: AtpAgentGlobalOpts) { - if (opts.fetch) { - AtpAgent.fetch = opts.fetch - } if (opts.appLabelers) { - AtpAgent.appLabelers = opts.appLabelers + this.appLabelers = [...opts.appLabelers] } } - constructor(opts: AtpAgentOpts) { - this.service = - opts.service instanceof URL ? opts.service : new URL(opts.service) - this._persistSession = opts.persistSession + api: AtpClient + labelersHeader: string[] = [] + proxyHeader?: string + + readonly sessionManager: SessionManager - // create an ATP client instance for this agent - this._baseClient = new AtpBaseClient() - this._baseClient.xrpc.fetch = this._fetch.bind(this) // patch its fetch implementation - this.api = this._baseClient.service(opts.service) + get com() { + return this.api.com + } + + constructor(options: AtpAgentOptions) { + this.sessionManager = isSessionManager(options) + ? options + : new StatelessSessionManager(options) + + this.api = new AtpClient((...args) => + // The function needs to be "bound" to the right context + this.sessionManager.fetchHandler(...args), + ) + this.api.setHeader('atproto-accept-labelers', () => + // Make sure to read the static property from the subclass in case it was + // overridden. + (this.constructor as typeof AtpAgent).appLabelers + .map((str) => `${str};redact`) + .concat(this.labelersHeader.filter((str) => str.startsWith('did:'))) + .slice(0, MAX_LABELERS) + .join(', '), + ) + this.api.setHeader('atproto-proxy', () => this.proxyHeader) } clone() { - const inst = new AtpAgent({ - service: this.service, - }) + const inst = new AtpAgent(this.sessionManager) this.copyInto(inst) return inst } copyInto(inst: AtpAgent) { - inst.session = this.session inst.labelersHeader = this.labelersHeader inst.proxyHeader = this.proxyHeader - inst.pdsUrl = this.pdsUrl - inst.api.xrpc.uri = this.pdsUrl || this.service } withProxy(serviceType: AtprotoServiceType, did: string) { @@ -100,19 +74,16 @@ export class AtpAgent { return inst } - /** - * Is there any active session? - */ - get hasSession() { - return !!this.session + /** @deprecated only used for a very particular use-case in the official Bluesky app */ + async getServiceUrl(): Promise { + return this.sessionManager.getServiceUrl() } /** - * Sets the "Persist Session" method which can be used to store access tokens - * as they change. + * Get the active session's DID */ - setPersistSessionHandler(handler?: AtpPersistSessionHandler) { - this._persistSession = handler + async getDid(): Promise { + return this.sessionManager.getDid() } /** @@ -133,249 +104,6 @@ export class AtpAgent { } } - /** - * Create a new account and hydrate its session in this agent. - */ - async createAccount( - opts: ComAtprotoServerCreateAccount.InputSchema, - ): Promise { - try { - const res = await this.api.com.atproto.server.createAccount(opts) - this.session = { - accessJwt: res.data.accessJwt, - refreshJwt: res.data.refreshJwt, - handle: res.data.handle, - did: res.data.did, - email: opts.email, - emailConfirmed: false, - emailAuthFactor: false, - } - this._updateApiEndpoint(res.data.didDoc) - return res - } catch (e) { - this.session = undefined - throw e - } finally { - if (this.session) { - this._persistSession?.('create', this.session) - } else { - this._persistSession?.('create-failed', undefined) - } - } - } - - /** - * Start a new session with this agent. - */ - async login( - opts: AtpAgentLoginOpts, - ): Promise { - try { - const res = await this.api.com.atproto.server.createSession({ - identifier: opts.identifier, - password: opts.password, - authFactorToken: opts.authFactorToken, - }) - this.session = { - accessJwt: res.data.accessJwt, - refreshJwt: res.data.refreshJwt, - handle: res.data.handle, - did: res.data.did, - email: res.data.email, - emailConfirmed: res.data.emailConfirmed, - emailAuthFactor: res.data.emailAuthFactor, - } - this._updateApiEndpoint(res.data.didDoc) - return res - } catch (e) { - this.session = undefined - throw e - } finally { - if (this.session) { - this._persistSession?.('create', this.session) - } else { - this._persistSession?.('create-failed', undefined) - } - } - } - - /** - * Resume a pre-existing session with this agent. - */ - async resumeSession( - session: AtpSessionData, - ): Promise { - try { - this.session = session - const res = await this.api.com.atproto.server.getSession() - if (res.data.did !== this.session.did) { - throw new XRPCError( - ResponseType.InvalidRequest, - 'Invalid session', - 'InvalidDID', - ) - } - this.session.email = res.data.email - this.session.handle = res.data.handle - this.session.emailConfirmed = res.data.emailConfirmed - this.session.emailAuthFactor = res.data.emailAuthFactor - this._updateApiEndpoint(res.data.didDoc) - this._persistSession?.('update', this.session) - return res - } catch (e) { - this.session = undefined - - if (e instanceof XRPCError) { - /* - * `ExpiredToken` and `InvalidToken` are handled in - * `this_refreshSession`, and emit an `expired` event there. - * - * Everything else is handled here. - */ - if ( - [1, 408, 425, 429, 500, 502, 503, 504, 522, 524].includes(e.status) - ) { - this._persistSession?.('network-error', undefined) - } else { - this._persistSession?.('expired', undefined) - } - } else { - this._persistSession?.('network-error', undefined) - } - - throw e - } - } - - /** - * Internal helper to add authorization headers to requests. - */ - private _addHeaders(reqHeaders: Record) { - if (!reqHeaders.authorization && this.session?.accessJwt) { - reqHeaders = { - ...reqHeaders, - authorization: `Bearer ${this.session.accessJwt}`, - } - } - if (this.proxyHeader) { - reqHeaders = { - ...reqHeaders, - 'atproto-proxy': this.proxyHeader, - } - } - reqHeaders = { - ...reqHeaders, - 'atproto-accept-labelers': AtpAgent.appLabelers - .map((str) => `${str};redact`) - .concat(this.labelersHeader.filter((str) => str.startsWith('did:'))) - .slice(0, MAX_LABELERS) - .join(', '), - } - return reqHeaders - } - - /** - * Internal fetch handler which adds access-token management - */ - private async _fetch( - reqUri: string, - reqMethod: string, - reqHeaders: Record, - reqBody: any, - ): Promise { - if (!AtpAgent.fetch) { - throw new Error('AtpAgent fetch() method not configured') - } - - // wait for any active session-refreshes to finish - await this._refreshSessionPromise - - // send the request - let res = await AtpAgent.fetch( - reqUri, - reqMethod, - this._addHeaders(reqHeaders), - reqBody, - ) - - // handle session-refreshes as needed - if (isErrorResponse(res, ['ExpiredToken']) && this.session?.refreshJwt) { - // attempt refresh - await this.refreshSession() - - // resend the request with the new access token - res = await AtpAgent.fetch( - reqUri, - reqMethod, - this._addHeaders(reqHeaders), - reqBody, - ) - } - - return res - } - - /** - * Internal helper to refresh sessions - * - Wraps the actual implementation in a promise-guard to ensure only - * one refresh is attempted at a time. - */ - async refreshSession() { - if (this._refreshSessionPromise) { - return this._refreshSessionPromise - } - this._refreshSessionPromise = this._refreshSessionInner() - try { - await this._refreshSessionPromise - } finally { - this._refreshSessionPromise = undefined - } - } - - /** - * Internal helper to refresh sessions (actual behavior) - */ - private async _refreshSessionInner() { - if (!AtpAgent.fetch) { - throw new Error('AtpAgent fetch() method not configured') - } - if (!this.session?.refreshJwt) { - return - } - - // send the refresh request - const url = new URL((this.pdsUrl || this.service).origin) - url.pathname = `/xrpc/${REFRESH_SESSION}` - const res = await AtpAgent.fetch( - url.toString(), - 'POST', - { - authorization: `Bearer ${this.session.refreshJwt}`, - }, - undefined, - ) - - if (isErrorResponse(res, ['ExpiredToken', 'InvalidToken'])) { - // failed due to a bad refresh token - this.session = undefined - this._persistSession?.('expired', undefined) - } else if (isNewSessionObject(this._baseClient, res.body)) { - // succeeded, update the session - this.session = { - ...(this.session || {}), - accessJwt: res.body.accessJwt, - refreshJwt: res.body.refreshJwt, - handle: res.body.handle, - did: res.body.did, - } - this._updateApiEndpoint(res.body.didDoc) - this._persistSession?.('update', this.session) - } - // else: other failures should be ignored - the issue will - // propagate in the _fetch() handler's second attempt to run - // the request - } - /** * Upload a binary blob to the server */ @@ -403,56 +131,4 @@ export class AtpAgent { */ createModerationReport: typeof this.api.com.atproto.moderation.createReport = (data, opts) => this.api.com.atproto.moderation.createReport(data, opts) - - /** - * Helper to update the pds endpoint dynamically. - * - * The session methods (create, resume, refresh) may respond with the user's - * did document which contains the user's canonical PDS endpoint. That endpoint - * may differ from the endpoint used to contact the server. We capture that - * PDS endpoint and update the client to use that given endpoint for future - * requests. (This helps ensure smooth migrations between PDSes, especially - * when the PDSes are operated by a single org.) - */ - private _updateApiEndpoint(didDoc: unknown) { - if (isValidDidDoc(didDoc)) { - const endpoint = getPdsEndpoint(didDoc) - this.pdsUrl = endpoint ? new URL(endpoint) : undefined - } - this.api.xrpc.uri = this.pdsUrl || this.service - } -} - -function isErrorObject(v: unknown): v is ErrorResponseBody { - return errorResponseBody.safeParse(v).success -} - -function isErrorResponse( - res: AtpAgentFetchHandlerResponse, - errorNames: string[], -): boolean { - if (res.status !== 400) { - return false - } - if (!isErrorObject(res.body)) { - return false - } - return ( - typeof res.body.error === 'string' && errorNames.includes(res.body.error) - ) -} - -function isNewSessionObject( - client: AtpBaseClient, - v: unknown, -): v is ComAtprotoServerRefreshSession.OutputSchema { - try { - client.xrpc.lex.assertValidXrpcOutput( - 'com.atproto.server.refreshSession', - v, - ) - return true - } catch { - return false - } } diff --git a/packages/api/src/bsky-agent.ts b/packages/api/src/bsky-agent.ts index c70a066978c..c4461866b41 100644 --- a/packages/api/src/bsky-agent.ts +++ b/packages/api/src/bsky-agent.ts @@ -51,9 +51,7 @@ declare global { export class BskyAgent extends AtpAgent { clone() { - const inst = new BskyAgent({ - service: this.service, - }) + const inst = new BskyAgent(this.sessionManager) this.copyInto(inst) return inst } @@ -126,13 +124,13 @@ export class BskyAgent extends AtpAgent { prefs: BskyPreferences | ModerationPrefs | string[], ): Promise> { // collect the labeler dids - let dids: string[] = BskyAgent.appLabelers + const dids = [...BskyAgent.appLabelers] if (isBskyPrefs(prefs)) { - dids = dids.concat(prefs.moderationPrefs.labelers.map((l) => l.did)) + dids.push(...prefs.moderationPrefs.labelers.map((l) => l.did)) } else if (isModPrefs(prefs)) { - dids = dids.concat(prefs.labelers.map((l) => l.did)) + dids.push(...prefs.labelers.map((l) => l.did)) } else { - dids = dids.concat(prefs) + dids.push(...prefs) } // fetch their definitions @@ -157,20 +155,18 @@ export class BskyAgent extends AtpAgent { record: Partial & Omit, ) { - if (!this.session) { - throw new Error('Not logged in') - } + const repo = await this.getDid() + record.createdAt = record.createdAt || new Date().toISOString() return this.api.app.bsky.feed.post.create( - { repo: this.session.did }, + { repo }, record as AppBskyFeedPost.Record, ) } async deletePost(postUri: string) { - if (!this.session) { - throw new Error('Not logged in') - } + await this.getDid() + const postUrip = new AtUri(postUri) return await this.api.app.bsky.feed.post.delete({ repo: postUrip.hostname, @@ -179,11 +175,10 @@ export class BskyAgent extends AtpAgent { } async like(uri: string, cid: string) { - if (!this.session) { - throw new Error('Not logged in') - } + const repo = await this.getDid() + return await this.api.app.bsky.feed.like.create( - { repo: this.session.did }, + { repo }, { subject: { uri, cid }, createdAt: new Date().toISOString(), @@ -192,9 +187,8 @@ export class BskyAgent extends AtpAgent { } async deleteLike(likeUri: string) { - if (!this.session) { - throw new Error('Not logged in') - } + await this.getDid() + const likeUrip = new AtUri(likeUri) return await this.api.app.bsky.feed.like.delete({ repo: likeUrip.hostname, @@ -203,11 +197,10 @@ export class BskyAgent extends AtpAgent { } async repost(uri: string, cid: string) { - if (!this.session) { - throw new Error('Not logged in') - } + const repo = await this.getDid() + return await this.api.app.bsky.feed.repost.create( - { repo: this.session.did }, + { repo }, { subject: { uri, cid }, createdAt: new Date().toISOString(), @@ -216,9 +209,8 @@ export class BskyAgent extends AtpAgent { } async deleteRepost(repostUri: string) { - if (!this.session) { - throw new Error('Not logged in') - } + await this.getDid() + const repostUrip = new AtUri(repostUri) return await this.api.app.bsky.feed.repost.delete({ repo: repostUrip.hostname, @@ -227,11 +219,10 @@ export class BskyAgent extends AtpAgent { } async follow(subjectDid: string) { - if (!this.session) { - throw new Error('Not logged in') - } + const repo = await this.getDid() + return await this.api.app.bsky.graph.follow.create( - { repo: this.session.did }, + { repo }, { subject: subjectDid, createdAt: new Date().toISOString(), @@ -240,9 +231,8 @@ export class BskyAgent extends AtpAgent { } async deleteFollow(followUri: string) { - if (!this.session) { - throw new Error('Not logged in') - } + await this.getDid() + const followUrip = new AtUri(followUri) return await this.api.app.bsky.graph.follow.delete({ repo: followUrip.hostname, @@ -255,16 +245,14 @@ export class BskyAgent extends AtpAgent { existing: AppBskyActorProfile.Record | undefined, ) => AppBskyActorProfile.Record | Promise, ) { - if (!this.session) { - throw new Error('Not logged in') - } + const repo = await this.getDid() let retriesRemaining = 5 while (retriesRemaining >= 0) { // fetch existing const existing = await this.com.atproto.repo .getRecord({ - repo: this.session.did, + repo, collection: 'app.bsky.actor.profile', rkey: 'self', }) @@ -285,7 +273,7 @@ export class BskyAgent extends AtpAgent { try { // attempt the put await this.com.atproto.repo.putRecord({ - repo: this.session.did, + repo, collection: 'app.bsky.actor.profile', rkey: 'self', record: updated, @@ -328,11 +316,10 @@ export class BskyAgent extends AtpAgent { } async blockModList(uri: string) { - if (!this.session) { - throw new Error('Not logged in') - } + const repo = await this.getDid() + return await this.api.app.bsky.graph.listblock.create( - { repo: this.session.did }, + { repo }, { subject: uri, createdAt: new Date().toISOString(), @@ -341,9 +328,8 @@ export class BskyAgent extends AtpAgent { } async unblockModList(uri: string) { - if (!this.session) { - throw new Error('Not logged in') - } + const repo = await this.getDid() + const listInfo = await this.api.app.bsky.graph.getList({ list: uri, limit: 1, @@ -353,7 +339,7 @@ export class BskyAgent extends AtpAgent { } const { rkey } = new AtUri(listInfo.data.list.viewer.blocked) return await this.api.app.bsky.graph.listblock.delete({ - repo: this.session.did, + repo, rkey, }) } diff --git a/packages/api/src/client/index.ts b/packages/api/src/client/index.ts index 6054b461fd3..524fb1093a1 100644 --- a/packages/api/src/client/index.ts +++ b/packages/api/src/client/index.ts @@ -1,10 +1,7 @@ /** * GENERATED CODE - DO NOT MODIFY */ -import { - Client as XrpcClient, - ServiceClient as XrpcServiceClient, -} from '@atproto/xrpc' +import { XrpcClient, FetchHandler, FetchHandlerOptions } from '@atproto/xrpc' import { schemas } from './lexicons' import { CID } from 'multiformats/cid' import * as ComAtprotoAdminDefs from './types/com/atproto/admin/defs' @@ -403,52 +400,38 @@ export const TOOLS_OZONE_MODERATION = { DefsReviewNone: 'tools.ozone.moderation.defs#reviewNone', } -export class AtpBaseClient { - xrpc: XrpcClient = new XrpcClient() - - constructor() { - this.xrpc.addLexicons(schemas) - } - - service(serviceUri: string | URL): AtpServiceClient { - return new AtpServiceClient(this, this.xrpc.service(serviceUri)) - } -} - -export class AtpServiceClient { - _baseClient: AtpBaseClient - xrpc: XrpcServiceClient +export class AtpClient extends XrpcClient { com: ComNS app: AppNS chat: ChatNS tools: ToolsNS - constructor(baseClient: AtpBaseClient, xrpcService: XrpcServiceClient) { - this._baseClient = baseClient - this.xrpc = xrpcService + constructor(options: FetchHandler | FetchHandlerOptions) { + super(options, schemas) this.com = new ComNS(this) this.app = new AppNS(this) this.chat = new ChatNS(this) this.tools = new ToolsNS(this) } - setHeader(key: string, value: string): void { - this.xrpc.setHeader(key, value) + /** @deprecated use `this` instead */ + get xrpc(): XrpcClient { + return this } } export class ComNS { - _service: AtpServiceClient + _client: XrpcClient atproto: ComAtprotoNS - constructor(service: AtpServiceClient) { - this._service = service - this.atproto = new ComAtprotoNS(service) + constructor(client: XrpcClient) { + this._client = client + this.atproto = new ComAtprotoNS(client) } } export class ComAtprotoNS { - _service: AtpServiceClient + _client: XrpcClient admin: ComAtprotoAdminNS identity: ComAtprotoIdentityNS label: ComAtprotoLabelNS @@ -458,31 +441,31 @@ export class ComAtprotoNS { sync: ComAtprotoSyncNS temp: ComAtprotoTempNS - constructor(service: AtpServiceClient) { - this._service = service - this.admin = new ComAtprotoAdminNS(service) - this.identity = new ComAtprotoIdentityNS(service) - this.label = new ComAtprotoLabelNS(service) - this.moderation = new ComAtprotoModerationNS(service) - this.repo = new ComAtprotoRepoNS(service) - this.server = new ComAtprotoServerNS(service) - this.sync = new ComAtprotoSyncNS(service) - this.temp = new ComAtprotoTempNS(service) + constructor(client: XrpcClient) { + this._client = client + this.admin = new ComAtprotoAdminNS(client) + this.identity = new ComAtprotoIdentityNS(client) + this.label = new ComAtprotoLabelNS(client) + this.moderation = new ComAtprotoModerationNS(client) + this.repo = new ComAtprotoRepoNS(client) + this.server = new ComAtprotoServerNS(client) + this.sync = new ComAtprotoSyncNS(client) + this.temp = new ComAtprotoTempNS(client) } } export class ComAtprotoAdminNS { - _service: AtpServiceClient + _client: XrpcClient - constructor(service: AtpServiceClient) { - this._service = service + constructor(client: XrpcClient) { + this._client = client } deleteAccount( data?: ComAtprotoAdminDeleteAccount.InputSchema, opts?: ComAtprotoAdminDeleteAccount.CallOptions, ): Promise { - return this._service.xrpc + return this._client .call('com.atproto.admin.deleteAccount', opts?.qp, data, opts) .catch((e) => { throw ComAtprotoAdminDeleteAccount.toKnownErr(e) @@ -493,7 +476,7 @@ export class ComAtprotoAdminNS { data?: ComAtprotoAdminDisableAccountInvites.InputSchema, opts?: ComAtprotoAdminDisableAccountInvites.CallOptions, ): Promise { - return this._service.xrpc + return this._client .call('com.atproto.admin.disableAccountInvites', opts?.qp, data, opts) .catch((e) => { throw ComAtprotoAdminDisableAccountInvites.toKnownErr(e) @@ -504,7 +487,7 @@ export class ComAtprotoAdminNS { data?: ComAtprotoAdminDisableInviteCodes.InputSchema, opts?: ComAtprotoAdminDisableInviteCodes.CallOptions, ): Promise { - return this._service.xrpc + return this._client .call('com.atproto.admin.disableInviteCodes', opts?.qp, data, opts) .catch((e) => { throw ComAtprotoAdminDisableInviteCodes.toKnownErr(e) @@ -515,7 +498,7 @@ export class ComAtprotoAdminNS { data?: ComAtprotoAdminEnableAccountInvites.InputSchema, opts?: ComAtprotoAdminEnableAccountInvites.CallOptions, ): Promise { - return this._service.xrpc + return this._client .call('com.atproto.admin.enableAccountInvites', opts?.qp, data, opts) .catch((e) => { throw ComAtprotoAdminEnableAccountInvites.toKnownErr(e) @@ -526,7 +509,7 @@ export class ComAtprotoAdminNS { params?: ComAtprotoAdminGetAccountInfo.QueryParams, opts?: ComAtprotoAdminGetAccountInfo.CallOptions, ): Promise { - return this._service.xrpc + return this._client .call('com.atproto.admin.getAccountInfo', params, undefined, opts) .catch((e) => { throw ComAtprotoAdminGetAccountInfo.toKnownErr(e) @@ -537,7 +520,7 @@ export class ComAtprotoAdminNS { params?: ComAtprotoAdminGetAccountInfos.QueryParams, opts?: ComAtprotoAdminGetAccountInfos.CallOptions, ): Promise { - return this._service.xrpc + return this._client .call('com.atproto.admin.getAccountInfos', params, undefined, opts) .catch((e) => { throw ComAtprotoAdminGetAccountInfos.toKnownErr(e) @@ -548,7 +531,7 @@ export class ComAtprotoAdminNS { params?: ComAtprotoAdminGetInviteCodes.QueryParams, opts?: ComAtprotoAdminGetInviteCodes.CallOptions, ): Promise { - return this._service.xrpc + return this._client .call('com.atproto.admin.getInviteCodes', params, undefined, opts) .catch((e) => { throw ComAtprotoAdminGetInviteCodes.toKnownErr(e) @@ -559,7 +542,7 @@ export class ComAtprotoAdminNS { params?: ComAtprotoAdminGetSubjectStatus.QueryParams, opts?: ComAtprotoAdminGetSubjectStatus.CallOptions, ): Promise { - return this._service.xrpc + return this._client .call('com.atproto.admin.getSubjectStatus', params, undefined, opts) .catch((e) => { throw ComAtprotoAdminGetSubjectStatus.toKnownErr(e) @@ -570,7 +553,7 @@ export class ComAtprotoAdminNS { data?: ComAtprotoAdminSendEmail.InputSchema, opts?: ComAtprotoAdminSendEmail.CallOptions, ): Promise { - return this._service.xrpc + return this._client .call('com.atproto.admin.sendEmail', opts?.qp, data, opts) .catch((e) => { throw ComAtprotoAdminSendEmail.toKnownErr(e) @@ -581,7 +564,7 @@ export class ComAtprotoAdminNS { data?: ComAtprotoAdminUpdateAccountEmail.InputSchema, opts?: ComAtprotoAdminUpdateAccountEmail.CallOptions, ): Promise { - return this._service.xrpc + return this._client .call('com.atproto.admin.updateAccountEmail', opts?.qp, data, opts) .catch((e) => { throw ComAtprotoAdminUpdateAccountEmail.toKnownErr(e) @@ -592,7 +575,7 @@ export class ComAtprotoAdminNS { data?: ComAtprotoAdminUpdateAccountHandle.InputSchema, opts?: ComAtprotoAdminUpdateAccountHandle.CallOptions, ): Promise { - return this._service.xrpc + return this._client .call('com.atproto.admin.updateAccountHandle', opts?.qp, data, opts) .catch((e) => { throw ComAtprotoAdminUpdateAccountHandle.toKnownErr(e) @@ -603,7 +586,7 @@ export class ComAtprotoAdminNS { data?: ComAtprotoAdminUpdateAccountPassword.InputSchema, opts?: ComAtprotoAdminUpdateAccountPassword.CallOptions, ): Promise { - return this._service.xrpc + return this._client .call('com.atproto.admin.updateAccountPassword', opts?.qp, data, opts) .catch((e) => { throw ComAtprotoAdminUpdateAccountPassword.toKnownErr(e) @@ -614,7 +597,7 @@ export class ComAtprotoAdminNS { data?: ComAtprotoAdminUpdateSubjectStatus.InputSchema, opts?: ComAtprotoAdminUpdateSubjectStatus.CallOptions, ): Promise { - return this._service.xrpc + return this._client .call('com.atproto.admin.updateSubjectStatus', opts?.qp, data, opts) .catch((e) => { throw ComAtprotoAdminUpdateSubjectStatus.toKnownErr(e) @@ -623,17 +606,17 @@ export class ComAtprotoAdminNS { } export class ComAtprotoIdentityNS { - _service: AtpServiceClient + _client: XrpcClient - constructor(service: AtpServiceClient) { - this._service = service + constructor(client: XrpcClient) { + this._client = client } getRecommendedDidCredentials( params?: ComAtprotoIdentityGetRecommendedDidCredentials.QueryParams, opts?: ComAtprotoIdentityGetRecommendedDidCredentials.CallOptions, ): Promise { - return this._service.xrpc + return this._client .call( 'com.atproto.identity.getRecommendedDidCredentials', params, @@ -649,7 +632,7 @@ export class ComAtprotoIdentityNS { data?: ComAtprotoIdentityRequestPlcOperationSignature.InputSchema, opts?: ComAtprotoIdentityRequestPlcOperationSignature.CallOptions, ): Promise { - return this._service.xrpc + return this._client .call( 'com.atproto.identity.requestPlcOperationSignature', opts?.qp, @@ -665,7 +648,7 @@ export class ComAtprotoIdentityNS { params?: ComAtprotoIdentityResolveHandle.QueryParams, opts?: ComAtprotoIdentityResolveHandle.CallOptions, ): Promise { - return this._service.xrpc + return this._client .call('com.atproto.identity.resolveHandle', params, undefined, opts) .catch((e) => { throw ComAtprotoIdentityResolveHandle.toKnownErr(e) @@ -676,7 +659,7 @@ export class ComAtprotoIdentityNS { data?: ComAtprotoIdentitySignPlcOperation.InputSchema, opts?: ComAtprotoIdentitySignPlcOperation.CallOptions, ): Promise { - return this._service.xrpc + return this._client .call('com.atproto.identity.signPlcOperation', opts?.qp, data, opts) .catch((e) => { throw ComAtprotoIdentitySignPlcOperation.toKnownErr(e) @@ -687,7 +670,7 @@ export class ComAtprotoIdentityNS { data?: ComAtprotoIdentitySubmitPlcOperation.InputSchema, opts?: ComAtprotoIdentitySubmitPlcOperation.CallOptions, ): Promise { - return this._service.xrpc + return this._client .call('com.atproto.identity.submitPlcOperation', opts?.qp, data, opts) .catch((e) => { throw ComAtprotoIdentitySubmitPlcOperation.toKnownErr(e) @@ -698,7 +681,7 @@ export class ComAtprotoIdentityNS { data?: ComAtprotoIdentityUpdateHandle.InputSchema, opts?: ComAtprotoIdentityUpdateHandle.CallOptions, ): Promise { - return this._service.xrpc + return this._client .call('com.atproto.identity.updateHandle', opts?.qp, data, opts) .catch((e) => { throw ComAtprotoIdentityUpdateHandle.toKnownErr(e) @@ -707,17 +690,17 @@ export class ComAtprotoIdentityNS { } export class ComAtprotoLabelNS { - _service: AtpServiceClient + _client: XrpcClient - constructor(service: AtpServiceClient) { - this._service = service + constructor(client: XrpcClient) { + this._client = client } queryLabels( params?: ComAtprotoLabelQueryLabels.QueryParams, opts?: ComAtprotoLabelQueryLabels.CallOptions, ): Promise { - return this._service.xrpc + return this._client .call('com.atproto.label.queryLabels', params, undefined, opts) .catch((e) => { throw ComAtprotoLabelQueryLabels.toKnownErr(e) @@ -726,17 +709,17 @@ export class ComAtprotoLabelNS { } export class ComAtprotoModerationNS { - _service: AtpServiceClient + _client: XrpcClient - constructor(service: AtpServiceClient) { - this._service = service + constructor(client: XrpcClient) { + this._client = client } createReport( data?: ComAtprotoModerationCreateReport.InputSchema, opts?: ComAtprotoModerationCreateReport.CallOptions, ): Promise { - return this._service.xrpc + return this._client .call('com.atproto.moderation.createReport', opts?.qp, data, opts) .catch((e) => { throw ComAtprotoModerationCreateReport.toKnownErr(e) @@ -745,17 +728,17 @@ export class ComAtprotoModerationNS { } export class ComAtprotoRepoNS { - _service: AtpServiceClient + _client: XrpcClient - constructor(service: AtpServiceClient) { - this._service = service + constructor(client: XrpcClient) { + this._client = client } applyWrites( data?: ComAtprotoRepoApplyWrites.InputSchema, opts?: ComAtprotoRepoApplyWrites.CallOptions, ): Promise { - return this._service.xrpc + return this._client .call('com.atproto.repo.applyWrites', opts?.qp, data, opts) .catch((e) => { throw ComAtprotoRepoApplyWrites.toKnownErr(e) @@ -766,7 +749,7 @@ export class ComAtprotoRepoNS { data?: ComAtprotoRepoCreateRecord.InputSchema, opts?: ComAtprotoRepoCreateRecord.CallOptions, ): Promise { - return this._service.xrpc + return this._client .call('com.atproto.repo.createRecord', opts?.qp, data, opts) .catch((e) => { throw ComAtprotoRepoCreateRecord.toKnownErr(e) @@ -777,7 +760,7 @@ export class ComAtprotoRepoNS { data?: ComAtprotoRepoDeleteRecord.InputSchema, opts?: ComAtprotoRepoDeleteRecord.CallOptions, ): Promise { - return this._service.xrpc + return this._client .call('com.atproto.repo.deleteRecord', opts?.qp, data, opts) .catch((e) => { throw ComAtprotoRepoDeleteRecord.toKnownErr(e) @@ -788,7 +771,7 @@ export class ComAtprotoRepoNS { params?: ComAtprotoRepoDescribeRepo.QueryParams, opts?: ComAtprotoRepoDescribeRepo.CallOptions, ): Promise { - return this._service.xrpc + return this._client .call('com.atproto.repo.describeRepo', params, undefined, opts) .catch((e) => { throw ComAtprotoRepoDescribeRepo.toKnownErr(e) @@ -799,7 +782,7 @@ export class ComAtprotoRepoNS { params?: ComAtprotoRepoGetRecord.QueryParams, opts?: ComAtprotoRepoGetRecord.CallOptions, ): Promise { - return this._service.xrpc + return this._client .call('com.atproto.repo.getRecord', params, undefined, opts) .catch((e) => { throw ComAtprotoRepoGetRecord.toKnownErr(e) @@ -810,7 +793,7 @@ export class ComAtprotoRepoNS { data?: ComAtprotoRepoImportRepo.InputSchema, opts?: ComAtprotoRepoImportRepo.CallOptions, ): Promise { - return this._service.xrpc + return this._client .call('com.atproto.repo.importRepo', opts?.qp, data, opts) .catch((e) => { throw ComAtprotoRepoImportRepo.toKnownErr(e) @@ -821,7 +804,7 @@ export class ComAtprotoRepoNS { params?: ComAtprotoRepoListMissingBlobs.QueryParams, opts?: ComAtprotoRepoListMissingBlobs.CallOptions, ): Promise { - return this._service.xrpc + return this._client .call('com.atproto.repo.listMissingBlobs', params, undefined, opts) .catch((e) => { throw ComAtprotoRepoListMissingBlobs.toKnownErr(e) @@ -832,7 +815,7 @@ export class ComAtprotoRepoNS { params?: ComAtprotoRepoListRecords.QueryParams, opts?: ComAtprotoRepoListRecords.CallOptions, ): Promise { - return this._service.xrpc + return this._client .call('com.atproto.repo.listRecords', params, undefined, opts) .catch((e) => { throw ComAtprotoRepoListRecords.toKnownErr(e) @@ -843,7 +826,7 @@ export class ComAtprotoRepoNS { data?: ComAtprotoRepoPutRecord.InputSchema, opts?: ComAtprotoRepoPutRecord.CallOptions, ): Promise { - return this._service.xrpc + return this._client .call('com.atproto.repo.putRecord', opts?.qp, data, opts) .catch((e) => { throw ComAtprotoRepoPutRecord.toKnownErr(e) @@ -854,7 +837,7 @@ export class ComAtprotoRepoNS { data?: ComAtprotoRepoUploadBlob.InputSchema, opts?: ComAtprotoRepoUploadBlob.CallOptions, ): Promise { - return this._service.xrpc + return this._client .call('com.atproto.repo.uploadBlob', opts?.qp, data, opts) .catch((e) => { throw ComAtprotoRepoUploadBlob.toKnownErr(e) @@ -863,17 +846,17 @@ export class ComAtprotoRepoNS { } export class ComAtprotoServerNS { - _service: AtpServiceClient + _client: XrpcClient - constructor(service: AtpServiceClient) { - this._service = service + constructor(client: XrpcClient) { + this._client = client } activateAccount( data?: ComAtprotoServerActivateAccount.InputSchema, opts?: ComAtprotoServerActivateAccount.CallOptions, ): Promise { - return this._service.xrpc + return this._client .call('com.atproto.server.activateAccount', opts?.qp, data, opts) .catch((e) => { throw ComAtprotoServerActivateAccount.toKnownErr(e) @@ -884,7 +867,7 @@ export class ComAtprotoServerNS { params?: ComAtprotoServerCheckAccountStatus.QueryParams, opts?: ComAtprotoServerCheckAccountStatus.CallOptions, ): Promise { - return this._service.xrpc + return this._client .call('com.atproto.server.checkAccountStatus', params, undefined, opts) .catch((e) => { throw ComAtprotoServerCheckAccountStatus.toKnownErr(e) @@ -895,7 +878,7 @@ export class ComAtprotoServerNS { data?: ComAtprotoServerConfirmEmail.InputSchema, opts?: ComAtprotoServerConfirmEmail.CallOptions, ): Promise { - return this._service.xrpc + return this._client .call('com.atproto.server.confirmEmail', opts?.qp, data, opts) .catch((e) => { throw ComAtprotoServerConfirmEmail.toKnownErr(e) @@ -906,7 +889,7 @@ export class ComAtprotoServerNS { data?: ComAtprotoServerCreateAccount.InputSchema, opts?: ComAtprotoServerCreateAccount.CallOptions, ): Promise { - return this._service.xrpc + return this._client .call('com.atproto.server.createAccount', opts?.qp, data, opts) .catch((e) => { throw ComAtprotoServerCreateAccount.toKnownErr(e) @@ -917,7 +900,7 @@ export class ComAtprotoServerNS { data?: ComAtprotoServerCreateAppPassword.InputSchema, opts?: ComAtprotoServerCreateAppPassword.CallOptions, ): Promise { - return this._service.xrpc + return this._client .call('com.atproto.server.createAppPassword', opts?.qp, data, opts) .catch((e) => { throw ComAtprotoServerCreateAppPassword.toKnownErr(e) @@ -928,7 +911,7 @@ export class ComAtprotoServerNS { data?: ComAtprotoServerCreateInviteCode.InputSchema, opts?: ComAtprotoServerCreateInviteCode.CallOptions, ): Promise { - return this._service.xrpc + return this._client .call('com.atproto.server.createInviteCode', opts?.qp, data, opts) .catch((e) => { throw ComAtprotoServerCreateInviteCode.toKnownErr(e) @@ -939,7 +922,7 @@ export class ComAtprotoServerNS { data?: ComAtprotoServerCreateInviteCodes.InputSchema, opts?: ComAtprotoServerCreateInviteCodes.CallOptions, ): Promise { - return this._service.xrpc + return this._client .call('com.atproto.server.createInviteCodes', opts?.qp, data, opts) .catch((e) => { throw ComAtprotoServerCreateInviteCodes.toKnownErr(e) @@ -950,7 +933,7 @@ export class ComAtprotoServerNS { data?: ComAtprotoServerCreateSession.InputSchema, opts?: ComAtprotoServerCreateSession.CallOptions, ): Promise { - return this._service.xrpc + return this._client .call('com.atproto.server.createSession', opts?.qp, data, opts) .catch((e) => { throw ComAtprotoServerCreateSession.toKnownErr(e) @@ -961,7 +944,7 @@ export class ComAtprotoServerNS { data?: ComAtprotoServerDeactivateAccount.InputSchema, opts?: ComAtprotoServerDeactivateAccount.CallOptions, ): Promise { - return this._service.xrpc + return this._client .call('com.atproto.server.deactivateAccount', opts?.qp, data, opts) .catch((e) => { throw ComAtprotoServerDeactivateAccount.toKnownErr(e) @@ -972,7 +955,7 @@ export class ComAtprotoServerNS { data?: ComAtprotoServerDeleteAccount.InputSchema, opts?: ComAtprotoServerDeleteAccount.CallOptions, ): Promise { - return this._service.xrpc + return this._client .call('com.atproto.server.deleteAccount', opts?.qp, data, opts) .catch((e) => { throw ComAtprotoServerDeleteAccount.toKnownErr(e) @@ -983,7 +966,7 @@ export class ComAtprotoServerNS { data?: ComAtprotoServerDeleteSession.InputSchema, opts?: ComAtprotoServerDeleteSession.CallOptions, ): Promise { - return this._service.xrpc + return this._client .call('com.atproto.server.deleteSession', opts?.qp, data, opts) .catch((e) => { throw ComAtprotoServerDeleteSession.toKnownErr(e) @@ -994,7 +977,7 @@ export class ComAtprotoServerNS { params?: ComAtprotoServerDescribeServer.QueryParams, opts?: ComAtprotoServerDescribeServer.CallOptions, ): Promise { - return this._service.xrpc + return this._client .call('com.atproto.server.describeServer', params, undefined, opts) .catch((e) => { throw ComAtprotoServerDescribeServer.toKnownErr(e) @@ -1005,7 +988,7 @@ export class ComAtprotoServerNS { params?: ComAtprotoServerGetAccountInviteCodes.QueryParams, opts?: ComAtprotoServerGetAccountInviteCodes.CallOptions, ): Promise { - return this._service.xrpc + return this._client .call('com.atproto.server.getAccountInviteCodes', params, undefined, opts) .catch((e) => { throw ComAtprotoServerGetAccountInviteCodes.toKnownErr(e) @@ -1016,7 +999,7 @@ export class ComAtprotoServerNS { params?: ComAtprotoServerGetServiceAuth.QueryParams, opts?: ComAtprotoServerGetServiceAuth.CallOptions, ): Promise { - return this._service.xrpc + return this._client .call('com.atproto.server.getServiceAuth', params, undefined, opts) .catch((e) => { throw ComAtprotoServerGetServiceAuth.toKnownErr(e) @@ -1027,7 +1010,7 @@ export class ComAtprotoServerNS { params?: ComAtprotoServerGetSession.QueryParams, opts?: ComAtprotoServerGetSession.CallOptions, ): Promise { - return this._service.xrpc + return this._client .call('com.atproto.server.getSession', params, undefined, opts) .catch((e) => { throw ComAtprotoServerGetSession.toKnownErr(e) @@ -1038,7 +1021,7 @@ export class ComAtprotoServerNS { params?: ComAtprotoServerListAppPasswords.QueryParams, opts?: ComAtprotoServerListAppPasswords.CallOptions, ): Promise { - return this._service.xrpc + return this._client .call('com.atproto.server.listAppPasswords', params, undefined, opts) .catch((e) => { throw ComAtprotoServerListAppPasswords.toKnownErr(e) @@ -1049,7 +1032,7 @@ export class ComAtprotoServerNS { data?: ComAtprotoServerRefreshSession.InputSchema, opts?: ComAtprotoServerRefreshSession.CallOptions, ): Promise { - return this._service.xrpc + return this._client .call('com.atproto.server.refreshSession', opts?.qp, data, opts) .catch((e) => { throw ComAtprotoServerRefreshSession.toKnownErr(e) @@ -1060,7 +1043,7 @@ export class ComAtprotoServerNS { data?: ComAtprotoServerRequestAccountDelete.InputSchema, opts?: ComAtprotoServerRequestAccountDelete.CallOptions, ): Promise { - return this._service.xrpc + return this._client .call('com.atproto.server.requestAccountDelete', opts?.qp, data, opts) .catch((e) => { throw ComAtprotoServerRequestAccountDelete.toKnownErr(e) @@ -1071,7 +1054,7 @@ export class ComAtprotoServerNS { data?: ComAtprotoServerRequestEmailConfirmation.InputSchema, opts?: ComAtprotoServerRequestEmailConfirmation.CallOptions, ): Promise { - return this._service.xrpc + return this._client .call('com.atproto.server.requestEmailConfirmation', opts?.qp, data, opts) .catch((e) => { throw ComAtprotoServerRequestEmailConfirmation.toKnownErr(e) @@ -1082,7 +1065,7 @@ export class ComAtprotoServerNS { data?: ComAtprotoServerRequestEmailUpdate.InputSchema, opts?: ComAtprotoServerRequestEmailUpdate.CallOptions, ): Promise { - return this._service.xrpc + return this._client .call('com.atproto.server.requestEmailUpdate', opts?.qp, data, opts) .catch((e) => { throw ComAtprotoServerRequestEmailUpdate.toKnownErr(e) @@ -1093,7 +1076,7 @@ export class ComAtprotoServerNS { data?: ComAtprotoServerRequestPasswordReset.InputSchema, opts?: ComAtprotoServerRequestPasswordReset.CallOptions, ): Promise { - return this._service.xrpc + return this._client .call('com.atproto.server.requestPasswordReset', opts?.qp, data, opts) .catch((e) => { throw ComAtprotoServerRequestPasswordReset.toKnownErr(e) @@ -1104,7 +1087,7 @@ export class ComAtprotoServerNS { data?: ComAtprotoServerReserveSigningKey.InputSchema, opts?: ComAtprotoServerReserveSigningKey.CallOptions, ): Promise { - return this._service.xrpc + return this._client .call('com.atproto.server.reserveSigningKey', opts?.qp, data, opts) .catch((e) => { throw ComAtprotoServerReserveSigningKey.toKnownErr(e) @@ -1115,7 +1098,7 @@ export class ComAtprotoServerNS { data?: ComAtprotoServerResetPassword.InputSchema, opts?: ComAtprotoServerResetPassword.CallOptions, ): Promise { - return this._service.xrpc + return this._client .call('com.atproto.server.resetPassword', opts?.qp, data, opts) .catch((e) => { throw ComAtprotoServerResetPassword.toKnownErr(e) @@ -1126,7 +1109,7 @@ export class ComAtprotoServerNS { data?: ComAtprotoServerRevokeAppPassword.InputSchema, opts?: ComAtprotoServerRevokeAppPassword.CallOptions, ): Promise { - return this._service.xrpc + return this._client .call('com.atproto.server.revokeAppPassword', opts?.qp, data, opts) .catch((e) => { throw ComAtprotoServerRevokeAppPassword.toKnownErr(e) @@ -1137,7 +1120,7 @@ export class ComAtprotoServerNS { data?: ComAtprotoServerUpdateEmail.InputSchema, opts?: ComAtprotoServerUpdateEmail.CallOptions, ): Promise { - return this._service.xrpc + return this._client .call('com.atproto.server.updateEmail', opts?.qp, data, opts) .catch((e) => { throw ComAtprotoServerUpdateEmail.toKnownErr(e) @@ -1146,17 +1129,17 @@ export class ComAtprotoServerNS { } export class ComAtprotoSyncNS { - _service: AtpServiceClient + _client: XrpcClient - constructor(service: AtpServiceClient) { - this._service = service + constructor(client: XrpcClient) { + this._client = client } getBlob( params?: ComAtprotoSyncGetBlob.QueryParams, opts?: ComAtprotoSyncGetBlob.CallOptions, ): Promise { - return this._service.xrpc + return this._client .call('com.atproto.sync.getBlob', params, undefined, opts) .catch((e) => { throw ComAtprotoSyncGetBlob.toKnownErr(e) @@ -1167,7 +1150,7 @@ export class ComAtprotoSyncNS { params?: ComAtprotoSyncGetBlocks.QueryParams, opts?: ComAtprotoSyncGetBlocks.CallOptions, ): Promise { - return this._service.xrpc + return this._client .call('com.atproto.sync.getBlocks', params, undefined, opts) .catch((e) => { throw ComAtprotoSyncGetBlocks.toKnownErr(e) @@ -1178,7 +1161,7 @@ export class ComAtprotoSyncNS { params?: ComAtprotoSyncGetCheckout.QueryParams, opts?: ComAtprotoSyncGetCheckout.CallOptions, ): Promise { - return this._service.xrpc + return this._client .call('com.atproto.sync.getCheckout', params, undefined, opts) .catch((e) => { throw ComAtprotoSyncGetCheckout.toKnownErr(e) @@ -1189,7 +1172,7 @@ export class ComAtprotoSyncNS { params?: ComAtprotoSyncGetHead.QueryParams, opts?: ComAtprotoSyncGetHead.CallOptions, ): Promise { - return this._service.xrpc + return this._client .call('com.atproto.sync.getHead', params, undefined, opts) .catch((e) => { throw ComAtprotoSyncGetHead.toKnownErr(e) @@ -1200,7 +1183,7 @@ export class ComAtprotoSyncNS { params?: ComAtprotoSyncGetLatestCommit.QueryParams, opts?: ComAtprotoSyncGetLatestCommit.CallOptions, ): Promise { - return this._service.xrpc + return this._client .call('com.atproto.sync.getLatestCommit', params, undefined, opts) .catch((e) => { throw ComAtprotoSyncGetLatestCommit.toKnownErr(e) @@ -1211,7 +1194,7 @@ export class ComAtprotoSyncNS { params?: ComAtprotoSyncGetRecord.QueryParams, opts?: ComAtprotoSyncGetRecord.CallOptions, ): Promise { - return this._service.xrpc + return this._client .call('com.atproto.sync.getRecord', params, undefined, opts) .catch((e) => { throw ComAtprotoSyncGetRecord.toKnownErr(e) @@ -1222,7 +1205,7 @@ export class ComAtprotoSyncNS { params?: ComAtprotoSyncGetRepo.QueryParams, opts?: ComAtprotoSyncGetRepo.CallOptions, ): Promise { - return this._service.xrpc + return this._client .call('com.atproto.sync.getRepo', params, undefined, opts) .catch((e) => { throw ComAtprotoSyncGetRepo.toKnownErr(e) @@ -1233,7 +1216,7 @@ export class ComAtprotoSyncNS { params?: ComAtprotoSyncListBlobs.QueryParams, opts?: ComAtprotoSyncListBlobs.CallOptions, ): Promise { - return this._service.xrpc + return this._client .call('com.atproto.sync.listBlobs', params, undefined, opts) .catch((e) => { throw ComAtprotoSyncListBlobs.toKnownErr(e) @@ -1244,7 +1227,7 @@ export class ComAtprotoSyncNS { params?: ComAtprotoSyncListRepos.QueryParams, opts?: ComAtprotoSyncListRepos.CallOptions, ): Promise { - return this._service.xrpc + return this._client .call('com.atproto.sync.listRepos', params, undefined, opts) .catch((e) => { throw ComAtprotoSyncListRepos.toKnownErr(e) @@ -1255,7 +1238,7 @@ export class ComAtprotoSyncNS { data?: ComAtprotoSyncNotifyOfUpdate.InputSchema, opts?: ComAtprotoSyncNotifyOfUpdate.CallOptions, ): Promise { - return this._service.xrpc + return this._client .call('com.atproto.sync.notifyOfUpdate', opts?.qp, data, opts) .catch((e) => { throw ComAtprotoSyncNotifyOfUpdate.toKnownErr(e) @@ -1266,7 +1249,7 @@ export class ComAtprotoSyncNS { data?: ComAtprotoSyncRequestCrawl.InputSchema, opts?: ComAtprotoSyncRequestCrawl.CallOptions, ): Promise { - return this._service.xrpc + return this._client .call('com.atproto.sync.requestCrawl', opts?.qp, data, opts) .catch((e) => { throw ComAtprotoSyncRequestCrawl.toKnownErr(e) @@ -1275,17 +1258,17 @@ export class ComAtprotoSyncNS { } export class ComAtprotoTempNS { - _service: AtpServiceClient + _client: XrpcClient - constructor(service: AtpServiceClient) { - this._service = service + constructor(client: XrpcClient) { + this._client = client } checkSignupQueue( params?: ComAtprotoTempCheckSignupQueue.QueryParams, opts?: ComAtprotoTempCheckSignupQueue.CallOptions, ): Promise { - return this._service.xrpc + return this._client .call('com.atproto.temp.checkSignupQueue', params, undefined, opts) .catch((e) => { throw ComAtprotoTempCheckSignupQueue.toKnownErr(e) @@ -1296,7 +1279,7 @@ export class ComAtprotoTempNS { params?: ComAtprotoTempFetchLabels.QueryParams, opts?: ComAtprotoTempFetchLabels.CallOptions, ): Promise { - return this._service.xrpc + return this._client .call('com.atproto.temp.fetchLabels', params, undefined, opts) .catch((e) => { throw ComAtprotoTempFetchLabels.toKnownErr(e) @@ -1307,7 +1290,7 @@ export class ComAtprotoTempNS { data?: ComAtprotoTempRequestPhoneVerification.InputSchema, opts?: ComAtprotoTempRequestPhoneVerification.CallOptions, ): Promise { - return this._service.xrpc + return this._client .call('com.atproto.temp.requestPhoneVerification', opts?.qp, data, opts) .catch((e) => { throw ComAtprotoTempRequestPhoneVerification.toKnownErr(e) @@ -1316,17 +1299,17 @@ export class ComAtprotoTempNS { } export class AppNS { - _service: AtpServiceClient + _client: XrpcClient bsky: AppBskyNS - constructor(service: AtpServiceClient) { - this._service = service - this.bsky = new AppBskyNS(service) + constructor(client: XrpcClient) { + this._client = client + this.bsky = new AppBskyNS(client) } } export class AppBskyNS { - _service: AtpServiceClient + _client: XrpcClient actor: AppBskyActorNS embed: AppBskyEmbedNS feed: AppBskyFeedNS @@ -1336,33 +1319,33 @@ export class AppBskyNS { richtext: AppBskyRichtextNS unspecced: AppBskyUnspeccedNS - constructor(service: AtpServiceClient) { - this._service = service - this.actor = new AppBskyActorNS(service) - this.embed = new AppBskyEmbedNS(service) - this.feed = new AppBskyFeedNS(service) - this.graph = new AppBskyGraphNS(service) - this.labeler = new AppBskyLabelerNS(service) - this.notification = new AppBskyNotificationNS(service) - this.richtext = new AppBskyRichtextNS(service) - this.unspecced = new AppBskyUnspeccedNS(service) + constructor(client: XrpcClient) { + this._client = client + this.actor = new AppBskyActorNS(client) + this.embed = new AppBskyEmbedNS(client) + this.feed = new AppBskyFeedNS(client) + this.graph = new AppBskyGraphNS(client) + this.labeler = new AppBskyLabelerNS(client) + this.notification = new AppBskyNotificationNS(client) + this.richtext = new AppBskyRichtextNS(client) + this.unspecced = new AppBskyUnspeccedNS(client) } } export class AppBskyActorNS { - _service: AtpServiceClient + _client: XrpcClient profile: ProfileRecord - constructor(service: AtpServiceClient) { - this._service = service - this.profile = new ProfileRecord(service) + constructor(client: XrpcClient) { + this._client = client + this.profile = new ProfileRecord(client) } getPreferences( params?: AppBskyActorGetPreferences.QueryParams, opts?: AppBskyActorGetPreferences.CallOptions, ): Promise { - return this._service.xrpc + return this._client .call('app.bsky.actor.getPreferences', params, undefined, opts) .catch((e) => { throw AppBskyActorGetPreferences.toKnownErr(e) @@ -1373,7 +1356,7 @@ export class AppBskyActorNS { params?: AppBskyActorGetProfile.QueryParams, opts?: AppBskyActorGetProfile.CallOptions, ): Promise { - return this._service.xrpc + return this._client .call('app.bsky.actor.getProfile', params, undefined, opts) .catch((e) => { throw AppBskyActorGetProfile.toKnownErr(e) @@ -1384,7 +1367,7 @@ export class AppBskyActorNS { params?: AppBskyActorGetProfiles.QueryParams, opts?: AppBskyActorGetProfiles.CallOptions, ): Promise { - return this._service.xrpc + return this._client .call('app.bsky.actor.getProfiles', params, undefined, opts) .catch((e) => { throw AppBskyActorGetProfiles.toKnownErr(e) @@ -1395,7 +1378,7 @@ export class AppBskyActorNS { params?: AppBskyActorGetSuggestions.QueryParams, opts?: AppBskyActorGetSuggestions.CallOptions, ): Promise { - return this._service.xrpc + return this._client .call('app.bsky.actor.getSuggestions', params, undefined, opts) .catch((e) => { throw AppBskyActorGetSuggestions.toKnownErr(e) @@ -1406,7 +1389,7 @@ export class AppBskyActorNS { data?: AppBskyActorPutPreferences.InputSchema, opts?: AppBskyActorPutPreferences.CallOptions, ): Promise { - return this._service.xrpc + return this._client .call('app.bsky.actor.putPreferences', opts?.qp, data, opts) .catch((e) => { throw AppBskyActorPutPreferences.toKnownErr(e) @@ -1417,7 +1400,7 @@ export class AppBskyActorNS { params?: AppBskyActorSearchActors.QueryParams, opts?: AppBskyActorSearchActors.CallOptions, ): Promise { - return this._service.xrpc + return this._client .call('app.bsky.actor.searchActors', params, undefined, opts) .catch((e) => { throw AppBskyActorSearchActors.toKnownErr(e) @@ -1428,7 +1411,7 @@ export class AppBskyActorNS { params?: AppBskyActorSearchActorsTypeahead.QueryParams, opts?: AppBskyActorSearchActorsTypeahead.CallOptions, ): Promise { - return this._service.xrpc + return this._client .call('app.bsky.actor.searchActorsTypeahead', params, undefined, opts) .catch((e) => { throw AppBskyActorSearchActorsTypeahead.toKnownErr(e) @@ -1437,10 +1420,10 @@ export class AppBskyActorNS { } export class ProfileRecord { - _service: AtpServiceClient + _client: XrpcClient - constructor(service: AtpServiceClient) { - this._service = service + constructor(client: XrpcClient) { + this._client = client } async list( @@ -1449,7 +1432,7 @@ export class ProfileRecord { cursor?: string records: { uri: string; value: AppBskyActorProfile.Record }[] }> { - const res = await this._service.xrpc.call('com.atproto.repo.listRecords', { + const res = await this._client.call('com.atproto.repo.listRecords', { collection: 'app.bsky.actor.profile', ...params, }) @@ -1459,7 +1442,7 @@ export class ProfileRecord { async get( params: Omit, ): Promise<{ uri: string; cid: string; value: AppBskyActorProfile.Record }> { - const res = await this._service.xrpc.call('com.atproto.repo.getRecord', { + const res = await this._client.call('com.atproto.repo.getRecord', { collection: 'app.bsky.actor.profile', ...params, }) @@ -1475,7 +1458,7 @@ export class ProfileRecord { headers?: Record, ): Promise<{ uri: string; cid: string }> { record.$type = 'app.bsky.actor.profile' - const res = await this._service.xrpc.call( + const res = await this._client.call( 'com.atproto.repo.createRecord', undefined, { collection: 'app.bsky.actor.profile', rkey: 'self', ...params, record }, @@ -1488,7 +1471,7 @@ export class ProfileRecord { params: Omit, headers?: Record, ): Promise { - await this._service.xrpc.call( + await this._client.call( 'com.atproto.repo.deleteRecord', undefined, { collection: 'app.bsky.actor.profile', ...params }, @@ -1498,35 +1481,35 @@ export class ProfileRecord { } export class AppBskyEmbedNS { - _service: AtpServiceClient + _client: XrpcClient - constructor(service: AtpServiceClient) { - this._service = service + constructor(client: XrpcClient) { + this._client = client } } export class AppBskyFeedNS { - _service: AtpServiceClient + _client: XrpcClient generator: GeneratorRecord like: LikeRecord post: PostRecord repost: RepostRecord threadgate: ThreadgateRecord - constructor(service: AtpServiceClient) { - this._service = service - this.generator = new GeneratorRecord(service) - this.like = new LikeRecord(service) - this.post = new PostRecord(service) - this.repost = new RepostRecord(service) - this.threadgate = new ThreadgateRecord(service) + constructor(client: XrpcClient) { + this._client = client + this.generator = new GeneratorRecord(client) + this.like = new LikeRecord(client) + this.post = new PostRecord(client) + this.repost = new RepostRecord(client) + this.threadgate = new ThreadgateRecord(client) } describeFeedGenerator( params?: AppBskyFeedDescribeFeedGenerator.QueryParams, opts?: AppBskyFeedDescribeFeedGenerator.CallOptions, ): Promise { - return this._service.xrpc + return this._client .call('app.bsky.feed.describeFeedGenerator', params, undefined, opts) .catch((e) => { throw AppBskyFeedDescribeFeedGenerator.toKnownErr(e) @@ -1537,7 +1520,7 @@ export class AppBskyFeedNS { params?: AppBskyFeedGetActorFeeds.QueryParams, opts?: AppBskyFeedGetActorFeeds.CallOptions, ): Promise { - return this._service.xrpc + return this._client .call('app.bsky.feed.getActorFeeds', params, undefined, opts) .catch((e) => { throw AppBskyFeedGetActorFeeds.toKnownErr(e) @@ -1548,7 +1531,7 @@ export class AppBskyFeedNS { params?: AppBskyFeedGetActorLikes.QueryParams, opts?: AppBskyFeedGetActorLikes.CallOptions, ): Promise { - return this._service.xrpc + return this._client .call('app.bsky.feed.getActorLikes', params, undefined, opts) .catch((e) => { throw AppBskyFeedGetActorLikes.toKnownErr(e) @@ -1559,7 +1542,7 @@ export class AppBskyFeedNS { params?: AppBskyFeedGetAuthorFeed.QueryParams, opts?: AppBskyFeedGetAuthorFeed.CallOptions, ): Promise { - return this._service.xrpc + return this._client .call('app.bsky.feed.getAuthorFeed', params, undefined, opts) .catch((e) => { throw AppBskyFeedGetAuthorFeed.toKnownErr(e) @@ -1570,7 +1553,7 @@ export class AppBskyFeedNS { params?: AppBskyFeedGetFeed.QueryParams, opts?: AppBskyFeedGetFeed.CallOptions, ): Promise { - return this._service.xrpc + return this._client .call('app.bsky.feed.getFeed', params, undefined, opts) .catch((e) => { throw AppBskyFeedGetFeed.toKnownErr(e) @@ -1581,7 +1564,7 @@ export class AppBskyFeedNS { params?: AppBskyFeedGetFeedGenerator.QueryParams, opts?: AppBskyFeedGetFeedGenerator.CallOptions, ): Promise { - return this._service.xrpc + return this._client .call('app.bsky.feed.getFeedGenerator', params, undefined, opts) .catch((e) => { throw AppBskyFeedGetFeedGenerator.toKnownErr(e) @@ -1592,7 +1575,7 @@ export class AppBskyFeedNS { params?: AppBskyFeedGetFeedGenerators.QueryParams, opts?: AppBskyFeedGetFeedGenerators.CallOptions, ): Promise { - return this._service.xrpc + return this._client .call('app.bsky.feed.getFeedGenerators', params, undefined, opts) .catch((e) => { throw AppBskyFeedGetFeedGenerators.toKnownErr(e) @@ -1603,7 +1586,7 @@ export class AppBskyFeedNS { params?: AppBskyFeedGetFeedSkeleton.QueryParams, opts?: AppBskyFeedGetFeedSkeleton.CallOptions, ): Promise { - return this._service.xrpc + return this._client .call('app.bsky.feed.getFeedSkeleton', params, undefined, opts) .catch((e) => { throw AppBskyFeedGetFeedSkeleton.toKnownErr(e) @@ -1614,7 +1597,7 @@ export class AppBskyFeedNS { params?: AppBskyFeedGetLikes.QueryParams, opts?: AppBskyFeedGetLikes.CallOptions, ): Promise { - return this._service.xrpc + return this._client .call('app.bsky.feed.getLikes', params, undefined, opts) .catch((e) => { throw AppBskyFeedGetLikes.toKnownErr(e) @@ -1625,7 +1608,7 @@ export class AppBskyFeedNS { params?: AppBskyFeedGetListFeed.QueryParams, opts?: AppBskyFeedGetListFeed.CallOptions, ): Promise { - return this._service.xrpc + return this._client .call('app.bsky.feed.getListFeed', params, undefined, opts) .catch((e) => { throw AppBskyFeedGetListFeed.toKnownErr(e) @@ -1636,7 +1619,7 @@ export class AppBskyFeedNS { params?: AppBskyFeedGetPostThread.QueryParams, opts?: AppBskyFeedGetPostThread.CallOptions, ): Promise { - return this._service.xrpc + return this._client .call('app.bsky.feed.getPostThread', params, undefined, opts) .catch((e) => { throw AppBskyFeedGetPostThread.toKnownErr(e) @@ -1647,7 +1630,7 @@ export class AppBskyFeedNS { params?: AppBskyFeedGetPosts.QueryParams, opts?: AppBskyFeedGetPosts.CallOptions, ): Promise { - return this._service.xrpc + return this._client .call('app.bsky.feed.getPosts', params, undefined, opts) .catch((e) => { throw AppBskyFeedGetPosts.toKnownErr(e) @@ -1658,7 +1641,7 @@ export class AppBskyFeedNS { params?: AppBskyFeedGetRepostedBy.QueryParams, opts?: AppBskyFeedGetRepostedBy.CallOptions, ): Promise { - return this._service.xrpc + return this._client .call('app.bsky.feed.getRepostedBy', params, undefined, opts) .catch((e) => { throw AppBskyFeedGetRepostedBy.toKnownErr(e) @@ -1669,7 +1652,7 @@ export class AppBskyFeedNS { params?: AppBskyFeedGetSuggestedFeeds.QueryParams, opts?: AppBskyFeedGetSuggestedFeeds.CallOptions, ): Promise { - return this._service.xrpc + return this._client .call('app.bsky.feed.getSuggestedFeeds', params, undefined, opts) .catch((e) => { throw AppBskyFeedGetSuggestedFeeds.toKnownErr(e) @@ -1680,7 +1663,7 @@ export class AppBskyFeedNS { params?: AppBskyFeedGetTimeline.QueryParams, opts?: AppBskyFeedGetTimeline.CallOptions, ): Promise { - return this._service.xrpc + return this._client .call('app.bsky.feed.getTimeline', params, undefined, opts) .catch((e) => { throw AppBskyFeedGetTimeline.toKnownErr(e) @@ -1691,7 +1674,7 @@ export class AppBskyFeedNS { params?: AppBskyFeedSearchPosts.QueryParams, opts?: AppBskyFeedSearchPosts.CallOptions, ): Promise { - return this._service.xrpc + return this._client .call('app.bsky.feed.searchPosts', params, undefined, opts) .catch((e) => { throw AppBskyFeedSearchPosts.toKnownErr(e) @@ -1702,7 +1685,7 @@ export class AppBskyFeedNS { data?: AppBskyFeedSendInteractions.InputSchema, opts?: AppBskyFeedSendInteractions.CallOptions, ): Promise { - return this._service.xrpc + return this._client .call('app.bsky.feed.sendInteractions', opts?.qp, data, opts) .catch((e) => { throw AppBskyFeedSendInteractions.toKnownErr(e) @@ -1711,10 +1694,10 @@ export class AppBskyFeedNS { } export class GeneratorRecord { - _service: AtpServiceClient + _client: XrpcClient - constructor(service: AtpServiceClient) { - this._service = service + constructor(client: XrpcClient) { + this._client = client } async list( @@ -1723,7 +1706,7 @@ export class GeneratorRecord { cursor?: string records: { uri: string; value: AppBskyFeedGenerator.Record }[] }> { - const res = await this._service.xrpc.call('com.atproto.repo.listRecords', { + const res = await this._client.call('com.atproto.repo.listRecords', { collection: 'app.bsky.feed.generator', ...params, }) @@ -1733,7 +1716,7 @@ export class GeneratorRecord { async get( params: Omit, ): Promise<{ uri: string; cid: string; value: AppBskyFeedGenerator.Record }> { - const res = await this._service.xrpc.call('com.atproto.repo.getRecord', { + const res = await this._client.call('com.atproto.repo.getRecord', { collection: 'app.bsky.feed.generator', ...params, }) @@ -1749,7 +1732,7 @@ export class GeneratorRecord { headers?: Record, ): Promise<{ uri: string; cid: string }> { record.$type = 'app.bsky.feed.generator' - const res = await this._service.xrpc.call( + const res = await this._client.call( 'com.atproto.repo.createRecord', undefined, { collection: 'app.bsky.feed.generator', ...params, record }, @@ -1762,7 +1745,7 @@ export class GeneratorRecord { params: Omit, headers?: Record, ): Promise { - await this._service.xrpc.call( + await this._client.call( 'com.atproto.repo.deleteRecord', undefined, { collection: 'app.bsky.feed.generator', ...params }, @@ -1772,10 +1755,10 @@ export class GeneratorRecord { } export class LikeRecord { - _service: AtpServiceClient + _client: XrpcClient - constructor(service: AtpServiceClient) { - this._service = service + constructor(client: XrpcClient) { + this._client = client } async list( @@ -1784,7 +1767,7 @@ export class LikeRecord { cursor?: string records: { uri: string; value: AppBskyFeedLike.Record }[] }> { - const res = await this._service.xrpc.call('com.atproto.repo.listRecords', { + const res = await this._client.call('com.atproto.repo.listRecords', { collection: 'app.bsky.feed.like', ...params, }) @@ -1794,7 +1777,7 @@ export class LikeRecord { async get( params: Omit, ): Promise<{ uri: string; cid: string; value: AppBskyFeedLike.Record }> { - const res = await this._service.xrpc.call('com.atproto.repo.getRecord', { + const res = await this._client.call('com.atproto.repo.getRecord', { collection: 'app.bsky.feed.like', ...params, }) @@ -1810,7 +1793,7 @@ export class LikeRecord { headers?: Record, ): Promise<{ uri: string; cid: string }> { record.$type = 'app.bsky.feed.like' - const res = await this._service.xrpc.call( + const res = await this._client.call( 'com.atproto.repo.createRecord', undefined, { collection: 'app.bsky.feed.like', ...params, record }, @@ -1823,7 +1806,7 @@ export class LikeRecord { params: Omit, headers?: Record, ): Promise { - await this._service.xrpc.call( + await this._client.call( 'com.atproto.repo.deleteRecord', undefined, { collection: 'app.bsky.feed.like', ...params }, @@ -1833,10 +1816,10 @@ export class LikeRecord { } export class PostRecord { - _service: AtpServiceClient + _client: XrpcClient - constructor(service: AtpServiceClient) { - this._service = service + constructor(client: XrpcClient) { + this._client = client } async list( @@ -1845,7 +1828,7 @@ export class PostRecord { cursor?: string records: { uri: string; value: AppBskyFeedPost.Record }[] }> { - const res = await this._service.xrpc.call('com.atproto.repo.listRecords', { + const res = await this._client.call('com.atproto.repo.listRecords', { collection: 'app.bsky.feed.post', ...params, }) @@ -1855,7 +1838,7 @@ export class PostRecord { async get( params: Omit, ): Promise<{ uri: string; cid: string; value: AppBskyFeedPost.Record }> { - const res = await this._service.xrpc.call('com.atproto.repo.getRecord', { + const res = await this._client.call('com.atproto.repo.getRecord', { collection: 'app.bsky.feed.post', ...params, }) @@ -1871,7 +1854,7 @@ export class PostRecord { headers?: Record, ): Promise<{ uri: string; cid: string }> { record.$type = 'app.bsky.feed.post' - const res = await this._service.xrpc.call( + const res = await this._client.call( 'com.atproto.repo.createRecord', undefined, { collection: 'app.bsky.feed.post', ...params, record }, @@ -1884,7 +1867,7 @@ export class PostRecord { params: Omit, headers?: Record, ): Promise { - await this._service.xrpc.call( + await this._client.call( 'com.atproto.repo.deleteRecord', undefined, { collection: 'app.bsky.feed.post', ...params }, @@ -1894,10 +1877,10 @@ export class PostRecord { } export class RepostRecord { - _service: AtpServiceClient + _client: XrpcClient - constructor(service: AtpServiceClient) { - this._service = service + constructor(client: XrpcClient) { + this._client = client } async list( @@ -1906,7 +1889,7 @@ export class RepostRecord { cursor?: string records: { uri: string; value: AppBskyFeedRepost.Record }[] }> { - const res = await this._service.xrpc.call('com.atproto.repo.listRecords', { + const res = await this._client.call('com.atproto.repo.listRecords', { collection: 'app.bsky.feed.repost', ...params, }) @@ -1916,7 +1899,7 @@ export class RepostRecord { async get( params: Omit, ): Promise<{ uri: string; cid: string; value: AppBskyFeedRepost.Record }> { - const res = await this._service.xrpc.call('com.atproto.repo.getRecord', { + const res = await this._client.call('com.atproto.repo.getRecord', { collection: 'app.bsky.feed.repost', ...params, }) @@ -1932,7 +1915,7 @@ export class RepostRecord { headers?: Record, ): Promise<{ uri: string; cid: string }> { record.$type = 'app.bsky.feed.repost' - const res = await this._service.xrpc.call( + const res = await this._client.call( 'com.atproto.repo.createRecord', undefined, { collection: 'app.bsky.feed.repost', ...params, record }, @@ -1945,7 +1928,7 @@ export class RepostRecord { params: Omit, headers?: Record, ): Promise { - await this._service.xrpc.call( + await this._client.call( 'com.atproto.repo.deleteRecord', undefined, { collection: 'app.bsky.feed.repost', ...params }, @@ -1955,10 +1938,10 @@ export class RepostRecord { } export class ThreadgateRecord { - _service: AtpServiceClient + _client: XrpcClient - constructor(service: AtpServiceClient) { - this._service = service + constructor(client: XrpcClient) { + this._client = client } async list( @@ -1967,7 +1950,7 @@ export class ThreadgateRecord { cursor?: string records: { uri: string; value: AppBskyFeedThreadgate.Record }[] }> { - const res = await this._service.xrpc.call('com.atproto.repo.listRecords', { + const res = await this._client.call('com.atproto.repo.listRecords', { collection: 'app.bsky.feed.threadgate', ...params, }) @@ -1981,7 +1964,7 @@ export class ThreadgateRecord { cid: string value: AppBskyFeedThreadgate.Record }> { - const res = await this._service.xrpc.call('com.atproto.repo.getRecord', { + const res = await this._client.call('com.atproto.repo.getRecord', { collection: 'app.bsky.feed.threadgate', ...params, }) @@ -1997,7 +1980,7 @@ export class ThreadgateRecord { headers?: Record, ): Promise<{ uri: string; cid: string }> { record.$type = 'app.bsky.feed.threadgate' - const res = await this._service.xrpc.call( + const res = await this._client.call( 'com.atproto.repo.createRecord', undefined, { collection: 'app.bsky.feed.threadgate', ...params, record }, @@ -2010,7 +1993,7 @@ export class ThreadgateRecord { params: Omit, headers?: Record, ): Promise { - await this._service.xrpc.call( + await this._client.call( 'com.atproto.repo.deleteRecord', undefined, { collection: 'app.bsky.feed.threadgate', ...params }, @@ -2020,27 +2003,27 @@ export class ThreadgateRecord { } export class AppBskyGraphNS { - _service: AtpServiceClient + _client: XrpcClient block: BlockRecord follow: FollowRecord list: ListRecord listblock: ListblockRecord listitem: ListitemRecord - constructor(service: AtpServiceClient) { - this._service = service - this.block = new BlockRecord(service) - this.follow = new FollowRecord(service) - this.list = new ListRecord(service) - this.listblock = new ListblockRecord(service) - this.listitem = new ListitemRecord(service) + constructor(client: XrpcClient) { + this._client = client + this.block = new BlockRecord(client) + this.follow = new FollowRecord(client) + this.list = new ListRecord(client) + this.listblock = new ListblockRecord(client) + this.listitem = new ListitemRecord(client) } getBlocks( params?: AppBskyGraphGetBlocks.QueryParams, opts?: AppBskyGraphGetBlocks.CallOptions, ): Promise { - return this._service.xrpc + return this._client .call('app.bsky.graph.getBlocks', params, undefined, opts) .catch((e) => { throw AppBskyGraphGetBlocks.toKnownErr(e) @@ -2051,7 +2034,7 @@ export class AppBskyGraphNS { params?: AppBskyGraphGetFollowers.QueryParams, opts?: AppBskyGraphGetFollowers.CallOptions, ): Promise { - return this._service.xrpc + return this._client .call('app.bsky.graph.getFollowers', params, undefined, opts) .catch((e) => { throw AppBskyGraphGetFollowers.toKnownErr(e) @@ -2062,7 +2045,7 @@ export class AppBskyGraphNS { params?: AppBskyGraphGetFollows.QueryParams, opts?: AppBskyGraphGetFollows.CallOptions, ): Promise { - return this._service.xrpc + return this._client .call('app.bsky.graph.getFollows', params, undefined, opts) .catch((e) => { throw AppBskyGraphGetFollows.toKnownErr(e) @@ -2073,7 +2056,7 @@ export class AppBskyGraphNS { params?: AppBskyGraphGetList.QueryParams, opts?: AppBskyGraphGetList.CallOptions, ): Promise { - return this._service.xrpc + return this._client .call('app.bsky.graph.getList', params, undefined, opts) .catch((e) => { throw AppBskyGraphGetList.toKnownErr(e) @@ -2084,7 +2067,7 @@ export class AppBskyGraphNS { params?: AppBskyGraphGetListBlocks.QueryParams, opts?: AppBskyGraphGetListBlocks.CallOptions, ): Promise { - return this._service.xrpc + return this._client .call('app.bsky.graph.getListBlocks', params, undefined, opts) .catch((e) => { throw AppBskyGraphGetListBlocks.toKnownErr(e) @@ -2095,7 +2078,7 @@ export class AppBskyGraphNS { params?: AppBskyGraphGetListMutes.QueryParams, opts?: AppBskyGraphGetListMutes.CallOptions, ): Promise { - return this._service.xrpc + return this._client .call('app.bsky.graph.getListMutes', params, undefined, opts) .catch((e) => { throw AppBskyGraphGetListMutes.toKnownErr(e) @@ -2106,7 +2089,7 @@ export class AppBskyGraphNS { params?: AppBskyGraphGetLists.QueryParams, opts?: AppBskyGraphGetLists.CallOptions, ): Promise { - return this._service.xrpc + return this._client .call('app.bsky.graph.getLists', params, undefined, opts) .catch((e) => { throw AppBskyGraphGetLists.toKnownErr(e) @@ -2117,7 +2100,7 @@ export class AppBskyGraphNS { params?: AppBskyGraphGetMutes.QueryParams, opts?: AppBskyGraphGetMutes.CallOptions, ): Promise { - return this._service.xrpc + return this._client .call('app.bsky.graph.getMutes', params, undefined, opts) .catch((e) => { throw AppBskyGraphGetMutes.toKnownErr(e) @@ -2128,7 +2111,7 @@ export class AppBskyGraphNS { params?: AppBskyGraphGetRelationships.QueryParams, opts?: AppBskyGraphGetRelationships.CallOptions, ): Promise { - return this._service.xrpc + return this._client .call('app.bsky.graph.getRelationships', params, undefined, opts) .catch((e) => { throw AppBskyGraphGetRelationships.toKnownErr(e) @@ -2139,7 +2122,7 @@ export class AppBskyGraphNS { params?: AppBskyGraphGetSuggestedFollowsByActor.QueryParams, opts?: AppBskyGraphGetSuggestedFollowsByActor.CallOptions, ): Promise { - return this._service.xrpc + return this._client .call( 'app.bsky.graph.getSuggestedFollowsByActor', params, @@ -2155,7 +2138,7 @@ export class AppBskyGraphNS { data?: AppBskyGraphMuteActor.InputSchema, opts?: AppBskyGraphMuteActor.CallOptions, ): Promise { - return this._service.xrpc + return this._client .call('app.bsky.graph.muteActor', opts?.qp, data, opts) .catch((e) => { throw AppBskyGraphMuteActor.toKnownErr(e) @@ -2166,7 +2149,7 @@ export class AppBskyGraphNS { data?: AppBskyGraphMuteActorList.InputSchema, opts?: AppBskyGraphMuteActorList.CallOptions, ): Promise { - return this._service.xrpc + return this._client .call('app.bsky.graph.muteActorList', opts?.qp, data, opts) .catch((e) => { throw AppBskyGraphMuteActorList.toKnownErr(e) @@ -2177,7 +2160,7 @@ export class AppBskyGraphNS { data?: AppBskyGraphUnmuteActor.InputSchema, opts?: AppBskyGraphUnmuteActor.CallOptions, ): Promise { - return this._service.xrpc + return this._client .call('app.bsky.graph.unmuteActor', opts?.qp, data, opts) .catch((e) => { throw AppBskyGraphUnmuteActor.toKnownErr(e) @@ -2188,7 +2171,7 @@ export class AppBskyGraphNS { data?: AppBskyGraphUnmuteActorList.InputSchema, opts?: AppBskyGraphUnmuteActorList.CallOptions, ): Promise { - return this._service.xrpc + return this._client .call('app.bsky.graph.unmuteActorList', opts?.qp, data, opts) .catch((e) => { throw AppBskyGraphUnmuteActorList.toKnownErr(e) @@ -2197,10 +2180,10 @@ export class AppBskyGraphNS { } export class BlockRecord { - _service: AtpServiceClient + _client: XrpcClient - constructor(service: AtpServiceClient) { - this._service = service + constructor(client: XrpcClient) { + this._client = client } async list( @@ -2209,7 +2192,7 @@ export class BlockRecord { cursor?: string records: { uri: string; value: AppBskyGraphBlock.Record }[] }> { - const res = await this._service.xrpc.call('com.atproto.repo.listRecords', { + const res = await this._client.call('com.atproto.repo.listRecords', { collection: 'app.bsky.graph.block', ...params, }) @@ -2219,7 +2202,7 @@ export class BlockRecord { async get( params: Omit, ): Promise<{ uri: string; cid: string; value: AppBskyGraphBlock.Record }> { - const res = await this._service.xrpc.call('com.atproto.repo.getRecord', { + const res = await this._client.call('com.atproto.repo.getRecord', { collection: 'app.bsky.graph.block', ...params, }) @@ -2235,7 +2218,7 @@ export class BlockRecord { headers?: Record, ): Promise<{ uri: string; cid: string }> { record.$type = 'app.bsky.graph.block' - const res = await this._service.xrpc.call( + const res = await this._client.call( 'com.atproto.repo.createRecord', undefined, { collection: 'app.bsky.graph.block', ...params, record }, @@ -2248,7 +2231,7 @@ export class BlockRecord { params: Omit, headers?: Record, ): Promise { - await this._service.xrpc.call( + await this._client.call( 'com.atproto.repo.deleteRecord', undefined, { collection: 'app.bsky.graph.block', ...params }, @@ -2258,10 +2241,10 @@ export class BlockRecord { } export class FollowRecord { - _service: AtpServiceClient + _client: XrpcClient - constructor(service: AtpServiceClient) { - this._service = service + constructor(client: XrpcClient) { + this._client = client } async list( @@ -2270,7 +2253,7 @@ export class FollowRecord { cursor?: string records: { uri: string; value: AppBskyGraphFollow.Record }[] }> { - const res = await this._service.xrpc.call('com.atproto.repo.listRecords', { + const res = await this._client.call('com.atproto.repo.listRecords', { collection: 'app.bsky.graph.follow', ...params, }) @@ -2280,7 +2263,7 @@ export class FollowRecord { async get( params: Omit, ): Promise<{ uri: string; cid: string; value: AppBskyGraphFollow.Record }> { - const res = await this._service.xrpc.call('com.atproto.repo.getRecord', { + const res = await this._client.call('com.atproto.repo.getRecord', { collection: 'app.bsky.graph.follow', ...params, }) @@ -2296,7 +2279,7 @@ export class FollowRecord { headers?: Record, ): Promise<{ uri: string; cid: string }> { record.$type = 'app.bsky.graph.follow' - const res = await this._service.xrpc.call( + const res = await this._client.call( 'com.atproto.repo.createRecord', undefined, { collection: 'app.bsky.graph.follow', ...params, record }, @@ -2309,7 +2292,7 @@ export class FollowRecord { params: Omit, headers?: Record, ): Promise { - await this._service.xrpc.call( + await this._client.call( 'com.atproto.repo.deleteRecord', undefined, { collection: 'app.bsky.graph.follow', ...params }, @@ -2319,10 +2302,10 @@ export class FollowRecord { } export class ListRecord { - _service: AtpServiceClient + _client: XrpcClient - constructor(service: AtpServiceClient) { - this._service = service + constructor(client: XrpcClient) { + this._client = client } async list( @@ -2331,7 +2314,7 @@ export class ListRecord { cursor?: string records: { uri: string; value: AppBskyGraphList.Record }[] }> { - const res = await this._service.xrpc.call('com.atproto.repo.listRecords', { + const res = await this._client.call('com.atproto.repo.listRecords', { collection: 'app.bsky.graph.list', ...params, }) @@ -2341,7 +2324,7 @@ export class ListRecord { async get( params: Omit, ): Promise<{ uri: string; cid: string; value: AppBskyGraphList.Record }> { - const res = await this._service.xrpc.call('com.atproto.repo.getRecord', { + const res = await this._client.call('com.atproto.repo.getRecord', { collection: 'app.bsky.graph.list', ...params, }) @@ -2357,7 +2340,7 @@ export class ListRecord { headers?: Record, ): Promise<{ uri: string; cid: string }> { record.$type = 'app.bsky.graph.list' - const res = await this._service.xrpc.call( + const res = await this._client.call( 'com.atproto.repo.createRecord', undefined, { collection: 'app.bsky.graph.list', ...params, record }, @@ -2370,7 +2353,7 @@ export class ListRecord { params: Omit, headers?: Record, ): Promise { - await this._service.xrpc.call( + await this._client.call( 'com.atproto.repo.deleteRecord', undefined, { collection: 'app.bsky.graph.list', ...params }, @@ -2380,10 +2363,10 @@ export class ListRecord { } export class ListblockRecord { - _service: AtpServiceClient + _client: XrpcClient - constructor(service: AtpServiceClient) { - this._service = service + constructor(client: XrpcClient) { + this._client = client } async list( @@ -2392,7 +2375,7 @@ export class ListblockRecord { cursor?: string records: { uri: string; value: AppBskyGraphListblock.Record }[] }> { - const res = await this._service.xrpc.call('com.atproto.repo.listRecords', { + const res = await this._client.call('com.atproto.repo.listRecords', { collection: 'app.bsky.graph.listblock', ...params, }) @@ -2406,7 +2389,7 @@ export class ListblockRecord { cid: string value: AppBskyGraphListblock.Record }> { - const res = await this._service.xrpc.call('com.atproto.repo.getRecord', { + const res = await this._client.call('com.atproto.repo.getRecord', { collection: 'app.bsky.graph.listblock', ...params, }) @@ -2422,7 +2405,7 @@ export class ListblockRecord { headers?: Record, ): Promise<{ uri: string; cid: string }> { record.$type = 'app.bsky.graph.listblock' - const res = await this._service.xrpc.call( + const res = await this._client.call( 'com.atproto.repo.createRecord', undefined, { collection: 'app.bsky.graph.listblock', ...params, record }, @@ -2435,7 +2418,7 @@ export class ListblockRecord { params: Omit, headers?: Record, ): Promise { - await this._service.xrpc.call( + await this._client.call( 'com.atproto.repo.deleteRecord', undefined, { collection: 'app.bsky.graph.listblock', ...params }, @@ -2445,10 +2428,10 @@ export class ListblockRecord { } export class ListitemRecord { - _service: AtpServiceClient + _client: XrpcClient - constructor(service: AtpServiceClient) { - this._service = service + constructor(client: XrpcClient) { + this._client = client } async list( @@ -2457,7 +2440,7 @@ export class ListitemRecord { cursor?: string records: { uri: string; value: AppBskyGraphListitem.Record }[] }> { - const res = await this._service.xrpc.call('com.atproto.repo.listRecords', { + const res = await this._client.call('com.atproto.repo.listRecords', { collection: 'app.bsky.graph.listitem', ...params, }) @@ -2467,7 +2450,7 @@ export class ListitemRecord { async get( params: Omit, ): Promise<{ uri: string; cid: string; value: AppBskyGraphListitem.Record }> { - const res = await this._service.xrpc.call('com.atproto.repo.getRecord', { + const res = await this._client.call('com.atproto.repo.getRecord', { collection: 'app.bsky.graph.listitem', ...params, }) @@ -2483,7 +2466,7 @@ export class ListitemRecord { headers?: Record, ): Promise<{ uri: string; cid: string }> { record.$type = 'app.bsky.graph.listitem' - const res = await this._service.xrpc.call( + const res = await this._client.call( 'com.atproto.repo.createRecord', undefined, { collection: 'app.bsky.graph.listitem', ...params, record }, @@ -2496,7 +2479,7 @@ export class ListitemRecord { params: Omit, headers?: Record, ): Promise { - await this._service.xrpc.call( + await this._client.call( 'com.atproto.repo.deleteRecord', undefined, { collection: 'app.bsky.graph.listitem', ...params }, @@ -2506,19 +2489,19 @@ export class ListitemRecord { } export class AppBskyLabelerNS { - _service: AtpServiceClient + _client: XrpcClient service: ServiceRecord - constructor(service: AtpServiceClient) { - this._service = service - this.service = new ServiceRecord(service) + constructor(client: XrpcClient) { + this._client = client + this.service = new ServiceRecord(client) } getServices( params?: AppBskyLabelerGetServices.QueryParams, opts?: AppBskyLabelerGetServices.CallOptions, ): Promise { - return this._service.xrpc + return this._client .call('app.bsky.labeler.getServices', params, undefined, opts) .catch((e) => { throw AppBskyLabelerGetServices.toKnownErr(e) @@ -2527,10 +2510,10 @@ export class AppBskyLabelerNS { } export class ServiceRecord { - _service: AtpServiceClient + _client: XrpcClient - constructor(service: AtpServiceClient) { - this._service = service + constructor(client: XrpcClient) { + this._client = client } async list( @@ -2539,7 +2522,7 @@ export class ServiceRecord { cursor?: string records: { uri: string; value: AppBskyLabelerService.Record }[] }> { - const res = await this._service.xrpc.call('com.atproto.repo.listRecords', { + const res = await this._client.call('com.atproto.repo.listRecords', { collection: 'app.bsky.labeler.service', ...params, }) @@ -2553,7 +2536,7 @@ export class ServiceRecord { cid: string value: AppBskyLabelerService.Record }> { - const res = await this._service.xrpc.call('com.atproto.repo.getRecord', { + const res = await this._client.call('com.atproto.repo.getRecord', { collection: 'app.bsky.labeler.service', ...params, }) @@ -2569,7 +2552,7 @@ export class ServiceRecord { headers?: Record, ): Promise<{ uri: string; cid: string }> { record.$type = 'app.bsky.labeler.service' - const res = await this._service.xrpc.call( + const res = await this._client.call( 'com.atproto.repo.createRecord', undefined, { @@ -2587,7 +2570,7 @@ export class ServiceRecord { params: Omit, headers?: Record, ): Promise { - await this._service.xrpc.call( + await this._client.call( 'com.atproto.repo.deleteRecord', undefined, { collection: 'app.bsky.labeler.service', ...params }, @@ -2597,17 +2580,17 @@ export class ServiceRecord { } export class AppBskyNotificationNS { - _service: AtpServiceClient + _client: XrpcClient - constructor(service: AtpServiceClient) { - this._service = service + constructor(client: XrpcClient) { + this._client = client } getUnreadCount( params?: AppBskyNotificationGetUnreadCount.QueryParams, opts?: AppBskyNotificationGetUnreadCount.CallOptions, ): Promise { - return this._service.xrpc + return this._client .call('app.bsky.notification.getUnreadCount', params, undefined, opts) .catch((e) => { throw AppBskyNotificationGetUnreadCount.toKnownErr(e) @@ -2618,7 +2601,7 @@ export class AppBskyNotificationNS { params?: AppBskyNotificationListNotifications.QueryParams, opts?: AppBskyNotificationListNotifications.CallOptions, ): Promise { - return this._service.xrpc + return this._client .call('app.bsky.notification.listNotifications', params, undefined, opts) .catch((e) => { throw AppBskyNotificationListNotifications.toKnownErr(e) @@ -2629,7 +2612,7 @@ export class AppBskyNotificationNS { data?: AppBskyNotificationRegisterPush.InputSchema, opts?: AppBskyNotificationRegisterPush.CallOptions, ): Promise { - return this._service.xrpc + return this._client .call('app.bsky.notification.registerPush', opts?.qp, data, opts) .catch((e) => { throw AppBskyNotificationRegisterPush.toKnownErr(e) @@ -2640,7 +2623,7 @@ export class AppBskyNotificationNS { data?: AppBskyNotificationUpdateSeen.InputSchema, opts?: AppBskyNotificationUpdateSeen.CallOptions, ): Promise { - return this._service.xrpc + return this._client .call('app.bsky.notification.updateSeen', opts?.qp, data, opts) .catch((e) => { throw AppBskyNotificationUpdateSeen.toKnownErr(e) @@ -2649,25 +2632,25 @@ export class AppBskyNotificationNS { } export class AppBskyRichtextNS { - _service: AtpServiceClient + _client: XrpcClient - constructor(service: AtpServiceClient) { - this._service = service + constructor(client: XrpcClient) { + this._client = client } } export class AppBskyUnspeccedNS { - _service: AtpServiceClient + _client: XrpcClient - constructor(service: AtpServiceClient) { - this._service = service + constructor(client: XrpcClient) { + this._client = client } getPopularFeedGenerators( params?: AppBskyUnspeccedGetPopularFeedGenerators.QueryParams, opts?: AppBskyUnspeccedGetPopularFeedGenerators.CallOptions, ): Promise { - return this._service.xrpc + return this._client .call( 'app.bsky.unspecced.getPopularFeedGenerators', params, @@ -2683,7 +2666,7 @@ export class AppBskyUnspeccedNS { params?: AppBskyUnspeccedGetSuggestionsSkeleton.QueryParams, opts?: AppBskyUnspeccedGetSuggestionsSkeleton.CallOptions, ): Promise { - return this._service.xrpc + return this._client .call( 'app.bsky.unspecced.getSuggestionsSkeleton', params, @@ -2699,7 +2682,7 @@ export class AppBskyUnspeccedNS { params?: AppBskyUnspeccedGetTaggedSuggestions.QueryParams, opts?: AppBskyUnspeccedGetTaggedSuggestions.CallOptions, ): Promise { - return this._service.xrpc + return this._client .call('app.bsky.unspecced.getTaggedSuggestions', params, undefined, opts) .catch((e) => { throw AppBskyUnspeccedGetTaggedSuggestions.toKnownErr(e) @@ -2710,7 +2693,7 @@ export class AppBskyUnspeccedNS { params?: AppBskyUnspeccedSearchActorsSkeleton.QueryParams, opts?: AppBskyUnspeccedSearchActorsSkeleton.CallOptions, ): Promise { - return this._service.xrpc + return this._client .call('app.bsky.unspecced.searchActorsSkeleton', params, undefined, opts) .catch((e) => { throw AppBskyUnspeccedSearchActorsSkeleton.toKnownErr(e) @@ -2721,7 +2704,7 @@ export class AppBskyUnspeccedNS { params?: AppBskyUnspeccedSearchPostsSkeleton.QueryParams, opts?: AppBskyUnspeccedSearchPostsSkeleton.CallOptions, ): Promise { - return this._service.xrpc + return this._client .call('app.bsky.unspecced.searchPostsSkeleton', params, undefined, opts) .catch((e) => { throw AppBskyUnspeccedSearchPostsSkeleton.toKnownErr(e) @@ -2730,43 +2713,43 @@ export class AppBskyUnspeccedNS { } export class ChatNS { - _service: AtpServiceClient + _client: XrpcClient bsky: ChatBskyNS - constructor(service: AtpServiceClient) { - this._service = service - this.bsky = new ChatBskyNS(service) + constructor(client: XrpcClient) { + this._client = client + this.bsky = new ChatBskyNS(client) } } export class ChatBskyNS { - _service: AtpServiceClient + _client: XrpcClient actor: ChatBskyActorNS convo: ChatBskyConvoNS moderation: ChatBskyModerationNS - constructor(service: AtpServiceClient) { - this._service = service - this.actor = new ChatBskyActorNS(service) - this.convo = new ChatBskyConvoNS(service) - this.moderation = new ChatBskyModerationNS(service) + constructor(client: XrpcClient) { + this._client = client + this.actor = new ChatBskyActorNS(client) + this.convo = new ChatBskyConvoNS(client) + this.moderation = new ChatBskyModerationNS(client) } } export class ChatBskyActorNS { - _service: AtpServiceClient + _client: XrpcClient declaration: DeclarationRecord - constructor(service: AtpServiceClient) { - this._service = service - this.declaration = new DeclarationRecord(service) + constructor(client: XrpcClient) { + this._client = client + this.declaration = new DeclarationRecord(client) } deleteAccount( data?: ChatBskyActorDeleteAccount.InputSchema, opts?: ChatBskyActorDeleteAccount.CallOptions, ): Promise { - return this._service.xrpc + return this._client .call('chat.bsky.actor.deleteAccount', opts?.qp, data, opts) .catch((e) => { throw ChatBskyActorDeleteAccount.toKnownErr(e) @@ -2777,7 +2760,7 @@ export class ChatBskyActorNS { params?: ChatBskyActorExportAccountData.QueryParams, opts?: ChatBskyActorExportAccountData.CallOptions, ): Promise { - return this._service.xrpc + return this._client .call('chat.bsky.actor.exportAccountData', params, undefined, opts) .catch((e) => { throw ChatBskyActorExportAccountData.toKnownErr(e) @@ -2786,10 +2769,10 @@ export class ChatBskyActorNS { } export class DeclarationRecord { - _service: AtpServiceClient + _client: XrpcClient - constructor(service: AtpServiceClient) { - this._service = service + constructor(client: XrpcClient) { + this._client = client } async list( @@ -2798,7 +2781,7 @@ export class DeclarationRecord { cursor?: string records: { uri: string; value: ChatBskyActorDeclaration.Record }[] }> { - const res = await this._service.xrpc.call('com.atproto.repo.listRecords', { + const res = await this._client.call('com.atproto.repo.listRecords', { collection: 'chat.bsky.actor.declaration', ...params, }) @@ -2812,7 +2795,7 @@ export class DeclarationRecord { cid: string value: ChatBskyActorDeclaration.Record }> { - const res = await this._service.xrpc.call('com.atproto.repo.getRecord', { + const res = await this._client.call('com.atproto.repo.getRecord', { collection: 'chat.bsky.actor.declaration', ...params, }) @@ -2828,7 +2811,7 @@ export class DeclarationRecord { headers?: Record, ): Promise<{ uri: string; cid: string }> { record.$type = 'chat.bsky.actor.declaration' - const res = await this._service.xrpc.call( + const res = await this._client.call( 'com.atproto.repo.createRecord', undefined, { @@ -2846,7 +2829,7 @@ export class DeclarationRecord { params: Omit, headers?: Record, ): Promise { - await this._service.xrpc.call( + await this._client.call( 'com.atproto.repo.deleteRecord', undefined, { collection: 'chat.bsky.actor.declaration', ...params }, @@ -2856,17 +2839,17 @@ export class DeclarationRecord { } export class ChatBskyConvoNS { - _service: AtpServiceClient + _client: XrpcClient - constructor(service: AtpServiceClient) { - this._service = service + constructor(client: XrpcClient) { + this._client = client } deleteMessageForSelf( data?: ChatBskyConvoDeleteMessageForSelf.InputSchema, opts?: ChatBskyConvoDeleteMessageForSelf.CallOptions, ): Promise { - return this._service.xrpc + return this._client .call('chat.bsky.convo.deleteMessageForSelf', opts?.qp, data, opts) .catch((e) => { throw ChatBskyConvoDeleteMessageForSelf.toKnownErr(e) @@ -2877,7 +2860,7 @@ export class ChatBskyConvoNS { params?: ChatBskyConvoGetConvo.QueryParams, opts?: ChatBskyConvoGetConvo.CallOptions, ): Promise { - return this._service.xrpc + return this._client .call('chat.bsky.convo.getConvo', params, undefined, opts) .catch((e) => { throw ChatBskyConvoGetConvo.toKnownErr(e) @@ -2888,7 +2871,7 @@ export class ChatBskyConvoNS { params?: ChatBskyConvoGetConvoForMembers.QueryParams, opts?: ChatBskyConvoGetConvoForMembers.CallOptions, ): Promise { - return this._service.xrpc + return this._client .call('chat.bsky.convo.getConvoForMembers', params, undefined, opts) .catch((e) => { throw ChatBskyConvoGetConvoForMembers.toKnownErr(e) @@ -2899,7 +2882,7 @@ export class ChatBskyConvoNS { params?: ChatBskyConvoGetLog.QueryParams, opts?: ChatBskyConvoGetLog.CallOptions, ): Promise { - return this._service.xrpc + return this._client .call('chat.bsky.convo.getLog', params, undefined, opts) .catch((e) => { throw ChatBskyConvoGetLog.toKnownErr(e) @@ -2910,7 +2893,7 @@ export class ChatBskyConvoNS { params?: ChatBskyConvoGetMessages.QueryParams, opts?: ChatBskyConvoGetMessages.CallOptions, ): Promise { - return this._service.xrpc + return this._client .call('chat.bsky.convo.getMessages', params, undefined, opts) .catch((e) => { throw ChatBskyConvoGetMessages.toKnownErr(e) @@ -2921,7 +2904,7 @@ export class ChatBskyConvoNS { data?: ChatBskyConvoLeaveConvo.InputSchema, opts?: ChatBskyConvoLeaveConvo.CallOptions, ): Promise { - return this._service.xrpc + return this._client .call('chat.bsky.convo.leaveConvo', opts?.qp, data, opts) .catch((e) => { throw ChatBskyConvoLeaveConvo.toKnownErr(e) @@ -2932,7 +2915,7 @@ export class ChatBskyConvoNS { params?: ChatBskyConvoListConvos.QueryParams, opts?: ChatBskyConvoListConvos.CallOptions, ): Promise { - return this._service.xrpc + return this._client .call('chat.bsky.convo.listConvos', params, undefined, opts) .catch((e) => { throw ChatBskyConvoListConvos.toKnownErr(e) @@ -2943,7 +2926,7 @@ export class ChatBskyConvoNS { data?: ChatBskyConvoMuteConvo.InputSchema, opts?: ChatBskyConvoMuteConvo.CallOptions, ): Promise { - return this._service.xrpc + return this._client .call('chat.bsky.convo.muteConvo', opts?.qp, data, opts) .catch((e) => { throw ChatBskyConvoMuteConvo.toKnownErr(e) @@ -2954,7 +2937,7 @@ export class ChatBskyConvoNS { data?: ChatBskyConvoSendMessage.InputSchema, opts?: ChatBskyConvoSendMessage.CallOptions, ): Promise { - return this._service.xrpc + return this._client .call('chat.bsky.convo.sendMessage', opts?.qp, data, opts) .catch((e) => { throw ChatBskyConvoSendMessage.toKnownErr(e) @@ -2965,7 +2948,7 @@ export class ChatBskyConvoNS { data?: ChatBskyConvoSendMessageBatch.InputSchema, opts?: ChatBskyConvoSendMessageBatch.CallOptions, ): Promise { - return this._service.xrpc + return this._client .call('chat.bsky.convo.sendMessageBatch', opts?.qp, data, opts) .catch((e) => { throw ChatBskyConvoSendMessageBatch.toKnownErr(e) @@ -2976,7 +2959,7 @@ export class ChatBskyConvoNS { data?: ChatBskyConvoUnmuteConvo.InputSchema, opts?: ChatBskyConvoUnmuteConvo.CallOptions, ): Promise { - return this._service.xrpc + return this._client .call('chat.bsky.convo.unmuteConvo', opts?.qp, data, opts) .catch((e) => { throw ChatBskyConvoUnmuteConvo.toKnownErr(e) @@ -2987,7 +2970,7 @@ export class ChatBskyConvoNS { data?: ChatBskyConvoUpdateRead.InputSchema, opts?: ChatBskyConvoUpdateRead.CallOptions, ): Promise { - return this._service.xrpc + return this._client .call('chat.bsky.convo.updateRead', opts?.qp, data, opts) .catch((e) => { throw ChatBskyConvoUpdateRead.toKnownErr(e) @@ -2996,17 +2979,17 @@ export class ChatBskyConvoNS { } export class ChatBskyModerationNS { - _service: AtpServiceClient + _client: XrpcClient - constructor(service: AtpServiceClient) { - this._service = service + constructor(client: XrpcClient) { + this._client = client } getActorMetadata( params?: ChatBskyModerationGetActorMetadata.QueryParams, opts?: ChatBskyModerationGetActorMetadata.CallOptions, ): Promise { - return this._service.xrpc + return this._client .call('chat.bsky.moderation.getActorMetadata', params, undefined, opts) .catch((e) => { throw ChatBskyModerationGetActorMetadata.toKnownErr(e) @@ -3017,7 +3000,7 @@ export class ChatBskyModerationNS { params?: ChatBskyModerationGetMessageContext.QueryParams, opts?: ChatBskyModerationGetMessageContext.CallOptions, ): Promise { - return this._service.xrpc + return this._client .call('chat.bsky.moderation.getMessageContext', params, undefined, opts) .catch((e) => { throw ChatBskyModerationGetMessageContext.toKnownErr(e) @@ -3028,7 +3011,7 @@ export class ChatBskyModerationNS { data?: ChatBskyModerationUpdateActorAccess.InputSchema, opts?: ChatBskyModerationUpdateActorAccess.CallOptions, ): Promise { - return this._service.xrpc + return this._client .call('chat.bsky.moderation.updateActorAccess', opts?.qp, data, opts) .catch((e) => { throw ChatBskyModerationUpdateActorAccess.toKnownErr(e) @@ -3037,39 +3020,39 @@ export class ChatBskyModerationNS { } export class ToolsNS { - _service: AtpServiceClient + _client: XrpcClient ozone: ToolsOzoneNS - constructor(service: AtpServiceClient) { - this._service = service - this.ozone = new ToolsOzoneNS(service) + constructor(client: XrpcClient) { + this._client = client + this.ozone = new ToolsOzoneNS(client) } } export class ToolsOzoneNS { - _service: AtpServiceClient + _client: XrpcClient communication: ToolsOzoneCommunicationNS moderation: ToolsOzoneModerationNS - constructor(service: AtpServiceClient) { - this._service = service - this.communication = new ToolsOzoneCommunicationNS(service) - this.moderation = new ToolsOzoneModerationNS(service) + constructor(client: XrpcClient) { + this._client = client + this.communication = new ToolsOzoneCommunicationNS(client) + this.moderation = new ToolsOzoneModerationNS(client) } } export class ToolsOzoneCommunicationNS { - _service: AtpServiceClient + _client: XrpcClient - constructor(service: AtpServiceClient) { - this._service = service + constructor(client: XrpcClient) { + this._client = client } createTemplate( data?: ToolsOzoneCommunicationCreateTemplate.InputSchema, opts?: ToolsOzoneCommunicationCreateTemplate.CallOptions, ): Promise { - return this._service.xrpc + return this._client .call('tools.ozone.communication.createTemplate', opts?.qp, data, opts) .catch((e) => { throw ToolsOzoneCommunicationCreateTemplate.toKnownErr(e) @@ -3080,7 +3063,7 @@ export class ToolsOzoneCommunicationNS { data?: ToolsOzoneCommunicationDeleteTemplate.InputSchema, opts?: ToolsOzoneCommunicationDeleteTemplate.CallOptions, ): Promise { - return this._service.xrpc + return this._client .call('tools.ozone.communication.deleteTemplate', opts?.qp, data, opts) .catch((e) => { throw ToolsOzoneCommunicationDeleteTemplate.toKnownErr(e) @@ -3091,7 +3074,7 @@ export class ToolsOzoneCommunicationNS { params?: ToolsOzoneCommunicationListTemplates.QueryParams, opts?: ToolsOzoneCommunicationListTemplates.CallOptions, ): Promise { - return this._service.xrpc + return this._client .call('tools.ozone.communication.listTemplates', params, undefined, opts) .catch((e) => { throw ToolsOzoneCommunicationListTemplates.toKnownErr(e) @@ -3102,7 +3085,7 @@ export class ToolsOzoneCommunicationNS { data?: ToolsOzoneCommunicationUpdateTemplate.InputSchema, opts?: ToolsOzoneCommunicationUpdateTemplate.CallOptions, ): Promise { - return this._service.xrpc + return this._client .call('tools.ozone.communication.updateTemplate', opts?.qp, data, opts) .catch((e) => { throw ToolsOzoneCommunicationUpdateTemplate.toKnownErr(e) @@ -3111,17 +3094,17 @@ export class ToolsOzoneCommunicationNS { } export class ToolsOzoneModerationNS { - _service: AtpServiceClient + _client: XrpcClient - constructor(service: AtpServiceClient) { - this._service = service + constructor(client: XrpcClient) { + this._client = client } emitEvent( data?: ToolsOzoneModerationEmitEvent.InputSchema, opts?: ToolsOzoneModerationEmitEvent.CallOptions, ): Promise { - return this._service.xrpc + return this._client .call('tools.ozone.moderation.emitEvent', opts?.qp, data, opts) .catch((e) => { throw ToolsOzoneModerationEmitEvent.toKnownErr(e) @@ -3132,7 +3115,7 @@ export class ToolsOzoneModerationNS { params?: ToolsOzoneModerationGetEvent.QueryParams, opts?: ToolsOzoneModerationGetEvent.CallOptions, ): Promise { - return this._service.xrpc + return this._client .call('tools.ozone.moderation.getEvent', params, undefined, opts) .catch((e) => { throw ToolsOzoneModerationGetEvent.toKnownErr(e) @@ -3143,7 +3126,7 @@ export class ToolsOzoneModerationNS { params?: ToolsOzoneModerationGetRecord.QueryParams, opts?: ToolsOzoneModerationGetRecord.CallOptions, ): Promise { - return this._service.xrpc + return this._client .call('tools.ozone.moderation.getRecord', params, undefined, opts) .catch((e) => { throw ToolsOzoneModerationGetRecord.toKnownErr(e) @@ -3154,7 +3137,7 @@ export class ToolsOzoneModerationNS { params?: ToolsOzoneModerationGetRepo.QueryParams, opts?: ToolsOzoneModerationGetRepo.CallOptions, ): Promise { - return this._service.xrpc + return this._client .call('tools.ozone.moderation.getRepo', params, undefined, opts) .catch((e) => { throw ToolsOzoneModerationGetRepo.toKnownErr(e) @@ -3165,7 +3148,7 @@ export class ToolsOzoneModerationNS { params?: ToolsOzoneModerationQueryEvents.QueryParams, opts?: ToolsOzoneModerationQueryEvents.CallOptions, ): Promise { - return this._service.xrpc + return this._client .call('tools.ozone.moderation.queryEvents', params, undefined, opts) .catch((e) => { throw ToolsOzoneModerationQueryEvents.toKnownErr(e) @@ -3176,7 +3159,7 @@ export class ToolsOzoneModerationNS { params?: ToolsOzoneModerationQueryStatuses.QueryParams, opts?: ToolsOzoneModerationQueryStatuses.CallOptions, ): Promise { - return this._service.xrpc + return this._client .call('tools.ozone.moderation.queryStatuses', params, undefined, opts) .catch((e) => { throw ToolsOzoneModerationQueryStatuses.toKnownErr(e) @@ -3187,7 +3170,7 @@ export class ToolsOzoneModerationNS { params?: ToolsOzoneModerationSearchRepos.QueryParams, opts?: ToolsOzoneModerationSearchRepos.CallOptions, ): Promise { - return this._service.xrpc + return this._client .call('tools.ozone.moderation.searchRepos', params, undefined, opts) .catch((e) => { throw ToolsOzoneModerationSearchRepos.toKnownErr(e) diff --git a/packages/api/src/index.ts b/packages/api/src/index.ts index 64fbb557f43..b53897b3f6d 100644 --- a/packages/api/src/index.ts +++ b/packages/api/src/index.ts @@ -11,7 +11,7 @@ export * from './types' export * from './const' export * from './util' export * from './client' -export * from './agent' +export * from './session' export * from './rich-text/rich-text' export * from './rich-text/sanitization' export * from './rich-text/unicode' @@ -20,5 +20,7 @@ export * from './moderation' export * from './moderation/types' export * from './mocker' export { LABELS, DEFAULT_LABEL_SETTINGS } from './moderation/const/labels' +export { AtpAgent } from './agent' export { BskyAgent } from './bsky-agent' + export { AtpAgent as default } from './agent' diff --git a/packages/api/src/session/atp-session-manager.ts b/packages/api/src/session/atp-session-manager.ts new file mode 100644 index 00000000000..39ebcee89c0 --- /dev/null +++ b/packages/api/src/session/atp-session-manager.ts @@ -0,0 +1,403 @@ +import { getPdsEndpoint, isValidDidDoc } from '@atproto/common-web' +import { + ErrorResponseBody, + ResponseType, + XRPCError, + errorResponseBody, +} from '@atproto/xrpc' +import { + AtpClient, + ComAtprotoServerCreateAccount, + ComAtprotoServerCreateSession, + ComAtprotoServerGetSession, +} from '../client' +import { + AtpAgentLoginOpts, + AtpPersistSessionHandler, + AtpSessionData, +} from '../types' +import { SessionManager } from './session-manager' + +const ReadableStream = globalThis.ReadableStream as + | typeof globalThis.ReadableStream + | undefined + +export type Fetch = (this: void, request: Request) => Promise +export interface AtpSessionManagerOptions { + service: string | URL + persistSession?: AtpPersistSessionHandler + fetch?: Fetch +} + +/** + * A {@link SessionManager} that uses legacy "com.atproto.server" endpoints to + * manage sessions and route XRPC requests. + */ +export class AtpSessionManager implements SessionManager { + public serviceUrl: URL + public pdsUrl?: URL // The PDS URL, driven by the did doc + public session?: AtpSessionData + + private fetch: Fetch + private client: AtpClient + private persistSession?: AtpPersistSessionHandler + private refreshSessionPromise: Promise | undefined + + constructor(options: AtpSessionManagerOptions) { + this.serviceUrl = new URL(options.service) + this.fetch = options.fetch || globalThis.fetch + this.setPersistSessionHandler(options.persistSession) + + // Private API client used to perform session management API calls on the + // service endpoint. + this.client = new AtpClient({ + fetch: async (request) => (0, this.fetch)(request), + service: () => this.getServiceUrl(), + headers: { + authorization: () => + this.session?.accessJwt && `Bearer ${this.session.accessJwt}`, + }, + }) + } + + getServiceUrl() { + return this.serviceUrl + } + + getDispatchUrl() { + return this.pdsUrl || this.serviceUrl + } + + /** + * fetch method that will be triggered by the ApiClient. This method will: + * - Set the proper origin for the request (pds or service) + * - Add the proper auth headers to the request + * - Handle session refreshes + * + * @note We define this as a method on the prototype instead of inlining the + * function in the constructor for readability. + */ + async fetchHandler(url: string, reqInit: RequestInit): Promise { + // wait for any active session-refreshes to finish + await this.refreshSessionPromise + + const initialUri = new URL(url, this.getDispatchUrl()) + const initialReq = new Request(initialUri, reqInit) + + const initialToken = this.session?.accessJwt + if (!initialToken || initialReq.headers.has('authorization')) { + return (0, this.fetch)(initialReq) + } + + initialReq.headers.set('authorization', `Bearer ${initialToken}`) + const initialRes = await (0, this.fetch)(initialReq) + + if (!this.session?.refreshJwt) { + return initialRes + } + const isExpiredToken = await isErrorResponse( + initialRes, + [400], + ['ExpiredToken'], + ) + + if (!isExpiredToken) { + return initialRes + } + + try { + await this.refreshSession() + } catch { + return initialRes + } + + if (reqInit?.signal?.aborted) { + return initialRes + } + + // The stream was already consumed. We cannot retry the request. A solution + // would be to tee() the input stream but that would bufferize the entire + // stream in memory which can lead to memory starvation. Instead, we will + // return the original response and let the calling code handle retries. + if (ReadableStream && reqInit.body instanceof ReadableStream) { + return initialRes + } + + // Return initial "ExpiredToken" response if the session was not refreshed. + const updatedToken = this.session?.accessJwt + if (!updatedToken || updatedToken === initialToken) { + return initialRes + } + + // Make sure the initial request is cancelled to avoid leaking resources + // (NodeJS 👀): https://undici.nodejs.org/#/?id=garbage-collection + await initialRes.body?.cancel() + + // We need to re-compute the URI in case the PDS endpoint has changed + const updatedUri = new URL(url, this.getDispatchUrl()) + const updatedReq = new Request(updatedUri, reqInit) + + updatedReq.headers.set('authorization', `Bearer ${updatedToken}`) + + return await (0, this.fetch)(updatedReq) + } + + getDid() { + const did = this.session?.did + if (did) return did + + throw new Error('Not logged in') + } + + /** + * Is there any active session? + */ + get hasSession() { + return !!this.session + } + + /** + * Sets a WhatWG "fetch()" function to be used for making HTTP requests. + */ + setFetchHandler(fetch: Fetch = globalThis.fetch) { + this.fetch = fetch + } + + /** + * Sets the "Persist Session" method which can be used to store access tokens + * as they change. + */ + setPersistSessionHandler(handler?: AtpPersistSessionHandler) { + this.persistSession = handler?.bind(null) + } + + /** + * Create a new account and hydrate its session in this agent. + */ + async createAccount( + opts: ComAtprotoServerCreateAccount.InputSchema, + ): Promise { + try { + const res = await this.client.com.atproto.server.createAccount(opts) + this.session = { + accessJwt: res.data.accessJwt, + refreshJwt: res.data.refreshJwt, + handle: res.data.handle, + did: res.data.did, + email: opts.email, + emailConfirmed: false, + emailAuthFactor: false, + } + this._updateApiEndpoint(res.data.didDoc) + return res + } catch (e) { + this.session = undefined + throw e + } finally { + if (this.session) { + this.persistSession?.('create', this.session) + } else { + this.persistSession?.('create-failed', undefined) + } + } + } + + /** + * Start a new session with this agent. + */ + async login( + opts: AtpAgentLoginOpts, + ): Promise { + try { + const res = await this.client.com.atproto.server.createSession({ + identifier: opts.identifier, + password: opts.password, + authFactorToken: opts.authFactorToken, + }) + this.session = { + accessJwt: res.data.accessJwt, + refreshJwt: res.data.refreshJwt, + handle: res.data.handle, + did: res.data.did, + email: res.data.email, + emailConfirmed: res.data.emailConfirmed, + emailAuthFactor: res.data.emailAuthFactor, + } + this._updateApiEndpoint(res.data.didDoc) + return res + } catch (e) { + this.session = undefined + throw e + } finally { + if (this.session) { + this.persistSession?.('create', this.session) + } else { + this.persistSession?.('create-failed', undefined) + } + } + } + + /** + * Resume a pre-existing session with this agent. + */ + async resumeSession( + session: AtpSessionData, + ): Promise { + try { + this.session = session + // For this particular call, we want this.fetchHandler() to be used in + // order to refresh the session if needed. So let's create a new client + // instance with the right fetchHandler. + const res = await new AtpClient( + this.fetchHandler, + ).com.atproto.server.getSession() + if (res.data.did !== this.session.did) { + throw new XRPCError( + ResponseType.InvalidRequest, + 'Invalid session', + 'InvalidDID', + ) + } + this.session.email = res.data.email + this.session.handle = res.data.handle + this.session.emailConfirmed = res.data.emailConfirmed + this.session.emailAuthFactor = res.data.emailAuthFactor + this._updateApiEndpoint(res.data.didDoc) + this.persistSession?.('update', this.session) + return res + } catch (e) { + this.session = undefined + + if (e instanceof XRPCError) { + /* + * `ExpiredToken` and `InvalidToken` are handled in + * `this_refreshSession`, and emit an `expired` event there. + * + * Everything else is handled here. + */ + if ( + [1, 408, 425, 429, 500, 502, 503, 504, 522, 524].includes(e.status) + ) { + this.persistSession?.('network-error', undefined) + } else { + this.persistSession?.('expired', undefined) + } + } else { + this.persistSession?.('network-error', undefined) + } + + throw e + } + } + + /** + * Internal helper to refresh sessions + * - Wraps the actual implementation in a promise-guard to ensure only + * one refresh is attempted at a time. + */ + async refreshSession(): Promise { + return (this.refreshSessionPromise ||= this._refreshSessionInner().finally( + () => { + this.refreshSessionPromise = undefined + }, + )) + } + + /** + * Internal helper to refresh sessions (actual behavior) + */ + async _refreshSessionInner() { + if (!this.session?.refreshJwt) { + return + } + + try { + const res = await this.client.com.atproto.server.refreshSession( + undefined, + { headers: { authorization: `Bearer ${this.session.refreshJwt}` } }, + ) + // succeeded, update the session + this.session = { + ...this.session, + accessJwt: res.data.accessJwt, + refreshJwt: res.data.refreshJwt, + handle: res.data.handle, + did: res.data.did, + } + this._updateApiEndpoint(res.data.didDoc) + this.persistSession?.('update', this.session) + } catch (err) { + if ( + err instanceof XRPCError && + err.error && + ['ExpiredToken', 'InvalidToken'].includes(err.error) + ) { + // failed due to a bad refresh token + this.session = undefined + this.persistSession?.('expired', undefined) + } + // else: other failures should be ignored - the issue will + // propagate in the _dispatch() second attempt to run + // the request + } + } + + /** + * Helper to update the pds endpoint dynamically. + * + * The session methods (create, resume, refresh) may respond with the user's + * did document which contains the user's canonical PDS endpoint. That endpoint + * may differ from the endpoint used to contact the server. We capture that + * PDS endpoint and update the client to use that given endpoint for future + * requests. (This helps ensure smooth migrations between PDSes, especially + * when the PDSes are operated by a single org.) + */ + private _updateApiEndpoint(didDoc: unknown) { + if (isValidDidDoc(didDoc)) { + const endpoint = getPdsEndpoint(didDoc) + this.pdsUrl = endpoint ? new URL(endpoint) : undefined + } else { + // If the did doc is invalid, we clear the pdsUrl (should never happen) + this.pdsUrl = undefined + } + } +} + +function isErrorObject(v: unknown): v is ErrorResponseBody { + return errorResponseBody.safeParse(v).success +} + +async function isErrorResponse( + response: Response, + status: number[], + errorNames: string[], +): Promise { + if (!status.includes(response.status)) return false + // Some engines (react-native 👀) don't expose a response.body property... + // if (!response.body) return false + try { + const json = await peekJson(response, 10 * 1024) + return isErrorObject(json) && (errorNames as any[]).includes(json.error) + } catch (err) { + return false + } +} + +async function peekJson( + response: Response, + maxSize = Infinity, +): Promise { + if (extractType(response) !== 'application/json') throw new Error('Not JSON') + if (extractLength(response) > maxSize) throw new Error('Response too large') + return response.clone().json() +} + +function extractLength({ headers }: Response) { + return headers.get('Content-Length') + ? Number(headers.get('Content-Length')) + : NaN +} + +function extractType({ headers }: Response) { + return headers.get('Content-Type')?.split(';')[0]?.trim() +} diff --git a/packages/api/src/session/index.ts b/packages/api/src/session/index.ts new file mode 100644 index 00000000000..3759556de9b --- /dev/null +++ b/packages/api/src/session/index.ts @@ -0,0 +1,3 @@ +export * from './session-manager' +export * from './atp-session-manager' +export * from './stateless-session-handler' diff --git a/packages/api/src/session/session-manager.ts b/packages/api/src/session/session-manager.ts new file mode 100644 index 00000000000..70fcd889e8e --- /dev/null +++ b/packages/api/src/session/session-manager.ts @@ -0,0 +1,20 @@ +export interface SessionManager { + fetchHandler(url: string, reqInit: RequestInit): Promise + getDid(): string | PromiseLike + + /** @deprecated only used for a very particular use-case in the official Bluesky app */ + getServiceUrl(): URL | PromiseLike +} + +export function isSessionManager(value: T): value is T & SessionManager { + return ( + value !== null && + typeof value === 'object' && + 'fetchHandler' in value && + typeof value.fetchHandler === 'function' && + 'getDid' in value && + typeof value.getDid === 'function' && + 'getServiceUrl' in value && + typeof value.getServiceUrl === 'function' + ) +} diff --git a/packages/api/src/session/stateless-session-handler.ts b/packages/api/src/session/stateless-session-handler.ts new file mode 100644 index 00000000000..c4757afc2da --- /dev/null +++ b/packages/api/src/session/stateless-session-handler.ts @@ -0,0 +1,43 @@ +import { SessionManager } from './session-manager' + +export type StatelessSessionManagerOptions = { + service: string | URL + headers?: { [_ in string]?: null | string } +} + +export class StatelessSessionManager implements SessionManager { + readonly serviceUrl: URL + readonly headers: Map + + constructor({ service, headers }: StatelessSessionManagerOptions) { + this.headers = new Map( + headers + ? Object.entries(headers).filter( + ( + e: T, + ): e is T & [T[0], NonNullable] => e[1] != null, + ) + : headers, + ) + this.serviceUrl = new URL(service) + } + + async getServiceUrl(): Promise { + return this.serviceUrl + } + + async getDid(): Promise { + throw new Error('Not logged in') + } + + async fetchHandler(url: string, reqInit: RequestInit): Promise { + const fullUrl = new URL(url, this.serviceUrl) + const headers = new Headers(reqInit.headers) + + for (const [key, value] of this.headers) { + if (value != null) headers.set(key, value) + } + + return globalThis.fetch(fullUrl.toString(), { ...reqInit, headers }) + } +} diff --git a/packages/api/src/types.ts b/packages/api/src/types.ts index 60a26d3545b..34b67e47646 100644 --- a/packages/api/src/types.ts +++ b/packages/api/src/types.ts @@ -37,14 +37,6 @@ export type AtpPersistSessionHandler = ( session: AtpSessionData | undefined, ) => void | Promise -/** - * AtpAgent constructor() opts - */ -export interface AtpAgentOpts { - service: string | URL - persistSession?: AtpPersistSessionHandler -} - /** * AtpAgent login() opts */ @@ -54,27 +46,10 @@ export interface AtpAgentLoginOpts { authFactorToken?: string | undefined } -/** - * AtpAgent global fetch handler - */ -type AtpAgentFetchHeaders = Record -export interface AtpAgentFetchHandlerResponse { - status: number - headers: Record - body: any -} -export type AtpAgentFetchHandler = ( - httpUri: string, - httpMethod: string, - httpHeaders: AtpAgentFetchHeaders, - httpReqBody: any, -) => Promise - /** * AtpAgent global config opts */ export interface AtpAgentGlobalOpts { - fetch?: AtpAgentFetchHandler appLabelers?: string[] } diff --git a/packages/api/tests/bsky-agent.test.ts b/packages/api/tests/bsky-agent.test.ts index 0b3fcdd4c4a..dde7746329a 100644 --- a/packages/api/tests/bsky-agent.test.ts +++ b/packages/api/tests/bsky-agent.test.ts @@ -1,17 +1,18 @@ import { TestNetworkNoAppView } from '@atproto/dev-env' import { TID } from '@atproto/common-web' import { + AppBskyActorDefs, + AppBskyActorProfile, BskyAgent, ComAtprotoRepoPutRecord, - AppBskyActorProfile, DEFAULT_LABEL_SETTINGS, + SessionDispatcher, } from '../src' import { - savedFeedsToUriArrays, getSavedFeedType, + savedFeedsToUriArrays, validateSavedFeed, } from '../src/util' -import { AppBskyActorDefs } from '../dist' describe('agent', () => { let network: TestNetworkNoAppView @@ -31,7 +32,7 @@ describe('agent', () => { ): Promise => { try { const res = await agent.api.app.bsky.actor.profile.get({ - repo: agent.session?.did || '', + repo: await agent.getDid(), rkey: 'self', }) return res.value.displayName ?? '' @@ -40,21 +41,16 @@ describe('agent', () => { } } - it('clones correctly', () => { - const agent = new BskyAgent({ service: network.pds.url }) - const agent2 = agent.clone() - expect(agent2 instanceof BskyAgent).toBeTruthy() - expect(agent.service).toEqual(agent2.service) - }) - it('upsertProfile correctly creates and updates profiles.', async () => { - const agent = new BskyAgent({ service: network.pds.url }) - - await agent.createAccount({ + const dispatcher = new SessionDispatcher({ service: network.pds.url }) + await dispatcher.createAccount({ handle: 'user1.test', email: 'user1@test.com', password: 'password', }) + + const agent = new BskyAgent(dispatcher) + const displayName1 = await getProfileDisplayName(agent) expect(displayName1).toBeFalsy() @@ -80,14 +76,15 @@ describe('agent', () => { }) it('upsertProfile correctly handles CAS failures.', async () => { - const agent = new BskyAgent({ service: network.pds.url }) - - await agent.createAccount({ + const dispatcher = new SessionDispatcher({ service: network.pds.url }) + await dispatcher.createAccount({ handle: 'user2.test', email: 'user2@test.com', password: 'password', }) + const agent = new BskyAgent(dispatcher) + const displayName1 = await getProfileDisplayName(agent) expect(displayName1).toBeFalsy() @@ -96,7 +93,7 @@ describe('agent', () => { await agent.upsertProfile(async (_existing) => { if (!hasConflicted) { await agent.com.atproto.repo.putRecord({ - repo: agent.session?.did || '', + repo: await agent.getDid(), collection: 'app.bsky.actor.profile', rkey: 'self', record: { @@ -119,20 +116,21 @@ describe('agent', () => { }) it('upsertProfile wont endlessly retry CAS failures.', async () => { - const agent = new BskyAgent({ service: network.pds.url }) - - await agent.createAccount({ + const dispatcher = new SessionDispatcher({ service: network.pds.url }) + await dispatcher.createAccount({ handle: 'user3.test', email: 'user3@test.com', password: 'password', }) + const agent = new BskyAgent(dispatcher) + const displayName1 = await getProfileDisplayName(agent) expect(displayName1).toBeFalsy() const p = agent.upsertProfile(async (_existing) => { await agent.com.atproto.repo.putRecord({ - repo: agent.session?.did || '', + repo: await agent.getDid(), collection: 'app.bsky.actor.profile', rkey: 'self', record: { @@ -148,14 +146,15 @@ describe('agent', () => { }) it('upsertProfile validates the record.', async () => { - const agent = new BskyAgent({ service: network.pds.url }) - - await agent.createAccount({ + const dispatcher = new SessionDispatcher({ service: network.pds.url }) + await dispatcher.createAccount({ handle: 'user4.test', email: 'user4@test.com', password: 'password', }) + const agent = new BskyAgent(dispatcher) + const p = agent.upsertProfile((_existing) => { return { displayName: { string: 'Bob' }, @@ -229,14 +228,15 @@ describe('agent', () => { describe('preferences methods', () => { it('gets and sets preferences correctly', async () => { - const agent = new BskyAgent({ service: network.pds.url }) - - await agent.createAccount({ + const dispatcher = new SessionDispatcher({ service: network.pds.url }) + await dispatcher.createAccount({ handle: 'user5.test', email: 'user5@test.com', password: 'password', }) + const agent = new BskyAgent(dispatcher) + const DEFAULT_LABELERS = BskyAgent.appLabelers.map((did) => ({ did, labels: {}, @@ -1082,14 +1082,15 @@ describe('agent', () => { }) it('resolves duplicates correctly', async () => { - const agent = new BskyAgent({ service: network.pds.url }) - - await agent.createAccount({ + const dispatcher = new SessionDispatcher({ service: network.pds.url }) + await dispatcher.createAccount({ handle: 'user6.test', email: 'user6@test.com', password: 'password', }) + const agent = new BskyAgent(dispatcher) + await agent.app.bsky.actor.putPreferences({ preferences: [ { @@ -1643,12 +1644,16 @@ describe('agent', () => { ] beforeAll(async () => { - agent = new BskyAgent({ service: network.pds.url }) - await agent.createAccount({ + const dispatcher = new SessionDispatcher({ + service: network.pds.url, + }) + await dispatcher.createAccount({ handle: 'user7.test', email: 'user7@test.com', password: 'password', }) + + agent = new BskyAgent(dispatcher) }) it('upsertMutedWords', async () => { @@ -1868,12 +1873,16 @@ describe('agent', () => { const postUri = 'at://did:plc:fake/app.bsky.feed.post/fake' beforeAll(async () => { - agent = new BskyAgent({ service: network.pds.url }) - await agent.createAccount({ + const dispatcher = new SessionDispatcher({ + service: network.pds.url, + }) + await dispatcher.createAccount({ handle: 'user8.test', email: 'user8@test.com', password: 'password', }) + + agent = new BskyAgent(dispatcher) }) it('hidePost', async () => { @@ -1907,12 +1916,16 @@ describe('agent', () => { const listUri = () => `at://bob.com/app.bsky.graph.list/${i++}` beforeAll(async () => { - agent = new BskyAgent({ service: network.pds.url }) - await agent.createAccount({ + const dispatcher = new SessionDispatcher({ + service: network.pds.url, + }) + await dispatcher.createAccount({ handle: 'user9.test', email: 'user9@test.com', password: 'password', }) + + agent = new BskyAgent(dispatcher) }) beforeEach(async () => { @@ -2380,12 +2393,16 @@ describe('agent', () => { const feedUri = () => `at://bob.com/app.bsky.feed.generator/${i++}` beforeAll(async () => { - agent = new BskyAgent({ service: network.pds.url }) - await agent.createAccount({ + const dispatcher = new SessionDispatcher({ + service: network.pds.url, + }) + await dispatcher.createAccount({ handle: 'user10.test', email: 'user10@test.com', password: 'password', }) + + agent = new BskyAgent(dispatcher) }) beforeEach(async () => { diff --git a/packages/api/tests/agent.test.ts b/packages/api/tests/dispatcher.test.ts similarity index 58% rename from packages/api/tests/agent.test.ts rename to packages/api/tests/dispatcher.test.ts index f618c0a5bc9..c17bd5f6234 100644 --- a/packages/api/tests/agent.test.ts +++ b/packages/api/tests/dispatcher.test.ts @@ -1,18 +1,22 @@ import assert from 'assert' import getPort from 'get-port' -import { defaultFetchHandler } from '@atproto/xrpc' import { AtpAgent, - AtpAgentFetchHandlerResponse, AtpSessionEvent, AtpSessionData, BSKY_LABELER_DID, -} from '..' + SessionDispatcher, +} from '../src' import { TestNetworkNoAppView } from '@atproto/dev-env' import { getPdsEndpoint, isValidDidDoc } from '@atproto/common-web' import { createHeaderEchoServer } from './util/echo-server' -describe('agent', () => { +const getPdsEndpointUrl = (...args: Parameters) => { + const endpoint = getPdsEndpoint(...args) + return endpoint ? new URL(endpoint) : endpoint +} + +describe('dispatcher', () => { let network: TestNetworkNoAppView beforeAll(async () => { @@ -28,14 +32,6 @@ describe('agent', () => { await network.close() }) - it('clones correctly', () => { - const persistSession = (_evt: AtpSessionEvent, _sess?: AtpSessionData) => {} - const agent = new AtpAgent({ service: network.pds.url, persistSession }) - const agent2 = agent.clone() - expect(agent2 instanceof AtpAgent).toBeTruthy() - expect(agent.service).toEqual(agent2.service) - }) - it('creates a new session on account creation.', async () => { const events: string[] = [] const sessions: (AtpSessionData | undefined)[] = [] @@ -44,27 +40,29 @@ describe('agent', () => { sessions.push(sess) } - const agent = new AtpAgent({ service: network.pds.url, persistSession }) + const dispatcher = new SessionDispatcher({ + service: network.pds.url, + persistSession, + }) - const res = await agent.createAccount({ + const res = await dispatcher.createAccount({ handle: 'user1.test', email: 'user1@test.com', password: 'password', }) - expect(agent.hasSession).toEqual(true) - expect(agent.session?.accessJwt).toEqual(res.data.accessJwt) - expect(agent.session?.refreshJwt).toEqual(res.data.refreshJwt) - expect(agent.session?.handle).toEqual(res.data.handle) - expect(agent.session?.did).toEqual(res.data.did) - expect(agent.session?.email).toEqual('user1@test.com') - expect(agent.session?.emailConfirmed).toEqual(false) + expect(dispatcher.hasSession).toEqual(true) + expect(dispatcher.session?.accessJwt).toEqual(res.data.accessJwt) + expect(dispatcher.session?.refreshJwt).toEqual(res.data.refreshJwt) + expect(dispatcher.session?.handle).toEqual(res.data.handle) + expect(dispatcher.session?.did).toEqual(res.data.did) + expect(dispatcher.session?.email).toEqual('user1@test.com') + expect(dispatcher.session?.emailConfirmed).toEqual(false) assert(isValidDidDoc(res.data.didDoc)) - expect(agent.api.xrpc.uri.origin).toEqual(getPdsEndpoint(res.data.didDoc)) + expect(dispatcher.pdsUrl).toEqual(getPdsEndpointUrl(res.data.didDoc)) - const { data: sessionInfo } = await agent.api.com.atproto.server.getSession( - {}, - ) + const agent = new AtpAgent(dispatcher) + const { data: sessionInfo } = await agent.com.atproto.server.getSession({}) expect(sessionInfo).toMatchObject({ did: res.data.did, handle: res.data.handle, @@ -76,7 +74,7 @@ describe('agent', () => { expect(events.length).toEqual(1) expect(events[0]).toEqual('create') expect(sessions.length).toEqual(1) - expect(sessions[0]?.accessJwt).toEqual(agent.session?.accessJwt) + expect(sessions[0]?.accessJwt).toEqual(dispatcher.session?.accessJwt) }) it('creates a new session on login.', async () => { @@ -87,7 +85,10 @@ describe('agent', () => { sessions.push(sess) } - const agent1 = new AtpAgent({ service: network.pds.url, persistSession }) + const agent1 = new SessionDispatcher({ + service: network.pds.url, + persistSession, + }) const email = 'user2@test.com' await agent1.createAccount({ @@ -96,24 +97,28 @@ describe('agent', () => { password: 'password', }) - const agent2 = new AtpAgent({ service: network.pds.url, persistSession }) - const res1 = await agent2.login({ + const dispatcher2 = new SessionDispatcher({ + service: network.pds.url, + persistSession, + }) + + const res1 = await dispatcher2.login({ identifier: 'user2.test', password: 'password', }) - expect(agent2.hasSession).toEqual(true) - expect(agent2.session?.accessJwt).toEqual(res1.data.accessJwt) - expect(agent2.session?.refreshJwt).toEqual(res1.data.refreshJwt) - expect(agent2.session?.handle).toEqual(res1.data.handle) - expect(agent2.session?.did).toEqual(res1.data.did) - expect(agent2.session?.email).toEqual('user2@test.com') - expect(agent2.session?.emailConfirmed).toEqual(false) + expect(dispatcher2.hasSession).toEqual(true) + expect(dispatcher2.session?.accessJwt).toEqual(res1.data.accessJwt) + expect(dispatcher2.session?.refreshJwt).toEqual(res1.data.refreshJwt) + expect(dispatcher2.session?.handle).toEqual(res1.data.handle) + expect(dispatcher2.session?.did).toEqual(res1.data.did) + expect(dispatcher2.session?.email).toEqual('user2@test.com') + expect(dispatcher2.session?.emailConfirmed).toEqual(false) assert(isValidDidDoc(res1.data.didDoc)) - expect(agent2.api.xrpc.uri.origin).toEqual(getPdsEndpoint(res1.data.didDoc)) + expect(dispatcher2.pdsUrl).toEqual(getPdsEndpointUrl(res1.data.didDoc)) - const { data: sessionInfo } = - await agent2.api.com.atproto.server.getSession({}) + const agent2 = new AtpAgent(dispatcher2) + const { data: sessionInfo } = await agent2.com.atproto.server.getSession({}) expect(sessionInfo).toMatchObject({ did: res1.data.did, handle: res1.data.handle, @@ -127,7 +132,7 @@ describe('agent', () => { expect(events[1]).toEqual('create') expect(sessions.length).toEqual(2) expect(sessions[0]?.accessJwt).toEqual(agent1.session?.accessJwt) - expect(sessions[1]?.accessJwt).toEqual(agent2.session?.accessJwt) + expect(sessions[1]?.accessJwt).toEqual(dispatcher2.session?.accessJwt) }) it('resumes an existing session.', async () => { @@ -138,28 +143,35 @@ describe('agent', () => { sessions.push(sess) } - const agent1 = new AtpAgent({ service: network.pds.url, persistSession }) + const dispatcher1 = new SessionDispatcher({ + service: network.pds.url, + persistSession, + }) - await agent1.createAccount({ + await dispatcher1.createAccount({ handle: 'user3.test', email: 'user3@test.com', password: 'password', }) - if (!agent1.session) { + if (!dispatcher1.session) { throw new Error('No session created') } - const agent2 = new AtpAgent({ service: network.pds.url, persistSession }) - const res1 = await agent2.resumeSession(agent1.session) + const dispatcher2 = new SessionDispatcher({ + service: network.pds.url, + persistSession, + }) + + const res1 = await dispatcher2.resumeSession(dispatcher1.session) - expect(agent2.hasSession).toEqual(true) - expect(agent2.session?.handle).toEqual(res1.data.handle) - expect(agent2.session?.did).toEqual(res1.data.did) + expect(dispatcher2.hasSession).toEqual(true) + expect(dispatcher2.session?.handle).toEqual(res1.data.handle) + expect(dispatcher2.session?.did).toEqual(res1.data.did) assert(isValidDidDoc(res1.data.didDoc)) - expect(agent2.api.xrpc.uri.origin).toEqual(getPdsEndpoint(res1.data.didDoc)) + expect(dispatcher2.pdsUrl).toEqual(getPdsEndpointUrl(res1.data.didDoc)) - const { data: sessionInfo } = - await agent2.api.com.atproto.server.getSession({}) + const agent2 = new AtpAgent(dispatcher2) + const { data: sessionInfo } = await agent2.com.atproto.server.getSession({}) expect(sessionInfo).toMatchObject({ did: res1.data.did, handle: res1.data.handle, @@ -172,8 +184,8 @@ describe('agent', () => { expect(events[0]).toEqual('create') expect(events[1]).toEqual('update') expect(sessions.length).toEqual(2) - expect(sessions[0]?.accessJwt).toEqual(agent1.session?.accessJwt) - expect(sessions[1]?.accessJwt).toEqual(agent2.session?.accessJwt) + expect(sessions[0]?.accessJwt).toEqual(dispatcher1.session?.accessJwt) + expect(sessions[1]?.accessJwt).toEqual(dispatcher2.session?.accessJwt) }) it('refreshes existing session.', async () => { @@ -184,18 +196,21 @@ describe('agent', () => { sessions.push(sess) } - const agent = new AtpAgent({ service: network.pds.url, persistSession }) + const dispatcher = new SessionDispatcher({ + service: network.pds.url, + persistSession, + }) // create an account and a session with it - await agent.createAccount({ + await dispatcher.createAccount({ handle: 'user4.test', email: 'user4@test.com', password: 'password', }) - if (!agent.session) { + if (!dispatcher.session) { throw new Error('No session created') } - const session1 = agent.session + const session1 = dispatcher.session const origAccessJwt = session1.accessJwt // wait 1 second so that a token refresh will issue a new access token @@ -203,42 +218,38 @@ describe('agent', () => { await new Promise((r) => setTimeout(r, 1000)) // patch the fetch handler to fake an expired token error on the next request - const tokenExpiredFetchHandler = async function ( - httpUri: string, - httpMethod: string, - httpHeaders: Record, - httpReqBody: unknown, - ): Promise { - if (httpHeaders.authorization === `Bearer ${origAccessJwt}`) { - return { + dispatcher.setFetchHandler(async (req) => { + if ( + req.headers.get('authorization') === `Bearer ${origAccessJwt}` && + !req.url.includes('com.atproto.server.refreshSession') + ) { + return new Response(JSON.stringify({ error: 'ExpiredToken' }), { status: 400, - headers: {}, - body: { error: 'ExpiredToken' }, - } + headers: { 'Content-Type': 'application/json' }, + }) } - return defaultFetchHandler(httpUri, httpMethod, httpHeaders, httpReqBody) - } + + return globalThis.fetch(req) + }) // put the agent through the auth flow - AtpAgent.configure({ fetch: tokenExpiredFetchHandler }) - const res1 = await createPost(agent) - AtpAgent.configure({ fetch: defaultFetchHandler }) + const res1 = await createPost(dispatcher) expect(res1.success).toEqual(true) - expect(agent.hasSession).toEqual(true) - expect(agent.session?.accessJwt).not.toEqual(session1.accessJwt) - expect(agent.session?.refreshJwt).not.toEqual(session1.refreshJwt) - expect(agent.session?.handle).toEqual(session1.handle) - expect(agent.session?.did).toEqual(session1.did) - expect(agent.session?.email).toEqual(session1.email) - expect(agent.session?.emailConfirmed).toEqual(session1.emailConfirmed) + expect(dispatcher.hasSession).toEqual(true) + expect(dispatcher.session?.accessJwt).not.toEqual(session1.accessJwt) + expect(dispatcher.session?.refreshJwt).not.toEqual(session1.refreshJwt) + expect(dispatcher.session?.handle).toEqual(session1.handle) + expect(dispatcher.session?.did).toEqual(session1.did) + expect(dispatcher.session?.email).toEqual(session1.email) + expect(dispatcher.session?.emailConfirmed).toEqual(session1.emailConfirmed) expect(events.length).toEqual(2) expect(events[0]).toEqual('create') expect(events[1]).toEqual('update') expect(sessions.length).toEqual(2) expect(sessions[0]?.accessJwt).toEqual(origAccessJwt) - expect(sessions[1]?.accessJwt).toEqual(agent.session?.accessJwt) + expect(sessions[1]?.accessJwt).toEqual(dispatcher.session?.accessJwt) }) it('dedupes session refreshes.', async () => { @@ -249,18 +260,21 @@ describe('agent', () => { sessions.push(sess) } - const agent = new AtpAgent({ service: network.pds.url, persistSession }) + const dispatcher = new SessionDispatcher({ + service: network.pds.url, + persistSession, + }) // create an account and a session with it - await agent.createAccount({ + await dispatcher.createAccount({ handle: 'user5.test', email: 'user5@test.com', password: 'password', }) - if (!agent.session) { + if (!dispatcher.session) { throw new Error('No session created') } - const session1 = agent.session + const session1 = dispatcher.session const origAccessJwt = session1.accessJwt // wait 1 second so that a token refresh will issue a new access token @@ -270,54 +284,46 @@ describe('agent', () => { // patch the fetch handler to fake an expired token error on the next request let expiredCalls = 0 let refreshCalls = 0 - const tokenExpiredFetchHandler = async function ( - httpUri: string, - httpMethod: string, - httpHeaders: Record, - httpReqBody: unknown, - ): Promise { - if (httpHeaders.authorization === `Bearer ${origAccessJwt}`) { + dispatcher.setFetchHandler(async (req) => { + if (req.headers.get('authorization') === `Bearer ${origAccessJwt}`) { expiredCalls++ - return { + return new Response(JSON.stringify({ error: 'ExpiredToken' }), { status: 400, - headers: {}, - body: { error: 'ExpiredToken' }, - } + headers: { 'Content-Type': 'application/json' }, + }) } - if (httpUri.includes('com.atproto.server.refreshSession')) { + if (req.url.includes('com.atproto.server.refreshSession')) { refreshCalls++ } - return defaultFetchHandler(httpUri, httpMethod, httpHeaders, httpReqBody) - } + return globalThis.fetch(req) + }) // put the agent through the auth flow - AtpAgent.configure({ fetch: tokenExpiredFetchHandler }) const [res1, res2, res3] = await Promise.all([ - createPost(agent), - createPost(agent), - createPost(agent), + createPost(dispatcher), + createPost(dispatcher), + createPost(dispatcher), ]) - AtpAgent.configure({ fetch: defaultFetchHandler }) expect(expiredCalls).toEqual(3) expect(refreshCalls).toEqual(1) expect(res1.success).toEqual(true) expect(res2.success).toEqual(true) expect(res3.success).toEqual(true) - expect(agent.hasSession).toEqual(true) - expect(agent.session?.accessJwt).not.toEqual(session1.accessJwt) - expect(agent.session?.refreshJwt).not.toEqual(session1.refreshJwt) - expect(agent.session?.handle).toEqual(session1.handle) - expect(agent.session?.did).toEqual(session1.did) - expect(agent.session?.email).toEqual(session1.email) - expect(agent.session?.emailConfirmed).toEqual(session1.emailConfirmed) + expect(dispatcher.hasSession).toEqual(true) + expect(dispatcher.session?.accessJwt).not.toEqual(session1.accessJwt) + expect(dispatcher.session?.refreshJwt).not.toEqual(session1.refreshJwt) + expect(dispatcher.session?.handle).toEqual(session1.handle) + expect(dispatcher.session?.did).toEqual(session1.did) + expect(dispatcher.session?.email).toEqual(session1.email) + expect(dispatcher.session?.emailConfirmed).toEqual(session1.emailConfirmed) expect(events.length).toEqual(2) expect(events[0]).toEqual('create') expect(events[1]).toEqual('update') expect(sessions.length).toEqual(2) expect(sessions[0]?.accessJwt).toEqual(origAccessJwt) - expect(sessions[1]?.accessJwt).toEqual(agent.session?.accessJwt) + expect(sessions[1]?.accessJwt).toEqual(dispatcher.session?.accessJwt) }) it('persists an empty session on login and resumeSession failures', async () => { @@ -328,20 +334,23 @@ describe('agent', () => { sessions.push(sess) } - const agent = new AtpAgent({ service: network.pds.url, persistSession }) + const dispatcher = new SessionDispatcher({ + service: network.pds.url, + persistSession, + }) try { - await agent.login({ + await dispatcher.login({ identifier: 'baduser.test', password: 'password', }) } catch (_e: any) { // ignore } - expect(agent.hasSession).toEqual(false) + expect(dispatcher.hasSession).toEqual(false) try { - await agent.resumeSession({ + await dispatcher.resumeSession({ accessJwt: 'bad', refreshJwt: 'bad', did: 'bad', @@ -350,7 +359,7 @@ describe('agent', () => { } catch (_e: any) { // ignore } - expect(agent.hasSession).toEqual(false) + expect(dispatcher.hasSession).toEqual(false) expect(events.length).toEqual(2) expect(events[0]).toEqual('create-failed') @@ -368,47 +377,40 @@ describe('agent', () => { sessions.push(sess) } - const agent = new AtpAgent({ service: network.pds.url, persistSession }) + const dispatcher = new SessionDispatcher({ + service: network.pds.url, + persistSession, + }) // create an account and a session with it - await agent.createAccount({ + await dispatcher.createAccount({ handle: 'user6.test', email: 'user6@test.com', password: 'password', }) - if (!agent.session) { + if (!dispatcher.session) { throw new Error('No session created') } - const session1 = agent.session + const session1 = dispatcher.session const origAccessJwt = session1.accessJwt // patch the fetch handler to fake an expired token error on the next request - const tokenExpiredFetchHandler = async function ( - httpUri: string, - httpMethod: string, - httpHeaders: Record, - httpReqBody: unknown, - ): Promise { - if (httpHeaders.authorization === `Bearer ${origAccessJwt}`) { - return { + dispatcher.setFetchHandler(async (req) => { + if (req.headers.get('authorization') === `Bearer ${origAccessJwt}`) { + return new Response(JSON.stringify({ error: 'ExpiredToken' }), { status: 400, - headers: {}, - body: { error: 'ExpiredToken' }, - } + headers: { 'Content-Type': 'application/json' }, + }) } - if (httpUri.includes('com.atproto.server.refreshSession')) { - return { - status: 500, - headers: {}, - body: undefined, - } + if (req.url.includes('com.atproto.server.refreshSession')) { + return new Response(undefined, { status: 500 }) } - return defaultFetchHandler(httpUri, httpMethod, httpHeaders, httpReqBody) - } + return globalThis.fetch(req) + }) // put the agent through the auth flow - AtpAgent.configure({ fetch: tokenExpiredFetchHandler }) try { + const agent = new AtpAgent(dispatcher) await agent.api.app.bsky.feed.getTimeline() throw new Error('Should have failed') } catch (e: any) { @@ -416,10 +418,9 @@ describe('agent', () => { expect(e.status).toEqual(400) expect(e.error).toEqual('ExpiredToken') } - AtpAgent.configure({ fetch: defaultFetchHandler }) // still has session because it wasn't invalidated - expect(agent.hasSession).toEqual(true) + expect(dispatcher.hasSession).toEqual(true) expect(events.length).toEqual(1) expect(events[0]).toEqual('create') @@ -439,9 +440,12 @@ describe('agent', () => { newHandlerCallCount++ } - const agent = new AtpAgent({ service: network.pds.url, persistSession }) + const dispatcher = new SessionDispatcher({ + service: network.pds.url, + persistSession, + }) - await agent.createAccount({ + await dispatcher.createAccount({ handle: 'user7.test', email: 'user7@test.com', password: 'password', @@ -449,10 +453,10 @@ describe('agent', () => { expect(originalHandlerCallCount).toEqual(1) - agent.setPersistSessionHandler(newPersistSession) - agent.session = undefined + dispatcher.setPersistSessionHandler(newPersistSession) + dispatcher.session = undefined - await agent.createAccount({ + await dispatcher.createAccount({ handle: 'user8.test', email: 'user8@test.com', password: 'password', @@ -472,18 +476,21 @@ describe('agent', () => { sessions.push(sess) } - const agent = new AtpAgent({ service: network.pds.url, persistSession }) + const dispatcher = new SessionDispatcher({ + service: network.pds.url, + persistSession, + }) await expect( - agent.createAccount({ + dispatcher.createAccount({ handle: '', email: '', password: 'password', }), ).rejects.toThrow() - expect(agent.hasSession).toEqual(false) - expect(agent.session).toEqual(undefined) + expect(dispatcher.hasSession).toEqual(false) + expect(dispatcher.session).toEqual(undefined) expect(events.length).toEqual(1) expect(events[0]).toEqual('create-failed') expect(sessions.length).toEqual(1) @@ -495,20 +502,20 @@ describe('agent', () => { it('adds the labelers header as expected', async () => { const port = await getPort() const server = await createHeaderEchoServer(port) - const agent = new AtpAgent({ service: `http://localhost:${port}` }) - const agent2 = new AtpAgent({ service: `http://localhost:${port}` }) + const dispatcher = new AtpAgent({ service: `http://localhost:${port}` }) + const dispatcher2 = new AtpAgent({ service: `http://localhost:${port}` }) - const res1 = await agent.com.atproto.server.describeServer() + const res1 = await dispatcher.com.atproto.server.describeServer() expect(res1.data['atproto-accept-labelers']).toEqual( `${BSKY_LABELER_DID};redact`, ) AtpAgent.configure({ appLabelers: ['did:plc:test1', 'did:plc:test2'] }) - const res2 = await agent.com.atproto.server.describeServer() + const res2 = await dispatcher.com.atproto.server.describeServer() expect(res2.data['atproto-accept-labelers']).toEqual( 'did:plc:test1;redact, did:plc:test2;redact', ) - const res3 = await agent2.com.atproto.server.describeServer() + const res3 = await dispatcher2.com.atproto.server.describeServer() expect(res3.data['atproto-accept-labelers']).toEqual( 'did:plc:test1;redact, did:plc:test2;redact', ) @@ -522,16 +529,16 @@ describe('agent', () => { it('adds the labelers header as expected', async () => { const port = await getPort() const server = await createHeaderEchoServer(port) - const agent = new AtpAgent({ service: `http://localhost:${port}` }) + const dispatcher = new AtpAgent({ service: `http://localhost:${port}` }) - agent.configureLabelersHeader(['did:plc:test1']) - const res1 = await agent.com.atproto.server.describeServer() + dispatcher.configureLabelersHeader(['did:plc:test1']) + const res1 = await dispatcher.com.atproto.server.describeServer() expect(res1.data['atproto-accept-labelers']).toEqual( `${BSKY_LABELER_DID};redact, did:plc:test1`, ) - agent.configureLabelersHeader(['did:plc:test1', 'did:plc:test2']) - const res2 = await agent.com.atproto.server.describeServer() + dispatcher.configureLabelersHeader(['did:plc:test1', 'did:plc:test2']) + const res2 = await dispatcher.com.atproto.server.describeServer() expect(res2.data['atproto-accept-labelers']).toEqual( `${BSKY_LABELER_DID};redact, did:plc:test1, did:plc:test2`, ) @@ -544,18 +551,18 @@ describe('agent', () => { it('adds the proxy header as expected', async () => { const port = await getPort() const server = await createHeaderEchoServer(port) - const agent = new AtpAgent({ service: `http://localhost:${port}` }) + const dispatcher = new AtpAgent({ service: `http://localhost:${port}` }) - const res1 = await agent.com.atproto.server.describeServer() + const res1 = await dispatcher.com.atproto.server.describeServer() expect(res1.data['atproto-proxy']).toBeFalsy() - agent.configureProxyHeader('atproto_labeler', 'did:plc:test1') - const res2 = await agent.com.atproto.server.describeServer() + dispatcher.configureProxyHeader('atproto_labeler', 'did:plc:test1') + const res2 = await dispatcher.com.atproto.server.describeServer() expect(res2.data['atproto-proxy']).toEqual( 'did:plc:test1#atproto_labeler', ) - const res3 = await agent + const res3 = await dispatcher .withProxy('atproto_labeler', 'did:plc:test2') .com.atproto.server.describeServer() expect(res3.data['atproto-proxy']).toEqual( @@ -567,9 +574,10 @@ describe('agent', () => { }) }) -const createPost = async (agent: AtpAgent) => { +const createPost = async (dispatcher: SessionDispatcher) => { + const agent = new AtpAgent(dispatcher) return agent.api.com.atproto.repo.createRecord({ - repo: agent.session?.did ?? '', + repo: await agent.getDid(), collection: 'app.bsky.feed.post', record: { text: 'hello there', diff --git a/packages/api/tests/moderation-prefs.test.ts b/packages/api/tests/moderation-prefs.test.ts index 46bd6268a01..5af8c81f90d 100644 --- a/packages/api/tests/moderation-prefs.test.ts +++ b/packages/api/tests/moderation-prefs.test.ts @@ -1,5 +1,5 @@ import { TestNetworkNoAppView } from '@atproto/dev-env' -import { BskyAgent, DEFAULT_LABEL_SETTINGS } from '..' +import { BskyAgent, SessionDispatcher, DEFAULT_LABEL_SETTINGS } from '../src' import './util/moderation-behavior' describe('agent', () => { @@ -16,14 +16,18 @@ describe('agent', () => { }) it('migrates legacy content-label prefs (no mutations)', async () => { - const agent = new BskyAgent({ service: network.pds.url }) + const xrpcAgent = new SessionDispatcher({ + service: network.pds.url, + }) - await agent.createAccount({ + await xrpcAgent.createAccount({ handle: 'user1.test', email: 'user1@test.com', password: 'password', }) + const agent = new BskyAgent(xrpcAgent) + await agent.app.bsky.actor.putPreferences({ preferences: [ { @@ -87,14 +91,18 @@ describe('agent', () => { }) it('adds/removes moderation services', async () => { - const agent = new BskyAgent({ service: network.pds.url }) + const xrpcAgent = new SessionDispatcher({ + service: network.pds.url, + }) - await agent.createAccount({ + await xrpcAgent.createAccount({ handle: 'user5.test', email: 'user5@test.com', password: 'password', }) + const agent = new BskyAgent(xrpcAgent) + await agent.addLabeler('did:plc:other') expect(agent.labelersHeader).toStrictEqual(['did:plc:other']) await expect(agent.getPreferences()).resolves.toStrictEqual({ @@ -165,14 +173,18 @@ describe('agent', () => { }) it('sets label preferences globally and per-moderator', async () => { - const agent = new BskyAgent({ service: network.pds.url }) + const xrpcAgent = new SessionDispatcher({ + service: network.pds.url, + }) - await agent.createAccount({ + await xrpcAgent.createAccount({ handle: 'user7.test', email: 'user7@test.com', password: 'password', }) + const agent = new BskyAgent(xrpcAgent) + await agent.addLabeler('did:plc:other') await agent.setContentLabelPref('porn', 'ignore') await agent.setContentLabelPref('porn', 'hide', 'did:plc:other') @@ -216,14 +228,18 @@ describe('agent', () => { }) it(`updates label pref`, async () => { - const agent = new BskyAgent({ service: network.pds.url }) + const xrpcAgent = new SessionDispatcher({ + service: network.pds.url, + }) - await agent.createAccount({ + await xrpcAgent.createAccount({ handle: 'user8.test', email: 'user8@test.com', password: 'password', }) + const agent = new BskyAgent(xrpcAgent) + await agent.addLabeler('did:plc:other') await agent.setContentLabelPref('porn', 'ignore') await agent.setContentLabelPref('porn', 'ignore', 'did:plc:other') @@ -240,14 +256,18 @@ describe('agent', () => { }) it(`double-write for legacy: 'graphic-media' in sync with 'gore'`, async () => { - const agent = new BskyAgent({ service: network.pds.url }) + const xrpcAgent = new SessionDispatcher({ + service: network.pds.url, + }) - await agent.createAccount({ + await xrpcAgent.createAccount({ handle: 'user9.test', email: 'user9@test.com', password: 'password', }) + const agent = new BskyAgent(xrpcAgent) + await agent.setContentLabelPref('graphic-media', 'hide') const a = await agent.getPreferences() @@ -262,14 +282,18 @@ describe('agent', () => { }) it(`double-write for legacy: 'porn' in sync with 'nsfw'`, async () => { - const agent = new BskyAgent({ service: network.pds.url }) + const xrpcAgent = new SessionDispatcher({ + service: network.pds.url, + }) - await agent.createAccount({ + await xrpcAgent.createAccount({ handle: 'user10.test', email: 'user10@test.com', password: 'password', }) + const agent = new BskyAgent(xrpcAgent) + await agent.setContentLabelPref('porn', 'hide') const a = await agent.getPreferences() @@ -284,14 +308,18 @@ describe('agent', () => { }) it(`double-write for legacy: 'sexual' in sync with 'suggestive'`, async () => { - const agent = new BskyAgent({ service: network.pds.url }) + const xrpcAgent = new SessionDispatcher({ + service: network.pds.url, + }) - await agent.createAccount({ + await xrpcAgent.createAccount({ handle: 'user11.test', email: 'user11@test.com', password: 'password', }) + const agent = new BskyAgent(xrpcAgent) + await agent.setContentLabelPref('sexual', 'hide') const a = await agent.getPreferences() @@ -306,14 +334,18 @@ describe('agent', () => { }) it(`double-write for legacy: filters out existing old label pref if double-written`, async () => { - const agent = new BskyAgent({ service: network.pds.url }) + const xrpcAgent = new SessionDispatcher({ + service: network.pds.url, + }) - await agent.createAccount({ + await xrpcAgent.createAccount({ handle: 'user12.test', email: 'user12@test.com', password: 'password', }) + const agent = new BskyAgent(xrpcAgent) + await agent.setContentLabelPref('nsfw', 'hide') await agent.setContentLabelPref('porn', 'hide') const a = await agent.app.bsky.actor.getPreferences({}) @@ -325,14 +357,18 @@ describe('agent', () => { }) it(`remaps old values to new on read`, async () => { - const agent = new BskyAgent({ service: network.pds.url }) + const xrpcAgent = new SessionDispatcher({ + service: network.pds.url, + }) - await agent.createAccount({ + await xrpcAgent.createAccount({ handle: 'user13.test', email: 'user13@test.com', password: 'password', }) + const agent = new BskyAgent(xrpcAgent) + await agent.setContentLabelPref('nsfw', 'hide') await agent.setContentLabelPref('gore', 'hide') await agent.setContentLabelPref('suggestive', 'hide') diff --git a/packages/api/tests/util/index.ts b/packages/api/tests/util/index.ts deleted file mode 100644 index 50334e8daf8..00000000000 --- a/packages/api/tests/util/index.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { AtpAgentFetchHandlerResponse } from '../../src' - -export async function fetchHandler( - httpUri: string, - httpMethod: string, - httpHeaders: Record, - httpReqBody: unknown, -): Promise { - // The duplex field is now required for streaming bodies, but not yet reflected - // anywhere in docs or types. See whatwg/fetch#1438, nodejs/node#46221. - const reqInit: RequestInit & { duplex: string } = { - method: httpMethod, - headers: httpHeaders, - body: httpReqBody - ? new TextEncoder().encode(JSON.stringify(httpReqBody)) - : undefined, - duplex: 'half', - } - const res = await fetch(httpUri, reqInit) - const resBody = await res.arrayBuffer() - return { - status: res.status, - headers: Object.fromEntries(res.headers.entries()), - body: resBody ? JSON.parse(new TextDecoder().decode(resBody)) : undefined, - } -} diff --git a/packages/bsky/src/index.ts b/packages/bsky/src/index.ts index 0f3c07253c6..a60ffc104e1 100644 --- a/packages/bsky/src/index.ts +++ b/packages/bsky/src/index.ts @@ -5,7 +5,7 @@ import events from 'events' import { createHttpTerminator, HttpTerminator } from 'http-terminator' import cors from 'cors' import compression from 'compression' -import AtpAgent from '@atproto/api' +import { AtpAgent } from '@atproto/api' import { IdResolver } from '@atproto/identity' import API, { health, wellKnown, blobResolver } from './api' import * as error from './error' @@ -76,14 +76,14 @@ export class BskyAppView { : undefined const suggestionsAgent = config.suggestionsUrl - ? new AtpAgent({ service: config.suggestionsUrl }) + ? new AtpAgent({ + service: config.suggestionsUrl, + headers: { + authorization: + config.suggestionsApiKey && `Bearer ${config.suggestionsApiKey}`, + }, + }) : undefined - if (suggestionsAgent && config.suggestionsApiKey) { - suggestionsAgent.api.setHeader( - 'authorization', - `Bearer ${config.suggestionsApiKey}`, - ) - } const dataplane = createDataPlaneClient(config.dataplaneUrls, { httpVersion: config.dataplaneHttpVersion, diff --git a/packages/bsky/tests/data-plane/indexing.test.ts b/packages/bsky/tests/data-plane/indexing.test.ts index 6703290cd3b..d67fb68571a 100644 --- a/packages/bsky/tests/data-plane/indexing.test.ts +++ b/packages/bsky/tests/data-plane/indexing.test.ts @@ -10,6 +10,7 @@ import AtpAgent, { AppBskyFeedLike, AppBskyFeedRepost, AppBskyGraphFollow, + SessionDispatcher, } from '@atproto/api' import { TestNetwork, SeedClient, usersSeed, basicSeed } from '@atproto/dev-env' import { forSnapshot } from '../_util' @@ -546,10 +547,12 @@ describe('indexing', () => { it('indexes handle for a fresh did', async () => { const now = new Date().toISOString() - const sessionAgent = new AtpAgent({ service: network.pds.url }) + const sessionDispatcher = new SessionDispatcher({ + service: network.pds.url, + }) const { data: { did }, - } = await sessionAgent.createAccount({ + } = await sessionDispatcher.createAccount({ email: 'did1@test.com', handle: 'did1.test', password: 'password', @@ -561,10 +564,13 @@ describe('indexing', () => { it('reindexes handle for existing did when forced', async () => { const now = new Date().toISOString() - const sessionAgent = new AtpAgent({ service: network.pds.url }) + const sessionDispatcher = new SessionDispatcher({ + service: network.pds.url, + }) + const sessionAgent = new AtpAgent(sessionDispatcher) const { data: { did }, - } = await sessionAgent.createAccount({ + } = await sessionDispatcher.createAccount({ email: 'did2@test.com', handle: 'did2.test', password: 'password', @@ -582,10 +588,12 @@ describe('indexing', () => { it('handles profile aggregations out of order', async () => { const now = new Date().toISOString() - const sessionAgent = new AtpAgent({ service: network.pds.url }) + const sessionDispatcher = new SessionDispatcher({ + service: network.pds.url, + }) const { data: { did }, - } = await sessionAgent.createAccount({ + } = await sessionDispatcher.createAccount({ email: 'did3@test.com', handle: 'did3.test', password: 'password', diff --git a/packages/dev-env/src/agent.ts b/packages/dev-env/src/agent.ts new file mode 100644 index 00000000000..550624afb62 --- /dev/null +++ b/packages/dev-env/src/agent.ts @@ -0,0 +1,36 @@ +import AtpAgent, { + AtpSessionManager, + AtpSessionManagerOptions, +} from '@atproto/api' +import { EXAMPLE_LABELER } from './const' + +export class TestAgent extends AtpAgent { + readonly sessionManager: AtpSessionManager + + constructor(options: AtpSessionManagerOptions) { + const sessionManager = new AtpSessionManager(options) + super(sessionManager) + this.sessionManager = sessionManager + this.configureLabelersHeader([EXAMPLE_LABELER]) + } + + get session() { + return this.sessionManager.session + } + + get hasSession() { + return this.sessionManager.hasSession + } + + get service() { + return this.sessionManager.serviceUrl + } + + login(...args: Parameters) { + return this.sessionManager.login(...args) + } + + createAccount(...args: Parameters) { + return this.sessionManager.createAccount(...args) + } +} diff --git a/packages/dev-env/src/bsky.ts b/packages/dev-env/src/bsky.ts index c4a5c398443..559eb49db62 100644 --- a/packages/dev-env/src/bsky.ts +++ b/packages/dev-env/src/bsky.ts @@ -1,12 +1,12 @@ import getPort from 'get-port' import * as ui8 from 'uint8arrays' import * as bsky from '@atproto/bsky' -import { AtpAgent } from '@atproto/api' import { Secp256k1Keypair } from '@atproto/crypto' import { BackgroundQueue } from '@atproto/bsky' import { Client as PlcClient } from '@did-plc/lib' import { BskyConfig } from './types' import { ADMIN_PASSWORD, EXAMPLE_LABELER } from './const' +import { TestAgent } from './agent' export * from '@atproto/bsky' @@ -106,9 +106,7 @@ export class TestBsky { } getClient() { - const agent = new AtpAgent({ service: this.url }) - agent.configureLabelersHeader([EXAMPLE_LABELER]) - return agent + return new TestAgent({ service: this.url }) } adminAuth(): string { diff --git a/packages/dev-env/src/index.ts b/packages/dev-env/src/index.ts index 5ddb61711e0..2eafe15728d 100644 --- a/packages/dev-env/src/index.ts +++ b/packages/dev-env/src/index.ts @@ -1,3 +1,4 @@ +export * from './agent' export * from './bsky' export * from './bsync' export * from './network' diff --git a/packages/dev-env/src/ozone-service-profile.ts b/packages/dev-env/src/ozone-service-profile.ts index 325e95ee1ce..48ebb0fb79c 100644 --- a/packages/dev-env/src/ozone-service-profile.ts +++ b/packages/dev-env/src/ozone-service-profile.ts @@ -1,11 +1,11 @@ import { TestPds } from './pds' -import { AtpAgent } from '@atproto/api' import { Secp256k1Keypair } from '@atproto/crypto' +import { TestAgent } from './agent' export class OzoneServiceProfile { did?: string key?: Secp256k1Keypair - thirdPartyPdsClient: AtpAgent + thirdPartyPdsClient: TestAgent modUserDetails = { email: 'mod-authority@test.com', diff --git a/packages/dev-env/src/ozone.ts b/packages/dev-env/src/ozone.ts index 8054170f950..7983fe9d9fb 100644 --- a/packages/dev-env/src/ozone.ts +++ b/packages/dev-env/src/ozone.ts @@ -2,13 +2,13 @@ import getPort from 'get-port' import * as ui8 from 'uint8arrays' import * as plc from '@did-plc/lib' import * as ozone from '@atproto/ozone' -import { AtpAgent } from '@atproto/api' import { createServiceJwt } from '@atproto/xrpc-server' import { Keypair, Secp256k1Keypair } from '@atproto/crypto' import { DidAndKey, OzoneConfig } from './types' import { ADMIN_PASSWORD } from './const' import { createDidAndKey } from './util' import { ModeratorClient } from './moderator-client' +import { TestAgent } from './agent' export class TestOzone { constructor( @@ -104,7 +104,7 @@ export class TestOzone { } getClient() { - return new AtpAgent({ service: this.url }) + return new TestAgent({ service: this.url }) } getModClient() { diff --git a/packages/dev-env/src/pds.ts b/packages/dev-env/src/pds.ts index 2eda6695a32..fa574f5d09c 100644 --- a/packages/dev-env/src/pds.ts +++ b/packages/dev-env/src/pds.ts @@ -6,9 +6,9 @@ import * as ui8 from 'uint8arrays' import * as pds from '@atproto/pds' import { createSecretKeyObject } from '@atproto/pds' import { Secp256k1Keypair, randomStr } from '@atproto/crypto' -import { AtpAgent } from '@atproto/api' import { PdsConfig } from './types' -import { ADMIN_PASSWORD, EXAMPLE_LABELER, JWT_SECRET } from './const' +import { ADMIN_PASSWORD, JWT_SECRET } from './const' +import { TestAgent } from './agent' export class TestPds { constructor( @@ -72,10 +72,8 @@ export class TestPds { return this.server.ctx } - getClient(): AtpAgent { - const agent = new AtpAgent({ service: this.url }) - agent.configureLabelersHeader([EXAMPLE_LABELER]) - return agent + getClient() { + return new TestAgent({ service: this.url }) } adminAuth(): string { diff --git a/packages/lex-cli/src/codegen/client.ts b/packages/lex-cli/src/codegen/client.ts index bf7c8892819..53daf6dd597 100644 --- a/packages/lex-cli/src/codegen/client.ts +++ b/packages/lex-cli/src/codegen/client.ts @@ -61,13 +61,14 @@ const indexTs = ( nsidTokens: Record, ) => gen(project, '/index.ts', async (file) => { - //= import {Client as XrpcClient, AtpServiceClient as XrpcServiceClient} from '@atproto/xrpc' + //= import { XrpcClient, FetchHandler, FetchHandlerOptions } from '@atproto/xrpc' const xrpcImport = file.addImportDeclaration({ moduleSpecifier: '@atproto/xrpc', }) xrpcImport.addNamedImports([ - { name: 'Client', alias: 'XrpcClient' }, - { name: 'ServiceClient', alias: 'XrpcServiceClient' }, + { name: 'XrpcClient' }, + { name: 'FetchHandler' }, + { name: 'FetchHandlerOptions' }, ]) //= import {schemas} from './lexicons' file @@ -115,91 +116,48 @@ const indexTs = ( }) } - //= export class AtpBaseClient {...} - const clientCls = file.addClass({ - name: 'AtpBaseClient', + //= export class AtpClient {...} + const atpClientCls = file.addClass({ + name: 'AtpClient', isExported: true, + extends: 'XrpcClient', }) - //= xrpc: XrpcClient = new XrpcClient() - clientCls.addProperty({ - name: 'xrpc', - type: 'XrpcClient', - initializer: 'new XrpcClient()', - }) - //= constructor () { - //= this.xrpc.addLexicons(schemas) - //= } - clientCls.addConstructor().setBodyText(`this.xrpc.addLexicons(schemas)`) - //= service(serviceUri: string | URL): AtpServiceClient { - //= return new AtpServiceClient(this, this.xrpc.service(serviceUri)) - //= } - clientCls - .addMethod({ - name: 'service', - parameters: [{ name: 'serviceUri', type: 'string | URL' }], - returnType: 'AtpServiceClient', - }) - .setBodyText( - `return new AtpServiceClient(this, this.xrpc.service(serviceUri))`, - ) - //= export class AtpServiceClient {...} - const serviceClientCls = file.addClass({ - name: 'AtpServiceClient', - isExported: true, - }) - //= _baseClient: AtpBaseClient - serviceClientCls.addProperty({ name: '_baseClient', type: 'AtpBaseClient' }) - //= xrpc: XrpcServiceClient - serviceClientCls.addProperty({ - name: 'xrpc', - type: 'XrpcServiceClient', - }) for (const ns of nsidTree) { //= ns: NS - serviceClientCls.addProperty({ + atpClientCls.addProperty({ name: ns.propName, type: ns.className, }) } - //= constructor (baseClient: AtpBaseClient, xrpcService: XrpcServiceClient) { - //= this.baseClient = baseClient - //= this.xrpcService = xrpcService + + //= constructor (options: FetchHandler | FetchHandlerOptions) { + //= super(options, schemas) //= {namespace declarations} //= } - serviceClientCls - .addConstructor({ - parameters: [ - { name: 'baseClient', type: 'AtpBaseClient' }, - { name: 'xrpcService', type: 'XrpcServiceClient' }, - ], - }) - .setBodyText( - [ - `this._baseClient = baseClient`, - `this.xrpc = xrpcService`, - ...nsidTree.map( - (ns) => `this.${ns.propName} = new ${ns.className}(this)`, - ), - ].join('\n'), - ) + atpClientCls.addConstructor({ + parameters: [ + { name: 'options', type: 'FetchHandler | FetchHandlerOptions' }, + ], + statements: [ + 'super(options, schemas)', + ...nsidTree.map( + (ns) => `this.${ns.propName} = new ${ns.className}(this)`, + ), + ], + }) - //= setHeader(key: string, value: string): void { - //= this.xrpc.setHeader(key, value) + //= /** @deprecated use `this` instead */ + //= get xrpc(): XrpcClient { + //= return this //= } - const setHeaderMethod = serviceClientCls.addMethod({ - name: 'setHeader', - returnType: 'void', - }) - setHeaderMethod.addParameter({ - name: 'key', - type: 'string', - }) - setHeaderMethod.addParameter({ - name: 'value', - type: 'string', - }) - setHeaderMethod.setBodyText('this.xrpc.setHeader(key, value)') + atpClientCls + .addGetAccessor({ + name: 'xrpc', + returnType: 'XrpcClient', + statements: ['return this'], + }) + .addJsDoc('@deprecated use `this` instead') // generate classes for the schemas for (const ns of nsidTree) { @@ -213,10 +171,10 @@ function genNamespaceCls(file: SourceFile, ns: DefTreeNode) { name: ns.className, isExported: true, }) - //= _service: AtpServiceClient + //= _client: XrpcClient cls.addProperty({ - name: '_service', - type: 'AtpServiceClient', + name: '_client', + type: 'XrpcClient', }) for (const userType of ns.userTypes) { @@ -242,21 +200,22 @@ function genNamespaceCls(file: SourceFile, ns: DefTreeNode) { genNamespaceCls(file, child) } - //= constructor(service: AtpServiceClient) { - //= this._service = service + //= constructor(public client: XrpcClient) { + //= this._client = client //= {child namespace prop declarations} //= {record prop declarations} //= } - const cons = cls.addConstructor() - cons.addParameter({ - name: 'service', - type: 'AtpServiceClient', - }) - cons.setBodyText( - [ - `this._service = service`, + cls.addConstructor({ + parameters: [ + { + name: 'client', + type: 'XrpcClient', + }, + ], + statements: [ + `this._client = client`, ...ns.children.map( - (ns) => `this.${ns.propName} = new ${ns.className}(service)`, + (ns) => `this.${ns.propName} = new ${ns.className}(client)`, ), ...ns.userTypes .filter((ut) => ut.def.type === 'record') @@ -264,10 +223,10 @@ function genNamespaceCls(file: SourceFile, ns: DefTreeNode) { const name = NSID.parse(ut.nsid).name || '' return `this.${toCamelCase(name)} = new ${toTitleCase( name, - )}Record(service)` + )}Record(client)` }), - ].join('\n'), - ) + ], + }) // methods for (const userType of ns.userTypes) { @@ -298,7 +257,7 @@ function genNamespaceCls(file: SourceFile, ns: DefTreeNode) { }) method.setBodyText( [ - `return this._service.xrpc`, + `return this._client`, isGetReq ? `.call('${userType.nsid}', params, undefined, opts)` : `.call('${userType.nsid}', opts?.qp, data, opts)`, @@ -325,21 +284,21 @@ function genRecordCls(file: SourceFile, nsid: string, lexRecord: LexRecord) { name: `${toTitleCase(name)}Record`, isExported: true, }) - //= _service: AtpServiceClient + //= _client: XrpcClient cls.addProperty({ - name: '_service', - type: 'AtpServiceClient', + name: '_client', + type: 'XrpcClient', }) - //= constructor(service: AtpServiceClient) { - //= this._service = service + //= constructor(client: XrpcClient) { + //= this._client = client //= } const cons = cls.addConstructor() cons.addParameter({ - name: 'service', - type: 'AtpServiceClient', + name: 'client', + type: 'XrpcClient', }) - cons.setBodyText(`this._service = service`) + cons.setBodyText(`this._client = client`) // methods const typeModule = toTitleCase(nsid) @@ -356,7 +315,7 @@ function genRecordCls(file: SourceFile, nsid: string, lexRecord: LexRecord) { }) method.setBodyText( [ - `const res = await this._service.xrpc.call('${ATP_METHODS.list}', { collection: '${nsid}', ...params })`, + `const res = await this._client.call('${ATP_METHODS.list}', { collection: '${nsid}', ...params })`, `return res.data`, ].join('\n'), ) @@ -374,7 +333,7 @@ function genRecordCls(file: SourceFile, nsid: string, lexRecord: LexRecord) { }) method.setBodyText( [ - `const res = await this._service.xrpc.call('${ATP_METHODS.get}', { collection: '${nsid}', ...params })`, + `const res = await this._client.call('${ATP_METHODS.get}', { collection: '${nsid}', ...params })`, `return res.data`, ].join('\n'), ) @@ -406,7 +365,7 @@ function genRecordCls(file: SourceFile, nsid: string, lexRecord: LexRecord) { method.setBodyText( [ `record.$type = '${nsid}'`, - `const res = await this._service.xrpc.call('${ATP_METHODS.create}', undefined, { collection: '${nsid}', ${maybeRkeyPart}...params, record }, {encoding: 'application/json', headers })`, + `const res = await this._client.call('${ATP_METHODS.create}', undefined, { collection: '${nsid}', ${maybeRkeyPart}...params, record }, {encoding: 'application/json', headers })`, `return res.data`, ].join('\n'), ) @@ -433,7 +392,7 @@ function genRecordCls(file: SourceFile, nsid: string, lexRecord: LexRecord) { // method.setBodyText( // [ // `record.$type = '${userType.nsid}'`, - // `const res = await this._service.xrpc.call('${ATP_METHODS.put}', undefined, { collection: '${userType.nsid}', record, ...params }, {encoding: 'application/json', headers})`, + // `const res = await this._client.call('${ATP_METHODS.put}', undefined, { collection: '${userType.nsid}', record, ...params }, {encoding: 'application/json', headers})`, // `return res.data`, // ].join('\n'), // ) @@ -458,7 +417,7 @@ function genRecordCls(file: SourceFile, nsid: string, lexRecord: LexRecord) { method.setBodyText( [ - `await this._service.xrpc.call('${ATP_METHODS.delete}', undefined, { collection: '${nsid}', ...params }, { headers })`, + `await this._client.call('${ATP_METHODS.delete}', undefined, { collection: '${nsid}', ...params }, { headers })`, ].join('\n'), ) } diff --git a/packages/ozone/tests/query-labels.test.ts b/packages/ozone/tests/query-labels.test.ts index 33ae952cf51..6e105080b49 100644 --- a/packages/ozone/tests/query-labels.test.ts +++ b/packages/ozone/tests/query-labels.test.ts @@ -1,5 +1,4 @@ -import AtpAgent from '@atproto/api' -import { EXAMPLE_LABELER, TestNetwork } from '@atproto/dev-env' +import { EXAMPLE_LABELER, TestAgent, TestNetwork } from '@atproto/dev-env' import { DisconnectError, Subscription } from '@atproto/xrpc-server' import { ids, lexicons } from '../src/lexicon/lexicons' import { Label } from '../src/lexicon/types/com/atproto/label/defs' @@ -14,7 +13,7 @@ import { getSigningKeyId } from '../src/util' describe('ozone query labels', () => { let network: TestNetwork - let agent: AtpAgent + let agent: TestAgent let labels: Label[] diff --git a/packages/pds/src/crawlers.ts b/packages/pds/src/crawlers.ts index 5ca20b10621..fb8c5cff840 100644 --- a/packages/pds/src/crawlers.ts +++ b/packages/pds/src/crawlers.ts @@ -6,16 +6,13 @@ import { BackgroundQueue } from './background' const NOTIFY_THRESHOLD = 20 * MINUTE export class Crawlers { - public agents: AtpAgent[] public lastNotified = 0 constructor( public hostname: string, public crawlers: string[], public backgroundQueue: BackgroundQueue, - ) { - this.agents = crawlers.map((service) => new AtpAgent({ service })) - } + ) {} async notifyOfUpdate() { const now = Date.now() @@ -25,16 +22,14 @@ export class Crawlers { this.backgroundQueue.add(async () => { await Promise.all( - this.agents.map(async (agent) => { + this.crawlers.map(async (service) => { try { + const agent = new AtpAgent({ service }) await agent.api.com.atproto.sync.requestCrawl({ hostname: this.hostname, }) } catch (err) { - log.warn( - { err, cralwer: agent.service.toString() }, - 'failed to request crawl', - ) + log.warn({ err, cralwer: service }, 'failed to request crawl') } }), ) diff --git a/packages/pds/tests/account-migration.test.ts b/packages/pds/tests/account-migration.test.ts index beb14599e38..152759d7deb 100644 --- a/packages/pds/tests/account-migration.test.ts +++ b/packages/pds/tests/account-migration.test.ts @@ -1,6 +1,7 @@ -import AtpAgent, { AtUri } from '@atproto/api' +import { AtUri } from '@atproto/api' import { SeedClient, + TestAgent, TestNetworkNoAppView, TestPds, mockNetworkUtilities, @@ -13,8 +14,8 @@ describe('account migration', () => { let newPds: TestPds let sc: SeedClient - let oldAgent: AtpAgent - let newAgent: AtpAgent + let oldAgent: TestAgent + let newAgent: TestAgent let alice: string diff --git a/packages/pds/tests/app-passwords.test.ts b/packages/pds/tests/app-passwords.test.ts index d228896a6ca..25b84e00c3a 100644 --- a/packages/pds/tests/app-passwords.test.ts +++ b/packages/pds/tests/app-passwords.test.ts @@ -1,11 +1,10 @@ -import { TestNetworkNoAppView } from '@atproto/dev-env' -import AtpAgent from '@atproto/api' +import { TestAgent, TestNetworkNoAppView } from '@atproto/dev-env' import * as jose from 'jose' describe('app_passwords', () => { let network: TestNetworkNoAppView - let accntAgent: AtpAgent - let appAgent: AtpAgent + let accntAgent: TestAgent + let appAgent: TestAgent beforeAll(async () => { network = await TestNetworkNoAppView.create({ diff --git a/packages/pds/tests/crud.test.ts b/packages/pds/tests/crud.test.ts index 64d64b204b2..baeee848100 100644 --- a/packages/pds/tests/crud.test.ts +++ b/packages/pds/tests/crud.test.ts @@ -5,7 +5,6 @@ import { BlobRef } from '@atproto/lexicon' import { TestNetworkNoAppView } from '@atproto/dev-env' import { cidForCbor, TID, ui8ToArrayBuffer } from '@atproto/common' import { BlobNotFoundError } from '@atproto/repo' -import { defaultFetchHandler } from '@atproto/xrpc' import * as Post from '../src/lexicon/types/app/bsky/feed/post' import { paginateAll } from './_util' import AppContext from '../src/context' @@ -1074,10 +1073,10 @@ describe('crud operations', () => { it("writes fail on values that can't reliably transform between cbor to lex", async () => { const passthroughBody = (data: unknown) => ui8ToArrayBuffer(new TextEncoder().encode(JSON.stringify(data))) - const result = await defaultFetchHandler( - aliceAgent.service.origin + `/xrpc/com.atproto.repo.createRecord`, - 'post', - { ...aliceAgent.api.xrpc.headers, 'content-type': 'application/json' }, + + const result = aliceAgent.api.call( + 'com.atproto.repo.createRecord', + {}, passthroughBody({ repo: alice.did, collection: 'app.bsky.feed.post', @@ -1087,9 +1086,13 @@ describe('crud operations', () => { deepObject: createDeepObject(3000), }, }), + { + encoding: 'application/json', + }, ) - expect(result.status).toEqual(400) - expect(result.body).toEqual({ + + await expect(result).rejects.toMatchObject({ + status: 400, error: 'InvalidRequest', message: 'Bad record', }) diff --git a/packages/pds/tests/invite-codes.test.ts b/packages/pds/tests/invite-codes.test.ts index f1066feb7d2..6a242cbb3ce 100644 --- a/packages/pds/tests/invite-codes.test.ts +++ b/packages/pds/tests/invite-codes.test.ts @@ -1,6 +1,9 @@ -import AtpAgent, { ComAtprotoServerCreateAccount } from '@atproto/api' +import AtpAgent, { + ComAtprotoServerCreateAccount, + SessionDispatcher, +} from '@atproto/api' import * as crypto from '@atproto/crypto' -import { TestNetworkNoAppView } from '@atproto/dev-env' +import { TestAgent, TestNetworkNoAppView } from '@atproto/dev-env' import { AppContext } from '../src' import { DAY } from '@atproto/common' import { genInvCodes } from '../src/api/com/atproto/server/util' @@ -8,7 +11,7 @@ import { genInvCodes } from '../src/api/com/atproto/server/util' describe('account', () => { let network: TestNetworkNoAppView let ctx: AppContext - let agent: AtpAgent + let agent: TestAgent beforeAll(async () => { network = await TestNetworkNoAppView.create({ @@ -321,7 +324,7 @@ describe('account', () => { const createInviteCode = async ( network: TestNetworkNoAppView, - agent: AtpAgent, + agent: TestAgent, uses: number, forAccount?: string, ): Promise => { @@ -335,7 +338,7 @@ const createInviteCode = async ( return res.data.code } -const createAccountWithInvite = async (agent: AtpAgent, code: string) => { +const createAccountWithInvite = async (agent: TestAgent, code: string) => { const name = crypto.randomStr(5, 'base32') const res = await agent.api.com.atproto.server.createAccount({ email: `${name}@test.com`, @@ -350,7 +353,7 @@ const createAccountWithInvite = async (agent: AtpAgent, code: string) => { } const createAccountsWithInvite = async ( - agent: AtpAgent, + agent: TestAgent, code: string, count = 0, ) => { @@ -361,16 +364,19 @@ const createAccountsWithInvite = async ( const makeLoggedInAccount = async ( network: TestNetworkNoAppView, - agent: AtpAgent, + agent: TestAgent, ): Promise<{ did: string; agent: AtpAgent }> => { const code = await createInviteCode(network, agent, 1) const account = await createAccountWithInvite(agent, code) const did = account.did - const loggedInAgent = new AtpAgent({ service: agent.service.toString() }) - await loggedInAgent.login({ + const dispatcher = new SessionDispatcher({ + service: agent.service.toString(), + }) + await dispatcher.login({ identifier: account.handle, password: account.password, }) + const loggedInAgent = new AtpAgent(dispatcher) return { did, agent: loggedInAgent, diff --git a/packages/pds/tests/races.test.ts b/packages/pds/tests/races.test.ts index d595b2ef397..0282111f731 100644 --- a/packages/pds/tests/races.test.ts +++ b/packages/pds/tests/races.test.ts @@ -1,6 +1,5 @@ -import AtpAgent from '@atproto/api' import { wait } from '@atproto/common' -import { TestNetworkNoAppView } from '@atproto/dev-env' +import { TestAgent, TestNetworkNoAppView } from '@atproto/dev-env' import { readCarWithRoot, verifyRepo } from '@atproto/repo' import AppContext from '../src/context' import { PreparedCreate, prepareCreate } from '../src/repo' @@ -9,7 +8,7 @@ import { Keypair } from '@atproto/crypto' describe('races', () => { let network: TestNetworkNoAppView let ctx: AppContext - let agent: AtpAgent + let agent: TestAgent let did: string let signingKey: Keypair @@ -25,7 +24,7 @@ describe('races', () => { handle: 'alice.test', password: 'alice-pass', }) - did = agent.session?.did || '' + did = await agent.getDid() signingKey = await network.pds.ctx.actorStore.keypair(did) }) diff --git a/packages/xrpc-server/tests/auth.test.ts b/packages/xrpc-server/tests/auth.test.ts index bbd202d1024..e0d5fed2f70 100644 --- a/packages/xrpc-server/tests/auth.test.ts +++ b/packages/xrpc-server/tests/auth.test.ts @@ -7,7 +7,7 @@ import * as ui8 from 'uint8arrays' import { MINUTE } from '@atproto/common' import { Secp256k1Keypair } from '@atproto/crypto' import { LexiconDoc } from '@atproto/lexicon' -import xrpc, { ServiceClient, XRPCError } from '@atproto/xrpc' +import { XrpcClient, XRPCError } from '@atproto/xrpc' import * as xrpcServer from '../src' import { createServer, @@ -62,13 +62,12 @@ describe('Auth', () => { } }, }) - xrpc.addLexicons(LEXICONS) - let client: ServiceClient + let client: XrpcClient beforeAll(async () => { const port = await getPort() s = await createServer(port, server) - client = xrpc.service(`http://localhost:${port}`) + client = new XrpcClient(`http://localhost:${port}`, LEXICONS) }) afterAll(async () => { await closeServer(s) diff --git a/packages/xrpc-server/tests/bodies.test.ts b/packages/xrpc-server/tests/bodies.test.ts index 7a13858cb1c..735ba9f45b9 100644 --- a/packages/xrpc-server/tests/bodies.test.ts +++ b/packages/xrpc-server/tests/bodies.test.ts @@ -4,7 +4,7 @@ import { ReadableStream } from 'stream/web' import { gzipSync } from 'zlib' import getPort from 'get-port' import { LexiconDoc } from '@atproto/lexicon' -import xrpc, { ServiceClient } from '@atproto/xrpc' +import { XrpcClient } from '@atproto/xrpc' import { bytesToStream, cidForCbor } from '@atproto/common' import { randomBytes } from '@atproto/crypto' import { createServer, closeServer } from './_util' @@ -132,15 +132,14 @@ describe('Bodies', () => { } }, ) - xrpc.addLexicons(LEXICONS) - let client: ServiceClient + let client: XrpcClient let url: string beforeAll(async () => { const port = await getPort() s = await createServer(port, server) url = `http://localhost:${port}` - client = xrpc.service(url) + client = new XrpcClient(url, LEXICONS) }) afterAll(async () => { await closeServer(s) diff --git a/packages/xrpc-server/tests/errors.test.ts b/packages/xrpc-server/tests/errors.test.ts index 05da883a771..5628147b428 100644 --- a/packages/xrpc-server/tests/errors.test.ts +++ b/packages/xrpc-server/tests/errors.test.ts @@ -1,14 +1,9 @@ import * as http from 'http' import getPort from 'get-port' import { LexiconDoc } from '@atproto/lexicon' +import { XRPCError, XRPCInvalidResponseError, XrpcClient } from '@atproto/xrpc' import { createServer, closeServer } from './_util' import * as xrpcServer from '../src' -import xrpc, { - Client, - ServiceClient, - XRPCError, - XRPCInvalidResponseError, -} from '@atproto/xrpc' const LEXICONS: LexiconDoc[] = [ { @@ -130,17 +125,14 @@ describe('Errors', () => { server.method('io.example.procedure', () => { return undefined }) - xrpc.addLexicons(LEXICONS) - const badXrpc = new Client() - badXrpc.addLexicons(MISMATCHED_LEXICONS) - let client: ServiceClient - let badClient: ServiceClient + let client: XrpcClient + let badClient: XrpcClient beforeAll(async () => { const port = await getPort() s = await createServer(port, server) - client = xrpc.service(`http://localhost:${port}`) - badClient = badXrpc.service(`http://localhost:${port}`) + client = new XrpcClient(`http://localhost:${port}`, LEXICONS) + badClient = new XrpcClient(`http://localhost:${port}`, MISMATCHED_LEXICONS) }) afterAll(async () => { await closeServer(s) diff --git a/packages/xrpc-server/tests/ipld.test.ts b/packages/xrpc-server/tests/ipld.test.ts index 4ba1fec867e..7d2c4b4cc53 100644 --- a/packages/xrpc-server/tests/ipld.test.ts +++ b/packages/xrpc-server/tests/ipld.test.ts @@ -1,6 +1,6 @@ import * as http from 'http' import { LexiconDoc } from '@atproto/lexicon' -import xrpc, { ServiceClient } from '@atproto/xrpc' +import { XrpcClient } from '@atproto/xrpc' import { CID } from 'multiformats/cid' import getPort from 'get-port' import { createServer, closeServer } from './_util' @@ -63,13 +63,12 @@ describe('Ipld vals', () => { return { encoding: 'application/json', body: ctx.input?.body } }, ) - xrpc.addLexicons(LEXICONS) - let client: ServiceClient + let client: XrpcClient beforeAll(async () => { const port = await getPort() s = await createServer(port, server) - client = xrpc.service(`http://localhost:${port}`) + client = new XrpcClient(`http://localhost:${port}`, LEXICONS) }) afterAll(async () => { await closeServer(s) diff --git a/packages/xrpc-server/tests/parameters.test.ts b/packages/xrpc-server/tests/parameters.test.ts index 6ab2066ae8f..2767d11da92 100644 --- a/packages/xrpc-server/tests/parameters.test.ts +++ b/packages/xrpc-server/tests/parameters.test.ts @@ -1,7 +1,7 @@ import * as http from 'http' import getPort from 'get-port' import { LexiconDoc } from '@atproto/lexicon' -import xrpc, { ServiceClient } from '@atproto/xrpc' +import { XrpcClient } from '@atproto/xrpc' import { createServer, closeServer } from './_util' import * as xrpcServer from '../src' @@ -41,13 +41,12 @@ describe('Parameters', () => { body: ctx.params, }), ) - xrpc.addLexicons(LEXICONS) - let client: ServiceClient + let client: XrpcClient beforeAll(async () => { const port = await getPort() s = await createServer(port, server) - client = xrpc.service(`http://localhost:${port}`) + client = new XrpcClient(`http://localhost:${port}`, LEXICONS) }) afterAll(async () => { await closeServer(s) diff --git a/packages/xrpc-server/tests/procedures.test.ts b/packages/xrpc-server/tests/procedures.test.ts index df38fc855f9..e0130a313d4 100644 --- a/packages/xrpc-server/tests/procedures.test.ts +++ b/packages/xrpc-server/tests/procedures.test.ts @@ -1,7 +1,7 @@ import * as http from 'http' import { Readable } from 'stream' import { LexiconDoc } from '@atproto/lexicon' -import xrpc, { ServiceClient } from '@atproto/xrpc' +import { XrpcClient } from '@atproto/xrpc' import getPort from 'get-port' import { createServer, closeServer } from './_util' import * as xrpcServer from '../src' @@ -121,13 +121,12 @@ describe('Procedures', () => { } }, ) - xrpc.addLexicons(LEXICONS) - let client: ServiceClient + let client: XrpcClient beforeAll(async () => { const port = await getPort() s = await createServer(port, server) - client = xrpc.service(`http://localhost:${port}`) + client = new XrpcClient(`http://localhost:${port}`, LEXICONS) }) afterAll(async () => { await closeServer(s) diff --git a/packages/xrpc-server/tests/queries.test.ts b/packages/xrpc-server/tests/queries.test.ts index fd45d812e00..560b4d98a5f 100644 --- a/packages/xrpc-server/tests/queries.test.ts +++ b/packages/xrpc-server/tests/queries.test.ts @@ -1,7 +1,7 @@ import * as http from 'http' import getPort from 'get-port' import { LexiconDoc } from '@atproto/lexicon' -import xrpc, { ServiceClient } from '@atproto/xrpc' +import { XrpcClient } from '@atproto/xrpc' import { createServer, closeServer } from './_util' import * as xrpcServer from '../src' @@ -89,13 +89,12 @@ describe('Queries', () => { } }, ) - xrpc.addLexicons(LEXICONS) - let client: ServiceClient + let client: XrpcClient beforeAll(async () => { const port = await getPort() s = await createServer(port, server) - client = xrpc.service(`http://localhost:${port}`) + client = new XrpcClient(`http://localhost:${port}`, LEXICONS) }) afterAll(async () => { await closeServer(s) diff --git a/packages/xrpc-server/tests/rate-limiter.test.ts b/packages/xrpc-server/tests/rate-limiter.test.ts index 9a00e37bb81..b9987492540 100644 --- a/packages/xrpc-server/tests/rate-limiter.test.ts +++ b/packages/xrpc-server/tests/rate-limiter.test.ts @@ -1,7 +1,7 @@ import * as http from 'http' import getPort from 'get-port' import { LexiconDoc } from '@atproto/lexicon' -import xrpc, { ServiceClient } from '@atproto/xrpc' +import { XrpcClient } from '@atproto/xrpc' import { createServer, closeServer } from './_util' import * as xrpcServer from '../src' import { RateLimiter } from '../src' @@ -190,13 +190,11 @@ describe('Parameters', () => { }), }) - xrpc.addLexicons(LEXICONS) - - let client: ServiceClient + let client: XrpcClient beforeAll(async () => { const port = await getPort() s = await createServer(port, server) - client = xrpc.service(`http://localhost:${port}`) + client = new XrpcClient(`http://localhost:${port}`, LEXICONS) }) afterAll(async () => { await closeServer(s) diff --git a/packages/xrpc-server/tests/responses.test.ts b/packages/xrpc-server/tests/responses.test.ts index b61467dca22..fbc394e191f 100644 --- a/packages/xrpc-server/tests/responses.test.ts +++ b/packages/xrpc-server/tests/responses.test.ts @@ -1,7 +1,7 @@ import * as http from 'http' import getPort from 'get-port' import { LexiconDoc } from '@atproto/lexicon' -import xrpc, { ServiceClient } from '@atproto/xrpc' +import { XrpcClient } from '@atproto/xrpc' import { byteIterableToStream } from '@atproto/common' import { createServer, closeServer } from './_util' import * as xrpcServer from '../src' @@ -47,15 +47,12 @@ describe('Responses', () => { } }, ) - xrpc.addLexicons(LEXICONS) - let client: ServiceClient - let url: string + let client: XrpcClient beforeAll(async () => { const port = await getPort() s = await createServer(port, server) - url = `http://localhost:${port}` - client = xrpc.service(url) + client = new XrpcClient(`http://localhost:${port}`, LEXICONS) }) afterAll(async () => { await closeServer(s) diff --git a/packages/xrpc/src/client.ts b/packages/xrpc/src/client.ts index 413e429368a..ffbe3eeb900 100644 --- a/packages/xrpc/src/client.ts +++ b/packages/xrpc/src/client.ts @@ -1,28 +1,9 @@ -import { LexiconDoc, Lexicons, ValidationError } from '@atproto/lexicon' -import { - getMethodSchemaHTTPMethod, - constructMethodCallUri, - constructMethodCallHeaders, - encodeMethodCallBody, - httpResponseCodeToEnum, - httpResponseBodyParse, -} from './util' -import { - FetchHandler, - FetchHandlerResponse, - Headers, - CallOptions, - QueryParams, - ResponseType, - errorResponseBody, - ErrorResponseBody, - XRPCResponse, - XRPCError, - XRPCInvalidResponseError, -} from './types' +import { LexiconDoc, Lexicons } from '@atproto/lexicon' +import { CallOptions, QueryParams } from './types' +import { XrpcClient } from './xrpc-client' +/** @deprecated Use {@link XrpcClient} instead */ export class Client { - fetch: FetchHandler = defaultFetchHandler lex = new Lexicons() // method calls @@ -32,7 +13,7 @@ export class Client { serviceUri: string | URL, methodNsid: string, params?: QueryParams, - data?: unknown, + data?: BodyInit | null, opts?: CallOptions, ) { return this.service(serviceUri).call(methodNsid, params, data, opts) @@ -60,111 +41,15 @@ export class Client { } } -export class ServiceClient { - baseClient: Client +/** @deprecated Use {@link XrpcClient} instead */ +export class ServiceClient extends XrpcClient { uri: URL - headers: Record = {} - constructor(baseClient: Client, serviceUri: string | URL) { - this.baseClient = baseClient - this.uri = typeof serviceUri === 'string' ? new URL(serviceUri) : serviceUri - } - - setHeader(key: string, value: string): void { - this.headers[key.toLowerCase()] = value - } - - unsetHeader(key: string): void { - delete this.headers[key.toLowerCase()] - } - - async call( - methodNsid: string, - params?: QueryParams, - data?: unknown, - opts?: CallOptions, + constructor( + public baseClient: Client, + serviceUri: string | URL, ) { - const def = this.baseClient.lex.getDefOrThrow(methodNsid) - if (!def || (def.type !== 'query' && def.type !== 'procedure')) { - throw new Error( - `Invalid lexicon: ${methodNsid}. Must be a query or procedure.`, - ) - } - - const httpMethod = getMethodSchemaHTTPMethod(def) - const httpUri = constructMethodCallUri(methodNsid, def, this.uri, params) - const httpHeaders = constructMethodCallHeaders(def, data, opts) - - for (const [k, v] of Object.entries(this.headers)) { - if (v != null && !Object.hasOwn(httpHeaders, k)) { - httpHeaders[k] = v - } - } - - const res = await this.baseClient.fetch( - httpUri, - httpMethod, - httpHeaders, - data, - ) - - const resCode = httpResponseCodeToEnum(res.status) - if (resCode === ResponseType.Success) { - try { - this.baseClient.lex.assertValidXrpcOutput(methodNsid, res.body) - } catch (e: any) { - if (e instanceof ValidationError) { - throw new XRPCInvalidResponseError(methodNsid, e, res.body) - } else { - throw e - } - } - return new XRPCResponse(res.body, res.headers) - } else { - if (res.body && isErrorResponseBody(res.body)) { - throw new XRPCError( - resCode, - res.body.error, - res.body.message, - res.headers, - ) - } else { - throw new XRPCError(resCode) - } - } - } -} - -export async function defaultFetchHandler( - httpUri: string, - httpMethod: string, - httpHeaders: Headers, - httpReqBody: unknown, -): Promise { - try { - // The duplex field is now required for streaming bodies, but not yet reflected - // anywhere in docs or types. See whatwg/fetch#1438, nodejs/node#46221. - const reqInit: RequestInit & { duplex: string } = { - method: httpMethod, - headers: httpHeaders, - body: encodeMethodCallBody(httpHeaders, httpReqBody), - duplex: 'half', - } - const res = await fetch(httpUri, reqInit) - const resBody = await res.arrayBuffer() - return { - status: res.status, - headers: Object.fromEntries(res.headers.entries()), - body: httpResponseBodyParse(res.headers.get('content-type'), resBody), - } - } catch (e) { - if (e instanceof XRPCError) throw e - const err = new XRPCError(ResponseType.Unknown, String(e)) - err.cause = e - throw err + super({ service: () => this.uri }, baseClient.lex) + this.uri = typeof serviceUri === 'string' ? new URL(serviceUri) : serviceUri } } - -function isErrorResponseBody(v: unknown): v is ErrorResponseBody { - return errorResponseBody.safeParse(v).success -} diff --git a/packages/xrpc/src/fetch-handler.ts b/packages/xrpc/src/fetch-handler.ts new file mode 100644 index 00000000000..fa7f0564ad8 --- /dev/null +++ b/packages/xrpc/src/fetch-handler.ts @@ -0,0 +1,83 @@ +import { Gettable } from './types' + +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 + +export type FetchHandlerOptions = BuildFetchHandlerOptions | string | URL + +export type BuildFetchHandlerOptions = { + /** + * The service URL to make requests to. This can be a string, URL, or a + * function that returns a string or URL. This is useful for dynamic URLs, + * such as a service URL that changes based on authentication. + */ + service: Gettable + + /** + * Headers to be added to every request. If a function is provided, it will be + * called on each request to get the headers. This is useful for dynamic + * headers, such as authentication tokens that may expire. + */ + headers?: + | { [_ in string]?: Gettable } + | (() => Iterable< + [name: string, value: string, options?: { override?: boolean }] + >) + + /** + * Bring your own fetch implementation. Typically useful for testing, logging, + * mocking, or adding retries, session management, signatures, proof of + * possession (DPoP), etc. Defaults to the global `fetch` function. + */ + fetch?: typeof globalThis.fetch +} + +export function buildFetchHandler(options: FetchHandlerOptions): FetchHandler { + const { + service, + headers: inputHeaders = undefined, + fetch = globalThis.fetch, + } = typeof options === 'string' || options instanceof URL + ? { service: options } + : options + + if (typeof fetch !== 'function') { + throw new TypeError( + 'XrpcDispatcher requires fetch() to be available in your environment.', + ) + } + + return async function (url, init) { + const base = typeof service === 'function' ? await service() : service + const fullUrl = new URL(url, base) + + const headers = new Headers(init.headers) + + if (typeof inputHeaders === 'function') { + for (const [key, value, options = undefined] of inputHeaders()) { + if (options?.override ?? !headers.has(key)) { + headers.set(key, value) + } + } + } else if (inputHeaders) { + for (const [key, getter] of Object.entries(inputHeaders)) { + if (headers.has(key)) continue + + const value = typeof getter === 'function' ? await getter() : getter + if (value == null) continue + + headers.set(key, value) + } + } + + return fetch(fullUrl, { ...init, headers }) + } +} diff --git a/packages/xrpc/src/index.ts b/packages/xrpc/src/index.ts index b084901fa9c..25289bed3dc 100644 --- a/packages/xrpc/src/index.ts +++ b/packages/xrpc/src/index.ts @@ -1,5 +1,7 @@ -export * from './types' export * from './client' +export * from './fetch-handler' +export * from './types' +export * from './xrpc-client' import { Client } from './client' const defaultInst = new Client() diff --git a/packages/xrpc/src/types.ts b/packages/xrpc/src/types.ts index b82fba737dc..de5fba4cbaf 100644 --- a/packages/xrpc/src/types.ts +++ b/packages/xrpc/src/types.ts @@ -3,25 +3,17 @@ import { ValidationError } from '@atproto/lexicon' export type QueryParams = Record export type Headers = Record +export type Awaitable = V | PromiseLike +export type Gettable = + | V + | (() => Awaitable) export interface CallOptions { encoding?: string + signal?: AbortSignal headers?: Headers } -export interface FetchHandlerResponse { - status: number - headers: Headers - body: ArrayBuffer | undefined -} - -export type FetchHandler = ( - httpUri: string, - httpMethod: string, - httpHeaders: Headers, - httpReqBody: any, -) => Promise - export const errorResponseBody = z.object({ error: z.string().optional(), message: z.string().optional(), @@ -45,7 +37,24 @@ export enum ResponseType { UpstreamTimeout = 504, } +export function httpResponseCodeToEnum(status: number): ResponseType { + if (status in ResponseType) { + return status + } else if (status >= 100 && status < 200) { + return ResponseType.XRPCNotSupported + } else if (status >= 200 && status < 300) { + return ResponseType.Success + } else if (status >= 300 && status < 400) { + return ResponseType.XRPCNotSupported + } else if (status >= 400 && status < 500) { + return ResponseType.InvalidRequest + } else { + return ResponseType.InternalServerError + } +} + export const ResponseTypeNames = { + [ResponseType.Unknown]: 'Unknown', [ResponseType.InvalidResponse]: 'InvalidResponse', [ResponseType.Success]: 'Success', [ResponseType.InvalidRequest]: 'InvalidRequest', @@ -61,7 +70,12 @@ export const ResponseTypeNames = { [ResponseType.UpstreamTimeout]: 'UpstreamTimeout', } +export function httpResponseCodeToName(status: number): string { + return ResponseTypeNames[httpResponseCodeToEnum(status)] +} + export const ResponseTypeStrings = { + [ResponseType.Unknown]: 'Unknown', [ResponseType.InvalidResponse]: 'Invalid Response', [ResponseType.Success]: 'Success', [ResponseType.InvalidRequest]: 'Invalid Request', @@ -77,6 +91,10 @@ export const ResponseTypeStrings = { [ResponseType.UpstreamTimeout]: 'Upstream Timeout', } +export function httpResponseCodeToString(status: number): string { + return ResponseTypeStrings[httpResponseCodeToEnum(status)] +} + export class XRPCResponse { success = true @@ -88,19 +106,48 @@ export class XRPCResponse { export class XRPCError extends Error { success = false - headers?: Headers + + public status: ResponseType constructor( - public status: ResponseType, - public error?: string, + statusCode: number, + public error: string = httpResponseCodeToName(statusCode), message?: string, - headers?: Headers, + public headers?: Headers, + options?: ErrorOptions, ) { - super(message || error || ResponseTypeStrings[status]) - if (!this.error) { - this.error = ResponseTypeNames[status] + super(message || error || httpResponseCodeToString(statusCode), options) + + this.status = httpResponseCodeToEnum(statusCode) + + // Pre 2022 runtimes won't handle the "options" constructor argument + const cause = options?.cause + if (this.cause === undefined && cause !== undefined) { + this.cause = cause + } + } + + static from(cause: unknown, fallbackStatus?: ResponseType): XRPCError { + if (cause instanceof XRPCError) { + return cause } - this.headers = headers + + // Extract status code from "http-errors" like errors + const statusCode: unknown = + cause instanceof Error + ? ('statusCode' in cause ? cause.statusCode : undefined) ?? + ('status' in cause ? cause.status : undefined) + : undefined + + const status: ResponseType = + typeof statusCode === 'number' + ? httpResponseCodeToEnum(statusCode) + : fallbackStatus ?? ResponseType.Unknown + + const error = ResponseTypeNames[status] + const message = cause instanceof Error ? cause.message : String(cause) + + return new XRPCError(status, error, message, undefined, { cause }) } } @@ -114,6 +161,8 @@ export class XRPCInvalidResponseError extends XRPCError { ResponseType.InvalidResponse, ResponseTypeStrings[ResponseType.InvalidResponse], `The server gave an invalid response and may be out of date.`, + undefined, + { cause: validationError }, ) } } diff --git a/packages/xrpc/src/util.ts b/packages/xrpc/src/util.ts index 74f46c4d9de..b08baaa9844 100644 --- a/packages/xrpc/src/util.ts +++ b/packages/xrpc/src/util.ts @@ -6,6 +6,8 @@ import { } from '@atproto/lexicon' import { CallOptions, + errorResponseBody, + ErrorResponseBody, Headers, QueryParams, ResponseType, @@ -22,6 +24,10 @@ const ReadableStream = } } as typeof globalThis.ReadableStream) +export function isErrorResponseBody(v: unknown): v is ErrorResponseBody { + return errorResponseBody.safeParse(v).success +} + export function getMethodSchemaHTTPMethod( schema: LexXrpcProcedure | LexXrpcQuery, ) { @@ -37,33 +43,43 @@ export function constructMethodCallUri( serviceUri: URL, params?: QueryParams, ): string { - const uri = new URL(serviceUri) - uri.pathname = `/xrpc/${nsid}` - - // given parameters - if (params) { - for (const [key, value] of Object.entries(params)) { - const paramSchema = schema.parameters?.properties?.[key] - if (!paramSchema) { - throw new Error(`Invalid query parameter: ${key}`) - } - if (value !== undefined) { - if (paramSchema.type === 'array') { - const vals: (typeof value)[] = [] - vals.concat(value).forEach((val) => { - uri.searchParams.append( - key, - encodeQueryParam(paramSchema.items.type, val), - ) - }) - } else { - uri.searchParams.set(key, encodeQueryParam(paramSchema.type, value)) + const uri = new URL(constructMethodCallUrl(nsid, schema, params), serviceUri) + return uri.toString() +} + +export function constructMethodCallUrl( + nsid: string, + schema: LexXrpcProcedure | LexXrpcQuery, + params?: QueryParams, +): string { + const pathname = `/xrpc/${encodeURIComponent(nsid)}` + if (!params) return pathname + + const searchParams: [string, string][] = [] + + for (const [key, value] of Object.entries(params)) { + const paramSchema = schema.parameters?.properties?.[key] + if (!paramSchema) { + throw new Error(`Invalid query parameter: ${key}`) + } + if (value !== undefined) { + if (paramSchema.type === 'array') { + const values = Array.isArray(value) ? value : [value] + for (const val of values) { + searchParams.push([ + key, + encodeQueryParam(paramSchema.items.type, val), + ]) } + } else { + searchParams.push([key, encodeQueryParam(paramSchema.type, value)]) } } } - return uri.toString() + if (!searchParams.length) return pathname + + return `${pathname}?${new URLSearchParams(searchParams).toString()}` } export function encodeQueryParam( @@ -365,53 +381,31 @@ const toUint8Array: (value: unknown) => Uint8Array | undefined = Buffer throw new TypeError(`Unsupported value type: ${typeof value}`) } -export function httpResponseCodeToEnum(status: number): ResponseType { - let resCode: ResponseType - if (status in ResponseType) { - resCode = status - } else if (status >= 100 && status < 200) { - resCode = ResponseType.XRPCNotSupported - } else if (status >= 200 && status < 300) { - resCode = ResponseType.Success - } else if (status >= 300 && status < 400) { - resCode = ResponseType.XRPCNotSupported - } else if (status >= 400 && status < 500) { - resCode = ResponseType.InvalidRequest - } else { - resCode = ResponseType.InternalServerError - } - return resCode -} - export function httpResponseBodyParse( mimeType: string | null, data: ArrayBuffer | undefined, ): any { - if (mimeType) { - if (mimeType.includes('application/json') && data?.byteLength) { - try { + try { + if (mimeType) { + if (mimeType.includes('application/json')) { const str = new TextDecoder().decode(data) return jsonStringToLex(str) - } catch (e) { - throw new XRPCError( - ResponseType.InvalidResponse, - `Failed to parse response body: ${String(e)}`, - ) } - } - if (mimeType.startsWith('text/') && data?.byteLength) { - try { + if (mimeType.startsWith('text/')) { return new TextDecoder().decode(data) - } catch (e) { - throw new XRPCError( - ResponseType.InvalidResponse, - `Failed to parse response body: ${String(e)}`, - ) } } + if (data instanceof ArrayBuffer) { + return new Uint8Array(data) + } + return data + } catch (cause) { + throw new XRPCError( + ResponseType.InvalidResponse, + undefined, + `Failed to parse response body: ${String(cause)}`, + undefined, + { cause }, + ) } - if (data instanceof ArrayBuffer) { - return new Uint8Array(data) - } - return data } diff --git a/packages/xrpc/src/xrpc-client.ts b/packages/xrpc/src/xrpc-client.ts new file mode 100644 index 00000000000..b7f4eeec156 --- /dev/null +++ b/packages/xrpc/src/xrpc-client.ts @@ -0,0 +1,120 @@ +import { LexiconDoc, Lexicons, ValidationError } from '@atproto/lexicon' +import { + CallOptions, + Gettable, + QueryParams, + ResponseType, + XRPCError, + XRPCInvalidResponseError, + XRPCResponse, + httpResponseCodeToEnum, +} from './types' +import { + constructMethodCallHeaders, + constructMethodCallUrl, + encodeMethodCallBody, + getMethodSchemaHTTPMethod, + httpResponseBodyParse, + isErrorResponseBody, +} from './util' +import { + FetchHandler, + FetchHandlerOptions, + buildFetchHandler, +} from './fetch-handler' + +export class XrpcClient { + readonly fetchHandler: FetchHandler + readonly lex: Lexicons + + protected headers = new Map>() + + constructor( + fetchHandler: FetchHandler | FetchHandlerOptions, + lex: Lexicons | Iterable, + ) { + this.fetchHandler = + typeof fetchHandler === 'function' + ? fetchHandler + : buildFetchHandler(fetchHandler) + + this.lex = lex instanceof Lexicons ? lex : new Lexicons(lex) + } + + setHeader(key: string, value: Gettable): void { + this.headers.set(key.toLowerCase(), value) + } + + unsetHeader(key: string): void { + this.headers.delete(key.toLowerCase()) + } + + async call( + methodNsid: string, + params?: QueryParams, + data?: unknown, + opts?: CallOptions, + ): Promise { + const def = this.lex.getDefOrThrow(methodNsid) + if (!def || (def.type !== 'query' && def.type !== 'procedure')) { + throw new TypeError( + `Invalid lexicon: ${methodNsid}. Must be a query or procedure.`, + ) + } + + const reqUrl = constructMethodCallUrl(methodNsid, def, params) + const reqMethod = getMethodSchemaHTTPMethod(def) + const reqHeaders = constructMethodCallHeaders(def, data, opts) + const reqBody = encodeMethodCallBody(reqHeaders, data) + + for (const [key, value] of this.headers) { + if (!Object.hasOwn(reqHeaders, key)) { + const header = typeof value === 'function' ? await value() : value + if (header != null) reqHeaders[key] = header + } + } + + // The duplex field is required for streaming bodies, but not yet reflected + // anywhere in docs or types. See whatwg/fetch#1438, nodejs/node#46221. + const init: RequestInit & { duplex: 'half' } = { + method: reqMethod, + headers: reqHeaders, + body: reqBody, + duplex: 'half', + signal: opts?.signal, + } + + try { + const response = await this.fetchHandler.call(undefined, reqUrl, init) + + const resStatus = response.status + const resHeaders = Object.fromEntries(response.headers.entries()) + const resBodyBytes = await response.arrayBuffer() + const resBody = httpResponseBodyParse( + response.headers.get('content-type'), + resBodyBytes, + ) + + const resCode = httpResponseCodeToEnum(resStatus) + if (resCode !== ResponseType.Success) { + const { error = undefined, message = undefined } = + resBody && isErrorResponseBody(resBody) ? resBody : {} + throw new XRPCError(resCode, error, message, resHeaders) + } + + try { + this.lex.assertValidXrpcOutput(methodNsid, resBody) + } catch (e: unknown) { + if (e instanceof ValidationError) { + throw new XRPCInvalidResponseError(methodNsid, e, resBody) + } + + throw e + } + + return new XRPCResponse(resBody, resHeaders) + } catch (err) { + throw XRPCError.from(err) + } + } +} diff --git a/tsconfig/tests.json b/tsconfig/tests.json index 321864788ee..07573723164 100644 --- a/tsconfig/tests.json +++ b/tsconfig/tests.json @@ -3,6 +3,7 @@ "extends": "./node.json", "compilerOptions": { "types": ["node", "jest"], + "lib": ["ES2022", "DOM", "DOM.Iterable"], "noEmit": true } }