harden browser routes with csrf checks and lock internal/dev endpoints

This commit is contained in:
Codex
2026-02-18 15:27:47 +00:00
parent 4814342156
commit f672677d4f
7 changed files with 200 additions and 30 deletions

View File

@@ -76,7 +76,7 @@ function createApp(options = {}) {
betterAuthBasePath: "/api/auth",
xOAuthClientId: "x-client-id",
xOAuthClientSecret: "x-client-secret",
internalApiToken: "",
internalApiToken: "internal-token",
convexDeploymentUrl: "",
convexAuthToken: "",
convexStateQuery: "state:getLatestSnapshot",
@@ -381,6 +381,7 @@ test("/api/x/mentions returns upstream mentions when configured", async () => {
const response = await call(app, {
method: "GET",
path: "/api/x/mentions",
headers: { "x-internal-token": "internal-token" },
query: { sinceId: "100" },
});
@@ -390,6 +391,61 @@ test("/api/x/mentions returns upstream mentions when configured", async () => {
assert.equal(body.mentions[0].id, "m1");
});
test("/api/x/mentions requires internal token", async () => {
const app = createApp({
xAdapter: {
isConfigured() {
return true;
},
async listMentions() {
return [];
},
},
});
const response = await call(app, {
method: "GET",
path: "/api/x/mentions",
query: { sinceId: "1" },
});
assert.equal(response.status, 401);
});
test("cross-site browser posts are blocked", async () => {
const app = createApp();
const response = await call(app, {
method: "POST",
path: "/app/actions/topup",
headers: {
cookie: "xartaudio_user=alice",
origin: "https://evil.example",
"sec-fetch-site": "cross-site",
},
body: "amount=5",
});
assert.equal(response.status, 403);
assert.match(response.body, /csrf_blocked|invalid_origin/);
});
test("dev dashboard routes can be disabled", async () => {
const app = createApp({
config: {
enableDevRoutes: false,
},
});
const response = await call(app, {
method: "POST",
path: "/app/actions/topup",
headers: { cookie: "xartaudio_user=alice" },
body: "amount=5",
});
assert.equal(response.status, 404);
});
test("simulate mention schedules background audio generation when service is configured", async () => {
const queued = [];
const app = createApp({
@@ -573,7 +629,11 @@ test("internal retention endpoint prunes stale content and assets", async () =>
});
test("internal endpoints are disabled when no token configured", async () => {
const app = createApp();
const app = createApp({
config: {
internalApiToken: "",
},
});
const response = await call(app, {
method: "POST",
path: "/internal/retention/run",

View File

@@ -59,6 +59,7 @@ test("config uses defaults when env is missing", () => {
assert.equal(config.minioUseSSL, true);
assert.equal(config.rateLimits.webhookPerMinute, 120);
assert.equal(config.allowInMemoryStateFallback, true);
assert.equal(config.enableDevRoutes, true);
assert.equal(config.abuse.maxJobsPerUserPerDay, 0);
assert.equal(config.abuse.cooldownSec, 0);
assert.deepEqual(config.abuse.denyUserIds, []);
@@ -117,6 +118,7 @@ test("config reads convex/qwen/minio overrides", () => {
assert.equal(config.abuse.cooldownSec, 120);
assert.deepEqual(config.abuse.denyUserIds, ["u1", "u2"]);
assert.equal(config.allowInMemoryStateFallback, false);
assert.equal(config.enableDevRoutes, false);
});
});

View File

@@ -45,6 +45,18 @@ test("app page renders stats and forms", () => {
assert.match(html, /Hello/);
});
test("app page can hide developer actions", () => {
const html = renderAppPage({
userId: "u1",
summary: { balance: 4, totalJobs: 2, totalCreditsSpent: 2 },
jobs: [],
showDeveloperActions: false,
});
assert.doesNotMatch(html, /Developer Actions/);
assert.doesNotMatch(html, /\/app\/actions\/topup/);
});
test("audio page shows unlock action when payment is required", () => {
const html = renderAudioPage({
audio: { id: "1", storageKey: "audio/1.mp3", articleTitle: "A", durationSec: 30 },