package handler import ( "net/http" "github.com/agentmail/gateway/internal/middleware" "github.com/agentmail/gateway/internal/repo" "github.com/agentmail/gateway/internal/sse" ) // GET /api/v1/events/stream // // 四种凭证,都必须真正验证过身份才能订阅: // Authorization: Bearer → Agent 通道(密钥认证) // X-Agent-Name + X-Agent-Secret → Agent 通道(旧方式,兼容) // 登录 Cookie 或 Bearer → 人类用户通道 // ?access_token= → 浏览器 EventSource 专用回退 // // 注意不能只凭 X-Agent-Name 就分流:那等于任何人报个名字就能读走别人的新邮件通知。 // query 令牌仅本端点接受(EventSource 无法带自定义头),其余接口一律要求请求头, // 因为 URL 里的令牌会进访问日志与 Referer。 func SSEStream(w http.ResponseWriter, r *http.Request) { agentName, ok := resolveStreamAgent(r) if !ok { Error(w, http.StatusUnauthorized, "凭证无效") return } userName := "" if agentName == "" { u := middleware.OptionalUserWithQuery(r) if u == nil { Error(w, http.StatusUnauthorized, "not authenticated") return } userName = u.Username } client := sse.Default.AddClient(w, r, agentName, userName) if client == nil { Error(w, http.StatusInternalServerError, "SSE not supported") return } <-r.Context().Done() sse.Default.RemoveClient(client.ID) } // resolveStreamAgent 校验 Agent 侧凭证。 // 返回 ("", true) 表示这不是 Agent 请求,交给人类用户分支; // 返回 ("", false) 表示带了 Agent 凭证但验证失败。 func resolveStreamAgent(r *http.Request) (string, bool) { // 密钥认证:Bearer 令牌可能是 Agent 密钥,也可能是用户密钥。 // 先按 Agent 密钥试,失败就落到人类分支(那里会再按用户密钥试)。 token := middleware.BearerToken(r) if token == "" { token = middleware.QueryToken(r) // EventSource 回退 } if token != "" { name, err := repo.VerifyAgentKey(r.Context(), token) if err == nil && name != "" { return name, true } return "", true } name := r.Header.Get("X-Agent-Name") if name == "" { name = r.URL.Query().Get("agent_name") } if name == "" { return "", true // 非 Agent 请求 } secret := r.Header.Get("X-Agent-Secret") if secret == "" { return "", false // 报了名字却没给凭证 } agent, err := repo.VerifyAgent(r.Context(), name, secret) if err != nil { return "", false } return agent.Name, true } // GET /api/v1/events/status func SSEStatus(w http.ResponseWriter, r *http.Request) { JSON(w, http.StatusOK, map[string]interface{}{ "connected_clients": sse.Default.ClientCount(), }) }