@helia/verified-fetch 3.2.2 → 4.0.0
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/README.md +5 -5
- package/dist/index.min.js +81 -71
- package/dist/index.min.js.map +4 -4
- package/dist/src/constants.d.ts +2 -0
- package/dist/src/constants.d.ts.map +1 -0
- package/dist/src/constants.js +2 -0
- package/dist/src/constants.js.map +1 -0
- package/dist/src/index.d.ts +57 -13
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +6 -6
- package/dist/src/index.js.map +1 -1
- package/dist/src/plugins/plugin-handle-car.d.ts.map +1 -1
- package/dist/src/plugins/plugin-handle-car.js +37 -27
- package/dist/src/plugins/plugin-handle-car.js.map +1 -1
- package/dist/src/plugins/plugin-handle-dag-cbor-html-preview.d.ts +1 -1
- package/dist/src/plugins/plugin-handle-dag-cbor-html-preview.d.ts.map +1 -1
- package/dist/src/plugins/plugin-handle-dag-cbor-html-preview.js +1 -1
- package/dist/src/plugins/plugin-handle-dag-cbor-html-preview.js.map +1 -1
- package/dist/src/plugins/plugin-handle-dag-cbor.js +5 -5
- package/dist/src/plugins/plugin-handle-dag-cbor.js.map +1 -1
- package/dist/src/plugins/plugin-handle-dag-pb.js +12 -12
- package/dist/src/plugins/plugin-handle-dag-pb.js.map +1 -1
- package/dist/src/plugins/plugin-handle-dag-walk.d.ts.map +1 -1
- package/dist/src/plugins/plugin-handle-dag-walk.js +5 -4
- package/dist/src/plugins/plugin-handle-dag-walk.js.map +1 -1
- package/dist/src/plugins/plugin-handle-ipns-record.d.ts.map +1 -1
- package/dist/src/plugins/plugin-handle-ipns-record.js +13 -19
- package/dist/src/plugins/plugin-handle-ipns-record.js.map +1 -1
- package/dist/src/plugins/plugin-handle-json.d.ts.map +1 -1
- package/dist/src/plugins/plugin-handle-json.js +5 -4
- package/dist/src/plugins/plugin-handle-json.js.map +1 -1
- package/dist/src/plugins/plugin-handle-raw.d.ts.map +1 -1
- package/dist/src/plugins/plugin-handle-raw.js +18 -5
- package/dist/src/plugins/plugin-handle-raw.js.map +1 -1
- package/dist/src/plugins/plugin-handle-tar.js +1 -1
- package/dist/src/plugins/plugin-handle-tar.js.map +1 -1
- package/dist/src/plugins/types.d.ts +10 -8
- package/dist/src/plugins/types.d.ts.map +1 -1
- package/dist/src/url-resolver.d.ts +21 -0
- package/dist/src/url-resolver.d.ts.map +1 -0
- package/dist/src/url-resolver.js +118 -0
- package/dist/src/url-resolver.js.map +1 -0
- package/dist/src/utils/byte-range-context.d.ts +1 -1
- package/dist/src/utils/content-type-parser.d.ts.map +1 -1
- package/dist/src/utils/content-type-parser.js +3 -0
- package/dist/src/utils/content-type-parser.js.map +1 -1
- package/dist/src/utils/get-content-type.d.ts +3 -3
- package/dist/src/utils/get-content-type.d.ts.map +1 -1
- package/dist/src/utils/get-content-type.js +1 -1
- package/dist/src/utils/get-content-type.js.map +1 -1
- package/dist/src/utils/get-e-tag.d.ts +1 -1
- package/dist/src/utils/get-offset-and-length.d.ts +6 -0
- package/dist/src/utils/get-offset-and-length.d.ts.map +1 -0
- package/dist/src/utils/get-offset-and-length.js +46 -0
- package/dist/src/utils/get-offset-and-length.js.map +1 -0
- package/dist/src/utils/get-resolved-accept-header.d.ts +2 -2
- package/dist/src/utils/get-resolved-accept-header.d.ts.map +1 -1
- package/dist/src/utils/handle-redirects.d.ts.map +1 -1
- package/dist/src/utils/handle-redirects.js +3 -3
- package/dist/src/utils/handle-redirects.js.map +1 -1
- package/dist/src/utils/ipfs-path-to-string.d.ts +6 -0
- package/dist/src/utils/ipfs-path-to-string.d.ts.map +1 -0
- package/dist/src/utils/ipfs-path-to-string.js +10 -0
- package/dist/src/utils/ipfs-path-to-string.js.map +1 -0
- package/dist/src/utils/is-accept-explicit.d.ts +6 -4
- package/dist/src/utils/is-accept-explicit.d.ts.map +1 -1
- package/dist/src/utils/is-accept-explicit.js +7 -4
- package/dist/src/utils/is-accept-explicit.js.map +1 -1
- package/dist/src/utils/parse-url-string.d.ts +1 -55
- package/dist/src/utils/parse-url-string.d.ts.map +1 -1
- package/dist/src/utils/parse-url-string.js +16 -217
- package/dist/src/utils/parse-url-string.js.map +1 -1
- package/dist/src/utils/response-headers.d.ts +1 -1
- package/dist/src/utils/response-headers.d.ts.map +1 -1
- package/dist/src/utils/responses.d.ts +1 -1
- package/dist/src/utils/select-output-type.d.ts +6 -2
- package/dist/src/utils/select-output-type.d.ts.map +1 -1
- package/dist/src/utils/select-output-type.js +28 -37
- package/dist/src/utils/select-output-type.js.map +1 -1
- package/dist/src/utils/server-timing.d.ts +5 -11
- package/dist/src/utils/server-timing.d.ts.map +1 -1
- package/dist/src/utils/server-timing.js +17 -15
- package/dist/src/utils/server-timing.js.map +1 -1
- package/dist/src/utils/walk-path.js +1 -1
- package/dist/src/utils/walk-path.js.map +1 -1
- package/dist/src/verified-fetch.d.ts +3 -10
- package/dist/src/verified-fetch.d.ts.map +1 -1
- package/dist/src/verified-fetch.js +68 -57
- package/dist/src/verified-fetch.js.map +1 -1
- package/dist/typedoc-urls.json +13 -2
- package/package.json +35 -36
- package/src/constants.ts +1 -0
- package/src/index.ts +73 -22
- package/src/plugins/plugin-handle-car.ts +54 -30
- package/src/plugins/plugin-handle-dag-cbor-html-preview.ts +2 -2
- package/src/plugins/plugin-handle-dag-cbor.ts +5 -5
- package/src/plugins/plugin-handle-dag-pb.ts +12 -12
- package/src/plugins/plugin-handle-dag-walk.ts +5 -4
- package/src/plugins/plugin-handle-ipns-record.ts +16 -19
- package/src/plugins/plugin-handle-json.ts +5 -4
- package/src/plugins/plugin-handle-raw.ts +21 -6
- package/src/plugins/plugin-handle-tar.ts +1 -1
- package/src/plugins/types.ts +12 -8
- package/src/url-resolver.ts +159 -0
- package/src/utils/byte-range-context.ts +1 -1
- package/src/utils/content-type-parser.ts +3 -0
- package/src/utils/get-content-type.ts +5 -4
- package/src/utils/get-e-tag.ts +1 -1
- package/src/utils/get-offset-and-length.ts +54 -0
- package/src/utils/get-resolved-accept-header.ts +2 -2
- package/src/utils/handle-redirects.ts +10 -3
- package/src/utils/ipfs-path-to-string.ts +9 -0
- package/src/utils/is-accept-explicit.ts +14 -7
- package/src/utils/parse-url-string.ts +20 -286
- package/src/utils/response-headers.ts +1 -1
- package/src/utils/responses.ts +1 -1
- package/src/utils/select-output-type.ts +38 -44
- package/src/utils/server-timing.ts +17 -30
- package/src/utils/walk-path.ts +1 -1
- package/src/verified-fetch.ts +78 -69
- package/dist/src/types.d.ts +0 -16
- package/dist/src/types.d.ts.map +0 -1
- package/dist/src/types.js +0 -2
- package/dist/src/types.js.map +0 -1
- package/dist/src/utils/parse-resource.d.ts +0 -18
- package/dist/src/utils/parse-resource.d.ts.map +0 -1
- package/dist/src/utils/parse-resource.js +0 -27
- package/dist/src/utils/parse-resource.js.map +0 -1
- package/src/types.ts +0 -17
- package/src/utils/parse-resource.ts +0 -42
|
@@ -55,8 +55,8 @@ export class DagPbPlugin extends BasePlugin {
|
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
async handle (context: PluginContext & Required<Pick<PluginContext, 'byteRangeContext' | 'pathDetails'>>): Promise<Response | null> {
|
|
58
|
-
const { cid, options,
|
|
59
|
-
const {
|
|
58
|
+
const { cid, options, pathDetails, query } = context
|
|
59
|
+
const { contentTypeParser, helia, getBlockstore } = this.pluginOptions
|
|
60
60
|
const log = this.log
|
|
61
61
|
let resource = context.resource
|
|
62
62
|
let path = context.path
|
|
@@ -93,10 +93,10 @@ export class DagPbPlugin extends BasePlugin {
|
|
|
93
93
|
try {
|
|
94
94
|
log.trace('found directory at %c/%s, looking for index.html', cid, path)
|
|
95
95
|
|
|
96
|
-
const entry = await
|
|
96
|
+
const entry = await context.serverTiming.time('exporter-dir', '', exporter(`/ipfs/${dirCid}/${rootFilePath}`, helia.blockstore, {
|
|
97
97
|
signal: options?.signal,
|
|
98
98
|
onProgress: options?.onProgress
|
|
99
|
-
})
|
|
99
|
+
}))
|
|
100
100
|
|
|
101
101
|
log.trace('found root file at %c/%s with cid %c', dirCid, rootFilePath, entry.cid)
|
|
102
102
|
path = rootFilePath
|
|
@@ -136,10 +136,10 @@ export class DagPbPlugin extends BasePlugin {
|
|
|
136
136
|
}
|
|
137
137
|
|
|
138
138
|
try {
|
|
139
|
-
const entry = await
|
|
139
|
+
const entry = await context.serverTiming.time('exporter-file', '', exporter(resolvedCID, helia.blockstore, {
|
|
140
140
|
signal: options?.signal,
|
|
141
141
|
onProgress: options?.onProgress
|
|
142
|
-
})
|
|
142
|
+
}))
|
|
143
143
|
|
|
144
144
|
let firstChunk: Uint8Array
|
|
145
145
|
let contentType: string
|
|
@@ -152,13 +152,13 @@ export class DagPbPlugin extends BasePlugin {
|
|
|
152
152
|
})
|
|
153
153
|
log('got async iterator for %c/%s', cid, path)
|
|
154
154
|
|
|
155
|
-
const streamAndFirstChunk = await
|
|
155
|
+
const streamAndFirstChunk = await context.serverTiming.time('stream-and-chunk', '', getStreamFromAsyncIterable(asyncIter, path ?? '', this.pluginOptions.logger, {
|
|
156
156
|
onProgress: options?.onProgress,
|
|
157
157
|
signal: options?.signal
|
|
158
|
-
})
|
|
158
|
+
}))
|
|
159
159
|
const stream = streamAndFirstChunk.stream
|
|
160
160
|
firstChunk = streamAndFirstChunk.firstChunk
|
|
161
|
-
contentType = await
|
|
161
|
+
contentType = await context.serverTiming.time('get-content-type', '', getContentType({ filename: query.filename, bytes: firstChunk, path, contentTypeParser, log }))
|
|
162
162
|
|
|
163
163
|
byteRangeContext.setBody(stream)
|
|
164
164
|
}
|
|
@@ -186,8 +186,8 @@ export class DagPbPlugin extends BasePlugin {
|
|
|
186
186
|
}
|
|
187
187
|
|
|
188
188
|
private async handleRangeRequest (context: PluginContext & Required<Pick<PluginContext, 'byteRangeContext' | 'pathDetails'>>, entry: UnixFSEntry): Promise<string> {
|
|
189
|
-
const { path, byteRangeContext, options
|
|
190
|
-
const {
|
|
189
|
+
const { path, byteRangeContext, options } = context
|
|
190
|
+
const { contentTypeParser } = this.pluginOptions
|
|
191
191
|
const log = this.log
|
|
192
192
|
|
|
193
193
|
// get the first chunk in order to determine the content type
|
|
@@ -203,7 +203,7 @@ export class DagPbPlugin extends BasePlugin {
|
|
|
203
203
|
onProgress: options?.onProgress,
|
|
204
204
|
signal: options?.signal
|
|
205
205
|
})
|
|
206
|
-
const contentType = await
|
|
206
|
+
const contentType = await context.serverTiming.time('get-content-type', '', getContentType({ bytes: firstChunk, path, contentTypeParser, log }))
|
|
207
207
|
|
|
208
208
|
byteRangeContext?.setBody((range): AsyncGenerator<Uint8Array, void, unknown> => {
|
|
209
209
|
if (options?.signal?.aborted) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { code as dagCborCode } from '@ipld/dag-cbor'
|
|
2
2
|
import { code as dagPbCode } from '@ipld/dag-pb'
|
|
3
|
+
import { CODEC_IDENTITY } from '../constants.ts'
|
|
3
4
|
import { handlePathWalking } from '../utils/walk-path.js'
|
|
4
5
|
import { BasePlugin } from './plugin-base.js'
|
|
5
6
|
import type { PluginContext } from './types.js'
|
|
@@ -23,16 +24,16 @@ export class DagWalkPlugin extends BasePlugin {
|
|
|
23
24
|
return false
|
|
24
25
|
}
|
|
25
26
|
|
|
26
|
-
return (cid.code === dagPbCode || cid.code === dagCborCode)
|
|
27
|
+
return (cid.code === dagPbCode || cid.code === dagCborCode || cid.multihash.code === CODEC_IDENTITY)
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
async handle (context: PluginContext): Promise<Response | null> {
|
|
30
|
-
const { cid, resource, options
|
|
31
|
-
const { getBlockstore
|
|
31
|
+
const { cid, resource, options } = context
|
|
32
|
+
const { getBlockstore } = this.pluginOptions
|
|
32
33
|
const blockstore = getBlockstore(cid, resource, options?.session ?? true, options)
|
|
33
34
|
|
|
34
35
|
// TODO: migrate handlePathWalking into this plugin
|
|
35
|
-
const pathDetails = await
|
|
36
|
+
const pathDetails = await context.serverTiming.time('path-walking', '', handlePathWalking({ ...context, blockstore, log: this.log }))
|
|
36
37
|
|
|
37
38
|
if (pathDetails instanceof Response) {
|
|
38
39
|
this.log.trace('path walking failed')
|
|
@@ -1,8 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { Key } from 'interface-datastore'
|
|
3
|
-
import { concat as uint8ArrayConcat } from 'uint8arrays/concat'
|
|
4
|
-
import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string'
|
|
5
|
-
import { toString as uint8ArrayToString } from 'uint8arrays/to-string'
|
|
1
|
+
import { marshalIPNSRecord } from 'ipns'
|
|
6
2
|
import { getPeerIdFromString } from '../utils/get-peer-id-from-string.js'
|
|
7
3
|
import { badRequestResponse, okRangeResponse } from '../utils/responses.js'
|
|
8
4
|
import { PluginFatalError } from './errors.js'
|
|
@@ -23,17 +19,19 @@ export class IpnsRecordPlugin extends BasePlugin {
|
|
|
23
19
|
return false
|
|
24
20
|
}
|
|
25
21
|
|
|
26
|
-
return accept === 'application/vnd.ipfs.ipns-record' || query.format === 'ipns-record'
|
|
22
|
+
return accept?.mimeType === 'application/vnd.ipfs.ipns-record' || query.format === 'ipns-record'
|
|
27
23
|
}
|
|
28
24
|
|
|
29
25
|
async handle (context: PluginContext & Required<Pick<PluginContext, 'byteRangeContext'>>): Promise<Response> {
|
|
30
|
-
const { resource, path, options } = context
|
|
31
|
-
const {
|
|
26
|
+
const { resource, path, query, options } = context
|
|
27
|
+
const { ipnsResolver } = this.pluginOptions
|
|
32
28
|
context.reqFormat = 'ipns-record'
|
|
29
|
+
|
|
33
30
|
if (path !== '' || !(resource.startsWith('ipns://') || resource.includes('.ipns.') || resource.includes('/ipns/'))) {
|
|
34
31
|
this.log.error('invalid request for IPNS name "%s" and path "%s"', resource, path)
|
|
35
32
|
throw new PluginFatalError('ERR_INVALID_IPNS_NAME', 'Invalid IPNS name', { response: badRequestResponse(resource, new Error('Invalid IPNS name')) })
|
|
36
33
|
}
|
|
34
|
+
|
|
37
35
|
let peerId: PeerId
|
|
38
36
|
|
|
39
37
|
try {
|
|
@@ -54,21 +52,20 @@ export class IpnsRecordPlugin extends BasePlugin {
|
|
|
54
52
|
throw new PluginFatalError('ERR_NO_PEER_ID_FOUND', 'could not parse peer id from url', { response: badRequestResponse(resource, err) })
|
|
55
53
|
}
|
|
56
54
|
|
|
57
|
-
//
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
const datastoreKey = new Key('/dht/record/' + uint8ArrayToString(routingKey, 'base32'), false)
|
|
65
|
-
const buf = await helia.datastore.get(datastoreKey, options)
|
|
66
|
-
const record = DHTRecord.deserialize(buf)
|
|
55
|
+
// force download in handleFinalResponse
|
|
56
|
+
query.filename = query.filename ?? `${peerId}.bin`
|
|
57
|
+
query.download = true
|
|
58
|
+
|
|
59
|
+
// @ts-expect-error progress handler types are incompatible
|
|
60
|
+
const result = await ipnsResolver.resolve(peerId, options)
|
|
61
|
+
const buf = marshalIPNSRecord(result.record)
|
|
67
62
|
|
|
68
|
-
context.byteRangeContext.setBody(
|
|
63
|
+
context.byteRangeContext.setBody(buf)
|
|
69
64
|
|
|
70
65
|
const response = okRangeResponse(resource, context.byteRangeContext.getBody('application/vnd.ipfs.ipns-record'), { byteRangeContext: context.byteRangeContext, log: this.log })
|
|
71
66
|
response.headers.set('content-type', context.byteRangeContext.getContentType() ?? 'application/vnd.ipfs.ipns-record')
|
|
67
|
+
response.headers.set('content-length', buf.byteLength.toString())
|
|
68
|
+
response.headers.set('x-ipfs-roots', result.cid.toV1().toString())
|
|
72
69
|
|
|
73
70
|
return response
|
|
74
71
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as ipldDagCbor from '@ipld/dag-cbor'
|
|
2
2
|
import * as ipldDagJson from '@ipld/dag-json'
|
|
3
|
+
import toBuffer from 'it-to-buffer'
|
|
3
4
|
import { code as jsonCode } from 'multiformats/codecs/json'
|
|
4
5
|
import { notAcceptableResponse, okRangeResponse } from '../utils/responses.js'
|
|
5
6
|
import { BasePlugin } from './plugin-base.js'
|
|
@@ -17,7 +18,7 @@ export class JsonPlugin extends BasePlugin {
|
|
|
17
18
|
return false
|
|
18
19
|
}
|
|
19
20
|
|
|
20
|
-
if (accept === 'application/vnd.ipld.dag-json' && cid.code !== ipldDagCbor.code) {
|
|
21
|
+
if (accept?.mimeType === 'application/vnd.ipld.dag-json' && cid.code !== ipldDagCbor.code) {
|
|
21
22
|
// we can handle application/vnd.ipld.dag-json, but if the CID codec is ipldDagCbor, DagCborPlugin should handle it
|
|
22
23
|
// TODO: remove the need for deny-listing cases in plugins
|
|
23
24
|
return true
|
|
@@ -35,10 +36,10 @@ export class JsonPlugin extends BasePlugin {
|
|
|
35
36
|
|
|
36
37
|
const terminalCid = context.pathDetails?.terminalElement.cid ?? context.cid
|
|
37
38
|
const blockstore = getBlockstore(terminalCid, resource, session, options)
|
|
38
|
-
const block = await blockstore.get(terminalCid, options)
|
|
39
|
+
const block = await toBuffer(blockstore.get(terminalCid, options))
|
|
39
40
|
let body: string | Uint8Array
|
|
40
41
|
|
|
41
|
-
if (accept === 'application/vnd.ipld.dag-cbor' || accept === 'application/cbor') {
|
|
42
|
+
if (accept?.mimeType === 'application/vnd.ipld.dag-cbor' || accept?.mimeType === 'application/cbor') {
|
|
42
43
|
try {
|
|
43
44
|
// if vnd.ipld.dag-cbor has been specified, convert to the format - note
|
|
44
45
|
// that this supports more data types than regular JSON, the content-type
|
|
@@ -62,7 +63,7 @@ export class JsonPlugin extends BasePlugin {
|
|
|
62
63
|
contentType = 'application/json'
|
|
63
64
|
}
|
|
64
65
|
} else {
|
|
65
|
-
contentType = accept.split(';')[0]
|
|
66
|
+
contentType = accept?.mimeType.split(';')[0]
|
|
66
67
|
}
|
|
67
68
|
|
|
68
69
|
context.byteRangeContext.setBody(body)
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import toBuffer from 'it-to-buffer'
|
|
1
2
|
import { code as rawCode } from 'multiformats/codecs/raw'
|
|
2
3
|
import { identity } from 'multiformats/hashes/identity'
|
|
3
4
|
import { getContentType } from '../utils/get-content-type.js'
|
|
@@ -5,6 +6,7 @@ import { notFoundResponse, okRangeResponse } from '../utils/responses.js'
|
|
|
5
6
|
import { PluginFatalError } from './errors.js'
|
|
6
7
|
import { BasePlugin } from './plugin-base.js'
|
|
7
8
|
import type { PluginContext } from './types.js'
|
|
9
|
+
import type { AcceptHeader } from '../utils/select-output-type.ts'
|
|
8
10
|
|
|
9
11
|
/**
|
|
10
12
|
* These are Accept header values that will cause content type sniffing to be
|
|
@@ -22,9 +24,9 @@ const RAW_HEADERS = [
|
|
|
22
24
|
* type. This avoids the user from receiving something different when they
|
|
23
25
|
* signal that they want to `Accept` a specific mime type.
|
|
24
26
|
*/
|
|
25
|
-
function getOverriddenRawContentType ({ headers, accept }: { headers?: HeadersInit, accept?:
|
|
27
|
+
function getOverriddenRawContentType ({ headers, accept }: { headers?: HeadersInit, accept?: AcceptHeader }): string | undefined {
|
|
26
28
|
// accept has already been resolved by getResolvedAcceptHeader, if we have it, use it.
|
|
27
|
-
const acceptHeader = accept ?? new Headers(headers).get('accept') ?? ''
|
|
29
|
+
const acceptHeader = accept?.mimeType ?? new Headers(headers).get('accept') ?? ''
|
|
28
30
|
|
|
29
31
|
// e.g. "Accept: text/html, application/xhtml+xml, application/xml;q=0.9, image/webp, */*;q=0.8"
|
|
30
32
|
const acceptHeaders = acceptHeader.split(',')
|
|
@@ -51,7 +53,7 @@ export class RawPlugin extends BasePlugin {
|
|
|
51
53
|
if (byteRangeContext == null) {
|
|
52
54
|
return false
|
|
53
55
|
}
|
|
54
|
-
return accept === 'application/vnd.ipld.raw' || query.format === 'raw'
|
|
56
|
+
return accept?.mimeType === 'application/vnd.ipld.raw' || query.format === 'raw'
|
|
55
57
|
}
|
|
56
58
|
|
|
57
59
|
async handle (context: PluginContext & Required<Pick<PluginContext, 'byteRangeContext'>>): Promise<Response> {
|
|
@@ -60,7 +62,7 @@ export class RawPlugin extends BasePlugin {
|
|
|
60
62
|
const session = options?.session ?? true
|
|
61
63
|
const log = this.log
|
|
62
64
|
|
|
63
|
-
if (accept === 'application/vnd.ipld.raw' || query.format === 'raw') {
|
|
65
|
+
if (accept?.mimeType === 'application/vnd.ipld.raw' || query.format === 'raw') {
|
|
64
66
|
context.reqFormat = 'raw'
|
|
65
67
|
context.query.download = true
|
|
66
68
|
context.query.filename = context.query.filename ?? `${cid.toString()}.bin`
|
|
@@ -78,18 +80,31 @@ export class RawPlugin extends BasePlugin {
|
|
|
78
80
|
|
|
79
81
|
const terminalCid = context.pathDetails?.terminalElement.cid ?? context.cid
|
|
80
82
|
const blockstore = getBlockstore(terminalCid, resource, session, options)
|
|
81
|
-
const result = await blockstore.get(terminalCid, options)
|
|
83
|
+
const result = await toBuffer(blockstore.get(terminalCid, options))
|
|
82
84
|
context.byteRangeContext.setBody(result)
|
|
83
85
|
|
|
84
86
|
// if the user has specified an `Accept` header that corresponds to a raw
|
|
85
87
|
// type, honour that header, so for example they don't request
|
|
86
88
|
// `application/vnd.ipld.raw` but get `application/octet-stream`
|
|
87
|
-
const contentType = await getContentType({
|
|
89
|
+
const contentType = await getContentType({
|
|
90
|
+
filename: query.filename,
|
|
91
|
+
bytes: result,
|
|
92
|
+
path,
|
|
93
|
+
defaultContentType: getOverriddenRawContentType({ headers: options?.headers, accept }),
|
|
94
|
+
contentTypeParser,
|
|
95
|
+
log
|
|
96
|
+
})
|
|
88
97
|
const response = okRangeResponse(resource, context.byteRangeContext.getBody(contentType), { byteRangeContext: context.byteRangeContext, log }, {
|
|
89
98
|
redirected: false
|
|
90
99
|
})
|
|
91
100
|
|
|
92
101
|
response.headers.set('content-type', context.byteRangeContext.getContentType() ?? contentType)
|
|
102
|
+
response.headers.set('x-ipfs-roots', terminalCid.toV1().toString())
|
|
103
|
+
|
|
104
|
+
// only set content-length if it is not a range request
|
|
105
|
+
if (!context.byteRangeContext.isRangeRequest) {
|
|
106
|
+
response.headers.set('content-length', result.byteLength.toString())
|
|
107
|
+
}
|
|
93
108
|
|
|
94
109
|
return response
|
|
95
110
|
}
|
|
@@ -19,7 +19,7 @@ export class TarPlugin extends BasePlugin {
|
|
|
19
19
|
if (byteRangeContext == null) {
|
|
20
20
|
return false
|
|
21
21
|
}
|
|
22
|
-
return accept === 'application/x-tar' || query.format === 'tar'
|
|
22
|
+
return accept?.mimeType === 'application/x-tar' || query.format === 'tar'
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
async handle (context: PluginContext & Required<Pick<PluginContext, 'byteRangeContext'>>): Promise<Response> {
|
package/src/plugins/types.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import type { PluginError } from './errors.js'
|
|
2
|
-
import type { VerifiedFetchInit } from '../index.js'
|
|
3
|
-
import type { ContentTypeParser, RequestFormatShorthand } from '../types.js'
|
|
2
|
+
import type { ResolveURLResult, UrlQuery, VerifiedFetchInit, ContentTypeParser, RequestFormatShorthand } from '../index.js'
|
|
4
3
|
import type { ByteRangeContext } from '../utils/byte-range-context.js'
|
|
5
|
-
import type {
|
|
4
|
+
import type { AcceptHeader } from '../utils/select-output-type.ts'
|
|
5
|
+
import type { ServerTiming } from '../utils/server-timing.ts'
|
|
6
6
|
import type { PathWalkerResponse } from '../utils/walk-path.js'
|
|
7
|
+
import type { IPNSResolver } from '@helia/ipns'
|
|
7
8
|
import type { AbortOptions, ComponentLogger, Logger } from '@libp2p/interface'
|
|
8
9
|
import type { Helia } from 'helia'
|
|
9
10
|
import type { Blockstore } from 'interface-blockstore'
|
|
@@ -19,9 +20,9 @@ import type { CustomProgressEvent } from 'progress-events'
|
|
|
19
20
|
export interface PluginOptions {
|
|
20
21
|
logger: ComponentLogger
|
|
21
22
|
getBlockstore(cid: CID, resource: string | CID, useSession?: boolean, options?: AbortOptions): Blockstore
|
|
22
|
-
handleServerTiming<T>(name: string, description: string, fn: () => Promise<T>, withServerTiming: boolean): Promise<T>
|
|
23
23
|
contentTypeParser?: ContentTypeParser
|
|
24
24
|
helia: Helia
|
|
25
|
+
ipnsResolver: IPNSResolver
|
|
25
26
|
}
|
|
26
27
|
|
|
27
28
|
/**
|
|
@@ -30,22 +31,22 @@ export interface PluginOptions {
|
|
|
30
31
|
* - Shared Data: Allows plugins to communicate partial results, discovered data, or interim errors.
|
|
31
32
|
* - Ephemeral: Typically discarded once fetch(...) completes.
|
|
32
33
|
*/
|
|
33
|
-
export interface PluginContext extends
|
|
34
|
+
export interface PluginContext extends ResolveURLResult {
|
|
34
35
|
readonly cid: CID
|
|
35
36
|
readonly path: string
|
|
36
37
|
readonly resource: string
|
|
37
|
-
readonly accept?:
|
|
38
|
+
readonly accept?: AcceptHeader
|
|
38
39
|
|
|
39
40
|
/**
|
|
40
41
|
* An array of plugin IDs that are all enabled. You can use this to check if a plugin is enabled and respond accordingly.
|
|
41
42
|
*/
|
|
42
43
|
plugins: string[]
|
|
44
|
+
|
|
43
45
|
/**
|
|
44
46
|
* The last time the context is modified, so we know whether a plugin has modified it.
|
|
45
47
|
* A plugin should increment this value if it modifies the context.
|
|
46
48
|
*/
|
|
47
49
|
modified: number
|
|
48
|
-
withServerTiming?: boolean
|
|
49
50
|
onProgress?(evt: CustomProgressEvent<any>): void
|
|
50
51
|
options?: Omit<VerifiedFetchInit, 'signal'> & AbortOptions
|
|
51
52
|
isDirectory?: boolean
|
|
@@ -53,7 +54,8 @@ export interface PluginContext extends ParsedUrlStringResults {
|
|
|
53
54
|
errors?: PluginError[]
|
|
54
55
|
reqFormat?: RequestFormatShorthand
|
|
55
56
|
pathDetails?: PathWalkerResponse
|
|
56
|
-
query:
|
|
57
|
+
query: UrlQuery
|
|
58
|
+
|
|
57
59
|
/**
|
|
58
60
|
* ByteRangeContext contains information about the size of the content and range requests.
|
|
59
61
|
* This can be used to set the Content-Length header without loading the entire body.
|
|
@@ -61,6 +63,8 @@ export interface PluginContext extends ParsedUrlStringResults {
|
|
|
61
63
|
* This is set by the ByteRangeContextPlugin
|
|
62
64
|
*/
|
|
63
65
|
byteRangeContext?: ByteRangeContext
|
|
66
|
+
serverTiming: ServerTiming
|
|
67
|
+
ipfsPath: string
|
|
64
68
|
[key: string]: unknown
|
|
65
69
|
}
|
|
66
70
|
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { peerIdFromCID, peerIdFromString } from '@libp2p/peer-id'
|
|
2
|
+
import { CID } from 'multiformats/cid'
|
|
3
|
+
import { matchURLString } from './utils/parse-url-string.ts'
|
|
4
|
+
import type { ResolveURLOptions, ResolveURLResult, Resource, URLResolver as URLResolverInterface } from './index.ts'
|
|
5
|
+
import type { ServerTiming } from './utils/server-timing.ts'
|
|
6
|
+
import type { DNSLink } from '@helia/dnslink'
|
|
7
|
+
import type { IPNSResolver } from '@helia/ipns'
|
|
8
|
+
import type { AbortOptions, PeerId } from '@libp2p/interface'
|
|
9
|
+
|
|
10
|
+
const CODEC_LIBP2P_KEY = 0x72
|
|
11
|
+
|
|
12
|
+
export interface URLResolverComponents {
|
|
13
|
+
ipnsResolver: IPNSResolver
|
|
14
|
+
dnsLink: DNSLink
|
|
15
|
+
timing: ServerTiming
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function toQuery (query?: string): Record<string, any> {
|
|
19
|
+
if (query == null) {
|
|
20
|
+
return {}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const params = new URLSearchParams(query)
|
|
24
|
+
const output: Record<string, any> = {}
|
|
25
|
+
|
|
26
|
+
for (const [key, value] of params.entries()) {
|
|
27
|
+
output[key] = value
|
|
28
|
+
|
|
29
|
+
if (value === 'true') {
|
|
30
|
+
output[key] = true
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (value === 'false') {
|
|
34
|
+
output[key] = false
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return output
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export class URLResolver implements URLResolverInterface {
|
|
42
|
+
private readonly components: URLResolverComponents
|
|
43
|
+
|
|
44
|
+
constructor (components: URLResolverComponents) {
|
|
45
|
+
this.components = components
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async resolve (resource: Resource, options: ResolveURLOptions = {}): Promise<ResolveURLResult> {
|
|
49
|
+
if (typeof resource === 'string') {
|
|
50
|
+
return this.parseUrlString(resource, options)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const cid = CID.asCID(resource)
|
|
54
|
+
|
|
55
|
+
if (cid != null) {
|
|
56
|
+
return this.resolveCIDResource(cid, '', {}, options)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
throw new TypeError(`Invalid resource. Cannot determine CID from resource: ${resource}`)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async parseUrlString (urlString: string, options: ResolveURLOptions = {}): Promise<ResolveURLResult> {
|
|
63
|
+
const { protocol, cidOrPeerIdOrDnsLink, path, query } = matchURLString(urlString)
|
|
64
|
+
|
|
65
|
+
if (protocol === 'ipfs') {
|
|
66
|
+
const cid = CID.parse(cidOrPeerIdOrDnsLink)
|
|
67
|
+
|
|
68
|
+
return this.resolveCIDResource(cid, path ?? '', toQuery(query), options)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (protocol === 'ipns') {
|
|
72
|
+
// try to parse target as peer id
|
|
73
|
+
let peerId: PeerId
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
peerId = peerIdFromString(cidOrPeerIdOrDnsLink)
|
|
77
|
+
} catch {
|
|
78
|
+
// fall back to DNSLink (e.g. /ipns/example.com)
|
|
79
|
+
return this.resolveDNSLink(cidOrPeerIdOrDnsLink, path ?? '', toQuery(query), options)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// parse multihash from string (e.g. /ipns/QmFoo...)
|
|
83
|
+
return this.resolveIPNSName(cidOrPeerIdOrDnsLink, peerId, path ?? '', toQuery(query), options)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
throw new TypeError(`Invalid resource. Cannot determine CID from resource: ${urlString}`)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async resolveCIDResource (cid: CID, path: string, query: Record<string, any>, options: ResolveURLOptions = {}): Promise<ResolveURLResult> {
|
|
90
|
+
if (cid.code === CODEC_LIBP2P_KEY) {
|
|
91
|
+
// special case - peer id encoded as a CID
|
|
92
|
+
return this.resolveIPNSName(cid.toString(), peerIdFromCID(cid), path, query, options)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
cid,
|
|
97
|
+
protocol: 'ipfs',
|
|
98
|
+
query,
|
|
99
|
+
path,
|
|
100
|
+
ttl: 29030400, // 1 year for ipfs content
|
|
101
|
+
ipfsPath: `/ipfs/${cid}${path === '' ? '' : `/${path}`}`
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async resolveDNSLink (domain: string, path: string, query: Record<string, any>, options?: ResolveURLOptions): Promise<ResolveURLResult> {
|
|
106
|
+
const results = await this.components.timing.time('dnsLink.resolve', `Resolve DNSLink ${domain}`, this.components.dnsLink.resolve(domain, options))
|
|
107
|
+
const result = results?.[0]
|
|
108
|
+
|
|
109
|
+
if (result == null) {
|
|
110
|
+
throw new TypeError(`Invalid resource. Cannot resolve DNSLink from domain: ${domain}`)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// dnslink resolved to IPNS name
|
|
114
|
+
if (result.namespace === 'ipns') {
|
|
115
|
+
return this.resolveIPNSName(domain, result.peerId, path, query, options)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// dnslink resolved to CID
|
|
119
|
+
if (result.namespace !== 'ipfs') {
|
|
120
|
+
// @ts-expect-error result namespace should only be ipns or ipfs
|
|
121
|
+
throw new TypeError(`Invalid resource. Unexpected DNSLink namespace ${result.namespace} from domain: ${domain}`)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
cid: result.cid,
|
|
126
|
+
path: concatPaths(result.path, path),
|
|
127
|
+
// dnslink is mutable so return 'ipns' protocol so we do not include immutable in cache-control header
|
|
128
|
+
protocol: 'ipns',
|
|
129
|
+
ttl: result.answer.TTL,
|
|
130
|
+
query,
|
|
131
|
+
ipfsPath: `/ipns/${domain}${path === '' ? '' : `/${path}`}`
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async resolveIPNSName (resource: string, key: PeerId, path: string, query: Record<string, any>, options?: AbortOptions): Promise<ResolveURLResult> {
|
|
136
|
+
const result = await this.components.timing.time('ipns.resolve', `Resolve IPNS name ${key}`, this.components.ipnsResolver.resolve(key, options))
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
cid: result.cid,
|
|
140
|
+
path: concatPaths(result.path, path),
|
|
141
|
+
query,
|
|
142
|
+
protocol: 'ipns',
|
|
143
|
+
// IPNS ttl is in nanoseconds, convert to seconds
|
|
144
|
+
ttl: Number((result.record.ttl ?? 0n) / BigInt(1e9)),
|
|
145
|
+
ipfsPath: `/ipns/${resource}${path === '' ? '' : `/${path}`}`
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function concatPaths (...paths: Array<string | undefined>): string {
|
|
151
|
+
return `${
|
|
152
|
+
paths
|
|
153
|
+
.filter(p => p != null && p !== '')
|
|
154
|
+
.join('/')
|
|
155
|
+
.replaceAll(/(\/+)/g, '/')
|
|
156
|
+
.replace(/^(\/)+/, '')
|
|
157
|
+
.replace(/(\/)+$/, '/')
|
|
158
|
+
}`
|
|
159
|
+
}
|
|
@@ -2,7 +2,7 @@ import toBrowserReadableStream from 'it-to-browser-readablestream'
|
|
|
2
2
|
import { InvalidRangeError } from '../errors.js'
|
|
3
3
|
import { calculateByteRangeIndexes, getHeader } from './request-headers.js'
|
|
4
4
|
import { getContentRangeHeader } from './response-headers.js'
|
|
5
|
-
import type { SupportedBodyTypes } from '../
|
|
5
|
+
import type { SupportedBodyTypes } from '../index.js'
|
|
6
6
|
import type { ComponentLogger, Logger } from '@libp2p/interface'
|
|
7
7
|
|
|
8
8
|
type SliceableBody = Exclude<SupportedBodyTypes, ReadableStream<Uint8Array> | null>
|
|
@@ -40,6 +40,9 @@ export async function contentTypeParser (bytes: Uint8Array, fileName?: string):
|
|
|
40
40
|
const detectedType = (await fileTypeFromBuffer(bytes))?.mime
|
|
41
41
|
if (detectedType != null) {
|
|
42
42
|
log('detectedType: %s', detectedType)
|
|
43
|
+
if (detectedType === 'application/xml' && fileName?.toLowerCase().endsWith('.svg')) {
|
|
44
|
+
return 'image/svg+xml'
|
|
45
|
+
}
|
|
43
46
|
return detectedType
|
|
44
47
|
}
|
|
45
48
|
log('no detectedType')
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { defaultMimeType } from './content-type-parser.js'
|
|
2
2
|
import { isPromise } from './type-guards.js'
|
|
3
|
-
import type { ContentTypeParser } from '../
|
|
3
|
+
import type { ContentTypeParser } from '../index.js'
|
|
4
4
|
import type { Logger } from '@libp2p/interface'
|
|
5
5
|
|
|
6
6
|
export interface GetContentTypeOptions {
|
|
7
7
|
bytes: Uint8Array
|
|
8
|
-
path
|
|
8
|
+
path?: string
|
|
9
9
|
defaultContentType?: string
|
|
10
|
-
contentTypeParser
|
|
10
|
+
contentTypeParser?: ContentTypeParser
|
|
11
11
|
log: Logger
|
|
12
12
|
|
|
13
13
|
/**
|
|
@@ -25,7 +25,7 @@ export async function getContentType ({ bytes, path, contentTypeParser, log, def
|
|
|
25
25
|
try {
|
|
26
26
|
let fileName
|
|
27
27
|
if (filenameParam == null) {
|
|
28
|
-
fileName = path
|
|
28
|
+
fileName = path?.split('/').pop()?.trim()
|
|
29
29
|
fileName = (fileName === '' || fileName?.split('.').length === 1) ? undefined : fileName
|
|
30
30
|
} else {
|
|
31
31
|
fileName = filenameParam
|
|
@@ -46,6 +46,7 @@ export async function getContentType ({ bytes, path, contentTypeParser, log, def
|
|
|
46
46
|
log.error('error parsing content type', err)
|
|
47
47
|
}
|
|
48
48
|
}
|
|
49
|
+
|
|
49
50
|
if (contentType === defaultMimeType) {
|
|
50
51
|
// if the content type is the default in our content-type-parser, instead, set it to the default content type provided to this function.
|
|
51
52
|
contentType = defaultContentType
|
package/src/utils/get-e-tag.ts
CHANGED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { UnixFSEntry } from 'ipfs-unixfs-exporter'
|
|
2
|
+
|
|
3
|
+
export function getOffsetAndLength (entry: UnixFSEntry, entityBytes?: string): { offset: number, length: number } {
|
|
4
|
+
if (entityBytes == null) {
|
|
5
|
+
return {
|
|
6
|
+
offset: 0,
|
|
7
|
+
length: Infinity
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const parts = entityBytes.split(':')
|
|
12
|
+
const start = parseInt(parts[0], 10)
|
|
13
|
+
const end = parts[1] === '*' ? Infinity : parseInt(parts[1], 10)
|
|
14
|
+
|
|
15
|
+
if (isNaN(start) || isNaN(end)) {
|
|
16
|
+
throw new Error('Could not parse entity-bytes')
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const entrySize = Number(entry.size)
|
|
20
|
+
|
|
21
|
+
if (start >= 0) {
|
|
22
|
+
if (end >= 0) {
|
|
23
|
+
return {
|
|
24
|
+
offset: start,
|
|
25
|
+
length: end - start
|
|
26
|
+
}
|
|
27
|
+
} else {
|
|
28
|
+
return {
|
|
29
|
+
offset: start,
|
|
30
|
+
length: (entrySize - start) + end
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// start < 0
|
|
36
|
+
let offset = entrySize + start
|
|
37
|
+
|
|
38
|
+
if (Math.abs(start) > entrySize) {
|
|
39
|
+
offset = 0
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (end >= 0) {
|
|
43
|
+
return {
|
|
44
|
+
offset,
|
|
45
|
+
length: (entrySize - offset) + end
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// end < 0
|
|
50
|
+
return {
|
|
51
|
+
offset,
|
|
52
|
+
length: (entrySize - offset) + end
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { isExplicitAcceptHeader, isExplicitFormatQuery, isExplicitIpldAcceptRequest } from './is-accept-explicit.js'
|
|
2
2
|
import { queryFormatToAcceptHeader } from './select-output-type.js'
|
|
3
|
-
import type {
|
|
3
|
+
import type { UrlQuery } from '../index.ts'
|
|
4
4
|
import type { ComponentLogger } from '@libp2p/interface'
|
|
5
5
|
|
|
6
6
|
export interface ResolvedAcceptHeaderOptions {
|
|
7
|
-
query?:
|
|
7
|
+
query?: UrlQuery
|
|
8
8
|
headers?: RequestInit['headers']
|
|
9
9
|
logger: ComponentLogger
|
|
10
10
|
}
|