@helia/verified-fetch 0.0.0-3851fe2

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 (49) hide show
  1. package/LICENSE +4 -0
  2. package/README.md +275 -0
  3. package/dist/index.min.js +140 -0
  4. package/dist/src/index.d.ts +271 -0
  5. package/dist/src/index.d.ts.map +1 -0
  6. package/dist/src/index.js +263 -0
  7. package/dist/src/index.js.map +1 -0
  8. package/dist/src/singleton.d.ts +3 -0
  9. package/dist/src/singleton.d.ts.map +1 -0
  10. package/dist/src/singleton.js +15 -0
  11. package/dist/src/singleton.js.map +1 -0
  12. package/dist/src/utils/get-content-type.d.ts +11 -0
  13. package/dist/src/utils/get-content-type.d.ts.map +1 -0
  14. package/dist/src/utils/get-content-type.js +43 -0
  15. package/dist/src/utils/get-content-type.js.map +1 -0
  16. package/dist/src/utils/get-stream-and-content-type.d.ts +10 -0
  17. package/dist/src/utils/get-stream-and-content-type.d.ts.map +1 -0
  18. package/dist/src/utils/get-stream-and-content-type.js +37 -0
  19. package/dist/src/utils/get-stream-and-content-type.js.map +1 -0
  20. package/dist/src/utils/parse-resource.d.ts +18 -0
  21. package/dist/src/utils/parse-resource.d.ts.map +1 -0
  22. package/dist/src/utils/parse-resource.js +24 -0
  23. package/dist/src/utils/parse-resource.js.map +1 -0
  24. package/dist/src/utils/parse-url-string.d.ts +26 -0
  25. package/dist/src/utils/parse-url-string.d.ts.map +1 -0
  26. package/dist/src/utils/parse-url-string.js +109 -0
  27. package/dist/src/utils/parse-url-string.js.map +1 -0
  28. package/dist/src/utils/tlru.d.ts +15 -0
  29. package/dist/src/utils/tlru.d.ts.map +1 -0
  30. package/dist/src/utils/tlru.js +40 -0
  31. package/dist/src/utils/tlru.js.map +1 -0
  32. package/dist/src/utils/walk-path.d.ts +13 -0
  33. package/dist/src/utils/walk-path.d.ts.map +1 -0
  34. package/dist/src/utils/walk-path.js +17 -0
  35. package/dist/src/utils/walk-path.js.map +1 -0
  36. package/dist/src/verified-fetch.d.ts +64 -0
  37. package/dist/src/verified-fetch.d.ts.map +1 -0
  38. package/dist/src/verified-fetch.js +261 -0
  39. package/dist/src/verified-fetch.js.map +1 -0
  40. package/package.json +175 -0
  41. package/src/index.ts +323 -0
  42. package/src/singleton.ts +20 -0
  43. package/src/utils/get-content-type.ts +55 -0
  44. package/src/utils/get-stream-and-content-type.ts +44 -0
  45. package/src/utils/parse-resource.ts +40 -0
  46. package/src/utils/parse-url-string.ts +139 -0
  47. package/src/utils/tlru.ts +52 -0
  48. package/src/utils/walk-path.ts +34 -0
  49. package/src/verified-fetch.ts +323 -0
@@ -0,0 +1,34 @@
1
+ import { walkPath as exporterWalk, type ExporterOptions, type ReadableStorage, type UnixFSEntry } from 'ipfs-unixfs-exporter'
2
+ import type { CID } from 'multiformats/cid'
3
+
4
+ export interface PathWalkerOptions extends ExporterOptions {
5
+
6
+ }
7
+ export interface PathWalkerResponse {
8
+ ipfsRoots: CID[]
9
+ terminalElement: UnixFSEntry
10
+
11
+ }
12
+
13
+ export interface PathWalkerFn {
14
+ (blockstore: ReadableStorage, path: string, options?: PathWalkerOptions): Promise<PathWalkerResponse>
15
+ }
16
+
17
+ export async function walkPath (blockstore: ReadableStorage, path: string, options?: PathWalkerOptions): Promise<PathWalkerResponse> {
18
+ const ipfsRoots: CID[] = []
19
+ let terminalElement: UnixFSEntry | undefined
20
+
21
+ for await (const entry of exporterWalk(path, blockstore, options)) {
22
+ ipfsRoots.push(entry.cid)
23
+ terminalElement = entry
24
+ }
25
+
26
+ if (terminalElement == null) {
27
+ throw new Error('No terminal element found')
28
+ }
29
+
30
+ return {
31
+ ipfsRoots,
32
+ terminalElement
33
+ }
34
+ }
@@ -0,0 +1,323 @@
1
+ import { dagCbor as heliaDagCbor, type DAGCBOR } from '@helia/dag-cbor'
2
+ import { dagJson as heliaDagJson, type DAGJSON } from '@helia/dag-json'
3
+ import { ipns as heliaIpns, type IPNS } from '@helia/ipns'
4
+ import { dnsJsonOverHttps } from '@helia/ipns/dns-resolvers'
5
+ import { json as heliaJson, type JSON } from '@helia/json'
6
+ import { unixfs as heliaUnixFs, type UnixFS as HeliaUnixFs, type UnixFSStats } from '@helia/unixfs'
7
+ import { code as dagCborCode } from '@ipld/dag-cbor'
8
+ import { code as dagJsonCode } from '@ipld/dag-json'
9
+ import { code as dagPbCode } from '@ipld/dag-pb'
10
+ import { code as jsonCode } from 'multiformats/codecs/json'
11
+ import { decode, code as rawCode } from 'multiformats/codecs/raw'
12
+ import { CustomProgressEvent } from 'progress-events'
13
+ import { getStreamAndContentType } from './utils/get-stream-and-content-type.js'
14
+ import { parseResource } from './utils/parse-resource.js'
15
+ import { walkPath, type PathWalkerFn } from './utils/walk-path.js'
16
+ import type { CIDDetail, Resource, VerifiedFetchInit as VerifiedFetchOptions } from './index.js'
17
+ import type { Helia } from '@helia/interface'
18
+ import type { AbortOptions, Logger } from '@libp2p/interface'
19
+ import type { UnixFSEntry } from 'ipfs-unixfs-exporter'
20
+ import type { CID } from 'multiformats/cid'
21
+
22
+ interface VerifiedFetchComponents {
23
+ helia: Helia
24
+ ipns?: IPNS
25
+ unixfs?: HeliaUnixFs
26
+ dagJson?: DAGJSON
27
+ json?: JSON
28
+ dagCbor?: DAGCBOR
29
+ pathWalker?: PathWalkerFn
30
+ }
31
+
32
+ /**
33
+ * Potential future options for the VerifiedFetch constructor.
34
+ */
35
+ // eslint-disable-next-line @typescript-eslint/no-empty-interface
36
+ interface VerifiedFetchInit {
37
+
38
+ }
39
+
40
+ interface FetchHandlerFunctionArg {
41
+ cid: CID
42
+ path: string
43
+ terminalElement?: UnixFSEntry
44
+ options?: Omit<VerifiedFetchOptions, 'signal'> & AbortOptions
45
+ }
46
+
47
+ interface FetchHandlerFunction {
48
+ (options: FetchHandlerFunctionArg): Promise<Response>
49
+ }
50
+
51
+ function convertOptions (options?: VerifiedFetchOptions): (Omit<VerifiedFetchOptions, 'signal'> & AbortOptions) | undefined {
52
+ if (options == null) {
53
+ return undefined
54
+ }
55
+
56
+ let signal: AbortSignal | undefined
57
+ if (options?.signal === null) {
58
+ signal = undefined
59
+ }
60
+ return {
61
+ ...options,
62
+ signal
63
+ }
64
+ }
65
+
66
+ export class VerifiedFetch {
67
+ private readonly helia: Helia
68
+ private readonly ipns: IPNS
69
+ private readonly unixfs: HeliaUnixFs
70
+ private readonly dagJson: DAGJSON
71
+ private readonly dagCbor: DAGCBOR
72
+ private readonly json: JSON
73
+ private readonly pathWalker: PathWalkerFn
74
+ private readonly log: Logger
75
+
76
+ constructor ({ helia, ipns, unixfs, dagJson, json, dagCbor, pathWalker }: VerifiedFetchComponents, init?: VerifiedFetchInit) {
77
+ this.helia = helia
78
+ this.log = helia.logger.forComponent('helia:verified-fetch')
79
+ this.ipns = ipns ?? heliaIpns(helia, {
80
+ resolvers: [
81
+ dnsJsonOverHttps('https://mozilla.cloudflare-dns.com/dns-query'),
82
+ dnsJsonOverHttps('https://dns.google/resolve')
83
+ ]
84
+ })
85
+ this.unixfs = unixfs ?? heliaUnixFs(helia)
86
+ this.dagJson = dagJson ?? heliaDagJson(helia)
87
+ this.json = json ?? heliaJson(helia)
88
+ this.dagCbor = dagCbor ?? heliaDagCbor(helia)
89
+ this.pathWalker = pathWalker ?? walkPath
90
+ this.log.trace('created VerifiedFetch instance')
91
+ }
92
+
93
+ // handle vnd.ipfs.ipns-record
94
+ private async handleIPNSRecord ({ cid, path, options }: FetchHandlerFunctionArg): Promise<Response> {
95
+ const response = new Response('vnd.ipfs.ipns-record support is not implemented', { status: 501 })
96
+ response.headers.set('X-Content-Type-Options', 'nosniff') // see https://specs.ipfs.tech/http-gateways/path-gateway/#x-content-type-options-response-header
97
+ return response
98
+ }
99
+
100
+ // handle vnd.ipld.car
101
+ private async handleIPLDCar ({ cid, path, options }: FetchHandlerFunctionArg): Promise<Response> {
102
+ const response = new Response('vnd.ipld.car support is not implemented', { status: 501 })
103
+ response.headers.set('X-Content-Type-Options', 'nosniff') // see https://specs.ipfs.tech/http-gateways/path-gateway/#x-content-type-options-response-header
104
+ return response
105
+ }
106
+
107
+ private async handleDagJson ({ cid, path, options }: FetchHandlerFunctionArg): Promise<Response> {
108
+ this.log.trace('fetching %c/%s', cid, path)
109
+ options?.onProgress?.(new CustomProgressEvent<CIDDetail>('verified-fetch:request:start', { cid: cid.toString(), path }))
110
+ const result = await this.dagJson.get(cid, {
111
+ signal: options?.signal,
112
+ onProgress: options?.onProgress
113
+ })
114
+ options?.onProgress?.(new CustomProgressEvent<CIDDetail>('verified-fetch:request:end', { cid: cid.toString(), path }))
115
+ const response = new Response(JSON.stringify(result), { status: 200 })
116
+ response.headers.set('content-type', 'application/json')
117
+ return response
118
+ }
119
+
120
+ private async handleJson ({ cid, path, options }: FetchHandlerFunctionArg): Promise<Response> {
121
+ this.log.trace('fetching %c/%s', cid, path)
122
+ options?.onProgress?.(new CustomProgressEvent<CIDDetail>('verified-fetch:request:start', { cid: cid.toString(), path }))
123
+ const result: Record<any, any> = await this.json.get(cid, {
124
+ signal: options?.signal,
125
+ onProgress: options?.onProgress
126
+ })
127
+ options?.onProgress?.(new CustomProgressEvent<CIDDetail>('verified-fetch:request:end', { cid: cid.toString(), path }))
128
+ const response = new Response(JSON.stringify(result), { status: 200 })
129
+ response.headers.set('content-type', 'application/json')
130
+ return response
131
+ }
132
+
133
+ private async handleDagCbor ({ cid, path, options }: FetchHandlerFunctionArg): Promise<Response> {
134
+ this.log.trace('fetching %c/%s', cid, path)
135
+ options?.onProgress?.(new CustomProgressEvent<CIDDetail>('verified-fetch:request:start', { cid: cid.toString(), path }))
136
+ const result = await this.dagCbor.get(cid, {
137
+ signal: options?.signal,
138
+ onProgress: options?.onProgress
139
+ })
140
+ options?.onProgress?.(new CustomProgressEvent<CIDDetail>('verified-fetch:request:end', { cid: cid.toString(), path }))
141
+ const response = new Response(JSON.stringify(result), { status: 200 })
142
+ response.headers.set('content-type', 'application/json')
143
+ return response
144
+ }
145
+
146
+ private async handleDagPb ({ cid, path, options, terminalElement }: FetchHandlerFunctionArg): Promise<Response> {
147
+ this.log.trace('fetching %c/%s', cid, path)
148
+ let resolvedCID = terminalElement?.cid ?? cid
149
+ let stat: UnixFSStats
150
+ if (terminalElement?.type === 'directory') {
151
+ const dirCid = terminalElement.cid
152
+
153
+ const rootFilePath = 'index.html'
154
+ try {
155
+ this.log.trace('found directory at %c/%s, looking for index.html', cid, path)
156
+ options?.onProgress?.(new CustomProgressEvent<CIDDetail>('verified-fetch:request:start', { cid: dirCid.toString(), path: rootFilePath }))
157
+ stat = await this.unixfs.stat(dirCid, {
158
+ path: rootFilePath,
159
+ signal: options?.signal,
160
+ onProgress: options?.onProgress
161
+ })
162
+ this.log.trace('found root file at %c/%s with cid %c', dirCid, rootFilePath, stat.cid)
163
+ path = rootFilePath
164
+ resolvedCID = stat.cid
165
+ // terminalElement = stat
166
+ } catch (err: any) {
167
+ this.log('error loading path %c/%s', dirCid, rootFilePath, err)
168
+ return new Response('Unable to find index.html for directory at given path. Support for directories with implicit root is not implemented', { status: 501 })
169
+ } finally {
170
+ options?.onProgress?.(new CustomProgressEvent<CIDDetail>('verified-fetch:request:end', { cid: dirCid.toString(), path: rootFilePath }))
171
+ }
172
+ }
173
+
174
+ options?.onProgress?.(new CustomProgressEvent<CIDDetail>('verified-fetch:request:start', { cid: resolvedCID.toString(), path: '' }))
175
+ const asyncIter = this.unixfs.cat(resolvedCID, {
176
+ signal: options?.signal,
177
+ onProgress: options?.onProgress
178
+ })
179
+ options?.onProgress?.(new CustomProgressEvent<CIDDetail>('verified-fetch:request:end', { cid: resolvedCID.toString(), path: '' }))
180
+ this.log('got async iterator for %c/%s', cid, path)
181
+
182
+ const { contentType, stream } = await getStreamAndContentType(asyncIter, path ?? '', this.helia.logger, {
183
+ onProgress: options?.onProgress
184
+ })
185
+ const response = new Response(stream, { status: 200 })
186
+ response.headers.set('content-type', contentType)
187
+
188
+ return response
189
+ }
190
+
191
+ private async handleRaw ({ cid, path, options }: FetchHandlerFunctionArg): Promise<Response> {
192
+ this.log.trace('fetching %c/%s', cid, path)
193
+ options?.onProgress?.(new CustomProgressEvent<CIDDetail>('verified-fetch:request:start', { cid: cid.toString(), path }))
194
+ const result = await this.helia.blockstore.get(cid)
195
+ options?.onProgress?.(new CustomProgressEvent<CIDDetail>('verified-fetch:request:end', { cid: cid.toString(), path }))
196
+ const response = new Response(decode(result), { status: 200 })
197
+ response.headers.set('content-type', 'application/octet-stream')
198
+ return response
199
+ }
200
+
201
+ /**
202
+ * Determines the format requested by the client, defaults to `null` if no format is requested.
203
+ *
204
+ * @see https://specs.ipfs.tech/http-gateways/path-gateway/#format-request-query-parameter
205
+ * @default 'raw'
206
+ */
207
+ private getFormat ({ headerFormat, queryFormat }: { headerFormat: string | null, queryFormat: string | null }): string | null {
208
+ const formatMap: Record<string, string> = {
209
+ 'vnd.ipld.raw': 'raw',
210
+ 'vnd.ipld.car': 'car',
211
+ 'application/x-tar': 'tar',
212
+ 'application/vnd.ipld.dag-json': 'dag-json',
213
+ 'application/vnd.ipld.dag-cbor': 'dag-cbor',
214
+ 'application/json': 'json',
215
+ 'application/cbor': 'cbor',
216
+ 'vnd.ipfs.ipns-record': 'ipns-record'
217
+ }
218
+
219
+ if (headerFormat != null) {
220
+ for (const format in formatMap) {
221
+ if (headerFormat.includes(format)) {
222
+ return formatMap[format]
223
+ }
224
+ }
225
+ } else if (queryFormat != null) {
226
+ return queryFormat
227
+ }
228
+
229
+ return null
230
+ }
231
+
232
+ /**
233
+ * Map of format to specific handlers for that format.
234
+ * These format handlers should adjust the response headers as specified in https://specs.ipfs.tech/http-gateways/path-gateway/#response-headers
235
+ */
236
+ private readonly formatHandlers: Record<string, FetchHandlerFunction> = {
237
+ raw: async () => new Response('application/vnd.ipld.raw support is not implemented', { status: 501 }),
238
+ car: this.handleIPLDCar,
239
+ 'ipns-record': this.handleIPNSRecord,
240
+ tar: async () => new Response('application/x-tar support is not implemented', { status: 501 }),
241
+ 'dag-json': async () => new Response('application/vnd.ipld.dag-json support is not implemented', { status: 501 }),
242
+ 'dag-cbor': async () => new Response('application/vnd.ipld.dag-cbor support is not implemented', { status: 501 }),
243
+ json: async () => new Response('application/json support is not implemented', { status: 501 }),
244
+ cbor: async () => new Response('application/cbor support is not implemented', { status: 501 })
245
+ }
246
+
247
+ private readonly codecHandlers: Record<number, FetchHandlerFunction> = {
248
+ [dagJsonCode]: this.handleDagJson,
249
+ [dagPbCode]: this.handleDagPb,
250
+ [jsonCode]: this.handleJson,
251
+ [dagCborCode]: this.handleDagCbor,
252
+ [rawCode]: this.handleRaw
253
+ }
254
+
255
+ async fetch (resource: Resource, opts?: VerifiedFetchOptions): Promise<Response> {
256
+ const options = convertOptions(opts)
257
+ const { path, query, ...rest } = await parseResource(resource, { ipns: this.ipns, logger: this.helia.logger }, options)
258
+ const cid = rest.cid
259
+ let response: Response | undefined
260
+
261
+ const format = this.getFormat({ headerFormat: new Headers(options?.headers).get('accept'), queryFormat: query.format ?? null })
262
+
263
+ if (format != null) {
264
+ // TODO: These should be handled last when they're returning something other than 501
265
+ const formatHandler = this.formatHandlers[format]
266
+
267
+ if (formatHandler != null) {
268
+ response = await formatHandler.call(this, { cid, path, options })
269
+
270
+ if (response.status === 501) {
271
+ return response
272
+ }
273
+ }
274
+ }
275
+
276
+ let terminalElement: UnixFSEntry | undefined
277
+ let ipfsRoots: CID[] | undefined
278
+
279
+ try {
280
+ const pathDetails = await this.pathWalker(this.helia.blockstore, `${cid.toString()}/${path}`, options)
281
+ ipfsRoots = pathDetails.ipfsRoots
282
+ terminalElement = pathDetails.terminalElement
283
+ } catch (err) {
284
+ this.log.error('Error walking path %s', path, err)
285
+ // return new Response(`Error walking path: ${(err as Error).message}`, { status: 500 })
286
+ }
287
+
288
+ if (response == null) {
289
+ const codecHandler = this.codecHandlers[cid.code]
290
+
291
+ if (codecHandler != null) {
292
+ response = await codecHandler.call(this, { cid, path, options, terminalElement })
293
+ } else {
294
+ return new Response(`Support for codec with code ${cid.code} is not yet implemented. Please open an issue at https://github.com/ipfs/helia/issues/new`, { status: 501 })
295
+ }
296
+ }
297
+
298
+ response.headers.set('etag', cid.toString()) // https://specs.ipfs.tech/http-gateways/path-gateway/#etag-response-header
299
+ response.headers.set('cache-cotrol', 'public, max-age=29030400, immutable')
300
+ response.headers.set('X-Ipfs-Path', resource.toString()) // https://specs.ipfs.tech/http-gateways/path-gateway/#x-ipfs-path-response-header
301
+
302
+ if (ipfsRoots != null) {
303
+ 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
304
+ }
305
+ // response.headers.set('Content-Disposition', `TODO`) // https://specs.ipfs.tech/http-gateways/path-gateway/#content-disposition-response-header
306
+
307
+ return response
308
+ }
309
+
310
+ /**
311
+ * Start the Helia instance
312
+ */
313
+ async start (): Promise<void> {
314
+ await this.helia.start()
315
+ }
316
+
317
+ /**
318
+ * Shut down the Helia instance
319
+ */
320
+ async stop (): Promise<void> {
321
+ await this.helia.stop()
322
+ }
323
+ }