{"record":{"id":"5a548e776ea736cd","repo":"OpenNHP/opennhp","slug":"keystore-get-agent-key-expiry-w","errorCode":null,"errorMessage":"keystore: get agent key expiry: %w","messagePattern":"keystore: get agent key expiry: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"endpoints/server/keystore.go","lineNumber":505,"sourceCode":"//\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 = ?`,\n\t\tuserId, deviceId,\n\t).Scan(&active, &expiresAt)\n\tif err == sql.ErrNoRows {\n\t\treturn false, nil, nil\n\t}\n\tif err != nil {\n\t\treturn false, nil, fmt.Errorf(\"keystore: get agent key expiry: %w\", err)\n\t}\n\tif active != 1 {\n\t\treturn false, nil, nil\n\t}\n\tif expiresAt.Valid && expiresAt.Int64 <= time.Now().Unix() {\n\t\treturn false, nil, nil\n\t}\n\tif expiresAt.Valid {\n\t\tts := expiresAt.Int64\n\t\treturn true, &ts, nil\n\t}\n\treturn true, nil, nil\n}\n\n// SweepExpiredDeactivates flips active=0 for any row whose expires_at has\n// elapsed. Returns the number of rows updated. NULL expires_at rows are\n// never swept (they are configured to never expire). The result of\n// FindAgentByPublicKey / IsAgentRegistered does not depend on this","sourceCodeStart":487,"sourceCodeEnd":523,"githubUrl":"https://github.com/OpenNHP/opennhp/blob/6e04ca5ff03222a699c24205cd4bf8fee9af7ffe/endpoints/server/keystore.go#L487-L523","documentation":"GetAgentKeyExpiry wraps non-ErrNoRows failures of the SELECT active, expires_at lookup for a user+device row. Missing rows return (false, nil, nil); this error indicates an actual DB failure. Note the query here does not filter on active/expires_at in SQL — it reads the raw row — but a query error is still fatal and wrapped with the same style as the rest of the keystore.","triggerScenarios":"Calling GetAgentKeyExpiry(userId, deviceId) from the plugin helper when the DB handle is closed, agent_keys is missing, the file is locked, or scanning the active/expires_at columns into int/sql.NullInt64 fails due to schema drift.","commonSituations":"Old DB files with different column types after an upgrade; plugin helper invoked before keystore initialization; SQLite lock contention during bulk sweeps.","solutions":["Inspect the wrapped cause; a scan-type error means the schema drifted — migrate agent_keys to the expected column types.","Verify agent_keys exists (run migrations) before plugins query expiry.","Enable WAL mode / busy_timeout to avoid lock failures during concurrent sweeps.","Confirm the keystore's DB handle is open for the lifetime of the plugin helper.","Treat (false, nil, nil) as the correct 'not registered/expired' answer instead of conflating it with this error."],"exampleFix":"null","handlingStrategy":"try-catch","validationCode":"// caller precheck\nif userId == \"\" || deviceId == \"\" { return errors.New(\"user and device required\") }","typeGuard":"active, exp, err := store.GetAgentKeyExpiry(user, dev)\nif err != nil {\n    return fmt.Errorf(\"expiry lookup failed: %w\", err)\n}\n// only inspect *exp when active == true","tryCatchPattern":"active, exp, err := store.GetAgentKeyExpiry(u, d)\nif err != nil {\n    log.Errorf(\"plugin expiry lookup: %v\", err)\n    return active, exp, err\n}\n// (false, nil, nil) means missing/inactive/expired — not an error","preventionTips":["Migrate agent_keys schema (types of active/expires_at) before plugins load","Initialize the keystore before plugin helpers run","Enable WAL mode to survive concurrent sweeps","Document the tri-state return so callers do not treat (false,nil,nil) as failure"],"tags":["go","database","sqlite","expiry"],"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"}