Spaces:
Runtime error
Runtime error
| /** | |
| * API Key Groups DB β CRUD operations for team/enterprise key grouping | |
| * | |
| * Tables: key_groups, group_model_permissions, key_group_members | |
| * Migration: 065_api_key_groups.sql | |
| * | |
| * Enables team-level API key management with model-level access control. | |
| */ | |
| import { getDbInstance } from "@/lib/db/core"; | |
| import { randomUUID } from "crypto"; | |
| // ββ Types ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| export interface KeyGroup { | |
| id: string; | |
| name: string; | |
| description: string; | |
| isActive: boolean; | |
| createdAt: string; | |
| updatedAt: string; | |
| } | |
| export interface GroupModelPermission { | |
| id: string; | |
| groupId: string; | |
| modelPattern: string; | |
| provider: string | null; | |
| accessType: "allow" | "deny"; | |
| createdAt: string; | |
| } | |
| export interface KeyGroupMember { | |
| keyId: string; | |
| groupId: string; | |
| createdAt: string; | |
| } | |
| export interface KeyGroupWithPermissions extends KeyGroup { | |
| permissions: GroupModelPermission[]; | |
| memberCount: number; | |
| } | |
| // ββ Key Groups CRUD ββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| export function getAllKeyGroups(): KeyGroup[] { | |
| const db = getDbInstance() as any; | |
| const rows = db.prepare("SELECT * FROM key_groups ORDER BY name ASC").all() as any[]; | |
| return rows.map(rowToGroup); | |
| } | |
| export function getKeyGroup(id: string): KeyGroup | undefined { | |
| const db = getDbInstance() as any; | |
| const row = db.prepare("SELECT * FROM key_groups WHERE id = ?").get(id) as any; | |
| return row ? rowToGroup(row) : undefined; | |
| } | |
| export function getKeyGroupWithPermissions(id: string): KeyGroupWithPermissions | undefined { | |
| const group = getKeyGroup(id); | |
| if (!group) return undefined; | |
| const permissions = getGroupPermissions(id); | |
| const memberCount = getGroupMemberCount(id); | |
| return { ...group, permissions, memberCount }; | |
| } | |
| export function createKeyGroup(name: string, description = ""): KeyGroup { | |
| const db = getDbInstance() as any; | |
| const id = randomUUID(); | |
| const now = new Date().toISOString(); | |
| db.prepare( | |
| "INSERT INTO key_groups (id, name, description, created_at, updated_at) VALUES (?, ?, ?, ?, ?)" | |
| ).run(id, name, description, now, now); | |
| return getKeyGroup(id)!; | |
| } | |
| export function updateKeyGroup( | |
| id: string, | |
| updates: { name?: string; description?: string; isActive?: boolean } | |
| ): KeyGroup | undefined { | |
| const existing = getKeyGroup(id); | |
| if (!existing) return undefined; | |
| const db = getDbInstance() as any; | |
| const sets: string[] = []; | |
| const params: Record<string, unknown> = { id }; | |
| if (updates.name !== undefined) { | |
| sets.push("name = @name"); | |
| params.name = updates.name; | |
| } | |
| if (updates.description !== undefined) { | |
| sets.push("description = @description"); | |
| params.description = updates.description; | |
| } | |
| if (updates.isActive !== undefined) { | |
| sets.push("is_active = @isActive"); | |
| params.isActive = updates.isActive ? 1 : 0; | |
| } | |
| if (sets.length === 0) return existing; | |
| sets.push("updated_at = datetime('now')"); | |
| db.prepare(`UPDATE key_groups SET ${sets.join(", ")} WHERE id = @id`).run(params); | |
| return getKeyGroup(id); | |
| } | |
| export function deleteKeyGroup(id: string): boolean { | |
| const db = getDbInstance() as any; | |
| // CASCADE deletes permissions and members | |
| const result = db.prepare("DELETE FROM key_groups WHERE id = ?").run(id); | |
| return result.changes > 0; | |
| } | |
| // ββ Group Permissions ββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| export function getGroupPermissions(groupId: string): GroupModelPermission[] { | |
| const db = getDbInstance() as any; | |
| const rows = db | |
| .prepare( | |
| "SELECT * FROM group_model_permissions WHERE group_id = ? ORDER BY access_type ASC, model_pattern ASC" | |
| ) | |
| .all(groupId) as any[]; | |
| return rows.map(rowToPermission); | |
| } | |
| export function addGroupPermission( | |
| groupId: string, | |
| modelPattern: string, | |
| accessType: "allow" | "deny", | |
| provider?: string | |
| ): GroupModelPermission { | |
| const db = getDbInstance() as any; | |
| const id = randomUUID(); | |
| const now = new Date().toISOString(); | |
| db.prepare( | |
| "INSERT INTO group_model_permissions (id, group_id, model_pattern, provider, access_type, created_at) VALUES (?, ?, ?, ?, ?, ?)" | |
| ).run(id, groupId, modelPattern, provider || null, accessType, now); | |
| return getGroupPermissions(groupId).find((p) => p.id === id)!; | |
| } | |
| export function removeGroupPermission(permissionId: string): boolean { | |
| const db = getDbInstance() as any; | |
| const result = db.prepare("DELETE FROM group_model_permissions WHERE id = ?").run(permissionId); | |
| return result.changes > 0; | |
| } | |
| export function clearGroupPermissions(groupId: string): void { | |
| const db = getDbInstance() as any; | |
| db.prepare("DELETE FROM group_model_permissions WHERE group_id = ?").run(groupId); | |
| } | |
| // ββ Key Group Members ββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| export function getGroupMembers(groupId: string): KeyGroupMember[] { | |
| const db = getDbInstance() as any; | |
| const rows = db | |
| .prepare("SELECT * FROM key_group_members WHERE group_id = ? ORDER BY created_at ASC") | |
| .all(groupId) as any[]; | |
| return rows.map(rowToMember); | |
| } | |
| export function getKeyGroupsForApiKey(keyId: string): KeyGroup[] { | |
| const db = getDbInstance() as any; | |
| const rows = db | |
| .prepare( | |
| ` | |
| SELECT g.* FROM key_groups g | |
| INNER JOIN key_group_members m ON g.id = m.group_id | |
| WHERE m.key_id = ? AND g.is_active = 1 | |
| ORDER BY g.name ASC | |
| ` | |
| ) | |
| .all(keyId) as any[]; | |
| return rows.map(rowToGroup); | |
| } | |
| export function addKeyToGroup(keyId: string, groupId: string): boolean { | |
| const db = getDbInstance() as any; | |
| try { | |
| db.prepare("INSERT OR IGNORE INTO key_group_members (key_id, group_id) VALUES (?, ?)").run( | |
| keyId, | |
| groupId | |
| ); | |
| return true; | |
| } catch { | |
| return false; | |
| } | |
| } | |
| export function removeKeyFromGroup(keyId: string, groupId: string): boolean { | |
| const db = getDbInstance() as any; | |
| const result = db | |
| .prepare("DELETE FROM key_group_members WHERE key_id = ? AND group_id = ?") | |
| .run(keyId, groupId); | |
| return result.changes > 0; | |
| } | |
| function getGroupMemberCount(groupId: string): number { | |
| const db = getDbInstance() as any; | |
| const row = db | |
| .prepare("SELECT COUNT(*) as count FROM key_group_members WHERE group_id = ?") | |
| .get(groupId) as any; | |
| return row?.count || 0; | |
| } | |
| // ββ Authorization Helper ββββββββββββββββββββββββββββββββββββββββββββββββ | |
| export interface ModelAccessCheck { | |
| allowed: boolean; | |
| matchedRules: GroupModelPermission[]; | |
| deniedBy: GroupModelPermission | null; | |
| } | |
| /** | |
| * Check if an API key has access to a specific model. | |
| * Deny rules override allow rules. If no rules match, access is allowed by default. | |
| */ | |
| export function checkKeyModelAccess( | |
| keyId: string, | |
| model: string, | |
| provider?: string | |
| ): ModelAccessCheck { | |
| const groups = getKeyGroupsForApiKey(keyId); | |
| if (groups.length === 0) { | |
| // No groups = no restrictions | |
| return { allowed: true, matchedRules: [], deniedBy: null }; | |
| } | |
| const db = getDbInstance() as any; | |
| const groupIds = groups.map((g) => g.id); | |
| const placeholders = groupIds.map(() => "?").join(","); | |
| const rules = db | |
| .prepare( | |
| ` | |
| SELECT * FROM group_model_permissions | |
| WHERE group_id IN (${placeholders}) | |
| ORDER BY access_type ASC | |
| ` | |
| ) | |
| .all(...groupIds) as any[]; | |
| const permissions = rules.map(rowToPermission); | |
| // Check deny rules first (they take precedence) | |
| const denyRules = permissions.filter( | |
| (p) => | |
| p.accessType === "deny" && | |
| matchesModelPattern(p.modelPattern, model) && | |
| (!p.provider || p.provider === provider) | |
| ); | |
| if (denyRules.length > 0) { | |
| return { allowed: false, matchedRules: permissions, deniedBy: denyRules[0] }; | |
| } | |
| // Check allow rules | |
| const allowRules = permissions.filter( | |
| (p) => | |
| p.accessType === "allow" && | |
| matchesModelPattern(p.modelPattern, model) && | |
| (!p.provider || p.provider === provider) | |
| ); | |
| if (allowRules.length > 0) { | |
| return { allowed: true, matchedRules: permissions, deniedBy: null }; | |
| } | |
| // No matching rules = restricted by group membership but no explicit allow | |
| return { allowed: false, matchedRules: permissions, deniedBy: null }; | |
| } | |
| function matchesModelPattern(pattern: string, model: string): boolean { | |
| if (pattern === "*") return true; | |
| if (pattern.includes("*")) { | |
| const regex = new RegExp("^" + pattern.replace(/\*/g, ".*") + "$"); | |
| return regex.test(model); | |
| } | |
| return pattern === model; | |
| } | |
| // ββ Row Mappers ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function rowToGroup(row: any): KeyGroup { | |
| return { | |
| id: row.id, | |
| name: row.name, | |
| description: row.description || "", | |
| isActive: row.is_active === 1, | |
| createdAt: row.created_at, | |
| updatedAt: row.updated_at, | |
| }; | |
| } | |
| function rowToPermission(row: any): GroupModelPermission { | |
| return { | |
| id: row.id, | |
| groupId: row.group_id, | |
| modelPattern: row.model_pattern, | |
| provider: row.provider || null, | |
| accessType: row.access_type, | |
| createdAt: row.created_at, | |
| }; | |
| } | |
| function rowToMember(row: any): KeyGroupMember { | |
| return { | |
| keyId: row.key_id, | |
| groupId: row.group_id, | |
| createdAt: row.created_at, | |
| }; | |
| } | |