@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/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
+ }
@@ -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
+ }
@@ -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
+ }