# 全局 APP_GUARD = JwtAuthGuard 会拦截所有 controller，包括自带鉴权的（webhook / MCP）

> **日期**: 2026-05-14
> **类型**: 框架陷阱 / NestJS APP_GUARD 全局守卫

## 现象

internal-app-platform 上测试服首测：
```bash
curl -X POST http://43.166.182.155:3000/api/v1/internal-apps/webhook/gitea \
  -H "X-Gitea-Signature: deadbeef" -d "{}"
# → {"code":"UNAUTHORIZED","message":"Unauthorized","stack":"...JwtAuthGuard.handleRequest..."}
```

错误来自 `JwtAuthGuard`——但我的 webhook controller 根本没引用它。HMAC 校验代码连入口都没碰到。

## 根因

`backend/src/app.module.ts` 注册了 3 个 `APP_GUARD`（NestJS 全局守卫）：

```ts
{ provide: APP_GUARD, useClass: JwtAuthGuard },        // 1. JWT
{ provide: APP_GUARD, useClass: ... },                 // 2. permissions
{ provide: APP_GUARD, useClass: ... },                 // 3. data scope
```

NestJS `APP_GUARD` 是**全局守卫**——对**每个 controller 的每个 endpoint 都生效**，不管这个 controller 是否打算用 JWT。

我新加的 webhook（走 HMAC）和 MCP（走 opaque bearer token + DB hash 校验）这种"自带鉴权"的 endpoint，默认都会被 JwtAuthGuard 先吃掉，因为没有有效 JWT → 401。

## 解决

项目已经有 `@Public()` decorator（在 `backend/src/common/decorators/public.decorator.ts`）：

```ts
export const IS_PUBLIC_KEY = 'isPublic';
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);
```

JwtAuthGuard 读这个 metadata：

```ts
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
  context.getHandler(), context.getClass(),
]);
if (isPublic) return true;  // 跳过 JWT 校验
```

Permission + DataScope 也是类似——它们都查 IS_PUBLIC_KEY 并 short-circuit。

**所以**：任何自带鉴权的 endpoint（webhook / MCP / 开放回调）必须加 `@Public()`：

```ts
@Public()                                  // ← 关键
@Post('gitea')
@SkipAssertAccess('webhook 无 user 上下文')
async handleGitea(...) { ... }
```

`@Public()` 只跳过 framework 的 JWT 守卫；自己的 HMAC / opaque token 校验仍然在 controller body 跑。

## 类比 / 适用面

- **Stripe / Slack / 钉钉 webhook**：第三方回调，必须 `@Public()` + 自家 HMAC
- **MCP / OAuth2 introspection / 公开健康检查**：非 user-session 流量
- **任何 controller 在 main.ts NestFactory + APP_GUARD 体系下，默认都被全局守卫"染色"**
- **不要把这种 endpoint 拆到独立 NestJS app 来"避开 guard"**——一个工程一个 monolith 简单，`@Public()` 才是项目通用模式

## 排查 checklist（下次遇到 401 但代码没引 JWT 时）

1. 接口被全局 guard 拦了吗？查 `app.module.ts` 里 `APP_GUARD` 注册
2. 401 错误体里有 `JwtAuthGuard.handleRequest` 栈帧吗？→ 99% 是全局守卫
3. 是否漏了 `@Public()` 装饰器？
4. `@Public()` 加在**方法**上即可；加到 class 全员公开也行但范围太大

## 状态

- ✅ webhook.controller.ts 加 `@Public()`
- ✅ mcp.controller.ts 加 `@Public()`
- ✅ 实测：webhook 路径不再返 JwtAuthGuard 401，走到自家 HMAC 校验返 `invalid_signature` 真 401
- 经验：今后所有新加非 JWT 鉴权的 endpoint 必加 `@Public()`，否则上服务器才发现
