import { randomBytes } from 'node:crypto'; import { sha256, newId } from '@rareindex/shared'; import { getDb, apiKeys, eq } from '@rareindex/database'; /** API tiers (ยง158). Limits are per key per minute + a daily quota. */ export const TIERS = { public: { rateLimitPerMinute: 20, dailyQuota: 500 }, free: { rateLimitPerMinute: 60, dailyQuota: 1_000 }, hobby: { rateLimitPerMinute: 120, dailyQuota: 10_000 }, professional: { rateLimitPerMinute: 600, dailyQuota: 100_000 }, research: { rateLimitPerMinute: 600, dailyQuota: 250_000 }, enterprise: { rateLimitPerMinute: 3_000, dailyQuota: 5_000_000 }, } as const; export type Tier = keyof typeof TIERS; export interface MintedKey { id: string; key: string; prefix: string; tier: Tier; } /** Create a key: `ri_live__`; only the hash is stored. The plaintext is returned once. */ export async function mintApiKey(input: { name: string; tier?: Tier; userId?: string | null }): Promise { const tier = input.tier ?? 'free'; const prefix = randomBytes(4).toString('hex'); const secret = randomBytes(24).toString('base64url'); const key = `ri_live_${prefix}_${secret}`; const id = newId('apiKey'); const t = TIERS[tier]; await getDb().insert(apiKeys).values({ id, userId: input.userId ?? null, name: input.name, prefix, keyHash: sha256(key), tier, rateLimitPerMinute: t.rateLimitPerMinute, dailyQuota: t.dailyQuota }); return { id, key, prefix, tier }; } export interface ResolvedKey { id: string; tier: Tier; rateLimitPerMinute: number; dailyQuota: number; userId: string | null; } export function parseBearer(header: string | undefined): string | null { if (!header) return null; const m = header.match(/^Bearer\s+(\S+)$/i); return m ? m[1]! : null; } export async function resolveApiKey(key: string): Promise { if (!key.startsWith('ri_live_')) return null; const [row] = await getDb().select().from(apiKeys).where(eq(apiKeys.keyHash, sha256(key))).limit(1); if (!row || row.revokedAt) return null; return { id: row.id, tier: (row.tier as Tier) ?? 'free', rateLimitPerMinute: row.rateLimitPerMinute, dailyQuota: row.dailyQuota, userId: row.userId }; }