effect 4.0.2 → 4.0.3
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/dist/Context.js +3 -3
- package/dist/Context.js.map +1 -1
- package/dist/Crypto.d.ts +33 -5
- package/dist/Crypto.d.ts.map +1 -1
- package/dist/Crypto.js +8 -2
- package/dist/Crypto.js.map +1 -1
- package/dist/DateTime.d.ts +9 -6
- package/dist/DateTime.d.ts.map +1 -1
- package/dist/DateTime.js +9 -6
- package/dist/DateTime.js.map +1 -1
- package/dist/Duration.d.ts +6 -6
- package/dist/Duration.d.ts.map +1 -1
- package/dist/Duration.js +27 -8
- package/dist/Duration.js.map +1 -1
- package/dist/FileSystem.d.ts +44 -2
- package/dist/FileSystem.d.ts.map +1 -1
- package/dist/FileSystem.js.map +1 -1
- package/dist/Schema.d.ts +473 -1
- package/dist/Schema.d.ts.map +1 -1
- package/dist/Schema.js +355 -4
- package/dist/Schema.js.map +1 -1
- package/dist/Stdio.d.ts +7 -1
- package/dist/Stdio.d.ts.map +1 -1
- package/dist/Stdio.js +1 -0
- package/dist/Stdio.js.map +1 -1
- package/dist/Stream.d.ts +11 -11
- package/dist/Stream.js +5 -5
- package/dist/ai/McpServer.d.ts +6 -0
- package/dist/ai/McpServer.d.ts.map +1 -1
- package/dist/ai/McpServer.js +65 -23
- package/dist/ai/McpServer.js.map +1 -1
- package/dist/ai/internal/mcpProtocol/v2026_07_28.js +5 -0
- package/dist/ai/internal/mcpProtocol/v2026_07_28.js.map +1 -1
- package/dist/ai/internal/mcpProtocol.js.map +1 -1
- package/dist/ai/internal/mcpRuntime.js +1 -0
- package/dist/ai/internal/mcpRuntime.js.map +1 -1
- package/dist/cli/CliOutput.d.ts +27 -0
- package/dist/cli/CliOutput.d.ts.map +1 -1
- package/dist/cli/CliOutput.js +33 -10
- package/dist/cli/CliOutput.js.map +1 -1
- package/dist/cli/Command.d.ts.map +1 -1
- package/dist/cli/Command.js +6 -3
- package/dist/cli/Command.js.map +1 -1
- package/dist/cli/Completions.d.ts +3 -3
- package/dist/cli/Completions.d.ts.map +1 -1
- package/dist/cli/Completions.js +8 -5
- package/dist/cli/Completions.js.map +1 -1
- package/dist/cli/GlobalFlag.d.ts +5 -4
- package/dist/cli/GlobalFlag.d.ts.map +1 -1
- package/dist/cli/GlobalFlag.js +3 -3
- package/dist/cli/GlobalFlag.js.map +1 -1
- package/dist/cli/Prompt.d.ts +17 -1
- package/dist/cli/Prompt.d.ts.map +1 -1
- package/dist/cli/Prompt.js +119 -114
- package/dist/cli/Prompt.js.map +1 -1
- package/dist/cli/internal/ansi.d.ts +0 -1
- package/dist/cli/internal/ansi.d.ts.map +1 -1
- package/dist/cli/internal/ansi.js +20 -3
- package/dist/cli/internal/ansi.js.map +1 -1
- package/dist/cli/internal/completions/powershell.d.ts +2 -0
- package/dist/cli/internal/completions/powershell.d.ts.map +1 -0
- package/dist/cli/internal/completions/powershell.js +273 -0
- package/dist/cli/internal/completions/powershell.js.map +1 -0
- package/dist/cli/internal/completions/zsh.js +3 -2
- package/dist/cli/internal/completions/zsh.js.map +1 -1
- package/dist/cli/internal/wizard.d.ts +3 -3
- package/dist/cli/internal/wizard.d.ts.map +1 -1
- package/dist/cli/internal/wizard.js +21 -18
- package/dist/cli/internal/wizard.js.map +1 -1
- package/dist/cluster/ClusterSchema.d.ts +28 -0
- package/dist/cluster/ClusterSchema.d.ts.map +1 -1
- package/dist/cluster/ClusterSchema.js +30 -0
- package/dist/cluster/ClusterSchema.js.map +1 -1
- package/dist/cluster/ClusterWorkflowEngine.d.ts.map +1 -1
- package/dist/cluster/ClusterWorkflowEngine.js +10 -18
- package/dist/cluster/ClusterWorkflowEngine.js.map +1 -1
- package/dist/cluster/MessageStorage.d.ts +18 -3
- package/dist/cluster/MessageStorage.d.ts.map +1 -1
- package/dist/cluster/MessageStorage.js +31 -11
- package/dist/cluster/MessageStorage.js.map +1 -1
- package/dist/cluster/Runners.d.ts.map +1 -1
- package/dist/cluster/Runners.js +8 -2
- package/dist/cluster/Runners.js.map +1 -1
- package/dist/cluster/Sharding.d.ts.map +1 -1
- package/dist/cluster/Sharding.js +181 -62
- package/dist/cluster/Sharding.js.map +1 -1
- package/dist/cluster/SqlMessageStorage.d.ts +59 -19
- package/dist/cluster/SqlMessageStorage.d.ts.map +1 -1
- package/dist/cluster/SqlMessageStorage.js +84 -55
- package/dist/cluster/SqlMessageStorage.js.map +1 -1
- package/dist/cluster/SqlRunnerStorage.d.ts +68 -19
- package/dist/cluster/SqlRunnerStorage.d.ts.map +1 -1
- package/dist/cluster/SqlRunnerStorage.js +191 -113
- package/dist/cluster/SqlRunnerStorage.js.map +1 -1
- package/dist/cluster/internal/entityManager.js +104 -37
- package/dist/cluster/internal/entityManager.js.map +1 -1
- package/dist/http/FindMyWay/internal/router.js +3 -6
- package/dist/http/FindMyWay/internal/router.js.map +1 -1
- package/dist/http/HttpClient.d.ts +6 -0
- package/dist/http/HttpClient.d.ts.map +1 -1
- package/dist/http/HttpClient.js +39 -2
- package/dist/http/HttpClient.js.map +1 -1
- package/dist/http/HttpTraceContext.d.ts.map +1 -1
- package/dist/http/HttpTraceContext.js +4 -2
- package/dist/http/HttpTraceContext.js.map +1 -1
- package/dist/internal/effect.js +12 -7
- package/dist/internal/effect.js.map +1 -1
- package/dist/internal/version.js +1 -1
- package/dist/net/AddressResolver.d.ts +145 -0
- package/dist/net/AddressResolver.d.ts.map +1 -0
- package/dist/net/AddressResolver.js +114 -0
- package/dist/net/AddressResolver.js.map +1 -0
- package/dist/net/Dns.d.ts +596 -0
- package/dist/net/Dns.d.ts.map +1 -0
- package/dist/net/Dns.js +549 -0
- package/dist/net/Dns.js.map +1 -0
- package/dist/net/Host.d.ts +320 -0
- package/dist/net/Host.d.ts.map +1 -0
- package/dist/net/Host.js +342 -0
- package/dist/net/Host.js.map +1 -0
- package/dist/net/IpInterface.d.ts +76 -3
- package/dist/net/IpInterface.d.ts.map +1 -1
- package/dist/net/IpInterface.js +53 -4
- package/dist/net/IpInterface.js.map +1 -1
- package/dist/net/IpNetwork.d.ts +82 -8
- package/dist/net/IpNetwork.d.ts.map +1 -1
- package/dist/net/IpNetwork.js +60 -4
- package/dist/net/IpNetwork.js.map +1 -1
- package/dist/net/NetAddress.d.ts +388 -35
- package/dist/net/NetAddress.d.ts.map +1 -1
- package/dist/net/NetAddress.js +350 -34
- package/dist/net/NetAddress.js.map +1 -1
- package/dist/net/index.d.ts +15 -0
- package/dist/net/index.d.ts.map +1 -1
- package/dist/net/index.js +15 -0
- package/dist/net/index.js.map +1 -1
- package/dist/observability/OtlpMetrics.d.ts.map +1 -1
- package/dist/observability/OtlpMetrics.js +15 -21
- package/dist/observability/OtlpMetrics.js.map +1 -1
- package/dist/observability/PrometheusMetrics.js +1 -0
- package/dist/observability/PrometheusMetrics.js.map +1 -1
- package/dist/persistence/PersistedQueue.d.ts +32 -3
- package/dist/persistence/PersistedQueue.d.ts.map +1 -1
- package/dist/persistence/PersistedQueue.js +75 -16
- package/dist/persistence/PersistedQueue.js.map +1 -1
- package/dist/rpc/RpcClient.d.ts +22 -6
- package/dist/rpc/RpcClient.d.ts.map +1 -1
- package/dist/rpc/RpcClient.js +12 -5
- package/dist/rpc/RpcClient.js.map +1 -1
- package/dist/rpc/RpcServer.d.ts +6 -0
- package/dist/rpc/RpcServer.d.ts.map +1 -1
- package/dist/rpc/RpcServer.js +42 -20
- package/dist/rpc/RpcServer.js.map +1 -1
- package/dist/socket/DatagramSocket.d.ts +456 -0
- package/dist/socket/DatagramSocket.d.ts.map +1 -0
- package/dist/socket/DatagramSocket.js +535 -0
- package/dist/socket/DatagramSocket.js.map +1 -0
- package/dist/socket/Socket.d.ts +14 -1
- package/dist/socket/Socket.d.ts.map +1 -1
- package/dist/socket/Socket.js +9 -1
- package/dist/socket/Socket.js.map +1 -1
- package/dist/socket/index.d.ts +5 -0
- package/dist/socket/index.d.ts.map +1 -1
- package/dist/socket/index.js +5 -0
- package/dist/socket/index.js.map +1 -1
- package/dist/sql/Migrator.d.ts.map +1 -1
- package/dist/sql/Migrator.js +7 -2
- package/dist/sql/Migrator.js.map +1 -1
- package/dist/sql/SqlClient.d.ts +7 -1
- package/dist/sql/SqlClient.d.ts.map +1 -1
- package/dist/sql/SqlClient.js +31 -6
- package/dist/sql/SqlClient.js.map +1 -1
- package/dist/testing/TestCrypto.d.ts +73 -0
- package/dist/testing/TestCrypto.d.ts.map +1 -0
- package/dist/testing/TestCrypto.js +87 -0
- package/dist/testing/TestCrypto.js.map +1 -0
- package/dist/testing/index.d.ts +5 -0
- package/dist/testing/index.d.ts.map +1 -1
- package/dist/testing/index.js +5 -0
- package/dist/testing/index.js.map +1 -1
- package/dist/workflow/DurableDeferred.d.ts +54 -0
- package/dist/workflow/DurableDeferred.d.ts.map +1 -1
- package/dist/workflow/DurableDeferred.js +78 -49
- package/dist/workflow/DurableDeferred.js.map +1 -1
- package/dist/workflow/WorkflowEngine.d.ts.map +1 -1
- package/dist/workflow/WorkflowEngine.js +2 -1
- package/dist/workflow/WorkflowEngine.js.map +1 -1
- package/package.json +1 -1
- package/src/Context.ts +3 -3
- package/src/Crypto.ts +43 -9
- package/src/DateTime.ts +9 -6
- package/src/Duration.ts +36 -16
- package/src/FileSystem.ts +48 -2
- package/src/Schema.ts +669 -5
- package/src/Stdio.ts +9 -2
- package/src/Stream.ts +11 -11
- package/src/ai/McpServer.ts +63 -23
- package/src/ai/internal/mcpProtocol/v2026_07_28.ts +5 -0
- package/src/ai/internal/mcpProtocol.ts +3 -0
- package/src/ai/internal/mcpRuntime.ts +2 -0
- package/src/cli/CliOutput.ts +33 -16
- package/src/cli/Command.ts +4 -3
- package/src/cli/Completions.ts +9 -6
- package/src/cli/GlobalFlag.ts +19 -11
- package/src/cli/Prompt.ts +159 -149
- package/src/cli/internal/ansi.ts +26 -3
- package/src/cli/internal/completions/powershell.ts +322 -0
- package/src/cli/internal/completions/zsh.ts +4 -2
- package/src/cli/internal/wizard.ts +34 -26
- package/src/cluster/ClusterSchema.ts +32 -0
- package/src/cluster/ClusterWorkflowEngine.ts +13 -24
- package/src/cluster/MessageStorage.ts +45 -14
- package/src/cluster/Runners.ts +8 -2
- package/src/cluster/Sharding.ts +207 -49
- package/src/cluster/SqlMessageStorage.ts +118 -84
- package/src/cluster/SqlRunnerStorage.ts +224 -128
- package/src/cluster/internal/entityManager.ts +151 -55
- package/src/http/FindMyWay/internal/router.ts +4 -7
- package/src/http/HttpClient.ts +56 -2
- package/src/http/HttpTraceContext.ts +8 -2
- package/src/internal/effect.ts +15 -13
- package/src/internal/version.ts +1 -1
- package/src/net/AddressResolver.ts +222 -0
- package/src/net/Dns.ts +1035 -0
- package/src/net/Host.ts +472 -0
- package/src/net/IpInterface.ts +117 -7
- package/src/net/IpNetwork.ts +116 -17
- package/src/net/NetAddress.ts +611 -76
- package/src/net/index.ts +18 -0
- package/src/observability/OtlpMetrics.ts +12 -23
- package/src/observability/PrometheusMetrics.ts +1 -0
- package/src/persistence/PersistedQueue.ts +146 -53
- package/src/rpc/RpcClient.ts +41 -19
- package/src/rpc/RpcServer.ts +60 -25
- package/src/socket/DatagramSocket.ts +919 -0
- package/src/socket/Socket.ts +22 -2
- package/src/socket/index.ts +6 -0
- package/src/sql/Migrator.ts +21 -5
- package/src/sql/SqlClient.ts +39 -15
- package/src/testing/TestCrypto.ts +94 -0
- package/src/testing/index.ts +6 -0
- package/src/workflow/DurableDeferred.ts +123 -48
- package/src/workflow/WorkflowEngine.ts +2 -1
package/src/net/Dns.ts
ADDED
|
@@ -0,0 +1,1035 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host name resolution, DNS queries, and DNS record values.
|
|
3
|
+
*
|
|
4
|
+
* `lookup` resolves a host name to the addresses used to connect to it,
|
|
5
|
+
* `resolve` queries DNS records of one type, and `reverse` looks up the names
|
|
6
|
+
* of an address. Runtime packages provide the `Dns` service from the host
|
|
7
|
+
* platform's resolver, whose `lookup` uses the operating system resolver
|
|
8
|
+
* (`getaddrinfo`) and therefore also reads the hosts file and other system
|
|
9
|
+
* sources, not only DNS. `AddressResolver` builds on `lookup` to resolve
|
|
10
|
+
* `host:port` endpoints.
|
|
11
|
+
*
|
|
12
|
+
* @stability experimental
|
|
13
|
+
* @since 4.0.0
|
|
14
|
+
*/
|
|
15
|
+
import * as Arr from "../Array.ts"
|
|
16
|
+
import * as Context from "../Context.ts"
|
|
17
|
+
import * as Data from "../Data.ts"
|
|
18
|
+
import * as Duration from "../Duration.ts"
|
|
19
|
+
import * as Effect from "../Effect.ts"
|
|
20
|
+
import * as Hex from "../encoding/Hex.ts"
|
|
21
|
+
import * as Equal from "../Equal.ts"
|
|
22
|
+
import * as Hash from "../Hash.ts"
|
|
23
|
+
import * as Inspectable from "../Inspectable.ts"
|
|
24
|
+
import * as Layer from "../Layer.ts"
|
|
25
|
+
import { hasProperty } from "../Predicate.ts"
|
|
26
|
+
import type { ReadonlyRecord } from "../Record.ts"
|
|
27
|
+
import * as Result from "../Result.ts"
|
|
28
|
+
import * as Host from "./Host.ts"
|
|
29
|
+
import * as NetAddress from "./NetAddress.ts"
|
|
30
|
+
|
|
31
|
+
const RecordTypeId = "~effect/net/Dns/DnsRecord" as const
|
|
32
|
+
|
|
33
|
+
// =============================================================================
|
|
34
|
+
// Records
|
|
35
|
+
// =============================================================================
|
|
36
|
+
|
|
37
|
+
interface RecordProto<Tag extends RecordType> extends Equal.Equal, Hash.Hash, Inspectable.Inspectable {
|
|
38
|
+
readonly _tag: Tag
|
|
39
|
+
readonly [RecordTypeId]: typeof RecordTypeId
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* An IPv4 address record.
|
|
44
|
+
*
|
|
45
|
+
* @stability experimental
|
|
46
|
+
* @category models
|
|
47
|
+
* @since 4.0.0
|
|
48
|
+
*/
|
|
49
|
+
export interface A extends RecordProto<"A"> {
|
|
50
|
+
readonly address: NetAddress.Ipv4Address
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* An IPv6 address record.
|
|
55
|
+
*
|
|
56
|
+
* @stability experimental
|
|
57
|
+
* @category models
|
|
58
|
+
* @since 4.0.0
|
|
59
|
+
*/
|
|
60
|
+
export interface Aaaa extends RecordProto<"AAAA"> {
|
|
61
|
+
readonly address: NetAddress.Ipv6Address
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* A certification authority authorization record.
|
|
66
|
+
*
|
|
67
|
+
* @stability experimental
|
|
68
|
+
* @category models
|
|
69
|
+
* @since 4.0.0
|
|
70
|
+
*/
|
|
71
|
+
export interface Caa extends RecordProto<"CAA"> {
|
|
72
|
+
readonly critical: boolean
|
|
73
|
+
readonly tag: string
|
|
74
|
+
readonly value: string
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* A canonical name (alias) record.
|
|
79
|
+
*
|
|
80
|
+
* @stability experimental
|
|
81
|
+
* @category models
|
|
82
|
+
* @since 4.0.0
|
|
83
|
+
*/
|
|
84
|
+
export interface Cname extends RecordProto<"CNAME"> {
|
|
85
|
+
readonly target: Host.DomainName
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* A mail exchange record.
|
|
90
|
+
*
|
|
91
|
+
* @stability experimental
|
|
92
|
+
* @category models
|
|
93
|
+
* @since 4.0.0
|
|
94
|
+
*/
|
|
95
|
+
export interface Mx extends RecordProto<"MX"> {
|
|
96
|
+
readonly exchange: Host.DomainName
|
|
97
|
+
readonly priority: number
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* A naming authority pointer record.
|
|
102
|
+
*
|
|
103
|
+
* @stability experimental
|
|
104
|
+
* @category models
|
|
105
|
+
* @since 4.0.0
|
|
106
|
+
*/
|
|
107
|
+
export interface Naptr extends RecordProto<"NAPTR"> {
|
|
108
|
+
readonly order: number
|
|
109
|
+
readonly preference: number
|
|
110
|
+
readonly flags: string
|
|
111
|
+
readonly service: string
|
|
112
|
+
readonly regexp: string
|
|
113
|
+
readonly replacement: Host.DomainName
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* A name server record.
|
|
118
|
+
*
|
|
119
|
+
* @stability experimental
|
|
120
|
+
* @category models
|
|
121
|
+
* @since 4.0.0
|
|
122
|
+
*/
|
|
123
|
+
export interface Ns extends RecordProto<"NS"> {
|
|
124
|
+
readonly host: Host.DomainName
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* A pointer record, used for reverse lookups and DNS service discovery.
|
|
129
|
+
*
|
|
130
|
+
* **Details**
|
|
131
|
+
*
|
|
132
|
+
* `host` is a fully qualified name. It is a host name for reverse lookups,
|
|
133
|
+
* but may be a DNS-SD service instance name containing spaces or other
|
|
134
|
+
* characters, such as `EPSON TM-m30III._ipp._tcp.local.`. Labels are UTF-8
|
|
135
|
+
* text, with dots and backslashes inside a label escaped as `\.` and `\\`.
|
|
136
|
+
*
|
|
137
|
+
* @stability experimental
|
|
138
|
+
* @category models
|
|
139
|
+
* @since 4.0.0
|
|
140
|
+
*/
|
|
141
|
+
export interface Ptr extends RecordProto<"PTR"> {
|
|
142
|
+
readonly host: string
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* A start of authority record.
|
|
147
|
+
*
|
|
148
|
+
* **Details**
|
|
149
|
+
*
|
|
150
|
+
* `admin` is the administrator mailbox written as a domain name, such as
|
|
151
|
+
* `hostmaster.example.com`. A dot inside the mailbox's local part is escaped,
|
|
152
|
+
* as in `john\.doe.example.com`, so `admin` is a plain string rather than a
|
|
153
|
+
* `Host.DomainName`. Labels are UTF-8 text, with dots and backslashes inside
|
|
154
|
+
* a label escaped as `\.` and `\\`.
|
|
155
|
+
*
|
|
156
|
+
* @stability experimental
|
|
157
|
+
* @category models
|
|
158
|
+
* @since 4.0.0
|
|
159
|
+
*/
|
|
160
|
+
export interface Soa extends RecordProto<"SOA"> {
|
|
161
|
+
readonly primary: Host.DomainName
|
|
162
|
+
readonly admin: string
|
|
163
|
+
readonly serial: number
|
|
164
|
+
readonly refresh: Duration.Duration
|
|
165
|
+
readonly retry: Duration.Duration
|
|
166
|
+
readonly expire: Duration.Duration
|
|
167
|
+
readonly minimum: Duration.Duration
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* A service location record.
|
|
172
|
+
*
|
|
173
|
+
* @stability experimental
|
|
174
|
+
* @category models
|
|
175
|
+
* @since 4.0.0
|
|
176
|
+
*/
|
|
177
|
+
export interface Srv extends RecordProto<"SRV"> {
|
|
178
|
+
readonly target: Host.DomainName
|
|
179
|
+
readonly port: number
|
|
180
|
+
readonly priority: number
|
|
181
|
+
readonly weight: number
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* A TLS authentication record (RFC 6698), which pins the certificate or public
|
|
186
|
+
* key that a TLS service presents, for DNS-Based Authentication of Named
|
|
187
|
+
* Entities (DANE). Records are published at a name made of the service's port
|
|
188
|
+
* and protocol, such as `_443._tcp.example.com`.
|
|
189
|
+
*
|
|
190
|
+
* **Details**
|
|
191
|
+
*
|
|
192
|
+
* `certUsage` says how the record constrains the certificate chain (0 to 3:
|
|
193
|
+
* PKIX-TA, PKIX-EE, DANE-TA, DANE-EE), `selector` whether `data` matches the
|
|
194
|
+
* full certificate (0) or its public key (1), and `matchingType` whether
|
|
195
|
+
* `data` holds the selected content itself (0) or its SHA-256 (1) or SHA-512
|
|
196
|
+
* (2) digest. Unassigned values are kept as they are.
|
|
197
|
+
*
|
|
198
|
+
* **Gotchas**
|
|
199
|
+
*
|
|
200
|
+
* `data` is a copy of the bytes the record was made from, but typed arrays
|
|
201
|
+
* cannot be frozen; treat it as read-only, because changing it changes the
|
|
202
|
+
* record's equality.
|
|
203
|
+
*
|
|
204
|
+
* @stability experimental
|
|
205
|
+
* @category models
|
|
206
|
+
* @since 4.0.0
|
|
207
|
+
*/
|
|
208
|
+
export interface Tlsa extends RecordProto<"TLSA"> {
|
|
209
|
+
readonly certUsage: number
|
|
210
|
+
readonly selector: number
|
|
211
|
+
readonly matchingType: number
|
|
212
|
+
readonly data: Uint8Array
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* A text record. DNS stores text as one or more character strings, which are
|
|
217
|
+
* kept as separate chunks; protocols such as SPF read them joined, for example
|
|
218
|
+
* with `record.chunks.join("")`.
|
|
219
|
+
*
|
|
220
|
+
* **Details**
|
|
221
|
+
*
|
|
222
|
+
* Platform services decode each character string as UTF-8, replacing bytes
|
|
223
|
+
* that are not valid UTF-8 with U+FFFD, and `formatRecord` writes chunks back
|
|
224
|
+
* as UTF-8 bytes.
|
|
225
|
+
*
|
|
226
|
+
* @stability experimental
|
|
227
|
+
* @category models
|
|
228
|
+
* @since 4.0.0
|
|
229
|
+
*/
|
|
230
|
+
export interface Txt extends RecordProto<"TXT"> {
|
|
231
|
+
readonly chunks: Arr.NonEmptyReadonlyArray<string>
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Any supported DNS record value.
|
|
236
|
+
*
|
|
237
|
+
* **Details**
|
|
238
|
+
*
|
|
239
|
+
* Records are immutable values tagged by record type, compared and hashed by
|
|
240
|
+
* their fields. They hold the record data only: owner names and TTLs are not
|
|
241
|
+
* included because platform resolvers do not report them consistently. Names
|
|
242
|
+
* returned by platform services are fully qualified and end with a dot, so
|
|
243
|
+
* passing them back to the resolver does not apply search domains.
|
|
244
|
+
*
|
|
245
|
+
* @stability experimental
|
|
246
|
+
* @category models
|
|
247
|
+
* @since 4.0.0
|
|
248
|
+
*/
|
|
249
|
+
export type DnsRecord = A | Aaaa | Caa | Cname | Mx | Naptr | Ns | Ptr | Soa | Srv | Tlsa | Txt
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* The type of a DNS record, such as `"A"` or `"SRV"`.
|
|
253
|
+
*
|
|
254
|
+
* @stability experimental
|
|
255
|
+
* @category models
|
|
256
|
+
* @since 4.0.0
|
|
257
|
+
*/
|
|
258
|
+
export type RecordType =
|
|
259
|
+
| "A"
|
|
260
|
+
| "AAAA"
|
|
261
|
+
| "CAA"
|
|
262
|
+
| "CNAME"
|
|
263
|
+
| "MX"
|
|
264
|
+
| "NAPTR"
|
|
265
|
+
| "NS"
|
|
266
|
+
| "PTR"
|
|
267
|
+
| "SOA"
|
|
268
|
+
| "SRV"
|
|
269
|
+
| "TLSA"
|
|
270
|
+
| "TXT"
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* The record value for a record type.
|
|
274
|
+
*
|
|
275
|
+
* @stability experimental
|
|
276
|
+
* @category models
|
|
277
|
+
* @since 4.0.0
|
|
278
|
+
*/
|
|
279
|
+
export type RecordFor<T extends RecordType> = Extract<DnsRecord, { readonly _tag: T }>
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* The fields of a record type, without its `_tag`.
|
|
283
|
+
*
|
|
284
|
+
* @see {@link makeRecord}
|
|
285
|
+
* @stability experimental
|
|
286
|
+
* @category models
|
|
287
|
+
* @since 4.0.0
|
|
288
|
+
*/
|
|
289
|
+
export type RecordFields<T extends RecordType> = T extends RecordType ? Omit<RecordFor<T>, keyof RecordProto<T>>
|
|
290
|
+
: never
|
|
291
|
+
|
|
292
|
+
const isUint8 = (u: unknown): boolean => Number.isInteger(u) && (u as number) >= 0 && (u as number) <= 0xff
|
|
293
|
+
const isUint16 = (u: unknown): boolean => Number.isInteger(u) && (u as number) >= 0 && (u as number) <= 0xffff
|
|
294
|
+
const isUint32 = (u: unknown): boolean => Number.isInteger(u) && (u as number) >= 0 && (u as number) <= 0xffffffff
|
|
295
|
+
const isString = (u: unknown): u is string => typeof u === "string"
|
|
296
|
+
// SOA timers are 32-bit unsigned counts of seconds on the wire.
|
|
297
|
+
const isTimer = (u: unknown): boolean =>
|
|
298
|
+
Duration.isDuration(u) && Duration.isFinite(u) && isUint32(Duration.toSeconds(u))
|
|
299
|
+
|
|
300
|
+
const recordFields: {
|
|
301
|
+
readonly [K in RecordType]: { readonly [F in keyof RecordFields<K>]-?: (u: unknown) => boolean }
|
|
302
|
+
} = {
|
|
303
|
+
A: {
|
|
304
|
+
address: NetAddress.isIpv4Address
|
|
305
|
+
},
|
|
306
|
+
AAAA: {
|
|
307
|
+
address: NetAddress.isIpv6Address
|
|
308
|
+
},
|
|
309
|
+
CAA: {
|
|
310
|
+
critical: (u) => typeof u === "boolean",
|
|
311
|
+
tag: (u) => isString(u) && /^[a-z0-9]+$/i.test(u),
|
|
312
|
+
value: isString
|
|
313
|
+
},
|
|
314
|
+
CNAME: {
|
|
315
|
+
target: Host.isDomainName
|
|
316
|
+
},
|
|
317
|
+
MX: {
|
|
318
|
+
exchange: Host.isDomainName,
|
|
319
|
+
priority: isUint16
|
|
320
|
+
},
|
|
321
|
+
NAPTR: {
|
|
322
|
+
order: isUint16,
|
|
323
|
+
preference: isUint16,
|
|
324
|
+
flags: isString,
|
|
325
|
+
service: isString,
|
|
326
|
+
regexp: isString,
|
|
327
|
+
replacement: Host.isDomainName
|
|
328
|
+
},
|
|
329
|
+
NS: {
|
|
330
|
+
host: Host.isDomainName
|
|
331
|
+
},
|
|
332
|
+
PTR: {
|
|
333
|
+
host: (u) => isString(u) && u.length > 0
|
|
334
|
+
},
|
|
335
|
+
SOA: {
|
|
336
|
+
primary: Host.isDomainName,
|
|
337
|
+
admin: (u) => isString(u) && u.length > 0,
|
|
338
|
+
serial: isUint32,
|
|
339
|
+
refresh: isTimer,
|
|
340
|
+
retry: isTimer,
|
|
341
|
+
expire: isTimer,
|
|
342
|
+
minimum: isTimer
|
|
343
|
+
},
|
|
344
|
+
SRV: {
|
|
345
|
+
target: Host.isDomainName,
|
|
346
|
+
port: isUint16,
|
|
347
|
+
priority: isUint16,
|
|
348
|
+
weight: isUint16
|
|
349
|
+
},
|
|
350
|
+
TLSA: {
|
|
351
|
+
certUsage: isUint8,
|
|
352
|
+
selector: isUint8,
|
|
353
|
+
matchingType: isUint8,
|
|
354
|
+
data: (u) => u instanceof Uint8Array && u.length > 0
|
|
355
|
+
},
|
|
356
|
+
TXT: {
|
|
357
|
+
chunks: (u) => Array.isArray(u) && u.length > 0 && u.every(isString)
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Every supported record type.
|
|
363
|
+
*
|
|
364
|
+
* @stability experimental
|
|
365
|
+
* @category constants
|
|
366
|
+
* @since 4.0.0
|
|
367
|
+
*/
|
|
368
|
+
export const recordTypes: Arr.NonEmptyReadonlyArray<RecordType> = Object.keys(recordFields) as Array<any> as any
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Returns `true` when a value is a supported record type.
|
|
372
|
+
*
|
|
373
|
+
* @stability experimental
|
|
374
|
+
* @category guards
|
|
375
|
+
* @since 4.0.0
|
|
376
|
+
*/
|
|
377
|
+
export const isRecordType = (u: unknown): u is RecordType => recordTypes.includes(u as RecordType)
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Returns `true` when a value is a DNS record.
|
|
381
|
+
*
|
|
382
|
+
* @stability experimental
|
|
383
|
+
* @category guards
|
|
384
|
+
* @since 4.0.0
|
|
385
|
+
*/
|
|
386
|
+
export const isDnsRecord = (u: unknown): u is DnsRecord => hasProperty(u, RecordTypeId)
|
|
387
|
+
|
|
388
|
+
const RecordPrototype = {
|
|
389
|
+
...Inspectable.BaseProto,
|
|
390
|
+
[RecordTypeId]: RecordTypeId,
|
|
391
|
+
[Equal.symbol](this: DnsRecord, that: Equal.Equal): boolean {
|
|
392
|
+
return isDnsRecord(that) && that._tag === this._tag &&
|
|
393
|
+
Object.keys(this).every((key) => Equal.equals((this as any)[key], (that as any)[key]))
|
|
394
|
+
},
|
|
395
|
+
[Hash.symbol](this: DnsRecord): number {
|
|
396
|
+
return Hash.structureKeys(this, Object.keys(this))
|
|
397
|
+
},
|
|
398
|
+
toString(this: DnsRecord): string {
|
|
399
|
+
return formatRecord(this)
|
|
400
|
+
},
|
|
401
|
+
toJSON(this: DnsRecord): unknown {
|
|
402
|
+
return { ...this }
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Creates a checked DNS record of a record type from its fields.
|
|
408
|
+
*
|
|
409
|
+
* **Details**
|
|
410
|
+
*
|
|
411
|
+
* Every field is checked at runtime: addresses must belong to the record's
|
|
412
|
+
* address family, names must be normalized domain names, priorities, weights,
|
|
413
|
+
* and ports must be 16-bit unsigned integers, the SOA serial and timers must be
|
|
414
|
+
* 32-bit unsigned integers (the timers in whole seconds), the TLSA fields must
|
|
415
|
+
* be 8-bit unsigned integers with non-empty `data`, which is copied, TXT
|
|
416
|
+
* records need at least one chunk, and CAA property tags must be alphanumeric.
|
|
417
|
+
* Fields that do not belong to the record type are ignored.
|
|
418
|
+
*
|
|
419
|
+
* **Example** (Creating a service record)
|
|
420
|
+
*
|
|
421
|
+
* ```ts import.meta.vitest
|
|
422
|
+
* import { Result } from "effect"
|
|
423
|
+
* import { Dns, Host } from "effect/net"
|
|
424
|
+
*
|
|
425
|
+
* const record = Result.getOrThrow(Dns.makeRecord("SRV", {
|
|
426
|
+
* target: Host.domainNameFromStringUnsafe("db.internal"),
|
|
427
|
+
* port: 5432,
|
|
428
|
+
* priority: 10,
|
|
429
|
+
* weight: 5
|
|
430
|
+
* }))
|
|
431
|
+
* Dns.formatRecord(record) // => "SRV 10 5 5432 db.internal"
|
|
432
|
+
* ```
|
|
433
|
+
*
|
|
434
|
+
* @stability experimental
|
|
435
|
+
* @category constructors
|
|
436
|
+
* @since 4.0.0
|
|
437
|
+
*/
|
|
438
|
+
export const makeRecord = <T extends RecordType>(
|
|
439
|
+
type: T,
|
|
440
|
+
fields: RecordFields<T>
|
|
441
|
+
): Result.Result<RecordFor<T>, NetAddress.NetAddressError> => {
|
|
442
|
+
if (!isRecordType(type)) {
|
|
443
|
+
return Result.fail(new NetAddress.NetAddressError({ input: type, message: "unknown DNS record type" }))
|
|
444
|
+
}
|
|
445
|
+
const input: Record<string, unknown> = typeof fields === "object" && fields !== null ? fields : {}
|
|
446
|
+
const self = Object.create(RecordPrototype)
|
|
447
|
+
self._tag = type
|
|
448
|
+
|
|
449
|
+
for (const [key, check] of Object.entries<(u: unknown) => boolean>(recordFields[type])) {
|
|
450
|
+
const value = input[key]
|
|
451
|
+
if (!check(value)) {
|
|
452
|
+
return Result.fail(new NetAddress.NetAddressError({ input: fields, message: `invalid ${type} record ${key}` }))
|
|
453
|
+
}
|
|
454
|
+
self[key] = Array.isArray(value)
|
|
455
|
+
? Object.freeze([...value])
|
|
456
|
+
: value instanceof Uint8Array
|
|
457
|
+
? new Uint8Array(value)
|
|
458
|
+
: value
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
return Result.succeed(Object.freeze(self))
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* Creates a trusted DNS record of a record type from its fields, throwing on
|
|
466
|
+
* failure.
|
|
467
|
+
*
|
|
468
|
+
* @stability experimental
|
|
469
|
+
* @category unsafe
|
|
470
|
+
* @since 4.0.0
|
|
471
|
+
*/
|
|
472
|
+
export const makeRecordUnsafe = <T extends RecordType>(type: T, fields: RecordFields<T>): RecordFor<T> =>
|
|
473
|
+
Result.getOrThrow(makeRecord(type, fields))
|
|
474
|
+
|
|
475
|
+
const encoder = new TextEncoder()
|
|
476
|
+
|
|
477
|
+
const decimalEscape = (byte: number): string => `\\${byte.toString().padStart(3, "0")}`
|
|
478
|
+
|
|
479
|
+
// Quotes a character string in DNS presentation format (RFC 1035, section 5.1):
|
|
480
|
+
// `"` and `\` are escaped, and bytes outside printable ASCII are written as `\DDD`.
|
|
481
|
+
const quote = (value: string): string => {
|
|
482
|
+
let out = "\""
|
|
483
|
+
for (const byte of encoder.encode(value)) {
|
|
484
|
+
out += byte === 0x22 || byte === 0x5c
|
|
485
|
+
? `\\${String.fromCharCode(byte)}`
|
|
486
|
+
: byte < 0x20 || byte > 0x7e
|
|
487
|
+
? decimalEscape(byte)
|
|
488
|
+
: String.fromCharCode(byte)
|
|
489
|
+
}
|
|
490
|
+
return out + "\""
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
const nameSpecials = [0x22, 0x24, 0x28, 0x29, 0x3b, 0x40]
|
|
494
|
+
|
|
495
|
+
// Writes a name held as text, such as `Ptr.host`, in DNS presentation format:
|
|
496
|
+
// its `\.` and `\\` escapes are kept, other special characters are escaped, and
|
|
497
|
+
// spaces and bytes outside printable ASCII are written as `\DDD`.
|
|
498
|
+
const formatName = (text: string): string => {
|
|
499
|
+
const bytes = encoder.encode(text)
|
|
500
|
+
let out = ""
|
|
501
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
502
|
+
const byte = bytes[i]
|
|
503
|
+
if (byte === 0x5c) {
|
|
504
|
+
const next = bytes[i + 1]
|
|
505
|
+
if (next === 0x2e || next === 0x5c) {
|
|
506
|
+
out += `\\${String.fromCharCode(next)}`
|
|
507
|
+
i++
|
|
508
|
+
} else {
|
|
509
|
+
out += "\\\\"
|
|
510
|
+
}
|
|
511
|
+
} else if (byte <= 0x20 || byte > 0x7e) {
|
|
512
|
+
out += decimalEscape(byte)
|
|
513
|
+
} else {
|
|
514
|
+
out += nameSpecials.includes(byte) ? `\\${String.fromCharCode(byte)}` : String.fromCharCode(byte)
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
return out
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* Formats a record in DNS presentation format, prefixed by its type.
|
|
522
|
+
*
|
|
523
|
+
* @stability experimental
|
|
524
|
+
* @category encoding
|
|
525
|
+
* @since 4.0.0
|
|
526
|
+
*/
|
|
527
|
+
export const formatRecord = (self: DnsRecord): string => {
|
|
528
|
+
switch (self._tag) {
|
|
529
|
+
case "A":
|
|
530
|
+
case "AAAA":
|
|
531
|
+
return `${self._tag} ${NetAddress.formatIp(self.address)}`
|
|
532
|
+
case "CAA":
|
|
533
|
+
return `CAA ${self.critical ? 128 : 0} ${self.tag} ${quote(self.value)}`
|
|
534
|
+
case "CNAME":
|
|
535
|
+
return `CNAME ${self.target}`
|
|
536
|
+
case "MX":
|
|
537
|
+
return `MX ${self.priority} ${self.exchange}`
|
|
538
|
+
case "NAPTR":
|
|
539
|
+
return `NAPTR ${self.order} ${self.preference} ${quote(self.flags)} ${quote(self.service)} ${
|
|
540
|
+
quote(self.regexp)
|
|
541
|
+
} ${self.replacement}`
|
|
542
|
+
case "NS":
|
|
543
|
+
return `NS ${self.host}`
|
|
544
|
+
case "PTR":
|
|
545
|
+
return `PTR ${formatName(self.host)}`
|
|
546
|
+
case "SOA":
|
|
547
|
+
return `SOA ${self.primary} ${formatName(self.admin)} ${self.serial} ${Duration.toSeconds(self.refresh)} ${
|
|
548
|
+
Duration.toSeconds(self.retry)
|
|
549
|
+
} ${Duration.toSeconds(self.expire)} ${Duration.toSeconds(self.minimum)}`
|
|
550
|
+
case "SRV":
|
|
551
|
+
return `SRV ${self.priority} ${self.weight} ${self.port} ${self.target}`
|
|
552
|
+
case "TLSA":
|
|
553
|
+
return `TLSA ${self.certUsage} ${self.selector} ${self.matchingType} ${Hex.encode(self.data).toUpperCase()}`
|
|
554
|
+
case "TXT":
|
|
555
|
+
return `TXT ${self.chunks.map(quote).join(" ")}`
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
/**
|
|
560
|
+
* Returns the reverse-lookup domain name for an IP address, in `in-addr.arpa`
|
|
561
|
+
* for IPv4 or `ip6.arpa` for IPv6.
|
|
562
|
+
*
|
|
563
|
+
* **Example** (Building reverse-lookup names)
|
|
564
|
+
*
|
|
565
|
+
* ```ts import.meta.vitest
|
|
566
|
+
* import { Dns, NetAddress } from "effect/net"
|
|
567
|
+
*
|
|
568
|
+
* Dns.reverseName(NetAddress.ipFromStringUnsafe("192.0.2.1")) // => "1.2.0.192.in-addr.arpa"
|
|
569
|
+
* ```
|
|
570
|
+
*
|
|
571
|
+
* @stability experimental
|
|
572
|
+
* @category converting
|
|
573
|
+
* @since 4.0.0
|
|
574
|
+
*/
|
|
575
|
+
export const reverseName = (address: NetAddress.IpAddress): Host.DomainName => {
|
|
576
|
+
if (NetAddress.isIpv4Address(address)) {
|
|
577
|
+
return `${[...NetAddress.ipv4ToOctets(address)].reverse().join(".")}.in-addr.arpa` as Host.DomainName
|
|
578
|
+
}
|
|
579
|
+
const nibbles: Array<string> = []
|
|
580
|
+
for (const octet of NetAddress.ipv6ToOctets(address)) {
|
|
581
|
+
nibbles.push((octet >> 4).toString(16), (octet & 0xf).toString(16))
|
|
582
|
+
}
|
|
583
|
+
return `${nibbles.reverse().join(".")}.ip6.arpa` as Host.DomainName
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// =============================================================================
|
|
587
|
+
// Name servers
|
|
588
|
+
// =============================================================================
|
|
589
|
+
|
|
590
|
+
/**
|
|
591
|
+
* Parses the address of a name server: an IP address, which uses port 53, or
|
|
592
|
+
* an IP address and port.
|
|
593
|
+
*
|
|
594
|
+
* **Details**
|
|
595
|
+
*
|
|
596
|
+
* IPv6 addresses with a port are written in brackets, and IPv6 addresses
|
|
597
|
+
* without one are not.
|
|
598
|
+
*
|
|
599
|
+
* **Example** (Parsing name server addresses)
|
|
600
|
+
*
|
|
601
|
+
* ```ts import.meta.vitest
|
|
602
|
+
* import { Result } from "effect"
|
|
603
|
+
* import { Dns, NetAddress } from "effect/net"
|
|
604
|
+
*
|
|
605
|
+
* const format = (input: string) => Result.map(Dns.nameServerFromString(input), NetAddress.formatInet)
|
|
606
|
+
*
|
|
607
|
+
* format("192.0.2.53") // => Result.succeed("192.0.2.53:53")
|
|
608
|
+
* format("2001:db8::53") // => Result.succeed("[2001:db8::53]:53")
|
|
609
|
+
* format("[2001:db8::53]:5353") // => Result.succeed("[2001:db8::53]:5353")
|
|
610
|
+
* Result.isFailure(format("ns.example")) // => true
|
|
611
|
+
* ```
|
|
612
|
+
*
|
|
613
|
+
* @stability experimental
|
|
614
|
+
* @category decoding
|
|
615
|
+
* @since 4.0.0
|
|
616
|
+
*/
|
|
617
|
+
export const nameServerFromString = (
|
|
618
|
+
input: string
|
|
619
|
+
): Result.Result<NetAddress.InetAddress, NetAddress.NetAddressError> =>
|
|
620
|
+
NetAddress.inetAddressFromString(
|
|
621
|
+
input.startsWith("[") || /^[^:]*:\d+$/.test(input) ? input : input.includes(":") ? `[${input}]:53` : `${input}:53`
|
|
622
|
+
)
|
|
623
|
+
|
|
624
|
+
/**
|
|
625
|
+
* Converts a name server to the internet address to query. Strings are parsed
|
|
626
|
+
* like `nameServerFromString`, internet addresses and address parts with a
|
|
627
|
+
* port are converted like `NetAddress.inetAddressFromInput`, and other IP
|
|
628
|
+
* address inputs are converted like `NetAddress.ipFromInput` and use port 53.
|
|
629
|
+
*
|
|
630
|
+
* @stability experimental
|
|
631
|
+
* @category constructors
|
|
632
|
+
* @since 4.0.0
|
|
633
|
+
*/
|
|
634
|
+
export const nameServerFromInput = (
|
|
635
|
+
input: NetAddress.IpAddressInput | NetAddress.InetAddressInput
|
|
636
|
+
): Result.Result<NetAddress.InetAddress, NetAddress.NetAddressError> =>
|
|
637
|
+
typeof input === "string"
|
|
638
|
+
? nameServerFromString(input)
|
|
639
|
+
: "port" in input
|
|
640
|
+
? NetAddress.inetAddressFromInput(input)
|
|
641
|
+
: Result.map(NetAddress.ipFromInput(input), (address) => NetAddress.inetAddressUnsafe(address, 53))
|
|
642
|
+
|
|
643
|
+
// =============================================================================
|
|
644
|
+
// Errors
|
|
645
|
+
// =============================================================================
|
|
646
|
+
|
|
647
|
+
/**
|
|
648
|
+
* The normalized reason for a failed name resolution.
|
|
649
|
+
*
|
|
650
|
+
* **Details**
|
|
651
|
+
*
|
|
652
|
+
* - `NotFound`: no records were found, either because the name does not exist
|
|
653
|
+
* or because it has no records of the requested type or address family.
|
|
654
|
+
* Like Go's `DNSError.IsNotFound`, the two cases are not distinguished
|
|
655
|
+
* because not every platform resolver can tell them apart.
|
|
656
|
+
* - `Timeout`, `Temporary`, and `ServerFailure`: transient failures that may
|
|
657
|
+
* succeed when retried.
|
|
658
|
+
* - `Refused`: the server refused the query or could not be reached.
|
|
659
|
+
* - `BadName`: the name was rejected as malformed.
|
|
660
|
+
* - `InvalidResponse`: the platform returned data that could not be converted
|
|
661
|
+
* into record values.
|
|
662
|
+
* - `Unsupported`: the platform does not support the operation or record type.
|
|
663
|
+
*
|
|
664
|
+
* @stability experimental
|
|
665
|
+
* @category errors
|
|
666
|
+
* @since 4.0.0
|
|
667
|
+
*/
|
|
668
|
+
export type DnsErrorReason =
|
|
669
|
+
| "NotFound"
|
|
670
|
+
| "Timeout"
|
|
671
|
+
| "Temporary"
|
|
672
|
+
| "Refused"
|
|
673
|
+
| "ServerFailure"
|
|
674
|
+
| "BadName"
|
|
675
|
+
| "InvalidResponse"
|
|
676
|
+
| "Unsupported"
|
|
677
|
+
| "Unknown"
|
|
678
|
+
|
|
679
|
+
/**
|
|
680
|
+
* A failed lookup, query, or reverse lookup.
|
|
681
|
+
*
|
|
682
|
+
* @stability experimental
|
|
683
|
+
* @category errors
|
|
684
|
+
* @since 4.0.0
|
|
685
|
+
*/
|
|
686
|
+
export class DnsError extends Data.TaggedError("DnsError")<{
|
|
687
|
+
readonly reason: DnsErrorReason
|
|
688
|
+
readonly method: "lookup" | "resolve" | "reverse"
|
|
689
|
+
readonly hostname: string
|
|
690
|
+
readonly recordType?: RecordType | undefined
|
|
691
|
+
readonly cause?: unknown
|
|
692
|
+
}> {
|
|
693
|
+
/**
|
|
694
|
+
* Formats the reason, operation, and name of the failed resolution.
|
|
695
|
+
*
|
|
696
|
+
* @since 4.0.0
|
|
697
|
+
*/
|
|
698
|
+
override get message(): string {
|
|
699
|
+
return `${this.reason}: Dns.${this.method} (${this.hostname}${
|
|
700
|
+
this.recordType === undefined ? "" : ` ${this.recordType}`
|
|
701
|
+
})`
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
/**
|
|
705
|
+
* Whether the failure is transient and the operation may succeed if retried.
|
|
706
|
+
*
|
|
707
|
+
* @since 4.0.0
|
|
708
|
+
*/
|
|
709
|
+
get isTemporary(): boolean {
|
|
710
|
+
return this.reason === "Timeout" || this.reason === "Temporary" || this.reason === "ServerFailure"
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
// =============================================================================
|
|
715
|
+
// Service
|
|
716
|
+
// =============================================================================
|
|
717
|
+
|
|
718
|
+
/**
|
|
719
|
+
* Options for address lookups. Without a `family`, addresses of both families
|
|
720
|
+
* are returned.
|
|
721
|
+
*
|
|
722
|
+
* @stability experimental
|
|
723
|
+
* @category models
|
|
724
|
+
* @since 4.0.0
|
|
725
|
+
*/
|
|
726
|
+
export interface LookupOptions {
|
|
727
|
+
readonly family?: NetAddress.IpFamily | undefined
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
/**
|
|
731
|
+
* Service that resolves names to addresses, queries DNS records, and looks up
|
|
732
|
+
* the names of addresses.
|
|
733
|
+
*
|
|
734
|
+
* **Details**
|
|
735
|
+
*
|
|
736
|
+
* Every operation either returns at least one result or fails with a
|
|
737
|
+
* {@link DnsError}.
|
|
738
|
+
*
|
|
739
|
+
* @stability experimental
|
|
740
|
+
* @category services
|
|
741
|
+
* @since 4.0.0
|
|
742
|
+
*/
|
|
743
|
+
export class Dns extends Context.Service<Dns, {
|
|
744
|
+
/**
|
|
745
|
+
* Looks up the addresses used to connect to a host name.
|
|
746
|
+
*
|
|
747
|
+
* **Details**
|
|
748
|
+
*
|
|
749
|
+
* Addresses are returned in the implementation's preferred order. Platform
|
|
750
|
+
* implementations use the operating system resolver and keep the system's
|
|
751
|
+
* order.
|
|
752
|
+
* Names are parsed and normalized; invalid names fail with `BadName`.
|
|
753
|
+
*
|
|
754
|
+
* **Gotchas**
|
|
755
|
+
*
|
|
756
|
+
* IPv6 link-local addresses (`fe80::/10`) are returned without the interface
|
|
757
|
+
* they belong to, because the runtimes' resolvers do not report it.
|
|
758
|
+
*/
|
|
759
|
+
lookup<F extends NetAddress.IpFamily>(
|
|
760
|
+
host: Host.DomainNameInput,
|
|
761
|
+
options: { readonly family: F }
|
|
762
|
+
): Effect.Effect<Arr.NonEmptyReadonlyArray<NetAddress.FamilyAddress<F>>, DnsError>
|
|
763
|
+
lookup(
|
|
764
|
+
host: Host.DomainNameInput,
|
|
765
|
+
options?: LookupOptions
|
|
766
|
+
): Effect.Effect<Arr.NonEmptyReadonlyArray<NetAddress.IpAddress>, DnsError>
|
|
767
|
+
|
|
768
|
+
/**
|
|
769
|
+
* Queries the DNS records of one type for a name.
|
|
770
|
+
* Names are parsed and normalized; invalid names fail with `BadName`.
|
|
771
|
+
*/
|
|
772
|
+
resolve<T extends RecordType>(
|
|
773
|
+
name: Host.DomainNameInput,
|
|
774
|
+
type: T
|
|
775
|
+
): Effect.Effect<Arr.NonEmptyReadonlyArray<RecordFor<T>>, DnsError>
|
|
776
|
+
|
|
777
|
+
/**
|
|
778
|
+
* Looks up the host names of an address with a reverse DNS query.
|
|
779
|
+
*
|
|
780
|
+
* **Details**
|
|
781
|
+
*
|
|
782
|
+
* Addresses given as strings are parsed; invalid addresses fail with
|
|
783
|
+
* `BadName`. Names that are not valid host names are skipped, including
|
|
784
|
+
* names with non-ASCII labels, which are not converted to their ASCII
|
|
785
|
+
* (`xn--`) form because that form is a different DNS name. When every
|
|
786
|
+
* returned name is skipped, the lookup fails with `InvalidResponse`.
|
|
787
|
+
*/
|
|
788
|
+
reverse(address: NetAddress.IpAddressInput): Effect.Effect<Arr.NonEmptyReadonlyArray<Host.DomainName>, DnsError>
|
|
789
|
+
}>()("effect/net/Dns") {}
|
|
790
|
+
|
|
791
|
+
const notFound = (method: DnsError["method"], hostname: string, recordType?: RecordType) =>
|
|
792
|
+
Effect.fail(new DnsError({ reason: "NotFound", method, hostname, recordType }))
|
|
793
|
+
|
|
794
|
+
const badName = (method: DnsError["method"], hostname: string, cause: unknown, recordType?: RecordType) =>
|
|
795
|
+
Effect.fail(new DnsError({ reason: "BadName", method, hostname, recordType, cause }))
|
|
796
|
+
|
|
797
|
+
const asciiName = /^[\w.-]+$/
|
|
798
|
+
|
|
799
|
+
/**
|
|
800
|
+
* Creates a `Dns` service from platform resolver operations.
|
|
801
|
+
*
|
|
802
|
+
* **Details**
|
|
803
|
+
*
|
|
804
|
+
* Lookup and query names are parsed and normalized, and reverse lookup
|
|
805
|
+
* addresses are parsed, before platform callbacks receive them. Invalid names
|
|
806
|
+
* and addresses fail with `BadName`.
|
|
807
|
+
*
|
|
808
|
+
* The constructor filters lookups by the requested address family, keeps only
|
|
809
|
+
* records of the requested type, removes duplicates, and turns empty results
|
|
810
|
+
* into `NotFound` failures. `reverse` receives the names of an address and
|
|
811
|
+
* keeps those that are valid host names, failing with `InvalidResponse` when
|
|
812
|
+
* there are names but none is valid. Without a `reverse` operation, the names
|
|
813
|
+
* are those of the PTR records at the address's `reverseName`, and failures of
|
|
814
|
+
* that query are reported as failures of `reverse`.
|
|
815
|
+
*
|
|
816
|
+
* Only `lookup` is required. Platforms that can resolve addresses but cannot
|
|
817
|
+
* query records, such as browsers, omit `resolve`; record queries, and reverse
|
|
818
|
+
* lookups without a `reverse` operation, then fail with `Unsupported`.
|
|
819
|
+
*
|
|
820
|
+
* @stability experimental
|
|
821
|
+
* @category constructors
|
|
822
|
+
* @since 4.0.0
|
|
823
|
+
*/
|
|
824
|
+
export const make = (impl: {
|
|
825
|
+
readonly lookup: (
|
|
826
|
+
host: Host.DomainName,
|
|
827
|
+
family: NetAddress.IpFamily | undefined
|
|
828
|
+
) => Effect.Effect<ReadonlyArray<NetAddress.IpAddress>, DnsError>
|
|
829
|
+
readonly resolve?:
|
|
830
|
+
| ((name: Host.DomainName, type: RecordType) => Effect.Effect<ReadonlyArray<DnsRecord>, DnsError>)
|
|
831
|
+
| undefined
|
|
832
|
+
readonly reverse?:
|
|
833
|
+
| ((address: NetAddress.IpAddress) => Effect.Effect<ReadonlyArray<string>, DnsError>)
|
|
834
|
+
| undefined
|
|
835
|
+
}): Dns["Service"] => {
|
|
836
|
+
const inFamily = (family: NetAddress.IpFamily | undefined) => (address: NetAddress.IpAddress): boolean =>
|
|
837
|
+
family === undefined || NetAddress.isFamily(address, family)
|
|
838
|
+
|
|
839
|
+
const resolveRecords = impl.resolve ??
|
|
840
|
+
((name: Host.DomainName, type: RecordType) =>
|
|
841
|
+
Effect.fail(new DnsError({ reason: "Unsupported", method: "resolve", hostname: name, recordType: type })))
|
|
842
|
+
|
|
843
|
+
const reverseNames = impl.reverse ??
|
|
844
|
+
((address: NetAddress.IpAddress) =>
|
|
845
|
+
resolveRecords(reverseName(address), "PTR").pipe(
|
|
846
|
+
Effect.map(Arr.flatMap((record) => record._tag === "PTR" ? [record.host] : [])),
|
|
847
|
+
Effect.mapError((error) =>
|
|
848
|
+
new DnsError({
|
|
849
|
+
reason: error.reason,
|
|
850
|
+
method: "reverse",
|
|
851
|
+
hostname: NetAddress.formatIp(address),
|
|
852
|
+
cause: error.cause
|
|
853
|
+
})
|
|
854
|
+
)
|
|
855
|
+
))
|
|
856
|
+
|
|
857
|
+
const lookup = (input: Host.DomainNameInput, options?: LookupOptions) =>
|
|
858
|
+
Result.match(Host.domainNameFromInput(input), {
|
|
859
|
+
onFailure: (cause) => badName("lookup", input, cause),
|
|
860
|
+
onSuccess: (host) =>
|
|
861
|
+
impl.lookup(host, options?.family).pipe(
|
|
862
|
+
Effect.flatMap((addresses) =>
|
|
863
|
+
Arr.match(Arr.dedupe(addresses.filter(inFamily(options?.family))), {
|
|
864
|
+
onEmpty: () => notFound("lookup", host),
|
|
865
|
+
onNonEmpty: Effect.succeed
|
|
866
|
+
})
|
|
867
|
+
)
|
|
868
|
+
)
|
|
869
|
+
})
|
|
870
|
+
|
|
871
|
+
return Dns.of({
|
|
872
|
+
lookup,
|
|
873
|
+
resolve: <T extends RecordType>(input: Host.DomainNameInput, type: T) =>
|
|
874
|
+
Result.match(Host.domainNameFromInput(input), {
|
|
875
|
+
onFailure: (cause) => badName("resolve", input, cause, type),
|
|
876
|
+
onSuccess: (name) =>
|
|
877
|
+
resolveRecords(name, type).pipe(
|
|
878
|
+
Effect.flatMap((records) =>
|
|
879
|
+
Arr.match(Arr.dedupe(records.filter((record): record is RecordFor<T> => record._tag === type)), {
|
|
880
|
+
onEmpty: () => notFound("resolve", name, type),
|
|
881
|
+
onNonEmpty: Effect.succeed
|
|
882
|
+
})
|
|
883
|
+
)
|
|
884
|
+
)
|
|
885
|
+
}),
|
|
886
|
+
reverse: (input) =>
|
|
887
|
+
Result.match(NetAddress.ipFromInput(input), {
|
|
888
|
+
onFailure: (cause) => badName("reverse", String(input), cause),
|
|
889
|
+
onSuccess: (address) =>
|
|
890
|
+
reverseNames(address).pipe(
|
|
891
|
+
Effect.flatMap((names) => {
|
|
892
|
+
const hostname = NetAddress.formatIp(address)
|
|
893
|
+
// Only ASCII names: `domainNameFromString` would convert UTF-8 labels
|
|
894
|
+
// to a different, `xn--` name.
|
|
895
|
+
const hosts = Arr.dedupe(
|
|
896
|
+
Arr.filterMap(names.filter((name) => asciiName.test(name)), Host.domainNameFromString)
|
|
897
|
+
)
|
|
898
|
+
return Arr.isReadonlyArrayNonEmpty(hosts)
|
|
899
|
+
? Effect.succeed(hosts)
|
|
900
|
+
: names.length > 0
|
|
901
|
+
? Effect.fail(new DnsError({ reason: "InvalidResponse", method: "reverse", hostname }))
|
|
902
|
+
: notFound("reverse", hostname)
|
|
903
|
+
})
|
|
904
|
+
)
|
|
905
|
+
})
|
|
906
|
+
})
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
// =============================================================================
|
|
910
|
+
// Static resolver
|
|
911
|
+
// =============================================================================
|
|
912
|
+
|
|
913
|
+
/**
|
|
914
|
+
* Fixed names, addresses, and records for a static resolver.
|
|
915
|
+
*
|
|
916
|
+
* **Details**
|
|
917
|
+
*
|
|
918
|
+
* `hosts` works like a hosts file: its entries are used by `lookup` and
|
|
919
|
+
* `reverse`, but not by `resolve`. `records` holds DNS records by owner name and
|
|
920
|
+
* is used by all three operations. Names are normalized like
|
|
921
|
+
* `Host.domainNameFromString`, and a trailing dot is ignored. Addresses are
|
|
922
|
+
* converted like `NetAddress.ipFromInput`.
|
|
923
|
+
*
|
|
924
|
+
* @stability experimental
|
|
925
|
+
* @category models
|
|
926
|
+
* @since 4.0.0
|
|
927
|
+
*/
|
|
928
|
+
export interface StaticZone {
|
|
929
|
+
readonly hosts?: ReadonlyRecord<string, ReadonlyArray<NetAddress.IpAddressInput>> | undefined
|
|
930
|
+
readonly records?: ReadonlyRecord<string, ReadonlyArray<DnsRecord>> | undefined
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
const zoneKey = (name: string): string => name.length > 1 && name.endsWith(".") ? name.slice(0, -1) : name
|
|
934
|
+
|
|
935
|
+
/**
|
|
936
|
+
* Creates a `Dns` service that answers from fixed data.
|
|
937
|
+
*
|
|
938
|
+
* **When to use**
|
|
939
|
+
*
|
|
940
|
+
* Use to test code that depends on `Dns`, or to provide fixed names on
|
|
941
|
+
* platforms without a resolver.
|
|
942
|
+
*
|
|
943
|
+
* **Details**
|
|
944
|
+
*
|
|
945
|
+
* Names without matching addresses or records fail with `NotFound`. CNAME
|
|
946
|
+
* records are returned by `CNAME` queries but are not followed.
|
|
947
|
+
*
|
|
948
|
+
* **Example** (Looking up a name with a static resolver)
|
|
949
|
+
*
|
|
950
|
+
* ```ts import.meta.vitest
|
|
951
|
+
* import { Effect, Result } from "effect"
|
|
952
|
+
* import { Dns, NetAddress } from "effect/net"
|
|
953
|
+
*
|
|
954
|
+
* const dns = Result.getOrThrow(Dns.makeStatic({
|
|
955
|
+
* hosts: { "db.internal": ["10.0.0.5"] }
|
|
956
|
+
* }))
|
|
957
|
+
*
|
|
958
|
+
* const program = dns.lookup("db.internal").pipe(
|
|
959
|
+
* Effect.map((addresses) => addresses.map(NetAddress.formatIp))
|
|
960
|
+
* )
|
|
961
|
+
*
|
|
962
|
+
* await Effect.runPromise(program) // => ["10.0.0.5"]
|
|
963
|
+
* ```
|
|
964
|
+
*
|
|
965
|
+
* @see {@link layerStatic}
|
|
966
|
+
* @stability experimental
|
|
967
|
+
* @category constructors
|
|
968
|
+
* @since 4.0.0
|
|
969
|
+
*/
|
|
970
|
+
export const makeStatic = (zone: StaticZone): Result.Result<Dns["Service"], NetAddress.NetAddressError> => {
|
|
971
|
+
const entries = new Map<string, {
|
|
972
|
+
readonly addresses: Array<NetAddress.IpAddress>
|
|
973
|
+
readonly records: Array<DnsRecord>
|
|
974
|
+
}>()
|
|
975
|
+
const entry = (name: string) =>
|
|
976
|
+
Result.map(Host.domainNameFromString(name), (domain) => {
|
|
977
|
+
const key = zoneKey(domain)
|
|
978
|
+
let current = entries.get(key)
|
|
979
|
+
if (current === undefined) {
|
|
980
|
+
current = { addresses: [], records: [] }
|
|
981
|
+
entries.set(key, current)
|
|
982
|
+
}
|
|
983
|
+
return current
|
|
984
|
+
})
|
|
985
|
+
|
|
986
|
+
for (const [name, addresses] of Object.entries(zone.hosts ?? {})) {
|
|
987
|
+
const result = entry(name)
|
|
988
|
+
if (Result.isFailure(result)) return Result.fail(result.failure)
|
|
989
|
+
for (const input of addresses) {
|
|
990
|
+
const address = NetAddress.ipFromInput(input)
|
|
991
|
+
if (Result.isFailure(address)) return Result.fail(address.failure)
|
|
992
|
+
result.success.addresses.push(address.success)
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
for (const [name, records] of Object.entries(zone.records ?? {})) {
|
|
997
|
+
const result = entry(name)
|
|
998
|
+
if (Result.isFailure(result)) return Result.fail(result.failure)
|
|
999
|
+
for (const record of records) {
|
|
1000
|
+
if (!isDnsRecord(record)) {
|
|
1001
|
+
return Result.fail(new NetAddress.NetAddressError({ input: record, message: "expected a DNS record" }))
|
|
1002
|
+
}
|
|
1003
|
+
result.success.records.push(record)
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
const recordsAt = (name: string): ReadonlyArray<DnsRecord> => entries.get(zoneKey(name))?.records ?? []
|
|
1008
|
+
|
|
1009
|
+
return Result.succeed(make({
|
|
1010
|
+
lookup: (host) =>
|
|
1011
|
+
Effect.sync(() => [
|
|
1012
|
+
...(entries.get(zoneKey(host))?.addresses ?? []),
|
|
1013
|
+
...recordsAt(host).flatMap((record) => record._tag === "A" || record._tag === "AAAA" ? [record.address] : [])
|
|
1014
|
+
]),
|
|
1015
|
+
resolve: (name) => Effect.sync(() => recordsAt(name)),
|
|
1016
|
+
reverse: (address) =>
|
|
1017
|
+
Effect.sync(() => [
|
|
1018
|
+
...[...entries].flatMap(([name, current]) =>
|
|
1019
|
+
current.addresses.some((candidate) => Equal.equals(candidate, address)) ? [name] : []
|
|
1020
|
+
),
|
|
1021
|
+
...recordsAt(reverseName(address)).flatMap((record) => record._tag === "PTR" ? [record.host] : [])
|
|
1022
|
+
])
|
|
1023
|
+
}))
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
/**
|
|
1027
|
+
* Creates a layer that provides a `Dns` service answering from fixed data.
|
|
1028
|
+
*
|
|
1029
|
+
* @see {@link makeStatic} for the resolution rules
|
|
1030
|
+
* @stability experimental
|
|
1031
|
+
* @category layers
|
|
1032
|
+
* @since 4.0.0
|
|
1033
|
+
*/
|
|
1034
|
+
export const layerStatic = (zone: StaticZone): Layer.Layer<Dns, NetAddress.NetAddressError> =>
|
|
1035
|
+
Layer.effect(Dns, Effect.fromResult(makeStatic(zone)))
|