@atproto/lex-data 0.0.6 → 0.0.8

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.ts CHANGED
@@ -1,22 +1,42 @@
1
1
  import { CID } from 'multiformats/cid'
2
+ import { create as createDigest } from 'multiformats/hashes/digest'
3
+ import { sha256, sha512 } from 'multiformats/hashes/sha2'
4
+ import { isObject } from './object.js'
5
+ import { ui8Equals } from './uint8array.js'
2
6
 
3
- export const DAG_CBOR_MULTICODEC = 0x71
4
- export const RAW_BIN_MULTICODEC = 0x55
7
+ export const DAG_CBOR_MULTICODEC = 0x71 // DRISL conformant DAG-CBOR
8
+ export type DAG_CBOR_MULTICODEC = typeof DAG_CBOR_MULTICODEC
5
9
 
6
- export const SHA2_256_MULTIHASH_CODE = 0x12
10
+ export const RAW_MULTICODEC = 0x55 // raw binary codec used in DASL CIDs
11
+ export type RAW_MULTICODEC = typeof RAW_MULTICODEC
7
12
 
8
- export type MultihashDigest<Code extends number = number> = {
9
- code: Code
13
+ export const SHA256_MULTIHASH = sha256.code
14
+ export type SHA256_MULTIHASH = typeof SHA256_MULTIHASH
15
+
16
+ export const SHA512_MULTIHASH = sha512.code
17
+ export type SHA512_MULTIHASH = typeof SHA512_MULTIHASH
18
+
19
+ export interface Multihash<TCode extends number = number> {
20
+ /**
21
+ * Code of the multihash
22
+ */
23
+ code: TCode
24
+
25
+ /**
26
+ * Raw digest
27
+ */
10
28
  digest: Uint8Array
11
- size: number
12
- bytes: Uint8Array
29
+ }
30
+
31
+ export function multihashEquals(a: Multihash, b: Multihash): boolean {
32
+ return a.code === b.code && ui8Equals(a.digest, b.digest)
13
33
  }
14
34
 
15
35
  declare module 'multiformats/cid' {
16
36
  /**
17
37
  * @deprecated use the {@link Cid} interface from `@atproto/lex-data`, and
18
- * related helpers ({@link asCid}, {@link parseCid}, {@link decodeCid},
19
- * {@link createCid}, {@link isCid}), instead.
38
+ * related helpers ({@link isCid}, {@link ifCid}, {@link asCid},
39
+ * {@link parseCid}, {@link decodeCid}), instead.
20
40
  *
21
41
  * This is marked as deprecated because we want to discourage direct usage of
22
42
  * `multiformats/cid` in dependent packages, and instead have them rely on the
@@ -29,8 +49,8 @@ declare module 'multiformats/cid' {
29
49
  *
30
50
  * In order to avoid compatibility issues, while preparing for future breaking
31
51
  * changes (CID in multiformats v10+ has a slightly different interface), as
32
- * we update or swap out `multiformats`, we provide our own stable {@link Cid}
33
- * interface.
52
+ * we update or swap out `multiformats`, `@atproto/lex-data` provides its own
53
+ * stable {@link Cid} interface.
34
54
  */
35
55
  interface CID {}
36
56
  }
@@ -53,76 +73,350 @@ declare module 'multiformats/cid' {
53
73
 
54
74
  // @NOTE Even though it is not portable, we still re-export CID here so that
55
75
  // dependent packages where it can be used, have access to it (instead of
56
- // importing directly from "multiformats" or"multiformats/cid").
57
- export { CID }
76
+ // importing directly from "multiformats" or "multiformats/cid").
77
+ export { /** @deprecated */ CID }
58
78
 
59
79
  /**
60
- * Interface for working with decoded CID string, compatible with
61
- * {@link CID} implementation.
80
+ * Converts a {@link Cid} to a multiformats {@link CID} instance.
81
+ *
82
+ * @deprecated Packages depending on `@atproto/lex-data` should use the
83
+ * {@link Cid} interface instead of relying on `multiformats`'s {@link CID}
84
+ * implementation directly. This is to avoid compatibility issues, and in order
85
+ * to allow better portability, compatibility and future updates.
62
86
  */
63
- export interface Cid {
64
- version: 0 | 1
65
- code: number
66
- multihash: MultihashDigest
67
- bytes: Uint8Array
68
- equals(other: unknown): boolean
87
+ export function asMultiformatsCID<
88
+ TVersion extends 0 | 1 = 0 | 1,
89
+ TCode extends number = number,
90
+ TMultihashCode extends number = number,
91
+ >(input: Cid<TVersion, TCode, TMultihashCode>) {
92
+ const cid =
93
+ // Already a multiformats CID instance
94
+ CID.asCID(input) ??
95
+ // Create a new multiformats CID instance
96
+ CID.create(
97
+ input.version,
98
+ input.code,
99
+ createDigest(input.multihash.code, input.multihash.digest),
100
+ )
101
+
102
+ // @NOTE: the "satisfies" operator is used here to ensure that the Cid
103
+ // interface is indeed compatible with multiformats' CID implementation, which
104
+ // allows us to safely rely on multiformats' CID implementation where Cid are
105
+ // needed.
106
+ return cid satisfies Cid as CID & Cid<TVersion, TCode, TMultihashCode>
107
+ }
108
+
109
+ /**
110
+ * Interface for working with CIDs
111
+ */
112
+ export interface Cid<
113
+ TVersion extends 0 | 1 = 0 | 1,
114
+ TCode extends number = number,
115
+ TMultihashCode extends number = number,
116
+ > {
117
+ // @NOTE This interface is compatible with multiformats' CID implementation
118
+ // which we are using under the hood.
119
+
120
+ readonly version: TVersion
121
+ readonly code: TCode
122
+ readonly multihash: Multihash<TMultihashCode>
123
+
124
+ /**
125
+ * Binary representation of the whole CID.
126
+ */
127
+ readonly bytes: Uint8Array
128
+
129
+ equals(other: Cid): boolean
69
130
  toString(): string
70
131
  }
71
132
 
72
- export function asCid(value: unknown): Cid | null {
73
- return CID.asCID(value)
133
+ /**
134
+ * Represents the cid of raw binary data (like media blobs).
135
+ *
136
+ * The use of {@link SHA256_MULTIHASH} is recommended but not required for raw CIDs.
137
+ *
138
+ * @see {@link https://atproto.com/specs/data-model#link-and-cid-formats ATproto Data Model - Link and CID Formats}
139
+ */
140
+ export type RawCid = Cid<1, RAW_MULTICODEC>
141
+
142
+ export function isRawCid(cid: Cid): cid is RawCid {
143
+ return cid.version === 1 && cid.code === RAW_MULTICODEC
144
+ }
145
+
146
+ /**
147
+ * Represents a DASL compliant CID.
148
+ * @see {@link https://dasl.ing/cid.html DASL-CIDs}
149
+ */
150
+ export type DaslCid = Cid<
151
+ 1,
152
+ RAW_MULTICODEC | DAG_CBOR_MULTICODEC,
153
+ SHA256_MULTIHASH
154
+ >
155
+
156
+ export function isDaslCid(cid: Cid): cid is DaslCid {
157
+ return (
158
+ 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
162
+ )
74
163
  }
75
164
 
76
- export function parseCid(input: string): Cid {
77
- return CID.parse(input)
165
+ /**
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}
168
+ */
169
+ export type CborCid = Cid<1, DAG_CBOR_MULTICODEC, SHA256_MULTIHASH>
170
+
171
+ export function isCborCid(cid: Cid): cid is CborCid {
172
+ return cid.code === DAG_CBOR_MULTICODEC && isDaslCid(cid)
78
173
  }
79
174
 
80
- export function decodeCid(bytes: Uint8Array): Cid {
81
- return CID.decode(bytes)
175
+ export type CheckCidOptions = {
176
+ flavor?: 'raw' | 'cbor' | 'dasl'
82
177
  }
83
178
 
84
- export function createCid(code: number, digest: MultihashDigest): Cid {
85
- return CID.createV1(code, digest)
179
+ export type InferCheckedCid<TOptions> = TOptions extends { flavor: 'raw' }
180
+ ? RawCid
181
+ : TOptions extends { flavor: 'cbor' }
182
+ ? CborCid
183
+ : Cid
184
+
185
+ /**
186
+ * Type guard to check whether a {@link Cid} instance meets specific flavor
187
+ * constraints.
188
+ */
189
+ export function checkCid<TOptions extends CheckCidOptions>(
190
+ cid: Cid,
191
+ options: TOptions,
192
+ ): cid is InferCheckedCid<TOptions>
193
+ export function checkCid(cid: Cid, options?: CheckCidOptions): boolean
194
+ export function checkCid(cid: Cid, options?: CheckCidOptions): boolean {
195
+ switch (options?.flavor) {
196
+ case undefined:
197
+ return true
198
+ case 'cbor':
199
+ return isCborCid(cid)
200
+ case 'dasl':
201
+ return isDaslCid(cid)
202
+ case 'raw':
203
+ return isRawCid(cid)
204
+ default:
205
+ throw new TypeError(`Unknown CID flavor: ${options?.flavor}`)
206
+ }
86
207
  }
87
208
 
88
- export function isCid(
209
+ /**
210
+ * Type guard to check whether a value is a valid {@link Cid} instance,
211
+ * optionally checking for specific flavor constraints.
212
+ */
213
+ export function isCid<TOptions extends CheckCidOptions>(
89
214
  value: unknown,
90
- options?: { strict?: boolean },
91
- ): value is Cid {
92
- const cid = asCid(value)
93
- if (!cid) {
94
- return false
95
- }
215
+ options: TOptions,
216
+ ): value is InferCheckedCid<TOptions>
217
+ export function isCid(value: unknown, options?: CheckCidOptions): value is Cid
218
+ export function isCid(value: unknown, options?: CheckCidOptions): value is Cid {
219
+ return isCidImplementation(value) && checkCid(value, options)
220
+ }
96
221
 
97
- if (options?.strict) {
98
- if (cid.version !== 1) {
99
- return false
100
- }
101
- if (cid.code !== RAW_BIN_MULTICODEC && cid.code !== DAG_CBOR_MULTICODEC) {
102
- return false
103
- }
104
- if (cid.multihash.code !== SHA2_256_MULTIHASH_CODE) {
105
- return false
106
- }
107
- }
222
+ /**
223
+ * Returns the input value as a {@link Cid} if it is valid, or `null` otherwise.
224
+ */
225
+ export function ifCid<TValue, TOptions extends CheckCidOptions>(
226
+ value: unknown,
227
+ options: TOptions,
228
+ ): (TValue & InferCheckedCid<TOptions>) | null
229
+ export function ifCid<TValue>(
230
+ value: TValue,
231
+ options?: CheckCidOptions,
232
+ ): (TValue & Cid) | null
233
+ export function ifCid(value: unknown, options?: CheckCidOptions): Cid | null {
234
+ if (isCidImplementation(value) && checkCid(value, options)) return value
235
+ return null
236
+ }
237
+
238
+ /**
239
+ * Returns the input value as a {@link Cid} if it is valid.
240
+ *
241
+ * @throws if the input is not a valid {@link Cid}.
242
+ */
243
+ export function asCid<TValue, TOptions extends CheckCidOptions>(
244
+ value: TValue,
245
+ options: TOptions,
246
+ ): TValue & InferCheckedCid<TOptions>
247
+ export function asCid<TValue>(
248
+ value: TValue,
249
+ options?: CheckCidOptions,
250
+ ): Cid & TValue
251
+ export function asCid(value: unknown, options?: CheckCidOptions): Cid {
252
+ if (isCidImplementation(value) && checkCid(value, options)) return value
253
+ throw new Error('Not a valid CID')
254
+ }
108
255
 
109
- return true
256
+ /**
257
+ * Decodes a CID from its binary representation.
258
+ *
259
+ * @see {@link https://dasl.ing/cid.html DASL-CIDs}
260
+ * @throws if the input do not represent a valid DASL {@link Cid}
261
+ */
262
+ export function decodeCid<TOptions extends CheckCidOptions>(
263
+ cidBytes: Uint8Array,
264
+ options: TOptions,
265
+ ): InferCheckedCid<TOptions>
266
+ export function decodeCid(cidBytes: Uint8Array, options?: CheckCidOptions): Cid
267
+ export function decodeCid(
268
+ cidBytes: Uint8Array,
269
+ options?: CheckCidOptions,
270
+ ): Cid {
271
+ const cid = CID.decode(cidBytes)
272
+ return asCid(cid, options)
110
273
  }
111
274
 
112
- export function validateCidString(input: string): boolean {
113
- return parseCidString(input)?.toString() === input
275
+ /**
276
+ * Parses a CID string into a Cid object.
277
+ *
278
+ * @throws if the input is not a valid CID string.
279
+ */
280
+ export function parseCid<TOptions extends CheckCidOptions>(
281
+ input: string,
282
+ options: TOptions,
283
+ ): InferCheckedCid<TOptions>
284
+ export function parseCid(input: string, options?: CheckCidOptions): Cid
285
+ export function parseCid(input: string, options?: CheckCidOptions): Cid {
286
+ const cid = CID.parse(input)
287
+ return asCid(cid, options)
114
288
  }
115
289
 
116
- export function parseCidString(input: string): Cid | undefined {
290
+ export function validateCidString(
291
+ input: string,
292
+ options?: CheckCidOptions,
293
+ ): boolean {
294
+ return parseCidSafe(input, options)?.toString() === input
295
+ }
296
+
297
+ export function parseCidSafe<TOptions extends CheckCidOptions>(
298
+ input: string,
299
+ options: TOptions,
300
+ ): InferCheckedCid<TOptions> | null
301
+ export function parseCidSafe(
302
+ input: string,
303
+ options?: CheckCidOptions,
304
+ ): Cid | null
305
+ export function parseCidSafe(
306
+ input: string,
307
+ options?: CheckCidOptions,
308
+ ): Cid | null {
117
309
  try {
118
- return parseCid(input)
310
+ return parseCid(input, options)
119
311
  } catch {
120
- return undefined
312
+ return null
121
313
  }
122
314
  }
123
315
 
124
- export function ensureValidCidString(input: string): void {
125
- if (!validateCidString(input)) {
316
+ export function ensureValidCidString(
317
+ input: string,
318
+ options?: CheckCidOptions,
319
+ ): void {
320
+ if (!validateCidString(input, options)) {
126
321
  throw new Error(`Invalid CID string`)
127
322
  }
128
323
  }
324
+
325
+ /**
326
+ * Verifies whether the multihash of a given {@link cid} matches the hash of the provided {@link bytes}.
327
+ * @params cid The CID to match against the bytes.
328
+ * @params bytes The bytes to verify.
329
+ * @returns true if the CID matches the bytes, false otherwise.
330
+ */
331
+ export async function isCidForBytes(
332
+ cid: Cid,
333
+ bytes: Uint8Array,
334
+ ): Promise<boolean> {
335
+ if (cid.multihash.code === sha256.code) {
336
+ const multihash = await sha256.digest(bytes)
337
+ return multihashEquals(multihash, cid.multihash)
338
+ }
339
+
340
+ if (cid.multihash.code === sha512.code) {
341
+ const multihash = await sha512.digest(bytes)
342
+ return multihashEquals(multihash, cid.multihash)
343
+ }
344
+
345
+ // Don't know how to verify other multihash codes
346
+ throw new Error('Unsupported CID multihash')
347
+ }
348
+
349
+ export function createCid<TCode extends number, TMultihashCode extends number>(
350
+ code: TCode,
351
+ multihashCode: TMultihashCode,
352
+ digest: Uint8Array,
353
+ ) {
354
+ const cid: Cid = CID.createV1(code, createDigest(multihashCode, digest))
355
+ return cid as Cid<1, TCode, TMultihashCode>
356
+ }
357
+
358
+ export async function cidForCbor(bytes: Uint8Array): Promise<CborCid> {
359
+ const multihash = await sha256.digest(bytes)
360
+ return CID.createV1(DAG_CBOR_MULTICODEC, multihash) as CborCid
361
+ }
362
+
363
+ export async function cidForRawBytes(bytes: Uint8Array): Promise<RawCid> {
364
+ const multihash = await sha256.digest(bytes)
365
+ return CID.createV1(RAW_MULTICODEC, multihash) as RawCid
366
+ }
367
+
368
+ export function cidForRawHash(digest: Uint8Array): RawCid {
369
+ // Fool-proofing
370
+ if (digest.length !== 32) {
371
+ throw new Error(`Invalid SHA-256 hash length: ${digest.length}`)
372
+ }
373
+ return createCid(RAW_MULTICODEC, sha256.code, digest)
374
+ }
375
+
376
+ /**
377
+ * @internal
378
+ */
379
+ function isCidImplementation(value: unknown): value is Cid {
380
+ if (CID.asCID(value)) {
381
+ // CIDs created using older multiformats versions did not have a "bytes"
382
+ // property.
383
+ return (value as { bytes?: Uint8Array }).bytes != null
384
+ } else {
385
+ // Unknown implementation, do a structural check
386
+ try {
387
+ if (!isObject(value)) return false
388
+
389
+ const val = value as Record<string, unknown>
390
+ if (val.version !== 0 && val.version !== 1) return false
391
+ if (!isUint8(val.code)) return false
392
+
393
+ if (!isObject(val.multihash)) return false
394
+ const mh = val.multihash as Record<string, unknown>
395
+ if (!isUint8(mh.code)) return false
396
+ if (!(mh.digest instanceof Uint8Array)) return false
397
+
398
+ // Ensure that the bytes array is consistent with other properties
399
+ if (!(val.bytes instanceof Uint8Array)) return false
400
+ if (val.bytes[0] !== val.version) return false
401
+ if (val.bytes[1] !== val.code) return false
402
+ if (val.bytes[2] !== mh.code) return false
403
+ if (val.bytes[3] !== mh.digest.length) return false
404
+ if (val.bytes.length !== 4 + mh.digest.length) return false
405
+ if (!ui8Equals(val.bytes.subarray(4), mh.digest)) return false
406
+
407
+ if (typeof val.equals !== 'function') return false
408
+ if (val.equals(val) !== true) return false
409
+
410
+ return true
411
+ } catch {
412
+ return false
413
+ }
414
+ }
415
+ }
416
+
417
+ /**
418
+ * @internal
419
+ */
420
+ function isUint8(val: unknown): val is number {
421
+ return Number.isInteger(val) && (val as number) >= 0 && (val as number) < 256
422
+ }
package/src/lex-equals.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { asCid, isCid } from './cid.js'
1
+ import { ifCid, isCid } from './cid.js'
2
2
  import { LexValue } from './lex.js'
3
3
  import { isPlainObject } from './object.js'
4
4
  import { ui8Equals } from './uint8array.js'
@@ -44,7 +44,7 @@ export function lexEquals(a: LexValue, b: LexValue): boolean {
44
44
  if (isCid(a)) {
45
45
  // @NOTE CID.equals returns its argument when it is falsy (e.g. null or
46
46
  // undefined) so we need to explicitly check that the output is "true".
47
- return asCid(a)!.equals(asCid(b)) === true
47
+ return ifCid(b)?.equals(a) === true
48
48
  } else if (isCid(b)) {
49
49
  return false
50
50
  }