feat: DNS provider abstraction — Technitium, Cloudflare, RFC 2136, Manual
CI / Test & Lint (push) Has been cancelled
CI / Security audit (push) Has been cancelled

- dns-providers/: adapter base class + registry with auto-discovery
- technitium.js: wraps existing Technitium API calls into adapter interface
- cloudflare.js: Cloudflare API v4 adapter (zones, records, credentials)
- rfc2136.js: RFC 2136 dynamic DNS via nsupdate (BIND, PowerDNS, etc.)
- manual.js: no-op adapter for external DNS management with instructions
- provider-dns.js: provider-aware DNS context, resolves active adapter from config
- Universal helper methods: universalCreateRecord/Delete/ResolveRecord
- All 7 route files updated to use universal methods instead of raw dns.call()
- Setup wizard: provider dropdown (Technitium, Cloudflare, RFC 2136, Manual)
- DNS template selector: added Cloudflare and External/Manual options
- Config schema: validates dns.provider field
- Capability gating on Technitium-specific endpoints (logs, restart, update)
- Backward compatible: no provider set = auto-detect (technitium if dns.ip exists)
This commit is contained in:
Hermes
2026-06-10 15:06:41 -07:00
parent 0aa1c3d077
commit 2de72ed506
21 changed files with 1965 additions and 33 deletions
+93
View File
@@ -0,0 +1,93 @@
/**
* Manual DNS Provider Adapter
* No-op adapter for users who manage DNS externally (manual, cPanel, other control panels).
* Provides propagation checking only — all record operations return helpful instructions.
*/
const BaseDNSProvider = require('./base');
class ManualDNSProvider extends BaseDNSProvider {
constructor(config, ctx) {
super(config, ctx);
this.providerId = 'manual';
this.displayName = 'Manual / External DNS';
this.description = 'Manage DNS records yourself via your provider\'s control panel';
}
supportsCapability(cap) {
return ['credentials'].includes(cap);
}
getCapabilities() {
return ['credentials'];
}
async authenticate() {
return { success: true, message: 'Manual DNS — no authentication needed' };
}
async createRecord({ domain, zone, type, value, ttl }) {
return {
status: 'manual',
message: `Create this record manually in your DNS control panel:`,
instructions: {
name: domain,
type: type || 'A',
value,
ttl: ttl || 300
}
};
}
async deleteRecord({ domain, type, value }) {
return {
status: 'manual',
message: `Delete this record manually from your DNS control panel:`,
instructions: {
name: domain,
type: type || 'A',
value: value || '(any)'
}
};
}
async resolveRecords({ domain, zone, type }) {
// Use Node.js built-in DNS to resolve regardless of provider
const dns = require('dns').promises;
try {
const resolver = new dns.Resolver();
resolver.setServers(['1.1.1.1', '8.8.8.8']);
const records = await resolver.resolve(domain, type || 'A');
return {
status: 'ok',
response: {
records: records.map(r => ({
type: type || 'A',
domain,
rData: { ipAddress: r },
ttl: 0,
manual: true
}))
}
};
} catch (err) {
return { status: 'ok', response: { records: [] } };
}
}
async getStatus() {
return {
providerId: this.providerId,
displayName: this.displayName,
description: this.description,
capabilities: this.getCapabilities(),
authenticated: true,
note: 'DNS records are managed externally. Use propagation checks to verify changes.'
};
}
validateConfig() {
return { valid: true, errors: [] };
}
}
module.exports = ManualDNSProvider;