{"record":{"id":"807324c5c2fc90ce","repo":"OpenNHP/opennhp","slug":"keystore-check-agent-registered-w","errorCode":null,"errorMessage":"keystore: check agent registered: %w","messagePattern":"keystore: check agent registered: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"endpoints/server/keystore.go","lineNumber":480,"sourceCode":"\t).Scan(&count)\n\tif err != nil {\n\t\treturn false, fmt.Errorf(\"keystore: find agent by pubkey: %w\", err)\n\t}\n\treturn count > 0, nil\n}\n\n// IsAgentRegistered returns true if the user+device pair has an active,\n// non-expired registered key.\nfunc (s *AgentKeyStore) IsAgentRegistered(userId, deviceId string) (bool, error) {\n\tvar count int\n\terr := s.db.QueryRow(\n\t\t`SELECT COUNT(*) FROM agent_keys\n\t\t WHERE usr_id = ? AND dev_id = ? AND active = 1\n\t\t   AND (expires_at IS NULL OR expires_at > ?)`,\n\t\tuserId, deviceId, time.Now().Unix(),\n\t).Scan(&count)\n\tif err != nil {\n\t\treturn false, fmt.Errorf(\"keystore: check agent registered: %w\", err)\n\t}\n\treturn count > 0, nil\n}\n\n// GetAgentKeyExpiry returns the expiry status for the given user+device:\n//\n//\t(true,  &ts, nil) — row exists and is active with expires_at = ts\n//\t(true,  nil,  nil) — row exists and is active with no expiry (NULL)\n//\t(false, nil,  nil) — row is missing, deactivated, or already expired\n//\n// Used by the plugin helper to surface \"valid until when?\" without\n// reaching into the keystore itself. The third return value is reserved\n// for future I/O errors; today it is always nil when the lookup ran.\nfunc (s *AgentKeyStore) GetAgentKeyExpiry(userId, deviceId string) (bool, *int64, error) {\n\tvar active int\n\tvar expiresAt sql.NullInt64\n\terr := s.db.QueryRow(\n\t\t`SELECT active, expires_at FROM agent_keys WHERE usr_id = ? AND dev_id = ?`,","sourceCodeStart":462,"sourceCodeEnd":498,"githubUrl":"https://github.com/OpenNHP/opennhp/blob/6e04ca5ff03222a699c24205cd4bf8fee9af7ffe/endpoints/server/keystore.go#L462-L498","documentation":"IsAgentRegistered wraps any QueryRow failure when counting active, non-expired keys for a user+device pair. Like the other lookups, an empty result is not an error — only genuine DB failures (closed handle, missing table, lock contention, driver faults) produce this wrapped error.","triggerScenarios":"Calling IsAgentRegistered(userId, deviceId) during registration or knock validation while the DB is locked by a writer, the table is absent, or the connection has been closed.","commonSituations":"Concurrent registration writes blocking the count query; server started with a fresh config pointing at a nonexistent DB file without migrations; disk I/O errors on the host.","solutions":["Unwrap the error to read the driver message (no such table / database is locked / sql: database is closed).","Ensure migrations create agent_keys with the usr_id/dev_id/active/expires_at columns before use.","Configure SQLite WAL + busy_timeout to let reads proceed during writes.","Check that registration handlers are not opening separate connections that lock the same file.","Verify filesystem health and free space on the DB volume."],"exampleFix":"null","handlingStrategy":"try-catch","validationCode":"var exists int\nif err := db.QueryRow(\"SELECT count(*) FROM sqlite_master WHERE type='table' AND name='agent_keys'\").Scan(&exists); err != nil || exists == 0 {\n    return errors.New(\"agent_keys table missing; run migrations\")\n}","typeGuard":"registered, err := store.IsAgentRegistered(user, dev)\nif err != nil {\n    log.Errorf(\"registration check failed: %v\", err)\n    return false, err\n}","tryCatchPattern":"ok, err := store.IsAgentRegistered(u, d)\nif err != nil {\n    if errors.Is(err, sql.ErrConnDone) || strings.Contains(err.Error(), \"closed\") {\n        // reinitialize keystore\n    }\n    return false, err\n}","preventionTips":["Serialize registration writes or use WAL so counts are not blocked","Add a startup self-check that queries agent_keys once","Keep the DB on local storage, not network mounts","Distinguish (false, nil) from error paths in callers"],"tags":["go","database","sqlite","registration"],"backgroundTag":"database-query-failed","analyzedSha":"6e04ca5ff03222a699c24205cd4bf8fee9af7ffe","analyzedAt":"2026-09-07T15:44:59.941Z","contentChangedAt":"2026-09-07T15:44:59.941Z","schemaVersion":2},"datasetVersion":"2026-09-16T04:17:20.429Z"}