diff --git a/operator-and-conversation/go/clients/operatorchannelclient/bench_test.go b/operator-and-conversation/go/clients/operatorchannelclient/bench_test.go new file mode 100644 index 0000000..f8a7512 --- /dev/null +++ b/operator-and-conversation/go/clients/operatorchannelclient/bench_test.go @@ -0,0 +1,58 @@ +package operatorchannelclient + +import ( + "bytes" + "encoding/json" + "testing" + "time" + + seat "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/seats/operatorchannel" + "git.novox.be/novox/mesh-sdk/go/channel" +) + +// TestTheBenchShapesAreTheChannelsWire: each shape the benches channel and intake carry, written by mesh-sdk's channel +// package, reads into the contract's type with no field dropped and reads back as the same wire. +func TestTheBenchShapesAreTheChannelsWire(t *testing.T) { + at := time.Date(2026, 11, 5, 12, 0, 0, 0, time.UTC) + who := channel.Sender{Identity: "42", Display: "Op", Authenticated: true} + cases := []struct { + name string + sdk any + into any + }{ + {"message", channel.Message{Handle: "h1", Title: "t", Body: "b", Buttons: []channel.Button{{Label: "Approve", Ticket: "tk"}}, + Note: "n", Urgent: true, Silent: true, Reply: "r", To: "42", Secret: true}, &seat.Message{}}, + {"a send", channel.Message{Handle: "h2", Title: "t", Body: "b"}, &seat.Message{}}, + {"choice", channel.Choice{ID: "c1", Ticket: "tk", Handle: "h1", Sender: who, At: at}, &seat.Choice{}}, + {"link", channel.LinkAsked{ID: "l1", Sender: who, At: at}, &seat.LinkAsked{}}, + {"failed", channel.Failed{Handle: "h1", Why: "blocked", Permanent: true, At: at}, &seat.Failed{}}, + {"standing", channel.Standing{Ready: true, Why: "w", EditsSilently: true, At: at}, &seat.Standing{}}, + {"code", channel.Code{Sender: who, Code: "123456", Purpose: "approve"}, &seat.Code{}}, + {"proof answer", channel.ProofAnswer{Accepted: true, Words: "ok"}, &seat.ProofAnswer{}}, + } + for _, c := range cases { + wire, err := json.Marshal(c.sdk) + if err != nil { + t.Fatal(err) + } + dec := json.NewDecoder(bytes.NewReader(wire)) + dec.DisallowUnknownFields() + if err := dec.Decode(c.into); err != nil { + t.Errorf("%s: %v", c.name, err) + continue + } + back, _ := json.Marshal(c.into) + var got, want any + _ = json.Unmarshal(back, &got) + _ = json.Unmarshal(wire, &want) + if !jsonEqual(got, want) { + t.Errorf("%s: reads back as %s, want %s", c.name, back, wire) + } + } +} + +func jsonEqual(a, b any) bool { + x, _ := json.Marshal(a) + y, _ := json.Marshal(b) + return bytes.Equal(x, y) +} diff --git a/operator-and-conversation/go/seats/operatorchannel/contract.go b/operator-and-conversation/go/seats/operatorchannel/contract.go index 540d371..f17556f 100644 --- a/operator-and-conversation/go/seats/operatorchannel/contract.go +++ b/operator-and-conversation/go/seats/operatorchannel/contract.go @@ -149,6 +149,36 @@ type Ask struct { WholeDetails string `json:"whole-details,omitempty"` } +// Button is one answer a channel offers: its label and the router's one-time ticket for it. +type Button struct { + // the button's label + Label string `json:"label"` + // the router's one-time ticket for it + Ticket string `json:"ticket"` +} + +// Choice is a button chosen on a channel, said on the bench intake as the event `choice`. +type Choice struct { + // when, RFC 3339 + At string `json:"at"` + // the message's handle + Handle string `json:"handle,omitempty"` + // the channel's own for this arrival, unique, so the router acts on it once + ID string `json:"id"` + Sender Sender `json:"sender"` + // the ticket of the button + Ticket string `json:"ticket"` +} + +// Code is a code a person typed on a channel, asked as the proof `code` on the bench intake by request and reply: never in an event, never kept. +type Code struct { + // the code + Code string `json:"code"` + // what it is for + Purpose string `json:"purpose"` + Sender Sender `json:"sender"` +} + // ConditionActionsItem is a type of the contract. type ConditionActionsItem struct { Arguments map[string]string `json:"arguments,omitempty"` @@ -197,6 +227,51 @@ type Condition struct { V int64 `json:"v"` } +// Failed is a message a channel could not deliver, said on the bench intake as the event `failed`. +type Failed struct { + // when, RFC 3339 + At string `json:"at"` + // the message's handle + Handle string `json:"handle"` + // trying again cannot help + Permanent bool `json:"permanent,omitempty"` + // why + Why string `json:"why"` +} + +// LinkAsked is somebody on a channel asking to be linked as the operator, said on the bench intake as the event `link`. +type LinkAsked struct { + // when, RFC 3339 + At string `json:"at"` + // the channel's own for this arrival + ID string `json:"id"` + Sender Sender `json:"sender"` +} + +// Message is the work a channel takes on the bench channel: shown with buttons (show), shown again in place (edit) or said (send); the words are the router's, shown as given. +type Message struct { + // its body + Body string `json:"body"` + // the answers it offers (none for a send) + Buttons []Button `json:"buttons,omitempty"` + // the router's name for it, the same across a show and its edits + Handle string `json:"handle"` + // its secondary words after the body and the buttons: who asked and when, what each answer does, when it expires, the ask's details + Note string `json:"note,omitempty"` + // the choice or link request this answers, by its id: the channel shows it where that was made + Reply string `json:"reply,omitempty"` + // the words carry something shown once (a link's code): the channel keeps no copy + Secret bool `json:"secret,omitempty"` + // shown without notifying + Silent bool `json:"silent,omitempty"` + // its title + Title string `json:"title"` + // the account linked as the operator on this kind, where the channel sends what is not a reply; empty when none is linked + To string `json:"to,omitempty"` + // urgent + Urgent bool `json:"urgent,omitempty"` +} + // Option is a type of the contract. type Option struct { // the digest of exactly what the asker performs when it is chosen @@ -225,6 +300,36 @@ type Person struct { Who string `json:"who"` } +// ProofAnswer is the router's answer to a proof. +type ProofAnswer struct { + // whether it was taken + Accepted bool `json:"accepted"` + // words to say to the person + Words string `json:"words"` +} + +// Sender is who a channel's service says sent something; the router alone judges whether that is the operator. +type Sender struct { + // whether the service authenticated it + Authenticated bool `json:"authenticated"` + // the name it shows + Display string `json:"display,omitempty"` + // the account + Identity string `json:"identity"` +} + +// Standing is what a channel says of itself at least every five minutes and whenever it changes, on the bench intake as the event `standing`. +type Standing struct { + // when, RFC 3339 + At string `json:"at"` + // its edits notify nobody + EditsSilently bool `json:"edits-silently,omitempty"` + // it can send now + Ready bool `json:"ready"` + // why not + Why string `json:"why,omitempty"` +} + // Warrant is a type of the contract. type Warrant struct { // what it was about diff --git a/operator-and-conversation/seats/operator-channel/seat.json b/operator-and-conversation/seats/operator-channel/seat.json index 0bd8c21..b40e63e 100644 --- a/operator-and-conversation/seats/operator-channel/seat.json +++ b/operator-and-conversation/seats/operator-channel/seat.json @@ -563,6 +563,246 @@ "subject", "state" ] + }, + "Button": { + "type": "object", + "description": "one answer a channel offers: its label and the router's one-time ticket for it", + "properties": { + "label": { + "type": "string", + "description": "the button's label" + }, + "ticket": { + "type": "string", + "description": "the router's one-time ticket for it" + } + }, + "required": [ + "label", + "ticket" + ] + }, + "Message": { + "type": "object", + "description": "the work a channel takes on the bench channel: shown with buttons (show), shown again in place (edit) or said (send); the words are the router's, shown as given", + "properties": { + "handle": { + "type": "string", + "description": "the router's name for it, the same across a show and its edits" + }, + "title": { + "type": "string", + "description": "its title" + }, + "body": { + "type": "string", + "description": "its body" + }, + "buttons": { + "type": "array", + "items": { + "$ref": "#/defs/Button" + }, + "description": "the answers it offers (none for a send)" + }, + "note": { + "type": "string", + "description": "its secondary words after the body and the buttons: who asked and when, what each answer does, when it expires, the ask's details" + }, + "urgent": { + "type": "boolean", + "description": "urgent" + }, + "silent": { + "type": "boolean", + "description": "shown without notifying" + }, + "reply": { + "type": "string", + "description": "the choice or link request this answers, by its id: the channel shows it where that was made" + }, + "to": { + "type": "string", + "description": "the account linked as the operator on this kind, where the channel sends what is not a reply; empty when none is linked" + }, + "secret": { + "type": "boolean", + "description": "the words carry something shown once (a link's code): the channel keeps no copy" + } + }, + "required": [ + "handle", + "title", + "body" + ] + }, + "Sender": { + "type": "object", + "description": "who a channel's service says sent something; the router alone judges whether that is the operator", + "properties": { + "identity": { + "type": "string", + "description": "the account" + }, + "display": { + "type": "string", + "description": "the name it shows" + }, + "authenticated": { + "type": "boolean", + "description": "whether the service authenticated it" + } + }, + "required": [ + "identity", + "authenticated" + ] + }, + "Choice": { + "type": "object", + "description": "a button chosen on a channel, said on the bench intake as the event `choice`", + "properties": { + "id": { + "type": "string", + "description": "the channel's own for this arrival, unique, so the router acts on it once" + }, + "ticket": { + "type": "string", + "description": "the ticket of the button" + }, + "handle": { + "type": "string", + "description": "the message's handle" + }, + "sender": { + "$ref": "#/defs/Sender" + }, + "at": { + "type": "string", + "description": "when, RFC 3339" + } + }, + "required": [ + "id", + "ticket", + "sender", + "at" + ] + }, + "LinkAsked": { + "type": "object", + "description": "somebody on a channel asking to be linked as the operator, said on the bench intake as the event `link`", + "properties": { + "id": { + "type": "string", + "description": "the channel's own for this arrival" + }, + "sender": { + "$ref": "#/defs/Sender" + }, + "at": { + "type": "string", + "description": "when, RFC 3339" + } + }, + "required": [ + "id", + "sender", + "at" + ] + }, + "Failed": { + "type": "object", + "description": "a message a channel could not deliver, said on the bench intake as the event `failed`", + "properties": { + "handle": { + "type": "string", + "description": "the message's handle" + }, + "why": { + "type": "string", + "description": "why" + }, + "permanent": { + "type": "boolean", + "description": "trying again cannot help" + }, + "at": { + "type": "string", + "description": "when, RFC 3339" + } + }, + "required": [ + "handle", + "why", + "at" + ] + }, + "Standing": { + "type": "object", + "description": "what a channel says of itself at least every five minutes and whenever it changes, on the bench intake as the event `standing`", + "properties": { + "ready": { + "type": "boolean", + "description": "it can send now" + }, + "why": { + "type": "string", + "description": "why not" + }, + "edits-silently": { + "type": "boolean", + "description": "its edits notify nobody" + }, + "at": { + "type": "string", + "description": "when, RFC 3339" + } + }, + "required": [ + "ready", + "at" + ] + }, + "Code": { + "type": "object", + "description": "a code a person typed on a channel, asked as the proof `code` on the bench intake by request and reply: never in an event, never kept", + "properties": { + "sender": { + "$ref": "#/defs/Sender" + }, + "code": { + "type": "string", + "description": "the code" + }, + "purpose": { + "type": "string", + "description": "what it is for" + } + }, + "required": [ + "sender", + "code", + "purpose" + ] + }, + "ProofAnswer": { + "type": "object", + "description": "the router's answer to a proof", + "properties": { + "accepted": { + "type": "boolean", + "description": "whether it was taken" + }, + "words": { + "type": "string", + "description": "words to say to the person" + } + }, + "required": [ + "accepted", + "words" + ] } } } diff --git a/operator-and-conversation/ts/seats/operatorchannel/src/index.ts b/operator-and-conversation/ts/seats/operatorchannel/src/index.ts index 4b9e593..fbffec0 100644 --- a/operator-and-conversation/ts/seats/operatorchannel/src/index.ts +++ b/operator-and-conversation/ts/seats/operatorchannel/src/index.ts @@ -108,6 +108,36 @@ export interface Ask { "whole-details"?: string; } +/** one answer a channel offers: its label and the router's one-time ticket for it */ +export interface Button { + /** the button's label */ + "label": string; + /** the router's one-time ticket for it */ + "ticket": string; +} + +/** a button chosen on a channel, said on the bench intake as the event `choice` */ +export interface Choice { + /** when, RFC 3339 */ + "at": string; + /** the message's handle */ + "handle"?: string; + /** the channel's own for this arrival, unique, so the router acts on it once */ + "id": string; + "sender": Sender; + /** the ticket of the button */ + "ticket": string; +} + +/** a code a person typed on a channel, asked as the proof `code` on the bench intake by request and reply: never in an event, never kept */ +export interface Code { + /** the code */ + "code": string; + /** what it is for */ + "purpose": string; + "sender": Sender; +} + export interface ConditionActionsItem { "arguments"?: Record; /** label */ @@ -153,6 +183,51 @@ export interface Condition { "v": number; } +/** a message a channel could not deliver, said on the bench intake as the event `failed` */ +export interface Failed { + /** when, RFC 3339 */ + "at": string; + /** the message's handle */ + "handle": string; + /** trying again cannot help */ + "permanent"?: boolean; + /** why */ + "why": string; +} + +/** somebody on a channel asking to be linked as the operator, said on the bench intake as the event `link` */ +export interface LinkAsked { + /** when, RFC 3339 */ + "at": string; + /** the channel's own for this arrival */ + "id": string; + "sender": Sender; +} + +/** the work a channel takes on the bench channel: shown with buttons (show), shown again in place (edit) or said (send); the words are the router's, shown as given */ +export interface Message { + /** its body */ + "body": string; + /** the answers it offers (none for a send) */ + "buttons"?: Button[]; + /** the router's name for it, the same across a show and its edits */ + "handle": string; + /** its secondary words after the body and the buttons: who asked and when, what each answer does, when it expires, the ask's details */ + "note"?: string; + /** the choice or link request this answers, by its id: the channel shows it where that was made */ + "reply"?: string; + /** the words carry something shown once (a link's code): the channel keeps no copy */ + "secret"?: boolean; + /** shown without notifying */ + "silent"?: boolean; + /** its title */ + "title": string; + /** the account linked as the operator on this kind, where the channel sends what is not a reply; empty when none is linked */ + "to"?: string; + /** urgent */ + "urgent"?: boolean; +} + export interface Option { /** the digest of exactly what the asker performs when it is chosen */ "binds"?: string; @@ -179,6 +254,36 @@ export interface Person { "who": string; } +/** the router's answer to a proof */ +export interface ProofAnswer { + /** whether it was taken */ + "accepted": boolean; + /** words to say to the person */ + "words": string; +} + +/** who a channel's service says sent something; the router alone judges whether that is the operator */ +export interface Sender { + /** whether the service authenticated it */ + "authenticated": boolean; + /** the name it shows */ + "display"?: string; + /** the account */ + "identity": string; +} + +/** what a channel says of itself at least every five minutes and whenever it changes, on the bench intake as the event `standing` */ +export interface Standing { + /** when, RFC 3339 */ + "at": string; + /** its edits notify nobody */ + "edits-silently"?: boolean; + /** it can send now */ + "ready": boolean; + /** why not */ + "why"?: string; +} + export interface Warrant { /** what it was about */ "about"?: string;