Files
whatsapp-inbox-platform/lib/notification.ts
Wira Basalamah adde003fba
Some checks failed
CI - Production Readiness / Verify (push) Has been cancelled
chore: initial project import
2026-04-21 09:29:29 +07:00

155 lines
4.2 KiB
TypeScript

import { getRequestAuditContext } from "@/lib/audit";
export type TransactionalNotificationInput = {
to: string;
subject: string;
text: string;
html?: string;
metadata?: Record<string, unknown>;
};
export type TransactionalNotificationResult = {
ok: boolean;
provider?: string;
error?: string;
};
type TransportName = "webhook" | "console" | "none";
function normalizeProvider(value: string | undefined) {
const normalized = value?.trim().toLowerCase();
if (!normalized) {
return "auto";
}
if (normalized === "webhook") {
return "webhook" as TransportName;
}
if (normalized === "console") {
return "console" as TransportName;
}
return normalized as "auto" | TransportName;
}
function isProduction() {
return process.env.NODE_ENV === "production";
}
function buildPayload(input: TransactionalNotificationInput, context?: { ipAddress?: string | null; userAgent?: string | null }) {
return {
to: input.to,
subject: input.subject,
text: input.text,
html: input.html ?? null,
metadata: input.metadata ?? null,
type: "transactional",
providerContext: {
ipAddress: context?.ipAddress ?? null,
userAgent: context?.userAgent ?? null
}
};
}
function getWebhookUrl() {
return process.env.NOTIFICATION_WEBHOOK_URL?.trim();
}
function getWebhookToken() {
return process.env.NOTIFICATION_WEBHOOK_TOKEN?.trim();
}
function getWebhookTimeoutMs() {
const raw = process.env.NOTIFICATION_WEBHOOK_TIMEOUT_MS?.trim();
const parsed = Number(raw);
if (!Number.isFinite(parsed) || parsed <= 0) {
return 10_000;
}
return Math.min(60_000, parsed);
}
async function sendViaWebhook(input: TransactionalNotificationInput, context?: { ipAddress?: string | null; userAgent?: string | null }) {
const url = getWebhookUrl();
if (!url) {
return {
ok: false,
error: "NOTIFICATION_WEBHOOK_URL is not configured"
} as const;
}
const headers: Record<string, string> = {
"Content-Type": "application/json"
};
const token = getWebhookToken();
if (token) {
headers.Authorization = `Bearer ${token}`;
}
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), getWebhookTimeoutMs());
try {
const response = await fetch(url, {
method: "POST",
headers,
body: JSON.stringify(buildPayload(input, context)),
signal: controller.signal
});
clearTimeout(timeout);
if (!response.ok) {
const errorBody = await response.text();
return {
ok: false,
error: `Webhook notification failed ${response.status}: ${errorBody}`.slice(0, 600)
};
}
return { ok: true, provider: "webhook" };
} catch (error) {
clearTimeout(timeout);
return {
ok: false,
error: error instanceof Error ? error.message : "Webhook notification error"
};
}
}
export async function sendTransactionalNotification(input: TransactionalNotificationInput): Promise<TransactionalNotificationResult> {
if (!input.to || !input.subject || !input.text) {
return { ok: false, error: "Notification payload is incomplete" };
}
const provider = normalizeProvider(process.env.NOTIFICATION_PROVIDER);
const explicitWebhookUrl = getWebhookUrl();
const requestContext = await getRequestAuditContext().catch(() => ({
ipAddress: null,
userAgent: null
}));
if (provider === "none") {
return { ok: false, error: "Notification delivery disabled" };
}
if (provider === "console" || (!provider && !isProduction())) {
// eslint-disable-next-line no-console
console.log(`NOTIFICATION subject=${input.subject} to=${input.to}`);
// eslint-disable-next-line no-console
console.log(`NOTIFICATION_BODY ${input.text}`);
return { ok: true, provider: "console" };
}
const result = await sendViaWebhook(input, requestContext);
if (!result.ok && !provider && !isProduction() && !explicitWebhookUrl) {
// eslint-disable-next-line no-console
console.log(`NOTIFICATION_FALLBACK subject=${input.subject} to=${input.to}`);
// eslint-disable-next-line no-console
console.log(`NOTIFICATION_BODY ${input.text}`);
return { ok: true, provider: "console" };
}
return result.ok ? { ok: true, provider: result.provider } : { ok: false, error: result.error };
}