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