refactor(mcp): apply Part 1 review I1+I3+I4+I5

- Rename MCPToken -> Token (mcp.Token reads cleaner; zero external callers yet)
- IssueSystemToken: guard against nil ACPSessionID / UserID (early input validation)
- TokenService: inject now func for clock injection (parity with RateLimiter)
- Repository CHECK constraint tests: assert wrapped CodeInternal

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-08 10:28:49 +08:00
parent 980c6fcf0e
commit a00f5a9efd
7 changed files with 106 additions and 65 deletions
+21 -11
View File
@@ -20,7 +20,7 @@ import (
type TokenService interface {
IssueAdmin(ctx context.Context, in IssueAdminInput) (IssueResult, error)
IssueSystemToken(ctx context.Context, in IssueSystemTokenInput) (IssueResult, error)
Authenticate(ctx context.Context, plaintext string) (*MCPToken, error)
Authenticate(ctx context.Context, plaintext string) (*Token, error)
Revoke(ctx context.Context, id, byUserID uuid.UUID) error
RevokeBySession(ctx context.Context, sessionID uuid.UUID) error
}
@@ -60,14 +60,18 @@ var defaultSystemTools = []string{
"create_requirement", "update_requirement", "close_requirement", "reopen_requirement", "set_requirement_phase",
}
// NewTokenService 构造 service。
func NewTokenService(repo Repository, audit audit.Recorder) TokenService {
return &tokenService{repo: repo, audit: audit}
// NewTokenService 构造 service。now 用于测试注入;nil 时默认 time.Now。
func NewTokenService(repo Repository, audit audit.Recorder, now func() time.Time) TokenService {
if now == nil {
now = time.Now
}
return &tokenService{repo: repo, audit: audit, now: now}
}
type tokenService struct {
repo Repository
audit audit.Recorder
now func() time.Time
}
// IssueAdmin 签发一个长期 admin token。Name + ExpiresAt 必填;scope 可空(= 全权)。
@@ -79,7 +83,7 @@ func (s *tokenService) IssueAdmin(ctx context.Context, in IssueAdminInput) (Issu
if in.ExpiresAt.IsZero() {
return IssueResult{}, errs.New(errs.CodeMcpTokenExpiresRequired, "expires_at is required for admin token")
}
if !in.ExpiresAt.After(time.Now()) {
if !in.ExpiresAt.After(s.now()) {
return IssueResult{}, errs.New(errs.CodeMcpTokenExpiresRequired, "expires_at must be in the future")
}
@@ -89,7 +93,7 @@ func (s *tokenService) IssueAdmin(ctx context.Context, in IssueAdminInput) (Issu
}
id := uuid.New()
created, err := s.repo.Create(ctx, &MCPToken{
created, err := s.repo.Create(ctx, &Token{
ID: id,
TokenHash: hash,
UserID: in.UserID,
@@ -140,6 +144,12 @@ func (s *tokenService) IssueAdmin(ctx context.Context, in IssueAdminInput) (Issu
// 默认 ExpiresIn = 24h;默认 Tools 为内置 PM 工具白名单(spec §6.2)。
// 写一条 audit `mcp.token.create_system`。
func (s *tokenService) IssueSystemToken(ctx context.Context, in IssueSystemTokenInput) (IssueResult, error) {
if in.ACPSessionID == uuid.Nil {
return IssueResult{}, errs.New(errs.CodeInvalidInput, "acp_session_id is required for system token")
}
if in.UserID == uuid.Nil {
return IssueResult{}, errs.New(errs.CodeInvalidInput, "user_id is required for system token")
}
if in.ExpiresIn <= 0 {
in.ExpiresIn = 24 * time.Hour
}
@@ -148,7 +158,7 @@ func (s *tokenService) IssueSystemToken(ctx context.Context, in IssueSystemToken
// 用 copy 避免共享 defaultSystemTools 给调用方修改
tools = append([]string{}, defaultSystemTools...)
}
expires := time.Now().Add(in.ExpiresIn)
expires := s.now().Add(in.ExpiresIn)
plain, hash, err := NewToken()
if err != nil {
@@ -156,7 +166,7 @@ func (s *tokenService) IssueSystemToken(ctx context.Context, in IssueSystemToken
}
id := uuid.New()
created, err := s.repo.Create(ctx, &MCPToken{
created, err := s.repo.Create(ctx, &Token{
ID: id,
TokenHash: hash,
UserID: in.UserID,
@@ -200,8 +210,8 @@ func (s *tokenService) IssueSystemToken(ctx context.Context, in IssueSystemToken
// - revoked_at 非空 → CodeMcpTokenRevoked (401)
// - expires_at 已过 → CodeMcpTokenExpired (401)
//
// 返回的 *MCPToken 不含 plaintext;调用方从中读 user_id + scope。
func (s *tokenService) Authenticate(ctx context.Context, plaintext string) (*MCPToken, error) {
// 返回的 *Token 不含 plaintext;调用方从中读 user_id + scope。
func (s *tokenService) Authenticate(ctx context.Context, plaintext string) (*Token, error) {
if strings.TrimSpace(plaintext) == "" {
return nil, errs.New(errs.CodeMcpTokenInvalid, "missing token")
}
@@ -212,7 +222,7 @@ func (s *tokenService) Authenticate(ctx context.Context, plaintext string) (*MCP
if tok.RevokedAt != nil {
return nil, errs.New(errs.CodeMcpTokenRevoked, "token revoked")
}
if tok.ExpiresAt != nil && !tok.ExpiresAt.After(time.Now()) {
if tok.ExpiresAt != nil && !tok.ExpiresAt.After(s.now()) {
return nil, errs.New(errs.CodeMcpTokenExpired, "token expired")
}