alias-dns-bridge/resolver.js

183 lines
6.2 KiB
JavaScript
Raw Permalink Normal View History

#!/usr/bin/env node
/**
* Lethean Alias-HNS Resolver
*
* Bridges the main chain alias system to the HNS sidechain.
* For each alias with v=lthn1;hns=name.lthn comment:
* 1. Queries HSD for the name's resource records
* 2. Verifies the TXT record contains the matching lthn-address
* 3. Returns the verified binding (alias HNS name DNS records)
*
* Usage:
* node resolver.js [--daemon http://127.0.0.1:46941] [--hsd http://127.0.0.1:14037]
*/
const DAEMON_URL = process.env.DAEMON_URL || 'http://127.0.0.1:46941';
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 rpcDaemon(method, params = {}) {
const resp = await fetch(`${DAEMON_URL}/json_rpc`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ jsonrpc: '2.0', id: '0', method, params })
});
const data = await resp.json();
if (data.error) throw new Error(`Daemon ${method}: ${data.error.message}`);
return data.result;
}
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 hsdGetNameResource(name) {
try {
return await rpcHSD('getnameresource', [name]);
} catch {
return null;
}
}
function parseComment(comment) {
if (!comment || !comment.includes('v=lthn1')) return null;
const fields = {};
comment.split(';').forEach(pair => {
const [key, ...rest] = pair.trim().split('=');
if (key && rest.length) fields[key.trim()] = rest.join('=').trim();
});
if (fields.v !== 'lthn1') return null;
return {
hns: fields.hns || null, // explicit override, usually derived from alias name
type: fields.type || 'user',
capabilities: fields.cap ? fields.cap.split(',') : [],
tls: fields.tls || null,
};
}
/**
* Resolve and verify a single alias HNS binding
*/
async function resolveAlias(alias) {
// 1. Get alias details from main chain
const result = await rpcDaemon('get_alias_details', { alias: alias.alias || alias });
if (result.status !== 'OK') return { alias: alias.alias || alias, verified: false, reason: 'alias not found' };
const details = result.alias_details;
const parsed = parseComment(details.comment);
if (!parsed) return { alias: alias.alias || alias, verified: false, reason: 'no v=lthn1 comment' };
// 2. HNS name: explicit from hns= field, or derived from alias name
// @charon → charon.lthn (implicit), hns=custom.lthn (explicit override)
const aliasName = alias.alias || alias;
const hnsFullName = parsed.hns || `${aliasName}.lthn`;
const hnsName = hnsFullName.replace(/\.lthn$/, '');
// 3. Query HSD for the name's resource records
const resource = await hsdGetNameResource(hnsName);
if (!resource || !resource.records) return { alias: alias.alias || alias, hns: parsed.hns, verified: false, reason: 'HNS name not found or no records' };
// 4. Find TXT record with lthn-address
let hnsAddress = null;
let dnsRecords = [];
for (const record of resource.records) {
dnsRecords.push(record);
if (record.type === 'TXT' && record.txt) {
for (const txt of record.txt) {
const match = txt.match(/lthn-address=([a-zA-Z0-9]+)/);
if (match) hnsAddress = match[1];
}
}
}
// 5. Verify the chain address matches the HNS TXT record
const verified = hnsAddress === details.address;
return {
alias: alias.alias || alias,
address: details.address,
hns: parsed.hns,
hnsName,
type: parsed.type,
capabilities: parsed.capabilities,
tls: parsed.tls,
dnsRecords,
hnsAddress,
verified,
reason: verified ? 'chain address matches HNS TXT record' : `mismatch: chain=${details.address?.slice(0,16)} hns=${hnsAddress?.slice(0,16) || 'none'}`,
};
}
/**
* Resolve all aliases and verify HNS bindings
*/
async function resolveAll() {
const chainInfo = await rpcDaemon('getinfo');
console.log(`Chain: height=${chainInfo.height}, aliases=${chainInfo.alias_count}`);
let hsdInfo;
try {
hsdInfo = await rpcHSD('getinfo');
console.log(`HSD: height=${hsdInfo.blocks}, network=${hsdInfo.network}`);
} catch (e) {
console.log(`HSD: not reachable (${e.message})`);
}
if (chainInfo.alias_count === 0) {
console.log('\nNo aliases registered yet (requires HF4)');
console.log('When aliases exist, this resolver will:');
console.log(' 1. Read all aliases from chain');
console.log(' 2. Parse v=lthn1 comments for HNS bindings');
console.log(' 3. Query HSD for each name\'s DNS records');
console.log(' 4. Verify chain address matches HNS TXT lthn-address');
console.log(' 5. Output verified bindings as JSON');
return;
}
const allAliases = await rpcDaemon('get_all_alias_details', {});
const aliases = allAliases.aliases || [];
const results = [];
for (const alias of aliases) {
const parsed = parseComment(alias.comment);
if (parsed) { // v=lthn1 present — HNS name derived from alias if not explicit
const resolved = await resolveAlias(alias);
results.push(resolved);
}
}
console.log(`\nResolved ${results.length} aliases with HNS bindings:`);
for (const r of results) {
const status = r.verified ? '✓ VERIFIED' : '✗ UNVERIFIED';
console.log(` ${r.alias}.lthn → ${r.hns} [${status}] ${r.reason}`);
if (r.dnsRecords) {
for (const rec of r.dnsRecords) {
console.log(` ${rec.type}: ${JSON.stringify(rec).slice(0, 80)}`);
}
}
}
// Write registry
const fs = await import('fs');
fs.writeFileSync('/tmp/lthn-resolved-registry.json', JSON.stringify({
timestamp: new Date().toISOString(),
chain: { height: chainInfo.height, aliases: chainInfo.alias_count },
hsd: hsdInfo ? { height: hsdInfo.blocks, network: hsdInfo.network } : null,
bindings: results,
}, null, 2));
console.log('\nRegistry written to /tmp/lthn-resolved-registry.json');
}
resolveAll().catch(console.error);