93 lines
3.4 KiB
TypeScript
93 lines
3.4 KiB
TypeScript
import crypto from 'crypto'
|
|
|
|
type NotificationCenterStatus = 'pending' | 'sending' | 'retrying' | 'sent' | 'unread' | 'read' | 'failed'
|
|
|
|
export type NotificationCenterDelivery = {
|
|
id: string
|
|
projectCode: string
|
|
status: NotificationCenterStatus
|
|
recipientUserId: string
|
|
title: string
|
|
processQueryKey?: string | null
|
|
lastErrorCode?: string | null
|
|
lastErrorMessage?: string | null
|
|
sentAt?: string | null
|
|
readAt?: string | null
|
|
raw?: unknown
|
|
}
|
|
|
|
const getConfig = () => ({
|
|
baseUrl: String(process.env.YYD_NOTIFICATION_CENTER_URL || '').trim().replace(/\/$/, ''),
|
|
projectCode: String(process.env.YYD_NOTIFICATION_CENTER_PROJECT_CODE || '').trim(),
|
|
secret: String(process.env.YYD_NOTIFICATION_CENTER_PROJECT_SECRET || '').trim(),
|
|
})
|
|
|
|
export const getNotificationCenterConfigStatus = () => {
|
|
const config = getConfig()
|
|
const missing: string[] = []
|
|
if (!config.baseUrl) missing.push('YYD_NOTIFICATION_CENTER_URL')
|
|
if (!config.projectCode) missing.push('YYD_NOTIFICATION_CENTER_PROJECT_CODE')
|
|
if (!config.secret) missing.push('YYD_NOTIFICATION_CENTER_PROJECT_SECRET')
|
|
return { ready: missing.length === 0, missing }
|
|
}
|
|
|
|
const sha256 = (value: string) => crypto.createHash('sha256').update(value).digest('hex')
|
|
|
|
const signedHeaders = (input: { method: string; path: string; body: string }) => {
|
|
const config = getConfig()
|
|
const timestamp = String(Math.floor(Date.now() / 1000))
|
|
const nonce = crypto.randomBytes(18).toString('base64url')
|
|
const canonical = [timestamp, nonce, input.method.toUpperCase(), input.path, sha256(input.body)].join('\n')
|
|
const signature = crypto.createHmac('sha256', config.secret).update(canonical).digest('hex')
|
|
return {
|
|
'Content-Type': 'application/json',
|
|
'x-yyd-project-code': config.projectCode,
|
|
'x-yyd-timestamp': timestamp,
|
|
'x-yyd-nonce': nonce,
|
|
'x-yyd-signature': signature,
|
|
}
|
|
}
|
|
|
|
const request = async (path: string, init: { method: 'GET' | 'POST'; body?: Record<string, unknown> }) => {
|
|
const config = getConfig()
|
|
const status = getNotificationCenterConfigStatus()
|
|
if (!status.ready) throw new Error(`由壹点通知中心配置不完整: ${status.missing.join(', ')}`)
|
|
const body = init.body ? JSON.stringify(init.body) : ''
|
|
const controller = new AbortController()
|
|
const timeout = setTimeout(() => controller.abort(), 8_000)
|
|
let response: Response
|
|
try {
|
|
response = await fetch(`${config.baseUrl}${path}`, {
|
|
method: init.method,
|
|
headers: signedHeaders({ method: init.method, path, body }),
|
|
body: body || undefined,
|
|
signal: controller.signal,
|
|
})
|
|
} finally {
|
|
clearTimeout(timeout)
|
|
}
|
|
const data = await response.json().catch(() => ({})) as Record<string, any>
|
|
if (!response.ok) {
|
|
throw new Error(`由壹点通知中心请求失败: ${data.error || response.status}`)
|
|
}
|
|
return data
|
|
}
|
|
|
|
export const submitYouyidianNotification = async (input: {
|
|
idempotencyKey: string
|
|
recipientUserId: string
|
|
title: string
|
|
content: string
|
|
actionTitle: string
|
|
actionUrl: string
|
|
}): Promise<NotificationCenterDelivery> => {
|
|
const data = await request('/v1/notifications', { method: 'POST', body: input })
|
|
return { ...(data as NotificationCenterDelivery), raw: data }
|
|
}
|
|
|
|
export const getYouyidianNotificationStatus = async (id: string): Promise<NotificationCenterDelivery> => {
|
|
const path = `/v1/notifications/${encodeURIComponent(id)}`
|
|
const data = await request(path, { method: 'GET' })
|
|
return { ...(data as NotificationCenterDelivery), raw: data }
|
|
}
|