@push.rocks/smartdns 7.12.1 → 8.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist_rust/rustdns-client_linux_amd64 +0 -0
- package/dist_rust/rustdns-client_linux_arm64 +0 -0
- package/dist_rust/rustdns_linux_amd64 +0 -0
- package/dist_rust/rustdns_linux_arm64 +0 -0
- package/dist_ts/00_commitinfo_data.js +2 -2
- package/dist_ts_client/classes.dnsclient.d.ts +24 -2
- package/dist_ts_client/classes.dnsclient.js +45 -19
- package/dist_ts_client/plugins.js +3 -2
- package/dist_ts_server/classes.dnsserver.d.ts +136 -4
- package/dist_ts_server/classes.dnsserver.js +290 -58
- package/dist_ts_server/classes.rustdnsbridge.d.ts +16 -3
- package/dist_ts_server/classes.rustdnsbridge.js +6 -3
- package/package.json +1 -1
- package/readme.hints.md +120 -0
- package/readme.md +31 -8
- package/ts/00_commitinfo_data.ts +1 -1
|
@@ -83,11 +83,14 @@ export class RustDnsBridge extends plugins.events.EventEmitter {
|
|
|
83
83
|
}
|
|
84
84
|
/**
|
|
85
85
|
* Process a raw DNS packet via IPC (for manual/passthrough mode).
|
|
86
|
-
* Returns the DNS response as a Buffer.
|
|
86
|
+
* Returns the DNS response as a Buffer. Pass `transport: 'udp'` when the
|
|
87
|
+
* response will be sent as a UDP datagram, so it is truncated (TC bit) to
|
|
88
|
+
* the client's advertised payload size instead of exceeding it.
|
|
87
89
|
*/
|
|
88
|
-
async processPacket(packet) {
|
|
90
|
+
async processPacket(packet, transport) {
|
|
89
91
|
const result = await this.bridge.sendCommand('processPacket', {
|
|
90
92
|
packet: packet.toString('base64'),
|
|
93
|
+
transport,
|
|
91
94
|
});
|
|
92
95
|
return Buffer.from(result.packet, 'base64');
|
|
93
96
|
}
|
|
@@ -134,4 +137,4 @@ function getPlatformSuffix() {
|
|
|
134
137
|
}
|
|
135
138
|
return null;
|
|
136
139
|
}
|
|
137
|
-
//# sourceMappingURL=data:application/json;base64,
|
|
140
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY2xhc3Nlcy5ydXN0ZG5zYnJpZGdlLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vdHNfc2VydmVyL2NsYXNzZXMucnVzdGRuc2JyaWRnZS50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEtBQUssT0FBTyxNQUFNLGNBQWMsQ0FBQztBQTBGeEM7O0dBRUc7QUFDSCxNQUFNLE9BQU8sYUFBYyxTQUFRLE9BQU8sQ0FBQyxNQUFNLENBQUMsWUFBWTtJQUc1RDtRQUNFLEtBQUssRUFBRSxDQUFDO1FBRVIsTUFBTSxVQUFVLEdBQUcsT0FBTyxDQUFDLElBQUksQ0FBQyxPQUFPLENBQ3JDLE9BQU8sQ0FBQyxJQUFJLENBQUMsT0FBTyxDQUFDLElBQUksR0FBRyxDQUFDLE1BQU0sQ0FBQyxJQUFJLENBQUMsR0FBRyxDQUFDLENBQUMsUUFBUSxDQUFDLEVBQ3ZELElBQUksQ0FDTCxDQUFDO1FBRUYsMkVBQTJFO1FBQzNFLE1BQU0sY0FBYyxHQUFHLGlCQUFpQixFQUFFLENBQUM7UUFDM0MsTUFBTSxVQUFVLEdBQWEsRUFBRSxDQUFDO1FBRWhDLDBFQUEwRTtRQUMxRSxJQUFJLGNBQWMsRUFBRSxDQUFDO1lBQ25CLFVBQVUsQ0FBQyxJQUFJLENBQUMsT0FBTyxDQUFDLElBQUksQ0FBQyxJQUFJLENBQUMsVUFBVSxFQUFFLFdBQVcsRUFBRSxXQUFXLGNBQWMsRUFBRSxDQUFDLENBQUMsQ0FBQztRQUMzRixDQUFDO1FBQ0QsMkNBQTJDO1FBQzNDLFVBQVUsQ0FBQyxJQUFJLENBQUMsT0FBTyxDQUFDLElBQUksQ0FBQyxJQUFJLENBQUMsVUFBVSxFQUFFLFdBQVcsRUFBRSxTQUFTLENBQUMsQ0FBQyxDQUFDO1FBQ3ZFLHdCQUF3QjtRQUN4QixVQUFVLENBQUMsSUFBSSxDQUFDLE9BQU8sQ0FBQyxJQUFJLENBQUMsSUFBSSxDQUFDLFVBQVUsRUFBRSxNQUFNLEVBQUUsUUFBUSxFQUFFLFNBQVMsRUFBRSxTQUFTLENBQUMsQ0FBQyxDQUFDO1FB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
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@push.rocks/smartdns",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "8.0.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "A robust TypeScript library providing advanced DNS management and resolution capabilities including support for DNSSEC, custom DNS servers, and integration with various DNS providers.",
|
|
6
6
|
"exports": {
|
package/readme.hints.md
CHANGED
|
@@ -143,9 +143,129 @@ The test suite demonstrates:
|
|
|
143
143
|
- Properly serializes both components for DNSSEC signature generation
|
|
144
144
|
- Enables mail exchange records to be authenticated with DNSSEC
|
|
145
145
|
|
|
146
|
+
## Authority Model (server)
|
|
147
|
+
|
|
148
|
+
Authority comes from **configuration**, never from the presence of a handler.
|
|
149
|
+
|
|
150
|
+
- `authoritativeZones` (falling back to `[dnssecZone]`) is the single source of
|
|
151
|
+
truth for the `aa` bit. An answer only carries `aa` when the queried name
|
|
152
|
+
falls inside one of those zones.
|
|
153
|
+
- `registerHandler(pattern, types, handler, options?)` takes
|
|
154
|
+
`options.authority`:
|
|
155
|
+
- `'authoritative'` (default) — the handler only contributes for names inside
|
|
156
|
+
a configured zone. A match outside every zone is **suppressed**, not
|
|
157
|
+
answered, and reported on the `refused` event. A registration that cannot
|
|
158
|
+
match any configured zone also logs a warning once, at registration time.
|
|
159
|
+
- `'non-authoritative'` — the handler may answer any matching name, but the
|
|
160
|
+
response never carries `aa` and is never DNSSEC-signed. This is the mode
|
|
161
|
+
for split-horizon / private-route overlays.
|
|
162
|
+
- `aa` is a per-message flag, so a single non-authoritative contribution
|
|
163
|
+
downgrades the whole response.
|
|
164
|
+
|
|
165
|
+
Why this matters: before v8, registering a handler *was* the public claim of
|
|
166
|
+
authority. A private-route overlay registered for a domain delegated elsewhere
|
|
167
|
+
made the public nameserver answer with `aa` and an RFC1918 address, while
|
|
168
|
+
refusing every other query type for the same name. Both halves came from the
|
|
169
|
+
same defect — two disagreeing sources of truth.
|
|
170
|
+
|
|
171
|
+
### Response outcomes
|
|
172
|
+
|
|
173
|
+
`TDnsQueryResponseKind` maps one-to-one onto the wire:
|
|
174
|
+
|
|
175
|
+
| kind | rcode | `aa` | authority section |
|
|
176
|
+
| --- | --- | --- | --- |
|
|
177
|
+
| `answer` | 0 | yes | — |
|
|
178
|
+
| `nonAuthoritativeAnswer` | 0 | no | — |
|
|
179
|
+
| `authoritativeNegative` (NODATA) | 0 | yes | zone SOA |
|
|
180
|
+
| `authoritativeNameError` (NXDOMAIN) | 3 | yes | zone SOA |
|
|
181
|
+
| `refused` | 5 | no | — |
|
|
182
|
+
| `servfail` | 2 | no | — |
|
|
183
|
+
|
|
184
|
+
NXDOMAIN is only emitted when the name provably does not exist: no handler
|
|
185
|
+
pattern matches it for any type, it is not the apex of a configured zone, no
|
|
186
|
+
configured zone sits below it, and no handler pattern is anchored strictly
|
|
187
|
+
below it (literal `.name` suffix, or a wildcard that matches a probe label —
|
|
188
|
+
mid-label wildcards such as `x.*.example.com` are not fully inverted by this
|
|
189
|
+
check and can still leave an ancestor at NXDOMAIN). The apex always exists
|
|
190
|
+
(it answers its own SOA), and a name with covered descendants is an empty
|
|
191
|
+
non-terminal — both get NODATA, because NXDOMAIN denies the entire subtree
|
|
192
|
+
(RFC 8020) and a wrong one lets aggressive negative caching darken the zone.
|
|
193
|
+
A catch-all pattern such as `'*'` therefore suppresses NXDOMAIN entirely —
|
|
194
|
+
that is deliberate.
|
|
195
|
+
|
|
196
|
+
The zone apex answers its own SOA even with no registered SOA handler; an
|
|
197
|
+
authoritative server that cannot answer SOA for its own zone breaks negative
|
|
198
|
+
caching and resolver heuristics.
|
|
199
|
+
|
|
200
|
+
One deliberate exception to the table: RFC 6761 localhost handling (when
|
|
201
|
+
`enableLocalhostHandling` is on) answers `localhost` A/AAAA with `aa` set even
|
|
202
|
+
though `localhost` is outside every configured zone — special-use names are
|
|
203
|
+
authoritative by definition, not by delegation.
|
|
204
|
+
|
|
205
|
+
## Handler Lifecycle
|
|
206
|
+
|
|
207
|
+
- `registerHandler` returns an `IDnsHandlerRegistration` with a unique `id` and
|
|
208
|
+
an `unregister()` that removes **only** that registration.
|
|
209
|
+
- `unregisterHandlerById(id)` and `unregisterHandlersByOwner(owner)` are the
|
|
210
|
+
precise teardown primitives; `listHandlers()` exposes the current set for
|
|
211
|
+
leak checks.
|
|
212
|
+
- `unregisterHandler(pattern, types)` is retained and unchanged: it still
|
|
213
|
+
removes *every* handler matching that pattern/type pair. It cannot distinguish
|
|
214
|
+
two consumers sharing a key — prefer the handle.
|
|
215
|
+
- ACME authorization follows the same rule: `filterAuthorizedDomains` /
|
|
216
|
+
`retrieveSslCertificate` admit exactly the domains inside a configured
|
|
217
|
+
authoritative zone. Handler registrations play no part — a DNS-01 challenge
|
|
218
|
+
for an out-of-zone domain could never validate, because its challenge TXT
|
|
219
|
+
handler would be suppressed.
|
|
220
|
+
|
|
221
|
+
## Transport And Protocol Notes
|
|
222
|
+
|
|
223
|
+
- **EDNS0**: a request carrying an OPT record always gets an OPT back
|
|
224
|
+
(RFC 6891 6.1.1), advertising `DNS_ADVERTISED_UDP_PAYLOAD` (1232). Requests
|
|
225
|
+
without OPT get no OPT. Client-advertised payload sizes are clamped up to 512
|
|
226
|
+
so a hostile OPT cannot force pathological truncation.
|
|
227
|
+
- **Truncation**: UDP responses are truncated to the client's advertised payload
|
|
228
|
+
size with the TC bit set, so resolvers retry over TCP. Without this, oversized
|
|
229
|
+
datagrams were emitted and silently dropped by real networks. Truncation is
|
|
230
|
+
size arithmetic, never a re-encode per dropped record — the encoder does not
|
|
231
|
+
compress names, so record sizes are additive (`DnsPacket::encoded_len`).
|
|
232
|
+
`processRawDnsPacketAsync(packet, 'udp')` declares the same UDP bound for the
|
|
233
|
+
manual path (`handleUdpMessage` passes it automatically); without the hint no
|
|
234
|
+
size bound is applied, which is correct for TCP and DoH framing. Note the
|
|
235
|
+
manual-path deadlock under Known Limitations before relying on this.
|
|
236
|
+
- **RA** is cleared on every path — this server never recurses.
|
|
237
|
+
- **RDATA validation** fails closed: a handler returning an unencodable value
|
|
238
|
+
(for example a non-IPv4 string for an A record) produces SERVFAIL and an error
|
|
239
|
+
log, rather than a malformed record on the wire.
|
|
240
|
+
- **SOA serial** is fixed for the lifetime of the server process and passed to
|
|
241
|
+
Rust via `soaSerial`, so apex answers and negative authorities agree.
|
|
242
|
+
|
|
243
|
+
## Observability
|
|
244
|
+
|
|
245
|
+
- `query` events carry `responseKind`, `authorityZone` and `suppressedHandlers`.
|
|
246
|
+
- `refused` events fire for every refusal and every suppressed handler match,
|
|
247
|
+
carrying the handler `id`, `domainPattern` and `owner`. Wire this up: the
|
|
248
|
+
original incident was invisible for weeks because nothing surfaced it.
|
|
249
|
+
|
|
146
250
|
## Known Limitations
|
|
147
251
|
|
|
148
252
|
1. **Handler Deduplication**: If the same handler is registered multiple times, it will contribute duplicate records (this may be desired behavior for some use cases)
|
|
253
|
+
2. **NXDOMAIN under catch-all patterns**: see the authority model above.
|
|
254
|
+
3. **SOA serial does not track zone changes** — it is a process-lifetime
|
|
255
|
+
constant, not a change counter. Secondaries driven by AXFR/IXFR are not
|
|
256
|
+
supported.
|
|
257
|
+
4. **Manual-path (`processPacket`) queries deadlock on handler resolution**
|
|
258
|
+
— pre-existing, not introduced by the v8 hardening. The IPC select loop in
|
|
259
|
+
`rust/crates/rustdns/src/management.rs` awaits `handle_request` inline, so
|
|
260
|
+
while `handle_process_packet` waits for the TypeScript `dnsQuery` callback
|
|
261
|
+
the loop can neither emit the query event nor read its result; every
|
|
262
|
+
handler-backed query through `processRawDnsPacketAsync` /
|
|
263
|
+
`handleUdpMessage` times out to SERVFAIL after 10 s. The built-in UDP, TCP
|
|
264
|
+
and DoH listeners are unaffected (they resolve outside the IPC request
|
|
265
|
+
loop). The `transport: 'udp'` truncation contract on this path is wired
|
|
266
|
+
and unit-tested but cannot be observed end-to-end until the loop spawns
|
|
267
|
+
`processPacket` handling instead of awaiting it inline. No test exercises
|
|
268
|
+
`processRawDnsPacketAsync`; fixing this needs its own release.
|
|
149
269
|
## Static Rust Binaries
|
|
150
270
|
|
|
151
271
|
- The Rust binaries in `dist_rust/` are statically linked (static-pie) via `"static": true` in the `@git.zone/tsrust` block of `.smartconfig.json` (tsrust >= 1.4.1). They run on both glibc (Debian/Ubuntu) and musl (Alpine) systems.
|
package/readme.md
CHANGED
|
@@ -114,7 +114,7 @@ interface ISmartDnsConstructorOptions {
|
|
|
114
114
|
|
|
115
115
|
| Strategy | Behavior |
|
|
116
116
|
|---|---|
|
|
117
|
-
| `prefer-system` | 🏠 Try the
|
|
117
|
+
| `prefer-system` | 🏠 Try the Node DNS resolver first, then fall back to Rust DoH. |
|
|
118
118
|
| `system` | 🏠 Use only the Node.js system resolver. No Rust binary needed. |
|
|
119
119
|
| `doh` | 🌐 Use only DNS-over-HTTPS (RFC 8484 wire format via Cloudflare). Rust-powered. |
|
|
120
120
|
| `udp` | ⚡ Use only raw UDP queries to upstream resolver (Cloudflare 1.1.1.1). Rust-powered. |
|
|
@@ -174,6 +174,11 @@ perform their preferred attempt first and, when enabled, at most one DoH fallbac
|
|
|
174
174
|
a missing result or an error. The fallback result is authoritative and `attempts` retains both outcomes.
|
|
175
175
|
Set `allowDohFallback: false` to prohibit that second attempt.
|
|
176
176
|
|
|
177
|
+
When `timeoutMs` is set, every strict `system` attempt owns an isolated Node resolver. SmartDNS
|
|
178
|
+
returns `DNS_QUERY_TIMEOUT` at the deadline and cancels the underlying native query, so timed-out
|
|
179
|
+
lookups do not continue accumulating in the background. Isolated resolvers inherit servers selected
|
|
180
|
+
through `makeNodeProcessUseDnsProvider()`.
|
|
181
|
+
|
|
177
182
|
The strict `system` path uses Node's explicit DNS resolvers (`resolve4`, `resolve6`, `resolveTxt`,
|
|
178
183
|
`resolveMx`, `resolveNs`, `resolveCname`, `resolvePtr`, `resolveSrv`, and `resolveSoa`). This makes
|
|
179
184
|
DNS response semantics observable and intentionally differs from the legacy A/AAAA path.
|
|
@@ -263,6 +268,7 @@ interface IDnsServerOptions {
|
|
|
263
268
|
httpsKey: string; // PEM private key (path or content)
|
|
264
269
|
httpsCert: string; // PEM certificate (path or content)
|
|
265
270
|
dnssecZone: string; // Zone for DNSSEC signing
|
|
271
|
+
authoritativeZones?: string[]; // Zones this server is authoritative for (default: [dnssecZone])
|
|
266
272
|
primaryNameserver?: string; // SOA mname field (default: 'ns1.{dnssecZone}')
|
|
267
273
|
udpBindInterface?: string; // IP to bind UDP (default: '0.0.0.0')
|
|
268
274
|
tcpBindInterface?: string; // IP to bind TCP (defaults to udpBindInterface)
|
|
@@ -312,6 +318,8 @@ await server.start();
|
|
|
312
318
|
|
|
313
319
|
Handlers use **glob patterns** (via `minimatch`) to match incoming query names. Multiple handlers can contribute records to the same response.
|
|
314
320
|
|
|
321
|
+
Registering a handler does **not** make the server authoritative for the matched names. Authority comes solely from `authoritativeZones` (falling back to `dnssecZone`): list every zone you serve there, or queries for it are withheld. A default-authority handler whose match falls outside every configured zone is suppressed and reported on the `refused` event; register with `{ authority: 'non-authoritative' }` to deliberately serve out-of-zone names without the `aa` bit or DNSSEC signatures.
|
|
322
|
+
|
|
315
323
|
```typescript
|
|
316
324
|
// Exact domain
|
|
317
325
|
server.registerHandler('example.com', ['A'], handler);
|
|
@@ -331,14 +339,15 @@ server.registerHandler('db-*.internal.example.com', ['A'], (question) => {
|
|
|
331
339
|
};
|
|
332
340
|
});
|
|
333
341
|
|
|
334
|
-
// Catch-all
|
|
342
|
+
// Catch-all — '*' is anchored in no configured zone, so it must be
|
|
343
|
+
// registered non-authoritative or its out-of-zone answers are suppressed
|
|
335
344
|
server.registerHandler('*', ['A'], (question) => ({
|
|
336
345
|
name: question.name,
|
|
337
346
|
type: 'A',
|
|
338
347
|
class: 'IN',
|
|
339
348
|
ttl: 300,
|
|
340
349
|
data: '127.0.0.1',
|
|
341
|
-
}));
|
|
350
|
+
}), { authority: 'non-authoritative' });
|
|
342
351
|
|
|
343
352
|
// Multiple record types
|
|
344
353
|
server.registerHandler('example.com', ['MX'], (question) => ({
|
|
@@ -349,11 +358,21 @@ server.registerHandler('example.com', ['MX'], (question) => ({
|
|
|
349
358
|
data: { preference: 10, exchange: 'mail.example.com' },
|
|
350
359
|
}));
|
|
351
360
|
|
|
352
|
-
// Unregister a handler
|
|
361
|
+
// Unregister a handler: registerHandler returns a handle identifying
|
|
362
|
+
// exactly that registration
|
|
363
|
+
const registration = server.registerHandler('example.com', ['A'], handler);
|
|
364
|
+
registration.unregister();
|
|
365
|
+
|
|
366
|
+
// Precise teardown also works by id or owner label, and listHandlers()
|
|
367
|
+
// exposes the current set
|
|
368
|
+
server.registerHandler('example.com', ['TXT'], handler, { owner: 'acme-module' });
|
|
369
|
+
server.unregisterHandlersByOwner('acme-module');
|
|
370
|
+
|
|
371
|
+
// The coarse form removes EVERY handler on the pattern/type pair
|
|
353
372
|
server.unregisterHandler('example.com', ['A']);
|
|
354
373
|
```
|
|
355
374
|
|
|
356
|
-
|
|
375
|
+
Negative answers are authoritative only inside configured zones: a name inside an authoritative zone that provably does not exist yields **NXDOMAIN** with the zone SOA in the AUTHORITY section; a name that exists — a covered name queried for a type no handler serves, the zone apex, or an empty non-terminal with covered names beneath it — yields **NODATA** with the SOA in AUTHORITY; names outside every configured zone are answered **REFUSED**.
|
|
357
376
|
|
|
358
377
|
### DNSSEC ✅
|
|
359
378
|
|
|
@@ -389,7 +408,7 @@ Supported algorithms: **ECDSAP256SHA256** (13), **ED25519** (15), **RSASHA256**
|
|
|
389
408
|
|
|
390
409
|
### SOA Records
|
|
391
410
|
|
|
392
|
-
The server
|
|
411
|
+
The server carries the zone SOA in the AUTHORITY section of negative answers (NXDOMAIN and NODATA), and the zone apex answers its own SOA even without a registered SOA handler. Customize the primary nameserver:
|
|
393
412
|
|
|
394
413
|
```typescript
|
|
395
414
|
const server = new DnsServer({
|
|
@@ -401,13 +420,13 @@ const server = new DnsServer({
|
|
|
401
420
|
// Generated SOA includes:
|
|
402
421
|
// mname: ns1.example.com
|
|
403
422
|
// rname: hostmaster.example.com
|
|
404
|
-
// serial:
|
|
423
|
+
// serial: captured once at server construction, stable for the process lifetime
|
|
405
424
|
// refresh: 3600, retry: 600, expire: 604800, minimum: 86400
|
|
406
425
|
```
|
|
407
426
|
|
|
408
427
|
### Let's Encrypt Integration 🔒
|
|
409
428
|
|
|
410
|
-
Built-in ACME DNS-01 challenge support for automatic SSL certificates:
|
|
429
|
+
Built-in ACME DNS-01 challenge support for automatic SSL certificates. Challenge TXT handlers are registered at default authority, so DNS-01 validation only completes for domains inside `authoritativeZones` (or `dnssecZone`):
|
|
411
430
|
|
|
412
431
|
```typescript
|
|
413
432
|
const server = new DnsServer({
|
|
@@ -504,6 +523,10 @@ Process individual DNS packets directly:
|
|
|
504
523
|
```typescript
|
|
505
524
|
// Asynchronous (via Rust bridge — includes DNSSEC signing)
|
|
506
525
|
const response = await server.processRawDnsPacketAsync(packetBuffer);
|
|
526
|
+
|
|
527
|
+
// When the response goes out as a UDP datagram, pass 'udp' so it is
|
|
528
|
+
// truncated (TC bit) to the client's advertised EDNS0 payload size
|
|
529
|
+
const udpResponse = await server.processRawDnsPacketAsync(packetBuffer, 'udp');
|
|
507
530
|
```
|
|
508
531
|
|
|
509
532
|
Manual TCP callers must handle DNS-over-TCP's two-byte length prefix themselves and pass only the raw DNS packet payload to `processRawDnsPacketAsync()`.
|
package/ts/00_commitinfo_data.ts
CHANGED
|
@@ -3,6 +3,6 @@
|
|
|
3
3
|
*/
|
|
4
4
|
export const commitinfo = {
|
|
5
5
|
name: '@push.rocks/smartdns',
|
|
6
|
-
version: '
|
|
6
|
+
version: '8.0.1',
|
|
7
7
|
description: 'A robust TypeScript library providing advanced DNS management and resolution capabilities including support for DNSSEC, custom DNS servers, and integration with various DNS providers.'
|
|
8
8
|
}
|