From a06fb3fab3cd7eb3012203ac044950e6653bb948 Mon Sep 17 00:00:00 2001 From: Jorge Cortes Date: Mon, 27 Jan 2025 10:45:32 -0500 Subject: [PATCH] [Components] opensrs - new components --- .../initiate-domain-transfer.mjs | 157 ++++++ .../register-domain/register-domain.mjs | 530 ++++++++++++++++++ .../update-dns-records/update-dns-records.mjs | 396 +++++++++++++ components/opensrs/common/constants.mjs | 43 ++ components/opensrs/common/utils.mjs | 177 ++++++ components/opensrs/opensrs.app.mjs | 85 ++- components/opensrs/package.json | 8 +- components/opensrs/sources/common/events.mjs | 13 + components/opensrs/sources/common/polling.mjs | 103 ++++ .../new-dns-zone-change.mjs | 29 + .../new-dns-zone-change/test-event.mjs | 11 + .../new-domain-registration.mjs | 29 + .../new-domain-registration/test-event.mjs | 12 + .../new-transfer-status.mjs | 29 + .../new-transfer-status/test-event.mjs | 12 + pnpm-lock.yaml | 11 +- 16 files changed, 1636 insertions(+), 9 deletions(-) create mode 100644 components/opensrs/actions/initiate-domain-transfer/initiate-domain-transfer.mjs create mode 100644 components/opensrs/actions/register-domain/register-domain.mjs create mode 100644 components/opensrs/actions/update-dns-records/update-dns-records.mjs create mode 100644 components/opensrs/common/constants.mjs create mode 100644 components/opensrs/common/utils.mjs create mode 100644 components/opensrs/sources/common/events.mjs create mode 100644 components/opensrs/sources/common/polling.mjs create mode 100644 components/opensrs/sources/new-dns-zone-change/new-dns-zone-change.mjs create mode 100644 components/opensrs/sources/new-dns-zone-change/test-event.mjs create mode 100644 components/opensrs/sources/new-domain-registration/new-domain-registration.mjs create mode 100644 components/opensrs/sources/new-domain-registration/test-event.mjs create mode 100644 components/opensrs/sources/new-transfer-status/new-transfer-status.mjs create mode 100644 components/opensrs/sources/new-transfer-status/test-event.mjs diff --git a/components/opensrs/actions/initiate-domain-transfer/initiate-domain-transfer.mjs b/components/opensrs/actions/initiate-domain-transfer/initiate-domain-transfer.mjs new file mode 100644 index 0000000000000..fc615c8dc6127 --- /dev/null +++ b/components/opensrs/actions/initiate-domain-transfer/initiate-domain-transfer.mjs @@ -0,0 +1,157 @@ +import app from "../../opensrs.app.mjs"; +import utils from "../../common/utils.mjs"; +import constants from "../../common/constants.mjs"; + +export default { + key: "opensrs-initiate-domain-transfer", + name: "Initiate Domain Transfer", + description: "Initiate a domain transfer to OpenSRS. [See the documentation](https://domains.opensrs.guide/docs/trade_domain).", + version: "0.0.1", + type: "action", + props: { + app, + domain: { + propDefinition: [ + app, + "domain", + ], + }, + email: { + type: "string", + label: "Email", + description: "The email address of the new owner of the domain.", + }, + firstName: { + type: "string", + label: "First Name", + description: "The first name of the new owner of the domain.", + }, + lastName: { + type: "string", + label: "Last Name", + description: "The last name of the new owner of the domain.", + }, + orgName: { + type: "string", + label: "Organization Name", + description: "The organization name of the new owner of the domain.", + }, + phone: { + type: "string", + label: "Phone", + description: "The phone number of the new owner of the domain. Required for all except `.BE`.", + optional: true, + }, + address1: { + type: "string", + label: "Address 1", + description: "The first line of the address of the new owner of the domain. Required for all except `.BE`.", + optional: true, + }, + city: { + type: "string", + label: "City", + description: "The city of the new owner of the domain. Required for all except `.BE`.", + optional: true, + }, + country: { + type: "string", + label: "Country", + description: "The country of the new owner of the domain. Required for all except `.BE`.", + optional: true, + }, + state: { + type: "string", + label: "State", + description: "The state of the new owner of the domain. Required for all except `.BE`.", + optional: true, + }, + postalCode: { + type: "string", + label: "Postal Code", + description: "The postal code of the new owner of the domain. Required for all except `.BE`.", + optional: true, + }, + domainAuthInfo: { + type: "string", + label: "Domain Authorization Info", + description: "The authorization code required for domain transfer. Required for `.BE`.", + optional: true, + }, + jsonOutput: { + type: "boolean", + label: "JSON Output", + description: "Whether to output the response in JSON format.", + optional: true, + default: true, + }, + }, + methods: { + getJsonBody() { + const { + domain, + email, + firstName, + lastName, + orgName, + phone, + address1, + city, + country, + state, + postalCode, + domainAuthInfo, + } = this; + + return { + data_block: { + dt_assoc: { + item: [ + ...utils.addItem("protocol", constants.PROTOCOL.XCP), + ...utils.addItem("object", constants.OBJECT_TYPE.DOMAIN), + ...utils.addItem("action", constants.ACTION_TYPE.TRADE_DOMAIN), + { + "@_key": "attributes", + "dt_assoc": { + item: [ + ...utils.addItem("domain", domain), + ...utils.addItem("email", email), + ...utils.addItem("first_name", firstName), + ...utils.addItem("last_name", lastName), + ...utils.addItem("org_name", orgName), + ...utils.addItem("phone", phone), + ...utils.addItem("address1", address1), + ...utils.addItem("city", city), + ...utils.addItem("country", country), + ...utils.addItem("state", state), + ...utils.addItem("postal_code", postalCode), + ...utils.addItem("domain_auth_info", domainAuthInfo), + ], + }, + }, + ], + }, + }, + }; + }, + initiateDomainTransfer(args = {}) { + return this.app.post(args); + }, + }, + async run({ $ }) { + const { + initiateDomainTransfer, + getJsonBody, + jsonOutput, + } = this; + + const response = await initiateDomainTransfer({ + $, + jsonBody: getJsonBody(), + jsonOutput, + }); + + $.export("$summary", "Successfully initiated domain transfer."); + return response; + }, +}; diff --git a/components/opensrs/actions/register-domain/register-domain.mjs b/components/opensrs/actions/register-domain/register-domain.mjs new file mode 100644 index 0000000000000..c37a85ee722eb --- /dev/null +++ b/components/opensrs/actions/register-domain/register-domain.mjs @@ -0,0 +1,530 @@ +import app from "../../opensrs.app.mjs"; +import constants from "../../common/constants.mjs"; +import utils from "../../common/utils.mjs"; + +export default { + key: "opensrs-register-domain", + name: "Register Domain", + description: "Register a new domain. [See the documentation](https://domains.opensrs.guide/docs/sw_register-domain-or-trust_service-).", + version: "0.0.1", + type: "action", + props: { + app, + domain: { + propDefinition: [ + app, + "domain", + ], + }, + regUsername: { + type: "string", + label: "Reseller Username", + description: "Usernames must be 3-20 characters in length. You can use any of the following alphanumeric characters: `A-Z`, `a-z`, `0-9`.", + }, + regPassword: { + type: "string", + label: "Reseller Password", + description: "Passwords must be 10-20 characters in length. You can use any of the following alphanumeric characters and symbols: `A-Z`, `a-z`, `0-9`, `! @$^,.~|=-+_{}#`.", + }, + ownerFirstName: { + type: "string", + label: "Owner First Name", + description: "The first name of the owner of the domain.", + }, + ownerLastName: { + type: "string", + label: "Owner Last Name", + description: "The last name of the owner of the domain.", + }, + ownerEmail: { + type: "string", + label: "Owner Email", + description: "The email of the owner of the domain.", + }, + ownerOrgName: { + type: "string", + label: "Owner Organization Name", + description: "The organization name of the owner of the domain.", + optional: true, + }, + ownerPhone: { + type: "string", + label: "Owner Phone", + description: "The phone number of the owner of the domain.", + }, + ownerAddress1: { + type: "string", + label: "Owner Address 1", + description: "The first line of the address of the owner of the domain.", + }, + ownerCity: { + type: "string", + label: "Owner City", + description: "The city of the owner of the domain.", + }, + ownerCountry: { + type: "string", + label: "Owner Country", + description: "The country of the owner of the domain.", + }, + ownerState: { + type: "string", + label: "Owner State", + description: "The state of the owner of the domain.", + }, + ownerPostalCode: { + type: "string", + label: "Owner Postal Code", + description: "The postal code of the owner of the domain.", + }, + adminFirstName: { + type: "string", + label: "Admin First Name", + description: "The first name of the admin of the domain.", + }, + adminLastName: { + type: "string", + label: "Admin Last Name", + description: "The last name of the admin of the domain.", + }, + adminEmail: { + type: "string", + label: "Admin Email", + description: "The email of the admin of the domain.", + }, + adminOrgName: { + type: "string", + label: "Admin Organization Name", + description: "The organization name of the admin of the domain.", + optional: true, + }, + adminPhone: { + type: "string", + label: "Admin Phone", + description: "The phone number of the admin of the domain.", + }, + adminAddress1: { + type: "string", + label: "Admin Address 1", + description: "The first line of the address of the admin of the domain.", + }, + adminCity: { + type: "string", + label: "Admin City", + description: "The city of the admin of the domain.", + }, + adminCountry: { + type: "string", + label: "Admin Country", + description: "The country of the admin of the domain.", + }, + adminState: { + type: "string", + label: "Admin State", + description: "The state of the admin of the domain.", + }, + adminPostalCode: { + type: "string", + label: "Admin Postal Code", + description: "The postal code of the admin of the domain.", + }, + billingFirstName: { + type: "string", + label: "Billing First Name", + description: "The first name of the billing contact of the domain.", + }, + billingLastName: { + type: "string", + label: "Billing Last Name", + description: "The last name of the billing contact of the domain.", + }, + billingEmail: { + type: "string", + label: "Billing Email", + description: "The email of the billing contact of the domain.", + }, + billingOrgName: { + type: "string", + label: "Billing Organization Name", + description: "The organization name of the billing contact of the domain.", + optional: true, + }, + billingPhone: { + type: "string", + label: "Billing Phone", + description: "The phone number of the billing contact of the domain.", + }, + billingAddress1: { + type: "string", + label: "Billing Address 1", + description: "The first line of the address of the billing contact of the domain.", + }, + billingCity: { + type: "string", + label: "Billing City", + description: "The city of the billing contact of the domain.", + }, + billingCountry: { + type: "string", + label: "Billing Country", + description: "The country of the billing contact of the domain.", + }, + billingState: { + type: "string", + label: "Billing State", + description: "The state of the billing contact of the domain.", + }, + billingPostalCode: { + type: "string", + label: "Billing Postal Code", + description: "The postal code of the billing contact of the domain.", + }, + techFirstName: { + type: "string", + label: "Tech First Name", + description: "The first name of the tech contact of the domain.", + }, + techLastName: { + type: "string", + label: "Tech Last Name", + description: "The last name of the tech contact of the domain.", + }, + techEmail: { + type: "string", + label: "Tech Email", + description: "The email of the tech contact of the domain.", + }, + techOrgName: { + type: "string", + label: "Tech Organization Name", + description: "The organization name of the tech contact of the domain.", + optional: true, + }, + techPhone: { + type: "string", + label: "Tech Phone", + description: "The phone number of the tech contact of the domain.", + }, + techAddress1: { + type: "string", + label: "Tech Address 1", + description: "The first line of the address of the tech contact of the domain.", + }, + techCity: { + type: "string", + label: "Tech City", + description: "The city of the tech contact of the domain.", + }, + techCountry: { + type: "string", + label: "Tech Country", + description: "The country of the tech contact of the domain.", + }, + techState: { + type: "string", + label: "Tech State", + description: "The state of the tech contact of the domain.", + }, + techPostalCode: { + type: "string", + label: "Tech Postal Code", + description: "The postal code of the tech contact of the domain.", + }, + customTechContact: { + type: "string", + label: "Custom Tech Contact", + description: "An indication of whether to use the RSP's tech contact info, or the tech contact info provided n the request.", + default: "0", + options: [ + { + label: "Use reseller's tech contact info.", + value: "0", + }, + { + label: "Use tech contact info provided in request.", + value: "1", + }, + ], + }, + autoRenew: { + type: "string", + label: "Auto Renew", + description: "Whether to automatically renew the domain.", + optional: true, + options: [ + { + label: "Do not auto-renew", + value: "0", + }, + { + label: "Auto-renew", + value: "1", + }, + ], + }, + period: { + type: "string", + label: "Period", + description: "The length of the registration period. Allowed values are `1-10`, depending on the TLD, that is, not all registries allow for a 1-year registration. The default is `2`, which is valid for all TLDs.", + default: "2", + options: [ + { + label: "1 Year", + value: "1", + }, + { + label: "2 Years", + value: "2", + }, + { + label: "3 Years", + value: "3", + }, + { + label: "4 Years", + value: "4", + }, + { + label: "5 Years", + value: "5", + }, + { + label: "6 Years", + value: "6", + }, + { + label: "7 Years", + value: "7", + }, + { + label: "8 Years", + value: "8", + }, + { + label: "9 Years", + value: "9", + }, + { + label: "10 Years", + value: "10", + }, + ], + }, + customNameservers: { + type: "string", + label: "Custom Nameservers", + description: "Custom nameservers for the domain.", + reloadProps: true, + options: [ + { + label: "Use reseller's default nameservers", + value: "0", + }, + { + label: "Use nameservers provided in request", + value: "1", + }, + ], + }, + jsonOutput: { + type: "boolean", + label: "JSON Output", + description: "Whether to output the response in JSON format.", + optional: true, + default: true, + }, + }, + additionalProps() { + const { customNameservers } = this; + if (customNameservers === "1") { + return { + nameserverList: { + type: "string[]", + label: "Nameserver List", + description: "List of nameservers for the domain. Eg. `ns1.opensrs.net`, `ns2.opensrs.net`.", + }, + }; + } + return {}; + }, + methods: { + getJsonBody() { + const { + domain, + regUsername, + regPassword, + ownerFirstName, + ownerLastName, + ownerEmail, + ownerOrgName, + ownerPhone, + ownerAddress1, + ownerCity, + ownerCountry, + ownerState, + ownerPostalCode, + adminFirstName, + adminLastName, + adminEmail, + adminOrgName, + adminPhone, + adminAddress1, + adminCity, + adminCountry, + adminState, + adminPostalCode, + billingFirstName, + billingLastName, + billingEmail, + billingOrgName, + billingPhone, + billingAddress1, + billingCity, + billingCountry, + billingState, + billingPostalCode, + techFirstName, + techLastName, + techEmail, + techOrgName, + techPhone, + techAddress1, + techCity, + techCountry, + techState, + techPostalCode, + customTechContact, + autoRenew, + customNameservers, + nameserverList, + period, + } = this; + return { + data_block: { + dt_assoc: { + item: [ + ...utils.addItem("protocol", constants.PROTOCOL.XCP), + ...utils.addItem("object", constants.OBJECT_TYPE.DOMAIN), + ...utils.addItem("action", constants.ACTION_TYPE.SW_REGISTER), + { + "@_key": "attributes", + "dt_assoc": { + item: [ + ...utils.addItem("domain", domain), + ...utils.addItem("reg_username", regUsername), + ...utils.addItem("reg_password", regPassword), + ...utils.addItem("reg_type", constants.REGISTRY_TYPE.NEW), + ...utils.addItem("custom_nameservers", customNameservers), + ...utils.addItem("period", period), + ...utils.addItem("auto_renew", autoRenew), + ...utils.addItemList( + "nameserver_list", + customNameservers === "1" + ? nameserverList || [] + : constants.DEFAULT_NAMESERVER_LIST, + ), + ...utils.addItem("custom_tech_contact", customTechContact), + { + "@_key": "contact_set", + "dt_assoc": { + item: [ + { + "@_key": "owner", + "dt_assoc": { + item: [ + ...utils.addItem("first_name", ownerFirstName), + ...utils.addItem("last_name", ownerLastName), + ...utils.addItem("phone", ownerPhone), + ...utils.addItem("email", ownerEmail), + ...utils.addItem("address1", ownerAddress1), + ...utils.addItem("city", ownerCity), + ...utils.addItem("state", ownerState), + ...utils.addItem("country", ownerCountry), + ...utils.addItem("postal_code", ownerPostalCode), + ...utils.addItem("org_name", ownerOrgName), + ], + }, + }, + { + "@_key": "admin", + "dt_assoc": { + item: [ + ...utils.addItem("first_name", adminFirstName), + ...utils.addItem("last_name", adminLastName), + ...utils.addItem("phone", adminPhone), + ...utils.addItem("email", adminEmail), + ...utils.addItem("address1", adminAddress1), + ...utils.addItem("city", adminCity), + ...utils.addItem("state", adminState), + ...utils.addItem("country", adminCountry), + ...utils.addItem("postal_code", adminPostalCode), + ...utils.addItem("org_name", adminOrgName), + ], + }, + }, + { + "@_key": "billing", + "dt_assoc": { + item: [ + ...utils.addItem("first_name", billingFirstName), + ...utils.addItem("last_name", billingLastName), + ...utils.addItem("phone", billingPhone), + ...utils.addItem("email", billingEmail), + ...utils.addItem("address1", billingAddress1), + ...utils.addItem("city", billingCity), + ...utils.addItem("state", billingState), + ...utils.addItem("country", billingCountry), + ...utils.addItem("postal_code", billingPostalCode), + ...utils.addItem("org_name", billingOrgName), + ], + }, + }, + { + "@_key": "tech", + "dt_assoc": { + item: [ + ...utils.addItem("first_name", techFirstName), + ...utils.addItem("last_name", techLastName), + ...utils.addItem("phone", techPhone), + ...utils.addItem("email", techEmail), + ...utils.addItem("address1", techAddress1), + ...utils.addItem("city", techCity), + ...utils.addItem("state", techState), + ...utils.addItem("country", techCountry), + ...utils.addItem("postal_code", techPostalCode), + ...utils.addItem("org_name", techOrgName), + ], + }, + }, + ], + }, + }, + ], + }, + }, + ], + }, + }, + }; + }, + registerDomain(args = {}) { + return this.app.post(args); + }, + }, + async run({ $ }) { + const { + registerDomain, + getJsonBody, + jsonOutput, + } = this; + + const response = await registerDomain({ + $, + jsonBody: getJsonBody(), + jsonOutput, + }); + + $.export("$summary", "Successfully registered domain."); + return response; + }, +}; diff --git a/components/opensrs/actions/update-dns-records/update-dns-records.mjs b/components/opensrs/actions/update-dns-records/update-dns-records.mjs new file mode 100644 index 0000000000000..b84a7175eeefe --- /dev/null +++ b/components/opensrs/actions/update-dns-records/update-dns-records.mjs @@ -0,0 +1,396 @@ +import app from "../../opensrs.app.mjs"; +import utils from "../../common/utils.mjs"; +import constants from "../../common/constants.mjs"; + +export default { + key: "opensrs-update-dns-records", + name: "Update DNS Records", + description: "Update DNS records for a specified domain. [See the documentation](https://domains.opensrs.guide/docs/set_dns_zone-).", + version: "0.0.1", + type: "action", + props: { + app, + domain: { + propDefinition: [ + app, + "domain", + ], + }, + nameserversOk: { + type: "string", + label: "Nameservers OK", + description: "Indicates whether the domain is set up to use the OpenSRS nameservers.", + default: "0", + options: [ + { + label: "Domain is not set up to use the OpenSRS nameservers", + value: "0", + }, + { + label: "Domain is set up to use the OpenSRS nameservers", + value: "1", + }, + ], + }, + numberOfARecords: { + type: "integer", + label: "Number of A Records", + description: "Number of A records to update", + default: 0, + reloadProps: true, + }, + numberOfAAAARecords: { + type: "integer", + label: "Number of AAAA Records", + description: "Number of AAAA records to update", + default: 0, + reloadProps: true, + }, + numberOfCNAMERecords: { + type: "integer", + label: "Number of CNAME Records", + description: "Number of CNAME records to update", + default: 0, + reloadProps: true, + }, + numberOfMXRecords: { + type: "integer", + label: "Number of MX Records", + description: "Number of MX records to update", + default: 0, + reloadProps: true, + }, + numberOfTXTRecords: { + type: "integer", + label: "Number of TXT Records", + description: "Number of TXT records to update", + default: 0, + reloadProps: true, + }, + jsonOutput: { + type: "boolean", + label: "JSON Output", + description: "Whether to output the response in JSON format.", + optional: true, + default: true, + }, + }, + additionalProps() { + const { + numberOfARecords, + numberOfAAAARecords, + numberOfCNAMERecords, + numberOfMXRecords, + numberOfTXTRecords, + getARecordPropDefinitions, + getAAAARecordPropDefinitions, + getCNAMERecordPropDefinitions, + getMXRecordPropDefinitions, + getTXTRecordPropDefinitions, + } = this; + + const aRecords = utils.getAdditionalProps({ + numberOfFields: numberOfARecords, + fieldName: "a", + getPropDefinitions: getARecordPropDefinitions, + }); + + const aaaaRecords = utils.getAdditionalProps({ + numberOfFields: numberOfAAAARecords, + fieldName: "aaaa", + getPropDefinitions: getAAAARecordPropDefinitions, + }); + + const cnameRecords = utils.getAdditionalProps({ + numberOfFields: numberOfCNAMERecords, + fieldName: "cname", + getPropDefinitions: getCNAMERecordPropDefinitions, + }); + + const mxRecords = utils.getAdditionalProps({ + numberOfFields: numberOfMXRecords, + fieldName: "mx", + getPropDefinitions: getMXRecordPropDefinitions, + }); + + const txtRecords = utils.getAdditionalProps({ + numberOfFields: numberOfTXTRecords, + fieldName: "txt", + getPropDefinitions: getTXTRecordPropDefinitions, + }); + + return Object.assign({}, aRecords, aaaaRecords, cnameRecords, mxRecords, txtRecords); + }, + methods: { + getARecordPropDefinitions({ + prefix, label, + } = {}) { + return { + [`${prefix}ipAddress`]: { + type: "string", + label: `${label} - IP Address`, + description: "The IPv4 address for the A record. A numeric address that computers recognize eg. `123.45.54.123`.", + }, + [`${prefix}subdomain`]: { + type: "string", + label: `${label} - Subdomain`, + description: "The subdomain for the A record. The third level of the domain name, such as `www` or `ftp`.", + optional: true, + }, + }; + }, + getAAAARecordPropDefinitions({ + prefix, label, + } = {}) { + return { + [`${prefix}ipv6Address`]: { + type: "string", + label: `${label} - IPv6 Address`, + description: "The IPv6 address for the AAAA record. A numeric address that computers recognize eg. `2001:0db8:85a3:0000:0000:8a2e:0370:7334`.", + }, + [`${prefix}subdomain`]: { + type: "string", + label: `${label} - Subdomain`, + description: "The subdomain for the AAAA record. The third level of the domain name, such as `www` or `ftp`.", + }, + }; + }, + getCNAMERecordPropDefinitions({ + prefix, label, + } = {}) { + return { + [`${prefix}hostname`]: { + type: "string", + label: `${label} - Hostname`, + description: "The FQDN of the domain that you want to access.", + }, + [`${prefix}subdomain`]: { + type: "string", + label: `${label} - Subdomain`, + description: "The third level of the domain name, such as `www` or `ftp`.", + }, + }; + }, + getMXRecordPropDefinitions({ + prefix, label, + } = {}) { + return { + [`${prefix}priority`]: { + type: "string", + label: `${label} - Priority`, + description: "The priority of the target host, lower value means more preferred.", + optional: true, + }, + [`${prefix}hostname`]: { + type: "string", + label: `${label} - Hostname`, + description: "The FQDN of the domain that you want to access.", + }, + [`${prefix}subdomain`]: { + type: "string", + label: `${label} - Subdomain`, + description: "The third level of the domain name, such as `www` or `ftp`.", + }, + }; + }, + getTXTRecordPropDefinitions({ + prefix, label, + } = {}) { + return { + [`${prefix}subdomain`]: { + type: "string", + label: `${label} - Subdomain`, + description: "The third level of the domain name, such as `www` or `ftp`.", + }, + [`${prefix}text`]: { + type: "string", + label: `${label} - Text`, + description: "The text content for the TXT record.", + }, + }; + }, + aRecordPropsMapper(prefix) { + const { + [`${prefix}ipAddress`]: ipAddress, + [`${prefix}subdomain`]: subdomain, + } = this; + return { + dt_assoc: { + item: [ + ...utils.addItem("subdomain", subdomain), + ...utils.addItem("ip_address", ipAddress), + ], + }, + }; + }, + aaaaRecordPropsMapper(prefix) { + const { + [`${prefix}ipv6Address`]: ipv6Address, + [`${prefix}subdomain`]: subdomain, + } = this; + return { + dt_assoc: { + item: [ + ...utils.addItem("subdomain", subdomain), + ...utils.addItem("ipv6_address", ipv6Address), + ], + }, + }; + }, + cnameRecordPropsMapper(prefix) { + const { + [`${prefix}hostname`]: hostname, + [`${prefix}subdomain`]: subdomain, + } = this; + return { + dt_assoc: { + item: [ + ...utils.addItem("subdomain", subdomain), + ...utils.addItem("hostname", hostname), + ], + }, + }; + }, + mxRecordPropsMapper(prefix) { + const { + [`${prefix}priority`]: priority, + [`${prefix}hostname`]: hostname, + [`${prefix}subdomain`]: subdomain, + } = this; + return { + dt_assoc: { + item: [ + ...utils.addItem("subdomain", subdomain), + ...utils.addItem("hostname", hostname), + ...utils.addItem("priority", priority), + ], + }, + }; + }, + txtRecordPropsMapper(prefix) { + const { + [`${prefix}subdomain`]: subdomain, + [`${prefix}text`]: text, + } = this; + return { + dt_assoc: { + item: [ + ...utils.addItem("subdomain", subdomain), + ...utils.addItem("text", text), + ], + }, + }; + }, + getJsonBody() { + const { + domain, + nameserversOk, + numberOfARecords, + numberOfAAAARecords, + numberOfCNAMERecords, + numberOfMXRecords, + numberOfTXTRecords, + aRecordPropsMapper, + aaaaRecordPropsMapper, + cnameRecordPropsMapper, + mxRecordPropsMapper, + txtRecordPropsMapper, + } = this; + + const aRecords = utils.getFieldsProps({ + numberOfFields: numberOfARecords, + fieldName: "a", + propsMapper: aRecordPropsMapper, + }); + + const aaaaRecords = utils.getFieldsProps({ + numberOfFields: numberOfAAAARecords, + fieldName: "aaaa", + propsMapper: aaaaRecordPropsMapper, + }); + + const cnameRecords = utils.getFieldsProps({ + numberOfFields: numberOfCNAMERecords, + fieldName: "cname", + propsMapper: cnameRecordPropsMapper, + }); + + const mxRecords = utils.getFieldsProps({ + numberOfFields: numberOfMXRecords, + fieldName: "mx", + propsMapper: mxRecordPropsMapper, + }); + + const txtRecords = utils.getFieldsProps({ + numberOfFields: numberOfTXTRecords, + fieldName: "txt", + propsMapper: txtRecordPropsMapper, + }); + + const records = [ + ...aRecords, + ...aaaaRecords, + ...cnameRecords, + ...mxRecords, + ...txtRecords, + ]; + + return { + data_block: { + dt_assoc: { + item: [ + ...utils.addItem("protocol", constants.PROTOCOL.XCP), + ...utils.addItem("object", constants.OBJECT_TYPE.DOMAIN), + ...utils.addItem("action", constants.ACTION_TYPE.SET_DNS_ZONE), + { + "@_key": "attributes", + "dt_assoc": { + item: [ + ...utils.addItem("domain", domain), + ...(records.length + ? [ + ...utils.addItem("nameservers_ok", nameserversOk), + { + "@_key": "records", + "dt_assoc": { + item: [ + ...utils.addDnsRecord("A", aRecords), + ...utils.addDnsRecord("AAAA", aaaaRecords), + ...utils.addDnsRecord("CNAME", cnameRecords), + ...utils.addDnsRecord("MX", mxRecords), + ...utils.addDnsRecord("TXT", txtRecords), + ], + }, + }, + ] + : [] + ), + ], + }, + }, + ], + }, + }, + }; + }, + updateDnsRecords(args) { + return this.app.post(args); + }, + }, + async run({ $ }) { + const { + updateDnsRecords, + getJsonBody, + jsonOutput, + } = this; + + const response = await updateDnsRecords({ + $, + jsonBody: getJsonBody(), + jsonOutput, + }); + + $.export("$summary", "Successfully updated DNS records."); + return response; + }, +}; diff --git a/components/opensrs/common/constants.mjs b/components/opensrs/common/constants.mjs new file mode 100644 index 0000000000000..d852bb88fa808 --- /dev/null +++ b/components/opensrs/common/constants.mjs @@ -0,0 +1,43 @@ +const DEFAULT_LIMIT = 100; +const SEP = "_"; + +const REGISTRY_TYPE = { + NEW: "new", + TRANSFER: "transfer", + SUNRISE: "sunrise", +}; + +const PROTOCOL = { + XCP: "XCP", +}; + +const OBJECT_TYPE = { + DOMAIN: "DOMAIN", + EVENT: "EVENT", + ORDER: "ORDER", + TRANSFER: "TRANSFER", +}; + +const ACTION_TYPE = { + SW_REGISTER: "SW_REGISTER", + SET_DNS_ZONE: "SET_DNS_ZONE", + TRADE_DOMAIN: "TRADE_DOMAIN", + POLL: "POLL", + ACKNOWLEDGE: "ACK", +}; + +const DEFAULT_NAMESERVER_LIST = [ + "ns1.systemdns.com", + "ns2.systemdns.com", + "ns3.systemdns.com", +]; + +export default { + DEFAULT_LIMIT, + SEP, + REGISTRY_TYPE, + PROTOCOL, + OBJECT_TYPE, + ACTION_TYPE, + DEFAULT_NAMESERVER_LIST, +}; diff --git a/components/opensrs/common/utils.mjs b/components/opensrs/common/utils.mjs new file mode 100644 index 0000000000000..610b09c16f156 --- /dev/null +++ b/components/opensrs/common/utils.mjs @@ -0,0 +1,177 @@ +import { XMLBuilder } from "fast-xml-parser"; +import constants from "./constants.mjs"; + +const builder = new XMLBuilder({ + ignoreAttributes: false, + suppressBooleanAttributes: false, + arrayNodeName: "item", + textNodeName: "value", +}); + +function toPascalCase(str) { + return str.replace(/(\w)(\w*)/g, (_, group1, group2) => + group1.toUpperCase() + group2.toLowerCase()); +} + +function toUpperCase(str) { + return str.toUpperCase(); +} + +function getMetadataProp({ + index, fieldName, prefix, label, labelTransform, +} = {}) { + const fieldIdx = index + 1; + const key = `${fieldName}${fieldIdx}`; + return { + prefix: prefix + ? `${prefix}${key}${constants.SEP}` + : `${key}${constants.SEP}`, + label: label + ? `${label} - ${labelTransform(fieldName)} ${fieldIdx}` + : `${labelTransform(fieldName)} ${fieldIdx}`, + }; +} + +function getFieldProps({ + index, fieldName, prefix, labelTransform = toUpperCase, + propsMapper = function propsMapper(prefix) { + const { [`${prefix}name`]: name } = this; + return { + name, + }; + }, +} = {}) { + const { prefix: metaPrefix } = getMetadataProp({ + index, + fieldName, + prefix, + labelTransform, + }); + return propsMapper(metaPrefix); +} + +function getFieldsProps({ + numberOfFields, fieldName, propsMapper, prefix, +} = {}) { + return Array.from({ + length: numberOfFields, + }).map((_, index) => getFieldProps({ + index, + fieldName, + prefix, + propsMapper, + })); +} + +function getAdditionalProps({ + numberOfFields, fieldName, prefix, label, labelTransform = toUpperCase, + getPropDefinitions = ({ + prefix, label, + }) => ({ + [`${prefix}name`]: { + type: "string", + label, + description: "The name of the field.", + optional: true, + }, + }), +} = {}) { + return Array.from({ + length: numberOfFields, + }).reduce((acc, _, index) => { + const { + prefix: metaPrefix, + label: metaLabel, + } = getMetadataProp({ + index, + fieldName, + prefix, + label, + labelTransform, + }); + + return Object.assign({}, acc, getPropDefinitions({ + prefix: metaPrefix, + label: metaLabel, + })); + }, {}); +} + +function getXml(body) { + return { + "?xml": { + "@_version": "1.0", + "@_encoding": "UTF-8", + "@_standalone": "no", + }, + "OPS_envelope": { + header: { + version: "0.9", + }, + body, + }, + }; +} + +function addItem(key, value) { + return value + ? [ + { + "@_key": key, + value, + }, + ] + : []; +} + +function addItemList(key, array) { + return array?.length + ? [ + { + "@_key": key, + "dt_array": { + item: array.map((value, index) => ({ + "@_key": index.toString(), + "dt_assoc": { + item: [ + ...addItem("name", value), + ...addItem("sortorder", index + 1), + ], + }, + })), + }, + }, + ] + : []; +} + +function addDnsRecord(key, records) { + return records.length + ? [ + { + "@_key": key, + "dt_array": { + item: records.map((record, index) => ({ + "@_key": index.toString(), + ...record, + })), + }, + }, + ] + : []; +} + +function buildXmlData(data) { + return builder.build(getXml(data)); +} + +export default { + buildXmlData, + addItem, + addItemList, + addDnsRecord, + getAdditionalProps, + getFieldsProps, + toUpperCase, + toPascalCase, +}; diff --git a/components/opensrs/opensrs.app.mjs b/components/opensrs/opensrs.app.mjs index 7e487826a6670..9cfe7fae96be7 100644 --- a/components/opensrs/opensrs.app.mjs +++ b/components/opensrs/opensrs.app.mjs @@ -1,11 +1,88 @@ +import { createHash } from "crypto"; +import { XMLParser } from "fast-xml-parser"; +import { axios } from "@pipedream/platform"; +import utils from "./common/utils.mjs"; + +const parser = new XMLParser({ + ignoreAttributes: false, + arrayMode: true, + textNodeName: "value", +}); + export default { type: "app", app: "opensrs", - propDefinitions: {}, + propDefinitions: { + domain: { + type: "string", + label: "Domain", + description: "The domain name to register, update, or transfer.", + }, + }, methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); + getUrl() { + const { api_host_port: url } = this.$auth; + return url; + }, + generateSignature(data) { + const { api_key: apiKey } = this.$auth; + const signature = createHash("md5") + .update(data + apiKey) + .digest("hex"); + return createHash("md5") + .update(signature + apiKey) + .digest("hex"); + }, + getHeaders(data, headers) { + const { reseller_username: username } = this.$auth; + return { + ...headers, + "Content-Type": "text/xml", + "X-Username": username, + "X-Signature": this.generateSignature(data), + }; + }, + throwIfError(jsonResponse) { + const { item: items } = jsonResponse?.OPS_envelope?.body?.data_block?.dt_assoc || {}; + const attributes = items?.find((item) => item["@_key"] === "attributes"); + const { item: errorItems } = attributes?.dt_assoc || {}; + const errorItem = errorItems?.find((item) => item["@_key"] === "error"); + if (errorItem) { + throw new Error(errorItem.value); + } + + const isSuccessItem = items?.find((item) => item["@_key"] === "is_success"); + const responseTextItem = items?.find((item) => item["@_key"] === "response_text"); + + if (isSuccessItem?.value === 0 && responseTextItem) { + console.log("response", JSON.stringify(jsonResponse, null, 2)); + throw new Error(responseTextItem.value); + } + }, + async _makeRequest({ + $ = this, jsonBody, headers, jsonOutput = true, ...args + } = {}) { + const data = utils.buildXmlData(jsonBody); + + const response = await axios($, { + ...args, + url: this.getUrl(), + headers: this.getHeaders(data, headers), + data, + }); + + const jsonResponse = parser.parse(response); + this.throwIfError(jsonResponse); + + return jsonOutput + ? jsonResponse + : response; + }, + post(args = {}) { + return this._makeRequest({ + method: "POST", + ...args, + }); }, }, }; diff --git a/components/opensrs/package.json b/components/opensrs/package.json index 9e34483b3007b..85ca2961ff30e 100644 --- a/components/opensrs/package.json +++ b/components/opensrs/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/opensrs", - "version": "0.0.1", + "version": "0.1.0", "description": "Pipedream OpenSRS Components", "main": "opensrs.app.mjs", "keywords": [ @@ -11,5 +11,9 @@ "author": "Pipedream (https://pipedream.com/)", "publishConfig": { "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3", + "fast-xml-parser": "^4.3.2" } -} \ No newline at end of file +} diff --git a/components/opensrs/sources/common/events.mjs b/components/opensrs/sources/common/events.mjs new file mode 100644 index 0000000000000..0c3b8161ece03 --- /dev/null +++ b/components/opensrs/sources/common/events.mjs @@ -0,0 +1,13 @@ +export default { + CREATED: "CREATED", + REGISTERED: "REGISTERED", + RENEWED: "RENEWED", + EXPIRED: "EXPIRED", + DELETED: "DELETED", + NAMESERVER_UPDATE: "NAMESERVER_UPDATE", + REGISTRANT_VERIFICATION_STATUS_CHANGE: "REGISTRANT_VERIFICATION_STATUS_CHANGE", + ZONE_CHECK_STATUS_CHANGE: "ZONE_CHECK_STATUS_CHANGE", + MESSAGE_STATUS_CHANGE: "MESSAGE_STATUS_CHANGE", + CLAIM_STATUS_CHANGE: "CLAIM_STATUS_CHANGE", + STATUS_CHANGE: "STATUS_CHANGE", +}; diff --git a/components/opensrs/sources/common/polling.mjs b/components/opensrs/sources/common/polling.mjs new file mode 100644 index 0000000000000..610eee2759cdd --- /dev/null +++ b/components/opensrs/sources/common/polling.mjs @@ -0,0 +1,103 @@ +import { + ConfigurationError, + DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, +} from "@pipedream/platform"; +import app from "../../opensrs.app.mjs"; +import utils from "../../common/utils.mjs"; +import constants from "../../common/constants.mjs"; + +export default { + props: { + app, + timer: { + type: "$.interface.timer", + label: "Polling Schedule", + description: "How often to poll the API", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + }, + methods: { + sortFn() { + return; + }, + isResourceRelevant() { + return true; + }, + getResourceName() { + throw new ConfigurationError("getResourceName is not implemented"); + }, + getJsonBody() { + return { + data_block: { + dt_assoc: { + item: [ + ...utils.addItem("protocol", constants.PROTOCOL.XCP), + ...utils.addItem("object", constants.OBJECT_TYPE.EVENT), + ...utils.addItem("action", constants.ACTION_TYPE.POLL), + { + "@_key": "attributes", + "dt_assoc": { + item: [ + ...utils.addItem("limit", constants.DEFAULT_LIMIT), + ], + }, + }, + ], + }, + }, + }; + }, + getResourcesFnArgs() { + return { + debug: true, + jsonBody: this.getJsonBody(), + }; + }, + processResource(resource) { + const meta = this.generateMeta(resource); + this.$emit(resource, meta); + }, + }, + async run() { + const { + app, + isResourceRelevant, + getResourcesFnArgs, + processResource, + } = this; + + const { OPS_envelope: envelope } = await app.post(getResourcesFnArgs()); + + const { item: items } = envelope?.body?.data_block?.dt_assoc || {}; + const attributes = items?.find((item) => item["@_key"] === "attributes"); + + const { item: metadataItems } = attributes?.dt_assoc || {}; + const eventsMetadata = metadataItems?.find((item) => item["@_key"] === "events"); + + const { item: eventItems } = eventsMetadata?.dt_array || {}; + + const resources = eventItems.map(({ dt_assoc: { item: resourceItems } }) => { + const data = resourceItems.reduce((acc, item) => + Object.assign(acc, { + [item["@_key"]]: item.value, + }), {}); + + const objectData = resourceItems.find((item) => item["@_key"] === "object_data"); + const objectDataItems = objectData?.dt_assoc?.item.reduce((acc, item) => + Object.assign(acc, { + [item["@_key"]]: item.value, + }), {}); + return { + ...data, + objectData: objectDataItems, + }; + }); + + Array.from(resources) + .reverse() + .filter(isResourceRelevant) + .forEach(processResource); + }, +}; diff --git a/components/opensrs/sources/new-dns-zone-change/new-dns-zone-change.mjs b/components/opensrs/sources/new-dns-zone-change/new-dns-zone-change.mjs new file mode 100644 index 0000000000000..e0d21ce94ce57 --- /dev/null +++ b/components/opensrs/sources/new-dns-zone-change/new-dns-zone-change.mjs @@ -0,0 +1,29 @@ +import common from "../common/polling.mjs"; +import events from "../common/events.mjs"; +import constants from "../../common/constants.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "opensrs-new-dns-zone-change", + name: "New DNS Zone Change", + description: "Emit new event when the DNS/ZONE check has passed or failed at the registry. [See the documentation](https://domains.opensrs.guide/docs/zone_check_status_change-domain).", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + isResourceRelevant(resource) { + return resource.object === constants.OBJECT_TYPE.DOMAIN + && resource.event === events.ZONE_CHECK_STATUS_CHANGE; + }, + generateMeta(resource) { + return { + id: resource.event_id, + summary: `New DNS Zone Change: ${resource.objectData.domain_name}`, + ts: Date.parse(resource.event_date), + }; + }, + }, + sampleEmit, +}; diff --git a/components/opensrs/sources/new-dns-zone-change/test-event.mjs b/components/opensrs/sources/new-dns-zone-change/test-event.mjs new file mode 100644 index 0000000000000..8642493bd5828 --- /dev/null +++ b/components/opensrs/sources/new-dns-zone-change/test-event.mjs @@ -0,0 +1,11 @@ +export default { + object: "DOMAIN", + event_id: "bf7b2cf6d8e835324241a4eae7be4ee5", + event: "ZONE_CHECK_STATUS_CHANGE", + event_date: "2014-05-27T19:29:25Z", + objectData: { + domain_name: "abc-fictional.de", + domain_id: "801743", + zone_check_status: "invalid", + }, +}; diff --git a/components/opensrs/sources/new-domain-registration/new-domain-registration.mjs b/components/opensrs/sources/new-domain-registration/new-domain-registration.mjs new file mode 100644 index 0000000000000..46534711eb24f --- /dev/null +++ b/components/opensrs/sources/new-domain-registration/new-domain-registration.mjs @@ -0,0 +1,29 @@ +import common from "../common/polling.mjs"; +import events from "../common/events.mjs"; +import constants from "../../common/constants.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "opensrs-new-domain-registration", + name: "New Domain Registration", + description: "Emit new event for each new domain registration. [See the documentation](https://domains.opensrs.guide/docs/registered-domain).", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + isResourceRelevant(resource) { + return resource.object === constants.OBJECT_TYPE.DOMAIN + && resource.event === events.REGISTERED; + }, + generateMeta(resource) { + return { + id: resource.event_id, + summary: `New domain: ${resource.objectData.domain_name}`, + ts: Date.parse(resource.event_date), + }; + }, + }, + sampleEmit, +}; diff --git a/components/opensrs/sources/new-domain-registration/test-event.mjs b/components/opensrs/sources/new-domain-registration/test-event.mjs new file mode 100644 index 0000000000000..d952995d8c5d4 --- /dev/null +++ b/components/opensrs/sources/new-domain-registration/test-event.mjs @@ -0,0 +1,12 @@ +export default { + object: "DOMAIN", + event_id: "6887c7d3838dcaec7517c531df527bb7", + event: "REGISTERED", + event_date: "2014-05-27T19:29:25Z", + objectData: { + domain_name: "abc-fictional.de", + period: "1", + domain_id: "801743", + expiration_date: "2015-05-27T19:29:25Z", + }, +}; diff --git a/components/opensrs/sources/new-transfer-status/new-transfer-status.mjs b/components/opensrs/sources/new-transfer-status/new-transfer-status.mjs new file mode 100644 index 0000000000000..b0486da5573ed --- /dev/null +++ b/components/opensrs/sources/new-transfer-status/new-transfer-status.mjs @@ -0,0 +1,29 @@ +import common from "../common/polling.mjs"; +import events from "../common/events.mjs"; +import constants from "../../common/constants.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "opensrs-new-transfer-status", + name: "New Transfer Status", + description: "Emit new event when the status of a domain transfer changes. [See the documentation](https://domains.opensrs.guide/docs/status_change-transfer).", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + isResourceRelevant(resource) { + return resource.object === constants.OBJECT_TYPE.TRANSFER + && resource.event === events.STATUS_CHANGE; + }, + generateMeta(resource) { + return { + id: resource.event_id, + summary: `New Transfer Status: ${resource.objectData.domain_name}`, + ts: Date.parse(resource.event_date), + }; + }, + }, + sampleEmit, +}; diff --git a/components/opensrs/sources/new-transfer-status/test-event.mjs b/components/opensrs/sources/new-transfer-status/test-event.mjs new file mode 100644 index 0000000000000..f0aa4961e3ce4 --- /dev/null +++ b/components/opensrs/sources/new-transfer-status/test-event.mjs @@ -0,0 +1,12 @@ +export default { + object: "TRANSFER", + event_id: "21bd704aba7804b839f87aee292fcf72", + event: "STATUS_CHANGE", + event_date: "2014-05-27T19:29:25Z", + objectData: { + domain_name: "abc-fictional.de", + transfer_id: "95094", + order_id: "701988", + transfer_status: "pending_owner", + }, +}; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 36bc979418a49..bb090872e109e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -7453,7 +7453,14 @@ importers: specifier: ^2.3.0 version: 2.3.0 - components/opensrs: {} + components/opensrs: + dependencies: + '@pipedream/platform': + specifier: ^3.0.3 + version: 3.0.3 + fast-xml-parser: + specifier: ^4.3.2 + version: 4.5.0 components/openweather_api: dependencies: @@ -32165,8 +32172,6 @@ snapshots: '@putout/operator-filesystem': 5.0.0(putout@36.13.1(eslint@8.57.1)(typescript@5.6.3)) '@putout/operator-json': 2.2.0 putout: 36.13.1(eslint@8.57.1)(typescript@5.6.3) - transitivePeerDependencies: - - supports-color '@putout/operator-regexp@1.0.0(putout@36.13.1(eslint@8.57.1)(typescript@5.6.3))': dependencies: