fix(webhooks): use next public app url instead of request origin for webhook registration (#1596)

* fix(webhooks): use next public app url instead of request origin for webhook registration

* ack PR comments

* ci: pin Bun to v1.2.22 to avoid Bun 1.3 breaking changes
This commit is contained in:
Waleed
2025-10-10 17:10:20 -07:00
committed by waleed
parent 241d9fd12d
commit 923595f57e
7 changed files with 36 additions and 61 deletions

View File

@@ -16,7 +16,7 @@ jobs:
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
bun-version: 1.2.22
- name: Setup Node
uses: actions/setup-node@v4

View File

@@ -282,11 +282,13 @@ export async function DELETE(
if (!resolvedExternalId) {
try {
const requestOrigin = new URL(request.url).origin
const effectiveOrigin = requestOrigin.includes('localhost')
? env.NEXT_PUBLIC_APP_URL || requestOrigin
: requestOrigin
const expectedNotificationUrl = `${effectiveOrigin}/api/webhooks/trigger/${foundWebhook.path}`
if (!env.NEXT_PUBLIC_APP_URL) {
logger.error(
`[${requestId}] NEXT_PUBLIC_APP_URL not configured, cannot match Airtable webhook`
)
throw new Error('NEXT_PUBLIC_APP_URL must be configured')
}
const expectedNotificationUrl = `${env.NEXT_PUBLIC_APP_URL}/api/webhooks/trigger/${foundWebhook.path}`
const listUrl = `https://api.airtable.com/v0/bases/${baseId}/webhooks`
const listResp = await fetch(listUrl, {

View File

@@ -432,25 +432,20 @@ async function createAirtableWebhookSubscription(
logger.warn(
`[${requestId}] Could not retrieve Airtable access token for user ${userId}. Cannot create webhook in Airtable.`
)
// Instead of silently returning, throw an error with clear user guidance
throw new Error(
'Airtable account connection required. Please connect your Airtable account in the trigger configuration and try again.'
)
}
const requestOrigin = new URL(request.url).origin
// Ensure origin does not point to localhost for external API calls
const effectiveOrigin = requestOrigin.includes('localhost')
? env.NEXT_PUBLIC_APP_URL || requestOrigin // Use env var if available, fallback to original
: requestOrigin
const notificationUrl = `${effectiveOrigin}/api/webhooks/trigger/${path}`
if (effectiveOrigin !== requestOrigin) {
logger.debug(
`[${requestId}] Remapped localhost origin to ${effectiveOrigin} for notificationUrl`
if (!env.NEXT_PUBLIC_APP_URL) {
logger.error(
`[${requestId}] NEXT_PUBLIC_APP_URL not configured, cannot register Airtable webhook`
)
throw new Error('NEXT_PUBLIC_APP_URL must be configured for Airtable webhook registration')
}
const notificationUrl = `${env.NEXT_PUBLIC_APP_URL}/api/webhooks/trigger/${path}`
const airtableApiUrl = `https://api.airtable.com/v0/bases/${baseId}/webhooks`
const specification: any = {
@@ -549,19 +544,15 @@ async function createTelegramWebhookSubscription(
return // Cannot proceed without botToken
}
const requestOrigin = new URL(request.url).origin
// Ensure origin does not point to localhost for external API calls
const effectiveOrigin = requestOrigin.includes('localhost')
? env.NEXT_PUBLIC_APP_URL || requestOrigin // Use env var if available, fallback to original
: requestOrigin
const notificationUrl = `${effectiveOrigin}/api/webhooks/trigger/${path}`
if (effectiveOrigin !== requestOrigin) {
logger.debug(
`[${requestId}] Remapped localhost origin to ${effectiveOrigin} for notificationUrl`
if (!env.NEXT_PUBLIC_APP_URL) {
logger.error(
`[${requestId}] NEXT_PUBLIC_APP_URL not configured, cannot register Telegram webhook`
)
throw new Error('NEXT_PUBLIC_APP_URL must be configured for Telegram webhook registration')
}
const notificationUrl = `${env.NEXT_PUBLIC_APP_URL}/api/webhooks/trigger/${path}`
const telegramApiUrl = `https://api.telegram.org/bot${botToken}/setWebhook`
const requestBody: any = {

View File

@@ -2,6 +2,7 @@ import { db } from '@sim/db'
import { webhook } from '@sim/db/schema'
import { eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { env } from '@/lib/env'
import { createLogger } from '@/lib/logs/console/logger'
import { generateRequestId } from '@/lib/utils'
@@ -13,7 +14,6 @@ export async function GET(request: NextRequest) {
const requestId = generateRequestId()
try {
// Get the webhook ID and provider from the query parameters
const { searchParams } = new URL(request.url)
const webhookId = searchParams.get('id')
@@ -24,7 +24,6 @@ export async function GET(request: NextRequest) {
logger.debug(`[${requestId}] Testing webhook with ID: ${webhookId}`)
// Find the webhook in the database
const webhooks = await db.select().from(webhook).where(eq(webhook.id, webhookId)).limit(1)
if (webhooks.length === 0) {
@@ -36,8 +35,14 @@ export async function GET(request: NextRequest) {
const provider = foundWebhook.provider || 'generic'
const providerConfig = (foundWebhook.providerConfig as Record<string, any>) || {}
// Construct the webhook URL
const baseUrl = new URL(request.url).origin
if (!env.NEXT_PUBLIC_APP_URL) {
logger.error(`[${requestId}] NEXT_PUBLIC_APP_URL not configured, cannot test webhook`)
return NextResponse.json(
{ success: false, error: 'NEXT_PUBLIC_APP_URL must be configured' },
{ status: 500 }
)
}
const baseUrl = env.NEXT_PUBLIC_APP_URL
const webhookUrl = `${baseUrl}/api/webhooks/trigger/${foundWebhook.path}`
logger.info(`[${requestId}] Testing webhook for provider: ${provider}`, {
@@ -46,7 +51,6 @@ export async function GET(request: NextRequest) {
isActive: foundWebhook.isActive,
})
// Provider-specific test logic
switch (provider) {
case 'whatsapp': {
const verificationToken = providerConfig.verificationToken
@@ -59,10 +63,8 @@ export async function GET(request: NextRequest) {
)
}
// Generate a test challenge
const challenge = `test_${Date.now()}`
// Construct the WhatsApp verification URL
const whatsappUrl = `${webhookUrl}?hub.mode=subscribe&hub.verify_token=${verificationToken}&hub.challenge=${challenge}`
logger.debug(`[${requestId}] Testing WhatsApp webhook verification`, {
@@ -70,19 +72,16 @@ export async function GET(request: NextRequest) {
challenge,
})
// Make a request to the webhook endpoint
const response = await fetch(whatsappUrl, {
headers: {
'User-Agent': 'facebookplatform/1.0',
},
})
// Get the response details
const status = response.status
const contentType = response.headers.get('content-type')
const responseText = await response.text()
// Check if the test was successful
const success = status === 200 && responseText === challenge
if (success) {
@@ -139,7 +138,6 @@ export async function GET(request: NextRequest) {
)
}
// Test the webhook endpoint with a simple message to check if it's reachable
const testMessage = {
update_id: 12345,
message: {
@@ -165,7 +163,6 @@ export async function GET(request: NextRequest) {
url: webhookUrl,
})
// Make a test request to the webhook endpoint
const response = await fetch(webhookUrl, {
method: 'POST',
headers: {
@@ -175,16 +172,12 @@ export async function GET(request: NextRequest) {
body: JSON.stringify(testMessage),
})
// Get the response details
const status = response.status
let responseText = ''
try {
responseText = await response.text()
} catch (_e) {
// Ignore if we can't get response text
}
} catch (_e) {}
// Consider success if we get a 2xx response
const success = status >= 200 && status < 300
if (success) {
@@ -196,7 +189,6 @@ export async function GET(request: NextRequest) {
})
}
// Get webhook info from Telegram API
let webhookInfo = null
try {
const webhookInfoUrl = `https://api.telegram.org/bot${botToken}/getWebhookInfo`
@@ -215,7 +207,6 @@ export async function GET(request: NextRequest) {
logger.warn(`[${requestId}] Failed to get Telegram webhook info`, e)
}
// Format the curl command for testing
const curlCommand = [
`curl -X POST "${webhookUrl}"`,
`-H "Content-Type: application/json"`,
@@ -288,16 +279,13 @@ export async function GET(request: NextRequest) {
}
case 'generic': {
// Get the general webhook configuration
const token = providerConfig.token
const secretHeaderName = providerConfig.secretHeaderName
const requireAuth = providerConfig.requireAuth
const allowedIps = providerConfig.allowedIps
// Generate sample curl command for testing
let curlCommand = `curl -X POST "${webhookUrl}" -H "Content-Type: application/json"`
// Add auth headers to the curl command if required
if (requireAuth && token) {
if (secretHeaderName) {
curlCommand += ` -H "${secretHeaderName}: ${token}"`
@@ -306,7 +294,6 @@ export async function GET(request: NextRequest) {
}
}
// Add a sample payload
curlCommand += ` -d '{"event":"test_event","timestamp":"${new Date().toISOString()}"}'`
logger.info(`[${requestId}] General webhook test successful: ${webhookId}`)
@@ -391,7 +378,6 @@ export async function GET(request: NextRequest) {
})
}
// Add the Airtable test case
case 'airtable': {
const baseId = providerConfig.baseId
const tableId = providerConfig.tableId
@@ -408,7 +394,6 @@ export async function GET(request: NextRequest) {
)
}
// Define a sample payload structure
const samplePayload = {
webhook: {
id: 'whiYOUR_WEBHOOK_ID',
@@ -418,16 +403,15 @@ export async function GET(request: NextRequest) {
},
payloadFormat: 'v0',
actionMetadata: {
source: 'tableOrViewChange', // Example source
source: 'tableOrViewChange',
sourceMetadata: {},
},
payloads: [
{
timestamp: new Date().toISOString(),
baseTransactionNumber: Date.now(), // Example transaction number
baseTransactionNumber: Date.now(),
changedTablesById: {
[tableId]: {
// Example changes - structure may vary based on actual event
changedRecordsById: {
recSAMPLEID1: {
current: { cellValuesByFieldId: { fldSAMPLEID: 'New Value' } },
@@ -442,7 +426,6 @@ export async function GET(request: NextRequest) {
],
}
// Generate sample curl command
let curlCommand = `curl -X POST "${webhookUrl}" -H "Content-Type: application/json"`
curlCommand += ` -d '${JSON.stringify(samplePayload, null, 2)}'`
@@ -519,7 +502,6 @@ export async function GET(request: NextRequest) {
}
default: {
// Generic webhook test
logger.info(`[${requestId}] Generic webhook test successful: ${webhookId}`)
return NextResponse.json({
success: true,

View File

@@ -1,7 +1,7 @@
# ========================================
# Base Stage: Alpine Linux with Bun
# ========================================
FROM oven/bun:alpine AS base
FROM oven/bun:1.2.22-alpine AS base
# ========================================
# Dependencies Stage: Install Dependencies

View File

@@ -1,7 +1,7 @@
# ========================================
# Dependencies Stage: Install Dependencies
# ========================================
FROM oven/bun:1.2.21-alpine AS deps
FROM oven/bun:1.2.22-alpine AS deps
WORKDIR /app
# Copy only package files needed for migrations
@@ -14,7 +14,7 @@ RUN bun install --ignore-scripts
# ========================================
# Runner Stage: Production Environment
# ========================================
FROM oven/bun:1.2.21-alpine AS runner
FROM oven/bun:1.2.22-alpine AS runner
WORKDIR /app
# Copy only the necessary files from deps

View File

@@ -1,7 +1,7 @@
# ========================================
# Base Stage: Alpine Linux with Bun
# ========================================
FROM oven/bun:alpine AS base
FROM oven/bun:1.2.22-alpine AS base
# ========================================
# Dependencies Stage: Install Dependencies