@helia/verified-fetch 0.0.0-a04e041 → 0.0.0-dc2e7a6
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/README.md +0 -33
- package/dist/index.min.js +4 -4
- package/dist/src/index.d.ts +0 -36
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +0 -33
- package/dist/src/index.js.map +1 -1
- package/dist/src/utils/parse-url-string.d.ts +0 -2
- package/dist/src/utils/parse-url-string.d.ts.map +1 -1
- package/dist/src/utils/parse-url-string.js +0 -6
- package/dist/src/utils/parse-url-string.js.map +1 -1
- package/dist/src/verified-fetch.d.ts +15 -17
- package/dist/src/verified-fetch.d.ts.map +1 -1
- package/dist/src/verified-fetch.js +129 -211
- package/dist/src/verified-fetch.js.map +1 -1
- package/package.json +12 -18
- package/src/index.ts +0 -37
- package/src/utils/parse-url-string.ts +1 -11
- package/src/verified-fetch.ts +134 -237
- package/dist/src/utils/get-content-disposition-filename.d.ts +0 -6
- package/dist/src/utils/get-content-disposition-filename.d.ts.map +0 -1
- package/dist/src/utils/get-content-disposition-filename.js +0 -16
- package/dist/src/utils/get-content-disposition-filename.js.map +0 -1
- package/dist/src/utils/responses.d.ts +0 -4
- package/dist/src/utils/responses.d.ts.map +0 -1
- package/dist/src/utils/responses.js +0 -21
- package/dist/src/utils/responses.js.map +0 -1
- package/dist/src/utils/select-output-type.d.ts +0 -12
- package/dist/src/utils/select-output-type.d.ts.map +0 -1
- package/dist/src/utils/select-output-type.js +0 -147
- package/dist/src/utils/select-output-type.js.map +0 -1
- package/src/utils/get-content-disposition-filename.ts +0 -18
- package/src/utils/responses.ts +0 -22
- package/src/utils/select-output-type.ts +0 -166
package/src/verified-fetch.ts
CHANGED
|
@@ -1,23 +1,18 @@
|
|
|
1
|
-
import { car } from '@helia/car'
|
|
2
1
|
import { ipns as heliaIpns, type IPNS } from '@helia/ipns'
|
|
3
2
|
import { dnsJsonOverHttps } from '@helia/ipns/dns-resolvers'
|
|
4
3
|
import { unixfs as heliaUnixFs, type UnixFS as HeliaUnixFs, type UnixFSStats } from '@helia/unixfs'
|
|
5
|
-
import
|
|
6
|
-
import
|
|
4
|
+
import { code as dagCborCode } from '@ipld/dag-cbor'
|
|
5
|
+
import { code as dagJsonCode } from '@ipld/dag-json'
|
|
7
6
|
import { code as dagPbCode } from '@ipld/dag-pb'
|
|
8
|
-
import toBrowserReadableStream from 'it-to-browser-readablestream'
|
|
9
7
|
import { code as jsonCode } from 'multiformats/codecs/json'
|
|
10
8
|
import { code as rawCode } from 'multiformats/codecs/raw'
|
|
11
9
|
import { identity } from 'multiformats/hashes/identity'
|
|
12
10
|
import { CustomProgressEvent } from 'progress-events'
|
|
13
11
|
import { dagCborToSafeJSON } from './utils/dag-cbor-to-safe-json.js'
|
|
14
|
-
import { getContentDispositionFilename } from './utils/get-content-disposition-filename.js'
|
|
15
12
|
import { getETag } from './utils/get-e-tag.js'
|
|
16
13
|
import { getStreamFromAsyncIterable } from './utils/get-stream-from-async-iterable.js'
|
|
17
14
|
import { parseResource } from './utils/parse-resource.js'
|
|
18
|
-
import {
|
|
19
|
-
import { selectOutputType, queryFormatToAcceptHeader } from './utils/select-output-type.js'
|
|
20
|
-
import { walkPath } from './utils/walk-path.js'
|
|
15
|
+
import { walkPath, type PathWalkerFn } from './utils/walk-path.js'
|
|
21
16
|
import type { CIDDetail, ContentTypeParser, Resource, VerifiedFetchInit as VerifiedFetchOptions } from './index.js'
|
|
22
17
|
import type { RequestFormatShorthand } from './types.js'
|
|
23
18
|
import type { Helia } from '@helia/interface'
|
|
@@ -29,6 +24,7 @@ interface VerifiedFetchComponents {
|
|
|
29
24
|
helia: Helia
|
|
30
25
|
ipns?: IPNS
|
|
31
26
|
unixfs?: HeliaUnixFs
|
|
27
|
+
pathWalker?: PathWalkerFn
|
|
32
28
|
}
|
|
33
29
|
|
|
34
30
|
/**
|
|
@@ -41,13 +37,8 @@ interface VerifiedFetchInit {
|
|
|
41
37
|
interface FetchHandlerFunctionArg {
|
|
42
38
|
cid: CID
|
|
43
39
|
path: string
|
|
40
|
+
terminalElement?: UnixFSEntry
|
|
44
41
|
options?: Omit<VerifiedFetchOptions, 'signal'> & AbortOptions
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* If present, the user has sent an accept header with this value - if the
|
|
48
|
-
* content cannot be represented in this format a 406 should be returned
|
|
49
|
-
*/
|
|
50
|
-
accept?: string
|
|
51
42
|
}
|
|
52
43
|
|
|
53
44
|
interface FetchHandlerFunction {
|
|
@@ -69,48 +60,29 @@ function convertOptions (options?: VerifiedFetchOptions): (Omit<VerifiedFetchOpt
|
|
|
69
60
|
}
|
|
70
61
|
}
|
|
71
62
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
'application/octet-stream'
|
|
79
|
-
]
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* if the user has specified an `Accept` header, and it's in our list of
|
|
83
|
-
* allowable "raw" format headers, use that instead of detecting the content
|
|
84
|
-
* type. This avoids the user from receiving something different when they
|
|
85
|
-
* signal that they want to `Accept` a specific mime type.
|
|
86
|
-
*/
|
|
87
|
-
function getOverridenRawContentType (headers?: HeadersInit): string | undefined {
|
|
88
|
-
const acceptHeader = new Headers(headers).get('accept') ?? ''
|
|
89
|
-
|
|
90
|
-
// e.g. "Accept: text/html, application/xhtml+xml, application/xml;q=0.9, image/webp, */*;q=0.8"
|
|
91
|
-
const acceptHeaders = acceptHeader.split(',')
|
|
92
|
-
.map(s => s.split(';')[0])
|
|
93
|
-
.map(s => s.trim())
|
|
94
|
-
|
|
95
|
-
for (const mimeType of acceptHeaders) {
|
|
96
|
-
if (mimeType === '*/*') {
|
|
97
|
-
return
|
|
98
|
-
}
|
|
63
|
+
function okResponse (body?: BodyInit | null): Response {
|
|
64
|
+
return new Response(body, {
|
|
65
|
+
status: 200,
|
|
66
|
+
statusText: 'OK'
|
|
67
|
+
})
|
|
68
|
+
}
|
|
99
69
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
70
|
+
function notSupportedResponse (body?: BodyInit | null): Response {
|
|
71
|
+
return new Response(body, {
|
|
72
|
+
status: 501,
|
|
73
|
+
statusText: 'Not Implemented'
|
|
74
|
+
})
|
|
104
75
|
}
|
|
105
76
|
|
|
106
77
|
export class VerifiedFetch {
|
|
107
78
|
private readonly helia: Helia
|
|
108
79
|
private readonly ipns: IPNS
|
|
109
80
|
private readonly unixfs: HeliaUnixFs
|
|
81
|
+
private readonly pathWalker: PathWalkerFn
|
|
110
82
|
private readonly log: Logger
|
|
111
83
|
private readonly contentTypeParser: ContentTypeParser | undefined
|
|
112
84
|
|
|
113
|
-
constructor ({ helia, ipns, unixfs }: VerifiedFetchComponents, init?: VerifiedFetchInit) {
|
|
85
|
+
constructor ({ helia, ipns, unixfs, pathWalker }: VerifiedFetchComponents, init?: VerifiedFetchInit) {
|
|
114
86
|
this.helia = helia
|
|
115
87
|
this.log = helia.logger.forComponent('helia:verified-fetch')
|
|
116
88
|
this.ipns = ipns ?? heliaIpns(helia, {
|
|
@@ -120,128 +92,60 @@ export class VerifiedFetch {
|
|
|
120
92
|
]
|
|
121
93
|
})
|
|
122
94
|
this.unixfs = unixfs ?? heliaUnixFs(helia)
|
|
95
|
+
this.pathWalker = pathWalker ?? walkPath
|
|
123
96
|
this.contentTypeParser = init?.contentTypeParser
|
|
124
97
|
this.log.trace('created VerifiedFetch instance')
|
|
125
98
|
}
|
|
126
99
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
private async handleIPNSRecord (resource: string, opts?: VerifiedFetchOptions): Promise<Response> {
|
|
132
|
-
return notSupportedResponse('vnd.ipfs.ipns-record support is not implemented')
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
/**
|
|
136
|
-
* Accepts a `CID` and returns a `Response` with a body stream that is a CAR
|
|
137
|
-
* of the `DAG` referenced by the `CID`.
|
|
138
|
-
*/
|
|
139
|
-
private async handleCar ({ cid, options }: FetchHandlerFunctionArg): Promise<Response> {
|
|
140
|
-
const c = car(this.helia)
|
|
141
|
-
const stream = toBrowserReadableStream(c.stream(cid, options))
|
|
142
|
-
|
|
143
|
-
const response = okResponse(stream)
|
|
144
|
-
response.headers.set('content-type', 'application/vnd.ipld.car; version=1')
|
|
145
|
-
|
|
100
|
+
// handle vnd.ipfs.ipns-record
|
|
101
|
+
private async handleIPNSRecord ({ cid, path, options }: FetchHandlerFunctionArg): Promise<Response> {
|
|
102
|
+
const response = notSupportedResponse('vnd.ipfs.ipns-record support is not implemented')
|
|
103
|
+
response.headers.set('X-Content-Type-Options', 'nosniff') // see https://specs.ipfs.tech/http-gateways/path-gateway/#x-content-type-options-response-header
|
|
146
104
|
return response
|
|
147
105
|
}
|
|
148
106
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
if (cid.code !== dagPbCode) {
|
|
155
|
-
return notAcceptableResponse('only dag-pb CIDs can be returned in TAR files')
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
return notSupportedResponse('application/tar support is not implemented')
|
|
107
|
+
// handle vnd.ipld.car
|
|
108
|
+
private async handleIPLDCar ({ cid, path, options }: FetchHandlerFunctionArg): Promise<Response> {
|
|
109
|
+
const response = notSupportedResponse('vnd.ipld.car support is not implemented')
|
|
110
|
+
response.headers.set('X-Content-Type-Options', 'nosniff') // see https://specs.ipfs.tech/http-gateways/path-gateway/#x-content-type-options-response-header
|
|
111
|
+
return response
|
|
159
112
|
}
|
|
160
113
|
|
|
161
|
-
private async handleJson ({ cid, path,
|
|
114
|
+
private async handleJson ({ cid, path, options }: FetchHandlerFunctionArg): Promise<Response> {
|
|
162
115
|
this.log.trace('fetching %c/%s', cid, path)
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
const obj = ipldDagJson.decode(block)
|
|
172
|
-
body = ipldDagCbor.encode(obj)
|
|
173
|
-
} catch (err) {
|
|
174
|
-
this.log.error('could not transform %c to application/vnd.ipld.dag-cbor', err)
|
|
175
|
-
return notAcceptableResponse()
|
|
176
|
-
}
|
|
177
|
-
} else {
|
|
178
|
-
// skip decoding
|
|
179
|
-
body = block
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
const response = okResponse(body)
|
|
183
|
-
response.headers.set('content-type', accept ?? 'application/json')
|
|
116
|
+
options?.onProgress?.(new CustomProgressEvent<CIDDetail>('verified-fetch:request:start', { cid, path }))
|
|
117
|
+
const result = await this.helia.blockstore.get(cid, {
|
|
118
|
+
signal: options?.signal,
|
|
119
|
+
onProgress: options?.onProgress
|
|
120
|
+
})
|
|
121
|
+
const response = okResponse(result)
|
|
122
|
+
response.headers.set('content-type', 'application/json')
|
|
123
|
+
options?.onProgress?.(new CustomProgressEvent<CIDDetail>('verified-fetch:request:end', { cid, path }))
|
|
184
124
|
return response
|
|
185
125
|
}
|
|
186
126
|
|
|
187
|
-
private async handleDagCbor ({ cid, path,
|
|
127
|
+
private async handleDagCbor ({ cid, path, options }: FetchHandlerFunctionArg): Promise<Response> {
|
|
188
128
|
this.log.trace('fetching %c/%s', cid, path)
|
|
189
|
-
|
|
190
|
-
|
|
129
|
+
options?.onProgress?.(new CustomProgressEvent<CIDDetail>('verified-fetch:request:start', { cid, path }))
|
|
130
|
+
// return body as binary
|
|
131
|
+
const block = await this.helia.blockstore.get(cid)
|
|
191
132
|
let body: string | Uint8Array
|
|
192
133
|
|
|
193
|
-
|
|
194
|
-
|
|
134
|
+
try {
|
|
135
|
+
body = dagCborToSafeJSON(block)
|
|
136
|
+
} catch (err) {
|
|
137
|
+
this.log('could not decode DAG-CBOR as JSON-safe, falling back to `application/octet-stream`', err)
|
|
195
138
|
body = block
|
|
196
|
-
} else if (accept === 'application/vnd.ipld.dag-json') {
|
|
197
|
-
try {
|
|
198
|
-
// if vnd.ipld.dag-json has been specified, convert to the format - note
|
|
199
|
-
// that this supports more data types than regular JSON, the content-type
|
|
200
|
-
// response header is set so the user knows to process it differently
|
|
201
|
-
const obj = ipldDagCbor.decode(block)
|
|
202
|
-
body = ipldDagJson.encode(obj)
|
|
203
|
-
} catch (err) {
|
|
204
|
-
this.log.error('could not transform %c to application/vnd.ipld.dag-json', err)
|
|
205
|
-
return notAcceptableResponse()
|
|
206
|
-
}
|
|
207
|
-
} else {
|
|
208
|
-
try {
|
|
209
|
-
body = dagCborToSafeJSON(block)
|
|
210
|
-
} catch (err) {
|
|
211
|
-
if (accept === 'application/json') {
|
|
212
|
-
this.log('could not decode DAG-CBOR as JSON-safe, but the client sent "Accept: application/json"', err)
|
|
213
|
-
|
|
214
|
-
return notAcceptableResponse()
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
this.log('could not decode DAG-CBOR as JSON-safe, falling back to `application/octet-stream`', err)
|
|
218
|
-
body = block
|
|
219
|
-
}
|
|
220
139
|
}
|
|
221
140
|
|
|
222
141
|
const response = okResponse(body)
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
accept = body instanceof Uint8Array ? 'application/octet-stream' : 'application/json'
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
response.headers.set('content-type', accept)
|
|
229
|
-
|
|
142
|
+
response.headers.set('content-type', body instanceof Uint8Array ? 'application/octet-stream' : 'application/json')
|
|
143
|
+
options?.onProgress?.(new CustomProgressEvent<CIDDetail>('verified-fetch:request:end', { cid, path }))
|
|
230
144
|
return response
|
|
231
145
|
}
|
|
232
146
|
|
|
233
|
-
private async handleDagPb ({ cid, path, options }: FetchHandlerFunctionArg): Promise<Response> {
|
|
234
|
-
|
|
235
|
-
let ipfsRoots: CID[] | undefined
|
|
236
|
-
|
|
237
|
-
try {
|
|
238
|
-
const pathDetails = await walkPath(this.helia.blockstore, `${cid.toString()}/${path}`, options)
|
|
239
|
-
ipfsRoots = pathDetails.ipfsRoots
|
|
240
|
-
terminalElement = pathDetails.terminalElement
|
|
241
|
-
} catch (err) {
|
|
242
|
-
this.log.error('Error walking path %s', path, err)
|
|
243
|
-
}
|
|
244
|
-
|
|
147
|
+
private async handleDagPb ({ cid, path, options, terminalElement }: FetchHandlerFunctionArg): Promise<Response> {
|
|
148
|
+
this.log.trace('fetching %c/%s', cid, path)
|
|
245
149
|
let resolvedCID = terminalElement?.cid ?? cid
|
|
246
150
|
let stat: UnixFSStats
|
|
247
151
|
if (terminalElement?.type === 'directory') {
|
|
@@ -250,6 +154,7 @@ export class VerifiedFetch {
|
|
|
250
154
|
const rootFilePath = 'index.html'
|
|
251
155
|
try {
|
|
252
156
|
this.log.trace('found directory at %c/%s, looking for index.html', cid, path)
|
|
157
|
+
options?.onProgress?.(new CustomProgressEvent<CIDDetail>('verified-fetch:request:start', { cid: dirCid, path: rootFilePath }))
|
|
253
158
|
stat = await this.unixfs.stat(dirCid, {
|
|
254
159
|
path: rootFilePath,
|
|
255
160
|
signal: options?.signal,
|
|
@@ -267,6 +172,7 @@ export class VerifiedFetch {
|
|
|
267
172
|
}
|
|
268
173
|
}
|
|
269
174
|
|
|
175
|
+
options?.onProgress?.(new CustomProgressEvent<CIDDetail>('verified-fetch:request:start', { cid: resolvedCID, path: '' }))
|
|
270
176
|
const asyncIter = this.unixfs.cat(resolvedCID, {
|
|
271
177
|
signal: options?.signal,
|
|
272
178
|
onProgress: options?.onProgress
|
|
@@ -279,27 +185,19 @@ export class VerifiedFetch {
|
|
|
279
185
|
const response = okResponse(stream)
|
|
280
186
|
await this.setContentType(firstChunk, path, response)
|
|
281
187
|
|
|
282
|
-
|
|
283
|
-
response.headers.set('X-Ipfs-Roots', ipfsRoots.map(cid => cid.toV1().toString()).join(',')) // https://specs.ipfs.tech/http-gateways/path-gateway/#x-ipfs-roots-response-header
|
|
284
|
-
}
|
|
188
|
+
options?.onProgress?.(new CustomProgressEvent<CIDDetail>('verified-fetch:request:end', { cid: resolvedCID, path: '' }))
|
|
285
189
|
|
|
286
190
|
return response
|
|
287
191
|
}
|
|
288
192
|
|
|
289
193
|
private async handleRaw ({ cid, path, options }: FetchHandlerFunctionArg): Promise<Response> {
|
|
290
|
-
|
|
194
|
+
this.log.trace('fetching %c/%s', cid, path)
|
|
195
|
+
options?.onProgress?.(new CustomProgressEvent<CIDDetail>('verified-fetch:request:start', { cid, path }))
|
|
196
|
+
const result = await this.helia.blockstore.get(cid)
|
|
291
197
|
const response = okResponse(result)
|
|
198
|
+
await this.setContentType(result, path, response)
|
|
292
199
|
|
|
293
|
-
|
|
294
|
-
// type, honour that header, so for example they don't request
|
|
295
|
-
// `application/vnd.ipld.raw` but get `application/octet-stream`
|
|
296
|
-
const overriddenContentType = getOverridenRawContentType(options?.headers)
|
|
297
|
-
if (overriddenContentType != null) {
|
|
298
|
-
response.headers.set('content-type', overriddenContentType)
|
|
299
|
-
} else {
|
|
300
|
-
await this.setContentType(result, path, response)
|
|
301
|
-
}
|
|
302
|
-
|
|
200
|
+
options?.onProgress?.(new CustomProgressEvent<CIDDetail>('verified-fetch:request:end', { cid, path }))
|
|
303
201
|
return response
|
|
304
202
|
}
|
|
305
203
|
|
|
@@ -330,112 +228,111 @@ export class VerifiedFetch {
|
|
|
330
228
|
}
|
|
331
229
|
|
|
332
230
|
/**
|
|
333
|
-
*
|
|
334
|
-
*
|
|
231
|
+
* Determines the format requested by the client, defaults to `null` if no format is requested.
|
|
232
|
+
*
|
|
233
|
+
* @see https://specs.ipfs.tech/http-gateways/path-gateway/#format-request-query-parameter
|
|
234
|
+
* @default 'raw'
|
|
235
|
+
*/
|
|
236
|
+
private getFormat ({ headerFormat, queryFormat }: { headerFormat: string | null, queryFormat: RequestFormatShorthand | null }): RequestFormatShorthand | null {
|
|
237
|
+
const formatMap: Record<string, RequestFormatShorthand> = {
|
|
238
|
+
'vnd.ipld.raw': 'raw',
|
|
239
|
+
'vnd.ipld.car': 'car',
|
|
240
|
+
'application/x-tar': 'tar',
|
|
241
|
+
'application/vnd.ipld.dag-json': 'dag-json',
|
|
242
|
+
'application/vnd.ipld.dag-cbor': 'dag-cbor',
|
|
243
|
+
'application/json': 'json',
|
|
244
|
+
'application/cbor': 'cbor',
|
|
245
|
+
'vnd.ipfs.ipns-record': 'ipns-record'
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (headerFormat != null) {
|
|
249
|
+
for (const format in formatMap) {
|
|
250
|
+
if (headerFormat.includes(format)) {
|
|
251
|
+
return formatMap[format]
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
} else if (queryFormat != null) {
|
|
255
|
+
return queryFormat
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return null
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Map of format to specific handlers for that format.
|
|
263
|
+
* These format handlers should adjust the response headers as specified in https://specs.ipfs.tech/http-gateways/path-gateway/#response-headers
|
|
335
264
|
*/
|
|
265
|
+
private readonly formatHandlers: Record<string, FetchHandlerFunction> = {
|
|
266
|
+
raw: async () => notSupportedResponse('application/vnd.ipld.raw support is not implemented'),
|
|
267
|
+
car: this.handleIPLDCar,
|
|
268
|
+
'ipns-record': this.handleIPNSRecord,
|
|
269
|
+
tar: async () => notSupportedResponse('application/x-tar support is not implemented'),
|
|
270
|
+
'dag-json': async () => notSupportedResponse('application/vnd.ipld.dag-json support is not implemented'),
|
|
271
|
+
'dag-cbor': async () => notSupportedResponse('application/vnd.ipld.dag-cbor support is not implemented'),
|
|
272
|
+
json: async () => notSupportedResponse('application/json support is not implemented'),
|
|
273
|
+
cbor: async () => notSupportedResponse('application/cbor support is not implemented')
|
|
274
|
+
}
|
|
275
|
+
|
|
336
276
|
private readonly codecHandlers: Record<number, FetchHandlerFunction> = {
|
|
337
277
|
[dagPbCode]: this.handleDagPb,
|
|
338
|
-
[
|
|
278
|
+
[dagJsonCode]: this.handleJson,
|
|
339
279
|
[jsonCode]: this.handleJson,
|
|
340
|
-
[
|
|
280
|
+
[dagCborCode]: this.handleDagCbor,
|
|
341
281
|
[rawCode]: this.handleRaw,
|
|
342
282
|
[identity.code]: this.handleRaw
|
|
343
283
|
}
|
|
344
284
|
|
|
345
285
|
async fetch (resource: Resource, opts?: VerifiedFetchOptions): Promise<Response> {
|
|
346
|
-
this.log('fetch %s', resource)
|
|
347
|
-
|
|
348
286
|
const options = convertOptions(opts)
|
|
287
|
+
const { path, query, ...rest } = await parseResource(resource, { ipns: this.ipns, logger: this.helia.logger }, options)
|
|
288
|
+
const cid = rest.cid
|
|
289
|
+
let response: Response | undefined
|
|
349
290
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
// resolve the CID/path from the requested resource
|
|
353
|
-
const { path, query, cid } = await parseResource(resource, { ipns: this.ipns, logger: this.helia.logger }, options)
|
|
291
|
+
const format = this.getFormat({ headerFormat: new Headers(options?.headers).get('accept'), queryFormat: query.format ?? null })
|
|
354
292
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
const incomingAcceptHeader = requestHeaders.get('accept')
|
|
359
|
-
|
|
360
|
-
if (incomingAcceptHeader != null) {
|
|
361
|
-
this.log('incoming accept header "%s"', incomingAcceptHeader)
|
|
362
|
-
}
|
|
293
|
+
if (format != null) {
|
|
294
|
+
// TODO: These should be handled last when they're returning something other than 501
|
|
295
|
+
const formatHandler = this.formatHandlers[format]
|
|
363
296
|
|
|
364
|
-
|
|
297
|
+
if (formatHandler != null) {
|
|
298
|
+
response = await formatHandler.call(this, { cid, path, options })
|
|
365
299
|
|
|
366
|
-
|
|
367
|
-
|
|
300
|
+
if (response.status === 501) {
|
|
301
|
+
return response
|
|
302
|
+
}
|
|
303
|
+
}
|
|
368
304
|
}
|
|
369
305
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
this.log('output type %s', accept)
|
|
306
|
+
let terminalElement: UnixFSEntry | undefined
|
|
307
|
+
let ipfsRoots: CID[] | undefined
|
|
373
308
|
|
|
374
|
-
|
|
375
|
-
|
|
309
|
+
try {
|
|
310
|
+
const pathDetails = await this.pathWalker(this.helia.blockstore, `${cid.toString()}/${path}`, options)
|
|
311
|
+
ipfsRoots = pathDetails.ipfsRoots
|
|
312
|
+
terminalElement = pathDetails.terminalElement
|
|
313
|
+
} catch (err) {
|
|
314
|
+
this.log.error('Error walking path %s', path, err)
|
|
315
|
+
// return new Response(`Error walking path: ${(err as Error).message}`, { status: 500 })
|
|
376
316
|
}
|
|
377
317
|
|
|
378
|
-
|
|
379
|
-
let reqFormat: RequestFormatShorthand | undefined
|
|
380
|
-
|
|
381
|
-
if (accept === 'application/vnd.ipfs.ipns-record') {
|
|
382
|
-
// the user requested a raw IPNS record
|
|
383
|
-
reqFormat = 'ipns-record'
|
|
384
|
-
response = await this.handleIPNSRecord(resource.toString(), options)
|
|
385
|
-
} else if (accept === 'application/vnd.ipld.car') {
|
|
386
|
-
// the user requested a CAR file
|
|
387
|
-
reqFormat = 'car'
|
|
388
|
-
query.download = true
|
|
389
|
-
query.filename = query.filename ?? `${cid.toString()}.car`
|
|
390
|
-
response = await this.handleCar({ cid, path, options })
|
|
391
|
-
} else if (accept === 'application/vnd.ipld.raw') {
|
|
392
|
-
// the user requested a raw block
|
|
393
|
-
reqFormat = 'raw'
|
|
394
|
-
query.download = true
|
|
395
|
-
query.filename = query.filename ?? `${cid.toString()}.bin`
|
|
396
|
-
response = await this.handleRaw({ cid, path, options })
|
|
397
|
-
} else if (accept === 'application/x-tar') {
|
|
398
|
-
// the user requested a TAR file
|
|
399
|
-
reqFormat = 'tar'
|
|
400
|
-
response = await this.handleTar({ cid, path, options })
|
|
401
|
-
} else {
|
|
402
|
-
// derive the handler from the CID type
|
|
318
|
+
if (response == null) {
|
|
403
319
|
const codecHandler = this.codecHandlers[cid.code]
|
|
404
320
|
|
|
405
|
-
if (codecHandler
|
|
321
|
+
if (codecHandler != null) {
|
|
322
|
+
response = await codecHandler.call(this, { cid, path, options, terminalElement })
|
|
323
|
+
} else {
|
|
406
324
|
return notSupportedResponse(`Support for codec with code ${cid.code} is not yet implemented. Please open an issue at https://github.com/ipfs/helia/issues/new`)
|
|
407
325
|
}
|
|
408
|
-
|
|
409
|
-
response = await codecHandler.call(this, { cid, path, accept, options })
|
|
410
326
|
}
|
|
411
327
|
|
|
412
|
-
response.headers.set('etag', getETag({ cid, reqFormat, weak: false }))
|
|
328
|
+
response.headers.set('etag', getETag({ cid, reqFormat: format ?? undefined, weak: false }))
|
|
413
329
|
response.headers.set('cache-control', 'public, max-age=29030400, immutable')
|
|
414
|
-
// https://specs.ipfs.tech/http-gateways/path-gateway/#x-ipfs-path-response-header
|
|
415
|
-
response.headers.set('X-Ipfs-Path', resource.toString())
|
|
416
|
-
|
|
417
|
-
// set Content-Disposition header
|
|
418
|
-
let contentDisposition: string | undefined
|
|
419
|
-
|
|
420
|
-
// force download if requested
|
|
421
|
-
if (query.download === true) {
|
|
422
|
-
contentDisposition = 'attachment'
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
// override filename if requested
|
|
426
|
-
if (query.filename != null) {
|
|
427
|
-
if (contentDisposition == null) {
|
|
428
|
-
contentDisposition = 'inline'
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
contentDisposition = `${contentDisposition}; ${getContentDispositionFilename(query.filename)}`
|
|
432
|
-
}
|
|
330
|
+
response.headers.set('X-Ipfs-Path', resource.toString()) // https://specs.ipfs.tech/http-gateways/path-gateway/#x-ipfs-path-response-header
|
|
433
331
|
|
|
434
|
-
if (
|
|
435
|
-
response.headers.set('
|
|
332
|
+
if (ipfsRoots != null) {
|
|
333
|
+
response.headers.set('X-Ipfs-Roots', ipfsRoots.map(cid => cid.toV1().toString()).join(',')) // https://specs.ipfs.tech/http-gateways/path-gateway/#x-ipfs-roots-response-header
|
|
436
334
|
}
|
|
437
|
-
|
|
438
|
-
options?.onProgress?.(new CustomProgressEvent<CIDDetail>('verified-fetch:request:end', { cid, path }))
|
|
335
|
+
// response.headers.set('Content-Disposition', `TODO`) // https://specs.ipfs.tech/http-gateways/path-gateway/#content-disposition-response-header
|
|
439
336
|
|
|
440
337
|
return response
|
|
441
338
|
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"get-content-disposition-filename.d.ts","sourceRoot":"","sources":["../../../src/utils/get-content-disposition-filename.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,wBAAgB,6BAA6B,CAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAQvE"}
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Takes a filename URL param and returns a string for use in a
|
|
3
|
-
* `Content-Disposition` header
|
|
4
|
-
*/
|
|
5
|
-
export function getContentDispositionFilename(filename) {
|
|
6
|
-
const asciiOnly = replaceNonAsciiCharacters(filename);
|
|
7
|
-
if (asciiOnly === filename) {
|
|
8
|
-
return `filename="${filename}"`;
|
|
9
|
-
}
|
|
10
|
-
return `filename="${asciiOnly}"; filename*=UTF-8''${encodeURIComponent(filename)}`;
|
|
11
|
-
}
|
|
12
|
-
function replaceNonAsciiCharacters(filename) {
|
|
13
|
-
// eslint-disable-next-line no-control-regex
|
|
14
|
-
return filename.replace(/[^\x00-\x7F]/g, '_');
|
|
15
|
-
}
|
|
16
|
-
//# sourceMappingURL=get-content-disposition-filename.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"get-content-disposition-filename.js","sourceRoot":"","sources":["../../../src/utils/get-content-disposition-filename.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,MAAM,UAAU,6BAA6B,CAAE,QAAgB;IAC7D,MAAM,SAAS,GAAG,yBAAyB,CAAC,QAAQ,CAAC,CAAA;IAErD,IAAI,SAAS,KAAK,QAAQ,EAAE,CAAC;QAC3B,OAAO,aAAa,QAAQ,GAAG,CAAA;IACjC,CAAC;IAED,OAAO,aAAa,SAAS,uBAAuB,kBAAkB,CAAC,QAAQ,CAAC,EAAE,CAAA;AACpF,CAAC;AAED,SAAS,yBAAyB,CAAE,QAAgB;IAClD,4CAA4C;IAC5C,OAAO,QAAQ,CAAC,OAAO,CAAC,eAAe,EAAE,GAAG,CAAC,CAAA;AAC/C,CAAC"}
|
|
@@ -1,4 +0,0 @@
|
|
|
1
|
-
export declare function okResponse(body?: BodyInit | null): Response;
|
|
2
|
-
export declare function notSupportedResponse(body?: BodyInit | null): Response;
|
|
3
|
-
export declare function notAcceptableResponse(body?: BodyInit | null): Response;
|
|
4
|
-
//# sourceMappingURL=responses.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"responses.d.ts","sourceRoot":"","sources":["../../../src/utils/responses.ts"],"names":[],"mappings":"AAAA,wBAAgB,UAAU,CAAE,IAAI,CAAC,EAAE,QAAQ,GAAG,IAAI,GAAG,QAAQ,CAK5D;AAED,wBAAgB,oBAAoB,CAAE,IAAI,CAAC,EAAE,QAAQ,GAAG,IAAI,GAAG,QAAQ,CAOtE;AAED,wBAAgB,qBAAqB,CAAE,IAAI,CAAC,EAAE,QAAQ,GAAG,IAAI,GAAG,QAAQ,CAKvE"}
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
export function okResponse(body) {
|
|
2
|
-
return new Response(body, {
|
|
3
|
-
status: 200,
|
|
4
|
-
statusText: 'OK'
|
|
5
|
-
});
|
|
6
|
-
}
|
|
7
|
-
export function notSupportedResponse(body) {
|
|
8
|
-
const response = new Response(body, {
|
|
9
|
-
status: 501,
|
|
10
|
-
statusText: 'Not Implemented'
|
|
11
|
-
});
|
|
12
|
-
response.headers.set('X-Content-Type-Options', 'nosniff'); // see https://specs.ipfs.tech/http-gateways/path-gateway/#x-content-type-options-response-header
|
|
13
|
-
return response;
|
|
14
|
-
}
|
|
15
|
-
export function notAcceptableResponse(body) {
|
|
16
|
-
return new Response(body, {
|
|
17
|
-
status: 406,
|
|
18
|
-
statusText: 'Not Acceptable'
|
|
19
|
-
});
|
|
20
|
-
}
|
|
21
|
-
//# sourceMappingURL=responses.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"responses.js","sourceRoot":"","sources":["../../../src/utils/responses.ts"],"names":[],"mappings":"AAAA,MAAM,UAAU,UAAU,CAAE,IAAsB;IAChD,OAAO,IAAI,QAAQ,CAAC,IAAI,EAAE;QACxB,MAAM,EAAE,GAAG;QACX,UAAU,EAAE,IAAI;KACjB,CAAC,CAAA;AACJ,CAAC;AAED,MAAM,UAAU,oBAAoB,CAAE,IAAsB;IAC1D,MAAM,QAAQ,GAAG,IAAI,QAAQ,CAAC,IAAI,EAAE;QAClC,MAAM,EAAE,GAAG;QACX,UAAU,EAAE,iBAAiB;KAC9B,CAAC,CAAA;IACF,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,wBAAwB,EAAE,SAAS,CAAC,CAAA,CAAC,iGAAiG;IAC3J,OAAO,QAAQ,CAAA;AACjB,CAAC;AAED,MAAM,UAAU,qBAAqB,CAAE,IAAsB;IAC3D,OAAO,IAAI,QAAQ,CAAC,IAAI,EAAE;QACxB,MAAM,EAAE,GAAG;QACX,UAAU,EAAE,gBAAgB;KAC7B,CAAC,CAAA;AACJ,CAAC"}
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import type { RequestFormatShorthand } from '../types.js';
|
|
2
|
-
import type { CID } from 'multiformats/cid';
|
|
3
|
-
/**
|
|
4
|
-
* Selects an output mime-type based on the CID and a passed `Accept` header
|
|
5
|
-
*/
|
|
6
|
-
export declare function selectOutputType(cid: CID, accept?: string): string | undefined;
|
|
7
|
-
/**
|
|
8
|
-
* Converts a `format=...` query param to a mime type as would be found in the
|
|
9
|
-
* `Accept` header, if a valid mapping is available
|
|
10
|
-
*/
|
|
11
|
-
export declare function queryFormatToAcceptHeader(format?: RequestFormatShorthand): string | undefined;
|
|
12
|
-
//# sourceMappingURL=select-output-type.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"select-output-type.d.ts","sourceRoot":"","sources":["../../../src/utils/select-output-type.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,aAAa,CAAA;AACzD,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,kBAAkB,CAAA;AAuD3C;;GAEG;AACH,wBAAgB,gBAAgB,CAAE,GAAG,EAAE,GAAG,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAM/E;AAuFD;;;GAGG;AACH,wBAAgB,yBAAyB,CAAE,MAAM,CAAC,EAAE,sBAAsB,GAAG,MAAM,GAAG,SAAS,CAI9F"}
|