Files
photo-live/packages/contracts/src/internal-sync.ts
T
2026-09-18 15:21:41 +08:00

80 lines
3.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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'),
);
}