mirror of
https://github.com/multica-ai/multica.git
synced 2026-08-02 10:05:41 +02:00
Phase 1 of RFC MUL-2297 — DB and credential contract for the new runtime
install flow. CLI/daemon/UI plumbing lands in later phases.
Schema (migration 091):
- daemon_token.revoked_at — explicit revoke replaces TTL-based expiry; the
exchange path now mints daemon_token rows with a ~100y expires_at so the
cleanup query stays intact while the credential is effectively long-lived
until revoked.
- install_token — short-lived (15m) single-use credential. used_at IS NULL
is the atomic gate enforced inside the UPDATE so a concurrent second
exchange returns zero rows.
API:
- POST /api/workspaces/{id}/install-tokens — admin-only mint, returns mit_
once; only the hash is stored.
- POST /api/install-tokens/exchange — public (the mit_ is the credential);
atomically consumes the install_token and returns a fresh mdt_.
Error contract for Phase 2 daemon installer:
- 401 invalid_install_token — unknown hash OR expired
- 401 install_token_already_used — hash exists but used_at IS NOT NULL
Co-authored-by: multica-agent <github@multica.ai>
66 lines
1.7 KiB
Go
66 lines
1.7 KiB
Go
package auth
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"os"
|
|
"sync"
|
|
)
|
|
|
|
const defaultJWTSecret = "multica-dev-secret-change-in-production"
|
|
|
|
var (
|
|
jwtSecret []byte
|
|
jwtSecretOnce sync.Once
|
|
)
|
|
|
|
func JWTSecret() []byte {
|
|
jwtSecretOnce.Do(func() {
|
|
secret := os.Getenv("JWT_SECRET")
|
|
if secret == "" {
|
|
secret = defaultJWTSecret
|
|
}
|
|
jwtSecret = []byte(secret)
|
|
})
|
|
|
|
return jwtSecret
|
|
}
|
|
|
|
// GeneratePATToken creates a new personal access token: "mul_" + 40 random hex chars.
|
|
func GeneratePATToken() (string, error) {
|
|
b := make([]byte, 20) // 20 bytes = 40 hex chars
|
|
if _, err := rand.Read(b); err != nil {
|
|
return "", fmt.Errorf("generate PAT token: %w", err)
|
|
}
|
|
return "mul_" + hex.EncodeToString(b), nil
|
|
}
|
|
|
|
// GenerateDaemonToken creates a new daemon auth token: "mdt_" + 40 random hex chars.
|
|
func GenerateDaemonToken() (string, error) {
|
|
b := make([]byte, 20) // 20 bytes = 40 hex chars
|
|
if _, err := rand.Read(b); err != nil {
|
|
return "", fmt.Errorf("generate daemon token: %w", err)
|
|
}
|
|
return "mdt_" + hex.EncodeToString(b), nil
|
|
}
|
|
|
|
// GenerateInstallToken creates a new single-use install token: "mit_" + 40
|
|
// random hex chars. The raw token is shown to the user once at mint time
|
|
// and exchanged by the daemon installer for a long-lived daemon_token.
|
|
// See RFC MUL-2297.
|
|
func GenerateInstallToken() (string, error) {
|
|
b := make([]byte, 20) // 20 bytes = 40 hex chars
|
|
if _, err := rand.Read(b); err != nil {
|
|
return "", fmt.Errorf("generate install token: %w", err)
|
|
}
|
|
return "mit_" + hex.EncodeToString(b), nil
|
|
}
|
|
|
|
// HashToken returns the hex-encoded SHA-256 hash of a token string.
|
|
func HashToken(token string) string {
|
|
h := sha256.Sum256([]byte(token))
|
|
return hex.EncodeToString(h[:])
|
|
}
|