Compare commits

..

4 Commits

Author SHA1 Message Date
jkunz 23df951023 v4.7.1
Default (tags) / security (push) Failing after 1s
Default (tags) / test (push) Failing after 1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-04-30 08:51:24 +00:00
jkunz 26d0fed2d4 fix(build): enforce stricter TypeScript checks and update build dependencies 2026-04-30 08:51:24 +00:00
jkunz d526a7d8dd v4.7.0
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-04-26 20:45:47 +00:00
jkunz 3e86e99d4f feat(ipintelligence): add canonical RDAP CIDR coverage and derive CIDRs from IPv4 start/end ranges 2026-04-26 20:45:47 +00:00
11 changed files with 844 additions and 318 deletions
+14
View File
@@ -1,5 +1,19 @@
# Changelog
## 2026-04-30 - 4.7.1 - fix(build)
enforce stricter TypeScript checks and update build dependencies
- enable noImplicitAny in tsconfig and remove the build flag override to enforce strict typing during builds
- add explicit null guards in tests for free port and default gateway results
- update development and runtime dependencies and refresh pnpm metadata
## 2026-04-26 - 4.7.0 - feat(ipintelligence)
add canonical RDAP CIDR coverage and derive CIDRs from IPv4 start/end ranges
- Expose a new networkCidrs field alongside networkRange in IP intelligence results
- Convert RDAP IPv4 start/end ranges into canonical CIDR blocks, preserving legacy range formatting when multiple prefixes are required
- Add tests covering single-prefix and multi-prefix RDAP range parsing
## 2026-04-13 - 4.6.0 - feat(domain-intelligence)
add domain intelligence lookups with RDAP and DNS enrichment
+8 -10
View File
@@ -1,6 +1,6 @@
{
"name": "@push.rocks/smartnetwork",
"version": "4.6.0",
"version": "4.7.1",
"private": false,
"description": "A toolkit for network diagnostics including speed tests, port availability checks, and more.",
"exports": {
@@ -11,20 +11,20 @@
"license": "MIT",
"scripts": {
"test": "(tstest test/ --verbose)",
"build": "(tsbuild tsfolders --allowimplicitany) && (tsrust)",
"build": "(tsbuild tsfolders) && (tsrust)",
"buildDocs": "tsdoc"
},
"devDependencies": {
"@git.zone/tsbuild": "^4.4.0",
"@git.zone/tsrun": "^2.0.2",
"@git.zone/tsrust": "^1.3.2",
"@git.zone/tstest": "^3.6.1",
"@types/node": "^25.5.0"
"@git.zone/tstest": "^3.6.3",
"@types/node": "^25.6.0"
},
"dependencies": {
"@push.rocks/smartdns": "^7.9.0",
"@push.rocks/smartrust": "^1.3.2",
"maxmind": "^5.0.5"
"@push.rocks/smartrust": "^1.4.0",
"maxmind": "^5.0.6"
},
"files": [
"ts/**/*",
@@ -36,6 +36,7 @@
"assets/**/*",
"cli.js",
".smartconfig.json",
"license",
"readme.md"
],
"browserslist": [
@@ -58,11 +59,8 @@
"type": "git",
"url": "https://code.foss.global/push.rocks/smartnetwork.git"
},
"packageManager": "pnpm@10.7.0+sha512.6b865ad4b62a1d9842b61d674a393903b871d9244954f652b8842c2b553c72176b278f64c463e52d40fff8aba385c235c8c9ecf5cc7de4fd78b8bb6d49633ab6",
"packageManager": "pnpm@10.28.2",
"bugs": {
"url": "https://code.foss.global/push.rocks/smartnetwork/issues"
},
"pnpm": {
"overrides": {}
}
}
+679 -293
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -37,7 +37,7 @@ Key features:
- `ensureBridge()` auto-starts on first use
### Build Pipeline
- `pnpm build` = `tsbuild tsfolders --allowimplicitany && tsrust`
- `pnpm build` = `tsbuild tsfolders && tsrust`
- Targets configured in `.smartconfig.json` under `@git.zone/tsrust`
## Key Components
+5 -3
View File
@@ -9,7 +9,7 @@ For reporting bugs, issues, or security vulnerabilities, please visit [community
## 🚀 Install
```bash
pnpm install @push.rocks/smartnetwork --save
pnpm add @push.rocks/smartnetwork
```
## 🎯 Overview
@@ -77,6 +77,7 @@ console.log(intel);
// registrantOrg: 'Google LLC',
// registrantCountry: 'United States',
// networkRange: '8.8.8.0/24',
// networkCidrs: ['8.8.8.0/24'],
// abuseContact: null,
// country: null,
// countryCode: 'US',
@@ -109,7 +110,8 @@ interface IIpIntelligenceResult {
// Registration (RDAP)
registrantOrg: string | null;
registrantCountry: string | null;
networkRange: string | null; // CIDR or range
networkRange: string | null; // primary CIDR, or legacy start-end range when multiple CIDRs are needed
networkCidrs: string[] | null; // canonical CIDR coverage for the RDAP network when available
abuseContact: string | null; // abuse email from RDAP
// Geolocation (MaxMind GeoLite2)
@@ -489,7 +491,7 @@ const monitor = async () => {
## License and Legal Information
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [LICENSE](./LICENSE) file.
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [license](./license) file.
**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
+3
View File
@@ -153,6 +153,9 @@ tap.test('isLocalPortUnused should detect used local port', async () => {
// findFreePort tests
tap.test('findFreePort should find an available port in range', async () => {
const freePort = await sharedSn.findFreePort(49152, 49200);
if (freePort === null) {
throw new Error('Expected to find a free port in test range');
}
expect(freePort).toBeGreaterThanOrEqual(49152);
expect(freePort).toBeLessThanOrEqual(49200);
+31
View File
@@ -41,6 +41,37 @@ tap.test('should get IP intelligence for 1.1.1.1 (Cloudflare)', async () => {
// Note: 1.1.1.1 is anycast — city-level geo may be null in GeoLite2
});
tap.test('should derive a single CIDR from RDAP start/end ranges', async () => {
const intelligence = new smartnetwork.IpIntelligence();
const result = (intelligence as any).parseRdapNetworkInfo({
startAddress: '203.0.113.0',
endAddress: '203.0.113.255',
});
expect(result).toEqual({
networkRange: '203.0.113.0/24',
networkCidrs: ['203.0.113.0/24'],
});
});
tap.test('should expose CIDRs for RDAP ranges that need multiple prefixes', async () => {
const intelligence = new smartnetwork.IpIntelligence();
const result = (intelligence as any).parseRdapNetworkInfo({
startAddress: '203.0.113.5',
endAddress: '203.0.113.10',
});
expect(result).toEqual({
networkRange: '203.0.113.5 - 203.0.113.10',
networkCidrs: [
'203.0.113.5/32',
'203.0.113.6/31',
'203.0.113.8/31',
'203.0.113.10/32',
],
});
});
tap.test('should get IP intelligence for own public IP', async () => {
const ips = await testSmartNetwork.getPublicIps();
if (ips.v4) {
+3
View File
@@ -47,6 +47,9 @@ tap.test('should get the default gateway', async () => {
console.log(defaultGw);
// verify default gateway contains ipv4 and ipv6 info
expect(defaultGw).toBeDefined();
if (!defaultGw) {
throw new Error('Expected a default gateway');
}
expect(defaultGw.ipv4).toBeDefined();
expect(defaultGw.ipv6).toBeDefined();
});
+1 -1
View File
@@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@push.rocks/smartnetwork',
version: '4.6.0',
version: '4.7.1',
description: 'A toolkit for network diagnostics including speed tests, port availability checks, and more.'
}
+97 -9
View File
@@ -30,6 +30,7 @@ export interface IIpIntelligenceResult {
registrantOrg: string | null;
registrantCountry: string | null;
networkRange: string | null;
networkCidrs: string[] | null;
abuseContact: string | null;
// Geolocation (MaxMind GeoLite2 City)
@@ -79,6 +80,11 @@ interface IBootstrapEntry {
baseUrl: string;
}
interface IRdapNetworkInfo {
networkRange: string | null;
networkCidrs: string[] | null;
}
/**
* IpIntelligence provides IP address intelligence by combining three data sources:
* - RDAP (direct to RIRs) for registration/org data
@@ -120,6 +126,7 @@ export class IpIntelligence {
registrantOrg: null,
registrantCountry: null,
networkRange: null,
networkCidrs: null,
abuseContact: null,
country: null,
countryCode: null,
@@ -143,6 +150,7 @@ export class IpIntelligence {
result.registrantOrg = rdap.registrantOrg;
result.registrantCountry = rdap.registrantCountry;
result.networkRange = rdap.networkRange;
result.networkCidrs = rdap.networkCidrs;
result.abuseContact = rdap.abuseContact;
}
@@ -257,6 +265,7 @@ export class IpIntelligence {
registrantOrg: string | null;
registrantCountry: string | null;
networkRange: string | null;
networkCidrs: string[] | null;
abuseContact: string | null;
} | null> {
await this.ensureBootstrap();
@@ -280,14 +289,7 @@ export class IpIntelligence {
let registrantCountry: string | null = data.country || null;
let abuseContact: string | null = null;
// Parse network range
let networkRange: string | null = null;
if (data.cidr0_cidrs && data.cidr0_cidrs.length > 0) {
const cidr = data.cidr0_cidrs[0];
networkRange = `${cidr.v4prefix || cidr.v6prefix}/${cidr.length}`;
} else if (data.startAddress && data.endAddress) {
networkRange = `${data.startAddress} - ${data.endAddress}`;
}
const { networkRange, networkCidrs } = this.parseRdapNetworkInfo(data);
// Parse entities
if (data.entities && Array.isArray(data.entities)) {
@@ -320,7 +322,7 @@ export class IpIntelligence {
}
}
return { registrantOrg, registrantCountry, networkRange, abuseContact };
return { registrantOrg, registrantCountry, networkRange, networkCidrs, abuseContact };
} catch (err: any) {
this.logger.debug?.(`RDAP query failed for ${ip}: ${err.message}`);
return null;
@@ -329,6 +331,40 @@ export class IpIntelligence {
}
}
private parseRdapNetworkInfo(data: any): IRdapNetworkInfo {
const cidrs = this.extractRdapCidrs(data);
if (cidrs.length > 0) {
return {
networkRange: cidrs[0],
networkCidrs: cidrs,
};
}
if (typeof data.startAddress === 'string' && typeof data.endAddress === 'string') {
const rangeCidrs = this.ipv4RangeToCidrs(data.startAddress, data.endAddress);
return {
networkRange: rangeCidrs.length === 1
? rangeCidrs[0]
: `${data.startAddress} - ${data.endAddress}`,
networkCidrs: rangeCidrs.length > 0 ? rangeCidrs : null,
};
}
return { networkRange: null, networkCidrs: null };
}
private extractRdapCidrs(data: any): string[] {
if (!Array.isArray(data.cidr0_cidrs)) return [];
return data.cidr0_cidrs
.map((cidr: any) => {
const prefix = cidr?.v4prefix || cidr?.v6prefix;
const length = Number(cidr?.length);
if (typeof prefix !== 'string' || !Number.isInteger(length)) return null;
return `${prefix}/${length}`;
})
.filter(Boolean) as string[];
}
/**
* Extract the 'fn' (formatted name) from an entity's vcardArray
*/
@@ -573,4 +609,56 @@ export class IpIntelligence {
parseInt(parts[3], 10)) >>> 0
);
}
private ipv4RangeToCidrs(startIp: string, endIp: string): string[] {
const start = this.ipv4ToBigInt(startIp);
const end = this.ipv4ToBigInt(endIp);
if (start === undefined || end === undefined || start > end) return [];
const cidrs: string[] = [];
let current = start;
while (current <= end) {
let maxBlockSize = current === 0n ? 1n << 32n : current & -current;
const remaining = end - current + 1n;
while (maxBlockSize > remaining) {
maxBlockSize = maxBlockSize / 2n;
}
const prefixLength = 32 - this.powerOfTwoExponent(maxBlockSize);
cidrs.push(`${this.numberToIpv4(current)}/${prefixLength}`);
current += maxBlockSize;
}
return cidrs;
}
private ipv4ToBigInt(ip: string): bigint | undefined {
const parts = ip.trim().split('.');
if (parts.length !== 4) return undefined;
let result = 0n;
for (const part of parts) {
if (!/^\d+$/.test(part)) return undefined;
const number = Number(part);
if (!Number.isInteger(number) || number < 0 || number > 255) return undefined;
result = (result * 256n) + BigInt(number);
}
return result;
}
private numberToIpv4(value: bigint): string {
return [
Number((value >> 24n) & 255n),
Number((value >> 16n) & 255n),
Number((value >> 8n) & 255n),
Number(value & 255n),
].join('.');
}
private powerOfTwoExponent(value: bigint): number {
let exponent = 0;
let remaining = value;
while (remaining > 1n) {
remaining >>= 1n;
exponent++;
}
return exponent;
}
}
+2 -1
View File
@@ -7,6 +7,7 @@
"moduleResolution": "NodeNext",
"esModuleInterop": true,
"verbatimModuleSyntax": true,
"noImplicitAny": true,
"types": [
"node"
]
@@ -14,4 +15,4 @@
"exclude": [
"dist_*/**/*.d.ts"
]
}
}