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,69 @@
1
+ import { assertValidDomain } from "../validate.js";
2
+
3
+ export interface MarketplaceListing {
4
+ source: string;
5
+ listed: boolean;
6
+ price: number | null;
7
+ currency: string;
8
+ url: string | null;
9
+ error: string | null;
10
+ }
11
+
12
+ async function checkEstibot(domain: string): Promise<MarketplaceListing> {
13
+ try {
14
+ const resp = await fetch(
15
+ `https://www.estibot.com/api/v1/appraisal?domain=${encodeURIComponent(domain)}`,
16
+ { signal: AbortSignal.timeout(8000) }
17
+ );
18
+ if (!resp.ok) {
19
+ return { source: "estibot", listed: false, price: null, currency: "USD", url: null, error: `HTTP ${resp.status}` };
20
+ }
21
+ const text = await resp.text();
22
+ // Estibot returns estimated value, not a listing
23
+ const match = text.match(/"appraised_value"\s*:\s*(\d+)/);
24
+ const value = match?.[1] ? parseInt(match[1], 10) : null;
25
+ return {
26
+ source: "estibot",
27
+ listed: false,
28
+ price: value,
29
+ currency: "USD",
30
+ url: `https://www.estibot.com/appraisal/${encodeURIComponent(domain)}`,
31
+ error: null,
32
+ };
33
+ } catch (err: unknown) {
34
+ return { source: "estibot", listed: false, price: null, currency: "USD", url: null, error: err instanceof Error ? err.message : "Failed" };
35
+ }
36
+ }
37
+
38
+ async function checkSedo(domain: string): Promise<MarketplaceListing> {
39
+ try {
40
+ const resp = await fetch(
41
+ `https://sedo.com/search/searchresult.php?keyword=${encodeURIComponent(domain)}&trackingid=domain-sniper`,
42
+ { signal: AbortSignal.timeout(8000), headers: { "User-Agent": "DomainSniper/2.0" } }
43
+ );
44
+ const listed = resp.ok;
45
+ return {
46
+ source: "sedo",
47
+ listed,
48
+ price: null,
49
+ currency: "USD",
50
+ url: `https://sedo.com/search/details/?domain=${encodeURIComponent(domain)}`,
51
+ error: null,
52
+ };
53
+ } catch (err: unknown) {
54
+ return { source: "sedo", listed: false, price: null, currency: "USD", url: null, error: err instanceof Error ? err.message : "Failed" };
55
+ }
56
+ }
57
+
58
+ export async function checkMarketplaces(domain: string): Promise<MarketplaceListing[]> {
59
+ assertValidDomain(domain);
60
+ const results = await Promise.allSettled([
61
+ checkEstibot(domain),
62
+ checkSedo(domain),
63
+ ]);
64
+ return results.map((r) =>
65
+ r.status === "fulfilled"
66
+ ? r.value
67
+ : { source: "unknown", listed: false, price: null, currency: "USD", url: null, error: r.reason?.message || "Failed" }
68
+ );
69
+ }
@@ -0,0 +1,123 @@
1
+ import { assertValidDomain } from "../validate.js";
2
+
3
+ export interface PathScanResult {
4
+ domain: string;
5
+ findings: PathFinding[];
6
+ scannedPaths: number;
7
+ error: string | null;
8
+ }
9
+
10
+ export interface PathFinding {
11
+ path: string;
12
+ status: number;
13
+ severity: "critical" | "high" | "medium" | "low" | "info";
14
+ description: string;
15
+ size: number | null;
16
+ }
17
+
18
+ const SENSITIVE_PATHS: { path: string; severity: "critical" | "high" | "medium" | "low" | "info"; description: string }[] = [
19
+ // Critical — secrets and credentials
20
+ { path: "/.env", severity: "critical", description: "Environment file — may contain API keys and passwords" },
21
+ { path: "/.env.local", severity: "critical", description: "Local environment file" },
22
+ { path: "/.env.production", severity: "critical", description: "Production environment file" },
23
+ { path: "/.git/config", severity: "critical", description: "Git config exposed — repo may be cloneable" },
24
+ { path: "/.git/HEAD", severity: "critical", description: "Git HEAD exposed — confirms .git directory" },
25
+ { path: "/wp-config.php.bak", severity: "critical", description: "WordPress config backup with DB credentials" },
26
+ { path: "/.aws/credentials", severity: "critical", description: "AWS credentials file" },
27
+ { path: "/config.json", severity: "high", description: "Configuration file possibly containing secrets" },
28
+ { path: "/config.yaml", severity: "high", description: "YAML configuration file" },
29
+ // High — admin and debug
30
+ { path: "/wp-admin/", severity: "high", description: "WordPress admin panel" },
31
+ { path: "/admin/", severity: "high", description: "Admin panel" },
32
+ { path: "/phpinfo.php", severity: "high", description: "PHP info page — exposes server configuration" },
33
+ { path: "/server-status", severity: "high", description: "Apache server status page" },
34
+ { path: "/server-info", severity: "high", description: "Apache server info page" },
35
+ { path: "/.htpasswd", severity: "high", description: "htpasswd file with hashed credentials" },
36
+ { path: "/debug/", severity: "high", description: "Debug endpoint" },
37
+ { path: "/_debug/", severity: "high", description: "Debug endpoint" },
38
+ { path: "/actuator", severity: "high", description: "Spring Boot actuator endpoints" },
39
+ { path: "/actuator/health", severity: "medium", description: "Spring Boot health endpoint" },
40
+ { path: "/actuator/env", severity: "critical", description: "Spring Boot environment — may expose secrets" },
41
+ // Medium — information disclosure
42
+ { path: "/robots.txt", severity: "info", description: "Robots.txt — may reveal hidden paths" },
43
+ { path: "/sitemap.xml", severity: "info", description: "Sitemap — shows site structure" },
44
+ { path: "/.DS_Store", severity: "medium", description: "macOS directory metadata — leaks filenames" },
45
+ { path: "/crossdomain.xml", severity: "medium", description: "Flash crossdomain policy" },
46
+ { path: "/security.txt", severity: "info", description: "Security contact information" },
47
+ { path: "/.well-known/security.txt", severity: "info", description: "Security contact (standard location)" },
48
+ { path: "/package.json", severity: "medium", description: "Node.js package manifest — shows dependencies" },
49
+ { path: "/composer.json", severity: "medium", description: "PHP Composer manifest" },
50
+ { path: "/Gemfile", severity: "medium", description: "Ruby Gemfile" },
51
+ { path: "/wp-json/", severity: "low", description: "WordPress REST API" },
52
+ { path: "/api/", severity: "info", description: "API endpoint" },
53
+ { path: "/graphql", severity: "medium", description: "GraphQL endpoint — may allow introspection" },
54
+ { path: "/swagger.json", severity: "medium", description: "Swagger/OpenAPI spec" },
55
+ { path: "/api-docs", severity: "medium", description: "API documentation endpoint" },
56
+ { path: "/.well-known/openid-configuration", severity: "info", description: "OpenID Connect configuration" },
57
+ { path: "/backup.sql", severity: "critical", description: "SQL database backup" },
58
+ { path: "/dump.sql", severity: "critical", description: "SQL database dump" },
59
+ { path: "/db.sql", severity: "critical", description: "SQL database file" },
60
+ ];
61
+
62
+ export async function scanPaths(
63
+ domain: string,
64
+ paths: typeof SENSITIVE_PATHS = SENSITIVE_PATHS,
65
+ concurrency: number = 5
66
+ ): Promise<PathScanResult> {
67
+ assertValidDomain(domain);
68
+
69
+ const result: PathScanResult = {
70
+ domain, findings: [], scannedPaths: 0, error: null,
71
+ };
72
+
73
+ try {
74
+ for (let i = 0; i < paths.length; i += concurrency) {
75
+ const batch = paths.slice(i, i + concurrency);
76
+ const batchResults = await Promise.all(
77
+ batch.map(async (p) => {
78
+ result.scannedPaths++;
79
+ try {
80
+ const resp = await fetch(`https://${domain}${p.path}`, {
81
+ method: "HEAD",
82
+ redirect: "manual",
83
+ signal: AbortSignal.timeout(5000),
84
+ headers: { "User-Agent": "DomainSniper/2.0" },
85
+ });
86
+
87
+ // 200 = found, 403 = exists but forbidden (still interesting)
88
+ if (resp.status === 200 || resp.status === 403) {
89
+ const size = resp.headers.get("content-length");
90
+ return {
91
+ path: p.path,
92
+ status: resp.status,
93
+ severity: p.severity,
94
+ description: resp.status === 403
95
+ ? `${p.description} (403 Forbidden — exists but protected)`
96
+ : p.description,
97
+ size: size ? parseInt(size, 10) : null,
98
+ };
99
+ }
100
+ return null;
101
+ } catch {
102
+ return null;
103
+ }
104
+ })
105
+ );
106
+
107
+ for (const r of batchResults) {
108
+ if (r) result.findings.push(r);
109
+ }
110
+ }
111
+
112
+ // Sort by severity
113
+ const severityOrder: Record<string, number> = { critical: 0, high: 1, medium: 2, low: 3, info: 4 };
114
+ result.findings.sort((a, b) => (severityOrder[a.severity] ?? 5) - (severityOrder[b.severity] ?? 5));
115
+
116
+ return result;
117
+ } catch (err: unknown) {
118
+ result.error = err instanceof Error ? err.message : "Path scan failed";
119
+ return result;
120
+ }
121
+ }
122
+
123
+ export { SENSITIVE_PATHS };
@@ -0,0 +1,132 @@
1
+ import { assertValidDomain } from "../validate.js";
2
+ import { connect } from "net";
3
+
4
+ export interface PortResult {
5
+ port: number;
6
+ service: string;
7
+ open: boolean;
8
+ banner: string | null;
9
+ }
10
+
11
+ export interface PortScanResult {
12
+ domain: string;
13
+ ip: string | null;
14
+ openPorts: PortResult[];
15
+ closedCount: number;
16
+ scanTime: number;
17
+ error: string | null;
18
+ }
19
+
20
+ const COMMON_PORTS: { port: number; service: string }[] = [
21
+ { port: 21, service: "FTP" },
22
+ { port: 22, service: "SSH" },
23
+ { port: 25, service: "SMTP" },
24
+ { port: 53, service: "DNS" },
25
+ { port: 80, service: "HTTP" },
26
+ { port: 110, service: "POP3" },
27
+ { port: 143, service: "IMAP" },
28
+ { port: 443, service: "HTTPS" },
29
+ { port: 465, service: "SMTPS" },
30
+ { port: 587, service: "Submission" },
31
+ { port: 993, service: "IMAPS" },
32
+ { port: 995, service: "POP3S" },
33
+ { port: 3000, service: "Dev Server" },
34
+ { port: 3306, service: "MySQL" },
35
+ { port: 5432, service: "PostgreSQL" },
36
+ { port: 6379, service: "Redis" },
37
+ { port: 8080, service: "HTTP Alt" },
38
+ { port: 8443, service: "HTTPS Alt" },
39
+ { port: 27017, service: "MongoDB" },
40
+ { port: 9200, service: "Elasticsearch" },
41
+ ];
42
+
43
+ function checkPort(host: string, port: number, timeoutMs: number = 3000): Promise<{ open: boolean; banner: string | null }> {
44
+ return new Promise((resolve) => {
45
+ const socket = connect({ host, port });
46
+ let banner: string | null = null;
47
+
48
+ const timer = setTimeout(() => {
49
+ socket.destroy();
50
+ resolve({ open: false, banner: null });
51
+ }, timeoutMs);
52
+
53
+ socket.on("connect", () => {
54
+ clearTimeout(timer);
55
+ // Try to grab a banner (wait briefly for data)
56
+ const bannerTimer = setTimeout(() => {
57
+ socket.destroy();
58
+ resolve({ open: true, banner });
59
+ }, 1500);
60
+
61
+ socket.once("data", (data) => {
62
+ clearTimeout(bannerTimer);
63
+ banner = data.toString("utf-8").trim().slice(0, 200);
64
+ socket.destroy();
65
+ resolve({ open: true, banner });
66
+ });
67
+ });
68
+
69
+ socket.on("error", () => {
70
+ clearTimeout(timer);
71
+ socket.destroy();
72
+ resolve({ open: false, banner: null });
73
+ });
74
+ });
75
+ }
76
+
77
+ export async function scanPorts(
78
+ domain: string,
79
+ ports: { port: number; service: string }[] = COMMON_PORTS,
80
+ concurrency: number = 10
81
+ ): Promise<PortScanResult> {
82
+ assertValidDomain(domain);
83
+ const startTime = Date.now();
84
+
85
+ const result: PortScanResult = {
86
+ domain,
87
+ ip: null,
88
+ openPorts: [],
89
+ closedCount: 0,
90
+ scanTime: 0,
91
+ error: null,
92
+ };
93
+
94
+ try {
95
+ // Resolve domain to IP first
96
+ const { execFile } = await import("child_process");
97
+ const { promisify } = await import("util");
98
+ const execFileAsync = promisify(execFile);
99
+ try {
100
+ const { stdout } = await execFileAsync("dig", ["+short", domain, "A"], { timeout: 5000 });
101
+ result.ip = stdout.trim().split("\n")[0] || null;
102
+ } catch {}
103
+
104
+ const host = result.ip || domain;
105
+
106
+ // Scan in batches
107
+ for (let i = 0; i < ports.length; i += concurrency) {
108
+ const batch = ports.slice(i, i + concurrency);
109
+ const batchResults = await Promise.all(
110
+ batch.map(async (p) => {
111
+ const { open, banner } = await checkPort(host, p.port);
112
+ return { port: p.port, service: p.service, open, banner };
113
+ })
114
+ );
115
+
116
+ for (const r of batchResults) {
117
+ if (r.open) {
118
+ result.openPorts.push(r);
119
+ } else {
120
+ result.closedCount++;
121
+ }
122
+ }
123
+ }
124
+ } catch (err: unknown) {
125
+ result.error = err instanceof Error ? err.message : "Port scan failed";
126
+ }
127
+
128
+ result.scanTime = Date.now() - startTime;
129
+ return result;
130
+ }
131
+
132
+ export { COMMON_PORTS };
@@ -0,0 +1,125 @@
1
+ import {
2
+ getPortfolioDomains,
3
+ updatePortfolioStatus,
4
+ updatePortfolioCategory,
5
+ addTransaction,
6
+ removePortfolioDomain,
7
+ type PortfolioStatus,
8
+ type TransactionType,
9
+ type DbPortfolioDomain,
10
+ } from "../db.js";
11
+ import { writeFileSync } from "fs";
12
+ import { getTaxExportData, getPortfolioPnL, getDomainPnL, getTransactions } from "../db.js";
13
+
14
+ export interface BulkResult {
15
+ total: number;
16
+ success: number;
17
+ failed: number;
18
+ errors: string[];
19
+ }
20
+
21
+ export function bulkUpdateStatus(domains: string[], status: PortfolioStatus): BulkResult {
22
+ const result: BulkResult = { total: domains.length, success: 0, failed: 0, errors: [] };
23
+ for (const domain of domains) {
24
+ try {
25
+ updatePortfolioStatus(domain, status);
26
+ result.success++;
27
+ } catch (err: unknown) {
28
+ result.failed++;
29
+ result.errors.push(`${domain}: ${err instanceof Error ? err.message : "failed"}`);
30
+ }
31
+ }
32
+ return result;
33
+ }
34
+
35
+ export function bulkUpdateCategory(domains: string[], category: string): BulkResult {
36
+ const result: BulkResult = { total: domains.length, success: 0, failed: 0, errors: [] };
37
+ for (const domain of domains) {
38
+ try {
39
+ updatePortfolioCategory(domain, category);
40
+ result.success++;
41
+ } catch (err: unknown) {
42
+ result.failed++;
43
+ result.errors.push(`${domain}: ${err instanceof Error ? err.message : "failed"}`);
44
+ }
45
+ }
46
+ return result;
47
+ }
48
+
49
+ export function bulkAddTransaction(
50
+ domains: string[],
51
+ type: TransactionType,
52
+ amount: number,
53
+ description: string = "",
54
+ date?: string
55
+ ): BulkResult {
56
+ const result: BulkResult = { total: domains.length, success: 0, failed: 0, errors: [] };
57
+ for (const domain of domains) {
58
+ try {
59
+ addTransaction(domain, type, amount, description, date);
60
+ result.success++;
61
+ } catch (err: unknown) {
62
+ result.failed++;
63
+ result.errors.push(`${domain}: ${err instanceof Error ? err.message : "failed"}`);
64
+ }
65
+ }
66
+ return result;
67
+ }
68
+
69
+ export function bulkRemove(domains: string[]): BulkResult {
70
+ const result: BulkResult = { total: domains.length, success: 0, failed: 0, errors: [] };
71
+ for (const domain of domains) {
72
+ try {
73
+ removePortfolioDomain(domain);
74
+ result.success++;
75
+ } catch (err: unknown) {
76
+ result.failed++;
77
+ result.errors.push(`${domain}: ${err instanceof Error ? err.message : "failed"}`);
78
+ }
79
+ }
80
+ return result;
81
+ }
82
+
83
+ // ─── Export ──────────────────────────────────────────────
84
+
85
+ export function exportPortfolioCSV(filePath: string): string {
86
+ const domains = getPortfolioDomains();
87
+ const headers = ["domain", "registrar", "status", "category", "purchase_date", "expiry_date", "purchase_price", "renewal_price", "estimated_value", "currency", "auto_renew", "tags", "notes"];
88
+ const rows = domains.map((d) =>
89
+ headers.map((h) => {
90
+ const val = String((d as any)[h] ?? "");
91
+ return val.includes(",") || val.includes('"') ? `"${val.replace(/"/g, '""')}"` : val;
92
+ }).join(",")
93
+ );
94
+ const csv = [headers.join(","), ...rows].join("\n");
95
+ writeFileSync(filePath, csv, "utf-8");
96
+ return filePath;
97
+ }
98
+
99
+ export function exportTaxCSV(filePath: string, year: number): string {
100
+ const data = getTaxExportData(year);
101
+ const headers = ["domain", "purchase_date", "purchase_price", "sale_date", "sale_price", "holding_days", "profit", "currency"];
102
+ const rows = data.map((d) => [
103
+ d.domain, d.purchaseDate, d.purchasePrice, d.saleDate || "", d.salePrice ?? "", d.holdingDays ?? "", d.profit, d.currency,
104
+ ].map((v) => {
105
+ const s = String(v);
106
+ return s.includes(",") ? `"${s}"` : s;
107
+ }).join(","));
108
+ const csv = [headers.join(","), ...rows].join("\n");
109
+ writeFileSync(filePath, csv, "utf-8");
110
+ return filePath;
111
+ }
112
+
113
+ export function exportTransactionsCSV(filePath: string, domain?: string): string {
114
+ const txns = getTransactions(domain, 1000);
115
+ const headers = ["id", "domain", "type", "amount", "currency", "description", "date"];
116
+ const rows = txns.map((t) =>
117
+ headers.map((h) => {
118
+ const val = String((t as any)[h] ?? "");
119
+ return val.includes(",") || val.includes('"') ? `"${val.replace(/"/g, '""')}"` : val;
120
+ }).join(",")
121
+ );
122
+ const csv = [headers.join(","), ...rows].join("\n");
123
+ writeFileSync(filePath, csv, "utf-8");
124
+ return filePath;
125
+ }
@@ -0,0 +1,214 @@
1
+ import {
2
+ getPortfolioDomains,
3
+ createAlert,
4
+ type DbPortfolioDomain,
5
+ type AlertSeverity,
6
+ } from "../db.js";
7
+ import { whoisLookup } from "../whois.js";
8
+ import { checkSsl } from "./ssl-check.js";
9
+ import { httpProbe } from "./http-probe.js";
10
+ import { lookupDns } from "./dns-details.js";
11
+
12
+ export interface HealthCheckResult {
13
+ domain: string;
14
+ whoisOk: boolean;
15
+ dnsOk: boolean;
16
+ httpOk: boolean;
17
+ sslOk: boolean;
18
+ sslDaysLeft: number | null;
19
+ expiryDaysLeft: number | null;
20
+ issues: string[];
21
+ checkedAt: string;
22
+ }
23
+
24
+ export interface MonitorReport {
25
+ checked: number;
26
+ healthy: number;
27
+ warnings: number;
28
+ critical: number;
29
+ results: HealthCheckResult[];
30
+ alerts: Array<{ domain: string; severity: string; message: string }>;
31
+ }
32
+
33
+ function daysUntil(dateStr: string | null): number | null {
34
+ if (!dateStr) return null;
35
+ try {
36
+ const d = new Date(dateStr);
37
+ if (isNaN(d.getTime())) return null;
38
+ return Math.floor((d.getTime() - Date.now()) / 86400000);
39
+ } catch { return null; }
40
+ }
41
+
42
+ export async function checkDomainHealth(domain: string): Promise<HealthCheckResult> {
43
+ const result: HealthCheckResult = {
44
+ domain,
45
+ whoisOk: false,
46
+ dnsOk: false,
47
+ httpOk: false,
48
+ sslOk: false,
49
+ sslDaysLeft: null,
50
+ expiryDaysLeft: null,
51
+ issues: [],
52
+ checkedAt: new Date().toISOString(),
53
+ };
54
+
55
+ // WHOIS check
56
+ try {
57
+ const whois = await whoisLookup(domain);
58
+ if (!whois.error && !whois.available) {
59
+ result.whoisOk = true;
60
+ result.expiryDaysLeft = daysUntil(whois.expiryDate);
61
+ } else if (whois.available) {
62
+ result.issues.push("Domain appears unregistered!");
63
+ }
64
+ } catch { result.issues.push("WHOIS check failed"); }
65
+
66
+ // DNS check
67
+ try {
68
+ const dns = await lookupDns(domain);
69
+ if (dns.a.length > 0 || dns.cname.length > 0) {
70
+ result.dnsOk = true;
71
+ } else {
72
+ result.issues.push("No DNS A/CNAME records");
73
+ }
74
+ } catch { result.issues.push("DNS check failed"); }
75
+
76
+ // HTTP check
77
+ try {
78
+ const probe = await httpProbe(domain);
79
+ if (probe.reachable && probe.status !== null && probe.status < 500) {
80
+ result.httpOk = true;
81
+ } else {
82
+ result.issues.push(probe.reachable ? `HTTP ${probe.status}` : "Site unreachable");
83
+ }
84
+ } catch { result.issues.push("HTTP check failed"); }
85
+
86
+ // SSL check
87
+ try {
88
+ const ssl = await checkSsl(domain);
89
+ if (ssl.valid && !ssl.error) {
90
+ result.sslOk = true;
91
+ result.sslDaysLeft = ssl.daysUntilExpiry;
92
+ if (ssl.daysUntilExpiry !== null && ssl.daysUntilExpiry < 30) {
93
+ result.issues.push(`SSL expires in ${ssl.daysUntilExpiry} days`);
94
+ }
95
+ } else {
96
+ result.issues.push(ssl.error || "Invalid SSL certificate");
97
+ }
98
+ } catch { result.issues.push("SSL check failed"); }
99
+
100
+ return result;
101
+ }
102
+
103
+ export async function runPortfolioHealthCheck(
104
+ onProgress?: (domain: string, index: number, total: number) => void
105
+ ): Promise<MonitorReport> {
106
+ const domains = getPortfolioDomains();
107
+ const report: MonitorReport = {
108
+ checked: 0,
109
+ healthy: 0,
110
+ warnings: 0,
111
+ critical: 0,
112
+ results: [],
113
+ alerts: [],
114
+ };
115
+
116
+ for (let i = 0; i < domains.length; i++) {
117
+ const d = domains[i]!;
118
+ onProgress?.(d.domain, i, domains.length);
119
+
120
+ const health = await checkDomainHealth(d.domain);
121
+ report.results.push(health);
122
+ report.checked++;
123
+
124
+ // Generate alerts
125
+ // Domain expiry
126
+ if (health.expiryDaysLeft !== null) {
127
+ if (health.expiryDaysLeft <= 7) {
128
+ const msg = `Domain expires in ${health.expiryDaysLeft} days!`;
129
+ createAlert(d.domain, "expiry", "critical", msg);
130
+ report.alerts.push({ domain: d.domain, severity: "critical", message: msg });
131
+ report.critical++;
132
+ } else if (health.expiryDaysLeft <= 30) {
133
+ const msg = `Domain expires in ${health.expiryDaysLeft} days`;
134
+ createAlert(d.domain, "expiry", "warning", msg);
135
+ report.alerts.push({ domain: d.domain, severity: "warning", message: msg });
136
+ report.warnings++;
137
+ } else if (health.expiryDaysLeft <= 90) {
138
+ const msg = `Domain expires in ${health.expiryDaysLeft} days`;
139
+ createAlert(d.domain, "expiry", "info", msg);
140
+ report.alerts.push({ domain: d.domain, severity: "info", message: msg });
141
+ }
142
+ }
143
+
144
+ // SSL expiry
145
+ if (health.sslDaysLeft !== null && health.sslDaysLeft <= 14) {
146
+ const severity: AlertSeverity = health.sslDaysLeft <= 3 ? "critical" : "warning";
147
+ const msg = `SSL certificate expires in ${health.sslDaysLeft} days`;
148
+ createAlert(d.domain, "ssl-expiry", severity, msg);
149
+ report.alerts.push({ domain: d.domain, severity, message: msg });
150
+ if (severity === "critical") report.critical++; else report.warnings++;
151
+ }
152
+
153
+ // Site down
154
+ if (!health.httpOk) {
155
+ const msg = "Site is unreachable or returning errors";
156
+ createAlert(d.domain, "downtime", "warning", msg);
157
+ report.alerts.push({ domain: d.domain, severity: "warning", message: msg });
158
+ report.warnings++;
159
+ }
160
+
161
+ // DNS missing
162
+ if (!health.dnsOk) {
163
+ const msg = "No DNS records found";
164
+ createAlert(d.domain, "dns", "warning", msg);
165
+ report.alerts.push({ domain: d.domain, severity: "warning", message: msg });
166
+ report.warnings++;
167
+ }
168
+
169
+ if (health.issues.length === 0) report.healthy++;
170
+
171
+ // Rate limit
172
+ await new Promise((r) => setTimeout(r, 1000));
173
+ }
174
+
175
+ return report;
176
+ }
177
+
178
+ export function generateRenewalCalendar(monthsAhead: number = 12): Array<{
179
+ domain: string;
180
+ expiryDate: string;
181
+ daysLeft: number;
182
+ renewalPrice: number;
183
+ autoRenew: boolean;
184
+ }> {
185
+ const domains = getPortfolioDomains();
186
+ const calendar: Array<{
187
+ domain: string; expiryDate: string; daysLeft: number; renewalPrice: number; autoRenew: boolean;
188
+ }> = [];
189
+
190
+ const cutoff = Date.now() + monthsAhead * 30 * 86400000;
191
+
192
+ for (const d of domains) {
193
+ if (!d.expiry_date) continue;
194
+ const expiry = new Date(d.expiry_date).getTime();
195
+ if (isNaN(expiry)) continue;
196
+ const daysLeft = Math.floor((expiry - Date.now()) / 86400000);
197
+ if (daysLeft >= 0 && expiry <= cutoff) {
198
+ calendar.push({
199
+ domain: d.domain,
200
+ expiryDate: d.expiry_date,
201
+ daysLeft,
202
+ renewalPrice: d.renewal_price,
203
+ autoRenew: !!d.auto_renew,
204
+ });
205
+ }
206
+ }
207
+
208
+ return calendar.sort((a, b) => a.daysLeft - b.daysLeft);
209
+ }
210
+
211
+ export function estimateAnnualRenewalCost(): number {
212
+ const domains = getPortfolioDomains();
213
+ return domains.reduce((sum, d) => sum + (d.renewal_price || 0), 0);
214
+ }