1import http from 'node:http';
2import { Actor } from 'apify';
3import {
4 isValidDomain,
5 isValidIP,
6 VALID_RECORD_TYPES,
7 forwardLookup,
8 reverseLookup,
9} from './dns-utils.js';
10
11await Actor.init();
12
13if (Actor.config.get('metaOrigin') === 'STANDBY') {
14 startStandbyServer();
15} else {
16 await runBatchMode();
17 await Actor.exit();
18}
19
20
21
22
23
24function startStandbyServer() {
25 const port = Actor.config.get('containerPort');
26
27 const server = http.createServer(async (req, res) => {
28
29 if (req.headers['x-apify-container-server-readiness-probe']) {
30 res.writeHead(200, { 'Content-Type': 'application/json' });
31 res.end(JSON.stringify({ status: 'ready' }));
32 return;
33 }
34
35
36 if (req.method !== 'GET') {
37 res.writeHead(405, { 'Content-Type': 'application/json' });
38 res.end(JSON.stringify({ error: 'Method not allowed. Use GET.' }));
39 return;
40 }
41
42
43 const reqUrl = new URL(req.url, `http://${req.headers.host}`);
44 if (!reqUrl.searchParams.get('domain') && !reqUrl.searchParams.get('ip')) {
45 res.writeHead(200, { 'Content-Type': 'text/html' });
46 res.end(getLandingPageHtml());
47 return;
48 }
49
50 try {
51 const result = await handleStandbyRequest(req);
52 await Actor.pushData(result);
53
54 res.writeHead(200, { 'Content-Type': 'application/json' });
55 res.end(JSON.stringify(result));
56 } catch (err) {
57 const statusCode = err.statusCode || 500;
58 res.writeHead(statusCode, { 'Content-Type': 'application/json' });
59 res.end(JSON.stringify({ error: err.message }));
60 }
61 });
62
63 server.listen(port, () => {
64 console.log(`Supernet DNS Lookup Standby server listening on port ${port}`);
65 });
66}
67
68async function handleStandbyRequest(req) {
69 const url = new URL(req.url, `http://${req.headers.host}`);
70 const domain = url.searchParams.get('domain');
71 const ip = url.searchParams.get('ip');
72 const typeParam = url.searchParams.get('type');
73
74
75 if (!domain && !ip) {
76 const err = new Error("Provide either a 'domain' or 'ip' query parameter.");
77 err.statusCode = 400;
78 throw err;
79 }
80 if (domain && ip) {
81 const err = new Error("Provide either 'domain' or 'ip', not both.");
82 err.statusCode = 400;
83 throw err;
84 }
85
86
87 if (ip) {
88 if (!isValidIP(ip)) {
89 const err = new Error(`Invalid IP address: ${ip}`);
90 err.statusCode = 400;
91 throw err;
92 }
93 return reverseLookup(ip);
94 }
95
96
97 if (!isValidDomain(domain)) {
98 const err = new Error(`Invalid domain: ${domain}`);
99 err.statusCode = 400;
100 throw err;
101 }
102
103 let recordTypes = null;
104 if (typeParam) {
105 recordTypes = typeParam.split(',').map((t) => t.trim().toUpperCase());
106 const invalid = recordTypes.filter((t) => !VALID_RECORD_TYPES.includes(t));
107 if (invalid.length > 0) {
108 const err = new Error(`Invalid record type(s): ${invalid.join(', ')}. Valid types: ${VALID_RECORD_TYPES.join(', ')}`);
109 err.statusCode = 400;
110 throw err;
111 }
112 }
113
114 return forwardLookup(domain, recordTypes);
115}
116
117
118
119
120
121async function runBatchMode() {
122 const input = await Actor.getInput();
123 const { domains, dnsRecordTypes, reverseLookup: isReverse } = input;
124
125 if (!domains || !Array.isArray(domains) || domains.length === 0) {
126 await Actor.pushData({ error: 'missing_domains', message: 'Provide a non-empty array of domains or IP addresses.' });
127 await Actor.exit('Skipped: Provide a non-empty array of domains or IP addresses.');
128 return;
129 }
130
131 const allDomains = domains.every(isValidDomain);
132 const allIPs = domains.every(isValidIP);
133
134 if (!allDomains && !allIPs) {
135 await Actor.pushData({ error: 'mixed_input', message: 'Mixed inputs detected. Provide either all valid domain names or all valid IP addresses.' });
136 await Actor.exit('Skipped: Mixed inputs detected. Provide either all valid domain names or all valid IP addresses.');
137 return;
138 }
139
140 if (isReverse && !allIPs) {
141 await Actor.pushData({ error: 'invalid_ips', message: 'For reverse lookups, provide only valid IP addresses.' });
142 await Actor.exit('Skipped: For reverse lookups, provide only valid IP addresses.');
143 return;
144 }
145
146 if (!isReverse && !allDomains) {
147 await Actor.pushData({ error: 'invalid_domains', message: 'For standard DNS lookups, provide only valid domain names.' });
148 await Actor.exit('Skipped: For standard DNS lookups, provide only valid domain names.');
149 return;
150 }
151
152 const typesToQuery = !isReverse && Array.isArray(dnsRecordTypes) && dnsRecordTypes.length > 0
153 ? dnsRecordTypes
154 : null;
155
156
157 const concurrency = 10;
158 for (let i = 0; i < domains.length; i += concurrency) {
159 const batch = domains.slice(i, i + concurrency);
160 const batchResults = await Promise.allSettled(
161 batch.map((entry) =>
162 isReverse ? reverseLookup(entry) : forwardLookup(entry, typesToQuery),
163 ),
164 );
165
166 const results = [];
167 for (const r of batchResults) {
168 if (r.status === 'fulfilled') {
169 results.push(r.value);
170 } else {
171 console.error(`DNS lookup failed: ${r.reason?.message}`);
172 }
173 }
174
175 if (results.length > 0) {
176 await Actor.pushData(results);
177 }
178
179 console.log(`Processed ${Math.min(i + concurrency, domains.length)}/${domains.length} entries`);
180
181 }
182}
183
184
185
186
187
188function getLandingPageHtml() {
189 return `<!DOCTYPE html>
190<html lang="en">
191<head>
192<meta charset="utf-8">
193<meta name="viewport" content="width=device-width, initial-scale=1">
194<title>Supernet DNS Lookup — Superlative</title>
195<link rel="preconnect" href="https://fonts.googleapis.com">
196<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
197<link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;700&family=Space+Mono:wght@400;700&display=swap" rel="stylesheet">
198<style>
199:root{--color-bg:#0a0e14;--color-surface:#12171f;--color-border:#1e2632;--color-text:#e8eaed;--color-text-muted:#8b95a5;--color-accent:#4ade80;--color-accent-dim:rgba(74,222,128,0.15);--font-display:'Space Mono',monospace;--font-body:'DM Sans',sans-serif}
200*{margin:0;padding:0;box-sizing:border-box}
201body{font-family:var(--font-body);background:var(--color-bg);color:var(--color-text);line-height:1.6;min-height:100vh;overflow-x:hidden}
202a{color:var(--color-accent);text-decoration:none;transition:color .2s ease}
203a:hover{color:#6ee7a0}
204.grid-bg{position:fixed;top:0;left:0;width:100%;height:100%;background-image:linear-gradient(var(--color-border) 1px,transparent 1px),linear-gradient(90deg,var(--color-border) 1px,transparent 1px);background-size:60px 60px;opacity:.3;pointer-events:none;z-index:0}
205.accent-line{position:fixed;top:0;left:0;width:100%;height:2px;background:linear-gradient(90deg,transparent 0%,var(--color-accent) 50%,transparent 100%);animation:shimmer 3s ease-in-out infinite;z-index:10}
206@keyframes shimmer{0%,100%{transform:translateX(-100%)}50%{transform:translateX(100%)}}
207@keyframes fadeUp{from{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}
208.container{position:relative;z-index:1;max-width:900px;margin:0 auto;padding:0 24px}
209header{padding:32px 0}
210.logo{font-family:var(--font-display);font-size:14px;font-weight:700;letter-spacing:.1em;text-transform:uppercase;color:var(--color-text);display:flex;align-items:center;gap:10px;text-decoration:none}
211.logo:hover{color:var(--color-text)}
212.logo-mark{width:28px;height:28px;background:var(--color-accent);display:flex;align-items:center;justify-content:center}
213.logo-mark svg{width:16px;height:16px}
214.hero{padding:80px 0 60px;animation:fadeUp .8s ease-out}
215.hero-label{font-family:var(--font-display);font-size:12px;font-weight:400;letter-spacing:.15em;text-transform:uppercase;color:var(--color-accent);margin-bottom:24px;display:flex;align-items:center;gap:12px}
216.hero-label::before{content:'';width:24px;height:1px;background:var(--color-accent)}
217h1{font-family:var(--font-display);font-size:clamp(28px,5vw,44px);font-weight:700;line-height:1.15;margin-bottom:20px;letter-spacing:-.02em}
218h1 .highlight{color:var(--color-accent)}
219.hero-desc{font-size:18px;color:var(--color-text-muted);max-width:540px;margin-bottom:28px;line-height:1.7}
220.hero-link{font-family:var(--font-display);font-size:13px;letter-spacing:.05em;color:var(--color-accent);display:inline-flex;align-items:center;gap:6px}
221.section{padding:60px 0;border-top:1px solid var(--color-border);animation:fadeUp .8s ease-out .2s backwards}
222.section-label{font-family:var(--font-display);font-size:11px;font-weight:400;letter-spacing:.2em;text-transform:uppercase;color:var(--color-text-muted);margin-bottom:24px}
223h2{font-family:var(--font-display);font-size:20px;font-weight:700;margin-bottom:16px;letter-spacing:-.01em}
224p,li{font-size:15px;color:var(--color-text-muted);line-height:1.7}
225li{margin-bottom:6px}
226ul{margin:.5rem 0 1rem 1.25rem}
227strong{color:var(--color-text);font-weight:600}
228pre{background:var(--color-surface);color:var(--color-text);padding:16px 20px;overflow-x:auto;margin:12px 0 16px;font-family:var(--font-display);font-size:13px;line-height:1.6;border:1px solid var(--color-border)}
229code{font-family:var(--font-display);font-size:13px}
230p code,li code,td code{background:var(--color-accent-dim);color:var(--color-accent);padding:2px 6px}
231table{width:100%;border-collapse:collapse;margin:12px 0 16px;font-size:14px}
232th,td{border:1px solid var(--color-border);padding:10px 14px;text-align:left}
233th{background:var(--color-surface);color:var(--color-text-muted);font-family:var(--font-display);font-size:12px;font-weight:400;letter-spacing:.05em;text-transform:uppercase}
234td{color:var(--color-text-muted)}
235.badge{display:inline-block;background:var(--color-accent-dim);color:var(--color-accent);padding:3px 8px;font-family:var(--font-display);font-size:11px;font-weight:400;letter-spacing:.05em}
236.note{font-size:13px;color:var(--color-text-muted);margin-top:8px}
237.product-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:16px;margin-top:24px}
238.product-card{background:var(--color-surface);border:1px solid var(--color-border);padding:24px;transition:all .3s ease;text-decoration:none;color:inherit;display:block}
239.product-card:hover{border-color:var(--color-accent);transform:translateY(-2px)}
240.product-name{font-family:var(--font-display);font-size:14px;font-weight:700;margin-bottom:8px;color:var(--color-text);display:flex;align-items:center;gap:8px}
241.product-name .status{font-size:9px;font-weight:400;padding:3px 6px;background:var(--color-accent-dim);color:var(--color-accent);letter-spacing:.1em}
242.product-desc{font-size:14px;color:var(--color-text-muted);line-height:1.5}
243footer{padding:48px 0;border-top:1px solid var(--color-border)}
244.footer-content{display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:24px}
245.footer-text{font-size:13px;color:var(--color-text-muted)}
246.footer-links{display:flex;gap:24px}
247.footer-links a{font-family:var(--font-display);font-size:12px;color:var(--color-text-muted);letter-spacing:.05em}
248.footer-links a:hover{color:var(--color-accent)}
249@media(max-width:600px){.hero{padding:60px 0 40px}.section{padding:40px 0}.footer-content{flex-direction:column;align-items:flex-start}pre{font-size:12px;padding:12px 14px}}
250</style>
251</head>
252<body>
253<div class="grid-bg"></div>
254<div class="accent-line"></div>
255
256<div class="container">
257 <header>
258 <a href="https://apify.com/superlativetech?fpr=8e9l1" class="logo">
259 <div class="logo-mark">
260 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
261 <polyline points="4 17 10 11 4 5"></polyline>
262 <line x1="12" y1="19" x2="20" y2="19"></line>
263 </svg>
264 </div>
265 Superlative
266 </a>
267 </header>
268
269 <section class="hero">
270 <div class="hero-label">Network Intelligence</div>
271 <h1>Supernet DNS <span class="highlight">Lookup</span></h1>
272 <p class="hero-desc">Instant DNS record lookups and reverse DNS resolution. Sub-second responses via Standby API.</p>
273 <a href="https://apify.com/superlativetech/dns-lookup?fpr=8e9l1" class="hero-link">View on Apify Store →</a>
274 </section>
275
276 <section class="section">
277 <div class="section-label">Features</div>
278 <h2>What this does</h2>
279 <ul>
280 <li><strong>Forward lookup</strong> — query A, AAAA, MX, TXT, NS, CNAME, SOA, SRV, CAA, or PTR records</li>
281 <li><strong>Reverse lookup</strong> — resolve IP addresses back to hostnames</li>
282 <li><strong>Selective types</strong> — request specific record types or get all by default</li>
283 <li><strong>Batch mode</strong> — process hundreds of domains via the standard Actor run</li>
284 </ul>
285 </section>
286
287 <section class="section">
288 <div class="section-label">Getting Started</div>
289 <h2>Quick start</h2>
290 <p>Replace <code>YOUR_TOKEN</code> with your Apify API token.</p>
291
292 <p style="margin-top:16px"><strong>Forward lookup (all record types):</strong></p>
293 <pre>curl "https://superlativetech--dns-lookup.apify.actor?token=YOUR_TOKEN&domain=apify.com"</pre>
294
295 <p><strong>Specific record types:</strong></p>
296 <pre>curl "https://superlativetech--dns-lookup.apify.actor?token=YOUR_TOKEN&domain=apify.com&type=MX,TXT"</pre>
297
298 <p><strong>Reverse lookup:</strong></p>
299 <pre>curl "https://superlativetech--dns-lookup.apify.actor?token=YOUR_TOKEN&ip=8.8.8.8"</pre>
300 </section>
301
302 <section class="section">
303 <div class="section-label">Reference</div>
304 <h2>Query parameters</h2>
305 <table>
306 <tr><th>Parameter</th><th>Required</th><th>Description</th></tr>
307 <tr><td><code>domain</code></td><td>*</td><td>Domain name to look up (e.g. <code>apify.com</code>)</td></tr>
308 <tr><td><code>ip</code></td><td>*</td><td>IP address for reverse lookup (e.g. <code>8.8.8.8</code>)</td></tr>
309 <tr><td><code>type</code></td><td>No</td><td>Comma-separated record types: A, AAAA, MX, TXT, NS, CNAME, SOA, SRV, CAA, PTR</td></tr>
310 <tr><td><code>token</code></td><td>Yes</td><td>Your Apify API token</td></tr>
311 </table>
312 <p class="note">* Provide either <code>domain</code> or <code>ip</code>, not both.</p>
313 </section>
314
315 <section class="section">
316 <div class="section-label">Response</div>
317 <h2>Response format</h2>
318 <pre>{
319 "domain": "apify.com",
320 "status": "OK",
321 "records": {
322 "A": ["18.196.0.40"],
323 "MX": [{ "exchange": "alt1.aspmx.l.google.com", "priority": 5 }],
324 "TXT": ["v=spf1 include:_spf.google.com ..."]
325 }
326}</pre>
327 </section>
328
329 <section class="section">
330 <div class="section-label">Cost</div>
331 <h2>Pricing</h2>
332 <p>Free to use — you only pay standard Apify platform usage.</p>
333 </section>
334
335 <section class="section">
336 <div class="section-label">Auth</div>
337 <h2>Authentication</h2>
338 <p>Authenticate using either method:</p>
339 <ul>
340 <li>Query parameter: <code>?token=YOUR_APIFY_TOKEN</code></li>
341 <li>Header: <code>Authorization: Bearer YOUR_APIFY_TOKEN</code></li>
342 </ul>
343 <p style="margin-top:8px">Get your token from <a href="https://console.apify.com/settings/integrations">Apify Console → Settings → Integrations</a>.</p>
344 </section>
345
346 <section class="section">
347 <div class="section-label">Superlative</div>
348 <h2>More from Superlative</h2>
349 <div class="product-grid">
350 <a href="https://apify.com/superlativetech/superclean-company-names?fpr=8e9l1" class="product-card">
351 <div class="product-name">Company Names <span class="status">Live</span></div>
352 <p class="product-desc">Normalize company names for CRM and cold email outreach.</p>
353 </a>
354 <a href="https://apify.com/superlativetech/superclean-job-titles?fpr=8e9l1" class="product-card">
355 <div class="product-name">Job Titles <span class="status">Live</span></div>
356 <p class="product-desc">Standardize job titles for outreach sequences.</p>
357 </a>
358 <a href="https://apify.com/superlativetech/superclean-person-names?fpr=8e9l1" class="product-card">
359 <div class="product-name">Person Names <span class="status">Live</span></div>
360 <p class="product-desc">Clean and format person names for personalization.</p>
361 </a>
362 <a href="https://apify.com/superlativetech/superclean-product-names?fpr=8e9l1" class="product-card">
363 <div class="product-name">Product Names <span class="status">Live</span></div>
364 <p class="product-desc">Normalize product and brand names from exports.</p>
365 </a>
366 <a href="https://apify.com/superlativetech/superclean-places?fpr=8e9l1" class="product-card">
367 <div class="product-name">Places <span class="status">Live</span></div>
368 <p class="product-desc">Parse and standardize location strings.</p>
369 </a>
370 <a href="https://apify.com/superlativetech/superclean-urls?fpr=8e9l1" class="product-card">
371 <div class="product-name">URLs <span class="status">Live</span></div>
372 <p class="product-desc">Clean and normalize URLs from lead data.</p>
373 </a>
374 <a href="https://apify.com/superlativetech/superclean-phone-numbers?fpr=8e9l1" class="product-card">
375 <div class="product-name">Phone Numbers <span class="status">Live</span></div>
376 <p class="product-desc">Format and validate phone numbers.</p>
377 </a>
378 <a href="https://apify.com/superlativetech/http-api?fpr=8e9l1" class="product-card">
379 <div class="product-name">HTTP API <span class="status">Live</span></div>
380 <p class="product-desc">General-purpose HTTP request utility.</p>
381 </a>
382 <a href="https://apify.com/superlativetech/supernet-domain-health?fpr=8e9l1" class="product-card">
383 <div class="product-name">Domain Health <span class="status">Live</span></div>
384 <p class="product-desc">Email reputation audit: SPF, DKIM, DMARC, blacklists, SSL.</p>
385 </a>
386 <a href="https://apify.com/superlativetech/supernet-whois-lookup?fpr=8e9l1" class="product-card">
387 <div class="product-name">WHOIS Lookup <span class="status">Live</span></div>
388 <p class="product-desc">Domain registration data: registrar, dates, age, name servers.</p>
389 </a>
390 </div>
391 </section>
392
393 <footer>
394 <div class="footer-content">
395 <p class="footer-text">© 2026 Superlative</p>
396 <div class="footer-links">
397 <a href="https://apify.com/superlativetech?fpr=8e9l1">Apify Store</a>
398 </div>
399 </div>
400 </footer>
401</div>
402
403</body>
404</html>`;
405}