// packages/contracts/src/internal-sync.ts —— /internal/** 服务间同步接口的 // HMAC 鉴权单一实现(P2 Task 3)。签名规则(openapi.yaml [internal] tag 描述): // X-Sync-Signature = hex(HMAC-SHA256(原始请求体字节, SYNC_INTERNAL_KEY)) // X-Sync-Timestamp = Unix 秒,与服务端时差 ±300s 内有效(防重放) // 发送方(services/sync sender)与接收方(cn bootstrap/reconcile、ov // content-batches 的 InternalSyncGuard)共用本模块,杜绝两侧规则漂移。 // 注意签名覆盖的是**原始请求体字节**(rawBody):接收端必须经 body-parser 的 // verify 钩子(NestFactory rawBody: true)在 JSON 解析前捕获原始字节,重序列 // 化的 body 与签名不一致会 401。 import { createHmac, timingSafeEqual } from 'node:crypto'; /** 内部同步 HMAC 密钥的 env 键名(仅 env 注入,不入库不入码)。 */ export const SYNC_INTERNAL_KEY_ENV = 'SYNC_INTERNAL_KEY'; /** HMAC 签名/时间戳请求头(小写;Node req.headers 已归一小写)。 */ export const SYNC_SIGNATURE_HEADER = 'x-sync-signature'; export const SYNC_TIMESTAMP_HEADER = 'x-sync-timestamp'; /** 时间戳容忍窗(秒,防重放;契约 [internal] tag 描述 ±300s)。 */ export const SYNC_TIMESTAMP_TOLERANCE_SECONDS = 300; /** * 计算请求体签名:hex(HMAC-SHA256(rawBody, key))。 * rawBody 必须是与线上传输完全一致的原始字节(发送侧:JSON.stringify 的产物 * 原样作为 fetch body;接收侧:body-parser verify 捕获的 Buffer)。 */ export function signSyncBody( key: string, rawBody: string | Buffer, ): string { return createHmac('sha256', key).update(rawBody).digest('hex'); } /** 验签输入:头取值原样传入(数组/缺失由本函数判 false,不抛错)。 */ export interface VerifySyncSignatureInput { key: string; rawBody: Buffer | string; timestampHeader: unknown; signatureHeader: unknown; /** 可注入时钟(测试用);缺省 Date.now()。 */ nowMs?: number; } /** * 接收端验签:时间戳在窗内 + 签名 hex(64) 形状 + 常时比较全过才 true。 * key 未配置(空串)恒 false——密钥缺失时内部端点整体不可用而非裸奔。 */ export function verifySyncSignature(input: VerifySyncSignatureInput): boolean { const { key, rawBody, timestampHeader, signatureHeader } = input; if (key === '') { return false; } const timestamp = typeof timestampHeader === 'string' ? Number(timestampHeader.trim()) : Number.NaN; if (!Number.isFinite(timestamp)) { return false; } const nowSeconds = Math.floor((input.nowMs ?? Date.now()) / 1000); if ( Math.abs(nowSeconds - timestamp) > SYNC_TIMESTAMP_TOLERANCE_SECONDS ) { return false; } if ( typeof signatureHeader !== 'string' || !/^[0-9a-f]{64}$/.test(signatureHeader.trim()) ) { return false; } const expected = signSyncBody(key, rawBody); const provided = signatureHeader.trim(); // 两串均为 64 hex 字符(上方正则保证),等长 Buffer,timingSafeEqual 安全。 return timingSafeEqual( Buffer.from(expected, 'hex'), Buffer.from(provided, 'hex'), ); }