@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.
- package/dist/index.min.js +49 -48
- package/dist/index.min.js.map +4 -4
- package/dist/src/errors.d.ts +15 -3
- package/dist/src/errors.d.ts.map +1 -1
- package/dist/src/errors.js +15 -12
- package/dist/src/errors.js.map +1 -1
- package/dist/src/index.d.ts +18 -2
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/plugins/plugin-handle-car.d.ts.map +1 -1
- package/dist/src/plugins/plugin-handle-car.js +1 -0
- package/dist/src/plugins/plugin-handle-car.js.map +1 -1
- package/dist/src/plugins/plugin-handle-ipld.d.ts.map +1 -1
- package/dist/src/plugins/plugin-handle-ipld.js +2 -0
- package/dist/src/plugins/plugin-handle-ipld.js.map +1 -1
- package/dist/src/plugins/plugin-handle-ipns-record.d.ts +1 -1
- package/dist/src/plugins/plugin-handle-ipns-record.d.ts.map +1 -1
- package/dist/src/plugins/plugin-handle-ipns-record.js +1 -0
- package/dist/src/plugins/plugin-handle-ipns-record.js.map +1 -1
- package/dist/src/plugins/plugin-handle-raw.d.ts.map +1 -1
- package/dist/src/plugins/plugin-handle-raw.js +2 -0
- package/dist/src/plugins/plugin-handle-raw.js.map +1 -1
- package/dist/src/plugins/plugin-handle-tar.d.ts.map +1 -1
- package/dist/src/plugins/plugin-handle-tar.js +1 -0
- package/dist/src/plugins/plugin-handle-tar.js.map +1 -1
- package/dist/src/plugins/plugin-handle-unixfs.js +2 -2
- package/dist/src/plugins/plugin-handle-unixfs.js.map +1 -1
- package/dist/src/url-resolver.d.ts +2 -1
- package/dist/src/url-resolver.d.ts.map +1 -1
- package/dist/src/url-resolver.js +47 -3
- package/dist/src/url-resolver.js.map +1 -1
- package/dist/src/utils/apply-redirect.d.ts +21 -0
- package/dist/src/utils/apply-redirect.d.ts.map +1 -0
- package/dist/src/utils/apply-redirect.js +159 -0
- package/dist/src/utils/apply-redirect.js.map +1 -0
- package/dist/src/verified-fetch.d.ts.map +1 -1
- package/dist/src/verified-fetch.js +5 -1
- package/dist/src/verified-fetch.js.map +1 -1
- package/package.json +3 -1
- package/src/errors.ts +17 -14
- package/src/index.ts +21 -2
- package/src/plugins/plugin-handle-car.ts +1 -0
- package/src/plugins/plugin-handle-ipld.ts +2 -0
- package/src/plugins/plugin-handle-ipns-record.ts +2 -1
- package/src/plugins/plugin-handle-raw.ts +2 -0
- package/src/plugins/plugin-handle-tar.ts +1 -0
- package/src/plugins/plugin-handle-unixfs.ts +2 -2
- package/src/url-resolver.ts +60 -9
- package/src/utils/apply-redirect.ts +200 -0
- 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
|
+
}
|
package/src/verified-fetch.ts
CHANGED
|
@@ -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
|