@hasna/microservices 0.0.5 → 0.0.6
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/bin/index.js +9 -1
- package/bin/mcp.js +9 -1
- package/dist/index.js +9 -1
- package/microservices/microservice-company/package.json +27 -0
- package/microservices/microservice-company/src/cli/index.ts +1126 -0
- package/microservices/microservice-company/src/db/company.ts +854 -0
- package/microservices/microservice-company/src/db/database.ts +93 -0
- package/microservices/microservice-company/src/db/migrations.ts +214 -0
- package/microservices/microservice-company/src/db/workflow-migrations.ts +44 -0
- package/microservices/microservice-company/src/index.ts +60 -0
- package/microservices/microservice-company/src/lib/audit.ts +168 -0
- package/microservices/microservice-company/src/lib/finance.ts +299 -0
- package/microservices/microservice-company/src/lib/settings.ts +85 -0
- package/microservices/microservice-company/src/lib/workflows.ts +698 -0
- package/microservices/microservice-company/src/mcp/index.ts +991 -0
- package/microservices/microservice-domains/src/cli/index.ts +420 -0
- package/microservices/microservice-domains/src/lib/brandsight.ts +285 -0
- package/microservices/microservice-domains/src/lib/godaddy.ts +328 -0
- package/microservices/microservice-domains/src/lib/namecheap.ts +474 -0
- package/microservices/microservice-domains/src/lib/registrar.ts +355 -0
- package/microservices/microservice-domains/src/mcp/index.ts +245 -0
- package/package.json +1 -1
|
@@ -0,0 +1,474 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Namecheap API integration for domain management
|
|
3
|
+
*
|
|
4
|
+
* Requires environment variables:
|
|
5
|
+
* NAMECHEAP_API_KEY — API key from Namecheap
|
|
6
|
+
* NAMECHEAP_USERNAME — Namecheap account username
|
|
7
|
+
* NAMECHEAP_CLIENT_IP — Whitelisted client IP address
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
// ============================================================
|
|
11
|
+
// Types
|
|
12
|
+
// ============================================================
|
|
13
|
+
|
|
14
|
+
export interface NamecheapConfig {
|
|
15
|
+
apiKey: string;
|
|
16
|
+
username: string;
|
|
17
|
+
clientIp: string;
|
|
18
|
+
sandbox?: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface NamecheapDomain {
|
|
22
|
+
domain: string;
|
|
23
|
+
expiry: string;
|
|
24
|
+
autoRenew: boolean;
|
|
25
|
+
isLocked: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface NamecheapDomainInfo {
|
|
29
|
+
domain: string;
|
|
30
|
+
registrar: string;
|
|
31
|
+
created: string;
|
|
32
|
+
expires: string;
|
|
33
|
+
nameservers: string[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface NamecheapDnsRecord {
|
|
37
|
+
hostId?: string;
|
|
38
|
+
type: string;
|
|
39
|
+
name: string;
|
|
40
|
+
address: string;
|
|
41
|
+
mxPref?: number;
|
|
42
|
+
ttl: number;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface NamecheapAvailability {
|
|
46
|
+
domain: string;
|
|
47
|
+
available: boolean;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface NamecheapRenewResult {
|
|
51
|
+
domain: string;
|
|
52
|
+
success: boolean;
|
|
53
|
+
transactionId?: string;
|
|
54
|
+
chargedAmount?: string;
|
|
55
|
+
orderId?: string;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface NamecheapSyncResult {
|
|
59
|
+
synced: number;
|
|
60
|
+
errors: string[];
|
|
61
|
+
domains: string[];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ============================================================
|
|
65
|
+
// Configuration
|
|
66
|
+
// ============================================================
|
|
67
|
+
|
|
68
|
+
const API_BASE = "https://api.namecheap.com/xml.response";
|
|
69
|
+
const SANDBOX_BASE = "https://api.sandbox.namecheap.com/xml.response";
|
|
70
|
+
|
|
71
|
+
export function getConfig(): NamecheapConfig {
|
|
72
|
+
const apiKey = process.env["NAMECHEAP_API_KEY"];
|
|
73
|
+
const username = process.env["NAMECHEAP_USERNAME"];
|
|
74
|
+
const clientIp = process.env["NAMECHEAP_CLIENT_IP"];
|
|
75
|
+
|
|
76
|
+
if (!apiKey) throw new Error("NAMECHEAP_API_KEY environment variable is not set");
|
|
77
|
+
if (!username) throw new Error("NAMECHEAP_USERNAME environment variable is not set");
|
|
78
|
+
if (!clientIp) throw new Error("NAMECHEAP_CLIENT_IP environment variable is not set");
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
apiKey,
|
|
82
|
+
username,
|
|
83
|
+
clientIp,
|
|
84
|
+
sandbox: process.env["NAMECHEAP_SANDBOX"] === "true",
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ============================================================
|
|
89
|
+
// XML Parsing Helpers (regex-based, no dependencies)
|
|
90
|
+
// ============================================================
|
|
91
|
+
|
|
92
|
+
export function extractTag(xml: string, tag: string): string | null {
|
|
93
|
+
const regex = new RegExp(`<${tag}[^>]*>([^<]*)</${tag}>`, "i");
|
|
94
|
+
const match = xml.match(regex);
|
|
95
|
+
return match ? match[1].trim() : null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function extractAttribute(xml: string, tag: string, attr: string): string | null {
|
|
99
|
+
const regex = new RegExp(`<${tag}\\s[^>]*${attr}="([^"]*)"`, "i");
|
|
100
|
+
const match = xml.match(regex);
|
|
101
|
+
return match ? match[1] : null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function extractAllTags(xml: string, tag: string): string[] {
|
|
105
|
+
const regex = new RegExp(`<${tag}(?:\\s[^>]*)?\\/?>(?:([^<]*)<\\/${tag}>)?`, "gi");
|
|
106
|
+
const results: string[] = [];
|
|
107
|
+
let match;
|
|
108
|
+
while ((match = regex.exec(xml)) !== null) {
|
|
109
|
+
// Only match exact tag name — skip tags like <DomainListResult> when searching for <Domain>
|
|
110
|
+
const fullMatch = match[0];
|
|
111
|
+
const tagNameCheck = new RegExp(`^<${tag}(?:\\s|\\/>|>)`, "i");
|
|
112
|
+
if (tagNameCheck.test(fullMatch)) {
|
|
113
|
+
results.push(fullMatch);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return results;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function extractAttributeFromElement(element: string, attr: string): string | null {
|
|
120
|
+
const regex = new RegExp(`${attr}="([^"]*)"`, "i");
|
|
121
|
+
const match = element.match(regex);
|
|
122
|
+
return match ? match[1] : null;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function checkApiError(xml: string): void {
|
|
126
|
+
const status = extractAttribute(xml, "ApiResponse", "Status");
|
|
127
|
+
if (status === "ERROR") {
|
|
128
|
+
const errorMsg = extractTag(xml, "Message") || extractTag(xml, "Err") || "Unknown Namecheap API error";
|
|
129
|
+
const errorNumber = extractAttribute(xml, "Error", "Number") || extractAttribute(xml, "Err", "Number");
|
|
130
|
+
throw new Error(`Namecheap API error${errorNumber ? ` (${errorNumber})` : ""}: ${errorMsg}`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// ============================================================
|
|
135
|
+
// HTTP Layer
|
|
136
|
+
// ============================================================
|
|
137
|
+
|
|
138
|
+
export function buildUrl(command: string, config: NamecheapConfig, extraParams?: Record<string, string>): string {
|
|
139
|
+
const base = config.sandbox ? SANDBOX_BASE : API_BASE;
|
|
140
|
+
const params = new URLSearchParams({
|
|
141
|
+
ApiUser: config.username,
|
|
142
|
+
ApiKey: config.apiKey,
|
|
143
|
+
UserName: config.username,
|
|
144
|
+
ClientIp: config.clientIp,
|
|
145
|
+
Command: command,
|
|
146
|
+
...extraParams,
|
|
147
|
+
});
|
|
148
|
+
return `${base}?${params.toString()}`;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export async function apiRequest(command: string, config: NamecheapConfig, extraParams?: Record<string, string>): Promise<string> {
|
|
152
|
+
const url = buildUrl(command, config, extraParams);
|
|
153
|
+
const response = await fetch(url, {
|
|
154
|
+
signal: AbortSignal.timeout(30000),
|
|
155
|
+
headers: { "User-Agent": "microservice-domains/0.0.1" },
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
if (!response.ok) {
|
|
159
|
+
throw new Error(`Namecheap API HTTP error: ${response.status} ${response.statusText}`);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const xml = await response.text();
|
|
163
|
+
checkApiError(xml);
|
|
164
|
+
return xml;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// ============================================================
|
|
168
|
+
// API Functions
|
|
169
|
+
// ============================================================
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* List all domains in the Namecheap account
|
|
173
|
+
* Command: namecheap.domains.getList
|
|
174
|
+
*/
|
|
175
|
+
export async function listNamecheapDomains(config?: NamecheapConfig): Promise<NamecheapDomain[]> {
|
|
176
|
+
const cfg = config || getConfig();
|
|
177
|
+
const xml = await apiRequest("namecheap.domains.getList", cfg, {
|
|
178
|
+
PageSize: "100",
|
|
179
|
+
Page: "1",
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
const domainElements = extractAllTags(xml, "Domain");
|
|
183
|
+
const domains: NamecheapDomain[] = [];
|
|
184
|
+
|
|
185
|
+
for (const el of domainElements) {
|
|
186
|
+
const name = extractAttributeFromElement(el, "Name");
|
|
187
|
+
const expires = extractAttributeFromElement(el, "Expires");
|
|
188
|
+
const autoRenew = extractAttributeFromElement(el, "AutoRenew");
|
|
189
|
+
const isLocked = extractAttributeFromElement(el, "IsLocked");
|
|
190
|
+
|
|
191
|
+
if (name) {
|
|
192
|
+
domains.push({
|
|
193
|
+
domain: name,
|
|
194
|
+
expiry: expires || "",
|
|
195
|
+
autoRenew: autoRenew === "true",
|
|
196
|
+
isLocked: isLocked === "true",
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return domains;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Get detailed info for a specific domain
|
|
206
|
+
* Command: namecheap.domains.getInfo
|
|
207
|
+
*/
|
|
208
|
+
export async function getDomainInfo(domain: string, config?: NamecheapConfig): Promise<NamecheapDomainInfo> {
|
|
209
|
+
const cfg = config || getConfig();
|
|
210
|
+
const xml = await apiRequest("namecheap.domains.getInfo", cfg, {
|
|
211
|
+
DomainName: domain,
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
// Parse creation and expiry dates
|
|
215
|
+
const createdDate = extractTag(xml, "CreatedDate") || extractAttribute(xml, "DomainGetInfoResult", "CreatedDate") || "";
|
|
216
|
+
const expiresDate = extractTag(xml, "ExpiredDate") || extractAttribute(xml, "DomainGetInfoResult", "ExpiredDate") || "";
|
|
217
|
+
|
|
218
|
+
// Parse nameservers
|
|
219
|
+
const nsSection = xml.match(/<DnsDetails[^>]*>([\s\S]*?)<\/DnsDetails>/i);
|
|
220
|
+
const nameservers: string[] = [];
|
|
221
|
+
if (nsSection) {
|
|
222
|
+
const nsElements = nsSection[1].matchAll(/<Nameserver[^>]*>([^<]*)<\/Nameserver>/gi);
|
|
223
|
+
for (const m of nsElements) {
|
|
224
|
+
if (m[1]) nameservers.push(m[1].trim().toLowerCase());
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return {
|
|
229
|
+
domain,
|
|
230
|
+
registrar: "Namecheap",
|
|
231
|
+
created: createdDate,
|
|
232
|
+
expires: expiresDate,
|
|
233
|
+
nameservers,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Renew a domain
|
|
239
|
+
* Command: namecheap.domains.renew
|
|
240
|
+
*/
|
|
241
|
+
export async function renewDomain(domain: string, years: number = 1, config?: NamecheapConfig): Promise<NamecheapRenewResult> {
|
|
242
|
+
const cfg = config || getConfig();
|
|
243
|
+
const xml = await apiRequest("namecheap.domains.renew", cfg, {
|
|
244
|
+
DomainName: domain,
|
|
245
|
+
Years: String(years),
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
const transactionId = extractAttribute(xml, "DomainRenewResult", "TransactionID") || undefined;
|
|
249
|
+
const chargedAmount = extractAttribute(xml, "DomainRenewResult", "ChargedAmount") || undefined;
|
|
250
|
+
const orderId = extractAttribute(xml, "DomainRenewResult", "OrderID") || undefined;
|
|
251
|
+
|
|
252
|
+
return {
|
|
253
|
+
domain,
|
|
254
|
+
success: true,
|
|
255
|
+
transactionId,
|
|
256
|
+
chargedAmount,
|
|
257
|
+
orderId,
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Get DNS host records for a domain
|
|
263
|
+
* Command: namecheap.domains.dns.getHosts
|
|
264
|
+
*/
|
|
265
|
+
export async function getDnsRecords(domain: string, sld: string, tld: string, config?: NamecheapConfig): Promise<NamecheapDnsRecord[]> {
|
|
266
|
+
const cfg = config || getConfig();
|
|
267
|
+
const xml = await apiRequest("namecheap.domains.dns.getHosts", cfg, {
|
|
268
|
+
SLD: sld,
|
|
269
|
+
TLD: tld,
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
const hostElements = extractAllTags(xml, "host");
|
|
273
|
+
const records: NamecheapDnsRecord[] = [];
|
|
274
|
+
|
|
275
|
+
for (const el of hostElements) {
|
|
276
|
+
const type = extractAttributeFromElement(el, "Type");
|
|
277
|
+
const name = extractAttributeFromElement(el, "Name");
|
|
278
|
+
const address = extractAttributeFromElement(el, "Address");
|
|
279
|
+
const hostId = extractAttributeFromElement(el, "HostId");
|
|
280
|
+
const mxPref = extractAttributeFromElement(el, "MXPref");
|
|
281
|
+
const ttl = extractAttributeFromElement(el, "TTL");
|
|
282
|
+
|
|
283
|
+
if (type && name && address) {
|
|
284
|
+
records.push({
|
|
285
|
+
hostId: hostId || undefined,
|
|
286
|
+
type,
|
|
287
|
+
name,
|
|
288
|
+
address,
|
|
289
|
+
mxPref: mxPref ? parseInt(mxPref) : undefined,
|
|
290
|
+
ttl: ttl ? parseInt(ttl) : 1800,
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return records;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Set DNS host records for a domain
|
|
300
|
+
* Command: namecheap.domains.dns.setHosts
|
|
301
|
+
*/
|
|
302
|
+
export async function setDnsRecords(
|
|
303
|
+
domain: string,
|
|
304
|
+
sld: string,
|
|
305
|
+
tld: string,
|
|
306
|
+
records: NamecheapDnsRecord[],
|
|
307
|
+
config?: NamecheapConfig
|
|
308
|
+
): Promise<boolean> {
|
|
309
|
+
const cfg = config || getConfig();
|
|
310
|
+
const params: Record<string, string> = {
|
|
311
|
+
SLD: sld,
|
|
312
|
+
TLD: tld,
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
for (let i = 0; i < records.length; i++) {
|
|
316
|
+
const r = records[i];
|
|
317
|
+
const idx = i + 1;
|
|
318
|
+
params[`HostName${idx}`] = r.name;
|
|
319
|
+
params[`RecordType${idx}`] = r.type;
|
|
320
|
+
params[`Address${idx}`] = r.address;
|
|
321
|
+
params[`TTL${idx}`] = String(r.ttl);
|
|
322
|
+
if (r.mxPref !== undefined) {
|
|
323
|
+
params[`MXPref${idx}`] = String(r.mxPref);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
await apiRequest("namecheap.domains.dns.setHosts", cfg, params);
|
|
328
|
+
return true;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Check domain availability
|
|
333
|
+
* Command: namecheap.domains.check
|
|
334
|
+
*/
|
|
335
|
+
export async function checkAvailability(domain: string, config?: NamecheapConfig): Promise<NamecheapAvailability> {
|
|
336
|
+
const cfg = config || getConfig();
|
|
337
|
+
const xml = await apiRequest("namecheap.domains.check", cfg, {
|
|
338
|
+
DomainList: domain,
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
const available = extractAttribute(xml, "DomainCheckResult", "Available");
|
|
342
|
+
|
|
343
|
+
return {
|
|
344
|
+
domain,
|
|
345
|
+
available: available === "true",
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Split a domain name into SLD (second-level domain) and TLD (top-level domain)
|
|
351
|
+
*/
|
|
352
|
+
export function splitDomain(domain: string): { sld: string; tld: string } {
|
|
353
|
+
const parts = domain.split(".");
|
|
354
|
+
if (parts.length < 2) {
|
|
355
|
+
throw new Error(`Invalid domain: ${domain}`);
|
|
356
|
+
}
|
|
357
|
+
// Handle multi-part TLDs like .co.uk
|
|
358
|
+
if (parts.length >= 3 && ["co", "com", "org", "net", "ac", "gov"].includes(parts[parts.length - 2])) {
|
|
359
|
+
return {
|
|
360
|
+
sld: parts.slice(0, -2).join("."),
|
|
361
|
+
tld: parts.slice(-2).join("."),
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
return {
|
|
365
|
+
sld: parts.slice(0, -1).join("."),
|
|
366
|
+
tld: parts[parts.length - 1],
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Sync domains from Namecheap to local database
|
|
372
|
+
* Calls listDomains + getDomainInfo for each, upserts into local domains table
|
|
373
|
+
*/
|
|
374
|
+
export async function syncToLocalDb(dbFunctions: {
|
|
375
|
+
getDomainByName: (name: string) => { id: string } | null;
|
|
376
|
+
createDomain: (input: {
|
|
377
|
+
name: string;
|
|
378
|
+
registrar?: string;
|
|
379
|
+
status?: string;
|
|
380
|
+
registered_at?: string;
|
|
381
|
+
expires_at?: string;
|
|
382
|
+
auto_renew?: boolean;
|
|
383
|
+
nameservers?: string[];
|
|
384
|
+
}) => { id: string; name: string };
|
|
385
|
+
updateDomain: (
|
|
386
|
+
id: string,
|
|
387
|
+
input: {
|
|
388
|
+
registrar?: string;
|
|
389
|
+
status?: string;
|
|
390
|
+
registered_at?: string;
|
|
391
|
+
expires_at?: string;
|
|
392
|
+
auto_renew?: boolean;
|
|
393
|
+
nameservers?: string[];
|
|
394
|
+
}
|
|
395
|
+
) => unknown;
|
|
396
|
+
}, config?: NamecheapConfig): Promise<NamecheapSyncResult> {
|
|
397
|
+
const cfg = config || getConfig();
|
|
398
|
+
const result: NamecheapSyncResult = { synced: 0, errors: [], domains: [] };
|
|
399
|
+
|
|
400
|
+
let ncDomains: NamecheapDomain[];
|
|
401
|
+
try {
|
|
402
|
+
ncDomains = await listNamecheapDomains(cfg);
|
|
403
|
+
} catch (error) {
|
|
404
|
+
throw new Error(`Failed to list Namecheap domains: ${error instanceof Error ? error.message : String(error)}`);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
for (const ncDomain of ncDomains) {
|
|
408
|
+
try {
|
|
409
|
+
// Get detailed info
|
|
410
|
+
let info: NamecheapDomainInfo;
|
|
411
|
+
try {
|
|
412
|
+
info = await getDomainInfo(ncDomain.domain, cfg);
|
|
413
|
+
} catch {
|
|
414
|
+
// Fall back to basic info if getInfo fails
|
|
415
|
+
info = {
|
|
416
|
+
domain: ncDomain.domain,
|
|
417
|
+
registrar: "Namecheap",
|
|
418
|
+
created: "",
|
|
419
|
+
expires: ncDomain.expiry,
|
|
420
|
+
nameservers: [],
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// Normalize dates to ISO format
|
|
425
|
+
const expiresAt = normalizeDate(info.expires || ncDomain.expiry);
|
|
426
|
+
const createdAt = normalizeDate(info.created);
|
|
427
|
+
|
|
428
|
+
// Upsert into local DB
|
|
429
|
+
const existing = dbFunctions.getDomainByName(ncDomain.domain);
|
|
430
|
+
if (existing) {
|
|
431
|
+
dbFunctions.updateDomain(existing.id, {
|
|
432
|
+
registrar: "Namecheap",
|
|
433
|
+
status: "active",
|
|
434
|
+
registered_at: createdAt || undefined,
|
|
435
|
+
expires_at: expiresAt || undefined,
|
|
436
|
+
auto_renew: ncDomain.autoRenew,
|
|
437
|
+
nameservers: info.nameservers.length > 0 ? info.nameservers : undefined,
|
|
438
|
+
});
|
|
439
|
+
} else {
|
|
440
|
+
dbFunctions.createDomain({
|
|
441
|
+
name: ncDomain.domain,
|
|
442
|
+
registrar: "Namecheap",
|
|
443
|
+
status: "active",
|
|
444
|
+
registered_at: createdAt || undefined,
|
|
445
|
+
expires_at: expiresAt || undefined,
|
|
446
|
+
auto_renew: ncDomain.autoRenew,
|
|
447
|
+
nameservers: info.nameservers,
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
result.synced++;
|
|
452
|
+
result.domains.push(ncDomain.domain);
|
|
453
|
+
} catch (error) {
|
|
454
|
+
result.errors.push(`${ncDomain.domain}: ${error instanceof Error ? error.message : String(error)}`);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
return result;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// ============================================================
|
|
462
|
+
// Helpers
|
|
463
|
+
// ============================================================
|
|
464
|
+
|
|
465
|
+
function normalizeDate(dateStr: string): string | null {
|
|
466
|
+
if (!dateStr) return null;
|
|
467
|
+
try {
|
|
468
|
+
const d = new Date(dateStr);
|
|
469
|
+
if (isNaN(d.getTime())) return null;
|
|
470
|
+
return d.toISOString();
|
|
471
|
+
} catch {
|
|
472
|
+
return null;
|
|
473
|
+
}
|
|
474
|
+
}
|