From bfdd7bc8af786f18a84a1cfa12fbf8c76f5597e5 Mon Sep 17 00:00:00 2001 From: dcccrypto Date: Wed, 2 Sep 2026 11:23:18 +0100 Subject: [PATCH 01/14] fix(#2243): give the footer social links real accessible names MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The four social links (GitHub / X / Discord / Telegram) carried `title` only. `title` is not a reliable accessible name — screen-reader support varies and it is invisible on keyboard focus — so each link announced as bare "link" and the decorative SVG glyph was exposed to the a11y tree. Each anchor now carries an explicit `aria-label`, and each glyph is `aria-hidden="true" focusable="false"`. Note the issue says "Header social icons"; they actually live in `components/layout/Footer.tsx`. The defect is exactly as described, only the file is different — `components/Header.tsx` does not exist. Test asserts one label per social plus a count invariant (labelled links <= aria-hidden glyphs), so adding a fifth link without hiding its glyph fails. Negative control run: stripping the attributes fails all 5. Launch suite: 3056 passed, 16 skipped, 0 failed. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01NgoNgagkvw7i5SSRC3FJ8D --- .../components/FooterSocialA11y.test.tsx | 28 +++++++++++++++++++ app/components/layout/Footer.tsx | 12 +++++--- 2 files changed, 36 insertions(+), 4 deletions(-) create mode 100644 app/__tests__/components/FooterSocialA11y.test.tsx diff --git a/app/__tests__/components/FooterSocialA11y.test.tsx b/app/__tests__/components/FooterSocialA11y.test.tsx new file mode 100644 index 00000000..9f61c3d5 --- /dev/null +++ b/app/__tests__/components/FooterSocialA11y.test.tsx @@ -0,0 +1,28 @@ +import { describe, expect, it } from "vitest"; +import fs from "node:fs"; +import path from "node:path"; + +// #2243 — the footer social icons carried `title` only. `title` is not a reliable +// accessible name (screen readers vary, and it is invisible on keyboard focus), so +// each link needs an explicit aria-label and each decorative glyph aria-hidden. +describe("#2243 footer social links have accessible names", () => { + const src = fs.readFileSync( + path.join(__dirname, "../../components/layout/Footer.tsx"), + "utf8", + ); + + const socials = ["GitHub", "X (Twitter)", "Discord", "Telegram"]; + + it.each(socials)("labels the %s link", (name) => { + expect(src).toContain(`aria-label="Percolator on ${name}"`); + }); + + it("hides every decorative social glyph from the a11y tree", () => { + // Count the social anchors by their aria-labels, then require at least as many + // aria-hidden svgs — so adding a link without hiding its glyph fails here. + const labelled = (src.match(/aria-label="Percolator on /g) ?? []).length; + const hidden = (src.match(/
= 0 ? "+" : ""}${r.toFixed(4)}%`} - className={`w-6 rounded-sm ${isPos ? "bg-green-500/60" : "bg-red-500/60"}`} + // #2368: semantic colours come from the design tokens, not Tailwind's + // palette — `bg-green-500`/`bg-red-500` are fixed sRGB and do not follow a + // theme change, so this sparkline drifted from every other long/short + // surface. + // + // Direction matches THIS FILE's own convention for the same quantity: a + // POSITIVE funding rate is rendered with --short (`:365` for + // eightHourRatePercent, `:298` for userPays), because positive funding + // means longs pay. The old green/red pair read the opposite way round, + // so the sparkline disagreed with the headline rate directly above it. + className={`w-6 rounded-sm ${isPos ? "bg-[var(--short)]/60" : "bg-[var(--long)]/60"}`} style={{ height: `${heightPct}%` }} /> ); From ce9c16e75665c7b99ff954d25966799e81265de7 Mon Sep 17 00:00:00 2001 From: dcccrypto Date: Wed, 2 Sep 2026 15:42:15 +0100 Subject: [PATCH 11/14] fix(#2341): say something when the distributed rate limiter is not configured MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The rate limiter's primary path is Upstash Redis (GH#1213), with an in-memory sliding window as a documented fallback for local dev and CI. On Vercel that fallback is per-isolate, so every cold start gets its own budget and an attacker spreading requests across instances bypasses the limit entirely — which is the issue's premise, and it is correct for any deployment that reaches it. The init-FAILURE branch already logged an error in production. MISSING env vars returned quietly. That is backwards: a Redis client that throws on construction is rare, while a deployment that simply never had UPSTASH_REDIS_REST_URL and UPSTASH_REDIS_REST_TOKEN set is the ordinary way to end up on the fallback — a fresh environment, or a preview promoted to production without the vars. The likely case was the silent one. Now both paths log in production. Logged rather than thrown deliberately: middleware runs on every request, so failing closed here would take the whole site down over a rate limiter. Trading availability for enforcement is the right call, and it is precisely why the degradation must not be silent — the operator has to be able to find out. This does not make the fallback distributed. It makes its absence visible, which is the part that can be fixed in code; configuring Upstash in production is an ops action and the issue stays open for it. Launch suite: 3094 passed / 16 skipped / 0 failed. Refs: dcccrypto/percolator-launch#2341 Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01NgoNgagkvw7i5SSRC3FJ8D --- app/middleware.ts | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/app/middleware.ts b/app/middleware.ts index 499b98bc..18772fe0 100644 --- a/app/middleware.ts +++ b/app/middleware.ts @@ -50,7 +50,29 @@ function getUpstashLimiters(): { general: Ratelimit | null; rpc: Ratelimit | nul const url = process.env.UPSTASH_REDIS_REST_URL; const token = process.env.UPSTASH_REDIS_REST_TOKEN; - if (!url || !token) return { general: null, rpc: null }; + if (!url || !token) { + // GH#2341: unconfigured Upstash silently degrades to the per-instance + // in-memory limiter below, which on Vercel is not really a rate limit — + // every cold start gets its own budget, so an attacker spreading requests + // across instances bypasses it entirely. + // + // The init-FAILURE branch already logged in production; MISSING env vars + // returned quietly, which is the more likely way to end up here (a fresh + // deployment, or a preview promoted to prod without the vars set). So the + // case that actually happens was the one that said nothing. + // + // Logged rather than thrown: middleware runs on every request, and failing + // closed here would take the whole site down over a rate limiter. That + // trade is deliberate, and is exactly why it has to be loud. + if (process.env.NODE_ENV === "production") { + console.error( + "[RateLimit] ERROR: UPSTASH_REDIS_REST_URL / UPSTASH_REDIS_REST_TOKEN are unset in " + + "production — falling back to the PER-INSTANCE in-memory limiter. Distributed rate " + + "limiting is NOT in effect and limits can be bypassed across serverless instances.", + ); + } + return { general: null, rpc: null }; + } try { const redis = new Redis({ url, token }); From 24398f5eb038efd68ffd984500a68acf76b9e667 Mon Sep 17 00:00:00 2001 From: dcccrypto Date: Wed, 2 Sep 2026 15:45:22 +0100 Subject: [PATCH 12/14] fix(#2220): next 16.2.9 -> 16.2.11, clearing the direct high-severity advisory MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #2220 reported next@16.2.3 against advisories fixed in >=16.2.5 / >=16.2.6. The dependency has since moved to 16.2.9, so those specific advisories are behind us — but the issue is NOT stale: newer 16.x advisories landed with a fix version of >=16.2.11, so `next` was still the one DIRECT dependency failing a production high-severity audit. This matters more here than a version number usually does, for the reason the issue gives: this app puts security-relevant controls in Next middleware — host gating, blocklisted markets, admin routes, rate limiting, security headers — and several of the outstanding advisories are middleware/proxy bypasses. A bypass in that layer is a bypass of those controls. Verified after the bump: typecheck clean, suite 3094 passed / 16 skipped / 0 failed, and `next build` completes. The transitive high-severity findings the issue also lists (nanoid, postcss, browserslist, undici, image-size, sharp, socket.io-parser, fast-uri, ip-address) are NOT addressed here. They arrive through @privy-io/react-auth and the build toolchain, so they need either an upstream release or a pnpm override, and an override that forces a version a dependency was not tested against can break the wallet path. Left for a deliberate pass rather than bundled into a security bump that is currently verifiable end to end. Refs: dcccrypto/percolator-launch#2220 Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01NgoNgagkvw7i5SSRC3FJ8D --- app/package.json | 2 +- pnpm-lock.yaml | 116 +++++++++++++++++++++-------------------------- 2 files changed, 53 insertions(+), 65 deletions(-) diff --git a/app/package.json b/app/package.json index bed5fc6c..910d5d96 100644 --- a/app/package.json +++ b/app/package.json @@ -34,7 +34,7 @@ "buffer": "^6.0.3", "gsap": "^3.14.2", "lightweight-charts": "^5.2.0", - "next": "16.2.9", + "next": "16.2.11", "postgres": "^3.4.9", "react": "18.3.1", "react-dom": "18.3.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9b4b5504..ecb51142 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -91,7 +91,7 @@ importers: version: 3.14.1(6ba7a9d9e0172521f33b8d058dae8da3) '@sentry/nextjs': specifier: 10.39.0 - version: 10.39.0(@opentelemetry/context-async-hooks@2.5.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.5.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.5.1(@opentelemetry/api@1.9.0))(next@16.2.9(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)(webpack@5.105.2(esbuild@0.27.3)) + version: 10.39.0(@opentelemetry/context-async-hooks@2.5.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.5.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.5.1(@opentelemetry/api@1.9.0))(next@16.2.11(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)(webpack@5.105.2(esbuild@0.27.3)) '@solana-program/memo': specifier: ^0.11.0 version: 0.11.0(@solana/kit@6.1.0(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@5.0.10)) @@ -127,7 +127,7 @@ importers: version: 1.36.4 '@vercel/analytics': specifier: ^2.0.1 - version: 2.0.1(next@16.2.9(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) + version: 2.0.1(next@16.2.11(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) '@vercel/blob': specifier: ^2.5.0 version: 2.6.1 @@ -144,8 +144,8 @@ importers: specifier: ^5.2.0 version: 5.2.0 next: - specifier: 16.2.9 - version: 16.2.9(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + specifier: 16.2.11 + version: 16.2.11(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) postgres: specifier: ^3.4.9 version: 3.4.9 @@ -1232,60 +1232,60 @@ packages: '@emnapi/core': ^1.7.1 '@emnapi/runtime': ^1.7.1 - '@next/env@16.2.9': - resolution: {integrity: sha512-ki5VxxXfzD/9TDe13wyeTKIjQTAwBVpnr8KhRDUr8ltMUq1/NBpWNT5tiPoxiGl+PHM4X2ahSOiPk6iAimIzPg==} + '@next/env@16.2.11': + resolution: {integrity: sha512-0do5A3BJ2gxWr0ZCMcD6BhW+e595jyxdTl3rXTS6lOtD8ektMiW6CO+EPwt1Eca1DBnm90r/7GdiKWBKxH++DA==} '@next/eslint-plugin-next@16.2.9': resolution: {integrity: sha512-UZi8+YT/MLgTC9nrrn2Xd4lBYv1B7lVmtWHfPcthAI5Tt/C1LuDe6DfmtCtJ+WQod3ksY4VrKSvk3oMVAnL7qw==} - '@next/swc-darwin-arm64@16.2.9': - resolution: {integrity: sha512-HkfxNYUCmcct0Xsqib5KxqMSHV4AHJq857BNRchyBDs4YS19aHzVfn1kDuBYKqLLQBjXgnkIsjV2Kd4d2wzYhw==} + '@next/swc-darwin-arm64@16.2.11': + resolution: {integrity: sha512-wryL4pjKmDwGv2ox6+GZDFxvmtSRLqApBR8kL1j4+vhB7Z5vJC/zAnXpiR9Xkfzl0AS8WLMnsuGV/UKI67/rrw==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] - '@next/swc-darwin-x64@16.2.9': - resolution: {integrity: sha512-7IAtK4MeybpqRV9GRABWEhJ62mOS+rzWOzOTFie4cSEtm12xsoOMJRcECoZx3FHPzFAqN/IJtHqWAFOLfl152w==} + '@next/swc-darwin-x64@16.2.11': + resolution: {integrity: sha512-aZl2j4f/fLyjQvOhv0Oe9UaMAQHolYpKhctsoYzplSumKJKPUmgjcf6545aBtysLTcu994TREd0+pSgNE4ohmg==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] - '@next/swc-linux-arm64-gnu@16.2.9': - resolution: {integrity: sha512-hBD75iWpUtkL9SmQmcRhmLomn9jgkPzCEkbOcLgHymPEKzv+6ONy13RRiIEz/iEObjkS2Jlb5gYS2XGoS3X4rw==} + '@next/swc-linux-arm64-gnu@16.2.11': + resolution: {integrity: sha512-5jEriyEnH/LWFy27L2ZG0XaLlyEJIjhsImEsiS9P563PKEVp2BVups/xfOucIrsvVntp11oNcZwjHvaDPYVB5g==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] libc: [glibc] - '@next/swc-linux-arm64-musl@16.2.9': - resolution: {integrity: sha512-qZTI3pf9SGc/obr8NkQAekBxmp1QK+kVm+VAf3BALLfFAj+1kUhkTxmrWpVos9R/UYIA8AWX2p6cGI5WdwzVUA==} + '@next/swc-linux-arm64-musl@16.2.11': + resolution: {integrity: sha512-eIjcpx2fnnFSSkZDbTxy74KnokUXDjfoLClpWelfgHLf621aTqswhwXQ7GkD5K5rplrS6LZ/Bj+mVuvzluBOEg==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] libc: [musl] - '@next/swc-linux-x64-gnu@16.2.9': - resolution: {integrity: sha512-xm0HfRNX+UkH4R3c18ynswjj5o5uEj/7iI9p9omdtTSIsRCzQqkGMA+10nzJ4EHnYC3as65IMhbbl5fWRUWHYg==} + '@next/swc-linux-x64-gnu@16.2.11': + resolution: {integrity: sha512-8WgzpaWMs46qJT9kiV47cje86L0x/Mu9t8/Gwj+pnbgW3rETVfCnaScPjlYUwNScpOozdcIMHWmAvuZJUonR2w==} engines: {node: '>= 10'} cpu: [x64] os: [linux] libc: [glibc] - '@next/swc-linux-x64-musl@16.2.9': - resolution: {integrity: sha512-QumimHkGEG6vM3PfEDWKyKen03NcqLOkeKB1EfcPe7VxzmEiCa4jNnMyBn/US5zcd/VE1CI+O8Ovb3lfjVHfGw==} + '@next/swc-linux-x64-musl@16.2.11': + resolution: {integrity: sha512-I3UgPds7G4ZYnTb/H+5GBGuUT2DhAk6j0mL6A4s63RjFs74wB2hOWP0vaxsK+3NJraExt3eYEPQ/UtT0x/64Nw==} engines: {node: '>= 10'} cpu: [x64] os: [linux] libc: [musl] - '@next/swc-win32-arm64-msvc@16.2.9': - resolution: {integrity: sha512-hzQpKZvw8rAwI6A2uQh6SacCSvNAXaIkPNsWwzqqfRiIMiXMfH936skDhz1OO6KpvdKkJrgHHtqQOq5PIXOvdQ==} + '@next/swc-win32-arm64-msvc@16.2.11': + resolution: {integrity: sha512-n89CjtcThnjrwgJMAiI5xbqwLY51zvwC9tSlArmVndAJLYVl9T9UAdlkXTmZvE++idoXe8KdglQlhNRdUp1c6g==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] - '@next/swc-win32-x64-msvc@16.2.9': - resolution: {integrity: sha512-qr2VL3Ce5QrwgO2yh1ujSBawrimjVKX8FGF/cOynmdYKJY0BdHpGVNIRK1tqONB10Vkm25Ub1BD2bkjWs4+96w==} + '@next/swc-win32-x64-msvc@16.2.11': + resolution: {integrity: sha512-md8CLNggS1Dx9pUgApzps5uAf+N8GN9xywzmNx9vHAWo94HtBwCCqkSnhIrdfQe83Dhz8Lfo/20Nb1Zxal092w==} engines: {node: '>= 10'} cpu: [x64] os: [win32] @@ -5119,11 +5119,6 @@ packages: resolution: {integrity: sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==} engines: {node: '>=6.0.0'} - baseline-browser-mapping@2.10.20: - resolution: {integrity: sha512-1AaXxEPfXT+GvTBJFuy4yXVHWJBXa4OdbIebGN/wX5DlsIkU0+wzGnd2lOzokSk51d5LUmqjgBLRLlypLUqInQ==} - engines: {node: '>=6.0.0'} - hasBin: true - baseline-browser-mapping@2.10.44: resolution: {integrity: sha512-T3ghW+sl/ZJ8w1v/yQx3qvJ9040DWoLBz8JT/CILbAKcFyG9b2MRe75v6W5uXjv6uH1lumK2Kv46y2zSkcej0Q==} engines: {node: '>=6.0.0'} @@ -5293,9 +5288,6 @@ packages: caniuse-lite@1.0.30001769: resolution: {integrity: sha512-BCfFL1sHijQlBGWBMuJyhZUhzo7wer5sVj9hqekB/7xn0Ypy+pER/edCYQm4exbXj4WiySGp40P8UuTh6w1srg==} - caniuse-lite@1.0.30001788: - resolution: {integrity: sha512-6q8HFp+lOQtcf7wBK+uEenxymVWkGKkjFpCvw5W25cmMwEDU45p1xQFBQv8JDlMMry7eNxyBaR+qxgmTUZkIRQ==} - caniuse-lite@1.0.30001806: resolution: {integrity: sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw==} @@ -7319,8 +7311,8 @@ packages: neo-async@2.6.2: resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} - next@16.2.9: - resolution: {integrity: sha512-MEOJiq/UvuezAdqVSceHbqDgZt1kDw2tpGVOlsdIoJsQdbN2JY2hpVG4xnXGkbdJUOEWhnRfiu/O4Hpc9Juwww==} + next@16.2.11: + resolution: {integrity: sha512-B339zaqbyK8cmxhoAvLrcwoabwCP1wz21zSzfqxqXAemTu2BXnH7tQnfcglKv1vnMUIDBc+Hth7XODQriTZiRQ==} engines: {node: '>=20.9.0'} hasBin: true peerDependencies: @@ -10167,7 +10159,7 @@ snapshots: '@img/sharp-wasm32@0.34.5': dependencies: - '@emnapi/runtime': 1.8.1 + '@emnapi/runtime': 1.11.1 optional: true '@img/sharp-win32-arm64@0.34.5': @@ -10564,34 +10556,34 @@ snapshots: '@tybys/wasm-util': 0.10.3 optional: true - '@next/env@16.2.9': {} + '@next/env@16.2.11': {} '@next/eslint-plugin-next@16.2.9': dependencies: fast-glob: 3.3.1 - '@next/swc-darwin-arm64@16.2.9': + '@next/swc-darwin-arm64@16.2.11': optional: true - '@next/swc-darwin-x64@16.2.9': + '@next/swc-darwin-x64@16.2.11': optional: true - '@next/swc-linux-arm64-gnu@16.2.9': + '@next/swc-linux-arm64-gnu@16.2.11': optional: true - '@next/swc-linux-arm64-musl@16.2.9': + '@next/swc-linux-arm64-musl@16.2.11': optional: true - '@next/swc-linux-x64-gnu@16.2.9': + '@next/swc-linux-x64-gnu@16.2.11': optional: true - '@next/swc-linux-x64-musl@16.2.9': + '@next/swc-linux-x64-musl@16.2.11': optional: true - '@next/swc-win32-arm64-msvc@16.2.9': + '@next/swc-win32-arm64-msvc@16.2.11': optional: true - '@next/swc-win32-x64-msvc@16.2.9': + '@next/swc-win32-x64-msvc@16.2.11': optional: true '@ngraveio/bc-ur@1.1.13': @@ -12339,7 +12331,7 @@ snapshots: '@sentry/core@10.39.0': {} - '@sentry/nextjs@10.39.0(@opentelemetry/context-async-hooks@2.5.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.5.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.5.1(@opentelemetry/api@1.9.0))(next@16.2.9(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)(webpack@5.105.2(esbuild@0.27.3))': + '@sentry/nextjs@10.39.0(@opentelemetry/context-async-hooks@2.5.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.5.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.5.1(@opentelemetry/api@1.9.0))(next@16.2.11(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)(webpack@5.105.2(esbuild@0.27.3))': dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/semantic-conventions': 1.39.0 @@ -12352,7 +12344,7 @@ snapshots: '@sentry/react': 10.39.0(react@18.3.1) '@sentry/vercel-edge': 10.39.0 '@sentry/webpack-plugin': 4.9.1(webpack@5.105.2(esbuild@0.27.3)) - next: 16.2.9(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + next: 16.2.11(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) rollup: 4.59.0 stacktrace-parser: 0.1.11 transitivePeerDependencies: @@ -15316,9 +15308,9 @@ snapshots: dependencies: uncrypto: 0.1.3 - '@vercel/analytics@2.0.1(next@16.2.9(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)': + '@vercel/analytics@2.0.1(next@16.2.11(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)': optionalDependencies: - next: 16.2.9(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + next: 16.2.11(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 '@vercel/blob@2.6.1': @@ -17302,8 +17294,6 @@ snapshots: base64url@3.0.1: {} - baseline-browser-mapping@2.10.20: {} - baseline-browser-mapping@2.10.44: {} baseline-browser-mapping@2.9.19: {} @@ -17496,8 +17486,6 @@ snapshots: caniuse-lite@1.0.30001769: {} - caniuse-lite@1.0.30001788: {} - caniuse-lite@1.0.30001806: {} canonicalize@2.1.0: {} @@ -19858,25 +19846,25 @@ snapshots: neo-async@2.6.2: {} - next@16.2.9(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + next@16.2.11(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: - '@next/env': 16.2.9 + '@next/env': 16.2.11 '@swc/helpers': 0.5.15 - baseline-browser-mapping: 2.10.20 - caniuse-lite: 1.0.30001788 + baseline-browser-mapping: 2.10.44 + caniuse-lite: 1.0.30001806 postcss: 8.4.31 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) styled-jsx: 5.1.6(@babel/core@7.29.0)(react@18.3.1) optionalDependencies: - '@next/swc-darwin-arm64': 16.2.9 - '@next/swc-darwin-x64': 16.2.9 - '@next/swc-linux-arm64-gnu': 16.2.9 - '@next/swc-linux-arm64-musl': 16.2.9 - '@next/swc-linux-x64-gnu': 16.2.9 - '@next/swc-linux-x64-musl': 16.2.9 - '@next/swc-win32-arm64-msvc': 16.2.9 - '@next/swc-win32-x64-msvc': 16.2.9 + '@next/swc-darwin-arm64': 16.2.11 + '@next/swc-darwin-x64': 16.2.11 + '@next/swc-linux-arm64-gnu': 16.2.11 + '@next/swc-linux-arm64-musl': 16.2.11 + '@next/swc-linux-x64-gnu': 16.2.11 + '@next/swc-linux-x64-musl': 16.2.11 + '@next/swc-win32-arm64-msvc': 16.2.11 + '@next/swc-win32-x64-msvc': 16.2.11 '@opentelemetry/api': 1.9.0 '@playwright/test': 1.58.2 sharp: 0.34.5 @@ -20343,7 +20331,7 @@ snapshots: postcss@8.4.31: dependencies: - nanoid: 3.3.11 + nanoid: 3.3.16 picocolors: 1.1.1 source-map-js: 1.2.1 @@ -20955,7 +20943,7 @@ snapshots: dependencies: '@img/colour': 1.0.0 detect-libc: 2.1.2 - semver: 7.7.4 + semver: 7.8.5 optionalDependencies: '@img/sharp-darwin-arm64': 0.34.5 '@img/sharp-darwin-x64': 0.34.5 From a0a731ae28c44a2340be4ed6f7913e8dcfa79005 Mon Sep 17 00:00:00 2001 From: dcccrypto Date: Wed, 2 Sep 2026 16:55:04 +0100 Subject: [PATCH 13/14] fix(#2525): correct the WS_AUTH_REQUIRED default in docs and the comment citing it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The #2525 half of an earlier combined commit, re-landed on its own. Its #2514 half is dropped — #2518 already fixed that on playground, deliberately choosing to stay non-fatal and surface the failure on the success screen. See the issue for the one thing the two approaches do differently. SECURITY.md:35 and README.md:451 both stated WS_AUTH_REQUIRED defaults to `false`. The server that implements it does not: percolator-api/src/routes/ws.ts:52-56 const WS_AUTH_REQUIRED = process.env.WS_AUTH_REQUIRED !== undefined ? process.env.WS_AUTH_REQUIRED === "true" : IS_PRODUCTION; So the default is environment-dependent — REQUIRED in production, optional otherwise — with fail-closed startup checks either side of it. That means the issue's stated concern ("if deployed on the SECURITY.md default, the price feed is public") does not hold on a production API. The real hazard ran the other way: a reader trusting the docs would believe production was open when it is not, and might "fix" it by setting something explicitly. priceStore.ts carried the same wrong figure in a SECURITY REVIEW comment, which is how a docs error becomes a code error — the next reader treats the comment as the specification and does not check. Corrected there too, with the live consequence stated the right way round: on a production API this client does not get an open feed, it fails to authenticate and silently falls back to REST. The genuine open question is preserved rather than closed over: whether this client should carry an HMAC token so it keeps the WS path in production instead of degrading to REST. Refs: dcccrypto/percolator-launch#2525 Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01NgoNgagkvw7i5SSRC3FJ8D --- README.md | 2 +- SECURITY.md | 11 +++++++++-- app/lib/priceStore/priceStore.ts | 19 +++++++++++++------ 3 files changed, 23 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 4bb16888..f99ab90c 100644 --- a/README.md +++ b/README.md @@ -448,7 +448,7 @@ All backend services share a single `.env` file (root or per-package). Copy `.en | `API_AUTH_KEY` | — | API key for protected endpoints | | `CORS_ORIGINS` | `http://localhost:3000` | Comma-separated allowed origins (required in production) | | `WS_AUTH_SECRET` | — | HMAC secret for WebSocket token auth | -| `WS_AUTH_REQUIRED` | `false` | Require WS auth tokens | +| `WS_AUTH_REQUIRED` | *(prod: `true`, else `false`)* | Require WS auth tokens. Default follows `NODE_ENV`; set explicitly to override. | | `MAX_WS_CONNECTIONS` | `1000` | Global WebSocket connection limit | ### Keeper Service diff --git a/SECURITY.md b/SECURITY.md index d6f3e052..9415efe6 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -32,7 +32,12 @@ WebSocket connections support optional authentication: ### Configuration -- `WS_AUTH_REQUIRED=true` — Require authentication (default: false) +- `WS_AUTH_REQUIRED` — Require authentication. **The default is environment-dependent, not `false`:** + required when `NODE_ENV=production`, optional otherwise. Setting the variable to + `true` or `false` overrides that in either direction. + (Implemented in `percolator-api/src/routes/ws.ts:52-56`; this repo only documents it.) + Startup fails closed: production without `WS_AUTH_SECRET` exits, and so does + `WS_AUTH_REQUIRED=true` without a secret. - `WS_AUTH_SECRET` — Secret key for HMAC tokens (change in production!) ### Authentication Methods @@ -164,7 +169,9 @@ Rate limit violations are: CORS_ORIGINS=http://localhost:3000,http://localhost:3001 ``` -2. **Keep auth disabled** for easier testing +2. **Auth is already off in development** — the default outside + `NODE_ENV=production` is optional, so no setting is needed. Set it explicitly + only to override: ```bash WS_AUTH_REQUIRED=false ``` diff --git a/app/lib/priceStore/priceStore.ts b/app/lib/priceStore/priceStore.ts index 8151278d..2bb9be04 100644 --- a/app/lib/priceStore/priceStore.ts +++ b/app/lib/priceStore/priceStore.ts @@ -67,12 +67,19 @@ export const EMPTY_PRICE_STATE: PriceState = Object.freeze({ /* ── WS URL resolution — ported verbatim from the pre-refactor useLivePrice.ts ── * SECURITY REVIEW (mainnet, carried over): the WS price feed sends no auth - * token. If WS_AUTH_REQUIRED=false server-side (current default per - * SECURITY.md), any caller can open unauthenticated connections and - * enumerate active markets; per-IP connection limits are the only defense. - * If WS_AUTH_REQUIRED=true, this client fails to authenticate and silently - * falls back to REST seeding — decide before mainnet whether the feed is - * intentionally public or needs HMAC tokens matching SECURITY.md. */ + * token. GH#2525: this used to cite "WS_AUTH_REQUIRED=false, the current + * default per SECURITY.md", which was wrong in the direction that matters — + * SECURITY.md said `false` flatly, but the server that implements it + * (percolator-api/src/routes/ws.ts:52-56) defaults to REQUIRED whenever + * NODE_ENV=production and only optional otherwise. Docs corrected. + * + * So the live consequence is the opposite of what was written here: on a + * production API this client does NOT get an open feed, it fails to + * authenticate and silently falls back to REST seeding. Off production, the + * feed is open and per-IP connection limits are the only defense. + * + * Still to decide before mainnet: whether this client should carry an HMAC + * token so it keeps the WS path in production instead of degrading to REST. */ function getWsUrl(): string { const explicit = process.env.NEXT_PUBLIC_WS_URL; if (explicit !== undefined) return explicit; From 59525f101b767739c2e233d2ec4f0579d981a996 Mon Sep 17 00:00:00 2001 From: dcccrypto Date: Wed, 2 Sep 2026 16:57:11 +0100 Subject: [PATCH 14/14] fix(#2513): scope the new trader-stats aggregation to the network too MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #2512 (GH#2510) replaced the 10k-row cap with a database aggregation, which is the better fix — but the new query landed without a network predicate: FROM trades WHERE trader = ${wallet} so a wallet's stats summed devnet AND mainnet trades into one answer. Every field it returns was affected: totalTrades, totalVolume, totalFees, uniqueMarkets, and both timestamps. Found by the guard added for #2513 rather than by reading the diff. That test scans indexer-db.ts for `FROM trades` / `FROM funding_history` without a following network predicate, and it named the line: expected [ 'line 526: FROM trades' ] to deeply equal [] which is exactly the job it was written for. #2513 patched 12 sites; this is the 13th, added after that sweep by an unrelated change. The row-fetch path directly below it (:565) already had the predicate, so the two sibling queries disagreed. Worth noting for anyone reviewing similar work: the aggregation is a strict improvement over the cap it replaced, and the missing filter is not an argument against it. It is an argument for the guard — a cross-network sum is invisible on a single-network deployment and silently wrong on a dual one, and no amount of careful reading reliably catches a missing WHERE clause. Launch suite: 3111 passed / 16 skipped / 0 failed. Refs: dcccrypto/percolator-launch#2513, dcccrypto/percolator-launch#2510 Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01NgoNgagkvw7i5SSRC3FJ8D --- app/lib/indexer-db.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/app/lib/indexer-db.ts b/app/lib/indexer-db.ts index d7855065..feb2d7cc 100644 --- a/app/lib/indexer-db.ts +++ b/app/lib/indexer-db.ts @@ -525,6 +525,7 @@ export async function queryTraderStatsAggregate( max(created_at) AS last_trade_at FROM trades WHERE trader = ${wallet} + AND network = ${getServerNetwork()} `; const r = rows[0]; const iso = (d: Date | null) =>