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,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
+ }