feat: add webhook-first app router with wallet, job, and unlock endpoints

This commit is contained in:
Codex
2026-02-18 12:36:05 +00:00
parent a7526e12ec
commit fedab9f7bd
3 changed files with 367 additions and 0 deletions

193
src/app.js Normal file
View File

@@ -0,0 +1,193 @@
"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 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 === "/") {
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,
};

28
src/config.js Normal file
View File

@@ -0,0 +1,28 @@
"use strict";
function intFromEnv(name, fallback) {
const raw = process.env[name];
if (!raw) {
return fallback;
}
const parsed = Number.parseInt(raw, 10);
return Number.isInteger(parsed) ? parsed : fallback;
}
const config = {
port: intFromEnv("PORT", 3000),
xWebhookSecret: process.env.X_WEBHOOK_SECRET || "dev-x-secret",
polarWebhookSecret: process.env.POLAR_WEBHOOK_SECRET || "dev-polar-secret",
credit: {
baseCredits: intFromEnv("BASE_CREDITS", 1),
includedChars: intFromEnv("INCLUDED_CHARS", 25000),
stepChars: intFromEnv("STEP_CHARS", 10000),
stepCredits: intFromEnv("STEP_CREDITS", 1),
maxCharsPerArticle: intFromEnv("MAX_CHARS_PER_ARTICLE", 120000),
},
};
module.exports = {
config,
};