@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.
@@ -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
+ }
@@ -31,7 +31,26 @@ import {
31
31
  validateDns,
32
32
  exportPortfolio,
33
33
  checkAllDomains,
34
+ getDomainByName,
34
35
  } from "../db/domains.js";
36
+ import {
37
+ syncToLocalDb,
38
+ renewDomain as namecheapRenew,
39
+ checkAvailability as namecheapCheck,
40
+ } from "../lib/namecheap.js";
41
+ import {
42
+ syncToLocalDb as godaddySyncToLocalDb,
43
+ renewDomain as godaddyRenewDomain,
44
+ } from "../lib/godaddy.js";
45
+ import {
46
+ getAvailableProviders,
47
+ syncAll,
48
+ } from "../lib/registrar.js";
49
+ import {
50
+ monitorBrand,
51
+ getSimilarDomains,
52
+ getThreatAssessment,
53
+ } from "../lib/brandsight.js";
35
54
 
36
55
  const server = new McpServer({
37
56
  name: "microservice-domains",
@@ -523,6 +542,232 @@ server.registerTool(
523
542
  }
524
543
  );
525
544
 
545
+ // --- Namecheap Integration ---
546
+
547
+ server.registerTool(
548
+ "sync_namecheap",
549
+ {
550
+ title: "Sync Namecheap Domains",
551
+ description: "Sync all domains from Namecheap account to local database. Requires NAMECHEAP_API_KEY, NAMECHEAP_USERNAME, and NAMECHEAP_CLIENT_IP env vars.",
552
+ inputSchema: {},
553
+ },
554
+ async () => {
555
+ try {
556
+ const result = await syncToLocalDb({
557
+ getDomainByName,
558
+ createDomain,
559
+ updateDomain,
560
+ });
561
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
562
+ } catch (error: unknown) {
563
+ return {
564
+ content: [{ type: "text", text: `Sync failed: ${error instanceof Error ? error.message : String(error)}` }],
565
+ isError: true,
566
+ };
567
+ }
568
+ }
569
+ );
570
+
571
+ server.registerTool(
572
+ "renew_via_namecheap",
573
+ {
574
+ title: "Renew Domain via Namecheap",
575
+ description: "Renew a domain through the Namecheap API.",
576
+ inputSchema: {
577
+ domain: z.string().describe("Domain name to renew (e.g. example.com)"),
578
+ years: z.number().default(1).describe("Number of years to renew"),
579
+ },
580
+ },
581
+ async ({ domain, years }) => {
582
+ try {
583
+ const result = await namecheapRenew(domain, years);
584
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
585
+ } catch (error: unknown) {
586
+ return {
587
+ content: [{ type: "text", text: `Renewal failed: ${error instanceof Error ? error.message : String(error)}` }],
588
+ isError: true,
589
+ };
590
+ }
591
+ }
592
+ );
593
+
594
+ server.registerTool(
595
+ "check_availability_namecheap",
596
+ {
597
+ title: "Check Domain Availability",
598
+ description: "Check if a domain name is available for registration via Namecheap.",
599
+ inputSchema: {
600
+ domain: z.string().describe("Domain name to check (e.g. example.com)"),
601
+ },
602
+ },
603
+ async ({ domain }) => {
604
+ try {
605
+ const result = await namecheapCheck(domain);
606
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
607
+ } catch (error: unknown) {
608
+ return {
609
+ content: [{ type: "text", text: `Check failed: ${error instanceof Error ? error.message : String(error)}` }],
610
+ isError: true,
611
+ };
612
+ }
613
+ }
614
+ );
615
+
616
+ // --- GoDaddy Integration ---
617
+
618
+ server.registerTool(
619
+ "sync_godaddy",
620
+ {
621
+ title: "Sync GoDaddy Domains",
622
+ description: "Sync all domains from GoDaddy account to local database. Requires GODADDY_API_KEY and GODADDY_API_SECRET env vars.",
623
+ inputSchema: {},
624
+ },
625
+ async () => {
626
+ try {
627
+ const result = await godaddySyncToLocalDb({
628
+ getDomainByName,
629
+ createDomain,
630
+ updateDomain,
631
+ });
632
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
633
+ } catch (error: unknown) {
634
+ return {
635
+ content: [{ type: "text", text: `Sync failed: ${error instanceof Error ? error.message : String(error)}` }],
636
+ isError: true,
637
+ };
638
+ }
639
+ }
640
+ );
641
+
642
+ server.registerTool(
643
+ "renew_via_godaddy",
644
+ {
645
+ title: "Renew Domain via GoDaddy",
646
+ description: "Renew a domain through the GoDaddy API for 1 year.",
647
+ inputSchema: {
648
+ domain: z.string().describe("Domain name to renew (e.g. example.com)"),
649
+ },
650
+ },
651
+ async ({ domain }) => {
652
+ try {
653
+ const result = await godaddyRenewDomain(domain);
654
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
655
+ } catch (error: unknown) {
656
+ return {
657
+ content: [{ type: "text", text: `Renewal failed: ${error instanceof Error ? error.message : String(error)}` }],
658
+ isError: true,
659
+ };
660
+ }
661
+ }
662
+ );
663
+
664
+ // --- Unified Provider Tools ---
665
+
666
+ server.registerTool(
667
+ "sync_all_providers",
668
+ {
669
+ title: "Sync All Providers",
670
+ description: "Sync domains from all configured registrar providers (Namecheap, GoDaddy) to local database.",
671
+ inputSchema: {},
672
+ },
673
+ async () => {
674
+ try {
675
+ const result = await syncAll({
676
+ getDomainByName,
677
+ createDomain,
678
+ updateDomain,
679
+ });
680
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
681
+ } catch (error: unknown) {
682
+ return {
683
+ content: [{ type: "text", text: `Sync failed: ${error instanceof Error ? error.message : String(error)}` }],
684
+ isError: true,
685
+ };
686
+ }
687
+ }
688
+ );
689
+
690
+ server.registerTool(
691
+ "list_providers",
692
+ {
693
+ title: "List Providers",
694
+ description: "Show which registrar providers are configured (have API keys set).",
695
+ inputSchema: {},
696
+ },
697
+ async () => {
698
+ const providers = getAvailableProviders();
699
+ return { content: [{ type: "text", text: JSON.stringify(providers, null, 2) }] };
700
+ }
701
+ );
702
+
703
+ // --- Brandsight Tools ---
704
+
705
+ server.registerTool(
706
+ "monitor_brand",
707
+ {
708
+ title: "Monitor Brand",
709
+ description: "Monitor a brand name for new domain registrations that are similar (typosquats, homoglyphs, keyword matches). Uses Brandsight API.",
710
+ inputSchema: {
711
+ brand: z.string().describe("Brand name to monitor"),
712
+ },
713
+ },
714
+ async ({ brand }) => {
715
+ try {
716
+ const result = await monitorBrand(brand);
717
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
718
+ } catch (error: unknown) {
719
+ return {
720
+ content: [{ type: "text", text: `Monitor failed: ${error instanceof Error ? error.message : String(error)}` }],
721
+ isError: true,
722
+ };
723
+ }
724
+ }
725
+ );
726
+
727
+ server.registerTool(
728
+ "similar_domains",
729
+ {
730
+ title: "Similar Domains",
731
+ description: "Find typosquat/competing domains similar to the given domain. Uses Brandsight API.",
732
+ inputSchema: {
733
+ domain: z.string().describe("Domain to find similar domains for"),
734
+ },
735
+ },
736
+ async ({ domain }) => {
737
+ try {
738
+ const result = await getSimilarDomains(domain);
739
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
740
+ } catch (error: unknown) {
741
+ return {
742
+ content: [{ type: "text", text: `Similar domains check failed: ${error instanceof Error ? error.message : String(error)}` }],
743
+ isError: true,
744
+ };
745
+ }
746
+ }
747
+ );
748
+
749
+ server.registerTool(
750
+ "domain_threats",
751
+ {
752
+ title: "Domain Threats",
753
+ description: "Get a threat assessment for a domain including risk level, threats, and recommendation. Uses Brandsight API.",
754
+ inputSchema: {
755
+ domain: z.string().describe("Domain to assess threats for"),
756
+ },
757
+ },
758
+ async ({ domain }) => {
759
+ try {
760
+ const result = await getThreatAssessment(domain);
761
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
762
+ } catch (error: unknown) {
763
+ return {
764
+ content: [{ type: "text", text: `Threat assessment failed: ${error instanceof Error ? error.message : String(error)}` }],
765
+ isError: true,
766
+ };
767
+ }
768
+ }
769
+ );
770
+
526
771
  // --- Start ---
527
772
  async function main() {
528
773
  const transport = new StdioServerTransport();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/microservices",
3
- "version": "0.0.5",
3
+ "version": "0.0.6",
4
4
  "description": "Mini business apps for AI agents - invoices, contacts, bookkeeping and more, each with its own SQLite database",
5
5
  "type": "module",
6
6
  "bin": {