Files
Momento/memento-note/lib/byok.ts
Antigravity bd495be965
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 12s
feat: design system overhaul — sidebar, AI chats, settings, brainstorm, color cleanup
- Sidebar: dynamic brand-accent colors, brainstorm section restyled
- AI chat general: popup panel with expand/collapse, hides when contextual AI open
- AI chat contextual: tabs reordered (Actions first), X close button, height fix
- Settings: all tabs restyled, 6 new color presets (sage, terracotta, iron, etc.)
- Global color cleanup: emerald/orange hardcoded → brand-accent dynamic
- Brainstorm page: orange → brand-accent throughout
- PageEntry animation component added to key pages
- Floating AI button: bg-brand-accent instead of hardcoded black
- i18n: all 15 locales updated with new AI/billing keys
- Billing: freemium quota tracking, BYOK, stripe subscription scaffolding
- Admin: integrated into new design
- AGENTS.md + CLAUDE.md project rules added
2026-05-16 12:59:30 +00:00

138 lines
3.5 KiB
TypeScript

import { prisma } from '@/lib/prisma';
import { decryptApiKey, encryptApiKey, hashApiKey } from '@/lib/crypto';
import {
VALID_PROVIDERS,
type AiGatewayProvider,
} from '@/lib/ai/router';
import { getProviderConfigKeys } from '@/lib/ai/factory';
import type { SubscriptionTier } from '@/lib/entitlements';
const PRO_BYOK_PROVIDERS: readonly AiGatewayProvider[] = [
'openai',
'anthropic',
'deepseek',
'openrouter',
'minimax',
'zai',
];
const BUSINESS_BYOK_PROVIDERS: readonly AiGatewayProvider[] = [
...VALID_PROVIDERS,
].filter((p) => p !== 'ollama' && p !== 'lmstudio') as AiGatewayProvider[];
export function getAllowedByokProviders(
tier: SubscriptionTier,
): readonly AiGatewayProvider[] {
if (tier === 'BASIC') return [];
if (tier === 'PRO') return PRO_BYOK_PROVIDERS;
return BUSINESS_BYOK_PROVIDERS;
}
export function isByokProviderAllowed(
tier: SubscriptionTier,
provider: string,
): boolean {
return getAllowedByokProviders(tier).includes(provider as AiGatewayProvider);
}
export async function hasAnyActiveByok(userId: string): Promise<boolean> {
const count = await prisma.userAPIKey.count({
where: { userId, isActive: true },
});
return count > 0;
}
export async function getActiveByokKey(userId: string, provider: string) {
return prisma.userAPIKey.findFirst({
where: { userId, provider, isActive: true },
});
}
export async function resolveByokApiKey(
userId: string,
providerType: string,
): Promise<{ plaintext: string; provider: string } | null> {
const row = await getActiveByokKey(userId, providerType);
if (!row) return null;
try {
const plaintext = await decryptApiKey(row.encryptedKey);
return { plaintext, provider: row.provider };
} catch (err) {
console.error('[byok] Failed to decrypt key for provider', providerType, err);
return null;
}
}
export async function applyByokToConfig(
billingUserId: string,
providerType: string,
config: Record<string, string>,
): Promise<{ config: Record<string, string>; usedByok: boolean }> {
const byok = await resolveByokApiKey(billingUserId, providerType);
if (!byok) return { config, usedByok: false };
const { apiKeyConfigKey } = getProviderConfigKeys(providerType);
if (!apiKeyConfigKey) return { config, usedByok: false };
return {
config: { ...config, [apiKeyConfigKey]: byok.plaintext },
usedByok: true,
};
}
export async function upsertUserApiKey(params: {
userId: string;
provider: AiGatewayProvider;
plaintext: string;
alias?: string;
model?: string;
}) {
const encryptedKey = await encryptApiKey(params.plaintext);
const keyHash = hashApiKey(params.plaintext);
return prisma.userAPIKey.upsert({
where: {
userId_provider: {
userId: params.userId,
provider: params.provider,
},
},
create: {
userId: params.userId,
provider: params.provider,
alias: params.alias ?? '',
encryptedKey,
keyHash,
model: params.model ?? null,
isActive: true,
},
update: {
alias: params.alias ?? '',
encryptedKey,
keyHash,
model: params.model ?? null,
isActive: true,
},
});
}
export function toPublicApiKey(row: {
provider: string;
alias: string;
model: string | null;
isActive: boolean;
lastUsedAt: Date | null;
createdAt: Date;
updatedAt: Date;
}) {
return {
provider: row.provider,
alias: row.alias,
model: row.model,
isActive: row.isActive,
lastUsedAt: row.lastUsedAt,
createdAt: row.createdAt,
updatedAt: row.updatedAt,
};
}