@atproto/lex-resolver 0.0.13 → 0.0.14

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.
@@ -24,51 +24,229 @@ import {
24
24
  import { LexResolverError } from './lex-resolver-error.js'
25
25
  import { com } from './lexicons/index.js'
26
26
 
27
+ /**
28
+ * Result returned when successfully resolving a lexicon document.
29
+ *
30
+ * Contains the full AT URI where the lexicon was found, the content-addressed
31
+ * identifier (CID) for integrity verification, and the parsed lexicon document.
32
+ */
27
33
  export type LexResolverResult = {
34
+ /** The AT URI where the lexicon document was found */
28
35
  uri: AtUri
36
+ /** Content identifier (CID) of the lexicon record for integrity verification */
29
37
  cid: Cid
38
+ /** The parsed and validated lexicon document */
30
39
  lexicon: LexiconDocument
31
40
  }
32
41
 
42
+ /**
43
+ * Result returned when fetching a lexicon document from a specific URI.
44
+ *
45
+ * This is a subset of {@link LexResolverResult} used internally and by hooks,
46
+ * containing only the CID and lexicon document (without the URI, which is
47
+ * already known from the fetch request).
48
+ */
33
49
  export type LexResolverFetchResult = {
50
+ /** Content identifier (CID) of the lexicon record */
34
51
  cid: Cid
52
+ /** The parsed and validated lexicon document */
35
53
  lexicon: LexiconDocument
36
54
  }
37
55
 
38
- type Awaitable<T> = T | Promise<T>
39
-
56
+ export type Awaitable<T> = T | Promise<T>
57
+
58
+ /**
59
+ * Callback hooks for customizing the lexicon resolution process.
60
+ *
61
+ * Hooks allow you to intercept, cache, or override the default resolution
62
+ * behavior at various stages. Each hook can be synchronous or asynchronous.
63
+ *
64
+ * @example Implementing a cache with hooks
65
+ * ```typescript
66
+ * import { LexResolver, LexResolverHooks, LexResolverFetchResult } from '@atproto/lex-resolver'
67
+ * import { AtUri } from '@atproto/syntax'
68
+ *
69
+ * const cache = new Map<string, LexResolverFetchResult>()
70
+ *
71
+ * const hooks: LexResolverHooks = {
72
+ * // Return cached result if available, bypassing network fetch
73
+ * onFetch({ uri }) {
74
+ * return cache.get(uri.toString())
75
+ * },
76
+ * // Cache successful fetches
77
+ * onFetchResult({ uri, cid, lexicon }) {
78
+ * cache.set(uri.toString(), { cid, lexicon })
79
+ * },
80
+ * // Log errors for monitoring
81
+ * onFetchError({ uri, err }) {
82
+ * console.error(`Failed to fetch ${uri}:`, err)
83
+ * }
84
+ * }
85
+ *
86
+ * const resolver = new LexResolver({ hooks })
87
+ * ```
88
+ *
89
+ * @example Overriding authority resolution for testing
90
+ * ```typescript
91
+ * const hooks: LexResolverHooks = {
92
+ * // Always resolve to a test DID
93
+ * onResolveAuthority({ nsid }) {
94
+ * if (nsid.authority === 'test.example') {
95
+ * return 'did:plc:test123'
96
+ * }
97
+ * // Return undefined to use default resolution
98
+ * }
99
+ * }
100
+ * ```
101
+ */
40
102
  export type LexResolverHooks = {
41
103
  /**
42
104
  * Hook called before resolving a lexicon authority DID. If a DID is returned,
43
105
  * it will be used instead of performing the default resolution. In that case,
44
106
  * the `onResolveAuthorityResult` and `onResolveAuthorityError` hooks will
45
107
  * not be called.
108
+ *
109
+ * @param data - Object containing the NSID being resolved
110
+ * @returns A DID to use instead of default resolution, or void/undefined to proceed normally
46
111
  */
47
112
  onResolveAuthority?(data: { nsid: NSID }): Awaitable<void | Did>
113
+
114
+ /**
115
+ * Hook called after successfully resolving a lexicon authority DID.
116
+ *
117
+ * @param data - Object containing the NSID and resolved DID
118
+ */
48
119
  onResolveAuthorityResult?(data: { nsid: NSID; did: Did }): Awaitable<void>
120
+
121
+ /**
122
+ * Hook called when authority resolution fails.
123
+ *
124
+ * @param data - Object containing the NSID and error that occurred
125
+ */
49
126
  onResolveAuthorityError?(data: { nsid: NSID; err: unknown }): Awaitable<void>
50
127
 
51
128
  /**
52
129
  * Hook called before fetching a lexicon URI. If a result is returned, it will
53
130
  * be used instead of performing the default fetch. In that case, the
54
131
  * `onFetchResult` and `onFetchError` hooks will not be called.
132
+ *
133
+ * @param data - Object containing the URI being fetched
134
+ * @returns A fetch result to use instead of default fetch, or void/undefined to proceed normally
55
135
  */
56
136
  onFetch?(data: { uri: AtUri }): Awaitable<void | LexResolverFetchResult>
137
+
138
+ /**
139
+ * Hook called after successfully fetching a lexicon document.
140
+ *
141
+ * @param data - Object containing the URI, CID, and parsed lexicon document
142
+ */
57
143
  onFetchResult?(data: {
58
144
  uri: AtUri
59
145
  cid: Cid
60
146
  lexicon: LexiconDocument
61
147
  }): Awaitable<void>
148
+
149
+ /**
150
+ * Hook called when fetching fails.
151
+ *
152
+ * @param data - Object containing the URI and error that occurred
153
+ */
62
154
  onFetchError?(data: { uri: AtUri; err: unknown }): Awaitable<void>
63
155
  }
64
156
 
157
+ /**
158
+ * Configuration options for the {@link LexResolver}.
159
+ *
160
+ * Extends DID resolver options with lexicon-specific hooks for customizing
161
+ * the resolution process.
162
+ *
163
+ * @see {@link CreateDidResolverOptions} for DID resolver configuration
164
+ */
65
165
  export type LexResolverOptions = CreateDidResolverOptions & {
166
+ /**
167
+ * Optional hooks for customizing the resolution process.
168
+ * See {@link LexResolverHooks} for available callbacks.
169
+ */
66
170
  hooks?: LexResolverHooks
67
171
  }
68
172
 
69
173
  export { AtUri, type Cid, NSID }
70
174
  export type { LexiconDocument, ResolveDidOptions }
71
175
 
176
+ /**
177
+ * Resolves Lexicon documents from the AT Protocol network.
178
+ *
179
+ * The {@link LexResolver} handles the complete process of resolving a lexicon
180
+ * by NSID:
181
+ * 1. **Authority Resolution**: Looks up the `_lexicon.<authority>` DNS TXT record
182
+ * to find the DID that controls lexicons for that namespace
183
+ * 2. **DID Resolution**: Resolves the DID document to find the PDS endpoint and
184
+ * signing key
185
+ * 3. **Record Fetch**: Fetches the lexicon record from the PDS with cryptographic
186
+ * proof verification
187
+ * 4. **Validation**: Validates the lexicon document structure
188
+ *
189
+ * @example Basic usage - resolve a lexicon by NSID
190
+ * ```typescript
191
+ * import { LexResolver } from '@atproto/lex-resolver'
192
+ *
193
+ * const resolver = new LexResolver({})
194
+ *
195
+ * // Get a lexicon document by its NSID
196
+ * const result = await resolver.get('app.bsky.feed.post')
197
+ * console.log(result.lexicon) // The parsed lexicon document
198
+ * console.log(result.uri) // AT URI where it was found
199
+ * console.log(result.cid) // Content identifier for verification
200
+ * ```
201
+ *
202
+ * @example Two-step resolution for more control
203
+ * ```typescript
204
+ * import { LexResolver } from '@atproto/lex-resolver'
205
+ *
206
+ * const resolver = new LexResolver({})
207
+ *
208
+ * // Step 1: Resolve the authority to get the AT URI
209
+ * const uri = await resolver.resolve('app.bsky.feed.post')
210
+ * console.log(uri.toString()) // 'at://did:plc:xxx/com.atproto.lexicon.schema/app.bsky.feed.post'
211
+ *
212
+ * // Step 2: Fetch the lexicon from the URI
213
+ * const result = await resolver.fetch(uri)
214
+ * console.log(result.lexicon)
215
+ * ```
216
+ *
217
+ * @example Using hooks for caching
218
+ * ```typescript
219
+ * import { LexResolver, LexResolverFetchResult } from '@atproto/lex-resolver'
220
+ *
221
+ * const cache = new Map<string, LexResolverFetchResult>()
222
+ *
223
+ * const resolver = new LexResolver({
224
+ * hooks: {
225
+ * onFetch({ uri }) {
226
+ * return cache.get(uri.toString())
227
+ * },
228
+ * onFetchResult({ uri, cid, lexicon }) {
229
+ * cache.set(uri.toString(), { cid, lexicon })
230
+ * }
231
+ * }
232
+ * })
233
+ * ```
234
+ *
235
+ * @example Error handling
236
+ * ```typescript
237
+ * import { LexResolver, LexResolverError } from '@atproto/lex-resolver'
238
+ *
239
+ * const resolver = new LexResolver({})
240
+ *
241
+ * try {
242
+ * const result = await resolver.get('com.example.unknown')
243
+ * } catch (error) {
244
+ * if (error instanceof LexResolverError) {
245
+ * console.error(`Failed to resolve ${error.nsid}: ${error.description}`)
246
+ * }
247
+ * }
248
+ * ```
249
+ */
72
250
  export class LexResolver {
73
251
  protected readonly didResolver: DidResolver<'plc' | 'web'>
74
252
 
@@ -76,6 +254,35 @@ export class LexResolver {
76
254
  this.didResolver = createDidResolver(options)
77
255
  }
78
256
 
257
+ /**
258
+ * Gets a lexicon document by its NSID.
259
+ *
260
+ * This is the primary method for resolving lexicons. It combines
261
+ * {@link resolve} and {@link fetch} into a single operation, handling
262
+ * authority resolution, DID lookup, and record fetching.
263
+ *
264
+ * @param nsidStr - The NSID to resolve, either as a string or NSID object
265
+ * @param options - Optional DID resolution options (e.g., signal for cancellation)
266
+ * @returns The resolved lexicon result containing URI, CID, and lexicon document
267
+ * @throws {LexResolverError} If resolution fails at any stage
268
+ *
269
+ * @example
270
+ * ```typescript
271
+ * // Resolve using string NSID
272
+ * const result = await resolver.get('app.bsky.feed.post')
273
+ *
274
+ * // Resolve using NSID object
275
+ * import { NSID } from '@atproto/syntax'
276
+ * const nsid = NSID.from('app.bsky.feed.post')
277
+ * const result = await resolver.get(nsid)
278
+ *
279
+ * // With abort signal for cancellation
280
+ * const controller = new AbortController()
281
+ * const result = await resolver.get('app.bsky.feed.post', {
282
+ * signal: controller.signal
283
+ * })
284
+ * ```
285
+ */
79
286
  async get(
80
287
  nsidStr: NSID | string,
81
288
  options?: ResolveDidOptions,
@@ -84,6 +291,33 @@ export class LexResolver {
84
291
  return this.fetch(uri, options)
85
292
  }
86
293
 
294
+ /**
295
+ * Resolves the authority for an NSID and returns the AT URI for the lexicon.
296
+ *
297
+ * This method performs the first stage of lexicon resolution:
298
+ * 1. Parses the NSID to extract the authority domain
299
+ * 2. Looks up the `_lexicon.<authority>` DNS TXT record
300
+ * 3. Extracts the DID from the TXT record (format: `did=<did>`)
301
+ * 4. Constructs the AT URI for the lexicon record
302
+ *
303
+ * Use this when you need the URI without fetching the actual document,
304
+ * or when you want to implement custom fetching logic.
305
+ *
306
+ * @param nsidStr - The NSID to resolve, either as a string or NSID object
307
+ * @returns The AT URI pointing to the lexicon record
308
+ * @throws {LexResolverError} If authority resolution fails (e.g., DNS lookup fails)
309
+ *
310
+ * @example
311
+ * ```typescript
312
+ * // Resolve to get the AT URI
313
+ * const uri = await resolver.resolve('app.bsky.feed.post')
314
+ * console.log(uri.toString())
315
+ * // Output: 'at://did:plc:z72i7hdynmk6r22z27h6tvur/com.atproto.lexicon.schema/app.bsky.feed.post'
316
+ *
317
+ * // The URI can then be used with fetch() or stored for later use
318
+ * const result = await resolver.fetch(uri)
319
+ * ```
320
+ */
87
321
  async resolve(nsidStr: NSID | string): Promise<AtUri> {
88
322
  const nsid = NSID.from(nsidStr)
89
323
 
@@ -119,6 +353,39 @@ export class LexResolver {
119
353
  }
120
354
  }
121
355
 
356
+ /**
357
+ * Fetches a lexicon document from a specific AT URI.
358
+ *
359
+ * This method performs the second stage of lexicon resolution:
360
+ * 1. Resolves the DID from the URI to find the PDS endpoint
361
+ * 2. Fetches the record from the PDS using `com.atproto.sync.getRecord`
362
+ * 3. Verifies the cryptographic proof (commit signature)
363
+ * 4. Validates the lexicon document structure
364
+ * 5. Ensures the document ID matches the URI rkey
365
+ *
366
+ * Use this when you already have an AT URI (e.g., from {@link resolve})
367
+ * and want to fetch the lexicon document.
368
+ *
369
+ * @param uriStr - The AT URI to fetch, either as a string or AtUri object
370
+ * @param options - Optional DID resolution options (e.g., signal for cancellation, noCache)
371
+ * @returns The resolved lexicon result containing URI, CID, and lexicon document
372
+ * @throws {LexResolverError} If fetching or validation fails
373
+ *
374
+ * @example
375
+ * ```typescript
376
+ * // Fetch from a known URI
377
+ * const result = await resolver.fetch(
378
+ * 'at://did:plc:xyz/com.atproto.lexicon.schema/app.bsky.feed.post'
379
+ * )
380
+ *
381
+ * // Fetch with no-cache to bypass any upstream caching
382
+ * const result = await resolver.fetch(uri, { noCache: true })
383
+ *
384
+ * // Fetch with abort signal
385
+ * const controller = new AbortController()
386
+ * const result = await resolver.fetch(uri, { signal: controller.signal })
387
+ * ```
388
+ */
122
389
  async fetch(
123
390
  uriStr: AtUri | string,
124
391
  options?: ResolveDidOptions,