@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/dist/index.d.cts CHANGED
@@ -178,10 +178,12 @@ interface EmailForward {
178
178
  forwardTo: string;
179
179
  }
180
180
  type DomainForwardType = 'temporary' | 'permanent' | 'masked';
181
+ type DomainForwardHttps = 'off' | 'on' | 'only';
181
182
  interface DomainForward {
182
183
  host: string;
183
184
  url: string;
184
185
  type: DomainForwardType;
186
+ https?: DomainForwardHttps;
185
187
  }
186
188
  interface DsRecord {
187
189
  keyTag: number;
@@ -575,6 +577,7 @@ declare class GandiRegistrar extends BaseRegistrar {
575
577
  setEmailForwarding(domainName: string, forwards: EmailForward[], opts?: RequestOptions): Promise<OperationResult>;
576
578
  getDomainForwarding(domainName: string, opts?: RequestOptions): Promise<DomainForward[]>;
577
579
  setDomainForwarding(domainName: string, forwards: DomainForward[], opts?: RequestOptions): Promise<OperationResult>;
580
+ private listWebredirs;
578
581
  setAutoRenew(domainName: string, enabled: boolean, opts?: RequestOptions): Promise<OperationResult>;
579
582
  setPrivacy(domainName: string, enabled: boolean, opts?: RequestOptions): Promise<OperationResult>;
580
583
  setDnsRecords(domainName: string, records: DnsRecord[], opts?: RequestOptions): Promise<OperationResult>;
@@ -916,7 +919,8 @@ type SettableDomainForward = DomainForward & {
916
919
  type: 'temporary' | 'permanent';
917
920
  };
918
921
  declare const MASKED_FORWARD_UNSUPPORTED = "masked/framed forwarding is not supported by this library; use \"temporary\" or \"permanent\"";
919
- declare function settableForwards(forwards: DomainForward[]): SettableDomainForward[];
922
+ declare function settableForwards(forwards: DomainForward[], httpsModes?: readonly DomainForwardHttps[]): SettableDomainForward[];
923
+ declare function unsupportedForwardHttps(mode: DomainForwardHttps, httpsModes?: readonly DomainForwardHttps[]): string;
920
924
  declare function requireConsent(provider: string, consent: RegistrationConsent | undefined): RegistrationConsent;
921
925
  declare function filterDomains(domains: Domain[], search?: string): Domain[];
922
926
  declare function normalizeDomain(domain: string): string;
@@ -926,4 +930,5 @@ declare function normalizeNameservers(nameservers: unknown): string[];
926
930
  declare function createDomain(data?: DomainInput): Domain;
927
931
  declare function asRead<T extends RequestOptions>(opts?: T): T;
928
932
 
929
- export { ALL_FEATURES, AbortError, AuthenticationError, AuthorizationError, BaseRegistrar, CORE_FEATURES, CloudflareRegistrar, type ConfigField, ConfigurationError, ConnectionError, type ConnectionResult, ConsentRequiredError, type Contact, type ContactSet, DEFAULT_OPTIONS, type DelegationSigner, type DnsEntry, type DnsRecord, DnsSyntaxError, type DnssecStatus, type Domain, Domain101Registrar, type DomainAvailability, type DomainForward, type DomainForwardType, type DomainInput, type DomainSource, type DsRecord, DynadotRegistrar, EXTENDED_FEATURES, type EmailForward, FEATURE_CALLS, Feature, type FeatureCall, GandiRegistrar, GoDaddyRegistrar, HttpClient, type HttpClientConfig, InvalidResponseError, type LabelMapper, type ListDomainsOptions, MASKED_FORWARD_UNSUPPORTED, NameBrightRegistrar, type NameRole, NameSiloRegistrar, NamecheapRegistrar, NamecomRegistrar, NotFoundError, NotImplementedError, type OperationResult, OutcomeUnknownError, ParsingError, PorkbunRegistrar, type PortfolioError, type PortfolioResult, REST_SAFE_METHODS, RateLimitError, type RecordClash, type RegisterDomainInput, type Registrar, RegistrarClient, type RegistrarClientOptions, type RegistrarConstructor, type RegistrarCredentials, type RegistrarEnvironment, RegistrarError, type RegistrarFeature, type RegistrarName, type RegistrarOptions, type RegistrationConsent, type RequestConfig, type RequestIntent, type RequestOptions, RetryableRegistrarError, type SettableDomainForward, SpaceshipRegistrar, TimeoutError, type TldPricing, type TransferDomainInput, USER_AGENT, asRead, backoffDelay, createDomain, createRegistrar, ensureArray, filterDomains, findCnameConflicts, findRepeatedRecords, isCoreFeature, isIpv4, isIpv6, listPortfolio, markNotSent, normalizeDomain, normalizeNameservers, parseDate, parseXml, prepareEntry, prepareName, prepareRdata, readDelegationSigner, registrars, requireConsent, resetLabelMapper, selectBaseUrl, settableForwards, sleep, toDnsEntry, toRegistrarError, useLabelMapper, wasNotSent };
933
+ export { ALL_FEATURES, AbortError, AuthenticationError, AuthorizationError, BaseRegistrar, CORE_FEATURES, CloudflareRegistrar, ConfigurationError, ConnectionError, ConsentRequiredError, DEFAULT_OPTIONS, DnsSyntaxError, Domain101Registrar, DynadotRegistrar, EXTENDED_FEATURES, FEATURE_CALLS, Feature, GandiRegistrar, GoDaddyRegistrar, HttpClient, InvalidResponseError, MASKED_FORWARD_UNSUPPORTED, NameBrightRegistrar, NameSiloRegistrar, NamecheapRegistrar, NamecomRegistrar, NotFoundError, NotImplementedError, OutcomeUnknownError, ParsingError, PorkbunRegistrar, REST_SAFE_METHODS, RateLimitError, RegistrarClient, RegistrarError, RetryableRegistrarError, SpaceshipRegistrar, TimeoutError, USER_AGENT, asRead, backoffDelay, createDomain, createRegistrar, ensureArray, filterDomains, findCnameConflicts, findRepeatedRecords, isCoreFeature, isIpv4, isIpv6, listPortfolio, markNotSent, normalizeDomain, normalizeNameservers, parseDate, parseXml, prepareEntry, prepareName, prepareRdata, readDelegationSigner, registrars, requireConsent, resetLabelMapper, selectBaseUrl, settableForwards, sleep, toDnsEntry, toRegistrarError, unsupportedForwardHttps, useLabelMapper, wasNotSent };
934
+ export type { ConfigField, ConnectionResult, Contact, ContactSet, DelegationSigner, DnsEntry, DnsRecord, DnssecStatus, Domain, DomainAvailability, DomainForward, DomainForwardHttps, DomainForwardType, DomainInput, DomainSource, DsRecord, EmailForward, FeatureCall, HttpClientConfig, LabelMapper, ListDomainsOptions, NameRole, OperationResult, PortfolioError, PortfolioResult, RecordClash, RegisterDomainInput, Registrar, RegistrarClientOptions, RegistrarConstructor, RegistrarCredentials, RegistrarEnvironment, RegistrarFeature, RegistrarName, RegistrarOptions, RegistrationConsent, RequestConfig, RequestIntent, RequestOptions, SettableDomainForward, TldPricing, TransferDomainInput };
package/dist/index.d.ts CHANGED
@@ -178,10 +178,12 @@ interface EmailForward {
178
178
  forwardTo: string;
179
179
  }
180
180
  type DomainForwardType = 'temporary' | 'permanent' | 'masked';
181
+ type DomainForwardHttps = 'off' | 'on' | 'only';
181
182
  interface DomainForward {
182
183
  host: string;
183
184
  url: string;
184
185
  type: DomainForwardType;
186
+ https?: DomainForwardHttps;
185
187
  }
186
188
  interface DsRecord {
187
189
  keyTag: number;
@@ -575,6 +577,7 @@ declare class GandiRegistrar extends BaseRegistrar {
575
577
  setEmailForwarding(domainName: string, forwards: EmailForward[], opts?: RequestOptions): Promise<OperationResult>;
576
578
  getDomainForwarding(domainName: string, opts?: RequestOptions): Promise<DomainForward[]>;
577
579
  setDomainForwarding(domainName: string, forwards: DomainForward[], opts?: RequestOptions): Promise<OperationResult>;
580
+ private listWebredirs;
578
581
  setAutoRenew(domainName: string, enabled: boolean, opts?: RequestOptions): Promise<OperationResult>;
579
582
  setPrivacy(domainName: string, enabled: boolean, opts?: RequestOptions): Promise<OperationResult>;
580
583
  setDnsRecords(domainName: string, records: DnsRecord[], opts?: RequestOptions): Promise<OperationResult>;
@@ -916,7 +919,8 @@ type SettableDomainForward = DomainForward & {
916
919
  type: 'temporary' | 'permanent';
917
920
  };
918
921
  declare const MASKED_FORWARD_UNSUPPORTED = "masked/framed forwarding is not supported by this library; use \"temporary\" or \"permanent\"";
919
- declare function settableForwards(forwards: DomainForward[]): SettableDomainForward[];
922
+ declare function settableForwards(forwards: DomainForward[], httpsModes?: readonly DomainForwardHttps[]): SettableDomainForward[];
923
+ declare function unsupportedForwardHttps(mode: DomainForwardHttps, httpsModes?: readonly DomainForwardHttps[]): string;
920
924
  declare function requireConsent(provider: string, consent: RegistrationConsent | undefined): RegistrationConsent;
921
925
  declare function filterDomains(domains: Domain[], search?: string): Domain[];
922
926
  declare function normalizeDomain(domain: string): string;
@@ -926,4 +930,5 @@ declare function normalizeNameservers(nameservers: unknown): string[];
926
930
  declare function createDomain(data?: DomainInput): Domain;
927
931
  declare function asRead<T extends RequestOptions>(opts?: T): T;
928
932
 
929
- export { ALL_FEATURES, AbortError, AuthenticationError, AuthorizationError, BaseRegistrar, CORE_FEATURES, CloudflareRegistrar, type ConfigField, ConfigurationError, ConnectionError, type ConnectionResult, ConsentRequiredError, type Contact, type ContactSet, DEFAULT_OPTIONS, type DelegationSigner, type DnsEntry, type DnsRecord, DnsSyntaxError, type DnssecStatus, type Domain, Domain101Registrar, type DomainAvailability, type DomainForward, type DomainForwardType, type DomainInput, type DomainSource, type DsRecord, DynadotRegistrar, EXTENDED_FEATURES, type EmailForward, FEATURE_CALLS, Feature, type FeatureCall, GandiRegistrar, GoDaddyRegistrar, HttpClient, type HttpClientConfig, InvalidResponseError, type LabelMapper, type ListDomainsOptions, MASKED_FORWARD_UNSUPPORTED, NameBrightRegistrar, type NameRole, NameSiloRegistrar, NamecheapRegistrar, NamecomRegistrar, NotFoundError, NotImplementedError, type OperationResult, OutcomeUnknownError, ParsingError, PorkbunRegistrar, type PortfolioError, type PortfolioResult, REST_SAFE_METHODS, RateLimitError, type RecordClash, type RegisterDomainInput, type Registrar, RegistrarClient, type RegistrarClientOptions, type RegistrarConstructor, type RegistrarCredentials, type RegistrarEnvironment, RegistrarError, type RegistrarFeature, type RegistrarName, type RegistrarOptions, type RegistrationConsent, type RequestConfig, type RequestIntent, type RequestOptions, RetryableRegistrarError, type SettableDomainForward, SpaceshipRegistrar, TimeoutError, type TldPricing, type TransferDomainInput, USER_AGENT, asRead, backoffDelay, createDomain, createRegistrar, ensureArray, filterDomains, findCnameConflicts, findRepeatedRecords, isCoreFeature, isIpv4, isIpv6, listPortfolio, markNotSent, normalizeDomain, normalizeNameservers, parseDate, parseXml, prepareEntry, prepareName, prepareRdata, readDelegationSigner, registrars, requireConsent, resetLabelMapper, selectBaseUrl, settableForwards, sleep, toDnsEntry, toRegistrarError, useLabelMapper, wasNotSent };
933
+ export { ALL_FEATURES, AbortError, AuthenticationError, AuthorizationError, BaseRegistrar, CORE_FEATURES, CloudflareRegistrar, ConfigurationError, ConnectionError, ConsentRequiredError, DEFAULT_OPTIONS, DnsSyntaxError, Domain101Registrar, DynadotRegistrar, EXTENDED_FEATURES, FEATURE_CALLS, Feature, GandiRegistrar, GoDaddyRegistrar, HttpClient, InvalidResponseError, MASKED_FORWARD_UNSUPPORTED, NameBrightRegistrar, NameSiloRegistrar, NamecheapRegistrar, NamecomRegistrar, NotFoundError, NotImplementedError, OutcomeUnknownError, ParsingError, PorkbunRegistrar, REST_SAFE_METHODS, RateLimitError, RegistrarClient, RegistrarError, RetryableRegistrarError, SpaceshipRegistrar, TimeoutError, USER_AGENT, asRead, backoffDelay, createDomain, createRegistrar, ensureArray, filterDomains, findCnameConflicts, findRepeatedRecords, isCoreFeature, isIpv4, isIpv6, listPortfolio, markNotSent, normalizeDomain, normalizeNameservers, parseDate, parseXml, prepareEntry, prepareName, prepareRdata, readDelegationSigner, registrars, requireConsent, resetLabelMapper, selectBaseUrl, settableForwards, sleep, toDnsEntry, toRegistrarError, unsupportedForwardHttps, useLabelMapper, wasNotSent };
934
+ export type { ConfigField, ConnectionResult, Contact, ContactSet, DelegationSigner, DnsEntry, DnsRecord, DnssecStatus, Domain, DomainAvailability, DomainForward, DomainForwardHttps, DomainForwardType, DomainInput, DomainSource, DsRecord, EmailForward, FeatureCall, HttpClientConfig, LabelMapper, ListDomainsOptions, NameRole, OperationResult, PortfolioError, PortfolioResult, RecordClash, RegisterDomainInput, Registrar, RegistrarClientOptions, RegistrarConstructor, RegistrarCredentials, RegistrarEnvironment, RegistrarFeature, RegistrarName, RegistrarOptions, RegistrationConsent, RequestConfig, RequestIntent, RequestOptions, SettableDomainForward, TldPricing, TransferDomainInput };
package/dist/index.js CHANGED
@@ -202,12 +202,18 @@ function sleep(ms) {
202
202
  return new Promise((resolve) => setTimeout(resolve, ms));
203
203
  }
204
204
  var MASKED_FORWARD_UNSUPPORTED = 'masked/framed forwarding is not supported by this library; use "temporary" or "permanent"';
205
- function settableForwards(forwards) {
205
+ function settableForwards(forwards, httpsModes = []) {
206
206
  for (const f of forwards) {
207
207
  if (f.type === "masked") throw new Error(MASKED_FORWARD_UNSUPPORTED);
208
+ if (f.https !== void 0 && !httpsModes.includes(f.https)) {
209
+ throw new Error(unsupportedForwardHttps(f.https, httpsModes));
210
+ }
208
211
  }
209
212
  return forwards;
210
213
  }
214
+ function unsupportedForwardHttps(mode, httpsModes = []) {
215
+ 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");
216
+ }
211
217
  function requireConsent(provider, consent) {
212
218
  if (!consent) {
213
219
  throw new ConsentRequiredError(
@@ -1268,7 +1274,7 @@ var CloudflareRegistrar = class _CloudflareRegistrar extends BaseRegistrar {
1268
1274
  */
1269
1275
  async setDomainForwarding(domainName, forwards, opts) {
1270
1276
  try {
1271
- const settable = settableForwards(forwards);
1277
+ const settable = settableForwards(forwards, ["on"]);
1272
1278
  const zone = await this.requireZone(domainName, opts);
1273
1279
  const existing = await this.getRedirectRuleset(zone.id, opts);
1274
1280
  if (settable.length === 0) {
@@ -2126,6 +2132,8 @@ var Domain101Registrar = class extends BaseRegistrar {
2126
2132
  throw new ConfigurationError(
2127
2133
  "101domain: only one permanent apex forwarding rule is supported"
2128
2134
  );
2135
+ if (forwards[0]?.https !== void 0)
2136
+ throw new ConfigurationError(`101domain: ${unsupportedForwardHttps(forwards[0].https)}`);
2129
2137
  if (forwards.length) {
2130
2138
  let url;
2131
2139
  try {
@@ -2644,13 +2652,17 @@ ${bodyStr}`;
2644
2652
  );
2645
2653
  }
2646
2654
  // Auto-renew via the renew_option endpoint. The accepted write values are
2647
- // "auto" (auto-renew), "donot" (do not renew), and "reset" (manual renewal) —
2648
- // note these differ from the read values ("auto-renew" etc.).
2655
+ // "auto" (auto-renew), "donot" (do not renew), and "reset" (manual renewal);
2656
+ // they read back as "auto-renew", "do not renew", and "manual renewal". Off
2657
+ // writes "reset": off means "won't renew automatically", and "donot" would
2658
+ // also stop Dynadot's expiration reminder emails, so this never writes it.
2659
+ // "reset" is always manual renewal, even when the account's default renew
2660
+ // option is auto (sandbox-verified 2026-10-28).
2649
2661
  async setAutoRenew(domainName, enabled, opts) {
2650
2662
  return this.mutate(
2651
2663
  "PUT",
2652
2664
  `/restful/v2/domains/${encodeURIComponent(domainName)}/renew_option`,
2653
- { renew_option: enabled ? "auto" : "donot" },
2665
+ { renew_option: enabled ? "auto" : "reset" },
2654
2666
  `Auto-renew ${enabled ? "enabled" : "disabled"} successfully`,
2655
2667
  opts
2656
2668
  );
@@ -3066,6 +3078,16 @@ var GANDI_TYPE_TO_FORWARD = {
3066
3078
  cloak: "masked"
3067
3079
  // read-only; setDomainForwarding rejects it
3068
3080
  };
3081
+ var GANDI_PROTOCOL = {
3082
+ off: "http",
3083
+ on: "https",
3084
+ only: "httpsonly"
3085
+ };
3086
+ var GANDI_PROTOCOL_TO_HTTPS = {
3087
+ http: "off",
3088
+ https: "on",
3089
+ httpsonly: "only"
3090
+ };
3069
3091
  var GandiRegistrar = class extends BaseRegistrar {
3070
3092
  name = "gandi";
3071
3093
  // a REST API: GET and HEAD never change state
@@ -3080,7 +3102,7 @@ var GandiRegistrar = class extends BaseRegistrar {
3080
3102
  static supportsSandbox = true;
3081
3103
  // On top of core: transfer-out auth code (`authinfo`), DNSSEC read/disable
3082
3104
  // (LiveDNS keys), email forwarding, and web/URL forwarding (webredirs;
3083
- // per-subdomain, no apex). Note core `setPrivacy` disabling is a no-op for
3105
+ // per host, apex included). Note core `setPrivacy` disabling is a no-op for
3084
3106
  // individual (natural-person) registrants — Gandi keeps WHOIS obfuscation on
3085
3107
  // for GDPR regardless of the request.
3086
3108
  static extendedFeatures = [
@@ -3450,73 +3472,128 @@ var GandiRegistrar = class extends BaseRegistrar {
3450
3472
  }
3451
3473
  /**
3452
3474
  * Read web/URL forwarding via GET /domain/domains/{fqdn}/webredirs. Gandi keys
3453
- * each redirect by the full FQDN of the source subdomain; we normalize that back
3454
- * to a host relative to the apex (e.g. `shop.example.com` -> `shop`).
3475
+ * each redirect by the full FQDN of the source host; we normalize that back to
3476
+ * a host relative to the apex (e.g. `shop.example.com` -> `shop`, and the bare
3477
+ * domain -> `@`).
3455
3478
  */
3456
3479
  async getDomainForwarding(domainName, opts) {
3457
- const redirs = await this.http.request({
3458
- path: `/domain/domains/${domainName}/webredirs`,
3459
- ...opts
3460
- });
3461
- return (redirs ?? []).map((r) => ({
3462
- host: gandiFqdnToHost(r.host ?? domainName, domainName),
3463
- url: r.url ?? "",
3464
- type: GANDI_TYPE_TO_FORWARD[(r.type ?? "").toLowerCase()] ?? "permanent"
3465
- }));
3480
+ const redirs = await this.listWebredirs(domainName, opts);
3481
+ return redirs.map((r) => fromGandiWebredir(r, domainName));
3466
3482
  }
3467
3483
  /**
3468
- * Replace web/URL forwarding (full replace; empty clears). Gandi's webredirs
3469
- * endpoint has no update — only create (POST) and delete (DELETE by FQDN) — so
3470
- * this diffs against the current set: delete entries that are gone or changed,
3471
- * then create the desired ones that aren't already present identically.
3484
+ * Replace web/URL forwarding (full replace; empty clears). Diffs against the
3485
+ * current webredirs by host:
3486
+ * - a host that's new is created (POST),
3487
+ * - a host whose url/type/https changed is updated in place (PATCH); with
3488
+ * `https` unset it keeps the rule's existing `protocol`, so an https
3489
+ * forward stays https,
3490
+ * - a host that's no longer wanted is deleted (DELETE),
3491
+ * - an identical rule (apex included) is left untouched.
3492
+ * Creates and updates run before any delete, so a failure part-way never leaves
3493
+ * a host that was meant to keep forwarding without a rule. A failure after some
3494
+ * writes landed is reported as a partial update listing what was applied.
3495
+ *
3496
+ * The apex (`@`) is the bare domain FQDN. Creating a redirect needs Gandi's own
3497
+ * DNS record at the source host, and the apex nearly always has one already
3498
+ * (POST then fails with an opaque 500), so creates send `override: true` to
3499
+ * let Gandi replace the host's record — forwarding the host is the point.
3472
3500
  *
3473
- * Gandi only supports forwarding a subdomain, not the apex; an apex host ("@")
3474
- * throws rather than silently failing with a 500 from the API.
3501
+ * `https` maps to Gandi's `protocol` (off=http, on=https, only=httpsonly).
3502
+ * A new rule with `https` unset omits `protocol`, so Gandi applies its
3503
+ * default (http). Asking for https makes Gandi provision a Let's Encrypt
3504
+ * certificate for the source host; production does this, but the sandbox
3505
+ * can't (it 500s). Gandi repoints the host at its https redirector at once,
3506
+ * and that refuses the host until the certificate exists, so switching a
3507
+ * live rule to https can leave it unreachable for a while (see gandi.md).
3475
3508
  */
3476
3509
  async setDomainForwarding(domainName, forwards, opts) {
3510
+ const applied = [];
3477
3511
  try {
3478
- const settable = settableForwards(forwards);
3512
+ const settable = settableForwards(forwards, ["off", "on", "only"]);
3513
+ const desired = /* @__PURE__ */ new Map();
3479
3514
  for (const f of settable) {
3480
- if (!f.host || f.host === "@") {
3481
- throw new Error(
3482
- `${this.name}: web forwarding requires a subdomain host; Gandi cannot forward the apex ("@")`
3483
- );
3515
+ const host = normalizeForwardHost(f.host);
3516
+ if (desired.has(host)) {
3517
+ throw new Error(`${this.name}: duplicate web forwarding host "${host}"`);
3484
3518
  }
3519
+ desired.set(host, { ...f, host });
3485
3520
  }
3486
- const current = await this.getDomainForwarding(domainName, opts);
3487
- const same = (a, b) => a.host === b.host && a.url === b.url && a.type === b.type;
3488
- for (const existing of current) {
3489
- if (!settable.some((f) => same(f, existing))) {
3490
- await this.http.request({
3491
- method: "DELETE",
3492
- path: `/domain/domains/${domainName}/webredirs/${gandiHostToFqdn(existing.host, domainName)}`,
3493
- ...opts
3521
+ const current = /* @__PURE__ */ new Map();
3522
+ for (const r of await this.listWebredirs(domainName, opts)) {
3523
+ current.set(gandiFqdnToHost(r.host ?? domainName, domainName), r);
3524
+ }
3525
+ const writes = [];
3526
+ const webredirPath = (host) => `/domain/domains/${domainName}/webredirs/${gandiHostToFqdn(host, domainName)}`;
3527
+ for (const [host, f] of desired) {
3528
+ const existing = current.get(host);
3529
+ if (!existing) {
3530
+ writes.push({
3531
+ label: `create ${host}`,
3532
+ run: () => this.http.request({
3533
+ method: "POST",
3534
+ path: `/domain/domains/${domainName}/webredirs`,
3535
+ body: {
3536
+ host: gandiHostToFqdn(host, domainName),
3537
+ type: GANDI_FORWARD_TYPE[f.type],
3538
+ url: f.url,
3539
+ ...f.https ? { protocol: GANDI_PROTOCOL[f.https] } : {},
3540
+ override: true
3541
+ },
3542
+ ...opts
3543
+ })
3494
3544
  });
3545
+ continue;
3495
3546
  }
3496
- }
3497
- for (const f of settable) {
3498
- if (!current.some((c) => same(c, f))) {
3499
- await this.http.request({
3500
- method: "POST",
3501
- path: `/domain/domains/${domainName}/webredirs`,
3502
- // `protocol` is intentionally omitted so Gandi applies its default
3503
- // (http). Requesting `https` makes Gandi provision a Let's Encrypt
3504
- // certificate for the source host, which our normalized model doesn't
3505
- // express (and which the sandbox can't do — it 500s).
3547
+ const now = fromGandiWebredir(existing, domainName);
3548
+ const httpsSame = f.https === void 0 || f.https === now.https;
3549
+ if (now.url === f.url && now.type === f.type && httpsSame) continue;
3550
+ const protocol = f.https ? GANDI_PROTOCOL[f.https] : existing.protocol;
3551
+ writes.push({
3552
+ label: `update ${host}`,
3553
+ run: () => this.http.request({
3554
+ method: "PATCH",
3555
+ path: webredirPath(host),
3506
3556
  body: {
3507
- host: gandiHostToFqdn(f.host, domainName),
3508
3557
  type: GANDI_FORWARD_TYPE[f.type],
3509
- url: f.url
3558
+ url: f.url,
3559
+ ...protocol ? { protocol } : {}
3510
3560
  },
3511
3561
  ...opts
3512
- });
3562
+ })
3563
+ });
3564
+ }
3565
+ for (const host of current.keys()) {
3566
+ if (desired.has(host)) continue;
3567
+ writes.push({
3568
+ label: `delete ${host}`,
3569
+ run: () => this.http.request({ method: "DELETE", path: webredirPath(host), ...opts })
3570
+ });
3571
+ }
3572
+ for (const w of writes) {
3573
+ try {
3574
+ await w.run();
3575
+ } catch (error) {
3576
+ if (applied.length === 0) throw error;
3577
+ const rest = writes.slice(applied.length + 1).map((x) => x.label);
3578
+ return {
3579
+ success: false,
3580
+ message: `Domain forwarding partially updated: ${w.label} failed (${toRegistrarError(error).message}); applied: ${applied.join(", ")}` + (rest.length ? `; not attempted: ${rest.join(", ")}` : "")
3581
+ };
3513
3582
  }
3583
+ applied.push(w.label);
3514
3584
  }
3515
3585
  return { success: true, message: "Domain forwarding updated successfully" };
3516
3586
  } catch (error) {
3517
3587
  return { success: false, message: toRegistrarError(error).message };
3518
3588
  }
3519
3589
  }
3590
+ async listWebredirs(domainName, opts) {
3591
+ const redirs = await this.http.request({
3592
+ path: `/domain/domains/${domainName}/webredirs`,
3593
+ ...opts
3594
+ });
3595
+ return redirs ?? [];
3596
+ }
3520
3597
  async setAutoRenew(domainName, enabled, opts) {
3521
3598
  try {
3522
3599
  await this.http.request({
@@ -3704,6 +3781,18 @@ function gandiFqdnToHost(fqdn, domain) {
3704
3781
  const suffix = `.${domain}`;
3705
3782
  return fqdn.endsWith(suffix) ? fqdn.slice(0, -suffix.length) : fqdn;
3706
3783
  }
3784
+ function normalizeForwardHost(host) {
3785
+ return host ? host : "@";
3786
+ }
3787
+ function fromGandiWebredir(r, domain) {
3788
+ const https = GANDI_PROTOCOL_TO_HTTPS[(r.protocol ?? "").toLowerCase()];
3789
+ return {
3790
+ host: gandiFqdnToHost(r.host ?? domain, domain),
3791
+ url: r.url ?? "",
3792
+ type: GANDI_TYPE_TO_FORWARD[(r.type ?? "").toLowerCase()] ?? "permanent",
3793
+ ...https ? { https } : {}
3794
+ };
3795
+ }
3707
3796
  function fromGandiContact(c) {
3708
3797
  if (!c) return void 0;
3709
3798
  return {
@@ -8416,6 +8505,6 @@ function toDnsEntry(record, defaultTtl = 3600) {
8416
8505
  };
8417
8506
  }
8418
8507
 
8419
- export { ALL_FEATURES, AbortError, AuthenticationError, AuthorizationError, BaseRegistrar, CORE_FEATURES, CloudflareRegistrar, ConfigurationError, ConnectionError, ConsentRequiredError, DEFAULT_OPTIONS, DnsSyntaxError, Domain101Registrar, DynadotRegistrar, EXTENDED_FEATURES, FEATURE_CALLS, Feature, GandiRegistrar, GoDaddyRegistrar, HttpClient, InvalidResponseError, MASKED_FORWARD_UNSUPPORTED, NameBrightRegistrar, NameSiloRegistrar, NamecheapRegistrar, NamecomRegistrar, NotFoundError, NotImplementedError, OutcomeUnknownError, ParsingError, PorkbunRegistrar, REST_SAFE_METHODS, RateLimitError, RegistrarClient, RegistrarError, RetryableRegistrarError, SpaceshipRegistrar, TimeoutError, USER_AGENT, asRead, backoffDelay, createDomain, createRegistrar, ensureArray, filterDomains, findCnameConflicts, findRepeatedRecords, isCoreFeature, isIpv4, isIpv6, listPortfolio, markNotSent, normalizeDomain, normalizeNameservers, parseDate, parseXml, prepareEntry, prepareName, prepareRdata, readDelegationSigner, registrars, requireConsent, resetLabelMapper, selectBaseUrl, settableForwards, sleep, toDnsEntry, toRegistrarError, useLabelMapper, wasNotSent };
8508
+ export { ALL_FEATURES, AbortError, AuthenticationError, AuthorizationError, BaseRegistrar, CORE_FEATURES, CloudflareRegistrar, ConfigurationError, ConnectionError, ConsentRequiredError, DEFAULT_OPTIONS, DnsSyntaxError, Domain101Registrar, DynadotRegistrar, EXTENDED_FEATURES, FEATURE_CALLS, Feature, GandiRegistrar, GoDaddyRegistrar, HttpClient, InvalidResponseError, MASKED_FORWARD_UNSUPPORTED, NameBrightRegistrar, NameSiloRegistrar, NamecheapRegistrar, NamecomRegistrar, NotFoundError, NotImplementedError, OutcomeUnknownError, ParsingError, PorkbunRegistrar, REST_SAFE_METHODS, RateLimitError, RegistrarClient, RegistrarError, RetryableRegistrarError, SpaceshipRegistrar, TimeoutError, USER_AGENT, asRead, backoffDelay, createDomain, createRegistrar, ensureArray, filterDomains, findCnameConflicts, findRepeatedRecords, isCoreFeature, isIpv4, isIpv6, listPortfolio, markNotSent, normalizeDomain, normalizeNameservers, parseDate, parseXml, prepareEntry, prepareName, prepareRdata, readDelegationSigner, registrars, requireConsent, resetLabelMapper, selectBaseUrl, settableForwards, sleep, toDnsEntry, toRegistrarError, unsupportedForwardHttps, useLabelMapper, wasNotSent };
8420
8509
  //# sourceMappingURL=index.js.map
8421
8510
  //# sourceMappingURL=index.js.map