@atproto/lex-client 0.0.15 → 0.0.17
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/CHANGELOG.md +28 -0
- package/dist/agent.d.ts +1 -1
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +7 -5
- package/dist/agent.js.map +1 -1
- package/dist/client.d.ts +24 -21
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +10 -7
- package/dist/client.js.map +1 -1
- package/dist/errors.d.ts +29 -17
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +43 -16
- package/dist/errors.js.map +1 -1
- package/dist/response.d.ts +61 -6
- package/dist/response.d.ts.map +1 -1
- package/dist/response.js +59 -40
- package/dist/response.js.map +1 -1
- package/dist/types.d.ts +8 -37
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +4 -0
- package/dist/types.js.map +1 -1
- package/dist/util.d.ts +15 -27
- package/dist/util.d.ts.map +1 -1
- package/dist/util.js +21 -6
- package/dist/util.js.map +1 -1
- package/dist/xrpc.d.ts +40 -15
- package/dist/xrpc.d.ts.map +1 -1
- package/dist/xrpc.js +6 -2
- package/dist/xrpc.js.map +1 -1
- package/package.json +6 -6
- package/src/agent.test.ts +216 -0
- package/src/agent.ts +8 -6
- package/src/client.ts +81 -31
- package/src/errors.test.ts +77 -8
- package/src/errors.ts +58 -21
- package/src/response.ts +186 -63
- package/src/types.ts +17 -40
- package/src/util.test.ts +333 -0
- package/src/util.ts +42 -36
- package/src/xrpc.test.ts +1453 -0
- package/src/xrpc.ts +76 -28
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
2
|
+
import {
|
|
3
|
+
type Agent,
|
|
4
|
+
type AgentConfig,
|
|
5
|
+
type FetchHandler,
|
|
6
|
+
buildAgent,
|
|
7
|
+
isAgent,
|
|
8
|
+
} from './agent.js'
|
|
9
|
+
|
|
10
|
+
// ============================================================================
|
|
11
|
+
// isAgent
|
|
12
|
+
// ============================================================================
|
|
13
|
+
|
|
14
|
+
describe(isAgent, () => {
|
|
15
|
+
it('returns true for a valid agent with did', () => {
|
|
16
|
+
const agent: Agent = {
|
|
17
|
+
did: 'did:plc:example',
|
|
18
|
+
fetchHandler: async () => new Response(),
|
|
19
|
+
}
|
|
20
|
+
expect(isAgent(agent)).toBe(true)
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it('returns true for an agent without did', () => {
|
|
24
|
+
const agent = { fetchHandler: async () => new Response() }
|
|
25
|
+
expect(isAgent(agent)).toBe(true)
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('returns true when did is undefined', () => {
|
|
29
|
+
const agent = { did: undefined, fetchHandler: async () => new Response() }
|
|
30
|
+
expect(isAgent(agent)).toBe(true)
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('returns false for null', () => {
|
|
34
|
+
expect(isAgent(null)).toBe(false)
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('returns false for non-objects', () => {
|
|
38
|
+
expect(isAgent('string')).toBe(false)
|
|
39
|
+
expect(isAgent(42)).toBe(false)
|
|
40
|
+
expect(isAgent(undefined)).toBe(false)
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('returns false when fetchHandler is not a function', () => {
|
|
44
|
+
expect(isAgent({ fetchHandler: 'not-a-function' })).toBe(false)
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('returns false when did is not a string', () => {
|
|
48
|
+
expect(isAgent({ did: 42, fetchHandler: async () => new Response() })).toBe(
|
|
49
|
+
false,
|
|
50
|
+
)
|
|
51
|
+
})
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
// ============================================================================
|
|
55
|
+
// buildAgent
|
|
56
|
+
// ============================================================================
|
|
57
|
+
|
|
58
|
+
describe(buildAgent, () => {
|
|
59
|
+
describe('from Agent', () => {
|
|
60
|
+
it('returns the same agent instance', () => {
|
|
61
|
+
const agent: Agent = {
|
|
62
|
+
did: 'did:plc:example',
|
|
63
|
+
fetchHandler: async () => new Response(),
|
|
64
|
+
}
|
|
65
|
+
expect(buildAgent(agent)).toBe(agent)
|
|
66
|
+
})
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
describe('from FetchHandler', () => {
|
|
70
|
+
it('wraps a function as an agent', () => {
|
|
71
|
+
const handler: FetchHandler = async () => new Response()
|
|
72
|
+
const agent = buildAgent(handler)
|
|
73
|
+
|
|
74
|
+
expect(agent.did).toBeUndefined()
|
|
75
|
+
expect(typeof agent.fetchHandler).toBe('function')
|
|
76
|
+
})
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
describe('from string URL', () => {
|
|
80
|
+
it('creates an agent that prepends the service URL', async () => {
|
|
81
|
+
const fetchFn = vi.fn<typeof globalThis.fetch>(async () =>
|
|
82
|
+
Response.json({ ok: true }),
|
|
83
|
+
)
|
|
84
|
+
const agent = buildAgent({
|
|
85
|
+
service: 'https://example.com',
|
|
86
|
+
fetch: fetchFn,
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
await agent.fetchHandler('/xrpc/io.example.test', { method: 'GET' })
|
|
90
|
+
|
|
91
|
+
expect(fetchFn).toHaveBeenCalledOnce()
|
|
92
|
+
const [url, init] = fetchFn.mock.calls[0]
|
|
93
|
+
expect(url).toEqual(
|
|
94
|
+
new URL('/xrpc/io.example.test', 'https://example.com'),
|
|
95
|
+
)
|
|
96
|
+
expect(init?.method).toBe('GET')
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
it('has undefined did', () => {
|
|
100
|
+
const agent = buildAgent('https://example.com')
|
|
101
|
+
expect(agent.did).toBeUndefined()
|
|
102
|
+
})
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
describe('from URL instance', () => {
|
|
106
|
+
it('creates an agent with the URL as service', async () => {
|
|
107
|
+
const fetchFn = vi.fn<typeof globalThis.fetch>(async () =>
|
|
108
|
+
Response.json({ ok: true }),
|
|
109
|
+
)
|
|
110
|
+
const agent = buildAgent({
|
|
111
|
+
service: new URL('https://example.com'),
|
|
112
|
+
fetch: fetchFn,
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
await agent.fetchHandler('/xrpc/io.example.test', { method: 'GET' })
|
|
116
|
+
|
|
117
|
+
expect(fetchFn).toHaveBeenCalledOnce()
|
|
118
|
+
const [url] = fetchFn.mock.calls[0]
|
|
119
|
+
expect(url).toEqual(
|
|
120
|
+
new URL('/xrpc/io.example.test', 'https://example.com'),
|
|
121
|
+
)
|
|
122
|
+
})
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
describe('from AgentConfig', () => {
|
|
126
|
+
it('exposes did from config', () => {
|
|
127
|
+
const agent = buildAgent({
|
|
128
|
+
did: 'did:plc:test123',
|
|
129
|
+
service: 'https://example.com',
|
|
130
|
+
})
|
|
131
|
+
expect(agent.did).toBe('did:plc:test123')
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
it('reflects did changes on the config object', () => {
|
|
135
|
+
const config: AgentConfig = {
|
|
136
|
+
did: 'did:plc:original',
|
|
137
|
+
service: 'https://example.com',
|
|
138
|
+
}
|
|
139
|
+
const agent = buildAgent(config)
|
|
140
|
+
expect(agent.did).toBe('did:plc:original')
|
|
141
|
+
|
|
142
|
+
config.did = 'did:plc:updated'
|
|
143
|
+
expect(agent.did).toBe('did:plc:updated')
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
it('throws TypeError when fetch is not available', () => {
|
|
147
|
+
const originalFetch = globalThis.fetch
|
|
148
|
+
try {
|
|
149
|
+
// @ts-expect-error removing fetch to simulate missing environment
|
|
150
|
+
globalThis.fetch = undefined
|
|
151
|
+
expect(() => buildAgent({ service: 'https://example.com' })).toThrow(
|
|
152
|
+
TypeError,
|
|
153
|
+
)
|
|
154
|
+
} finally {
|
|
155
|
+
globalThis.fetch = originalFetch
|
|
156
|
+
}
|
|
157
|
+
})
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
describe('headers', () => {
|
|
161
|
+
it('sends config headers when no request headers', async () => {
|
|
162
|
+
const fetchFn = vi.fn<typeof globalThis.fetch>(async () =>
|
|
163
|
+
Response.json({}),
|
|
164
|
+
)
|
|
165
|
+
const agent = buildAgent({
|
|
166
|
+
service: 'https://example.com',
|
|
167
|
+
headers: { Authorization: 'Bearer token123' },
|
|
168
|
+
fetch: fetchFn,
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
await agent.fetchHandler('/xrpc/test', { method: 'GET' })
|
|
172
|
+
|
|
173
|
+
const [, init] = fetchFn.mock.calls[0]
|
|
174
|
+
expect(init?.headers).toEqual({ Authorization: 'Bearer token123' })
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
it('sends request headers when no config headers', async () => {
|
|
178
|
+
const fetchFn = vi.fn<typeof globalThis.fetch>(async () =>
|
|
179
|
+
Response.json({}),
|
|
180
|
+
)
|
|
181
|
+
const agent = buildAgent({
|
|
182
|
+
service: 'https://example.com',
|
|
183
|
+
fetch: fetchFn,
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
await agent.fetchHandler('/xrpc/test', {
|
|
187
|
+
method: 'GET',
|
|
188
|
+
headers: { 'X-Custom': 'value' },
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
const [, init] = fetchFn.mock.calls[0]
|
|
192
|
+
expect(init?.headers).toEqual({ 'X-Custom': 'value' })
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
it('merges config and request headers, with request taking priority', async () => {
|
|
196
|
+
const fetchFn = vi.fn<typeof globalThis.fetch>(async () =>
|
|
197
|
+
Response.json({}),
|
|
198
|
+
)
|
|
199
|
+
const agent = buildAgent({
|
|
200
|
+
service: 'https://example.com',
|
|
201
|
+
headers: { Authorization: 'Bearer default', 'X-Default': 'yes' },
|
|
202
|
+
fetch: fetchFn,
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
await agent.fetchHandler('/xrpc/test', {
|
|
206
|
+
method: 'GET',
|
|
207
|
+
headers: { Authorization: 'Bearer override' },
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
const [, init] = fetchFn.mock.calls[0]
|
|
211
|
+
const headers = new Headers(init?.headers)
|
|
212
|
+
expect(headers.get('Authorization')).toBe('Bearer override')
|
|
213
|
+
expect(headers.get('X-Default')).toBe('yes')
|
|
214
|
+
})
|
|
215
|
+
})
|
|
216
|
+
})
|
package/src/agent.ts
CHANGED
|
@@ -101,7 +101,7 @@ export type AgentConfig = {
|
|
|
101
101
|
*
|
|
102
102
|
* Can be a full {@link AgentConfig} object, or a simple service URL string/{@link URL}.
|
|
103
103
|
*/
|
|
104
|
-
export type AgentOptions = AgentConfig | string | URL
|
|
104
|
+
export type AgentOptions = AgentConfig | FetchHandler | string | URL
|
|
105
105
|
|
|
106
106
|
/**
|
|
107
107
|
* Creates an {@link Agent} from various input types.
|
|
@@ -132,12 +132,14 @@ export function buildAgent<O extends Agent | AgentOptions>(
|
|
|
132
132
|
options: O,
|
|
133
133
|
): O extends Agent ? O : Agent
|
|
134
134
|
export function buildAgent(options: Agent | AgentOptions): Agent {
|
|
135
|
-
if (isAgent(options)) return options
|
|
136
|
-
|
|
137
135
|
const config: Agent | AgentConfig =
|
|
138
|
-
typeof options === '
|
|
139
|
-
? { did: undefined,
|
|
140
|
-
: options
|
|
136
|
+
typeof options === 'function'
|
|
137
|
+
? { did: undefined, fetchHandler: options }
|
|
138
|
+
: typeof options === 'string' || options instanceof URL
|
|
139
|
+
? { did: undefined, service: options }
|
|
140
|
+
: options
|
|
141
|
+
|
|
142
|
+
if (isAgent(config)) return config
|
|
141
143
|
|
|
142
144
|
const { service, fetch = globalThis.fetch } = config
|
|
143
145
|
|
package/src/client.ts
CHANGED
|
@@ -20,10 +20,24 @@ import {
|
|
|
20
20
|
import { Agent, AgentOptions, buildAgent } from './agent.js'
|
|
21
21
|
import { XrpcFailure } from './errors.js'
|
|
22
22
|
import { com } from './lexicons/index.js'
|
|
23
|
-
import {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
23
|
+
import {
|
|
24
|
+
XrpcResponse,
|
|
25
|
+
XrpcResponseBody,
|
|
26
|
+
XrpcResponseOptions,
|
|
27
|
+
} from './response.js'
|
|
28
|
+
import { BinaryBodyInit, Service } from './types.js'
|
|
29
|
+
import {
|
|
30
|
+
XrpcRequestHeadersOptions,
|
|
31
|
+
applyDefaults,
|
|
32
|
+
buildXrpcRequestHeaders,
|
|
33
|
+
} from './util.js'
|
|
34
|
+
import {
|
|
35
|
+
XrpcOptions,
|
|
36
|
+
XrpcRequestParams,
|
|
37
|
+
XrpcRequestProcessingOptions,
|
|
38
|
+
xrpc,
|
|
39
|
+
xrpcSafe,
|
|
40
|
+
} from './xrpc.js'
|
|
27
41
|
|
|
28
42
|
export type {
|
|
29
43
|
AtIdentifierString,
|
|
@@ -58,13 +72,13 @@ export type {
|
|
|
58
72
|
* }
|
|
59
73
|
* ```
|
|
60
74
|
*/
|
|
61
|
-
export type ClientOptions =
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
|
|
75
|
+
export type ClientOptions = XrpcRequestHeadersOptions &
|
|
76
|
+
Pick<XrpcRequestProcessingOptions, 'validateRequest'> &
|
|
77
|
+
XrpcResponseOptions
|
|
78
|
+
|
|
79
|
+
export type ActionOptions = {
|
|
80
|
+
/** AbortSignal to cancel the request. */
|
|
81
|
+
signal?: AbortSignal
|
|
68
82
|
}
|
|
69
83
|
|
|
70
84
|
/**
|
|
@@ -87,7 +101,7 @@ export type ClientOptions = {
|
|
|
87
101
|
export type Action<I = any, O = any> = (
|
|
88
102
|
client: Client,
|
|
89
103
|
input: I,
|
|
90
|
-
options:
|
|
104
|
+
options: ActionOptions,
|
|
91
105
|
) => O | Promise<O>
|
|
92
106
|
|
|
93
107
|
/**
|
|
@@ -109,7 +123,10 @@ export type InferActionOutput<A extends Action> =
|
|
|
109
123
|
*
|
|
110
124
|
* @see {@link Client.createRecord}
|
|
111
125
|
*/
|
|
112
|
-
export type CreateRecordOptions =
|
|
126
|
+
export type CreateRecordOptions = Omit<
|
|
127
|
+
XrpcOptions<typeof com.atproto.repo.createRecord.main>,
|
|
128
|
+
'body'
|
|
129
|
+
> & {
|
|
113
130
|
/** Repository identifier (DID or handle). Defaults to authenticated user's DID. */
|
|
114
131
|
repo?: AtIdentifierString
|
|
115
132
|
/** Compare-and-swap on the repo commit. If specified, must match current commit. */
|
|
@@ -123,7 +140,10 @@ export type CreateRecordOptions = CallOptions & {
|
|
|
123
140
|
*
|
|
124
141
|
* @see {@link Client.deleteRecord}
|
|
125
142
|
*/
|
|
126
|
-
export type DeleteRecordOptions =
|
|
143
|
+
export type DeleteRecordOptions = Omit<
|
|
144
|
+
XrpcOptions<typeof com.atproto.repo.deleteRecord.main>,
|
|
145
|
+
'params'
|
|
146
|
+
> & {
|
|
127
147
|
/** Repository identifier (DID or handle). Defaults to authenticated user's DID. */
|
|
128
148
|
repo?: AtIdentifierString
|
|
129
149
|
/** Compare-and-swap on the repo commit. If specified, must match current commit. */
|
|
@@ -137,7 +157,10 @@ export type DeleteRecordOptions = CallOptions & {
|
|
|
137
157
|
*
|
|
138
158
|
* @see {@link Client.getRecord}
|
|
139
159
|
*/
|
|
140
|
-
export type GetRecordOptions =
|
|
160
|
+
export type GetRecordOptions = Omit<
|
|
161
|
+
XrpcOptions<typeof com.atproto.repo.getRecord.main>,
|
|
162
|
+
'params'
|
|
163
|
+
> & {
|
|
141
164
|
/** Repository identifier (DID or handle). Defaults to authenticated user's DID. */
|
|
142
165
|
repo?: AtIdentifierString
|
|
143
166
|
}
|
|
@@ -147,7 +170,10 @@ export type GetRecordOptions = CallOptions & {
|
|
|
147
170
|
*
|
|
148
171
|
* @see {@link Client.putRecord}
|
|
149
172
|
*/
|
|
150
|
-
export type PutRecordOptions =
|
|
173
|
+
export type PutRecordOptions = Omit<
|
|
174
|
+
XrpcOptions<typeof com.atproto.repo.putRecord.main>,
|
|
175
|
+
'body'
|
|
176
|
+
> & {
|
|
151
177
|
/** Repository identifier (DID or handle). Defaults to authenticated user's DID. */
|
|
152
178
|
repo?: AtIdentifierString
|
|
153
179
|
/** Compare-and-swap on the repo commit. If specified, must match current commit. */
|
|
@@ -163,7 +189,10 @@ export type PutRecordOptions = CallOptions & {
|
|
|
163
189
|
*
|
|
164
190
|
* @see {@link Client.listRecords}
|
|
165
191
|
*/
|
|
166
|
-
export type ListRecordsOptions =
|
|
192
|
+
export type ListRecordsOptions = Omit<
|
|
193
|
+
XrpcOptions<typeof com.atproto.repo.listRecords.main>,
|
|
194
|
+
'params'
|
|
195
|
+
> & {
|
|
167
196
|
/** Repository identifier (DID or handle). Defaults to authenticated user's DID. */
|
|
168
197
|
repo?: AtIdentifierString
|
|
169
198
|
/** Maximum number of records to return. */
|
|
@@ -174,6 +203,16 @@ export type ListRecordsOptions = CallOptions & {
|
|
|
174
203
|
reverse?: boolean
|
|
175
204
|
}
|
|
176
205
|
|
|
206
|
+
export type UploadBlobOptions = Omit<
|
|
207
|
+
XrpcOptions<typeof com.atproto.repo.uploadBlob.main>,
|
|
208
|
+
'body'
|
|
209
|
+
>
|
|
210
|
+
|
|
211
|
+
export type GetBlobOptions = Omit<
|
|
212
|
+
XrpcOptions<typeof com.atproto.sync.getBlob.main>,
|
|
213
|
+
'params'
|
|
214
|
+
>
|
|
215
|
+
|
|
177
216
|
export type RecordKeyOptions<
|
|
178
217
|
T extends RecordSchema,
|
|
179
218
|
AlsoOptionalWhenRecordKeyIs extends LexiconRecordKey = never,
|
|
@@ -315,11 +354,22 @@ export class Client implements Agent {
|
|
|
315
354
|
/** Set of labeler DIDs specific to this client instance. */
|
|
316
355
|
public readonly labelers: Set<DidString>
|
|
317
356
|
|
|
357
|
+
public readonly xrpcDefaults: {
|
|
358
|
+
readonly validateRequest: boolean
|
|
359
|
+
readonly validateResponse: boolean
|
|
360
|
+
readonly strictResponseProcessing: boolean
|
|
361
|
+
}
|
|
362
|
+
|
|
318
363
|
constructor(agent: Agent | AgentOptions, options: ClientOptions = {}) {
|
|
319
364
|
this.agent = buildAgent(agent)
|
|
320
365
|
this.service = options.service
|
|
321
366
|
this.labelers = new Set(options.labelers)
|
|
322
367
|
this.headers = new Headers(options.headers)
|
|
368
|
+
this.xrpcDefaults = Object.freeze({
|
|
369
|
+
validateRequest: options.validateRequest ?? false,
|
|
370
|
+
validateResponse: options.validateResponse ?? true,
|
|
371
|
+
strictResponseProcessing: options.strictResponseProcessing ?? true,
|
|
372
|
+
})
|
|
323
373
|
}
|
|
324
374
|
|
|
325
375
|
/**
|
|
@@ -392,7 +442,7 @@ export class Client implements Agent {
|
|
|
392
442
|
path: `/${string}`,
|
|
393
443
|
init: RequestInit,
|
|
394
444
|
): Promise<Response> {
|
|
395
|
-
const headers =
|
|
445
|
+
const headers = buildXrpcRequestHeaders({
|
|
396
446
|
headers: init.headers,
|
|
397
447
|
service: this.service,
|
|
398
448
|
labelers: [
|
|
@@ -454,7 +504,7 @@ export class Client implements Agent {
|
|
|
454
504
|
ns: Main<M>,
|
|
455
505
|
options: XrpcOptions<M> = {} as XrpcOptions<M>,
|
|
456
506
|
): Promise<XrpcResponse<M>> {
|
|
457
|
-
return xrpc(this, ns, options)
|
|
507
|
+
return xrpc(this, ns, applyDefaults(options, this.xrpcDefaults))
|
|
458
508
|
}
|
|
459
509
|
|
|
460
510
|
/**
|
|
@@ -493,7 +543,7 @@ export class Client implements Agent {
|
|
|
493
543
|
ns: Main<M>,
|
|
494
544
|
options: XrpcOptions<M> = {} as XrpcOptions<M>,
|
|
495
545
|
): Promise<XrpcResponse<M> | XrpcFailure<M>> {
|
|
496
|
-
return xrpcSafe(this, ns, options)
|
|
546
|
+
return xrpcSafe(this, ns, applyDefaults(options, this.xrpcDefaults))
|
|
497
547
|
}
|
|
498
548
|
|
|
499
549
|
/**
|
|
@@ -649,14 +699,8 @@ export class Client implements Agent {
|
|
|
649
699
|
* console.log(response.body.blob) // Use this ref in records
|
|
650
700
|
* ```
|
|
651
701
|
*/
|
|
652
|
-
async uploadBlob(
|
|
653
|
-
body
|
|
654
|
-
options?: CallOptions & { encoding?: `${string}/${string}` },
|
|
655
|
-
) {
|
|
656
|
-
return this.xrpc(com.atproto.repo.uploadBlob.main, {
|
|
657
|
-
...options,
|
|
658
|
-
body,
|
|
659
|
-
})
|
|
702
|
+
async uploadBlob(body: BinaryBodyInit, options?: UploadBlobOptions) {
|
|
703
|
+
return this.xrpc(com.atproto.repo.uploadBlob.main, { ...options, body })
|
|
660
704
|
}
|
|
661
705
|
|
|
662
706
|
/**
|
|
@@ -666,7 +710,7 @@ export class Client implements Agent {
|
|
|
666
710
|
* @param cid - The CID of the blob
|
|
667
711
|
* @param options - Call options
|
|
668
712
|
*/
|
|
669
|
-
async getBlob(did: DidString, cid: CidString, options?:
|
|
713
|
+
async getBlob(did: DidString, cid: CidString, options?: GetBlobOptions) {
|
|
670
714
|
return this.xrpc(com.atproto.sync.getBlob.main, {
|
|
671
715
|
...options,
|
|
672
716
|
params: { did, cid },
|
|
@@ -723,7 +767,13 @@ export class Client implements Agent {
|
|
|
723
767
|
: T extends Query
|
|
724
768
|
? XrpcRequestParams<T>
|
|
725
769
|
: never,
|
|
726
|
-
options?:
|
|
770
|
+
options?: T extends Action
|
|
771
|
+
? ActionOptions
|
|
772
|
+
: T extends Procedure
|
|
773
|
+
? Omit<XrpcOptions<T>, 'body'>
|
|
774
|
+
: T extends Query
|
|
775
|
+
? Omit<XrpcOptions<T>, 'params'>
|
|
776
|
+
: never,
|
|
727
777
|
): Promise<
|
|
728
778
|
T extends Action
|
|
729
779
|
? InferActionOutput<T>
|
|
@@ -736,7 +786,7 @@ export class Client implements Agent {
|
|
|
736
786
|
public async call(
|
|
737
787
|
ns: Main<Action> | Main<Procedure> | Main<Query>,
|
|
738
788
|
arg?: LexValue | Params,
|
|
739
|
-
options:
|
|
789
|
+
options: ActionOptions = {},
|
|
740
790
|
): Promise<unknown> {
|
|
741
791
|
const method = getMain(ns)
|
|
742
792
|
|
package/src/errors.test.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { describe, expect, it } from 'vitest'
|
|
|
2
2
|
import { IssueInvalidType, LexValidationError, l } from '@atproto/lex-schema'
|
|
3
3
|
import {
|
|
4
4
|
XrpcAuthenticationError,
|
|
5
|
+
XrpcFetchError,
|
|
5
6
|
XrpcInternalError,
|
|
6
7
|
XrpcInvalidResponseError,
|
|
7
8
|
XrpcResponseError,
|
|
@@ -13,14 +14,14 @@ import {
|
|
|
13
14
|
const testQuery = l.query(
|
|
14
15
|
'io.example.test',
|
|
15
16
|
l.params(),
|
|
16
|
-
l.
|
|
17
|
+
l.jsonPayload({ value: l.string() }),
|
|
17
18
|
['TestError', 'AnotherError'],
|
|
18
19
|
)
|
|
19
20
|
|
|
20
21
|
const testQueryNoErrors = l.query(
|
|
21
22
|
'io.example.noErrors',
|
|
22
23
|
l.params(),
|
|
23
|
-
l.
|
|
24
|
+
l.jsonPayload({ value: l.string() }),
|
|
24
25
|
)
|
|
25
26
|
|
|
26
27
|
// ============================================================================
|
|
@@ -42,6 +43,7 @@ describe(XrpcResponseError, () => {
|
|
|
42
43
|
|
|
43
44
|
it('exposes status from the response', () => {
|
|
44
45
|
const err = createResponseError(404, 'NotFound')
|
|
46
|
+
expect(err.reason).toBe(err)
|
|
45
47
|
expect(err.status).toBe(404)
|
|
46
48
|
})
|
|
47
49
|
|
|
@@ -54,6 +56,7 @@ describe(XrpcResponseError, () => {
|
|
|
54
56
|
encoding: 'application/json',
|
|
55
57
|
body: { error: 'TestError' },
|
|
56
58
|
})
|
|
59
|
+
expect(err.reason).toBe(err)
|
|
57
60
|
expect(err.headers.get('X-Test')).toBe('value')
|
|
58
61
|
})
|
|
59
62
|
|
|
@@ -63,7 +66,7 @@ describe(XrpcResponseError, () => {
|
|
|
63
66
|
})
|
|
64
67
|
|
|
65
68
|
describe('toDownstreamError', () => {
|
|
66
|
-
it('returns 502 for
|
|
69
|
+
it('returns 502 for upstream 500 errors', () => {
|
|
67
70
|
const err = createResponseError(
|
|
68
71
|
500,
|
|
69
72
|
'InternalServerError',
|
|
@@ -78,6 +81,17 @@ describe(XrpcResponseError, () => {
|
|
|
78
81
|
})
|
|
79
82
|
})
|
|
80
83
|
|
|
84
|
+
it('preserves original status for non-500 5xx errors', () => {
|
|
85
|
+
const err = createResponseError(503, 'ServiceUnavailable', 'Try later')
|
|
86
|
+
const downstream = err.toDownstreamError()
|
|
87
|
+
|
|
88
|
+
expect(downstream.status).toBe(503)
|
|
89
|
+
expect(downstream.body).toEqual({
|
|
90
|
+
error: 'ServiceUnavailable',
|
|
91
|
+
message: 'Try later',
|
|
92
|
+
})
|
|
93
|
+
})
|
|
94
|
+
|
|
81
95
|
it('preserves original status for 4xx errors', () => {
|
|
82
96
|
const err = createResponseError(404, 'NotFound', 'Record not found')
|
|
83
97
|
const downstream = err.toDownstreamError()
|
|
@@ -159,6 +173,7 @@ describe(XrpcAuthenticationError, () => {
|
|
|
159
173
|
encoding: 'application/json',
|
|
160
174
|
body: { error: 'AuthenticationRequired' },
|
|
161
175
|
})
|
|
176
|
+
expect(err.reason).toBe(err)
|
|
162
177
|
expect(err.wwwAuthenticate).toHaveProperty('Bearer')
|
|
163
178
|
})
|
|
164
179
|
|
|
@@ -195,6 +210,7 @@ describe(XrpcUpstreamError, () => {
|
|
|
195
210
|
it('has error code UpstreamFailure', () => {
|
|
196
211
|
const response = new Response(null, { status: 200 })
|
|
197
212
|
const err = new XrpcUpstreamError(testQuery, response)
|
|
213
|
+
expect(err.reason).toBe(err)
|
|
198
214
|
expect(err.error).toBe('UpstreamFailure')
|
|
199
215
|
})
|
|
200
216
|
|
|
@@ -236,6 +252,7 @@ describe(XrpcInvalidResponseError, () => {
|
|
|
236
252
|
)
|
|
237
253
|
|
|
238
254
|
expect(err).toBeInstanceOf(XrpcUpstreamError)
|
|
255
|
+
expect(err.reason).toBe(err)
|
|
239
256
|
expect(err.error).toBe('UpstreamFailure')
|
|
240
257
|
expect(err.cause).toBe(validationError)
|
|
241
258
|
})
|
|
@@ -277,14 +294,10 @@ describe(XrpcInvalidResponseError, () => {
|
|
|
277
294
|
describe(XrpcInternalError, () => {
|
|
278
295
|
it('has error code InternalServerError', () => {
|
|
279
296
|
const err = new XrpcInternalError(testQuery)
|
|
297
|
+
expect(err.reason).toBe(err)
|
|
280
298
|
expect(err.error).toBe('InternalServerError')
|
|
281
299
|
})
|
|
282
300
|
|
|
283
|
-
it('is always retryable', () => {
|
|
284
|
-
const err = new XrpcInternalError(testQuery)
|
|
285
|
-
expect(err.shouldRetry()).toBe(true)
|
|
286
|
-
})
|
|
287
|
-
|
|
288
301
|
it('toJSON does not expose internal details', () => {
|
|
289
302
|
const err = new XrpcInternalError(
|
|
290
303
|
testQuery,
|
|
@@ -305,6 +318,62 @@ describe(XrpcInternalError, () => {
|
|
|
305
318
|
expect(downstream.body.error).toBe('InternalServerError')
|
|
306
319
|
expect(downstream.body.message).toBe('Internal Server Error')
|
|
307
320
|
})
|
|
321
|
+
|
|
322
|
+
it('is not retryable', () => {
|
|
323
|
+
const err = new XrpcInternalError(testQuery, 'something broke')
|
|
324
|
+
expect(err.shouldRetry()).toBe(false)
|
|
325
|
+
})
|
|
326
|
+
})
|
|
327
|
+
|
|
328
|
+
// ============================================================================
|
|
329
|
+
// XrpcFetchError
|
|
330
|
+
// ============================================================================
|
|
331
|
+
|
|
332
|
+
describe(XrpcFetchError, () => {
|
|
333
|
+
it('extends XrpcInternalError', () => {
|
|
334
|
+
const err = new XrpcFetchError(testQuery, new TypeError('fetch failed'))
|
|
335
|
+
expect(err).toBeInstanceOf(XrpcInternalError)
|
|
336
|
+
expect(err.error).toBe('InternalServerError')
|
|
337
|
+
})
|
|
338
|
+
|
|
339
|
+
it('uses cause message when cause is an Error', () => {
|
|
340
|
+
const cause = new TypeError('Failed to fetch')
|
|
341
|
+
const err = new XrpcFetchError(testQuery, cause)
|
|
342
|
+
expect(err.message).toBe('Unexpected fetchHandler() error: Failed to fetch')
|
|
343
|
+
expect(err.cause).toBe(cause)
|
|
344
|
+
})
|
|
345
|
+
|
|
346
|
+
it('uses fallback message when cause is not an Error', () => {
|
|
347
|
+
const err = new XrpcFetchError(testQuery, 'string cause')
|
|
348
|
+
expect(err.message).toBe('Unexpected fetchHandler() error: string cause')
|
|
349
|
+
expect(err.cause).toBe('string cause')
|
|
350
|
+
})
|
|
351
|
+
|
|
352
|
+
it('is retryable', () => {
|
|
353
|
+
const err = new XrpcFetchError(testQuery, new Error('network timeout'))
|
|
354
|
+
expect(err.shouldRetry()).toBe(true)
|
|
355
|
+
})
|
|
356
|
+
|
|
357
|
+
it('toJSON does not expose internal details', () => {
|
|
358
|
+
const err = new XrpcFetchError(
|
|
359
|
+
testQuery,
|
|
360
|
+
new Error('ECONNREFUSED 10.0.0.1:443'),
|
|
361
|
+
)
|
|
362
|
+
const json = err.toJSON()
|
|
363
|
+
|
|
364
|
+
expect(json.error).toBe('InternalServerError')
|
|
365
|
+
expect(json.message).toBe('Failed to perform upstream request')
|
|
366
|
+
expect(json.message).not.toContain('ECONNREFUSED')
|
|
367
|
+
})
|
|
368
|
+
|
|
369
|
+
it('toDownstreamError returns 502', () => {
|
|
370
|
+
const err = new XrpcFetchError(testQuery, new Error('DNS lookup failed'))
|
|
371
|
+
const downstream = err.toDownstreamError()
|
|
372
|
+
|
|
373
|
+
expect(downstream.status).toBe(502)
|
|
374
|
+
expect(downstream.body.error).toBe('InternalServerError')
|
|
375
|
+
expect(downstream.body.message).toBe('Failed to perform upstream request')
|
|
376
|
+
})
|
|
308
377
|
})
|
|
309
378
|
|
|
310
379
|
// ============================================================================
|