feat(frontend): storage consent that stops idle WalletConnect pulse and Coinbase CCA #165
Labels
No labels
agent:implement
agent:ready
backend
bug
cannot-reproduce
confirmed
desktop
docs
documentation
duplicate
enhancement
feature
frontend
good first issue
help wanted
high-risk
in-review
invalid
mobile
needs-triage
P0-critical
P1-high
P2-medium
P3-low
qa
QA
question
ready
report
responsive
security
security-escalate
smart-contract
solana
tablet
test-pass
ux
wallet-issue
wallet:keplr
wallet:metamask
wallet:station
wallet:walletconnect
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
code/cl8y-bridge-monorepo#165
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Ship a real storage / cookie notice on the first-party SPAs, with a Refuse control that actually stops or down-scopes third-party processing. Today there is no banner, no category gate, and no opt-out. Legal clickwrap and the DEX risk modal are accept-to-proceed product gates, not storage consent.
This issue is the shared spec. The implementer worktree for this repo is
code/cl8y-bridge-monorepo(bridge SPA). Matching changes are required incode/cl8y-dex-terraclassic,code/CL8Y-web,code/cl8y-ecosystem-legal, andcode/votingusing the same consent record schema and the same idle-load fail-closed rule. Do not edit those trees from a bridge worktree.Bundle (do not split):
localStorageand named third-party SDKs. Do not ship a dummy “we use cookies” bar that does nothing.telegram-web-app.json a cold visit.pulse.walletconnect.org, nocca-lite.coinbase.com, no AppKit/Web3Modal telemetry, no wagmireconnectOnMountfor WC/Coinbase, no DEX/bridge WC auto-reconnect. Injected / extension wallets still work.Not a duplicate of CL8Y-web #12 (SPA fallthrough on
/privacy//cookies//opt-out; cookie banners explicitly out of scope), DEX #1202 (first-party product events; must respect this gate), bridge #134 / DEX #517 (TermsGate clickwrap), or legal #2 (T&Cs checkbox).Current codebase
Bridge (
code/cl8y-bridge-monorepo) — worst idle third-party surfacepackages/frontend/src/main.tsxstatically importspackages/frontend/src/lib/wagmi.tsand wraps the tree inWagmiProvider.createConfigruns at module scope with:walletConnect({ projectId, showQrModal: true, … })whenVITE_WC_PROJECT_IDis set (required in production.env.example)coinbaseWallet()always, with nopreference.telemetry: falsemultiInjectedProviderDiscovery: trueHydratedefaultreconnectOnMount: true(not overridden)There is no first-party
createAppKit. Reown AppKit arrives transitively via@walletconnect/ethereum-provider(VitemanualChunksalready groupswagmi/@walletconnect/@reownintowallet-evm). Coinbase Client Analytics (CCA) arrives from@coinbase/wallet-sdk(loadTelemetryScript/initCCAunless telemetry is disabled). WalletConnect heartbeat emitsheartbeat_pulse(~5s) after WC core init — that is the “pulse” to gate, not Tailwindanimate-pulse.Terra WC is separate (
@goblinhunt/cosmes):installWalletConnectPairingHook()runs ininit()on every boot. Zustand persistcl8y-bridge-wallet-storagehydrates wallet type/address andattemptReconnectcan restore a WalletConnect session after refresh.First-party keys already in play:
cl8y-theme(read inindex.htmlbefore React),cl8y-bridge-wallet-storage,cl8y-bridge-solana-wallet-storage,cl8y-bridge-transactions,cl8y-bridge-verifications,cl8y-token-info-cache. SDK keys after (re)connect:wagmi.*,wc@2:*,cosmes.wallet.*.wcSession,cbwsdk.store.BridgeTermsGatemounts only when an account exists and only around mutative transfer UI (INV-FE-CLICKWRAP-1). Refusing T&Cs does not stop wagmi/WC/Coinbase. There is no cookie banner.Bridge
index.htmlCSP allows broadconnect-src https: wss:and WalletConnect / Web3Modal image hosts. That is compatible with idle third-party calls today.DEX (
code/cl8y-dex-terraclassic) — WC pulse allowlisted; auto-reconnect on loadNo wagmi/AppKit/Coinbase connector. Terra WC via cosmes +
@walletconnect/legacy-client.frontend-dapp/viteCsp.tsproductionconnect-srcexplicitly allowshttps://pulse.walletconnect.orgplus relay / verify / explorer.frontend-dapp/src/hooks/useWallet.tsreadscl8y_wallet_connectiononwindowloadand auto-reconnects, includingWalletType.WALLETCONNECT. Cold visit with no saved wallet does not open relay; a returning WC user talks to WC (including pulse) without a new click and without a storage notice.cl8y-dex-risk-ackandConnectedTermsGateare accept-to-use.LegalFooterNotice.tsxhas no Privacy / Cookies / Opt-out link. Fonts are self-hosted (@fontsource/*). #1202 will add first-party product events; those must no-op when optional processing is refused.Marketing (
code/CL8Y-web) — Google Fonts + unused wagmi/WCindex.htmlpreconnects and stylesheets Google Fonts (fonts.googleapis.com/fonts.gstatic.com) on every paint.src/providers/AppProviders.tsxalways mountsWagmiProviderwithinjected+ optionalwalletConnect({ showQrModal: true }). There is no Connect UI in current features, butreconnectOnMountcan still revive a prior WC session. Transitive@reown/appkit*and@coinbase/wallet-sdksit in the lockfile; Coinbase connector is not registered (unlike bridge). Footer has no storage notice. #12 only stops/cookiesfrom painting the homepage.Legal portal (
code/cl8y-ecosystem-legal) — Telegram script on every pageweb/index.htmlloadshttps://telegram.org/js/telegram-web-app.jsin<head>on home and all sign routes, including ordinary browser visits that are not a Telegram Mini App. WalletConnect SignClient is already deferred until the user picks WC on a sign path (web/src/evm/walletConnect.ts,web/src/terra/walletConnect.ts) — keep that. T&Cs checkbox is not a storage CMP.Voting (
code/voting) — wagmi + WC + clickwrapfrontend/package.jsondepends onwagmi,viem,@walletconnect/legacy-client,@goblinhunt/cosmes,@plasticdigits/cl8y-clickwrap. Apply the same idle fail-closed + notice. Do not implement from the bridge worktree.Why the work is needed
pulse.walletconnect.org. Marketing hits Google Fonts before any UI. Legal hits Telegram’s script before any T&Cs action. None of that is disclosed, and none of it can be refused.cookie_ok=1whilereconnectOnMountstill constructs Coinbase/WC is worse than no banner. The control is the product: optional SDK init happens after Accept or a JIT unlock, not before.This is frontend consent + lazy SDK init. No contracts, no indexer, no T&Cs rewrite, no Privacy Notice authorship (link the notice when that document exists).
Constraints and guardrails
cl8y-theme/cl8y-dex-theme), this consent record, DEX risk-ack version blob, strictly functional UI prefs the user already set (sounds, expert mode) after they used those controls. Do not treat WC session restore or CCA as necessary.coinbaseWallet()must not rungetProvider()/ telemetry on idle. Passpreference: { telemetry: false }unless optional telemetry is accepted. Do not callcreateAppKit. Do not init@walletconnect/ethereum-provider/ SignClient / heartbeat until Connect (and consent/JIT)./privacy.localStorage(and onlylocalStorage/ memory — no third-party cookie). Schema below. Bumpvwhen disclosure copy materially changes so returning users see the notice again (same idea asRISK_ACK_VERSION). Never put wallet addresses, WC URIs, or signatures in the consent blob.dangerouslySetInnerHTML. Noevalof query?consent=.javascript:/data:in any “learn more” href is forbidden;URL+ first-party origin only.shell-panel*/card-glasswrapping Swap.VITE_PLAYWRIGHT_E2Eas a silent skip of this notice. Tests must either exercise Accept/Refuse/JIT or set an explicit test-only decision helper that writes the same schema. Default E2E that needs a wallet should Accept inbeforeEachor use injected wallets under Refuse. Playwright workers: 5.pulse.walletconnect.orgorcca-lite.coinbase.com. Prefer not listing those hosts in DEX production CSP until optional Accept (relay/verify may be added when the user JIT-unlocks WC). Do not widen CSP.postMessagefrom untrusted origins, ordocument.referrer.Consent record (shared schema)
Key:
cl8y-storage-consent(same name on every host).v→{ decided: false, optionalThirdParty: false }.{ v: 1, decided: true, optionalThirdParty: false }and clearsjit*unless the user later JIT-unlocks.{ v: 1, decided: true, optionalThirdParty: true }.optionalThirdPartywithout accepting WC if the user only wanted fonts — keep it one boolean for this issue (optional third-party as a bundle). Do not ship a five-vendor matrix in v1.jitWalletConnectorjitCoinbaseafter an explicit second prompt; does not setoptionalThirdParty: true.Relevant files
packages/frontend/src/lib/wagmi.tswalletConnect+coinbaseWallet()— must become consent-gated / lazypackages/frontend/src/main.tsxinstallWalletConnectPairingHookon bootpackages/frontend/index.htmllocalStorageread; CSPpackages/frontend/vite.config.tswallet-evmchunk (@walletconnect/@reown)packages/frontend/src/stores/wallet.tscl8y-bridge-wallet-storage+ WC reconnectpackages/frontend/src/stores/solanaWallet.tspackages/frontend/src/components/Layout.tsxpackages/frontend/src/components/ConnectWallet.tsx+wallet/EvmWalletModal.tsxpackages/frontend/src/components/transfer/BridgeTermsGate.tsxfrontend-dapp/src/hooks/useWallet.tscl8y_wallet_connectionauto-reconnectfrontend-dapp/viteCsp.tspulse.walletconnect.orgfrontend-dapp/src/utils/riskAcknowledgement.tsfrontend-dapp/src/components/legal/LegalFooterNotice.tsxcode/CL8Y-webindex.html+src/providers/AppProviders.tsxcode/cl8y-ecosystem-legalweb/index.htmlcode/votingfrontend/Recommended direction
Pure consent helper (Vitest-first) in each app, same schema:
readStorageConsent(),writeStorageConsent(),allowsOptionalThirdParty(),allowsWalletConnectInfra(),allowsCoinbaseSdk(). Never throw onQuotaExceededError/ private mode — treat as Refuse.Banner UI (first visit and when
decided === false): short copy naming first-partylocalStorage, WalletConnect/Reown (relay + pulse), Coinbase CCA, and (per host) Google Fonts or Telegram WebApp. Buttons: Accept optional, Refuse optional, Manage (same two-way choice + list).data-testid="storage-consent-banner". 44px targets, focus trap optional but focus ring required,role="dialog"orregion+aria-label. Do not block reading the page chrome; do block optional SDK init until chosen.Lazy wagmi on bridge (and voting / CL8Y-web):
import { config } from './lib/wagmi'at the top ofmain.tsxin a way that constructs WC/Coinbase connectors before consent.createInjectedConfig()always (EIP-6963 / mock in DEV_MODE) vscreateWalletConnectConnector()/createCoinbaseConnector()after Allow or JIT.WagmiProviderreconnectOnMount={allowsOptionalThirdParty() \|\| jit}.coinbaseWallet({ preference: { telemetry: allowsOptionalThirdParty() } })— telemetry stays false on JIT-only Coinbase unless optional was accepted.import()so thewallet-evm/@reownchunk does not download on a refusing cold visit.DEX: Gate
attemptAutoReconnectwhen savedwalletType === WALLETCONNECTbehindallowsWalletConnectInfra(). Pairing hook may stay installed as a no-network stub; must not open relay until Connect + allow/JIT. Theme + risk-ack unchanged. Wire #1202 emitters toallowsOptionalThirdParty().CL8Y-web: Self-host Inter / Space Grotesk / Roboto Mono (DEX/bridge already use
@fontsource) or load Google Fonts only after Accept. Strongly prefer self-host so Refuse still looks like the site and marketing does not need optional-Accept for typography. If wagmi is unused by UI, do not mountWagmiProvideruntil a connect surface exists; that is the smallest idle-load fix.Legal: Load
telegram-web-app.jsonly whenwindow.Telegramis needed (Telegram WebApp user-agent /initDatapresent). Ordinary browser visits to the T&Cs portal must not fetchtelegram.org. Keep WC SignClient lazy on the sign path; add the notice on home + sign shells; JIT if someone starts WC sign after Refuse.Footer / settings: Persistent Storage preferences control that reopens Manage (not only first-visit). Changing Refuse later must disconnect WC/Coinbase,
localStorage.removeItemonwc@2:*/cbwsdk.store/wagmi.*wallet keys (not theme, not consent record), and setreconnectOnMountfalse.Acceptance criteria
pulse.walletconnect.org,cca-lite.coinbase.com,*.walletconnect.comrelay,api.web3modal.com/api.web3modal.org, or Coinbase telemetry until Accept or JIT. Banner is visible (decided === false).optionalThirdParty: false, and AC1 network still holds on reload.vbumps.jitWalletConnect/jitCoinbase), not a silent global Accept.optionalThirdPartyis false andjitWalletConnectis false. Extension auto-reconnect may remain.telegram.org/js/telegram-web-app.js. Telegram Mini App path still can.?consent=acceptdoes nothing.Test plan (functional paths)
/optionalThirdPartyremains false/no saved wallet//in ordinary Chrometelegram-web-app.js/localStoragefull / throwsVitest: schema parse/migrate, fail-closed defaults, JIT flags, wagmi factory does not register WC/Coinbase when refused. RTL: banner buttons, JIT modal. Playwright (5 workers): network idle assertions on T1/T2/T9/T13/T14 (
page.on('request')allowlist).PLAYWRIGHT_SKIP_CHAIN=1OK for network-gate tests.Test plan (attack / hack / abuse)
localStorage['cl8y-storage-consent'] = '<script>'?consent=1/#accept/postMessage('accept')optionalThirdParty: truewithoutdecideddecided === true&&v === 1)__proto__)javascript:/https://evilreconnectOnMountstill truepreference.telemetry === falseunless Acceptcbwsdk.store/wc@2:index.htmltagtelegram.orgon the ordinary-browser pathVerification criteria
pulse\|cca-lite\|web3modal\|walletconnectempty until Accept/JIT.connect-srcdoes not includepulse.walletconnect.orguntil optional allow (or documents JIT-time). Risk-ack and clickwrap tests stay green.index.htmlhas no Google Fonts or fonts load only after Accept;AppProvidersdoes not reconnect WC on Refuse.web/index.htmlhas no unconditional Telegram script; ordinary-browser Playwright request log has notelegram.org.Out of scope
code/cl8y-ecosystem-legal; routing remains CL8Y-web #12).S1 opt-out is required (cl8y-pm inbox). Storage-consent work on this ticket should treat opt-out as required before DEX product events.
cl8y-agent-control: queued
implementjob727559db-25c3-4c2e-8ee0-68ce50e71ab4(not executed; no Hetzner VM).cl8y-agent-control: needs_human inbox card POST failed. Job stays parked.