Publish ballot rationales to IPFS (and close the open pin endpoint) - #362
Merged
Conversation
…endpoint Agents can now draft a rationale and publish it; submitting the vote and signing remain with the wallet's signers. **Security fix.** POST /api/pinata-storage/put had no authentication at all — anyone on the internet could pin arbitrary content to the project's Pinata account, burn its quota, and leave no record of who did it. It now requires the app's wallet session or a v1 bearer token. Both existing callers (registerDrep, updateDrep) are same-origin in-app fetches that already send the session cookie, so they are unaffected. **New endpoint.** POST /api/v1/ballotRationaleAnchor turns the text already stored in a ballot's rationaleComments into a CIP-100/136 JSON-LD document, pins it, and writes the anchor URL and hash back at the same proposal index. Anchors were previously only settable by hand — pasted in, or round-tripped through the ballot CSV. Authorization matches botBallotsUpsert: bots need ballot:write plus wallet access, humans are gated on the shared signer-or-owner check. **The hashing is the subtle part.** hashDrepAnchor is not a JSON-LD canonicalisation — it is blake2b-256 over JSON.stringify(doc, null, 2). Verified empirically: reordering keys changes the digest, and the digest matches the two-space pretty form, not the minified one. So the bytes pinned must be exactly that form or a verifier fetching the URL and re-hashing gets a different answer than the chain records. serializeRationale is the single place that decides those bytes, and both the pin and the hash read from it. Checked end to end against the real hashDrepAnchor: hash === blake2b256(pinned bytes). Empty optional fields are omitted rather than emitted as empty strings, since every key changes the hash. Extracts the Pinata call into src/lib/server/pinataUpload.ts so the new endpoint pins directly instead of looping back through an HTTP route. The MCP tool `ballot_publish_rationale` (scope ballots:write) is the second and last write tool. mcpTools.test.ts now asserts exactly two writable tools, both non-destructive — widening that list stays a deliberate act. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Agents can now draft a rationale and publish it. Submitting the vote and signing stay with the wallet's signers.
Security fix: the pin endpoint was wide open
POST /api/pinata-storage/puthad no authentication at all. Anyone on the internet could pin arbitrary content to the project's Pinata account, burn its quota, and leave no record of who did it. It now requires the app's wallet session or a v1 bearer token.Both existing callers (
registerDrep,updateDrep) are same-origin in-app fetches that already send the session cookie, so they're unaffected. Verified against a running server: unauthenticated requests to both this and the new endpoint return 401.This is why I didn't build agent uploads on top of it earlier — doing so would have widened the hole rather than added a feature.
New endpoint
POST /api/v1/ballotRationaleAnchorturns the text already stored in a ballot'srationaleCommentsinto a CIP-100/136 JSON-LD document, pins it, and writes the anchor URL and hash back at the same proposal index.Anchors were previously only settable by hand — pasted in, or round-tripped through the ballot CSV. Authorization matches
botBallotsUpsert: bots needballot:writeplus wallet access; humans are gated on the shared signer-or-owner check.The hashing is the subtle part
hashDrepAnchoris not a JSON-LD canonicalisation — it's blake2b-256 overJSON.stringify(doc, null, 2). Verified empirically: reordering keys changes the digest, and the digest matches the two-space pretty form, not the minified one.So the bytes pinned to IPFS must be exactly that form, or a verifier fetching the URL and re-hashing gets a different answer than the chain records.
serializeRationaleis the single place that decides those bytes, and both the pin and the hash read from it.Checked end to end against the real
hashDrepAnchor:Empty optional fields are omitted rather than emitted as empty strings, since every key changes the hash.
Boundary
ballot_publish_rationale(scopeballots:write) is the second and last write tool.mcpTools.test.tsnow asserts exactly two writable tools, both non-destructive, so widening that list stays a deliberate act rather than a quiet registry addition.Verification
tscclean, build green, 814 + 76 tests. Nine new tests on the anchor: context shape, 300-char summary truncation, empty-field omission, pretty-JSON serialization,hash === blake2b256(pinned bytes), determinism, and filename sanitisation (rationale-aa11#0/../../etc/passwd→rationale-aa11-0.jsonld).🤖 Generated with Claude Code