import { createServer } from 'node:http'; import { pathToFileURL, domainToASCII, domainToUnicode } from 'node:url'; // example:imports import { parseAPI } from '@parseapi/sdk'; // /example:imports class InputError extends Error {} const escape = value => String(value).replace(/[&<>"']/g, char => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[char])); const text = (value, max = 160) => typeof value === 'string' && value.length > 0 && value.length <= max && !/[\u0000-\u001f\u007f]/u.test(value); function exactDomain(value) { if (typeof value !== 'string' || value.trim().length > 253 || /[\s\u0000-\u0020\u007f/:@?#\\%\[\]]/u.test(value.trim())) throw new InputError('Enter one domain without a URL, path, or email address.'); const domain = domainToASCII(value.trim().toLowerCase()).replace(/\.$/, ''); const pattern = /^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+(?:[a-z]{2,63}|xn--[a-z0-9](?:[a-z0-9-]{0,57}[a-z0-9])?)$/; if (!domain || domain.length > 253 || !pattern.test(domain) || /(?:^|\.)xn--/i.test(domainToUnicode(domain))) throw new InputError('Enter a valid domain name.'); return domain; } const TITLE = 'Add website context to an account'; const INTRO = 'Keep a company website beside its detected technologies, so your team has context before a conversation.'; const FAILURE = 'Website technologies could not be checked. The account card has no new inventory. Try again.'; const FIELDS = ['website']; const CATEGORIES = { cms: 'CMS', servers: 'Web servers', frameworks: 'Frameworks', ecommerce: 'Ecommerce', analytics: 'Analytics', chat: 'Chat', payments: 'Payments', hosting: 'Hosting' }; export async function accountTechnologies(input, parse, signal) { const website = exactDomain(input); // example:request const stack = await parse.stack(website, { signal }); // /example:request if (!stack || stack.domain !== website || !text(stack.url, 2048)) throw new Error('Unexpected website response.'); const page = new URL(stack.url); if (!['https:', 'http:'].includes(page.protocol) || page.username || page.password || page.search || page.hash) throw new Error('Unexpected page URL.'); if (!(stack.checked_at === null || typeof stack.checked_at === 'string' && /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,3})?Z$/.test(stack.checked_at) && Number.isFinite(Date.parse(stack.checked_at)) && new Date(stack.checked_at).toISOString().slice(0, 19) === stack.checked_at.slice(0, 19))) throw new Error('Unexpected check time.'); let total = 0; for (const category of Object.keys(CATEGORIES)) { const entries = stack[category]; if (stack.checked_at === null ? entries !== null : !Array.isArray(entries)) throw new Error('Unexpected technology collection.'); if (entries === null) continue; total += entries.length; if (total > 100) throw new Error('Unexpected technology count.'); const ids = new Set(); for (const entry of entries) { if (!entry || !text(entry.technology, 80) || ids.has(entry.technology) || !text(entry.name, 120) || !(entry.version === null || text(entry.version, 80))) throw new Error('Unexpected technology.'); ids.add(entry.technology); } } return stack; } function renderForm(fields = {}) { // example:form const form = `

Acme Studio

Account website

Use a public hostname, without https:// or a path.

`; // /example:form return form; } async function submit(fields, parse, signal) { const stack = await accountTechnologies(fields.website, parse, signal); if (stack.checked_at === null) return '

Website could not be checked

No technology inventory is available for this account website. Try again later.

'; // example:result const groups = Object.entries(CATEGORIES) .filter(([key]) => stack[key].length > 0) .map(([key, label]) => `

${label}

`).join(''); const unmatched = Object.entries(CATEGORIES) .filter(([key]) => stack[key].length === 0) .map(([, label]) => label); // /example:result // example:integration return `

Acme Studio website technologies

${escape(stack.domain)}

Checked . Cached results keep the original check time.

Page: ${escape(stack.url)}

${groups}
${unmatched.length ? `

No matches detected for: ${unmatched.join(', ')}.

` : ''}

Detection uses the homepage and response headers. A missing match does not establish that a technology is absent.

`; // /example:integration } const CSS = ` :root{color-scheme:light dark;font:16px/1.55 system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;color:#202124;background:#f5f5f7} *{box-sizing:border-box}body{margin:0}main{max-width:720px;margin:0 auto;padding:56px 20px 72px}h1{font-size:clamp(2rem,6vw,2.8rem);line-height:1.12;letter-spacing:-.045em;margin:0 0 16px}h2{font-size:1.35rem;line-height:1.3;letter-spacing:-.025em;margin:0 0 12px}h3{font-size:1rem;margin:0 0 8px}p{margin:0 0 16px}.intro,.note,small{color:#62646a}.intro{max-width:56ch}form,.result,.account{background:#fff;border:1px solid #e2e3e4;border-radius:18px;padding:24px;margin-top:24px}label{display:block;font-weight:600;margin-bottom:8px}input,select,button{font:inherit;min-height:44px;border-radius:8px}input,select{width:100%;border:1px solid #acafb5;background:#fff;color:inherit;padding:10px 12px}select{appearance:none;background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 16 16'%3E%3Cpath fill='none' stroke='%2371717a' stroke-width='1.5' d='m4 6 4 4 4-4'/%3E%3C/svg%3E");background-repeat:no-repeat;background-position:right 12px center;padding-right:36px}button{border:0;background:#087f9c;color:#fff;font-weight:600;padding:10px 18px;margin-top:20px;cursor:pointer}button:disabled{opacity:.65;cursor:wait}:focus-visible{outline:3px solid #0891b2;outline-offset:3px}.note{font-size:.875rem;margin-top:12px}.status{min-height:1.5em;margin-top:14px}ul{padding-left:20px}li{overflow-wrap:anywhere}.result{overflow-wrap:anywhere}.result p:last-child,.account p:last-child{margin-bottom:0}.grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:24px}.technology{display:block}time{font-variant-numeric:tabular-nums} @media(max-width:480px){main{padding-top:32px}form,.result,.account{padding:20px}.grid{grid-template-columns:1fr}button{width:100%}} @media(prefers-color-scheme:dark){:root{background:#000;color:#f5f5f7}form,.result,.account{background:#1d1d1f;border-color:#424245}input,select{background-color:#1d1d1f;border-color:#69696e}.intro,.note,small{color:#a1a1a6}button{background:#087f9c}} `; const CLIENT = ` const form = document.querySelector('form'); const result = document.querySelector('#result'); const status = document.querySelector('#status'); const button = form.querySelector('button'); let controller, revision = 0; form.addEventListener('input', () => { revision++; controller?.abort(); result.replaceChildren(); status.textContent = ''; button.disabled = false; }); form.addEventListener('submit', async event => { event.preventDefault(); controller?.abort(); controller = new AbortController(); const current = ++revision, signal = controller.signal; result.replaceChildren(); status.textContent = 'Checking...'; button.disabled = true; try { const response = await fetch('/', { method: 'POST', signal, headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'X-Example-Fragment': '1' }, body: new URLSearchParams(new FormData(form)) }); const html = await response.text(); if (current !== revision || signal.aborted) return; if (!response.headers.get('content-type')?.startsWith('text/html')) throw new Error(); result.innerHTML = html; status.textContent = ''; } catch { if (current !== revision || signal.aborted) return; status.textContent = 'The check did not finish. Try again.'; } finally { if (current === revision) button.disabled = false; } }); `; function page(form, result = '') { return `${TITLE}

${TITLE}

${INTRO}

${form}

${result}
`; } function readForm(req, allowed) { if (req.headers['content-type']?.split(';')[0] !== 'application/x-www-form-urlencoded') return Promise.reject(new InputError('Submit this form again.')); return new Promise((resolve, reject) => { const chunks = []; let bytes = 0; const fail = error => { cleanup(); req.resume(); reject(error); }; const chunk = value => { bytes += value.length; if (bytes > 4096) return fail(new InputError('The form is too large.')); chunks.push(value); }; const finish = () => { cleanup(); const fields = new URLSearchParams(Buffer.concat(chunks).toString('utf8')); if ([...fields.keys()].some(key => !allowed.includes(key)) || allowed.some(key => fields.getAll(key).length !== 1)) return reject(new InputError('Submit one value for each field.')); resolve(Object.fromEntries(fields)); }; const aborted = () => fail(new Error('Request ended.')); const cleanup = () => { req.off('data', chunk); req.off('end', finish); req.off('aborted', aborted); req.off('error', fail); }; req.on('data', chunk); req.once('end', finish); req.once('aborted', aborted); req.once('error', fail); }); } export function createApp(parse, { now = () => new Date() } = {}) { const server = createServer(async (req, res) => { const controller = new AbortController(); const disconnect = () => { if (!res.writableEnded) controller.abort(); }; res.once('close', disconnect); const send = (status, html) => { if (controller.signal.aborted) return; res.writeHead(status, { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-store', 'X-Content-Type-Options': 'nosniff', 'Referrer-Policy': 'no-referrer', 'Content-Security-Policy': "default-src 'none'; img-src data:; style-src 'unsafe-inline'; script-src 'unsafe-inline'; connect-src 'self'; form-action 'self'; base-uri 'none'; frame-ancestors 'none'" }); res.end(html); }; let fields = {}; const fragment = req.headers['x-example-fragment'] === '1'; try { const host = req.headers.host ?? ''; if (!/^(?:127\.0\.0\.1|localhost)(?::[0-9]{1,5})?$/.test(host) || req.headers.origin && req.headers.origin !== `http://${host}` || req.headers['sec-fetch-site'] === 'cross-site') return send(403, '

Open the app on 127.0.0.1 to use this form.

'); if (req.url !== '/') return send(404, '

Page not found.

'); if (req.method === 'GET') return send(200, page(renderForm({}, now()))); if (req.method !== 'POST') return send(405, '

Use the form to submit a check.

'); fields = await readForm(req, FIELDS); const result = await submit(fields, parse, controller.signal, now()); send(200, fragment ? result : page(renderForm(fields, now()), result)); } catch (error) { if (controller.signal.aborted) return; const message = error instanceof InputError ? error.message : FAILURE; const result = ``; send(error instanceof InputError ? 400 : 502, fragment ? result : page(renderForm(fields, now()), result)); } }); server.requestTimeout = 10000; server.headersTimeout = 10000; return server; } function main() { const key = process.env.PARSEAPI_KEY?.trim(); if (!key || key.startsWith('parse_public_') || key.startsWith('parse_app_') || /[\r\n]/.test(key)) throw new Error('Set PARSEAPI_KEY to a server secret key.'); const port = process.env.PORT === undefined ? 8000 : Number(process.env.PORT); if (!Number.isInteger(port) || port < 1 || port > 65535) throw new Error('PORT must be from 1 to 65535.'); // API 2.1.0 is selected by @parseapi/sdk 1.14.0. No automatic retry of a submitted form. // example:client const parse = parseAPI(key, { retries: 0 }); // /example:client const server = createApp(parse); server.on('error', () => { console.error('The local app could not start. Check PORT.'); process.exitCode = 1; }); server.listen(port, '127.0.0.1', () => console.log(`Open http://127.0.0.1:${port}`)); } if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { try { main(); } catch (error) { console.error(error.message); process.exitCode = 1; } } // The website embeds this same app; the standalone server remains unchanged. export { page, renderForm, submit };