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 === '/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 === '/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 submitMeme(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 submitMeme(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'; }