/** * Irizpro Publish API v1 — receiver for Next.js (App Router). * Guide: https:///docs/publish-api * * Save as app/api/irizpro-publish/route.ts → endpoint https://yoursite.com/api/irizpro-publish * Env: IRIZPRO_SITE_ID, IRIZPRO_SIGNING_SECRET (server-only, never NEXT_PUBLIC_). * Implement savePost() at the bottom, then click "Send test" in Irizpro. */ import { createHmac, timingSafeEqual } from 'node:crypto'; import { NextResponse } from 'next/server'; export const runtime = 'nodejs'; const SITE_ID = process.env.IRIZPRO_SITE_ID || ''; const SIGNING_SECRET = process.env.IRIZPRO_SIGNING_SECRET || ''; interface IrizproPost { external_id: string; slug: string; title: string; content_html: string; full_html: string; excerpt: string | null; seo: { meta_title: string; meta_description: string | null; focus_keyword: string | null; keywords: string[] }; featured_image: { url: string; alt: string } | null; author: { name: string } | null; categories: string[]; tags: string[]; faqs: Array<{ question: string; answer: string }>; schema: { article: Record | null; faq: Record | null }; published_at: string; language: string; } const reply = (status: number, body: Record) => NextResponse.json(body, { status }); export async function POST(req: Request) { if (!SITE_ID || !SIGNING_SECRET) return reply(500, { ok: false, error: 'Receiver is not configured' }); // 1. Verify the signature on the RAW body. const raw = await req.text(); const ts = req.headers.get('x-irizpro-timestamp') || ''; const sig = req.headers.get('x-irizpro-signature') || ''; if (!/^\d+$/.test(ts) || Math.abs(Date.now() / 1000 - Number(ts)) > 300) return reply(401, { ok: false, error: 'Stale or missing timestamp' }); const expected = Buffer.from('sha256=' + createHmac('sha256', SIGNING_SECRET).update(`${ts}.${raw}`).digest('hex')); const given = Buffer.from(sig); if (expected.length !== given.length || !timingSafeEqual(expected, given)) return reply(401, { ok: false, error: 'Invalid signature' }); // 2. Parse and validate. let p: { spec?: string; event?: string; test?: boolean; site_id?: string; post?: IrizproPost }; try { p = JSON.parse(raw); } catch { return reply(400, { ok: false, error: 'Body must be JSON' }); } if (p.spec !== 'irizpro.publish.v1') return reply(400, { ok: false, error: 'Unsupported spec' }); if (p.site_id !== SITE_ID) return reply(401, { ok: false, error: 'Wrong site_id' }); for (const f of ['external_id', 'slug', 'title', 'content_html'] as const) { if (!p.post || !String(p.post[f] || '').trim()) return reply(422, { ok: false, error: `post.${f} is required` }); } // 3. Test deliveries: validate only, NEVER save. if (p.event === 'post.test' || p.test) return reply(200, { ok: true, test: true, action: 'validated' }); if (p.event !== 'post.publish') return reply(400, { ok: false, error: 'Unknown event' }); // 4. Save (create, or update when external_id already exists). try { const r = await savePost(p.post!); return reply(200, { ok: true, url: r.url, id: r.id, action: r.action }); } catch (err) { return reply(500, { ok: false, error: `Saving failed: ${err instanceof Error ? err.message : 'unknown error'}` }); } } /** Store the post your way; update when external_id already exists. */ async function savePost(post: IrizproPost): Promise<{ url: string; id: string | number | null; action: 'created' | 'updated' }> { void post; throw new Error('savePost() is not implemented yet'); // Example (Prisma): const row = await prisma.post.upsert({ where: { externalId: post.external_id }, create: {...}, update: {...} }); // return { url: `https://example.com/blog/${post.slug}`, id: row.id, action: ... }; }