feat(federation): add SQL user authentication for peer sync

Merge SQL user authentication with Emma federation sync implementation:

- Add federation_peers table for encrypted credential storage
- Add credentials.go with AES-256-GCM encryption, SHA-256 key derivation
- Extend FederatedStorage interface with credential methods
- Add --user, --password, --sovereignty flags to bd federation add-peer
- Integrate credentials into PushTo/PullFrom/Fetch via withPeerCredentials
- DOLT_REMOTE_USER/PASSWORD env vars protected by mutex for concurrency

Credentials automatically used when syncing with peers that have stored auth.

Continues: bd-wkumz.10, Closes: bd-4p67y

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
beads/crew/jane
2026-01-20 21:15:00 -08:00
committed by Steve Yegge
parent ea51c4b0bd
commit d3d2326a8b
6 changed files with 490 additions and 29 deletions

View File

@@ -2,16 +2,23 @@ package main
import (
"fmt"
"os"
"strings"
"syscall"
"github.com/spf13/cobra"
"github.com/steveyegge/beads/internal/storage"
"github.com/steveyegge/beads/internal/storage/dolt"
"github.com/steveyegge/beads/internal/ui"
"golang.org/x/term"
)
var (
federationPeer string
federationStrategy string
federationUser string
federationPassword string
federationSov string
)
var federationCmd = &cobra.Command{
@@ -66,17 +73,22 @@ Examples:
var federationAddPeerCmd = &cobra.Command{
Use: "add-peer <name> <url>",
Short: "Add a federation peer",
Long: `Add a new federation peer remote.
Short: "Add a federation peer with optional SQL credentials",
Long: `Add a new federation peer remote with optional SQL user authentication.
The URL can be:
- dolthub://org/repo DoltHub hosted repository
- host:port/database Direct dolt sql-server connection
- file:///path/to/repo Local file path (for testing)
Credentials are encrypted and stored locally. They are used automatically
when syncing with the peer. If --user is provided without --password,
you will be prompted for the password interactively.
Examples:
bd federation add-peer town-beta dolthub://acme/town-beta-beads
bd federation add-peer town-gamma 192.168.1.100:3306/beads`,
bd federation add-peer town-gamma 192.168.1.100:3306/beads --user sync-bot
bd federation add-peer partner https://partner.example.com/beads --user admin --password secret`,
Args: cobra.ExactArgs(2),
Run: runFederationAddPeer,
}
@@ -109,6 +121,11 @@ func init() {
// Flags for status
federationStatusCmd.Flags().StringVar(&federationPeer, "peer", "", "Specific peer to check")
// Flags for add-peer (SQL user authentication)
federationAddPeerCmd.Flags().StringVarP(&federationUser, "user", "u", "", "SQL username for authentication")
federationAddPeerCmd.Flags().StringVarP(&federationPassword, "password", "p", "", "SQL password (prompted if --user set without --password)")
federationAddPeerCmd.Flags().StringVar(&federationSov, "sovereignty", "", "Sovereignty tier (T1, T2, T3, T4)")
rootCmd.AddCommand(federationCmd)
}
@@ -288,19 +305,63 @@ func runFederationAddPeer(cmd *cobra.Command, args []string) {
FatalErrorRespectJSON("federation requires Dolt backend")
}
if err := fs.AddRemote(ctx, name, url); err != nil {
FatalErrorRespectJSON("failed to add peer: %v", err)
// If user is provided but password is not, prompt for it
password := federationPassword
if federationUser != "" && password == "" {
fmt.Fprint(os.Stderr, "Password: ")
pwBytes, err := term.ReadPassword(int(syscall.Stdin))
fmt.Fprintln(os.Stderr) // newline after password
if err != nil {
FatalErrorRespectJSON("failed to read password: %v", err)
}
password = string(pwBytes)
}
// Validate sovereignty tier if provided
sov := federationSov
if sov != "" {
sov = strings.ToUpper(sov)
if sov != "T1" && sov != "T2" && sov != "T3" && sov != "T4" {
FatalErrorRespectJSON("invalid sovereignty tier: %s (must be T1, T2, T3, or T4)", federationSov)
}
}
// If credentials provided, use AddFederationPeer to store them
if federationUser != "" {
peer := &storage.FederationPeer{
Name: name,
RemoteURL: url,
Username: federationUser,
Password: password,
Sovereignty: sov,
}
if err := fs.AddFederationPeer(ctx, peer); err != nil {
FatalErrorRespectJSON("failed to add peer: %v", err)
}
} else {
// No credentials, just add the remote
if err := fs.AddRemote(ctx, name, url); err != nil {
FatalErrorRespectJSON("failed to add peer: %v", err)
}
}
if jsonOutput {
outputJSON(map[string]interface{}{
"added": name,
"url": url,
"added": name,
"url": url,
"has_auth": federationUser != "",
"sovereignty": sov,
})
return
}
fmt.Printf("Added peer %s: %s\n", ui.RenderAccent(name), url)
if federationUser != "" {
fmt.Printf(" User: %s (credentials stored)\n", federationUser)
}
if sov != "" {
fmt.Printf(" Sovereignty: %s\n", sov)
}
}
func runFederationRemovePeer(cmd *cobra.Command, args []string) {