Social recovery protects a Stellar wallet backup using:
-
Password-based encryption (client-side) — The mnemonic or secret key is encrypted with AES-GCM (PBKDF2-derived key) before any sharing. The recovery password is never sent to the server.
-
Shamir secret sharing (t-of-n) — The encrypted backup is split into
nshares with thresholdt. Each guardian receives one share by email invitation. No single guardian or the server stores the full encrypted backup. -
Guardian approval — Recovery sessions require at least
tguardians to approve before shares are combined. The combined ciphertext is returned only when the threshold is met. -
Final decryption — The account owner supplies the recovery password locally to decrypt the restored ciphertext.
| Component | Current (mock API) | Production requirement |
|---|---|---|
| Share storage | In-memory guardianShares map |
Encrypted at rest; guardian-authenticated retrieval |
| Email invitations | console.info via sendGuardianInvitation |
Transactional email (SES, SendGrid, etc.) with signed approval links |
| Guardian approval | Simulated in UI | Authenticated guardian endpoints + audit log |
Wallet and Stellar errors sent to Sentry use beforeSend and beforeBreadcrumb hooks with shared redaction in app/lib/sentryRedaction.ts. Secret keys, mnemonics, and similar fields are never included in events or breadcrumbs.
invoke_contract operations are not supported in the Horizon batch builder. See SOROBAN_SMART_WALLET_SPIKE.md for the planned Soroban RPC integration.
NextAuth handles CSRF for its own /api/auth/* routes automatically via a double-submit cookie. Custom state-mutating routes are not covered by NextAuth and require their own protection:
| Route | Method | Protected |
|---|---|---|
/api/jobs/[id] |
POST (restart) | ✅ checkCsrf |
/api/upload |
POST | ✅ checkCsrf |
Read-only GET routes and the NextAuth routes are not in scope.
The helper lives in app/lib/csrf.ts and is called at the top of every mutating handler, before authentication:
const csrfError = checkCsrf(request);
if (csrfError) return csrfError; // 403 ForbiddenHow it works:
- The
Originheader is read first. Browsers always include it on cross-origin requests (and on same-origin POST requests in all modern browsers). - If
Originis absent, theRefererheader is used as a fallback — some corporate proxies stripOriginbut preserveReferer. - The extracted origin is compared against the origin component of
NEXTAUTH_URL(e.g.https://app.clipcash.ai). Only an exact match passes — subdomains, different ports, andhttpvshttpsare all rejected. - Requests with neither header are rejected by default. Routes that must accept server-to-server calls without browser headers can opt in with
checkCsrf(request, { allowMissingOrigin: true }). - If
NEXTAUTH_URLis not set, the check is skipped with aconsole.warn.validateEnvwill have already warned at startup.
Why not a token? Double-submit cookies require client-side coordination (reading a cookie, adding a header or body field). Origin/Referer checking provides equivalent protection for this application's threat model with zero client overhead. This approach is used by Django, Rails, and other frameworks as a primary CSRF mitigation.
Apply checkCsrf to any new POST, PATCH, or DELETE handler:
import { checkCsrf } from "@/app/lib/csrf";
export async function POST(request: NextRequest) {
const csrfError = checkCsrf(request);
if (csrfError) return csrfError;
// ... rest of handler
}Integration tests should cover both the same-origin (pass) and cross-origin (403) cases. See __tests__/api/jobs.csrf.test.ts and __tests__/api/upload.csrf.test.ts for examples.