@helia/verified-fetch 1.4.1 → 1.4.3

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.
@@ -24,18 +24,19 @@ import { getETag } from './utils/get-e-tag.js'
24
24
  import { getResolvedAcceptHeader } from './utils/get-resolved-accept-header.js'
25
25
  import { getStreamFromAsyncIterable } from './utils/get-stream-from-async-iterable.js'
26
26
  import { tarStream } from './utils/get-tar-stream.js'
27
+ import { getRedirectResponse } from './utils/handle-redirects.js'
27
28
  import { parseResource } from './utils/parse-resource.js'
29
+ import { type ParsedUrlStringResults } from './utils/parse-url-string.js'
28
30
  import { resourceToSessionCacheKey } from './utils/resource-to-cache-key.js'
29
- import { setCacheControlHeader } from './utils/response-headers.js'
31
+ import { setCacheControlHeader, setIpfsRoots } from './utils/response-headers.js'
30
32
  import { badRequestResponse, movedPermanentlyResponse, notAcceptableResponse, notSupportedResponse, okResponse, badRangeResponse, okRangeResponse, badGatewayResponse, notFoundResponse } from './utils/responses.js'
31
33
  import { selectOutputType } from './utils/select-output-type.js'
32
- import { isObjectNode, walkPath } from './utils/walk-path.js'
34
+ import { handlePathWalking, isObjectNode } from './utils/walk-path.js'
33
35
  import type { CIDDetail, ContentTypeParser, CreateVerifiedFetchOptions, Resource, VerifiedFetchInit as VerifiedFetchOptions } from './index.js'
34
- import type { RequestFormatShorthand } from './types.js'
35
- import type { ParsedUrlStringResults } from './utils/parse-url-string'
36
+ import type { FetchHandlerFunctionArg, RequestFormatShorthand } from './types.js'
36
37
  import type { Helia, SessionBlockstore } from '@helia/interface'
37
38
  import type { Blockstore } from 'interface-blockstore'
38
- import type { ObjectNode, UnixFSEntry } from 'ipfs-unixfs-exporter'
39
+ import type { ObjectNode } from 'ipfs-unixfs-exporter'
39
40
  import type { CID } from 'multiformats/cid'
40
41
 
41
42
  const SESSION_CACHE_MAX_SIZE = 100
@@ -46,36 +47,6 @@ interface VerifiedFetchComponents {
46
47
  ipns?: IPNS
47
48
  }
48
49
 
49
- interface FetchHandlerFunctionArg {
50
- cid: CID
51
- path: string
52
-
53
- /**
54
- * A key for use with the blockstore session cache
55
- */
56
- cacheKey: string
57
-
58
- /**
59
- * Whether to use a session during fetch operations
60
- *
61
- * @default true
62
- */
63
- session: boolean
64
-
65
- options?: Omit<VerifiedFetchOptions, 'signal'> & AbortOptions
66
-
67
- /**
68
- * If present, the user has sent an accept header with this value - if the
69
- * content cannot be represented in this format a 406 should be returned
70
- */
71
- accept?: string
72
-
73
- /**
74
- * The originally requested resource
75
- */
76
- resource: string
77
- }
78
-
79
50
  interface FetchHandlerFunction {
80
51
  (options: FetchHandlerFunctionArg): Promise<Response>
81
52
  }
@@ -156,7 +127,8 @@ export class VerifiedFetch {
156
127
  this.log.trace('created VerifiedFetch instance')
157
128
  }
158
129
 
159
- private getBlockstore (root: CID, key: string, useSession: boolean, options?: AbortOptions): Blockstore {
130
+ private getBlockstore (root: CID, resource: string | CID, useSession: boolean, options?: AbortOptions): Blockstore {
131
+ const key = resourceToSessionCacheKey(resource)
160
132
  if (!useSession) {
161
133
  return this.helia.blockstore
162
134
  }
@@ -211,8 +183,8 @@ export class VerifiedFetch {
211
183
  * Accepts a `CID` and returns a `Response` with a body stream that is a CAR
212
184
  * of the `DAG` referenced by the `CID`.
213
185
  */
214
- private async handleCar ({ resource, cid, session, cacheKey, options }: FetchHandlerFunctionArg): Promise<Response> {
215
- const blockstore = this.getBlockstore(cid, cacheKey, session, options)
186
+ private async handleCar ({ resource, cid, session, options }: FetchHandlerFunctionArg): Promise<Response> {
187
+ const blockstore = this.getBlockstore(cid, resource, session, options)
216
188
  const c = car({ blockstore, dagWalkers: this.helia.dagWalkers })
217
189
  const stream = toBrowserReadableStream(c.stream(cid, options))
218
190
 
@@ -226,12 +198,12 @@ export class VerifiedFetch {
226
198
  * Accepts a UnixFS `CID` and returns a `.tar` file containing the file or
227
199
  * directory structure referenced by the `CID`.
228
200
  */
229
- private async handleTar ({ resource, cid, path, session, cacheKey, options }: FetchHandlerFunctionArg): Promise<Response> {
201
+ private async handleTar ({ resource, cid, path, session, options }: FetchHandlerFunctionArg): Promise<Response> {
230
202
  if (cid.code !== dagPbCode && cid.code !== rawCode) {
231
203
  return notAcceptableResponse('only UnixFS data can be returned in a TAR file')
232
204
  }
233
205
 
234
- const blockstore = this.getBlockstore(cid, cacheKey, session, options)
206
+ const blockstore = this.getBlockstore(cid, resource, session, options)
235
207
  const stream = toBrowserReadableStream<Uint8Array>(tarStream(`/ipfs/${cid}/${path}`, blockstore, options))
236
208
 
237
209
  const response = okResponse(resource, stream)
@@ -240,9 +212,9 @@ export class VerifiedFetch {
240
212
  return response
241
213
  }
242
214
 
243
- private async handleJson ({ resource, cid, path, accept, session, cacheKey, options }: FetchHandlerFunctionArg): Promise<Response> {
215
+ private async handleJson ({ resource, cid, path, accept, session, options }: FetchHandlerFunctionArg): Promise<Response> {
244
216
  this.log.trace('fetching %c/%s', cid, path)
245
- const blockstore = this.getBlockstore(cid, cacheKey, session, options)
217
+ const blockstore = this.getBlockstore(cid, resource, session, options)
246
218
  const block = await blockstore.get(cid, options)
247
219
  let body: string | Uint8Array
248
220
 
@@ -267,33 +239,26 @@ export class VerifiedFetch {
267
239
  return response
268
240
  }
269
241
 
270
- private async handleDagCbor ({ resource, cid, path, accept, session, cacheKey, options }: FetchHandlerFunctionArg): Promise<Response> {
242
+ private async handleDagCbor ({ resource, cid, path, accept, session, options }: FetchHandlerFunctionArg): Promise<Response> {
271
243
  this.log.trace('fetching %c/%s', cid, path)
272
- let terminalElement: ObjectNode | undefined
273
- let ipfsRoots: CID[] | undefined
274
- const blockstore = this.getBlockstore(cid, cacheKey, session, options)
244
+ let terminalElement: ObjectNode
245
+ const blockstore = this.getBlockstore(cid, resource, session, options)
275
246
 
276
247
  // need to walk path, if it exists, to get the terminal element
277
- try {
278
- const pathDetails = await walkPath(blockstore, `${cid.toString()}/${path}`, options)
279
- ipfsRoots = pathDetails.ipfsRoots
280
- const potentialTerminalElement = pathDetails.terminalElement
281
- if (potentialTerminalElement == null) {
282
- return notFoundResponse(resource)
283
- }
284
- if (isObjectNode(potentialTerminalElement)) {
285
- terminalElement = potentialTerminalElement
286
- }
287
- } catch (err: any) {
288
- options?.signal?.throwIfAborted()
289
- if (['ERR_NO_PROP', 'ERR_NO_TERMINAL_ELEMENT'].includes(err.code)) {
290
- return notFoundResponse(resource)
291
- }
292
-
293
- this.log.error('error walking path %s', path, err)
294
- return badGatewayResponse(resource, 'Error walking path')
248
+ const pathDetails = await handlePathWalking({ cid, path, resource, options, blockstore, log: this.log })
249
+ if (pathDetails instanceof Response) {
250
+ return pathDetails
251
+ }
252
+ const ipfsRoots = pathDetails.ipfsRoots
253
+ if (isObjectNode(pathDetails.terminalElement)) {
254
+ terminalElement = pathDetails.terminalElement
255
+ } else {
256
+ // this should never happen, but if it does, we should log it and return notSupportedResponse
257
+ this.log.error('terminal element is not a dag-cbor node')
258
+ return notSupportedResponse(resource, 'Terminal element is not a dag-cbor node')
295
259
  }
296
- const block = terminalElement?.node ?? await blockstore.get(cid, options)
260
+
261
+ const block = terminalElement.node
297
262
 
298
263
  let body: string | Uint8Array
299
264
 
@@ -333,36 +298,23 @@ export class VerifiedFetch {
333
298
  }
334
299
 
335
300
  response.headers.set('content-type', accept)
336
-
337
- if (ipfsRoots != null) {
338
- 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
339
- }
301
+ setIpfsRoots(response, ipfsRoots)
340
302
 
341
303
  return response
342
304
  }
343
305
 
344
- private async handleDagPb ({ cid, path, resource, cacheKey, session, options }: FetchHandlerFunctionArg): Promise<Response> {
345
- let terminalElement: UnixFSEntry | undefined
346
- let ipfsRoots: CID[] | undefined
306
+ private async handleDagPb ({ cid, path, resource, session, options }: FetchHandlerFunctionArg): Promise<Response> {
347
307
  let redirected = false
348
308
  const byteRangeContext = new ByteRangeContext(this.helia.logger, options?.headers)
349
- const blockstore = this.getBlockstore(cid, cacheKey, session, options)
350
-
351
- try {
352
- const pathDetails = await walkPath(blockstore, `${cid.toString()}/${path}`, options)
353
- ipfsRoots = pathDetails.ipfsRoots
354
- terminalElement = pathDetails.terminalElement
355
- } catch (err: any) {
356
- options?.signal?.throwIfAborted()
357
- if (['ERR_NO_PROP', 'ERR_NO_TERMINAL_ELEMENT', 'ERR_NOT_FOUND'].includes(err.code)) {
358
- return notFoundResponse(resource.toString())
359
- }
360
- this.log.error('error walking path %s', path, err)
361
-
362
- return badGatewayResponse(resource.toString(), 'Error walking path')
309
+ const blockstore = this.getBlockstore(cid, resource, session, options)
310
+ const pathDetails = await handlePathWalking({ cid, path, resource, options, blockstore, log: this.log })
311
+ if (pathDetails instanceof Response) {
312
+ return pathDetails
363
313
  }
314
+ const ipfsRoots = pathDetails.ipfsRoots
315
+ const terminalElement = pathDetails.terminalElement
316
+ let resolvedCID = terminalElement.cid
364
317
 
365
- let resolvedCID = terminalElement?.cid ?? cid
366
318
  if (terminalElement?.type === 'directory') {
367
319
  const dirCid = terminalElement.cid
368
320
  const redirectCheckNeeded = path === '' ? !resource.toString().endsWith('/') : !path.endsWith('/')
@@ -438,10 +390,8 @@ export class VerifiedFetch {
438
390
  })
439
391
 
440
392
  await this.setContentType(firstChunk, path, response)
393
+ setIpfsRoots(response, ipfsRoots)
441
394
 
442
- if (ipfsRoots != null) {
443
- 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
444
- }
445
395
  return response
446
396
  } catch (err: any) {
447
397
  options?.signal?.throwIfAborted()
@@ -453,9 +403,19 @@ export class VerifiedFetch {
453
403
  }
454
404
  }
455
405
 
456
- private async handleRaw ({ resource, cid, path, session, cacheKey, options, accept }: FetchHandlerFunctionArg): Promise<Response> {
406
+ private async handleRaw ({ resource, cid, path, session, options, accept }: FetchHandlerFunctionArg): Promise<Response> {
407
+ /**
408
+ * if we have a path, we can't walk it, so we need to return a 404.
409
+ *
410
+ * @see https://github.com/ipfs/gateway-conformance/blob/26994cfb056b717a23bf694ce4e94386728748dd/tests/subdomain_gateway_ipfs_test.go#L198-L204
411
+ */
412
+ if (path !== '') {
413
+ this.log.trace('404-ing raw codec request for %c/%s', cid, path)
414
+ return notFoundResponse(resource, 'Raw codec does not support paths')
415
+ }
416
+
457
417
  const byteRangeContext = new ByteRangeContext(this.helia.logger, options?.headers)
458
- const blockstore = this.getBlockstore(cid, cacheKey, session, options)
418
+ const blockstore = this.getBlockstore(cid, resource, session, options)
459
419
  const result = await blockstore.get(cid, options)
460
420
  byteRangeContext.setBody(result)
461
421
  const response = okRangeResponse(resource, byteRangeContext.getBody(), { byteRangeContext, log: this.log }, {
@@ -559,8 +519,12 @@ export class VerifiedFetch {
559
519
  let response: Response
560
520
  let reqFormat: RequestFormatShorthand | undefined
561
521
 
562
- const cacheKey = resourceToSessionCacheKey(resource)
563
- const handlerArgs: FetchHandlerFunctionArg = { resource: resource.toString(), cid, path, accept, cacheKey, session: options?.session ?? true, options }
522
+ const redirectResponse = await getRedirectResponse({ resource, options, logger: this.helia.logger, cid })
523
+ if (redirectResponse != null) {
524
+ return redirectResponse
525
+ }
526
+
527
+ const handlerArgs: FetchHandlerFunctionArg = { resource: resource.toString(), cid, path, accept, session: options?.session ?? true, options }
564
528
 
565
529
  if (accept === 'application/vnd.ipfs.ipns-record') {
566
530
  // the user requested a raw IPNS record