// InstantAdPay — membership advertising with immutable on-chain settlement. // Zero-dependency Node server (RM Circle pattern): static pages + JSON API, // wallet sign-in (SIWE), live contract ledger, sponsor join links. // // Chain selection (Amoy rehearsal vs mainnet) lives in data/config.json — // see chain.js. Wipe the volume's accounts/sessions + flip config = launch. const http = require('http'); const fs = require('fs'); const path = require('path'); const { URL } = require('url'); const chain = require('./chain'); const auth = require('./auth'); const accounts = require('./accounts'); const PORT = Number(process.env.PORT || 3000); const ROOT = __dirname; const PUBLIC_DIR = path.join(ROOT, 'public'); const DATA_DIR = process.env.DATA_DIR || path.join(ROOT, 'data'); const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD || 'changeme'; const IS_PROD = process.env.NODE_ENV === 'production'; const SITE_FILE = path.join(DATA_DIR, 'site.json'); fs.mkdirSync(DATA_DIR, { recursive: true }); chain.init({ onEvent: ev => pushFeed(ev) }); auth.init({ dataDir: DATA_DIR, chain, isProd: IS_PROD, site: 'instantadpay.com' }); accounts.init({ dataDir: DATA_DIR }); function siteConfig() { let saved = {}; try { saved = JSON.parse(fs.readFileSync(SITE_FILE, 'utf8')); } catch (e) {} return Object.assign({ siteName: 'InstantAdPay', tagline: 'Advertise and earn. Locked in code, not promises.', rehearsal: true // shows the testnet banner; flipped off at mainnet launch }, saved); } // ---- helpers ---- const MIME = { '.html': 'text/html; charset=utf-8', '.css': 'text/css', '.js': 'text/javascript', '.png': 'image/png', '.jpg': 'image/jpeg', '.svg': 'image/svg+xml', '.webp': 'image/webp', '.ico': 'image/x-icon', '.json': 'application/json', '.mp4': 'video/mp4', '.woff2': 'font/woff2' }; const CSP = "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self'; font-src 'self' data:; form-action 'self'"; function baseHeaders(extra) { return Object.assign({ 'Content-Security-Policy': CSP, 'X-Content-Type-Options': 'nosniff', 'Referrer-Policy': 'strict-origin-when-cross-origin' }, extra || {}); } function json(res, code, obj, extra) { const body = JSON.stringify(obj); res.writeHead(code, baseHeaders(Object.assign({ 'Content-Type': 'application/json', 'Cache-Control': 'no-store' }, extra))); res.end(body); } function sendFile(res, file) { fs.readFile(file, (err, data) => { if (err) { res.writeHead(404, baseHeaders({ 'Content-Type': 'text/plain' })); return res.end('Not found'); } const ext = path.extname(file).toLowerCase(); res.writeHead(200, baseHeaders({ 'Content-Type': MIME[ext] || 'application/octet-stream', 'Cache-Control': ext === '.html' ? 'no-cache' : 'public, max-age=3600' })); res.end(data); }); } function readBody(req) { return new Promise((resolve, reject) => { let d = ''; let n = 0; req.on('data', c => { n += c.length; if (n > 64 * 1024) { req.destroy(); reject(new Error('too big')); } d += c; }); req.on('end', () => { try { resolve(d ? JSON.parse(d) : {}); } catch (e) { reject(e); } }); req.on('error', reject); }); } function parseCookies(req) { const out = {}; for (const p of (req.headers.cookie || '').split(';')) { const i = p.indexOf('='); if (i > 0) out[p.slice(0, i).trim()] = decodeURIComponent(p.slice(i + 1).trim()); } return out; } function isAdmin(req) { const h = req.headers.authorization || ''; return h === 'Bearer ' + ADMIN_PASSWORD; } // ---- live feed (SSE) ---- const feedClients = new Set(); function pushFeed(ev) { const line = 'data: ' + JSON.stringify(ev) + '\n\n'; for (const res of feedClients) { try { res.write(line); } catch (e) { feedClients.delete(res); } } } // ---- server ---- const server = http.createServer(async (req, res) => { try { const u = new URL(req.url, 'http://x'); const p = u.pathname; // -- join links: /join/ — first-touch attribution cookie let m = /^\/join\/(\d{1,9})$/.exec(p); if (m && req.method === 'GET') { const sid = Number(m[1]); const cookies = parseCookies(req); const headers = { Location: '/' }; if (!cookies['iap.sponsor']) { headers['Set-Cookie'] = `iap.sponsor=${sid}; Path=/; SameSite=Lax; Max-Age=${180 * 24 * 3600}${IS_PROD ? '; Secure' : ''}`; } res.writeHead(302, baseHeaders(headers)); return res.end(); } // -- public API if (p === '/api/config' && req.method === 'GET') { const c = chain.getConfig(); return json(res, 200, Object.assign({ contract: c.contract, chainId: c.chainId, chainName: c.chainName, explorer: c.explorer, rpc: c.rpcs[0] }, siteConfig())); } if (p === '/api/catalog' && req.method === 'GET') { return json(res, 200, { products: await chain.catalog() }); } if (p === '/api/feed' && req.method === 'GET') { return json(res, 200, { events: chain.recentEvents(Number(u.searchParams.get('n')) || 100) }); } if (p === '/api/feed/live' && req.method === 'GET') { res.writeHead(200, baseHeaders({ 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-store', Connection: 'keep-alive' })); res.write(': connected\n\n'); feedClients.add(res); req.on('close', () => feedClients.delete(res)); return; } if (p === '/api/sponsor' && req.method === 'GET') { const sid = Number(parseCookies(req)['iap.sponsor']) || 0; let sponsor = null; if (sid) { try { const mm = await chain.member(sid); if (mm.account !== '0x' + '0'.repeat(40)) sponsor = { id: sid }; } catch (e) {} } return json(res, 200, { sponsorId: sponsor ? sid : 0 }); } if (p === '/api/stats' && req.method === 'GET') { let members = 0; try { members = await chain.memberCount(); } catch (e) {} return json(res, 200, { onchainMembers: members, siteAccounts: accounts.count() }); } // -- accounts: email + password is the normal join path (wallet comes // out only at purchase / payout-activation time and gets linked then) if (p === '/api/signup' && req.method === 'POST') { const b = await readBody(req); const sid = Number(parseCookies(req)['iap.sponsor']) || 0; // first-touch attribution const r = accounts.signup(b.email, b.password, sid); if (r.error) return json(res, 400, r); const token = auth.mintSession({ email: r.account.email }); return json(res, 200, { ok: true, account: r.account }, { 'Set-Cookie': auth.sessionCookie(token) }); } if (p === '/api/login' && req.method === 'POST') { const b = await readBody(req); const r = accounts.login(b.email, b.password); if (r.error) return json(res, 400, r); let memberId = 0; if (r.account.address) { try { memberId = await chain.memberIdByAccount(r.account.address); } catch (e) {} } const token = auth.mintSession({ email: r.account.email, address: r.account.address, memberId }); return json(res, 200, { ok: true, account: r.account }, { 'Set-Cookie': auth.sessionCookie(token) }); } // -- wallet auth: link-to-account when an email session exists, or // wallet-first sign-in for crypto-native users if (p === '/api/auth/challenge' && req.method === 'POST') { const b = await readBody(req); const r = auth.makeChallenge(b.address); return json(res, r.error ? 400 : 200, r); } if (p === '/api/auth/verify' && req.method === 'POST') { const b = await readBody(req); const r = await auth.verifyChallenge(b.address, b.signature); if (r.error) return json(res, 400, r); let memberId = 0; try { memberId = await chain.memberIdByAccount(r.address); } catch (e) {} const s = auth.fromRequest(req); if (s && s.email) { const lr = accounts.linkWallet(s.email, r.address); if (lr.error) return json(res, 400, lr); auth.updateSession(s.token, { address: r.address, memberId }); return json(res, 200, { ok: true, linked: true, address: r.address, memberId }); } const acct = accounts.byAddress(r.address); const token = auth.mintSession({ email: acct ? acct.email : null, address: r.address, memberId }); return json(res, 200, { ok: true, address: r.address, memberId }, { 'Set-Cookie': auth.sessionCookie(token) }); } if (p === '/api/auth/logout' && req.method === 'POST') { auth.logout(req); return json(res, 200, { ok: true }, { 'Set-Cookie': auth.clearCookie() }); } if (p === '/api/me' && req.method === 'GET') { const s = auth.fromRequest(req); if (!s) return json(res, 200, { signedIn: false }); const memberId = await auth.refreshMemberId(s); const acct = (s.email && accounts.byEmail(s.email)) || (s.address && accounts.byAddress(s.address)) || null; const out = { signedIn: true, email: s.email || (acct && acct.email) || null, address: s.address || (acct && acct.address) || null, memberId, sponsorId: (acct && acct.sponsorId) || Number(parseCookies(req)['iap.sponsor']) || 0 }; if (memberId) { try { const mm = await chain.member(memberId); out.buyerCount = mm.buyerCount; out.onchainSponsorId = mm.sponsorId; out.credits = await chain.creditBalance(memberId, 0); } catch (e) { out.chainReadError = true; } } return json(res, 200, out); } if (p === '/api/my/activity' && req.method === 'GET') { const s = auth.fromRequest(req); if (!s) return json(res, 401, { error: 'Sign in first.' }); const id = s.memberId || await auth.refreshMemberId(s); if (!id) return json(res, 200, { memberId: 0, earnings: [], purchases: [], referrals: [] }); const evs = chain.recentEvents(600); return json(res, 200, { memberId: id, earnings: evs.filter(e => (e.type === 'TierPaid' && e.recipientId === id) || (e.type === 'AwardPaid' && e.toId === id)), purchases: evs.filter(e => e.type === 'Purchase' && e.buyerId === id), referrals: evs.filter(e => (e.type === 'MemberActivated' && e.sponsorId === id) || (e.type === 'BuyerCounted' && e.sponsorId === id)) }); } // -- admin (Bearer ADMIN_PASSWORD) if (p === '/api/admin/site' && req.method === 'PATCH') { if (!isAdmin(req)) return json(res, 401, { error: 'auth' }); const b = await readBody(req); const cur = siteConfig(); fs.writeFileSync(SITE_FILE, JSON.stringify(Object.assign(cur, b), null, 2)); return json(res, 200, { ok: true, site: siteConfig() }); } if (p === '/api/admin/chain' && req.method === 'PATCH') { if (!isAdmin(req)) return json(res, 401, { error: 'auth' }); const b = await readBody(req); const file = path.join(DATA_DIR, 'config.json'); let cur = {}; try { cur = JSON.parse(fs.readFileSync(file, 'utf8')); } catch (e) {} fs.writeFileSync(file, JSON.stringify(Object.assign(cur, b), null, 2)); chain.reloadConfig(); return json(res, 200, { ok: true, config: chain.getConfig() }); } // -- pages if (req.method === 'GET') { if (p === '/') return sendFile(res, path.join(PUBLIC_DIR, 'index.html')); if (p === '/ledger') return sendFile(res, path.join(PUBLIC_DIR, 'ledger.html')); if (p === '/my') return sendFile(res, path.join(PUBLIC_DIR, 'my.html')); const safe = path.normalize(p).replace(/^([.\\/])+/, ''); const file = path.join(PUBLIC_DIR, safe); if (file.startsWith(PUBLIC_DIR) && fs.existsSync(file) && fs.statSync(file).isFile()) return sendFile(res, file); } res.writeHead(404, baseHeaders({ 'Content-Type': 'text/plain' })); res.end('Not found'); } catch (e) { console.error('request error', req.url, e.message); try { json(res, 500, { error: 'server error' }); } catch (_) {} } }); server.listen(PORT, () => console.log(`InstantAdPay site on :${PORT} — chain: ${chain.getConfig().chainName}`));