@helia/verified-fetch 6.3.1 → 6.4.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 (50) hide show
  1. package/dist/index.min.js +49 -48
  2. package/dist/index.min.js.map +4 -4
  3. package/dist/src/errors.d.ts +15 -3
  4. package/dist/src/errors.d.ts.map +1 -1
  5. package/dist/src/errors.js +15 -12
  6. package/dist/src/errors.js.map +1 -1
  7. package/dist/src/index.d.ts +18 -2
  8. package/dist/src/index.d.ts.map +1 -1
  9. package/dist/src/index.js.map +1 -1
  10. package/dist/src/plugins/plugin-handle-car.d.ts.map +1 -1
  11. package/dist/src/plugins/plugin-handle-car.js +1 -0
  12. package/dist/src/plugins/plugin-handle-car.js.map +1 -1
  13. package/dist/src/plugins/plugin-handle-ipld.d.ts.map +1 -1
  14. package/dist/src/plugins/plugin-handle-ipld.js +2 -0
  15. package/dist/src/plugins/plugin-handle-ipld.js.map +1 -1
  16. package/dist/src/plugins/plugin-handle-ipns-record.d.ts +1 -1
  17. package/dist/src/plugins/plugin-handle-ipns-record.d.ts.map +1 -1
  18. package/dist/src/plugins/plugin-handle-ipns-record.js +1 -0
  19. package/dist/src/plugins/plugin-handle-ipns-record.js.map +1 -1
  20. package/dist/src/plugins/plugin-handle-raw.d.ts.map +1 -1
  21. package/dist/src/plugins/plugin-handle-raw.js +2 -0
  22. package/dist/src/plugins/plugin-handle-raw.js.map +1 -1
  23. package/dist/src/plugins/plugin-handle-tar.d.ts.map +1 -1
  24. package/dist/src/plugins/plugin-handle-tar.js +1 -0
  25. package/dist/src/plugins/plugin-handle-tar.js.map +1 -1
  26. package/dist/src/plugins/plugin-handle-unixfs.js +2 -2
  27. package/dist/src/plugins/plugin-handle-unixfs.js.map +1 -1
  28. package/dist/src/url-resolver.d.ts +2 -1
  29. package/dist/src/url-resolver.d.ts.map +1 -1
  30. package/dist/src/url-resolver.js +47 -3
  31. package/dist/src/url-resolver.js.map +1 -1
  32. package/dist/src/utils/apply-redirect.d.ts +21 -0
  33. package/dist/src/utils/apply-redirect.d.ts.map +1 -0
  34. package/dist/src/utils/apply-redirect.js +159 -0
  35. package/dist/src/utils/apply-redirect.js.map +1 -0
  36. package/dist/src/verified-fetch.d.ts.map +1 -1
  37. package/dist/src/verified-fetch.js +5 -1
  38. package/dist/src/verified-fetch.js.map +1 -1
  39. package/package.json +3 -1
  40. package/src/errors.ts +17 -14
  41. package/src/index.ts +21 -2
  42. package/src/plugins/plugin-handle-car.ts +1 -0
  43. package/src/plugins/plugin-handle-ipld.ts +2 -0
  44. package/src/plugins/plugin-handle-ipns-record.ts +2 -1
  45. package/src/plugins/plugin-handle-raw.ts +2 -0
  46. package/src/plugins/plugin-handle-tar.ts +1 -0
  47. package/src/plugins/plugin-handle-unixfs.ts +2 -2
  48. package/src/url-resolver.ts +60 -9
  49. package/src/utils/apply-redirect.ts +200 -0
  50. package/src/verified-fetch.ts +6 -1
@@ -0,0 +1,200 @@
1
+ import escape from 'regexp.escape'
2
+ import { DuplicatePlaceholderError, InvalidRedirectStatusCodeError, RedirectsFileTooLargeError } from '../errors.ts'
3
+ import { errorToObject } from './error-to-object.ts'
4
+
5
+ /**
6
+ * @see https://specs.ipfs.tech/http-gateways/web-redirects-file/#status
7
+ */
8
+ const ALLOWED_STATUSES = [
9
+ 200, 301, 302, 303, 307, 308, 404, 410, 451
10
+ ]
11
+
12
+ /**
13
+ * @see https://specs.ipfs.tech/http-gateways/web-redirects-file/#max-file-size
14
+ */
15
+ const MAX_REDIRECTS_FILE_SIZE = 65_536
16
+
17
+ interface Redirect {
18
+ from: RegExp
19
+ to: string
20
+ status: number
21
+ }
22
+
23
+ export interface ApplyRedirectsOptions {
24
+ redirect?: RequestInit['redirect']
25
+ }
26
+
27
+ /**
28
+ * Examine the passed redirects file and translate the passed path.
29
+ *
30
+ * If `undefined` is returned it means no redirect was necessary.
31
+ * If a `string` is returned, an internal redirect to that path should be performed
32
+ * If a `Response` is returned, the user has signalled they wish to process
33
+ * redirects manually so it should be returned as a response
34
+ */
35
+ export function applyRedirects (url: URL, _redirects: string, options: ApplyRedirectsOptions = {}): URL | Response | undefined {
36
+ try {
37
+ // @see https://specs.ipfs.tech/http-gateways/web-redirects-file/#max-file-size
38
+ if (_redirects.length > MAX_REDIRECTS_FILE_SIZE) {
39
+ throw new RedirectsFileTooLargeError('_redirects file too large')
40
+ }
41
+
42
+ const redirects: Redirect[] = _redirects
43
+ .split('\n')
44
+ .filter(line => {
45
+ line = line.trim()
46
+
47
+ return line !== '' && !line.startsWith('#')
48
+ })
49
+ .map(line => {
50
+ const [from, to, status] = line.split(/\s+/)
51
+
52
+ let statusCode = parseInt(status)
53
+
54
+ if (isNaN(statusCode)) {
55
+ // @see https://specs.ipfs.tech/http-gateways/web-redirects-file/#status
56
+ statusCode = 301
57
+ }
58
+
59
+ // @see https://specs.ipfs.tech/http-gateways/web-redirects-file/#status
60
+ if (!ALLOWED_STATUSES.includes(statusCode)) {
61
+ throw new InvalidRedirectStatusCodeError(`Status code ${statusCode} is not allowed`)
62
+ }
63
+
64
+ const placeholders = new Set<string>()
65
+
66
+ const fromParts = from.split('/')
67
+ const fromPattern = fromParts.map((key, index) => {
68
+ if (key.startsWith(':')) {
69
+ const placeholder = key.substring(1)
70
+ if (placeholders.has(placeholder)) {
71
+ // @see https://specs.ipfs.tech/http-gateways/web-redirects-file/#placeholders
72
+ throw new DuplicatePlaceholderError('Duplicate placeholders in from path')
73
+ }
74
+
75
+ placeholders.add(placeholder)
76
+
77
+ // @see https://specs.ipfs.tech/http-gateways/web-redirects-file/#placeholders
78
+ return `(?<${placeholder}>[^/]+)`
79
+ }
80
+
81
+ if (key === '*' && index === (fromParts.length - 1)) {
82
+ // @see https://specs.ipfs.tech/http-gateways/web-redirects-file/#catch-all-splat
83
+ return '(?<splat>.+)'
84
+ }
85
+
86
+ // TODO: use RegExp.escape - requires Node.js 24+
87
+ return escape(key)
88
+ })
89
+ .join('\\/')
90
+
91
+ return {
92
+ from: new RegExp(fromPattern),
93
+ to,
94
+ status: statusCode
95
+ }
96
+ })
97
+
98
+ for (const { from, to, status } of redirects) {
99
+ const match = url.pathname.match(from)
100
+
101
+ if (match == null) {
102
+ continue
103
+ }
104
+
105
+ return toResult(url, to, status, match.groups, options)
106
+ }
107
+ } catch (err: any) {
108
+ // must be a 500
109
+ // @see https://specs.ipfs.tech/http-gateways/web-redirects-file/#error-handling
110
+ return new Response(JSON.stringify(errorToObject(err), null, 2), {
111
+ status: 500
112
+ })
113
+ }
114
+ }
115
+
116
+ function toResult (url: URL, to: string, status: number, groups?: Record<string, string> | null, options: ApplyRedirectsOptions = {}): URL | Response {
117
+ // cannot continue with redirect set to 'error'
118
+ if (options.redirect === 'error') {
119
+ throw new TypeError('Failed to fetch')
120
+ }
121
+
122
+ if (groups != null) {
123
+ // placeholders or splat present
124
+ for (const [key, value] of Object.entries(groups)) {
125
+ to = to.replaceAll(`:${key}`, value)
126
+ }
127
+ }
128
+
129
+ // preserve query/hash/etc from original URL
130
+ // @see https://specs.ipfs.tech/http-gateways/web-redirects-file/#query-parameters
131
+ const search = createSearch(url, new URL(`${url.protocol}//${url.host}${to}`))
132
+ const rawLocation = new URL(`${url.protocol}//${url.host}${to}`)
133
+
134
+ const location = new URL(`${url.protocol}//${url.host}${rawLocation.pathname}${formatSearch(search)}${url.hash}`)
135
+
136
+ if (options.redirect === 'manual') {
137
+ return new Response('', {
138
+ status,
139
+ headers: {
140
+ location: location.toString()
141
+ }
142
+ })
143
+ }
144
+
145
+ return location
146
+ }
147
+
148
+ function createSearch (userUrl: URL, redirectUrl: URL): Record<string, string | string[]> {
149
+ const search: Record<string, string | string[]> = {}
150
+
151
+ for (const [key, value] of userUrl.searchParams) {
152
+ redirectUrl.searchParams.delete(key)
153
+ addToSearch(key, value, search)
154
+ }
155
+
156
+ for (const [key, value] of redirectUrl.searchParams) {
157
+ addToSearch(key, value, search)
158
+ }
159
+
160
+ return search
161
+ }
162
+
163
+ function addToSearch (key: string, value: string, search: Record<string, string | string[]>): void {
164
+ if (typeof search[key] === 'string') {
165
+ search[key] = [search[key]]
166
+ }
167
+
168
+ if (Array.isArray(search[key])) {
169
+ search[key].push(value)
170
+ } else {
171
+ search[key] = value
172
+ }
173
+ }
174
+
175
+ /**
176
+ * Turns a record of key/value pairs into a URL-safe search params string
177
+ *
178
+ * We cannot use the native URLSearchParams to encode as it uses
179
+ * `application/x-www-form-urlencoded` encoding which encodes " " as "+" and
180
+ * not "%20" so use encodeURIComponent instead
181
+ */
182
+ export function formatSearch (params: Record<string, string | string[]>): string {
183
+ const search = [...Object.entries(params)]
184
+ .map(([key, value]) => {
185
+ if (!Array.isArray(value)) {
186
+ value = [value]
187
+ }
188
+
189
+ return value
190
+ .map(val => `${encodeURIComponent(key)}=${encodeURIComponent(val)}`)
191
+ .join('&')
192
+ })
193
+ .join('&')
194
+
195
+ if (search === '') {
196
+ return search
197
+ }
198
+
199
+ return `?${search}`
200
+ }
@@ -186,12 +186,17 @@ export class VerifiedFetch {
186
186
  range,
187
187
  url,
188
188
  resource,
189
- options
189
+ options,
190
+ redirected: false
190
191
  }))
191
192
  }
192
193
 
193
194
  const resolveResult = await this.urlResolver.resolve(url, serverTiming, options)
194
195
 
196
+ if (resolveResult instanceof Response) {
197
+ return this.handleFinalResponse(resolveResult)
198
+ }
199
+
195
200
  options?.onProgress?.(new CustomProgressEvent<CIDDetail>('verified-fetch:request:resolve', {
196
201
  cid: resolveResult.terminalElement.cid,
197
202
  path: resolveResult.url.pathname