@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/package.json
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@aoxborrow/registrar-client",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "One consistent, fully-typed TypeScript interface to many domain registrar APIs (GoDaddy, Cloudflare, Namecheap, Spaceship, Dynadot, Porkbun, and more) behind a single pluggable multi-provider client",
|
|
5
|
+
"author": {
|
|
6
|
+
"name": "Aaron Oxborrow",
|
|
7
|
+
"url": "https://github.com/aoxborrow"
|
|
8
|
+
},
|
|
9
|
+
"license": "MIT",
|
|
10
|
+
"publishConfig": {
|
|
11
|
+
"access": "public"
|
|
12
|
+
},
|
|
13
|
+
"homepage": "https://github.com/aoxborrow/registrar-client",
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "git+https://github.com/aoxborrow/registrar-client.git"
|
|
17
|
+
},
|
|
18
|
+
"bugs": {
|
|
19
|
+
"url": "https://github.com/aoxborrow/registrar-client/issues"
|
|
20
|
+
},
|
|
21
|
+
"keywords": [
|
|
22
|
+
"registrar",
|
|
23
|
+
"registrar-api",
|
|
24
|
+
"domains",
|
|
25
|
+
"domain-registration",
|
|
26
|
+
"domain-management",
|
|
27
|
+
"dns",
|
|
28
|
+
"epp",
|
|
29
|
+
"whois",
|
|
30
|
+
"rdap",
|
|
31
|
+
"typescript",
|
|
32
|
+
"cloudflare",
|
|
33
|
+
"godaddy",
|
|
34
|
+
"namecheap",
|
|
35
|
+
"gandi",
|
|
36
|
+
"porkbun",
|
|
37
|
+
"dynadot",
|
|
38
|
+
"namesilo",
|
|
39
|
+
"spaceship",
|
|
40
|
+
"namebright"
|
|
41
|
+
],
|
|
42
|
+
"type": "module",
|
|
43
|
+
"main": "./dist/index.cjs",
|
|
44
|
+
"module": "./dist/index.js",
|
|
45
|
+
"types": "./dist/index.d.ts",
|
|
46
|
+
"exports": {
|
|
47
|
+
".": {
|
|
48
|
+
"types": "./dist/index.d.ts",
|
|
49
|
+
"import": "./dist/index.js",
|
|
50
|
+
"require": "./dist/index.cjs",
|
|
51
|
+
"default": "./dist/index.js"
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
"files": [
|
|
55
|
+
"dist",
|
|
56
|
+
"src",
|
|
57
|
+
"README.md",
|
|
58
|
+
"LICENSE"
|
|
59
|
+
],
|
|
60
|
+
"engines": {
|
|
61
|
+
"node": ">=20.0.0"
|
|
62
|
+
},
|
|
63
|
+
"sideEffects": false,
|
|
64
|
+
"scripts": {
|
|
65
|
+
"build": "tsup",
|
|
66
|
+
"lint": "npm run tsc && npm run eslint && npm run prettier",
|
|
67
|
+
"lint:fix": "npm run eslint:fix && npm run prettier:fix",
|
|
68
|
+
"eslint": "eslint --cache --cache-location .eslintcache .",
|
|
69
|
+
"eslint:fix": "eslint --cache --cache-location .eslintcache . --fix",
|
|
70
|
+
"prettier": "prettier --check .",
|
|
71
|
+
"prettier:fix": "prettier --write --list-different .",
|
|
72
|
+
"ts": "npm run lint",
|
|
73
|
+
"ts:fix": "npm run lint:fix",
|
|
74
|
+
"tsc": "tsc --noEmit",
|
|
75
|
+
"typecheck": "npm run tsc",
|
|
76
|
+
"test": "vitest run",
|
|
77
|
+
"test:watch": "vitest",
|
|
78
|
+
"test:coverage": "vitest run --coverage",
|
|
79
|
+
"test:integration": "vitest run --config vitest.integration.config.ts",
|
|
80
|
+
"test:integration:watch": "vitest --config vitest.integration.config.ts",
|
|
81
|
+
"clear:ts": "rm -rf tsconfig.tsbuildinfo tsconfig.build.tsbuildinfo",
|
|
82
|
+
"clear:eslint": "rm -rf .eslintcache",
|
|
83
|
+
"clear": "npm run clear:ts && npm run clear:eslint && rm -rf dist coverage",
|
|
84
|
+
"prepublishOnly": "npm run build",
|
|
85
|
+
"release:patch": "npm version patch && git push --follow-tags && gh release create v$(node -p \"require('./package.json').version\") --generate-notes",
|
|
86
|
+
"release:minor": "npm version minor && git push --follow-tags && gh release create v$(node -p \"require('./package.json').version\") --generate-notes",
|
|
87
|
+
"release:major": "npm version major && git push --follow-tags && gh release create v$(node -p \"require('./package.json').version\") --generate-notes"
|
|
88
|
+
},
|
|
89
|
+
"devDependencies": {
|
|
90
|
+
"@eslint/js": "^10.0.1",
|
|
91
|
+
"@types/node": "^26.2.0",
|
|
92
|
+
"@typescript-eslint/eslint-plugin": "^8.56.0",
|
|
93
|
+
"@typescript-eslint/parser": "^8.56.0",
|
|
94
|
+
"@vitest/coverage-v8": "^4.1.11",
|
|
95
|
+
"dotenv": "^17.4.2",
|
|
96
|
+
"eslint": "^10.9.0",
|
|
97
|
+
"eslint-config-prettier": "^10.1.8",
|
|
98
|
+
"eslint-import-resolver-typescript": "^4.4.4",
|
|
99
|
+
"eslint-plugin-import-x": "^4.17.1",
|
|
100
|
+
"globals": "^17.11.0",
|
|
101
|
+
"prettier": "^3.6.2",
|
|
102
|
+
"tsup": "^8.5.0",
|
|
103
|
+
"typescript": "^5.9.3",
|
|
104
|
+
"typescript-eslint": "^8.56.0",
|
|
105
|
+
"vitest": "^4.1.11"
|
|
106
|
+
},
|
|
107
|
+
"dependencies": {
|
|
108
|
+
"fast-xml-parser": "^5.11.0"
|
|
109
|
+
}
|
|
110
|
+
}
|
package/src/client.ts
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { normalizeDomain, normalizeNameservers } from './utils';
|
|
2
|
+
import type {
|
|
3
|
+
ConnectionResult,
|
|
4
|
+
ContactSet,
|
|
5
|
+
DnsRecord,
|
|
6
|
+
Domain,
|
|
7
|
+
DomainAvailability,
|
|
8
|
+
DomainForward,
|
|
9
|
+
EmailForward,
|
|
10
|
+
ListDomainsOptions,
|
|
11
|
+
OperationResult,
|
|
12
|
+
Registrar,
|
|
13
|
+
RegisterDomainInput,
|
|
14
|
+
RequestOptions,
|
|
15
|
+
TldPricing,
|
|
16
|
+
TransferDomainInput,
|
|
17
|
+
} from './types';
|
|
18
|
+
|
|
19
|
+
// A thin, provider-agnostic facade over a single `Registrar`. Normalizes
|
|
20
|
+
// domain names and delegates to the underlying provider.
|
|
21
|
+
//
|
|
22
|
+
// const client = new RegistrarClient(createRegistrar('cloudflare', creds));
|
|
23
|
+
// await client.listDomains();
|
|
24
|
+
export class RegistrarClient {
|
|
25
|
+
constructor(public readonly provider: Registrar) {}
|
|
26
|
+
|
|
27
|
+
testConnection(opts?: RequestOptions): Promise<ConnectionResult> {
|
|
28
|
+
return this.provider.testConnection(opts);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async listDomains(opts?: ListDomainsOptions): Promise<Domain[]> {
|
|
32
|
+
const domains = await this.provider.listDomains(opts);
|
|
33
|
+
if (!opts?.detailed) return domains;
|
|
34
|
+
return this.enrichWithDetail(domains, opts);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Fills fields that a provider's list endpoint omits (nameservers, privacy,
|
|
38
|
+
// lock) by fetching each domain's detail and merging it over the summary.
|
|
39
|
+
// Runs with bounded concurrency; a per-domain detail failure leaves that
|
|
40
|
+
// domain's summary values untouched rather than failing the whole listing.
|
|
41
|
+
private async enrichWithDetail(domains: Domain[], opts: RequestOptions): Promise<Domain[]> {
|
|
42
|
+
const CONCURRENCY = 5;
|
|
43
|
+
const result = [...domains];
|
|
44
|
+
let next = 0;
|
|
45
|
+
const worker = async (): Promise<void> => {
|
|
46
|
+
while (next < domains.length) {
|
|
47
|
+
const index = next++;
|
|
48
|
+
const name = domains[index].domainName;
|
|
49
|
+
try {
|
|
50
|
+
const detail = await this.provider.getDomain(name, opts);
|
|
51
|
+
let merged = { ...domains[index], ...detail };
|
|
52
|
+
// Some providers expose nameservers only via a dedicated endpoint, not
|
|
53
|
+
// on the domain detail — fetch them separately when still empty.
|
|
54
|
+
if (merged.nameservers.length === 0) {
|
|
55
|
+
try {
|
|
56
|
+
const nameservers = normalizeNameservers(
|
|
57
|
+
await this.provider.getNameservers(name, opts)
|
|
58
|
+
);
|
|
59
|
+
if (nameservers.length > 0) merged = { ...merged, nameservers };
|
|
60
|
+
} catch {
|
|
61
|
+
// leave nameservers empty if this provider can't supply them
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
result[index] = merged;
|
|
65
|
+
} catch {
|
|
66
|
+
// keep the summary values for this domain on detail failure
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
const workers = Array.from({ length: Math.min(CONCURRENCY, domains.length) }, worker);
|
|
71
|
+
await Promise.all(workers);
|
|
72
|
+
return result;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
getDomain(domainName: string, opts?: RequestOptions): Promise<Domain> {
|
|
76
|
+
return this.provider.getDomain(normalizeDomain(domainName), opts);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
checkAvailability(domainNames: string[], opts?: RequestOptions): Promise<DomainAvailability[]> {
|
|
80
|
+
return this.provider.checkAvailability(domainNames.map(normalizeDomain), opts);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
getPricing(tldOrDomain: string, opts?: RequestOptions): Promise<TldPricing> {
|
|
84
|
+
return this.provider.getPricing(normalizeDomain(tldOrDomain), opts);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
registerDomain(
|
|
88
|
+
domainName: string,
|
|
89
|
+
input: RegisterDomainInput,
|
|
90
|
+
opts?: RequestOptions
|
|
91
|
+
): Promise<OperationResult> {
|
|
92
|
+
return this.provider.registerDomain(normalizeDomain(domainName), input, opts);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
renewDomain(domainName: string, years?: number, opts?: RequestOptions): Promise<OperationResult> {
|
|
96
|
+
return this.provider.renewDomain(normalizeDomain(domainName), years, opts);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
setAutoRenew(
|
|
100
|
+
domainName: string,
|
|
101
|
+
enabled: boolean,
|
|
102
|
+
opts?: RequestOptions
|
|
103
|
+
): Promise<OperationResult> {
|
|
104
|
+
return this.provider.setAutoRenew(normalizeDomain(domainName), enabled, opts);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
transferIn(
|
|
108
|
+
domainName: string,
|
|
109
|
+
input: TransferDomainInput,
|
|
110
|
+
opts?: RequestOptions
|
|
111
|
+
): Promise<OperationResult> {
|
|
112
|
+
return this.provider.transferIn(normalizeDomain(domainName), input, opts);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
updateNameservers(
|
|
116
|
+
domainName: string,
|
|
117
|
+
nameservers: string[],
|
|
118
|
+
opts?: RequestOptions
|
|
119
|
+
): Promise<OperationResult> {
|
|
120
|
+
return this.provider.updateNameservers(normalizeDomain(domainName), nameservers, opts);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async getNameservers(domainName: string, opts?: RequestOptions): Promise<string[]> {
|
|
124
|
+
return normalizeNameservers(
|
|
125
|
+
await this.provider.getNameservers(normalizeDomain(domainName), opts)
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
lockDomain(domainName: string, opts?: RequestOptions): Promise<OperationResult> {
|
|
130
|
+
return this.provider.lockDomain(normalizeDomain(domainName), opts);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
unlockDomain(domainName: string, opts?: RequestOptions): Promise<OperationResult> {
|
|
134
|
+
return this.provider.unlockDomain(normalizeDomain(domainName), opts);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
setPrivacy(
|
|
138
|
+
domainName: string,
|
|
139
|
+
enabled: boolean,
|
|
140
|
+
opts?: RequestOptions
|
|
141
|
+
): Promise<OperationResult> {
|
|
142
|
+
return this.provider.setPrivacy(normalizeDomain(domainName), enabled, opts);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
getContacts(domainName: string, opts?: RequestOptions): Promise<ContactSet> {
|
|
146
|
+
return this.provider.getContacts(normalizeDomain(domainName), opts);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
updateContacts(
|
|
150
|
+
domainName: string,
|
|
151
|
+
contacts: ContactSet,
|
|
152
|
+
opts?: RequestOptions
|
|
153
|
+
): Promise<OperationResult> {
|
|
154
|
+
return this.provider.updateContacts(normalizeDomain(domainName), contacts, opts);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
getDnsRecords(domainName: string, opts?: RequestOptions): Promise<DnsRecord[]> {
|
|
158
|
+
return this.provider.getDnsRecords(normalizeDomain(domainName), opts);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
setDnsRecords(
|
|
162
|
+
domainName: string,
|
|
163
|
+
records: DnsRecord[],
|
|
164
|
+
opts?: RequestOptions
|
|
165
|
+
): Promise<OperationResult> {
|
|
166
|
+
return this.provider.setDnsRecords(normalizeDomain(domainName), records, opts);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// --- extended: forwarding ---
|
|
170
|
+
//
|
|
171
|
+
// Opt-in per provider (gate on `provider.supports(Feature.…)`); providers that
|
|
172
|
+
// don't declare the feature reject with NotImplementedError. Email forwarding
|
|
173
|
+
// (alias → address) and URL/domain forwarding (host → URL redirect) are
|
|
174
|
+
// unrelated capabilities with separate feature flags. Each `set…` is a full
|
|
175
|
+
// replace: any rule omitted is removed, an empty array clears forwarding.
|
|
176
|
+
|
|
177
|
+
getEmailForwarding(domainName: string, opts?: RequestOptions): Promise<EmailForward[]> {
|
|
178
|
+
return this.provider.getEmailForwarding(normalizeDomain(domainName), opts);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
setEmailForwarding(
|
|
182
|
+
domainName: string,
|
|
183
|
+
forwards: EmailForward[],
|
|
184
|
+
opts?: RequestOptions
|
|
185
|
+
): Promise<OperationResult> {
|
|
186
|
+
return this.provider.setEmailForwarding(normalizeDomain(domainName), forwards, opts);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
getDomainForwarding(domainName: string, opts?: RequestOptions): Promise<DomainForward[]> {
|
|
190
|
+
return this.provider.getDomainForwarding(normalizeDomain(domainName), opts);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
setDomainForwarding(
|
|
194
|
+
domainName: string,
|
|
195
|
+
forwards: DomainForward[],
|
|
196
|
+
opts?: RequestOptions
|
|
197
|
+
): Promise<OperationResult> {
|
|
198
|
+
return this.provider.setDomainForwarding(normalizeDomain(domainName), forwards, opts);
|
|
199
|
+
}
|
|
200
|
+
}
|
package/src/constants.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { RegistrarClientOptions } from './types';
|
|
2
|
+
|
|
3
|
+
// default client-level options
|
|
4
|
+
export const DEFAULT_OPTIONS: RegistrarClientOptions = {
|
|
5
|
+
timeout: 30_000, // 30s — registrar APIs can be slow, especially for registration
|
|
6
|
+
retries: 2,
|
|
7
|
+
backoff: 250,
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
// package identifier used in the default User-Agent header
|
|
11
|
+
export const USER_AGENT = '@aoxborrow/registrar-client';
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
// convert any thrown value to a RegistrarError (preserves existing RegistrarError instances)
|
|
2
|
+
export function toRegistrarError(error: unknown): RegistrarError {
|
|
3
|
+
// already a RegistrarError, return as-is
|
|
4
|
+
if (error instanceof RegistrarError) {
|
|
5
|
+
return error;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
// extract message from Error or convert unknown to string
|
|
9
|
+
const message =
|
|
10
|
+
error instanceof Error ? error.message : String(error) || 'An unknown error occurred';
|
|
11
|
+
|
|
12
|
+
// wrap in a concrete RegistrarError instance
|
|
13
|
+
const wrapped = new (class extends RegistrarError {})(message);
|
|
14
|
+
|
|
15
|
+
// preserve the original error's identity where possible
|
|
16
|
+
if (error instanceof Error) {
|
|
17
|
+
wrapped.name = error.name;
|
|
18
|
+
wrapped.stack = error.stack;
|
|
19
|
+
} else {
|
|
20
|
+
wrapped.name = 'RegistrarError';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return wrapped;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// base error class for registrar client errors
|
|
27
|
+
// `status` mirrors an HTTP-style status code where meaningful
|
|
28
|
+
export class RegistrarError extends Error {
|
|
29
|
+
public status: number;
|
|
30
|
+
// optional machine-readable code from the underlying registrar API
|
|
31
|
+
public providerCode?: string;
|
|
32
|
+
|
|
33
|
+
constructor(message: string) {
|
|
34
|
+
super(message);
|
|
35
|
+
this.name = 'RegistrarError';
|
|
36
|
+
this.status = -1;
|
|
37
|
+
|
|
38
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
39
|
+
|
|
40
|
+
// maintain proper stack trace (V8/Node only)
|
|
41
|
+
if (Error.captureStackTrace) {
|
|
42
|
+
Error.captureStackTrace(this, this.constructor);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// determine if this error should trigger a retry
|
|
47
|
+
// overridden by subclasses for custom logic
|
|
48
|
+
shouldRetry(): boolean {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// base class for transient errors that are safe to retry
|
|
54
|
+
export class RetryableRegistrarError extends RegistrarError {
|
|
55
|
+
constructor(message: string) {
|
|
56
|
+
super(message);
|
|
57
|
+
this.name = 'RetryableRegistrarError';
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
shouldRetry(): boolean {
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// request timed out
|
|
66
|
+
export class TimeoutError extends RetryableRegistrarError {
|
|
67
|
+
public status = 408; // Request Timeout
|
|
68
|
+
|
|
69
|
+
constructor(message: string) {
|
|
70
|
+
super(message);
|
|
71
|
+
this.name = 'TimeoutError';
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// network/connection failure reaching the registrar API
|
|
76
|
+
export class ConnectionError extends RetryableRegistrarError {
|
|
77
|
+
public status = 503; // Service Unavailable
|
|
78
|
+
|
|
79
|
+
constructor(message: string) {
|
|
80
|
+
super(message);
|
|
81
|
+
this.name = 'ConnectionError';
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// registrar API rejected the credentials
|
|
86
|
+
export class AuthenticationError extends RegistrarError {
|
|
87
|
+
public status = 401; // Unauthorized
|
|
88
|
+
|
|
89
|
+
constructor(message: string) {
|
|
90
|
+
super(message);
|
|
91
|
+
this.name = 'AuthenticationError';
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// caller lacks permission for the requested operation
|
|
96
|
+
export class AuthorizationError extends RegistrarError {
|
|
97
|
+
public status = 403; // Forbidden
|
|
98
|
+
|
|
99
|
+
constructor(message: string) {
|
|
100
|
+
super(message);
|
|
101
|
+
this.name = 'AuthorizationError';
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// requested resource (domain, contact, etc.) was not found
|
|
106
|
+
export class NotFoundError extends RegistrarError {
|
|
107
|
+
public status = 404; // Not Found
|
|
108
|
+
|
|
109
|
+
constructor(message: string) {
|
|
110
|
+
super(message);
|
|
111
|
+
this.name = 'NotFoundError';
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// registrar API rate limit exceeded
|
|
116
|
+
export class RateLimitError extends RetryableRegistrarError {
|
|
117
|
+
public status = 429; // Too Many Requests
|
|
118
|
+
// seconds to wait before retrying, if provided by the API
|
|
119
|
+
public retryAfter?: number;
|
|
120
|
+
|
|
121
|
+
constructor(message: string, retryAfter?: number) {
|
|
122
|
+
super(message);
|
|
123
|
+
this.name = 'RateLimitError';
|
|
124
|
+
this.retryAfter = retryAfter;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// registrar API returned a response we couldn't understand
|
|
129
|
+
export class InvalidResponseError extends RetryableRegistrarError {
|
|
130
|
+
public status = 502; // Bad Gateway
|
|
131
|
+
|
|
132
|
+
constructor(message: string) {
|
|
133
|
+
super(message);
|
|
134
|
+
this.name = 'InvalidResponseError';
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// failed to parse a response payload
|
|
139
|
+
export class ParsingError extends RegistrarError {
|
|
140
|
+
public status = 422; // Unprocessable Entity
|
|
141
|
+
|
|
142
|
+
constructor(message: string) {
|
|
143
|
+
super(message);
|
|
144
|
+
this.name = 'ParsingError';
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// invalid client configuration or arguments
|
|
149
|
+
export class ConfigurationError extends RegistrarError {
|
|
150
|
+
public status = 500; // Internal Server Error
|
|
151
|
+
|
|
152
|
+
constructor(message: string) {
|
|
153
|
+
super(message);
|
|
154
|
+
this.name = 'ConfigurationError';
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// a registration/transfer was attempted without the consent the registrar
|
|
159
|
+
// requires (accepting its registration agreements). Distinct from
|
|
160
|
+
// NotImplementedError: the capability exists, but the caller must supply consent.
|
|
161
|
+
export class ConsentRequiredError extends RegistrarError {
|
|
162
|
+
public status = 400; // Bad Request
|
|
163
|
+
|
|
164
|
+
constructor(message: string) {
|
|
165
|
+
super(message);
|
|
166
|
+
this.name = 'ConsentRequiredError';
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// the requested capability is not implemented by this provider
|
|
171
|
+
export class NotImplementedError extends RegistrarError {
|
|
172
|
+
public status = 501; // Not Implemented
|
|
173
|
+
|
|
174
|
+
constructor(message: string) {
|
|
175
|
+
super(message);
|
|
176
|
+
this.name = 'NotImplementedError';
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// request was cancelled via an AbortSignal
|
|
181
|
+
export class AbortError extends RegistrarError {
|
|
182
|
+
public status = 499; // Client Closed Request
|
|
183
|
+
|
|
184
|
+
constructor(message: string) {
|
|
185
|
+
super(message);
|
|
186
|
+
this.name = 'AbortError';
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
shouldRetry(): boolean {
|
|
190
|
+
return false;
|
|
191
|
+
}
|
|
192
|
+
}
|
package/src/features.ts
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// Capability model for registrar providers.
|
|
2
|
+
//
|
|
3
|
+
// Features are identified by the string constants on `Feature`. Always reference
|
|
4
|
+
// them through that object (e.g. `Feature.GetPricing`) rather than writing the
|
|
5
|
+
// raw string — you get autocomplete and find-all-references, and typos become
|
|
6
|
+
// compile errors.
|
|
7
|
+
//
|
|
8
|
+
// There are two kinds of feature:
|
|
9
|
+
//
|
|
10
|
+
// - CORE — the contract every provider is expected to fulfil. A provider
|
|
11
|
+
// inherits the core surface by extending `BaseRegistrar`; it does not
|
|
12
|
+
// re-declare it. Core is a deliberate product decision about what this
|
|
13
|
+
// library guarantees, NOT the intersection of what today's registrars
|
|
14
|
+
// happen to expose — so a single registrar's missing/undocumented endpoint
|
|
15
|
+
// never demotes a feature out of core. Where a registrar's API path for a
|
|
16
|
+
// core feature isn't wired up yet (or hasn't been found — e.g. DNS via
|
|
17
|
+
// Cloudflare's separate Zones API), the method throws `NotImplementedError`
|
|
18
|
+
// until implemented. The contract is the promise; `NotImplementedError` is
|
|
19
|
+
// the "not yet".
|
|
20
|
+
//
|
|
21
|
+
// - EXTENDED — opt-in capabilities a provider declares via its static
|
|
22
|
+
// `extendedFeatures`. This is where providers genuinely differ.
|
|
23
|
+
//
|
|
24
|
+
// A provider's full capability set is `CORE_FEATURES` ∪ its `extendedFeatures`,
|
|
25
|
+
// exposed as `provider.features` (and statically as `Registrar.features`).
|
|
26
|
+
|
|
27
|
+
// All feature identifiers. Reference these instead of string literals.
|
|
28
|
+
export const Feature = {
|
|
29
|
+
// --- core (guaranteed contract; inherited from BaseRegistrar) ---
|
|
30
|
+
TestConnection: 'testConnection', // verify credentials
|
|
31
|
+
ListDomains: 'listDomains', // list domains in the account
|
|
32
|
+
GetDomain: 'getDomain', // fetch a single domain's details
|
|
33
|
+
CheckAvailability: 'checkAvailability', // check whether domains can be registered
|
|
34
|
+
GetPricing: 'getPricing', // TLD/domain pricing lookup
|
|
35
|
+
RegisterDomain: 'registerDomain', // register a new domain
|
|
36
|
+
RenewDomain: 'renewDomain', // renew a domain
|
|
37
|
+
SetAutoRenew: 'setAutoRenew', // toggle auto-renew
|
|
38
|
+
TransferIn: 'transferIn', // transfer a domain into the account
|
|
39
|
+
UpdateNameservers: 'updateNameservers', // replace a domain's nameservers
|
|
40
|
+
GetNameservers: 'getNameservers', // read a domain's nameservers
|
|
41
|
+
LockDomain: 'lockDomain', // enable the transfer lock
|
|
42
|
+
UnlockDomain: 'unlockDomain', // disable the transfer lock
|
|
43
|
+
SetPrivacy: 'setPrivacy', // toggle WHOIS privacy
|
|
44
|
+
GetContacts: 'getContacts', // read registrant/admin/tech contacts
|
|
45
|
+
UpdateContacts: 'updateContacts', // update registrant/admin/tech contacts
|
|
46
|
+
GetDnsRecords: 'getDnsRecords', // read DNS records
|
|
47
|
+
SetDnsRecords: 'setDnsRecords', // write DNS records
|
|
48
|
+
|
|
49
|
+
// --- extended (opt-in; declared per provider) ---
|
|
50
|
+
GetAuthCode: 'getAuthCode', // retrieve transfer auth/EPP code (transfer out)
|
|
51
|
+
GetDnssec: 'getDnssec', // read whether DNSSEC is enabled (DS / key records)
|
|
52
|
+
DisableDnssec: 'disableDnssec', // turn DNSSEC off (no enable / key management)
|
|
53
|
+
GetEmailForwarding: 'getEmailForwarding', // read alias-style email forwarding rules
|
|
54
|
+
SetEmailForwarding: 'setEmailForwarding', // alias-style email forwarding (redirect only)
|
|
55
|
+
GetDomainForwarding: 'getDomainForwarding', // read URL redirect / domain forwarding
|
|
56
|
+
SetDomainForwarding: 'setDomainForwarding', // URL redirect / domain forwarding
|
|
57
|
+
} as const;
|
|
58
|
+
|
|
59
|
+
// A registrar capability identifier — the value of one of the `Feature` members.
|
|
60
|
+
export type RegistrarFeature = (typeof Feature)[keyof typeof Feature];
|
|
61
|
+
|
|
62
|
+
// The guaranteed core contract: every provider is expected to support these.
|
|
63
|
+
export const CORE_FEATURES = [
|
|
64
|
+
Feature.TestConnection,
|
|
65
|
+
Feature.ListDomains,
|
|
66
|
+
Feature.GetDomain,
|
|
67
|
+
Feature.CheckAvailability,
|
|
68
|
+
Feature.GetPricing,
|
|
69
|
+
Feature.RegisterDomain,
|
|
70
|
+
Feature.RenewDomain,
|
|
71
|
+
Feature.SetAutoRenew,
|
|
72
|
+
Feature.TransferIn,
|
|
73
|
+
Feature.UpdateNameservers,
|
|
74
|
+
Feature.GetNameservers,
|
|
75
|
+
Feature.LockDomain,
|
|
76
|
+
Feature.UnlockDomain,
|
|
77
|
+
Feature.SetPrivacy,
|
|
78
|
+
Feature.GetContacts,
|
|
79
|
+
Feature.UpdateContacts,
|
|
80
|
+
Feature.GetDnsRecords,
|
|
81
|
+
Feature.SetDnsRecords,
|
|
82
|
+
] as const satisfies readonly RegistrarFeature[];
|
|
83
|
+
|
|
84
|
+
// Every feature that is not part of the core contract. A provider opts into any
|
|
85
|
+
// of these via its static `extendedFeatures`.
|
|
86
|
+
export const EXTENDED_FEATURES = [
|
|
87
|
+
Feature.GetAuthCode,
|
|
88
|
+
Feature.GetDnssec,
|
|
89
|
+
Feature.DisableDnssec,
|
|
90
|
+
Feature.GetEmailForwarding,
|
|
91
|
+
Feature.SetEmailForwarding,
|
|
92
|
+
Feature.GetDomainForwarding,
|
|
93
|
+
Feature.SetDomainForwarding,
|
|
94
|
+
] as const satisfies readonly RegistrarFeature[];
|
|
95
|
+
|
|
96
|
+
// Every feature, core first.
|
|
97
|
+
export const ALL_FEATURES = [...CORE_FEATURES, ...EXTENDED_FEATURES] as const;
|
|
98
|
+
|
|
99
|
+
const CORE_FEATURE_SET: ReadonlySet<RegistrarFeature> = new Set(CORE_FEATURES);
|
|
100
|
+
|
|
101
|
+
// true if `feature` is part of the guaranteed core contract
|
|
102
|
+
export function isCoreFeature(feature: RegistrarFeature): boolean {
|
|
103
|
+
return CORE_FEATURE_SET.has(feature);
|
|
104
|
+
}
|