@aoxborrow/registrar-client 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 +21 -0
- package/README.md +415 -0
- package/dist/index.cjs +6798 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +768 -0
- package/dist/index.d.ts +768 -0
- package/dist/index.js +6747 -0
- package/dist/index.js.map +1 -0
- package/package.json +110 -0
- package/src/client.ts +200 -0
- package/src/constants.ts +11 -0
- package/src/errors.ts +192 -0
- package/src/features.ts +104 -0
- package/src/http.ts +230 -0
- package/src/index.ts +39 -0
- package/src/portfolio.ts +65 -0
- package/src/registrar.ts +240 -0
- package/src/registrars/cloudflare.ts +1205 -0
- package/src/registrars/dynadot.ts +1097 -0
- package/src/registrars/gandi.ts +970 -0
- package/src/registrars/godaddy.ts +1342 -0
- package/src/registrars/index.ts +41 -0
- package/src/registrars/namebright.ts +775 -0
- package/src/registrars/namecheap.ts +870 -0
- package/src/registrars/namesilo.ts +997 -0
- package/src/registrars/porkbun.ts +930 -0
- package/src/registrars/spaceship.ts +736 -0
- package/src/types.ts +446 -0
- package/src/utils.ts +135 -0
- package/src/xml.ts +39 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Aaron Oxborrow
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
# registrar-client
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@aoxborrow/registrar-client)
|
|
4
|
+
[](https://opensource.org/licenses/MIT)
|
|
5
|
+
[](https://nodejs.org)
|
|
6
|
+
[](https://www.typescriptlang.org)
|
|
7
|
+
|
|
8
|
+
> One consistent, fully-typed TypeScript interface to many domain registrar APIs.
|
|
9
|
+
|
|
10
|
+
Talk to GoDaddy, Cloudflare, Namecheap, Spaceship, Dynadot, Porkbun, and more
|
|
11
|
+
through a single pluggable `Registrar` contract — one typed API instead of a
|
|
12
|
+
different one for every registrar. It speaks both JSON and XML registrar APIs and
|
|
13
|
+
normalizes every provider's responses to the same shared models. The full list of
|
|
14
|
+
supported providers is [below](#providers).
|
|
15
|
+
|
|
16
|
+
**PRs for additional registrars are welcome** — adding one means extending a base
|
|
17
|
+
class and overriding the operations that provider's API supports (see
|
|
18
|
+
[Adding a provider](#adding-a-provider)).
|
|
19
|
+
|
|
20
|
+
Powers **[DomBot](https://dombot.ai)**, a desktop app and MCP server for managing
|
|
21
|
+
a domain portfolio spread across many registrars from one place.
|
|
22
|
+
|
|
23
|
+
Portable by design: its single runtime dependency
|
|
24
|
+
([`fast-xml-parser`](https://github.com/NaturalIntelligence/fast-xml-parser),
|
|
25
|
+
pure JS) uses no Node built-ins, so it runs anywhere `fetch` is available —
|
|
26
|
+
Node 20+, Deno, Bun, browsers, and edge runtimes.
|
|
27
|
+
|
|
28
|
+
> **Status:** every bundled provider implements the full 18-method core
|
|
29
|
+
> contract, plus their extended capabilities (auth code, DNSSEC, forwarding).
|
|
30
|
+
> Where a registrar's API genuinely can't do something, that method throws
|
|
31
|
+
> `NotImplementedError` with a specific reason — see
|
|
32
|
+
> [Supported functionality](#supported-functionality). Most read and write paths
|
|
33
|
+
> have been verified against live accounts or provider sandboxes (2026-08). Still
|
|
34
|
+
> treat paid, irreversible operations (register / renew / transfer) as
|
|
35
|
+
> use-at-your-own-risk: some remain documented-but-unrun on funded accounts.
|
|
36
|
+
|
|
37
|
+
## Install
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
npm install @aoxborrow/registrar-client
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Providers
|
|
44
|
+
|
|
45
|
+
| id | Provider | Auth | Sandbox | Notes |
|
|
46
|
+
| ------------ | ---------- | -------------------------- | ------- | -------------------------------------------- |
|
|
47
|
+
| `cloudflare` | Cloudflare | API token + Account ID | — | manages existing domains only |
|
|
48
|
+
| `dynadot` | Dynadot | API key | ✓ | `api-sandbox.dynadot.com`; key in query |
|
|
49
|
+
| `gandi` | Gandi.net | API key | ✓ | `api.sandbox.gandi.net` |
|
|
50
|
+
| `godaddy` | GoDaddy | PAT, or key + secret | ✓ | hybrid v3/v1; sandbox = OTE (v1 only) |
|
|
51
|
+
| `namebright` | NameBright | client ID + secret | — | OAuth2 bearer; production-only |
|
|
52
|
+
| `namecheap` | Namecheap | username + key + client IP | ✓ | XML API; IP allowlisting required |
|
|
53
|
+
| `namesilo` | NameSilo | API key | ✓ | JSON/XML API; key sent in query string |
|
|
54
|
+
| `porkbun` | Porkbun | key + secret | ✓ | sandbox via `pk1_sb_` key; per-domain opt-in |
|
|
55
|
+
| `spaceship` | Spaceship | key + secret | — | |
|
|
56
|
+
|
|
57
|
+
Each provider class exposes static discovery metadata — `displayName`,
|
|
58
|
+
`configFields` (the credential fields it needs), and `helpText` (where to get
|
|
59
|
+
them) — so you can build a credential UI generically.
|
|
60
|
+
|
|
61
|
+
Research notes for additional registrars live in
|
|
62
|
+
[`docs/registrars/`](docs/registrars). Three are documented but **not**
|
|
63
|
+
implemented because they have no usable public API: **Squarespace**
|
|
64
|
+
(reseller-program-gated), **Network Solutions** (partner-gated Partner Protocol /
|
|
65
|
+
SRSplus), and **eDomains** (WHMCS-based, no documented API). They can be added if
|
|
66
|
+
API access becomes available.
|
|
67
|
+
|
|
68
|
+
## Usage
|
|
69
|
+
|
|
70
|
+
Construct a provider (directly or via the `createRegistrar` factory) and, optionally, wrap it in
|
|
71
|
+
the `RegistrarClient` facade:
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
import { RegistrarClient, createRegistrar } from '@aoxborrow/registrar-client';
|
|
75
|
+
|
|
76
|
+
const client = new RegistrarClient(
|
|
77
|
+
createRegistrar('cloudflare', {
|
|
78
|
+
apiToken: process.env.CF_API_TOKEN!,
|
|
79
|
+
accountId: process.env.CF_ACCOUNT_ID!,
|
|
80
|
+
})
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
await client.testConnection();
|
|
84
|
+
const domains = await client.listDomains();
|
|
85
|
+
const [availability] = await client.checkAvailability(['example.com']);
|
|
86
|
+
const records = await client.getDnsRecords('example.com');
|
|
87
|
+
await client.updateNameservers('example.com', ['ns1.example.net', 'ns2.example.net']);
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Every provider implements the same `Registrar` interface — the full core
|
|
91
|
+
contract is `testConnection`, `listDomains`, `getDomain`, `checkAvailability`,
|
|
92
|
+
`getPricing`, `registerDomain`, `renewDomain`, `setAutoRenew`, `transferIn`,
|
|
93
|
+
`updateNameservers`, `getNameservers`, `lockDomain`, `unlockDomain`,
|
|
94
|
+
`setPrivacy`, `getContacts`, `updateContacts`, `getDnsRecords`, and
|
|
95
|
+
`setDnsRecords`. Every provider implements it; where a registrar's API can't
|
|
96
|
+
express a given method, that method throws `NotImplementedError` with a specific
|
|
97
|
+
reason (see [Supported functionality](#supported-functionality)). The gaps are
|
|
98
|
+
API limits, not missing work — e.g. **Cloudflare** has no post-registration write
|
|
99
|
+
endpoints, **Porkbun** exposes no transfer-lock / privacy / contact-read toggles,
|
|
100
|
+
**NameBright** has no transfer-in endpoint, and **Spaceship** / **NameBright**
|
|
101
|
+
have no pricing endpoint. **Namecheap** and **Gandi** are the only providers with
|
|
102
|
+
a real per-TLD `getPricing` table; **GoDaddy** and **Dynadot** price per-domain
|
|
103
|
+
via their availability endpoint.
|
|
104
|
+
|
|
105
|
+
## Listing domains & portfolios
|
|
106
|
+
|
|
107
|
+
`listDomains` returns the **full account**, paginating internally at each
|
|
108
|
+
provider's maximum page size (so it makes the fewest requests each API allows).
|
|
109
|
+
Options:
|
|
110
|
+
|
|
111
|
+
- `search` — a domain-name substring filter, applied server-side where the API
|
|
112
|
+
supports it (**Namecheap** `SearchTerm`, **Gandi** `fqdn`) and client-side
|
|
113
|
+
everywhere else.
|
|
114
|
+
- `detailed` — fetch full per-domain detail after listing (see below).
|
|
115
|
+
|
|
116
|
+
The usual request options (timeout/retries/signal) still apply.
|
|
117
|
+
|
|
118
|
+
```ts
|
|
119
|
+
const all = await client.listDomains();
|
|
120
|
+
const acme = await client.listDomains({ search: 'acme' });
|
|
121
|
+
const full = await client.listDomains({ detailed: true });
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Several providers' list endpoints return only a **summary** that omits
|
|
125
|
+
`nameservers`, `privacy`, and/or the transfer `locked` flag, which then read as
|
|
126
|
+
their defaults (`[]` / `false`). Nameservers come back in the single list call
|
|
127
|
+
only on **GoDaddy** (via `includes=nameServers`), **Spaceship**, and
|
|
128
|
+
**Dynadot**; **Gandi**, **Namecheap**, **Porkbun**, **NameSilo**, and
|
|
129
|
+
**NameBright** omit them (and **Gandi**/**NameSilo** also omit `privacy`).
|
|
130
|
+
(**Cloudflare** manages nameservers via its Zones API, not the Registrar list.)
|
|
131
|
+
|
|
132
|
+
Pass `detailed: true` and the facade follows up with a `getDomain` per domain
|
|
133
|
+
(bounded concurrency, falling back to `getNameservers` where detail still lacks
|
|
134
|
+
them) and merges the result — so `nameservers`, `privacy`, and `locked` are
|
|
135
|
+
accurate. It costs one extra request per domain, so it's opt-in; leave it off
|
|
136
|
+
for a fast summary, or call `getDomain` / `getNameservers` yourself for
|
|
137
|
+
individual domains.
|
|
138
|
+
|
|
139
|
+
To build a combined view across registrars, `listPortfolio` fans out over many
|
|
140
|
+
providers concurrently with per-registrar error isolation (one provider being
|
|
141
|
+
down never sinks the whole view). Every `Domain` already carries its `registrar`:
|
|
142
|
+
|
|
143
|
+
```ts
|
|
144
|
+
import { listPortfolio, createRegistrar } from '@aoxborrow/registrar-client';
|
|
145
|
+
|
|
146
|
+
const { domains, errors } = await listPortfolio([
|
|
147
|
+
createRegistrar('godaddy', godaddyCreds),
|
|
148
|
+
createRegistrar('namecheap', namecheapCreds),
|
|
149
|
+
createRegistrar('gandi', gandiCreds),
|
|
150
|
+
]);
|
|
151
|
+
// domains: Domain[] (each tagged with .registrar); errors: { registrar, error }[]
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## Supported functionality
|
|
155
|
+
|
|
156
|
+
What each provider implements today, read straight from the code. **Core** methods
|
|
157
|
+
are the guaranteed contract; **extended** methods are opt-in per provider.
|
|
158
|
+
|
|
159
|
+
**Legend:** ✓ implemented · ✗ not available via the provider's API (throws
|
|
160
|
+
`NotImplementedError`) · a superscript marks a caveat noted below.
|
|
161
|
+
|
|
162
|
+
| Core method | CF | DY | GA | GD | NB | NC | NS | PB | SP |
|
|
163
|
+
| ------------------- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
|
|
164
|
+
| `testConnection` | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
|
|
165
|
+
| `listDomains` | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
|
|
166
|
+
| `getDomain` | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
|
|
167
|
+
| `checkAvailability` | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
|
|
168
|
+
| `getPricing` | ✓ | ✓ᵃ | ✓ | ✓ᵃ | ✗ᵇ | ✓ | ✓ | ✓ | ✗ᵇ |
|
|
169
|
+
| `registerDomain` | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
|
|
170
|
+
| `renewDomain` | ✗ᶜ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
|
|
171
|
+
| `setAutoRenew` | ✗ᶜ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
|
|
172
|
+
| `transferIn` | ✗ᶜ | ✓ | ✓ | ✓ | ✗ᵈ | ✓ | ✓ | ✓ | ✓ |
|
|
173
|
+
| `updateNameservers` | ✗ᶜ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
|
|
174
|
+
| `getNameservers` | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
|
|
175
|
+
| `lockDomain` | ✗ᶜ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✗ᵉ | ✓ |
|
|
176
|
+
| `unlockDomain` | ✗ᶜ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✗ᵉ | ✓ |
|
|
177
|
+
| `setPrivacy` | ✓ | ✓ | ✓ᶠ | ✓ᵍ | ✓ | ✓ | ✓ | ✗ᵉ | ✓ |
|
|
178
|
+
| `getContacts` | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✗ᵉ | ✓ |
|
|
179
|
+
| `updateContacts` | ✗ᶜ | ✓ | ✓ | ✓ | ✗ᵈ | ✓ | ✓ | ✓ | ✓ |
|
|
180
|
+
| `getDnsRecords` | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
|
|
181
|
+
| `setDnsRecords` | ✓ | ✓ʰ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ʰ |
|
|
182
|
+
|
|
183
|
+
| Extended method | CF | DY | GA | GD | NB | NC | NS | PB | SP |
|
|
184
|
+
| --------------------- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
|
|
185
|
+
| `getAuthCode` | ✗ | ✓ | ✓ | ✓ | ✓ | ✗ⁱ | ✗ⁱ | ✗ | ✓ |
|
|
186
|
+
| `getDnssec` | ✗ | ✓ | ✓ | ✗ | ✗ | ✗ | ✓ | ✓ | ✗ |
|
|
187
|
+
| `disableDnssec` | ✗ | ✓ | ✓ | ✗ | ✗ | ✗ | ✓ | ✓ | ✗ |
|
|
188
|
+
| `getEmailForwarding` | ✓ | ✓ | ✓ | ✗ | ✗ | ✓ | ✓ | ✗ | ✗ |
|
|
189
|
+
| `setEmailForwarding` | ✓ | ✓ | ✓ | ✗ | ✗ | ✓ | ✓ | ✗ | ✗ |
|
|
190
|
+
| `getDomainForwarding` | ✓ | ✓ʲ | ✓ᵏ | ✓ˡ | ✗ | ✓ | ✓ʲ | ✓ | ✗ |
|
|
191
|
+
| `setDomainForwarding` | ✓ | ✓ʲ | ✓ᵏ | ✓ˡ | ✗ | ✓ | ✓ʲ | ✓ | ✗ |
|
|
192
|
+
|
|
193
|
+
`CF` Cloudflare · `DY` Dynadot · `GA` Gandi · `GD` GoDaddy · `NB` NameBright ·
|
|
194
|
+
`NC` Namecheap · `NS` NameSilo · `PB` Porkbun · `SP` Spaceship.
|
|
195
|
+
|
|
196
|
+
**Caveats**
|
|
197
|
+
|
|
198
|
+
- **ᵃ** `getPricing` is per-domain only (pass a full name, not a bare TLD) —
|
|
199
|
+
Dynadot and GoDaddy price via their availability endpoint and have no per-TLD
|
|
200
|
+
price table.
|
|
201
|
+
- **ᵇ** No pricing endpoint at all; a per-domain registration price is still
|
|
202
|
+
available through `checkAvailability`.
|
|
203
|
+
- **ᶜ** Cloudflare Registrar has no post-registration write API — auto-renew,
|
|
204
|
+
privacy, and lock are settable only at registration; nameserver changes require
|
|
205
|
+
Cloudflare support; there is no renew, transfer-in, or contact-update endpoint.
|
|
206
|
+
These throw with a message pointing to the dashboard.
|
|
207
|
+
- **ᵈ** NameBright's REST API has no transfer-in or contact-update endpoint.
|
|
208
|
+
- **ᵉ** Porkbun exposes the transfer-lock and privacy state read-only (settable
|
|
209
|
+
only at registration) and has no WHOIS/contact-read endpoint.
|
|
210
|
+
- **ᶠ** Gandi `setPrivacy(false)` is a no-op for individual registrants — WHOIS
|
|
211
|
+
stays obfuscated for GDPR.
|
|
212
|
+
- **ᵍ** GoDaddy `setPrivacy`: enabling is a paid add-on (not implemented);
|
|
213
|
+
disabling works for paid privacy, but GoDaddy's free privacy ("Free DBP")
|
|
214
|
+
cannot be disabled via the API (returns a clear error pointing to the dashboard).
|
|
215
|
+
- **ʰ** `setDnsRecords` writes cover the common record types; SRV/CAA and other
|
|
216
|
+
sub-field-bearing types throw on write (reads handle every type).
|
|
217
|
+
- **ⁱ** Namecheap and NameSilo only email the EPP/auth code to the registrant;
|
|
218
|
+
it can't be returned synchronously.
|
|
219
|
+
- **ʲ** Dynadot and NameSilo forward the whole domain (a single `@` rule);
|
|
220
|
+
clearing a forward restores the default nameservers.
|
|
221
|
+
- **ᵏ** Gandi web redirects are subdomain-only — the apex (`@`) is rejected.
|
|
222
|
+
- **ˡ** GoDaddy domain forwarding uses the v2 customer-scoped API, so it requires
|
|
223
|
+
a `customerId` (the customer UUID, or a numeric shopper ID the client resolves to
|
|
224
|
+
the UUID via an sso-key-only lookup); the old v1 forwarding route was removed.
|
|
225
|
+
|
|
226
|
+
For the deeper cross-provider notes and per-registrar quirks, see
|
|
227
|
+
[docs/registrars/FEATURES.md](docs/registrars/FEATURES.md).
|
|
228
|
+
|
|
229
|
+
## Capabilities
|
|
230
|
+
|
|
231
|
+
Registrars don't all support the same operations. **Core** is a fixed contract
|
|
232
|
+
every provider is expected to fulfil (inherited by extending `BaseRegistrar`);
|
|
233
|
+
**extended** capabilities are opt-in and declared per provider. Where a core
|
|
234
|
+
method's API path isn't wired up yet, it throws `NotImplementedError` — the
|
|
235
|
+
contract is the promise, not a claim that every byte is implemented today.
|
|
236
|
+
|
|
237
|
+
Reference capabilities through the `Feature` object (autocomplete +
|
|
238
|
+
find-all-references), and introspect statically — no instance or credentials
|
|
239
|
+
needed, which makes this easy to surface to a UI or an MCP tool:
|
|
240
|
+
|
|
241
|
+
```ts
|
|
242
|
+
import { registrars, createRegistrar, Feature, CORE_FEATURES } from '@aoxborrow/registrar-client';
|
|
243
|
+
|
|
244
|
+
registrars.dynadot.features; // full set: core + Dynadot's extras
|
|
245
|
+
registrars.dynadot.extendedFeatures; // just the extras
|
|
246
|
+
CORE_FEATURES.includes(Feature.GetDnsRecords); // true — DNS is core
|
|
247
|
+
|
|
248
|
+
const gd = createRegistrar('godaddy', creds);
|
|
249
|
+
gd.supports(Feature.RegisterDomain); // true (core)
|
|
250
|
+
gd.supports(Feature.GetAuthCode); // true (its extended)
|
|
251
|
+
gd.supports(Feature.SetDomainForwarding); // false (not declared)
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
See [docs/registrars/FEATURES.md](docs/registrars/FEATURES.md) for the full
|
|
255
|
+
feature matrix and the core vs. extended breakdown.
|
|
256
|
+
|
|
257
|
+
## Forwarding
|
|
258
|
+
|
|
259
|
+
Two opt-in extended capabilities cover forwarding, both modelled as replace-all
|
|
260
|
+
sets:
|
|
261
|
+
|
|
262
|
+
- **Domain (URL) forwarding** — `getDomainForwarding` / `setDomainForwarding`
|
|
263
|
+
take `DomainForward[]` (`{ host, url, type }`). `host` is apex-relative (`@`,
|
|
264
|
+
`www`, …); `type` is `temporary` (302) or `permanent` (301). HTTPS targets work
|
|
265
|
+
wherever the provider issues a certificate for the domain (all supported
|
|
266
|
+
providers do).
|
|
267
|
+
- **Email forwarding** — `getEmailForwarding` / `setEmailForwarding` take
|
|
268
|
+
`EmailForward[]` (`{ alias, forwardTo }`). `alias` is the local part; `*` (or
|
|
269
|
+
`@`) is the catch-all. This is alias→inbox **forwarding**, not mailbox hosting.
|
|
270
|
+
|
|
271
|
+
**Masked/framed forwarding is read-only.** `DomainForwardType` also has a
|
|
272
|
+
`masked` value — a "cloaked" forward where an iframe keeps the source URL in the
|
|
273
|
+
address bar. The library **never creates** one (`setDomainForwarding` rejects
|
|
274
|
+
`masked`): it breaks HTTPS, SEO, and modern browser protections, and several
|
|
275
|
+
providers (e.g. Cloudflare) can't do it at all. But `getDomainForwarding`
|
|
276
|
+
**reports** `masked` truthfully when a provider already has one configured, so
|
|
277
|
+
reading state stays accurate. (One consequence: reading a masked forward and
|
|
278
|
+
writing the same array straight back is rejected — by design.)
|
|
279
|
+
|
|
280
|
+
Providers that expose a native forwarding feature do this in one call.
|
|
281
|
+
**Cloudflare** has no such primitive, so the provider composes it from other
|
|
282
|
+
Cloudflare APIs: domain forwarding writes a Rules redirect plus a proxied
|
|
283
|
+
placeholder DNS record so the edge can apply it, and email forwarding uses Email
|
|
284
|
+
Routing (enabling it adds the required MX/SPF records). Email Routing
|
|
285
|
+
destinations must be verified on the Cloudflare account before a rule activates;
|
|
286
|
+
where the token can manage addresses, `setEmailForwarding` adds any unknown
|
|
287
|
+
destination (sending its verification email) and reports which are still
|
|
288
|
+
pending. See [docs/registrars/cloudflare.md](docs/registrars/cloudflare.md).
|
|
289
|
+
|
|
290
|
+
## Registering & transferring domains
|
|
291
|
+
|
|
292
|
+
Registering or transferring a domain forms a legal contract with the registry, so
|
|
293
|
+
registrars gate `registerDomain`/`transferIn` behind **consent** to their
|
|
294
|
+
agreements. Consent is supplied **per operation** (matching how the registrar
|
|
295
|
+
APIs model it — there is no "consent once" server state):
|
|
296
|
+
|
|
297
|
+
```ts
|
|
298
|
+
await client.registerDomain('example.com', {
|
|
299
|
+
years: 1,
|
|
300
|
+
contacts: { registrant: { firstName: 'Ada', lastName: 'Lovelace' /* … */ } },
|
|
301
|
+
consent: { agreedBy: userIpAddress }, // the consenting party's IP
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
await client.transferIn('example.com', {
|
|
305
|
+
authCode: 'EPP-CODE-FROM-LOSING-REGISTRAR',
|
|
306
|
+
consent: { agreedBy: userIpAddress },
|
|
307
|
+
});
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
You only affirm **who** consents (`agreedBy` — the consenting user's IP address)
|
|
311
|
+
and optionally **when** (`agreedAt`, defaulting to now). The provider fetches the
|
|
312
|
+
specific per-TLD agreement documents itself and attaches them. Omitting `consent`
|
|
313
|
+
throws `ConsentRequiredError` (distinct from `NotImplementedError` — the
|
|
314
|
+
capability exists; you just have to consent).
|
|
315
|
+
|
|
316
|
+
Contacts are supplied on the same call, where the registrar needs them: GoDaddy
|
|
317
|
+
and Spaceship take a full contact set on registration (omitted roles fall back to
|
|
318
|
+
the registrant); Namecheap requires all four roles; Dynadot and Porkbun use the
|
|
319
|
+
account's default WHOIS contact. Transfers carry over the existing contacts, so
|
|
320
|
+
most providers need only the auth code (`TransferDomainInput`).
|
|
321
|
+
|
|
322
|
+
> `registerDomain` is implemented for **every bundled provider**, and `transferIn`
|
|
323
|
+
> for every one whose API has a transfer endpoint (all except **Cloudflare** and
|
|
324
|
+
> **NameBright**). `registerDomain` has been exercised live against several
|
|
325
|
+
> sandboxes and one real account (a live Cloudflare `.dev` registration), but since
|
|
326
|
+
> these operations spend real money, treat any path you haven't run yourself as
|
|
327
|
+
> documented-but-unverified. Known gap: Namecheap registration doesn't yet send
|
|
328
|
+
> per-TLD extended attributes, so TLDs that require them (`.us`, `.eu`, …) aren't
|
|
329
|
+
> registrable there yet.
|
|
330
|
+
|
|
331
|
+
## Sandbox environments
|
|
332
|
+
|
|
333
|
+
Providers that offer a test environment accept `{ environment: 'sandbox' }` at
|
|
334
|
+
construction, so you can exercise real API calls without touching live domains:
|
|
335
|
+
|
|
336
|
+
```ts
|
|
337
|
+
const gd = createRegistrar('godaddy', oteCredentials, { environment: 'sandbox' });
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
Requesting `sandbox` on a provider that has none (`cloudflare`, `namebright`,
|
|
341
|
+
`spaceship`) throws a `ConfigurationError` — a test can never silently hit
|
|
342
|
+
production. Check `SomeRegistrar.supportsSandbox` to discover which do.
|
|
343
|
+
|
|
344
|
+
Some providers key their sandbox off the **credential**, not a separate host.
|
|
345
|
+
**Porkbun**'s sandbox shares the production base URL — a sandbox key is prefixed
|
|
346
|
+
`pk1_sb_`, starts with $1,000 of fake credit, and marks every response with
|
|
347
|
+
`"sandbox": true` (top up / reset via `POST /sandbox/topup` · `POST
|
|
348
|
+
/sandbox/reset`). There, `{ environment: 'sandbox' }` is cosmetic; the `pk1_sb_`
|
|
349
|
+
key is what routes to the test environment.
|
|
350
|
+
|
|
351
|
+
### Integration tests
|
|
352
|
+
|
|
353
|
+
Real sandbox integration tests live in `test/integration/` and are **credential-
|
|
354
|
+
gated**: each registrar's suite skips unless its env vars are set, so the default
|
|
355
|
+
suite stays offline.
|
|
356
|
+
|
|
357
|
+
```bash
|
|
358
|
+
cp .env.example .env # fill in sandbox credentials for the registrars you want
|
|
359
|
+
npm run test:integration
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
They exercise read-only operations (`testConnection`, `listDomains`) against each
|
|
363
|
+
configured sandbox. Unit tests (`npm test`) never make network calls.
|
|
364
|
+
|
|
365
|
+
## Architecture
|
|
366
|
+
|
|
367
|
+
The package separates the **caller-facing facade** from **provider
|
|
368
|
+
implementations**:
|
|
369
|
+
|
|
370
|
+
- **`RegistrarClient`** — a thin, provider-agnostic facade. Normalizes input and
|
|
371
|
+
delegates to the configured provider.
|
|
372
|
+
- **`Registrar`** — the core interface every backend implements (the 18 core
|
|
373
|
+
operations listed under [Usage](#usage), plus the capability accessors).
|
|
374
|
+
Defined in `src/types.ts`, with shared payload types (`Contact`, `DnsRecord`,
|
|
375
|
+
`DomainAvailability`, `TldPricing`, `RegisterDomainInput`) alongside it.
|
|
376
|
+
- **`BaseRegistrar`** (`src/registrar.ts`) — an abstract base that owns the
|
|
377
|
+
shared HTTP/auth/retry plumbing (via `HttpClient`) and provides
|
|
378
|
+
`NotImplementedError`-rejecting defaults. Concrete providers under
|
|
379
|
+
`src/registrars/` extend it and override only what their API supports.
|
|
380
|
+
- **`HttpClient`** — a small `fetch` wrapper with timeout, `AbortSignal`
|
|
381
|
+
linking, retry-with-backoff, `request`/`requestText` (JSON and raw/XML), and
|
|
382
|
+
typed error mapping. No Node built-ins.
|
|
383
|
+
- **`parseXml` / `ensureArray`** — shared XML helpers (backed by
|
|
384
|
+
`fast-xml-parser`) with one consistent config across XML providers: attributes
|
|
385
|
+
kept, values left as raw strings so providers coerce them explicitly.
|
|
386
|
+
- **`registrars` / `createRegistrar`** — a lookup of the built-in providers by
|
|
387
|
+
id, and a factory to construct one.
|
|
388
|
+
|
|
389
|
+
### Adding a provider
|
|
390
|
+
|
|
391
|
+
Extend `BaseRegistrar`, pass a `baseUrl` + auth headers to `super()`, add the
|
|
392
|
+
static `displayName` / `configFields` / `helpText` metadata, override the
|
|
393
|
+
operations the API supports (mapping payloads to the shared types in
|
|
394
|
+
`src/types.ts`), and add the class to `src/registrars/index.ts`. The existing
|
|
395
|
+
providers under `src/registrars/` are working references.
|
|
396
|
+
|
|
397
|
+
**New providers are welcome** — if you've wired up a registrar, open a PR.
|
|
398
|
+
|
|
399
|
+
## Errors
|
|
400
|
+
|
|
401
|
+
All failures throw a typed subclass of `RegistrarError` (`AuthenticationError`,
|
|
402
|
+
`NotFoundError`, `RateLimitError`, `TimeoutError`, `NotImplementedError`, …),
|
|
403
|
+
each carrying an HTTP-style `status`.
|
|
404
|
+
|
|
405
|
+
## Security
|
|
406
|
+
|
|
407
|
+
Credentials are sent as request **headers** wherever the registrar's API
|
|
408
|
+
supports it. Two providers require query-string authentication by their own API
|
|
409
|
+
design — **Dynadot** (`key`) and **Namecheap** (`ApiUser`/`ApiKey`) — so for
|
|
410
|
+
those the key necessarily appears in the request URL. This is noted in each
|
|
411
|
+
provider's source. Credentials are not interpolated into error messages.
|
|
412
|
+
|
|
413
|
+
## License
|
|
414
|
+
|
|
415
|
+
MIT © Aaron Oxborrow
|