@atproto/lex-client 0.0.14 → 0.0.16

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atproto/lex-client",
3
- "version": "0.0.14",
3
+ "version": "0.0.16",
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.15"
43
43
  },
44
44
  "devDependencies": {
45
45
  "vitest": "^4.0.16",
46
- "@atproto/lex-cbor": "^0.0.13",
47
- "@atproto/lex-builder": "^0.0.16"
46
+ "@atproto/lex-cbor": "^0.0.14",
47
+ "@atproto/lex-builder": "^0.0.18"
48
48
  },
49
49
  "scripts": {
50
50
  "prebuild": "node ./scripts/lex-build.mjs",
@@ -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 === 'string' || options instanceof URL
139
- ? { did: undefined, service: options }
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
@@ -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,
@@ -331,7 +331,7 @@ export class Client implements Agent {
331
331
 
332
332
  /**
333
333
  * The DID of the authenticated user.
334
- * @throws {LexError} with code 'AuthenticationRequired' if not authenticated
334
+ * @throws {Error} if not authenticated
335
335
  */
336
336
  get assertDid(): DidString {
337
337
  this.assertAuthenticated()
@@ -342,7 +342,7 @@ export class Client implements Agent {
342
342
  * Asserts that the client is authenticated.
343
343
  * Use as a type guard when you need to ensure authentication.
344
344
  *
345
- * @throws {LexError} with code 'AuthenticationRequired' if not authenticated
345
+ * @throws {Error} if not authenticated
346
346
  *
347
347
  * @example
348
348
  * ```typescript
@@ -352,7 +352,7 @@ export class Client implements Agent {
352
352
  * ```
353
353
  */
354
354
  public assertAuthenticated(): asserts this is { did: DidString } {
355
- if (!this.did) throw new LexError('AuthenticationRequired')
355
+ if (!this.did) throw new Error('Client is not authenticated')
356
356
  }
357
357
 
358
358
  /**