Skip to content

Document Sign-Off MVP + roadmap MRP mapping - #356

Open
QSchlegel wants to merge 4 commits into
preprodfrom
claude/document-signoff-mvp
Open

Document Sign-Off MVP + roadmap MRP mapping#356
QSchlegel wants to merge 4 commits into
preprodfrom
claude/document-signoff-mvp

Conversation

@QSchlegel

@QSchlegel QSchlegel commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator

Three independent changes, one per commit — review them separately.

1. docs(roadmap) — MRP mapping and the April–July window

The 2026-08-03 renumbering (984aa46) moved Month 1 to April, but MRP task cards created before that still carry the following month's bullet text — the card headed "MRP Month 2" lists the June workstreams. This writes the mapping down so it stops costing time.

2. feat(documents) — Document Sign-Off MVP (PRD-001)

A wallet-native, off-chain approval layer: bind approval to an exact content hash, inherit the wallet's signers and threshold, export a proof anyone can verify without an account. No chain dependency.

Two rules carry the feature, and both are enforced server-side, not in the UI:

  1. Version-hash binding. submitSignerAction rebuilds the canonical signed payload from the server's own records and requires a byte-identical match before the signature is checked. A signature collected for one version cannot be replayed onto another, and a tampered comment invalidates the submission. Inline content is re-hashed server-side and rejected on mismatch.
  2. Threshold from a frozen snapshot. DocumentSignerSnapshot captures the wallet's signers, threshold and policy hash when the round starts. Approval counting reads the snapshot, never the live wallet — changing wallet membership cannot rewrite a decision already made.

Data modelDocument, DocumentVersion, DocumentReview, DocumentSignerSnapshot, DocumentEvent, migration 20260805090000_add_document_signoff. The migration enables RLS with deny-all PostgREST policies on all five tables, matching the contract in 20260706100000_enable_rls_followup_tables (#332).

RoutercreateDocument, uploadVersion, startReview, submitSignerAction, exportProof, verifyProof. CIP-8 verification via Mesh's checkSignature against the signer's address. verifyProof is public on purpose: a counterparty holding the JSON and the file must be able to check it without an account, and it reads nothing from the database.

Four routes under /wallets/[wallet]/documents — list, create, detail, version review. Files are hashed in the browser via WebCrypto; only the digest is sent, so the bytes never leave the signer's machine.

src/lib/documents/ is dependency-free apart from node crypto, with the signature check injected into the verifier — the same code runs in an offline verifier with no Mesh install.

3. feat(seo) — a social card per route

Every page shared one og-image.png, so a /governance link and a /roadmap link produced identical previews: the card said "Mesh Multisig" and nothing about where it pointed.

  • scripts/generate-og-image.mjs now renders ten cards from one template — home, features, governance, DRep explorer, roadmap, feature graph, blog, API docs, dApps, import wallet. Each carries its own eyebrow, headline, subhead, footer chips and accent tint, so the destination is legible at feed-thumbnail size. Copy is authored as explicit lines (SVG has no wrapping) and the script warns when an estimated line width would overflow the content column.
  • routeSeo entries carry image + imageAlt; getRouteSeo resolves them, _app falls back route-card-first, and Metatags emits real alt text instead of repeating the title. Blog posts without their own artwork now fall back to the blog card rather than the home card.
  • og:image URLs carry ?v= (OG_IMAGE_VERSION). Scrapers cache by image URL and would otherwise keep serving the old artwork indefinitely — the query string is what makes X, LinkedIn and Slack re-fetch.

Nothing at build time ties the committed PNGs to the paths in seo.ts, so src/__tests__/ogCards.test.ts asserts every referenced card exists, is exactly 1200×630, and has alt text.

Heads-up for the deploy

This adds a migration. Railway's prestart runs prisma migrate deploy, so merging to preprod will apply 20260805090000_add_document_signoff to the preprod database on the next deploy. Five new tables, no changes to existing ones.

Verification

Re-run after merging origin/preprod (the only conflict was positional — both branches appended models to the end of prisma/schema.prisma, Document Sign-Off here and the OAuth 2.1 server on preprod; both blocks kept):

  • npx tsc --noEmit — clean. The txScriptRecovery.test.ts errors noted earlier are gone; preprod fixed them.
  • npx jest1084 passing, 2 skipped, 100 suites. Includes the 28 Document Sign-Off tests (canonicalization, version-hash binding, threshold evaluation, proof verification under tampering, duplicate signers, out-of-snapshot signers and non-canonical payloads) and the 29 new OG card tests.
  • npx prisma validate — schema valid after the merge
  • npm run build — passes; all four document routes register as server-rendered
  • /roadmap checked in a local production build (next dev cannot serve SSR in a worktree — Turbopack resolves the whisky WASM to /ROOT/node_modules/…)

Not included

PRD-001 is still status: Draft. Proof export is JSON only — the PDF summary and the public verify page are the M5 half of the MVP.

🤖 Generated with Claude Code

QSchlegel and others added 2 commits August 7, 2026 09:20
…m/hours line

The month headings were renumbered on 2026-08-03 (984aa46) so that Month 1 =
April, but MRP task cards created before that date still carry bullet text for
the following month — the card headed "MRP Month 2" lists the June workstreams.
That mismatch has now cost enough time to be worth writing down.

- Add an MRP task mapping table at the top: MRP Month N = roadmap Month N =
  calendar month, for all twelve months, with the on-chain task hashes we have
  and a note explaining why the cards disagree.
- Route each MRP month to the actual merged PRs behind it (April 10, May 3,
  June 51, July 16), each count linking to the exact GitHub search so any row
  can be reproduced rather than taken on trust.
- Widen "Delivered to date" from May–July to April–July. April's output is
  infrastructure, so it folds into the existing sections: the preprod
  environment and real-chain smoke CI (#218, #217) under Testing & CI, and a
  new transaction-and-signing-integrity through-line under Platform (#217 VKey
  witness filtering -> #227 invalid-CBOR guard -> #257 Mesh pin + witness
  verification guard).
- Recover two May items the 2026-07-26 audit had missed: the Import Wallet
  wizard (#259) and the #257 signing fix.
- Fix the timeline left stale by the renumbering: April 2026 – March 2027, in
  ROADMAP.md, the /roadmap page and the SEO description.
- Remove the "Quirin + Andre · ~25 h/wk" line from ROADMAP.md and the public
  roadmap page.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…gn-off

Implements PRD-001: a wallet-native, off-chain approval layer where a team
binds approval to an exact content hash, inherits the wallet's signer set and
threshold, and exports a proof anyone can verify without an account.

Two rules carry the feature, and both are enforced server-side rather than in
the UI:

1. Version-hash binding. submitSignerAction rebuilds the canonical signed
   payload from the server's own records and requires a byte-identical match
   *before* the signature is checked, so a signature collected for one version
   can never be replayed onto another, and a tampered comment invalidates the
   submission. Inline content is re-hashed server-side and rejected on mismatch.

2. Threshold inheritance from a frozen snapshot. DocumentSignerSnapshot captures
   the wallet's signers, threshold and policy hash when the round starts;
   approval counting reads the snapshot, never the live wallet, so changing
   wallet membership cannot rewrite a decision already made.

Data model (5 models + migration 20260805090000): Document, DocumentVersion,
DocumentReview, DocumentSignerSnapshot, DocumentEvent. The migration enables RLS
with deny-all PostgREST policies on all five tables, matching the contract in
20260706100000_enable_rls_followup_tables (#332).

Router (src/server/api/routers/documents.ts): createDocument, uploadVersion,
startReview, submitSignerAction, exportProof, verifyProof, plus the reads the
pages need. CIP-8 verification uses Mesh's checkSignature against the signer's
address. verifyProof is public on purpose — a counterparty holding the JSON and
the file must be able to check it without an account, and it touches no DB.

Four routes under /wallets/[wallet]/documents: list, create, detail, and the
version review page. Files are hashed in the browser (SHA-256 via WebCrypto);
only the digest is sent, so the bytes never leave the signer's machine.

src/lib/documents/ is dependency-free apart from node crypto, with the signature
check injected into the verifier — the same code can run in an offline verifier
with no Mesh install.

Uploading a new version supersedes the previous one and starts a fresh round at
zero approvals: approval is bound to the hash, not the title.

Tests: 28 covering canonicalization, version-hash binding, threshold evaluation
and proof verification including tampering, duplicate signers, out-of-snapshot
signers and non-canonical payloads.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Aug 7, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
multisig Ready Ready Preview Aug 19, 2026 10:30am

Request Review

Both branches appended new models to the end of prisma/schema.prisma —
Document Sign-Off (five entities) here, the OAuth 2.1 authorization server
on preprod — so the conflict was positional, not semantic. Kept both
blocks. Everything else auto-merged: the `document` router sits alongside
the new `mcp` router in root.ts, and preprod's `/oauth` noindex prefix
landed next to the existing ones in seo.ts.
Every page shared the same og-image, so a /governance link and a /roadmap
link were indistinguishable in a feed — the preview said "Mesh Multisig"
and nothing about where it pointed.

- generate-og-image.mjs now renders ten cards from one template. Each has
  its own eyebrow, headline, subhead, footer chips and accent tint, so the
  card identifies the destination at thumbnail size. Copy is authored as
  explicit lines (SVG has no wrapping) and the script warns when a line's
  estimated width would overflow the content column.
- routeSeo entries carry `image` + `imageAlt`; getRouteSeo resolves them,
  _app falls back route-card-first, and Metatags emits real alt text
  instead of repeating the title. Blog posts without their own artwork now
  get the blog card rather than the home card.
- og:image URLs carry ?v= (OG_IMAGE_VERSION). Scrapers cache by URL and
  would otherwise keep serving the old artwork indefinitely.

Nothing at build time ties the PNGs to the paths in seo.ts, so ogCards
asserts each referenced card exists, is 1200x630, and has alt text.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant