feat: add job lifecycle controls abuse policies and retention operations

This commit is contained in:
Codex
2026-02-18 14:19:21 +00:00
parent e056d38ec7
commit 141d7b42a8
6 changed files with 1055 additions and 26 deletions

View File

@@ -18,6 +18,7 @@ function createApp(options = {}) {
betterAuthSecret: "test-better-auth-secret",
betterAuthBasePath: "/api/auth",
betterAuthDevPassword: "xartaudio-dev-password",
internalApiToken: "",
convexDeploymentUrl: "",
convexAuthToken: "",
convexStateQuery: "state:getLatestSnapshot",
@@ -40,6 +41,11 @@ function createApp(options = {}) {
authPerMinute: 30,
actionPerMinute: 60,
},
abuse: {
maxJobsPerUserPerDay: 0,
cooldownSec: 0,
denyUserIds: [],
},
credit: {
baseCredits: 1,
includedChars: 25000,
@@ -57,6 +63,10 @@ function createApp(options = {}) {
...baseConfig.rateLimits,
...(overrideConfig.rateLimits || {}),
},
abuse: {
...baseConfig.abuse,
...(overrideConfig.abuse || {}),
},
credit: {
...baseConfig.credit,
...(overrideConfig.credit || {}),
@@ -218,6 +228,46 @@ test("audio flow requires auth for unlock and supports permanent unlock", async
assert.equal(walletData.balance, 4);
});
test("owner can delete audio while non owner is forbidden", async () => {
const app = createApp();
await call(app, {
method: "POST",
path: "/app/actions/topup",
headers: { cookie: "xartaudio_user=owner-delete" },
body: "amount=5",
});
const generated = await call(app, {
method: "POST",
path: "/app/actions/simulate-mention",
headers: { cookie: "xartaudio_user=owner-delete" },
body: "title=Delete+Me&body=Body",
});
const assetId = generated.headers.location.split("?")[0].replace("/audio/", "");
const forbidden = await call(app, {
method: "DELETE",
path: `/api/audio/${assetId}`,
headers: { cookie: "xartaudio_user=someone-else" },
});
assert.equal(forbidden.status, 403);
const deleted = await call(app, {
method: "DELETE",
path: `/api/audio/${assetId}`,
headers: { cookie: "xartaudio_user=owner-delete" },
});
assert.equal(deleted.status, 200);
assert.equal(JSON.parse(deleted.body).status, "deleted");
const pageAfterDelete = await call(app, {
method: "GET",
path: `/audio/${assetId}`,
headers: { cookie: "xartaudio_user=owner-delete" },
});
assert.match(pageAfterDelete.body, /Audio not found/);
});
test("audio page uses signed storage URL when storage adapter is configured", async () => {
const app = createApp({
storageAdapter: {
@@ -312,6 +362,166 @@ test("simulate mention schedules background audio generation when service is con
assert.equal(queued[0].text, "hello world");
});
test("failed background generation refunds charged credits", async () => {
const app = createApp({
audioGenerationService: {
isConfigured() {
return true;
},
async enqueueJob(payload) {
payload.onFailed(new Error("tts_outage"));
},
},
});
await call(app, {
method: "POST",
path: "/app/actions/topup",
headers: { cookie: "xartaudio_user=alice" },
body: "amount=3",
});
await call(app, {
method: "POST",
path: "/app/actions/simulate-mention",
headers: { cookie: "xartaudio_user=alice" },
body: "title=T&body=hello+world",
});
const wallet = await call(app, {
method: "GET",
path: "/api/me/wallet",
headers: { cookie: "xartaudio_user=alice" },
});
assert.equal(JSON.parse(wallet.body).balance, 3);
});
test("internal worker endpoints require token and can complete jobs", async () => {
const queued = [];
const app = createApp({
config: {
internalApiToken: "internal-token",
},
audioGenerationService: {
isConfigured() {
return true;
},
async enqueueJob(payload) {
queued.push(payload);
},
},
});
await call(app, {
method: "POST",
path: "/app/actions/topup",
headers: { cookie: "xartaudio_user=alice" },
body: "amount=2",
});
await call(app, {
method: "POST",
path: "/app/actions/simulate-mention",
headers: { cookie: "xartaudio_user=alice" },
body: "title=Queued&body=hello",
});
const job = app.engine.listJobsForUser("alice")[0];
assert.equal(job.status, "synthesizing");
const denied = await call(app, {
method: "POST",
path: `/internal/jobs/${job.id}/complete`,
body: JSON.stringify({}),
});
assert.equal(denied.status, 401);
const completed = await call(app, {
method: "POST",
path: `/internal/jobs/${job.id}/complete`,
headers: { "x-internal-token": "internal-token" },
body: JSON.stringify({
asset: {
storageKey: "audio/worker.mp3",
sizeBytes: 999,
},
}),
});
assert.equal(completed.status, 200);
const completedBody = JSON.parse(completed.body);
assert.equal(completedBody.job.status, "completed");
const readJob = await call(app, {
method: "GET",
path: `/api/jobs/${job.id}`,
headers: { cookie: "xartaudio_user=alice" },
});
assert.equal(readJob.status, 200);
assert.equal(JSON.parse(readJob.body).job.status, "completed");
});
test("internal retention endpoint prunes stale content and assets", async () => {
const app = createApp({
config: {
internalApiToken: "internal-token",
},
});
await call(app, {
method: "POST",
path: "/app/actions/topup",
headers: { cookie: "xartaudio_user=retention-owner" },
body: "amount=2",
});
await call(app, {
method: "POST",
path: "/app/actions/simulate-mention",
headers: { cookie: "xartaudio_user=retention-owner" },
body: "title=Retention&body=Body",
});
const job = app.engine.listJobsForUser("retention-owner")[0];
const asset = app.engine.getAsset(job.assetId, { includeDeleted: true });
job.createdAt = "2020-01-01T00:00:00.000Z";
asset.createdAt = "2020-01-01T00:00:00.000Z";
const denied = await call(app, {
method: "POST",
path: "/internal/retention/run",
body: JSON.stringify({ rawArticleHours: 1, audioDays: 1 }),
});
assert.equal(denied.status, 401);
const run = await call(app, {
method: "POST",
path: "/internal/retention/run",
headers: { "x-internal-token": "internal-token" },
body: JSON.stringify({ rawArticleHours: 1, audioDays: 1 }),
});
assert.equal(run.status, 200);
const summary = JSON.parse(run.body).summary;
assert.equal(summary.prunedArticleBodies >= 1, true);
assert.equal(summary.deletedAssets >= 1, true);
const page = await call(app, {
method: "GET",
path: `/audio/${job.assetId}`,
headers: { cookie: "xartaudio_user=retention-owner" },
});
assert.match(page.body, /Audio not found/);
});
test("internal endpoints are disabled when no token configured", async () => {
const app = createApp();
const response = await call(app, {
method: "POST",
path: "/internal/retention/run",
body: "{}",
});
assert.equal(response.status, 503);
assert.equal(JSON.parse(response.body).error, "internal_api_disabled");
});
test("/api/payments/create-checkout returns 503 when Polar is not configured", async () => {
const app = createApp();
@@ -391,6 +601,125 @@ test("X webhook valid flow processes article", async () => {
assert.equal(body.creditsCharged, 1);
});
test("X webhook supports CRC challenge response", async () => {
const app = createApp();
const response = await call(app, {
method: "GET",
path: "/api/webhooks/x",
query: { crc_token: "token-123" },
});
assert.equal(response.status, 200);
const body = JSON.parse(response.body);
assert.match(body.response_token, /^sha256=/);
});
test("X webhook can normalize mentionTweetId payload and reply via adapter", async () => {
const replies = [];
const app = createApp({
xAdapter: {
isConfigured() {
return true;
},
async listMentions() {
return [];
},
async fetchParentPostFromMention() {
return {
id: "parent-1",
authorId: "author-1",
article: { id: "article-1", title: "From X", body: "Article body" },
};
},
async replyToMention(payload) {
replies.push(payload);
},
},
});
await postJSONWebhook(app, "/api/webhooks/polar", { userId: "u10", credits: 5, eventId: "evt10" }, "polar-secret");
const response = await postJSONWebhook(app, "/api/webhooks/x", {
mentionTweetId: "mention-10",
callerUserId: "u10",
}, "x-secret");
assert.equal(response.status, 200);
const body = JSON.parse(response.body);
assert.equal(body.status, "completed");
assert.equal(body.replied, true);
assert.equal(replies.length, 1);
assert.equal(replies[0].mentionTweetId, "mention-10");
assert.match(replies[0].text, /audiobook/i);
});
test("X webhook replies with not article message when parent is not article", async () => {
const replies = [];
const app = createApp({
xAdapter: {
isConfigured() {
return true;
},
async listMentions() {
return [];
},
async fetchParentPostFromMention() {
return { id: "parent-2", text: "not article" };
},
async replyToMention(payload) {
replies.push(payload);
},
},
});
await postJSONWebhook(app, "/api/webhooks/polar", { userId: "u11", credits: 5, eventId: "evt11" }, "polar-secret");
const response = await postJSONWebhook(app, "/api/webhooks/x", {
mentionTweetId: "mention-11",
callerUserId: "u11",
}, "x-secret");
assert.equal(response.status, 200);
const body = JSON.parse(response.body);
assert.equal(body.status, "not_article");
assert.equal(body.replied, true);
assert.equal(replies.length, 1);
assert.match(replies[0].text, /not an X Article/);
});
test("X webhook can normalize tweet_create_events payload", async () => {
const app = createApp({
xAdapter: {
isConfigured() {
return true;
},
async listMentions() {
return [];
},
async fetchParentPostFromMention(mentionTweetId) {
assert.equal(mentionTweetId, "mention-evt-1");
return {
id: "parent-evt-1",
authorId: "author-evt-1",
article: { id: "article-evt-1", title: "Evt", body: "Body" },
};
},
async replyToMention() {},
},
});
await postJSONWebhook(app, "/api/webhooks/polar", { userId: "u-evt", credits: 4, eventId: "evt-seed" }, "polar-secret");
const response = await postJSONWebhook(app, "/api/webhooks/x", {
tweet_create_events: [
{
id_str: "mention-evt-1",
user: { id_str: "u-evt" },
},
],
}, "x-secret");
assert.equal(response.status, 200);
assert.equal(JSON.parse(response.body).status, "completed");
});
test("Polar webhook uses adapter parsing for standard webhook headers", async () => {
const app = createApp({
polarAdapter: {
@@ -524,6 +853,96 @@ test("rate limits repeated webhook calls", async () => {
assert.equal(second.status, 429);
});
test("anti abuse deny list blocks webhook generation", async () => {
const app = createApp({
config: {
abuse: {
denyUserIds: ["blocked-user"],
},
},
});
const response = await postJSONWebhook(app, "/api/webhooks/x", {
mentionPostId: "m-deny",
callerUserId: "blocked-user",
parentPost: {
id: "p-deny",
article: { id: "a-deny", title: "T", body: "hello" },
},
}, "x-secret");
assert.equal(response.status, 429);
assert.equal(JSON.parse(response.body).error, "user_denied");
});
test("anti abuse daily limit blocks second generated job", async () => {
const app = createApp({
config: {
abuse: {
maxJobsPerUserPerDay: 1,
},
},
});
await postJSONWebhook(app, "/api/webhooks/polar", { userId: "u-limit", credits: 4, eventId: "evt-limit" }, "polar-secret");
const first = await postJSONWebhook(app, "/api/webhooks/x", {
mentionPostId: "m-limit-1",
callerUserId: "u-limit",
parentPost: {
id: "p-limit-1",
article: { id: "a-limit-1", title: "T1", body: "hello" },
},
}, "x-secret");
const second = await postJSONWebhook(app, "/api/webhooks/x", {
mentionPostId: "m-limit-2",
callerUserId: "u-limit",
parentPost: {
id: "p-limit-2",
article: { id: "a-limit-2", title: "T2", body: "hello" },
},
}, "x-secret");
assert.equal(first.status, 200);
assert.equal(second.status, 429);
assert.equal(JSON.parse(second.body).error, "daily_limit_exceeded");
});
test("anti abuse cooldown reports retry delay", async () => {
const app = createApp({
config: {
abuse: {
cooldownSec: 60,
},
},
});
await postJSONWebhook(app, "/api/webhooks/polar", { userId: "u-cool", credits: 4, eventId: "evt-cool" }, "polar-secret");
const first = await postJSONWebhook(app, "/api/webhooks/x", {
mentionPostId: "m-cool-1",
callerUserId: "u-cool",
parentPost: {
id: "p-cool-1",
article: { id: "a-cool-1", title: "T1", body: "hello" },
},
}, "x-secret");
const second = await postJSONWebhook(app, "/api/webhooks/x", {
mentionPostId: "m-cool-2",
callerUserId: "u-cool",
parentPost: {
id: "p-cool-2",
article: { id: "a-cool-2", title: "T2", body: "hello" },
},
}, "x-secret");
assert.equal(first.status, 200);
assert.equal(second.status, 429);
const body = JSON.parse(second.body);
assert.equal(body.error, "cooldown_active");
assert.equal(typeof body.retryAfterSec, "number");
});
test("rate limits repeated login attempts from same IP", async () => {
const app = createApp({
config: {