@helia/ipns 2.0.2 → 3.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 (60) hide show
  1. package/README.md +110 -0
  2. package/dist/index.min.js +26 -15
  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 +102 -9
  28. package/dist/src/index.d.ts.map +1 -1
  29. package/dist/src/index.js +82 -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 +3 -3
  33. package/dist/src/routing/local-store.js.map +1 -1
  34. package/dist/src/routing/pubsub.js.map +1 -1
  35. package/dist/src/utils/dns.d.ts +35 -0
  36. package/dist/src/utils/dns.d.ts.map +1 -0
  37. package/dist/src/utils/dns.js +79 -0
  38. package/dist/src/utils/dns.js.map +1 -0
  39. package/dist/src/utils/tlru.js.map +1 -1
  40. package/dist/typedoc-urls.json +8 -0
  41. package/package.json +14 -5
  42. package/src/dns-resolvers/default.ts +9 -0
  43. package/src/dns-resolvers/dns-json-over-https.ts +90 -0
  44. package/src/dns-resolvers/dns-over-https.ts +146 -0
  45. package/src/dns-resolvers/index.ts +2 -0
  46. package/src/dns-resolvers/resolver.browser.ts +50 -0
  47. package/src/dns-resolvers/resolver.ts +25 -0
  48. package/src/index.ts +121 -13
  49. package/src/routing/local-store.ts +3 -3
  50. package/src/utils/dns.ts +126 -0
  51. package/dist/src/utils/resolve-dns-link.browser.d.ts +0 -6
  52. package/dist/src/utils/resolve-dns-link.browser.d.ts.map +0 -1
  53. package/dist/src/utils/resolve-dns-link.browser.js +0 -46
  54. package/dist/src/utils/resolve-dns-link.browser.js.map +0 -1
  55. package/dist/src/utils/resolve-dns-link.d.ts +0 -3
  56. package/dist/src/utils/resolve-dns-link.d.ts.map +0 -1
  57. package/dist/src/utils/resolve-dns-link.js +0 -54
  58. package/dist/src/utils/resolve-dns-link.js.map +0 -1
  59. package/src/utils/resolve-dns-link.browser.ts +0 -61
  60. 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,20 @@
5
5
  *
6
6
  * @example
7
7
  *
8
+ * With {@link IPNSRouting} routers:
9
+ *
8
10
  * ```typescript
9
11
  * import { createHelia } from 'helia'
10
12
  * import { dht, pubsub } from '@helia/ipns/routing'
11
13
  * import { unixfs } from '@helia/unixfs'
12
14
  *
13
15
  * const helia = await createHelia()
14
- * const name = ipns(helia, [
15
- * dht(helia),
16
- * pubsub(helia)
17
- * ])
16
+ * const name = ipns(helia, {
17
+ * routers: [
18
+ * dht(helia),
19
+ * pubsub(helia)
20
+ * ]
21
+ * })
18
22
  *
19
23
  * // create a public key to publish as an IPNS name
20
24
  * const keyInfo = await helia.libp2p.keychain.createKey('my-key')
@@ -33,13 +37,76 @@
33
37
  *
34
38
  * @example
35
39
  *
40
+ * With default {@link DNSResolver} resolvers:
41
+ *
42
+ * ```typescript
43
+ * import { createHelia } from 'helia'
44
+ * import { dht, pubsub } from '@helia/ipns/routing'
45
+ * import { unixfs } from '@helia/unixfs'
46
+ *
47
+ * const helia = await createHelia()
48
+ * const name = ipns(helia, {
49
+ * resolvers: [
50
+ * dnsOverHttps('https://private-dns-server.me/dns-query'),
51
+ * ]
52
+ * })
53
+ *
54
+ * const cid = name.resolveDns('some-domain-with-dnslink-entry.com')
55
+ * ```
56
+ *
57
+ * @example
58
+ *
59
+ * Calling `resolveDns` with the `@helia/ipns` instance:
60
+ *
36
61
  * ```typescript
37
- * // resolve a CID from a TXT record in a DNS zone file, eg:
38
- * // > dig ipfs.io TXT
62
+ * // resolve a CID from a TXT record in a DNS zone file, using the default
63
+ * // resolver for the current platform eg:
64
+ * // > dig _dnslink.ipfs.io TXT
39
65
  * // ;; ANSWER SECTION:
40
- * // ipfs.io. 435 IN TXT "dnslink=/ipfs/Qmfoo"
66
+ * // _dnslink.ipfs.io. 60 IN TXT "dnslink=/ipns/website.ipfs.io"
67
+ * // > dig _dnslink.website.ipfs.io TXT
68
+ * // ;; ANSWER SECTION:
69
+ * // _dnslink.website.ipfs.io. 60 IN TXT "dnslink=/ipfs/QmWebsite"
41
70
  *
42
71
  * const cid = name.resolveDns('ipfs.io')
72
+ *
73
+ * console.info(cid)
74
+ * // QmWebsite
75
+ * ```
76
+ *
77
+ * @example
78
+ *
79
+ * This example uses the Mozilla provided RFC 1035 DNS over HTTPS service. This
80
+ * uses binary DNS records so requires extra dependencies to process the
81
+ * response which can increase browser bundle sizes.
82
+ *
83
+ * If this is a concern, use the DNS-JSON-Over-HTTPS resolver instead.
84
+ *
85
+ * ```typescript
86
+ * // use DNS-Over-HTTPS
87
+ * import { dnsOverHttps } from '@helia/ipns/dns-resolvers'
88
+ *
89
+ * const cid = name.resolveDns('ipfs.io', {
90
+ * resolvers: [
91
+ * dnsOverHttps('https://mozilla.cloudflare-dns.com/dns-query')
92
+ * ]
93
+ * })
94
+ * ```
95
+ *
96
+ * @example
97
+ *
98
+ * DNS-JSON-Over-HTTPS resolvers use the RFC 8427 `application/dns-json` and can
99
+ * result in a smaller browser bundle due to the response being plain JSON.
100
+ *
101
+ * ```typescript
102
+ * // use DNS-JSON-Over-HTTPS
103
+ * import { dnsJsonOverHttps } from '@helia/ipns/dns-resolvers'
104
+ *
105
+ * const cid = name.resolveDns('ipfs.io', {
106
+ * resolvers: [
107
+ * dnsJsonOverHttps('https://mozilla.cloudflare-dns.com/dns-query')
108
+ * ]
109
+ * })
43
110
  * ```
44
111
  */
45
112
 
@@ -51,9 +118,10 @@ import { ipnsSelector } from 'ipns/selector'
51
118
  import { ipnsValidator } from 'ipns/validator'
52
119
  import { CID } from 'multiformats/cid'
53
120
  import { CustomProgressEvent } from 'progress-events'
121
+ import { defaultResolver } from './dns-resolvers/default.js'
54
122
  import { localStore, type LocalStore } from './routing/local-store.js'
55
- import { resolveDnslink } from './utils/resolve-dns-link.js'
56
123
  import type { IPNSRouting, IPNSRoutingEvents } from './routing/index.js'
124
+ import type { DNSResponse } from './utils/dns.js'
57
125
  import type { AbortOptions } from '@libp2p/interface'
58
126
  import type { PeerId } from '@libp2p/interface/peer-id'
59
127
  import type { Datastore } from 'interface-datastore'
@@ -83,6 +151,11 @@ export type RepublishProgressEvents =
83
151
  ProgressEvent<'ipns:republish:success', IPNSRecord> |
84
152
  ProgressEvent<'ipns:republish:error', { record: IPNSRecord, err: Error }>
85
153
 
154
+ export type ResolveDnsLinkProgressEvents =
155
+ ProgressEvent<'dnslink:cache', string> |
156
+ ProgressEvent<'dnslink:query', string> |
157
+ ProgressEvent<'dnslink:answer', DNSResponse>
158
+
86
159
  export interface PublishOptions extends AbortOptions, ProgressOptions<PublishProgressEvents | IPNSRoutingEvents> {
87
160
  /**
88
161
  * Time duration of the record in ms (default: 24hrs)
@@ -108,11 +181,35 @@ export interface ResolveOptions extends AbortOptions, ProgressOptions<ResolvePro
108
181
  offline?: boolean
109
182
  }
110
183
 
111
- export interface ResolveDNSOptions extends ResolveOptions {
184
+ export interface ResolveDnsLinkOptions extends AbortOptions, ProgressOptions<ResolveDnsLinkProgressEvents> {
185
+ /**
186
+ * Do not use cached DNS entries (default: false)
187
+ */
188
+ nocache?: boolean
189
+ }
190
+
191
+ export interface DNSResolver {
192
+ (domain: string, options?: ResolveDnsLinkOptions): Promise<string>
193
+ }
194
+
195
+ export interface ResolveDNSOptions extends AbortOptions, ProgressOptions<ResolveProgressEvents | IPNSRoutingEvents | ResolveDnsLinkProgressEvents> {
196
+ /**
197
+ * Do not query the network for the IPNS record (default: false)
198
+ */
199
+ offline?: boolean
200
+
112
201
  /**
113
202
  * Do not use cached DNS entries (default: false)
114
203
  */
115
204
  nocache?: boolean
205
+
206
+ /**
207
+ * These resolvers will be used to resolve the dnslink entries, if unspecified node will
208
+ * fall back to the `dns` module and browsers fall back to querying google/cloudflare DoH
209
+ *
210
+ * @see https://github.com/ipfs/helia-ipns/pull/55#discussion_r1270096881
211
+ */
212
+ resolvers?: DNSResolver[]
116
213
  }
117
214
 
118
215
  export interface RepublishOptions extends AbortOptions, ProgressOptions<RepublishProgressEvents | IPNSRoutingEvents> {
@@ -157,10 +254,12 @@ class DefaultIPNS implements IPNS {
157
254
  private readonly routers: IPNSRouting[]
158
255
  private readonly localStore: LocalStore
159
256
  private timeout?: ReturnType<typeof setTimeout>
257
+ private readonly defaultResolvers: DNSResolver[]
160
258
 
161
- constructor (components: IPNSComponents, routers: IPNSRouting[] = []) {
259
+ constructor (components: IPNSComponents, routers: IPNSRouting[] = [], resolvers: DNSResolver[] = []) {
162
260
  this.routers = routers
163
261
  this.localStore = localStore(components.datastore)
262
+ this.defaultResolvers = resolvers.length > 0 ? resolvers : [defaultResolver()]
164
263
  }
165
264
 
166
265
  async publish (key: PeerId, value: CID | PeerId, options: PublishOptions = {}): Promise<IPNSRecord> {
@@ -201,7 +300,11 @@ class DefaultIPNS implements IPNS {
201
300
  }
202
301
 
203
302
  async resolveDns (domain: string, options: ResolveDNSOptions = {}): Promise<CID> {
204
- const dnslink = await resolveDnslink(domain, options)
303
+ const resolvers = options.resolvers ?? this.defaultResolvers
304
+
305
+ const dnslink = await Promise.any(
306
+ resolvers.map(async resolver => resolver(domain, options))
307
+ )
205
308
 
206
309
  return this.#resolve(dnslink, options)
207
310
  }
@@ -298,8 +401,13 @@ class DefaultIPNS implements IPNS {
298
401
  }
299
402
  }
300
403
 
301
- export function ipns (components: IPNSComponents, routers: IPNSRouting[] = []): IPNS {
302
- return new DefaultIPNS(components, routers)
404
+ export interface IPNSOptions {
405
+ routers?: IPNSRouting[]
406
+ resolvers?: DNSResolver[]
407
+ }
408
+
409
+ export function ipns (components: IPNSComponents, { routers = [], resolvers = [] }: IPNSOptions): IPNS {
410
+ return new DefaultIPNS(components, routers, resolvers)
303
411
  }
304
412
 
305
413
  export { ipnsValidator }
@@ -1,4 +1,4 @@
1
- import { Libp2pRecord } from '@libp2p/record'
1
+ import { Record } from '@libp2p/kad-dht'
2
2
  import { type Datastore, Key } from 'interface-datastore'
3
3
  import { CustomProgressEvent, type ProgressEvent } from 'progress-events'
4
4
  import { toString as uint8ArrayToString } from 'uint8arrays/to-string'
@@ -30,7 +30,7 @@ export function localStore (datastore: Datastore): LocalStore {
30
30
  const key = dhtRoutingKey(routingKey)
31
31
 
32
32
  // Marshal to libp2p record as the DHT does
33
- const record = new Libp2pRecord(routingKey, marshalledRecord, new Date())
33
+ const record = new Record(routingKey, marshalledRecord, new Date())
34
34
 
35
35
  options.onProgress?.(new CustomProgressEvent('ipns:routing:datastore:put'))
36
36
  await datastore.put(key, record.serialize(), options)
@@ -47,7 +47,7 @@ export function localStore (datastore: Datastore): LocalStore {
47
47
  const buf = await datastore.get(key, options)
48
48
 
49
49
  // Unmarshal libp2p record as the DHT does
50
- const record = Libp2pRecord.deserialize(buf)
50
+ const record = Record.deserialize(buf)
51
51
 
52
52
  return record.value
53
53
  } catch (err: any) {
@@ -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"}