@helia/verified-fetch 0.0.0-28d62f7

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.
Files changed (79) hide show
  1. package/LICENSE +4 -0
  2. package/README.md +531 -0
  3. package/dist/index.min.js +118 -0
  4. package/dist/src/index.d.ts +539 -0
  5. package/dist/src/index.d.ts.map +1 -0
  6. package/dist/src/index.js +505 -0
  7. package/dist/src/index.js.map +1 -0
  8. package/dist/src/singleton.d.ts +3 -0
  9. package/dist/src/singleton.d.ts.map +1 -0
  10. package/dist/src/singleton.js +15 -0
  11. package/dist/src/singleton.js.map +1 -0
  12. package/dist/src/types.d.ts +2 -0
  13. package/dist/src/types.d.ts.map +1 -0
  14. package/dist/src/types.js +2 -0
  15. package/dist/src/types.js.map +1 -0
  16. package/dist/src/utils/dag-cbor-to-safe-json.d.ts +7 -0
  17. package/dist/src/utils/dag-cbor-to-safe-json.d.ts.map +1 -0
  18. package/dist/src/utils/dag-cbor-to-safe-json.js +37 -0
  19. package/dist/src/utils/dag-cbor-to-safe-json.js.map +1 -0
  20. package/dist/src/utils/get-content-disposition-filename.d.ts +6 -0
  21. package/dist/src/utils/get-content-disposition-filename.d.ts.map +1 -0
  22. package/dist/src/utils/get-content-disposition-filename.js +16 -0
  23. package/dist/src/utils/get-content-disposition-filename.js.map +1 -0
  24. package/dist/src/utils/get-e-tag.d.ts +28 -0
  25. package/dist/src/utils/get-e-tag.d.ts.map +1 -0
  26. package/dist/src/utils/get-e-tag.js +18 -0
  27. package/dist/src/utils/get-e-tag.js.map +1 -0
  28. package/dist/src/utils/get-stream-from-async-iterable.d.ts +10 -0
  29. package/dist/src/utils/get-stream-from-async-iterable.d.ts.map +1 -0
  30. package/dist/src/utils/get-stream-from-async-iterable.js +38 -0
  31. package/dist/src/utils/get-stream-from-async-iterable.js.map +1 -0
  32. package/dist/src/utils/get-tar-stream.d.ts +4 -0
  33. package/dist/src/utils/get-tar-stream.d.ts.map +1 -0
  34. package/dist/src/utils/get-tar-stream.js +46 -0
  35. package/dist/src/utils/get-tar-stream.js.map +1 -0
  36. package/dist/src/utils/parse-resource.d.ts +18 -0
  37. package/dist/src/utils/parse-resource.d.ts.map +1 -0
  38. package/dist/src/utils/parse-resource.js +24 -0
  39. package/dist/src/utils/parse-resource.js.map +1 -0
  40. package/dist/src/utils/parse-url-string.d.ts +32 -0
  41. package/dist/src/utils/parse-url-string.d.ts.map +1 -0
  42. package/dist/src/utils/parse-url-string.js +115 -0
  43. package/dist/src/utils/parse-url-string.js.map +1 -0
  44. package/dist/src/utils/responses.d.ts +5 -0
  45. package/dist/src/utils/responses.d.ts.map +1 -0
  46. package/dist/src/utils/responses.js +27 -0
  47. package/dist/src/utils/responses.js.map +1 -0
  48. package/dist/src/utils/select-output-type.d.ts +12 -0
  49. package/dist/src/utils/select-output-type.d.ts.map +1 -0
  50. package/dist/src/utils/select-output-type.js +148 -0
  51. package/dist/src/utils/select-output-type.js.map +1 -0
  52. package/dist/src/utils/tlru.d.ts +15 -0
  53. package/dist/src/utils/tlru.d.ts.map +1 -0
  54. package/dist/src/utils/tlru.js +40 -0
  55. package/dist/src/utils/tlru.js.map +1 -0
  56. package/dist/src/utils/walk-path.d.ts +13 -0
  57. package/dist/src/utils/walk-path.d.ts.map +1 -0
  58. package/dist/src/utils/walk-path.js +17 -0
  59. package/dist/src/utils/walk-path.js.map +1 -0
  60. package/dist/src/verified-fetch.d.ts +59 -0
  61. package/dist/src/verified-fetch.d.ts.map +1 -0
  62. package/dist/src/verified-fetch.js +408 -0
  63. package/dist/src/verified-fetch.js.map +1 -0
  64. package/package.json +197 -0
  65. package/src/index.ts +596 -0
  66. package/src/singleton.ts +20 -0
  67. package/src/types.ts +1 -0
  68. package/src/utils/dag-cbor-to-safe-json.ts +44 -0
  69. package/src/utils/get-content-disposition-filename.ts +18 -0
  70. package/src/utils/get-e-tag.ts +36 -0
  71. package/src/utils/get-stream-from-async-iterable.ts +45 -0
  72. package/src/utils/get-tar-stream.ts +68 -0
  73. package/src/utils/parse-resource.ts +40 -0
  74. package/src/utils/parse-url-string.ts +154 -0
  75. package/src/utils/responses.ts +29 -0
  76. package/src/utils/select-output-type.ts +167 -0
  77. package/src/utils/tlru.ts +52 -0
  78. package/src/utils/walk-path.ts +34 -0
  79. package/src/verified-fetch.ts +509 -0
@@ -0,0 +1,509 @@
1
+ import { car } from '@helia/car'
2
+ import { ipns as heliaIpns, type IPNS } from '@helia/ipns'
3
+ import { dnsJsonOverHttps } from '@helia/ipns/dns-resolvers'
4
+ import { unixfs as heliaUnixFs, type UnixFS as HeliaUnixFs, type UnixFSStats } from '@helia/unixfs'
5
+ import * as ipldDagCbor from '@ipld/dag-cbor'
6
+ import * as ipldDagJson from '@ipld/dag-json'
7
+ import { code as dagPbCode } from '@ipld/dag-pb'
8
+ import { Record as DHTRecord } from '@libp2p/kad-dht'
9
+ import { peerIdFromString } from '@libp2p/peer-id'
10
+ import { Key } from 'interface-datastore'
11
+ import toBrowserReadableStream from 'it-to-browser-readablestream'
12
+ import { code as jsonCode } from 'multiformats/codecs/json'
13
+ import { code as rawCode } from 'multiformats/codecs/raw'
14
+ import { identity } from 'multiformats/hashes/identity'
15
+ import { CustomProgressEvent } from 'progress-events'
16
+ import { concat as uint8ArrayConcat } from 'uint8arrays/concat'
17
+ import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string'
18
+ import { toString as uint8ArrayToString } from 'uint8arrays/to-string'
19
+ import { dagCborToSafeJSON } from './utils/dag-cbor-to-safe-json.js'
20
+ import { getContentDispositionFilename } from './utils/get-content-disposition-filename.js'
21
+ import { getETag } from './utils/get-e-tag.js'
22
+ import { getStreamFromAsyncIterable } from './utils/get-stream-from-async-iterable.js'
23
+ import { tarStream } from './utils/get-tar-stream.js'
24
+ import { parseResource } from './utils/parse-resource.js'
25
+ import { badRequestResponse, notAcceptableResponse, notSupportedResponse, okResponse } from './utils/responses.js'
26
+ import { selectOutputType, queryFormatToAcceptHeader } from './utils/select-output-type.js'
27
+ import { walkPath } from './utils/walk-path.js'
28
+ import type { CIDDetail, ContentTypeParser, Resource, VerifiedFetchInit as VerifiedFetchOptions } from './index.js'
29
+ import type { RequestFormatShorthand } from './types.js'
30
+ import type { Helia } from '@helia/interface'
31
+ import type { AbortOptions, Logger, PeerId } from '@libp2p/interface'
32
+ import type { UnixFSEntry } from 'ipfs-unixfs-exporter'
33
+ import type { CID } from 'multiformats/cid'
34
+
35
+ interface VerifiedFetchComponents {
36
+ helia: Helia
37
+ ipns?: IPNS
38
+ unixfs?: HeliaUnixFs
39
+ }
40
+
41
+ /**
42
+ * Potential future options for the VerifiedFetch constructor.
43
+ */
44
+ interface VerifiedFetchInit {
45
+ contentTypeParser?: ContentTypeParser
46
+ }
47
+
48
+ interface FetchHandlerFunctionArg {
49
+ cid: CID
50
+ path: string
51
+ options?: Omit<VerifiedFetchOptions, 'signal'> & AbortOptions
52
+
53
+ /**
54
+ * If present, the user has sent an accept header with this value - if the
55
+ * content cannot be represented in this format a 406 should be returned
56
+ */
57
+ accept?: string
58
+
59
+ /**
60
+ * The originally requested resource
61
+ */
62
+ resource: string
63
+ }
64
+
65
+ interface FetchHandlerFunction {
66
+ (options: FetchHandlerFunctionArg): Promise<Response>
67
+ }
68
+
69
+ function convertOptions (options?: VerifiedFetchOptions): (Omit<VerifiedFetchOptions, 'signal'> & AbortOptions) | undefined {
70
+ if (options == null) {
71
+ return undefined
72
+ }
73
+
74
+ let signal: AbortSignal | undefined
75
+ if (options?.signal === null) {
76
+ signal = undefined
77
+ }
78
+ return {
79
+ ...options,
80
+ signal
81
+ }
82
+ }
83
+
84
+ /**
85
+ * These are Accept header values that will cause content type sniffing to be
86
+ * skipped and set to these values.
87
+ */
88
+ const RAW_HEADERS = [
89
+ 'application/vnd.ipld.raw',
90
+ 'application/octet-stream'
91
+ ]
92
+
93
+ /**
94
+ * if the user has specified an `Accept` header, and it's in our list of
95
+ * allowable "raw" format headers, use that instead of detecting the content
96
+ * type. This avoids the user from receiving something different when they
97
+ * signal that they want to `Accept` a specific mime type.
98
+ */
99
+ function getOverridenRawContentType (headers?: HeadersInit): string | undefined {
100
+ const acceptHeader = new Headers(headers).get('accept') ?? ''
101
+
102
+ // e.g. "Accept: text/html, application/xhtml+xml, application/xml;q=0.9, image/webp, */*;q=0.8"
103
+ const acceptHeaders = acceptHeader.split(',')
104
+ .map(s => s.split(';')[0])
105
+ .map(s => s.trim())
106
+
107
+ for (const mimeType of acceptHeaders) {
108
+ if (mimeType === '*/*') {
109
+ return
110
+ }
111
+
112
+ if (RAW_HEADERS.includes(mimeType ?? '')) {
113
+ return mimeType
114
+ }
115
+ }
116
+ }
117
+
118
+ export class VerifiedFetch {
119
+ private readonly helia: Helia
120
+ private readonly ipns: IPNS
121
+ private readonly unixfs: HeliaUnixFs
122
+ private readonly log: Logger
123
+ private readonly contentTypeParser: ContentTypeParser | undefined
124
+
125
+ constructor ({ helia, ipns, unixfs }: VerifiedFetchComponents, init?: VerifiedFetchInit) {
126
+ this.helia = helia
127
+ this.log = helia.logger.forComponent('helia:verified-fetch')
128
+ this.ipns = ipns ?? heliaIpns(helia, {
129
+ resolvers: [
130
+ dnsJsonOverHttps('https://mozilla.cloudflare-dns.com/dns-query'),
131
+ dnsJsonOverHttps('https://dns.google/resolve')
132
+ ]
133
+ })
134
+ this.unixfs = unixfs ?? heliaUnixFs(helia)
135
+ this.contentTypeParser = init?.contentTypeParser
136
+ this.log.trace('created VerifiedFetch instance')
137
+ }
138
+
139
+ /**
140
+ * Accepts an `ipns://...` URL as a string and returns a `Response` containing
141
+ * a raw IPNS record.
142
+ */
143
+ private async handleIPNSRecord ({ resource, cid, path, options }: FetchHandlerFunctionArg): Promise<Response> {
144
+ if (path !== '' || !resource.startsWith('ipns://')) {
145
+ return badRequestResponse('Invalid IPNS name')
146
+ }
147
+
148
+ let peerId: PeerId
149
+
150
+ try {
151
+ peerId = peerIdFromString(resource.replace('ipns://', ''))
152
+ } catch (err) {
153
+ this.log.error('could not parse peer id from IPNS url %s', resource)
154
+
155
+ return badRequestResponse('Invalid IPNS name')
156
+ }
157
+
158
+ // since this call happens after parseResource, we've already resolved the
159
+ // IPNS name so a local copy should be in the helia datastore, so we can
160
+ // just read it out..
161
+ const routingKey = uint8ArrayConcat([
162
+ uint8ArrayFromString('/ipns/'),
163
+ peerId.toBytes()
164
+ ])
165
+ const datastoreKey = new Key('/dht/record/' + uint8ArrayToString(routingKey, 'base32'), false)
166
+ const buf = await this.helia.datastore.get(datastoreKey, options)
167
+ const record = DHTRecord.deserialize(buf)
168
+
169
+ const response = okResponse(record.value)
170
+ response.headers.set('content-type', 'application/vnd.ipfs.ipns-record')
171
+
172
+ return response
173
+ }
174
+
175
+ /**
176
+ * Accepts a `CID` and returns a `Response` with a body stream that is a CAR
177
+ * of the `DAG` referenced by the `CID`.
178
+ */
179
+ private async handleCar ({ cid, options }: FetchHandlerFunctionArg): Promise<Response> {
180
+ const c = car(this.helia)
181
+ const stream = toBrowserReadableStream(c.stream(cid, options))
182
+
183
+ const response = okResponse(stream)
184
+ response.headers.set('content-type', 'application/vnd.ipld.car; version=1')
185
+
186
+ return response
187
+ }
188
+
189
+ /**
190
+ * Accepts a UnixFS `CID` and returns a `.tar` file containing the file or
191
+ * directory structure referenced by the `CID`.
192
+ */
193
+ private async handleTar ({ cid, path, options }: FetchHandlerFunctionArg): Promise<Response> {
194
+ if (cid.code !== dagPbCode && cid.code !== rawCode) {
195
+ return notAcceptableResponse('only UnixFS data can be returned in a TAR file')
196
+ }
197
+
198
+ const stream = toBrowserReadableStream<Uint8Array>(tarStream(`/ipfs/${cid}/${path}`, this.helia.blockstore, options))
199
+
200
+ const response = okResponse(stream)
201
+ response.headers.set('content-type', 'application/x-tar')
202
+
203
+ return response
204
+ }
205
+
206
+ private async handleJson ({ cid, path, accept, options }: FetchHandlerFunctionArg): Promise<Response> {
207
+ this.log.trace('fetching %c/%s', cid, path)
208
+ const block = await this.helia.blockstore.get(cid, options)
209
+ let body: string | Uint8Array
210
+
211
+ if (accept === 'application/vnd.ipld.dag-cbor' || accept === 'application/cbor') {
212
+ try {
213
+ // if vnd.ipld.dag-cbor has been specified, convert to the format - note
214
+ // that this supports more data types than regular JSON, the content-type
215
+ // response header is set so the user knows to process it differently
216
+ const obj = ipldDagJson.decode(block)
217
+ body = ipldDagCbor.encode(obj)
218
+ } catch (err) {
219
+ this.log.error('could not transform %c to application/vnd.ipld.dag-cbor', err)
220
+ return notAcceptableResponse()
221
+ }
222
+ } else {
223
+ // skip decoding
224
+ body = block
225
+ }
226
+
227
+ const response = okResponse(body)
228
+ response.headers.set('content-type', accept ?? 'application/json')
229
+ return response
230
+ }
231
+
232
+ private async handleDagCbor ({ cid, path, accept, options }: FetchHandlerFunctionArg): Promise<Response> {
233
+ this.log.trace('fetching %c/%s', cid, path)
234
+
235
+ const block = await this.helia.blockstore.get(cid, options)
236
+ let body: string | Uint8Array
237
+
238
+ if (accept === 'application/octet-stream' || accept === 'application/vnd.ipld.dag-cbor' || accept === 'application/cbor') {
239
+ // skip decoding
240
+ body = block
241
+ } else if (accept === 'application/vnd.ipld.dag-json') {
242
+ try {
243
+ // if vnd.ipld.dag-json has been specified, convert to the format - note
244
+ // that this supports more data types than regular JSON, the content-type
245
+ // response header is set so the user knows to process it differently
246
+ const obj = ipldDagCbor.decode(block)
247
+ body = ipldDagJson.encode(obj)
248
+ } catch (err) {
249
+ this.log.error('could not transform %c to application/vnd.ipld.dag-json', err)
250
+ return notAcceptableResponse()
251
+ }
252
+ } else {
253
+ try {
254
+ body = dagCborToSafeJSON(block)
255
+ } catch (err) {
256
+ if (accept === 'application/json') {
257
+ this.log('could not decode DAG-CBOR as JSON-safe, but the client sent "Accept: application/json"', err)
258
+
259
+ return notAcceptableResponse()
260
+ }
261
+
262
+ this.log('could not decode DAG-CBOR as JSON-safe, falling back to `application/octet-stream`', err)
263
+ body = block
264
+ }
265
+ }
266
+
267
+ const response = okResponse(body)
268
+
269
+ if (accept == null) {
270
+ accept = body instanceof Uint8Array ? 'application/octet-stream' : 'application/json'
271
+ }
272
+
273
+ response.headers.set('content-type', accept)
274
+
275
+ return response
276
+ }
277
+
278
+ private async handleDagPb ({ cid, path, options }: FetchHandlerFunctionArg): Promise<Response> {
279
+ let terminalElement: UnixFSEntry | undefined
280
+ let ipfsRoots: CID[] | undefined
281
+
282
+ try {
283
+ const pathDetails = await walkPath(this.helia.blockstore, `${cid.toString()}/${path}`, options)
284
+ ipfsRoots = pathDetails.ipfsRoots
285
+ terminalElement = pathDetails.terminalElement
286
+ } catch (err) {
287
+ this.log.error('Error walking path %s', path, err)
288
+ }
289
+
290
+ let resolvedCID = terminalElement?.cid ?? cid
291
+ let stat: UnixFSStats
292
+ if (terminalElement?.type === 'directory') {
293
+ const dirCid = terminalElement.cid
294
+
295
+ const rootFilePath = 'index.html'
296
+ try {
297
+ this.log.trace('found directory at %c/%s, looking for index.html', cid, path)
298
+ stat = await this.unixfs.stat(dirCid, {
299
+ path: rootFilePath,
300
+ signal: options?.signal,
301
+ onProgress: options?.onProgress
302
+ })
303
+ this.log.trace('found root file at %c/%s with cid %c', dirCid, rootFilePath, stat.cid)
304
+ path = rootFilePath
305
+ resolvedCID = stat.cid
306
+ // terminalElement = stat
307
+ } catch (err: any) {
308
+ this.log('error loading path %c/%s', dirCid, rootFilePath, err)
309
+ return notSupportedResponse('Unable to find index.html for directory at given path. Support for directories with implicit root is not implemented')
310
+ } finally {
311
+ options?.onProgress?.(new CustomProgressEvent<CIDDetail>('verified-fetch:request:end', { cid: dirCid, path: rootFilePath }))
312
+ }
313
+ }
314
+
315
+ const asyncIter = this.unixfs.cat(resolvedCID, {
316
+ signal: options?.signal,
317
+ onProgress: options?.onProgress
318
+ })
319
+ this.log('got async iterator for %c/%s', cid, path)
320
+
321
+ const { stream, firstChunk } = await getStreamFromAsyncIterable(asyncIter, path ?? '', this.helia.logger, {
322
+ onProgress: options?.onProgress
323
+ })
324
+ const response = okResponse(stream)
325
+ await this.setContentType(firstChunk, path, response)
326
+
327
+ if (ipfsRoots != null) {
328
+ response.headers.set('X-Ipfs-Roots', ipfsRoots.map(cid => cid.toV1().toString()).join(',')) // https://specs.ipfs.tech/http-gateways/path-gateway/#x-ipfs-roots-response-header
329
+ }
330
+
331
+ return response
332
+ }
333
+
334
+ private async handleRaw ({ cid, path, options }: FetchHandlerFunctionArg): Promise<Response> {
335
+ const result = await this.helia.blockstore.get(cid, options)
336
+ const response = okResponse(result)
337
+
338
+ // if the user has specified an `Accept` header that corresponds to a raw
339
+ // type, honour that header, so for example they don't request
340
+ // `application/vnd.ipld.raw` but get `application/octet-stream`
341
+ const overriddenContentType = getOverridenRawContentType(options?.headers)
342
+ if (overriddenContentType != null) {
343
+ response.headers.set('content-type', overriddenContentType)
344
+ } else {
345
+ await this.setContentType(result, path, response)
346
+ }
347
+
348
+ return response
349
+ }
350
+
351
+ private async setContentType (bytes: Uint8Array, path: string, response: Response): Promise<void> {
352
+ let contentType = 'application/octet-stream'
353
+
354
+ if (this.contentTypeParser != null) {
355
+ try {
356
+ let fileName = path.split('/').pop()?.trim()
357
+ fileName = fileName === '' ? undefined : fileName
358
+ const parsed = this.contentTypeParser(bytes, fileName)
359
+
360
+ if (isPromise(parsed)) {
361
+ const result = await parsed
362
+
363
+ if (result != null) {
364
+ contentType = result
365
+ }
366
+ } else if (parsed != null) {
367
+ contentType = parsed
368
+ }
369
+ } catch (err) {
370
+ this.log.error('Error parsing content type', err)
371
+ }
372
+ }
373
+
374
+ response.headers.set('content-type', contentType)
375
+ }
376
+
377
+ /**
378
+ * If the user has not specified an Accept header or format query string arg,
379
+ * use the CID codec to choose an appropriate handler for the block data.
380
+ */
381
+ private readonly codecHandlers: Record<number, FetchHandlerFunction> = {
382
+ [dagPbCode]: this.handleDagPb,
383
+ [ipldDagJson.code]: this.handleJson,
384
+ [jsonCode]: this.handleJson,
385
+ [ipldDagCbor.code]: this.handleDagCbor,
386
+ [rawCode]: this.handleRaw,
387
+ [identity.code]: this.handleRaw
388
+ }
389
+
390
+ async fetch (resource: Resource, opts?: VerifiedFetchOptions): Promise<Response> {
391
+ this.log('fetch %s', resource)
392
+
393
+ const options = convertOptions(opts)
394
+
395
+ options?.onProgress?.(new CustomProgressEvent<CIDDetail>('verified-fetch:request:start', { resource }))
396
+
397
+ // resolve the CID/path from the requested resource
398
+ const { path, query, cid } = await parseResource(resource, { ipns: this.ipns, logger: this.helia.logger }, options)
399
+
400
+ options?.onProgress?.(new CustomProgressEvent<CIDDetail>('verified-fetch:request:resolve', { cid, path }))
401
+
402
+ const requestHeaders = new Headers(options?.headers)
403
+ const incomingAcceptHeader = requestHeaders.get('accept')
404
+
405
+ if (incomingAcceptHeader != null) {
406
+ this.log('incoming accept header "%s"', incomingAcceptHeader)
407
+ }
408
+
409
+ const queryFormatMapping = queryFormatToAcceptHeader(query.format)
410
+
411
+ if (query.format != null) {
412
+ this.log('incoming query format "%s", mapped to %s', query.format, queryFormatMapping)
413
+ }
414
+
415
+ const acceptHeader = incomingAcceptHeader ?? queryFormatMapping
416
+ const accept = selectOutputType(cid, acceptHeader)
417
+ this.log('output type %s', accept)
418
+
419
+ if (acceptHeader != null && accept == null) {
420
+ return notAcceptableResponse()
421
+ }
422
+
423
+ let response: Response
424
+ let reqFormat: RequestFormatShorthand | undefined
425
+
426
+ const handlerArgs = { resource: resource.toString(), cid, path, accept, options }
427
+
428
+ if (accept === 'application/vnd.ipfs.ipns-record') {
429
+ // the user requested a raw IPNS record
430
+ reqFormat = 'ipns-record'
431
+ response = await this.handleIPNSRecord(handlerArgs)
432
+ } else if (accept === 'application/vnd.ipld.car') {
433
+ // the user requested a CAR file
434
+ reqFormat = 'car'
435
+ query.download = true
436
+ query.filename = query.filename ?? `${cid.toString()}.car`
437
+ response = await this.handleCar(handlerArgs)
438
+ } else if (accept === 'application/vnd.ipld.raw') {
439
+ // the user requested a raw block
440
+ reqFormat = 'raw'
441
+ query.download = true
442
+ query.filename = query.filename ?? `${cid.toString()}.bin`
443
+ response = await this.handleRaw(handlerArgs)
444
+ } else if (accept === 'application/x-tar') {
445
+ // the user requested a TAR file
446
+ reqFormat = 'tar'
447
+ query.download = true
448
+ query.filename = query.filename ?? `${cid.toString()}.tar`
449
+ response = await this.handleTar(handlerArgs)
450
+ } else {
451
+ // derive the handler from the CID type
452
+ const codecHandler = this.codecHandlers[cid.code]
453
+
454
+ if (codecHandler == null) {
455
+ return notSupportedResponse(`Support for codec with code ${cid.code} is not yet implemented. Please open an issue at https://github.com/ipfs/helia/issues/new`)
456
+ }
457
+
458
+ response = await codecHandler.call(this, handlerArgs)
459
+ }
460
+
461
+ response.headers.set('etag', getETag({ cid, reqFormat, weak: false }))
462
+ response.headers.set('cache-control', 'public, max-age=29030400, immutable')
463
+ // https://specs.ipfs.tech/http-gateways/path-gateway/#x-ipfs-path-response-header
464
+ response.headers.set('X-Ipfs-Path', resource.toString())
465
+
466
+ // set Content-Disposition header
467
+ let contentDisposition: string | undefined
468
+
469
+ // force download if requested
470
+ if (query.download === true) {
471
+ contentDisposition = 'attachment'
472
+ }
473
+
474
+ // override filename if requested
475
+ if (query.filename != null) {
476
+ if (contentDisposition == null) {
477
+ contentDisposition = 'inline'
478
+ }
479
+
480
+ contentDisposition = `${contentDisposition}; ${getContentDispositionFilename(query.filename)}`
481
+ }
482
+
483
+ if (contentDisposition != null) {
484
+ response.headers.set('Content-Disposition', contentDisposition)
485
+ }
486
+
487
+ options?.onProgress?.(new CustomProgressEvent<CIDDetail>('verified-fetch:request:end', { cid, path }))
488
+
489
+ return response
490
+ }
491
+
492
+ /**
493
+ * Start the Helia instance
494
+ */
495
+ async start (): Promise<void> {
496
+ await this.helia.start()
497
+ }
498
+
499
+ /**
500
+ * Shut down the Helia instance
501
+ */
502
+ async stop (): Promise<void> {
503
+ await this.helia.stop()
504
+ }
505
+ }
506
+
507
+ function isPromise <T> (p?: any): p is Promise<T> {
508
+ return p?.then != null
509
+ }