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