@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,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
+ }