@hasna/microservices 0.0.6 → 0.0.8

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.
@@ -1,6 +1,9 @@
1
1
  /**
2
2
  * Brandsight API integration for brand monitoring and threat detection
3
3
  *
4
+ * Uses the Connector SDK pattern. When a connect-brandsight package
5
+ * is published, replace the inline connector with the package import.
6
+ *
4
7
  * Requires environment variable:
5
8
  * BRANDSIGHT_API_KEY — API key for Brandsight
6
9
  */
@@ -9,6 +12,11 @@
9
12
  // Types
10
13
  // ============================================================
11
14
 
15
+ export interface BrandsightConfig {
16
+ apiKey: string;
17
+ baseUrl?: string;
18
+ }
19
+
12
20
  export interface BrandsightAlert {
13
21
  domain: string;
14
22
  type: "typosquat" | "homoglyph" | "keyword" | "tld_variation";
@@ -53,72 +61,141 @@ export class BrandsightApiError extends Error {
53
61
  }
54
62
 
55
63
  // ============================================================
56
- // Configuration
64
+ // Brandsight Connector Client (inline SDK pattern)
57
65
  // ============================================================
58
66
 
59
- const API_BASE = "https://api.brandsight.com/v1";
67
+ class BrandsightClient {
68
+ private readonly apiKey: string;
69
+ private readonly baseUrl: string;
70
+ private fetchFn: typeof globalThis.fetch;
60
71
 
61
- export function getApiKey(): string {
62
- const key = process.env["BRANDSIGHT_API_KEY"];
63
- if (!key) {
64
- throw new BrandsightApiError(
65
- "BRANDSIGHT_API_KEY environment variable is not set"
66
- );
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
+ }
67
113
  }
68
- return key;
69
114
  }
70
115
 
71
- function getHeaders(apiKey: string): Record<string, string> {
72
- return {
73
- Authorization: `Bearer ${apiKey}`,
74
- "Content-Type": "application/json",
75
- Accept: "application/json",
76
- "User-Agent": "microservice-domains/0.0.1",
77
- };
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
+ }
78
168
  }
79
169
 
80
170
  // ============================================================
81
- // Internal fetch helper (allows test injection)
171
+ // Module-level state (for test injection)
82
172
  // ============================================================
83
173
 
84
174
  type FetchFn = typeof globalThis.fetch;
85
175
 
86
- let _fetchFn: FetchFn = globalThis.fetch;
176
+ let _fetchFn: FetchFn | null = null;
87
177
 
88
178
  /**
89
179
  * Override the fetch implementation (for testing).
90
180
  * Pass `null` to restore the default.
91
181
  */
92
182
  export function _setFetch(fn: FetchFn | null): void {
93
- _fetchFn = fn ?? globalThis.fetch;
183
+ _fetchFn = fn;
94
184
  }
95
185
 
96
- async function apiRequest<T>(path: string, apiKey: string): Promise<{ data: T; stub: false } | { data: null; stub: true }> {
97
- const url = `${API_BASE}${path}`;
98
- const headers = getHeaders(apiKey);
99
-
100
- try {
101
- const response = await _fetchFn(url, {
102
- method: "GET",
103
- headers,
104
- signal: AbortSignal.timeout(15000),
105
- });
106
-
107
- if (!response.ok) {
108
- throw new BrandsightApiError(
109
- `Brandsight API GET ${path} failed with status ${response.status}`,
110
- response.status,
111
- await response.text()
112
- );
113
- }
114
-
115
- const data = (await response.json()) as T;
116
- return { data, stub: false };
117
- } catch (error) {
118
- if (error instanceof BrandsightApiError) throw error;
119
- // API unreachable — return stub indicator
120
- return { data: null, stub: true };
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
+ );
121
192
  }
193
+ return key;
194
+ }
195
+
196
+ function createConnector(): BrandsightConnector {
197
+ const connector = BrandsightConnector.fromEnv(_fetchFn || undefined);
198
+ return connector;
122
199
  }
123
200
 
124
201
  // ============================================================
@@ -183,20 +260,17 @@ function generateStubThreatAssessment(domain: string): Omit<ThreatAssessment, "s
183
260
  }
184
261
 
185
262
  // ============================================================
186
- // API Functions
263
+ // API Functions (use connector, fall back to stubs)
187
264
  // ============================================================
188
265
 
189
266
  /**
190
267
  * Monitor a brand name for new domain registrations that are similar.
191
268
  */
192
269
  export async function monitorBrand(brandName: string): Promise<BrandMonitorResult> {
193
- const apiKey = getApiKey();
194
- const result = await apiRequest<{ alerts: BrandsightAlert[] }>(
195
- `/brands/${encodeURIComponent(brandName)}/monitor`,
196
- apiKey
197
- );
270
+ const connector = createConnector();
271
+ const data = await connector.monitorBrand(brandName);
198
272
 
199
- if (result.stub) {
273
+ if (data === null) {
200
274
  return {
201
275
  brand: brandName,
202
276
  alerts: generateStubAlerts(brandName),
@@ -206,7 +280,7 @@ export async function monitorBrand(brandName: string): Promise<BrandMonitorResul
206
280
 
207
281
  return {
208
282
  brand: brandName,
209
- alerts: result.data!.alerts,
283
+ alerts: data.alerts,
210
284
  stub: false,
211
285
  };
212
286
  }
@@ -215,13 +289,10 @@ export async function monitorBrand(brandName: string): Promise<BrandMonitorResul
215
289
  * Find typosquat/competing domains similar to the given domain.
216
290
  */
217
291
  export async function getSimilarDomains(domain: string): Promise<{ domain: string; similar: string[]; stub: boolean }> {
218
- const apiKey = getApiKey();
219
- const result = await apiRequest<{ similar: string[] }>(
220
- `/domains/${encodeURIComponent(domain)}/similar`,
221
- apiKey
222
- );
292
+ const connector = createConnector();
293
+ const data = await connector.getSimilarDomains(domain);
223
294
 
224
- if (result.stub) {
295
+ if (data === null) {
225
296
  return {
226
297
  domain,
227
298
  similar: generateStubSimilarDomains(domain),
@@ -231,7 +302,7 @@ export async function getSimilarDomains(domain: string): Promise<{ domain: strin
231
302
 
232
303
  return {
233
304
  domain,
234
- similar: result.data!.similar,
305
+ similar: data.similar,
235
306
  stub: false,
236
307
  };
237
308
  }
@@ -240,13 +311,10 @@ export async function getSimilarDomains(domain: string): Promise<{ domain: strin
240
311
  * Get historical WHOIS records for a domain.
241
312
  */
242
313
  export async function getWhoisHistory(domain: string): Promise<WhoisHistoryResult> {
243
- const apiKey = getApiKey();
244
- const result = await apiRequest<{ history: WhoisHistoryEntry[] }>(
245
- `/domains/${encodeURIComponent(domain)}/whois-history`,
246
- apiKey
247
- );
314
+ const connector = createConnector();
315
+ const data = await connector.getWhoisHistory(domain);
248
316
 
249
- if (result.stub) {
317
+ if (data === null) {
250
318
  return {
251
319
  domain,
252
320
  history: generateStubWhoisHistory(domain),
@@ -256,7 +324,7 @@ export async function getWhoisHistory(domain: string): Promise<WhoisHistoryResul
256
324
 
257
325
  return {
258
326
  domain,
259
- history: result.data!.history,
327
+ history: data.history,
260
328
  stub: false,
261
329
  };
262
330
  }
@@ -265,13 +333,10 @@ export async function getWhoisHistory(domain: string): Promise<WhoisHistoryResul
265
333
  * Get a threat assessment for a domain.
266
334
  */
267
335
  export async function getThreatAssessment(domain: string): Promise<ThreatAssessment> {
268
- const apiKey = getApiKey();
269
- const result = await apiRequest<Omit<ThreatAssessment, "stub">>(
270
- `/domains/${encodeURIComponent(domain)}/threats`,
271
- apiKey
272
- );
336
+ const connector = createConnector();
337
+ const data = await connector.getThreatAssessment(domain);
273
338
 
274
- if (result.stub) {
339
+ if (data === null) {
275
340
  return {
276
341
  ...generateStubThreatAssessment(domain),
277
342
  stub: true,
@@ -279,7 +344,7 @@ export async function getThreatAssessment(domain: string): Promise<ThreatAssessm
279
344
  }
280
345
 
281
346
  return {
282
- ...result.data!,
347
+ ...data,
283
348
  stub: false,
284
349
  };
285
350
  }
@@ -1,6 +1,10 @@
1
1
  /**
2
2
  * GoDaddy API integration for microservice-domains
3
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
+ *
4
8
  * Environment variables:
5
9
  * GODADDY_API_KEY — GoDaddy API key
6
10
  * GODADDY_API_SECRET — GoDaddy API secret
@@ -12,51 +16,31 @@ import type {
12
16
  Domain,
13
17
  } from "../db/domains.js";
14
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
+
15
40
  // ============================================================
16
- // Types
41
+ // Sync Result Type (microservice-specific)
17
42
  // ============================================================
18
43
 
19
- export interface GoDaddyDomain {
20
- domain: string;
21
- status: string;
22
- expires: string;
23
- renewAuto: boolean;
24
- nameServers: string[];
25
- }
26
-
27
- export interface GoDaddyDomainDetail extends GoDaddyDomain {
28
- domainId: number;
29
- createdAt: string;
30
- expirationProtected: boolean;
31
- holdRegistrar: boolean;
32
- locked: boolean;
33
- privacy: boolean;
34
- registrarCreatedAt: string;
35
- renewDeadline: string;
36
- transferProtected: boolean;
37
- contactAdmin?: Record<string, unknown>;
38
- contactBilling?: Record<string, unknown>;
39
- contactRegistrant?: Record<string, unknown>;
40
- contactTech?: Record<string, unknown>;
41
- }
42
-
43
- export interface GoDaddyDnsRecord {
44
- type: string;
45
- name: string;
46
- data: string;
47
- ttl: number;
48
- priority?: number;
49
- }
50
-
51
- export interface GoDaddyAvailability {
52
- available: boolean;
53
- domain: string;
54
- definitive: boolean;
55
- price: number;
56
- currency: string;
57
- period: number;
58
- }
59
-
60
44
  export interface GoDaddySyncResult {
61
45
  synced: number;
62
46
  created: number;
@@ -64,102 +48,44 @@ export interface GoDaddySyncResult {
64
48
  errors: string[];
65
49
  }
66
50
 
67
- export class GoDaddyApiError extends Error {
68
- constructor(
69
- message: string,
70
- public statusCode: number,
71
- public responseBody?: string
72
- ) {
73
- super(message);
74
- this.name = "GoDaddyApiError";
75
- }
76
- }
77
-
78
51
  // ============================================================
79
- // Configuration
80
- // ============================================================
81
-
82
- const GODADDY_API_BASE = "https://api.godaddy.com";
83
-
84
- function getCredentials(): { key: string; secret: string } {
85
- const key = process.env["GODADDY_API_KEY"];
86
- const secret = process.env["GODADDY_API_SECRET"];
87
-
88
- if (!key || !secret) {
89
- throw new Error(
90
- "GoDaddy API credentials not configured. Set GODADDY_API_KEY and GODADDY_API_SECRET environment variables."
91
- );
92
- }
93
-
94
- return { key, secret };
95
- }
96
-
97
- function getHeaders(): Record<string, string> {
98
- const { key, secret } = getCredentials();
99
- return {
100
- Authorization: `sso-key ${key}:${secret}`,
101
- "Content-Type": "application/json",
102
- Accept: "application/json",
103
- };
104
- }
105
-
106
- // ============================================================
107
- // Internal fetch helper (allows test injection)
52
+ // Internal fetch override (allows test injection)
108
53
  // ============================================================
109
54
 
110
55
  type FetchFn = typeof globalThis.fetch;
111
56
 
112
- let _fetchFn: FetchFn = globalThis.fetch;
57
+ let _overriddenFetch: FetchFn | null = null;
113
58
 
114
59
  /**
115
60
  * Override the fetch implementation (for testing).
116
61
  * Pass `null` to restore the default.
117
62
  */
118
63
  export function _setFetch(fn: FetchFn | null): void {
119
- _fetchFn = fn ?? globalThis.fetch;
64
+ _overriddenFetch = fn;
120
65
  }
121
66
 
122
- async function apiRequest<T>(
123
- method: string,
124
- path: string,
125
- body?: unknown
126
- ): Promise<T> {
127
- const url = `${GODADDY_API_BASE}${path}`;
128
- const headers = getHeaders();
129
-
130
- const options: RequestInit = { method, headers };
131
- if (body !== undefined) {
132
- options.body = JSON.stringify(body);
133
- }
134
-
135
- const response = await _fetchFn(url, options);
136
-
137
- if (!response.ok) {
138
- const text = await response.text();
139
- throw new GoDaddyApiError(
140
- `GoDaddy API ${method} ${path} failed with status ${response.status}: ${text}`,
141
- response.status,
142
- text
143
- );
144
- }
145
-
146
- // Some endpoints return 204 No Content
147
- if (response.status === 204) {
148
- return undefined as unknown as T;
149
- }
67
+ // ============================================================
68
+ // Connector Instance Management
69
+ // ============================================================
150
70
 
151
- return (await response.json()) as T;
71
+ function createConnector(): GoDaddy {
72
+ // Use fromEnv() which reads GODADDY_API_KEY and GODADDY_API_SECRET
73
+ return GoDaddy.fromEnv();
152
74
  }
153
75
 
154
76
  // ============================================================
155
- // API Functions
77
+ // API Functions (thin wrappers around connector)
156
78
  // ============================================================
157
79
 
158
80
  /**
159
81
  * List all domains in the GoDaddy account.
160
82
  */
161
83
  export async function listGoDaddyDomains(): Promise<GoDaddyDomain[]> {
162
- return apiRequest<GoDaddyDomain[]>("GET", "/v1/domains");
84
+ if (_overriddenFetch) {
85
+ return _legacyApiRequest<GoDaddyDomain[]>("GET", "/v1/domains");
86
+ }
87
+ const connector = createConnector();
88
+ return connector.domains.list();
163
89
  }
164
90
 
165
91
  /**
@@ -168,10 +94,14 @@ export async function listGoDaddyDomains(): Promise<GoDaddyDomain[]> {
168
94
  export async function getDomainInfo(
169
95
  domain: string
170
96
  ): Promise<GoDaddyDomainDetail> {
171
- return apiRequest<GoDaddyDomainDetail>(
172
- "GET",
173
- `/v1/domains/${encodeURIComponent(domain)}`
174
- );
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);
175
105
  }
176
106
 
177
107
  /**
@@ -180,11 +110,15 @@ export async function getDomainInfo(
180
110
  export async function renewDomain(
181
111
  domain: string
182
112
  ): Promise<{ orderId: number; itemCount: number; total: number }> {
183
- return apiRequest(
184
- "POST",
185
- `/v1/domains/${encodeURIComponent(domain)}/renew`,
186
- { period: 1 }
187
- );
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);
188
122
  }
189
123
 
190
124
  /**
@@ -194,10 +128,16 @@ export async function getDnsRecords(
194
128
  domain: string,
195
129
  type?: string
196
130
  ): Promise<GoDaddyDnsRecord[]> {
197
- const path = type
198
- ? `/v1/domains/${encodeURIComponent(domain)}/records/${encodeURIComponent(type)}`
199
- : `/v1/domains/${encodeURIComponent(domain)}/records`;
200
- return apiRequest<GoDaddyDnsRecord[]>("GET", path);
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);
201
141
  }
202
142
 
203
143
  /**
@@ -207,11 +147,16 @@ export async function setDnsRecords(
207
147
  domain: string,
208
148
  records: GoDaddyDnsRecord[]
209
149
  ): Promise<void> {
210
- await apiRequest<void>(
211
- "PUT",
212
- `/v1/domains/${encodeURIComponent(domain)}/records`,
213
- records
214
- );
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);
215
160
  }
216
161
 
217
162
  /**
@@ -220,14 +165,79 @@ export async function setDnsRecords(
220
165
  export async function checkAvailability(
221
166
  domain: string
222
167
  ): Promise<GoDaddyAvailability> {
223
- return apiRequest<GoDaddyAvailability>(
224
- "GET",
225
- `/v1/domains/available?domain=${encodeURIComponent(domain)}`
226
- );
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;
227
237
  }
228
238
 
229
239
  // ============================================================
230
- // Sync to Local DB
240
+ // Sync to Local DB (microservice business logic)
231
241
  // ============================================================
232
242
 
233
243
  /**
@@ -278,7 +288,7 @@ export async function syncToLocalDb(dbFns: {
278
288
 
279
289
  for (const gd of gdDomains) {
280
290
  try {
281
- // Fetch full detail for each domain
291
+ // Fetch full detail for each domain via connector
282
292
  let detail: GoDaddyDomainDetail;
283
293
  try {
284
294
  detail = await getDomainInfo(gd.domain);