@helia/verified-fetch 3.2.2 → 4.0.0

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 (130) hide show
  1. package/README.md +5 -5
  2. package/dist/index.min.js +81 -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 +57 -13
  9. package/dist/src/index.d.ts.map +1 -1
  10. package/dist/src/index.js +6 -6
  11. package/dist/src/index.js.map +1 -1
  12. package/dist/src/plugins/plugin-handle-car.d.ts.map +1 -1
  13. package/dist/src/plugins/plugin-handle-car.js +37 -27
  14. package/dist/src/plugins/plugin-handle-car.js.map +1 -1
  15. package/dist/src/plugins/plugin-handle-dag-cbor-html-preview.d.ts +1 -1
  16. package/dist/src/plugins/plugin-handle-dag-cbor-html-preview.d.ts.map +1 -1
  17. package/dist/src/plugins/plugin-handle-dag-cbor-html-preview.js +1 -1
  18. package/dist/src/plugins/plugin-handle-dag-cbor-html-preview.js.map +1 -1
  19. package/dist/src/plugins/plugin-handle-dag-cbor.js +5 -5
  20. package/dist/src/plugins/plugin-handle-dag-cbor.js.map +1 -1
  21. package/dist/src/plugins/plugin-handle-dag-pb.js +12 -12
  22. package/dist/src/plugins/plugin-handle-dag-pb.js.map +1 -1
  23. package/dist/src/plugins/plugin-handle-dag-walk.d.ts.map +1 -1
  24. package/dist/src/plugins/plugin-handle-dag-walk.js +5 -4
  25. package/dist/src/plugins/plugin-handle-dag-walk.js.map +1 -1
  26. package/dist/src/plugins/plugin-handle-ipns-record.d.ts.map +1 -1
  27. package/dist/src/plugins/plugin-handle-ipns-record.js +13 -19
  28. package/dist/src/plugins/plugin-handle-ipns-record.js.map +1 -1
  29. package/dist/src/plugins/plugin-handle-json.d.ts.map +1 -1
  30. package/dist/src/plugins/plugin-handle-json.js +5 -4
  31. package/dist/src/plugins/plugin-handle-json.js.map +1 -1
  32. package/dist/src/plugins/plugin-handle-raw.d.ts.map +1 -1
  33. package/dist/src/plugins/plugin-handle-raw.js +18 -5
  34. package/dist/src/plugins/plugin-handle-raw.js.map +1 -1
  35. package/dist/src/plugins/plugin-handle-tar.js +1 -1
  36. package/dist/src/plugins/plugin-handle-tar.js.map +1 -1
  37. package/dist/src/plugins/types.d.ts +10 -8
  38. package/dist/src/plugins/types.d.ts.map +1 -1
  39. package/dist/src/url-resolver.d.ts +21 -0
  40. package/dist/src/url-resolver.d.ts.map +1 -0
  41. package/dist/src/url-resolver.js +118 -0
  42. package/dist/src/url-resolver.js.map +1 -0
  43. package/dist/src/utils/byte-range-context.d.ts +1 -1
  44. package/dist/src/utils/content-type-parser.d.ts.map +1 -1
  45. package/dist/src/utils/content-type-parser.js +3 -0
  46. package/dist/src/utils/content-type-parser.js.map +1 -1
  47. package/dist/src/utils/get-content-type.d.ts +3 -3
  48. package/dist/src/utils/get-content-type.d.ts.map +1 -1
  49. package/dist/src/utils/get-content-type.js +1 -1
  50. package/dist/src/utils/get-content-type.js.map +1 -1
  51. package/dist/src/utils/get-e-tag.d.ts +1 -1
  52. package/dist/src/utils/get-offset-and-length.d.ts +6 -0
  53. package/dist/src/utils/get-offset-and-length.d.ts.map +1 -0
  54. package/dist/src/utils/get-offset-and-length.js +46 -0
  55. package/dist/src/utils/get-offset-and-length.js.map +1 -0
  56. package/dist/src/utils/get-resolved-accept-header.d.ts +2 -2
  57. package/dist/src/utils/get-resolved-accept-header.d.ts.map +1 -1
  58. package/dist/src/utils/handle-redirects.d.ts.map +1 -1
  59. package/dist/src/utils/handle-redirects.js +3 -3
  60. package/dist/src/utils/handle-redirects.js.map +1 -1
  61. package/dist/src/utils/ipfs-path-to-string.d.ts +6 -0
  62. package/dist/src/utils/ipfs-path-to-string.d.ts.map +1 -0
  63. package/dist/src/utils/ipfs-path-to-string.js +10 -0
  64. package/dist/src/utils/ipfs-path-to-string.js.map +1 -0
  65. package/dist/src/utils/is-accept-explicit.d.ts +6 -4
  66. package/dist/src/utils/is-accept-explicit.d.ts.map +1 -1
  67. package/dist/src/utils/is-accept-explicit.js +7 -4
  68. package/dist/src/utils/is-accept-explicit.js.map +1 -1
  69. package/dist/src/utils/parse-url-string.d.ts +1 -55
  70. package/dist/src/utils/parse-url-string.d.ts.map +1 -1
  71. package/dist/src/utils/parse-url-string.js +16 -217
  72. package/dist/src/utils/parse-url-string.js.map +1 -1
  73. package/dist/src/utils/response-headers.d.ts +1 -1
  74. package/dist/src/utils/response-headers.d.ts.map +1 -1
  75. package/dist/src/utils/responses.d.ts +1 -1
  76. package/dist/src/utils/select-output-type.d.ts +6 -2
  77. package/dist/src/utils/select-output-type.d.ts.map +1 -1
  78. package/dist/src/utils/select-output-type.js +28 -37
  79. package/dist/src/utils/select-output-type.js.map +1 -1
  80. package/dist/src/utils/server-timing.d.ts +5 -11
  81. package/dist/src/utils/server-timing.d.ts.map +1 -1
  82. package/dist/src/utils/server-timing.js +17 -15
  83. package/dist/src/utils/server-timing.js.map +1 -1
  84. package/dist/src/utils/walk-path.js +1 -1
  85. package/dist/src/utils/walk-path.js.map +1 -1
  86. package/dist/src/verified-fetch.d.ts +3 -10
  87. package/dist/src/verified-fetch.d.ts.map +1 -1
  88. package/dist/src/verified-fetch.js +68 -57
  89. package/dist/src/verified-fetch.js.map +1 -1
  90. package/dist/typedoc-urls.json +13 -2
  91. package/package.json +35 -36
  92. package/src/constants.ts +1 -0
  93. package/src/index.ts +73 -22
  94. package/src/plugins/plugin-handle-car.ts +54 -30
  95. package/src/plugins/plugin-handle-dag-cbor-html-preview.ts +2 -2
  96. package/src/plugins/plugin-handle-dag-cbor.ts +5 -5
  97. package/src/plugins/plugin-handle-dag-pb.ts +12 -12
  98. package/src/plugins/plugin-handle-dag-walk.ts +5 -4
  99. package/src/plugins/plugin-handle-ipns-record.ts +16 -19
  100. package/src/plugins/plugin-handle-json.ts +5 -4
  101. package/src/plugins/plugin-handle-raw.ts +21 -6
  102. package/src/plugins/plugin-handle-tar.ts +1 -1
  103. package/src/plugins/types.ts +12 -8
  104. package/src/url-resolver.ts +159 -0
  105. package/src/utils/byte-range-context.ts +1 -1
  106. package/src/utils/content-type-parser.ts +3 -0
  107. package/src/utils/get-content-type.ts +5 -4
  108. package/src/utils/get-e-tag.ts +1 -1
  109. package/src/utils/get-offset-and-length.ts +54 -0
  110. package/src/utils/get-resolved-accept-header.ts +2 -2
  111. package/src/utils/handle-redirects.ts +10 -3
  112. package/src/utils/ipfs-path-to-string.ts +9 -0
  113. package/src/utils/is-accept-explicit.ts +14 -7
  114. package/src/utils/parse-url-string.ts +20 -286
  115. package/src/utils/response-headers.ts +1 -1
  116. package/src/utils/responses.ts +1 -1
  117. package/src/utils/select-output-type.ts +38 -44
  118. package/src/utils/server-timing.ts +17 -30
  119. package/src/utils/walk-path.ts +1 -1
  120. package/src/verified-fetch.ts +78 -69
  121. package/dist/src/types.d.ts +0 -16
  122. package/dist/src/types.d.ts.map +0 -1
  123. package/dist/src/types.js +0 -2
  124. package/dist/src/types.js.map +0 -1
  125. package/dist/src/utils/parse-resource.d.ts +0 -18
  126. package/dist/src/utils/parse-resource.d.ts.map +0 -1
  127. package/dist/src/utils/parse-resource.js +0 -27
  128. package/dist/src/utils/parse-resource.js.map +0 -1
  129. package/src/types.ts +0 -17
  130. package/src/utils/parse-resource.ts +0 -42
@@ -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 {
@@ -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
  }
@@ -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
  }