alias-dns-bridge/dump-zone.js
Claude d5c4d4c52a
feat(alias-dns-bridge): initial push
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 22:42:01 +01:00

146 lines
4.4 KiB
JavaScript

#!/usr/bin/env node
/**
* HSD → CoreDNS Zone File Generator
*
* Reads all registered names from the HSD sidechain and generates
* a BIND-format zone file that CoreDNS can serve.
*
* Usage:
* node dump-zone.js > /opt/noc/coredns/db.lthn
* # Or via cron:
* cron: every 5 min, run dump-zone.js > db.lthn.new && mv db.lthn.new db.lthn
*
* Environment:
* HSD_URL — HSD RPC endpoint (default: http://127.0.0.1:14037)
* HSD_API_KEY — HSD API key (default: testkey)
*/
const HSD_URL = process.env.HSD_URL || 'http://127.0.0.1:14037';
const HSD_API_KEY = process.env.HSD_API_KEY || 'testkey';
async function rpcHSD(method, params = []) {
const auth = Buffer.from(`x:${HSD_API_KEY}`).toString('base64');
const resp = await fetch(HSD_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Authorization': `Basic ${auth}` },
body: JSON.stringify({ method, params })
});
const data = await resp.json();
if (data.error) throw new Error(`HSD ${method}: ${data.error.message}`);
return data.result;
}
async function getAllNames() {
// Get chain height for serial number
const info = await rpcHSD('getinfo');
const height = info.blocks;
// Scan for names — iterate through known names
// HSD doesn't have a "list all names with records" RPC, so we try known names
// In production, this would scan the UTXO set or maintain a name registry
const names = [];
// Try names from our sunrise list + common names
const candidates = [
'lethean', 'snider', 'darbs', 'charon', 'cladius',
'athena', 'hypnos', 'clotho', 'testnet', 'gateway',
'vpn', 'dns', 'proxy', 'explorer', 'trade',
];
for (const name of candidates) {
try {
const resource = await rpcHSD('getnameresource', [name]);
if (resource && resource.records && resource.records.length > 0) {
names.push({ name, records: resource.records });
}
} catch {
// Name not registered — skip
}
}
return { height, names };
}
function recordToZone(name, record) {
const lines = [];
switch (record.type) {
case 'GLUE4':
// GLUE4 = NS + A glue record
lines.push(`${name} IN NS ${record.ns}`);
// Extract subdomain from ns for glue
const nsSub = record.ns.replace(/\.$/, '');
lines.push(`${nsSub} IN A ${record.address}`);
break;
case 'GLUE6':
lines.push(`${name} IN NS ${record.ns}`);
const ns6Sub = record.ns.replace(/\.$/, '');
lines.push(`${ns6Sub} IN AAAA ${record.address}`);
break;
case 'NS':
lines.push(`${name} IN NS ${record.ns}`);
break;
case 'TXT':
for (const txt of record.txt || []) {
lines.push(`${name} IN TXT "${txt}"`);
}
break;
case 'DS':
lines.push(`; DS record for ${name} (DNSSEC delegation)`);
break;
default:
lines.push(`; Unknown record type ${record.type} for ${name}`);
}
return lines;
}
async function generateZone() {
const { height, names } = await getAllNames();
const serial = `${new Date().toISOString().slice(0,10).replace(/-/g,'')}${String(height).padStart(4,'0')}`;
const lines = [
`; .lthn zone — auto-generated from HSD sidechain (height ${height})`,
`; Generated: ${new Date().toISOString()}`,
`; Source: ${HSD_URL}`,
`; DO NOT EDIT — this file is overwritten by dump-zone.js`,
``,
`$ORIGIN lthn.`,
`$TTL 300`,
``,
`@ IN SOA ns5.lthn.io. admin.lthn.io. (`,
` ${serial} ; serial (date + chain height)`,
` 3600 ; refresh`,
` 600 ; retry`,
` 86400 ; expire`,
` 300 ) ; minimum`,
``,
`; Nameservers (CoreDNS instances serving this zone)`,
` IN NS ns5.lthn.io.`,
` IN NS ns6.lthn.io.`,
``,
`; === Names from HSD sidechain ===`,
];
if (names.length === 0) {
lines.push(`; No names registered on sidechain yet`);
}
for (const { name, records } of names) {
lines.push(``);
lines.push(`; ${name}.lthn`);
for (const record of records) {
lines.push(...recordToZone(name, record));
}
}
lines.push(``);
lines.push(`; === End of auto-generated zone ===`);
return lines.join('\n');
}
generateZone().then(zone => {
process.stdout.write(zone + '\n');
}).catch(err => {
process.stderr.write(`Error: ${err.message}\n`);
process.exit(1);
});