@atproto/lex-data 0.0.7 → 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,9 +1,8 @@
1
1
  import { CID } from 'multiformats/cid'
2
- import {
3
- create as createDigest,
4
- equals as digestEquals,
5
- } from 'multiformats/hashes/digest'
2
+ import { create as createDigest } from 'multiformats/hashes/digest'
6
3
  import { sha256, sha512 } from 'multiformats/hashes/sha2'
4
+ import { isObject } from './object.js'
5
+ import { ui8Equals } from './uint8array.js'
7
6
 
8
7
  export const DAG_CBOR_MULTICODEC = 0x71 // DRISL conformant DAG-CBOR
9
8
  export type DAG_CBOR_MULTICODEC = typeof DAG_CBOR_MULTICODEC
@@ -14,11 +13,23 @@ export type RAW_MULTICODEC = typeof RAW_MULTICODEC
14
13
  export const SHA256_MULTIHASH = sha256.code
15
14
  export type SHA256_MULTIHASH = typeof SHA256_MULTIHASH
16
15
 
17
- export type MultihashDigest<Code extends number = number> = {
18
- code: Code
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
+ */
19
28
  digest: Uint8Array
20
- size: number
21
- bytes: Uint8Array
29
+ }
30
+
31
+ export function multihashEquals(a: Multihash, b: Multihash): boolean {
32
+ return a.code === b.code && ui8Equals(a.digest, b.digest)
22
33
  }
23
34
 
24
35
  declare module 'multiformats/cid' {
@@ -38,8 +49,8 @@ declare module 'multiformats/cid' {
38
49
  *
39
50
  * In order to avoid compatibility issues, while preparing for future breaking
40
51
  * changes (CID in multiformats v10+ has a slightly different interface), as
41
- * we update or swap out `multiformats`, we provide our own stable {@link Cid}
42
- * interface.
52
+ * we update or swap out `multiformats`, `@atproto/lex-data` provides its own
53
+ * stable {@link Cid} interface.
43
54
  */
44
55
  interface CID {}
45
56
  }
@@ -62,30 +73,71 @@ declare module 'multiformats/cid' {
62
73
 
63
74
  // @NOTE Even though it is not portable, we still re-export CID here so that
64
75
  // dependent packages where it can be used, have access to it (instead of
65
- // importing directly from "multiformats" or"multiformats/cid").
66
- export { CID }
76
+ // importing directly from "multiformats" or "multiformats/cid").
77
+ export { /** @deprecated */ CID }
67
78
 
68
79
  /**
69
- * Interface for working with decoded CID string, compatible with
70
- * {@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.
71
86
  */
72
- export interface Cid {
73
- version: 0 | 1
74
- code: number
75
- multihash: MultihashDigest
76
- bytes: Uint8Array
77
- 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
78
130
  toString(): string
79
131
  }
80
132
 
81
133
  /**
82
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
+ *
83
138
  * @see {@link https://atproto.com/specs/data-model#link-and-cid-formats ATproto Data Model - Link and CID Formats}
84
139
  */
85
- export interface RawCid extends Cid {
86
- version: 1
87
- code: RAW_MULTICODEC
88
- }
140
+ export type RawCid = Cid<1, RAW_MULTICODEC>
89
141
 
90
142
  export function isRawCid(cid: Cid): cid is RawCid {
91
143
  return cid.version === 1 && cid.code === RAW_MULTICODEC
@@ -95,18 +147,18 @@ export function isRawCid(cid: Cid): cid is RawCid {
95
147
  * Represents a DASL compliant CID.
96
148
  * @see {@link https://dasl.ing/cid.html DASL-CIDs}
97
149
  */
98
- export interface DaslCid extends Cid {
99
- version: 1
100
- code: RAW_MULTICODEC | DAG_CBOR_MULTICODEC
101
- multihash: MultihashDigest<SHA256_MULTIHASH>
102
- }
150
+ export type DaslCid = Cid<
151
+ 1,
152
+ RAW_MULTICODEC | DAG_CBOR_MULTICODEC,
153
+ SHA256_MULTIHASH
154
+ >
103
155
 
104
156
  export function isDaslCid(cid: Cid): cid is DaslCid {
105
157
  return (
106
158
  cid.version === 1 &&
107
159
  (cid.code === RAW_MULTICODEC || cid.code === DAG_CBOR_MULTICODEC) &&
108
160
  cid.multihash.code === SHA256_MULTIHASH &&
109
- cid.multihash.size === 32 // Should always be 32 bytes (256 bits) for SHA-256
161
+ cid.multihash.digest.byteLength === 32 // Should always be 32 bytes (256 bits) for SHA-256, but double-checking anyways
110
162
  )
111
163
  }
112
164
 
@@ -114,17 +166,16 @@ export function isDaslCid(cid: Cid): cid is DaslCid {
114
166
  * Represents the cid of ATProto DAG-CBOR data (like repository MST nodes).
115
167
  * @see {@link https://atproto.com/specs/data-model#link-and-cid-formats ATproto Data Model - Link and CID Formats}
116
168
  */
117
- export interface CborCid extends DaslCid {
118
- code: DAG_CBOR_MULTICODEC
119
- }
169
+ export type CborCid = Cid<1, DAG_CBOR_MULTICODEC, SHA256_MULTIHASH>
120
170
 
121
171
  export function isCborCid(cid: Cid): cid is CborCid {
122
172
  return cid.code === DAG_CBOR_MULTICODEC && isDaslCid(cid)
123
173
  }
124
174
 
125
- export type CidCheckOptions = {
175
+ export type CheckCidOptions = {
126
176
  flavor?: 'raw' | 'cbor' | 'dasl'
127
177
  }
178
+
128
179
  export type InferCheckedCid<TOptions> = TOptions extends { flavor: 'raw' }
129
180
  ? RawCid
130
181
  : TOptions extends { flavor: 'cbor' }
@@ -132,67 +183,74 @@ export type InferCheckedCid<TOptions> = TOptions extends { flavor: 'raw' }
132
183
  : Cid
133
184
 
134
185
  /**
135
- * Coerces the input value to a Cid, or returns null if not possible.
186
+ * Type guard to check whether a {@link Cid} instance meets specific flavor
187
+ * constraints.
136
188
  */
137
- export function ifCid<TOptions extends CidCheckOptions>(
138
- value: unknown,
189
+ export function checkCid<TOptions extends CheckCidOptions>(
190
+ cid: Cid,
139
191
  options: TOptions,
140
- ): InferCheckedCid<TOptions> | null
141
- export function ifCid(value: unknown, options?: CidCheckOptions): Cid | null
142
- export function ifCid(value: unknown, options?: CidCheckOptions): Cid | null {
143
- const cid = CID.asCID(value)
144
- if (!cid) {
145
- return null
146
- }
147
-
192
+ ): cid is InferCheckedCid<TOptions>
193
+ export function checkCid(cid: Cid, options?: CheckCidOptions): boolean
194
+ export function checkCid(cid: Cid, options?: CheckCidOptions): boolean {
148
195
  switch (options?.flavor) {
196
+ case undefined:
197
+ return true
149
198
  case 'cbor':
150
- return isCborCid(cid) ? cid : null
151
- case 'raw':
152
- return isRawCid(cid) ? cid : null
199
+ return isCborCid(cid)
153
200
  case 'dasl':
154
- return isDaslCid(cid) ? cid : null
201
+ return isDaslCid(cid)
202
+ case 'raw':
203
+ return isRawCid(cid)
155
204
  default:
156
- return cid
205
+ throw new TypeError(`Unknown CID flavor: ${options?.flavor}`)
157
206
  }
158
207
  }
159
208
 
160
- export function isCid<TOptions extends CidCheckOptions>(
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>(
161
214
  value: unknown,
162
215
  options: TOptions,
163
216
  ): value is InferCheckedCid<TOptions>
164
- export function isCid(value: unknown, options?: CidCheckOptions): value is Cid
165
- export function isCid(value: unknown, options?: CidCheckOptions): value is Cid {
166
- return ifCid(value, options) !== null
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)
167
220
  }
168
221
 
169
222
  /**
170
- * Coerces the input value to a Cid, or throws if not possible.
223
+ * Returns the input value as a {@link Cid} if it is valid, or `null` otherwise.
171
224
  */
172
- export function asCid<TOptions extends CidCheckOptions>(
225
+ export function ifCid<TValue, TOptions extends CheckCidOptions>(
173
226
  value: unknown,
174
227
  options: TOptions,
175
- ): InferCheckedCid<TOptions>
176
- export function asCid(value: unknown, options?: CidCheckOptions): Cid
177
- export function asCid(value: unknown, options?: CidCheckOptions): Cid {
178
- const cid = ifCid(value, options)
179
- if (cid) return cid
180
- throw new Error('Not a valid CID')
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
181
236
  }
182
237
 
183
238
  /**
184
- * Parses a CID string into a Cid object.
239
+ * Returns the input value as a {@link Cid} if it is valid.
185
240
  *
186
- * @throws if the input is not a valid CID string.
241
+ * @throws if the input is not a valid {@link Cid}.
187
242
  */
188
- export function parseCid<TOptions extends CidCheckOptions>(
189
- input: string,
243
+ export function asCid<TValue, TOptions extends CheckCidOptions>(
244
+ value: TValue,
190
245
  options: TOptions,
191
- ): InferCheckedCid<TOptions>
192
- export function parseCid(input: string, options?: CidCheckOptions): Cid
193
- export function parseCid(input: string, options?: CidCheckOptions): Cid {
194
- const cid = CID.parse(input)
195
- return asCid(cid, options)
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')
196
254
  }
197
255
 
198
256
  /**
@@ -201,48 +259,63 @@ export function parseCid(input: string, options?: CidCheckOptions): Cid {
201
259
  * @see {@link https://dasl.ing/cid.html DASL-CIDs}
202
260
  * @throws if the input do not represent a valid DASL {@link Cid}
203
261
  */
204
- export function decodeCid<TOptions extends CidCheckOptions>(
262
+ export function decodeCid<TOptions extends CheckCidOptions>(
205
263
  cidBytes: Uint8Array,
206
264
  options: TOptions,
207
265
  ): InferCheckedCid<TOptions>
208
- export function decodeCid(cidBytes: Uint8Array, options?: CidCheckOptions): Cid
266
+ export function decodeCid(cidBytes: Uint8Array, options?: CheckCidOptions): Cid
209
267
  export function decodeCid(
210
268
  cidBytes: Uint8Array,
211
- options?: CidCheckOptions,
269
+ options?: CheckCidOptions,
212
270
  ): Cid {
213
271
  const cid = CID.decode(cidBytes)
214
272
  return asCid(cid, options)
215
273
  }
216
274
 
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)
288
+ }
289
+
217
290
  export function validateCidString(
218
291
  input: string,
219
- options?: CidCheckOptions,
292
+ options?: CheckCidOptions,
220
293
  ): boolean {
221
- return parseCidString(input, options)?.toString() === input
294
+ return parseCidSafe(input, options)?.toString() === input
222
295
  }
223
296
 
224
- export function parseCidString<TOptions extends CidCheckOptions>(
297
+ export function parseCidSafe<TOptions extends CheckCidOptions>(
225
298
  input: string,
226
299
  options: TOptions,
227
- ): InferCheckedCid<TOptions> | undefined
228
- export function parseCidString(
300
+ ): InferCheckedCid<TOptions> | null
301
+ export function parseCidSafe(
229
302
  input: string,
230
- options?: CidCheckOptions,
231
- ): Cid | undefined
232
- export function parseCidString(
303
+ options?: CheckCidOptions,
304
+ ): Cid | null
305
+ export function parseCidSafe(
233
306
  input: string,
234
- options?: CidCheckOptions,
235
- ): Cid | undefined {
307
+ options?: CheckCidOptions,
308
+ ): Cid | null {
236
309
  try {
237
310
  return parseCid(input, options)
238
311
  } catch {
239
- return undefined
312
+ return null
240
313
  }
241
314
  }
242
315
 
243
316
  export function ensureValidCidString(
244
317
  input: string,
245
- options?: CidCheckOptions,
318
+ options?: CheckCidOptions,
246
319
  ): void {
247
320
  if (!validateCidString(input, options)) {
248
321
  throw new Error(`Invalid CID string`)
@@ -260,34 +333,90 @@ export async function isCidForBytes(
260
333
  bytes: Uint8Array,
261
334
  ): Promise<boolean> {
262
335
  if (cid.multihash.code === sha256.code) {
263
- const digest = await sha256.digest(bytes)
264
- return digestEquals(cid.multihash, digest)
336
+ const multihash = await sha256.digest(bytes)
337
+ return multihashEquals(multihash, cid.multihash)
265
338
  }
266
339
 
267
340
  if (cid.multihash.code === sha512.code) {
268
- const digest = await sha512.digest(bytes)
269
- return digestEquals(cid.multihash, digest)
341
+ const multihash = await sha512.digest(bytes)
342
+ return multihashEquals(multihash, cid.multihash)
270
343
  }
271
344
 
272
345
  // Don't know how to verify other multihash codes
273
346
  throw new Error('Unsupported CID multihash')
274
347
  }
275
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
+
276
358
  export async function cidForCbor(bytes: Uint8Array): Promise<CborCid> {
277
- const digest = await sha256.digest(bytes)
278
- return CID.createV1(DAG_CBOR_MULTICODEC, digest) as CborCid
359
+ const multihash = await sha256.digest(bytes)
360
+ return CID.createV1(DAG_CBOR_MULTICODEC, multihash) as CborCid
279
361
  }
280
362
 
281
363
  export async function cidForRawBytes(bytes: Uint8Array): Promise<RawCid> {
282
- const digest = await sha256.digest(bytes)
283
- return CID.createV1(RAW_MULTICODEC, digest) as RawCid
364
+ const multihash = await sha256.digest(bytes)
365
+ return CID.createV1(RAW_MULTICODEC, multihash) as RawCid
284
366
  }
285
367
 
286
- export function cidForRawHash(hash: Uint8Array): RawCid {
368
+ export function cidForRawHash(digest: Uint8Array): RawCid {
287
369
  // Fool-proofing
288
- if (hash.length !== 32) {
289
- throw new Error(`Invalid SHA-256 hash length: ${hash.length}`)
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
+ }
290
414
  }
291
- const digest = createDigest(sha256.code, hash)
292
- return CID.createV1(RAW_MULTICODEC, digest) as RawCid
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
293
422
  }