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,378 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Namecheap Registrar Adapter.
|
|
3
|
+
*
|
|
4
|
+
* Namecheap uses an XML-based API.
|
|
5
|
+
* API Docs: https://www.namecheap.com/support/api/intro/
|
|
6
|
+
*
|
|
7
|
+
* Note: Namecheap requires IP whitelisting for API access.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import axios, { type AxiosInstance, type AxiosError } from 'axios';
|
|
11
|
+
import { z } from 'zod';
|
|
12
|
+
import { RegistrarAdapter } from './base.js';
|
|
13
|
+
import type { DomainResult, TLDInfo } from '../types.js';
|
|
14
|
+
import { config } from '../config.js';
|
|
15
|
+
import { logger } from '../utils/logger.js';
|
|
16
|
+
import {
|
|
17
|
+
AuthenticationError,
|
|
18
|
+
RateLimitError,
|
|
19
|
+
RegistrarApiError,
|
|
20
|
+
} from '../utils/errors.js';
|
|
21
|
+
|
|
22
|
+
const NAMECHEAP_API_BASE = 'https://api.namecheap.com/xml.response';
|
|
23
|
+
const NAMECHEAP_SANDBOX_BASE = 'https://api.sandbox.namecheap.com/xml.response';
|
|
24
|
+
|
|
25
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
26
|
+
// Zod Schemas for Parsed XML Validation
|
|
27
|
+
// SECURITY: Validate parsed XML data to ensure expected structure
|
|
28
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Schema for parsed domain check result.
|
|
32
|
+
*/
|
|
33
|
+
const NamecheapCheckResultSchema = z.object({
|
|
34
|
+
available: z.boolean(),
|
|
35
|
+
premium: z.boolean(),
|
|
36
|
+
price: z.number().optional(),
|
|
37
|
+
renewalPrice: z.number().optional(),
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
type NamecheapCheckResult = z.infer<typeof NamecheapCheckResultSchema>;
|
|
41
|
+
|
|
42
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
43
|
+
// XML Parsing Utilities (Internal - hardcoded tags only)
|
|
44
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Allowed XML tags for parsing.
|
|
48
|
+
* SECURITY: Only these hardcoded tags can be parsed to prevent injection.
|
|
49
|
+
*/
|
|
50
|
+
const ALLOWED_XML_TAGS = new Set([
|
|
51
|
+
'Error',
|
|
52
|
+
'Errors',
|
|
53
|
+
'DomainCheckResult',
|
|
54
|
+
] as const);
|
|
55
|
+
|
|
56
|
+
const ALLOWED_XML_ATTRS = new Set([
|
|
57
|
+
'Count',
|
|
58
|
+
'Available',
|
|
59
|
+
'IsPremiumName',
|
|
60
|
+
'PremiumRegistrationPrice',
|
|
61
|
+
'PremiumRenewalPrice',
|
|
62
|
+
] as const);
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Parse XML response to extract domain info.
|
|
66
|
+
* Simple regex-based parsing since we don't want xml2js dependency.
|
|
67
|
+
*
|
|
68
|
+
* SECURITY: Only parses allowed tags defined in ALLOWED_XML_TAGS.
|
|
69
|
+
*/
|
|
70
|
+
function parseXmlValue(xml: string, tag: string): string | undefined {
|
|
71
|
+
if (!ALLOWED_XML_TAGS.has(tag as typeof ALLOWED_XML_TAGS extends Set<infer T> ? T : never)) {
|
|
72
|
+
logger.warn('Attempted to parse disallowed XML tag', { tag });
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
75
|
+
// Escape special regex chars in tag name for safety
|
|
76
|
+
const escapedTag = tag.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
77
|
+
// Reason: tag parameter is validated against ALLOWED_XML_TAGS whitelist above, not user input
|
|
78
|
+
const regex = new RegExp(`<${escapedTag}>([^<]*)</${escapedTag}>`, 'i'); // nosemgrep: detect-non-literal-regexp
|
|
79
|
+
const match = xml.match(regex);
|
|
80
|
+
return match?.[1];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function parseXmlAttribute(xml: string, tag: string, attr: string): string | undefined {
|
|
84
|
+
if (!ALLOWED_XML_TAGS.has(tag as typeof ALLOWED_XML_TAGS extends Set<infer T> ? T : never)) {
|
|
85
|
+
logger.warn('Attempted to parse disallowed XML tag', { tag });
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
88
|
+
if (!ALLOWED_XML_ATTRS.has(attr as typeof ALLOWED_XML_ATTRS extends Set<infer T> ? T : never)) {
|
|
89
|
+
logger.warn('Attempted to parse disallowed XML attribute', { attr });
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
// Escape special regex chars for safety
|
|
93
|
+
const escapedTag = tag.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
94
|
+
const escapedAttr = attr.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
95
|
+
// Reason: tag/attr parameters are validated against ALLOWED_XML_TAGS/ATTRS whitelists above, not user input
|
|
96
|
+
const regex = new RegExp(`<${escapedTag}[^>]*${escapedAttr}="([^"]*)"`, 'i'); // nosemgrep: detect-non-literal-regexp
|
|
97
|
+
const match = xml.match(regex);
|
|
98
|
+
return match?.[1];
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function parseXmlBool(value: string | undefined): boolean {
|
|
102
|
+
return value?.toLowerCase() === 'true';
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Namecheap adapter implementation.
|
|
107
|
+
*/
|
|
108
|
+
export class NamecheapAdapter extends RegistrarAdapter {
|
|
109
|
+
readonly name = 'Namecheap';
|
|
110
|
+
readonly id = 'namecheap';
|
|
111
|
+
|
|
112
|
+
private readonly client: AxiosInstance;
|
|
113
|
+
private readonly apiKey?: string;
|
|
114
|
+
private readonly apiUser?: string;
|
|
115
|
+
private readonly useSandbox: boolean;
|
|
116
|
+
|
|
117
|
+
constructor() {
|
|
118
|
+
// Namecheap has stricter rate limits, ~20/min is safe
|
|
119
|
+
super(20);
|
|
120
|
+
|
|
121
|
+
this.apiKey = config.namecheap.apiKey;
|
|
122
|
+
this.apiUser = config.namecheap.apiUser;
|
|
123
|
+
this.useSandbox = false; // Set to true for testing
|
|
124
|
+
|
|
125
|
+
const baseURL = this.useSandbox ? NAMECHEAP_SANDBOX_BASE : NAMECHEAP_API_BASE;
|
|
126
|
+
|
|
127
|
+
this.client = axios.create({
|
|
128
|
+
baseURL,
|
|
129
|
+
timeout: this.timeoutMs,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Check if Namecheap API is enabled.
|
|
135
|
+
*/
|
|
136
|
+
isEnabled(): boolean {
|
|
137
|
+
return config.namecheap.enabled;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Search for domain availability.
|
|
142
|
+
*/
|
|
143
|
+
async search(domain: string, tld: string): Promise<DomainResult> {
|
|
144
|
+
if (!this.isEnabled()) {
|
|
145
|
+
throw new AuthenticationError(
|
|
146
|
+
'namecheap',
|
|
147
|
+
'API credentials not configured',
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const fullDomain = `${domain}.${tld}`;
|
|
152
|
+
logger.debug('Namecheap search', { domain: fullDomain });
|
|
153
|
+
|
|
154
|
+
try {
|
|
155
|
+
const result = await this.retryWithBackoff(async () => {
|
|
156
|
+
const response = await this.client.get('', {
|
|
157
|
+
params: {
|
|
158
|
+
ApiUser: this.apiUser,
|
|
159
|
+
ApiKey: this.apiKey,
|
|
160
|
+
UserName: this.apiUser,
|
|
161
|
+
ClientIp: this.getClientIp(),
|
|
162
|
+
Command: 'namecheap.domains.check',
|
|
163
|
+
DomainList: fullDomain,
|
|
164
|
+
},
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
return this.parseCheckResponse(response.data, fullDomain);
|
|
168
|
+
}, `check ${fullDomain}`);
|
|
169
|
+
|
|
170
|
+
return this.createResult(domain, tld, {
|
|
171
|
+
available: result.available,
|
|
172
|
+
premium: result.premium,
|
|
173
|
+
price_first_year: result.price,
|
|
174
|
+
price_renewal: result.renewalPrice,
|
|
175
|
+
privacy_included: false, // Namecheap charges for privacy
|
|
176
|
+
source: 'namecheap_api',
|
|
177
|
+
premium_reason: result.premium ? 'Premium domain' : undefined,
|
|
178
|
+
});
|
|
179
|
+
} catch (error) {
|
|
180
|
+
this.handleApiError(error, fullDomain);
|
|
181
|
+
throw error;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Parse the check response XML.
|
|
187
|
+
* SECURITY: Validates parsed result with Zod schema.
|
|
188
|
+
*/
|
|
189
|
+
private parseCheckResponse(
|
|
190
|
+
xml: string,
|
|
191
|
+
domain: string,
|
|
192
|
+
): NamecheapCheckResult {
|
|
193
|
+
// Check for API errors
|
|
194
|
+
const errorCount = parseXmlAttribute(xml, 'Errors', 'Count');
|
|
195
|
+
if (errorCount && parseInt(errorCount, 10) > 0) {
|
|
196
|
+
const errorMsg = parseXmlValue(xml, 'Error') || 'Unknown API error';
|
|
197
|
+
|
|
198
|
+
if (errorMsg.includes('IP not whitelisted')) {
|
|
199
|
+
throw new AuthenticationError('namecheap', 'IP not whitelisted. Add your IP in Namecheap dashboard.');
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
throw new RegistrarApiError(this.name, errorMsg);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Parse domain result
|
|
206
|
+
const available = parseXmlAttribute(xml, 'DomainCheckResult', 'Available');
|
|
207
|
+
const isPremium = parseXmlAttribute(xml, 'DomainCheckResult', 'IsPremiumName');
|
|
208
|
+
const premiumPrice = parseXmlAttribute(xml, 'DomainCheckResult', 'PremiumRegistrationPrice');
|
|
209
|
+
const premiumRenewal = parseXmlAttribute(xml, 'DomainCheckResult', 'PremiumRenewalPrice');
|
|
210
|
+
|
|
211
|
+
const rawResult = {
|
|
212
|
+
available: parseXmlBool(available),
|
|
213
|
+
premium: parseXmlBool(isPremium),
|
|
214
|
+
price: premiumPrice ? parseFloat(premiumPrice) : undefined,
|
|
215
|
+
renewalPrice: premiumRenewal ? parseFloat(premiumRenewal) : undefined,
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
// Validate parsed result with Zod
|
|
219
|
+
const parseResult = NamecheapCheckResultSchema.safeParse(rawResult);
|
|
220
|
+
if (!parseResult.success) {
|
|
221
|
+
logger.warn('Namecheap API response validation failed', {
|
|
222
|
+
domain,
|
|
223
|
+
errors: parseResult.error.errors,
|
|
224
|
+
});
|
|
225
|
+
throw new RegistrarApiError(
|
|
226
|
+
this.name,
|
|
227
|
+
'Invalid API response format',
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return parseResult.data;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Get TLD information.
|
|
236
|
+
*/
|
|
237
|
+
async getTldInfo(tld: string): Promise<TLDInfo | null> {
|
|
238
|
+
// Namecheap doesn't have a great TLD info endpoint
|
|
239
|
+
// Return basic info based on known data
|
|
240
|
+
return {
|
|
241
|
+
tld,
|
|
242
|
+
description: `${tld.toUpperCase()} domain`,
|
|
243
|
+
typical_use: this.getTldUseCase(tld),
|
|
244
|
+
price_range: {
|
|
245
|
+
min: 8.88,
|
|
246
|
+
max: 15.98,
|
|
247
|
+
currency: 'USD',
|
|
248
|
+
},
|
|
249
|
+
renewal_price_typical: 12.98,
|
|
250
|
+
restrictions: [],
|
|
251
|
+
popularity: this.getTldPopularity(tld),
|
|
252
|
+
category: this.getTldCategory(tld),
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Get client IP for API requests.
|
|
258
|
+
* Namecheap requires this for all API calls.
|
|
259
|
+
*
|
|
260
|
+
* SECURITY: We no longer call external services (ipify.org) to get IP.
|
|
261
|
+
* The IP must be configured via NAMECHEAP_CLIENT_IP environment variable.
|
|
262
|
+
* This prevents unintended IP disclosure to third parties.
|
|
263
|
+
*/
|
|
264
|
+
private getClientIp(): string {
|
|
265
|
+
const clientIp = config.namecheap.clientIp;
|
|
266
|
+
|
|
267
|
+
if (!clientIp) {
|
|
268
|
+
throw new AuthenticationError(
|
|
269
|
+
'namecheap',
|
|
270
|
+
'NAMECHEAP_CLIENT_IP not configured. Add your whitelisted IP to .env file.',
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Basic IP format validation (IPv4 or IPv6)
|
|
275
|
+
const ipv4Pattern = /^(\d{1,3}\.){3}\d{1,3}$/;
|
|
276
|
+
const ipv6Pattern = /^([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$/;
|
|
277
|
+
|
|
278
|
+
if (!ipv4Pattern.test(clientIp) && !ipv6Pattern.test(clientIp)) {
|
|
279
|
+
throw new AuthenticationError(
|
|
280
|
+
'namecheap',
|
|
281
|
+
`Invalid NAMECHEAP_CLIENT_IP format: "${clientIp}". Must be a valid IPv4 or IPv6 address.`,
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
return clientIp;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Handle API errors with user-friendly messages.
|
|
290
|
+
*/
|
|
291
|
+
private handleApiError(error: unknown, domain: string): never {
|
|
292
|
+
if (axios.isAxiosError(error)) {
|
|
293
|
+
const axiosError = error as AxiosError;
|
|
294
|
+
|
|
295
|
+
if (axiosError.response) {
|
|
296
|
+
const status = axiosError.response.status;
|
|
297
|
+
|
|
298
|
+
if (status === 401 || status === 403) {
|
|
299
|
+
throw new AuthenticationError('namecheap', 'Invalid API credentials');
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (status === 429) {
|
|
303
|
+
throw new RateLimitError('namecheap');
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
throw new RegistrarApiError(
|
|
307
|
+
this.name,
|
|
308
|
+
`HTTP ${status}: ${axiosError.message}`,
|
|
309
|
+
status,
|
|
310
|
+
error,
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (axiosError.code === 'ECONNABORTED') {
|
|
315
|
+
throw new RegistrarApiError(
|
|
316
|
+
this.name,
|
|
317
|
+
`Request timed out for ${domain}`,
|
|
318
|
+
undefined,
|
|
319
|
+
error,
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
throw new RegistrarApiError(
|
|
325
|
+
this.name,
|
|
326
|
+
error instanceof Error ? error.message : 'Unknown error',
|
|
327
|
+
undefined,
|
|
328
|
+
error instanceof Error ? error : undefined,
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Get typical use case for a TLD.
|
|
334
|
+
*/
|
|
335
|
+
private getTldUseCase(tld: string): string {
|
|
336
|
+
const useCases: Record<string, string> = {
|
|
337
|
+
com: 'General commercial websites',
|
|
338
|
+
io: 'Tech startups and SaaS products',
|
|
339
|
+
dev: 'Developer tools and portfolios',
|
|
340
|
+
app: 'Mobile and web applications',
|
|
341
|
+
co: 'Companies and startups',
|
|
342
|
+
net: 'Network services and utilities',
|
|
343
|
+
org: 'Non-profit organizations',
|
|
344
|
+
};
|
|
345
|
+
return useCases[tld] || 'General purpose';
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Get TLD popularity rating.
|
|
350
|
+
*/
|
|
351
|
+
private getTldPopularity(tld: string): 'high' | 'medium' | 'low' {
|
|
352
|
+
const highPopularity = ['com', 'net', 'org', 'io', 'co'];
|
|
353
|
+
const mediumPopularity = ['dev', 'app', 'ai', 'me'];
|
|
354
|
+
|
|
355
|
+
if (highPopularity.includes(tld)) return 'high';
|
|
356
|
+
if (mediumPopularity.includes(tld)) return 'medium';
|
|
357
|
+
return 'low';
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Get TLD category.
|
|
362
|
+
*/
|
|
363
|
+
private getTldCategory(tld: string): TLDInfo['category'] {
|
|
364
|
+
const countryTlds = ['uk', 'de', 'fr', 'jp', 'cn', 'au', 'ca', 'us'];
|
|
365
|
+
const sponsoredTlds = ['edu', 'gov', 'mil'];
|
|
366
|
+
const newTlds = ['io', 'dev', 'app', 'ai', 'xyz', 'tech', 'cloud'];
|
|
367
|
+
|
|
368
|
+
if (countryTlds.includes(tld)) return 'country';
|
|
369
|
+
if (sponsoredTlds.includes(tld)) return 'sponsored';
|
|
370
|
+
if (newTlds.includes(tld)) return 'new';
|
|
371
|
+
return 'generic';
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Singleton instance.
|
|
377
|
+
*/
|
|
378
|
+
export const namecheapAdapter = new NamecheapAdapter();
|