package link import ( "context" "encoding/json" "fmt" "time" ) // Signer is whatever holds the control plane's signing key. type Signer interface { Sign(ctx context.Context, message []byte) ([]byte, error) } // ActingGate is what every send passes before it is made (novox/hq to-be 45 §6): whether this process // may act under the controller's lease. Set by the controller; nil passes every send — a test, a tool. var ActingGate func(ctx context.Context) error // Declare sends a node what it should be, signed. // // The signature is over the declaration exactly as it is published — the same bytes the node // verifies. Anything that re-encoded between here and there would produce a signature over // something else, and the node would refuse a declaration that was genuinely the mesh's. // // Published to the node's own queue, which its account alone may read. func Declare(ctx context.Context, bus Bus, signer Signer, node string, declaration []byte, timeout time.Duration) error { if !json.Valid(declaration) { return fmt.Errorf("refusing to send %s something that is not a declaration", node) } // **At the send, not only where it was composed**: a lease lost between the two stops this one. if ActingGate != nil { if err := ActingGate(ctx); err != nil { return fmt.Errorf("%s was not sent its declaration: %w", node, err) } } signature, err := signer.Sign(ctx, declaration) if err != nil { return fmt.Errorf("cannot sign a declaration for %s: %w", node, err) } body, err := Envelope(declaration, signature) if err != nil { return err } // **Measured before it is sent** (novox/hq issue 1689): the bare bus error said only "maximum payload // exceeded", on 2026-10-21 for the control node's declaration, 2 KB over. The limit is the connection's own. if limit := payloadLimitOf(bus); limit > 0 { size := int64(len(body)) if DeclarationMeasured != nil { DeclarationMeasured(ctx, node, size, limit) } if size > limit { return fmt.Errorf("%s's declaration is %d bytes on the wire (%d bytes of declaration, signed and encoded), "+ "over the bus's largest message of %d bytes by %d: it cannot be sent (novox/hq issue 1689)", node, size, len(declaration), limit, size-limit) } } publish, cancel := context.WithTimeout(ctx, timeout) defer cancel() return bus.PublishDeclaration(publish, node, body) } // Envelope is the message a declaration goes on the bus as: the declaration and its signature, each as base64 in one JSON // document. Its length is what the bus's largest message bounds. func Envelope(declaration, signature []byte) ([]byte, error) { return json.Marshal(Signed{Declaration: declaration, Signature: signature}) } // NearTheLimit is the share of the bus's largest message at which a declaration is said to be near it: three quarters, so that // a declaration which should stay far from the limit raises the condition while there is still room to act (novox/hq ADR 0486 // §8; the first fix of issue 1689 warned at nine tenths while every file was still inline). const NearTheLimit = 0.75 // DeclarationMeasured is told, at every send of a declaration, its size on the wire and the bus's largest message, // so the controller raises a condition when it nears the limit and clears it when it is back under (issue 1689). // Set by the controller; nil in a test or a tool. var DeclarationMeasured func(ctx context.Context, node string, size, limit int64) // payloadLimiter is a bus that knows its largest message. type payloadLimiter interface{ MaxPayload() int64 } // payloadLimitOf is the bus's largest message in bytes, or 0 when the bus does not say. func payloadLimitOf(bus Bus) int64 { if l, ok := bus.(payloadLimiter); ok { return l.MaxPayload() } return 0 }