113 lines
3.4 KiB
TypeScript
113 lines
3.4 KiB
TypeScript
import { NextRequest, NextResponse } from "next/server";
|
|
import { z } from "zod";
|
|
import { admin, required } from "@/lib/supabase";
|
|
import { verifySupportWebhook } from "@/lib/support-signature";
|
|
import { supportCommand } from "@/shared/bug-reports";
|
|
import { reconcileBugStatus, scheduleBugCleanup } from "@/lib/bug-lifecycle";
|
|
|
|
export const runtime = "nodejs";
|
|
export async function POST(request: NextRequest) {
|
|
const secret = process.env.LINEAR_WEBHOOK_SECRET;
|
|
if (!secret)
|
|
return NextResponse.json(
|
|
{ error: "Support integration is not configured" },
|
|
{ status: 503 },
|
|
);
|
|
const reader = request.body?.getReader();
|
|
if (!reader) return new NextResponse(null, { status: 400 });
|
|
const chunks: Uint8Array[] = [];
|
|
let size = 0;
|
|
while (true) {
|
|
const { done, value } = await reader.read();
|
|
if (done) break;
|
|
size += value.length;
|
|
if (size > 256 * 1024) {
|
|
await reader.cancel();
|
|
return new NextResponse(null, { status: 413 });
|
|
}
|
|
chunks.push(value);
|
|
}
|
|
const raw = Buffer.concat(chunks).toString("utf8");
|
|
if (
|
|
!verifySupportWebhook(raw, request.headers.get("linear-signature"), secret)
|
|
)
|
|
return new NextResponse(null, { status: 401 });
|
|
let decoded: unknown;
|
|
try {
|
|
decoded = JSON.parse(raw);
|
|
} catch {
|
|
return new NextResponse(null, { status: 400 });
|
|
}
|
|
const payload = z
|
|
.object({
|
|
action: z.string(),
|
|
type: z.string(),
|
|
actor: z
|
|
.object({
|
|
id: z.string(),
|
|
type: z.string(),
|
|
name: z.string().optional(),
|
|
})
|
|
.nullable()
|
|
.optional(),
|
|
data: z.object({
|
|
id: z.string().uuid(),
|
|
issueId: z.string().uuid().optional(),
|
|
body: z.string().optional(),
|
|
userId: z.string().optional(),
|
|
}),
|
|
})
|
|
.safeParse(decoded);
|
|
if (!payload.success) return new NextResponse(null, { status: 400 });
|
|
const event = payload.data;
|
|
if (event.type === "Issue" && ["create", "update"].includes(event.action)) {
|
|
try {
|
|
await reconcileBugStatus(event.data.id, true);
|
|
return NextResponse.json({ ok: true });
|
|
} catch {
|
|
return NextResponse.json(
|
|
{ error: "Support status delivery failed" },
|
|
{ status: 503 },
|
|
);
|
|
}
|
|
}
|
|
// Only explicit new staff comments are public. Internal comments and user echoes stay private.
|
|
if (
|
|
event.action !== "create" ||
|
|
event.type !== "Comment" ||
|
|
event.actor?.type.toLowerCase() !== "user" ||
|
|
event.data.userId !== event.actor.id ||
|
|
!event.data.issueId
|
|
)
|
|
return NextResponse.json({ ok: true });
|
|
const command = supportCommand(event.data.body ?? "");
|
|
if (!command) return NextResponse.json({ ok: true });
|
|
try {
|
|
if (command.kind === "close") {
|
|
required(
|
|
await admin().rpc("receive_bug_close", {
|
|
p_report: event.data.issueId,
|
|
p_external: event.data.id,
|
|
}),
|
|
);
|
|
await scheduleBugCleanup(event.data.issueId);
|
|
return NextResponse.json({ ok: true });
|
|
}
|
|
required(
|
|
await admin().rpc("receive_bug_reply", {
|
|
p_report: event.data.issueId,
|
|
p_external: event.data.id,
|
|
p_author: event.actor.name ?? "Support",
|
|
p_body: command.kind === "reply" ? command.text : "",
|
|
p_glow: command.kind === "glow" ? command.enabled : null,
|
|
}),
|
|
);
|
|
return NextResponse.json({ ok: true });
|
|
} catch {
|
|
return NextResponse.json(
|
|
{ error: "Support delivery failed" },
|
|
{ status: 503 },
|
|
);
|
|
}
|
|
}
|