@helia/verified-fetch 0.0.0-1ee6a4a

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