@atproto/lex-resolver 0.0.12 → 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.
- package/CHANGELOG.md +24 -0
- package/dist/lex-resolver-error.d.ts +99 -2
- package/dist/lex-resolver-error.d.ts.map +1 -1
- package/dist/lex-resolver-error.js +94 -3
- package/dist/lex-resolver-error.js.map +1 -1
- package/dist/lex-resolver.d.ts +264 -1
- package/dist/lex-resolver.d.ts.map +1 -1
- package/dist/lex-resolver.js +163 -0
- package/dist/lex-resolver.js.map +1 -1
- package/package.json +12 -11
- package/src/lex-resolver-error.ts +91 -2
- package/src/lex-resolver.ts +269 -2
package/src/lex-resolver.ts
CHANGED
|
@@ -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,
|