@helia/verified-fetch 4.0.0 → 4.0.2

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 (126) hide show
  1. package/README.md +7 -49
  2. package/dist/index.min.js +56 -51
  3. package/dist/index.min.js.map +4 -4
  4. package/dist/src/index.d.ts +12 -52
  5. package/dist/src/index.d.ts.map +1 -1
  6. package/dist/src/index.js +8 -50
  7. package/dist/src/index.js.map +1 -1
  8. package/dist/src/plugins/index.d.ts +0 -1
  9. package/dist/src/plugins/index.d.ts.map +1 -1
  10. package/dist/src/plugins/index.js +0 -1
  11. package/dist/src/plugins/index.js.map +1 -1
  12. package/dist/src/plugins/plugin-base.d.ts.map +1 -1
  13. package/dist/src/plugins/plugin-base.js +3 -2
  14. package/dist/src/plugins/plugin-base.js.map +1 -1
  15. package/dist/src/plugins/plugin-handle-car.d.ts.map +1 -1
  16. package/dist/src/plugins/plugin-handle-car.js +0 -1
  17. package/dist/src/plugins/plugin-handle-car.js.map +1 -1
  18. package/dist/src/plugins/plugin-handle-dag-cbor-html-preview.d.ts +3 -3
  19. package/dist/src/plugins/plugin-handle-dag-cbor-html-preview.d.ts.map +1 -1
  20. package/dist/src/plugins/plugin-handle-dag-cbor-html-preview.js +5 -6
  21. package/dist/src/plugins/plugin-handle-dag-cbor-html-preview.js.map +1 -1
  22. package/dist/src/plugins/plugin-handle-dag-cbor.d.ts.map +1 -1
  23. package/dist/src/plugins/plugin-handle-dag-cbor.js +0 -1
  24. package/dist/src/plugins/plugin-handle-dag-cbor.js.map +1 -1
  25. package/dist/src/plugins/plugin-handle-dag-pb.d.ts.map +1 -1
  26. package/dist/src/plugins/plugin-handle-dag-pb.js +34 -39
  27. package/dist/src/plugins/plugin-handle-dag-pb.js.map +1 -1
  28. package/dist/src/plugins/plugin-handle-dag-walk.d.ts +8 -4
  29. package/dist/src/plugins/plugin-handle-dag-walk.d.ts.map +1 -1
  30. package/dist/src/plugins/plugin-handle-dag-walk.js +8 -5
  31. package/dist/src/plugins/plugin-handle-dag-walk.js.map +1 -1
  32. package/dist/src/plugins/plugin-handle-ipns-record.d.ts +1 -1
  33. package/dist/src/plugins/plugin-handle-ipns-record.d.ts.map +1 -1
  34. package/dist/src/plugins/plugin-handle-ipns-record.js +4 -6
  35. package/dist/src/plugins/plugin-handle-ipns-record.js.map +1 -1
  36. package/dist/src/plugins/plugin-handle-json.d.ts.map +1 -1
  37. package/dist/src/plugins/plugin-handle-json.js +0 -1
  38. package/dist/src/plugins/plugin-handle-json.js.map +1 -1
  39. package/dist/src/plugins/plugin-handle-raw.d.ts.map +1 -1
  40. package/dist/src/plugins/plugin-handle-raw.js +4 -8
  41. package/dist/src/plugins/plugin-handle-raw.js.map +1 -1
  42. package/dist/src/plugins/plugin-handle-tar.d.ts.map +1 -1
  43. package/dist/src/plugins/plugin-handle-tar.js +0 -1
  44. package/dist/src/plugins/plugin-handle-tar.js.map +1 -1
  45. package/dist/src/plugins/types.d.ts +6 -11
  46. package/dist/src/plugins/types.d.ts.map +1 -1
  47. package/dist/src/url-resolver.d.ts +4 -3
  48. package/dist/src/url-resolver.d.ts.map +1 -1
  49. package/dist/src/url-resolver.js +35 -47
  50. package/dist/src/url-resolver.js.map +1 -1
  51. package/dist/src/utils/byte-range-context.d.ts +2 -2
  52. package/dist/src/utils/byte-range-context.d.ts.map +1 -1
  53. package/dist/src/utils/byte-range-context.js +1 -1
  54. package/dist/src/utils/byte-range-context.js.map +1 -1
  55. package/dist/src/utils/content-type-parser.d.ts.map +1 -1
  56. package/dist/src/utils/content-type-parser.js +0 -10
  57. package/dist/src/utils/content-type-parser.js.map +1 -1
  58. package/dist/src/utils/dnslink-label.d.ts +26 -0
  59. package/dist/src/utils/dnslink-label.d.ts.map +1 -0
  60. package/dist/src/utils/dnslink-label.js +35 -0
  61. package/dist/src/utils/dnslink-label.js.map +1 -0
  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 +1 -1
  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-stream-from-async-iterable.d.ts +1 -2
  71. package/dist/src/utils/get-stream-from-async-iterable.d.ts.map +1 -1
  72. package/dist/src/utils/get-stream-from-async-iterable.js +1 -3
  73. package/dist/src/utils/get-stream-from-async-iterable.js.map +1 -1
  74. package/dist/src/utils/handle-redirects.js +2 -2
  75. package/dist/src/utils/ipfs-path-to-url.d.ts +16 -0
  76. package/dist/src/utils/ipfs-path-to-url.d.ts.map +1 -0
  77. package/dist/src/utils/ipfs-path-to-url.js +45 -0
  78. package/dist/src/utils/ipfs-path-to-url.js.map +1 -0
  79. package/dist/src/utils/parse-url-string.d.ts +18 -5
  80. package/dist/src/utils/parse-url-string.d.ts.map +1 -1
  81. package/dist/src/utils/parse-url-string.js +126 -44
  82. package/dist/src/utils/parse-url-string.js.map +1 -1
  83. package/dist/src/utils/resource-to-cache-key.js +2 -2
  84. package/dist/src/utils/responses.d.ts +2 -1
  85. package/dist/src/utils/responses.d.ts.map +1 -1
  86. package/dist/src/utils/responses.js +16 -1
  87. package/dist/src/utils/responses.js.map +1 -1
  88. package/dist/src/utils/walk-path.js +1 -1
  89. package/dist/src/utils/walk-path.js.map +1 -1
  90. package/dist/src/verified-fetch.d.ts.map +1 -1
  91. package/dist/src/verified-fetch.js +36 -28
  92. package/dist/src/verified-fetch.js.map +1 -1
  93. package/dist/typedoc-urls.json +0 -2
  94. package/package.json +10 -10
  95. package/src/index.ts +12 -52
  96. package/src/plugins/index.ts +0 -1
  97. package/src/plugins/plugin-base.ts +3 -2
  98. package/src/plugins/plugin-handle-car.ts +0 -2
  99. package/src/plugins/plugin-handle-dag-cbor-html-preview.ts +10 -9
  100. package/src/plugins/plugin-handle-dag-cbor.ts +3 -1
  101. package/src/plugins/plugin-handle-dag-pb.ts +49 -37
  102. package/src/plugins/plugin-handle-dag-walk.ts +10 -5
  103. package/src/plugins/plugin-handle-ipns-record.ts +6 -6
  104. package/src/plugins/plugin-handle-json.ts +1 -1
  105. package/src/plugins/plugin-handle-raw.ts +7 -8
  106. package/src/plugins/plugin-handle-tar.ts +2 -1
  107. package/src/plugins/types.ts +7 -12
  108. package/src/url-resolver.ts +37 -56
  109. package/src/utils/byte-range-context.ts +3 -3
  110. package/src/utils/content-type-parser.ts +5 -11
  111. package/src/utils/dnslink-label.ts +38 -0
  112. package/src/utils/error-to-object.ts +22 -0
  113. package/src/utils/get-content-type.ts +2 -2
  114. package/src/utils/get-stream-from-async-iterable.ts +1 -4
  115. package/src/utils/handle-redirects.ts +2 -2
  116. package/src/utils/ipfs-path-to-url.ts +54 -0
  117. package/src/utils/parse-url-string.ts +166 -49
  118. package/src/utils/resource-to-cache-key.ts +2 -2
  119. package/src/utils/responses.ts +21 -1
  120. package/src/utils/walk-path.ts +1 -1
  121. package/src/verified-fetch.ts +46 -28
  122. package/dist/src/plugins/errors.d.ts +0 -25
  123. package/dist/src/plugins/errors.d.ts.map +0 -1
  124. package/dist/src/plugins/errors.js +0 -33
  125. package/dist/src/plugins/errors.js.map +0 -1
  126. package/src/plugins/errors.ts +0 -37
@@ -3,7 +3,6 @@ import { code as rawCode } from 'multiformats/codecs/raw'
3
3
  import { identity } from 'multiformats/hashes/identity'
4
4
  import { getContentType } from '../utils/get-content-type.js'
5
5
  import { notFoundResponse, okRangeResponse } from '../utils/responses.js'
6
- import { PluginFatalError } from './errors.js'
7
6
  import { BasePlugin } from './plugin-base.js'
8
7
  import type { PluginContext } from './types.js'
9
8
  import type { AcceptHeader } from '../utils/select-output-type.ts'
@@ -49,10 +48,10 @@ export class RawPlugin extends BasePlugin {
49
48
  codes: number[] = [rawCode, identity.code]
50
49
 
51
50
  canHandle ({ cid, accept, query, byteRangeContext }: PluginContext): boolean {
52
- this.log('checking if we can handle %c with accept %s', cid, accept)
53
51
  if (byteRangeContext == null) {
54
52
  return false
55
53
  }
54
+
56
55
  return accept?.mimeType === 'application/vnd.ipld.raw' || query.format === 'raw'
57
56
  }
58
57
 
@@ -66,21 +65,20 @@ export class RawPlugin extends BasePlugin {
66
65
  context.reqFormat = 'raw'
67
66
  context.query.download = true
68
67
  context.query.filename = context.query.filename ?? `${cid.toString()}.bin`
69
- log.trace('Set content disposition...')
68
+ log.trace('set content disposition to force download')
70
69
  } else {
71
- log.trace('Did NOT set content disposition...')
70
+ log.trace('did not set content disposition, raw block will display inline')
72
71
  }
73
72
 
74
- if (path !== '' && cid.code === rawCode) {
73
+ if (path.length > 0 && cid.code === rawCode) {
75
74
  log.trace('404-ing raw codec request for %c/%s', cid, path)
76
- // throw new PluginError('ERR_RAW_PATHS_NOT_SUPPORTED', 'Raw codec does not support paths')
77
- // return notFoundResponse(resource, 'Raw codec does not support paths')
78
- throw new PluginFatalError('ERR_RAW_PATHS_NOT_SUPPORTED', 'Raw codec does not support paths', { response: notFoundResponse(resource, 'Raw codec does not support paths') })
75
+ return notFoundResponse(resource)
79
76
  }
80
77
 
81
78
  const terminalCid = context.pathDetails?.terminalElement.cid ?? context.cid
82
79
  const blockstore = getBlockstore(terminalCid, resource, session, options)
83
80
  const result = await toBuffer(blockstore.get(terminalCid, options))
81
+
84
82
  context.byteRangeContext.setBody(result)
85
83
 
86
84
  // if the user has specified an `Accept` header that corresponds to a raw
@@ -94,6 +92,7 @@ export class RawPlugin extends BasePlugin {
94
92
  contentTypeParser,
95
93
  log
96
94
  })
95
+
97
96
  const response = okRangeResponse(resource, context.byteRangeContext.getBody(contentType), { byteRangeContext: context.byteRangeContext, log }, {
98
97
  redirected: false
99
98
  })
@@ -14,11 +14,12 @@ import type { PluginContext } from './types.js'
14
14
  export class TarPlugin extends BasePlugin {
15
15
  readonly id = 'tar-plugin'
16
16
  readonly codes = []
17
+
17
18
  canHandle ({ cid, accept, query, byteRangeContext }: PluginContext): boolean {
18
- this.log('checking if we can handle %c with accept %s', cid, accept)
19
19
  if (byteRangeContext == null) {
20
20
  return false
21
21
  }
22
+
22
23
  return accept?.mimeType === 'application/x-tar' || query.format === 'tar'
23
24
  }
24
25
 
@@ -1,11 +1,10 @@
1
- import type { PluginError } from './errors.js'
2
- import type { ResolveURLResult, UrlQuery, VerifiedFetchInit, ContentTypeParser, RequestFormatShorthand } from '../index.js'
1
+ import type { ResolveURLResult, VerifiedFetchInit, ContentTypeParser, RequestFormatShorthand } from '../index.js'
3
2
  import type { ByteRangeContext } from '../utils/byte-range-context.js'
4
3
  import type { AcceptHeader } from '../utils/select-output-type.ts'
5
4
  import type { ServerTiming } from '../utils/server-timing.ts'
6
5
  import type { PathWalkerResponse } from '../utils/walk-path.js'
7
6
  import type { IPNSResolver } from '@helia/ipns'
8
- import type { AbortOptions, ComponentLogger, Logger } from '@libp2p/interface'
7
+ import type { AbortOptions, Logger } from '@libp2p/interface'
9
8
  import type { Helia } from 'helia'
10
9
  import type { Blockstore } from 'interface-blockstore'
11
10
  import type { UnixFSEntry } from 'ipfs-unixfs-exporter'
@@ -18,7 +17,7 @@ import type { CustomProgressEvent } from 'progress-events'
18
17
  * - Persistent: Relevant even after the request completes (e.g., logging or metrics).
19
18
  */
20
19
  export interface PluginOptions {
21
- logger: ComponentLogger
20
+ logger: Logger
22
21
  getBlockstore(cid: CID, resource: string | CID, useSession?: boolean, options?: AbortOptions): Blockstore
23
22
  contentTypeParser?: ContentTypeParser
24
23
  helia: Helia
@@ -32,8 +31,6 @@ export interface PluginOptions {
32
31
  * - Ephemeral: Typically discarded once fetch(...) completes.
33
32
  */
34
33
  export interface PluginContext extends ResolveURLResult {
35
- readonly cid: CID
36
- readonly path: string
37
34
  readonly resource: string
38
35
  readonly accept?: AcceptHeader
39
36
 
@@ -51,10 +48,8 @@ export interface PluginContext extends ResolveURLResult {
51
48
  options?: Omit<VerifiedFetchInit, 'signal'> & AbortOptions
52
49
  isDirectory?: boolean
53
50
  directoryEntries?: UnixFSEntry[]
54
- errors?: PluginError[]
55
51
  reqFormat?: RequestFormatShorthand
56
52
  pathDetails?: PathWalkerResponse
57
- query: UrlQuery
58
53
 
59
54
  /**
60
55
  * ByteRangeContext contains information about the size of the content and range requests.
@@ -65,6 +60,10 @@ export interface PluginContext extends ResolveURLResult {
65
60
  byteRangeContext?: ByteRangeContext
66
61
  serverTiming: ServerTiming
67
62
  ipfsPath: string
63
+
64
+ /**
65
+ * Allow arbitrary keys/values
66
+ */
68
67
  [key: string]: unknown
69
68
  }
70
69
 
@@ -85,7 +84,3 @@ export interface PluginErrorOptions {
85
84
  details?: Record<string, any>
86
85
  response?: Response
87
86
  }
88
-
89
- export interface FatalPluginErrorOptions extends PluginErrorOptions {
90
- response: Response
91
- }
@@ -1,7 +1,8 @@
1
1
  import { peerIdFromCID, peerIdFromString } from '@libp2p/peer-id'
2
2
  import { CID } from 'multiformats/cid'
3
- import { matchURLString } from './utils/parse-url-string.ts'
3
+ import { parseURLString } from './utils/parse-url-string.ts'
4
4
  import type { ResolveURLOptions, ResolveURLResult, Resource, URLResolver as URLResolverInterface } from './index.ts'
5
+ import type { ParsedURL } from './utils/parse-url-string.ts'
5
6
  import type { ServerTiming } from './utils/server-timing.ts'
6
7
  import type { DNSLink } from '@helia/dnslink'
7
8
  import type { IPNSResolver } from '@helia/ipns'
@@ -15,29 +16,6 @@ export interface URLResolverComponents {
15
16
  timing: ServerTiming
16
17
  }
17
18
 
18
- function toQuery (query?: string): Record<string, any> {
19
- if (query == null) {
20
- return {}
21
- }
22
-
23
- const params = new URLSearchParams(query)
24
- const output: Record<string, any> = {}
25
-
26
- for (const [key, value] of params.entries()) {
27
- output[key] = value
28
-
29
- if (value === 'true') {
30
- output[key] = true
31
- }
32
-
33
- if (value === 'false') {
34
- output[key] = false
35
- }
36
- }
37
-
38
- return output
39
- }
40
-
41
19
  export class URLResolver implements URLResolverInterface {
42
20
  private readonly components: URLResolverComponents
43
21
 
@@ -53,56 +31,60 @@ export class URLResolver implements URLResolverInterface {
53
31
  const cid = CID.asCID(resource)
54
32
 
55
33
  if (cid != null) {
56
- return this.resolveCIDResource(cid, '', {}, options)
34
+ return this.resolveCIDResource(cid, {
35
+ url: new URL(`ipfs://${cid}`)
36
+ }, options)
57
37
  }
58
38
 
59
39
  throw new TypeError(`Invalid resource. Cannot determine CID from resource: ${resource}`)
60
40
  }
61
41
 
62
42
  async parseUrlString (urlString: string, options: ResolveURLOptions = {}): Promise<ResolveURLResult> {
63
- const { protocol, cidOrPeerIdOrDnsLink, path, query } = matchURLString(urlString)
43
+ const result = parseURLString(urlString)
64
44
 
65
- if (protocol === 'ipfs') {
66
- const cid = CID.parse(cidOrPeerIdOrDnsLink)
45
+ if (result.protocol === 'ipfs') {
46
+ const cid = CID.parse(result.cidOrPeerIdOrDnsLink)
67
47
 
68
- return this.resolveCIDResource(cid, path ?? '', toQuery(query), options)
48
+ return this.resolveCIDResource(cid, result, options)
69
49
  }
70
50
 
71
- if (protocol === 'ipns') {
51
+ if (result.protocol === 'ipns') {
72
52
  // try to parse target as peer id
73
53
  let peerId: PeerId
74
54
 
75
55
  try {
76
- peerId = peerIdFromString(cidOrPeerIdOrDnsLink)
56
+ peerId = peerIdFromString(result.cidOrPeerIdOrDnsLink)
77
57
  } catch {
78
58
  // fall back to DNSLink (e.g. /ipns/example.com)
79
- return this.resolveDNSLink(cidOrPeerIdOrDnsLink, path ?? '', toQuery(query), options)
59
+ return this.resolveDNSLink(result.cidOrPeerIdOrDnsLink, result, options)
80
60
  }
81
61
 
82
62
  // parse multihash from string (e.g. /ipns/QmFoo...)
83
- return this.resolveIPNSName(cidOrPeerIdOrDnsLink, peerId, path ?? '', toQuery(query), options)
63
+ return this.resolveIPNSName(result.cidOrPeerIdOrDnsLink, peerId, result, options)
84
64
  }
85
65
 
86
66
  throw new TypeError(`Invalid resource. Cannot determine CID from resource: ${urlString}`)
87
67
  }
88
68
 
89
- async resolveCIDResource (cid: CID, path: string, query: Record<string, any>, options: ResolveURLOptions = {}): Promise<ResolveURLResult> {
69
+ async resolveCIDResource (cid: CID, parsed: Partial<ParsedURL> & Pick<ParsedURL, 'url'>, options: ResolveURLOptions = {}): Promise<ResolveURLResult> {
90
70
  if (cid.code === CODEC_LIBP2P_KEY) {
91
71
  // special case - peer id encoded as a CID
92
- return this.resolveIPNSName(cid.toString(), peerIdFromCID(cid), path, query, options)
72
+ return this.resolveIPNSName(cid.toString(), peerIdFromCID(cid), parsed, options)
93
73
  }
94
74
 
95
75
  return {
76
+ url: parsed.url,
96
77
  cid,
97
78
  protocol: 'ipfs',
98
- query,
99
- path,
79
+ query: parsed.query ?? {},
80
+ path: parsed.path ?? [],
81
+ fragment: parsed.fragment ?? '',
100
82
  ttl: 29030400, // 1 year for ipfs content
101
- ipfsPath: `/ipfs/${cid}${path === '' ? '' : `/${path}`}`
83
+ ipfsPath: `/ipfs/${cid}${parsed.url.pathname}`
102
84
  }
103
85
  }
104
86
 
105
- async resolveDNSLink (domain: string, path: string, query: Record<string, any>, options?: ResolveURLOptions): Promise<ResolveURLResult> {
87
+ async resolveDNSLink (domain: string, parsed: ParsedURL, options?: ResolveURLOptions): Promise<ResolveURLResult> {
106
88
  const results = await this.components.timing.time('dnsLink.resolve', `Resolve DNSLink ${domain}`, this.components.dnsLink.resolve(domain, options))
107
89
  const result = results?.[0]
108
90
 
@@ -112,7 +94,7 @@ export class URLResolver implements URLResolverInterface {
112
94
 
113
95
  // dnslink resolved to IPNS name
114
96
  if (result.namespace === 'ipns') {
115
- return this.resolveIPNSName(domain, result.peerId, path, query, options)
97
+ return this.resolveIPNSName(domain, result.peerId, parsed, options)
116
98
  }
117
99
 
118
100
  // dnslink resolved to CID
@@ -122,38 +104,37 @@ export class URLResolver implements URLResolverInterface {
122
104
  }
123
105
 
124
106
  return {
107
+ url: parsed.url,
125
108
  cid: result.cid,
126
- path: concatPaths(result.path, path),
109
+ path: concatPaths(...(result.path ?? '').split('/'), ...(parsed.path ?? [])),
110
+ fragment: parsed.fragment,
127
111
  // dnslink is mutable so return 'ipns' protocol so we do not include immutable in cache-control header
128
112
  protocol: 'ipns',
129
113
  ttl: result.answer.TTL,
130
- query,
131
- ipfsPath: `/ipns/${domain}${path === '' ? '' : `/${path}`}`
114
+ query: parsed.query,
115
+ ipfsPath: `/ipns/${domain}${parsed.url.pathname}`
132
116
  }
133
117
  }
134
118
 
135
- async resolveIPNSName (resource: string, key: PeerId, path: string, query: Record<string, any>, options?: AbortOptions): Promise<ResolveURLResult> {
119
+ async resolveIPNSName (resource: string, key: PeerId, parsed: Partial<ParsedURL> & Pick<ParsedURL, 'url'>, options?: AbortOptions): Promise<ResolveURLResult> {
136
120
  const result = await this.components.timing.time('ipns.resolve', `Resolve IPNS name ${key}`, this.components.ipnsResolver.resolve(key, options))
137
121
 
138
122
  return {
123
+ url: parsed.url,
139
124
  cid: result.cid,
140
- path: concatPaths(result.path, path),
141
- query,
125
+ path: concatPaths(...(result.path ?? '').split('/'), ...(parsed.path ?? [])),
126
+ query: parsed.query ?? {},
127
+ fragment: parsed.fragment ?? '',
142
128
  protocol: 'ipns',
143
129
  // IPNS ttl is in nanoseconds, convert to seconds
144
130
  ttl: Number((result.record.ttl ?? 0n) / BigInt(1e9)),
145
- ipfsPath: `/ipns/${resource}${path === '' ? '' : `/${path}`}`
131
+ ipfsPath: `/ipns/${resource}${parsed.url.pathname}`
146
132
  }
147
133
  }
148
134
  }
149
135
 
150
- function concatPaths (...paths: Array<string | undefined>): string {
151
- return `${
152
- paths
153
- .filter(p => p != null && p !== '')
154
- .join('/')
155
- .replaceAll(/(\/+)/g, '/')
156
- .replace(/^(\/)+/, '')
157
- .replace(/(\/)+$/, '/')
158
- }`
136
+ function concatPaths (...paths: Array<string | undefined>): string[] {
137
+ // @ts-expect-error undefined is filtered out
138
+ return paths
139
+ .filter(p => p != null && p !== '')
159
140
  }
@@ -3,7 +3,7 @@ import { InvalidRangeError } from '../errors.js'
3
3
  import { calculateByteRangeIndexes, getHeader } from './request-headers.js'
4
4
  import { getContentRangeHeader } from './response-headers.js'
5
5
  import type { SupportedBodyTypes } from '../index.js'
6
- import type { ComponentLogger, Logger } from '@libp2p/interface'
6
+ import type { Logger } from '@libp2p/interface'
7
7
 
8
8
  type SliceableBody = Exclude<SupportedBodyTypes, ReadableStream<Uint8Array> | null>
9
9
 
@@ -90,8 +90,8 @@ export class ByteRangeContext {
90
90
  // to be set by isValidRangeRequest so that we don't need to re-check the byteRanges
91
91
  private _isValidRangeRequest: boolean = false
92
92
 
93
- constructor (logger: ComponentLogger, private readonly headers?: HeadersInit) {
94
- this.log = logger.forComponent('helia:verified-fetch:byte-range-context')
93
+ constructor (logger: Logger, private readonly headers?: HeadersInit) {
94
+ this.log = logger.newScope('byte-range-context')
95
95
  this.rangeRequestHeader = getHeader(this.headers, 'Range')
96
96
 
97
97
  if (this.rangeRequestHeader != null) {
@@ -1,28 +1,22 @@
1
- import { logger } from '@libp2p/logger'
2
1
  import { fileTypeFromBuffer } from 'file-type'
3
2
 
4
- const log = logger('helia:verified-fetch:content-type-parser')
5
-
6
3
  export const defaultMimeType = 'application/octet-stream'
7
4
  function checkForSvg (text: string): boolean {
8
- log('checking for svg')
9
5
  return /^(<\?xml[^>]+>)?[^<^\w]+<svg/ig.test(text)
10
6
  }
11
7
 
12
8
  async function checkForJson (text: string): Promise<boolean> {
13
- log('checking for json')
14
9
  try {
15
10
  JSON.parse(text)
16
11
  return true
17
12
  } catch (err) {
18
- log('failed to parse as json', err)
19
13
  return false
20
14
  }
21
15
  }
22
16
 
23
17
  function getText (bytes: Uint8Array): string | null {
24
- log('checking for text')
25
18
  const decoder = new TextDecoder('utf-8', { fatal: true })
19
+
26
20
  try {
27
21
  return decoder.decode(bytes)
28
22
  } catch (err) {
@@ -31,25 +25,24 @@ function getText (bytes: Uint8Array): string | null {
31
25
  }
32
26
 
33
27
  async function checkForHtml (text: string): Promise<boolean> {
34
- log('checking for html')
35
28
  return /^\s*<(?:!doctype\s+html|html|head|body)\b/i.test(text)
36
29
  }
37
30
 
38
31
  export async function contentTypeParser (bytes: Uint8Array, fileName?: string): Promise<string> {
39
- log('contentTypeParser called for fileName: %s, byte size=%s', fileName, bytes.length)
40
32
  const detectedType = (await fileTypeFromBuffer(bytes))?.mime
33
+
41
34
  if (detectedType != null) {
42
- log('detectedType: %s', detectedType)
43
35
  if (detectedType === 'application/xml' && fileName?.toLowerCase().endsWith('.svg')) {
44
36
  return 'image/svg+xml'
45
37
  }
38
+
46
39
  return detectedType
47
40
  }
48
- log('no detectedType')
49
41
 
50
42
  if (fileName == null) {
51
43
  // it's likely text... no other way to determine file-type.
52
44
  const text = getText(bytes)
45
+
53
46
  if (text != null) {
54
47
  // check for svg, json, html, or it's plain text.
55
48
  if (checkForSvg(text)) {
@@ -62,6 +55,7 @@ export async function contentTypeParser (bytes: Uint8Array, fileName?: string):
62
55
  return 'text/plain; charset=utf-8'
63
56
  }
64
57
  }
58
+
65
59
  return defaultMimeType
66
60
  }
67
61
 
@@ -0,0 +1,38 @@
1
+ /**
2
+ * For DNSLink see https://specs.ipfs.tech/http-gateways/subdomain-gateway/#host-request-header
3
+ * DNSLink names include . which means they must be inlined into a single DNS label to provide unique origin and work with wildcard TLS certificates.
4
+ */
5
+
6
+ // DNS label can have up to 63 characters, consisting of alphanumeric
7
+ // characters or hyphens -, but it must not start or end with a hyphen.
8
+ const dnsLabelRegex = /^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$/
9
+
10
+ /**
11
+ * Checks if label looks like inlined DNSLink.
12
+ * (https://specs.ipfs.tech/http-gateways/subdomain-gateway/#host-request-header)
13
+ */
14
+ export function isInlinedDnsLink (label: string): boolean {
15
+ return dnsLabelRegex.test(label) && label.includes('-') && !label.includes('.')
16
+ }
17
+
18
+ /**
19
+ * DNSLink label decoding
20
+ * - Every standalone - is replaced with .
21
+ * - Every remaining -- is replaced with -
22
+ *
23
+ * @example en-wikipedia--on--ipfs-org -> en.wikipedia-on-ipfs.org
24
+ */
25
+ export function decodeDNSLinkLabel (label: string): string {
26
+ return label.replace(/--/g, '%').replace(/-/g, '.').replace(/%/g, '-')
27
+ }
28
+
29
+ /**
30
+ * DNSLink label encoding
31
+ * - Every - is replaced with --
32
+ * - Every . is replaced with -
33
+ *
34
+ * @example en.wikipedia-on-ipfs.org -> en-wikipedia--on--ipfs-org
35
+ */
36
+ export function encodeDNSLinkLabel (name: string): string {
37
+ return name.replace(/-/g, '--').replace(/\./g, '-')
38
+ }
@@ -0,0 +1,22 @@
1
+ function isAggregateError (err?: any): err is AggregateError {
2
+ return err instanceof AggregateError || (err?.name === 'AggregateError' && Array.isArray(err?.errors))
3
+ }
4
+
5
+ /**
6
+ * Error instance properties are not enumerable so we must transform the error
7
+ * into a plain object if we want to pass it to `JSON.stringify` or similar.
8
+ */
9
+ export function errorToObject (err: Error): any {
10
+ let errors
11
+
12
+ if (isAggregateError(err)) {
13
+ errors = err.errors.map(err => errorToObject(err))
14
+ }
15
+
16
+ return {
17
+ name: err.name,
18
+ message: err.message,
19
+ stack: err.stack,
20
+ errors
21
+ }
22
+ }
@@ -5,7 +5,7 @@ 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
10
  contentTypeParser?: ContentTypeParser
11
11
  log: Logger
@@ -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?.[path.length - 1]?.trim()
29
29
  fileName = (fileName === '' || fileName?.split('.').length === 1) ? undefined : fileName
30
30
  } else {
31
31
  fileName = filenameParam
@@ -2,18 +2,15 @@ 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'
6
5
 
7
6
  /**
8
7
  * Converts an async iterator of Uint8Array bytes to a stream and returns the first chunk of bytes
9
8
  */
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')
9
+ export async function getStreamFromAsyncIterable (iterator: AsyncIterable<Uint8Array>, options?: Pick<VerifiedFetchInit, 'onProgress' | 'signal'>): Promise<{ stream: ReadableStream<Uint8Array>, firstChunk: Uint8Array }> {
12
10
  const reader = iterator[Symbol.asyncIterator]()
13
11
  const { value: firstChunk, done } = await reader.next()
14
12
 
15
13
  if (done === true) {
16
- log.error('no content found for path', path)
17
14
  throw new NoContentError()
18
15
  }
19
16
 
@@ -1,5 +1,5 @@
1
1
  import { SubdomainNotSupportedError } from '../errors.js'
2
- import { matchURLString } from './parse-url-string.js'
2
+ import { parseURLString } from './parse-url-string.js'
3
3
  import { movedPermanentlyResponse } from './responses.js'
4
4
  import type { VerifiedFetchInit, Resource } from '../index.js'
5
5
  import type { AbortOptions, ComponentLogger } from '@libp2p/interface'
@@ -47,7 +47,7 @@ export async function getRedirectResponse ({ resource, options, logger, cid, fet
47
47
  // if x-forwarded-host is passed, we need to set the location header to the
48
48
  // subdomain so that the browser can redirect to the correct subdomain
49
49
  try {
50
- const urlParts = matchURLString(resource)
50
+ const urlParts = parseURLString(resource)
51
51
  const reqUrl = new URL(resource)
52
52
  const actualHost = forwardedHost ?? reqUrl.host
53
53
  const subdomainUrl = new URL(reqUrl)
@@ -0,0 +1,54 @@
1
+ import { InvalidParametersError } from '@libp2p/interface'
2
+
3
+ /**
4
+ * Turns an IPFS or IPNS path into a HTTP URL. Path gateway syntax is used to
5
+ * preserve any case sensitivity
6
+ *
7
+ * - `/ipfs/cid` -> `https://example.org/ipfs/cid`
8
+ * - `/ipns/name` -> `https://example.org/ipns/name`
9
+ */
10
+ export function ipfsPathToUrl (path: string): string {
11
+ if (!path.startsWith('/ipfs/') && !path.startsWith('/ipns/')) {
12
+ throw new InvalidParametersError(`Path ${path} did not start with /ipfs/ or /ipns/`)
13
+ }
14
+
15
+ // trim fragment
16
+ const fragmentIndex = path.indexOf('#')
17
+ let fragment = ''
18
+
19
+ if (fragmentIndex > -1) {
20
+ fragment = path.substring(fragmentIndex)
21
+ path = path.substring(0, fragmentIndex)
22
+ }
23
+
24
+ // trim query
25
+ const queryIndex = path.indexOf('?')
26
+ let query = ''
27
+
28
+ if (queryIndex > -1) {
29
+ query = path.substring(queryIndex)
30
+ path = path.substring(0, queryIndex)
31
+ }
32
+
33
+ const type = path.substring(1, 5)
34
+ const rest = path.substring(6)
35
+
36
+ return `https://example.org/${type}/${rest}${query}${fragment}`
37
+ }
38
+
39
+ /**
40
+ * Turns an IPFS or IPNS URL into a HTTP URL. Path gateway syntax is used to
41
+ * preserve and case sensitivity
42
+ *
43
+ * `ipfs://cid` -> `https://example.org/ipfs/cid`
44
+ */
45
+ export function ipfsUrlToUrl (url: string): string {
46
+ if (!url.startsWith('ipfs://') && !url.startsWith('ipns://')) {
47
+ throw new InvalidParametersError(`URL ${url} did not start with ipfs:// or ipns://`)
48
+ }
49
+
50
+ const type = url.substring(0, 4)
51
+ const rest = url.substring(7)
52
+
53
+ return `https://example.org/${type}/${rest}`
54
+ }