From b8209164340308d255c79365ebd383991724ca9c Mon Sep 17 00:00:00 2001 From: Automation Date: Sun, 24 May 2026 20:33:27 +0200 Subject: [PATCH] feat(auth): API key groups with model-level permissions - Groups with model permission management (wildcard patterns, deny-override) - API keys can be assigned to groups, inheriting their restrictions - Permission check integrated into request pipeline via keyGroupAuth - DB migration 065 with full CRUD - REST API at /api/keys/groups/ --- open-sse/services/keyGroupAuth.ts | 84 ++++++ src/app/api/keys/groups/[id]/keys/route.ts | 64 ++++ .../api/keys/groups/[id]/permissions/route.ts | 66 +++++ src/app/api/keys/groups/[id]/route.ts | 68 +++++ src/app/api/keys/groups/route.ts | 35 +++ src/lib/db/apiKeyGroups.ts | 279 ++++++++++++++++++ src/lib/db/apiKeys.ts | 8 + src/lib/db/migrations/065_api_key_groups.sql | 50 ++++ src/lib/localDb.ts | 59 ++++ 9 files changed, 713 insertions(+) create mode 100644 open-sse/services/keyGroupAuth.ts create mode 100644 src/app/api/keys/groups/[id]/keys/route.ts create mode 100644 src/app/api/keys/groups/[id]/permissions/route.ts create mode 100644 src/app/api/keys/groups/[id]/route.ts create mode 100644 src/app/api/keys/groups/route.ts create mode 100644 src/lib/db/apiKeyGroups.ts create mode 100644 src/lib/db/migrations/065_api_key_groups.sql diff --git a/open-sse/services/keyGroupAuth.ts b/open-sse/services/keyGroupAuth.ts new file mode 100644 index 000000000000..9e58578feed1 --- /dev/null +++ b/open-sse/services/keyGroupAuth.ts @@ -0,0 +1,84 @@ +/** + * Key Group Authorization Service + * + * Enforces model-level access control based on API key group membership. + * Used in the request pipeline to check if an API key's groups allow + * access to the requested model. + * + * If the API key is not in any group, all models are allowed (no restriction). + * If the API key IS in a group, model access is determined by permissions: + * - Deny rules override allow rules + * - Patterns support wildcards: "gpt-*", "claude-*", "*" for all + * - Provider-specific rules: "openai/gpt-4" + */ + +import { checkKeyModelAccess, getKeyGroupsForApiKey } from "@/lib/localDb"; + +export interface KeyGroupAuthResult { + /** Whether the request is authorized */ + authorized: boolean; + /** Human-readable reason if denied */ + reason?: string; + /** Groups that apply to this key */ + groups: Array<{ id: string; name: string }>; +} + +/** + * Check if an API key has access to a specific model. + * This is the main entry point for the request pipeline. + * + * @param apiKeyId - The API key ID from the auth pipeline + * @param model - The model string (e.g., "gpt-4", "claude-opus-4-7") + * @param provider - Optional provider (e.g., "openai", "anthropic") + * @returns KeyGroupAuthResult + */ +export function authorizeKeyModelAccess( + apiKeyId: string | undefined, + model: string, + provider?: string, +): KeyGroupAuthResult { + if (!apiKeyId) { + // No API key = no restriction (public endpoint) + return { authorized: true, groups: [] }; + } + + const groups = getKeyGroupsForApiKey(apiKeyId); + if (groups.length === 0) { + // Key not in any group = no restriction + return { authorized: true, groups: [] }; + } + + const accessCheck = checkKeyModelAccess(apiKeyId, model, provider); + + if (accessCheck.allowed) { + return { + authorized: true, + groups: groups.map((g) => ({ id: g.id, name: g.name })), + }; + } + + const denyReason = accessCheck.deniedBy + ? `Model "${model}" is denied by group permission (pattern: ${accessCheck.deniedBy.modelPattern})` + : `Model "${model}" is not in the allowed models for your API key group(s). ` + + `Configure group permissions or contact your administrator.`; + + return { + authorized: false, + reason: denyReason, + groups: groups.map((g) => ({ id: g.id, name: g.name })), + }; +} + +/** + * Get a summary of group memberships for an API key (for dashboard display). + */ +export function getKeyGroupSummary(apiKeyId: string): { + groups: Array<{ id: string; name: string }>; + restricted: boolean; +} { + const groups = getKeyGroupsForApiKey(apiKeyId); + return { + groups: groups.map((g) => ({ id: g.id, name: g.name })), + restricted: groups.length > 0, + }; +} diff --git a/src/app/api/keys/groups/[id]/keys/route.ts b/src/app/api/keys/groups/[id]/keys/route.ts new file mode 100644 index 000000000000..858650923e66 --- /dev/null +++ b/src/app/api/keys/groups/[id]/keys/route.ts @@ -0,0 +1,64 @@ +import { NextResponse } from "next/server"; +import { addKeyToGroup, removeKeyFromGroup, getGroupMembers, getKeyGroup } from "@/lib/localDb"; + +type RouteParams = { params: Promise<{ id: string }> }; + +/** + * GET /api/keys/groups/[id]/keys — List API keys in a group + */ +export async function GET(request: Request, { params }: RouteParams) { + try { + const { id } = await params; + const group = getKeyGroup(id); + if (!group) return NextResponse.json({ error: "Group not found" }, { status: 404 }); + const members = getGroupMembers(id); + return NextResponse.json({ members }); + } catch (error) { + return NextResponse.json({ error: "Failed to list members" }, { status: 500 }); + } +} + +/** + * POST /api/keys/groups/[id]/keys — Add an API key to the group + * Body: { keyId } + */ +export async function POST(request: Request, { params }: RouteParams) { + try { + const { id } = await params; + const group = getKeyGroup(id); + if (!group) return NextResponse.json({ error: "Group not found" }, { status: 404 }); + + const body = await request.json(); + if (!body.keyId) { + return NextResponse.json({ error: "keyId is required" }, { status: 400 }); + } + const added = addKeyToGroup(body.keyId, id); + if (!added) { + return NextResponse.json({ error: "Failed to add key" }, { status: 500 }); + } + return NextResponse.json({ success: true }, { status: 201 }); + } catch (error) { + return NextResponse.json({ error: "Failed to add key to group" }, { status: 500 }); + } +} + +/** + * DELETE /api/keys/groups/[id]/keys?keyId=xxx — Remove an API key from the group + */ +export async function DELETE(request: Request, { params }: RouteParams) { + try { + const { id } = await params; + const url = new URL(request.url); + const keyId = url.searchParams.get("keyId"); + if (!keyId) { + return NextResponse.json({ error: "keyId query param required" }, { status: 400 }); + } + const removed = removeKeyFromGroup(keyId, id); + if (!removed) { + return NextResponse.json({ error: "Key not found in group" }, { status: 404 }); + } + return NextResponse.json({ success: true }); + } catch (error) { + return NextResponse.json({ error: "Failed to remove key from group" }, { status: 500 }); + } +} diff --git a/src/app/api/keys/groups/[id]/permissions/route.ts b/src/app/api/keys/groups/[id]/permissions/route.ts new file mode 100644 index 000000000000..c234a03e885d --- /dev/null +++ b/src/app/api/keys/groups/[id]/permissions/route.ts @@ -0,0 +1,66 @@ +import { NextResponse } from "next/server"; +import { addGroupPermission, removeGroupPermission, getGroupPermissions, getKeyGroup } from "@/lib/localDb"; + +type RouteParams = { params: Promise<{ id: string }> }; + +/** + * GET /api/keys/groups/[id]/permissions — List permissions for a group + */ +export async function GET(request: Request, { params }: RouteParams) { + try { + const { id } = await params; + const group = getKeyGroup(id); + if (!group) return NextResponse.json({ error: "Group not found" }, { status: 404 }); + + const permissions = getGroupPermissions(id); + return NextResponse.json({ permissions }); + } catch (error) { + return NextResponse.json({ error: "Failed to list permissions" }, { status: 500 }); + } +} + +/** + * POST /api/keys/groups/[id]/permissions — Add a permission rule + * Body: { modelPattern, accessType, provider? } + */ +export async function POST(request: Request, { params }: RouteParams) { + try { + const { id } = await params; + const group = getKeyGroup(id); + if (!group) return NextResponse.json({ error: "Group not found" }, { status: 404 }); + + const body = await request.json(); + if (!body.modelPattern || !body.accessType) { + return NextResponse.json({ error: "modelPattern and accessType are required" }, { status: 400 }); + } + if (body.accessType !== "allow" && body.accessType !== "deny") { + return NextResponse.json({ error: "accessType must be 'allow' or 'deny'" }, { status: 400 }); + } + + const permission = addGroupPermission(id, body.modelPattern, body.accessType, body.provider); + return NextResponse.json({ permission }, { status: 201 }); + } catch (error) { + return NextResponse.json({ error: "Failed to add permission" }, { status: 500 }); + } +} + +/** + * DELETE /api/keys/groups/[id]/permissions?permissionId=xxx — Remove a permission + */ +export async function DELETE(request: Request, { params }: RouteParams) { + try { + const { id } = await params; + const url = new URL(request.url); + const permissionId = url.searchParams.get("permissionId"); + if (!permissionId) { + return NextResponse.json({ error: "permissionId query param required" }, { status: 400 }); + } + const removed = removeGroupPermission(permissionId); + if (!removed) { + return NextResponse.json({ error: "Permission not found" }, { status: 404 }); + } + return NextResponse.json({ success: true }); + } catch (error) { + return NextResponse.json({ error: "Failed to remove permission" }, { status: 500 }); + } +} diff --git a/src/app/api/keys/groups/[id]/route.ts b/src/app/api/keys/groups/[id]/route.ts new file mode 100644 index 000000000000..8ea46c914d48 --- /dev/null +++ b/src/app/api/keys/groups/[id]/route.ts @@ -0,0 +1,68 @@ +import { NextResponse } from "next/server"; +import { + getKeyGroupWithPermissions, + updateKeyGroup, + deleteKeyGroup, + getGroupMembers, + addGroupPermission, + removeGroupPermission, + addKeyToGroup, + removeKeyFromGroup, +} from "@/lib/localDb"; + +type RouteParams = { params: Promise<{ id: string }> }; + +/** + * GET /api/keys/groups/[id] — Get group details with permissions and members + */ +export async function GET(request: Request, { params }: RouteParams) { + try { + const { id } = await params; + const group = getKeyGroupWithPermissions(id); + if (!group) { + return NextResponse.json({ error: "Group not found" }, { status: 404 }); + } + const members = getGroupMembers(id); + return NextResponse.json({ group, members }); + } catch (error) { + return NextResponse.json({ error: "Failed to get group" }, { status: 500 }); + } +} + +/** + * PUT /api/keys/groups/[id] — Update a group + */ +export async function PUT(request: Request, { params }: RouteParams) { + try { + const { id } = await params; + const body = await request.json(); + const updates: { name?: string; description?: string; isActive?: boolean } = {}; + if (body.name !== undefined) updates.name = body.name; + if (body.description !== undefined) updates.description = body.description; + if (body.isActive !== undefined) updates.isActive = body.isActive; + + const group = updateKeyGroup(id, updates); + if (!group) { + return NextResponse.json({ error: "Group not found" }, { status: 404 }); + } + return NextResponse.json({ group }); + } catch (error) { + return NextResponse.json({ error: "Failed to update group" }, { status: 500 }); + } +} + +/** + * DELETE /api/keys/groups/[id] — Delete a group + */ +export async function DELETE(request: Request, { params }: RouteParams) { + try { + const { id } = await params; + const deleted = deleteKeyGroup(id); + if (!deleted) { + return NextResponse.json({ error: "Group not found" }, { status: 404 }); + } + return NextResponse.json({ success: true }); + } catch (error) { + return NextResponse.json({ error: "Failed to delete group" }, { status: 500 }); + } +} diff --git a/src/app/api/keys/groups/route.ts b/src/app/api/keys/groups/route.ts new file mode 100644 index 000000000000..4a03760f4f88 --- /dev/null +++ b/src/app/api/keys/groups/route.ts @@ -0,0 +1,35 @@ +import { NextResponse } from "next/server"; +import { + getAllKeyGroups, + createKeyGroup, + getKeyGroup, +} from "@/lib/localDb"; + +/** + * GET /api/keys/groups — List all key groups + */ +export async function GET() { + try { + const groups = getAllKeyGroups(); + return NextResponse.json({ groups }); + } catch (error) { + return NextResponse.json({ error: "Failed to list groups" }, { status: 500 }); + } +} + +/** + * POST /api/keys/groups — Create a key group + * Body: { name, description? } + */ +export async function POST(request: Request) { + try { + const body = await request.json(); + if (!body.name || typeof body.name !== "string" || !body.name.trim()) { + return NextResponse.json({ error: "name is required" }, { status: 400 }); + } + const group = createKeyGroup(body.name.trim(), body.description || ""); + return NextResponse.json({ group }, { status: 201 }); + } catch (error) { + return NextResponse.json({ error: "Failed to create group" }, { status: 500 }); + } +} diff --git a/src/lib/db/apiKeyGroups.ts b/src/lib/db/apiKeyGroups.ts new file mode 100644 index 000000000000..e5f5fe7c6b05 --- /dev/null +++ b/src/lib/db/apiKeyGroups.ts @@ -0,0 +1,279 @@ +/** + * 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 = { 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, + }; +} diff --git a/src/lib/db/apiKeys.ts b/src/lib/db/apiKeys.ts index aca2127cb7d5..9f44fab27114 100644 --- a/src/lib/db/apiKeys.ts +++ b/src/lib/db/apiKeys.ts @@ -7,6 +7,7 @@ import { v4 as uuidv4 } from "uuid"; import { getDbInstance, rowToCamel } from "./core"; import { backupDbFile } from "./backup"; import { registerDbStateResetter } from "./stateReset"; +import { getKeyGroupsForApiKey, checkKeyModelAccess } from "./apiKeyGroups"; import { setNoLog } from "../compliance"; // ──────────────── Performance Optimizations ──────────────── @@ -1235,6 +1236,13 @@ export async function isModelAllowedForKey( } } + // If key belongs to groups, also check group-level permissions + if (metadata.id) { + const groupAccess = checkKeyModelAccess(metadata.id, modelId || ""); + if (!groupAccess.allowed) { + allowed = false; + } + } // Cache the result evictIfNeeded(_modelPermissionCache); _modelPermissionCache.set(cacheKey, { allowed, timestamp: now }); diff --git a/src/lib/db/migrations/065_api_key_groups.sql b/src/lib/db/migrations/065_api_key_groups.sql new file mode 100644 index 000000000000..c015b9c8daa9 --- /dev/null +++ b/src/lib/db/migrations/065_api_key_groups.sql @@ -0,0 +1,50 @@ +-- Migration 065: API Key Groups & Model Permissions +-- Enables team/enterprise key grouping with model-level access control. + +BEGIN TRANSACTION; + +-- Key groups (logical grouping of API keys for team management) +CREATE TABLE IF NOT EXISTS key_groups ( + id TEXT PRIMARY KEY NOT NULL, + name TEXT NOT NULL, + description TEXT NOT NULL DEFAULT '', + is_active INTEGER NOT NULL DEFAULT 1, + created_at TEXT NOT NULL DEFAULT (datetime('now')), + updated_at TEXT NOT NULL DEFAULT (datetime('now')) +); + +-- Group-to-model permissions: which models each group can access +CREATE TABLE IF NOT EXISTS group_model_permissions ( + id TEXT PRIMARY KEY NOT NULL, + group_id TEXT NOT NULL, + -- Model identifier (supports wildcards: "gpt-*", "claude-*", "*" for all) + model_pattern TEXT NOT NULL, + -- Provider constraint (NULL = any provider, otherwise specific provider ID) + provider TEXT, + -- Allow or deny + access_type TEXT NOT NULL DEFAULT 'allow' CHECK(access_type IN ('allow', 'deny')), + created_at TEXT NOT NULL DEFAULT (datetime('now')), + FOREIGN KEY (group_id) REFERENCES key_groups(id) ON DELETE CASCADE +); + +-- Group-to-key membership +CREATE TABLE IF NOT EXISTS key_group_members ( + key_id TEXT NOT NULL, + group_id TEXT NOT NULL, + created_at TEXT NOT NULL DEFAULT (datetime('now')), + PRIMARY KEY (key_id, group_id), + FOREIGN KEY (group_id) REFERENCES key_groups(id) ON DELETE CASCADE, + FOREIGN KEY (key_id) REFERENCES api_keys(id) ON DELETE CASCADE +); + +-- Indexes +CREATE INDEX IF NOT EXISTS idx_group_permissions_group + ON group_model_permissions(group_id); + +CREATE INDEX IF NOT EXISTS idx_group_members_key + ON key_group_members(key_id); + +CREATE INDEX IF NOT EXISTS idx_group_members_group + ON key_group_members(group_id); + +COMMIT; diff --git a/src/lib/localDb.ts b/src/lib/localDb.ts index e068618d30aa..df6083dc3201 100755 --- a/src/lib/localDb.ts +++ b/src/lib/localDb.ts @@ -421,3 +421,62 @@ export type { } from "./db/gamification"; export * from "./db/featureFlags"; + +export { + upsertHandoff, + getHandoff, + deleteHandoff, + cleanupExpiredHandoffs, + hasActiveHandoff, + recordSessionModelUsage, + getLastSessionModel, +} from "./db/contextHandoffs"; + +export type { HandoffPayload } from "./db/contextHandoffs"; + +export { + getAllMiddlewareHooks, + getEnabledMiddlewareHooks, + getComboMiddlewareHooks, + getMiddlewareHook, + createMiddlewareHook, + updateMiddlewareHook, + deleteMiddlewareHook, + recordHookExecution, + insertHookLog, + getHookLogs, + cleanupHookLogs, +} from "./db/middleware"; + +export { + getAllKeyGroups, + getKeyGroup, + getKeyGroupWithPermissions, + createKeyGroup, + updateKeyGroup, + deleteKeyGroup, + getGroupPermissions, + addGroupPermission, + removeGroupPermission, + clearGroupPermissions, + getGroupMembers, + getKeyGroupsForApiKey, + addKeyToGroup, + removeKeyFromGroup, + checkKeyModelAccess, +} from "./db/apiKeyGroups"; + +export { + createRelayToken, + getRelayTokens, + getRelayToken, + getRelayTokenByHash, + updateRelayToken, + deleteRelayToken, + toggleRelayToken, + checkRateLimit, + recordRelayUsage, + getRelayUsage, + getRelayLogs, +} from "./db/relayProxies"; +export type { RelayToken, RelayTokenRow, RelayLogRow, CreateRelayTokenInput, RelayTokenWithSecret } from "./db/relayProxies";