@helia/verified-fetch 4.1.1 → 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 +7 -40
  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 +302 -238
  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 +199 -68
  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 +353 -270
  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 -69
@@ -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,100 +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
+ }));
107
121
  }
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);
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));
112
137
  }
113
- return session;
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
+ }
170
+ }
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);
180
+ }
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
- const timingHeader = context?.serverTiming.getHeader();
123
- if (timingHeader !== '') {
124
- response.headers.set('Server-Timing', timingHeader);
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
+ }
125
236
  }
237
+ catch { }
238
+ // yolo content-type
239
+ accept = '*/*';
240
+ // return []
126
241
  }
127
- // 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
128
- if (response.headers.get('Transfer-Encoding') !== 'chunked') {
129
- if (context?.byteRangeContext != null) {
130
- const contentLength = context.byteRangeContext.getLength();
131
- if (contentLength != null) {
132
- this.log.trace('Setting Content-Length from byteRangeContext: %d', contentLength);
133
- 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
+ });
134
297
  }
135
298
  }
136
299
  }
137
- // set Content-Disposition header
138
- let contentDisposition;
139
- // force download if requested
140
- if (context?.query?.download === true) {
141
- this.log.trace('download requested');
142
- 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);
143
304
  }
144
- // override filename if requested
145
- if (context?.query?.filename != null) {
146
- this.log.trace('specific filename requested');
147
- if (contentDisposition == null) {
148
- 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);
149
319
  }
150
- contentDisposition = `${contentDisposition}; ${getContentDispositionFilename(context.query.filename)}`;
151
- }
152
- if (contentDisposition != null) {
153
- this.log.trace('content disposition %s', contentDisposition);
154
- response.headers.set('Content-Disposition', contentDisposition);
155
320
  }
156
- if (context?.cid != null && response.headers.get('etag') == null) {
321
+ if (context?.terminalElement.cid != null && response.headers.get('etag') == null) {
157
322
  response.headers.set('etag', getETag({
158
- cid: context.pathDetails?.terminalElement.cid ?? context.cid,
159
- reqFormat: context.reqFormat,
160
- weak: false
323
+ cid: context.terminalElement.cid,
324
+ contentType
161
325
  }));
162
326
  }
163
- if (context?.protocol != null && context.ttl != null) {
327
+ if (context?.url?.protocol != null && context.ttl != null) {
164
328
  setCacheControlHeader({
165
329
  response,
166
330
  ttl: context.ttl,
167
- protocol: context.protocol
331
+ protocol: context.url.protocol
168
332
  });
169
333
  }
170
- if (context?.ipfsPath != null) {
171
- response.headers.set('X-Ipfs-Path', uriEncodeIPFSPath(context.ipfsPath));
172
- }
173
- // 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.
174
- response.headers.set('Access-Control-Allow-Origin', '*');
175
- response.headers.set('Access-Control-Allow-Methods', 'GET, HEAD, OPTIONS');
176
- response.headers.set('Access-Control-Allow-Headers', 'Range, X-Requested-With');
177
- response.headers.set('Access-Control-Expose-Headers', 'Content-Range, Content-Length, X-Ipfs-Path, X-Ipfs-Roots, X-Stream-Output');
178
- if (context?.reqFormat !== 'car') {
179
- // if we are not doing streaming responses, set the Accept-Ranges header to bytes to enable range requests
180
- response.headers.set('Accept-Ranges', 'bytes');
181
- }
182
- else {
183
- // set accept-ranges to none to disable range requests for streaming responses
184
- response.headers.set('Accept-Ranges', 'none');
185
- }
186
- if (response.headers.get('Content-Type')?.includes('application/vnd.ipld.car') === true || response.headers.get('Content-Type')?.includes('application/vnd.ipld.raw') === true) {
187
- // see https://specs.ipfs.tech/http-gateways/path-gateway/#x-content-type-options-response-header
188
- 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}`);
189
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');
190
347
  if (context?.options?.method === 'HEAD') {
191
348
  // don't send the body for HEAD requests
192
349
  return new Response(null, {
@@ -200,74 +357,48 @@ export class VerifiedFetch {
200
357
  }
201
358
  return response;
202
359
  }
203
- /**
204
- * Runs plugins in a loop. After each plugin that returns `null` (partial/no final),
205
- * we re-check `canHandle()` for all plugins in the next iteration if the context changed.
206
- */
207
- async runPluginPipeline(context, maxPasses = 3) {
360
+ async runPluginPipeline(context) {
208
361
  let finalResponse;
209
- let passCount = 0;
210
362
  const pluginsUsed = new Set();
211
- let prevModificationId = context.modified;
212
- while (passCount < maxPasses) {
213
- this.log(`starting pipeline pass #${passCount + 1}`);
214
- passCount++;
215
- this.log.trace('checking which plugins can handle %c%s with accept %o', context.cid, context.path.length > 0 ? `/${context.path.join('/')}` : '', context.accept);
216
- // gather plugins that say they can handle the *current* context, but haven't been used yet
217
- const readyPlugins = this.plugins.filter(p => !pluginsUsed.has(p.id)).filter(p => p.canHandle(context));
218
- if (readyPlugins.length === 0) {
219
- this.log.trace('no plugins can handle the current context, checking by CID code');
220
- const plugins = this.plugins.filter(p => p.codes.includes(context.cid.code));
221
- if (plugins.length > 0) {
222
- readyPlugins.push(...plugins);
223
- }
224
- else {
225
- 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;
226
383
  break;
227
384
  }
228
385
  }
229
- this.log.trace('plugins ready to handle request: %s', readyPlugins.map(p => p.id).join(', '));
230
- // track if any plugin changed the context or returned a response
231
- let contextChanged = false;
232
- let pluginHandled = false;
233
- for (const plugin of readyPlugins) {
234
- try {
235
- this.log('invoking plugin: %s', plugin.id);
236
- pluginsUsed.add(plugin.id);
237
- const maybeResponse = await plugin.handle(context);
238
- this.log('plugin response %s %o', plugin.id, maybeResponse);
239
- if (maybeResponse != null) {
240
- // if a plugin returns a final Response, short-circuit
241
- finalResponse = maybeResponse;
242
- pluginHandled = true;
243
- break;
244
- }
245
- }
246
- catch (err) {
247
- if (context.options?.signal?.aborted) {
248
- throw new AbortError(context.options?.signal?.reason);
249
- }
250
- this.log.error('error in plugin %s - %e', plugin.id, err);
251
- return internalServerErrorResponse(context.resource, JSON.stringify({
252
- error: errorToObject(err)
253
- }), {
254
- headers: {
255
- 'content-type': 'application/json'
256
- }
257
- });
386
+ catch (err) {
387
+ if (context.options?.signal?.aborted) {
388
+ throw new AbortError(context.options?.signal?.reason);
258
389
  }
259
- finally {
260
- // on each plugin call, check for changes in the context
261
- const newModificationId = context.modified;
262
- contextChanged = newModificationId !== prevModificationId;
263
- if (contextChanged) {
264
- 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'
265
396
  }
266
- }
267
- if (finalResponse != null) {
268
- this.log.trace('plugin %s produced final response', plugin.id);
269
- break;
270
- }
397
+ });
398
+ }
399
+ if (finalResponse != null) {
400
+ this.log.trace('plugin %s produced final response', plugin.id);
401
+ break;
271
402
  }
272
403
  if (pluginHandled && finalResponse != null) {
273
404
  break;
@@ -285,73 +416,6 @@ export class VerifiedFetch {
285
416
  }
286
417
  });
287
418
  }
288
- /**
289
- * We're starting to get to the point where we need a queue or pipeline of
290
- * operations to perform and a single place to handle errors.
291
- *
292
- * TODO: move operations called by fetch to a queue of operations where we can
293
- * always exit early (and cleanly) if a given signal is aborted
294
- */
295
- async fetch(resource, opts) {
296
- this.log('fetch %s', resource);
297
- if (opts?.method === 'OPTIONS') {
298
- return this.handleFinalResponse(new Response(null, { status: 200 }));
299
- }
300
- const options = convertOptions(opts);
301
- const serverTiming = new ServerTiming();
302
- const urlResolver = new URLResolver({
303
- ipnsResolver: this.ipnsResolver,
304
- dnsLink: this.dnsLink,
305
- timing: serverTiming
306
- });
307
- options?.onProgress?.(new CustomProgressEvent('verified-fetch:request:start', { resource }));
308
- let parsedResult;
309
- try {
310
- parsedResult = await urlResolver.resolve(resource, options);
311
- }
312
- catch (err) {
313
- if (options?.signal?.aborted) {
314
- throw new AbortError(options?.signal?.reason);
315
- }
316
- this.log.error('error parsing resource %s', resource, err);
317
- return this.handleFinalResponse(badRequestResponse(resource.toString(), err));
318
- }
319
- options?.onProgress?.(new CustomProgressEvent('verified-fetch:request:resolve', { cid: parsedResult.cid, path: parsedResult.path }));
320
- const acceptHeader = getResolvedAcceptHeader({ query: parsedResult.query, headers: options?.headers, logger: this.helia.logger });
321
- const accept = selectOutputType(parsedResult.cid, acceptHeader);
322
- this.log('accept %o', accept);
323
- if (acceptHeader != null && accept == null) {
324
- this.log.error('could not fulfil request based on accept header');
325
- return this.handleFinalResponse(notAcceptableResponse(resource.toString()));
326
- }
327
- const responseContentType = accept?.mimeType.split(';')[0] ?? 'application/octet-stream';
328
- const redirectResponse = await getRedirectResponse({ resource, options, logger: this.helia.logger, cid: parsedResult.cid });
329
- if (redirectResponse != null) {
330
- return this.handleFinalResponse(redirectResponse);
331
- }
332
- const context = {
333
- ...parsedResult,
334
- resource: resource.toString(),
335
- accept,
336
- options,
337
- onProgress: options?.onProgress,
338
- modified: 0,
339
- plugins: this.plugins.map(p => p.id),
340
- query: parsedResult.query ?? {},
341
- withServerTiming: Boolean(options?.withServerTiming) || Boolean(this.withServerTiming),
342
- serverTiming
343
- };
344
- this.log.trace('finding handler for cid code "%s" and response content type "%s"', parsedResult.cid.code, responseContentType);
345
- const response = await this.runPluginPipeline(context);
346
- options?.onProgress?.(new CustomProgressEvent('verified-fetch:request:end', {
347
- cid: parsedResult.cid,
348
- path: parsedResult.path
349
- }));
350
- if (response == null) {
351
- this.log.error('no plugin could handle request for %s', resource);
352
- }
353
- return this.handleFinalResponse(response, context);
354
- }
355
419
  /**
356
420
  * Start the Helia instance
357
421
  */