@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,19 +1,17 @@
1
- import type { VerifiedFetchPlugin, PluginContext, PluginOptions } from './types.js'
1
+ import type { VerifiedFetchPlugin, PluginContext, PluginOptions } from '../index.js'
2
2
  import type { Logger } from '@libp2p/interface'
3
3
 
4
4
  /**
5
- * Base class for verified-fetch plugins. This class provides a basic implementation of the `FetchHandlerPlugin`
6
- * interface.
5
+ * Base class for verified-fetch plugins. This class provides a basic
6
+ * implementation of the `VerifiedFetchPlugin` interface.
7
7
  *
8
- * Subclasses must implement the `id` property and the `canHandle` and `handle` methods.
9
- * Subclasses may override the `codes` and `log` properties.
8
+ * Subclasses must implement the `id` property, the `canHandle`, and `handle`
9
+ * methods.
10
10
  *
11
- * If your plugin adds/edits the context supplied in `handle`, you should increment the `context.modified` property.
11
+ * Subclasses may override the `codes` and `log` properties.
12
12
  */
13
13
  export abstract class BasePlugin implements VerifiedFetchPlugin {
14
- readonly codes: number[] = []
15
14
  readonly pluginOptions: PluginOptions
16
- abstract readonly id: string
17
15
  protected _log?: Logger
18
16
 
19
17
  get log (): Logger {
@@ -29,7 +27,7 @@ export abstract class BasePlugin implements VerifiedFetchPlugin {
29
27
  this.pluginOptions = options
30
28
  }
31
29
 
30
+ abstract readonly id: string
32
31
  abstract canHandle (context: PluginContext): boolean
33
-
34
- abstract handle (context: PluginContext): Promise<Response | null>
32
+ abstract handle (context: PluginContext): Promise<Response>
35
33
  }
@@ -2,17 +2,15 @@ import { BlockExporter, car, CIDPath, depthFirstWalker, naturalOrderWalker, Subg
2
2
  import { code as dagPbCode } from '@ipld/dag-pb'
3
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'
6
- import { notAcceptableResponse, okRangeResponse } from '../utils/responses.js'
5
+ import { CONTENT_TYPE_CAR, MEDIA_TYPE_CAR } from '../utils/content-types.ts'
6
+ import { getContentDispositionFilename } from '../utils/get-content-disposition-filename.ts'
7
+ import { entityBytesToOffsetAndLength } from '../utils/get-offset-and-length.ts'
8
+ import { badRequestResponse, notAcceptableResponse, okResponse } from '../utils/responses.js'
7
9
  import { BasePlugin } from './plugin-base.js'
8
- import type { PluginContext } from './types.js'
10
+ import type { PluginContext } from '../index.js'
9
11
  import type { ExportCarOptions, UnixFSExporterOptions } from '@helia/car'
10
12
 
11
- function getFilename ({ cid, ipfsPath, query }: Pick<PluginContext, 'query' | 'cid' | 'ipfsPath'>): string {
12
- if (query.filename != null) {
13
- return query.filename
14
- }
15
-
13
+ function getFilename (ipfsPath: string): string {
16
14
  // convert context.ipfsPath to a filename. replace all / with _, replace prefix protocol with empty string
17
15
  const filename = ipfsPath
18
16
  .replace(/\/ipfs\//, '')
@@ -23,17 +21,20 @@ function getFilename ({ cid, ipfsPath, query }: Pick<PluginContext, 'query' | 'c
23
21
  return `${filename}.car`
24
22
  }
25
23
 
26
- // https://specs.ipfs.tech/http-gateways/trustless-gateway/#dag-scope-request-query-parameter
24
+ /**
25
+ * @see https://specs.ipfs.tech/http-gateways/trustless-gateway/#dag-scope-request-query-parameter
26
+ */
27
27
  type DagScope = 'all' | 'entity' | 'block'
28
- function getDagScope ({ query }: Pick<PluginContext, 'query'>): DagScope | null {
29
- const dagScope = query['dag-scope']
28
+
29
+ function getDagScope ({ url }: PluginContext): DagScope | null {
30
+ const dagScope = url.searchParams.get('dag-scope')
30
31
 
31
32
  if (dagScope === 'all' || dagScope === 'entity' || dagScope === 'block') {
32
33
  return dagScope
33
34
  }
34
35
 
35
36
  // entity-bytes implies entity scope
36
- if (query['entity-bytes']) {
37
+ if (url.searchParams.has('entity-bytes')) {
37
38
  return 'entity'
38
39
  }
39
40
 
@@ -47,38 +48,36 @@ function getDagScope ({ query }: Pick<PluginContext, 'query'>): DagScope | null
47
48
  export class CarPlugin extends BasePlugin {
48
49
  readonly id = 'car-plugin'
49
50
 
50
- canHandle (context: PluginContext): boolean {
51
- if (context.byteRangeContext == null) {
52
- return false
53
- }
54
-
55
- if (context.pathDetails == null) {
56
- return false
57
- }
58
-
59
- return context.accept?.mimeType.startsWith('application/vnd.ipld.car') === true || context.query.format === 'car' // application/vnd.ipld.car
51
+ canHandle ({ accept }: PluginContext): boolean {
52
+ return accept.some(header => header.contentType.mediaType === MEDIA_TYPE_CAR)
60
53
  }
61
54
 
62
- async handle (context: PluginContext & Required<Pick<PluginContext, 'byteRangeContext'>>): Promise<Response> {
63
- const { options, pathDetails, cid, query, accept, resource } = context
55
+ async handle (context: PluginContext): Promise<Response> {
56
+ const { options, url, accept, resource, blockstore, range, ipfsRoots, terminalElement } = context
57
+
58
+ if (range != null) {
59
+ return badRequestResponse(resource, new Error('Range requests are not supported for CAR files'))
60
+ }
64
61
 
65
- const order = accept?.options.order === 'dfs' || context.query['car-order'] === 'dfs' ? 'dfs' : 'unk'
66
- const duplicates = accept?.options.dups !== 'n' && context.query['car-dups'] !== 'n'
62
+ const acceptCar = accept.filter(header => header.contentType.mediaType === MEDIA_TYPE_CAR).pop()
67
63
 
68
- if (pathDetails == null) {
69
- throw new Error('attempted to handle request for car with no path details')
64
+ // we have already asserted that the CAR media type is present so this
65
+ // branch should never be hit
66
+ if (acceptCar == null) {
67
+ return badRequestResponse(resource, new Error('Could not find CAR media type in accept header'))
70
68
  }
71
69
 
70
+ const order = acceptCar.options.order === 'dfs' || url.searchParams.get('car-order') === 'dfs' ? 'dfs' : 'unk'
71
+ const duplicates = acceptCar.options.dups !== 'n' && url.searchParams.get('car-dups') !== 'n'
72
+
72
73
  // TODO: `@ipld/car` only supports CARv1
73
- if (accept?.options.version === '2' || context.query['car-version'] === '2') {
74
- return notAcceptableResponse(resource, 'Only CARv1 is supported')
74
+ if (acceptCar.options.version === '2' || url.searchParams.get('car-version') === '2') {
75
+ return notAcceptableResponse(resource, [
76
+ CONTENT_TYPE_CAR
77
+ ])
75
78
  }
76
79
 
77
- const { getBlockstore, helia } = this.pluginOptions
78
- context.reqFormat = 'car'
79
- context.query.download = true
80
- context.query.filename = getFilename(context)
81
- const blockstore = getBlockstore(cid, context.resource, options?.session ?? true, options)
80
+ const helia = this.pluginOptions.helia
82
81
 
83
82
  const c = car({
84
83
  blockstore,
@@ -95,12 +94,12 @@ export class CarPlugin extends BasePlugin {
95
94
  carExportOptions.blockFilter = createScalableCuckooFilter(1024)
96
95
  }
97
96
 
98
- if (pathDetails.ipfsRoots.length > 1) {
99
- carExportOptions.traversal = new CIDPath(pathDetails.ipfsRoots)
97
+ if (ipfsRoots.length > 1) {
98
+ carExportOptions.traversal = new CIDPath(ipfsRoots)
100
99
  }
101
100
 
102
101
  const dagScope = getDagScope(context)
103
- const target = pathDetails.terminalElement.cid ?? cid
102
+ const target = terminalElement.cid
104
103
 
105
104
  if (dagScope === 'block') {
106
105
  carExportOptions.exporter = new BlockExporter()
@@ -112,7 +111,7 @@ export class CarPlugin extends BasePlugin {
112
111
  listingOnly: true
113
112
  }
114
113
 
115
- const slice = getOffsetAndLength(pathDetails.terminalElement, query['entity-bytes']?.toString())
114
+ const slice = entityBytesToOffsetAndLength(terminalElement.size, url.searchParams.get('entity-bytes'))
116
115
  options.offset = slice.offset
117
116
  options.length = slice.length
118
117
 
@@ -126,14 +125,17 @@ export class CarPlugin extends BasePlugin {
126
125
  })
127
126
  }
128
127
 
129
- context.byteRangeContext.setBody(toBrowserReadableStream(c.export(target, carExportOptions)))
128
+ const stream = toBrowserReadableStream(c.export(target, carExportOptions))
130
129
 
131
- const response = okRangeResponse(context.resource, context.byteRangeContext.getBody('application/vnd.ipld.car; version=1'), {
132
- byteRangeContext: context.byteRangeContext,
133
- log: this.log
130
+ return okResponse(resource, stream, {
131
+ headers: {
132
+ 'content-type': `${MEDIA_TYPE_CAR}; version=1; order=${order}; dups=${duplicates ? 'y' : 'n'}`,
133
+ 'content-disposition': `attachment; ${
134
+ getContentDispositionFilename(url.searchParams.get('filename') ?? getFilename(`/ipfs/${url.hostname}${url.pathname}`))
135
+ }`,
136
+ 'x-content-type-options': 'nosniff',
137
+ 'accept-ranges': 'none'
138
+ }
134
139
  })
135
- response.headers.set('content-type', context.byteRangeContext.getContentType() ?? `application/vnd.ipld.car; version=1; order=${order}; dups=${duplicates ? 'y' : 'n'}`)
136
-
137
- return response
138
140
  }
139
141
  }
@@ -0,0 +1,93 @@
1
+ import * as ipldDagCbor from '@ipld/dag-cbor'
2
+ import * as ipldDagJson from '@ipld/dag-json'
3
+ import * as dagPb from '@ipld/dag-pb'
4
+ import toBuffer from 'it-to-buffer'
5
+ import * as json from 'multiformats/codecs/json'
6
+ import * as raw from 'multiformats/codecs/raw'
7
+ import { identity } from 'multiformats/hashes/identity'
8
+ import { CODEC_CBOR } from '../constants.ts'
9
+ import { getContentTypesForCid, MEDIA_TYPE_CBOR, MEDIA_TYPE_DAG_CBOR, MEDIA_TYPE_DAG_JSON, MEDIA_TYPE_JSON, MEDIA_TYPE_OCTET_STREAM, MEDIA_TYPE_RAW } from '../utils/content-types.ts'
10
+ import { convertOutput } from '../utils/convert-output.ts'
11
+ import { getContentDispositionFilename } from '../utils/get-content-disposition-filename.ts'
12
+ import { notAcceptableResponse, okResponse, partialContentResponse } from '../utils/responses.js'
13
+ import { BasePlugin } from './plugin-base.js'
14
+ import type { ContentType, PluginContext } from '../index.ts'
15
+
16
+ /**
17
+ * Handles loading JSON and CBOR content
18
+ */
19
+ export class IpldPlugin extends BasePlugin {
20
+ readonly id = 'ipld-plugin'
21
+ readonly codes = [
22
+ CODEC_CBOR,
23
+ ipldDagCbor.code,
24
+ ipldDagJson.code,
25
+ json.code,
26
+ raw.code,
27
+ dagPb.code,
28
+ identity.code
29
+ ]
30
+
31
+ canHandle ({ terminalElement, accept }: PluginContext): boolean {
32
+ const supportsCid = this.codes.includes(terminalElement.cid.code)
33
+ const supportsAccept = accept.length === 0 ||
34
+ accept.some(header => header.contentType.mediaType === MEDIA_TYPE_CBOR ||
35
+ header.contentType.mediaType === MEDIA_TYPE_DAG_CBOR ||
36
+ header.contentType.mediaType === MEDIA_TYPE_JSON ||
37
+ header.contentType.mediaType === MEDIA_TYPE_DAG_JSON ||
38
+ header.contentType.mediaType === MEDIA_TYPE_RAW ||
39
+ header.contentType.mediaType === MEDIA_TYPE_OCTET_STREAM
40
+ )
41
+
42
+ return supportsCid && supportsAccept
43
+ }
44
+
45
+ async handle (context: PluginContext): Promise<Response> {
46
+ const { url, resource, accept, ipfsRoots, terminalElement, blockstore, options } = context
47
+
48
+ this.log.trace('fetching %c/%s', terminalElement.cid, url.pathname)
49
+ let block: Uint8Array
50
+ if (terminalElement.node == null) {
51
+ block = await toBuffer(blockstore.get(terminalElement.cid, options))
52
+ } else if (terminalElement.type === 'object' || terminalElement.type === 'raw' || terminalElement.type === 'identity') {
53
+ block = terminalElement.node
54
+ } else {
55
+ block = dagPb.encode(terminalElement.node)
56
+ }
57
+
58
+ let contentType: ContentType
59
+
60
+ try {
61
+ // maybe convert output to different binary format
62
+ const result = convertOutput(terminalElement.cid, block, accept)
63
+ block = result.output
64
+ contentType = result.contentType
65
+ } catch (err) {
66
+ this.log.error('could not decode object from block - %e', err)
67
+ return notAcceptableResponse(resource, getContentTypesForCid(terminalElement.cid))
68
+ }
69
+
70
+ const headers = {
71
+ 'content-length': `${block.byteLength}`,
72
+ 'content-type': contentType.mediaType,
73
+ 'content-disposition': `${url.searchParams.get('download') === 'true' ? 'attachment' : contentType.disposition}; ${
74
+ getContentDispositionFilename(url.searchParams.get('filename') ?? `${terminalElement.cid}${contentType.extension}`)
75
+ }`,
76
+ 'x-ipfs-roots': ipfsRoots.map(cid => cid.toV1()).join(','),
77
+ 'x-content-type-options': 'nosniff',
78
+ 'accept-ranges': 'bytes'
79
+ }
80
+
81
+ if (context.range != null) {
82
+ return partialContentResponse(resource, async function * (offset, length) {
83
+ yield block.subarray(offset, offset + length)
84
+ }, context.range, block.byteLength, {
85
+ headers
86
+ })
87
+ }
88
+
89
+ return okResponse(resource, block, {
90
+ headers
91
+ })
92
+ }
93
+ }
@@ -1,8 +1,10 @@
1
+ import { peerIdFromString } from '@libp2p/peer-id'
1
2
  import { marshalIPNSRecord } from 'ipns'
2
- import { getPeerIdFromString } from '../utils/get-peer-id-from-string.js'
3
- import { badRequestResponse, okRangeResponse } from '../utils/responses.js'
3
+ import { CONTENT_TYPE_IPNS, MEDIA_TYPE_IPNS_RECORD } from '../utils/content-types.ts'
4
+ import { getContentDispositionFilename } from '../utils/get-content-disposition-filename.ts'
5
+ import { badRequestResponse, okResponse } from '../utils/responses.js'
4
6
  import { BasePlugin } from './plugin-base.js'
5
- import type { PluginContext } from './types.js'
7
+ import type { PluginContext } from '../index.js'
6
8
  import type { PeerId } from '@libp2p/interface'
7
9
 
8
10
  /**
@@ -13,60 +15,48 @@ export class IpnsRecordPlugin extends BasePlugin {
13
15
  readonly id = 'ipns-record-plugin'
14
16
  readonly codes = []
15
17
 
16
- canHandle ({ accept, query, byteRangeContext }: PluginContext): boolean {
17
- if (byteRangeContext == null) {
18
- return false
19
- }
20
-
21
- return accept?.mimeType === 'application/vnd.ipfs.ipns-record' || query.format === 'ipns-record'
18
+ canHandle ({ accept }: PluginContext): boolean {
19
+ return accept.some(header => header.contentType.mediaType === MEDIA_TYPE_IPNS_RECORD)
22
20
  }
23
21
 
24
- async handle (context: PluginContext & Required<Pick<PluginContext, 'byteRangeContext'>>): Promise<Response> {
25
- const { resource, path, query, options } = context
22
+ async handle (context: Pick<PluginContext, 'resource' | 'url' | 'options' | 'range'>): Promise<Response> {
23
+ const { resource, url, options, range } = context
26
24
  const { ipnsResolver } = this.pluginOptions
27
- context.reqFormat = 'ipns-record'
28
25
 
29
- if (path.length > 0 || !(resource.startsWith('ipns://') || resource.includes('.ipns.') || resource.includes('/ipns/'))) {
30
- this.log.error('invalid request for IPNS name "%s" and path "%s"', resource, path)
26
+ if (url.pathname !== '' || url.protocol !== 'ipns:') {
27
+ this.log.error('invalid request for IPNS name "%s" and path "%s"', resource, url.pathname)
31
28
  return badRequestResponse(resource, new Error('Invalid IPNS name'))
32
29
  }
33
30
 
31
+ if (range != null) {
32
+ return badRequestResponse(resource, new Error('Range requests are not supported for IPNS records'))
33
+ }
34
+
34
35
  let peerId: PeerId
35
36
 
36
37
  try {
37
- let peerIdString: string
38
-
39
- if (resource.startsWith('ipns://')) {
40
- peerIdString = resource.replace('ipns://', '')
41
- } else if (resource.includes('/ipns/')) {
42
- peerIdString = resource.split('/ipns/')[1].split('/')[0].split('?')[0]
43
- } else {
44
- peerIdString = resource.split('.ipns.')[0].split('://')[1]
45
- }
46
-
47
- this.log.trace('trying to parse peer id from "%s"', peerIdString)
48
- peerId = getPeerIdFromString(peerIdString)
38
+ this.log.trace('trying to parse peer id from "%s"', url.hostname)
39
+ peerId = peerIdFromString(url.hostname)
49
40
  } catch (err: any) {
50
41
  this.log.error('could not parse peer id from IPNS url %s', resource, err)
51
42
 
52
43
  return badRequestResponse(resource, err)
53
44
  }
54
45
 
55
- // force download in handleFinalResponse
56
- query.filename = query.filename ?? `${peerId}.bin`
57
- query.download = true
58
-
59
- // @ts-expect-error progress handler types are incompatible
60
46
  const result = await ipnsResolver.resolve(peerId, options)
61
- const buf = marshalIPNSRecord(result.record)
62
-
63
- context.byteRangeContext.setBody(buf)
64
-
65
- const response = okRangeResponse(resource, context.byteRangeContext.getBody('application/vnd.ipfs.ipns-record'), { byteRangeContext: context.byteRangeContext, log: this.log })
66
- response.headers.set('content-type', context.byteRangeContext.getContentType() ?? 'application/vnd.ipfs.ipns-record')
67
- response.headers.set('content-length', buf.byteLength.toString())
68
- response.headers.set('x-ipfs-roots', result.cid.toV1().toString())
69
-
70
- return response
47
+ const block = marshalIPNSRecord(result.record)
48
+
49
+ return okResponse(resource, block, {
50
+ headers: {
51
+ 'content-length': `${block.byteLength}`,
52
+ 'content-type': CONTENT_TYPE_IPNS.mediaType,
53
+ 'content-disposition': `attachment; ${
54
+ getContentDispositionFilename(url.searchParams.get('filename') ?? `${peerId}${CONTENT_TYPE_IPNS.extension}`)
55
+ }`,
56
+ 'x-ipfs-roots': result.cid.toV1().toString(),
57
+ 'cache-control': `public, max-age=${Number((result.record.ttl ?? 0n) / BigInt(1e9))}`,
58
+ 'accept-ranges': 'none'
59
+ }
60
+ })
71
61
  }
72
62
  }
@@ -1,11 +1,13 @@
1
+ import { NotUnixFSError } from '@helia/unixfs/errors'
1
2
  import { code as dagPbCode } from '@ipld/dag-pb'
2
3
  import toBrowserReadableStream from 'it-to-browser-readablestream'
3
4
  import { code as rawCode } from 'multiformats/codecs/raw'
4
- import { getETag } from '../utils/get-e-tag.js'
5
+ import { MEDIA_TYPE_TAR } from '../utils/content-types.ts'
6
+ import { getContentDispositionFilename } from '../utils/get-content-disposition-filename.ts'
5
7
  import { tarStream } from '../utils/get-tar-stream.js'
6
- import { notAcceptableResponse, okRangeResponse } from '../utils/responses.js'
8
+ import { badRequestResponse, okResponse } from '../utils/responses.js'
7
9
  import { BasePlugin } from './plugin-base.js'
8
- import type { PluginContext } from './types.js'
10
+ import type { PluginContext } from '../index.js'
9
11
 
10
12
  /**
11
13
  * Accepts a UnixFS `CID` and returns a `.tar` file containing the file or
@@ -15,37 +17,31 @@ export class TarPlugin extends BasePlugin {
15
17
  readonly id = 'tar-plugin'
16
18
  readonly codes = []
17
19
 
18
- canHandle ({ cid, accept, query, byteRangeContext }: PluginContext): boolean {
19
- if (byteRangeContext == null) {
20
- return false
21
- }
22
-
23
- return accept?.mimeType === 'application/x-tar' || query.format === 'tar'
20
+ canHandle ({ accept }: PluginContext): boolean {
21
+ return accept.some(header => header.contentType.mediaType === MEDIA_TYPE_TAR)
24
22
  }
25
23
 
26
- async handle (context: PluginContext & Required<Pick<PluginContext, 'byteRangeContext'>>): Promise<Response> {
27
- const { cid, path, resource, options, pathDetails } = context
28
- const { getBlockstore } = this.pluginOptions
24
+ async handle (context: PluginContext): Promise<Response> {
25
+ const { terminalElement, url, resource, options, blockstore, range } = context
29
26
 
30
- const terminusElement = pathDetails?.terminalElement.cid ?? cid
31
- if (terminusElement.code !== dagPbCode && terminusElement.code !== rawCode) {
32
- return notAcceptableResponse('only UnixFS data can be returned in a TAR file')
27
+ if (terminalElement.cid.code !== dagPbCode && terminalElement.cid.code !== rawCode) {
28
+ return badRequestResponse(resource, new NotUnixFSError('Only UnixFS data can be returned in a TAR file'))
33
29
  }
34
30
 
35
- context.reqFormat = 'tar'
36
- context.query.download = true
37
- context.query.filename = context.query.filename ?? `${terminusElement.toString()}.tar`
38
-
39
- const blockstore = getBlockstore(terminusElement, resource, options?.session, options)
40
- const stream = toBrowserReadableStream<Uint8Array>(tarStream(`/ipfs/${cid}/${path}`, blockstore, options))
41
-
42
- context.byteRangeContext.setBody(stream)
43
-
44
- const response = okRangeResponse(resource, context.byteRangeContext.getBody('application/x-tar'), { byteRangeContext: context.byteRangeContext, log: this.log })
45
- response.headers.set('content-type', context.byteRangeContext.getContentType() ?? 'application/x-tar')
46
-
47
- response.headers.set('etag', getETag({ cid: terminusElement, reqFormat: context.reqFormat, weak: true }))
31
+ if (range != null) {
32
+ return badRequestResponse(resource, new Error('Range requests are not supported for TAR files'))
33
+ }
48
34
 
49
- return response
35
+ const stream = toBrowserReadableStream<Uint8Array>(tarStream(`/ipfs/${terminalElement.cid}${url.pathname}`, blockstore, options))
36
+
37
+ return okResponse(resource, stream, {
38
+ headers: {
39
+ 'content-type': MEDIA_TYPE_TAR,
40
+ 'content-disposition': `attachment; ${
41
+ getContentDispositionFilename(url.searchParams.get('filename') ?? `${terminalElement.cid.toString()}.tar`)
42
+ }`,
43
+ 'accept-ranges': 'none'
44
+ }
45
+ })
50
46
  }
51
47
  }