@helia/verified-fetch 4.0.0 → 4.0.2
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 +7 -49
- package/dist/index.min.js +56 -51
- package/dist/index.min.js.map +4 -4
- package/dist/src/index.d.ts +12 -52
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +8 -50
- 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 +0 -1
- package/dist/src/plugins/plugin-handle-car.js.map +1 -1
- package/dist/src/plugins/plugin-handle-dag-cbor-html-preview.d.ts +3 -3
- 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 +5 -6
- 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 +0 -1
- 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 +34 -39
- 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 +8 -5
- 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 +4 -6
- 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 +0 -1
- 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 +4 -8
- 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 +0 -1
- package/dist/src/plugins/plugin-handle-tar.js.map +1 -1
- package/dist/src/plugins/types.d.ts +6 -11
- package/dist/src/plugins/types.d.ts.map +1 -1
- package/dist/src/url-resolver.d.ts +4 -3
- package/dist/src/url-resolver.d.ts.map +1 -1
- package/dist/src/url-resolver.js +35 -47
- package/dist/src/url-resolver.js.map +1 -1
- package/dist/src/utils/byte-range-context.d.ts +2 -2
- 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/dnslink-label.d.ts +26 -0
- package/dist/src/utils/dnslink-label.d.ts.map +1 -0
- package/dist/src/utils/dnslink-label.js +35 -0
- package/dist/src/utils/dnslink-label.js.map +1 -0
- 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 +1 -1
- 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-stream-from-async-iterable.d.ts +1 -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 +1 -3
- package/dist/src/utils/get-stream-from-async-iterable.js.map +1 -1
- package/dist/src/utils/handle-redirects.js +2 -2
- package/dist/src/utils/ipfs-path-to-url.d.ts +16 -0
- package/dist/src/utils/ipfs-path-to-url.d.ts.map +1 -0
- package/dist/src/utils/ipfs-path-to-url.js +45 -0
- package/dist/src/utils/ipfs-path-to-url.js.map +1 -0
- package/dist/src/utils/parse-url-string.d.ts +18 -5
- package/dist/src/utils/parse-url-string.d.ts.map +1 -1
- package/dist/src/utils/parse-url-string.js +126 -44
- package/dist/src/utils/parse-url-string.js.map +1 -1
- package/dist/src/utils/resource-to-cache-key.js +2 -2
- package/dist/src/utils/responses.d.ts +2 -1
- package/dist/src/utils/responses.d.ts.map +1 -1
- package/dist/src/utils/responses.js +16 -1
- package/dist/src/utils/responses.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.map +1 -1
- package/dist/src/verified-fetch.js +36 -28
- package/dist/src/verified-fetch.js.map +1 -1
- package/dist/typedoc-urls.json +0 -2
- package/package.json +10 -10
- package/src/index.ts +12 -52
- package/src/plugins/index.ts +0 -1
- package/src/plugins/plugin-base.ts +3 -2
- package/src/plugins/plugin-handle-car.ts +0 -2
- package/src/plugins/plugin-handle-dag-cbor-html-preview.ts +10 -9
- package/src/plugins/plugin-handle-dag-cbor.ts +3 -1
- package/src/plugins/plugin-handle-dag-pb.ts +49 -37
- package/src/plugins/plugin-handle-dag-walk.ts +10 -5
- package/src/plugins/plugin-handle-ipns-record.ts +6 -6
- package/src/plugins/plugin-handle-json.ts +1 -1
- package/src/plugins/plugin-handle-raw.ts +7 -8
- package/src/plugins/plugin-handle-tar.ts +2 -1
- package/src/plugins/types.ts +7 -12
- package/src/url-resolver.ts +37 -56
- package/src/utils/byte-range-context.ts +3 -3
- package/src/utils/content-type-parser.ts +5 -11
- package/src/utils/dnslink-label.ts +38 -0
- package/src/utils/error-to-object.ts +22 -0
- package/src/utils/get-content-type.ts +2 -2
- package/src/utils/get-stream-from-async-iterable.ts +1 -4
- package/src/utils/handle-redirects.ts +2 -2
- package/src/utils/ipfs-path-to-url.ts +54 -0
- package/src/utils/parse-url-string.ts +166 -49
- package/src/utils/resource-to-cache-key.ts +2 -2
- package/src/utils/responses.ts +21 -1
- package/src/utils/walk-path.ts +1 -1
- package/src/verified-fetch.ts +46 -28
- 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/src/plugins/errors.ts +0 -37
|
@@ -1,77 +1,194 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
const SUBDOMAIN_GATEWAY_REGEX = /^https?:\/\/(?<cidOrPeerIdOrDnsLink>[^/?]+)\.(?<protocol>ip[fn]s)\.([^/?]+)\/?(?<path>[^?]*)\??(?<query>.*)$/
|
|
1
|
+
import { InvalidParametersError } from '@libp2p/interface'
|
|
2
|
+
import { decodeDNSLinkLabel, isInlinedDnsLink } from './dnslink-label.ts'
|
|
3
|
+
import { ipfsPathToUrl, ipfsUrlToUrl } from './ipfs-path-to-url.ts'
|
|
5
4
|
|
|
6
|
-
interface
|
|
5
|
+
interface SubdomainMatchGroups {
|
|
7
6
|
protocol: 'ipfs' | 'ipns'
|
|
8
7
|
cidOrPeerIdOrDnsLink: string
|
|
9
|
-
path?: string
|
|
10
|
-
query?: string
|
|
11
8
|
}
|
|
12
9
|
|
|
13
|
-
|
|
10
|
+
const SUBDOMAIN_GATEWAY_REGEX = /^(?<cidOrPeerIdOrDnsLink>[^/?]+)\.(?<protocol>ip[fn]s)\.([^/?]+)$/
|
|
11
|
+
|
|
12
|
+
function matchSubdomainGroupsGuard (groups?: null | { [key in string]: string; } | SubdomainMatchGroups): groups is SubdomainMatchGroups {
|
|
14
13
|
const protocol = groups?.protocol
|
|
15
|
-
|
|
14
|
+
|
|
15
|
+
if (protocol !== 'ipfs' && protocol !== 'ipns') {
|
|
16
|
+
return false
|
|
17
|
+
}
|
|
18
|
+
|
|
16
19
|
const cidOrPeerIdOrDnsLink = groups?.cidOrPeerIdOrDnsLink
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
(path == null || typeof path === 'string') &&
|
|
24
|
-
(query == null || typeof query === 'string')
|
|
20
|
+
|
|
21
|
+
if (cidOrPeerIdOrDnsLink == null) {
|
|
22
|
+
return false
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return true
|
|
25
26
|
}
|
|
26
27
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
28
|
+
export interface ParsedURL {
|
|
29
|
+
url: URL,
|
|
30
|
+
protocol: 'ipfs' | 'ipns'
|
|
31
|
+
cidOrPeerIdOrDnsLink: string
|
|
32
|
+
path: string[]
|
|
33
|
+
query: Record<string, any>
|
|
34
|
+
fragment: string
|
|
35
|
+
}
|
|
31
36
|
|
|
32
|
-
|
|
33
|
-
|
|
37
|
+
function toQuery (query?: URLSearchParams): Record<string, any> {
|
|
38
|
+
if (query == null) {
|
|
39
|
+
return {}
|
|
40
|
+
}
|
|
34
41
|
|
|
35
|
-
|
|
36
|
-
groups.path = decodeURIComponent(groups.path)
|
|
37
|
-
}
|
|
42
|
+
const output: Record<string, any> = {}
|
|
38
43
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
44
|
+
for (const [key, value] of query.entries()) {
|
|
45
|
+
output[key] = value
|
|
46
|
+
|
|
47
|
+
if (value === 'true') {
|
|
48
|
+
output[key] = true
|
|
49
|
+
}
|
|
43
50
|
|
|
44
|
-
|
|
51
|
+
if (value === 'false') {
|
|
52
|
+
output[key] = false
|
|
45
53
|
}
|
|
46
54
|
}
|
|
47
55
|
|
|
48
|
-
|
|
56
|
+
return output
|
|
49
57
|
}
|
|
50
58
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
*/
|
|
59
|
+
function stripLeadingHash (pathname: string): string {
|
|
60
|
+
return stripLeading(pathname, '#')
|
|
61
|
+
}
|
|
55
62
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
63
|
+
function stripLeading (str: string, char: string): string {
|
|
64
|
+
while (str.startsWith(char)) {
|
|
65
|
+
str = str.substring(1)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return str
|
|
69
|
+
}
|
|
59
70
|
|
|
60
71
|
/**
|
|
61
|
-
*
|
|
62
|
-
* (
|
|
72
|
+
* If the caller has passed a case-sensitive identifier (like a base58btc
|
|
73
|
+
* encoded CID or PeerId) in a case-insensitive location (like a subdomain),
|
|
74
|
+
* be nice and return the original identifier from the passed string
|
|
63
75
|
*/
|
|
64
|
-
function
|
|
65
|
-
|
|
76
|
+
function findOriginalCidOrPeer (needle: string, haystack: string): string {
|
|
77
|
+
const start = haystack.toLowerCase().indexOf(needle)
|
|
78
|
+
|
|
79
|
+
if (start === -1) {
|
|
80
|
+
return needle
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return haystack.substring(start, start + needle.length)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function toUrl (urlString: string): URL {
|
|
87
|
+
// turn IPFS/IPNS path into gateway URL string
|
|
88
|
+
if (urlString.startsWith('/ipfs/') || urlString.startsWith('/ipns/')) {
|
|
89
|
+
urlString = ipfsPathToUrl(urlString)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// turn IPFS/IPNS URL into gateway URL string
|
|
93
|
+
if (urlString.startsWith('ipfs://') || urlString.startsWith('ipns://')) {
|
|
94
|
+
urlString = ipfsUrlToUrl(urlString)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (urlString.startsWith('http://') || urlString.startsWith('https://')) {
|
|
98
|
+
return new URL(urlString)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
throw new InvalidParametersError(`Invalid URL: ${urlString}`)
|
|
66
102
|
}
|
|
67
103
|
|
|
68
104
|
/**
|
|
69
|
-
*
|
|
70
|
-
* - Every standalone - is replaced with .
|
|
71
|
-
* - Every remaining -- is replaced with -
|
|
105
|
+
* Accepts the following url strings:
|
|
72
106
|
*
|
|
73
|
-
*
|
|
107
|
+
* - /ipfs/Qmfoo/path
|
|
108
|
+
* - /ipns/Qmfoo/path
|
|
109
|
+
* - ipfs://cid/path
|
|
110
|
+
* - ipns://name/path
|
|
111
|
+
* - http://cid.ipfs.example.com/path
|
|
112
|
+
* - http://name.ipns.example.com/path
|
|
113
|
+
* - http://example.com/ipfs/cid/path
|
|
114
|
+
* - http://example.com/ipns/name/path
|
|
74
115
|
*/
|
|
75
|
-
function
|
|
76
|
-
|
|
116
|
+
export function parseURLString (urlString: string): ParsedURL {
|
|
117
|
+
// validate url
|
|
118
|
+
const url = toUrl(urlString)
|
|
119
|
+
|
|
120
|
+
// test for subdomain gateway URL
|
|
121
|
+
const subdomainMatch = url.hostname.match(SUBDOMAIN_GATEWAY_REGEX)
|
|
122
|
+
|
|
123
|
+
if (matchSubdomainGroupsGuard(subdomainMatch?.groups)) {
|
|
124
|
+
const groups = subdomainMatch.groups
|
|
125
|
+
|
|
126
|
+
if (groups.protocol === 'ipns' && isInlinedDnsLink(groups.cidOrPeerIdOrDnsLink)) {
|
|
127
|
+
// decode inline dnslink domain if present
|
|
128
|
+
groups.cidOrPeerIdOrDnsLink = decodeDNSLinkLabel(groups.cidOrPeerIdOrDnsLink)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const cidOrPeerIdOrDnsLink = findOriginalCidOrPeer(groups.cidOrPeerIdOrDnsLink, urlString)
|
|
132
|
+
|
|
133
|
+
// parse url as not http(s):// - this is necessary because URL makes
|
|
134
|
+
// `.pathname` default to `/` for http URLs, even if no trailing slash was
|
|
135
|
+
// present in the string URL and we need to be able to round-trip the user's
|
|
136
|
+
// input while also maintaining a sane canonical URL for the resource. Phew.
|
|
137
|
+
const wat = new URL(`not-${urlString}`)
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
url: new URL(`${groups.protocol}://${cidOrPeerIdOrDnsLink}${wat.pathname}${url.search}${url.hash}`),
|
|
141
|
+
protocol: groups.protocol,
|
|
142
|
+
cidOrPeerIdOrDnsLink,
|
|
143
|
+
path: url.pathname.split('/')
|
|
144
|
+
.filter(str => str !== '')
|
|
145
|
+
.map(str => decodeURIComponent(str)),
|
|
146
|
+
query: toQuery(url.searchParams),
|
|
147
|
+
fragment: stripLeadingHash(url.hash)
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// test for IPFS path gateway URL
|
|
152
|
+
if (url.pathname.startsWith('/ipfs/')) {
|
|
153
|
+
const parts = url.pathname.substring(6).split('/')
|
|
154
|
+
const cid = parts.shift()
|
|
155
|
+
|
|
156
|
+
if (cid == null) {
|
|
157
|
+
throw new InvalidParametersError(`Path gateway URL ${urlString} had no CID`)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return {
|
|
161
|
+
url: new URL(`ipfs://${cid}${url.pathname.replace(`/ipfs/${cid}`, '')}${url.search}${url.hash}`),
|
|
162
|
+
protocol: 'ipfs',
|
|
163
|
+
cidOrPeerIdOrDnsLink: cid,
|
|
164
|
+
path: parts
|
|
165
|
+
.filter(str => str !== '')
|
|
166
|
+
.map(str => decodeURIComponent(str)),
|
|
167
|
+
query: toQuery(url.searchParams),
|
|
168
|
+
fragment: stripLeadingHash(url.hash)
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// test for IPNS path gateway URL
|
|
173
|
+
if (url.pathname.startsWith('/ipns/')) {
|
|
174
|
+
const parts = url.pathname.substring(6).split('/')
|
|
175
|
+
const name = parts.shift()
|
|
176
|
+
|
|
177
|
+
if (name == null) {
|
|
178
|
+
throw new InvalidParametersError(`Path gateway URL ${urlString} had no name`)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return {
|
|
182
|
+
url: new URL(`ipns://${name}${url.pathname.replace(`/ipns/${name}`, '')}${url.search}${url.hash}`),
|
|
183
|
+
protocol: 'ipns',
|
|
184
|
+
cidOrPeerIdOrDnsLink: name,
|
|
185
|
+
path: parts
|
|
186
|
+
.filter(str => str !== '')
|
|
187
|
+
.map(str => decodeURIComponent(str)),
|
|
188
|
+
query: toQuery(url.searchParams),
|
|
189
|
+
fragment: stripLeadingHash(url.hash)
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
throw new TypeError(`Invalid URL: ${urlString}, please use ipfs://, ipns://, or gateway URLs only`)
|
|
77
194
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { CID } from 'multiformats/cid'
|
|
2
|
-
import {
|
|
2
|
+
import { parseURLString } from './parse-url-string.js'
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Takes a resource and returns a session cache key as an IPFS or IPNS path with
|
|
@@ -24,7 +24,7 @@ export function resourceToSessionCacheKey (url: string | CID): string {
|
|
|
24
24
|
return `ipfs://${CID.parse(url.toString())}`
|
|
25
25
|
} catch {}
|
|
26
26
|
|
|
27
|
-
const { protocol, cidOrPeerIdOrDnsLink } =
|
|
27
|
+
const { protocol, cidOrPeerIdOrDnsLink } = parseURLString(url.toString())
|
|
28
28
|
|
|
29
29
|
return `${protocol}://${cidOrPeerIdOrDnsLink}`
|
|
30
30
|
}
|
package/src/utils/responses.ts
CHANGED
|
@@ -16,6 +16,12 @@ function setType (response: Response, value: 'basic' | 'cors' | 'error' | 'opaqu
|
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
function setUrl (response: Response, value: string): void {
|
|
19
|
+
const fragmentStart = value.indexOf('#')
|
|
20
|
+
|
|
21
|
+
if (fragmentStart > -1) {
|
|
22
|
+
value = value.substring(0, fragmentStart)
|
|
23
|
+
}
|
|
24
|
+
|
|
19
25
|
setField(response, 'url', value)
|
|
20
26
|
}
|
|
21
27
|
|
|
@@ -44,6 +50,20 @@ export function okResponse (url: string, body?: SupportedBodyTypes, init?: Respo
|
|
|
44
50
|
return response
|
|
45
51
|
}
|
|
46
52
|
|
|
53
|
+
export function internalServerErrorResponse (url: string, body?: SupportedBodyTypes, init?: ResponseInit): Response {
|
|
54
|
+
const response = new Response(body, {
|
|
55
|
+
...(init ?? {}),
|
|
56
|
+
status: 500,
|
|
57
|
+
statusText: 'Internal Server Error'
|
|
58
|
+
})
|
|
59
|
+
response.headers.set('X-Content-Type-Options', 'nosniff') // see https://specs.ipfs.tech/http-gateways/path-gateway/#x-content-type-options-response-header
|
|
60
|
+
|
|
61
|
+
setType(response, 'basic')
|
|
62
|
+
setUrl(response, url)
|
|
63
|
+
|
|
64
|
+
return response
|
|
65
|
+
}
|
|
66
|
+
|
|
47
67
|
export function badGatewayResponse (url: string, body?: SupportedBodyTypes, init?: ResponseInit): Response {
|
|
48
68
|
const response = new Response(body, {
|
|
49
69
|
...(init ?? {}),
|
|
@@ -57,7 +77,7 @@ export function badGatewayResponse (url: string, body?: SupportedBodyTypes, init
|
|
|
57
77
|
return response
|
|
58
78
|
}
|
|
59
79
|
|
|
60
|
-
export function
|
|
80
|
+
export function notImplementedResponse (url: string, body?: SupportedBodyTypes, init?: ResponseInit): Response {
|
|
61
81
|
const response = new Response(body, {
|
|
62
82
|
...(init ?? {}),
|
|
63
83
|
status: 501,
|
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.join('/')}`, options)
|
|
56
56
|
} catch (err: any) {
|
|
57
57
|
if (options?.signal?.aborted) {
|
|
58
58
|
throw new AbortError(options?.signal?.reason)
|
package/src/verified-fetch.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { dnsLink } from '@helia/dnslink'
|
|
2
2
|
import { ipnsResolver } from '@helia/ipns'
|
|
3
3
|
import { AbortError } from '@libp2p/interface'
|
|
4
|
-
import { prefixLogger } from '@libp2p/logger'
|
|
5
4
|
import { CustomProgressEvent } from 'progress-events'
|
|
6
5
|
import QuickLRU from 'quick-lru'
|
|
7
6
|
import { ByteRangeContextPlugin } from './plugins/plugin-handle-byte-range-context.js'
|
|
@@ -15,6 +14,7 @@ import { RawPlugin } from './plugins/plugin-handle-raw.js'
|
|
|
15
14
|
import { TarPlugin } from './plugins/plugin-handle-tar.js'
|
|
16
15
|
import { URLResolver } from './url-resolver.ts'
|
|
17
16
|
import { contentTypeParser } from './utils/content-type-parser.js'
|
|
17
|
+
import { errorToObject } from './utils/error-to-object.ts'
|
|
18
18
|
import { getContentDispositionFilename } from './utils/get-content-disposition-filename.js'
|
|
19
19
|
import { getETag } from './utils/get-e-tag.js'
|
|
20
20
|
import { getResolvedAcceptHeader } from './utils/get-resolved-accept-header.js'
|
|
@@ -22,7 +22,7 @@ import { getRedirectResponse } from './utils/handle-redirects.js'
|
|
|
22
22
|
import { uriEncodeIPFSPath } from './utils/ipfs-path-to-string.ts'
|
|
23
23
|
import { resourceToSessionCacheKey } from './utils/resource-to-cache-key.js'
|
|
24
24
|
import { setCacheControlHeader } from './utils/response-headers.js'
|
|
25
|
-
import { badRequestResponse, notAcceptableResponse,
|
|
25
|
+
import { badRequestResponse, notAcceptableResponse, internalServerErrorResponse, notImplementedResponse } from './utils/responses.js'
|
|
26
26
|
import { selectOutputType } from './utils/select-output-type.js'
|
|
27
27
|
import { ServerTiming } from './utils/server-timing.js'
|
|
28
28
|
import type { CIDDetail, ContentTypeParser, CreateVerifiedFetchOptions, ResolveURLResult, Resource, ResourceDetail, VerifiedFetchInit as VerifiedFetchOptions } from './index.js'
|
|
@@ -83,7 +83,7 @@ export class VerifiedFetch {
|
|
|
83
83
|
|
|
84
84
|
const pluginOptions: PluginOptions = {
|
|
85
85
|
...init,
|
|
86
|
-
logger:
|
|
86
|
+
logger: helia.logger.forComponent('verified-fetch'),
|
|
87
87
|
getBlockstore: (cid, resource, useSession, options) => this.getBlockstore(cid, resource, useSession, options),
|
|
88
88
|
helia,
|
|
89
89
|
contentTypeParser: this.contentTypeParser,
|
|
@@ -116,8 +116,6 @@ export class VerifiedFetch {
|
|
|
116
116
|
} else {
|
|
117
117
|
this.plugins = defaultPlugins
|
|
118
118
|
}
|
|
119
|
-
|
|
120
|
-
this.log.trace('created VerifiedFetch instance')
|
|
121
119
|
}
|
|
122
120
|
|
|
123
121
|
private getBlockstore (root: CID, resource: string | CID, useSession: boolean = true, options: AbortOptions = {}): Blockstore {
|
|
@@ -160,30 +158,26 @@ export class VerifiedFetch {
|
|
|
160
158
|
// set Content-Disposition header
|
|
161
159
|
let contentDisposition: string | undefined
|
|
162
160
|
|
|
163
|
-
this.log.trace('checking for content disposition')
|
|
164
|
-
|
|
165
161
|
// force download if requested
|
|
166
162
|
if (context?.query?.download === true) {
|
|
163
|
+
this.log.trace('download requested')
|
|
167
164
|
contentDisposition = 'attachment'
|
|
168
|
-
} else {
|
|
169
|
-
this.log.trace('download not requested')
|
|
170
165
|
}
|
|
171
166
|
|
|
172
167
|
// override filename if requested
|
|
173
168
|
if (context?.query?.filename != null) {
|
|
169
|
+
this.log.trace('specific filename requested')
|
|
170
|
+
|
|
174
171
|
if (contentDisposition == null) {
|
|
175
172
|
contentDisposition = 'inline'
|
|
176
173
|
}
|
|
177
174
|
|
|
178
175
|
contentDisposition = `${contentDisposition}; ${getContentDispositionFilename(context.query.filename)}`
|
|
179
|
-
} else {
|
|
180
|
-
this.log.trace('no filename specified in query')
|
|
181
176
|
}
|
|
182
177
|
|
|
183
178
|
if (contentDisposition != null) {
|
|
179
|
+
this.log.trace('content disposition %s', contentDisposition)
|
|
184
180
|
response.headers.set('Content-Disposition', contentDisposition)
|
|
185
|
-
} else {
|
|
186
|
-
this.log.trace('no content disposition specified')
|
|
187
181
|
}
|
|
188
182
|
|
|
189
183
|
if (context?.cid != null && response.headers.get('etag') == null) {
|
|
@@ -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>()
|
|
@@ -251,20 +245,24 @@ export class VerifiedFetch {
|
|
|
251
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('no plugins can handle the current context
|
|
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('no plugins found that can handle request by CID code; exiting pipeline
|
|
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('plugins ready to handle request: ', readyPlugins.map(p => p.id).join(', '))
|
|
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
|
|
@@ -317,7 +322,13 @@ export class VerifiedFetch {
|
|
|
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
|
/**
|
|
@@ -363,7 +374,7 @@ export class VerifiedFetch {
|
|
|
363
374
|
const acceptHeader = getResolvedAcceptHeader({ query: parsedResult.query, headers: options?.headers, logger: this.helia.logger })
|
|
364
375
|
|
|
365
376
|
const accept: AcceptHeader | undefined = selectOutputType(parsedResult.cid, acceptHeader)
|
|
366
|
-
this.log('
|
|
377
|
+
this.log('accept %o', accept)
|
|
367
378
|
|
|
368
379
|
if (acceptHeader != null && accept == null) {
|
|
369
380
|
this.log.error('could not fulfil request based on accept header')
|
|
@@ -394,9 +405,16 @@ export class VerifiedFetch {
|
|
|
394
405
|
|
|
395
406
|
const response = await this.runPluginPipeline(context)
|
|
396
407
|
|
|
397
|
-
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
|
+
}
|
|
398
416
|
|
|
399
|
-
return this.handleFinalResponse(response
|
|
417
|
+
return this.handleFinalResponse(response, context)
|
|
400
418
|
}
|
|
401
419
|
|
|
402
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
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../../src/plugins/errors.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,uBAAuB,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAA;AAE7E;;GAEG;AACH,qBAAa,WAAY,SAAQ,KAAK;IAC7B,IAAI,SAAgB;IACpB,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,OAAO,CAAA;IACd,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IAC7B,QAAQ,CAAC,EAAE,GAAG,CAAA;gBAER,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,kBAAkB;CAOzE;AAED;;;;;;;GAOG;AACH,qBAAa,gBAAiB,SAAQ,WAAW;IACxC,IAAI,SAAqB;gBAEnB,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,uBAAuB;CAI7E"}
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* If a plugin encounters an error, it should throw an instance of this class.
|
|
3
|
-
*/
|
|
4
|
-
export class PluginError extends Error {
|
|
5
|
-
name = 'PluginError';
|
|
6
|
-
code;
|
|
7
|
-
fatal;
|
|
8
|
-
details;
|
|
9
|
-
response;
|
|
10
|
-
constructor(code, message, options) {
|
|
11
|
-
super(message);
|
|
12
|
-
this.code = code;
|
|
13
|
-
this.fatal = options?.fatal ?? false;
|
|
14
|
-
this.details = options?.details;
|
|
15
|
-
this.response = options?.response;
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
/**
|
|
19
|
-
* If a plugin encounters a fatal error and verified-fetch should not continue processing the request, it should throw
|
|
20
|
-
* an instance of this class.
|
|
21
|
-
*
|
|
22
|
-
* Note that you should be very careful when throwing a `PluginFatalError`, as it will stop the request from being
|
|
23
|
-
* processed further. If you do not have a response to return to the client, you should consider throwing a
|
|
24
|
-
* `PluginError` instead.
|
|
25
|
-
*/
|
|
26
|
-
export class PluginFatalError extends PluginError {
|
|
27
|
-
name = 'PluginFatalError';
|
|
28
|
-
constructor(code, message, options) {
|
|
29
|
-
super(code, message, { ...options, fatal: true });
|
|
30
|
-
this.name = 'PluginFatalError';
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
//# sourceMappingURL=errors.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../../../src/plugins/errors.ts"],"names":[],"mappings":"AAEA;;GAEG;AACH,MAAM,OAAO,WAAY,SAAQ,KAAK;IAC7B,IAAI,GAAG,aAAa,CAAA;IACpB,IAAI,CAAQ;IACZ,KAAK,CAAS;IACd,OAAO,CAAsB;IAC7B,QAAQ,CAAM;IAErB,YAAa,IAAY,EAAE,OAAe,EAAE,OAA4B;QACtE,KAAK,CAAC,OAAO,CAAC,CAAA;QACd,IAAI,CAAC,IAAI,GAAG,IAAI,CAAA;QAChB,IAAI,CAAC,KAAK,GAAG,OAAO,EAAE,KAAK,IAAI,KAAK,CAAA;QACpC,IAAI,CAAC,OAAO,GAAG,OAAO,EAAE,OAAO,CAAA;QAC/B,IAAI,CAAC,QAAQ,GAAG,OAAO,EAAE,QAAQ,CAAA;IACnC,CAAC;CACF;AAED;;;;;;;GAOG;AACH,MAAM,OAAO,gBAAiB,SAAQ,WAAW;IACxC,IAAI,GAAG,kBAAkB,CAAA;IAEhC,YAAa,IAAY,EAAE,OAAe,EAAE,OAAgC;QAC1E,KAAK,CAAC,IAAI,EAAE,OAAO,EAAE,EAAE,GAAG,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAA;QACjD,IAAI,CAAC,IAAI,GAAG,kBAAkB,CAAA;IAChC,CAAC;CACF"}
|
package/src/plugins/errors.ts
DELETED
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
import type { FatalPluginErrorOptions, PluginErrorOptions } from './types.js'
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* If a plugin encounters an error, it should throw an instance of this class.
|
|
5
|
-
*/
|
|
6
|
-
export class PluginError extends Error {
|
|
7
|
-
public name = 'PluginError'
|
|
8
|
-
public code: string
|
|
9
|
-
public fatal: boolean
|
|
10
|
-
public details?: Record<string, any>
|
|
11
|
-
public response?: any
|
|
12
|
-
|
|
13
|
-
constructor (code: string, message: string, options?: PluginErrorOptions) {
|
|
14
|
-
super(message)
|
|
15
|
-
this.code = code
|
|
16
|
-
this.fatal = options?.fatal ?? false
|
|
17
|
-
this.details = options?.details
|
|
18
|
-
this.response = options?.response
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* If a plugin encounters a fatal error and verified-fetch should not continue processing the request, it should throw
|
|
24
|
-
* an instance of this class.
|
|
25
|
-
*
|
|
26
|
-
* Note that you should be very careful when throwing a `PluginFatalError`, as it will stop the request from being
|
|
27
|
-
* processed further. If you do not have a response to return to the client, you should consider throwing a
|
|
28
|
-
* `PluginError` instead.
|
|
29
|
-
*/
|
|
30
|
-
export class PluginFatalError extends PluginError {
|
|
31
|
-
public name = 'PluginFatalError'
|
|
32
|
-
|
|
33
|
-
constructor (code: string, message: string, options: FatalPluginErrorOptions) {
|
|
34
|
-
super(code, message, { ...options, fatal: true })
|
|
35
|
-
this.name = 'PluginFatalError'
|
|
36
|
-
}
|
|
37
|
-
}
|