chore: import upstream snapshot with attribution
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,164 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { sha256Hex } from '@sim/security/hash'
|
||||
import { toError } from '@sim/utils/errors'
|
||||
import * as jose from 'jose'
|
||||
import { NextResponse } from 'next/server'
|
||||
import type {
|
||||
AuthContext,
|
||||
FormatInputContext,
|
||||
FormatInputResult,
|
||||
WebhookProviderHandler,
|
||||
} from '@/lib/webhooks/providers/types'
|
||||
|
||||
const logger = createLogger('WebhookProvider:Gong')
|
||||
|
||||
/** providerConfig key: PEM or raw base64 RSA public key from Gong (Signed JWT header auth). */
|
||||
export const GONG_JWT_PUBLIC_KEY_CONFIG_KEY = 'gongJwtPublicKeyPem'
|
||||
|
||||
/**
|
||||
* Gong automation webhooks support either URL secrecy (token in path) or a signed JWT in
|
||||
* `Authorization` (see https://help.gong.io/docs/create-a-webhook-rule).
|
||||
* When {@link GONG_JWT_PUBLIC_KEY_CONFIG_KEY} is set, we verify RS256 per Gong's JWT guide.
|
||||
* When unset, only the unguessable Sim webhook path authenticates the request (same as before).
|
||||
*/
|
||||
export function normalizeGongPublicKeyPem(input: string): string | null {
|
||||
const trimmed = input.trim()
|
||||
if (!trimmed) return null
|
||||
if (trimmed.includes('BEGIN PUBLIC KEY')) {
|
||||
return trimmed
|
||||
}
|
||||
const b64 = trimmed.replace(/\s/g, '')
|
||||
if (!/^[A-Za-z0-9+/]+=*$/.test(b64)) {
|
||||
return null
|
||||
}
|
||||
const chunked = b64.match(/.{1,64}/g)?.join('\n') ?? b64
|
||||
return `-----BEGIN PUBLIC KEY-----\n${chunked}\n-----END PUBLIC KEY-----`
|
||||
}
|
||||
|
||||
function normalizeUrlForGongJwtClaim(url: string): string {
|
||||
try {
|
||||
const u = new URL(url)
|
||||
let path = u.pathname
|
||||
if (path.length > 1 && path.endsWith('/')) {
|
||||
path = path.slice(0, -1)
|
||||
}
|
||||
return `${u.protocol}//${u.host.toLowerCase()}${path}`
|
||||
} catch {
|
||||
return url.trim()
|
||||
}
|
||||
}
|
||||
|
||||
function parseAuthorizationJwt(authHeader: string | null): string | null {
|
||||
if (!authHeader) return null
|
||||
const trimmed = authHeader.trim()
|
||||
if (trimmed.toLowerCase().startsWith('bearer ')) {
|
||||
return trimmed.slice(7).trim() || null
|
||||
}
|
||||
return trimmed || null
|
||||
}
|
||||
|
||||
export async function verifyGongJwtAuth(ctx: AuthContext): Promise<NextResponse | null> {
|
||||
const { request, rawBody, requestId, providerConfig } = ctx
|
||||
const rawKey = providerConfig[GONG_JWT_PUBLIC_KEY_CONFIG_KEY]
|
||||
if (typeof rawKey !== 'string') {
|
||||
return null
|
||||
}
|
||||
|
||||
const pem = normalizeGongPublicKeyPem(rawKey)
|
||||
if (!pem) {
|
||||
logger.warn(`[${requestId}] Gong JWT public key configured but could not be normalized`)
|
||||
return new NextResponse('Unauthorized - Invalid Gong JWT public key configuration', {
|
||||
status: 401,
|
||||
})
|
||||
}
|
||||
|
||||
const token = parseAuthorizationJwt(request.headers.get('authorization'))
|
||||
if (!token) {
|
||||
logger.warn(`[${requestId}] Gong JWT verification enabled but Authorization header missing`)
|
||||
return new NextResponse('Unauthorized - Missing Gong JWT', { status: 401 })
|
||||
}
|
||||
|
||||
let payload: jose.JWTPayload
|
||||
try {
|
||||
const key = await jose.importSPKI(pem, 'RS256')
|
||||
const verified = await jose.jwtVerify(token, key, { algorithms: ['RS256'] })
|
||||
payload = verified.payload
|
||||
} catch (error) {
|
||||
logger.warn(`[${requestId}] Gong JWT verification failed`, {
|
||||
message: toError(error).message,
|
||||
})
|
||||
return new NextResponse('Unauthorized - Invalid Gong JWT', { status: 401 })
|
||||
}
|
||||
|
||||
const claimUrl = payload.webhook_url
|
||||
if (typeof claimUrl !== 'string' || !claimUrl) {
|
||||
logger.warn(`[${requestId}] Gong JWT missing webhook_url claim`)
|
||||
return new NextResponse('Unauthorized - Invalid Gong JWT claims', { status: 401 })
|
||||
}
|
||||
|
||||
const claimDigest = payload.body_sha256
|
||||
if (typeof claimDigest !== 'string' || !claimDigest) {
|
||||
logger.warn(`[${requestId}] Gong JWT missing body_sha256 claim`)
|
||||
return new NextResponse('Unauthorized - Invalid Gong JWT claims', { status: 401 })
|
||||
}
|
||||
|
||||
const expectedDigest = sha256Hex(rawBody)
|
||||
if (claimDigest !== expectedDigest) {
|
||||
logger.warn(`[${requestId}] Gong JWT body_sha256 mismatch`)
|
||||
return new NextResponse('Unauthorized - Gong JWT body mismatch', { status: 401 })
|
||||
}
|
||||
|
||||
const receivedNorm = normalizeUrlForGongJwtClaim(request.url)
|
||||
const claimNorm = normalizeUrlForGongJwtClaim(claimUrl)
|
||||
if (receivedNorm !== claimNorm) {
|
||||
logger.warn(`[${requestId}] Gong JWT webhook_url mismatch`, {
|
||||
receivedNorm,
|
||||
claimNorm,
|
||||
})
|
||||
return new NextResponse('Unauthorized - Gong JWT URL mismatch', { status: 401 })
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export const gongHandler: WebhookProviderHandler = {
|
||||
verifyAuth: verifyGongJwtAuth,
|
||||
|
||||
extractIdempotencyId(body: unknown): string | null {
|
||||
const obj = body as Record<string, unknown>
|
||||
const callData = obj.callData as Record<string, unknown> | undefined
|
||||
const metaData = callData?.metaData as Record<string, unknown> | undefined
|
||||
const id = metaData?.id
|
||||
if (typeof id === 'string' && id) {
|
||||
return `gong:${id}`
|
||||
}
|
||||
if (typeof id === 'number') {
|
||||
return `gong:${id}`
|
||||
}
|
||||
return null
|
||||
},
|
||||
|
||||
async formatInput({ body }: FormatInputContext): Promise<FormatInputResult> {
|
||||
const b = body as Record<string, unknown>
|
||||
const callData = b.callData as Record<string, unknown> | undefined
|
||||
const metaData = (callData?.metaData as Record<string, unknown>) || {}
|
||||
const content = callData?.content as Record<string, unknown> | undefined
|
||||
const callId =
|
||||
typeof metaData.id === 'string' || typeof metaData.id === 'number' ? String(metaData.id) : ''
|
||||
|
||||
return {
|
||||
input: {
|
||||
isTest: b.isTest ?? false,
|
||||
callData,
|
||||
metaData,
|
||||
parties: (callData?.parties as unknown[]) || [],
|
||||
context: (callData?.context as unknown[]) || [],
|
||||
trackers: (content?.trackers as unknown[]) || [],
|
||||
topics: (content?.topics as unknown[]) || [],
|
||||
highlights: (content?.highlights as unknown[]) || [],
|
||||
eventType: 'gong.automation_rule',
|
||||
callId,
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
Reference in New Issue
Block a user