domain-search-mcp 1.0.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 +52 -0
- package/Dockerfile +15 -0
- package/LICENSE +21 -0
- package/README.md +426 -0
- package/SECURITY.md +252 -0
- package/dist/config.d.ts +25 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +117 -0
- package/dist/config.js.map +1 -0
- package/dist/fallbacks/index.d.ts +6 -0
- package/dist/fallbacks/index.d.ts.map +1 -0
- package/dist/fallbacks/index.js +14 -0
- package/dist/fallbacks/index.js.map +1 -0
- package/dist/fallbacks/rdap.d.ts +18 -0
- package/dist/fallbacks/rdap.d.ts.map +1 -0
- package/dist/fallbacks/rdap.js +339 -0
- package/dist/fallbacks/rdap.js.map +1 -0
- package/dist/fallbacks/whois.d.ts +27 -0
- package/dist/fallbacks/whois.d.ts.map +1 -0
- package/dist/fallbacks/whois.js +219 -0
- package/dist/fallbacks/whois.js.map +1 -0
- package/dist/registrars/base.d.ts +89 -0
- package/dist/registrars/base.d.ts.map +1 -0
- package/dist/registrars/base.js +203 -0
- package/dist/registrars/base.js.map +1 -0
- package/dist/registrars/index.d.ts +7 -0
- package/dist/registrars/index.d.ts.map +1 -0
- package/dist/registrars/index.js +15 -0
- package/dist/registrars/index.js.map +1 -0
- package/dist/registrars/namecheap.d.ts +69 -0
- package/dist/registrars/namecheap.d.ts.map +1 -0
- package/dist/registrars/namecheap.js +307 -0
- package/dist/registrars/namecheap.js.map +1 -0
- package/dist/registrars/porkbun.d.ts +63 -0
- package/dist/registrars/porkbun.d.ts.map +1 -0
- package/dist/registrars/porkbun.js +299 -0
- package/dist/registrars/porkbun.js.map +1 -0
- package/dist/server.d.ts +19 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +209 -0
- package/dist/server.js.map +1 -0
- package/dist/services/domain-search.d.ts +40 -0
- package/dist/services/domain-search.d.ts.map +1 -0
- package/dist/services/domain-search.js +438 -0
- package/dist/services/domain-search.js.map +1 -0
- package/dist/services/index.d.ts +5 -0
- package/dist/services/index.d.ts.map +1 -0
- package/dist/services/index.js +11 -0
- package/dist/services/index.js.map +1 -0
- package/dist/tools/bulk_search.d.ts +72 -0
- package/dist/tools/bulk_search.d.ts.map +1 -0
- package/dist/tools/bulk_search.js +108 -0
- package/dist/tools/bulk_search.js.map +1 -0
- package/dist/tools/check_socials.d.ts +71 -0
- package/dist/tools/check_socials.d.ts.map +1 -0
- package/dist/tools/check_socials.js +357 -0
- package/dist/tools/check_socials.js.map +1 -0
- package/dist/tools/compare_registrars.d.ts +80 -0
- package/dist/tools/compare_registrars.d.ts.map +1 -0
- package/dist/tools/compare_registrars.js +116 -0
- package/dist/tools/compare_registrars.js.map +1 -0
- package/dist/tools/index.d.ts +10 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +31 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/search_domain.d.ts +61 -0
- package/dist/tools/search_domain.d.ts.map +1 -0
- package/dist/tools/search_domain.js +81 -0
- package/dist/tools/search_domain.js.map +1 -0
- package/dist/tools/suggest_domains.d.ts +82 -0
- package/dist/tools/suggest_domains.d.ts.map +1 -0
- package/dist/tools/suggest_domains.js +227 -0
- package/dist/tools/suggest_domains.js.map +1 -0
- package/dist/tools/tld_info.d.ts +56 -0
- package/dist/tools/tld_info.d.ts.map +1 -0
- package/dist/tools/tld_info.js +273 -0
- package/dist/tools/tld_info.js.map +1 -0
- package/dist/types.d.ts +193 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +9 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/cache.d.ts +81 -0
- package/dist/utils/cache.d.ts.map +1 -0
- package/dist/utils/cache.js +192 -0
- package/dist/utils/cache.js.map +1 -0
- package/dist/utils/errors.d.ts +87 -0
- package/dist/utils/errors.d.ts.map +1 -0
- package/dist/utils/errors.js +191 -0
- package/dist/utils/errors.js.map +1 -0
- package/dist/utils/index.d.ts +8 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +24 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/logger.d.ts +27 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +132 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/premium-analyzer.d.ts +33 -0
- package/dist/utils/premium-analyzer.d.ts.map +1 -0
- package/dist/utils/premium-analyzer.js +273 -0
- package/dist/utils/premium-analyzer.js.map +1 -0
- package/dist/utils/validators.d.ts +53 -0
- package/dist/utils/validators.d.ts.map +1 -0
- package/dist/utils/validators.js +159 -0
- package/dist/utils/validators.js.map +1 -0
- package/docs/marketing/devto-post.md +135 -0
- package/docs/marketing/hackernews.md +42 -0
- package/docs/marketing/producthunt.md +109 -0
- package/docs/marketing/reddit-post.md +59 -0
- package/docs/marketing/twitter-thread.md +105 -0
- package/examples/bulk-search-50-domains.ts +131 -0
- package/examples/cli-interactive.ts +280 -0
- package/examples/compare-registrars.ts +78 -0
- package/examples/search-single-domain.ts +54 -0
- package/examples/suggest-names.ts +110 -0
- package/glama.json +6 -0
- package/jest.config.js +35 -0
- package/package.json +62 -0
- package/smithery.yaml +36 -0
- package/src/config.ts +121 -0
- package/src/fallbacks/index.ts +6 -0
- package/src/fallbacks/rdap.ts +407 -0
- package/src/fallbacks/whois.ts +250 -0
- package/src/registrars/base.ts +264 -0
- package/src/registrars/index.ts +7 -0
- package/src/registrars/namecheap.ts +378 -0
- package/src/registrars/porkbun.ts +380 -0
- package/src/server.ts +276 -0
- package/src/services/domain-search.ts +567 -0
- package/src/services/index.ts +9 -0
- package/src/tools/bulk_search.ts +142 -0
- package/src/tools/check_socials.ts +467 -0
- package/src/tools/compare_registrars.ts +162 -0
- package/src/tools/index.ts +45 -0
- package/src/tools/search_domain.ts +93 -0
- package/src/tools/suggest_domains.ts +284 -0
- package/src/tools/tld_info.ts +294 -0
- package/src/types.ts +289 -0
- package/src/utils/cache.ts +238 -0
- package/src/utils/errors.ts +262 -0
- package/src/utils/index.ts +8 -0
- package/src/utils/logger.ts +162 -0
- package/src/utils/premium-analyzer.ts +303 -0
- package/src/utils/validators.ts +193 -0
- package/tests/premium-analyzer.test.ts +310 -0
- package/tests/unit/cache.test.ts +123 -0
- package/tests/unit/errors.test.ts +190 -0
- package/tests/unit/tld-info.test.ts +62 -0
- package/tests/unit/tools.test.ts +200 -0
- package/tests/unit/validators.test.ts +146 -0
- package/tsconfig.json +25 -0
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WHOIS Fallback (RFC 3912).
|
|
3
|
+
*
|
|
4
|
+
* Legacy protocol for domain lookup.
|
|
5
|
+
* Public, no authentication required.
|
|
6
|
+
* Slower than RDAP - use as last resort.
|
|
7
|
+
*
|
|
8
|
+
* Note: We use a public WHOIS API to avoid raw TCP connections
|
|
9
|
+
* which aren't well-supported in all Node.js environments.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import axios, { type AxiosError } from 'axios';
|
|
13
|
+
import type { DomainResult } from '../types.js';
|
|
14
|
+
import { logger } from '../utils/logger.js';
|
|
15
|
+
import { TimeoutError, RegistrarApiError } from '../utils/errors.js';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* WHOIS server mappings for common TLDs.
|
|
19
|
+
*/
|
|
20
|
+
const WHOIS_SERVERS: Record<string, string> = {
|
|
21
|
+
com: 'whois.verisign-grs.com',
|
|
22
|
+
net: 'whois.verisign-grs.com',
|
|
23
|
+
org: 'whois.pir.org',
|
|
24
|
+
io: 'whois.nic.io',
|
|
25
|
+
dev: 'whois.nic.google',
|
|
26
|
+
app: 'whois.nic.google',
|
|
27
|
+
co: 'whois.nic.co',
|
|
28
|
+
ai: 'whois.nic.ai',
|
|
29
|
+
me: 'whois.nic.me',
|
|
30
|
+
cc: 'ccwhois.verisign-grs.com',
|
|
31
|
+
xyz: 'whois.nic.xyz',
|
|
32
|
+
sh: 'whois.nic.sh',
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Patterns that indicate a domain is NOT available.
|
|
37
|
+
*/
|
|
38
|
+
const REGISTERED_PATTERNS = [
|
|
39
|
+
/domain name:/i,
|
|
40
|
+
/registrant:/i,
|
|
41
|
+
/creation date:/i,
|
|
42
|
+
/name server:/i,
|
|
43
|
+
/status:\s*(?:active|ok|registered)/i,
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Patterns that indicate a domain IS available.
|
|
48
|
+
*/
|
|
49
|
+
const AVAILABLE_PATTERNS = [
|
|
50
|
+
/no match/i,
|
|
51
|
+
/not found/i,
|
|
52
|
+
/no data found/i,
|
|
53
|
+
/no entries found/i,
|
|
54
|
+
/no object found/i,
|
|
55
|
+
/domain not found/i,
|
|
56
|
+
/no whois server/i,
|
|
57
|
+
/available for registration/i,
|
|
58
|
+
/is free/i,
|
|
59
|
+
/status:\s*free/i,
|
|
60
|
+
];
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Parse WHOIS response to determine availability.
|
|
64
|
+
*/
|
|
65
|
+
function parseWhoisResponse(response: string): boolean {
|
|
66
|
+
const text = response.toLowerCase();
|
|
67
|
+
|
|
68
|
+
// Check for "available" patterns first
|
|
69
|
+
for (const pattern of AVAILABLE_PATTERNS) {
|
|
70
|
+
if (pattern.test(text)) {
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Check for "registered" patterns
|
|
76
|
+
for (const pattern of REGISTERED_PATTERNS) {
|
|
77
|
+
if (pattern.test(text)) {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// If no clear indication, assume not available (safer)
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Check domain availability using a public WHOIS API.
|
|
88
|
+
*
|
|
89
|
+
* We use a web-based WHOIS lookup to avoid TCP connection issues.
|
|
90
|
+
* This is more reliable across different environments.
|
|
91
|
+
*/
|
|
92
|
+
export async function checkWhois(
|
|
93
|
+
domain: string,
|
|
94
|
+
tld: string,
|
|
95
|
+
): Promise<DomainResult> {
|
|
96
|
+
const fullDomain = `${domain}.${tld}`;
|
|
97
|
+
logger.debug('WHOIS check', { domain: fullDomain });
|
|
98
|
+
|
|
99
|
+
// Use a public WHOIS API service
|
|
100
|
+
// There are several options; we'll try a few
|
|
101
|
+
const apis = [
|
|
102
|
+
{
|
|
103
|
+
url: `https://whoisjson.com/api/v1/whois`,
|
|
104
|
+
params: { domain: fullDomain },
|
|
105
|
+
parser: (data: Record<string, unknown>) => {
|
|
106
|
+
// If we get a result, domain is registered
|
|
107
|
+
if (data.domain_name || data.registrar) {
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
// If error or no data, domain might be available
|
|
111
|
+
return true;
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
];
|
|
115
|
+
|
|
116
|
+
// Try each API in order
|
|
117
|
+
for (const api of apis) {
|
|
118
|
+
try {
|
|
119
|
+
const response = await axios.get(api.url, {
|
|
120
|
+
params: api.params,
|
|
121
|
+
timeout: 15000,
|
|
122
|
+
headers: {
|
|
123
|
+
Accept: 'application/json',
|
|
124
|
+
},
|
|
125
|
+
validateStatus: () => true, // Don't throw on any status
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
if (response.status === 200 && response.data) {
|
|
129
|
+
// Try to parse the response
|
|
130
|
+
let available: boolean;
|
|
131
|
+
|
|
132
|
+
if (typeof response.data === 'string') {
|
|
133
|
+
available = parseWhoisResponse(response.data);
|
|
134
|
+
} else {
|
|
135
|
+
available = api.parser(response.data as Record<string, unknown>);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return createWhoisResult(domain, tld, available);
|
|
139
|
+
}
|
|
140
|
+
} catch (error) {
|
|
141
|
+
logger.debug('WHOIS API failed, trying next', {
|
|
142
|
+
api: api.url,
|
|
143
|
+
error: error instanceof Error ? error.message : String(error),
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// If all APIs fail, try a simple text-based WHOIS lookup
|
|
149
|
+
try {
|
|
150
|
+
const available = await textBasedWhoisCheck(fullDomain, tld);
|
|
151
|
+
return createWhoisResult(domain, tld, available);
|
|
152
|
+
} catch (error) {
|
|
153
|
+
if (error instanceof Error && error.message.includes('timeout')) {
|
|
154
|
+
throw new TimeoutError('WHOIS lookup', 15000);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
throw new RegistrarApiError(
|
|
158
|
+
'whois',
|
|
159
|
+
error instanceof Error ? error.message : 'All WHOIS lookups failed',
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Simple text-based WHOIS check using a web proxy.
|
|
166
|
+
*/
|
|
167
|
+
async function textBasedWhoisCheck(
|
|
168
|
+
fullDomain: string,
|
|
169
|
+
tld: string,
|
|
170
|
+
): Promise<boolean> {
|
|
171
|
+
// Try who.is web service
|
|
172
|
+
try {
|
|
173
|
+
const response = await axios.get(`https://who.is/whois/${fullDomain}`, {
|
|
174
|
+
timeout: 15000,
|
|
175
|
+
headers: {
|
|
176
|
+
'User-Agent': 'Domain-Search-MCP/1.0',
|
|
177
|
+
},
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
const html = response.data as string;
|
|
181
|
+
|
|
182
|
+
// Check for "not registered" indicators in the page
|
|
183
|
+
if (
|
|
184
|
+
html.includes('is available for registration') ||
|
|
185
|
+
html.includes('No match for') ||
|
|
186
|
+
html.includes('not found')
|
|
187
|
+
) {
|
|
188
|
+
return true;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Check for registered indicators
|
|
192
|
+
if (
|
|
193
|
+
html.includes('Registrar:') ||
|
|
194
|
+
html.includes('Creation Date:') ||
|
|
195
|
+
html.includes('Name Server:')
|
|
196
|
+
) {
|
|
197
|
+
return false;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Default to not available
|
|
201
|
+
return false;
|
|
202
|
+
} catch (error) {
|
|
203
|
+
if (axios.isAxiosError(error)) {
|
|
204
|
+
const axiosError = error as AxiosError;
|
|
205
|
+
|
|
206
|
+
if (axiosError.code === 'ECONNABORTED') {
|
|
207
|
+
throw new Error('timeout');
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
throw error;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Create a standardized result from WHOIS.
|
|
217
|
+
*/
|
|
218
|
+
function createWhoisResult(
|
|
219
|
+
domain: string,
|
|
220
|
+
tld: string,
|
|
221
|
+
available: boolean,
|
|
222
|
+
): DomainResult {
|
|
223
|
+
return {
|
|
224
|
+
domain: `${domain}.${tld}`,
|
|
225
|
+
available,
|
|
226
|
+
premium: false, // WHOIS doesn't tell us about premium status
|
|
227
|
+
price_first_year: null, // WHOIS doesn't provide pricing
|
|
228
|
+
price_renewal: null,
|
|
229
|
+
currency: 'USD',
|
|
230
|
+
privacy_included: false,
|
|
231
|
+
transfer_price: null,
|
|
232
|
+
registrar: 'unknown',
|
|
233
|
+
source: 'whois',
|
|
234
|
+
checked_at: new Date().toISOString(),
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Get WHOIS server for a TLD.
|
|
240
|
+
*/
|
|
241
|
+
export function getWhoisServer(tld: string): string | null {
|
|
242
|
+
return WHOIS_SERVERS[tld] || null;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Check if WHOIS is available for a TLD.
|
|
247
|
+
*/
|
|
248
|
+
export function isWhoisAvailable(tld: string): boolean {
|
|
249
|
+
return WHOIS_SERVERS[tld] !== undefined;
|
|
250
|
+
}
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base Registrar Adapter.
|
|
3
|
+
*
|
|
4
|
+
* Abstract class that all registrar adapters extend.
|
|
5
|
+
* Provides common functionality:
|
|
6
|
+
* - Rate limiting (token bucket)
|
|
7
|
+
* - Retry with exponential backoff
|
|
8
|
+
* - Error handling and logging
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { DomainResult, TLDInfo } from '../types.js';
|
|
12
|
+
import { logger } from '../utils/logger.js';
|
|
13
|
+
import {
|
|
14
|
+
RateLimitError,
|
|
15
|
+
RegistrarApiError,
|
|
16
|
+
TimeoutError,
|
|
17
|
+
wrapError,
|
|
18
|
+
} from '../utils/errors.js';
|
|
19
|
+
import { config } from '../config.js';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Token bucket rate limiter.
|
|
23
|
+
*/
|
|
24
|
+
export class RateLimiter {
|
|
25
|
+
private tokens: number;
|
|
26
|
+
private readonly maxTokens: number;
|
|
27
|
+
private readonly refillRate: number; // tokens per second
|
|
28
|
+
private lastRefill: number;
|
|
29
|
+
|
|
30
|
+
constructor(maxPerMinute: number = 60) {
|
|
31
|
+
this.maxTokens = maxPerMinute;
|
|
32
|
+
this.tokens = maxPerMinute;
|
|
33
|
+
this.refillRate = maxPerMinute / 60; // Convert to per-second
|
|
34
|
+
this.lastRefill = Date.now();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Try to consume a token. Returns true if successful.
|
|
39
|
+
*/
|
|
40
|
+
tryConsume(): boolean {
|
|
41
|
+
this.refill();
|
|
42
|
+
|
|
43
|
+
if (this.tokens >= 1) {
|
|
44
|
+
this.tokens -= 1;
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Wait until a token is available.
|
|
53
|
+
*/
|
|
54
|
+
async waitForToken(): Promise<void> {
|
|
55
|
+
while (!this.tryConsume()) {
|
|
56
|
+
// Calculate wait time for next token
|
|
57
|
+
const waitMs = Math.ceil(1000 / this.refillRate);
|
|
58
|
+
await sleep(waitMs);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Refill tokens based on elapsed time.
|
|
64
|
+
*/
|
|
65
|
+
private refill(): void {
|
|
66
|
+
const now = Date.now();
|
|
67
|
+
const elapsed = (now - this.lastRefill) / 1000;
|
|
68
|
+
const refillAmount = elapsed * this.refillRate;
|
|
69
|
+
|
|
70
|
+
this.tokens = Math.min(this.maxTokens, this.tokens + refillAmount);
|
|
71
|
+
this.lastRefill = now;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Get seconds until next token is available.
|
|
76
|
+
*/
|
|
77
|
+
getWaitSeconds(): number {
|
|
78
|
+
if (this.tokens >= 1) return 0;
|
|
79
|
+
const neededTokens = 1 - this.tokens;
|
|
80
|
+
return Math.ceil(neededTokens / this.refillRate);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Sleep helper.
|
|
86
|
+
*/
|
|
87
|
+
function sleep(ms: number): Promise<void> {
|
|
88
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Abstract base class for registrar adapters.
|
|
93
|
+
*/
|
|
94
|
+
export abstract class RegistrarAdapter {
|
|
95
|
+
/** Human-readable name of the registrar */
|
|
96
|
+
abstract readonly name: string;
|
|
97
|
+
|
|
98
|
+
/** Identifier used in results */
|
|
99
|
+
abstract readonly id: string;
|
|
100
|
+
|
|
101
|
+
/** Rate limiter for this registrar */
|
|
102
|
+
protected readonly rateLimiter: RateLimiter;
|
|
103
|
+
|
|
104
|
+
/** Max retry attempts */
|
|
105
|
+
protected readonly maxRetries: number = 3;
|
|
106
|
+
|
|
107
|
+
/** Base delay for exponential backoff (ms) */
|
|
108
|
+
protected readonly baseDelayMs: number = 2000;
|
|
109
|
+
|
|
110
|
+
/** Request timeout (ms) */
|
|
111
|
+
protected readonly timeoutMs: number = 10000;
|
|
112
|
+
|
|
113
|
+
constructor(requestsPerMinute: number = 60) {
|
|
114
|
+
this.rateLimiter = new RateLimiter(requestsPerMinute);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Check domain availability and get pricing.
|
|
119
|
+
* This is the main method each adapter must implement.
|
|
120
|
+
*/
|
|
121
|
+
abstract search(domain: string, tld: string): Promise<DomainResult>;
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Get information about a TLD.
|
|
125
|
+
* Optional - not all registrars provide this.
|
|
126
|
+
*/
|
|
127
|
+
abstract getTldInfo(tld: string): Promise<TLDInfo | null>;
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Check if this adapter is enabled (has required credentials).
|
|
131
|
+
*/
|
|
132
|
+
abstract isEnabled(): boolean;
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Execute a function with rate limiting.
|
|
136
|
+
*/
|
|
137
|
+
protected async rateLimitedCall<T>(fn: () => Promise<T>): Promise<T> {
|
|
138
|
+
// Check if we should even try (dry run mode)
|
|
139
|
+
if (config.dryRun) {
|
|
140
|
+
throw new RegistrarApiError(this.name, 'Dry run mode - no API calls made');
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Wait for rate limit
|
|
144
|
+
const waitSeconds = this.rateLimiter.getWaitSeconds();
|
|
145
|
+
if (waitSeconds > 0) {
|
|
146
|
+
logger.debug(`Rate limiting: waiting ${waitSeconds}s for ${this.name}`);
|
|
147
|
+
}
|
|
148
|
+
await this.rateLimiter.waitForToken();
|
|
149
|
+
|
|
150
|
+
return fn();
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Execute with retry and exponential backoff.
|
|
155
|
+
*/
|
|
156
|
+
protected async retryWithBackoff<T>(
|
|
157
|
+
fn: () => Promise<T>,
|
|
158
|
+
operation: string,
|
|
159
|
+
): Promise<T> {
|
|
160
|
+
let lastError: Error | undefined;
|
|
161
|
+
|
|
162
|
+
for (let attempt = 1; attempt <= this.maxRetries; attempt++) {
|
|
163
|
+
try {
|
|
164
|
+
return await this.rateLimitedCall(fn);
|
|
165
|
+
} catch (error) {
|
|
166
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
167
|
+
|
|
168
|
+
// Check if we should retry
|
|
169
|
+
const wrapped = wrapError(error);
|
|
170
|
+
if (!wrapped.retryable) {
|
|
171
|
+
throw wrapped;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Check if it's a rate limit error with retry-after
|
|
175
|
+
if (error instanceof RateLimitError && error.retryAfter) {
|
|
176
|
+
const waitMs = error.retryAfter - Date.now();
|
|
177
|
+
if (waitMs > 0 && waitMs < 60000) {
|
|
178
|
+
logger.info(`Rate limited, waiting ${waitMs}ms`, {
|
|
179
|
+
registrar: this.name,
|
|
180
|
+
attempt,
|
|
181
|
+
});
|
|
182
|
+
await sleep(waitMs);
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Exponential backoff
|
|
188
|
+
if (attempt < this.maxRetries) {
|
|
189
|
+
const delay = this.baseDelayMs * Math.pow(2, attempt - 1);
|
|
190
|
+
logger.warn(`Retry ${attempt}/${this.maxRetries} for ${operation}`, {
|
|
191
|
+
registrar: this.name,
|
|
192
|
+
delay_ms: delay,
|
|
193
|
+
error: lastError.message,
|
|
194
|
+
});
|
|
195
|
+
await sleep(delay);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// All retries failed
|
|
201
|
+
throw lastError || new Error(`Failed after ${this.maxRetries} retries`);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Create a timeout wrapper for a promise.
|
|
206
|
+
*/
|
|
207
|
+
protected withTimeout<T>(
|
|
208
|
+
promise: Promise<T>,
|
|
209
|
+
operation: string,
|
|
210
|
+
timeoutMs: number = this.timeoutMs,
|
|
211
|
+
): Promise<T> {
|
|
212
|
+
return new Promise((resolve, reject) => {
|
|
213
|
+
const timer = setTimeout(() => {
|
|
214
|
+
reject(new TimeoutError(operation, timeoutMs));
|
|
215
|
+
}, timeoutMs);
|
|
216
|
+
|
|
217
|
+
promise
|
|
218
|
+
.then((result) => {
|
|
219
|
+
clearTimeout(timer);
|
|
220
|
+
resolve(result);
|
|
221
|
+
})
|
|
222
|
+
.catch((error) => {
|
|
223
|
+
clearTimeout(timer);
|
|
224
|
+
reject(error);
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Log an error with context.
|
|
231
|
+
*/
|
|
232
|
+
protected logError(error: Error, context?: Record<string, unknown>): void {
|
|
233
|
+
logger.logError(`${this.name} error`, error, {
|
|
234
|
+
registrar: this.id,
|
|
235
|
+
...context,
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Create a standardized DomainResult.
|
|
241
|
+
*/
|
|
242
|
+
protected createResult(
|
|
243
|
+
domain: string,
|
|
244
|
+
tld: string,
|
|
245
|
+
data: Partial<DomainResult>,
|
|
246
|
+
): DomainResult {
|
|
247
|
+
return {
|
|
248
|
+
domain: `${domain}.${tld}`,
|
|
249
|
+
available: data.available ?? false,
|
|
250
|
+
premium: data.premium ?? false,
|
|
251
|
+
price_first_year: data.price_first_year ?? null,
|
|
252
|
+
price_renewal: data.price_renewal ?? null,
|
|
253
|
+
currency: data.currency ?? 'USD',
|
|
254
|
+
privacy_included: data.privacy_included ?? false,
|
|
255
|
+
transfer_price: data.transfer_price ?? null,
|
|
256
|
+
registrar: this.id,
|
|
257
|
+
source: data.source ?? (`${this.id}_api` as DomainResult['source']),
|
|
258
|
+
checked_at: new Date().toISOString(),
|
|
259
|
+
premium_reason: data.premium_reason,
|
|
260
|
+
tld_restrictions: data.tld_restrictions,
|
|
261
|
+
score: data.score,
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
}
|