OpenNHP/opennhp · error

keystore: get agent key expiry

Error message

keystore: get agent key expiry: %w

What it means

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.

Solutions

  1. Inspect the wrapped cause; a scan-type error means the schema drifted — migrate agent_keys to the expected column types.
  2. Verify agent_keys exists (run migrations) before plugins query expiry.
  3. Enable WAL mode / busy_timeout to avoid lock failures during concurrent sweeps.
  4. Confirm the keystore's DB handle is open for the lifetime of the plugin helper.
  5. Treat (false, nil, nil) as the correct 'not registered/expired' answer instead of conflating it with this error.

Example fix

null
Defensive patterns

Strategy: try-catch

Validate before calling

// caller precheck
if userId == "" || deviceId == "" { return errors.New("user and device required") }

Type guard

active, exp, err := store.GetAgentKeyExpiry(user, dev)
if err != nil {
    return fmt.Errorf("expiry lookup failed: %w", err)
}
// only inspect *exp when active == true

Try / catch

active, exp, err := store.GetAgentKeyExpiry(u, d)
if err != nil {
    log.Errorf("plugin expiry lookup: %v", err)
    return active, exp, err
}
// (false, nil, nil) means missing/inactive/expired — not an error

Prevention

When it happens

Trigger: 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.

Common situations: Old DB files with different column types after an upgrade; plugin helper invoked before keystore initialization; SQLite lock contention during bulk sweeps.

Understand the failure class

Background: Database query failed: Internal Server Error 500s wrapping SQL, Prisma, and connection failures — what to check first — this error's family across 16 libraries.

Related errors


AI-assisted analysis of OpenNHP/opennhp@6e04ca5ff0 (2026-09-07). Data as JSON: /api/errors/5a548e776ea736cd. Report an issue: GitHub.

Appendix: source

Thrown at endpoints/server/keystore.go:505

//	(true,  &ts, nil) — row exists and is active with expires_at = ts
//	(true,  nil,  nil) — row exists and is active with no expiry (NULL)
//	(false, nil,  nil) — row is missing, deactivated, or already expired
//
// Used by the plugin helper to surface "valid until when?" without
// reaching into the keystore itself. The third return value is reserved
// for future I/O errors; today it is always nil when the lookup ran.
func (s *AgentKeyStore) GetAgentKeyExpiry(userId, deviceId string) (bool, *int64, error) {
	var active int
	var expiresAt sql.NullInt64
	err := s.db.QueryRow(
		`SELECT active, expires_at FROM agent_keys WHERE usr_id = ? AND dev_id = ?`,
		userId, deviceId,
	).Scan(&active, &expiresAt)
	if err == sql.ErrNoRows {
		return false, nil, nil
	}
	if err != nil {
		return false, nil, fmt.Errorf("keystore: get agent key expiry: %w", err)
	}
	if active != 1 {
		return false, nil, nil
	}
	if expiresAt.Valid && expiresAt.Int64 <= time.Now().Unix() {
		return false, nil, nil
	}
	if expiresAt.Valid {
		ts := expiresAt.Int64
		return true, &ts, nil
	}
	return true, nil, nil
}

// SweepExpiredDeactivates flips active=0 for any row whose expires_at has
// elapsed. Returns the number of rows updated. NULL expires_at rows are
// never swept (they are configured to never expire). The result of
// FindAgentByPublicKey / IsAgentRegistered does not depend on this

View on GitHub (pinned to 6e04ca5ff0)