@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.
@@ -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: Record<string, string>
28
+ query: ParsedUrlQuery
24
29
  }
25
30
 
26
31
  const URL_REGEX = /^(?<protocol>ip[fn]s):\/\/(?<cidOrPeerIdOrDnsLink>[^/$?]+)\/?(?<path>[^$?]*)\??(?<queryString>.*)$/
@@ -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 { decode, code as rawCode } from 'multiformats/codecs/raw'
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, dagJson, json, dagCbor, pathWalker }: VerifiedFetchComponents, init?: VerifiedFetchInit) {
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 = new Response('vnd.ipfs.ipns-record support is not implemented', { status: 501 })
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 = new Response('vnd.ipld.car support is not implemented', { status: 501 })
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: Record<any, any> = await this.json.get(cid, {
117
+ const result = await this.helia.blockstore.get(cid, {
125
118
  signal: options?.signal,
126
119
  onProgress: options?.onProgress
127
120
  })
128
- options?.onProgress?.(new CustomProgressEvent<CIDDetail>('verified-fetch:request:end', { cid, path }))
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
- const result = await this.dagCbor.get<Uint8Array>(cid, {
138
- signal: options?.signal,
139
- onProgress: options?.onProgress
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 new Response('Unable to find index.html for directory at given path. Support for directories with implicit root is not implemented', { status: 501 })
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 = new Response(stream, { status: 200 })
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
- options?.onProgress?.(new CustomProgressEvent<CIDDetail>('verified-fetch:request:end', { cid, path }))
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: string | null }): string | null {
235
- const formatMap: Record<string, 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 () => new Response('application/vnd.ipld.raw support is not implemented', { status: 501 }),
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 () => new Response('application/x-tar support is not implemented', { status: 501 }),
268
- 'dag-json': async () => new Response('application/vnd.ipld.dag-json support is not implemented', { status: 501 }),
269
- 'dag-cbor': async () => new Response('application/vnd.ipld.dag-cbor support is not implemented', { status: 501 }),
270
- json: async () => new Response('application/json support is not implemented', { status: 501 }),
271
- cbor: async () => new Response('application/cbor support is not implemented', { status: 501 })
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 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 })
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.toString()) // https://specs.ipfs.tech/http-gateways/path-gateway/#etag-response-header
326
- response.headers.set('cache-cotrol', 'public, max-age=29030400, immutable')
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) {