@helia/verified-fetch 0.0.0-8a5bc6f

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 (44) hide show
  1. package/LICENSE +4 -0
  2. package/README.md +262 -0
  3. package/dist/index.min.js +140 -0
  4. package/dist/src/index.d.ts +259 -0
  5. package/dist/src/index.d.ts.map +1 -0
  6. package/dist/src/index.js +251 -0
  7. package/dist/src/index.js.map +1 -0
  8. package/dist/src/utils/get-content-type.d.ts +11 -0
  9. package/dist/src/utils/get-content-type.d.ts.map +1 -0
  10. package/dist/src/utils/get-content-type.js +43 -0
  11. package/dist/src/utils/get-content-type.js.map +1 -0
  12. package/dist/src/utils/get-stream-and-content-type.d.ts +10 -0
  13. package/dist/src/utils/get-stream-and-content-type.d.ts.map +1 -0
  14. package/dist/src/utils/get-stream-and-content-type.js +37 -0
  15. package/dist/src/utils/get-stream-and-content-type.js.map +1 -0
  16. package/dist/src/utils/parse-resource.d.ts +18 -0
  17. package/dist/src/utils/parse-resource.d.ts.map +1 -0
  18. package/dist/src/utils/parse-resource.js +24 -0
  19. package/dist/src/utils/parse-resource.js.map +1 -0
  20. package/dist/src/utils/parse-url-string.d.ts +26 -0
  21. package/dist/src/utils/parse-url-string.d.ts.map +1 -0
  22. package/dist/src/utils/parse-url-string.js +109 -0
  23. package/dist/src/utils/parse-url-string.js.map +1 -0
  24. package/dist/src/utils/tlru.d.ts +15 -0
  25. package/dist/src/utils/tlru.d.ts.map +1 -0
  26. package/dist/src/utils/tlru.js +40 -0
  27. package/dist/src/utils/tlru.js.map +1 -0
  28. package/dist/src/utils/walk-path.d.ts +13 -0
  29. package/dist/src/utils/walk-path.d.ts.map +1 -0
  30. package/dist/src/utils/walk-path.js +17 -0
  31. package/dist/src/utils/walk-path.js.map +1 -0
  32. package/dist/src/verified-fetch.d.ts +64 -0
  33. package/dist/src/verified-fetch.d.ts.map +1 -0
  34. package/dist/src/verified-fetch.js +261 -0
  35. package/dist/src/verified-fetch.js.map +1 -0
  36. package/package.json +176 -0
  37. package/src/index.ts +310 -0
  38. package/src/utils/get-content-type.ts +55 -0
  39. package/src/utils/get-stream-and-content-type.ts +44 -0
  40. package/src/utils/parse-resource.ts +40 -0
  41. package/src/utils/parse-url-string.ts +139 -0
  42. package/src/utils/tlru.ts +52 -0
  43. package/src/utils/walk-path.ts +34 -0
  44. package/src/verified-fetch.ts +323 -0
@@ -0,0 +1,139 @@
1
+ import { peerIdFromString } from '@libp2p/peer-id'
2
+ import { CID } from 'multiformats/cid'
3
+ import { TLRU } from './tlru.js'
4
+ import type { IPNS, IPNSRoutingEvents, ResolveDnsLinkProgressEvents, ResolveProgressEvents, ResolveResult } from '@helia/ipns'
5
+ import type { ComponentLogger } from '@libp2p/interface'
6
+ import type { ProgressOptions } from 'progress-events'
7
+
8
+ const ipnsCache = new TLRU<ResolveResult>(1000)
9
+
10
+ export interface ParseUrlStringInput {
11
+ urlString: string
12
+ ipns: IPNS
13
+ logger: ComponentLogger
14
+ }
15
+ export interface ParseUrlStringOptions extends ProgressOptions<ResolveProgressEvents | IPNSRoutingEvents | ResolveDnsLinkProgressEvents> {
16
+
17
+ }
18
+
19
+ export interface ParsedUrlStringResults {
20
+ protocol: string
21
+ path: string
22
+ cid: CID
23
+ query: Record<string, string>
24
+ }
25
+
26
+ const URL_REGEX = /^(?<protocol>ip[fn]s):\/\/(?<cidOrPeerIdOrDnsLink>[^/$?]+)\/?(?<path>[^$?]*)\??(?<queryString>.*)$/
27
+
28
+ /**
29
+ * A function that parses ipfs:// and ipns:// URLs, returning an object with easily recognizable properties.
30
+ *
31
+ * After determining the protocol successfully, we process the cidOrPeerIdOrDnsLink:
32
+ * * If it's ipfs, it parses the CID or throws an Aggregate error
33
+ * * If it's ipns, it attempts to resolve the PeerId and then the DNSLink. If both fail, an Aggregate error is thrown.
34
+ */
35
+ export async function parseUrlString ({ urlString, ipns, logger }: ParseUrlStringInput, options?: ParseUrlStringOptions): Promise<ParsedUrlStringResults> {
36
+ const log = logger.forComponent('helia:verified-fetch:parse-url-string')
37
+ const match = urlString.match(URL_REGEX)
38
+
39
+ if (match == null || match.groups == null) {
40
+ throw new TypeError(`Invalid URL: ${urlString}, please use ipfs:// or ipns:// URLs only.`)
41
+ }
42
+
43
+ const { protocol, cidOrPeerIdOrDnsLink, path: urlPath, queryString } = match.groups
44
+
45
+ let cid: CID | undefined
46
+ let resolvedPath: string | undefined
47
+ const errors: Error[] = []
48
+
49
+ if (protocol === 'ipfs') {
50
+ try {
51
+ cid = CID.parse(cidOrPeerIdOrDnsLink)
52
+ } catch (err) {
53
+ log.error(err)
54
+ errors.push(new TypeError('Invalid CID for ipfs://<cid> URL'))
55
+ }
56
+ } else {
57
+ let resolveResult = ipnsCache.get(cidOrPeerIdOrDnsLink)
58
+
59
+ if (resolveResult != null) {
60
+ cid = resolveResult.cid
61
+ resolvedPath = resolveResult.path
62
+ log.trace('resolved %s to %c from cache', cidOrPeerIdOrDnsLink, cid)
63
+ } else {
64
+ // protocol is ipns
65
+ log.trace('Attempting to resolve PeerId for %s', cidOrPeerIdOrDnsLink)
66
+ let peerId = null
67
+
68
+ try {
69
+ peerId = peerIdFromString(cidOrPeerIdOrDnsLink)
70
+ resolveResult = await ipns.resolve(peerId, { onProgress: options?.onProgress })
71
+ cid = resolveResult?.cid
72
+ resolvedPath = resolveResult?.path
73
+ log.trace('resolved %s to %c', cidOrPeerIdOrDnsLink, cid)
74
+ ipnsCache.set(cidOrPeerIdOrDnsLink, resolveResult, 60 * 1000 * 2)
75
+ } catch (err) {
76
+ if (peerId == null) {
77
+ log.error('Could not parse PeerId string "%s"', cidOrPeerIdOrDnsLink, err)
78
+ errors.push(new TypeError(`Could not parse PeerId in ipns url "${cidOrPeerIdOrDnsLink}", ${(err as Error).message}`))
79
+ } else {
80
+ log.error('Could not resolve PeerId %c', peerId, err)
81
+ errors.push(new TypeError(`Could not resolve PeerId "${cidOrPeerIdOrDnsLink}", ${(err as Error).message}`))
82
+ }
83
+ }
84
+
85
+ if (cid == null) {
86
+ log.trace('Attempting to resolve DNSLink for %s', cidOrPeerIdOrDnsLink)
87
+
88
+ try {
89
+ resolveResult = await ipns.resolveDns(cidOrPeerIdOrDnsLink, { onProgress: options?.onProgress })
90
+ cid = resolveResult?.cid
91
+ resolvedPath = resolveResult?.path
92
+ log.trace('resolved %s to %c', cidOrPeerIdOrDnsLink, cid)
93
+ ipnsCache.set(cidOrPeerIdOrDnsLink, resolveResult, 60 * 1000 * 2)
94
+ } catch (err) {
95
+ log.error('Could not resolve DnsLink for "%s"', cidOrPeerIdOrDnsLink, err)
96
+ errors.push(err as Error)
97
+ }
98
+ }
99
+ }
100
+ }
101
+
102
+ if (cid == null) {
103
+ throw new AggregateError(errors, `Invalid resource. Cannot determine CID from URL "${urlString}"`)
104
+ }
105
+
106
+ // parse query string
107
+ const query: Record<string, string> = {}
108
+
109
+ if (queryString != null && queryString.length > 0) {
110
+ const queryParts = queryString.split('&')
111
+ for (const part of queryParts) {
112
+ const [key, value] = part.split('=')
113
+ query[key] = decodeURIComponent(value)
114
+ }
115
+ }
116
+
117
+ /**
118
+ * join the path from resolve result & given path.
119
+ * e.g. /ipns/<peerId>/ that is resolved to /ipfs/<cid>/<path1>, when requested as /ipns/<peerId>/<path2>, should be
120
+ * resolved to /ipfs/<cid>/<path1>/<path2>
121
+ */
122
+ const pathParts = []
123
+
124
+ if (urlPath.length > 0) {
125
+ pathParts.push(urlPath)
126
+ }
127
+
128
+ if (resolvedPath != null && resolvedPath.length > 0) {
129
+ pathParts.push(resolvedPath)
130
+ }
131
+ const path = pathParts.join('/')
132
+
133
+ return {
134
+ protocol,
135
+ cid,
136
+ path,
137
+ query
138
+ }
139
+ }
@@ -0,0 +1,52 @@
1
+ import hashlru from 'hashlru'
2
+
3
+ /**
4
+ * Time Aware Least Recent Used Cache
5
+ *
6
+ * @see https://arxiv.org/pdf/1801.00390
7
+ */
8
+ export class TLRU<T> {
9
+ private readonly lru: ReturnType<typeof hashlru>
10
+
11
+ constructor (maxSize: number) {
12
+ this.lru = hashlru(maxSize)
13
+ }
14
+
15
+ get (key: string): T | undefined {
16
+ const value = this.lru.get(key)
17
+
18
+ if (value != null) {
19
+ if (value.expire != null && value.expire < Date.now()) {
20
+ this.lru.remove(key)
21
+
22
+ return undefined
23
+ }
24
+
25
+ return value.value
26
+ }
27
+
28
+ return undefined
29
+ }
30
+
31
+ set (key: string, value: T, ttl: number): void {
32
+ this.lru.set(key, { value, expire: Date.now() + ttl })
33
+ }
34
+
35
+ has (key: string): boolean {
36
+ const value = this.get(key)
37
+
38
+ if (value != null) {
39
+ return true
40
+ }
41
+
42
+ return false
43
+ }
44
+
45
+ remove (key: string): void {
46
+ this.lru.remove(key)
47
+ }
48
+
49
+ clear (): void {
50
+ this.lru.clear()
51
+ }
52
+ }
@@ -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
+ }