@framers/agentos-ext-domain-porkbun 0.1.0
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/LICENSE +98 -0
- package/dist/PorkbunService.d.ts +119 -0
- package/dist/PorkbunService.d.ts.map +1 -0
- package/dist/PorkbunService.js +224 -0
- package/dist/PorkbunService.js.map +1 -0
- package/dist/index.d.ts +43 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +76 -0
- package/dist/index.js.map +1 -0
- package/dist/tools/configureDns.d.ts +75 -0
- package/dist/tools/configureDns.d.ts.map +1 -0
- package/dist/tools/configureDns.js +91 -0
- package/dist/tools/configureDns.js.map +1 -0
- package/dist/tools/listDomains.d.ts +26 -0
- package/dist/tools/listDomains.d.ts.map +1 -0
- package/dist/tools/listDomains.js +32 -0
- package/dist/tools/listDomains.js.map +1 -0
- package/dist/tools/registerDomain.d.ts +89 -0
- package/dist/tools/registerDomain.d.ts.map +1 -0
- package/dist/tools/registerDomain.js +58 -0
- package/dist/tools/registerDomain.js.map +1 -0
- package/dist/tools/searchDomain.d.ts +34 -0
- package/dist/tools/searchDomain.d.ts.map +1 -0
- package/dist/tools/searchDomain.js +34 -0
- package/dist/tools/searchDomain.js.map +1 -0
- package/dist/tools/transferDomain.d.ts +39 -0
- package/dist/tools/transferDomain.d.ts.map +1 -0
- package/dist/tools/transferDomain.js +35 -0
- package/dist/tools/transferDomain.js.map +1 -0
- package/manifest.json +33 -0
- package/package.json +65 -0
- package/src/PorkbunService.ts +327 -0
- package/src/index.ts +133 -0
- package/src/tools/configureDns.ts +114 -0
- package/src/tools/listDomains.ts +33 -0
- package/src/tools/registerDomain.ts +74 -0
- package/src/tools/searchDomain.ts +39 -0
- package/src/tools/transferDomain.ts +41 -0
- package/test/PorkbunService.test.ts +434 -0
- package/test/index.test.ts +50 -0
- package/test/tools.test.ts +399 -0
- package/tsconfig.json +22 -0
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
/**
|
|
3
|
+
* @fileoverview PorkbunRegisterDomainTool — register (purchase) a domain through Porkbun.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { PorkbunService, RegisterDomainResult } from '../PorkbunService.js';
|
|
7
|
+
|
|
8
|
+
export interface RegisterDomainInput {
|
|
9
|
+
domain: string;
|
|
10
|
+
years?: number;
|
|
11
|
+
firstName?: string;
|
|
12
|
+
lastName?: string;
|
|
13
|
+
email?: string;
|
|
14
|
+
phone?: string;
|
|
15
|
+
city?: string;
|
|
16
|
+
state?: string;
|
|
17
|
+
zip?: string;
|
|
18
|
+
country?: string;
|
|
19
|
+
organization?: string;
|
|
20
|
+
address?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export class PorkbunRegisterDomainTool {
|
|
24
|
+
readonly id = 'porkbunRegisterDomain';
|
|
25
|
+
readonly name = 'porkbunRegisterDomain';
|
|
26
|
+
readonly displayName = 'Register Domain';
|
|
27
|
+
readonly description = 'Register and purchase a domain through Porkbun. WARNING: This is a REAL PURCHASE that will charge the payment method on file in the connected Porkbun account. The domain will be registered for the specified number of years (default: 1). Contact fields are optional — Porkbun uses account defaults if omitted.';
|
|
28
|
+
readonly category = 'domain';
|
|
29
|
+
readonly version = '0.1.0';
|
|
30
|
+
readonly hasSideEffects = true;
|
|
31
|
+
|
|
32
|
+
readonly inputSchema = {
|
|
33
|
+
type: 'object' as const,
|
|
34
|
+
properties: {
|
|
35
|
+
domain: { type: 'string', description: 'The domain name to register (e.g. "example.com")' },
|
|
36
|
+
years: { type: 'number', description: 'Number of years to register (default: 1, max: 10)' },
|
|
37
|
+
firstName: { type: 'string', description: 'Registrant first name (uses account default if omitted)' },
|
|
38
|
+
lastName: { type: 'string', description: 'Registrant last name (uses account default if omitted)' },
|
|
39
|
+
email: { type: 'string', description: 'Registrant email (uses account default if omitted)' },
|
|
40
|
+
phone: { type: 'string', description: 'Registrant phone number (uses account default if omitted)' },
|
|
41
|
+
city: { type: 'string', description: 'Registrant city (uses account default if omitted)' },
|
|
42
|
+
state: { type: 'string', description: 'Registrant state/province (uses account default if omitted)' },
|
|
43
|
+
zip: { type: 'string', description: 'Registrant postal code (uses account default if omitted)' },
|
|
44
|
+
country: { type: 'string', description: 'Registrant country code, e.g. "US" (uses account default if omitted)' },
|
|
45
|
+
organization: { type: 'string', description: 'Registrant organization (optional)' },
|
|
46
|
+
address: { type: 'string', description: 'Registrant street address (uses account default if omitted)' },
|
|
47
|
+
},
|
|
48
|
+
required: ['domain'],
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
constructor(private service: PorkbunService) {}
|
|
52
|
+
|
|
53
|
+
async execute(args: RegisterDomainInput): Promise<{ success: boolean; data?: RegisterDomainResult; error?: string }> {
|
|
54
|
+
try {
|
|
55
|
+
const result = await this.service.registerDomain({
|
|
56
|
+
domain: args.domain,
|
|
57
|
+
years: args.years,
|
|
58
|
+
firstName: args.firstName,
|
|
59
|
+
lastName: args.lastName,
|
|
60
|
+
email: args.email,
|
|
61
|
+
phone: args.phone,
|
|
62
|
+
city: args.city,
|
|
63
|
+
state: args.state,
|
|
64
|
+
zip: args.zip,
|
|
65
|
+
country: args.country,
|
|
66
|
+
organization: args.organization,
|
|
67
|
+
address: args.address,
|
|
68
|
+
});
|
|
69
|
+
return { success: true, data: result };
|
|
70
|
+
} catch (err: any) {
|
|
71
|
+
return { success: false, error: err.message };
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
/**
|
|
3
|
+
* @fileoverview PorkbunSearchDomainTool — check domain availability and get pricing.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { PorkbunService, DomainAvailability } from '../PorkbunService.js';
|
|
7
|
+
|
|
8
|
+
export interface SearchDomainInput {
|
|
9
|
+
domain: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export class PorkbunSearchDomainTool {
|
|
13
|
+
readonly id = 'porkbunSearchDomain';
|
|
14
|
+
readonly name = 'porkbunSearchDomain';
|
|
15
|
+
readonly displayName = 'Search Domain Availability';
|
|
16
|
+
readonly description = 'Check if a domain name is available for registration on Porkbun and retrieve pricing information. Provide the full domain including TLD (e.g. "example.com", "mysite.dev").';
|
|
17
|
+
readonly category = 'domain';
|
|
18
|
+
readonly version = '0.1.0';
|
|
19
|
+
readonly hasSideEffects = false;
|
|
20
|
+
|
|
21
|
+
readonly inputSchema = {
|
|
22
|
+
type: 'object' as const,
|
|
23
|
+
properties: {
|
|
24
|
+
domain: { type: 'string', description: 'The domain name to check (e.g. "example.com", "coolsite.dev")' },
|
|
25
|
+
},
|
|
26
|
+
required: ['domain'],
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
constructor(private service: PorkbunService) {}
|
|
30
|
+
|
|
31
|
+
async execute(args: SearchDomainInput): Promise<{ success: boolean; data?: DomainAvailability; error?: string }> {
|
|
32
|
+
try {
|
|
33
|
+
const result = await this.service.checkAvailability(args.domain);
|
|
34
|
+
return { success: true, data: result };
|
|
35
|
+
} catch (err: any) {
|
|
36
|
+
return { success: false, error: err.message };
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
/**
|
|
3
|
+
* @fileoverview PorkbunTransferDomainTool — initiate a domain transfer to Porkbun.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { PorkbunService, TransferDomainResult } from '../PorkbunService.js';
|
|
7
|
+
|
|
8
|
+
export interface TransferDomainInput {
|
|
9
|
+
domain: string;
|
|
10
|
+
authCode: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export class PorkbunTransferDomainTool {
|
|
14
|
+
readonly id = 'porkbunTransferDomain';
|
|
15
|
+
readonly name = 'porkbunTransferDomain';
|
|
16
|
+
readonly displayName = 'Transfer Domain';
|
|
17
|
+
readonly description = 'Initiate a domain transfer to Porkbun from another registrar. Requires the domain name and the authorization/EPP code from the current registrar. The domain must be unlocked at the current registrar before transferring.';
|
|
18
|
+
readonly category = 'domain';
|
|
19
|
+
readonly version = '0.1.0';
|
|
20
|
+
readonly hasSideEffects = true;
|
|
21
|
+
|
|
22
|
+
readonly inputSchema = {
|
|
23
|
+
type: 'object' as const,
|
|
24
|
+
properties: {
|
|
25
|
+
domain: { type: 'string', description: 'The domain name to transfer (e.g. "example.com")' },
|
|
26
|
+
authCode: { type: 'string', description: 'The authorization/EPP code from the current registrar (required to authorize the transfer)' },
|
|
27
|
+
},
|
|
28
|
+
required: ['domain', 'authCode'],
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
constructor(private service: PorkbunService) {}
|
|
32
|
+
|
|
33
|
+
async execute(args: TransferDomainInput): Promise<{ success: boolean; data?: TransferDomainResult; error?: string }> {
|
|
34
|
+
try {
|
|
35
|
+
const result = await this.service.transferDomain(args.domain, args.authCode);
|
|
36
|
+
return { success: true, data: result };
|
|
37
|
+
} catch (err: any) {
|
|
38
|
+
return { success: false, error: err.message };
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,434 @@
|
|
|
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
|
+
});
|
|
@@ -0,0 +1,50 @@
|
|
|
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
|
+
});
|