@helia/verified-fetch 3.2.3 → 4.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 (126) hide show
  1. package/README.md +5 -5
  2. package/dist/index.min.js +81 -71
  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 -0
  6. package/dist/src/constants.js +2 -0
  7. package/dist/src/constants.js.map +1 -0
  8. package/dist/src/index.d.ts +57 -13
  9. package/dist/src/index.d.ts.map +1 -1
  10. package/dist/src/index.js +6 -6
  11. package/dist/src/index.js.map +1 -1
  12. package/dist/src/plugins/plugin-handle-car.d.ts.map +1 -1
  13. package/dist/src/plugins/plugin-handle-car.js +37 -27
  14. package/dist/src/plugins/plugin-handle-car.js.map +1 -1
  15. package/dist/src/plugins/plugin-handle-dag-cbor-html-preview.d.ts +1 -1
  16. package/dist/src/plugins/plugin-handle-dag-cbor-html-preview.d.ts.map +1 -1
  17. package/dist/src/plugins/plugin-handle-dag-cbor-html-preview.js +1 -1
  18. package/dist/src/plugins/plugin-handle-dag-cbor-html-preview.js.map +1 -1
  19. package/dist/src/plugins/plugin-handle-dag-cbor.js +5 -5
  20. package/dist/src/plugins/plugin-handle-dag-cbor.js.map +1 -1
  21. package/dist/src/plugins/plugin-handle-dag-pb.js +12 -12
  22. package/dist/src/plugins/plugin-handle-dag-pb.js.map +1 -1
  23. package/dist/src/plugins/plugin-handle-dag-walk.d.ts.map +1 -1
  24. package/dist/src/plugins/plugin-handle-dag-walk.js +5 -4
  25. package/dist/src/plugins/plugin-handle-dag-walk.js.map +1 -1
  26. package/dist/src/plugins/plugin-handle-ipns-record.d.ts.map +1 -1
  27. package/dist/src/plugins/plugin-handle-ipns-record.js +13 -19
  28. package/dist/src/plugins/plugin-handle-ipns-record.js.map +1 -1
  29. package/dist/src/plugins/plugin-handle-json.d.ts.map +1 -1
  30. package/dist/src/plugins/plugin-handle-json.js +5 -4
  31. package/dist/src/plugins/plugin-handle-json.js.map +1 -1
  32. package/dist/src/plugins/plugin-handle-raw.d.ts.map +1 -1
  33. package/dist/src/plugins/plugin-handle-raw.js +18 -5
  34. package/dist/src/plugins/plugin-handle-raw.js.map +1 -1
  35. package/dist/src/plugins/plugin-handle-tar.js +1 -1
  36. package/dist/src/plugins/plugin-handle-tar.js.map +1 -1
  37. package/dist/src/plugins/types.d.ts +10 -8
  38. package/dist/src/plugins/types.d.ts.map +1 -1
  39. package/dist/src/url-resolver.d.ts +21 -0
  40. package/dist/src/url-resolver.d.ts.map +1 -0
  41. package/dist/src/url-resolver.js +118 -0
  42. package/dist/src/url-resolver.js.map +1 -0
  43. package/dist/src/utils/byte-range-context.d.ts +1 -1
  44. package/dist/src/utils/get-content-type.d.ts +3 -3
  45. package/dist/src/utils/get-content-type.d.ts.map +1 -1
  46. package/dist/src/utils/get-content-type.js +1 -1
  47. package/dist/src/utils/get-content-type.js.map +1 -1
  48. package/dist/src/utils/get-e-tag.d.ts +1 -1
  49. package/dist/src/utils/get-offset-and-length.d.ts +6 -0
  50. package/dist/src/utils/get-offset-and-length.d.ts.map +1 -0
  51. package/dist/src/utils/get-offset-and-length.js +46 -0
  52. package/dist/src/utils/get-offset-and-length.js.map +1 -0
  53. package/dist/src/utils/get-resolved-accept-header.d.ts +2 -2
  54. package/dist/src/utils/get-resolved-accept-header.d.ts.map +1 -1
  55. package/dist/src/utils/handle-redirects.d.ts.map +1 -1
  56. package/dist/src/utils/handle-redirects.js +3 -3
  57. package/dist/src/utils/handle-redirects.js.map +1 -1
  58. package/dist/src/utils/ipfs-path-to-string.d.ts +6 -0
  59. package/dist/src/utils/ipfs-path-to-string.d.ts.map +1 -0
  60. package/dist/src/utils/ipfs-path-to-string.js +10 -0
  61. package/dist/src/utils/ipfs-path-to-string.js.map +1 -0
  62. package/dist/src/utils/is-accept-explicit.d.ts +6 -4
  63. package/dist/src/utils/is-accept-explicit.d.ts.map +1 -1
  64. package/dist/src/utils/is-accept-explicit.js +7 -4
  65. package/dist/src/utils/is-accept-explicit.js.map +1 -1
  66. package/dist/src/utils/parse-url-string.d.ts +1 -55
  67. package/dist/src/utils/parse-url-string.d.ts.map +1 -1
  68. package/dist/src/utils/parse-url-string.js +16 -217
  69. package/dist/src/utils/parse-url-string.js.map +1 -1
  70. package/dist/src/utils/response-headers.d.ts +1 -1
  71. package/dist/src/utils/response-headers.d.ts.map +1 -1
  72. package/dist/src/utils/responses.d.ts +1 -1
  73. package/dist/src/utils/select-output-type.d.ts +6 -2
  74. package/dist/src/utils/select-output-type.d.ts.map +1 -1
  75. package/dist/src/utils/select-output-type.js +28 -37
  76. package/dist/src/utils/select-output-type.js.map +1 -1
  77. package/dist/src/utils/server-timing.d.ts +5 -11
  78. package/dist/src/utils/server-timing.d.ts.map +1 -1
  79. package/dist/src/utils/server-timing.js +17 -15
  80. package/dist/src/utils/server-timing.js.map +1 -1
  81. package/dist/src/utils/walk-path.js +1 -1
  82. package/dist/src/utils/walk-path.js.map +1 -1
  83. package/dist/src/verified-fetch.d.ts +3 -10
  84. package/dist/src/verified-fetch.d.ts.map +1 -1
  85. package/dist/src/verified-fetch.js +68 -57
  86. package/dist/src/verified-fetch.js.map +1 -1
  87. package/dist/typedoc-urls.json +13 -2
  88. package/package.json +35 -36
  89. package/src/constants.ts +1 -0
  90. package/src/index.ts +73 -22
  91. package/src/plugins/plugin-handle-car.ts +54 -30
  92. package/src/plugins/plugin-handle-dag-cbor-html-preview.ts +2 -2
  93. package/src/plugins/plugin-handle-dag-cbor.ts +5 -5
  94. package/src/plugins/plugin-handle-dag-pb.ts +12 -12
  95. package/src/plugins/plugin-handle-dag-walk.ts +5 -4
  96. package/src/plugins/plugin-handle-ipns-record.ts +16 -19
  97. package/src/plugins/plugin-handle-json.ts +5 -4
  98. package/src/plugins/plugin-handle-raw.ts +21 -6
  99. package/src/plugins/plugin-handle-tar.ts +1 -1
  100. package/src/plugins/types.ts +12 -8
  101. package/src/url-resolver.ts +159 -0
  102. package/src/utils/byte-range-context.ts +1 -1
  103. package/src/utils/get-content-type.ts +5 -4
  104. package/src/utils/get-e-tag.ts +1 -1
  105. package/src/utils/get-offset-and-length.ts +54 -0
  106. package/src/utils/get-resolved-accept-header.ts +2 -2
  107. package/src/utils/handle-redirects.ts +10 -3
  108. package/src/utils/ipfs-path-to-string.ts +9 -0
  109. package/src/utils/is-accept-explicit.ts +14 -7
  110. package/src/utils/parse-url-string.ts +20 -286
  111. package/src/utils/response-headers.ts +1 -1
  112. package/src/utils/responses.ts +1 -1
  113. package/src/utils/select-output-type.ts +38 -44
  114. package/src/utils/server-timing.ts +17 -30
  115. package/src/utils/walk-path.ts +1 -1
  116. package/src/verified-fetch.ts +78 -69
  117. package/dist/src/types.d.ts +0 -16
  118. package/dist/src/types.d.ts.map +0 -1
  119. package/dist/src/types.js +0 -2
  120. package/dist/src/types.js.map +0 -1
  121. package/dist/src/utils/parse-resource.d.ts +0 -18
  122. package/dist/src/utils/parse-resource.d.ts.map +0 -1
  123. package/dist/src/utils/parse-resource.js +0 -27
  124. package/dist/src/utils/parse-resource.js.map +0 -1
  125. package/src/types.ts +0 -17
  126. package/src/utils/parse-resource.ts +0 -42
@@ -11,23 +11,34 @@
11
11
  "CIDDetailError": "https://ipfs.github.io/helia-verified-fetch/interfaces/index.CIDDetailError.html",
12
12
  ".:CIDDetailError": "https://ipfs.github.io/helia-verified-fetch/interfaces/index.CIDDetailError.html",
13
13
  "ContentTypeParser": "https://ipfs.github.io/helia-verified-fetch/interfaces/index.ContentTypeParser.html",
14
+ ".:ContentTypeParser": "https://ipfs.github.io/helia-verified-fetch/interfaces/index.ContentTypeParser.html",
14
15
  "CreateVerifiedFetchInit": "https://ipfs.github.io/helia-verified-fetch/interfaces/index.CreateVerifiedFetchInit.html",
15
16
  ".:CreateVerifiedFetchInit": "https://ipfs.github.io/helia-verified-fetch/interfaces/index.CreateVerifiedFetchInit.html",
16
17
  "CreateVerifiedFetchOptions": "https://ipfs.github.io/helia-verified-fetch/interfaces/index.CreateVerifiedFetchOptions.html",
17
18
  ".:CreateVerifiedFetchOptions": "https://ipfs.github.io/helia-verified-fetch/interfaces/index.CreateVerifiedFetchOptions.html",
18
19
  "PluginContext": "https://ipfs.github.io/helia-verified-fetch/interfaces/index.PluginContext.html",
19
20
  "PluginOptions": "https://ipfs.github.io/helia-verified-fetch/interfaces/index.PluginOptions.html",
21
+ "ResolveURLOptions": "https://ipfs.github.io/helia-verified-fetch/interfaces/index.ResolveURLOptions.html",
22
+ ".:ResolveURLOptions": "https://ipfs.github.io/helia-verified-fetch/interfaces/index.ResolveURLOptions.html",
23
+ "ResolveURLResult": "https://ipfs.github.io/helia-verified-fetch/interfaces/index.ResolveURLResult.html",
24
+ ".:ResolveURLResult": "https://ipfs.github.io/helia-verified-fetch/interfaces/index.ResolveURLResult.html",
20
25
  "ResourceDetail": "https://ipfs.github.io/helia-verified-fetch/interfaces/index.ResourceDetail.html",
21
26
  ".:ResourceDetail": "https://ipfs.github.io/helia-verified-fetch/interfaces/index.ResourceDetail.html",
27
+ "UrlQuery": "https://ipfs.github.io/helia-verified-fetch/interfaces/index.UrlQuery.html",
28
+ ".:UrlQuery": "https://ipfs.github.io/helia-verified-fetch/interfaces/index.UrlQuery.html",
29
+ "URLResolver": "https://ipfs.github.io/helia-verified-fetch/interfaces/index.URLResolver.html",
30
+ ".:URLResolver": "https://ipfs.github.io/helia-verified-fetch/interfaces/index.URLResolver.html",
22
31
  "VerifiedFetch": "https://ipfs.github.io/helia-verified-fetch/interfaces/index.VerifiedFetch.html",
23
32
  ".:VerifiedFetch": "https://ipfs.github.io/helia-verified-fetch/interfaces/index.VerifiedFetch.html",
24
33
  "VerifiedFetchInit": "https://ipfs.github.io/helia-verified-fetch/interfaces/index.VerifiedFetchInit.html",
25
34
  ".:VerifiedFetchInit": "https://ipfs.github.io/helia-verified-fetch/interfaces/index.VerifiedFetchInit.html",
26
35
  "VerifiedFetchPluginFactory": "https://ipfs.github.io/helia-verified-fetch/interfaces/index.VerifiedFetchPluginFactory.html",
27
- "BubbledProgressEvents": "https://ipfs.github.io/helia-verified-fetch/types/index.BubbledProgressEvents.html",
28
- ".:BubbledProgressEvents": "https://ipfs.github.io/helia-verified-fetch/types/index.BubbledProgressEvents.html",
36
+ "RequestFormatShorthand": "https://ipfs.github.io/helia-verified-fetch/types/index.RequestFormatShorthand.html",
37
+ ".:RequestFormatShorthand": "https://ipfs.github.io/helia-verified-fetch/types/index.RequestFormatShorthand.html",
29
38
  "Resource": "https://ipfs.github.io/helia-verified-fetch/types/index.Resource.html",
30
39
  ".:Resource": "https://ipfs.github.io/helia-verified-fetch/types/index.Resource.html",
40
+ "SupportedBodyTypes": "https://ipfs.github.io/helia-verified-fetch/types/index.SupportedBodyTypes.html",
41
+ ".:SupportedBodyTypes": "https://ipfs.github.io/helia-verified-fetch/types/index.SupportedBodyTypes.html",
31
42
  "VerifiedFetchProgressEvents": "https://ipfs.github.io/helia-verified-fetch/types/index.VerifiedFetchProgressEvents.html",
32
43
  ".:VerifiedFetchProgressEvents": "https://ipfs.github.io/helia-verified-fetch/types/index.VerifiedFetchProgressEvents.html",
33
44
  "dagCborHtmlPreviewPluginFactory": "https://ipfs.github.io/helia-verified-fetch/variables/index.dagCborHtmlPreviewPluginFactory.html",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@helia/verified-fetch",
3
- "version": "3.2.3",
3
+ "version": "4.0.0",
4
4
  "description": "A fetch-like API for obtaining verified & trustless IPFS content on the web",
5
5
  "license": "Apache-2.0 OR MIT",
6
6
  "homepage": "https://github.com/ipfs/helia-verified-fetch/tree/main/packages/verified-fetch#readme",
@@ -164,61 +164,60 @@
164
164
  "release": "aegir release"
165
165
  },
166
166
  "dependencies": {
167
- "@helia/block-brokers": "^4.2.1",
168
- "@helia/car": "^4.1.1",
169
- "@helia/delegated-routing-v1-http-api-client": "^4.2.5",
170
- "@helia/interface": "^5.3.1",
171
- "@helia/ipns": "^8.2.2",
172
- "@helia/routers": "^3.1.1",
173
- "@helia/unixfs": "^5.1.0",
174
- "@ipld/car": "^5.4.2",
167
+ "@helia/block-brokers": "^5.0.4",
168
+ "@helia/car": "^5.2.0",
169
+ "@helia/delegated-routing-v1-http-api-client": "^5.0.0",
170
+ "@helia/dnslink": "^1.0.1",
171
+ "@helia/interface": "^6.0.0",
172
+ "@helia/ipns": "^9.0.0",
173
+ "@helia/routers": "^4.0.0",
174
+ "@helia/unixfs": "^6.0.0",
175
175
  "@ipld/dag-cbor": "^9.2.3",
176
176
  "@ipld/dag-json": "^10.2.4",
177
177
  "@ipld/dag-pb": "^4.1.5",
178
- "@libp2p/interface": "^2.10.1",
179
- "@libp2p/kad-dht": "^15.1.1",
180
- "@libp2p/logger": "^5.1.17",
181
- "@libp2p/peer-id": "^5.1.4",
182
- "@libp2p/webrtc": "^5.2.14",
183
- "@libp2p/websockets": "^9.2.12",
178
+ "@libp2p/interface": "^3.0.0",
179
+ "@libp2p/kad-dht": "^16.0.0",
180
+ "@libp2p/logger": "^6.0.0",
181
+ "@libp2p/peer-id": "^6.0.0",
182
+ "@libp2p/utils": "^7.0.5",
183
+ "@libp2p/webrtc": "^6.0.0",
184
+ "@libp2p/websockets": "^10.0.0",
184
185
  "@multiformats/dns": "^1.0.6",
185
186
  "cborg": "^4.2.11",
186
- "file-type": "^20.5.0",
187
- "helia": "^5.4.1",
188
- "interface-blockstore": "^5.3.1",
189
- "interface-datastore": "^8.3.1",
190
- "ipfs-unixfs-exporter": "^13.7.2",
191
- "ipns": "^10.0.2",
187
+ "file-type": "^21.0.0",
188
+ "helia": "^6.0.5",
189
+ "interface-blockstore": "^6.0.1",
190
+ "ipfs-unixfs-exporter": "^14.0.1",
191
+ "ipns": "^10.1.3",
192
192
  "it-map": "^3.1.3",
193
193
  "it-pipe": "^3.0.1",
194
194
  "it-tar": "^6.0.5",
195
195
  "it-to-browser-readablestream": "^2.0.11",
196
- "libp2p": "^2.8.7",
196
+ "it-to-buffer": "^4.0.9",
197
+ "libp2p": "^3.0.0",
197
198
  "multiformats": "^13.3.6",
198
199
  "progress-events": "^1.0.1",
199
- "quick-lru": "^7.0.1",
200
- "uint8arrays": "^5.1.0"
200
+ "quick-lru": "^7.0.1"
201
201
  },
202
202
  "devDependencies": {
203
- "@helia/dag-cbor": "^4.0.5",
204
- "@helia/dag-json": "^4.0.5",
205
- "@helia/http": "^2.1.1",
206
- "@helia/json": "^4.0.5",
203
+ "@helia/dag-cbor": "^5.0.0",
204
+ "@helia/dag-json": "^5.0.0",
205
+ "@helia/http": "^3.0.5",
206
+ "@helia/json": "^5.0.0",
207
+ "@ipld/car": "^5.4.2",
207
208
  "@libp2p/crypto": "^5.1.3",
208
209
  "@types/sinon": "^17.0.4",
209
- "aegir": "^47.0.21",
210
- "blockstore-core": "^5.0.2",
210
+ "aegir": "^47.0.24",
211
211
  "browser-readablestream-to-it": "^2.0.9",
212
- "datastore-core": "^10.0.2",
213
- "helia": "^5.4.1",
214
- "ipfs-unixfs-importer": "^15.4.0",
212
+ "ipfs-unixfs-importer": "^16.0.1",
215
213
  "it-all": "^3.0.8",
216
214
  "it-last": "^3.0.8",
217
- "it-to-buffer": "^4.0.9",
218
215
  "magic-bytes.js": "^1.12.1",
219
216
  "p-defer": "^4.0.1",
220
- "sinon": "^20.0.0",
221
- "sinon-ts": "^2.0.0"
217
+ "race-signal": "^2.0.0",
218
+ "sinon": "^21.0.0",
219
+ "sinon-ts": "^2.0.0",
220
+ "uint8arrays": "^5.1.0"
222
221
  },
223
222
  "browser": {
224
223
  "./dist/src/utils/libp2p-defaults.js": "./dist/src/utils/libp2p-defaults.browser.js"
@@ -0,0 +1 @@
1
+ export const CODEC_IDENTITY = 0x00
package/src/index.ts CHANGED
@@ -745,12 +745,12 @@
745
745
  * canHandle(context: PluginContext): boolean {
746
746
  * // Only handle requests if the Accept header matches your custom type
747
747
  * // Or check context for pathDetails, custom values, etc...
748
- * return context.accept === 'application/vnd.my-custom-type'
748
+ * return context.accept?.mimeType === 'application/vnd.my-custom-type'
749
749
  * }
750
750
  *
751
751
  * async handle(context: PluginContext): Promise<Response | null> {
752
752
  * // Perform any partial processing here, e.g., modify the context:
753
- * context.customProcessed = true;
753
+ * context.customProcessed = true
754
754
  *
755
755
  * // If you are ready to finalize the response:
756
756
  * return new Response('Hello, world!', {
@@ -758,7 +758,7 @@
758
758
  * headers: {
759
759
  * 'Content-Type': 'text/plain'
760
760
  * }
761
- * });
761
+ * })
762
762
  *
763
763
  * // Or, if further processing is needed by another plugin, simply return null.
764
764
  * }
@@ -812,13 +812,13 @@
812
812
  * details: {
813
813
  * someKey: 'Additional details here'
814
814
  * }
815
- * });
815
+ * })
816
816
  * }
817
817
  *
818
818
  * if (recoverable === false) {
819
819
  * throw new PluginFatalError('MY_CUSTOM_FATAL', 'A critical error occurred', {
820
820
  * response: new Response('Something happened', { status: 500 }) // Required: supply your own error response
821
- * });
821
+ * })
822
822
  * }
823
823
  *
824
824
  * // Otherwise, continue processing...
@@ -857,10 +857,10 @@ import { createLibp2p } from 'libp2p'
857
857
  import { getLibp2pConfig } from './utils/libp2p-defaults.js'
858
858
  import { VerifiedFetch as VerifiedFetchClass } from './verified-fetch.js'
859
859
  import type { VerifiedFetchPluginFactory } from './plugins/types.js'
860
- import type { ContentTypeParser } from './types.js'
860
+ import type { DNSLink, ResolveProgressEvents as ResolveDNSLinkProgressEvents } from '@helia/dnslink'
861
861
  import type { GetBlockProgressEvents, Helia, Routing } from '@helia/interface'
862
- import type { ResolveDNSLinkProgressEvents } from '@helia/ipns'
863
- import type { Libp2p, ServiceMap } from '@libp2p/interface'
862
+ import type { IPNSResolver } from '@helia/ipns'
863
+ import type { AbortOptions, Libp2p, ServiceMap } from '@libp2p/interface'
864
864
  import type { DNSResolvers, DNS } from '@multiformats/dns'
865
865
  import type { DNSResolver } from '@multiformats/dns/resolvers'
866
866
  import type { HeliaInit } from 'helia'
@@ -868,6 +868,25 @@ import type { ExporterProgressEvents } from 'ipfs-unixfs-exporter'
868
868
  import type { Libp2pOptions } from 'libp2p'
869
869
  import type { CID } from 'multiformats/cid'
870
870
  import type { ProgressEvent, ProgressOptions } from 'progress-events'
871
+
872
+ export type RequestFormatShorthand = 'raw' | 'car' | 'tar' | 'ipns-record' | 'dag-json' | 'dag-cbor' | 'json' | 'cbor'
873
+
874
+ export type SupportedBodyTypes = string | Uint8Array | ArrayBuffer | Blob | ReadableStream<Uint8Array> | null
875
+
876
+ /**
877
+ * A ContentTypeParser attempts to return the mime type of a given file. It
878
+ * receives the first chunk of the file data and the file name, if it is
879
+ * available. The function can be sync or async and if it returns/resolves to
880
+ * `undefined`, `application/octet-stream` will be used.
881
+ */
882
+ export interface ContentTypeParser {
883
+ /**
884
+ * Attempt to determine a mime type, either via of the passed bytes or the
885
+ * filename if it is available.
886
+ */
887
+ (bytes: Uint8Array, fileName?: string): Promise<string | undefined> | string | undefined
888
+ }
889
+
871
890
  /**
872
891
  * The types for the first argument of the `verifiedFetch` function.
873
892
  */
@@ -879,7 +898,7 @@ export interface ResourceDetail {
879
898
 
880
899
  export interface CIDDetail {
881
900
  cid: CID
882
- path: string
901
+ path?: string
883
902
  }
884
903
 
885
904
  export interface CIDDetailError extends CIDDetail {
@@ -995,24 +1014,32 @@ export interface CreateVerifiedFetchOptions {
995
1014
  * If you want to replace one of the default plugins, you can do so by passing a plugin with the same name.
996
1015
  */
997
1016
  plugins?: VerifiedFetchPluginFactory[]
998
- }
999
1017
 
1000
- export type { ContentTypeParser } from './types.js'
1018
+ /**
1019
+ * Used to resolve IPNS names
1020
+ */
1021
+ ipnsResolver?: IPNSResolver
1001
1022
 
1002
- export type BubbledProgressEvents =
1003
- // unixfs-exporter
1004
- ExporterProgressEvents |
1005
- // helia blockstore
1006
- GetBlockProgressEvents |
1007
- // ipns
1008
- ResolveDNSLinkProgressEvents
1023
+ /**
1024
+ * Used to resolve DNSLink entries to IPNS names or CIDs
1025
+ */
1026
+ dnsLink?: DNSLink
1027
+
1028
+ /**
1029
+ * Used to turn URLs into CIDs/paths
1030
+ */
1031
+ urlResolver?: URLResolver
1032
+ }
1009
1033
 
1010
1034
  export type VerifiedFetchProgressEvents =
1011
1035
  ProgressEvent<'verified-fetch:request:start', CIDDetail> |
1012
1036
  ProgressEvent<'verified-fetch:request:info', string> |
1013
- ProgressEvent<'verified-fetch:request:progress:chunk', CIDDetail> |
1037
+ ProgressEvent<'verified-fetch:request:progress:chunk'> |
1014
1038
  ProgressEvent<'verified-fetch:request:end', CIDDetail> |
1015
- ProgressEvent<'verified-fetch:request:error', CIDDetailError>
1039
+ ProgressEvent<'verified-fetch:request:error', CIDDetailError> |
1040
+ ExporterProgressEvents |
1041
+ GetBlockProgressEvents |
1042
+ ResolveDNSLinkProgressEvents
1016
1043
 
1017
1044
  /**
1018
1045
  * Options for the `fetch` function returned by `createVerifiedFetch`.
@@ -1021,7 +1048,7 @@ export type VerifiedFetchProgressEvents =
1021
1048
  * passed to `fetch` in browsers, plus an `onProgress` option to listen for
1022
1049
  * progress events.
1023
1050
  */
1024
- export interface VerifiedFetchInit extends RequestInit, ProgressOptions<BubbledProgressEvents | VerifiedFetchProgressEvents> {
1051
+ export interface VerifiedFetchInit extends RequestInit, ProgressOptions<VerifiedFetchProgressEvents> {
1025
1052
  /**
1026
1053
  * If true, try to create a blockstore session - this can reduce overall
1027
1054
  * network traffic by first querying for a set of peers that have the data we
@@ -1072,6 +1099,30 @@ export interface VerifiedFetchInit extends RequestInit, ProgressOptions<BubbledP
1072
1099
  withServerTiming?: boolean
1073
1100
  }
1074
1101
 
1102
+ export interface ResolveURLOptions extends ProgressOptions<VerifiedFetchProgressEvents>, AbortOptions {
1103
+
1104
+ }
1105
+
1106
+ export interface UrlQuery extends Record<string, string | unknown> {
1107
+ format?: RequestFormatShorthand
1108
+ download?: boolean
1109
+ filename?: string
1110
+ 'dag-scope'?: string
1111
+ }
1112
+
1113
+ export interface ResolveURLResult {
1114
+ cid: CID
1115
+ protocol: string
1116
+ ttl: number
1117
+ path: string
1118
+ query: UrlQuery
1119
+ ipfsPath: string
1120
+ }
1121
+
1122
+ export interface URLResolver {
1123
+ resolve (resource: Resource, options?: ResolveURLOptions): Promise<ResolveURLResult>
1124
+ }
1125
+
1075
1126
  /**
1076
1127
  * Create and return a Helia node
1077
1128
  */
@@ -1116,7 +1167,7 @@ export async function createVerifiedFetch (init?: Helia | CreateVerifiedFetchIni
1116
1167
  init.logger.forComponent('helia:verified-fetch').trace('created verified-fetch with libp2p config: %j', libp2pConfig)
1117
1168
  }
1118
1169
 
1119
- const verifiedFetchInstance = new VerifiedFetchClass({ helia: init }, options)
1170
+ const verifiedFetchInstance = new VerifiedFetchClass(init, options)
1120
1171
  async function verifiedFetch (resource: Resource, options?: VerifiedFetchInit): Promise<Response> {
1121
1172
  return verifiedFetchInstance.fetch(resource, options)
1122
1173
  }
@@ -1,11 +1,12 @@
1
1
  import { BlockExporter, car, CIDPath, SubgraphExporter, UnixFSExporter } from '@helia/car'
2
- import { CarWriter } from '@ipld/car'
3
2
  import { code as dagPbCode } from '@ipld/dag-pb'
3
+ import { createScalableCuckooFilter } from '@libp2p/utils'
4
4
  import toBrowserReadableStream from 'it-to-browser-readablestream'
5
+ import { getOffsetAndLength } from '../utils/get-offset-and-length.ts'
5
6
  import { okRangeResponse } from '../utils/responses.js'
6
7
  import { BasePlugin } from './plugin-base.js'
7
8
  import type { PluginContext } from './types.js'
8
- import type { ExportCarOptions } from '@helia/car'
9
+ import type { ExportCarOptions, UnixFSExporterOptions } from '@helia/car'
9
10
 
10
11
  function getFilename ({ cid, ipfsPath, query }: Pick<PluginContext, 'query' | 'cid' | 'ipfsPath'>): string {
11
12
  if (query.filename != null) {
@@ -13,7 +14,10 @@ function getFilename ({ cid, ipfsPath, query }: Pick<PluginContext, 'query' | 'c
13
14
  }
14
15
 
15
16
  // convert context.ipfsPath to a filename. replace all / with _, replace prefix protocol with empty string
16
- const filename = ipfsPath.replace(/\/ipfs\//, '').replace(/\/ipns\//, '').replace(/\//g, '_')
17
+ const filename = ipfsPath
18
+ .replace(/\/ipfs\//, '')
19
+ .replace(/\/ipns\//, '')
20
+ .replace(/\//g, '_')
17
21
 
18
22
  return `${filename}.car`
19
23
  }
@@ -22,9 +26,16 @@ function getFilename ({ cid, ipfsPath, query }: Pick<PluginContext, 'query' | 'c
22
26
  type DagScope = 'all' | 'entity' | 'block'
23
27
  function getDagScope ({ query }: Pick<PluginContext, 'query'>): DagScope | null {
24
28
  const dagScope = query['dag-scope']
29
+
25
30
  if (dagScope === 'all' || dagScope === 'entity' || dagScope === 'block') {
26
31
  return dagScope
27
32
  }
33
+
34
+ // entity-bytes implies entity scope
35
+ if (query['entity-bytes']) {
36
+ return 'entity'
37
+ }
38
+
28
39
  return 'all'
29
40
  }
30
41
 
@@ -37,21 +48,28 @@ export class CarPlugin extends BasePlugin {
37
48
 
38
49
  canHandle (context: PluginContext): boolean {
39
50
  this.log('checking if we can handle %c with accept %s', context.cid, context.accept)
51
+
40
52
  if (context.byteRangeContext == null) {
41
53
  return false
42
54
  }
55
+
43
56
  if (context.pathDetails == null) {
44
57
  return false
45
58
  }
46
59
 
47
- return context.accept?.startsWith('application/vnd.ipld.car') === true || context.query.format === 'car' // application/vnd.ipld.car
60
+ return context.accept?.mimeType.startsWith('application/vnd.ipld.car') === true || context.query.format === 'car' // application/vnd.ipld.car
48
61
  }
49
62
 
50
63
  async handle (context: PluginContext & Required<Pick<PluginContext, 'byteRangeContext'>>): Promise<Response> {
51
- const { options, pathDetails, cid } = context
64
+ const { options, pathDetails, cid, query, accept } = context
65
+
66
+ const order = accept?.options.order === 'dfs' ? 'dfs' : 'unk'
67
+ const duplicates = accept?.options.dups !== 'n'
68
+
52
69
  if (pathDetails == null) {
53
70
  throw new Error('attempted to handle request for car with no path details')
54
71
  }
72
+
55
73
  const { getBlockstore, helia } = this.pluginOptions
56
74
  context.reqFormat = 'car'
57
75
  context.query.download = true
@@ -63,46 +81,52 @@ export class CarPlugin extends BasePlugin {
63
81
  getCodec: helia.getCodec,
64
82
  logger: helia.logger
65
83
  })
66
- const ipfsRootsWithoutDagRoot = pathDetails.ipfsRoots.filter(pathCid => !pathCid.equals(cid))
84
+
67
85
  const carExportOptions: ExportCarOptions = {
68
- ...options
86
+ ...options,
87
+ includeTraversalBlocks: true
69
88
  }
70
- if (ipfsRootsWithoutDagRoot.length > 0) {
71
- carExportOptions.traversal = new CIDPath(ipfsRootsWithoutDagRoot)
89
+
90
+ if (!duplicates) {
91
+ carExportOptions.blockFilter = createScalableCuckooFilter(1024)
72
92
  }
93
+
94
+ if (pathDetails.ipfsRoots.length > 1) {
95
+ carExportOptions.traversal = new CIDPath(pathDetails.ipfsRoots)
96
+ }
97
+
73
98
  const dagScope = getDagScope(context)
74
- // root should be the terminal element if it exists, otherwise the root cid.. because of this, we can't use the @helia/car stream() method.
75
- const root = pathDetails.terminalElement.cid ?? cid
99
+ const target = pathDetails.terminalElement.cid ?? cid
100
+
76
101
  if (dagScope === 'block') {
77
102
  carExportOptions.exporter = new BlockExporter()
78
103
  } else if (dagScope === 'entity') {
79
- // if its unixFS, we need to enumerate a directory, or get all blocks for the entity, otherwise, use blockExporter
80
- if (root.code === dagPbCode) {
81
- carExportOptions.exporter = new UnixFSExporter()
104
+ // if its unixFS, we need to enumerate a directory, or get all/some blocks
105
+ // for the entity, otherwise, use blockExporter
106
+ if (target.code === dagPbCode) {
107
+ const options: UnixFSExporterOptions = {
108
+ listingOnly: true
109
+ }
110
+
111
+ const slice = getOffsetAndLength(pathDetails.terminalElement, query['entity-bytes']?.toString())
112
+ options.offset = slice.offset
113
+ options.length = slice.length
114
+
115
+ carExportOptions.exporter = new UnixFSExporter(options)
82
116
  } else {
83
117
  carExportOptions.exporter = new BlockExporter()
84
118
  }
85
119
  } else {
86
120
  carExportOptions.exporter = new SubgraphExporter()
87
121
  }
88
- const { writer, out } = CarWriter.create(root)
89
- const iter = async function * (): AsyncIterable<Uint8Array> {
90
- for await (const buf of out) {
91
- yield buf
92
- }
93
- }
94
122
 
95
- // the root passed to export should be the root CID of the DAG, not the terminal element.
96
- c.export(cid, writer, carExportOptions)
97
- .catch((err) => {
98
- this.log.error('error exporting car - %e', err)
99
- })
100
- // export will close the writer when it's done, no finally needed.
123
+ context.byteRangeContext.setBody(toBrowserReadableStream(c.export(target, carExportOptions)))
101
124
 
102
- context.byteRangeContext.setBody(toBrowserReadableStream(iter()))
103
-
104
- const response = okRangeResponse(context.resource, context.byteRangeContext.getBody('application/vnd.ipld.car; version=1'), { byteRangeContext: context.byteRangeContext, log: this.log })
105
- response.headers.set('content-type', context.byteRangeContext.getContentType() ?? 'application/vnd.ipld.car; version=1')
125
+ const response = okRangeResponse(context.resource, context.byteRangeContext.getBody('application/vnd.ipld.car; version=1'), {
126
+ byteRangeContext: context.byteRangeContext,
127
+ log: this.log
128
+ })
129
+ response.headers.set('content-type', context.byteRangeContext.getContentType() ?? `application/vnd.ipld.car; version=1; order=${order}; dups=${duplicates ? 'y' : 'n'}`)
106
130
 
107
131
  return response
108
132
  }
@@ -62,7 +62,7 @@ export class DagCborHtmlPreviewPlugin extends BasePlugin {
62
62
  return false
63
63
  }
64
64
 
65
- if (accept == null || !accept.includes('text/html')) {
65
+ if (accept == null || !accept?.mimeType.includes('text/html')) {
66
66
  return false
67
67
  }
68
68
 
@@ -97,7 +97,7 @@ export class DagCborHtmlPreviewPlugin extends BasePlugin {
97
97
  })
98
98
  }
99
99
 
100
- getHtml ({ path, obj, cid }: { path: string, obj: Record<string, any>, cid: CID }): string {
100
+ getHtml ({ path, obj, cid }: { path?: string, obj: Record<string, any>, cid: CID }): string {
101
101
  const style = `
102
102
  :root {
103
103
  --sans-serif: "Plex", system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell, Noto Sans, sans-serif;
@@ -30,7 +30,7 @@ export class DagCborPlugin extends BasePlugin {
30
30
  return false
31
31
  }
32
32
 
33
- if (accept != null && accept.includes('text/html') && plugins.includes('dag-cbor-plugin-html-preview')) {
33
+ if (accept != null && accept.mimeType === 'text/html' && plugins.includes('dag-cbor-plugin-html-preview')) {
34
34
  // let the dag-cbor-html-preview plugin handle it
35
35
  return false
36
36
  }
@@ -47,10 +47,10 @@ export class DagCborPlugin extends BasePlugin {
47
47
 
48
48
  let body: string | Uint8Array
49
49
 
50
- if (accept === 'application/octet-stream' || accept === 'application/vnd.ipld.dag-cbor' || accept === 'application/cbor') {
50
+ if (accept?.mimeType === 'application/octet-stream' || accept?.mimeType === 'application/vnd.ipld.dag-cbor' || accept?.mimeType === 'application/cbor') {
51
51
  // skip decoding
52
52
  body = block
53
- } else if (accept === 'application/vnd.ipld.dag-json') {
53
+ } else if (accept?.mimeType === 'application/vnd.ipld.dag-json') {
54
54
  try {
55
55
  // if vnd.ipld.dag-json has been specified, convert to the format - note
56
56
  // that this supports more data types than regular JSON, the content-type
@@ -65,7 +65,7 @@ export class DagCborPlugin extends BasePlugin {
65
65
  try {
66
66
  body = dagCborToSafeJSON(block)
67
67
  } catch (err) {
68
- if (accept === 'application/json') {
68
+ if (accept?.mimeType === 'application/json') {
69
69
  this.log('could not decode DAG-CBOR as JSON-safe, but the client sent "Accept: application/json"', err)
70
70
 
71
71
  return notAcceptableResponse(resource)
@@ -78,7 +78,7 @@ export class DagCborPlugin extends BasePlugin {
78
78
 
79
79
  context.byteRangeContext.setBody(body)
80
80
 
81
- const responseContentType = accept ?? (body instanceof Uint8Array ? 'application/octet-stream' : 'application/json')
81
+ const responseContentType = accept?.mimeType ?? (body instanceof Uint8Array ? 'application/octet-stream' : 'application/json')
82
82
  const response = okRangeResponse(resource, context.byteRangeContext.getBody(responseContentType), { byteRangeContext: context.byteRangeContext, log: this.log })
83
83
 
84
84
  response.headers.set('content-type', context.byteRangeContext.getContentType() ?? responseContentType)
@@ -55,8 +55,8 @@ export class DagPbPlugin extends BasePlugin {
55
55
  }
56
56
 
57
57
  async handle (context: PluginContext & Required<Pick<PluginContext, 'byteRangeContext' | 'pathDetails'>>): Promise<Response | null> {
58
- const { cid, options, withServerTiming = false, pathDetails, query } = context
59
- const { handleServerTiming, contentTypeParser, helia, getBlockstore } = this.pluginOptions
58
+ const { cid, options, pathDetails, query } = context
59
+ const { contentTypeParser, helia, getBlockstore } = this.pluginOptions
60
60
  const log = this.log
61
61
  let resource = context.resource
62
62
  let path = context.path
@@ -93,10 +93,10 @@ export class DagPbPlugin extends BasePlugin {
93
93
  try {
94
94
  log.trace('found directory at %c/%s, looking for index.html', cid, path)
95
95
 
96
- const entry = await handleServerTiming('exporter-dir', '', async () => exporter(`/ipfs/${dirCid}/${rootFilePath}`, helia.blockstore, {
96
+ const entry = await context.serverTiming.time('exporter-dir', '', exporter(`/ipfs/${dirCid}/${rootFilePath}`, helia.blockstore, {
97
97
  signal: options?.signal,
98
98
  onProgress: options?.onProgress
99
- }), withServerTiming)
99
+ }))
100
100
 
101
101
  log.trace('found root file at %c/%s with cid %c', dirCid, rootFilePath, entry.cid)
102
102
  path = rootFilePath
@@ -136,10 +136,10 @@ export class DagPbPlugin extends BasePlugin {
136
136
  }
137
137
 
138
138
  try {
139
- const entry = await handleServerTiming('exporter-file', '', async () => exporter(resolvedCID, helia.blockstore, {
139
+ const entry = await context.serverTiming.time('exporter-file', '', exporter(resolvedCID, helia.blockstore, {
140
140
  signal: options?.signal,
141
141
  onProgress: options?.onProgress
142
- }), withServerTiming)
142
+ }))
143
143
 
144
144
  let firstChunk: Uint8Array
145
145
  let contentType: string
@@ -152,13 +152,13 @@ export class DagPbPlugin extends BasePlugin {
152
152
  })
153
153
  log('got async iterator for %c/%s', cid, path)
154
154
 
155
- const streamAndFirstChunk = await handleServerTiming('stream-and-chunk', '', async () => getStreamFromAsyncIterable(asyncIter, path ?? '', this.pluginOptions.logger, {
155
+ const streamAndFirstChunk = await context.serverTiming.time('stream-and-chunk', '', getStreamFromAsyncIterable(asyncIter, path ?? '', this.pluginOptions.logger, {
156
156
  onProgress: options?.onProgress,
157
157
  signal: options?.signal
158
- }), withServerTiming)
158
+ }))
159
159
  const stream = streamAndFirstChunk.stream
160
160
  firstChunk = streamAndFirstChunk.firstChunk
161
- contentType = await handleServerTiming('get-content-type', '', async () => getContentType({ filename: query.filename, bytes: firstChunk, path, contentTypeParser, log }), withServerTiming)
161
+ contentType = await context.serverTiming.time('get-content-type', '', getContentType({ filename: query.filename, bytes: firstChunk, path, contentTypeParser, log }))
162
162
 
163
163
  byteRangeContext.setBody(stream)
164
164
  }
@@ -186,8 +186,8 @@ export class DagPbPlugin extends BasePlugin {
186
186
  }
187
187
 
188
188
  private async handleRangeRequest (context: PluginContext & Required<Pick<PluginContext, 'byteRangeContext' | 'pathDetails'>>, entry: UnixFSEntry): Promise<string> {
189
- const { path, byteRangeContext, options, withServerTiming = false } = context
190
- const { handleServerTiming, contentTypeParser } = this.pluginOptions
189
+ const { path, byteRangeContext, options } = context
190
+ const { contentTypeParser } = this.pluginOptions
191
191
  const log = this.log
192
192
 
193
193
  // get the first chunk in order to determine the content type
@@ -203,7 +203,7 @@ export class DagPbPlugin extends BasePlugin {
203
203
  onProgress: options?.onProgress,
204
204
  signal: options?.signal
205
205
  })
206
- const contentType = await handleServerTiming('get-content-type', '', async () => getContentType({ bytes: firstChunk, path, contentTypeParser, log }), withServerTiming)
206
+ const contentType = await context.serverTiming.time('get-content-type', '', getContentType({ bytes: firstChunk, path, contentTypeParser, log }))
207
207
 
208
208
  byteRangeContext?.setBody((range): AsyncGenerator<Uint8Array, void, unknown> => {
209
209
  if (options?.signal?.aborted) {
@@ -1,5 +1,6 @@
1
1
  import { code as dagCborCode } from '@ipld/dag-cbor'
2
2
  import { code as dagPbCode } from '@ipld/dag-pb'
3
+ import { CODEC_IDENTITY } from '../constants.ts'
3
4
  import { handlePathWalking } from '../utils/walk-path.js'
4
5
  import { BasePlugin } from './plugin-base.js'
5
6
  import type { PluginContext } from './types.js'
@@ -23,16 +24,16 @@ export class DagWalkPlugin extends BasePlugin {
23
24
  return false
24
25
  }
25
26
 
26
- return (cid.code === dagPbCode || cid.code === dagCborCode)
27
+ return (cid.code === dagPbCode || cid.code === dagCborCode || cid.multihash.code === CODEC_IDENTITY)
27
28
  }
28
29
 
29
30
  async handle (context: PluginContext): Promise<Response | null> {
30
- const { cid, resource, options, withServerTiming = false } = context
31
- const { getBlockstore, handleServerTiming } = this.pluginOptions
31
+ const { cid, resource, options } = context
32
+ const { getBlockstore } = this.pluginOptions
32
33
  const blockstore = getBlockstore(cid, resource, options?.session ?? true, options)
33
34
 
34
35
  // TODO: migrate handlePathWalking into this plugin
35
- const pathDetails = await handleServerTiming('path-walking', '', async () => handlePathWalking({ ...context, blockstore, log: this.log }), withServerTiming)
36
+ const pathDetails = await context.serverTiming.time('path-walking', '', handlePathWalking({ ...context, blockstore, log: this.log }))
36
37
 
37
38
  if (pathDetails instanceof Response) {
38
39
  this.log.trace('path walking failed')