@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.
- package/dist/index.min.js +8 -8
- package/dist/src/types.d.ts +23 -0
- package/dist/src/types.d.ts.map +1 -1
- package/dist/src/types.js +2 -1
- package/dist/src/types.js.map +1 -1
- package/dist/src/utils/handle-redirects.d.ts +16 -0
- package/dist/src/utils/handle-redirects.d.ts.map +1 -0
- package/dist/src/utils/handle-redirects.js +85 -0
- package/dist/src/utils/handle-redirects.js.map +1 -0
- package/dist/src/utils/parse-url-string.js +1 -1
- package/dist/src/utils/parse-url-string.js.map +1 -1
- package/dist/src/utils/response-headers.d.ts +7 -0
- package/dist/src/utils/response-headers.d.ts.map +1 -1
- package/dist/src/utils/response-headers.js +10 -0
- package/dist/src/utils/response-headers.js.map +1 -1
- package/dist/src/utils/walk-path.d.ts +13 -0
- package/dist/src/utils/walk-path.d.ts.map +1 -1
- package/dist/src/utils/walk-path.js +22 -0
- package/dist/src/utils/walk-path.js.map +1 -1
- package/dist/src/verified-fetch.d.ts.map +1 -1
- package/dist/src/verified-fetch.js +53 -58
- package/dist/src/verified-fetch.js.map +1 -1
- package/dist/typedoc-urls.json +25 -25
- package/package.json +22 -22
- package/src/types.ts +29 -0
- package/src/utils/handle-redirects.ts +101 -0
- package/src/utils/parse-url-string.ts +1 -1
- package/src/utils/response-headers.ts +13 -0
- package/src/utils/walk-path.ts +24 -1
- package/src/verified-fetch.ts +58 -94
package/src/verified-fetch.ts
CHANGED
|
@@ -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 {
|
|
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
|
|
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,
|
|
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,
|
|
215
|
-
const blockstore = this.getBlockstore(cid,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
|
273
|
-
|
|
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
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
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
|
-
|
|
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,
|
|
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,
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
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,
|
|
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,
|
|
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
|
|
563
|
-
|
|
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
|