package controlplane import ( "context" "crypto/rand" "crypto/sha256" "crypto/subtle" "crypto/tls" "encoding/base64" "encoding/hex" "encoding/json" "errors" "fmt" "io" "io/fs" "mime" "net/http" "net/url" "path" "sort" "strconv" "strings" "sync" "time" webassets "git.ipao.vip/rogee/wx-win-agent/control-plane/web" ) type ServerConfig struct { ListenAddr string DataFile string NodeTokens map[string]string WebUsers map[string]string LeaseTTL time.Duration HeartbeatTimeout time.Duration SessionTTL time.Duration TLSCertFile string TLSKeyFile string MTLSClientCAFile string MTLSRequireNodeCert bool MTLSRevokedCertsFile string BackupDir string BackupCount int BackupInterval time.Duration TaskRetention time.Duration EventRetention time.Duration AuditRetention time.Duration AIBaseURL string AIAPIKey string AIModel string AITimeout time.Duration AISchedulerInterval time.Duration AIProvider AIProvider } func DefaultServerConfig() ServerConfig { return ServerConfig{ ListenAddr: "127.0.0.1:8090", DataFile: "control-plane-data.json", NodeTokens: map[string]string{}, WebUsers: map[string]string{}, LeaseTTL: 30 * time.Second, HeartbeatTimeout: 45 * time.Second, SessionTTL: 8 * time.Hour, BackupCount: 7, BackupInterval: 5 * time.Minute, TaskRetention: 30 * 24 * time.Hour, EventRetention: 30 * 24 * time.Hour, AuditRetention: 90 * 24 * time.Hour, AIModel: "gpt-4o-mini", AITimeout: 60 * time.Second, AISchedulerInterval: 5 * time.Second, } } type Server struct { config ServerConfig store *Store tlsConfig *tls.Config nodeTokenHashes map[string][32]byte userPasswords map[string][32]byte sessionMu sync.Mutex sessions map[string]session aiProvider AIProvider aiCtx context.Context aiCancel context.CancelFunc aiQueue chan string aiWG sync.WaitGroup closeOnce sync.Once closeErr error } type session struct { Username string Expires time.Time } type requestError struct { status int code string message string } func (e requestError) Error() string { return e.message } func NewServer(config ServerConfig) (*Server, error) { defaults := DefaultServerConfig() if config.ListenAddr == "" { config.ListenAddr = defaults.ListenAddr } if config.DataFile == "" { config.DataFile = defaults.DataFile } if config.BackupDir == "" { config.BackupDir = config.DataFile + ".backups" } if config.BackupCount == 0 { config.BackupCount = defaults.BackupCount } if config.BackupInterval == 0 { config.BackupInterval = defaults.BackupInterval } if config.TaskRetention == 0 { config.TaskRetention = defaults.TaskRetention } if config.EventRetention == 0 { config.EventRetention = defaults.EventRetention } if config.AuditRetention == 0 { config.AuditRetention = defaults.AuditRetention } if config.LeaseTTL <= 0 { config.LeaseTTL = defaults.LeaseTTL } if config.HeartbeatTimeout <= 0 { config.HeartbeatTimeout = defaults.HeartbeatTimeout } if config.SessionTTL <= 0 { config.SessionTTL = defaults.SessionTTL } if config.AIModel == "" { config.AIModel = defaults.AIModel } if config.AITimeout <= 0 { config.AITimeout = defaults.AITimeout } if config.AISchedulerInterval <= 0 { config.AISchedulerInterval = defaults.AISchedulerInterval } if config.AIProvider == nil && config.AIBaseURL != "" { config.AIProvider = &OpenAICompatibleProvider{BaseURL: config.AIBaseURL, APIKey: config.AIAPIKey, Model: config.AIModel} } if config.NodeTokens == nil { config.NodeTokens = map[string]string{} } if config.WebUsers == nil { config.WebUsers = map[string]string{} } if config.BackupCount < 0 || config.BackupInterval < 0 || config.TaskRetention < 0 || config.EventRetention < 0 || config.AuditRetention < 0 || config.AITimeout < 0 || config.AISchedulerInterval < 0 { return nil, errors.New("backup count and retention settings must be non-negative") } tlsConfig, err := newTLSConfig(config) if err != nil { return nil, err } store, err := OpenStore(config.DataFile, StoreOptions{ BackupDir: config.BackupDir, BackupCount: config.BackupCount, BackupInterval: config.BackupInterval, TaskRetention: config.TaskRetention, EventRetention: config.EventRetention, AuditRetention: config.AuditRetention, }) if err != nil { return nil, err } aiCtx, aiCancel := context.WithCancel(context.Background()) s := &Server{ config: config, store: store, tlsConfig: tlsConfig, nodeTokenHashes: map[string][32]byte{}, userPasswords: map[string][32]byte{}, sessions: map[string]session{}, aiProvider: config.AIProvider, aiCtx: aiCtx, aiCancel: aiCancel, aiQueue: make(chan string, 1000), } for nodeID, token := range config.NodeTokens { if validIdentifier(nodeID, 200) && token != "" { s.nodeTokenHashes[nodeID] = sha256.Sum256([]byte(token)) } } for username, password := range config.WebUsers { if username != "" && password != "" { s.userPasswords[username] = sha256.Sum256([]byte(password)) } } s.aiWG.Add(2) go s.aiWorker() go s.aiScheduler() return s, nil } func (s *Server) Handler() http.Handler { return s } func (s *Server) ListenAndServe(ctx context.Context) error { server := &http.Server{Addr: s.config.ListenAddr, Handler: s, ReadHeaderTimeout: 5 * time.Second, ReadTimeout: 15 * time.Second, WriteTimeout: 15 * time.Second, IdleTimeout: 60 * time.Second} go func() { <-ctx.Done() shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() _ = server.Shutdown(shutdownCtx) }() defer func() { _ = s.Close() }() var err error if s.tlsConfig != nil { server.TLSConfig = s.tlsConfig err = server.ListenAndServeTLS("", "") } else { err = server.ListenAndServe() } if errors.Is(err, http.ErrServerClosed) { return nil } return err } // Close releases the active/passive store lock and stops background AI work. func (s *Server) Close() error { s.closeOnce.Do(func() { s.aiCancel() s.aiWG.Wait() s.closeErr = s.store.Close() }) return s.closeErr } func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { correlationID := r.Header.Get("X-Correlation-Id") if !validIdentifier(correlationID, 128) { correlationID = randomID() } w.Header().Set("X-Correlation-Id", correlationID) w.Header().Set("Cache-Control", "no-store") w.Header().Set("X-Content-Type-Options", "nosniff") w.Header().Set("Referrer-Policy", "no-referrer") if r.Method == http.MethodOptions { w.WriteHeader(http.StatusNoContent) return } var err error switch { case r.URL.Path == "/" && r.Method == http.MethodGet: s.serveFrontend(w, "dist/index.html") return case strings.HasPrefix(r.URL.Path, "/assets/") && r.Method == http.MethodGet: s.serveFrontend(w, path.Join("dist", strings.TrimPrefix(r.URL.Path, "/"))) return case r.URL.Path == "/healthz" && r.Method == http.MethodGet: writeJSON(w, http.StatusOK, map[string]any{"status": "ok", "protocol_version": ProtocolVersion, "correlation_id": correlationID}) return case r.URL.Path == "/readyz" && r.Method == http.MethodGet: err = s.ready(w, correlationID) case r.URL.Path == "/v1/auth/login" && r.Method == http.MethodPost: err = s.login(w, r, correlationID) case r.URL.Path == "/v1/nodes" && r.Method == http.MethodGet: err = s.listNodes(w, r) case r.URL.Path == "/v1/nodes/register" && r.Method == http.MethodPost: err = s.registerNode(w, r, correlationID) case strings.HasPrefix(r.URL.Path, "/v1/nodes/"): err = s.nodeRoute(w, r, correlationID) case strings.HasPrefix(r.URL.Path, "/v1/reads/"): err = s.readRoute(w, r, correlationID) case strings.HasPrefix(r.URL.Path, "/v1/ai/"): err = s.aiRoute(w, r, correlationID) case r.URL.Path == "/v1/tasks" || strings.HasPrefix(r.URL.Path, "/v1/tasks/"): err = s.taskRoute(w, r, correlationID) case r.URL.Path == "/v1/events": err = s.eventRoute(w, r, correlationID) case r.URL.Path == "/v1/audit" && r.Method == http.MethodGet: err = s.auditRoute(w, r, correlationID) default: err = requestError{status: http.StatusNotFound, code: "NotFound", message: "Resource was not found."} } if err != nil { writeError(w, err, correlationID) } } func (s *Server) ready(w http.ResponseWriter, correlationID string) error { if err := s.store.Read(func(PersistedState) error { return nil }); err != nil { return requestError{status: http.StatusServiceUnavailable, code: "NotReady", message: "The control plane store is not ready."} } writeJSON(w, http.StatusOK, map[string]any{"status": "ready", "protocol_version": ProtocolVersion, "correlation_id": correlationID}) return nil } func (s *Server) login(w http.ResponseWriter, r *http.Request, correlationID string) error { var request struct { Username string `json:"username"` Password string `json:"password"` } if err := decodeJSON(r, &request, 8*1024); err != nil { return err } password, exists := s.userPasswords[request.Username] provided := sha256.Sum256([]byte(request.Password)) if !exists || subtle.ConstantTimeCompare(password[:], provided[:]) != 1 { return requestError{status: http.StatusUnauthorized, code: "Unauthorized", message: "Authentication failed."} } token := randomID() s.sessionMu.Lock() s.sessions[token] = session{Username: request.Username, Expires: time.Now().UTC().Add(s.config.SessionTTL)} s.sessionMu.Unlock() _ = s.appendAudit(request.Username, "login", "session", correlationID, "success") writeJSON(w, http.StatusOK, map[string]any{"access_token": token, "token_type": "Bearer", "expires_in": int(s.config.SessionTTL.Seconds()), "correlation_id": correlationID}) return nil } func (s *Server) registerNode(w http.ResponseWriter, r *http.Request, correlationID string) error { nodeID, err := s.authenticateNode(r) if err != nil { return err } var request NodeRegistration if err := decodeJSON(r, &request, 128*1024); err != nil { return err } if request.NodeID != nodeID { return requestError{status: http.StatusForbidden, code: "NodeIdentityMismatch", message: "The token is not assigned to this node."} } if (request.ConnectionID != "" && !validIdentifier(request.ConnectionID, 200)) || !validIdentifier(request.AgentVersion, 80) || request.ProtocolVersion != ProtocolVersion || len(request.Capabilities) > 100 || !validCapabilities(request.Capabilities) || !validAccountSummaries(request.Accounts) { return requestError{status: http.StatusBadRequest, code: "InvalidRegistration", message: "Node registration is invalid."} } now := time.Now().UTC() if err := s.store.Mutate(func(state *PersistedState) error { node := state.Nodes[nodeID] if node.NodeID != "" && node.ConnectionID != request.ConnectionID { quarantineNodeTasks(state, nodeID, now) } node.NodeID = nodeID node.ConnectionID = request.ConnectionID node.AgentVersion = request.AgentVersion node.ProtocolVersion = request.ProtocolVersion node.Capabilities = append([]string(nil), request.Capabilities...) node.Status = NodeOnline node.LastHeartbeatAt = &now node.ReportingConfigVersion = request.ReportingConfigVersion node.Accounts = append([]AccountSummary(nil), request.Accounts...) node.LastCorrelationID = correlationID state.Nodes[nodeID] = node state.Audit = appendAudit(state.Audit, "node:"+nodeID, "node.register", nodeID, correlationID, "success", now) return nil }); err != nil { return requestError{status: http.StatusInternalServerError, code: "PersistenceFailed", message: "The control plane could not persist the node registration."} } writeJSON(w, http.StatusOK, NodeResponse{NodeID: nodeID, ConnectionID: request.ConnectionID, Status: NodeOnline, Authenticated: true, CorrelationID: correlationID}) return nil } func (s *Server) nodeRoute(w http.ResponseWriter, r *http.Request, correlationID string) error { parts := pathParts(r.URL.Path) if len(parts) < 3 || parts[0] != "v1" || parts[1] != "nodes" { return requestError{status: http.StatusNotFound, code: "NotFound", message: "Resource was not found."} } nodeID, err := s.authenticateNode(r) if err != nil { return err } if nodeID != parts[2] { return requestError{status: http.StatusForbidden, code: "NodeIdentityMismatch", message: "The token is not assigned to this node."} } if len(parts) == 3 && r.Method == http.MethodGet { return s.nodeDetails(w, nodeID, correlationID) } if len(parts) < 4 { return requestError{status: http.StatusNotFound, code: "NotFound", message: "Resource was not found."} } switch parts[3] { case "heartbeat": if r.Method != http.MethodPost || len(parts) != 4 { return requestError{status: http.StatusMethodNotAllowed, code: "MethodNotAllowed", message: "Method is not allowed."} } return s.heartbeat(w, r, nodeID, correlationID) case "tasks": return s.nodeTaskRoute(w, r, nodeID, parts[4:], correlationID) case "events": if r.Method != http.MethodPost || len(parts) != 4 { return requestError{status: http.StatusMethodNotAllowed, code: "MethodNotAllowed", message: "Method is not allowed."} } return s.ingestEvent(w, r, nodeID, correlationID) default: return requestError{status: http.StatusNotFound, code: "NotFound", message: "Resource was not found."} } } func (s *Server) listNodes(w http.ResponseWriter, r *http.Request) error { if _, err := s.authenticateWeb(r); err != nil { return err } var nodes []Node if err := s.store.Mutate(func(state *PersistedState) error { now := time.Now().UTC() for nodeID, value := range state.Nodes { node := value nodeChanged := false if node.LastHeartbeatAt == nil || now.Sub(*node.LastHeartbeatAt) > s.config.HeartbeatTimeout { if node.Status != NodeOffline { node.Status = NodeOffline nodeChanged = true } } if nodeChanged { quarantineNodeTasks(state, nodeID, now) state.Audit = appendAudit(state.Audit, "node:"+nodeID, "node.offline", nodeID, "", "success", now) state.Nodes[nodeID] = node } nodes = append(nodes, node) } return nil }); err != nil { return err } sort.Slice(nodes, func(i, j int) bool { return nodes[i].NodeID < nodes[j].NodeID }) writeJSON(w, http.StatusOK, map[string]any{"nodes": nodes}) return nil } func (s *Server) nodeDetails(w http.ResponseWriter, nodeID, _ string) error { var node Node if err := s.store.Read(func(state PersistedState) error { value, ok := state.Nodes[nodeID] if !ok { return requestError{status: http.StatusNotFound, code: "NodeNotFound", message: "Node was not registered."} } node = value return nil }); err != nil { return err } writeJSON(w, http.StatusOK, node) return nil } func (s *Server) heartbeat(w http.ResponseWriter, r *http.Request, nodeID, correlationID string) error { var request Heartbeat if err := decodeJSON(r, &request, 32*1024); err != nil { return err } if request.NodeID != nodeID || (request.ConnectionID != "" && !validIdentifier(request.ConnectionID, 200)) || request.ProtocolVersion != ProtocolVersion || !validIdentifier(request.AgentVersion, 80) || !validNodeStatus(request.NodeStatus) || request.QueueLength < 0 || request.QueueLength > 100000 { return requestError{status: http.StatusBadRequest, code: "InvalidHeartbeat", message: "Heartbeat is invalid."} } now := time.Now().UTC() if err := s.store.Mutate(func(state *PersistedState) error { node := state.Nodes[nodeID] if node.NodeID == "" { return requestError{status: http.StatusConflict, code: "NodeNotRegistered", message: "Register the node before sending a heartbeat."} } if node.ConnectionID != request.ConnectionID { return requestError{status: http.StatusConflict, code: "StaleConnection", message: "The heartbeat belongs to an older Client connection."} } node.AgentVersion = request.AgentVersion node.ProtocolVersion = request.ProtocolVersion node.Status = request.NodeStatus node.WechatRunning = request.WechatRunning node.WechatLoggedIn = request.WechatLoggedIn node.SessionLocked = request.SessionLocked node.ActiveAccountID = request.ActiveAccountID node.QueueLength = request.QueueLength node.ReportingConfigVersion = request.ReportingConfigVersion node.LastErrorCode = request.LastErrorCode node.LastHeartbeatAt = &now node.LastCorrelationID = correlationID state.Nodes[nodeID] = node state.Audit = appendAudit(state.Audit, "node:"+nodeID, "node.heartbeat", nodeID, correlationID, "success", now) return nil }); err != nil { return err } writeJSON(w, http.StatusOK, NodeResponse{NodeID: nodeID, ConnectionID: request.ConnectionID, Status: request.NodeStatus, LastHeartbeatAt: &now, CorrelationID: correlationID}) return nil } func (s *Server) nodeTaskRoute(w http.ResponseWriter, r *http.Request, nodeID string, parts []string, correlationID string) error { if len(parts) == 0 && r.Method == http.MethodGet { return s.pollTasks(w, r, nodeID, correlationID) } if len(parts) != 2 || r.Method != http.MethodPost { return requestError{status: http.StatusNotFound, code: "NotFound", message: "Resource was not found."} } taskID := parts[0] switch parts[1] { case "ack": return s.ackTask(w, r, nodeID, taskID, correlationID) case "start": return s.startTask(w, r, nodeID, taskID, correlationID) case "renew": return s.renewTask(w, r, nodeID, taskID, correlationID) case "result": return s.recordTaskResult(w, r, nodeID, taskID, correlationID) default: return requestError{status: http.StatusNotFound, code: "NotFound", message: "Resource was not found."} } } func (s *Server) pollTasks(w http.ResponseWriter, r *http.Request, nodeID, correlationID string) error { waitSeconds := queryWaitSeconds(r.URL.Query().Get("wait_seconds")) deadline := time.Now().Add(time.Duration(waitSeconds) * time.Second) for { tasks, err := s.claimTasks(r, nodeID, correlationID) if err != nil { return err } if len(tasks) > 0 || waitSeconds == 0 || !time.Now().Before(deadline) { writeJSON(w, http.StatusOK, TaskBatch{Tasks: tasks}) return nil } select { case <-r.Context().Done(): return nil case <-time.After(250 * time.Millisecond): } } } func (s *Server) claimTasks(r *http.Request, nodeID, correlationID string) ([]Task, error) { accountID := r.URL.Query().Get("account_id") if accountID != "" && !validIdentifier(accountID, 200) { return nil, requestError{status: http.StatusBadRequest, code: "InvalidAccount", message: "account_id is invalid."} } now := time.Now().UTC() var tasks []Task if err := s.store.Mutate(func(state *PersistedState) error { for taskID, value := range state.Tasks { task := value if task.NodeID != nodeID || (accountID != "" && task.AccountID != accountID) || terminal(task.Status) { continue } changed := false if task.NotAfter != nil && !now.Before(*task.NotAfter) { if task.Status == TaskPending { task.Status = TaskExpired task.StateVersion++ task.UpdatedAt = now task.LeaseOwner = "" task.LeaseExpiresAt = nil changed = true } else if task.Status == TaskAccepted || task.Status == TaskRunning { markUnconfirmed(&task, "TaskDeadlineReached", now) changed = true } } if terminal(task.Status) { if changed { state.Tasks[taskID] = task } continue } if task.CancelRequestedAt != nil && task.Status == TaskPending && !leaseActive(task, now) { task.Status = TaskCancelled task.StateVersion++ task.UpdatedAt = now task.LeaseOwner = "" task.LeaseExpiresAt = nil changed = true } if terminal(task.Status) { if changed { state.Tasks[taskID] = task } continue } if task.Status == TaskPending { if leaseActive(task, now) && task.LeaseOwner != nodeID { continue } if !leaseActive(task, now) { task.LeaseGeneration++ expires := now.Add(s.config.LeaseTTL) task.LeaseExpiresAt = &expires task.LeaseOwner = nodeID task.UpdatedAt = now task.LastCorrelationID = correlationID changed = true } } if task.Status == TaskAccepted || task.Status == TaskRunning { if task.LeaseOwner != nodeID || !leaseActive(task, now) { if task.LeaseOwner == nodeID && !leaseActive(task, now) { markUnconfirmed(&task, "LeaseExpired", now) changed = true } if changed { state.Tasks[taskID] = task } continue } } if changed { state.Tasks[taskID] = task } if task.Status == TaskPending || task.Status == TaskAccepted || task.Status == TaskRunning { tasks = append(tasks, task) } } state.Audit = appendAudit(state.Audit, "node:"+nodeID, "task.poll", nodeID, correlationID, "success", now) return nil }); err != nil { return nil, err } sort.Slice(tasks, func(i, j int) bool { return tasks[i].CreatedAt.Before(tasks[j].CreatedAt) }) return tasks, nil } func (s *Server) ackTask(w http.ResponseWriter, r *http.Request, nodeID, taskID, correlationID string) error { var request TaskAck if err := decodeJSON(r, &request, 8*1024); err != nil { return err } if request.TaskID != taskID { return requestError{status: http.StatusBadRequest, code: "TaskIdentityMismatch", message: "Task identity does not match the path."} } var task Task err := s.store.Mutate(func(state *PersistedState) error { value, ok := state.Tasks[taskID] if !ok || value.NodeID != nodeID { return requestError{status: http.StatusNotFound, code: "TaskNotFound", message: "Task was not found."} } task = value if task.AccountID != request.AccountID { return requestError{status: http.StatusForbidden, code: "AccountMismatch", message: "Task account does not match."} } if terminal(task.Status) { return nil } if task.LeaseGeneration != request.LeaseGeneration || task.LeaseOwner != nodeID || !leaseActive(task, time.Now().UTC()) { return requestError{status: http.StatusConflict, code: "LeaseMismatch", message: "The task lease is no longer valid."} } if task.Status != TaskPending { if task.Status == TaskAccepted { return nil } return requestError{status: http.StatusConflict, code: "InvalidTaskState", message: "The task cannot be accepted in its current state."} } if task.CancelRequestedAt != nil { task.Status = TaskCancelled task.StateVersion++ task.LeaseOwner = "" task.LeaseExpiresAt = nil } else { task.Status = TaskAccepted task.StateVersion++ } task.UpdatedAt = time.Now().UTC() task.LastCorrelationID = correlationID state.Tasks[taskID] = task state.Audit = appendAudit(state.Audit, "node:"+nodeID, "task.accept", taskID, correlationID, "success", task.UpdatedAt) return nil }) if err != nil { return err } writeJSON(w, http.StatusOK, task) return nil } func (s *Server) startTask(w http.ResponseWriter, r *http.Request, nodeID, taskID, correlationID string) error { var request TaskAck if err := decodeJSON(r, &request, 8*1024); err != nil { return err } if request.TaskID != taskID { return requestError{status: http.StatusBadRequest, code: "TaskIdentityMismatch", message: "Task identity does not match the path."} } var task Task err := s.store.Mutate(func(state *PersistedState) error { value, ok := state.Tasks[taskID] if !ok || value.NodeID != nodeID { return requestError{status: http.StatusNotFound, code: "TaskNotFound", message: "Task was not found."} } task = value if task.AccountID != request.AccountID || task.LeaseGeneration != request.LeaseGeneration || task.LeaseOwner != nodeID { return requestError{status: http.StatusConflict, code: "LeaseMismatch", message: "The task lease is no longer valid."} } now := time.Now().UTC() if terminal(task.Status) { return nil } if task.Status != TaskAccepted { return requestError{status: http.StatusConflict, code: "InvalidTaskState", message: "The task must be accepted before execution."} } if task.CancelRequestedAt != nil { task.Status = TaskCancelled task.StateVersion++ task.LeaseOwner = "" task.LeaseExpiresAt = nil } else if task.NotAfter != nil && !now.Before(*task.NotAfter) { task.Status = TaskExpired task.StateVersion++ task.LeaseOwner = "" task.LeaseExpiresAt = nil } else if !leaseActive(task, now) { return requestError{status: http.StatusConflict, code: "LeaseExpired", message: "The task lease expired before execution started."} } else { task.Status = TaskRunning task.StateVersion++ } task.UpdatedAt = now task.LastCorrelationID = correlationID state.Tasks[taskID] = task state.Audit = appendAudit(state.Audit, "node:"+nodeID, "task.start", taskID, correlationID, "success", now) return nil }) if err != nil { return err } writeJSON(w, http.StatusOK, task) return nil } func (s *Server) renewTask(w http.ResponseWriter, r *http.Request, nodeID, taskID, correlationID string) error { var request TaskAck if err := decodeJSON(r, &request, 8*1024); err != nil { return err } if request.TaskID != taskID { return requestError{status: http.StatusBadRequest, code: "TaskIdentityMismatch", message: "Task identity does not match the path."} } var task Task if err := s.store.Mutate(func(state *PersistedState) error { value, ok := state.Tasks[taskID] if !ok || value.NodeID != nodeID { return requestError{status: http.StatusNotFound, code: "TaskNotFound", message: "Task was not found."} } task = value if task.AccountID != request.AccountID || task.LeaseGeneration != request.LeaseGeneration || task.LeaseOwner != nodeID { return requestError{status: http.StatusConflict, code: "LeaseMismatch", message: "The task lease is no longer valid."} } if task.CancelRequestedAt != nil { return requestError{status: http.StatusConflict, code: "CancelRequested", message: "The task lease cannot be renewed after cancellation was requested."} } now := time.Now().UTC() if task.Status != TaskAccepted && task.Status != TaskRunning || !leaseActive(task, now) { return requestError{status: http.StatusConflict, code: "LeaseExpired", message: "The task lease is no longer renewable."} } expires := now.Add(s.config.LeaseTTL) task.LeaseExpiresAt = &expires task.StateVersion++ task.UpdatedAt = now task.LastCorrelationID = correlationID state.Tasks[taskID] = task state.Audit = appendAudit(state.Audit, "node:"+nodeID, "task.renew", taskID, correlationID, "success", now) return nil }); err != nil { return err } writeJSON(w, http.StatusOK, task) return nil } func (s *Server) recordTaskResult(w http.ResponseWriter, r *http.Request, nodeID, taskID, correlationID string) error { var result TaskResult if err := decodeJSON(r, &result, MaxTaskResultBytes+32*1024); err != nil { return err } if result.TaskID != taskID || !terminal(result.Status) { return requestError{status: http.StatusBadRequest, code: "InvalidTaskResult", message: "Task result or terminal status is invalid."} } if len(result.Content) > MaxTaskResultBytes { return requestError{status: http.StatusRequestEntityTooLarge, code: "ContentTooLarge", message: "The task result content is too large."} } var task Task err := s.store.Mutate(func(state *PersistedState) error { value, ok := state.Tasks[taskID] if !ok || value.NodeID != nodeID { return requestError{status: http.StatusNotFound, code: "TaskNotFound", message: "Task was not found."} } task = value if task.AccountID != result.AccountID { return requestError{status: http.StatusForbidden, code: "AccountMismatch", message: "Task account does not match."} } if hasTaskContent(result.Content) && !isReadTaskKind(task.Kind) { return requestError{status: http.StatusForbidden, code: "ContentNotAllowed", message: "Only read task results may contain content."} } if terminal(task.Status) { if task.Result != nil && task.Result.Status == result.Status && task.Result.ErrorCode == result.ErrorCode { return nil } return requestError{status: http.StatusConflict, code: "TerminalState", message: "The task already has a different terminal result."} } if task.LeaseGeneration != result.LeaseGeneration || task.LeaseOwner != nodeID { return requestError{status: http.StatusConflict, code: "LeaseMismatch", message: "The result belongs to an expired task lease."} } if task.Status != TaskAccepted && task.Status != TaskRunning { return requestError{status: http.StatusConflict, code: "InvalidTaskState", message: "The task is not awaiting a final result."} } result.CorrelationID = firstNonEmpty(result.CorrelationID, correlationID) task.Result = &result task.Status = result.Status task.StateVersion++ task.UpdatedAt = time.Now().UTC() task.LeaseOwner = "" task.LeaseExpiresAt = nil task.LastCorrelationID = correlationID state.Tasks[taskID] = task state.Audit = appendAudit(state.Audit, "node:"+nodeID, "task.result", taskID, correlationID, "success", task.UpdatedAt) return nil }) if err != nil { return err } if task.Kind == "read-messages" { go s.handleAITaskResult(taskID) } writeJSON(w, http.StatusOK, task) return nil } func (s *Server) readRoute(w http.ResponseWriter, r *http.Request, correlationID string) error { username, err := s.authenticateWeb(r) if err != nil { return err } parts := pathParts(r.URL.Path) if len(parts) != 3 || parts[0] != "v1" || parts[1] != "reads" || r.Method != http.MethodPost { return requestError{status: http.StatusNotFound, code: "NotFound", message: "Resource was not found."} } kind := "read-" + parts[2] if !isReadTaskKind(kind) { return requestError{status: http.StatusNotFound, code: "NotFound", message: "Resource was not found."} } var request ReadTaskSubmission if err := decodeJSON(r, &request, 64*1024); err != nil { return err } payload, err := readTaskPayload(kind, request) if err != nil { return err } return s.createTaskSubmission(w, TaskSubmission{ NodeID: request.NodeID, AccountID: request.AccountID, Kind: kind, IdempotencyKey: request.IdempotencyKey, Payload: payload, NotAfter: request.NotAfter, }, username, correlationID) } type readSessionsPayload struct { Limit int `json:"limit"` Offset int `json:"offset"` } type readContactsPayload struct { Limit int `json:"limit"` Offset int `json:"offset"` Contains string `json:"contains,omitempty"` GroupsOnly bool `json:"groups_only"` } type readMessagesPayload struct { Limit int `json:"limit"` Offset int `json:"offset"` ChatID string `json:"chat_id"` IncludeContent bool `json:"include_content"` } func isReadTaskKind(kind string) bool { switch kind { case "read-sessions", "read-contacts", "read-messages": return true default: return false } } func readTaskPayload(kind string, request ReadTaskSubmission) (jsonRaw, error) { limit := request.Limit if limit == 0 { limit = 50 } if limit < 1 || limit > 200 || request.Offset < 0 { return nil, requestError{status: http.StatusBadRequest, code: "InvalidPagination", message: "limit must be 1..200 and offset must be non-negative."} } if request.Contains != "" && !validIdentifier(request.Contains, 200) { return nil, requestError{status: http.StatusBadRequest, code: "InvalidReadRequest", message: "contains is invalid."} } switch kind { case "read-sessions": if request.Contains != "" || request.GroupsOnly != nil || request.ChatID != "" || request.IncludeContent { return nil, requestError{status: http.StatusBadRequest, code: "InvalidReadRequest", message: "The sessions read request contains unsupported fields."} } return json.Marshal(readSessionsPayload{Limit: limit, Offset: request.Offset}) case "read-contacts": if request.GroupsOnly == nil || request.ChatID != "" || request.IncludeContent { return nil, requestError{status: http.StatusBadRequest, code: "InvalidReadRequest", message: "contacts requires groups_only and does not accept chat_id or include_content."} } return json.Marshal(readContactsPayload{Limit: limit, Offset: request.Offset, Contains: request.Contains, GroupsOnly: *request.GroupsOnly}) case "read-messages": if !validIdentifier(request.ChatID, 512) || request.GroupsOnly != nil || request.Contains != "" { return nil, requestError{status: http.StatusBadRequest, code: "InvalidReadRequest", message: "messages requires a stable chat_id and does not accept contains or groups_only."} } return json.Marshal(readMessagesPayload{Limit: limit, Offset: request.Offset, ChatID: request.ChatID, IncludeContent: request.IncludeContent}) default: return nil, requestError{status: http.StatusNotFound, code: "NotFound", message: "Resource was not found."} } } func validTaskPayload(kind string, payload jsonRaw) bool { if kind == "send-text" { return validSendTextPayload(payload) } if !isReadTaskKind(kind) || len(payload) == 0 || len(payload) > 64*1024 || !json.Valid(payload) { return false } switch kind { case "read-sessions": var value readSessionsPayload return decodeRaw(payload, &value) && validPagination(value.Limit, value.Offset) case "read-contacts": var value readContactsPayload return decodeRaw(payload, &value) && validPagination(value.Limit, value.Offset) && (value.Contains == "" || validIdentifier(value.Contains, 200)) case "read-messages": var value readMessagesPayload return decodeRaw(payload, &value) && validPagination(value.Limit, value.Offset) && validIdentifier(value.ChatID, 512) default: return false } } func decodeRaw(payload jsonRaw, target any) bool { decoder := json.NewDecoder(strings.NewReader(string(payload))) decoder.DisallowUnknownFields() if decoder.Decode(target) != nil { return false } var extra any return decoder.Decode(&extra) == io.EOF } func validPagination(limit, offset int) bool { return limit >= 1 && limit <= 200 && offset >= 0 } func hasTaskContent(content jsonRaw) bool { return len(content) != 0 && string(content) != "null" } func (s *Server) taskRoute(w http.ResponseWriter, r *http.Request, correlationID string) error { username, err := s.authenticateWeb(r) if err != nil { return err } parts := pathParts(r.URL.Path) if len(parts) == 2 && parts[0] == "v1" && parts[1] == "tasks" { switch r.Method { case http.MethodGet: return s.listTasks(w, r) case http.MethodPost: return s.createTask(w, r, username, correlationID) default: return requestError{status: http.StatusMethodNotAllowed, code: "MethodNotAllowed", message: "Method is not allowed."} } } if len(parts) == 3 && r.Method == http.MethodGet { return s.getTask(w, parts[2]) } if len(parts) == 4 && parts[3] == "cancel" && r.Method == http.MethodPost { return s.cancelTask(w, parts[2], username, correlationID) } if len(parts) == 4 && parts[3] == "resume" && r.Method == http.MethodPost { return s.resumeTask(w, parts[2], username, correlationID) } return requestError{status: http.StatusNotFound, code: "NotFound", message: "Resource was not found."} } func (s *Server) createTask(w http.ResponseWriter, r *http.Request, username, correlationID string) error { var request TaskSubmission if err := decodeJSON(r, &request, 128*1024); err != nil { return err } return s.createTaskSubmission(w, request, username, correlationID) } func (s *Server) createTaskSubmission(w http.ResponseWriter, request TaskSubmission, username, correlationID string) error { if !validIdentifier(request.NodeID, 200) || !validIdentifier(request.AccountID, 200) || !validIdentifier(request.Kind, 80) || !validIdentifier(request.IdempotencyKey, 128) { return requestError{status: http.StatusBadRequest, code: "InvalidTask", message: "Task identity fields are invalid."} } if !validTaskPayload(request.Kind, request.Payload) { return requestError{status: http.StatusBadRequest, code: "UnsupportedTask", message: "The task kind or payload is not supported."} } if request.NotAfter != nil && !request.NotAfter.After(time.Now().UTC()) { return requestError{status: http.StatusBadRequest, code: "InvalidDeadline", message: "not_after must be in the future."} } var response TaskSubmissionResponse err := s.store.Mutate(func(state *PersistedState) error { node, registered := state.Nodes[request.NodeID] if !registered { return requestError{status: http.StatusConflict, code: "NodeNotReady", message: "The target node is not registered."} } if !nodeHasAccount(node, request.AccountID) { return requestError{status: http.StatusConflict, code: "AccountNotReady", message: "The target account is not currently registered on the node."} } payloadKey := string(request.Payload) for _, existing := range state.Tasks { if existing.NodeID != request.NodeID || existing.AccountID != request.AccountID || existing.IdempotencyKey != request.IdempotencyKey { continue } if existing.Kind != request.Kind || string(existing.Payload) != payloadKey { return requestError{status: http.StatusConflict, code: "IdempotencyConflict", message: "The idempotency key is already bound to different task parameters."} } response = TaskSubmissionResponse{TaskID: existing.TaskID, Status: existing.Status, Duplicate: true, StateVersion: existing.StateVersion} return nil } now := time.Now().UTC() status := TaskPending if !nodeAvailable(node, now, s.config.HeartbeatTimeout) { status = TaskWaitingForClient } task := Task{ TaskID: randomID(), NodeID: request.NodeID, AccountID: request.AccountID, Kind: request.Kind, IdempotencyKey: request.IdempotencyKey, Payload: append(jsonRaw(nil), request.Payload...), NotAfter: request.NotAfter, Status: status, StateVersion: 1, CreatedAt: now, UpdatedAt: now, LastCorrelationID: correlationID, } state.Tasks[task.TaskID] = task state.Audit = appendAudit(state.Audit, "user:"+username, "task.create", task.TaskID, correlationID, "success", now) response = TaskSubmissionResponse{TaskID: task.TaskID, Status: task.Status, Duplicate: false, StateVersion: task.StateVersion} return nil }) if err != nil { return err } writeJSON(w, http.StatusAccepted, response) return nil } func (s *Server) listTasks(w http.ResponseWriter, r *http.Request) error { nodeID := r.URL.Query().Get("node_id") accountID := r.URL.Query().Get("account_id") limit := queryLimit(r.URL.Query().Get("limit")) var tasks []Task if err := s.store.Read(func(state PersistedState) error { for _, task := range state.Tasks { if nodeID != "" && task.NodeID != nodeID || accountID != "" && task.AccountID != accountID { continue } tasks = append(tasks, task) } return nil }); err != nil { return err } sort.Slice(tasks, func(i, j int) bool { return tasks[i].CreatedAt.After(tasks[j].CreatedAt) }) if len(tasks) > limit { tasks = tasks[:limit] } writeJSON(w, http.StatusOK, map[string]any{"tasks": tasks}) return nil } func (s *Server) getTask(w http.ResponseWriter, taskID string) error { if !validIdentifier(taskID, 200) { return requestError{status: http.StatusBadRequest, code: "InvalidTask", message: "Task ID is invalid."} } var task Task if err := s.store.Read(func(state PersistedState) error { value, ok := state.Tasks[taskID] if !ok { return requestError{status: http.StatusNotFound, code: "TaskNotFound", message: "Task was not found."} } task = value return nil }); err != nil { return err } writeJSON(w, http.StatusOK, task) return nil } func (s *Server) cancelTask(w http.ResponseWriter, taskID, username, correlationID string) error { var task Task err := s.store.Mutate(func(state *PersistedState) error { value, ok := state.Tasks[taskID] if !ok { return requestError{status: http.StatusNotFound, code: "TaskNotFound", message: "Task was not found."} } task = value if terminal(task.Status) { return nil } now := time.Now().UTC() if task.CancelRequestedAt == nil { task.CancelRequestedAt = &now task.StateVersion++ if (task.Status == TaskPending || task.Status == TaskWaitingForClient) && !leaseActive(task, now) { task.Status = TaskCancelled task.LeaseOwner = "" task.LeaseExpiresAt = nil } task.UpdatedAt = now state.Audit = appendAudit(state.Audit, "user:"+username, "task.cancel-request", taskID, correlationID, "success", now) state.Tasks[taskID] = task } return nil }) if err != nil { return err } writeJSON(w, http.StatusOK, task) return nil } func (s *Server) resumeTask(w http.ResponseWriter, taskID, username, correlationID string) error { var task Task err := s.store.Mutate(func(state *PersistedState) error { value, ok := state.Tasks[taskID] if !ok { return requestError{status: http.StatusNotFound, code: "TaskNotFound", message: "Task was not found."} } task = value if terminal(task.Status) { return requestError{status: http.StatusConflict, code: "TerminalState", message: "The task cannot be resumed after it reached a terminal state."} } if task.Status != TaskWaitingForClient { return requestError{status: http.StatusConflict, code: "InvalidTaskState", message: "Only tasks waiting for a Client can be resumed."} } if task.CancelRequestedAt != nil { return requestError{status: http.StatusConflict, code: "CancelRequested", message: "The task was cancelled and cannot be resumed."} } node, exists := state.Nodes[task.NodeID] now := time.Now().UTC() if !exists || !nodeAvailable(node, now, s.config.HeartbeatTimeout) || !nodeHasAccount(node, task.AccountID) { return requestError{status: http.StatusConflict, code: "ClientNotReady", message: "The target Client is not connected with the requested account."} } if task.NotAfter != nil && !now.Before(*task.NotAfter) { task.Status = TaskExpired task.StateVersion++ task.UpdatedAt = now } else { task.Status = TaskPending task.StateVersion++ task.UpdatedAt = now task.LastCorrelationID = correlationID } state.Tasks[taskID] = task state.Audit = appendAudit(state.Audit, "user:"+username, "task.resume", taskID, correlationID, "success", now) return nil }) if err != nil { return err } writeJSON(w, http.StatusOK, task) return nil } func (s *Server) eventRoute(w http.ResponseWriter, r *http.Request, _ string) error { if r.Method != http.MethodGet { return requestError{status: http.StatusMethodNotAllowed, code: "MethodNotAllowed", message: "Method is not allowed."} } if _, err := s.authenticateWeb(r); err != nil { return err } return s.listEvents(w, r) } func (s *Server) ingestEvent(w http.ResponseWriter, r *http.Request, nodeID, correlationID string) error { var event MessageEvent if err := decodeJSON(r, &event, 64*1024); err != nil { return err } if event.NodeID != nodeID || !validIdentifier(event.AccountID, 200) || !validIdentifier(event.ChatID, 512) || event.EventSeq <= 0 || !validIdentifier(event.EventType, 80) || event.OccurredAt.IsZero() || event.ConfigVersion <= 0 || event.AuthorizationVersion <= 0 { return requestError{status: http.StatusBadRequest, code: "InvalidEvent", message: "Event identity or authorization metadata is invalid."} } if event.ChatType != ChatGroup && event.ChatType != ChatPrivate { return requestError{status: http.StatusBadRequest, code: "InvalidChatType", message: "Only group and private chat events are supported."} } if event.EventType != "message" { return requestError{status: http.StatusBadRequest, code: "UnsupportedEvent", message: "Only message events are supported in v1."} } if len(event.Content) > 16*1024 { return requestError{status: http.StatusBadRequest, code: "EventTooLarge", message: "Event content is too large."} } if !event.Authorized { return requestError{status: http.StatusForbidden, code: "ReportingNotAuthorized", message: "The node did not authorize this event."} } contentHash := eventHash(event) var receipt EventReceipt var stored StoredEvent err := s.store.Mutate(func(state *PersistedState) error { for _, existing := range state.Events { if existing.NodeID != event.NodeID || existing.AccountID != event.AccountID || existing.ChatID != event.ChatID || existing.EventSeq != event.EventSeq { continue } if existing.ContentHash != contentHash { return requestError{status: http.StatusConflict, code: "EventIdempotencyConflict", message: "The event sequence is already bound to different content."} } receipt = EventReceipt{Accepted: true, Duplicate: true, EventID: existing.EventID} return nil } now := time.Now().UTC() stored = StoredEvent{MessageEvent: event, EventID: randomID(), ContentHash: contentHash, ReceivedAt: now} state.Events = append(state.Events, stored) if len(state.Events) > 10000 { state.Events = state.Events[len(state.Events)-10000:] } state.Audit = appendAudit(state.Audit, "node:"+nodeID, "event.accept", event.AccountID+":"+event.ChatID, correlationID, "success", now) receipt = EventReceipt{Accepted: true, EventID: stored.EventID} return nil }) if err != nil { return err } if !receipt.Duplicate { go s.enqueueEventRuns(stored) } writeJSON(w, http.StatusAccepted, receipt) return nil } func (s *Server) listEvents(w http.ResponseWriter, r *http.Request) error { query := r.URL.Query() nodeID, accountID, chatID := query.Get("node_id"), query.Get("account_id"), query.Get("chat_id") limit := queryLimit(query.Get("limit")) var events []StoredEvent if err := s.store.Read(func(state PersistedState) error { for index := len(state.Events) - 1; index >= 0 && len(events) < limit; index-- { event := state.Events[index] if nodeID != "" && event.NodeID != nodeID || accountID != "" && event.AccountID != accountID || chatID != "" && event.ChatID != chatID { continue } events = append(events, event) } return nil }); err != nil { return err } writeJSON(w, http.StatusOK, map[string]any{"events": events}) return nil } func (s *Server) auditRoute(w http.ResponseWriter, r *http.Request, _ string) error { if _, err := s.authenticateWeb(r); err != nil { return err } limit := queryLimit(r.URL.Query().Get("limit")) var audit []AuditEntry if err := s.store.Read(func(state PersistedState) error { for index := len(state.Audit) - 1; index >= 0 && len(audit) < limit; index-- { audit = append(audit, state.Audit[index]) } return nil }); err != nil { return err } writeJSON(w, http.StatusOK, map[string]any{"audit": audit}) return nil } func (s *Server) authenticateNode(r *http.Request) (string, error) { if !s.clientCertificateAllowed(r) { return "", requestError{status: http.StatusUnauthorized, code: "Unauthorized", message: "Node certificate authentication failed."} } token := bearerToken(r) if token == "" { return "", requestError{status: http.StatusUnauthorized, code: "Unauthorized", message: "Node authentication is required."} } provided := sha256.Sum256([]byte(token)) for nodeID, expected := range s.nodeTokenHashes { if subtle.ConstantTimeCompare(provided[:], expected[:]) == 1 { return nodeID, nil } } return "", requestError{status: http.StatusUnauthorized, code: "Unauthorized", message: "Node authentication failed."} } func (s *Server) authenticateWeb(r *http.Request) (string, error) { token := bearerToken(r) if token == "" { return "", requestError{status: http.StatusUnauthorized, code: "Unauthorized", message: "Web authentication is required."} } now := time.Now().UTC() s.sessionMu.Lock() value, ok := s.sessions[token] if ok && !now.Before(value.Expires) { delete(s.sessions, token) ok = false } s.sessionMu.Unlock() if !ok { return "", requestError{status: http.StatusUnauthorized, code: "Unauthorized", message: "Web session is missing or expired."} } return value.Username, nil } func (s *Server) appendAudit(principal, action, resource, correlationID, outcome string) error { return s.store.Mutate(func(state *PersistedState) error { state.Audit = appendAudit(state.Audit, principal, action, resource, correlationID, outcome, time.Now().UTC()) return nil }) } func (s *Server) serveFrontend(w http.ResponseWriter, name string) { if name == "" || name == "." || strings.Contains(name, "..") { writeError(w, requestError{status: http.StatusNotFound, code: "NotFound", message: "Resource was not found."}, randomID()) return } content, err := fs.ReadFile(webassets.Dist, name) if err != nil { writeError(w, requestError{status: http.StatusNotFound, code: "NotFound", message: "Resource was not found."}, randomID()) return } contentType := mime.TypeByExtension(path.Ext(name)) if contentType == "" { contentType = "application/octet-stream" } w.Header().Set("Content-Type", contentType+"; charset=utf-8") w.Header().Set("Content-Security-Policy", "default-src 'self'; script-src 'self'; style-src 'self'; connect-src 'self'; frame-ancestors 'none'; base-uri 'none'; form-action 'self'") w.WriteHeader(http.StatusOK) _, _ = w.Write(content) } func decodeJSON(r *http.Request, target any, maxBytes int64) error { defer r.Body.Close() decoder := json.NewDecoder(io.LimitReader(r.Body, maxBytes)) decoder.DisallowUnknownFields() if err := decoder.Decode(target); err != nil { return requestError{status: http.StatusBadRequest, code: "InvalidRequest", message: "Request JSON is invalid."} } var extra any if decoder.Decode(&extra) != io.EOF { return requestError{status: http.StatusBadRequest, code: "InvalidRequest", message: "Request must contain one JSON value."} } return nil } func writeJSON(w http.ResponseWriter, status int, value any) { w.Header().Set("Content-Type", "application/json; charset=utf-8") w.WriteHeader(status) _ = json.NewEncoder(w).Encode(value) } func writeError(w http.ResponseWriter, err error, correlationID string) { requestErr, ok := err.(requestError) if !ok { requestErr = requestError{status: http.StatusInternalServerError, code: "InternalError", message: "Request failed; use the correlation ID for diagnosis."} } writeJSON(w, requestErr.status, map[string]any{ "error": map[string]string{"code": requestErr.code, "message": requestErr.message}, "correlation_id": correlationID, }) } func pathParts(path string) []string { trimmed := strings.Trim(path, "/") if trimmed == "" { return nil } parts := strings.Split(trimmed, "/") for index, part := range parts { decoded, err := url.PathUnescape(part) if err == nil { parts[index] = decoded } } return parts } func bearerToken(r *http.Request) string { value := r.Header.Get("Authorization") if len(value) < 8 || !strings.EqualFold(value[:7], "Bearer ") { return "" } return strings.TrimSpace(value[7:]) } func validIdentifier(value string, max int) bool { return value != "" && len(value) <= max && !strings.ContainsAny(value, "\r\n\x00") } func terminal(status TaskStatus) bool { switch status { case TaskSucceeded, TaskFailed, TaskCancelled, TaskExpired, TaskResultUnconfirmed: return true default: return false } } func leaseActive(task Task, now time.Time) bool { return task.LeaseOwner != "" && task.LeaseExpiresAt != nil && now.Before(*task.LeaseExpiresAt) } func markUnconfirmed(task *Task, reason string, now time.Time) { task.Status = TaskResultUnconfirmed task.StateVersion++ task.UpdatedAt = now task.LeaseOwner = "" task.LeaseExpiresAt = nil task.Result = &TaskResult{TaskID: task.TaskID, AccountID: task.AccountID, LeaseGeneration: task.LeaseGeneration, Status: TaskResultUnconfirmed, ErrorCode: reason, Message: "The control plane could not prove that the previous executor stopped before the lease expired.", CorrelationID: task.LastCorrelationID} } func quarantineNodeTasks(state *PersistedState, nodeID string, now time.Time) { for taskID, value := range state.Tasks { if value.NodeID != nodeID || terminal(value.Status) { continue } task := value switch task.Status { case TaskPending, TaskWaitingForClient: if task.Status == TaskWaitingForClient && task.LeaseOwner == "" && task.LeaseExpiresAt == nil { continue } task.Status = TaskWaitingForClient task.StateVersion++ task.UpdatedAt = now task.LeaseOwner = "" task.LeaseExpiresAt = nil case TaskAccepted, TaskRunning: markUnconfirmed(&task, "ClientDisconnected", now) default: continue } state.Tasks[taskID] = task } } func validCapabilities(capabilities []string) bool { for _, capability := range capabilities { if !validIdentifier(capability, 80) { return false } } return true } func validAccountSummaries(accounts []AccountSummary) bool { seen := make(map[string]struct{}, len(accounts)) for _, account := range accounts { if !validIdentifier(account.AccountID, 200) || account.AllowedGroupCount < 0 || account.AllowedPrivateCount < 0 { return false } if _, exists := seen[account.AccountID]; exists { return false } seen[account.AccountID] = struct{}{} } return len(accounts) <= 100 } func validNodeStatus(status NodeStatus) bool { switch status { case NodeRegistered, NodeOnline, NodeDegraded, NodeOffline, NodeSessionLocked, NodeWechatNotRunning, NodeWechatNotLogged: return true default: return false } } func nodeHasAccount(node Node, accountID string) bool { for _, account := range node.Accounts { if account.AccountID == accountID && account.Active && account.Verified { return true } } return false } func nodeAvailable(node Node, now time.Time, heartbeatTimeout time.Duration) bool { return node.Status != NodeOffline && node.LastHeartbeatAt != nil && now.Sub(*node.LastHeartbeatAt) <= heartbeatTimeout } func validSendTextPayload(payload jsonRaw) bool { if len(payload) == 0 || len(payload) > 64*1024 || !json.Valid(payload) { return false } var value struct { TargetID string `json:"target_id"` Text string `json:"text"` Confirmed bool `json:"confirmed"` } if json.Unmarshal(payload, &value) != nil { return false } return validIdentifier(value.TargetID, 512) && len(value.Text) > 0 && len(value.Text) <= 4000 && value.Confirmed } func eventHash(event MessageEvent) string { value := struct { NodeID string `json:"node_id"` AccountID string `json:"account_id"` ChatID string `json:"chat_id"` ChatType ChatType `json:"chat_type"` EventSeq int64 `json:"event_seq"` EventType string `json:"event_type"` OccurredAt time.Time `json:"occurred_at"` Content string `json:"content"` ConfigVersion int64 `json:"config_version"` }{event.NodeID, event.AccountID, event.ChatID, event.ChatType, event.EventSeq, event.EventType, event.OccurredAt, event.Content, event.ConfigVersion} data, _ := json.Marshal(value) digest := sha256.Sum256(data) return hex.EncodeToString(digest[:]) } func appendAudit(entries []AuditEntry, principal, action, resource, correlationID, outcome string, at time.Time) []AuditEntry { entries = append(entries, AuditEntry{ID: randomID(), At: at, Principal: principal, Action: action, Resource: resource, CorrelationID: correlationID, Outcome: outcome}) if len(entries) > 10000 { entries = entries[len(entries)-10000:] } return entries } func queryLimit(raw string) int { if value, err := strconv.Atoi(raw); err == nil && value >= 1 && value <= 200 { return value } return 50 } func queryWaitSeconds(raw string) int { if value, err := strconv.Atoi(raw); err == nil && value >= 1 && value <= 30 { return value } return 0 } func firstNonEmpty(value, fallback string) string { if value != "" { return value } return fallback } func randomID() string { buffer := make([]byte, 16) if _, err := rand.Read(buffer); err != nil { return fmt.Sprintf("fallback-%d", time.Now().UnixNano()) } return base64.RawURLEncoding.EncodeToString(buffer) }