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,53 @@
|
|
|
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
|
+
const COMMON_SUBDOMAINS = [
|
|
8
|
+
"www", "mail", "ftp", "smtp", "pop", "imap",
|
|
9
|
+
"api", "app", "cdn", "dev", "staging", "beta",
|
|
10
|
+
"admin", "portal", "blog", "shop", "store",
|
|
11
|
+
"ns1", "ns2", "mx", "vpn", "remote",
|
|
12
|
+
"docs", "wiki", "git", "ci", "status",
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
export interface SubdomainResult {
|
|
16
|
+
subdomain: string;
|
|
17
|
+
full: string;
|
|
18
|
+
resolved: boolean;
|
|
19
|
+
ip: string | null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function discoverSubdomains(
|
|
23
|
+
domain: string,
|
|
24
|
+
subdomains: string[] = COMMON_SUBDOMAINS
|
|
25
|
+
): Promise<SubdomainResult[]> {
|
|
26
|
+
assertValidDomain(domain);
|
|
27
|
+
|
|
28
|
+
const results: SubdomainResult[] = [];
|
|
29
|
+
const BATCH = 10;
|
|
30
|
+
|
|
31
|
+
for (let i = 0; i < subdomains.length; i += BATCH) {
|
|
32
|
+
const batch = subdomains.slice(i, i + BATCH);
|
|
33
|
+
const batchResults = await Promise.all(
|
|
34
|
+
batch.map(async (sub) => {
|
|
35
|
+
const full = `${sub}.${domain}`;
|
|
36
|
+
try {
|
|
37
|
+
const { stdout } = await execFileAsync("dig", ["+short", full, "A"], { timeout: 5000 });
|
|
38
|
+
const ip = stdout.trim().split("\n")[0] || null;
|
|
39
|
+
return { subdomain: sub, full, resolved: !!ip, ip };
|
|
40
|
+
} catch {
|
|
41
|
+
return { subdomain: sub, full, resolved: false, ip: null };
|
|
42
|
+
}
|
|
43
|
+
})
|
|
44
|
+
);
|
|
45
|
+
results.push(...batchResults);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return results;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function getActiveSubdomains(results: SubdomainResult[]): SubdomainResult[] {
|
|
52
|
+
return results.filter((r) => r.resolved);
|
|
53
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
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 TakeoverResult {
|
|
8
|
+
domain: string;
|
|
9
|
+
vulnerable: boolean;
|
|
10
|
+
findings: TakeoverFinding[];
|
|
11
|
+
checkedSubdomains: number;
|
|
12
|
+
error: string | null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface TakeoverFinding {
|
|
16
|
+
subdomain: string;
|
|
17
|
+
cname: string;
|
|
18
|
+
service: string;
|
|
19
|
+
status: "vulnerable" | "potential" | "safe";
|
|
20
|
+
detail: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Services known to be susceptible to subdomain takeover
|
|
24
|
+
const TAKEOVER_SIGNATURES: { service: string; cnames: string[]; responseIndicators: string[] }[] = [
|
|
25
|
+
{ service: "GitHub Pages", cnames: ["github.io"], responseIndicators: ["There isn't a GitHub Pages site here"] },
|
|
26
|
+
{ service: "Heroku", cnames: ["herokuapp.com", "herokussl.com"], responseIndicators: ["No such app", "no-such-app"] },
|
|
27
|
+
{ service: "AWS S3", cnames: ["s3.amazonaws.com", "s3-website"], responseIndicators: ["NoSuchBucket", "The specified bucket does not exist"] },
|
|
28
|
+
{ service: "Netlify", cnames: ["netlify.app", "netlify.com"], responseIndicators: ["Not Found - Request ID"] },
|
|
29
|
+
{ service: "Vercel", cnames: ["vercel.app", "now.sh"], responseIndicators: ["The deployment could not be found"] },
|
|
30
|
+
{ service: "Surge.sh", cnames: ["surge.sh"], responseIndicators: ["project not found"] },
|
|
31
|
+
{ service: "Fly.io", cnames: ["fly.dev"], responseIndicators: ["404 Not Found"] },
|
|
32
|
+
{ service: "Shopify", cnames: ["myshopify.com"], responseIndicators: ["Sorry, this shop is currently unavailable"] },
|
|
33
|
+
{ service: "Tumblr", cnames: ["tumblr.com"], responseIndicators: ["There's nothing here", "Whatever you were looking for doesn't currently exist"] },
|
|
34
|
+
{ service: "WordPress.com", cnames: ["wordpress.com"], responseIndicators: ["Do you want to register"] },
|
|
35
|
+
{ service: "Ghost", cnames: ["ghost.io"], responseIndicators: ["Site not found"] },
|
|
36
|
+
{ service: "Fastly", cnames: ["fastly.net"], responseIndicators: ["Fastly error: unknown domain"] },
|
|
37
|
+
{ service: "Pantheon", cnames: ["pantheonsite.io"], responseIndicators: ["404 error unknown site"] },
|
|
38
|
+
{ service: "Azure", cnames: ["azurewebsites.net", "cloudapp.azure.com", "trafficmanager.net"], responseIndicators: ["404 Web Site not found"] },
|
|
39
|
+
{ service: "Unbounce", cnames: ["unbouncepages.com"], responseIndicators: ["The requested URL was not found"] },
|
|
40
|
+
{ service: "Cargo", cnames: ["cargocollective.com"], responseIndicators: ["404 Not Found"] },
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
async function getCname(subdomain: string): Promise<string | null> {
|
|
44
|
+
try {
|
|
45
|
+
const { stdout } = await execFileAsync("dig", ["+short", subdomain, "CNAME"], { timeout: 5000 });
|
|
46
|
+
const cname = stdout.trim().replace(/\.$/, "");
|
|
47
|
+
return cname || null;
|
|
48
|
+
} catch {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function checkResponse(url: string): Promise<string | null> {
|
|
54
|
+
try {
|
|
55
|
+
const resp = await fetch(url, {
|
|
56
|
+
signal: AbortSignal.timeout(6000),
|
|
57
|
+
headers: { "User-Agent": "DomainSniper/2.0" },
|
|
58
|
+
});
|
|
59
|
+
const body = await resp.text();
|
|
60
|
+
return body.slice(0, 5000);
|
|
61
|
+
} catch {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export async function detectTakeover(
|
|
67
|
+
domain: string,
|
|
68
|
+
subdomains?: string[]
|
|
69
|
+
): Promise<TakeoverResult> {
|
|
70
|
+
assertValidDomain(domain);
|
|
71
|
+
|
|
72
|
+
const result: TakeoverResult = {
|
|
73
|
+
domain,
|
|
74
|
+
vulnerable: false,
|
|
75
|
+
findings: [],
|
|
76
|
+
checkedSubdomains: 0,
|
|
77
|
+
error: null,
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
// Default subdomains to check
|
|
81
|
+
const toCheck = subdomains || [
|
|
82
|
+
domain,
|
|
83
|
+
`www.${domain}`, `blog.${domain}`, `shop.${domain}`, `app.${domain}`,
|
|
84
|
+
`dev.${domain}`, `staging.${domain}`, `beta.${domain}`, `docs.${domain}`,
|
|
85
|
+
`api.${domain}`, `cdn.${domain}`, `mail.${domain}`, `status.${domain}`,
|
|
86
|
+
`portal.${domain}`, `admin.${domain}`, `help.${domain}`, `support.${domain}`,
|
|
87
|
+
];
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
for (const sub of toCheck) {
|
|
91
|
+
result.checkedSubdomains++;
|
|
92
|
+
const cname = await getCname(sub);
|
|
93
|
+
if (!cname) continue;
|
|
94
|
+
|
|
95
|
+
// Check if CNAME points to a known vulnerable service
|
|
96
|
+
for (const sig of TAKEOVER_SIGNATURES) {
|
|
97
|
+
const matchesCname = sig.cnames.some((c) => cname.toLowerCase().includes(c));
|
|
98
|
+
if (!matchesCname) continue;
|
|
99
|
+
|
|
100
|
+
// Check if the service returns a "not found" indicator
|
|
101
|
+
const body = await checkResponse(`https://${sub}`);
|
|
102
|
+
if (body) {
|
|
103
|
+
const isVulnerable = sig.responseIndicators.some((ind) =>
|
|
104
|
+
body.toLowerCase().includes(ind.toLowerCase())
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
if (isVulnerable) {
|
|
108
|
+
result.vulnerable = true;
|
|
109
|
+
result.findings.push({
|
|
110
|
+
subdomain: sub,
|
|
111
|
+
cname,
|
|
112
|
+
service: sig.service,
|
|
113
|
+
status: "vulnerable",
|
|
114
|
+
detail: `CNAME points to ${sig.service} but the resource is unclaimed`,
|
|
115
|
+
});
|
|
116
|
+
} else {
|
|
117
|
+
result.findings.push({
|
|
118
|
+
subdomain: sub,
|
|
119
|
+
cname,
|
|
120
|
+
service: sig.service,
|
|
121
|
+
status: "safe",
|
|
122
|
+
detail: `CNAME points to ${sig.service} and resource exists`,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
} else {
|
|
126
|
+
// Couldn't connect — could be vulnerable
|
|
127
|
+
result.findings.push({
|
|
128
|
+
subdomain: sub,
|
|
129
|
+
cname,
|
|
130
|
+
service: sig.service,
|
|
131
|
+
status: "potential",
|
|
132
|
+
detail: `CNAME points to ${sig.service} but could not verify (connection failed)`,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return result;
|
|
139
|
+
} catch (err: unknown) {
|
|
140
|
+
result.error = err instanceof Error ? err.message : "Takeover detection failed";
|
|
141
|
+
return result;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { assertValidDomain } from "../validate.js";
|
|
2
|
+
|
|
3
|
+
export interface TechStackResult {
|
|
4
|
+
server: string | null;
|
|
5
|
+
poweredBy: string | null;
|
|
6
|
+
cms: string | null;
|
|
7
|
+
framework: string | null;
|
|
8
|
+
analytics: string[];
|
|
9
|
+
cdn: string | null;
|
|
10
|
+
ssl: string | null;
|
|
11
|
+
technologies: TechDetection[];
|
|
12
|
+
error: string | null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface TechDetection {
|
|
16
|
+
name: string;
|
|
17
|
+
category: string;
|
|
18
|
+
confidence: "high" | "medium" | "low";
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Detection patterns for HTML content
|
|
22
|
+
const HTML_PATTERNS: { name: string; category: string; pattern: RegExp }[] = [
|
|
23
|
+
// CMS
|
|
24
|
+
{ name: "WordPress", category: "CMS", pattern: /wp-content|wp-includes|wordpress/i },
|
|
25
|
+
{ name: "Drupal", category: "CMS", pattern: /drupal|sites\/default\/files/i },
|
|
26
|
+
{ name: "Joomla", category: "CMS", pattern: /joomla|\/media\/system\/js/i },
|
|
27
|
+
{ name: "Shopify", category: "CMS", pattern: /shopify|cdn\.shopify\.com/i },
|
|
28
|
+
{ name: "Squarespace", category: "CMS", pattern: /squarespace|sqsp/i },
|
|
29
|
+
{ name: "Wix", category: "CMS", pattern: /wix\.com|wixstatic\.com/i },
|
|
30
|
+
{ name: "Webflow", category: "CMS", pattern: /webflow/i },
|
|
31
|
+
{ name: "Ghost", category: "CMS", pattern: /ghost\.io|ghost-api/i },
|
|
32
|
+
{ name: "Hugo", category: "CMS", pattern: /hugo-/i },
|
|
33
|
+
// Frameworks
|
|
34
|
+
{ name: "React", category: "Framework", pattern: /react|__next|_next\/static/i },
|
|
35
|
+
{ name: "Next.js", category: "Framework", pattern: /_next\/|__NEXT_DATA__/i },
|
|
36
|
+
{ name: "Vue.js", category: "Framework", pattern: /vue\.js|__vue|nuxt/i },
|
|
37
|
+
{ name: "Nuxt", category: "Framework", pattern: /__nuxt|nuxt\.js/i },
|
|
38
|
+
{ name: "Angular", category: "Framework", pattern: /ng-version|angular/i },
|
|
39
|
+
{ name: "Svelte", category: "Framework", pattern: /svelte/i },
|
|
40
|
+
{ name: "Remix", category: "Framework", pattern: /remix/i },
|
|
41
|
+
{ name: "Astro", category: "Framework", pattern: /astro/i },
|
|
42
|
+
{ name: "Laravel", category: "Framework", pattern: /laravel/i },
|
|
43
|
+
{ name: "Ruby on Rails", category: "Framework", pattern: /csrf-token.*authenticity|ruby/i },
|
|
44
|
+
{ name: "Django", category: "Framework", pattern: /csrfmiddlewaretoken|django/i },
|
|
45
|
+
// Analytics
|
|
46
|
+
{ name: "Google Analytics", category: "Analytics", pattern: /google-analytics|gtag|googletagmanager/i },
|
|
47
|
+
{ name: "Plausible", category: "Analytics", pattern: /plausible\.io/i },
|
|
48
|
+
{ name: "Fathom", category: "Analytics", pattern: /usefathom\.com/i },
|
|
49
|
+
{ name: "Hotjar", category: "Analytics", pattern: /hotjar/i },
|
|
50
|
+
{ name: "Segment", category: "Analytics", pattern: /segment\.com|analytics\.js/i },
|
|
51
|
+
{ name: "Mixpanel", category: "Analytics", pattern: /mixpanel/i },
|
|
52
|
+
// Hosting/CDN
|
|
53
|
+
{ name: "Vercel", category: "Hosting", pattern: /vercel/i },
|
|
54
|
+
{ name: "Netlify", category: "Hosting", pattern: /netlify/i },
|
|
55
|
+
{ name: "AWS", category: "Hosting", pattern: /amazonaws\.com/i },
|
|
56
|
+
{ name: "Google Cloud", category: "Hosting", pattern: /googleapis\.com|gstatic\.com/i },
|
|
57
|
+
// Other
|
|
58
|
+
{ name: "jQuery", category: "Library", pattern: /jquery/i },
|
|
59
|
+
{ name: "Bootstrap", category: "Library", pattern: /bootstrap/i },
|
|
60
|
+
{ name: "Tailwind CSS", category: "Library", pattern: /tailwindcss|tailwind/i },
|
|
61
|
+
{ name: "Stripe", category: "Payment", pattern: /stripe\.com|stripe\.js/i },
|
|
62
|
+
{ name: "Intercom", category: "Support", pattern: /intercom/i },
|
|
63
|
+
{ name: "Crisp", category: "Support", pattern: /crisp\.chat/i },
|
|
64
|
+
{ name: "Cloudflare", category: "CDN", pattern: /cloudflare/i },
|
|
65
|
+
];
|
|
66
|
+
|
|
67
|
+
// Header-based detection
|
|
68
|
+
const HEADER_PATTERNS: { header: string; name: string; category: string; pattern?: RegExp }[] = [
|
|
69
|
+
{ header: "x-powered-by", name: "Express", category: "Framework", pattern: /express/i },
|
|
70
|
+
{ header: "x-powered-by", name: "PHP", category: "Language", pattern: /php/i },
|
|
71
|
+
{ header: "x-powered-by", name: "ASP.NET", category: "Framework", pattern: /asp\.net/i },
|
|
72
|
+
{ header: "x-powered-by", name: "Next.js", category: "Framework", pattern: /next/i },
|
|
73
|
+
{ header: "server", name: "nginx", category: "Server", pattern: /nginx/i },
|
|
74
|
+
{ header: "server", name: "Apache", category: "Server", pattern: /apache/i },
|
|
75
|
+
{ header: "server", name: "Cloudflare", category: "CDN", pattern: /cloudflare/i },
|
|
76
|
+
{ header: "server", name: "LiteSpeed", category: "Server", pattern: /litespeed/i },
|
|
77
|
+
{ header: "server", name: "Caddy", category: "Server", pattern: /caddy/i },
|
|
78
|
+
{ header: "x-vercel-id", name: "Vercel", category: "Hosting" },
|
|
79
|
+
{ header: "x-nf-request-id", name: "Netlify", category: "Hosting" },
|
|
80
|
+
{ header: "fly-request-id", name: "Fly.io", category: "Hosting" },
|
|
81
|
+
{ header: "cf-ray", name: "Cloudflare", category: "CDN" },
|
|
82
|
+
{ header: "x-cache", name: "CDN Cache", category: "CDN" },
|
|
83
|
+
];
|
|
84
|
+
|
|
85
|
+
export async function detectTechStack(domain: string): Promise<TechStackResult> {
|
|
86
|
+
assertValidDomain(domain);
|
|
87
|
+
|
|
88
|
+
const result: TechStackResult = {
|
|
89
|
+
server: null, poweredBy: null, cms: null, framework: null,
|
|
90
|
+
analytics: [], cdn: null, ssl: null, technologies: [], error: null,
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
const resp = await fetch(`https://${domain}`, {
|
|
95
|
+
signal: AbortSignal.timeout(10000),
|
|
96
|
+
headers: { "User-Agent": "DomainSniper/2.0" },
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// Extract from headers
|
|
100
|
+
result.server = resp.headers.get("server");
|
|
101
|
+
result.poweredBy = resp.headers.get("x-powered-by");
|
|
102
|
+
|
|
103
|
+
for (const hp of HEADER_PATTERNS) {
|
|
104
|
+
const val = resp.headers.get(hp.header);
|
|
105
|
+
if (val && (!hp.pattern || hp.pattern.test(val))) {
|
|
106
|
+
const exists = result.technologies.some((t) => t.name === hp.name);
|
|
107
|
+
if (!exists) {
|
|
108
|
+
result.technologies.push({ name: hp.name, category: hp.category, confidence: "high" });
|
|
109
|
+
}
|
|
110
|
+
if (hp.category === "CDN" && !result.cdn) result.cdn = hp.name;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Parse HTML body for patterns
|
|
115
|
+
const body = await resp.text();
|
|
116
|
+
const seen = new Set<string>();
|
|
117
|
+
|
|
118
|
+
for (const pat of HTML_PATTERNS) {
|
|
119
|
+
if (pat.pattern.test(body) && !seen.has(pat.name)) {
|
|
120
|
+
seen.add(pat.name);
|
|
121
|
+
result.technologies.push({ name: pat.name, category: pat.category, confidence: "medium" });
|
|
122
|
+
|
|
123
|
+
if (pat.category === "CMS" && !result.cms) result.cms = pat.name;
|
|
124
|
+
if (pat.category === "Framework" && !result.framework) result.framework = pat.name;
|
|
125
|
+
if (pat.category === "Analytics") result.analytics.push(pat.name);
|
|
126
|
+
if (pat.category === "CDN" && !result.cdn) result.cdn = pat.name;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return result;
|
|
131
|
+
} catch (err: unknown) {
|
|
132
|
+
result.error = err instanceof Error ? err.message : "Tech detection failed";
|
|
133
|
+
return result;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TLD Expansion — enter "coolstartup" and check across all popular TLDs
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export const POPULAR_TLDS = [
|
|
6
|
+
"com", "io", "dev", "ai", "app", "co", "net", "org",
|
|
7
|
+
"xyz", "me", "tech", "so", "sh", "gg", "cc", "to",
|
|
8
|
+
"cloud", "run", "live", "site", "online", "store",
|
|
9
|
+
] as const;
|
|
10
|
+
|
|
11
|
+
export const PREMIUM_TLDS = [
|
|
12
|
+
"com", "io", "dev", "ai", "app", "co",
|
|
13
|
+
] as const;
|
|
14
|
+
|
|
15
|
+
export const STARTUP_TLDS = [
|
|
16
|
+
"com", "io", "dev", "ai", "app", "co", "so", "sh", "gg", "run",
|
|
17
|
+
] as const;
|
|
18
|
+
|
|
19
|
+
export type TldPreset = "popular" | "premium" | "startup" | "all";
|
|
20
|
+
|
|
21
|
+
export function expandTlds(
|
|
22
|
+
baseName: string,
|
|
23
|
+
preset: TldPreset = "popular",
|
|
24
|
+
customTlds?: string[]
|
|
25
|
+
): string[] {
|
|
26
|
+
// Strip any existing TLD
|
|
27
|
+
const name = baseName.replace(/\.[a-z]+$/i, "").trim().toLowerCase();
|
|
28
|
+
if (!name) return [];
|
|
29
|
+
|
|
30
|
+
if (customTlds && customTlds.length > 0) {
|
|
31
|
+
return customTlds.map((tld) => `${name}.${tld.replace(/^\./, "")}`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
let tlds: readonly string[];
|
|
35
|
+
switch (preset) {
|
|
36
|
+
case "premium": tlds = PREMIUM_TLDS; break;
|
|
37
|
+
case "startup": tlds = STARTUP_TLDS; break;
|
|
38
|
+
case "all": tlds = POPULAR_TLDS; break;
|
|
39
|
+
default: tlds = POPULAR_TLDS;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return tlds.map((tld) => `${name}.${tld}`);
|
|
43
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typo & variation generator — check misspellings, hyphens, plurals, prefixes
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export interface VariationOptions {
|
|
6
|
+
plurals?: boolean;
|
|
7
|
+
hyphens?: boolean;
|
|
8
|
+
prefixes?: boolean;
|
|
9
|
+
suffixes?: boolean;
|
|
10
|
+
typos?: boolean;
|
|
11
|
+
abbreviations?: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const DEFAULT_OPTIONS: VariationOptions = {
|
|
15
|
+
plurals: true,
|
|
16
|
+
hyphens: true,
|
|
17
|
+
prefixes: true,
|
|
18
|
+
suffixes: true,
|
|
19
|
+
typos: true,
|
|
20
|
+
abbreviations: true,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
// Common keyboard-adjacent typos
|
|
24
|
+
const TYPO_MAP: Record<string, string[]> = {
|
|
25
|
+
a: ["s", "q", "z"], b: ["v", "n", "g"], c: ["x", "v", "d"],
|
|
26
|
+
d: ["s", "f", "e"], e: ["w", "r", "d"], f: ["d", "g", "r"],
|
|
27
|
+
g: ["f", "h", "t"], h: ["g", "j", "y"], i: ["u", "o", "k"],
|
|
28
|
+
j: ["h", "k", "u"], k: ["j", "l", "i"], l: ["k", "o", "p"],
|
|
29
|
+
m: ["n", "k"], n: ["b", "m", "h"], o: ["i", "p", "l"],
|
|
30
|
+
p: ["o", "l"], q: ["w", "a"], r: ["e", "t", "f"],
|
|
31
|
+
s: ["a", "d", "w"], t: ["r", "y", "g"], u: ["y", "i", "j"],
|
|
32
|
+
v: ["c", "b", "f"], w: ["q", "e", "s"], x: ["z", "c", "s"],
|
|
33
|
+
y: ["t", "u", "h"], z: ["a", "x", "s"],
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const PREFIXES = ["get", "my", "the", "go", "try", "use", "hey"];
|
|
37
|
+
const SUFFIXES = ["app", "hq", "io", "lab", "hub", "ly", "ify", "ize"];
|
|
38
|
+
|
|
39
|
+
export function generateVariations(
|
|
40
|
+
domain: string,
|
|
41
|
+
options: VariationOptions = DEFAULT_OPTIONS
|
|
42
|
+
): string[] {
|
|
43
|
+
const parts = domain.split(".");
|
|
44
|
+
const tld = parts.slice(1).join(".") || "com";
|
|
45
|
+
const name = parts[0]!.toLowerCase();
|
|
46
|
+
const variations = new Set<string>();
|
|
47
|
+
|
|
48
|
+
// Plurals
|
|
49
|
+
if (options.plurals) {
|
|
50
|
+
if (name.endsWith("s")) {
|
|
51
|
+
variations.add(`${name.slice(0, -1)}.${tld}`);
|
|
52
|
+
} else {
|
|
53
|
+
variations.add(`${name}s.${tld}`);
|
|
54
|
+
}
|
|
55
|
+
if (name.endsWith("y")) {
|
|
56
|
+
variations.add(`${name.slice(0, -1)}ies.${tld}`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Hyphens — split camelCase or compound words
|
|
61
|
+
if (options.hyphens) {
|
|
62
|
+
// Add hyphen at common word boundaries
|
|
63
|
+
for (let i = 2; i < name.length - 1; i++) {
|
|
64
|
+
const left = name.slice(0, i);
|
|
65
|
+
const right = name.slice(i);
|
|
66
|
+
if (left.length >= 2 && right.length >= 2) {
|
|
67
|
+
variations.add(`${left}-${right}.${tld}`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
// Remove existing hyphens
|
|
71
|
+
if (name.includes("-")) {
|
|
72
|
+
variations.add(`${name.replace(/-/g, "")}.${tld}`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Prefixes
|
|
77
|
+
if (options.prefixes) {
|
|
78
|
+
for (const prefix of PREFIXES) {
|
|
79
|
+
if (!name.startsWith(prefix)) {
|
|
80
|
+
variations.add(`${prefix}${name}.${tld}`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Suffixes
|
|
86
|
+
if (options.suffixes) {
|
|
87
|
+
for (const suffix of SUFFIXES) {
|
|
88
|
+
if (!name.endsWith(suffix)) {
|
|
89
|
+
variations.add(`${name}${suffix}.${tld}`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Typos — swap adjacent chars, drop chars, double chars
|
|
95
|
+
if (options.typos) {
|
|
96
|
+
// Character swaps
|
|
97
|
+
for (let i = 0; i < name.length - 1; i++) {
|
|
98
|
+
const swapped = name.slice(0, i) + name[i + 1] + name[i] + name.slice(i + 2);
|
|
99
|
+
variations.add(`${swapped}.${tld}`);
|
|
100
|
+
}
|
|
101
|
+
// Character drops
|
|
102
|
+
for (let i = 0; i < name.length; i++) {
|
|
103
|
+
if (name.length > 2) {
|
|
104
|
+
const dropped = name.slice(0, i) + name.slice(i + 1);
|
|
105
|
+
variations.add(`${dropped}.${tld}`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
// Keyboard-adjacent substitutions (limit to first 3)
|
|
109
|
+
let typoCount = 0;
|
|
110
|
+
for (let i = 0; i < name.length && typoCount < 3; i++) {
|
|
111
|
+
const char = name[i]!;
|
|
112
|
+
const adjacent = TYPO_MAP[char];
|
|
113
|
+
if (adjacent) {
|
|
114
|
+
const sub = name.slice(0, i) + adjacent[0] + name.slice(i + 1);
|
|
115
|
+
variations.add(`${sub}.${tld}`);
|
|
116
|
+
typoCount++;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Abbreviations
|
|
122
|
+
if (options.abbreviations) {
|
|
123
|
+
// Remove vowels
|
|
124
|
+
const noVowels = name.replace(/[aeiou]/g, "");
|
|
125
|
+
if (noVowels.length >= 2 && noVowels !== name) {
|
|
126
|
+
variations.add(`${noVowels}.${tld}`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Remove the original domain
|
|
131
|
+
variations.delete(domain);
|
|
132
|
+
|
|
133
|
+
return Array.from(variations);
|
|
134
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Check if a newer version of domain-sniper is available.
|
|
3
|
+
* Uses Bun.semver for version comparison.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { join } from "path";
|
|
7
|
+
import { readFileSync } from "fs";
|
|
8
|
+
|
|
9
|
+
export function getCurrentVersion(): string {
|
|
10
|
+
try {
|
|
11
|
+
const raw = readFileSync(join(import.meta.dir, "../../../package.json"), "utf-8");
|
|
12
|
+
const pkg = JSON.parse(raw) as { version?: string };
|
|
13
|
+
return pkg.version || "0.0.0";
|
|
14
|
+
} catch {
|
|
15
|
+
return "2.0.0";
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function checkForUpdates(): Promise<{
|
|
20
|
+
current: string;
|
|
21
|
+
latest: string | null;
|
|
22
|
+
updateAvailable: boolean;
|
|
23
|
+
error: string | null;
|
|
24
|
+
}> {
|
|
25
|
+
const current = getCurrentVersion();
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
const resp = await fetch("https://registry.npmjs.org/domain-sniper/latest", {
|
|
29
|
+
signal: AbortSignal.timeout(5000),
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
if (!resp.ok) {
|
|
33
|
+
return { current, latest: null, updateAvailable: false, error: null };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const data = (await resp.json()) as { version?: string };
|
|
37
|
+
const latest = data.version || null;
|
|
38
|
+
|
|
39
|
+
if (!latest) {
|
|
40
|
+
return { current, latest: null, updateAvailable: false, error: null };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const updateAvailable = Bun.semver.order(current, latest) < 0;
|
|
44
|
+
|
|
45
|
+
return { current, latest, updateAvailable, error: null };
|
|
46
|
+
} catch (err: unknown) {
|
|
47
|
+
return {
|
|
48
|
+
current,
|
|
49
|
+
latest: null,
|
|
50
|
+
updateAvailable: false,
|
|
51
|
+
error: err instanceof Error ? err.message : "Check failed",
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function formatUpdateMessage(current: string, latest: string): string {
|
|
57
|
+
return `Update available: ${current} -> ${latest}\nRun: bun update -g domain-sniper`;
|
|
58
|
+
}
|