@framers/agentos-ext-domain-porkbun 0.1.0 → 0.1.2

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,434 +0,0 @@
1
- // @ts-nocheck
2
- import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
3
- import { PorkbunService, type PorkbunConfig } from '../src/PorkbunService';
4
-
5
- // ---------------------------------------------------------------------------
6
- // Helpers
7
- // ---------------------------------------------------------------------------
8
-
9
- function mockResponse(data: unknown, status = 200): Response {
10
- return {
11
- ok: status >= 200 && status < 300,
12
- status,
13
- statusText: status === 200 ? 'OK' : 'Error',
14
- headers: new Headers(),
15
- redirected: false,
16
- type: 'basic' as ResponseType,
17
- url: '',
18
- clone: () => mockResponse(data, status),
19
- body: null,
20
- bodyUsed: false,
21
- arrayBuffer: () => Promise.resolve(new ArrayBuffer(0)),
22
- blob: () => Promise.resolve(new Blob()),
23
- formData: () => Promise.resolve(new FormData()),
24
- json: () => Promise.resolve(data),
25
- text: () => Promise.resolve(JSON.stringify(data)),
26
- bytes: () => Promise.resolve(new Uint8Array()),
27
- } as Response;
28
- }
29
-
30
- const DEFAULT_CONFIG: PorkbunConfig = {
31
- apiKey: 'pk_test_123',
32
- secretApiKey: 'sk_test_456',
33
- baseUrl: 'https://api.porkbun.com/api/json/v3',
34
- };
35
-
36
- let mockFetch: ReturnType<typeof vi.fn>;
37
- let service: PorkbunService;
38
-
39
- // ---------------------------------------------------------------------------
40
- // Tests
41
- // ---------------------------------------------------------------------------
42
-
43
- describe('PorkbunService', () => {
44
- beforeEach(() => {
45
- mockFetch = vi.fn();
46
- vi.stubGlobal('fetch', mockFetch);
47
- service = new PorkbunService(DEFAULT_CONFIG);
48
- });
49
-
50
- afterEach(() => {
51
- vi.restoreAllMocks();
52
- });
53
-
54
- // ── Constructor ──────────────────────────────────────────────────────────
55
-
56
- it('should use default baseUrl when none is provided', () => {
57
- const svc = new PorkbunService({ apiKey: 'a', secretApiKey: 'b' });
58
- // Accessing private field via any to verify default
59
- expect((svc as any).config.baseUrl).toBe('https://api.porkbun.com/api/json/v3');
60
- });
61
-
62
- it('should use custom baseUrl when provided', () => {
63
- const svc = new PorkbunService({ apiKey: 'a', secretApiKey: 'b', baseUrl: 'https://custom.api.dev' });
64
- expect((svc as any).config.baseUrl).toBe('https://custom.api.dev');
65
- });
66
-
67
- // ── initialize() ────────────────────────────────────────────────────────
68
-
69
- it('should initialize successfully on ping SUCCESS', async () => {
70
- mockFetch.mockResolvedValueOnce(mockResponse({ status: 'SUCCESS', yourIp: '1.2.3.4' }));
71
-
72
- await service.initialize();
73
-
74
- expect(service.isRunning).toBe(true);
75
- expect(mockFetch).toHaveBeenCalledTimes(1);
76
- const [url, init] = mockFetch.mock.calls[0];
77
- expect(url).toBe('https://api.porkbun.com/api/json/v3/ping');
78
- expect(init.method).toBe('POST');
79
- const body = JSON.parse(init.body);
80
- expect(body.apikey).toBe('pk_test_123');
81
- expect(body.secretapikey).toBe('sk_test_456');
82
- });
83
-
84
- it('should throw on ping failure', async () => {
85
- mockFetch.mockResolvedValueOnce(
86
- mockResponse({ status: 'ERROR', message: 'Invalid API key' }),
87
- );
88
-
89
- await expect(service.initialize()).rejects.toThrow('Porkbun auth failed');
90
- expect(service.isRunning).toBe(false);
91
- });
92
-
93
- // ── shutdown() ──────────────────────────────────────────────────────────
94
-
95
- it('should set isRunning to false on shutdown', async () => {
96
- mockFetch.mockResolvedValueOnce(mockResponse({ status: 'SUCCESS' }));
97
- await service.initialize();
98
- expect(service.isRunning).toBe(true);
99
-
100
- await service.shutdown();
101
- expect(service.isRunning).toBe(false);
102
- });
103
-
104
- // ── checkAvailability() ─────────────────────────────────────────────────
105
-
106
- it('should return available=true when domain is available', async () => {
107
- mockFetch.mockResolvedValueOnce(
108
- mockResponse({ status: 'SUCCESS', avail: true }),
109
- );
110
- // pricing call
111
- mockFetch.mockResolvedValueOnce(
112
- mockResponse({ status: 'SUCCESS', pricing: { registration: '9.73', renewal: '9.73', transfer: '9.73' } }),
113
- );
114
-
115
- const result = await service.checkAvailability('example.dev');
116
-
117
- expect(result.domain).toBe('example.dev');
118
- expect(result.available).toBe(true);
119
- expect(result.pricing?.registration).toBe('9.73');
120
- });
121
-
122
- it('should return available=false when domain is taken', async () => {
123
- mockFetch.mockResolvedValueOnce(
124
- mockResponse({ status: 'SUCCESS', avail: false }),
125
- );
126
- // pricing may fail for unavailable domains — mock failure
127
- mockFetch.mockResolvedValueOnce(
128
- mockResponse({ status: 'ERROR', message: 'No pricing' }),
129
- );
130
-
131
- const result = await service.checkAvailability('google.com');
132
-
133
- expect(result.domain).toBe('google.com');
134
- expect(result.available).toBe(false);
135
- expect(result.pricing).toBeUndefined();
136
- });
137
-
138
- it('should handle string "true" for avail field', async () => {
139
- mockFetch.mockResolvedValueOnce(
140
- mockResponse({ status: 'SUCCESS', avail: 'true' }),
141
- );
142
- mockFetch.mockResolvedValueOnce(
143
- mockResponse({ status: 'ERROR', message: 'nope' }),
144
- );
145
-
146
- const result = await service.checkAvailability('test.io');
147
- expect(result.available).toBe(true);
148
- });
149
-
150
- // ── getPricing() ────────────────────────────────────────────────────────
151
-
152
- it('should return pricing for a domain', async () => {
153
- mockFetch.mockResolvedValueOnce(
154
- mockResponse({ status: 'SUCCESS', pricing: { registration: '10.00', renewal: '10.00', transfer: '10.00', couponed: '8.00' } }),
155
- );
156
-
157
- const pricing = await service.getPricing('example.com');
158
-
159
- expect(pricing.registration).toBe('10.00');
160
- expect(pricing.renewal).toBe('10.00');
161
- expect(pricing.transfer).toBe('10.00');
162
- expect(pricing.couponed).toBe('8.00');
163
- });
164
-
165
- // ── registerDomain() ───────────────────────────────────────────────────
166
-
167
- it('should register a domain successfully', async () => {
168
- mockFetch.mockResolvedValueOnce(
169
- mockResponse({ status: 'SUCCESS', message: 'Domain registered' }),
170
- );
171
-
172
- const result = await service.registerDomain({ domain: 'newsite.dev', years: 2 });
173
-
174
- expect(result.domain).toBe('newsite.dev');
175
- expect(result.status).toBe('SUCCESS');
176
- const body = JSON.parse(mockFetch.mock.calls[0][1].body);
177
- expect(body.domain).toBe('newsite.dev');
178
- expect(body.years).toBe(2);
179
- });
180
-
181
- it('should default to 1 year when years not specified', async () => {
182
- mockFetch.mockResolvedValueOnce(
183
- mockResponse({ status: 'SUCCESS' }),
184
- );
185
-
186
- await service.registerDomain({ domain: 'newsite.dev' });
187
-
188
- const body = JSON.parse(mockFetch.mock.calls[0][1].body);
189
- expect(body.years).toBe(1);
190
- });
191
-
192
- it('should include optional contact fields', async () => {
193
- mockFetch.mockResolvedValueOnce(
194
- mockResponse({ status: 'SUCCESS' }),
195
- );
196
-
197
- await service.registerDomain({
198
- domain: 'newsite.dev',
199
- firstName: 'John',
200
- lastName: 'Doe',
201
- email: 'john@example.com',
202
- phone: '+1.5551234567',
203
- city: 'New York',
204
- state: 'NY',
205
- zip: '10001',
206
- country: 'US',
207
- organization: 'Acme',
208
- address: '123 Main St',
209
- });
210
-
211
- const body = JSON.parse(mockFetch.mock.calls[0][1].body);
212
- expect(body.firstName).toBe('John');
213
- expect(body.stateProvince).toBe('NY');
214
- expect(body.addressLine1).toBe('123 Main St');
215
- expect(body.organization).toBe('Acme');
216
- });
217
-
218
- // ── listDomains() ──────────────────────────────────────────────────────
219
-
220
- it('should list all domains', async () => {
221
- mockFetch.mockResolvedValueOnce(
222
- mockResponse({
223
- status: 'SUCCESS',
224
- domains: [
225
- {
226
- domain: 'foo.dev',
227
- status: 'ACTIVE',
228
- tld: 'dev',
229
- createDate: '2024-01-01',
230
- expireDate: '2025-01-01',
231
- securityLock: '1',
232
- whoisPrivacy: true,
233
- autoRenew: '1',
234
- notLocal: '0',
235
- },
236
- ],
237
- }),
238
- );
239
-
240
- const domains = await service.listDomains();
241
-
242
- expect(domains).toHaveLength(1);
243
- expect(domains[0].domain).toBe('foo.dev');
244
- expect(domains[0].securityLock).toBe(true);
245
- expect(domains[0].whoisPrivacy).toBe(true);
246
- expect(domains[0].autoRenew).toBe(true);
247
- expect(domains[0].notLocal).toBe(false);
248
- });
249
-
250
- it('should return empty array when no domains', async () => {
251
- mockFetch.mockResolvedValueOnce(
252
- mockResponse({ status: 'SUCCESS' }),
253
- );
254
-
255
- const domains = await service.listDomains();
256
- expect(domains).toEqual([]);
257
- });
258
-
259
- // ── createDnsRecord() ──────────────────────────────────────────────────
260
-
261
- it('should create a DNS record and return its id', async () => {
262
- mockFetch.mockResolvedValueOnce(
263
- mockResponse({ status: 'SUCCESS', id: '12345' }),
264
- );
265
-
266
- const result = await service.createDnsRecord('example.com', {
267
- type: 'A',
268
- content: '1.2.3.4',
269
- name: 'www',
270
- });
271
-
272
- expect(result.id).toBe('12345');
273
- const [url] = mockFetch.mock.calls[0];
274
- expect(url).toContain('/dns/create/example.com');
275
- });
276
-
277
- // ── editDnsRecord() ────────────────────────────────────────────────────
278
-
279
- it('should edit an existing DNS record', async () => {
280
- mockFetch.mockResolvedValueOnce(
281
- mockResponse({ status: 'SUCCESS' }),
282
- );
283
-
284
- await service.editDnsRecord('example.com', '12345', {
285
- content: '5.6.7.8',
286
- ttl: '600',
287
- });
288
-
289
- const [url] = mockFetch.mock.calls[0];
290
- expect(url).toContain('/dns/edit/example.com/12345');
291
- const body = JSON.parse(mockFetch.mock.calls[0][1].body);
292
- expect(body.content).toBe('5.6.7.8');
293
- expect(body.ttl).toBe('600');
294
- });
295
-
296
- // ── deleteDnsRecord() ──────────────────────────────────────────────────
297
-
298
- it('should delete a DNS record', async () => {
299
- mockFetch.mockResolvedValueOnce(
300
- mockResponse({ status: 'SUCCESS' }),
301
- );
302
-
303
- await service.deleteDnsRecord('example.com', '12345');
304
-
305
- const [url] = mockFetch.mock.calls[0];
306
- expect(url).toContain('/dns/delete/example.com/12345');
307
- });
308
-
309
- // ── listDnsRecords() ──────────────────────────────────────────────────
310
-
311
- it('should list DNS records for a domain', async () => {
312
- mockFetch.mockResolvedValueOnce(
313
- mockResponse({
314
- status: 'SUCCESS',
315
- records: [
316
- { id: '1', name: 'www', type: 'A', content: '1.2.3.4', ttl: '300', prio: null, notes: null },
317
- { id: '2', name: 'mail', type: 'MX', content: 'mx.example.com', ttl: '600', prio: '10', notes: 'Primary MX' },
318
- ],
319
- }),
320
- );
321
-
322
- const records = await service.listDnsRecords('example.com');
323
-
324
- expect(records).toHaveLength(2);
325
- expect(records[0].id).toBe('1');
326
- expect(records[0].type).toBe('A');
327
- expect(records[1].prio).toBe('10');
328
- expect(records[1].notes).toBe('Primary MX');
329
- });
330
-
331
- // ── getDnsRecordsByNameType() ──────────────────────────────────────────
332
-
333
- it('should retrieve DNS records by name and type', async () => {
334
- mockFetch.mockResolvedValueOnce(
335
- mockResponse({
336
- status: 'SUCCESS',
337
- records: [
338
- { id: '3', name: 'www', type: 'A', content: '1.2.3.4', ttl: '300' },
339
- ],
340
- }),
341
- );
342
-
343
- const records = await service.getDnsRecordsByNameType('example.com', 'A', 'www');
344
-
345
- expect(records).toHaveLength(1);
346
- const [url] = mockFetch.mock.calls[0];
347
- expect(url).toContain('/dns/retrieveByNameType/example.com/A/www');
348
- });
349
-
350
- it('should use empty subdomain when not specified', async () => {
351
- mockFetch.mockResolvedValueOnce(
352
- mockResponse({ status: 'SUCCESS', records: [] }),
353
- );
354
-
355
- await service.getDnsRecordsByNameType('example.com', 'CNAME');
356
-
357
- const [url] = mockFetch.mock.calls[0];
358
- expect(url).toContain('/dns/retrieveByNameType/example.com/CNAME/');
359
- });
360
-
361
- // ── transferDomain() ───────────────────────────────────────────────────
362
-
363
- it('should initiate a domain transfer', async () => {
364
- mockFetch.mockResolvedValueOnce(
365
- mockResponse({ status: 'SUCCESS', message: 'Transfer initiated' }),
366
- );
367
-
368
- const result = await service.transferDomain('example.com', 'AUTH123');
369
-
370
- expect(result.domain).toBe('example.com');
371
- expect(result.status).toBe('SUCCESS');
372
- const body = JSON.parse(mockFetch.mock.calls[0][1].body);
373
- expect(body.domain).toBe('example.com');
374
- expect(body.authCode).toBe('AUTH123');
375
- });
376
-
377
- // ── Error handling ─────────────────────────────────────────────────────
378
-
379
- it('should throw on HTTP error with message from response', async () => {
380
- mockFetch.mockResolvedValueOnce(
381
- mockResponse({ status: 'ERROR', message: 'Unauthorized' }, 401),
382
- );
383
-
384
- await expect(service.checkAvailability('test.com')).rejects.toThrow(
385
- 'Porkbun API 401: Unauthorized',
386
- );
387
- });
388
-
389
- it('should throw on invalid JSON response', async () => {
390
- mockFetch.mockResolvedValueOnce({
391
- ok: true,
392
- status: 200,
393
- text: () => Promise.resolve('not json'),
394
- } as any);
395
-
396
- await expect(service.checkAvailability('test.com')).rejects.toThrow(
397
- 'invalid JSON',
398
- );
399
- });
400
-
401
- it('should throw on API-level ERROR status', async () => {
402
- mockFetch.mockResolvedValueOnce(
403
- mockResponse({ status: 'ERROR', message: 'Domain not found' }),
404
- );
405
-
406
- await expect(service.listDnsRecords('nonexistent.com')).rejects.toThrow(
407
- 'Porkbun API error: Domain not found',
408
- );
409
- });
410
-
411
- // ── Auth payload ──────────────────────────────────────────────────────
412
-
413
- it('should include apikey and secretapikey in every POST body', async () => {
414
- mockFetch.mockResolvedValueOnce(
415
- mockResponse({ status: 'SUCCESS', domains: [] }),
416
- );
417
-
418
- await service.listDomains();
419
-
420
- const body = JSON.parse(mockFetch.mock.calls[0][1].body);
421
- expect(body.apikey).toBe('pk_test_123');
422
- expect(body.secretapikey).toBe('sk_test_456');
423
- });
424
-
425
- it('should always use POST method', async () => {
426
- mockFetch.mockResolvedValueOnce(
427
- mockResponse({ status: 'SUCCESS', domains: [] }),
428
- );
429
-
430
- await service.listDomains();
431
-
432
- expect(mockFetch.mock.calls[0][1].method).toBe('POST');
433
- });
434
- });
@@ -1,50 +0,0 @@
1
- // @ts-nocheck
2
- import { describe, it, expect, vi, beforeEach } from 'vitest';
3
- import { createExtensionPack } from '../src/index.js';
4
-
5
- const mockFetch = vi.fn();
6
- vi.stubGlobal('fetch', mockFetch);
7
-
8
- function okResponse(data: any = {}) {
9
- return { ok: true, status: 200, text: async () => JSON.stringify({ status: 'SUCCESS', ...data }), json: async () => ({ status: 'SUCCESS', ...data }) } as Response;
10
- }
11
-
12
- describe('createExtensionPack (Porkbun)', () => {
13
- beforeEach(() => {
14
- vi.clearAllMocks();
15
- mockFetch.mockResolvedValue(okResponse({ yourIp: '1.2.3.4' }));
16
- });
17
-
18
- it('should return a valid ExtensionPack', () => {
19
- const pack = createExtensionPack({ secrets: { 'porkbun.apiKey': 'ak', 'porkbun.secretApiKey': 'sak' } });
20
- expect(pack.name).toBe('@framers/agentos-ext-domain-porkbun');
21
- expect(pack.version).toBe('0.1.0');
22
- expect(pack.descriptors).toHaveLength(5);
23
- });
24
-
25
- it('should register correct tool IDs', () => {
26
- const pack = createExtensionPack({ secrets: { 'porkbun.apiKey': 'ak', 'porkbun.secretApiKey': 'sak' } });
27
- const ids = pack.descriptors.map((d) => d.id);
28
- expect(ids).toContain('porkbunSearchDomain');
29
- expect(ids).toContain('porkbunRegisterDomain');
30
- expect(ids).toContain('porkbunListDomains');
31
- expect(ids).toContain('porkbunConfigureDns');
32
- expect(ids).toContain('porkbunTransferDomain');
33
- });
34
-
35
- it('onActivate should throw without credentials', async () => {
36
- const pack = createExtensionPack({ secrets: {} });
37
- await expect(pack.onActivate!()).rejects.toThrow();
38
- });
39
-
40
- it('onActivate should succeed with valid credentials', async () => {
41
- const pack = createExtensionPack({ secrets: { 'porkbun.apiKey': 'ak', 'porkbun.secretApiKey': 'sak' } });
42
- await expect(pack.onActivate!()).resolves.toBeUndefined();
43
- });
44
-
45
- it('onDeactivate should complete', async () => {
46
- const pack = createExtensionPack({ secrets: { 'porkbun.apiKey': 'ak', 'porkbun.secretApiKey': 'sak' } });
47
- await pack.onActivate!();
48
- await expect(pack.onDeactivate!()).resolves.toBeUndefined();
49
- });
50
- });