Initial commit - Porkbun DNS management skill
This commit is contained in:
178
SKILL.md
Normal file
178
SKILL.md
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
---
|
||||||
|
name: porkbun
|
||||||
|
description: Manage Porkbun DNS records and domains via API v3. Use when Codex needs to create, read, update, or delete DNS records on Porkbun; list domains; configure API access; work with common record types (A, AAAA, CNAME, MX, TXT, etc.). The skill includes a CLI tool `scripts/porkbun-dns.js` for executing DNS operations reliably.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Porkbun DNS Management
|
||||||
|
|
||||||
|
Manage DNS records and domains on Porkbun via their REST API v3.
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
### Set up API credentials
|
||||||
|
|
||||||
|
1. Generate API keys: https://porkbun.com/account/api
|
||||||
|
2. Save credentials to config file: `~/.config/porkbun/config.json`
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"apiKey": "your-api-key",
|
||||||
|
"secretApiKey": "your-secret-api-key"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Or set environment variables:
|
||||||
|
```bash
|
||||||
|
export PORKBUN_API_KEY="your-api-key"
|
||||||
|
export PORKBUN_SECRET_API_KEY="your-secret-api-key"
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Enable API access for each domain: Domain Management → Details → API Access → Enable
|
||||||
|
|
||||||
|
### Test connection
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node ~/.openclaw/workspace/skills/public/porkbun/scripts/porkbun-dns.js ping
|
||||||
|
```
|
||||||
|
|
||||||
|
## Using the CLI Tool
|
||||||
|
|
||||||
|
The `scripts/porkbun-dns.js` script provides a reliable, deterministic way to execute DNS operations. Use it directly for common tasks instead of writing custom code.
|
||||||
|
|
||||||
|
### Common Operations
|
||||||
|
|
||||||
|
#### List domains
|
||||||
|
```bash
|
||||||
|
node scripts/porkbun-dns.js list
|
||||||
|
```
|
||||||
|
|
||||||
|
#### List DNS records
|
||||||
|
```bash
|
||||||
|
node scripts/porkbun-dns.js records example.com
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Create records
|
||||||
|
```bash
|
||||||
|
# A record
|
||||||
|
node scripts/porkbun-dns.js create example.com type=A name=www content=1.1.1.1 ttl=600
|
||||||
|
|
||||||
|
# CNAME
|
||||||
|
node scripts/porkbun-dns.js create example.com type=CNAME name=docs content=example.com
|
||||||
|
|
||||||
|
# MX record
|
||||||
|
node scripts/porkbun-dns.js create example.com type=MX name= content="mail.example.com" prio=10
|
||||||
|
|
||||||
|
# TXT record ( SPF for email)
|
||||||
|
node scripts/porkbun-dns.js create example.com type=TXT name= content="v=spf1 include:_spf.google.com ~all"
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Edit records
|
||||||
|
```bash
|
||||||
|
# By ID (get ID from records command)
|
||||||
|
node scripts/porkbun-dns.js edit example.com 123456 content=2.2.2.2
|
||||||
|
|
||||||
|
# By type and subdomain (updates all matching records)
|
||||||
|
node scripts/porkbun-dns.js edit-by example.com A www content=2.2.2.2
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Delete records
|
||||||
|
```bash
|
||||||
|
# By ID
|
||||||
|
node scripts/porkbun-dns.js delete example.com 123456
|
||||||
|
|
||||||
|
# By type and subdomain
|
||||||
|
node scripts/porkbun-dns.js delete-by example.com A www
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Get specific records
|
||||||
|
```bash
|
||||||
|
# All records
|
||||||
|
node scripts/porkbun-dns.js get example.com
|
||||||
|
|
||||||
|
# Filter by type
|
||||||
|
node scripts/porkbun-dns.js get example.com A
|
||||||
|
|
||||||
|
# Filter by type and subdomain
|
||||||
|
node scripts/porkbun-dns.js get example.com A www
|
||||||
|
```
|
||||||
|
|
||||||
|
## Record Types
|
||||||
|
|
||||||
|
Supported record types: A, AAAA, CNAME, ALIAS, TXT, NS, MX, SRV, TLSA, CAA, HTTPS, SVCB, SSHFP
|
||||||
|
|
||||||
|
For detailed field requirements and examples, see [references/dns-record-types.md](references/dns-record-types.md).
|
||||||
|
|
||||||
|
## Common Patterns
|
||||||
|
|
||||||
|
### Website Setup
|
||||||
|
|
||||||
|
Create root A record and www CNAME:
|
||||||
|
```bash
|
||||||
|
node scripts/porkbun-dns.js create example.com type=A name= content=192.0.2.1
|
||||||
|
node scripts/porkbun-dns.js create example.com type=CNAME name=www content=example.com
|
||||||
|
```
|
||||||
|
|
||||||
|
### Email Configuration
|
||||||
|
|
||||||
|
Set up MX records for Google Workspace:
|
||||||
|
```bash
|
||||||
|
node scripts/porkbun-dns.js create example.com type=MX name= content="aspmx.l.google.com" prio=1
|
||||||
|
node scripts/porkbun-dns.js create example.com type=MX name= content="alt1.aspmx.l.google.com" prio=5
|
||||||
|
node scripts/porkbun-dns.js create example.com type=MX name= content="alt2.aspmx.l.google.com" prio=5
|
||||||
|
node scripts/porkbun-dns.js create example.com type=MX name= content="alt3.aspmx.l.google.com" prio=10
|
||||||
|
node scripts/porkbun-dns.js create example.com type=MX name= content="alt4.aspmx.l.google.com" prio=10
|
||||||
|
```
|
||||||
|
|
||||||
|
Add SPF record:
|
||||||
|
```bash
|
||||||
|
node scripts/porkbun-dns.js create example.com type=TXT name= content="v=spf1 include:_spf.google.com ~all"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Dynamic DNS
|
||||||
|
|
||||||
|
Update home IP address (can be scripted/automated):
|
||||||
|
```bash
|
||||||
|
HOME_IP=$(curl -s ifconfig.me)
|
||||||
|
node scripts/porkbun-dns.js edit-by example.com A home content=$HOME_IP
|
||||||
|
```
|
||||||
|
|
||||||
|
### Wildcard DNS
|
||||||
|
|
||||||
|
Create a wildcard record pointing to root:
|
||||||
|
```bash
|
||||||
|
node scripts/porkbun-dns.js create example.com type=A name=* content=192.0.2.1
|
||||||
|
```
|
||||||
|
|
||||||
|
## Reference Documentation
|
||||||
|
|
||||||
|
- **[references/dns-record-types.md](references/dns-record-types.md)** - Detailed reference for all DNS record types and field requirements
|
||||||
|
- **[https://porkbun.com/api/json/v3/documentation](https://porkbun.com/api/json/v3/documentation)** - Full API documentation
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### "API key not found"
|
||||||
|
- Verify config file exists at `~/.config/porkbun/config.json`
|
||||||
|
- Check environment variables: `echo $PORKBUN_API_KEY`
|
||||||
|
- Ensure API access is enabled for the specific domain
|
||||||
|
|
||||||
|
### "Invalid type passed"
|
||||||
|
- Record types must be uppercase (e.g., `A`, not `a`)
|
||||||
|
- See supported types list above
|
||||||
|
|
||||||
|
### HTTP errors
|
||||||
|
- Verify API keys are valid at https://porkbun.com/account/api
|
||||||
|
- Check network connectivity
|
||||||
|
- Confirm API endpoint is `api.porkbun.com` (not `porkbun.com`)
|
||||||
|
|
||||||
|
### TTL errors
|
||||||
|
- Minimum TTL is 600 seconds (10 minutes)
|
||||||
|
- Default TTL is 600 seconds
|
||||||
|
- Common values: 300 (dynamic), 3600 (standard), 86400 (stable)
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- TTL minimum is 600 seconds
|
||||||
|
- Use "@" for root domain records
|
||||||
|
- Use "*" for wildcard records
|
||||||
|
- TXT records with spaces need quotes
|
||||||
|
- Multiple MX records allowed with different priorities
|
||||||
|
- API v3 current hostname: `api.porkbun.com`
|
||||||
114
references/dns-record-types.md
Normal file
114
references/dns-record-types.md
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
# DNS Record Types Reference
|
||||||
|
|
||||||
|
Quick reference for common DNS record types and field requirements.
|
||||||
|
|
||||||
|
## Record Types
|
||||||
|
|
||||||
|
### A (Address)
|
||||||
|
Maps a hostname to an IPv4 address.
|
||||||
|
- **name**: Hostname (e.g., "www", "@" for root, "*" for wildcard)
|
||||||
|
- **content**: IPv4 address (e.g., "1.1.1.1")
|
||||||
|
- **ttl**: Time to live in seconds (min 600, default 600)
|
||||||
|
|
||||||
|
### AAAA (IPv6 Address)
|
||||||
|
Maps a hostname to an IPv6 address.
|
||||||
|
- **name**: Hostname
|
||||||
|
- **content**: IPv6 address (e.g., "2001:0db8:85a3:0000:0000:8a2e:0370:7334")
|
||||||
|
- **ttl**: Time to live
|
||||||
|
|
||||||
|
### CNAME (Canonical Name)
|
||||||
|
Creates an alias for another domain name.
|
||||||
|
- **name**: Alias hostname (e.g., "www")
|
||||||
|
- **content**: Target domain (e.g., "example.com")
|
||||||
|
- **ttl**: Time to live
|
||||||
|
|
||||||
|
### MX (Mail Exchange)
|
||||||
|
Specifies mail servers for the domain.
|
||||||
|
- **name**: Usually "@" (root domain)
|
||||||
|
- **content**: Mail server hostname (e.g., "mail.example.com")
|
||||||
|
- **prio**: Priority (lower = higher preference, e.g., 10, 20)
|
||||||
|
- **ttl**: Time to live
|
||||||
|
|
||||||
|
### TXT (Text)
|
||||||
|
Stores text data, commonly used for SPF, DKIM, verification.
|
||||||
|
- **name**: Hostname
|
||||||
|
- **content**: Text string (quotes for spaces, e.g., "v=spf1 include:_spf.google.com ~all")
|
||||||
|
- **ttl**: Time to live
|
||||||
|
|
||||||
|
### NS (Name Server)
|
||||||
|
Specifies authoritative name servers for the domain.
|
||||||
|
- **name**: Usually "@" (at delegation points) or subdomain name
|
||||||
|
- **content**: Nameserver hostname (e.g., "ns1.example.com")
|
||||||
|
- **ttl**: Time to live
|
||||||
|
|
||||||
|
### ALIAS
|
||||||
|
Similar to CNAME but works at the root domain level.
|
||||||
|
- **name**: Usually "@"
|
||||||
|
- **content**: Target domain or hostname
|
||||||
|
- **ttl**: Time to live
|
||||||
|
|
||||||
|
### SRV (Service)
|
||||||
|
Specifies location of services (e.g., LDAP, VoIP).
|
||||||
|
- **name**: Service name format: `_service._protocol` (e.g., "_sip._tcp")
|
||||||
|
- **content**: Priority, weight, port, target (e.g., "10 60 5060 sipserver.example.com")
|
||||||
|
- **prio**: Not used for SRV
|
||||||
|
- **ttl**: Time to live
|
||||||
|
|
||||||
|
### CAA (Certification Authority Authorization)
|
||||||
|
Restricts which CAs can issue certificates for your domain.
|
||||||
|
- **name**: Usually "@"
|
||||||
|
- **content**: Flags tag value (e.g., "0 issueletsencrypt.org")
|
||||||
|
- **ttl**: Time to live
|
||||||
|
|
||||||
|
### TLSA (TLS Authentication)
|
||||||
|
Specifies TLS certificate information for DANE.
|
||||||
|
- **name**: Port format: `_port._protocol` (e.g., "_443._tcp")
|
||||||
|
- **content**: Certificate usage, selector, matching type, certificate association data
|
||||||
|
- **ttl**: Time to live
|
||||||
|
|
||||||
|
### HTTPS / SVCB (Service Binding)
|
||||||
|
Modern DNS record types for HTTPS and service binding.
|
||||||
|
- **name**: Hostname
|
||||||
|
- **content**: Priority, target, service params
|
||||||
|
- **ttl**: Time to live
|
||||||
|
|
||||||
|
### SSHFP (SSH Fingerprint)
|
||||||
|
Stores SSH public key fingerprints.
|
||||||
|
- **name**: Hostname
|
||||||
|
- **content**: Algorithm, type, fingerprint (hex)
|
||||||
|
- **ttl**: Time to live
|
||||||
|
|
||||||
|
## Common TTL Values
|
||||||
|
|
||||||
|
- **300** (5 minutes) - Fast-changing records (dynamic DNS)
|
||||||
|
- **600** (10 minutes) - Default minimum, common for dynamic records
|
||||||
|
- **3600** (1 hour) - Standard for most records
|
||||||
|
- **86400** (24 hours) - Stable records with infrequent changes
|
||||||
|
|
||||||
|
## MX Priority Pattern
|
||||||
|
|
||||||
|
Typical MX configuration:
|
||||||
|
- Priority 10: Primary mail server
|
||||||
|
- Priority 20: Secondary/failover mail server
|
||||||
|
- Priority 30: Tertiary backup mail server
|
||||||
|
|
||||||
|
Email delivery tries lowest priority first.
|
||||||
|
|
||||||
|
## Example TXT Records
|
||||||
|
|
||||||
|
### SPF (Sender Policy Framework)
|
||||||
|
```
|
||||||
|
v=spf1 include:_spf.google.com ~all
|
||||||
|
v=spf1 ip4:192.0.2.0/24 -all
|
||||||
|
v=spf1 a mx include:sendgrid.net ~all
|
||||||
|
```
|
||||||
|
|
||||||
|
### Domain Verification
|
||||||
|
```
|
||||||
|
google-site-verification=your-verification-token
|
||||||
|
```
|
||||||
|
|
||||||
|
### DKIM
|
||||||
|
```
|
||||||
|
k=rsa; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC...
|
||||||
|
```
|
||||||
424
scripts/porkbun-dns.js
Executable file
424
scripts/porkbun-dns.js
Executable file
@@ -0,0 +1,424 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* Porkbun DNS Management Tool
|
||||||
|
* Interact with Porkbun API v3 for DNS management
|
||||||
|
*
|
||||||
|
* API Docs: https://porkbun.com/api/json/v3/documentation
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* node porkbun-dns.js <command> [options]
|
||||||
|
*
|
||||||
|
* Commands:
|
||||||
|
* ping Test API connection
|
||||||
|
* list List all domains
|
||||||
|
* list-domains Alias for list
|
||||||
|
* records <domain> List all DNS records for a domain
|
||||||
|
* get <domain> [type] [name] Get specific record(s)
|
||||||
|
* create <domain> Create a DNS record
|
||||||
|
* edit <domain> <id> Edit a record by ID
|
||||||
|
* edit-by <domain> <type> [name] Edit by type/subdomain
|
||||||
|
* delete <domain> <id> Delete a record by ID
|
||||||
|
* delete-by <domain> <type> [name] Delete by type/subdomain
|
||||||
|
*
|
||||||
|
* Environment (required):
|
||||||
|
* PORKBUN_API_KEY Your API key
|
||||||
|
* PORKBUN_SECRET_API_KEY Your secret API key
|
||||||
|
*
|
||||||
|
* Or use config file: ~/.config/porkbun/config.json
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const https = require('https');
|
||||||
|
const http = require('http');
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const { URL } = require('url');
|
||||||
|
|
||||||
|
class PorkbunAPI {
|
||||||
|
constructor(apiKey, secretApiKey) {
|
||||||
|
this.apiKey = apiKey;
|
||||||
|
this.secretApiKey = secretApiKey;
|
||||||
|
this.baseUrl = 'https://api.porkbun.com/api/json/v3';
|
||||||
|
}
|
||||||
|
|
||||||
|
async _request(endpoint, data = {}) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const url = new URL(`${this.baseUrl}${endpoint}`);
|
||||||
|
const payload = JSON.stringify({
|
||||||
|
...data,
|
||||||
|
secretapikey: this.secretApiKey,
|
||||||
|
apikey: this.apiKey,
|
||||||
|
});
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
hostname: url.hostname,
|
||||||
|
port: url.port,
|
||||||
|
path: url.pathname,
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Content-Length': Buffer.byteLength(payload),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const protocol = url.protocol === 'https:' ? https : http;
|
||||||
|
const req = protocol.request(options, (res) => {
|
||||||
|
let body = '';
|
||||||
|
res.on('data', (chunk) => body += chunk);
|
||||||
|
res.on('end', () => {
|
||||||
|
try {
|
||||||
|
const json = JSON.parse(body);
|
||||||
|
if (res.statusCode !== 200 || json.status !== 'SUCCESS') {
|
||||||
|
reject(new Error(json.message || `HTTP ${res.statusCode}`));
|
||||||
|
} else {
|
||||||
|
resolve(json);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
reject(new Error(`Invalid JSON response: ${body}`));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
req.on('error', reject);
|
||||||
|
req.write(payload);
|
||||||
|
req.end();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ping - Test API connection
|
||||||
|
async ping() {
|
||||||
|
return await this._request('/ping');
|
||||||
|
}
|
||||||
|
|
||||||
|
// List all domains
|
||||||
|
async listDomains() {
|
||||||
|
return await this._request('/domain/listAll', { includeLabels: 'yes' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all DNS records for a domain
|
||||||
|
async getRecords(domain) {
|
||||||
|
return await this._request(`/dns/retrieve/${domain}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get DNS records by domain, type, and optionally subdomain
|
||||||
|
async getRecordsByNameType(domain, type, name = null) {
|
||||||
|
const endpoint = name
|
||||||
|
? `/dns/retrieveByNameType/${domain}/${type}/${name}`
|
||||||
|
: `/dns/retrieveByNameType/${domain}/${type}`;
|
||||||
|
return await this._request(endpoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a DNS record
|
||||||
|
async createRecord(domain, record) {
|
||||||
|
const required = ['type', 'content'];
|
||||||
|
for (const field of required) {
|
||||||
|
if (!record[field]) {
|
||||||
|
throw new Error(`Missing required field: ${field}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const validTypes = ['A', 'MX', 'CNAME', 'ALIAS', 'TXT', 'NS', 'AAAA', 'SRV', 'TLSA', 'CAA', 'HTTPS', 'SVCB', 'SSHFP'];
|
||||||
|
if (!validTypes.includes(record.type)) {
|
||||||
|
throw new Error(`Invalid type: ${record.type}. Valid types: ${validTypes.join(', ')}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return await this._request(`/dns/create/${domain}`, record);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Edit a record by ID
|
||||||
|
async editRecord(domain, id, record) {
|
||||||
|
return await this._request(`/dns/edit/${domain}/${id}`, record);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Edit records by type and subdomain
|
||||||
|
async editRecordByNameType(domain, type, record, name = null) {
|
||||||
|
const endpoint = name
|
||||||
|
? `/dns/editByNameType/${domain}/${type}/${name}`
|
||||||
|
: `/dns/editByNameType/${domain}/${type}`;
|
||||||
|
return await this._request(endpoint, record);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete a record by ID
|
||||||
|
async deleteRecord(domain, id) {
|
||||||
|
return await this._request(`/dns/delete/${domain}/${id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete records by type and subdomain
|
||||||
|
async deleteRecordByNameType(domain, type, name = null) {
|
||||||
|
const endpoint = name
|
||||||
|
? `/dns/deleteByNameType/${domain}/${type}/${name}`
|
||||||
|
: `/dns/deleteByNameType/${domain}/${type}`;
|
||||||
|
return await this._request(endpoint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load config from file
|
||||||
|
function loadConfig() {
|
||||||
|
const configPath = path.join(process.env.HOME, '.config', 'porkbun', 'config.json');
|
||||||
|
if (fs.existsSync(configPath)) {
|
||||||
|
try {
|
||||||
|
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
||||||
|
return {
|
||||||
|
apiKey: config.apiKey || process.env.PORKBUN_API_KEY,
|
||||||
|
secretApiKey: config.secretApiKey || process.env.PORKBUN_SECRET_API_KEY,
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`Error reading config file: ${e.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize API client
|
||||||
|
function createAPI() {
|
||||||
|
let apiKey = process.env.PORKBUN_API_KEY;
|
||||||
|
let secretApiKey = process.env.PORKBUN_SECRET_API_KEY;
|
||||||
|
|
||||||
|
// Try config file if env vars not set
|
||||||
|
if (!apiKey || !secretApiKey) {
|
||||||
|
const config = loadConfig();
|
||||||
|
if (config) {
|
||||||
|
apiKey = apiKey || config.apiKey;
|
||||||
|
secretApiKey = secretApiKey || config.secretApiKey;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!apiKey || !secretApiKey) {
|
||||||
|
console.error('Error: PORKBUN_API_KEY and PORKBUN_SECRET_API_KEY must be set');
|
||||||
|
console.error('');
|
||||||
|
console.error('Either set environment variables or create a config file:');
|
||||||
|
console.error(' ~/.config/porkbun/config.json');
|
||||||
|
console.error('');
|
||||||
|
console.error('Config file format:');
|
||||||
|
console.err(JSON.stringify({
|
||||||
|
apiKey: 'your-api-key',
|
||||||
|
secretApiKey: 'your-secret-api-key',
|
||||||
|
}, null, 2));
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new PorkbunAPI(apiKey, secretApiKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print helper functions
|
||||||
|
function printJSON(data) {
|
||||||
|
console.log(JSON.stringify(data, null, 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
function printTable(data) {
|
||||||
|
if (!data.length) {
|
||||||
|
console.log('No records found.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const columns = ['id', 'name', 'type', 'content', 'ttl', 'prio'];
|
||||||
|
const widths = {};
|
||||||
|
columns.forEach(col => widths[col] = Math.max(col.length, ...data.map(r => String(r[col] || '').length)));
|
||||||
|
|
||||||
|
// Header
|
||||||
|
console.log(columns.map(col => col.padEnd(widths[col])).join(' | '));
|
||||||
|
console.log(columns.map(() => '-'.repeat(40)).join('-+-'));
|
||||||
|
|
||||||
|
// Rows
|
||||||
|
data.forEach(row => {
|
||||||
|
console.log(
|
||||||
|
columns.map(col => {
|
||||||
|
let val = String(row[col] || '');
|
||||||
|
if (col === 'content' && val.length > widths[col]) {
|
||||||
|
val = val.slice(0, widths[col] - 3) + '...';
|
||||||
|
}
|
||||||
|
return val.padEnd(widths[col]);
|
||||||
|
}).join(' | ')
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// CLI Handler
|
||||||
|
async function main() {
|
||||||
|
const args = process.argv.slice(2);
|
||||||
|
|
||||||
|
if (args.length === 0 || args[0] === 'help' || args[0] === '--help' || args[0] === '-h') {
|
||||||
|
console.log(fs.readFileSync(__filename, 'utf8').match(/\/\*\*[\s\S]*?\*\//)[0]
|
||||||
|
.replace(/\/\*\*|\*\//g, '')
|
||||||
|
.replace(/^\s*\*\s?/gm, ''));
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
const api = createAPI();
|
||||||
|
const command = args[0];
|
||||||
|
|
||||||
|
try {
|
||||||
|
switch (command) {
|
||||||
|
case 'ping':
|
||||||
|
const ping = await api.ping();
|
||||||
|
printJSON(ping);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'list':
|
||||||
|
case 'list-domains':
|
||||||
|
const domains = await api.listDomains();
|
||||||
|
console.log(`${domains.domains.length} domain(s) found:\n`);
|
||||||
|
domains.domains.forEach(d => {
|
||||||
|
console.log(` ${d.domain} (${d.status})`);
|
||||||
|
console.log(` Expires: ${d.expireDate}`);
|
||||||
|
console.log(` Auto-renew: ${d.autoRenew ? 'ON' : 'OFF'}`);
|
||||||
|
if (d.labels && d.labels.length) {
|
||||||
|
console.log(` Labels: ${d.labels.map(l => l.title).join(', ')}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'records': {
|
||||||
|
const domain = args[1];
|
||||||
|
if (!domain) {
|
||||||
|
console.error('Error: domain required');
|
||||||
|
console.error('Usage: node porkbun-dns.js records <domain>');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
const records = await api.getRecords(domain);
|
||||||
|
console.log(`${records.records.length} DNS record(s) for ${domain}:\n`);
|
||||||
|
printTable(records.records);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'get': {
|
||||||
|
const [_, _cmd, domain, type, name] = args;
|
||||||
|
if (!domain) {
|
||||||
|
console.error('Error: domain required');
|
||||||
|
console.error('Usage: node porkbun-dns.js get <domain> [type] [name]');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
let result;
|
||||||
|
if (type) {
|
||||||
|
result = await api.getRecordsByNameType(domain, type, name);
|
||||||
|
} else {
|
||||||
|
result = await api.getRecords(domain);
|
||||||
|
}
|
||||||
|
console.log(`${result.records.length} record(s) found:\n`);
|
||||||
|
printTable(result.records);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'create': {
|
||||||
|
const [_, _cmd, domain, ...recordArgs] = args;
|
||||||
|
if (!domain) {
|
||||||
|
console.error('Error: domain required');
|
||||||
|
console.error('Usage: node porkbun-dns.js create <domain>');
|
||||||
|
console.error('Then provide record fields as key=value pairs:');
|
||||||
|
console.error(' type=A name=www content=1.1.1.1 ttl=600');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse record arguments
|
||||||
|
const record = {};
|
||||||
|
for (const arg of recordArgs) {
|
||||||
|
const [key, ...valParts] = arg.split('=');
|
||||||
|
const value = valParts.join('=');
|
||||||
|
if (!value) {
|
||||||
|
console.error(`Error: Invalid argument format: ${arg}`);
|
||||||
|
console.error('Expected: key=value');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
record[key] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await api.createRecord(domain, record);
|
||||||
|
console.log(`✅ Record created successfully!`);
|
||||||
|
console.log(` ID: ${result.id}`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'edit': {
|
||||||
|
const [_, _cmd, domain, id, ...recordArgs] = args;
|
||||||
|
if (!domain || !id) {
|
||||||
|
console.error('Error: domain and ID required');
|
||||||
|
console.error('Usage: node porkbun-dns.js edit <domain> <id>');
|
||||||
|
console.error('Then provide record fields to update as key=value pairs:');
|
||||||
|
console.error(' content=1.1.1.2 ttl=600');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const record = {};
|
||||||
|
for (const arg of recordArgs) {
|
||||||
|
const [key, ...valParts] = arg.split('=');
|
||||||
|
const value = valParts.join('=');
|
||||||
|
if (!value) {
|
||||||
|
console.error(`Error: Invalid argument format: ${arg}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
record[key] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
await api.editRecord(domain, id, record);
|
||||||
|
console.log(`✅ Record ${id} updated successfully!`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'edit-by': {
|
||||||
|
const [_, _cmd, domain, type, ...rest] = args;
|
||||||
|
if (!domain || !type) {
|
||||||
|
console.error('Error: domain and type required');
|
||||||
|
console.error('Usage: node porkbun-dns.js edit-by <domain> <type> [name]');
|
||||||
|
console.error('Then provide record fields to update as key=value pairs:');
|
||||||
|
console.error(' content=1.1.1.2 ttl=600');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const name = rest[0] && rest[0].includes('=') ? null : rest[0];
|
||||||
|
const recordArgs = name ? rest.slice(1) : rest;
|
||||||
|
|
||||||
|
const record = {};
|
||||||
|
for (const arg of recordArgs) {
|
||||||
|
const [key, ...valParts] = arg.split('=');
|
||||||
|
const value = valParts.join('=');
|
||||||
|
if (!value) {
|
||||||
|
console.error(`Error: Invalid argument format: ${arg}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
record[key] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
await api.editRecordByNameType(domain, type, record, name);
|
||||||
|
console.log(`✅ Records for ${domain}${name ? `/${name}` : ''} (${type}) updated successfully!`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'delete': {
|
||||||
|
const [_, _cmd, domain, id] = args;
|
||||||
|
if (!domain || !id) {
|
||||||
|
console.error('Error: domain and ID required');
|
||||||
|
console.error('Usage: node porkbun-dns.js delete <domain> <id>');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
await api.deleteRecord(domain, id);
|
||||||
|
console.log(`✅ Record ${id} deleted successfully!`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'delete-by': {
|
||||||
|
const [_, _cmd, domain, type, name = null] = args;
|
||||||
|
if (!domain || !type) {
|
||||||
|
console.error('Error: domain and type required');
|
||||||
|
console.error('Usage: node porkbun-dns.js delete-by <domain> <type> [name]');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
await api.deleteRecordByNameType(domain, type, name);
|
||||||
|
console.log(`✅ Records for ${domain}${name ? `/${name}` : ''} (${type}) deleted successfully!`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
console.error(`Unknown command: ${command}`);
|
||||||
|
console.error('Run: node porkbun-dns.js help');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`❌ Error: ${error.message}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
||||||
Reference in New Issue
Block a user