SPB Git forge

spb/polyllm

Public
15commits 1branches 0releases
2.2 MBsize
maindefault branch
13 days agolast push
TypeScript 97.4% SQL 1% JavaScript 0.9% CSS 0.6%
6.8 KB · 128 lines tsx
Raw Blame History
1import * as React from "react";2import { ArrowDown, ArrowRight, Cloud, KeyRound, Lock, LockOpen, Monitor, ScrollText, ShieldCheck } from "lucide-react";3import { cn } from "@/lib/utils";4import { Reveal } from "./reveal";56/* ------------------------------------------------------------------------------------------------7 * Browser → PolyLLM encrypted server layer → Provider8 * ---------------------------------------------------------------------------------------------- */9function Node({ icon, title, sub, items, tone = "default", className }: { icon: React.ReactNode; title: string; sub: string; items: string[]; tone?: "default" | "accent"; className?: string }) {10  return (11    <div className={cn("relative flex min-w-0 flex-1 flex-col rounded-2xl p-4 sm:p-5", tone === "accent" ? "bg-accent-soft/60 ring-1 ring-accent/30" : "panel", className)}>12      <div className="flex items-center gap-2.5">13        <span className={cn("flex size-9 shrink-0 items-center justify-center rounded-lg [&_svg]:size-4.5", tone === "accent" ? "bg-accent text-accent-fg" : "bg-bg-elevated text-fg-muted")}>{icon}</span>14        <div className="min-w-0">15          <p className="text-[15px] font-semibold tracking-tight">{title}</p>16          <p className="truncate text-[12.5px] text-fg-muted">{sub}</p>17        </div>18      </div>19      <ul className="mt-3.5 space-y-1.5 text-[13px] leading-5 text-fg-muted">20        {items.map((it) => (21          <li key={it} className="flex gap-2">22            <span className="mt-[7px] size-1.5 shrink-0 rounded-full bg-border-strong" aria-hidden />23            <span>{it}</span>24          </li>25        ))}26      </ul>27    </div>28  );29}3031function Arrow({ label }: { label: string }) {32  return (33    <div className="flex items-center justify-center lg:flex-col" aria-hidden>34      <span className="hidden font-mono text-[10.5px] uppercase tracking-[0.1em] text-fg-subtle lg:block">{label}</span>35      <ArrowDown className="size-4 text-fg-subtle lg:hidden" />36      <ArrowRight className="hidden size-4 text-fg-subtle lg:block" />37    </div>38  );39}4041export function SecurityFlow() {42  return (43    <Reveal className="grid gap-3 lg:grid-cols-[1fr_auto_1.2fr_auto_1fr] lg:items-stretch lg:gap-4">44      <Node icon={<Monitor />} title="Your browser" sub="never holds a provider key" items={["Sends the key once, over TLS, when you connect a provider", "Receives only a hint like sk-••••••••9A2K afterwards", "Session in an HttpOnly cookie — not readable by scripts"]} />45      <Arrow label="TLS" />46      <Node47        icon={<Lock />}48        title="PolyLLM server"49        sub="encrypted key layer"50        tone="accent"51        items={["Encrypts the key with AES-256-GCM (HKDF-derived data key, per-user AAD) before it is stored", "Decrypts in memory only when it is about to call the provider", "Never logs, returns or serializes the key; logs pass through a secret redactor"]}52      />53      <Arrow label="TLS" />54      <Node icon={<Cloud />} title="Provider API" sub="the one you selected" items={["Receives your prompt and your key directly from PolyLLM's server", "Bills your own account at list price — no markup, no reseller", "Nothing is routed through third-party inference brokers"]} />55    </Reveal>56  );57}5859/* ------------------------------------------------------------------------------------------------60 * API key lifecycle61 * ---------------------------------------------------------------------------------------------- */62const LIFECYCLE: { icon: React.ReactNode; title: string; body: React.ReactNode; code?: string }[] = [63  {64    icon: <KeyRound />,65    title: "1 · You paste a key",66    body: "Sent once over TLS to PUT /api/providers. Checked for shape (8–512 characters, no whitespace) and validated against the provider with a real request, so you know it works before it is saved.",67  },68  {69    icon: <Lock />,70    title: "2 · Encrypted with AES-256-GCM",71    body: (72      <>73        The data key is derived from <code>API_KEY_ENCRYPTION_SECRET</code> with HKDF-SHA256 (info <code>polyllm:provider-key:v1</code>), so the raw secret never touches the cipher. A random 12-byte IV and a 16-byte authentication tag protect every envelope. The additional authenticated data is <code>userId|provider</code>: a ciphertext copied to another user or provider cannot be decrypted.74      </>75    ),76    code: "v1.<iv 12B>.<authTag 16B>.<ciphertext>   (base64url)",77  },78  {79    icon: <ShieldCheck />,80    title: "3 · Stored with a hint and a fingerprint",81    body: (82      <>83        The database row holds the envelope, a display hint (recognizable prefix + last 4 characters) and a SHA-256 fingerprint used only to detect “same key” on rotation. <code>PublicConnection</code>, the shape the browser sees, has no key field at all.84      </>85    ),86  },87  {88    icon: <LockOpen />,89    title: "4 · Decrypted only for the request",90    body: (91      <>92        <code>getDecryptedKey()</code> runs immediately before a provider call — chat, Arena, key validation, model sync — and the plaintext lives in memory only for that call. It is never written to logs, error messages or exports.93      </>94    ),95  },96  {97    icon: <ScrollText />,98    title: "5 · Audited, rotatable, deletable",99    body: (100      <>101        Adding, replacing and removing a key writes an audit event (<code>provider.key_added</code>, <code>provider.key_replaced</code>, <code>provider.key_deleted</code>) with the requesting IP. Removing a key deletes the encrypted row immediately; revoking it in the provider console makes the stored copy useless.102      </>103    ),104  },105];106107export function KeyLifecycle() {108  return (109    <ol className="relative space-y-6 border-l border-border pl-6 sm:space-y-7 sm:pl-8">110      {LIFECYCLE.map((step, i) => (111        <Reveal key={step.title} as="li" delay={0.04 * i} className="relative">112          <span className="absolute -left-[calc(1.5rem+9px)] top-1 flex size-[18px] items-center justify-center rounded-full bg-bg ring-1 ring-border sm:-left-[calc(2rem+9px)]" aria-hidden>113            <span className="size-2 rounded-full bg-accent" />114          </span>115          <div className="flex items-start gap-3">116            <span className="hidden size-8 shrink-0 items-center justify-center rounded-lg bg-accent-soft text-accent sm:flex [&_svg]:size-4">{step.icon}</span>117            <div className="min-w-0">118              <h3 className="text-[15px] font-semibold tracking-tight">{step.title}</h3>119              <p className="mt-1 text-[14px] leading-6 text-fg-muted [&_code]:rounded [&_code]:bg-bg-muted [&_code]:px-1 [&_code]:py-0.5 [&_code]:font-mono [&_code]:text-[12px] [&_code]:text-fg">{step.body}</p>120              {step.code ? <pre className="mt-2 overflow-x-auto rounded-lg bg-bg-inset px-3 py-2 font-mono text-[12px] text-fg-muted">{step.code}</pre> : null}121            </div>122          </div>123        </Reveal>124      ))}125    </ol>126  );127}128