From 1a6166abc3601f11009369f6434833b31ee1bf62 Mon Sep 17 00:00:00 2001 From: martbost Date: Sat, 19 Sep 2026 10:16:11 -0500 Subject: [PATCH] PolHunter shell: default-deny gates (OUTBOUND, SIGNUPS, CURTAIN), health, placeholder page Co-Authored-By: Claude Fable 5.1 --- .gitignore | 5 +++ Dockerfile | 10 ++++++ package.json | 15 +++++++++ public/index.html | 30 +++++++++++++++++ server.js | 82 +++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 142 insertions(+) create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 package.json create mode 100644 public/index.html create mode 100644 server.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..eb21fc4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +node_modules/ +data/ +*.log +*.key +.env diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..4dc953c --- /dev/null +++ b/Dockerfile @@ -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"] diff --git a/package.json b/package.json new file mode 100644 index 0000000..323a033 --- /dev/null +++ b/package.json @@ -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": {} +} diff --git a/public/index.html b/public/index.html new file mode 100644 index 0000000..f09cf96 --- /dev/null +++ b/public/index.html @@ -0,0 +1,30 @@ + + + + + +PolHunter + + + +
+
PolHunter
+

Visit. Find it. Get paid in POL.

+

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.

+ Building — opens soon +
Rewards are for completed missions, not income. Cryptocurrency involves risk of loss.
+
+ + diff --git a/server.js b/server.js new file mode 100644 index 0000000..4486bc6 --- /dev/null +++ b/server.js @@ -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= while set, every request gets a contentless "Coming soon" page unless the +// browser has visited ?k= 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 = ` + +Coming soon

Coming soon

This site is still being built.

`; + +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'}`));