@aoxborrow/registrar-client 0.9.2 → 0.10.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/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 subdomain-only — the apex (`@`) is rejected.
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). HTTPS targets work
278
- wherever the provider issues a certificate for the domain (all supported
279
- providers do).
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 {
@@ -3068,6 +3076,16 @@ var GANDI_TYPE_TO_FORWARD = {
3068
3076
  cloak: "masked"
3069
3077
  // read-only; setDomainForwarding rejects it
3070
3078
  };
3079
+ var GANDI_PROTOCOL = {
3080
+ off: "http",
3081
+ on: "https",
3082
+ only: "httpsonly"
3083
+ };
3084
+ var GANDI_PROTOCOL_TO_HTTPS = {
3085
+ http: "off",
3086
+ https: "on",
3087
+ httpsonly: "only"
3088
+ };
3071
3089
  var GandiRegistrar = class extends BaseRegistrar {
3072
3090
  name = "gandi";
3073
3091
  // a REST API: GET and HEAD never change state
@@ -3082,7 +3100,7 @@ var GandiRegistrar = class extends BaseRegistrar {
3082
3100
  static supportsSandbox = true;
3083
3101
  // On top of core: transfer-out auth code (`authinfo`), DNSSEC read/disable
3084
3102
  // (LiveDNS keys), email forwarding, and web/URL forwarding (webredirs;
3085
- // per-subdomain, no apex). Note core `setPrivacy` disabling is a no-op for
3103
+ // per host, apex included). Note core `setPrivacy` disabling is a no-op for
3086
3104
  // individual (natural-person) registrants — Gandi keeps WHOIS obfuscation on
3087
3105
  // for GDPR regardless of the request.
3088
3106
  static extendedFeatures = [
@@ -3452,73 +3470,128 @@ var GandiRegistrar = class extends BaseRegistrar {
3452
3470
  }
3453
3471
  /**
3454
3472
  * Read web/URL forwarding via GET /domain/domains/{fqdn}/webredirs. Gandi keys
3455
- * each redirect by the full FQDN of the source subdomain; we normalize that back
3456
- * to a host relative to the apex (e.g. `shop.example.com` -> `shop`).
3473
+ * each redirect by the full FQDN of the source host; we normalize that back to
3474
+ * a host relative to the apex (e.g. `shop.example.com` -> `shop`, and the bare
3475
+ * domain -> `@`).
3457
3476
  */
3458
3477
  async getDomainForwarding(domainName, opts) {
3459
- const redirs = await this.http.request({
3460
- path: `/domain/domains/${domainName}/webredirs`,
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
- }));
3478
+ const redirs = await this.listWebredirs(domainName, opts);
3479
+ return redirs.map((r) => fromGandiWebredir(r, domainName));
3468
3480
  }
3469
3481
  /**
3470
- * Replace web/URL forwarding (full replace; empty clears). Gandi's webredirs
3471
- * endpoint has no update — only create (POST) and delete (DELETE by FQDN) — so
3472
- * this diffs against the current set: delete entries that are gone or changed,
3473
- * then create the desired ones that aren't already present identically.
3482
+ * Replace web/URL forwarding (full replace; empty clears). Diffs against the
3483
+ * current webredirs by host:
3484
+ * - a host that's new is created (POST),
3485
+ * - a host whose url/type/https changed is updated in place (PATCH); with
3486
+ * `https` unset it keeps the rule's existing `protocol`, so an https
3487
+ * forward stays https,
3488
+ * - a host that's no longer wanted is deleted (DELETE),
3489
+ * - an identical rule (apex included) is left untouched.
3490
+ * Creates and updates run before any delete, so a failure part-way never leaves
3491
+ * a host that was meant to keep forwarding without a rule. A failure after some
3492
+ * writes landed is reported as a partial update listing what was applied.
3493
+ *
3494
+ * The apex (`@`) is the bare domain FQDN. Creating a redirect needs Gandi's own
3495
+ * DNS record at the source host, and the apex nearly always has one already
3496
+ * (POST then fails with an opaque 500), so creates send `override: true` to
3497
+ * let Gandi replace the host's record — forwarding the host is the point.
3474
3498
  *
3475
- * Gandi only supports forwarding a subdomain, not the apex; an apex host ("@")
3476
- * throws rather than silently failing with a 500 from the API.
3499
+ * `https` maps to Gandi's `protocol` (off=http, on=https, only=httpsonly).
3500
+ * A new rule with `https` unset omits `protocol`, so Gandi applies its
3501
+ * default (http). Asking for https makes Gandi provision a Let's Encrypt
3502
+ * certificate for the source host; production does this, but the sandbox
3503
+ * can't (it 500s). Gandi repoints the host at its https redirector at once,
3504
+ * and that refuses the host until the certificate exists, so switching a
3505
+ * live rule to https can leave it unreachable for a while (see gandi.md).
3477
3506
  */
3478
3507
  async setDomainForwarding(domainName, forwards, opts) {
3508
+ const applied = [];
3479
3509
  try {
3480
- const settable = settableForwards(forwards);
3510
+ const settable = settableForwards(forwards, ["off", "on", "only"]);
3511
+ const desired = /* @__PURE__ */ new Map();
3481
3512
  for (const f of settable) {
3482
- if (!f.host || f.host === "@") {
3483
- throw new Error(
3484
- `${this.name}: web forwarding requires a subdomain host; Gandi cannot forward the apex ("@")`
3485
- );
3513
+ const host = normalizeForwardHost(f.host);
3514
+ if (desired.has(host)) {
3515
+ throw new Error(`${this.name}: duplicate web forwarding host "${host}"`);
3486
3516
  }
3517
+ desired.set(host, { ...f, host });
3487
3518
  }
3488
- const current = await this.getDomainForwarding(domainName, opts);
3489
- const same = (a, b) => a.host === b.host && a.url === b.url && a.type === b.type;
3490
- for (const existing of current) {
3491
- if (!settable.some((f) => same(f, existing))) {
3492
- await this.http.request({
3493
- method: "DELETE",
3494
- path: `/domain/domains/${domainName}/webredirs/${gandiHostToFqdn(existing.host, domainName)}`,
3495
- ...opts
3519
+ const current = /* @__PURE__ */ new Map();
3520
+ for (const r of await this.listWebredirs(domainName, opts)) {
3521
+ current.set(gandiFqdnToHost(r.host ?? domainName, domainName), r);
3522
+ }
3523
+ const writes = [];
3524
+ const webredirPath = (host) => `/domain/domains/${domainName}/webredirs/${gandiHostToFqdn(host, domainName)}`;
3525
+ for (const [host, f] of desired) {
3526
+ const existing = current.get(host);
3527
+ if (!existing) {
3528
+ writes.push({
3529
+ label: `create ${host}`,
3530
+ run: () => this.http.request({
3531
+ method: "POST",
3532
+ path: `/domain/domains/${domainName}/webredirs`,
3533
+ body: {
3534
+ host: gandiHostToFqdn(host, domainName),
3535
+ type: GANDI_FORWARD_TYPE[f.type],
3536
+ url: f.url,
3537
+ ...f.https ? { protocol: GANDI_PROTOCOL[f.https] } : {},
3538
+ override: true
3539
+ },
3540
+ ...opts
3541
+ })
3496
3542
  });
3543
+ continue;
3497
3544
  }
3498
- }
3499
- for (const f of settable) {
3500
- if (!current.some((c) => same(c, f))) {
3501
- await this.http.request({
3502
- method: "POST",
3503
- path: `/domain/domains/${domainName}/webredirs`,
3504
- // `protocol` is intentionally omitted so Gandi applies its default
3505
- // (http). Requesting `https` makes Gandi provision a Let's Encrypt
3506
- // certificate for the source host, which our normalized model doesn't
3507
- // express (and which the sandbox can't do — it 500s).
3545
+ const now = fromGandiWebredir(existing, domainName);
3546
+ const httpsSame = f.https === void 0 || f.https === now.https;
3547
+ if (now.url === f.url && now.type === f.type && httpsSame) continue;
3548
+ const protocol = f.https ? GANDI_PROTOCOL[f.https] : existing.protocol;
3549
+ writes.push({
3550
+ label: `update ${host}`,
3551
+ run: () => this.http.request({
3552
+ method: "PATCH",
3553
+ path: webredirPath(host),
3508
3554
  body: {
3509
- host: gandiHostToFqdn(f.host, domainName),
3510
3555
  type: GANDI_FORWARD_TYPE[f.type],
3511
- url: f.url
3556
+ url: f.url,
3557
+ ...protocol ? { protocol } : {}
3512
3558
  },
3513
3559
  ...opts
3514
- });
3560
+ })
3561
+ });
3562
+ }
3563
+ for (const host of current.keys()) {
3564
+ if (desired.has(host)) continue;
3565
+ writes.push({
3566
+ label: `delete ${host}`,
3567
+ run: () => this.http.request({ method: "DELETE", path: webredirPath(host), ...opts })
3568
+ });
3569
+ }
3570
+ for (const w of writes) {
3571
+ try {
3572
+ await w.run();
3573
+ } catch (error) {
3574
+ if (applied.length === 0) throw error;
3575
+ const rest = writes.slice(applied.length + 1).map((x) => x.label);
3576
+ return {
3577
+ success: false,
3578
+ message: `Domain forwarding partially updated: ${w.label} failed (${toRegistrarError(error).message}); applied: ${applied.join(", ")}` + (rest.length ? `; not attempted: ${rest.join(", ")}` : "")
3579
+ };
3515
3580
  }
3581
+ applied.push(w.label);
3516
3582
  }
3517
3583
  return { success: true, message: "Domain forwarding updated successfully" };
3518
3584
  } catch (error) {
3519
3585
  return { success: false, message: toRegistrarError(error).message };
3520
3586
  }
3521
3587
  }
3588
+ async listWebredirs(domainName, opts) {
3589
+ const redirs = await this.http.request({
3590
+ path: `/domain/domains/${domainName}/webredirs`,
3591
+ ...opts
3592
+ });
3593
+ return redirs ?? [];
3594
+ }
3522
3595
  async setAutoRenew(domainName, enabled, opts) {
3523
3596
  try {
3524
3597
  await this.http.request({
@@ -3706,6 +3779,18 @@ function gandiFqdnToHost(fqdn, domain) {
3706
3779
  const suffix = `.${domain}`;
3707
3780
  return fqdn.endsWith(suffix) ? fqdn.slice(0, -suffix.length) : fqdn;
3708
3781
  }
3782
+ function normalizeForwardHost(host) {
3783
+ return host ? host : "@";
3784
+ }
3785
+ function fromGandiWebredir(r, domain) {
3786
+ const https = GANDI_PROTOCOL_TO_HTTPS[(r.protocol ?? "").toLowerCase()];
3787
+ return {
3788
+ host: gandiFqdnToHost(r.host ?? domain, domain),
3789
+ url: r.url ?? "",
3790
+ type: GANDI_TYPE_TO_FORWARD[(r.type ?? "").toLowerCase()] ?? "permanent",
3791
+ ...https ? { https } : {}
3792
+ };
3793
+ }
3709
3794
  function fromGandiContact(c) {
3710
3795
  if (!c) return void 0;
3711
3796
  return {
@@ -8486,6 +8571,7 @@ exports.settableForwards = settableForwards;
8486
8571
  exports.sleep = sleep;
8487
8572
  exports.toDnsEntry = toDnsEntry;
8488
8573
  exports.toRegistrarError = toRegistrarError;
8574
+ exports.unsupportedForwardHttps = unsupportedForwardHttps;
8489
8575
  exports.useLabelMapper = useLabelMapper;
8490
8576
  exports.wasNotSent = wasNotSent;
8491
8577
  //# sourceMappingURL=index.cjs.map