@atproto/lex-client 0.0.13 → 0.0.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/xrpc.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"xrpc.js","sourceRoot":"","sources":["../src/xrpc.ts"],"names":[],"mappings":"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","sourcesContent":["import { LexValue, isLexScalar, isPlainObject } from '@atproto/lex-data'\nimport { lexStringify } from '@atproto/lex-json'\nimport {\n InferInput,\n InferPayload,\n Main,\n Params,\n Payload,\n Procedure,\n Query,\n Restricted,\n Subscription,\n getMain,\n} from '@atproto/lex-schema'\nimport { Agent } from './agent.js'\nimport { XrpcFailure, asXrpcFailure } from './errors.js'\nimport { XrpcResponse } from './response.js'\nimport { BinaryBodyInit, CallOptions } from './types.js'\nimport {\n buildAtprotoHeaders,\n isAsyncIterable,\n isBlobLike,\n toReadableStream,\n} from './util.js'\n\n// If all params are optional, allow omitting the params object\ntype XrpcParamsOptions<P extends Params> =\n NonNullable<unknown> extends P ? { params?: P } : { params: P }\n\n/**\n * The query/path parameters type for an XRPC method, inferred from its schema.\n *\n * @typeParam M - The XRPC method type (Procedure, Query, or Subscription)\n */\nexport type XrpcRequestParams<M extends Procedure | Query | Subscription> =\n InferInput<M['parameters']>\n\ntype XrpcRequestPayload<M extends Procedure | Query> = M extends Procedure\n ? InferPayload<M['input'], BinaryBodyInit>\n : undefined\n\ntype XrpcInputOptions<In> = In extends { body: infer B; encoding: infer E }\n ? // encoding will be inferred from the schema at runtime if not provided\n { body: B; encoding?: E }\n : { body?: undefined; encoding?: undefined }\n\n/**\n * Options for making an XRPC request.\n *\n * Combines {@link CallOptions} with method-specific params and body requirements.\n * The type system ensures required params/body are provided based on the method schema.\n *\n * @typeParam M - The XRPC method type (Procedure or Query)\n * @see {@link CallOptions} for general request options like signal and validateRequest\n * @see {@link XrpcParamsOptions} for method-specific query parameters\n * @see {@link XrpcInputOptions} for method-specific body and encoding requirements\n *\n * @example Query with params\n * ```typescript\n * const options: XrpcOptions<typeof app.bsky.feed.getTimeline.main> = {\n * params: { limit: 50 }\n * }\n * ```\n *\n * @example Procedure with body\n * ```typescript\n * const options: XrpcOptions<typeof com.atproto.repo.createRecord.main> = {\n * body: { repo: did, collection: 'app.bsky.feed.post', record: { ... } }\n * }\n * ```\n */\nexport type XrpcOptions<M extends Procedure | Query = Procedure | Query> =\n CallOptions &\n XrpcInputOptions<XrpcRequestPayload<M>> &\n XrpcParamsOptions<XrpcRequestParams<M>>\n\n/**\n * Makes an XRPC request and throws on failure.\n *\n * This is the low-level function for making XRPC calls. For most use cases,\n * prefer using {@link Client.xrpc} which provides a more ergonomic API.\n *\n * @param agent - The {@link Agent} to use for making the request\n * @param ns - The lexicon method definition\n * @param options - Request {@link XrpcOptions options} (params, body, headers, etc.)\n * @returns The successful {@link XrpcResponse}\n * @throws {XrpcFailure} When the request fails\n *\n * @example\n * ```typescript\n * const response = await xrpc(agent, app.bsky.feed.getTimeline.main, {\n * params: { limit: 50 }\n * })\n * ```\n */\nexport async function xrpc<const M extends Query | Procedure>(\n agent: Agent,\n ns: NonNullable<unknown> extends XrpcOptions<M>\n ? Main<M>\n : Restricted<'This XRPC method requires an \"options\" argument'>,\n): Promise<XrpcResponse<M>>\nexport async function xrpc<const M extends Query | Procedure>(\n agent: Agent,\n ns: Main<M>,\n options: XrpcOptions<M>,\n): Promise<XrpcResponse<M>>\nexport async function xrpc<const M extends Query | Procedure>(\n agent: Agent,\n ns: Main<M>,\n options: XrpcOptions<M> = {} as XrpcOptions<M>,\n): Promise<XrpcResponse<M>> {\n const response = await xrpcSafe<M>(agent, ns, options)\n if (response.success) return response\n else throw response\n}\n\n/**\n * Union type representing either a successful response or a failure.\n *\n * Both {@link XrpcResponse} and {@link XrpcFailure} have a `success` property\n * that can be used to discriminate between them.\n *\n * @typeParam M - The XRPC method type\n */\nexport type XrpcResult<M extends Procedure | Query> =\n | XrpcResponse<M>\n | XrpcFailure<M>\n\n/**\n * Makes an XRPC request without throwing on failure.\n *\n * Returns a discriminated union that can be checked via the `success` property.\n * This is useful for handling errors without try/catch blocks. This also allow\n * failure results to be typed with the method schema, which can provide better\n * type safety when handling errors (e.g. checking for specific error codes).\n *\n * @param agent - The {@link Agent} to use for making the request\n * @param ns - The lexicon method definition\n * @param options - Request {@link XrpcOptions options} (params, body, headers, etc.)\n * @returns Either a successful {@link XrpcResponse} or an {@link XrpcFailure}\n *\n * @example\n * ```typescript\n * const result = await xrpcSafe(agent, app.bsky.actor.getProfile.main, {\n * params: { actor: 'alice.bsky.social' }\n * })\n *\n * if (result.success) {\n * console.log(result.body.displayName)\n * } else {\n * console.error('Request failed:', result.error)\n * }\n * ```\n */\nexport async function xrpcSafe<const M extends Query | Procedure>(\n agent: Agent,\n ns: NonNullable<unknown> extends XrpcOptions<M>\n ? Main<M>\n : Restricted<'This XRPC method requires an \"options\" argument'>,\n): Promise<XrpcResult<M>>\nexport async function xrpcSafe<const M extends Query | Procedure>(\n agent: Agent,\n ns: Main<M>,\n options: XrpcOptions<M>,\n): Promise<XrpcResult<M>>\nexport async function xrpcSafe<const M extends Query | Procedure>(\n agent: Agent,\n ns: Main<M>,\n options: XrpcOptions<M> = {} as XrpcOptions<M>,\n): Promise<XrpcResult<M>> {\n options.signal?.throwIfAborted()\n const method: M = getMain(ns)\n try {\n const url = xrpcRequestUrl(method, options)\n const request = xrpcRequestInit(method, options)\n const response = await agent.fetchHandler(url, request)\n return await XrpcResponse.fromFetchResponse<M>(method, response, options)\n } catch (cause) {\n return asXrpcFailure(method, cause)\n }\n}\n\nfunction xrpcRequestUrl<M extends Procedure | Query | Subscription>(\n method: M,\n options: CallOptions & { params?: Params },\n) {\n const path = `/xrpc/${method.nsid}`\n\n const queryString = method.parameters\n ?.toURLSearchParams(options.params ?? {})\n .toString()\n\n return queryString ? `${path}?${queryString}` : path\n}\n\nfunction xrpcRequestInit<T extends Procedure | Query>(\n schema: T,\n options: CallOptions & {\n body?: LexValue | BinaryBodyInit\n encoding?: string\n },\n): RequestInit & { duplex?: 'half' } {\n const headers = buildAtprotoHeaders(options)\n\n // Tell the server what type of response we're expecting\n if (schema.output.encoding) {\n headers.set('accept', schema.output.encoding)\n }\n\n // Caller should not set content-type header\n if (headers.has('content-type')) {\n const contentType = headers.get('content-type')\n throw new TypeError(`Unexpected content-type header (${contentType})`)\n }\n\n // Requests with body\n if ('input' in schema) {\n const encodingHint = options.encoding\n const input = xrpcProcedureInput(schema, options, encodingHint)\n\n if (input) {\n headers.set('content-type', input.encoding)\n } else if (encodingHint != null) {\n throw new TypeError(`Unexpected encoding hint (${encodingHint})`)\n }\n\n return {\n duplex: 'half',\n redirect: 'follow',\n referrerPolicy: 'strict-origin-when-cross-origin', // (default)\n mode: 'cors', // (default)\n signal: options.signal,\n method: 'POST',\n headers,\n body: input?.body,\n }\n }\n\n // Requests without body\n return {\n duplex: 'half',\n redirect: 'follow',\n referrerPolicy: 'strict-origin-when-cross-origin', // (default)\n mode: 'cors', // (default)\n signal: options.signal,\n method: 'GET',\n headers,\n }\n}\n\nfunction xrpcProcedureInput(\n method: Procedure,\n options: CallOptions & { body?: LexValue | BinaryBodyInit },\n encodingHint?: string,\n): null | { body: BodyInit; encoding: string } {\n const { input } = method\n const { body } = options\n\n if (options.validateRequest) {\n input.schema?.check(body)\n }\n\n // Special handling for endpoints expecting application/json input\n if (input.encoding === 'application/json') {\n // @NOTE **NOT** using isLexValue here to avoid deep checks in order to\n // distinguish between LexValue and BinaryBodyInit.\n if (!isLexScalar(body) && !isPlainObject(body) && !Array.isArray(body)) {\n throw new TypeError(`Expected LexValue body, got ${typeof body}`)\n }\n\n return buildPayload(input, lexStringify(body), encodingHint)\n }\n\n // Other encodings will be sent unaltered (ie. as binary data)\n switch (typeof body) {\n case 'undefined':\n case 'string':\n return buildPayload(input, body, encodingHint)\n case 'object': {\n if (body === null) break\n if (\n ArrayBuffer.isView(body) ||\n body instanceof ArrayBuffer ||\n body instanceof ReadableStream\n ) {\n return buildPayload(input, body, encodingHint)\n } else if (isAsyncIterable(body)) {\n return buildPayload(input, toReadableStream(body), encodingHint)\n } else if (isBlobLike(body)) {\n return buildPayload(input, body, encodingHint || body.type)\n }\n }\n }\n\n throw new TypeError(\n `Invalid ${typeof body} body for ${input.encoding} encoding`,\n )\n}\n\nfunction buildPayload(\n schema: Payload,\n body: undefined | BodyInit,\n encodingHint?: string,\n): null | { body: BodyInit; encoding: string } {\n if (schema.encoding === undefined) {\n if (body !== undefined) {\n throw new TypeError(\n `Cannot send a ${typeof body} body with undefined encoding`,\n )\n }\n\n return null\n }\n\n if (body === undefined) {\n // This error would be returned by the server, but we can catch it earlier\n // to avoid un-necessary requests. Note that a content-length of 0 does not\n // necessary mean that the body is \"empty\" (e.g. an empty txt file).\n throw new TypeError(`A request body is expected but none was provided`)\n }\n\n const encoding = buildEncoding(schema, encodingHint)\n return { encoding, body }\n}\n\nfunction buildEncoding(schema: Payload, encodingHint?: string): string {\n // Should never happen (required for type safety)\n if (!schema.encoding) {\n throw new TypeError('Unexpected payload')\n }\n\n if (encodingHint?.length) {\n if (!schema.matchesEncoding(encodingHint)) {\n throw new TypeError(\n `Cannot send a body with content-type \"${encodingHint}\" for \"${schema.encoding}\" encoding`,\n )\n }\n return encodingHint\n }\n\n // Fallback\n\n if (schema.encoding === '*/*') {\n return 'application/octet-stream'\n }\n\n if (schema.encoding.startsWith('text/')) {\n return schema.encoding.includes('*')\n ? 'text/plain; charset=utf-8'\n : `${schema.encoding}; charset=utf-8`\n }\n\n if (!schema.encoding.includes('*')) {\n return schema.encoding\n }\n\n throw new TypeError(\n `Unable to determine payload encoding. Please provide a 'content-type' header matching ${schema.encoding}.`,\n )\n}\n"]}
1
+ {"version":3,"file":"xrpc.js","sourceRoot":"","sources":["../src/xrpc.ts"],"names":[],"mappings":";;AAiHA,oBAQC;AAmDD,4BAgBC;AA5LD,gDAAwE;AACxE,gDAAgD;AAChD,oDAY4B;AAC5B,yCAA4D;AAC5D,2CAAwD;AACxD,+CAA4C;AAE5C,uCAKkB;AAyFX,KAAK,UAAU,IAAI,CACxB,SAA+B,EAC/B,EAAW,EACX,UAA0B,EAAoB;IAE9C,MAAM,QAAQ,GAAG,MAAM,QAAQ,CAAI,SAAS,EAAE,EAAE,EAAE,OAAO,CAAC,CAAA;IAC1D,IAAI,QAAQ,CAAC,OAAO;QAAE,OAAO,QAAQ,CAAA;;QAChC,MAAM,QAAQ,CAAA;AACrB,CAAC;AAmDM,KAAK,UAAU,QAAQ,CAC5B,SAA+B,EAC/B,EAAW,EACX,UAA0B,EAAoB;IAE9C,OAAO,CAAC,MAAM,EAAE,cAAc,EAAE,CAAA;IAChC,MAAM,MAAM,GAAM,IAAA,oBAAO,EAAC,EAAE,CAAC,CAAA;IAC7B,IAAI,CAAC;QACH,MAAM,KAAK,GAAG,IAAA,qBAAU,EAAC,SAAS,CAAC,CAAA;QACnC,MAAM,GAAG,GAAG,cAAc,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;QAC3C,MAAM,OAAO,GAAG,eAAe,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;QAChD,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,YAAY,CAAC,GAAG,EAAE,OAAO,CAAC,CAAA;QACvD,OAAO,MAAM,0BAAY,CAAC,iBAAiB,CAAI,MAAM,EAAE,QAAQ,EAAE,OAAO,CAAC,CAAA;IAC3E,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO,IAAA,yBAAa,EAAC,MAAM,EAAE,KAAK,CAAC,CAAA;IACrC,CAAC;AACH,CAAC;AAED,SAAS,cAAc,CACrB,MAAS,EACT,OAA0C;IAE1C,MAAM,IAAI,GAAG,SAAS,MAAM,CAAC,IAAI,EAAW,CAAA;IAE5C,MAAM,WAAW,GAAG,MAAM,CAAC,UAAU;QACnC,EAAE,iBAAiB,CAAC,OAAO,CAAC,MAAM,IAAI,EAAE,CAAC;SACxC,QAAQ,EAAE,CAAA;IAEb,OAAO,WAAW,CAAC,CAAC,CAAE,GAAG,IAAI,IAAI,WAAW,EAAY,CAAC,CAAC,CAAC,IAAI,CAAA;AACjE,CAAC;AAED,SAAS,eAAe,CACtB,MAAS,EACT,OAGC;IAED,MAAM,OAAO,GAAG,IAAA,6BAAmB,EAAC,OAAO,CAAC,CAAA;IAE5C,wDAAwD;IACxD,IAAI,MAAM,CAAC,MAAM,CAAC,QAAQ,EAAE,CAAC;QAC3B,OAAO,CAAC,GAAG,CAAC,QAAQ,EAAE,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAA;IAC/C,CAAC;IAED,4CAA4C;IAC5C,IAAI,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC,EAAE,CAAC;QAChC,MAAM,WAAW,GAAG,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC,CAAA;QAC/C,MAAM,IAAI,SAAS,CAAC,mCAAmC,WAAW,GAAG,CAAC,CAAA;IACxE,CAAC;IAED,qBAAqB;IACrB,IAAI,OAAO,IAAI,MAAM,EAAE,CAAC;QACtB,MAAM,YAAY,GAAG,OAAO,CAAC,QAAQ,CAAA;QACrC,MAAM,KAAK,GAAG,kBAAkB,CAAC,MAAM,EAAE,OAAO,EAAE,YAAY,CAAC,CAAA;QAE/D,IAAI,KAAK,EAAE,CAAC;YACV,OAAO,CAAC,GAAG,CAAC,cAAc,EAAE,KAAK,CAAC,QAAQ,CAAC,CAAA;QAC7C,CAAC;aAAM,IAAI,YAAY,IAAI,IAAI,EAAE,CAAC;YAChC,MAAM,IAAI,SAAS,CAAC,6BAA6B,YAAY,GAAG,CAAC,CAAA;QACnE,CAAC;QAED,OAAO;YACL,MAAM,EAAE,MAAM;YACd,QAAQ,EAAE,QAAQ;YAClB,cAAc,EAAE,iCAAiC,EAAE,YAAY;YAC/D,IAAI,EAAE,MAAM,EAAE,YAAY;YAC1B,MAAM,EAAE,OAAO,CAAC,MAAM;YACtB,MAAM,EAAE,MAAM;YACd,OAAO;YACP,IAAI,EAAE,KAAK,EAAE,IAAI;SAClB,CAAA;IACH,CAAC;IAED,wBAAwB;IACxB,OAAO;QACL,MAAM,EAAE,MAAM;QACd,QAAQ,EAAE,QAAQ;QAClB,cAAc,EAAE,iCAAiC,EAAE,YAAY;QAC/D,IAAI,EAAE,MAAM,EAAE,YAAY;QAC1B,MAAM,EAAE,OAAO,CAAC,MAAM;QACtB,MAAM,EAAE,KAAK;QACb,OAAO;KACR,CAAA;AACH,CAAC;AAED,SAAS,kBAAkB,CACzB,MAAiB,EACjB,OAA2D,EAC3D,YAAqB;IAErB,MAAM,EAAE,KAAK,EAAE,GAAG,MAAM,CAAA;IACxB,MAAM,EAAE,IAAI,EAAE,GAAG,OAAO,CAAA;IAExB,IAAI,OAAO,CAAC,eAAe,EAAE,CAAC;QAC5B,KAAK,CAAC,MAAM,EAAE,KAAK,CAAC,IAAI,CAAC,CAAA;IAC3B,CAAC;IAED,kEAAkE;IAClE,IAAI,KAAK,CAAC,QAAQ,KAAK,kBAAkB,EAAE,CAAC;QAC1C,uEAAuE;QACvE,mDAAmD;QACnD,IAAI,CAAC,IAAA,sBAAW,EAAC,IAAI,CAAC,IAAI,CAAC,IAAA,wBAAa,EAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;YACvE,MAAM,IAAI,SAAS,CAAC,+BAA+B,OAAO,IAAI,EAAE,CAAC,CAAA;QACnE,CAAC;QAED,OAAO,YAAY,CAAC,KAAK,EAAE,IAAA,uBAAY,EAAC,IAAI,CAAC,EAAE,YAAY,CAAC,CAAA;IAC9D,CAAC;IAED,8DAA8D;IAC9D,QAAQ,OAAO,IAAI,EAAE,CAAC;QACpB,KAAK,WAAW,CAAC;QACjB,KAAK,QAAQ;YACX,OAAO,YAAY,CAAC,KAAK,EAAE,IAAI,EAAE,YAAY,CAAC,CAAA;QAChD,KAAK,QAAQ,CAAC,CAAC,CAAC;YACd,IAAI,IAAI,KAAK,IAAI;gBAAE,MAAK;YACxB,IACE,WAAW,CAAC,MAAM,CAAC,IAAI,CAAC;gBACxB,IAAI,YAAY,WAAW;gBAC3B,IAAI,YAAY,cAAc,EAC9B,CAAC;gBACD,OAAO,YAAY,CAAC,KAAK,EAAE,IAAI,EAAE,YAAY,CAAC,CAAA;YAChD,CAAC;iBAAM,IAAI,IAAA,yBAAe,EAAC,IAAI,CAAC,EAAE,CAAC;gBACjC,OAAO,YAAY,CAAC,KAAK,EAAE,IAAA,0BAAgB,EAAC,IAAI,CAAC,EAAE,YAAY,CAAC,CAAA;YAClE,CAAC;iBAAM,IAAI,IAAA,oBAAU,EAAC,IAAI,CAAC,EAAE,CAAC;gBAC5B,OAAO,YAAY,CAAC,KAAK,EAAE,IAAI,EAAE,YAAY,IAAI,IAAI,CAAC,IAAI,CAAC,CAAA;YAC7D,CAAC;QACH,CAAC;IACH,CAAC;IAED,MAAM,IAAI,SAAS,CACjB,WAAW,OAAO,IAAI,aAAa,KAAK,CAAC,QAAQ,WAAW,CAC7D,CAAA;AACH,CAAC;AAED,SAAS,YAAY,CACnB,MAAe,EACf,IAA0B,EAC1B,YAAqB;IAErB,IAAI,MAAM,CAAC,QAAQ,KAAK,SAAS,EAAE,CAAC;QAClC,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;YACvB,MAAM,IAAI,SAAS,CACjB,iBAAiB,OAAO,IAAI,+BAA+B,CAC5D,CAAA;QACH,CAAC;QAED,OAAO,IAAI,CAAA;IACb,CAAC;IAED,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;QACvB,0EAA0E;QAC1E,2EAA2E;QAC3E,oEAAoE;QACpE,MAAM,IAAI,SAAS,CAAC,kDAAkD,CAAC,CAAA;IACzE,CAAC;IAED,MAAM,QAAQ,GAAG,aAAa,CAAC,MAAM,EAAE,YAAY,CAAC,CAAA;IACpD,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAA;AAC3B,CAAC;AAED,SAAS,aAAa,CAAC,MAAe,EAAE,YAAqB;IAC3D,iDAAiD;IACjD,IAAI,CAAC,MAAM,CAAC,QAAQ,EAAE,CAAC;QACrB,MAAM,IAAI,SAAS,CAAC,oBAAoB,CAAC,CAAA;IAC3C,CAAC;IAED,IAAI,YAAY,EAAE,MAAM,EAAE,CAAC;QACzB,IAAI,CAAC,MAAM,CAAC,eAAe,CAAC,YAAY,CAAC,EAAE,CAAC;YAC1C,MAAM,IAAI,SAAS,CACjB,yCAAyC,YAAY,UAAU,MAAM,CAAC,QAAQ,YAAY,CAC3F,CAAA;QACH,CAAC;QACD,OAAO,YAAY,CAAA;IACrB,CAAC;IAED,WAAW;IAEX,IAAI,MAAM,CAAC,QAAQ,KAAK,KAAK,EAAE,CAAC;QAC9B,OAAO,0BAA0B,CAAA;IACnC,CAAC;IAED,IAAI,MAAM,CAAC,QAAQ,CAAC,UAAU,CAAC,OAAO,CAAC,EAAE,CAAC;QACxC,OAAO,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC,GAAG,CAAC;YAClC,CAAC,CAAC,2BAA2B;YAC7B,CAAC,CAAC,GAAG,MAAM,CAAC,QAAQ,iBAAiB,CAAA;IACzC,CAAC;IAED,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC;QACnC,OAAO,MAAM,CAAC,QAAQ,CAAA;IACxB,CAAC;IAED,MAAM,IAAI,SAAS,CACjB,yFAAyF,MAAM,CAAC,QAAQ,GAAG,CAC5G,CAAA;AACH,CAAC","sourcesContent":["import { LexValue, isLexScalar, isPlainObject } from '@atproto/lex-data'\nimport { lexStringify } from '@atproto/lex-json'\nimport {\n InferInput,\n InferPayload,\n Main,\n NsidString,\n Params,\n Payload,\n Procedure,\n Query,\n Restricted,\n Subscription,\n getMain,\n} from '@atproto/lex-schema'\nimport { Agent, AgentOptions, buildAgent } from './agent.js'\nimport { XrpcFailure, asXrpcFailure } from './errors.js'\nimport { XrpcResponse } from './response.js'\nimport { BinaryBodyInit, CallOptions } from './types.js'\nimport {\n buildAtprotoHeaders,\n isAsyncIterable,\n isBlobLike,\n toReadableStream,\n} from './util.js'\n\n// If all params are optional, allow omitting the params object\ntype XrpcParamsOptions<P extends Params> =\n NonNullable<unknown> extends P ? { params?: P } : { params: P }\n\n/**\n * The query/path parameters type for an XRPC method, inferred from its schema.\n *\n * @typeParam M - The XRPC method type (Procedure, Query, or Subscription)\n */\nexport type XrpcRequestParams<M extends Procedure | Query | Subscription> =\n InferInput<M['parameters']>\n\ntype XrpcRequestPayload<M extends Procedure | Query> = M extends Procedure\n ? InferPayload<M['input'], BinaryBodyInit>\n : undefined\n\ntype XrpcInputOptions<In> = In extends { body: infer B; encoding: infer E }\n ? // encoding will be inferred from the schema at runtime if not provided\n { body: B; encoding?: E }\n : { body?: undefined; encoding?: undefined }\n\n/**\n * Options for making an XRPC request.\n *\n * Combines {@link CallOptions} with method-specific params and body requirements.\n * The type system ensures required params/body are provided based on the method schema.\n *\n * @typeParam M - The XRPC method type (Procedure or Query)\n * @see {@link CallOptions} for general request options like signal and validateRequest\n * @see {@link XrpcParamsOptions} for method-specific query parameters\n * @see {@link XrpcInputOptions} for method-specific body and encoding requirements\n *\n * @example Query with params\n * ```typescript\n * const options: XrpcOptions<typeof app.bsky.feed.getTimeline.main> = {\n * params: { limit: 50 }\n * }\n * ```\n *\n * @example Procedure with body\n * ```typescript\n * const options: XrpcOptions<typeof com.atproto.repo.createRecord.main> = {\n * body: { repo: did, collection: 'app.bsky.feed.post', record: { ... } }\n * }\n * ```\n */\nexport type XrpcOptions<M extends Procedure | Query = Procedure | Query> =\n CallOptions &\n XrpcInputOptions<XrpcRequestPayload<M>> &\n XrpcParamsOptions<XrpcRequestParams<M>>\n\n/**\n * Makes an XRPC request and throws on failure.\n *\n * This is the low-level function for making XRPC calls.\n *\n * @param agent - The {@link Agent} to use for making the request\n * @param ns - The lexicon method definition\n * @param options - Request {@link XrpcOptions options} (params, body, headers, etc.)\n * @returns The successful {@link XrpcResponse}\n * @throws {XrpcFailure} When the request fails\n *\n * @example\n * ```typescript\n * const response = await xrpc('https://bsky.network', com.atproto.identity.resolveHandle, {\n * params: { handle: \"atproto.com\" }\n * })\n * ```\n *\n * @example\n * ```typescript\n * const response = await xrpc(agent, app.bsky.feed.getTimeline.main, {\n * params: { limit: 50 }\n * })\n * ```\n */\nexport async function xrpc<const M extends Query | Procedure>(\n agentOpts: Agent | AgentOptions,\n ns: NonNullable<unknown> extends XrpcOptions<M>\n ? Main<M>\n : Restricted<'This XRPC method requires an \"options\" argument'>,\n): Promise<XrpcResponse<M>>\nexport async function xrpc<const M extends Query | Procedure>(\n agentOpts: Agent | AgentOptions,\n ns: Main<M>,\n options: XrpcOptions<M>,\n): Promise<XrpcResponse<M>>\nexport async function xrpc<const M extends Query | Procedure>(\n agentOpts: Agent | AgentOptions,\n ns: Main<M>,\n options: XrpcOptions<M> = {} as XrpcOptions<M>,\n): Promise<XrpcResponse<M>> {\n const response = await xrpcSafe<M>(agentOpts, ns, options)\n if (response.success) return response\n else throw response\n}\n\n/**\n * Union type representing either a successful response or a failure.\n *\n * Both {@link XrpcResponse} and {@link XrpcFailure} have a `success` property\n * that can be used to discriminate between them.\n *\n * @typeParam M - The XRPC method type\n */\nexport type XrpcResult<M extends Procedure | Query> =\n | XrpcResponse<M>\n | XrpcFailure<M>\n\n/**\n * Makes an XRPC request without throwing on failure.\n *\n * Returns a discriminated union that can be checked via the `success` property.\n * This is useful for handling errors without try/catch blocks. This also allow\n * failure results to be typed with the method schema, which can provide better\n * type safety when handling errors (e.g. checking for specific error codes).\n *\n * @param agent - The {@link Agent} to use for making the request\n * @param ns - The lexicon method definition\n * @param options - Request {@link XrpcOptions options} (params, body, headers, etc.)\n * @returns Either a successful {@link XrpcResponse} or an {@link XrpcFailure}\n *\n * @example\n * ```typescript\n * const result = await xrpcSafe('https://example.com', app.bsky.actor.getProfile, {\n * params: { actor: 'alice.bsky.social' }\n * })\n *\n * if (result.success) {\n * console.log(result.body.displayName)\n * } else {\n * console.error('Request failed:', result.error)\n * }\n * ```\n */\nexport async function xrpcSafe<const M extends Query | Procedure>(\n agentOpts: Agent | AgentOptions,\n ns: NonNullable<unknown> extends XrpcOptions<M>\n ? Main<M>\n : Restricted<'This XRPC method requires an \"options\" argument'>,\n): Promise<XrpcResult<M>>\nexport async function xrpcSafe<const M extends Query | Procedure>(\n agentOpts: Agent | AgentOptions,\n ns: Main<M>,\n options: XrpcOptions<M>,\n): Promise<XrpcResult<M>>\nexport async function xrpcSafe<const M extends Query | Procedure>(\n agentOpts: Agent | AgentOptions,\n ns: Main<M>,\n options: XrpcOptions<M> = {} as XrpcOptions<M>,\n): Promise<XrpcResult<M>> {\n options.signal?.throwIfAborted()\n const method: M = getMain(ns)\n try {\n const agent = buildAgent(agentOpts)\n const url = xrpcRequestUrl(method, options)\n const request = xrpcRequestInit(method, options)\n const response = await agent.fetchHandler(url, request)\n return await XrpcResponse.fromFetchResponse<M>(method, response, options)\n } catch (cause) {\n return asXrpcFailure(method, cause)\n }\n}\n\nfunction xrpcRequestUrl<M extends Procedure | Query | Subscription>(\n method: M,\n options: CallOptions & { params?: Params },\n): `/xrpc/${NsidString}${'' | `?${string}`}` {\n const path = `/xrpc/${method.nsid}` as const\n\n const queryString = method.parameters\n ?.toURLSearchParams(options.params ?? {})\n .toString()\n\n return queryString ? (`${path}?${queryString}` as const) : path\n}\n\nfunction xrpcRequestInit<T extends Procedure | Query>(\n schema: T,\n options: CallOptions & {\n body?: LexValue | BinaryBodyInit\n encoding?: string\n },\n): RequestInit & { duplex?: 'half' } {\n const headers = buildAtprotoHeaders(options)\n\n // Tell the server what type of response we're expecting\n if (schema.output.encoding) {\n headers.set('accept', schema.output.encoding)\n }\n\n // Caller should not set content-type header\n if (headers.has('content-type')) {\n const contentType = headers.get('content-type')\n throw new TypeError(`Unexpected content-type header (${contentType})`)\n }\n\n // Requests with body\n if ('input' in schema) {\n const encodingHint = options.encoding\n const input = xrpcProcedureInput(schema, options, encodingHint)\n\n if (input) {\n headers.set('content-type', input.encoding)\n } else if (encodingHint != null) {\n throw new TypeError(`Unexpected encoding hint (${encodingHint})`)\n }\n\n return {\n duplex: 'half',\n redirect: 'follow',\n referrerPolicy: 'strict-origin-when-cross-origin', // (default)\n mode: 'cors', // (default)\n signal: options.signal,\n method: 'POST',\n headers,\n body: input?.body,\n }\n }\n\n // Requests without body\n return {\n duplex: 'half',\n redirect: 'follow',\n referrerPolicy: 'strict-origin-when-cross-origin', // (default)\n mode: 'cors', // (default)\n signal: options.signal,\n method: 'GET',\n headers,\n }\n}\n\nfunction xrpcProcedureInput(\n method: Procedure,\n options: CallOptions & { body?: LexValue | BinaryBodyInit },\n encodingHint?: string,\n): null | { body: BodyInit; encoding: string } {\n const { input } = method\n const { body } = options\n\n if (options.validateRequest) {\n input.schema?.check(body)\n }\n\n // Special handling for endpoints expecting application/json input\n if (input.encoding === 'application/json') {\n // @NOTE **NOT** using isLexValue here to avoid deep checks in order to\n // distinguish between LexValue and BinaryBodyInit.\n if (!isLexScalar(body) && !isPlainObject(body) && !Array.isArray(body)) {\n throw new TypeError(`Expected LexValue body, got ${typeof body}`)\n }\n\n return buildPayload(input, lexStringify(body), encodingHint)\n }\n\n // Other encodings will be sent unaltered (ie. as binary data)\n switch (typeof body) {\n case 'undefined':\n case 'string':\n return buildPayload(input, body, encodingHint)\n case 'object': {\n if (body === null) break\n if (\n ArrayBuffer.isView(body) ||\n body instanceof ArrayBuffer ||\n body instanceof ReadableStream\n ) {\n return buildPayload(input, body, encodingHint)\n } else if (isAsyncIterable(body)) {\n return buildPayload(input, toReadableStream(body), encodingHint)\n } else if (isBlobLike(body)) {\n return buildPayload(input, body, encodingHint || body.type)\n }\n }\n }\n\n throw new TypeError(\n `Invalid ${typeof body} body for ${input.encoding} encoding`,\n )\n}\n\nfunction buildPayload(\n schema: Payload,\n body: undefined | BodyInit,\n encodingHint?: string,\n): null | { body: BodyInit; encoding: string } {\n if (schema.encoding === undefined) {\n if (body !== undefined) {\n throw new TypeError(\n `Cannot send a ${typeof body} body with undefined encoding`,\n )\n }\n\n return null\n }\n\n if (body === undefined) {\n // This error would be returned by the server, but we can catch it earlier\n // to avoid un-necessary requests. Note that a content-length of 0 does not\n // necessary mean that the body is \"empty\" (e.g. an empty txt file).\n throw new TypeError(`A request body is expected but none was provided`)\n }\n\n const encoding = buildEncoding(schema, encodingHint)\n return { encoding, body }\n}\n\nfunction buildEncoding(schema: Payload, encodingHint?: string): string {\n // Should never happen (required for type safety)\n if (!schema.encoding) {\n throw new TypeError('Unexpected payload')\n }\n\n if (encodingHint?.length) {\n if (!schema.matchesEncoding(encodingHint)) {\n throw new TypeError(\n `Cannot send a body with content-type \"${encodingHint}\" for \"${schema.encoding}\" encoding`,\n )\n }\n return encodingHint\n }\n\n // Fallback\n\n if (schema.encoding === '*/*') {\n return 'application/octet-stream'\n }\n\n if (schema.encoding.startsWith('text/')) {\n return schema.encoding.includes('*')\n ? 'text/plain; charset=utf-8'\n : `${schema.encoding}; charset=utf-8`\n }\n\n if (!schema.encoding.includes('*')) {\n return schema.encoding\n }\n\n throw new TypeError(\n `Unable to determine payload encoding. Please provide a 'content-type' header matching ${schema.encoding}.`,\n )\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atproto/lex-client",
3
- "version": "0.0.13",
3
+ "version": "0.0.15",
4
4
  "license": "MIT",
5
5
  "description": "HTTP client for interacting with Lexicon based APIs",
6
6
  "keywords": [
@@ -37,14 +37,14 @@
37
37
  },
38
38
  "dependencies": {
39
39
  "tslib": "^2.8.1",
40
- "@atproto/lex-data": "^0.0.12",
41
- "@atproto/lex-json": "^0.0.12",
42
- "@atproto/lex-schema": "^0.0.13"
40
+ "@atproto/lex-data": "^0.0.13",
41
+ "@atproto/lex-json": "^0.0.13",
42
+ "@atproto/lex-schema": "^0.0.14"
43
43
  },
44
44
  "devDependencies": {
45
45
  "vitest": "^4.0.16",
46
- "@atproto/lex-cbor": "^0.0.12",
47
- "@atproto/lex-builder": "^0.0.16"
46
+ "@atproto/lex-cbor": "^0.0.14",
47
+ "@atproto/lex-builder": "^0.0.17"
48
48
  },
49
49
  "scripts": {
50
50
  "prebuild": "node ./scripts/lex-build.mjs",
package/src/agent.ts CHANGED
@@ -5,7 +5,7 @@ import { DidString } from '@atproto/lex-schema'
5
5
  *
6
6
  * The handler is responsible for adding the origin (protocol, hostname, and
7
7
  * port) to the provided path, typically based on authentication or service
8
- * configuration. The handler are also responsible for adding any necessary
8
+ * configuration. The handler can also be responsible for adding any necessary
9
9
  * headers, such as authorization tokens.
10
10
  *
11
11
  * @param path - The URL path (pathname + query parameters) without the origin
@@ -18,16 +18,23 @@ export type FetchHandler = (
18
18
  * origin. The origin (protocol, hostname, and port) must be added by this
19
19
  * {@link FetchHandler}, typically based on authentication or other factors.
20
20
  */
21
- path: string,
21
+ path: `/${string}`,
22
22
  init: RequestInit,
23
23
  ) => Promise<Response>
24
24
 
25
25
  /**
26
- * Core interface for making XRPC requests.
26
+ * Core interface for making XRPC requests towards a specific service.
27
27
  *
28
- * An Agent encapsulates an identity and request handling for AT Protocol
29
- * operations. It can represent an authenticated user session or an
30
- * unauthenticated service client.
28
+ * An {@link Agent} encapsulates an identity and request handling for AT
29
+ * Protocol operations. Agents will typically handle authentication, service URL
30
+ * resolution, and other request configuration, allowing client code to make
31
+ * requests without needing to manage these details directly. The key component
32
+ * of an Agent is the {@link FetchHandler}, which is responsible for
33
+ * constructing the full request URL and adding any necessary headers or
34
+ * authentication tokens. The Agent's `did` property represents the
35
+ * authenticated user's DID, if available, and can be used for operations that
36
+ * require knowledge of the user's identity (such as creating AT Protocol
37
+ * records).
31
38
  *
32
39
  * @see {@link buildAgent} for creating (simple) Agent instances.
33
40
  *
@@ -35,7 +42,10 @@ export type FetchHandler = (
35
42
  * ```typescript
36
43
  * const agent: Agent = {
37
44
  * did: 'did:plc:example123',
38
- * fetchHandler: (path, init) => fetch(new URL(path, 'https://bsky.social'), init)
45
+ * fetchHandler: async (path, init) => {
46
+ * const url = new URL(path, 'https://bsky.social')
47
+ * return fetch(url, init)
48
+ * }
39
49
  * }
40
50
  * ```
41
51
  */
@@ -46,6 +56,18 @@ export interface Agent {
46
56
  fetchHandler: FetchHandler
47
57
  }
48
58
 
59
+ export function isAgent(value: unknown): value is Agent {
60
+ return (
61
+ typeof value === 'object' &&
62
+ value !== null &&
63
+ 'fetchHandler' in value &&
64
+ typeof value.fetchHandler === 'function' &&
65
+ (!('did' in value) ||
66
+ value.did === undefined ||
67
+ typeof value.did === 'string')
68
+ )
69
+ }
70
+
49
71
  export type AgentConfig = {
50
72
  /**
51
73
  * The identifier (DID) of the user represented by this agent.
@@ -84,20 +106,20 @@ export type AgentOptions = AgentConfig | string | URL
84
106
  /**
85
107
  * Creates an {@link Agent} from various input types.
86
108
  *
87
- * This factory function accepts an existing Agent (returned as-is), a service URL,
88
- * or a full configuration object. It handles the common case of creating an
89
- * unauthenticated agent from just a service URL.
109
+ * This factory function accepts an existing Agent (returned as-is), a service
110
+ * URL, or a full configuration object. It handles the common case of creating
111
+ * an unauthenticated agent from just a service URL.
90
112
  *
91
113
  * @param options - Agent instance, configuration object, or service URL
92
114
  * @returns A configured Agent ready for making requests
93
115
  * @throws {TypeError} If fetch() is not available in the environment
94
116
  *
95
- * @example From URL string
117
+ * @example // From URL string
96
118
  * ```typescript
97
119
  * const agent = buildAgent('https://public.api.bsky.app')
98
120
  * ```
99
121
  *
100
- * @example From configuration
122
+ * @example // From configuration
101
123
  * ```typescript
102
124
  * const agent = buildAgent({
103
125
  * did: 'did:plc:example',
@@ -105,17 +127,12 @@ export type AgentOptions = AgentConfig | string | URL
105
127
  * headers: { 'Authorization': 'Bearer ...' }
106
128
  * })
107
129
  * ```
108
- *
109
- * @example Pass-through existing agent
110
- * ```typescript
111
- * const existing: Agent = { ... }
112
- * const agent = buildAgent(existing) // Returns existing unchanged
113
- * ```
114
130
  */
131
+ export function buildAgent<O extends Agent | AgentOptions>(
132
+ options: O,
133
+ ): O extends Agent ? O : Agent
115
134
  export function buildAgent(options: Agent | AgentOptions): Agent {
116
- if (typeof options === 'object' && 'fetchHandler' in options) {
117
- return options
118
- }
135
+ if (isAgent(options)) return options
119
136
 
120
137
  const config: Agent | AgentConfig =
121
138
  typeof options === 'string' || options instanceof URL
package/src/client.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { LexError, LexMap, LexValue, TypedLexMap } from '@atproto/lex-data'
1
+ import { LexMap, LexValue, TypedLexMap } from '@atproto/lex-data'
2
2
  import {
3
3
  AtIdentifierString,
4
4
  CidString,
@@ -282,11 +282,12 @@ export type ListRecord<Value extends LexMap> =
282
282
  * services. It provides type-safe methods for XRPC calls, record operations,
283
283
  * and blob handling.
284
284
  *
285
- * @example Basic usage
285
+ * @example // Basic usage
286
286
  * ```typescript
287
287
  * import { Client } from '@atproto/lex'
288
288
  *
289
- * const client = new Client(agent)
289
+ * const client = new Client(oauthSession)
290
+ *
290
291
  * const response = await client.xrpc(app.bsky.feed.getTimeline.main, {
291
292
  * params: { limit: 50 }
292
293
  * })
@@ -330,7 +331,7 @@ export class Client implements Agent {
330
331
 
331
332
  /**
332
333
  * The DID of the authenticated user.
333
- * @throws {LexError} with code 'AuthenticationRequired' if not authenticated
334
+ * @throws {Error} if not authenticated
334
335
  */
335
336
  get assertDid(): DidString {
336
337
  this.assertAuthenticated()
@@ -341,7 +342,7 @@ export class Client implements Agent {
341
342
  * Asserts that the client is authenticated.
342
343
  * Use as a type guard when you need to ensure authentication.
343
344
  *
344
- * @throws {LexError} with code 'AuthenticationRequired' if not authenticated
345
+ * @throws {Error} if not authenticated
345
346
  *
346
347
  * @example
347
348
  * ```typescript
@@ -351,7 +352,7 @@ export class Client implements Agent {
351
352
  * ```
352
353
  */
353
354
  public assertAuthenticated(): asserts this is { did: DidString } {
354
- if (!this.did) throw new LexError('AuthenticationRequired')
355
+ if (!this.did) throw new Error('Client is not authenticated')
355
356
  }
356
357
 
357
358
  /**
@@ -379,11 +380,18 @@ export class Client implements Agent {
379
380
  }
380
381
 
381
382
  /**
382
- * Low-level fetch handler for making requests.
383
+ * {@link Agent}'s {@link Agent.fetchHandler} implementation, which adds
384
+ * labelers and service proxying headers. This method allow a {@link Client}
385
+ * instance to be used directly as an {@link Agent} for another
386
+ * {@link Client}, enabling composition of headers (labelers, proxying, etc.).
387
+ *
383
388
  * @param path - The request path
384
389
  * @param init - Request initialization options
385
390
  */
386
- public fetchHandler(path: string, init: RequestInit): Promise<Response> {
391
+ public fetchHandler(
392
+ path: `/${string}`,
393
+ init: RequestInit,
394
+ ): Promise<Response> {
387
395
  const headers = buildAtprotoHeaders({
388
396
  headers: init.headers,
389
397
  service: this.service,
@@ -0,0 +1,346 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { IssueInvalidType, LexValidationError, l } from '@atproto/lex-schema'
3
+ import {
4
+ XrpcAuthenticationError,
5
+ XrpcInternalError,
6
+ XrpcInvalidResponseError,
7
+ XrpcResponseError,
8
+ XrpcUpstreamError,
9
+ asXrpcFailure,
10
+ } from './errors.js'
11
+
12
+ // Minimal method fixture
13
+ const testQuery = l.query(
14
+ 'io.example.test',
15
+ l.params(),
16
+ l.payload('application/json', l.object({ value: l.string() })),
17
+ ['TestError', 'AnotherError'],
18
+ )
19
+
20
+ const testQueryNoErrors = l.query(
21
+ 'io.example.noErrors',
22
+ l.params(),
23
+ l.payload('application/json', l.object({ value: l.string() })),
24
+ )
25
+
26
+ // ============================================================================
27
+ // XrpcResponseError
28
+ // ============================================================================
29
+
30
+ describe(XrpcResponseError, () => {
31
+ function createResponseError(
32
+ status: number,
33
+ errorCode: string,
34
+ message?: string,
35
+ ) {
36
+ const response = new Response(null, { status })
37
+ return new XrpcResponseError(testQuery, response, {
38
+ encoding: 'application/json',
39
+ body: { error: errorCode, message },
40
+ })
41
+ }
42
+
43
+ it('exposes status from the response', () => {
44
+ const err = createResponseError(404, 'NotFound')
45
+ expect(err.status).toBe(404)
46
+ })
47
+
48
+ it('exposes headers from the response', () => {
49
+ const response = new Response(null, {
50
+ status: 400,
51
+ headers: { 'X-Test': 'value' },
52
+ })
53
+ const err = new XrpcResponseError(testQuery, response, {
54
+ encoding: 'application/json',
55
+ body: { error: 'TestError' },
56
+ })
57
+ expect(err.headers.get('X-Test')).toBe('value')
58
+ })
59
+
60
+ it('exposes body from the payload', () => {
61
+ const err = createResponseError(400, 'TestError', 'details')
62
+ expect(err.body).toEqual({ error: 'TestError', message: 'details' })
63
+ })
64
+
65
+ describe('toDownstreamError', () => {
66
+ it('returns 502 for 5xx upstream errors', () => {
67
+ const err = createResponseError(
68
+ 500,
69
+ 'InternalServerError',
70
+ 'Upstream crashed',
71
+ )
72
+ const downstream = err.toDownstreamError()
73
+
74
+ expect(downstream.status).toBe(502)
75
+ expect(downstream.body).toEqual({
76
+ error: 'InternalServerError',
77
+ message: 'Upstream crashed',
78
+ })
79
+ })
80
+
81
+ it('preserves original status for 4xx errors', () => {
82
+ const err = createResponseError(404, 'NotFound', 'Record not found')
83
+ const downstream = err.toDownstreamError()
84
+
85
+ expect(downstream.status).toBe(404)
86
+ expect(downstream.body).toEqual({
87
+ error: 'NotFound',
88
+ message: 'Record not found',
89
+ })
90
+ })
91
+ })
92
+
93
+ describe('toJSON', () => {
94
+ it('returns the payload body', () => {
95
+ const err = createResponseError(400, 'TestError', 'message')
96
+ expect(err.toJSON()).toEqual({ error: 'TestError', message: 'message' })
97
+ })
98
+ })
99
+
100
+ describe('matchesSchemaErrors', () => {
101
+ it('returns true when error matches method declared errors', () => {
102
+ const err = createResponseError(400, 'TestError')
103
+ expect(err.matchesSchemaErrors()).toBe(true)
104
+ })
105
+
106
+ it('returns false for undeclared error codes', () => {
107
+ const err = createResponseError(400, 'UnknownError')
108
+ expect(err.matchesSchemaErrors()).toBe(false)
109
+ })
110
+
111
+ it('returns false when method has no declared errors', () => {
112
+ const response = new Response(null, { status: 400 })
113
+ const err = new XrpcResponseError(testQueryNoErrors, response, {
114
+ encoding: 'application/json',
115
+ body: { error: 'SomeError' },
116
+ })
117
+ expect(err.matchesSchemaErrors()).toBe(false)
118
+ })
119
+ })
120
+
121
+ describe('shouldRetry', () => {
122
+ it('returns true for retryable status codes', () => {
123
+ expect(createResponseError(429, 'RateLimit').shouldRetry()).toBe(true)
124
+ expect(createResponseError(500, 'Internal').shouldRetry()).toBe(true)
125
+ expect(createResponseError(502, 'BadGateway').shouldRetry()).toBe(true)
126
+ expect(createResponseError(503, 'Unavailable').shouldRetry()).toBe(true)
127
+ })
128
+
129
+ it('returns false for non-retryable status codes', () => {
130
+ expect(createResponseError(400, 'BadRequest').shouldRetry()).toBe(false)
131
+ expect(createResponseError(401, 'Unauthorized').shouldRetry()).toBe(false)
132
+ expect(createResponseError(404, 'NotFound').shouldRetry()).toBe(false)
133
+ })
134
+ })
135
+ })
136
+
137
+ // ============================================================================
138
+ // XrpcAuthenticationError
139
+ // ============================================================================
140
+
141
+ describe(XrpcAuthenticationError, () => {
142
+ it('is never retryable', () => {
143
+ const response = new Response(null, { status: 401 })
144
+ const err = new XrpcAuthenticationError(testQuery, response, {
145
+ encoding: 'application/json',
146
+ body: { error: 'AuthenticationRequired' },
147
+ })
148
+ expect(err.shouldRetry()).toBe(false)
149
+ })
150
+
151
+ it('parses WWW-Authenticate header', () => {
152
+ const response = new Response(null, {
153
+ status: 401,
154
+ headers: {
155
+ 'WWW-Authenticate': 'Bearer realm="api", error="InvalidToken"',
156
+ },
157
+ })
158
+ const err = new XrpcAuthenticationError(testQuery, response, {
159
+ encoding: 'application/json',
160
+ body: { error: 'AuthenticationRequired' },
161
+ })
162
+ expect(err.wwwAuthenticate).toHaveProperty('Bearer')
163
+ })
164
+
165
+ it('returns empty object when no WWW-Authenticate header', () => {
166
+ const response = new Response(null, { status: 401 })
167
+ const err = new XrpcAuthenticationError(testQuery, response, {
168
+ encoding: 'application/json',
169
+ body: { error: 'AuthenticationRequired' },
170
+ })
171
+ expect(err.wwwAuthenticate).toEqual({})
172
+ })
173
+
174
+ it('toDownstreamError always returns 401', () => {
175
+ const response = new Response(null, { status: 401 })
176
+ const err = new XrpcAuthenticationError(testQuery, response, {
177
+ encoding: 'application/json',
178
+ body: { error: 'AuthenticationRequired', message: 'No token' },
179
+ })
180
+ const downstream = err.toDownstreamError()
181
+
182
+ expect(downstream.status).toBe(401)
183
+ expect(downstream.body).toEqual({
184
+ error: 'AuthenticationRequired',
185
+ message: 'No token',
186
+ })
187
+ })
188
+ })
189
+
190
+ // ============================================================================
191
+ // XrpcUpstreamError
192
+ // ============================================================================
193
+
194
+ describe(XrpcUpstreamError, () => {
195
+ it('has error code UpstreamFailure', () => {
196
+ const response = new Response(null, { status: 200 })
197
+ const err = new XrpcUpstreamError(testQuery, response)
198
+ expect(err.error).toBe('UpstreamFailure')
199
+ })
200
+
201
+ it('toDownstreamError returns 502', () => {
202
+ const response = new Response(null, { status: 200 })
203
+ const err = new XrpcUpstreamError(testQuery, response)
204
+ const downstream = err.toDownstreamError()
205
+ expect(downstream.status).toBe(502)
206
+ })
207
+
208
+ it('shouldRetry is true for retryable status codes', () => {
209
+ const response = new Response(null, { status: 502 })
210
+ const err = new XrpcUpstreamError(testQuery, response)
211
+ expect(err.shouldRetry()).toBe(true)
212
+ })
213
+
214
+ it('shouldRetry is false for non-retryable status codes', () => {
215
+ const response = new Response(null, { status: 200 })
216
+ const err = new XrpcUpstreamError(testQuery, response)
217
+ expect(err.shouldRetry()).toBe(false)
218
+ })
219
+ })
220
+
221
+ // ============================================================================
222
+ // XrpcInvalidResponseError
223
+ // ============================================================================
224
+
225
+ describe(XrpcInvalidResponseError, () => {
226
+ it('extends XrpcUpstreamError', () => {
227
+ const response = new Response(null, { status: 200 })
228
+ const validationError = new LexValidationError([
229
+ new IssueInvalidType([], 42, ['string']),
230
+ ])
231
+ const err = new XrpcInvalidResponseError(
232
+ testQuery,
233
+ response,
234
+ { encoding: 'application/json', body: { value: 42 } },
235
+ validationError,
236
+ )
237
+
238
+ expect(err).toBeInstanceOf(XrpcUpstreamError)
239
+ expect(err.error).toBe('UpstreamFailure')
240
+ expect(err.cause).toBe(validationError)
241
+ })
242
+
243
+ it('includes validation error message', () => {
244
+ const validationError = new LexValidationError([
245
+ new IssueInvalidType([], 42, ['string']),
246
+ ])
247
+ const err = new XrpcInvalidResponseError(
248
+ testQuery,
249
+ new Response(null, { status: 200 }),
250
+ { encoding: 'application/json', body: { value: 42 } },
251
+ validationError,
252
+ )
253
+
254
+ expect(err.message).toContain('Invalid response:')
255
+ expect(err.message).toContain(validationError.message)
256
+ })
257
+
258
+ it('toDownstreamError returns 502', () => {
259
+ const validationError = new LexValidationError([
260
+ new IssueInvalidType([], 42, ['string']),
261
+ ])
262
+ const err = new XrpcInvalidResponseError(
263
+ testQuery,
264
+ new Response(null, { status: 200 }),
265
+ { encoding: 'application/json', body: { value: 42 } },
266
+ validationError,
267
+ )
268
+ const downstream = err.toDownstreamError()
269
+ expect(downstream.status).toBe(502)
270
+ })
271
+ })
272
+
273
+ // ============================================================================
274
+ // XrpcInternalError
275
+ // ============================================================================
276
+
277
+ describe(XrpcInternalError, () => {
278
+ it('has error code InternalServerError', () => {
279
+ const err = new XrpcInternalError(testQuery)
280
+ expect(err.error).toBe('InternalServerError')
281
+ })
282
+
283
+ it('is always retryable', () => {
284
+ const err = new XrpcInternalError(testQuery)
285
+ expect(err.shouldRetry()).toBe(true)
286
+ })
287
+
288
+ it('toJSON does not expose internal details', () => {
289
+ const err = new XrpcInternalError(
290
+ testQuery,
291
+ 'Secret database connection string leaked',
292
+ )
293
+ const json = err.toJSON()
294
+
295
+ expect(json.error).toBe('InternalServerError')
296
+ expect(json.message).toBe('Internal Server Error')
297
+ expect(json.message).not.toContain('Secret')
298
+ })
299
+
300
+ it('toDownstreamError returns 500', () => {
301
+ const err = new XrpcInternalError(testQuery, 'internal details')
302
+ const downstream = err.toDownstreamError()
303
+
304
+ expect(downstream.status).toBe(500)
305
+ expect(downstream.body.error).toBe('InternalServerError')
306
+ expect(downstream.body.message).toBe('Internal Server Error')
307
+ })
308
+ })
309
+
310
+ // ============================================================================
311
+ // asXrpcFailure
312
+ // ============================================================================
313
+
314
+ describe('asXrpcFailure', () => {
315
+ it('returns existing XrpcResponseError for the same method', () => {
316
+ const response = new Response(null, { status: 400 })
317
+ const err = new XrpcResponseError(testQuery, response, {
318
+ encoding: 'application/json',
319
+ body: { error: 'TestError' },
320
+ })
321
+ expect(asXrpcFailure(testQuery, err)).toBe(err)
322
+ })
323
+
324
+ it('wraps unknown errors in XrpcInternalError', () => {
325
+ const err = new TypeError('fetch failed')
326
+ const failure = asXrpcFailure(testQuery, err)
327
+
328
+ expect(failure).toBeInstanceOf(XrpcInternalError)
329
+ expect(failure.cause).toBe(err)
330
+ })
331
+
332
+ it('wraps XrpcError for a different method in XrpcInternalError', () => {
333
+ const otherQuery = l.query(
334
+ 'io.example.other',
335
+ l.params(),
336
+ l.payload('application/json', l.object({ value: l.string() })),
337
+ )
338
+ const response = new Response(null, { status: 400 })
339
+ const err = new XrpcResponseError(otherQuery, response, {
340
+ encoding: 'application/json',
341
+ body: { error: 'TestError' },
342
+ })
343
+ const failure = asXrpcFailure(testQuery, err)
344
+ expect(failure).toBeInstanceOf(XrpcInternalError)
345
+ })
346
+ })