'use strict'; const crypto = require('crypto'); const axios = require('axios'); const { getYydNotificationCenterRuntimeConfig, } = require('./yydNotificationCenterSettings'); const NOTIFICATION_PATH = '/v1/notifications'; const DEFAULT_TIMEOUT_MS = 15000; function normalizeText(value, maxLength = 4800) { return String(value ?? '').trim().slice(0, maxLength); } async function getYydNotificationCenterConfig(options = {}) { const config = await getYydNotificationCenterRuntimeConfig(options); return { baseUrl: normalizeText(config.baseUrl, 1000).replace(/\/+$/, ''), projectCode: normalizeText(config.projectCode, 120), projectSecret: normalizeText(config.projectSecret, 1000), timeoutMs: Math.max(Number(config.timeoutMs) || DEFAULT_TIMEOUT_MS, 3000), }; } function isYydNotificationCenterConfigured(config = {}) { return Boolean(config.baseUrl && config.projectCode && config.projectSecret); } function buildYydNotificationSignature({ projectSecret, timestamp, nonce, method = 'POST', path = NOTIFICATION_PATH, rawBody = '', }) { const bodyHash = crypto.createHash('sha256').update(rawBody, 'utf8').digest('hex'); const canonical = `${timestamp}\n${nonce}\n${String(method).toUpperCase()}\n${path}\n${bodyHash}`; return crypto.createHmac('sha256', projectSecret).update(canonical, 'utf8').digest('hex'); } function validateNotificationPayload(input = {}) { const payload = { idempotencyKey: normalizeText(input.idempotencyKey, 160), title: normalizeText(input.title, 100), content: normalizeText(input.content, 4800), }; const recipientUserId = normalizeText(input.recipientUserId, 160); const recipientAlias = normalizeText(input.recipientAlias, 50); if (!payload.idempotencyKey || !payload.title || !payload.content) { throw new Error('通知缺少幂等键、标题或正文'); } if (Boolean(recipientUserId) === Boolean(recipientAlias)) { throw new Error('通知收件人必须且只能提供 recipientUserId 或 recipientAlias'); } if (recipientUserId) payload.recipientUserId = recipientUserId; if (recipientAlias) payload.recipientAlias = recipientAlias; const actionTitle = normalizeText(input.actionTitle, 20); const actionUrl = normalizeText(input.actionUrl, 2000); if (actionTitle) payload.actionTitle = actionTitle; if (actionUrl) payload.actionUrl = actionUrl; return payload; } async function sendYydNotification(input = {}, options = {}) { const config = options.config || await getYydNotificationCenterConfig(); if (!isYydNotificationCenterConfigured(config)) { const error = new Error('统一通知中心尚未配置'); error.code = 'YYD_NOTIFICATION_CENTER_NOT_CONFIGURED'; throw error; } const payload = validateNotificationPayload(input); const rawBody = JSON.stringify(payload); const timestamp = String(options.timestamp || Math.trunc(Date.now() / 1000)); const nonce = String(options.nonce || crypto.randomBytes(16).toString('hex')); const signature = buildYydNotificationSignature({ projectSecret: config.projectSecret, timestamp, nonce, rawBody, }); try { const response = await axios.post(`${config.baseUrl}${NOTIFICATION_PATH}`, rawBody, { timeout: config.timeoutMs, headers: { 'Content-Type': 'application/json', 'x-yyd-project-code': config.projectCode, 'x-yyd-timestamp': timestamp, 'x-yyd-nonce': nonce, 'x-yyd-signature': signature, }, transformRequest: [(body) => body], }); return response.data; } catch (error) { const detail = error?.response?.data; const wrapped = new Error( detail?.message || detail?.error || error.message || '统一通知中心请求失败' ); wrapped.code = detail?.code || error.code || 'YYD_NOTIFICATION_CENTER_REQUEST_FAILED'; wrapped.status = Number(error?.response?.status || 0) || 502; throw wrapped; } } module.exports = { NOTIFICATION_PATH, buildYydNotificationSignature, getYydNotificationCenterConfig, isYydNotificationCenterConfigured, sendYydNotification, validateNotificationPayload, };