feat(coderd/database): add AI Gateway key auth lookup and last-used queries (#26505)

Adds DB methods`GetAIGatewayKeyIDByHashedSecret` and `UpdateAIGatewayKeyLastUsedAt`.
`GetAIGatewayKeyIDByHashedSecret` - returns AI Gateway key ID by hashed secret value.
`UpdateAIGatewayKeyLastUsedAt` - updates last used timestamp for given AI Gateway key. 
Used by standalone AI Gateway for authentication and keeping track of currently used keys.
This commit is contained in:
Paweł Banaszewski
2026-06-26 18:16:01 +02:00
committed by GitHub
parent e71d4ca69b
commit 0f1e792f3f
24 changed files with 271 additions and 7 deletions
+79
View File
@@ -14958,6 +14958,85 @@ func TestAIGatewayKeysQueries(t *testing.T) {
requireAIGatewayKeysRow(t, keys[0], second, secondRow.CreatedAt)
}
func TestGetAIGatewayKeyByHashedSecret(t *testing.T) {
t.Parallel()
db, _ := dbtestutil.NewDB(t)
ctx := testutil.Context(t, testutil.WaitLong)
first := aiGatewayKeyParams("lookup-first", "key_lookup1")
second := aiGatewayKeyParams("lookup-second", "key_lookup2")
_, err := db.InsertAIGatewayKey(ctx, first)
require.NoError(t, err)
_, err = db.InsertAIGatewayKey(ctx, second)
require.NoError(t, err)
key, err := db.GetAIGatewayKeyByHashedSecret(ctx, first.HashedSecret)
require.NoError(t, err)
require.Equal(t, first.ID, key.ID)
require.Equal(t, first.Name, key.Name)
require.Equal(t, first.SecretPrefix, key.SecretPrefix)
require.Equal(t, first.HashedSecret, key.HashedSecret)
key, err = db.GetAIGatewayKeyByHashedSecret(ctx, second.HashedSecret)
require.NoError(t, err)
require.Equal(t, second.ID, key.ID)
// An unknown secret returns no rows
key, err = db.GetAIGatewayKeyByHashedSecret(ctx, []byte("does-not-exist"))
require.ErrorIs(t, err, sql.ErrNoRows)
require.Empty(t, key.ID)
}
func TestUpdateAIGatewayKeyLastUsedAt(t *testing.T) {
t.Parallel()
db, _, sqlDB := dbtestutil.NewDBWithSQLDB(t)
ctx := testutil.Context(t, testutil.WaitLong)
params := aiGatewayKeyParams("liveness-key", "key_live___")
row, err := db.InsertAIGatewayKey(ctx, params)
require.NoError(t, err)
// last_used_at starts NULL until a session records liveness.
keys, err := db.ListAIGatewayKeys(ctx)
require.NoError(t, err)
require.Len(t, keys, 1)
require.False(t, keys[0].LastUsedAt.Valid)
rows, err := db.UpdateAIGatewayKeyLastUsedAt(ctx, params.ID)
require.NoError(t, err)
require.EqualValues(t, 1, rows)
keys, err = db.ListAIGatewayKeys(ctx)
require.NoError(t, err)
require.Len(t, keys, 1)
require.True(t, keys[0].LastUsedAt.Valid)
// The database stamps the timestamp, so compare against the row's
// DB-generated CreatedAt to avoid client clock skew.
require.False(t, keys[0].LastUsedAt.Time.Before(row.CreatedAt))
// Updating a key that does not exist is a no-op, not an error.
rows, err = db.UpdateAIGatewayKeyLastUsedAt(ctx, uuid.New())
require.NoError(t, err)
require.EqualValues(t, 0, rows)
// Set last_used_at to old time to confirm the update overwrites it with a fresh timestamp.
staleTime := row.CreatedAt.Add(-time.Hour)
_, err = sqlDB.ExecContext(ctx, "UPDATE ai_gateway_keys SET last_used_at = $1 WHERE id = $2", staleTime, params.ID)
require.NoError(t, err)
rows, err = db.UpdateAIGatewayKeyLastUsedAt(ctx, params.ID)
require.NoError(t, err)
require.EqualValues(t, 1, rows)
keys, err = db.ListAIGatewayKeys(ctx)
require.NoError(t, err)
require.Len(t, keys, 1)
require.True(t, keys[0].LastUsedAt.Time.After(staleTime))
}
func aiGatewayKeyParams(name string, secretPrefix string) database.InsertAIGatewayKeyParams {
return database.InsertAIGatewayKeyParams{
ID: uuid.New(),