@helia/verified-fetch 0.0.0-9b1ddf8 → 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 +205 -6
- package/dist/index.min.js +4 -4
- package/dist/src/index.d.ts +186 -4
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +186 -4
- package/dist/src/index.js.map +1 -1
- package/dist/src/types.d.ts +2 -0
- package/dist/src/types.d.ts.map +1 -0
- package/dist/src/types.js +2 -0
- package/dist/src/types.js.map +1 -0
- package/dist/src/utils/dag-cbor-to-safe-json.d.ts +7 -0
- package/dist/src/utils/dag-cbor-to-safe-json.d.ts.map +1 -0
- package/dist/src/utils/dag-cbor-to-safe-json.js +37 -0
- package/dist/src/utils/dag-cbor-to-safe-json.js.map +1 -0
- package/dist/src/utils/get-e-tag.d.ts +28 -0
- package/dist/src/utils/get-e-tag.d.ts.map +1 -0
- package/dist/src/utils/get-e-tag.js +18 -0
- package/dist/src/utils/get-e-tag.js.map +1 -0
- package/dist/src/utils/parse-url-string.d.ts +5 -1
- package/dist/src/utils/parse-url-string.d.ts.map +1 -1
- package/dist/src/utils/parse-url-string.js.map +1 -1
- package/dist/src/verified-fetch.d.ts +1 -11
- package/dist/src/verified-fetch.d.ts.map +1 -1
- package/dist/src/verified-fetch.js +51 -50
- package/dist/src/verified-fetch.js.map +1 -1
- package/package.json +21 -15
- package/src/index.ts +186 -4
- package/src/types.ts +1 -0
- package/src/utils/dag-cbor-to-safe-json.ts +44 -0
- package/src/utils/get-e-tag.ts +36 -0
- package/src/utils/parse-url-string.ts +6 -1
- package/src/verified-fetch.ts +59 -56
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { RequestFormatShorthand } from '../types.js'
|
|
2
|
+
import type { CID } from 'multiformats/cid'
|
|
3
|
+
|
|
4
|
+
interface GetETagArg {
|
|
5
|
+
cid: CID
|
|
6
|
+
reqFormat?: RequestFormatShorthand
|
|
7
|
+
rangeStart?: number
|
|
8
|
+
rangeEnd?: number
|
|
9
|
+
/**
|
|
10
|
+
* Weak Etag is used when we can't guarantee byte-for-byte-determinism (generated, or mutable content).
|
|
11
|
+
* Some examples:
|
|
12
|
+
* - IPNS requests
|
|
13
|
+
* - CAR streamed with blocks in non-deterministic order
|
|
14
|
+
* - TAR streamed with files in non-deterministic order
|
|
15
|
+
*/
|
|
16
|
+
weak?: boolean
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* etag
|
|
21
|
+
* you need to wrap cid with ""
|
|
22
|
+
* we use strong Etags for immutable responses and weak one (prefixed with W/ ) for mutable/generated ones (ipns and generated HTML).
|
|
23
|
+
* block and car responses should have different etag than deserialized one, so you can add some prefix like we do in existing gateway
|
|
24
|
+
*
|
|
25
|
+
* @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag
|
|
26
|
+
* @see https://specs.ipfs.tech/http-gateways/path-gateway/#etag-response-header
|
|
27
|
+
*/
|
|
28
|
+
export function getETag ({ cid, reqFormat, weak, rangeStart, rangeEnd }: GetETagArg): string {
|
|
29
|
+
const prefix = weak === true ? 'W/' : ''
|
|
30
|
+
let suffix = reqFormat == null ? '' : `.${reqFormat}`
|
|
31
|
+
if (rangeStart != null || rangeEnd != null) {
|
|
32
|
+
suffix += `.${rangeStart ?? '0'}-${rangeEnd ?? 'N'}`
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return `${prefix}"${cid.toString()}${suffix}"`
|
|
36
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { peerIdFromString } from '@libp2p/peer-id'
|
|
2
2
|
import { CID } from 'multiformats/cid'
|
|
3
3
|
import { TLRU } from './tlru.js'
|
|
4
|
+
import type { RequestFormatShorthand } from '../types.js'
|
|
4
5
|
import type { IPNS, IPNSRoutingEvents, ResolveDnsLinkProgressEvents, ResolveProgressEvents, ResolveResult } from '@helia/ipns'
|
|
5
6
|
import type { ComponentLogger } from '@libp2p/interface'
|
|
6
7
|
import type { ProgressOptions } from 'progress-events'
|
|
@@ -16,11 +17,15 @@ export interface ParseUrlStringOptions extends ProgressOptions<ResolveProgressEv
|
|
|
16
17
|
|
|
17
18
|
}
|
|
18
19
|
|
|
20
|
+
export interface ParsedUrlQuery extends Record<string, string | unknown> {
|
|
21
|
+
format?: RequestFormatShorthand
|
|
22
|
+
}
|
|
23
|
+
|
|
19
24
|
export interface ParsedUrlStringResults {
|
|
20
25
|
protocol: string
|
|
21
26
|
path: string
|
|
22
27
|
cid: CID
|
|
23
|
-
query:
|
|
28
|
+
query: ParsedUrlQuery
|
|
24
29
|
}
|
|
25
30
|
|
|
26
31
|
const URL_REGEX = /^(?<protocol>ip[fn]s):\/\/(?<cidOrPeerIdOrDnsLink>[^/$?]+)\/?(?<path>[^$?]*)\??(?<queryString>.*)$/
|
package/src/verified-fetch.ts
CHANGED
|
@@ -1,19 +1,20 @@
|
|
|
1
|
-
import { dagCbor as heliaDagCbor, type DAGCBOR } from '@helia/dag-cbor'
|
|
2
|
-
import { dagJson as heliaDagJson, type DAGJSON } from '@helia/dag-json'
|
|
3
1
|
import { ipns as heliaIpns, type IPNS } from '@helia/ipns'
|
|
4
2
|
import { dnsJsonOverHttps } from '@helia/ipns/dns-resolvers'
|
|
5
|
-
import { json as heliaJson, type JSON } from '@helia/json'
|
|
6
3
|
import { unixfs as heliaUnixFs, type UnixFS as HeliaUnixFs, type UnixFSStats } from '@helia/unixfs'
|
|
7
4
|
import { code as dagCborCode } from '@ipld/dag-cbor'
|
|
8
5
|
import { code as dagJsonCode } from '@ipld/dag-json'
|
|
9
6
|
import { code as dagPbCode } from '@ipld/dag-pb'
|
|
10
7
|
import { code as jsonCode } from 'multiformats/codecs/json'
|
|
11
|
-
import {
|
|
8
|
+
import { code as rawCode } from 'multiformats/codecs/raw'
|
|
9
|
+
import { identity } from 'multiformats/hashes/identity'
|
|
12
10
|
import { CustomProgressEvent } from 'progress-events'
|
|
11
|
+
import { dagCborToSafeJSON } from './utils/dag-cbor-to-safe-json.js'
|
|
12
|
+
import { getETag } from './utils/get-e-tag.js'
|
|
13
13
|
import { getStreamFromAsyncIterable } from './utils/get-stream-from-async-iterable.js'
|
|
14
14
|
import { parseResource } from './utils/parse-resource.js'
|
|
15
15
|
import { walkPath, type PathWalkerFn } from './utils/walk-path.js'
|
|
16
16
|
import type { CIDDetail, ContentTypeParser, Resource, VerifiedFetchInit as VerifiedFetchOptions } from './index.js'
|
|
17
|
+
import type { RequestFormatShorthand } from './types.js'
|
|
17
18
|
import type { Helia } from '@helia/interface'
|
|
18
19
|
import type { AbortOptions, Logger } from '@libp2p/interface'
|
|
19
20
|
import type { UnixFSEntry } from 'ipfs-unixfs-exporter'
|
|
@@ -23,9 +24,6 @@ interface VerifiedFetchComponents {
|
|
|
23
24
|
helia: Helia
|
|
24
25
|
ipns?: IPNS
|
|
25
26
|
unixfs?: HeliaUnixFs
|
|
26
|
-
dagJson?: DAGJSON
|
|
27
|
-
json?: JSON
|
|
28
|
-
dagCbor?: DAGCBOR
|
|
29
27
|
pathWalker?: PathWalkerFn
|
|
30
28
|
}
|
|
31
29
|
|
|
@@ -62,18 +60,29 @@ function convertOptions (options?: VerifiedFetchOptions): (Omit<VerifiedFetchOpt
|
|
|
62
60
|
}
|
|
63
61
|
}
|
|
64
62
|
|
|
63
|
+
function okResponse (body?: BodyInit | null): Response {
|
|
64
|
+
return new Response(body, {
|
|
65
|
+
status: 200,
|
|
66
|
+
statusText: 'OK'
|
|
67
|
+
})
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function notSupportedResponse (body?: BodyInit | null): Response {
|
|
71
|
+
return new Response(body, {
|
|
72
|
+
status: 501,
|
|
73
|
+
statusText: 'Not Implemented'
|
|
74
|
+
})
|
|
75
|
+
}
|
|
76
|
+
|
|
65
77
|
export class VerifiedFetch {
|
|
66
78
|
private readonly helia: Helia
|
|
67
79
|
private readonly ipns: IPNS
|
|
68
80
|
private readonly unixfs: HeliaUnixFs
|
|
69
|
-
private readonly dagJson: DAGJSON
|
|
70
|
-
private readonly dagCbor: DAGCBOR
|
|
71
|
-
private readonly json: JSON
|
|
72
81
|
private readonly pathWalker: PathWalkerFn
|
|
73
82
|
private readonly log: Logger
|
|
74
83
|
private readonly contentTypeParser: ContentTypeParser | undefined
|
|
75
84
|
|
|
76
|
-
constructor ({ helia, ipns, unixfs,
|
|
85
|
+
constructor ({ helia, ipns, unixfs, pathWalker }: VerifiedFetchComponents, init?: VerifiedFetchInit) {
|
|
77
86
|
this.helia = helia
|
|
78
87
|
this.log = helia.logger.forComponent('helia:verified-fetch')
|
|
79
88
|
this.ipns = ipns ?? heliaIpns(helia, {
|
|
@@ -83,9 +92,6 @@ export class VerifiedFetch {
|
|
|
83
92
|
]
|
|
84
93
|
})
|
|
85
94
|
this.unixfs = unixfs ?? heliaUnixFs(helia)
|
|
86
|
-
this.dagJson = dagJson ?? heliaDagJson(helia)
|
|
87
|
-
this.json = json ?? heliaJson(helia)
|
|
88
|
-
this.dagCbor = dagCbor ?? heliaDagCbor(helia)
|
|
89
95
|
this.pathWalker = pathWalker ?? walkPath
|
|
90
96
|
this.contentTypeParser = init?.contentTypeParser
|
|
91
97
|
this.log.trace('created VerifiedFetch instance')
|
|
@@ -93,54 +99,48 @@ export class VerifiedFetch {
|
|
|
93
99
|
|
|
94
100
|
// handle vnd.ipfs.ipns-record
|
|
95
101
|
private async handleIPNSRecord ({ cid, path, options }: FetchHandlerFunctionArg): Promise<Response> {
|
|
96
|
-
const response =
|
|
102
|
+
const response = notSupportedResponse('vnd.ipfs.ipns-record support is not implemented')
|
|
97
103
|
response.headers.set('X-Content-Type-Options', 'nosniff') // see https://specs.ipfs.tech/http-gateways/path-gateway/#x-content-type-options-response-header
|
|
98
104
|
return response
|
|
99
105
|
}
|
|
100
106
|
|
|
101
107
|
// handle vnd.ipld.car
|
|
102
108
|
private async handleIPLDCar ({ cid, path, options }: FetchHandlerFunctionArg): Promise<Response> {
|
|
103
|
-
const response =
|
|
109
|
+
const response = notSupportedResponse('vnd.ipld.car support is not implemented')
|
|
104
110
|
response.headers.set('X-Content-Type-Options', 'nosniff') // see https://specs.ipfs.tech/http-gateways/path-gateway/#x-content-type-options-response-header
|
|
105
111
|
return response
|
|
106
112
|
}
|
|
107
113
|
|
|
108
|
-
private async handleDagJson ({ cid, path, options }: FetchHandlerFunctionArg): Promise<Response> {
|
|
109
|
-
this.log.trace('fetching %c/%s', cid, path)
|
|
110
|
-
options?.onProgress?.(new CustomProgressEvent<CIDDetail>('verified-fetch:request:start', { cid, path }))
|
|
111
|
-
const result = await this.dagJson.get(cid, {
|
|
112
|
-
signal: options?.signal,
|
|
113
|
-
onProgress: options?.onProgress
|
|
114
|
-
})
|
|
115
|
-
options?.onProgress?.(new CustomProgressEvent<CIDDetail>('verified-fetch:request:end', { cid, path }))
|
|
116
|
-
const response = new Response(JSON.stringify(result), { status: 200 })
|
|
117
|
-
response.headers.set('content-type', 'application/json')
|
|
118
|
-
return response
|
|
119
|
-
}
|
|
120
|
-
|
|
121
114
|
private async handleJson ({ cid, path, options }: FetchHandlerFunctionArg): Promise<Response> {
|
|
122
115
|
this.log.trace('fetching %c/%s', cid, path)
|
|
123
116
|
options?.onProgress?.(new CustomProgressEvent<CIDDetail>('verified-fetch:request:start', { cid, path }))
|
|
124
|
-
const result
|
|
117
|
+
const result = await this.helia.blockstore.get(cid, {
|
|
125
118
|
signal: options?.signal,
|
|
126
119
|
onProgress: options?.onProgress
|
|
127
120
|
})
|
|
128
|
-
|
|
129
|
-
const response = new Response(JSON.stringify(result), { status: 200 })
|
|
121
|
+
const response = okResponse(result)
|
|
130
122
|
response.headers.set('content-type', 'application/json')
|
|
123
|
+
options?.onProgress?.(new CustomProgressEvent<CIDDetail>('verified-fetch:request:end', { cid, path }))
|
|
131
124
|
return response
|
|
132
125
|
}
|
|
133
126
|
|
|
134
127
|
private async handleDagCbor ({ cid, path, options }: FetchHandlerFunctionArg): Promise<Response> {
|
|
135
128
|
this.log.trace('fetching %c/%s', cid, path)
|
|
136
129
|
options?.onProgress?.(new CustomProgressEvent<CIDDetail>('verified-fetch:request:start', { cid, path }))
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
130
|
+
// return body as binary
|
|
131
|
+
const block = await this.helia.blockstore.get(cid)
|
|
132
|
+
let body: string | Uint8Array
|
|
133
|
+
|
|
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)
|
|
138
|
+
body = block
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const response = okResponse(body)
|
|
142
|
+
response.headers.set('content-type', body instanceof Uint8Array ? 'application/octet-stream' : 'application/json')
|
|
141
143
|
options?.onProgress?.(new CustomProgressEvent<CIDDetail>('verified-fetch:request:end', { cid, path }))
|
|
142
|
-
const response = new Response(result, { status: 200 })
|
|
143
|
-
await this.setContentType(result, path, response)
|
|
144
144
|
return response
|
|
145
145
|
}
|
|
146
146
|
|
|
@@ -166,7 +166,7 @@ export class VerifiedFetch {
|
|
|
166
166
|
// terminalElement = stat
|
|
167
167
|
} catch (err: any) {
|
|
168
168
|
this.log('error loading path %c/%s', dirCid, rootFilePath, err)
|
|
169
|
-
return
|
|
169
|
+
return notSupportedResponse('Unable to find index.html for directory at given path. Support for directories with implicit root is not implemented')
|
|
170
170
|
} finally {
|
|
171
171
|
options?.onProgress?.(new CustomProgressEvent<CIDDetail>('verified-fetch:request:end', { cid: dirCid, path: rootFilePath }))
|
|
172
172
|
}
|
|
@@ -177,15 +177,16 @@ export class VerifiedFetch {
|
|
|
177
177
|
signal: options?.signal,
|
|
178
178
|
onProgress: options?.onProgress
|
|
179
179
|
})
|
|
180
|
-
options?.onProgress?.(new CustomProgressEvent<CIDDetail>('verified-fetch:request:end', { cid: resolvedCID, path: '' }))
|
|
181
180
|
this.log('got async iterator for %c/%s', cid, path)
|
|
182
181
|
|
|
183
182
|
const { stream, firstChunk } = await getStreamFromAsyncIterable(asyncIter, path ?? '', this.helia.logger, {
|
|
184
183
|
onProgress: options?.onProgress
|
|
185
184
|
})
|
|
186
|
-
const response =
|
|
185
|
+
const response = okResponse(stream)
|
|
187
186
|
await this.setContentType(firstChunk, path, response)
|
|
188
187
|
|
|
188
|
+
options?.onProgress?.(new CustomProgressEvent<CIDDetail>('verified-fetch:request:end', { cid: resolvedCID, path: '' }))
|
|
189
|
+
|
|
189
190
|
return response
|
|
190
191
|
}
|
|
191
192
|
|
|
@@ -193,9 +194,10 @@ export class VerifiedFetch {
|
|
|
193
194
|
this.log.trace('fetching %c/%s', cid, path)
|
|
194
195
|
options?.onProgress?.(new CustomProgressEvent<CIDDetail>('verified-fetch:request:start', { cid, path }))
|
|
195
196
|
const result = await this.helia.blockstore.get(cid)
|
|
196
|
-
|
|
197
|
-
const response = new Response(decode(result), { status: 200 })
|
|
197
|
+
const response = okResponse(result)
|
|
198
198
|
await this.setContentType(result, path, response)
|
|
199
|
+
|
|
200
|
+
options?.onProgress?.(new CustomProgressEvent<CIDDetail>('verified-fetch:request:end', { cid, path }))
|
|
199
201
|
return response
|
|
200
202
|
}
|
|
201
203
|
|
|
@@ -231,8 +233,8 @@ export class VerifiedFetch {
|
|
|
231
233
|
* @see https://specs.ipfs.tech/http-gateways/path-gateway/#format-request-query-parameter
|
|
232
234
|
* @default 'raw'
|
|
233
235
|
*/
|
|
234
|
-
private getFormat ({ headerFormat, queryFormat }: { headerFormat: string | null, queryFormat:
|
|
235
|
-
const formatMap: Record<string,
|
|
236
|
+
private getFormat ({ headerFormat, queryFormat }: { headerFormat: string | null, queryFormat: RequestFormatShorthand | null }): RequestFormatShorthand | null {
|
|
237
|
+
const formatMap: Record<string, RequestFormatShorthand> = {
|
|
236
238
|
'vnd.ipld.raw': 'raw',
|
|
237
239
|
'vnd.ipld.car': 'car',
|
|
238
240
|
'application/x-tar': 'tar',
|
|
@@ -261,22 +263,23 @@ export class VerifiedFetch {
|
|
|
261
263
|
* These format handlers should adjust the response headers as specified in https://specs.ipfs.tech/http-gateways/path-gateway/#response-headers
|
|
262
264
|
*/
|
|
263
265
|
private readonly formatHandlers: Record<string, FetchHandlerFunction> = {
|
|
264
|
-
raw: async () =>
|
|
266
|
+
raw: async () => notSupportedResponse('application/vnd.ipld.raw support is not implemented'),
|
|
265
267
|
car: this.handleIPLDCar,
|
|
266
268
|
'ipns-record': this.handleIPNSRecord,
|
|
267
|
-
tar: async () =>
|
|
268
|
-
'dag-json': async () =>
|
|
269
|
-
'dag-cbor': async () =>
|
|
270
|
-
json: async () =>
|
|
271
|
-
cbor: async () =>
|
|
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')
|
|
272
274
|
}
|
|
273
275
|
|
|
274
276
|
private readonly codecHandlers: Record<number, FetchHandlerFunction> = {
|
|
275
|
-
[dagJsonCode]: this.handleDagJson,
|
|
276
277
|
[dagPbCode]: this.handleDagPb,
|
|
278
|
+
[dagJsonCode]: this.handleJson,
|
|
277
279
|
[jsonCode]: this.handleJson,
|
|
278
280
|
[dagCborCode]: this.handleDagCbor,
|
|
279
|
-
[rawCode]: this.handleRaw
|
|
281
|
+
[rawCode]: this.handleRaw,
|
|
282
|
+
[identity.code]: this.handleRaw
|
|
280
283
|
}
|
|
281
284
|
|
|
282
285
|
async fetch (resource: Resource, opts?: VerifiedFetchOptions): Promise<Response> {
|
|
@@ -318,12 +321,12 @@ export class VerifiedFetch {
|
|
|
318
321
|
if (codecHandler != null) {
|
|
319
322
|
response = await codecHandler.call(this, { cid, path, options, terminalElement })
|
|
320
323
|
} else {
|
|
321
|
-
return
|
|
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`)
|
|
322
325
|
}
|
|
323
326
|
}
|
|
324
327
|
|
|
325
|
-
response.headers.set('etag', cid
|
|
326
|
-
response.headers.set('cache-
|
|
328
|
+
response.headers.set('etag', getETag({ cid, reqFormat: format ?? undefined, weak: false }))
|
|
329
|
+
response.headers.set('cache-control', 'public, max-age=29030400, immutable')
|
|
327
330
|
response.headers.set('X-Ipfs-Path', resource.toString()) // https://specs.ipfs.tech/http-gateways/path-gateway/#x-ipfs-path-response-header
|
|
328
331
|
|
|
329
332
|
if (ipfsRoots != null) {
|