@helia/ipns 2.0.3 → 3.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 (58) hide show
  1. package/README.md +92 -25
  2. package/dist/index.min.js +24 -24
  3. package/dist/src/dns-resolvers/default.d.ts +3 -0
  4. package/dist/src/dns-resolvers/default.d.ts.map +1 -0
  5. package/dist/src/dns-resolvers/default.js +8 -0
  6. package/dist/src/dns-resolvers/default.js.map +1 -0
  7. package/dist/src/dns-resolvers/dns-json-over-https.d.ts +18 -0
  8. package/dist/src/dns-resolvers/dns-json-over-https.d.ts.map +1 -0
  9. package/dist/src/dns-resolvers/dns-json-over-https.js +72 -0
  10. package/dist/src/dns-resolvers/dns-json-over-https.js.map +1 -0
  11. package/dist/src/dns-resolvers/dns-over-https.d.ts +16 -0
  12. package/dist/src/dns-resolvers/dns-over-https.d.ts.map +1 -0
  13. package/dist/src/dns-resolvers/dns-over-https.js +123 -0
  14. package/dist/src/dns-resolvers/dns-over-https.js.map +1 -0
  15. package/dist/src/dns-resolvers/index.d.ts +3 -0
  16. package/dist/src/dns-resolvers/index.d.ts.map +1 -0
  17. package/dist/src/dns-resolvers/index.js +3 -0
  18. package/dist/src/dns-resolvers/index.js.map +1 -0
  19. package/dist/src/dns-resolvers/resolver.browser.d.ts +4 -0
  20. package/dist/src/dns-resolvers/resolver.browser.d.ts.map +1 -0
  21. package/dist/src/dns-resolvers/resolver.browser.js +39 -0
  22. package/dist/src/dns-resolvers/resolver.browser.js.map +1 -0
  23. package/dist/src/dns-resolvers/resolver.d.ts +4 -0
  24. package/dist/src/dns-resolvers/resolver.d.ts.map +1 -0
  25. package/dist/src/dns-resolvers/resolver.js +21 -0
  26. package/dist/src/dns-resolvers/resolver.js.map +1 -0
  27. package/dist/src/index.d.ts +105 -10
  28. package/dist/src/index.d.ts.map +1 -1
  29. package/dist/src/index.js +84 -12
  30. package/dist/src/index.js.map +1 -1
  31. package/dist/src/routing/dht.js.map +1 -1
  32. package/dist/src/routing/local-store.js.map +1 -1
  33. package/dist/src/routing/pubsub.js.map +1 -1
  34. package/dist/src/utils/dns.d.ts +35 -0
  35. package/dist/src/utils/dns.d.ts.map +1 -0
  36. package/dist/src/utils/dns.js +79 -0
  37. package/dist/src/utils/dns.js.map +1 -0
  38. package/dist/src/utils/tlru.js.map +1 -1
  39. package/dist/typedoc-urls.json +8 -0
  40. package/package.json +14 -4
  41. package/src/dns-resolvers/default.ts +9 -0
  42. package/src/dns-resolvers/dns-json-over-https.ts +90 -0
  43. package/src/dns-resolvers/dns-over-https.ts +146 -0
  44. package/src/dns-resolvers/index.ts +2 -0
  45. package/src/dns-resolvers/resolver.browser.ts +50 -0
  46. package/src/dns-resolvers/resolver.ts +25 -0
  47. package/src/index.ts +124 -14
  48. package/src/utils/dns.ts +126 -0
  49. package/dist/src/utils/resolve-dns-link.browser.d.ts +0 -6
  50. package/dist/src/utils/resolve-dns-link.browser.d.ts.map +0 -1
  51. package/dist/src/utils/resolve-dns-link.browser.js +0 -46
  52. package/dist/src/utils/resolve-dns-link.browser.js.map +0 -1
  53. package/dist/src/utils/resolve-dns-link.d.ts +0 -3
  54. package/dist/src/utils/resolve-dns-link.d.ts.map +0 -1
  55. package/dist/src/utils/resolve-dns-link.js +0 -54
  56. package/dist/src/utils/resolve-dns-link.js.map +0 -1
  57. package/src/utils/resolve-dns-link.browser.ts +0 -61
  58. package/src/utils/resolve-dns-link.ts +0 -65
@@ -0,0 +1,146 @@
1
+ /* eslint-env browser */
2
+
3
+ import { Buffer } from 'buffer'
4
+ import dnsPacket, { type DecodedPacket } from 'dns-packet'
5
+ import { base64url } from 'multiformats/bases/base64'
6
+ import PQueue from 'p-queue'
7
+ import { CustomProgressEvent } from 'progress-events'
8
+ import { toString as uint8ArrayToString } from 'uint8arrays/to-string'
9
+ import { type DNSResponse, MAX_RECURSIVE_DEPTH, recursiveResolveDnslink, ipfsPathAndAnswer } from '../utils/dns.js'
10
+ import { TLRU } from '../utils/tlru.js'
11
+ import type { ResolveDnsLinkOptions, DNSResolver } from '../index.js'
12
+
13
+ // Avoid sending multiple queries for the same hostname by caching results
14
+ const cache = new TLRU<string>(1000)
15
+ // This TTL will be used if the remote service does not return one
16
+ const ttl = 60 * 1000
17
+
18
+ /**
19
+ * Uses the RFC 1035 'application/dns-message' content-type to resolve DNS
20
+ * queries.
21
+ *
22
+ * This resolver needs more dependencies than the non-standard
23
+ * DNS-JSON-over-HTTPS resolver so can result in a larger bundle size and
24
+ * consequently is not preferred for browser use.
25
+ *
26
+ * @see https://datatracker.ietf.org/doc/html/rfc1035
27
+ * @see https://developers.cloudflare.com/1.1.1.1/encryption/dns-over-https/make-api-requests/dns-wireformat/
28
+ * @see https://github.com/curl/curl/wiki/DNS-over-HTTPS#publicly-available-servers
29
+ * @see https://dnsprivacy.org/public_resolvers/
30
+ */
31
+ export function dnsOverHttps (url: string): DNSResolver {
32
+ // browsers limit concurrent connections per host,
33
+ // we don't want preload calls to exhaust the limit (~6)
34
+ const httpQueue = new PQueue({ concurrency: 4 })
35
+
36
+ const resolve = async (fqdn: string, options: ResolveDnsLinkOptions = {}): Promise<string> => {
37
+ const dnsQuery = dnsPacket.encode({
38
+ type: 'query',
39
+ id: 0,
40
+ flags: dnsPacket.RECURSION_DESIRED,
41
+ questions: [{
42
+ type: 'TXT',
43
+ name: fqdn
44
+ }]
45
+ })
46
+
47
+ const searchParams = new URLSearchParams()
48
+ searchParams.set('dns', base64url.encode(dnsQuery).substring(1))
49
+
50
+ const query = searchParams.toString()
51
+
52
+ // try cache first
53
+ if (options.nocache !== true && cache.has(query)) {
54
+ const response = cache.get(query)
55
+
56
+ if (response != null) {
57
+ options.onProgress?.(new CustomProgressEvent<string>('dnslink:cache', { detail: response }))
58
+ return response
59
+ }
60
+ }
61
+
62
+ options.onProgress?.(new CustomProgressEvent<string>('dnslink:query', { detail: fqdn }))
63
+
64
+ // query DNS over HTTPS server
65
+ const response = await httpQueue.add(async () => {
66
+ const res = await fetch(`${url}?${searchParams}`, {
67
+ headers: {
68
+ accept: 'application/dns-message'
69
+ },
70
+ signal: options.signal
71
+ })
72
+
73
+ if (res.status !== 200) {
74
+ throw new Error(`Unexpected HTTP status: ${res.status} - ${res.statusText}`)
75
+ }
76
+
77
+ const query = new URL(res.url).search.slice(1)
78
+ const buf = await res.arrayBuffer()
79
+ // map to expected response format
80
+ const json = toDNSResponse(dnsPacket.decode(Buffer.from(buf)))
81
+
82
+ options.onProgress?.(new CustomProgressEvent<DNSResponse>('dnslink:answer', { detail: json }))
83
+
84
+ const { ipfsPath, answer } = ipfsPathAndAnswer(fqdn, json)
85
+
86
+ cache.set(query, ipfsPath, answer.TTL ?? ttl)
87
+
88
+ return ipfsPath
89
+ }, {
90
+ signal: options.signal
91
+ })
92
+
93
+ if (response == null) {
94
+ throw new Error('No DNS response received')
95
+ }
96
+
97
+ return response
98
+ }
99
+
100
+ return async (domain: string, options: ResolveDnsLinkOptions = {}) => {
101
+ return recursiveResolveDnslink(domain, MAX_RECURSIVE_DEPTH, resolve, options)
102
+ }
103
+ }
104
+
105
+ function toDNSResponse (response: DecodedPacket): DNSResponse {
106
+ const txtType = 16
107
+
108
+ return {
109
+ Status: 0,
110
+ TC: response.flag_tc ?? false,
111
+ RD: response.flag_rd ?? false,
112
+ RA: response.flag_ra ?? false,
113
+ AD: response.flag_ad ?? false,
114
+ CD: response.flag_cd ?? false,
115
+ Question: response.questions?.map(q => ({
116
+ name: q.name,
117
+ type: txtType
118
+ })) ?? [],
119
+ Answer: response.answers?.map(a => {
120
+ if (a.type !== 'TXT' || a.data.length < 1) {
121
+ return {
122
+ name: a.name,
123
+ type: txtType,
124
+ TTL: 0,
125
+ data: 'invalid'
126
+ }
127
+ }
128
+
129
+ if (!Buffer.isBuffer(a.data[0])) {
130
+ return {
131
+ name: a.name,
132
+ type: txtType,
133
+ TTL: a.ttl ?? ttl,
134
+ data: String(a.data[0])
135
+ }
136
+ }
137
+
138
+ return {
139
+ name: a.name,
140
+ type: txtType,
141
+ TTL: a.ttl ?? ttl,
142
+ data: uint8ArrayToString(a.data[0])
143
+ }
144
+ }) ?? []
145
+ }
146
+ }
@@ -0,0 +1,2 @@
1
+ export { dnsOverHttps } from './dns-over-https.js'
2
+ export { dnsJsonOverHttps } from './dns-json-over-https.js'
@@ -0,0 +1,50 @@
1
+ import Resolver from 'dns-over-http-resolver'
2
+ import PQueue from 'p-queue'
3
+ import { CustomProgressEvent } from 'progress-events'
4
+ import { resolveFn, type DNSResponse } from '../utils/dns.js'
5
+ import { TLRU } from '../utils/tlru.js'
6
+ import type { DNSResolver } from '../index.js'
7
+
8
+ const cache = new TLRU<string>(1000)
9
+ // We know browsers themselves cache DNS records for at least 1 minute,
10
+ // which acts a provisional default ttl: https://stackoverflow.com/a/36917902/11518426
11
+ const ttl = 60 * 1000
12
+
13
+ // browsers limit concurrent connections per host,
14
+ // we don't want to exhaust the limit (~6)
15
+ const httpQueue = new PQueue({ concurrency: 4 })
16
+
17
+ const resolve: DNSResolver = async function resolve (domain, options = {}) {
18
+ const resolver = new Resolver({ maxCache: 0 })
19
+ // try cache first
20
+ if (options.nocache !== true && cache.has(domain)) {
21
+ const response = cache.get(domain)
22
+
23
+ if (response != null) {
24
+ options?.onProgress?.(new CustomProgressEvent<string>('dnslink:cache', { detail: response }))
25
+ return response
26
+ }
27
+ }
28
+
29
+ options.onProgress?.(new CustomProgressEvent<string>('dnslink:query', { detail: domain }))
30
+
31
+ // Add the query to the queue
32
+ const response = await httpQueue.add(async () => {
33
+ const dnslinkRecord = await resolveFn(resolver, domain)
34
+
35
+ options.onProgress?.(new CustomProgressEvent<DNSResponse>('dnslink:answer', { detail: dnslinkRecord }))
36
+ cache.set(domain, dnslinkRecord, ttl)
37
+
38
+ return dnslinkRecord
39
+ }, {
40
+ signal: options?.signal
41
+ })
42
+
43
+ if (response == null) {
44
+ throw new Error('No DNS response received')
45
+ }
46
+
47
+ return response
48
+ }
49
+
50
+ export default resolve
@@ -0,0 +1,25 @@
1
+ import { Resolver } from 'dns/promises'
2
+ import { CustomProgressEvent } from 'progress-events'
3
+ import { resolveFn, type DNSResponse } from '../utils/dns.js'
4
+ import type { DNSResolver } from '../index.js'
5
+
6
+ const resolve: DNSResolver = async function resolve (domain, options = {}) {
7
+ const resolver = new Resolver()
8
+ const listener = (): void => {
9
+ resolver.cancel()
10
+ }
11
+
12
+ options.signal?.addEventListener('abort', listener)
13
+
14
+ try {
15
+ options.onProgress?.(new CustomProgressEvent<string>('dnslink:query', { detail: domain }))
16
+ const dnslinkRecord = await resolveFn(resolver, domain)
17
+
18
+ options.onProgress?.(new CustomProgressEvent<DNSResponse>('dnslink:answer', { detail: dnslinkRecord }))
19
+ return dnslinkRecord
20
+ } finally {
21
+ options.signal?.removeEventListener('abort', listener)
22
+ }
23
+ }
24
+
25
+ export default resolve
package/src/index.ts CHANGED
@@ -5,16 +5,21 @@
5
5
  *
6
6
  * @example
7
7
  *
8
+ * With {@link IPNSRouting} routers:
9
+ *
8
10
  * ```typescript
9
11
  * import { createHelia } from 'helia'
12
+ * import { ipns } from '@helia/ipns'
10
13
  * import { dht, pubsub } from '@helia/ipns/routing'
11
14
  * import { unixfs } from '@helia/unixfs'
12
15
  *
13
16
  * const helia = await createHelia()
14
- * const name = ipns(helia, [
15
- * dht(helia),
16
- * pubsub(helia)
17
- * ])
17
+ * const name = ipns(helia, {
18
+ * routers: [
19
+ * dht(helia),
20
+ * pubsub(helia)
21
+ * ]
22
+ * })
18
23
  *
19
24
  * // create a public key to publish as an IPNS name
20
25
  * const keyInfo = await helia.libp2p.keychain.createKey('my-key')
@@ -33,13 +38,77 @@
33
38
  *
34
39
  * @example
35
40
  *
41
+ * With default {@link DNSResolver} resolvers:
42
+ *
43
+ * ```typescript
44
+ * import { createHelia } from 'helia'
45
+ * import { ipns } from '@helia/ipns'
46
+ * import { unixfs } from '@helia/unixfs'
47
+ * import { dnsOverHttps } from '@helia/ipns/dns-resolvers'
48
+ *
49
+ * const helia = await createHelia()
50
+ * const name = ipns(helia, {
51
+ * resolvers: [
52
+ * dnsOverHttps('https://private-dns-server.me/dns-query'),
53
+ * ]
54
+ * })
55
+ *
56
+ * const cid = name.resolveDns('some-domain-with-dnslink-entry.com')
57
+ * ```
58
+ *
59
+ * @example
60
+ *
61
+ * Calling `resolveDns` with the `@helia/ipns` instance:
62
+ *
36
63
  * ```typescript
37
- * // resolve a CID from a TXT record in a DNS zone file, eg:
38
- * // > dig ipfs.io TXT
64
+ * // resolve a CID from a TXT record in a DNS zone file, using the default
65
+ * // resolver for the current platform eg:
66
+ * // > dig _dnslink.ipfs.io TXT
39
67
  * // ;; ANSWER SECTION:
40
- * // ipfs.io. 435 IN TXT "dnslink=/ipfs/Qmfoo"
68
+ * // _dnslink.ipfs.io. 60 IN TXT "dnslink=/ipns/website.ipfs.io"
69
+ * // > dig _dnslink.website.ipfs.io TXT
70
+ * // ;; ANSWER SECTION:
71
+ * // _dnslink.website.ipfs.io. 60 IN TXT "dnslink=/ipfs/QmWebsite"
41
72
  *
42
73
  * const cid = name.resolveDns('ipfs.io')
74
+ *
75
+ * console.info(cid)
76
+ * // QmWebsite
77
+ * ```
78
+ *
79
+ * @example
80
+ *
81
+ * This example uses the Mozilla provided RFC 1035 DNS over HTTPS service. This
82
+ * uses binary DNS records so requires extra dependencies to process the
83
+ * response which can increase browser bundle sizes.
84
+ *
85
+ * If this is a concern, use the DNS-JSON-Over-HTTPS resolver instead.
86
+ *
87
+ * ```typescript
88
+ * // use DNS-Over-HTTPS
89
+ * import { dnsOverHttps } from '@helia/ipns/dns-resolvers'
90
+ *
91
+ * const cid = name.resolveDns('ipfs.io', {
92
+ * resolvers: [
93
+ * dnsOverHttps('https://mozilla.cloudflare-dns.com/dns-query')
94
+ * ]
95
+ * })
96
+ * ```
97
+ *
98
+ * @example
99
+ *
100
+ * DNS-JSON-Over-HTTPS resolvers use the RFC 8427 `application/dns-json` and can
101
+ * result in a smaller browser bundle due to the response being plain JSON.
102
+ *
103
+ * ```typescript
104
+ * // use DNS-JSON-Over-HTTPS
105
+ * import { dnsJsonOverHttps } from '@helia/ipns/dns-resolvers'
106
+ *
107
+ * const cid = name.resolveDns('ipfs.io', {
108
+ * resolvers: [
109
+ * dnsJsonOverHttps('https://mozilla.cloudflare-dns.com/dns-query')
110
+ * ]
111
+ * })
43
112
  * ```
44
113
  */
45
114
 
@@ -51,9 +120,10 @@ import { ipnsSelector } from 'ipns/selector'
51
120
  import { ipnsValidator } from 'ipns/validator'
52
121
  import { CID } from 'multiformats/cid'
53
122
  import { CustomProgressEvent } from 'progress-events'
123
+ import { defaultResolver } from './dns-resolvers/default.js'
54
124
  import { localStore, type LocalStore } from './routing/local-store.js'
55
- import { resolveDnslink } from './utils/resolve-dns-link.js'
56
125
  import type { IPNSRouting, IPNSRoutingEvents } from './routing/index.js'
126
+ import type { DNSResponse } from './utils/dns.js'
57
127
  import type { AbortOptions } from '@libp2p/interface'
58
128
  import type { PeerId } from '@libp2p/interface/peer-id'
59
129
  import type { Datastore } from 'interface-datastore'
@@ -83,6 +153,11 @@ export type RepublishProgressEvents =
83
153
  ProgressEvent<'ipns:republish:success', IPNSRecord> |
84
154
  ProgressEvent<'ipns:republish:error', { record: IPNSRecord, err: Error }>
85
155
 
156
+ export type ResolveDnsLinkProgressEvents =
157
+ ProgressEvent<'dnslink:cache', string> |
158
+ ProgressEvent<'dnslink:query', string> |
159
+ ProgressEvent<'dnslink:answer', DNSResponse>
160
+
86
161
  export interface PublishOptions extends AbortOptions, ProgressOptions<PublishProgressEvents | IPNSRoutingEvents> {
87
162
  /**
88
163
  * Time duration of the record in ms (default: 24hrs)
@@ -108,11 +183,35 @@ export interface ResolveOptions extends AbortOptions, ProgressOptions<ResolvePro
108
183
  offline?: boolean
109
184
  }
110
185
 
111
- export interface ResolveDNSOptions extends ResolveOptions {
186
+ export interface ResolveDnsLinkOptions extends AbortOptions, ProgressOptions<ResolveDnsLinkProgressEvents> {
187
+ /**
188
+ * Do not use cached DNS entries (default: false)
189
+ */
190
+ nocache?: boolean
191
+ }
192
+
193
+ export interface DNSResolver {
194
+ (domain: string, options?: ResolveDnsLinkOptions): Promise<string>
195
+ }
196
+
197
+ export interface ResolveDNSOptions extends AbortOptions, ProgressOptions<ResolveProgressEvents | IPNSRoutingEvents | ResolveDnsLinkProgressEvents> {
198
+ /**
199
+ * Do not query the network for the IPNS record (default: false)
200
+ */
201
+ offline?: boolean
202
+
112
203
  /**
113
204
  * Do not use cached DNS entries (default: false)
114
205
  */
115
206
  nocache?: boolean
207
+
208
+ /**
209
+ * These resolvers will be used to resolve the dnslink entries, if unspecified node will
210
+ * fall back to the `dns` module and browsers fall back to querying google/cloudflare DoH
211
+ *
212
+ * @see https://github.com/ipfs/helia-ipns/pull/55#discussion_r1270096881
213
+ */
214
+ resolvers?: DNSResolver[]
116
215
  }
117
216
 
118
217
  export interface RepublishOptions extends AbortOptions, ProgressOptions<RepublishProgressEvents | IPNSRoutingEvents> {
@@ -126,7 +225,7 @@ export interface IPNS {
126
225
  /**
127
226
  * Creates an IPNS record signed by the passed PeerId that will resolve to the passed value
128
227
  *
129
- * If the valid is a PeerId, a recursive IPNS record will be created.
228
+ * If the value is a PeerId, a recursive IPNS record will be created.
130
229
  */
131
230
  publish(key: PeerId, value: CID | PeerId, options?: PublishOptions): Promise<IPNSRecord>
132
231
 
@@ -157,10 +256,12 @@ class DefaultIPNS implements IPNS {
157
256
  private readonly routers: IPNSRouting[]
158
257
  private readonly localStore: LocalStore
159
258
  private timeout?: ReturnType<typeof setTimeout>
259
+ private readonly defaultResolvers: DNSResolver[]
160
260
 
161
- constructor (components: IPNSComponents, routers: IPNSRouting[] = []) {
261
+ constructor (components: IPNSComponents, routers: IPNSRouting[] = [], resolvers: DNSResolver[] = []) {
162
262
  this.routers = routers
163
263
  this.localStore = localStore(components.datastore)
264
+ this.defaultResolvers = resolvers.length > 0 ? resolvers : [defaultResolver()]
164
265
  }
165
266
 
166
267
  async publish (key: PeerId, value: CID | PeerId, options: PublishOptions = {}): Promise<IPNSRecord> {
@@ -201,7 +302,11 @@ class DefaultIPNS implements IPNS {
201
302
  }
202
303
 
203
304
  async resolveDns (domain: string, options: ResolveDNSOptions = {}): Promise<CID> {
204
- const dnslink = await resolveDnslink(domain, options)
305
+ const resolvers = options.resolvers ?? this.defaultResolvers
306
+
307
+ const dnslink = await Promise.any(
308
+ resolvers.map(async resolver => resolver(domain, options))
309
+ )
205
310
 
206
311
  return this.#resolve(dnslink, options)
207
312
  }
@@ -298,8 +403,13 @@ class DefaultIPNS implements IPNS {
298
403
  }
299
404
  }
300
405
 
301
- export function ipns (components: IPNSComponents, routers: IPNSRouting[] = []): IPNS {
302
- return new DefaultIPNS(components, routers)
406
+ export interface IPNSOptions {
407
+ routers?: IPNSRouting[]
408
+ resolvers?: DNSResolver[]
409
+ }
410
+
411
+ export function ipns (components: IPNSComponents, { routers = [], resolvers = [] }: IPNSOptions): IPNS {
412
+ return new DefaultIPNS(components, routers, resolvers)
303
413
  }
304
414
 
305
415
  export { ipnsValidator }
@@ -0,0 +1,126 @@
1
+ import { CodeError } from '@libp2p/interface/errors'
2
+ import * as isIPFS from 'is-ipfs'
3
+ import type { DNSResolver, ResolveDnsLinkOptions } from '../index.js'
4
+
5
+ export interface Question {
6
+ name: string
7
+ type: number
8
+ }
9
+
10
+ export interface Answer {
11
+ name: string
12
+ type: number
13
+ TTL: number
14
+ data: string
15
+ }
16
+
17
+ export interface DNSResponse {
18
+ Status: number
19
+ TC: boolean
20
+ RD: boolean
21
+ RA: boolean
22
+ AD: boolean
23
+ CD: boolean
24
+ Question: Question[]
25
+ Answer?: Answer[]
26
+ }
27
+
28
+ export const ipfsPathForAnswer = (answer: Answer): string => {
29
+ let data = answer.data
30
+
31
+ if (data.startsWith('"')) {
32
+ data = data.substring(1)
33
+ }
34
+
35
+ if (data.endsWith('"')) {
36
+ data = data.substring(0, data.length - 1)
37
+ }
38
+
39
+ return data.replace('dnslink=', '')
40
+ }
41
+
42
+ export const ipfsPathAndAnswer = (domain: string, response: DNSResponse): { ipfsPath: string, answer: Answer } => {
43
+ const answer = findDNSLinkAnswer(domain, response)
44
+
45
+ return {
46
+ ipfsPath: ipfsPathForAnswer(answer),
47
+ answer
48
+ }
49
+ }
50
+
51
+ export const findDNSLinkAnswer = (domain: string, response: DNSResponse): Answer => {
52
+ const answer = response.Answer?.filter(a => a.data.includes('dnslink=/ipfs') || a.data.includes('dnslink=/ipns')).pop()
53
+
54
+ if (answer == null) {
55
+ throw new CodeError(`No dnslink records found for domain: ${domain}`, 'ERR_DNSLINK_NOT_FOUND')
56
+ }
57
+
58
+ return answer
59
+ }
60
+
61
+ export const MAX_RECURSIVE_DEPTH = 32
62
+
63
+ export const recursiveResolveDnslink = async (domain: string, depth: number, resolve: DNSResolver, options: ResolveDnsLinkOptions = {}): Promise<string> => {
64
+ if (depth === 0) {
65
+ throw new Error('recursion limit exceeded')
66
+ }
67
+
68
+ let dnslinkRecord: string
69
+
70
+ try {
71
+ dnslinkRecord = await resolve(domain, options)
72
+ } catch (err: any) {
73
+ // If the code is not ENOTFOUND or ERR_DNSLINK_NOT_FOUND or ENODATA then throw the error
74
+ if (err.code !== 'ENOTFOUND' && err.code !== 'ERR_DNSLINK_NOT_FOUND' && err.code !== 'ENODATA') {
75
+ throw err
76
+ }
77
+
78
+ if (domain.startsWith('_dnslink.')) {
79
+ // The supplied domain contains a _dnslink component
80
+ // Check the non-_dnslink domain
81
+ domain = domain.replace('_dnslink.', '')
82
+ } else {
83
+ // Check the _dnslink subdomain
84
+ domain = `_dnslink.${domain}`
85
+ }
86
+
87
+ // If this throws then we propagate the error
88
+ dnslinkRecord = await resolve(domain, options)
89
+ }
90
+
91
+ const result = dnslinkRecord.replace('dnslink=', '')
92
+ // result is now a `/ipfs/<cid>` or `/ipns/<cid>` string
93
+ const domainOrCID = result.split('/')[2] // e.g. ["", "ipfs", "<cid>"]
94
+ const isIPFSCID = isIPFS.cid(domainOrCID)
95
+
96
+ // if the result is a CID, or depth is 1, we've reached the end of the recursion
97
+ // if depth is 1, another recursive call will be made, but it would throw.
98
+ // we could return if depth is 1 and allow users to handle, but that may be a breaking change
99
+ if (isIPFSCID) {
100
+ return result
101
+ }
102
+
103
+ return recursiveResolveDnslink(domainOrCID, depth - 1, resolve, options)
104
+ }
105
+
106
+ interface DnsResolver {
107
+ resolveTxt(domain: string): Promise<string[][]>
108
+ }
109
+
110
+ const DNSLINK_REGEX = /^dnslink=.+$/
111
+ export const resolveFn = async (resolver: DnsResolver, domain: string): Promise<string> => {
112
+ const records = await resolver.resolveTxt(domain)
113
+ const dnslinkRecords = records.flat()
114
+ .filter(record => DNSLINK_REGEX.test(record))
115
+
116
+ // we now have dns text entries as an array of strings
117
+ // only records passing the DNSLINK_REGEX text are included
118
+ // TODO: support multiple dnslink records
119
+ const dnslinkRecord = dnslinkRecords[0]
120
+
121
+ if (dnslinkRecord == null) {
122
+ throw new CodeError(`No dnslink records found for domain: ${domain}`, 'ERR_DNSLINK_NOT_FOUND')
123
+ }
124
+
125
+ return dnslinkRecord
126
+ }
@@ -1,6 +0,0 @@
1
- import type { AbortOptions } from '@libp2p/interface';
2
- export interface ResolveDnsLinkOptions extends AbortOptions {
3
- nocache?: boolean;
4
- }
5
- export declare function resolveDnslink(fqdn: string, opts?: ResolveDnsLinkOptions): Promise<string>;
6
- //# sourceMappingURL=resolve-dns-link.browser.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"resolve-dns-link.browser.d.ts","sourceRoot":"","sources":["../../../src/utils/resolve-dns-link.browser.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAoBrD,MAAM,WAAW,qBAAsB,SAAQ,YAAY;IACzD,OAAO,CAAC,EAAE,OAAO,CAAA;CAClB;AAED,wBAAsB,cAAc,CAAE,IAAI,EAAE,MAAM,EAAE,IAAI,GAAE,qBAA0B,GAAG,OAAO,CAAC,MAAM,CAAC,CAgCrG"}
@@ -1,46 +0,0 @@
1
- /* eslint-env browser */
2
- import PQueue from 'p-queue';
3
- import { TLRU } from './tlru.js';
4
- // Avoid sending multiple queries for the same hostname by caching results
5
- const cache = new TLRU(1000);
6
- // TODO: /api/v0/dns does not return TTL yet: https://github.com/ipfs/go-ipfs/issues/5884
7
- // However we know browsers themselves cache DNS records for at least 1 minute,
8
- // which acts a provisional default ttl: https://stackoverflow.com/a/36917902/11518426
9
- const ttl = 60 * 1000;
10
- // browsers limit concurrent connections per host,
11
- // we don't want preload calls to exhaust the limit (~6)
12
- const httpQueue = new PQueue({ concurrency: 4 });
13
- const ipfsPath = (response) => {
14
- if (response.Path != null) {
15
- return response.Path;
16
- }
17
- throw new Error(response.Message);
18
- };
19
- export async function resolveDnslink(fqdn, opts = {}) {
20
- const resolve = async (fqdn, opts = {}) => {
21
- // @ts-expect-error - URLSearchParams does not take boolean options, only strings
22
- const searchParams = new URLSearchParams(opts);
23
- searchParams.set('arg', fqdn);
24
- // try cache first
25
- const query = searchParams.toString();
26
- if (opts.nocache !== true && cache.has(query)) {
27
- const response = cache.get(query);
28
- if (response != null) {
29
- return ipfsPath(response);
30
- }
31
- }
32
- // fallback to delegated DNS resolver
33
- const response = await httpQueue.add(async () => {
34
- // Delegated HTTP resolver sending DNSLink queries to ipfs.io
35
- // TODO: replace hardcoded host with configurable DNS over HTTPS: https://github.com/ipfs/js-ipfs/issues/2212
36
- const res = await fetch(`https://ipfs.io/api/v0/dns?${searchParams}`);
37
- const query = new URL(res.url).search.slice(1);
38
- const json = await res.json();
39
- cache.set(query, json, ttl);
40
- return json;
41
- });
42
- return ipfsPath(response);
43
- };
44
- return resolve(fqdn, opts);
45
- }
46
- //# sourceMappingURL=resolve-dns-link.browser.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"resolve-dns-link.browser.js","sourceRoot":"","sources":["../../../src/utils/resolve-dns-link.browser.ts"],"names":[],"mappings":"AAAA,wBAAwB;AAExB,OAAO,MAAM,MAAM,SAAS,CAAA;AAC5B,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAA;AAGhC,0EAA0E;AAC1E,MAAM,KAAK,GAAG,IAAI,IAAI,CAAoC,IAAI,CAAC,CAAA;AAC/D,yFAAyF;AACzF,+EAA+E;AAC/E,sFAAsF;AACtF,MAAM,GAAG,GAAG,EAAE,GAAG,IAAI,CAAA;AAErB,kDAAkD;AAClD,wDAAwD;AACxD,MAAM,SAAS,GAAG,IAAI,MAAM,CAAC,EAAE,WAAW,EAAE,CAAC,EAAE,CAAC,CAAA;AAEhD,MAAM,QAAQ,GAAG,CAAC,QAA2C,EAAU,EAAE;IACvE,IAAI,QAAQ,CAAC,IAAI,IAAI,IAAI,EAAE;QACzB,OAAO,QAAQ,CAAC,IAAI,CAAA;KACrB;IACD,MAAM,IAAI,KAAK,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAA;AACnC,CAAC,CAAA;AAMD,MAAM,CAAC,KAAK,UAAU,cAAc,CAAE,IAAY,EAAE,OAA8B,EAAE;IAClF,MAAM,OAAO,GAAG,KAAK,EAAE,IAAY,EAAE,OAA8B,EAAE,EAAmB,EAAE;QACxF,iFAAiF;QACjF,MAAM,YAAY,GAAG,IAAI,eAAe,CAAC,IAAI,CAAC,CAAA;QAC9C,YAAY,CAAC,GAAG,CAAC,KAAK,EAAE,IAAI,CAAC,CAAA;QAE7B,kBAAkB;QAClB,MAAM,KAAK,GAAG,YAAY,CAAC,QAAQ,EAAE,CAAA;QACrC,IAAI,IAAI,CAAC,OAAO,KAAK,IAAI,IAAI,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE;YAC7C,MAAM,QAAQ,GAAG,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,CAAA;YAEjC,IAAI,QAAQ,IAAI,IAAI,EAAE;gBACpB,OAAO,QAAQ,CAAC,QAAQ,CAAC,CAAA;aAC1B;SACF;QAED,qCAAqC;QACrC,MAAM,QAAQ,GAAG,MAAM,SAAS,CAAC,GAAG,CAAC,KAAK,IAAI,EAAE;YAC9C,6DAA6D;YAC7D,6GAA6G;YAC7G,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,8BAA8B,YAAY,EAAE,CAAC,CAAA;YACrE,MAAM,KAAK,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAA;YAC9C,MAAM,IAAI,GAAG,MAAM,GAAG,CAAC,IAAI,EAAE,CAAA;YAC7B,KAAK,CAAC,GAAG,CAAC,KAAK,EAAE,IAAI,EAAE,GAAG,CAAC,CAAA;YAE3B,OAAO,IAAI,CAAA;QACb,CAAC,CAAC,CAAA;QAEF,OAAO,QAAQ,CAAC,QAAQ,CAAC,CAAA;IAC3B,CAAC,CAAA;IAED,OAAO,OAAO,CAAC,IAAI,EAAE,IAAI,CAAC,CAAA;AAC5B,CAAC"}
@@ -1,3 +0,0 @@
1
- import type { AbortOptions } from '@libp2p/interface';
2
- export declare function resolveDnslink(domain: string, options?: AbortOptions): Promise<string>;
3
- //# sourceMappingURL=resolve-dns-link.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"resolve-dns-link.d.ts","sourceRoot":"","sources":["../../../src/utils/resolve-dns-link.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAIrD,wBAAsB,cAAc,CAAE,MAAM,EAAE,MAAM,EAAE,OAAO,GAAE,YAAiB,GAAG,OAAO,CAAC,MAAM,CAAC,CAEjG"}