@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.
- package/README.md +110 -0
- package/dist/index.min.js +26 -15
- package/dist/src/dns-resolvers/default.d.ts +3 -0
- package/dist/src/dns-resolvers/default.d.ts.map +1 -0
- package/dist/src/dns-resolvers/default.js +8 -0
- package/dist/src/dns-resolvers/default.js.map +1 -0
- package/dist/src/dns-resolvers/dns-json-over-https.d.ts +18 -0
- package/dist/src/dns-resolvers/dns-json-over-https.d.ts.map +1 -0
- package/dist/src/dns-resolvers/dns-json-over-https.js +72 -0
- package/dist/src/dns-resolvers/dns-json-over-https.js.map +1 -0
- package/dist/src/dns-resolvers/dns-over-https.d.ts +16 -0
- package/dist/src/dns-resolvers/dns-over-https.d.ts.map +1 -0
- package/dist/src/dns-resolvers/dns-over-https.js +123 -0
- package/dist/src/dns-resolvers/dns-over-https.js.map +1 -0
- package/dist/src/dns-resolvers/index.d.ts +3 -0
- package/dist/src/dns-resolvers/index.d.ts.map +1 -0
- package/dist/src/dns-resolvers/index.js +3 -0
- package/dist/src/dns-resolvers/index.js.map +1 -0
- package/dist/src/dns-resolvers/resolver.browser.d.ts +4 -0
- package/dist/src/dns-resolvers/resolver.browser.d.ts.map +1 -0
- package/dist/src/dns-resolvers/resolver.browser.js +39 -0
- package/dist/src/dns-resolvers/resolver.browser.js.map +1 -0
- package/dist/src/dns-resolvers/resolver.d.ts +4 -0
- package/dist/src/dns-resolvers/resolver.d.ts.map +1 -0
- package/dist/src/dns-resolvers/resolver.js +21 -0
- package/dist/src/dns-resolvers/resolver.js.map +1 -0
- package/dist/src/index.d.ts +102 -9
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +82 -12
- package/dist/src/index.js.map +1 -1
- package/dist/src/routing/dht.js.map +1 -1
- package/dist/src/routing/local-store.js +3 -3
- package/dist/src/routing/local-store.js.map +1 -1
- package/dist/src/routing/pubsub.js.map +1 -1
- package/dist/src/utils/dns.d.ts +35 -0
- package/dist/src/utils/dns.d.ts.map +1 -0
- package/dist/src/utils/dns.js +79 -0
- package/dist/src/utils/dns.js.map +1 -0
- package/dist/src/utils/tlru.js.map +1 -1
- package/dist/typedoc-urls.json +8 -0
- package/package.json +14 -5
- package/src/dns-resolvers/default.ts +9 -0
- package/src/dns-resolvers/dns-json-over-https.ts +90 -0
- package/src/dns-resolvers/dns-over-https.ts +146 -0
- package/src/dns-resolvers/index.ts +2 -0
- package/src/dns-resolvers/resolver.browser.ts +50 -0
- package/src/dns-resolvers/resolver.ts +25 -0
- package/src/index.ts +121 -13
- package/src/routing/local-store.ts +3 -3
- package/src/utils/dns.ts +126 -0
- package/dist/src/utils/resolve-dns-link.browser.d.ts +0 -6
- package/dist/src/utils/resolve-dns-link.browser.d.ts.map +0 -1
- package/dist/src/utils/resolve-dns-link.browser.js +0 -46
- package/dist/src/utils/resolve-dns-link.browser.js.map +0 -1
- package/dist/src/utils/resolve-dns-link.d.ts +0 -3
- package/dist/src/utils/resolve-dns-link.d.ts.map +0 -1
- package/dist/src/utils/resolve-dns-link.js +0 -54
- package/dist/src/utils/resolve-dns-link.js.map +0 -1
- package/src/utils/resolve-dns-link.browser.ts +0 -61
- 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,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
|
-
*
|
|
16
|
-
*
|
|
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,
|
|
38
|
-
* //
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
302
|
-
|
|
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 {
|
|
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
|
|
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 =
|
|
50
|
+
const record = Record.deserialize(buf)
|
|
51
51
|
|
|
52
52
|
return record.value
|
|
53
53
|
} catch (err: any) {
|
package/src/utils/dns.ts
ADDED
|
@@ -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 +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"}
|