@helia/verified-fetch 2.2.2 → 2.3.1

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/index.ts CHANGED
@@ -5,7 +5,12 @@
5
5
  *
6
6
  * All content is retrieved in a [trustless manner](https://www.techopedia.com/definition/trustless), and the integrity of all bytes are verified by comparing hashes of the data.
7
7
  *
8
- * By default, providers for CIDs are found with delegated routers and retrieved over HTTP from [trustless gateways](https://specs.ipfs.tech/http-gateways/trustless-gateway/), and WebTransport and WebRTC providers if available.
8
+ * By default, providers for CIDs are found using [delegated routing endpoints](https://docs.ipfs.tech/concepts/public-utilities/#delegated-routing).
9
+ *
10
+ * Data is retrieved using the following strategies:
11
+ * - Directly from providers, using [Bitswap](https://docs.ipfs.tech/concepts/bitswap/) over WebSockets and WebRTC if available.
12
+ * - Directly from providers exposing a [trustless gateway](https://specs.ipfs.tech/http-gateways/trustless-gateway/) over HTTPS.
13
+ * - As a fallback, if no providers reachable from a browser are found, data is retrieved using recursive gateways, e.g. `trustless-gateway.link` which can be configured.
9
14
  *
10
15
  * This is a marked improvement over `fetch` which offers no such protections and is vulnerable to all sorts of attacks like [Content Spoofing](https://owasp.org/www-community/attacks/Content_Spoofing), [DNS Hijacking](https://en.wikipedia.org/wiki/DNS_hijacking), etc.
11
16
  *
@@ -189,6 +194,25 @@
189
194
  * }
190
195
  * })
191
196
  * ```
197
+ * ### Custom Hashers
198
+ *
199
+ * By default, `@helia/verified-fetch` supports `sha256`, `sha512`, and `identity` hashers.
200
+ *
201
+ * If you need to use a different hasher, you can provide a [custom `hasher` function](https://multiformats.github.io/js-multiformats/interfaces/hashes_interface.MultihashHasher.html) as an option to `createVerifiedFetch`.
202
+ *
203
+ * @example Passing a custom hashing function
204
+ *
205
+ * ```typescript
206
+ * import { createVerifiedFetch } from '@helia/verified-fetch'
207
+ * import { blake2b256 } from '@multiformats/blake2/blake2b'
208
+ *
209
+ * const verifiedFetch = await createVerifiedFetch({
210
+ * gateways: ['https://ipfs.io'],
211
+ * hashers: [blake2b256]
212
+ * })
213
+ *
214
+ * const resp = await verifiedFetch('ipfs://cid-using-blake2b256')
215
+ * ```
192
216
  *
193
217
  * ### IPLD codec handling
194
218
  *
@@ -602,8 +626,9 @@ import { type ResolveDNSLinkProgressEvents } from '@helia/ipns'
602
626
  import { httpGatewayRouting, libp2pRouting } from '@helia/routers'
603
627
  import { type Libp2p, type ServiceMap } from '@libp2p/interface'
604
628
  import { dns } from '@multiformats/dns'
605
- import { createHelia } from 'helia'
629
+ import { createHelia, type HeliaInit } from 'helia'
606
630
  import { createLibp2p, type Libp2pOptions } from 'libp2p'
631
+ import { type ContentTypeParser } from './types.js'
607
632
  import { getLibp2pConfig } from './utils/libp2p-defaults.js'
608
633
  import { VerifiedFetch as VerifiedFetchClass } from './verified-fetch.js'
609
634
  import type { GetBlockProgressEvents, Helia, Routing } from '@helia/interface'
@@ -612,7 +637,6 @@ import type { DNSResolver } from '@multiformats/dns/resolvers'
612
637
  import type { ExporterProgressEvents } from 'ipfs-unixfs-exporter'
613
638
  import type { CID } from 'multiformats/cid'
614
639
  import type { ProgressEvent, ProgressOptions } from 'progress-events'
615
-
616
640
  /**
617
641
  * The types for the first argument of the `verifiedFetch` function.
618
642
  */
@@ -657,6 +681,13 @@ export interface CreateVerifiedFetchInit {
657
681
  */
658
682
  dnsResolvers?: DNSResolver[] | DNSResolvers
659
683
 
684
+ /**
685
+ * By default sha256, sha512 and identity hashes are supported for
686
+ * retrieval operations. To retrieve blocks by CIDs using other hashes
687
+ * pass appropriate MultihashHashers here.
688
+ */
689
+ hashers?: HeliaInit['hashers']
690
+
660
691
  /**
661
692
  * By default we will not connect to any HTTP Gateways providers over local or
662
693
  * loopback addresses, this is because they are typically running on remote
@@ -721,19 +752,7 @@ export interface CreateVerifiedFetchOptions {
721
752
  sessionTTLms?: number
722
753
  }
723
754
 
724
- /**
725
- * A ContentTypeParser attempts to return the mime type of a given file. It
726
- * receives the first chunk of the file data and the file name, if it is
727
- * available. The function can be sync or async and if it returns/resolves to
728
- * `undefined`, `application/octet-stream` will be used.
729
- */
730
- export interface ContentTypeParser {
731
- /**
732
- * Attempt to determine a mime type, either via of the passed bytes or the
733
- * filename if it is available.
734
- */
735
- (bytes: Uint8Array, fileName?: string): Promise<string | undefined> | string | undefined
736
- }
755
+ export type { ContentTypeParser } from './types.js'
737
756
 
738
757
  export type BubbledProgressEvents =
739
758
  // unixfs-exporter
@@ -838,7 +857,8 @@ export async function createVerifiedFetch (init?: Helia | CreateVerifiedFetchIni
838
857
  libp2p,
839
858
  blockBrokers,
840
859
  dns,
841
- routers
860
+ routers,
861
+ hashers: init?.hashers
842
862
  })
843
863
  init.logger.forComponent('helia:verified-fetch').trace('created verified-fetch with libp2p config: %j', libp2pConfig)
844
864
  }
package/src/types.ts CHANGED
@@ -30,3 +30,17 @@ export interface FetchHandlerFunctionArg {
30
30
  */
31
31
  resource: string
32
32
  }
33
+
34
+ /**
35
+ * A ContentTypeParser attempts to return the mime type of a given file. It
36
+ * receives the first chunk of the file data and the file name, if it is
37
+ * available. The function can be sync or async and if it returns/resolves to
38
+ * `undefined`, `application/octet-stream` will be used.
39
+ */
40
+ export interface ContentTypeParser {
41
+ /**
42
+ * Attempt to determine a mime type, either via of the passed bytes or the
43
+ * filename if it is available.
44
+ */
45
+ (bytes: Uint8Array, fileName?: string): Promise<string | undefined> | string | undefined
46
+ }
@@ -0,0 +1,38 @@
1
+ import { type Logger } from '@libp2p/interface'
2
+ import { type ContentTypeParser } from '../types.js'
3
+ import { isPromise } from './type-guards.js'
4
+
5
+ export interface SetContentTypeOptions {
6
+ bytes: Uint8Array
7
+ path: string
8
+ response: Response
9
+ defaultContentType?: string
10
+ contentTypeParser: ContentTypeParser | undefined
11
+ log: Logger
12
+ }
13
+
14
+ export async function setContentType ({ bytes, path, response, contentTypeParser, log, defaultContentType = 'application/octet-stream' }: SetContentTypeOptions): Promise<void> {
15
+ let contentType: string | undefined
16
+
17
+ if (contentTypeParser != null) {
18
+ try {
19
+ let fileName = path.split('/').pop()?.trim()
20
+ fileName = fileName === '' ? undefined : fileName
21
+ const parsed = contentTypeParser(bytes, fileName)
22
+
23
+ if (isPromise(parsed)) {
24
+ const result = await parsed
25
+
26
+ if (result != null) {
27
+ contentType = result
28
+ }
29
+ } else if (parsed != null) {
30
+ contentType = parsed
31
+ }
32
+ } catch (err) {
33
+ log.error('error parsing content type', err)
34
+ }
35
+ }
36
+ log.trace('setting content type to "%s"', contentType ?? defaultContentType)
37
+ response.headers.set('content-type', contentType ?? defaultContentType)
38
+ }
@@ -0,0 +1,3 @@
1
+ export function isPromise <T> (p?: any): p is Promise<T> {
2
+ return p?.then != null
3
+ }
@@ -19,7 +19,7 @@ export interface PathWalkerFn {
19
19
  (blockstore: ReadableStorage, path: string, options?: PathWalkerOptions): Promise<PathWalkerResponse>
20
20
  }
21
21
 
22
- export async function walkPath (blockstore: ReadableStorage, path: string, options?: PathWalkerOptions): Promise<PathWalkerResponse> {
22
+ async function walkPath (blockstore: ReadableStorage, path: string, options?: PathWalkerOptions): Promise<PathWalkerResponse> {
23
23
  const ipfsRoots: CID[] = []
24
24
  let terminalElement: UnixFSEntry | undefined
25
25
 
@@ -6,7 +6,7 @@ import { code as dagPbCode } from '@ipld/dag-pb'
6
6
  import { type AbortOptions, type Logger, type PeerId } from '@libp2p/interface'
7
7
  import { Record as DHTRecord } from '@libp2p/kad-dht'
8
8
  import { Key } from 'interface-datastore'
9
- import { exporter } from 'ipfs-unixfs-exporter'
9
+ import { exporter, type ObjectNode } from 'ipfs-unixfs-exporter'
10
10
  import toBrowserReadableStream from 'it-to-browser-readablestream'
11
11
  import { LRUCache } from 'lru-cache'
12
12
  import { type CID } from 'multiformats/cid'
@@ -32,12 +32,12 @@ import { resourceToSessionCacheKey } from './utils/resource-to-cache-key.js'
32
32
  import { setCacheControlHeader, setIpfsRoots } from './utils/response-headers.js'
33
33
  import { badRequestResponse, movedPermanentlyResponse, notAcceptableResponse, notSupportedResponse, okResponse, badRangeResponse, okRangeResponse, badGatewayResponse, notFoundResponse } from './utils/responses.js'
34
34
  import { selectOutputType } from './utils/select-output-type.js'
35
+ import { setContentType } from './utils/set-content-type.js'
35
36
  import { handlePathWalking, isObjectNode } from './utils/walk-path.js'
36
37
  import type { CIDDetail, ContentTypeParser, CreateVerifiedFetchOptions, Resource, ResourceDetail, VerifiedFetchInit as VerifiedFetchOptions } from './index.js'
37
38
  import type { FetchHandlerFunctionArg, RequestFormatShorthand } from './types.js'
38
39
  import type { Helia, SessionBlockstore } from '@helia/interface'
39
40
  import type { Blockstore } from 'interface-blockstore'
40
- import type { ObjectNode } from 'ipfs-unixfs-exporter'
41
41
 
42
42
  const SESSION_CACHE_MAX_SIZE = 100
43
43
  const SESSION_CACHE_TTL_MS = 60 * 1000
@@ -398,7 +398,7 @@ export class VerifiedFetch {
398
398
  redirected
399
399
  })
400
400
 
401
- await this.setContentType(firstChunk, path, response)
401
+ await setContentType({ bytes: firstChunk, path, response, contentTypeParser: this.contentTypeParser, log: this.log })
402
402
  setIpfsRoots(response, ipfsRoots)
403
403
 
404
404
  return response
@@ -434,37 +434,11 @@ export class VerifiedFetch {
434
434
  // if the user has specified an `Accept` header that corresponds to a raw
435
435
  // type, honour that header, so for example they don't request
436
436
  // `application/vnd.ipld.raw` but get `application/octet-stream`
437
- await this.setContentType(result, path, response, getOverridenRawContentType({ headers: options?.headers, accept }))
437
+ await setContentType({ bytes: result, path, response, defaultContentType: getOverridenRawContentType({ headers: options?.headers, accept }), contentTypeParser: this.contentTypeParser, log: this.log })
438
438
 
439
439
  return response
440
440
  }
441
441
 
442
- private async setContentType (bytes: Uint8Array, path: string, response: Response, defaultContentType = 'application/octet-stream'): Promise<void> {
443
- let contentType: string | undefined
444
-
445
- if (this.contentTypeParser != null) {
446
- try {
447
- let fileName = path.split('/').pop()?.trim()
448
- fileName = fileName === '' ? undefined : fileName
449
- const parsed = this.contentTypeParser(bytes, fileName)
450
-
451
- if (isPromise(parsed)) {
452
- const result = await parsed
453
-
454
- if (result != null) {
455
- contentType = result
456
- }
457
- } else if (parsed != null) {
458
- contentType = parsed
459
- }
460
- } catch (err) {
461
- this.log.error('error parsing content type', err)
462
- }
463
- }
464
- this.log.trace('setting content type to "%s"', contentType ?? defaultContentType)
465
- response.headers.set('content-type', contentType ?? defaultContentType)
466
- }
467
-
468
442
  /**
469
443
  * If the user has not specified an Accept header or format query string arg,
470
444
  * use the CID codec to choose an appropriate handler for the block data.
@@ -614,7 +588,3 @@ export class VerifiedFetch {
614
588
  await this.helia.stop()
615
589
  }
616
590
  }
617
-
618
- function isPromise <T> (p?: any): p is Promise<T> {
619
- return p?.then != null
620
- }