CreateScopedAPIKey issues a fresh API key with an explicit scope (Slice 5a). - ScopeAccount: account-wide admin; agentID must be empty; prefix e2a_acct_. - ScopeAgent: bound to agentID (which must be a non-empty agent owned by the user); prefix e2a_agt_. The key can only act as that one agent. The
(ctx context.Context, userID, name, scope, agentID string, expiresAt *time.Time)
| 3395 | // The visible prefix makes a key's blast radius obvious at a glance, and the DB |
| 3396 | // CHECK (scope='agent') == (agent_id IS NOT NULL) backstops the binding. |
| 3397 | func (s *Store) CreateScopedAPIKey(ctx context.Context, userID, name, scope, agentID string, expiresAt *time.Time) (*APIKey, error) { |
| 3398 | if !ValidScope(scope) { |
| 3399 | return nil, fmt.Errorf("invalid credential scope %q", scope) |
| 3400 | } |
| 3401 | if scope == ScopeAgent && agentID == "" { |
| 3402 | return nil, fmt.Errorf("agent-scoped key requires an agent_id") |
| 3403 | } |
| 3404 | if scope == ScopeAccount && agentID != "" { |
| 3405 | return nil, fmt.Errorf("account-scoped key must not name an agent") |
| 3406 | } |
| 3407 | // For an agent-scoped key, the named agent must exist and be owned by the |
| 3408 | // same user — otherwise a caller could mint a key bound to someone else's |
| 3409 | // agent (the FK alone wouldn't catch cross-user binding). |
| 3410 | if scope == ScopeAgent { |
| 3411 | owns, err := s.userOwnsAgent(ctx, agentID, userID) |
| 3412 | if err != nil { |
| 3413 | return nil, err |
| 3414 | } |
| 3415 | if !owns { |
| 3416 | return nil, fmt.Errorf("agent %q not found or not owned by user", agentID) |
| 3417 | } |
| 3418 | } |
| 3419 | |
| 3420 | id := "apk_" + generateID() |
| 3421 | plaintext := generateAPIKey(scope) |
| 3422 | keyHash := hashAPIKey(plaintext) |
| 3423 | // Show the scoped prefix + a few key chars (e.g. "e2a_agt_abcd…"). |
| 3424 | prefix := plaintext[:16] |
| 3425 | now := time.Now() |
| 3426 | var agentCol *string |
| 3427 | if scope == ScopeAgent { |
| 3428 | agentCol = &agentID |
| 3429 | } |
| 3430 | ak := &APIKey{ |
| 3431 | ID: id, |
| 3432 | UserID: userID, |
| 3433 | Name: name, |
| 3434 | KeyPrefix: prefix, |
| 3435 | PlaintextKey: plaintext, |
| 3436 | CreatedAt: now, |
| 3437 | Scope: scope, |
| 3438 | AgentID: agentCol, |
| 3439 | ExpiresAt: expiresAt, |
| 3440 | } |
| 3441 | _, err := s.pool.Exec(ctx, |
| 3442 | `INSERT INTO api_keys (id, user_id, name, key_prefix, key_hash, scope, agent_id, created_at, expires_at) |
| 3443 | VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`, |
| 3444 | ak.ID, ak.UserID, ak.Name, ak.KeyPrefix, keyHash, ak.Scope, agentCol, ak.CreatedAt, ak.ExpiresAt, |
| 3445 | ) |
| 3446 | if err != nil { |
| 3447 | return nil, err |
| 3448 | } |
| 3449 | return ak, nil |
| 3450 | } |
| 3451 | |
| 3452 | // userOwnsAgent reports whether agentID exists and is owned by userID. |
| 3453 | func (s *Store) userOwnsAgent(ctx context.Context, agentID, userID string) (bool, error) { |