Files
mesh-sdk/go/delivery/node_logins.go
T
jschoubben f913918a60 Take the tunnel key at issue and place a joining node on the operator's yes, so the hub peers it before redeem
The operator's decision of 2026-11-02 01:05: issue requires overlay_key, the machine's tunnel public key; the vault's
enrolment-token tunnel declares all five fields a token needs (hub_key, hub_endpoint, address, key, range). An
add-node is proposed at the yes with the tunnel key alone; the identity keys come whole at redeem or not at all. A new
remove-node, mesh-enrolment's alone on the same warrant and the proposal's one change, takes a joining node running
nothing off placement when its enrolment ends unconfirmed (RemovedNodes names the key the holder deletes; LayChanges
lays nothing for it and refuses a node that runs modules or holds seats). NodeLogin gains Acts, the hand acts that
warranted its rotations. Tested as rows of TestPlacement, TestNodeRecord and the two seat tests; no new test function
(1438 unit tests).

(issue 774, review round 1b)

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 01:41:36 +01:00

138 lines
5.7 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"`
// 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.<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)
}
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
}