package security import ( "crypto/rand" "encoding/hex" "fmt" "strings" ) // GenerateToken returns a cryptographically random access token. The prefix // makes accidentally-leaked tokens grep-able in logs and lets frontends tell // app tokens apart from other credential kinds. func GenerateToken(prefix string) (string, error) { b := make([]byte, 24) if _, err := rand.Read(b); err != nil { return "", fmt.Errorf("read random: %w", err) } p := strings.TrimSpace(prefix) if p == "" { p = "ca" } return fmt.Sprintf("%s_live_%s", p, hex.EncodeToString(b)), nil } // GenerateAPIKeyPreview returns the last 4 characters of an API key, for // display in the admin UI ("...ab12") so operators can identify a key // without exposing it. func GenerateAPIKeyPreview(key string) string { key = strings.TrimSpace(key) if len(key) <= 4 { return "" } return key[len(key)-4:] } // RandomSlug returns a short random slug for cases where a name has no // ASCII slug characters (pure CJK names, emoji, ...). Used as a fallback so // every app/skill is still addressable on disk. func RandomSlug() string { b := make([]byte, 5) if _, err := rand.Read(b); err != nil { // rand.Read failing is catastrophic; fall back to a fixed value so we // never return an empty slug. return "app" } return "app-" + hex.EncodeToString(b) } // Slugify converts an arbitrary string into a filesystem- and URL-safe slug. // Used for app and skill identifiers that become directory / file names. // Non-ASCII characters (CJK, etc.) are dropped; callers that need a guaranteed // usable slug should fall back to a generated token when the result is empty. func Slugify(s string) string { s = strings.ToLower(strings.TrimSpace(s)) var b strings.Builder b.Grow(len(s)) for _, r := range s { switch { case r >= 'a' && r <= 'z', r >= '0' && r <= '9': b.WriteRune(r) case r == ' ' || r == '-' || r == '_': b.WriteRune('-') } } return strings.Trim(b.String(), "-") }