GetPrincipalByAPIKey authenticates a bearer token and returns the full principal — the owning user PLUS the key's scope and bound agent (Slice 5a). Same validation/last_used semantics as GetUserByAPIKey (it delegates here). A legacy key with a NULL scope column resolves to ScopeAccount, preserving p
(ctx context.Context, apiKey string)
| 3523 | // A legacy key with a NULL scope column resolves to ScopeAccount, preserving |
| 3524 | // pre-redesign authority. |
| 3525 | func (s *Store) GetPrincipalByAPIKey(ctx context.Context, apiKey string) (*Principal, error) { |
| 3526 | keyHash := hashAPIKey(apiKey) |
| 3527 | u := &User{} |
| 3528 | var scope string |
| 3529 | var agentID *string |
| 3530 | err := s.pool.QueryRow(ctx, |
| 3531 | `WITH touched AS ( |
| 3532 | UPDATE api_keys SET last_used_at = now() |
| 3533 | WHERE key_hash = $1 |
| 3534 | AND revoked_at IS NULL |
| 3535 | AND (expires_at IS NULL OR expires_at > now()) |
| 3536 | RETURNING user_id, COALESCE(scope, 'account') AS scope, agent_id |
| 3537 | ) |
| 3538 | SELECT u.id, u.email, u.name, u.google_subject, u.created_at, t.scope, t.agent_id |
| 3539 | FROM touched t JOIN users u ON u.id = t.user_id`, keyHash, |
| 3540 | ).Scan(&u.ID, &u.Email, &u.Name, &u.GoogleSubject, &u.CreatedAt, &scope, &agentID) |
| 3541 | if err != nil { |
| 3542 | return nil, err |
| 3543 | } |
| 3544 | p := &Principal{User: u, Scope: scope} |
| 3545 | if agentID != nil { |
| 3546 | p.AgentID = *agentID |
| 3547 | } |
| 3548 | return p, nil |
| 3549 | } |
| 3550 | |
| 3551 | func generateID() string { |
| 3552 | b := make([]byte, 16) |