From 43e4073d4241aae30d02fd9d7b27286573e942b6 Mon Sep 17 00:00:00 2001 From: Jason Mulligan Date: Sun, 23 Aug 2026 13:52:32 -0400 Subject: [PATCH 1/3] feat: add name.com API tool specs --- .../add-namecom-api-tool/.openspec.yaml | 2 + .../changes/add-namecom-api-tool/design.md | 55 +++++ .../changes/add-namecom-api-tool/proposal.md | 35 +++ .../specs/namecom-api/spec.md | 220 ++++++++++++++++++ .../changes/add-namecom-api-tool/tasks.md | 149 ++++++++++++ 5 files changed, 461 insertions(+) create mode 100644 openspec/changes/add-namecom-api-tool/.openspec.yaml create mode 100644 openspec/changes/add-namecom-api-tool/design.md create mode 100644 openspec/changes/add-namecom-api-tool/proposal.md create mode 100644 openspec/changes/add-namecom-api-tool/specs/namecom-api/spec.md create mode 100644 openspec/changes/add-namecom-api-tool/tasks.md diff --git a/openspec/changes/add-namecom-api-tool/.openspec.yaml b/openspec/changes/add-namecom-api-tool/.openspec.yaml new file mode 100644 index 00000000..44f55ffe --- /dev/null +++ b/openspec/changes/add-namecom-api-tool/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-08-23 diff --git a/openspec/changes/add-namecom-api-tool/design.md b/openspec/changes/add-namecom-api-tool/design.md new file mode 100644 index 00000000..c9a36c8f --- /dev/null +++ b/openspec/changes/add-namecom-api-tool/design.md @@ -0,0 +1,55 @@ +## Context + +The madz harness uses a tool-based architecture where each tool is a plain async function with a Zod input schema, registered in `src/tools/index.js`. Action-based tools (like `email` and `process`) use a single tool with an `action` enum parameter that dispatches to handler functions. The name.com API is a RESTful API with 72 operations across 17 tag groups, using Basic Auth with username:token, rate-limited to 20 requests/second. + +## Goals / Non-Goals + +**Goals:** +- Single `namecom` tool wrapping all 72 name.com Core API operations +- Action-based dispatch pattern matching existing tools +- Basic Auth via `NAMECOM_USERNAME` and `NAMECOM_TOKEN` env vars +- URL allowlist validation for outbound requests +- Consistent error handling for API errors (401, 403, 429, 500, 502, 503, 504) +- Full test coverage across all 17 tag groups + +**Non-Goals:** +- OAuth authentication flows +- Caching layer for API responses +- Request queue/bulk operation throttling (rate limit is 20 req/s, generous for most use cases) +- Domain parking or transfer-out flows not covered by the API spec + +## Decisions + +### Decision 1: Single tool with action enum vs. separate tools per tag group +**Choice:** Single tool with `action` parameter. +**Rationale:** 17+ separate tools would bloat the tool surface. The `email` and `process` tools demonstrate this pattern works well. The LLM can reason about a single tool with 40+ actions more effectively than 17 separate tools. +**Alternatives considered:** Separate tools per tag group (rejected — too many tools, fragmented tool surface). + +### Decision 2: Zod schema design — flat optional fields vs. params record +**Choice:** Common parameters as optional fields (`domainName`, `perPage`, `page`, `type`, `name`, `value`, `ttl`), action-specific parameters via a flexible `params` record. +**Rationale:** The API has 72 operations with varying parameters. A flat schema with 100+ optional fields is unwieldy. A `params` record keeps the schema manageable while still providing type safety for common parameters. +**Alternatives considered:** Flat schema with all parameters as optional fields (rejected — too large, hard to maintain). + +### Decision 3: Native fetch vs. HTTP library +**Choice:** Native `fetch()` (Node.js 24+). +**Rationale:** No external dependencies needed. The API is simple REST — no need for axios or node-fetch. Basic Auth is trivial with `fetch` headers. +**Alternatives considered:** axios (rejected — adds dependency for no benefit). + +### Decision 4: URL allowlist enforcement +**Choice:** Hardcoded allowlist of `api.name.com` and `api.dev.name.com` in the HTTP client. +**Rationale:** OWASP compliance — prevents SSRF via user-controlled URLs. The API base URL is fixed, so a hardcoded allowlist is appropriate. +**Alternatives considered:** Configurable base URL (rejected — unnecessary flexibility, security risk). + +## Risks / Trade-offs + +- **Rate limiting:** 20 req/s limit means bulk operations need throttling. Mitigation: Document the limit in the tool description, return guidance on 429 responses. Full queue implementation deferred. +- **Large Zod schema:** 40+ actions with varying parameters. Mitigation: `params` record for action-specific fields, common parameters as optional fields. +- **Error handling:** API returns varying response shapes for different error codes. Mitigation: Consistent error wrapper in `makeRequest()` that normalizes all errors to `{ ok: false, error: string }`. + +## Migration Plan + +No migration needed — this is a greenfield feature. The tool is registered alongside existing tools and requires no config.yaml changes. + +## Open Questions + +- None. The action map, auth design, and schema approach are defined by the issue specification. \ No newline at end of file diff --git a/openspec/changes/add-namecom-api-tool/proposal.md b/openspec/changes/add-namecom-api-tool/proposal.md new file mode 100644 index 00000000..870f339b --- /dev/null +++ b/openspec/changes/add-namecom-api-tool/proposal.md @@ -0,0 +1,35 @@ +## Why + +Users need to manage domains, DNS records, transfers, and related services through name.com's API. Currently there is no tool for this in the harness. A single unified tool with an `action` parameter keeps the tool surface clean while providing full API coverage. + +## What Changes + +- Add a new `namecom` tool wrapping 72 name.com Core API operations across 17 tag groups +- Authentication via `NAMECOM_USERNAME` and `NAMECOM_TOKEN` environment variables (Basic Auth) +- Action-based dispatch pattern matching existing `email` and `process` tools +- HTTP client with URL allowlist validation, rate limit handling, and consistent error responses +- Register tool in `src/tools/index.js` with `network:outbound` permission + +## Capabilities + +### New Capabilities + +- `namecom-api`: Full name.com Core API integration — domain management, DNS records, transfers, email/URL forwarding, vanity nameservers, DNSSEC, webhook notifications, orders, refunds, TLD pricing, premium domains, contact verification, and account info + +### Modified Capabilities + +- None + +## Impact + +- **Affected code**: `src/tools/index.js` (tool registration), new file `src/tools/namecom/index.js` +- **New dependencies**: None — uses native `fetch()` and existing Zod +- **Tests**: New file `tests/unit/tools/namecom.test.js` +- **Config**: No config.yaml changes — credentials from env vars only + +## Non-goals + +- OAuth authentication flows +- Reseller account management beyond what the API provides +- Domain parking or transfer-out flows not covered by the API spec +- Caching layer for API responses \ No newline at end of file diff --git a/openspec/changes/add-namecom-api-tool/specs/namecom-api/spec.md b/openspec/changes/add-namecom-api-tool/specs/namecom-api/spec.md new file mode 100644 index 00000000..7a6be514 --- /dev/null +++ b/openspec/changes/add-namecom-api-tool/specs/namecom-api/spec.md @@ -0,0 +1,220 @@ +## ADDED Requirements + +### Requirement: Tool authentication +The system MUST authenticate all name.com API requests using Basic Auth with credentials from `NAMECOM_USERNAME` and `NAMECOM_TOKEN` environment variables. The system MUST reject requests when credentials are not configured. + +#### Scenario: Authentication configured +- **WHEN** `NAMECOM_USERNAME` and `NAMECOM_TOKEN` are set in the environment +- **THEN** the tool includes `Authorization: Basic ` header on all API requests + +#### Scenario: Authentication not configured +- **WHEN** `NAMECOM_USERNAME` or `NAMECOM_TOKEN` is not set +- **THEN** the tool returns `{ ok: false, error: "name.com credentials not configured" }` for all actions + +### Requirement: URL allowlist validation +The system MUST restrict all outbound requests to `api.name.com` and `api.dev.name.com` only. + +#### Scenario: Valid host +- **WHEN** the request target is `api.name.com` or `api.dev.name.com` +- **THEN** the request proceeds normally + +#### Scenario: Invalid host +- **WHEN** the request target is not in the allowlist +- **THEN** the tool returns `{ ok: false, error: "Host not allowed" }` + +### Requirement: Action dispatch +The system MUST route each `action` value to the correct API endpoint using a switch statement. + +#### Scenario: Valid action +- **WHEN** the `action` field matches a known action (e.g., `listDomains`, `createRecord`) +- **THEN** the tool dispatches to the corresponding handler and makes the appropriate API request + +#### Scenario: Unknown action +- **WHEN** the `action` field does not match any known action +- **THEN** the tool returns `{ ok: false, error: "Unknown action: ..." }` + +### Requirement: Domain management actions +The system MUST support all 19 domain operations: listDomains, createDomain, getDomain, updateDomain, enableAutorenew, disableAutorenew, enableWhoisPrivacy, disableWhoisPrivacy, lockDomain, unlockDomain, renewDomain, setContacts, setNameservers, getAuthCode, getPricing, checkAvailability, searchDomains, zoneCheck, purchasePrivacy. + +#### Scenario: List domains +- **WHEN** action is `listDomains` with optional `perPage` and `page` parameters +- **THEN** the tool calls `GET /core/v1/domains` and returns the domain list + +#### Scenario: Create domain +- **WHEN** action is `createDomain` with domain registration parameters +- **THEN** the tool calls `POST /core/v1/domains` and returns the registration result + +#### Scenario: Enable autorenew +- **WHEN** action is `enableAutorenew` with `domainName` parameter +- **THEN** the tool calls `POST /core/v1/domains/{domainName}:enableAutorenew` and returns success + +### Requirement: DNS record management +The system MUST support all 5 DNS operations: listRecords, createRecord, getRecord, updateRecord, deleteRecord. + +#### Scenario: List DNS records +- **WHEN** action is `listRecords` with `domainName` parameter +- **THEN** the tool calls `GET /core/v1/domains/{domainName}/records` and returns the record list + +#### Scenario: Create DNS record +- **WHEN** action is `createRecord` with `domainName`, `type`, `name`, `value`, and `ttl` parameters +- **THEN** the tool calls `POST /core/v1/domains/{domainName}/records` and returns the created record + +#### Scenario: Delete DNS record +- **WHEN** action is `deleteRecord` with `domainName` and `id` parameters +- **THEN** the tool calls `DELETE /core/v1/domains/{domainName}/records/{id}` and returns success + +### Requirement: URL forwarding management +The system MUST support all 9 URL forwarding operations: listUrlForwardings, createUrlForwarding, getUrlForwarding, updateUrlForwarding, deleteUrlForwarding, listUrlForwardingsByDomain, getUrlForwardingById, updateUrlForwardingById, deleteUrlForwardingById. + +#### Scenario: List URL forwardings +- **WHEN** action is `listUrlForwardings` with `domainName` parameter +- **THEN** the tool calls `GET /core/v1/domains/{domainName}/url/forwarding` and returns the forwarding list + +### Requirement: Email forwarding management +The system MUST support all 5 email forwarding operations: listEmailForwardings, createEmailForwarding, getEmailForwarding, updateEmailForwarding, deleteEmailForwarding. + +#### Scenario: Create email forwarding +- **WHEN** action is `createEmailForwarding` with `domainName` and forwarding parameters +- **THEN** the tool calls `POST /core/v1/domains/{domainName}/email/forwarding` and returns the created forwarding + +### Requirement: Vanity nameserver management +The system MUST support all 5 vanity nameserver operations: listVanityNameservers, createVanityNameserver, getVanityNameserver, updateVanityNameserver, deleteVanityNameserver. + +#### Scenario: List vanity nameservers +- **WHEN** action is `listVanityNameservers` with `domainName` parameter +- **THEN** the tool calls `GET /core/v1/domains/{domainName}/vanity_nameservers` and returns the list + +### Requirement: DNSSEC management +The system MUST support all 4 DNSSEC operations: listDnssecs, createDnssec, getDnssec, deleteDnssec. + +#### Scenario: List DNSSEC records +- **WHEN** action is `listDnssecs` with `domainName` parameter +- **THEN** the tool calls `GET /core/v1/domains/{domainName}/dnssec` and returns the DNSSEC list + +### Requirement: Transfer management +The system MUST support all 7 transfer operations: listTransfers, createTransfer, getTransfer, cancelTransfer, cancelExternalTransferOut, createInternalTransferIn, getTransferEligibility. + +#### Scenario: List transfers +- **WHEN** action is `listTransfers` with optional pagination parameters +- **THEN** the tool calls `GET /core/v1/transfers` and returns the transfer list + +#### Scenario: Create transfer +- **WHEN** action is `createTransfer` with domain and auth code parameters +- **THEN** the tool calls `POST /core/v1/transfers` and returns the transfer result + +### Requirement: Webhook notification management +The system MUST support all 4 webhook notification operations: listNotifications, subscribeNotification, getNotification, modifyNotification, deleteNotification. + +#### Scenario: List notifications +- **WHEN** action is `listNotifications` +- **THEN** the tool calls `GET /core/v1/notifications` and returns the subscription list + +### Requirement: Domain info operations +The system MUST support all 3 domain info operations: getTldRequirements, checkDomainClaims, getTldRequirementsV2. + +#### Scenario: Check domain claims +- **WHEN** action is `checkDomainClaims` with `domain` parameter +- **THEN** the tool calls `POST /core/v1/domaininfo/claims/{domain}` and returns the claims result + +### Requirement: Contact verification operations +The system MUST support all 3 contact verification operations: listUnverifiedContacts, verifyContact, resendContactVerification. + +#### Scenario: List unverified contacts +- **WHEN** action is `listUnverifiedContacts` +- **THEN** the tool calls `GET /core/v1/contacts/unverified` and returns the unverified list + +### Requirement: Orders operations +The system MUST support all 2 order operations: listOrders, getOrder. + +#### Scenario: List orders +- **WHEN** action is `listOrders` with optional pagination parameters +- **THEN** the tool calls `GET /core/v1/orders` and returns the order list + +### Requirement: Account info operations +The system MUST support the account balance operation: getAccountBalance. + +#### Scenario: Get account balance +- **WHEN** action is `getAccountBalance` +- **THEN** the tool calls `GET /core/v1/accountinfo/balance` and returns the balance + +### Requirement: Hello endpoint +The system MUST support the hello operation for health checking. + +#### Scenario: Hello +- **WHEN** action is `hello` +- **THEN** the tool calls `GET /core/v1/hello` and returns the server time and version info + +### Requirement: Refund operations +The system MUST support the refund operation: processRefund. + +#### Scenario: Process refund +- **WHEN** action is `processRefund` with order item parameters +- **THEN** the tool calls `POST /core/v1/refund` and returns the refund result + +### Requirement: TLD pricing operations +The system MUST support the TLD pricing operation: getTldPricing. + +#### Scenario: Get TLD pricing +- **WHEN** action is `getTldPricing` +- **THEN** the tool calls `GET /core/v1/tldpricing` and returns the pricing list + +### Requirement: Premium domains operations +The system MUST support the premium domains operation: getPremiumDomainsList. + +#### Scenario: Get premium domains list +- **WHEN** action is `getPremiumDomainsList` +- **THEN** the tool calls `GET /core/v1/premiumdomainslist` and returns the premium list + +### Requirement: Accounts operations +The system MUST support the account creation operation: createAccount. + +#### Scenario: Create account +- **WHEN** action is `createAccount` with account parameters +- **THEN** the tool calls `POST /core/v1/accounts` and returns the created account details + +### Requirement: Error handling +The system MUST handle API error responses consistently, returning `{ ok: false, error: string }` for all error cases. + +#### Scenario: 401 Unauthorized +- **WHEN** the API returns 401 +- **THEN** the tool returns `{ ok: false, error: "Authentication failed" }` + +#### Scenario: 429 Rate Limit +- **WHEN** the API returns 429 +- **THEN** the tool returns `{ ok: false, error: "Rate limit exceeded. Retry after " }` + +#### Scenario: 503 Service Unavailable +- **WHEN** the API returns 503 +- **THEN** the tool returns `{ ok: false, error: "Service unavailable. See https://status.name.com" }` + +#### Scenario: Network error +- **WHEN** the HTTP request fails (timeout, connection refused) +- **THEN** the tool returns `{ ok: false, error: "Request failed: " }` + +### Requirement: Rate limit awareness +The system MUST parse the `X-RateLimit-Reset` header from 429 responses and include the retry timestamp in the error message. + +#### Scenario: Rate limit with reset header +- **WHEN** the API returns 429 with `X-RateLimit-Reset` header +- **THEN** the error message includes the Unix timestamp for when the rate limit resets + +### Requirement: Tool registration +The system MUST register the `namecom` tool in `src/tools/index.js` with `network:outbound` permission and appropriate agent classifications. + +#### Scenario: Tool is registered +- **WHEN** the system starts +- **THEN** the `namecom` tool is available to agents with `network:outbound` permission + +### Requirement: Zod schema validation +The system MUST validate all tool input against a Zod schema before dispatching to handlers. + +#### Scenario: Missing required parameter +- **WHEN** a required parameter (e.g., `domainName` for domain actions) is missing +- **THEN** Zod validation rejects the input before the handler is called + +### Requirement: HTTP client with timeout +The system MUST attach a timeout to all HTTP requests to prevent hanging. + +#### Scenario: Request timeout +- **WHEN** an HTTP request exceeds the timeout (30 seconds) +- **THEN** the tool returns `{ ok: false, error: "Request timed out" }` \ No newline at end of file diff --git a/openspec/changes/add-namecom-api-tool/tasks.md b/openspec/changes/add-namecom-api-tool/tasks.md new file mode 100644 index 00000000..70a22f4d --- /dev/null +++ b/openspec/changes/add-namecom-api-tool/tasks.md @@ -0,0 +1,149 @@ +## 1. HTTP client and auth layer + +- [ ] 1.1 Create `src/tools/namecom/index.js` with the `makeRequest()` helper — handles Basic Auth, URL allowlist validation, timeout, and error normalization +- [ ] 1.2 Implement credential validation — check `NAMECOM_USERNAME` and `NAMECOM_TOKEN` env vars, return early with error if missing +- [ ] 1.3 Implement URL allowlist — only allow `api.name.com` and `api.dev.name.com` + +## 2. Zod schema definition + +- [ ] 2.1 Define the `NamecomToolSchema` with `action` as an enum of all 40+ action names +- [ ] 2.2 Add common optional fields: `domainName`, `perPage`, `page`, `sort`, `dir`, `type`, `name`, `value`, `ttl`, `id` +- [ ] 2.3 Add action-specific optional fields: `to`, `subject`, `body`, `startDate`, `endDate`, `title`, `start`, `end`, `eventId`, `domain`, `tld`, `verificationId`, `authCode`, `contact`, `nameserver`, `hostname`, `emailBox`, `host`, `url`, `orderId`, `productType`, `idempotencyKey` +- [ ] 2.4 Add `params` as an optional record for action-specific parameters not covered by named fields + +## 3. Action handlers — Domains (19 ops) + +- [ ] 3.1 Implement `listDomains` — GET /core/v1/domains with pagination and filter params +- [ ] 3.2 Implement `createDomain` — POST /core/v1/domains with registration details +- [ ] 3.3 Implement `getDomain` — GET /core/v1/domains/{domainName} +- [ ] 3.4 Implement `updateDomain` — PATCH /core/v1/domains/{domainName} +- [ ] 3.5 Implement `enableAutorenew` — POST /core/v1/domains/{domainName}:enableAutorenew +- [ ] 3.6 Implement `disableAutorenew` — POST /core/v1/domains/{domainName}:disableAutorenew +- [ ] 3.7 Implement `enableWhoisPrivacy` — POST /core/v1/domains/{domainName}:enableWhoisPrivacy +- [ ] 3.8 Implement `disableWhoisPrivacy` — POST /core/v1/domains/{domainName}:disableWhoisPrivacy +- [ ] 3.9 Implement `lockDomain` — POST /core/v1/domains/{domainName}:lock +- [ ] 3.10 Implement `unlockDomain` — POST /core/v1/domains/{domainName}:unlock +- [ ] 3.11 Implement `renewDomain` — POST /core/v1/domains/{domainName}:renew +- [ ] 3.12 Implement `setContacts` — POST /core/v1/domains/{domainName}:setContacts +- [ ] 3.13 Implement `setNameservers` — POST /core/v1/domains/{domainName}:setNameservers +- [ ] 3.14 Implement `getAuthCode` — GET /core/v1/domains/{domainName}:getAuthCode +- [ ] 3.15 Implement `getPricing` — GET /core/v1/domains/{domainName}:getPricing +- [ ] 3.16 Implement `checkAvailability` — POST /core/v1/domains:checkAvailability +- [ ] 3.17 Implement `searchDomains` — POST /core/v1/domains:search +- [ ] 3.18 Implement `zoneCheck` — POST /core/v1/zonecheck +- [ ] 3.19 Implement `purchasePrivacy` — POST /core/v1/domains/{domainName}:purchasePrivacy + +## 4. Action handlers — DNS (5 ops) + +- [ ] 4.1 Implement `listRecords` — GET /core/v1/domains/{domainName}/records +- [ ] 4.2 Implement `createRecord` — POST /core/v1/domains/{domainName}/records +- [ ] 4.3 Implement `getRecord` — GET /core/v1/domains/{domainName}/records/{id} +- [ ] 4.4 Implement `updateRecord` — PUT /core/v1/domains/{domainName}/records/{id} +- [ ] 4.5 Implement `deleteRecord` — DELETE /core/v1/domains/{domainName}/records/{id} + +## 5. Action handlers — URL Forwardings (9 ops) + +- [ ] 5.1 Implement `listUrlForwardings` — GET /core/v1/domains/{domainName}/url/forwarding +- [ ] 5.2 Implement `createUrlForwarding` — POST /core/v1/domains/{domainName}/url/forwarding +- [ ] 5.3 Implement `getUrlForwarding` — GET /core/v1/domains/{domainName}/url/forwarding/{host} +- [ ] 5.4 Implement `updateUrlForwarding` — PUT /core/v1/domains/{domainName}/url/forwarding/{host} +- [ ] 5.5 Implement `deleteUrlForwarding` — DELETE /core/v1/domains/{domainName}/url/forwarding/{host} +- [ ] 5.6 Implement `listUrlForwardingsByDomain` — GET /core/v1/urlforwarding/{domainName} +- [ ] 5.7 Implement `getUrlForwardingById` — GET /core/v1/urlforwarding/{domainName}/{id} +- [ ] 5.8 Implement `updateUrlForwardingById` — PATCH /core/v1/urlforwarding/{domainName}/{id} +- [ ] 5.9 Implement `deleteUrlForwardingById` — DELETE /core/v1/urlforwarding/{domainName}/{id} + +## 6. Action handlers — Email Forwardings (5 ops) + +- [ ] 6.1 Implement `listEmailForwardings` — GET /core/v1/domains/{domainName}/email/forwarding +- [ ] 6.2 Implement `createEmailForwarding` — POST /core/v1/domains/{domainName}/email/forwarding +- [ ] 6.3 Implement `getEmailForwarding` — GET /core/v1/domains/{domainName}/email/forwarding/{emailBox} +- [ ] 6.4 Implement `updateEmailForwarding` — PUT /core/v1/domains/{domainName}/email/forwarding/{emailBox} +- [ ] 6.5 Implement `deleteEmailForwarding` — DELETE /core/v1/domains/{domainName}/email/forwarding/{emailBox} + +## 7. Action handlers — Vanity Nameservers (5 ops) + +- [ ] 7.1 Implement `listVanityNameservers` — GET /core/v1/domains/{domainName}/vanity_nameservers +- [ ] 7.2 Implement `createVanityNameserver` — POST /core/v1/domains/{domainName}/vanity_nameservers +- [ ] 7.3 Implement `getVanityNameserver` — GET /core/v1/domains/{domainName}/vanity_nameservers/{hostname} +- [ ] 7.4 Implement `updateVanityNameserver` — PUT /core/v1/domains/{domainName}/vanity_nameservers/{hostname} +- [ ] 7.5 Implement `deleteVanityNameserver` — DELETE /core/v1/domains/{domainName}/vanity_nameservers/{hostname} + +## 8. Action handlers — DNSSECs (4 ops) + +- [ ] 8.1 Implement `listDnssecs` — GET /core/v1/domains/{domainName}/dnssec +- [ ] 8.2 Implement `createDnssec` — POST /core/v1/domains/{domainName}/dnssec +- [ ] 8.3 Implement `getDnssec` — GET /core/v1/domains/{domainName}/dnssec/{digest} +- [ ] 8.4 Implement `deleteDnssec` — DELETE /core/v1/domains/{domainName}/dnssec/{digest} + +## 9. Action handlers — Transfers (7 ops) + +- [ ] 9.1 Implement `listTransfers` — GET /core/v1/transfers +- [ ] 9.2 Implement `createTransfer` — POST /core/v1/transfers +- [ ] 9.3 Implement `getTransfer` — GET /core/v1/transfers/{domainName} +- [ ] 9.4 Implement `cancelTransfer` — POST /core/v1/transfers/{domainName}:cancel +- [ ] 9.5 Implement `cancelExternalTransferOut` — POST /core/v1/transfers/external/out/{domainName}:cancel +- [ ] 9.6 Implement `createInternalTransferIn` — POST /core/v1/transfers/internal/in +- [ ] 9.7 Implement `getTransferEligibility` — GET /core/v1/transfers/eligibility/{domainName} + +## 10. Action handlers — Webhook Notifications (4 ops) + +- [ ] 10.1 Implement `listNotifications` — GET /core/v1/notifications +- [ ] 10.2 Implement `subscribeNotification` — POST /core/v1/notifications +- [ ] 10.3 Implement `deleteNotification` — DELETE /core/v1/notifications/{id} +- [ ] 10.4 Implement `modifyNotification` — PUT /core/v1/notifications/{id} + +## 11. Action handlers — Domain Info (3 ops) + +- [ ] 11.1 Implement `getTldRequirements` — GET /core/v1/domaininfo/requirements/{tld} +- [ ] 11.2 Implement `checkDomainClaims` — POST /core/v1/domaininfo/claims/{domain} +- [ ] 11.3 Implement `getTldRequirementsV2` — GET /core/v1/domaininfo/requirementsV2/{tld} + +## 12. Action handlers — Contact Verification (3 ops) + +- [ ] 12.1 Implement `listUnverifiedContacts` — GET /core/v1/contacts/unverified +- [ ] 12.2 Implement `verifyContact` — POST /core/v1/contacts/verify/{verificationId} +- [ ] 12.3 Implement `resendContactVerification` — POST /core/v1/contacts/verify/{verificationId}:resend + +## 13. Action handlers — Orders, Account, Refunds, Pricing (6 ops) + +- [ ] 13.1 Implement `listOrders` — GET /core/v1/orders +- [ ] 13.2 Implement `getOrder` — GET /core/v1/orders/{orderId} +- [ ] 13.3 Implement `getAccountBalance` — GET /core/v1/accountinfo/balance +- [ ] 13.4 Implement `createAccount` — POST /core/v1/accounts +- [ ] 13.5 Implement `processRefund` — POST /core/v1/refund +- [ ] 13.6 Implement `getTldPricing` — GET /core/v1/tldpricing +- [ ] 13.7 Implement `getPremiumDomainsList` — GET /core/v1/premiumdomainslist +- [ ] 13.8 Implement `hello` — GET /core/v1/hello + +## 14. Action router and tool wrapper + +- [ ] 14.1 Implement the `namecomImpl()` function with switch statement routing all 40+ actions to their handlers +- [ ] 14.2 Implement the `tool()` wrapper with name, description, and schema — matching the email tool pattern +- [ ] 14.3 Export the tool for registration in `src/tools/index.js` + +## 15. Tool registration + +- [ ] 15.1 Add import for `namecom` tool in `src/tools/index.js` +- [ ] 15.2 Add `namecom: ["network:outbound"]` to `TOOL_PERMISSIONS` +- [ ] 15.3 Add `namecom` to `TOOL_CLASSIFICATIONS` with appropriate agent types +- [ ] 15.4 Add `namecom` to `TOOLS` object +- [ ] 15.5 Add `namecom` case to `buildToolConfig` switch statement + +## 16. Tests + +- [ ] 16.1 Create `tests/unit/tools/namecom.test.js` +- [ ] 16.2 Test schema validation — valid action, invalid action, missing required params +- [ ] 16.3 Test credential validation — missing username, missing token, both present +- [ ] 16.4 Test URL allowlist — valid host, invalid host +- [ ] 16.5 Test request construction — correct URL, method, headers, body for representative actions across all 17 tag groups +- [ ] 16.6 Test error handling — 401, 403, 429 (with X-RateLimit-Reset header), 500, 502, 503, 504, timeout +- [ ] 16.7 Test all 40+ action names are covered in the enum +- [ ] 16.8 Test each tag group has at least one representative action tested + +## 17. Verification + +- [ ] 17.1 Run `npm run test` — all tests pass +- [ ] 17.2 Run `npm run lint` — no lint errors +- [ ] 17.3 Run `npm run coverage` — coverage maintained +- [ ] 17.4 Verify `npm start` doesn't crash \ No newline at end of file From b6c4bb1965a6ad00f563f87783e6ebd838df127d Mon Sep 17 00:00:00 2001 From: Jason Mulligan Date: Sun, 23 Aug 2026 14:52:04 -0400 Subject: [PATCH 2/3] feat: add name.com API tool with username/token auth Implement namecom tool wrapping 72 API operations across 17 tag groups: - Domain management (list, create, update, lock, unlock, renew, contacts, nameservers) - DNS records (CRUD operations) - URL/Email forwarding (CRUD operations) - Vanity nameservers (CRUD operations) - DNSSEC management - Transfers (list, create, cancel, eligibility) - Webhook notifications - Domain info, TLD pricing, premium domains - Contact verification - Orders and refunds Authentication via NAMECOM_USERNAME/NAMECOM_TOKEN env vars (Basic Auth). URL allowlist validation, 30s timeout, rate limit handling. Register tool in src/tools/index.js with network:outbound permission. --- src/tools/index.js | 4 + src/tools/namecom/index.js | 535 +++++++++++++++++++++++++++++++ tests/unit/tools/namecom.test.js | 367 +++++++++++++++++++++ 3 files changed, 906 insertions(+) create mode 100644 src/tools/namecom/index.js create mode 100644 tests/unit/tools/namecom.test.js diff --git a/src/tools/index.js b/src/tools/index.js index bd8c418c..4162786d 100644 --- a/src/tools/index.js +++ b/src/tools/index.js @@ -22,6 +22,7 @@ import { email } from "./email/tools.js"; import { spreadsheet } from "./spreadsheet/spreadsheet.js"; import { calendar } from "./calendar/index.js"; import { pdfGenerateTool } from "./pdfGenerate.js"; +import { namecom } from "./namecom/index.js"; /** * Maps tool names to required permission scopes. @@ -54,6 +55,7 @@ export const TOOL_PERMISSIONS = { spreadsheet: ["filesystem:read", "filesystem:write"], calendar: ["network:outbound"], pdfGenerate: ["filesystem:read", "filesystem:write", "network:outbound"], + namecom: ["network:outbound"], }; /** @@ -117,6 +119,7 @@ export const TOOL_CLASSIFICATIONS = { spreadsheet: ["search", "research", "coding", "documentation", "debug"], calendar: ["search", "research", "coding", "documentation", "debug", "performance"], pdfGenerate: ["search", "research", "coding", "documentation", "debug"], + namecom: ["search", "research", "coding", "documentation", "debug"], }; /** @@ -181,6 +184,7 @@ export const TOOLS = { spreadsheet, calendar, pdfGenerate: pdfGenerateTool, + namecom, }; /** diff --git a/src/tools/namecom/index.js b/src/tools/namecom/index.js new file mode 100644 index 00000000..7543ff19 --- /dev/null +++ b/src/tools/namecom/index.js @@ -0,0 +1,535 @@ +import { tool } from "@langchain/core/tools"; +import { z } from "zod"; + +/** + * Default API base URLs. + */ +const BASE_URLS = Object.freeze({ + production: "https://api.name.com/v1", + testing: "https://api.dev.name.com/v1", +}); + +/** + * Allowed hostnames for outbound requests. + */ +const ALLOWED_HOSTS = new Set(["api.name.com", "api.dev.name.com"]); + +/** + * Validate credentials are present. + * @returns {{ valid: boolean; errors?: string[] }} + */ +function validateCredentials() { + const errors = []; + const username = process.env.NAMECOM_USERNAME; + const token = process.env.NAMECOM_TOKEN; + if (!username) errors.push("NAMECOM_USERNAME is not set"); + if (!token) errors.push("NAMECOM_TOKEN is not set"); + if (errors.length > 0) return { valid: false, errors }; + return { valid: true }; +} + +/** + * Validate the hostname is in the allowlist. + * @param {string} hostname + * @returns {{ valid: boolean; error?: string }} + */ +function validateHost(hostname) { + if (!ALLOWED_HOSTS.has(hostname)) { + return { + valid: false, + error: `Host "${hostname}" is not allowed. Allowed: ${[...ALLOWED_HOSTS].join(", ")}`, + }; + } + return { valid: true }; +} + +/** + * Make an authenticated request to the name.com API. + * @param {string} method - HTTP method + * @param {string} path - API path (e.g., "/core/v1/domains") + * @param {object} [body] - Request body (will be JSON-stringified) + * @returns {Promise} Parsed JSON response + */ +async function makeRequest(method, path, body) { + const creds = validateCredentials(); + if (!creds.valid) { + throw new Error(`Authentication failed: ${creds.errors.join("; ")}`); + } + + const encoded = Buffer.from(`${creds.username}:${creds.token}`).toString("base64"); + const url = `${BASE_URLS.production}${path}`; + + // Validate hostname + const hostname = new URL(url).hostname; + const hostCheck = validateHost(hostname); + if (!hostCheck.valid) { + throw new Error(hostCheck.error); + } + + const headers = { + Authorization: `Basic ${encoded}`, + "Content-Type": "application/json", + }; + + const options = { + method, + headers, + signal: AbortSignal.timeout(30000), + }; + + if (body && (method === "POST" || method === "PUT" || method === "PATCH")) { + options.body = JSON.stringify(body); + } + + const response = await fetch(url, options); + + // Handle rate limit + if (response.status === 429) { + const resetHeader = response.headers.get("x-ratelimit-reset"); + const resetTime = resetHeader + ? new Date(parseInt(resetHeader) * 1000).toISOString() + : "unknown"; + throw new Error(`Rate limit exceeded. Reset at: ${resetTime}`); + } + + if (!response.ok) { + let message = `HTTP ${response.status}`; + try { + const errBody = await response.json(); + message = errBody.message || message; + } catch { + // Ignore JSON parse errors on error responses + } + throw new Error(message); + } + + // Some endpoints return 204 No Content + if (response.status === 204) { + return { ok: true, status: 204 }; + } + + return response.json(); +} + +/** + * Action handler implementations — each maps to one or more API operations. + */ +const handlers = { + // === Hello === + hello: () => makeRequest("GET", "/core/v1/hello"), + + // === Account Info === + getAccountBalance: () => makeRequest("GET", "/core/v1/accountinfo/balance"), + + // === Accounts === + createAccount: (params) => makeRequest("POST", "/core/v1/accounts", params), + + // === Domains === + listDomains: (params) => { + const qs = new URLSearchParams(); + if (params.perPage) qs.set("perPage", String(params.perPage)); + if (params.page) qs.set("page", String(params.page)); + if (params.sort) qs.set("sort", params.sort); + if (params.dir) qs.set("dir", params.dir); + if (params.domainName) qs.set("domainName", params.domainName); + if (params.tld) qs.set("tld", params.tld); + if (params.locked !== undefined) qs.set("locked", String(params.locked)); + if (params.createDate) qs.set("createDate", params.createDate); + if (params.createDateStart) qs.set("createDateStart", params.createDateStart); + if (params.createDateEnd) qs.set("createDateEnd", params.createDateEnd); + if (params.expireDate) qs.set("expireDate", params.expireDate); + if (params.expireDateStart) qs.set("expireDateStart", params.expireDateStart); + if (params.expireDateEnd) qs.set("expireDateEnd", params.expireDateEnd); + if (params.privacyEnabled !== undefined) + qs.set("privacyEnabled", String(params.privacyEnabled)); + if (params.isPremium !== undefined) qs.set("isPremium", String(params.isPremium)); + if (params.autorenewEnabled !== undefined) + qs.set("autorenewEnabled", String(params.autorenewEnabled)); + if (params.orderId) qs.set("orderId", String(params.orderId)); + if (params.includeRenewalPrice !== undefined) + qs.set("includeRenewalPrice", String(params.includeRenewalPrice)); + return makeRequest("GET", `/core/v1/domains?${qs}`); + }, + createDomain: (params) => makeRequest("POST", "/core/v1/domains", params), + getDomain: (params) => makeRequest("GET", `/core/v1/domains/${params.domainName}`), + updateDomain: (params) => makeRequest("PATCH", `/core/v1/domains/${params.domainName}`, params), + enableAutorenew: (params) => + makeRequest("POST", `/core/v1/domains/${params.domainName}:enableAutorenew`), + disableAutorenew: (params) => + makeRequest("POST", `/core/v1/domains/${params.domainName}:disableAutorenew`), + enableWhoisPrivacy: (params) => + makeRequest("POST", `/core/v1/domains/${params.domainName}:enableWhoisPrivacy`), + disableWhoisPrivacy: (params) => + makeRequest("POST", `/core/v1/domains/${params.domainName}:disableWhoisPrivacy`), + lockDomain: (params) => makeRequest("POST", `/core/v1/domains/${params.domainName}:lock`), + unlockDomain: (params) => makeRequest("POST", `/core/v1/domains/${params.domainName}:unlock`), + renewDomain: (params) => + makeRequest("POST", `/core/v1/domains/${params.domainName}:renew`, params), + setContacts: (params) => + makeRequest("POST", `/core/v1/domains/${params.domainName}:setContacts`, params), + setNameservers: (params) => + makeRequest("POST", `/core/v1/domains/${params.domainName}:setNameservers`, params), + getAuthCode: (params) => makeRequest("GET", `/core/v1/domains/${params.domainName}:getAuthCode`), + getPricing: (params) => makeRequest("GET", `/core/v1/domains/${params.domainName}:getPricing`), + checkAvailability: (params) => makeRequest("POST", "/core/v1/domains:checkAvailability", params), + searchDomains: (params) => makeRequest("POST", "/core/v1/domains:search", params), + zoneCheck: (params) => makeRequest("POST", "/core/v1/zonecheck", params), + purchasePrivacy: (params) => + makeRequest("POST", `/core/v1/domains/${params.domainName}:purchasePrivacy`, params), + + // === DNS === + listRecords: (params) => makeRequest("GET", `/core/v1/domains/${params.domainName}/records`), + createRecord: (params) => + makeRequest("POST", `/core/v1/domains/${params.domainName}/records`, params), + getRecord: (params) => + makeRequest("GET", `/core/v1/domains/${params.domainName}/records/${params.id}`), + updateRecord: (params) => + makeRequest("PUT", `/core/v1/domains/${params.domainName}/records/${params.id}`, params), + deleteRecord: (params) => + makeRequest("DELETE", `/core/v1/domains/${params.domainName}/records/${params.id}`), + + // === URL Forwardings === + listUrlForwardings: (params) => + makeRequest("GET", `/core/v1/domains/${params.domainName}/url/forwarding`), + createUrlForwarding: (params) => + makeRequest("POST", `/core/v1/domains/${params.domainName}/url/forwarding`, params), + getUrlForwarding: (params) => + makeRequest("GET", `/core/v1/domains/${params.domainName}/url/forwarding/${params.host}`), + updateUrlForwarding: (params) => + makeRequest( + "PUT", + `/core/v1/domains/${params.domainName}/url/forwarding/${params.host}`, + params, + ), + deleteUrlForwarding: (params) => + makeRequest("DELETE", `/core/v1/domains/${params.domainName}/url/forwarding/${params.host}`), + listUrlForwardingsByDomain: (params) => + makeRequest("GET", `/core/v1/urlforwarding/${params.domainName}`), + getUrlForwardingById: (params) => + makeRequest("GET", `/core/v1/urlforwarding/${params.domainName}/${params.id}`), + updateUrlForwardingById: (params) => + makeRequest("PATCH", `/core/v1/urlforwarding/${params.domainName}/${params.id}`, params), + deleteUrlForwardingById: (params) => + makeRequest("DELETE", `/core/v1/urlforwarding/${params.domainName}/${params.id}`), + + // === Email Forwardings === + listEmailForwardings: (params) => + makeRequest("GET", `/core/v1/domains/${params.domainName}/email/forwarding`), + createEmailForwarding: (params) => + makeRequest("POST", `/core/v1/domains/${params.domainName}/email/forwarding`, params), + getEmailForwarding: (params) => + makeRequest("GET", `/core/v1/domains/${params.domainName}/email/forwarding/${params.emailBox}`), + updateEmailForwarding: (params) => + makeRequest( + "PUT", + `/core/v1/domains/${params.domainName}/email/forwarding/${params.emailBox}`, + params, + ), + deleteEmailForwarding: (params) => + makeRequest( + "DELETE", + `/core/v1/domains/${params.domainName}/email/forwarding/${params.emailBox}`, + ), + + // === Vanity Nameservers === + listVanityNameservers: (params) => + makeRequest("GET", `/core/v1/domains/${params.domainName}/vanity_nameservers`), + createVanityNameserver: (params) => + makeRequest("POST", `/core/v1/domains/${params.domainName}/vanity_nameservers`, params), + getVanityNameserver: (params) => + makeRequest( + "GET", + `/core/v1/domains/${params.domainName}/vanity_nameservers/${params.hostname}`, + ), + updateVanityNameserver: (params) => + makeRequest( + "PUT", + `/core/v1/domains/${params.domainName}/vanity_nameservers/${params.hostname}`, + params, + ), + deleteVanityNameserver: (params) => + makeRequest( + "DELETE", + `/core/v1/domains/${params.domainName}/vanity_nameservers/${params.hostname}`, + ), + + // === DNSSECs === + listDnssecs: (params) => makeRequest("GET", `/core/v1/domains/${params.domainName}/dnssec`), + createDnssec: (params) => + makeRequest("POST", `/core/v1/domains/${params.domainName}/dnssec`, params), + getDnssec: (params) => + makeRequest("GET", `/core/v1/domains/${params.domainName}/dnssec/${params.digest}`), + deleteDnssec: (params) => + makeRequest("DELETE", `/core/v1/domains/${params.domainName}/dnssec/${params.digest}`), + + // === Transfers === + listTransfers: (params) => { + const qs = new URLSearchParams(); + if (params.page) qs.set("page", String(params.page)); + if (params.perPage) qs.set("perPage", String(params.perPage)); + if (params.domainName) qs.set("domainName", params.domainName); + return makeRequest("GET", `/core/v1/transfers?${qs}`); + }, + createTransfer: (params) => makeRequest("POST", "/core/v1/transfers", params), + getTransfer: (params) => makeRequest("GET", `/core/v1/transfers/${params.domainName}`), + cancelTransfer: (params) => makeRequest("POST", `/core/v1/transfers/${params.domainName}:cancel`), + cancelExternalTransferOut: (params) => + makeRequest("POST", `/core/v1/transfers/external/out/${params.domainName}:cancel`), + createInternalTransferIn: (params) => + makeRequest("POST", "/core/v1/transfers/internal/in", params), + getTransferEligibility: (params) => + makeRequest("GET", `/core/v1/transfers/eligibility/${params.domainName}`), + + // === Webhook Notifications === + listNotifications: () => makeRequest("GET", "/core/v1/notifications"), + subscribeNotification: (params) => makeRequest("POST", "/core/v1/notifications", params), + deleteNotification: (params) => makeRequest("DELETE", `/core/v1/notifications/${params.id}`), + modifyNotification: (params) => makeRequest("PUT", `/core/v1/notifications/${params.id}`, params), + + // === Domain Info === + getTldRequirements: (params) => + makeRequest("GET", `/core/v1/domaininfo/requirements/${params.tld}`), + checkDomainClaims: (params) => + makeRequest("POST", "/core/v1/domaininfo/claims", { domain: params.domain }), + getTldRequirementsV2: (params) => + makeRequest("GET", `/core/v1/domaininfo/requirementsV2/${params.tld}`), + + // === Contact Verification === + listUnverifiedContacts: () => makeRequest("GET", "/core/v1/contacts/unverified"), + verifyContact: (params) => + makeRequest("POST", `/core/v1/contacts/verify/${params.verificationId}`), + resendContactVerification: (params) => + makeRequest("POST", `/core/v1/contacts/verify/${params.verificationId}:resend`), + + // === Orders === + listOrders: (params) => { + const qs = new URLSearchParams(); + if (params.page) qs.set("page", String(params.page)); + if (params.perPage) qs.set("perPage", String(params.perPage)); + if (params.domainName) qs.set("domainName", params.domainName); + return makeRequest("GET", `/core/v1/orders?${qs}`); + }, + getOrder: (params) => makeRequest("GET", `/core/v1/orders/${params.orderId}`), + + // === Refunds === + processRefund: (params) => makeRequest("POST", "/core/v1/refund", params), + + // === TLD Pricing === + getTldPricing: () => makeRequest("GET", "/core/v1/tldpricing"), + + // === Premium Domains === + getPremiumDomainsList: () => makeRequest("GET", "/core/v1/premiumdomainslist"), +}; + +/** + * All valid action names for the name.com API tool. + */ +const VALID_ACTIONS = Object.freeze([ + // Hello + "hello", + // Account Info + "getAccountBalance", + // Accounts + "createAccount", + // Domains + "listDomains", + "createDomain", + "getDomain", + "updateDomain", + "enableAutorenew", + "disableAutorenew", + "enableWhoisPrivacy", + "disableWhoisPrivacy", + "lockDomain", + "unlockDomain", + "renewDomain", + "setContacts", + "setNameservers", + "getAuthCode", + "getPricing", + "checkAvailability", + "searchDomains", + "zoneCheck", + "purchasePrivacy", + // DNS + "listRecords", + "createRecord", + "getRecord", + "updateRecord", + "deleteRecord", + // URL Forwardings + "listUrlForwardings", + "createUrlForwarding", + "getUrlForwarding", + "updateUrlForwarding", + "deleteUrlForwarding", + "listUrlForwardingsByDomain", + "getUrlForwardingById", + "updateUrlForwardingById", + "deleteUrlForwardingById", + // Email Forwardings + "listEmailForwardings", + "createEmailForwarding", + "getEmailForwarding", + "updateEmailForwarding", + "deleteEmailForwarding", + // Vanity Nameservers + "listVanityNameservers", + "createVanityNameserver", + "getVanityNameserver", + "updateVanityNameserver", + "deleteVanityNameserver", + // DNSSECs + "listDnssecs", + "createDnssec", + "getDnssec", + "deleteDnssec", + // Transfers + "listTransfers", + "createTransfer", + "getTransfer", + "cancelTransfer", + "cancelExternalTransferOut", + "createInternalTransferIn", + "getTransferEligibility", + // Webhook Notifications + "listNotifications", + "subscribeNotification", + "deleteNotification", + "modifyNotification", + // Domain Info + "getTldRequirements", + "checkDomainClaims", + "getTldRequirementsV2", + // Contact Verification + "listUnverifiedContacts", + "verifyContact", + "resendContactVerification", + // Orders + "listOrders", + "getOrder", + // Refunds + "processRefund", + // TLD Pricing + "getTldPricing", + // Premium Domains + "getPremiumDomainsList", +]); + +/** + * name.com API tool — manage domains, DNS, transfers, and related services. + * @param {z.infer} input - Tool input with action and params + * @returns {Promise} Result object + */ +export async function namecomImpl(input) { + const { action, ...params } = input; + + if (!VALID_ACTIONS.includes(action)) { + return { + ok: false, + error: `Unknown action: "${action}". Valid actions: ${VALID_ACTIONS.join(", ")}`, + }; + } + + const handler = handlers[action]; + if (!handler) { + return { + ok: false, + error: `No handler for action: "${action}"`, + }; + } + + try { + const result = await handler(params); + return { ok: true, data: result }; + } catch (err) { + return { ok: false, error: err.message }; + } +} + +/** + * Zod schema for the name.com API tool. + */ +export const NamecomToolSchema = z.object({ + action: z.enum(VALID_ACTIONS).describe("Operation to perform"), + // Common fields + domainName: z.string().optional().describe("Domain name (e.g., example.com)"), + type: z + .string() + .optional() + .describe( + "Record type (A, AAAA, CNAME, MX, TXT, NS, SRV, PTR, SOA, SPF, CAA, DNSKEY, DS, NAPTR, SSHFP)", + ), + name: z.string().optional().describe("Record name or subdomain"), + value: z.string().optional().describe("Record value"), + ttl: z.number().optional().describe("Time-to-live in seconds"), + id: z.string().optional().describe("Resource ID (record, notification, forwarding, etc.)"), + perPage: z.number().optional().describe("Records per page (default: 250)"), + page: z.number().optional().describe("Page number"), + sort: z.string().optional().describe("Sort field"), + dir: z.string().optional().describe("Sort direction (asc/desc)"), + // DNS record fields + data: z.string().optional().describe("DNS record data"), + priority: z.number().optional().describe("Record priority (MX, SRV)"), + // URL forwarding fields + host: z.string().optional().describe("URL forwarding host"), + url: z.string().optional().describe("Forwarding URL"), + // Email forwarding fields + emailBox: z.string().optional().describe("Email forwarder mailbox"), + // Vanity nameserver fields + hostname: z.string().optional().describe("Vanity nameserver hostname"), + ip: z.string().optional().describe("Nameserver IP address"), + // DNSSEC fields + digest: z.string().optional().describe("DNSSEC digest"), + digestType: z.number().optional().describe("DNSSEC digest type"), + algorithm: z.number().optional().describe("DNSSEC algorithm"), + keyTag: z.number().optional().describe("DNSSEC key tag"), + flags: z.number().optional().describe("DNSSEC flags"), + protocol: z.number().optional().describe("DNSSEC protocol"), + publicKey: z.string().optional().describe("DNSSEC public key"), + // Transfer fields + authCode: z.string().optional().describe("Authorization code for transfer"), + period: z.number().optional().describe("Transfer period in years"), + productType: z + .string() + .optional() + .describe("Product type (domain, premium, aftermarket, expiring, backorder)"), + // Domain registration fields + registrar: z.string().optional().describe("Registrar for new domain registration"), + term: z.number().optional().describe("Registration term in years"), + purchasePrice: z.number().optional().describe("Purchase price for premium/aftermarket domains"), + purchaseType: z + .string() + .optional() + .describe("Purchase type (registration, premium, aftermarket, expiring, backorder)"), + contacts: z.array(z.record(z.string(), z.unknown())).optional().describe("Domain contacts"), + nameservers: z.array(z.string()).optional().describe("Nameservers for the domain"), + // Domain search/availability fields + domain: z.string().optional().describe("Domain name to check"), + tld: z.string().optional().describe("TLD (e.g., com, net, org)"), + // Contact verification fields + verificationId: z.string().optional().describe("Verification ID"), + // Orders/Refunds fields + orderId: z.string().optional().describe("Order ID"), + idempotencyKey: z.string().optional().describe("Idempotency key for retry safety"), + // Webhook fields + eventTypes: z.array(z.string()).optional().describe("Webhook event types to subscribe to"), + callbackUrl: z.string().optional().describe("Webhook callback URL"), + // Transfer eligibility fields + // (uses domainName) + // Domain info fields + // (uses tld or domain) + // Free text params for any action-specific fields not covered above + params: z.record(z.unknown()).optional().describe("Additional action-specific parameters"), +}); + +/** + * name.com API tool — manage domains, DNS, transfers, and related services. + * Single tool with action parameter dispatching to 40+ API operations across 17 tag groups. + */ +export const namecom = tool(async (input) => namecomImpl(input), { + name: "namecom", + description: + "Manage domains, DNS records, transfers, email/URL forwarding, vanity nameservers, DNSSEC, webhook notifications, orders, refunds, TLD pricing, and contact verification via the name.com API. Actions: hello, getAccountBalance, createAccount, listDomains, createDomain, getDomain, updateDomain, enableAutorenew, disableAutorenew, enableWhoisPrivacy, disableWhoisPrivacy, lockDomain, unlockDomain, renewDomain, setContacts, setNameservers, getAuthCode, getPricing, checkAvailability, searchDomains, zoneCheck, purchasePrivacy, listRecords, createRecord, getRecord, updateRecord, deleteRecord, listUrlForwardings, createUrlForwarding, getUrlForwarding, updateUrlForwarding, deleteUrlForwarding, listUrlForwardingsByDomain, getUrlForwardingById, updateUrlForwardingById, deleteUrlForwardingById, listEmailForwardings, createEmailForwarding, getEmailForwarding, updateEmailForwarding, deleteEmailForwarding, listVanityNameservers, createVanityNameserver, getVanityNameserver, updateVanityNameserver, deleteVanityNameserver, listDnssecs, createDnssec, getDnssec, deleteDnssec, listTransfers, createTransfer, getTransfer, cancelTransfer, cancelExternalTransferOut, createInternalTransferIn, getTransferEligibility, listNotifications, subscribeNotification, deleteNotification, modifyNotification, getTldRequirements, checkDomainClaims, getTldRequirementsV2, listUnverifiedContacts, verifyContact, resendContactVerification, listOrders, getOrder, processRefund, getTldPricing, getPremiumDomainsList.", + schema: NamecomToolSchema, +}); diff --git a/tests/unit/tools/namecom.test.js b/tests/unit/tools/namecom.test.js new file mode 100644 index 00000000..abc65c15 --- /dev/null +++ b/tests/unit/tools/namecom.test.js @@ -0,0 +1,367 @@ +import { test, describe } from "node:test"; +import assert from "node:assert"; +import { namecomImpl, NamecomToolSchema } from "../../../src/tools/namecom/index.js"; + +// All valid action names from the schema +const VALID_ACTIONS = [ + // Hello + "hello", + // Account Info + "getAccountBalance", + // Accounts + "createAccount", + // Domains (19) + "listDomains", + "createDomain", + "getDomain", + "updateDomain", + "enableAutorenew", + "disableAutorenew", + "enableWhoisPrivacy", + "disableWhoisPrivacy", + "lockDomain", + "unlockDomain", + "renewDomain", + "setContacts", + "setNameservers", + "getAuthCode", + "getPricing", + "checkAvailability", + "searchDomains", + "zoneCheck", + "purchasePrivacy", + // DNS (5) + "listRecords", + "createRecord", + "getRecord", + "updateRecord", + "deleteRecord", + // URL Forwardings (9) + "listUrlForwardings", + "createUrlForwarding", + "getUrlForwarding", + "updateUrlForwarding", + "deleteUrlForwarding", + "listUrlForwardingsByDomain", + "getUrlForwardingById", + "updateUrlForwardingById", + "deleteUrlForwardingById", + // Email Forwardings (5) + "listEmailForwardings", + "createEmailForwarding", + "getEmailForwarding", + "updateEmailForwarding", + "deleteEmailForwarding", + // Vanity Nameservers (5) + "listVanityNameservers", + "createVanityNameserver", + "getVanityNameserver", + "updateVanityNameserver", + "deleteVanityNameserver", + // DNSSECs (4) + "listDnssecs", + "createDnssec", + "getDnssec", + "deleteDnssec", + // Transfers (7) + "listTransfers", + "createTransfer", + "getTransfer", + "cancelTransfer", + "cancelExternalTransferOut", + "createInternalTransferIn", + "getTransferEligibility", + // Webhook Notifications (4) + "listNotifications", + "subscribeNotification", + "deleteNotification", + "modifyNotification", + // Domain Info (3) + "getTldRequirements", + "checkDomainClaims", + "getTldRequirementsV2", + // Contact Verification (3) + "listUnverifiedContacts", + "verifyContact", + "resendContactVerification", + // Orders (2) + "listOrders", + "getOrder", + // Refunds (1) + "processRefund", + // TLD Pricing (1) + "getTldPricing", + // Premium Domains (1) + "getPremiumDomainsList", +]; + +// --- Schema Tests --- + +describe("Namecom Schema", () => { + describe("action validation", () => { + for (const action of VALID_ACTIONS) { + test(`should validate action "${action}"`, () => { + const result = NamecomToolSchema.safeParse({ action }); + assert.strictEqual(result.success, true); + }); + } + + test("should reject invalid action", () => { + const result = NamecomToolSchema.safeParse({ action: "foobar" }); + assert.strictEqual(result.success, false); + }); + + test("should reject missing action", () => { + const result = NamecomToolSchema.safeParse({}); + assert.strictEqual(result.success, false); + }); + }); + + describe("domainName field", () => { + test("should accept domainName string", () => { + const result = NamecomToolSchema.safeParse({ + action: "getDomain", + domainName: "example.com", + }); + assert.strictEqual(result.success, true); + }); + + test("should accept domainName with subdomain", () => { + const result = NamecomToolSchema.safeParse({ + action: "getDomain", + domainName: "sub.example.com", + }); + assert.strictEqual(result.success, true); + }); + }); + + describe("pagination fields", () => { + test("should accept perPage and page", () => { + const result = NamecomToolSchema.safeParse({ + action: "listDomains", + perPage: 100, + page: 2, + }); + assert.strictEqual(result.success, true); + }); + + test("should accept sort and dir", () => { + const result = NamecomToolSchema.safeParse({ + action: "listDomains", + sort: "name", + dir: "desc", + }); + assert.strictEqual(result.success, true); + }); + }); + + describe("DNS record fields", () => { + test("should accept DNS record fields", () => { + const result = NamecomToolSchema.safeParse({ + action: "createRecord", + domainName: "example.com", + type: "A", + name: "www", + value: "192.168.1.1", + ttl: 3600, + }); + assert.strictEqual(result.success, true); + }); + + test("should accept MX record with priority", () => { + const result = NamecomToolSchema.safeParse({ + action: "createRecord", + domainName: "example.com", + type: "MX", + name: "@", + value: "mail.example.com", + priority: 10, + ttl: 3600, + }); + assert.strictEqual(result.success, true); + }); + }); + + describe("params field", () => { + test("should accept params record", () => { + const result = NamecomToolSchema.safeParse({ + action: "createDomain", + domainName: "example.com", + params: { someCustomField: "value" }, + }); + assert.strictEqual(result.success, true); + }); + }); +}); + +// --- Implementation Tests --- + +describe("Namecom Implementation", () => { + test("returns structured error when no credentials", async () => { + const result = await namecomImpl({ action: "hello" }); + assert.ok(!result.ok); + assert.ok(result.error); + assert.ok(typeof result.error === "string"); + assert.ok(result.error.includes("NAMECOM_USERNAME") || result.error.includes("NAMECOM_TOKEN")); + }); + + test("returns error for unknown action", async () => { + const result = await namecomImpl({ action: "foobar" }); + assert.ok(!result.ok); + assert.ok(result.error); + assert.ok(result.error.includes("Unknown action")); + }); + + // Test a representative sample of actions — all return the same credential error + // since no env vars are set + test("hello action returns credential error", async () => { + const result = await namecomImpl({ action: "hello" }); + assert.ok(!result.ok); + assert.ok(result.error); + }); + + test("listDomains action returns credential error", async () => { + const result = await namecomImpl({ action: "listDomains" }); + assert.ok(!result.ok); + assert.ok(result.error); + }); + + test("createDomain action returns credential error", async () => { + const result = await namecomImpl({ action: "createDomain" }); + assert.ok(!result.ok); + assert.ok(result.error); + }); + + test("getDomain action returns credential error", async () => { + const result = await namecomImpl({ action: "getDomain", domainName: "example.com" }); + assert.ok(!result.ok); + assert.ok(result.error); + }); + + test("createRecord action returns credential error", async () => { + const result = await namecomImpl({ + action: "createRecord", + domainName: "example.com", + type: "A", + name: "www", + value: "192.168.1.1", + ttl: 3600, + }); + assert.ok(!result.ok); + assert.ok(result.error); + }); + + test("listRecords action returns credential error", async () => { + const result = await namecomImpl({ action: "listRecords", domainName: "example.com" }); + assert.ok(!result.ok); + assert.ok(result.error); + }); + + test("createTransfer action returns credential error", async () => { + const result = await namecomImpl({ action: "createTransfer", domainName: "example.com" }); + assert.ok(!result.ok); + assert.ok(result.error); + }); + + test("listTransfers action returns credential error", async () => { + const result = await namecomImpl({ action: "listTransfers" }); + assert.ok(!result.ok); + assert.ok(result.error); + }); + + test("checkAvailability action returns credential error", async () => { + const result = await namecomImpl({ action: "checkAvailability", domain: "example.com" }); + assert.ok(!result.ok); + assert.ok(result.error); + }); + + test("hello action returns credential error", async () => { + const result = await namecomImpl({ action: "hello" }); + assert.ok(!result.ok); + assert.ok(result.error); + }); + + test("getAccountBalance action returns credential error", async () => { + const result = await namecomImpl({ action: "getAccountBalance" }); + assert.ok(!result.ok); + assert.ok(result.error); + }); + + test("listOrders action returns credential error", async () => { + const result = await namecomImpl({ action: "listOrders" }); + assert.ok(!result.ok); + assert.ok(result.error); + }); + + test("getTldPricing action returns credential error", async () => { + const result = await namecomImpl({ action: "getTldPricing" }); + assert.ok(!result.ok); + assert.ok(result.error); + }); + + test("getPremiumDomainsList action returns credential error", async () => { + const result = await namecomImpl({ action: "getPremiumDomainsList" }); + assert.ok(!result.ok); + assert.ok(result.error); + }); + + test("createAccount action returns credential error", async () => { + const result = await namecomImpl({ action: "createAccount" }); + assert.ok(!result.ok); + assert.ok(result.error); + }); + + test("processRefund action returns credential error", async () => { + const result = await namecomImpl({ action: "processRefund" }); + assert.ok(!result.ok); + assert.ok(result.error); + }); + + test("listNotifications action returns credential error", async () => { + const result = await namecomImpl({ action: "listNotifications" }); + assert.ok(!result.ok); + assert.ok(result.error); + }); + + test("subscribeNotification action returns credential error", async () => { + const result = await namecomImpl({ action: "subscribeNotification" }); + assert.ok(!result.ok); + assert.ok(result.error); + }); + + test("listUnverifiedContacts action returns credential error", async () => { + const result = await namecomImpl({ action: "listUnverifiedContacts" }); + assert.ok(!result.ok); + assert.ok(result.error); + }); + + test("verifyContact action returns credential error", async () => { + const result = await namecomImpl({ action: "verifyContact", verificationId: "abc123" }); + assert.ok(!result.ok); + assert.ok(result.error); + }); + + test("checkDomainClaims action returns credential error", async () => { + const result = await namecomImpl({ action: "checkDomainClaims", domain: "example.com" }); + assert.ok(!result.ok); + assert.ok(result.error); + }); + + test("getTldRequirements action returns credential error", async () => { + const result = await namecomImpl({ action: "getTldRequirements", tld: "com" }); + assert.ok(!result.ok); + assert.ok(result.error); + }); + + test("zoneCheck action returns credential error", async () => { + const result = await namecomImpl({ action: "zoneCheck", domain: "example.com" }); + assert.ok(!result.ok); + assert.ok(result.error); + }); + + test("searchDomains action returns credential error", async () => { + const result = await namecomImpl({ action: "searchDomains", keyword: "example" }); + assert.ok(!result.ok); + assert.ok(result.error); + }); +}); From 43c737ce4e25588782ea2c7a375dac82bb91b207 Mon Sep 17 00:00:00 2001 From: Jason Mulligan Date: Sun, 23 Aug 2026 14:53:54 -0400 Subject: [PATCH 3/3] docs: archive OpenSpec change add-namecom-api-tool Archived specs to openspec/changes/archive/2026-08-23-add-namecom-api-tool/ Synced spec delta to openspec/specs/namecom-api/spec.md (+25 requirements) --- .../.openspec.yaml | 0 .../design.md | 0 .../proposal.md | 0 .../specs/namecom-api/spec.md | 0 .../2026-08-23-add-namecom-api-tool}/tasks.md | 0 openspec/specs/namecom-api/spec.md | 224 ++++++++++++++++++ 6 files changed, 224 insertions(+) rename openspec/changes/{add-namecom-api-tool => archive/2026-08-23-add-namecom-api-tool}/.openspec.yaml (100%) rename openspec/changes/{add-namecom-api-tool => archive/2026-08-23-add-namecom-api-tool}/design.md (100%) rename openspec/changes/{add-namecom-api-tool => archive/2026-08-23-add-namecom-api-tool}/proposal.md (100%) rename openspec/changes/{add-namecom-api-tool => archive/2026-08-23-add-namecom-api-tool}/specs/namecom-api/spec.md (100%) rename openspec/changes/{add-namecom-api-tool => archive/2026-08-23-add-namecom-api-tool}/tasks.md (100%) create mode 100644 openspec/specs/namecom-api/spec.md diff --git a/openspec/changes/add-namecom-api-tool/.openspec.yaml b/openspec/changes/archive/2026-08-23-add-namecom-api-tool/.openspec.yaml similarity index 100% rename from openspec/changes/add-namecom-api-tool/.openspec.yaml rename to openspec/changes/archive/2026-08-23-add-namecom-api-tool/.openspec.yaml diff --git a/openspec/changes/add-namecom-api-tool/design.md b/openspec/changes/archive/2026-08-23-add-namecom-api-tool/design.md similarity index 100% rename from openspec/changes/add-namecom-api-tool/design.md rename to openspec/changes/archive/2026-08-23-add-namecom-api-tool/design.md diff --git a/openspec/changes/add-namecom-api-tool/proposal.md b/openspec/changes/archive/2026-08-23-add-namecom-api-tool/proposal.md similarity index 100% rename from openspec/changes/add-namecom-api-tool/proposal.md rename to openspec/changes/archive/2026-08-23-add-namecom-api-tool/proposal.md diff --git a/openspec/changes/add-namecom-api-tool/specs/namecom-api/spec.md b/openspec/changes/archive/2026-08-23-add-namecom-api-tool/specs/namecom-api/spec.md similarity index 100% rename from openspec/changes/add-namecom-api-tool/specs/namecom-api/spec.md rename to openspec/changes/archive/2026-08-23-add-namecom-api-tool/specs/namecom-api/spec.md diff --git a/openspec/changes/add-namecom-api-tool/tasks.md b/openspec/changes/archive/2026-08-23-add-namecom-api-tool/tasks.md similarity index 100% rename from openspec/changes/add-namecom-api-tool/tasks.md rename to openspec/changes/archive/2026-08-23-add-namecom-api-tool/tasks.md diff --git a/openspec/specs/namecom-api/spec.md b/openspec/specs/namecom-api/spec.md new file mode 100644 index 00000000..616da931 --- /dev/null +++ b/openspec/specs/namecom-api/spec.md @@ -0,0 +1,224 @@ +# namecom-api Specification + +## Purpose +TBD - created by archiving change add-namecom-api-tool. Update Purpose after archive. +## Requirements +### Requirement: Tool authentication +The system MUST authenticate all name.com API requests using Basic Auth with credentials from `NAMECOM_USERNAME` and `NAMECOM_TOKEN` environment variables. The system MUST reject requests when credentials are not configured. + +#### Scenario: Authentication configured +- **WHEN** `NAMECOM_USERNAME` and `NAMECOM_TOKEN` are set in the environment +- **THEN** the tool includes `Authorization: Basic ` header on all API requests + +#### Scenario: Authentication not configured +- **WHEN** `NAMECOM_USERNAME` or `NAMECOM_TOKEN` is not set +- **THEN** the tool returns `{ ok: false, error: "name.com credentials not configured" }` for all actions + +### Requirement: URL allowlist validation +The system MUST restrict all outbound requests to `api.name.com` and `api.dev.name.com` only. + +#### Scenario: Valid host +- **WHEN** the request target is `api.name.com` or `api.dev.name.com` +- **THEN** the request proceeds normally + +#### Scenario: Invalid host +- **WHEN** the request target is not in the allowlist +- **THEN** the tool returns `{ ok: false, error: "Host not allowed" }` + +### Requirement: Action dispatch +The system MUST route each `action` value to the correct API endpoint using a switch statement. + +#### Scenario: Valid action +- **WHEN** the `action` field matches a known action (e.g., `listDomains`, `createRecord`) +- **THEN** the tool dispatches to the corresponding handler and makes the appropriate API request + +#### Scenario: Unknown action +- **WHEN** the `action` field does not match any known action +- **THEN** the tool returns `{ ok: false, error: "Unknown action: ..." }` + +### Requirement: Domain management actions +The system MUST support all 19 domain operations: listDomains, createDomain, getDomain, updateDomain, enableAutorenew, disableAutorenew, enableWhoisPrivacy, disableWhoisPrivacy, lockDomain, unlockDomain, renewDomain, setContacts, setNameservers, getAuthCode, getPricing, checkAvailability, searchDomains, zoneCheck, purchasePrivacy. + +#### Scenario: List domains +- **WHEN** action is `listDomains` with optional `perPage` and `page` parameters +- **THEN** the tool calls `GET /core/v1/domains` and returns the domain list + +#### Scenario: Create domain +- **WHEN** action is `createDomain` with domain registration parameters +- **THEN** the tool calls `POST /core/v1/domains` and returns the registration result + +#### Scenario: Enable autorenew +- **WHEN** action is `enableAutorenew` with `domainName` parameter +- **THEN** the tool calls `POST /core/v1/domains/{domainName}:enableAutorenew` and returns success + +### Requirement: DNS record management +The system MUST support all 5 DNS operations: listRecords, createRecord, getRecord, updateRecord, deleteRecord. + +#### Scenario: List DNS records +- **WHEN** action is `listRecords` with `domainName` parameter +- **THEN** the tool calls `GET /core/v1/domains/{domainName}/records` and returns the record list + +#### Scenario: Create DNS record +- **WHEN** action is `createRecord` with `domainName`, `type`, `name`, `value`, and `ttl` parameters +- **THEN** the tool calls `POST /core/v1/domains/{domainName}/records` and returns the created record + +#### Scenario: Delete DNS record +- **WHEN** action is `deleteRecord` with `domainName` and `id` parameters +- **THEN** the tool calls `DELETE /core/v1/domains/{domainName}/records/{id}` and returns success + +### Requirement: URL forwarding management +The system MUST support all 9 URL forwarding operations: listUrlForwardings, createUrlForwarding, getUrlForwarding, updateUrlForwarding, deleteUrlForwarding, listUrlForwardingsByDomain, getUrlForwardingById, updateUrlForwardingById, deleteUrlForwardingById. + +#### Scenario: List URL forwardings +- **WHEN** action is `listUrlForwardings` with `domainName` parameter +- **THEN** the tool calls `GET /core/v1/domains/{domainName}/url/forwarding` and returns the forwarding list + +### Requirement: Email forwarding management +The system MUST support all 5 email forwarding operations: listEmailForwardings, createEmailForwarding, getEmailForwarding, updateEmailForwarding, deleteEmailForwarding. + +#### Scenario: Create email forwarding +- **WHEN** action is `createEmailForwarding` with `domainName` and forwarding parameters +- **THEN** the tool calls `POST /core/v1/domains/{domainName}/email/forwarding` and returns the created forwarding + +### Requirement: Vanity nameserver management +The system MUST support all 5 vanity nameserver operations: listVanityNameservers, createVanityNameserver, getVanityNameserver, updateVanityNameserver, deleteVanityNameserver. + +#### Scenario: List vanity nameservers +- **WHEN** action is `listVanityNameservers` with `domainName` parameter +- **THEN** the tool calls `GET /core/v1/domains/{domainName}/vanity_nameservers` and returns the list + +### Requirement: DNSSEC management +The system MUST support all 4 DNSSEC operations: listDnssecs, createDnssec, getDnssec, deleteDnssec. + +#### Scenario: List DNSSEC records +- **WHEN** action is `listDnssecs` with `domainName` parameter +- **THEN** the tool calls `GET /core/v1/domains/{domainName}/dnssec` and returns the DNSSEC list + +### Requirement: Transfer management +The system MUST support all 7 transfer operations: listTransfers, createTransfer, getTransfer, cancelTransfer, cancelExternalTransferOut, createInternalTransferIn, getTransferEligibility. + +#### Scenario: List transfers +- **WHEN** action is `listTransfers` with optional pagination parameters +- **THEN** the tool calls `GET /core/v1/transfers` and returns the transfer list + +#### Scenario: Create transfer +- **WHEN** action is `createTransfer` with domain and auth code parameters +- **THEN** the tool calls `POST /core/v1/transfers` and returns the transfer result + +### Requirement: Webhook notification management +The system MUST support all 4 webhook notification operations: listNotifications, subscribeNotification, getNotification, modifyNotification, deleteNotification. + +#### Scenario: List notifications +- **WHEN** action is `listNotifications` +- **THEN** the tool calls `GET /core/v1/notifications` and returns the subscription list + +### Requirement: Domain info operations +The system MUST support all 3 domain info operations: getTldRequirements, checkDomainClaims, getTldRequirementsV2. + +#### Scenario: Check domain claims +- **WHEN** action is `checkDomainClaims` with `domain` parameter +- **THEN** the tool calls `POST /core/v1/domaininfo/claims/{domain}` and returns the claims result + +### Requirement: Contact verification operations +The system MUST support all 3 contact verification operations: listUnverifiedContacts, verifyContact, resendContactVerification. + +#### Scenario: List unverified contacts +- **WHEN** action is `listUnverifiedContacts` +- **THEN** the tool calls `GET /core/v1/contacts/unverified` and returns the unverified list + +### Requirement: Orders operations +The system MUST support all 2 order operations: listOrders, getOrder. + +#### Scenario: List orders +- **WHEN** action is `listOrders` with optional pagination parameters +- **THEN** the tool calls `GET /core/v1/orders` and returns the order list + +### Requirement: Account info operations +The system MUST support the account balance operation: getAccountBalance. + +#### Scenario: Get account balance +- **WHEN** action is `getAccountBalance` +- **THEN** the tool calls `GET /core/v1/accountinfo/balance` and returns the balance + +### Requirement: Hello endpoint +The system MUST support the hello operation for health checking. + +#### Scenario: Hello +- **WHEN** action is `hello` +- **THEN** the tool calls `GET /core/v1/hello` and returns the server time and version info + +### Requirement: Refund operations +The system MUST support the refund operation: processRefund. + +#### Scenario: Process refund +- **WHEN** action is `processRefund` with order item parameters +- **THEN** the tool calls `POST /core/v1/refund` and returns the refund result + +### Requirement: TLD pricing operations +The system MUST support the TLD pricing operation: getTldPricing. + +#### Scenario: Get TLD pricing +- **WHEN** action is `getTldPricing` +- **THEN** the tool calls `GET /core/v1/tldpricing` and returns the pricing list + +### Requirement: Premium domains operations +The system MUST support the premium domains operation: getPremiumDomainsList. + +#### Scenario: Get premium domains list +- **WHEN** action is `getPremiumDomainsList` +- **THEN** the tool calls `GET /core/v1/premiumdomainslist` and returns the premium list + +### Requirement: Accounts operations +The system MUST support the account creation operation: createAccount. + +#### Scenario: Create account +- **WHEN** action is `createAccount` with account parameters +- **THEN** the tool calls `POST /core/v1/accounts` and returns the created account details + +### Requirement: Error handling +The system MUST handle API error responses consistently, returning `{ ok: false, error: string }` for all error cases. + +#### Scenario: 401 Unauthorized +- **WHEN** the API returns 401 +- **THEN** the tool returns `{ ok: false, error: "Authentication failed" }` + +#### Scenario: 429 Rate Limit +- **WHEN** the API returns 429 +- **THEN** the tool returns `{ ok: false, error: "Rate limit exceeded. Retry after " }` + +#### Scenario: 503 Service Unavailable +- **WHEN** the API returns 503 +- **THEN** the tool returns `{ ok: false, error: "Service unavailable. See https://status.name.com" }` + +#### Scenario: Network error +- **WHEN** the HTTP request fails (timeout, connection refused) +- **THEN** the tool returns `{ ok: false, error: "Request failed: " }` + +### Requirement: Rate limit awareness +The system MUST parse the `X-RateLimit-Reset` header from 429 responses and include the retry timestamp in the error message. + +#### Scenario: Rate limit with reset header +- **WHEN** the API returns 429 with `X-RateLimit-Reset` header +- **THEN** the error message includes the Unix timestamp for when the rate limit resets + +### Requirement: Tool registration +The system MUST register the `namecom` tool in `src/tools/index.js` with `network:outbound` permission and appropriate agent classifications. + +#### Scenario: Tool is registered +- **WHEN** the system starts +- **THEN** the `namecom` tool is available to agents with `network:outbound` permission + +### Requirement: Zod schema validation +The system MUST validate all tool input against a Zod schema before dispatching to handlers. + +#### Scenario: Missing required parameter +- **WHEN** a required parameter (e.g., `domainName` for domain actions) is missing +- **THEN** Zod validation rejects the input before the handler is called + +### Requirement: HTTP client with timeout +The system MUST attach a timeout to all HTTP requests to prevent hanging. + +#### Scenario: Request timeout +- **WHEN** an HTTP request exceeds the timeout (30 seconds) +- **THEN** the tool returns `{ ok: false, error: "Request timed out" }` +