@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/CHANGELOG.md +8 -0
- package/dist/blob.d.ts +143 -6
- package/dist/blob.d.ts.map +1 -1
- package/dist/blob.js +23 -0
- package/dist/blob.js.map +1 -1
- package/dist/cid.d.ts +211 -27
- package/dist/cid.d.ts.map +1 -1
- package/dist/cid.js +115 -19
- package/dist/cid.js.map +1 -1
- package/dist/lex-equals.d.ts +38 -0
- package/dist/lex-equals.d.ts.map +1 -1
- package/dist/lex-equals.js +38 -0
- package/dist/lex-equals.js.map +1 -1
- package/dist/lex-error.d.ts +80 -1
- package/dist/lex-error.d.ts.map +1 -1
- package/dist/lex-error.js +49 -1
- package/dist/lex-error.js.map +1 -1
- package/dist/lex.d.ts +184 -2
- package/dist/lex.d.ts.map +1 -1
- package/dist/lex.js +99 -0
- package/dist/lex.js.map +1 -1
- package/dist/object.d.ts +54 -4
- package/dist/object.d.ts.map +1 -1
- package/dist/object.js +54 -4
- package/dist/object.js.map +1 -1
- package/dist/uint8array.d.ts +96 -4
- package/dist/uint8array.d.ts.map +1 -1
- package/dist/uint8array.js +96 -4
- package/dist/uint8array.js.map +1 -1
- package/dist/utf8.d.ts +77 -0
- package/dist/utf8.d.ts.map +1 -1
- package/dist/utf8.js +77 -0
- package/dist/utf8.js.map +1 -1
- package/package.json +2 -2
- package/src/blob.ts +143 -6
- package/src/cid-implementation.test.ts +7 -13
- package/src/cid.test.ts +14 -14
- package/src/cid.ts +228 -52
- package/src/lex-equals.ts +38 -0
- package/src/lex-error.ts +80 -1
- package/src/lex.ts +187 -1
- package/src/object.ts +54 -4
- package/src/uint8array.ts +96 -4
- package/src/utf8.ts +77 -0
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
|
-
|
|
8
|
-
|
|
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
|
-
|
|
32
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
322
|
-
expect(cid.multihash.code).toBe(
|
|
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
|
-
|
|
8
|
-
|
|
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
|
-
|
|
11
|
-
|
|
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
|
-
|
|
14
|
-
|
|
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
|
-
|
|
17
|
-
|
|
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
|
-
|
|
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
|
|
47
|
+
* Code of the hash algorithm (e.g., SHA256_HASH_CODE).
|
|
22
48
|
*/
|
|
23
|
-
code:
|
|
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
|
-
|
|
90
|
-
|
|
91
|
-
>(input: Cid<TVersion,
|
|
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,
|
|
140
|
+
return cid satisfies Cid as CID & Cid<TVersion, TCodec, THashCode>
|
|
107
141
|
}
|
|
108
142
|
|
|
109
143
|
/**
|
|
110
|
-
*
|
|
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
|
-
|
|
115
|
-
|
|
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
|
-
|
|
122
|
-
readonly
|
|
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
|
|
209
|
+
* Represents the CID of raw binary data (like media blobs).
|
|
135
210
|
*
|
|
136
|
-
* The use of {@link
|
|
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
|
|
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,
|
|
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 ===
|
|
224
|
+
return cid.version === 1 && cid.code === RAW_DATA_CODEC
|
|
144
225
|
}
|
|
145
226
|
|
|
146
227
|
/**
|
|
147
228
|
* Represents a DASL compliant CID.
|
|
148
|
-
*
|
|
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 ===
|
|
160
|
-
cid.multihash.code ===
|
|
161
|
-
cid.multihash.digest.byteLength ===
|
|
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
|
|
167
|
-
*
|
|
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,
|
|
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 ===
|
|
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
|
-
|
|
350
|
-
|
|
351
|
-
|
|
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,
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
*
|
|
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 })
|