@atproto/lex-data 0.0.10 → 0.0.11

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/src/cid.test.ts CHANGED
@@ -3,10 +3,10 @@ import { sha256, sha512 } from 'multiformats/hashes/sha2'
3
3
  import { describe, expect, it } from 'vitest'
4
4
  import { BytesCid, createCustomCid } from './cid-implementation.test.js'
5
5
  import {
6
+ CBOR_DATA_CODEC,
6
7
  Cid,
7
- DAG_CBOR_MULTICODEC,
8
- RAW_MULTICODEC,
9
- SHA256_MULTIHASH,
8
+ RAW_DATA_CODEC,
9
+ SHA256_HASH_CODE,
10
10
  asMultiformatsCID,
11
11
  cidForRawHash,
12
12
  decodeCid,
@@ -28,8 +28,8 @@ const rawCid = parseCid(rawCidStr, { flavor: 'raw' })
28
28
 
29
29
  const rawCidLike: Cid = createCustomCid(
30
30
  1,
31
- RAW_MULTICODEC,
32
- SHA256_MULTIHASH,
31
+ RAW_DATA_CODEC,
32
+ SHA256_HASH_CODE,
33
33
  rawCid.multihash.digest,
34
34
  )
35
35
  const rawBytesCid = new BytesCid(rawCid.bytes)
@@ -49,7 +49,7 @@ describe(isCid, () => {
49
49
  it('returns true for CID v0 and v1', async () => {
50
50
  const digest = await sha256.digest(Buffer.from('hello world'))
51
51
  const cidV0 = CID.createV0(digest)
52
- const cidV1 = CID.createV1(RAW_MULTICODEC, digest)
52
+ const cidV1 = CID.createV1(RAW_DATA_CODEC, digest)
53
53
  expect(isCid(cidV0)).toBe(true)
54
54
  expect(isCid(cidV1)).toBe(true)
55
55
  })
@@ -65,13 +65,13 @@ describe(isCid, () => {
65
65
  describe('raw', () => {
66
66
  it('validated "raw" cids', async () => {
67
67
  const digest = await sha256.digest(Buffer.from('hello world'))
68
- const cid = CID.createV1(RAW_MULTICODEC, digest)
68
+ const cid = CID.createV1(RAW_DATA_CODEC, digest)
69
69
  expect(isCid(cid, { flavor: 'raw' })).toBe(true)
70
70
  })
71
71
 
72
72
  it('allows other hash algorithms', async () => {
73
73
  const digest = await sha512.digest(Buffer.from('hello world'))
74
- const cid = CID.createV1(RAW_MULTICODEC, digest)
74
+ const cid = CID.createV1(RAW_DATA_CODEC, digest)
75
75
  expect(isCid(cid, { flavor: 'raw' })).toBe(true)
76
76
  })
77
77
 
@@ -91,13 +91,13 @@ describe(isCid, () => {
91
91
  describe('cbor', () => {
92
92
  it('validated "cbor" cids', async () => {
93
93
  const digest = await sha256.digest(Buffer.from('hello world'))
94
- const cid = CID.createV1(DAG_CBOR_MULTICODEC, digest)
94
+ const cid = CID.createV1(CBOR_DATA_CODEC, digest)
95
95
  expect(isCid(cid, { flavor: 'cbor' })).toBe(true)
96
96
  })
97
97
 
98
98
  it('rejects CIDs with invalid hash algorithm', async () => {
99
99
  const digest = await sha512.digest(Buffer.from('hello world'))
100
- const cid = CID.createV1(RAW_MULTICODEC, digest)
100
+ const cid = CID.createV1(RAW_DATA_CODEC, digest)
101
101
  expect(isCid(cid, { flavor: 'cbor' })).toBe(false)
102
102
  })
103
103
 
@@ -266,7 +266,7 @@ describe(isCidForBytes, () => {
266
266
  for (const hasher of [sha256, sha512]) {
267
267
  const data = new TextEncoder().encode('hello world')
268
268
  const digest = await hasher.digest(data)
269
- const cid = CID.createV1(RAW_MULTICODEC, digest)
269
+ const cid = CID.createV1(RAW_DATA_CODEC, digest)
270
270
  expect(await isCidForBytes(cid, data)).toBe(true)
271
271
 
272
272
  data[0] = data[0] ^ 0xff
@@ -281,7 +281,7 @@ describe(isCidForBytes, () => {
281
281
  // @NOTE this is not valid CBOR, but sufficient for testing the hash
282
282
  const data = new TextEncoder().encode('hello world')
283
283
  const digest = await hasher.digest(data)
284
- const cid = CID.createV1(DAG_CBOR_MULTICODEC, digest)
284
+ const cid = CID.createV1(CBOR_DATA_CODEC, digest)
285
285
  expect(await isCidForBytes(cid, data)).toBe(true)
286
286
 
287
287
  data[0] = data[0] ^ 0xff
@@ -318,8 +318,8 @@ describe(cidForRawHash, () => {
318
318
  it('creates a RawCid from a SHA-256 hash', () => {
319
319
  const hash = new Uint8Array(32)
320
320
  const cid = cidForRawHash(hash)
321
- expect(cid.code).toBe(RAW_MULTICODEC)
322
- expect(cid.multihash.code).toBe(SHA256_MULTIHASH)
321
+ expect(cid.code).toBe(RAW_DATA_CODEC)
322
+ expect(cid.multihash.code).toBe(SHA256_HASH_CODE)
323
323
  expect(ui8Equals(cid.multihash.digest, hash)).toBe(true)
324
324
  })
325
325
 
package/src/cid.ts CHANGED
@@ -4,31 +4,65 @@ import { sha256, sha512 } from 'multiformats/hashes/sha2'
4
4
  import { isObject } from './object.js'
5
5
  import { ui8Equals } from './uint8array.js'
6
6
 
7
- export const DAG_CBOR_MULTICODEC = 0x71 // DRISL conformant DAG-CBOR
8
- export type DAG_CBOR_MULTICODEC = typeof DAG_CBOR_MULTICODEC
7
+ /**
8
+ * Codec code that indicates the CID references a CBOR-encoded data structure.
9
+ *
10
+ * Used when encoding structured data in AT Protocol repositories.
11
+ *
12
+ * @see {@link https://dasl.ing/cid.html Content IDs (DASL)}
13
+ */
14
+ export const CBOR_DATA_CODEC = 0x71
15
+ export type CBOR_DATA_CODEC = typeof CBOR_DATA_CODEC
9
16
 
10
- export const RAW_MULTICODEC = 0x55 // raw binary codec used in DASL CIDs
11
- export type RAW_MULTICODEC = typeof RAW_MULTICODEC
17
+ /**
18
+ * Codec code that indicates the CID references raw binary data (like media blobs).
19
+ *
20
+ * Used in DASL CIDs for binary blobs like images and media.
21
+ *
22
+ * @see {@link https://dasl.ing/cid.html Content IDs (DASL)}
23
+ */
24
+ export const RAW_DATA_CODEC = 0x55
25
+ export type RAW_DATA_CODEC = typeof RAW_DATA_CODEC
12
26
 
13
- export const SHA256_MULTIHASH = sha256.code
14
- export type SHA256_MULTIHASH = typeof SHA256_MULTIHASH
27
+ /**
28
+ * Hash code that indicates that a CID uses SHA-256.
29
+ */
30
+ export const SHA256_HASH_CODE = sha256.code
31
+ export type SHA256_HASH_CODE = typeof SHA256_HASH_CODE
15
32
 
16
- export const SHA512_MULTIHASH = sha512.code
17
- export type SHA512_MULTIHASH = typeof SHA512_MULTIHASH
33
+ /**
34
+ * Hash code that indicates that a CID uses SHA-512.
35
+ */
36
+ export const SHA512_HASH_CODE = sha512.code
37
+ export type SHA512_HASH_CODE = typeof SHA512_HASH_CODE
18
38
 
19
- export interface Multihash<TCode extends number = number> {
39
+ /**
40
+ * Represent the hash part of a CID, which includes the hash algorithm code and
41
+ * the raw digest bytes.
42
+ *
43
+ * @see {@link https://dasl.ing/cid.html Content IDs (DASL)}
44
+ */
45
+ export interface Multihash<THashCode extends number = number> {
20
46
  /**
21
- * Code of the multihash
47
+ * Code of the hash algorithm (e.g., SHA256_HASH_CODE).
22
48
  */
23
- code: TCode
49
+ code: THashCode
24
50
 
25
51
  /**
26
- * Raw digest
52
+ * Raw digest bytes.
27
53
  */
28
54
  digest: Uint8Array
29
55
  }
30
56
 
57
+ /**
58
+ * Compares two {@link Multihash} for equality.
59
+ *
60
+ * @param a - First {@link Multihash}
61
+ * @param b - Second {@link Multihash}
62
+ * @returns `true` if both multihashes have the same code and digest
63
+ */
31
64
  export function multihashEquals(a: Multihash, b: Multihash): boolean {
65
+ if (a === b) return true
32
66
  return a.code === b.code && ui8Equals(a.digest, b.digest)
33
67
  }
34
68
 
@@ -86,9 +120,9 @@ export { /** @deprecated */ CID }
86
120
  */
87
121
  export function asMultiformatsCID<
88
122
  TVersion extends 0 | 1 = 0 | 1,
89
- TCode extends number = number,
90
- TMultihashCode extends number = number,
91
- >(input: Cid<TVersion, TCode, TMultihashCode>) {
123
+ TCodec extends number = number,
124
+ THashCode extends number = number,
125
+ >(input: Cid<TVersion, TCodec, THashCode>) {
92
126
  const cid =
93
127
  // Already a multiformats CID instance
94
128
  CID.asCID(input) ??
@@ -103,79 +137,155 @@ export function asMultiformatsCID<
103
137
  // interface is indeed compatible with multiformats' CID implementation, which
104
138
  // allows us to safely rely on multiformats' CID implementation where Cid are
105
139
  // needed.
106
- return cid satisfies Cid as CID & Cid<TVersion, TCode, TMultihashCode>
140
+ return cid satisfies Cid as CID & Cid<TVersion, TCodec, THashCode>
107
141
  }
108
142
 
109
143
  /**
110
- * Interface for working with CIDs
144
+ * Content Identifier (CID) for addressing content by its hash.
145
+ *
146
+ * CIDs are self-describing content addresses used throughout AT Protocol for
147
+ * linking to data by its cryptographic hash. This interface provides a
148
+ * stable API that is compatible with the `multiformats` library but avoids
149
+ * direct dependency issues.
150
+ *
151
+ * @typeParam TVersion - CID version (0 or 1)
152
+ * @typeParam TCodec - Multicodec content type code
153
+ * @typeParam THashCode - Multihash algorithm code
154
+ *
155
+ * @example
156
+ * ```typescript
157
+ * import type { Cid } from '@atproto/lex-data'
158
+ * import { parseCid, isCid } from '@atproto/lex-data'
159
+ *
160
+ * // Parse a CID from a string
161
+ * const cid: Cid = parseCid('bafyreib...')
162
+ *
163
+ * // Check if a value is a CID
164
+ * if (isCid(value)) {
165
+ * console.log(cid.toString())
166
+ * }
167
+ * ```
168
+ *
169
+ * @see {@link isCid} to check if a value is a valid CID
170
+ * @see {@link parseCid} to parse a CID from a string
171
+ * @see {@link decodeCid} to decode a CID from bytes
172
+ * @see {@link https://dasl.ing/cid.html Content IDs (DASL)}
111
173
  */
112
174
  export interface Cid<
113
175
  TVersion extends 0 | 1 = 0 | 1,
114
- TCode extends number = number,
115
- TMultihashCode extends number = number,
176
+ TCodec extends number = number,
177
+ THashCode extends number = number,
116
178
  > {
117
179
  // @NOTE This interface is compatible with multiformats' CID implementation
118
180
  // which we are using under the hood.
119
181
 
182
+ /** CID version (0 or 1). AT Protocol uses CIDv1. */
120
183
  readonly version: TVersion
121
- readonly code: TCode
122
- readonly multihash: Multihash<TMultihashCode>
184
+ /** Coded (e.g., {@link CBOR_DATA_CODEC}, {@link RAW_DATA_CODEC}). */
185
+ readonly code: TCodec
186
+ /** The multihash containing the hash algorithm and digest. */
187
+ readonly multihash: Multihash<THashCode>
123
188
 
124
189
  /**
125
190
  * Binary representation of the whole CID.
126
191
  */
127
192
  readonly bytes: Uint8Array
128
193
 
194
+ /**
195
+ * Compares this CID with another for equality.
196
+ *
197
+ * @param other - The CID to compare with
198
+ * @returns `true` if the CIDs are equal
199
+ */
129
200
  equals(other: Cid): boolean
201
+
202
+ /**
203
+ * Returns the string representation of this CID (base32 for v1, base58btc for v0).
204
+ */
130
205
  toString(): string
131
206
  }
132
207
 
133
208
  /**
134
- * Represents the cid of raw binary data (like media blobs).
209
+ * Represents the CID of raw binary data (like media blobs).
135
210
  *
136
- * The use of {@link SHA256_MULTIHASH} is recommended but not required for raw CIDs.
211
+ * The use of {@link SHA256_HASH_CODE} is recommended but not required for raw CIDs.
137
212
  *
138
- * @see {@link https://atproto.com/specs/data-model#link-and-cid-formats ATproto Data Model - Link and CID Formats}
213
+ * @see {@link https://atproto.com/specs/data-model#link-and-cid-formats AT Protocol Data Model - Link and CID Formats}
139
214
  */
140
- export type RawCid = Cid<1, RAW_MULTICODEC>
215
+ export type RawCid = Cid<1, RAW_DATA_CODEC>
141
216
 
217
+ /**
218
+ * Type guard to check if a CID is a raw binary CID.
219
+ *
220
+ * @param cid - The CID to check
221
+ * @returns `true` if the CID is a version 1 CID with raw multicodec
222
+ */
142
223
  export function isRawCid(cid: Cid): cid is RawCid {
143
- return cid.version === 1 && cid.code === RAW_MULTICODEC
224
+ return cid.version === 1 && cid.code === RAW_DATA_CODEC
144
225
  }
145
226
 
146
227
  /**
147
228
  * Represents a DASL compliant CID.
148
- * @see {@link https://dasl.ing/cid.html DASL-CIDs}
229
+ *
230
+ * DASL CIDs are version 1 CIDs using either raw or DAG-CBOR multicodec
231
+ * with SHA-256 multihash.
232
+ *
233
+ * @see {@link https://dasl.ing/cid.html Content IDs (DASL)}
149
234
  */
150
- export type DaslCid = Cid<
151
- 1,
152
- RAW_MULTICODEC | DAG_CBOR_MULTICODEC,
153
- SHA256_MULTIHASH
154
- >
235
+ export type DaslCid = Cid<1, RAW_DATA_CODEC | CBOR_DATA_CODEC, SHA256_HASH_CODE>
155
236
 
237
+ /**
238
+ * Type guard to check if a CID is DASL compliant.
239
+ *
240
+ * @param cid - The CID to check
241
+ * @returns `true` if the CID is DASL compliant (v1, raw/dag-cbor, sha256)
242
+ */
156
243
  export function isDaslCid(cid: Cid): cid is DaslCid {
157
244
  return (
158
245
  cid.version === 1 &&
159
- (cid.code === RAW_MULTICODEC || cid.code === DAG_CBOR_MULTICODEC) &&
160
- cid.multihash.code === SHA256_MULTIHASH &&
161
- cid.multihash.digest.byteLength === 32 // Should always be 32 bytes (256 bits) for SHA-256, but double-checking anyways
246
+ (cid.code === RAW_DATA_CODEC || cid.code === CBOR_DATA_CODEC) &&
247
+ cid.multihash.code === SHA256_HASH_CODE &&
248
+ cid.multihash.digest.byteLength === 0x20 // Should always be 32 bytes (256 bits) for SHA-256, but double-checking anyways
162
249
  )
163
250
  }
164
251
 
165
252
  /**
166
- * Represents the cid of ATProto DAG-CBOR data (like repository MST nodes).
167
- * @see {@link https://atproto.com/specs/data-model#link-and-cid-formats ATproto Data Model - Link and CID Formats}
253
+ * Represents the CID of AT Protocol DAG-CBOR data (like repository MST nodes).
254
+ *
255
+ * CBOR CIDs are version 1 CIDs using DAG-CBOR multicodec with SHA-256 multihash.
256
+ *
257
+ * @see {@link https://atproto.com/specs/data-model#link-and-cid-formats AT Protocol Data Model - Link and CID Formats}
168
258
  */
169
- export type CborCid = Cid<1, DAG_CBOR_MULTICODEC, SHA256_MULTIHASH>
259
+ export type CborCid = Cid<1, CBOR_DATA_CODEC, SHA256_HASH_CODE>
170
260
 
261
+ /**
262
+ * Type guard to check if a CID is a DAG-CBOR CID.
263
+ *
264
+ * @param cid - The CID to check
265
+ * @returns `true` if the CID is a DAG-CBOR CID (v1, dag-cbor, sha256)
266
+ */
171
267
  export function isCborCid(cid: Cid): cid is CborCid {
172
- return cid.code === DAG_CBOR_MULTICODEC && isDaslCid(cid)
268
+ return cid.code === CBOR_DATA_CODEC && isDaslCid(cid)
173
269
  }
174
270
 
271
+ /**
272
+ * Options for checking CID flavor constraints.
273
+ */
175
274
  export type CheckCidOptions = {
275
+ /**
276
+ * The CID flavor to check for.
277
+ * - `'raw'` - Raw binary CID ({@link RawCid})
278
+ * - `'cbor'` - DAG-CBOR CID ({@link CborCid})
279
+ * - `'dasl'` - DASL compliant CID ({@link DaslCid})
280
+ */
176
281
  flavor?: 'raw' | 'cbor' | 'dasl'
177
282
  }
178
283
 
284
+ /**
285
+ * Infers the CID type based on check options.
286
+ *
287
+ * @typeParam TOptions - The options used for checking
288
+ */
179
289
  export type InferCheckedCid<TOptions> = TOptions extends { flavor: 'raw' }
180
290
  ? RawCid
181
291
  : TOptions extends { flavor: 'cbor' }
@@ -287,6 +397,16 @@ export function parseCid(input: string, options?: CheckCidOptions): Cid {
287
397
  return asCid(cid, options)
288
398
  }
289
399
 
400
+ /**
401
+ * Validates that a string is a valid CID representation.
402
+ *
403
+ * Unlike {@link parseCid}, this function returns a boolean instead of throwing.
404
+ * It also verifies that the string is the canonical representation of the CID.
405
+ *
406
+ * @param input - The string to validate
407
+ * @param options - Optional flavor constraints
408
+ * @returns `true` if the string is a valid CID
409
+ */
290
410
  export function validateCidString(
291
411
  input: string,
292
412
  options?: CheckCidOptions,
@@ -294,6 +414,23 @@ export function validateCidString(
294
414
  return parseCidSafe(input, options)?.toString() === input
295
415
  }
296
416
 
417
+ /**
418
+ * Safely parses a CID string, returning `null` on failure instead of throwing.
419
+ *
420
+ * @param input - The string to parse
421
+ * @param options - Optional flavor constraints
422
+ * @returns The parsed CID, or `null` if parsing fails
423
+ *
424
+ * @example
425
+ * ```typescript
426
+ * import { parseCidSafe } from '@atproto/lex-data'
427
+ *
428
+ * const cid = parseCidSafe('bafyreib...')
429
+ * if (cid) {
430
+ * console.log(cid.toString())
431
+ * }
432
+ * ```
433
+ */
297
434
  export function parseCidSafe<TOptions extends CheckCidOptions>(
298
435
  input: string,
299
436
  options: TOptions,
@@ -313,6 +450,13 @@ export function parseCidSafe(
313
450
  }
314
451
  }
315
452
 
453
+ /**
454
+ * Ensures that a string is a valid CID representation.
455
+ *
456
+ * @param input - The string to validate
457
+ * @param options - Optional flavor constraints
458
+ * @throws If the string is not a valid CID
459
+ */
316
460
  export function ensureValidCidString(
317
461
  input: string,
318
462
  options?: CheckCidOptions,
@@ -346,36 +490,71 @@ export async function isCidForBytes(
346
490
  throw new Error('Unsupported CID multihash')
347
491
  }
348
492
 
349
- export function createCid<TCode extends number, TMultihashCode extends number>(
350
- code: TCode,
351
- multihashCode: TMultihashCode,
493
+ /**
494
+ * Creates a CID from a multicodec, multihash code, and digest.
495
+ *
496
+ * @param code - The multicodec content type code
497
+ * @param multihashCode - The multihash algorithm code
498
+ * @param digest - The raw hash digest bytes
499
+ * @returns A new CIDv1 instance
500
+ *
501
+ * @example
502
+ * ```typescript
503
+ * import { createCid, RAW_DATA_CODEC, SHA256_HASH_CODE } from '@atproto/lex-data'
504
+ *
505
+ * const cid = createCid(RAW_DATA_CODEC, SHA256_HASH_CODE, hashDigest)
506
+ * ```
507
+ */
508
+ export function createCid<TCodec extends number, THashCode extends number>(
509
+ code: TCodec,
510
+ multihashCode: THashCode,
352
511
  digest: Uint8Array,
353
512
  ) {
354
513
  const cid: Cid = CID.createV1(code, createDigest(multihashCode, digest))
355
- return cid as Cid<1, TCode, TMultihashCode>
514
+ return cid as Cid<1, TCodec, THashCode>
356
515
  }
357
516
 
517
+ /**
518
+ * Creates a DAG-CBOR CID for the given CBOR bytes.
519
+ *
520
+ * Computes the SHA-256 hash of the bytes and creates a CIDv1 with DAG-CBOR multicodec.
521
+ *
522
+ * @param bytes - The CBOR-encoded bytes to hash
523
+ * @returns A promise that resolves to the CborCid
524
+ */
358
525
  export async function cidForCbor(bytes: Uint8Array): Promise<CborCid> {
359
526
  const multihash = await sha256.digest(bytes)
360
- return CID.createV1(DAG_CBOR_MULTICODEC, multihash) as CborCid
527
+ return CID.createV1(CBOR_DATA_CODEC, multihash) as CborCid
361
528
  }
362
529
 
530
+ /**
531
+ * Creates a raw CID for the given binary bytes.
532
+ *
533
+ * Computes the SHA-256 hash of the bytes and creates a CIDv1 with raw multicodec.
534
+ *
535
+ * @param bytes - The raw binary bytes to hash
536
+ * @returns A promise that resolves to the RawCid
537
+ */
363
538
  export async function cidForRawBytes(bytes: Uint8Array): Promise<RawCid> {
364
539
  const multihash = await sha256.digest(bytes)
365
- return CID.createV1(RAW_MULTICODEC, multihash) as RawCid
540
+ return CID.createV1(RAW_DATA_CODEC, multihash) as RawCid
366
541
  }
367
542
 
543
+ /**
544
+ * Creates a raw CID from an existing SHA-256 hash digest.
545
+ *
546
+ * @param digest - The SHA-256 hash digest (must be 32 bytes)
547
+ * @returns A RawCid with the given digest
548
+ * @throws If the digest length is not 32 bytes
549
+ */
368
550
  export function cidForRawHash(digest: Uint8Array): RawCid {
369
551
  // Fool-proofing
370
552
  if (digest.length !== 32) {
371
553
  throw new Error(`Invalid SHA-256 hash length: ${digest.length}`)
372
554
  }
373
- return createCid(RAW_MULTICODEC, sha256.code, digest)
555
+ return createCid(RAW_DATA_CODEC, sha256.code, digest)
374
556
  }
375
557
 
376
- /**
377
- * @internal
378
- */
379
558
  function isCidImplementation(value: unknown): value is Cid {
380
559
  if (CID.asCID(value)) {
381
560
  // CIDs created using older multiformats versions did not have a "bytes"
@@ -414,9 +593,6 @@ function isCidImplementation(value: unknown): value is Cid {
414
593
  }
415
594
  }
416
595
 
417
- /**
418
- * @internal
419
- */
420
596
  function isUint8(val: unknown): val is number {
421
597
  return Number.isInteger(val) && (val as number) >= 0 && (val as number) < 256
422
598
  }
package/src/lex-equals.ts CHANGED
@@ -3,6 +3,44 @@ import { LexValue } from './lex.js'
3
3
  import { isPlainObject } from './object.js'
4
4
  import { ui8Equals } from './uint8array.js'
5
5
 
6
+ /**
7
+ * Performs deep equality comparison between two {@link LexValue}s.
8
+ *
9
+ * This function correctly handles all Lexicon data types including:
10
+ * - Primitives (string, number, boolean, null)
11
+ * - Arrays (recursive element comparison)
12
+ * - Objects/LexMaps (recursive key-value comparison)
13
+ * - Uint8Arrays (byte-by-byte comparison)
14
+ * - CIDs (using CID equality)
15
+ *
16
+ * @param a - First LexValue to compare
17
+ * @param b - Second LexValue to compare
18
+ * @returns `true` if the values are deeply equal
19
+ * @throws {TypeError} If either value is not a valid LexValue (e.g., contains unsupported types)
20
+ *
21
+ * @example
22
+ * ```typescript
23
+ * import { lexEquals } from '@atproto/lex-data'
24
+ *
25
+ * // Primitives
26
+ * lexEquals('hello', 'hello') // true
27
+ * lexEquals(42, 42) // true
28
+ *
29
+ * // Arrays
30
+ * lexEquals([1, 2, 3], [1, 2, 3]) // true
31
+ * lexEquals([1, 2], [1, 2, 3]) // false
32
+ *
33
+ * // Objects
34
+ * lexEquals({ a: 1, b: 2 }, { a: 1, b: 2 }) // true
35
+ * lexEquals({ a: 1 }, { a: 1, b: 2 }) // false
36
+ *
37
+ * // CIDs
38
+ * lexEquals(cid1, cid2) // true if CIDs are equal
39
+ *
40
+ * // Uint8Arrays
41
+ * lexEquals(new Uint8Array([1, 2]), new Uint8Array([1, 2])) // true
42
+ * ```
43
+ */
6
44
  export function lexEquals(a: LexValue, b: LexValue): boolean {
7
45
  if (Object.is(a, b)) {
8
46
  return true
package/src/lex-error.ts CHANGED
@@ -1,13 +1,78 @@
1
+ /**
2
+ * Error code type for Lexicon errors.
3
+ *
4
+ * Error codes identify the type of error that occurred (e.g., 'InvalidRequest').
5
+ *
6
+ * @example
7
+ * ```typescript
8
+ * import type { LexErrorCode } from '@atproto/lex-data'
9
+ *
10
+ * const errorCode: LexErrorCode = 'InvalidRequest'
11
+ * ```
12
+ */
1
13
  export type LexErrorCode = string & NonNullable<unknown>
2
14
 
15
+ /**
16
+ * JSON-serializable error data structure.
17
+ *
18
+ * This is the standard format for error responses in the AT Protocol XRPC protocol.
19
+ *
20
+ * @typeParam N - The specific error code type
21
+ *
22
+ * @example
23
+ * ```typescript
24
+ * import type { LexErrorData } from '@atproto/lex-data'
25
+ *
26
+ * const errorData: LexErrorData = {
27
+ * error: 'InvalidRequest',
28
+ * message: 'Missing required field: handle'
29
+ * }
30
+ * ```
31
+ */
3
32
  export type LexErrorData<N extends LexErrorCode = LexErrorCode> = {
33
+ /** The error code identifying the type of error. */
4
34
  error: N
35
+ /** Optional human-readable error message. */
5
36
  message?: string
6
37
  }
7
38
 
39
+ /**
40
+ * Error class for Lexicon-related errors.
41
+ *
42
+ * LexError extends the standard JavaScript {@link Error} with AT Protocol-specific
43
+ * functionality including:
44
+ * - An error code for programmatic error handling
45
+ * - JSON serialization for API responses
46
+ * - HTTP Response generation
47
+ *
48
+ * @typeParam N - The specific error code type
49
+ *
50
+ * @example
51
+ * ```typescript
52
+ * import { LexError } from '@atproto/lex-data'
53
+ *
54
+ * // Throw a Lexicon error
55
+ * throw new LexError('InvalidRequest', 'Missing required field')
56
+ *
57
+ * // Create and serialize
58
+ * const error = new LexError('NotFound', 'Record not found')
59
+ * console.log(error.toJSON())
60
+ * // { error: 'NotFound', message: 'Record not found' }
61
+ *
62
+ * // Return as HTTP response
63
+ * return error.toResponse() // 400 Bad Request with JSON body
64
+ * ```
65
+ */
8
66
  export class LexError<N extends LexErrorCode = LexErrorCode> extends Error {
9
67
  name = 'LexError'
10
68
 
69
+ /**
70
+ * Creates a new LexError.
71
+ *
72
+ * @param error - The error code identifying the type of error
73
+ * @param message - Optional human-readable error message
74
+ * @param options - Standard Error options (e.g., cause)
75
+ */
11
76
  constructor(
12
77
  readonly error: N,
13
78
  message?: string, // Defaults to empty string in Error constructor
@@ -16,17 +81,31 @@ export class LexError<N extends LexErrorCode = LexErrorCode> extends Error {
16
81
  super(message, options)
17
82
  }
18
83
 
84
+ /**
85
+ * Returns a string representation of this error.
86
+ *
87
+ * @returns A formatted string: "LexError: [ERROR_CODE] message"
88
+ */
19
89
  toString(): string {
20
90
  return `${this.name}: [${this.error}] ${this.message}`
21
91
  }
22
92
 
93
+ /**
94
+ * Converts this error to a JSON-serializable object.
95
+ *
96
+ * @returns The error data suitable for JSON serialization
97
+ */
23
98
  toJSON(): LexErrorData<N> {
24
99
  const { error, message } = this
25
100
  return { error, message: message || undefined }
26
101
  }
27
102
 
28
103
  /**
29
- * Translate into an HTTP response for downstream clients.
104
+ * Converts this error to an HTTP Response for downstream clients.
105
+ *
106
+ * Returns a 400 Bad Request response with the JSON-serialized error body.
107
+ *
108
+ * @returns A Response object with status 400 and JSON body
30
109
  */
31
110
  toResponse(): Response {
32
111
  return Response.json(this.toJSON(), { status: 400 })