Phase B E2E: UNION rewrites into /news, accept distribute-rmp payload shape

src/lib/articles/legacy-source.ts
  - getLegacyArticleBySlug, getLegacyArticlesBySection, searchLegacyArticles,
    getLegacyRecentSlugs, getLegacyRecentSitemapEntries: each now UNIONs
    bb.ai_rewritten_articles (status=published) with bb.wp_imported_posts.
    Rewrites carry persona attribution (author/avatar/title) via a
    persona:ai_personas(slug,name,beat,avatar_url) embed.
  - Section routing for rewrites is by category, not tags (rewrites do not
    use the WP tag taxonomy — they have category strings written by Claude).

src/app/api/ai/submit/route.ts
  - Bearer auth via BB_INGEST_BEARER (preferred) or x-internal-key (legacy)
  - Polymorphic payload: accepts native {rawTitle, rawContent, ...} OR the
    distribute-rmp shape {title, body, contributor:{...}, attachments,
    submission_id, ...}. Distribute attachments + featured image survive in
    bb.ai_original_submissions.metadata.

src/app/api/ai/rewrite/route.ts
  - Same bearer auth pattern as submit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Claude (Phase B)
2026-05-14 19:28:36 +00:00
parent f20f4c3250
commit 85599d14cc
3 changed files with 314 additions and 53 deletions

View File

@@ -4,14 +4,25 @@ import { rewriteSubmission } from "@/lib/ai/rewrite-pipeline";
export const runtime = "nodejs";
export const maxDuration = 300;
function bearerToken(req: NextRequest): string | null {
const h = req.headers.get("authorization") || "";
const m = h.match(/^Bearer\s+(.+)$/i);
return m ? m[1].trim() : null;
}
function checkAuth(req: NextRequest): boolean {
const wantBearer = process.env.BB_INGEST_BEARER || "";
const wantInternalKey = process.env.BB_INTERNAL_INGEST_KEY || "";
if (!wantBearer && !wantInternalKey) return false;
if (wantBearer && bearerToken(req) === wantBearer) return true;
if (wantInternalKey && (req.headers.get("x-internal-key") || "") === wantInternalKey) return true;
return false;
}
export async function POST(req: NextRequest) {
try {
const internalKey = process.env.BB_INTERNAL_INGEST_KEY;
if (internalKey) {
const got = req.headers.get("x-internal-key");
if (got !== internalKey) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
if (!checkAuth(req)) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const { submissionId, personaSlug } = (await req.json()) as {

View File

@@ -4,7 +4,7 @@ import { ingestRawSubmission, rewriteSubmission } from "@/lib/ai/rewrite-pipelin
export const runtime = "nodejs";
export const maxDuration = 300;
interface SubmitBody {
interface NativePayload {
rawTitle: string;
rawContent: string;
rawHtml?: string;
@@ -12,25 +12,92 @@ interface SubmitBody {
contributorName?: string;
personaSlug?: string;
source?: string;
metadata?: Record<string, unknown>;
}
interface DistributePayload {
submission_id?: string;
property?: string;
title: string;
body: string;
featured_image_url?: string | null;
attachments?: unknown[];
contributor?: {
id?: string;
email?: string;
firm_name?: string;
contact_name?: string;
phone?: string;
};
}
function bearerToken(req: NextRequest): string | null {
const h = req.headers.get("authorization") || "";
const m = h.match(/^Bearer\s+(.+)$/i);
return m ? m[1].trim() : null;
}
function checkAuth(req: NextRequest): { ok: boolean; reason?: string } {
const wantBearer = process.env.BB_INGEST_BEARER || "";
const wantInternalKey = process.env.BB_INTERNAL_INGEST_KEY || "";
if (!wantBearer && !wantInternalKey) {
return { ok: false, reason: "ingest auth not configured (set BB_INGEST_BEARER)" };
}
if (wantBearer && bearerToken(req) === wantBearer) return { ok: true };
if (wantInternalKey && (req.headers.get("x-internal-key") || "") === wantInternalKey) return { ok: true };
return { ok: false, reason: "Unauthorized" };
}
function normalize(raw: any): NativePayload | null {
if (!raw || typeof raw !== "object") return null;
// Native shape: { rawTitle, rawContent, ... }
if (typeof raw.rawTitle === "string" && typeof raw.rawContent === "string") {
return raw as NativePayload;
}
// Distribute-rmp shape: { title, body, contributor: {...} }
if (typeof raw.title === "string" && typeof raw.body === "string") {
const d = raw as DistributePayload;
const contributorName = d.contributor?.contact_name || d.contributor?.firm_name || undefined;
const contributorEmail = d.contributor?.email || undefined;
return {
rawTitle: d.title,
rawContent: d.body.replace(/<[^>]*>/g, "").replace(/\s+/g, " ").trim(),
rawHtml: d.body,
contributorName,
contributorEmail,
source: "distribute-rmp",
metadata: {
distribute_submission_id: d.submission_id || null,
property: d.property || null,
featured_image_url: d.featured_image_url || null,
attachments: d.attachments || [],
contributor_firm: d.contributor?.firm_name || null,
contributor_phone: d.contributor?.phone || null,
contributor_id: d.contributor?.id || null,
},
};
}
return null;
}
export async function POST(req: NextRequest) {
try {
const internalKey = process.env.BB_INTERNAL_INGEST_KEY;
if (internalKey) {
const got = req.headers.get("x-internal-key");
if (got !== internalKey) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const auth = checkAuth(req);
if (!auth.ok) {
return NextResponse.json({ error: auth.reason || "Unauthorized" }, { status: 401 });
}
const body = (await req.json()) as SubmitBody;
if (!body.rawTitle || !body.rawContent) {
return NextResponse.json({ error: "rawTitle and rawContent required" }, { status: 400 });
const raw = await req.json();
const payload = normalize(raw);
if (!payload) {
return NextResponse.json(
{ error: "expected { rawTitle, rawContent } or { title, body }" },
{ status: 400 }
);
}
const submissionId = await ingestRawSubmission(body);
const outcome = await rewriteSubmission({ submissionId, personaSlug: body.personaSlug });
const submissionId = await ingestRawSubmission(payload);
const outcome = await rewriteSubmission({ submissionId, personaSlug: payload.personaSlug });
return NextResponse.json({ ok: true, submissionId, outcome });
} catch (err: any) {