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:
76
packages/shared-ts/src/federation/router-auth.guard.ts
Normal file
76
packages/shared-ts/src/federation/router-auth.guard.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user