Skip to content

chore: back-merge main into dev (keeps the CSP fix) - #2545

Merged
Hugo0 merged 35 commits into
devfrom
backmerge-main-20260728
Jul 28, 2026
Merged

chore: back-merge main into dev (keeps the CSP fix)#2545
Hugo0 merged 35 commits into
devfrom
backmerge-main-20260728

Conversation

@Hugo0

@Hugo0 Hugo0 commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Summary

Back-merge maindev. main had drifted 34 commits ahead of dev, and the most important of those is the CSP fix. Without this back-merge, the next devmain release would revert that fix and re-open the gap that breaks the card page's charges WebSocket the moment CSP is promoted to enforcing.

Merge commit only — no new work.

What came across from main

  • CSP fix (the reason for this PR)
    • next.config.js: wss://api.peanut.me + wss://*.peanut.me in connect-src (the scheme trap that makes /card the loudest violation in Sentry), wss://*.sumsub.com, https://*.bridge.xyz in frame-src, plus https://api.coingecko.com, https://www.googletagmanager.com, and https://*.google-analytics.com (wildcarded for GA4 region sharding).
    • Report sink moved off direct-to-Sentry: new same-origin collector src/app/api/csp-report/route.ts + src/utils/csp-report.utils.ts (+ tests), with report-uri /api/csp-report and the matching Reporting-Endpoints header. sentryCspReportUri() is gone.
  • fix(security): removed the orphaned guest onramp server action (createOnrampForGuest, which called the unauthenticated POST /bridge/onramp/create-for-guest route deleted in peanut-api-ts feat: handle onesignal push notification initialization  #1247) and its two tests.
  • MDX sanitisation: src/lib/mdx-security.ts + tests, wired into src/lib/mdx.ts.
  • WebSocket auth: getSessionTokenForSocket() in src/utils/auth-token.ts, consumed by src/services/websocket.ts / src/hooks/useWebSocket.ts (+ useWebSocket tests) — the charges socket cannot use a header or the host-only web cookie, so the token is handed over as the first frame.
  • Content pipeline workflows: new content-pipeline-watchdog.yml, updates to content-publish-automerge.yml and update-content.yml.
  • Regenerated API types (src/types/api.openapi.json, src/types/api.generated.ts) — dev had not touched these since the merge base, so main's newer snapshot applies cleanly.
  • src/components/Home/HomeHistory.tsx tweaks.

Conflict resolutions

Two conflicts. Everything else auto-merged.

1. src/utils/auth-token.ts — genuine semantic conflict, resolved toward dev's model.

The two branches hold different answers to native token custody:

  • main: "JS is never a token custodian on native" — the CapacitorHttp native cookie jar is the credential store; getAuthToken() returns null on native.
  • dev (newer): the jar's Android GET proxy stalls (PEANUT-UI-R44), so the token lives in native Preferences, hydrated into an in-memory cache and sent as an Authorization header, with the jar kept only as a fallback for older cookie-auth binaries.

Git auto-merged every function body to dev's model; the only textual conflict was the doc comment straddling main's new getSessionTokenForSocket() and dev's rewritten hasNativeSession() comment. Resolution:

  • Kept both functions (union), each with its own doc comment — nothing deleted.
  • Adapted getSessionTokenForSocket() to dev's model: it now await authReady() and returns the hydrated nativeToken first, falling back to the CapacitorHttp jar. Taking main's body verbatim would have read only the jar, which under dev is no longer where new native sessions are stored — the socket would have silently failed to authenticate on every current native build. This mirrors hasNativeSession() exactly (cache first, jar fallback).
  • Reworded that function's doc comment so it no longer claims "every other call rides the native cookie jar" (true on main, false on dev).

2. src/content submodule — took main's pointer (strictly newer).

Git refused the submodule merge ("commits don't follow merge-base"). Pointers:

  • merge base: 1078c28 (2026-07-27 16:20)
  • dev: e3a05f7 (2026-07-15) — dev had regressed the pointer ~12 days behind the merge base
  • main: c268e5d (2026-07-27 19:02)

c268e5d is a descendant of both, so taking main's pointer is a pure fast-forward that loses nothing and un-does dev's accidental regression. (Worth a separate look at how dev's content pointer went backwards — likely a stale base on a long-lived branch.)

Verification that the CSP fix survived

$ grep -c 'wss://api.peanut.me' next.config.js
2
$ ls src/app/api/csp-report/route.ts src/utils/csp-report.utils.ts
src/app/api/csp-report/route.ts
src/utils/csp-report.utils.ts
$ grep -rn 'sentryCspReportUri' --include=*.ts --include=*.tsx --include=*.js .
(none)
$ grep -n 'report-uri\|Reporting-Endpoints' next.config.js
144:    directives.push(`report-uri ${CSP_REPORT_PATH}`, `report-to ${CSP_REPORT_GROUP}`)
151:    return [{ key: 'Reporting-Endpoints', value: `${CSP_REPORT_GROUP}="${CSP_REPORT_PATH}"` }]

dev's own next.config.js work (the campaign / campaignTag / code/invite redirects) is preserved — the merged file differs from main only by that block.

Local gate

  • pnpm prettier --check .
  • npm run typecheck ✅ clean (needed a real pnpm install in the worktree; the shared node_modules predates dev's next-intl / capacitor deps and produces phantom TS2307s)
  • npm test ✅ 174 suites, 2303 passed, 3 skipped
  • npm run build — see checks

Risk

Merge-only, but two behaviour deltas reach dev:

  1. getSessionTokenForSocket() on native now prefers the Preferences-backed cache over the cookie jar. Required for the charges socket to authenticate at all under dev's auth model.
  2. dev picks up main's API type snapshot. dev had not modified it, so no route definitions are lost.

Screenshots: N/A (no visible change).

Hugo0 and others added 30 commits July 27, 2026 14:45
The report-only CSP has been posting violations straight to Sentry's
security endpoint, which put ~70k events across ~1.8k users into
peanut-ui in a week and buried real signal. Those reports never pass
through the Sentry browser SDK, so no beforeSend/shouldIgnoreError
filter could ever have touched them — the only place to filter is an
endpoint we own, so reports now go to a same-origin collector that
forwards to Sentry minus the noise.

The noise is overwhelmingly repetition: Sentry groups CSP issues by
directive + blocked origin, so one missing allow-list entry generated
14k identical events. The collector forwards the first sighting of each
group unconditionally — a genuinely new violation still creates its
issue and fires its alert — and samples the repeats.

Separately, connect-src was missing wss://api.peanut.me. CSP treats
wss: as a scheme distinct from https:, so neither https://api.peanut.me
nor https://*.peanut.me authorized the charges websocket every logged-in
session opens. That single gap is 84% of the violations still firing,
and it would have broken the socket for every user the moment the
policy was promoted to enforcing. The Sumsub WebSDK's liveness socket
had the identical scheme trap, and the Bridge terms-of-service iframe
was missing from frame-src, which would have dead-ended KYC.
Chromium prefers report-to and ignores report-uri whenever both are
present, so an endpoint value it declines to parse would cost us every
Chromium report — silently, with no error anywhere to notice it by.
The spec says an endpoint URL is resolved against the document, but MDN
and Chrome's own docs both describe the value as absolute. An absolute
URL is valid under either reading, so stop betting on the ambiguity.
report-uri keeps the relative path, which is unambiguously a
URI-reference and stays correct on preview origins too.
Flagged by the code-analysis bot: the exported route handler had no
return type annotation.
…p fan-out

Two defects found reviewing the collector against Sentry's actual
ingest behaviour.

The group key was coarser than Sentry's csp:v1 grouping strategy, which
drops the blocked URI and keys on the keyword when script-src is
violated by 'unsafe-inline' or 'unsafe-eval'. Sentry raises two issues
there; the key collapsed them into one, so whichever arrived second
looked like a duplicate and was sampled away — its issue possibly never
created. Those two keywords are the top of this policy's "tighten before
enforcing" list, so that was exactly the signal the filter must not eat.

The forward loop was also uncapped. The endpoint is unauthenticated and
a Reporting-API payload is an array, so one POST could fan out
arbitrarily many events — and Sentry bills CSP reports against the
*error* quota under a per-key rate limit, making an uncapped fan-out a
way to starve real exception reporting rather than merely noisy.
Reverts the absolute Reporting-Endpoints URL from 2a5fda9. That change
was made on the strength of MDN describing the value as absolute, but
the Reporting API spec resolves an endpoint "with base URL set to
response's url" (W3C Reporting §3.3) — root-relative is valid and is
resolved against whichever origin served the page.

Relative is also strictly safer here. An absolute URL has to come from
NEXT_PUBLIC_BASE_URL, which is unset on preview deploys, so previews
would have pointed at production's collector — cross-origin, and
Chromium ignores report-uri whenever report-to is present, so those
reports would have been dropped rather than falling back.
…amily

Both from CodeRabbit review.

The fan-out cap ran after the de-duplication filter, but shouldForward
records every group it inspects as seen. Truncating afterwards meant
groups past the cap were marked seen without ever being sent, so their
real first sighting was lost and later repeats were sampled away as
duplicates — the exact failure the first-sighting guarantee exists to
prevent. Cap the candidates first, then de-duplicate.

The keyword grouping also only matched a bare `script-src`, but browsers
report the sub-directive they actually checked: inline <script> arrives
as script-src-elem and inline handlers as script-src-attr, with only
eval left under plain script-src. script-src-elem is the second-largest
directive in the current reports, so this branch was mostly not firing.
Numbers predated the Sentry query: ~70k events / ~1.8k users over 7d,
not 65k/1.3k over 4d.
charges-ws pushes {uuid,status} pings on charge completion and expects
clients to refetch /users/history. The FE rendered them as full history
entries — with no extraData.kind the transformer falls back to a generic
strategy, so completed withdrawals/request-pays flashed as 'Sent to
Transaction $0.00 · Completed' on home/history until a refetch, and
HomeHistory's uuid-keyed merge could even overwrite the real fetched row
with the ping (PEANUT-UI-QCW, 104 users; TASK-20808).
The /history page's per-entry callback used to invalidate the balance
query on every WS entry; kindless pings no longer reach callbacks, so the
shared guard refreshes balance itself — completions move balance.
Several mounted useWebSocket instances see the same ping; TanStack's
default cancelRefetch=true made each invalidation abort and restart the
same /users/history fetch.
createOnrampForGuest called POST /bridge/onramp/create-for-guest, the
unauthenticated route removed in peanut-api-ts #1247. Nothing in the UI
ever called it — only its own two tests did — so this is dead code either
way, but peanut-ui is a public repo and leaving it here keeps advertising
the calling pattern it exploited: userId in the body, no auth header.

Deleting the function orphans CreateOnrampGuestParams and three imports
(CountryData, getCurrencyConfig, getCurrencyPrice), all of which existed
solely to serve it. cancelOnramp and its serverFetch import are untouched.

Generated API types are deliberately left alone: sync-openapi.yml pulls
from staging on a weekday cron and owns them, and staging will not carry
the route removal until the main->dev back-merge lands.

Reported by Gibran, 27 Jul 2026.
…ch-timeouts

fix(fetch): context-aware fetch timeouts — stop applying a stale Vercel limit to browsers
Companion to peanut-api-ts #1247, which closes the PR #644 authorization
fallout. The API now refuses to stream anything on /ws/charges/:username
until the client proves who it is.

The socket carries charge activity, Bridge/Manteca/Sumsub KYC status -
including rejectLabels - and Rain card balance changes, and it was keyed
on a public username with no credential at all. The frontend consumes
those payloads (useWebSocket passes rejectLabels straight to the KYC
handlers), so stripping them was not an option; the fix has to be auth.

PeanutWebSocket now sends {type:'auth', token} as its first frame and the
server subscribes to nothing until it verifies. A message frame rather
than a header or query param because the browser WebSocket API cannot set
headers, the jwt-token cookie is host-only on peanut.me and so never
reaches api.peanut.me, and a token in a URL would land in every access log
in between. It also suits native: getSessionTokenForSocket reads the token
from the Capacitor cookie jar asynchronously, which a post-open frame can
wait for. That helper is a deliberate, single-caller exception to "JS is
not a token custodian on native" - the WebView's own WebSocket cannot see
CapacitorHttp's jar, so nothing else can carry the credential.

An auth rejection arrives as a non-clean close, which would otherwise fall
into the 5x exponential-backoff loop and hammer the API with a credential
already known to be bad. Close codes 4401/4408 and the auth_error frame
now short-circuit that.

HomeHistory subscribed to the `username` PROP, which on a public profile
is the profile owner - so a logged-in viewer opened a socket on a
stranger's channel and received their KYC and card events. It now always
subscribes to the signed-in user's own channel, matching every other
useWebSocket caller. The visible list is unchanged: it still comes from
useTransactionHistory keyed on the viewed username with filterMutualTxs,
which is what makes a profile show transactions mutual to the viewer.

API types regenerated from the #1247 branch's openapi.json. Beyond the
routes that PR deletes, this picks up four the committed spec had drifted
behind (auth/step-up/*, unsubscribe, users/logout) - types only.
…268e5d)

Bumps src/content on production (main) from 1078c28 → c268e5d
(peanut-content latest = mono@0b0740a). Single-file submodule
pointer change. Publishes content already merged + mirrored from mono that the
dev-targeted auto-PRs never promote to main.
…202607271917

content: publish latest to production (src/content → peanut-content@c268e5d)
…onnect on the no-session path

Two real bugs from the previous commit, both caught by CodeRabbit.

Switching the socket to the signed-in user's own channel stopped the
cross-user leak, but the merge downstream was left untouched - so on
someone else's profile the VIEWER's own live transactions were still
folded into the list being rendered, which is the profile owner's
history. Derive liveEntries, empty unless viewing your own history, and
merge that instead.

The no-session branch emitted 'connect' (the socket did open), then
'auth_error', then called disconnect(). But disconnect() nulls onclose
before closing, so handleClose - the only other place that emits
'disconnect' - never ran. useWebSocket maps those two events straight
onto its status, so a client with no session sat there reporting
"connected" forever with no reconnect scheduled to correct it. Emit
'disconnect' explicitly on that path.
Content merged to mono never reached peanut.me on its own. update-content.yml
opened its submodule-bump PRs against `dev`, but content only ever flows
main->dev via the back-merge, so `dev` was never ahead of `main` on
`src/content` and a release could not carry a bump up. 29 auto-PRs were opened
against `dev`; not one merged. Every publish was a manual signed PR to `main`.

Retarget the auto-PR to `main` and let content-publish-automerge.yml own the
gate. Because update-content.yml authors as CONTENT_BOT_TOKEN's user and GitHub
forbids self-approval, the existing approve-then-auto-merge path can never
complete for these PRs — so add a workflow_run fallback that merges via the
org-admin bypass.

That bypass skips the required status check as well as the review, so the
fallback re-establishes green `ci-success` itself before merging: at least one
ci-success check run on the exact head commit, all of them green, diff exactly
`src/content`, PR open, non-draft, same-repo. Fail-closed throughout.
…action

feat(security): authenticate the charges websocket + drop the orphaned guest onramp action
…tch drops the update

A fetch already running when the ping arrives snapshotted state before
the charge/claim committed; cancelRefetch:false let it satisfy the
invalidation without the new row (stuck until the next 30s poll).
Abort-restart is the correct edge-case behavior (jjramirezn).
Two failure modes the retarget would otherwise move from `dev` onto `main`.

A burst of content pushes dispatches several runs of update-content.yml. Each
branches off the same `main` and opens its own PR; once the first merges, the
rest conflict on the `src/content` gitlink and can never merge. Serialise with
a concurrency group so the newest dispatch wins — it checks out peanut-content's
tip, so it already contains whatever the cancelled runs would have published.

Any auto-PR left open is then dead weight, so close superseded ones on open and
delete their branches. This is a chore that was already being done by hand: all
29 auto-PRs ever opened against `dev` were closed manually, one at a time, none
merged. Best-effort — a failure to close must not fail the publish just opened.
MDX is code. An expression in a content file compiles straight through to
executable JavaScript — `{require("fs").readFileSync("/etc/passwd")}` survives
compilation verbatim — and then runs wherever the page renders: the CI runner
during `next build`, and the production build on Vercel. Both carry secrets.

That was tolerable while a human looked at every content publish. The preceding
commits remove that human: content-only bumps now merge to production on green
CI alone, on the premise that content cannot do anything dangerous. Enforce the
premise rather than assume it.

The check is on *executable*, not on braces. Content uses two inert forms that
must keep working — `{/* editor comments */}` and literal props like
`<Step number={1} />` — so an expression is allowed when its parsed program is
empty or a single literal, and rejected when it can call, read or reference.
Imports, exports and JSX spreads have no inert form and are always rejected.
Fail-closed: an expression whose program cannot be parsed is rejected.

Verified against the real compiler: 7 attack payloads rejected (fs read, env
exfil, spread, import, IIFE, template literal, bare identifier), and all 758
files under src/content compile unchanged. Content has 17 expressions in total —
15 literals and 2 comments — and no executable JavaScript anywhere, so this
removes nothing that is in use.
Auto-publishing removed the human who used to notice. It did not add anything
to notice in their place: a broken internal link fails `validate-links`, the
publish PR never merges, and nothing goes red anywhere — the content simply
never appears until somebody spots a stale page.

Assert the end-to-end invariant rather than instrumenting each failure: is
production serving the content that is on mono `main`? That one question covers
a dead mirror, a dispatch that never fired, a failed update-content run, red CI,
a conflicted PR, and whatever else we have not thought of.

It lives in peanut-ui because peanut-ui already holds tokens for all three repos
(MONO_READ_TOKEN, SUBMODULE_TOKEN) and mono has no secrets at all. Checking hop 1
matters: comparing only peanut-content against peanut-ui would look perfectly
consistent while the mirror was stuck and mono raced ahead.

Loud means loud. A stuck pipeline always fails the run — red in Actions, and
re-failing every 15 minutes so it cannot scroll away — and additionally posts to
Discord when CONTENT_PIPELINE_DISCORD_WEBHOOK is set. A missing webhook degrades
the alert; it never silences it. Red CI skips the grace period, since it will
never resolve on its own.
Cloudflare 403s Discord webhook posts made with python-urllib's default
User-Agent. Caught by actually firing the webhook rather than trusting it:
the alert path would have failed silently in CI, which is precisely the
failure this watchdog exists to prevent.
I claimed this closed a live RCE. It does not. next-mdx-remote@6 already strips
the same node set by default — removeJavaScriptExpressions via blockJS, and
removeImportsExportsPlugin via useDynamicImport:false — so the production path
was never exposed. My proof compiled through raw @mdx-js/mdx, which is not what
renderContent calls.

The guard still earns its place, for smaller and truer reasons: it pins an
invariant that is currently only a dependency default on a path that publishes
to production unreviewed, and because it runs first in remarkPlugins it throws
where next-mdx-remote silently strips — so '{price}' written by an author
becomes a failed build instead of a page that quietly renders nothing.
The Android, iOS and demo-mode runbooks were consolidated into mono at
engineering/native/ so there is one source of truth. The copies here had
already drifted and now state wrong facts (Capgo deploy triggers, a secret
name that no longer exists, the wrong Sumsub plugin package), so a reader
landing on them gets misled.

Kept as pointers rather than deleted because three files in the release
pipeline still reference these paths. Paths are repo-relative, not
github.com URLs, so they also resolve for readers who only have a partial
copy of mono.
…o-main

ci(content): publish content to production automatically
…publish

The watchdog's first live run cried wolf and would have done so every 15
minutes forever.

It compared mono's latest commit touching `content/` against the sha stamped
in peanut-content's "mirror: sync from mono@<sha>" tip. But the mirror rsyncs
`content/` with `--exclude='_system/'` (plus `_system/generated/` separately),
so a commit touching only `content/_system/**` is correctly never published.
The comparison read that as the mirror being stuck. d9a33de — a docs edit to
content/_system/ARCHITECTURE.md — tripped it within minutes of merge.

The stamp can't answer "is the mirror healthy" because it only ever names
commits that happened to touch mirrored paths. Ask something that has a real
answer: did the mirror's last completed run succeed? A run that published
nothing is healthy; one that errored is not. A broken mirror also skips the
grace period, since it will not fix itself.

Verified against the same live state that produced the false alarm: now
reports CURRENT and stays silent.
…ty scripts

Our own script-src contains 'unsafe-inline' and 'unsafe-eval', and
Firefox/Safari echo the entire source list back in violated-directive.
The keyword branch matched that text without checking the violation was
local, so EVERY script-src report — including a genuinely blocked
third-party script — keyed as the ubiquitous inline violation and got
sampled away at 1%. That is the exact issue-loss the de-duplication was
written to prevent, in the directive that matters most for XSS.

Gate the branch on a local blocked-uri and match the quoted keyword, as
Sentry's csp:v1 strategy does. Normalize the directive to its first
token too: Firefox omits effective-directive, so keys were embedding the
whole policy and churning on every edit.

Also, forwarding through our own route made Sentry attribute every event
to one Vercel egress IP running undici, deleting the browser/user
dimension these reports are read for — pass the original User-Agent and
X-Forwarded-For through. De-duplicate within a batch so repeats of one
violation cannot crowd a new group out of the per-request cap, and evict
the oldest seen group instead of dumping all 500 at the ceiling.
…d-report-noise

fix(csp): close the wss allow-list gap and stop the Sentry report flood
…ters-main

docs(native): point the three native runbooks at mono (main)
Hugo0 and others added 5 commits July 28, 2026 11:50
…-filter

fix(watchdog): stop alerting on content the mirror is never meant to publish
fix(ws): don't render minimal charge-completion pings as $0.00 history rows
… faults

The first fix traded one false alarm for another. Checking the mirror's run
status via the Actions API is the natural question to ask, but MONO_READ_TOKEN
carries contents:read only and 403s on actions:read — so the step died mid-run
under `bash -e` and surfaced as a bare red run, indistinguishable from a real
content alert. I had dry-run it with my own token, which has both scopes.

Answer the same question with contents:read: walk back through mono's recent
`content/` commits and take the newest one that touched a path the mirror
actually publishes (content/** minus _system/, plus _system/generated/), then
compare that against the "mirror: sync from mono@<sha>" stamp. Verified against
live history — it skips d9a33de and 539b81f, both content/_system/ only, and
lands on 0b0740a, which matches the stamp. Silent, correctly.

Two hardening changes from the same incident:

Every API call is now `|| true`. The step shell is `bash -e`, so any unguarded
non-zero aborts the script at that line and the operator sees a red run with no
message explaining it.

"Cannot evaluate" is now its own labelled state — WATCHDOG DEGRADED — rather
than looking identical to "content is stuck". A watchdog that cannot watch is
still worth waking someone for, but not for the wrong reason.
fix(watchdog): use a scope the token actually has, and label watchdog faults
# Conflicts:
#	src/content
#	src/utils/auth-token.ts
@cursor

cursor Bot commented Jul 28, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@vercel

vercel Bot commented Jul 28, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
peanut-wallet Ready Ready Preview, Comment Jul 28, 2026 11:27am

Request Review

@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: f02f91dd-dec6-4112-b7cb-bb01ae9f02eb

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch backmerge-main-20260728

Comment @coderabbitai help to get the list of available commands.

@Hugo0
Hugo0 marked this pull request as ready for review July 28, 2026 11:22
@github-actions

Copy link
Copy Markdown
Contributor

Code-analysis diff

Painscore total: 6633.7 → 6656.29 (+22.59)
Findings: +4 net (+387 new, -383 resolved)

🆕 New findings (387)

  • critical complexity — src/utils/demo-api.ts — CC 106, MI 59.49, SLOC 908
  • critical complexity — src/components/Home/HomeHistory.tsx — CC 104, MI 57.65, SLOC 321
  • high structural-dup — types/api.generated.ts:346 — 83 duplicate lines / 384 tokens with types/api.generated.ts:1059
  • high structural-dup — types/api.generated.ts:356 — 74 duplicate lines / 344 tokens with types/api.generated.ts:1069
  • high structural-dup — types/api.generated.ts:139 — 72 duplicate lines / 269 tokens with types/api.generated.ts:9519
  • high structural-dup — types/api.generated.ts:139 — 70 duplicate lines / 264 tokens with types/api.generated.ts:9605
  • high structural-dup — types/api.generated.ts:139 — 67 duplicate lines / 249 tokens with types/api.generated.ts:9196
  • high structural-dup — types/api.generated.ts:9141 — 55 duplicate lines / 162 tokens with types/api.generated.ts:9293
  • high structural-dup — types/api.generated.ts:9141 — 55 duplicate lines / 162 tokens with types/api.generated.ts:9464
  • high structural-dup — types/api.generated.ts:9141 — 55 duplicate lines / 165 tokens with types/api.generated.ts:9722
  • high complexity — src/hooks/useWebSocket.ts — CC 41, MI 59.56, SLOC 187
  • high hotspot — src/components/Home/HomeHistory.tsx — 33 commits, +348/-243 lines since 6 months ago
  • high complexity — src/utils/csp-report.utils.ts — CC 32, MI 60.73, SLOC 87
  • medium high-mdd — src/components/Home/HomeHistory.tsx:50 — HomeHistory: MDD 101.4 (uses across many lines from declarations)
  • medium high-mdd — src/hooks/useWebSocket.ts:35 — useWebSocket: MDD 101.2 (uses across many lines from declarations)
  • medium high-mdd — src/hooks/useWebSocket.ts:127 — : MDD 87.9 (uses across many lines from declarations)
  • medium structural-dup — types/api.generated.ts:3207 — 49 duplicate lines / 139 tokens with types/api.generated.ts:6586
  • medium structural-dup — types/api.generated.ts:3207 — 49 duplicate lines / 137 tokens with types/api.generated.ts:7161
  • medium high-mdd — src/services/websocket.ts:183 — handleMessage: MDD 41.8 (uses across many lines from declarations)
  • medium structural-dup — types/api.generated.ts:667 — 42 duplicate lines / 152 tokens with types/api.generated.ts:2243

…and 367 more.

✅ Resolved (383)

  • src/utils/demo-api.ts — CC 107, MI 59.53, SLOC 914
  • src/components/Home/HomeHistory.tsx — CC 102, MI 57.43, SLOC 317
  • types/api.generated.ts:421 — 83 duplicate lines / 384 tokens with types/api.generated.ts:1220
  • types/api.generated.ts:431 — 74 duplicate lines / 344 tokens with types/api.generated.ts:1230
  • types/api.generated.ts:214 — 72 duplicate lines / 269 tokens with types/api.generated.ts:9646
  • types/api.generated.ts:214 — 70 duplicate lines / 264 tokens with types/api.generated.ts:9732
  • types/api.generated.ts:214 — 67 duplicate lines / 249 tokens with types/api.generated.ts:9323
  • types/api.generated.ts:9268 — 55 duplicate lines / 162 tokens with types/api.generated.ts:9420
  • types/api.generated.ts:9268 — 55 duplicate lines / 162 tokens with types/api.generated.ts:9591
  • types/api.generated.ts:9268 — 55 duplicate lines / 165 tokens with types/api.generated.ts:9849
  • src/hooks/useWebSocket.ts — CC 40, MI 60.02, SLOC 179
  • src/components/Home/HomeHistory.tsx — 31 commits, +323/-238 lines since 6 months ago
  • src/components/Home/HomeHistory.tsx:50 — HomeHistory: MDD 97.7 (uses across many lines from declarations)
  • src/hooks/useWebSocket.ts:33 — useWebSocket: MDD 92.0 (uses across many lines from declarations)
  • src/hooks/useWebSocket.ts:124 — : MDD 79.9 (uses across many lines from declarations)
  • types/api.generated.ts:3467 — 49 duplicate lines / 139 tokens with types/api.generated.ts:6713
  • types/api.generated.ts:3467 — 49 duplicate lines / 137 tokens with types/api.generated.ts:7288
  • types/api.generated.ts:7365 — 40 duplicate lines / 108 tokens with types/api.generated.ts:7437
  • types/api.generated.ts:9283 — 40 duplicate lines / 120 tokens with types/api.generated.ts:9520
  • types/api.generated.ts:748 — 39 duplicate lines / 152 tokens with types/api.generated.ts:2287

…and 363 more.

📈 Painscore deltas (top movers)

File Before After Δ
src/app/api/csp-report/route.ts 0.0 10.2 +10.2
src/utils/csp-report.utils.ts 0.0 6.8 +6.8
src/lib/mdx-security.ts 0.0 5.5 +5.5
src/hooks/useWebSocket.ts 10.1 10.9 +0.8
src/app/actions/onramp.ts 8.4 6.2 -2.1

@github-actions

Copy link
Copy Markdown
Contributor

🧪 UI test report — ✅ all green

Suites

  • unit: 2306 ran, 0 failed, 0 skipped, 40.2s

📊 Coverage (unit)

metric %
statements 60.9%
branches 44.1%
functions 50.1%
lines 61.4%
⏱ 10 slowest test cases
time test
3.6s src/components/Card/share-asset/__tests__/shareAssetLayout.test.ts › never places two stickers in heavy overlap (broad seed sweep)
1.1s src/utils/__tests__/demo-api.test.ts › isDemoMode() is false when not running under Capacitor
0.5s src/components/Card/share-asset/__tests__/shareAssetLayout.test.ts › every sticker stays within canvas at any count
0.3s src/utils/__tests__/sentry.utils.test.ts › defaults to the client budget under a browser global
0.3s src/app/actions/__tests__/api-headers.test.ts › should include Content-Type in validateInviteCode
0.3s src/app/(mobile-ui)/withdraw/__tests__/withdraw-states.test.tsx › Bank withdrawal keeps the $1 minimum for sub-$1 amounts
0.3s src/utils/__tests__/sentry.utils.test.ts › still lets a per-call timeoutMs win over the default
0.3s src/components/Card/share-asset/__tests__/shareAssetLayout.test.ts › keeps stickers off the username pill (final pass respects the keep-out)
0.3s src/app/actions/__tests__/api-headers-extended.test.ts › should not include apiKey in validateInviteCode body
0.3s src/utils/__tests__/demo-balance.test.ts › resetDemoBalance refills and restarts the TTL window
📍 Inline annotations are in the **Unit test report** check above. Coverage artifact: `coverage-unit`. Generated by `.github/workflows/tests.yml`.

@Hugo0
Hugo0 merged commit 134cf01 into dev Jul 28, 2026
20 checks passed
@Hugo0
Hugo0 deleted the backmerge-main-20260728 branch July 28, 2026 11:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants