Phase 5 begins. This is the persistence layer for the things the cluster cannot reconstruct — who may call, what they are allowed, what they used. Everything else llmgw knows is still derived from the fleet, which cannot be wrong about itself the way a database can (§6.5). SQLite behind an interface. The plan justified that by write volume, which was the wrong argument: the write path is batched anyway, and a 500-row transaction every 200ms is nothing for either engine. The real argument is the failure mode. Postgres is a second service, and a gateway that stops serving because its accounting database is unreachable is worse than one that buffers some accounting. Embedded means the store shares the gateway's fate instead of adding an independent way to be down. That reasoning stops holding at two instances, where budgets have to be counted somewhere shared — so the interface is here from the start, the SQL is portable, and migrations are per dialect with shared version numbers. Keys are stored as sha256 and nothing else, so a database leak hands over no working credential. The fast hash is deliberate: the input is 256 random bits with no dictionary to attack, so argon2 would buy nothing and charge tens of milliseconds per request. That assumption is written down next to the code that depends on it. Two things the schema now says that it did not: budgets are integer micro-units, because repeated addition of floats drifts; and an empty model allowlist forbids everything while an absent one permits everything, which the resolver has to keep distinct. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
79 lines
2.1 KiB
Go
79 lines
2.1 KiB
Go
package store
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestMintedKeysAreUniqueAndWellFormed(t *testing.T) {
|
|
seen := map[string]struct{}{}
|
|
for range 200 {
|
|
secret, k, err := NewKey("id", "name", "proj")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, dup := seen[secret]; dup {
|
|
t.Fatal("two keys came out identical")
|
|
}
|
|
seen[secret] = struct{}{}
|
|
|
|
if err := ParseKey(secret); err != nil {
|
|
t.Fatalf("a minted key does not parse: %v (%q)", err, secret)
|
|
}
|
|
// Fixed width, so a key's length says nothing about its value and
|
|
// listings line up.
|
|
if len(secret) != len(KeyPrefix)+43 {
|
|
t.Fatalf("length %d: %q", len(secret), secret)
|
|
}
|
|
sum := sha256.Sum256([]byte(secret))
|
|
if string(k.Hash) != string(sum[:]) {
|
|
t.Fatal("the stored hash is not the hash of the key")
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestParseRejectsWhatCannotBeAKey(t *testing.T) {
|
|
// Cheap rejection, so a bearer token from another system never reaches the
|
|
// database at all.
|
|
for _, bad := range []string{
|
|
"", "hello", "Bearer abc",
|
|
"sk-llmgw-",
|
|
"sk-llmgw-short",
|
|
"sk-proj-0123456789012345678901234567890123456789012",
|
|
"sk-llmgw-0123456789012345678901234567890123456789+/=",
|
|
} {
|
|
if err := ParseKey(bad); err == nil {
|
|
t.Errorf("%q was accepted as a key", bad)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestDisplayPrefixRevealsAlmostNothing(t *testing.T) {
|
|
secret, k, _ := NewKey("id", "name", "proj")
|
|
if !strings.HasPrefix(secret, k.Prefix[:len(KeyPrefix)+4]) {
|
|
t.Errorf("prefix %q does not belong to %q", k.Prefix, secret)
|
|
}
|
|
// Four characters of base62 is enough to tell keys apart in a listing and
|
|
// about 15 million times too few to guess the rest.
|
|
body := strings.TrimSuffix(strings.TrimPrefix(k.Prefix, KeyPrefix), "…")
|
|
if len(body) != 4 {
|
|
t.Errorf("the display prefix shows %d characters", len(body))
|
|
}
|
|
}
|
|
|
|
func TestEqualHashIsUsable(t *testing.T) {
|
|
a := HashKey("sk-llmgw-aaa")
|
|
b := HashKey("sk-llmgw-aaa")
|
|
c := HashKey("sk-llmgw-bbb")
|
|
if !EqualHash(a, b) {
|
|
t.Error("equal hashes compared unequal")
|
|
}
|
|
if EqualHash(a, c) {
|
|
t.Error("different hashes compared equal")
|
|
}
|
|
if EqualHash(a, c[:16]) {
|
|
t.Error("a truncated hash compared equal")
|
|
}
|
|
}
|