@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,328 @@
1
+ /**
2
+ * GoDaddy API integration for microservice-domains
3
+ *
4
+ * Environment variables:
5
+ * GODADDY_API_KEY — GoDaddy API key
6
+ * GODADDY_API_SECRET — GoDaddy API secret
7
+ */
8
+
9
+ import type {
10
+ CreateDomainInput,
11
+ UpdateDomainInput,
12
+ Domain,
13
+ } from "../db/domains.js";
14
+
15
+ // ============================================================
16
+ // Types
17
+ // ============================================================
18
+
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
+ export interface GoDaddySyncResult {
61
+ synced: number;
62
+ created: number;
63
+ updated: number;
64
+ errors: string[];
65
+ }
66
+
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
+ // ============================================================
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)
108
+ // ============================================================
109
+
110
+ type FetchFn = typeof globalThis.fetch;
111
+
112
+ let _fetchFn: FetchFn = globalThis.fetch;
113
+
114
+ /**
115
+ * Override the fetch implementation (for testing).
116
+ * Pass `null` to restore the default.
117
+ */
118
+ export function _setFetch(fn: FetchFn | null): void {
119
+ _fetchFn = fn ?? globalThis.fetch;
120
+ }
121
+
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
+ }
150
+
151
+ return (await response.json()) as T;
152
+ }
153
+
154
+ // ============================================================
155
+ // API Functions
156
+ // ============================================================
157
+
158
+ /**
159
+ * List all domains in the GoDaddy account.
160
+ */
161
+ export async function listGoDaddyDomains(): Promise<GoDaddyDomain[]> {
162
+ return apiRequest<GoDaddyDomain[]>("GET", "/v1/domains");
163
+ }
164
+
165
+ /**
166
+ * Get detailed info for a single domain.
167
+ */
168
+ export async function getDomainInfo(
169
+ domain: string
170
+ ): Promise<GoDaddyDomainDetail> {
171
+ return apiRequest<GoDaddyDomainDetail>(
172
+ "GET",
173
+ `/v1/domains/${encodeURIComponent(domain)}`
174
+ );
175
+ }
176
+
177
+ /**
178
+ * Renew a domain for 1 year.
179
+ */
180
+ export async function renewDomain(
181
+ domain: string
182
+ ): Promise<{ orderId: number; itemCount: number; total: number }> {
183
+ return apiRequest(
184
+ "POST",
185
+ `/v1/domains/${encodeURIComponent(domain)}/renew`,
186
+ { period: 1 }
187
+ );
188
+ }
189
+
190
+ /**
191
+ * Get DNS records for a domain, optionally filtered by type.
192
+ */
193
+ export async function getDnsRecords(
194
+ domain: string,
195
+ type?: string
196
+ ): 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);
201
+ }
202
+
203
+ /**
204
+ * Replace all DNS records for a domain.
205
+ */
206
+ export async function setDnsRecords(
207
+ domain: string,
208
+ records: GoDaddyDnsRecord[]
209
+ ): Promise<void> {
210
+ await apiRequest<void>(
211
+ "PUT",
212
+ `/v1/domains/${encodeURIComponent(domain)}/records`,
213
+ records
214
+ );
215
+ }
216
+
217
+ /**
218
+ * Check domain availability for purchase.
219
+ */
220
+ export async function checkAvailability(
221
+ domain: string
222
+ ): Promise<GoDaddyAvailability> {
223
+ return apiRequest<GoDaddyAvailability>(
224
+ "GET",
225
+ `/v1/domains/available?domain=${encodeURIComponent(domain)}`
226
+ );
227
+ }
228
+
229
+ // ============================================================
230
+ // Sync to Local DB
231
+ // ============================================================
232
+
233
+ /**
234
+ * Maps GoDaddy status strings to local domain statuses.
235
+ */
236
+ function mapGoDaddyStatus(
237
+ gdStatus: string
238
+ ): "active" | "expired" | "transferring" | "redemption" {
239
+ const s = gdStatus.toUpperCase();
240
+ if (s === "ACTIVE") return "active";
241
+ if (s === "EXPIRED") return "expired";
242
+ if (
243
+ s === "TRANSFERRED_OUT" ||
244
+ s === "TRANSFERRING" ||
245
+ s === "PENDING_TRANSFER"
246
+ )
247
+ return "transferring";
248
+ if (s === "REDEMPTION" || s === "PENDING_REDEMPTION") return "redemption";
249
+ return "active";
250
+ }
251
+
252
+ /**
253
+ * Sync all GoDaddy domains into the local database.
254
+ *
255
+ * Accepts DB helpers so the caller can inject the actual CRUD functions.
256
+ */
257
+ export async function syncToLocalDb(dbFns: {
258
+ getDomainByName: (name: string) => Domain | null;
259
+ createDomain: (input: CreateDomainInput) => Domain;
260
+ updateDomain: (id: string, input: UpdateDomainInput) => Domain | null;
261
+ }): Promise<GoDaddySyncResult> {
262
+ const result: GoDaddySyncResult = {
263
+ synced: 0,
264
+ created: 0,
265
+ updated: 0,
266
+ errors: [],
267
+ };
268
+
269
+ let gdDomains: GoDaddyDomain[];
270
+ try {
271
+ gdDomains = await listGoDaddyDomains();
272
+ } catch (err) {
273
+ result.errors.push(
274
+ `Failed to list domains: ${err instanceof Error ? err.message : String(err)}`
275
+ );
276
+ return result;
277
+ }
278
+
279
+ for (const gd of gdDomains) {
280
+ try {
281
+ // Fetch full detail for each domain
282
+ let detail: GoDaddyDomainDetail;
283
+ try {
284
+ detail = await getDomainInfo(gd.domain);
285
+ } catch {
286
+ // Fall back to list-level data if detail fetch fails
287
+ detail = gd as GoDaddyDomainDetail;
288
+ }
289
+
290
+ const existing = dbFns.getDomainByName(gd.domain);
291
+
292
+ const domainData = {
293
+ name: gd.domain,
294
+ registrar: "GoDaddy",
295
+ status: mapGoDaddyStatus(gd.status),
296
+ expires_at: gd.expires
297
+ ? new Date(gd.expires).toISOString()
298
+ : undefined,
299
+ auto_renew: gd.renewAuto,
300
+ nameservers: gd.nameServers || [],
301
+ registered_at: detail.createdAt
302
+ ? new Date(detail.createdAt).toISOString()
303
+ : undefined,
304
+ metadata: {
305
+ godaddy_domain_id: (detail as GoDaddyDomainDetail).domainId,
306
+ provider: "godaddy",
307
+ locked: (detail as GoDaddyDomainDetail).locked,
308
+ privacy: (detail as GoDaddyDomainDetail).privacy,
309
+ },
310
+ };
311
+
312
+ if (existing) {
313
+ dbFns.updateDomain(existing.id, domainData);
314
+ result.updated++;
315
+ } else {
316
+ dbFns.createDomain(domainData);
317
+ result.created++;
318
+ }
319
+ result.synced++;
320
+ } catch (err) {
321
+ result.errors.push(
322
+ `Failed to sync ${gd.domain}: ${err instanceof Error ? err.message : String(err)}`
323
+ );
324
+ }
325
+ }
326
+
327
+ return result;
328
+ }