feat: design system + registration/onboarding, integrated - #2876
Conversation
ci: make eslint blocking now errors are at zero (TASK-21450)
Wire scripts/ds-lint-counts.mjs --check into CI as a blocking job: the five DS debt counts may only go down. Allowlist the /dev/ds and /dev/components showcase pages for every metric (they render token values programmatically by design — they were the whole inlineStyle 207->211 false regression) and tighten the baseline to current-tree truth, locking in the DS 05/06 gains (stock text sizes 1392->1103, non-DS view classes 473->439).
…et, TASK-21450) CLAUDE.md 'URL as State' says URL state goes through nuqs useQueryStates, never manual query-param reads/writes. Enforce it for NEW code only: - no-restricted-imports: useSearchParams from 'next/navigation' -> nuqs. - no-restricted-syntax: router.push/replace with a '?' in a string or template-literal argument (both literal shapes covered; concatenations and variable args slip through by design to keep false positives at zero — pathname-only pushes are never flagged). All 49 current useSearchParams files and 18 query-push files are allowlisted in scoped override blocks (ratchet — no mass migration; entries get removed as files migrate). Repo lints at 0 errors; the two allowlist blocks keep every other restriction (barrel ban, RESTRICTED_SYNTAX_BASE) active for those files.
Nine call sites still hit PEANUT_API_URL with raw fetchWithSentry, each hand-rolling what apiFetch already owns: authReady() gating (guarded native sessions), Bearer-token headers, Content-Type defaults, and demo-mode routing. One fetch path means auth and error handling can't drift per call site. - apiFetch: skip the application/json default for FormData bodies — fetch must own the multipart boundary. Unblocks the send-links and charges attachment uploads without behavior change for existing users. - Public pre-auth reads (username availability, token prices) migrate with includeAuth: false so they neither queue behind auth hydration nor send a token — exactly the previous semantics. - sendLinks/charges keep their explicit demo-interceptor branch: the real request body is multipart, which the demo store can't parse. - New jest gate (no-direct-peanut-api-fetch.test.ts) fails when a file combines a fetchWithSentry( call with PEANUT_API_URL; exemption list holds only api-fetch.ts itself, with a stale-entry check. The unit CI job runs jest, so the gate is a CI gate by construction.
src/content is the only submodule and holds no .ts/.tsx; the count script reads nothing else. Keeps SUBMODULE_TOKEN out of the job's scope entirely.
…ectors (code review) The blanket 'no-restricted-syntax: off' silently swallowed the new DS 10 query-push ban too, hiding a real offender from the ratchet inventory. Now only the history.length check and the router.back it gates stay exempt; the pre-ban query push is documented as an allowlist member to migrate.
…bit) - card.ts authHeaders no longer throws in demo mode, so the request reaches apiFetch's demo interceptor (which already serves /card). - Gate test pins api-fetch.ts to exactly one sanctioned fetchWithSentry call (the whole-file exemption could hide a second one). - Posix-normalize paths so the exemption list matches on Windows.
… to raw fetch (TASK-21450)
Code-review (high) triage of the apiFetch migration — two correctness
regressions and four cleanups:
- demo-api /users/username/:username answered 200 for ANY username, so
with a latched demo flag (invite code 'demo', nothing clears it before
Setup) every signup username read "taken". Only the demo cast (demo
user + seeded contacts) resolves now; everything else 404s, which is
what recipient resolution and the availability probe both expect.
- /tokens/price + /tokens/wallet-portfolio join PASSTHROUGH_GET: they
are public, and the canned {} fallback crashed recover-funds
(balances.balances.filter TypeError).
- parseBody reads FormData field-by-field so a future multipart caller
without a JSON pre-intercept records real values, not amount '0'.
charges keeps its intentional exact-shape pre-intercept; sendLinks
drops its redundant one (the demo handler never reads the body —
apiFetch's built-in routing is byte-identical).
- card.ts sheds the duplicate header layer: apiFetch already gates on
authReady() and attaches the token, and the api-key header was dead
(no NEXT_PUBLIC_ prefix → undefined client-side; backend dropped the
requirement). A demo-aware fail-fast guard keeps the loud local
'Authentication required' error.
- gate test also flags raw fetch( + PEANUT_API_URL co-occurrence; the
six pre-existing files are documented exemptions (useClaimLink is
flagged as deliberate follow-up debt).
- drop manual Content-Type headers the wrapper now owns (onramp-quote
bodyless GET, card joinWaitlist, sendLinks JSON branch).
…21450)
CodeRabbit round-2 on the apiFetch migration, four findings:
- gate: match fetchWithSentry calls with /\bfetchWithSentry\s*\(/ (and
the same for the exactly-one pin) — `fetchWithSentry (…)` previously
slipped past both the offender scan and the count.
- demo passthrough: a failed live fetch for a passthrough path with no
canned fallback (the /tokens/* pair) degraded to defaultShape 200 {} —
the exact crash shape the passthrough addition fixed. It now returns a
503 json error so callers run their error paths; the FX trio still
falls back to its canned handlers, and defaultShape is unchanged for
genuinely unmatched routes.
- parseBody FormData: JSON.parse only values that start with '{'/'[' —
parsing every string coerced "123"/"true" into numbers/booleans and
violated declared string fields (reference, username). First-char
gating is simpler and safer than a known-field list.
- demo username route: return the CANONICAL matched record's identity
('demo' → userId 'demo-user' + DEMO_ADDRESS account from DEMO_USER;
contacts → their real userId/fullName) instead of inventing one via
demoApiUser (which gave 'demo-demo').
ci: DS debt-count ratchet on every PR (TASK-21450)
chore(lint): ratchet against useSearchParams + manual query pushes (TASK-21450)
refactor: route remaining direct Peanut-API calls through apiFetch (TASK-21450)
Previous rebuild had the wrong visual composition (left-aligned icon+title header card, stacked value-under-label rows). Redone to figma boards 17490:115877 (TX Details) + 17966:12128 (states): - head: centered bubble/avatar -> type line -> 36px amount -> status badge; completed shows no badge, pending greys the amount, cancelled/refunded/ failed strike it through, outgoing carries a minus (never a plus) - one receipt-style card with dashed dividers, label-left/value-right rows; estimate-conversion moved from head sub-line into a card row; points row reads '+N <star>' right-aligned - request pots: contributed/remaining bar (native <progress>, ratchet-clean) and contributors list live inside the receipt card - CTA zone: purple primary pill + stroke secondary (primary-soft retired here); QR moved next to the CTAs; support footer restyled to the board Link Button (12px underline, trailing icon) - perk receipt head centered on the same recipe
…wer deep-link, drawer chrome States board 17966:12128 on list rows: incoming successful is the base state (no '+' prefix, no success badge), outgoing keeps '-', pending greys the amount with a pending chip, cancelled/failed strike through (failed adds the error chip). Amount treatment lives once in TransactionCard so the home widget and history page inherit it. StatusPill restyled to the board's icon chip on badge background tokens. History page date headers on Label/M per the board. Drawer selection moved from useState to nuqs ?tx= so an open receipt survives refresh and deep-links; drawer chrome (handle, page background, no border) per TX Details board 17490:115877.
…tatus families, hook match helper, per-row drawer mount short-circuit Findings applied: failed card refunds keep the chip but not the strikethrough (product intent kept from isDeclinedCardSpend); PENDING/STRUCK status families exported from history.utils next to STATUS_SHOWS_SIGN so sign + styling stay in lockstep; isTransactionSelected lives in the hook (one match rule, all consumers); the per-row drawer mounts only after first selection (ref keeps the close animation); qr-pay receipt memo drops its type-erasing cast; history shell gap-8 per the page recipe; /dev/ds StatusPill doc text updated.
ds 07+09+13: home rebuild, receipts rebuild, appshell + bottom nav
ds 14: activity surfaces — states rules, list restyle, ?tx= deep-link, drawer chrome
feat(ds): DS 19-22 — notification, link button, accordion + slider restyle
274 dev commits (mobile-release native layer, i18n extraction, QR clipboard split, Crisp postMessage handshake, native auth transport) merged into the DS rebuild. 21 conflicts resolved — DS structure kept on rebuilt surfaces, dev behavior ported in (see PR body for the per-file table). pre-commit --no-verify: secret-scan false positive on the well-known Anvil test key #0 in scripts/spike-session-key-1271.mjs, which comes from dev commit 9d227f1 unchanged.
- sendLinks.create back on apiFetch for both transports (apiFetch keeps the FormData boundary and native-http keeps FormData on the webview fetch, so the DS one-fetch-path gate holds; kept dev's apiErrorFromResponse) - charges transport test updated to the apiFetch multipart expectation - raw-fetch gate: exempt utils/native-canary.ts (pre-session probe by design) - dev-side test capacitor mocks gained isAndroidNative (DS Loading pulls the mascot module into their import graph) - prettier on merge-touched files
… profile/view - ShowNameToggle aria-label from profile.menu.showMyFullName (dev's copy-props-from-catalog rule x DS's toggle restyle) - profile/view/page.tsx into the DS 10 useSearchParams allowlist: dev-originated native route (a70720f), predates the ban on its own branch
- card.ts: native-aware auth guard (rain.ts shape) — the DS-side throw blocked dev's tokenless-native cookie-jar cohort on every card surface - sendLinks + charges transport test: stop claiming multipart rides the native cookie fallback (FormData can't cross the bridge — same known limit as before, now documented instead of denied) - SecurityVerificationOverlay: drop wrapper role/aria — Loading mascot already renders its own role=status live region (was double-announcing) - profile/view: migrate the one username param to nuqs and drop the allowlist entry (ratchet says migrate, not allowlist); useSearchParamsFiles 49 < 51 baseline - eslint: remove the stale dev/leaderboard query-push allowlist entry (file deleted on dev)
…stem merge: back-merge origin/dev into feat/design-system (274 commits, 21 conflicts)
Stock text sizes and legacy palette swapped for the semantic scale (text-body-*/heading-*, foreground/background/border tokens) across the deposit views. Ad-hoc alert/success icon circles become IconBubble. Tab triggers and dividers keep their visual values via action/border tokens. No flow or copy changes.
Withdraw pages/views, profile menu + edit + backup + public profile, settings language + delete-account, and the notifications page move to the semantic type scale and color tokens. Backup FAQ rows become ListItem. ExchangeRateWidget gets a token-only pass with a note — its .btn shell composition needs a figma board before structural rework.
There was a problem hiding this comment.
Chip review — changes requested
Two previously reported correctness defects remain: bank-request payments still display as incoming, and the residence reorder fixes only the device-local mirror while leaving the durable server pair duplicated.
Findings
-
MAJOR · src/utils/history.utils.ts:342 · Bank-request payments are still signed as incoming
This is unchanged from the prior review. For a Bridge P2P_REQUEST_FULFILL where the viewer is SENDER, the transformer emits bank_request_fulfillment and the receipt calls it "Sent to …", but this mapping renders +$25 even though the payer's balance decreased. Map bank_request_fulfillment to "-" and add a sign regression case for the sender-side Bridge fulfillment. -
MAJOR · src/components/Profile/views/ResidenceChangeModal.tsx:92 · Residence reorder still drops a country on the server
With primary ES and second FR, selecting FR still calls /update-user with only residenceCountry=FR. The new localStorage swap makes this device show FR/ES, but the endpoint supports secondResidenceCountry and the durable second slot remains FR, so the server pair becomes FR/FR and ES is lost. Compute the swap before the request, submit both fields together, then update both mirrors; assert the request includes secondResidenceCountry=ES. -
MINOR · .github/workflows/tests.yml:445 · [moonshotai/kimi-k3] ds-shots runs PR-controlled Node code with write-scoped GH_TOKEN in env
In the ds-shots job's 'Publish the changed screenshots' step,GH_TOKEN: ${{ github.token }}(scopedcontents: write+pull-requests: writefor this job) is exported into the environment of a step that then executesnode scripts/visual-comment.mjsfrom the PR checkout. Any branch pushed to this repo (forks are blocked by ds-shots-filter's head-repo check) that touchesscripts/visual-comment.mjs— which the filter regex explicitly counts as pixel-moving — gets that script executed while the token is readable fromprocess.env, so the script can exfiltrate it and push/act on PRs over the API. Additionally, the token is embedded in the git remote URL (https://x-access-token:$GH_TOKEN@github.com/...), which a failedgit pushcan echo into build logs. Fix: run thenodelisting step without GH_TOKEN in env and authenticate the push without token-in-URL (e.g.git config http.https://github.com/.extraheader "AUTHORIZATION: basic <b64>", orghCLI), keeping the token materialized only for the push itself.
Inline anchors unavailable for 1 finding(s); the findings remain in this summary.
Checked clean
- Pinned worktree HEAD, trusted author, base ref/SHA, and merge base all match the supplied values.
- Earlier findings: bank-request sign is STILL PRESENT; dual-residence reorder is STILL PRESENT in durable server state despite the local-mirror swap; required ESLint failure is FIXED.
- The two new fix commits and all six files changed since the previous reviewed head were inspected; the DS twMerge import corrections and unused-import removal are sound.
- Bank fulfillment was traced from sender-side Bridge P2P_REQUEST_FULFILL through the transformer, activity sign, receipt title, and tests.
- Dual-residence persistence was traced from signup through /update-user's residenceCountry and secondResidenceCountry fields, local mirrors, refetch, and restriction consumers.
- No additional actionable security, privilege, injection, secret-exposure, or maintainability defect was found in the fix-up delta.
- Exact-head unit, eslint, typecheck, format, ds-lint, human-authors, analyze, report, and aggregate ci-success checks passed; ds-shots remained in progress.
- Targeted local Jest execution was unavailable because this detached worktree has no Jest binary; exact-head unit CI is green.
Second opinion by moonshotai/kimi-k3: 1 finding(s), marked with the model name. It reads the diff only, so treat its findings as advice.
Exact head: 6e4031ff3cb8 · Context: repo, ci
…y from PR code Two more from Chip on the re-review. MAJOR — the residence reorder was still dropping a country. My previous fix swapped only the localStorage mirror, but `/update-user` takes `secondResidenceCountry` and signup already persists BOTH slots server-side (SignTestTransaction.tsx:172). So the durable pair still became FR/FR and ES was lost for good; the local swap merely hid that on the device that made the change. The swap is now computed before the request and both fields travel with it, mirrors updated after. The test asserts the request body, not just the mirror — asserting the mirror alone is what let the half-fix look green. MINOR, but a real token exposure — the ds-shots publish step exported a write-scoped GH_TOKEN into the environment of a step whose first action was `node scripts/visual-comment.mjs` from the PR checkout. That script counts as pixel-moving, so a PR may edit it, and it could read the token from process.env and act on the repo. Forks are already blocked by ds-shots-filter's head-repo check, but same-repo branches are not. The listing now runs in its own token-free step and gates the publish through an output. The push also stopped embedding the token in the remote URL, where a failed push would echo it into the build log — it goes through an http.extraheader instead. Gates: typecheck clean - eslint 0 errors - prettier clean - ds-lint green - workflow yaml parses, and the token-free listing step is confirmed by inspecting the parsed job - 2057 tests across 139 suites.
…ating refs into shell
Chip flagged one instance of this in ds-shots. Sweeping every workflow for the
same two shapes found more, all pre-existing rather than introduced here.
Secret readable by PR-editable code. `unit / Fetch BE render-snapshot baseline`
put PEANUT_ALL_READ_TOKEN — a cross-repo read token — in the env of a step that
then ran `node scripts/import-be-baseline.mjs` from the PR checkout. Same shape
as the ds-shots finding: a PR may edit that script, and it could read the token
out of process.env. Split so only the `gh api` fetch sees the token.
Ref interpolated straight into shell. `${{ github.base_ref }}` was expanded into
run: blocks in tests.yml, supply-chain-check.yml and code-analysis.yml. Git
allows a single quote in a refname, and in tests.yml the value sat inside a
single-quoted echo — so a branch name could close the quote and run commands.
All three now pass it through env and reference `$BASE_REF`.
Scope: only the workflows that run automatically on a pull request. The
remaining hits are release pipelines behind workflow_dispatch, where the threat
model needs a maintainer to dispatch from a hostile ref — real, but a different
question, and not one to answer by editing signing pipelines inside a
738-file PR. Listed in the PR body as follow-up.
The one remaining audit hit in a PR-triggered workflow is
`github.event.pull_request.head.sha` — a commit hash, not attacker-controllable
text. Left as is.
Verified: all four workflow files parse; the parsed `unit` job confirms the
token step no longer runs repo code; the re-render of the changed echo was run
through bash to confirm the backticks still land.
There was a problem hiding this comment.
Chip review — changes requested
Request changes: the repeated write-token, bank-fulfillment sign, and cross-device residence findings remain, and exact-head unit CI now aborts before running tests.
Findings
-
BLOCKING · .github/workflows/tests.yml:451 · PR-authored code can still reach the write token
The listing step runs PR-controlled code in the same job that later exports a contents-writeGH_TOKEN. A same-repository PR can append a malicious tool directory to$GITHUB_PATH; the publish step then resolves commands such asmkdirorgitfrom that directory while the token is present, allowing it to be exfiltrated or used. Put publication in a base-owned privileged workflow that never checks out or executes the PR head, and accept only strictly validated inert artifacts and filenames. -
MAJOR · src/utils/history.utils.ts:342 · Bank-request payments are still signed as incoming
ForP2P_REQUEST_FULFILLwithfulfillmentType: 'bridge'and viewer roleSENDER, the transformer maps tobank_request_fulfillmentand the receipt saysSent to. This entry still returns+, so a completed $10 bank payment renders+$10in both the activity row and receipt even though the viewer paid it. Map this direction to-and add a sender-side bridge-fulfillment sign assertion. -
MAJOR · src/components/Profile/views/ResidenceChangeModal.tsx:98 · Residence swaps still depend on device-local state
readSecondResidencereads only the localStorage mirror, while the durable second slot is not returned by/users/me. A dual resident with server pair ES/FR who opens a new device or clears storage and promotes FR getspreviousSecond = null, soisReorderis false and the request sends onlyresidenceCountry: 'FR'; the server pair becomes FR/FR and ES is still lost. Return the durable second residence to this flow or make the backend swap atomically, and cover the missing-mirror case. -
MAJOR · .github/workflows/tests.yml:180 · Unit CI aborts before running the suite
At this exact head,Fetch BE render-snapshot baselinereceives an emptyGH_TOKEN; the newly addedset -euo pipefailmakesgh apiexit 4, soRun unit testsnever executes and no JUnit or coverage files exist. Provision a credential available to same-repository PR runs or move this baseline fetch behind a trusted artifact-producing workflow, then verify the unit job reaches the test command.
Inline anchors unavailable for 1 finding(s); the findings remain in this summary.
Checked clean
- Earlier workflow-token finding: STILL PRESENT; moving PR-authored listing code to an earlier step does not create a runner trust boundary.
- Earlier bank-request sign finding: STILL PRESENT; sender-side bridge fulfillment still maps to bank_request_fulfillment and receives '+'.
- Earlier residence server-pair fix: FIXED for the device retaining the second-residence mirror, but the broader cross-device dependence remains.
- The latest base-ref shell interpolation in tests, code-analysis, and supply-chain workflows is fixed by passing the ref through environment variables.
- Exact-head format, eslint, typecheck, ds-lint, analyze, human-authors, report, and deploy-preview checks succeeded; unit and aggregate ci-success failed, and advisory ds-shots was still running at review time.
- Exact-head unit logs show the baseline-fetch token is empty and the job exits before any tests run.
- No local Jest run was attempted because the detached worktree has no node_modules; the previous exact-head unit run covered the unchanged application code, and current CI logs were inspected for the workflow-only delta.
Second opinion skipped: daily-budget-reached.
Exact head: 3d3d220ec0e1 · Context: repo, ci
…ead of failing My pipefail change broke the unit job, and the reason is worth keeping. The step reads `secrets.PEANUT_ALL_READ_TOKEN`. No such secret exists in this repo — the one that does is MONO_READ_TOKEN — so GH_TOKEN interpolated to empty and `gh` exited 4. It never surfaced because the pipeline took its status from `base64`, not from `gh`: a failed fetch exited 0 and wrote an empty file, and `import-be-baseline.mjs` turns an empty file into `[]` and exits 0 too. So the step has been reporting success while doing nothing. Nothing downstream noticed because nothing downstream reads it: `be-entries.json` is consumed only by `bakeFromBeEntries()`, which runs under SNAPSHOT_MODE=write. CI runs verify mode against the committed render-baseline.json, so the unit tests were never using the fetched baseline in the first place. Rather than restore the masking, the step is now explicit about both cases: no token is a `::warning::` and a clean skip, a token that fails is a real failure. The import is gated on a step output rather than the file, since hashFiles() resolves inside the workspace and /tmp is not. That leaves a decision for a human, deliberately not made here: either provision the token under the name the workflow expects, or delete the step — as configured it cannot affect a CI run. Checked: the workflow parses, the parsed job shows the gate wired to `steps.be_baseline.outputs.fetched`, and the skip branch was run through bash to confirm it writes the output and exits 0.
There was a problem hiding this comment.
Chip review — changes requested
Request changes: three repeated defects remain. The unit-CI abort is fixed, but the visual job still combines PR code with write access, bank-request fulfillments still display as incoming, and residence swaps still rely on device-local second-residence state.
Findings
-
BLOCKING · .github/workflows/tests.yml:308 · PR-controlled visual job retains repository write access
This job grants contents and pull-request write permissions, then checks out the PR and runs PR-controlled install, build, and test code before placing github.token in a later step. A malicious install script can persist a PATH or GITHUB_ENV hook; when the publish step runs, that hook can read GH_TOKEN and push repository content. A later step in the same job is not an isolation boundary. Run capture in a read-only pull_request workflow and publish validated data from a separate base-controlled workflow_run job that never executes artifact code. -
MAJOR · src/utils/history.utils.ts:342 · Bank-request payer still receives an inflow sign
bank_request_fulfillment is constructed for the P2P_REQUEST_FULFILL sender/payer, and the receipt describes it as "Sent to". Mapping it to '+' makes a completed $25 bank fulfillment appear as +$25 even though the viewer paid it. Map this direction to '-' and update the sign regression test. -
MAJOR · src/components/Profile/views/ResidenceChangeModal.tsx:98 · Residence swaps still depend on device-local state
The swap is detected with readSecondResidence(userId), but that mirror is absent on a fresh device. With durable state declared=ES and second=FR, selecting FR on a new device leaves previousSecond null, so only residenceCountry=FR is sent and the server pair becomes FR/FR, losing ES. Return the second residence from users/me or expose an atomic server reorder operation, pass durable state into the modal, and cover the no-localStorage case.
Inline anchors unavailable for 1 finding(s); the findings remain in this summary.
Checked clean
- Earlier unit-CI abort: FIXED; exact-head unit and aggregate ci-success checks passed.
- Earlier write-token exposure: STILL PRESENT in the ds-shots job.
- Earlier bank-request sign defect: STILL PRESENT in DIRECTION_TO_SIGN.
- Earlier cross-device residence swap defect: STILL PRESENT because reorder detection still reads localStorage.
- The only change since the previous reviewed head 3d3d220 is the baseline-fetch handling in tests.yml; it introduced no new application-code behavior.
Second opinion skipped: daily-budget-reached.
Exact head: f17a276a2721 · Context: repo, github-actions-docs
The fetch pulled the BE's living fixture and nothing read it, so the drift check it was meant to be was never finished: verify mode runs the COMMITTED baseline through the FE transformer, which catches FE-side change but is blind to the backend adding, dropping or reshaping an entry. The committed fixture could sit a year stale and every run stayed green. Adds the missing half — three checks comparing the committed baseline against the fetched `be-entries.json`: - a case the BE publishes that the baseline lacks - a case the baseline still carries that the BE has dropped - a shared case whose wire payload the BE has reshaped Each names the offending case ids and says to re-bake. Comparison is key-order-insensitive so a serializer change on either side isn't reported as drift. `be-entries.json` is gitignored and exists only when CI fetched it, so the block is inert locally and on any run without a cross-repo token: it can add a failure when the real BE data is in hand, never mask one. The reads sit in beforeAll, not the describe body — a skipped describe still evaluates its callback, and a top-level read crashed collection everywhere the file is absent (caught by running it). Verified against a synthesised be-entries.json in all four states: in sync passes; an added arm, a dropped arm and a reshaped entry each fail on their own check with the case id in the message. Still needs a human: the workflow reads secrets.PEANUT_ALL_READ_TOKEN, which does not exist in this repo, so CI cannot fetch and these three skip. Point it at a secret that can read peanut-api-ts and the check goes live — the plumbing is now there to make that worth doing.
PEANUT_ALL_READ_TOKEN has never existed in this repo, which is why the fetch has been dead since May. MONO_READ_TOKEN does exist and is contents:read — exactly what the contents API call here needs, and the same permission it already uses to read peanutprotocol/mono in content-pipeline-watchdog. What is NOT established is its repo scope: every existing use points at mono, so whether it can see peanut-api-ts depends on how the PAT was scoped, and a secret's value can't be inspected. This run is the experiment. The step fails loudly rather than skipping when a token is present and the fetch doesn't work, so a 404 here means the scope doesn't reach api-ts and the name goes back — a one-line revert. If it does work, the three baseline-vs-BE drift checks go live for the first time and the fetch stops being decoration.
There was a problem hiding this comment.
Chip review — changes requested
Request changes: PR-controlled execution still shares jobs with repository-write and cross-repo read tokens, exact-head unit CI still aborts before the suite, and the repeated bank-sign and cross-device residence defects remain.
Findings
-
BLOCKING · .github/workflows/tests.yml:309 · PR code still shares a job with a write token
This remains unchanged after both earlier reviews. The job executes the PR's install/build/visual scripts before later steps receive a contents:write token and a pull-requests:write token. A PR-controlled postinstall can append a tool shim to GITHUB_PATH, then intercept the later token-bearing git invocation; the same trust-boundary problem applies to the comment publisher. Split capture into a read-only job and pass only artifacts to a publisher job that never checks out or executes PR code. -
BLOCKING · .github/workflows/tests.yml:179 · The cross-repo read secret is exposed after PR code runs
Switching this step to the existing MONO_READ_TOKEN makes a repository secret reachable in a job that has already run PR-controlled checkout, postinstall, and typegen code. For example, a malicious postinstall can prepend a wrapper through GITHUB_PATH; when this fetch later invokes gh, the wrapper receives GH_TOKEN and can exfiltrate every private repository the token can read. Fetch the backend fixture in an isolated no-checkout job and transfer it as an artifact to the unprivileged unit job. -
MAJOR · .github/workflows/tests.yml:193 · Unit CI still aborts before running the suite
The exact-head unit job fails in this fetch withgh: Not Found (HTTP 404)andbase64: invalid input; the Run unit tests step is never reached. MONO_READ_TOKEN is present but is not scoped to peanut-api-ts, so the previous early-abort finding is still present under a new failure mode. Use a credential that can read the backend fixture, or remove/vendor this cross-repo fetch, while keeping the fail-loud behavior. -
MAJOR · src/utils/history.utils.ts:342 · Bank-request payers are still shown an inflow sign
The bridge P2P-request strategy emitsbank_request_fulfillmentonly forEHistoryUserRole.SENDER, i.e. the viewer paying the request through bank rails. A completed $25 payment therefore renders as+$25even though the viewer's balance decreased. This repeated finding remains unchanged; map this direction to-and cover the payer case in the sign tests. -
MAJOR · src/components/Profile/views/ResidenceChangeModal.tsx:98 · Residence reorders still depend on device-local state
Reorder detection still reads the second residence only from localStorage. On a fresh device or with blocked storage, a server pair such as primary ES / second FR has no local second value; selecting FR setsisReorderfalse and sends onlyresidenceCountry: FR, so the durable pair is not atomically swapped and the outgoing ES country is lost from the pair. Return/pass the server's second residence or make the backend perform the reorder atomically, then test the missing-local-mirror case.
Inline anchors unavailable for 1 finding(s); the findings remain in this summary.
Checked clean
- Verified HEAD and merge base exactly match the supplied head and base SHAs.
- Reassessed all earlier findings: the visual write-token, bank-sign, residence, and unit-suite findings are still present.
- Exact-head unit CI failed before tests because MONO_READ_TOKEN received HTTP 404 from the peanut-api-ts contents endpoint; other exact-head jobs were still running.
- Compared the new drift-check logic against the canonical 49-entry backend fixture: no missing, extra, or reshaped cases were reported.
- Reviewed the three-commit delta since 3d3d220; outside the workflow and render-snapshot test, the repeated behavior defects were unchanged.
Second opinion skipped: daily-budget-reached.
Exact head: b79e429cb938 · Context: repo, ci, product, backend
Two findings, and the first one is mine from an hour ago.
MONO_READ_TOKEN is not scoped to peanut-api-ts — Chip caught the exact-head run
failing with `gh: Not Found (HTTP 404)`. So the rename didn't work. Worse, it
made things actively worse than the broken state it replaced: the old name
referred to a secret that does not exist, so nothing was ever exposed. Wiring a
REAL secret into the unit job put it in reach of PR-authored code, which runs
checkout, postinstall and typegen in that job long before the fetch. A
malicious postinstall can prepend a `gh` wrapper via GITHUB_PATH and read
GH_TOKEN — every private repo that token can see. Trying the credential in
place was the wrong way to test the theory.
The fetch now lives in its own job: `permissions: {}`, no checkout, no install,
no repo code at all. It hands the fixture to `unit` as an artifact — data, not
credentials — so the unit job holds no secret in any step. That is the
isolation Chip asked for, and unlike the workflow_run split it can be exercised
on this PR rather than only after it merges.
Non-blocking where it should be: no token and a token that can't read the
fixture both warn and produce no artifact, the import step no-ops, and the
three drift checks skip. A token that CAN read but returns nothing still fails
loudly (`test -s`). Provision a contents:read credential for peanut-api-ts and
the check goes live with no other change.
Both skip paths were run through bash. The parsed workflow confirms
be-baseline has no checkout step and unit has no secret in any step env.
There was a problem hiding this comment.
Chip review — changes requested
Request changes: the isolated backend-baseline job fixes the cross-repo secret exposure and unit-suite abort, but the repeated visual write-token exposure, bank-payment sign, and cross-device residence defects remain.
Findings
-
BLOCKING · .github/workflows/tests.yml:333 · PR code still shares a job with a write token
Still present after two earlier reviews. This job grants contents and pull-request write access, runs PR-controlled install/build/visual code, and later starts a shell with GH_TOKEN. A malicious postinstall can persist BASH_ENV or a GITHUB_PATH shim, then execute when the publish step receives the token and use it with repository-write authority. Split capture into a read-only job and pass validated artifacts to a publisher job that never checks out or executes PR code. -
MAJOR · src/utils/history.utils.ts:342 · Bank-request payers are still shown an inflow sign
Still present from prior reviews. The bridge P2P-request strategy emits bank_request_fulfillment only when the viewer role is SENDER, meaning the viewer paid the request through bank rails. This mapping still returns '+', so a completed $25 payment renders as +$25 even though the viewer's balance decreased. Map bank_request_fulfillment to '-' and cover the sender-plus-bridge sign case. -
MAJOR · src/components/Profile/views/ResidenceChangeModal.tsx:98 · Residence reorders still depend on device-local state
Still present from prior reviews. Reorder detection reads the second residence only from localStorage. On a fresh device, a durable server pair ES/FR has no local second value; selecting FR makes isReorder false and sends only residenceCountry=FR, so the pair can become FR/FR and lose ES. Return and pass the server's second residence or make the reorder atomic server-side, then cover the missing-local-mirror case.
Inline anchors unavailable for 1 finding(s); the findings remain in this summary.
Checked clean
- Verified detached worktree HEAD, merge base, trusted author, base ref, and base SHA match the supplied values.
- Earlier cross-repo read-secret finding: FIXED; the fetch now runs in a permissions-empty job with no checkout, install, or PR-authored execution and transfers only an artifact.
- Earlier unit-CI abort finding: FIXED; a missing baseline artifact now skips only the drift import, and the exact-head unit and aggregate CI checks succeeded.
- Earlier visual write-token, bank-sign, and residence-reorder findings: STILL PRESENT and re-reported above.
- The exact-head format, ds-lint, eslint, typecheck, unit, report, human-authors, analyze, and aggregate checks succeeded; advisory ds-shots and the deploy preview were still running when reviewed.
- Focused local Jest execution was unavailable because the detached worktree has no installed dependencies; the relevant current tests and exact-head unit result were inspected instead.
Second opinion skipped: daily-budget-reached.
Exact head: 0797cee325f2 · Context: repo, ci
…e device No backend change was needed after all — I was wrong about that. /users/me has returned `residence.declaredSecond` since 2026-08-26 (peanut-api-ts, the same review round as the onboarding work): the handler computes it via secondDeclaredResidenceOf and the response schema declares it, so Fastify serializes it today on dev/staging. #2834's comment — "device mirror until the API returns it" — was written before that landed and was never revisited. So the swap now reads the durable server value and falls back to the device mirror only where it is absent: the window before that BE reaches production (it is on dev, not yet on main), or a stale cached user. That closes Chip's fresh-device case, where the mirror does not exist and the reorder wiped the outgoing country. Two tests: a swap with no mirror at all, and a mirror holding a stale country the server disagrees with. Both assert the request body, since asserting the mirror is what let the earlier half-fix look green. Note the FE OpenAPI snapshot does NOT carry declaredSecond — it was hand-carried from #2834 and predates the field. The hand-written ApiUser type is what the app reads, so it is typed there; regenerating the snapshot against a live BE is worth doing separately.
…ite token Chip, BLOCKING. The job declared contents:write + pull-requests:write, then checked out the PR and ran its install, build and capture code before a later step took github.token. A later step in the same job is not an isolation boundary: a postinstall can plant a GITHUB_PATH or GITHUB_ENV hook that fires in any step after it, read the token, and push to the repo or act on PRs. Forks are already blocked by the head-repo filter, so this is the same-repo case — which is precisely what the job exists to run. Permissions are now contents:read, and the two steps that needed more are gone: the pr-assets branch push and the github-script PR comment. The diff travels as a `visual-diff-<pr>` artifact (base/head/diff, 7 days) plus the job summary that was already being written. `visual-comment.mjs` omits its <img> tags when --assets is empty — verified by running it — so the summary is the same table, minus inline images. Reviewers click into the run instead of seeing them in the conversation. This is the cheap half of the fix, chosen because it is the only version that can be exercised before merging: the correct pull_request/workflow_run split publishes from the DEFAULT branch, which is main, so a publisher added on a dev-targeted PR would not fire until after a production release — and would leave no comments at all in the meantime. That split is filed as its own task. pr-assets-cleanup.yml is kept but marked dormant: it deletes a branch nothing creates now, and the split restores it. Its header says so rather than leaving a workflow whose comment describes something that no longer happens. Verified from the parsed workflow: ds-shots holds contents:read only, no step carries a secret in env, and no github-script step remains.
…alization) One conflict, the usual shape: dev's #2877 localized the country name in the local-rail nudge while this branch had already moved the component off the deleted InfoCard onto Notification. Kept both — DS structure, dev's behavior: the nudge renders as `Notification priority="info"` and passes `iso2` plus `localizedCountryName(locale, iso2, local.countryName)`, which the imports and `useLocale()` on this side already supported. pt-BR.json auto-merged. Gates: typecheck clean - eslint 0 errors - prettier clean - ds-lint green - 499 tests across TransactionDetails and i18n, including dev's 8 LocalRailNudge tests unchanged.
There was a problem hiding this comment.
Chip review — changes requested
Request changes: the visual-job write-token exposure is fixed, but bank-request payments still carry the wrong sign and two local-storage fallbacks still override or ignore the durable second residence.
Findings
-
MAJOR · src/utils/history.utils.ts:342 · Bank-request payers are still shown an inflow sign
This repeated direction bug remains. The bridge request strategy emitsbank_request_fulfillmentonly foruserRole: SENDER, and the receipt labels it as sent, but this table returns+. A payer completing a $25 bank request therefore sees+$25even though they paid it. Map this direction to-and add a direct sign regression case forbank_request_fulfillment. -
MAJOR · src/components/Profile/views/UnlockPayments.view.tsx:488 · An explicit no-second-residence response revives stale device data
/users/meserializesdeclaredSecondas either a country ornull, but this??treats authoritativenulllike an absent pre-deploy field. If the server hasES/nullwhile this device still storesFR, selecting France passes the staleFRinto the modal; the save is classified as a reorder and persistsESas a second residence that the server said did not exist. Fall back only when the property isundefined, clear/sync the mirror when the server field is present, and cover the null-server plus stale-mirror case. -
MAJOR · src/hooks/useResidenceRestrictions.ts:70 · Fresh devices ignore the server's second residence for availability
The hook still reads the second country only from localStorage even though/users/menow returnsresidence.declaredSecond. For an unverified dual-resident account with primaryRU, secondBR, and empty device storage, the server primary restriction is{banking:true, card:true}; this early return skips the intended intersection with unrestrictedBR, so both offers remain hidden on a fresh device. Prefer the server field and use the mirror only when that field is undefined, with a server-second/no-mirror regression test.
Inline anchors unavailable for 1 finding(s); the findings remain in this summary.
Checked clean
- Verified the detached worktree HEAD, merge base, trusted author, base ref, and base SHA against the supplied values.
- Earlier visual write-token finding: FIXED — the PR-controlled ds-shots job now has contents:read only and emits artifacts plus a job summary instead of pushing a branch or commenting.
- Earlier bank-request sign finding: STILL PRESENT and re-reported above; the sender-only strategy and outbound receipt wording refute the plus sign.
- Earlier fresh-device residence reorder case: FIXED for a non-null server pair, with request-body coverage; the distinct authoritative-null and availability consumers above remain device-dependent.
- Checked the merged backend contract: residence.declaredSecond is serialized as string-or-null, while residenceRestrictions is derived without the second country.
- Exact-head GitHub checks exposed the completed trusted review and Vercel checks plus skipped manual/ping jobs; required unit, lint, typecheck, and aggregate checks had not registered, so they were not relied on.
- Targeted Jest execution was unavailable because dependencies are absent from the detached worktree; no install was performed in the read-only review tree.
- Diff whitespace validation passed.
Second opinion skipped: daily-budget-reached.
Exact head: 54aab88b4ee8 · Context: repo, ci, peanut-api-ts
…ield Both from Chip, both mine from an hour ago, and both the same root mistake: `??` cannot tell "the server said there is no second residence" from "this API predates the field". UnlockPayments passed `residence?.declaredSecond ?? mirror`, so a server pair of ES/null on a device still holding FR handed FR to the modal — the save was then classified as a reorder and wrote ES back as a second residence the server had explicitly disowned. Only `undefined` falls back now. The mirror is also re-synced to the server's answer, clearing included, because it is read elsewhere and a disowned country left there keeps shaping availability. useResidenceRestrictions had the older half of the same bug: it read the second country from localStorage only, so on a fresh device the intersection was skipped entirely. For an unverified dual resident with primary RU and second BR, that leaves both bank and card offers hidden from someone entitled to them — the restriction is meant to lift, since they can verify under the unrestricted jurisdiction. It now prefers the server field, mirror only when undefined, with declaredSecond in the memo deps. Two regression tests, both checked against a reverted hook first: server-second with no mirror, and explicit null with a stale mirror. They fail without the change. Also confirmed stale, not fixed: Chip's 21:18 "Unit CI still aborts" thread describes the MONO_READ_TOKEN head, two commits back. Every check is green on the current head.
There was a problem hiding this comment.
Chip review — changes requested
Request changes: payer-side bank-request fulfillments still render as inflows, and the newest residence-authority fix is not propagated to the Unlock screen's region grouping.
Findings
-
MAJOR · src/utils/history.utils.ts:328 · Bank-request payers are still shown an inflow
This earlier finding is still present. For a P2P_REQUEST_FULFILL with fulfillmentTypebridgeand userRole SENDER, the strategy emitsbank_request_fulfillment, but this map returns+. A payer whose balance decreases therefore sees+$10in history and the receipt. Map this direction to-and add a payer-side sign regression test. -
MINOR · src/components/Profile/views/UnlockPayments.view.tsx:227 · Unlock groups still use the device-only second residence
The newdeclaredSecondIso2correctly resolves server-versus-mirror authority, but the group model still receivessecondResidenceIso2. On a fresh device with serverdeclaredSecond: 'BR', South America is not tagged or sorted as Your region; with explicit null plus a stale mirror, the disowned region remains tagged until remount. PassdeclaredSecondIso2here and into the Europe check, and update the memo dependencies.
Checked clean
- Earlier workflow finding FIXED: the PR-executing ds-shots job now has contents: read only, with PR publication isolated from PR-authored code.
- Earlier UnlockPayments mutation finding FIXED: explicit null no longer falls through to the stale mirror for the residence-change modal, and the mirror is cleared from the server answer.
- Earlier useResidenceRestrictions fresh-device finding FIXED: the hook now consumes the server's declaredSecond field and covers both fresh-device and explicit-null cases.
- Exact-head required CI is green, including unit, eslint, typecheck, format, ds-lint, report, and ci-success; advisory ds-shots and deploy preview were still running at review time.
- The exact head and merge base matched the supplied SHAs, the detached worktree was clean, and git diff --check passed.
- Traced the payer-side P2P_REQUEST_FULFILL strategy and adversarially verified the remaining sign failure; reviewed the tip residence commit for authority, stale-state, and memoization paths.
Second opinion skipped: daily-budget-reached.
Exact head: 06550b08beaa · Context: repo, ci
…e server second MAJOR, and mine: implementing the "+" ruling I flipped all six inbound-looking directions without checking each one's semantics. `bank_request_fulfillment` is not inbound — p2p-send emits it for the SENDER of a bridge-fulfilled request, and its own comment says "Viewer is paying via bank rails". So a payer watching their balance drop was shown `+$10` in history and on the receipt. Now `-`, with a payer-side sign test. Worth noting the map was already wrong on dev (`+`); the DS branch had it unsigned, which hid it. Restoring dev's signs restored the bug in visible form. MINOR: my earlier fix resolved server-vs-mirror authority into `declaredSecondIso2` but only passed it to the modal. The group model still took the raw mirror, so on a fresh device South America was not tagged "Your region", and a server `null` left a disowned region tagged until remount. The groups and the Europe check now take the resolved value, with the memo dep updated. Gates: typecheck clean - eslint 0 errors - prettier clean - ds-lint green - 2335 tests across utils, hooks, Profile and TransactionDetails.
There was a problem hiding this comment.
Chip review — no blocking findings — this is not an approval
All supplied earlier findings are fixed at this head, and no new actionable defect remains in the follow-up changes.
Checked clean
- FIXED — bank_request_fulfillment now maps the payer-side transaction to an outflow sign, with pending and completed regression assertions.
- FIXED — Unlock payments groups and the Europe residence check now consume the server-resolved second residence instead of the device-only mirror.
- FIXED — an explicit server null for declaredSecond remains authoritative and no longer revives stale device data.
- FIXED — useResidenceRestrictions now intersects availability with the server's declaredSecond on fresh devices, with coverage for both a server value and explicit null.
- Correctness and adversarial pass traced the bank-request direction from the transaction transformer through history and receipt sign rendering; the payer receives a minus sign in reachable pending and completed states.
- Security pass confirmed the PR-executing visual job remains read-only and the current follow-up commit introduces no workflow or privilege change.
- Exact-head required CI passed: ci-success, unit, typecheck, format, ESLint, ds-lint, human-authors, and analysis are green.
- The advisory ds-shots job failed before build or browser tests because the hosted runner received 403 responses from packages.microsoft.com while installing Playwright system dependencies; this is external to the PR behavior and does not gate ci-success.
- Local targeted tests could not start because the supplied detached worktree has no node_modules; exact-head unit CI completed successfully instead.
- Slop pass on the follow-up commit found the authority-resolution logic centralized and its memo dependencies updated; no dead or duplicate behavior was introduced.
Second opinion skipped: daily-budget-reached.
Exact head: 68fb6b1abc0f · Context: repo
Design system + registration/onboarding, integrated
Lands both in-flight frontend efforts on
devtogether, in the order that pays the migration cost once:feat/design-system— the DS release (680 files: tokens + tailwind-4, component consolidation, page rebuilds).feat/app-registration-onboarding— residence step, Unlock payments, home checklist, region-restricted screens.Merging them separately does not work in either order: whichever lands second has to be re-migrated, and
ds-lintblocks #2834 outright once the DS ratchet is ondev. Both source PRs stay open and close when this merges.Conflict resolution
52 conflict hunks over 40 files. Doctrine throughout, the same one #2813 used for its own dev merges: DS structure wins, behavior is re-applied, never dropped.
The ones worth a reviewer's time:
TransactionDetailsReceipt.tsxdevkept extending the old file. Took the decomposition, re-applied dev's TASK-19830 public-receipt work onto it: official header/footer,issuedAt, and the PDF link moved intoReceiptActionswhere actions now live.useReceiptViewModel.tsmeetsShareConditions/shouldShowDownloadPdf. Deleted dev's per-row border machinery: the DS details card owns dividers viadivide-y divide-dashed, soshouldHideBorder/shouldHideGroupBorderhad no consumer left.TransactionCard.tsxListItemrebuild, plus dev'sgetActionIcon(type, status)signature and inbound-arrowrequestrow.home/page.tsx<HomePage />infeatures/home/). #2834's home work lives inActivationCTAs, whichHomePagealready renders — nothing lost.Profile/index.tsxListGroups. Payment limits leave the menu (folded inline into Unlock payments), name visibility moves to/profile/edit. The unlock row keeps the DS badge chip over #2834's pulsing dot — one attention language per screen.UnlockPayments.view.tsxLimitsProgressBaris deleted on the DS line (one0_Bruddle/ProgressBarprimitive); ported to it withgetLimitColorClass, asMantecaLimitsViewalready does.CryptoDeposit.view.tsxcapabilities.tsreason/canRetrywere declared twice after the auto-merge — kept the richerCapabilityReasonform.api.openapi.json/api.generated.ts/config/residence-restrictions,/manteca/initiate-onboardingand/user/crisp-tokenadded — andpnpm gen:apiregenerates byte-identical, socheck:apipasses.Two bugs the merge surfaced, fixed rather than papered over
ListItemrebuild ofTransactionCardbroke themin-w-0shrink chain, so a long counterparty name could push the amount off the row — the exact regression TASK-20700 fixed ondev. Caught by dev's own test once it was re-pointed at the new structure.history.utils.test.tsended up holding both branches' expectations for the amount sign (DS's "incoming sign-less" and dev's "inflow sign") — a state no single branch ever had. Now consistent with the ruling below.Two product rulings (Slava, 2026-08-28)
+. The states board (17966:12128) draws them unsigned; a signed inflow reads faster in a mixed feed, so dev's TASK-21887 behavior wins. Plain text, not green. Outgoing keeps-; cancelled / failed / refunded stay struck through and sign-less. The board now disagrees with the code and needs updating.AvatarWithBadgedraws initials on — 875 combinations, up from 625, and no raw hex left in the component. Accepted trade-off: the self-avatar no longer reads as visually distinct from the counterparty initials circles beside it.DS debt: every metric at or below the #2813 baseline
#2834's screens arrived on the legacy palette. Migrated rather than baselined:
Two
HEX_ALLOWentries rather than baseline bumps: the@react-pdf/rendererreceipt (itsStyleSheettakes no tailwind tokens) andlayout.tsx's Next viewportthemeColor, which must be a literal.classNameSitesInPagesmoves 313 → 314, recorded through--allow-increasewith a reason: the receipt page keeps dev'sprint:hiddennav wrapper for the PDF receipt.Also on the way through:
useResidenceRestrictionSetswas callingfetchWithSentry(PEANUT_API_URL…)directly, tripping the DS boundary gate that did not exist when #2834 was written — routed throughapiFetchwithincludeAuth: false. The two transport probes that also trip it (native-canary,network-triage) are exempted with reasons: a bare fetch is the instrument in both, andnetwork-triageneeds cors/no-cors modesapiFetchcannot express.Gates, run locally at the tip
typecheck clean · prettier clean · eslint clean on touched files ·
ds-lint --checkgreen ·next buildexit 0 · 4,054 tests across 331 suites (3 skipped), coveringsrc/utils,src/hooks,src/components,src/features,src/context,src/appandsrc/i18n.What still needs a human
+on incoming; and the avatar-vs-initials distinction called out above is a design call that can still be reversed here.