feat(shared-ts): federation shared utilities for apollo subgraphs

- RouterAuthGuard: validate Router-Authorization header (ADR-036)

- DataLoader factory: request-scoped batching (ADR-035)

- ScopeTokenService: Redis-backed scope token (ADR-041)

- GraphqlContext: build context from HTTP headers

- FederationExceptionFilter: HTTP-to-GraphQL error mapping
This commit is contained in:
SpecialX
2026-07-14 23:46:51 +08:00
parent 3c2ea50c7f
commit 5fcb831a18
9 changed files with 1074 additions and 4 deletions

View File

@@ -0,0 +1,76 @@
/**
* RouterAuthGuard - Apollo Router 信任凭证校验
*
* 强制约束v2.1 §3.7 / ADR-036
* Apollo Router 请求子图时必须携带 Router-Authorization Header
* 各服务 NestJS Guard 拦截并校验,拒绝任何非 Router 发起的 GraphQL 请求。
*
* 部署模式:
* - 生产ROUTER_AUTH_SECRET 环境变量配置共享密钥
* - 开发DEV_MODE=true 时跳过校验(仅限本地)
*/
import {
CanActivate,
ExecutionContext,
Injectable,
ForbiddenException,
Logger,
} from "@nestjs/common";
export const ROUTER_AUTH_HEADER = "router-authorization";
export interface RouterAuthConfig {
/** 共享密钥(生产环境由 Secret 注入) */
secret: string;
/** 开发模式跳过校验 */
devMode?: boolean;
}
@Injectable()
export class RouterAuthGuard implements CanActivate {
private readonly logger = new Logger(RouterAuthGuard.name);
private readonly config: RouterAuthConfig;
constructor(config: RouterAuthConfig) {
this.config = config;
}
canActivate(ctx: ExecutionContext): boolean {
if (this.config.devMode === true) {
return true;
}
const req = ctx.switchToHttp().getRequest<{
headers: Record<string, string | undefined>;
url: string;
}>();
// 健康检查端点豁免
if (req.url?.startsWith("/health") || req.url?.startsWith("/ready")) {
return true;
}
const routerAuth = req.headers[ROUTER_AUTH_HEADER];
const expected = this.config.secret;
if (!expected) {
this.logger.error(
`${ROUTER_AUTH_HEADER} secret not configured (ROUTER_AUTH_SECRET env required)`,
);
throw new ForbiddenException(
"Router authorization not configured on server",
);
}
if (!routerAuth || routerAuth !== expected) {
this.logger.warn(
`Direct GraphQL access denied (path=${req.url}); must go through Apollo Router`,
);
throw new ForbiddenException(
"Direct GraphQL access denied; must go through Apollo Router",
);
}
return true;
}
}