@helia/verified-fetch 3.2.3 → 4.0.1
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 +10 -52
- package/dist/index.min.js +86 -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 +63 -61
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +12 -54
- package/dist/src/index.js.map +1 -1
- package/dist/src/plugins/index.d.ts +0 -1
- package/dist/src/plugins/index.d.ts.map +1 -1
- package/dist/src/plugins/index.js +0 -1
- package/dist/src/plugins/index.js.map +1 -1
- package/dist/src/plugins/plugin-base.d.ts.map +1 -1
- package/dist/src/plugins/plugin-base.js +3 -2
- package/dist/src/plugins/plugin-base.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 -28
- 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 -2
- package/dist/src/plugins/plugin-handle-dag-cbor-html-preview.js.map +1 -1
- package/dist/src/plugins/plugin-handle-dag-cbor.d.ts.map +1 -1
- package/dist/src/plugins/plugin-handle-dag-cbor.js +5 -6
- package/dist/src/plugins/plugin-handle-dag-cbor.js.map +1 -1
- package/dist/src/plugins/plugin-handle-dag-pb.d.ts.map +1 -1
- package/dist/src/plugins/plugin-handle-dag-pb.js +24 -27
- package/dist/src/plugins/plugin-handle-dag-pb.js.map +1 -1
- package/dist/src/plugins/plugin-handle-dag-walk.d.ts +8 -4
- package/dist/src/plugins/plugin-handle-dag-walk.d.ts.map +1 -1
- package/dist/src/plugins/plugin-handle-dag-walk.js +13 -9
- package/dist/src/plugins/plugin-handle-dag-walk.js.map +1 -1
- package/dist/src/plugins/plugin-handle-ipns-record.d.ts +1 -1
- package/dist/src/plugins/plugin-handle-ipns-record.d.ts.map +1 -1
- package/dist/src/plugins/plugin-handle-ipns-record.js +16 -24
- 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 -5
- 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 +21 -12
- package/dist/src/plugins/plugin-handle-raw.js.map +1 -1
- package/dist/src/plugins/plugin-handle-tar.d.ts.map +1 -1
- package/dist/src/plugins/plugin-handle-tar.js +1 -2
- package/dist/src/plugins/plugin-handle-tar.js.map +1 -1
- package/dist/src/plugins/types.d.ts +15 -15
- 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 +3 -3
- package/dist/src/utils/byte-range-context.d.ts.map +1 -1
- package/dist/src/utils/byte-range-context.js +1 -1
- package/dist/src/utils/byte-range-context.js.map +1 -1
- package/dist/src/utils/content-type-parser.d.ts.map +1 -1
- package/dist/src/utils/content-type-parser.js +0 -10
- package/dist/src/utils/content-type-parser.js.map +1 -1
- package/dist/src/utils/error-to-object.d.ts +6 -0
- package/dist/src/utils/error-to-object.d.ts.map +1 -0
- package/dist/src/utils/error-to-object.js +20 -0
- package/dist/src/utils/error-to-object.js.map +1 -0
- 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/get-stream-from-async-iterable.d.ts +2 -2
- package/dist/src/utils/get-stream-from-async-iterable.d.ts.map +1 -1
- package/dist/src/utils/get-stream-from-async-iterable.js +2 -2
- package/dist/src/utils/get-stream-from-async-iterable.js.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 +3 -2
- package/dist/src/utils/responses.d.ts.map +1 -1
- package/dist/src/utils/responses.js +12 -1
- package/dist/src/utils/responses.js.map +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 +2 -2
- 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 +99 -80
- package/dist/src/verified-fetch.js.map +1 -1
- package/dist/typedoc-urls.json +13 -4
- package/package.json +35 -36
- package/src/constants.ts +1 -0
- package/src/index.ts +79 -70
- package/src/plugins/index.ts +0 -1
- package/src/plugins/plugin-base.ts +3 -2
- package/src/plugins/plugin-handle-car.ts +53 -31
- package/src/plugins/plugin-handle-dag-cbor-html-preview.ts +4 -3
- package/src/plugins/plugin-handle-dag-cbor.ts +8 -6
- package/src/plugins/plugin-handle-dag-pb.ts +34 -26
- package/src/plugins/plugin-handle-dag-walk.ts +15 -9
- package/src/plugins/plugin-handle-ipns-record.ts +21 -24
- package/src/plugins/plugin-handle-json.ts +6 -5
- package/src/plugins/plugin-handle-raw.ts +27 -13
- package/src/plugins/plugin-handle-tar.ts +3 -2
- package/src/plugins/types.ts +18 -16
- package/src/url-resolver.ts +159 -0
- package/src/utils/byte-range-context.ts +4 -4
- package/src/utils/content-type-parser.ts +5 -11
- package/src/utils/error-to-object.ts +22 -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/get-stream-from-async-iterable.ts +4 -4
- 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 +16 -2
- package/src/utils/select-output-type.ts +38 -44
- package/src/utils/server-timing.ts +17 -30
- package/src/utils/walk-path.ts +2 -2
- package/src/verified-fetch.ts +119 -92
- package/dist/src/plugins/errors.d.ts +0 -25
- package/dist/src/plugins/errors.d.ts.map +0 -1
- package/dist/src/plugins/errors.js +0 -33
- package/dist/src/plugins/errors.js.map +0 -1
- 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/plugins/errors.ts +0 -37
- package/src/types.ts +0 -17
- package/src/utils/parse-resource.ts +0 -42
|
@@ -3,7 +3,7 @@ import { code as dagJsonCode } from '@ipld/dag-json'
|
|
|
3
3
|
import { code as dagPbCode } from '@ipld/dag-pb'
|
|
4
4
|
import { code as jsonCode } from 'multiformats/codecs/json'
|
|
5
5
|
import { code as rawCode } from 'multiformats/codecs/raw'
|
|
6
|
-
import type { RequestFormatShorthand } from '../
|
|
6
|
+
import type { RequestFormatShorthand } from '../index.js'
|
|
7
7
|
import type { CID } from 'multiformats/cid'
|
|
8
8
|
|
|
9
9
|
/**
|
|
@@ -62,10 +62,15 @@ const CID_TYPE_MAP: Record<number, string[]> = {
|
|
|
62
62
|
]
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
export interface AcceptHeader {
|
|
66
|
+
mimeType: string
|
|
67
|
+
options: Record<string, string>
|
|
68
|
+
}
|
|
69
|
+
|
|
65
70
|
/**
|
|
66
71
|
* Selects an output mime-type based on the CID and a passed `Accept` header
|
|
67
72
|
*/
|
|
68
|
-
export function selectOutputType (cid: CID, accept?: string):
|
|
73
|
+
export function selectOutputType (cid: CID, accept?: string): AcceptHeader | undefined {
|
|
69
74
|
const cidMimeTypes = CID_TYPE_MAP[cid.code]
|
|
70
75
|
|
|
71
76
|
if (accept != null) {
|
|
@@ -73,80 +78,69 @@ export function selectOutputType (cid: CID, accept?: string): string | undefined
|
|
|
73
78
|
}
|
|
74
79
|
}
|
|
75
80
|
|
|
76
|
-
function chooseMimeType (accept: string, validMimeTypes: string[]):
|
|
81
|
+
function chooseMimeType (accept: string, validMimeTypes: string[]): AcceptHeader | undefined {
|
|
77
82
|
const requestedMimeTypes = accept
|
|
78
83
|
.split(',')
|
|
79
84
|
.map(s => {
|
|
80
85
|
const parts = s.trim().split(';')
|
|
81
86
|
|
|
87
|
+
const options: Record<string, string> = {
|
|
88
|
+
q: '0'
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
for (let i = 1; i < parts.length; i++) {
|
|
92
|
+
const [key, value] = parts[i].split('=').map(s => s.trim())
|
|
93
|
+
|
|
94
|
+
options[key] = value
|
|
95
|
+
}
|
|
96
|
+
|
|
82
97
|
return {
|
|
83
98
|
mimeType: `${parts[0]}`.trim(),
|
|
84
|
-
|
|
99
|
+
options
|
|
85
100
|
}
|
|
86
101
|
})
|
|
87
102
|
.sort((a, b) => {
|
|
88
|
-
if (a.
|
|
103
|
+
if (a.options.q === b.options.q) {
|
|
89
104
|
return 0
|
|
90
105
|
}
|
|
91
106
|
|
|
92
|
-
if (a.
|
|
107
|
+
if (a.options.q > b.options.q) {
|
|
93
108
|
return -1
|
|
94
109
|
}
|
|
95
110
|
|
|
96
111
|
return 1
|
|
97
112
|
})
|
|
98
|
-
.map(s => s.mimeType)
|
|
99
113
|
|
|
100
114
|
for (const headerFormat of requestedMimeTypes) {
|
|
101
115
|
for (const mimeType of validMimeTypes) {
|
|
102
|
-
if (headerFormat.includes(mimeType)) {
|
|
103
|
-
return
|
|
116
|
+
if (headerFormat.mimeType.includes(mimeType)) {
|
|
117
|
+
return headerFormat
|
|
104
118
|
}
|
|
105
119
|
|
|
106
|
-
if (headerFormat === '*/*') {
|
|
107
|
-
return
|
|
120
|
+
if (headerFormat.mimeType === '*/*') {
|
|
121
|
+
return {
|
|
122
|
+
mimeType,
|
|
123
|
+
options: headerFormat.options
|
|
124
|
+
}
|
|
108
125
|
}
|
|
109
126
|
|
|
110
|
-
if (headerFormat.startsWith('*/') && mimeType.split('/')[1] === headerFormat.split('/')[1]) {
|
|
111
|
-
return
|
|
127
|
+
if (headerFormat.mimeType.startsWith('*/') && mimeType.split('/')[1] === headerFormat.mimeType.split('/')[1]) {
|
|
128
|
+
return {
|
|
129
|
+
mimeType,
|
|
130
|
+
options: headerFormat.options
|
|
131
|
+
}
|
|
112
132
|
}
|
|
113
133
|
|
|
114
|
-
if (headerFormat.endsWith('/*') && mimeType.split('/')[0] === headerFormat.split('/')[0]) {
|
|
115
|
-
return
|
|
134
|
+
if (headerFormat.mimeType.endsWith('/*') && mimeType.split('/')[0] === headerFormat.mimeType.split('/')[0]) {
|
|
135
|
+
return {
|
|
136
|
+
mimeType,
|
|
137
|
+
options: headerFormat.options
|
|
138
|
+
}
|
|
116
139
|
}
|
|
117
140
|
}
|
|
118
141
|
}
|
|
119
142
|
}
|
|
120
143
|
|
|
121
|
-
/**
|
|
122
|
-
* Parses q-factor weighting from the accept header to allow letting some mime
|
|
123
|
-
* types take precedence over others.
|
|
124
|
-
*
|
|
125
|
-
* If the q-factor for an acceptable mime representation is omitted it defaults
|
|
126
|
-
* to `1`.
|
|
127
|
-
*
|
|
128
|
-
* All specified values should be in the range 0-1.
|
|
129
|
-
*
|
|
130
|
-
* @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept#q
|
|
131
|
-
*/
|
|
132
|
-
function parseQFactor (str?: string): number {
|
|
133
|
-
if (str != null) {
|
|
134
|
-
str = str.trim()
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
if (str?.startsWith('q=') !== true) {
|
|
138
|
-
return 1
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
const factor = parseFloat(str.replace('q=', ''))
|
|
142
|
-
|
|
143
|
-
if (isNaN(factor)) {
|
|
144
|
-
return 0
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
return factor
|
|
148
|
-
}
|
|
149
|
-
|
|
150
144
|
export const FORMAT_TO_MIME_TYPE: Record<RequestFormatShorthand, string> = {
|
|
151
145
|
raw: 'application/vnd.ipld.raw',
|
|
152
146
|
car: 'application/vnd.ipld.car',
|
|
@@ -1,37 +1,24 @@
|
|
|
1
|
-
export
|
|
2
|
-
|
|
3
|
-
result: T
|
|
4
|
-
header: string
|
|
5
|
-
}
|
|
6
|
-
export interface ServerTimingError {
|
|
7
|
-
result: null
|
|
8
|
-
error: Error
|
|
9
|
-
header: string
|
|
10
|
-
}
|
|
11
|
-
export type ServerTimingResult<T> = ServerTimingSuccess<T> | ServerTimingError
|
|
1
|
+
export class ServerTiming {
|
|
2
|
+
private headers: string[]
|
|
12
3
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
fn: () => Promise<T>
|
|
17
|
-
): Promise<ServerTimingResult<T>> {
|
|
18
|
-
const startTime = performance.now()
|
|
4
|
+
constructor () {
|
|
5
|
+
this.headers = []
|
|
6
|
+
}
|
|
19
7
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
8
|
+
getHeader (): string {
|
|
9
|
+
return this.headers.join(',')
|
|
10
|
+
}
|
|
23
11
|
|
|
24
|
-
|
|
12
|
+
async time <T> (name: string, description: string, promise: Promise<T>): Promise<T> {
|
|
13
|
+
const startTime = performance.now()
|
|
25
14
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
const duration = (endTime - startTime).toFixed(1)
|
|
15
|
+
try {
|
|
16
|
+
return await promise // Execute the function
|
|
17
|
+
} finally {
|
|
18
|
+
const endTime = performance.now()
|
|
19
|
+
const duration = (endTime - startTime).toFixed(1) // Duration in milliseconds
|
|
32
20
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
return { result: null, error, header } // Pass error with timing info
|
|
21
|
+
this.headers.push(`${name};dur=${duration};desc="${description}"`)
|
|
22
|
+
}
|
|
36
23
|
}
|
|
37
24
|
}
|
package/src/utils/walk-path.ts
CHANGED
|
@@ -52,7 +52,7 @@ export function isObjectNode (node: UnixFSEntry): node is ObjectNode {
|
|
|
52
52
|
*/
|
|
53
53
|
export async function handlePathWalking ({ cid, path, resource, options, blockstore, log }: PluginContext & { blockstore: Blockstore, log: Logger }): Promise<PathWalkerResponse | Response> {
|
|
54
54
|
try {
|
|
55
|
-
return await walkPath(blockstore, `${cid
|
|
55
|
+
return await walkPath(blockstore, `${cid}/${path}`, options)
|
|
56
56
|
} catch (err: any) {
|
|
57
57
|
if (options?.signal?.aborted) {
|
|
58
58
|
throw new AbortError(options?.signal?.reason)
|
|
@@ -62,7 +62,7 @@ export async function handlePathWalking ({ cid, path, resource, options, blockst
|
|
|
62
62
|
return notFoundResponse(resource)
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
-
log.error('error walking path %s', path, err)
|
|
65
|
+
log.error('error walking path "%s" - %e', path, err)
|
|
66
66
|
return badGatewayResponse(resource, 'Error walking path')
|
|
67
67
|
}
|
|
68
68
|
}
|
package/src/verified-fetch.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { dnsLink } from '@helia/dnslink'
|
|
2
|
+
import { ipnsResolver } from '@helia/ipns'
|
|
2
3
|
import { AbortError } from '@libp2p/interface'
|
|
3
|
-
import { prefixLogger } from '@libp2p/logger'
|
|
4
4
|
import { CustomProgressEvent } from 'progress-events'
|
|
5
5
|
import QuickLRU from 'quick-lru'
|
|
6
6
|
import { ByteRangeContextPlugin } from './plugins/plugin-handle-byte-range-context.js'
|
|
@@ -12,22 +12,25 @@ import { IpnsRecordPlugin } from './plugins/plugin-handle-ipns-record.js'
|
|
|
12
12
|
import { JsonPlugin } from './plugins/plugin-handle-json.js'
|
|
13
13
|
import { RawPlugin } from './plugins/plugin-handle-raw.js'
|
|
14
14
|
import { TarPlugin } from './plugins/plugin-handle-tar.js'
|
|
15
|
+
import { URLResolver } from './url-resolver.ts'
|
|
15
16
|
import { contentTypeParser } from './utils/content-type-parser.js'
|
|
17
|
+
import { errorToObject } from './utils/error-to-object.ts'
|
|
16
18
|
import { getContentDispositionFilename } from './utils/get-content-disposition-filename.js'
|
|
17
19
|
import { getETag } from './utils/get-e-tag.js'
|
|
18
20
|
import { getResolvedAcceptHeader } from './utils/get-resolved-accept-header.js'
|
|
19
21
|
import { getRedirectResponse } from './utils/handle-redirects.js'
|
|
20
|
-
import {
|
|
22
|
+
import { uriEncodeIPFSPath } from './utils/ipfs-path-to-string.ts'
|
|
21
23
|
import { resourceToSessionCacheKey } from './utils/resource-to-cache-key.js'
|
|
22
24
|
import { setCacheControlHeader } from './utils/response-headers.js'
|
|
23
|
-
import { badRequestResponse, notAcceptableResponse,
|
|
25
|
+
import { badRequestResponse, notAcceptableResponse, internalServerErrorResponse, notImplementedResponse } from './utils/responses.js'
|
|
24
26
|
import { selectOutputType } from './utils/select-output-type.js'
|
|
25
|
-
import {
|
|
26
|
-
import type { CIDDetail, ContentTypeParser, CreateVerifiedFetchOptions, Resource, ResourceDetail, VerifiedFetchInit as VerifiedFetchOptions } from './index.js'
|
|
27
|
+
import { ServerTiming } from './utils/server-timing.js'
|
|
28
|
+
import type { CIDDetail, ContentTypeParser, CreateVerifiedFetchOptions, ResolveURLResult, Resource, ResourceDetail, VerifiedFetchInit as VerifiedFetchOptions } from './index.js'
|
|
27
29
|
import type { VerifiedFetchPlugin, PluginContext, PluginOptions } from './plugins/types.js'
|
|
28
|
-
import type {
|
|
30
|
+
import type { AcceptHeader } from './utils/select-output-type.js'
|
|
31
|
+
import type { DNSLink } from '@helia/dnslink'
|
|
29
32
|
import type { Helia, SessionBlockstore } from '@helia/interface'
|
|
30
|
-
import type {
|
|
33
|
+
import type { IPNSResolver } from '@helia/ipns'
|
|
31
34
|
import type { AbortOptions, Logger } from '@libp2p/interface'
|
|
32
35
|
import type { Blockstore } from 'interface-blockstore'
|
|
33
36
|
import type { CID } from 'multiformats/cid'
|
|
@@ -35,11 +38,6 @@ import type { CID } from 'multiformats/cid'
|
|
|
35
38
|
const SESSION_CACHE_MAX_SIZE = 100
|
|
36
39
|
const SESSION_CACHE_TTL_MS = 60 * 1000
|
|
37
40
|
|
|
38
|
-
interface VerifiedFetchComponents {
|
|
39
|
-
helia: Helia
|
|
40
|
-
ipns?: IPNS
|
|
41
|
-
}
|
|
42
|
-
|
|
43
41
|
function convertOptions (options?: VerifiedFetchOptions): (Omit<VerifiedFetchOptions, 'signal'> & AbortOptions) | undefined {
|
|
44
42
|
if (options == null) {
|
|
45
43
|
return undefined
|
|
@@ -60,19 +58,20 @@ function convertOptions (options?: VerifiedFetchOptions): (Omit<VerifiedFetchOpt
|
|
|
60
58
|
|
|
61
59
|
export class VerifiedFetch {
|
|
62
60
|
private readonly helia: Helia
|
|
63
|
-
private readonly
|
|
61
|
+
private readonly ipnsResolver: IPNSResolver
|
|
62
|
+
private readonly dnsLink: DNSLink
|
|
64
63
|
private readonly log: Logger
|
|
65
64
|
private readonly contentTypeParser: ContentTypeParser | undefined
|
|
66
65
|
private readonly blockstoreSessions: QuickLRU<string, SessionBlockstore>
|
|
67
|
-
private serverTimingHeaders: string[] = []
|
|
68
66
|
private readonly withServerTiming: boolean
|
|
69
67
|
private readonly plugins: VerifiedFetchPlugin[] = []
|
|
70
68
|
|
|
71
|
-
constructor (
|
|
69
|
+
constructor (helia: Helia, init: CreateVerifiedFetchOptions = {}) {
|
|
72
70
|
this.helia = helia
|
|
73
71
|
this.log = helia.logger.forComponent('helia:verified-fetch')
|
|
74
|
-
this.
|
|
75
|
-
this.
|
|
72
|
+
this.ipnsResolver = init.ipnsResolver ?? ipnsResolver(helia)
|
|
73
|
+
this.dnsLink = init.dnsLink ?? dnsLink(helia)
|
|
74
|
+
this.contentTypeParser = init.contentTypeParser ?? contentTypeParser
|
|
76
75
|
this.blockstoreSessions = new QuickLRU({
|
|
77
76
|
maxSize: init?.sessionCacheSize ?? SESSION_CACHE_MAX_SIZE,
|
|
78
77
|
maxAge: init?.sessionTTLms ?? SESSION_CACHE_TTL_MS,
|
|
@@ -84,11 +83,11 @@ export class VerifiedFetch {
|
|
|
84
83
|
|
|
85
84
|
const pluginOptions: PluginOptions = {
|
|
86
85
|
...init,
|
|
87
|
-
logger:
|
|
86
|
+
logger: helia.logger.forComponent('verified-fetch'),
|
|
88
87
|
getBlockstore: (cid, resource, useSession, options) => this.getBlockstore(cid, resource, useSession, options),
|
|
89
|
-
handleServerTiming: async (name, description, fn) => this.handleServerTiming(name, description, fn, this.withServerTiming),
|
|
90
88
|
helia,
|
|
91
|
-
contentTypeParser: this.contentTypeParser
|
|
89
|
+
contentTypeParser: this.contentTypeParser,
|
|
90
|
+
ipnsResolver: this.ipnsResolver
|
|
92
91
|
}
|
|
93
92
|
|
|
94
93
|
const defaultPlugins = [
|
|
@@ -103,7 +102,7 @@ export class VerifiedFetch {
|
|
|
103
102
|
new DagPbPlugin(pluginOptions)
|
|
104
103
|
]
|
|
105
104
|
|
|
106
|
-
const customPlugins = init
|
|
105
|
+
const customPlugins = init.plugins?.map((pluginFactory) => pluginFactory(pluginOptions)) ?? []
|
|
107
106
|
|
|
108
107
|
if (customPlugins.length > 0) {
|
|
109
108
|
// allow custom plugins to replace default plugins
|
|
@@ -117,8 +116,6 @@ export class VerifiedFetch {
|
|
|
117
116
|
} else {
|
|
118
117
|
this.plugins = defaultPlugins
|
|
119
118
|
}
|
|
120
|
-
|
|
121
|
-
this.log.trace('created VerifiedFetch instance')
|
|
122
119
|
}
|
|
123
120
|
|
|
124
121
|
private getBlockstore (root: CID, resource: string | CID, useSession: boolean = true, options: AbortOptions = {}): Blockstore {
|
|
@@ -137,35 +134,20 @@ export class VerifiedFetch {
|
|
|
137
134
|
return session
|
|
138
135
|
}
|
|
139
136
|
|
|
140
|
-
private async handleServerTiming<T> (name: string, description: string, fn: () => Promise<T>, withServerTiming: boolean): Promise<T> {
|
|
141
|
-
if (!withServerTiming) {
|
|
142
|
-
return fn()
|
|
143
|
-
}
|
|
144
|
-
const { error, result, header } = await serverTiming(name, description, fn)
|
|
145
|
-
this.serverTimingHeaders.push(header)
|
|
146
|
-
if (error != null) {
|
|
147
|
-
throw error
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
return result
|
|
151
|
-
}
|
|
152
|
-
|
|
153
137
|
/**
|
|
154
138
|
* The last place a Response touches in verified-fetch before being returned to the user. This is where we add the
|
|
155
139
|
* Server-Timing header to the response if it has been collected. It should be used for any final processing of the
|
|
156
140
|
* response before it is returned to the user.
|
|
157
141
|
*/
|
|
158
|
-
private handleFinalResponse (response: Response,
|
|
159
|
-
if (this.
|
|
160
|
-
|
|
161
|
-
response.headers.set('Server-Timing', headerString)
|
|
162
|
-
this.serverTimingHeaders = []
|
|
142
|
+
private handleFinalResponse (response: Response, context?: Partial<PluginContext>): Response {
|
|
143
|
+
if ((this.withServerTiming || context?.withServerTiming === true) && context?.serverTiming != null) {
|
|
144
|
+
response.headers.set('Server-Timing', context?.serverTiming.getHeader())
|
|
163
145
|
}
|
|
164
146
|
|
|
165
147
|
// 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
|
|
166
148
|
if (response.headers.get('Transfer-Encoding') !== 'chunked') {
|
|
167
|
-
if (byteRangeContext != null) {
|
|
168
|
-
const contentLength = byteRangeContext.getLength()
|
|
149
|
+
if (context?.byteRangeContext != null) {
|
|
150
|
+
const contentLength = context.byteRangeContext.getLength()
|
|
169
151
|
if (contentLength != null) {
|
|
170
152
|
this.log.trace('Setting Content-Length from byteRangeContext: %d', contentLength)
|
|
171
153
|
response.headers.set('Content-Length', contentLength.toString())
|
|
@@ -176,50 +158,55 @@ export class VerifiedFetch {
|
|
|
176
158
|
// set Content-Disposition header
|
|
177
159
|
let contentDisposition: string | undefined
|
|
178
160
|
|
|
179
|
-
this.log.trace('checking for content disposition')
|
|
180
|
-
|
|
181
161
|
// force download if requested
|
|
182
|
-
if (query?.download === true) {
|
|
162
|
+
if (context?.query?.download === true) {
|
|
163
|
+
this.log.trace('download requested')
|
|
183
164
|
contentDisposition = 'attachment'
|
|
184
|
-
} else {
|
|
185
|
-
this.log.trace('download not requested')
|
|
186
165
|
}
|
|
187
166
|
|
|
188
167
|
// override filename if requested
|
|
189
|
-
if (query?.filename != null) {
|
|
168
|
+
if (context?.query?.filename != null) {
|
|
169
|
+
this.log.trace('specific filename requested')
|
|
170
|
+
|
|
190
171
|
if (contentDisposition == null) {
|
|
191
172
|
contentDisposition = 'inline'
|
|
192
173
|
}
|
|
193
174
|
|
|
194
|
-
contentDisposition = `${contentDisposition}; ${getContentDispositionFilename(query.filename)}`
|
|
195
|
-
} else {
|
|
196
|
-
this.log.trace('no filename specified in query')
|
|
175
|
+
contentDisposition = `${contentDisposition}; ${getContentDispositionFilename(context.query.filename)}`
|
|
197
176
|
}
|
|
198
177
|
|
|
199
178
|
if (contentDisposition != null) {
|
|
179
|
+
this.log.trace('content disposition %s', contentDisposition)
|
|
200
180
|
response.headers.set('Content-Disposition', contentDisposition)
|
|
201
|
-
} else {
|
|
202
|
-
this.log.trace('no content disposition specified')
|
|
203
181
|
}
|
|
204
182
|
|
|
205
|
-
if (cid != null && response.headers.get('etag') == null) {
|
|
206
|
-
response.headers.set('etag', getETag({
|
|
183
|
+
if (context?.cid != null && response.headers.get('etag') == null) {
|
|
184
|
+
response.headers.set('etag', getETag({
|
|
185
|
+
cid: context.pathDetails?.terminalElement.cid ?? context.cid,
|
|
186
|
+
reqFormat: context.reqFormat,
|
|
187
|
+
weak: false
|
|
188
|
+
}))
|
|
207
189
|
}
|
|
208
190
|
|
|
209
|
-
if (protocol != null) {
|
|
210
|
-
setCacheControlHeader({
|
|
191
|
+
if (context?.protocol != null && context.ttl != null) {
|
|
192
|
+
setCacheControlHeader({
|
|
193
|
+
response,
|
|
194
|
+
ttl: context.ttl,
|
|
195
|
+
protocol: context.protocol
|
|
196
|
+
})
|
|
211
197
|
}
|
|
212
|
-
|
|
213
|
-
|
|
198
|
+
|
|
199
|
+
if (context?.ipfsPath != null) {
|
|
200
|
+
response.headers.set('X-Ipfs-Path', uriEncodeIPFSPath(context.ipfsPath))
|
|
214
201
|
}
|
|
215
202
|
|
|
216
203
|
// set CORS headers. If hosting your own gateway with verified-fetch behind the scenes, you can alter these before you send the response to the client.
|
|
217
204
|
response.headers.set('Access-Control-Allow-Origin', '*')
|
|
218
205
|
response.headers.set('Access-Control-Allow-Methods', 'GET, HEAD, OPTIONS')
|
|
219
206
|
response.headers.set('Access-Control-Allow-Headers', 'Range, X-Requested-With')
|
|
220
|
-
response.headers.set('Access-Control-Expose-Headers', 'Content-Range, Content-Length, X-Ipfs-Path, X-Stream-Output')
|
|
207
|
+
response.headers.set('Access-Control-Expose-Headers', 'Content-Range, Content-Length, X-Ipfs-Path, X-Ipfs-Roots, X-Stream-Output')
|
|
221
208
|
|
|
222
|
-
if (reqFormat !== 'car') {
|
|
209
|
+
if (context?.reqFormat !== 'car') {
|
|
223
210
|
// if we are not doing streaming responses, set the Accept-Ranges header to bytes to enable range requests
|
|
224
211
|
response.headers.set('Accept-Ranges', 'bytes')
|
|
225
212
|
} else {
|
|
@@ -227,10 +214,17 @@ export class VerifiedFetch {
|
|
|
227
214
|
response.headers.set('Accept-Ranges', 'none')
|
|
228
215
|
}
|
|
229
216
|
|
|
230
|
-
if (
|
|
217
|
+
if (response.headers.get('Content-Type')?.includes('application/vnd.ipld.car') === true || response.headers.get('Content-Type')?.includes('application/vnd.ipld.raw') === true) {
|
|
218
|
+
// see https://specs.ipfs.tech/http-gateways/path-gateway/#x-content-type-options-response-header
|
|
219
|
+
response.headers.set('X-Content-Type-Options', 'nosniff')
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (context?.options?.method === 'HEAD') {
|
|
231
223
|
// don't send the body for HEAD requests
|
|
232
|
-
|
|
233
|
-
|
|
224
|
+
return new Response(null, {
|
|
225
|
+
status: 200,
|
|
226
|
+
headers: response.headers
|
|
227
|
+
})
|
|
234
228
|
}
|
|
235
229
|
|
|
236
230
|
return response
|
|
@@ -240,7 +234,7 @@ export class VerifiedFetch {
|
|
|
240
234
|
* Runs plugins in a loop. After each plugin that returns `null` (partial/no final),
|
|
241
235
|
* we re-check `canHandle()` for all plugins in the next iteration if the context changed.
|
|
242
236
|
*/
|
|
243
|
-
private async runPluginPipeline (context: PluginContext, maxPasses: number = 3): Promise<Response
|
|
237
|
+
private async runPluginPipeline (context: PluginContext, maxPasses: number = 3): Promise<Response> {
|
|
244
238
|
let finalResponse: Response | undefined
|
|
245
239
|
let passCount = 0
|
|
246
240
|
const pluginsUsed = new Set<string>()
|
|
@@ -248,23 +242,27 @@ export class VerifiedFetch {
|
|
|
248
242
|
let prevModificationId = context.modified
|
|
249
243
|
|
|
250
244
|
while (passCount < maxPasses) {
|
|
251
|
-
this.log(`
|
|
245
|
+
this.log(`starting pipeline pass #${passCount + 1}`)
|
|
252
246
|
passCount++
|
|
253
247
|
|
|
248
|
+
this.log.trace('checking which plugins can handle %c%s with accept %o', context.cid, context.path != null ? `/${context.path}` : '', context.accept)
|
|
249
|
+
|
|
254
250
|
// gather plugins that say they can handle the *current* context, but haven't been used yet
|
|
255
251
|
const readyPlugins = this.plugins.filter(p => !pluginsUsed.has(p.id)).filter(p => p.canHandle(context))
|
|
252
|
+
|
|
256
253
|
if (readyPlugins.length === 0) {
|
|
257
|
-
this.log.trace('
|
|
254
|
+
this.log.trace('no plugins can handle the current context, checking by CID code')
|
|
258
255
|
const plugins = this.plugins.filter(p => p.codes.includes(context.cid.code))
|
|
256
|
+
|
|
259
257
|
if (plugins.length > 0) {
|
|
260
258
|
readyPlugins.push(...plugins)
|
|
261
259
|
} else {
|
|
262
|
-
this.log.trace('
|
|
260
|
+
this.log.trace('no plugins found that can handle request by CID code; exiting pipeline')
|
|
263
261
|
break
|
|
264
262
|
}
|
|
265
263
|
}
|
|
266
264
|
|
|
267
|
-
this.log.trace('
|
|
265
|
+
this.log.trace('plugins ready to handle request: %s', readyPlugins.map(p => p.id).join(', '))
|
|
268
266
|
|
|
269
267
|
// track if any plugin changed the context or returned a response
|
|
270
268
|
let contextChanged = false
|
|
@@ -272,10 +270,13 @@ export class VerifiedFetch {
|
|
|
272
270
|
|
|
273
271
|
for (const plugin of readyPlugins) {
|
|
274
272
|
try {
|
|
275
|
-
this.log
|
|
273
|
+
this.log('invoking plugin: %s', plugin.id)
|
|
276
274
|
pluginsUsed.add(plugin.id)
|
|
277
275
|
|
|
278
276
|
const maybeResponse = await plugin.handle(context)
|
|
277
|
+
|
|
278
|
+
this.log('plugin response %s %o', plugin.id, maybeResponse)
|
|
279
|
+
|
|
279
280
|
if (maybeResponse != null) {
|
|
280
281
|
// if a plugin returns a final Response, short-circuit
|
|
281
282
|
finalResponse = maybeResponse
|
|
@@ -286,12 +287,16 @@ export class VerifiedFetch {
|
|
|
286
287
|
if (context.options?.signal?.aborted) {
|
|
287
288
|
throw new AbortError(context.options?.signal?.reason)
|
|
288
289
|
}
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
}
|
|
290
|
+
|
|
291
|
+
this.log.error('error in plugin %s - %e', plugin.id, err)
|
|
292
|
+
|
|
293
|
+
return internalServerErrorResponse(context.resource, JSON.stringify({
|
|
294
|
+
error: errorToObject(err)
|
|
295
|
+
}), {
|
|
296
|
+
headers: {
|
|
297
|
+
'content-type': 'application/json'
|
|
298
|
+
}
|
|
299
|
+
})
|
|
295
300
|
} finally {
|
|
296
301
|
// on each plugin call, check for changes in the context
|
|
297
302
|
const newModificationId = context.modified
|
|
@@ -302,7 +307,7 @@ export class VerifiedFetch {
|
|
|
302
307
|
}
|
|
303
308
|
|
|
304
309
|
if (finalResponse != null) {
|
|
305
|
-
this.log.trace('
|
|
310
|
+
this.log.trace('plugin %s produced final response', plugin.id)
|
|
306
311
|
break
|
|
307
312
|
}
|
|
308
313
|
}
|
|
@@ -312,12 +317,18 @@ export class VerifiedFetch {
|
|
|
312
317
|
}
|
|
313
318
|
|
|
314
319
|
if (!contextChanged) {
|
|
315
|
-
this.log.trace('
|
|
320
|
+
this.log.trace('no context changes and no final response; exiting pipeline.')
|
|
316
321
|
break
|
|
317
322
|
}
|
|
318
323
|
}
|
|
319
324
|
|
|
320
|
-
return finalResponse
|
|
325
|
+
return finalResponse ?? notImplementedResponse(context.resource, JSON.stringify({
|
|
326
|
+
error: errorToObject(new Error('No verified fetch plugin could handle the request'))
|
|
327
|
+
}), {
|
|
328
|
+
headers: {
|
|
329
|
+
'content-type': 'application/json'
|
|
330
|
+
}
|
|
331
|
+
})
|
|
321
332
|
}
|
|
322
333
|
|
|
323
334
|
/**
|
|
@@ -335,14 +346,20 @@ export class VerifiedFetch {
|
|
|
335
346
|
}
|
|
336
347
|
|
|
337
348
|
const options = convertOptions(opts)
|
|
338
|
-
const
|
|
349
|
+
const serverTiming = new ServerTiming()
|
|
350
|
+
|
|
351
|
+
const urlResolver = new URLResolver({
|
|
352
|
+
ipnsResolver: this.ipnsResolver,
|
|
353
|
+
dnsLink: this.dnsLink,
|
|
354
|
+
timing: serverTiming
|
|
355
|
+
})
|
|
339
356
|
|
|
340
357
|
options?.onProgress?.(new CustomProgressEvent<ResourceDetail>('verified-fetch:request:start', { resource }))
|
|
341
358
|
|
|
342
|
-
let parsedResult:
|
|
359
|
+
let parsedResult: ResolveURLResult
|
|
360
|
+
|
|
343
361
|
try {
|
|
344
|
-
parsedResult = await
|
|
345
|
-
this.serverTimingHeaders.push(...parsedResult.serverTimings.map(({ header }) => header))
|
|
362
|
+
parsedResult = await urlResolver.resolve(resource, options)
|
|
346
363
|
} catch (err: any) {
|
|
347
364
|
if (options?.signal?.aborted) {
|
|
348
365
|
throw new AbortError(options?.signal?.reason)
|
|
@@ -356,14 +373,15 @@ export class VerifiedFetch {
|
|
|
356
373
|
|
|
357
374
|
const acceptHeader = getResolvedAcceptHeader({ query: parsedResult.query, headers: options?.headers, logger: this.helia.logger })
|
|
358
375
|
|
|
359
|
-
const accept:
|
|
360
|
-
this.log('
|
|
376
|
+
const accept: AcceptHeader | undefined = selectOutputType(parsedResult.cid, acceptHeader)
|
|
377
|
+
this.log('accept %o', accept)
|
|
361
378
|
|
|
362
379
|
if (acceptHeader != null && accept == null) {
|
|
380
|
+
this.log.error('could not fulfil request based on accept header')
|
|
363
381
|
return this.handleFinalResponse(notAcceptableResponse(resource.toString()))
|
|
364
382
|
}
|
|
365
383
|
|
|
366
|
-
const responseContentType: string = accept?.split(';')[0] ?? 'application/octet-stream'
|
|
384
|
+
const responseContentType: string = accept?.mimeType.split(';')[0] ?? 'application/octet-stream'
|
|
367
385
|
|
|
368
386
|
const redirectResponse = await getRedirectResponse({ resource, options, logger: this.helia.logger, cid: parsedResult.cid })
|
|
369
387
|
if (redirectResponse != null) {
|
|
@@ -375,19 +393,28 @@ export class VerifiedFetch {
|
|
|
375
393
|
resource: resource.toString(),
|
|
376
394
|
accept,
|
|
377
395
|
options,
|
|
378
|
-
withServerTiming,
|
|
379
396
|
onProgress: options?.onProgress,
|
|
380
397
|
modified: 0,
|
|
381
|
-
plugins: this.plugins.map(p => p.id)
|
|
398
|
+
plugins: this.plugins.map(p => p.id),
|
|
399
|
+
query: parsedResult.query ?? {},
|
|
400
|
+
withServerTiming: Boolean(options?.withServerTiming) || Boolean(this.withServerTiming),
|
|
401
|
+
serverTiming
|
|
382
402
|
}
|
|
383
403
|
|
|
384
404
|
this.log.trace('finding handler for cid code "%s" and response content type "%s"', parsedResult.cid.code, responseContentType)
|
|
385
405
|
|
|
386
406
|
const response = await this.runPluginPipeline(context)
|
|
387
407
|
|
|
388
|
-
options?.onProgress?.(new CustomProgressEvent<CIDDetail>('verified-fetch:request:end', {
|
|
408
|
+
options?.onProgress?.(new CustomProgressEvent<CIDDetail>('verified-fetch:request:end', {
|
|
409
|
+
cid: parsedResult.cid,
|
|
410
|
+
path: parsedResult.path
|
|
411
|
+
}))
|
|
412
|
+
|
|
413
|
+
if (response == null) {
|
|
414
|
+
this.log.error('no plugin could handle request for %s', resource)
|
|
415
|
+
}
|
|
389
416
|
|
|
390
|
-
return this.handleFinalResponse(response
|
|
417
|
+
return this.handleFinalResponse(response, context)
|
|
391
418
|
}
|
|
392
419
|
|
|
393
420
|
/**
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import type { FatalPluginErrorOptions, PluginErrorOptions } from './types.js';
|
|
2
|
-
/**
|
|
3
|
-
* If a plugin encounters an error, it should throw an instance of this class.
|
|
4
|
-
*/
|
|
5
|
-
export declare class PluginError extends Error {
|
|
6
|
-
name: string;
|
|
7
|
-
code: string;
|
|
8
|
-
fatal: boolean;
|
|
9
|
-
details?: Record<string, any>;
|
|
10
|
-
response?: any;
|
|
11
|
-
constructor(code: string, message: string, options?: PluginErrorOptions);
|
|
12
|
-
}
|
|
13
|
-
/**
|
|
14
|
-
* If a plugin encounters a fatal error and verified-fetch should not continue processing the request, it should throw
|
|
15
|
-
* an instance of this class.
|
|
16
|
-
*
|
|
17
|
-
* Note that you should be very careful when throwing a `PluginFatalError`, as it will stop the request from being
|
|
18
|
-
* processed further. If you do not have a response to return to the client, you should consider throwing a
|
|
19
|
-
* `PluginError` instead.
|
|
20
|
-
*/
|
|
21
|
-
export declare class PluginFatalError extends PluginError {
|
|
22
|
-
name: string;
|
|
23
|
-
constructor(code: string, message: string, options: FatalPluginErrorOptions);
|
|
24
|
-
}
|
|
25
|
-
//# sourceMappingURL=errors.d.ts.map
|