diff --git a/README.md b/README.md index b90c5c8..18e496d 100644 --- a/README.md +++ b/README.md @@ -336,7 +336,9 @@ EdDSA owner authentication. ``` The derived address is `@user_bot@domain`. `agent` may contain letters, digits, -dots, and hyphens, but not underscores. +dots, and hyphens, but not underscores. The derived address is automatically +registered with fmsgid (with default quotas) if it doesn't already exist there +— no manual fmsgid step is needed for derived sub-accounts. Delegated identities such as `@sales@example.com` are not created by this self-service route. They are operator-created with `api-key create-delegation` diff --git a/internal/handlers/subaccounts.go b/internal/handlers/subaccounts.go index 5ea394f..a0bfa70 100644 --- a/internal/handlers/subaccounts.go +++ b/internal/handlers/subaccounts.go @@ -125,6 +125,14 @@ func (h *SubAccountHandler) Create(c *gin.Context) { c.JSON(http.StatusBadRequest, gin.H{"error": "agent must be 1-64 letters/digits/dots/hyphens and contain no underscores"}) return } + // Derived sub-accounts belong to an already-verified owner, so register + // them in fmsgid automatically rather than requiring a manual step. + // RegisterFmsgID never touches an address that already exists there. + if err := middleware.RegisterFmsgID(h.idURL, subAddr); err != nil { + log.Printf("sub-account create: fmsgid registration: addr=%s: %v", subAddr, err) + c.JSON(http.StatusServiceUnavailable, gin.H{"error": "identity service unavailable"}) + return + } if !checkAcceptingFmsgID(c, h.idURL, subAddr) { return } diff --git a/internal/middleware/jwt.go b/internal/middleware/jwt.go index b65abb8..0a93beb 100644 --- a/internal/middleware/jwt.go +++ b/internal/middleware/jwt.go @@ -2,8 +2,10 @@ package middleware import ( + "bytes" "context" "crypto/ed25519" + "encoding/json" "errors" "fmt" "log" @@ -429,6 +431,30 @@ func CheckFmsgID(idURL, addr string) (int, bool, error) { return res.code, res.acceptingNew, nil } +// RegisterFmsgID registers addr with fmsgid using default quotas, idempotently. +// It never modifies an address that already exists there. +func RegisterFmsgID(idURL, addr string) error { + payload, err := json.Marshal(struct { + Address string `json:"address"` + }{Address: addr}) + if err != nil { + return err + } + + url := strings.TrimRight(idURL, "/") + "/fmsgid" + resp, err := fmsgIDClient.Post(url, "application/json", bytes.NewReader(payload)) //nolint:gosec // URL constructed from trusted config + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated { + return fmt.Errorf("fmsgid registration failed: status %d", resp.StatusCode) + } + fmsgIDCache.Delete(addr) + return nil +} + func fetchFmsgID(idURL, addr string) (fmsgIDResult, error) { url := strings.TrimRight(idURL, "/") + "/fmsgid/" + addr resp, err := fmsgIDClient.Get(url) //nolint:gosec // URL constructed from trusted config + validated addr