PolHunter shell: default-deny gates (OUTBOUND, SIGNUPS, CURTAIN), health, placeholder page
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
node_modules/
|
||||
data/
|
||||
*.log
|
||||
*.key
|
||||
.env
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
FROM node:22-alpine
|
||||
WORKDIR /app
|
||||
COPY package.json ./
|
||||
RUN npm install --omit=dev --no-audit --no-fund
|
||||
COPY . .
|
||||
RUN mkdir -p /app/data
|
||||
ENV NODE_ENV=production
|
||||
ENV PORT=3000
|
||||
EXPOSE 3000
|
||||
CMD ["node","server.js"]
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "polhunter",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"description": "PolHunter: gamified visits across the network, paid in POL.",
|
||||
"main": "server.js",
|
||||
"scripts": {
|
||||
"start": "node server.js",
|
||||
"dev": "node --watch server.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
},
|
||||
"dependencies": {}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>PolHunter</title>
|
||||
<style>
|
||||
:root{--bg:#0b0a14;--ink:#f1eefb;--muted:#a79fc4;--pol:#8247e5;--pol-hi:#a97cf5;--gold:#f3be43}
|
||||
*{margin:0;padding:0;box-sizing:border-box}
|
||||
body{min-height:100vh;display:flex;align-items:center;justify-content:center;padding:32px 20px;background:radial-gradient(circle at 20% 10%,rgba(130,71,229,.28),transparent 45%),radial-gradient(circle at 85% 90%,rgba(243,190,67,.16),transparent 40%),var(--bg);color:var(--ink);font:16px/1.6 system-ui,-apple-system,"Segoe UI",sans-serif}
|
||||
.wrap{max-width:640px;text-align:center}
|
||||
.mark{display:inline-flex;align-items:center;gap:12px;font-weight:800;font-size:22px;letter-spacing:-.3px;margin-bottom:28px}
|
||||
.mark i{width:34px;height:34px;border-radius:50%;background:var(--pol);box-shadow:0 0 34px rgba(130,71,229,.7)}
|
||||
h1{font-size:clamp(32px,7vw,54px);line-height:1.05;font-weight:800;letter-spacing:-1px;margin-bottom:16px}
|
||||
h1 b{color:var(--gold)}
|
||||
p{color:var(--muted);font-size:18px;max-width:520px;margin:0 auto 22px}
|
||||
.pill{display:inline-block;padding:8px 16px;border:1px solid rgba(167,159,196,.35);border-radius:999px;color:var(--muted);font-size:13px;letter-spacing:.08em;text-transform:uppercase}
|
||||
.fine{margin-top:40px;font-size:12px;color:#6e678a}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<div class="mark"><i></i>PolHunter</div>
|
||||
<h1>Visit. Find it. <b>Get paid in POL.</b></h1>
|
||||
<p>Missions across the network. Each one takes a few minutes on a site, a thing to find, and a drip of POL to your wallet when you find it.</p>
|
||||
<span class="pill">Building — opens soon</span>
|
||||
<div class="fine">Rewards are for completed missions, not income. Cryptocurrency involves risk of loss.</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,82 @@
|
||||
// PolHunter: gamified visits across the network, paid in POL.
|
||||
//
|
||||
// This is the shell the hunt engine will grow inside. What it does today is refuse to do anything
|
||||
// dangerous by default, because the day before it was built a test area in this same network
|
||||
// emailed 213 real people. Three gates, all default-deny, all read from the environment:
|
||||
//
|
||||
// OUTBOUND=on nothing can send mail (or anything else outward) without it. There is no
|
||||
// mailer in this app yet; when one arrives it must check outbound() first.
|
||||
// SIGNUPS=open the sign-in door stays shut until this is set. Hunters will sign in with
|
||||
// their InstantAdPay account; that bridge does not exist yet either.
|
||||
// CURTAIN=<secret> while set, every request gets a contentless "Coming soon" page unless the
|
||||
// browser has visited ?k=<secret> once. Lift it by unsetting the variable.
|
||||
//
|
||||
// A missing variable means silence, never delivery.
|
||||
'use strict';
|
||||
const http = require('http');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const PORT = Number(process.env.PORT || 3000);
|
||||
const DATA_DIR = process.env.DATA_DIR || path.join(__dirname, 'data');
|
||||
const PUBLIC_DIR = path.join(__dirname, 'public');
|
||||
const CURTAIN = String(process.env.CURTAIN || '').trim();
|
||||
const outbound = () => process.env.OUTBOUND === 'on';
|
||||
const signupsOpen = () => process.env.SIGNUPS === 'open';
|
||||
|
||||
try { fs.mkdirSync(DATA_DIR, { recursive: true }); } catch (e) {}
|
||||
|
||||
const CURTAIN_PAGE = `<!doctype html><html lang="en"><head><meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1"><meta name="robots" content="noindex,nofollow">
|
||||
<title>Coming soon</title><style>
|
||||
*{margin:0;padding:0;box-sizing:border-box} html,body{height:100%}
|
||||
body{display:flex;align-items:center;justify-content:center;padding:24px;background:#0d1117;color:#e6edf3;font:16px/1.6 system-ui,-apple-system,"Segoe UI",sans-serif}
|
||||
.card{max-width:420px;text-align:center} h1{font-size:clamp(28px,7vw,44px);font-weight:700;letter-spacing:-.5px;margin-bottom:14px} p{color:#8b949e}
|
||||
</style></head><body><div class="card"><h1>Coming soon</h1><p>This site is still being built.</p></div></body></html>`;
|
||||
|
||||
function curtained(req, res, u) {
|
||||
if (!CURTAIN) return false;
|
||||
if (u.searchParams.get('k') === CURTAIN) {
|
||||
u.searchParams.delete('k');
|
||||
res.writeHead(302, {
|
||||
'Set-Cookie': 'ph.pass=' + encodeURIComponent(CURTAIN) + '; Path=/; Max-Age=2592000; HttpOnly; SameSite=Lax; Secure',
|
||||
Location: u.pathname + (u.searchParams.toString() ? '?' + u.searchParams : ''),
|
||||
'Cache-Control': 'no-store'
|
||||
});
|
||||
res.end();
|
||||
return true;
|
||||
}
|
||||
const m = /(?:^|;\s*)ph\.pass=([^;]*)/.exec(req.headers.cookie || '');
|
||||
if (m && decodeURIComponent(m[1]) === CURTAIN) return false;
|
||||
res.writeHead(503, { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-store', 'X-Robots-Tag': 'noindex, nofollow' });
|
||||
res.end(req.method === 'HEAD' ? '' : CURTAIN_PAGE);
|
||||
return true;
|
||||
}
|
||||
|
||||
const TYPES = { '.html': 'text/html; charset=utf-8', '.css': 'text/css', '.js': 'application/javascript', '.png': 'image/png', '.jpg': 'image/jpeg', '.svg': 'image/svg+xml', '.ico': 'image/x-icon', '.json': 'application/json' };
|
||||
function sendFile(res, file) {
|
||||
fs.readFile(file, (err, buf) => {
|
||||
if (err) { res.writeHead(404, { 'Content-Type': 'text/plain' }); return res.end('Not found'); }
|
||||
res.writeHead(200, { 'Content-Type': TYPES[path.extname(file)] || 'application/octet-stream', 'Cache-Control': 'no-store' });
|
||||
res.end(buf);
|
||||
});
|
||||
}
|
||||
const json = (res, code, body) => { res.writeHead(code, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' }); res.end(JSON.stringify(body)); };
|
||||
|
||||
const server = http.createServer((req, res) => {
|
||||
const u = new URL(req.url, 'http://x');
|
||||
const p = u.pathname;
|
||||
if (p === '/health') return json(res, 200, { ok: true, outbound: outbound(), signups: signupsOpen(), curtain: !!CURTAIN });
|
||||
if (curtained(req, res, u)) return; // nothing below runs for an uninvited visitor
|
||||
|
||||
if (p === '/api/config') return json(res, 200, { name: 'PolHunter', signupsOpen: signupsOpen(), outbound: outbound() });
|
||||
if (p.startsWith('/api/')) return json(res, 404, { error: 'Not built yet.' });
|
||||
|
||||
// static: the public dir, index for /
|
||||
const safe = path.normalize(p).replace(/^(\.\.[/\\])+/, '');
|
||||
const file = path.join(PUBLIC_DIR, safe === '/' || safe === '\\' ? 'index.html' : safe);
|
||||
if (!file.startsWith(PUBLIC_DIR)) { res.writeHead(400); return res.end(); }
|
||||
sendFile(res, file);
|
||||
});
|
||||
|
||||
server.listen(PORT, () => console.log(`PolHunter on :${PORT} — outbound: ${outbound() ? 'ON' : 'OFF'} — sign-ups: ${signupsOpen() ? 'open' : 'CLOSED'} — curtain: ${CURTAIN ? 'up' : 'down'}`));
|
||||
Reference in New Issue
Block a user