From da5f4d538a558074dcaab22f5a1a3f11ac8e66dc Mon Sep 17 00:00:00 2001 From: Adebanjo Date: Tue, 9 Jun 2026 15:00:40 +0100 Subject: [PATCH] feat: require wallet signature before onboarding form (fixes silent profile-save failures) --- frontend/src/hooks/useProfile.js | 9 ++++++- frontend/src/pages/app/Setup.jsx | 40 +++++++++++++++++++++++++++++++- ship.sh | 21 +++++++++++++++-- 3 files changed, 66 insertions(+), 4 deletions(-) diff --git a/frontend/src/hooks/useProfile.js b/frontend/src/hooks/useProfile.js index fc92f47..9778db6 100644 --- a/frontend/src/hooks/useProfile.js +++ b/frontend/src/hooks/useProfile.js @@ -129,7 +129,14 @@ export function useProfile(address) { // ── Save to server + caches ────────────────────────────────────────────── const saveProfile = useCallback(async (data, { authFetch } = {}) => { if (!address) return; - const _fetch = authFetch ?? fetch; + // The POST /api/v1/profile route requires a JWT. Falling back to an + // unauthenticated `fetch` silently 401s and drops the save (see the Setup + // onboarding bug), so require an authenticated fetch up front. + if (typeof authFetch !== 'function') { + console.error('[useProfile] saveProfile called without authFetch — refusing to POST unauthenticated. Pass { authFetch } from useAuth().'); + return { ok: false, error: 'Not authenticated. Reconnect your wallet and try again.' }; + } + const _fetch = authFetch; const payload = { address, diff --git a/frontend/src/pages/app/Setup.jsx b/frontend/src/pages/app/Setup.jsx index c629a64..bf212bd 100644 --- a/frontend/src/pages/app/Setup.jsx +++ b/frontend/src/pages/app/Setup.jsx @@ -2,6 +2,7 @@ import { useState, useEffect, useRef } from 'react'; import { useNavigate } from 'react-router-dom'; import { useAccount } from 'wagmi'; import { useProfile } from '../../hooks/useProfile'; +import { useAuth } from '../../context/AuthContext'; const AGENT_URL = import.meta.env.VITE_AGENT_URL ?? 'http://localhost:3000'; @@ -62,6 +63,7 @@ export default function Setup() { const navigate = useNavigate(); const { address } = useAccount(); const { saveProfile, profile, synced } = useProfile(address); + const { authFetch, isAuthed, signing, signIn, signError } = useAuth(); const [role, setRole] = useState(null); const [loading, setLoading] = useState(false); @@ -87,7 +89,7 @@ export default function Setup() { if (role === 'contractor' && !form.github.trim()) return; setLoading(true); setError(null); - const result = await saveProfile({ role, ...form }); + const result = await saveProfile({ role, ...form }, { authFetch }); setLoading(false); if (!result?.ok) { setError(result?.error ?? 'Something went wrong. Please try again.'); @@ -107,6 +109,42 @@ export default function Setup() { ); } + // Require a wallet signature (SIWE) BEFORE the onboarding form. Saving the + // profile needs an authenticated session; gating here means the user can never + // fill the form only to have the save rejected for lack of a signature. + if (!isAuthed) { + return ( +
+
+
+
+ C +
+ CronStream +
+
+

Verify your wallet

+

+ Sign a message to prove you own this wallet. It is free, off-chain, and uses no gas. + You only do this once to start onboarding. +

+ + {signError && ( +

{signError}

+ )} +
+
+
+ ); + } + return (
diff --git a/ship.sh b/ship.sh index 4e09c2c..bb620af 100755 --- a/ship.sh +++ b/ship.sh @@ -3,12 +3,18 @@ # ship.sh — turn the current working changes into a merged PR in one command. # # ./ship.sh "what I shipped" +# ./ship.sh "what I shipped" 0x # route the extension to a specific stream # # Flow: branch off main → commit all changes → push → open PR → enable # auto-merge. CI runs on the PR; once it passes (and any branch-protection # rules are satisfied) GitHub merges automatically, which fires the CronStream # webhook and extends the stream. # +# Pass a stream id as the 2nd argument to embed a +# CronStream-Stream-Id: 0x… +# line in the PR body, which routes the verified extension to that exact stream +# (useful when several streams watch the same repo). +# # Requirements: # - git remote 'origin' points at the repo # - GitHub CLI installed and authenticated: gh auth status @@ -17,8 +23,13 @@ set -euo pipefail MSG="${1:-}" +STREAM_ID="${2:-}" if [ -z "$MSG" ]; then - echo "Usage: ./ship.sh \"commit message\"" >&2 + echo "Usage: ./ship.sh \"commit message\" [0x]" >&2 + exit 1 +fi +if [ -n "$STREAM_ID" ] && ! [[ "$STREAM_ID" =~ ^0x[a-fA-F0-9]{64}$ ]]; then + echo "✗ Stream id must be 0x followed by 64 hex chars." >&2 exit 1 fi @@ -49,7 +60,13 @@ echo "→ Pushing" git push -u origin "$BRANCH" --quiet echo "→ Opening PR" -gh pr create --base "$DEFAULT_BRANCH" --head "$BRANCH" --title "$MSG" --fill +if [ -n "$STREAM_ID" ]; then + gh pr create --base "$DEFAULT_BRANCH" --head "$BRANCH" --title "$MSG" \ + --body "$MSG"$'\n\n'"CronStream-Stream-Id: $STREAM_ID" + echo " ↳ routing extension to stream $STREAM_ID" +else + gh pr create --base "$DEFAULT_BRANCH" --head "$BRANCH" --title "$MSG" --fill +fi echo "→ Enabling auto-merge" # --auto merges as soon as required checks/reviews pass. If auto-merge isn't