domsniper 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +40 -0
- package/LICENSE +21 -0
- package/README.md +246 -0
- package/package.json +72 -0
- package/src/app.tsx +2062 -0
- package/src/completions.ts +65 -0
- package/src/core/db.ts +1313 -0
- package/src/core/features/asn-lookup.ts +91 -0
- package/src/core/features/backlinks.ts +83 -0
- package/src/core/features/blacklist-check.ts +67 -0
- package/src/core/features/cert-transparency.ts +87 -0
- package/src/core/features/config.ts +81 -0
- package/src/core/features/cors-check.ts +90 -0
- package/src/core/features/dns-details.ts +27 -0
- package/src/core/features/domain-age.ts +33 -0
- package/src/core/features/domain-suggest.ts +87 -0
- package/src/core/features/drop-catch.ts +159 -0
- package/src/core/features/email-security.ts +112 -0
- package/src/core/features/expiring-feed.ts +160 -0
- package/src/core/features/export.ts +74 -0
- package/src/core/features/filter.ts +96 -0
- package/src/core/features/http-probe.ts +46 -0
- package/src/core/features/marketplace.ts +69 -0
- package/src/core/features/path-scanner.ts +123 -0
- package/src/core/features/port-scanner.ts +132 -0
- package/src/core/features/portfolio-bulk.ts +125 -0
- package/src/core/features/portfolio-monitor.ts +214 -0
- package/src/core/features/portfolio.ts +98 -0
- package/src/core/features/price-compare.ts +39 -0
- package/src/core/features/rdap.ts +128 -0
- package/src/core/features/reverse-ip.ts +73 -0
- package/src/core/features/s3-export.ts +99 -0
- package/src/core/features/scoring.ts +121 -0
- package/src/core/features/security-headers.ts +162 -0
- package/src/core/features/session.ts +74 -0
- package/src/core/features/snipe.ts +264 -0
- package/src/core/features/social-check.ts +81 -0
- package/src/core/features/ssl-check.ts +88 -0
- package/src/core/features/subdomain-discovery.ts +53 -0
- package/src/core/features/takeover-detect.ts +143 -0
- package/src/core/features/tech-stack.ts +135 -0
- package/src/core/features/tld-expand.ts +43 -0
- package/src/core/features/variations.ts +134 -0
- package/src/core/features/version-check.ts +58 -0
- package/src/core/features/waf-detect.ts +171 -0
- package/src/core/features/watch.ts +120 -0
- package/src/core/features/wayback.ts +64 -0
- package/src/core/features/webhooks.ts +126 -0
- package/src/core/features/whois-history.ts +99 -0
- package/src/core/features/zone-transfer.ts +75 -0
- package/src/core/index.ts +50 -0
- package/src/core/paths.ts +9 -0
- package/src/core/registrar.ts +413 -0
- package/src/core/theme.ts +140 -0
- package/src/core/types.ts +143 -0
- package/src/core/validate.ts +58 -0
- package/src/core/whois.ts +265 -0
- package/src/index.tsx +1888 -0
- package/src/market-client.ts +186 -0
- package/src/proxy/ca.ts +116 -0
- package/src/proxy/db.ts +175 -0
- package/src/proxy/server.ts +155 -0
- package/tsconfig.json +30 -0
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared type definitions used across all modules
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { DomainStatus } from "./theme.js";
|
|
6
|
+
import type { WhoisResult } from "./whois.js";
|
|
7
|
+
import type { RegistrationResult } from "./registrar.js";
|
|
8
|
+
import type { RdapResult } from "./features/rdap.js";
|
|
9
|
+
import type { SslResult } from "./features/ssl-check.js";
|
|
10
|
+
import type { SubdomainResult } from "./features/subdomain-discovery.js";
|
|
11
|
+
import type { MarketplaceListing } from "./features/marketplace.js";
|
|
12
|
+
import type { SocialCheckResult } from "./features/social-check.js";
|
|
13
|
+
import type { TechStackResult } from "./features/tech-stack.js";
|
|
14
|
+
import type { BlacklistResult } from "./features/blacklist-check.js";
|
|
15
|
+
import type { BacklinkResult } from "./features/backlinks.js";
|
|
16
|
+
import type { PortScanResult } from "./features/port-scanner.js";
|
|
17
|
+
import type { ReverseIpResult } from "./features/reverse-ip.js";
|
|
18
|
+
import type { AsnResult } from "./features/asn-lookup.js";
|
|
19
|
+
import type { EmailSecurityResult } from "./features/email-security.js";
|
|
20
|
+
import type { ZoneTransferResult } from "./features/zone-transfer.js";
|
|
21
|
+
import type { CertTransparencyResult } from "./features/cert-transparency.js";
|
|
22
|
+
import type { TakeoverResult } from "./features/takeover-detect.js";
|
|
23
|
+
import type { SecurityHeadersResult } from "./features/security-headers.js";
|
|
24
|
+
import type { WafResult } from "./features/waf-detect.js";
|
|
25
|
+
import type { PathScanResult } from "./features/path-scanner.js";
|
|
26
|
+
import type { CorsResult } from "./features/cors-check.js";
|
|
27
|
+
|
|
28
|
+
export type { RdapResult } from "./features/rdap.js";
|
|
29
|
+
export type { SslResult } from "./features/ssl-check.js";
|
|
30
|
+
export type { SubdomainResult } from "./features/subdomain-discovery.js";
|
|
31
|
+
export type { MarketplaceListing } from "./features/marketplace.js";
|
|
32
|
+
export type { SocialCheckResult } from "./features/social-check.js";
|
|
33
|
+
export type { TechStackResult } from "./features/tech-stack.js";
|
|
34
|
+
export type { BlacklistResult } from "./features/blacklist-check.js";
|
|
35
|
+
export type { BacklinkResult } from "./features/backlinks.js";
|
|
36
|
+
export type { PortScanResult } from "./features/port-scanner.js";
|
|
37
|
+
export type { ReverseIpResult } from "./features/reverse-ip.js";
|
|
38
|
+
export type { AsnResult } from "./features/asn-lookup.js";
|
|
39
|
+
export type { EmailSecurityResult } from "./features/email-security.js";
|
|
40
|
+
export type { ZoneTransferResult } from "./features/zone-transfer.js";
|
|
41
|
+
export type { CertTransparencyResult } from "./features/cert-transparency.js";
|
|
42
|
+
export type { TakeoverResult } from "./features/takeover-detect.js";
|
|
43
|
+
export type { SecurityHeadersResult } from "./features/security-headers.js";
|
|
44
|
+
export type { WafResult } from "./features/waf-detect.js";
|
|
45
|
+
export type { PathScanResult } from "./features/path-scanner.js";
|
|
46
|
+
export type { CorsResult } from "./features/cors-check.js";
|
|
47
|
+
|
|
48
|
+
export interface DnsDetails {
|
|
49
|
+
a: string[];
|
|
50
|
+
aaaa: string[];
|
|
51
|
+
mx: string[];
|
|
52
|
+
txt: string[];
|
|
53
|
+
cname: string[];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface HttpProbeResult {
|
|
57
|
+
status: number | null;
|
|
58
|
+
redirectUrl: string | null;
|
|
59
|
+
server: string | null;
|
|
60
|
+
parked: boolean;
|
|
61
|
+
reachable: boolean;
|
|
62
|
+
error: string | null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface WaybackResult {
|
|
66
|
+
hasHistory: boolean;
|
|
67
|
+
firstArchived: string | null;
|
|
68
|
+
lastArchived: string | null;
|
|
69
|
+
snapshots: number;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface DomainEntry {
|
|
73
|
+
domain: string;
|
|
74
|
+
status: DomainStatus;
|
|
75
|
+
whois: WhoisResult | null;
|
|
76
|
+
verification: { available: boolean; confidence: string; checks: string[] } | null;
|
|
77
|
+
registrarCheck: { available: boolean; price?: number; currency?: string } | null;
|
|
78
|
+
registration: RegistrationResult | null;
|
|
79
|
+
error: string | null;
|
|
80
|
+
tagged: boolean;
|
|
81
|
+
dns: DnsDetails | null;
|
|
82
|
+
httpProbe: HttpProbeResult | null;
|
|
83
|
+
wayback: WaybackResult | null;
|
|
84
|
+
domainAge: string | null;
|
|
85
|
+
rdap: RdapResult | null;
|
|
86
|
+
ssl: SslResult | null;
|
|
87
|
+
subdomains: SubdomainResult[] | null;
|
|
88
|
+
marketplace: MarketplaceListing[] | null;
|
|
89
|
+
socialMedia: SocialCheckResult[] | null;
|
|
90
|
+
techStack: TechStackResult | null;
|
|
91
|
+
blacklist: BlacklistResult | null;
|
|
92
|
+
backlinks: BacklinkResult | null;
|
|
93
|
+
portScan: PortScanResult | null;
|
|
94
|
+
reverseIp: ReverseIpResult | null;
|
|
95
|
+
asn: AsnResult | null;
|
|
96
|
+
emailSecurity: EmailSecurityResult | null;
|
|
97
|
+
zoneTransfer: ZoneTransferResult | null;
|
|
98
|
+
certTransparency: CertTransparencyResult | null;
|
|
99
|
+
takeover: TakeoverResult | null;
|
|
100
|
+
securityHeaders: SecurityHeadersResult | null;
|
|
101
|
+
waf: WafResult | null;
|
|
102
|
+
pathScan: PathScanResult | null;
|
|
103
|
+
cors: CorsResult | null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Create an empty DomainEntry with sensible defaults
|
|
108
|
+
*/
|
|
109
|
+
export function createEmptyEntry(domain: string): DomainEntry {
|
|
110
|
+
return {
|
|
111
|
+
domain,
|
|
112
|
+
status: "pending",
|
|
113
|
+
whois: null,
|
|
114
|
+
verification: null,
|
|
115
|
+
registrarCheck: null,
|
|
116
|
+
registration: null,
|
|
117
|
+
error: null,
|
|
118
|
+
tagged: false,
|
|
119
|
+
dns: null,
|
|
120
|
+
httpProbe: null,
|
|
121
|
+
wayback: null,
|
|
122
|
+
domainAge: null,
|
|
123
|
+
rdap: null,
|
|
124
|
+
ssl: null,
|
|
125
|
+
subdomains: null,
|
|
126
|
+
marketplace: null,
|
|
127
|
+
socialMedia: null,
|
|
128
|
+
techStack: null,
|
|
129
|
+
blacklist: null,
|
|
130
|
+
backlinks: null,
|
|
131
|
+
portScan: null,
|
|
132
|
+
reverseIp: null,
|
|
133
|
+
asn: null,
|
|
134
|
+
emailSecurity: null,
|
|
135
|
+
zoneTransfer: null,
|
|
136
|
+
certTransparency: null,
|
|
137
|
+
takeover: null,
|
|
138
|
+
securityHeaders: null,
|
|
139
|
+
waf: null,
|
|
140
|
+
pathScan: null,
|
|
141
|
+
cors: null,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Input validation utilities for domain names, session IDs, and file paths
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { resolve, normalize } from "path";
|
|
6
|
+
|
|
7
|
+
export const DOMAIN_RE =
|
|
8
|
+
/^[a-z0-9]([a-z0-9\-]{0,61}[a-z0-9])?(\.[a-z0-9]([a-z0-9\-]{0,61}[a-z0-9])?)*\.[a-z]{2,}$/;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Check if a string is a valid domain name (case-insensitive)
|
|
12
|
+
*/
|
|
13
|
+
export function isValidDomain(domain: string): boolean {
|
|
14
|
+
if (domain.length > 253) return false;
|
|
15
|
+
return DOMAIN_RE.test(domain.toLowerCase());
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Assert that a string is a valid domain name, throwing if not
|
|
20
|
+
*/
|
|
21
|
+
export function assertValidDomain(domain: string): void {
|
|
22
|
+
if (!isValidDomain(domain)) {
|
|
23
|
+
throw new Error(`Invalid domain: ${domain}`);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Filter and normalize a list of domains to only valid ones (lowercased)
|
|
29
|
+
*/
|
|
30
|
+
export function sanitizeDomainList(domains: string[]): string[] {
|
|
31
|
+
return domains
|
|
32
|
+
.map((d) => d.toLowerCase())
|
|
33
|
+
.filter(isValidDomain);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Check if a string is a valid session ID (alphanumeric + hyphens, max 100 chars)
|
|
38
|
+
*/
|
|
39
|
+
export function isValidSessionId(id: string): boolean {
|
|
40
|
+
if (id.length === 0 || id.length > 100) return false;
|
|
41
|
+
return /^[a-z0-9\-]+$/.test(id);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Resolve and normalize a file path, ensuring it falls within one of the allowed root directories.
|
|
46
|
+
* Throws if the resolved path is outside all allowed roots.
|
|
47
|
+
*/
|
|
48
|
+
export function safePath(input: string, allowedRoots: string[]): string {
|
|
49
|
+
const resolved = normalize(resolve(input));
|
|
50
|
+
const isAllowed = allowedRoots.some((root) => {
|
|
51
|
+
const normalizedRoot = normalize(resolve(root));
|
|
52
|
+
return resolved.startsWith(normalizedRoot + "/") || resolved === normalizedRoot;
|
|
53
|
+
});
|
|
54
|
+
if (!isAllowed) {
|
|
55
|
+
throw new Error(`Path "${input}" is outside allowed roots`);
|
|
56
|
+
}
|
|
57
|
+
return resolved;
|
|
58
|
+
}
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import { execFile } from "child_process";
|
|
2
|
+
import { promisify } from "util";
|
|
3
|
+
import { assertValidDomain, isValidDomain } from "./validate.js";
|
|
4
|
+
|
|
5
|
+
const execFileAsync = promisify(execFile);
|
|
6
|
+
|
|
7
|
+
export interface WhoisResult {
|
|
8
|
+
domain: string;
|
|
9
|
+
available: boolean;
|
|
10
|
+
expired: boolean;
|
|
11
|
+
expiryDate: string | null;
|
|
12
|
+
registrar: string | null;
|
|
13
|
+
createdDate: string | null;
|
|
14
|
+
updatedDate: string | null;
|
|
15
|
+
status: string[];
|
|
16
|
+
nameServers: string[];
|
|
17
|
+
rawText: string;
|
|
18
|
+
error: string | null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Parse a WHOIS response to extract domain info
|
|
23
|
+
*/
|
|
24
|
+
function parseWhoisResponse(domain: string, raw: string): WhoisResult {
|
|
25
|
+
const lines = raw.split("\n").map((l) => l.trim());
|
|
26
|
+
|
|
27
|
+
const result: WhoisResult = {
|
|
28
|
+
domain,
|
|
29
|
+
available: false,
|
|
30
|
+
expired: false,
|
|
31
|
+
expiryDate: null,
|
|
32
|
+
registrar: null,
|
|
33
|
+
createdDate: null,
|
|
34
|
+
updatedDate: null,
|
|
35
|
+
status: [],
|
|
36
|
+
nameServers: [],
|
|
37
|
+
rawText: raw,
|
|
38
|
+
error: null,
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
// Check for "not found" / available indicators
|
|
42
|
+
const notFoundPatterns = [
|
|
43
|
+
"no match for",
|
|
44
|
+
"not found",
|
|
45
|
+
"no entries found",
|
|
46
|
+
"no data found",
|
|
47
|
+
"domain not found",
|
|
48
|
+
"no object found",
|
|
49
|
+
"nothing found",
|
|
50
|
+
"status: free",
|
|
51
|
+
"status: available",
|
|
52
|
+
"is available for registration",
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
const lowerRaw = raw.toLowerCase();
|
|
56
|
+
for (const pattern of notFoundPatterns) {
|
|
57
|
+
if (lowerRaw.includes(pattern)) {
|
|
58
|
+
result.available = true;
|
|
59
|
+
return result;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Extract fields
|
|
64
|
+
for (const line of lines) {
|
|
65
|
+
const lower = line.toLowerCase();
|
|
66
|
+
|
|
67
|
+
// Expiry date
|
|
68
|
+
if (
|
|
69
|
+
!result.expiryDate &&
|
|
70
|
+
(lower.startsWith("registry expiry date:") ||
|
|
71
|
+
lower.startsWith("registrar registration expiration date:") ||
|
|
72
|
+
lower.startsWith("expiration date:") ||
|
|
73
|
+
lower.startsWith("expires:") ||
|
|
74
|
+
lower.startsWith("expiry date:") ||
|
|
75
|
+
lower.startsWith("paid-till:") ||
|
|
76
|
+
lower.startsWith("expire:"))
|
|
77
|
+
) {
|
|
78
|
+
result.expiryDate = line.split(":").slice(1).join(":").trim();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Registrar
|
|
82
|
+
if (!result.registrar && lower.startsWith("registrar:")) {
|
|
83
|
+
result.registrar = line.split(":").slice(1).join(":").trim();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Created date
|
|
87
|
+
if (
|
|
88
|
+
!result.createdDate &&
|
|
89
|
+
(lower.startsWith("creation date:") ||
|
|
90
|
+
lower.startsWith("created:") ||
|
|
91
|
+
lower.startsWith("created date:") ||
|
|
92
|
+
lower.startsWith("registration date:"))
|
|
93
|
+
) {
|
|
94
|
+
result.createdDate = line.split(":").slice(1).join(":").trim();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Updated date
|
|
98
|
+
if (
|
|
99
|
+
!result.updatedDate &&
|
|
100
|
+
(lower.startsWith("updated date:") || lower.startsWith("last updated:"))
|
|
101
|
+
) {
|
|
102
|
+
result.updatedDate = line.split(":").slice(1).join(":").trim();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Status
|
|
106
|
+
if (
|
|
107
|
+
lower.startsWith("domain status:") ||
|
|
108
|
+
lower.startsWith("status:")
|
|
109
|
+
) {
|
|
110
|
+
const status = line.split(":").slice(1).join(":").trim();
|
|
111
|
+
if (status) result.status.push(status);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Name servers
|
|
115
|
+
if (lower.startsWith("name server:") || lower.startsWith("nserver:")) {
|
|
116
|
+
const ns = line.split(":").slice(1).join(":").trim();
|
|
117
|
+
if (ns) result.nameServers.push(ns);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Check if domain is expired
|
|
122
|
+
if (result.expiryDate) {
|
|
123
|
+
try {
|
|
124
|
+
const expiry = new Date(result.expiryDate);
|
|
125
|
+
const now = new Date();
|
|
126
|
+
if (expiry < now) {
|
|
127
|
+
result.expired = true;
|
|
128
|
+
}
|
|
129
|
+
} catch {
|
|
130
|
+
// Could not parse date
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Check status for expiration indicators
|
|
135
|
+
const expiredStatuses = [
|
|
136
|
+
"redemptionperiod",
|
|
137
|
+
"pendingdelete",
|
|
138
|
+
"expired",
|
|
139
|
+
"autorenewperiod",
|
|
140
|
+
];
|
|
141
|
+
for (const s of result.status) {
|
|
142
|
+
const lowerStatus = s.toLowerCase();
|
|
143
|
+
for (const es of expiredStatuses) {
|
|
144
|
+
if (lowerStatus.includes(es)) {
|
|
145
|
+
result.expired = true;
|
|
146
|
+
break;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return result;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Perform a WHOIS lookup for a domain
|
|
156
|
+
*/
|
|
157
|
+
export async function whoisLookup(domain: string): Promise<WhoisResult> {
|
|
158
|
+
assertValidDomain(domain);
|
|
159
|
+
try {
|
|
160
|
+
const { stdout, stderr } = await execFileAsync("whois", [domain], {
|
|
161
|
+
timeout: 15000,
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
const raw = stdout || stderr || "";
|
|
165
|
+
return parseWhoisResponse(domain, raw);
|
|
166
|
+
} catch (err: any) {
|
|
167
|
+
// whois command may return non-zero but still have useful output
|
|
168
|
+
if (err.stdout) {
|
|
169
|
+
return parseWhoisResponse(domain, err.stdout);
|
|
170
|
+
}
|
|
171
|
+
return {
|
|
172
|
+
domain,
|
|
173
|
+
available: false,
|
|
174
|
+
expired: false,
|
|
175
|
+
expiryDate: null,
|
|
176
|
+
registrar: null,
|
|
177
|
+
createdDate: null,
|
|
178
|
+
updatedDate: null,
|
|
179
|
+
status: [],
|
|
180
|
+
nameServers: [],
|
|
181
|
+
rawText: "",
|
|
182
|
+
error: err.message || "WHOIS lookup failed",
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Verify domain availability using multiple methods to avoid false positives
|
|
189
|
+
*/
|
|
190
|
+
export async function verifyAvailability(
|
|
191
|
+
domain: string
|
|
192
|
+
): Promise<{ available: boolean; confidence: "high" | "medium" | "low"; checks: string[] }> {
|
|
193
|
+
assertValidDomain(domain);
|
|
194
|
+
const checks: string[] = [];
|
|
195
|
+
let availableCount = 0;
|
|
196
|
+
let totalChecks = 0;
|
|
197
|
+
|
|
198
|
+
// Check 1: WHOIS lookup
|
|
199
|
+
const whois = await whoisLookup(domain);
|
|
200
|
+
totalChecks++;
|
|
201
|
+
if (whois.available) {
|
|
202
|
+
availableCount++;
|
|
203
|
+
checks.push("✓ WHOIS: Domain not found in registry");
|
|
204
|
+
} else if (whois.expired) {
|
|
205
|
+
availableCount += 0.5;
|
|
206
|
+
checks.push("⚠ WHOIS: Domain expired (may be in grace period)");
|
|
207
|
+
} else {
|
|
208
|
+
checks.push("✗ WHOIS: Domain is registered");
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Check 2: DNS resolution check
|
|
212
|
+
try {
|
|
213
|
+
const { stdout } = await execFileAsync("dig", ["+short", domain, "A"], {
|
|
214
|
+
timeout: 10000,
|
|
215
|
+
});
|
|
216
|
+
totalChecks++;
|
|
217
|
+
if (!stdout.trim()) {
|
|
218
|
+
availableCount++;
|
|
219
|
+
checks.push("✓ DNS: No A records found");
|
|
220
|
+
} else {
|
|
221
|
+
checks.push(`✗ DNS: Resolves to ${stdout.trim().split("\n")[0]}`);
|
|
222
|
+
}
|
|
223
|
+
} catch {
|
|
224
|
+
checks.push("⚠ DNS: Check failed");
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Check 3: NS record check
|
|
228
|
+
try {
|
|
229
|
+
const { stdout } = await execFileAsync("dig", ["+short", domain, "NS"], {
|
|
230
|
+
timeout: 10000,
|
|
231
|
+
});
|
|
232
|
+
totalChecks++;
|
|
233
|
+
if (!stdout.trim()) {
|
|
234
|
+
availableCount++;
|
|
235
|
+
checks.push("✓ NS: No nameservers found");
|
|
236
|
+
} else {
|
|
237
|
+
checks.push(`✗ NS: Has nameservers (${stdout.trim().split("\n")[0]})`);
|
|
238
|
+
}
|
|
239
|
+
} catch {
|
|
240
|
+
checks.push("⚠ NS: Check failed");
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const ratio = totalChecks > 0 ? availableCount / totalChecks : 0;
|
|
244
|
+
let confidence: "high" | "medium" | "low" = "low";
|
|
245
|
+
if (ratio >= 0.8) confidence = "high";
|
|
246
|
+
else if (ratio >= 0.5) confidence = "medium";
|
|
247
|
+
|
|
248
|
+
return {
|
|
249
|
+
available: ratio >= 0.5,
|
|
250
|
+
confidence,
|
|
251
|
+
checks,
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Parse a domain list from file content (one domain per line)
|
|
257
|
+
*/
|
|
258
|
+
export function parseDomainList(content: string): string[] {
|
|
259
|
+
return content
|
|
260
|
+
.split("\n")
|
|
261
|
+
.map((line) => line.trim())
|
|
262
|
+
.filter((line) => line && !line.startsWith("#") && !line.startsWith("//"))
|
|
263
|
+
.map((line) => line.toLowerCase())
|
|
264
|
+
.filter(isValidDomain);
|
|
265
|
+
}
|