@aoxborrow/registrar-client 0.9.1 → 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/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 {
@@ -3066,6 +3074,16 @@ var GANDI_TYPE_TO_FORWARD = {
3066
3074
  cloak: "masked"
3067
3075
  // read-only; setDomainForwarding rejects it
3068
3076
  };
3077
+ var GANDI_PROTOCOL = {
3078
+ off: "http",
3079
+ on: "https",
3080
+ only: "httpsonly"
3081
+ };
3082
+ var GANDI_PROTOCOL_TO_HTTPS = {
3083
+ http: "off",
3084
+ https: "on",
3085
+ httpsonly: "only"
3086
+ };
3069
3087
  var GandiRegistrar = class extends BaseRegistrar {
3070
3088
  name = "gandi";
3071
3089
  // a REST API: GET and HEAD never change state
@@ -3080,7 +3098,7 @@ var GandiRegistrar = class extends BaseRegistrar {
3080
3098
  static supportsSandbox = true;
3081
3099
  // On top of core: transfer-out auth code (`authinfo`), DNSSEC read/disable
3082
3100
  // (LiveDNS keys), email forwarding, and web/URL forwarding (webredirs;
3083
- // per-subdomain, no apex). Note core `setPrivacy` disabling is a no-op for
3101
+ // per host, apex included). Note core `setPrivacy` disabling is a no-op for
3084
3102
  // individual (natural-person) registrants — Gandi keeps WHOIS obfuscation on
3085
3103
  // for GDPR regardless of the request.
3086
3104
  static extendedFeatures = [
@@ -3450,73 +3468,128 @@ var GandiRegistrar = class extends BaseRegistrar {
3450
3468
  }
3451
3469
  /**
3452
3470
  * 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`).
3471
+ * each redirect by the full FQDN of the source host; we normalize that back to
3472
+ * a host relative to the apex (e.g. `shop.example.com` -> `shop`, and the bare
3473
+ * domain -> `@`).
3455
3474
  */
3456
3475
  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
- }));
3476
+ const redirs = await this.listWebredirs(domainName, opts);
3477
+ return redirs.map((r) => fromGandiWebredir(r, domainName));
3466
3478
  }
3467
3479
  /**
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.
3480
+ * Replace web/URL forwarding (full replace; empty clears). Diffs against the
3481
+ * current webredirs by host:
3482
+ * - a host that's new is created (POST),
3483
+ * - a host whose url/type/https changed is updated in place (PATCH); with
3484
+ * `https` unset it keeps the rule's existing `protocol`, so an https
3485
+ * forward stays https,
3486
+ * - a host that's no longer wanted is deleted (DELETE),
3487
+ * - an identical rule (apex included) is left untouched.
3488
+ * Creates and updates run before any delete, so a failure part-way never leaves
3489
+ * a host that was meant to keep forwarding without a rule. A failure after some
3490
+ * writes landed is reported as a partial update listing what was applied.
3491
+ *
3492
+ * The apex (`@`) is the bare domain FQDN. Creating a redirect needs Gandi's own
3493
+ * DNS record at the source host, and the apex nearly always has one already
3494
+ * (POST then fails with an opaque 500), so creates send `override: true` to
3495
+ * let Gandi replace the host's record — forwarding the host is the point.
3472
3496
  *
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.
3497
+ * `https` maps to Gandi's `protocol` (off=http, on=https, only=httpsonly).
3498
+ * A new rule with `https` unset omits `protocol`, so Gandi applies its
3499
+ * default (http). Asking for https makes Gandi provision a Let's Encrypt
3500
+ * certificate for the source host; production does this, but the sandbox
3501
+ * can't (it 500s). Gandi repoints the host at its https redirector at once,
3502
+ * and that refuses the host until the certificate exists, so switching a
3503
+ * live rule to https can leave it unreachable for a while (see gandi.md).
3475
3504
  */
3476
3505
  async setDomainForwarding(domainName, forwards, opts) {
3506
+ const applied = [];
3477
3507
  try {
3478
- const settable = settableForwards(forwards);
3508
+ const settable = settableForwards(forwards, ["off", "on", "only"]);
3509
+ const desired = /* @__PURE__ */ new Map();
3479
3510
  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
- );
3511
+ const host = normalizeForwardHost(f.host);
3512
+ if (desired.has(host)) {
3513
+ throw new Error(`${this.name}: duplicate web forwarding host "${host}"`);
3484
3514
  }
3515
+ desired.set(host, { ...f, host });
3485
3516
  }
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
3517
+ const current = /* @__PURE__ */ new Map();
3518
+ for (const r of await this.listWebredirs(domainName, opts)) {
3519
+ current.set(gandiFqdnToHost(r.host ?? domainName, domainName), r);
3520
+ }
3521
+ const writes = [];
3522
+ const webredirPath = (host) => `/domain/domains/${domainName}/webredirs/${gandiHostToFqdn(host, domainName)}`;
3523
+ for (const [host, f] of desired) {
3524
+ const existing = current.get(host);
3525
+ if (!existing) {
3526
+ writes.push({
3527
+ label: `create ${host}`,
3528
+ run: () => this.http.request({
3529
+ method: "POST",
3530
+ path: `/domain/domains/${domainName}/webredirs`,
3531
+ body: {
3532
+ host: gandiHostToFqdn(host, domainName),
3533
+ type: GANDI_FORWARD_TYPE[f.type],
3534
+ url: f.url,
3535
+ ...f.https ? { protocol: GANDI_PROTOCOL[f.https] } : {},
3536
+ override: true
3537
+ },
3538
+ ...opts
3539
+ })
3494
3540
  });
3541
+ continue;
3495
3542
  }
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).
3543
+ const now = fromGandiWebredir(existing, domainName);
3544
+ const httpsSame = f.https === void 0 || f.https === now.https;
3545
+ if (now.url === f.url && now.type === f.type && httpsSame) continue;
3546
+ const protocol = f.https ? GANDI_PROTOCOL[f.https] : existing.protocol;
3547
+ writes.push({
3548
+ label: `update ${host}`,
3549
+ run: () => this.http.request({
3550
+ method: "PATCH",
3551
+ path: webredirPath(host),
3506
3552
  body: {
3507
- host: gandiHostToFqdn(f.host, domainName),
3508
3553
  type: GANDI_FORWARD_TYPE[f.type],
3509
- url: f.url
3554
+ url: f.url,
3555
+ ...protocol ? { protocol } : {}
3510
3556
  },
3511
3557
  ...opts
3512
- });
3558
+ })
3559
+ });
3560
+ }
3561
+ for (const host of current.keys()) {
3562
+ if (desired.has(host)) continue;
3563
+ writes.push({
3564
+ label: `delete ${host}`,
3565
+ run: () => this.http.request({ method: "DELETE", path: webredirPath(host), ...opts })
3566
+ });
3567
+ }
3568
+ for (const w of writes) {
3569
+ try {
3570
+ await w.run();
3571
+ } catch (error) {
3572
+ if (applied.length === 0) throw error;
3573
+ const rest = writes.slice(applied.length + 1).map((x) => x.label);
3574
+ return {
3575
+ success: false,
3576
+ message: `Domain forwarding partially updated: ${w.label} failed (${toRegistrarError(error).message}); applied: ${applied.join(", ")}` + (rest.length ? `; not attempted: ${rest.join(", ")}` : "")
3577
+ };
3513
3578
  }
3579
+ applied.push(w.label);
3514
3580
  }
3515
3581
  return { success: true, message: "Domain forwarding updated successfully" };
3516
3582
  } catch (error) {
3517
3583
  return { success: false, message: toRegistrarError(error).message };
3518
3584
  }
3519
3585
  }
3586
+ async listWebredirs(domainName, opts) {
3587
+ const redirs = await this.http.request({
3588
+ path: `/domain/domains/${domainName}/webredirs`,
3589
+ ...opts
3590
+ });
3591
+ return redirs ?? [];
3592
+ }
3520
3593
  async setAutoRenew(domainName, enabled, opts) {
3521
3594
  try {
3522
3595
  await this.http.request({
@@ -3704,6 +3777,18 @@ function gandiFqdnToHost(fqdn, domain) {
3704
3777
  const suffix = `.${domain}`;
3705
3778
  return fqdn.endsWith(suffix) ? fqdn.slice(0, -suffix.length) : fqdn;
3706
3779
  }
3780
+ function normalizeForwardHost(host) {
3781
+ return host ? host : "@";
3782
+ }
3783
+ function fromGandiWebredir(r, domain) {
3784
+ const https = GANDI_PROTOCOL_TO_HTTPS[(r.protocol ?? "").toLowerCase()];
3785
+ return {
3786
+ host: gandiFqdnToHost(r.host ?? domain, domain),
3787
+ url: r.url ?? "",
3788
+ type: GANDI_TYPE_TO_FORWARD[(r.type ?? "").toLowerCase()] ?? "permanent",
3789
+ ...https ? { https } : {}
3790
+ };
3791
+ }
3707
3792
  function fromGandiContact(c) {
3708
3793
  if (!c) return void 0;
3709
3794
  return {
@@ -4637,6 +4722,80 @@ function dnsFieldsDiffer(a, b) {
4637
4722
 
4638
4723
  // src/registrars/namebright.ts
4639
4724
  var TOKEN_URL = "https://api.namebright.com/auth/token";
4725
+ var NB_KEYS = new Map(
4726
+ [
4727
+ // domains
4728
+ "DomainName",
4729
+ "Status",
4730
+ "ExpirationDate",
4731
+ "RegistrationDate",
4732
+ "AutoRenew",
4733
+ "Locked",
4734
+ "WhoIsPrivacy",
4735
+ "AuthCode",
4736
+ "Category",
4737
+ "UpgradedDomain",
4738
+ "ResultsTotal",
4739
+ "CurrentPage",
4740
+ "Domains",
4741
+ "NameServers",
4742
+ // contacts
4743
+ "RegistrantContact",
4744
+ "AdministrativeContact",
4745
+ "TechnicalContact",
4746
+ "FirstName",
4747
+ "LastName",
4748
+ "Organization",
4749
+ "Department",
4750
+ "Email",
4751
+ "Address1",
4752
+ "Address2",
4753
+ "City",
4754
+ "Region",
4755
+ "Country",
4756
+ "PostalCode",
4757
+ "PhoneCountry",
4758
+ "Phone",
4759
+ "FaxCountry",
4760
+ "Fax",
4761
+ // host records
4762
+ "ARecords",
4763
+ "AAAARecords",
4764
+ "CNAMERecords",
4765
+ "MXRecords",
4766
+ "TXTRecords",
4767
+ "SRVRecords",
4768
+ "RecordId",
4769
+ "Subdomain",
4770
+ "IPV4Address",
4771
+ "IPV6Address",
4772
+ "RedirectDomain",
4773
+ "MailServer",
4774
+ "Priority",
4775
+ "TextRecord",
4776
+ "Service",
4777
+ "Protocol",
4778
+ "Weight",
4779
+ "Port",
4780
+ "Target",
4781
+ // availability + orders
4782
+ "ProductTypeName",
4783
+ "UnitPrice",
4784
+ "Promotion",
4785
+ "PromotionPrice",
4786
+ "Discount",
4787
+ "Description",
4788
+ "OrderId",
4789
+ "TotalPrice"
4790
+ ].map((k) => [k.toLowerCase(), k])
4791
+ );
4792
+ function canonicalKeys(value) {
4793
+ if (Array.isArray(value)) return value.map(canonicalKeys);
4794
+ if (value === null || typeof value !== "object") return value;
4795
+ return Object.fromEntries(
4796
+ Object.entries(value).map(([k, v]) => [NB_KEYS.get(k.toLowerCase()) ?? k, canonicalKeys(v)])
4797
+ );
4798
+ }
4640
4799
  var NameBrightRegistrar = class extends BaseRegistrar {
4641
4800
  name = "namebright";
4642
4801
  // a REST API: GET and HEAD never change state
@@ -4690,14 +4849,16 @@ var NameBrightRegistrar = class extends BaseRegistrar {
4690
4849
  this.tokenExpiresAt = now + ttlMs - 6e4;
4691
4850
  return this.token;
4692
4851
  }
4693
- // issue an authenticated REST request, attaching a fresh bearer token
4852
+ // issue an authenticated REST request, attaching a fresh bearer token. The
4853
+ // response's keys are normalized to PascalCase (see NB_KEYS).
4694
4854
  async authed(config, opts) {
4695
4855
  const token = await this.getToken(opts);
4696
- return this.http.request({
4856
+ const res = await this.http.request({
4697
4857
  ...config,
4698
4858
  headers: { Authorization: `Bearer ${token}` },
4699
4859
  ...opts
4700
4860
  });
4861
+ return canonicalKeys(res);
4701
4862
  }
4702
4863
  async testConnection(opts) {
4703
4864
  try {
@@ -4718,8 +4879,8 @@ var NameBrightRegistrar = class extends BaseRegistrar {
4718
4879
  { path: "account/domains", query: { page, domainsPerPage: perPage } },
4719
4880
  reqOpts
4720
4881
  );
4721
- const list = Array.isArray(data) ? data : data?.Domains ?? data?.domains ?? [];
4722
- for (const d of list) domains.push(this.toDomain(d));
4882
+ const list = Array.isArray(data) ? data : data?.Domains ?? [];
4883
+ for (const d of list) if (d.DomainName) domains.push(this.toDomain(d));
4723
4884
  hasMore = list.length === perPage;
4724
4885
  page++;
4725
4886
  }
@@ -4756,7 +4917,7 @@ var NameBrightRegistrar = class extends BaseRegistrar {
4756
4917
  opts
4757
4918
  );
4758
4919
  if (Array.isArray(res)) return res.map(String);
4759
- return (res.NameServers ?? res.Nameservers ?? res.nameservers ?? []).map(String);
4920
+ return (res.NameServers ?? []).map(String);
4760
4921
  }
4761
4922
  /**
4762
4923
  * Read the registrant/admin/tech contacts via GET
@@ -4893,10 +5054,10 @@ var NameBrightRegistrar = class extends BaseRegistrar {
4893
5054
  async setDomainFlag(domainName, change, successMessage, opts) {
4894
5055
  try {
4895
5056
  const path = `account/domains/${encodeURIComponent(domainName)}`;
4896
- const current = await this.authed(
4897
- { path },
4898
- opts
4899
- );
5057
+ const current = await this.authed({ path }, opts);
5058
+ if (!current.DomainName) {
5059
+ return { success: false, message: `NameBright: could not read ${domainName} to update it` };
5060
+ }
4900
5061
  const body = {
4901
5062
  DomainName: current.DomainName,
4902
5063
  Status: current.Status,
@@ -4973,7 +5134,7 @@ var NameBrightRegistrar = class extends BaseRegistrar {
4973
5134
  // normalized Domain shape. Neither endpoint returns nameservers.
4974
5135
  toDomain(d) {
4975
5136
  return createDomain({
4976
- domainName: d.DomainName ?? d.domain,
5137
+ domainName: d.DomainName,
4977
5138
  registrar: this.name,
4978
5139
  status: d.Status ?? "ok",
4979
5140
  createdDate: d.RegistrationDate,
@@ -8340,6 +8501,6 @@ function toDnsEntry(record, defaultTtl = 3600) {
8340
8501
  };
8341
8502
  }
8342
8503
 
8343
- 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 };
8504
+ 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 };
8344
8505
  //# sourceMappingURL=index.js.map
8345
8506
  //# sourceMappingURL=index.js.map