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
package/src/index.tsx
ADDED
|
@@ -0,0 +1,1888 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import { createCliRenderer } from "@opentui/core";
|
|
3
|
+
import { createRoot } from "@opentui/react";
|
|
4
|
+
import { App } from "./app.js";
|
|
5
|
+
import { Command } from "commander";
|
|
6
|
+
import { lookupDns } from "./core/features/dns-details.js";
|
|
7
|
+
import { httpProbe } from "./core/features/http-probe.js";
|
|
8
|
+
import { checkWayback } from "./core/features/wayback.js";
|
|
9
|
+
import { calculateDomainAge } from "./core/features/domain-age.js";
|
|
10
|
+
import { sanitizeDomainList, safePath } from "./core/validate.js";
|
|
11
|
+
import { bashCompletions, zshCompletions, fishCompletions } from "./completions.js";
|
|
12
|
+
import { checkSocialMedia } from "./core/features/social-check.js";
|
|
13
|
+
import { detectTechStack } from "./core/features/tech-stack.js";
|
|
14
|
+
import { checkBlacklists } from "./core/features/blacklist-check.js";
|
|
15
|
+
import { estimateBacklinks } from "./core/features/backlinks.js";
|
|
16
|
+
import { scanPorts } from "./core/features/port-scanner.js";
|
|
17
|
+
import { reverseIpLookup } from "./core/features/reverse-ip.js";
|
|
18
|
+
import { lookupAsn } from "./core/features/asn-lookup.js";
|
|
19
|
+
import { checkEmailSecurity } from "./core/features/email-security.js";
|
|
20
|
+
import { checkZoneTransfer } from "./core/features/zone-transfer.js";
|
|
21
|
+
import { queryCertTransparency } from "./core/features/cert-transparency.js";
|
|
22
|
+
import { detectTakeover } from "./core/features/takeover-detect.js";
|
|
23
|
+
import { auditSecurityHeaders } from "./core/features/security-headers.js";
|
|
24
|
+
import { detectWaf } from "./core/features/waf-detect.js";
|
|
25
|
+
import { scanPaths } from "./core/features/path-scanner.js";
|
|
26
|
+
import { checkCors } from "./core/features/cors-check.js";
|
|
27
|
+
import { rdapLookup } from "./core/features/rdap.js";
|
|
28
|
+
import { checkSsl } from "./core/features/ssl-check.js";
|
|
29
|
+
import type { PortScanResult } from "./core/features/port-scanner.js";
|
|
30
|
+
import type { ReverseIpResult } from "./core/features/reverse-ip.js";
|
|
31
|
+
import type { AsnResult } from "./core/features/asn-lookup.js";
|
|
32
|
+
import type { EmailSecurityResult } from "./core/features/email-security.js";
|
|
33
|
+
import type { ZoneTransferResult } from "./core/features/zone-transfer.js";
|
|
34
|
+
import type { CertTransparencyResult } from "./core/features/cert-transparency.js";
|
|
35
|
+
import type { TakeoverResult } from "./core/features/takeover-detect.js";
|
|
36
|
+
import type { SecurityHeadersResult } from "./core/features/security-headers.js";
|
|
37
|
+
import type { WafResult } from "./core/features/waf-detect.js";
|
|
38
|
+
import type { PathScanResult } from "./core/features/path-scanner.js";
|
|
39
|
+
import type { CorsResult } from "./core/features/cors-check.js";
|
|
40
|
+
|
|
41
|
+
const program = new Command();
|
|
42
|
+
|
|
43
|
+
program
|
|
44
|
+
.name("dsniper")
|
|
45
|
+
.description("All-in-one domain intelligence toolkit — availability checker, security recon, portfolio manager")
|
|
46
|
+
.version("0.1.0")
|
|
47
|
+
.argument("[domains...]", "Domain(s) to check")
|
|
48
|
+
.option("-f, --file <path>", "Path to file with domains (one per line)")
|
|
49
|
+
.option("-a, --auto-register", "Automatically register available domains", false)
|
|
50
|
+
.option("--headless", "Run in non-interactive mode (print results to stdout)", false)
|
|
51
|
+
.option("--json", "Output results as JSON", false)
|
|
52
|
+
.option("-c, --concurrency <n>", "Concurrent lookups (default: 5)", "5")
|
|
53
|
+
.option("--recon", "Enable full recon mode in headless scanning", false)
|
|
54
|
+
.action(async (domains: string[], options: CliOptions) => {
|
|
55
|
+
// Stdin pipe support: read from stdin when no TTY and no domains/file provided
|
|
56
|
+
if (!process.stdin.isTTY && domains.length === 0 && !options.file) {
|
|
57
|
+
const { parseDomainList } = await import("./core/whois.js");
|
|
58
|
+
const chunks: Buffer[] = [];
|
|
59
|
+
for await (const chunk of process.stdin) {
|
|
60
|
+
chunks.push(chunk as Buffer);
|
|
61
|
+
}
|
|
62
|
+
const stdinContent = Buffer.concat(chunks).toString("utf-8");
|
|
63
|
+
const stdinDomains = parseDomainList(stdinContent);
|
|
64
|
+
domains.push(...stdinDomains);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (options.headless || !process.stdout.isTTY) {
|
|
68
|
+
// Non-interactive mode
|
|
69
|
+
await runHeadless(domains, options);
|
|
70
|
+
} else {
|
|
71
|
+
// TUI mode
|
|
72
|
+
const renderer = await createCliRenderer({
|
|
73
|
+
exitOnCtrlC: true,
|
|
74
|
+
screenMode: "alternate-screen",
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
createRoot(renderer).render(
|
|
78
|
+
<App
|
|
79
|
+
initialDomains={domains.length > 0 ? domains : undefined}
|
|
80
|
+
batchFile={options.file}
|
|
81
|
+
autoRegister={options.autoRegister}
|
|
82
|
+
/>
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// ─── Completions subcommand ──────────────────────────────
|
|
88
|
+
|
|
89
|
+
program
|
|
90
|
+
.command("completions <shell>")
|
|
91
|
+
.description("Generate shell completions (bash, zsh, fish)")
|
|
92
|
+
.action((shell: string) => {
|
|
93
|
+
switch (shell.toLowerCase()) {
|
|
94
|
+
case "bash":
|
|
95
|
+
process.stdout.write(bashCompletions());
|
|
96
|
+
break;
|
|
97
|
+
case "zsh":
|
|
98
|
+
process.stdout.write(zshCompletions());
|
|
99
|
+
break;
|
|
100
|
+
case "fish":
|
|
101
|
+
process.stdout.write(fishCompletions());
|
|
102
|
+
break;
|
|
103
|
+
default:
|
|
104
|
+
console.error(`Unknown shell: ${shell}. Supported: bash, zsh, fish`);
|
|
105
|
+
process.exit(1);
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// ─── Suggest subcommand ──────────────────────────────────
|
|
110
|
+
|
|
111
|
+
program
|
|
112
|
+
.command("suggest <keyword>")
|
|
113
|
+
.description("Generate domain name suggestions from a keyword")
|
|
114
|
+
.option("-t, --tld <tld>", "TLD to use", "com")
|
|
115
|
+
.option("-n, --count <n>", "Number of suggestions", "20")
|
|
116
|
+
.option("--check", "Check availability of suggestions", false)
|
|
117
|
+
.action(async (keyword: string, opts: { tld: string; count: string; check: boolean }) => {
|
|
118
|
+
const { generateSuggestions } = await import("./core/features/domain-suggest.js");
|
|
119
|
+
const suggestions = generateSuggestions(keyword, opts.tld, parseInt(opts.count, 10));
|
|
120
|
+
|
|
121
|
+
if (opts.check) {
|
|
122
|
+
const { whoisLookup } = await import("./core/whois.js");
|
|
123
|
+
console.log(`\nChecking ${suggestions.length} suggestions for "${keyword}"...\n`);
|
|
124
|
+
for (const s of suggestions) {
|
|
125
|
+
const whois = await whoisLookup(s.domain);
|
|
126
|
+
const status = whois.available ? "\x1b[32mAVAILABLE\x1b[0m" : "\x1b[31mTAKEN\x1b[0m";
|
|
127
|
+
console.log(` ${status} ${s.domain} (${s.strategy})`);
|
|
128
|
+
await new Promise((r) => setTimeout(r, 1000));
|
|
129
|
+
}
|
|
130
|
+
} else {
|
|
131
|
+
console.log(`\nSuggestions for "${keyword}" (.${opts.tld}):\n`);
|
|
132
|
+
for (const s of suggestions) {
|
|
133
|
+
console.log(` ${s.domain} (${s.strategy})`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
console.log();
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// ─── Portfolio subcommand ────────────────────────────────
|
|
140
|
+
|
|
141
|
+
program
|
|
142
|
+
.command("portfolio")
|
|
143
|
+
.description("Manage your domain portfolio")
|
|
144
|
+
.option("--add <domain>", "Add a domain to portfolio")
|
|
145
|
+
.option("--remove <domain>", "Remove a domain from portfolio")
|
|
146
|
+
.option("--status <domain:status>", "Set domain status (active|parked|for-sale|development|archived)")
|
|
147
|
+
.option("--category <domain:category>", "Set domain category")
|
|
148
|
+
.option("--value <domain:amount>", "Set estimated value")
|
|
149
|
+
.option("--transaction <domain:type:amount>", "Record transaction (purchase|renewal|sale|parking-revenue|affiliate-revenue|expense)")
|
|
150
|
+
.option("--expiring [days]", "Show domains expiring within N days")
|
|
151
|
+
.option("--renewals", "Show renewal calendar")
|
|
152
|
+
.option("--health", "Run health check on all portfolio domains")
|
|
153
|
+
.option("--pnl [domain]", "Show P&L (for specific domain or total)")
|
|
154
|
+
.option("--monthly [months]", "Show monthly financial report")
|
|
155
|
+
.option("--pipeline", "Show acquisition pipeline")
|
|
156
|
+
.option("--pipeline-add <domain>", "Add domain to acquisition pipeline")
|
|
157
|
+
.option("--alerts", "Show unacknowledged alerts")
|
|
158
|
+
.option("--dismiss-alerts", "Dismiss all alerts")
|
|
159
|
+
.option("--categories", "List categories")
|
|
160
|
+
.option("--export-csv <path>", "Export portfolio to CSV")
|
|
161
|
+
.option("--export-tax <year>", "Export tax data for year")
|
|
162
|
+
.option("--export-transactions <path>", "Export transactions to CSV")
|
|
163
|
+
.option("--upload-s3", "Upload latest export to S3/R2")
|
|
164
|
+
.option("--dashboard", "Show portfolio dashboard summary")
|
|
165
|
+
.option("--stats", "Show portfolio statistics")
|
|
166
|
+
.option("--json", "Output as JSON")
|
|
167
|
+
.action(async (opts: {
|
|
168
|
+
add?: string; remove?: string; status?: string; category?: string; value?: string;
|
|
169
|
+
transaction?: string; expiring?: string; renewals?: boolean; health?: boolean;
|
|
170
|
+
pnl?: string | boolean; monthly?: string | boolean; pipeline?: boolean; pipelineAdd?: string;
|
|
171
|
+
alerts?: boolean; dismissAlerts?: boolean; categories?: boolean;
|
|
172
|
+
exportCsv?: string; exportTax?: string; exportTransactions?: string;
|
|
173
|
+
uploadS3?: boolean; dashboard?: boolean; stats?: boolean; json?: boolean;
|
|
174
|
+
}) => {
|
|
175
|
+
if (opts.add) {
|
|
176
|
+
const { addToPortfolio } = await import("./core/features/portfolio.js");
|
|
177
|
+
addToPortfolio(opts.add);
|
|
178
|
+
console.log(`Added ${opts.add} to portfolio`);
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (opts.remove) {
|
|
183
|
+
const { removeFromPortfolio } = await import("./core/features/portfolio.js");
|
|
184
|
+
removeFromPortfolio(opts.remove);
|
|
185
|
+
console.log(`Removed ${opts.remove} from portfolio`);
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (opts.status) {
|
|
190
|
+
const [domain, status] = opts.status.split(":");
|
|
191
|
+
if (!domain || !status) { console.error("Usage: --status domain.com:active"); process.exit(1); }
|
|
192
|
+
const { updatePortfolioStatus } = await import("./core/db.js");
|
|
193
|
+
updatePortfolioStatus(domain, status as any);
|
|
194
|
+
console.log(`Set ${domain} status to ${status}`);
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (opts.category) {
|
|
199
|
+
const [domain, category] = opts.category.split(":");
|
|
200
|
+
if (!domain || !category) { console.error("Usage: --category domain.com:investments"); process.exit(1); }
|
|
201
|
+
const { updatePortfolioCategory } = await import("./core/db.js");
|
|
202
|
+
updatePortfolioCategory(domain, category);
|
|
203
|
+
console.log(`Set ${domain} category to ${category}`);
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (opts.value) {
|
|
208
|
+
const [domain, amountStr] = opts.value.split(":");
|
|
209
|
+
if (!domain || !amountStr) { console.error("Usage: --value domain.com:5000"); process.exit(1); }
|
|
210
|
+
const { updatePortfolioValue } = await import("./core/db.js");
|
|
211
|
+
updatePortfolioValue(domain, parseFloat(amountStr));
|
|
212
|
+
console.log(`Set ${domain} estimated value to $${amountStr}`);
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (opts.transaction) {
|
|
217
|
+
const parts = opts.transaction.split(":");
|
|
218
|
+
if (parts.length < 3) { console.error("Usage: --transaction domain.com:purchase:9.99"); process.exit(1); }
|
|
219
|
+
const [domain, type, amountStr] = parts;
|
|
220
|
+
const { addTransaction } = await import("./core/db.js");
|
|
221
|
+
addTransaction(domain!, type as any, parseFloat(amountStr!));
|
|
222
|
+
console.log(`Recorded ${type} of $${amountStr} for ${domain}`);
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (opts.expiring !== undefined) {
|
|
227
|
+
const { getPortfolioExpiring, closeDb } = await import("./core/db.js");
|
|
228
|
+
const days = parseInt(String(opts.expiring) || "30", 10) || 30;
|
|
229
|
+
const expiring = getPortfolioExpiring(days);
|
|
230
|
+
if (opts.json) { console.log(JSON.stringify(expiring, null, 2)); closeDb(); return; }
|
|
231
|
+
console.log(`\nDomains expiring within ${days} days:\n`);
|
|
232
|
+
if (expiring.length === 0) { console.log(" None"); }
|
|
233
|
+
for (const d of expiring) { console.log(` ${d.domain} ${d.expiry_date} ${d.registrar}`); }
|
|
234
|
+
console.log();
|
|
235
|
+
closeDb();
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (opts.renewals) {
|
|
240
|
+
const { generateRenewalCalendar, estimateAnnualRenewalCost } = await import("./core/features/portfolio-monitor.js");
|
|
241
|
+
const calendar = generateRenewalCalendar(12);
|
|
242
|
+
const annualCost = estimateAnnualRenewalCost();
|
|
243
|
+
if (opts.json) { console.log(JSON.stringify({ calendar, annualCost }, null, 2)); return; }
|
|
244
|
+
console.log(`\nRenewal Calendar (est. annual cost: $${annualCost.toFixed(0)}):\n`);
|
|
245
|
+
if (calendar.length === 0) { console.log(" No upcoming renewals."); }
|
|
246
|
+
for (const r of calendar) {
|
|
247
|
+
const urgency = r.daysLeft <= 7 ? "!!" : r.daysLeft <= 30 ? "! " : " ";
|
|
248
|
+
console.log(` ${urgency} ${r.domain.padEnd(30)} ${String(r.daysLeft).padStart(4)}d $${r.renewalPrice}${r.autoRenew ? " (auto-renew)" : ""}`);
|
|
249
|
+
}
|
|
250
|
+
console.log();
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (opts.health) {
|
|
255
|
+
const { runPortfolioHealthCheck } = await import("./core/features/portfolio-monitor.js");
|
|
256
|
+
console.log("\nRunning portfolio health check...\n");
|
|
257
|
+
const report = await runPortfolioHealthCheck((domain, i, total) => {
|
|
258
|
+
process.stdout.write(`\r Checking ${i + 1}/${total}: ${domain}...`);
|
|
259
|
+
});
|
|
260
|
+
console.log("\r" + " ".repeat(60) + "\r");
|
|
261
|
+
if (opts.json) { console.log(JSON.stringify(report, null, 2)); return; }
|
|
262
|
+
console.log(`Health Check Results:`);
|
|
263
|
+
console.log(` Checked: ${report.checked}`);
|
|
264
|
+
console.log(` Healthy: ${report.healthy}`);
|
|
265
|
+
console.log(` Warnings: ${report.warnings}`);
|
|
266
|
+
console.log(` Critical: ${report.critical}`);
|
|
267
|
+
if (report.alerts.length > 0) {
|
|
268
|
+
console.log(`\nAlerts:`);
|
|
269
|
+
for (const a of report.alerts) {
|
|
270
|
+
const icon = a.severity === "critical" ? "!!" : a.severity === "warning" ? "! " : " ";
|
|
271
|
+
console.log(` ${icon} ${a.domain}: ${a.message}`);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
console.log();
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (opts.pnl !== undefined) {
|
|
279
|
+
const { getDomainPnL, getPortfolioPnL } = await import("./core/db.js");
|
|
280
|
+
if (typeof opts.pnl === "string") {
|
|
281
|
+
const pnl = getDomainPnL(opts.pnl);
|
|
282
|
+
if (opts.json) { console.log(JSON.stringify(pnl, null, 2)); return; }
|
|
283
|
+
console.log(`\nP&L for ${opts.pnl}:`);
|
|
284
|
+
console.log(` Costs: $${pnl.costs.toFixed(2)}`);
|
|
285
|
+
console.log(` Revenue: $${pnl.revenue.toFixed(2)}`);
|
|
286
|
+
console.log(` Profit: $${pnl.profit.toFixed(2)}\n`);
|
|
287
|
+
} else {
|
|
288
|
+
const pnl = getPortfolioPnL();
|
|
289
|
+
if (opts.json) { console.log(JSON.stringify(pnl, null, 2)); return; }
|
|
290
|
+
console.log(`\nPortfolio P&L:`);
|
|
291
|
+
console.log(` Total Costs: $${pnl.totalCosts.toFixed(2)}`);
|
|
292
|
+
console.log(` Total Revenue: $${pnl.totalRevenue.toFixed(2)}`);
|
|
293
|
+
console.log(` Total Profit: $${pnl.totalProfit.toFixed(2)}`);
|
|
294
|
+
console.log(` Domains: ${pnl.domainCount}\n`);
|
|
295
|
+
}
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if (opts.monthly !== undefined) {
|
|
300
|
+
const { getMonthlyReport } = await import("./core/db.js");
|
|
301
|
+
const months = typeof opts.monthly === "string" ? parseInt(opts.monthly, 10) : 12;
|
|
302
|
+
const report = getMonthlyReport(months);
|
|
303
|
+
if (opts.json) { console.log(JSON.stringify(report, null, 2)); return; }
|
|
304
|
+
console.log(`\nMonthly Report (${months} months):\n`);
|
|
305
|
+
console.log(` ${"Month".padEnd(10)} ${"Costs".padEnd(12)} ${"Revenue".padEnd(12)} ${"Profit".padEnd(12)}`);
|
|
306
|
+
console.log(` ${"─".repeat(10)} ${"─".repeat(12)} ${"─".repeat(12)} ${"─".repeat(12)}`);
|
|
307
|
+
for (const m of report) {
|
|
308
|
+
console.log(` ${m.month.padEnd(10)} $${m.costs.toFixed(2).padStart(10)} $${m.revenue.toFixed(2).padStart(10)} $${m.profit.toFixed(2).padStart(10)}`);
|
|
309
|
+
}
|
|
310
|
+
console.log();
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (opts.pipeline) {
|
|
315
|
+
const { getPipeline } = await import("./core/db.js");
|
|
316
|
+
const pipeline = getPipeline();
|
|
317
|
+
if (opts.json) { console.log(JSON.stringify(pipeline, null, 2)); return; }
|
|
318
|
+
console.log(`\nAcquisition Pipeline (${pipeline.length} domains):\n`);
|
|
319
|
+
if (pipeline.length === 0) { console.log(" Empty. Use --pipeline-add <domain> to add."); }
|
|
320
|
+
for (const p of pipeline) {
|
|
321
|
+
console.log(` ${p.domain.padEnd(30)} ${p.status.padEnd(14)} ${p.priority}${p.max_bid ? ` max: $${p.max_bid}` : ""}${p.notes ? ` ${p.notes}` : ""}`);
|
|
322
|
+
}
|
|
323
|
+
console.log();
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
if (opts.pipelineAdd) {
|
|
328
|
+
const { addToPipeline } = await import("./core/db.js");
|
|
329
|
+
addToPipeline(opts.pipelineAdd);
|
|
330
|
+
console.log(`Added ${opts.pipelineAdd} to acquisition pipeline`);
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if (opts.alerts) {
|
|
335
|
+
const { getUnacknowledgedAlerts } = await import("./core/db.js");
|
|
336
|
+
const alerts = getUnacknowledgedAlerts();
|
|
337
|
+
if (opts.json) { console.log(JSON.stringify(alerts, null, 2)); return; }
|
|
338
|
+
console.log(`\nAlerts (${alerts.length} unacknowledged):\n`);
|
|
339
|
+
if (alerts.length === 0) { console.log(" No alerts."); }
|
|
340
|
+
for (const a of alerts) {
|
|
341
|
+
const icon = a.severity === "critical" ? "!!" : a.severity === "warning" ? "! " : " ";
|
|
342
|
+
console.log(` ${icon} [${a.severity}] ${a.domain}: ${a.message} (${a.created_at})`);
|
|
343
|
+
}
|
|
344
|
+
console.log();
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
if (opts.dismissAlerts) {
|
|
349
|
+
const { acknowledgeAllAlerts } = await import("./core/db.js");
|
|
350
|
+
acknowledgeAllAlerts();
|
|
351
|
+
console.log("All alerts dismissed");
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
if (opts.categories) {
|
|
356
|
+
const { getCategories } = await import("./core/db.js");
|
|
357
|
+
const categories = getCategories();
|
|
358
|
+
if (opts.json) { console.log(JSON.stringify(categories, null, 2)); return; }
|
|
359
|
+
console.log(`\nCategories:\n`);
|
|
360
|
+
for (const c of categories) {
|
|
361
|
+
console.log(` ${c.name.padEnd(20)} ${c.count} domain(s)${c.description ? ` — ${c.description}` : ""}`);
|
|
362
|
+
}
|
|
363
|
+
console.log();
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
if (opts.exportCsv) {
|
|
368
|
+
const { exportPortfolioCSV } = await import("./core/features/portfolio-bulk.js");
|
|
369
|
+
const path = exportPortfolioCSV(opts.exportCsv);
|
|
370
|
+
console.log(`Portfolio exported to ${path}`);
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
if (opts.exportTax) {
|
|
375
|
+
const year = parseInt(opts.exportTax, 10);
|
|
376
|
+
if (isNaN(year)) { console.error("Usage: --export-tax 2025"); process.exit(1); }
|
|
377
|
+
const { exportTaxCSV } = await import("./core/features/portfolio-bulk.js");
|
|
378
|
+
const path = exportTaxCSV(`tax-${year}.csv`, year);
|
|
379
|
+
console.log(`Tax data for ${year} exported to ${path}`);
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
if (opts.exportTransactions) {
|
|
384
|
+
const { exportTransactionsCSV } = await import("./core/features/portfolio-bulk.js");
|
|
385
|
+
const path = exportTransactionsCSV(opts.exportTransactions);
|
|
386
|
+
console.log(`Transactions exported to ${path}`);
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
if (opts.uploadS3) {
|
|
391
|
+
const { isS3Configured, uploadPortfolioExport } = await import("./core/features/s3-export.js");
|
|
392
|
+
if (!isS3Configured()) {
|
|
393
|
+
console.error("S3 not configured. Set S3_BUCKET, S3_ACCESS_KEY_ID, S3_SECRET_ACCESS_KEY in .env");
|
|
394
|
+
process.exit(1);
|
|
395
|
+
}
|
|
396
|
+
const { exportPortfolioCSV } = await import("./core/features/portfolio-bulk.js");
|
|
397
|
+
const csvPath = exportPortfolioCSV("/tmp/ds-portfolio-export.csv");
|
|
398
|
+
const csvContent = await Bun.file(csvPath).text();
|
|
399
|
+
const result = await uploadPortfolioExport(csvContent);
|
|
400
|
+
console.log(`Uploaded to S3: ${result.key}`);
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
if (opts.dashboard) {
|
|
405
|
+
const { getPortfolioDashboard, getTotalPortfolioValue } = await import("./core/db.js");
|
|
406
|
+
const { estimateAnnualRenewalCost } = await import("./core/features/portfolio-monitor.js");
|
|
407
|
+
const dash = getPortfolioDashboard();
|
|
408
|
+
const annualCost = estimateAnnualRenewalCost();
|
|
409
|
+
if (opts.json) { console.log(JSON.stringify(dash, null, 2)); return; }
|
|
410
|
+
console.log(`\n◆ Portfolio Dashboard`);
|
|
411
|
+
console.log(` Domains: ${dash.totalDomains} | Value: $${dash.totalValue.toFixed(0)} | Annual cost: ~$${annualCost.toFixed(0)}`);
|
|
412
|
+
console.log(` P&L: -$${dash.totalCosts.toFixed(0)} costs + $${dash.totalRevenue.toFixed(0)} revenue = $${dash.totalProfit.toFixed(0)} profit`);
|
|
413
|
+
console.log(` Expiring: ${dash.expiringIn30} in 30d, ${dash.expiringIn90} in 90d`);
|
|
414
|
+
console.log(` Alerts: ${dash.activeAlerts} | Pipeline: ${dash.pipelineCount}`);
|
|
415
|
+
if (Object.keys(dash.byStatus).length > 0) {
|
|
416
|
+
console.log(` Status: ${Object.entries(dash.byStatus).map(([s, c]) => `${s}(${c})`).join(" ")}`);
|
|
417
|
+
}
|
|
418
|
+
console.log();
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
if (opts.stats) {
|
|
423
|
+
const { getPortfolioStatsDb, closeDb } = await import("./core/db.js");
|
|
424
|
+
const stats = getPortfolioStatsDb();
|
|
425
|
+
if (opts.json) { console.log(JSON.stringify(stats, null, 2)); closeDb(); return; }
|
|
426
|
+
console.log(`\nPortfolio Stats:`);
|
|
427
|
+
console.log(` Domains: ${stats.total}`);
|
|
428
|
+
console.log(` Total spent: $${stats.totalSpent}`);
|
|
429
|
+
console.log(` Expiring (30d): ${stats.expiringIn30}`);
|
|
430
|
+
console.log(` Expiring (90d): ${stats.expiringIn90}`);
|
|
431
|
+
if (Object.keys(stats.byRegistrar).length > 0) {
|
|
432
|
+
console.log(` By registrar:`);
|
|
433
|
+
for (const [reg, count] of Object.entries(stats.byRegistrar)) {
|
|
434
|
+
console.log(` ${reg}: ${count}`);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
console.log();
|
|
438
|
+
closeDb();
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// Default: list all portfolio domains
|
|
443
|
+
const { getPortfolioDomains, closeDb } = await import("./core/db.js");
|
|
444
|
+
const domains = getPortfolioDomains();
|
|
445
|
+
if (opts.json) { console.log(JSON.stringify(domains, null, 2)); closeDb(); return; }
|
|
446
|
+
console.log(`\nDomain Portfolio (${domains.length} domains):\n`);
|
|
447
|
+
if (domains.length === 0) { console.log(" Empty. Use --add <domain> to add domains."); }
|
|
448
|
+
for (const d of domains) {
|
|
449
|
+
console.log(` ${d.domain.padEnd(30)} ${(d as any).status?.padEnd(12) || "active".padEnd(12)} ${d.registrar.padEnd(16)} ${d.expiry_date || "no expiry"} $${d.purchase_price}`);
|
|
450
|
+
}
|
|
451
|
+
console.log();
|
|
452
|
+
closeDb();
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
// ─── Config subcommand ───────────────────────────────────
|
|
456
|
+
|
|
457
|
+
program
|
|
458
|
+
.command("config")
|
|
459
|
+
.description("View or edit configuration")
|
|
460
|
+
.option("--path", "Show config file path")
|
|
461
|
+
.option("--show", "Show current config")
|
|
462
|
+
.option("--reset", "Reset to defaults")
|
|
463
|
+
.option("--set <key=value>", "Set a config value (e.g., concurrency=10)")
|
|
464
|
+
.action(async (opts: { path?: boolean; show?: boolean; reset?: boolean; set?: string }) => {
|
|
465
|
+
const { loadConfig, saveConfig, getConfigPath, resetConfig } = await import("./core/features/config.js");
|
|
466
|
+
|
|
467
|
+
if (opts.path) { console.log(getConfigPath()); return; }
|
|
468
|
+
if (opts.reset) { resetConfig(); console.log("Config reset to defaults"); return; }
|
|
469
|
+
if (opts.set) {
|
|
470
|
+
const [key, ...valueParts] = opts.set.split("=");
|
|
471
|
+
const value = valueParts.join("=");
|
|
472
|
+
if (!key || !value) { console.error("Usage: --set key=value"); process.exit(1); }
|
|
473
|
+
const config = loadConfig();
|
|
474
|
+
// Handle nested keys like "notifications.webhookUrl"
|
|
475
|
+
const keys = key.split(".");
|
|
476
|
+
let obj: Record<string, unknown> = config as unknown as Record<string, unknown>;
|
|
477
|
+
for (let i = 0; i < keys.length - 1; i++) {
|
|
478
|
+
const k = keys[i]!;
|
|
479
|
+
if (obj[k] === undefined || typeof obj[k] !== "object" || obj[k] === null) {
|
|
480
|
+
console.error(`Unknown key: ${key}`);
|
|
481
|
+
process.exit(1);
|
|
482
|
+
}
|
|
483
|
+
obj = obj[k] as Record<string, unknown>;
|
|
484
|
+
}
|
|
485
|
+
const lastKey = keys[keys.length - 1]!;
|
|
486
|
+
// Try to preserve type
|
|
487
|
+
const numVal = Number(value);
|
|
488
|
+
if (value === "true") obj[lastKey] = true;
|
|
489
|
+
else if (value === "false") obj[lastKey] = false;
|
|
490
|
+
else if (value === "null") obj[lastKey] = null;
|
|
491
|
+
else if (!isNaN(numVal) && value !== "") obj[lastKey] = numVal;
|
|
492
|
+
else obj[lastKey] = value;
|
|
493
|
+
saveConfig(config);
|
|
494
|
+
console.log(`Set ${key} = ${value}`);
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
// Default: show config
|
|
498
|
+
const config = loadConfig();
|
|
499
|
+
console.log(JSON.stringify(config, null, 2));
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
// ─── Expiring subcommand ────────────────────────────────
|
|
503
|
+
|
|
504
|
+
program
|
|
505
|
+
.command("expiring")
|
|
506
|
+
.description("Browse expiring/dropping domains")
|
|
507
|
+
.option("--tld <tld>", "Filter by TLD (e.g., com, net)")
|
|
508
|
+
.option("--min-age <years>", "Minimum domain age in years")
|
|
509
|
+
.option("-n, --limit <n>", "Max results", "20")
|
|
510
|
+
.option("--api-key <key>", "WhoisFreaks API key")
|
|
511
|
+
.option("--json", "Output as JSON")
|
|
512
|
+
.action(async (opts: { tld?: string; minAge?: string; limit: string; apiKey?: string; json?: boolean }) => {
|
|
513
|
+
const { getExpiringFeed } = await import("./core/features/expiring-feed.js");
|
|
514
|
+
const results = await getExpiringFeed({
|
|
515
|
+
apiKey: opts.apiKey || process.env.WHOISFREAKS_API_KEY,
|
|
516
|
+
tld: opts.tld,
|
|
517
|
+
minAge: opts.minAge ? parseInt(opts.minAge, 10) : undefined,
|
|
518
|
+
limit: parseInt(opts.limit, 10),
|
|
519
|
+
});
|
|
520
|
+
if (opts.json) { console.log(JSON.stringify(results, null, 2)); return; }
|
|
521
|
+
console.log(`\nExpiring Domains (${results.length} found):\n`);
|
|
522
|
+
if (results.length === 0) { console.log(" No results. Provide --api-key or set WHOISFREAKS_API_KEY env var."); }
|
|
523
|
+
for (const d of results) {
|
|
524
|
+
console.log(` ${d.domain} expires: ${d.expiryDate}${d.registrar ? ` [${d.registrar}]` : ""}${d.age ? ` age: ${d.age}` : ""}`);
|
|
525
|
+
}
|
|
526
|
+
console.log();
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
// ─── Drop catch subcommand ──────────────────────────────
|
|
530
|
+
|
|
531
|
+
program
|
|
532
|
+
.command("dropcatch <domain>")
|
|
533
|
+
.description("Monitor an expiring domain and auto-register when it drops")
|
|
534
|
+
.option("--interval <seconds>", "Poll interval in seconds", "30")
|
|
535
|
+
.option("--max-hours <hours>", "Maximum hours to monitor", "24")
|
|
536
|
+
.action(async (domain: string, opts: { interval: string; maxHours: string }) => {
|
|
537
|
+
const { createDropCatcher, formatDropCatchStatus } = await import("./core/features/drop-catch.js");
|
|
538
|
+
const { loadConfigFromEnv } = await import("./core/registrar.js");
|
|
539
|
+
const config = loadConfigFromEnv();
|
|
540
|
+
if (!config?.apiKey) { console.error("Registrar credentials required. Set REGISTRAR_PROVIDER and REGISTRAR_API_KEY."); process.exit(1); }
|
|
541
|
+
const intervalMs = parseInt(opts.interval, 10) * 1000;
|
|
542
|
+
const maxAttempts = Math.ceil((parseInt(opts.maxHours, 10) * 3600000) / intervalMs);
|
|
543
|
+
console.log(`\nDrop Catch: ${domain}`);
|
|
544
|
+
console.log(` Polling every ${opts.interval}s for up to ${opts.maxHours}h (${maxAttempts} attempts)\n`);
|
|
545
|
+
const catcher = createDropCatcher({
|
|
546
|
+
domain, registrarConfig: config, pollIntervalMs: intervalMs, maxAttempts,
|
|
547
|
+
onStatus: (s) => console.log(` ${formatDropCatchStatus(s)}`),
|
|
548
|
+
onSuccess: () => { console.log(`\n SUCCESS — ${domain} registered!\n`); process.exit(0); },
|
|
549
|
+
onFailed: (_d, err) => { console.log(`\n FAILED — ${err}\n`); process.exit(1); },
|
|
550
|
+
});
|
|
551
|
+
await catcher.start();
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
// ─── Recon subcommand ──────────────────────────────────
|
|
555
|
+
|
|
556
|
+
program
|
|
557
|
+
.command("recon <domain>")
|
|
558
|
+
.description("Full security reconnaissance scan — ports, headers, email security, WAF, paths, CORS, certificates, and more")
|
|
559
|
+
.option("--json", "Output as JSON")
|
|
560
|
+
.action(async (domain: string, opts: { json?: boolean }) => {
|
|
561
|
+
await checkDependencies();
|
|
562
|
+
const { whoisLookup, verifyAvailability } = await import("./core/whois.js");
|
|
563
|
+
|
|
564
|
+
if (!opts.json) {
|
|
565
|
+
console.log(`\n\x1b[1m=== RECON: ${domain} ===\x1b[0m\n`);
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
const results = await Promise.allSettled([
|
|
569
|
+
whoisLookup(domain),
|
|
570
|
+
lookupDns(domain),
|
|
571
|
+
httpProbe(domain),
|
|
572
|
+
checkWayback(domain),
|
|
573
|
+
rdapLookup(domain),
|
|
574
|
+
checkSsl(domain),
|
|
575
|
+
checkSocialMedia(domain),
|
|
576
|
+
detectTechStack(domain),
|
|
577
|
+
checkBlacklists(domain),
|
|
578
|
+
estimateBacklinks(domain),
|
|
579
|
+
scanPorts(domain),
|
|
580
|
+
reverseIpLookup(domain),
|
|
581
|
+
lookupAsn(domain),
|
|
582
|
+
checkEmailSecurity(domain),
|
|
583
|
+
checkZoneTransfer(domain),
|
|
584
|
+
queryCertTransparency(domain),
|
|
585
|
+
detectTakeover(domain),
|
|
586
|
+
auditSecurityHeaders(domain),
|
|
587
|
+
detectWaf(domain),
|
|
588
|
+
scanPaths(domain),
|
|
589
|
+
checkCors(domain),
|
|
590
|
+
]);
|
|
591
|
+
|
|
592
|
+
const val = <T,>(r: PromiseSettledResult<T>): T | null =>
|
|
593
|
+
r.status === "fulfilled" ? r.value : null;
|
|
594
|
+
|
|
595
|
+
const whois = val(results[0]!);
|
|
596
|
+
const dns = val(results[1]!);
|
|
597
|
+
const http = val(results[2]!);
|
|
598
|
+
const wayback = val(results[3]!);
|
|
599
|
+
const rdap = val(results[4]!);
|
|
600
|
+
const ssl = val(results[5]!);
|
|
601
|
+
const social = val(results[6]!);
|
|
602
|
+
const tech = val(results[7]!);
|
|
603
|
+
const blacklist = val(results[8]!);
|
|
604
|
+
const backlinks = val(results[9]!);
|
|
605
|
+
const ports = val(results[10]!) as PortScanResult | null;
|
|
606
|
+
const reverseIp = val(results[11]!) as ReverseIpResult | null;
|
|
607
|
+
const asn = val(results[12]!) as AsnResult | null;
|
|
608
|
+
const emailSec = val(results[13]!) as EmailSecurityResult | null;
|
|
609
|
+
const zoneXfer = val(results[14]!) as ZoneTransferResult | null;
|
|
610
|
+
const certs = val(results[15]!) as CertTransparencyResult | null;
|
|
611
|
+
const takeover = val(results[16]!) as TakeoverResult | null;
|
|
612
|
+
const secHeaders = val(results[17]!) as SecurityHeadersResult | null;
|
|
613
|
+
const waf = val(results[18]!) as WafResult | null;
|
|
614
|
+
const paths = val(results[19]!) as PathScanResult | null;
|
|
615
|
+
const cors = val(results[20]!) as CorsResult | null;
|
|
616
|
+
|
|
617
|
+
if (opts.json) {
|
|
618
|
+
console.log(JSON.stringify({
|
|
619
|
+
domain,
|
|
620
|
+
timestamp: new Date().toISOString(),
|
|
621
|
+
whois, dns, http, wayback, rdap, ssl, social, tech, blacklist, backlinks,
|
|
622
|
+
ports, reverseIp, asn, emailSecurity: emailSec, zoneTransfer: zoneXfer,
|
|
623
|
+
certTransparency: certs, takeover, securityHeaders: secHeaders, waf,
|
|
624
|
+
pathScan: paths, cors,
|
|
625
|
+
}, null, 2));
|
|
626
|
+
return;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// --- Text output ---
|
|
630
|
+
const g = "\x1b[32m", r = "\x1b[31m", y = "\x1b[33m", c = "\x1b[36m";
|
|
631
|
+
const b = "\x1b[1m", d = "\x1b[2m", x = "\x1b[0m";
|
|
632
|
+
|
|
633
|
+
// WHOIS
|
|
634
|
+
if (whois) {
|
|
635
|
+
console.log(`${b}WHOIS${x}`);
|
|
636
|
+
if (whois.available) console.log(` ${g}AVAILABLE${x}`);
|
|
637
|
+
else if (whois.expired) console.log(` ${y}EXPIRED${x}`);
|
|
638
|
+
else console.log(` ${r}TAKEN${x}`);
|
|
639
|
+
if (whois.registrar) console.log(` Registrar: ${whois.registrar}`);
|
|
640
|
+
if (whois.createdDate) console.log(` Created: ${whois.createdDate}`);
|
|
641
|
+
if (whois.expiryDate) console.log(` Expires: ${whois.expiryDate}`);
|
|
642
|
+
const age = calculateDomainAge(whois.createdDate);
|
|
643
|
+
if (age) console.log(` Age: ${age}`);
|
|
644
|
+
console.log();
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
// DNS
|
|
648
|
+
if (dns) {
|
|
649
|
+
console.log(`${b}DNS RECORDS${x}`);
|
|
650
|
+
if (dns.a.length) console.log(` A: ${dns.a.join(", ")}`);
|
|
651
|
+
if (dns.aaaa.length) console.log(` AAAA: ${dns.aaaa.join(", ")}`);
|
|
652
|
+
if (dns.mx.length) console.log(` MX: ${dns.mx.join(", ")}`);
|
|
653
|
+
if (dns.txt.length) console.log(` TXT: ${dns.txt.slice(0, 3).join(", ")}`);
|
|
654
|
+
if (dns.cname.length) console.log(` CNAME: ${dns.cname.join(", ")}`);
|
|
655
|
+
console.log();
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
// ASN / Network
|
|
659
|
+
if (asn && !asn.error) {
|
|
660
|
+
console.log(`${b}NETWORK${x}`);
|
|
661
|
+
if (asn.asn) console.log(` ASN: ${asn.asn}${asn.asnName ? ` (${asn.asnName})` : ""}`);
|
|
662
|
+
if (asn.org) console.log(` Org: ${asn.org}`);
|
|
663
|
+
if (asn.country) console.log(` Location: ${asn.city || ""}${asn.city && asn.country ? ", " : ""}${asn.country}`);
|
|
664
|
+
if (asn.isp) console.log(` ISP: ${asn.isp}`);
|
|
665
|
+
console.log();
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
// Port scan
|
|
669
|
+
if (ports && ports.openPorts.length > 0) {
|
|
670
|
+
console.log(`${b}${r}OPEN PORTS (${ports.openPorts.length})${x}`);
|
|
671
|
+
for (const p of ports.openPorts.slice(0, 20)) {
|
|
672
|
+
console.log(` ${y}${String(p.port).padEnd(6)}${x} ${p.service.padEnd(14)} ${d}${p.banner || ""}${x}`);
|
|
673
|
+
}
|
|
674
|
+
if (ports.ip) console.log(` ${d}IP: ${ports.ip} (${ports.scanTime}ms)${x}`);
|
|
675
|
+
console.log();
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
// Reverse IP
|
|
679
|
+
if (reverseIp && reverseIp.sharedDomains.length > 0) {
|
|
680
|
+
console.log(`${b}SHARED HOSTING (${reverseIp.sharedDomains.length} domains on ${reverseIp.ip})${x}`);
|
|
681
|
+
for (const dd of reverseIp.sharedDomains.slice(0, 10)) console.log(` ${dd}`);
|
|
682
|
+
if (reverseIp.sharedDomains.length > 10) console.log(` ${d}+${reverseIp.sharedDomains.length - 10} more${x}`);
|
|
683
|
+
console.log();
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
// SSL
|
|
687
|
+
if (ssl && !ssl.error) {
|
|
688
|
+
console.log(`${b}SSL CERTIFICATE${x}`);
|
|
689
|
+
console.log(` Valid: ${ssl.valid ? `${g}Yes${x}` : `${r}No${x}`}`);
|
|
690
|
+
if (ssl.issuer) console.log(` Issuer: ${ssl.issuer}`);
|
|
691
|
+
if (ssl.daysUntilExpiry !== null) console.log(` Expires: ${ssl.daysUntilExpiry}d`);
|
|
692
|
+
console.log();
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
// Email Security
|
|
696
|
+
if (emailSec) {
|
|
697
|
+
const ec = emailSec.grade <= "B" ? g : r;
|
|
698
|
+
console.log(`${b}EMAIL SECURITY (${ec}${emailSec.grade}${x}${b})${x}`);
|
|
699
|
+
console.log(` SPF: ${emailSec.spf.found ? `${g}Found${x}` : `${r}Missing${x}`}`);
|
|
700
|
+
console.log(` DKIM: ${emailSec.dkim.found ? `${g}Found (${emailSec.dkim.selector})${x}` : `${r}Missing${x}`}`);
|
|
701
|
+
console.log(` DMARC: ${emailSec.dmarc.found ? `${g}p=${emailSec.dmarc.policy || "?"}${x}` : `${r}Missing${x}`}`);
|
|
702
|
+
for (const issue of emailSec.issues.slice(0, 5)) console.log(` ${y}! ${issue}${x}`);
|
|
703
|
+
console.log();
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
// Security Headers
|
|
707
|
+
if (secHeaders && !secHeaders.error) {
|
|
708
|
+
const hc = secHeaders.grade <= "B" ? g : r;
|
|
709
|
+
console.log(`${b}SECURITY HEADERS (${hc}${secHeaders.grade}${x}${b} — ${secHeaders.score}/100)${x}`);
|
|
710
|
+
for (const h of secHeaders.missing.slice(0, 6)) console.log(` ${r}x ${h}${x}`);
|
|
711
|
+
for (const h of secHeaders.headers.filter((h) => h.status === "good").slice(0, 4)) console.log(` ${g}+ ${h.name}${x}`);
|
|
712
|
+
console.log();
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
// WAF
|
|
716
|
+
if (waf) {
|
|
717
|
+
console.log(`${b}WAF${x}`);
|
|
718
|
+
console.log(` ${waf.detected ? `${c}${waf.waf} (${waf.confidence})${x}` : `${d}None detected${x}`}`);
|
|
719
|
+
console.log();
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
// Zone Transfer
|
|
723
|
+
if (zoneXfer && zoneXfer.vulnerable) {
|
|
724
|
+
console.log(`${b}${r}!! ZONE TRANSFER VULNERABLE${x}`);
|
|
725
|
+
for (const ns of zoneXfer.vulnerableNs) console.log(` ${r}${ns}${x}`);
|
|
726
|
+
console.log();
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
// Cert Transparency
|
|
730
|
+
if (certs && certs.subdomains.length > 0) {
|
|
731
|
+
console.log(`${b}CERT TRANSPARENCY (${certs.subdomains.length} subdomains, ${certs.totalCerts} certs)${x}`);
|
|
732
|
+
for (const s of certs.subdomains.slice(0, 15)) console.log(` ${s}`);
|
|
733
|
+
if (certs.subdomains.length > 15) console.log(` ${d}+${certs.subdomains.length - 15} more${x}`);
|
|
734
|
+
console.log();
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
// Takeover
|
|
738
|
+
if (takeover && takeover.vulnerable) {
|
|
739
|
+
console.log(`${b}${r}!! SUBDOMAIN TAKEOVER${x}`);
|
|
740
|
+
for (const f of takeover.findings.filter((f) => f.status === "vulnerable")) {
|
|
741
|
+
console.log(` ${r}${f.subdomain} -> ${f.service}${x}`);
|
|
742
|
+
}
|
|
743
|
+
console.log();
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
// Path Scanner
|
|
747
|
+
if (paths && paths.findings.length > 0) {
|
|
748
|
+
console.log(`${b}${r}EXPOSED PATHS (${paths.findings.length})${x}`);
|
|
749
|
+
for (const f of paths.findings.slice(0, 15)) {
|
|
750
|
+
const fc = f.severity === "critical" ? r : f.severity === "high" ? y : d;
|
|
751
|
+
console.log(` ${fc}${f.severity === "critical" ? "!!" : f.severity === "high" ? "! " : " "} ${f.path} [${f.status}]${x}`);
|
|
752
|
+
}
|
|
753
|
+
console.log();
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
// CORS
|
|
757
|
+
if (cors && cors.vulnerable) {
|
|
758
|
+
console.log(`${b}${r}!! CORS MISCONFIGURATION${x}`);
|
|
759
|
+
for (const f of cors.findings.filter((f) => f.allowed).slice(0, 5)) {
|
|
760
|
+
console.log(` ${r}${f.detail}${x}`);
|
|
761
|
+
}
|
|
762
|
+
console.log();
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
// Tech Stack
|
|
766
|
+
if (tech && tech.technologies.length > 0) {
|
|
767
|
+
console.log(`${b}TECH STACK${x}`);
|
|
768
|
+
if (tech.cms) console.log(` CMS: ${tech.cms}`);
|
|
769
|
+
if (tech.framework) console.log(` Framework: ${tech.framework}`);
|
|
770
|
+
if (tech.cdn) console.log(` CDN: ${tech.cdn}`);
|
|
771
|
+
console.log();
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
// Blacklist
|
|
775
|
+
if (blacklist) {
|
|
776
|
+
if (blacklist.listed) {
|
|
777
|
+
const names = blacklist.lists.filter((l) => l.listed).map((l) => l.name).join(", ");
|
|
778
|
+
console.log(`${b}${r}BLACKLISTED: ${names}${x}`);
|
|
779
|
+
} else {
|
|
780
|
+
console.log(`${b}REPUTATION${x}: ${g}clean (${blacklist.cleanCount}/${blacklist.lists.length})${x}`);
|
|
781
|
+
}
|
|
782
|
+
console.log();
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
// Backlinks
|
|
786
|
+
if (backlinks) {
|
|
787
|
+
const parts: string[] = [];
|
|
788
|
+
if (backlinks.pageRank !== null) parts.push(`PageRank: ${backlinks.pageRank}`);
|
|
789
|
+
if (backlinks.commonCrawlPages !== null) parts.push(`CC pages: ~${backlinks.commonCrawlPages}`);
|
|
790
|
+
if (parts.length > 0) console.log(`${b}AUTHORITY${x}: ${parts.join(", ")}`);
|
|
791
|
+
console.log();
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
// Social
|
|
795
|
+
if (social) {
|
|
796
|
+
const avail = social.filter((s) => s.available && !s.error);
|
|
797
|
+
if (avail.length > 0) {
|
|
798
|
+
console.log(`${b}SOCIAL AVAILABLE${x}: ${avail.map((s) => s.platform).join(", ")}`);
|
|
799
|
+
console.log();
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
console.log(`${d}Scan complete.${x}\n`);
|
|
804
|
+
|
|
805
|
+
// Clean up database connection
|
|
806
|
+
try {
|
|
807
|
+
const { closeDb: closeReconDb } = await import("./core/db.js");
|
|
808
|
+
closeReconDb();
|
|
809
|
+
} catch {}
|
|
810
|
+
});
|
|
811
|
+
|
|
812
|
+
// ─── Database management subcommand ─────────────────────
|
|
813
|
+
|
|
814
|
+
program
|
|
815
|
+
.command("db")
|
|
816
|
+
.description("Database management")
|
|
817
|
+
.option("--stats", "Show database statistics")
|
|
818
|
+
.option("--clear-cache", "Clear all cached scan results")
|
|
819
|
+
.option("--import-legacy", "Import data from legacy JSON files")
|
|
820
|
+
.option("--history <domain>", "Show scan history for a domain")
|
|
821
|
+
.option("--json", "Output as JSON")
|
|
822
|
+
.action(async (opts: { stats?: boolean; clearCache?: boolean; importLegacy?: boolean; history?: string; json?: boolean }) => {
|
|
823
|
+
const { getDbStats, clearCache, importLegacyPortfolio, importLegacySessions, getScanHistory, closeDb } = await import("./core/db.js");
|
|
824
|
+
const { PORTFOLIO_FILE, SESSION_DIR } = await import("./core/paths.js");
|
|
825
|
+
|
|
826
|
+
if (opts.clearCache) {
|
|
827
|
+
const count = clearCache();
|
|
828
|
+
console.log(`Cleared ${count} cached entries`);
|
|
829
|
+
closeDb();
|
|
830
|
+
return;
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
if (opts.importLegacy) {
|
|
834
|
+
const portfolioCount = importLegacyPortfolio(PORTFOLIO_FILE);
|
|
835
|
+
const sessionCount = importLegacySessions(SESSION_DIR);
|
|
836
|
+
console.log(`Imported: ${portfolioCount} portfolio domains, ${sessionCount} sessions`);
|
|
837
|
+
closeDb();
|
|
838
|
+
return;
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
if (opts.history) {
|
|
842
|
+
const history = getScanHistory(opts.history, 20);
|
|
843
|
+
if (opts.json) { console.log(JSON.stringify(history, null, 2)); closeDb(); return; }
|
|
844
|
+
console.log(`\nScan history for ${opts.history} (${history.length} scans):\n`);
|
|
845
|
+
for (const h of history) {
|
|
846
|
+
console.log(` ${h.scanned_at} ${h.status}${h.score ? ` score: ${h.score}` : ""}`);
|
|
847
|
+
}
|
|
848
|
+
console.log();
|
|
849
|
+
closeDb();
|
|
850
|
+
return;
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
// Default: show stats
|
|
854
|
+
const stats = getDbStats();
|
|
855
|
+
if (opts.json) { console.log(JSON.stringify(stats, null, 2)); closeDb(); return; }
|
|
856
|
+
console.log(`\nDatabase Statistics:`);
|
|
857
|
+
console.log(` Domains tracked: ${stats.totalDomains}`);
|
|
858
|
+
console.log(` Total scans: ${stats.totalScans}`);
|
|
859
|
+
console.log(` Sessions: ${stats.totalSessions}`);
|
|
860
|
+
console.log(` Portfolio: ${stats.portfolioSize}`);
|
|
861
|
+
console.log(` WHOIS snapshots: ${stats.whoisSnapshots}`);
|
|
862
|
+
console.log(` Cache entries: ${stats.cacheEntries}`);
|
|
863
|
+
console.log(` Database size: ${(stats.dbSizeBytes / 1024).toFixed(1)} KB`);
|
|
864
|
+
console.log();
|
|
865
|
+
closeDb();
|
|
866
|
+
});
|
|
867
|
+
|
|
868
|
+
// ─── Serve subcommand ──────────────────────────────────
|
|
869
|
+
|
|
870
|
+
program
|
|
871
|
+
.command("serve")
|
|
872
|
+
.description("Start the marketplace server")
|
|
873
|
+
.option("--port <port>", "Port number", "3000")
|
|
874
|
+
.action(async (opts: { port: string }) => {
|
|
875
|
+
process.env.MARKET_PORT = opts.port;
|
|
876
|
+
await import("../marketplace/index.js");
|
|
877
|
+
});
|
|
878
|
+
|
|
879
|
+
// ─── Market subcommand ─────────────────────────────────
|
|
880
|
+
|
|
881
|
+
const market = program
|
|
882
|
+
.command("market")
|
|
883
|
+
.description("Domain marketplace — buy, sell, and trade domains");
|
|
884
|
+
|
|
885
|
+
market
|
|
886
|
+
.command("signup")
|
|
887
|
+
.description("Create a marketplace account")
|
|
888
|
+
.option("--server <url>", "Marketplace server URL", "http://localhost:3000")
|
|
889
|
+
.option("--email <email>", "Email address")
|
|
890
|
+
.option("--password <password>", "Password (min 8 chars)")
|
|
891
|
+
.option("--name <name>", "Display name")
|
|
892
|
+
.action(async (opts: { server: string; email?: string; password?: string; name?: string }) => {
|
|
893
|
+
const { signUp } = await import("./market-client.js");
|
|
894
|
+
let name = opts.name;
|
|
895
|
+
let email = opts.email;
|
|
896
|
+
let password = opts.password;
|
|
897
|
+
|
|
898
|
+
if (!name || !email || !password) {
|
|
899
|
+
const readline = await import("readline");
|
|
900
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
901
|
+
const ask = (q: string): Promise<string> => new Promise((r) => rl.question(q, r));
|
|
902
|
+
if (!name) name = await ask("Name: ");
|
|
903
|
+
if (!email) email = await ask("Email: ");
|
|
904
|
+
if (!password) password = await ask("Password (min 8 chars): ");
|
|
905
|
+
rl.close();
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
const result = await signUp(email!, password!, name!, opts.server);
|
|
909
|
+
if (result.success) {
|
|
910
|
+
console.log(`\n✓ Account created. Signed in as ${email}\n`);
|
|
911
|
+
} else {
|
|
912
|
+
console.error(`\n✗ ${result.error}\n`);
|
|
913
|
+
process.exit(1);
|
|
914
|
+
}
|
|
915
|
+
});
|
|
916
|
+
|
|
917
|
+
market
|
|
918
|
+
.command("login")
|
|
919
|
+
.description("Sign in to the marketplace")
|
|
920
|
+
.option("--server <url>", "Marketplace server URL", "http://localhost:3000")
|
|
921
|
+
.option("--email <email>", "Email address")
|
|
922
|
+
.option("--password <password>", "Password")
|
|
923
|
+
.action(async (opts: { server: string; email?: string; password?: string }) => {
|
|
924
|
+
const { signIn } = await import("./market-client.js");
|
|
925
|
+
let email = opts.email;
|
|
926
|
+
let password = opts.password;
|
|
927
|
+
|
|
928
|
+
if (!email || !password) {
|
|
929
|
+
const readline = await import("readline");
|
|
930
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
931
|
+
const ask = (q: string): Promise<string> => new Promise((r) => rl.question(q, r));
|
|
932
|
+
if (!email) email = await ask("Email: ");
|
|
933
|
+
if (!password) password = await ask("Password: ");
|
|
934
|
+
rl.close();
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
const result = await signIn(email!, password!, opts.server);
|
|
938
|
+
if (result.success) {
|
|
939
|
+
console.log(`\n✓ Signed in as ${email}\n`);
|
|
940
|
+
} else {
|
|
941
|
+
console.error(`\n✗ ${result.error}\n`);
|
|
942
|
+
process.exit(1);
|
|
943
|
+
}
|
|
944
|
+
});
|
|
945
|
+
|
|
946
|
+
market
|
|
947
|
+
.command("logout")
|
|
948
|
+
.description("Sign out of the marketplace")
|
|
949
|
+
.action(async () => {
|
|
950
|
+
const { signOut } = await import("./market-client.js");
|
|
951
|
+
signOut();
|
|
952
|
+
console.log("Signed out.");
|
|
953
|
+
});
|
|
954
|
+
|
|
955
|
+
market
|
|
956
|
+
.command("whoami")
|
|
957
|
+
.description("Show current auth status")
|
|
958
|
+
.action(async () => {
|
|
959
|
+
const { getAuthInfo, getServerUrl } = await import("./market-client.js");
|
|
960
|
+
const info = getAuthInfo();
|
|
961
|
+
if (info) {
|
|
962
|
+
console.log(`\nSigned in as: ${info.name} (${info.email})`);
|
|
963
|
+
console.log(`Server: ${getServerUrl()}\n`);
|
|
964
|
+
} else {
|
|
965
|
+
console.log("\nNot signed in. Use: domain-sniper market login\n");
|
|
966
|
+
}
|
|
967
|
+
});
|
|
968
|
+
|
|
969
|
+
market
|
|
970
|
+
.command("browse")
|
|
971
|
+
.description("Browse domain listings")
|
|
972
|
+
.option("-q, --query <search>", "Search domains")
|
|
973
|
+
.option("--category <cat>", "Filter by category")
|
|
974
|
+
.option("--min <price>", "Minimum price")
|
|
975
|
+
.option("--max <price>", "Maximum price")
|
|
976
|
+
.option("--verified", "Only verified listings")
|
|
977
|
+
.option("--sort <field>", "Sort: newest, price_asc, price_desc, popular", "newest")
|
|
978
|
+
.option("-n, --limit <n>", "Results per page", "20")
|
|
979
|
+
.option("--json", "Output as JSON")
|
|
980
|
+
.action(async (opts: any) => {
|
|
981
|
+
const { browseListings } = await import("./market-client.js");
|
|
982
|
+
const result = await browseListings({
|
|
983
|
+
search: opts.query, category: opts.category,
|
|
984
|
+
minPrice: opts.min ? parseFloat(opts.min) : undefined,
|
|
985
|
+
maxPrice: opts.max ? parseFloat(opts.max) : undefined,
|
|
986
|
+
verified: opts.verified, sort: opts.sort,
|
|
987
|
+
limit: parseInt(opts.limit, 10),
|
|
988
|
+
});
|
|
989
|
+
if (!result.ok) { console.error("Failed to fetch listings:", result.data?.error); process.exit(1); }
|
|
990
|
+
if (opts.json) { console.log(JSON.stringify(result.data, null, 2)); return; }
|
|
991
|
+
|
|
992
|
+
const { listings, total } = result.data;
|
|
993
|
+
console.log(`\n◆ Domain Marketplace (${total} listings)\n`);
|
|
994
|
+
if (listings.length === 0) { console.log(" No listings found.\n"); return; }
|
|
995
|
+
console.log(` ${"DOMAIN".padEnd(30)} ${"PRICE".padStart(10)} ${"STATUS".padEnd(10)} VERIFIED`);
|
|
996
|
+
console.log(` ${"─".repeat(30)} ${"─".repeat(10)} ${"─".repeat(10)} ${"─".repeat(8)}`);
|
|
997
|
+
for (const l of listings) {
|
|
998
|
+
console.log(` ${l.domain.padEnd(30)} ${"$" + l.asking_price.toFixed(2).padStart(9)} ${l.status.padEnd(10)} ${l.verified ? "✓" : " "}`);
|
|
999
|
+
}
|
|
1000
|
+
console.log();
|
|
1001
|
+
});
|
|
1002
|
+
|
|
1003
|
+
market
|
|
1004
|
+
.command("list <domain>")
|
|
1005
|
+
.description("List a domain for sale")
|
|
1006
|
+
.requiredOption("-p, --price <amount>", "Asking price")
|
|
1007
|
+
.option("-t, --title <title>", "Listing title")
|
|
1008
|
+
.option("-d, --description <desc>", "Description")
|
|
1009
|
+
.option("--min-offer <amount>", "Minimum acceptable offer")
|
|
1010
|
+
.option("--buy-now", "Enable buy-now at asking price")
|
|
1011
|
+
.option("--category <cat>", "Category")
|
|
1012
|
+
.action(async (domain: string, opts: any) => {
|
|
1013
|
+
const { isLoggedIn, createListingApi } = await import("./market-client.js");
|
|
1014
|
+
if (!isLoggedIn()) { console.error("Not signed in. Use: domain-sniper market login"); process.exit(1); }
|
|
1015
|
+
|
|
1016
|
+
const result = await createListingApi(domain, parseFloat(opts.price), {
|
|
1017
|
+
title: opts.title, description: opts.description,
|
|
1018
|
+
minOffer: opts.minOffer ? parseFloat(opts.minOffer) : undefined,
|
|
1019
|
+
buyNow: opts.buyNow, category: opts.category,
|
|
1020
|
+
});
|
|
1021
|
+
if (!result.ok) { console.error("Failed:", result.data?.error); process.exit(1); }
|
|
1022
|
+
|
|
1023
|
+
const { listing, verification } = result.data;
|
|
1024
|
+
console.log(`\n✓ Listing created (#${listing.id}): ${domain} at $${listing.asking_price}`);
|
|
1025
|
+
console.log(`\nVerify ownership to activate listing:\n`);
|
|
1026
|
+
console.log(`Option 1 — DNS TXT Record:`);
|
|
1027
|
+
console.log(` ${verification.instructions.dns}\n`);
|
|
1028
|
+
console.log(`Option 2 — HTTP File:`);
|
|
1029
|
+
console.log(` ${verification.instructions.http}\n`);
|
|
1030
|
+
console.log(`Option 3 — Meta Tag:`);
|
|
1031
|
+
console.log(` ${verification.instructions.meta}\n`);
|
|
1032
|
+
});
|
|
1033
|
+
|
|
1034
|
+
market
|
|
1035
|
+
.command("verify <domain>")
|
|
1036
|
+
.description("Verify domain ownership for a listing")
|
|
1037
|
+
.action(async (domain: string) => {
|
|
1038
|
+
const { isLoggedIn, getMyListings, verifyListingApi } = await import("./market-client.js");
|
|
1039
|
+
if (!isLoggedIn()) { console.error("Not signed in."); process.exit(1); }
|
|
1040
|
+
|
|
1041
|
+
const myListings = await getMyListings();
|
|
1042
|
+
if (!myListings.ok) { console.error("Failed to fetch listings"); process.exit(1); }
|
|
1043
|
+
|
|
1044
|
+
const listing = myListings.data.find((l: any) => l.domain === domain && !l.verified);
|
|
1045
|
+
if (!listing) { console.error(`No unverified listing found for ${domain}`); process.exit(1); }
|
|
1046
|
+
|
|
1047
|
+
console.log(`\nVerifying ${domain}...`);
|
|
1048
|
+
const result = await verifyListingApi(listing.id);
|
|
1049
|
+
if (result.ok && result.data.verified) {
|
|
1050
|
+
console.log(`✓ Verified via ${result.data.method}! Listing is now active.\n`);
|
|
1051
|
+
} else {
|
|
1052
|
+
console.error(`✗ ${result.data.error || "Verification failed"}`);
|
|
1053
|
+
console.error(` Make sure your DNS TXT record, HTTP file, or meta tag is set up.\n`);
|
|
1054
|
+
process.exit(1);
|
|
1055
|
+
}
|
|
1056
|
+
});
|
|
1057
|
+
|
|
1058
|
+
market
|
|
1059
|
+
.command("offer")
|
|
1060
|
+
.description("Make an offer on a listing")
|
|
1061
|
+
.requiredOption("-l, --listing <id>", "Listing ID")
|
|
1062
|
+
.requiredOption("-a, --amount <price>", "Offer amount")
|
|
1063
|
+
.option("-m, --message <msg>", "Message to seller")
|
|
1064
|
+
.action(async (opts: any) => {
|
|
1065
|
+
const { isLoggedIn, makeOffer } = await import("./market-client.js");
|
|
1066
|
+
if (!isLoggedIn()) { console.error("Not signed in."); process.exit(1); }
|
|
1067
|
+
|
|
1068
|
+
const result = await makeOffer(parseInt(opts.listing, 10), parseFloat(opts.amount), opts.message || "");
|
|
1069
|
+
if (result.ok) {
|
|
1070
|
+
console.log(`\n✓ Offer of $${opts.amount} submitted on listing #${opts.listing}\n`);
|
|
1071
|
+
} else {
|
|
1072
|
+
console.error(`✗ ${result.data?.error || "Failed"}\n`);
|
|
1073
|
+
process.exit(1);
|
|
1074
|
+
}
|
|
1075
|
+
});
|
|
1076
|
+
|
|
1077
|
+
market
|
|
1078
|
+
.command("my-listings")
|
|
1079
|
+
.description("Show your listings")
|
|
1080
|
+
.option("--json", "Output as JSON")
|
|
1081
|
+
.action(async (opts: any) => {
|
|
1082
|
+
const { isLoggedIn, getMyListings } = await import("./market-client.js");
|
|
1083
|
+
if (!isLoggedIn()) { console.error("Not signed in."); process.exit(1); }
|
|
1084
|
+
|
|
1085
|
+
const result = await getMyListings();
|
|
1086
|
+
if (!result.ok) { console.error("Failed"); process.exit(1); }
|
|
1087
|
+
if (opts.json) { console.log(JSON.stringify(result.data, null, 2)); return; }
|
|
1088
|
+
|
|
1089
|
+
const listings = result.data;
|
|
1090
|
+
console.log(`\nYour Listings (${listings.length}):\n`);
|
|
1091
|
+
for (const l of listings) {
|
|
1092
|
+
console.log(` #${l.id} ${l.domain.padEnd(25)} $${l.asking_price} ${l.status} ${l.verified ? "✓ verified" : "unverified"}`);
|
|
1093
|
+
}
|
|
1094
|
+
console.log();
|
|
1095
|
+
});
|
|
1096
|
+
|
|
1097
|
+
market
|
|
1098
|
+
.command("my-offers")
|
|
1099
|
+
.description("Show offers you've made or received")
|
|
1100
|
+
.option("--role <role>", "buyer or seller", "buyer")
|
|
1101
|
+
.option("--json", "Output as JSON")
|
|
1102
|
+
.action(async (opts: any) => {
|
|
1103
|
+
const { isLoggedIn, getMyOffers } = await import("./market-client.js");
|
|
1104
|
+
if (!isLoggedIn()) { console.error("Not signed in."); process.exit(1); }
|
|
1105
|
+
|
|
1106
|
+
const result = await getMyOffers(opts.role);
|
|
1107
|
+
if (!result.ok) { console.error("Failed"); process.exit(1); }
|
|
1108
|
+
if (opts.json) { console.log(JSON.stringify(result.data, null, 2)); return; }
|
|
1109
|
+
|
|
1110
|
+
const offers = result.data;
|
|
1111
|
+
console.log(`\nYour Offers as ${opts.role} (${offers.length}):\n`);
|
|
1112
|
+
for (const o of offers) {
|
|
1113
|
+
console.log(` #${o.id} ${o.domain.padEnd(25)} $${o.amount} ${o.status}`);
|
|
1114
|
+
}
|
|
1115
|
+
console.log();
|
|
1116
|
+
});
|
|
1117
|
+
|
|
1118
|
+
market
|
|
1119
|
+
.command("stats")
|
|
1120
|
+
.description("Show marketplace statistics")
|
|
1121
|
+
.option("--json", "Output as JSON")
|
|
1122
|
+
.action(async (opts: any) => {
|
|
1123
|
+
const { getMarketStatsApi } = await import("./market-client.js");
|
|
1124
|
+
const result = await getMarketStatsApi();
|
|
1125
|
+
if (!result.ok) { console.error("Cannot reach marketplace server"); process.exit(1); }
|
|
1126
|
+
if (opts.json) { console.log(JSON.stringify(result.data, null, 2)); return; }
|
|
1127
|
+
|
|
1128
|
+
const s = result.data;
|
|
1129
|
+
console.log(`\n◆ Marketplace Stats`);
|
|
1130
|
+
console.log(` Total listings: ${s.totalListings}`);
|
|
1131
|
+
console.log(` Active: ${s.activeListings}`);
|
|
1132
|
+
console.log(` Total offers: ${s.totalOffers}`);
|
|
1133
|
+
console.log(` Users: ${s.totalUsers}\n`);
|
|
1134
|
+
});
|
|
1135
|
+
|
|
1136
|
+
// ─── Proxy commands ──────────────────────────────────────
|
|
1137
|
+
|
|
1138
|
+
const proxy = program
|
|
1139
|
+
.command("proxy")
|
|
1140
|
+
.description("HTTP intercepting proxy — capture and analyze traffic");
|
|
1141
|
+
|
|
1142
|
+
proxy
|
|
1143
|
+
.command("start")
|
|
1144
|
+
.description("Start the intercepting proxy")
|
|
1145
|
+
.option("-p, --port <port>", "Proxy port", "8080")
|
|
1146
|
+
.option("--https", "Enable HTTPS interception (requires CA cert)", false)
|
|
1147
|
+
.option("--filter <hosts>", "Only intercept these hosts (comma-separated)")
|
|
1148
|
+
.action(async (opts: { port: string; https: boolean; filter?: string }) => {
|
|
1149
|
+
const { startProxy } = await import("./proxy/server.js");
|
|
1150
|
+
const filterHosts = opts.filter ? opts.filter.split(",").map((h) => h.trim()) : undefined;
|
|
1151
|
+
|
|
1152
|
+
startProxy({
|
|
1153
|
+
port: parseInt(opts.port, 10),
|
|
1154
|
+
httpsInterception: opts.https,
|
|
1155
|
+
filterHosts,
|
|
1156
|
+
onRequest: (entry) => {
|
|
1157
|
+
const statusColor = entry.statusCode && entry.statusCode < 400 ? "\x1b[32m" : "\x1b[31m";
|
|
1158
|
+
console.log(` ${entry.method.padEnd(6)} ${statusColor}${entry.statusCode || "ERR"}\x1b[0m ${entry.host}${entry.url.replace(/^https?:\/\/[^/]+/, "")} (${entry.durationMs}ms, ${entry.size}b)`);
|
|
1159
|
+
},
|
|
1160
|
+
});
|
|
1161
|
+
|
|
1162
|
+
// Keep running
|
|
1163
|
+
console.log("Press Ctrl+C to stop\n");
|
|
1164
|
+
});
|
|
1165
|
+
|
|
1166
|
+
proxy
|
|
1167
|
+
.command("history")
|
|
1168
|
+
.description("Browse intercepted requests")
|
|
1169
|
+
.option("--host <host>", "Filter by host")
|
|
1170
|
+
.option("--method <method>", "Filter by method (GET, POST, etc.)")
|
|
1171
|
+
.option("--search <query>", "Search in URL/body")
|
|
1172
|
+
.option("--flagged", "Show only flagged requests")
|
|
1173
|
+
.option("-n, --limit <n>", "Number of results", "30")
|
|
1174
|
+
.option("--json", "Output as JSON")
|
|
1175
|
+
.action(async (opts: { host?: string; method?: string; search?: string; flagged?: boolean; limit: string; json?: boolean }) => {
|
|
1176
|
+
const { getRequests } = await import("./proxy/db.js");
|
|
1177
|
+
const result = getRequests({
|
|
1178
|
+
host: opts.host, method: opts.method, search: opts.search,
|
|
1179
|
+
flagged: opts.flagged, limit: parseInt(opts.limit, 10),
|
|
1180
|
+
});
|
|
1181
|
+
if (opts.json) { console.log(JSON.stringify(result, null, 2)); return; }
|
|
1182
|
+
|
|
1183
|
+
console.log(`\nIntercepted Requests (${result.total} total):\n`);
|
|
1184
|
+
console.log(` ${"ID".padEnd(6)} ${"METHOD".padEnd(8)} ${"STATUS".padEnd(8)} ${"HOST".padEnd(30)} ${"PATH".padEnd(30)} ${"MS".padEnd(6)}`);
|
|
1185
|
+
console.log(` ${"─".repeat(6)} ${"─".repeat(8)} ${"─".repeat(8)} ${"─".repeat(30)} ${"─".repeat(30)} ${"─".repeat(6)}`);
|
|
1186
|
+
for (const r of result.requests) {
|
|
1187
|
+
const statusColor = r.status_code && r.status_code < 400 ? "\x1b[32m" : r.status_code && r.status_code < 500 ? "\x1b[33m" : "\x1b[31m";
|
|
1188
|
+
console.log(` ${String(r.id).padEnd(6)} ${r.method.padEnd(8)} ${statusColor}${String(r.status_code || "ERR").padEnd(8)}\x1b[0m ${r.host.slice(0, 30).padEnd(30)} ${r.path.slice(0, 30).padEnd(30)} ${String(r.duration_ms).padEnd(6)}`);
|
|
1189
|
+
}
|
|
1190
|
+
console.log();
|
|
1191
|
+
});
|
|
1192
|
+
|
|
1193
|
+
proxy
|
|
1194
|
+
.command("inspect <id>")
|
|
1195
|
+
.description("View full details of an intercepted request")
|
|
1196
|
+
.option("--json", "Output as JSON")
|
|
1197
|
+
.action(async (id: string, opts: { json?: boolean }) => {
|
|
1198
|
+
const { getRequest } = await import("./proxy/db.js");
|
|
1199
|
+
const req = getRequest(parseInt(id, 10));
|
|
1200
|
+
if (!req) { console.error("Request not found"); process.exit(1); }
|
|
1201
|
+
|
|
1202
|
+
if (opts.json) { console.log(JSON.stringify(req, null, 2)); return; }
|
|
1203
|
+
|
|
1204
|
+
console.log(`\n── Request #${req.id} ──────────────────────────────`);
|
|
1205
|
+
console.log(`${req.method} ${req.url}`);
|
|
1206
|
+
console.log(`Host: ${req.host}`);
|
|
1207
|
+
console.log(`Time: ${req.intercepted_at} (${req.duration_ms}ms)\n`);
|
|
1208
|
+
|
|
1209
|
+
console.log("── Request Headers ─────────────────────────────");
|
|
1210
|
+
try {
|
|
1211
|
+
const headers = JSON.parse(req.request_headers) as Record<string, string>;
|
|
1212
|
+
for (const [k, v] of Object.entries(headers)) console.log(` ${k}: ${v}`);
|
|
1213
|
+
} catch {}
|
|
1214
|
+
|
|
1215
|
+
if (req.request_body) {
|
|
1216
|
+
console.log("\n── Request Body ────────────────────────────────");
|
|
1217
|
+
console.log(req.request_body.slice(0, 2000));
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
console.log(`\n── Response (${req.status_code}) ───────────────────────────`);
|
|
1221
|
+
try {
|
|
1222
|
+
const headers = JSON.parse(req.response_headers) as Record<string, string>;
|
|
1223
|
+
for (const [k, v] of Object.entries(headers)) console.log(` ${k}: ${v}`);
|
|
1224
|
+
} catch {}
|
|
1225
|
+
|
|
1226
|
+
if (req.response_body) {
|
|
1227
|
+
console.log("\n── Response Body ───────────────────────────────");
|
|
1228
|
+
console.log(req.response_body.slice(0, 5000));
|
|
1229
|
+
}
|
|
1230
|
+
console.log();
|
|
1231
|
+
});
|
|
1232
|
+
|
|
1233
|
+
proxy
|
|
1234
|
+
.command("replay <id>")
|
|
1235
|
+
.description("Replay an intercepted request")
|
|
1236
|
+
.action(async (id: string) => {
|
|
1237
|
+
const { getRequest } = await import("./proxy/db.js");
|
|
1238
|
+
const req = getRequest(parseInt(id, 10));
|
|
1239
|
+
if (!req) { console.error("Request not found"); process.exit(1); }
|
|
1240
|
+
|
|
1241
|
+
console.log(`\nReplaying: ${req.method} ${req.url}`);
|
|
1242
|
+
const startTime = Date.now();
|
|
1243
|
+
try {
|
|
1244
|
+
const headers = JSON.parse(req.request_headers) as Record<string, string>;
|
|
1245
|
+
const resp = await fetch(req.url, {
|
|
1246
|
+
method: req.method,
|
|
1247
|
+
headers,
|
|
1248
|
+
body: req.method !== "GET" && req.method !== "HEAD" ? req.request_body : undefined,
|
|
1249
|
+
});
|
|
1250
|
+
const body = await resp.text();
|
|
1251
|
+
const duration = Date.now() - startTime;
|
|
1252
|
+
console.log(`Status: ${resp.status} (${duration}ms, ${body.length} bytes)`);
|
|
1253
|
+
console.log(body.slice(0, 2000));
|
|
1254
|
+
} catch (err: unknown) {
|
|
1255
|
+
console.error(`Failed: ${err instanceof Error ? err.message : "unknown"}`);
|
|
1256
|
+
}
|
|
1257
|
+
console.log();
|
|
1258
|
+
});
|
|
1259
|
+
|
|
1260
|
+
proxy
|
|
1261
|
+
.command("clear")
|
|
1262
|
+
.description("Clear intercepted request history")
|
|
1263
|
+
.option("--host <host>", "Only clear requests for this host")
|
|
1264
|
+
.action(async (opts: { host?: string }) => {
|
|
1265
|
+
const { clearRequests } = await import("./proxy/db.js");
|
|
1266
|
+
const count = clearRequests(opts.host);
|
|
1267
|
+
console.log(`Cleared ${count} requests${opts.host ? ` for ${opts.host}` : ""}`);
|
|
1268
|
+
});
|
|
1269
|
+
|
|
1270
|
+
proxy
|
|
1271
|
+
.command("stats")
|
|
1272
|
+
.description("Show proxy statistics")
|
|
1273
|
+
.option("--json", "Output as JSON")
|
|
1274
|
+
.action(async (opts: { json?: boolean }) => {
|
|
1275
|
+
const { getProxyStats, getTopHosts } = await import("./proxy/db.js");
|
|
1276
|
+
const stats = getProxyStats();
|
|
1277
|
+
const topHosts = getTopHosts(5);
|
|
1278
|
+
if (opts.json) { console.log(JSON.stringify({ stats, topHosts }, null, 2)); return; }
|
|
1279
|
+
|
|
1280
|
+
console.log(`\nProxy Statistics:`);
|
|
1281
|
+
console.log(` Total requests: ${stats.totalRequests}`);
|
|
1282
|
+
console.log(` Unique hosts: ${stats.uniqueHosts}`);
|
|
1283
|
+
console.log(` Flagged: ${stats.flagged}`);
|
|
1284
|
+
console.log(` Avg latency: ${stats.avgDuration}ms`);
|
|
1285
|
+
if (topHosts.length > 0) {
|
|
1286
|
+
console.log(`\n Top Hosts:`);
|
|
1287
|
+
for (const h of topHosts) console.log(` ${h.host.padEnd(30)} ${h.count} requests`);
|
|
1288
|
+
}
|
|
1289
|
+
console.log();
|
|
1290
|
+
});
|
|
1291
|
+
|
|
1292
|
+
proxy
|
|
1293
|
+
.command("ca")
|
|
1294
|
+
.description("Manage CA certificate for HTTPS interception")
|
|
1295
|
+
.option("--generate", "Generate CA certificate")
|
|
1296
|
+
.option("--install", "Show installation instructions")
|
|
1297
|
+
.option("--path", "Show CA certificate path")
|
|
1298
|
+
.action(async (opts: { generate?: boolean; install?: boolean; path?: boolean }) => {
|
|
1299
|
+
const { generateCA, getCACertPath, hasCA, getInstallInstructions } = await import("./proxy/ca.js");
|
|
1300
|
+
if (opts.generate) {
|
|
1301
|
+
generateCA();
|
|
1302
|
+
console.log("CA certificate generated.");
|
|
1303
|
+
console.log(`Path: ${getCACertPath()}`);
|
|
1304
|
+
return;
|
|
1305
|
+
}
|
|
1306
|
+
if (opts.path) {
|
|
1307
|
+
console.log(getCACertPath());
|
|
1308
|
+
return;
|
|
1309
|
+
}
|
|
1310
|
+
// Default: show install instructions
|
|
1311
|
+
if (!hasCA()) {
|
|
1312
|
+
console.log("No CA certificate found. Generate one first:");
|
|
1313
|
+
console.log(" domain-sniper proxy ca --generate\n");
|
|
1314
|
+
return;
|
|
1315
|
+
}
|
|
1316
|
+
console.log(getInstallInstructions());
|
|
1317
|
+
});
|
|
1318
|
+
|
|
1319
|
+
// ─── Snipe subcommand ──────────────────────────────────
|
|
1320
|
+
|
|
1321
|
+
const snipeCmd = program
|
|
1322
|
+
.command("snipe")
|
|
1323
|
+
.description("Snipe domains — automatically watch, detect expiry, and register");
|
|
1324
|
+
|
|
1325
|
+
snipeCmd
|
|
1326
|
+
.command("add <domain>")
|
|
1327
|
+
.description("Add a domain to snipe list")
|
|
1328
|
+
.option("--max-price <price>", "Maximum price to pay")
|
|
1329
|
+
.action(async (domain: string, opts: { maxPrice?: string }) => {
|
|
1330
|
+
const { snipeDomain } = await import("./core/features/snipe.js");
|
|
1331
|
+
const { whoisLookup } = await import("./core/whois.js");
|
|
1332
|
+
|
|
1333
|
+
console.log(`\nChecking ${domain}...`);
|
|
1334
|
+
const whois = await whoisLookup(domain);
|
|
1335
|
+
|
|
1336
|
+
snipeDomain(domain, {
|
|
1337
|
+
expiryDate: whois.expiryDate || undefined,
|
|
1338
|
+
maxPrice: opts.maxPrice ? parseFloat(opts.maxPrice) : undefined,
|
|
1339
|
+
});
|
|
1340
|
+
|
|
1341
|
+
if (whois.available) {
|
|
1342
|
+
console.log(` ${domain} is ALREADY AVAILABLE — consider registering now!`);
|
|
1343
|
+
console.log(` Use: domain-sniper snipe run\n`);
|
|
1344
|
+
} else if (whois.expired) {
|
|
1345
|
+
console.log(` ${domain} is EXPIRED — snipe engine will monitor and register when it drops`);
|
|
1346
|
+
if (whois.expiryDate) console.log(` Expiry: ${whois.expiryDate}`);
|
|
1347
|
+
} else {
|
|
1348
|
+
console.log(` ${domain} is currently registered`);
|
|
1349
|
+
if (whois.expiryDate) {
|
|
1350
|
+
const daysLeft = Math.floor((new Date(whois.expiryDate).getTime() - Date.now()) / 86400000);
|
|
1351
|
+
console.log(` Expires: ${whois.expiryDate} (${daysLeft} days)`);
|
|
1352
|
+
}
|
|
1353
|
+
console.log(` Snipe engine will watch for expiry and auto-register when it drops`);
|
|
1354
|
+
}
|
|
1355
|
+
console.log(`\nStart the snipe engine: domain-sniper snipe run\n`);
|
|
1356
|
+
});
|
|
1357
|
+
|
|
1358
|
+
snipeCmd
|
|
1359
|
+
.command("remove <domain>")
|
|
1360
|
+
.description("Remove a domain from snipe list")
|
|
1361
|
+
.action(async (domain: string) => {
|
|
1362
|
+
const { cancelSnipe } = await import("./core/features/snipe.js");
|
|
1363
|
+
cancelSnipe(domain);
|
|
1364
|
+
console.log(`Removed ${domain} from snipe list`);
|
|
1365
|
+
});
|
|
1366
|
+
|
|
1367
|
+
snipeCmd
|
|
1368
|
+
.command("list")
|
|
1369
|
+
.description("Show all snipe targets")
|
|
1370
|
+
.option("--json", "Output as JSON")
|
|
1371
|
+
.action(async (opts: { json?: boolean }) => {
|
|
1372
|
+
const { getSnipeTargets } = await import("./core/features/snipe.js");
|
|
1373
|
+
const { getSnipeStats } = await import("./core/db.js");
|
|
1374
|
+
const targets = getSnipeTargets();
|
|
1375
|
+
const stats = getSnipeStats();
|
|
1376
|
+
|
|
1377
|
+
if (opts.json) { console.log(JSON.stringify({ targets, stats }, null, 2)); return; }
|
|
1378
|
+
|
|
1379
|
+
console.log(`\nSnipe Targets (${stats.total} total — ${stats.watching} watching, ${stats.expiring} expiring, ${stats.dropping} dropping, ${stats.registered} registered):\n`);
|
|
1380
|
+
if (targets.length === 0) { console.log(" No active snipe targets. Add one: domain-sniper snipe add example.com\n"); return; }
|
|
1381
|
+
|
|
1382
|
+
console.log(` ${"DOMAIN".padEnd(30)} ${"STATUS".padEnd(12)} ${"PHASE".padEnd(12)} ${"CHECKS".padEnd(8)} LAST CHECK`);
|
|
1383
|
+
console.log(` ${"─".repeat(30)} ${"─".repeat(12)} ${"─".repeat(12)} ${"─".repeat(8)} ${"─".repeat(20)}`);
|
|
1384
|
+
for (const t of targets) {
|
|
1385
|
+
const statusColor = t.status === "dropping" ? "\x1b[31m" : t.status === "expiring" ? "\x1b[33m" : "\x1b[90m";
|
|
1386
|
+
console.log(` ${t.domain.padEnd(30)} ${statusColor}${t.status.padEnd(12)}\x1b[0m ${t.phase.padEnd(12)} ${String(t.checkCount).padEnd(8)} ${t.lastChecked || "never"}`);
|
|
1387
|
+
}
|
|
1388
|
+
console.log();
|
|
1389
|
+
});
|
|
1390
|
+
|
|
1391
|
+
snipeCmd
|
|
1392
|
+
.command("run")
|
|
1393
|
+
.description("Start the snipe engine (runs in foreground)")
|
|
1394
|
+
.action(async () => {
|
|
1395
|
+
const { createSnipeEngine, getSnipeTargets } = await import("./core/features/snipe.js");
|
|
1396
|
+
const targets = getSnipeTargets();
|
|
1397
|
+
|
|
1398
|
+
if (targets.length === 0) {
|
|
1399
|
+
console.error("No snipe targets. Add one first: domain-sniper snipe add example.com");
|
|
1400
|
+
process.exit(1);
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
console.log(`\n◆ Snipe Engine Starting — ${targets.length} target(s)\n`);
|
|
1404
|
+
for (const t of targets) {
|
|
1405
|
+
console.log(` ${t.domain.padEnd(30)} ${t.status.padEnd(12)} ${t.phase}`);
|
|
1406
|
+
}
|
|
1407
|
+
console.log(`\nPhases: hourly (registered) → frequent/5min (expired) → aggressive/30s (pending delete)`);
|
|
1408
|
+
console.log("Press Ctrl+C to stop\n");
|
|
1409
|
+
|
|
1410
|
+
const engine = createSnipeEngine({
|
|
1411
|
+
onStatusChange: (domain, status, phase, message) => {
|
|
1412
|
+
const ts = new Date().toISOString().slice(11, 19);
|
|
1413
|
+
const color = status === "registered" ? "\x1b[32m" : status === "dropping" ? "\x1b[31m" : status === "expiring" ? "\x1b[33m" : "\x1b[90m";
|
|
1414
|
+
console.log(` ${ts} ${color}[${status}/${phase}]\x1b[0m ${message}`);
|
|
1415
|
+
},
|
|
1416
|
+
onRegistered: (domain) => {
|
|
1417
|
+
console.log(`\n ★ SUCCESS — ${domain} has been REGISTERED!\n`);
|
|
1418
|
+
},
|
|
1419
|
+
onFailed: (domain, error) => {
|
|
1420
|
+
console.log(`\n ✗ FAILED — ${domain}: ${error}\n`);
|
|
1421
|
+
},
|
|
1422
|
+
});
|
|
1423
|
+
|
|
1424
|
+
await engine.start();
|
|
1425
|
+
|
|
1426
|
+
// Keep running
|
|
1427
|
+
process.on("SIGINT", () => {
|
|
1428
|
+
console.log("\nStopping snipe engine...");
|
|
1429
|
+
engine.stop();
|
|
1430
|
+
process.exit(0);
|
|
1431
|
+
});
|
|
1432
|
+
});
|
|
1433
|
+
|
|
1434
|
+
snipeCmd
|
|
1435
|
+
.command("stats")
|
|
1436
|
+
.description("Show snipe statistics")
|
|
1437
|
+
.option("--json", "Output as JSON")
|
|
1438
|
+
.action(async (opts: { json?: boolean }) => {
|
|
1439
|
+
const { getSnipeStats, getAllSnipes } = await import("./core/db.js");
|
|
1440
|
+
const stats = getSnipeStats();
|
|
1441
|
+
const all = getAllSnipes();
|
|
1442
|
+
if (opts.json) { console.log(JSON.stringify({ stats, snipes: all }, null, 2)); return; }
|
|
1443
|
+
|
|
1444
|
+
console.log(`\nSnipe Stats:`);
|
|
1445
|
+
console.log(` Watching: ${stats.watching}`);
|
|
1446
|
+
console.log(` Expiring: ${stats.expiring}`);
|
|
1447
|
+
console.log(` Dropping: ${stats.dropping}`);
|
|
1448
|
+
console.log(` Registered: ${stats.registered}`);
|
|
1449
|
+
console.log(` Failed: ${stats.failed}`);
|
|
1450
|
+
console.log(` Total: ${stats.total}\n`);
|
|
1451
|
+
});
|
|
1452
|
+
|
|
1453
|
+
// ─── Check-update subcommand ────────────────────────────
|
|
1454
|
+
|
|
1455
|
+
program
|
|
1456
|
+
.command("check-update")
|
|
1457
|
+
.description("Check for newer versions")
|
|
1458
|
+
.action(async () => {
|
|
1459
|
+
const { checkForUpdates, formatUpdateMessage } = await import("./core/features/version-check.js");
|
|
1460
|
+
console.log("Checking for updates...");
|
|
1461
|
+
const result = await checkForUpdates();
|
|
1462
|
+
if (result.updateAvailable && result.latest) {
|
|
1463
|
+
console.log(formatUpdateMessage(result.current, result.latest));
|
|
1464
|
+
} else {
|
|
1465
|
+
console.log(`You're on the latest version (${result.current})`);
|
|
1466
|
+
}
|
|
1467
|
+
});
|
|
1468
|
+
|
|
1469
|
+
program.parse();
|
|
1470
|
+
|
|
1471
|
+
// ─── Types ───────────────────────────────────────────────
|
|
1472
|
+
|
|
1473
|
+
interface CliOptions {
|
|
1474
|
+
file?: string;
|
|
1475
|
+
autoRegister: boolean;
|
|
1476
|
+
headless: boolean;
|
|
1477
|
+
json: boolean;
|
|
1478
|
+
concurrency: string;
|
|
1479
|
+
recon: boolean;
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
interface JsonOutputResult {
|
|
1483
|
+
domain: string;
|
|
1484
|
+
status: string;
|
|
1485
|
+
available: boolean;
|
|
1486
|
+
expired: boolean;
|
|
1487
|
+
confidence: string;
|
|
1488
|
+
registrar: string | null;
|
|
1489
|
+
createdDate: string | null;
|
|
1490
|
+
expiryDate: string | null;
|
|
1491
|
+
age: string | null;
|
|
1492
|
+
dns: { a: string[]; aaaa: string[]; mx: string[]; txt: string[]; cname: string[] } | null;
|
|
1493
|
+
http: { status: number | null; server: string | null; parked: boolean; reachable: boolean; redirectUrl: string | null } | null;
|
|
1494
|
+
wayback: { hasHistory: boolean; snapshots: number; firstArchived: string | null; lastArchived: string | null } | null;
|
|
1495
|
+
price: number | null;
|
|
1496
|
+
social: Awaited<ReturnType<typeof checkSocialMedia>> | null;
|
|
1497
|
+
techStack: Awaited<ReturnType<typeof detectTechStack>> | null;
|
|
1498
|
+
blacklist: Awaited<ReturnType<typeof checkBlacklists>> | null;
|
|
1499
|
+
backlinks: Awaited<ReturnType<typeof estimateBacklinks>> | null;
|
|
1500
|
+
portScan?: PortScanResult | null;
|
|
1501
|
+
reverseIp?: ReverseIpResult | null;
|
|
1502
|
+
asn?: AsnResult | null;
|
|
1503
|
+
emailSecurity?: EmailSecurityResult | null;
|
|
1504
|
+
zoneTransfer?: ZoneTransferResult | null;
|
|
1505
|
+
certTransparency?: CertTransparencyResult | null;
|
|
1506
|
+
takeover?: TakeoverResult | null;
|
|
1507
|
+
securityHeaders?: SecurityHeadersResult | null;
|
|
1508
|
+
waf?: WafResult | null;
|
|
1509
|
+
pathScan?: PathScanResult | null;
|
|
1510
|
+
cors?: CorsResult | null;
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1513
|
+
interface JsonOutput {
|
|
1514
|
+
timestamp: string;
|
|
1515
|
+
count: number;
|
|
1516
|
+
results: JsonOutputResult[];
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1519
|
+
// ─── Headless / non-interactive mode ──────────────────────
|
|
1520
|
+
|
|
1521
|
+
async function checkDependencies(): Promise<void> {
|
|
1522
|
+
const { execFile } = await import("child_process");
|
|
1523
|
+
const { promisify } = await import("util");
|
|
1524
|
+
const execFileAsync = promisify(execFile);
|
|
1525
|
+
|
|
1526
|
+
const missing: string[] = [];
|
|
1527
|
+
try { await execFileAsync("which", ["whois"]); } catch { missing.push("whois"); }
|
|
1528
|
+
try { await execFileAsync("which", ["dig"]); } catch { missing.push("dig"); }
|
|
1529
|
+
|
|
1530
|
+
if (missing.length > 0) {
|
|
1531
|
+
console.error(`Missing required tools: ${missing.join(", ")}`);
|
|
1532
|
+
console.error("Install them:");
|
|
1533
|
+
console.error(" macOS: brew install whois bind (for dig)");
|
|
1534
|
+
console.error(" Ubuntu/Debian: apt install whois dnsutils");
|
|
1535
|
+
console.error(" Alpine: apk add whois bind-tools");
|
|
1536
|
+
process.exit(1);
|
|
1537
|
+
}
|
|
1538
|
+
}
|
|
1539
|
+
|
|
1540
|
+
async function runHeadless(domains: string[], options: CliOptions) {
|
|
1541
|
+
await checkDependencies();
|
|
1542
|
+
|
|
1543
|
+
const { whoisLookup, verifyAvailability, parseDomainList } = await import("./core/whois.js");
|
|
1544
|
+
const { loadConfigFromEnv, checkAvailabilityViaRegistrar, registerDomain } = await import("./core/registrar.js");
|
|
1545
|
+
const { readFileSync, existsSync } = await import("fs");
|
|
1546
|
+
|
|
1547
|
+
let domainList = [...domains];
|
|
1548
|
+
|
|
1549
|
+
// Load from file if specified
|
|
1550
|
+
if (options.file) {
|
|
1551
|
+
const filePath = safePath(options.file, [process.cwd()]);
|
|
1552
|
+
if (!existsSync(filePath)) {
|
|
1553
|
+
console.error(`File not found: ${filePath}`);
|
|
1554
|
+
process.exit(1);
|
|
1555
|
+
}
|
|
1556
|
+
const content = readFileSync(filePath, "utf-8");
|
|
1557
|
+
domainList.push(...parseDomainList(content));
|
|
1558
|
+
}
|
|
1559
|
+
|
|
1560
|
+
const rawCount = domainList.length;
|
|
1561
|
+
domainList = sanitizeDomainList(domainList);
|
|
1562
|
+
|
|
1563
|
+
if (domainList.length === 0) {
|
|
1564
|
+
if (rawCount > 0) {
|
|
1565
|
+
console.error(`No valid domains found (${rawCount} input(s) rejected). Domains must be like: example.com`);
|
|
1566
|
+
} else {
|
|
1567
|
+
console.error("No domains specified. Use: domain-sniper example.com or -f domains.txt");
|
|
1568
|
+
}
|
|
1569
|
+
process.exit(1);
|
|
1570
|
+
}
|
|
1571
|
+
|
|
1572
|
+
const config = loadConfigFromEnv();
|
|
1573
|
+
const isJsonMode = options.json;
|
|
1574
|
+
|
|
1575
|
+
// JSON mode: collect results
|
|
1576
|
+
const jsonResults: JsonOutputResult[] = [];
|
|
1577
|
+
|
|
1578
|
+
if (!isJsonMode) {
|
|
1579
|
+
console.log(`\n🔍 Domain Sniper - Checking ${domainList.length} domain(s)...\n`);
|
|
1580
|
+
}
|
|
1581
|
+
|
|
1582
|
+
for (let i = 0; i < domainList.length; i++) {
|
|
1583
|
+
const domain = domainList[i]!;
|
|
1584
|
+
if (!isJsonMode) {
|
|
1585
|
+
process.stdout.write(` Checking ${domain}...`);
|
|
1586
|
+
}
|
|
1587
|
+
|
|
1588
|
+
const whois = await whoisLookup(domain);
|
|
1589
|
+
const verification = await verifyAvailability(domain);
|
|
1590
|
+
|
|
1591
|
+
let status = "TAKEN";
|
|
1592
|
+
let available = false;
|
|
1593
|
+
|
|
1594
|
+
if (whois.available && verification.confidence === "high") {
|
|
1595
|
+
status = "AVAILABLE";
|
|
1596
|
+
available = true;
|
|
1597
|
+
} else if (whois.expired) {
|
|
1598
|
+
status = "EXPIRED";
|
|
1599
|
+
} else if (whois.available) {
|
|
1600
|
+
status = `AVAILABLE (${verification.confidence} confidence)`;
|
|
1601
|
+
available = true;
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
// DNS details
|
|
1605
|
+
let dnsResult: { a: string[]; aaaa: string[]; mx: string[]; txt: string[]; cname: string[] } | null = null;
|
|
1606
|
+
try {
|
|
1607
|
+
dnsResult = await lookupDns(domain);
|
|
1608
|
+
} catch {}
|
|
1609
|
+
|
|
1610
|
+
// HTTP probe
|
|
1611
|
+
let httpResult: { status: number | null; server: string | null; parked: boolean; reachable: boolean; redirectUrl: string | null } | null = null;
|
|
1612
|
+
try {
|
|
1613
|
+
const probe = await httpProbe(domain);
|
|
1614
|
+
httpResult = {
|
|
1615
|
+
status: probe.status,
|
|
1616
|
+
server: probe.server,
|
|
1617
|
+
parked: probe.parked,
|
|
1618
|
+
reachable: probe.reachable,
|
|
1619
|
+
redirectUrl: probe.redirectUrl,
|
|
1620
|
+
};
|
|
1621
|
+
} catch {}
|
|
1622
|
+
|
|
1623
|
+
// Wayback Machine
|
|
1624
|
+
let waybackResult: { hasHistory: boolean; snapshots: number; firstArchived: string | null; lastArchived: string | null } | null = null;
|
|
1625
|
+
try {
|
|
1626
|
+
waybackResult = await checkWayback(domain);
|
|
1627
|
+
} catch {}
|
|
1628
|
+
|
|
1629
|
+
// Domain age
|
|
1630
|
+
const age = calculateDomainAge(whois.createdDate);
|
|
1631
|
+
|
|
1632
|
+
// Registrar price check
|
|
1633
|
+
let price: number | null = null;
|
|
1634
|
+
if (config?.apiKey) {
|
|
1635
|
+
const regCheck = await checkAvailabilityViaRegistrar(domain, config);
|
|
1636
|
+
if (regCheck.price !== undefined) {
|
|
1637
|
+
price = regCheck.price;
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1641
|
+
// New feature data collection
|
|
1642
|
+
let socialResult: Awaited<ReturnType<typeof checkSocialMedia>> | null = null;
|
|
1643
|
+
try { socialResult = await checkSocialMedia(domain); } catch {}
|
|
1644
|
+
|
|
1645
|
+
let techResult: Awaited<ReturnType<typeof detectTechStack>> | null = null;
|
|
1646
|
+
try { techResult = await detectTechStack(domain); } catch {}
|
|
1647
|
+
|
|
1648
|
+
let blacklistResult: Awaited<ReturnType<typeof checkBlacklists>> | null = null;
|
|
1649
|
+
try { blacklistResult = await checkBlacklists(domain); } catch {}
|
|
1650
|
+
|
|
1651
|
+
let backlinkResult: Awaited<ReturnType<typeof estimateBacklinks>> | null = null;
|
|
1652
|
+
try { backlinkResult = await estimateBacklinks(domain); } catch {}
|
|
1653
|
+
|
|
1654
|
+
// Recon features — only when --recon flag is set
|
|
1655
|
+
let reconData: {
|
|
1656
|
+
portScan: PortScanResult | null;
|
|
1657
|
+
reverseIp: ReverseIpResult | null;
|
|
1658
|
+
asn: AsnResult | null;
|
|
1659
|
+
emailSecurity: EmailSecurityResult | null;
|
|
1660
|
+
zoneTransfer: ZoneTransferResult | null;
|
|
1661
|
+
certTransparency: CertTransparencyResult | null;
|
|
1662
|
+
takeover: TakeoverResult | null;
|
|
1663
|
+
securityHeaders: SecurityHeadersResult | null;
|
|
1664
|
+
waf: WafResult | null;
|
|
1665
|
+
pathScan: PathScanResult | null;
|
|
1666
|
+
cors: CorsResult | null;
|
|
1667
|
+
} | null = null;
|
|
1668
|
+
|
|
1669
|
+
if (options.recon) {
|
|
1670
|
+
const [portsR, reverseIpR, asnR, emailSecR, zoneXferR, certsR, takeoverR, secHeadersR, wafR, pathsR, corsR] = await Promise.all([
|
|
1671
|
+
scanPorts(domain).catch(() => null),
|
|
1672
|
+
reverseIpLookup(domain).catch(() => null),
|
|
1673
|
+
lookupAsn(domain).catch(() => null),
|
|
1674
|
+
checkEmailSecurity(domain).catch(() => null),
|
|
1675
|
+
checkZoneTransfer(domain).catch(() => null),
|
|
1676
|
+
queryCertTransparency(domain).catch(() => null),
|
|
1677
|
+
detectTakeover(domain).catch(() => null),
|
|
1678
|
+
auditSecurityHeaders(domain).catch(() => null),
|
|
1679
|
+
detectWaf(domain).catch(() => null),
|
|
1680
|
+
scanPaths(domain).catch(() => null),
|
|
1681
|
+
checkCors(domain).catch(() => null),
|
|
1682
|
+
]);
|
|
1683
|
+
reconData = {
|
|
1684
|
+
portScan: portsR,
|
|
1685
|
+
reverseIp: reverseIpR,
|
|
1686
|
+
asn: asnR,
|
|
1687
|
+
emailSecurity: emailSecR,
|
|
1688
|
+
zoneTransfer: zoneXferR,
|
|
1689
|
+
certTransparency: certsR,
|
|
1690
|
+
takeover: takeoverR,
|
|
1691
|
+
securityHeaders: secHeadersR,
|
|
1692
|
+
waf: wafR,
|
|
1693
|
+
pathScan: pathsR,
|
|
1694
|
+
cors: corsR,
|
|
1695
|
+
};
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1698
|
+
if (isJsonMode) {
|
|
1699
|
+
jsonResults.push({
|
|
1700
|
+
domain,
|
|
1701
|
+
status,
|
|
1702
|
+
available,
|
|
1703
|
+
expired: whois.expired,
|
|
1704
|
+
confidence: verification.confidence,
|
|
1705
|
+
registrar: whois.registrar,
|
|
1706
|
+
createdDate: whois.createdDate,
|
|
1707
|
+
expiryDate: whois.expiryDate,
|
|
1708
|
+
age,
|
|
1709
|
+
dns: dnsResult,
|
|
1710
|
+
http: httpResult,
|
|
1711
|
+
wayback: waybackResult,
|
|
1712
|
+
price,
|
|
1713
|
+
social: socialResult,
|
|
1714
|
+
techStack: techResult,
|
|
1715
|
+
blacklist: blacklistResult,
|
|
1716
|
+
backlinks: backlinkResult,
|
|
1717
|
+
...(reconData || {}),
|
|
1718
|
+
});
|
|
1719
|
+
} else {
|
|
1720
|
+
// Normal text output
|
|
1721
|
+
let color = "\x1b[31m"; // red
|
|
1722
|
+
if (available) {
|
|
1723
|
+
color = "\x1b[32m"; // green
|
|
1724
|
+
} else if (whois.expired) {
|
|
1725
|
+
color = "\x1b[33m"; // yellow
|
|
1726
|
+
}
|
|
1727
|
+
|
|
1728
|
+
console.log(`\r ${color}${status}\x1b[0m ${domain}`);
|
|
1729
|
+
|
|
1730
|
+
// Verification details
|
|
1731
|
+
for (const check of verification.checks) {
|
|
1732
|
+
console.log(` ${check}`);
|
|
1733
|
+
}
|
|
1734
|
+
|
|
1735
|
+
// DNS details
|
|
1736
|
+
if (dnsResult) {
|
|
1737
|
+
if (dnsResult.a.length) console.log(` DNS A: ${dnsResult.a.join(", ")}`);
|
|
1738
|
+
if (dnsResult.mx.length) console.log(` DNS MX: ${dnsResult.mx.join(", ")}`);
|
|
1739
|
+
if (dnsResult.aaaa.length) console.log(` DNS AAAA: ${dnsResult.aaaa.join(", ")}`);
|
|
1740
|
+
}
|
|
1741
|
+
|
|
1742
|
+
// HTTP probe
|
|
1743
|
+
if (httpResult?.reachable) {
|
|
1744
|
+
let httpLine = ` HTTP: ${httpResult.status}`;
|
|
1745
|
+
if (httpResult.parked) httpLine += " (PARKED)";
|
|
1746
|
+
if (httpResult.server) httpLine += ` [${httpResult.server}]`;
|
|
1747
|
+
console.log(httpLine);
|
|
1748
|
+
if (httpResult.redirectUrl) console.log(` Redirect: ${httpResult.redirectUrl}`);
|
|
1749
|
+
}
|
|
1750
|
+
|
|
1751
|
+
// Wayback Machine
|
|
1752
|
+
if (waybackResult?.hasHistory) {
|
|
1753
|
+
console.log(` Wayback: ~${waybackResult.snapshots} snapshots${waybackResult.firstArchived ? ` (${waybackResult.firstArchived}` : ""}${waybackResult.lastArchived ? ` - ${waybackResult.lastArchived})` : waybackResult.firstArchived ? ")" : ""}`);
|
|
1754
|
+
}
|
|
1755
|
+
|
|
1756
|
+
// Domain age
|
|
1757
|
+
if (age) console.log(` Age: ${age}`);
|
|
1758
|
+
|
|
1759
|
+
// Social media
|
|
1760
|
+
if (socialResult) {
|
|
1761
|
+
const avail = socialResult.filter((s) => s.available && !s.error);
|
|
1762
|
+
if (avail.length > 0) {
|
|
1763
|
+
console.log(` Social avail: ${avail.map((s) => s.platform).join(", ")}`);
|
|
1764
|
+
}
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1767
|
+
// Tech stack
|
|
1768
|
+
if (techResult) {
|
|
1769
|
+
const items: string[] = [];
|
|
1770
|
+
if (techResult.cms) items.push(techResult.cms);
|
|
1771
|
+
if (techResult.framework) items.push(techResult.framework);
|
|
1772
|
+
if (techResult.cdn) items.push(techResult.cdn);
|
|
1773
|
+
if (items.length > 0) console.log(` Tech: ${items.join(", ")}`);
|
|
1774
|
+
}
|
|
1775
|
+
|
|
1776
|
+
// Blacklist
|
|
1777
|
+
if (blacklistResult) {
|
|
1778
|
+
if (blacklistResult.listed) {
|
|
1779
|
+
const names = blacklistResult.lists.filter((l) => l.listed).map((l) => l.name).join(", ");
|
|
1780
|
+
console.log(` \x1b[31mBLACKLISTED: ${names}\x1b[0m`);
|
|
1781
|
+
} else {
|
|
1782
|
+
console.log(` Reputation: clean (${blacklistResult.cleanCount}/${blacklistResult.lists.length})`);
|
|
1783
|
+
}
|
|
1784
|
+
}
|
|
1785
|
+
|
|
1786
|
+
// Backlinks
|
|
1787
|
+
if (backlinkResult) {
|
|
1788
|
+
const parts: string[] = [];
|
|
1789
|
+
if (backlinkResult.pageRank !== null) parts.push(`PageRank: ${backlinkResult.pageRank}`);
|
|
1790
|
+
if (backlinkResult.commonCrawlPages !== null) parts.push(`CC pages: ~${backlinkResult.commonCrawlPages}`);
|
|
1791
|
+
if (parts.length > 0) console.log(` Authority: ${parts.join(", ")}`);
|
|
1792
|
+
}
|
|
1793
|
+
|
|
1794
|
+
// Recon data (if --recon was set)
|
|
1795
|
+
if (reconData) {
|
|
1796
|
+
if (reconData.asn && !reconData.asn.error) {
|
|
1797
|
+
const asnParts: string[] = [];
|
|
1798
|
+
if (reconData.asn.asn) asnParts.push(reconData.asn.asn);
|
|
1799
|
+
if (reconData.asn.org) asnParts.push(reconData.asn.org);
|
|
1800
|
+
if (reconData.asn.country) asnParts.push(reconData.asn.country);
|
|
1801
|
+
if (asnParts.length > 0) console.log(` Network: ${asnParts.join(" | ")}`);
|
|
1802
|
+
}
|
|
1803
|
+
if (reconData.portScan && reconData.portScan.openPorts.length > 0) {
|
|
1804
|
+
console.log(` \x1b[31mOpen ports: ${reconData.portScan.openPorts.map((p) => `${p.port}/${p.service}`).join(", ")}\x1b[0m`);
|
|
1805
|
+
}
|
|
1806
|
+
if (reconData.emailSecurity) {
|
|
1807
|
+
console.log(` Email security: ${reconData.emailSecurity.grade} (SPF:${reconData.emailSecurity.spf.found ? "ok" : "missing"} DKIM:${reconData.emailSecurity.dkim.found ? "ok" : "missing"} DMARC:${reconData.emailSecurity.dmarc.found ? "ok" : "missing"})`);
|
|
1808
|
+
}
|
|
1809
|
+
if (reconData.securityHeaders && !reconData.securityHeaders.error) {
|
|
1810
|
+
console.log(` Security headers: ${reconData.securityHeaders.grade} (${reconData.securityHeaders.score}/100)`);
|
|
1811
|
+
}
|
|
1812
|
+
if (reconData.waf?.detected) {
|
|
1813
|
+
console.log(` WAF: ${reconData.waf.waf} (${reconData.waf.confidence})`);
|
|
1814
|
+
}
|
|
1815
|
+
if (reconData.zoneTransfer?.vulnerable) {
|
|
1816
|
+
console.log(` \x1b[31m!! Zone transfer vulnerable: ${reconData.zoneTransfer.vulnerableNs.join(", ")}\x1b[0m`);
|
|
1817
|
+
}
|
|
1818
|
+
if (reconData.takeover?.vulnerable) {
|
|
1819
|
+
const vulnSubs = reconData.takeover.findings.filter((f) => f.status === "vulnerable");
|
|
1820
|
+
console.log(` \x1b[31m!! Subdomain takeover: ${vulnSubs.map((f) => f.subdomain).join(", ")}\x1b[0m`);
|
|
1821
|
+
}
|
|
1822
|
+
if (reconData.pathScan && reconData.pathScan.findings.length > 0) {
|
|
1823
|
+
console.log(` \x1b[31mExposed paths: ${reconData.pathScan.findings.slice(0, 5).map((f) => f.path).join(", ")}\x1b[0m`);
|
|
1824
|
+
}
|
|
1825
|
+
if (reconData.cors?.vulnerable) {
|
|
1826
|
+
console.log(` \x1b[31m!! CORS misconfiguration\x1b[0m`);
|
|
1827
|
+
}
|
|
1828
|
+
if (reconData.certTransparency && reconData.certTransparency.subdomains.length > 0) {
|
|
1829
|
+
console.log(` CT subdomains: ${reconData.certTransparency.subdomains.length} found`);
|
|
1830
|
+
}
|
|
1831
|
+
if (reconData.reverseIp && reconData.reverseIp.sharedDomains.length > 0) {
|
|
1832
|
+
console.log(` Shared hosting: ${reconData.reverseIp.sharedDomains.length} domains on ${reconData.reverseIp.ip}`);
|
|
1833
|
+
}
|
|
1834
|
+
}
|
|
1835
|
+
|
|
1836
|
+
// Registrar check
|
|
1837
|
+
if (config?.apiKey) {
|
|
1838
|
+
const regCheck = await checkAvailabilityViaRegistrar(domain, config);
|
|
1839
|
+
if (regCheck.available) {
|
|
1840
|
+
console.log(` ✓ Registrar (${config.provider}): Available${regCheck.price ? ` - $${regCheck.price}` : ""}`);
|
|
1841
|
+
}
|
|
1842
|
+
|
|
1843
|
+
// Auto-register
|
|
1844
|
+
if (options.autoRegister && (whois.available || whois.expired) && verification.available) {
|
|
1845
|
+
console.log(` ◎ Registering via ${config.provider}...`);
|
|
1846
|
+
const result = await registerDomain(domain, config);
|
|
1847
|
+
if (result.success) {
|
|
1848
|
+
console.log(` ★ ${result.message}`);
|
|
1849
|
+
} else {
|
|
1850
|
+
console.log(` ✗ Registration failed: ${result.error}`);
|
|
1851
|
+
}
|
|
1852
|
+
}
|
|
1853
|
+
}
|
|
1854
|
+
|
|
1855
|
+
console.log();
|
|
1856
|
+
|
|
1857
|
+
// Visual separator between multi-domain results
|
|
1858
|
+
if (i < domainList.length - 1) {
|
|
1859
|
+
console.log(" ───────────────────────────────────");
|
|
1860
|
+
}
|
|
1861
|
+
}
|
|
1862
|
+
|
|
1863
|
+
// Rate limit between lookups
|
|
1864
|
+
if (i < domainList.length - 1) {
|
|
1865
|
+
await new Promise((r) => setTimeout(r, 1500));
|
|
1866
|
+
}
|
|
1867
|
+
}
|
|
1868
|
+
|
|
1869
|
+
if (isJsonMode) {
|
|
1870
|
+
const output: JsonOutput = {
|
|
1871
|
+
timestamp: new Date().toISOString(),
|
|
1872
|
+
count: jsonResults.length,
|
|
1873
|
+
results: jsonResults,
|
|
1874
|
+
};
|
|
1875
|
+
console.log(JSON.stringify(output, null, 2));
|
|
1876
|
+
// Exit with error code if any domain had an error status
|
|
1877
|
+
const hasErrors = jsonResults.some((r) => r.status === "error");
|
|
1878
|
+
if (hasErrors) process.exitCode = 1;
|
|
1879
|
+
} else {
|
|
1880
|
+
console.log("Done!\n");
|
|
1881
|
+
}
|
|
1882
|
+
|
|
1883
|
+
// Clean up database connection
|
|
1884
|
+
try {
|
|
1885
|
+
const { closeDb } = await import("./core/db.js");
|
|
1886
|
+
closeDb();
|
|
1887
|
+
} catch {}
|
|
1888
|
+
}
|