@helia/verified-fetch 3.2.3 → 4.0.1

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 (165) hide show
  1. package/README.md +10 -52
  2. package/dist/index.min.js +86 -71
  3. package/dist/index.min.js.map +4 -4
  4. package/dist/src/constants.d.ts +2 -0
  5. package/dist/src/constants.d.ts.map +1 -0
  6. package/dist/src/constants.js +2 -0
  7. package/dist/src/constants.js.map +1 -0
  8. package/dist/src/index.d.ts +63 -61
  9. package/dist/src/index.d.ts.map +1 -1
  10. package/dist/src/index.js +12 -54
  11. package/dist/src/index.js.map +1 -1
  12. package/dist/src/plugins/index.d.ts +0 -1
  13. package/dist/src/plugins/index.d.ts.map +1 -1
  14. package/dist/src/plugins/index.js +0 -1
  15. package/dist/src/plugins/index.js.map +1 -1
  16. package/dist/src/plugins/plugin-base.d.ts.map +1 -1
  17. package/dist/src/plugins/plugin-base.js +3 -2
  18. package/dist/src/plugins/plugin-base.js.map +1 -1
  19. package/dist/src/plugins/plugin-handle-car.d.ts.map +1 -1
  20. package/dist/src/plugins/plugin-handle-car.js +37 -28
  21. package/dist/src/plugins/plugin-handle-car.js.map +1 -1
  22. package/dist/src/plugins/plugin-handle-dag-cbor-html-preview.d.ts +1 -1
  23. package/dist/src/plugins/plugin-handle-dag-cbor-html-preview.d.ts.map +1 -1
  24. package/dist/src/plugins/plugin-handle-dag-cbor-html-preview.js +1 -2
  25. package/dist/src/plugins/plugin-handle-dag-cbor-html-preview.js.map +1 -1
  26. package/dist/src/plugins/plugin-handle-dag-cbor.d.ts.map +1 -1
  27. package/dist/src/plugins/plugin-handle-dag-cbor.js +5 -6
  28. package/dist/src/plugins/plugin-handle-dag-cbor.js.map +1 -1
  29. package/dist/src/plugins/plugin-handle-dag-pb.d.ts.map +1 -1
  30. package/dist/src/plugins/plugin-handle-dag-pb.js +24 -27
  31. package/dist/src/plugins/plugin-handle-dag-pb.js.map +1 -1
  32. package/dist/src/plugins/plugin-handle-dag-walk.d.ts +8 -4
  33. package/dist/src/plugins/plugin-handle-dag-walk.d.ts.map +1 -1
  34. package/dist/src/plugins/plugin-handle-dag-walk.js +13 -9
  35. package/dist/src/plugins/plugin-handle-dag-walk.js.map +1 -1
  36. package/dist/src/plugins/plugin-handle-ipns-record.d.ts +1 -1
  37. package/dist/src/plugins/plugin-handle-ipns-record.d.ts.map +1 -1
  38. package/dist/src/plugins/plugin-handle-ipns-record.js +16 -24
  39. package/dist/src/plugins/plugin-handle-ipns-record.js.map +1 -1
  40. package/dist/src/plugins/plugin-handle-json.d.ts.map +1 -1
  41. package/dist/src/plugins/plugin-handle-json.js +5 -5
  42. package/dist/src/plugins/plugin-handle-json.js.map +1 -1
  43. package/dist/src/plugins/plugin-handle-raw.d.ts.map +1 -1
  44. package/dist/src/plugins/plugin-handle-raw.js +21 -12
  45. package/dist/src/plugins/plugin-handle-raw.js.map +1 -1
  46. package/dist/src/plugins/plugin-handle-tar.d.ts.map +1 -1
  47. package/dist/src/plugins/plugin-handle-tar.js +1 -2
  48. package/dist/src/plugins/plugin-handle-tar.js.map +1 -1
  49. package/dist/src/plugins/types.d.ts +15 -15
  50. package/dist/src/plugins/types.d.ts.map +1 -1
  51. package/dist/src/url-resolver.d.ts +21 -0
  52. package/dist/src/url-resolver.d.ts.map +1 -0
  53. package/dist/src/url-resolver.js +118 -0
  54. package/dist/src/url-resolver.js.map +1 -0
  55. package/dist/src/utils/byte-range-context.d.ts +3 -3
  56. package/dist/src/utils/byte-range-context.d.ts.map +1 -1
  57. package/dist/src/utils/byte-range-context.js +1 -1
  58. package/dist/src/utils/byte-range-context.js.map +1 -1
  59. package/dist/src/utils/content-type-parser.d.ts.map +1 -1
  60. package/dist/src/utils/content-type-parser.js +0 -10
  61. package/dist/src/utils/content-type-parser.js.map +1 -1
  62. package/dist/src/utils/error-to-object.d.ts +6 -0
  63. package/dist/src/utils/error-to-object.d.ts.map +1 -0
  64. package/dist/src/utils/error-to-object.js +20 -0
  65. package/dist/src/utils/error-to-object.js.map +1 -0
  66. package/dist/src/utils/get-content-type.d.ts +3 -3
  67. package/dist/src/utils/get-content-type.d.ts.map +1 -1
  68. package/dist/src/utils/get-content-type.js +1 -1
  69. package/dist/src/utils/get-content-type.js.map +1 -1
  70. package/dist/src/utils/get-e-tag.d.ts +1 -1
  71. package/dist/src/utils/get-offset-and-length.d.ts +6 -0
  72. package/dist/src/utils/get-offset-and-length.d.ts.map +1 -0
  73. package/dist/src/utils/get-offset-and-length.js +46 -0
  74. package/dist/src/utils/get-offset-and-length.js.map +1 -0
  75. package/dist/src/utils/get-resolved-accept-header.d.ts +2 -2
  76. package/dist/src/utils/get-resolved-accept-header.d.ts.map +1 -1
  77. package/dist/src/utils/get-stream-from-async-iterable.d.ts +2 -2
  78. package/dist/src/utils/get-stream-from-async-iterable.d.ts.map +1 -1
  79. package/dist/src/utils/get-stream-from-async-iterable.js +2 -2
  80. package/dist/src/utils/get-stream-from-async-iterable.js.map +1 -1
  81. package/dist/src/utils/handle-redirects.d.ts.map +1 -1
  82. package/dist/src/utils/handle-redirects.js +3 -3
  83. package/dist/src/utils/handle-redirects.js.map +1 -1
  84. package/dist/src/utils/ipfs-path-to-string.d.ts +6 -0
  85. package/dist/src/utils/ipfs-path-to-string.d.ts.map +1 -0
  86. package/dist/src/utils/ipfs-path-to-string.js +10 -0
  87. package/dist/src/utils/ipfs-path-to-string.js.map +1 -0
  88. package/dist/src/utils/is-accept-explicit.d.ts +6 -4
  89. package/dist/src/utils/is-accept-explicit.d.ts.map +1 -1
  90. package/dist/src/utils/is-accept-explicit.js +7 -4
  91. package/dist/src/utils/is-accept-explicit.js.map +1 -1
  92. package/dist/src/utils/parse-url-string.d.ts +1 -55
  93. package/dist/src/utils/parse-url-string.d.ts.map +1 -1
  94. package/dist/src/utils/parse-url-string.js +16 -217
  95. package/dist/src/utils/parse-url-string.js.map +1 -1
  96. package/dist/src/utils/response-headers.d.ts +1 -1
  97. package/dist/src/utils/response-headers.d.ts.map +1 -1
  98. package/dist/src/utils/responses.d.ts +3 -2
  99. package/dist/src/utils/responses.d.ts.map +1 -1
  100. package/dist/src/utils/responses.js +12 -1
  101. package/dist/src/utils/responses.js.map +1 -1
  102. package/dist/src/utils/select-output-type.d.ts +6 -2
  103. package/dist/src/utils/select-output-type.d.ts.map +1 -1
  104. package/dist/src/utils/select-output-type.js +28 -37
  105. package/dist/src/utils/select-output-type.js.map +1 -1
  106. package/dist/src/utils/server-timing.d.ts +5 -11
  107. package/dist/src/utils/server-timing.d.ts.map +1 -1
  108. package/dist/src/utils/server-timing.js +17 -15
  109. package/dist/src/utils/server-timing.js.map +1 -1
  110. package/dist/src/utils/walk-path.js +2 -2
  111. package/dist/src/utils/walk-path.js.map +1 -1
  112. package/dist/src/verified-fetch.d.ts +3 -10
  113. package/dist/src/verified-fetch.d.ts.map +1 -1
  114. package/dist/src/verified-fetch.js +99 -80
  115. package/dist/src/verified-fetch.js.map +1 -1
  116. package/dist/typedoc-urls.json +13 -4
  117. package/package.json +35 -36
  118. package/src/constants.ts +1 -0
  119. package/src/index.ts +79 -70
  120. package/src/plugins/index.ts +0 -1
  121. package/src/plugins/plugin-base.ts +3 -2
  122. package/src/plugins/plugin-handle-car.ts +53 -31
  123. package/src/plugins/plugin-handle-dag-cbor-html-preview.ts +4 -3
  124. package/src/plugins/plugin-handle-dag-cbor.ts +8 -6
  125. package/src/plugins/plugin-handle-dag-pb.ts +34 -26
  126. package/src/plugins/plugin-handle-dag-walk.ts +15 -9
  127. package/src/plugins/plugin-handle-ipns-record.ts +21 -24
  128. package/src/plugins/plugin-handle-json.ts +6 -5
  129. package/src/plugins/plugin-handle-raw.ts +27 -13
  130. package/src/plugins/plugin-handle-tar.ts +3 -2
  131. package/src/plugins/types.ts +18 -16
  132. package/src/url-resolver.ts +159 -0
  133. package/src/utils/byte-range-context.ts +4 -4
  134. package/src/utils/content-type-parser.ts +5 -11
  135. package/src/utils/error-to-object.ts +22 -0
  136. package/src/utils/get-content-type.ts +5 -4
  137. package/src/utils/get-e-tag.ts +1 -1
  138. package/src/utils/get-offset-and-length.ts +54 -0
  139. package/src/utils/get-resolved-accept-header.ts +2 -2
  140. package/src/utils/get-stream-from-async-iterable.ts +4 -4
  141. package/src/utils/handle-redirects.ts +10 -3
  142. package/src/utils/ipfs-path-to-string.ts +9 -0
  143. package/src/utils/is-accept-explicit.ts +14 -7
  144. package/src/utils/parse-url-string.ts +20 -286
  145. package/src/utils/response-headers.ts +1 -1
  146. package/src/utils/responses.ts +16 -2
  147. package/src/utils/select-output-type.ts +38 -44
  148. package/src/utils/server-timing.ts +17 -30
  149. package/src/utils/walk-path.ts +2 -2
  150. package/src/verified-fetch.ts +119 -92
  151. package/dist/src/plugins/errors.d.ts +0 -25
  152. package/dist/src/plugins/errors.d.ts.map +0 -1
  153. package/dist/src/plugins/errors.js +0 -33
  154. package/dist/src/plugins/errors.js.map +0 -1
  155. package/dist/src/types.d.ts +0 -16
  156. package/dist/src/types.d.ts.map +0 -1
  157. package/dist/src/types.js +0 -2
  158. package/dist/src/types.js.map +0 -1
  159. package/dist/src/utils/parse-resource.d.ts +0 -18
  160. package/dist/src/utils/parse-resource.d.ts.map +0 -1
  161. package/dist/src/utils/parse-resource.js +0 -27
  162. package/dist/src/utils/parse-resource.js.map +0 -1
  163. package/src/plugins/errors.ts +0 -37
  164. package/src/types.ts +0 -17
  165. package/src/utils/parse-resource.ts +0 -42
@@ -3,7 +3,7 @@ import { code as dagJsonCode } from '@ipld/dag-json'
3
3
  import { code as dagPbCode } from '@ipld/dag-pb'
4
4
  import { code as jsonCode } from 'multiformats/codecs/json'
5
5
  import { code as rawCode } from 'multiformats/codecs/raw'
6
- import type { RequestFormatShorthand } from '../types.js'
6
+ import type { RequestFormatShorthand } from '../index.js'
7
7
  import type { CID } from 'multiformats/cid'
8
8
 
9
9
  /**
@@ -62,10 +62,15 @@ const CID_TYPE_MAP: Record<number, string[]> = {
62
62
  ]
63
63
  }
64
64
 
65
+ export interface AcceptHeader {
66
+ mimeType: string
67
+ options: Record<string, string>
68
+ }
69
+
65
70
  /**
66
71
  * Selects an output mime-type based on the CID and a passed `Accept` header
67
72
  */
68
- export function selectOutputType (cid: CID, accept?: string): string | undefined {
73
+ export function selectOutputType (cid: CID, accept?: string): AcceptHeader | undefined {
69
74
  const cidMimeTypes = CID_TYPE_MAP[cid.code]
70
75
 
71
76
  if (accept != null) {
@@ -73,80 +78,69 @@ export function selectOutputType (cid: CID, accept?: string): string | undefined
73
78
  }
74
79
  }
75
80
 
76
- function chooseMimeType (accept: string, validMimeTypes: string[]): string | undefined {
81
+ function chooseMimeType (accept: string, validMimeTypes: string[]): AcceptHeader | undefined {
77
82
  const requestedMimeTypes = accept
78
83
  .split(',')
79
84
  .map(s => {
80
85
  const parts = s.trim().split(';')
81
86
 
87
+ const options: Record<string, string> = {
88
+ q: '0'
89
+ }
90
+
91
+ for (let i = 1; i < parts.length; i++) {
92
+ const [key, value] = parts[i].split('=').map(s => s.trim())
93
+
94
+ options[key] = value
95
+ }
96
+
82
97
  return {
83
98
  mimeType: `${parts[0]}`.trim(),
84
- weight: parseQFactor(parts[1])
99
+ options
85
100
  }
86
101
  })
87
102
  .sort((a, b) => {
88
- if (a.weight === b.weight) {
103
+ if (a.options.q === b.options.q) {
89
104
  return 0
90
105
  }
91
106
 
92
- if (a.weight > b.weight) {
107
+ if (a.options.q > b.options.q) {
93
108
  return -1
94
109
  }
95
110
 
96
111
  return 1
97
112
  })
98
- .map(s => s.mimeType)
99
113
 
100
114
  for (const headerFormat of requestedMimeTypes) {
101
115
  for (const mimeType of validMimeTypes) {
102
- if (headerFormat.includes(mimeType)) {
103
- return mimeType
116
+ if (headerFormat.mimeType.includes(mimeType)) {
117
+ return headerFormat
104
118
  }
105
119
 
106
- if (headerFormat === '*/*') {
107
- return mimeType
120
+ if (headerFormat.mimeType === '*/*') {
121
+ return {
122
+ mimeType,
123
+ options: headerFormat.options
124
+ }
108
125
  }
109
126
 
110
- if (headerFormat.startsWith('*/') && mimeType.split('/')[1] === headerFormat.split('/')[1]) {
111
- return mimeType
127
+ if (headerFormat.mimeType.startsWith('*/') && mimeType.split('/')[1] === headerFormat.mimeType.split('/')[1]) {
128
+ return {
129
+ mimeType,
130
+ options: headerFormat.options
131
+ }
112
132
  }
113
133
 
114
- if (headerFormat.endsWith('/*') && mimeType.split('/')[0] === headerFormat.split('/')[0]) {
115
- return mimeType
134
+ if (headerFormat.mimeType.endsWith('/*') && mimeType.split('/')[0] === headerFormat.mimeType.split('/')[0]) {
135
+ return {
136
+ mimeType,
137
+ options: headerFormat.options
138
+ }
116
139
  }
117
140
  }
118
141
  }
119
142
  }
120
143
 
121
- /**
122
- * Parses q-factor weighting from the accept header to allow letting some mime
123
- * types take precedence over others.
124
- *
125
- * If the q-factor for an acceptable mime representation is omitted it defaults
126
- * to `1`.
127
- *
128
- * All specified values should be in the range 0-1.
129
- *
130
- * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept#q
131
- */
132
- function parseQFactor (str?: string): number {
133
- if (str != null) {
134
- str = str.trim()
135
- }
136
-
137
- if (str?.startsWith('q=') !== true) {
138
- return 1
139
- }
140
-
141
- const factor = parseFloat(str.replace('q=', ''))
142
-
143
- if (isNaN(factor)) {
144
- return 0
145
- }
146
-
147
- return factor
148
- }
149
-
150
144
  export const FORMAT_TO_MIME_TYPE: Record<RequestFormatShorthand, string> = {
151
145
  raw: 'application/vnd.ipld.raw',
152
146
  car: 'application/vnd.ipld.car',
@@ -1,37 +1,24 @@
1
- export interface ServerTimingSuccess<T> {
2
- error: null
3
- result: T
4
- header: string
5
- }
6
- export interface ServerTimingError {
7
- result: null
8
- error: Error
9
- header: string
10
- }
11
- export type ServerTimingResult<T> = ServerTimingSuccess<T> | ServerTimingError
1
+ export class ServerTiming {
2
+ private headers: string[]
12
3
 
13
- export async function serverTiming<T> (
14
- name: string,
15
- description: string,
16
- fn: () => Promise<T>
17
- ): Promise<ServerTimingResult<T>> {
18
- const startTime = performance.now()
4
+ constructor () {
5
+ this.headers = []
6
+ }
19
7
 
20
- try {
21
- const result = await fn() // Execute the function
22
- const endTime = performance.now()
8
+ getHeader (): string {
9
+ return this.headers.join(',')
10
+ }
23
11
 
24
- const duration = (endTime - startTime).toFixed(1) // Duration in milliseconds
12
+ async time <T> (name: string, description: string, promise: Promise<T>): Promise<T> {
13
+ const startTime = performance.now()
25
14
 
26
- // Create the Server-Timing header string
27
- const header = `${name};dur=${duration};desc="${description}"`
28
- return { result, header, error: null }
29
- } catch (error: any) {
30
- const endTime = performance.now()
31
- const duration = (endTime - startTime).toFixed(1)
15
+ try {
16
+ return await promise // Execute the function
17
+ } finally {
18
+ const endTime = performance.now()
19
+ const duration = (endTime - startTime).toFixed(1) // Duration in milliseconds
32
20
 
33
- // Still return a timing header even on error
34
- const header = `${name};dur=${duration};desc="${description}"`
35
- return { result: null, error, header } // Pass error with timing info
21
+ this.headers.push(`${name};dur=${duration};desc="${description}"`)
22
+ }
36
23
  }
37
24
  }
@@ -52,7 +52,7 @@ export function isObjectNode (node: UnixFSEntry): node is ObjectNode {
52
52
  */
53
53
  export async function handlePathWalking ({ cid, path, resource, options, blockstore, log }: PluginContext & { blockstore: Blockstore, log: Logger }): Promise<PathWalkerResponse | Response> {
54
54
  try {
55
- return await walkPath(blockstore, `${cid.toString()}/${path}`, options)
55
+ return await walkPath(blockstore, `${cid}/${path}`, options)
56
56
  } catch (err: any) {
57
57
  if (options?.signal?.aborted) {
58
58
  throw new AbortError(options?.signal?.reason)
@@ -62,7 +62,7 @@ export async function handlePathWalking ({ cid, path, resource, options, blockst
62
62
  return notFoundResponse(resource)
63
63
  }
64
64
 
65
- log.error('error walking path %s', path, err)
65
+ log.error('error walking path "%s" - %e', path, err)
66
66
  return badGatewayResponse(resource, 'Error walking path')
67
67
  }
68
68
  }
@@ -1,6 +1,6 @@
1
- import { ipns as heliaIpns } from '@helia/ipns'
1
+ import { dnsLink } from '@helia/dnslink'
2
+ import { ipnsResolver } from '@helia/ipns'
2
3
  import { AbortError } from '@libp2p/interface'
3
- import { prefixLogger } from '@libp2p/logger'
4
4
  import { CustomProgressEvent } from 'progress-events'
5
5
  import QuickLRU from 'quick-lru'
6
6
  import { ByteRangeContextPlugin } from './plugins/plugin-handle-byte-range-context.js'
@@ -12,22 +12,25 @@ import { IpnsRecordPlugin } from './plugins/plugin-handle-ipns-record.js'
12
12
  import { JsonPlugin } from './plugins/plugin-handle-json.js'
13
13
  import { RawPlugin } from './plugins/plugin-handle-raw.js'
14
14
  import { TarPlugin } from './plugins/plugin-handle-tar.js'
15
+ import { URLResolver } from './url-resolver.ts'
15
16
  import { contentTypeParser } from './utils/content-type-parser.js'
17
+ import { errorToObject } from './utils/error-to-object.ts'
16
18
  import { getContentDispositionFilename } from './utils/get-content-disposition-filename.js'
17
19
  import { getETag } from './utils/get-e-tag.js'
18
20
  import { getResolvedAcceptHeader } from './utils/get-resolved-accept-header.js'
19
21
  import { getRedirectResponse } from './utils/handle-redirects.js'
20
- import { parseResource } from './utils/parse-resource.js'
22
+ import { uriEncodeIPFSPath } from './utils/ipfs-path-to-string.ts'
21
23
  import { resourceToSessionCacheKey } from './utils/resource-to-cache-key.js'
22
24
  import { setCacheControlHeader } from './utils/response-headers.js'
23
- import { badRequestResponse, notAcceptableResponse, notSupportedResponse, badGatewayResponse } from './utils/responses.js'
25
+ import { badRequestResponse, notAcceptableResponse, internalServerErrorResponse, notImplementedResponse } from './utils/responses.js'
24
26
  import { selectOutputType } from './utils/select-output-type.js'
25
- import { serverTiming } from './utils/server-timing.js'
26
- import type { CIDDetail, ContentTypeParser, CreateVerifiedFetchOptions, Resource, ResourceDetail, VerifiedFetchInit as VerifiedFetchOptions } from './index.js'
27
+ import { ServerTiming } from './utils/server-timing.js'
28
+ import type { CIDDetail, ContentTypeParser, CreateVerifiedFetchOptions, ResolveURLResult, Resource, ResourceDetail, VerifiedFetchInit as VerifiedFetchOptions } from './index.js'
27
29
  import type { VerifiedFetchPlugin, PluginContext, PluginOptions } from './plugins/types.js'
28
- import type { ParsedUrlStringResults } from './utils/parse-url-string.js'
30
+ import type { AcceptHeader } from './utils/select-output-type.js'
31
+ import type { DNSLink } from '@helia/dnslink'
29
32
  import type { Helia, SessionBlockstore } from '@helia/interface'
30
- import type { IPNS } from '@helia/ipns'
33
+ import type { IPNSResolver } from '@helia/ipns'
31
34
  import type { AbortOptions, Logger } from '@libp2p/interface'
32
35
  import type { Blockstore } from 'interface-blockstore'
33
36
  import type { CID } from 'multiformats/cid'
@@ -35,11 +38,6 @@ import type { CID } from 'multiformats/cid'
35
38
  const SESSION_CACHE_MAX_SIZE = 100
36
39
  const SESSION_CACHE_TTL_MS = 60 * 1000
37
40
 
38
- interface VerifiedFetchComponents {
39
- helia: Helia
40
- ipns?: IPNS
41
- }
42
-
43
41
  function convertOptions (options?: VerifiedFetchOptions): (Omit<VerifiedFetchOptions, 'signal'> & AbortOptions) | undefined {
44
42
  if (options == null) {
45
43
  return undefined
@@ -60,19 +58,20 @@ function convertOptions (options?: VerifiedFetchOptions): (Omit<VerifiedFetchOpt
60
58
 
61
59
  export class VerifiedFetch {
62
60
  private readonly helia: Helia
63
- private readonly ipns: IPNS
61
+ private readonly ipnsResolver: IPNSResolver
62
+ private readonly dnsLink: DNSLink
64
63
  private readonly log: Logger
65
64
  private readonly contentTypeParser: ContentTypeParser | undefined
66
65
  private readonly blockstoreSessions: QuickLRU<string, SessionBlockstore>
67
- private serverTimingHeaders: string[] = []
68
66
  private readonly withServerTiming: boolean
69
67
  private readonly plugins: VerifiedFetchPlugin[] = []
70
68
 
71
- constructor ({ helia, ipns }: VerifiedFetchComponents, init?: CreateVerifiedFetchOptions) {
69
+ constructor (helia: Helia, init: CreateVerifiedFetchOptions = {}) {
72
70
  this.helia = helia
73
71
  this.log = helia.logger.forComponent('helia:verified-fetch')
74
- this.ipns = ipns ?? heliaIpns(helia)
75
- this.contentTypeParser = init?.contentTypeParser ?? contentTypeParser
72
+ this.ipnsResolver = init.ipnsResolver ?? ipnsResolver(helia)
73
+ this.dnsLink = init.dnsLink ?? dnsLink(helia)
74
+ this.contentTypeParser = init.contentTypeParser ?? contentTypeParser
76
75
  this.blockstoreSessions = new QuickLRU({
77
76
  maxSize: init?.sessionCacheSize ?? SESSION_CACHE_MAX_SIZE,
78
77
  maxAge: init?.sessionTTLms ?? SESSION_CACHE_TTL_MS,
@@ -84,11 +83,11 @@ export class VerifiedFetch {
84
83
 
85
84
  const pluginOptions: PluginOptions = {
86
85
  ...init,
87
- logger: prefixLogger('helia:verified-fetch'),
86
+ logger: helia.logger.forComponent('verified-fetch'),
88
87
  getBlockstore: (cid, resource, useSession, options) => this.getBlockstore(cid, resource, useSession, options),
89
- handleServerTiming: async (name, description, fn) => this.handleServerTiming(name, description, fn, this.withServerTiming),
90
88
  helia,
91
- contentTypeParser: this.contentTypeParser
89
+ contentTypeParser: this.contentTypeParser,
90
+ ipnsResolver: this.ipnsResolver
92
91
  }
93
92
 
94
93
  const defaultPlugins = [
@@ -103,7 +102,7 @@ export class VerifiedFetch {
103
102
  new DagPbPlugin(pluginOptions)
104
103
  ]
105
104
 
106
- const customPlugins = init?.plugins?.map((pluginFactory) => pluginFactory(pluginOptions)) ?? []
105
+ const customPlugins = init.plugins?.map((pluginFactory) => pluginFactory(pluginOptions)) ?? []
107
106
 
108
107
  if (customPlugins.length > 0) {
109
108
  // allow custom plugins to replace default plugins
@@ -117,8 +116,6 @@ export class VerifiedFetch {
117
116
  } else {
118
117
  this.plugins = defaultPlugins
119
118
  }
120
-
121
- this.log.trace('created VerifiedFetch instance')
122
119
  }
123
120
 
124
121
  private getBlockstore (root: CID, resource: string | CID, useSession: boolean = true, options: AbortOptions = {}): Blockstore {
@@ -137,35 +134,20 @@ export class VerifiedFetch {
137
134
  return session
138
135
  }
139
136
 
140
- private async handleServerTiming<T> (name: string, description: string, fn: () => Promise<T>, withServerTiming: boolean): Promise<T> {
141
- if (!withServerTiming) {
142
- return fn()
143
- }
144
- const { error, result, header } = await serverTiming(name, description, fn)
145
- this.serverTimingHeaders.push(header)
146
- if (error != null) {
147
- throw error
148
- }
149
-
150
- return result
151
- }
152
-
153
137
  /**
154
138
  * The last place a Response touches in verified-fetch before being returned to the user. This is where we add the
155
139
  * Server-Timing header to the response if it has been collected. It should be used for any final processing of the
156
140
  * response before it is returned to the user.
157
141
  */
158
- private handleFinalResponse (response: Response, { query, cid, reqFormat, ttl, protocol, ipfsPath, pathDetails, byteRangeContext, options }: Partial<PluginContext> = {}): Response {
159
- if (this.serverTimingHeaders.length > 0) {
160
- const headerString = this.serverTimingHeaders.join(', ')
161
- response.headers.set('Server-Timing', headerString)
162
- this.serverTimingHeaders = []
142
+ private handleFinalResponse (response: Response, context?: Partial<PluginContext>): Response {
143
+ if ((this.withServerTiming || context?.withServerTiming === true) && context?.serverTiming != null) {
144
+ response.headers.set('Server-Timing', context?.serverTiming.getHeader())
163
145
  }
164
146
 
165
147
  // if there are multiple ranges, we should omit the content-length header. see https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Transfer-Encoding
166
148
  if (response.headers.get('Transfer-Encoding') !== 'chunked') {
167
- if (byteRangeContext != null) {
168
- const contentLength = byteRangeContext.getLength()
149
+ if (context?.byteRangeContext != null) {
150
+ const contentLength = context.byteRangeContext.getLength()
169
151
  if (contentLength != null) {
170
152
  this.log.trace('Setting Content-Length from byteRangeContext: %d', contentLength)
171
153
  response.headers.set('Content-Length', contentLength.toString())
@@ -176,50 +158,55 @@ export class VerifiedFetch {
176
158
  // set Content-Disposition header
177
159
  let contentDisposition: string | undefined
178
160
 
179
- this.log.trace('checking for content disposition')
180
-
181
161
  // force download if requested
182
- if (query?.download === true) {
162
+ if (context?.query?.download === true) {
163
+ this.log.trace('download requested')
183
164
  contentDisposition = 'attachment'
184
- } else {
185
- this.log.trace('download not requested')
186
165
  }
187
166
 
188
167
  // override filename if requested
189
- if (query?.filename != null) {
168
+ if (context?.query?.filename != null) {
169
+ this.log.trace('specific filename requested')
170
+
190
171
  if (contentDisposition == null) {
191
172
  contentDisposition = 'inline'
192
173
  }
193
174
 
194
- contentDisposition = `${contentDisposition}; ${getContentDispositionFilename(query.filename)}`
195
- } else {
196
- this.log.trace('no filename specified in query')
175
+ contentDisposition = `${contentDisposition}; ${getContentDispositionFilename(context.query.filename)}`
197
176
  }
198
177
 
199
178
  if (contentDisposition != null) {
179
+ this.log.trace('content disposition %s', contentDisposition)
200
180
  response.headers.set('Content-Disposition', contentDisposition)
201
- } else {
202
- this.log.trace('no content disposition specified')
203
181
  }
204
182
 
205
- if (cid != null && response.headers.get('etag') == null) {
206
- response.headers.set('etag', getETag({ cid: pathDetails?.terminalElement.cid ?? cid, reqFormat, weak: false }))
183
+ if (context?.cid != null && response.headers.get('etag') == null) {
184
+ response.headers.set('etag', getETag({
185
+ cid: context.pathDetails?.terminalElement.cid ?? context.cid,
186
+ reqFormat: context.reqFormat,
187
+ weak: false
188
+ }))
207
189
  }
208
190
 
209
- if (protocol != null) {
210
- setCacheControlHeader({ response, ttl, protocol })
191
+ if (context?.protocol != null && context.ttl != null) {
192
+ setCacheControlHeader({
193
+ response,
194
+ ttl: context.ttl,
195
+ protocol: context.protocol
196
+ })
211
197
  }
212
- if (ipfsPath != null) {
213
- response.headers.set('X-Ipfs-Path', ipfsPath)
198
+
199
+ if (context?.ipfsPath != null) {
200
+ response.headers.set('X-Ipfs-Path', uriEncodeIPFSPath(context.ipfsPath))
214
201
  }
215
202
 
216
203
  // set CORS headers. If hosting your own gateway with verified-fetch behind the scenes, you can alter these before you send the response to the client.
217
204
  response.headers.set('Access-Control-Allow-Origin', '*')
218
205
  response.headers.set('Access-Control-Allow-Methods', 'GET, HEAD, OPTIONS')
219
206
  response.headers.set('Access-Control-Allow-Headers', 'Range, X-Requested-With')
220
- response.headers.set('Access-Control-Expose-Headers', 'Content-Range, Content-Length, X-Ipfs-Path, X-Stream-Output')
207
+ response.headers.set('Access-Control-Expose-Headers', 'Content-Range, Content-Length, X-Ipfs-Path, X-Ipfs-Roots, X-Stream-Output')
221
208
 
222
- if (reqFormat !== 'car') {
209
+ if (context?.reqFormat !== 'car') {
223
210
  // if we are not doing streaming responses, set the Accept-Ranges header to bytes to enable range requests
224
211
  response.headers.set('Accept-Ranges', 'bytes')
225
212
  } else {
@@ -227,10 +214,17 @@ export class VerifiedFetch {
227
214
  response.headers.set('Accept-Ranges', 'none')
228
215
  }
229
216
 
230
- if (options?.method === 'HEAD') {
217
+ if (response.headers.get('Content-Type')?.includes('application/vnd.ipld.car') === true || response.headers.get('Content-Type')?.includes('application/vnd.ipld.raw') === true) {
218
+ // see https://specs.ipfs.tech/http-gateways/path-gateway/#x-content-type-options-response-header
219
+ response.headers.set('X-Content-Type-Options', 'nosniff')
220
+ }
221
+
222
+ if (context?.options?.method === 'HEAD') {
231
223
  // don't send the body for HEAD requests
232
- const headers = response?.headers
233
- return new Response(null, { status: 200, headers })
224
+ return new Response(null, {
225
+ status: 200,
226
+ headers: response.headers
227
+ })
234
228
  }
235
229
 
236
230
  return response
@@ -240,7 +234,7 @@ export class VerifiedFetch {
240
234
  * Runs plugins in a loop. After each plugin that returns `null` (partial/no final),
241
235
  * we re-check `canHandle()` for all plugins in the next iteration if the context changed.
242
236
  */
243
- private async runPluginPipeline (context: PluginContext, maxPasses: number = 3): Promise<Response | undefined> {
237
+ private async runPluginPipeline (context: PluginContext, maxPasses: number = 3): Promise<Response> {
244
238
  let finalResponse: Response | undefined
245
239
  let passCount = 0
246
240
  const pluginsUsed = new Set<string>()
@@ -248,23 +242,27 @@ export class VerifiedFetch {
248
242
  let prevModificationId = context.modified
249
243
 
250
244
  while (passCount < maxPasses) {
251
- this.log(`Starting pipeline pass #${passCount + 1}`)
245
+ this.log(`starting pipeline pass #${passCount + 1}`)
252
246
  passCount++
253
247
 
248
+ this.log.trace('checking which plugins can handle %c%s with accept %o', context.cid, context.path != null ? `/${context.path}` : '', context.accept)
249
+
254
250
  // gather plugins that say they can handle the *current* context, but haven't been used yet
255
251
  const readyPlugins = this.plugins.filter(p => !pluginsUsed.has(p.id)).filter(p => p.canHandle(context))
252
+
256
253
  if (readyPlugins.length === 0) {
257
- this.log.trace('No plugins can handle the current context.. checking by CID code')
254
+ this.log.trace('no plugins can handle the current context, checking by CID code')
258
255
  const plugins = this.plugins.filter(p => p.codes.includes(context.cid.code))
256
+
259
257
  if (plugins.length > 0) {
260
258
  readyPlugins.push(...plugins)
261
259
  } else {
262
- this.log.trace('No plugins found that can handle request by CID code; exiting pipeline.')
260
+ this.log.trace('no plugins found that can handle request by CID code; exiting pipeline')
263
261
  break
264
262
  }
265
263
  }
266
264
 
267
- this.log.trace('Plugins ready to handle request: ', readyPlugins.map(p => p.id).join(', '))
265
+ this.log.trace('plugins ready to handle request: %s', readyPlugins.map(p => p.id).join(', '))
268
266
 
269
267
  // track if any plugin changed the context or returned a response
270
268
  let contextChanged = false
@@ -272,10 +270,13 @@ export class VerifiedFetch {
272
270
 
273
271
  for (const plugin of readyPlugins) {
274
272
  try {
275
- this.log.trace('Invoking plugin:', plugin.id)
273
+ this.log('invoking plugin: %s', plugin.id)
276
274
  pluginsUsed.add(plugin.id)
277
275
 
278
276
  const maybeResponse = await plugin.handle(context)
277
+
278
+ this.log('plugin response %s %o', plugin.id, maybeResponse)
279
+
279
280
  if (maybeResponse != null) {
280
281
  // if a plugin returns a final Response, short-circuit
281
282
  finalResponse = maybeResponse
@@ -286,12 +287,16 @@ export class VerifiedFetch {
286
287
  if (context.options?.signal?.aborted) {
287
288
  throw new AbortError(context.options?.signal?.reason)
288
289
  }
289
- this.log.error('Error in plugin:', plugin.constructor.name, err)
290
- // if fatal, short-circuit the pipeline
291
- if (err.name === 'PluginFatalError') {
292
- // if plugin provides a custom error response, return it
293
- return err.response ?? badGatewayResponse(context.resource, 'Failed to fetch')
294
- }
290
+
291
+ this.log.error('error in plugin %s - %e', plugin.id, err)
292
+
293
+ return internalServerErrorResponse(context.resource, JSON.stringify({
294
+ error: errorToObject(err)
295
+ }), {
296
+ headers: {
297
+ 'content-type': 'application/json'
298
+ }
299
+ })
295
300
  } finally {
296
301
  // on each plugin call, check for changes in the context
297
302
  const newModificationId = context.modified
@@ -302,7 +307,7 @@ export class VerifiedFetch {
302
307
  }
303
308
 
304
309
  if (finalResponse != null) {
305
- this.log.trace('Plugin produced final response:', plugin.id)
310
+ this.log.trace('plugin %s produced final response', plugin.id)
306
311
  break
307
312
  }
308
313
  }
@@ -312,12 +317,18 @@ export class VerifiedFetch {
312
317
  }
313
318
 
314
319
  if (!contextChanged) {
315
- this.log.trace('No context changes and no final response; exiting pipeline.')
320
+ this.log.trace('no context changes and no final response; exiting pipeline.')
316
321
  break
317
322
  }
318
323
  }
319
324
 
320
- return finalResponse
325
+ return finalResponse ?? notImplementedResponse(context.resource, JSON.stringify({
326
+ error: errorToObject(new Error('No verified fetch plugin could handle the request'))
327
+ }), {
328
+ headers: {
329
+ 'content-type': 'application/json'
330
+ }
331
+ })
321
332
  }
322
333
 
323
334
  /**
@@ -335,14 +346,20 @@ export class VerifiedFetch {
335
346
  }
336
347
 
337
348
  const options = convertOptions(opts)
338
- const withServerTiming = options?.withServerTiming ?? this.withServerTiming
349
+ const serverTiming = new ServerTiming()
350
+
351
+ const urlResolver = new URLResolver({
352
+ ipnsResolver: this.ipnsResolver,
353
+ dnsLink: this.dnsLink,
354
+ timing: serverTiming
355
+ })
339
356
 
340
357
  options?.onProgress?.(new CustomProgressEvent<ResourceDetail>('verified-fetch:request:start', { resource }))
341
358
 
342
- let parsedResult: ParsedUrlStringResults
359
+ let parsedResult: ResolveURLResult
360
+
343
361
  try {
344
- parsedResult = await this.handleServerTiming('parse-resource', '', async () => parseResource(resource, { ipns: this.ipns, logger: this.helia.logger }, { withServerTiming, ...options }), withServerTiming)
345
- this.serverTimingHeaders.push(...parsedResult.serverTimings.map(({ header }) => header))
362
+ parsedResult = await urlResolver.resolve(resource, options)
346
363
  } catch (err: any) {
347
364
  if (options?.signal?.aborted) {
348
365
  throw new AbortError(options?.signal?.reason)
@@ -356,14 +373,15 @@ export class VerifiedFetch {
356
373
 
357
374
  const acceptHeader = getResolvedAcceptHeader({ query: parsedResult.query, headers: options?.headers, logger: this.helia.logger })
358
375
 
359
- const accept: string | undefined = selectOutputType(parsedResult.cid, acceptHeader)
360
- this.log('output type %s', accept)
376
+ const accept: AcceptHeader | undefined = selectOutputType(parsedResult.cid, acceptHeader)
377
+ this.log('accept %o', accept)
361
378
 
362
379
  if (acceptHeader != null && accept == null) {
380
+ this.log.error('could not fulfil request based on accept header')
363
381
  return this.handleFinalResponse(notAcceptableResponse(resource.toString()))
364
382
  }
365
383
 
366
- const responseContentType: string = accept?.split(';')[0] ?? 'application/octet-stream'
384
+ const responseContentType: string = accept?.mimeType.split(';')[0] ?? 'application/octet-stream'
367
385
 
368
386
  const redirectResponse = await getRedirectResponse({ resource, options, logger: this.helia.logger, cid: parsedResult.cid })
369
387
  if (redirectResponse != null) {
@@ -375,19 +393,28 @@ export class VerifiedFetch {
375
393
  resource: resource.toString(),
376
394
  accept,
377
395
  options,
378
- withServerTiming,
379
396
  onProgress: options?.onProgress,
380
397
  modified: 0,
381
- plugins: this.plugins.map(p => p.id)
398
+ plugins: this.plugins.map(p => p.id),
399
+ query: parsedResult.query ?? {},
400
+ withServerTiming: Boolean(options?.withServerTiming) || Boolean(this.withServerTiming),
401
+ serverTiming
382
402
  }
383
403
 
384
404
  this.log.trace('finding handler for cid code "%s" and response content type "%s"', parsedResult.cid.code, responseContentType)
385
405
 
386
406
  const response = await this.runPluginPipeline(context)
387
407
 
388
- options?.onProgress?.(new CustomProgressEvent<CIDDetail>('verified-fetch:request:end', { cid: parsedResult.cid, path: parsedResult.path }))
408
+ options?.onProgress?.(new CustomProgressEvent<CIDDetail>('verified-fetch:request:end', {
409
+ cid: parsedResult.cid,
410
+ path: parsedResult.path
411
+ }))
412
+
413
+ if (response == null) {
414
+ this.log.error('no plugin could handle request for %s', resource)
415
+ }
389
416
 
390
- return this.handleFinalResponse(response ?? notSupportedResponse(resource.toString()), context)
417
+ return this.handleFinalResponse(response, context)
391
418
  }
392
419
 
393
420
  /**
@@ -1,25 +0,0 @@
1
- import type { FatalPluginErrorOptions, PluginErrorOptions } from './types.js';
2
- /**
3
- * If a plugin encounters an error, it should throw an instance of this class.
4
- */
5
- export declare class PluginError extends Error {
6
- name: string;
7
- code: string;
8
- fatal: boolean;
9
- details?: Record<string, any>;
10
- response?: any;
11
- constructor(code: string, message: string, options?: PluginErrorOptions);
12
- }
13
- /**
14
- * If a plugin encounters a fatal error and verified-fetch should not continue processing the request, it should throw
15
- * an instance of this class.
16
- *
17
- * Note that you should be very careful when throwing a `PluginFatalError`, as it will stop the request from being
18
- * processed further. If you do not have a response to return to the client, you should consider throwing a
19
- * `PluginError` instead.
20
- */
21
- export declare class PluginFatalError extends PluginError {
22
- name: string;
23
- constructor(code: string, message: string, options: FatalPluginErrorOptions);
24
- }
25
- //# sourceMappingURL=errors.d.ts.map