Files
Cyrene_v2/internal/auth/apikey.go
T
Aska 6dbb4a2e18
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
feat: 唤醒式架构批次——capacity/L4/会话编排/管理面板写CRUD/前端集成(77-81)
【capacity 唤醒式核心(77)】internal/runtime/capacity:(Decider L1启发式+L2上下文+人格覆盖→三态静息/唤醒/全能→允许工具集+capacity_awaken事件)。loop 外围允许集过滤(不硬阻断=能力≠边界红线)。config 加 Capacity(Enabled/MinConfidence/ToolSets)。wire buildCapacity 接入 loop。

【L4 记忆驱动意图(80)】recall.go:从记忆召回用户习惯(搞一下xx=办事)→唤醒L1/L2漏掉/升置信;群聊陌生人不读owner习惯(隔离红线);RecordIntent闭环(判定写回记忆越用越准);默认保守静息。

【会话消息编排(78)】gateway/session_orchestrator.go:每会话(模式/pending有界5/tail上下文尾巴20);Submit非阻塞(idle立即/busy入队ack/满拒绝);完成回调按序续接(tail+新消息合并增量注入非全量重放);Interrupt中断;REST 202/409+WS session_busy/pending_ack事件;不重造任务引擎复用loop。

【前端集成(79)】ws.ts 加 capacity_awaken/session_busy/pending_ack 解析;store 加 awakenState/sessionBusy;TopBar/ChatShell 加唤醒态徽标+忙态提示(不阻塞输入框);状态组件 status.tsx;Miuix风格。

【管理面板写CRUD(81)】20个写端点(POST/PUT/DELETE /admin/*,owner+审计落库):personas(真持久化YAML)/users apikey(签发掩码+撤销)/iot控制(真下发)/automations(建/触发/删)/llm(test真调用+临时)/channels(connect/close/身份测试actor白名单)/plugins(卸载)/memories(search真检索)/sessions(export)。无存储如实501不伪造。拆4文件满足≤500红线。前端9模块接真写。

【wire收口】buildCapacity接入loop+OnCapacity事件;server.go newOrchestrator接hub。

实机验证:iot ac.living control 200 真实改变 on:true;observability/metrics 200、health ok;全仓 build/vet/test 47包绿+前端tsc/vite。
2026-09-06 21:54:40 +08:00

123 lines
3.6 KiB
Go

package auth
import (
"crypto/rand"
"encoding/base64"
"fmt"
"log/slog"
"sync"
"git.yeij.top/AskaEth/Cyrene_v2/internal/config"
"git.yeij.top/AskaEth/Cyrene_v2/internal/logger"
)
// APIKey manages a bounded, in-memory constant table of API keys, each bound to
// a Role so keys can be minted for admin/owner to reach administrative
// endpoints. Keys are generated with crypto/rand and stored by their string
// form. This suits the current single-binary milestone; production should
// persist keys (e.g. hashed) and support revocation.
type APIKey struct {
mu sync.RWMutex
cfg config.AuthConfig
keys map[string]Role
log *slog.Logger
}
// NewAPIKey returns an empty key manager. cfg is accepted for constructor
// parity and future persistence settings; keys are runtime-generated.
func NewAPIKey(cfg config.AuthConfig) *APIKey {
return &APIKey{
cfg: cfg,
keys: make(map[string]Role),
log: logger.WithSource(slog.Default(), source),
}
}
// Generate mints a new random key bound to role and stores it. Returns the
// plaintext key exactly once; it must not be logged.
func (k *APIKey) Generate(role Role) (string, error) {
if !role.Valid() {
return "", fmt.Errorf("%w: %q", ErrInvalidRole, role)
}
raw := make([]byte, keyLength)
if _, err := rand.Read(raw); err != nil {
return "", fmt.Errorf("auth: generate api key: %w", err)
}
key := base64.RawURLEncoding.EncodeToString(raw)
k.mu.Lock()
k.keys[key] = role
k.mu.Unlock()
return key, nil
}
// Verify reports whether key is a known, unrevoked API key.
func (k *APIKey) Verify(key string) bool {
_, ok := k.Lookup(key)
return ok
}
// Lookup returns the role bound to key.
func (k *APIKey) Lookup(key string) (Role, bool) {
k.mu.RLock()
defer k.mu.RUnlock()
role, ok := k.keys[key]
return role, ok
}
// Revoke removes key from the constant table.
func (k *APIKey) Revoke(key string) {
k.mu.Lock()
defer k.mu.Unlock()
delete(k.keys, key)
}
// RevokeByMask removes every key whose masked identifier (the same derived id
// List returns) matches mask. It returns true when at least one key was revoked.
// The mask is lossy, so it is matched by recomputing it for each key; the table
// is small so a linear scan is fine. An empty or nil-returning call is a no-op.
func (k *APIKey) RevokeByMask(mask string) bool {
if mask == "" {
return false
}
k.mu.Lock()
defer k.mu.Unlock()
revoked := false
for key := range k.keys {
if maskKey(key) == mask {
delete(k.keys, key)
revoked = true
}
}
return revoked
}
// List returns a summary of every currently registered API key for the admin
// panel. Key material is never returned; each identifier is masked so the panel
// can tell keys apart without exposing the secret. The in-memory store keeps no
// creation time, so CreatedAt is nil for every entry.
func (k *APIKey) List() []UserSummary {
k.mu.RLock()
defer k.mu.RUnlock()
out := make([]UserSummary, 0, len(k.keys))
for key, role := range k.keys {
out = append(out, UserSummary{ID: maskKey(key), Username: "apikey", Role: role})
}
return out
}
// MaskKey renders a key's leading and trailing groups so the panel can identify
// an entry without revealing the secret. Short keys are fully hidden. It is the
// exported form of maskKey so callers (e.g. the admin issue handler) can return
// the same masked id the List page shows.
func MaskKey(key string) string { return maskKey(key) }
// maskKey renders a key's leading and trailing groups so the panel can identify
// an entry without revealing the secret. Short keys are fully hidden.
func maskKey(key string) string {
if len(key) <= 8 {
return "••••"
}
return key[:4] + "••••" + key[len(key)-4:]
}