@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,248 +0,0 @@
1
- import { unixfs } from '@helia/unixfs'
2
- import { code as dagPbCode } from '@ipld/dag-pb'
3
- import { AbortError } from '@libp2p/interface'
4
- import { exporter } from 'ipfs-unixfs-exporter'
5
- import { CustomProgressEvent } from 'progress-events'
6
- import { getContentType } from '../utils/get-content-type.js'
7
- import { getStreamFromAsyncIterable } from '../utils/get-stream-from-async-iterable.js'
8
- import { setIpfsRoots } from '../utils/response-headers.js'
9
- import { badGatewayResponse, badRangeResponse, movedPermanentlyResponse, okRangeResponse } from '../utils/responses.js'
10
- import { BasePlugin } from './plugin-base.js'
11
- import type { PluginContext } from './types.js'
12
- import type { CIDDetail } from '../index.js'
13
- import type { UnixFSEntry } from 'ipfs-unixfs-exporter'
14
-
15
- /**
16
- * Handles UnixFS and dag-pb content.
17
- */
18
- export class DagPbPlugin extends BasePlugin {
19
- readonly id = 'dag-pb-plugin'
20
- readonly codes = [dagPbCode]
21
-
22
- canHandle ({ cid, accept, pathDetails, byteRangeContext }: PluginContext): boolean {
23
- if (pathDetails == null) {
24
- return false
25
- }
26
-
27
- if (byteRangeContext == null) {
28
- return false
29
- }
30
-
31
- // TODO: this may be too restrictive?
32
- if (accept != null && accept.mimeType !== 'application/octet-stream') {
33
- return false
34
- }
35
-
36
- return cid.code === dagPbCode
37
- }
38
-
39
- /**
40
- * @see https://specs.ipfs.tech/http-gateways/path-gateway/#use-in-directory-url-normalization
41
- */
42
- getRedirectUrl (context: PluginContext): string | null {
43
- const { resource, url, isDirectory } = context
44
-
45
- let uri: URL
46
-
47
- try {
48
- // try the requested resource
49
- uri = new URL(resource)
50
- } catch {
51
- // fall back to the canonical URL
52
- uri = url
53
- }
54
-
55
- // directories must be requested with a trailing slash
56
- if (isDirectory && !uri.pathname.endsWith('/')) {
57
- // make sure we append slash to end of the path
58
- uri.pathname += '/'
59
- return uri.toString()
60
- }
61
-
62
- return null
63
- }
64
-
65
- async handle (context: PluginContext & Required<Pick<PluginContext, 'byteRangeContext' | 'pathDetails'>>): Promise<Response | null> {
66
- const { cid, options, pathDetails, query } = context
67
- const { contentTypeParser, helia, getBlockstore } = this.pluginOptions
68
- const log = this.log
69
- let resource = context.resource
70
- const path = context.path
71
- let filename = query?.filename
72
-
73
- let redirected = false
74
-
75
- const byteRangeContext = context.byteRangeContext
76
- const ipfsRoots = pathDetails.ipfsRoots
77
- const terminalElement = pathDetails.terminalElement
78
- let resolvedCID = terminalElement.cid
79
- const fs = unixfs({ ...helia, blockstore: getBlockstore(context.cid, context.resource, options?.session ?? true, options) })
80
-
81
- context.isDirectory = terminalElement?.type === 'directory'
82
-
83
- if (terminalElement?.type === 'directory') {
84
- const redirectUrl = this.getRedirectUrl(context)
85
-
86
- if (redirectUrl != null) {
87
- log.trace('directory url normalization spec requires redirect...')
88
- if (options?.redirect === 'error') {
89
- log('could not redirect to %s as redirect option was set to "error"', redirectUrl)
90
- throw new TypeError('Failed to fetch')
91
- } else if (options?.redirect === 'manual') {
92
- log('returning 301 permanent redirect to %s', redirectUrl)
93
- return movedPermanentlyResponse(resource, redirectUrl)
94
- }
95
- log('following redirect to %s', redirectUrl)
96
-
97
- // fall-through simulates following the redirect?
98
- resource = redirectUrl
99
- redirected = true
100
- }
101
-
102
- const dirCid = terminalElement.cid
103
- const rootFilePath = 'index.html'
104
-
105
- try {
106
- log.trace('found directory at %c/%s, looking for index.html', cid, path)
107
-
108
- const entry = await context.serverTiming.time('exporter-dir', '', exporter(`/ipfs/${dirCid}/${rootFilePath}`, helia.blockstore, {
109
- signal: options?.signal,
110
- onProgress: options?.onProgress
111
- }))
112
-
113
- log.trace('found root file at %c/%s with cid %c', dirCid, rootFilePath, entry.cid)
114
- resolvedCID = entry.cid
115
-
116
- // use `index.html` as the file name to help with content types
117
- filename = rootFilePath
118
- } catch (err: any) {
119
- if (options?.signal?.aborted) {
120
- throw new AbortError(options?.signal?.reason)
121
- }
122
-
123
- this.log.error('error loading path %c/%s - %e', dirCid, rootFilePath, err)
124
-
125
- context.isDirectory = true
126
- context.directoryEntries = []
127
- context.modified++
128
-
129
- this.log.trace('attempting to get directory entries because index.html was not found')
130
- for await (const dirItem of fs.ls(dirCid, { signal: options?.signal, onProgress: options?.onProgress, extended: false })) {
131
- context.directoryEntries.push(dirItem)
132
- }
133
-
134
- // dir-index-html plugin or dir-index-json (future idea?) plugin should handle this
135
- return null
136
- } finally {
137
- options?.onProgress?.(new CustomProgressEvent<CIDDetail>('verified-fetch:request:end', { cid: dirCid, path: [rootFilePath] }))
138
- }
139
- }
140
-
141
- try {
142
- // attempt to get the exact file size, but timeout quickly.
143
- const stat = await fs.stat(resolvedCID, {
144
- extended: true,
145
- signal: AbortSignal.timeout(500)
146
- })
147
-
148
- byteRangeContext.setFileSize(stat.size)
149
- } catch (err: any) {
150
- log.error('error getting exact file size for %c/%s - %e', cid, path, err)
151
- byteRangeContext.setFileSize(pathDetails.terminalElement.size)
152
- log.trace('using terminal element size of %d for %c/%s', pathDetails.terminalElement.size, cid, path)
153
- }
154
-
155
- try {
156
- const entry = await context.serverTiming.time('exporter-file', '', exporter(resolvedCID, helia.blockstore, {
157
- signal: options?.signal,
158
- onProgress: options?.onProgress
159
- }))
160
-
161
- let firstChunk: Uint8Array
162
- let contentType: string
163
- if (byteRangeContext.isValidRangeRequest) {
164
- contentType = await this.handleRangeRequest(context, entry)
165
- } else {
166
- const asyncIter = entry.content({
167
- signal: options?.signal,
168
- onProgress: options?.onProgress
169
- })
170
- log('got async iterator for %c/%s', cid, path)
171
-
172
- const streamAndFirstChunk = await context.serverTiming.time('stream-and-chunk', '', getStreamFromAsyncIterable(asyncIter, {
173
- onProgress: options?.onProgress,
174
- signal: options?.signal
175
- }))
176
- const stream = streamAndFirstChunk.stream
177
- firstChunk = streamAndFirstChunk.firstChunk
178
- contentType = await context.serverTiming.time('get-content-type', '', getContentType({
179
- path,
180
- filename,
181
- bytes: firstChunk,
182
- contentTypeParser,
183
- log
184
- }))
185
-
186
- byteRangeContext.setBody(stream)
187
- }
188
-
189
- // if not a valid range request, okRangeRequest will call okResponse
190
- const response = okRangeResponse(resource, byteRangeContext.getBody(contentType), { byteRangeContext, log }, {
191
- redirected
192
- })
193
-
194
- response.headers.set('Content-Type', byteRangeContext.getContentType() ?? contentType)
195
-
196
- setIpfsRoots(response, ipfsRoots)
197
-
198
- return response
199
- } catch (err: any) {
200
- if (options?.signal?.aborted) {
201
- throw new AbortError(options?.signal?.reason)
202
- }
203
-
204
- log.error('error streaming %c/%s - %e', cid, path, err)
205
-
206
- if (byteRangeContext.isRangeRequest && err.code === 'ERR_INVALID_PARAMS') {
207
- return badRangeResponse(resource)
208
- }
209
-
210
- return badGatewayResponse(resource, 'Unable to stream content')
211
- }
212
- }
213
-
214
- private async handleRangeRequest (context: PluginContext & Required<Pick<PluginContext, 'byteRangeContext' | 'pathDetails'>>, entry: UnixFSEntry): Promise<string> {
215
- const { path, byteRangeContext, options } = context
216
- const { contentTypeParser } = this.pluginOptions
217
- const log = this.log
218
-
219
- // get the first chunk in order to determine the content type
220
- const asyncIter = entry.content({
221
- signal: options?.signal,
222
- onProgress: options?.onProgress,
223
- offset: 0,
224
- // 8kb in order to determine the content type
225
- length: 8192
226
- })
227
-
228
- const { firstChunk } = await getStreamFromAsyncIterable(asyncIter, {
229
- onProgress: options?.onProgress,
230
- signal: options?.signal
231
- })
232
- const contentType = await context.serverTiming.time('get-content-type', '', getContentType({ bytes: firstChunk, path, contentTypeParser, log }))
233
-
234
- byteRangeContext?.setBody((range): AsyncGenerator<Uint8Array, void, unknown> => {
235
- if (options?.signal?.aborted) {
236
- throw new AbortError(options?.signal?.reason ?? 'aborted while streaming')
237
- }
238
- return entry.content({
239
- signal: options?.signal,
240
- onProgress: options?.onProgress,
241
- offset: range.start ?? 0,
242
- length: byteRangeContext.getLength(range)
243
- })
244
- }, contentType)
245
-
246
- return contentType
247
- }
248
- }
@@ -1,110 +0,0 @@
1
- import * as dagCbor from '@ipld/dag-cbor'
2
- import * as dagJson 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 { CODEC_CBOR, CODEC_IDENTITY } from '../constants.ts'
8
- import { handlePathWalking } from '../utils/walk-path.js'
9
- import { BasePlugin } from './plugin-base.js'
10
- import type { PluginContext } from './types.js'
11
- import type { PathWalkerResponse } from '../utils/walk-path.js'
12
-
13
- const ENTITY_CODECS = [
14
- CODEC_CBOR,
15
- json.code,
16
- raw.code
17
- ]
18
-
19
- const WALKABLE_CODECS = [
20
- dagPb.code,
21
- dagCbor.code,
22
- dagJson.code,
23
- ...ENTITY_CODECS
24
- ]
25
-
26
- /**
27
- * This plugin should almost always run first because it's going to handle path
28
- * walking if needed, and will only say it can handle the request if path
29
- * walking is possible (path is not empty, terminalCid is unknown, and the path
30
- * has not been walked yet).
31
- *
32
- * Once this plugin has run, the PluginContext will be updated and then this
33
- * plugin will return false for canHandle, so it won't run again.
34
- */
35
- export class DagWalkPlugin extends BasePlugin {
36
- readonly id = 'dag-walk-plugin'
37
-
38
- /**
39
- * Return false if the path has already been walked, otherwise return true if
40
- * the CID is encoded with a codec that supports pathing.
41
- */
42
- canHandle (context: PluginContext): boolean {
43
- const { pathDetails, cid } = context
44
-
45
- if (pathDetails != null) {
46
- // path has already been walked
47
- return false
48
- }
49
-
50
- return WALKABLE_CODECS.includes(cid.code)
51
- }
52
-
53
- async handle (context: PluginContext): Promise<Response | null> {
54
- const { cid, resource, options } = context
55
- const { getBlockstore } = this.pluginOptions
56
- const blockstore = getBlockstore(cid, resource, options?.session ?? true, options)
57
-
58
- let pathDetails: PathWalkerResponse | Response
59
-
60
- // entity codecs contain all the bytes for an entity in one block and no
61
- // path walking outside of that block is possible
62
- if (ENTITY_CODECS.includes(cid.code)) {
63
- let bytes: Uint8Array
64
-
65
- if (cid.multihash.code === CODEC_IDENTITY) {
66
- bytes = cid.multihash.digest
67
- } else {
68
- bytes = await toBuffer(blockstore.get(cid, context.options))
69
- }
70
-
71
- pathDetails = {
72
- ipfsRoots: [cid],
73
- terminalElement: {
74
- name: cid.toString(),
75
- path: cid.toString(),
76
- depth: 0,
77
- type: 'object',
78
- node: bytes,
79
- cid,
80
- size: BigInt(bytes.byteLength),
81
- content: async function * () {
82
- yield bytes
83
- }
84
- }
85
- }
86
- } else {
87
- // TODO: migrate handlePathWalking into this plugin
88
- pathDetails = await context.serverTiming.time('path-walking', '', handlePathWalking({ ...context, blockstore, log: this.log }))
89
- }
90
-
91
- if (pathDetails instanceof Response) {
92
- this.log.trace('path walking failed')
93
-
94
- if (pathDetails.status === 404) {
95
- // invalid or incorrect path.. we walked the path but nothing is there
96
- // send the 404 response
97
- return pathDetails
98
- }
99
-
100
- // some other error walking the path (codec doesn't support pathing,
101
- // etc..), let the next plugin try to handle it
102
- return null
103
- }
104
-
105
- context.modified++
106
- context.pathDetails = pathDetails
107
-
108
- return null
109
- }
110
- }
@@ -1,72 +0,0 @@
1
- import { code as dagPbCode } from '@ipld/dag-pb'
2
- import { base32 } from 'multiformats/bases/base32'
3
- import { sha256 } from 'multiformats/hashes/sha2'
4
- import { dirIndexHtml } from '../utils/dir-index-html.js'
5
- import { getETag } from '../utils/get-e-tag.js'
6
- import { getIpfsRoots } from '../utils/response-headers.js'
7
- import { okRangeResponse } from '../utils/responses.js'
8
- import { BasePlugin } from './plugin-base.js'
9
- import type { PluginContext, VerifiedFetchPluginFactory } from './types.js'
10
- import type { UnixFSEntry } from 'ipfs-unixfs-exporter'
11
-
12
- /**
13
- * Converts a list of directory entries into a small hash that can be used in the etag header.
14
- *
15
- * @see https://github.com/ipfs/boxo/blob/dc60fe747c375c631a92fcfd6c7456f44a760d24/gateway/assets/assets.go#L84
16
- * @see https://github.com/ipfs/boxo/blob/dc60fe747c375c631a92fcfd6c7456f44a760d24/gateway/handler_unixfs_dir.go#L233-L235
17
- */
18
- async function getAssetHash (directoryEntries: UnixFSEntry[]): Promise<string> {
19
- const entryDetails = directoryEntries.reduce((acc, entry) => {
20
- return `${acc}${entry.name}${entry.cid.toString()}`
21
- }, '')
22
- const hashBytes = await sha256.encode(new TextEncoder().encode(entryDetails))
23
- return base32.encode(hashBytes)
24
- }
25
-
26
- export class DirIndexHtmlPlugin extends BasePlugin {
27
- readonly id = 'dir-index-html-plugin'
28
- readonly codes = [dagPbCode]
29
- canHandle (context: PluginContext): boolean {
30
- const { cid, pathDetails, directoryEntries } = context
31
- if (pathDetails == null) {
32
- return false
33
- }
34
- if (pathDetails.terminalElement?.type !== 'directory') {
35
- return false
36
- }
37
-
38
- if (directoryEntries == null || directoryEntries.length === 0) {
39
- return false
40
- }
41
-
42
- return cid.code === dagPbCode
43
- }
44
-
45
- async handle (context: PluginContext & Required<Pick<PluginContext, 'byteRangeContext' | 'pathDetails' | 'directoryEntries'>>): Promise<Response> {
46
- const { resource, pathDetails, directoryEntries } = context
47
-
48
- const { terminalElement, ipfsRoots } = pathDetails
49
-
50
- const gatewayURL = resource
51
- const htmlResponse = dirIndexHtml(terminalElement, directoryEntries, { gatewayURL, log: this.log })
52
-
53
- context.byteRangeContext.setBody(htmlResponse)
54
-
55
- const etagPrefix = `DirIndex-${await getAssetHash(directoryEntries)}_CID-`
56
-
57
- const response = okRangeResponse(resource, context.byteRangeContext.getBody('text/html'), { byteRangeContext: context.byteRangeContext, log: this.log }, {
58
- headers: {
59
- 'Content-Type': context.byteRangeContext.getContentType() ?? 'text/html',
60
- // see https://github.com/ipfs/gateway-conformance/pull/219
61
- 'Cache-Control': 'public, max-age=604800, stale-while-revalidate=2678400',
62
- 'X-Ipfs-Roots': getIpfsRoots(ipfsRoots),
63
- // e.g. DirIndex-<asset_hash>_CID-<cid>
64
- Etag: getETag({ cid: terminalElement.cid, reqFormat: context.reqFormat, contentPrefix: etagPrefix })
65
- }
66
- })
67
-
68
- return response
69
- }
70
- }
71
-
72
- export const dirIndexHtmlPluginFactory: VerifiedFetchPluginFactory = (opts) => new DirIndexHtmlPlugin(opts)
@@ -1,80 +0,0 @@
1
- import * as ipldDagCbor from '@ipld/dag-cbor'
2
- import * as ipldDagJson from '@ipld/dag-json'
3
- import toBuffer from 'it-to-buffer'
4
- import { code as jsonCode } from 'multiformats/codecs/json'
5
- import { CODEC_CBOR } from '../constants.ts'
6
- import { notAcceptableResponse, okRangeResponse } from '../utils/responses.js'
7
- import { BasePlugin } from './plugin-base.js'
8
- import type { PluginContext } from './types.js'
9
-
10
- /**
11
- * Handles `dag-json` content, including requests with Accept: `application/vnd.ipld.dag-cbor` and `application/cbor`.
12
- */
13
- export class JsonPlugin extends BasePlugin {
14
- readonly id = 'json-plugin'
15
- readonly codes = [ipldDagJson.code, jsonCode]
16
-
17
- canHandle ({ cid, accept, byteRangeContext }: PluginContext): boolean {
18
- if (byteRangeContext == null) {
19
- return false
20
- }
21
-
22
- if (accept?.mimeType === 'application/vnd.ipld.dag-json' && cid.code !== ipldDagCbor.code && cid.code !== CODEC_CBOR) {
23
- // we can handle application/vnd.ipld.dag-json, but if the CID codec is
24
- // cbor related, cbor related plugins will handle it
25
- // TODO: remove the need for deny-listing cases in plugins
26
- return true
27
- }
28
-
29
- return ipldDagJson.code === cid.code || jsonCode === cid.code
30
- }
31
-
32
- async handle (context: PluginContext & Required<Pick<PluginContext, 'byteRangeContext'>>): Promise<Response> {
33
- const { path, resource, cid, accept, options } = context
34
- const { getBlockstore } = this.pluginOptions
35
- const session = options?.session ?? true
36
-
37
- this.log.trace('fetching %c/%s', cid, path)
38
-
39
- const terminalCid = context.pathDetails?.terminalElement.cid ?? context.cid
40
- const blockstore = getBlockstore(terminalCid, resource, session, options)
41
- const block = await toBuffer(blockstore.get(terminalCid, options))
42
- let body: string | Uint8Array
43
-
44
- if (accept?.mimeType === 'application/vnd.ipld.dag-cbor' || accept?.mimeType === 'application/cbor') {
45
- try {
46
- // if vnd.ipld.dag-cbor has been specified, convert to the format - note
47
- // that this supports more data types than regular JSON, the content-type
48
- // response header is set so the user knows to process it differently
49
- const obj = ipldDagJson.decode(block)
50
- body = ipldDagCbor.encode(obj)
51
- } catch (err) {
52
- this.log.error('could not transform %c to application/vnd.ipld.dag-cbor - %e', err)
53
- return notAcceptableResponse(resource)
54
- }
55
- } else {
56
- // skip decoding
57
- body = block
58
- }
59
-
60
- let contentType: string
61
- if (accept == null) {
62
- if (ipldDagJson.code === cid.code) {
63
- contentType = 'application/vnd.ipld.dag-json'
64
- } else {
65
- contentType = 'application/json'
66
- }
67
- } else {
68
- contentType = accept?.mimeType.split(';')[0]
69
- }
70
-
71
- context.byteRangeContext.setBody(body)
72
-
73
- const response = okRangeResponse(resource, context.byteRangeContext.getBody(contentType), { byteRangeContext: context.byteRangeContext, log: this.log })
74
- response.headers.set('content-type', context.byteRangeContext.getContentType() ?? contentType)
75
- if (!context.byteRangeContext.isValidRangeRequest) {
76
- response.headers.set('content-length', body.length.toString())
77
- }
78
- return response
79
- }
80
- }
@@ -1,110 +0,0 @@
1
- import toBuffer from 'it-to-buffer'
2
- import { code as rawCode } from 'multiformats/codecs/raw'
3
- import { identity } from 'multiformats/hashes/identity'
4
- import { getContentType } from '../utils/get-content-type.js'
5
- import { notFoundResponse, okRangeResponse } from '../utils/responses.js'
6
- import { BasePlugin } from './plugin-base.js'
7
- import type { PluginContext } from './types.js'
8
- import type { AcceptHeader } from '../utils/select-output-type.ts'
9
-
10
- /**
11
- * These are Accept header values that will cause content type sniffing to be
12
- * skipped and set to these values.
13
- */
14
- const RAW_HEADERS = [
15
- 'application/vnd.ipld.dag-json',
16
- 'application/vnd.ipld.raw',
17
- 'application/octet-stream'
18
- ]
19
-
20
- /**
21
- * if the user has specified an `Accept` header, and it's in our list of
22
- * allowable "raw" format headers, use that instead of detecting the content
23
- * type. This avoids the user from receiving something different when they
24
- * signal that they want to `Accept` a specific mime type.
25
- */
26
- function getOverriddenRawContentType ({ headers, accept }: { headers?: HeadersInit, accept?: AcceptHeader }): string | undefined {
27
- // accept has already been resolved by getResolvedAcceptHeader, if we have it, use it.
28
- const acceptHeader = accept?.mimeType ?? new Headers(headers).get('accept') ?? ''
29
-
30
- // e.g. "Accept: text/html, application/xhtml+xml, application/xml;q=0.9, image/webp, */*;q=0.8"
31
- const acceptHeaders = acceptHeader.split(',')
32
- .map(s => s.split(';')[0])
33
- .map(s => s.trim())
34
-
35
- for (const mimeType of acceptHeaders) {
36
- if (mimeType === '*/*') {
37
- return
38
- }
39
-
40
- if (RAW_HEADERS.includes(mimeType ?? '')) {
41
- return mimeType
42
- }
43
- }
44
- }
45
-
46
- export class RawPlugin extends BasePlugin {
47
- readonly id = 'raw-plugin'
48
- codes: number[] = [rawCode, identity.code]
49
-
50
- canHandle ({ cid, accept, query, byteRangeContext }: PluginContext): boolean {
51
- if (byteRangeContext == null) {
52
- return false
53
- }
54
-
55
- return accept?.mimeType === 'application/vnd.ipld.raw' || query.format === 'raw'
56
- }
57
-
58
- async handle (context: PluginContext & Required<Pick<PluginContext, 'byteRangeContext'>>): Promise<Response> {
59
- const { path, resource, cid, accept, query, options } = context
60
- const { getBlockstore, contentTypeParser } = this.pluginOptions
61
- const session = options?.session ?? true
62
- const log = this.log
63
-
64
- if (accept?.mimeType === 'application/vnd.ipld.raw' || query.format === 'raw') {
65
- context.reqFormat = 'raw'
66
- context.query.download = true
67
- context.query.filename = context.query.filename ?? `${cid.toString()}.bin`
68
- log.trace('set content disposition to force download')
69
- } else {
70
- log.trace('did not set content disposition, raw block will display inline')
71
- }
72
-
73
- if (path.length > 0 && cid.code === rawCode) {
74
- log.trace('404-ing raw codec request for %c/%s', cid, path)
75
- return notFoundResponse(resource)
76
- }
77
-
78
- const terminalCid = context.pathDetails?.terminalElement.cid ?? context.cid
79
- const blockstore = getBlockstore(terminalCid, resource, session, options)
80
- const result = await toBuffer(blockstore.get(terminalCid, options))
81
-
82
- context.byteRangeContext.setBody(result)
83
-
84
- // if the user has specified an `Accept` header that corresponds to a raw
85
- // type, honour that header, so for example they don't request
86
- // `application/vnd.ipld.raw` but get `application/octet-stream`
87
- const contentType = await getContentType({
88
- filename: query.filename,
89
- bytes: result,
90
- path,
91
- defaultContentType: getOverriddenRawContentType({ headers: options?.headers, accept }),
92
- contentTypeParser,
93
- log
94
- })
95
-
96
- const response = okRangeResponse(resource, context.byteRangeContext.getBody(contentType), { byteRangeContext: context.byteRangeContext, log }, {
97
- redirected: false
98
- })
99
-
100
- response.headers.set('content-type', context.byteRangeContext.getContentType() ?? contentType)
101
- response.headers.set('x-ipfs-roots', terminalCid.toV1().toString())
102
-
103
- // only set content-length if it is not a range request
104
- if (!context.byteRangeContext.isRangeRequest) {
105
- response.headers.set('content-length', result.byteLength.toString())
106
- }
107
-
108
- return response
109
- }
110
- }
@@ -1,5 +0,0 @@
1
- /**
2
- * Export extension (non-default) plugins here
3
- */
4
- export { DirIndexHtmlPlugin, dirIndexHtmlPluginFactory } from './plugin-handle-dir-index-html.js'
5
- export { DagCborHtmlPreviewPlugin, dagCborHtmlPreviewPluginFactory } from './plugin-handle-dag-cbor-html-preview.js'