@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.
Files changed (126) hide show
  1. package/README.md +7 -49
  2. package/dist/index.min.js +56 -51
  3. package/dist/index.min.js.map +4 -4
  4. package/dist/src/index.d.ts +12 -52
  5. package/dist/src/index.d.ts.map +1 -1
  6. package/dist/src/index.js +8 -50
  7. package/dist/src/index.js.map +1 -1
  8. package/dist/src/plugins/index.d.ts +0 -1
  9. package/dist/src/plugins/index.d.ts.map +1 -1
  10. package/dist/src/plugins/index.js +0 -1
  11. package/dist/src/plugins/index.js.map +1 -1
  12. package/dist/src/plugins/plugin-base.d.ts.map +1 -1
  13. package/dist/src/plugins/plugin-base.js +3 -2
  14. package/dist/src/plugins/plugin-base.js.map +1 -1
  15. package/dist/src/plugins/plugin-handle-car.d.ts.map +1 -1
  16. package/dist/src/plugins/plugin-handle-car.js +0 -1
  17. package/dist/src/plugins/plugin-handle-car.js.map +1 -1
  18. package/dist/src/plugins/plugin-handle-dag-cbor-html-preview.d.ts +3 -3
  19. package/dist/src/plugins/plugin-handle-dag-cbor-html-preview.d.ts.map +1 -1
  20. package/dist/src/plugins/plugin-handle-dag-cbor-html-preview.js +5 -6
  21. package/dist/src/plugins/plugin-handle-dag-cbor-html-preview.js.map +1 -1
  22. package/dist/src/plugins/plugin-handle-dag-cbor.d.ts.map +1 -1
  23. package/dist/src/plugins/plugin-handle-dag-cbor.js +0 -1
  24. package/dist/src/plugins/plugin-handle-dag-cbor.js.map +1 -1
  25. package/dist/src/plugins/plugin-handle-dag-pb.d.ts.map +1 -1
  26. package/dist/src/plugins/plugin-handle-dag-pb.js +34 -39
  27. package/dist/src/plugins/plugin-handle-dag-pb.js.map +1 -1
  28. package/dist/src/plugins/plugin-handle-dag-walk.d.ts +8 -4
  29. package/dist/src/plugins/plugin-handle-dag-walk.d.ts.map +1 -1
  30. package/dist/src/plugins/plugin-handle-dag-walk.js +8 -5
  31. package/dist/src/plugins/plugin-handle-dag-walk.js.map +1 -1
  32. package/dist/src/plugins/plugin-handle-ipns-record.d.ts +1 -1
  33. package/dist/src/plugins/plugin-handle-ipns-record.d.ts.map +1 -1
  34. package/dist/src/plugins/plugin-handle-ipns-record.js +4 -6
  35. package/dist/src/plugins/plugin-handle-ipns-record.js.map +1 -1
  36. package/dist/src/plugins/plugin-handle-json.d.ts.map +1 -1
  37. package/dist/src/plugins/plugin-handle-json.js +0 -1
  38. package/dist/src/plugins/plugin-handle-json.js.map +1 -1
  39. package/dist/src/plugins/plugin-handle-raw.d.ts.map +1 -1
  40. package/dist/src/plugins/plugin-handle-raw.js +4 -8
  41. package/dist/src/plugins/plugin-handle-raw.js.map +1 -1
  42. package/dist/src/plugins/plugin-handle-tar.d.ts.map +1 -1
  43. package/dist/src/plugins/plugin-handle-tar.js +0 -1
  44. package/dist/src/plugins/plugin-handle-tar.js.map +1 -1
  45. package/dist/src/plugins/types.d.ts +6 -11
  46. package/dist/src/plugins/types.d.ts.map +1 -1
  47. package/dist/src/url-resolver.d.ts +4 -3
  48. package/dist/src/url-resolver.d.ts.map +1 -1
  49. package/dist/src/url-resolver.js +35 -47
  50. package/dist/src/url-resolver.js.map +1 -1
  51. package/dist/src/utils/byte-range-context.d.ts +2 -2
  52. package/dist/src/utils/byte-range-context.d.ts.map +1 -1
  53. package/dist/src/utils/byte-range-context.js +1 -1
  54. package/dist/src/utils/byte-range-context.js.map +1 -1
  55. package/dist/src/utils/content-type-parser.d.ts.map +1 -1
  56. package/dist/src/utils/content-type-parser.js +0 -10
  57. package/dist/src/utils/content-type-parser.js.map +1 -1
  58. package/dist/src/utils/dnslink-label.d.ts +26 -0
  59. package/dist/src/utils/dnslink-label.d.ts.map +1 -0
  60. package/dist/src/utils/dnslink-label.js +35 -0
  61. package/dist/src/utils/dnslink-label.js.map +1 -0
  62. package/dist/src/utils/error-to-object.d.ts +6 -0
  63. package/dist/src/utils/error-to-object.d.ts.map +1 -0
  64. package/dist/src/utils/error-to-object.js +20 -0
  65. package/dist/src/utils/error-to-object.js.map +1 -0
  66. package/dist/src/utils/get-content-type.d.ts +1 -1
  67. package/dist/src/utils/get-content-type.d.ts.map +1 -1
  68. package/dist/src/utils/get-content-type.js +1 -1
  69. package/dist/src/utils/get-content-type.js.map +1 -1
  70. package/dist/src/utils/get-stream-from-async-iterable.d.ts +1 -2
  71. package/dist/src/utils/get-stream-from-async-iterable.d.ts.map +1 -1
  72. package/dist/src/utils/get-stream-from-async-iterable.js +1 -3
  73. package/dist/src/utils/get-stream-from-async-iterable.js.map +1 -1
  74. package/dist/src/utils/handle-redirects.js +2 -2
  75. package/dist/src/utils/ipfs-path-to-url.d.ts +16 -0
  76. package/dist/src/utils/ipfs-path-to-url.d.ts.map +1 -0
  77. package/dist/src/utils/ipfs-path-to-url.js +45 -0
  78. package/dist/src/utils/ipfs-path-to-url.js.map +1 -0
  79. package/dist/src/utils/parse-url-string.d.ts +18 -5
  80. package/dist/src/utils/parse-url-string.d.ts.map +1 -1
  81. package/dist/src/utils/parse-url-string.js +126 -44
  82. package/dist/src/utils/parse-url-string.js.map +1 -1
  83. package/dist/src/utils/resource-to-cache-key.js +2 -2
  84. package/dist/src/utils/responses.d.ts +2 -1
  85. package/dist/src/utils/responses.d.ts.map +1 -1
  86. package/dist/src/utils/responses.js +16 -1
  87. package/dist/src/utils/responses.js.map +1 -1
  88. package/dist/src/utils/walk-path.js +1 -1
  89. package/dist/src/utils/walk-path.js.map +1 -1
  90. package/dist/src/verified-fetch.d.ts.map +1 -1
  91. package/dist/src/verified-fetch.js +36 -28
  92. package/dist/src/verified-fetch.js.map +1 -1
  93. package/dist/typedoc-urls.json +0 -2
  94. package/package.json +10 -10
  95. package/src/index.ts +12 -52
  96. package/src/plugins/index.ts +0 -1
  97. package/src/plugins/plugin-base.ts +3 -2
  98. package/src/plugins/plugin-handle-car.ts +0 -2
  99. package/src/plugins/plugin-handle-dag-cbor-html-preview.ts +10 -9
  100. package/src/plugins/plugin-handle-dag-cbor.ts +3 -1
  101. package/src/plugins/plugin-handle-dag-pb.ts +49 -37
  102. package/src/plugins/plugin-handle-dag-walk.ts +10 -5
  103. package/src/plugins/plugin-handle-ipns-record.ts +6 -6
  104. package/src/plugins/plugin-handle-json.ts +1 -1
  105. package/src/plugins/plugin-handle-raw.ts +7 -8
  106. package/src/plugins/plugin-handle-tar.ts +2 -1
  107. package/src/plugins/types.ts +7 -12
  108. package/src/url-resolver.ts +37 -56
  109. package/src/utils/byte-range-context.ts +3 -3
  110. package/src/utils/content-type-parser.ts +5 -11
  111. package/src/utils/dnslink-label.ts +38 -0
  112. package/src/utils/error-to-object.ts +22 -0
  113. package/src/utils/get-content-type.ts +2 -2
  114. package/src/utils/get-stream-from-async-iterable.ts +1 -4
  115. package/src/utils/handle-redirects.ts +2 -2
  116. package/src/utils/ipfs-path-to-url.ts +54 -0
  117. package/src/utils/parse-url-string.ts +166 -49
  118. package/src/utils/resource-to-cache-key.ts +2 -2
  119. package/src/utils/responses.ts +21 -1
  120. package/src/utils/walk-path.ts +1 -1
  121. package/src/verified-fetch.ts +46 -28
  122. package/dist/src/plugins/errors.d.ts +0 -25
  123. package/dist/src/plugins/errors.d.ts.map +0 -1
  124. package/dist/src/plugins/errors.js +0 -33
  125. package/dist/src/plugins/errors.js.map +0 -1
  126. package/src/plugins/errors.ts +0 -37
@@ -1,77 +1,194 @@
1
- const URL_REGEX = /^(?<protocol>ip[fn]s):\/\/(?<cidOrPeerIdOrDnsLink>[^/?]+)\/?(?<path>[^?]*)\??(?<query>.*)$/
2
- const PATH_REGEX = /^\/(?<protocol>ip[fn]s)\/(?<cidOrPeerIdOrDnsLink>[^/?]+)\/?(?<path>[^?]*)\??(?<query>.*)$/
3
- const PATH_GATEWAY_REGEX = /^https?:\/\/(.*[^/])\/(?<protocol>ip[fn]s)\/(?<cidOrPeerIdOrDnsLink>[^/?]+)\/?(?<path>[^?]*)\??(?<query>.*)$/
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 MatchUrlGroups {
5
+ interface SubdomainMatchGroups {
7
6
  protocol: 'ipfs' | 'ipns'
8
7
  cidOrPeerIdOrDnsLink: string
9
- path?: string
10
- query?: string
11
8
  }
12
9
 
13
- function matchUrlGroupsGuard (groups?: null | { [key in string]: string; } | MatchUrlGroups): groups is MatchUrlGroups {
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
- if (protocol == null) { return false }
14
+
15
+ if (protocol !== 'ipfs' && protocol !== 'ipns') {
16
+ return false
17
+ }
18
+
16
19
  const cidOrPeerIdOrDnsLink = groups?.cidOrPeerIdOrDnsLink
17
- if (cidOrPeerIdOrDnsLink == null) { return false }
18
- const path = groups?.path
19
- const query = groups?.query
20
-
21
- return ['ipns', 'ipfs'].includes(protocol) &&
22
- typeof cidOrPeerIdOrDnsLink === 'string' &&
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
- // TODO: can this be replaced with `new URL`?
28
- export function matchURLString (urlString: string): MatchUrlGroups {
29
- for (const pattern of [SUBDOMAIN_GATEWAY_REGEX, URL_REGEX, PATH_GATEWAY_REGEX, PATH_REGEX]) {
30
- const match = urlString.match(pattern)
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
- if (matchUrlGroupsGuard(match?.groups)) {
33
- const groups = match.groups satisfies MatchUrlGroups
37
+ function toQuery (query?: URLSearchParams): Record<string, any> {
38
+ if (query == null) {
39
+ return {}
40
+ }
34
41
 
35
- if (groups.path != null) {
36
- groups.path = decodeURIComponent(groups.path)
37
- }
42
+ const output: Record<string, any> = {}
38
43
 
39
- // decode inline dnslink domain if present
40
- if (pattern === SUBDOMAIN_GATEWAY_REGEX && groups.protocol === 'ipns' && isInlinedDnsLink(groups.cidOrPeerIdOrDnsLink)) {
41
- groups.cidOrPeerIdOrDnsLink = dnsLinkLabelDecoder(groups.cidOrPeerIdOrDnsLink)
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
- return groups
51
+ if (value === 'false') {
52
+ output[key] = false
45
53
  }
46
54
  }
47
55
 
48
- throw new TypeError(`Invalid URL: ${urlString}, please use ipfs://, ipns://, or gateway URLs only`)
56
+ return output
49
57
  }
50
58
 
51
- /**
52
- * For DNSLink see https://specs.ipfs.tech/http-gateways/subdomain-gateway/#host-request-header
53
- * DNSLink names include . which means they must be inlined into a single DNS label to provide unique origin and work with wildcard TLS certificates.
54
- */
59
+ function stripLeadingHash (pathname: string): string {
60
+ return stripLeading(pathname, '#')
61
+ }
55
62
 
56
- // DNS label can have up to 63 characters, consisting of alphanumeric
57
- // characters or hyphens -, but it must not start or end with a hyphen.
58
- const dnsLabelRegex = /^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$/
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
- * Checks if label looks like inlined DNSLink.
62
- * (https://specs.ipfs.tech/http-gateways/subdomain-gateway/#host-request-header)
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 isInlinedDnsLink (label: string): boolean {
65
- return dnsLabelRegex.test(label) && label.includes('-') && !label.includes('.')
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
- * DNSLink label decoding
70
- * - Every standalone - is replaced with .
71
- * - Every remaining -- is replaced with -
105
+ * Accepts the following url strings:
72
106
  *
73
- * @example en-wikipedia--on--ipfs-org.ipns.example.net -> example.net/ipns/en.wikipedia-on-ipfs.org
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 dnsLinkLabelDecoder (linkLabel: string): string {
76
- return linkLabel.replace(/--/g, '%').replace(/-/g, '.').replace(/%/g, '-')
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 { matchURLString } from './parse-url-string.js'
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 } = matchURLString(url.toString())
27
+ const { protocol, cidOrPeerIdOrDnsLink } = parseURLString(url.toString())
28
28
 
29
29
  return `${protocol}://${cidOrPeerIdOrDnsLink}`
30
30
  }
@@ -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 notSupportedResponse (url: string, body?: SupportedBodyTypes, init?: ResponseInit): Response {
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,
@@ -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.toString()}/${path}`, options)
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)
@@ -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, notSupportedResponse, badGatewayResponse } from './utils/responses.js'
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: prefixLogger('helia:verified-fetch'),
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 | undefined> {
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.. checking by CID code')
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.trace('invoking plugin:', plugin.id)
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
- this.log.error('error in plugin %s - %e', plugin.constructor.name, err)
290
- // if fatal, short-circuit the pipeline
291
- if (err.name === 'PluginFatalError') {
292
- // if plugin provides a custom error response, return it
293
- return err.response ?? badGatewayResponse(context.resource, 'Failed to fetch')
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('output type %s', accept)
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', { cid: parsedResult.cid, path: parsedResult.path }))
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 ?? notSupportedResponse(resource.toString()), context)
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"}
@@ -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
- }