Files
bitsforfree/server.js
T
smileBeda 48f05bcf8a feat: release meme protocol v1.1.0
Add one-click meme copying, complete viewer actions, fixed-footer pagination, protocol lore, and the Homo Reseticus download chooser.

Install dedicated social and favicon assets, expand local demo data, and synchronize the changelog, documentation, package, and OpenAPI release metadata.
2026-08-14 18:54:37 -03:00

500 lines
17 KiB
JavaScript

import http from 'node:http';
import crypto from 'node:crypto';
import fs from 'node:fs/promises';
import { URL } from 'node:url';
import { parseMultipartUpload } from './src/multipart.js';
import { sendFile, sendHtml, sendJson, sendText, withSecurityHeaders } from './src/http.js';
import { createStore } from './src/store.js';
import { validateImage } from './src/image.js';
import { normalizeToWebp } from './src/normalize.js';
import { seedDemoMemes } from './src/seed.js';
import { moderateImage } from './src/moderation.js';
import { createUploadLimiter } from './src/uploadLimits.js';
import { createIpRateLimiter } from './src/rateLimits.js';
import { feedJson, llmsTxt, manifest, noIndex, publicBaseUrl, renderIndex, robotsTxt, sitemapXml } from './src/seo.js';
await loadDotEnv();
const PORT = Number.parseInt(process.env.PORT || '8080', 10);
const HOST = process.env.HOST || '0.0.0.0';
const DATA_DIR = process.env.DATA_DIR || './data';
const PAGE_SIZE_MAX = 48;
const UPLOAD_MAX_BYTES = 5 * 1024 * 1024;
const REQUEST_MAX_BYTES = 6 * 1024 * 1024;
const API_READ_LIMIT_PER_MINUTE = 120;
const MEDIA_READ_LIMIT_PER_MINUTE = 600;
const SSE_HEARTBEAT_MS = 25_000;
const events = new Set();
const DISCOVERY_ROUTES = new Set([
'/openapi.json',
'/meme-api.skill.md',
'/robots.txt',
'/llms.txt',
'/site.webmanifest',
'/feed.json',
'/sitemap.xml'
]);
const ADMIN_TOKEN = process.env.ADMIN_TOKEN || crypto.randomBytes(24).toString('hex');
const indexTemplate = await fs.readFile('./public/index.html', 'utf8');
const openApiSpec = JSON.parse(await fs.readFile('./openapi.json', 'utf8'));
const memeApiSkillTemplate = await fs.readFile('./meme-api.skill.md', 'utf8');
const store = await createStore({ dataDir: DATA_DIR });
const uploadLimiter = await createUploadLimiter({ dataDir: DATA_DIR });
const apiReadLimiter = createIpRateLimiter({
windowMs: 60_000,
max: API_READ_LIMIT_PER_MINUTE,
message: 'API read limit exceeded. Try again shortly.'
});
const mediaReadLimiter = createIpRateLimiter({
windowMs: 60_000,
max: MEDIA_READ_LIMIT_PER_MINUTE,
message: 'Media read limit exceeded. Try again shortly.'
});
if (process.env.SEED_DEMO_MEMES !== 'false') {
await seedDemoMemes(store);
}
if (!process.env.ADMIN_TOKEN) {
console.log(`Admin review URL: /admin/${ADMIN_TOKEN}`);
}
const server = http.createServer(async (req, res) => {
try {
const url = new URL(req.url || '/', `http://${req.headers.host || 'localhost'}`);
withSecurityHeaders(res, { contentSecurityPolicy: !isDiscoveryRoute(req, url) });
const baseUrl = publicBaseUrl(req);
if (req.method === 'GET' && url.pathname === '/') {
const nonce = crypto.randomBytes(16).toString('base64');
withSecurityHeaders(res, { scriptNonce: nonce });
return sendHtml(res, 200, renderIndex({
template: indexTemplate,
baseUrl,
nonce,
approvedCount: store.count('approved')
}));
}
if (req.method === 'GET' && (url.pathname === '/lore' || url.pathname === '/lore/')) {
const nonce = crypto.randomBytes(16).toString('base64');
withSecurityHeaders(res, { scriptNonce: nonce });
return sendHtml(res, 200, renderIndex({
template: indexTemplate,
baseUrl,
nonce,
approvedCount: store.count('approved'),
lore: true
}));
}
const memePageMatch = url.pathname.match(/^\/meme\/([a-f0-9]{64})\/?$/);
if (req.method === 'GET' && memePageMatch) {
const meme = store.get(memePageMatch[1]);
if (!meme || meme.status !== 'approved') return sendText(res, 404, 'Not found');
const nonce = crypto.randomBytes(16).toString('base64');
withSecurityHeaders(res, { scriptNonce: nonce });
return sendHtml(res, 200, renderIndex({
template: indexTemplate,
baseUrl,
nonce,
approvedCount: store.count('approved'),
meme: publicMeme(meme)
}));
}
if (req.method === 'GET' && url.pathname === '/openapi.json') {
res.setHeader('Cache-Control', 'public, max-age=300');
return sendJson(res, 200, openApiSpecFor(baseUrl));
}
if (req.method === 'GET' && url.pathname === '/meme-api.skill.md') {
res.setHeader('Cache-Control', 'public, max-age=300');
return sendText(res, 200, renderMemeApiSkill(baseUrl), 'text/markdown; charset=utf-8');
}
if (req.method === 'GET' && url.pathname === '/robots.txt') {
return sendText(res, 200, robotsTxt(baseUrl));
}
if (req.method === 'GET' && url.pathname === '/llms.txt') {
return sendText(res, 200, llmsTxt(baseUrl));
}
if (req.method === 'GET' && url.pathname === '/site.webmanifest') {
return sendJson(res, 200, manifest(baseUrl));
}
if (req.method === 'GET' && url.pathname === '/favicon.ico') {
return sendFile(res, './public/assets/favicon/favicon.ico');
}
if (req.method === 'GET' && url.pathname === '/feed.json') {
return sendJson(res, 200, feedJson(baseUrl, store.listForReview({ status: 'approved' }).memes));
}
if (req.method === 'GET' && url.pathname === '/sitemap.xml') {
const body = sitemapXml(baseUrl, store.listForReview({ status: 'approved' }).memes);
res.writeHead(200, {
'Content-Type': 'application/xml; charset=utf-8',
'Content-Length': String(Buffer.byteLength(body))
});
return res.end(body);
}
const adminPageMatch = url.pathname.match(/^\/admin\/([A-Za-z0-9_-]{24,128})$/);
if (req.method === 'GET' && adminPageMatch) {
if (!isAdminToken(adminPageMatch[1])) return sendText(res, 404, 'Not found');
noIndex(res);
return sendFile(res, './public/admin.html');
}
if (req.method === 'GET' && url.pathname.startsWith('/assets/')) {
return sendFile(res, `./public${url.pathname}`);
}
if (req.method === 'GET' && url.pathname === '/api/memes') {
noIndex(res);
const readQuota = checkReadLimit(req, res, apiReadLimiter);
const page = positiveInt(url.searchParams.get('page'), 1);
const pageSize = Math.min(positiveInt(url.searchParams.get('pageSize'), 12), PAGE_SIZE_MAX);
return sendJson(res, 200, {
...store.list({ page, pageSize }),
rateLimit: readQuota
});
}
if (req.method === 'GET' && url.pathname === '/api/status') {
noIndex(res);
checkReadLimit(req, res, apiReadLimiter);
return sendJson(res, 200, {
ok: true,
memeCount: store.count('approved'),
liveClients: events.size,
uptimeSeconds: Math.floor(process.uptime())
});
}
if (req.method === 'GET' && url.pathname === '/api/admin/pending') {
noIndex(res);
if (!isAdminRequest(req)) return sendJson(res, 404, { error: 'Not found' });
return sendJson(res, 200, store.listForReview({ status: 'pending' }));
}
if (req.method === 'POST' && url.pathname === '/api/admin/approve') {
noIndex(res);
if (!isAdminRequest(req)) return sendJson(res, 404, { error: 'Not found' });
const body = await readJsonBody(req, 64 * 1024);
const approved = await store.approve(safeIds(body.ids));
return sendJson(res, 200, { approved });
}
if (req.method === 'POST' && url.pathname === '/api/admin/delete') {
noIndex(res);
if (!isAdminRequest(req)) return sendJson(res, 404, { error: 'Not found' });
const body = await readJsonBody(req, 64 * 1024);
const deleted = await store.delete(safeIds(body.ids));
return sendJson(res, 200, { deleted });
}
if (req.method === 'GET' && url.pathname === '/api/events') {
noIndex(res);
res.writeHead(200, {
'Content-Type': 'text/event-stream; charset=utf-8',
'Cache-Control': 'no-cache, no-transform',
Connection: 'keep-alive',
'X-Accel-Buffering': 'no'
});
res.write('retry: 5000\n');
res.write('event: ready\ndata: {}\n\n');
events.add(res);
const heartbeat = setInterval(() => {
res.write(': keep-alive\n\n');
}, SSE_HEARTBEAT_MS);
req.on('close', () => {
clearInterval(heartbeat);
events.delete(res);
});
return;
}
if (req.method === 'POST' && url.pathname === '/api/memes') {
noIndex(res);
const { meme, quota } = await addMeme(req);
return sendJson(res, meme.status === 'approved' ? 201 : 202, {
meme,
quota,
moderationScore: meme.moderationScore,
moderationReason: meme.moderationReason,
message: meme.status === 'approved' ? 'Upload approved.' : 'Upload queued for admin review.'
});
}
const apiMemeMatch = url.pathname.match(/^\/api\/memes\/([a-f0-9]{64})$/);
if (req.method === 'GET' && apiMemeMatch) {
noIndex(res);
const readQuota = checkReadLimit(req, res, apiReadLimiter);
const meme = store.get(apiMemeMatch[1]);
if (!meme || meme.status !== 'approved') return sendJson(res, 404, { error: 'Not found' });
return sendJson(res, 200, { meme: publicMeme(meme), rateLimit: readQuota });
}
const viewMatch = url.pathname.match(/^\/api\/memes\/([a-f0-9]{64})\/view$/);
if (req.method === 'POST' && viewMatch) {
if (store.get(viewMatch[1])?.status !== 'approved') return sendText(res, 404, 'Not found');
const meme = await store.incrementMetric(viewMatch[1], 'viewCount');
if (!meme) return sendText(res, 404, 'Not found');
broadcastMetric(meme);
return sendJson(res, 200, { meme });
}
const mediaMatch = url.pathname.match(/^\/media\/([a-f0-9]{64})$/);
if (req.method === 'GET' && mediaMatch) {
checkReadLimit(req, res, mediaReadLimiter);
const meme = store.get(mediaMatch[1]);
if (!meme || meme.status !== 'approved') return sendText(res, 404, 'Not found');
res.setHeader('Content-Type', meme.mime);
res.setHeader('Content-Length', String(meme.byteSize));
res.setHeader('Cache-Control', 'public, max-age=31536000, immutable');
res.setHeader('X-Content-Type-Options', 'nosniff');
return sendFile(res, store.absolutePath(meme.storageKey), { absolute: true });
}
const adminMediaMatch = url.pathname.match(/^\/admin-media\/([A-Za-z0-9_-]{24,128})\/([a-f0-9]{64})$/);
if (req.method === 'GET' && adminMediaMatch) {
noIndex(res);
if (!isAdminToken(adminMediaMatch[1])) return sendText(res, 404, 'Not found');
const meme = store.get(adminMediaMatch[2]);
if (!meme) return sendText(res, 404, 'Not found');
res.setHeader('Content-Type', meme.mime);
res.setHeader('Content-Length', String(meme.byteSize));
res.setHeader('Cache-Control', 'no-store');
res.setHeader('X-Content-Type-Options', 'nosniff');
return sendFile(res, store.absolutePath(meme.storageKey), { absolute: true });
}
const downloadMatch = url.pathname.match(/^\/download\/([a-f0-9]{64})$/);
if (req.method === 'GET' && downloadMatch) {
noIndex(res);
checkReadLimit(req, res, mediaReadLimiter);
if (store.get(downloadMatch[1])?.status !== 'approved') return sendText(res, 404, 'Not found');
const updated = await store.incrementMetric(downloadMatch[1], 'downloadCount');
const meme = store.get(downloadMatch[1]);
if (!meme) return sendText(res, 404, 'Not found');
broadcastMetric(updated);
res.setHeader('Content-Type', meme.mime);
res.setHeader('Content-Disposition', `attachment; filename="${downloadName(meme)}"`);
res.setHeader('X-Content-Type-Options', 'nosniff');
return sendFile(res, store.absolutePath(meme.storageKey), { absolute: true });
}
if (req.method === 'GET' && url.pathname === '/healthz') {
noIndex(res);
return sendJson(res, 200, { ok: true });
}
return sendText(res, 404, 'Not found');
} catch (error) {
const status = error.statusCode || 500;
const message = status === 500 ? 'Internal server error.' : error.message;
if (status === 429 && error.retryAfterSeconds) {
res.setHeader('Retry-After', String(error.retryAfterSeconds));
}
if (status === 429 && error.rateLimit) {
setRateLimitHeaders(res, error.rateLimit);
}
if (error.payload && typeof error.payload === 'object') {
return sendJson(res, status, error.payload);
}
return sendJson(res, status, { error: message });
}
});
server.listen(PORT, HOST, () => {
console.log(`The Meme Protocol listening on http://${HOST}:${PORT}`);
});
function positiveInt(value, fallback) {
const parsed = Number.parseInt(value || '', 10);
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
}
function isDiscoveryRoute(req, url) {
if (req.method !== 'GET') return false;
return DISCOVERY_ROUTES.has(url.pathname);
}
async function loadDotEnv() {
let content = '';
try {
content = await fs.readFile('.env', 'utf8');
} catch (error) {
if (error.code !== 'ENOENT') throw error;
return;
}
for (const line of content.split('\n')) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith('#')) continue;
const match = trimmed.match(/^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/);
if (!match || process.env[match[1]] !== undefined) continue;
process.env[match[1]] = unquoteEnvValue(match[2].trim());
}
}
function unquoteEnvValue(value) {
if (
(value.startsWith('"') && value.endsWith('"')) ||
(value.startsWith("'") && value.endsWith("'"))
) {
return value.slice(1, -1);
}
return value;
}
function openApiSpecFor(baseUrl) {
return {
...openApiSpec,
servers: [{ url: baseUrl }]
};
}
function renderMemeApiSkill(baseUrl) {
return memeApiSkillTemplate.replaceAll('__API_BASE_URL__', baseUrl);
}
function downloadName(meme) {
return `meme-protocol-${meme.id.slice(0, 12)}.${meme.ext}`;
}
function broadcastMetric(meme) {
const payload = JSON.stringify({
id: meme.id,
viewCount: meme.viewCount,
downloadCount: meme.downloadCount
});
for (const res of events) {
res.write(`event: metric\ndata: ${payload}\n\n`);
}
}
async function addMeme(req) {
const contentLength = Number.parseInt(req.headers['content-length'] || '0', 10);
if (!Number.isFinite(contentLength) || contentLength <= 0) {
const error = new Error('Missing upload size.');
error.statusCode = 411;
throw error;
}
if (contentLength > REQUEST_MAX_BYTES) {
const error = new Error('Upload request is too large.');
error.statusCode = 413;
throw error;
}
const quota = await uploadLimiter.checkAndConsume(clientIp(req));
const upload = await parseMultipartUpload(req, {
maxRequestBytes: REQUEST_MAX_BYTES,
maxFileBytes: UPLOAD_MAX_BYTES,
fieldName: 'meme'
});
const image = validateImage(upload.buffer, {
maxBytes: UPLOAD_MAX_BYTES,
maxWidth: 6000,
maxHeight: 6000,
maxPixels: 20_000_000
});
const normalized = await normalizeToWebp(upload.buffer);
const moderation = await moderateImage({ buffer: normalized.buffer, mime: normalized.image.mime });
if (moderation.status === 'rejected') {
const error = new Error(`Upload rejected: ${moderation.reason}`);
error.statusCode = 422;
error.payload = {
error: error.message,
moderationScore: moderation.score,
moderationReason: moderation.reason
};
throw error;
}
const meme = await store.save({
buffer: normalized.buffer,
image: normalized.image,
originalName: upload.filename,
originalMime: image.mime,
status: moderation.status,
moderationScore: moderation.score,
moderationReason: moderation.reason
});
return { meme, quota };
}
function checkReadLimit(req, res, limiter) {
const quota = limiter.check(clientIp(req));
setRateLimitHeaders(res, quota);
return quota;
}
function setRateLimitHeaders(res, quota) {
res.setHeader('RateLimit-Limit', String(quota.limit));
res.setHeader('RateLimit-Remaining', String(quota.remaining));
res.setHeader('RateLimit-Reset', quota.resetAt);
}
function publicMeme(record) {
return {
id: record.id,
createdAt: record.createdAt,
byteSize: record.byteSize,
width: record.width,
height: record.height,
mime: record.mime,
originalMime: record.originalMime,
status: record.status,
moderationScore: record.moderationScore,
moderationReason: record.moderationReason,
viewCount: record.viewCount,
downloadCount: record.downloadCount,
originalName: record.originalName,
url: `/media/${record.id}`,
downloadUrl: `/download/${record.id}`,
demo: Boolean(record.demo)
};
}
function isAdminRequest(req) {
return isAdminToken(req.headers['x-admin-token'] || '');
}
function isAdminToken(value) {
const received = Buffer.from(String(value));
const expected = Buffer.from(ADMIN_TOKEN);
return received.length === expected.length && crypto.timingSafeEqual(received, expected);
}
async function readJsonBody(req, maxBytes) {
const chunks = [];
let total = 0;
for await (const chunk of req) {
total += chunk.length;
if (total > maxBytes) {
const error = new Error('Request body is too large.');
error.statusCode = 413;
throw error;
}
chunks.push(chunk);
}
if (total === 0) return {};
return JSON.parse(Buffer.concat(chunks, total).toString('utf8'));
}
function safeIds(ids) {
if (!Array.isArray(ids)) return [];
return ids.filter((id) => typeof id === 'string' && /^[a-f0-9]{64}$/.test(id));
}
function clientIp(req) {
if (process.env.TRUST_PROXY === 'true') {
const forwarded = String(req.headers['x-forwarded-for'] || '').split(',')[0].trim();
if (forwarded) return forwarded;
}
return req.socket.remoteAddress || 'unknown';
}