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,162 @@
|
|
|
1
|
+
import { assertValidDomain } from "../validate.js";
|
|
2
|
+
|
|
3
|
+
export interface SecurityHeadersResult {
|
|
4
|
+
domain: string;
|
|
5
|
+
grade: "A+" | "A" | "B" | "C" | "D" | "F";
|
|
6
|
+
score: number;
|
|
7
|
+
headers: HeaderCheck[];
|
|
8
|
+
missing: string[];
|
|
9
|
+
error: string | null;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface HeaderCheck {
|
|
13
|
+
name: string;
|
|
14
|
+
present: boolean;
|
|
15
|
+
value: string | null;
|
|
16
|
+
status: "good" | "warn" | "bad" | "missing";
|
|
17
|
+
detail: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const HEADER_CHECKS: {
|
|
21
|
+
name: string;
|
|
22
|
+
header: string;
|
|
23
|
+
weight: number;
|
|
24
|
+
check: (value: string | null) => { status: "good" | "warn" | "bad" | "missing"; detail: string };
|
|
25
|
+
}[] = [
|
|
26
|
+
{
|
|
27
|
+
name: "Strict-Transport-Security",
|
|
28
|
+
header: "strict-transport-security",
|
|
29
|
+
weight: 20,
|
|
30
|
+
check: (v) => {
|
|
31
|
+
if (!v) return { status: "missing", detail: "HSTS not set — allows protocol downgrade attacks" };
|
|
32
|
+
if (v.includes("max-age=0")) return { status: "bad", detail: "HSTS max-age=0 effectively disables it" };
|
|
33
|
+
const maxAge = parseInt(v.match(/max-age=(\d+)/)?.[1] || "0", 10);
|
|
34
|
+
if (maxAge < 31536000) return { status: "warn", detail: `HSTS max-age=${maxAge} (recommended: 31536000+)` };
|
|
35
|
+
if (v.includes("includeSubDomains") && v.includes("preload")) return { status: "good", detail: "HSTS with preload and includeSubDomains" };
|
|
36
|
+
return { status: "good", detail: `HSTS max-age=${maxAge}` };
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
name: "Content-Security-Policy",
|
|
41
|
+
header: "content-security-policy",
|
|
42
|
+
weight: 20,
|
|
43
|
+
check: (v) => {
|
|
44
|
+
if (!v) return { status: "missing", detail: "No CSP — vulnerable to XSS and injection" };
|
|
45
|
+
if (v.includes("unsafe-inline") && v.includes("unsafe-eval")) return { status: "warn", detail: "CSP allows unsafe-inline and unsafe-eval" };
|
|
46
|
+
if (v.includes("unsafe-inline")) return { status: "warn", detail: "CSP allows unsafe-inline" };
|
|
47
|
+
return { status: "good", detail: "CSP configured" };
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
name: "X-Frame-Options",
|
|
52
|
+
header: "x-frame-options",
|
|
53
|
+
weight: 15,
|
|
54
|
+
check: (v) => {
|
|
55
|
+
if (!v) return { status: "missing", detail: "No X-Frame-Options — clickjacking possible" };
|
|
56
|
+
if (v.toUpperCase() === "DENY" || v.toUpperCase() === "SAMEORIGIN") return { status: "good", detail: `X-Frame-Options: ${v}` };
|
|
57
|
+
return { status: "warn", detail: `X-Frame-Options: ${v} (unusual value)` };
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
name: "X-Content-Type-Options",
|
|
62
|
+
header: "x-content-type-options",
|
|
63
|
+
weight: 10,
|
|
64
|
+
check: (v) => {
|
|
65
|
+
if (!v) return { status: "missing", detail: "No X-Content-Type-Options — MIME sniffing possible" };
|
|
66
|
+
return { status: "good", detail: "nosniff enabled" };
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
name: "Referrer-Policy",
|
|
71
|
+
header: "referrer-policy",
|
|
72
|
+
weight: 10,
|
|
73
|
+
check: (v) => {
|
|
74
|
+
if (!v) return { status: "missing", detail: "No Referrer-Policy — may leak URLs to third parties" };
|
|
75
|
+
if (v === "unsafe-url") return { status: "bad", detail: "Referrer-Policy: unsafe-url leaks full URLs" };
|
|
76
|
+
return { status: "good", detail: `Referrer-Policy: ${v}` };
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
name: "Permissions-Policy",
|
|
81
|
+
header: "permissions-policy",
|
|
82
|
+
weight: 10,
|
|
83
|
+
check: (v) => {
|
|
84
|
+
if (!v) return { status: "missing", detail: "No Permissions-Policy — browser features unrestricted" };
|
|
85
|
+
return { status: "good", detail: "Permissions-Policy configured" };
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
name: "X-XSS-Protection",
|
|
90
|
+
header: "x-xss-protection",
|
|
91
|
+
weight: 5,
|
|
92
|
+
check: (v) => {
|
|
93
|
+
if (!v) return { status: "warn", detail: "No X-XSS-Protection (deprecated but still useful)" };
|
|
94
|
+
if (v.startsWith("0")) return { status: "warn", detail: "XSS Protection explicitly disabled" };
|
|
95
|
+
return { status: "good", detail: `X-XSS-Protection: ${v}` };
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
name: "Server Header",
|
|
100
|
+
header: "server",
|
|
101
|
+
weight: 5,
|
|
102
|
+
check: (v) => {
|
|
103
|
+
if (!v) return { status: "good", detail: "Server header hidden (good practice)" };
|
|
104
|
+
if (/\d+\.\d+/.test(v)) return { status: "warn", detail: `Server: ${v} (version exposed)` };
|
|
105
|
+
return { status: "good", detail: `Server: ${v}` };
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
name: "X-Powered-By",
|
|
110
|
+
header: "x-powered-by",
|
|
111
|
+
weight: 5,
|
|
112
|
+
check: (v) => {
|
|
113
|
+
if (!v) return { status: "good", detail: "X-Powered-By hidden (good practice)" };
|
|
114
|
+
return { status: "warn", detail: `X-Powered-By: ${v} (leaks technology info)` };
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
];
|
|
118
|
+
|
|
119
|
+
export async function auditSecurityHeaders(domain: string): Promise<SecurityHeadersResult> {
|
|
120
|
+
assertValidDomain(domain);
|
|
121
|
+
|
|
122
|
+
const result: SecurityHeadersResult = {
|
|
123
|
+
domain, grade: "F", score: 0, headers: [], missing: [], error: null,
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
try {
|
|
127
|
+
const resp = await fetch(`https://${domain}`, {
|
|
128
|
+
signal: AbortSignal.timeout(10000),
|
|
129
|
+
headers: { "User-Agent": "DomainSniper/2.0" },
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
let totalWeight = 0;
|
|
133
|
+
let earnedWeight = 0;
|
|
134
|
+
|
|
135
|
+
for (const hc of HEADER_CHECKS) {
|
|
136
|
+
const value = resp.headers.get(hc.header);
|
|
137
|
+
const { status, detail } = hc.check(value);
|
|
138
|
+
totalWeight += hc.weight;
|
|
139
|
+
|
|
140
|
+
if (status === "good") earnedWeight += hc.weight;
|
|
141
|
+
else if (status === "warn") earnedWeight += hc.weight * 0.5;
|
|
142
|
+
// bad and missing = 0 points
|
|
143
|
+
|
|
144
|
+
result.headers.push({ name: hc.name, present: !!value, value, status, detail });
|
|
145
|
+
if (status === "missing") result.missing.push(hc.name);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
result.score = Math.round((earnedWeight / totalWeight) * 100);
|
|
149
|
+
|
|
150
|
+
if (result.score >= 95) result.grade = "A+";
|
|
151
|
+
else if (result.score >= 80) result.grade = "A";
|
|
152
|
+
else if (result.score >= 65) result.grade = "B";
|
|
153
|
+
else if (result.score >= 45) result.grade = "C";
|
|
154
|
+
else if (result.score >= 25) result.grade = "D";
|
|
155
|
+
else result.grade = "F";
|
|
156
|
+
|
|
157
|
+
return result;
|
|
158
|
+
} catch (err: unknown) {
|
|
159
|
+
result.error = err instanceof Error ? err.message : "Security headers check failed";
|
|
160
|
+
return result;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session persistence — SQLite-backed save/load scan results
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
createSession as dbCreateSession,
|
|
7
|
+
updateSessionCount,
|
|
8
|
+
listAllSessions,
|
|
9
|
+
getSession,
|
|
10
|
+
getSessionScans,
|
|
11
|
+
deleteSessionById,
|
|
12
|
+
upsertDomain,
|
|
13
|
+
saveScan,
|
|
14
|
+
} from "../db.js";
|
|
15
|
+
import type { DomainEntry } from "../types.js";
|
|
16
|
+
import { scoreDomain } from "./scoring.js";
|
|
17
|
+
|
|
18
|
+
export interface SavedSession {
|
|
19
|
+
id: string;
|
|
20
|
+
timestamp: string;
|
|
21
|
+
domains: DomainEntry[];
|
|
22
|
+
watchlist: string[];
|
|
23
|
+
tags: Record<string, string[]>;
|
|
24
|
+
notes: Record<string, string>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function saveSession(
|
|
28
|
+
domains: DomainEntry[],
|
|
29
|
+
watchlist: string[] = [],
|
|
30
|
+
tags: Record<string, string[]> = {},
|
|
31
|
+
notes: Record<string, string> = {}
|
|
32
|
+
): string {
|
|
33
|
+
const sessionId = dbCreateSession();
|
|
34
|
+
for (const d of domains) {
|
|
35
|
+
const domainId = upsertDomain(d.domain);
|
|
36
|
+
const score = scoreDomain(d.domain);
|
|
37
|
+
saveScan(domainId, d.status, d, sessionId, score.total);
|
|
38
|
+
}
|
|
39
|
+
updateSessionCount(sessionId, domains.length);
|
|
40
|
+
return `session-${sessionId}`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function loadSession(id: string): SavedSession | null {
|
|
44
|
+
// Accept both "session-123" format and raw number
|
|
45
|
+
const numId = parseInt(id.replace("session-", "").replace("scan-", ""), 10);
|
|
46
|
+
if (isNaN(numId)) return null;
|
|
47
|
+
const session = getSession(numId);
|
|
48
|
+
if (!session) return null;
|
|
49
|
+
const domains = getSessionScans(numId);
|
|
50
|
+
return {
|
|
51
|
+
id: `session-${session.id}`,
|
|
52
|
+
timestamp: session.created_at,
|
|
53
|
+
domains,
|
|
54
|
+
watchlist: [],
|
|
55
|
+
tags: {},
|
|
56
|
+
notes: {},
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function listSessions(): { id: string; timestamp: string; count: number; path: string }[] {
|
|
61
|
+
return listAllSessions().map((s) => ({
|
|
62
|
+
id: `session-${s.id}`,
|
|
63
|
+
timestamp: s.created_at,
|
|
64
|
+
count: s.domain_count,
|
|
65
|
+
path: `db:session-${s.id}`,
|
|
66
|
+
}));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function deleteSession(id: string): boolean {
|
|
70
|
+
const numId = parseInt(id.replace("session-", "").replace("scan-", ""), 10);
|
|
71
|
+
if (isNaN(numId)) return false;
|
|
72
|
+
deleteSessionById(numId);
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import { execFile } from "child_process";
|
|
2
|
+
import { promisify } from "util";
|
|
3
|
+
import { whoisLookup } from "../whois.js";
|
|
4
|
+
import { registerDomain, loadConfigFromEnv, type RegistrarConfig } from "../registrar.js";
|
|
5
|
+
import { assertValidDomain } from "../validate.js";
|
|
6
|
+
import {
|
|
7
|
+
addSnipe, getSnipe, getActiveSnipes, updateSnipeStatus, updateSnipeCheck,
|
|
8
|
+
markSnipeRegistered, type SnipeStatus, type SnipePhase,
|
|
9
|
+
} from "../db.js";
|
|
10
|
+
import { sendWebhook, type WebhookPayload } from "./webhooks.js";
|
|
11
|
+
import { loadConfig } from "./config.js";
|
|
12
|
+
|
|
13
|
+
const execFileAsync = promisify(execFile);
|
|
14
|
+
|
|
15
|
+
export interface SnipeConfig {
|
|
16
|
+
registrarConfig: RegistrarConfig | null;
|
|
17
|
+
webhookUrl?: string;
|
|
18
|
+
maxPrice?: number;
|
|
19
|
+
onStatusChange?: (domain: string, status: SnipeStatus, phase: SnipePhase, message: string) => void;
|
|
20
|
+
onRegistered?: (domain: string) => void;
|
|
21
|
+
onFailed?: (domain: string, error: string) => void;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface SnipeTarget {
|
|
25
|
+
domain: string;
|
|
26
|
+
expiryDate: string | null;
|
|
27
|
+
status: SnipeStatus;
|
|
28
|
+
phase: SnipePhase;
|
|
29
|
+
checkCount: number;
|
|
30
|
+
lastChecked: string | null;
|
|
31
|
+
lastStatus: string | null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Phase intervals
|
|
35
|
+
const PHASE_INTERVALS: Record<SnipePhase, number> = {
|
|
36
|
+
hourly: 3600000, // 1 hour — domain is registered, just watching
|
|
37
|
+
frequent: 300000, // 5 minutes — domain expired, checking often
|
|
38
|
+
aggressive: 30000, // 30 seconds — domain in pending delete, sniping
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export class SnipeEngine {
|
|
42
|
+
private config: SnipeConfig;
|
|
43
|
+
private timer: ReturnType<typeof setInterval> | null = null;
|
|
44
|
+
private _running = false;
|
|
45
|
+
|
|
46
|
+
constructor(config: SnipeConfig) {
|
|
47
|
+
this.config = config;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
get running() { return this._running; }
|
|
51
|
+
|
|
52
|
+
async start(): Promise<void> {
|
|
53
|
+
if (this._running) return;
|
|
54
|
+
this._running = true;
|
|
55
|
+
|
|
56
|
+
this.emit("engine", "watching", "hourly", "Snipe engine started");
|
|
57
|
+
|
|
58
|
+
// Run immediately
|
|
59
|
+
await this.tick();
|
|
60
|
+
|
|
61
|
+
// Main loop — check every 30s, but only act on domains whose interval has elapsed
|
|
62
|
+
this.timer = setInterval(() => { void this.tick(); }, 30000);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
stop(): void {
|
|
66
|
+
this._running = false;
|
|
67
|
+
if (this.timer) {
|
|
68
|
+
clearInterval(this.timer);
|
|
69
|
+
this.timer = null;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
private emit(domain: string, status: SnipeStatus, phase: SnipePhase, message: string) {
|
|
74
|
+
this.config.onStatusChange?.(domain, status, phase, message);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
private async tick(): Promise<void> {
|
|
78
|
+
if (!this._running) return;
|
|
79
|
+
const snipes = getActiveSnipes();
|
|
80
|
+
|
|
81
|
+
for (const snipe of snipes) {
|
|
82
|
+
// Check if enough time has elapsed since last check
|
|
83
|
+
const interval = PHASE_INTERVALS[snipe.phase as SnipePhase] || PHASE_INTERVALS.hourly;
|
|
84
|
+
if (snipe.last_checked) {
|
|
85
|
+
const elapsed = Date.now() - new Date(snipe.last_checked + "Z").getTime();
|
|
86
|
+
if (elapsed < interval) continue;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
await this.checkDomain(snipe);
|
|
90
|
+
|
|
91
|
+
// Small delay between domains to avoid rate limiting
|
|
92
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
private async checkDomain(snipe: any): Promise<void> {
|
|
97
|
+
const domain = snipe.domain;
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
// Quick DNS check first (fastest)
|
|
101
|
+
const _hasNs = await this.hasNameservers(domain);
|
|
102
|
+
|
|
103
|
+
// Full WHOIS check
|
|
104
|
+
const whois = await whoisLookup(domain);
|
|
105
|
+
|
|
106
|
+
if (whois.available) {
|
|
107
|
+
// DOMAIN IS AVAILABLE — attempt registration!
|
|
108
|
+
this.emit(domain, "registering", "aggressive", `${domain} is AVAILABLE — registering!`);
|
|
109
|
+
updateSnipeStatus(domain, "registering", "aggressive");
|
|
110
|
+
|
|
111
|
+
await this.attemptRegistration(domain, snipe);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (whois.expired) {
|
|
116
|
+
// Domain is expired — ramp up checking
|
|
117
|
+
const currentPhase = snipe.phase as SnipePhase;
|
|
118
|
+
|
|
119
|
+
// Check for pending delete indicators
|
|
120
|
+
const isPendingDelete = whois.status.some((s: string) =>
|
|
121
|
+
s.toLowerCase().includes("pendingdelete") || s.toLowerCase().includes("pending delete")
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
if (isPendingDelete) {
|
|
125
|
+
// About to drop — go aggressive
|
|
126
|
+
if (currentPhase !== "aggressive") {
|
|
127
|
+
updateSnipeStatus(domain, "dropping", "aggressive");
|
|
128
|
+
this.emit(domain, "dropping", "aggressive", `${domain} is PENDING DELETE — checking every 30s!`);
|
|
129
|
+
await this.notify(domain, "dropping", `${domain} is in pending delete — sniping aggressively!`);
|
|
130
|
+
}
|
|
131
|
+
} else {
|
|
132
|
+
// Expired but not yet pending delete
|
|
133
|
+
if (currentPhase === "hourly") {
|
|
134
|
+
updateSnipeStatus(domain, "expiring", "frequent");
|
|
135
|
+
this.emit(domain, "expiring", "frequent", `${domain} has EXPIRED — checking every 5 min`);
|
|
136
|
+
await this.notify(domain, "expiring", `${domain} has expired — monitoring closely`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
updateSnipeCheck(domain, whois.expired ? "expired" : "taken");
|
|
141
|
+
} else {
|
|
142
|
+
// Still registered and active
|
|
143
|
+
updateSnipeCheck(domain, "taken");
|
|
144
|
+
|
|
145
|
+
// Check if expiry date is approaching
|
|
146
|
+
if (whois.expiryDate) {
|
|
147
|
+
const daysLeft = Math.floor((new Date(whois.expiryDate).getTime() - Date.now()) / 86400000);
|
|
148
|
+
if (daysLeft <= 30 && snipe.phase === "hourly") {
|
|
149
|
+
this.emit(domain, "watching", "hourly", `${domain} expires in ${daysLeft} days`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
} catch (err: unknown) {
|
|
154
|
+
const msg = err instanceof Error ? err.message : "Check failed";
|
|
155
|
+
updateSnipeCheck(domain, `error: ${msg}`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
private async attemptRegistration(domain: string, snipe: any): Promise<void> {
|
|
160
|
+
const config = this.config.registrarConfig || loadConfigFromEnv();
|
|
161
|
+
if (!config?.apiKey) {
|
|
162
|
+
updateSnipeStatus(domain, "failed");
|
|
163
|
+
this.emit(domain, "failed", "aggressive", "No registrar configured — cannot register");
|
|
164
|
+
this.config.onFailed?.(domain, "No registrar configured");
|
|
165
|
+
await this.notify(domain, "failed", `${domain} is available but no registrar configured!`);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Check price constraint
|
|
170
|
+
if (snipe.max_price) {
|
|
171
|
+
// We don't have real-time pricing here, so just attempt registration
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
try {
|
|
175
|
+
const result = await registerDomain(domain, config);
|
|
176
|
+
|
|
177
|
+
if (result.success) {
|
|
178
|
+
markSnipeRegistered(domain);
|
|
179
|
+
this.emit(domain, "registered", "aggressive", `SUCCESS — ${domain} registered!`);
|
|
180
|
+
this.config.onRegistered?.(domain);
|
|
181
|
+
await this.notify(domain, "registered", `${domain} has been REGISTERED successfully!`);
|
|
182
|
+
} else {
|
|
183
|
+
// Registration failed — keep trying
|
|
184
|
+
updateSnipeCheck(domain, `reg-failed: ${result.error}`);
|
|
185
|
+
this.emit(domain, "dropping", "aggressive", `Registration attempt failed: ${result.error} — retrying...`);
|
|
186
|
+
}
|
|
187
|
+
} catch (err: unknown) {
|
|
188
|
+
const msg = err instanceof Error ? err.message : "Registration error";
|
|
189
|
+
updateSnipeCheck(domain, `reg-error: ${msg}`);
|
|
190
|
+
this.emit(domain, "dropping", "aggressive", `Registration error: ${msg} — retrying...`);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
private async hasNameservers(domain: string): Promise<boolean> {
|
|
195
|
+
try {
|
|
196
|
+
assertValidDomain(domain);
|
|
197
|
+
const { stdout } = await execFileAsync("dig", ["+short", domain, "NS"], { timeout: 5000 });
|
|
198
|
+
return !!stdout.trim();
|
|
199
|
+
} catch {
|
|
200
|
+
return false;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
private async notify(domain: string, status: string, message: string): Promise<void> {
|
|
205
|
+
// Desktop notification
|
|
206
|
+
try {
|
|
207
|
+
const script = `display notification "${message.replace(/["\\]/g, "")}" with title "Domain Sniper" sound name "Glass"`;
|
|
208
|
+
execFileAsync("osascript", ["-e", script]).catch(() => {});
|
|
209
|
+
} catch {}
|
|
210
|
+
|
|
211
|
+
// Webhook
|
|
212
|
+
const webhookUrl = this.config.webhookUrl || loadConfig().notifications.webhookUrl;
|
|
213
|
+
if (webhookUrl) {
|
|
214
|
+
const payload: WebhookPayload = {
|
|
215
|
+
domain,
|
|
216
|
+
status,
|
|
217
|
+
timestamp: new Date().toISOString(),
|
|
218
|
+
details: { message },
|
|
219
|
+
};
|
|
220
|
+
await sendWebhook(webhookUrl, payload).catch(() => {});
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// ─── Convenience functions ───────────────────────────────
|
|
226
|
+
|
|
227
|
+
export function snipeDomain(domain: string, options: {
|
|
228
|
+
expiryDate?: string;
|
|
229
|
+
maxPrice?: number;
|
|
230
|
+
} = {}): number {
|
|
231
|
+
assertValidDomain(domain);
|
|
232
|
+
return addSnipe(domain, {
|
|
233
|
+
expiryDate: options.expiryDate,
|
|
234
|
+
maxPrice: options.maxPrice,
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export function cancelSnipe(domain: string): void {
|
|
239
|
+
updateSnipeStatus(domain, "cancelled");
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export function getSnipeTargets(): SnipeTarget[] {
|
|
243
|
+
const snipes = getActiveSnipes();
|
|
244
|
+
return snipes.map((s: any) => ({
|
|
245
|
+
domain: s.domain,
|
|
246
|
+
expiryDate: s.expiry_date,
|
|
247
|
+
status: s.status,
|
|
248
|
+
phase: s.phase,
|
|
249
|
+
checkCount: s.check_count,
|
|
250
|
+
lastChecked: s.last_checked,
|
|
251
|
+
lastStatus: s.last_status,
|
|
252
|
+
}));
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export function createSnipeEngine(config: Partial<SnipeConfig> = {}): SnipeEngine {
|
|
256
|
+
return new SnipeEngine({
|
|
257
|
+
registrarConfig: config.registrarConfig || loadConfigFromEnv(),
|
|
258
|
+
webhookUrl: config.webhookUrl,
|
|
259
|
+
maxPrice: config.maxPrice,
|
|
260
|
+
onStatusChange: config.onStatusChange,
|
|
261
|
+
onRegistered: config.onRegistered,
|
|
262
|
+
onFailed: config.onFailed,
|
|
263
|
+
});
|
|
264
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { assertValidDomain } from "../validate.js";
|
|
2
|
+
|
|
3
|
+
export interface SocialCheckResult {
|
|
4
|
+
platform: string;
|
|
5
|
+
url: string;
|
|
6
|
+
available: boolean;
|
|
7
|
+
error: string | null;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const PLATFORMS: { name: string; urlTemplate: string }[] = [
|
|
11
|
+
{ name: "Twitter/X", urlTemplate: "https://x.com/{name}" },
|
|
12
|
+
{ name: "GitHub", urlTemplate: "https://github.com/{name}" },
|
|
13
|
+
{ name: "Instagram", urlTemplate: "https://www.instagram.com/{name}/" },
|
|
14
|
+
{ name: "Reddit", urlTemplate: "https://www.reddit.com/user/{name}" },
|
|
15
|
+
{ name: "TikTok", urlTemplate: "https://www.tiktok.com/@{name}" },
|
|
16
|
+
{ name: "YouTube", urlTemplate: "https://www.youtube.com/@{name}" },
|
|
17
|
+
{ name: "LinkedIn", urlTemplate: "https://www.linkedin.com/company/{name}" },
|
|
18
|
+
{ name: "Twitch", urlTemplate: "https://www.twitch.tv/{name}" },
|
|
19
|
+
{ name: "Pinterest", urlTemplate: "https://www.pinterest.com/{name}/" },
|
|
20
|
+
{ name: "npm", urlTemplate: "https://www.npmjs.com/package/{name}" },
|
|
21
|
+
{ name: "PyPI", urlTemplate: "https://pypi.org/project/{name}/" },
|
|
22
|
+
{ name: "Mastodon", urlTemplate: "https://mastodon.social/@{name}" },
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
async function checkPlatform(
|
|
26
|
+
platform: { name: string; urlTemplate: string },
|
|
27
|
+
username: string
|
|
28
|
+
): Promise<SocialCheckResult> {
|
|
29
|
+
const url = platform.urlTemplate.replace("{name}", encodeURIComponent(username));
|
|
30
|
+
try {
|
|
31
|
+
const resp = await fetch(url, {
|
|
32
|
+
method: "HEAD",
|
|
33
|
+
redirect: "manual",
|
|
34
|
+
signal: AbortSignal.timeout(6000),
|
|
35
|
+
headers: { "User-Agent": "DomainSniper/2.0" },
|
|
36
|
+
});
|
|
37
|
+
// 404 = available, 200 = taken, 3xx = usually taken (redirect to profile)
|
|
38
|
+
const available = resp.status === 404;
|
|
39
|
+
return { platform: platform.name, url, available, error: null };
|
|
40
|
+
} catch (err: unknown) {
|
|
41
|
+
// If HEAD fails, try GET (some platforms block HEAD)
|
|
42
|
+
try {
|
|
43
|
+
const resp = await fetch(url, {
|
|
44
|
+
redirect: "manual",
|
|
45
|
+
signal: AbortSignal.timeout(6000),
|
|
46
|
+
headers: { "User-Agent": "DomainSniper/2.0" },
|
|
47
|
+
});
|
|
48
|
+
const available = resp.status === 404;
|
|
49
|
+
return { platform: platform.name, url, available, error: null };
|
|
50
|
+
} catch {
|
|
51
|
+
return { platform: platform.name, url, available: false, error: "Unreachable" };
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export async function checkSocialMedia(
|
|
57
|
+
domain: string,
|
|
58
|
+
platforms: typeof PLATFORMS = PLATFORMS
|
|
59
|
+
): Promise<SocialCheckResult[]> {
|
|
60
|
+
// Extract name part from domain
|
|
61
|
+
const name = domain.split(".")[0] || "";
|
|
62
|
+
if (!name || name.length < 2) return [];
|
|
63
|
+
|
|
64
|
+
// Check in batches of 4 to avoid rate limiting
|
|
65
|
+
const results: SocialCheckResult[] = [];
|
|
66
|
+
const BATCH = 4;
|
|
67
|
+
for (let i = 0; i < platforms.length; i += BATCH) {
|
|
68
|
+
const batch = platforms.slice(i, i + BATCH);
|
|
69
|
+
const batchResults = await Promise.all(
|
|
70
|
+
batch.map((p) => checkPlatform(p, name))
|
|
71
|
+
);
|
|
72
|
+
results.push(...batchResults);
|
|
73
|
+
}
|
|
74
|
+
return results;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function getAvailablePlatforms(results: SocialCheckResult[]): SocialCheckResult[] {
|
|
78
|
+
return results.filter((r) => r.available && !r.error);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export { PLATFORMS };
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { assertValidDomain } from "../validate.js";
|
|
2
|
+
import { connect } from "tls";
|
|
3
|
+
|
|
4
|
+
export interface SslResult {
|
|
5
|
+
valid: boolean;
|
|
6
|
+
issuer: string | null;
|
|
7
|
+
subject: string | null;
|
|
8
|
+
validFrom: string | null;
|
|
9
|
+
validTo: string | null;
|
|
10
|
+
daysUntilExpiry: number | null;
|
|
11
|
+
sans: string[];
|
|
12
|
+
protocol: string | null;
|
|
13
|
+
error: string | null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function checkSsl(domain: string): Promise<SslResult> {
|
|
17
|
+
assertValidDomain(domain);
|
|
18
|
+
|
|
19
|
+
return new Promise((resolve) => {
|
|
20
|
+
const timeout = setTimeout(() => {
|
|
21
|
+
socket.destroy();
|
|
22
|
+
resolve({
|
|
23
|
+
valid: false, issuer: null, subject: null, validFrom: null,
|
|
24
|
+
validTo: null, daysUntilExpiry: null, sans: [], protocol: null,
|
|
25
|
+
error: "Connection timeout",
|
|
26
|
+
});
|
|
27
|
+
}, 8000);
|
|
28
|
+
|
|
29
|
+
const socket = connect(
|
|
30
|
+
{ host: domain, port: 443, servername: domain, rejectUnauthorized: false },
|
|
31
|
+
() => {
|
|
32
|
+
clearTimeout(timeout);
|
|
33
|
+
try {
|
|
34
|
+
const cert = (socket as any).getPeerCertificate?.();
|
|
35
|
+
if (!cert || !cert.subject) {
|
|
36
|
+
socket.destroy();
|
|
37
|
+
resolve({
|
|
38
|
+
valid: false, issuer: null, subject: null, validFrom: null,
|
|
39
|
+
validTo: null, daysUntilExpiry: null, sans: [], protocol: null,
|
|
40
|
+
error: "No certificate",
|
|
41
|
+
});
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const validTo = cert.valid_to ? new Date(cert.valid_to) : null;
|
|
46
|
+
const daysLeft = validTo ? Math.floor((validTo.getTime() - Date.now()) / 86400000) : null;
|
|
47
|
+
|
|
48
|
+
const sans: string[] = [];
|
|
49
|
+
if (cert.subjectaltname) {
|
|
50
|
+
const parts = (cert.subjectaltname as string).split(",").map((s: string) => s.trim());
|
|
51
|
+
for (const part of parts) {
|
|
52
|
+
if (part.startsWith("DNS:")) sans.push(part.slice(4));
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
socket.destroy();
|
|
57
|
+
resolve({
|
|
58
|
+
valid: (socket as any).authorized ?? (daysLeft !== null && daysLeft > 0),
|
|
59
|
+
issuer: cert.issuer?.O || cert.issuer?.CN || null,
|
|
60
|
+
subject: cert.subject?.CN || null,
|
|
61
|
+
validFrom: cert.valid_from || null,
|
|
62
|
+
validTo: cert.valid_to || null,
|
|
63
|
+
daysUntilExpiry: daysLeft,
|
|
64
|
+
sans,
|
|
65
|
+
protocol: (socket as any).getProtocol?.() || null,
|
|
66
|
+
error: null,
|
|
67
|
+
});
|
|
68
|
+
} catch (err: unknown) {
|
|
69
|
+
socket.destroy();
|
|
70
|
+
resolve({
|
|
71
|
+
valid: false, issuer: null, subject: null, validFrom: null,
|
|
72
|
+
validTo: null, daysUntilExpiry: null, sans: [], protocol: null,
|
|
73
|
+
error: err instanceof Error ? err.message : "SSL check failed",
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
socket.on("error", (err) => {
|
|
80
|
+
clearTimeout(timeout);
|
|
81
|
+
resolve({
|
|
82
|
+
valid: false, issuer: null, subject: null, validFrom: null,
|
|
83
|
+
validTo: null, daysUntilExpiry: null, sans: [], protocol: null,
|
|
84
|
+
error: err.message,
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
}
|