d25d482dc2
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
105 lines
4.2 KiB
TypeScript
105 lines
4.2 KiB
TypeScript
import { createLogger } from '@sim/logger'
|
|
import { isRecordLike } from '@sim/utils/object'
|
|
import { getProviderConfig } from '@/lib/webhooks/provider-subscription-utils'
|
|
import type {
|
|
EventMatchContext,
|
|
FormatInputContext,
|
|
FormatInputResult,
|
|
WebhookProviderHandler,
|
|
} from '@/lib/webhooks/providers/types'
|
|
|
|
const logger = createLogger('WebhookProvider:Sendblue')
|
|
|
|
/**
|
|
* Maps Sendblue trigger IDs to the expected value of the webhook payload's
|
|
* `is_outbound` flag, used to route inbound vs. outbound status events.
|
|
* The handler is the only runtime consumer, so the map lives here (single
|
|
* source of truth) rather than crossing from the triggers graph into the
|
|
* webhook-providers graph.
|
|
*/
|
|
const SENDBLUE_TRIGGER_IS_OUTBOUND: Record<string, boolean> = {
|
|
sendblue_message_received: false,
|
|
sendblue_message_status_updated: true,
|
|
}
|
|
|
|
/**
|
|
* Sendblue webhook handler.
|
|
*
|
|
* No `verifyAuth` is implemented: Sendblue supports an optional per-webhook
|
|
* `secret`/`globalSecret` that it "includes in the webhook request headers,"
|
|
* but the official docs never name the header or specify whether the value is
|
|
* a plain token echo or an HMAC signature. Implementing verification today
|
|
* would require guessing the header name, so it is deferred. When Sendblue
|
|
* documents the scheme, wire `verifyTokenAuth` (plain token) or
|
|
* `createHmacVerifier` (HMAC) from `@/lib/webhooks/providers/utils` and add a
|
|
* secret sub-block to the block definition.
|
|
*/
|
|
export const sendblueHandler: WebhookProviderHandler = {
|
|
matchEvent({ body, webhook, requestId }: EventMatchContext): boolean {
|
|
const providerConfig = getProviderConfig(webhook)
|
|
const triggerId = providerConfig.triggerId as string | undefined
|
|
if (!triggerId || !(triggerId in SENDBLUE_TRIGGER_IS_OUTBOUND)) return true
|
|
|
|
if (!isRecordLike(body)) {
|
|
logger.warn(`[${requestId}] Sendblue webhook payload was not an object`)
|
|
return false
|
|
}
|
|
|
|
const expected = SENDBLUE_TRIGGER_IS_OUTBOUND[triggerId]
|
|
const isOutbound = body.is_outbound === true
|
|
if (isOutbound !== expected) {
|
|
logger.info(`[${requestId}] Sendblue event did not match trigger`, { triggerId, isOutbound })
|
|
return false
|
|
}
|
|
|
|
return true
|
|
},
|
|
|
|
extractIdempotencyId(body: unknown): string | null {
|
|
if (!isRecordLike(body)) return null
|
|
const handle = body.message_handle
|
|
if (typeof handle !== 'string' || handle.length === 0) return null
|
|
// A single outbound message emits multiple status callbacks (e.g. SENT then
|
|
// DELIVERED) that share one message_handle, so the status is part of the key
|
|
// to keep distinct transitions from being deduped as retries.
|
|
const status = typeof body.status === 'string' && body.status.length > 0 ? body.status : null
|
|
return status ? `${handle}:${status}` : handle
|
|
},
|
|
|
|
async formatInput({ body }: FormatInputContext): Promise<FormatInputResult> {
|
|
const b = isRecordLike(body) ? body : {}
|
|
return {
|
|
input: {
|
|
account_email: b.accountEmail ?? b.account_email ?? null,
|
|
content: b.content ?? null,
|
|
media_url: (typeof b.media_url === 'string' && b.media_url) || null,
|
|
is_outbound: b.is_outbound ?? null,
|
|
status: b.status ?? null,
|
|
error_code: b.error_code ?? null,
|
|
error_message: b.error_message ?? null,
|
|
error_reason: b.error_reason ?? null,
|
|
error_detail: b.error_detail ?? null,
|
|
message_handle: b.message_handle ?? null,
|
|
date_sent: b.date_sent ?? null,
|
|
date_updated: b.date_updated ?? null,
|
|
from_number: b.from_number ?? null,
|
|
number: b.number ?? null,
|
|
to_number: b.to_number ?? null,
|
|
was_downgraded: b.was_downgraded ?? null,
|
|
plan: b.plan ?? null,
|
|
message_type: b.message_type ?? null,
|
|
group_id: (typeof b.group_id === 'string' && b.group_id) || null,
|
|
participants: b.participants ?? [],
|
|
send_style: b.send_style ?? null,
|
|
opted_out: b.opted_out ?? null,
|
|
sendblue_number: b.sendblue_number ?? null,
|
|
service: b.service ?? null,
|
|
group_display_name: b.group_display_name ?? null,
|
|
sender_email: b.sender_email ?? null,
|
|
seat_id: b.seat_id ?? null,
|
|
raw: JSON.stringify(b),
|
|
},
|
|
}
|
|
},
|
|
}
|