feat(sync): cn event coverage, bootstrap and outbox sender

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
浅念
2026-09-18 15:21:41 +08:00
parent 3d6e9ce175
commit 65726702f3
43 changed files with 3333 additions and 181 deletions
@@ -0,0 +1,58 @@
// services/api/src/shared/internal-sync.guard.ts —— /internal/** 服务间同步
// 端点的 HMAC 守卫(P2 Task 3)。规则单一实现见 @photo/contracts internal-sync.ts
// (与 services/sync 发送器共用,防两侧漂移):
// X-Sync-Signature = hex(HMAC-SHA256(原始请求体, SYNC_INTERNAL_KEY))
// X-Sync-Timestamp = Unix 秒(±300s 防重放)
// 失败路径(头缺失/签名不符/时间戳超窗/密钥未配置)一律同码 401 SYNC_AUTH_FAILED,
// 不区分具体原因(不向调用方泄露校验进度)。原始请求体依赖 NestFactory 的
// rawBody: true(body-parser verify 钩子在 JSON 解析前捕获 Buffer 到 req.rawBody,
// 两个部署入口 main-cn/main-ov 均已开启);未经解析器捕获的请求无 rawBody → 401。
// 使用方式:@UseGuards(InternalSyncGuard)(按控制器/路由启用,不注册 APP_GUARD)。
// 守卫无依赖(纯 env + 纯函数校验),任意模块可直接引用。
import {
type CanActivate,
type ExecutionContext,
HttpStatus,
Injectable,
} from '@nestjs/common';
import {
SYNC_INTERNAL_KEY_ENV,
SYNC_SIGNATURE_HEADER,
SYNC_TIMESTAMP_HEADER,
verifySyncSignature,
} from '@photo/contracts';
import { ApiException } from '../api-exception';
import { ErrorCodes } from '@photo/contracts';
/** 经 body-parser verify 捕获的原始请求体(NestFactory rawBody: true)。 */
export interface RequestWithRawBody {
rawBody?: Buffer;
headers: Record<string, string | string[] | undefined>;
}
function syncUnauthorized(): ApiException {
return new ApiException(
ErrorCodes.SYNC_AUTH_FAILED,
'Internal sync signature is missing, invalid, or expired',
HttpStatus.UNAUTHORIZED,
);
}
@Injectable()
export class InternalSyncGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean {
const req = context.switchToHttp().getRequest<RequestWithRawBody>();
// rawBody 缺失(未走 JSON 解析器的请求体/入口未开 rawBody)以空串参与校验,
// 结果恒 false——与签名不符同一出口,不单独暴露「原始体缺失」这一内部细节。
const ok = verifySyncSignature({
key: process.env[SYNC_INTERNAL_KEY_ENV] ?? '',
rawBody: req.rawBody ?? '',
timestampHeader: req.headers[SYNC_TIMESTAMP_HEADER],
signatureHeader: req.headers[SYNC_SIGNATURE_HEADER],
});
if (!ok) {
throw syncUnauthorized();
}
return true;
}
}