Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
e903b4d
fix(seo): content-gate loaders + receive-from sources to kill live 404s
0xkkonrad May 27, 2026
32699f1
fix(badges): restore event badge catalog entries dropped in BADGES re…
0xkkonrad Jun 1, 2026
52412dc
Merge pull request #2153 from peanutprotocol/hotfix/event-badge-catal…
Hugo0 Jun 1, 2026
d18819d
feat(observability): attach user to Sentry alongside posthog.identify
Hugo0 Jun 1, 2026
3f0f757
feat(observability): scrubber + Sentry beforeSend backstop + setup-fl…
Hugo0 Jun 1, 2026
994bed3
chore(observability): plug PII gaps, lock onchain addresses unredacte…
Hugo0 Jun 1, 2026
e4bdcbd
style: null-coalesce email in setSentryUser for consistency (CodeRabb…
Hugo0 Jun 1, 2026
983d3c2
security(observability): prototype-pollution defense in scrubObject (…
Hugo0 Jun 1, 2026
729bce2
style: prettier --write src/utils/__tests__/sentry.utils.test.ts
Hugo0 Jun 1, 2026
0543fac
security(observability): use Object.defineProperty in scrubObject (Co…
Hugo0 Jun 1, 2026
c88fbc6
Merge pull request #2149 from peanutprotocol/feat/fe-sentry-user
Hugo0 Jun 1, 2026
e5fd6e4
fix(capabilities): hoist `ready` above blocked/accept-tos/fixable in …
Hugo0 Jun 1, 2026
1fca0ac
Merge pull request #2157 from peanutprotocol/fix/gate-prefer-ready-rail
Hugo0 Jun 1, 2026
6e358af
feat(badge): Skip Pass — bypass the waitlist via /invite?campaign=skip
0xkkonrad May 27, 2026
7911b3d
chore(invite): TODO-flag the WET skip flow for card-beta→open cleanup
0xkkonrad May 28, 2026
d314a30
fix(invite): drop unused useCallback import (eslint)
Hugo0 Jun 1, 2026
94bb18a
Merge pull request #2158 from peanutprotocol/feat/skip-waitlist-badge…
Hugo0 Jun 1, 2026
ad91622
refactor(invite): fold the Skip Pass path into the shared claim flow
Hugo0 Jun 1, 2026
722e07c
Merge pull request #2159 from peanutprotocol/chore/invite-page-skip-c…
Hugo0 Jun 1, 2026
d8b7fe1
fix(saved-accounts): include Manteca accounts in useSavedAccounts
Hugo0 Jun 1, 2026
697be26
hotfix(kyc): derive region intent from destination country in bank flows
jjramirezn Jun 2, 2026
69ddaa5
fix(kyc): drop legacy 'STANDARD' Bridge-rail marker in cross-region c…
jjramirezn Jun 2, 2026
53398d1
test(add-money): mock new transitive imports from regions.utils
jjramirezn Jun 2, 2026
1d3e07c
Merge pull request #2163 from peanutprotocol/hotfix/kyc-region-intent…
Hugo0 Jun 2, 2026
2c88c0e
Merge remote-tracking branch 'origin/main' into hotfix/manteca-saved-…
Hugo0 Jun 2, 2026
b331412
fix(saved-accounts): import AccountType from @/interfaces/interfaces …
Hugo0 Jun 2, 2026
56e5c08
feat(card): host Rain card legal pages + wire onboarding links
Hugo0 Jun 1, 2026
ccd79ac
Merge pull request #2160 from peanutprotocol/hotfix/manteca-saved-acc…
Hugo0 Jun 2, 2026
81b4653
chore(content): bump content submodule to publish card legal pages
Hugo0 Jun 2, 2026
53e9d40
Merge pull request #2143 from peanutprotocol/feat/card-legal-pages
Hugo0 Jun 2, 2026
5897fa5
fix(exchange-rate): short-circuit non-Bridge AccountTypes (PEANUT-UI-…
Hugo0 Jun 2, 2026
b540719
Merge pull request #2169 from peanutprotocol/hotfix/exchange-rate-non…
Hugo0 Jun 2, 2026
73dfae3
Merge pull request #2167 from peanutprotocol/chore/bump-content-card-…
Hugo0 Jun 2, 2026
4e128ff
Merge pull request #2118 from peanutprotocol/fix/seo-loader-content-g…
Hugo0 Jun 3, 2026
66600a1
chore(content): bump content submodule to publish polished card legal…
Hugo0 Jun 3, 2026
d453068
Merge pull request #2176 from peanutprotocol/chore/bump-content-polished
Hugo0 Jun 3, 2026
98713da
chore: back-merge main into dev (2026-06-03 hotfixes)
Hugo0 Jun 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions public/badges/skip_pass.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
15 changes: 13 additions & 2 deletions scripts/verify-content.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,17 @@ function listDirs(dir: string): string[] {
.map((d) => d.name)
}

/**
* Receive-money-from pages render only for corridor origins that actually have
* a receive-from article. Mirrors RECEIVE_SOURCES in src/data/seo/corridors.ts.
* Without this gate, both the route index and the sitemap "expected URLs" would
* agree with each other on a slug that 404s at runtime (e.g. colombia, mexico).
*/
function gateReceiveSources(corridors: Array<{ from: string; to: string }>): string[] {
const origins = [...new Set(corridors.map((c) => c.from))]
return origins.filter((slug) => fs.existsSync(path.join(CONTENT_DIR, 'receive-from', slug, 'en.md')))
}

function getAllMdFiles(dir: string): string[] {
const results: string[] = []
if (!fs.existsSync(dir)) return results
Expand Down Expand Up @@ -192,7 +203,7 @@ function discoverRoutes(): Set<string> {
corridors.push({ to: dest, from: origin })
}
}
const receiveSources = [...new Set(corridors.map((c) => c.from))]
const receiveSources = gateReceiveSources(corridors)

// Check which routes actually have page.tsx files
const hasRoute = (routePath: string) => {
Expand Down Expand Up @@ -704,7 +715,7 @@ function expectedSitemapUrls(): string[] {
const fromDir = path.join(CONTENT_DIR, 'send-to', dest, 'from')
for (const origin of listDirs(fromDir)) corridors.push({ from: origin, to: dest })
}
const receiveSources = [...new Set(corridors.map((c) => c.from))]
const receiveSources = gateReceiveSources(corridors)

for (const locale of SUPPORTED_LOCALES) {
for (const slug of countrySlugs) {
Expand Down
223 changes: 219 additions & 4 deletions sentry.utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,13 +82,228 @@ export function shouldIgnoreError(event: ErrorEvent): boolean {
}

/**
* Clean sensitive headers from events
* Defense-in-depth: even when call-site code (fetchWithSentry, etc) has
* already scrubbed payloads, walk every Sentry event one more time and
* redact known sensitive headers, fields, and breadcrumb data before it
* leaves the browser. Catches the long tail of errors that come from
* places we don't control (third-party SDKs, error boundaries, console
* spam) and might carry PII / card data / passwords in `extra`.
*
* What stays unredacted: userId, username, email, inviteCode — identity
* fields already shared with PostHog and intentionally queryable in
* Sentry too.
*/
const SENSITIVE_HEADERS = [
'authorization',
'cookie',
'set-cookie',
'x-auth-token',
'api-key',
'x-api-key',
'apikey',
'md-api-key',
'x-app-token',
'x-app-access-sig',
'x-app-access-ts',
]

/**
* IMPORTANT: EXACT-MATCH set. We deliberately do NOT substring-match
* because Peanut has first-class onchain addresses everywhere (walletAddress,
* recipientAddress, tokenAddress, sdaAddress, depositAddress, …) and those
* are public chain data that must stay visible. Substring on `address`
* would clobber every one. Same for `pin` / `token` / `seed` — share names
* with non-sensitive concepts.
*/
const SENSITIVE_KEYS = new Set([
// Secrets
'password',
'pwd',
'passphrase',
'secret',
'secretkey',
'apikey',
'apitoken',
'bearer',
'authtoken',
'jwt',
'token',
'sessiontoken',
'refreshtoken',
'accesstoken',
'idtoken',
'privatekey',
'mnemonic',
'seed',
'seedphrase',
'recoveryphrase',
// Card data
'pan',
'cardnumber',
'cvv',
'cvc',
'securitycode',
'cardpin',
'pin',
'cardholdername',
'expirydate',
'expirymonth',
'expiryyear',
'expmonth',
'expyear',
// Government IDs (English + Bridge long-form)
'ssn',
'socialsecurity',
'socialsecuritynumber',
'taxid',
'taxidentificationnumber',
'tin',
'dni',
'cuit',
'cuil',
'rfc',
'curp',
'nif',
'governmentid',
'governmentidnumber',
'documentnumber',
'passport',
'passportnumber',
'driverslicense',
'licensenumber',
'idnumber',
'nationalid',
'nationalidnumber',
// Manteca (Spanish)
'documento',
'numerodocumento',
'numerodedocumento',
// Bank accounts
'iban',
'swift',
'bic',
'sortcode',
'routingnumber',
'accountnumber',
'bankaccountnumber',
'cbu',
'cvu',
'clabe',
// PII — names (English + Manteca Spanish)
'firstname',
'lastname',
'fullname',
'givenname',
'familyname',
'surname',
'middlename',
'mothername',
'mothersmaidenname',
'maidenname',
'customerfirstname',
'customerlastname',
'nombre',
'apellido',
// PII — address. NOTE: `address` alone is NOT here — onchain addresses
// (walletAddress, recipientAddress, etc.) must stay visible for
// debugging onchain flows.
'streetaddress',
'street1',
'street2',
'street3',
'streetline1',
'streetline2',
'streetline3',
'addressline1',
'addressline2',
'addressline3',
'billingaddress',
'homeaddress',
'mailingaddress',
'residentialaddress',
'permanentaddress',
'direccion',
'domicilio',
'postalcode',
'zipcode',
'zip',
'postcode',
'dob',
'dateofbirth',
'birthdate',
'birthday',
'phonenumber',
'mobilenumber',
'telephone',
'telefono',
// 2FA
'otp',
'verificationcode',
'totpsecret',
'twofactor',
'twofactorsecret',
])

function isSensitiveKey(key: string): boolean {
return SENSITIVE_KEYS.has(key.toLowerCase().replace(/[_-]/g, ''))
}

function scrubObject(value: unknown, depth = 0): unknown {
if (depth > 10) return '[REDACTED: max depth]'
if (value === null || value === undefined) return value
if (typeof value !== 'object') return value
if (Array.isArray(value)) return value.map((item) => scrubObject(item, depth + 1))
// Prototype-pollution defense — see src/utils/sentry.utils.ts for the
// full rationale. Object.create(null) + Object.defineProperty + explicit
// dangerous-key skip. The defineProperty form is what CodeQL recognises
// as a sanitizer; direct `out[key] = …` triggers the alert even when
// keys are validated at runtime.
const out: Record<string, unknown> = Object.create(null)
for (const [key, val] of Object.entries(value as Record<string, unknown>)) {
if (key === '__proto__' || key === 'constructor' || key === 'prototype') continue
Object.defineProperty(out, key, {
value: isSensitiveKey(key) ? '[REDACTED]' : scrubObject(val, depth + 1),
writable: true,
enumerable: true,
configurable: true,
})
}
return out
}

/**
* Clean sensitive headers, extras, request data, and breadcrumbs from events
* before they leave the browser.
*/
export function cleanSensitiveHeaders(event: ErrorEvent): void {
if (event.request?.headers) {
delete event.request.headers['Authorization']
delete event.request.headers['api-key']
delete event.request.headers['cookie']
for (const key of Object.keys(event.request.headers)) {
if (SENSITIVE_HEADERS.includes(key.toLowerCase())) {
event.request.headers[key] = '[REDACTED]'
}
}
}
if (event.request?.data) {
event.request.data = scrubObject(event.request.data)
}
if (event.extra) {
event.extra = scrubObject(event.extra) as Record<string, unknown>
}
if (event.contexts) {
for (const [key, value] of Object.entries(event.contexts)) {
if (key === 'trace') continue
;(event.contexts as Record<string, unknown>)[key] = scrubObject(value)
}
}
if (event.breadcrumbs) {
event.breadcrumbs = event.breadcrumbs.map((crumb) => ({
...crumb,
data: crumb.data
? (Object.fromEntries(
Object.entries(crumb.data).map(([k, v]) => [k, isSensitiveKey(k) ? '[REDACTED]' : scrubObject(v)])
) as Record<string, unknown>)
: crumb.data,
}))
}
}

Expand Down
6 changes: 4 additions & 2 deletions src/app/(mobile-ui)/add-money/[country]/bank/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
import { ANALYTICS_EVENTS } from '@/constants/analytics.consts'
import { addMoneyCountryUrl } from '@/utils/native-routes'
import { useSafeBack } from '@/hooks/useSafeBack'
import { getRegionIntent } from '@/utils/regions.utils'

// Step type for URL state
type BridgeBankStep = 'inputAmount' | 'showDetails'
Expand All @@ -62,7 +63,7 @@
// Local UI state (not URL-appropriate - transient)
const [showWarningModal, setShowWarningModal] = useState<boolean>(false)
const [showKycModal, setShowKycModal] = useState<boolean>(false)
const [isRiskAccepted, setIsRiskAccepted] = useState<boolean>(false)

Check failure on line 66 in src/app/(mobile-ui)/add-money/[country]/bank/page.tsx

View workflow job for this annotation

GitHub Actions / eslint

'isRiskAccepted' is assigned a value but never used. Allowed unused elements of array destructuring must match /^_/u

Check failure on line 66 in src/app/(mobile-ui)/add-money/[country]/bank/page.tsx

View workflow job for this annotation

GitHub Actions / eslint

'isRiskAccepted' is assigned a value but never used. Allowed unused elements of array destructuring must match /^_/u
const { setError, error, setOnrampData, onrampData } = useOnrampFlow()

const { balance } = useWallet()
Expand All @@ -71,7 +72,8 @@

// inline sumsub kyc flow for bridge bank onramp
// regionIntent is NOT passed here to avoid creating a backend record on mount.
// intent is passed at call time: handleInitiateKyc('STANDARD')
// intent is passed at call time, derived from the destination country
// (e.g. /add-money/usa → NA → bridge-requirements).
const sumsubFlow = useMultiPhaseKycFlow({
onKycSuccess: () => {
setUrlState({ step: 'inputAmount' })
Expand Down Expand Up @@ -119,7 +121,7 @@

useEffect(() => {
fetchUser()
}, [])

Check warning on line 124 in src/app/(mobile-ui)/add-money/[country]/bank/page.tsx

View workflow job for this annotation

GitHub Actions / eslint

React Hook useEffect has a missing dependency: 'fetchUser'. Either include it or remove the dependency array

Check warning on line 124 in src/app/(mobile-ui)/add-money/[country]/bank/page.tsx

View workflow job for this annotation

GitHub Actions / eslint

React Hook useEffect has a missing dependency: 'fetchUser'. Either include it or remove the dependency array

const peanutWalletBalance = useMemo(() => {
return balance !== undefined ? formatAmount(formatUnits(balance, PEANUT_WALLET_TOKEN_DECIMALS)) : ''
Expand Down Expand Up @@ -418,7 +420,7 @@
await sumsubFlow.handleSelfHealResubmit('BRIDGE')
} else {
await sumsubFlow.handleInitiateKyc(
'STANDARD',
getRegionIntent(selectedCountry?.region ?? 'rest-of-the-world'),
undefined,
gate.kind === 'needs-enrollment' || undefined
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,7 @@ jest.mock('@/constants/countryCurrencyMapping', () => ({
],
isNonEuroSepaCountry: jest.fn(() => false),
isUKCountry: jest.fn(() => false),
getFlagUrl: jest.fn((iso2: string) => `/flags/${iso2}.svg`),
}))

// Rhino consts
Expand Down Expand Up @@ -681,6 +682,7 @@ jest.mock('@/components/AddMoney/consts', () => ({
{ type: 'country', id: 'XX', path: 'unknown', currency: 'USD', iso3: 'XXX' },
],
ALL_COUNTRIES_ALPHA3_TO_ALPHA2: { ARG: 'AR', BRA: 'BR', USA: 'US', DEU: 'DE', MEX: 'MX', GBR: 'GB' },
BRIDGE_ALPHA3_TO_ALPHA2: { USA: 'US', DEU: 'DE', MEX: 'MX', GBR: 'GB' },
}))

jest.mock('@/components/TransactionDetails/transactionTransformer', () => ({}))
Expand Down
4 changes: 2 additions & 2 deletions src/app/(mobile-ui)/withdraw/[country]/bank/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ import { getKycModalVariant, getGateUserMessage } from '@/utils/capability-gate'
import { useModalsContext } from '@/context/ModalsContext'
import ExchangeRate from '@/components/ExchangeRate'
import countryCurrencyMappings, { isNonEuroSepaCountry } from '@/constants/countryCurrencyMapping'
import { isBridgeSupportedCountry } from '@/utils/regions.utils'
import { isBridgeSupportedCountry, getRegionIntent } from '@/utils/regions.utils'
import { PointsAction } from '@/services/services.types'
import { usePointsCalculation } from '@/hooks/usePointsCalculation'
import { parseUnits } from 'viem'
Expand Down Expand Up @@ -533,7 +533,7 @@ export default function WithdrawBankPage() {
await sumsubFlow.handleSelfHealResubmit('BRIDGE')
} else {
await sumsubFlow.handleInitiateKyc(
'STANDARD',
getRegionIntent(getCountryFromPath(country)?.region ?? 'rest-of-the-world'),
undefined,
gate.kind === 'needs-enrollment' || undefined
)
Expand Down
Loading
Loading