@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
@@ -1,13 +1,13 @@
1
1
  import { defaultMimeType } from './content-type-parser.js'
2
2
  import { isPromise } from './type-guards.js'
3
- import type { ContentTypeParser } from '../types.js'
3
+ import type { ContentTypeParser } from '../index.js'
4
4
  import type { Logger } from '@libp2p/interface'
5
5
 
6
6
  export interface GetContentTypeOptions {
7
7
  bytes: Uint8Array
8
- path: string
8
+ path?: string
9
9
  defaultContentType?: string
10
- contentTypeParser: ContentTypeParser | undefined
10
+ contentTypeParser?: ContentTypeParser
11
11
  log: Logger
12
12
 
13
13
  /**
@@ -25,7 +25,7 @@ export async function getContentType ({ bytes, path, contentTypeParser, log, def
25
25
  try {
26
26
  let fileName
27
27
  if (filenameParam == null) {
28
- fileName = path.split('/').pop()?.trim()
28
+ fileName = path?.split('/').pop()?.trim()
29
29
  fileName = (fileName === '' || fileName?.split('.').length === 1) ? undefined : fileName
30
30
  } else {
31
31
  fileName = filenameParam
@@ -46,6 +46,7 @@ export async function getContentType ({ bytes, path, contentTypeParser, log, def
46
46
  log.error('error parsing content type', err)
47
47
  }
48
48
  }
49
+
49
50
  if (contentType === defaultMimeType) {
50
51
  // if the content type is the default in our content-type-parser, instead, set it to the default content type provided to this function.
51
52
  contentType = defaultContentType
@@ -1,4 +1,4 @@
1
- import type { RequestFormatShorthand } from '../types.js'
1
+ import type { RequestFormatShorthand } from '../index.js'
2
2
  import type { CID } from 'multiformats/cid'
3
3
 
4
4
  interface GetETagArg {
@@ -0,0 +1,54 @@
1
+ import type { UnixFSEntry } from 'ipfs-unixfs-exporter'
2
+
3
+ export function getOffsetAndLength (entry: UnixFSEntry, entityBytes?: string): { offset: number, length: number } {
4
+ if (entityBytes == null) {
5
+ return {
6
+ offset: 0,
7
+ length: Infinity
8
+ }
9
+ }
10
+
11
+ const parts = entityBytes.split(':')
12
+ const start = parseInt(parts[0], 10)
13
+ const end = parts[1] === '*' ? Infinity : parseInt(parts[1], 10)
14
+
15
+ if (isNaN(start) || isNaN(end)) {
16
+ throw new Error('Could not parse entity-bytes')
17
+ }
18
+
19
+ const entrySize = Number(entry.size)
20
+
21
+ if (start >= 0) {
22
+ if (end >= 0) {
23
+ return {
24
+ offset: start,
25
+ length: end - start
26
+ }
27
+ } else {
28
+ return {
29
+ offset: start,
30
+ length: (entrySize - start) + end
31
+ }
32
+ }
33
+ }
34
+
35
+ // start < 0
36
+ let offset = entrySize + start
37
+
38
+ if (Math.abs(start) > entrySize) {
39
+ offset = 0
40
+ }
41
+
42
+ if (end >= 0) {
43
+ return {
44
+ offset,
45
+ length: (entrySize - offset) + end
46
+ }
47
+ }
48
+
49
+ // end < 0
50
+ return {
51
+ offset,
52
+ length: (entrySize - offset) + end
53
+ }
54
+ }
@@ -1,10 +1,10 @@
1
1
  import { isExplicitAcceptHeader, isExplicitFormatQuery, isExplicitIpldAcceptRequest } from './is-accept-explicit.js'
2
2
  import { queryFormatToAcceptHeader } from './select-output-type.js'
3
- import type { ParsedUrlStringResults } from './parse-url-string.js'
3
+ import type { UrlQuery } from '../index.ts'
4
4
  import type { ComponentLogger } from '@libp2p/interface'
5
5
 
6
6
  export interface ResolvedAcceptHeaderOptions {
7
- query?: ParsedUrlStringResults['query']
7
+ query?: UrlQuery
8
8
  headers?: RequestInit['headers']
9
9
  logger: ComponentLogger
10
10
  }
@@ -2,18 +2,18 @@ import { AbortError } from '@libp2p/interface'
2
2
  import { CustomProgressEvent } from 'progress-events'
3
3
  import { NoContentError } from '../errors.js'
4
4
  import type { VerifiedFetchInit } from '../index.js'
5
- import type { ComponentLogger } from '@libp2p/interface'
5
+ import type { Logger } from '@libp2p/interface'
6
6
 
7
7
  /**
8
8
  * Converts an async iterator of Uint8Array bytes to a stream and returns the first chunk of bytes
9
9
  */
10
- export async function getStreamFromAsyncIterable (iterator: AsyncIterable<Uint8Array>, path: string, logger: ComponentLogger, options?: Pick<VerifiedFetchInit, 'onProgress' | 'signal'>): Promise<{ stream: ReadableStream<Uint8Array>, firstChunk: Uint8Array }> {
11
- const log = logger.forComponent('helia:verified-fetch:get-stream-from-async-iterable')
10
+ export async function getStreamFromAsyncIterable (iterator: AsyncIterable<Uint8Array>, path: string, logger: Logger, options?: Pick<VerifiedFetchInit, 'onProgress' | 'signal'>): Promise<{ stream: ReadableStream<Uint8Array>, firstChunk: Uint8Array }> {
11
+ const log = logger.newScope('get-stream-from-async-iterable')
12
12
  const reader = iterator[Symbol.asyncIterator]()
13
13
  const { value: firstChunk, done } = await reader.next()
14
14
 
15
15
  if (done === true) {
16
- log.error('no content found for path', path)
16
+ log.error('no content found for path "%s"', path)
17
17
  throw new NoContentError()
18
18
  }
19
19
 
@@ -37,19 +37,21 @@ export async function getRedirectResponse ({ resource, options, logger, cid, fet
37
37
  const forwardedHost = headers.get('x-forwarded-host')
38
38
  const headerHost = headers.get('host')
39
39
  const forwardedFor = headers.get('x-forwarded-for')
40
+
40
41
  if (forwardedFor == null && forwardedHost == null && headerHost == null) {
41
42
  log.trace('no redirect info found in headers')
42
43
  return null
43
44
  }
44
45
 
45
46
  log.trace('checking for redirect info')
46
- // if x-forwarded-host is passed, we need to set the location header to the subdomain
47
- // so that the browser can redirect to the correct subdomain
47
+ // if x-forwarded-host is passed, we need to set the location header to the
48
+ // subdomain so that the browser can redirect to the correct subdomain
48
49
  try {
49
50
  const urlParts = matchURLString(resource)
50
51
  const reqUrl = new URL(resource)
51
52
  const actualHost = forwardedHost ?? reqUrl.host
52
53
  const subdomainUrl = new URL(reqUrl)
54
+
53
55
  if (urlParts.protocol === 'ipfs' && cid.version === 0) {
54
56
  subdomainUrl.host = `${cid.toV1()}.ipfs.${actualHost}`
55
57
  } else {
@@ -65,6 +67,7 @@ export async function getRedirectResponse ({ resource, options, logger, cid, fet
65
67
  log.trace('host header is not the same as the subdomain url host, not setting location header')
66
68
  return null
67
69
  }
70
+
68
71
  if (reqUrl.host === subdomainUrl.host) {
69
72
  log.trace('req url is the same as the subdomain url, not setting location header')
70
73
  return null
@@ -75,6 +78,7 @@ export async function getRedirectResponse ({ resource, options, logger, cid, fet
75
78
  const pathUrl = new URL(reqUrl, `${reqUrl.protocol}//${actualHost}`)
76
79
  pathUrl.pathname = maybeAddTrailingSlash(reqUrl.pathname)
77
80
  log.trace('path url %s', pathUrl.href)
81
+
78
82
  // try to query subdomain with HEAD request to see if it's supported
79
83
  try {
80
84
  const subdomainTest = await fetch(subdomainUrl, { method: 'HEAD' })
@@ -86,11 +90,14 @@ export async function getRedirectResponse ({ resource, options, logger, cid, fet
86
90
  throw new SubdomainNotSupportedError('subdomain not supported')
87
91
  }
88
92
  } catch (err: any) {
89
- log('subdomain not supported', err)
93
+ log('subdomain not supported - %e', err)
94
+
90
95
  if (pathUrl.href === reqUrl.href) {
91
96
  log('path url is the same as the request url, not setting location header')
97
+
92
98
  return null
93
99
  }
100
+
94
101
  // pathUrl is different from request URL (maybe even with just a trailing slash)
95
102
  return movedPermanentlyResponse(resource.toString(), pathUrl.href)
96
103
  }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Joins an array of strings as an IPFS path and URI encodes individual
3
+ * components
4
+ */
5
+ export function uriEncodeIPFSPath (str: string): string {
6
+ return str.split('/')
7
+ .map(p => encodeURIComponent(p))
8
+ .join('/')
9
+ }
@@ -1,29 +1,36 @@
1
1
  import { FORMAT_TO_MIME_TYPE } from './select-output-type.js'
2
- import type { ParsedUrlStringResults } from './parse-url-string.js'
2
+ import type { UrlQuery } from '../index.ts'
3
3
 
4
4
  export interface IsAcceptExplicitOptions {
5
- query?: ParsedUrlStringResults['query']
5
+ query?: UrlQuery
6
6
  headers: Headers
7
7
  }
8
8
 
9
9
  export function isExplicitAcceptHeader (headers: Headers): boolean {
10
10
  const incomingAcceptHeader = headers.get('accept')
11
- if (incomingAcceptHeader != null && Object.values(FORMAT_TO_MIME_TYPE).includes(incomingAcceptHeader)) {
12
- return true
11
+
12
+ if (incomingAcceptHeader == null) {
13
+ return false
13
14
  }
14
- return false
15
+
16
+ return Object.values(FORMAT_TO_MIME_TYPE)
17
+ .some(mimeType => incomingAcceptHeader.includes(mimeType))
15
18
  }
16
19
 
17
- export function isExplicitFormatQuery (query?: ParsedUrlStringResults['query']): boolean {
20
+ export function isExplicitFormatQuery (query?: UrlQuery): boolean {
18
21
  const formatQuery = query?.format
22
+
19
23
  if (formatQuery != null && Object.keys(FORMAT_TO_MIME_TYPE).includes(formatQuery)) {
20
24
  return true
21
25
  }
26
+
22
27
  return false
23
28
  }
24
29
 
25
30
  /**
26
- * The user can provide an explicit `accept` header in the request headers or a `format` query parameter in the URL.
31
+ * The user can provide an explicit `accept` header in the request headers or a
32
+ * `format` query parameter in the URL.
33
+ *
27
34
  * If either of these are provided, this function returns true.
28
35
  */
29
36
  export function isExplicitIpldAcceptRequest ({ query, headers }: IsAcceptExplicitOptions): boolean {
@@ -1,68 +1,13 @@
1
- import { AbortError } from '@libp2p/interface'
2
- import { CID } from 'multiformats/cid'
3
- import { getPeerIdFromString } from './get-peer-id-from-string.js'
4
- import { serverTiming } from './server-timing.js'
5
- import { TLRU } from './tlru.js'
6
- import type { ServerTimingResult } from './server-timing.js'
7
- import type { RequestFormatShorthand } from '../types.js'
8
- import type { DNSLinkResolveResult, IPNS, IPNSResolveResult, IPNSRoutingEvents, ResolveDNSLinkProgressEvents, ResolveProgressEvents, ResolveResult } from '@helia/ipns'
9
- import type { AbortOptions, ComponentLogger, PeerId } from '@libp2p/interface'
10
- import type { ProgressOptions } from 'progress-events'
11
-
12
- export const ipnsCache = new TLRU<DNSLinkResolveResult | IPNSResolveResult>(1000)
13
-
14
- export interface ParseUrlStringInput {
15
- urlString: string
16
- ipns: IPNS
17
- logger: ComponentLogger
18
- withServerTiming?: boolean
19
- }
20
- export interface ParseUrlStringOptions extends ProgressOptions<ResolveProgressEvents | IPNSRoutingEvents | ResolveDNSLinkProgressEvents>, AbortOptions {
21
-
22
- }
23
-
24
- export interface ParsedUrlQuery extends Record<string, string | unknown> {
25
- format?: RequestFormatShorthand
26
- download?: boolean
27
- filename?: string
28
- 'dag-scope'?: string
29
- }
30
-
31
- export interface ParsedUrlStringResults extends ResolveResult {
32
- protocol: 'ipfs' | 'ipns'
33
- query: ParsedUrlQuery
34
-
35
- /**
36
- * The value for the IPFS gateway spec compliant header `X-Ipfs-Path` on the
37
- * response.
38
- * The value of this header should be the original requested content path,
39
- * prior to any path resolution or traversal.
40
- *
41
- * @see https://specs.ipfs.tech/http-gateways/path-gateway/#x-ipfs-path-response-header
42
- */
43
- ipfsPath: string
44
-
45
- /**
46
- * seconds as a number
47
- */
48
- ttl?: number
49
-
50
- /**
51
- * serverTiming items
52
- */
53
- serverTimings: Array<ServerTimingResult<any>>
54
- }
55
-
56
- const URL_REGEX = /^(?<protocol>ip[fn]s):\/\/(?<cidOrPeerIdOrDnsLink>[^/?]+)\/?(?<path>[^?]*)\??(?<queryString>.*)$/
57
- const PATH_REGEX = /^\/(?<protocol>ip[fn]s)\/(?<cidOrPeerIdOrDnsLink>[^/?]+)\/?(?<path>[^?]*)\??(?<queryString>.*)$/
58
- const PATH_GATEWAY_REGEX = /^https?:\/\/(.*[^/])\/(?<protocol>ip[fn]s)\/(?<cidOrPeerIdOrDnsLink>[^/?]+)\/?(?<path>[^?]*)\??(?<queryString>.*)$/
59
- const SUBDOMAIN_GATEWAY_REGEX = /^https?:\/\/(?<cidOrPeerIdOrDnsLink>[^/?]+)\.(?<protocol>ip[fn]s)\.([^/?]+)\/?(?<path>[^?]*)\??(?<queryString>.*)$/
1
+ const URL_REGEX = /^(?<protocol>ip[fn]s):\/\/(?<cidOrPeerIdOrDnsLink>[^/?]+)\/?(?<path>[^?]*)\??(?<query>.*)$/
2
+ const PATH_REGEX = /^\/(?<protocol>ip[fn]s)\/(?<cidOrPeerIdOrDnsLink>[^/?]+)\/?(?<path>[^?]*)\??(?<query>.*)$/
3
+ const PATH_GATEWAY_REGEX = /^https?:\/\/(.*[^/])\/(?<protocol>ip[fn]s)\/(?<cidOrPeerIdOrDnsLink>[^/?]+)\/?(?<path>[^?]*)\??(?<query>.*)$/
4
+ const SUBDOMAIN_GATEWAY_REGEX = /^https?:\/\/(?<cidOrPeerIdOrDnsLink>[^/?]+)\.(?<protocol>ip[fn]s)\.([^/?]+)\/?(?<path>[^?]*)\??(?<query>.*)$/
60
5
 
61
6
  interface MatchUrlGroups {
62
7
  protocol: 'ipfs' | 'ipns'
63
8
  cidOrPeerIdOrDnsLink: string
64
9
  path?: string
65
- queryString?: string
10
+ query?: string
66
11
  }
67
12
 
68
13
  function matchUrlGroupsGuard (groups?: null | { [key in string]: string; } | MatchUrlGroups): groups is MatchUrlGroups {
@@ -71,50 +16,38 @@ function matchUrlGroupsGuard (groups?: null | { [key in string]: string; } | Mat
71
16
  const cidOrPeerIdOrDnsLink = groups?.cidOrPeerIdOrDnsLink
72
17
  if (cidOrPeerIdOrDnsLink == null) { return false }
73
18
  const path = groups?.path
74
- const queryString = groups?.queryString
19
+ const query = groups?.query
75
20
 
76
21
  return ['ipns', 'ipfs'].includes(protocol) &&
77
22
  typeof cidOrPeerIdOrDnsLink === 'string' &&
78
23
  (path == null || typeof path === 'string') &&
79
- (queryString == null || typeof queryString === 'string')
24
+ (query == null || typeof query === 'string')
80
25
  }
81
26
 
27
+ // TODO: can this be replaced with `new URL`?
82
28
  export function matchURLString (urlString: string): MatchUrlGroups {
83
29
  for (const pattern of [SUBDOMAIN_GATEWAY_REGEX, URL_REGEX, PATH_GATEWAY_REGEX, PATH_REGEX]) {
84
30
  const match = urlString.match(pattern)
85
31
 
86
32
  if (matchUrlGroupsGuard(match?.groups)) {
87
- return match.groups satisfies MatchUrlGroups
33
+ const groups = match.groups satisfies MatchUrlGroups
34
+
35
+ if (groups.path != null) {
36
+ groups.path = decodeURIComponent(groups.path)
37
+ }
38
+
39
+ // decode inline dnslink domain if present
40
+ if (pattern === SUBDOMAIN_GATEWAY_REGEX && groups.protocol === 'ipns' && isInlinedDnsLink(groups.cidOrPeerIdOrDnsLink)) {
41
+ groups.cidOrPeerIdOrDnsLink = dnsLinkLabelDecoder(groups.cidOrPeerIdOrDnsLink)
42
+ }
43
+
44
+ return groups
88
45
  }
89
46
  }
90
47
 
91
48
  throw new TypeError(`Invalid URL: ${urlString}, please use ipfs://, ipns://, or gateway URLs only`)
92
49
  }
93
50
 
94
- /**
95
- * determines the TTL for the resolved resource that will be used for the `Cache-Control` header's `max-age` directive.
96
- * max-age is in seconds
97
- *
98
- * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#response_directives
99
- *
100
- * If we have ipnsTtlNs, it will be a BigInt representing "nanoseconds". We need to convert it back to seconds.
101
- *
102
- * For more TTL nuances:
103
- *
104
- * @see https://github.com/ipfs/js-ipns/blob/16e0e10682fa9a663e0bb493a44d3e99a5200944/src/index.ts#L200
105
- * @see https://github.com/ipfs/js-ipns/pull/308
106
- * @returns the ttl in seconds
107
- */
108
- function calculateTtl (resolveResult?: IPNSResolveResult | DNSLinkResolveResult): number | undefined {
109
- if (resolveResult == null) {
110
- return undefined
111
- }
112
- const dnsLinkTtl = (resolveResult as DNSLinkResolveResult).answer?.TTL
113
- const ipnsTtlNs = (resolveResult as IPNSResolveResult).record?.ttl
114
- const ipnsTtl = ipnsTtlNs != null ? Number(ipnsTtlNs / BigInt(1e9)) : undefined
115
- return dnsLinkTtl ?? ipnsTtl
116
- }
117
-
118
51
  /**
119
52
  * For DNSLink see https://specs.ipfs.tech/http-gateways/subdomain-gateway/#host-request-header
120
53
  * DNSLink names include . which means they must be inlined into a single DNS label to provide unique origin and work with wildcard TLS certificates.
@@ -142,202 +75,3 @@ function isInlinedDnsLink (label: string): boolean {
142
75
  function dnsLinkLabelDecoder (linkLabel: string): string {
143
76
  return linkLabel.replace(/--/g, '%').replace(/-/g, '.').replace(/%/g, '-')
144
77
  }
145
-
146
- /**
147
- * A function that parses ipfs:// and ipns:// URLs, returning an object with easily recognizable properties.
148
- *
149
- * After determining the protocol successfully, we process the cidOrPeerIdOrDnsLink:
150
- * - If it's ipfs, it parses the CID or throws Error[]
151
- * - If it's ipns, it attempts to resolve the PeerId and then the DNSLink. If both fail, Error[] is thrown.
152
- *
153
- * @todo we need to break out each step of this function (cid parsing, ipns resolving, dnslink resolving) into separate functions and then remove the eslint-disable comment
154
- *
155
- * @throws {Error[]}
156
- */
157
- // eslint-disable-next-line complexity
158
- export async function parseUrlString ({ urlString, ipns, logger, withServerTiming = false }: ParseUrlStringInput, options?: ParseUrlStringOptions): Promise<ParsedUrlStringResults> {
159
- const log = logger.forComponent('helia:verified-fetch:parse-url-string')
160
- const { protocol, cidOrPeerIdOrDnsLink, path: urlPath, queryString } = matchURLString(urlString)
161
-
162
- let cid: CID | undefined
163
- let resolvedPath: string | undefined
164
- const errors: Error[] = []
165
- let resolveResult: IPNSResolveResult | DNSLinkResolveResult | undefined
166
- const serverTimings: Array<ServerTimingResult<any>> = []
167
-
168
- if (protocol === 'ipfs') {
169
- try {
170
- cid = CID.parse(cidOrPeerIdOrDnsLink)
171
- /**
172
- * no ttl set. @link {setCacheControlHeader}
173
- */
174
- } catch (err) {
175
- log.error(err)
176
- errors.push(new TypeError('Invalid CID for ipfs://<cid> URL'))
177
- }
178
- } else {
179
- // protocol is ipns
180
- resolveResult = ipnsCache.get(cidOrPeerIdOrDnsLink)
181
-
182
- if (resolveResult != null) {
183
- cid = resolveResult.cid
184
- resolvedPath = resolveResult.path
185
- log.trace('resolved %s to %c from cache', cidOrPeerIdOrDnsLink, cid)
186
- } else {
187
- log.trace('Attempting to resolve PeerId for %s', cidOrPeerIdOrDnsLink)
188
- let peerId: PeerId | undefined
189
- try {
190
- // try resolving as an IPNS name
191
-
192
- peerId = getPeerIdFromString(cidOrPeerIdOrDnsLink)
193
- const pubKey = peerId?.publicKey
194
- if (pubKey == null) {
195
- throw new TypeError('cidOrPeerIdOrDnsLink contains no public key')
196
- }
197
-
198
- if (withServerTiming) {
199
- const resolveIpns = async (): Promise<IPNSResolveResult> => {
200
- return ipns.resolve(pubKey, options)
201
- }
202
- const resolveResultWithServerTiming = await serverTiming('ipns.resolve', `Resolve IPNS name ${cidOrPeerIdOrDnsLink}`, resolveIpns)
203
- serverTimings.push(resolveResultWithServerTiming)
204
-
205
- // eslint-disable-next-line max-depth
206
- if (resolveResultWithServerTiming.error != null) {
207
- throw resolveResultWithServerTiming.error
208
- }
209
- resolveResult = resolveResultWithServerTiming.result
210
- } else {
211
- resolveResult = await ipns.resolve(pubKey, options)
212
- }
213
- cid = resolveResult?.cid
214
- resolvedPath = resolveResult?.path
215
- log.trace('resolved %s to %c', cidOrPeerIdOrDnsLink, cid)
216
- } catch (err) {
217
- if (options?.signal?.aborted) {
218
- throw new AbortError(options?.signal?.reason)
219
- }
220
- if (peerId == null) {
221
- log.error('could not parse PeerId string "%s"', cidOrPeerIdOrDnsLink, err)
222
- errors.push(new TypeError(`Could not parse PeerId in ipns url "${cidOrPeerIdOrDnsLink}", ${(err as Error).message}`))
223
- } else {
224
- log.error('could not resolve PeerId %c', peerId, err)
225
- errors.push(new TypeError(`Could not resolve PeerId "${cidOrPeerIdOrDnsLink}": ${(err as Error).message}`))
226
- }
227
- }
228
-
229
- if (cid == null) {
230
- // cid is still null, try resolving as a DNSLink
231
- let decodedDnsLinkLabel = cidOrPeerIdOrDnsLink
232
- if (isInlinedDnsLink(cidOrPeerIdOrDnsLink)) {
233
- decodedDnsLinkLabel = dnsLinkLabelDecoder(cidOrPeerIdOrDnsLink)
234
- log.trace('decoded dnslink from "%s" to "%s"', cidOrPeerIdOrDnsLink, decodedDnsLinkLabel)
235
- }
236
- log.trace('Attempting to resolve DNSLink for %s', decodedDnsLinkLabel)
237
-
238
- try {
239
- // eslint-disable-next-line max-depth
240
- if (withServerTiming) {
241
- const resolveResultWithServerTiming = await serverTiming('ipns.resolveDNSLink', `Resolve DNSLink ${decodedDnsLinkLabel}`, ipns.resolveDNSLink.bind(ipns, decodedDnsLinkLabel, options))
242
- serverTimings.push(resolveResultWithServerTiming)
243
- // eslint-disable-next-line max-depth
244
- if (resolveResultWithServerTiming.error != null) {
245
- throw resolveResultWithServerTiming.error
246
- }
247
- resolveResult = resolveResultWithServerTiming.result
248
- } else {
249
- resolveResult = await ipns.resolveDNSLink(decodedDnsLinkLabel, options)
250
- }
251
-
252
- cid = resolveResult?.cid
253
- resolvedPath = resolveResult?.path
254
- log.trace('resolved %s to %c', decodedDnsLinkLabel, cid)
255
- } catch (err: any) {
256
- // eslint-disable-next-line max-depth
257
- if (options?.signal?.aborted) {
258
- throw new AbortError(options?.signal?.reason)
259
- }
260
- log.error('could not resolve DnsLink for "%s"', cidOrPeerIdOrDnsLink, err)
261
- errors.push(err)
262
- }
263
- }
264
- }
265
- }
266
-
267
- if (cid == null) {
268
- if (errors.length === 1) {
269
- throw errors[0]
270
- }
271
-
272
- errors.push(new Error(`Invalid resource. Cannot determine CID from URL "${urlString}".`))
273
-
274
- // eslint-disable-next-line @typescript-eslint/only-throw-error
275
- throw errors
276
- }
277
-
278
- let ttl = calculateTtl(resolveResult)
279
-
280
- if (resolveResult != null) {
281
- // use the ttl for the resolved resource for the cache, but fallback to 2 minutes if not available
282
- ttl = ttl ?? 60 * 2
283
- log.trace('caching %s resolved to %s with TTL: %s', cidOrPeerIdOrDnsLink, cid, ttl)
284
- // convert ttl from seconds to ms for the cache
285
- ipnsCache.set(cidOrPeerIdOrDnsLink, resolveResult, ttl * 1000)
286
- }
287
-
288
- // parse query string
289
- const query: Record<string, any> = {}
290
-
291
- if (queryString != null && queryString.length > 0) {
292
- const queryParts = queryString.split('&')
293
- for (const part of queryParts) {
294
- const [key, value] = part.split('=')
295
- query[key] = decodeURIComponent(value)
296
- }
297
-
298
- if (query.download != null) {
299
- query.download = query.download === 'true'
300
- }
301
-
302
- if (query.filename != null) {
303
- query.filename = query.filename.toString()
304
- }
305
- }
306
-
307
- return {
308
- protocol,
309
- cid,
310
- path: joinPaths(resolvedPath, urlPath ?? ''),
311
- query,
312
- ttl,
313
- ipfsPath: `/${protocol}/${cidOrPeerIdOrDnsLink}${urlPath != null && urlPath !== '' ? `/${urlPath}` : ''}`,
314
- serverTimings
315
- } satisfies ParsedUrlStringResults
316
- }
317
-
318
- /**
319
- * join the path from resolve result & given path.
320
- * e.g. /ipns/<peerId>/ that is resolved to /ipfs/<cid>/<path1>, when requested as /ipns/<peerId>/<path2>, should be
321
- * resolved to /ipfs/<cid>/<path1>/<path2>
322
- */
323
- function joinPaths (resolvedPath: string | undefined, urlPath: string): string {
324
- let path = ''
325
-
326
- if (resolvedPath != null) {
327
- path += resolvedPath
328
- }
329
-
330
- if (urlPath.length > 0) {
331
- path = `${path.length > 0 ? `${path}/` : path}${urlPath}`
332
- }
333
-
334
- // replace duplicate forward slashes
335
- path = path.replace(/\/(\/)+/g, '/')
336
-
337
- // strip trailing forward slash if present
338
- if (path.startsWith('/')) {
339
- path = path.substring(1)
340
- }
341
-
342
- return path.split('/').map(decodeURIComponent).join('/')
343
- }
@@ -8,7 +8,7 @@ interface CacheControlHeaderOptions {
8
8
  * See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#response_directives
9
9
  */
10
10
  ttl?: number
11
- protocol: 'ipfs' | 'ipns'
11
+ protocol: string
12
12
  response: Response
13
13
  }
14
14
 
@@ -1,5 +1,5 @@
1
1
  import type { ByteRangeContext } from './byte-range-context.js'
2
- import type { SupportedBodyTypes } from '../types.js'
2
+ import type { SupportedBodyTypes } from '../index.js'
3
3
  import type { Logger } from '@libp2p/interface'
4
4
 
5
5
  function setField (response: Response, name: string, value: string | boolean): void {
@@ -44,6 +44,20 @@ export function okResponse (url: string, body?: SupportedBodyTypes, init?: Respo
44
44
  return response
45
45
  }
46
46
 
47
+ export function internalServerErrorResponse (url: string, body?: SupportedBodyTypes, init?: ResponseInit): Response {
48
+ const response = new Response(body, {
49
+ ...(init ?? {}),
50
+ status: 500,
51
+ statusText: 'Internal Server Error'
52
+ })
53
+ response.headers.set('X-Content-Type-Options', 'nosniff') // see https://specs.ipfs.tech/http-gateways/path-gateway/#x-content-type-options-response-header
54
+
55
+ setType(response, 'basic')
56
+ setUrl(response, url)
57
+
58
+ return response
59
+ }
60
+
47
61
  export function badGatewayResponse (url: string, body?: SupportedBodyTypes, init?: ResponseInit): Response {
48
62
  const response = new Response(body, {
49
63
  ...(init ?? {}),
@@ -57,7 +71,7 @@ export function badGatewayResponse (url: string, body?: SupportedBodyTypes, init
57
71
  return response
58
72
  }
59
73
 
60
- export function notSupportedResponse (url: string, body?: SupportedBodyTypes, init?: ResponseInit): Response {
74
+ export function notImplementedResponse (url: string, body?: SupportedBodyTypes, init?: ResponseInit): Response {
61
75
  const response = new Response(body, {
62
76
  ...(init ?? {}),
63
77
  status: 501,