Files
backend/internal/security/tokens.go

67 lines
1.9 KiB
Go
Raw Normal View History

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(), "-")
}