@helia/verified-fetch 4.1.0 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (231) hide show
  1. package/README.md +6 -40
  2. package/dist/index.min.js +73 -534
  3. package/dist/index.min.js.map +4 -4
  4. package/dist/src/constants.d.ts +2 -0
  5. package/dist/src/constants.d.ts.map +1 -1
  6. package/dist/src/constants.js +2 -0
  7. package/dist/src/constants.js.map +1 -1
  8. package/dist/src/index.d.ts +162 -68
  9. package/dist/src/index.d.ts.map +1 -1
  10. package/dist/src/index.js +11 -43
  11. package/dist/src/index.js.map +1 -1
  12. package/dist/src/plugins/index.d.ts +0 -5
  13. package/dist/src/plugins/index.d.ts.map +1 -1
  14. package/dist/src/plugins/index.js +0 -4
  15. package/dist/src/plugins/index.js.map +1 -1
  16. package/dist/src/plugins/plugin-base.d.ts +8 -9
  17. package/dist/src/plugins/plugin-base.d.ts.map +1 -1
  18. package/dist/src/plugins/plugin-base.js +5 -6
  19. package/dist/src/plugins/plugin-base.js.map +1 -1
  20. package/dist/src/plugins/plugin-handle-car.d.ts +3 -3
  21. package/dist/src/plugins/plugin-handle-car.d.ts.map +1 -1
  22. package/dist/src/plugins/plugin-handle-car.js +38 -39
  23. package/dist/src/plugins/plugin-handle-car.js.map +1 -1
  24. package/dist/src/plugins/plugin-handle-ipld.d.ts +12 -0
  25. package/dist/src/plugins/plugin-handle-ipld.d.ts.map +1 -0
  26. package/dist/src/plugins/plugin-handle-ipld.js +83 -0
  27. package/dist/src/plugins/plugin-handle-ipld.js.map +1 -0
  28. package/dist/src/plugins/plugin-handle-ipns-record.d.ts +3 -3
  29. package/dist/src/plugins/plugin-handle-ipns-record.d.ts.map +1 -1
  30. package/dist/src/plugins/plugin-handle-ipns-record.js +25 -34
  31. package/dist/src/plugins/plugin-handle-ipns-record.js.map +1 -1
  32. package/dist/src/plugins/plugin-handle-tar.d.ts +3 -3
  33. package/dist/src/plugins/plugin-handle-tar.d.ts.map +1 -1
  34. package/dist/src/plugins/plugin-handle-tar.js +20 -22
  35. package/dist/src/plugins/plugin-handle-tar.js.map +1 -1
  36. package/dist/src/plugins/plugin-handle-unixfs.d.ts +14 -0
  37. package/dist/src/plugins/plugin-handle-unixfs.d.ts.map +1 -0
  38. package/dist/src/plugins/plugin-handle-unixfs.js +180 -0
  39. package/dist/src/plugins/plugin-handle-unixfs.js.map +1 -0
  40. package/dist/src/plugins/types.d.ts +1 -77
  41. package/dist/src/plugins/types.d.ts.map +1 -1
  42. package/dist/src/url-resolver.d.ts +29 -11
  43. package/dist/src/url-resolver.d.ts.map +1 -1
  44. package/dist/src/url-resolver.js +152 -74
  45. package/dist/src/url-resolver.js.map +1 -1
  46. package/dist/src/utils/content-type-parser.d.ts.map +1 -1
  47. package/dist/src/utils/content-type-parser.js +4 -3
  48. package/dist/src/utils/content-type-parser.js.map +1 -1
  49. package/dist/src/utils/content-types.d.ts +26 -0
  50. package/dist/src/utils/content-types.d.ts.map +1 -0
  51. package/dist/src/utils/content-types.js +137 -0
  52. package/dist/src/utils/content-types.js.map +1 -0
  53. package/dist/src/utils/convert-output.d.ts +17 -0
  54. package/dist/src/utils/convert-output.d.ts.map +1 -0
  55. package/dist/src/utils/convert-output.js +176 -0
  56. package/dist/src/utils/convert-output.js.map +1 -0
  57. package/dist/src/utils/error-to-response.d.ts +3 -0
  58. package/dist/src/utils/error-to-response.d.ts.map +1 -0
  59. package/dist/src/utils/error-to-response.js +40 -0
  60. package/dist/src/utils/error-to-response.js.map +1 -0
  61. package/dist/src/utils/get-content-disposition-filename.d.ts +1 -1
  62. package/dist/src/utils/get-content-disposition-filename.d.ts.map +1 -1
  63. package/dist/src/utils/get-content-disposition-filename.js +4 -0
  64. package/dist/src/utils/get-content-disposition-filename.js.map +1 -1
  65. package/dist/src/utils/get-e-tag.d.ts +20 -15
  66. package/dist/src/utils/get-e-tag.d.ts.map +1 -1
  67. package/dist/src/utils/get-e-tag.js +8 -22
  68. package/dist/src/utils/get-e-tag.js.map +1 -1
  69. package/dist/src/utils/get-offset-and-length.d.ts +12 -2
  70. package/dist/src/utils/get-offset-and-length.d.ts.map +1 -1
  71. package/dist/src/utils/get-offset-and-length.js +63 -21
  72. package/dist/src/utils/get-offset-and-length.js.map +1 -1
  73. package/dist/src/utils/get-range-header.d.ts +22 -0
  74. package/dist/src/utils/get-range-header.d.ts.map +1 -0
  75. package/dist/src/utils/get-range-header.js +69 -0
  76. package/dist/src/utils/get-range-header.js.map +1 -0
  77. package/dist/src/utils/parse-url-string.d.ts +2 -1
  78. package/dist/src/utils/parse-url-string.d.ts.map +1 -1
  79. package/dist/src/utils/parse-url-string.js +46 -71
  80. package/dist/src/utils/parse-url-string.js.map +1 -1
  81. package/dist/src/utils/resource-to-cache-key.d.ts +3 -3
  82. package/dist/src/utils/resource-to-cache-key.js +5 -5
  83. package/dist/src/utils/resource-to-cache-key.js.map +1 -1
  84. package/dist/src/utils/response-headers.d.ts +4 -14
  85. package/dist/src/utils/response-headers.d.ts.map +1 -1
  86. package/dist/src/utils/response-headers.js +36 -36
  87. package/dist/src/utils/response-headers.js.map +1 -1
  88. package/dist/src/utils/responses.d.ts +30 -11
  89. package/dist/src/utils/responses.d.ts.map +1 -1
  90. package/dist/src/utils/responses.js +146 -39
  91. package/dist/src/utils/responses.js.map +1 -1
  92. package/dist/src/verified-fetch.d.ts +16 -15
  93. package/dist/src/verified-fetch.d.ts.map +1 -1
  94. package/dist/src/verified-fetch.js +307 -236
  95. package/dist/src/verified-fetch.js.map +1 -1
  96. package/dist/typedoc-urls.json +64 -45
  97. package/package.json +4 -3
  98. package/src/constants.ts +3 -0
  99. package/src/index.ts +203 -71
  100. package/src/plugins/index.ts +0 -6
  101. package/src/plugins/plugin-base.ts +8 -10
  102. package/src/plugins/plugin-handle-car.ts +48 -46
  103. package/src/plugins/plugin-handle-ipld.ts +93 -0
  104. package/src/plugins/plugin-handle-ipns-record.ts +31 -41
  105. package/src/plugins/plugin-handle-tar.ts +25 -29
  106. package/src/plugins/plugin-handle-unixfs.ts +217 -0
  107. package/src/plugins/types.ts +0 -86
  108. package/src/url-resolver.ts +197 -83
  109. package/src/utils/content-type-parser.ts +4 -3
  110. package/src/utils/content-types.ts +159 -0
  111. package/src/utils/convert-output.ts +187 -0
  112. package/src/utils/error-to-response.ts +49 -0
  113. package/src/utils/get-content-disposition-filename.ts +7 -1
  114. package/src/utils/get-e-tag.ts +26 -35
  115. package/src/utils/get-offset-and-length.ts +75 -21
  116. package/src/utils/get-range-header.ts +107 -0
  117. package/src/utils/parse-url-string.ts +51 -80
  118. package/src/utils/resource-to-cache-key.ts +5 -5
  119. package/src/utils/response-headers.ts +40 -41
  120. package/src/utils/responses.ts +186 -45
  121. package/src/verified-fetch.ts +359 -267
  122. package/dist/src/plugins/plugin-handle-byte-range-context.d.ts +0 -14
  123. package/dist/src/plugins/plugin-handle-byte-range-context.d.ts.map +0 -1
  124. package/dist/src/plugins/plugin-handle-byte-range-context.js +0 -25
  125. package/dist/src/plugins/plugin-handle-byte-range-context.js.map +0 -1
  126. package/dist/src/plugins/plugin-handle-cbor.d.ts +0 -17
  127. package/dist/src/plugins/plugin-handle-cbor.d.ts.map +0 -1
  128. package/dist/src/plugins/plugin-handle-cbor.js +0 -94
  129. package/dist/src/plugins/plugin-handle-cbor.js.map +0 -1
  130. package/dist/src/plugins/plugin-handle-dag-cbor-html-preview.d.ts +0 -27
  131. package/dist/src/plugins/plugin-handle-dag-cbor-html-preview.d.ts.map +0 -1
  132. package/dist/src/plugins/plugin-handle-dag-cbor-html-preview.js +0 -279
  133. package/dist/src/plugins/plugin-handle-dag-cbor-html-preview.js.map +0 -1
  134. package/dist/src/plugins/plugin-handle-dag-cbor.d.ts +0 -17
  135. package/dist/src/plugins/plugin-handle-dag-cbor.d.ts.map +0 -1
  136. package/dist/src/plugins/plugin-handle-dag-cbor.js +0 -66
  137. package/dist/src/plugins/plugin-handle-dag-cbor.js.map +0 -1
  138. package/dist/src/plugins/plugin-handle-dag-pb.d.ts +0 -17
  139. package/dist/src/plugins/plugin-handle-dag-pb.d.ts.map +0 -1
  140. package/dist/src/plugins/plugin-handle-dag-pb.js +0 -209
  141. package/dist/src/plugins/plugin-handle-dag-pb.js.map +0 -1
  142. package/dist/src/plugins/plugin-handle-dag-walk.d.ts +0 -21
  143. package/dist/src/plugins/plugin-handle-dag-walk.d.ts.map +0 -1
  144. package/dist/src/plugins/plugin-handle-dag-walk.js +0 -95
  145. package/dist/src/plugins/plugin-handle-dag-walk.js.map +0 -1
  146. package/dist/src/plugins/plugin-handle-dir-index-html.d.ts +0 -10
  147. package/dist/src/plugins/plugin-handle-dir-index-html.d.ts.map +0 -1
  148. package/dist/src/plugins/plugin-handle-dir-index-html.js +0 -59
  149. package/dist/src/plugins/plugin-handle-dir-index-html.js.map +0 -1
  150. package/dist/src/plugins/plugin-handle-json.d.ts +0 -12
  151. package/dist/src/plugins/plugin-handle-json.d.ts.map +0 -1
  152. package/dist/src/plugins/plugin-handle-json.js +0 -73
  153. package/dist/src/plugins/plugin-handle-json.js.map +0 -1
  154. package/dist/src/plugins/plugin-handle-raw.d.ts +0 -9
  155. package/dist/src/plugins/plugin-handle-raw.d.ts.map +0 -1
  156. package/dist/src/plugins/plugin-handle-raw.js +0 -92
  157. package/dist/src/plugins/plugin-handle-raw.js.map +0 -1
  158. package/dist/src/plugins/plugins.d.ts +0 -6
  159. package/dist/src/plugins/plugins.d.ts.map +0 -1
  160. package/dist/src/plugins/plugins.js +0 -6
  161. package/dist/src/plugins/plugins.js.map +0 -1
  162. package/dist/src/utils/byte-range-context.d.ts +0 -103
  163. package/dist/src/utils/byte-range-context.d.ts.map +0 -1
  164. package/dist/src/utils/byte-range-context.js +0 -504
  165. package/dist/src/utils/byte-range-context.js.map +0 -1
  166. package/dist/src/utils/dag-cbor-to-safe-json.d.ts +0 -15
  167. package/dist/src/utils/dag-cbor-to-safe-json.d.ts.map +0 -1
  168. package/dist/src/utils/dag-cbor-to-safe-json.js +0 -54
  169. package/dist/src/utils/dag-cbor-to-safe-json.js.map +0 -1
  170. package/dist/src/utils/dir-index-html.d.ts +0 -19
  171. package/dist/src/utils/dir-index-html.d.ts.map +0 -1
  172. package/dist/src/utils/dir-index-html.js +0 -438
  173. package/dist/src/utils/dir-index-html.js.map +0 -1
  174. package/dist/src/utils/get-peer-id-from-string.d.ts +0 -3
  175. package/dist/src/utils/get-peer-id-from-string.d.ts.map +0 -1
  176. package/dist/src/utils/get-peer-id-from-string.js +0 -10
  177. package/dist/src/utils/get-peer-id-from-string.js.map +0 -1
  178. package/dist/src/utils/get-resolved-accept-header.d.ts +0 -9
  179. package/dist/src/utils/get-resolved-accept-header.d.ts.map +0 -1
  180. package/dist/src/utils/get-resolved-accept-header.js +0 -27
  181. package/dist/src/utils/get-resolved-accept-header.js.map +0 -1
  182. package/dist/src/utils/get-stream-from-async-iterable.d.ts +0 -9
  183. package/dist/src/utils/get-stream-from-async-iterable.d.ts.map +0 -1
  184. package/dist/src/utils/get-stream-from-async-iterable.js +0 -43
  185. package/dist/src/utils/get-stream-from-async-iterable.js.map +0 -1
  186. package/dist/src/utils/handle-redirects.d.ts +0 -16
  187. package/dist/src/utils/handle-redirects.d.ts.map +0 -1
  188. package/dist/src/utils/handle-redirects.js +0 -84
  189. package/dist/src/utils/handle-redirects.js.map +0 -1
  190. package/dist/src/utils/is-accept-explicit.d.ts +0 -15
  191. package/dist/src/utils/is-accept-explicit.d.ts.map +0 -1
  192. package/dist/src/utils/is-accept-explicit.js +0 -26
  193. package/dist/src/utils/is-accept-explicit.js.map +0 -1
  194. package/dist/src/utils/request-headers.d.ts +0 -13
  195. package/dist/src/utils/request-headers.d.ts.map +0 -1
  196. package/dist/src/utils/request-headers.js +0 -63
  197. package/dist/src/utils/request-headers.js.map +0 -1
  198. package/dist/src/utils/select-output-type.d.ts +0 -17
  199. package/dist/src/utils/select-output-type.d.ts.map +0 -1
  200. package/dist/src/utils/select-output-type.js +0 -153
  201. package/dist/src/utils/select-output-type.js.map +0 -1
  202. package/dist/src/utils/tlru.d.ts +0 -15
  203. package/dist/src/utils/tlru.d.ts.map +0 -1
  204. package/dist/src/utils/tlru.js +0 -34
  205. package/dist/src/utils/tlru.js.map +0 -1
  206. package/dist/src/utils/walk-path.d.ts +0 -27
  207. package/dist/src/utils/walk-path.d.ts.map +0 -1
  208. package/dist/src/utils/walk-path.js +0 -45
  209. package/dist/src/utils/walk-path.js.map +0 -1
  210. package/src/plugins/plugin-handle-byte-range-context.ts +0 -30
  211. package/src/plugins/plugin-handle-cbor.ts +0 -107
  212. package/src/plugins/plugin-handle-dag-cbor-html-preview.ts +0 -295
  213. package/src/plugins/plugin-handle-dag-cbor.ts +0 -83
  214. package/src/plugins/plugin-handle-dag-pb.ts +0 -248
  215. package/src/plugins/plugin-handle-dag-walk.ts +0 -110
  216. package/src/plugins/plugin-handle-dir-index-html.ts +0 -72
  217. package/src/plugins/plugin-handle-json.ts +0 -80
  218. package/src/plugins/plugin-handle-raw.ts +0 -110
  219. package/src/plugins/plugins.ts +0 -5
  220. package/src/utils/byte-range-context.ts +0 -597
  221. package/src/utils/dag-cbor-to-safe-json.ts +0 -63
  222. package/src/utils/dir-index-html.ts +0 -505
  223. package/src/utils/get-peer-id-from-string.ts +0 -12
  224. package/src/utils/get-resolved-accept-header.ts +0 -42
  225. package/src/utils/get-stream-from-async-iterable.ts +0 -49
  226. package/src/utils/handle-redirects.ts +0 -109
  227. package/src/utils/is-accept-explicit.ts +0 -38
  228. package/src/utils/request-headers.ts +0 -65
  229. package/src/utils/select-output-type.ts +0 -175
  230. package/src/utils/tlru.ts +0 -42
  231. package/src/utils/walk-path.ts +0 -68
@@ -1,71 +1,86 @@
1
1
  import { dnsLink } from '@helia/dnslink'
2
2
  import { ipnsResolver } from '@helia/ipns'
3
3
  import { AbortError } from '@libp2p/interface'
4
+ import { CID } from 'multiformats/cid'
4
5
  import { CustomProgressEvent } from 'progress-events'
5
- import QuickLRU from 'quick-lru'
6
- import { ByteRangeContextPlugin } from './plugins/plugin-handle-byte-range-context.js'
6
+ import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string'
7
+ import { toString as uint8ArrayToString } from 'uint8arrays/to-string'
7
8
  import { CarPlugin } from './plugins/plugin-handle-car.js'
8
- import { CborPlugin } from './plugins/plugin-handle-cbor.js'
9
- import { DagCborPlugin } from './plugins/plugin-handle-dag-cbor.js'
10
- import { DagPbPlugin } from './plugins/plugin-handle-dag-pb.js'
11
- import { DagWalkPlugin } from './plugins/plugin-handle-dag-walk.js'
9
+ import { IpldPlugin } from './plugins/plugin-handle-ipld.js'
12
10
  import { IpnsRecordPlugin } from './plugins/plugin-handle-ipns-record.js'
13
- import { JsonPlugin } from './plugins/plugin-handle-json.js'
14
- import { RawPlugin } from './plugins/plugin-handle-raw.js'
15
11
  import { TarPlugin } from './plugins/plugin-handle-tar.js'
12
+ import { UnixFSPlugin } from './plugins/plugin-handle-unixfs.js'
16
13
  import { URLResolver } from './url-resolver.ts'
17
14
  import { contentTypeParser } from './utils/content-type-parser.js'
15
+ import { getContentType, getSupportedContentTypes, CONTENT_TYPE_OCTET_STREAM, CONTENT_TYPE_CAR, MEDIA_TYPE_IPNS_RECORD, MEDIA_TYPE_RAW } from './utils/content-types.ts'
18
16
  import { errorToObject } from './utils/error-to-object.ts'
19
- import { getContentDispositionFilename } from './utils/get-content-disposition-filename.js'
17
+ import { errorToResponse } from './utils/error-to-response.ts'
20
18
  import { getETag } from './utils/get-e-tag.js'
21
- import { getResolvedAcceptHeader } from './utils/get-resolved-accept-header.js'
22
- import { getRedirectResponse } from './utils/handle-redirects.js'
23
- import { uriEncodeIPFSPath } from './utils/ipfs-path-to-string.ts'
24
- import { resourceToSessionCacheKey } from './utils/resource-to-cache-key.js'
19
+ import { getRangeHeader } from './utils/get-range-header.ts'
20
+ import { parseURLString } from './utils/parse-url-string.ts'
25
21
  import { setCacheControlHeader } from './utils/response-headers.js'
26
- import { badRequestResponse, notAcceptableResponse, internalServerErrorResponse, notImplementedResponse } from './utils/responses.js'
27
- import { selectOutputType } from './utils/select-output-type.js'
22
+ import { badRequestResponse, internalServerErrorResponse, notAcceptableResponse, notImplementedResponse } from './utils/responses.js'
28
23
  import { ServerTiming } from './utils/server-timing.js'
29
- import type { CIDDetail, ContentTypeParser, CreateVerifiedFetchOptions, ResolveURLResult, Resource, ResourceDetail, VerifiedFetchInit as VerifiedFetchOptions } from './index.js'
30
- import type { VerifiedFetchPlugin, PluginContext, PluginOptions } from './plugins/types.js'
31
- import type { AcceptHeader } from './utils/select-output-type.js'
24
+ import type { AcceptHeader, CIDDetail, ContentTypeParser, CreateVerifiedFetchOptions, ResolveURLResult, Resource, ResourceDetail, VerifiedFetchInit as VerifiedFetchOptions, VerifiedFetchPlugin, PluginContext, PluginOptions } from './index.js'
32
25
  import type { DNSLink } from '@helia/dnslink'
33
- import type { Helia, SessionBlockstore } from '@helia/interface'
26
+ import type { Helia } from '@helia/interface'
34
27
  import type { IPNSResolver } from '@helia/ipns'
35
28
  import type { AbortOptions, Logger } from '@libp2p/interface'
36
- import type { Blockstore } from 'interface-blockstore'
37
- import type { CID } from 'multiformats/cid'
38
-
39
- const SESSION_CACHE_MAX_SIZE = 100
40
- const SESSION_CACHE_TTL_MS = 60 * 1000
41
29
 
30
+ /**
31
+ * Retypes the `.signal` property of the options from
32
+ * `AbortSignal | null | undefined` to `AbortSignal | undefined`.
33
+ */
42
34
  function convertOptions (options?: VerifiedFetchOptions): (Omit<VerifiedFetchOptions, 'signal'> & AbortOptions) | undefined {
43
35
  if (options == null) {
44
- return undefined
45
- }
46
-
47
- let signal: AbortSignal | undefined
48
- if (options?.signal === null) {
49
- signal = undefined
50
- } else {
51
- signal = options?.signal
36
+ return
52
37
  }
53
38
 
54
39
  return {
55
40
  ...options,
56
- signal
41
+ signal: options?.signal == null ? undefined : options?.signal
57
42
  }
58
43
  }
59
44
 
45
+ /**
46
+ * Returns true if the quest is only for an IPNS record
47
+ */
48
+ function isIPNSRecordRequest (headers: Headers): boolean {
49
+ const acceptHeaders = headers.get('accept')?.split(',') ?? []
50
+
51
+ if (acceptHeaders.length !== 1) {
52
+ return false
53
+ }
54
+
55
+ const mediaType = acceptHeaders[0].split(';')[0]
56
+
57
+ return mediaType === MEDIA_TYPE_IPNS_RECORD
58
+ }
59
+
60
+ /**
61
+ * Returns true if the quest is only for an IPNS record
62
+ */
63
+ function isRawBlockRequest (headers: Headers): boolean {
64
+ const acceptHeaders = headers.get('accept')?.split(',') ?? []
65
+
66
+ if (acceptHeaders.length !== 1) {
67
+ return false
68
+ }
69
+
70
+ const mediaType = acceptHeaders[0].split(';')[0]
71
+
72
+ return mediaType === MEDIA_TYPE_RAW
73
+ }
74
+
60
75
  export class VerifiedFetch {
61
76
  private readonly helia: Helia
62
77
  private readonly ipnsResolver: IPNSResolver
63
78
  private readonly dnsLink: DNSLink
64
79
  private readonly log: Logger
65
80
  private readonly contentTypeParser: ContentTypeParser | undefined
66
- private readonly blockstoreSessions: QuickLRU<string, SessionBlockstore>
67
81
  private readonly withServerTiming: boolean
68
82
  private readonly plugins: VerifiedFetchPlugin[] = []
83
+ private readonly urlResolver: URLResolver
69
84
 
70
85
  constructor (helia: Helia, init: CreateVerifiedFetchOptions = {}) {
71
86
  this.helia = helia
@@ -73,35 +88,27 @@ export class VerifiedFetch {
73
88
  this.ipnsResolver = init.ipnsResolver ?? ipnsResolver(helia)
74
89
  this.dnsLink = init.dnsLink ?? dnsLink(helia)
75
90
  this.contentTypeParser = init.contentTypeParser ?? contentTypeParser
76
- this.blockstoreSessions = new QuickLRU({
77
- maxSize: init?.sessionCacheSize ?? SESSION_CACHE_MAX_SIZE,
78
- maxAge: init?.sessionTTLms ?? SESSION_CACHE_TTL_MS,
79
- onEviction: (key, store) => {
80
- store.close()
81
- }
82
- })
83
91
  this.withServerTiming = init?.withServerTiming ?? false
92
+ this.urlResolver = new URLResolver({
93
+ ipnsResolver: this.ipnsResolver,
94
+ dnsLink: this.dnsLink,
95
+ helia: this.helia
96
+ }, init)
84
97
 
85
98
  const pluginOptions: PluginOptions = {
86
99
  ...init,
87
100
  logger: helia.logger.forComponent('verified-fetch'),
88
- getBlockstore: (cid, resource, useSession, options) => this.getBlockstore(cid, resource, useSession, options),
89
101
  helia,
90
102
  contentTypeParser: this.contentTypeParser,
91
103
  ipnsResolver: this.ipnsResolver
92
104
  }
93
105
 
94
106
  const defaultPlugins = [
95
- new DagWalkPlugin(pluginOptions),
96
- new ByteRangeContextPlugin(pluginOptions),
97
- new IpnsRecordPlugin(pluginOptions),
107
+ new UnixFSPlugin(pluginOptions),
108
+ new IpldPlugin(pluginOptions),
98
109
  new CarPlugin(pluginOptions),
99
- new RawPlugin(pluginOptions),
100
110
  new TarPlugin(pluginOptions),
101
- new JsonPlugin(pluginOptions),
102
- new DagCborPlugin(pluginOptions),
103
- new DagPbPlugin(pluginOptions),
104
- new CborPlugin(pluginOptions)
111
+ new IpnsRecordPlugin(pluginOptions)
105
112
  ]
106
113
 
107
114
  const customPlugins = init.plugins?.map((pluginFactory) => pluginFactory(pluginOptions)) ?? []
@@ -113,310 +120,395 @@ export class VerifiedFetch {
113
120
 
114
121
  this.plugins = defaultPlugins.map(plugin => customPluginMap.get(plugin.id) ?? plugin)
115
122
 
116
- // Add any remaining custom plugins that don't replace a default plugin
117
- this.plugins.push(...customPlugins.filter(plugin => !defaultPluginMap.has(plugin.id)))
123
+ // add any custom plugins that don't replace default ones with a higher
124
+ // priority than anything built-in
125
+ this.plugins.unshift(...customPlugins.filter(plugin => !defaultPluginMap.has(plugin.id)))
118
126
  } else {
119
127
  this.plugins = defaultPlugins
120
128
  }
121
129
  }
122
130
 
123
- private getBlockstore (root: CID, resource: string | CID, useSession: boolean = true, options: AbortOptions = {}): Blockstore {
124
- const key = resourceToSessionCacheKey(resource)
125
- if (!useSession) {
126
- return this.helia.blockstore
127
- }
128
-
129
- let session = this.blockstoreSessions.get(key)
131
+ /**
132
+ * Load a resource from the IPFS network and ensure the retrieved data is the
133
+ * data that was expected to be loaded.
134
+ *
135
+ * Like [fetch](https://developer.mozilla.org/en-US/docs/Web/API/Window/fetch)
136
+ * but verified.
137
+ */
138
+ async fetch (resource: Resource, opts?: VerifiedFetchOptions): Promise<Response> {
139
+ this.log('fetch %s %s', opts?.method ?? 'GET', resource)
130
140
 
131
- if (session == null) {
132
- session = this.helia.blockstore.createSession(root, options)
133
- this.blockstoreSessions.set(key, session)
141
+ if (opts?.method === 'OPTIONS') {
142
+ return this.handleFinalResponse(new Response(null, {
143
+ status: 200
144
+ }))
134
145
  }
135
146
 
136
- return session
137
- }
147
+ const options = convertOptions(opts)
148
+ const headers = new Headers(options?.headers)
149
+ const serverTiming = new ServerTiming()
138
150
 
139
- /**
140
- * The last place a Response touches in verified-fetch before being returned to the user. This is where we add the
141
- * Server-Timing header to the response if it has been collected. It should be used for any final processing of the
142
- * response before it is returned to the user.
143
- */
144
- private handleFinalResponse (response: Response, context?: Partial<PluginContext>): Response {
145
- if ((this.withServerTiming || context?.withServerTiming === true) && context?.serverTiming != null) {
146
- response.headers.set('Server-Timing', context?.serverTiming.getHeader())
147
- }
151
+ options?.onProgress?.(new CustomProgressEvent<ResourceDetail>('verified-fetch:request:start', { resource }))
148
152
 
149
- // 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
150
- if (response.headers.get('Transfer-Encoding') !== 'chunked') {
151
- if (context?.byteRangeContext != null) {
152
- const contentLength = context.byteRangeContext.getLength()
153
- if (contentLength != null) {
154
- this.log.trace('Setting Content-Length from byteRangeContext: %d', contentLength)
155
- response.headers.set('Content-Length', contentLength.toString())
156
- }
157
- }
153
+ const range = getRangeHeader(resource.toString(), headers)
154
+
155
+ if (range instanceof Response) {
156
+ // invalid range request
157
+ return this.handleFinalResponse(range)
158
158
  }
159
159
 
160
- // set Content-Disposition header
161
- let contentDisposition: string | undefined
160
+ let url: URL
162
161
 
163
- // force download if requested
164
- if (context?.query?.download === true) {
165
- this.log.trace('download requested')
166
- contentDisposition = 'attachment'
162
+ try {
163
+ url = parseURLString(typeof resource === 'string' ? resource : `ipfs://${resource}`)
164
+ } catch (err: any) {
165
+ return this.handleFinalResponse(badRequestResponse(resource.toString(), err))
167
166
  }
168
167
 
169
- // override filename if requested
170
- if (context?.query?.filename != null) {
171
- this.log.trace('specific filename requested')
168
+ let parsedResult: ResolveURLResult
172
169
 
173
- if (contentDisposition == null) {
174
- contentDisposition = 'inline'
170
+ // if just an IPNS record has been requested, don't try to load the block
171
+ // the record points to or do any recursive IPNS resolving
172
+ if (isIPNSRecordRequest(headers)) {
173
+ if (url.protocol !== 'ipns:') {
174
+ return notAcceptableResponse(url, [])
175
175
  }
176
176
 
177
- contentDisposition = `${contentDisposition}; ${getContentDispositionFilename(context.query.filename)}`
178
- }
177
+ // @ts-expect-error ipnsRecordPlugin may not be of type IpnsRecordPlugin
178
+ const ipnsRecordPlugin: IpnsRecordPlugin | undefined = this.plugins.find(plugin => plugin.id === 'ipns-record-plugin')
179
179
 
180
- if (contentDisposition != null) {
181
- this.log.trace('content disposition %s', contentDisposition)
182
- response.headers.set('Content-Disposition', contentDisposition)
183
- }
180
+ if (ipnsRecordPlugin == null) {
181
+ return notAcceptableResponse(url, [])
182
+ }
184
183
 
185
- if (context?.cid != null && response.headers.get('etag') == null) {
186
- response.headers.set('etag', getETag({
187
- cid: context.pathDetails?.terminalElement.cid ?? context.cid,
188
- reqFormat: context.reqFormat,
189
- weak: false
184
+ return this.handleFinalResponse(await ipnsRecordPlugin.handle({
185
+ range,
186
+ url,
187
+ resource: resource.toString(),
188
+ options
190
189
  }))
190
+ } else {
191
+ try {
192
+ parsedResult = await this.urlResolver.resolve(url, serverTiming, {
193
+ ...options,
194
+ isRawBlockRequest: isRawBlockRequest(headers),
195
+ onlyIfCached: headers.get('cache-control') === 'only-if-cached'
196
+ })
197
+ } catch (err: any) {
198
+ options?.signal?.throwIfAborted()
199
+
200
+ this.log.error('error parsing resource %s - %e', resource, err)
201
+ return this.handleFinalResponse(errorToResponse(resource, err))
202
+ }
191
203
  }
192
204
 
193
- if (context?.protocol != null && context.ttl != null) {
194
- setCacheControlHeader({
195
- response,
196
- ttl: context.ttl,
197
- protocol: context.protocol
198
- })
199
- }
205
+ options?.onProgress?.(new CustomProgressEvent<CIDDetail>('verified-fetch:request:resolve', {
206
+ cid: parsedResult.terminalElement.cid,
207
+ path: parsedResult.url.pathname
208
+ }))
200
209
 
201
- if (context?.ipfsPath != null) {
202
- response.headers.set('X-Ipfs-Path', uriEncodeIPFSPath(context.ipfsPath))
203
- }
210
+ const accept = this.getAcceptHeader(parsedResult.url, headers.get('accept'), parsedResult.terminalElement.cid)
204
211
 
205
- // 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.
206
- response.headers.set('Access-Control-Allow-Origin', '*')
207
- response.headers.set('Access-Control-Allow-Methods', 'GET, HEAD, OPTIONS')
208
- response.headers.set('Access-Control-Allow-Headers', 'Range, X-Requested-With')
209
- response.headers.set('Access-Control-Expose-Headers', 'Content-Range, Content-Length, X-Ipfs-Path, X-Ipfs-Roots, X-Stream-Output')
212
+ if (accept instanceof Response) {
213
+ this.log('allowed media types for requested CID did not contain anything the client can understand')
210
214
 
211
- if (context?.reqFormat !== 'car') {
212
- // if we are not doing streaming responses, set the Accept-Ranges header to bytes to enable range requests
213
- response.headers.set('Accept-Ranges', 'bytes')
214
- } else {
215
- // set accept-ranges to none to disable range requests for streaming responses
216
- response.headers.set('Accept-Ranges', 'none')
215
+ // invalid accept header
216
+ return this.handleFinalResponse(accept)
217
217
  }
218
218
 
219
- if (response.headers.get('Content-Type')?.includes('application/vnd.ipld.car') === true || response.headers.get('Content-Type')?.includes('application/vnd.ipld.raw') === true) {
220
- // see https://specs.ipfs.tech/http-gateways/path-gateway/#x-content-type-options-response-header
221
- response.headers.set('X-Content-Type-Options', 'nosniff')
219
+ const context: PluginContext = {
220
+ ...parsedResult,
221
+ resource: resource.toString(),
222
+ accept,
223
+ range,
224
+ options,
225
+ onProgress: options?.onProgress,
226
+ serverTiming,
227
+ headers
222
228
  }
223
229
 
224
- if (context?.options?.method === 'HEAD') {
225
- // don't send the body for HEAD requests
226
- return new Response(null, {
227
- status: 200,
228
- headers: response.headers
229
- })
230
+ this.log.trace('finding handler for cid code "0x%s" and response content types %s', parsedResult.terminalElement.cid.code.toString(16), accept.map(header => header.contentType.mediaType).join(', '))
231
+
232
+ const response = await this.runPluginPipeline(context)
233
+
234
+ options?.onProgress?.(new CustomProgressEvent<CIDDetail>('verified-fetch:request:end', {
235
+ cid: parsedResult.terminalElement.cid,
236
+ path: parsedResult.url.pathname
237
+ }))
238
+
239
+ if (response == null) {
240
+ this.log.error('no plugin could handle request for %s', resource)
230
241
  }
231
242
 
232
- return response
243
+ return this.handleFinalResponse(response, Boolean(options?.withServerTiming) || Boolean(this.withServerTiming), context)
233
244
  }
234
245
 
235
246
  /**
236
- * Runs plugins in a loop. After each plugin that returns `null` (partial/no final),
237
- * we re-check `canHandle()` for all plugins in the next iteration if the context changed.
247
+ * Returns a prioritized list of acceptable content types for the response
248
+ * based on the CID and a passed `Accept` header
238
249
  */
239
- private async runPluginPipeline (context: PluginContext, maxPasses: number = 3): Promise<Response> {
240
- let finalResponse: Response | undefined
241
- let passCount = 0
242
- const pluginsUsed = new Set<string>()
250
+ private getAcceptHeader (url: URL, accept?: string | null, cid?: CID): AcceptHeader[] | Response {
251
+ if (accept == null || accept === '') {
252
+ // if the user has specified CAR options but no Accept header, default to
253
+ // the car content type with the passed options
254
+ try {
255
+ const dagScope = url.searchParams.get('dag-scope')
256
+ const entityBytes = url.searchParams.get('entity-bytes')
257
+ const dups = url.searchParams.get('car-dups')
258
+ const order = url.searchParams.get('car-order')
259
+ const version = url.searchParams.get('car-version')
260
+
261
+ if (dagScope != null ||
262
+ entityBytes != null ||
263
+ dups != null ||
264
+ entityBytes != null ||
265
+ order != null
266
+ ) {
267
+ const options: Record<string, string> = {}
268
+
269
+ if (dups != null) {
270
+ options.dups = dups
271
+ }
272
+
273
+ if (order != null) {
274
+ options.order = order
275
+ }
243
276
 
244
- let prevModificationId = context.modified
277
+ if (version != null) {
278
+ options.version = version
279
+ }
245
280
 
246
- while (passCount < maxPasses) {
247
- this.log(`starting pipeline pass #${passCount + 1}`)
248
- passCount++
281
+ return [{
282
+ contentType: CONTENT_TYPE_CAR,
283
+ options
284
+ }]
285
+ }
286
+ } catch {}
249
287
 
250
- this.log.trace('checking which plugins can handle %c%s with accept %o', context.cid, context.path.length > 0 ? `/${context.path.join('/')}` : '', context.accept)
288
+ // yolo content-type
289
+ accept = '*/*'
290
+ // return []
291
+ }
251
292
 
252
- // gather plugins that say they can handle the *current* context, but haven't been used yet
253
- const readyPlugins = this.plugins.filter(p => !pluginsUsed.has(p.id)).filter(p => p.canHandle(context))
293
+ // allow user to choose specific output type
294
+ const acceptable: AcceptHeader[] = []
254
295
 
255
- if (readyPlugins.length === 0) {
256
- this.log.trace('no plugins can handle the current context, checking by CID code')
257
- const plugins = this.plugins.filter(p => p.codes.includes(context.cid.code))
296
+ const requestedMimeTypes = accept
297
+ .split(',')
298
+ .map(s => {
299
+ const parts = s.trim().split(';')
258
300
 
259
- if (plugins.length > 0) {
260
- readyPlugins.push(...plugins)
261
- } else {
262
- this.log.trace('no plugins found that can handle request by CID code; exiting pipeline')
263
- break
301
+ const options: Record<string, string> = {
302
+ q: '1'
264
303
  }
265
- }
266
304
 
267
- this.log.trace('plugins ready to handle request: %s', readyPlugins.map(p => p.id).join(', '))
305
+ for (let i = 1; i < parts.length; i++) {
306
+ const [key, value] = parts[i].split('=').map(s => s.trim())
268
307
 
269
- // track if any plugin changed the context or returned a response
270
- let contextChanged = false
271
- let pluginHandled = false
308
+ options[key] = value
309
+ }
310
+
311
+ return {
312
+ mediaType: `${parts[0]}`.trim(),
313
+ options
314
+ }
315
+ })
316
+ .sort((a, b) => {
317
+ if (a.options.q === b.options.q) {
318
+ return 0
319
+ }
272
320
 
273
- for (const plugin of readyPlugins) {
274
- try {
275
- this.log('invoking plugin: %s', plugin.id)
276
- pluginsUsed.add(plugin.id)
321
+ if (a.options.q > b.options.q) {
322
+ return -1
323
+ }
277
324
 
278
- const maybeResponse = await plugin.handle(context)
325
+ return 1
326
+ })
279
327
 
280
- this.log('plugin response %s %o', plugin.id, maybeResponse)
328
+ const supportedContentTypes = getSupportedContentTypes(url.protocol, cid)
281
329
 
282
- if (maybeResponse != null) {
283
- // if a plugin returns a final Response, short-circuit
284
- finalResponse = maybeResponse
285
- pluginHandled = true
286
- break
287
- }
288
- } catch (err: any) {
289
- if (context.options?.signal?.aborted) {
290
- throw new AbortError(context.options?.signal?.reason)
291
- }
330
+ for (const headerFormat of requestedMimeTypes) {
331
+ const [headerFormatType, headerFormatSubType] = headerFormat.mediaType.split('/')
292
332
 
293
- this.log.error('error in plugin %s - %e', plugin.id, err)
333
+ for (const contentType of supportedContentTypes) {
334
+ const [contentTypeType, contentTypeSubType] = contentType.mediaType.split('/')
294
335
 
295
- return internalServerErrorResponse(context.resource, JSON.stringify({
296
- error: errorToObject(err)
297
- }), {
298
- headers: {
299
- 'content-type': 'application/json'
300
- }
336
+ if (headerFormat.mediaType.includes(contentType.mediaType)) {
337
+ acceptable.push({
338
+ contentType,
339
+ options: headerFormat.options
301
340
  })
302
- } finally {
303
- // on each plugin call, check for changes in the context
304
- const newModificationId = context.modified
305
- contextChanged = newModificationId !== prevModificationId
306
- if (contextChanged) {
307
- prevModificationId = newModificationId
308
- }
309
341
  }
310
342
 
311
- if (finalResponse != null) {
312
- this.log.trace('plugin %s produced final response', plugin.id)
313
- break
343
+ if (headerFormat.mediaType === '*/*') {
344
+ acceptable.push({
345
+ contentType,
346
+ options: headerFormat.options
347
+ })
314
348
  }
315
- }
316
349
 
317
- if (pluginHandled && finalResponse != null) {
318
- break
319
- }
350
+ if (headerFormat.mediaType.startsWith('*/') && contentTypeSubType === headerFormatSubType) {
351
+ acceptable.push({
352
+ contentType,
353
+ options: headerFormat.options
354
+ })
355
+ }
320
356
 
321
- if (!contextChanged) {
322
- this.log.trace('no context changes and no final response; exiting pipeline.')
323
- break
357
+ if (headerFormat.mediaType.endsWith('/*') && contentTypeType === headerFormatType) {
358
+ acceptable.push({
359
+ contentType,
360
+ options: headerFormat.options
361
+ })
362
+ }
324
363
  }
325
364
  }
326
365
 
327
- return finalResponse ?? notImplementedResponse(context.resource, JSON.stringify({
328
- error: errorToObject(new Error('No verified fetch plugin could handle the request'))
329
- }), {
330
- headers: {
331
- 'content-type': 'application/json'
332
- }
333
- })
366
+ if (acceptable.length === 0) {
367
+ this.log('requested %o', requestedMimeTypes.map(({ mediaType }) => mediaType))
368
+ this.log('supported %o', supportedContentTypes.map(({ mediaType }) => mediaType))
369
+
370
+ return notAcceptableResponse(url, supportedContentTypes)
371
+ }
372
+
373
+ return acceptable
334
374
  }
335
375
 
336
376
  /**
337
- * We're starting to get to the point where we need a queue or pipeline of
338
- * operations to perform and a single place to handle errors.
339
- *
340
- * TODO: move operations called by fetch to a queue of operations where we can
341
- * always exit early (and cleanly) if a given signal is aborted
377
+ * The last place a Response touches in verified-fetch before being returned
378
+ * to the user. This is where we add the Server-Timing header to the response
379
+ * if it has been collected. It should be used for any final processing of the
380
+ * response before it is returned to the user.
342
381
  */
343
- async fetch (resource: Resource, opts?: VerifiedFetchOptions): Promise<Response> {
344
- this.log('fetch %s', resource)
382
+ private handleFinalResponse (response: Response, withServerTiming?: boolean, context?: PluginContext): Response {
383
+ const contentType = getContentType(response.headers.get('content-type')) ?? CONTENT_TYPE_OCTET_STREAM
345
384
 
346
- if (opts?.method === 'OPTIONS') {
347
- return this.handleFinalResponse(new Response(null, { status: 200 }))
385
+ if (withServerTiming === true && context?.serverTiming != null) {
386
+ const timingHeader = context?.serverTiming.getHeader()
387
+
388
+ if (timingHeader !== '') {
389
+ response.headers.set('server-timing', timingHeader)
390
+ }
348
391
  }
349
392
 
350
- const options = convertOptions(opts)
351
- const serverTiming = new ServerTiming()
393
+ if (context?.terminalElement.cid != null && response.headers.get('etag') == null) {
394
+ response.headers.set('etag', getETag({
395
+ cid: context.terminalElement.cid,
396
+ contentType
397
+ }))
398
+ }
352
399
 
353
- const urlResolver = new URLResolver({
354
- ipnsResolver: this.ipnsResolver,
355
- dnsLink: this.dnsLink,
356
- timing: serverTiming
357
- })
400
+ if (context?.url?.protocol != null && context.ttl != null) {
401
+ setCacheControlHeader({
402
+ response,
403
+ ttl: context.ttl,
404
+ protocol: context.url.protocol
405
+ })
406
+ }
358
407
 
359
- options?.onProgress?.(new CustomProgressEvent<ResourceDetail>('verified-fetch:request:start', { resource }))
408
+ if (context?.terminalElement.cid != null) {
409
+ // headers can ony contain extended ASCII but IPFS paths can be unicode
410
+ const decodedPath = decodeURI(context?.url.pathname)
411
+ const path = uint8ArrayToString(uint8ArrayFromString(decodedPath), 'ascii')
360
412
 
361
- let parsedResult: ResolveURLResult
413
+ response.headers.set('x-ipfs-path', `/${context.url.protocol === 'ipfs:' ? 'ipfs' : 'ipns'}/${context?.url.hostname}${path}`)
414
+ }
362
415
 
363
- try {
364
- parsedResult = await urlResolver.resolve(resource, options)
365
- } catch (err: any) {
366
- if (options?.signal?.aborted) {
367
- throw new AbortError(options?.signal?.reason)
368
- }
369
- this.log.error('error parsing resource %s', resource, err)
416
+ // set CORS headers. If hosting your own gateway with verified-fetch behind
417
+ // the scenes, you can alter these before you send the response to the
418
+ // client.
419
+ response.headers.set('access-control-allow-origin', '*')
420
+ response.headers.set('access-control-allow-methods', 'GET, HEAD, OPTIONS')
421
+ response.headers.set('access-control-allow-headers', 'Range, X-Requested-With')
422
+ response.headers.set('access-control-expose-headers', 'Content-Range, Content-Length, X-Ipfs-Path, X-Ipfs-Roots, X-Stream-Output')
370
423
 
371
- return this.handleFinalResponse(badRequestResponse(resource.toString(), err))
424
+ if (context?.options?.method === 'HEAD') {
425
+ // don't send the body for HEAD requests
426
+ return new Response(null, {
427
+ status: 200,
428
+ headers: response.headers
429
+ })
372
430
  }
373
431
 
374
- options?.onProgress?.(new CustomProgressEvent<CIDDetail>('verified-fetch:request:resolve', { cid: parsedResult.cid, path: parsedResult.path }))
432
+ // make sure users are not expected to "download" error responses
433
+ if (response.status > 399) {
434
+ response.headers.delete('content-disposition')
435
+ }
375
436
 
376
- const acceptHeader = getResolvedAcceptHeader({ query: parsedResult.query, headers: options?.headers, logger: this.helia.logger })
437
+ return response
438
+ }
377
439
 
378
- const accept: AcceptHeader | undefined = selectOutputType(parsedResult.cid, acceptHeader)
379
- this.log('accept %o', accept)
440
+ private async runPluginPipeline (context: PluginContext): Promise<Response> {
441
+ let finalResponse: Response | undefined
442
+ const pluginsUsed = new Set<string>()
380
443
 
381
- if (acceptHeader != null && accept == null) {
382
- this.log.error('could not fulfil request based on accept header')
383
- return this.handleFinalResponse(notAcceptableResponse(resource.toString()))
384
- }
444
+ this.log.trace('checking which plugins can handle %c%s with accept %s', context.terminalElement.cid, context.url.pathname, context.accept.map(contentType => contentType.contentType.mediaType).join(', '))
385
445
 
386
- const responseContentType: string = accept?.mimeType.split(';')[0] ?? 'application/octet-stream'
446
+ const plugins = this.plugins.filter(p => !pluginsUsed.has(p.id)).filter(p => p.canHandle(context))
387
447
 
388
- const redirectResponse = await getRedirectResponse({ resource, options, logger: this.helia.logger, cid: parsedResult.cid })
389
- if (redirectResponse != null) {
390
- return this.handleFinalResponse(redirectResponse)
448
+ if (plugins.length === 0) {
449
+ this.log.trace('no plugins found that can handle request; exiting pipeline')
450
+ return notImplementedResponse(context.resource)
391
451
  }
392
452
 
393
- const context: PluginContext = {
394
- ...parsedResult,
395
- resource: resource.toString(),
396
- accept,
397
- options,
398
- onProgress: options?.onProgress,
399
- modified: 0,
400
- plugins: this.plugins.map(p => p.id),
401
- query: parsedResult.query ?? {},
402
- withServerTiming: Boolean(options?.withServerTiming) || Boolean(this.withServerTiming),
403
- serverTiming
404
- }
453
+ this.log.trace('plugins ready to handle request: %s', plugins.map(p => p.id).join(', '))
405
454
 
406
- this.log.trace('finding handler for cid code "%s" and response content type "%s"', parsedResult.cid.code, responseContentType)
455
+ // track if any plugin changed the context or returned a response
456
+ const contextChanged = false
457
+ let pluginHandled = false
407
458
 
408
- const response = await this.runPluginPipeline(context)
459
+ for (const plugin of plugins) {
460
+ try {
461
+ this.log('invoking plugin: %s', plugin.id)
462
+ pluginsUsed.add(plugin.id)
409
463
 
410
- options?.onProgress?.(new CustomProgressEvent<CIDDetail>('verified-fetch:request:end', {
411
- cid: parsedResult.cid,
412
- path: parsedResult.path
413
- }))
464
+ const maybeResponse = await plugin.handle(context)
414
465
 
415
- if (response == null) {
416
- this.log.error('no plugin could handle request for %s', resource)
466
+ this.log('plugin response %s %o', plugin.id, maybeResponse)
467
+
468
+ if (maybeResponse != null) {
469
+ // if a plugin returns a final Response, short-circuit
470
+ finalResponse = maybeResponse
471
+ pluginHandled = true
472
+ break
473
+ }
474
+ } catch (err: any) {
475
+ if (context.options?.signal?.aborted) {
476
+ throw new AbortError(context.options?.signal?.reason)
477
+ }
478
+
479
+ this.log.error('error in plugin %s - %e', plugin.id, err)
480
+
481
+ return internalServerErrorResponse(context.resource, JSON.stringify({
482
+ error: errorToObject(err)
483
+ }), {
484
+ headers: {
485
+ 'content-type': 'application/json'
486
+ }
487
+ })
488
+ }
489
+
490
+ if (finalResponse != null) {
491
+ this.log.trace('plugin %s produced final response', plugin.id)
492
+ break
493
+ }
494
+
495
+ if (pluginHandled && finalResponse != null) {
496
+ break
497
+ }
498
+
499
+ if (!contextChanged) {
500
+ this.log.trace('no context changes and no final response; exiting pipeline.')
501
+ break
502
+ }
417
503
  }
418
504
 
419
- return this.handleFinalResponse(response, context)
505
+ return finalResponse ?? notImplementedResponse(context.resource, JSON.stringify({
506
+ error: errorToObject(new Error('No verified fetch plugin could handle the request'))
507
+ }), {
508
+ headers: {
509
+ 'content-type': 'application/json'
510
+ }
511
+ })
420
512
  }
421
513
 
422
514
  /**