diff --git a/identity-and-access/go/clients/meshnodeenrollerclient/enrolment_test.go b/identity-and-access/go/clients/meshnodeenrollerclient/enrolment_test.go index 41098fe..fc61d8b 100644 --- a/identity-and-access/go/clients/meshnodeenrollerclient/enrolment_test.go +++ b/identity-and-access/go/clients/meshnodeenrollerclient/enrolment_test.go @@ -1,8 +1,13 @@ package meshnodeenrollerclient import ( + "bytes" + "encoding/json" + "reflect" "testing" "time" + + seat "git.novox.be/novox/mesh-contracts/identity-and-access/go/seats/meshnodeenroller" ) func TestATokenLifeAndTheRedeemProofAreAsTheHolderMakesThem(t *testing.T) { @@ -28,3 +33,26 @@ func TestATokenLifeAndTheRedeemProofAreAsTheHolderMakesThem(t *testing.T) { t.Errorf("SignedBytes = %s", got) } } + +// TestTheRedeemAnswerIsTheContractsWire: the client's answer reads into the contract's type with no field dropped, and +// the contract's reads back as the client's wire. +func TestTheRedeemAnswerIsTheContractsWire(t *testing.T) { + mine := RedeemAnswer{Node: "g14", Login: "g14-login", Account: "YQ==", Certificate: "pem", Signer: "dg==", Signature: "cw=="} + wire, _ := json.Marshal(mine) + var theirs seat.RedeemAnswer + d := json.NewDecoder(bytes.NewReader(wire)) + d.DisallowUnknownFields() + if err := d.Decode(&theirs); err != nil { + t.Fatal(err) + } + if theirs.Node != "g14" || theirs.Signer != "dg==" || theirs.Signature != "cw==" { + t.Errorf("the contract reads %+v", theirs) + } + back, _ := json.Marshal(theirs) + var a, b map[string]string + _ = json.Unmarshal(wire, &a) + _ = json.Unmarshal(back, &b) + if len(a) != 6 || !reflect.DeepEqual(a, b) { + t.Errorf("the contract reads %s back as %s", wire, back) + } +} diff --git a/identity-and-access/go/seats/meshnodeenroller/contract.go b/identity-and-access/go/seats/meshnodeenroller/contract.go index 9f1cc08..3cdab36 100644 --- a/identity-and-access/go/seats/meshnodeenroller/contract.go +++ b/identity-and-access/go/seats/meshnodeenroller/contract.go @@ -187,7 +187,20 @@ type RedeemInput struct { } // RedeemAnswer is what redeem answers. -type RedeemAnswer = map[string]any +type RedeemAnswer struct { + // its account sealed to the sealing key it presented, base64 + Account string `json:"account"` + // its certificate, PEM + Certificate string `json:"certificate"` + // the login it dials as next + Login string `json:"login"` + // the node + Node string `json:"node"` + // the vault's signature over the answer without it, under the context of the enrolment kind, base64 + Signature string `json:"signature,omitempty"` + // the vault's public signing half, base64 + Signer string `json:"signer"` +} // ConfirmInput is what confirm takes. type ConfirmInput struct { diff --git a/identity-and-access/go/seats/meshnodeenroller/contract_test.go b/identity-and-access/go/seats/meshnodeenroller/contract_test.go index 6e1d2bf..9fa2469 100644 --- a/identity-and-access/go/seats/meshnodeenroller/contract_test.go +++ b/identity-and-access/go/seats/meshnodeenroller/contract_test.go @@ -31,6 +31,8 @@ func TestFixtures(t *testing.T) { err := json.Unmarshal(b, &v) return v, err }, subjectIs: nil}, + {name: "redeem answers the account sealed to the node, signed by the vault", subject: "", given: "{\"account\":\"c2VhbGVk\",\"certificate\":\"-----BEGIN CERTIFICATE-----\\nMIIB\\n-----END CERTIFICATE-----\\n\",\"login\":\"g14-login\",\"node\":\"g14\",\"signature\":\"c2ln\",\"signer\":\"dmF1bHQ=\"}", wire: "{\"account\":\"c2VhbGVk\",\"certificate\":\"-----BEGIN CERTIFICATE-----\\nMIIB\\n-----END CERTIFICATE-----\\n\",\"login\":\"g14-login\",\"node\":\"g14\",\"signature\":\"c2ln\",\"signer\":\"dmF1bHQ=\"}", + decode: func(b []byte) (any, error) { var v RedeemAnswer; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, } for _, c := range cases { v, err := c.decode([]byte(c.given)) diff --git a/identity-and-access/seats/mesh-node-enroller/fixtures/verbs.json b/identity-and-access/seats/mesh-node-enroller/fixtures/verbs.json new file mode 100644 index 0000000..d594ec3 --- /dev/null +++ b/identity-and-access/seats/mesh-node-enroller/fixtures/verbs.json @@ -0,0 +1,14 @@ +[ + { + "name": "redeem answers the account sealed to the node, signed by the vault", + "of": "verb:redeem.answer", + "given": { + "node": "g14", + "login": "g14-login", + "account": "c2VhbGVk", + "certificate": "-----BEGIN CERTIFICATE-----\nMIIB\n-----END CERTIFICATE-----\n", + "signer": "dmF1bHQ=", + "signature": "c2ln" + } + } +] diff --git a/identity-and-access/seats/mesh-node-enroller/seat.json b/identity-and-access/seats/mesh-node-enroller/seat.json index ecb4170..4d21cd0 100644 --- a/identity-and-access/seats/mesh-node-enroller/seat.json +++ b/identity-and-access/seats/mesh-node-enroller/seat.json @@ -90,7 +90,40 @@ }, "answer": { "type": "object", - "description": "the account sealed to the node, its certificate and the vault's public half, signed by the vault" + "description": "the account sealed to the node, its certificate and the vault's public half, signed by the vault", + "properties": { + "node": { + "type": "string", + "description": "the node" + }, + "login": { + "type": "string", + "description": "the login it dials as next" + }, + "account": { + "type": "string", + "description": "its account sealed to the sealing key it presented, base64" + }, + "certificate": { + "type": "string", + "description": "its certificate, PEM" + }, + "signer": { + "type": "string", + "description": "the vault's public signing half, base64" + }, + "signature": { + "type": "string", + "description": "the vault's signature over the answer without it, under the context of the enrolment kind, base64" + } + }, + "required": [ + "node", + "login", + "account", + "certificate", + "signer" + ] } }, { diff --git a/identity-and-access/ts/seats/meshnodeenroller/src/index.ts b/identity-and-access/ts/seats/meshnodeenroller/src/index.ts index 4068a1d..3ca423d 100644 --- a/identity-and-access/ts/seats/meshnodeenroller/src/index.ts +++ b/identity-and-access/ts/seats/meshnodeenroller/src/index.ts @@ -123,7 +123,20 @@ export interface RedeemInput { } /** is what redeem answers. */ -export type RedeemAnswer = Record; +export interface RedeemAnswer { + /** its account sealed to the sealing key it presented, base64 */ + "account": string; + /** its certificate, PEM */ + "certificate": string; + /** the login it dials as next */ + "login": string; + /** the node */ + "node": string; + /** the vault's signature over the answer without it, under the context of the enrolment kind, base64 */ + "signature"?: string; + /** the vault's public signing half, base64 */ + "signer": string; +} /** is what confirm takes. */ export interface ConfirmInput { diff --git a/operator-and-conversation/go/seats/operatorchannel/contract.go b/operator-and-conversation/go/seats/operatorchannel/contract.go index f17556f..6d30be7 100644 --- a/operator-and-conversation/go/seats/operatorchannel/contract.go +++ b/operator-and-conversation/go/seats/operatorchannel/contract.go @@ -477,4 +477,23 @@ const ( ) // AsksByAskerIDValue is one value of the state asks under the key .. -type AsksByAskerIDValue = map[string]any +type AsksByAskerIDValue struct { + // how it ended, in words + Answer string `json:"answer,omitempty"` + // its asker + Asker string `json:"asker"` + // its asker has been told how it ended + AskerTold bool `json:"asker_told,omitempty"` + // when it ends unanswered, RFC 3339 + Expires string `json:"expires"` + // its headline + Headline string `json:"headline"` + // . + Key string `json:"key"` + // the channels it was offered on + OfferedOn []string `json:"offered_on,omitempty"` + // when it was opened, RFC 3339 + Opened string `json:"opened"` + // how it stands: open, chosen, expired, cancelled, replaced or refused + State string `json:"state"` +} diff --git a/operator-and-conversation/seats/operator-channel/seat.json b/operator-and-conversation/seats/operator-channel/seat.json index b40e63e..a9a5365 100644 --- a/operator-and-conversation/seats/operator-channel/seat.json +++ b/operator-and-conversation/seats/operator-channel/seat.json @@ -182,7 +182,56 @@ "key": ".", "value": { "type": "object", - "description": "one ask the router holds and how it stands" + "description": "one ask the router holds and how it stands, as the router's tool states it", + "properties": { + "key": { + "type": "string", + "description": "." + }, + "asker": { + "type": "string", + "description": "its asker" + }, + "headline": { + "type": "string", + "description": "its headline" + }, + "state": { + "type": "string", + "description": "how it stands: open, chosen, expired, cancelled, replaced or refused" + }, + "opened": { + "type": "string", + "description": "when it was opened, RFC 3339" + }, + "expires": { + "type": "string", + "description": "when it ends unanswered, RFC 3339" + }, + "offered_on": { + "type": "array", + "items": { + "type": "string" + }, + "description": "the channels it was offered on" + }, + "asker_told": { + "type": "boolean", + "description": "its asker has been told how it ended" + }, + "answer": { + "type": "string", + "description": "how it ended, in words" + } + }, + "required": [ + "key", + "asker", + "headline", + "state", + "opened", + "expires" + ] }, "description": "one ask the router holds and how it stands" } diff --git a/operator-and-conversation/ts/seats/operatorchannel/src/index.ts b/operator-and-conversation/ts/seats/operatorchannel/src/index.ts index fbffec0..4b2837b 100644 --- a/operator-and-conversation/ts/seats/operatorchannel/src/index.ts +++ b/operator-and-conversation/ts/seats/operatorchannel/src/index.ts @@ -406,5 +406,24 @@ export interface ClosedHolderEvent { } /** is one value of the state asks under the key .. */ -export type AsksByAskerIDValue = Record; +export interface AsksByAskerIDValue { + /** how it ended, in words */ + "answer"?: string; + /** its asker */ + "asker": string; + /** its asker has been told how it ended */ + "asker_told"?: boolean; + /** when it ends unanswered, RFC 3339 */ + "expires": string; + /** its headline */ + "headline": string; + /** . */ + "key": string; + /** the channels it was offered on */ + "offered_on"?: string[]; + /** when it was opened, RFC 3339 */ + "opened": string; + /** how it stands: open, chosen, expired, cancelled, replaced or refused */ + "state": string; +}