package delivery import ( "encoding/json" "fmt" "strings" "time" ) // **A node's two logins** (novox/hq ADR 0583, issue 774). A node's bus account rotates between the login `node.` and // `node.--b`, each with the node's permissions and its inbox `_INBOX.node.` unchanged (§1). Which one a node uses // is the vault's state `node-logins`, one key per node (§1, insight 2026-11-01), written by the vault alone: the login // in use, the login rotating if any, and the rotation's stage. Each node-engine reads its own node's key alone; the // grants part reads them all and composes both logins while a node rotates (§2). The key lives while the node does: // its rotating part is cleared at rotation-done or abandon, and the key deleted when the node leaves placement. const ( NodeLoginsState = "node-logins" NodeLoginsBucket = VaultModule + "_" + NodeLoginsState NodeLoginsRead = VaultModule + "." + NodeLoginsState // The stages of a rotation (§2-§4): begun when the vault made the other login's value and wrote it rotating; ready // when the bus node's have shows a list holding both, said node-account-ready; confirmed when the engine called // node-account-confirm as the new login. Done (rotation-done) is no stage: the rotating part is cleared. RotationBegun = "begun" RotationReady = "ready" RotationConfirmed = "confirmed" // The vault's rotation verbs on its seat (§2, §3, §5). RotateNodeVerb = "rotate-node" NodeAccountConfirmVerb = "node-account-confirm" AbandonVerb = "abandon" // The vault's events of a rotation, said as its own (§2, §4), each carrying the node's NodeLogin. RotationBegunEvent = "rotation-begun" NodeAccountReadyEvent = "node-account-ready" RotationDoneEvent = "rotation-done" // NodeAccountFallbackFinding is what a node-engine says when it fell back to its old login after repeated // authentication failures with the new one, before rotation-done; the watchman raises it (§6). NodeAccountFallbackFinding = "node-account-fallback" nodeLoginPrefix = "node." // SecondLoginSuffix marks a node's second login, as a module's account rotates over `--b` (ADR 0114). SecondLoginSuffix = "--b" ) // RotationEventSubject is where the vault says event of a rotation. func RotationEventSubject(event string) string { return "mesh.mod." + VaultModule + ".event." + event } // NodeLogin is one key of `node-logins`, the node's name: the login in use, the login rotating to (empty when none, and // then no stage), the rotation's stage, since when the key says what it says, and the user list's revision that held // both logins, from ready on. type NodeLogin struct { V int `json:"v"` Node string `json:"node"` Login string `json:"login"` Rotating string `json:"rotating,omitempty"` Stage string `json:"stage,omitempty"` Since time.Time `json:"since"` ListRevision uint64 `json:"list-revision,omitempty"` // Acts are the hand-act ids that warranted the node's rotations, oldest first (ADR 0583 §2): the vault records each. Acts []string `json:"acts,omitempty"` } // FirstLogin is a node's first login, `node.`, the one every node had before rotation; SecondLogin its other. func FirstLogin(node string) string { return nodeLoginPrefix + node } func SecondLogin(node string) string { return nodeLoginPrefix + node + SecondLoginSuffix } // OtherLogin is the login a node rotates to from login: the second from the first, the first from the second. func OtherLogin(node, login string) string { if login == SecondLogin(node) { return FirstLogin(node) } return SecondLogin(node) } // LoginNode is the node a node login names, either of its two, and whether login is one. func LoginNode(login string) (string, bool) { rest, ok := strings.CutPrefix(login, nodeLoginPrefix) rest = strings.TrimSuffix(rest, SecondLoginSuffix) return rest, ok && checkName("node", rest) == nil } // Rotates says the node is rotating: a login rotating is named. func (l NodeLogin) Rotates() bool { return l.Rotating != "" } func (l NodeLogin) check() error { if err := checkName("node", l.Node); err != nil { return err } if l.Login != FirstLogin(l.Node) && l.Login != SecondLogin(l.Node) { return fmt.Errorf("delivery: the node %s logs in as %q, neither of its two logins", l.Node, l.Login) } for _, act := range l.Acts { if strings.TrimSpace(act) == "" { return fmt.Errorf("delivery: the node %s names an empty hand act among its rotations' warrants", l.Node) } } if !l.Rotates() { if l.Stage != "" || l.ListRevision != 0 { return fmt.Errorf("delivery: the node %s rotates to nothing and names stage %q", l.Node, l.Stage) } return nil } if l.Rotating != OtherLogin(l.Node, l.Login) { return fmt.Errorf("delivery: the node %s rotates from %s to %q, not its other login", l.Node, l.Login, l.Rotating) } switch l.Stage { case RotationBegun, RotationReady, RotationConfirmed: default: return fmt.Errorf("delivery: the node %s's rotation is at stage %q, not begun, ready or confirmed", l.Node, l.Stage) } return nil } // EncodeNodeLogin is l as JSON at Version; it refuses what DecodeNodeLogin refuses. func EncodeNodeLogin(l NodeLogin) ([]byte, error) { if err := l.check(); err != nil { return nil, err } l.V = Version return json.Marshal(l) } // DecodeNodeLogin reads a key of `node-logins`; it refuses an unknown version (ErrUnknownVersion), a login that is not // one of the node's two, a rotation to anything but the other, and a stage without a rotation or an unknown one. An // empty rotating reads as none. func DecodeNodeLogin(data []byte) (NodeLogin, error) { var l NodeLogin if err := decode(data, &l); err != nil { return NodeLogin{}, err } if err := l.check(); err != nil { return NodeLogin{}, err } return l, nil }