@push.rocks/smartnetwork 4.5.2 → 4.7.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.
@@ -30,6 +30,7 @@ export interface IIpIntelligenceResult {
30
30
  registrantOrg: string | null;
31
31
  registrantCountry: string | null;
32
32
  networkRange: string | null;
33
+ networkCidrs: string[] | null;
33
34
  abuseContact: string | null;
34
35
 
35
36
  // Geolocation (MaxMind GeoLite2 City)
@@ -50,6 +51,13 @@ export interface IIpIntelligenceOptions {
50
51
  dbMaxAge?: number;
51
52
  /** Timeout (ms) for RDAP/DNS/CDN requests. Default: 5000 */
52
53
  timeout?: number;
54
+ /**
55
+ * Optional injected smartdns client. When provided, IpIntelligence will
56
+ * not create or destroy its own client (the owner — typically SmartNetwork —
57
+ * manages lifecycle). When omitted, a short-lived client is created per
58
+ * Team Cymru lookup and destroyed in finally.
59
+ */
60
+ dnsClient?: InstanceType<typeof plugins.smartdns.dnsClientMod.Smartdns>;
53
61
  }
54
62
 
55
63
  // CDN URLs for GeoLite2 MMDB files (served via jsDelivr from npm packages)
@@ -72,6 +80,11 @@ interface IBootstrapEntry {
72
80
  baseUrl: string;
73
81
  }
74
82
 
83
+ interface IRdapNetworkInfo {
84
+ networkRange: string | null;
85
+ networkCidrs: string[] | null;
86
+ }
87
+
75
88
  /**
76
89
  * IpIntelligence provides IP address intelligence by combining three data sources:
77
90
  * - RDAP (direct to RIRs) for registration/org data
@@ -93,9 +106,13 @@ export class IpIntelligence {
93
106
  private bootstrapEntries: IBootstrapEntry[] | null = null;
94
107
  private bootstrapPromise: Promise<void> | null = null;
95
108
 
109
+ // Optional injected smartdns client (shared by SmartNetwork)
110
+ private readonly sharedDnsClient: InstanceType<typeof plugins.smartdns.dnsClientMod.Smartdns> | null;
111
+
96
112
  constructor(options?: IIpIntelligenceOptions) {
97
113
  this.dbMaxAge = options?.dbMaxAge ?? DEFAULT_DB_MAX_AGE;
98
114
  this.timeout = options?.timeout ?? DEFAULT_TIMEOUT;
115
+ this.sharedDnsClient = options?.dnsClient ?? null;
99
116
  }
100
117
 
101
118
  /**
@@ -109,6 +126,7 @@ export class IpIntelligence {
109
126
  registrantOrg: null,
110
127
  registrantCountry: null,
111
128
  networkRange: null,
129
+ networkCidrs: null,
112
130
  abuseContact: null,
113
131
  country: null,
114
132
  countryCode: null,
@@ -132,6 +150,7 @@ export class IpIntelligence {
132
150
  result.registrantOrg = rdap.registrantOrg;
133
151
  result.registrantCountry = rdap.registrantCountry;
134
152
  result.networkRange = rdap.networkRange;
153
+ result.networkCidrs = rdap.networkCidrs;
135
154
  result.abuseContact = rdap.abuseContact;
136
155
  }
137
156
 
@@ -246,6 +265,7 @@ export class IpIntelligence {
246
265
  registrantOrg: string | null;
247
266
  registrantCountry: string | null;
248
267
  networkRange: string | null;
268
+ networkCidrs: string[] | null;
249
269
  abuseContact: string | null;
250
270
  } | null> {
251
271
  await this.ensureBootstrap();
@@ -269,14 +289,7 @@ export class IpIntelligence {
269
289
  let registrantCountry: string | null = data.country || null;
270
290
  let abuseContact: string | null = null;
271
291
 
272
- // Parse network range
273
- let networkRange: string | null = null;
274
- if (data.cidr0_cidrs && data.cidr0_cidrs.length > 0) {
275
- const cidr = data.cidr0_cidrs[0];
276
- networkRange = `${cidr.v4prefix || cidr.v6prefix}/${cidr.length}`;
277
- } else if (data.startAddress && data.endAddress) {
278
- networkRange = `${data.startAddress} - ${data.endAddress}`;
279
- }
292
+ const { networkRange, networkCidrs } = this.parseRdapNetworkInfo(data);
280
293
 
281
294
  // Parse entities
282
295
  if (data.entities && Array.isArray(data.entities)) {
@@ -309,7 +322,7 @@ export class IpIntelligence {
309
322
  }
310
323
  }
311
324
 
312
- return { registrantOrg, registrantCountry, networkRange, abuseContact };
325
+ return { registrantOrg, registrantCountry, networkRange, networkCidrs, abuseContact };
313
326
  } catch (err: any) {
314
327
  this.logger.debug?.(`RDAP query failed for ${ip}: ${err.message}`);
315
328
  return null;
@@ -318,6 +331,40 @@ export class IpIntelligence {
318
331
  }
319
332
  }
320
333
 
334
+ private parseRdapNetworkInfo(data: any): IRdapNetworkInfo {
335
+ const cidrs = this.extractRdapCidrs(data);
336
+ if (cidrs.length > 0) {
337
+ return {
338
+ networkRange: cidrs[0],
339
+ networkCidrs: cidrs,
340
+ };
341
+ }
342
+
343
+ if (typeof data.startAddress === 'string' && typeof data.endAddress === 'string') {
344
+ const rangeCidrs = this.ipv4RangeToCidrs(data.startAddress, data.endAddress);
345
+ return {
346
+ networkRange: rangeCidrs.length === 1
347
+ ? rangeCidrs[0]
348
+ : `${data.startAddress} - ${data.endAddress}`,
349
+ networkCidrs: rangeCidrs.length > 0 ? rangeCidrs : null,
350
+ };
351
+ }
352
+
353
+ return { networkRange: null, networkCidrs: null };
354
+ }
355
+
356
+ private extractRdapCidrs(data: any): string[] {
357
+ if (!Array.isArray(data.cidr0_cidrs)) return [];
358
+ return data.cidr0_cidrs
359
+ .map((cidr: any) => {
360
+ const prefix = cidr?.v4prefix || cidr?.v6prefix;
361
+ const length = Number(cidr?.length);
362
+ if (typeof prefix !== 'string' || !Number.isInteger(length)) return null;
363
+ return `${prefix}/${length}`;
364
+ })
365
+ .filter(Boolean) as string[];
366
+ }
367
+
321
368
  /**
322
369
  * Extract the 'fn' (formatted name) from an entity's vcardArray
323
370
  */
@@ -385,16 +432,19 @@ export class IpIntelligence {
385
432
  * Response: "ASN | prefix | CC | rir | date"
386
433
  */
387
434
  private async queryTeamCymru(ip: string): Promise<{ asn: number; prefix: string; country: string } | null> {
388
- let dnsClient: InstanceType<typeof plugins.smartdns.dnsClientMod.Smartdns> | null = null;
435
+ const external = this.sharedDnsClient !== null;
436
+ let dnsClient = this.sharedDnsClient;
389
437
  try {
390
438
  const reversed = ip.split('.').reverse().join('.');
391
439
  const queryName = `${reversed}.origin.asn.cymru.com`;
392
440
 
393
- dnsClient = new plugins.smartdns.dnsClientMod.Smartdns({
394
- strategy: 'prefer-system',
395
- allowDohFallback: true,
396
- timeoutMs: this.timeout,
397
- });
441
+ if (!dnsClient) {
442
+ dnsClient = new plugins.smartdns.dnsClientMod.Smartdns({
443
+ strategy: 'prefer-system',
444
+ allowDohFallback: true,
445
+ timeoutMs: this.timeout,
446
+ });
447
+ }
398
448
 
399
449
  const records = await dnsClient.getRecordsTxt(queryName);
400
450
  if (!records || records.length === 0) return null;
@@ -418,7 +468,8 @@ export class IpIntelligence {
418
468
  this.logger.debug?.(`Team Cymru DNS query failed for ${ip}: ${err.message}`);
419
469
  return null;
420
470
  } finally {
421
- if (dnsClient) {
471
+ // Only destroy clients we created ourselves; leave injected ones alone.
472
+ if (!external && dnsClient) {
422
473
  dnsClient.destroy();
423
474
  }
424
475
  }
@@ -558,4 +609,56 @@ export class IpIntelligence {
558
609
  parseInt(parts[3], 10)) >>> 0
559
610
  );
560
611
  }
612
+
613
+ private ipv4RangeToCidrs(startIp: string, endIp: string): string[] {
614
+ const start = this.ipv4ToBigInt(startIp);
615
+ const end = this.ipv4ToBigInt(endIp);
616
+ if (start === undefined || end === undefined || start > end) return [];
617
+
618
+ const cidrs: string[] = [];
619
+ let current = start;
620
+ while (current <= end) {
621
+ let maxBlockSize = current === 0n ? 1n << 32n : current & -current;
622
+ const remaining = end - current + 1n;
623
+ while (maxBlockSize > remaining) {
624
+ maxBlockSize = maxBlockSize / 2n;
625
+ }
626
+ const prefixLength = 32 - this.powerOfTwoExponent(maxBlockSize);
627
+ cidrs.push(`${this.numberToIpv4(current)}/${prefixLength}`);
628
+ current += maxBlockSize;
629
+ }
630
+ return cidrs;
631
+ }
632
+
633
+ private ipv4ToBigInt(ip: string): bigint | undefined {
634
+ const parts = ip.trim().split('.');
635
+ if (parts.length !== 4) return undefined;
636
+ let result = 0n;
637
+ for (const part of parts) {
638
+ if (!/^\d+$/.test(part)) return undefined;
639
+ const number = Number(part);
640
+ if (!Number.isInteger(number) || number < 0 || number > 255) return undefined;
641
+ result = (result * 256n) + BigInt(number);
642
+ }
643
+ return result;
644
+ }
645
+
646
+ private numberToIpv4(value: bigint): string {
647
+ return [
648
+ Number((value >> 24n) & 255n),
649
+ Number((value >> 16n) & 255n),
650
+ Number((value >> 8n) & 255n),
651
+ Number(value & 255n),
652
+ ].join('.');
653
+ }
654
+
655
+ private powerOfTwoExponent(value: bigint): number {
656
+ let exponent = 0;
657
+ let remaining = value;
658
+ while (remaining > 1n) {
659
+ remaining >>= 1n;
660
+ exponent++;
661
+ }
662
+ return exponent;
663
+ }
561
664
  }
@@ -2,10 +2,14 @@ import * as plugins from './smartnetwork.plugins.js';
2
2
  import { CloudflareSpeed } from './smartnetwork.classes.cloudflarespeed.js';
3
3
  import { PublicIp } from './smartnetwork.classes.publicip.js';
4
4
  import { IpIntelligence, type IIpIntelligenceResult } from './smartnetwork.classes.ipintelligence.js';
5
+ import { DomainIntelligence, type IDomainIntelligenceResult } from './smartnetwork.classes.domainintelligence.js';
5
6
  import { getLogger } from './logging.js';
6
7
  import { NetworkError } from './errors.js';
7
8
  import { RustNetworkBridge } from './smartnetwork.classes.rustbridge.js';
8
9
 
10
+ /** Type alias for the shared Smartdns client instance */
11
+ type TSmartdnsClient = InstanceType<typeof plugins.smartdns.dnsClientMod.Smartdns>;
12
+
9
13
  /**
10
14
  * Configuration options for SmartNetwork
11
15
  */
@@ -54,6 +58,8 @@ export class SmartNetwork {
54
58
  private rustBridge: RustNetworkBridge;
55
59
  private bridgeStarted = false;
56
60
  private ipIntelligence: IpIntelligence | null = null;
61
+ private domainIntelligence: DomainIntelligence | null = null;
62
+ private dnsClient: TSmartdnsClient | null = null;
57
63
 
58
64
  constructor(options?: SmartNetworkOptions) {
59
65
  this.options = options || {};
@@ -73,13 +79,24 @@ export class SmartNetwork {
73
79
  }
74
80
 
75
81
  /**
76
- * Stop the Rust binary bridge.
82
+ * Stop the Rust binary bridge and tear down the shared Smartdns client.
83
+ * Call this before your Node process exits if you've used any DNS or
84
+ * Rust-backed features, otherwise the smartdns Rust backend may keep
85
+ * the event loop alive.
77
86
  */
78
87
  public async stop(): Promise<void> {
79
88
  if (this.bridgeStarted) {
80
89
  await this.rustBridge.stop();
81
90
  this.bridgeStarted = false;
82
91
  }
92
+ if (this.dnsClient) {
93
+ this.dnsClient.destroy();
94
+ this.dnsClient = null;
95
+ // Intelligence instances hold a stale reference to the destroyed
96
+ // client; drop them so the next call rebuilds with a fresh one.
97
+ this.ipIntelligence = null;
98
+ this.domainIntelligence = null;
99
+ }
83
100
  }
84
101
 
85
102
  /**
@@ -91,6 +108,23 @@ export class SmartNetwork {
91
108
  }
92
109
  }
93
110
 
111
+ /**
112
+ * Lazily create the shared Smartdns client. The Rust backend inside
113
+ * Smartdns is only spawned on first query that requires it (NS/MX/SOA
114
+ * with prefer-system strategy, or any query with doh/udp strategy).
115
+ * The client is destroyed by stop().
116
+ */
117
+ private ensureDnsClient(): TSmartdnsClient {
118
+ if (!this.dnsClient) {
119
+ this.dnsClient = new plugins.smartdns.dnsClientMod.Smartdns({
120
+ strategy: 'prefer-system',
121
+ allowDohFallback: true,
122
+ timeoutMs: 5000,
123
+ });
124
+ }
125
+ return this.dnsClient;
126
+ }
127
+
94
128
  /**
95
129
  * Get network speed via Cloudflare speed test (pure TS, no Rust needed).
96
130
  */
@@ -310,16 +344,15 @@ export class SmartNetwork {
310
344
  }
311
345
 
312
346
  /**
313
- * Resolve DNS records (A, AAAA, MX) — uses smartdns, no Rust needed.
347
+ * Resolve DNS records (A, AAAA, MX) via the shared smartdns client.
348
+ * The client is lifecycle-managed by start()/stop() — MX queries spawn
349
+ * the smartdns Rust bridge, which is torn down by stop().
314
350
  */
315
351
  public async resolveDns(
316
352
  host: string,
317
353
  ): Promise<{ A: string[]; AAAA: string[]; MX: { exchange: string; priority: number }[] }> {
318
354
  try {
319
- const dnsClient = new plugins.smartdns.dnsClientMod.Smartdns({
320
- strategy: 'prefer-system',
321
- allowDohFallback: true,
322
- });
355
+ const dnsClient = this.ensureDnsClient();
323
356
 
324
357
  const [aRecords, aaaaRecords, mxRecords] = await Promise.all([
325
358
  dnsClient.getRecordsA(host).catch((): any[] => []),
@@ -404,7 +437,7 @@ export class SmartNetwork {
404
437
  */
405
438
  public async getIpIntelligence(ip: string): Promise<IIpIntelligenceResult> {
406
439
  if (!this.ipIntelligence) {
407
- this.ipIntelligence = new IpIntelligence();
440
+ this.ipIntelligence = new IpIntelligence({ dnsClient: this.ensureDnsClient() });
408
441
  }
409
442
  const fetcher = () => this.ipIntelligence!.getIntelligence(ip);
410
443
  if (this.options.cacheTtl && this.options.cacheTtl > 0) {
@@ -413,6 +446,22 @@ export class SmartNetwork {
413
446
  return fetcher();
414
447
  }
415
448
 
449
+ /**
450
+ * Get domain intelligence: registrar, registrant, nameservers, registration
451
+ * events, status flags, DNSSEC, and abuse contact via RDAP. Pure TS, no
452
+ * Rust needed.
453
+ */
454
+ public async getDomainIntelligence(domain: string): Promise<IDomainIntelligenceResult> {
455
+ if (!this.domainIntelligence) {
456
+ this.domainIntelligence = new DomainIntelligence({ dnsClient: this.ensureDnsClient() });
457
+ }
458
+ const fetcher = () => this.domainIntelligence!.getIntelligence(domain);
459
+ if (this.options.cacheTtl && this.options.cacheTtl > 0) {
460
+ return this.getCached(`domainIntelligence:${domain}`, fetcher);
461
+ }
462
+ return fetcher();
463
+ }
464
+
416
465
  /**
417
466
  * Internal caching helper
418
467
  */