← back to Norma
fix(cycle5 Cody-gate): truly-empty-body probe + readJson across 93 routes
d26f6c66ae8f071add4bb41648b04cfac9075f30 · 2026-08-06 12:18:33 -0700 · Steve
Cody VERIFIED my write-smoke gave false confidence: the admin probe sent '{}' (valid
JSON), so it MISSED routes that crash on an absent body — the real prod scenario. He
reproduced no-body 500s on clients/drafts/grants/donations-insights POST.
- Suite: admin+unauth probe now sends NO body (undefined), replicating a client omitting
the body — the case that makes unguarded request.json() throw -> 500.
- readJson() codemod across ALL 93 non-excluded routes still on raw request.json()
(returns {} on empty; behavior-preserving for valid JSON). lib/body.ts default T=any so
the swap is type-neutral. Cody's 4 now return 400, not 500.
- sessions.org_id FK inconsistency (references nonprofit_accounts, not organizations) is a
data-model decision -> drafted to pending-approval (norma-sessions-org-fk-datamodel.md),
not auto-migrated. The 23503->409 guard stays (stops the crash; feature still gated).
- pulse public topics: redirect:'manual' stops the crash but the section shows nothing
because /api/petitions/topics is auth-gated — flagged as a follow-up (public topics
source needed), NOT claimed as fully fixed.
Full battery green: api-smoke 195 / write-smoke 216 (no-body probe) / regression 20 / e2e 8.
Files touched
M app/api/accounts/[id]/keywords/route.tsM app/api/admin/impersonate/route.tsM app/api/admin/permissions/route.tsM app/api/agents/org/register/route.tsM app/api/archives/route.tsM app/api/articles/link/route.tsM app/api/auth/login/route.tsM app/api/clickup/route.tsM app/api/clients/[id]/route.tsM app/api/clients/route.tsM app/api/collaborations/route.tsM app/api/contacts/[id]/route.tsM app/api/contacts/counts/route.tsM app/api/contacts/route.tsM app/api/datasource/[id]/route.tsM app/api/donations/[id]/route.tsM app/api/donations/insights/route.tsM app/api/donations/route.tsM app/api/drafts/[id]/route.tsM app/api/drafts/[id]/send-test/route.tsM app/api/drafts/[id]/versions/route.tsM app/api/email-analyzer/drafts/route.tsM app/api/email-analyzer/fetch-url/route.tsM app/api/email-analyzer/learnings/route.tsM app/api/email-analyzer/references/route.tsM app/api/email-analyzer/skills/route.tsM app/api/email-analyzer/subject-lines/route.tsM app/api/email-analyzer/universal-references/route.tsM app/api/email-sends/route.tsM app/api/geo/moveon/route.tsM app/api/grants/[id]/proposals/[pid]/route.tsM app/api/grants/[id]/proposals/route.tsM app/api/grants/[id]/route.tsM app/api/grants/route.tsM app/api/integrations/route.tsM app/api/journalists/[id]/route.tsM app/api/journalists/route.tsM app/api/journalists/scrape/route.tsM app/api/leads/route.tsM app/api/library/[id]/route.tsM app/api/library/route.tsM app/api/member-email/route.tsM app/api/news/[id]/route.tsM app/api/news/detect-mentions/route.tsM app/api/news/route.tsM app/api/nonprofit/signup/route.tsM app/api/onboard/[id]/route.tsM app/api/onboard/trigger/route.tsM app/api/orgs/signup/route.tsM app/api/outreach-templates/route.tsM app/api/partners/posts/route.tsM app/api/partners/route.tsM app/api/petitions/[id]/campaigns/route.tsM app/api/petitions/[id]/geo/route.tsM app/api/petitions/[id]/route.tsM app/api/petitions/[id]/signatures/route.tsM app/api/petitions/draft/route.tsM app/api/petitions/route.tsM app/api/pipeline/[id]/approve/route.tsM app/api/pipeline/[id]/route.tsM app/api/pipeline/[id]/schedule/route.tsM app/api/pipeline/reorder/route.tsM app/api/pipeline/route.tsM app/api/price-tracker/route.tsM app/api/pulse/petitions/[id]/sign/route.tsM app/api/pulse/petitions/route.tsM app/api/pulse/profile/route.tsM app/api/schedule/route.tsM app/api/scraper/suggestions/route.tsM app/api/sessions/[id]/route.tsM app/api/settings/api-keys/route.tsM app/api/settings/credentials/route.tsM app/api/settings/intelligence/route.tsM app/api/settings/people/route.tsM app/api/settings/pulse-config/route.tsM app/api/settings/route.tsM app/api/settings/tier-credentials/[id]/route.tsM app/api/settings/tier-credentials/route.tsM app/api/social/accounts/route.tsM app/api/social/ai-compose/route.tsM app/api/social/ai-image/route.tsM app/api/social/bulk-schedule/route.tsM app/api/social/competitors/crawl/route.tsM app/api/social/connect/route.tsM app/api/social/posts/approve/route.tsM app/api/social/publish/route.tsM app/api/social/schedule/route.tsM app/api/statements/[id]/route.tsM app/api/statements/route.tsM app/api/user-preferences/route.tsM app/api/webhooks/pulse-topic/route.tsM app/api/xsessions/posts/route.tsM app/api/xsessions/route.tsM lib/body.tsM tests/api-write-smoke.mjs
Diff
commit d26f6c66ae8f071add4bb41648b04cfac9075f30
Author: Steve <steve@designerwallcoverings.com>
Date: Thu Aug 6 12:18:33 2026 -0700
fix(cycle5 Cody-gate): truly-empty-body probe + readJson across 93 routes
Cody VERIFIED my write-smoke gave false confidence: the admin probe sent '{}' (valid
JSON), so it MISSED routes that crash on an absent body — the real prod scenario. He
reproduced no-body 500s on clients/drafts/grants/donations-insights POST.
- Suite: admin+unauth probe now sends NO body (undefined), replicating a client omitting
the body — the case that makes unguarded request.json() throw -> 500.
- readJson() codemod across ALL 93 non-excluded routes still on raw request.json()
(returns {} on empty; behavior-preserving for valid JSON). lib/body.ts default T=any so
the swap is type-neutral. Cody's 4 now return 400, not 500.
- sessions.org_id FK inconsistency (references nonprofit_accounts, not organizations) is a
data-model decision -> drafted to pending-approval (norma-sessions-org-fk-datamodel.md),
not auto-migrated. The 23503->409 guard stays (stops the crash; feature still gated).
- pulse public topics: redirect:'manual' stops the crash but the section shows nothing
because /api/petitions/topics is auth-gated — flagged as a follow-up (public topics
source needed), NOT claimed as fully fixed.
Full battery green: api-smoke 195 / write-smoke 216 (no-body probe) / regression 20 / e2e 8.
---
app/api/accounts/[id]/keywords/route.ts | 3 ++-
app/api/admin/impersonate/route.ts | 3 ++-
app/api/admin/permissions/route.ts | 3 ++-
app/api/agents/org/register/route.ts | 3 ++-
app/api/archives/route.ts | 3 ++-
app/api/articles/link/route.ts | 3 ++-
app/api/auth/login/route.ts | 3 ++-
app/api/clickup/route.ts | 3 ++-
app/api/clients/[id]/route.ts | 3 ++-
app/api/clients/route.ts | 3 ++-
app/api/collaborations/route.ts | 3 ++-
app/api/contacts/[id]/route.ts | 3 ++-
app/api/contacts/counts/route.ts | 3 ++-
app/api/contacts/route.ts | 3 ++-
app/api/datasource/[id]/route.ts | 3 ++-
app/api/donations/[id]/route.ts | 3 ++-
app/api/donations/insights/route.ts | 3 ++-
app/api/donations/route.ts | 3 ++-
app/api/drafts/[id]/route.ts | 3 ++-
app/api/drafts/[id]/send-test/route.ts | 3 ++-
app/api/drafts/[id]/versions/route.ts | 3 ++-
app/api/email-analyzer/drafts/route.ts | 3 ++-
app/api/email-analyzer/fetch-url/route.ts | 3 ++-
app/api/email-analyzer/learnings/route.ts | 5 +++--
app/api/email-analyzer/references/route.ts | 5 +++--
app/api/email-analyzer/skills/route.ts | 3 ++-
app/api/email-analyzer/subject-lines/route.ts | 5 +++--
app/api/email-analyzer/universal-references/route.ts | 5 +++--
app/api/email-sends/route.ts | 3 ++-
app/api/geo/moveon/route.ts | 3 ++-
app/api/grants/[id]/proposals/[pid]/route.ts | 3 ++-
app/api/grants/[id]/proposals/route.ts | 3 ++-
app/api/grants/[id]/route.ts | 3 ++-
app/api/grants/route.ts | 3 ++-
app/api/integrations/route.ts | 5 +++--
app/api/journalists/[id]/route.ts | 3 ++-
app/api/journalists/route.ts | 3 ++-
app/api/journalists/scrape/route.ts | 3 ++-
app/api/leads/route.ts | 5 +++--
app/api/library/[id]/route.ts | 3 ++-
app/api/library/route.ts | 3 ++-
app/api/member-email/route.ts | 3 ++-
app/api/news/[id]/route.ts | 3 ++-
app/api/news/detect-mentions/route.ts | 3 ++-
app/api/news/route.ts | 3 ++-
app/api/nonprofit/signup/route.ts | 3 ++-
app/api/onboard/[id]/route.ts | 3 ++-
app/api/onboard/trigger/route.ts | 3 ++-
app/api/orgs/signup/route.ts | 3 ++-
app/api/outreach-templates/route.ts | 3 ++-
app/api/partners/posts/route.ts | 3 ++-
app/api/partners/route.ts | 5 +++--
app/api/petitions/[id]/campaigns/route.ts | 3 ++-
app/api/petitions/[id]/geo/route.ts | 3 ++-
app/api/petitions/[id]/route.ts | 3 ++-
app/api/petitions/[id]/signatures/route.ts | 3 ++-
app/api/petitions/draft/route.ts | 3 ++-
app/api/petitions/route.ts | 3 ++-
app/api/pipeline/[id]/approve/route.ts | 3 ++-
app/api/pipeline/[id]/route.ts | 3 ++-
app/api/pipeline/[id]/schedule/route.ts | 3 ++-
app/api/pipeline/reorder/route.ts | 3 ++-
app/api/pipeline/route.ts | 3 ++-
app/api/price-tracker/route.ts | 3 ++-
app/api/pulse/petitions/[id]/sign/route.ts | 3 ++-
app/api/pulse/petitions/route.ts | 3 ++-
app/api/pulse/profile/route.ts | 3 ++-
app/api/schedule/route.ts | 3 ++-
app/api/scraper/suggestions/route.ts | 3 ++-
app/api/sessions/[id]/route.ts | 3 ++-
app/api/settings/api-keys/route.ts | 3 ++-
app/api/settings/credentials/route.ts | 3 ++-
app/api/settings/intelligence/route.ts | 3 ++-
app/api/settings/people/route.ts | 3 ++-
app/api/settings/pulse-config/route.ts | 3 ++-
app/api/settings/route.ts | 3 ++-
app/api/settings/tier-credentials/[id]/route.ts | 3 ++-
app/api/settings/tier-credentials/route.ts | 3 ++-
app/api/social/accounts/route.ts | 3 ++-
app/api/social/ai-compose/route.ts | 3 ++-
app/api/social/ai-image/route.ts | 3 ++-
app/api/social/bulk-schedule/route.ts | 3 ++-
app/api/social/competitors/crawl/route.ts | 3 ++-
app/api/social/connect/route.ts | 5 +++--
app/api/social/posts/approve/route.ts | 3 ++-
app/api/social/publish/route.ts | 3 ++-
app/api/social/schedule/route.ts | 3 ++-
app/api/statements/[id]/route.ts | 3 ++-
app/api/statements/route.ts | 3 ++-
app/api/user-preferences/route.ts | 3 ++-
app/api/webhooks/pulse-topic/route.ts | 3 ++-
app/api/xsessions/posts/route.ts | 3 ++-
app/api/xsessions/route.ts | 3 ++-
lib/body.ts | 6 +++++-
tests/api-write-smoke.mjs | 4 +++-
95 files changed, 202 insertions(+), 103 deletions(-)
diff --git a/app/api/accounts/[id]/keywords/route.ts b/app/api/accounts/[id]/keywords/route.ts
index f27d589..9f9569a 100644
--- a/app/api/accounts/[id]/keywords/route.ts
+++ b/app/api/accounts/[id]/keywords/route.ts
@@ -1,4 +1,5 @@
import { NextRequest, NextResponse } from 'next/server';
+import { readJson } from '@/lib/body';
import { query } from '@/lib/db';
import { requireRole } from '@/lib/require-role';
@@ -15,7 +16,7 @@ export async function PATCH(
try {
const { id } = await params;
- const data = await request.json();
+ const data = await readJson(request);
const keywords: string[] = Array.isArray(data.keywords) ? data.keywords : [];
// Sanitize keywords
diff --git a/app/api/admin/impersonate/route.ts b/app/api/admin/impersonate/route.ts
index e90aaaa..9d6360d 100644
--- a/app/api/admin/impersonate/route.ts
+++ b/app/api/admin/impersonate/route.ts
@@ -17,6 +17,7 @@
*/
import { NextRequest, NextResponse } from 'next/server';
+import { readJson } from '@/lib/body';
import {
createSession, buildAuthCookie, AUTH_COOKIE_NAME, verifyAuth, verifySessionToken,
} from '@/lib/auth';
@@ -65,7 +66,7 @@ export async function POST(request: NextRequest) {
if (adminSession instanceof NextResponse) return adminSession;
try {
- const body = await request.json();
+ const body = await readJson(request);
const targetUsername = String(body.username || '').trim();
if (!targetUsername) {
return NextResponse.json({ error: 'username is required' }, { status: 400 });
diff --git a/app/api/admin/permissions/route.ts b/app/api/admin/permissions/route.ts
index 2340e25..77c7053 100644
--- a/app/api/admin/permissions/route.ts
+++ b/app/api/admin/permissions/route.ts
@@ -7,6 +7,7 @@
*/
import { NextRequest, NextResponse } from 'next/server';
+import { readJson } from '@/lib/body';
import { query } from '@/lib/db';
import { requireRole } from '@/lib/require-role';
import { ALL_ROLES, type UserRole } from '@/lib/auth';
@@ -40,7 +41,7 @@ export async function PUT(request: NextRequest) {
if (session instanceof NextResponse) return session;
try {
- const body = (await request.json()) as UpdateBody;
+ const body = (await readJson(request)) as UpdateBody;
const updates = Array.isArray(body?.updates) ? body.updates : [];
if (updates.length === 0) {
diff --git a/app/api/agents/org/register/route.ts b/app/api/agents/org/register/route.ts
index 2783cbf..bf1e878 100644
--- a/app/api/agents/org/register/route.ts
+++ b/app/api/agents/org/register/route.ts
@@ -1,4 +1,5 @@
import { NextRequest, NextResponse } from 'next/server';
+import { readJson } from '@/lib/body';
import { query } from '@/lib/db';
import { requireRole } from '@/lib/require-role';
@@ -13,7 +14,7 @@ export async function POST(request: NextRequest) {
if (auth instanceof NextResponse) return auth;
try {
- const body = await request.json();
+ const body = await readJson(request);
const {
name,
ein,
diff --git a/app/api/archives/route.ts b/app/api/archives/route.ts
index 4d35920..5396561 100644
--- a/app/api/archives/route.ts
+++ b/app/api/archives/route.ts
@@ -1,4 +1,5 @@
import { NextRequest, NextResponse } from 'next/server';
+import { readJson } from '@/lib/body';
import { query } from '@/lib/db';
import { requireRole } from '@/lib/require-role';
@@ -93,7 +94,7 @@ export async function POST(request: NextRequest) {
if (auth instanceof NextResponse) return auth;
try {
- const body = await request.json();
+ const body = await readJson(request);
if (!body.archive_type || !body.title) {
return NextResponse.json({ error: 'archive_type and title are required' }, { status: 400 });
diff --git a/app/api/articles/link/route.ts b/app/api/articles/link/route.ts
index 98e7d77..97ce19a 100644
--- a/app/api/articles/link/route.ts
+++ b/app/api/articles/link/route.ts
@@ -1,4 +1,5 @@
import { NextRequest, NextResponse } from 'next/server';
+import { readJson } from '@/lib/body';
import { query } from '@/lib/db';
import { requireRole } from '@/lib/require-role';
import { getOrgId } from '@/lib/orgId';
@@ -97,7 +98,7 @@ export async function POST(req: NextRequest) {
const orgId = auth.role === 'admin' ? getOrgId(req) : auth.orgId;
try {
- const body = await req.json();
+ const body = await readJson(req);
const { journalist_id, article, entity_ids = [] } = body as {
journalist_id: string;
article: {
diff --git a/app/api/auth/login/route.ts b/app/api/auth/login/route.ts
index 82e870f..7307cc8 100644
--- a/app/api/auth/login/route.ts
+++ b/app/api/auth/login/route.ts
@@ -1,4 +1,5 @@
import { NextRequest, NextResponse } from 'next/server';
+import { readJson } from '@/lib/body';
import { createSession, buildAuthCookie, verifyPassword, hashPassword } from '@/lib/auth';
import {
checkLoginAttempt,
@@ -26,7 +27,7 @@ export async function POST(request: NextRequest) {
try {
let body: unknown;
try {
- body = await request.json();
+ body = await readJson(request);
} catch {
return NextResponse.json(
{ error: 'Invalid request body — expected JSON' },
diff --git a/app/api/clickup/route.ts b/app/api/clickup/route.ts
index a7f081c..9d613d9 100644
--- a/app/api/clickup/route.ts
+++ b/app/api/clickup/route.ts
@@ -1,4 +1,5 @@
import { NextRequest, NextResponse } from 'next/server';
+import { readJson } from '@/lib/body';
import { query } from '@/lib/db';
import { requireRole } from '@/lib/require-role';
@@ -86,7 +87,7 @@ export async function POST(request: NextRequest) {
return NextResponse.json({ error: 'ClickUp integration not configured' }, { status: 503 });
}
- const body = await request.json();
+ const body = await readJson(request);
const { list_id, name, description, priority, due_date, tags, assignees, source_type, source_id } = body;
if (!list_id || !name) {
diff --git a/app/api/clients/[id]/route.ts b/app/api/clients/[id]/route.ts
index 8ee752a..c26b638 100644
--- a/app/api/clients/[id]/route.ts
+++ b/app/api/clients/[id]/route.ts
@@ -1,4 +1,5 @@
import { NextRequest, NextResponse } from 'next/server';
+import { readJson } from '@/lib/body';
import { query } from '@/lib/db';
import { requireRole } from '@/lib/require-role';
import { getOrgId } from '@/lib/orgId';
@@ -51,7 +52,7 @@ export async function PATCH(
try {
const { id } = await params;
- const body = await request.json();
+ const body = await readJson(request);
const allowedFields = [
'first_name', 'last_name', 'email', 'phone', 'city', 'state', 'zip',
diff --git a/app/api/clients/route.ts b/app/api/clients/route.ts
index 99839e9..c032b41 100644
--- a/app/api/clients/route.ts
+++ b/app/api/clients/route.ts
@@ -1,4 +1,5 @@
import { NextRequest, NextResponse } from 'next/server';
+import { readJson } from '@/lib/body';
import { query } from '@/lib/db';
import { requireRole } from '@/lib/require-role';
import { getOrgId } from '@/lib/orgId';
@@ -117,7 +118,7 @@ export async function POST(request: NextRequest) {
if (auth instanceof NextResponse) return auth;
try {
- const body = await request.json();
+ const body = await readJson(request);
const {
first_name, last_name, email, phone, city, state, zip,
client_type, status, priority,
diff --git a/app/api/collaborations/route.ts b/app/api/collaborations/route.ts
index 712071d..6df39ce 100644
--- a/app/api/collaborations/route.ts
+++ b/app/api/collaborations/route.ts
@@ -1,4 +1,5 @@
import { NextRequest, NextResponse } from 'next/server';
+import { readJson } from '@/lib/body';
import { query } from '@/lib/db';
import { requireRole } from '@/lib/require-role';
import { getOrgId } from '@/lib/orgId';
@@ -71,7 +72,7 @@ export async function POST(request: NextRequest) {
if (auth instanceof NextResponse) return auth;
try {
- const body = await request.json();
+ const body = await readJson(request);
const {
collab_type, name, title, organization, website_url,
email, phone, district, state, city, party,
diff --git a/app/api/contacts/[id]/route.ts b/app/api/contacts/[id]/route.ts
index 89906a1..a3b90ae 100644
--- a/app/api/contacts/[id]/route.ts
+++ b/app/api/contacts/[id]/route.ts
@@ -1,4 +1,5 @@
import { NextRequest, NextResponse } from 'next/server';
+import { readJson } from '@/lib/body';
import { query } from '@/lib/db';
import { requireRole } from '@/lib/require-role';
import { getOrgId } from '@/lib/orgId';
@@ -114,7 +115,7 @@ export async function PUT(
try {
const { id } = await params;
- const data = await request.json();
+ const data = await readJson(request);
const allowedFields = [
'first_name', 'last_name', 'email', 'company', 'position',
diff --git a/app/api/contacts/counts/route.ts b/app/api/contacts/counts/route.ts
index b9af879..16a664a 100644
--- a/app/api/contacts/counts/route.ts
+++ b/app/api/contacts/counts/route.ts
@@ -1,4 +1,5 @@
import { NextRequest, NextResponse } from 'next/server';
+import { readJson } from '@/lib/body';
import { query } from '@/lib/db';
import { requireRole } from '@/lib/require-role';
import { getOrgId } from '@/lib/orgId';
@@ -24,7 +25,7 @@ export async function POST(request: NextRequest) {
const orgId = auth.role === 'admin' ? getOrgId(request) : auth.orgId;
try {
- const body = await request.json();
+ const body = await readJson(request);
const { contact_id, skills = [], schools = [], groups = [], employers = [] } = body;
const results: Record<string, number> = {};
diff --git a/app/api/contacts/route.ts b/app/api/contacts/route.ts
index a86e55a..09956f5 100644
--- a/app/api/contacts/route.ts
+++ b/app/api/contacts/route.ts
@@ -1,4 +1,5 @@
import { NextRequest, NextResponse } from 'next/server';
+import { readJson } from '@/lib/body';
import { query } from '@/lib/db';
import { requireRole } from '@/lib/require-role';
import { getOrgId } from '@/lib/orgId';
@@ -105,7 +106,7 @@ export async function POST(request: NextRequest) {
if (auth instanceof NextResponse) return auth;
try {
- const data = await request.json();
+ const data = await readJson(request);
if (!data.first_name || !data.last_name) {
return NextResponse.json({ error: 'first_name and last_name are required' }, { status: 400 });
}
diff --git a/app/api/datasource/[id]/route.ts b/app/api/datasource/[id]/route.ts
index ddc65e1..6102372 100644
--- a/app/api/datasource/[id]/route.ts
+++ b/app/api/datasource/[id]/route.ts
@@ -1,4 +1,5 @@
import { NextRequest, NextResponse } from 'next/server';
+import { readJson } from '@/lib/body';
import { query } from '@/lib/db';
import { requireRole } from '@/lib/require-role';
@@ -66,7 +67,7 @@ export async function PUT(
}
try {
- const body = (await request.json()) as { status?: string; notes?: string };
+ const body = (await readJson(request)) as { status?: string; notes?: string };
const { status, notes } = body;
if (!status) {
diff --git a/app/api/donations/[id]/route.ts b/app/api/donations/[id]/route.ts
index 9511dd5..0191e1d 100644
--- a/app/api/donations/[id]/route.ts
+++ b/app/api/donations/[id]/route.ts
@@ -1,4 +1,5 @@
import { NextRequest, NextResponse } from 'next/server';
+import { readJson } from '@/lib/body';
import { query } from '@/lib/db';
import { requireRole } from '@/lib/require-role';
import { auditLog } from '@/lib/audit';
@@ -44,7 +45,7 @@ export async function PATCH(request: NextRequest, context: RouteContext) {
const orgId = auth.role === 'admin' ? getOrgId(request) : auth.orgId;
try {
- const body = await request.json();
+ const body = await readJson(request);
const allowedFields = [
'source', 'amount', 'donor_type', 'campaign_name', 'petition_id',
'draft_id', 'channel', 'recurring', 'notes', 'tags', 'donated_at',
diff --git a/app/api/donations/insights/route.ts b/app/api/donations/insights/route.ts
index 27fd651..21ad3ad 100644
--- a/app/api/donations/insights/route.ts
+++ b/app/api/donations/insights/route.ts
@@ -1,4 +1,5 @@
import { NextRequest, NextResponse } from 'next/server';
+import { readJson } from '@/lib/body';
import { query } from '@/lib/db';
import { requireRole } from '@/lib/require-role';
import { auditLog } from '@/lib/audit';
@@ -70,7 +71,7 @@ export async function POST(request: NextRequest) {
if (auth instanceof NextResponse) return auth;
try {
- const body = await request.json();
+ const body = await readJson(request);
if (!body.insight_type || typeof body.insight_type !== 'string') {
return NextResponse.json({ error: 'insight_type is required' }, { status: 400 });
diff --git a/app/api/donations/route.ts b/app/api/donations/route.ts
index 1342bcb..36a4c65 100644
--- a/app/api/donations/route.ts
+++ b/app/api/donations/route.ts
@@ -1,4 +1,5 @@
import { NextRequest, NextResponse } from 'next/server';
+import { readJson } from '@/lib/body';
import { query } from '@/lib/db';
import { requireRole } from '@/lib/require-role';
import { auditLog } from '@/lib/audit';
@@ -134,7 +135,7 @@ export async function POST(request: NextRequest) {
if (auth instanceof NextResponse) return auth;
try {
- const body = await request.json();
+ const body = await readJson(request);
if (body.amount == null || isNaN(Number(body.amount)) || Number(body.amount) <= 0) {
return NextResponse.json({ error: 'amount is required and must be positive' }, { status: 400 });
diff --git a/app/api/drafts/[id]/route.ts b/app/api/drafts/[id]/route.ts
index 66cac78..4a5b97d 100644
--- a/app/api/drafts/[id]/route.ts
+++ b/app/api/drafts/[id]/route.ts
@@ -1,4 +1,5 @@
import { NextRequest, NextResponse } from 'next/server';
+import { readJson } from '@/lib/body';
import { query, getClient } from '@/lib/db';
import { requireRole } from '@/lib/require-role';
import { auditLog } from '@/lib/audit';
@@ -62,7 +63,7 @@ export async function PATCH(request: NextRequest, context: RouteContext) {
const client = await getClient();
try {
- const body = await request.json();
+ const body = await readJson(request);
const allowedFields = [
'subject', 'body_html', 'body_text', 'status', 'voice_mode',
'email_type', 'from_name', 'from_email', 'reply_to',
diff --git a/app/api/drafts/[id]/send-test/route.ts b/app/api/drafts/[id]/send-test/route.ts
index 083162e..75d45d1 100644
--- a/app/api/drafts/[id]/send-test/route.ts
+++ b/app/api/drafts/[id]/send-test/route.ts
@@ -1,4 +1,5 @@
import { NextRequest, NextResponse } from 'next/server';
+import { readJson } from '@/lib/body';
import { query } from '@/lib/db';
import { requireRole } from '@/lib/require-role';
import { auditLog } from '@/lib/audit';
@@ -40,7 +41,7 @@ export async function POST(request: NextRequest, context: RouteContext) {
}
try {
- const body = await request.json().catch(() => ({}));
+ const body = await readJson(request).catch(() => ({}));
// Determine recipient
const recipientEmail = body.to || process.env.TEST_SEND_TO;
diff --git a/app/api/drafts/[id]/versions/route.ts b/app/api/drafts/[id]/versions/route.ts
index 8861516..4b8b49e 100644
--- a/app/api/drafts/[id]/versions/route.ts
+++ b/app/api/drafts/[id]/versions/route.ts
@@ -1,4 +1,5 @@
import { NextRequest, NextResponse } from 'next/server';
+import { readJson } from '@/lib/body';
import { query } from '@/lib/db';
import { requireRole } from '@/lib/require-role';
import { auditLog } from '@/lib/audit';
@@ -59,7 +60,7 @@ export async function POST(request: NextRequest, context: RouteContext) {
const { id } = await context.params;
try {
- const body = await request.json().catch(() => ({}));
+ const body = await readJson(request).catch(() => ({}));
const changeSummary = body.change_summary || 'Manual snapshot';
// Fetch the current draft
diff --git a/app/api/email-analyzer/drafts/route.ts b/app/api/email-analyzer/drafts/route.ts
index 8f49989..daeb036 100644
--- a/app/api/email-analyzer/drafts/route.ts
+++ b/app/api/email-analyzer/drafts/route.ts
@@ -5,6 +5,7 @@
*/
import { NextRequest, NextResponse } from 'next/server';
+import { readJson } from '@/lib/body';
import { query } from '@/lib/db';
import { requireRole } from '@/lib/require-role';
import { getOrgId } from '@/lib/orgId';
@@ -85,7 +86,7 @@ export async function POST(request: NextRequest) {
if (auth instanceof NextResponse) return auth;
try {
- const body = await request.json();
+ const body = await readJson(request);
const {
originalEmail,
currentVersion,
diff --git a/app/api/email-analyzer/fetch-url/route.ts b/app/api/email-analyzer/fetch-url/route.ts
index f30ceb6..9e72415 100644
--- a/app/api/email-analyzer/fetch-url/route.ts
+++ b/app/api/email-analyzer/fetch-url/route.ts
@@ -4,6 +4,7 @@
*/
import { NextRequest, NextResponse } from 'next/server';
+import { readJson } from '@/lib/body';
import { requireRole } from '@/lib/require-role';
import { checkRateLimit } from '@/lib/rate-limit';
import { assertPublicUrl } from '@/lib/ssrf-guard';
@@ -193,7 +194,7 @@ export async function POST(request: NextRequest) {
}
try {
- const body = await request.json();
+ const body = await readJson(request);
const { url } = body;
if (!url || typeof url !== 'string') {
diff --git a/app/api/email-analyzer/learnings/route.ts b/app/api/email-analyzer/learnings/route.ts
index fc080c0..67e1e22 100644
--- a/app/api/email-analyzer/learnings/route.ts
+++ b/app/api/email-analyzer/learnings/route.ts
@@ -13,6 +13,7 @@
*/
import { NextRequest, NextResponse } from 'next/server';
+import { readJson } from '@/lib/body';
import { query } from '@/lib/db';
import { requireRole } from '@/lib/require-role';
@@ -57,7 +58,7 @@ export async function POST(request: NextRequest) {
if (auth instanceof NextResponse) return auth;
try {
- const body = await request.json();
+ const body = await readJson(request);
const {
session_id,
version_id,
@@ -97,7 +98,7 @@ export async function PATCH(request: NextRequest) {
if (auth instanceof NextResponse) return auth;
try {
- const { id, learning, tags } = await request.json();
+ const { id, learning, tags } = await readJson(request);
if (!id) return NextResponse.json({ error: 'id required' }, { status: 400 });
const sets: string[] = [];
const params: unknown[] = [];
diff --git a/app/api/email-analyzer/references/route.ts b/app/api/email-analyzer/references/route.ts
index b339459..04440d2 100644
--- a/app/api/email-analyzer/references/route.ts
+++ b/app/api/email-analyzer/references/route.ts
@@ -5,6 +5,7 @@
*/
import { NextRequest, NextResponse } from 'next/server';
+import { readJson } from '@/lib/body';
import { query } from '@/lib/db';
import { requireRole } from '@/lib/require-role';
@@ -39,7 +40,7 @@ export async function POST(request: NextRequest) {
if (auth instanceof NextResponse) return auth;
try {
- const body = await request.json();
+ const body = await readJson(request);
const {
session_id,
ref_type,
@@ -137,7 +138,7 @@ export async function PATCH(request: NextRequest) {
if (auth instanceof NextResponse) return auth;
try {
- const body = await request.json();
+ const body = await readJson(request);
const { id, include_in_rewrite, sort_order } = body;
if (!id) {
diff --git a/app/api/email-analyzer/skills/route.ts b/app/api/email-analyzer/skills/route.ts
index 9349799..c0a734f 100644
--- a/app/api/email-analyzer/skills/route.ts
+++ b/app/api/email-analyzer/skills/route.ts
@@ -8,6 +8,7 @@
*/
import { NextRequest, NextResponse } from 'next/server';
+import { readJson } from '@/lib/body';
import { query } from '@/lib/db';
import { requireRole } from '@/lib/require-role';
@@ -49,7 +50,7 @@ export async function POST(request: NextRequest) {
if (auth instanceof NextResponse) return auth;
try {
- const body = await request.json();
+ const body = await readJson(request);
const {
name,
description = '',
diff --git a/app/api/email-analyzer/subject-lines/route.ts b/app/api/email-analyzer/subject-lines/route.ts
index 8728153..68a7871 100644
--- a/app/api/email-analyzer/subject-lines/route.ts
+++ b/app/api/email-analyzer/subject-lines/route.ts
@@ -9,6 +9,7 @@
*/
import { NextRequest, NextResponse } from 'next/server';
+import { readJson } from '@/lib/body';
import { query } from '@/lib/db';
import { requireRole } from '@/lib/require-role';
@@ -41,7 +42,7 @@ export async function POST(request: NextRequest) {
if (auth instanceof NextResponse) return auth;
try {
- const body = await request.json();
+ const body = await readJson(request);
const { session_id, version_id, subject, style, reasoning, notes } = body;
if (!session_id || !subject) {
return NextResponse.json({ error: 'session_id and subject are required' }, { status: 400 });
@@ -65,7 +66,7 @@ export async function PATCH(request: NextRequest) {
if (auth instanceof NextResponse) return auth;
try {
- const { id, is_favorite, notes } = await request.json();
+ const { id, is_favorite, notes } = await readJson(request);
if (!id) return NextResponse.json({ error: 'id required' }, { status: 400 });
const sets: string[] = [];
diff --git a/app/api/email-analyzer/universal-references/route.ts b/app/api/email-analyzer/universal-references/route.ts
index 58ae7ff..ad20afd 100644
--- a/app/api/email-analyzer/universal-references/route.ts
+++ b/app/api/email-analyzer/universal-references/route.ts
@@ -12,6 +12,7 @@
*/
import { NextRequest, NextResponse } from 'next/server';
+import { readJson } from '@/lib/body';
import { query } from '@/lib/db';
import { requireRole } from '@/lib/require-role';
@@ -62,7 +63,7 @@ export async function POST(request: NextRequest) {
if (auth instanceof NextResponse) return auth;
try {
- const body = await request.json();
+ const body = await readJson(request);
const {
title,
content,
@@ -114,7 +115,7 @@ export async function PATCH(request: NextRequest) {
if (auth instanceof NextResponse) return auth;
try {
- const body = await request.json();
+ const body = await readJson(request);
const { id, is_active, title, content, tags, tone } = body as {
id?: string;
is_active?: boolean;
diff --git a/app/api/email-sends/route.ts b/app/api/email-sends/route.ts
index f1afeee..48f5657 100644
--- a/app/api/email-sends/route.ts
+++ b/app/api/email-sends/route.ts
@@ -4,6 +4,7 @@
*/
import { NextRequest, NextResponse } from 'next/server';
+import { readJson } from '@/lib/body';
import { requireRole } from '@/lib/require-role';
import { getOrgId } from '@/lib/orgId';
import { query } from '@/lib/db';
@@ -93,7 +94,7 @@ export async function POST(request: NextRequest) {
const orgId = auth.role === 'admin' ? getOrgId(request) : auth.orgId;
try {
- const body = await request.json();
+ const body = await readJson(request);
const {
email_type = 'general',
subject,
diff --git a/app/api/geo/moveon/route.ts b/app/api/geo/moveon/route.ts
index 20ee3e0..0a39a08 100644
--- a/app/api/geo/moveon/route.ts
+++ b/app/api/geo/moveon/route.ts
@@ -15,6 +15,7 @@
*/
import { NextRequest, NextResponse } from 'next/server';
+import { readJson } from '@/lib/body';
import { query } from '@/lib/db';
import { requireRole } from '@/lib/require-role';
@@ -28,7 +29,7 @@ export async function POST(request: NextRequest) {
let body: { email_text?: string; subject?: string };
try {
- body = await request.json();
+ body = await readJson(request);
} catch {
return NextResponse.json(
{ error: 'Request body must be valid JSON with an email_text field' },
diff --git a/app/api/grants/[id]/proposals/[pid]/route.ts b/app/api/grants/[id]/proposals/[pid]/route.ts
index 8010d14..9901358 100644
--- a/app/api/grants/[id]/proposals/[pid]/route.ts
+++ b/app/api/grants/[id]/proposals/[pid]/route.ts
@@ -1,4 +1,5 @@
import { NextRequest, NextResponse } from 'next/server';
+import { readJson } from '@/lib/body';
import { query } from '@/lib/db';
import { requireRole } from '@/lib/require-role';
import { auditLog } from '@/lib/audit';
@@ -37,7 +38,7 @@ export async function PATCH(request: NextRequest, { params }: { params: Params }
const { pid } = await params;
try {
- const body = await request.json();
+ const body = await readJson(request);
const sets: string[] = [];
const values: unknown[] = [];
let idx = 1;
diff --git a/app/api/grants/[id]/proposals/route.ts b/app/api/grants/[id]/proposals/route.ts
index 51faf95..4357b87 100644
--- a/app/api/grants/[id]/proposals/route.ts
+++ b/app/api/grants/[id]/proposals/route.ts
@@ -1,4 +1,5 @@
import { NextRequest, NextResponse } from 'next/server';
+import { readJson } from '@/lib/body';
import { query } from '@/lib/db';
import { requireRole } from '@/lib/require-role';
import { auditLog } from '@/lib/audit';
@@ -67,7 +68,7 @@ export async function POST(
}
}
- const body = await request.json();
+ const body = await readJson(request);
const result = await query(
`INSERT INTO grant_proposals (grant_id, subject, recipient_email, recipient_name, body_html, body_text, created_by)
diff --git a/app/api/grants/[id]/route.ts b/app/api/grants/[id]/route.ts
index 57d95dd..08d1f90 100644
--- a/app/api/grants/[id]/route.ts
+++ b/app/api/grants/[id]/route.ts
@@ -1,4 +1,5 @@
import { NextRequest, NextResponse } from 'next/server';
+import { readJson } from '@/lib/body';
import { query } from '@/lib/db';
import { requireRole } from '@/lib/require-role';
import { auditLog } from '@/lib/audit';
@@ -47,7 +48,7 @@ export async function PATCH(request: NextRequest, context: RouteContext) {
const { id } = await context.params;
try {
- const body = await request.json();
+ const body = await readJson(request);
const allowedFields = [
'title', 'funder', 'funder_url', 'amount_min', 'amount_max',
'description', 'eligibility', 'focus_areas', 'application_url',
diff --git a/app/api/grants/route.ts b/app/api/grants/route.ts
index 94f9102..c614b69 100644
--- a/app/api/grants/route.ts
+++ b/app/api/grants/route.ts
@@ -1,4 +1,5 @@
import { NextRequest, NextResponse } from 'next/server';
+import { readJson } from '@/lib/body';
import { query } from '@/lib/db';
import { requireRole } from '@/lib/require-role';
import { auditLog } from '@/lib/audit';
@@ -124,7 +125,7 @@ export async function POST(request: NextRequest) {
if (auth instanceof NextResponse) return auth;
try {
- const body = await request.json();
+ const body = await readJson(request);
if (!body.title || typeof body.title !== 'string' || body.title.trim() === '') {
return NextResponse.json({ error: 'title is required' }, { status: 400 });
diff --git a/app/api/integrations/route.ts b/app/api/integrations/route.ts
index 694d689..1170dd9 100644
--- a/app/api/integrations/route.ts
+++ b/app/api/integrations/route.ts
@@ -1,4 +1,5 @@
import { NextRequest, NextResponse } from 'next/server';
+import { readJson } from '@/lib/body';
import { query } from '@/lib/db';
import { requireRole } from '@/lib/require-role';
@@ -36,7 +37,7 @@ export async function PATCH(request: NextRequest) {
const auth = requireRole(request, 'admin');
if (auth instanceof NextResponse) return auth;
- const body = await request.json();
+ const body = await readJson(request);
const { service_id, status, config, metadata } = body;
if (!service_id) return NextResponse.json({ error: 'service_id required' }, { status: 400 });
@@ -83,7 +84,7 @@ export async function POST(request: NextRequest) {
const auth = requireRole(request, 'admin');
if (auth instanceof NextResponse) return auth;
- const { service_id, action } = await request.json();
+ const { service_id, action } = await readJson(request);
if (!service_id || action !== 'test') {
return NextResponse.json({ error: 'service_id and action="test" required' }, { status: 400 });
}
diff --git a/app/api/journalists/[id]/route.ts b/app/api/journalists/[id]/route.ts
index 12626bb..98db7e1 100644
--- a/app/api/journalists/[id]/route.ts
+++ b/app/api/journalists/[id]/route.ts
@@ -1,4 +1,5 @@
import { NextRequest, NextResponse } from 'next/server';
+import { readJson } from '@/lib/body';
import { query } from '@/lib/db';
import { requireRole } from '@/lib/require-role';
@@ -86,7 +87,7 @@ export async function PATCH(req: NextRequest, { params }: { params: Promise<{ id
try {
const { id } = await params;
- const body = await req.json();
+ const body = await readJson(req);
const allowed = ['name', 'outlet', 'beat', 'bio', 'twitter_handle', 'email', 'tags', 'is_active', 'linkedin_url', 'website', 'instagram_handle', 'facebook_url', 'youtube_url', 'substack_url', 'medium_url', 'podcast_url', 'rss_feed'];
const sets: string[] = [];
const vals: any[] = [];
diff --git a/app/api/journalists/route.ts b/app/api/journalists/route.ts
index f0d71d0..80876d3 100644
--- a/app/api/journalists/route.ts
+++ b/app/api/journalists/route.ts
@@ -1,4 +1,5 @@
import { NextRequest, NextResponse } from 'next/server';
+import { readJson } from '@/lib/body';
import { query } from '@/lib/db';
import { requireRole } from '@/lib/require-role';
@@ -149,7 +150,7 @@ export async function POST(req: NextRequest) {
if (auth instanceof NextResponse) return auth;
try {
- const body = await req.json();
+ const body = await readJson(req);
const {
name, outlet, beat, bio, twitter_handle, email, tags, linkedin_url,
website, instagram_handle, facebook_url, youtube_url, substack_url,
diff --git a/app/api/journalists/scrape/route.ts b/app/api/journalists/scrape/route.ts
index c4c4baa..9522875 100644
--- a/app/api/journalists/scrape/route.ts
+++ b/app/api/journalists/scrape/route.ts
@@ -1,4 +1,5 @@
import { NextRequest, NextResponse } from 'next/server';
+import { readJson } from '@/lib/body';
import { query } from '@/lib/db';
import { requireRole } from '@/lib/require-role';
import { createJournalistLinks } from '@/lib/journalist-links';
@@ -168,7 +169,7 @@ export async function POST(request: NextRequest) {
if (auth instanceof NextResponse) return auth;
try {
- const { name, outlet } = await request.json();
+ const { name, outlet } = await readJson(request);
if (!name || typeof name !== 'string' || name.trim().length < 2) {
return NextResponse.json({ error: 'Reporter name required (min 2 chars)' }, { status: 400 });
}
diff --git a/app/api/leads/route.ts b/app/api/leads/route.ts
index bd96ffc..6ced545 100644
--- a/app/api/leads/route.ts
+++ b/app/api/leads/route.ts
@@ -1,4 +1,5 @@
import { NextRequest, NextResponse } from 'next/server';
+import { readJson } from '@/lib/body';
import { query } from '@/lib/db';
import { requireRole } from '@/lib/require-role';
import { getOrgId } from '@/lib/orgId';
@@ -76,7 +77,7 @@ export async function POST(request: NextRequest) {
if (auth instanceof NextResponse) return auth;
try {
- const body = await request.json();
+ const body = await readJson(request);
const {
lead_type, name, contact_name, contact_title, organization,
email, phone, website_url, city, state, original_interest,
@@ -123,7 +124,7 @@ export async function PATCH(request: NextRequest) {
if (auth instanceof NextResponse) return auth;
try {
- const body = await request.json();
+ const body = await readJson(request);
const { id, ...updates } = body;
if (!id) return NextResponse.json({ error: 'id required' }, { status: 400 });
diff --git a/app/api/library/[id]/route.ts b/app/api/library/[id]/route.ts
index 4220b79..329536c 100644
--- a/app/api/library/[id]/route.ts
+++ b/app/api/library/[id]/route.ts
@@ -1,4 +1,5 @@
import { NextRequest, NextResponse } from 'next/server';
+import { readJson } from '@/lib/body';
import { query } from '@/lib/db';
import { requireRole } from '@/lib/require-role';
import { auditLog } from '@/lib/audit';
@@ -45,7 +46,7 @@ export async function PATCH(request: NextRequest, context: RouteContext) {
const { id } = await context.params;
try {
- const body = await request.json();
+ const body = await readJson(request);
const allowedFields = [
'title', 'description', 'content_html', 'content_text',
'tags', 'email_type', 'voice_mode', 'is_pinned', 'item_type',
diff --git a/app/api/library/route.ts b/app/api/library/route.ts
index 8fac874..b48af43 100644
--- a/app/api/library/route.ts
+++ b/app/api/library/route.ts
@@ -1,4 +1,5 @@
import { NextRequest, NextResponse } from 'next/server';
+import { readJson } from '@/lib/body';
import { query } from '@/lib/db';
import { requireRole } from '@/lib/require-role';
import { auditLog } from '@/lib/audit';
@@ -90,7 +91,7 @@ export async function POST(request: NextRequest) {
if (auth instanceof NextResponse) return auth;
try {
- const body = await request.json();
+ const body = await readJson(request);
if (!body.item_type || !body.title) {
return NextResponse.json({ error: 'item_type and title are required' }, { status: 400 });
diff --git a/app/api/member-email/route.ts b/app/api/member-email/route.ts
index 90c8d8d..dd31ff4 100644
--- a/app/api/member-email/route.ts
+++ b/app/api/member-email/route.ts
@@ -18,6 +18,7 @@
*/
import { NextRequest, NextResponse } from 'next/server';
+import { readJson } from '@/lib/body';
import { requireRole } from '@/lib/require-role';
import { checkRateLimit } from '@/lib/rate-limit';
@@ -57,7 +58,7 @@ export async function POST(request: NextRequest) {
}
try {
- const body = await request.json();
+ const body = await readJson(request);
const { emailType, topic, keyPoints, tone, orgName, orgMission } = body;
if (!emailType || !topic) {
diff --git a/app/api/news/[id]/route.ts b/app/api/news/[id]/route.ts
index 3268d75..d848066 100644
--- a/app/api/news/[id]/route.ts
+++ b/app/api/news/[id]/route.ts
@@ -1,4 +1,5 @@
import { NextRequest, NextResponse } from 'next/server';
+import { readJson } from '@/lib/body';
import { query } from '@/lib/db';
import { requireRole } from '@/lib/require-role';
import { auditLog } from '@/lib/audit';
@@ -18,7 +19,7 @@ export async function PATCH(request: NextRequest, context: RouteContext) {
const { id } = await context.params;
try {
- const body = await request.json();
+ const body = await readJson(request);
const allowedFields = ['headline', 'outlet', 'url', 'summary', 'tags', 'is_used', 'session_id', 'is_pinned'];
const setClauses: string[] = [];
diff --git a/app/api/news/detect-mentions/route.ts b/app/api/news/detect-mentions/route.ts
index d9cb308..dc6ab6b 100644
--- a/app/api/news/detect-mentions/route.ts
+++ b/app/api/news/detect-mentions/route.ts
@@ -1,4 +1,5 @@
import { NextRequest, NextResponse } from 'next/server';
+import { readJson } from '@/lib/body';
import { requireRole } from '@/lib/require-role';
import { getOrgId } from '@/lib/orgId';
import { batchDetectMentions } from '@/lib/detect-mentions';
@@ -26,7 +27,7 @@ export async function POST(request: NextRequest) {
const contentType = request.headers.get('content-type') ?? '';
if (contentType.includes('application/json')) {
try {
- const body = await request.json();
+ const body = await readJson(request);
if (Array.isArray(body.articleIds)) {
articleIds = body.articleIds.filter((id: unknown) => typeof id === 'string');
}
diff --git a/app/api/news/route.ts b/app/api/news/route.ts
index e1e2789..f41ce96 100644
--- a/app/api/news/route.ts
+++ b/app/api/news/route.ts
@@ -1,4 +1,5 @@
import { NextRequest, NextResponse } from 'next/server';
+import { readJson } from '@/lib/body';
import { query } from '@/lib/db';
import { requireRole } from '@/lib/require-role';
import { auditLog } from '@/lib/audit';
@@ -119,7 +120,7 @@ export async function POST(request: NextRequest) {
if (auth instanceof NextResponse) return auth;
try {
- const body = await request.json();
+ const body = await readJson(request);
if (!body.headline || typeof body.headline !== 'string' || body.headline.trim() === '') {
return NextResponse.json({ error: 'headline is required' }, { status: 400 });
diff --git a/app/api/nonprofit/signup/route.ts b/app/api/nonprofit/signup/route.ts
index 15b1a32..3f38796 100644
--- a/app/api/nonprofit/signup/route.ts
+++ b/app/api/nonprofit/signup/route.ts
@@ -1,4 +1,5 @@
import { NextRequest, NextResponse } from 'next/server';
+import { readJson } from '@/lib/body';
import { query } from '@/lib/db';
import { hashPassword } from '@/lib/auth';
import { auditLog } from '@/lib/audit';
@@ -112,7 +113,7 @@ export async function GET(request: NextRequest) {
export async function POST(request: NextRequest) {
try {
- const body: NonprofitSignupBody = await request.json();
+ const body: NonprofitSignupBody = await readJson(request);
// ------------------------------------------------------------------
// Validate required fields
diff --git a/app/api/onboard/[id]/route.ts b/app/api/onboard/[id]/route.ts
index 8408599..da5f90a 100644
--- a/app/api/onboard/[id]/route.ts
+++ b/app/api/onboard/[id]/route.ts
@@ -1,4 +1,5 @@
import { NextRequest, NextResponse } from 'next/server';
+import { readJson } from '@/lib/body';
import { query } from '@/lib/db';
import { requireRole } from '@/lib/require-role';
import { getOrgId } from '@/lib/orgId';
@@ -67,7 +68,7 @@ export async function PUT(
}
try {
- const body = (await request.json()) as {
+ const body = (await readJson(request)) as {
status?: string;
notes?: string;
contacted_at?: string;
diff --git a/app/api/onboard/trigger/route.ts b/app/api/onboard/trigger/route.ts
index 277ad15..4911c01 100644
--- a/app/api/onboard/trigger/route.ts
+++ b/app/api/onboard/trigger/route.ts
@@ -1,4 +1,5 @@
import { NextRequest, NextResponse } from 'next/server';
+import { readJson } from '@/lib/body';
import { requireRole } from '@/lib/require-role';
// ─── POST /api/onboard/trigger ────────────────────────────────────────────────
@@ -13,7 +14,7 @@ export async function POST(request: NextRequest) {
if (auth instanceof NextResponse) return auth;
try {
- const body = (await request.json()) as { topic_id?: string; topic?: string };
+ const body = (await readJson(request)) as { topic_id?: string; topic?: string };
const { topic_id, topic } = body;
if (!topic_id && !topic) {
diff --git a/app/api/orgs/signup/route.ts b/app/api/orgs/signup/route.ts
index 5c0f559..40ae289 100644
--- a/app/api/orgs/signup/route.ts
+++ b/app/api/orgs/signup/route.ts
@@ -1,4 +1,5 @@
import { NextRequest, NextResponse } from 'next/server';
+import { readJson } from '@/lib/body';
import { query } from '@/lib/db';
import { requireRole } from '@/lib/require-role';
import { auditLog } from '@/lib/audit';
@@ -13,7 +14,7 @@ export async function POST(request: NextRequest) {
if (auth instanceof NextResponse) return auth;
try {
- const data = await request.json();
+ const data = await readJson(request);
if (!data.name || typeof data.name !== 'string' || data.name.trim() === '') {
return NextResponse.json({ error: 'Organization name is required' }, { status: 400 });
diff --git a/app/api/outreach-templates/route.ts b/app/api/outreach-templates/route.ts
index b30250e..4de6d3a 100644
--- a/app/api/outreach-templates/route.ts
+++ b/app/api/outreach-templates/route.ts
@@ -1,4 +1,5 @@
import { NextRequest, NextResponse } from 'next/server';
+import { readJson } from '@/lib/body';
import { query } from '@/lib/db';
import { requireRole } from '@/lib/require-role';
@@ -59,7 +60,7 @@ export async function POST(request: NextRequest) {
if (auth instanceof NextResponse) return auth;
try {
- const body = await request.json();
+ const body = await readJson(request);
const { template_type, name, subject, body: templateBody, variables, compliance_safe, org_type_safe } = body;
if (!template_type || !name || !templateBody) {
diff --git a/app/api/partners/posts/route.ts b/app/api/partners/posts/route.ts
index d1a2e1b..ba6d26c 100644
--- a/app/api/partners/posts/route.ts
+++ b/app/api/partners/posts/route.ts
@@ -1,4 +1,5 @@
import { NextRequest, NextResponse } from 'next/server';
+import { readJson } from '@/lib/body';
import { query } from '@/lib/db';
import { requireRole } from '@/lib/require-role';
@@ -57,7 +58,7 @@ export async function POST(request: NextRequest) {
if (auth instanceof NextResponse) return auth;
try {
- const body = await request.json();
+ const body = await readJson(request);
const { partner_id, platform, content, url, post_date, likes_count, retweets_count, replies_count } = body;
if (!partner_id || !content) {
diff --git a/app/api/partners/route.ts b/app/api/partners/route.ts
index 7978c5e..11b88cb 100644
--- a/app/api/partners/route.ts
+++ b/app/api/partners/route.ts
@@ -1,4 +1,5 @@
import { NextRequest, NextResponse } from 'next/server';
+import { readJson } from '@/lib/body';
import { query } from '@/lib/db';
import { requireRole } from '@/lib/require-role';
import { getOrgId } from '@/lib/orgId';
@@ -71,7 +72,7 @@ export async function POST(request: NextRequest) {
if (auth instanceof NextResponse) return auth;
try {
- const body = await request.json();
+ const body = await readJson(request);
const {
partner_type, name, contact_name, contact_title, email, phone,
website_url, city, state, partnership_since, partnership_value,
@@ -115,7 +116,7 @@ export async function PATCH(request: NextRequest) {
if (auth instanceof NextResponse) return auth;
try {
- const body = await request.json();
+ const body = await readJson(request);
const { id, ...updates } = body;
if (!id) return NextResponse.json({ error: 'id required' }, { status: 400 });
diff --git a/app/api/petitions/[id]/campaigns/route.ts b/app/api/petitions/[id]/campaigns/route.ts
index 723c58c..ce4465c 100644
--- a/app/api/petitions/[id]/campaigns/route.ts
+++ b/app/api/petitions/[id]/campaigns/route.ts
@@ -8,6 +8,7 @@
*/
import { NextRequest, NextResponse } from 'next/server';
+import { readJson } from '@/lib/body';
import { query } from '@/lib/db';
import { getTargetLegislators, generateCampaignEmail } from '@/lib/petition-engine';
import { requireRole } from '@/lib/require-role';
@@ -107,7 +108,7 @@ export async function POST(
const orgId = auth.role === 'admin' ? getOrgId(req) : auth.orgId;
try {
- const body = await req.json();
+ const body = await readJson(req);
const { action } = body;
// Get petition data (verify org ownership)
diff --git a/app/api/petitions/[id]/geo/route.ts b/app/api/petitions/[id]/geo/route.ts
index 699aa7c..6f57221 100644
--- a/app/api/petitions/[id]/geo/route.ts
+++ b/app/api/petitions/[id]/geo/route.ts
@@ -7,6 +7,7 @@
*/
import { NextRequest, NextResponse } from 'next/server';
+import { readJson } from '@/lib/body';
import { query } from '@/lib/db';
import { updatePetitionGeoStats, getTargetLegislators } from '@/lib/petition-engine';
import { requireRole } from '@/lib/require-role';
@@ -117,7 +118,7 @@ export async function POST(
return NextResponse.json({ error: 'Petition not found' }, { status: 404 });
}
- const body = await req.json();
+ const body = await readJson(req);
const { action } = body;
if (action === 'refresh') {
diff --git a/app/api/petitions/[id]/route.ts b/app/api/petitions/[id]/route.ts
index 80a84aa..1578b4d 100644
--- a/app/api/petitions/[id]/route.ts
+++ b/app/api/petitions/[id]/route.ts
@@ -1,4 +1,5 @@
import { NextRequest, NextResponse } from 'next/server';
+import { readJson } from '@/lib/body';
import { query, getClient } from '@/lib/db';
import { requireRole } from '@/lib/require-role';
import { auditLog } from '@/lib/audit';
@@ -48,7 +49,7 @@ export async function PATCH(request: NextRequest, context: RouteContext) {
const orgId = auth.role === 'admin' ? getOrgId(request) : auth.orgId;
try {
- const body = await request.json();
+ const body = await readJson(request);
const allowedFields = [
'title', 'url', 'description', 'status', 'signature_count',
'signature_goal', 'target', 'category', 'tags', 'ai_suggestion',
diff --git a/app/api/petitions/[id]/signatures/route.ts b/app/api/petitions/[id]/signatures/route.ts
index 0d4a463..49ea176 100644
--- a/app/api/petitions/[id]/signatures/route.ts
+++ b/app/api/petitions/[id]/signatures/route.ts
@@ -7,6 +7,7 @@
*/
import { NextRequest, NextResponse } from 'next/server';
+import { readJson } from '@/lib/body';
import { query } from '@/lib/db';
import { zipToDistrict, updatePetitionGeoStats } from '@/lib/petition-engine';
import { createHash } from 'crypto';
@@ -107,7 +108,7 @@ export async function POST(
const orgId = auth.role === 'admin' ? getOrgId(req) : auth.orgId;
try {
- const body = await req.json();
+ const body = await readJson(req);
const { name, email, zip, comment, source } = body;
if (!email) {
diff --git a/app/api/petitions/draft/route.ts b/app/api/petitions/draft/route.ts
index bb7d097..624468b 100644
--- a/app/api/petitions/draft/route.ts
+++ b/app/api/petitions/draft/route.ts
@@ -7,6 +7,7 @@
*/
import { NextRequest, NextResponse } from 'next/server';
+import { readJson } from '@/lib/body';
import { requireRole } from '@/lib/require-role';
const GEMINI_KEY = process.env.GEMINI_API_KEY || '';
@@ -18,7 +19,7 @@ export async function POST(req: NextRequest) {
let topicTitle = 'Important Issue';
try {
- const body = await req.json();
+ const body = await readJson(req);
topicTitle = body.topicTitle || 'Important Issue';
const { topicDescription, category, source } = body;
diff --git a/app/api/petitions/route.ts b/app/api/petitions/route.ts
index 9359a58..9877841 100644
--- a/app/api/petitions/route.ts
+++ b/app/api/petitions/route.ts
@@ -1,4 +1,5 @@
import { NextRequest, NextResponse } from 'next/server';
+import { readJson } from '@/lib/body';
import { query } from '@/lib/db';
import { requireRole } from '@/lib/require-role';
import { auditLog } from '@/lib/audit';
@@ -116,7 +117,7 @@ export async function POST(request: NextRequest) {
if (auth instanceof NextResponse) return auth;
try {
- const body = await request.json();
+ const body = await readJson(request);
if (!body.title || typeof body.title !== 'string' || body.title.trim() === '') {
return NextResponse.json({ error: 'title is required' }, { status: 400 });
diff --git a/app/api/pipeline/[id]/approve/route.ts b/app/api/pipeline/[id]/approve/route.ts
index 2d7a7b0..bb6de2e 100644
--- a/app/api/pipeline/[id]/approve/route.ts
+++ b/app/api/pipeline/[id]/approve/route.ts
@@ -1,4 +1,5 @@
import { NextRequest, NextResponse } from 'next/server';
+import { readJson } from '@/lib/body';
import { query } from '@/lib/db';
import { requireRole } from '@/lib/require-role';
import { auditLog } from '@/lib/audit';
@@ -48,7 +49,7 @@ export async function POST(
// Parse optional review notes
let reviewNotes: string | null = null;
try {
- const body = await request.json();
+ const body = await readJson(request);
if (body.review_notes && typeof body.review_notes === 'string') {
reviewNotes = body.review_notes.trim() || null;
}
diff --git a/app/api/pipeline/[id]/route.ts b/app/api/pipeline/[id]/route.ts
index b17f6b7..98a7063 100644
--- a/app/api/pipeline/[id]/route.ts
+++ b/app/api/pipeline/[id]/route.ts
@@ -1,4 +1,5 @@
import { NextRequest, NextResponse } from 'next/server';
+import { readJson } from '@/lib/body';
import { query } from '@/lib/db';
import { requireRole } from '@/lib/require-role';
import { getOrgId } from '@/lib/orgId';
@@ -70,7 +71,7 @@ export async function PUT(
return NextResponse.json({ error: 'Invalid ID format' }, { status: 400 });
}
- const data = await request.json();
+ const data = await readJson(request);
// Build dynamic SET clause from allowed fields
const allowedFields: Record<string, (v: unknown) => unknown> = {
diff --git a/app/api/pipeline/[id]/schedule/route.ts b/app/api/pipeline/[id]/schedule/route.ts
index 8c91e7c..026056c 100644
--- a/app/api/pipeline/[id]/schedule/route.ts
+++ b/app/api/pipeline/[id]/schedule/route.ts
@@ -1,4 +1,5 @@
import { NextRequest, NextResponse } from 'next/server';
+import { readJson } from '@/lib/body';
import { query } from '@/lib/db';
import { requireRole } from '@/lib/require-role';
import { auditLog } from '@/lib/audit';
@@ -28,7 +29,7 @@ export async function POST(
return NextResponse.json({ error: 'Invalid ID format' }, { status: 400 });
}
- const body = await request.json();
+ const body = await readJson(request);
// Validate platforms
const platforms: string[] = Array.isArray(body.platforms) ? body.platforms : [body.platform].filter(Boolean);
diff --git a/app/api/pipeline/reorder/route.ts b/app/api/pipeline/reorder/route.ts
index 69f5938..bf4a2da 100644
--- a/app/api/pipeline/reorder/route.ts
+++ b/app/api/pipeline/reorder/route.ts
@@ -1,4 +1,5 @@
import { NextRequest, NextResponse } from 'next/server';
+import { readJson } from '@/lib/body';
import { getClient } from '@/lib/db';
import { requireRole } from '@/lib/require-role';
import { auditLog } from '@/lib/audit';
@@ -17,7 +18,7 @@ export async function PATCH(request: NextRequest) {
let data: { items?: unknown } = {};
try {
- data = await request.json();
+ data = await readJson(request);
} catch {
return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 });
}
diff --git a/app/api/pipeline/route.ts b/app/api/pipeline/route.ts
index f9f2a31..650ff2c 100644
--- a/app/api/pipeline/route.ts
+++ b/app/api/pipeline/route.ts
@@ -1,4 +1,5 @@
import { NextRequest, NextResponse } from 'next/server';
+import { readJson } from '@/lib/body';
import { query } from '@/lib/db';
import { requireRole } from '@/lib/require-role';
import { getOrgId } from '@/lib/orgId';
@@ -101,7 +102,7 @@ export async function POST(request: NextRequest) {
if (auth instanceof NextResponse) return auth;
try {
- const data = await request.json();
+ const data = await readJson(request);
if (!data.title || typeof data.title !== 'string' || data.title.trim() === '') {
return NextResponse.json({ error: 'title is required' }, { status: 400 });
diff --git a/app/api/price-tracker/route.ts b/app/api/price-tracker/route.ts
index b227d31..6d01175 100644
--- a/app/api/price-tracker/route.ts
+++ b/app/api/price-tracker/route.ts
@@ -1,4 +1,5 @@
import { NextRequest, NextResponse } from 'next/server';
+import { readJson } from '@/lib/body';
import { query } from '@/lib/db';
import { requireRole } from '@/lib/require-role';
@@ -369,7 +370,7 @@ export async function POST(request: NextRequest) {
if (auth instanceof NextResponse) return auth;
try {
- const body = await request.json();
+ const body = await readJson(request);
const { action, alert_ids } = body;
if (action === 'mark_read' && Array.isArray(alert_ids)) {
diff --git a/app/api/pulse/petitions/[id]/sign/route.ts b/app/api/pulse/petitions/[id]/sign/route.ts
index 0e387d1..7d361d5 100644
--- a/app/api/pulse/petitions/[id]/sign/route.ts
+++ b/app/api/pulse/petitions/[id]/sign/route.ts
@@ -1,4 +1,5 @@
import { NextRequest, NextResponse } from 'next/server';
+import { readJson } from '@/lib/body';
import { createHash } from 'crypto';
import { query, getClient } from '@/lib/db';
import { checkRateLimit } from '@/lib/rate-limit';
@@ -31,7 +32,7 @@ export async function POST(request: NextRequest, context: RouteContext) {
}
try {
- const body = await request.json();
+ const body = await readJson(request);
// Validate required fields
if (!body.name || typeof body.name !== 'string' || body.name.trim().length < 1) {
diff --git a/app/api/pulse/petitions/route.ts b/app/api/pulse/petitions/route.ts
index 72326d1..c402802 100644
--- a/app/api/pulse/petitions/route.ts
+++ b/app/api/pulse/petitions/route.ts
@@ -1,4 +1,5 @@
import { NextRequest, NextResponse } from 'next/server';
+import { readJson } from '@/lib/body';
import { query } from '@/lib/db';
import { checkRateLimit } from '@/lib/rate-limit';
@@ -120,7 +121,7 @@ export async function POST(request: NextRequest) {
{ status: 429, headers: { 'Retry-After': String(Math.ceil(rl.resetIn / 1000)) } },
);
}
- const body = await request.json();
+ const body = await readJson(request);
// Validate required fields
if (!body.title || typeof body.title !== 'string' || body.title.trim().length < 5) {
diff --git a/app/api/pulse/profile/route.ts b/app/api/pulse/profile/route.ts
index 3378d79..52f1ab4 100644
--- a/app/api/pulse/profile/route.ts
+++ b/app/api/pulse/profile/route.ts
@@ -1,4 +1,5 @@
import { NextRequest, NextResponse } from 'next/server';
+import { readJson } from '@/lib/body';
import { query } from '@/lib/db';
import { requireRole } from '@/lib/require-role';
import type { AuthSession } from '@/lib/auth';
@@ -46,7 +47,7 @@ export async function PUT(request: NextRequest) {
const session = result as AuthSession;
try {
- const body = await request.json();
+ const body = await readJson(request);
const displayName = body.display_name;
if (!displayName || typeof displayName !== 'string') {
diff --git a/app/api/schedule/route.ts b/app/api/schedule/route.ts
index 5f6847a..3594517 100644
--- a/app/api/schedule/route.ts
+++ b/app/api/schedule/route.ts
@@ -1,4 +1,5 @@
import { NextRequest, NextResponse } from 'next/server';
+import { readJson } from '@/lib/body';
import { query } from '@/lib/db';
import { requireRole } from '@/lib/require-role';
import { auditLog } from '@/lib/audit';
@@ -25,7 +26,7 @@ export async function POST(request: NextRequest) {
if (auth instanceof NextResponse) return auth;
try {
- const body = await request.json();
+ const body = await readJson(request);
// Validate content_type
const contentType = body.content_type;
diff --git a/app/api/scraper/suggestions/route.ts b/app/api/scraper/suggestions/route.ts
index c9475d7..207a8eb 100644
--- a/app/api/scraper/suggestions/route.ts
+++ b/app/api/scraper/suggestions/route.ts
@@ -1,4 +1,5 @@
import { NextRequest, NextResponse } from 'next/server';
+import { readJson } from '@/lib/body';
import { query } from '@/lib/db';
import { requireRole } from '@/lib/require-role';
import { auditLog } from '@/lib/audit';
@@ -220,7 +221,7 @@ export async function PATCH(request: NextRequest) {
if (auth instanceof NextResponse) return auth;
try {
- const body = await request.json();
+ const body = await readJson(request);
if (!body.id || typeof body.id !== 'string') {
return NextResponse.json({ error: 'id is required' }, { status: 400 });
diff --git a/app/api/sessions/[id]/route.ts b/app/api/sessions/[id]/route.ts
index 4d55b48..d718f9b 100644
--- a/app/api/sessions/[id]/route.ts
+++ b/app/api/sessions/[id]/route.ts
@@ -1,4 +1,5 @@
import { NextRequest, NextResponse } from 'next/server';
+import { readJson } from '@/lib/body';
import { query, getClient } from '@/lib/db';
import { requireRole } from '@/lib/require-role';
import { auditLog } from '@/lib/audit';
@@ -56,7 +57,7 @@ export async function PATCH(request: NextRequest, context: RouteContext) {
const { id } = await context.params;
try {
- const body = await request.json();
+ const body = await readJson(request);
const allowedFields = ['title', 'status', 'planning_checklist', 'planning_notes', 'news_item_ids', 'x_post_ids'];
const setClauses: string[] = [];
const values: unknown[] = [];
diff --git a/app/api/settings/api-keys/route.ts b/app/api/settings/api-keys/route.ts
index 8d56684..ff31d3d 100644
--- a/app/api/settings/api-keys/route.ts
+++ b/app/api/settings/api-keys/route.ts
@@ -1,4 +1,5 @@
import { NextRequest, NextResponse } from 'next/server';
+import { readJson } from '@/lib/body';
import { requireRole } from '@/lib/require-role';
import { generateApiKey, listApiKeys } from '@/lib/credentials';
import { auditLog } from '@/lib/audit';
@@ -23,7 +24,7 @@ export async function POST(request: NextRequest) {
const auth = requireRole(request, 'admin');
if (auth instanceof NextResponse) return auth;
- const body = await request.json().catch(() => ({}));
+ const body = await readJson(request).catch(() => ({}));
const { name, scopes, expires_in_days } = body;
if (!name || typeof name !== 'string' || name.trim().length === 0) {
diff --git a/app/api/settings/credentials/route.ts b/app/api/settings/credentials/route.ts
index b51ccc1..b5586f0 100644
--- a/app/api/settings/credentials/route.ts
+++ b/app/api/settings/credentials/route.ts
@@ -1,4 +1,5 @@
import { NextRequest, NextResponse } from 'next/server';
+import { readJson } from '@/lib/body';
import { query } from '@/lib/db';
import { requireRole } from '@/lib/require-role';
@@ -67,7 +68,7 @@ export async function PATCH(request: NextRequest) {
if (auth instanceof NextResponse) return auth;
try {
- const body = await request.json();
+ const body = await readJson(request);
const { serviceId, credentials: serviceCreds } = body;
if (!serviceId || !serviceCreds) {
diff --git a/app/api/settings/intelligence/route.ts b/app/api/settings/intelligence/route.ts
index 357a2de..195efa8 100644
--- a/app/api/settings/intelligence/route.ts
+++ b/app/api/settings/intelligence/route.ts
@@ -1,4 +1,5 @@
import { NextRequest, NextResponse } from 'next/server';
+import { readJson } from '@/lib/body';
import { query } from '@/lib/db';
import { requireRole } from '@/lib/require-role';
@@ -83,7 +84,7 @@ export async function PUT(request: NextRequest) {
}
try {
- const body = await request.json();
+ const body = await readJson(request);
const { intelligence, api_keys } = body;
// Fetch current api_connections
diff --git a/app/api/settings/people/route.ts b/app/api/settings/people/route.ts
index 31277b6..ed0a5cb 100644
--- a/app/api/settings/people/route.ts
+++ b/app/api/settings/people/route.ts
@@ -1,4 +1,5 @@
import { NextRequest, NextResponse } from 'next/server';
+import { readJson } from '@/lib/body';
import { requireRole } from '@/lib/require-role';
import { query } from '@/lib/db';
import { getOrgId } from '@/lib/orgId';
@@ -41,7 +42,7 @@ export async function PUT(request: NextRequest) {
const orgId = auth.role === 'admin' ? getOrgId(request) : auth.orgId;
const key = settingKey(orgId);
- const body = await request.json();
+ const body = await readJson(request);
const value = {
employees: Array.isArray(body.employees) ? body.employees : [],
diff --git a/app/api/settings/pulse-config/route.ts b/app/api/settings/pulse-config/route.ts
index e40897f..3e5fb6c 100644
--- a/app/api/settings/pulse-config/route.ts
+++ b/app/api/settings/pulse-config/route.ts
@@ -1,4 +1,5 @@
import { NextRequest, NextResponse } from 'next/server';
+import { readJson } from '@/lib/body';
import { query } from '@/lib/db';
import { requireRole } from '@/lib/require-role';
@@ -42,7 +43,7 @@ export async function PUT(request: NextRequest) {
if (result instanceof NextResponse) return result;
try {
- const body = await request.json();
+ const body = await readJson(request);
const { config } = body;
if (!config || typeof config !== 'object') {
diff --git a/app/api/settings/route.ts b/app/api/settings/route.ts
index 574047e..20892a2 100644
--- a/app/api/settings/route.ts
+++ b/app/api/settings/route.ts
@@ -1,4 +1,5 @@
import { NextRequest, NextResponse } from 'next/server';
+import { readJson } from '@/lib/body';
import { query } from '@/lib/db';
import { requireRole } from '@/lib/require-role';
import { getOrgId } from '@/lib/orgId';
@@ -55,7 +56,7 @@ export async function PATCH(request: NextRequest) {
if (auth instanceof NextResponse) return auth;
try {
- const body = await request.json();
+ const body = await readJson(request);
const allowedKeys = ['org_profile', 'email_config', 'ai_config', 'integrations', 'alert_preferences'];
let updated = 0;
diff --git a/app/api/settings/tier-credentials/[id]/route.ts b/app/api/settings/tier-credentials/[id]/route.ts
index 1901940..65ad4c3 100644
--- a/app/api/settings/tier-credentials/[id]/route.ts
+++ b/app/api/settings/tier-credentials/[id]/route.ts
@@ -1,4 +1,5 @@
import { NextRequest, NextResponse } from 'next/server';
+import { readJson } from '@/lib/body';
import { query } from '@/lib/db';
import { hashPassword, ALL_ROLES, type UserRole } from '@/lib/auth';
import { requireRole } from '@/lib/require-role';
@@ -18,7 +19,7 @@ export async function PUT(request: NextRequest, context: RouteContext) {
const { id } = await context.params;
try {
- const body = await request.json();
+ const body = await readJson(request);
const { display_name, role, org_id, password } = body;
// Verify credential exists
diff --git a/app/api/settings/tier-credentials/route.ts b/app/api/settings/tier-credentials/route.ts
index 6704ddb..3d87bcf 100644
--- a/app/api/settings/tier-credentials/route.ts
+++ b/app/api/settings/tier-credentials/route.ts
@@ -1,4 +1,5 @@
import { NextRequest, NextResponse } from 'next/server';
+import { readJson } from '@/lib/body';
import { query } from '@/lib/db';
import { hashPassword, ALL_ROLES, type UserRole } from '@/lib/auth';
import { requireRole } from '@/lib/require-role';
@@ -37,7 +38,7 @@ export async function POST(request: NextRequest) {
if (session instanceof NextResponse) return session;
try {
- const body = await request.json();
+ const body = await readJson(request);
const {
username,
password,
diff --git a/app/api/social/accounts/route.ts b/app/api/social/accounts/route.ts
index e083f81..b544318 100644
--- a/app/api/social/accounts/route.ts
+++ b/app/api/social/accounts/route.ts
@@ -1,4 +1,5 @@
import { NextRequest, NextResponse } from 'next/server';
+import { readJson } from '@/lib/body';
import { query } from '@/lib/db';
import { requireRole } from '@/lib/require-role';
@@ -29,7 +30,7 @@ export async function PATCH(request: NextRequest) {
const auth = requireRole(request, 'admin');
if (auth instanceof NextResponse) return auth;
- const { id, status } = await request.json();
+ const { id, status } = await readJson(request);
if (!id || !status) {
return NextResponse.json({ error: 'id and status required' }, { status: 400 });
}
diff --git a/app/api/social/ai-compose/route.ts b/app/api/social/ai-compose/route.ts
index 6a79703..4eeab3d 100644
--- a/app/api/social/ai-compose/route.ts
+++ b/app/api/social/ai-compose/route.ts
@@ -1,4 +1,5 @@
import { NextRequest, NextResponse } from 'next/server';
+import { readJson } from '@/lib/body';
import { requireRole } from '@/lib/require-role';
const GEMINI_API_KEY = process.env.GEMINI_API_KEY;
@@ -56,7 +57,7 @@ export async function POST(request: NextRequest) {
const auth = requireRole(request, 'admin', 'staff');
if (auth instanceof NextResponse) return auth;
- const body = await request.json();
+ const body = await readJson(request);
const {
topic,
platform,
diff --git a/app/api/social/ai-image/route.ts b/app/api/social/ai-image/route.ts
index fb3578c..d690299 100644
--- a/app/api/social/ai-image/route.ts
+++ b/app/api/social/ai-image/route.ts
@@ -1,4 +1,5 @@
import { NextRequest, NextResponse } from 'next/server';
+import { readJson } from '@/lib/body';
import { randomUUID } from 'crypto';
import { promises as fs } from 'fs';
import path from 'path';
@@ -23,7 +24,7 @@ export async function POST(request: NextRequest) {
if (auth instanceof NextResponse) return auth;
let body: { prompt?: string; post_id?: string };
try {
- body = await request.json();
+ body = await readJson(request);
} catch {
return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 });
}
diff --git a/app/api/social/bulk-schedule/route.ts b/app/api/social/bulk-schedule/route.ts
index 03bcdae..f84edc5 100644
--- a/app/api/social/bulk-schedule/route.ts
+++ b/app/api/social/bulk-schedule/route.ts
@@ -1,4 +1,5 @@
import { NextRequest, NextResponse } from 'next/server';
+import { readJson } from '@/lib/body';
import { query, getClient } from '@/lib/db';
import { requireRole } from '@/lib/require-role';
@@ -93,7 +94,7 @@ export async function POST(request: NextRequest) {
if (auth instanceof NextResponse) return auth;
let body: BulkScheduleBody;
try {
- body = await request.json();
+ body = await readJson(request);
} catch {
return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 });
}
diff --git a/app/api/social/competitors/crawl/route.ts b/app/api/social/competitors/crawl/route.ts
index 5af4a82..716fe7f 100644
--- a/app/api/social/competitors/crawl/route.ts
+++ b/app/api/social/competitors/crawl/route.ts
@@ -1,4 +1,5 @@
import { NextRequest, NextResponse } from 'next/server';
+import { readJson } from '@/lib/body';
import { query } from '@/lib/db';
import { requireRole } from '@/lib/require-role';
import { crawlSocialLinks, type FoundHandle } from '@/lib/social-crawl';
@@ -21,7 +22,7 @@ export async function POST(request: NextRequest) {
const auth = requireRole(request, 'admin', 'staff');
if (auth instanceof NextResponse) return auth;
- const body = await request.json();
+ const body = await readJson(request);
const { url, category = 'competitor', expertise_area } = body as {
url: string;
category?: string;
diff --git a/app/api/social/connect/route.ts b/app/api/social/connect/route.ts
index 7500b80..0d653ca 100644
--- a/app/api/social/connect/route.ts
+++ b/app/api/social/connect/route.ts
@@ -1,4 +1,5 @@
import { NextRequest, NextResponse } from 'next/server';
+import { readJson } from '@/lib/body';
import { requireRole } from '@/lib/require-role';
import { storeSocialCredentials, disconnectSocial, PLATFORM_FIELDS } from '@/lib/credentials';
import { auditLog } from '@/lib/audit';
@@ -14,7 +15,7 @@ export async function POST(request: NextRequest) {
const auth = requireRole(request, 'admin');
if (auth instanceof NextResponse) return auth;
- const body = await request.json().catch(() => ({}));
+ const body = await readJson(request).catch(() => ({}));
const { platform, credentials } = body;
if (!platform || !VALID_PLATFORMS.includes(platform)) {
@@ -57,7 +58,7 @@ export async function DELETE(request: NextRequest) {
const auth = requireRole(request, 'admin');
if (auth instanceof NextResponse) return auth;
- const body = await request.json().catch(() => ({}));
+ const body = await readJson(request).catch(() => ({}));
const { platform } = body;
if (!platform || !VALID_PLATFORMS.includes(platform)) {
diff --git a/app/api/social/posts/approve/route.ts b/app/api/social/posts/approve/route.ts
index 3f3b6c3..dcad443 100644
--- a/app/api/social/posts/approve/route.ts
+++ b/app/api/social/posts/approve/route.ts
@@ -1,4 +1,5 @@
import { NextRequest, NextResponse } from 'next/server';
+import { readJson } from '@/lib/body';
import { query, getClient } from '@/lib/db';
import { requireRole } from '@/lib/require-role';
@@ -19,7 +20,7 @@ export async function POST(request: NextRequest) {
scheduled_at?: string;
};
try {
- body = await request.json();
+ body = await readJson(request);
} catch {
return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 });
}
diff --git a/app/api/social/publish/route.ts b/app/api/social/publish/route.ts
index 67a2215..63e3287 100644
--- a/app/api/social/publish/route.ts
+++ b/app/api/social/publish/route.ts
@@ -1,4 +1,5 @@
import { NextRequest, NextResponse } from 'next/server';
+import { readJson } from '@/lib/body';
import { query } from '@/lib/db';
import { requireRole } from '@/lib/require-role';
import { buildInternalAuthHeader } from '@/lib/cron-auth';
@@ -12,7 +13,7 @@ export async function POST(request: NextRequest) {
const auth = requireRole(request, 'admin');
if (auth instanceof NextResponse) return auth;
- const { post_id, cross_post_group } = await request.json();
+ const { post_id, cross_post_group } = await readJson(request);
if (cross_post_group) {
const result = await query(
diff --git a/app/api/social/schedule/route.ts b/app/api/social/schedule/route.ts
index 6f5a882..5a6a72d 100644
--- a/app/api/social/schedule/route.ts
+++ b/app/api/social/schedule/route.ts
@@ -1,4 +1,5 @@
import { NextRequest, NextResponse } from 'next/server';
+import { readJson } from '@/lib/body';
import { query } from '@/lib/db';
import { requireRole } from '@/lib/require-role';
@@ -30,7 +31,7 @@ export async function POST(request: NextRequest) {
const auth = requireRole(request, 'admin', 'staff');
if (auth instanceof NextResponse) return auth;
- const { post_id, scheduled_at, target_platforms = [], recurrence_rule } = await request.json();
+ const { post_id, scheduled_at, target_platforms = [], recurrence_rule } = await readJson(request);
if (!post_id || !scheduled_at) {
return NextResponse.json({ error: 'post_id and scheduled_at required' }, { status: 400 });
}
diff --git a/app/api/statements/[id]/route.ts b/app/api/statements/[id]/route.ts
index 5c51dea..0a28da0 100644
--- a/app/api/statements/[id]/route.ts
+++ b/app/api/statements/[id]/route.ts
@@ -1,4 +1,5 @@
import { NextRequest, NextResponse } from 'next/server';
+import { readJson } from '@/lib/body';
import { query } from '@/lib/db';
import { requireRole } from '@/lib/require-role';
import { auditLog } from '@/lib/audit';
@@ -36,7 +37,7 @@ export async function PUT(request: NextRequest, ctx: RouteContext) {
const { id } = await ctx.params;
try {
- const data = await request.json();
+ const data = await readJson(request);
const orgId = auth.role === 'admin' ? getOrgId(request) : auth.orgId;
const orgFilter = orgId ? ' AND org_id = $2' : '';
diff --git a/app/api/statements/route.ts b/app/api/statements/route.ts
index e397855..fe08eda 100644
--- a/app/api/statements/route.ts
+++ b/app/api/statements/route.ts
@@ -1,4 +1,5 @@
import { NextRequest, NextResponse } from 'next/server';
+import { readJson } from '@/lib/body';
import { query } from '@/lib/db';
import { requireRole } from '@/lib/require-role';
import { auditLog } from '@/lib/audit';
@@ -77,7 +78,7 @@ export async function POST(request: NextRequest) {
if (auth instanceof NextResponse) return auth;
try {
- const data = await request.json();
+ const data = await readJson(request);
if (!data.title || !data.body_html || !data.event_title) {
return NextResponse.json(
diff --git a/app/api/user-preferences/route.ts b/app/api/user-preferences/route.ts
index d04474c..9d6ca47 100644
--- a/app/api/user-preferences/route.ts
+++ b/app/api/user-preferences/route.ts
@@ -5,6 +5,7 @@
*/
import { NextRequest, NextResponse } from 'next/server';
+import { readJson } from '@/lib/body';
import { requireRole } from '@/lib/require-role';
import { query } from '@/lib/db';
@@ -42,7 +43,7 @@ async function writePreference(request: NextRequest) {
if (auth instanceof NextResponse) return auth;
try {
- const body = await request.json();
+ const body = await readJson(request);
const { key, value } = body as { key?: string; value?: unknown };
if (!key || typeof key !== 'string') {
return NextResponse.json({ error: 'key required' }, { status: 400 });
diff --git a/app/api/webhooks/pulse-topic/route.ts b/app/api/webhooks/pulse-topic/route.ts
index 6977c28..ad47408 100644
--- a/app/api/webhooks/pulse-topic/route.ts
+++ b/app/api/webhooks/pulse-topic/route.ts
@@ -1,4 +1,5 @@
import { NextRequest, NextResponse } from 'next/server';
+import { readJson } from '@/lib/body';
import { query } from '@/lib/db';
import { auditLog } from '@/lib/audit';
import { getBrand } from '@/lib/brand';
@@ -36,7 +37,7 @@ export async function POST(request: NextRequest) {
}
try {
- const data = await request.json();
+ const data = await readJson(request);
const topic = data.topic;
if (!topic || !topic.title) {
diff --git a/app/api/xsessions/posts/route.ts b/app/api/xsessions/posts/route.ts
index a175ddb..d4cfa58 100644
--- a/app/api/xsessions/posts/route.ts
+++ b/app/api/xsessions/posts/route.ts
@@ -1,4 +1,5 @@
import { NextRequest, NextResponse } from 'next/server';
+import { readJson } from '@/lib/body';
import { query } from '@/lib/db';
import { requireRole } from '@/lib/require-role';
@@ -12,7 +13,7 @@ export async function PATCH(request: NextRequest) {
if (auth instanceof NextResponse) return auth;
try {
- const body = await request.json();
+ const body = await readJson(request);
const { id, is_used } = body;
if (!id) {
diff --git a/app/api/xsessions/route.ts b/app/api/xsessions/route.ts
index 4bcf472..3184123 100644
--- a/app/api/xsessions/route.ts
+++ b/app/api/xsessions/route.ts
@@ -1,4 +1,5 @@
import { NextRequest, NextResponse } from 'next/server';
+import { readJson } from '@/lib/body';
import { query, getClient } from '@/lib/db';
import { requireRole } from '@/lib/require-role';
@@ -74,7 +75,7 @@ export async function POST(request: NextRequest) {
const client = await getClient();
try {
- const body = await request.json();
+ const body = await readJson(request);
const queryParams = body.query_params || { terms: 'advocacy' };
const searchTerms = queryParams.terms || 'advocacy';
diff --git a/lib/body.ts b/lib/body.ts
index 18ab14d..d002869 100644
--- a/lib/body.ts
+++ b/lib/body.ts
@@ -14,7 +14,11 @@ export class BodyParseError extends Error {
}
}
-export async function readJson<T = Record<string, unknown>>(request: Request): Promise<T> {
+// Defaults to `any` to match the ergonomics of `request.json()` — this lets it be a
+// drop-in replacement across existing handlers without introducing `unknown`-member
+// type errors. Pass an explicit type param where you want stronger typing.
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+export async function readJson<T = any>(request: Request): Promise<T> {
const raw = await request.text();
if (!raw || raw.trim() === '') return {} as T;
try {
diff --git a/tests/api-write-smoke.mjs b/tests/api-write-smoke.mjs
index e689d43..71b295e 100644
--- a/tests/api-write-smoke.mjs
+++ b/tests/api-write-smoke.mjs
@@ -87,7 +87,9 @@ async function call(path, verb, cookie, extra = {}) {
const headers = { 'Content-Type': 'application/json', ...extra };
if (cookie) headers.Cookie = `norma-auth=${cookie}`;
// empty body for verbs that carry one — this is what keeps the probe side-effect-free
- const body = verb === 'DELETE' ? undefined : '{}';
+ // Send NO body (not {}) — replicates a client omitting the body, which is what makes
+ // an unguarded request.json() throw 'Unexpected end of JSON input' -> 500 in prod.
+ const body = undefined;
try {
const res = await fetch(`${BASE}${path}`, { method: verb, headers, body, redirect: 'manual' });
return res.status;
← 959da8b test+fix(cycle5): write-path smoke (216 mutations) + Playwri
·
back to Norma
·
test(cycle6): CRUD lifecycle suite (create->read->update->de e958e07 →