1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
|
import { NextResponse } from "next/server"
import { createHmac } from "crypto"
import { createSupabaseServerClient } from "@/lib/supabase/server"
import { TIER_LIMITS, type SubscriptionTier } from "@asa-news/shared"
import { rateLimit } from "@/lib/rate-limit"
import { validateWebhookUrl } from "@/lib/validate-webhook-url"
import { checkBotId } from "botid/server"
export async function POST() {
const botVerification = await checkBotId()
if (botVerification.isBot) {
return NextResponse.json({ error: "access denied" }, { status: 403 })
}
const supabaseClient = await createSupabaseServerClient()
const {
data: { user },
} = await supabaseClient.auth.getUser()
if (!user) {
return NextResponse.json({ error: "not authenticated" }, { status: 401 })
}
const rateLimitResult = await rateLimit(`webhook-test:${user.id}`, 5, 60_000)
if (!rateLimitResult.success) {
return NextResponse.json({ error: "too many requests" }, { status: 429 })
}
const { data: profile } = await supabaseClient
.from("user_profiles")
.select(
"tier, webhook_url, webhook_secret, webhook_enabled"
)
.eq("id", user.id)
.single()
if (
!profile ||
!TIER_LIMITS[profile.tier as SubscriptionTier]?.allowsWebhooks
) {
return NextResponse.json(
{ error: "webhooks require the developer plan" },
{ status: 403 }
)
}
if (!profile.webhook_url) {
return NextResponse.json(
{ error: "no webhook url configured" },
{ status: 400 }
)
}
const validationResult = await validateWebhookUrl(profile.webhook_url)
if (!validationResult.valid) {
return NextResponse.json(
{ error: validationResult.error },
{ status: 400 }
)
}
const testPayload = {
event: "test",
timestamp: new Date().toISOString(),
entries: [
{
entryIdentifier: "test-entry-000",
feedIdentifier: "test-feed-000",
title: "test webhook delivery",
url: "https://asa.news",
author: "asa.news",
summary: "This is a test webhook payload to verify your endpoint.",
publishedAt: new Date().toISOString(),
},
],
}
const payloadString = JSON.stringify(testPayload)
const headers: Record<string, string> = {
"Content-Type": "application/json",
"User-Agent": "asa.news Webhook/1.0",
}
if (profile.webhook_secret) {
const signature = createHmac("sha256", profile.webhook_secret)
.update(payloadString)
.digest("hex")
headers["X-Asa-Signature-256"] = `sha256=${signature}`
}
try {
const response = await fetch(profile.webhook_url, {
method: "POST",
headers,
body: payloadString,
signal: AbortSignal.timeout(10_000),
})
return NextResponse.json({
delivered: true,
statusCode: response.status,
})
} catch (deliveryError) {
const errorMessage =
deliveryError instanceof Error
? deliveryError.message
: "Unknown error"
return NextResponse.json({
delivered: false,
error: errorMessage,
})
}
}
|