Vote API: reauthentication_required, held, highStakes; Standard scope, trimming, auth per route, unbuilt limits

Closed refusal set gains reauthentication_required (401, sealed commit on a
highStakes motion, Bifrost #37 D2) and held (503, the voting brake, D5);
motion and list item carry highStakes, fixed at open. Spec text: the 413 cap
and per-account 429 are not on Bifrost's member API yet; commitment mismatch
(plain 400) and contended reveal (WrongPhase 409) must be split first; Bifrost
trims candidate and reason today and must stop; Standard motions are not
covered here; auth is stated per route now and after #37 with an opaque bearer
on the wire. A reason is counted in UTF-16 units: the schema alone cannot say
it, so a rule and two astral vectors do.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
soul 2026-11-10 15:20:04 +00:00
commit 0ddcc90be2
3 changed files with 217 additions and 23 deletions

View file

@ -93,7 +93,8 @@
"round": 1,
"phase": "Commit",
"commitClosesAtUtc": "2026-11-12T18:00:00Z",
"closesAtUtc": "2026-11-15T18:00:00Z"
"closesAtUtc": "2026-11-15T18:00:00Z",
"highStakes": false
},
{
"id": "a7d3f0c2-6b15-4e29-8f3a-91c4d5e6f708",
@ -101,11 +102,12 @@
"round": 1,
"phase": "Closed",
"commitClosesAtUtc": "2026-11-12T18:00:00Z",
"closesAtUtc": "2026-11-15T18:00:00Z"
"closesAtUtc": "2026-11-15T18:00:00Z",
"highStakes": true
}
]
},
"messagePackHex": "82AD736368656D6156657273696F6EBC67616D6563756C742E766F74652E6D6F74696F6E5F6C6973742E7630A76D6F74696F6E739286A26964D92435623163326539612D336634642D346338652D396136312D306437653266346238633133A57469746C65B14C6564676572206F72206C616E7465726EA5726F756E6401A57068617365A6436F6D6D6974B1636F6D6D6974436C6F7365734174557463B4323032362D31312D31325431383A30303A30305AAB636C6F7365734174557463B4323032362D31312D31355431383A30303A30305A86A26964D92461376433663063322D366231352D346532392D386633612D393163346435653666373038A57469746C65AF5365636F6E642074686F7567687473A5726F756E6401A57068617365A6436C6F736564B1636F6D6D6974436C6F7365734174557463B4323032362D31312D31325431383A30303A30305AAB636C6F7365734174557463B4323032362D31312D31355431383A30303A30305A"
"messagePackHex": "82AD736368656D6156657273696F6EBC67616D6563756C742E766F74652E6D6F74696F6E5F6C6973742E7630A76D6F74696F6E739287A26964D92435623163326539612D336634642D346338652D396136312D306437653266346238633133A57469746C65B14C6564676572206F72206C616E7465726EA5726F756E6401A57068617365A6436F6D6D6974B1636F6D6D6974436C6F7365734174557463B4323032362D31312D31325431383A30303A30305AAB636C6F7365734174557463B4323032362D31312D31355431383A30303A30305AAA686967685374616B6573C287A26964D92461376433663063322D366231352D346532392D386633612D393163346435653666373038A57469746C65AF5365636F6E642074686F7567687473A5726F756E6401A57068617365A6436C6F736564B1636F6D6D6974436C6F7365734174557463B4323032362D31312D31325431383A30303A30305AAB636C6F7365734174557463B4323032362D31312D31355431383A30303A30305AAA686967685374616B6573C3"
},
{
"name": "motion_list_empty",
@ -143,11 +145,37 @@
],
"commitClosesAtUtc": "2026-11-12T18:00:00Z",
"closesAtUtc": "2026-11-15T18:00:00Z",
"highStakes": false,
"yourVoterId": "0c9f6a4e-7d2b-4a35-b8e1-3f5a6c7d8e90",
"yourCommitment": null,
"announcement": null
},
"messagePackHex": "8EAD736368656D6156657273696F6EB767616D6563756C742E766F74652E6D6F74696F6E2E7630A26964D92435623163326539612D336634642D346338652D396136312D306437653266346238633133A57469746C65B14C6564676572206F72206C616E7465726EA773756D6D617279A95069636B206F6E652EA5726F756E6401AD7072696F724D6F74696F6E4964C0AC62616C6C6F74536368656D61B967616D6563756C742E7365616C65645F62616C6C6F742E7630A57068617365A6436F6D6D6974AA63616E6469646174657392B041646F707420746865206C6564676572B2446566657220746F20726F756E642074776FB1636F6D6D6974436C6F7365734174557463B4323032362D31312D31325431383A30303A30305AAB636C6F7365734174557463B4323032362D31312D31355431383A30303A30305AAB796F7572566F7465724964D92430633966366134652D376432622D346133352D623865312D336635613663376438653930AE796F7572436F6D6D69746D656E74C0AC616E6E6F756E63656D656E74C0"
"messagePackHex": "8FAD736368656D6156657273696F6EB767616D6563756C742E766F74652E6D6F74696F6E2E7630A26964D92435623163326539612D336634642D346338652D396136312D306437653266346238633133A57469746C65B14C6564676572206F72206C616E7465726EA773756D6D617279A95069636B206F6E652EA5726F756E6401AD7072696F724D6F74696F6E4964C0AC62616C6C6F74536368656D61B967616D6563756C742E7365616C65645F62616C6C6F742E7630A57068617365A6436F6D6D6974AA63616E6469646174657392B041646F707420746865206C6564676572B2446566657220746F20726F756E642074776FB1636F6D6D6974436C6F7365734174557463B4323032362D31312D31325431383A30303A30305AAB636C6F7365734174557463B4323032362D31312D31355431383A30303A30305AAA686967685374616B6573C2AB796F7572566F7465724964D92430633966366134652D376432622D346133352D623865312D336635613663376438653930AE796F7572436F6D6D69746D656E74C0AC616E6E6F756E63656D656E74C0"
},
{
"name": "motion_high_stakes_commit_phase",
"def": "motion",
"message": {
"schemaVersion": "gamecult.vote.motion.v0",
"id": "5b1c2e9a-3f4d-4c8e-9a61-0d7e2f4b8c13",
"title": "Ledger or lantern",
"summary": "Pick one.",
"round": 1,
"priorMotionId": null,
"ballotSchema": "gamecult.sealed_ballot.v0",
"phase": "Commit",
"candidates": [
"Adopt the ledger",
"Defer to round two"
],
"commitClosesAtUtc": "2026-11-12T18:00:00Z",
"closesAtUtc": "2026-11-15T18:00:00Z",
"highStakes": true,
"yourVoterId": "0c9f6a4e-7d2b-4a35-b8e1-3f5a6c7d8e90",
"yourCommitment": null,
"announcement": null
},
"messagePackHex": "8FAD736368656D6156657273696F6EB767616D6563756C742E766F74652E6D6F74696F6E2E7630A26964D92435623163326539612D336634642D346338652D396136312D306437653266346238633133A57469746C65B14C6564676572206F72206C616E7465726EA773756D6D617279A95069636B206F6E652EA5726F756E6401AD7072696F724D6F74696F6E4964C0AC62616C6C6F74536368656D61B967616D6563756C742E7365616C65645F62616C6C6F742E7630A57068617365A6436F6D6D6974AA63616E6469646174657392B041646F707420746865206C6564676572B2446566657220746F20726F756E642074776FB1636F6D6D6974436C6F7365734174557463B4323032362D31312D31325431383A30303A30305AAB636C6F7365734174557463B4323032362D31312D31355431383A30303A30305AAA686967685374616B6573C3AB796F7572566F7465724964D92430633966366134652D376432622D346133352D623865312D336635613663376438653930AE796F7572436F6D6D69746D656E74C0AC616E6E6F756E63656D656E74C0"
},
{
"name": "motion_reveal_phase_committed",
@ -167,6 +195,7 @@
],
"commitClosesAtUtc": "2026-11-12T18:00:00Z",
"closesAtUtc": "2026-11-15T18:00:00Z",
"highStakes": false,
"yourVoterId": "0c9f6a4e-7d2b-4a35-b8e1-3f5a6c7d8e90",
"yourCommitment": {
"schemaVersion": "gamecult.vote.receipt.v0",
@ -179,7 +208,7 @@
},
"announcement": null
},
"messagePackHex": "8EAD736368656D6156657273696F6EB767616D6563756C742E766F74652E6D6F74696F6E2E7630A26964D92435623163326539612D336634642D346338652D396136312D306437653266346238633133A57469746C65B14C6564676572206F72206C616E7465726EA773756D6D617279A95069636B206F6E652EA5726F756E6401AD7072696F724D6F74696F6E4964C0AC62616C6C6F74536368656D61B967616D6563756C742E7365616C65645F62616C6C6F742E7630A57068617365A652657665616CAA63616E6469646174657392B041646F707420746865206C6564676572B2446566657220746F20726F756E642074776FB1636F6D6D6974436C6F7365734174557463B4323032362D31312D31325431383A30303A30305AAB636C6F7365734174557463B4323032362D31312D31355431383A30303A30305AAB796F7572566F7465724964D92430633966366134652D376432622D346133352D623865312D336635613663376438653930AE796F7572436F6D6D69746D656E7487AD736368656D6156657273696F6EB867616D6563756C742E766F74652E726563656970742E7630A86D6F74696F6E4964D92435623163326539612D336634642D346338652D396136312D306437653266346238633133B2766F746572557365724163636F756E744964D92430633966366134652D376432622D346133352D623865312D336635613663376438653930AA636F6D6D69746D656E74D94038363533323863623461333134386638656263616137356237323936653235376332383761623766656336376663363062363664396463363461306433653862A87265766973696F6E01AE636F6D6D69747465644174557463BC323032362D31312D31325431393A30343A31312E313233343536375AA872657665616C6564C2AC616E6E6F756E63656D656E74C0"
"messagePackHex": "8FAD736368656D6156657273696F6EB767616D6563756C742E766F74652E6D6F74696F6E2E7630A26964D92435623163326539612D336634642D346338652D396136312D306437653266346238633133A57469746C65B14C6564676572206F72206C616E7465726EA773756D6D617279A95069636B206F6E652EA5726F756E6401AD7072696F724D6F74696F6E4964C0AC62616C6C6F74536368656D61B967616D6563756C742E7365616C65645F62616C6C6F742E7630A57068617365A652657665616CAA63616E6469646174657392B041646F707420746865206C6564676572B2446566657220746F20726F756E642074776FB1636F6D6D6974436C6F7365734174557463B4323032362D31312D31325431383A30303A30305AAB636C6F7365734174557463B4323032362D31312D31355431383A30303A30305AAA686967685374616B6573C2AB796F7572566F7465724964D92430633966366134652D376432622D346133352D623865312D336635613663376438653930AE796F7572436F6D6D69746D656E7487AD736368656D6156657273696F6EB867616D6563756C742E766F74652E726563656970742E7630A86D6F74696F6E4964D92435623163326539612D336634642D346338652D396136312D306437653266346238633133B2766F746572557365724163636F756E744964D92430633966366134652D376432622D346133352D623865312D336635613663376438653930AA636F6D6D69746D656E74D94038363533323863623461333134386638656263616137356237323936653235376332383761623766656336376663363062363664396463363461306433653862A87265766973696F6E01AE636F6D6D69747465644174557463BC323032362D31312D31325431393A30343A31312E313233343536375AA872657665616C6564C2AC616E6E6F756E63656D656E74C0"
},
{
"name": "motion_closed_revealed",
@ -199,6 +228,7 @@
],
"commitClosesAtUtc": "2026-11-12T18:00:00Z",
"closesAtUtc": "2026-11-15T18:00:00Z",
"highStakes": false,
"yourVoterId": "0c9f6a4e-7d2b-4a35-b8e1-3f5a6c7d8e90",
"yourCommitment": {
"schemaVersion": "gamecult.vote.receipt.v0",
@ -211,7 +241,27 @@
},
"announcement": null
},
"messagePackHex": "8EAD736368656D6156657273696F6EB767616D6563756C742E766F74652E6D6F74696F6E2E7630A26964D92435623163326539612D336634642D346338652D396136312D306437653266346238633133A57469746C65B14C6564676572206F72206C616E7465726EA773756D6D617279A95069636B206F6E652EA5726F756E6401AD7072696F724D6F74696F6E4964C0AC62616C6C6F74536368656D61B967616D6563756C742E7365616C65645F62616C6C6F742E7630A57068617365A6436C6F736564AA63616E6469646174657392B041646F707420746865206C6564676572B2446566657220746F20726F756E642074776FB1636F6D6D6974436C6F7365734174557463B4323032362D31312D31325431383A30303A30305AAB636C6F7365734174557463B4323032362D31312D31355431383A30303A30305AAB796F7572566F7465724964D92430633966366134652D376432622D346133352D623865312D336635613663376438653930AE796F7572436F6D6D69746D656E7487AD736368656D6156657273696F6EB867616D6563756C742E766F74652E726563656970742E7630A86D6F74696F6E4964D92435623163326539612D336634642D346338652D396136312D306437653266346238633133B2766F746572557365724163636F756E744964D92430633966366134652D376432622D346133352D623865312D336635613663376438653930AA636F6D6D69746D656E74D94038363533323863623461333134386638656263616137356237323936653235376332383761623766656336376663363062363664396463363461306433653862A87265766973696F6E02AE636F6D6D69747465644174557463BC323032362D31312D31325431393A30343A31312E313233343536375AA872657665616C6564C3AC616E6E6F756E63656D656E74C0"
"messagePackHex": "8FAD736368656D6156657273696F6EB767616D6563756C742E766F74652E6D6F74696F6E2E7630A26964D92435623163326539612D336634642D346338652D396136312D306437653266346238633133A57469746C65B14C6564676572206F72206C616E7465726EA773756D6D617279A95069636B206F6E652EA5726F756E6401AD7072696F724D6F74696F6E4964C0AC62616C6C6F74536368656D61B967616D6563756C742E7365616C65645F62616C6C6F742E7630A57068617365A6436C6F736564AA63616E6469646174657392B041646F707420746865206C6564676572B2446566657220746F20726F756E642074776FB1636F6D6D6974436C6F7365734174557463B4323032362D31312D31325431383A30303A30305AAB636C6F7365734174557463B4323032362D31312D31355431383A30303A30305AAA686967685374616B6573C2AB796F7572566F7465724964D92430633966366134652D376432622D346133352D623865312D336635613663376438653930AE796F7572436F6D6D69746D656E7487AD736368656D6156657273696F6EB867616D6563756C742E766F74652E726563656970742E7630A86D6F74696F6E4964D92435623163326539612D336634642D346338652D396136312D306437653266346238633133B2766F746572557365724163636F756E744964D92430633966366134652D376432622D346133352D623865312D336635613663376438653930AA636F6D6D69746D656E74D94038363533323863623461333134386638656263616137356237323936653235376332383761623766656336376663363062363664396463363461306433653862A87265766973696F6E02AE636F6D6D69747465644174557463BC323032362D31312D31325431393A30343A31312E313233343536375AA872657665616C6564C3AC616E6E6F756E63656D656E74C0"
},
{
"name": "cast_reveal_reason_140_astral_280_units",
"def": "cast",
"message": {
"schemaVersion": "gamecult.vote.cast.v0",
"motionId": "5b1c2e9a-3f4d-4c8e-9a61-0d7e2f4b8c13",
"verb": "reveal",
"commitment": null,
"ballot": {
"schema": "gamecult.sealed_ballot.v0",
"motionId": "5b1c2e9a-3f4d-4c8e-9a61-0d7e2f4b8c13",
"round": 1,
"candidate": "Adopt the ledger",
"reason": "😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀",
"salt": "d41d8cd98f00b204e9800998ecf8427e5f1c0a7b3e92d6a48b15c7f0e3a9d2b6",
"persona": ""
}
},
"messagePackHex": "85AD736368656D6156657273696F6EB567616D6563756C742E766F74652E636173742E7630A86D6F74696F6E4964D92435623163326539612D336634642D346338652D396136312D306437653266346238633133A476657262A672657665616CAA636F6D6D69746D656E74C0A662616C6C6F7487A6736368656D61B967616D6563756C742E7365616C65645F62616C6C6F742E7630A86D6F74696F6E4964D92435623163326539612D336634642D346338652D396136312D306437653266346238633133A5726F756E6401A963616E646964617465B041646F707420746865206C6564676572A6726561736F6EDA0230F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880A473616C74D94064343164386364393866303062323034653938303039393865636638343237653566316330613762336539326436613438623135633766306533613964326236A7706572736F6E61A0"
},
{
"name": "cast_commit",
@ -466,6 +516,27 @@
"why": "reason is at most 280 characters",
"messagePackHex": "85AD736368656D6156657273696F6EB567616D6563756C742E766F74652E636173742E7630A86D6F74696F6E4964D92435623163326539612D336634642D346338652D396136312D306437653266346238633133A476657262A672657665616CAA636F6D6D69746D656E74C0A662616C6C6F7487A6736368656D61B967616D6563756C742E7365616C65645F62616C6C6F742E7630A86D6F74696F6E4964D92435623163326539612D336634642D346338652D396136312D306437653266346238633133A5726F756E6401A963616E646964617465B041646F707420746865206C6564676572A6726561736F6EDA01197878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878A473616C74D94064343164386364393866303062323034653938303039393865636638343237653566316330613762336539326436613438623135633766306533613964326236A7706572736F6E61A0"
},
{
"name": "ballot_reason_141_astral_282_units",
"def": "cast",
"message": {
"schemaVersion": "gamecult.vote.cast.v0",
"motionId": "5b1c2e9a-3f4d-4c8e-9a61-0d7e2f4b8c13",
"verb": "reveal",
"commitment": null,
"ballot": {
"schema": "gamecult.sealed_ballot.v0",
"motionId": "5b1c2e9a-3f4d-4c8e-9a61-0d7e2f4b8c13",
"round": 1,
"candidate": "Adopt the ledger",
"reason": "😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀",
"salt": "d41d8cd98f00b204e9800998ecf8427e5f1c0a7b3e92d6a48b15c7f0e3a9d2b6",
"persona": ""
}
},
"why": "reason is at most 280 UTF-16 code units (vote-api.md); the schema counts code points and accepts this, so a rule refuses it",
"messagePackHex": "85AD736368656D6156657273696F6EB567616D6563756C742E766F74652E636173742E7630A86D6F74696F6E4964D92435623163326539612D336634642D346338652D396136312D306437653266346238633133A476657262A672657665616CAA636F6D6D69746D656E74C0A662616C6C6F7487A6736368656D61B967616D6563756C742E7365616C65645F62616C6C6F742E7630A86D6F74696F6E4964D92435623163326539612D336634642D346338652D396136312D306437653266346238633133A5726F756E6401A963616E646964617465B041646F707420746865206C6564676572A6726561736F6EDA0234F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880A473616C74D94064343164386364393866303062323034653938303039393865636638343237653566316330613762336539326436613438623135633766306533613964326236A7706572736F6E61A0"
},
{
"name": "ballot_reason_multiline",
"def": "cast",
@ -713,12 +784,13 @@
],
"commitClosesAtUtc": "2026-11-12T18:00:00Z",
"closesAtUtc": "2026-11-15T18:00:00Z",
"highStakes": false,
"yourVoterId": "0c9f6a4e-7d2b-4a35-b8e1-3f5a6c7d8e90",
"yourCommitment": null,
"announcement": null
},
"why": "a motion has two or more candidates",
"messagePackHex": "8EAD736368656D6156657273696F6EB767616D6563756C742E766F74652E6D6F74696F6E2E7630A26964D92435623163326539612D336634642D346338652D396136312D306437653266346238633133A57469746C65B14C6564676572206F72206C616E7465726EA773756D6D617279A95069636B206F6E652EA5726F756E6401AD7072696F724D6F74696F6E4964C0AC62616C6C6F74536368656D61B967616D6563756C742E7365616C65645F62616C6C6F742E7630A57068617365A6436F6D6D6974AA63616E6469646174657391A44F6E6C79B1636F6D6D6974436C6F7365734174557463B4323032362D31312D31325431383A30303A30305AAB636C6F7365734174557463B4323032362D31312D31355431383A30303A30305AAB796F7572566F7465724964D92430633966366134652D376432622D346133352D623865312D336635613663376438653930AE796F7572436F6D6D69746D656E74C0AC616E6E6F756E63656D656E74C0"
"messagePackHex": "8FAD736368656D6156657273696F6EB767616D6563756C742E766F74652E6D6F74696F6E2E7630A26964D92435623163326539612D336634642D346338652D396136312D306437653266346238633133A57469746C65B14C6564676572206F72206C616E7465726EA773756D6D617279A95069636B206F6E652EA5726F756E6401AD7072696F724D6F74696F6E4964C0AC62616C6C6F74536368656D61B967616D6563756C742E7365616C65645F62616C6C6F742E7630A57068617365A6436F6D6D6974AA63616E6469646174657391A44F6E6C79B1636F6D6D6974436C6F7365734174557463B4323032362D31312D31325431383A30303A30305AAB636C6F7365734174557463B4323032362D31312D31355431383A30303A30305AAA686967685374616B6573C2AB796F7572566F7465724964D92430633966366134652D376432622D346133352D623865312D336635613663376438653930AE796F7572436F6D6D69746D656E74C0AC616E6E6F756E63656D656E74C0"
},
{
"name": "motion_phase_unknown",
@ -738,12 +810,78 @@
],
"commitClosesAtUtc": "2026-11-12T18:00:00Z",
"closesAtUtc": "2026-11-15T18:00:00Z",
"highStakes": false,
"yourVoterId": "0c9f6a4e-7d2b-4a35-b8e1-3f5a6c7d8e90",
"yourCommitment": null,
"announcement": null
},
"why": "phase is Commit, Reveal or Closed",
"messagePackHex": "8EAD736368656D6156657273696F6EB767616D6563756C742E766F74652E6D6F74696F6E2E7630A26964D92435623163326539612D336634642D346338652D396136312D306437653266346238633133A57469746C65B14C6564676572206F72206C616E7465726EA773756D6D617279A95069636B206F6E652EA5726F756E6401AD7072696F724D6F74696F6E4964C0AC62616C6C6F74536368656D61B967616D6563756C742E7365616C65645F62616C6C6F742E7630A57068617365A44F70656EAA63616E6469646174657392B041646F707420746865206C6564676572B2446566657220746F20726F756E642074776FB1636F6D6D6974436C6F7365734174557463B4323032362D31312D31325431383A30303A30305AAB636C6F7365734174557463B4323032362D31312D31355431383A30303A30305AAB796F7572566F7465724964D92430633966366134652D376432622D346133352D623865312D336635613663376438653930AE796F7572436F6D6D69746D656E74C0AC616E6E6F756E63656D656E74C0"
"messagePackHex": "8FAD736368656D6156657273696F6EB767616D6563756C742E766F74652E6D6F74696F6E2E7630A26964D92435623163326539612D336634642D346338652D396136312D306437653266346238633133A57469746C65B14C6564676572206F72206C616E7465726EA773756D6D617279A95069636B206F6E652EA5726F756E6401AD7072696F724D6F74696F6E4964C0AC62616C6C6F74536368656D61B967616D6563756C742E7365616C65645F62616C6C6F742E7630A57068617365A44F70656EAA63616E6469646174657392B041646F707420746865206C6564676572B2446566657220746F20726F756E642074776FB1636F6D6D6974436C6F7365734174557463B4323032362D31312D31325431383A30303A30305AAB636C6F7365734174557463B4323032362D31312D31355431383A30303A30305AAA686967685374616B6573C2AB796F7572566F7465724964D92430633966366134652D376432622D346133352D623865312D336635613663376438653930AE796F7572436F6D6D69746D656E74C0AC616E6E6F756E63656D656E74C0"
},
{
"name": "motion_high_stakes_not_boolean",
"def": "motion",
"message": {
"schemaVersion": "gamecult.vote.motion.v0",
"id": "5b1c2e9a-3f4d-4c8e-9a61-0d7e2f4b8c13",
"title": "Ledger or lantern",
"summary": "Pick one.",
"round": 1,
"priorMotionId": null,
"ballotSchema": "gamecult.sealed_ballot.v0",
"phase": "Commit",
"candidates": [
"Adopt the ledger",
"Defer to round two"
],
"commitClosesAtUtc": "2026-11-12T18:00:00Z",
"closesAtUtc": "2026-11-15T18:00:00Z",
"highStakes": "yes",
"yourVoterId": "0c9f6a4e-7d2b-4a35-b8e1-3f5a6c7d8e90",
"yourCommitment": null,
"announcement": null
},
"why": "highStakes is a boolean, fixed at open",
"messagePackHex": "8FAD736368656D6156657273696F6EB767616D6563756C742E766F74652E6D6F74696F6E2E7630A26964D92435623163326539612D336634642D346338652D396136312D306437653266346238633133A57469746C65B14C6564676572206F72206C616E7465726EA773756D6D617279A95069636B206F6E652EA5726F756E6401AD7072696F724D6F74696F6E4964C0AC62616C6C6F74536368656D61B967616D6563756C742E7365616C65645F62616C6C6F742E7630A57068617365A6436F6D6D6974AA63616E6469646174657392B041646F707420746865206C6564676572B2446566657220746F20726F756E642074776FB1636F6D6D6974436C6F7365734174557463B4323032362D31312D31325431383A30303A30305AAB636C6F7365734174557463B4323032362D31312D31355431383A30303A30305AAA686967685374616B6573A3796573AB796F7572566F7465724964D92430633966366134652D376432622D346133352D623865312D336635613663376438653930AE796F7572436F6D6D69746D656E74C0AC616E6E6F756E63656D656E74C0"
},
{
"name": "motion_without_high_stakes",
"def": "motion",
"message": {
"schemaVersion": "gamecult.vote.motion.v0",
"id": "5b1c2e9a-3f4d-4c8e-9a61-0d7e2f4b8c13",
"title": "Ledger or lantern",
"summary": "Pick one.",
"round": 1,
"priorMotionId": null,
"ballotSchema": "gamecult.sealed_ballot.v0",
"phase": "Commit",
"candidates": [
"Adopt the ledger",
"Defer to round two"
],
"commitClosesAtUtc": "2026-11-12T18:00:00Z",
"closesAtUtc": "2026-11-15T18:00:00Z",
"yourVoterId": "0c9f6a4e-7d2b-4a35-b8e1-3f5a6c7d8e90",
"yourCommitment": null,
"announcement": null
},
"why": "map-mode writes every key; a motion states whether it is high-stakes",
"messagePackHex": "8EAD736368656D6156657273696F6EB767616D6563756C742E766F74652E6D6F74696F6E2E7630A26964D92435623163326539612D336634642D346338652D396136312D306437653266346238633133A57469746C65B14C6564676572206F72206C616E7465726EA773756D6D617279A95069636B206F6E652EA5726F756E6401AD7072696F724D6F74696F6E4964C0AC62616C6C6F74536368656D61B967616D6563756C742E7365616C65645F62616C6C6F742E7630A57068617365A6436F6D6D6974AA63616E6469646174657392B041646F707420746865206C6564676572B2446566657220746F20726F756E642074776FB1636F6D6D6974436C6F7365734174557463B4323032362D31312D31325431383A30303A30305AAB636C6F7365734174557463B4323032362D31312D31355431383A30303A30305AAB796F7572566F7465724964D92430633966366134652D376432622D346133352D623865312D336635613663376438653930AE796F7572436F6D6D69746D656E74C0AC616E6E6F756E63656D656E74C0"
},
{
"name": "motion_list_item_without_high_stakes",
"def": "motionListItem",
"message": {
"id": "5b1c2e9a-3f4d-4c8e-9a61-0d7e2f4b8c13",
"title": "x",
"round": 1,
"phase": "Commit",
"commitClosesAtUtc": "2026-11-12T18:00:00Z",
"closesAtUtc": "2026-11-15T18:00:00Z"
},
"why": "every list item states whether the motion is high-stakes",
"messagePackHex": "86A26964D92435623163326539612D336634642D346338652D396136312D306437653266346238633133A57469746C65A178A5726F756E6401A57068617365A6436F6D6D6974B1636F6D6D6974436C6F7365734174557463B4323032362D31312D31325431383A30303A30305AAB636C6F7365734174557463B4323032362D31312D31355431383A30303A30305A"
},
{
"name": "motion_leaks_tally",
@ -763,13 +901,14 @@
],
"commitClosesAtUtc": "2026-11-12T18:00:00Z",
"closesAtUtc": "2026-11-15T18:00:00Z",
"highStakes": false,
"yourVoterId": "0c9f6a4e-7d2b-4a35-b8e1-3f5a6c7d8e90",
"yourCommitment": null,
"announcement": null,
"tally": []
},
"why": "mid-vote a client sees no tallies; the document is closed",
"messagePackHex": "8FAD736368656D6156657273696F6EB767616D6563756C742E766F74652E6D6F74696F6E2E7630A26964D92435623163326539612D336634642D346338652D396136312D306437653266346238633133A57469746C65B14C6564676572206F72206C616E7465726EA773756D6D617279A95069636B206F6E652EA5726F756E6401AD7072696F724D6F74696F6E4964C0AC62616C6C6F74536368656D61B967616D6563756C742E7365616C65645F62616C6C6F742E7630A57068617365A652657665616CAA63616E6469646174657392B041646F707420746865206C6564676572B2446566657220746F20726F756E642074776FB1636F6D6D6974436C6F7365734174557463B4323032362D31312D31325431383A30303A30305AAB636C6F7365734174557463B4323032362D31312D31355431383A30303A30305AAB796F7572566F7465724964D92430633966366134652D376432622D346133352D623865312D336635613663376438653930AE796F7572436F6D6D69746D656E74C0AC616E6E6F756E63656D656E74C0A574616C6C7990"
"messagePackHex": "DE0010AD736368656D6156657273696F6EB767616D6563756C742E766F74652E6D6F74696F6E2E7630A26964D92435623163326539612D336634642D346338652D396136312D306437653266346238633133A57469746C65B14C6564676572206F72206C616E7465726EA773756D6D617279A95069636B206F6E652EA5726F756E6401AD7072696F724D6F74696F6E4964C0AC62616C6C6F74536368656D61B967616D6563756C742E7365616C65645F62616C6C6F742E7630A57068617365A652657665616CAA63616E6469646174657392B041646F707420746865206C6564676572B2446566657220746F20726F756E642074776FB1636F6D6D6974436C6F7365734174557463B4323032362D31312D31325431383A30303A30305AAB636C6F7365734174557463B4323032362D31312D31355431383A30303A30305AAA686967685374616B6573C2AB796F7572566F7465724964D92430633966366134652D376432622D346133352D623865312D336635613663376438653930AE796F7572436F6D6D69746D656E74C0AC616E6E6F756E63656D656E74C0A574616C6C7990"
},
{
"name": "motion_list_item_without_phase",
@ -782,12 +921,13 @@
"title": "x",
"round": 1,
"commitClosesAtUtc": "2026-11-12T18:00:00Z",
"closesAtUtc": "2026-11-15T18:00:00Z"
"closesAtUtc": "2026-11-15T18:00:00Z",
"highStakes": false
}
]
},
"why": "every item names its phase",
"messagePackHex": "82AD736368656D6156657273696F6EBC67616D6563756C742E766F74652E6D6F74696F6E5F6C6973742E7630A76D6F74696F6E739185A26964D92435623163326539612D336634642D346338652D396136312D306437653266346238633133A57469746C65A178A5726F756E6401B1636F6D6D6974436C6F7365734174557463B4323032362D31312D31325431383A30303A30305AAB636C6F7365734174557463B4323032362D31312D31355431383A30303A30305A"
"messagePackHex": "82AD736368656D6156657273696F6EBC67616D6563756C742E766F74652E6D6F74696F6E5F6C6973742E7630A76D6F74696F6E739186A26964D92435623163326539612D336634642D346338652D396136312D306437653266346238633133A57469746C65A178A5726F756E6401B1636F6D6D6974436C6F7365734174557463B4323032362D31312D31325431383A30303A30305AAB636C6F7365734174557463B4323032362D31312D31355431383A30303A30305AAA686967685374616B6573C2"
},
{
"name": "refusal_code_unknown",
@ -967,6 +1107,18 @@
},
"messagePackHex": "84AD736368656D6156657273696F6EB867616D6563756C742E766F74652E7265667573616C2E7630A4636F6465AF756E61757468656E74696361746564A56572726F72BB412062656172657220746F6B656E2069732072657175697265642EB1726574727941667465725365636F6E6473C0"
},
{
"name": "reauthentication_required",
"httpStatus": 401,
"trigger": "a commit on a high-stakes motion with no sign-in fresh enough (Bifrost PR #37, D2); the voter signs in again at the top level and retries",
"message": {
"schemaVersion": "gamecult.vote.refusal.v0",
"code": "reauthentication_required",
"error": "Sign in again to commit on this motion.",
"retryAfterSeconds": null
},
"messagePackHex": "84AD736368656D6156657273696F6EB867616D6563756C742E766F74652E7265667573616C2E7630A4636F6465B9726561757468656E7469636174696F6E5F7265717569726564A56572726F72D9275369676E20696E20616761696E20746F20636F6D6D6974206F6E2074686973206D6F74696F6E2EB1726574727941667465725365636F6E6473C0"
},
{
"name": "forbidden",
"httpStatus": 403,
@ -1051,10 +1203,22 @@
},
"messagePackHex": "84AD736368656D6156657273696F6EB867616D6563756C742E766F74652E7265667573616C2E7630A4636F6465B17061796C6F61645F746F6F5F6C61726765A56572726F72BC54686520626F6479206578636565647320343039362062797465732EB1726574727941667465725365636F6E6473C0"
},
{
"name": "held",
"httpStatus": 503,
"trigger": "the Ratatoskr voting brake is set (Bifrost PR #37, D5): no cast is read or stored while it is",
"message": {
"schemaVersion": "gamecult.vote.refusal.v0",
"code": "held",
"error": "Voting is held.",
"retryAfterSeconds": null
},
"messagePackHex": "84AD736368656D6156657273696F6EB867616D6563756C742E766F74652E7265667573616C2E7630A4636F6465A468656C64A56572726F72AF566F74696E672069732068656C642EB1726574727941667465725365636F6E6473C0"
},
{
"name": "rate_limited",
"httpStatus": 429,
"trigger": "more than the per-credential cast rate; Retry-After mirrors retryAfterSeconds",
"trigger": "more than the per-account cast rate; Retry-After mirrors retryAfterSeconds",
"message": {
"schemaVersion": "gamecult.vote.refusal.v0",
"code": "rate_limited",

View file

@ -6,7 +6,7 @@ Facts it rests on: `/opt/commons/handoff/bifrost-browser-voting-facts.md` (Bifro
## Scope
Sealed-ballot motions only (`gamecult.sealed_ballot.v0`). Standard motions have no JSON read or cast API at all today, no Mind path, and no receipt; they are not in v0 (open choice 1). The contract names four exchanges and one refusal document:
**Which API covers which.** This contract covers **sealed-ballot motions only** (`gamecult.sealed_ballot.v0`). **Standard motions are not covered here.** Bifrost PR #37 also serves them (`PUT /api/votes/motions/{id}/ballot`, a receipt `{motionId, choice, revision, castAtUtc, receiptId}`), and that API is #37's own: this page defines no document for it, and a Standard motion id sent to any exchange below is answered `not_found`. Today Standard motions have no JSON read or cast API at all, no Mind path, and no receipt (open choice 1). The contract names four exchanges and one refusal document:
| exchange | request | response |
|---|---|---|
@ -15,22 +15,23 @@ Sealed-ballot motions only (`gamecult.sealed_ballot.v0`). Standard motions have
| cast (commit or reveal) | `gamecult.vote.cast.v0` | `gamecult.vote.receipt.v0` |
| any refusal | | `gamecult.vote.refusal.v0` |
Every document is CultNet map-mode: all keys are written, nil where absent, and the schema is closed (`additionalProperties: false`). Instants are RFC 3339 UTC with a `Z` and up to seven fractional digits. Ids are lowercase `D`-format UUIDs. The bytes of every document are pinned by `messagePackHex` in the vectors (shortest integers, shortest strings, keys in the order the schema lists them); JSON with the same keys is the xenos-boundary form of the same documents (open choice 4: the wire format).
Every document is CultNet map-mode: all keys are written, nil where absent, and the schema is closed (`additionalProperties: false`). Instants are RFC 3339 UTC with a `Z` and up to seven fractional digits. Ids are lowercase `D`-format UUIDs. A motion states `highStakes`, a boolean fixed when the motion opens and never changed after (Bifrost PR #37, D2): it is the property that makes a commit demand a fresh sign-in (`reauthentication_required`). The bytes of every document are pinned by `messagePackHex` in the vectors (shortest integers, shortest strings, keys in the order the schema lists them); JSON with the same keys is the xenos-boundary form of the same documents (open choice 4: the wire format).
## Documents
- **motion_list_request**: `{schemaVersion, phase}`; `phase` is `Commit`, `Reveal`, `Closed` or nil for all.
- **motion_list**: `{schemaVersion, motions: [{id, title, round, phase, commitClosesAtUtc, closesAtUtc}]}`. **Heimdall must add this endpoint**: Bifrost has no list route for sealed motions today, only fetch-by-id. No pagination in v0 (open choice 2).
- **motion_list**: `{schemaVersion, motions: [{id, title, round, phase, commitClosesAtUtc, closesAtUtc, highStakes}]}`. **Heimdall must add this endpoint**: Bifrost has no list route for sealed motions today, only fetch-by-id. No pagination in v0 (open choice 2).
- **motion_read_request**: `{schemaVersion, motionId}`.
- **motion**: Bifrost's `SealedMotionState` with its names: `{schemaVersion, id, title, summary, round, priorMotionId, ballotSchema, phase, candidates (two or more), commitClosesAtUtc, closesAtUtc, yourVoterId, yourCommitment (a receipt or nil), announcement}`. It never carries a tally, a turnout, or another voter's commitment or ballot (a `tally` key is a negative vector). `yourVoterId` is the voter's `UserAccount.Id`, the `voterUserAccountId` of the commitment.
- **motion**: Bifrost's `SealedMotionState` with its names: `{schemaVersion, id, title, summary, round, priorMotionId, ballotSchema, phase, candidates (two or more), commitClosesAtUtc, closesAtUtc, highStakes, yourVoterId, yourCommitment (a receipt or nil), announcement}`. It never carries a tally, a turnout, or another voter's commitment or ballot (a `tally` key is a negative vector). `yourVoterId` is the voter's `UserAccount.Id`, the `voterUserAccountId` of the commitment.
- **cast**: `{schemaVersion, motionId, verb, commitment, ballot}` with exactly one of two shapes: `verb: "commit"` with a 64-hex `commitment` and `ballot: nil`, or `verb: "reveal"` with `commitment: nil` and a `ballot`. One document for both verbs; today's two routes (`/commit`, `/reveal`) may stay, taking this document as the body, with a `verb` that must match the route (open choice 3).
- **ballot** (inside a reveal): `{schema: "gamecult.sealed_ballot.v0", motionId, round, candidate, reason, salt, persona}`. `candidate` and `reason` are single-line and **already trimmed** (no Unicode White_Space at either end, using the White_Space list of `write-receipt.md`); the contract refuses an untrimmed value rather than trimming it, so the bytes hashed are the bytes sent (Bifrost trims silently today: open choice 5). `reason` is 1 to 280 characters (UTF-16 code units, as Bifrost counts; the vectors keep the boundary cases to the BMP). `salt` is 32 to 128 lowercase hex. `persona` is `""` for a member and a printable-ASCII, space-free id of at most 64 characters for a Mind.
- **ballot** (inside a reveal): `{schema: "gamecult.sealed_ballot.v0", motionId, round, candidate, reason, salt, persona}`. `candidate` and `reason` are single-line and **already trimmed** (no Unicode White_Space at either end, using the White_Space list of `write-receipt.md`); the contract refuses an untrimmed value rather than trimming it, so the bytes hashed are the bytes sent. **This is a behaviour change Bifrost must make:** it trims `candidate` and `reason` silently on reveal today (facts file section 3c, `SealedBallotService.cs:154, 385-387`), so an untrimmed reveal is accepted and hashed trimmed; under this contract it is `invalid` (open choice 5). `reason` is 1 to 280 characters counted in UTF-16 code units, as Bifrost counts. A JSON Schema `maxLength` counts code points, so the schema alone accepts up to 280 astral characters; both suites add the rule (vectors `cast_reveal_reason_140_astral_280_units` valid, `ballot_reason_141_astral_282_units` refused). `salt` is 32 to 128 lowercase hex. `persona` is `""` for a member and a printable-ASCII, space-free id of at most 64 characters for a Mind.
- **receipt**: Bifrost's `SealedBallotReceipt`: `{schemaVersion, motionId, voterUserAccountId, commitment, revision (>= 1), committedAtUtc, revealed}`. The receipt for a reveal has `revealed: true` and **echoes nothing of the ballot** (no candidate, reason or salt; negative vectors for each).
- **refusal**: `{schemaVersion, code, error, retryAfterSeconds}`; the `code` set is closed:
| code | HTTP | trigger |
|---|---|---|
| `unauthenticated` | 401 | no bearer, or one Heimdall does not know (the API must answer this, not redirect to a sign-in page) |
| `reauthentication_required` | 401 | a commit on a `highStakes` motion with no sign-in fresh enough (#37 D2: `prompt=login`, `max_age`); the voter signs in again at the top level and retries |
| `forbidden` | 403 | authenticated but not an Active member, or a Mind key used on a route it may not call |
| `not_found` | 404 | unknown motion id, a Standard motion id, or a reveal with no prior commit |
| `wrong_phase` | 409 | commit outside Commit, reveal outside Reveal |
@ -38,10 +39,15 @@ Every document is CultNet map-mode: all keys are written, nil where absent, and
| `invalid` | 400 | the body fails the schema or an in-document rule (untrimmed text, candidate not in the motion, `round` or `motionId` mismatch, `persona` wrong for the credential) |
| `commitment_mismatch` | 400 | a reveal whose canonical bytes do not hash to the stored commitment |
| `payload_too_large` | 413 | body over 4096 bytes |
| `rate_limited` | 429 | cast rate exceeded; `retryAfterSeconds >= 1`, mirrored in `Retry-After` |
| `rate_limited` | 429 | cast rate exceeded, per account; `retryAfterSeconds >= 1`, mirrored in `Retry-After` |
| `held` | 503 | the Ratatoskr voting brake is set (#37 D5): nothing is read or stored while it is |
Each code has a vector (`refusals`), and every failure of the schema has a negative vector (`invalid`).
**Not on Bifrost's member API yet.** The **413 body cap of 4096 bytes** and the **429 per-account cast limit** are not on Bifrost's member sealed API today (facts file sections 3c and 5): both exist only on the Mind intake route (4096 bytes; 30 requests a minute per source address). #37 proposes 30 casts per 10 minutes per account for the member routes (its D6). `reauthentication_required` and `held` are #37 proposals (D2, D5) and exist nowhere in code. All four are spec text until Heimdall builds them.
**Two refusals Bifrost must split first.** Bifrost returns a reveal whose bytes do not hash to the stored commitment as a plain `400` `Invalid` ("The ballot does not match the stored commitment."), the same status and error type as every other bad body, so `commitment_mismatch` and `invalid` are one today. It returns a contended reveal (a race with the close fence, `SealedBallotService.cs:186-189`) as `WrongPhase` `409`, the same type as a reveal outside the Reveal phase, so `contended` and `wrong_phase` are one today. A client cannot tell "send it again" from "too late", or "wrong ballot" from "malformed body", until Bifrost splits each pair.
## Commitment
`commitment = lowercase hex SHA-256 of the UTF-8 bytes of`
@ -60,7 +66,19 @@ A Persona votes only on sealed motions, through a Mind key (Bifrost binds an Ed2
## Authentication
An opaque `Authorization: Bearer` header. The token route, claims, scopes and audience belong to Heimdall and Bifrost and are not designed here. A request without a usable bearer is answered `unauthenticated` as a typed refusal.
On the wire the credential is an opaque `Authorization: Bearer` header. The token route, claims, scopes and audience belong to Heimdall and Bifrost and are not designed here. A request without a usable bearer is answered `unauthenticated` as a typed refusal.
**Auth per route, now and after #37.** Routes are Bifrost's; this contract fixes the documents, not the paths (open choice 3).
| exchange | today (`/api/sealed-motions`, facts file 3c) | after #37 (`/api/votes`) |
|---|---|---|
| motion list | no route | bearer, scope `votes.read` |
| motion read | session cookie + Active member | bearer, scope `votes.read` |
| commit, reveal | session cookie + Active member | bearer, scope `votes.cast`, audience `bifrost-votes`; a high-stakes commit also needs a fresh sign-in |
| Mind cast | anonymous; the Ed25519 signature on a `MindEnvelope` is the credential | unchanged: Minds never use `/api/votes` (#37) |
| open a motion, Mind-key registry | session cookie + moderator | unchanged, cookie only |
Today a member route has **no bearer scheme**: it accepts the host-only `__Host-bifrost_session` cookie and answers an unauthenticated call with a redirect to the sign-in page, not a 401. After #37 `/api/votes` is bearer-only and answers a typed 401. The wire header stays an opaque bearer either way; only Bifrost knows which of the two it is checking.
## PR #37 (Bifrost `docs/ratatoskr-voting-auth.md`, head `447a7cb`)
@ -72,9 +90,9 @@ Checked 2026-11-10 by an Eyes pass over the PR and the schema; conflicts for Hei
4. **Refusals.** The PR lists 401/403/404/409/429 with `WWW-Authenticate`; the contract adds 400 (`invalid`, `commitment_mismatch`) and 413, and a typed body.
5. **Minds.** The PR says Minds never use `/api/votes` and keeps the anonymous signed `/mind` route; the contract carries `persona` in the ballot and a `forbidden` case for a Mind key on a route it may not call. The PR's token is never a Persona's.
6. **Authority.** The contract's `unauthenticated` says a bearer "Heimdall does not know"; the PR makes Bifrost the OIDC issuer (a second client, scopes `votes.read` and `votes.cast`, audience `bifrost-votes`) and notes that Bifrost OIDC is not live (its D7).
7. **Rate limits.** The PR keys per account (30 casts per 10 minutes); the contract says per credential.
7. **Rate limits.** The PR keys per account (30 casts per 10 minutes); the contract now says per account too, and neither exists on the member API yet (above).
8. **Wire format.** The PR's errors are JSON; the contract's vectors are MessagePack. Nothing settles the media type.
9. **The PR's seven open decisions** (consent for `votes.cast`, re-authentication, no refresh token, grant revocation, a Ratatoskr brake, rate numbers, MAS rollout order) assume nothing the contract contradicts. D2 (re-authentication at cast) would need a refusal code if answered yes; `unauthenticated` is the nearest.
9. **The PR's seven open decisions** (consent for `votes.cast`, re-authentication, no refresh token, grant revocation, a Ratatoskr brake, rate numbers, MAS rollout order) assume nothing the contract contradicts. D2 (re-authentication at cast) is answered yes for sealed commit in this contract: `reauthentication_required`, with `highStakes` fixed at open. D5 (a brake) is `held`.
## Open choices for Heimdall

View file

@ -99,7 +99,8 @@
"round",
"phase",
"commitClosesAtUtc",
"closesAtUtc"
"closesAtUtc",
"highStakes"
],
"properties": {
"id": {
@ -125,6 +126,10 @@
},
"closesAtUtc": {
"$ref": "#/$defs/instant"
},
"highStakes": {
"type": "boolean",
"description": "Fixed when the motion opens and never changed after: a high-stakes sealed motion needs a fresh sign-in at commit (Bifrost PR #37, D2)."
}
}
},
@ -215,6 +220,7 @@
"candidates",
"commitClosesAtUtc",
"closesAtUtc",
"highStakes",
"yourVoterId",
"yourCommitment",
"announcement"
@ -270,6 +276,10 @@
"closesAtUtc": {
"$ref": "#/$defs/instant"
},
"highStakes": {
"type": "boolean",
"description": "Fixed when the motion opens and never changed after: a high-stakes sealed motion needs a fresh sign-in at commit (Bifrost PR #37, D2)."
},
"yourVoterId": {
"$ref": "#/$defs/uuid"
},
@ -378,13 +388,15 @@
"enum": [
"unauthenticated",
"forbidden",
"reauthentication_required",
"not_found",
"wrong_phase",
"contended",
"invalid",
"commitment_mismatch",
"payload_too_large",
"rate_limited"
"rate_limited",
"held"
]
},
"error": {