@agentcommunity/aid-engine
Version:
Core engine for Agent Identity & Discovery (AID) validation and discovery
463 lines (455 loc) • 17.4 kB
JavaScript
var aid = require('@agentcommunity/aid');
var tls = require('tls');
var url = require('url');
var crypto = require('crypto');
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
var tls__default = /*#__PURE__*/_interopDefault(tls);
// src/checker.ts
async function runBaseDiscovery(domain, options) {
try {
const res = await aid.discover(domain, {
protocol: options.protocol,
timeout: options.timeoutMs,
wellKnownFallback: options.allowFallback,
wellKnownTimeoutMs: options.wellKnownTimeoutMs
});
return { ok: true, value: res };
} catch (e) {
return { ok: false, error: e };
}
}
async function inspectTls(uri, timeoutMs = 5e3) {
const u = new url.URL(uri);
const host = u.hostname;
const port = u.port ? Number(u.port) : 443;
return await new Promise((resolve, reject) => {
const socket = tls__default.default.connect(
{
host,
port,
servername: host,
timeout: timeoutMs,
rejectUnauthorized: true
},
() => {
const peer = socket.getPeerCertificate(true);
const issuer = peer?.issuer?.CN || peer?.issuer?.commonName || null;
const san = typeof peer?.subjectaltname === "string" ? peer.subjectaltname.split(",").map((s) => s.trim()).filter(Boolean) : null;
const validFrom = peer?.valid_from ? new Date(peer.valid_from).toISOString() : null;
const validTo = peer?.valid_to ? new Date(peer.valid_to).toISOString() : null;
let daysRemaining = null;
if (validTo) {
const ms = new Date(validTo).getTime() - Date.now();
daysRemaining = Math.floor(ms / (24 * 3600 * 1e3));
}
resolve({ host, sni: host, issuer, san, validFrom, validTo, daysRemaining });
socket.end();
}
);
socket.on("error", (err) => reject(err));
socket.on("timeout", () => {
socket.destroy();
reject(new Error("TLS timeout"));
});
});
}
// src/dnssec.ts
async function probeDnssecRrsigTxt(name, doh = "https://cloudflare-dns.com/dns-query", timeoutMs = 5e3) {
const url = new URL(doh);
url.searchParams.set("name", name);
url.searchParams.set("type", "RRSIG");
const controller = new AbortController();
const id = setTimeout(() => controller.abort(), timeoutMs);
try {
const res = await fetch(url.toString(), {
headers: { Accept: "application/dns-json" },
signal: controller.signal
});
if (!res.ok) return { present: false, method: "RRSIG", proof: null };
const json = await res.json();
const answers = json.Answer ?? [];
const hasRrsig = answers.some((a) => a.type === 46);
return { present: hasRrsig, method: "RRSIG", proof: hasRrsig ? answers : null };
} catch {
return { present: false, method: "RRSIG", proof: null };
} finally {
clearTimeout(id);
}
}
async function runProtocolProbe(domain, protocol, timeoutMs) {
const name = `_agent._${protocol}.${domain}`;
try {
const res = await aid.discover(name, {
timeout: timeoutMs,
wellKnownFallback: false
// Probes are DNS-only
});
return {
attempt: {
name,
type: "TXT",
result: "NOERROR",
ttl: res.ttl,
byteLength: new TextEncoder().encode(res.raw).length
}
};
} catch (e) {
const error = e;
if (error.errorCode === "ERR_NO_RECORD") {
return { attempt: { name, type: "TXT", result: "NXDOMAIN" }, error };
}
return {
attempt: { name, type: "TXT", result: "ERROR", reason: error.errorCode || "unknown" },
error
};
}
}
// src/error_messages.ts
var ERROR_MESSAGES = {
// General
UNKNOWN_ERROR: "An unexpected error occurred. Please check your input and try again.",
// DNS
DNS_LOOKUP_FAILED: "DNS lookup failed for the specified domain. Check network connectivity and domain spelling.",
NO_RECORD_FOUND: "No AID TXT record found for the domain. Ensure the record exists at _agent.<domain>.",
// Record validation
INVALID_TXT_FORMAT: "The AID TXT record has an invalid format. Ensure it follows v=aid1;key=value;... structure.",
UNSUPPORTED_PROTOCOL: "The specified protocol is not supported. See the official protocol registry for valid tokens.",
DEPRECATED_RECORD: "The AID record has been deprecated. Check the deprecation date and update accordingly.",
// Security
SECURITY_VIOLATION: "A security check failed. The record or endpoint may be compromised.",
TLS_VALIDATION_FAILED: "TLS certificate validation failed. Ensure the certificate is valid and not expired.",
PKA_HANDSHAKE_FAILED: "PKA endpoint proof handshake failed. Verify the public key and private key configuration.",
// Fallback
FALLBACK_FAILED: "The .well-known fallback failed. Ensure the HTTPS endpoint returns valid JSON.",
// Byte limits
BYTE_LIMIT_EXCEEDED: "The record exceeds the 255-byte DNS limit. Use aliases and shorten fields.",
// Warnings
BYTE_LIMIT_WARNING: "Record size is close to the 255-byte limit. Consider using aliases.",
TLS_EXPIRING_SOON: "TLS certificate expires soon. Renew to avoid interruptions.",
DNSSEC_NOT_DETECTED: "DNSSEC not detected. Enable for better integrity.",
PKA_NOT_PRESENT: "Endpoint proof (PKA) not present. Consider adding for security.",
DOWNGRADE_DETECTED: "Security downgrade detected: a previously present PKA or KID has been removed.",
// Recommendations
ENABLE_DNSSEC: "Enable DNSSEC at your domain registrar to improve DNS integrity.",
ADD_PKA: "Add PKA endpoint proof by running 'aid-doctor pka generate'.",
RENEW_TLS: "Renew your TLS certificate soon to avoid expiration.",
USE_ALIASES: "Use single-letter aliases (e.g., u for uri) to reduce record size."
};
// src/checker.ts
function initReport(domain, protocol) {
return {
domain,
queried: {
strategy: "base-first",
hint: { proto: protocol, source: "cli", present: Boolean(protocol) },
attempts: [],
wellKnown: {
attempted: false,
used: false,
url: null,
httpStatus: null,
contentType: null,
byteLength: null,
status: null,
snippet: null
}
},
record: { raw: null, parsed: null, valid: false, warnings: [], errors: [] },
dnssec: { present: false, method: "RRSIG", proof: null },
tls: {
checked: false,
valid: null,
host: null,
sni: null,
issuer: null,
san: null,
validFrom: null,
validTo: null,
daysRemaining: null,
redirectBlocked: null
},
pka: {
present: false,
attempted: false,
verified: null,
kid: null,
alg: null,
createdSkewSec: null,
covered: null
},
downgrade: { checked: false, previous: null, status: null },
exitCode: 1,
cacheEntry: null
};
}
async function runCheck(domain, opts) {
const report = initReport(domain, opts.protocol);
try {
const dnsRes = await runBaseDiscovery(domain, {
...opts.protocol && { protocol: opts.protocol },
timeoutMs: opts.timeoutMs,
allowFallback: opts.allowFallback,
wellKnownTimeoutMs: opts.wellKnownTimeoutMs
});
const value = dnsRes.value;
const queryName = value.queryName;
const attempt = {
name: queryName,
type: "TXT",
result: "NOERROR",
ttl: value.ttl
};
report.queried.attempts.push(attempt);
if (value.queryName.startsWith("https")) {
report.queried.wellKnown.used = true;
report.queried.wellKnown.attempted = true;
report.queried.wellKnown.url = value.queryName;
}
if (opts.protocol && opts.probeProtoEvenIfBase) {
const probeRes = await runProtocolProbe(domain, opts.protocol, opts.timeoutMs);
report.queried.attempts.push(probeRes.attempt);
if (!probeRes.error) {
report.record.warnings.push({
code: "PROTOCOL_SUBDOMAIN_EXISTS",
message: `A record exists at the protocol-specific subdomain _agent._${opts.protocol}.${domain}, which may differ from the base record.`
});
}
}
const record = value.record;
report.record.parsed = record;
report.record.valid = true;
report.record.raw = (() => {
const parts = ["v=aid1"];
if (record.uri) parts.push(`u=${record.uri}`);
if (record.proto) parts.push(`p=${record.proto}`);
if (record.auth) parts.push(`a=${record.auth}`);
if (record.desc) parts.push(`s=${record.desc}`);
if (record.docs) parts.push(`d=${record.docs}`);
if (record.dep) {
const depDate = new Date(record.dep);
if (depDate.getTime() < Date.now()) {
report.record.errors.push({
code: "DEPRECATED",
message: ERROR_MESSAGES.DEPRECATED_RECORD
});
report.record.valid = false;
report.exitCode = 1001;
} else {
report.record.warnings.push({
code: "DEPRECATION_SCHEDULED",
message: `Record is scheduled for deprecation on ${record.dep}`
});
}
}
if (record.pka) parts.push(`k=${record.pka}`);
if (record.kid) parts.push(`i=${record.kid}`);
return parts.join(";");
})();
const byteLen = new TextEncoder().encode(report.record.raw).length;
if (byteLen > 255) {
report.record.warnings.push({
code: "BYTE_LIMIT",
message: ERROR_MESSAGES.BYTE_LIMIT_EXCEEDED
});
}
const skipSecurity = typeof process !== "undefined" && process.env && process.env.AID_SKIP_SECURITY === "1";
if (!skipSecurity && record.proto !== "local" && record.proto !== "zeroconf") {
try {
await aid.enforceRedirectPolicy(record.uri, opts.timeoutMs);
if (record.uri.startsWith("https://")) {
const tlsInfo = await inspectTls(record.uri, opts.timeoutMs);
report.tls.checked = true;
report.tls.valid = true;
report.tls.host = tlsInfo.host;
report.tls.sni = tlsInfo.sni;
report.tls.issuer = tlsInfo.issuer;
report.tls.san = tlsInfo.san;
report.tls.validFrom = tlsInfo.validFrom;
report.tls.validTo = tlsInfo.validTo;
report.tls.daysRemaining = tlsInfo.daysRemaining;
if (tlsInfo.daysRemaining !== null && tlsInfo.daysRemaining < 21) {
report.record.warnings.push({
code: "TLS_EXPIRING",
message: ERROR_MESSAGES.TLS_EXPIRING_SOON
});
}
} else {
report.tls.checked = false;
report.tls.valid = null;
}
} catch (e) {
const err = e;
report.tls.checked = true;
report.tls.valid = false;
report.record.errors.push({ code: err.errorCode ?? "ERR_SECURITY", message: err.message });
report.exitCode = err instanceof aid.AidError ? err.code : 1003;
return report;
}
}
try {
const r = await probeDnssecRrsigTxt(value.queryName);
report.dnssec.present = r.present;
report.dnssec.proof = r.proof;
} catch {
}
if (record.pka && record.kid) {
report.pka.present = true;
report.pka.kid = record.kid;
report.pka.alg = "ed25519";
report.pka.attempted = true;
try {
await aid.performPKAHandshake(record.uri, record.pka, record.kid);
report.pka.verified = true;
} catch (e) {
const err = e;
report.pka.verified = false;
report.record.errors.push({
code: err.errorCode ?? "ERR_SECURITY",
message: ERROR_MESSAGES.PKA_HANDSHAKE_FAILED
});
report.exitCode = err instanceof aid.AidError ? err.code : 1003;
return report;
}
}
if (opts.checkDowngrade) {
report.downgrade.checked = true;
if (opts.previousCacheEntry) {
const prev = opts.previousCacheEntry;
report.downgrade.previous = { pka: prev.pka, kid: prev.kid };
const nowPka = record.pka ?? null;
const nowKid = record.kid ?? null;
if (prev.pka && !nowPka) {
report.downgrade.status = "downgrade";
report.record.warnings.push({
code: "DOWNGRADE",
message: ERROR_MESSAGES.DOWNGRADE_DETECTED
});
} else if (prev.pka && nowPka && prev.pka !== nowPka) {
report.downgrade.status = "downgrade";
report.record.warnings.push({
code: "DOWNGRADE",
message: ERROR_MESSAGES.DOWNGRADE_DETECTED
});
} else if (prev.kid && nowKid && prev.kid !== nowKid) {
report.downgrade.status = "downgrade";
report.record.warnings.push({
code: "DOWNGRADE",
message: ERROR_MESSAGES.DOWNGRADE_DETECTED
});
} else {
report.downgrade.status = "no_change";
}
} else {
report.downgrade.status = "first_seen";
}
report.cacheEntry = {
lastSeen: (/* @__PURE__ */ new Date()).toISOString(),
pka: record.pka ?? null,
kid: record.kid ?? null
};
}
report.exitCode = 0;
return report;
} catch (e) {
const err = e;
report.exitCode = err instanceof aid.AidError ? err.code : 1e3;
report.record.errors.push({ code: err.errorCode ?? "ERR_NO_RECORD", message: err.message });
if (err.errorCode === "ERR_FALLBACK_FAILED" && err.details) {
report.queried.wellKnown.attempted = true;
report.queried.wellKnown.httpStatus = err.details.httpStatus;
report.queried.wellKnown.contentType = err.details.contentType;
report.queried.wellKnown.snippet = err.details.snippet;
report.queried.wellKnown.byteLength = err.details.byteLength;
}
if (opts.protocol && opts.probeProtoSubdomain) {
const probeRes = await runProtocolProbe(domain, opts.protocol, opts.timeoutMs);
report.queried.attempts.push(probeRes.attempt);
}
return report;
}
}
function buildTxtRecordVariant(formData, useAliases) {
const parts = ["v=aid1"];
const k = (full, alias) => useAliases ? alias : full;
if (formData.uri) parts.push(`${k("uri", "u")}=${formData.uri}`);
if (formData.proto) parts.push(`${k("proto", "p")}=${formData.proto}`);
if (formData.auth) parts.push(`${k("auth", "a")}=${formData.auth}`);
if (formData.desc) parts.push(`${k("desc", "s")}=${formData.desc}`);
if (formData.docs) parts.push(`${k("docs", "d")}=${formData.docs}`);
if (formData.dep) parts.push(`${k("dep", "e")}=${formData.dep}`);
if (formData.pka) parts.push(`${k("pka", "k")}=${formData.pka}`);
if (formData.kid) parts.push(`${k("kid", "i")}=${formData.kid}`);
return parts.join(";");
}
function buildTxtRecord(formData) {
const full = buildTxtRecordVariant(formData, false);
const alias = buildTxtRecordVariant(formData, true);
return new TextEncoder().encode(alias).length <= new TextEncoder().encode(full).length ? alias : full;
}
function validateTxtRecord(record) {
try {
aid.parse(record);
return { isValid: true };
} catch (error) {
return { isValid: false, error: error instanceof Error ? error.message : "Invalid" };
}
}
function base58btcEncode(bytes) {
const ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
let zeros = 0;
while (zeros < bytes.length && bytes[zeros] === 0) zeros++;
const size = Math.ceil(bytes.length * Math.log(256) / Math.log(58)) + 1;
const b = new Uint8Array(size);
let length = 0;
for (let i = zeros; i < bytes.length; i++) {
let carry = bytes[i];
let j = size - 1;
while (carry !== 0 || j >= size - length) {
carry += 256 * b[j];
b[j] = carry % 58;
carry = Math.floor(carry / 58);
j--;
}
length = size - 1 - j;
}
let it = size - length;
while (it < size && b[it] === 0) it++;
let out = "1".repeat(zeros);
for (let i = it; i < size; i++) out += ALPHABET[b[i]];
return out;
}
async function generateEd25519KeyPair() {
const kp = await crypto.webcrypto.subtle.generateKey("Ed25519", true, [
"sign",
"verify"
]);
const rawPub = new Uint8Array(await crypto.webcrypto.subtle.exportKey("raw", kp.publicKey));
const pkcs8 = new Uint8Array(await crypto.webcrypto.subtle.exportKey("pkcs8", kp.privateKey));
const pubMb = "z" + base58btcEncode(rawPub);
const pem = "-----BEGIN PRIVATE KEY-----\n" + Buffer.from(pkcs8).toString("base64") + "\n-----END PRIVATE KEY-----\n";
return {
publicKey: pubMb,
privateKeyPem: pem,
privateKeyBytes: pkcs8
};
}
function verifyPka(pka) {
if (!pka || pka[0] !== "z") return { valid: false, reason: "Missing z multibase prefix" };
const s = pka.slice(1);
const ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
for (const c of s)
if (!ALPHABET.includes(c)) return { valid: false, reason: "Invalid base58 character" };
const approx = Math.floor(s.length * Math.log(58) / Math.log(256));
if (approx !== 32 && approx !== 33) return { valid: false, reason: "Unexpected key length" };
return { valid: true };
}
exports.ERROR_MESSAGES = ERROR_MESSAGES;
exports.buildTxtRecord = buildTxtRecord;
exports.buildTxtRecordVariant = buildTxtRecordVariant;
exports.generateEd25519KeyPair = generateEd25519KeyPair;
exports.runCheck = runCheck;
exports.validateTxtRecord = validateTxtRecord;
exports.verifyPka = verifyPka;
//# sourceMappingURL=index.cjs.map
//# sourceMappingURL=index.cjs.map
;