// porkbun-client // Copyright (C) 2025 Oxtaly // // This program is free software: you can redistribute it and/or modify // // it under the terms of the GNU General Public License as published by // // the Free Software Foundation, either version 3 of the License, or // // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // // but WITHOUT ANY WARRANTY; without even the implied warranty of // // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // // along with this program. If not, see . "use strict"; /** * @typedef {import('./types.d.ts').PorkbunAPIDNSRecordTypes} PorkbunAPIDNSRecordTypes * @typedef {import('./types.d.ts').PorkbunAPIRecordIDType} PorkbunAPIRecordIDType * @typedef {import('./types.d.ts').PorkbunAPIResponses} PorkbunAPIResponses * @typedef {import('./types.d.ts').PorkbunClientOptions} PorkbunClientOptions * * @typedef {import('./types.d.ts').PorkbunAPIDNSRecord} PorkbunAPIDNSRecord * @typedef {import('./types.d.ts').PorkbunAPIStatuses} PorkbunAPIStatuses */ /** * @template SuccessData * @typedef {import('./types.d.ts').PorkbunAPIResponse} PorkbunAPIResponse */ /** Error thrown when the API does not respond as expected (eg: content-type is not set to application/json). */ class ResponseError extends Error { /** @type {{ url: string, body: Object }} */ context = null; /** @type {Response} */ response = null; } /** Error thrown when the API responds with a status of "ERROR". */ class APIError extends Error { /** @type {{ url: string, body: Object }} */ context = null; /** @type {Object} */ apiResponse = null; } /** * @param {string} url * @returns */ function isValidURL(url) { try { new URL(url); return true; } catch (e) { return false; } } class PorkbunClient { /** @private @readonly Internal version tracker. Incremented on behavior changes (including additions and deletions). */ static get version() { return "7"; } /** @private @readonly @type {string} */ _endpoint = `https://api.porkbun.com/api/json/v3`; /** @private @readonly @type {string} */ _secretKey = null; /** @private @readonly @type {string} */ _apiKey = null; /** @private @readonly @type {PorkbunClientOptions['queryLogger']} */ _queryLogger = null; /** @private @readonly @type {string} */ _userAgent = "porkbun-wrapper"; /** * @param {PorkbunClientOptions} options */ constructor(options) { if(options === undefined) throw new TypeError('Missing options parameter!'); if(typeof options !== 'object') throw new TypeError(`Invalid options parameter type! Expected 'object', received '${typeof options}'!`); if(options.apiKey === undefined) throw new TypeError('Missing options.apiKey parameter!'); if(typeof options.apiKey !== 'string') throw new TypeError(`Invalid options.apiKey parameter type! Expected 'string', received '${typeof options.apiKey}'!`); this._apiKey = options.apiKey; if(options.secretKey === undefined) throw new TypeError('Missing options.secretKey parameter!'); if(typeof options.secretKey !== 'string') throw new TypeError(`Invalid options.secretKey parameter type! Expected 'string', received '${typeof options.secretKey}'!`); this._secretKey = options.secretKey; if(options.userAgent !== undefined && options.userAgent !== null && typeof options.userAgent !== 'string') throw new TypeError(`Invalid options.userAgent parameter type! Expected 'null' | 'string', received '${options.userAgent}'!`); if(options.userAgent !== undefined) this._userAgent = options.userAgent; if(options.endpoint && typeof options.endpoint !== 'string') throw new TypeError(`Invalid options.endpoint parameter type! Expected 'string', received '${typeof options.endpoint}'!`); if(options.endpoint && !isValidURL(options.endpoint)) throw new TypeError(`Invalid options.endpoint parameter value! Expected a valid URL, received '${options.endpoint}'!`); if(options.endpoint) this._endpoint = options.endpoint; if(this._endpoint.endsWith('/')) this._endpoint = this._endpoint.slice(0, -1); if(options.queryLogger && typeof options.queryLogger !== 'function') throw new TypeError(`Invalid options.logger parameter type! Expected 'function (query) => void', received '${typeof options.queryLogger}'!`); if(options.queryLogger) this._queryLogger = options.queryLogger; } /** * Used internally to get the full endpoint URL for a given endpoint. * * @private * @param {string} endpoint */ _getEndpoint(endpoint) { return this._endpoint + (endpoint.startsWith('/') ? endpoint : `/${endpoint}`); } /** * Used internally as a shortcut for requesting a post endpoint with the set credentials, along with handling the response. * * @private * @param {Object} options * @param {string} options.url * @param {any} [options.body] */ _request(options) { return new Promise((resolve, reject) => { this._postRequest(options.url, options.body) .then((response) => this._responseHandler(resolve, reject, response, { url: options.url, body: options.body ?? {} })) .catch((error) => reject(error)); }); } /** * Used internally to make a post request to a given url with the set credentials. * * @private * @param {string} url * @param {Object} body */ _postRequest(url, body) { if(!body) body = {}; if(this._queryLogger && typeof this._queryLogger === 'function') { if(body.secretapikey) delete body.secretapikey; if(body.apikey) delete body.apikey; this._queryLogger({ url, body }); } body.secretapikey = this._secretKey; body.apikey = this._apiKey; const headers = { 'accept': 'application/json', 'content-type': 'application/json' } if(this._userAgent) headers['User-Agent'] = this._userAgent; return fetch(url, { headers, body: JSON.stringify(body), method: "POST" }); } /** * Used internally to handle/check API responses before returning them. * * @private * @param {(reason?: any) => void} reject * @param {(value?: any) => void} resolve * @param {Response} response * @param {{ url: string, body: Object }} context */ async _responseHandler(resolve, reject, response, context) { if(response.headers.get('content-type') !== 'application/json') { const error = new ResponseError(`Invalid content type! Expected 'application/json', received '${response.headers.get('content-type')}'!`); error.response = response; error.context = context; reject(error); return; } const data = await response.text(); try { const json = JSON.parse(data); if(!json.status) { const error = new ResponseError(`Missing response data.status! Expected 'SUCCESS' or 'ERROR', received '${json.status}'!`); error.response = response; error.context = context; return reject(error); } if(json.status !== 'SUCCESS' && json.status !== 'ERROR') { const error = new ResponseError(`Invalid response data.status! Expected 'SUCCESS' or 'ERROR', received '${json.status}'!`); error.response = response; error.context = context; return reject(error); } if(json.status === "ERROR") { const message = json.message || "The API responded with an error but did not provide a message!"; const error = new APIError(message); error.apiResponse = json; error.context = context; return reject(error); } return resolve(json); } catch (caughtError) { const error = new ResponseError(`An error happened parsing response JSON!`); error.cause = caughtError; error.response = response; error.context = context; return reject(error); } } /** * Check default domain pricing information for all supported TLDs. This endpoint does not require authentication (keys will not be sent). * * @documentation {@link https://porkbun.com/api/json/v3/documentation#Domain%20Pricing} * @returns {Promise} */ getPricing() { return new Promise((resolve, reject) => { const url = this._getEndpoint(`/pricing/get`); if(this._queryLogger) this._queryLogger({ url, body: {} }); const headers = { 'accept': 'application/json', } if(this._userAgent) headers['User-Agent'] = this._userAgent; fetch(url, { headers, method: "GET" }) .then((response) => this._responseHandler(resolve, reject, response, { url, body: {} })) .catch((error) => reject(error)); }); } /** * Pings the porkbun api with your keys to test authentication. * * @documentation {@link https://porkbun.com/api/json/v3/documentation#Authentication} * @returns {Promise} */ ping() { return this._request({ url: this._getEndpoint(`/ping`) }); } /** * Get the name servers for the given domain. * * @documentation {@link https://porkbun.com/api/json/v3/documentation#Domain%20Get%20Name%20Servers} * @param {string} domain * @returns {Promise} */ getNameServers(domain) { if(!domain) return Promise.reject(new TypeError('Missing domain parameter!')); if(typeof domain !== 'string') return Promise.reject(new TypeError(`Invalid domain parameter type! Expected 'string', received '${typeof domain}'!`)); return this._request({ url: this._getEndpoint(`/domain/getNs/${domain}`) }); } /** * Update the name servers for the given domain. * * @documentation {@link https://porkbun.com/api/json/v3/documentation#Domain%20Update%20Name%20Servers} * @param {string} domain * @param {string[]} nameservers * @returns {Promise} */ updateNameServers(domain, nameservers) { if(!domain) return Promise.reject(new TypeError('Missing domain parameter!')); if(typeof domain !== 'string') return Promise.reject(new TypeError(`Invalid domain parameter type! Expected 'string', received '${typeof domain}'!`)); if(!nameservers) return Promise.reject(new TypeError('Missing nameservers parameter!')); if(!Array.isArray(nameservers)) ///@ts-expect-error - constructor.name is not typed for Object but available on all objects return Promise.reject(new TypeError(`Invalid nameservers parameter type! Expected 'array', received '${typeof nameservers === 'object' ? (nameservers?.constructor?.name ?? 'object') : typeof nameservers}'!`)); const wrongTypedEntries = nameservers.map((nameserver, i) => [nameserver, i]).filter(([nameserver]) => typeof nameserver !== 'string') if(wrongTypedEntries.length) return Promise.reject(new TypeError(`Invalid nameservers parameter entries type! Expected 'string', received [${wrongTypedEntries.map(([nameserver, i]) => `${i}: '${typeof nameserver}'`).join(', ')}]!`)); return this._request({ url: this._getEndpoint(`/domain/updateNs/${domain}`), body: { ns: nameservers } }); } /** * Get all domain names for the logged in account. Domains are returned in chunks of 1000. * * @documentation {@link https://porkbun.com/api/json/v3/documentation#Domain%20List%20All} * @param {Object} [options] * @param {number | `${number}`} [options.start] - An index to start at when retrieving the domains, defaults to 0. To get all domains increment by 1000 until you receive an empty array. * @param {boolean} [options.includeLabels] - If set to true, the request will return label information for the domains if it exists. * @returns {Promise} */ getDomains(options) { if(options !== null && options !== undefined && typeof options !== 'object') return Promise.reject(new TypeError(`Invalid options parameter type! Expected 'object', received '${typeof options}'!`)); if(options && options.start !== null && options.start !== undefined && typeof options.start !== 'number' && typeof options.start !== 'string') return Promise.reject(new TypeError(`Invalid options.start parameter type! Expected 'number', received '${typeof options.start}'!`)); if(options && options.start !== null && options.start !== undefined && !isNaN(options.start)) return Promise.reject(new TypeError(`Invalid options.start parameter value! Expected a valid number, received '${options.start}'!`)); if(options && options.includeLabels !== null && options.includeLabels !== undefined && typeof options.includeLabels !== 'boolean') return Promise.reject(new TypeError(`Invalid options.includeLabels parameter type! Expected 'boolean', received '${typeof options.includeLabels}'!`)); const requestBody = {}; if(options && !isNaN(options.start)) requestBody.start = parseInt(options.start).toString(); if(options && options.includeLabels === true) requestBody.includeLabels = 'yes'; return this._request({ url: this._getEndpoint(`/domain/listAll`), body: requestBody }); } /** * Get URL forwarding for the given domain. * * @documentation {@link https://porkbun.com/api/json/v3/documentation#Domain%20Get%20URL%20Forwarding} * @param {string} domain * @returns {Promise} */ getURLForwardings(domain) { if(!domain) return Promise.reject(new TypeError('Missing domain parameter!')); if(typeof domain !== 'string') return Promise.reject(new TypeError(`Invalid domain parameter type! Expected 'string', received '${typeof domain}'!`)); return this._request({ url: this._getEndpoint(`/domain/getUrlForwarding/${domain}`) }); } /** * Add an URL forward for the given domain. * * @documentation {@link https://porkbun.com/api/json/v3/documentation#Domain%20Add%20URL%20Forward} * @param {string} domain * @param {Object} forwardData * @param {string | null | undefined} [forwardData.subdomain] - A subdomain that you would like to add URL forwarding for. Leave unset, set blank, to undefined or to null for the root domain. * @param {string} forwardData.location - Where you'd like to forward the domain to. * @param {"temporary"|"permanent"} forwardData.type - The type of forward. Valid types are: temporary or permanent * @param {boolean} forwardData.includePath - Whether or not to include the URI path in the redirection. * @param {boolean} forwardData.wildcard - Wether or not to forward all subdomains of the domain. * @returns {Promise} */ addURLForward(domain, forwardData) { if(!domain) return Promise.reject(new TypeError('Missing domain parameter!')); if(typeof domain !== 'string') return Promise.reject(new TypeError(`Invalid domain parameter type! Expected 'string', received '${typeof domain}'!`)); if(!forwardData) return Promise.reject(new TypeError('Missing forwardData parameter!')); if(typeof forwardData !== 'object') return Promise.reject(new TypeError(`Invalid forwardData parameter type! Expected 'object', received '${typeof domain}'!`)); if(forwardData.subdomain !== null && forwardData.subdomain !== undefined && typeof forwardData.subdomain !== 'string') return Promise.reject(new TypeError(`Invalid forwardData.subdomain parameter type! Expected 'string', received '${typeof forwardData.subdomain}'!`)); if(!forwardData.location) return Promise.reject(new TypeError('Missing forwardData.location parameter!')); if(typeof forwardData.location !== 'string') return Promise.reject(new TypeError(`Invalid forwardData.location parameter type! Expected 'string', received '${typeof forwardData.location}'!`)); if(!forwardData.type) return Promise.reject(new TypeError('Missing forwardData.type parameter!')); if(typeof forwardData.type !== 'string') return Promise.reject(new TypeError(`Invalid forwardData.type parameter type! Expected 'permanent' | 'temporary', received '${typeof forwardData.type}'!`)); if(forwardData.type !== 'permanent' && forwardData.type !== 'temporary') return Promise.reject(new TypeError(`Invalid forwardData.type parameter type! Expected 'permanent' | 'temporary', received '${forwardData.type}'!`)); if(!forwardData.includePath && forwardData.includePath !== false) return Promise.reject(new TypeError('Missing forwardData.includePath parameter!')); if(typeof forwardData.includePath !== 'boolean') return Promise.reject(new TypeError(`Invalid forwardData.includePath parameter type! Expected 'boolean', received '${typeof forwardData.includePath}'!`)); if(!forwardData.wildcard && forwardData.wildcard !== false) return Promise.reject(new TypeError('Missing forwardData.wildcard parameter!')); if(typeof forwardData.wildcard !== 'boolean') return Promise.reject(new TypeError(`Invalid forwardData.wildcard parameter type! Expected 'boolean', received '${typeof forwardData.wildcard}'!`)); return this._request({ url: this._getEndpoint(`/domain/addUrlForward/${domain}`), body: { subdomain: forwardData.subdomain || '', location: forwardData.location, type: forwardData.type, includePath: forwardData.includePath ? 'yes' : 'no', wildcard: forwardData.wildcard ? 'yes' : 'no', } }); } /** * Delete a URL forward for a domain. * * @documentation {@link https://porkbun.com/api/json/v3/documentation#Domain%20Delete%20URL%20Forward} * @param {string} domain * @param {PorkbunAPIRecordIDType} recordID * @returns {Promise} */ deleteURLForward(domain, recordID) { if(!domain) return Promise.reject(new TypeError('Missing domain parameter!')); if(typeof domain !== 'string') return Promise.reject(new TypeError(`Invalid domain parameter type! Expected 'string', received '${typeof domain}'!`)); if(!recordID) return Promise.reject(new TypeError('Missing recordID parameter!')); if(typeof recordID !== 'string') return Promise.reject(new TypeError(`Invalid recordID parameter type! Expected 'string', received '${typeof recordID}'!`)); return this._request({ url: this._getEndpoint(`/domain/deleteUrlForward/${domain}/${recordID}`) }); } /** * Check a domain's availability. Please note that domain checks are rate limited and you will be notified of your limit when you cross it. * Rate limit is also supplied within the success response in the form of the "limits" property. * * @documentation {@link https://porkbun.com/api/json/v3/documentation#Domain%20Check} * @param {string} domain * @returns {Promise} */ checkDomain(domain) { if(!domain) return Promise.reject(new TypeError('Missing domain parameter!')); if(typeof domain !== 'string') return Promise.reject(new TypeError(`Invalid domain parameter type! Expected 'string', received '${typeof domain}'!`)); return this._request({ url: this._getEndpoint(`/domain/checkDomain/${domain}`) }); } /** * Gets existing glue records for a domain. * * @documentation {@link https://porkbun.com/api/json/v3/documentation#Domain%20Get%20Glue%20Records} * @param {string} domain * @returns {Promise} */ getGlueRecords(domain) { if(!domain) return Promise.reject(new TypeError('Missing domain parameter!')); if(typeof domain !== 'string') return Promise.reject(new TypeError(`Invalid domain parameter type! Expected 'string', received '${typeof domain}'!`)); return this._request({ url: this._getEndpoint(`/domain/getGlue/${domain}`) }); } /** * Create a glue record for a domain. * * @documentation {@link https://porkbun.com/api/json/v3/documentation#Domain%20Create%20Glue%20Record} * @param {string} domain * @param {string} glueHostSubdomain - THe subdomain that will be used for the glue record. (eg. 'ns1' for 'ns1.example.com') * @param {string[]} ips - An array of IP addresses to associate with the glue record. Accepts both IPv4 and IPv6 addresses. * @returns {Promise} */ createGlueRecord(domain, glueHostSubdomain, ips) { if(!domain) return Promise.reject(new TypeError('Missing domain parameter!')); if(typeof domain !== 'string') return Promise.reject(new TypeError(`Invalid domain parameter type! Expected 'string', received '${typeof domain}'!`)); if(!glueHostSubdomain) return Promise.reject(new TypeError('Missing glueHostSubdomain parameter!')); if(typeof glueHostSubdomain !== 'string') return Promise.reject(new TypeError(`Invalid glueHostSubdomain parameter type! Expected 'string', received '${typeof glueHostSubdomain}'!`)); if(!ips) return Promise.reject(new TypeError('Missing ips parameter!')); if(!Array.isArray(ips)) ///@ts-expect-error - constructor.name is not typed for Object but available on all objects return Promise.reject(new TypeError(`Invalid ips parameter type! Expected 'array', received '${typeof ips === 'object' ? (ips?.constructor?.name ?? 'object') : typeof ips}'!`)); const wrongTypedEntries = ips.map((ip, i) => [ip, i]).filter(([ip]) => typeof ip !== 'string') if(wrongTypedEntries.length) return Promise.reject(new TypeError(`Invalid ips parameter entries type! Expected 'string', received [${wrongTypedEntries.map(([ip, i]) => `${i}: '${typeof ip}'`).join(', ')}]!`)); return this._request({ url: this._getEndpoint(`/domain/createGlue/${domain}/${glueHostSubdomain}`), body: { ips } }); } /** * Update a glue record for a domain. * * @documentation {@link https://porkbun.com/api/json/v3/documentation#Domain%20Update%20Glue%20Record} * @param {string} domain * @param {string} glueHostSubdomain - THe subdomain used for the glue record. (eg. 'ns1' for 'ns1.example.com') * @param {string[]} ips - An array of IP addresses to associate with the glue record. Accepts both IPv4 and IPv6 addresses. Will replace existing glue record ip addresses for the subdomain. * @returns {Promise} */ updateGlueRecord(domain, glueHostSubdomain, ips) { if(!domain) return Promise.reject(new TypeError('Missing domain parameter!')); if(typeof domain !== 'string') return Promise.reject(new TypeError(`Invalid domain parameter type! Expected 'string', received '${typeof domain}'!`)); if(!glueHostSubdomain) return Promise.reject(new TypeError('Missing glueHostSubdomain parameter!')); if(typeof glueHostSubdomain !== 'string') return Promise.reject(new TypeError(`Invalid glueHostSubdomain parameter type! Expected 'string', received '${typeof glueHostSubdomain}'!`)); if(!ips) return Promise.reject(new TypeError('Missing ips parameter!')); if(!Array.isArray(ips)) ///@ts-expect-error - constructor.name is not typed for Object but available on all objects return Promise.reject(new TypeError(`Invalid ips parameter type! Expected 'array', received '${typeof ips === 'object' ? (ips?.constructor?.name ?? 'object') : typeof ips}'!`)); const wrongTypedEntries = ips.map((ip, i) => [ip, i]).filter(([ip]) => typeof ip !== 'string') if(wrongTypedEntries.length) return Promise.reject(new TypeError(`Invalid ips parameter entries type! Expected 'string', received [${wrongTypedEntries.map(([ip, i]) => `${i}: '${typeof ip}'`).join(', ')}]!`)); return this._request({ url: this._getEndpoint(`/domain/updateGlue/${domain}/${glueHostSubdomain}`), body: { ips } }); } /** * Delete a glue record for a domain. * * @documentation {@link https://porkbun.com/api/json/v3/documentation#Domain%20Delete%20Glue%20Record} * @param {string} domain * @param {string} glueHostSubdomain - THe subdomain used for the glue record. (eg. 'ns1' for 'ns1.example.com') * @returns {Promise} */ deleteGlueRecord(domain, glueHostSubdomain) { if(!domain) return Promise.reject(new TypeError('Missing domain parameter!')); if(typeof domain !== 'string') return Promise.reject(new TypeError(`Invalid domain parameter type! Expected 'string', received '${typeof domain}'!`)); if(!glueHostSubdomain) return Promise.reject(new TypeError('Missing glueHostSubdomain parameter!')); if(typeof glueHostSubdomain !== 'string') return Promise.reject(new TypeError(`Invalid glueHostSubdomain parameter type! Expected 'string', received '${typeof glueHostSubdomain}'!`)); return this._request({ url: this._getEndpoint(`/domain/deleteGlue/${domain}/${glueHostSubdomain}`) }); } /** * Get all DNS records for a domain, optionally filtered by type and subdomain. * * documentation: {@link https://porkbun.com/api/json/v3/documentation#DNS%20Retrieve%20Records%20by%20Domain%20or%20ID} * * documentation: {@link https://porkbun.com/api/json/v3/documentation#DNS%20Retrieve%20Records%20by%20Domain,%20Subdomain%20and%20Type} * * @overload * Get all DNS records of a certain type and subdomain. * @param {string} domain * @param {PorkbunAPIDNSRecordTypes} recordType * @param {string | undefined | null} [subdomain] - Leave blank, unset, set to undefined or to null for the root domain. * @returns {Promise} * @overload * Get all DNS records for a domain. * @param {string} domain * @returns {Promise} */ getDNSRecords(domain, recordType, subdomain) { if(!domain) return Promise.reject(new TypeError('Missing domain parameter!')); if(typeof domain !== 'string') return Promise.reject(new TypeError(`Invalid domain parameter type! Expected 'string', received '${typeof domain}'!`)); if(recordType !== null && recordType !== undefined && typeof recordType !== 'string') return Promise.reject(new TypeError(`Invalid recordType parameter type! Expected 'string', received '${typeof recordType}'!`)); if(subdomain !== null && subdomain !== undefined && !recordType) return Promise.reject(new TypeError('Missing recordType parameter!')); if(subdomain !== null && subdomain !== undefined && typeof subdomain !== 'string') return Promise.reject(new TypeError(`Invalid subdomain parameter type! Expected 'string', received '${typeof subdomain}'!`)); let apiURL = this._getEndpoint(`/dns/retrieve/${domain}`); if(recordType && subdomain) apiURL = this._getEndpoint(`/dns/retrieveByNameType/${domain}/${recordType}/${subdomain}`); else if(recordType) apiURL = this._getEndpoint(`/dns/retrieveByNameType/${domain}/${recordType}`); return this._request({ url: apiURL }); } /** * Get a DNS record by it's ID. * * @documentation {@link https://porkbun.com/api/json/v3/documentation#DNS%20Retrieve%20Records%20by%20Domain%20or%20ID} * @param {string} domain * @param {string} recordID * @returns {Promise} */ getDNSRecord(domain, recordID) { if(!domain) return Promise.reject(new TypeError('Missing domain parameter!')); if(typeof domain !== 'string') return Promise.reject(new TypeError(`Invalid domain parameter type! Expected 'string', received '${typeof domain}'!`)); if(!recordID) return Promise.reject(new TypeError('Missing recordID parameter!')); if(typeof recordID !== 'string' && typeof recordID !== 'number') return Promise.reject(new TypeError(`Invalid recordID parameter type! Expected 'string'|'number', received '${typeof recordID}'!`)); return this._request({ url: this._getEndpoint(`/dns/retrieve/${domain}/${recordID}`) }); } /** * Create a DNS record for the given domain. * * @documentation {@link https://porkbun.com/api/json/v3/documentation#DNS%20Create%20Record} * @param {string} domain * @param {Object} recordData * @param {string|null} recordData.name - The subdomain for the record being created, not including the domain itself. Set as null to create a record on the root domain. Use * to create a wildcard record. * @param {PorkbunAPIDNSRecordTypes} recordData.type - The type of record being created. Valid types are: A, MX, CNAME, ALIAS, TXT, NS, AAAA, SRV, TLSA, CAA, HTTPS, SVCB * @param {string} recordData.content - The answer content for the record. Please see the DNS management popup from Porkbun's domain management console for proper formatting of each record type. * @param {number} [recordData.ttl] - The time to live in seconds for the record. The minimum and the default is 600 seconds. * @param {number} [recordData.priority] - The priority of the record for those that support it. * @returns {Promise} */ createDNSRecord(domain, recordData) { if(!domain) return Promise.reject(new TypeError('Missing domain parameter!')); if(typeof domain !== 'string') return Promise.reject(new TypeError(`Invalid domain parameter type! Expected 'string', received '${typeof domain}'!`)); if(!recordData) return Promise.reject(new TypeError('Missing recordData parameter!')); if(typeof recordData !== 'object') return Promise.reject(new TypeError(`Invalid recordData parameter type! Expected 'object', received '${typeof recordData}'!`)); //* Required properties enforcement & type checker if(!recordData.content) return Promise.reject(new TypeError('Missing recordData.content parameter!')); if(typeof recordData.content !== 'string') return Promise.reject(new TypeError(`Invalid recordData.content parameter type! Expected 'string', received '${typeof recordData.content}'!`)); if(!recordData.type) return Promise.reject(new TypeError('Missing recordData.type parameter!')); if(typeof recordData.type !== 'string') return Promise.reject(new TypeError(`Invalid recordData.type parameter type! Expected 'string', received '${typeof recordData.type}'!`)); //* Optional properties type checker if(recordData.name !== null && recordData.name !== undefined && typeof recordData.name !== 'string') return Promise.reject(new TypeError(`Invalid recordData.name parameter type! Expected 'string', received '${typeof recordData.name}'!`)); if(recordData.ttl !== null && recordData.ttl !== undefined && isNaN(recordData.ttl)) return Promise.reject(new TypeError(`Invalid recordData.ttl parameter type! Expected 'number', received '${typeof recordData.ttl}'!`)); if(recordData.priority !== null && recordData.priority !== undefined && isNaN(recordData.priority)) return Promise.reject(new TypeError(`Invalid recordData.priority parameter type! Expected 'number', received '${typeof recordData.priority}'!`)); const requestBody = { name: recordData.name, type: recordData.type, content: recordData.content }; if(recordData.priority !== null && recordData.priority !== undefined) requestBody.prio = parseInt(recordData.priority); if(recordData.ttl !== null && recordData.ttl !== undefined) requestBody.ttl = parseInt(recordData.ttl); return this._request({ url: this._getEndpoint(`/dns/create/${domain}`), body: requestBody }); } /** * Edit a DNS record by it's ID. * * Note: Although the API documentation states that the name field is optional, it is not possible to edit a record without providing one. * If you want to keep the name unchanged, you must provide the current name of the record. * * @documentation {@link https://porkbun.com/api/json/v3/documentation#DNS%20Edit%20Record%20by%20Domain%20and%20ID} * @param {string} domain * @param {string} recordID * @param {Object} recordData * @param {string|null} recordData.name - The subdomain for the record being edited, not including the domain itself. Set as null to create a record on the root domain. Use * to create a wildcard record. * @param {PorkbunAPIDNSRecordTypes} recordData.type - The type of record being created. Valid types are: A, MX, CNAME, ALIAS, TXT, NS, AAAA, SRV, TLSA, CAA, HTTPS, SVCB * @param {string} recordData.content - The answer content for the record. Please see the DNS management popup from Porkbun's domain management console for proper formatting of each record type. * @param {number} [recordData.ttl] - The time to live in seconds for the record. The minimum and the default is 600 seconds. * @param {number} [recordData.priority] - The priority of the record for those that support it. * @returns {Promise} */ editDNSRecord(domain, recordID, recordData) { if(!domain) return Promise.reject(new TypeError('Missing domain parameter!')); if(typeof domain !== 'string') return Promise.reject(new TypeError(`Invalid domain parameter type! Expected 'string', received '${typeof domain}'!`)); if(!recordID) return Promise.reject(new TypeError('Missing recordID parameter!')); if(typeof recordID !== 'string' && typeof recordID !== 'number') return Promise.reject(new TypeError(`Invalid recordID parameter type! Expected 'string'|'number', received '${typeof recordID}'!`)); if(!recordData) return Promise.reject(new TypeError('Missing recordData parameter!')); if(typeof recordData !== 'object') return Promise.reject(new TypeError(`Invalid recordData parameter type! Expected 'object', received '${typeof recordData}'!`)); if(recordData.content !== null && recordData.content !== undefined && typeof recordData.content !== 'string') return Promise.reject(new TypeError(`Invalid recordData.content parameter type! Expected 'string', received '${typeof recordData.content}'!`)); if(recordData.type !== null && recordData.type !== undefined && typeof recordData.type !== 'string') return Promise.reject(new TypeError(`Invalid recordData.type parameter type! Expected 'string', received '${typeof recordData.type}'!`)); if(recordData.name !== null && recordData.name !== undefined && typeof recordData.name !== 'string') return Promise.reject(new TypeError(`Invalid recordData.name parameter type! Expected 'string', received '${typeof recordData.name}'!`)); if(recordData.ttl !== null && recordData.ttl !== undefined && isNaN(recordData.ttl)) return Promise.reject(new TypeError(`Invalid recordData.ttl parameter type! Expected 'number', received '${typeof recordData.ttl}'!`)); if(recordData.priority !== null && recordData.priority !== undefined && isNaN(recordData.priority)) return Promise.reject(new TypeError(`Invalid recordData.priority parameter type! Expected 'number', received '${typeof recordData.priority}'!`)); const requestBody = {}; if(recordData.name !== null && recordData.name !== undefined) requestBody.name = recordData.name; if(recordData.type !== null && recordData.type !== undefined) requestBody.type = recordData.type; if(recordData.content !== null && recordData.content !== undefined) requestBody.content = recordData.content; if(recordData.priority !== null && recordData.priority !== undefined) requestBody.prio = parseInt(recordData.priority); if(recordData.ttl !== null && recordData.ttl !== undefined) requestBody.ttl = parseInt(recordData.ttl); return this._request({ url: this._getEndpoint(`/dns/edit/${domain}/${recordID}`), body: requestBody }); } /** * Edit all records for the domain that match a particular type and subdomain. * * @documentation {@link https://porkbun.com/api/json/v3/documentation#DNS%20Edit%20Record%20by%20Domain,%20Subdomain%20and%20Type} * @param {string} domain * @param {PorkbunAPIDNSRecordTypes} recordType * @param {string | null | undefined} subdomain - Leave blank, set to undefined or to null for the root domain. * @param {Object} recordData * @param {PorkbunAPIDNSRecordTypes} [recordData.type] - The type of record being created. Valid types are: A, MX, CNAME, ALIAS, TXT, NS, AAAA, SRV, TLSA, CAA, HTTPS, SVCB * @param {string} recordData.content - The answer content for the record. Please see the DNS management popup from Porkbun's domain management console for proper formatting of each record type. * @param {number | `${number}`} [recordData.ttl] - The time to live in seconds for the record. The minimum and the default is 600 seconds. * @param {number | `${number}`} [recordData.priority] - The priority of the record for those that support it. * @returns {Promise} */ editDNSRecords(domain, recordType, subdomain, recordData) { if(!domain) return Promise.reject(new TypeError('Missing domain parameter!')); if(typeof domain !== 'string') return Promise.reject(new TypeError(`Invalid domain parameter type! Expected 'string', received '${typeof domain}'!`)); if(!recordType) return Promise.reject(new TypeError('Missing recordType parameter!')); if(typeof recordType !== 'string') return Promise.reject(new TypeError(`Invalid recordType parameter type! Expected 'string', received '${typeof recordType}'!`)); if(subdomain !== undefined && subdomain !== null && typeof subdomain !== 'string') return Promise.reject(new TypeError(`Invalid subdomain parameter type! Expected 'string', received '${typeof subdomain}'!`)); if(!recordData) return Promise.reject(new TypeError('Missing recordData parameter!')); if(typeof recordData !== 'object') return Promise.reject(new TypeError(`Invalid recordData parameter type! Expected 'object', received '${typeof recordData}'!`)); // if(!recordData.content) // return Promise.reject(new TypeError('Missing recordData.content parameter!')); if(recordData.content !== null && recordData.content !== undefined && typeof recordData.content !== 'string') return Promise.reject(new TypeError(`Invalid recordData.content parameter type! Expected 'string', received '${typeof recordData.content}'!`)); if(recordData.type !== null && recordData.type !== undefined && typeof recordData.type !== 'string') return Promise.reject(new TypeError(`Invalid recordData.type parameter type! Expected 'string', received '${typeof recordData.type}'!`)); if(recordData.ttl !== null && recordData.ttl !== undefined && isNaN(recordData.ttl)) return Promise.reject(new TypeError(`Invalid recordData.ttl parameter type! Expected 'number' | '\`\${number}\`', received '${typeof recordData.ttl}'!`)); if(recordData.priority !== null && recordData.priority !== undefined && isNaN(recordData.priority)) return Promise.reject(new TypeError(`Invalid recordData.priority parameter type! Expected 'number' | '\`\${number}\`', received '${typeof recordData.priority}'!`)); const requestBody = {}; if(recordData.type !== null && recordData.type !== undefined) requestBody.type = recordData.type; if(recordData.content !== null && recordData.content !== undefined) requestBody.content = recordData.content; if(recordData.priority !== null && recordData.priority !== undefined) requestBody.prio = parseInt(recordData.priority).toString(); if(recordData.ttl !== null && recordData.ttl !== undefined) requestBody.ttl = parseInt(recordData.ttl).toString(); let apiURL = this._getEndpoint(`/dns/editByNameType/${domain}/${recordType}`); if(subdomain) apiURL = this._getEndpoint(`/dns/editByNameType/${domain}/${recordType}/${subdomain}`); return this._request({ url: apiURL, body: requestBody }); } /** * Delete a DNS record by it's ID. * * @documentation {@link https://porkbun.com/api/json/v3/documentation#DNS%20Delete%20Record%20by%20Domain%20and%20ID} * @param {string} domain * @param {string} recordID * @returns {Promise} */ deleteDNSRecord(domain, recordID) { if(!domain) return Promise.reject(new TypeError('Missing domain parameter!')); if(typeof domain !== 'string') return Promise.reject(new TypeError(`Invalid domain parameter type! Expected 'string', received '${typeof domain}'!`)); if(!recordID) return Promise.reject(new TypeError('Missing recordID parameter!')); if(typeof recordID !== 'string' && typeof recordID !== 'number') return Promise.reject(new TypeError(`Invalid recordID parameter type! Expected 'string'|'number', received '${typeof recordID}'!`)); return this._request({ url: this._getEndpoint(`/dns/delete/${domain}/${recordID}`) }); } /** * Delete all records for the domain that match a particular subdomain and type. * * @documentation {@link https://porkbun.com/api/json/v3/documentation#DNS%20Delete%20Records%20by%20Domain,%20Subdomain%20and%20Type} * @param {string} domain * @param {PorkbunAPIDNSRecordTypes} recordType * @param {string | null | undefined} [subdomain] - Leave blank, unset, set to undefined or to null for the root domain. * @returns {Promise} */ deleteDNSRecords(domain, recordType, subdomain) { if(!domain) return Promise.reject(new TypeError('Missing domain parameter!')); if(typeof domain !== 'string') return Promise.reject(new TypeError(`Invalid domain parameter type! Expected 'string', received '${typeof domain}'!`)); if(!recordType) return Promise.reject(new TypeError('Missing recordType parameter!')); if(typeof recordType !== 'string') return Promise.reject(new TypeError(`Invalid recordType parameter type! Expected 'string', received '${typeof recordType}'!`)); if(subdomain !== null && subdomain !== undefined && typeof subdomain !== 'string') return Promise.reject(new TypeError(`Invalid subdomain parameter type! Expected 'string', received '${typeof subdomain}'!`)); let apiURL = this._getEndpoint(`/dns/deleteByNameType/${domain}/${recordType}`); if(subdomain) apiURL = this._getEndpoint(`/dns/deleteByNameType/${domain}/${recordType}/${subdomain}`); return this._request({ url: apiURL }); } /** * Get the DNSSEC records associated with the domain at the registry. * * @documentation {@link https://porkbun.com/api/json/v3/documentation#DNSSEC%20Get%20Records} * @param {string} domain * @returns {Promise} */ getDNSSECRecords(domain) { if(!domain) return Promise.reject(new TypeError('Missing domain parameter!')); if(typeof domain !== 'string') return Promise.reject(new TypeError(`Invalid domain parameter type! Expected 'string', received '${typeof domain}'!`)); return this._request({ url: this._getEndpoint(`/dns/getDnssecRecords/${domain}`) }); } /** * Create a DNSSEC record at the registry. * Please note that DNSSEC creation differs at the various registries and some elements may or may not be required. * Most often the max sig life and key data elements are not required. * * @documentation {@link https://porkbun.com/api/json/v3/documentation#DNSSEC%20Create%20Record} * @param {string} domain * @param {Object} recordData * @param {string | number} recordData.keyTag - Key Tag * @param {string | number} recordData.alg - DS Data Algorithm * @param {string | number} recordData.digestType - Digest Type * @param {string} recordData.digest - Digest * @param {string} [recordData.maxSigLife] - Max Sig Life * @param {string} [recordData.keyDataFlags] - Key Data Flags * @param {string} [recordData.keyDataProtocol] - Key Data Protocol * @param {string} [recordData.keyDataAlgo] - Key Data Algorithm * @param {string} [recordData.keyDataPubKey] - Key Data Public Key * @returns {Promise} */ createDNSSECRecord(domain, recordData) { if(!domain) return Promise.reject(new TypeError('Missing domain parameter!')); if(typeof domain !== 'string') return Promise.reject(new TypeError(`Invalid domain parameter type! Expected 'string', received '${typeof domain}'!`)); if(!recordData) return Promise.reject(new TypeError('Missing recordData parameter!')); if(typeof recordData !== 'object') return Promise.reject(new TypeError(`Invalid recordData parameter type! Expected 'object', received '${typeof recordData}'!`)); if(!recordData.keyTag) return Promise.reject(new TypeError('Missing recordData.keyTag parameter!')); if(recordData.keyTag !== null && recordData.keyTag !== undefined && typeof recordData.keyTag !== 'number' && typeof recordData.keyTag !== 'string') return Promise.reject(new TypeError(`Invalid recordData.keyTag parameter type! Expected 'number' | 'string', received '${typeof recordData.keyTag}'!`)); if(!recordData.alg) return Promise.reject(new TypeError('Missing recordData.alg parameter!')); if(recordData.alg !== null && recordData.alg !== undefined && typeof recordData.alg !== 'number' && typeof recordData.alg !== 'string') return Promise.reject(new TypeError(`Invalid recordData.alg parameter type! Expected 'number' | 'string', received '${typeof recordData.alg}'!`)); if(!recordData.digestType) return Promise.reject(new TypeError('Missing recordData.digestType parameter!')); if(recordData.digestType !== null && recordData.digestType !== undefined && typeof recordData.digestType !== 'number' && typeof recordData.digestType !== 'string') return Promise.reject(new TypeError(`Invalid recordData.digestType parameter type! Expected 'number' | 'string', received '${typeof recordData.digestType}'!`)); if(!recordData.digest) return Promise.reject(new TypeError('Missing recordData.digest parameter!')); if(recordData.digest !== null && recordData.digest !== undefined && typeof recordData.digest !== 'string') return Promise.reject(new TypeError(`Invalid recordData.digest parameter type! Expected 'string', received '${typeof recordData.digest}'!`)); if(recordData.maxSigLife !== null && recordData.maxSigLife !== undefined && typeof recordData.maxSigLife !== 'string') return Promise.reject(new TypeError(`Invalid recordData.maxSigLife parameter type! Expected 'string', received '${typeof recordData.maxSigLife}'!`)); if(recordData.keyDataFlags !== null && recordData.keyDataFlags !== undefined && typeof recordData.keyDataFlags !== 'string') return Promise.reject(new TypeError(`Invalid recordData.keyDataFlags parameter type! Expected 'string', received '${typeof recordData.keyDataFlags}'!`)); if(recordData.keyDataProtocol !== null && recordData.keyDataProtocol !== undefined && typeof recordData.keyDataProtocol !== 'string') return Promise.reject(new TypeError(`Invalid recordData.keyDataProtocol parameter type! Expected 'string', received '${typeof recordData.keyDataProtocol}'!`)); if(recordData.keyDataAlgo !== null && recordData.keyDataAlgo !== undefined && typeof recordData.keyDataAlgo !== 'string') return Promise.reject(new TypeError(`Invalid recordData.keyDataAlgo parameter type! Expected 'string', received '${typeof recordData.keyDataAlgo}'!`)); if(recordData.keyDataPubKey !== null && recordData.keyDataPubKey !== undefined && typeof recordData.keyDataPubKey !== 'string') return Promise.reject(new TypeError(`Invalid recordData.keyDataPubKey parameter type! Expected 'string', received '${typeof recordData.keyDataPubKey}'!`)); const requestBody = { keyTag: typeof recordData.keyTag === 'number' ? recordData.keyTag.toString() : recordData.keyTag, alg: typeof recordData.alg === 'number' ? recordData.alg.toString() : recordData.alg, digestType: typeof recordData.digestType === 'number' ? recordData.digestType.toString() : recordData.digestType, digest: recordData.digest }; if(recordData.maxSigLife !== null && recordData.maxSigLife !== undefined) requestBody.maxSigLife = recordData.maxSigLife; if(recordData.keyDataFlags !== null && recordData.keyDataFlags !== undefined) requestBody.keyDataFlags = recordData.keyDataFlags; if(recordData.keyDataProtocol !== null && recordData.keyDataProtocol !== undefined) requestBody.keyDataProtocol = recordData.keyDataProtocol; if(recordData.keyDataAlgo !== null && recordData.keyDataAlgo !== undefined) requestBody.keyDataAlgo = recordData.keyDataAlgo; if(recordData.keyDataPubKey !== null && recordData.keyDataPubKey !== undefined) requestBody.keyDataPubKey = recordData.keyDataPubKey; return this._request({ url: this._getEndpoint(`/dns/createDnssecRecord/${domain}`), body: requestBody }); } /** * Delete a DNSSEC record associated with the domain at the registry. * Please note that most registries will delete all records with matching data, not just the record with the matching key tag. * * @documentation {@link https://porkbun.com/api/json/v3/documentation#DNSSEC%20Delete%20Record} * @param {string} domain * @param {string | number} keyTag * @returns {Promise} */ deleteDNSSECRecord(domain, keyTag) { if(!domain) return Promise.reject(new TypeError('Missing domain parameter!')); if(typeof domain !== 'string') return Promise.reject(new TypeError(`Invalid domain parameter type! Expected 'string', received '${typeof domain}'!`)); if(!keyTag && keyTag !== 0) return Promise.reject(new TypeError('Missing keyTag parameter!')); if(typeof keyTag !== 'number' && typeof keyTag !== 'string') return Promise.reject(new TypeError(`Invalid keyTag parameter type! Expected 'number' | 'string', received '${typeof keyTag}'!`)); return this._request({ url: this._getEndpoint(`/dns/deleteDnssecRecord/${domain}/${keyTag}`) }); } /** * Retrieve the SSL certificate bundle for the given domain. * * @documentation {@link https://porkbun.com/api/json/v3/documentation#SSL%20Retrieve%20Bundle%20by%20Domain} * @param {string} domain * @returns {Promise} */ getSSLBundle(domain) { if(!domain) return Promise.reject(new TypeError('Missing domain parameter!')); if(typeof domain !== 'string') return Promise.reject(new TypeError(`Invalid domain parameter type! Expected 'string', received '${typeof domain}'!`)); return this._request({ url: this._getEndpoint(`/ssl/retrieve/${domain}`) }); } } module.exports = { PorkbunClient, ResponseError, APIError };