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.
Files changed (63) hide show
  1. package/.env.example +40 -0
  2. package/LICENSE +21 -0
  3. package/README.md +246 -0
  4. package/package.json +72 -0
  5. package/src/app.tsx +2062 -0
  6. package/src/completions.ts +65 -0
  7. package/src/core/db.ts +1313 -0
  8. package/src/core/features/asn-lookup.ts +91 -0
  9. package/src/core/features/backlinks.ts +83 -0
  10. package/src/core/features/blacklist-check.ts +67 -0
  11. package/src/core/features/cert-transparency.ts +87 -0
  12. package/src/core/features/config.ts +81 -0
  13. package/src/core/features/cors-check.ts +90 -0
  14. package/src/core/features/dns-details.ts +27 -0
  15. package/src/core/features/domain-age.ts +33 -0
  16. package/src/core/features/domain-suggest.ts +87 -0
  17. package/src/core/features/drop-catch.ts +159 -0
  18. package/src/core/features/email-security.ts +112 -0
  19. package/src/core/features/expiring-feed.ts +160 -0
  20. package/src/core/features/export.ts +74 -0
  21. package/src/core/features/filter.ts +96 -0
  22. package/src/core/features/http-probe.ts +46 -0
  23. package/src/core/features/marketplace.ts +69 -0
  24. package/src/core/features/path-scanner.ts +123 -0
  25. package/src/core/features/port-scanner.ts +132 -0
  26. package/src/core/features/portfolio-bulk.ts +125 -0
  27. package/src/core/features/portfolio-monitor.ts +214 -0
  28. package/src/core/features/portfolio.ts +98 -0
  29. package/src/core/features/price-compare.ts +39 -0
  30. package/src/core/features/rdap.ts +128 -0
  31. package/src/core/features/reverse-ip.ts +73 -0
  32. package/src/core/features/s3-export.ts +99 -0
  33. package/src/core/features/scoring.ts +121 -0
  34. package/src/core/features/security-headers.ts +162 -0
  35. package/src/core/features/session.ts +74 -0
  36. package/src/core/features/snipe.ts +264 -0
  37. package/src/core/features/social-check.ts +81 -0
  38. package/src/core/features/ssl-check.ts +88 -0
  39. package/src/core/features/subdomain-discovery.ts +53 -0
  40. package/src/core/features/takeover-detect.ts +143 -0
  41. package/src/core/features/tech-stack.ts +135 -0
  42. package/src/core/features/tld-expand.ts +43 -0
  43. package/src/core/features/variations.ts +134 -0
  44. package/src/core/features/version-check.ts +58 -0
  45. package/src/core/features/waf-detect.ts +171 -0
  46. package/src/core/features/watch.ts +120 -0
  47. package/src/core/features/wayback.ts +64 -0
  48. package/src/core/features/webhooks.ts +126 -0
  49. package/src/core/features/whois-history.ts +99 -0
  50. package/src/core/features/zone-transfer.ts +75 -0
  51. package/src/core/index.ts +50 -0
  52. package/src/core/paths.ts +9 -0
  53. package/src/core/registrar.ts +413 -0
  54. package/src/core/theme.ts +140 -0
  55. package/src/core/types.ts +143 -0
  56. package/src/core/validate.ts +58 -0
  57. package/src/core/whois.ts +265 -0
  58. package/src/index.tsx +1888 -0
  59. package/src/market-client.ts +186 -0
  60. package/src/proxy/ca.ts +116 -0
  61. package/src/proxy/db.ts +175 -0
  62. package/src/proxy/server.ts +155 -0
  63. package/tsconfig.json +30 -0
@@ -0,0 +1,159 @@
1
+ import { whoisLookup } from "../whois.js";
2
+ import { registerDomain, type RegistrarConfig } from "../registrar.js";
3
+ import { assertValidDomain } from "../validate.js";
4
+ import { execFile } from "child_process";
5
+ import { promisify } from "util";
6
+
7
+ const execFileAsync = promisify(execFile);
8
+
9
+ export interface DropCatchConfig {
10
+ domain: string;
11
+ registrarConfig: RegistrarConfig;
12
+ pollIntervalMs?: number; // how often to check (default: 30000 = 30s)
13
+ maxAttempts?: number; // max polling cycles (default: 2880 = 24h at 30s)
14
+ onStatus?: (status: DropCatchStatus) => void;
15
+ onSuccess?: (domain: string) => void;
16
+ onFailed?: (domain: string, error: string) => void;
17
+ }
18
+
19
+ export interface DropCatchStatus {
20
+ domain: string;
21
+ attempt: number;
22
+ maxAttempts: number;
23
+ phase: "watching" | "detected" | "registering" | "success" | "failed" | "expired_timeout";
24
+ message: string;
25
+ timestamp: string;
26
+ }
27
+
28
+ type ResolvedDropCatchConfig = DropCatchConfig & Required<Pick<DropCatchConfig, "pollIntervalMs" | "maxAttempts">>;
29
+
30
+ export class DropCatcher {
31
+ private config: ResolvedDropCatchConfig;
32
+ private timer: ReturnType<typeof setInterval> | null = null;
33
+ private attempt = 0;
34
+ private _running = false;
35
+ private _succeeded = false;
36
+
37
+ constructor(config: DropCatchConfig) {
38
+ assertValidDomain(config.domain);
39
+ this.config = {
40
+ pollIntervalMs: 30000,
41
+ maxAttempts: 2880,
42
+ ...config,
43
+ };
44
+ }
45
+
46
+ get running() { return this._running; }
47
+ get succeeded() { return this._succeeded; }
48
+
49
+ private emit(phase: DropCatchStatus["phase"], message: string) {
50
+ this.config.onStatus?.({
51
+ domain: this.config.domain,
52
+ attempt: this.attempt,
53
+ maxAttempts: this.config.maxAttempts,
54
+ phase,
55
+ message,
56
+ timestamp: new Date().toISOString(),
57
+ });
58
+ }
59
+
60
+ async start(): Promise<void> {
61
+ if (this._running) return;
62
+ this._running = true;
63
+ this.attempt = 0;
64
+
65
+ this.emit("watching", `Monitoring ${this.config.domain} for drop...`);
66
+
67
+ // Initial check
68
+ await this.poll();
69
+
70
+ // Set up polling
71
+ this.timer = setInterval(() => {
72
+ void this.poll();
73
+ }, this.config.pollIntervalMs);
74
+ }
75
+
76
+ stop(): void {
77
+ this._running = false;
78
+ if (this.timer) {
79
+ clearInterval(this.timer);
80
+ this.timer = null;
81
+ }
82
+ }
83
+
84
+ private async poll(): Promise<void> {
85
+ if (!this._running) return;
86
+ this.attempt++;
87
+
88
+ if (this.attempt > this.config.maxAttempts) {
89
+ this.emit("expired_timeout", "Max attempts reached, stopping.");
90
+ this.stop();
91
+ this.config.onFailed?.(this.config.domain, "Max attempts reached");
92
+ return;
93
+ }
94
+
95
+ try {
96
+ // Quick DNS check first (faster than full WHOIS)
97
+ const dnsAvailable = await this.quickDnsCheck();
98
+
99
+ if (dnsAvailable) {
100
+ this.emit("detected", "Domain may be available! Verifying...");
101
+
102
+ // Verify with WHOIS
103
+ const whois = await whoisLookup(this.config.domain);
104
+
105
+ if (whois.available) {
106
+ this.emit("registering", "Domain is available! Attempting registration...");
107
+
108
+ // Attempt registration immediately
109
+ const result = await registerDomain(this.config.domain, this.config.registrarConfig);
110
+
111
+ if (result.success) {
112
+ this._succeeded = true;
113
+ this.emit("success", `Successfully registered ${this.config.domain}!`);
114
+ this.config.onSuccess?.(this.config.domain);
115
+ this.stop();
116
+ } else {
117
+ this.emit("failed", `Registration failed: ${result.error || "Unknown error"}`);
118
+ // Don't stop — keep trying in case of transient failure
119
+ }
120
+ } else {
121
+ this.emit("watching", `Attempt ${this.attempt}/${this.config.maxAttempts} — DNS empty but WHOIS still registered`);
122
+ }
123
+ } else {
124
+ if (this.attempt % 10 === 0) {
125
+ this.emit("watching", `Attempt ${this.attempt}/${this.config.maxAttempts} — still registered`);
126
+ }
127
+ }
128
+ } catch (err: unknown) {
129
+ const msg = err instanceof Error ? err.message : "Unknown error";
130
+ this.emit("watching", `Check error: ${msg} (will retry)`);
131
+ }
132
+ }
133
+
134
+ private async quickDnsCheck(): Promise<boolean> {
135
+ try {
136
+ const { stdout } = await execFileAsync("dig", ["+short", this.config.domain, "NS"], { timeout: 5000 });
137
+ // Empty response = no nameservers = possibly available
138
+ return !stdout.trim();
139
+ } catch {
140
+ return false;
141
+ }
142
+ }
143
+ }
144
+
145
+ export function createDropCatcher(config: DropCatchConfig): DropCatcher {
146
+ return new DropCatcher(config);
147
+ }
148
+
149
+ export function formatDropCatchStatus(status: DropCatchStatus): string {
150
+ const progress = `[${status.attempt}/${status.maxAttempts}]`;
151
+ switch (status.phase) {
152
+ case "watching": return `${progress} Watching ${status.domain}...`;
153
+ case "detected": return `${progress} DETECTED — ${status.domain} may be dropping!`;
154
+ case "registering": return `${progress} REGISTERING ${status.domain}...`;
155
+ case "success": return `${progress} SUCCESS — ${status.domain} registered!`;
156
+ case "failed": return `${progress} FAILED — ${status.message}`;
157
+ case "expired_timeout": return `${progress} TIMEOUT — stopped monitoring`;
158
+ }
159
+ }
@@ -0,0 +1,112 @@
1
+ import { execFile } from "child_process";
2
+ import { promisify } from "util";
3
+ import { assertValidDomain } from "../validate.js";
4
+
5
+ const execFileAsync = promisify(execFile);
6
+
7
+ export interface EmailSecurityResult {
8
+ domain: string;
9
+ spf: { found: boolean; record: string | null; issues: string[] };
10
+ dkim: { found: boolean; record: string | null; selector: string | null };
11
+ dmarc: { found: boolean; record: string | null; policy: string | null; issues: string[] };
12
+ grade: "A" | "B" | "C" | "D" | "F";
13
+ issues: string[];
14
+ }
15
+
16
+ async function digTxt(query: string): Promise<string[]> {
17
+ try {
18
+ const { stdout } = await execFileAsync("dig", ["+short", query, "TXT"], { timeout: 8000 });
19
+ return stdout.trim().split("\n").filter(Boolean).map((l) => l.replace(/^"|"$/g, "").replace(/"\s*"/g, ""));
20
+ } catch {
21
+ return [];
22
+ }
23
+ }
24
+
25
+ export async function checkEmailSecurity(domain: string): Promise<EmailSecurityResult> {
26
+ assertValidDomain(domain);
27
+
28
+ const result: EmailSecurityResult = {
29
+ domain,
30
+ spf: { found: false, record: null, issues: [] },
31
+ dkim: { found: false, record: null, selector: null },
32
+ dmarc: { found: false, record: null, policy: null, issues: [] },
33
+ grade: "F",
34
+ issues: [],
35
+ };
36
+
37
+ // SPF check
38
+ const txtRecords = await digTxt(domain);
39
+ const spfRecord = txtRecords.find((r) => r.startsWith("v=spf1"));
40
+ if (spfRecord) {
41
+ result.spf.found = true;
42
+ result.spf.record = spfRecord;
43
+ if (spfRecord.includes("+all")) {
44
+ result.spf.issues.push("SPF uses +all (allows anyone to send)");
45
+ result.issues.push("SPF +all: anyone can spoof emails from this domain");
46
+ }
47
+ if (spfRecord.includes("?all")) {
48
+ result.spf.issues.push("SPF uses ?all (neutral — no enforcement)");
49
+ }
50
+ if (!spfRecord.includes("-all") && !spfRecord.includes("~all")) {
51
+ result.spf.issues.push("SPF missing strict -all or ~all");
52
+ }
53
+ // Check for too many lookups (max 10)
54
+ const lookups = (spfRecord.match(/include:|redirect=|a:|mx:|ptr:/g) || []).length;
55
+ if (lookups > 10) {
56
+ result.spf.issues.push(`SPF has ${lookups} lookups (max 10 allowed)`);
57
+ }
58
+ } else {
59
+ result.issues.push("No SPF record found");
60
+ }
61
+
62
+ // DMARC check
63
+ const dmarcRecords = await digTxt(`_dmarc.${domain}`);
64
+ const dmarcRecord = dmarcRecords.find((r) => r.startsWith("v=DMARC1"));
65
+ if (dmarcRecord) {
66
+ result.dmarc.found = true;
67
+ result.dmarc.record = dmarcRecord;
68
+ const policyMatch = dmarcRecord.match(/;\s*p\s*=\s*(\w+)/);
69
+ result.dmarc.policy = policyMatch ? policyMatch[1]! : null;
70
+ if (result.dmarc.policy === "none") {
71
+ result.dmarc.issues.push("DMARC policy is 'none' (monitoring only, no enforcement)");
72
+ result.issues.push("DMARC p=none: emails failing checks are still delivered");
73
+ }
74
+ if (!dmarcRecord.includes("rua=")) {
75
+ result.dmarc.issues.push("No aggregate reporting URI (rua) configured");
76
+ }
77
+ } else {
78
+ result.issues.push("No DMARC record found");
79
+ }
80
+
81
+ // DKIM check — try common selectors
82
+ const selectors = ["default", "google", "selector1", "selector2", "k1", "s1", "s2", "dkim", "mail"];
83
+ for (const selector of selectors) {
84
+ const dkimRecords = await digTxt(`${selector}._domainkey.${domain}`);
85
+ const dkimRecord = dkimRecords.find((r) => r.includes("v=DKIM1") || r.includes("p="));
86
+ if (dkimRecord) {
87
+ result.dkim.found = true;
88
+ result.dkim.record = dkimRecord;
89
+ result.dkim.selector = selector;
90
+ break;
91
+ }
92
+ }
93
+ if (!result.dkim.found) {
94
+ result.issues.push("No DKIM record found (checked common selectors)");
95
+ }
96
+
97
+ // Grade calculation
98
+ let score = 0;
99
+ if (result.spf.found && result.spf.issues.length === 0) score += 3;
100
+ else if (result.spf.found) score += 1;
101
+ if (result.dmarc.found && result.dmarc.policy !== "none") score += 4;
102
+ else if (result.dmarc.found) score += 1;
103
+ if (result.dkim.found) score += 3;
104
+
105
+ if (score >= 9) result.grade = "A";
106
+ else if (score >= 7) result.grade = "B";
107
+ else if (score >= 4) result.grade = "C";
108
+ else if (score >= 2) result.grade = "D";
109
+ else result.grade = "F";
110
+
111
+ return result;
112
+ }
@@ -0,0 +1,160 @@
1
+ import { assertValidDomain } from "../validate.js";
2
+
3
+ export interface ExpiringDomain {
4
+ domain: string;
5
+ expiryDate: string;
6
+ deleteDate: string | null;
7
+ registrar: string | null;
8
+ age: string | null;
9
+ source: string;
10
+ }
11
+
12
+ export interface ExpiringFeedConfig {
13
+ apiKey?: string; // WhoisFreaks API key (optional)
14
+ tld?: string; // Filter by TLD
15
+ minAge?: number; // Minimum domain age in years
16
+ limit?: number; // Max results
17
+ }
18
+
19
+ /**
20
+ * Fetch expiring/dropped domains from WhoisFreaks API
21
+ * Free tier: 100 requests/month
22
+ * Get a key at: https://whoisfreaks.com/
23
+ */
24
+ async function fetchFromWhoisFreaks(
25
+ config: ExpiringFeedConfig
26
+ ): Promise<ExpiringDomain[]> {
27
+ if (!config.apiKey) return [];
28
+
29
+ try {
30
+ const params = new URLSearchParams({
31
+ apiKey: config.apiKey,
32
+ whoisType: "expiring",
33
+ });
34
+ if (config.tld) params.set("tld", config.tld);
35
+ if (config.limit) params.set("page_size", String(Math.min(config.limit, 100)));
36
+
37
+ const resp = await fetch(
38
+ `https://api.whoisfreaks.com/v1.0/whois?${params.toString()}`,
39
+ { signal: AbortSignal.timeout(15000) }
40
+ );
41
+
42
+ if (!resp.ok) return [];
43
+
44
+ const data = await resp.json() as {
45
+ whois_domains_list?: Array<{
46
+ domain_name?: string;
47
+ expiry_date?: string;
48
+ create_date?: string;
49
+ registrar_name?: string;
50
+ }>;
51
+ };
52
+
53
+ if (!data.whois_domains_list) return [];
54
+
55
+ return data.whois_domains_list.map((d) => ({
56
+ domain: d.domain_name || "",
57
+ expiryDate: d.expiry_date || "",
58
+ deleteDate: null,
59
+ registrar: d.registrar_name || null,
60
+ age: d.create_date ? calculateAge(d.create_date) : null,
61
+ source: "WhoisFreaks",
62
+ })).filter((d) => d.domain);
63
+ } catch {
64
+ return [];
65
+ }
66
+ }
67
+
68
+ /**
69
+ * Check a specific list of domains for pending-delete status via RDAP
70
+ */
71
+ export async function checkPendingDelete(domains: string[]): Promise<ExpiringDomain[]> {
72
+ const results: ExpiringDomain[] = [];
73
+
74
+ for (const domain of domains) {
75
+ try {
76
+ assertValidDomain(domain);
77
+ const resp = await fetch(
78
+ `https://rdap.org/domain/${encodeURIComponent(domain)}`,
79
+ { signal: AbortSignal.timeout(8000) }
80
+ );
81
+
82
+ if (!resp.ok) continue;
83
+
84
+ const data = await resp.json() as {
85
+ status?: string[];
86
+ events?: Array<{ eventAction: string; eventDate: string }>;
87
+ entities?: Array<{ roles?: string[]; vcardArray?: unknown[] }>;
88
+ };
89
+
90
+ const status = data.status || [];
91
+ const isPendingDelete = status.some((s) =>
92
+ s.toLowerCase().includes("pending delete") ||
93
+ s.toLowerCase().includes("redemption period") ||
94
+ s.toLowerCase().includes("pendingdelete")
95
+ );
96
+
97
+ if (isPendingDelete) {
98
+ let expiryDate = "";
99
+ const registrar: string | null = null;
100
+ let createdDate: string | null = null;
101
+
102
+ if (data.events) {
103
+ for (const event of data.events) {
104
+ if (event.eventAction === "expiration") expiryDate = event.eventDate;
105
+ if (event.eventAction === "registration") createdDate = event.eventDate;
106
+ }
107
+ }
108
+
109
+ results.push({
110
+ domain,
111
+ expiryDate,
112
+ deleteDate: null, // Not always available from RDAP
113
+ registrar,
114
+ age: createdDate ? calculateAge(createdDate) : null,
115
+ source: "RDAP",
116
+ });
117
+ }
118
+
119
+ // Rate limit
120
+ await new Promise((r) => setTimeout(r, 500));
121
+ } catch {
122
+ continue;
123
+ }
124
+ }
125
+
126
+ return results;
127
+ }
128
+
129
+ /**
130
+ * Get a feed of expiring domains
131
+ */
132
+ export async function getExpiringFeed(config: ExpiringFeedConfig = {}): Promise<ExpiringDomain[]> {
133
+ const results: ExpiringDomain[] = [];
134
+
135
+ // Try WhoisFreaks if API key provided
136
+ const wfResults = await fetchFromWhoisFreaks(config);
137
+ results.push(...wfResults);
138
+
139
+ // Apply age filter
140
+ if (config.minAge && config.minAge > 0) {
141
+ return results.filter((d) => {
142
+ if (!d.age) return false;
143
+ const years = parseInt(d.age, 10);
144
+ return !isNaN(years) && years >= config.minAge!;
145
+ });
146
+ }
147
+
148
+ return results.slice(0, config.limit || 50);
149
+ }
150
+
151
+ function calculateAge(createDate: string): string | null {
152
+ try {
153
+ const created = new Date(createDate);
154
+ if (isNaN(created.getTime())) return null;
155
+ const years = Math.floor((Date.now() - created.getTime()) / (365.25 * 86400000));
156
+ return `${years}y`;
157
+ } catch {
158
+ return null;
159
+ }
160
+ }
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Export results to CSV / JSON
3
+ */
4
+
5
+ import { writeFileSync } from "fs";
6
+ import { scoreDomain } from "./scoring.js";
7
+ import { safePath } from "../validate.js";
8
+ import type { DomainEntry } from "../types.js";
9
+
10
+ interface ExportEntry {
11
+ domain: string;
12
+ status: string;
13
+ available: boolean;
14
+ expired: boolean;
15
+ confidence: string;
16
+ registrar: string;
17
+ expiryDate: string;
18
+ createdDate: string;
19
+ nameServers: string;
20
+ score: number;
21
+ grade: string;
22
+ price: string;
23
+ registered: boolean;
24
+ }
25
+
26
+ function toExportEntry(d: DomainEntry): ExportEntry {
27
+ const score = scoreDomain(d.domain);
28
+ return {
29
+ domain: d.domain,
30
+ status: d.status,
31
+ available: d.status === "available",
32
+ expired: d.status === "expired",
33
+ confidence: d.verification?.confidence || "",
34
+ registrar: d.whois?.registrar || "",
35
+ expiryDate: d.whois?.expiryDate || "",
36
+ createdDate: d.whois?.createdDate || "",
37
+ nameServers: (d.whois?.nameServers || []).join("; "),
38
+ score: score.total,
39
+ grade: score.total >= 85 ? "A+" : score.total >= 75 ? "A" : score.total >= 65 ? "B+" : score.total >= 55 ? "B" : score.total >= 45 ? "C+" : score.total >= 35 ? "C" : "D",
40
+ price: d.registrarCheck?.price ? `${d.registrarCheck.price}` : "",
41
+ registered: d.status === "registered",
42
+ };
43
+ }
44
+
45
+ export function exportToCSV(domains: DomainEntry[], filePath: string): string {
46
+ if (domains.length === 0) throw new Error("No domains to export");
47
+ const safe = safePath(filePath, [process.cwd()]);
48
+ const entries = domains.map(toExportEntry);
49
+ const headers = Object.keys(entries[0] || {});
50
+ const rows = entries.map((e) =>
51
+ headers.map((h) => {
52
+ const val = String((e as any)[h] ?? "");
53
+ return val.includes(",") || val.includes('"') ? `"${val.replace(/"/g, '""')}"` : val;
54
+ }).join(",")
55
+ );
56
+ const csv = [headers.join(","), ...rows].join("\n");
57
+ writeFileSync(safe, csv, "utf-8");
58
+ return safe;
59
+ }
60
+
61
+ export function exportToJSON(domains: DomainEntry[], filePath: string): string {
62
+ if (domains.length === 0) throw new Error("No domains to export");
63
+ const safe = safePath(filePath, [process.cwd()]);
64
+ const entries = domains.map(toExportEntry);
65
+ const json = JSON.stringify({
66
+ exported: new Date().toISOString(),
67
+ total: entries.length,
68
+ available: entries.filter((e) => e.available).length,
69
+ expired: entries.filter((e) => e.expired).length,
70
+ domains: entries,
71
+ }, null, 2);
72
+ writeFileSync(safe, json, "utf-8");
73
+ return safe;
74
+ }
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Filter & sort domain results
3
+ */
4
+
5
+ import type { DomainScore } from "./scoring.js";
6
+ import { scoreDomain } from "./scoring.js";
7
+ import type { DomainEntry } from "../types.js";
8
+
9
+ export type SortField = "domain" | "status" | "score" | "expiry" | "price";
10
+ export type SortOrder = "asc" | "desc";
11
+ export type FilterStatus = "all" | "available" | "expired" | "taken" | "registered" | "actionable";
12
+
13
+ export interface FilterConfig {
14
+ status: FilterStatus;
15
+ search: string;
16
+ sort: SortField;
17
+ order: SortOrder;
18
+ minScore: number;
19
+ }
20
+
21
+ export const DEFAULT_FILTER: FilterConfig = {
22
+ status: "all",
23
+ search: "",
24
+ sort: "domain",
25
+ order: "asc",
26
+ minScore: 0,
27
+ };
28
+
29
+ export function filterDomains(domains: DomainEntry[], config: FilterConfig): DomainEntry[] {
30
+ let filtered = [...domains];
31
+
32
+ // Status filter
33
+ if (config.status !== "all") {
34
+ if (config.status === "actionable") {
35
+ filtered = filtered.filter((d) => d.status === "available" || d.status === "expired");
36
+ } else {
37
+ filtered = filtered.filter((d) => d.status === config.status);
38
+ }
39
+ }
40
+
41
+ // Search filter
42
+ if (config.search) {
43
+ const q = config.search.toLowerCase();
44
+ filtered = filtered.filter((d) => d.domain.toLowerCase().includes(q));
45
+ }
46
+
47
+ // Score filter
48
+ if (config.minScore > 0) {
49
+ filtered = filtered.filter((d) => scoreDomain(d.domain).total >= config.minScore);
50
+ }
51
+
52
+ // Sort
53
+ filtered.sort((a, b) => {
54
+ let cmp = 0;
55
+ switch (config.sort) {
56
+ case "domain":
57
+ cmp = a.domain.localeCompare(b.domain);
58
+ break;
59
+ case "status": {
60
+ const order: Record<string, number> = { available: 0, expired: 1, registering: 2, registered: 3, taken: 4, error: 5, pending: 6, checking: 7 };
61
+ cmp = (order[a.status] ?? 9) - (order[b.status] ?? 9);
62
+ break;
63
+ }
64
+ case "score":
65
+ cmp = scoreDomain(b.domain).total - scoreDomain(a.domain).total;
66
+ break;
67
+ case "expiry": {
68
+ const aExp = a.whois?.expiryDate ? new Date(a.whois.expiryDate).getTime() : Infinity;
69
+ const bExp = b.whois?.expiryDate ? new Date(b.whois.expiryDate).getTime() : Infinity;
70
+ cmp = aExp - bExp;
71
+ break;
72
+ }
73
+ case "price": {
74
+ const aP = a.registrarCheck?.price ?? Infinity;
75
+ const bP = b.registrarCheck?.price ?? Infinity;
76
+ cmp = aP - bP;
77
+ break;
78
+ }
79
+ }
80
+ return config.order === "desc" ? -cmp : cmp;
81
+ });
82
+
83
+ return filtered;
84
+ }
85
+
86
+ export function nextStatus(current: FilterStatus): FilterStatus {
87
+ const order: FilterStatus[] = ["all", "available", "expired", "taken", "registered", "actionable"];
88
+ const idx = order.indexOf(current);
89
+ return order[(idx + 1) % order.length]!;
90
+ }
91
+
92
+ export function nextSort(current: SortField): SortField {
93
+ const order: SortField[] = ["domain", "status", "score", "expiry", "price"];
94
+ const idx = order.indexOf(current);
95
+ return order[(idx + 1) % order.length]!;
96
+ }
@@ -0,0 +1,46 @@
1
+ import { assertValidDomain } from "../validate.js";
2
+ import type { HttpProbeResult } from "../types.js";
3
+
4
+ const PARKED_INDICATORS = [
5
+ "parked", "for sale", "buy this domain", "domain parking",
6
+ "godaddy", "sedo", "afternic", "hugedomains", "dan.com",
7
+ "this domain is for sale", "under construction",
8
+ ];
9
+
10
+ export async function httpProbe(domain: string): Promise<HttpProbeResult> {
11
+ assertValidDomain(domain);
12
+
13
+ for (const scheme of ["https", "http"] as const) {
14
+ try {
15
+ const resp = await fetch(`${scheme}://${domain}`, {
16
+ redirect: "manual",
17
+ signal: AbortSignal.timeout(8000),
18
+ headers: { "User-Agent": "DomainSniper/2.0" },
19
+ });
20
+
21
+ const redirectUrl = resp.status >= 300 && resp.status < 400
22
+ ? resp.headers.get("location")
23
+ : null;
24
+
25
+ let parked = false;
26
+ try {
27
+ const body = await resp.text();
28
+ const lower = body.toLowerCase();
29
+ parked = PARKED_INDICATORS.some((ind) => lower.includes(ind));
30
+ } catch {}
31
+
32
+ return {
33
+ status: resp.status,
34
+ redirectUrl,
35
+ server: resp.headers.get("server"),
36
+ parked,
37
+ reachable: true,
38
+ error: null,
39
+ };
40
+ } catch {
41
+ continue;
42
+ }
43
+ }
44
+
45
+ return { status: null, redirectUrl: null, server: null, parked: false, reachable: false, error: "Unreachable" };
46
+ }