@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.
- 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 +350 -0
- package/microservices/microservice-domains/src/lib/godaddy.ts +338 -0
- package/microservices/microservice-domains/src/lib/namecheap.ts +262 -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,350 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Brandsight API integration for brand monitoring and threat detection
|
|
3
|
+
*
|
|
4
|
+
* Uses the Connector SDK pattern. When a connect-brandsight package
|
|
5
|
+
* is published, replace the inline connector with the package import.
|
|
6
|
+
*
|
|
7
|
+
* Requires environment variable:
|
|
8
|
+
* BRANDSIGHT_API_KEY — API key for Brandsight
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
// ============================================================
|
|
12
|
+
// Types
|
|
13
|
+
// ============================================================
|
|
14
|
+
|
|
15
|
+
export interface BrandsightConfig {
|
|
16
|
+
apiKey: string;
|
|
17
|
+
baseUrl?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface BrandsightAlert {
|
|
21
|
+
domain: string;
|
|
22
|
+
type: "typosquat" | "homoglyph" | "keyword" | "tld_variation";
|
|
23
|
+
registered_at: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface BrandMonitorResult {
|
|
27
|
+
brand: string;
|
|
28
|
+
alerts: BrandsightAlert[];
|
|
29
|
+
stub: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface WhoisHistoryEntry {
|
|
33
|
+
registrant: string;
|
|
34
|
+
date: string;
|
|
35
|
+
changes: string[];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface WhoisHistoryResult {
|
|
39
|
+
domain: string;
|
|
40
|
+
history: WhoisHistoryEntry[];
|
|
41
|
+
stub: boolean;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface ThreatAssessment {
|
|
45
|
+
domain: string;
|
|
46
|
+
risk_level: "low" | "medium" | "high" | "critical";
|
|
47
|
+
threats: string[];
|
|
48
|
+
recommendation: string;
|
|
49
|
+
stub: boolean;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export class BrandsightApiError extends Error {
|
|
53
|
+
constructor(
|
|
54
|
+
message: string,
|
|
55
|
+
public statusCode?: number,
|
|
56
|
+
public responseBody?: string
|
|
57
|
+
) {
|
|
58
|
+
super(message);
|
|
59
|
+
this.name = "BrandsightApiError";
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ============================================================
|
|
64
|
+
// Brandsight Connector Client (inline SDK pattern)
|
|
65
|
+
// ============================================================
|
|
66
|
+
|
|
67
|
+
class BrandsightClient {
|
|
68
|
+
private readonly apiKey: string;
|
|
69
|
+
private readonly baseUrl: string;
|
|
70
|
+
private fetchFn: typeof globalThis.fetch;
|
|
71
|
+
|
|
72
|
+
constructor(config: BrandsightConfig, fetchFn?: typeof globalThis.fetch) {
|
|
73
|
+
this.apiKey = config.apiKey;
|
|
74
|
+
this.baseUrl = config.baseUrl || "https://api.brandsight.com/v1";
|
|
75
|
+
this.fetchFn = fetchFn || globalThis.fetch;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
setFetch(fn: typeof globalThis.fetch): void {
|
|
79
|
+
this.fetchFn = fn;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async get<T>(path: string): Promise<{ data: T; stub: false } | { data: null; stub: true }> {
|
|
83
|
+
const url = `${this.baseUrl}${path}`;
|
|
84
|
+
const headers: Record<string, string> = {
|
|
85
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
86
|
+
"Content-Type": "application/json",
|
|
87
|
+
Accept: "application/json",
|
|
88
|
+
"User-Agent": "microservice-domains/0.0.1",
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
const response = await this.fetchFn(url, {
|
|
93
|
+
method: "GET",
|
|
94
|
+
headers,
|
|
95
|
+
signal: AbortSignal.timeout(15000),
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
if (!response.ok) {
|
|
99
|
+
throw new BrandsightApiError(
|
|
100
|
+
`Brandsight API GET ${path} failed with status ${response.status}`,
|
|
101
|
+
response.status,
|
|
102
|
+
await response.text()
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const data = (await response.json()) as T;
|
|
107
|
+
return { data, stub: false };
|
|
108
|
+
} catch (error) {
|
|
109
|
+
if (error instanceof BrandsightApiError) throw error;
|
|
110
|
+
// API unreachable — return stub indicator
|
|
111
|
+
return { data: null, stub: true };
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ============================================================
|
|
117
|
+
// Brandsight Connector (SDK pattern)
|
|
118
|
+
// ============================================================
|
|
119
|
+
|
|
120
|
+
class BrandsightConnector {
|
|
121
|
+
private readonly client: BrandsightClient;
|
|
122
|
+
|
|
123
|
+
constructor(config: BrandsightConfig, fetchFn?: typeof globalThis.fetch) {
|
|
124
|
+
this.client = new BrandsightClient(config, fetchFn);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
static fromEnv(fetchFn?: typeof globalThis.fetch): BrandsightConnector {
|
|
128
|
+
const apiKey = process.env["BRANDSIGHT_API_KEY"];
|
|
129
|
+
if (!apiKey) {
|
|
130
|
+
throw new BrandsightApiError(
|
|
131
|
+
"BRANDSIGHT_API_KEY environment variable is not set"
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
return new BrandsightConnector({ apiKey }, fetchFn);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
setFetch(fn: typeof globalThis.fetch): void {
|
|
138
|
+
this.client.setFetch(fn);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async monitorBrand(brandName: string): Promise<{ alerts: BrandsightAlert[] } | null> {
|
|
142
|
+
const result = await this.client.get<{ alerts: BrandsightAlert[] }>(
|
|
143
|
+
`/brands/${encodeURIComponent(brandName)}/monitor`
|
|
144
|
+
);
|
|
145
|
+
return result.stub ? null : result.data;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async getSimilarDomains(domain: string): Promise<{ similar: string[] } | null> {
|
|
149
|
+
const result = await this.client.get<{ similar: string[] }>(
|
|
150
|
+
`/domains/${encodeURIComponent(domain)}/similar`
|
|
151
|
+
);
|
|
152
|
+
return result.stub ? null : result.data;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async getWhoisHistory(domain: string): Promise<{ history: WhoisHistoryEntry[] } | null> {
|
|
156
|
+
const result = await this.client.get<{ history: WhoisHistoryEntry[] }>(
|
|
157
|
+
`/domains/${encodeURIComponent(domain)}/whois-history`
|
|
158
|
+
);
|
|
159
|
+
return result.stub ? null : result.data;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async getThreatAssessment(domain: string): Promise<Omit<ThreatAssessment, "stub"> | null> {
|
|
163
|
+
const result = await this.client.get<Omit<ThreatAssessment, "stub">>(
|
|
164
|
+
`/domains/${encodeURIComponent(domain)}/threats`
|
|
165
|
+
);
|
|
166
|
+
return result.stub ? null : result.data;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// ============================================================
|
|
171
|
+
// Module-level state (for test injection)
|
|
172
|
+
// ============================================================
|
|
173
|
+
|
|
174
|
+
type FetchFn = typeof globalThis.fetch;
|
|
175
|
+
|
|
176
|
+
let _fetchFn: FetchFn | null = null;
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Override the fetch implementation (for testing).
|
|
180
|
+
* Pass `null` to restore the default.
|
|
181
|
+
*/
|
|
182
|
+
export function _setFetch(fn: FetchFn | null): void {
|
|
183
|
+
_fetchFn = fn;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export function getApiKey(): string {
|
|
187
|
+
const key = process.env["BRANDSIGHT_API_KEY"];
|
|
188
|
+
if (!key) {
|
|
189
|
+
throw new BrandsightApiError(
|
|
190
|
+
"BRANDSIGHT_API_KEY environment variable is not set"
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
return key;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function createConnector(): BrandsightConnector {
|
|
197
|
+
const connector = BrandsightConnector.fromEnv(_fetchFn || undefined);
|
|
198
|
+
return connector;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// ============================================================
|
|
202
|
+
// Stub Data Generators
|
|
203
|
+
// ============================================================
|
|
204
|
+
|
|
205
|
+
function generateStubAlerts(brandName: string): BrandsightAlert[] {
|
|
206
|
+
const now = new Date().toISOString();
|
|
207
|
+
return [
|
|
208
|
+
{
|
|
209
|
+
domain: `${brandName}-deals.com`,
|
|
210
|
+
type: "keyword",
|
|
211
|
+
registered_at: now,
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
domain: `${brandName.replace(/a/gi, "4").replace(/e/gi, "3")}.com`,
|
|
215
|
+
type: "homoglyph",
|
|
216
|
+
registered_at: now,
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
domain: `${brandName}s.com`,
|
|
220
|
+
type: "typosquat",
|
|
221
|
+
registered_at: now,
|
|
222
|
+
},
|
|
223
|
+
];
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function generateStubSimilarDomains(domain: string): string[] {
|
|
227
|
+
const base = domain.replace(/\.[^.]+$/, "");
|
|
228
|
+
const tld = domain.slice(base.length);
|
|
229
|
+
return [
|
|
230
|
+
`${base}-online${tld}`,
|
|
231
|
+
`${base}s${tld}`,
|
|
232
|
+
`${base.replace(/a/gi, "4")}${tld}`,
|
|
233
|
+
`${base}-app${tld}`,
|
|
234
|
+
`get${base}${tld}`,
|
|
235
|
+
];
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function generateStubWhoisHistory(domain: string): WhoisHistoryEntry[] {
|
|
239
|
+
return [
|
|
240
|
+
{
|
|
241
|
+
registrant: "Privacy Proxy Service",
|
|
242
|
+
date: "2023-01-15T00:00:00Z",
|
|
243
|
+
changes: ["registrant_changed", "nameserver_changed"],
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
registrant: "Original Owner LLC",
|
|
247
|
+
date: "2020-06-01T00:00:00Z",
|
|
248
|
+
changes: ["initial_registration"],
|
|
249
|
+
},
|
|
250
|
+
];
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function generateStubThreatAssessment(domain: string): Omit<ThreatAssessment, "stub"> {
|
|
254
|
+
return {
|
|
255
|
+
domain,
|
|
256
|
+
risk_level: "low",
|
|
257
|
+
threats: [],
|
|
258
|
+
recommendation: "No immediate threats detected. Continue routine monitoring.",
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// ============================================================
|
|
263
|
+
// API Functions (use connector, fall back to stubs)
|
|
264
|
+
// ============================================================
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Monitor a brand name for new domain registrations that are similar.
|
|
268
|
+
*/
|
|
269
|
+
export async function monitorBrand(brandName: string): Promise<BrandMonitorResult> {
|
|
270
|
+
const connector = createConnector();
|
|
271
|
+
const data = await connector.monitorBrand(brandName);
|
|
272
|
+
|
|
273
|
+
if (data === null) {
|
|
274
|
+
return {
|
|
275
|
+
brand: brandName,
|
|
276
|
+
alerts: generateStubAlerts(brandName),
|
|
277
|
+
stub: true,
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return {
|
|
282
|
+
brand: brandName,
|
|
283
|
+
alerts: data.alerts,
|
|
284
|
+
stub: false,
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Find typosquat/competing domains similar to the given domain.
|
|
290
|
+
*/
|
|
291
|
+
export async function getSimilarDomains(domain: string): Promise<{ domain: string; similar: string[]; stub: boolean }> {
|
|
292
|
+
const connector = createConnector();
|
|
293
|
+
const data = await connector.getSimilarDomains(domain);
|
|
294
|
+
|
|
295
|
+
if (data === null) {
|
|
296
|
+
return {
|
|
297
|
+
domain,
|
|
298
|
+
similar: generateStubSimilarDomains(domain),
|
|
299
|
+
stub: true,
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
return {
|
|
304
|
+
domain,
|
|
305
|
+
similar: data.similar,
|
|
306
|
+
stub: false,
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Get historical WHOIS records for a domain.
|
|
312
|
+
*/
|
|
313
|
+
export async function getWhoisHistory(domain: string): Promise<WhoisHistoryResult> {
|
|
314
|
+
const connector = createConnector();
|
|
315
|
+
const data = await connector.getWhoisHistory(domain);
|
|
316
|
+
|
|
317
|
+
if (data === null) {
|
|
318
|
+
return {
|
|
319
|
+
domain,
|
|
320
|
+
history: generateStubWhoisHistory(domain),
|
|
321
|
+
stub: true,
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
return {
|
|
326
|
+
domain,
|
|
327
|
+
history: data.history,
|
|
328
|
+
stub: false,
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Get a threat assessment for a domain.
|
|
334
|
+
*/
|
|
335
|
+
export async function getThreatAssessment(domain: string): Promise<ThreatAssessment> {
|
|
336
|
+
const connector = createConnector();
|
|
337
|
+
const data = await connector.getThreatAssessment(domain);
|
|
338
|
+
|
|
339
|
+
if (data === null) {
|
|
340
|
+
return {
|
|
341
|
+
...generateStubThreatAssessment(domain),
|
|
342
|
+
stub: true,
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
return {
|
|
347
|
+
...data,
|
|
348
|
+
stub: false,
|
|
349
|
+
};
|
|
350
|
+
}
|
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GoDaddy API integration for microservice-domains
|
|
3
|
+
*
|
|
4
|
+
* Uses the Connector SDK pattern from connect-godaddy.
|
|
5
|
+
* When @hasna/connect-godaddy is published to npm, replace the
|
|
6
|
+
* relative connector import with the package import.
|
|
7
|
+
*
|
|
8
|
+
* Environment variables:
|
|
9
|
+
* GODADDY_API_KEY — GoDaddy API key
|
|
10
|
+
* GODADDY_API_SECRET — GoDaddy API secret
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type {
|
|
14
|
+
CreateDomainInput,
|
|
15
|
+
UpdateDomainInput,
|
|
16
|
+
Domain,
|
|
17
|
+
} from "../db/domains.js";
|
|
18
|
+
|
|
19
|
+
// Re-export types used by consumers
|
|
20
|
+
export type {
|
|
21
|
+
GoDaddyDomain,
|
|
22
|
+
GoDaddyDomainDetail,
|
|
23
|
+
GoDaddyDnsRecord,
|
|
24
|
+
GoDaddyAvailability,
|
|
25
|
+
GoDaddyRenewResponse,
|
|
26
|
+
} from "../../../../../open-connectors/connectors/connect-godaddy/src/index.js";
|
|
27
|
+
|
|
28
|
+
import {
|
|
29
|
+
GoDaddy,
|
|
30
|
+
GoDaddyApiError,
|
|
31
|
+
type GoDaddyDomain,
|
|
32
|
+
type GoDaddyDomainDetail,
|
|
33
|
+
type GoDaddyDnsRecord,
|
|
34
|
+
type GoDaddyAvailability,
|
|
35
|
+
type GoDaddyConfig,
|
|
36
|
+
} from "../../../../../open-connectors/connectors/connect-godaddy/src/index.js";
|
|
37
|
+
|
|
38
|
+
export { GoDaddyApiError };
|
|
39
|
+
|
|
40
|
+
// ============================================================
|
|
41
|
+
// Sync Result Type (microservice-specific)
|
|
42
|
+
// ============================================================
|
|
43
|
+
|
|
44
|
+
export interface GoDaddySyncResult {
|
|
45
|
+
synced: number;
|
|
46
|
+
created: number;
|
|
47
|
+
updated: number;
|
|
48
|
+
errors: string[];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ============================================================
|
|
52
|
+
// Internal fetch override (allows test injection)
|
|
53
|
+
// ============================================================
|
|
54
|
+
|
|
55
|
+
type FetchFn = typeof globalThis.fetch;
|
|
56
|
+
|
|
57
|
+
let _overriddenFetch: FetchFn | null = null;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Override the fetch implementation (for testing).
|
|
61
|
+
* Pass `null` to restore the default.
|
|
62
|
+
*/
|
|
63
|
+
export function _setFetch(fn: FetchFn | null): void {
|
|
64
|
+
_overriddenFetch = fn;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ============================================================
|
|
68
|
+
// Connector Instance Management
|
|
69
|
+
// ============================================================
|
|
70
|
+
|
|
71
|
+
function createConnector(): GoDaddy {
|
|
72
|
+
// Use fromEnv() which reads GODADDY_API_KEY and GODADDY_API_SECRET
|
|
73
|
+
return GoDaddy.fromEnv();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ============================================================
|
|
77
|
+
// API Functions (thin wrappers around connector)
|
|
78
|
+
// ============================================================
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* List all domains in the GoDaddy account.
|
|
82
|
+
*/
|
|
83
|
+
export async function listGoDaddyDomains(): Promise<GoDaddyDomain[]> {
|
|
84
|
+
if (_overriddenFetch) {
|
|
85
|
+
return _legacyApiRequest<GoDaddyDomain[]>("GET", "/v1/domains");
|
|
86
|
+
}
|
|
87
|
+
const connector = createConnector();
|
|
88
|
+
return connector.domains.list();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Get detailed info for a single domain.
|
|
93
|
+
*/
|
|
94
|
+
export async function getDomainInfo(
|
|
95
|
+
domain: string
|
|
96
|
+
): Promise<GoDaddyDomainDetail> {
|
|
97
|
+
if (_overriddenFetch) {
|
|
98
|
+
return _legacyApiRequest<GoDaddyDomainDetail>(
|
|
99
|
+
"GET",
|
|
100
|
+
`/v1/domains/${encodeURIComponent(domain)}`
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
const connector = createConnector();
|
|
104
|
+
return connector.domains.getInfo(domain);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Renew a domain for 1 year.
|
|
109
|
+
*/
|
|
110
|
+
export async function renewDomain(
|
|
111
|
+
domain: string
|
|
112
|
+
): Promise<{ orderId: number; itemCount: number; total: number }> {
|
|
113
|
+
if (_overriddenFetch) {
|
|
114
|
+
return _legacyApiRequest(
|
|
115
|
+
"POST",
|
|
116
|
+
`/v1/domains/${encodeURIComponent(domain)}/renew`,
|
|
117
|
+
{ period: 1 }
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
const connector = createConnector();
|
|
121
|
+
return connector.domains.renew(domain);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Get DNS records for a domain, optionally filtered by type.
|
|
126
|
+
*/
|
|
127
|
+
export async function getDnsRecords(
|
|
128
|
+
domain: string,
|
|
129
|
+
type?: string
|
|
130
|
+
): Promise<GoDaddyDnsRecord[]> {
|
|
131
|
+
if (_overriddenFetch) {
|
|
132
|
+
const path = type
|
|
133
|
+
? `/v1/domains/${encodeURIComponent(domain)}/records/${encodeURIComponent(type)}`
|
|
134
|
+
: `/v1/domains/${encodeURIComponent(domain)}/records`;
|
|
135
|
+
return _legacyApiRequest<GoDaddyDnsRecord[]>("GET", path);
|
|
136
|
+
}
|
|
137
|
+
const connector = createConnector();
|
|
138
|
+
return type
|
|
139
|
+
? connector.dns.getRecords(domain, type)
|
|
140
|
+
: connector.dns.getRecords(domain);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Replace all DNS records for a domain.
|
|
145
|
+
*/
|
|
146
|
+
export async function setDnsRecords(
|
|
147
|
+
domain: string,
|
|
148
|
+
records: GoDaddyDnsRecord[]
|
|
149
|
+
): Promise<void> {
|
|
150
|
+
if (_overriddenFetch) {
|
|
151
|
+
await _legacyApiRequest<void>(
|
|
152
|
+
"PUT",
|
|
153
|
+
`/v1/domains/${encodeURIComponent(domain)}/records`,
|
|
154
|
+
records
|
|
155
|
+
);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
const connector = createConnector();
|
|
159
|
+
await connector.dns.replaceAllRecords(domain, records);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Check domain availability for purchase.
|
|
164
|
+
*/
|
|
165
|
+
export async function checkAvailability(
|
|
166
|
+
domain: string
|
|
167
|
+
): Promise<GoDaddyAvailability> {
|
|
168
|
+
if (_overriddenFetch) {
|
|
169
|
+
return _legacyApiRequest<GoDaddyAvailability>(
|
|
170
|
+
"GET",
|
|
171
|
+
`/v1/domains/available?domain=${encodeURIComponent(domain)}`
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
const connector = createConnector();
|
|
175
|
+
return connector.domains.checkAvailability(domain);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// ============================================================
|
|
179
|
+
// Legacy fetch helper (for test injection compatibility)
|
|
180
|
+
// ============================================================
|
|
181
|
+
|
|
182
|
+
const GODADDY_API_BASE = "https://api.godaddy.com";
|
|
183
|
+
|
|
184
|
+
function _getCredentials(): { key: string; secret: string } {
|
|
185
|
+
const key = process.env["GODADDY_API_KEY"];
|
|
186
|
+
const secret = process.env["GODADDY_API_SECRET"];
|
|
187
|
+
|
|
188
|
+
if (!key || !secret) {
|
|
189
|
+
throw new Error(
|
|
190
|
+
"GoDaddy API credentials not configured. Set GODADDY_API_KEY and GODADDY_API_SECRET environment variables."
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return { key, secret };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function _getHeaders(): Record<string, string> {
|
|
198
|
+
const { key, secret } = _getCredentials();
|
|
199
|
+
return {
|
|
200
|
+
Authorization: `sso-key ${key}:${secret}`,
|
|
201
|
+
"Content-Type": "application/json",
|
|
202
|
+
Accept: "application/json",
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async function _legacyApiRequest<T>(
|
|
207
|
+
method: string,
|
|
208
|
+
path: string,
|
|
209
|
+
body?: unknown
|
|
210
|
+
): Promise<T> {
|
|
211
|
+
const fetchFn = _overriddenFetch || globalThis.fetch;
|
|
212
|
+
const url = `${GODADDY_API_BASE}${path}`;
|
|
213
|
+
const headers = _getHeaders();
|
|
214
|
+
|
|
215
|
+
const options: RequestInit = { method, headers };
|
|
216
|
+
if (body !== undefined) {
|
|
217
|
+
options.body = JSON.stringify(body);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const response = await fetchFn(url, options);
|
|
221
|
+
|
|
222
|
+
if (!response.ok) {
|
|
223
|
+
const text = await response.text();
|
|
224
|
+
throw new GoDaddyApiError(
|
|
225
|
+
`GoDaddy API ${method} ${path} failed with status ${response.status}: ${text}`,
|
|
226
|
+
response.status,
|
|
227
|
+
{ responseBody: text }
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Some endpoints return 204 No Content
|
|
232
|
+
if (response.status === 204) {
|
|
233
|
+
return undefined as unknown as T;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return (await response.json()) as T;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// ============================================================
|
|
240
|
+
// Sync to Local DB (microservice business logic)
|
|
241
|
+
// ============================================================
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Maps GoDaddy status strings to local domain statuses.
|
|
245
|
+
*/
|
|
246
|
+
function mapGoDaddyStatus(
|
|
247
|
+
gdStatus: string
|
|
248
|
+
): "active" | "expired" | "transferring" | "redemption" {
|
|
249
|
+
const s = gdStatus.toUpperCase();
|
|
250
|
+
if (s === "ACTIVE") return "active";
|
|
251
|
+
if (s === "EXPIRED") return "expired";
|
|
252
|
+
if (
|
|
253
|
+
s === "TRANSFERRED_OUT" ||
|
|
254
|
+
s === "TRANSFERRING" ||
|
|
255
|
+
s === "PENDING_TRANSFER"
|
|
256
|
+
)
|
|
257
|
+
return "transferring";
|
|
258
|
+
if (s === "REDEMPTION" || s === "PENDING_REDEMPTION") return "redemption";
|
|
259
|
+
return "active";
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Sync all GoDaddy domains into the local database.
|
|
264
|
+
*
|
|
265
|
+
* Accepts DB helpers so the caller can inject the actual CRUD functions.
|
|
266
|
+
*/
|
|
267
|
+
export async function syncToLocalDb(dbFns: {
|
|
268
|
+
getDomainByName: (name: string) => Domain | null;
|
|
269
|
+
createDomain: (input: CreateDomainInput) => Domain;
|
|
270
|
+
updateDomain: (id: string, input: UpdateDomainInput) => Domain | null;
|
|
271
|
+
}): Promise<GoDaddySyncResult> {
|
|
272
|
+
const result: GoDaddySyncResult = {
|
|
273
|
+
synced: 0,
|
|
274
|
+
created: 0,
|
|
275
|
+
updated: 0,
|
|
276
|
+
errors: [],
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
let gdDomains: GoDaddyDomain[];
|
|
280
|
+
try {
|
|
281
|
+
gdDomains = await listGoDaddyDomains();
|
|
282
|
+
} catch (err) {
|
|
283
|
+
result.errors.push(
|
|
284
|
+
`Failed to list domains: ${err instanceof Error ? err.message : String(err)}`
|
|
285
|
+
);
|
|
286
|
+
return result;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
for (const gd of gdDomains) {
|
|
290
|
+
try {
|
|
291
|
+
// Fetch full detail for each domain via connector
|
|
292
|
+
let detail: GoDaddyDomainDetail;
|
|
293
|
+
try {
|
|
294
|
+
detail = await getDomainInfo(gd.domain);
|
|
295
|
+
} catch {
|
|
296
|
+
// Fall back to list-level data if detail fetch fails
|
|
297
|
+
detail = gd as GoDaddyDomainDetail;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const existing = dbFns.getDomainByName(gd.domain);
|
|
301
|
+
|
|
302
|
+
const domainData = {
|
|
303
|
+
name: gd.domain,
|
|
304
|
+
registrar: "GoDaddy",
|
|
305
|
+
status: mapGoDaddyStatus(gd.status),
|
|
306
|
+
expires_at: gd.expires
|
|
307
|
+
? new Date(gd.expires).toISOString()
|
|
308
|
+
: undefined,
|
|
309
|
+
auto_renew: gd.renewAuto,
|
|
310
|
+
nameservers: gd.nameServers || [],
|
|
311
|
+
registered_at: detail.createdAt
|
|
312
|
+
? new Date(detail.createdAt).toISOString()
|
|
313
|
+
: undefined,
|
|
314
|
+
metadata: {
|
|
315
|
+
godaddy_domain_id: (detail as GoDaddyDomainDetail).domainId,
|
|
316
|
+
provider: "godaddy",
|
|
317
|
+
locked: (detail as GoDaddyDomainDetail).locked,
|
|
318
|
+
privacy: (detail as GoDaddyDomainDetail).privacy,
|
|
319
|
+
},
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
if (existing) {
|
|
323
|
+
dbFns.updateDomain(existing.id, domainData);
|
|
324
|
+
result.updated++;
|
|
325
|
+
} else {
|
|
326
|
+
dbFns.createDomain(domainData);
|
|
327
|
+
result.created++;
|
|
328
|
+
}
|
|
329
|
+
result.synced++;
|
|
330
|
+
} catch (err) {
|
|
331
|
+
result.errors.push(
|
|
332
|
+
`Failed to sync ${gd.domain}: ${err instanceof Error ? err.message : String(err)}`
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
return result;
|
|
338
|
+
}
|