alias-dns-bridge/zone-cache.cjs

151 lines
5.1 KiB
JavaScript
Raw Permalink Normal View History

#!/usr/bin/env node
/**
* HSD Zone Cache on-demand query with SOA-based invalidation
*
* Instead of periodic dumps, this:
* 1. Queries HSD tree root hash every N seconds
* 2. If tree root changed regenerate zone from HSD
* 3. If unchanged serve cached zone
* 4. CoreDNS reads the cached zone file
*
* The zone file is ONLY written from HSD RPC data.
* No manual edits. The sidechain IS the source of truth.
*
* Usage:
* node zone-cache.js
* # Runs as daemon, writes to /opt/noc/coredns/db.lthn.auto
*/
const fs = require('fs');
const { execFileSync } = require('child_process');
const HSD_URL = process.env.HSD_URL || 'http://127.0.0.1:14037';
const HSD_API_KEY = process.env.HSD_API_KEY || 'testkey';
const ZONE_FILE = process.env.ZONE_FILE || '/opt/noc/coredns/db.lthn.auto';
const CHECK_INTERVAL = parseInt(process.env.CHECK_INTERVAL || '15') * 1000;
// Known names to query — in production, scan the UTXO set
const KNOWN_NAMES = [
'lethean', 'snider', 'darbs', 'charon', 'cladius',
'explorer', 'testnet', 'trading', 'gateway', 'network',
'monitor', 'storage', 'support', 'trade',
'builder', 'develop', 'miners', 'relayer',
];
let lastTreeRoot = null;
let lastZone = null;
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(data.error.message);
return data.result;
}
async function getTreeRoot() {
const info = await rpcHSD('getinfo');
return { height: info.blocks, treeRoot: info.treeroot || 'unknown' };
}
async function generateZone(height) {
const serial = `${new Date().toISOString().slice(0, 10).replace(/-/g, '')}${String(height).padStart(4, '0')}`;
const lines = [
`; .lthn zone — generated from HSD sidechain (height ${height})`,
`; Generated: ${new Date().toISOString()}`,
`; Source: ${HSD_URL} (READ-ONLY — do not edit this file)`,
`; Invalidation: SOA changes when HSD tree root changes`,
``,
`lthn. 300 IN SOA ns5.lthn.io. admin.lthn.io. ${serial} 3600 600 86400 300`,
`lthn. 300 IN NS ns5.lthn.io.`,
`lthn. 300 IN NS ns6.lthn.io.`,
``,
`; === Records from HSD sidechain (source of truth) ===`,
];
for (const name of KNOWN_NAMES) {
try {
const resource = await rpcHSD('getnameresource', [name]);
if (resource && resource.records && resource.records.length > 0) {
lines.push(`; ${name}.lthn`);
for (const record of resource.records) {
switch (record.type) {
case 'GLUE4':
lines.push(`${name}.lthn. 300 IN NS ${record.ns}`);
lines.push(`${record.ns.replace(/\.$/, '')}.lthn. 300 IN A ${record.address}`);
break;
case 'GLUE6':
lines.push(`${name}.lthn. 300 IN NS ${record.ns}`);
lines.push(`${record.ns.replace(/\.$/, '')}.lthn. 300 IN AAAA ${record.address}`);
break;
case 'TXT':
for (const txt of record.txt || []) lines.push(`${name}.lthn. 300 IN TXT "${txt}"`);
break;
case 'DS':
lines.push(`${name}.lthn. 300 IN DS ${record.keyTag} ${record.algorithm} ${record.digestType} ${record.digest}`);
break;
case 'NS':
lines.push(`${name}.lthn. 300 IN NS ${record.ns}`);
break;
}
}
}
} catch { /* name not registered — skip */ }
}
lines.push(``, `; === End (${KNOWN_NAMES.length} names checked) ===`);
return lines.join('\n') + '\n';
}
async function checkAndUpdate() {
try {
const { height, treeRoot } = await getTreeRoot();
if (treeRoot === lastTreeRoot) {
return; // No change — cache is valid
}
console.log(`[${new Date().toISOString().slice(11, 19)}] Tree root changed at height ${height} — regenerating zone`);
const zone = await generateZone(height);
// Atomic write — temp file then rename
const tmpFile = `${ZONE_FILE}.tmp`;
fs.writeFileSync(tmpFile, zone);
// Validate before replacing
if (!zone.includes('$ORIGIN lthn.')) {
console.error('Zone validation failed — not replacing');
fs.unlinkSync(tmpFile);
return;
}
fs.renameSync(tmpFile, ZONE_FILE);
lastTreeRoot = treeRoot;
lastZone = zone;
const recordCount = (zone.match(/\tIN\s/g) || []).length;
console.log(` Zone updated: ${recordCount} records, serial based on height ${height}`);
} catch (err) {
console.error(` Error: ${err.message} — keeping cached zone`);
}
}
console.log('HSD Zone Cache');
console.log(` HSD: ${HSD_URL}`);
console.log(` Zone: ${ZONE_FILE}`);
console.log(` Check: every ${CHECK_INTERVAL / 1000}s`);
console.log(` Source: HSD sidechain ONLY (read-only output)`);
console.log('');
// Initial generation
checkAndUpdate();
// Polling loop — checks tree root, only regenerates on change
setInterval(checkAndUpdate, CHECK_INTERVAL);