feat(frontend): storage consent that stops idle WalletConnect pulse and Coinbase CCA #165

Open
opened 2026-09-05 16:03:38 +00:00 by PlasticDigits · 3 comments

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 in code/cl8y-dex-terraclassic, code/CL8Y-web, code/cl8y-ecosystem-legal, and code/voting using 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):

  1. Notice + Refuse / Accept / Manage — first-party UI. Disclose first-party localStorage and named third-party SDKs. Do not ship a dummy “we use cookies” bar that does nothing.
  2. Idle fail-closed — before a choice, treat optional third-party the same as Refuse. Do not init WalletConnect core, Reown AppKit, Coinbase Wallet SDK / CCA, WC heartbeat/pulse, Google Fonts, or telegram-web-app.js on a cold visit.
  3. Refuse actually down-scopes — no pulse.walletconnect.org, no cca-lite.coinbase.com, no AppKit/Web3Modal telemetry, no wagmi reconnectOnMount for WC/Coinbase, no DEX/bridge WC auto-reconnect. Injected / extension wallets still work.
  4. Just-in-time (JIT) unlock — if the user refused (or has not chosen) and then picks WalletConnect or Coinbase, show a scoped prompt for that connector only. Do not silently flip global optional-on.
  5. Same consent record shape across the four product shells plus voting, so the disclosure text and tests do not drift.

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 surface

packages/frontend/src/main.tsx statically imports packages/frontend/src/lib/wagmi.ts and wraps the tree in WagmiProvider. createConfig runs at module scope with:

  • walletConnect({ projectId, showQrModal: true, … }) when VITE_WC_PROJECT_ID is set (required in production .env.example)
  • coinbaseWallet() always, with no preference.telemetry: false
  • multiInjectedProviderDiscovery: true
  • wagmi Hydrate default reconnectOnMount: true (not overridden)

There is no first-party createAppKit. Reown AppKit arrives transitively via @walletconnect/ethereum-provider (Vite manualChunks already groups wagmi / @walletconnect / @reown into wallet-evm). Coinbase Client Analytics (CCA) arrives from @coinbase/wallet-sdk (loadTelemetryScript / initCCA unless telemetry is disabled). WalletConnect heartbeat emits heartbeat_pulse (~5s) after WC core init — that is the “pulse” to gate, not Tailwind animate-pulse.

Terra WC is separate (@goblinhunt/cosmes): installWalletConnectPairingHook() runs in init() on every boot. Zustand persist cl8y-bridge-wallet-storage hydrates wallet type/address and attemptReconnect can restore a WalletConnect session after refresh.

First-party keys already in play: cl8y-theme (read in index.html before 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.

BridgeTermsGate mounts 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.html CSP allows broad connect-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 load

No wagmi/AppKit/Coinbase connector. Terra WC via cosmes + @walletconnect/legacy-client. frontend-dapp/viteCsp.ts production connect-src explicitly allows https://pulse.walletconnect.org plus relay / verify / explorer.

frontend-dapp/src/hooks/useWallet.ts reads cl8y_wallet_connection on window load and auto-reconnects, including WalletType.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-ack and ConnectedTermsGate are accept-to-use. LegalFooterNotice.tsx has 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/WC

index.html preconnects and stylesheets Google Fonts (fonts.googleapis.com / fonts.gstatic.com) on every paint. src/providers/AppProviders.tsx always mounts WagmiProvider with injected + optional walletConnect({ showQrModal: true }). There is no Connect UI in current features, but reconnectOnMount can still revive a prior WC session. Transitive @reown/appkit* and @coinbase/wallet-sdk sit in the lockfile; Coinbase connector is not registered (unlike bridge). Footer has no storage notice. #12 only stops /cookies from painting the homepage.

web/index.html loads https://telegram.org/js/telegram-web-app.js in <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 + clickwrap

frontend/package.json depends on wagmi, 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

  1. Idle third-party processing with no notice. Bridge boots wagmi/Coinbase/WC config on every visit. DEX CSP already admits 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.
  2. Clickwrap ≠ storage consent. TermsGate and the DEX risk modal block product use until accept. They do not disable CCA, WC pulse, AppKit, fonts, or Telegram JS. A user who never connects still sits behind those SDKs on bridge/marketing/legal.
  3. Refuse must be real. A banner that only writes cookie_ok=1 while reconnectOnMount still 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.
  4. One schema. Splitting bridge vs DEX vs marketing vs legal produces one host that gates pulse and another that still auto-reconnects WC. Bundle the record shape, the idle rule, and the named SDK list.

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

  1. Fail-closed until choice. Optional third-party network I/O is forbidden until Accept or a JIT unlock for that connector. Pre-choice === Refuse for optional processing.
  2. Necessary first-party storage may run. Disclose it. Allowed without Accept: theme (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.
  3. Injected wallets stay usable on Refuse. Keplr / Station / MetaMask / EIP-6963 discovery is local. Do not hide Connect. Do not require optional-Accept to use an extension.
  4. WC / Coinbase / AppKit / CCA / pulse are optional (or JIT). coinbaseWallet() must not run getProvider() / telemetry on idle. Pass preference: { telemetry: false } unless optional telemetry is accepted. Do not call createAppKit. Do not init @walletconnect/ethereum-provider / SignClient / heartbeat until Connect (and consent/JIT).
  5. No CMP vendor SDK (no OneTrust / Cookiebot / etc.). First-party banner + helper. No new npm product dependency for the notice itself.
  6. Do not author a Privacy Notice in these shells. Short operational copy in the banner is required (what we store, which third parties). Link the canonical Privacy Notice URL only if that HTTPS document already exists; otherwise no invented policy prose and no T&Cs paste. #12 remains the routing issue for /privacy.
  7. Do not change TermsGate / clickwrap semantics (#134, DEX #517). Storage consent is a separate gate and must not be stored as T&Cs acceptance.
  8. Consent record is first-party, versioned, non-secret. JSON in localStorage (and only localStorage / memory — no third-party cookie). Schema below. Bump v when disclosure copy materially changes so returning users see the notice again (same idea as RISK_ACK_VERSION). Never put wallet addresses, WC URIs, or signatures in the consent blob.
  9. XSS / injection. Banner copy is static or from a typed map. No dangerouslySetInnerHTML. No eval of query ?consent=. javascript: / data: in any “learn more” href is forbidden; URL + first-party origin only.
  10. Worktree isolation. Bridge PRs cannot patch DEX / CL8Y-web / legal / voting. Copy the schema and idle rule into those repos in separate PRs.
  11. DEX chrome invariant (C653). Notice is a single non-nested overlay/banner. No extra shell-panel* / card-glass wrapping Swap.
  12. Playwright. Do not reuse VITE_PLAYWRIGHT_E2E as 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 in beforeEach or use injected wallets under Refuse. Playwright workers: 5.
  13. #1202 product events no-op unless optional processing is accepted. Do not invent a third analytics vendor.
  14. CSP. After Refuse, clients must not call pulse.walletconnect.org or cca-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.
  15. No open redirect from “Learn more” / Manage. No consent value taken from query, hash, postMessage from untrusted origins, or document.referrer.

Key: cl8y-storage-consent (same name on every host).

type StorageConsentV1 = {
  v: 1
  optionalThirdParty: boolean // false = refuse / pre-choice
  decided: boolean            // false = show notice; treat optional as refused
  jitWalletConnect?: boolean  // session or persisted scoped unlock
  jitCoinbase?: boolean
}
  • Missing / invalid JSON / wrong v → { decided: false, optionalThirdParty: false }.
  • Refuse writes { v: 1, decided: true, optionalThirdParty: false } and clears jit* unless the user later JIT-unlocks.
  • Accept writes { v: 1, decided: true, optionalThirdParty: true }.
  • Manage may set optionalThirdParty without 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.
  • JIT sets only jitWalletConnect or jitCoinbase after an explicit second prompt; does not set optionalThirdParty: true.

Relevant files

Path Role
packages/frontend/src/lib/wagmi.ts Eager walletConnect + coinbaseWallet() — must become consent-gated / lazy
packages/frontend/src/main.tsx WagmiProvider + installWalletConnectPairingHook on boot
packages/frontend/index.html Theme localStorage read; CSP
packages/frontend/vite.config.ts wallet-evm chunk (@walletconnect / @reown)
packages/frontend/src/stores/wallet.ts cl8y-bridge-wallet-storage + WC reconnect
packages/frontend/src/stores/solanaWallet.ts Solana persist
packages/frontend/src/components/Layout.tsx Theme write; wallet modal mount
packages/frontend/src/components/ConnectWallet.tsx + wallet/EvmWalletModal.tsx JIT when user picks WC / Coinbase
packages/frontend/src/components/transfer/BridgeTermsGate.tsx Do not reuse as CMP
Sibling DEX frontend-dapp/src/hooks/useWallet.ts cl8y_wallet_connection auto-reconnect
Sibling DEX frontend-dapp/viteCsp.ts pulse.walletconnect.org
Sibling DEX frontend-dapp/src/utils/riskAcknowledgement.ts Necessary ack; not CMP
Sibling DEX frontend-dapp/src/components/legal/LegalFooterNotice.tsx Optional link to Manage / notice
Sibling code/CL8Y-web index.html + src/providers/AppProviders.tsx Google Fonts; unused wagmi/WC
Sibling code/cl8y-ecosystem-legal web/index.html Idle Telegram WebApp JS
Sibling code/voting frontend/ wagmi + WC + clickwrap
  1. Pure consent helper (Vitest-first) in each app, same schema: readStorageConsent(), writeStorageConsent(), allowsOptionalThirdParty(), allowsWalletConnectInfra(), allowsCoinbaseSdk(). Never throw on QuotaExceededError / private mode — treat as Refuse.

  2. Banner UI (first visit and when decided === false): short copy naming first-party localStorage, 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" or region + aria-label. Do not block reading the page chrome; do block optional SDK init until chosen.

  3. Lazy wagmi on bridge (and voting / CL8Y-web):

    • Do not import { config } from './lib/wagmi' at the top of main.tsx in a way that constructs WC/Coinbase connectors before consent.
    • Split: createInjectedConfig() always (EIP-6963 / mock in DEV_MODE) vs createWalletConnectConnector() / createCoinbaseConnector() after Allow or JIT.
    • WagmiProvider reconnectOnMount={allowsOptionalThirdParty() \|\| jit}.
    • Coinbase: coinbaseWallet({ preference: { telemetry: allowsOptionalThirdParty() } }) — telemetry stays false on JIT-only Coinbase unless optional was accepted.
    • Prefer dynamic import() so the wallet-evm / @reown chunk does not download on a refusing cold visit.
  4. DEX: Gate attemptAutoReconnect when saved walletType === WALLETCONNECT behind allowsWalletConnectInfra(). 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 to allowsOptionalThirdParty().

  5. 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 mount WagmiProvider until a connect surface exists; that is the smallest idle-load fix.

  6. Legal: Load telegram-web-app.js only when window.Telegram is needed (Telegram WebApp user-agent / initData present). Ordinary browser visits to the T&Cs portal must not fetch telegram.org. Keep WC SignClient lazy on the sign path; add the notice on home + sign shells; JIT if someone starts WC sign after Refuse.

  7. Footer / settings: Persistent Storage preferences control that reopens Manage (not only first-visit). Changing Refuse later must disconnect WC/Coinbase, localStorage.removeItem on wc@2:* / cbwsdk.store / wagmi.* wallet keys (not theme, not consent record), and set reconnectOnMount false.

Acceptance criteria

  • AC1. Cold visit (cleared site data) on bridge: no requests to pulse.walletconnect.org, cca-lite.coinbase.com, *.walletconnect.com relay, api.web3modal.com / api.web3modal.org, or Coinbase telemetry until Accept or JIT. Banner is visible (decided === false).
  • AC2. Refuse dismisses the banner, writes the schema with optionalThirdParty: false, and AC1 network still holds on reload.
  • AC3. Accept allows current WC/Coinbase/reconnect behavior; banner stays dismissed across reload until v bumps.
  • AC4. Injected / extension Connect still works after Refuse (Terra Keplr/Station; EVM injected). No forced optional-Accept.
  • AC5. After Refuse, choosing WalletConnect or Coinbase in the connect modal shows a JIT confirm. Cancel → no provider init. Confirm → that connector only (jitWalletConnect / jitCoinbase), not a silent global Accept.
  • AC6. DEX: saved WC session does not auto-reconnect when optionalThirdParty is false and jitWalletConnect is false. Extension auto-reconnect may remain.
  • AC7. CL8Y-web: no Google Fonts request on Refuse / pre-choice if still using Google; or fonts are self-hosted and documented as necessary with no Google request. Wagmi/WC does not reconnect on idle when refused.
  • AC8. Legal: ordinary browser load does not fetch telegram.org/js/telegram-web-app.js. Telegram Mini App path still can.
  • AC9. Invalid / hostile consent JSON is ignored (fail-closed). Query ?consent=accept does nothing.
  • AC10. TermsGate / risk-ack / clickwrap unchanged. Consent blob is not treated as T&Cs acceptance. No Privacy Notice novel in these repos.
  • AC11. Voting SPA follows AC1–AC5.
  • AC12. Docs/skills in each touched repo: idle fail-closed, schema, JIT, and “Refuse is not a connect-wallet block”.

Test plan (functional paths)

# Path Expect
T1 Bridge cold / Banner shown; no WC pulse / CCA / AppKit / relay
T2 Bridge Refuse + reload Banner gone; still no pulse/CCA; theme still applies
T3 Bridge Accept + reload Banner gone; WC/Coinbase may init as today when user connects or reconnects
T4 Bridge Refuse + injected EVM/Terra connect Connects; still no CCA/pulse
T5 Bridge Refuse + pick WalletConnect + JIT cancel No ethereum-provider init / no relay
T6 Bridge Refuse + pick WalletConnect + JIT confirm WC works this session; optionalThirdParty remains false
T7 Bridge Refuse + pick Coinbase + JIT confirm Coinbase SDK may load; CCA telemetry off unless Accept
T8 Bridge Manage → Refuse after prior Accept WC/Coinbase disconnect; sdk keys cleared; pulse/CCA stop
T9 DEX cold / no saved wallet Banner; no pulse
T10 DEX saved WC + Refuse No auto-reconnect; user can still open modal
T11 DEX saved extension + Refuse Auto-reconnect OK
T12 DEX Accept then #1202-style event hook Events may fire; under Refuse they no-op (stub if #1202 unlanded)
T13 CL8Y-web cold / No Google Fonts or self-hosted; no WC reconnect
T14 Legal cold / in ordinary Chrome No telegram-web-app.js
T15 Legal Telegram WebApp Script may load; T&Cs sign still works
T16 Voting cold / Same as T1/T2/T4
T17 localStorage full / throws Helper treats as Refuse; no crash
T18 Risk modal + storage banner both first visit (DEX) Both usable; neither swallowed; no nested chrome

Vitest: 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=1 OK for network-gate tests.

Test plan (attack / hack / abuse)

# Vector Expect
A1 localStorage['cl8y-storage-consent'] = '<script>' Parse fail → Refuse; no HTML inject
A2 ?consent=1 / #accept / postMessage('accept') Ignored
A3 Forge optionalThirdParty: true without decided Invalid → fail-closed (require decided === true && v === 1)
A4 Prototype-polluted JSON (__proto__) Reject non-plain / unexpected keys
A5 Banner “Learn more” javascript: / https://evil Impossible; constant first-party URL or in-panel Manage
A6 Refuse then reconnectOnMount still true Forbidden; test wagmi props
A7 Coinbase JIT with telemetry left default-on preference.telemetry === false unless Accept
A8 Clearing only the banner DOM (devtools) without writing Refuse SDKs still not inited because helper is source of truth, not CSS
A9 Double-click Accept/Refuse One write; no banner respawn loop
A10 iframe embed of the SPA Existing frame-ancestors / clickjacking work unchanged; do not weaken
A11 Consent key overwrite to enable pulse without UI User can write their own LS; we do not treat that as an app bug. Do not read consent from cookies set by other origins.
A12 WC URI / address stored in consent blob Never
A13 CSP still allows pulse after Refuse while app “promises” not to call App must not call; DEX should drop unused pulse host from default CSP until allow
A14 Manage Refuse does not drop cbwsdk.store / wc@2: Must remove known third-party keys on downgrade
A15 Legal still loads Telegram JS via bundler after removing index.html tag Grep: no leftover telegram.org on the ordinary-browser path

Verification criteria

  1. Bridge: unit + RTL + Playwright network tests for T1–T8, A1–A7, A14. Preview cold load with DevTools Network filter pulse\|cca-lite\|web3modal\|walletconnect empty until Accept/JIT.
  2. DEX sibling PR: auto-reconnect table (extension vs WC) + CSP test that production connect-src does not include pulse.walletconnect.org until optional allow (or documents JIT-time). Risk-ack and clickwrap tests stay green.
  3. CL8Y-web sibling PR: index.html has no Google Fonts or fonts load only after Accept; AppProviders does not reconnect WC on Refuse.
  4. Legal sibling PR: web/index.html has no unconditional Telegram script; ordinary-browser Playwright request log has no telegram.org.
  5. Voting sibling PR: AC11.
  6. Manual: three browsers, cold profile: Refuse on bridge, connect Keplr, confirm no pulse/CCA; JIT WC once; Manage → Refuse clears session. Repeat DEX WC saved-session. Open marketing and legal in ordinary browser.

Out of scope

  • Writing the Privacy Notice document (legal-doc work in code/cl8y-ecosystem-legal; routing remains CL8Y-web #12).
  • Footer Privacy Notice links until that URL exists.
  • Vendor CMP / cookie walls that block all product UI including injected connect.
  • Changing T&Cs text, TermsGate, or treating storage consent as clickwrap.
  • Removing WalletConnect or Coinbase as connect options.
  • Indexer / contracts / operator.
  • Per-vendor granularity beyond optional bundle + WC/Coinbase JIT.
  • Other product hosts beyond the five named here (copy the schema later if needed).
## 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 in `code/cl8y-dex-terraclassic`, `code/CL8Y-web`, `code/cl8y-ecosystem-legal`, and `code/voting` using 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): 1. **Notice + Refuse / Accept / Manage** — first-party UI. Disclose first-party `localStorage` and named third-party SDKs. Do not ship a dummy “we use cookies” bar that does nothing. 2. **Idle fail-closed** — before a choice, treat optional third-party the same as Refuse. Do not init WalletConnect core, Reown AppKit, Coinbase Wallet SDK / CCA, WC heartbeat/pulse, Google Fonts, or `telegram-web-app.js` on a cold visit. 3. **Refuse actually down-scopes** — no `pulse.walletconnect.org`, no `cca-lite.coinbase.com`, no AppKit/Web3Modal telemetry, no wagmi `reconnectOnMount` for WC/Coinbase, no DEX/bridge WC auto-reconnect. Injected / extension wallets still work. 4. **Just-in-time (JIT) unlock** — if the user refused (or has not chosen) and then picks WalletConnect or Coinbase, show a scoped prompt for **that connector only**. Do not silently flip global optional-on. 5. **Same consent record shape** across the four product shells plus voting, so the disclosure text and tests do not drift. Not a duplicate of [CL8Y-web #12](https://git.cl8y.com/code/CL8Y-web/issues/12) (SPA fallthrough on `/privacy` / `/cookies` / `/opt-out`; cookie banners explicitly out of scope), [DEX #1202](https://git.cl8y.com/code/cl8y-dex-terraclassic/issues/1202) (first-party product events; must **respect** this gate), [bridge #134](https://git.cl8y.com/code/cl8y-bridge-monorepo/issues/134) / [DEX #517](https://git.cl8y.com/code/cl8y-dex-terraclassic/issues/517) (TermsGate clickwrap), or legal [#2](https://git.cl8y.com/code/cl8y-ecosystem-legal/issues/2) (T&Cs checkbox). ## Current codebase ### Bridge (`code/cl8y-bridge-monorepo`) — worst idle third-party surface [`packages/frontend/src/main.tsx`](packages/frontend/src/main.tsx) statically imports [`packages/frontend/src/lib/wagmi.ts`](packages/frontend/src/lib/wagmi.ts) and wraps the tree in `WagmiProvider`. `createConfig` runs at **module scope** with: - `walletConnect({ projectId, showQrModal: true, … })` when `VITE_WC_PROJECT_ID` is set (required in production `.env.example`) - `coinbaseWallet()` **always**, with **no** `preference.telemetry: false` - `multiInjectedProviderDiscovery: true` - wagmi `Hydrate` default **`reconnectOnMount: true`** (not overridden) There is **no** first-party `createAppKit`. Reown AppKit arrives **transitively** via `@walletconnect/ethereum-provider` (Vite `manualChunks` already groups `wagmi` / `@walletconnect` / `@reown` into `wallet-evm`). Coinbase Client Analytics (CCA) arrives from `@coinbase/wallet-sdk` (`loadTelemetryScript` / `initCCA` unless telemetry is disabled). WalletConnect heartbeat emits `heartbeat_pulse` (~5s) after WC core init — that is the “pulse” to gate, not Tailwind `animate-pulse`. Terra WC is separate (`@goblinhunt/cosmes`): [`installWalletConnectPairingHook()`](packages/frontend/src/services/terra/walletConnectPairingHook.ts) runs in `init()` on every boot. Zustand persist [`cl8y-bridge-wallet-storage`](packages/frontend/src/stores/wallet.ts) hydrates wallet type/address and [`attemptReconnect`](packages/frontend/src/stores/wallet.ts) can restore a **WalletConnect** session after refresh. First-party keys already in play: `cl8y-theme` (read in `index.html` before 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`. [`BridgeTermsGate`](packages/frontend/src/components/transfer/BridgeTermsGate.tsx) mounts 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.html` CSP allows broad `connect-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 load No wagmi/AppKit/Coinbase connector. Terra WC via cosmes + `@walletconnect/legacy-client`. [`frontend-dapp/viteCsp.ts`](https://git.cl8y.com/code/cl8y-dex-terraclassic/src/branch/main/frontend-dapp/viteCsp.ts) production `connect-src` **explicitly allows** `https://pulse.walletconnect.org` plus relay / verify / explorer. [`frontend-dapp/src/hooks/useWallet.ts`](https://git.cl8y.com/code/cl8y-dex-terraclassic/src/branch/main/frontend-dapp/src/hooks/useWallet.ts) reads `cl8y_wallet_connection` on `window` `load` and **auto-reconnects**, including `WalletType.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-ack`](https://git.cl8y.com/code/cl8y-dex-terraclassic/src/branch/main/frontend-dapp/src/utils/riskAcknowledgement.ts) and [`ConnectedTermsGate`](https://git.cl8y.com/code/cl8y-dex-terraclassic/src/branch/main/frontend-dapp/src/components/legal/ConnectedTermsGate.tsx) are accept-to-use. [`LegalFooterNotice.tsx`](https://git.cl8y.com/code/cl8y-dex-terraclassic/src/branch/main/frontend-dapp/src/components/legal/LegalFooterNotice.tsx) has no Privacy / Cookies / Opt-out link. Fonts are self-hosted (`@fontsource/*`). [#1202](https://git.cl8y.com/code/cl8y-dex-terraclassic/issues/1202) will add first-party product events; those must no-op when optional processing is refused. ### Marketing (`code/CL8Y-web`) — Google Fonts + unused wagmi/WC [`index.html`](https://git.cl8y.com/code/CL8Y-web/src/branch/main/index.html) preconnects and stylesheets **Google Fonts** (`fonts.googleapis.com` / `fonts.gstatic.com`) on every paint. [`src/providers/AppProviders.tsx`](https://git.cl8y.com/code/CL8Y-web/src/branch/main/src/providers/AppProviders.tsx) always mounts `WagmiProvider` with `injected` + optional `walletConnect({ showQrModal: true })`. There is no Connect UI in current features, but `reconnectOnMount` can still revive a **prior** WC session. Transitive `@reown/appkit*` and `@coinbase/wallet-sdk` sit in the lockfile; Coinbase connector is **not** registered (unlike bridge). Footer has no storage notice. [#12](https://git.cl8y.com/code/CL8Y-web/issues/12) only stops `/cookies` from painting the homepage. ### Legal portal (`code/cl8y-ecosystem-legal`) — Telegram script on every page [`web/index.html`](https://git.cl8y.com/code/cl8y-ecosystem-legal/src/branch/main/web/index.html) loads `https://telegram.org/js/telegram-web-app.js` in `<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 + clickwrap [`frontend/package.json`](https://git.cl8y.com/code/voting/src/branch/main/frontend/package.json) depends on `wagmi`, `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 1. **Idle third-party processing with no notice.** Bridge boots wagmi/Coinbase/WC config on every visit. DEX CSP already admits `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. 2. **Clickwrap ≠ storage consent.** TermsGate and the DEX risk modal block product use until accept. They do not disable CCA, WC pulse, AppKit, fonts, or Telegram JS. A user who never connects still sits behind those SDKs on bridge/marketing/legal. 3. **Refuse must be real.** A banner that only writes `cookie_ok=1` while `reconnectOnMount` still 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. 4. **One schema.** Splitting bridge vs DEX vs marketing vs legal produces one host that gates pulse and another that still auto-reconnects WC. Bundle the record shape, the idle rule, and the named SDK list. 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 1. **Fail-closed until choice.** Optional third-party network I/O is forbidden until Accept **or** a JIT unlock for that connector. Pre-choice === Refuse for optional processing. 2. **Necessary first-party storage may run.** Disclose it. Allowed without Accept: theme (`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. 3. **Injected wallets stay usable on Refuse.** Keplr / Station / MetaMask / EIP-6963 discovery is local. Do not hide Connect. Do not require optional-Accept to use an extension. 4. **WC / Coinbase / AppKit / CCA / pulse are optional (or JIT).** `coinbaseWallet()` must not run `getProvider()` / telemetry on idle. Pass `preference: { telemetry: false }` unless optional telemetry is accepted. Do not call `createAppKit`. Do not init `@walletconnect/ethereum-provider` / SignClient / heartbeat until Connect (and consent/JIT). 5. **No CMP vendor SDK** (no OneTrust / Cookiebot / etc.). First-party banner + helper. No new npm product dependency for the notice itself. 6. **Do not author a Privacy Notice** in these shells. Short operational copy in the banner is required (what we store, which third parties). Link the canonical Privacy Notice URL only if that HTTPS document already exists; otherwise no invented policy prose and no T&Cs paste. [#12](https://git.cl8y.com/code/CL8Y-web/issues/12) remains the routing issue for `/privacy`. 7. **Do not change TermsGate / clickwrap semantics** ([#134](https://git.cl8y.com/code/cl8y-bridge-monorepo/issues/134), DEX #517). Storage consent is a separate gate and must not be stored as T&Cs acceptance. 8. **Consent record is first-party, versioned, non-secret.** JSON in `localStorage` (and only `localStorage` / memory — no third-party cookie). Schema below. Bump `v` when disclosure copy materially changes so returning users see the notice again (same idea as `RISK_ACK_VERSION`). Never put wallet addresses, WC URIs, or signatures in the consent blob. 9. **XSS / injection.** Banner copy is static or from a typed map. No `dangerouslySetInnerHTML`. No `eval` of query `?consent=`. `javascript:` / `data:` in any “learn more” href is forbidden; `URL` + first-party origin only. 10. **Worktree isolation.** Bridge PRs cannot patch DEX / CL8Y-web / legal / voting. Copy the schema and idle rule into those repos in separate PRs. 11. **DEX chrome invariant (C653).** Notice is a single non-nested overlay/banner. No extra `shell-panel*` / `card-glass` wrapping Swap. 12. **Playwright.** Do not reuse `VITE_PLAYWRIGHT_E2E` as 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 in `beforeEach` or use injected wallets under Refuse. Playwright workers: 5. 13. **[#1202](https://git.cl8y.com/code/cl8y-dex-terraclassic/issues/1202) product events** no-op unless optional processing is accepted. Do not invent a third analytics vendor. 14. **CSP.** After Refuse, clients must not call `pulse.walletconnect.org` or `cca-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. 15. **No open redirect** from “Learn more” / Manage. No consent value taken from query, hash, `postMessage` from untrusted origins, or `document.referrer`. ## Consent record (shared schema) Key: `cl8y-storage-consent` (same name on every host). ```ts type StorageConsentV1 = { v: 1 optionalThirdParty: boolean // false = refuse / pre-choice decided: boolean // false = show notice; treat optional as refused jitWalletConnect?: boolean // session or persisted scoped unlock jitCoinbase?: boolean } ``` - Missing / invalid JSON / wrong `v` → `{ decided: false, optionalThirdParty: false }`. - Refuse writes `{ v: 1, decided: true, optionalThirdParty: false }` and clears `jit*` unless the user later JIT-unlocks. - Accept writes `{ v: 1, decided: true, optionalThirdParty: true }`. - Manage may set `optionalThirdParty` without 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. - JIT sets only `jitWalletConnect` or `jitCoinbase` after an explicit second prompt; does not set `optionalThirdParty: true`. ## Relevant files | Path | Role | |------|------| | `packages/frontend/src/lib/wagmi.ts` | Eager `walletConnect` + `coinbaseWallet()` — must become consent-gated / lazy | | `packages/frontend/src/main.tsx` | WagmiProvider + `installWalletConnectPairingHook` on boot | | `packages/frontend/index.html` | Theme `localStorage` read; CSP | | `packages/frontend/vite.config.ts` | `wallet-evm` chunk (`@walletconnect` / `@reown`) | | `packages/frontend/src/stores/wallet.ts` | `cl8y-bridge-wallet-storage` + WC reconnect | | `packages/frontend/src/stores/solanaWallet.ts` | Solana persist | | `packages/frontend/src/components/Layout.tsx` | Theme write; wallet modal mount | | `packages/frontend/src/components/ConnectWallet.tsx` + `wallet/EvmWalletModal.tsx` | JIT when user picks WC / Coinbase | | `packages/frontend/src/components/transfer/BridgeTermsGate.tsx` | Do not reuse as CMP | | Sibling DEX `frontend-dapp/src/hooks/useWallet.ts` | `cl8y_wallet_connection` auto-reconnect | | Sibling DEX `frontend-dapp/viteCsp.ts` | `pulse.walletconnect.org` | | Sibling DEX `frontend-dapp/src/utils/riskAcknowledgement.ts` | Necessary ack; not CMP | | Sibling DEX `frontend-dapp/src/components/legal/LegalFooterNotice.tsx` | Optional link to Manage / notice | | Sibling `code/CL8Y-web` `index.html` + `src/providers/AppProviders.tsx` | Google Fonts; unused wagmi/WC | | Sibling `code/cl8y-ecosystem-legal` `web/index.html` | Idle Telegram WebApp JS | | Sibling `code/voting` `frontend/` | wagmi + WC + clickwrap | ## Recommended direction 1. **Pure consent helper** (Vitest-first) in each app, same schema: `readStorageConsent()`, `writeStorageConsent()`, `allowsOptionalThirdParty()`, `allowsWalletConnectInfra()`, `allowsCoinbaseSdk()`. Never throw on `QuotaExceededError` / private mode — treat as Refuse. 2. **Banner UI** (first visit and when `decided === false`): short copy naming first-party `localStorage`, 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"` or `region` + `aria-label`. Do not block reading the page chrome; **do** block optional SDK init until chosen. 3. **Lazy wagmi on bridge (and voting / CL8Y-web):** - Do not `import { config } from './lib/wagmi'` at the top of `main.tsx` in a way that constructs WC/Coinbase connectors before consent. - Split: `createInjectedConfig()` always (EIP-6963 / mock in DEV_MODE) vs `createWalletConnectConnector()` / `createCoinbaseConnector()` after Allow or JIT. - `WagmiProvider` `reconnectOnMount={allowsOptionalThirdParty() \|\| jit}`. - Coinbase: `coinbaseWallet({ preference: { telemetry: allowsOptionalThirdParty() } })` — telemetry stays **false** on JIT-only Coinbase unless optional was accepted. - Prefer dynamic `import()` so the `wallet-evm` / `@reown` chunk does not download on a refusing cold visit. 4. **DEX:** Gate `attemptAutoReconnect` when saved `walletType === WALLETCONNECT` behind `allowsWalletConnectInfra()`. 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 to `allowsOptionalThirdParty()`. 5. **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 mount `WagmiProvider` until a connect surface exists; that is the smallest idle-load fix. 6. **Legal:** Load `telegram-web-app.js` only when `window.Telegram` is needed (Telegram WebApp user-agent / `initData` present). Ordinary browser visits to the T&Cs portal must not fetch `telegram.org`. Keep WC SignClient lazy on the sign path; add the notice on home + sign shells; JIT if someone starts WC sign after Refuse. 7. **Footer / settings:** Persistent **Storage preferences** control that reopens Manage (not only first-visit). Changing Refuse later must disconnect WC/Coinbase, `localStorage.removeItem` on `wc@2:*` / `cbwsdk.store` / `wagmi.*` **wallet** keys (not theme, not consent record), and set `reconnectOnMount` false. ## Acceptance criteria - [ ] **AC1.** Cold visit (cleared site data) on bridge: no requests to `pulse.walletconnect.org`, `cca-lite.coinbase.com`, `*.walletconnect.com` relay, `api.web3modal.com` / `api.web3modal.org`, or Coinbase telemetry **until** Accept or JIT. Banner is visible (`decided === false`). - [ ] **AC2.** **Refuse** dismisses the banner, writes the schema with `optionalThirdParty: false`, and AC1 network still holds on reload. - [ ] **AC3.** **Accept** allows current WC/Coinbase/reconnect behavior; banner stays dismissed across reload until `v` bumps. - [ ] **AC4.** Injected / extension Connect still works after Refuse (Terra Keplr/Station; EVM injected). No forced optional-Accept. - [ ] **AC5.** After Refuse, choosing WalletConnect or Coinbase in the connect modal shows a **JIT** confirm. Cancel → no provider init. Confirm → that connector only (`jitWalletConnect` / `jitCoinbase`), not a silent global Accept. - [ ] **AC6.** DEX: saved WC session does **not** auto-reconnect when `optionalThirdParty` is false and `jitWalletConnect` is false. Extension auto-reconnect may remain. - [ ] **AC7.** CL8Y-web: no Google Fonts request on Refuse / pre-choice if still using Google; **or** fonts are self-hosted and documented as necessary with no Google request. Wagmi/WC does not reconnect on idle when refused. - [ ] **AC8.** Legal: ordinary browser load does not fetch `telegram.org/js/telegram-web-app.js`. Telegram Mini App path still can. - [ ] **AC9.** Invalid / hostile consent JSON is ignored (fail-closed). Query `?consent=accept` does nothing. - [ ] **AC10.** TermsGate / risk-ack / clickwrap unchanged. Consent blob is not treated as T&Cs acceptance. No Privacy Notice novel in these repos. - [ ] **AC11.** Voting SPA follows AC1–AC5. - [ ] **AC12.** Docs/skills in each touched repo: idle fail-closed, schema, JIT, and “Refuse is not a connect-wallet block”. ## Test plan (functional paths) | # | Path | Expect | |---|------|--------| | T1 | Bridge cold `/` | Banner shown; no WC pulse / CCA / AppKit / relay | | T2 | Bridge Refuse + reload | Banner gone; still no pulse/CCA; theme still applies | | T3 | Bridge Accept + reload | Banner gone; WC/Coinbase may init as today when user connects or reconnects | | T4 | Bridge Refuse + injected EVM/Terra connect | Connects; still no CCA/pulse | | T5 | Bridge Refuse + pick WalletConnect + JIT cancel | No ethereum-provider init / no relay | | T6 | Bridge Refuse + pick WalletConnect + JIT confirm | WC works this session; `optionalThirdParty` remains false | | T7 | Bridge Refuse + pick Coinbase + JIT confirm | Coinbase SDK may load; **CCA telemetry off** unless Accept | | T8 | Bridge Manage → Refuse after prior Accept | WC/Coinbase disconnect; sdk keys cleared; pulse/CCA stop | | T9 | DEX cold `/` no saved wallet | Banner; no pulse | | T10 | DEX saved WC + Refuse | No auto-reconnect; user can still open modal | | T11 | DEX saved extension + Refuse | Auto-reconnect OK | | T12 | DEX Accept then #1202-style event hook | Events may fire; under Refuse they no-op (stub if #1202 unlanded) | | T13 | CL8Y-web cold `/` | No Google Fonts **or** self-hosted; no WC reconnect | | T14 | Legal cold `/` in ordinary Chrome | No `telegram-web-app.js` | | T15 | Legal Telegram WebApp | Script may load; T&Cs sign still works | | T16 | Voting cold `/` | Same as T1/T2/T4 | | T17 | `localStorage` full / throws | Helper treats as Refuse; no crash | | T18 | Risk modal + storage banner both first visit (DEX) | Both usable; neither swallowed; no nested chrome | Vitest: 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=1` OK for network-gate tests. ## Test plan (attack / hack / abuse) | # | Vector | Expect | |---|--------|--------| | A1 | `localStorage['cl8y-storage-consent'] = '<script>'` | Parse fail → Refuse; no HTML inject | | A2 | `?consent=1` / `#accept` / `postMessage('accept')` | Ignored | | A3 | Forge `optionalThirdParty: true` without `decided` | Invalid → fail-closed (require `decided === true` && `v === 1`) | | A4 | Prototype-polluted JSON (`__proto__`) | Reject non-plain / unexpected keys | | A5 | Banner “Learn more” `javascript:` / `https://evil` | Impossible; constant first-party URL or in-panel Manage | | A6 | Refuse then `reconnectOnMount` still true | Forbidden; test wagmi props | | A7 | Coinbase JIT with telemetry left default-on | `preference.telemetry === false` unless Accept | | A8 | Clearing only the banner DOM (devtools) without writing Refuse | SDKs still not inited because helper is source of truth, not CSS | | A9 | Double-click Accept/Refuse | One write; no banner respawn loop | | A10 | iframe embed of the SPA | Existing frame-ancestors / clickjacking work unchanged; do not weaken | | A11 | Consent key overwrite to enable pulse without UI | User can write their own LS; we do not treat that as an app bug. Do **not** read consent from cookies set by other origins. | | A12 | WC URI / address stored in consent blob | Never | | A13 | CSP still allows pulse after Refuse while app “promises” not to call | App must not call; DEX should drop unused pulse host from default CSP until allow | | A14 | Manage Refuse does not drop `cbwsdk.store` / `wc@2:` | Must remove known third-party keys on downgrade | | A15 | Legal still loads Telegram JS via bundler after removing `index.html` tag | Grep: no leftover `telegram.org` on the ordinary-browser path | ## Verification criteria 1. **Bridge:** unit + RTL + Playwright network tests for T1–T8, A1–A7, A14. Preview cold load with DevTools Network filter `pulse\|cca-lite\|web3modal\|walletconnect` empty until Accept/JIT. 2. **DEX sibling PR:** auto-reconnect table (extension vs WC) + CSP test that production `connect-src` does not include `pulse.walletconnect.org` until optional allow (or documents JIT-time). Risk-ack and clickwrap tests stay green. 3. **CL8Y-web sibling PR:** `index.html` has no Google Fonts **or** fonts load only after Accept; `AppProviders` does not reconnect WC on Refuse. 4. **Legal sibling PR:** `web/index.html` has no unconditional Telegram script; ordinary-browser Playwright request log has no `telegram.org`. 5. **Voting sibling PR:** AC11. 6. **Manual:** three browsers, cold profile: Refuse on bridge, connect Keplr, confirm no pulse/CCA; JIT WC once; Manage → Refuse clears session. Repeat DEX WC saved-session. Open marketing and legal in ordinary browser. ## Out of scope - Writing the Privacy Notice document (legal-doc work in `code/cl8y-ecosystem-legal`; routing remains [CL8Y-web #12](https://git.cl8y.com/code/CL8Y-web/issues/12)). - Footer Privacy Notice links until that URL exists. - Vendor CMP / cookie walls that block **all** product UI including injected connect. - Changing T&Cs text, TermsGate, or treating storage consent as clickwrap. - Removing WalletConnect or Coinbase as connect options. - Indexer / contracts / operator. - Per-vendor granularity beyond optional bundle + WC/Coinbase JIT. - Other product hosts beyond the five named here (copy the schema later if needed).
Author
Owner

S1 opt-out is required (cl8y-pm inbox). Storage-consent work on this ticket should treat opt-out as required before DEX product events.

S1 opt-out is **required** (cl8y-pm inbox). Storage-consent work on this ticket should treat opt-out as required before DEX product events.
Author
Owner

cl8y-agent-control: queued implement job 727559db-25c3-4c2e-8ee0-68ce50e71ab4 (not executed; no Hetzner VM).

cl8y-agent-control: queued `implement` job `727559db-25c3-4c2e-8ee0-68ce50e71ab4` (not executed; no Hetzner VM).
Author
Owner

cl8y-agent-control: needs_human inbox card POST failed. Job stays parked.

cl8y-agent-control: needs_human inbox card POST failed. Job stays parked.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
code/cl8y-bridge-monorepo#165
No description provided.