@helia/verified-fetch 1.3.5 → 1.3.7
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 +7 -7
- package/dist/src/utils/get-resolved-accept-header.d.ts +9 -0
- package/dist/src/utils/get-resolved-accept-header.d.ts.map +1 -0
- package/dist/src/utils/get-resolved-accept-header.js +27 -0
- package/dist/src/utils/get-resolved-accept-header.js.map +1 -0
- package/dist/src/utils/is-accept-explicit.d.ts +13 -0
- package/dist/src/utils/is-accept-explicit.d.ts.map +1 -0
- package/dist/src/utils/is-accept-explicit.js +23 -0
- package/dist/src/utils/is-accept-explicit.js.map +1 -0
- package/dist/src/utils/responses.d.ts +1 -0
- package/dist/src/utils/responses.d.ts.map +1 -1
- package/dist/src/utils/responses.js +10 -0
- package/dist/src/utils/responses.js.map +1 -1
- package/dist/src/utils/select-output-type.d.ts +1 -0
- package/dist/src/utils/select-output-type.d.ts.map +1 -1
- package/dist/src/utils/select-output-type.js +2 -1
- package/dist/src/utils/select-output-type.js.map +1 -1
- package/dist/src/utils/walk-path.d.ts +2 -1
- package/dist/src/utils/walk-path.d.ts.map +1 -1
- package/dist/src/utils/walk-path.js +5 -1
- 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 +44 -31
- package/dist/src/verified-fetch.js.map +1 -1
- package/package.json +1 -1
- package/src/utils/get-resolved-accept-header.ts +42 -0
- package/src/utils/is-accept-explicit.ts +31 -0
- package/src/utils/responses.ts +13 -0
- package/src/utils/select-output-type.ts +2 -1
- package/src/utils/walk-path.ts +7 -2
- package/src/verified-fetch.ts +48 -36
package/src/utils/responses.ts
CHANGED
|
@@ -85,6 +85,19 @@ export function notAcceptableResponse (url: string, body?: SupportedBodyTypes, i
|
|
|
85
85
|
return response
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
+
export function notFoundResponse (url: string, body?: SupportedBodyTypes, init?: ResponseInit): Response {
|
|
89
|
+
const response = new Response(body, {
|
|
90
|
+
...(init ?? {}),
|
|
91
|
+
status: 404,
|
|
92
|
+
statusText: 'Not Found'
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
setType(response, 'basic')
|
|
96
|
+
setUrl(response, url)
|
|
97
|
+
|
|
98
|
+
return response
|
|
99
|
+
}
|
|
100
|
+
|
|
88
101
|
/**
|
|
89
102
|
* if body is an Error, it will be converted to a string containing the error message.
|
|
90
103
|
*/
|
|
@@ -55,6 +55,7 @@ const CID_TYPE_MAP: Record<number, string[]> = {
|
|
|
55
55
|
'application/octet-stream',
|
|
56
56
|
'application/vnd.ipld.raw',
|
|
57
57
|
'application/vnd.ipfs.ipns-record',
|
|
58
|
+
'application/vnd.ipld.dag-json',
|
|
58
59
|
'application/vnd.ipld.car',
|
|
59
60
|
'application/x-tar'
|
|
60
61
|
]
|
|
@@ -145,7 +146,7 @@ function parseQFactor (str?: string): number {
|
|
|
145
146
|
return factor
|
|
146
147
|
}
|
|
147
148
|
|
|
148
|
-
const FORMAT_TO_MIME_TYPE: Record<RequestFormatShorthand, string> = {
|
|
149
|
+
export const FORMAT_TO_MIME_TYPE: Record<RequestFormatShorthand, string> = {
|
|
149
150
|
raw: 'application/vnd.ipld.raw',
|
|
150
151
|
car: 'application/vnd.ipld.car',
|
|
151
152
|
'dag-json': 'application/vnd.ipld.dag-json',
|
package/src/utils/walk-path.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { CodeError } from '@libp2p/interface'
|
|
2
|
+
import { walkPath as exporterWalk, type ExporterOptions, type ReadableStorage, type ObjectNode, type UnixFSEntry } from 'ipfs-unixfs-exporter'
|
|
2
3
|
import type { CID } from 'multiformats/cid'
|
|
3
4
|
|
|
4
5
|
export interface PathWalkerOptions extends ExporterOptions {
|
|
@@ -24,7 +25,7 @@ export async function walkPath (blockstore: ReadableStorage, path: string, optio
|
|
|
24
25
|
}
|
|
25
26
|
|
|
26
27
|
if (terminalElement == null) {
|
|
27
|
-
throw new
|
|
28
|
+
throw new CodeError('No terminal element found', 'ERR_NO_TERMINAL_ELEMENT')
|
|
28
29
|
}
|
|
29
30
|
|
|
30
31
|
return {
|
|
@@ -32,3 +33,7 @@ export async function walkPath (blockstore: ReadableStorage, path: string, optio
|
|
|
32
33
|
terminalElement
|
|
33
34
|
}
|
|
34
35
|
}
|
|
36
|
+
|
|
37
|
+
export function isObjectNode (node: UnixFSEntry): node is ObjectNode {
|
|
38
|
+
return node.type === 'object'
|
|
39
|
+
}
|
package/src/verified-fetch.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { unixfs as heliaUnixFs, type UnixFS as HeliaUnixFs } from '@helia/unixfs
|
|
|
4
4
|
import * as ipldDagCbor from '@ipld/dag-cbor'
|
|
5
5
|
import * as ipldDagJson from '@ipld/dag-json'
|
|
6
6
|
import { code as dagPbCode } from '@ipld/dag-pb'
|
|
7
|
-
import {
|
|
7
|
+
import { type AbortOptions, type Logger, type PeerId } from '@libp2p/interface'
|
|
8
8
|
import { Record as DHTRecord } from '@libp2p/kad-dht'
|
|
9
9
|
import { peerIdFromString } from '@libp2p/peer-id'
|
|
10
10
|
import { Key } from 'interface-datastore'
|
|
@@ -20,19 +20,20 @@ import { ByteRangeContext } from './utils/byte-range-context.js'
|
|
|
20
20
|
import { dagCborToSafeJSON } from './utils/dag-cbor-to-safe-json.js'
|
|
21
21
|
import { getContentDispositionFilename } from './utils/get-content-disposition-filename.js'
|
|
22
22
|
import { getETag } from './utils/get-e-tag.js'
|
|
23
|
+
import { getResolvedAcceptHeader } from './utils/get-resolved-accept-header.js'
|
|
23
24
|
import { getStreamFromAsyncIterable } from './utils/get-stream-from-async-iterable.js'
|
|
24
25
|
import { tarStream } from './utils/get-tar-stream.js'
|
|
25
26
|
import { parseResource } from './utils/parse-resource.js'
|
|
26
27
|
import { setCacheControlHeader } from './utils/response-headers.js'
|
|
27
|
-
import { badRequestResponse, movedPermanentlyResponse, notAcceptableResponse, notSupportedResponse, okResponse, badRangeResponse, okRangeResponse, badGatewayResponse } from './utils/responses.js'
|
|
28
|
-
import { selectOutputType
|
|
29
|
-
import { walkPath } from './utils/walk-path.js'
|
|
28
|
+
import { badRequestResponse, movedPermanentlyResponse, notAcceptableResponse, notSupportedResponse, okResponse, badRangeResponse, okRangeResponse, badGatewayResponse, notFoundResponse } from './utils/responses.js'
|
|
29
|
+
import { selectOutputType } from './utils/select-output-type.js'
|
|
30
|
+
import { isObjectNode, walkPath } from './utils/walk-path.js'
|
|
30
31
|
import type { CIDDetail, ContentTypeParser, Resource, VerifiedFetchInit as VerifiedFetchOptions } from './index.js'
|
|
31
32
|
import type { RequestFormatShorthand } from './types.js'
|
|
32
33
|
import type { ParsedUrlStringResults } from './utils/parse-url-string'
|
|
33
34
|
import type { Helia } from '@helia/interface'
|
|
34
35
|
import type { DNSResolver } from '@multiformats/dns/resolvers'
|
|
35
|
-
import type { UnixFSEntry } from 'ipfs-unixfs-exporter'
|
|
36
|
+
import type { ObjectNode, UnixFSEntry } from 'ipfs-unixfs-exporter'
|
|
36
37
|
import type { CID } from 'multiformats/cid'
|
|
37
38
|
|
|
38
39
|
interface VerifiedFetchComponents {
|
|
@@ -93,6 +94,7 @@ function convertOptions (options?: VerifiedFetchOptions): (Omit<VerifiedFetchOpt
|
|
|
93
94
|
* skipped and set to these values.
|
|
94
95
|
*/
|
|
95
96
|
const RAW_HEADERS = [
|
|
97
|
+
'application/vnd.ipld.dag-json',
|
|
96
98
|
'application/vnd.ipld.raw',
|
|
97
99
|
'application/octet-stream'
|
|
98
100
|
]
|
|
@@ -103,8 +105,9 @@ const RAW_HEADERS = [
|
|
|
103
105
|
* type. This avoids the user from receiving something different when they
|
|
104
106
|
* signal that they want to `Accept` a specific mime type.
|
|
105
107
|
*/
|
|
106
|
-
function getOverridenRawContentType (headers?: HeadersInit): string | undefined {
|
|
107
|
-
|
|
108
|
+
function getOverridenRawContentType ({ headers, accept }: { headers?: HeadersInit, accept?: string }): string | undefined {
|
|
109
|
+
// accept has already been resolved by getResolvedAcceptHeader, if we have it, use it.
|
|
110
|
+
const acceptHeader = accept ?? new Headers(headers).get('accept') ?? ''
|
|
108
111
|
|
|
109
112
|
// e.g. "Accept: text/html, application/xhtml+xml, application/xml;q=0.9, image/webp, */*;q=0.8"
|
|
110
113
|
const acceptHeaders = acceptHeader.split(',')
|
|
@@ -233,8 +236,31 @@ export class VerifiedFetch {
|
|
|
233
236
|
|
|
234
237
|
private async handleDagCbor ({ resource, cid, path, accept, options }: FetchHandlerFunctionArg): Promise<Response> {
|
|
235
238
|
this.log.trace('fetching %c/%s', cid, path)
|
|
239
|
+
let terminalElement: ObjectNode | undefined
|
|
240
|
+
let ipfsRoots: CID[] | undefined
|
|
241
|
+
|
|
242
|
+
// need to walk path, if it exists, to get the terminal element
|
|
243
|
+
try {
|
|
244
|
+
const pathDetails = await walkPath(this.helia.blockstore, `${cid.toString()}/${path}`, options)
|
|
245
|
+
ipfsRoots = pathDetails.ipfsRoots
|
|
246
|
+
const potentialTerminalElement = pathDetails.terminalElement
|
|
247
|
+
if (potentialTerminalElement == null) {
|
|
248
|
+
return notFoundResponse(resource)
|
|
249
|
+
}
|
|
250
|
+
if (isObjectNode(potentialTerminalElement)) {
|
|
251
|
+
terminalElement = potentialTerminalElement
|
|
252
|
+
}
|
|
253
|
+
} catch (err: any) {
|
|
254
|
+
options?.signal?.throwIfAborted()
|
|
255
|
+
if (['ERR_NO_PROP', 'ERR_NO_TERMINAL_ELEMENT'].includes(err.code)) {
|
|
256
|
+
return notFoundResponse(resource)
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
this.log.error('error walking path %s', path, err)
|
|
260
|
+
return badGatewayResponse(resource, 'Error walking path')
|
|
261
|
+
}
|
|
262
|
+
const block = terminalElement?.node ?? await this.helia.blockstore.get(cid, options)
|
|
236
263
|
|
|
237
|
-
const block = await this.helia.blockstore.get(cid, options)
|
|
238
264
|
let body: string | Uint8Array
|
|
239
265
|
|
|
240
266
|
if (accept === 'application/octet-stream' || accept === 'application/vnd.ipld.dag-cbor' || accept === 'application/cbor') {
|
|
@@ -274,6 +300,10 @@ export class VerifiedFetch {
|
|
|
274
300
|
|
|
275
301
|
response.headers.set('content-type', accept)
|
|
276
302
|
|
|
303
|
+
if (ipfsRoots != null) {
|
|
304
|
+
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
|
|
305
|
+
}
|
|
306
|
+
|
|
277
307
|
return response
|
|
278
308
|
}
|
|
279
309
|
|
|
@@ -288,8 +318,9 @@ export class VerifiedFetch {
|
|
|
288
318
|
ipfsRoots = pathDetails.ipfsRoots
|
|
289
319
|
terminalElement = pathDetails.terminalElement
|
|
290
320
|
} catch (err: any) {
|
|
291
|
-
|
|
292
|
-
|
|
321
|
+
options?.signal?.throwIfAborted()
|
|
322
|
+
if (['ERR_NO_PROP', 'ERR_NO_TERMINAL_ELEMENT'].includes(err.code)) {
|
|
323
|
+
return notFoundResponse(resource.toString())
|
|
293
324
|
}
|
|
294
325
|
this.log.error('error walking path %s', path, err)
|
|
295
326
|
|
|
@@ -328,9 +359,7 @@ export class VerifiedFetch {
|
|
|
328
359
|
path = rootFilePath
|
|
329
360
|
resolvedCID = stat.cid
|
|
330
361
|
} catch (err: any) {
|
|
331
|
-
|
|
332
|
-
throw new AbortError('signal aborted by user')
|
|
333
|
-
}
|
|
362
|
+
options?.signal?.throwIfAborted()
|
|
334
363
|
this.log('error loading path %c/%s', dirCid, rootFilePath, err)
|
|
335
364
|
return notSupportedResponse('Unable to find index.html for directory at given path. Support for directories with implicit root is not implemented')
|
|
336
365
|
} finally {
|
|
@@ -374,9 +403,7 @@ export class VerifiedFetch {
|
|
|
374
403
|
|
|
375
404
|
return response
|
|
376
405
|
} catch (err: any) {
|
|
377
|
-
|
|
378
|
-
throw new AbortError('signal aborted by user')
|
|
379
|
-
}
|
|
406
|
+
options?.signal?.throwIfAborted()
|
|
380
407
|
this.log.error('error streaming %c/%s', cid, path, err)
|
|
381
408
|
if (byteRangeContext.isRangeRequest && err.code === 'ERR_INVALID_PARAMS') {
|
|
382
409
|
return badRangeResponse(resource)
|
|
@@ -385,7 +412,7 @@ export class VerifiedFetch {
|
|
|
385
412
|
}
|
|
386
413
|
}
|
|
387
414
|
|
|
388
|
-
private async handleRaw ({ resource, cid, path, options }: FetchHandlerFunctionArg): Promise<Response> {
|
|
415
|
+
private async handleRaw ({ resource, cid, path, options, accept }: FetchHandlerFunctionArg): Promise<Response> {
|
|
389
416
|
const byteRangeContext = new ByteRangeContext(this.helia.logger, options?.headers)
|
|
390
417
|
const result = await this.helia.blockstore.get(cid, options)
|
|
391
418
|
byteRangeContext.setBody(result)
|
|
@@ -396,7 +423,7 @@ export class VerifiedFetch {
|
|
|
396
423
|
// if the user has specified an `Accept` header that corresponds to a raw
|
|
397
424
|
// type, honour that header, so for example they don't request
|
|
398
425
|
// `application/vnd.ipld.raw` but get `application/octet-stream`
|
|
399
|
-
const overriddenContentType = getOverridenRawContentType(options?.headers)
|
|
426
|
+
const overriddenContentType = getOverridenRawContentType({ headers: options?.headers, accept })
|
|
400
427
|
if (overriddenContentType != null) {
|
|
401
428
|
response.headers.set('content-type', overriddenContentType)
|
|
402
429
|
} else {
|
|
@@ -452,7 +479,6 @@ export class VerifiedFetch {
|
|
|
452
479
|
* TODO: move operations called by fetch to a queue of operations where we can
|
|
453
480
|
* always exit early (and cleanly) if a given signal is aborted
|
|
454
481
|
*/
|
|
455
|
-
// eslint-disable-next-line complexity
|
|
456
482
|
async fetch (resource: Resource, opts?: VerifiedFetchOptions): Promise<Response> {
|
|
457
483
|
this.log('fetch %s', resource)
|
|
458
484
|
|
|
@@ -474,9 +500,7 @@ export class VerifiedFetch {
|
|
|
474
500
|
ttl = result.ttl
|
|
475
501
|
protocol = result.protocol
|
|
476
502
|
} catch (err: any) {
|
|
477
|
-
|
|
478
|
-
throw new AbortError('signal aborted by user')
|
|
479
|
-
}
|
|
503
|
+
options?.signal?.throwIfAborted()
|
|
480
504
|
this.log.error('error parsing resource %s', resource, err)
|
|
481
505
|
|
|
482
506
|
return badRequestResponse(resource.toString(), err)
|
|
@@ -484,20 +508,8 @@ export class VerifiedFetch {
|
|
|
484
508
|
|
|
485
509
|
options?.onProgress?.(new CustomProgressEvent<CIDDetail>('verified-fetch:request:resolve', { cid, path }))
|
|
486
510
|
|
|
487
|
-
const
|
|
488
|
-
const incomingAcceptHeader = requestHeaders.get('accept')
|
|
489
|
-
|
|
490
|
-
if (incomingAcceptHeader != null) {
|
|
491
|
-
this.log('incoming accept header "%s"', incomingAcceptHeader)
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
const queryFormatMapping = queryFormatToAcceptHeader(query.format)
|
|
495
|
-
|
|
496
|
-
if (query.format != null) {
|
|
497
|
-
this.log('incoming query format "%s", mapped to %s', query.format, queryFormatMapping)
|
|
498
|
-
}
|
|
511
|
+
const acceptHeader = getResolvedAcceptHeader({ query, headers: options?.headers, logger: this.helia.logger })
|
|
499
512
|
|
|
500
|
-
const acceptHeader = incomingAcceptHeader ?? queryFormatMapping
|
|
501
513
|
const accept = selectOutputType(cid, acceptHeader)
|
|
502
514
|
this.log('output type %s', accept)
|
|
503
515
|
|
|
@@ -508,7 +520,7 @@ export class VerifiedFetch {
|
|
|
508
520
|
let response: Response
|
|
509
521
|
let reqFormat: RequestFormatShorthand | undefined
|
|
510
522
|
|
|
511
|
-
const handlerArgs = { resource: resource.toString(), cid, path, accept, options }
|
|
523
|
+
const handlerArgs: FetchHandlerFunctionArg = { resource: resource.toString(), cid, path, accept, options }
|
|
512
524
|
|
|
513
525
|
if (accept === 'application/vnd.ipfs.ipns-record') {
|
|
514
526
|
// the user requested a raw IPNS record
|