From 580194e009bf356266f64c20fb47ce51789d66ee Mon Sep 17 00:00:00 2001 From: dcccrypto Date: Wed, 2 Sep 2026 11:23:18 +0100 Subject: [PATCH 01/15] 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 000000000..9f61c3d5a --- /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 c524010207aa9976bdbd136cb49d476dae3fffef Mon Sep 17 00:00:00 2001 From: dcccrypto Date: Wed, 2 Sep 2026 15:12:59 +0100 Subject: [PATCH 13/15] fix(#2514,#2525): verify backing seeds landed; correct WS_AUTH_REQUIRED default in docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #2514 — sequential launch could report "Market created!" with neither backing domain seeded. The Step-3 TopUpBackingBucket transaction was caught, warned to console, and fell through to success. Nothing ever read the market account to establish that domains 0 and 1 were funded — yet market-params.ts declares both mandatory at 100% of LP collateral each. Affects the sequential path only (retry/resume with startStep <= 3, or the pre-broadcast fallback from fresh batching); the batched M3a path bundles the deposit and both top-ups atomically and is unaffected. Fixed by asserting the OUTCOME, which is exactly the treatment the insurance seed already gets twice in this same file (useCreateMarket.ts:1424, :3034) and for the same stated reason: reading the engine's own state catches never-built, reverted and partially-applied alike — including the half-failure where one domain landed and the other did not, which no transaction result can express. The assertion is on bucket STATUS + nonzero backing, deliberately not on amount: the bucket stores a u128 BackingNum, not collateral atoms, so comparing against backingSeedPerDomain() would be a units mismatch. That is the same class of error as the "insurance already topped up" miscount recorded in this file, which compared a vault balance that also held these very backing seeds against an insurance target and concluded insurance had landed when it had not. An unreadable slab does NOT fail the launch: unlike the insurance seed, an unseeded backing domain is recoverable (TopUpBackingBucket is replayable by the backing authority, ExpireBackingBucket is permissionless), so failing on a transient RPC error would strand a live market for no gain. Logic extracted to findUnseededBackingDomains() so it is testable without driving the wizard. Negative control: removing the guard fails 4 of the 8 new tests. #2525 item 2 — the documented default was wrong, in the unsafe direction. SECURITY.md:35 and README.md:451 both said WS_AUTH_REQUIRED defaults to `false`. The server that actually implements it (percolator-api/src/routes/ws.ts:52-56) defaults to `IS_PRODUCTION` — required in production, optional otherwise, with fail-closed startup checks. So the issue's premise ("if deployed on the SECURITY.md default, the price feed is public") does not hold on a production API; the real hazard was a reader trusting the docs and believing prod was open when it is not. Corrected in SECURITY.md, README.md, and the priceStore comment that cited it. #2525 item 3 was already fixed by #2381 (assertCanonicalMatcher), more strictly than the issue asked: it pins the canonical matcher rather than accepting any allowlisted program. Item 1 needs a design decision and stays open. Launch suite: 3094 passed / 16 skipped / 0 failed. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01NgoNgagkvw7i5SSRC3FJ8D --- README.md | 2 +- SECURITY.md | 11 ++- ...t-params.backing-seed-verification.test.ts | 90 +++++++++++++++++++ app/hooks/useCreateMarket.ts | 57 +++++++++++- app/lib/market-params.ts | 47 ++++++++++ app/lib/priceStore/priceStore.ts | 19 ++-- 6 files changed, 216 insertions(+), 10 deletions(-) create mode 100644 app/__tests__/lib/market-params.backing-seed-verification.test.ts diff --git a/README.md b/README.md index 4bb16888b..f99ab90c4 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 d6f3e0523..9415efe6a 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/__tests__/lib/market-params.backing-seed-verification.test.ts b/app/__tests__/lib/market-params.backing-seed-verification.test.ts new file mode 100644 index 000000000..338e690a7 --- /dev/null +++ b/app/__tests__/lib/market-params.backing-seed-verification.test.ts @@ -0,0 +1,90 @@ +import { describe, expect, it } from 'vitest'; +import { + findUnseededBackingDomains, + LAUNCH_BACKING_DOMAINS, + type BackingBucketLike, +} from '@/lib/market-params'; + +/** + * GH#2514 — the sequential launch path reported `Market created!` even when the + * transaction carrying BOTH required backing top-ups failed. It caught the + * error, warned to the console, and continued without ever reading the market + * account to establish that domains 0 and 1 were seeded. + * + * These are the outcome cases the launch must reject. Deleting the status / + * nonzero-backing test inside findUnseededBackingDomains makes every "rejects" + * case below fail, which is the negative control for the fix. + */ + +// Fresh(1) with real backing — what a landed TopUpBackingBucket produces. +const seeded = (domain: number): BackingBucketLike => ({ + domain, + status: 1, + statusName: 'Fresh', + freshUnlienedBackingNum: 1_000_000n, +}); + +const withStatus = ( + domain: number, + status: number, + statusName: string, + num = 1_000_000n, +): BackingBucketLike => ({ domain, status, statusName, freshUnlienedBackingNum: num }); + +describe('findUnseededBackingDomains (GH#2514)', () => { + it('accepts a launch where both domains landed', () => { + expect(findUnseededBackingDomains([seeded(0), seeded(1)])).toEqual([]); + }); + + it('rejects the reported bug: the whole top-up transaction failed', () => { + // Neither instruction landed, so neither bucket ever left Empty. + const missing = findUnseededBackingDomains([ + withStatus(0, 0, 'Empty', 0n), + withStatus(1, 0, 'Empty', 0n), + ]); + expect(missing).toHaveLength(2); + expect(missing[0]).toContain('domain 0 (long)'); + expect(missing[1]).toContain('domain 1 (short)'); + }); + + it('rejects a HALF failure, which no transaction result can distinguish', () => { + // The single tx carries both top-ups, so this needs partial application — + // but the point is that reading the outcome catches it and a caught throw + // does not. + const missing = findUnseededBackingDomains([seeded(0), withStatus(1, 0, 'Empty', 0n)]); + expect(missing).toEqual(['domain 1 (short): Empty']); + }); + + it('rejects an absent bucket (slab too short / domain never addressable)', () => { + expect(findUnseededBackingDomains([seeded(0)])).toEqual(['domain 1 (short): absent']); + expect(findUnseededBackingDomains([])).toHaveLength(2); + }); + + it('rejects Expired and Impaired — neither is a usable seed', () => { + expect(findUnseededBackingDomains([withStatus(0, 2, 'Expired'), seeded(1)])) + .toEqual(['domain 0 (long): Expired']); + expect(findUnseededBackingDomains([seeded(0), withStatus(1, 3, 'Impaired')])) + .toEqual(['domain 1 (short): Impaired']); + }); + + it('rejects Fresh-but-zero: status alone is not proof of a seed', () => { + expect(findUnseededBackingDomains([seeded(0), withStatus(1, 1, 'Fresh', 0n)])) + .toEqual(['domain 1 (short): Fresh']); + }); + + it('ignores domains beyond the two the launch seeds', () => { + // A multi-asset market has domains 2..2n; the launch only funds asset 0. + expect( + findUnseededBackingDomains([ + seeded(0), + seeded(1), + withStatus(2, 0, 'Empty', 0n), + withStatus(3, 0, 'Empty', 0n), + ]), + ).toEqual([]); + }); + + it('checks exactly the asset-0 long/short pair', () => { + expect(LAUNCH_BACKING_DOMAINS).toEqual([0, 1]); + }); +}); diff --git a/app/hooks/useCreateMarket.ts b/app/hooks/useCreateMarket.ts index 30ab9327a..9cb5a02a6 100644 --- a/app/hooks/useCreateMarket.ts +++ b/app/hooks/useCreateMarket.ts @@ -70,9 +70,10 @@ import { // resending it — see the Step 3 block below. parsePortfolioV17, parseMarketGroupV17OI, + parseBackingBucketsV17, } from "@percolatorct/sdk"; import { PERCOLATOR_NFT_PROGRAM_ID } from "@/lib/nft-program"; -import { deriveMarketParams, MIN_LEVERAGE_X, backingSeedPerDomain, leverageFromMarginBps } from "@/lib/market-params"; +import { deriveMarketParams, MIN_LEVERAGE_X, backingSeedPerDomain, leverageFromMarginBps, findUnseededBackingDomains } from "@/lib/market-params"; // v17: SetOracleAuthority (tag 17), PushOraclePrice (tag 16), SetOraclePriceCap (tag 16), // and UpdateConfig (tag 14) do not exist in v17. All oracle + risk params are embedded // in InitMarket (extended tail). The sdk-compat stubs throw at runtime if called. @@ -2869,6 +2870,7 @@ export function useCreateMarket() { // must not strand an otherwise-successful market creation — a retry of // this step (or a later maintainer backfill) is safe because a repeat // TopUp against an already-Fresh-at-MAX bucket hits the harmless no-op arm. + let backingSeedError: unknown = null; if (isV17SlabDeposit) { try { const backingVaultToken = vaultTokenAta; @@ -2897,6 +2899,7 @@ export function useCreateMarket() { }); setState((s) => ({ ...s, txSigs: [...s.txSigs, backingSig] })); } catch (backingBucketErr) { + backingSeedError = backingBucketErr; console.warn( "[useCreateMarket] Step 3 backing-bucket seeding (deadlock prevention) failed — " + "market is otherwise live, but domains 0/1 may still be vulnerable to the freshness " + @@ -2904,6 +2907,58 @@ export function useCreateMarket() { backingBucketErr, ); } + + // GH#2514: assert the OUTCOME, not the transaction result. + // + // The catch above used to be the whole story: it warned to the console + // and fell through to "Market created!". So a sequential launch (an + // explicit retry/resume with startStep <= 3, or the pre-broadcast + // fallback from fresh batching) could report success with BOTH backing + // domains unseeded — the two allocations this same revision declares + // mandatory at 100% of LP collateral each (backingSeedPerDomain, + // lib/market-params.ts). The batched M3a path is unaffected: it bundles + // the deposit and both top-ups in one atomic transaction. + // + // This is the same treatment the insurance seed already gets twice in + // this file, for the same reason and in the same shape: reading the + // engine's own state catches never-built, reverted and partially + // applied alike — including the half-failure where one domain landed + // and the other did not, which no transaction result can distinguish. + // + // The assertion is deliberately on STATUS, not amount. The bucket + // stores a u128 BackingNum, not collateral atoms, so comparing it + // against backingSeed would be a units mismatch — exactly the class of + // bug that produced the "insurance already topped up" miscount recorded + // below. Empty(0) means never seeded; a Fresh bucket with nonzero + // fresh-unliened backing is what TopUpBackingBucket produces. + { + let unseeded: string[] | null = null; + try { + const slabInfoBacking = await connection.getAccountInfo(slabPk); + if (slabInfoBacking?.data) { + const parsed = parseBackingBucketsV17(new Uint8Array(slabInfoBacking.data)); + const missing = findUnseededBackingDomains(parsed.buckets); + if (missing.length > 0) unseeded = missing; + } + // Unreadable/unparseable slab: fall through without throwing. Unlike + // the insurance seed, an unseeded backing domain is recoverable — + // TopUpBackingBucket is replayable by the backing authority, and + // ExpireBackingBucket is permissionless — so failing the launch on a + // transient RPC error would strand a live market for no gain. + } catch { + // Same rationale as above. + } + if (unseeded) { + throw new Error( + `Backing domains were not seeded (${unseeded.join("; ")}). ` + + "The market exists and its liquidity is deposited — retry this step to seed them. " + + "Until then the market is exposed to the backing-freshness deadlock." + + (backingSeedError + ? ` Cause: ${backingSeedError instanceof Error ? backingSeedError.message : String(backingSeedError)}` + : ""), + ); + } + } } // TopUpInsurance + final crank — NOT part of the proven on-chain sequence diff --git a/app/lib/market-params.ts b/app/lib/market-params.ts index caf34c51d..4a18d1843 100644 --- a/app/lib/market-params.ts +++ b/app/lib/market-params.ts @@ -258,3 +258,50 @@ export function deriveMarketParams( estimatedFreezeSecondsFor26PctMove: Math.round((2600 / maxPriceMoveBpsPerSlot) * 0.4), }; } + +/** + * GH#2514: which of the two launch backing domains are NOT seeded. + * + * `backingSeedPerDomain` above declares both domains mandatory (100% of LP + * collateral each), but the sequential launch path used to treat the + * TopUpBackingBucket transaction as best-effort: it caught the error, warned to + * the console, and reported `Market created!` anyway. So a retry/resume launch + * could finish with neither allocation present and nothing saying so. + * + * Pure so it can be tested without driving the whole wizard. Takes the buckets + * from `parseBackingBucketsV17`. + * + * The check is on STATUS + nonzero backing, deliberately NOT on amount: the + * bucket stores a u128 BackingNum, not collateral atoms, so comparing it + * against a `backingSeedPerDomain` result would be a units mismatch. (That is + * the same class of error as the "insurance already topped up" miscount in + * useCreateMarket, which compared a vault balance that also held these very + * backing seeds against an insurance target.) + * + * Empty means never seeded. Expired/Impaired are reported too: neither is a + * usable seed, and both are states the launch is supposed to have prevented. + */ +export const LAUNCH_BACKING_DOMAINS = [0, 1] as const; + +export interface BackingBucketLike { + domain: number; + status: number; + statusName: string; + freshUnlienedBackingNum: bigint; +} + +export function findUnseededBackingDomains( + buckets: readonly BackingBucketLike[], +): string[] { + const missing: string[] = []; + for (const domain of LAUNCH_BACKING_DOMAINS) { + const b = buckets.find((x) => x.domain === domain); + const side = domain === 0 ? "long" : "short"; + if (!b) { + missing.push(`domain ${domain} (${side}): absent`); + } else if (b.status !== 1 /* Fresh */ || b.freshUnlienedBackingNum === 0n) { + missing.push(`domain ${domain} (${side}): ${b.statusName}`); + } + } + return missing; +} diff --git a/app/lib/priceStore/priceStore.ts b/app/lib/priceStore/priceStore.ts index 8151278d8..2bb9be042 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 06d708465673bc64df6c379c7effc4e0297c1faf Mon Sep 17 00:00:00 2001 From: dcccrypto Date: Wed, 2 Sep 2026 15:42:15 +0100 Subject: [PATCH 14/15] 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 499b98bc7..18772fe0c 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 4fb32105e211a385497ce64f314486b21d567122 Mon Sep 17 00:00:00 2001 From: dcccrypto Date: Wed, 2 Sep 2026 15:45:22 +0100 Subject: [PATCH 15/15] 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 bed5fc6c2..910d5d96e 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 9b4b55040..ecb51142a 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