@hasna/microservices 0.0.5 → 0.0.7

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.
@@ -0,0 +1,262 @@
1
+ /**
2
+ * Namecheap API integration for domain management
3
+ *
4
+ * Uses the Connector SDK pattern from connect-namecheap.
5
+ * When @hasna/connect-namecheap is published to npm, replace the
6
+ * relative connector import with the package import.
7
+ *
8
+ * Requires environment variables:
9
+ * NAMECHEAP_API_KEY — API key from Namecheap
10
+ * NAMECHEAP_USERNAME — Namecheap account username
11
+ * NAMECHEAP_CLIENT_IP — Whitelisted client IP address
12
+ */
13
+
14
+ // Re-export types used by consumers (CLI, MCP, registrar)
15
+ export type {
16
+ ConnectorConfig as NamecheapConfig,
17
+ Domain as NamecheapDomain,
18
+ DomainInfo as NamecheapDomainInfo,
19
+ DnsRecord as NamecheapDnsRecord,
20
+ AvailabilityResult as NamecheapAvailability,
21
+ RenewResult as NamecheapRenewResult,
22
+ } from "../../../../../open-connectors/connectors/connect-namecheap/src/index.js";
23
+
24
+ import {
25
+ Connector,
26
+ type ConnectorConfig,
27
+ type Domain as NamecheapDomain,
28
+ type DomainInfo as NamecheapDomainInfo,
29
+ type DnsRecord as NamecheapDnsRecord,
30
+ } from "../../../../../open-connectors/connectors/connect-namecheap/src/index.js";
31
+
32
+ // ============================================================
33
+ // Sync Result Type (microservice-specific, not in connector)
34
+ // ============================================================
35
+
36
+ export interface NamecheapSyncResult {
37
+ synced: number;
38
+ errors: string[];
39
+ domains: string[];
40
+ }
41
+
42
+ // ============================================================
43
+ // Connector Instance Management
44
+ // ============================================================
45
+
46
+ /**
47
+ * Create a Namecheap connector from environment variables.
48
+ * Wraps Connector.fromEnv() with the same env var validation.
49
+ */
50
+ export function getConfig(): ConnectorConfig {
51
+ const apiKey = process.env["NAMECHEAP_API_KEY"];
52
+ const username = process.env["NAMECHEAP_USERNAME"];
53
+ const clientIp = process.env["NAMECHEAP_CLIENT_IP"];
54
+
55
+ if (!apiKey) throw new Error("NAMECHEAP_API_KEY environment variable is not set");
56
+ if (!username) throw new Error("NAMECHEAP_USERNAME environment variable is not set");
57
+ if (!clientIp) throw new Error("NAMECHEAP_CLIENT_IP environment variable is not set");
58
+
59
+ return {
60
+ apiKey,
61
+ username,
62
+ clientIp,
63
+ sandbox: process.env["NAMECHEAP_SANDBOX"] === "true",
64
+ };
65
+ }
66
+
67
+ function createConnector(config?: ConnectorConfig): Connector {
68
+ return new Connector(config || getConfig());
69
+ }
70
+
71
+ // ============================================================
72
+ // API Functions (thin wrappers around connector)
73
+ // ============================================================
74
+
75
+ /**
76
+ * List all domains in the Namecheap account
77
+ */
78
+ export async function listNamecheapDomains(config?: ConnectorConfig): Promise<NamecheapDomain[]> {
79
+ const connector = createConnector(config);
80
+ return connector.domains.list();
81
+ }
82
+
83
+ /**
84
+ * Get detailed info for a specific domain
85
+ */
86
+ export async function getDomainInfo(domain: string, config?: ConnectorConfig): Promise<NamecheapDomainInfo> {
87
+ const connector = createConnector(config);
88
+ return connector.domains.getInfo(domain);
89
+ }
90
+
91
+ /**
92
+ * Renew a domain
93
+ */
94
+ export async function renewDomain(domain: string, years: number = 1, config?: ConnectorConfig) {
95
+ const connector = createConnector(config);
96
+ return connector.domains.renew(domain, years);
97
+ }
98
+
99
+ /**
100
+ * Get DNS host records for a domain
101
+ */
102
+ export async function getDnsRecords(domain: string, sld: string, tld: string, config?: ConnectorConfig): Promise<NamecheapDnsRecord[]> {
103
+ const connector = createConnector(config);
104
+ return connector.dns.getHosts(sld, tld);
105
+ }
106
+
107
+ /**
108
+ * Set DNS host records for a domain
109
+ */
110
+ export async function setDnsRecords(
111
+ domain: string,
112
+ sld: string,
113
+ tld: string,
114
+ records: NamecheapDnsRecord[],
115
+ config?: ConnectorConfig
116
+ ): Promise<boolean> {
117
+ const connector = createConnector(config);
118
+ return connector.dns.setHosts(sld, tld, records);
119
+ }
120
+
121
+ /**
122
+ * Check domain availability
123
+ */
124
+ export async function checkAvailability(domain: string, config?: ConnectorConfig) {
125
+ const connector = createConnector(config);
126
+ return connector.domains.check(domain);
127
+ }
128
+
129
+ // ============================================================
130
+ // Domain Helpers (microservice-specific)
131
+ // ============================================================
132
+
133
+ /**
134
+ * Split a domain name into SLD (second-level domain) and TLD (top-level domain)
135
+ */
136
+ export function splitDomain(domain: string): { sld: string; tld: string } {
137
+ const parts = domain.split(".");
138
+ if (parts.length < 2) {
139
+ throw new Error(`Invalid domain: ${domain}`);
140
+ }
141
+ // Handle multi-part TLDs like .co.uk
142
+ if (parts.length >= 3 && ["co", "com", "org", "net", "ac", "gov"].includes(parts[parts.length - 2])) {
143
+ return {
144
+ sld: parts.slice(0, -2).join("."),
145
+ tld: parts.slice(-2).join("."),
146
+ };
147
+ }
148
+ return {
149
+ sld: parts.slice(0, -1).join("."),
150
+ tld: parts[parts.length - 1],
151
+ };
152
+ }
153
+
154
+ // ============================================================
155
+ // Sync to Local DB (microservice business logic)
156
+ // ============================================================
157
+
158
+ /**
159
+ * Sync domains from Namecheap to local database
160
+ * Calls listDomains + getDomainInfo for each, upserts into local domains table
161
+ */
162
+ export async function syncToLocalDb(dbFunctions: {
163
+ getDomainByName: (name: string) => { id: string } | null;
164
+ createDomain: (input: {
165
+ name: string;
166
+ registrar?: string;
167
+ status?: string;
168
+ registered_at?: string;
169
+ expires_at?: string;
170
+ auto_renew?: boolean;
171
+ nameservers?: string[];
172
+ }) => { id: string; name: string };
173
+ updateDomain: (
174
+ id: string,
175
+ input: {
176
+ registrar?: string;
177
+ status?: string;
178
+ registered_at?: string;
179
+ expires_at?: string;
180
+ auto_renew?: boolean;
181
+ nameservers?: string[];
182
+ }
183
+ ) => unknown;
184
+ }, config?: ConnectorConfig): Promise<NamecheapSyncResult> {
185
+ const connector = createConnector(config);
186
+ const result: NamecheapSyncResult = { synced: 0, errors: [], domains: [] };
187
+
188
+ let ncDomains: NamecheapDomain[];
189
+ try {
190
+ ncDomains = await connector.domains.list();
191
+ } catch (error) {
192
+ throw new Error(`Failed to list Namecheap domains: ${error instanceof Error ? error.message : String(error)}`);
193
+ }
194
+
195
+ for (const ncDomain of ncDomains) {
196
+ try {
197
+ // Get detailed info via connector
198
+ let info: NamecheapDomainInfo;
199
+ try {
200
+ info = await connector.domains.getInfo(ncDomain.domain);
201
+ } catch {
202
+ // Fall back to basic info if getInfo fails
203
+ info = {
204
+ domain: ncDomain.domain,
205
+ registrar: "Namecheap",
206
+ created: "",
207
+ expires: ncDomain.expiry,
208
+ nameservers: [],
209
+ };
210
+ }
211
+
212
+ // Normalize dates to ISO format
213
+ const expiresAt = normalizeDate(info.expires || ncDomain.expiry);
214
+ const createdAt = normalizeDate(info.created);
215
+
216
+ // Upsert into local DB
217
+ const existing = dbFunctions.getDomainByName(ncDomain.domain);
218
+ if (existing) {
219
+ dbFunctions.updateDomain(existing.id, {
220
+ registrar: "Namecheap",
221
+ status: "active",
222
+ registered_at: createdAt || undefined,
223
+ expires_at: expiresAt || undefined,
224
+ auto_renew: ncDomain.autoRenew,
225
+ nameservers: info.nameservers.length > 0 ? info.nameservers : undefined,
226
+ });
227
+ } else {
228
+ dbFunctions.createDomain({
229
+ name: ncDomain.domain,
230
+ registrar: "Namecheap",
231
+ status: "active",
232
+ registered_at: createdAt || undefined,
233
+ expires_at: expiresAt || undefined,
234
+ auto_renew: ncDomain.autoRenew,
235
+ nameservers: info.nameservers,
236
+ });
237
+ }
238
+
239
+ result.synced++;
240
+ result.domains.push(ncDomain.domain);
241
+ } catch (error) {
242
+ result.errors.push(`${ncDomain.domain}: ${error instanceof Error ? error.message : String(error)}`);
243
+ }
244
+ }
245
+
246
+ return result;
247
+ }
248
+
249
+ // ============================================================
250
+ // Helpers
251
+ // ============================================================
252
+
253
+ function normalizeDate(dateStr: string): string | null {
254
+ if (!dateStr) return null;
255
+ try {
256
+ const d = new Date(dateStr);
257
+ if (isNaN(d.getTime())) return null;
258
+ return d.toISOString();
259
+ } catch {
260
+ return null;
261
+ }
262
+ }
@@ -0,0 +1,355 @@
1
+ /**
2
+ * Unified registrar provider system
3
+ *
4
+ * Wraps Namecheap, GoDaddy, and Brandsight into a common RegistrarProvider interface.
5
+ */
6
+
7
+ import type { Domain, CreateDomainInput, UpdateDomainInput } from "../db/domains.js";
8
+ import * as namecheap from "./namecheap.js";
9
+ import * as godaddy from "./godaddy.js";
10
+
11
+ // ============================================================
12
+ // Types
13
+ // ============================================================
14
+
15
+ export interface ProviderDnsRecord {
16
+ type: string;
17
+ name: string;
18
+ value: string;
19
+ ttl: number;
20
+ priority?: number;
21
+ }
22
+
23
+ export interface ProviderDomainInfo {
24
+ domain: string;
25
+ registrar: string;
26
+ created: string;
27
+ expires: string;
28
+ nameservers: string[];
29
+ status: string;
30
+ auto_renew: boolean;
31
+ }
32
+
33
+ export interface ProviderRenewResult {
34
+ domain: string;
35
+ success: boolean;
36
+ orderId?: string;
37
+ chargedAmount?: string;
38
+ }
39
+
40
+ export interface ProviderSyncResult {
41
+ synced: number;
42
+ created: number;
43
+ updated: number;
44
+ errors: string[];
45
+ }
46
+
47
+ export interface ProviderAvailability {
48
+ domain: string;
49
+ available: boolean;
50
+ }
51
+
52
+ export type DbFunctions = {
53
+ getDomainByName: (name: string) => Domain | null;
54
+ createDomain: (input: CreateDomainInput) => Domain;
55
+ updateDomain: (id: string, input: UpdateDomainInput) => Domain | null;
56
+ };
57
+
58
+ export interface RegistrarProvider {
59
+ name: string;
60
+ listDomains(): Promise<ProviderDomainInfo[]>;
61
+ getDomainInfo(domain: string): Promise<ProviderDomainInfo>;
62
+ renewDomain(domain: string): Promise<ProviderRenewResult>;
63
+ getDnsRecords(domain: string): Promise<ProviderDnsRecord[]>;
64
+ setDnsRecords(domain: string, records: ProviderDnsRecord[]): Promise<boolean>;
65
+ checkAvailability(domain: string): Promise<ProviderAvailability>;
66
+ syncToLocalDb(dbFns: DbFunctions): Promise<ProviderSyncResult>;
67
+ }
68
+
69
+ export interface ProviderInfo {
70
+ name: string;
71
+ configured: boolean;
72
+ envVars: string[];
73
+ }
74
+
75
+ export interface SyncAllResult {
76
+ providers: { name: string; result: ProviderSyncResult }[];
77
+ totalSynced: number;
78
+ totalErrors: string[];
79
+ }
80
+
81
+ // ============================================================
82
+ // Namecheap Provider Adapter
83
+ // ============================================================
84
+
85
+ function createNamecheapProvider(): RegistrarProvider {
86
+ return {
87
+ name: "namecheap",
88
+
89
+ async listDomains(): Promise<ProviderDomainInfo[]> {
90
+ const config = namecheap.getConfig();
91
+ const domains = await namecheap.listNamecheapDomains(config);
92
+ return domains.map((d) => ({
93
+ domain: d.domain,
94
+ registrar: "Namecheap",
95
+ created: "",
96
+ expires: d.expiry,
97
+ nameservers: [],
98
+ status: "active",
99
+ auto_renew: d.autoRenew,
100
+ }));
101
+ },
102
+
103
+ async getDomainInfo(domain: string): Promise<ProviderDomainInfo> {
104
+ const config = namecheap.getConfig();
105
+ const info = await namecheap.getDomainInfo(domain, config);
106
+ return {
107
+ domain: info.domain,
108
+ registrar: info.registrar,
109
+ created: info.created,
110
+ expires: info.expires,
111
+ nameservers: info.nameservers,
112
+ status: "active",
113
+ auto_renew: true,
114
+ };
115
+ },
116
+
117
+ async renewDomain(domain: string): Promise<ProviderRenewResult> {
118
+ const config = namecheap.getConfig();
119
+ const result = await namecheap.renewDomain(domain, 1, config);
120
+ return {
121
+ domain: result.domain,
122
+ success: result.success,
123
+ orderId: result.orderId,
124
+ chargedAmount: result.chargedAmount,
125
+ };
126
+ },
127
+
128
+ async getDnsRecords(domain: string): Promise<ProviderDnsRecord[]> {
129
+ const config = namecheap.getConfig();
130
+ const { sld, tld } = namecheap.splitDomain(domain);
131
+ const records = await namecheap.getDnsRecords(domain, sld, tld, config);
132
+ return records.map((r) => ({
133
+ type: r.type,
134
+ name: r.name,
135
+ value: r.address,
136
+ ttl: r.ttl,
137
+ priority: r.mxPref,
138
+ }));
139
+ },
140
+
141
+ async setDnsRecords(domain: string, records: ProviderDnsRecord[]): Promise<boolean> {
142
+ const config = namecheap.getConfig();
143
+ const { sld, tld } = namecheap.splitDomain(domain);
144
+ const ncRecords = records.map((r) => ({
145
+ type: r.type,
146
+ name: r.name,
147
+ address: r.value,
148
+ ttl: r.ttl,
149
+ mxPref: r.priority,
150
+ }));
151
+ return namecheap.setDnsRecords(domain, sld, tld, ncRecords, config);
152
+ },
153
+
154
+ async checkAvailability(domain: string): Promise<ProviderAvailability> {
155
+ const config = namecheap.getConfig();
156
+ const result = await namecheap.checkAvailability(domain, config);
157
+ return { domain: result.domain, available: result.available };
158
+ },
159
+
160
+ async syncToLocalDb(dbFns: DbFunctions): Promise<ProviderSyncResult> {
161
+ const result = await namecheap.syncToLocalDb(dbFns);
162
+ return {
163
+ synced: result.synced,
164
+ created: 0,
165
+ updated: 0,
166
+ errors: result.errors,
167
+ };
168
+ },
169
+ };
170
+ }
171
+
172
+ // ============================================================
173
+ // GoDaddy Provider Adapter
174
+ // ============================================================
175
+
176
+ function createGoDaddyProvider(): RegistrarProvider {
177
+ return {
178
+ name: "godaddy",
179
+
180
+ async listDomains(): Promise<ProviderDomainInfo[]> {
181
+ const domains = await godaddy.listGoDaddyDomains();
182
+ return domains.map((d) => ({
183
+ domain: d.domain,
184
+ registrar: "GoDaddy",
185
+ created: "",
186
+ expires: d.expires,
187
+ nameservers: d.nameServers || [],
188
+ status: d.status.toLowerCase(),
189
+ auto_renew: d.renewAuto,
190
+ }));
191
+ },
192
+
193
+ async getDomainInfo(domain: string): Promise<ProviderDomainInfo> {
194
+ const detail = await godaddy.getDomainInfo(domain);
195
+ return {
196
+ domain: detail.domain,
197
+ registrar: "GoDaddy",
198
+ created: detail.createdAt || "",
199
+ expires: detail.expires,
200
+ nameservers: detail.nameServers || [],
201
+ status: detail.status.toLowerCase(),
202
+ auto_renew: detail.renewAuto,
203
+ };
204
+ },
205
+
206
+ async renewDomain(domain: string): Promise<ProviderRenewResult> {
207
+ const result = await godaddy.renewDomain(domain);
208
+ return {
209
+ domain,
210
+ success: true,
211
+ orderId: String(result.orderId),
212
+ chargedAmount: String(result.total),
213
+ };
214
+ },
215
+
216
+ async getDnsRecords(domain: string): Promise<ProviderDnsRecord[]> {
217
+ const records = await godaddy.getDnsRecords(domain);
218
+ return records.map((r) => ({
219
+ type: r.type,
220
+ name: r.name,
221
+ value: r.data,
222
+ ttl: r.ttl,
223
+ priority: r.priority,
224
+ }));
225
+ },
226
+
227
+ async setDnsRecords(domain: string, records: ProviderDnsRecord[]): Promise<boolean> {
228
+ const gdRecords = records.map((r) => ({
229
+ type: r.type,
230
+ name: r.name,
231
+ data: r.value,
232
+ ttl: r.ttl,
233
+ priority: r.priority,
234
+ }));
235
+ await godaddy.setDnsRecords(domain, gdRecords);
236
+ return true;
237
+ },
238
+
239
+ async checkAvailability(domain: string): Promise<ProviderAvailability> {
240
+ const result = await godaddy.checkAvailability(domain);
241
+ return { domain: result.domain, available: result.available };
242
+ },
243
+
244
+ async syncToLocalDb(dbFns: DbFunctions): Promise<ProviderSyncResult> {
245
+ const result = await godaddy.syncToLocalDb(dbFns);
246
+ return {
247
+ synced: result.synced,
248
+ created: result.created,
249
+ updated: result.updated,
250
+ errors: result.errors,
251
+ };
252
+ },
253
+ };
254
+ }
255
+
256
+ // ============================================================
257
+ // Provider Factory
258
+ // ============================================================
259
+
260
+ /**
261
+ * Get a unified RegistrarProvider by name.
262
+ */
263
+ export function getProvider(name: "namecheap" | "godaddy"): RegistrarProvider {
264
+ switch (name) {
265
+ case "namecheap":
266
+ return createNamecheapProvider();
267
+ case "godaddy":
268
+ return createGoDaddyProvider();
269
+ default:
270
+ throw new Error(`Unknown registrar provider: ${name}`);
271
+ }
272
+ }
273
+
274
+ /**
275
+ * Check which providers have their API keys configured.
276
+ */
277
+ export function getAvailableProviders(): ProviderInfo[] {
278
+ const providers: ProviderInfo[] = [
279
+ {
280
+ name: "namecheap",
281
+ configured: !!(
282
+ process.env["NAMECHEAP_API_KEY"] &&
283
+ process.env["NAMECHEAP_USERNAME"] &&
284
+ process.env["NAMECHEAP_CLIENT_IP"]
285
+ ),
286
+ envVars: ["NAMECHEAP_API_KEY", "NAMECHEAP_USERNAME", "NAMECHEAP_CLIENT_IP"],
287
+ },
288
+ {
289
+ name: "godaddy",
290
+ configured: !!(
291
+ process.env["GODADDY_API_KEY"] &&
292
+ process.env["GODADDY_API_SECRET"]
293
+ ),
294
+ envVars: ["GODADDY_API_KEY", "GODADDY_API_SECRET"],
295
+ },
296
+ {
297
+ name: "brandsight",
298
+ configured: !!process.env["BRANDSIGHT_API_KEY"],
299
+ envVars: ["BRANDSIGHT_API_KEY"],
300
+ },
301
+ ];
302
+
303
+ return providers;
304
+ }
305
+
306
+ /**
307
+ * Sync domains from ALL configured registrar providers sequentially.
308
+ */
309
+ export async function syncAll(dbFns: DbFunctions): Promise<SyncAllResult> {
310
+ const available = getAvailableProviders().filter(
311
+ (p) => p.configured && (p.name === "namecheap" || p.name === "godaddy")
312
+ );
313
+
314
+ const result: SyncAllResult = {
315
+ providers: [],
316
+ totalSynced: 0,
317
+ totalErrors: [],
318
+ };
319
+
320
+ for (const info of available) {
321
+ try {
322
+ const provider = getProvider(info.name as "namecheap" | "godaddy");
323
+ const syncResult = await provider.syncToLocalDb(dbFns);
324
+ result.providers.push({ name: info.name, result: syncResult });
325
+ result.totalSynced += syncResult.synced;
326
+ result.totalErrors.push(...syncResult.errors.map((e) => `[${info.name}] ${e}`));
327
+ } catch (error) {
328
+ const msg = `[${info.name}] Sync failed: ${error instanceof Error ? error.message : String(error)}`;
329
+ result.totalErrors.push(msg);
330
+ result.providers.push({
331
+ name: info.name,
332
+ result: { synced: 0, created: 0, updated: 0, errors: [msg] },
333
+ });
334
+ }
335
+ }
336
+
337
+ return result;
338
+ }
339
+
340
+ /**
341
+ * Auto-detect which registrar provider a domain uses based on its DB record.
342
+ * Returns the provider name or null if not determinable.
343
+ */
344
+ export function autoDetectRegistrar(
345
+ domain: string,
346
+ getDomainByName: (name: string) => Domain | null
347
+ ): "namecheap" | "godaddy" | null {
348
+ const dbDomain = getDomainByName(domain);
349
+ if (!dbDomain || !dbDomain.registrar) return null;
350
+
351
+ const registrar = dbDomain.registrar.toLowerCase();
352
+ if (registrar.includes("namecheap")) return "namecheap";
353
+ if (registrar.includes("godaddy")) return "godaddy";
354
+ return null;
355
+ }