Hub: accounts, IMVU linking by tagline, proxy sign-in, presence, console
CI / test (push) Failing after 42s

People sign up on the website and link IMVU accounts by putting a code in the account's tagline, which the
hub reads from IMVU's public API (the most recent proof wins; link caps per person). IMVU's age verification
or Access Pass make an account adult. T5DE's proxy signs in with a device code and keeps a WebSocket open,
reporting the IMVU account in use and the community products it wears; /online shows those who opted in.
Moderators get a console: who's online, people, link caps, bans, roles, and the audit log. Tests run the
website and API against an embedded Postgres and a fake IMVU API.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
dhkatzandClaude Opus 5.5 committed 2026-10-29 03:45:52 -07:00
1 parent 54e569bea3
commit b9dc3f0ec1
36 files changed
+3548 -5

No files matched your search

+117
View File
@@ -0,0 +1,117 @@
// Package token issues and checks the hub's access tokens: JWTs signed with Ed25519 ("EdDSA"), short-lived. The
// public keys are published (JWKS), so catalogs can check tokens without asking the hub.
package token
import (
"crypto/ed25519"
"crypto/rand"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"os"
"path/filepath"
"strings"
"time"
)
// Claims are what a token says.
type Claims struct {
Issuer string `json:"iss"`
Subject string `json:"sub"` // "person:<id>" for the hub's own API
Audience string `json:"aud"`
IssuedAt int64 `json:"iat"`
Expires int64 `json:"exp"`
Scope string `json:"scope,omitempty"`
}
// Keys signs and checks tokens.
type Keys struct {
id string
private ed25519.PrivateKey
issuer string
}
// LoadOrCreate reads the signing key from path, making one if there's none.
func LoadOrCreate(path, issuer string) (*Keys, error) {
seed, err := os.ReadFile(path)
if errors.Is(err, os.ErrNotExist) {
seed = make([]byte, ed25519.SeedSize)
if _, err := rand.Read(seed); err != nil {
return nil, err
}
if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {
return nil, err
}
if err := os.WriteFile(path, seed, 0o600); err != nil {
return nil, err
}
} else if err != nil {
return nil, err
}
if len(seed) != ed25519.SeedSize {
return nil, fmt.Errorf("%s: not an Ed25519 seed", path)
}
key := ed25519.NewKeyFromSeed(seed)
pub := key.Public().(ed25519.PublicKey)
return &Keys{id: b64(pub[:8]), private: key, issuer: issuer}, nil
}
func b64(b []byte) string { return base64.RawURLEncoding.EncodeToString(b) }
// Issue signs a token for subject and audience, valid for ttl.
func (k *Keys) Issue(subject, audience, scope string, ttl time.Duration) (string, error) {
now := time.Now()
header, _ := json.Marshal(map[string]string{"alg": "EdDSA", "typ": "JWT", "kid": k.id})
claims, _ := json.Marshal(Claims{Issuer: k.issuer, Subject: subject, Audience: audience, IssuedAt: now.Unix(),
Expires: now.Add(ttl).Unix(), Scope: scope})
signing := b64(header) + "." + b64(claims)
return signing + "." + b64(ed25519.Sign(k.private, []byte(signing))), nil
}
// ErrInvalid means a token is malformed, forged, for someone else, or expired.
var ErrInvalid = errors.New("invalid token")
// Check verifies a token for audience and returns its claims.
func (k *Keys) Check(token, audience string) (*Claims, error) {
parts := strings.Split(token, ".")
if len(parts) != 3 {
return nil, ErrInvalid
}
headerJSON, err1 := base64.RawURLEncoding.DecodeString(parts[0])
claimsJSON, err2 := base64.RawURLEncoding.DecodeString(parts[1])
sig, err3 := base64.RawURLEncoding.DecodeString(parts[2])
if err1 != nil || err2 != nil || err3 != nil {
return nil, ErrInvalid
}
var header struct{ Alg, Kid string }
if json.Unmarshal(headerJSON, &header) != nil || header.Alg != "EdDSA" || header.Kid != k.id {
return nil, ErrInvalid
}
if !ed25519.Verify(k.private.Public().(ed25519.PublicKey), []byte(parts[0]+"."+parts[1]), sig) {
return nil, ErrInvalid
}
var c Claims
if json.Unmarshal(claimsJSON, &c) != nil || c.Issuer != k.issuer || c.Audience != audience || time.Now().Unix() >= c.Expires {
return nil, ErrInvalid
}
return &c, nil
}
// JWKS is the public key set, as JSON (RFC 8037 OKP keys).
func (k *Keys) JWKS() []byte {
pub := k.private.Public().(ed25519.PublicKey)
b, _ := json.Marshal(map[string]any{"keys": []map[string]string{{
"kty": "OKP", "crv": "Ed25519", "alg": "EdDSA", "use": "sig", "kid": k.id, "x": b64(pub),
}}})
return b
}
// Random is a random URL-safe string of n bytes' entropy, for session and refresh tokens.
func Random(n int) string {
b := make([]byte, n)
if _, err := rand.Read(b); err != nil {
panic(err)
}
return b64(b)
}
+44
View File
@@ -0,0 +1,44 @@
package token
import (
"encoding/json"
"path/filepath"
"strings"
"testing"
"time"
)
func TestIssueAndCheck(t *testing.T) {
path := filepath.Join(t.TempDir(), "key")
k, err := LoadOrCreate(path, "https://hub.test")
if err != nil {
t.Fatal(err)
}
tok, _ := k.Issue("person:1", "t5de-hub", "proxy", time.Minute)
c, err := k.Check(tok, "t5de-hub")
if err != nil || c.Subject != "person:1" || c.Scope != "proxy" {
t.Fatalf("%+v %v", c, err)
}
if _, err := k.Check(tok, "a-catalog"); err == nil {
t.Fatal("accepted for another audience")
}
// The same key comes back from the file.
k2, _ := LoadOrCreate(path, "https://hub.test")
if _, err := k2.Check(tok, "t5de-hub"); err != nil {
t.Fatal(err)
}
// Tampering and expiry are caught.
parts := strings.Split(tok, ".")
forged := parts[0] + "." + b64([]byte(`{"iss":"https://hub.test","sub":"person:2","aud":"t5de-hub","exp":9999999999}`)) + "." + parts[2]
if _, err := k.Check(forged, "t5de-hub"); err == nil {
t.Fatal("accepted a forged token")
}
old, _ := k.Issue("person:1", "t5de-hub", "", -time.Second)
if _, err := k.Check(old, "t5de-hub"); err == nil {
t.Fatal("accepted an expired token")
}
var jwks struct{ Keys []map[string]string }
if json.Unmarshal(k.JWKS(), &jwks) != nil || len(jwks.Keys) != 1 || jwks.Keys[0]["crv"] != "Ed25519" {
t.Fatalf("%s", k.JWKS())
}
}