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

This commit is contained in:
wehub-resource-sync
2026-07-13 13:20:55 +08:00
commit d25d482dc2
13754 changed files with 4996608 additions and 0 deletions
+145
View File
@@ -0,0 +1,145 @@
import type { Logger } from '@sim/logger'
import { createLogger } from '@sim/logger'
import { safeCompare } from '@sim/security/compare'
import { sha256Hex } from '@sim/security/hash'
import { NextResponse } from 'next/server'
import type { AuthContext, EventFilterContext } from '@/lib/webhooks/providers/types'
const logger = createLogger('WebhookProviderAuth')
/**
* Deterministic JSON serialization with object keys sorted, so structurally
* identical payloads produce identical output regardless of key order.
*/
function stableSerialize(value: unknown): string {
if (Array.isArray(value)) {
return `[${value.map((item) => stableSerialize(item)).join(',')}]`
}
if (value && typeof value === 'object') {
return `{${Object.entries(value as Record<string, unknown>)
.sort(([a], [b]) => a.localeCompare(b))
.map(([key, nested]) => `${JSON.stringify(key)}:${stableSerialize(nested)}`)
.join(',')}}`
}
return JSON.stringify(value)
}
/**
* Fallback idempotency fingerprint for payloads with no stable delivery id
* or content timestamp to key on. A provider retry resends identical bytes,
* so this hash is stable across retries of the same delivery while still
* differentiating distinct events.
*/
export function buildFallbackDeliveryFingerprint(body: unknown): string {
return sha256Hex(stableSerialize(body))
}
interface HmacVerifierOptions {
configKey: string
headerName: string
validateFn: (secret: string, signature: string, rawBody: string) => boolean | Promise<boolean>
providerLabel: string
/**
* When true, reject (401) if no secret is configured instead of skipping
* verification. Use for providers where the secret is always present (e.g.
* auto-registered webhooks) so a missing secret fails closed.
*/
requireSecret?: boolean
}
/**
* Factory that creates a `verifyAuth` implementation for HMAC-signature-based providers.
* Covers the common pattern: get secret → check header → validate signature → return 401 or null.
*/
export function createHmacVerifier({
configKey,
headerName,
validateFn,
providerLabel,
requireSecret = false,
}: HmacVerifierOptions) {
return async ({
request,
rawBody,
requestId,
providerConfig,
}: AuthContext): Promise<NextResponse | null> => {
const secret = providerConfig[configKey] as string | undefined
if (!secret) {
if (requireSecret) {
logger.warn(`[${requestId}] ${providerLabel} webhook secret not configured`)
return new NextResponse(`Unauthorized - Missing ${providerLabel} webhook secret`, {
status: 401,
})
}
return null
}
const signature = request.headers.get(headerName)
if (!signature) {
logger.warn(`[${requestId}] ${providerLabel} webhook missing signature header`)
return new NextResponse(`Unauthorized - Missing ${providerLabel} signature`, { status: 401 })
}
const isValid = await validateFn(secret, signature, rawBody)
if (!isValid) {
logger.warn(`[${requestId}] ${providerLabel} signature verification failed`, {
signatureLength: signature.length,
secretLength: secret.length,
})
return new NextResponse(`Unauthorized - Invalid ${providerLabel} signature`, { status: 401 })
}
return null
}
}
/**
* Verify a bearer token or custom header token using timing-safe comparison.
* Used by generic webhooks, Google Forms, and the default handler.
*/
export function verifyTokenAuth(
request: Request,
expectedToken: string,
secretHeaderName?: string
): boolean {
if (secretHeaderName) {
const headerValue = request.headers.get(secretHeaderName.toLowerCase())
return !!headerValue && safeCompare(headerValue, expectedToken)
}
const authHeader = request.headers.get('authorization')
if (authHeader?.toLowerCase().startsWith('bearer ')) {
const token = authHeader.substring(7)
return safeCompare(token, expectedToken)
}
return false
}
/**
* Skip events whose `body.type` is not in the `providerConfig.eventTypes` allowlist.
* Shared by providers that use a simple event-type filter (Stripe, Grain, etc.).
*/
export function skipByEventTypes(
{ webhook, body, requestId, providerConfig }: EventFilterContext,
providerLabel: string,
eventLogger: Logger
): boolean {
const eventTypes = providerConfig.eventTypes
if (!eventTypes || !Array.isArray(eventTypes) || eventTypes.length === 0) {
return false
}
const eventType = (body as Record<string, unknown>)?.type as string | undefined
if (eventType && !eventTypes.includes(eventType)) {
eventLogger.info(
`[${requestId}] ${providerLabel} event type '${eventType}' not in allowed list for webhook ${webhook.id as string}, skipping`
)
return true
}
return false
}