mirror of
https://github.com/fosrl/pangolin.git
synced 2026-04-20 08:46:37 +00:00
First pass
This commit is contained in:
132
server/private/lib/alerts/sendAlertWebhook.ts
Normal file
132
server/private/lib/alerts/sendAlertWebhook.ts
Normal file
@@ -0,0 +1,132 @@
|
||||
/*
|
||||
* This file is part of a proprietary work.
|
||||
*
|
||||
* Copyright (c) 2025 Fossorial, Inc.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This file is licensed under the Fossorial Commercial License.
|
||||
* You may not use this file except in compliance with the License.
|
||||
* Unauthorized use, copying, modification, or distribution is strictly prohibited.
|
||||
*
|
||||
* This file is not licensed under the AGPLv3.
|
||||
*/
|
||||
|
||||
import logger from "@server/logger";
|
||||
import { AlertContext, WebhookAlertConfig } from "./types";
|
||||
|
||||
const REQUEST_TIMEOUT_MS = 15_000;
|
||||
|
||||
/**
|
||||
* Sends a single webhook POST for an alert event.
|
||||
*
|
||||
* The payload shape is:
|
||||
* ```json
|
||||
* {
|
||||
* "event": "site_online",
|
||||
* "timestamp": "2024-01-01T00:00:00.000Z",
|
||||
* "data": { ... }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* Authentication headers are applied according to `config.authType`,
|
||||
* mirroring the same strategies supported by HttpLogDestination:
|
||||
* none | bearer | basic | custom.
|
||||
*/
|
||||
export async function sendAlertWebhook(
|
||||
url: string,
|
||||
webhookConfig: WebhookAlertConfig,
|
||||
context: AlertContext
|
||||
): Promise<void> {
|
||||
const payload = {
|
||||
event: context.eventType,
|
||||
timestamp: new Date().toISOString(),
|
||||
data: {
|
||||
orgId: context.orgId,
|
||||
...context.data
|
||||
}
|
||||
};
|
||||
|
||||
const body = JSON.stringify(payload);
|
||||
const headers = buildHeaders(webhookConfig);
|
||||
|
||||
const controller = new AbortController();
|
||||
const timeoutHandle = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
|
||||
|
||||
let response: Response;
|
||||
try {
|
||||
response = await fetch(url, {
|
||||
method: "POST",
|
||||
headers,
|
||||
body,
|
||||
signal: controller.signal
|
||||
});
|
||||
} catch (err: unknown) {
|
||||
const isAbort = err instanceof Error && err.name === "AbortError";
|
||||
if (isAbort) {
|
||||
throw new Error(
|
||||
`Alert webhook: request to "${url}" timed out after ${REQUEST_TIMEOUT_MS} ms`
|
||||
);
|
||||
}
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
throw new Error(`Alert webhook: request to "${url}" failed – ${msg}`);
|
||||
} finally {
|
||||
clearTimeout(timeoutHandle);
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
let snippet = "";
|
||||
try {
|
||||
const text = await response.text();
|
||||
snippet = text.slice(0, 300);
|
||||
} catch {
|
||||
// best-effort
|
||||
}
|
||||
throw new Error(
|
||||
`Alert webhook: server at "${url}" returned HTTP ${response.status} ${response.statusText}` +
|
||||
(snippet ? ` – ${snippet}` : "")
|
||||
);
|
||||
}
|
||||
|
||||
logger.debug(`Alert webhook sent successfully to "${url}" for event "${context.eventType}"`);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Header construction (mirrors HttpLogDestination.buildHeaders)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function buildHeaders(webhookConfig: WebhookAlertConfig): Record<string, string> {
|
||||
const headers: Record<string, string> = {
|
||||
"Content-Type": "application/json"
|
||||
};
|
||||
|
||||
switch (webhookConfig.authType) {
|
||||
case "bearer": {
|
||||
const token = webhookConfig.bearerToken?.trim();
|
||||
if (token) {
|
||||
headers["Authorization"] = `Bearer ${token}`;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "basic": {
|
||||
const creds = webhookConfig.basicCredentials?.trim();
|
||||
if (creds) {
|
||||
const encoded = Buffer.from(creds).toString("base64");
|
||||
headers["Authorization"] = `Basic ${encoded}`;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "custom": {
|
||||
const name = webhookConfig.customHeaderName?.trim();
|
||||
const value = webhookConfig.customHeaderValue ?? "";
|
||||
if (name) {
|
||||
headers[name] = value;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "none":
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return headers;
|
||||
}
|
||||
Reference in New Issue
Block a user