feat(phase-2): wallet_contacts — data model + API (#67)#73
Draft
feat(phase-2): wallet_contacts — data model + API (#67)#73
Conversation
Adds the wallet_contacts table (migration), repository, service, controller,
and routes. GET /:address is public and returns only { email_set, notifications_enabled }.
PUT /:address is guarded by requireOwnWallet (new SIWS middleware). validateEmail
added to server/api/utils/validation.js. 85 tests passing.
|
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.
Summary
Phase 2 P2-01 — identity-layer plumbing. Adds the
wallet_contactstable keyed by SS58 address with an optional email and a singlenotifications_enabledflag. Surfaced through:GET /api/wallet-contacts/:address— public, returns{ email_set, notifications_enabled }only. Never leaks the raw email (asserted in tests).PUT /api/wallet-contacts/:address— SIWS-gated for the matching wallet via a newrequireOwnWalletmiddleware. Partial updates: fields not present in the body are preserved.Closes #67.
What's in the diff
Two commits:
f37ac2d— initial implementation (8 new files: migration + repo + service + controller + routes + 3 test files; 3 modified: middleware, validation utils, server.js mount).d3e0c09— review-blocker fixes:DISABLE_SIWS_DOMAIN_CHECK/EXPECTED_DOMAIN) torequireOwnWallet, matchingrequireAdmin.upsertByWalleta true partial update (read-modify-write using'key' in fieldsto distinguish omitted from explicit-null) — fixed a data-loss bug wherePUT { email: 'x' }would silently resetnotifications_enabledtotrue.expect(validateEmail).not.toHaveBeenCalled()assertion on the no-email PUT path.New surface
supabase/migrations/20260430000000_create_wallet_contacts.sql— table + partial indexWHERE email IS NOT NULL.requireOwnWalletinauth.middleware.js— SIWS-verifies + asserts the signer's pubkey matches the route param's:addresspubkey (prefix-agnostic viau8aToHex(decodeAddress(...))). Honorsdev-bypass. New SIWS statement"Update notification preferences for wallet on Stadium".validateEmailinvalidation.js— RFC 5321 simplified, returns the project-standard{ valid, error, normalised }envelope; lowercases + trims intonormalised.Test plan
cd server && npm test— 89/89 passing (was 80/80 on develop; this PR adds 9 server-side tests across 3 files, plus the regression test).cd client && npm run build— clean.cd client && npm run lint— 0 errors, 0 warnings.UI verification (stadium-tester)
Not applicable for this PR — server-only API, no UI surface.
Per the issue's scope ("No UI yet — UI lands in P2-05"), there's nothing for
stadium-testerto drive. The mock-mode preview short-circuits client API calls; this PR doesn't add client API calls. UI verification will run on P2-05 whenNotificationsCard.tsxand the focus-refetch wiring land.The issue's 5
## Test scenariosare server endpoint behaviour. Each is mapped to a Vitest assertion below:GET /api/wallet-contacts/<unknown>→200 { email_set: false, notifications_enabled: true }wallet-contact.controller.test.js— "GET unknown wallet returns default shape"PUT /api/wallet-contacts/<address>matching signer + valid email → 200, GET reflectsemail_set: truewallet-contact.controller.test.js— "PUT happy path" +wallet-contact.repository.test.js—upsertByWalletround-tripPUTsigned by mismatched wallet → 403wallet-contact.middleware.test.js— "signer mismatch returns 403 with reason: not-your-wallet"PUTwith malformed email → 400 (impl returns 422 per the project's field-validation convention; matchesprogram.controller.js)wallet-contact.controller.test.js— "PUT with malformed email returns 422"emailkeywallet-contact.controller.test.js—expect(body.data).not.toHaveProperty('email')on both empty + populated casesManual QA from the issue (real-wallet smoke) is for the human reviewer to walk before merge. Runs against local dev with a real wallet — outside the agent's scope per workflow.
Backlog items logged
Appended to
docs/improvement-backlog.md:requireTeamMemberOrAdminlacks the same SIWS domain check thatrequireAdmin(and nowrequireOwnWallet) enforce. Pre-existing inconsistency, separate hardening pass.Invariants verified
BYPASS_ADMIN_CHECKstill false (untouched).auth.middleware.js— new PUT route usesrequireOwnWalletdeclared there.console.log/warn/errorin client (no client changes).server/api/**.emailkey — enforced at the service boundary, asserted in controller tests.