@aoxborrow/registrar-client 0.9.2 → 0.10.1
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/README.md +8 -5
- package/dist/index.cjs +139 -49
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +7 -2
- package/dist/index.d.ts +7 -2
- package/dist/index.js +139 -50
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/registrars/cloudflare.ts +2 -1
- package/src/registrars/domain101.ts +3 -1
- package/src/registrars/dynadot.ts +7 -3
- package/src/registrars/gandi.ts +144 -52
- package/src/types.ts +14 -0
- package/src/utils.ts +24 -4
package/README.md
CHANGED
|
@@ -231,7 +231,7 @@ not a claim of available API endpoints. See [101domain](docs/registrars/101domai
|
|
|
231
231
|
it can't be returned synchronously.
|
|
232
232
|
- **ʲ** Dynadot and NameSilo forward the whole domain (a single `@` rule);
|
|
233
233
|
clearing a forward restores the default nameservers.
|
|
234
|
-
- **ᵏ** Gandi web redirects are
|
|
234
|
+
- **ᵏ** Gandi web redirects are per host, apex included; creating one replaces the host's DNS record.
|
|
235
235
|
- **ˡ** GoDaddy domain forwarding uses the v2 customer-scoped API, so it requires
|
|
236
236
|
a `customerId` (the customer UUID, or a numeric shopper ID the client resolves to
|
|
237
237
|
the UUID via an sso-key-only lookup); the old v1 forwarding route was removed.
|
|
@@ -273,10 +273,13 @@ Two opt-in extended capabilities cover forwarding, both modelled as replace-all
|
|
|
273
273
|
sets:
|
|
274
274
|
|
|
275
275
|
- **Domain (URL) forwarding** — `getDomainForwarding` / `setDomainForwarding`
|
|
276
|
-
take `DomainForward[]` (`{ host, url, type }`). `host` is apex-relative
|
|
277
|
-
`www`, …); `type` is `temporary` (302) or `permanent` (301).
|
|
278
|
-
|
|
279
|
-
|
|
276
|
+
take `DomainForward[]` (`{ host, url, type, https? }`). `host` is apex-relative
|
|
277
|
+
(`@`, `www`, …); `type` is `temporary` (302) or `permanent` (301). The target's
|
|
278
|
+
scheme is part of `url`. `https` is whether the _source_ host answers on HTTPS
|
|
279
|
+
(`off` / `on` / `only`, where `only` also redirects HTTP to HTTPS). Only Gandi
|
|
280
|
+
sets it per rule; Cloudflare is always `on`; other providers manage it
|
|
281
|
+
themselves, leave it unset on read, and reject an explicit value. Unset on
|
|
282
|
+
write keeps a rule's current mode.
|
|
280
283
|
- **Email forwarding** — `getEmailForwarding` / `setEmailForwarding` take
|
|
281
284
|
`EmailForward[]` (`{ alias, forwardTo }`). `alias` is the local part; `*` (or
|
|
282
285
|
`@`) is the catch-all. This is alias→inbox **forwarding**, not mailbox hosting.
|
package/dist/index.cjs
CHANGED
|
@@ -204,12 +204,18 @@ function sleep(ms) {
|
|
|
204
204
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
205
205
|
}
|
|
206
206
|
var MASKED_FORWARD_UNSUPPORTED = 'masked/framed forwarding is not supported by this library; use "temporary" or "permanent"';
|
|
207
|
-
function settableForwards(forwards) {
|
|
207
|
+
function settableForwards(forwards, httpsModes = []) {
|
|
208
208
|
for (const f of forwards) {
|
|
209
209
|
if (f.type === "masked") throw new Error(MASKED_FORWARD_UNSUPPORTED);
|
|
210
|
+
if (f.https !== void 0 && !httpsModes.includes(f.https)) {
|
|
211
|
+
throw new Error(unsupportedForwardHttps(f.https, httpsModes));
|
|
212
|
+
}
|
|
210
213
|
}
|
|
211
214
|
return forwards;
|
|
212
215
|
}
|
|
216
|
+
function unsupportedForwardHttps(mode, httpsModes = []) {
|
|
217
|
+
return `forwarding https "${mode}" can't be set on this provider` + (httpsModes.length ? ` (supported: ${httpsModes.map((m) => `"${m}"`).join(", ")})` : "; leave `https` unset to use its default");
|
|
218
|
+
}
|
|
213
219
|
function requireConsent(provider, consent) {
|
|
214
220
|
if (!consent) {
|
|
215
221
|
throw new ConsentRequiredError(
|
|
@@ -1270,7 +1276,7 @@ var CloudflareRegistrar = class _CloudflareRegistrar extends BaseRegistrar {
|
|
|
1270
1276
|
*/
|
|
1271
1277
|
async setDomainForwarding(domainName, forwards, opts) {
|
|
1272
1278
|
try {
|
|
1273
|
-
const settable = settableForwards(forwards);
|
|
1279
|
+
const settable = settableForwards(forwards, ["on"]);
|
|
1274
1280
|
const zone = await this.requireZone(domainName, opts);
|
|
1275
1281
|
const existing = await this.getRedirectRuleset(zone.id, opts);
|
|
1276
1282
|
if (settable.length === 0) {
|
|
@@ -2128,6 +2134,8 @@ var Domain101Registrar = class extends BaseRegistrar {
|
|
|
2128
2134
|
throw new ConfigurationError(
|
|
2129
2135
|
"101domain: only one permanent apex forwarding rule is supported"
|
|
2130
2136
|
);
|
|
2137
|
+
if (forwards[0]?.https !== void 0)
|
|
2138
|
+
throw new ConfigurationError(`101domain: ${unsupportedForwardHttps(forwards[0].https)}`);
|
|
2131
2139
|
if (forwards.length) {
|
|
2132
2140
|
let url;
|
|
2133
2141
|
try {
|
|
@@ -2646,13 +2654,17 @@ ${bodyStr}`;
|
|
|
2646
2654
|
);
|
|
2647
2655
|
}
|
|
2648
2656
|
// Auto-renew via the renew_option endpoint. The accepted write values are
|
|
2649
|
-
// "auto" (auto-renew), "donot" (do not renew), and "reset" (manual renewal)
|
|
2650
|
-
//
|
|
2657
|
+
// "auto" (auto-renew), "donot" (do not renew), and "reset" (manual renewal);
|
|
2658
|
+
// they read back as "auto-renew", "do not renew", and "manual renewal". Off
|
|
2659
|
+
// writes "reset": off means "won't renew automatically", and "donot" would
|
|
2660
|
+
// also stop Dynadot's expiration reminder emails, so this never writes it.
|
|
2661
|
+
// "reset" is always manual renewal, even when the account's default renew
|
|
2662
|
+
// option is auto (sandbox-verified 2026-10-28).
|
|
2651
2663
|
async setAutoRenew(domainName, enabled, opts) {
|
|
2652
2664
|
return this.mutate(
|
|
2653
2665
|
"PUT",
|
|
2654
2666
|
`/restful/v2/domains/${encodeURIComponent(domainName)}/renew_option`,
|
|
2655
|
-
{ renew_option: enabled ? "auto" : "
|
|
2667
|
+
{ renew_option: enabled ? "auto" : "reset" },
|
|
2656
2668
|
`Auto-renew ${enabled ? "enabled" : "disabled"} successfully`,
|
|
2657
2669
|
opts
|
|
2658
2670
|
);
|
|
@@ -3068,6 +3080,16 @@ var GANDI_TYPE_TO_FORWARD = {
|
|
|
3068
3080
|
cloak: "masked"
|
|
3069
3081
|
// read-only; setDomainForwarding rejects it
|
|
3070
3082
|
};
|
|
3083
|
+
var GANDI_PROTOCOL = {
|
|
3084
|
+
off: "http",
|
|
3085
|
+
on: "https",
|
|
3086
|
+
only: "httpsonly"
|
|
3087
|
+
};
|
|
3088
|
+
var GANDI_PROTOCOL_TO_HTTPS = {
|
|
3089
|
+
http: "off",
|
|
3090
|
+
https: "on",
|
|
3091
|
+
httpsonly: "only"
|
|
3092
|
+
};
|
|
3071
3093
|
var GandiRegistrar = class extends BaseRegistrar {
|
|
3072
3094
|
name = "gandi";
|
|
3073
3095
|
// a REST API: GET and HEAD never change state
|
|
@@ -3082,7 +3104,7 @@ var GandiRegistrar = class extends BaseRegistrar {
|
|
|
3082
3104
|
static supportsSandbox = true;
|
|
3083
3105
|
// On top of core: transfer-out auth code (`authinfo`), DNSSEC read/disable
|
|
3084
3106
|
// (LiveDNS keys), email forwarding, and web/URL forwarding (webredirs;
|
|
3085
|
-
// per
|
|
3107
|
+
// per host, apex included). Note core `setPrivacy` disabling is a no-op for
|
|
3086
3108
|
// individual (natural-person) registrants — Gandi keeps WHOIS obfuscation on
|
|
3087
3109
|
// for GDPR regardless of the request.
|
|
3088
3110
|
static extendedFeatures = [
|
|
@@ -3452,73 +3474,128 @@ var GandiRegistrar = class extends BaseRegistrar {
|
|
|
3452
3474
|
}
|
|
3453
3475
|
/**
|
|
3454
3476
|
* Read web/URL forwarding via GET /domain/domains/{fqdn}/webredirs. Gandi keys
|
|
3455
|
-
* each redirect by the full FQDN of the source
|
|
3456
|
-
*
|
|
3477
|
+
* each redirect by the full FQDN of the source host; we normalize that back to
|
|
3478
|
+
* a host relative to the apex (e.g. `shop.example.com` -> `shop`, and the bare
|
|
3479
|
+
* domain -> `@`).
|
|
3457
3480
|
*/
|
|
3458
3481
|
async getDomainForwarding(domainName, opts) {
|
|
3459
|
-
const redirs = await this.
|
|
3460
|
-
|
|
3461
|
-
...opts
|
|
3462
|
-
});
|
|
3463
|
-
return (redirs ?? []).map((r) => ({
|
|
3464
|
-
host: gandiFqdnToHost(r.host ?? domainName, domainName),
|
|
3465
|
-
url: r.url ?? "",
|
|
3466
|
-
type: GANDI_TYPE_TO_FORWARD[(r.type ?? "").toLowerCase()] ?? "permanent"
|
|
3467
|
-
}));
|
|
3482
|
+
const redirs = await this.listWebredirs(domainName, opts);
|
|
3483
|
+
return redirs.map((r) => fromGandiWebredir(r, domainName));
|
|
3468
3484
|
}
|
|
3469
3485
|
/**
|
|
3470
|
-
* Replace web/URL forwarding (full replace; empty clears).
|
|
3471
|
-
*
|
|
3472
|
-
*
|
|
3473
|
-
*
|
|
3486
|
+
* Replace web/URL forwarding (full replace; empty clears). Diffs against the
|
|
3487
|
+
* current webredirs by host:
|
|
3488
|
+
* - a host that's new is created (POST),
|
|
3489
|
+
* - a host whose url/type/https changed is updated in place (PATCH); with
|
|
3490
|
+
* `https` unset it keeps the rule's existing `protocol`, so an https
|
|
3491
|
+
* forward stays https,
|
|
3492
|
+
* - a host that's no longer wanted is deleted (DELETE),
|
|
3493
|
+
* - an identical rule (apex included) is left untouched.
|
|
3494
|
+
* Creates and updates run before any delete, so a failure part-way never leaves
|
|
3495
|
+
* a host that was meant to keep forwarding without a rule. A failure after some
|
|
3496
|
+
* writes landed is reported as a partial update listing what was applied.
|
|
3497
|
+
*
|
|
3498
|
+
* The apex (`@`) is the bare domain FQDN. Creating a redirect needs Gandi's own
|
|
3499
|
+
* DNS record at the source host, and the apex nearly always has one already
|
|
3500
|
+
* (POST then fails with an opaque 500), so creates send `override: true` to
|
|
3501
|
+
* let Gandi replace the host's record — forwarding the host is the point.
|
|
3474
3502
|
*
|
|
3475
|
-
*
|
|
3476
|
-
*
|
|
3503
|
+
* `https` maps to Gandi's `protocol` (off=http, on=https, only=httpsonly).
|
|
3504
|
+
* A new rule with `https` unset omits `protocol`, so Gandi applies its
|
|
3505
|
+
* default (http). Asking for https makes Gandi provision a Let's Encrypt
|
|
3506
|
+
* certificate for the source host; production does this, but the sandbox
|
|
3507
|
+
* can't (it 500s). Gandi repoints the host at its https redirector at once,
|
|
3508
|
+
* and that refuses the host until the certificate exists, so switching a
|
|
3509
|
+
* live rule to https can leave it unreachable for a while (see gandi.md).
|
|
3477
3510
|
*/
|
|
3478
3511
|
async setDomainForwarding(domainName, forwards, opts) {
|
|
3512
|
+
const applied = [];
|
|
3479
3513
|
try {
|
|
3480
|
-
const settable = settableForwards(forwards);
|
|
3514
|
+
const settable = settableForwards(forwards, ["off", "on", "only"]);
|
|
3515
|
+
const desired = /* @__PURE__ */ new Map();
|
|
3481
3516
|
for (const f of settable) {
|
|
3482
|
-
|
|
3483
|
-
|
|
3484
|
-
|
|
3485
|
-
);
|
|
3517
|
+
const host = normalizeForwardHost(f.host);
|
|
3518
|
+
if (desired.has(host)) {
|
|
3519
|
+
throw new Error(`${this.name}: duplicate web forwarding host "${host}"`);
|
|
3486
3520
|
}
|
|
3521
|
+
desired.set(host, { ...f, host });
|
|
3487
3522
|
}
|
|
3488
|
-
const current =
|
|
3489
|
-
const
|
|
3490
|
-
|
|
3491
|
-
|
|
3492
|
-
|
|
3493
|
-
|
|
3494
|
-
|
|
3495
|
-
|
|
3523
|
+
const current = /* @__PURE__ */ new Map();
|
|
3524
|
+
for (const r of await this.listWebredirs(domainName, opts)) {
|
|
3525
|
+
current.set(gandiFqdnToHost(r.host ?? domainName, domainName), r);
|
|
3526
|
+
}
|
|
3527
|
+
const writes = [];
|
|
3528
|
+
const webredirPath = (host) => `/domain/domains/${domainName}/webredirs/${gandiHostToFqdn(host, domainName)}`;
|
|
3529
|
+
for (const [host, f] of desired) {
|
|
3530
|
+
const existing = current.get(host);
|
|
3531
|
+
if (!existing) {
|
|
3532
|
+
writes.push({
|
|
3533
|
+
label: `create ${host}`,
|
|
3534
|
+
run: () => this.http.request({
|
|
3535
|
+
method: "POST",
|
|
3536
|
+
path: `/domain/domains/${domainName}/webredirs`,
|
|
3537
|
+
body: {
|
|
3538
|
+
host: gandiHostToFqdn(host, domainName),
|
|
3539
|
+
type: GANDI_FORWARD_TYPE[f.type],
|
|
3540
|
+
url: f.url,
|
|
3541
|
+
...f.https ? { protocol: GANDI_PROTOCOL[f.https] } : {},
|
|
3542
|
+
override: true
|
|
3543
|
+
},
|
|
3544
|
+
...opts
|
|
3545
|
+
})
|
|
3496
3546
|
});
|
|
3547
|
+
continue;
|
|
3497
3548
|
}
|
|
3498
|
-
|
|
3499
|
-
|
|
3500
|
-
if (
|
|
3501
|
-
|
|
3502
|
-
|
|
3503
|
-
|
|
3504
|
-
|
|
3505
|
-
|
|
3506
|
-
|
|
3507
|
-
// express (and which the sandbox can't do — it 500s).
|
|
3549
|
+
const now = fromGandiWebredir(existing, domainName);
|
|
3550
|
+
const httpsSame = f.https === void 0 || f.https === now.https;
|
|
3551
|
+
if (now.url === f.url && now.type === f.type && httpsSame) continue;
|
|
3552
|
+
const protocol = f.https ? GANDI_PROTOCOL[f.https] : existing.protocol;
|
|
3553
|
+
writes.push({
|
|
3554
|
+
label: `update ${host}`,
|
|
3555
|
+
run: () => this.http.request({
|
|
3556
|
+
method: "PATCH",
|
|
3557
|
+
path: webredirPath(host),
|
|
3508
3558
|
body: {
|
|
3509
|
-
host: gandiHostToFqdn(f.host, domainName),
|
|
3510
3559
|
type: GANDI_FORWARD_TYPE[f.type],
|
|
3511
|
-
url: f.url
|
|
3560
|
+
url: f.url,
|
|
3561
|
+
...protocol ? { protocol } : {}
|
|
3512
3562
|
},
|
|
3513
3563
|
...opts
|
|
3514
|
-
})
|
|
3564
|
+
})
|
|
3565
|
+
});
|
|
3566
|
+
}
|
|
3567
|
+
for (const host of current.keys()) {
|
|
3568
|
+
if (desired.has(host)) continue;
|
|
3569
|
+
writes.push({
|
|
3570
|
+
label: `delete ${host}`,
|
|
3571
|
+
run: () => this.http.request({ method: "DELETE", path: webredirPath(host), ...opts })
|
|
3572
|
+
});
|
|
3573
|
+
}
|
|
3574
|
+
for (const w of writes) {
|
|
3575
|
+
try {
|
|
3576
|
+
await w.run();
|
|
3577
|
+
} catch (error) {
|
|
3578
|
+
if (applied.length === 0) throw error;
|
|
3579
|
+
const rest = writes.slice(applied.length + 1).map((x) => x.label);
|
|
3580
|
+
return {
|
|
3581
|
+
success: false,
|
|
3582
|
+
message: `Domain forwarding partially updated: ${w.label} failed (${toRegistrarError(error).message}); applied: ${applied.join(", ")}` + (rest.length ? `; not attempted: ${rest.join(", ")}` : "")
|
|
3583
|
+
};
|
|
3515
3584
|
}
|
|
3585
|
+
applied.push(w.label);
|
|
3516
3586
|
}
|
|
3517
3587
|
return { success: true, message: "Domain forwarding updated successfully" };
|
|
3518
3588
|
} catch (error) {
|
|
3519
3589
|
return { success: false, message: toRegistrarError(error).message };
|
|
3520
3590
|
}
|
|
3521
3591
|
}
|
|
3592
|
+
async listWebredirs(domainName, opts) {
|
|
3593
|
+
const redirs = await this.http.request({
|
|
3594
|
+
path: `/domain/domains/${domainName}/webredirs`,
|
|
3595
|
+
...opts
|
|
3596
|
+
});
|
|
3597
|
+
return redirs ?? [];
|
|
3598
|
+
}
|
|
3522
3599
|
async setAutoRenew(domainName, enabled, opts) {
|
|
3523
3600
|
try {
|
|
3524
3601
|
await this.http.request({
|
|
@@ -3706,6 +3783,18 @@ function gandiFqdnToHost(fqdn, domain) {
|
|
|
3706
3783
|
const suffix = `.${domain}`;
|
|
3707
3784
|
return fqdn.endsWith(suffix) ? fqdn.slice(0, -suffix.length) : fqdn;
|
|
3708
3785
|
}
|
|
3786
|
+
function normalizeForwardHost(host) {
|
|
3787
|
+
return host ? host : "@";
|
|
3788
|
+
}
|
|
3789
|
+
function fromGandiWebredir(r, domain) {
|
|
3790
|
+
const https = GANDI_PROTOCOL_TO_HTTPS[(r.protocol ?? "").toLowerCase()];
|
|
3791
|
+
return {
|
|
3792
|
+
host: gandiFqdnToHost(r.host ?? domain, domain),
|
|
3793
|
+
url: r.url ?? "",
|
|
3794
|
+
type: GANDI_TYPE_TO_FORWARD[(r.type ?? "").toLowerCase()] ?? "permanent",
|
|
3795
|
+
...https ? { https } : {}
|
|
3796
|
+
};
|
|
3797
|
+
}
|
|
3709
3798
|
function fromGandiContact(c) {
|
|
3710
3799
|
if (!c) return void 0;
|
|
3711
3800
|
return {
|
|
@@ -8486,6 +8575,7 @@ exports.settableForwards = settableForwards;
|
|
|
8486
8575
|
exports.sleep = sleep;
|
|
8487
8576
|
exports.toDnsEntry = toDnsEntry;
|
|
8488
8577
|
exports.toRegistrarError = toRegistrarError;
|
|
8578
|
+
exports.unsupportedForwardHttps = unsupportedForwardHttps;
|
|
8489
8579
|
exports.useLabelMapper = useLabelMapper;
|
|
8490
8580
|
exports.wasNotSent = wasNotSent;
|
|
8491
8581
|
//# sourceMappingURL=index.cjs.map
|