domsniper 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/.env.example +40 -0
  2. package/LICENSE +21 -0
  3. package/README.md +246 -0
  4. package/package.json +72 -0
  5. package/src/app.tsx +2062 -0
  6. package/src/completions.ts +65 -0
  7. package/src/core/db.ts +1313 -0
  8. package/src/core/features/asn-lookup.ts +91 -0
  9. package/src/core/features/backlinks.ts +83 -0
  10. package/src/core/features/blacklist-check.ts +67 -0
  11. package/src/core/features/cert-transparency.ts +87 -0
  12. package/src/core/features/config.ts +81 -0
  13. package/src/core/features/cors-check.ts +90 -0
  14. package/src/core/features/dns-details.ts +27 -0
  15. package/src/core/features/domain-age.ts +33 -0
  16. package/src/core/features/domain-suggest.ts +87 -0
  17. package/src/core/features/drop-catch.ts +159 -0
  18. package/src/core/features/email-security.ts +112 -0
  19. package/src/core/features/expiring-feed.ts +160 -0
  20. package/src/core/features/export.ts +74 -0
  21. package/src/core/features/filter.ts +96 -0
  22. package/src/core/features/http-probe.ts +46 -0
  23. package/src/core/features/marketplace.ts +69 -0
  24. package/src/core/features/path-scanner.ts +123 -0
  25. package/src/core/features/port-scanner.ts +132 -0
  26. package/src/core/features/portfolio-bulk.ts +125 -0
  27. package/src/core/features/portfolio-monitor.ts +214 -0
  28. package/src/core/features/portfolio.ts +98 -0
  29. package/src/core/features/price-compare.ts +39 -0
  30. package/src/core/features/rdap.ts +128 -0
  31. package/src/core/features/reverse-ip.ts +73 -0
  32. package/src/core/features/s3-export.ts +99 -0
  33. package/src/core/features/scoring.ts +121 -0
  34. package/src/core/features/security-headers.ts +162 -0
  35. package/src/core/features/session.ts +74 -0
  36. package/src/core/features/snipe.ts +264 -0
  37. package/src/core/features/social-check.ts +81 -0
  38. package/src/core/features/ssl-check.ts +88 -0
  39. package/src/core/features/subdomain-discovery.ts +53 -0
  40. package/src/core/features/takeover-detect.ts +143 -0
  41. package/src/core/features/tech-stack.ts +135 -0
  42. package/src/core/features/tld-expand.ts +43 -0
  43. package/src/core/features/variations.ts +134 -0
  44. package/src/core/features/version-check.ts +58 -0
  45. package/src/core/features/waf-detect.ts +171 -0
  46. package/src/core/features/watch.ts +120 -0
  47. package/src/core/features/wayback.ts +64 -0
  48. package/src/core/features/webhooks.ts +126 -0
  49. package/src/core/features/whois-history.ts +99 -0
  50. package/src/core/features/zone-transfer.ts +75 -0
  51. package/src/core/index.ts +50 -0
  52. package/src/core/paths.ts +9 -0
  53. package/src/core/registrar.ts +413 -0
  54. package/src/core/theme.ts +140 -0
  55. package/src/core/types.ts +143 -0
  56. package/src/core/validate.ts +58 -0
  57. package/src/core/whois.ts +265 -0
  58. package/src/index.tsx +1888 -0
  59. package/src/market-client.ts +186 -0
  60. package/src/proxy/ca.ts +116 -0
  61. package/src/proxy/db.ts +175 -0
  62. package/src/proxy/server.ts +155 -0
  63. package/tsconfig.json +30 -0
@@ -0,0 +1,186 @@
1
+ import { readFileSync, writeFileSync, existsSync, mkdirSync, unlinkSync } from "fs";
2
+ import { join } from "path";
3
+ import { homedir } from "os";
4
+
5
+ const APP_DIR = join(homedir(), ".domain-sniper");
6
+ const AUTH_FILE = join(APP_DIR, "market-auth.json");
7
+
8
+ interface AuthState {
9
+ serverUrl: string;
10
+ cookies: string;
11
+ userId: string;
12
+ email: string;
13
+ name: string;
14
+ }
15
+
16
+ function loadAuth(): AuthState | null {
17
+ if (!existsSync(AUTH_FILE)) return null;
18
+ try {
19
+ return JSON.parse(readFileSync(AUTH_FILE, "utf-8"));
20
+ } catch { return null; }
21
+ }
22
+
23
+ function saveAuth(state: AuthState): void {
24
+ if (!existsSync(APP_DIR)) mkdirSync(APP_DIR, { recursive: true });
25
+ writeFileSync(AUTH_FILE, JSON.stringify(state, null, 2), "utf-8");
26
+ }
27
+
28
+ function clearAuth(): void {
29
+ if (existsSync(AUTH_FILE)) {
30
+ unlinkSync(AUTH_FILE);
31
+ }
32
+ }
33
+
34
+ export function getServerUrl(): string {
35
+ return loadAuth()?.serverUrl || process.env.MARKET_URL || "http://localhost:3000";
36
+ }
37
+
38
+ export function isLoggedIn(): boolean {
39
+ return loadAuth() !== null;
40
+ }
41
+
42
+ export function getAuthInfo(): { email: string; name: string; userId: string } | null {
43
+ const auth = loadAuth();
44
+ if (!auth) return null;
45
+ return { email: auth.email, name: auth.name, userId: auth.userId };
46
+ }
47
+
48
+ async function request(method: string, path: string, body?: unknown): Promise<{ ok: boolean; status: number; data: any }> {
49
+ const auth = loadAuth();
50
+ const url = `${getServerUrl()}${path}`;
51
+ const headers: Record<string, string> = { "Content-Type": "application/json", "Origin": getServerUrl() };
52
+ if (auth?.cookies) headers["Cookie"] = auth.cookies;
53
+
54
+ try {
55
+ const resp = await fetch(url, {
56
+ method,
57
+ headers,
58
+ body: body ? JSON.stringify(body) : undefined,
59
+ });
60
+
61
+ // Save cookies from response
62
+ const setCookies = resp.headers.getSetCookie();
63
+ if (setCookies.length > 0 && auth) {
64
+ const cookieStr = setCookies.map((c) => c.split(";")[0]).join("; ");
65
+ saveAuth({ ...auth, cookies: cookieStr });
66
+ }
67
+
68
+ const data = await resp.json().catch(() => null);
69
+ return { ok: resp.ok, status: resp.status, data };
70
+ } catch (err: unknown) {
71
+ return { ok: false, status: 0, data: { error: err instanceof Error ? err.message : "Request failed" } };
72
+ }
73
+ }
74
+
75
+ // ─── Auth ────────────────────────────────────────────────
76
+
77
+ export async function signUp(email: string, password: string, name: string, serverUrl?: string): Promise<{ success: boolean; error?: string }> {
78
+ const base = serverUrl || getServerUrl();
79
+ try {
80
+ const resp = await fetch(`${base}/api/auth/sign-up/email`, {
81
+ method: "POST",
82
+ headers: { "Content-Type": "application/json", "Origin": base },
83
+ body: JSON.stringify({ email, password, name }),
84
+ });
85
+ const data = await resp.json() as any;
86
+ if (resp.ok && data?.user) {
87
+ const setCookies = resp.headers.getSetCookie();
88
+ const cookieStr = setCookies.map((c: string) => c.split(";")[0]).join("; ");
89
+ saveAuth({ serverUrl: base, cookies: cookieStr, userId: data.user.id, email: data.user.email, name: data.user.name });
90
+ return { success: true };
91
+ }
92
+ return { success: false, error: data?.message || data?.error || "Sign up failed" };
93
+ } catch (err: unknown) {
94
+ return { success: false, error: err instanceof Error ? err.message : "Connection failed" };
95
+ }
96
+ }
97
+
98
+ export async function signIn(email: string, password: string, serverUrl?: string): Promise<{ success: boolean; error?: string }> {
99
+ const base = serverUrl || getServerUrl();
100
+ try {
101
+ const resp = await fetch(`${base}/api/auth/sign-in/email`, {
102
+ method: "POST",
103
+ headers: { "Content-Type": "application/json", "Origin": base },
104
+ body: JSON.stringify({ email, password }),
105
+ });
106
+ const data = await resp.json() as any;
107
+ if (resp.ok && data?.user) {
108
+ const setCookies = resp.headers.getSetCookie();
109
+ const cookieStr = setCookies.map((c: string) => c.split(";")[0]).join("; ");
110
+ saveAuth({ serverUrl: base, cookies: cookieStr, userId: data.user.id, email: data.user.email, name: data.user.name });
111
+ return { success: true };
112
+ }
113
+ return { success: false, error: data?.message || data?.error || "Sign in failed" };
114
+ } catch (err: unknown) {
115
+ return { success: false, error: err instanceof Error ? err.message : "Connection failed" };
116
+ }
117
+ }
118
+
119
+ export function signOut(): void {
120
+ clearAuth();
121
+ }
122
+
123
+ // ─── Listings ────────────────────────────────────────────
124
+
125
+ export async function browseListings(query: {
126
+ search?: string; category?: string; minPrice?: number; maxPrice?: number;
127
+ verified?: boolean; sort?: string; limit?: number; offset?: number;
128
+ } = {}): Promise<{ ok: boolean; data: any }> {
129
+ const params = new URLSearchParams();
130
+ if (query.search) params.set("q", query.search);
131
+ if (query.category) params.set("category", query.category);
132
+ if (query.minPrice !== undefined) params.set("min", String(query.minPrice));
133
+ if (query.maxPrice !== undefined) params.set("max", String(query.maxPrice));
134
+ if (query.verified) params.set("verified", "true");
135
+ if (query.sort) params.set("sort", query.sort);
136
+ if (query.limit) params.set("limit", String(query.limit));
137
+ if (query.offset) params.set("offset", String(query.offset));
138
+ const qs = params.toString();
139
+ return request("GET", `/api/listings${qs ? `?${qs}` : ""}`);
140
+ }
141
+
142
+ export async function viewListing(id: number): Promise<{ ok: boolean; data: any }> {
143
+ return request("GET", `/api/listings/${id}`);
144
+ }
145
+
146
+ export async function createListingApi(domain: string, askingPrice: number, details: {
147
+ title?: string; description?: string; minOffer?: number; buyNow?: boolean; category?: string;
148
+ } = {}): Promise<{ ok: boolean; data: any }> {
149
+ return request("POST", "/api/listings", { domain, askingPrice, ...details });
150
+ }
151
+
152
+ export async function verifyListingApi(listingId: number): Promise<{ ok: boolean; data: any }> {
153
+ return request("POST", `/api/listings/${listingId}/verify`);
154
+ }
155
+
156
+ export async function cancelListingApi(listingId: number): Promise<{ ok: boolean; data: any }> {
157
+ return request("DELETE", `/api/listings/${listingId}`);
158
+ }
159
+
160
+ export async function getMyListings(): Promise<{ ok: boolean; data: any }> {
161
+ return request("GET", "/api/my/listings");
162
+ }
163
+
164
+ // ─── Offers ──────────────────────────────────────────────
165
+
166
+ export async function makeOffer(listingId: number, amount: number, message: string = ""): Promise<{ ok: boolean; data: any }> {
167
+ return request("POST", "/api/offers", { listingId, amount, message });
168
+ }
169
+
170
+ export async function respondToOffer(offerId: number, status: string, counterAmount?: number): Promise<{ ok: boolean; data: any }> {
171
+ return request("PUT", `/api/offers/${offerId}`, { status, counterAmount });
172
+ }
173
+
174
+ export async function getMyOffers(role: "buyer" | "seller" = "buyer"): Promise<{ ok: boolean; data: any }> {
175
+ return request("GET", `/api/my/offers?role=${role}`);
176
+ }
177
+
178
+ // ─── Other ───────────────────────────────────────────────
179
+
180
+ export async function getMarketStatsApi(): Promise<{ ok: boolean; data: any }> {
181
+ return request("GET", "/api/stats");
182
+ }
183
+
184
+ export async function getUnreadApi(): Promise<{ ok: boolean; data: any }> {
185
+ return request("GET", "/api/my/unread");
186
+ }
@@ -0,0 +1,116 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, unlinkSync, chmodSync } from "fs";
2
+ import { join } from "path";
3
+ import { homedir } from "os";
4
+ import { execFileSync } from "child_process";
5
+
6
+ const CA_DIR = join(homedir(), ".domain-sniper", "ca");
7
+ const CA_KEY = join(CA_DIR, "ca.key");
8
+ const CA_CERT = join(CA_DIR, "ca.crt");
9
+ const CERTS_DIR = join(CA_DIR, "certs");
10
+
11
+ function ensureDirs(): void {
12
+ if (!existsSync(CA_DIR)) mkdirSync(CA_DIR, { recursive: true });
13
+ if (!existsSync(CERTS_DIR)) mkdirSync(CERTS_DIR, { recursive: true });
14
+ }
15
+
16
+ export function hasCA(): boolean {
17
+ return existsSync(CA_KEY) && existsSync(CA_CERT);
18
+ }
19
+
20
+ export function getCACertPath(): string {
21
+ return CA_CERT;
22
+ }
23
+
24
+ export function generateCA(): { keyPath: string; certPath: string } {
25
+ ensureDirs();
26
+ if (hasCA()) return { keyPath: CA_KEY, certPath: CA_CERT };
27
+
28
+ // Generate CA private key
29
+ execFileSync("openssl", [
30
+ "genrsa", "-out", CA_KEY, "2048",
31
+ ], { stdio: "pipe" });
32
+ chmodSync(CA_KEY, 0o600);
33
+
34
+ // Generate CA certificate (valid for 10 years)
35
+ execFileSync("openssl", [
36
+ "req", "-new", "-x509", "-key", CA_KEY,
37
+ "-out", CA_CERT, "-days", "3650",
38
+ "-subj", "/CN=Domain Sniper CA/O=Domain Sniper/OU=Proxy",
39
+ ], { stdio: "pipe" });
40
+
41
+ return { keyPath: CA_KEY, certPath: CA_CERT };
42
+ }
43
+
44
+ export function generateHostCert(hostname: string): { key: string; cert: string } {
45
+ // Validate hostname format
46
+ const HOSTNAME_RE = /^[a-z0-9]([a-z0-9\-]{0,61}[a-z0-9])?(\.[a-z0-9]([a-z0-9\-]{0,61}[a-z0-9])?)*$/i;
47
+ if (!HOSTNAME_RE.test(hostname)) {
48
+ throw new Error(`Invalid hostname: ${hostname}`);
49
+ }
50
+
51
+ ensureDirs();
52
+ if (!hasCA()) generateCA();
53
+
54
+ // Sanitize hostname for filename and openssl arguments
55
+ const safe = hostname.replace(/[^a-z0-9.-]/gi, "_");
56
+ const hostKey = join(CERTS_DIR, `${safe}.key`);
57
+ const hostCert = join(CERTS_DIR, `${safe}.crt`);
58
+ const hostCsr = join(CERTS_DIR, `${safe}.csr`);
59
+ const extFile = join(CERTS_DIR, `${safe}.ext`);
60
+
61
+ // Return cached cert if exists and not expired
62
+ if (existsSync(hostKey) && existsSync(hostCert)) {
63
+ return { key: readFileSync(hostKey, "utf-8"), cert: readFileSync(hostCert, "utf-8") };
64
+ }
65
+
66
+ // Generate host key
67
+ execFileSync("openssl", ["genrsa", "-out", hostKey, "2048"], { stdio: "pipe" });
68
+ chmodSync(hostKey, 0o600);
69
+
70
+ // Generate CSR
71
+ execFileSync("openssl", [
72
+ "req", "-new", "-key", hostKey, "-out", hostCsr,
73
+ "-subj", `/CN=${safe}`,
74
+ ], { stdio: "pipe" });
75
+
76
+ // Create extensions file for SAN
77
+ writeFileSync(extFile, [
78
+ "authorityKeyIdentifier=keyid,issuer",
79
+ "basicConstraints=CA:FALSE",
80
+ "keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment",
81
+ `subjectAltName = DNS:${safe}, DNS:*.${safe}`,
82
+ ].join("\n"), "utf-8");
83
+
84
+ // Sign with CA
85
+ execFileSync("openssl", [
86
+ "x509", "-req", "-in", hostCsr, "-CA", CA_CERT, "-CAkey", CA_KEY,
87
+ "-CAcreateserial", "-out", hostCert, "-days", "365",
88
+ "-extfile", extFile,
89
+ ], { stdio: "pipe" });
90
+
91
+ // Clean up CSR and ext
92
+ try { unlinkSync(hostCsr); } catch {}
93
+ try { unlinkSync(extFile); } catch {}
94
+
95
+ return { key: readFileSync(hostKey, "utf-8"), cert: readFileSync(hostCert, "utf-8") };
96
+ }
97
+
98
+ export function getInstallInstructions(): string {
99
+ const certPath = getCACertPath();
100
+ return [
101
+ "Install the Domain Sniper CA certificate to intercept HTTPS:",
102
+ "",
103
+ "macOS:",
104
+ ` sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain "${certPath}"`,
105
+ "",
106
+ "Linux (Ubuntu/Debian):",
107
+ ` sudo cp "${certPath}" /usr/local/share/ca-certificates/domain-sniper-ca.crt`,
108
+ " sudo update-ca-certificates",
109
+ "",
110
+ "Firefox (manual):",
111
+ " Settings > Privacy & Security > Certificates > View Certificates > Import",
112
+ ` Select: ${certPath}`,
113
+ "",
114
+ `Certificate: ${certPath}`,
115
+ ].join("\n");
116
+ }
@@ -0,0 +1,175 @@
1
+ import { Database } from "bun:sqlite";
2
+ import { mkdirSync, existsSync } from "fs";
3
+ import { join } from "path";
4
+ import { homedir } from "os";
5
+
6
+ const APP_DIR = join(homedir(), ".domain-sniper");
7
+ const PROXY_DB_FILE = join(APP_DIR, "proxy.db");
8
+
9
+ let _db: Database | null = null;
10
+ let _dbPath: string | null = null;
11
+
12
+ /**
13
+ * Override the database path. Call BEFORE getProxyDb().
14
+ * Use ":memory:" for tests to avoid polluting the real database.
15
+ */
16
+ export function setProxyDbPath(path: string): void {
17
+ if (_db) { _db.close(); _db = null; }
18
+ _dbPath = path;
19
+ }
20
+
21
+ export function getProxyDb(): Database {
22
+ if (_db) return _db;
23
+ const dbPath = _dbPath || PROXY_DB_FILE;
24
+ if (dbPath !== ":memory:") {
25
+ if (!existsSync(APP_DIR)) mkdirSync(APP_DIR, { recursive: true });
26
+ }
27
+ _db = new Database(dbPath);
28
+ _db.run("PRAGMA journal_mode = WAL");
29
+ _db.run("PRAGMA foreign_keys = ON");
30
+ initSchema(_db);
31
+ return _db;
32
+ }
33
+
34
+ export function closeProxyDb(): void {
35
+ if (_db) { _db.close(); _db = null; }
36
+ }
37
+
38
+ function initSchema(db: Database): void {
39
+ db.run(`
40
+ CREATE TABLE IF NOT EXISTS requests (
41
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
42
+ method TEXT NOT NULL,
43
+ url TEXT NOT NULL,
44
+ host TEXT NOT NULL,
45
+ path TEXT NOT NULL,
46
+ scheme TEXT DEFAULT 'http',
47
+ request_headers TEXT DEFAULT '{}',
48
+ request_body TEXT DEFAULT '',
49
+ request_size INTEGER DEFAULT 0,
50
+ status_code INTEGER,
51
+ response_headers TEXT DEFAULT '{}',
52
+ response_body TEXT DEFAULT '',
53
+ response_size INTEGER DEFAULT 0,
54
+ duration_ms INTEGER DEFAULT 0,
55
+ intercepted_at TEXT DEFAULT (datetime('now')),
56
+ tags TEXT DEFAULT '[]',
57
+ notes TEXT DEFAULT '',
58
+ flagged INTEGER DEFAULT 0
59
+ )
60
+ `);
61
+
62
+ db.run("CREATE INDEX IF NOT EXISTS idx_req_host ON requests(host)");
63
+ db.run("CREATE INDEX IF NOT EXISTS idx_req_method ON requests(method)");
64
+ db.run("CREATE INDEX IF NOT EXISTS idx_req_status ON requests(status_code)");
65
+ db.run("CREATE INDEX IF NOT EXISTS idx_req_time ON requests(intercepted_at)");
66
+ db.run("CREATE INDEX IF NOT EXISTS idx_req_flagged ON requests(flagged)");
67
+ }
68
+
69
+ // ─── CRUD ────────────────────────────────────────────────
70
+
71
+ export interface InterceptedRequest {
72
+ id: number;
73
+ method: string;
74
+ url: string;
75
+ host: string;
76
+ path: string;
77
+ scheme: string;
78
+ request_headers: string;
79
+ request_body: string;
80
+ request_size: number;
81
+ status_code: number | null;
82
+ response_headers: string;
83
+ response_body: string;
84
+ response_size: number;
85
+ duration_ms: number;
86
+ intercepted_at: string;
87
+ tags: string;
88
+ notes: string;
89
+ flagged: number;
90
+ }
91
+
92
+ export function saveRequest(data: {
93
+ method: string;
94
+ url: string;
95
+ host: string;
96
+ path: string;
97
+ scheme: string;
98
+ requestHeaders: Record<string, string>;
99
+ requestBody: string;
100
+ statusCode: number | null;
101
+ responseHeaders: Record<string, string>;
102
+ responseBody: string;
103
+ durationMs: number;
104
+ }): number {
105
+ const db = getProxyDb();
106
+ const result = db.run(`
107
+ INSERT INTO requests (method, url, host, path, scheme, request_headers, request_body, request_size, status_code, response_headers, response_body, response_size, duration_ms)
108
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
109
+ `, [
110
+ data.method, data.url, data.host, data.path, data.scheme,
111
+ JSON.stringify(data.requestHeaders), data.requestBody, data.requestBody.length,
112
+ data.statusCode,
113
+ JSON.stringify(data.responseHeaders), data.responseBody, data.responseBody.length,
114
+ data.durationMs,
115
+ ]);
116
+ return Number(result.lastInsertRowid);
117
+ }
118
+
119
+ export function getRequests(options: {
120
+ host?: string; method?: string; statusCode?: number;
121
+ search?: string; flagged?: boolean;
122
+ limit?: number; offset?: number;
123
+ } = {}): { requests: InterceptedRequest[]; total: number } {
124
+ const db = getProxyDb();
125
+ const conditions: string[] = [];
126
+ const params: (string | number)[] = [];
127
+
128
+ if (options.host) { conditions.push("host LIKE ?"); params.push(`%${options.host}%`); }
129
+ if (options.method) { conditions.push("method = ?"); params.push(options.method); }
130
+ if (options.statusCode) { conditions.push("status_code = ?"); params.push(options.statusCode); }
131
+ if (options.search) { conditions.push("(url LIKE ? OR request_body LIKE ? OR response_body LIKE ?)"); params.push(`%${options.search}%`, `%${options.search}%`, `%${options.search}%`); }
132
+ if (options.flagged) { conditions.push("flagged = 1"); }
133
+
134
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
135
+ const limit = Math.min(options.limit || 50, 500);
136
+ const offset = options.offset || 0;
137
+
138
+ const total = (db.query(`SELECT COUNT(*) as c FROM requests ${where}`).get(...params) as { c: number }).c;
139
+ const requests = db.query(`SELECT * FROM requests ${where} ORDER BY intercepted_at DESC LIMIT ? OFFSET ?`).all(...params, limit, offset) as InterceptedRequest[];
140
+
141
+ return { requests, total };
142
+ }
143
+
144
+ export function getRequest(id: number): InterceptedRequest | null {
145
+ return getProxyDb().query("SELECT * FROM requests WHERE id = ?").get(id) as InterceptedRequest | null;
146
+ }
147
+
148
+ export function flagRequest(id: number, flagged: boolean = true): void {
149
+ getProxyDb().run("UPDATE requests SET flagged = ? WHERE id = ?", [flagged ? 1 : 0, id]);
150
+ }
151
+
152
+ export function addNote(id: number, note: string): void {
153
+ getProxyDb().run("UPDATE requests SET notes = ? WHERE id = ?", [note, id]);
154
+ }
155
+
156
+ export function clearRequests(host?: string): number {
157
+ const db = getProxyDb();
158
+ if (host) {
159
+ return db.run("DELETE FROM requests WHERE host LIKE ?", [`%${host}%`]).changes;
160
+ }
161
+ return db.run("DELETE FROM requests").changes;
162
+ }
163
+
164
+ export function getProxyStats(): { totalRequests: number; uniqueHosts: number; flagged: number; avgDuration: number } {
165
+ const db = getProxyDb();
166
+ const total = (db.query("SELECT COUNT(*) as c FROM requests").get() as { c: number }).c;
167
+ const hosts = (db.query("SELECT COUNT(DISTINCT host) as c FROM requests").get() as { c: number }).c;
168
+ const flagged = (db.query("SELECT COUNT(*) as c FROM requests WHERE flagged = 1").get() as { c: number }).c;
169
+ const avg = (db.query("SELECT COALESCE(AVG(duration_ms), 0) as a FROM requests").get() as { a: number }).a;
170
+ return { totalRequests: total, uniqueHosts: hosts, flagged, avgDuration: Math.round(avg) };
171
+ }
172
+
173
+ export function getTopHosts(limit: number = 10): Array<{ host: string; count: number }> {
174
+ return getProxyDb().query("SELECT host, COUNT(*) as count FROM requests GROUP BY host ORDER BY count DESC LIMIT ?").all(limit) as Array<{ host: string; count: number }>;
175
+ }
@@ -0,0 +1,155 @@
1
+ import { generateHostCert, hasCA, generateCA } from "./ca.js";
2
+ import { saveRequest } from "./db.js";
3
+
4
+ export interface ProxyConfig {
5
+ port: number;
6
+ httpsInterception: boolean;
7
+ onRequest?: (req: ProxyLogEntry) => void;
8
+ filterHosts?: string[]; // Only intercept these hosts (empty = all)
9
+ }
10
+
11
+ export interface ProxyLogEntry {
12
+ id: number;
13
+ method: string;
14
+ url: string;
15
+ host: string;
16
+ statusCode: number | null;
17
+ durationMs: number;
18
+ size: number;
19
+ }
20
+
21
+ export function startProxy(config: ProxyConfig): { stop: () => void } {
22
+ const { port, httpsInterception, onRequest, filterHosts } = config;
23
+
24
+ // Ensure CA exists if HTTPS interception is enabled
25
+ if (httpsInterception && !hasCA()) {
26
+ generateCA();
27
+ }
28
+
29
+ function shouldIntercept(host: string): boolean {
30
+ if (!filterHosts || filterHosts.length === 0) return true;
31
+ return filterHosts.some((f) => host.includes(f));
32
+ }
33
+
34
+ const server = Bun.serve({
35
+ port,
36
+ hostname: "127.0.0.1", // Only bind to localhost — never expose on public interface
37
+ async fetch(req) {
38
+ const url = new URL(req.url);
39
+ const host = url.hostname || req.headers.get("host")?.split(":")[0] || "";
40
+
41
+ if (!shouldIntercept(host)) {
42
+ // Pass through without logging
43
+ try {
44
+ const resp = await fetch(req.url, {
45
+ method: req.method,
46
+ headers: req.headers,
47
+ body: req.method !== "GET" && req.method !== "HEAD" ? await req.text() : undefined,
48
+ });
49
+ return resp;
50
+ } catch {
51
+ return new Response("Proxy error", { status: 502 });
52
+ }
53
+ }
54
+
55
+ const startTime = Date.now();
56
+ const method = req.method;
57
+ const path = url.pathname + url.search;
58
+ const scheme = url.protocol.replace(":", "");
59
+
60
+ // Capture request
61
+ const SENSITIVE_HEADERS = ["authorization", "cookie", "set-cookie", "x-api-key", "proxy-authorization"];
62
+ const reqHeaders: Record<string, string> = {};
63
+ req.headers.forEach((v, k) => { reqHeaders[k] = v; });
64
+ for (const h of SENSITIVE_HEADERS) {
65
+ if (reqHeaders[h]) reqHeaders[h] = "[REDACTED]";
66
+ }
67
+ let reqBody = "";
68
+ if (method !== "GET" && method !== "HEAD") {
69
+ try { reqBody = await req.clone().text(); } catch {}
70
+ }
71
+
72
+ // Forward request
73
+ let statusCode: number | null = null;
74
+ let respHeaders: Record<string, string> = {};
75
+ let respBody = "";
76
+
77
+ try {
78
+ const targetUrl = req.url;
79
+ const forwardHeaders = new Headers(req.headers);
80
+ // Remove proxy headers
81
+ forwardHeaders.delete("proxy-connection");
82
+
83
+ const resp = await fetch(targetUrl, {
84
+ method: req.method,
85
+ headers: forwardHeaders,
86
+ body: method !== "GET" && method !== "HEAD" ? reqBody : undefined,
87
+ redirect: "manual",
88
+ });
89
+
90
+ statusCode = resp.status;
91
+ resp.headers.forEach((v, k) => { respHeaders[k] = v; });
92
+
93
+ // Capture response body (limit to 1MB)
94
+ try {
95
+ const bodyBuf = await resp.arrayBuffer();
96
+ if (bodyBuf.byteLength < 1024 * 1024) {
97
+ respBody = new TextDecoder().decode(bodyBuf);
98
+ } else {
99
+ respBody = `[Body too large: ${bodyBuf.byteLength} bytes]`;
100
+ }
101
+ } catch {}
102
+
103
+ const durationMs = Date.now() - startTime;
104
+
105
+ // Redact sensitive response headers before saving
106
+ for (const h of SENSITIVE_HEADERS) {
107
+ if (respHeaders[h]) respHeaders[h] = "[REDACTED]";
108
+ }
109
+
110
+ // Save to database
111
+ const id = saveRequest({
112
+ method, url: req.url, host, path, scheme,
113
+ requestHeaders: reqHeaders, requestBody: reqBody,
114
+ statusCode, responseHeaders: respHeaders, responseBody: respBody,
115
+ durationMs,
116
+ });
117
+
118
+ // Notify callback
119
+ onRequest?.({ id, method, url: req.url, host, statusCode, durationMs, size: respBody.length });
120
+
121
+ // Return response to client
122
+ return new Response(respBody, {
123
+ status: statusCode,
124
+ headers: respHeaders,
125
+ });
126
+ } catch (err: unknown) {
127
+ const durationMs = Date.now() - startTime;
128
+ const errMsg = err instanceof Error ? err.message : "Proxy error";
129
+
130
+ saveRequest({
131
+ method, url: req.url, host, path, scheme,
132
+ requestHeaders: reqHeaders, requestBody: reqBody,
133
+ statusCode: 502, responseHeaders: {}, responseBody: errMsg,
134
+ durationMs,
135
+ });
136
+
137
+ return new Response(errMsg, { status: 502 });
138
+ }
139
+ },
140
+ });
141
+
142
+ console.log(`\n◆ Domain Sniper Proxy running on http://127.0.0.1:${port}`);
143
+ console.log(` ⚠ Bound to localhost only — do not expose on a public interface`);
144
+ console.log(` HTTPS interception: ${httpsInterception ? "ON (CA cert required)" : "OFF"}`);
145
+ if (filterHosts && filterHosts.length > 0) {
146
+ console.log(` Filtering: ${filterHosts.join(", ")}`);
147
+ }
148
+ console.log(` Requests are logged to ~/.domain-sniper/proxy.db\n`);
149
+
150
+ return {
151
+ stop() {
152
+ server.stop();
153
+ },
154
+ };
155
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "compilerOptions": {
3
+ // Environment setup & latest features
4
+ "lib": ["ESNext"],
5
+ "target": "ESNext",
6
+ "module": "Preserve",
7
+ "moduleDetection": "force",
8
+ "jsx": "react-jsx",
9
+ "jsxImportSource": "@opentui/react",
10
+ "allowJs": true,
11
+
12
+ // Bundler mode
13
+ "moduleResolution": "bundler",
14
+ "allowImportingTsExtensions": true,
15
+ "verbatimModuleSyntax": true,
16
+ "noEmit": true,
17
+
18
+ // Best practices
19
+ "strict": true,
20
+ "skipLibCheck": true,
21
+ "noFallthroughCasesInSwitch": true,
22
+ "noUncheckedIndexedAccess": true,
23
+ "noImplicitOverride": true,
24
+
25
+ // Some stricter flags (disabled by default)
26
+ "noUnusedLocals": false,
27
+ "noUnusedParameters": false,
28
+ "noPropertyAccessFromIndexSignature": false
29
+ }
30
+ }