mirror of
https://github.com/simstudioai/sim.git
synced 2026-01-09 15:07:55 -05:00
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:
2
.github/workflows/test-build.yml
vendored
2
.github/workflows/test-build.yml
vendored
@@ -16,7 +16,7 @@ jobs:
|
|||||||
- name: Setup Bun
|
- name: Setup Bun
|
||||||
uses: oven-sh/setup-bun@v2
|
uses: oven-sh/setup-bun@v2
|
||||||
with:
|
with:
|
||||||
bun-version: latest
|
bun-version: 1.2.22
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
|
|||||||
@@ -282,11 +282,13 @@ export async function DELETE(
|
|||||||
|
|
||||||
if (!resolvedExternalId) {
|
if (!resolvedExternalId) {
|
||||||
try {
|
try {
|
||||||
const requestOrigin = new URL(request.url).origin
|
if (!env.NEXT_PUBLIC_APP_URL) {
|
||||||
const effectiveOrigin = requestOrigin.includes('localhost')
|
logger.error(
|
||||||
? env.NEXT_PUBLIC_APP_URL || requestOrigin
|
`[${requestId}] NEXT_PUBLIC_APP_URL not configured, cannot match Airtable webhook`
|
||||||
: requestOrigin
|
)
|
||||||
const expectedNotificationUrl = `${effectiveOrigin}/api/webhooks/trigger/${foundWebhook.path}`
|
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 listUrl = `https://api.airtable.com/v0/bases/${baseId}/webhooks`
|
||||||
const listResp = await fetch(listUrl, {
|
const listResp = await fetch(listUrl, {
|
||||||
|
|||||||
@@ -432,25 +432,20 @@ async function createAirtableWebhookSubscription(
|
|||||||
logger.warn(
|
logger.warn(
|
||||||
`[${requestId}] Could not retrieve Airtable access token for user ${userId}. Cannot create webhook in Airtable.`
|
`[${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(
|
throw new Error(
|
||||||
'Airtable account connection required. Please connect your Airtable account in the trigger configuration and try again.'
|
'Airtable account connection required. Please connect your Airtable account in the trigger configuration and try again.'
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const requestOrigin = new URL(request.url).origin
|
if (!env.NEXT_PUBLIC_APP_URL) {
|
||||||
// Ensure origin does not point to localhost for external API calls
|
logger.error(
|
||||||
const effectiveOrigin = requestOrigin.includes('localhost')
|
`[${requestId}] NEXT_PUBLIC_APP_URL not configured, cannot register Airtable webhook`
|
||||||
? 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`
|
|
||||||
)
|
)
|
||||||
|
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 airtableApiUrl = `https://api.airtable.com/v0/bases/${baseId}/webhooks`
|
||||||
|
|
||||||
const specification: any = {
|
const specification: any = {
|
||||||
@@ -549,19 +544,15 @@ async function createTelegramWebhookSubscription(
|
|||||||
return // Cannot proceed without botToken
|
return // Cannot proceed without botToken
|
||||||
}
|
}
|
||||||
|
|
||||||
const requestOrigin = new URL(request.url).origin
|
if (!env.NEXT_PUBLIC_APP_URL) {
|
||||||
// Ensure origin does not point to localhost for external API calls
|
logger.error(
|
||||||
const effectiveOrigin = requestOrigin.includes('localhost')
|
`[${requestId}] NEXT_PUBLIC_APP_URL not configured, cannot register Telegram webhook`
|
||||||
? 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`
|
|
||||||
)
|
)
|
||||||
|
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 telegramApiUrl = `https://api.telegram.org/bot${botToken}/setWebhook`
|
||||||
|
|
||||||
const requestBody: any = {
|
const requestBody: any = {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { db } from '@sim/db'
|
|||||||
import { webhook } from '@sim/db/schema'
|
import { webhook } from '@sim/db/schema'
|
||||||
import { eq } from 'drizzle-orm'
|
import { eq } from 'drizzle-orm'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
|
import { env } from '@/lib/env'
|
||||||
import { createLogger } from '@/lib/logs/console/logger'
|
import { createLogger } from '@/lib/logs/console/logger'
|
||||||
import { generateRequestId } from '@/lib/utils'
|
import { generateRequestId } from '@/lib/utils'
|
||||||
|
|
||||||
@@ -13,7 +14,6 @@ export async function GET(request: NextRequest) {
|
|||||||
const requestId = generateRequestId()
|
const requestId = generateRequestId()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Get the webhook ID and provider from the query parameters
|
|
||||||
const { searchParams } = new URL(request.url)
|
const { searchParams } = new URL(request.url)
|
||||||
const webhookId = searchParams.get('id')
|
const webhookId = searchParams.get('id')
|
||||||
|
|
||||||
@@ -24,7 +24,6 @@ export async function GET(request: NextRequest) {
|
|||||||
|
|
||||||
logger.debug(`[${requestId}] Testing webhook with ID: ${webhookId}`)
|
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)
|
const webhooks = await db.select().from(webhook).where(eq(webhook.id, webhookId)).limit(1)
|
||||||
|
|
||||||
if (webhooks.length === 0) {
|
if (webhooks.length === 0) {
|
||||||
@@ -36,8 +35,14 @@ export async function GET(request: NextRequest) {
|
|||||||
const provider = foundWebhook.provider || 'generic'
|
const provider = foundWebhook.provider || 'generic'
|
||||||
const providerConfig = (foundWebhook.providerConfig as Record<string, any>) || {}
|
const providerConfig = (foundWebhook.providerConfig as Record<string, any>) || {}
|
||||||
|
|
||||||
// Construct the webhook URL
|
if (!env.NEXT_PUBLIC_APP_URL) {
|
||||||
const baseUrl = new URL(request.url).origin
|
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}`
|
const webhookUrl = `${baseUrl}/api/webhooks/trigger/${foundWebhook.path}`
|
||||||
|
|
||||||
logger.info(`[${requestId}] Testing webhook for provider: ${provider}`, {
|
logger.info(`[${requestId}] Testing webhook for provider: ${provider}`, {
|
||||||
@@ -46,7 +51,6 @@ export async function GET(request: NextRequest) {
|
|||||||
isActive: foundWebhook.isActive,
|
isActive: foundWebhook.isActive,
|
||||||
})
|
})
|
||||||
|
|
||||||
// Provider-specific test logic
|
|
||||||
switch (provider) {
|
switch (provider) {
|
||||||
case 'whatsapp': {
|
case 'whatsapp': {
|
||||||
const verificationToken = providerConfig.verificationToken
|
const verificationToken = providerConfig.verificationToken
|
||||||
@@ -59,10 +63,8 @@ export async function GET(request: NextRequest) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate a test challenge
|
|
||||||
const challenge = `test_${Date.now()}`
|
const challenge = `test_${Date.now()}`
|
||||||
|
|
||||||
// Construct the WhatsApp verification URL
|
|
||||||
const whatsappUrl = `${webhookUrl}?hub.mode=subscribe&hub.verify_token=${verificationToken}&hub.challenge=${challenge}`
|
const whatsappUrl = `${webhookUrl}?hub.mode=subscribe&hub.verify_token=${verificationToken}&hub.challenge=${challenge}`
|
||||||
|
|
||||||
logger.debug(`[${requestId}] Testing WhatsApp webhook verification`, {
|
logger.debug(`[${requestId}] Testing WhatsApp webhook verification`, {
|
||||||
@@ -70,19 +72,16 @@ export async function GET(request: NextRequest) {
|
|||||||
challenge,
|
challenge,
|
||||||
})
|
})
|
||||||
|
|
||||||
// Make a request to the webhook endpoint
|
|
||||||
const response = await fetch(whatsappUrl, {
|
const response = await fetch(whatsappUrl, {
|
||||||
headers: {
|
headers: {
|
||||||
'User-Agent': 'facebookplatform/1.0',
|
'User-Agent': 'facebookplatform/1.0',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
// Get the response details
|
|
||||||
const status = response.status
|
const status = response.status
|
||||||
const contentType = response.headers.get('content-type')
|
const contentType = response.headers.get('content-type')
|
||||||
const responseText = await response.text()
|
const responseText = await response.text()
|
||||||
|
|
||||||
// Check if the test was successful
|
|
||||||
const success = status === 200 && responseText === challenge
|
const success = status === 200 && responseText === challenge
|
||||||
|
|
||||||
if (success) {
|
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 = {
|
const testMessage = {
|
||||||
update_id: 12345,
|
update_id: 12345,
|
||||||
message: {
|
message: {
|
||||||
@@ -165,7 +163,6 @@ export async function GET(request: NextRequest) {
|
|||||||
url: webhookUrl,
|
url: webhookUrl,
|
||||||
})
|
})
|
||||||
|
|
||||||
// Make a test request to the webhook endpoint
|
|
||||||
const response = await fetch(webhookUrl, {
|
const response = await fetch(webhookUrl, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
@@ -175,16 +172,12 @@ export async function GET(request: NextRequest) {
|
|||||||
body: JSON.stringify(testMessage),
|
body: JSON.stringify(testMessage),
|
||||||
})
|
})
|
||||||
|
|
||||||
// Get the response details
|
|
||||||
const status = response.status
|
const status = response.status
|
||||||
let responseText = ''
|
let responseText = ''
|
||||||
try {
|
try {
|
||||||
responseText = await response.text()
|
responseText = await response.text()
|
||||||
} catch (_e) {
|
} catch (_e) {}
|
||||||
// Ignore if we can't get response text
|
|
||||||
}
|
|
||||||
|
|
||||||
// Consider success if we get a 2xx response
|
|
||||||
const success = status >= 200 && status < 300
|
const success = status >= 200 && status < 300
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
@@ -196,7 +189,6 @@ export async function GET(request: NextRequest) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get webhook info from Telegram API
|
|
||||||
let webhookInfo = null
|
let webhookInfo = null
|
||||||
try {
|
try {
|
||||||
const webhookInfoUrl = `https://api.telegram.org/bot${botToken}/getWebhookInfo`
|
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)
|
logger.warn(`[${requestId}] Failed to get Telegram webhook info`, e)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Format the curl command for testing
|
|
||||||
const curlCommand = [
|
const curlCommand = [
|
||||||
`curl -X POST "${webhookUrl}"`,
|
`curl -X POST "${webhookUrl}"`,
|
||||||
`-H "Content-Type: application/json"`,
|
`-H "Content-Type: application/json"`,
|
||||||
@@ -288,16 +279,13 @@ export async function GET(request: NextRequest) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
case 'generic': {
|
case 'generic': {
|
||||||
// Get the general webhook configuration
|
|
||||||
const token = providerConfig.token
|
const token = providerConfig.token
|
||||||
const secretHeaderName = providerConfig.secretHeaderName
|
const secretHeaderName = providerConfig.secretHeaderName
|
||||||
const requireAuth = providerConfig.requireAuth
|
const requireAuth = providerConfig.requireAuth
|
||||||
const allowedIps = providerConfig.allowedIps
|
const allowedIps = providerConfig.allowedIps
|
||||||
|
|
||||||
// Generate sample curl command for testing
|
|
||||||
let curlCommand = `curl -X POST "${webhookUrl}" -H "Content-Type: application/json"`
|
let curlCommand = `curl -X POST "${webhookUrl}" -H "Content-Type: application/json"`
|
||||||
|
|
||||||
// Add auth headers to the curl command if required
|
|
||||||
if (requireAuth && token) {
|
if (requireAuth && token) {
|
||||||
if (secretHeaderName) {
|
if (secretHeaderName) {
|
||||||
curlCommand += ` -H "${secretHeaderName}: ${token}"`
|
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()}"}'`
|
curlCommand += ` -d '{"event":"test_event","timestamp":"${new Date().toISOString()}"}'`
|
||||||
|
|
||||||
logger.info(`[${requestId}] General webhook test successful: ${webhookId}`)
|
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': {
|
case 'airtable': {
|
||||||
const baseId = providerConfig.baseId
|
const baseId = providerConfig.baseId
|
||||||
const tableId = providerConfig.tableId
|
const tableId = providerConfig.tableId
|
||||||
@@ -408,7 +394,6 @@ export async function GET(request: NextRequest) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Define a sample payload structure
|
|
||||||
const samplePayload = {
|
const samplePayload = {
|
||||||
webhook: {
|
webhook: {
|
||||||
id: 'whiYOUR_WEBHOOK_ID',
|
id: 'whiYOUR_WEBHOOK_ID',
|
||||||
@@ -418,16 +403,15 @@ export async function GET(request: NextRequest) {
|
|||||||
},
|
},
|
||||||
payloadFormat: 'v0',
|
payloadFormat: 'v0',
|
||||||
actionMetadata: {
|
actionMetadata: {
|
||||||
source: 'tableOrViewChange', // Example source
|
source: 'tableOrViewChange',
|
||||||
sourceMetadata: {},
|
sourceMetadata: {},
|
||||||
},
|
},
|
||||||
payloads: [
|
payloads: [
|
||||||
{
|
{
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
baseTransactionNumber: Date.now(), // Example transaction number
|
baseTransactionNumber: Date.now(),
|
||||||
changedTablesById: {
|
changedTablesById: {
|
||||||
[tableId]: {
|
[tableId]: {
|
||||||
// Example changes - structure may vary based on actual event
|
|
||||||
changedRecordsById: {
|
changedRecordsById: {
|
||||||
recSAMPLEID1: {
|
recSAMPLEID1: {
|
||||||
current: { cellValuesByFieldId: { fldSAMPLEID: 'New Value' } },
|
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"`
|
let curlCommand = `curl -X POST "${webhookUrl}" -H "Content-Type: application/json"`
|
||||||
curlCommand += ` -d '${JSON.stringify(samplePayload, null, 2)}'`
|
curlCommand += ` -d '${JSON.stringify(samplePayload, null, 2)}'`
|
||||||
|
|
||||||
@@ -519,7 +502,6 @@ export async function GET(request: NextRequest) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
default: {
|
default: {
|
||||||
// Generic webhook test
|
|
||||||
logger.info(`[${requestId}] Generic webhook test successful: ${webhookId}`)
|
logger.info(`[${requestId}] Generic webhook test successful: ${webhookId}`)
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
success: true,
|
success: true,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# ========================================
|
# ========================================
|
||||||
# Base Stage: Alpine Linux with Bun
|
# 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
|
# Dependencies Stage: Install Dependencies
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# ========================================
|
# ========================================
|
||||||
# Dependencies Stage: Install Dependencies
|
# Dependencies Stage: Install Dependencies
|
||||||
# ========================================
|
# ========================================
|
||||||
FROM oven/bun:1.2.21-alpine AS deps
|
FROM oven/bun:1.2.22-alpine AS deps
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Copy only package files needed for migrations
|
# Copy only package files needed for migrations
|
||||||
@@ -14,7 +14,7 @@ RUN bun install --ignore-scripts
|
|||||||
# ========================================
|
# ========================================
|
||||||
# Runner Stage: Production Environment
|
# Runner Stage: Production Environment
|
||||||
# ========================================
|
# ========================================
|
||||||
FROM oven/bun:1.2.21-alpine AS runner
|
FROM oven/bun:1.2.22-alpine AS runner
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Copy only the necessary files from deps
|
# Copy only the necessary files from deps
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# ========================================
|
# ========================================
|
||||||
# Base Stage: Alpine Linux with Bun
|
# 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
|
# Dependencies Stage: Install Dependencies
|
||||||
|
|||||||
Reference in New Issue
Block a user