"use strict"; const { XArtAudioEngine } = require("./lib/engine"); const { verifySignature } = require("./lib/signature"); const { renderHomePage, renderAudioPage } = require("./views/pages"); function json(status, data) { return { status, headers: { "content-type": "application/json; charset=utf-8" }, body: JSON.stringify(data), }; } function html(status, markup) { return { status, headers: { "content-type": "text/html; charset=utf-8" }, body: markup, }; } function text(status, body, contentType) { return { status, headers: { "content-type": contentType || "text/plain; charset=utf-8" }, body, }; } function parseJSON(rawBody) { if (!rawBody) { return {}; } try { return JSON.parse(rawBody); } catch { throw new Error("invalid_json"); } } function getUserIdFromHeaders(headers) { return headers["x-user-id"] || null; } function buildApp({ config }) { const engine = new XArtAudioEngine({ creditConfig: config.credit, }); function handleXWebhook(headers, rawBody) { const signature = headers["x-signature"]; const isValid = verifySignature({ payload: rawBody, secret: config.xWebhookSecret, signature, }); if (!isValid) { return json(401, { error: "invalid_signature" }); } const payload = parseJSON(rawBody); const result = engine.processMention({ mentionPostId: payload.mentionPostId, callerUserId: payload.callerUserId, parentPost: payload.parentPost, }); if (!result.ok && result.status === "not_article") { return json(200, { status: "not_article", message: "This parent post is not an X Article.", }); } return json(200, { status: "completed", deduped: result.deduped, jobId: result.job.id, creditsCharged: result.job.creditsCharged, publicLink: result.reply ? result.reply.publicLink : `/audio/${result.job.assetId}`, }); } function handlePolarWebhook(headers, rawBody) { const signature = headers["x-signature"]; const isValid = verifySignature({ payload: rawBody, secret: config.polarWebhookSecret, signature, }); if (!isValid) { return json(401, { error: "invalid_signature" }); } const payload = parseJSON(rawBody); engine.topUpCredits(payload.userId, payload.credits, `polar:${payload.eventId}`); return json(200, { status: "credited" }); } function getJobsForUser(userId) { if (!userId) { return []; } return Array.from(engine.jobs.values()) .filter((job) => job.callerUserId === userId) .sort((a, b) => b.id.localeCompare(a.id)); } function handleRequest({ method, path, headers, rawBody }) { const safeHeaders = headers || {}; const userId = getUserIdFromHeaders(safeHeaders); if (method === "GET" && path === "/health") { return json(200, { ok: true }); } if (method === "GET" && path === "/manifest.webmanifest") { return text( 200, JSON.stringify({ name: "X Article to Audio", short_name: "XArtAudio", start_url: "/", display: "standalone", background_color: "#1d232a", theme_color: "#1d232a", icons: [], }), "application/manifest+json; charset=utf-8", ); } if (method === "GET" && path === "/sw.js") { return text( 200, "self.addEventListener('install', () => self.skipWaiting()); self.addEventListener('activate', () => self.clients.claim());", "application/javascript; charset=utf-8", ); } if (method === "GET" && path === "/") { return html(200, renderHomePage({ authenticated: Boolean(userId), userId, balance: userId ? engine.getWalletBalance(userId) : 0, jobs: getJobsForUser(userId), })); } if (method === "POST" && path === "/api/webhooks/x") { return handleXWebhook(safeHeaders, rawBody); } if (method === "POST" && path === "/api/webhooks/polar") { return handlePolarWebhook(safeHeaders, rawBody); } if (method === "POST" && path === "/api/dev/topup") { const body = parseJSON(rawBody); engine.topUpCredits(body.userId, body.amount, `dev:${Date.now()}:${body.userId}`); return json(200, { status: "credited" }); } if (method === "GET" && path === "/api/me/wallet") { if (!userId) { return json(401, { error: "auth_required" }); } return json(200, { balance: engine.getWalletBalance(userId) }); } if (method === "GET" && path.startsWith("/api/jobs/")) { if (!userId) { return json(401, { error: "auth_required" }); } const jobId = path.slice("/api/jobs/".length); const job = engine.getJob(jobId); if (!job) { return json(404, { error: "not_found" }); } if (job.callerUserId !== userId) { return json(403, { error: "forbidden" }); } return json(200, { job }); } if (method === "POST" && path.startsWith("/api/audio/") && path.endsWith("/unlock")) { if (!userId) { return json(401, { error: "auth_required" }); } const assetId = path.slice("/api/audio/".length, -"/unlock".length); try { const result = engine.unlockAudio(assetId, userId); return json(200, result); } catch (error) { return json(400, { error: error.message }); } } if (method === "GET" && path.startsWith("/audio/")) { const assetId = path.slice("/audio/".length); const audio = engine.getAsset(assetId); const accessDecision = audio ? engine.checkAudioAccess(assetId, userId) : { allowed: false, reason: "not_found" }; return html(200, renderAudioPage({ audio, accessDecision })); } return json(404, { error: "not_found" }); } return { engine, handleRequest, }; } module.exports = { buildApp, };