feat(webhook): test ping — admin верифицирует receiver одной кнопкой
После создания subscription / rotate secret admin не имел способа
проверить что receiver жив и валидирует HMAC без ожидания реального
business event. Теперь:
- POST /api/v1/admin/webhooks/subscriptions/{id}/test
- Synchronous: receiver получает synthetic event с теми же headers
что у production delivery (X-Ordinis-Signature, X-Ordinis-Event-Type,
X-Ordinis-Scope) + дополнительный X-Ordinis-Test=true чтобы
receiver мог логировать/филтровать как тест.
- HMAC sign'ится через тот же HmacSigner — admin раскопировал secret
правильно если receiver валидирует. Иначе receiver вернёт 4xx
и UI покажет ошибку.
- SSRF guard validate URL host (private CIDR + allowed-hosts override)
— same policy как у dispatcher.
Bypass'ит outbox/dispatcher — нет webhook_deliveries row, нет attempt
count, нет следов в аудите доставок (это test, не business event).
UI:
- Кнопка 'Тест ping' с PaperPlaneTilt иконкой рядом с 'Сменить secret'
на webhook detail page.
- Inline Alert после ответа: success/failure/rejected с HTTP status,
latency, body preview / error message. Dismiss '✕'.
RBAC: RESTRICTED — endpoint отправляет HTTP request на user-controlled
URL (potential abuse vector если бы был INTERNAL).
Tests: rest-api compiles, 76 admin-ui passed.
This commit is contained in:
@@ -8,6 +8,7 @@ import {
|
||||
type RecordResponse,
|
||||
type WebhookDelivery,
|
||||
type WebhookSubscription,
|
||||
type WebhookTestPingResult,
|
||||
} from './client'
|
||||
|
||||
const idempotencyKey = () =>
|
||||
@@ -166,6 +167,24 @@ export const useRotateWebhookSecret = () => {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Test ping — receiver получает synthetic event с тем же HMAC и headers
|
||||
* что у production delivery. Возвращает result sync для inline feedback.
|
||||
*
|
||||
* Use case: после создания subscription / rotate secret — verify что
|
||||
* receiver достижим, валидирует HMAC, не возвращает 5xx.
|
||||
*/
|
||||
export const useTestWebhook = () => {
|
||||
return useMutation({
|
||||
mutationFn: async (id: string): Promise<WebhookTestPingResult> => {
|
||||
const { data } = await apiClient.post<WebhookTestPingResult>(
|
||||
`/admin/webhooks/subscriptions/${encodeURIComponent(id)}/test`,
|
||||
)
|
||||
return data
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Replay delivery из DLQ или retrying — backend сбрасывает attemptCount=0,
|
||||
* dlqAt=null, status=retrying. Dispatcher подхватит на следующем sendTick.
|
||||
|
||||
Reference in New Issue
Block a user