@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,597 +0,0 @@
1
- import toBrowserReadableStream from 'it-to-browser-readablestream'
2
- import { InvalidRangeError } from '../errors.js'
3
- import { calculateByteRangeIndexes, getHeader } from './request-headers.js'
4
- import { getContentRangeHeader } from './response-headers.js'
5
- import type { SupportedBodyTypes } from '../index.js'
6
- import type { Logger } from '@libp2p/interface'
7
-
8
- type SliceableBody = Exclude<SupportedBodyTypes, ReadableStream<Uint8Array> | null>
9
-
10
- interface RequestRange {
11
- start: number | undefined
12
- end: number | undefined
13
- }
14
-
15
- interface ByteRange extends RequestRange {
16
- size: number | undefined
17
- }
18
-
19
- /**
20
- * Gets the body size of a given body if it's possible to calculate it synchronously.
21
- */
22
- function getBodySizeSync (body: SupportedBodyTypes): number | null {
23
- if (typeof body === 'string') {
24
- return body.length
25
- }
26
- if (body instanceof ArrayBuffer || body instanceof Uint8Array) {
27
- return body.byteLength
28
- }
29
- if (body instanceof Blob) {
30
- return body.size
31
- }
32
-
33
- if (body instanceof ReadableStream) {
34
- return null
35
- }
36
-
37
- return null
38
- }
39
-
40
- function getByteRangeFromHeader (rangeHeader: string): { ranges: Array<{ start: string | null, end: string | null }> } {
41
- /**
42
- * Range: bytes=<start>-<end> | bytes=<start2>- | bytes=-<end2> | bytes=<start1>-<end1>,<start2>-<end2>,...
43
- */
44
- if (!rangeHeader.startsWith('bytes=')) {
45
- throw new InvalidRangeError('Invalid range request')
46
- }
47
-
48
- const rangesStr = rangeHeader.substring(6) // Remove "bytes=" prefix
49
- const rangeParts = rangesStr.split(',').map(part => part.trim())
50
- const ranges: Array<{ start: string | null, end: string | null }> = []
51
-
52
- for (const part of rangeParts) {
53
- const match = part.match(/^(?<start>\d+)?-(?<end>\d+)?$/)
54
- if (match?.groups == null) {
55
- throw new InvalidRangeError(`Invalid range specification: ${part}`)
56
- }
57
-
58
- const { start, end } = match.groups
59
- ranges.push({
60
- start: start ?? null,
61
- end: end ?? null
62
- })
63
- }
64
-
65
- if (ranges.length === 0) {
66
- throw new InvalidRangeError('No valid ranges found')
67
- }
68
-
69
- return { ranges }
70
- }
71
-
72
- export class ByteRangeContext {
73
- public readonly isRangeRequest: boolean
74
-
75
- /**
76
- * This property is purposefully only set in `set fileSize` and should not be set directly.
77
- */
78
- private _fileSize: number | null | undefined
79
- private _body: SupportedBodyTypes = null
80
- private readonly rangeRequestHeader: string | undefined
81
- private readonly log: Logger
82
- /**
83
- * multiPartBoundary is required for multipart responses
84
- */
85
- private readonly multiPartBoundary?: string
86
- private readonly requestRanges: Array<{ start: number | null, end: number | null }> = []
87
- private byteRanges: ByteRange[] = []
88
- readonly isMultiRangeRequest: boolean = false
89
-
90
- // to be set by isValidRangeRequest so that we don't need to re-check the byteRanges
91
- private _isValidRangeRequest: boolean = false
92
-
93
- constructor (logger: Logger, private readonly headers?: HeadersInit) {
94
- this.log = logger.newScope('byte-range-context')
95
- this.rangeRequestHeader = getHeader(this.headers, 'Range')
96
-
97
- if (this.rangeRequestHeader != null) {
98
- this.isRangeRequest = true
99
- this.log.trace('range request detected')
100
-
101
- try {
102
- const { ranges } = getByteRangeFromHeader(this.rangeRequestHeader)
103
- this.isMultiRangeRequest = ranges.length > 1
104
-
105
- this.requestRanges = ranges.map(range => ({
106
- start: range.start != null ? parseInt(range.start) : null,
107
- end: range.end != null ? parseInt(range.end) : null
108
- }))
109
-
110
- this.multiPartBoundary = `multipart_byteranges_${Math.floor(Math.random() * 1000000000)}`
111
- } catch (e) {
112
- this.log.error('error parsing range request header - %e', e)
113
- this.requestRanges = []
114
- }
115
-
116
- this.setOffsetDetails()
117
- } else {
118
- this.log.trace('no range request detected')
119
- this.isRangeRequest = false
120
- }
121
- }
122
-
123
- public getByteRanges (): ByteRange[] {
124
- return this.byteRanges
125
- }
126
-
127
- /**
128
- * You can pass a function when you need to support multi-range requests but have your own slicing logic, such as in the case of dag-pb/unixfs.
129
- *
130
- * @param bodyOrProvider - A supported body type or a function that returns a supported body type.
131
- * @param contentType - The content type of the body.
132
- */
133
- public setBody (
134
- bodyOrProvider: SupportedBodyTypes | ((range: ByteRange) => AsyncGenerator<Uint8Array, void, unknown>),
135
- contentType: string = 'application/octet-stream'
136
- ): void {
137
- if (typeof bodyOrProvider === 'function') {
138
- this._body = this.createRangeStream(bodyOrProvider, contentType)
139
- } else {
140
- this._body = bodyOrProvider
141
-
142
- // if fileSize was already set, don't recalculate it
143
- this.setFileSize(this._fileSize ?? getBodySizeSync(bodyOrProvider))
144
- }
145
-
146
- this.log.trace('set request body with fileSize %o', this._fileSize)
147
- }
148
-
149
- public getBody (responseContentType?: string): SupportedBodyTypes {
150
- const body = this._body
151
- if (body == null) {
152
- this.log.trace('body is null')
153
- return body
154
- }
155
-
156
- if (!this.isRangeRequest || !this.isValidRangeRequest) {
157
- this.log.trace('returning body unmodified for non-range, or invalid range, request')
158
- return body
159
- }
160
-
161
- if (this.isMultiRangeRequest) {
162
- if (this._body instanceof ReadableStream) {
163
- return this._body
164
- }
165
- return toBrowserReadableStream(this.getMultipartBody(responseContentType))
166
- }
167
-
168
- // Single range request handling
169
- if (this.byteRanges.length > 0) {
170
- const range = this.byteRanges[0]
171
- if (body instanceof ReadableStream) {
172
- // stream should already be spliced by `unixfs.cat`
173
- // TODO: if the content is not unixfs and unixfs.cat was not called, we need to slice the body here.
174
- return body
175
- }
176
- if (range.start != null || range.end != null) {
177
- this.log.trace('returning body with byteStart=%o, byteEnd=%o, byteSize=%o', range.start, range.end, range.size)
178
- }
179
- return this.getSlicedBody(body, range)
180
- }
181
-
182
- // we should not reach this point, but return body untouched.
183
- this.log.error('returning unmodified body for valid range request')
184
- return body
185
- }
186
-
187
- private getSlicedBody <T extends SliceableBody>(body: T, range: ByteRange): SliceableBody {
188
- const offset = range.start ?? 0
189
-
190
- // Calculate the correct number of bytes to return
191
- // For a range like bytes=1000-2000, we want exactly 1001 bytes
192
- let length: number | undefined
193
-
194
- if (range.end != null && range.start != null) {
195
- // Exact number of bytes is (end - start + 1) due to inclusive ranges
196
- length = range.end - range.start + 1
197
- } else {
198
- length = undefined
199
- }
200
-
201
- this.log.trace('slicing body with offset=%o and length=%o', offset, length)
202
-
203
- if (typeof body === 'string') {
204
- // String slicing works with start and end indices
205
- return body.slice(offset, length !== undefined ? offset + length : undefined) satisfies SliceableBody
206
- } else if (body instanceof Blob) {
207
- // Blob.slice takes start and end positions
208
- return body.slice(offset, length !== undefined ? offset + length : undefined) satisfies SliceableBody
209
- } else if (body instanceof ArrayBuffer || body instanceof Uint8Array) {
210
- // ArrayBuffer.slice and Uint8Array.slice take start and end positions
211
- return body.slice(offset, length !== undefined ? offset + length : undefined) satisfies SliceableBody
212
- }
213
-
214
- // This should never happen due to type constraints
215
- return body satisfies SliceableBody
216
- }
217
-
218
- /**
219
- * Sometimes, we need to set the fileSize explicitly because we can't calculate
220
- * the size of the body (e.g. for unixfs content where we call .stat).
221
- *
222
- * This fileSize should otherwise only be called from `setBody`.
223
- */
224
- public setFileSize (size: number | bigint | null): void {
225
- this._fileSize = size != null ? Number(size) : null
226
- this._isValidRangeRequest = false // body has changed, so we need to re-validate the byte ranges
227
- this.log.trace('set _fileSize to %o', this._fileSize)
228
- // when fileSize changes, we need to recalculate the offset details
229
- this.setOffsetDetails()
230
- }
231
-
232
- public getFileSize (): number | null | undefined {
233
- return this._fileSize
234
- }
235
-
236
- private isValidByteStart (byteStart: number | undefined, byteEnd: number | undefined): boolean {
237
- if (byteStart != null) {
238
- if (byteStart < 0) {
239
- return false
240
- }
241
- if (this._fileSize != null && byteStart >= this._fileSize) {
242
- return false
243
- }
244
- if (byteEnd != null && byteStart > byteEnd) {
245
- return false
246
- }
247
- }
248
- return true
249
- }
250
-
251
- private isValidByteEnd (byteStart: number | undefined, byteEnd: number | undefined): boolean {
252
- if (byteEnd != null) {
253
- if (byteEnd < 0) {
254
- this.log.trace('invalid range request, byteEnd is less than 0')
255
- return false
256
- }
257
- if (this._fileSize != null && byteEnd >= this._fileSize) {
258
- this.log.trace('invalid range request, byteEnd is greater than fileSize')
259
- return false
260
- }
261
- if (byteStart != null && byteEnd < byteStart) {
262
- this.log.trace('invalid range request, byteEnd is less than byteStart')
263
- return false
264
- }
265
- }
266
- return true
267
- }
268
-
269
- private isValidByteRange (range: ByteRange): boolean {
270
- this.log.trace('validating byte range: %o', range)
271
- if (range.start != null && !this.isValidByteStart(range.start, range.end)) {
272
- this.log.trace('invalid range request, byteStart is less than 0 or greater than fileSize')
273
- return false
274
- }
275
- if (range.end != null && !this.isValidByteEnd(range.start, range.end)) {
276
- this.log.trace('invalid range request, byteEnd is less than 0 or greater than fileSize')
277
- return false
278
- }
279
-
280
- return true
281
- }
282
-
283
- /**
284
- * We may get the values required to determine if this is a valid range request at different times
285
- * so we need to calculate it when asked.
286
- */
287
- public get isValidRangeRequest (): boolean {
288
- if (this._isValidRangeRequest) {
289
- // prevent unnecessary re-validation of each byte range
290
- return true
291
- }
292
- if (!this.isRangeRequest) {
293
- return false
294
- }
295
-
296
- if (this.byteRanges.length === 0) {
297
- this.log.trace('invalid range request, no valid ranges')
298
- return false
299
- }
300
-
301
- const isValid = this.byteRanges.every(range => this.isValidByteRange(range))
302
- if (!isValid) {
303
- this.log.trace('invalid range request, not all ranges are valid')
304
- return false
305
- }
306
-
307
- this._isValidRangeRequest = true
308
-
309
- return true
310
- }
311
-
312
- // /**
313
- // * Given all the information we have, this function returns the offset that will be used when:
314
- // * 1. calling unixfs.cat
315
- // * 2. slicing the body
316
- // */
317
- // public offset (range: ByteRange): number {
318
- // if (this.byteRanges.length > 0) {
319
- // return this.byteRanges[0].start ?? 0
320
- // }
321
- // return 0
322
- // }
323
-
324
- /**
325
- * Given all the information we have, this function returns the length that will be used when:
326
- * 1. calling unixfs.cat
327
- * 2. slicing the body
328
- */
329
- public getLength (range?: ByteRange): number | undefined {
330
- if (!this.isValidRangeRequest) {
331
- this.log.error('cannot get length for invalid range request')
332
- return undefined
333
- }
334
-
335
- if (this.isMultiRangeRequest && range == null) {
336
- /**
337
- * The content-length for a multi-range request is the sum of the lengths of all the ranges, plus the boundaries and part headers and newlines.
338
- */
339
- // TODO: figure out a way to calculate the correct content-length for multi-range requests' response.
340
- return undefined
341
- }
342
- range ??= this.byteRanges[0]
343
- this.log.trace('getting length for range: %o', range)
344
-
345
- if (range.end != null && range.start != null) {
346
- // For a range like bytes=1000-2000, we want a length of 1001 bytes
347
- return range.end - range.start + 1
348
- }
349
- if (range.end != null) {
350
- return range.end + 1
351
- }
352
- return range.size
353
- }
354
-
355
- /**
356
- * Converts a range request header into helia/unixfs supported range options
357
- * Note that the gateway specification says we "MAY" support multiple ranges (https://specs.ipfs.tech/http-gateways/path-gateway/#range-request-header) but we don't
358
- *
359
- * Also note that @helia/unixfs and ipfs-unixfs-exporter expect length and offset to be numbers, the range header is a string, and the size of the resource is likely a bigint.
360
- *
361
- * SUPPORTED:
362
- * Range: bytes=<range-start>-<range-end>
363
- * Range: bytes=<range-start>-
364
- * Range: bytes=-<suffix-length> // must pass size so we can calculate the offset. suffix-length is the number of bytes from the end of the file.
365
- * Range: bytes=<range-start>-<range-end>, <range-start>-<range-end>
366
- * Range: bytes=<range-start>-<range-end>, <range-start>-<range-end>, <range-start>-<range-end>
367
- *
368
- * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Range#directives
369
- */
370
- private setOffsetDetails (): void {
371
- if (this.requestRanges.length === 0) {
372
- this.log.trace('no request ranges defined')
373
- return
374
- }
375
-
376
- try {
377
- // Calculate byte ranges for all requests
378
- this.byteRanges = this.requestRanges.map(range => {
379
- const { start, end, byteSize } = calculateByteRangeIndexes(
380
- range.start ?? undefined,
381
- range.end ?? undefined,
382
- this._fileSize ?? undefined
383
- )
384
- return { start, end, size: byteSize }
385
- })
386
-
387
- this.log.trace('set byte ranges: %o', this.byteRanges)
388
- } catch (e) {
389
- this.log.error('error setting offset details: %o', e)
390
- this.byteRanges = []
391
- }
392
- }
393
-
394
- /**
395
- * Helper to convert a SliceableBody to a Uint8Array
396
- */
397
- private async convertToUint8Array (content: SliceableBody): Promise<Uint8Array> {
398
- if (typeof content === 'string') {
399
- return new TextEncoder().encode(content)
400
- }
401
-
402
- if ('arrayBuffer' in content && typeof content.arrayBuffer === 'function') {
403
- // This is a Blob
404
- const buffer = await content.arrayBuffer()
405
- return new Uint8Array(buffer)
406
- }
407
-
408
- if ('byteLength' in content && !('buffer' in content)) {
409
- // This is an ArrayBuffer
410
- return new Uint8Array(content)
411
- }
412
-
413
- if ('buffer' in content && 'byteLength' in content && 'byteOffset' in content) {
414
- // This is a Uint8Array
415
- return content as Uint8Array
416
- }
417
-
418
- throw new Error('Unsupported content type for multipart response')
419
- }
420
-
421
- private async * getMultipartBody (responseContentType: string = 'application/octet-stream'): AsyncIterable<Uint8Array> {
422
- const body = this._body
423
- if (body instanceof ReadableStream) {
424
- // in the case of unixfs, the body is a readable stream, and setBody is called with a function that returns a readable stream that generates the
425
- // correct multipartBody.. so we just return that body.
426
- return body
427
- }
428
-
429
- if (body === null) {
430
- throw new Error('Cannot create multipart body from null')
431
- }
432
-
433
- const encoder = new TextEncoder()
434
-
435
- for (const range of this.byteRanges) {
436
- if (range.start === undefined || range.end === undefined) {
437
- continue
438
- }
439
-
440
- // Calculate part headers
441
- const partHeaderString =
442
- `\r\n--${this.multiPartBoundary}\r\n` +
443
- `Content-Type: ${responseContentType}\r\n` +
444
- `Content-Range: ${getContentRangeHeader({
445
- byteStart: range.start,
446
- byteEnd: range.end,
447
- byteSize: this._fileSize ?? undefined
448
- })}\r\n\r\n`
449
-
450
- // Convert header to Uint8Array
451
- yield encoder.encode(partHeaderString)
452
-
453
- // Get content for this range and convert to Uint8Array
454
- const slicedContent = this.getSlicedBodyForRange(body, range.start, range.end)
455
- yield await this.convertToUint8Array(slicedContent)
456
- }
457
-
458
- // Add final this.multiPartBoundary
459
- yield encoder.encode(`\r\n--${this.multiPartBoundary}--`)
460
- }
461
-
462
- private getSlicedBodyForRange<T extends SliceableBody>(
463
- body: T,
464
- start: number,
465
- end: number
466
- ): SliceableBody {
467
- // Calculate the correct number of bytes to return
468
- // For a range like bytes=1000-2000, we want exactly 1001 bytes
469
- const offset = start
470
- const length = end - start + 1
471
-
472
- this.log.trace('slicing body with offset=%o and length=%o', offset, length)
473
-
474
- if (typeof body === 'string') {
475
- return body.slice(offset, offset + length) satisfies SliceableBody
476
- } else if (body instanceof Blob) {
477
- return body.slice(offset, offset + length) satisfies SliceableBody
478
- } else if (body instanceof ArrayBuffer || body instanceof Uint8Array) {
479
- return body.slice(offset, offset + length) satisfies SliceableBody
480
- } else {
481
- // This should never happen due to type constraints
482
- return body as SliceableBody
483
- }
484
- }
485
-
486
- /**
487
- * Returns the content type for the response.
488
- * For multipart ranges, this will be multipart/byteranges with a boundary.
489
- */
490
- public getContentType (): string | undefined {
491
- if (this.isMultiRangeRequest && this.isValidRangeRequest) {
492
- return `multipart/byteranges; boundary=${this.multiPartBoundary}`
493
- }
494
- return undefined
495
- }
496
-
497
- /**
498
- * This function returns the value of the "content-range" header.
499
- *
500
- * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Range
501
- *
502
- * Returns a string representing the following content ranges:
503
- *
504
- * @example
505
- * - Content-Range: <unit> <byteStart>-<byteEnd>/<byteSize>
506
- * - Content-Range: <unit> <byteStart>-<byteEnd>/*
507
- */
508
- // - Content-Range: <unit> */<byteSize> // this is purposefully not in jsdoc block
509
- public get contentRangeHeaderValue (): string {
510
- // For multipart responses, this will be included in each part
511
- // So this method is only used for single-range responses
512
- if (!this.isValidRangeRequest) {
513
- this.log.error('cannot get contentRangeHeaderValue for invalid range request')
514
- throw new InvalidRangeError('Invalid range request')
515
- }
516
-
517
- if (this.isMultiRangeRequest) {
518
- this.log.error('contentRangeHeaderValue should not be called for multipart responses')
519
- throw new InvalidRangeError('Content-Range header not applicable for multipart responses')
520
- }
521
-
522
- if (this.byteRanges.length > 0) {
523
- const range = this.byteRanges[0]
524
- return getContentRangeHeader({
525
- byteStart: range.start,
526
- byteEnd: range.end,
527
- byteSize: this._fileSize ?? undefined
528
- })
529
- }
530
-
531
- throw new InvalidRangeError('No valid ranges found')
532
- }
533
-
534
- // Unified method to create a stream for either single or multi-range requests
535
- private createRangeStream (
536
- contentProvider: ((range: ByteRange) => AsyncGenerator<Uint8Array, void, unknown>),
537
- contentType: string
538
- ): ReadableStream<Uint8Array> {
539
- const encoder = new TextEncoder()
540
- const byteRanges = this.byteRanges
541
- const multiPartBoundary = this.multiPartBoundary
542
- const fileSize = this._fileSize
543
- const log = this.log
544
- const isMultiRangeRequest = this.isMultiRangeRequest
545
-
546
- if (byteRanges.length === 0) {
547
- // TODO: create a stream with a range of *
548
- log.error('Cannot create range stream with no byte ranges')
549
- throw new InvalidRangeError('No valid ranges found')
550
- }
551
-
552
- return new ReadableStream({
553
- async start (controller) {
554
- try {
555
- // For multi-range requests, we need to handle multiple parts with headers
556
- for (const range of byteRanges) {
557
- // Write part header for multipart responses
558
- if (isMultiRangeRequest) {
559
- const partHeader =
560
- `\r\n--${multiPartBoundary}\r\n` +
561
- `Content-Type: ${contentType}\r\n` +
562
- `Content-Range: ${getContentRangeHeader({
563
- byteStart: range.start,
564
- byteEnd: range.end,
565
- byteSize: fileSize ?? undefined
566
- })}\r\n\r\n`
567
-
568
- controller.enqueue(encoder.encode(partHeader))
569
- }
570
-
571
- // Get and stream content for this range
572
- try {
573
- // Get content for this range
574
- const rangeContent = contentProvider(range)
575
- for await (const chunk of rangeContent) {
576
- controller.enqueue(chunk)
577
- }
578
- } catch (err) {
579
- log.error('Error processing range %o: %o', range, err)
580
- throw err // Re-throw to be caught by the outer try/catch
581
- }
582
- }
583
-
584
- if (isMultiRangeRequest) {
585
- // Write final boundary for multipart
586
- controller.enqueue(encoder.encode(`\r\n--${multiPartBoundary}--`))
587
- }
588
-
589
- controller.close()
590
- } catch (err) {
591
- log.error('Error processing range(s) - %e', err)
592
- controller.error(err)
593
- }
594
- }
595
- })
596
- }
597
- }
@@ -1,63 +0,0 @@
1
- import { decode, encode } from 'cborg'
2
- import { encode as jsonEncode } from 'cborg/json'
3
- import { CID } from 'multiformats/cid'
4
- import type { DecodeOptions } from 'cborg'
5
-
6
- // https://github.com/ipfs/go-ipfs/issues/3570#issuecomment-273931692
7
- const CID_CBOR_TAG = 42
8
-
9
- const options: DecodeOptions = {
10
- allowIndefinite: false,
11
- coerceUndefinedToNull: false,
12
- allowNaN: false,
13
- allowInfinity: false,
14
- strict: true,
15
- useMaps: false,
16
- rejectDuplicateMapKeys: true,
17
- tags: [],
18
-
19
- // this is different to `DAG-CBOR` - the reason we disallow BigInts is
20
- // because we are about to re-encode to `JSON` which does not support
21
- // BigInts. Blocks containing large numbers should be deserialized using a
22
- // cbor decoder instead
23
- allowBigInt: false.valueOf
24
- }
25
-
26
- /**
27
- * Take a `DAG-CBOR` encoded `Uint8Array`, deserialize it as an object and
28
- * re-serialize it in a form that can be passed to `JSON.serialize` and then
29
- * `JSON.parse` without losing any data.
30
- */
31
- export function dagCborToSafeJSON (buf: Uint8Array): string {
32
- const opts: DecodeOptions = {
33
- ...options,
34
- tags: []
35
- }
36
- opts.tags[CID_CBOR_TAG] = (bytes: Uint8Array): any => {
37
- if (bytes[0] !== 0) {
38
- throw new Error('Invalid CID for CBOR tag 42; expected leading 0x00')
39
- }
40
-
41
- return {
42
- '/': CID.decode(bytes.subarray(1)).toString() // ignore leading 0x00
43
- }
44
- }
45
-
46
- const obj = decode(buf, opts)
47
-
48
- return new TextDecoder().decode(jsonEncode(obj))
49
- }
50
-
51
- /**
52
- * Decode CBOR to object without CID tag support
53
- */
54
- export function cborToObject (buf: Uint8Array): any {
55
- return decode(buf, options)
56
- }
57
-
58
- /**
59
- * Decode CBOR to object without CID tag support
60
- */
61
- export function objectToCbor (obj: any): Uint8Array {
62
- return encode(obj, options)
63
- }