ADR 0583 §1 (insight 2026-11-01) keeps which login a node uses in the vault's state node-logins, one key per node: the login in use, the one rotating and the stage (begun, ready, confirmed), with the list revision that held both. §2, §3 and §5 give the vault's seat rotate-node (with the recorded hand act's id), node-account-confirm (the caller is the proof) and abandon, optional until the vault serves them; the events rotation-begun, node-account-ready, rotation-done and the engine's finding node-account-fallback are named beside them. The codec refuses a stage without a rotation and a rotation to anything but the node's other login; an empty rotating reads as none. Tested as rows of TestNodeRecord and TestTheVaultsSeatPromisesItsIntakeVerbs, no new test function. (issue 774, 774/S38) Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
131 lines
5.4 KiB
Go
131 lines
5.4 KiB
Go
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.<n>` and
|
|
// `node.<n>--b`, each with the node's permissions and its inbox `_INBOX.node.<n>` 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"`
|
|
}
|
|
|
|
// FirstLogin is a node's first login, `node.<n>`, 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)
|
|
}
|
|
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
|
|
}
|