Files
supabase/apps/studio/components/interfaces/Platform/Webhooks/PlatformWebhooks.utils.ts
T
Monica KhouryandCharis Lam 2d5768d3c1 feat(studio): wire Platform Webhooks UI to real backend API (#51352)
## What
Wires the existing (mocked) Platform Webhooks UI to the real Management
API backend, per FE-3749.

- New `data/platform-webhooks/` query/mutation hooks, calling the real
`/v2/organizations/{slug}/webhooks/...` and
`/v2/projects/{ref}/webhooks/...` endpoints
- Updated `PlatformWebhooks.types.ts` to match the real API contract
(field names, shapes)
- Added a signing secret field to the create-endpoint form (API requires
a user-provided secret, not server-generated like the mock assumed)
- Removed the event-type column from the deliveries table (the real
list-deliveries endpoint doesn't return event type, only the
single-delivery endpoint does)
- Removed `PlatformWebhooks.mock.ts` / `.store.ts` and their tests

## Why
UI was built ~80% against mock data months ago; this closes the gap to a
real, working feature.

## Known open items (not blocking, tracked separately)
- `name` field has no backend storage yet — client-only for now, open
question with Ali/Paweł
- Deliveries table only fetches the backend's default single page — real
server-side pagination not yet wired (see FE-3750)

## Follow-up PR (stacked)
Tests for the new `data/platform-webhooks/` hooks (MSW-based), per the
ticket's own scope split.

## Testing
Manually tested end-to-end against local backend: created/edited/deleted
an endpoint, verified the real signature using webhook.site,
independently recomputed the HMAC and confirmed it matches the
`webhook-signature` header.

Linear: FE-3749

---------

Co-authored-by: Charis Lam <26616127+charislam@users.noreply.github.com>
2026-10-09 22:41:09 +03:00

95 lines
2.4 KiB
TypeScript

import type { WebhookDelivery, WebhookEndpoint } from './PlatformWebhooks.types'
const WEBHOOK_NAME_ADJECTIVES = [
'swift',
'winged',
'wayfinding',
'moonlit',
'fleet',
'nimble',
'roving',
'brisk',
'gliding',
'steady',
'northbound',
'starlit',
'quiet',
'amber',
'far-flung',
'secret',
'flying',
]
const WEBHOOK_NAME_NOUNS = [
'pigeon',
'courier',
'postmark',
'relay',
'dispatch',
'lantern',
'beacon',
'messenger',
'waypost',
'sparrow',
'satchel',
'signalfire',
'envelope',
'parcel',
'gull',
'kite',
'beagle',
]
const getRandomItem = (values: string[]) => values[Math.floor(Math.random() * values.length)]
export const generateWebhookEndpointName = () =>
`${getRandomItem(WEBHOOK_NAME_ADJECTIVES)}-${getRandomItem(WEBHOOK_NAME_NOUNS)}`
const secureRandomHex = (length: number) => {
if (length <= 0) return ''
const cryptoApi = globalThis.crypto
if (!cryptoApi?.getRandomValues) {
throw new Error('Web Crypto API is not available')
}
const byteCount = Math.ceil(length / 2)
const bytes = new Uint8Array(byteCount)
cryptoApi.getRandomValues(bytes)
return Array.from(bytes, (byte) => byte.toString(16).padStart(2, '0'))
.join('')
.slice(0, length)
}
export const generateSigningSecret = () => `whsec_${secureRandomHex(16)}`
export const getWebhookEndpointDisplayName = (endpoint: Pick<WebhookEndpoint, 'name' | 'url'>) =>
endpoint.name.trim() || endpoint.url
const normalizeSearch = (value: string) => value.trim().toLowerCase()
export const filterWebhookEndpoints = (endpoints: WebhookEndpoint[], search: string) => {
const normalizedSearch = normalizeSearch(search)
if (normalizedSearch.length === 0) return endpoints
return endpoints.filter((endpoint) => {
const haystack =
`${getWebhookEndpointDisplayName(endpoint)} ${endpoint.url} ${endpoint.description}`.toLowerCase()
return haystack.includes(normalizedSearch)
})
}
export const filterWebhookDeliveries = (deliveries: WebhookDelivery[], search: string) => {
const normalizedSearch = normalizeSearch(search)
return deliveries
.filter((delivery) => {
if (normalizedSearch.length === 0) return true
const haystack =
`${delivery.eventType ?? ''} ${delivery.status} ${delivery.responseCode ?? ''}`.toLowerCase()
return haystack.includes(normalizedSearch)
})
.sort((a, b) => new Date(b.attemptAt).getTime() - new Date(a.attemptAt).getTime())
}