YouYiDianNotificationCenter/examples/node/notification-client.mjs
2026-07-16 14:49:08 +08:00

78 lines
2.6 KiB
JavaScript

import crypto from 'node:crypto'
const sha256 = (value) => crypto.createHash('sha256').update(value).digest('hex')
const cleanBaseUrl = (value) => {
const url = new URL(String(value || '').trim())
if (url.protocol !== 'https:' && url.hostname !== '127.0.0.1' && url.hostname !== 'localhost') {
throw new Error('通知中心地址必须使用 HTTPS')
}
if (url.username || url.password || url.search || url.hash) {
throw new Error('通知中心地址不能包含凭据、查询参数或片段')
}
return url.toString().replace(/\/$/, '')
}
export class YouYiDianNotificationClient {
constructor({ baseUrl, projectCode, projectSecret, fetchImpl = fetch }) {
if (!/^[a-z0-9_-]{2,40}$/.test(String(projectCode || ''))) {
throw new Error('projectCode 格式不正确')
}
if (String(projectSecret || '').length < 24) {
throw new Error('projectSecret 未配置或长度不足')
}
this.baseUrl = cleanBaseUrl(baseUrl)
this.projectCode = projectCode
this.projectSecret = projectSecret
this.fetchImpl = fetchImpl
}
createHeaders({ method, path, body = '' }) {
const timestamp = String(Math.floor(Date.now() / 1000))
const nonce = crypto.randomBytes(18).toString('base64url')
const canonical = [timestamp, nonce, method.toUpperCase(), path, sha256(body)].join('\n')
const signature = crypto.createHmac('sha256', this.projectSecret).update(canonical).digest('hex')
return {
'content-type': 'application/json',
'x-yyd-project-code': this.projectCode,
'x-yyd-timestamp': timestamp,
'x-yyd-nonce': nonce,
'x-yyd-signature': signature,
}
}
async request({ method, path, body }) {
const rawBody = body === undefined ? '' : JSON.stringify(body)
const response = await this.fetchImpl(`${this.baseUrl}${path}`, {
method,
headers: this.createHeaders({ method, path, body: rawBody }),
...(rawBody ? { body: rawBody } : {}),
})
const payload = await response.json().catch(() => ({ error: 'INVALID_JSON_RESPONSE' }))
if (!response.ok) {
const error = new Error(payload.message || payload.error || `通知中心返回 HTTP ${response.status}`)
error.status = response.status
error.code = payload.error
error.payload = payload
throw error
}
return payload
}
send(input) {
return this.request({
method: 'POST',
path: '/v1/notifications',
body: input,
})
}
getDelivery(id) {
if (!id) throw new Error('投递 id 不能为空')
return this.request({
method: 'GET',
path: `/v1/notifications/${encodeURIComponent(id)}`,
})
}
}