{"record":{"id":"87abc0c4ea6f4075","repo":"OpenNHP/opennhp","slug":"keystore-get-agent-key-w","errorCode":null,"errorMessage":"keystore: get agent key: %w","messagePattern":"keystore: get agent key: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"endpoints/server/keystore.go","lineNumber":441,"sourceCode":"// GetAgentKey returns the public key for a given user+device, or nil if\n// not found OR if the row is past its expires_at. Expired rows are\n// indistinguishable from never-registered ones to all callers.\nfunc (s *AgentKeyStore) GetAgentKey(userId, deviceId string) (*AgentKeyRecord, error) {\n\trec := &AgentKeyRecord{}\n\tvar expiresAt sql.NullInt64\n\tvar active int\n\terr := s.db.QueryRow(\n\t\t`SELECT usr_id, dev_id, public_key, cipher, created_at, expires_at, active\n\t\t 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(&rec.UserId, &rec.DeviceId, &rec.PublicKey, &rec.Cipher, &rec.CreatedAt, &expiresAt, &active)\n\tif err == sql.ErrNoRows {\n\t\treturn nil, nil\n\t}\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"keystore: get agent key: %w\", err)\n\t}\n\n\trec.Active = active == 1\n\tif expiresAt.Valid {\n\t\trec.ExpiresAt = &expiresAt.Int64\n\t}\n\treturn rec, nil\n}\n\n// FindAgentByPublicKey returns true if the given base64-encoded public key\n// is registered, active, and not expired. This is the gate consulted by\n// the noise-layer peer validation fallback; an expired key behaves as if\n// the agent were never registered.\nfunc (s *AgentKeyStore) FindAgentByPublicKey(pubKeyBase64 string) (bool, error) {\n\tvar count int\n\terr := s.db.QueryRow(\n\t\t`SELECT COUNT(*) FROM agent_keys\n\t\t WHERE public_key = ? AND active = 1","sourceCodeStart":423,"sourceCodeEnd":459,"githubUrl":"https://github.com/OpenNHP/opennhp/blob/6e04ca5ff03222a699c24205cd4bf8fee9af7ffe/endpoints/server/keystore.go#L423-L459","documentation":"AgentKeyStore.GetAgentKey wraps any non-ErrNoRows failure from the QueryRow lookup of an agent's public key in the agent_keys table. sql.ErrNoRows is treated as a legitimate 'not found' (returns nil, nil); this error means the query itself failed — DB closed, table missing, driver error, or scan type failure. The underlying driver error is preserved via %w so callers can inspect it with errors.As/Is.","triggerScenarios":"Calling GetAgentKey(userId, deviceId) when the SQLite/database handle is closed or corrupted, the agent_keys table does not exist (schema not migrated), the DB file is locked by another process beyond the busy timeout, or a column type cannot be scanned into the expected fields.","commonSituations":"Server started before migrations ran; the DB file was deleted or moved while nhp-serverd was running; read-only filesystem; concurrent writers hitting SQLite 'database is locked'; misconfigured DSN in config.toml.","solutions":["Inspect the wrapped cause with errors.Unwrap / %v of the returned error to identify the driver failure (locked vs missing table vs closed DB).","Verify the agent_keys table exists and migrations ran before the keystore is used.","Check DB file permissions and that no other process holds an exclusive lock on the SQLite file.","Ensure the keystore's *sql.DB is opened once and not closed by another code path while in use.","Enable SQLite busy_timeout / WAL mode to tolerate concurrent access."],"exampleFix":"// before\nrec, err := store.GetAgentKey(user, dev)\nif err != nil { return err }\n// after\nrec, err := store.GetAgentKey(user, dev)\nif err != nil {\n    log.Errorf(\"agent key lookup failed for %s/%s: %v\", user, dev, err) // logged cause is the wrapped driver error\n    return err\n}","handlingStrategy":"try-catch","validationCode":"if err := db.Ping(); err != nil { return fmt.Errorf(\"keystore db unavailable: %w\", err) }","typeGuard":"rec, err := store.GetAgentKey(user, dev)\nif err != nil {\n    var derr *driverError\n    if errors.As(err, &derr) { /* handle driver failure */ }\n    return err\n}\n// rec == nil means not registered (not an error)","tryCatchPattern":"rec, err := store.GetAgentKey(userId, deviceId)\nif err != nil {\n    if strings.Contains(err.Error(), \"database is locked\") {\n        // retry with backoff\n    }\n    return fmt.Errorf(\"agent key lookup unavailable: %w\", err)\n}","preventionTips":["Run schema migrations at startup before any keystore call","Enable WAL mode and busy_timeout on the SQLite handle","Never close the shared *sql.DB while the server is serving","Distinguish nil-rec (not found) from err (DB failure) at every call site"],"tags":["go","database","sqlite","keystore"],"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"}