Skip to content

Instantly share code, notes, and snippets.

@silversword411
Created December 16, 2025 17:32
Show Gist options
  • Select an option

  • Save silversword411/a9b83b53d445de04d2773edf79a24317 to your computer and use it in GitHub Desktop.

Select an option

Save silversword411/a9b83b53d445de04d2773edf79a24317 to your computer and use it in GitHub Desktop.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Domain Analysis (RDAP + DoH) v1.0</title>
<style>
body{font-family:system-ui,Segoe UI,Roboto,Arial,sans-serif;margin:0;line-height:1.35;overflow-x:hidden}
.container{max-width:100%;margin:0 auto;padding:20px 16px}
input,button,textarea{font:inherit}
.row{display:flex;gap:12px;flex-wrap:wrap;align-items:flex-start}
.row > *{margin:4px 0}
input[type=text]{padding:10px;min-width:280px;border:1px solid #ddd;border-radius:8px;width:100%;box-sizing:border-box;display:block}
textarea{padding:10px;width:100%;min-height:96px;border:1px solid #ddd;border-radius:8px;box-sizing:border-box;display:block}
button{padding:10px 14px;cursor:pointer;border:1px solid #ddd;border-radius:8px;background:#fff}
button:hover{background:#f7f7f7}
.form-card{border:1px solid #ddd;border-radius:12px;padding:12px;background:#fff;margin-top:12px;box-sizing:border-box;width:100%}
.form-grid{display:grid;grid-template-columns:1fr 1fr;gap:12px}
.form-field{display:flex;flex-direction:column}
.form-field label{font-weight:600;color:#333;margin-bottom:6px}
.hint{color:#666;font-size:12px;margin-top:6px}
.actions{display:flex;gap:10px;align-items:center;margin-top:8px;flex-wrap:wrap}
@media (max-width: 900px){
.form-grid{grid-template-columns:1fr}
.actions{flex-wrap:wrap}
}
.grid{display:grid;grid-template-columns:1fr;gap:16px;margin-top:16px;width:100%;box-sizing:border-box}
.card{border:1px solid #ddd;border-radius:10px;padding:12px;box-sizing:border-box;max-width:100%}
.card h2{margin:0 0 8px 0;font-size:18px}
pre{background:#f7f7f7;border:1px solid #eee;border-radius:8px;padding:10px;overflow:auto;white-space:pre-wrap;word-wrap:break-word;word-break:break-word;overflow-wrap:anywhere;max-width:100%;box-sizing:border-box}
.muted{color:#666;font-size:13px;line-height:1.4}
.ok{color:#0a7}
.warn{color:#b80}
.err{color:#c00}
.pill{display:inline-block;border:1px solid #ddd;border-radius:999px;padding:2px 8px;margin-right:6px;font-size:12px;color:#444}
.expires-soon{background:#fff3cd;color:#856404;font-weight:bold;padding:2px 4px;border-radius:3px}
.expires-critical{background:#f8d7da;color:#721c24;font-weight:bold;padding:2px 4px;border-radius:3px}
/* Responsive domain summary cards */
.domains-grid{display:block;width:100%;min-width:0}
.domain-card{border:1px solid #e5e5e5;border-radius:10px;padding:12px;background:#fff;box-sizing:border-box;min-width:0;width:100%;max-width:100%;margin-bottom:12px;overflow:hidden}
.domain-card + .domain-card{margin-top:8px}
.domain-title{display:flex;align-items:center;justify-content:space-between;margin:0 0 8px 0}
.domain-name{font-weight:600;color:#222}
.badge{border-radius:999px;padding:2px 8px;font-size:12px;border:1px solid #eee}
.badge-ok{background:#e9fbf5;color:#0a7;border-color:#c9f3e6}
.badge-warn{background:#fff6e6;color:#b80;border-color:#ffe3b3}
.badge-err{background:#fdeceb;color:#c00;border-color:#f6c8c5}
.issues-section ul{margin:6px 0 0 0;padding-left:18px}
.issues-section{word-wrap:break-word;word-break:break-word;overflow-wrap:anywhere}
/* Summary: always one domain per line */
</style>
</head>
<body>
<div class="container">
<h1>Domain Analysis v1.0</h1>
<div class="muted">
Runs entirely in the browser using <span class="pill">RDAP</span> and <span class="pill">DNS-over-HTTPS</span>.
Some TLDs don’t publish all dates/registrar details via RDAP.
</div>
<div class="form-card">
<div class="form-grid">
<div class="form-field">
<label for="domain">Domains</label>
<textarea id="domain" placeholder="example.com\nexample.org"></textarea>
<div class="hint">Paste list: comma, newline, space. Wildcards and URLs are normalized.</div>
<div id="domainsPreview" class="muted"></div>
</div>
<div class="form-field">
<label for="selectors">DKIM selectors</label>
<textarea id="selectors">selector1,selector2,default,google,s1,s2,k1,smtp,mail</textarea>
<div class="hint">Accepts comma/newline/space; trimmed, blanks ignored, deduped.</div>
<div id="selectorsPreview" class="muted"></div>
</div>
</div>
<div class="actions">
<button id="run">Run</button>
<button id="copy">Copy JSON</button>
</div>
</div>
<div id="status" class="muted" style="margin-top:6px"></div>
<div class="grid">
<div class="card">
<h2>Summary</h2>
<div id="issues"></div>
</div>
<div class="card">
<h2>RDAP (Registrar / Dates)</h2>
<pre id="rdap"></pre>
</div>
<div class="card">
<h2>DNS</h2>
<pre id="dns"></pre>
</div>
<div class="card">
<h2>Raw JSON</h2>
<pre id="raw"></pre>
</div>
</div>
</div>
<script>
// --- helpers ---
const $ = (id) => document.getElementById(id);
function normalizeDomain(d) {
d = (d || "").trim().toLowerCase();
d = d.replace(/^\*\./, "");
d = d.replace(/^https?:\/\//, "");
d = d.split("/")[0];
d = d.replace(/\.$/, "");
return d;
}
function uniq(arr) {
return [...new Set((arr || []).filter(x => x && String(x).trim()))];
}
function parseList(text) {
return uniq((text || "")
.split(/[\s,;]+/)
.map(s => s.trim())
.filter(Boolean));
}
function parseSelectors(text) {
// Supports comma, newline, semicolon, tab, and spaces
return parseList(text);
}
function renderIssues(issues) {
if (!issues.length) return `<div class="ok">No obvious issues found.</div>`;
// Categorize issues
const errors = [];
const warnings = [];
const info = [];
issues.forEach(i => {
let msg = i;
if (i.startsWith("OK:")) {
msg = i.substring(3);
info.push(msg);
} else if (i.startsWith("ERR:") || i.startsWith("❌")) {
msg = i.startsWith("ERR:") ? i.substring(4) : i;
errors.push(msg);
} else if (i.startsWith("⚠️")) {
warnings.push(i);
} else if (i.startsWith("ℹ️")) {
info.push(i);
} else {
warnings.push(i);
}
});
let html = '';
if (errors.length) {
html += `<h3 style="color:#c00;margin:8px 0 4px 0">Errors</h3>`;
html += `<ul>` + errors.map(msg => `<li class="err">${escapeHtml(msg)}</li>`).join("") + `</ul>`;
}
if (warnings.length) {
html += `<h3 style="color:#b80;margin:8px 0 4px 0">Warnings</h3>`;
html += `<ul>` + warnings.map(msg => `<li class="warn">${escapeHtml(msg)}</li>`).join("") + `</ul>`;
}
if (info.length) {
html += `<h3 style="color:#0a7;margin:8px 0 4px 0">Info</h3>`;
html += `<ul>` + info.map(msg => `<li class="ok">${escapeHtml(msg)}</li>`).join("") + `</ul>`;
}
return html;
}
function escapeHtml(s) {
return String(s).replace(/[&<>"']/g, c => ({
"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"
}[c]));
}
function checkExpirationWarning(expiresDate) {
if (!expiresDate) return null;
const now = new Date();
const expires = new Date(expiresDate);
const diffMs = expires - now;
const diffDays = diffMs / (1000 * 60 * 60 * 24);
if (diffDays < 0) return 'expired';
if (diffDays <= 30) return 'critical'; // 1 month
if (diffDays <= 90) return 'soon'; // 3 months
return null;
}
function formatDateWithWarning(dateStr) {
if (!dateStr) return 'null';
const warning = checkExpirationWarning(dateStr);
const escaped = escapeHtml(dateStr);
if (warning === 'critical') {
return `<span class="expires-critical">${escaped}</span>`;
} else if (warning === 'soon') {
return `<span class="expires-soon">${escaped}</span>`;
} else if (warning === 'expired') {
return `<span class="err">${escaped} (EXPIRED)</span>`;
}
return escaped;
}
// --- DNS-over-HTTPS ---
// Using Google's DoH JSON endpoint for simplicity:
// https://dns.google/resolve?name=example.com&type=MX
async function dohQuery(name, type) {
const url = `https://dns.google/resolve?name=${encodeURIComponent(name)}&type=${encodeURIComponent(type)}`;
const r = await fetch(url, { headers: { "accept": "application/dns-json" } });
if (!r.ok) throw new Error(`DoH query failed ${r.status} for ${name} ${type}`);
return r.json();
}
function extractDoHAnswers(resp) {
// resp.Answer entries: { name, type, TTL, data }
if (!resp || !resp.Answer) return [];
return resp.Answer.map(a => a.data);
}
function findSpf(txts) {
return (txts || []).filter(t => /^v=spf1\s/i.test(t.trim()));
}
function findDmarc(txts) {
return (txts || []).filter(t => /^v=DMARC1\s*;/i.test(t.trim()));
}
function findDkim(txts) {
return (txts || []).filter(t => /^v=DKIM1\s*;/i.test(t.trim()));
}
// --- SPF Recursive Lookup Counter ---
async function countSpfLookups(spf, domain, visited = new Set(), depth = 0) {
if (depth > 15 || visited.has(domain)) return 0; // Prevent infinite loops
visited.add(domain);
let count = 0;
// Count direct lookup mechanisms
const includes = spf.match(/include:([^\s]+)/gi) || [];
const redirects = spf.match(/redirect=([^\s]+)/gi) || [];
const aMatches = spf.match(/\ba\b|\ba:/gi) || [];
const mxMatches = spf.match(/\bmx\b|\bmx:/gi) || [];
const existsMatches = spf.match(/exists:([^\s]+)/gi) || [];
count += aMatches.length;
count += mxMatches.length;
count += existsMatches.length;
// Recursively fetch and count includes
for (const inc of includes) {
const incDomain = inc.replace(/include:/i, '').trim();
count++; // Count the include itself
try {
const txts = await dohQuery(incDomain, "TXT").then(r => extractDoHAnswers(r));
const incSpf = findSpf(txts)[0];
if (incSpf) {
count += await countSpfLookups(incSpf, incDomain, visited, depth + 1);
}
} catch (e) {
// If we can't fetch, still count the lookup attempt
}
}
// Handle redirect (only if no 'all' mechanism)
for (const redir of redirects) {
const redirDomain = redir.replace(/redirect=/i, '').trim();
count++; // Count the redirect itself
try {
const txts = await dohQuery(redirDomain, "TXT").then(r => extractDoHAnswers(r));
const redirSpf = findSpf(txts)[0];
if (redirSpf) {
count += await countSpfLookups(redirSpf, redirDomain, visited, depth + 1);
}
} catch (e) {
// If we can't fetch, still count the lookup attempt
}
}
return count;
}
// --- SPF Validation ---
async function validateSpf(spfRecords, domain, issues) {
if (!spfRecords || spfRecords.length === 0) {
issues.push("❌ No SPF record found - emails may be rejected or marked as spam");
return;
}
if (spfRecords.length > 1) {
issues.push(`❌ Multiple SPF records found (${spfRecords.length}) - only one SPF record is allowed per domain`);
}
const spf = spfRecords[0];
// Check for proper version tag
if (!spf.match(/^v=spf1\s/i)) {
issues.push("❌ SPF record must start with 'v=spf1 ' (version tag)");
}
// Check for proper termination
if (!spf.match(/[-~?]all$/i)) {
issues.push("⚠️ SPF record doesn't end with a proper 'all' mechanism (-all, ~all, ?all)");
}
// Check if it's too permissive
if (spf.match(/\+all$/i)) {
issues.push("❌ SPF record ends with '+all' - this allows ANY server to send email (severe security risk)");
}
// Count DNS lookups recursively (max 10 allowed)
const totalLookups = await countSpfLookups(spf, domain);
if (totalLookups > 10) {
issues.push(`❌ SPF has ${totalLookups} DNS lookups (max 10 allowed) - will cause PermError`);
} else if (totalLookups > 8) {
issues.push(`⚠️ SPF has ${totalLookups} DNS lookups (approaching limit of 10)`);
} else if (totalLookups > 0) {
issues.push(`OK:ℹ️ SPF has ${totalLookups} DNS lookups recursively (limit is 10)`);
}
// Check length (255 char limit for single string, 512 for combined)
if (spf.length > 450) {
issues.push(`⚠️ SPF record is ${spf.length} characters (approaching DNS TXT record limits)`);
}
// Check for common mistakes
if (spf.includes('ip4:') && !spf.match(/ip4:\d{1,3}\.\d{1,3}\.\d{1,3}\./)) {
issues.push("⚠️ SPF record has 'ip4:' but may be malformed");
}
if (spf.includes('include:') && spf.match(/include:\s/)) {
issues.push("⚠️ SPF 'include:' mechanism has a space after colon (should be no space)");
}
// Warn about redirect without -all
if (spf.includes('redirect=') && spf.match(/[-~?]all/)) {
issues.push("⚠️ SPF has both 'redirect=' and 'all' mechanism - 'all' will be ignored");
}
}
// --- DKIM Validation ---
function validateDkim(dkimRecords, domain, issues) {
if (!dkimRecords || dkimRecords.length === 0) {
issues.push("⚠️ No DKIM records found with common selectors - email authentication may fail");
return;
}
for (const dkimEntry of dkimRecords) {
const rec = dkimEntry.record;
const sel = dkimEntry.selector;
// Check for proper version tag
if (!rec.match(/v=DKIM1/i)) {
issues.push(`❌ DKIM selector '${sel}' missing or invalid version tag (must have v=DKIM1)`);
}
// Check for public key
if (!rec.includes('p=')) {
issues.push(`❌ DKIM selector '${sel}' missing public key (p= tag)`);
continue;
}
// Check if key is revoked (empty p= value)
if (rec.match(/p=\s*;/) || rec.match(/p=\s*$/)) {
issues.push(`❌ DKIM selector '${sel}' has revoked/empty public key`);
continue;
}
// Extract and check key length
const keyMatch = rec.match(/p=([A-Za-z0-9+/=]+)/);
if (keyMatch && keyMatch[1]) {
const keyData = keyMatch[1];
const keyLen = keyData.length;
// RSA keys are typically 200+ chars for 1024-bit, 350+ for 2048-bit
if (keyLen < 200) {
issues.push(`⚠️ DKIM selector '${sel}' has a short public key (${keyLen} chars) - may be weak or test key`);
}
}
// Check for key type
if (rec.includes('k=')) {
const keyType = rec.match(/k=([^;\s]+)/);
if (keyType && keyType[1] !== 'rsa') {
// Note: ed25519 is valid but less common
issues.push(`ℹ️ DKIM selector '${sel}' uses key type: ${keyType[1]} (non-RSA)`);
}
}
// Check testing flag
if (rec.match(/t=.*y/)) {
issues.push(`⚠️ DKIM selector '${sel}' is in testing mode (t=y) - verification failures will be ignored`);
}
// Check service type restrictions
if (rec.includes('s=') && !rec.includes('s=email') && !rec.includes('s=*')) {
const serviceMatch = rec.match(/s=([^;\s]+)/);
if (serviceMatch) {
issues.push(`ℹ️ DKIM selector '${sel}' has service type restriction: ${serviceMatch[1]}`);
}
}
}
}
// --- DMARC Validation ---
function validateDmarc(dmarcRecords, domain, issues) {
if (!dmarcRecords || dmarcRecords.length === 0) {
issues.push("⚠️ No DMARC record found - no policy for handling failed authentication");
return;
}
if (dmarcRecords.length > 1) {
issues.push(`❌ Multiple DMARC records found (${dmarcRecords.length}) - only one is allowed`);
}
const dmarc = dmarcRecords[0];
// Check for proper version tag
if (!dmarc.match(/^v=DMARC1\s*;/i)) {
issues.push("❌ DMARC record must start with 'v=DMARC1;' (version tag)");
}
// Check policy
const policyMatch = dmarc.match(/p=([^;\s]+)/);
if (!policyMatch) {
issues.push("❌ DMARC record missing 'p=' (policy) tag");
return;
}
const policy = policyMatch[1];
if (policy === 'none') {
issues.push("⚠️ DMARC policy is 'none' (monitoring only) - consider 'quarantine' or 'reject' for protection");
} else if (policy === 'quarantine') {
issues.push("ℹ️ DMARC policy is 'quarantine' (good protection)");
} else if (policy === 'reject') {
issues.push("ℹ️ DMARC policy is 'reject' (maximum protection)");
}
// Check percentage
const pctMatch = dmarc.match(/pct=(\d+)/);
if (pctMatch && parseInt(pctMatch[1]) < 100) {
issues.push(`ℹ️ DMARC policy applied to ${pctMatch[1]}% of messages (not 100%)`);
}
// Check for reporting
if (!dmarc.includes('rua=')) {
issues.push("ℹ️ DMARC record missing 'rua=' (aggregate reports) - no visibility into email authentication");
}
// Check subdomain policy
const spMatch = dmarc.match(/sp=([^;\s]+)/);
if (!spMatch) {
issues.push("ℹ️ DMARC record missing 'sp=' (subdomain policy) - subdomains inherit main policy");
}
}
// --- RDAP ---
async function getRdap(domain) {
// IANA bootstrap mapping TLD -> RDAP base URL
const boot = await fetch("https://data.iana.org/rdap/dns.json", { headers: { "accept":"application/json" } }).then(r => r.json());
const tld = domain.split(".").pop();
let base = null;
for (const svc of boot.services) {
const tlds = svc[0], urls = svc[1];
if (tlds.includes(tld)) { base = urls[0]; break; }
}
if (!base) return null;
base = base.replace(/\/$/, "");
const rdapUrl = `${base}/domain/${encodeURIComponent(domain)}`;
try {
const rdap = await fetch(rdapUrl, { headers: { "accept": "application/rdap+json, application/json" } }).then(r => r.ok ? r.json() : null);
return { rdapUrl, rdap };
} catch {
return { rdapUrl, rdap: null };
}
}
function rdapRegistrar(rdap) {
if (!rdap || !rdap.entities) return null;
for (const ent of rdap.entities) {
if ((ent.roles || []).includes("registrar")) {
let name = null;
if (ent.vcardArray && ent.vcardArray.length >= 2) {
for (const row of ent.vcardArray[1]) {
if (row[0] === "fn") name = row[3];
}
}
return { name, handle: ent.handle || null };
}
}
return null;
}
function rdapEvent(rdap, actions) {
if (!rdap || !rdap.events) return null;
for (const e of rdap.events) {
if (actions.includes(e.eventAction)) return e.eventDate || null;
}
return null;
}
async function run(domain, selectors) {
const reportTime = new Date().toISOString();
const issues = [];
// --- RDAP ---
const { rdapUrl, rdap } = await getRdap(domain) || { rdapUrl:null, rdap:null };
const registrar = rdapRegistrar(rdap);
const created = rdapEvent(rdap, ["registration","registered","created"]);
const updated = rdapEvent(rdap, ["last changed","updated","modified"]);
const expires = rdapEvent(rdap, ["expiration","expires","expiry"]);
const renewal = rdapEvent(rdap, ["renewal","renewed"]); // often absent
// --- DNS queries (DoH) ---
const dns = {};
async function q(name, type) {
try {
const resp = await dohQuery(name, type);
return uniq(extractDoHAnswers(resp));
} catch (e) {
return [];
}
}
dns.NS = await q(domain, "NS");
dns.SOA = await q(domain, "SOA");
dns.Apex = {
A: await q(domain, "A"),
AAAA: await q(domain, "AAAA"),
CNAME: await q(domain, "CNAME")
};
const www = `www.${domain}`;
dns.WWW = {
A: await q(www, "A"),
AAAA: await q(www, "AAAA"),
CNAME: await q(www, "CNAME")
};
dns.MX = await q(domain, "MX");
dns.TXT = await q(domain, "TXT");
dns.SPF = findSpf(dns.TXT);
const dmarcName = `_dmarc.${domain}`;
dns.DMARC = findDmarc(await q(dmarcName, "TXT"));
// DKIM by selector
dns.DKIM = [];
for (const sel of selectors) {
const dkimName = `${sel}._domainkey.${domain}`;
const txts = await q(dkimName, "TXT");
const rec = findDkim(txts)[0];
if (rec) dns.DKIM.push({ selector: sel, name: dkimName, record: rec });
}
// Extras
dns.CAA = await q(domain, "CAA");
dns.MTA_STS = await q(`_mta-sts.${domain}`, "TXT");
dns.TLS_RPT = await q(`_smtp._tls.${domain}`, "TXT");
dns.BIMI = await q(`default._bimi.${domain}`, "TXT");
// quick “issues” heuristics
if (!dns.NS.length) issues.push("No NS records returned (DNS issue, DNSSEC failure, or blocked resolver).");
if (!dns.SOA.length) issues.push("No SOA record returned (DNS issue).");
// Validate SPF, DKIM, DMARC with detailed checks
await validateSpf(dns.SPF, domain, issues);
validateDkim(dns.DKIM, domain, issues);
validateDmarc(dns.DMARC, domain, issues);
// Check expiration
const expWarning = checkExpirationWarning(expires);
if (expWarning === 'expired') issues.push(`⚠️ Domain EXPIRED on ${expires}`);
else if (expWarning === 'critical') issues.push(`⚠️ Domain expires VERY SOON (within 30 days): ${expires}`);
else if (expWarning === 'soon') issues.push(`⚠️ Domain expires within 90 days: ${expires}`);
const out = {
domain,
reportTime,
rdap: {
rdapUrl,
registrar,
created,
updated,
renewal,
expires,
note: "RDAP fields vary by registry/TLD; renewal date is often not published.",
raw: rdap
},
dns,
issues
};
return out;
}
function pretty(obj) {
return JSON.stringify(obj, null, 2);
}
async function onRun() {
const inputDomains = parseList($("domain").value).map(normalizeDomain).filter(d => d && d.indexOf('.') > 0);
const selectors = parseSelectors($("selectors").value);
if (!inputDomains.length) {
$("status").innerHTML = `<span class="err">Enter at least one valid domain (e.g., example.com).</span>`;
return;
}
$("status").textContent = "Running…";
$("issues").innerHTML = "";
$("rdap").innerHTML = "";
$("dns").textContent = "";
$("raw").textContent = "";
try {
const results = [];
for (const d of inputDomains) {
const out = await run(d, selectors);
results.push(out);
}
window.__LAST__ = results;
// Render Issues per domain
const issuesHtml = `
<div class="domains-grid">
${results.map(r => {
const hasErr = r.issues.some(i => i.startsWith('ERR:') || i.startsWith('❌'));
const hasWarn = r.issues.some(i => i.startsWith('⚠️'));
const badgeClass = hasErr ? 'badge-err' : (hasWarn ? 'badge-warn' : 'badge-ok');
const badgeText = hasErr ? 'Issues Found' : (hasWarn ? 'Warnings' : 'All Good');
return `
<div class="domain-card">
<div class="domain-title">
<div class="domain-name">${escapeHtml(r.domain)}</div>
<span class="badge ${badgeClass}">${badgeText}</span>
</div>
<div class="issues-section">${renderIssues(r.issues)}</div>
</div>
`;
}).join('')}
</div>`;
$("issues").innerHTML = issuesHtml;
// Render RDAP per domain with expiration highlighting
const rdapSections = results.map(r => {
const rd = r.rdap;
let rdapHtml = '{';
rdapHtml += `\n "domain": ${escapeHtml(JSON.stringify(r.domain))},`;
rdapHtml += `\n "rdapUrl": ${escapeHtml(JSON.stringify(rd.rdapUrl))},`;
rdapHtml += `\n "registrar": ${escapeHtml(JSON.stringify(rd.registrar))},`;
rdapHtml += `\n "created": ${escapeHtml(JSON.stringify(rd.created))},`;
rdapHtml += `\n "updated": ${escapeHtml(JSON.stringify(rd.updated))},`;
rdapHtml += `\n "renewal": ${escapeHtml(JSON.stringify(rd.renewal))},`;
rdapHtml += `\n "expires": ${formatDateWithWarning(rd.expires)},`;
rdapHtml += `\n "note": ${escapeHtml(JSON.stringify(rd.note))}`;
rdapHtml += '\n}';
return `<div style="margin:8px 0">${rdapHtml}</div>`;
}).join('\n');
$("rdap").innerHTML = rdapSections;
// Render DNS and Raw JSON (arrays of results)
$("dns").textContent = pretty(results.map(r => ({ domain: r.domain, dns: r.dns })));
$("raw").textContent = pretty(results);
$("status").innerHTML = `<span class="ok">Done.</span>`;
} catch (e) {
$("status").innerHTML = `<span class="err">Error: ${escapeHtml(e.message || String(e))}</span>`;
}
}
async function onCopy() {
const data = window.__LAST__ ? JSON.stringify(window.__LAST__, null, 2) : "";
if (!data) { alert("Run it first."); return; }
await navigator.clipboard.writeText(data);
$("status").innerHTML = `<span class="ok">Copied JSON to clipboard.</span>`;
}
$("run").addEventListener("click", onRun);
$("copy").addEventListener("click", onCopy);
// convenience: run when pressing Enter in domain box
$("domain").addEventListener("keydown", (e) => {
if (e.key === "Enter" && (e.ctrlKey || e.metaKey)) onRun();
});
// live preview of parsed domains
function updateDomainsPreview() {
const list = parseList($("domain").value).map(normalizeDomain).filter(Boolean);
$("domainsPreview").innerHTML = `Parsed domains (${list.length}): ` + list.map(escapeHtml).join(", ");
}
$("domain").addEventListener("input", updateDomainsPreview);
updateDomainsPreview();
// live preview of parsed selectors
function updateSelectorsPreview() {
const list = parseSelectors($("selectors").value);
$("selectorsPreview").innerHTML = `Parsed selectors (${list.length}): ` + list.map(escapeHtml).join(", ");
}
$("selectors").addEventListener("input", updateSelectorsPreview);
updateSelectorsPreview();
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment