SPB Git forge

spb/rareindex

Public
54commits 1branches 0releases
7.1 MBsize
maindefault branch
10 days agolast push
TypeScript 61.9% HTML 37.2% SQL 0.7%
2.2 KB · 55 lines typescript
Raw Blame History
1import { randomBytes } from 'node:crypto';2import { sha256, newId } from '@rareindex/shared';3import { getDb, apiKeys, eq } from '@rareindex/database';45/** API tiers (§158). Limits are per key per minute + a daily quota. */6export const TIERS = {7  public: { rateLimitPerMinute: 20, dailyQuota: 500 },8  free: { rateLimitPerMinute: 60, dailyQuota: 1_000 },9  hobby: { rateLimitPerMinute: 120, dailyQuota: 10_000 },10  professional: { rateLimitPerMinute: 600, dailyQuota: 100_000 },11  research: { rateLimitPerMinute: 600, dailyQuota: 250_000 },12  enterprise: { rateLimitPerMinute: 3_000, dailyQuota: 5_000_000 },13} as const;14export type Tier = keyof typeof TIERS;1516export interface MintedKey {17  id: string;18  key: string;19  prefix: string;20  tier: Tier;21}2223/** Create a key: `ri_live_<prefix>_<secret>`; only the hash is stored. The plaintext is returned once. */24export async function mintApiKey(input: { name: string; tier?: Tier; userId?: string | null }): Promise<MintedKey> {25  const tier = input.tier ?? 'free';26  const prefix = randomBytes(4).toString('hex');27  const secret = randomBytes(24).toString('base64url');28  const key = `ri_live_${prefix}_${secret}`;29  const id = newId('apiKey');30  const t = TIERS[tier];31  await getDb().insert(apiKeys).values({ id, userId: input.userId ?? null, name: input.name, prefix, keyHash: sha256(key), tier, rateLimitPerMinute: t.rateLimitPerMinute, dailyQuota: t.dailyQuota });32  return { id, key, prefix, tier };33}3435export interface ResolvedKey {36  id: string;37  tier: Tier;38  rateLimitPerMinute: number;39  dailyQuota: number;40  userId: string | null;41}4243export function parseBearer(header: string | undefined): string | null {44  if (!header) return null;45  const m = header.match(/^Bearer\s+(\S+)$/i);46  return m ? m[1]! : null;47}4849export async function resolveApiKey(key: string): Promise<ResolvedKey | null> {50  if (!key.startsWith('ri_live_')) return null;51  const [row] = await getDb().select().from(apiKeys).where(eq(apiKeys.keyHash, sha256(key))).limit(1);52  if (!row || row.revokedAt) return null;53  return { id: row.id, tier: (row.tier as Tier) ?? 'free', rateLimitPerMinute: row.rateLimitPerMinute, dailyQuota: row.dailyQuota, userId: row.userId };54}55