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,91 @@
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 AsnResult {
8
+ domain: string;
9
+ ip: string | null;
10
+ asn: string | null;
11
+ asnName: string | null;
12
+ org: string | null;
13
+ country: string | null;
14
+ city: string | null;
15
+ region: string | null;
16
+ isp: string | null;
17
+ error: string | null;
18
+ }
19
+
20
+ export async function lookupAsn(domain: string): Promise<AsnResult> {
21
+ assertValidDomain(domain);
22
+
23
+ const result: AsnResult = {
24
+ domain, ip: null, asn: null, asnName: null,
25
+ org: null, country: null, city: null, region: null, isp: null, error: null,
26
+ };
27
+
28
+ try {
29
+ // Resolve IP
30
+ try {
31
+ const { stdout } = await execFileAsync("dig", ["+short", domain, "A"], { timeout: 5000 });
32
+ result.ip = stdout.trim().split("\n")[0] || null;
33
+ } catch {}
34
+
35
+ if (!result.ip) {
36
+ result.error = "Could not resolve IP";
37
+ return result;
38
+ }
39
+
40
+ // Use ip-api.com (free, 45 req/min, no key needed)
41
+ try {
42
+ const resp = await fetch(
43
+ `http://ip-api.com/json/${encodeURIComponent(result.ip)}?fields=status,country,regionName,city,isp,org,as,asname`,
44
+ { signal: AbortSignal.timeout(8000) }
45
+ );
46
+ const data = await resp.json() as {
47
+ status?: string;
48
+ country?: string;
49
+ regionName?: string;
50
+ city?: string;
51
+ isp?: string;
52
+ org?: string;
53
+ as?: string;
54
+ asname?: string;
55
+ };
56
+
57
+ if (data.status === "success") {
58
+ result.country = data.country || null;
59
+ result.region = data.regionName || null;
60
+ result.city = data.city || null;
61
+ result.isp = data.isp || null;
62
+ result.org = data.org || null;
63
+ result.asn = data.as || null;
64
+ result.asnName = data.asname || null;
65
+ }
66
+ } catch {}
67
+
68
+ // Fallback: DNS-based ASN lookup via Team Cymru
69
+ if (!result.asn && result.ip) {
70
+ try {
71
+ const reversed = result.ip.split(".").reverse().join(".");
72
+ const { stdout } = await execFileAsync(
73
+ "dig", ["+short", `${reversed}.origin.asn.cymru.com`, "TXT"],
74
+ { timeout: 5000 }
75
+ );
76
+ const txt = stdout.trim().replace(/"/g, "");
77
+ // Format: "ASN | IP/Prefix | CC | Registry | Date"
78
+ const parts = txt.split("|").map((p) => p.trim());
79
+ if (parts.length >= 3) {
80
+ result.asn = parts[0] ? `AS${parts[0]}` : null;
81
+ result.country = parts[2] || null;
82
+ }
83
+ } catch {}
84
+ }
85
+
86
+ return result;
87
+ } catch (err: unknown) {
88
+ result.error = err instanceof Error ? err.message : "ASN lookup failed";
89
+ return result;
90
+ }
91
+ }
@@ -0,0 +1,83 @@
1
+ import { assertValidDomain } from "../validate.js";
2
+
3
+ export interface BacklinkResult {
4
+ domain: string;
5
+ estimatedBacklinks: number | null;
6
+ pageRank: number | null;
7
+ commonCrawlPages: number | null;
8
+ sources: BacklinkSource[];
9
+ error: string | null;
10
+ }
11
+
12
+ export interface BacklinkSource {
13
+ name: string;
14
+ value: number | null;
15
+ error: string | null;
16
+ }
17
+
18
+ async function checkCommonCrawl(domain: string): Promise<BacklinkSource> {
19
+ try {
20
+ // Use Common Crawl index API to estimate pages
21
+ const resp = await fetch(
22
+ `https://index.commoncrawl.org/CC-MAIN-2025-51-index?url=*.${encodeURIComponent(domain)}&output=json&limit=1&showNumPages=true`,
23
+ { signal: AbortSignal.timeout(10000) }
24
+ );
25
+ if (!resp.ok) {
26
+ return { name: "CommonCrawl", value: null, error: `HTTP ${resp.status}` };
27
+ }
28
+ const text = await resp.text();
29
+ // The showNumPages response is a single number
30
+ const pages = parseInt(text.trim(), 10);
31
+ if (!isNaN(pages)) {
32
+ return { name: "CommonCrawl", value: pages, error: null };
33
+ }
34
+ // Try parsing as JSON lines
35
+ const lines = text.trim().split("\n").filter(Boolean);
36
+ return { name: "CommonCrawl", value: lines.length, error: null };
37
+ } catch (err: unknown) {
38
+ return { name: "CommonCrawl", value: null, error: err instanceof Error ? err.message : "Failed" };
39
+ }
40
+ }
41
+
42
+ async function checkOpenPageRank(domain: string): Promise<BacklinkSource> {
43
+ try {
44
+ // Open PageRank - free API, no key needed for basic lookups
45
+ const resp = await fetch(
46
+ `https://openpagerank.com/api/v1.0/getPageRank?domains[]=${encodeURIComponent(domain)}`,
47
+ {
48
+ signal: AbortSignal.timeout(8000),
49
+ headers: { "User-Agent": "DomainSniper/2.0" },
50
+ }
51
+ );
52
+ if (!resp.ok) {
53
+ return { name: "OpenPageRank", value: null, error: `HTTP ${resp.status}` };
54
+ }
55
+ const data = await resp.json() as {
56
+ status_code?: number;
57
+ response?: Array<{ page_rank_decimal?: number; rank?: number }>;
58
+ };
59
+ const first = data.response?.[0];
60
+ const rank = first?.page_rank_decimal ?? first?.rank ?? null;
61
+ return { name: "OpenPageRank", value: typeof rank === "number" ? rank : null, error: null };
62
+ } catch (err: unknown) {
63
+ return { name: "OpenPageRank", value: null, error: err instanceof Error ? err.message : "Failed" };
64
+ }
65
+ }
66
+
67
+ export async function estimateBacklinks(domain: string): Promise<BacklinkResult> {
68
+ assertValidDomain(domain);
69
+
70
+ const [ccResult, prResult] = await Promise.all([
71
+ checkCommonCrawl(domain),
72
+ checkOpenPageRank(domain),
73
+ ]);
74
+
75
+ return {
76
+ domain,
77
+ estimatedBacklinks: ccResult.value,
78
+ pageRank: prResult.value,
79
+ commonCrawlPages: ccResult.value,
80
+ sources: [ccResult, prResult],
81
+ error: null,
82
+ };
83
+ }
@@ -0,0 +1,67 @@
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 BlacklistResult {
8
+ domain: string;
9
+ listed: boolean;
10
+ lists: BlacklistEntry[];
11
+ cleanCount: number;
12
+ listedCount: number;
13
+ }
14
+
15
+ export interface BlacklistEntry {
16
+ name: string;
17
+ listed: boolean;
18
+ detail: string | null;
19
+ }
20
+
21
+ // DNS-based blacklists for domain reputation
22
+ const DOMAIN_BLACKLISTS = [
23
+ { name: "Spamhaus DBL", suffix: "dbl.spamhaus.org" },
24
+ { name: "SURBL", suffix: "multi.surbl.org" },
25
+ { name: "URIBL", suffix: "multi.uribl.com" },
26
+ { name: "Spamhaus ZEN", suffix: "zen.spamhaus.org" },
27
+ { name: "Barracuda", suffix: "b.barracudacentral.org" },
28
+ { name: "SpamCop", suffix: "bl.spamcop.net" },
29
+ { name: "PhishTank", suffix: "phishtank.org" },
30
+ { name: "SORBS", suffix: "dnsbl.sorbs.net" },
31
+ ];
32
+
33
+ async function checkBlacklist(
34
+ domain: string,
35
+ bl: { name: string; suffix: string }
36
+ ): Promise<BlacklistEntry> {
37
+ try {
38
+ const query = `${domain}.${bl.suffix}`;
39
+ const { stdout } = await execFileAsync("dig", ["+short", query, "A"], { timeout: 5000 });
40
+ const result = stdout.trim();
41
+ // A response (typically 127.0.0.x) means listed
42
+ if (result && result.startsWith("127.")) {
43
+ return { name: bl.name, listed: true, detail: result };
44
+ }
45
+ return { name: bl.name, listed: false, detail: null };
46
+ } catch {
47
+ // NXDOMAIN or timeout = not listed (which is good)
48
+ return { name: bl.name, listed: false, detail: null };
49
+ }
50
+ }
51
+
52
+ export async function checkBlacklists(domain: string): Promise<BlacklistResult> {
53
+ assertValidDomain(domain);
54
+
55
+ const results = await Promise.all(
56
+ DOMAIN_BLACKLISTS.map((bl) => checkBlacklist(domain, bl))
57
+ );
58
+
59
+ const listedCount = results.filter((r) => r.listed).length;
60
+ return {
61
+ domain,
62
+ listed: listedCount > 0,
63
+ lists: results,
64
+ cleanCount: results.length - listedCount,
65
+ listedCount,
66
+ };
67
+ }
@@ -0,0 +1,87 @@
1
+ import { assertValidDomain } from "../validate.js";
2
+
3
+ export interface CertTransparencyResult {
4
+ domain: string;
5
+ subdomains: string[];
6
+ certificates: CertEntry[];
7
+ totalCerts: number;
8
+ error: string | null;
9
+ }
10
+
11
+ export interface CertEntry {
12
+ commonName: string;
13
+ issuer: string;
14
+ notBefore: string;
15
+ notAfter: string;
16
+ }
17
+
18
+ export async function queryCertTransparency(domain: string): Promise<CertTransparencyResult> {
19
+ assertValidDomain(domain);
20
+
21
+ const result: CertTransparencyResult = {
22
+ domain,
23
+ subdomains: [],
24
+ certificates: [],
25
+ totalCerts: 0,
26
+ error: null,
27
+ };
28
+
29
+ try {
30
+ const resp = await fetch(
31
+ `https://crt.sh/?q=%25.${encodeURIComponent(domain)}&output=json`,
32
+ { signal: AbortSignal.timeout(15000) }
33
+ );
34
+
35
+ if (!resp.ok) {
36
+ result.error = `crt.sh returned HTTP ${resp.status}`;
37
+ return result;
38
+ }
39
+
40
+ const data = await resp.json() as Array<{
41
+ common_name?: string;
42
+ name_value?: string;
43
+ issuer_name?: string;
44
+ not_before?: string;
45
+ not_after?: string;
46
+ }>;
47
+
48
+ result.totalCerts = data.length;
49
+
50
+ // Extract unique subdomains
51
+ const subdomainSet = new Set<string>();
52
+ for (const cert of data) {
53
+ if (cert.common_name) {
54
+ const cn = cert.common_name.toLowerCase().replace(/^\*\./, "");
55
+ if (cn.endsWith(domain) || cn === domain) subdomainSet.add(cn);
56
+ }
57
+ if (cert.name_value) {
58
+ const names = cert.name_value.split("\n");
59
+ for (const name of names) {
60
+ const clean = name.trim().toLowerCase().replace(/^\*\./, "");
61
+ if (clean.endsWith(domain) || clean === domain) subdomainSet.add(clean);
62
+ }
63
+ }
64
+ }
65
+
66
+ result.subdomains = Array.from(subdomainSet).sort();
67
+
68
+ // Keep recent certs (deduplicated by common name)
69
+ const seen = new Set<string>();
70
+ for (const cert of data.slice(0, 50)) {
71
+ const cn = cert.common_name || "";
72
+ if (seen.has(cn)) continue;
73
+ seen.add(cn);
74
+ result.certificates.push({
75
+ commonName: cn,
76
+ issuer: cert.issuer_name || "",
77
+ notBefore: cert.not_before || "",
78
+ notAfter: cert.not_after || "",
79
+ });
80
+ }
81
+
82
+ return result;
83
+ } catch (err: unknown) {
84
+ result.error = err instanceof Error ? err.message : "Certificate transparency lookup failed";
85
+ return result;
86
+ }
87
+ }
@@ -0,0 +1,81 @@
1
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
2
+ import type { RegistrarProvider } from "../registrar.js";
3
+ import { APP_DIR, CONFIG_FILE } from "../paths.js";
4
+
5
+ const CONFIG_DIR = APP_DIR;
6
+
7
+ export interface DomainSniperConfig {
8
+ concurrency: number;
9
+ rateLimitMs: number;
10
+ defaultTldPreset: "popular" | "premium" | "startup" | "all";
11
+ registrar: {
12
+ provider: RegistrarProvider;
13
+ apiKey: string;
14
+ apiSecret: string;
15
+ accountId: string;
16
+ username: string;
17
+ clientIp: string;
18
+ } | null;
19
+ notifications: {
20
+ webhookUrl: string | null;
21
+ emailTo: string | null;
22
+ smtpHost: string | null;
23
+ smtpPort: number;
24
+ smtpUser: string | null;
25
+ smtpPass: string | null;
26
+ };
27
+ watch: {
28
+ intervalMs: number;
29
+ desktopNotify: boolean;
30
+ };
31
+ }
32
+
33
+ const DEFAULT_CONFIG: DomainSniperConfig = {
34
+ concurrency: 5,
35
+ rateLimitMs: 500,
36
+ defaultTldPreset: "popular",
37
+ registrar: null,
38
+ notifications: {
39
+ webhookUrl: null,
40
+ emailTo: null,
41
+ smtpHost: null,
42
+ smtpPort: 587,
43
+ smtpUser: null,
44
+ smtpPass: null,
45
+ },
46
+ watch: {
47
+ intervalMs: 3600000,
48
+ desktopNotify: true,
49
+ },
50
+ };
51
+
52
+ function ensureDir(): void {
53
+ if (!existsSync(CONFIG_DIR)) {
54
+ mkdirSync(CONFIG_DIR, { recursive: true });
55
+ }
56
+ }
57
+
58
+ export function loadConfig(): DomainSniperConfig {
59
+ ensureDir();
60
+ if (!existsSync(CONFIG_FILE)) return { ...DEFAULT_CONFIG };
61
+ try {
62
+ const content = readFileSync(CONFIG_FILE, "utf-8");
63
+ const parsed = JSON.parse(content);
64
+ return { ...DEFAULT_CONFIG, ...parsed };
65
+ } catch {
66
+ return { ...DEFAULT_CONFIG };
67
+ }
68
+ }
69
+
70
+ export function saveConfig(config: DomainSniperConfig): void {
71
+ ensureDir();
72
+ writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), "utf-8");
73
+ }
74
+
75
+ export function getConfigPath(): string {
76
+ return CONFIG_FILE;
77
+ }
78
+
79
+ export function resetConfig(): void {
80
+ saveConfig(DEFAULT_CONFIG);
81
+ }
@@ -0,0 +1,90 @@
1
+ import { assertValidDomain } from "../validate.js";
2
+
3
+ export interface CorsResult {
4
+ domain: string;
5
+ vulnerable: boolean;
6
+ findings: CorsFinding[];
7
+ error: string | null;
8
+ }
9
+
10
+ export interface CorsFinding {
11
+ test: string;
12
+ origin: string;
13
+ allowed: boolean;
14
+ credentials: boolean;
15
+ severity: "critical" | "high" | "medium" | "low" | "info";
16
+ detail: string;
17
+ }
18
+
19
+ const CORS_TESTS: { name: string; origin: string; severity: "critical" | "high" | "medium" }[] = [
20
+ { name: "Wildcard origin", origin: "https://evil.com", severity: "critical" },
21
+ { name: "Null origin", origin: "null", severity: "high" },
22
+ { name: "Subdomain reflection", origin: "https://evil.TARGET", severity: "high" },
23
+ { name: "Prefix match bypass", origin: "https://TARGETevil.com", severity: "medium" },
24
+ { name: "Suffix match bypass", origin: "https://evil-TARGET", severity: "medium" },
25
+ { name: "HTTP downgrade", origin: "http://TARGET", severity: "medium" },
26
+ ];
27
+
28
+ export async function checkCors(domain: string): Promise<CorsResult> {
29
+ assertValidDomain(domain);
30
+
31
+ const result: CorsResult = {
32
+ domain, vulnerable: false, findings: [], error: null,
33
+ };
34
+
35
+ try {
36
+ for (const test of CORS_TESTS) {
37
+ const origin = test.origin
38
+ .replace(/TARGET/g, domain);
39
+
40
+ try {
41
+ const resp = await fetch(`https://${domain}`, {
42
+ signal: AbortSignal.timeout(8000),
43
+ headers: {
44
+ "Origin": origin,
45
+ "User-Agent": "DomainSniper/2.0",
46
+ },
47
+ });
48
+
49
+ const acao = resp.headers.get("access-control-allow-origin");
50
+ const acac = resp.headers.get("access-control-allow-credentials");
51
+ const allowed = acao === origin || acao === "*";
52
+ const credentials = acac === "true";
53
+
54
+ if (allowed) {
55
+ const finding: CorsFinding = {
56
+ test: test.name,
57
+ origin,
58
+ allowed: true,
59
+ credentials,
60
+ severity: credentials ? "critical" : test.severity,
61
+ detail: credentials
62
+ ? `Reflects origin ${origin} WITH credentials — full account takeover possible`
63
+ : `Reflects origin ${origin} (no credentials)`,
64
+ };
65
+
66
+ result.findings.push(finding);
67
+ if (credentials || test.severity === "critical") {
68
+ result.vulnerable = true;
69
+ }
70
+ } else if (acao === "*") {
71
+ result.findings.push({
72
+ test: test.name,
73
+ origin,
74
+ allowed: true,
75
+ credentials: false,
76
+ severity: "medium",
77
+ detail: "Wildcard ACAO (*) — allows any origin to read responses",
78
+ });
79
+ }
80
+ } catch {
81
+ // Connection error — skip this test
82
+ }
83
+ }
84
+
85
+ return result;
86
+ } catch (err: unknown) {
87
+ result.error = err instanceof Error ? err.message : "CORS check failed";
88
+ return result;
89
+ }
90
+ }
@@ -0,0 +1,27 @@
1
+ import { execFile } from "child_process";
2
+ import { promisify } from "util";
3
+ import { assertValidDomain } from "../validate.js";
4
+ import type { DnsDetails } from "../types.js";
5
+
6
+ const execFileAsync = promisify(execFile);
7
+
8
+ async function digQuery(domain: string, type: string): Promise<string[]> {
9
+ try {
10
+ const { stdout } = await execFileAsync("dig", ["+short", domain, type], { timeout: 10000 });
11
+ return stdout.trim().split("\n").filter(Boolean);
12
+ } catch {
13
+ return [];
14
+ }
15
+ }
16
+
17
+ export async function lookupDns(domain: string): Promise<DnsDetails> {
18
+ assertValidDomain(domain);
19
+ const [a, aaaa, mx, txt, cname] = await Promise.all([
20
+ digQuery(domain, "A"),
21
+ digQuery(domain, "AAAA"),
22
+ digQuery(domain, "MX"),
23
+ digQuery(domain, "TXT"),
24
+ digQuery(domain, "CNAME"),
25
+ ]);
26
+ return { a, aaaa, mx, txt, cname };
27
+ }
@@ -0,0 +1,33 @@
1
+ export function calculateDomainAge(createdDate: string | null): string | null {
2
+ if (!createdDate) return null;
3
+ try {
4
+ const created = new Date(createdDate);
5
+ if (isNaN(created.getTime())) return null;
6
+ const now = new Date();
7
+ const diffMs = now.getTime() - created.getTime();
8
+ if (diffMs < 0) return "Not yet created";
9
+ const days = Math.floor(diffMs / 86400000);
10
+ if (days < 1) return "< 1 day";
11
+ if (days < 30) return `${days}d`;
12
+ if (days < 365) {
13
+ const months = Math.floor(days / 30);
14
+ return `${months}mo`;
15
+ }
16
+ const years = Math.floor(days / 365);
17
+ const remainingMonths = Math.floor((days % 365) / 30);
18
+ return remainingMonths > 0 ? `${years}y ${remainingMonths}mo` : `${years}y`;
19
+ } catch {
20
+ return null;
21
+ }
22
+ }
23
+
24
+ export function daysUntilExpiry(expiryDate: string | null): number | null {
25
+ if (!expiryDate) return null;
26
+ try {
27
+ const expiry = new Date(expiryDate);
28
+ if (isNaN(expiry.getTime())) return null;
29
+ return Math.floor((expiry.getTime() - Date.now()) / 86400000);
30
+ } catch {
31
+ return null;
32
+ }
33
+ }
@@ -0,0 +1,87 @@
1
+ const PREFIXES = [
2
+ "get", "try", "use", "go", "my", "hey", "the",
3
+ "super", "hyper", "ultra", "mega", "meta", "neo",
4
+ "re", "un", "co", "ai",
5
+ ];
6
+
7
+ const SUFFIXES = [
8
+ "app", "hq", "hub", "lab", "labs", "io", "ly",
9
+ "ify", "ize", "ful", "box", "kit", "now",
10
+ "run", "dev", "ops", "ai", "x", "up",
11
+ ];
12
+
13
+ const TECH_WORDS = [
14
+ "sync", "flow", "stack", "link", "dash", "grid",
15
+ "node", "edge", "core", "loop", "ping", "bolt",
16
+ "wave", "spark", "cloud", "beam", "data", "byte",
17
+ "pixel", "craft", "forge", "vault", "pulse", "shift",
18
+ ];
19
+
20
+ export interface Suggestion {
21
+ name: string;
22
+ domain: string;
23
+ strategy: string;
24
+ }
25
+
26
+ export function generateSuggestions(
27
+ keyword: string,
28
+ tld: string = "com",
29
+ maxResults: number = 30
30
+ ): Suggestion[] {
31
+ const word = keyword.toLowerCase().replace(/[^a-z0-9]/g, "");
32
+ if (!word) return [];
33
+
34
+ const suggestions: Suggestion[] = [];
35
+ const seen = new Set<string>();
36
+
37
+ function add(name: string, strategy: string) {
38
+ const domain = `${name}.${tld}`;
39
+ if (!seen.has(domain) && name.length >= 3 && name.length <= 20) {
40
+ seen.add(domain);
41
+ suggestions.push({ name, domain, strategy });
42
+ }
43
+ }
44
+
45
+ // Prefix combinations
46
+ for (const prefix of PREFIXES) {
47
+ if (suggestions.length >= maxResults) break;
48
+ add(`${prefix}${word}`, `prefix: ${prefix}+`);
49
+ }
50
+
51
+ // Suffix combinations
52
+ for (const suffix of SUFFIXES) {
53
+ if (suggestions.length >= maxResults) break;
54
+ add(`${word}${suffix}`, `suffix: +${suffix}`);
55
+ }
56
+
57
+ // Word mashups
58
+ for (const tech of TECH_WORDS) {
59
+ if (suggestions.length >= maxResults) break;
60
+ add(`${word}${tech}`, `mashup: +${tech}`);
61
+ add(`${tech}${word}`, `mashup: ${tech}+`);
62
+ }
63
+
64
+ // Truncations
65
+ if (word.length > 4) {
66
+ add(word.slice(0, 4), "truncation: first 4");
67
+ add(word.slice(0, 5), "truncation: first 5");
68
+ }
69
+
70
+ // Vowel removal
71
+ const noVowels = word.replace(/[aeiou]/g, "");
72
+ if (noVowels.length >= 3 && noVowels !== word) {
73
+ add(noVowels, "vowel removal");
74
+ }
75
+
76
+ // Double last letter
77
+ add(`${word}${word.slice(-1)}`, "doubled ending");
78
+
79
+ // Rhyme patterns
80
+ const rhymeSuffixes = ["oo", "ee", "ify", "ly", "er", "le"];
81
+ for (const r of rhymeSuffixes) {
82
+ if (suggestions.length >= maxResults) break;
83
+ add(`${word}${r}`, `rhyme: +${r}`);
84
+ }
85
+
86
+ return suggestions.slice(0, maxResults);
87
+ }