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:
@@ -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 {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user