@aoxborrow/registrar-client 0.11.0 → 0.12.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
@@ -187,9 +187,9 @@ are the guaranteed contract; **extended** methods are opt-in per provider.
187
187
  | Extended method | CF | DY | GA | GD | NB | NC | NS | PB | SP | NM | 101 |
188
188
  | --------------------- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
189
189
  | `getAuthCode` | ✗ | ✓ | ✓ | ✓ | ✓ | ✗ⁱ | ✗ⁱ | ✗ | ✓ | ✓ | ✗ |
190
- | `getTransferOut` | ✗ | ✓ⁿ | ✗ | ✗ⁿ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ |
191
- | `approveTransferOut` | ✗ | ✓ⁿ | ✗ⁿ | ✗ⁿ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ |
192
- | `rejectTransferOut` | ✗ | ✓ⁿ | ✗ⁿ | ✗ⁿ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ⁿ | ✗ |
190
+ | `getTransferOut` | ✗ | ✓ⁿ | ✓ⁿ | ✓ⁿ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ |
191
+ | `approveTransferOut` | ✗ | ✓ⁿ | ✓ⁿ | ✓ⁿ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ |
192
+ | `rejectTransferOut` | ✗ | ✓ⁿ | ✓ⁿ | ✓ⁿ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ⁿ | ✗ |
193
193
  | `getDnssec` | ✗ | ✓ | ✓ | ✗ | ✗ | ✗ | ✓ | ✓ | ✗ | ✗ᵐ | ✗ |
194
194
  | `disableDnssec` | ✗ | ✓ | ✓ | ✗ | ✗ | ✗ | ✓ | ✓ | ✗ | ✗ᵐ | ✗ |
195
195
  | `getEmailForwarding` | ✓ | ✓ | ✓ | ✗ | ✗ | ✓ | ✓ | ✗ | ✗ | ✗ᵐ | ✗ |
@@ -238,10 +238,13 @@ not a claim of available API endpoints. See [101domain](docs/registrars/101domai
238
238
  - **ⁿ** Transfer-out approval answers a transfer another registrar requested with
239
239
  the auth code. Dynadot's is sandbox-verified only up to "no such order" (its
240
240
  sandbox can't start an outbound transfer); a real pending one's read was
241
- checked in production. GoDaddy (v2, needs a `customerId`)
242
- and Gandi have approve and reject endpoints, and Name.com a reject, none wired
243
- up yet; the rest leave it to the dashboard or the transfer email, and release
244
- the domain after about 5 days. See each registrar's doc.
241
+ checked in production. GoDaddy's uses the v2 customer-scoped API, so it needs a
242
+ `customerId` as forwarding does (ˡ); it reads the domain's recent actions, with
243
+ no expiry date. Gandi has no transfer-out to read: a `pendingTransfer` status
244
+ reads as pending, with no dates, and approving sends the current auth code
245
+ back. Neither has been tried on a real transfer yet. Name.com can only reject
246
+ (not wired up); the rest leave it to the dashboard or the transfer email, and
247
+ release the domain after about 5 days. See each registrar's doc.
245
248
  - **ˡ** GoDaddy domain forwarding uses the v2 customer-scoped API, so it requires
246
249
  a `customerId` (the customer UUID, or a numeric shopper ID the client resolves to
247
250
  the UUID via an sso-key-only lookup); the old v1 forwarding route was removed.
package/dist/index.cjs CHANGED
@@ -3212,9 +3212,9 @@ var GandiRegistrar = class extends BaseRegistrar {
3212
3212
  ];
3213
3213
  // Gandi's v5 API offers a sandbox at api.sandbox.gandi.net (separate account)
3214
3214
  static supportsSandbox = true;
3215
- // On top of core: transfer-out auth code (`authinfo`), DNSSEC read/disable
3216
- // (LiveDNS keys), email forwarding, and web/URL forwarding (webredirs;
3217
- // per host, apex included). Note core `setPrivacy` disabling is a no-op for
3215
+ // On top of core: transfer-out auth code (`authinfo`) and approval, DNSSEC
3216
+ // read/disable (LiveDNS keys), email forwarding, and web/URL forwarding
3217
+ // (webredirs; per host, apex included). Note core `setPrivacy` disabling is a no-op for
3218
3218
  // individual (natural-person) registrants — Gandi keeps WHOIS obfuscation on
3219
3219
  // for GDPR regardless of the request.
3220
3220
  static extendedFeatures = [
@@ -3224,7 +3224,10 @@ var GandiRegistrar = class extends BaseRegistrar {
3224
3224
  Feature.GetEmailForwarding,
3225
3225
  Feature.SetEmailForwarding,
3226
3226
  Feature.GetDomainForwarding,
3227
- Feature.SetDomainForwarding
3227
+ Feature.SetDomainForwarding,
3228
+ Feature.GetTransferOut,
3229
+ Feature.ApproveTransferOut,
3230
+ Feature.RejectTransferOut
3228
3231
  ];
3229
3232
  constructor(credentials, options) {
3230
3233
  super(
@@ -3476,6 +3479,58 @@ var GandiRegistrar = class extends BaseRegistrar {
3476
3479
  });
3477
3480
  return d.authinfo ?? "";
3478
3481
  }
3482
+ /**
3483
+ * The domain's pending outbound transfer. Gandi has no transfer-out resource
3484
+ * to read: a domain in the account carries the documented `pendingTransfer`
3485
+ * EPP status while another registrar's request waits (inbound transfers live
3486
+ * under /domain/transferin, so on a domain you hold it's outbound — inferred,
3487
+ * not documented). Gandi reports no dates for it, and nothing once it's
3488
+ * answered or done, so this reads `pending` or null.
3489
+ */
3490
+ async getTransferOut(domainName, opts) {
3491
+ const d = await this.http.request({
3492
+ path: `/domain/domains/${domainName}`,
3493
+ ...opts
3494
+ });
3495
+ return pendingTransferOut(d);
3496
+ }
3497
+ /**
3498
+ * Approves the pending outbound transfer, releasing the domain now instead of
3499
+ * after Gandi's five days: POST .../transferout with `accept: true` and the
3500
+ * domain's current auth code, which Gandi requires back (→ 202).
3501
+ */
3502
+ async approveTransferOut(domainName, opts) {
3503
+ return this.answerTransferOut(domainName, true, opts);
3504
+ }
3505
+ // Declines the pending outbound transfer (`accept: false`, no reason); the
3506
+ // domain stays in the account.
3507
+ async rejectTransferOut(domainName, opts) {
3508
+ return this.answerTransferOut(domainName, false, opts);
3509
+ }
3510
+ // One read for both the pending status and the auth code the answer needs.
3511
+ async answerTransferOut(domainName, accept, opts) {
3512
+ let d;
3513
+ try {
3514
+ d = await this.http.request({
3515
+ path: `/domain/domains/${domainName}`,
3516
+ ...opts
3517
+ });
3518
+ } catch (error) {
3519
+ return { success: false, message: toRegistrarError(error).message };
3520
+ }
3521
+ if (!pendingTransferOut(d)) {
3522
+ return { success: false, message: `No transfer-out of ${domainName} to answer` };
3523
+ }
3524
+ return this.mutate(
3525
+ {
3526
+ method: "POST",
3527
+ path: `/domain/domains/${domainName}/transferout`,
3528
+ body: { accept, authinfo: d.authinfo ?? "" }
3529
+ },
3530
+ `Transfer of ${domainName} ${accept ? "approved" : "rejected"}`,
3531
+ opts
3532
+ );
3533
+ }
3479
3534
  /**
3480
3535
  * DNSSEC status via Gandi LiveDNS keys (GET /livedns/domains/{fqdn}/keys). A
3481
3536
  * non-deleted key means DNSSEC is enabled; each key's `ds` field is the full DS
@@ -3843,6 +3898,17 @@ var GandiRegistrar = class extends BaseRegistrar {
3843
3898
  }
3844
3899
  }
3845
3900
  };
3901
+ function pendingTransferOut(d) {
3902
+ const statuses = Array.isArray(d.status) ? d.status : d.status ? [d.status] : [];
3903
+ if (!statuses.includes("pendingTransfer")) return null;
3904
+ return {
3905
+ status: "pending",
3906
+ rawStatus: "pendingTransfer",
3907
+ requestedDate: null,
3908
+ expirationDate: null,
3909
+ completedDate: null
3910
+ };
3911
+ }
3846
3912
  function toGandiContact(c) {
3847
3913
  const contact = {
3848
3914
  given: c.firstName,
@@ -3983,17 +4049,22 @@ var GoDaddyRegistrar = class _GoDaddyRegistrar extends BaseRegistrar {
3983
4049
  ];
3984
4050
  // GoDaddy's OTE ("Operational Test Environment") is its sandbox
3985
4051
  static supportsSandbox = true;
3986
- // Beyond core: domain forwarding and transfer-out auth code (the `authCode`
3987
- // field on the v1 domain-detail response). Forwarding uses the v2
3988
- // customer-scoped route /v2/customers/{customerId}/domains/forwards/{fqdn}
3989
- // (the old v1 /v1/domains/forwards routes now 404); it requires `customerId`
3990
- // to be configured — verified live 2026-08-29. DNSSEC has no dedicated
4052
+ // Beyond core: domain forwarding, transfer-out auth code (the `authCode`
4053
+ // field on the v1 domain-detail response) and transfer-out approval.
4054
+ // Forwarding uses the v2 customer-scoped route
4055
+ // /v2/customers/{customerId}/domains/forwards/{fqdn} (the old v1
4056
+ // /v1/domains/forwards routes now 404); it requires `customerId` to be
4057
+ // configured — verified live 2026-08-29. Transfer-out approval is v2
4058
+ // customer-scoped too, so it needs `customerId` as well. DNSSEC has no dedicated
3991
4059
  // endpoint (DS records go through the generic DNS API), and there's no
3992
4060
  // glue-record, email, or push-webhook API.
3993
4061
  static extendedFeatures = [
3994
4062
  Feature.GetAuthCode,
3995
4063
  Feature.GetDomainForwarding,
3996
- Feature.SetDomainForwarding
4064
+ Feature.SetDomainForwarding,
4065
+ Feature.GetTransferOut,
4066
+ Feature.ApproveTransferOut,
4067
+ Feature.RejectTransferOut
3997
4068
  ];
3998
4069
  // true → prefer the v3 API (production + PAT). false → all calls use v1
3999
4070
  // (sso-key auth, or OTE/sandbox where v3 does not exist).
@@ -4594,7 +4665,7 @@ var GoDaddyRegistrar = class _GoDaddyRegistrar extends BaseRegistrar {
4594
4665
  const configured = this.credentials.customerId?.trim();
4595
4666
  if (!configured) {
4596
4667
  throw new ConfigurationError(
4597
- `${this.name}: domain forwarding uses the v2 customer-scoped API, which requires a customer ID. Set \`customerId\` to your GoDaddy customer UUID (or your numeric shopper/account ID, which will be resolved to the UUID).`
4668
+ `${this.name}: domain forwarding and transfer-out approval use the v2 customer-scoped API, which requires a customer ID. Set \`customerId\` to your GoDaddy customer UUID (or your numeric shopper/account ID, which will be resolved to the UUID).`
4598
4669
  );
4599
4670
  }
4600
4671
  if (UUID_RE.test(configured)) return configured;
@@ -4694,6 +4765,52 @@ var GoDaddyRegistrar = class _GoDaddyRegistrar extends BaseRegistrar {
4694
4765
  forwardPath(customerId, fqdn) {
4695
4766
  return `/v2/customers/${encodeURIComponent(customerId)}/domains/forwards/${encodeURIComponent(fqdn)}`;
4696
4767
  }
4768
+ // the v2 customer-scoped path for a domain
4769
+ domainPathV2(customerId, domainName) {
4770
+ return `/v2/customers/${encodeURIComponent(customerId)}/domains/${encodeURIComponent(domainName)}`;
4771
+ }
4772
+ /**
4773
+ * The domain's latest outbound transfer, read from its recent actions
4774
+ * (GET /v2/customers/{customerId}/domains/{domain}/actions): the TRANSFER_OUT
4775
+ * action, and an accept or reject logged after it. Null when there is no
4776
+ * TRANSFER_OUT on record. GoDaddy reports no expiry; left alone, it releases
4777
+ * the domain within seven days. Shapes from the published v2 spec; a real
4778
+ * transfer-out is not yet seen live.
4779
+ */
4780
+ async getTransferOut(domainName, opts) {
4781
+ const customerId = await this.resolveCustomerId(opts);
4782
+ const actions = await this.http.request({
4783
+ path: `${this.domainPathV2(customerId, domainName)}/actions`,
4784
+ ...opts
4785
+ });
4786
+ return toTransferOut2(Array.isArray(actions) ? actions : []);
4787
+ }
4788
+ /**
4789
+ * Approves the pending outbound transfer, releasing the domain now
4790
+ * (POST .../transferOutAccept → 202). GoDaddy answers 409 when none is
4791
+ * pending.
4792
+ */
4793
+ async approveTransferOut(domainName, opts) {
4794
+ return this.answerTransferOut(domainName, "transferOutAccept", "approved", opts);
4795
+ }
4796
+ // Rejects the pending outbound transfer (POST .../transferOutReject, with no
4797
+ // reason); the domain stays in the account.
4798
+ async rejectTransferOut(domainName, opts) {
4799
+ return this.answerTransferOut(domainName, "transferOutReject", "rejected", opts);
4800
+ }
4801
+ async answerTransferOut(domainName, action, answer, opts) {
4802
+ let customerId;
4803
+ try {
4804
+ customerId = await this.resolveCustomerId(opts);
4805
+ } catch (error) {
4806
+ return { success: false, message: toRegistrarError(error).message };
4807
+ }
4808
+ return this.mutate(
4809
+ { method: "POST", path: `${this.domainPathV2(customerId, domainName)}/${action}` },
4810
+ `Transfer of ${domainName} ${answer}`,
4811
+ opts
4812
+ );
4813
+ }
4697
4814
  /**
4698
4815
  * Transfer authorization (EPP) code. GoDaddy exposes it as the `authCode`
4699
4816
  * field on the v1 domain-detail response; transfers were never moved to v3,
@@ -4750,6 +4867,41 @@ var GoDaddyRegistrar = class _GoDaddyRegistrar extends BaseRegistrar {
4750
4867
  }
4751
4868
  }
4752
4869
  };
4870
+ function toTransferOut2(actions) {
4871
+ const time = (a) => parseDate(a.createdAt)?.getTime() ?? 0;
4872
+ const byLatest = (types) => actions.filter((a) => types.includes(a.type ?? "")).sort((a, b) => time(b) - time(a));
4873
+ const transfer = byLatest(["TRANSFER_OUT"])[0];
4874
+ if (!transfer) return null;
4875
+ const answer = byLatest(["TRANSFER_OUT_ACCEPT", "TRANSFER_OUT_REJECT"]).find(
4876
+ (a) => time(a) >= time(transfer) && !/^(FAILED|CANCELLED)$/i.test(a.status ?? "")
4877
+ );
4878
+ const rejected = answer?.type === "TRANSFER_OUT_REJECT";
4879
+ const rawStatus = transfer.status ?? "";
4880
+ let status;
4881
+ switch (rawStatus.toUpperCase()) {
4882
+ case "ACCEPTED":
4883
+ case "AWAITING":
4884
+ case "PENDING":
4885
+ status = !answer ? "pending" : rejected ? "rejected" : "approved";
4886
+ break;
4887
+ case "SUCCESS":
4888
+ status = "completed";
4889
+ break;
4890
+ case "FAILED":
4891
+ case "CANCELLED":
4892
+ status = rejected ? "rejected" : "cancelled";
4893
+ break;
4894
+ default:
4895
+ status = "unknown";
4896
+ }
4897
+ return {
4898
+ status,
4899
+ rawStatus,
4900
+ requestedDate: parseDate(transfer.createdAt),
4901
+ expirationDate: null,
4902
+ completedDate: status === "completed" ? parseDate(transfer.completedAt) : null
4903
+ };
4904
+ }
4753
4905
  function fromGoDaddyContact(c) {
4754
4906
  if (!c) return void 0;
4755
4907
  const a = c.addressMailing ?? {};