@helia/verified-fetch 2.6.4 → 2.6.6
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 +109 -44
- package/dist/src/plugins/plugin-handle-byte-range-context.d.ts +13 -0
- package/dist/src/plugins/plugin-handle-byte-range-context.d.ts.map +1 -0
- package/dist/src/plugins/plugin-handle-byte-range-context.js +19 -0
- package/dist/src/plugins/plugin-handle-byte-range-context.js.map +1 -0
- package/dist/src/plugins/plugin-handle-car.d.ts +1 -1
- package/dist/src/plugins/plugin-handle-car.d.ts.map +1 -1
- package/dist/src/plugins/plugin-handle-car.js +9 -2
- package/dist/src/plugins/plugin-handle-car.js.map +1 -1
- package/dist/src/plugins/plugin-handle-dag-cbor.d.ts +2 -2
- package/dist/src/plugins/plugin-handle-dag-cbor.d.ts.map +1 -1
- package/dist/src/plugins/plugin-handle-dag-cbor.js +7 -6
- package/dist/src/plugins/plugin-handle-dag-cbor.js.map +1 -1
- package/dist/src/plugins/plugin-handle-dag-pb.d.ts +2 -2
- package/dist/src/plugins/plugin-handle-dag-pb.d.ts.map +1 -1
- package/dist/src/plugins/plugin-handle-dag-pb.js +5 -6
- package/dist/src/plugins/plugin-handle-dag-pb.js.map +1 -1
- package/dist/src/plugins/plugin-handle-dir-index-html.d.ts +1 -1
- package/dist/src/plugins/plugin-handle-dir-index-html.d.ts.map +1 -1
- package/dist/src/plugins/plugin-handle-dir-index-html.js +7 -15
- package/dist/src/plugins/plugin-handle-dir-index-html.js.map +1 -1
- package/dist/src/plugins/plugin-handle-ipns-record.d.ts +2 -2
- package/dist/src/plugins/plugin-handle-ipns-record.d.ts.map +1 -1
- package/dist/src/plugins/plugin-handle-ipns-record.js +8 -4
- package/dist/src/plugins/plugin-handle-ipns-record.js.map +1 -1
- package/dist/src/plugins/plugin-handle-json.d.ts +2 -2
- package/dist/src/plugins/plugin-handle-json.d.ts.map +1 -1
- package/dist/src/plugins/plugin-handle-json.js +7 -3
- package/dist/src/plugins/plugin-handle-json.js.map +1 -1
- package/dist/src/plugins/plugin-handle-raw.d.ts +2 -2
- package/dist/src/plugins/plugin-handle-raw.d.ts.map +1 -1
- package/dist/src/plugins/plugin-handle-raw.js +6 -5
- package/dist/src/plugins/plugin-handle-raw.js.map +1 -1
- package/dist/src/plugins/plugin-handle-tar.d.ts +2 -2
- package/dist/src/plugins/plugin-handle-tar.d.ts.map +1 -1
- package/dist/src/plugins/plugin-handle-tar.js +7 -3
- package/dist/src/plugins/plugin-handle-tar.js.map +1 -1
- package/dist/src/plugins/types.d.ts +10 -1
- package/dist/src/plugins/types.d.ts.map +1 -1
- package/dist/src/utils/byte-range-context.d.ts.map +1 -1
- package/dist/src/utils/byte-range-context.js +1 -0
- package/dist/src/utils/byte-range-context.js.map +1 -1
- package/dist/src/utils/parse-url-string.d.ts +4 -2
- package/dist/src/utils/parse-url-string.d.ts.map +1 -1
- package/dist/src/utils/parse-url-string.js +15 -7
- package/dist/src/utils/parse-url-string.js.map +1 -1
- package/dist/src/utils/responses.d.ts +1 -4
- package/dist/src/utils/responses.d.ts.map +1 -1
- package/dist/src/utils/responses.js +25 -9
- package/dist/src/utils/responses.js.map +1 -1
- package/dist/src/verified-fetch.d.ts.map +1 -1
- package/dist/src/verified-fetch.js +13 -1
- package/dist/src/verified-fetch.js.map +1 -1
- package/package.json +19 -19
- package/src/plugins/plugin-handle-byte-range-context.ts +22 -0
- package/src/plugins/plugin-handle-car.ts +10 -3
- package/src/plugins/plugin-handle-dag-cbor.ts +10 -7
- package/src/plugins/plugin-handle-dag-pb.ts +6 -7
- package/src/plugins/plugin-handle-dir-index-html.ts +11 -16
- package/src/plugins/plugin-handle-ipns-record.ts +10 -5
- package/src/plugins/plugin-handle-json.ts +9 -4
- package/src/plugins/plugin-handle-raw.ts +7 -6
- package/src/plugins/plugin-handle-tar.ts +9 -4
- package/src/plugins/types.ts +10 -1
- package/src/utils/byte-range-context.ts +1 -0
- package/src/utils/parse-url-string.ts +16 -7
- package/src/utils/responses.ts +27 -9
- package/src/verified-fetch.ts +14 -1
|
@@ -2,7 +2,7 @@ import * as ipldDagCbor from '@ipld/dag-cbor'
|
|
|
2
2
|
import * as ipldDagJson from '@ipld/dag-json'
|
|
3
3
|
import { dagCborToSafeJSON } from '../utils/dag-cbor-to-safe-json.js'
|
|
4
4
|
import { setIpfsRoots } from '../utils/response-headers.js'
|
|
5
|
-
import { notAcceptableResponse,
|
|
5
|
+
import { notAcceptableResponse, okRangeResponse } from '../utils/responses.js'
|
|
6
6
|
import { isObjectNode } from '../utils/walk-path.js'
|
|
7
7
|
import { BasePlugin } from './plugin-base.js'
|
|
8
8
|
import type { PluginContext } from './types.js'
|
|
@@ -14,7 +14,7 @@ import type { ObjectNode } from 'ipfs-unixfs-exporter'
|
|
|
14
14
|
export class DagCborPlugin extends BasePlugin {
|
|
15
15
|
readonly codes = [ipldDagCbor.code]
|
|
16
16
|
|
|
17
|
-
canHandle ({ cid, accept, pathDetails }: PluginContext): boolean {
|
|
17
|
+
canHandle ({ cid, accept, pathDetails, byteRangeContext }: PluginContext): boolean {
|
|
18
18
|
this.log('checking if we can handle %c with accept %s', cid, accept)
|
|
19
19
|
if (pathDetails == null) {
|
|
20
20
|
return false
|
|
@@ -25,17 +25,18 @@ export class DagCborPlugin extends BasePlugin {
|
|
|
25
25
|
if (cid.code !== ipldDagCbor.code) {
|
|
26
26
|
return false
|
|
27
27
|
}
|
|
28
|
+
if (byteRangeContext == null) {
|
|
29
|
+
return false
|
|
30
|
+
}
|
|
28
31
|
|
|
29
32
|
return isObjectNode(pathDetails.terminalElement)
|
|
30
33
|
}
|
|
31
34
|
|
|
32
|
-
async handle (context: PluginContext): Promise<Response> {
|
|
35
|
+
async handle (context: PluginContext & Required<Pick<PluginContext, 'byteRangeContext' | 'pathDetails'>>): Promise<Response> {
|
|
33
36
|
const { cid, path, resource, accept, pathDetails } = context
|
|
34
37
|
|
|
35
38
|
this.log.trace('fetching %c/%s', cid, path)
|
|
36
|
-
|
|
37
|
-
throw new Error('pathDetails is null')
|
|
38
|
-
}
|
|
39
|
+
|
|
39
40
|
const ipfsRoots = pathDetails.ipfsRoots
|
|
40
41
|
const terminalElement = pathDetails.terminalElement as ObjectNode // checked in canHandle fn.
|
|
41
42
|
|
|
@@ -72,7 +73,9 @@ export class DagCborPlugin extends BasePlugin {
|
|
|
72
73
|
}
|
|
73
74
|
}
|
|
74
75
|
|
|
75
|
-
|
|
76
|
+
context.byteRangeContext.setBody(body)
|
|
77
|
+
|
|
78
|
+
const response = okRangeResponse(resource, context.byteRangeContext.getBody(), { byteRangeContext: context.byteRangeContext, log: this.log })
|
|
76
79
|
|
|
77
80
|
const responseContentType = accept ?? (body instanceof Uint8Array ? 'application/octet-stream' : 'application/json')
|
|
78
81
|
|
|
@@ -2,7 +2,6 @@ import { unixfs } from '@helia/unixfs'
|
|
|
2
2
|
import { code as dagPbCode } from '@ipld/dag-pb'
|
|
3
3
|
import { exporter } from 'ipfs-unixfs-exporter'
|
|
4
4
|
import { CustomProgressEvent } from 'progress-events'
|
|
5
|
-
import { ByteRangeContext } from '../utils/byte-range-context.js'
|
|
6
5
|
import { getStreamFromAsyncIterable } from '../utils/get-stream-from-async-iterable.js'
|
|
7
6
|
import { setIpfsRoots } from '../utils/response-headers.js'
|
|
8
7
|
import { badGatewayResponse, badRangeResponse, movedPermanentlyResponse, notSupportedResponse, okRangeResponse } from '../utils/responses.js'
|
|
@@ -16,11 +15,14 @@ import type { CIDDetail } from '../index.js'
|
|
|
16
15
|
*/
|
|
17
16
|
export class DagPbPlugin extends BasePlugin {
|
|
18
17
|
readonly codes = [dagPbCode]
|
|
19
|
-
canHandle ({ cid, accept, pathDetails }: PluginContext): boolean {
|
|
18
|
+
canHandle ({ cid, accept, pathDetails, byteRangeContext }: PluginContext): boolean {
|
|
20
19
|
this.log('checking if we can handle %c with accept %s', cid, accept)
|
|
21
20
|
if (pathDetails == null) {
|
|
22
21
|
return false
|
|
23
22
|
}
|
|
23
|
+
if (byteRangeContext == null) {
|
|
24
|
+
return false
|
|
25
|
+
}
|
|
24
26
|
|
|
25
27
|
return cid.code === dagPbCode
|
|
26
28
|
}
|
|
@@ -49,7 +51,7 @@ export class DagPbPlugin extends BasePlugin {
|
|
|
49
51
|
return null
|
|
50
52
|
}
|
|
51
53
|
|
|
52
|
-
async handle (context: PluginContext): Promise<Response | null> {
|
|
54
|
+
async handle (context: PluginContext & Required<Pick<PluginContext, 'byteRangeContext' | 'pathDetails'>>): Promise<Response | null> {
|
|
53
55
|
const { cid, options, withServerTiming = false, pathDetails } = context
|
|
54
56
|
const { handleServerTiming, contentTypeParser, helia, getBlockstore } = this.pluginOptions
|
|
55
57
|
const log = this.log
|
|
@@ -57,11 +59,8 @@ export class DagPbPlugin extends BasePlugin {
|
|
|
57
59
|
let path = context.path
|
|
58
60
|
|
|
59
61
|
let redirected = false
|
|
60
|
-
const byteRangeContext = new ByteRangeContext(this.pluginOptions.logger, options?.headers)
|
|
61
62
|
|
|
62
|
-
|
|
63
|
-
throw new TypeError('Path details are required')
|
|
64
|
-
}
|
|
63
|
+
const byteRangeContext = context.byteRangeContext
|
|
65
64
|
const ipfsRoots = pathDetails.ipfsRoots
|
|
66
65
|
const terminalElement = pathDetails.terminalElement
|
|
67
66
|
let resolvedCID = terminalElement.cid
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { code as dagPbCode } from '@ipld/dag-pb'
|
|
2
2
|
import { dirIndexHtml } from '../utils/dir-index-html.js'
|
|
3
|
+
import { okRangeResponse } from '../utils/responses.js'
|
|
3
4
|
import { BasePlugin } from './plugin-base.js'
|
|
4
5
|
import type { PluginContext, VerifiedFetchPluginFactory } from './types.js'
|
|
5
6
|
|
|
@@ -21,29 +22,23 @@ export class DirIndexHtmlPlugin extends BasePlugin {
|
|
|
21
22
|
return cid.code === dagPbCode
|
|
22
23
|
}
|
|
23
24
|
|
|
24
|
-
async handle (context: PluginContext): Promise<Response> {
|
|
25
|
+
async handle (context: PluginContext & Required<Pick<PluginContext, 'byteRangeContext' | 'pathDetails' | 'directoryEntries'>>): Promise<Response> {
|
|
25
26
|
const { resource, pathDetails, directoryEntries } = context
|
|
26
27
|
|
|
27
|
-
if (pathDetails?.terminalElement == null) {
|
|
28
|
-
throw new Error('Path details are required')
|
|
29
|
-
}
|
|
30
|
-
if (directoryEntries == null || directoryEntries?.length === 0) {
|
|
31
|
-
throw new Error('Directory entries are required')
|
|
32
|
-
}
|
|
33
28
|
const terminalElement = pathDetails.terminalElement
|
|
34
29
|
|
|
35
30
|
const gatewayURL = resource
|
|
36
31
|
const htmlResponse = dirIndexHtml(terminalElement, directoryEntries, { gatewayURL, log: this.log })
|
|
37
32
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
33
|
+
context.byteRangeContext.setBody(htmlResponse)
|
|
34
|
+
|
|
35
|
+
const response = okRangeResponse(resource, context.byteRangeContext.getBody(), { byteRangeContext: context.byteRangeContext, log: this.log })
|
|
36
|
+
|
|
37
|
+
response.headers.set('Content-Type', 'text/html')
|
|
38
|
+
// see https://github.com/ipfs/gateway-conformance/pull/219
|
|
39
|
+
response.headers.set('Cache-Control', 'public, max-age=604800, stale-while-revalidate=2678400')
|
|
40
|
+
|
|
41
|
+
return response
|
|
47
42
|
}
|
|
48
43
|
}
|
|
49
44
|
|
|
@@ -4,7 +4,7 @@ import { concat as uint8ArrayConcat } from 'uint8arrays/concat'
|
|
|
4
4
|
import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string'
|
|
5
5
|
import { toString as uint8ArrayToString } from 'uint8arrays/to-string'
|
|
6
6
|
import { getPeerIdFromString } from '../utils/get-peer-id-from-string.js'
|
|
7
|
-
import { badRequestResponse,
|
|
7
|
+
import { badRequestResponse, okRangeResponse } from '../utils/responses.js'
|
|
8
8
|
import { PluginFatalError } from './errors.js'
|
|
9
9
|
import { BasePlugin } from './plugin-base.js'
|
|
10
10
|
import type { PluginContext } from './types.js'
|
|
@@ -16,19 +16,22 @@ import type { PeerId } from '@libp2p/interface'
|
|
|
16
16
|
*/
|
|
17
17
|
export class IpnsRecordPlugin extends BasePlugin {
|
|
18
18
|
readonly codes = []
|
|
19
|
-
canHandle ({ cid, accept, query }: PluginContext): boolean {
|
|
19
|
+
canHandle ({ cid, accept, query, byteRangeContext }: PluginContext): boolean {
|
|
20
20
|
this.log('checking if we can handle %c with accept %s', cid, accept)
|
|
21
|
+
if (byteRangeContext == null) {
|
|
22
|
+
return false
|
|
23
|
+
}
|
|
21
24
|
|
|
22
25
|
return accept === 'application/vnd.ipfs.ipns-record' || query.format === 'ipns-record'
|
|
23
26
|
}
|
|
24
27
|
|
|
25
|
-
async handle (context: PluginContext): Promise<Response> {
|
|
28
|
+
async handle (context: PluginContext & Required<Pick<PluginContext, 'byteRangeContext'>>): Promise<Response> {
|
|
26
29
|
const { resource, path, options } = context
|
|
27
30
|
const { helia } = this.pluginOptions
|
|
28
31
|
context.reqFormat = 'ipns-record'
|
|
29
32
|
if (path !== '' || !(resource.startsWith('ipns://') || resource.includes('.ipns.') || resource.includes('/ipns/'))) {
|
|
30
33
|
this.log.error('invalid request for IPNS name "%s" and path "%s"', resource, path)
|
|
31
|
-
throw new PluginFatalError('ERR_INVALID_IPNS_NAME', 'Invalid IPNS name', { response: badRequestResponse(resource, 'Invalid IPNS name') })
|
|
34
|
+
throw new PluginFatalError('ERR_INVALID_IPNS_NAME', 'Invalid IPNS name', { response: badRequestResponse(resource, new Error('Invalid IPNS name')) })
|
|
32
35
|
}
|
|
33
36
|
let peerId: PeerId
|
|
34
37
|
|
|
@@ -61,7 +64,9 @@ export class IpnsRecordPlugin extends BasePlugin {
|
|
|
61
64
|
const buf = await helia.datastore.get(datastoreKey, options)
|
|
62
65
|
const record = DHTRecord.deserialize(buf)
|
|
63
66
|
|
|
64
|
-
|
|
67
|
+
context.byteRangeContext.setBody(record.value)
|
|
68
|
+
|
|
69
|
+
const response = okRangeResponse(resource, context.byteRangeContext.getBody(), { byteRangeContext: context.byteRangeContext, log: this.log })
|
|
65
70
|
response.headers.set('content-type', 'application/vnd.ipfs.ipns-record')
|
|
66
71
|
|
|
67
72
|
return response
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as ipldDagCbor from '@ipld/dag-cbor'
|
|
2
2
|
import * as ipldDagJson from '@ipld/dag-json'
|
|
3
3
|
import { code as jsonCode } from 'multiformats/codecs/json'
|
|
4
|
-
import { notAcceptableResponse,
|
|
4
|
+
import { notAcceptableResponse, okRangeResponse } from '../utils/responses.js'
|
|
5
5
|
import { BasePlugin } from './plugin-base.js'
|
|
6
6
|
import type { PluginContext } from './types.js'
|
|
7
7
|
|
|
@@ -10,8 +10,11 @@ import type { PluginContext } from './types.js'
|
|
|
10
10
|
*/
|
|
11
11
|
export class JsonPlugin extends BasePlugin {
|
|
12
12
|
readonly codes = [ipldDagJson.code, jsonCode]
|
|
13
|
-
canHandle ({ cid, accept }: PluginContext): boolean {
|
|
13
|
+
canHandle ({ cid, accept, byteRangeContext }: PluginContext): boolean {
|
|
14
14
|
this.log('checking if we can handle %c with accept %s', cid, accept)
|
|
15
|
+
if (byteRangeContext == null) {
|
|
16
|
+
return false
|
|
17
|
+
}
|
|
15
18
|
|
|
16
19
|
if (accept === 'application/vnd.ipld.dag-json' && cid.code !== ipldDagCbor.code) {
|
|
17
20
|
// we can handle application/vnd.ipld.dag-json, but if the CID codec is ipldDagCbor, DagCborPlugin should handle it
|
|
@@ -22,7 +25,7 @@ export class JsonPlugin extends BasePlugin {
|
|
|
22
25
|
return ipldDagJson.code === cid.code || jsonCode === cid.code
|
|
23
26
|
}
|
|
24
27
|
|
|
25
|
-
async handle (context: PluginContext): Promise<Response> {
|
|
28
|
+
async handle (context: PluginContext & Required<Pick<PluginContext, 'byteRangeContext'>>): Promise<Response> {
|
|
26
29
|
const { path, resource, cid, accept, options } = context
|
|
27
30
|
const { getBlockstore } = this.pluginOptions
|
|
28
31
|
const session = options?.session ?? true
|
|
@@ -50,7 +53,9 @@ export class JsonPlugin extends BasePlugin {
|
|
|
50
53
|
body = block
|
|
51
54
|
}
|
|
52
55
|
|
|
53
|
-
|
|
56
|
+
context.byteRangeContext.setBody(body)
|
|
57
|
+
|
|
58
|
+
const response = okRangeResponse(resource, context.byteRangeContext.getBody(), { byteRangeContext: context.byteRangeContext, log: this.log })
|
|
54
59
|
response.headers.set('content-type', accept ?? 'application/json')
|
|
55
60
|
return response
|
|
56
61
|
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { code as rawCode } from 'multiformats/codecs/raw'
|
|
2
2
|
import { identity } from 'multiformats/hashes/identity'
|
|
3
|
-
import { ByteRangeContext } from '../utils/byte-range-context.js'
|
|
4
3
|
import { notFoundResponse, okRangeResponse } from '../utils/responses.js'
|
|
5
4
|
import { setContentType } from '../utils/set-content-type.js'
|
|
6
5
|
import { PluginFatalError } from './errors.js'
|
|
@@ -46,12 +45,15 @@ function getOverridenRawContentType ({ headers, accept }: { headers?: HeadersIni
|
|
|
46
45
|
export class RawPlugin extends BasePlugin {
|
|
47
46
|
codes: number[] = [rawCode, identity.code]
|
|
48
47
|
|
|
49
|
-
canHandle ({ cid, accept, query }: PluginContext): boolean {
|
|
48
|
+
canHandle ({ cid, accept, query, byteRangeContext }: PluginContext): boolean {
|
|
50
49
|
this.log('checking if we can handle %c with accept %s', cid, accept)
|
|
50
|
+
if (byteRangeContext == null) {
|
|
51
|
+
return false
|
|
52
|
+
}
|
|
51
53
|
return accept === 'application/vnd.ipld.raw' || query.format === 'raw'
|
|
52
54
|
}
|
|
53
55
|
|
|
54
|
-
async handle (context: PluginContext): Promise<Response> {
|
|
56
|
+
async handle (context: PluginContext & Required<Pick<PluginContext, 'byteRangeContext'>>): Promise<Response> {
|
|
55
57
|
const { path, resource, cid, accept, query, options } = context
|
|
56
58
|
const { getBlockstore, contentTypeParser } = this.pluginOptions
|
|
57
59
|
const session = options?.session ?? true
|
|
@@ -73,12 +75,11 @@ export class RawPlugin extends BasePlugin {
|
|
|
73
75
|
throw new PluginFatalError('ERR_RAW_PATHS_NOT_SUPPORTED', 'Raw codec does not support paths', { response: notFoundResponse(resource, 'Raw codec does not support paths') })
|
|
74
76
|
}
|
|
75
77
|
|
|
76
|
-
const byteRangeContext = new ByteRangeContext(this.pluginOptions.logger, options?.headers)
|
|
77
78
|
const terminalCid = context.pathDetails?.terminalElement.cid ?? context.cid
|
|
78
79
|
const blockstore = getBlockstore(terminalCid, resource, session, options)
|
|
79
80
|
const result = await blockstore.get(terminalCid, options)
|
|
80
|
-
byteRangeContext.setBody(result)
|
|
81
|
-
const response = okRangeResponse(resource, byteRangeContext.getBody(), { byteRangeContext, log }, {
|
|
81
|
+
context.byteRangeContext.setBody(result)
|
|
82
|
+
const response = okRangeResponse(resource, context.byteRangeContext.getBody(), { byteRangeContext: context.byteRangeContext, log }, {
|
|
82
83
|
redirected: false
|
|
83
84
|
})
|
|
84
85
|
|
|
@@ -3,7 +3,7 @@ import toBrowserReadableStream from 'it-to-browser-readablestream'
|
|
|
3
3
|
import { code as rawCode } from 'multiformats/codecs/raw'
|
|
4
4
|
import { getETag } from '../utils/get-e-tag.js'
|
|
5
5
|
import { tarStream } from '../utils/get-tar-stream.js'
|
|
6
|
-
import { notAcceptableResponse,
|
|
6
|
+
import { notAcceptableResponse, okRangeResponse } from '../utils/responses.js'
|
|
7
7
|
import { BasePlugin } from './plugin-base.js'
|
|
8
8
|
import type { PluginContext } from './types.js'
|
|
9
9
|
|
|
@@ -13,12 +13,15 @@ import type { PluginContext } from './types.js'
|
|
|
13
13
|
*/
|
|
14
14
|
export class TarPlugin extends BasePlugin {
|
|
15
15
|
readonly codes = []
|
|
16
|
-
canHandle ({ cid, accept, query }: PluginContext): boolean {
|
|
16
|
+
canHandle ({ cid, accept, query, byteRangeContext }: PluginContext): boolean {
|
|
17
17
|
this.log('checking if we can handle %c with accept %s', cid, accept)
|
|
18
|
+
if (byteRangeContext == null) {
|
|
19
|
+
return false
|
|
20
|
+
}
|
|
18
21
|
return accept === 'application/x-tar' || query.format === 'tar'
|
|
19
22
|
}
|
|
20
23
|
|
|
21
|
-
async handle (context: PluginContext): Promise<Response> {
|
|
24
|
+
async handle (context: PluginContext & Required<Pick<PluginContext, 'byteRangeContext'>>): Promise<Response> {
|
|
22
25
|
const { cid, path, resource, options, pathDetails } = context
|
|
23
26
|
const { getBlockstore } = this.pluginOptions
|
|
24
27
|
|
|
@@ -34,7 +37,9 @@ export class TarPlugin extends BasePlugin {
|
|
|
34
37
|
const blockstore = getBlockstore(terminusElement, resource, options?.session, options)
|
|
35
38
|
const stream = toBrowserReadableStream<Uint8Array>(tarStream(`/ipfs/${cid}/${path}`, blockstore, options))
|
|
36
39
|
|
|
37
|
-
|
|
40
|
+
context.byteRangeContext.setBody(stream)
|
|
41
|
+
|
|
42
|
+
const response = okRangeResponse(resource, context.byteRangeContext.getBody(), { byteRangeContext: context.byteRangeContext, log: this.log })
|
|
38
43
|
response.headers.set('content-type', 'application/x-tar')
|
|
39
44
|
|
|
40
45
|
response.headers.set('etag', getETag({ cid: terminusElement, reqFormat: context.reqFormat, weak: true }))
|
package/src/plugins/types.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { PluginError } from './errors.js'
|
|
2
2
|
import type { VerifiedFetchInit } from '../index.js'
|
|
3
3
|
import type { ContentTypeParser, RequestFormatShorthand } from '../types.js'
|
|
4
|
+
import type { ByteRangeContext } from '../utils/byte-range-context.js'
|
|
4
5
|
import type { ParsedUrlStringResults } from '../utils/parse-url-string.js'
|
|
5
6
|
import type { PathWalkerResponse } from '../utils/walk-path.js'
|
|
6
7
|
import type { AbortOptions, ComponentLogger, Logger } from '@libp2p/interface'
|
|
@@ -12,7 +13,7 @@ import type { CustomProgressEvent } from 'progress-events'
|
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
15
|
* Contains common components and functions required by plugins to handle a request.
|
|
15
|
-
* - Read-Only: Plugins can read but shouldn
|
|
16
|
+
* - Read-Only: Plugins can read but shouldn't rewrite them.
|
|
16
17
|
* - Persistent: Relevant even after the request completes (e.g., logging or metrics).
|
|
17
18
|
*/
|
|
18
19
|
export interface PluginOptions {
|
|
@@ -47,6 +48,14 @@ export interface PluginContext extends ParsedUrlStringResults {
|
|
|
47
48
|
errors?: PluginError[]
|
|
48
49
|
reqFormat?: RequestFormatShorthand
|
|
49
50
|
pathDetails?: PathWalkerResponse
|
|
51
|
+
query: ParsedUrlStringResults['query']
|
|
52
|
+
/**
|
|
53
|
+
* ByteRangeContext contains information about the size of the content and range requests.
|
|
54
|
+
* This can be used to set the Content-Length header without loading the entire body.
|
|
55
|
+
*
|
|
56
|
+
* This is set by the ByteRangeContextPlugin
|
|
57
|
+
*/
|
|
58
|
+
byteRangeContext?: ByteRangeContext
|
|
50
59
|
[key: string]: unknown
|
|
51
60
|
}
|
|
52
61
|
|
|
@@ -107,6 +107,7 @@ export class ByteRangeContext {
|
|
|
107
107
|
this.log.trace('returning body with byteStart=%o, byteEnd=%o, byteSize=%o', byteStart, byteEnd, byteSize)
|
|
108
108
|
if (body instanceof ReadableStream) {
|
|
109
109
|
// stream should already be spliced by `unixfs.cat`
|
|
110
|
+
// TODO: if the content is not unixfs and unixfs.cat was not called, we need to slice the body here.
|
|
110
111
|
return body
|
|
111
112
|
}
|
|
112
113
|
return this.getSlicedBody(body)
|
|
@@ -144,10 +144,12 @@ function dnsLinkLabelDecoder (linkLabel: string): string {
|
|
|
144
144
|
* A function that parses ipfs:// and ipns:// URLs, returning an object with easily recognizable properties.
|
|
145
145
|
*
|
|
146
146
|
* After determining the protocol successfully, we process the cidOrPeerIdOrDnsLink:
|
|
147
|
-
* * If it's ipfs, it parses the CID or throws
|
|
148
|
-
* * If it's ipns, it attempts to resolve the PeerId and then the DNSLink. If both fail,
|
|
147
|
+
* * If it's ipfs, it parses the CID or throws Error[]
|
|
148
|
+
* * If it's ipns, it attempts to resolve the PeerId and then the DNSLink. If both fail, Error[] is thrown.
|
|
149
149
|
*
|
|
150
150
|
* @todo we need to break out each step of this function (cid parsing, ipns resolving, dnslink resolving) into separate functions and then remove the eslint-disable comment
|
|
151
|
+
*
|
|
152
|
+
* @throws {Error[]}
|
|
151
153
|
*/
|
|
152
154
|
// eslint-disable-next-line complexity
|
|
153
155
|
export async function parseUrlString ({ urlString, ipns, logger, withServerTiming = false }: ParseUrlStringInput, options?: ParseUrlStringOptions): Promise<ParsedUrlStringResults> {
|
|
@@ -185,12 +187,16 @@ export async function parseUrlString ({ urlString, ipns, logger, withServerTimin
|
|
|
185
187
|
// try resolving as an IPNS name
|
|
186
188
|
|
|
187
189
|
peerId = getPeerIdFromString(cidOrPeerIdOrDnsLink)
|
|
188
|
-
|
|
190
|
+
const pubKey = peerId?.publicKey
|
|
191
|
+
if (pubKey == null) {
|
|
189
192
|
throw new TypeError('cidOrPeerIdOrDnsLink contains no public key')
|
|
190
193
|
}
|
|
191
194
|
|
|
192
195
|
if (withServerTiming) {
|
|
193
|
-
const
|
|
196
|
+
const resolveIpns = async (): Promise<IPNSResolveResult> => {
|
|
197
|
+
return ipns.resolve(pubKey, options)
|
|
198
|
+
}
|
|
199
|
+
const resolveResultWithServerTiming = await serverTiming('ipns.resolve', `Resolve IPNS name ${cidOrPeerIdOrDnsLink}`, resolveIpns)
|
|
194
200
|
serverTimings.push(resolveResultWithServerTiming)
|
|
195
201
|
|
|
196
202
|
// eslint-disable-next-line max-depth
|
|
@@ -199,7 +205,7 @@ export async function parseUrlString ({ urlString, ipns, logger, withServerTimin
|
|
|
199
205
|
}
|
|
200
206
|
resolveResult = resolveResultWithServerTiming.result
|
|
201
207
|
} else {
|
|
202
|
-
resolveResult = await ipns.resolve(
|
|
208
|
+
resolveResult = await ipns.resolve(pubKey, options)
|
|
203
209
|
}
|
|
204
210
|
cid = resolveResult?.cid
|
|
205
211
|
resolvedPath = resolveResult?.path
|
|
@@ -211,7 +217,7 @@ export async function parseUrlString ({ urlString, ipns, logger, withServerTimin
|
|
|
211
217
|
errors.push(new TypeError(`Could not parse PeerId in ipns url "${cidOrPeerIdOrDnsLink}", ${(err as Error).message}`))
|
|
212
218
|
} else {
|
|
213
219
|
log.error('could not resolve PeerId %c', peerId, err)
|
|
214
|
-
errors.push(new TypeError(`Could not resolve PeerId "${cidOrPeerIdOrDnsLink}"
|
|
220
|
+
errors.push(new TypeError(`Could not resolve PeerId "${cidOrPeerIdOrDnsLink}": ${(err as Error).message}`))
|
|
215
221
|
}
|
|
216
222
|
}
|
|
217
223
|
|
|
@@ -255,7 +261,10 @@ export async function parseUrlString ({ urlString, ipns, logger, withServerTimin
|
|
|
255
261
|
throw errors[0]
|
|
256
262
|
}
|
|
257
263
|
|
|
258
|
-
|
|
264
|
+
errors.push(new Error(`Invalid resource. Cannot determine CID from URL "${urlString}".`))
|
|
265
|
+
|
|
266
|
+
// eslint-disable-next-line @typescript-eslint/no-throw-literal
|
|
267
|
+
throw errors
|
|
259
268
|
}
|
|
260
269
|
|
|
261
270
|
let ttl = calculateTtl(resolveResult)
|
package/src/utils/responses.ts
CHANGED
|
@@ -98,17 +98,35 @@ export function notFoundResponse (url: string, body?: SupportedBodyTypes, init?:
|
|
|
98
98
|
return response
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
101
|
+
function isArrayOfErrors (body: unknown | Error | Error[]): body is Error[] {
|
|
102
|
+
return Array.isArray(body) && body.every(e => e instanceof Error)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function badRequestResponse (url: string, errors: Error | Error[], init?: ResponseInit): Response {
|
|
106
|
+
// stacktrace of the single error, or the stacktrace of the last error in the array
|
|
107
|
+
let stack: string | undefined
|
|
108
|
+
let convertedErrors: Array<{ message: string, stack: string }> | undefined
|
|
109
|
+
if (isArrayOfErrors(errors)) {
|
|
110
|
+
stack = errors[errors.length - 1].stack
|
|
111
|
+
convertedErrors = errors.map(e => ({ message: e.message, stack: e.stack ?? '' }))
|
|
112
|
+
} else if (errors instanceof Error) {
|
|
113
|
+
stack = errors.stack
|
|
114
|
+
convertedErrors = [{ message: errors.message, stack: errors.stack ?? '' }]
|
|
107
115
|
}
|
|
108
|
-
|
|
109
|
-
|
|
116
|
+
|
|
117
|
+
const bodyJson = JSON.stringify({
|
|
118
|
+
stack,
|
|
119
|
+
errors: convertedErrors
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
const response = new Response(bodyJson, {
|
|
110
123
|
status: 400,
|
|
111
|
-
statusText: 'Bad Request'
|
|
124
|
+
statusText: 'Bad Request',
|
|
125
|
+
...(init ?? {}),
|
|
126
|
+
headers: {
|
|
127
|
+
...(init?.headers ?? {}),
|
|
128
|
+
'Content-Type': 'application/json'
|
|
129
|
+
}
|
|
112
130
|
})
|
|
113
131
|
|
|
114
132
|
setType(response, 'basic')
|
package/src/verified-fetch.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { prefixLogger } from '@libp2p/logger'
|
|
|
4
4
|
import { LRUCache } from 'lru-cache'
|
|
5
5
|
import { type CID } from 'multiformats/cid'
|
|
6
6
|
import { CustomProgressEvent } from 'progress-events'
|
|
7
|
+
import { ByteRangeContextPlugin } from './plugins/plugin-handle-byte-range-context.js'
|
|
7
8
|
import { CarPlugin } from './plugins/plugin-handle-car.js'
|
|
8
9
|
import { DagCborPlugin } from './plugins/plugin-handle-dag-cbor.js'
|
|
9
10
|
import { DagPbPlugin } from './plugins/plugin-handle-dag-pb.js'
|
|
@@ -90,6 +91,7 @@ export class VerifiedFetch {
|
|
|
90
91
|
|
|
91
92
|
const defaultPlugins = [
|
|
92
93
|
new DagWalkPlugin(pluginOptions),
|
|
94
|
+
new ByteRangeContextPlugin(pluginOptions),
|
|
93
95
|
new IpnsRecordPlugin(pluginOptions),
|
|
94
96
|
new CarPlugin(pluginOptions),
|
|
95
97
|
new RawPlugin(pluginOptions),
|
|
@@ -151,13 +153,24 @@ export class VerifiedFetch {
|
|
|
151
153
|
* Server-Timing header to the response if it has been collected. It should be used for any final processing of the
|
|
152
154
|
* response before it is returned to the user.
|
|
153
155
|
*/
|
|
154
|
-
private handleFinalResponse (response: Response, { query, cid, reqFormat, ttl, protocol, ipfsPath, pathDetails }: Partial<PluginContext> = {}): Response {
|
|
156
|
+
private handleFinalResponse (response: Response, { query, cid, reqFormat, ttl, protocol, ipfsPath, pathDetails, byteRangeContext }: Partial<PluginContext> = {}): Response {
|
|
155
157
|
if (this.serverTimingHeaders.length > 0) {
|
|
156
158
|
const headerString = this.serverTimingHeaders.join(', ')
|
|
157
159
|
response.headers.set('Server-Timing', headerString)
|
|
158
160
|
this.serverTimingHeaders = []
|
|
159
161
|
}
|
|
160
162
|
|
|
163
|
+
// if there are multiple ranges, we should omit the content-length header. see https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Transfer-Encoding
|
|
164
|
+
if (response.headers.get('Transfer-Encoding') !== 'chunked') {
|
|
165
|
+
if (byteRangeContext != null) {
|
|
166
|
+
const contentLength = byteRangeContext.length
|
|
167
|
+
if (contentLength != null) {
|
|
168
|
+
this.log.trace('Setting Content-Length from byteRangeContext: %d', contentLength)
|
|
169
|
+
response.headers.set('Content-Length', contentLength.toString())
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
161
174
|
// set Content-Disposition header
|
|
162
175
|
let contentDisposition: string | undefined
|
|
163
176
|
|