From 16835f50503df009056407e48fb1cfac3e37e342 Mon Sep 17 00:00:00 2001 From: arcabotai <261107832+arcabotai@users.noreply.github.com> Date: Mon, 22 Jun 2026 08:27:20 -0400 Subject: [PATCH] fix: audit quick-wins + security hardening MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit From a multi-agent codebase audit. Quick wins: - og: fix undefined `superlogo` (every cast OG / share-preview image 500'd; it was masked by a @ts-ignore) — reuse the existing castoraLogo. - search-suggestions, profile/search: guard a missing `query` param (was a 500 via .replace on null) — return empty results. - notifications: reject an invalid `mode` with 400 instead of a 500. - finish-registration: wrap the 4 account-creation writes in a $transaction so a partial failure can't orphan rows / wedge the unique fid on retry. - dead code: remove orphaned components (banner, PwaInstallPrompt), the dead super-auth routes (create-connection, create-signer, verify-farcaster), and unused deps (lokijs, encoding). Security: - url-preview/other (public, unauthenticated): add an SSRF guard — block private/reserved/link-local/metadata IPs (resolved, not just the literal) and disable redirect-following. (new URL() normalizes alt IP encodings — verified.) - crypto-checkout webhook (Daimo, money path): constant-time token compare; make payment_completed exactly-once via a compare-and-swap claim with revert-on-failure (was: double-provision on every retry); fix a bare `return;` that returned undefined from the handler. Verified: typecheck + build green; adversarial review of the SSRF + webhook changes (alt-IP-encoding & zone-id "bypasses" confirmed non-issues via new URL() normalization; webhook reworked to a CAS claim). Follow-up: make handleSuccessfulPayment idempotent (defense-in-depth) and retire the remaining dead super-auth code (AccountConnectionSession model, signer.ts) — both need a Prisma migration, kept separate. --- CHANGELOG.md | 4 + package-lock.json | 606 +++++++++++++++++- package.json | 2 - .../api/account/create-connection/route.ts | 29 - src/app/api/account/create-signer/route.ts | 14 - .../api/account/finish-registration/route.ts | 54 +- src/app/api/account/verify-farcaster/route.ts | 12 - src/app/api/crypto-checkout/webhook/route.ts | 68 +- src/app/api/notifications/route.ts | 2 + src/app/api/og/route.tsx | 4 +- src/app/api/profile/search/route.ts | 6 +- src/app/api/search-suggestions/route.ts | 6 +- src/app/api/url-preview/other/route.ts | 51 ++ src/app/changelog/entries.ts | 9 + src/components/PwaInstallPrompt.tsx | 39 -- src/components/banner.tsx | 14 - 16 files changed, 743 insertions(+), 177 deletions(-) delete mode 100644 src/app/api/account/create-connection/route.ts delete mode 100644 src/app/api/account/create-signer/route.ts delete mode 100644 src/app/api/account/verify-farcaster/route.ts delete mode 100644 src/components/PwaInstallPrompt.tsx delete mode 100644 src/components/banner.tsx diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d45dd2..d7bcfe9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ the public changelog at [castora.social/changelog](https://castora.social/change > `src/app/changelog/entries.ts` (the website source of truth, newest first) and > mirror it here. Keep wording user-friendly. See `AGENTS.md`. +## 2026-06-22 — Fixes & hardening +- Fixed the preview image shown when a Castora cast is shared elsewhere — it was failing to render. +- Behind-the-scenes reliability and security work: sturdier handling of malformed requests, safer link previews, a hardened crypto-payment webhook, and a cleanup of unused code and dependencies. + ## 2026-06-22 — Multiple accounts are back - Connect more than one Farcaster account and switch between them from the profile menu — browse, post, and get notifications as any of your accounts. - The "Add account" button works again (sign in with Neynar): a new account is attached alongside your existing ones instead of replacing them. diff --git a/package-lock.json b/package-lock.json index 8d8d321..0213196 100644 --- a/package-lock.json +++ b/package-lock.json @@ -56,10 +56,8 @@ "dotenv": "^16.4.5", "embla-carousel-react": "^8.1.5", "emoji-picker-react": "^4.6.15", - "encoding": "^0.1.13", "ioredis": "^5.4.2", "lodash": "^4.17.21", - "lokijs": "^1.5.12", "lucide-react": "^0.334.0", "next": "13.5.5", "next-pwa": "^5.6.0", @@ -2533,6 +2531,70 @@ "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.1.tgz", "integrity": "sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==" }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz", + "integrity": "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.12.tgz", + "integrity": "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.12.tgz", + "integrity": "sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.12.tgz", + "integrity": "sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, "node_modules/@esbuild/darwin-arm64": { "version": "0.19.12", "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.12.tgz", @@ -2548,6 +2610,294 @@ "node": ">=12" } }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.12.tgz", + "integrity": "sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.12.tgz", + "integrity": "sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.12.tgz", + "integrity": "sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.12.tgz", + "integrity": "sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.12.tgz", + "integrity": "sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.12.tgz", + "integrity": "sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.12.tgz", + "integrity": "sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.12.tgz", + "integrity": "sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.12.tgz", + "integrity": "sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.12.tgz", + "integrity": "sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.12.tgz", + "integrity": "sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.12.tgz", + "integrity": "sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.12.tgz", + "integrity": "sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.12.tgz", + "integrity": "sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.12.tgz", + "integrity": "sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.12.tgz", + "integrity": "sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.12.tgz", + "integrity": "sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.12.tgz", + "integrity": "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, "node_modules/@ethereumjs/common": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/@ethereumjs/common/-/common-3.2.0.tgz", @@ -5002,6 +5352,26 @@ "@parcel/watcher-win32-x64": "2.4.1" } }, + "node_modules/@parcel/watcher-android-arm64": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.4.1.tgz", + "integrity": "sha512-LOi/WTbbh3aTn2RYddrO8pnapixAziFl6SMxHM69r3tvdSm94JtCenaKgk1GRg5FJ5wpMCpHeW+7yqPlvZv7kg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, "node_modules/@parcel/watcher-darwin-arm64": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.4.1.tgz", @@ -5021,6 +5391,161 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/@parcel/watcher-darwin-x64": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.4.1.tgz", + "integrity": "sha512-yrw81BRLjjtHyDu7J61oPuSoeYWR3lDElcPGJyOvIXmor6DEo7/G2u1o7I38cwlcoBHQFULqF6nesIX3tsEXMg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-freebsd-x64": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.4.1.tgz", + "integrity": "sha512-TJa3Pex/gX3CWIx/Co8k+ykNdDCLx+TuZj3f3h7eOjgpdKM+Mnix37RYsYU4LHhiYJz3DK5nFCCra81p6g050w==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-glibc": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.4.1.tgz", + "integrity": "sha512-4rVYDlsMEYfa537BRXxJ5UF4ddNwnr2/1O4MHM5PjI9cvV2qymvhwZSFgXqbS8YoTk5i/JR0L0JDs69BUn45YA==", + "cpu": [ + "arm" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-glibc": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.4.1.tgz", + "integrity": "sha512-BJ7mH985OADVLpbrzCLgrJ3TOpiZggE9FMblfO65PlOCdG++xJpKUJ0Aol74ZUIYfb8WsRlUdgrZxKkz3zXWYA==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-musl": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.4.1.tgz", + "integrity": "sha512-p4Xb7JGq3MLgAfYhslU2SjoV9G0kI0Xry0kuxeG/41UfpjHGOhv7UoUDAz/jb1u2elbhazy4rRBL8PegPJFBhA==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-glibc": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.4.1.tgz", + "integrity": "sha512-s9O3fByZ/2pyYDPoLM6zt92yu6P4E39a03zvO0qCHOTjxmt3GHRMLuRZEWhWLASTMSrrnVNWdVI/+pUElJBBBg==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-musl": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.4.1.tgz", + "integrity": "sha512-L2nZTYR1myLNST0O632g0Dx9LyMNHrn6TOt76sYxWLdff3cB22/GZX2UPtJnaqQPdCRoszoY5rcOj4oMTtp5fQ==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, "node_modules/@parcel/watcher-wasm": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/@parcel/watcher-wasm/-/watcher-wasm-2.4.1.tgz", @@ -5046,6 +5571,66 @@ "inBundle": true, "license": "MIT" }, + "node_modules/@parcel/watcher-win32-arm64": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.4.1.tgz", + "integrity": "sha512-Uq2BPp5GWhrq/lcuItCHoqxjULU1QYEcyjSO5jqqOK8RNFDBQnenMMx4gAl3v8GiWa59E9+uDM7yZ6LxwUIfRg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-ia32": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.4.1.tgz", + "integrity": "sha512-maNRit5QQV2kgHFSYwftmPBxiuK5u4DXjbXx7q6eKjq5dsLXZ4FJiVvlcw35QXzk0KrUecJmuVFbj4uV9oYrcw==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-x64": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.4.1.tgz", + "integrity": "sha512-+DvS92F9ezicfswqrvIRM2njcYJbd5mb9CUgtrHCHmvn7pPPa+nMDRu1o1bYYz/l5IB2NVGNJWiH7h1E58IF2A==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, "node_modules/@parcel/watcher/node_modules/node-addon-api": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", @@ -11535,16 +12120,6 @@ "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", "peer": true }, - "node_modules/@upstash/redis": { - "version": "1.34.3", - "resolved": "https://registry.npmjs.org/@upstash/redis/-/redis-1.34.3.tgz", - "integrity": "sha512-VT25TyODGy/8ljl7GADnJoMmtmJ1F8d84UXfGonRRF8fWYJz7+2J6GzW+a6ETGtk4OyuRTt7FRSvFG5GvrfSdQ==", - "optional": true, - "peer": true, - "dependencies": { - "crypto-js": "^4.2.0" - } - }, "node_modules/@vanilla-extract/css": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/@vanilla-extract/css/-/css-1.14.1.tgz", @@ -14572,13 +15147,6 @@ "node": "*" } }, - "node_modules/crypto-js": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz", - "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==", - "optional": true, - "peer": true - }, "node_modules/crypto-random-string": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", diff --git a/package.json b/package.json index edd90f9..07f1075 100644 --- a/package.json +++ b/package.json @@ -70,10 +70,8 @@ "dotenv": "^16.4.5", "embla-carousel-react": "^8.1.5", "emoji-picker-react": "^4.6.15", - "encoding": "^0.1.13", "ioredis": "^5.4.2", "lodash": "^4.17.21", - "lokijs": "^1.5.12", "lucide-react": "^0.334.0", "next": "13.5.5", "next-pwa": "^5.6.0", diff --git a/src/app/api/account/create-connection/route.ts b/src/app/api/account/create-connection/route.ts deleted file mode 100644 index ec239a7..0000000 --- a/src/app/api/account/create-connection/route.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { prisma } from "@/prisma/client"; -import { isAuthenticated } from "@/utils/auth/isAuthenticated"; -import { isAuthorized } from "@/utils/auth/isAuthorized"; -import axios from "axios"; - -export async function POST(req: Request) { - - const { authenticated, supercastUser } = await isAuthenticated(req) - - if (!authenticated) { - return Response.json({ "error": "Not authenticated" }, { status: 401 }) - } - - const targetFid = Number(req.headers.get("asFid")) - - const { authorized, error_message } = await isAuthorized(supercastUser, targetFid) - - if (!authorized) { - return Response.json({ "error": error_message }, { status: 403 }) - } - - const connectionSession = await prisma.accountConnectionSession.create({ - data: { - supercastPrivyUserId: supercastUser.id, - } - }) - - return Response.json({ "connectionSession": connectionSession.id }); -} \ No newline at end of file diff --git a/src/app/api/account/create-signer/route.ts b/src/app/api/account/create-signer/route.ts deleted file mode 100644 index a573337..0000000 --- a/src/app/api/account/create-signer/route.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { isAuthenticated } from '@/utils/auth/isAuthenticated'; - -export async function POST(req: Request) { - - const { authenticated } = await isAuthenticated(req) - - if (!authenticated) { - return Response.json({ "error": "Not authenticated" }, { status: 401 }) - } - - return Response.json({ - "error": "Legacy signer creation is disabled. Connect your Farcaster account with Sign in with Neynar from onboarding/settings." - }, { status: 410 }) -}; diff --git a/src/app/api/account/finish-registration/route.ts b/src/app/api/account/finish-registration/route.ts index ed8a1f3..22e6aac 100644 --- a/src/app/api/account/finish-registration/route.ts +++ b/src/app/api/account/finish-registration/route.ts @@ -67,34 +67,32 @@ export async function POST(req: Request) { const newNeynarSigner = response.data.signer.signer_uuid; - await prisma.supercastPrivyUser.update({ - where: { - id: supercastUser.id - }, - data: { - fid: fid - } - }) - - const supercastFarcasterAccount = await prisma.supercastFarcasterAccount.create({ - data: { - fid: fid, - signerUUID: newNeynarSigner - } - }) - - const connectedAccount = await prisma.connectedAccount.create({ - data: { - supercastFarcasterAccountId: supercastFarcasterAccount.id, - supercastPrivyUserId: supercastUser.id, - } - }) - - const createdAccount = await prisma.createdAccount.create({ - data: { - createdById: supercastUser.id, - createdSupercastAccountId: supercastUser.id, - } + // Atomic: a partial failure here would orphan rows and, because + // SupercastFarcasterAccount.fid is unique, a retry would then throw on the + // duplicate — wedging the registration. Roll back all-or-nothing. + await prisma.$transaction(async (tx) => { + await tx.supercastPrivyUser.update({ + where: { id: supercastUser.id }, + data: { fid: fid }, + }) + + const supercastFarcasterAccount = await tx.supercastFarcasterAccount.create({ + data: { fid: fid, signerUUID: newNeynarSigner }, + }) + + await tx.connectedAccount.create({ + data: { + supercastFarcasterAccountId: supercastFarcasterAccount.id, + supercastPrivyUserId: supercastUser.id, + }, + }) + + await tx.createdAccount.create({ + data: { + createdById: supercastUser.id, + createdSupercastAccountId: supercastUser.id, + }, + }) }) trackPosthogEvent(fid, "account_created", {}) diff --git a/src/app/api/account/verify-farcaster/route.ts b/src/app/api/account/verify-farcaster/route.ts deleted file mode 100644 index c72af16..0000000 --- a/src/app/api/account/verify-farcaster/route.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { getBearerToken } from "@/utils/auth/getBearerToken"; - -export async function POST(req: Request) { - const authToken = getBearerToken(req); - if (!authToken) { - return Response.json({ error: "Invalid auth" }, { status: 401 }); - } - - return Response.json({ - error: "Legacy Farcaster verification is disabled. Connect with Sign in with Neynar via /api/account/siwn." - }, { status: 410 }); -} diff --git a/src/app/api/crypto-checkout/webhook/route.ts b/src/app/api/crypto-checkout/webhook/route.ts index 434ca85..0d3493d 100644 --- a/src/app/api/crypto-checkout/webhook/route.ts +++ b/src/app/api/crypto-checkout/webhook/route.ts @@ -4,6 +4,7 @@ import { PLAN_STATUS, SESSION_STATUS, } from "@prisma/client"; +import crypto from "node:crypto"; import { prisma } from "../../../../prisma/client"; import { trackPosthogEvent } from "../../../../utils/posthogAnalytics"; import { memberOnboarding } from "@/utils/members"; @@ -11,11 +12,17 @@ import { handleSuccessfulPayment } from "@/utils/checkout"; /** Handle Daimo Pay webhook */ export async function POST(request: Request) { - // Auth + // Auth — constant-time comparison so the static token can't be recovered via + // response timing. const daimoPayWebhookToken = process.env.DAIMO_PAY_WEBHOOK_TOKEN; - const auth = request.headers.get("Authorization"); - if (auth !== `Basic ${daimoPayWebhookToken}`) { - console.error(`DaimoPayWebhook: bad auth: ${auth}`); + const expectedBytes = new TextEncoder().encode(daimoPayWebhookToken ? `Basic ${daimoPayWebhookToken}` : ""); + const providedBytes = new TextEncoder().encode(request.headers.get("Authorization") ?? ""); + const authOk = + !!daimoPayWebhookToken && + providedBytes.length === expectedBytes.length && + crypto.timingSafeEqual(providedBytes, expectedBytes); + if (!authOk) { + console.error("DaimoPayWebhook: bad auth"); return Response.json({}, { status: 401 }); } @@ -29,23 +36,52 @@ export async function POST(request: Request) { } else if (type === "payment_completed") { if (chainId !== 8453) { console.warn(`DaimoPayWebhook: unexpected chainId: ${chainId}`); - return; + return Response.json({ ignored: true }); } // TODO: potentially check if the usd value matches const receiptUrl = `https://basescan.org/tx/${txHash}`; - const updatedPaymentSession = await prisma.paymentSession.update({ - where: { - sessionId: paymentId, - }, - data: { - sessionStatus: SESSION_STATUS.SUCCESS, - receiptUrl - }, - }) - - await handleSuccessfulPayment(updatedPaymentSession) + // Legit Daimo retries re-deliver this event, so make provisioning exactly-once. + const existing = await prisma.paymentSession.findUnique({ + where: { sessionId: paymentId }, + }); + + if (!existing) { + console.warn(`DaimoPayWebhook: unknown session ${paymentId}; ignoring`); + return Response.json({ success: true }); + } + + if (existing.sessionStatus === SESSION_STATUS.SUCCESS) { + console.warn(`DaimoPayWebhook: session ${paymentId} already processed; skipping`); + return Response.json({ success: true }); + } + + // Atomically claim the session (compare-and-swap on its current status) so two + // concurrent deliveries can't both provision — only the one that flips it wins. + const claim = await prisma.paymentSession.updateMany({ + where: { sessionId: paymentId, sessionStatus: existing.sessionStatus }, + data: { sessionStatus: SESSION_STATUS.SUCCESS, receiptUrl }, + }); + + if (claim.count === 0) { + console.warn(`DaimoPayWebhook: session ${paymentId} already claimed concurrently; skipping`); + return Response.json({ success: true }); + } + + try { + await handleSuccessfulPayment({ ...existing, sessionStatus: SESSION_STATUS.SUCCESS, receiptUrl }); + } catch (error) { + // Provisioning failed after we claimed — revert so a Daimo retry can re-run it + // (avoids "paid but not provisioned"). NOTE: handleSuccessfulPayment is not yet + // idempotent, so a partial-then-failed provision could double-grant on retry — + // making it idempotent (a processed-payment guard) is the proper follow-up. + await prisma.paymentSession.update({ + where: { sessionId: paymentId }, + data: { sessionStatus: existing.sessionStatus, receiptUrl: existing.receiptUrl }, + }); + throw error; + } return Response.json({ success: true }); } else { diff --git a/src/app/api/notifications/route.ts b/src/app/api/notifications/route.ts index de0e09c..62146e4 100644 --- a/src/app/api/notifications/route.ts +++ b/src/app/api/notifications/route.ts @@ -36,6 +36,8 @@ export const GET = withAuthInfra(async (req: Request) => { response = await neynar.get(`/v2/farcaster/notifications/?fid=${targetFid}&priority_mode=${priority}&cursor=${cursor}`) } else if (mode === "mentions") { response = await neynar.get(`/v2/farcaster/notifications/?fid=${targetFid}&type=mentions,replies&priority_mode=${priority}&cursor=${cursor}`) + } else { + return Response.json({ "error": "Invalid mode (expected 'all' or 'mentions')" }, { status: 400 }) } return Response.json({ "unread": response.data.unseen_notifications_count, "notifications": response.data.notifications, "cursor": response.data.next.cursor }) diff --git a/src/app/api/og/route.tsx b/src/app/api/og/route.tsx index 11a6473..57c2c36 100644 --- a/src/app/api/og/route.tsx +++ b/src/app/api/og/route.tsx @@ -108,8 +108,8 @@ export async function GET(req: NextRequest) {
- {/* @ts-ignore */} - + {/* @ts-ignore — @vercel/og accepts an ArrayBuffer src at runtime */} +

Castora.

Build on Farcaster ツ

diff --git a/src/app/api/profile/search/route.ts b/src/app/api/profile/search/route.ts index 30c1301..b7b9168 100644 --- a/src/app/api/profile/search/route.ts +++ b/src/app/api/profile/search/route.ts @@ -21,7 +21,11 @@ export async function GET(req: Request) { const url = new URL(req.url) - const query = url.searchParams.get("query").replace("@", "") + const rawQuery = url.searchParams.get("query") + if (!rawQuery) { + return Response.json({ "users": [] }) + } + const query = rawQuery.replace("@", "") const response = await neynar.get(`/v2/farcaster/user/search/?viewer_fid=${targetFid}&q=${query}`) diff --git a/src/app/api/search-suggestions/route.ts b/src/app/api/search-suggestions/route.ts index cd433bf..5bbcee3 100644 --- a/src/app/api/search-suggestions/route.ts +++ b/src/app/api/search-suggestions/route.ts @@ -21,7 +21,11 @@ export async function GET(req: Request) { const url = new URL(req.url) - const query = url.searchParams.get("query").replace("@", "").replace("/", "") + const rawQuery = url.searchParams.get("query") + if (!rawQuery) { + return Response.json({ channels: [], users: [] }) + } + const query = rawQuery.replace("@", "").replace("/", "") const [channelResponse, profileResponse] = await Promise.all([ neynar.get(`/v2/farcaster/channel/search/?q=${query}`), diff --git a/src/app/api/url-preview/other/route.ts b/src/app/api/url-preview/other/route.ts index 20217c9..318ac47 100644 --- a/src/app/api/url-preview/other/route.ts +++ b/src/app/api/url-preview/other/route.ts @@ -1,7 +1,55 @@ import axios from 'axios'; import cheerio from 'cheerio'; +import dns from 'node:dns/promises'; +import net from 'node:net'; import { publicCacheHeaders } from '@/utils/cacheHeaders'; +// This endpoint fetches a USER-SUPPLIED URL, so it must not be usable to reach +// internal services (SSRF). Block private/reserved/link-local/metadata addresses +// (resolved, not just the literal) and disable redirect-following so a public host +// can't redirect to an internal one. +const isBlockedIp = (ip: string): boolean => { + const type = net.isIP(ip); + if (type === 4) { + const [a, b] = ip.split('.').map(Number); + if (a === 0 || a === 10 || a === 127) return true; // this-network, private, loopback + if (a === 169 && b === 254) return true; // link-local incl. cloud metadata 169.254.169.254 + if (a === 172 && b >= 16 && b <= 31) return true; // private + if (a === 192 && b === 168) return true; // private + if (a === 100 && b >= 64 && b <= 127) return true; // CGNAT + if (a >= 224) return true; // multicast + reserved + return false; + } + if (type === 6) { + const lower = ip.toLowerCase(); + if (lower === '::1' || lower === '::') return true; // loopback / unspecified + if (lower.startsWith('fe80')) return true; // link-local + if (lower.startsWith('fc') || lower.startsWith('fd')) return true; // unique local + if (lower.startsWith('::ffff:')) return isBlockedIp(lower.slice('::ffff:'.length)); // IPv4-mapped + return false; + } + return true; // not a valid IP -> block +}; + +const assertPublicUrl = async (targetUrl: URL): Promise => { + const host = targetUrl.hostname.replace(/^\[|\]$/g, ''); // strip IPv6 brackets + const lowerHost = host.toLowerCase(); + if (!host || lowerHost === 'localhost' || lowerHost.endsWith('.local') || lowerHost.endsWith('.internal')) { + throw new Error('Blocked host'); + } + if (net.isIP(host)) { + if (isBlockedIp(host)) throw new Error('Blocked IP'); + return; + } + // Resolve and reject if ANY address is internal. (Best-effort: there is a small + // TOCTOU window vs. the fetch's own resolution; maxRedirects:0 below closes the + // common redirect-to-internal bypass.) + const records = await dns.lookup(host, { all: true }); + if (records.length === 0 || records.some((r) => isBlockedIp(r.address))) { + throw new Error('Blocked resolved IP'); + } +}; + export async function GET(req: Request) { try { const url = new URL(req.url) @@ -18,9 +66,12 @@ export async function GET(req: Request) { return Response.json({ error: "Unsupported URL protocol" }, { status: 400 }) } + await assertPublicUrl(targetUrl) + const response = await axios.get(targetUrl.toString(), { headers: { 'User-Agent': 'CastoraBot/1.0 (https://castora.social)' }, maxContentLength: 2_000_000, + maxRedirects: 0, timeout: 5000, }); const html = response.data; diff --git a/src/app/changelog/entries.ts b/src/app/changelog/entries.ts index 5c6ee31..ef507bf 100644 --- a/src/app/changelog/entries.ts +++ b/src/app/changelog/entries.ts @@ -18,6 +18,15 @@ export type ChangelogEntry = { } export const changelog: ChangelogEntry[] = [ + { + date: '2026-06-22', + title: 'Fixes & hardening', + tag: 'fix', + items: [ + 'Fixed the preview image shown when a Castora cast is shared elsewhere — it was failing to render.', + 'Behind-the-scenes reliability and security work: sturdier handling of malformed requests, safer link previews, a hardened crypto-payment webhook, and a cleanup of unused code and dependencies.', + ], + }, { date: '2026-06-22', title: 'Multiple accounts are back', diff --git a/src/components/PwaInstallPrompt.tsx b/src/components/PwaInstallPrompt.tsx deleted file mode 100644 index 720ca15..0000000 --- a/src/components/PwaInstallPrompt.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import { useState, useEffect } from "react" - -export function InstallPrompt() { - const [isIOS, setIsIOS] = useState(false) - const [isStandalone, setIsStandalone] = useState(false) - - useEffect(() => { - setIsIOS( - /iPad|iPhone|iPod/.test(navigator.userAgent) && !(window as any).MSStream - ) - - setIsStandalone(window.matchMedia('(display-mode: standalone)').matches) - }, []) - - if (isStandalone) { - return null // Don't show install button if already installed - } - - return ( -
-

Install App

- - {isIOS && ( -

- To install this app on your iOS device, tap the share button - - {' '} - ⎋{' '} - - and then "Add to Home Screen" - - {' '} - ➕{' '} - . -

- )} -
- ) -} \ No newline at end of file diff --git a/src/components/banner.tsx b/src/components/banner.tsx deleted file mode 100644 index 50f6047..0000000 --- a/src/components/banner.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { XMarkIcon } from '@heroicons/react/20/solid' - -export default function Banner() { - return ( -
-

- Notifications performance is downgraded. In case of any issues, please contact us on{' '} - - telegram {'->'} - -

-
- ) -}