'use strict'; const axios = require('axios'); const {DnsApi} = require('./common'); class PorkBun extends DnsApi{ static _keyMap = { 'apiKey': {isRequired: true, type: 'string', isPrivate: true, displayName: 'API key'}, 'secretApiKey': {isRequired: true, type: 'string', isPrivate: true, displayName: 'API Secret key'}, } static displayName = 'PorkBun'; static displayIconUni = ''; static displayIconHtml = ` ` constructor(args){ super() this.apiKey = args.apiKey; this.secretApiKey = args.secretApiKey; } async post(url, data){ let res; try{ data = { ...(data || {}), secretapikey: this.secretApiKey, apikey: this.apiKey, }; res = await axios.post(`https://api.porkbun.com/api/json/v3${url}`, data); return res; }catch(error){ if(!error.response) throw error; if(error.response.data.message.includes('Invalid API key')){ throw this.errors.unauthorized(); } // console.error('API error:', error) throw this.errors.other(error.response.status, error.response.data.message) } } __typeCheck(type){ if(!['A', 'MX', 'CNAME', 'ALIAS', 'TXT', 'NS', 'AAAA', 'SRV', 'TLSA', 'CAA', 'HTTPS', 'SVCB'].includes(type)) throw new Error('PorkBun API: Invalid type passed') } __parseName(domain, name){ if(name && !name.endsWith('.'+domain)){ return `${name}.${domain}` } return name; } /* The API and the generic class interface have different opinions of what keys hold what data, the __parseOptions and __pastseRes normal the keys to what the class expects What the the API calls it : What the class wants it as. */ __apiKeyMap = { 'content': 'data' } async getRecords(domain, options){ let res = await this.post(`/dns/retrieve/${domain}`); let records = this.__parseRes(res.data.records) if(!options) return records; options = this.__parseOptions(options); return records.filter((record)=>{ let matchCount = 0 for(let key in options){ if(record[key] === options[key] && ++matchCount === Object.keys(options).length){ return true; } } }); } // Porkbun names an apex record with an empty name. apexName(domainName){ return ''; } async createRecord(domain, options, force = true){ try{ // Apex ('@') maps to an empty name for Porkbun; because that name is // legitimately falsy, only type+data are required here (not name). if(options.name === '@') options.name = this.apexName(domain.domain); options = this.__parseOptions(options, ['type', 'data']); // Delete the current records if(force){ let forceOptions = new Map(Object.entries(options)); forceOptions.delete('data'); forceOptions.delete('content'); await this.deleteRecords(domain, Object.fromEntries(forceOptions)); } let res = await this.post(`/dns/create/${domain}`, options); return res.data.result; }catch(error){ if(error.message && error.message.includes('We were unable to create the DNS record')){ return (await this.getRecords(domain, options))[0]; } } } async deleteRecordById(domain, id){ let res = await this.post(`/dns/delete/${domain.domain}/${id}`); return res.data; } async deleteRecords(domain, options){ let records = await this.getRecords(domain, options); for(let record of records){ await this.deleteRecordById(domain, record.id) } } async listDomains(){ let res = await this.post(`/domain/listAll`, {"includeLabels": "yes"}); return res.data.domains; } } module.exports = PorkBun;